зеркало из https://github.com/Azure/ARO-RP.git
Merge branch 'master' into juliens/osx-dev
This commit is contained in:
Коммит
1410903c7f
|
@ -16,3 +16,7 @@ __pycache__
|
|||
/python/az/aro/dist
|
||||
/secrets
|
||||
/mdm_statsd.socket
|
||||
/uts.txt
|
||||
/cover.out
|
||||
/coverage.*
|
||||
/report.xml
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
# Azure DevOps Pipeline running CI
|
||||
trigger:
|
||||
- master
|
||||
|
||||
variables:
|
||||
- template: vars.yml
|
||||
|
||||
jobs:
|
||||
- job: "Python_Unit_Tests"
|
||||
pool:
|
||||
vmImage: "ubuntu-latest"
|
||||
strategy:
|
||||
matrix:
|
||||
Python27:
|
||||
python.version: "2.7"
|
||||
Python35:
|
||||
python.version: "3.5"
|
||||
|
||||
steps:
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: "$(python.version)"
|
||||
- template: ./templates/template-setup-golang-env.yml
|
||||
parameters:
|
||||
gobin: ${{ variables.GOBIN }}
|
||||
gopath: ${{ variables.GOPATH }}
|
||||
goroot: ${{ variables.GOROOT }}
|
||||
modulePath: ${{ variables.modulePath }}
|
||||
- script: |
|
||||
set -x
|
||||
pip install virtualenv
|
||||
make test-python
|
||||
displayName: "🧪Run Python Unit Tests : $(python.version)"
|
||||
workingDirectory: "${{ variables.modulePath }}"
|
||||
|
||||
- job: "Golang_Unit_Tests"
|
||||
pool:
|
||||
vmImage: "ubuntu-latest"
|
||||
|
||||
steps:
|
||||
- template: ./templates/template-setup-golang-env.yml
|
||||
parameters:
|
||||
gobin: ${{ variables.GOBIN }}
|
||||
gopath: ${{ variables.GOPATH }}
|
||||
goroot: ${{ variables.GOROOT }}
|
||||
modulePath: ${{ variables.modulePath }}
|
||||
- script: |
|
||||
set -x
|
||||
make test-go
|
||||
[[ -z "$(git status -s)" ]]
|
||||
go run ./vendor/github.com/jstemmer/go-junit-report/go-junit-report.go < uts.txt > report.xml
|
||||
go run ./vendor/github.com/axw/gocov/gocov/*.go convert cover.out > coverage.json
|
||||
go run ./vendor/github.com/AlekSi/gocov-xml/gocov-xml.go < coverage.json > coverage.xml
|
||||
workingDirectory: "${{ variables.modulePath }}"
|
||||
displayName: "🧪Run Golang Unit Tests"
|
||||
- task: PublishTestResults@2
|
||||
displayName: "📊 Publish tests results"
|
||||
inputs:
|
||||
testResultsFiles: $(System.DefaultWorkingDirectory)/**/report.xml
|
||||
condition: succeededOrFailed()
|
||||
|
||||
- task: PublishCodeCoverageResults@1
|
||||
displayName: "📈 Publish code coverage"
|
||||
inputs:
|
||||
codeCoverageTool: Cobertura
|
||||
summaryFileLocation: "$(System.DefaultWorkingDirectory)/**/coverage.xml"
|
||||
reportDirectory: "$(System.DefaultWorkingDirectory)/**/coverage"
|
||||
failIfCoverageEmpty: false
|
||||
condition: succeededOrFailed()
|
|
@ -0,0 +1,18 @@
|
|||
parameters:
|
||||
gobin: ""
|
||||
gopath: ""
|
||||
goroot: ""
|
||||
modulePath: ""
|
||||
steps:
|
||||
- script: |
|
||||
mkdir -p '${{ parameters.gobin }}'
|
||||
mkdir -p '${{ parameters.gopath }}/pkg'
|
||||
mkdir -p '${{ parameters.modulePath }}'
|
||||
ls -a | grep -v ${{ parameters.gopath }} | xargs mv -t ${{ parameters.modulePath }}
|
||||
echo "##vso[task.prependpath]${{ parameters.gobin }}"
|
||||
echo "##vso[task.prependpath]${{ parameters.goroot }}/bin"
|
||||
sudo add-apt-repository ppa:kubuntu-ppa/backports
|
||||
sudo apt-get update
|
||||
sudo apt-get install libgpgme-dev gcc -y
|
||||
go version
|
||||
displayName: "⚙️ Set up the workspace"
|
|
@ -0,0 +1,5 @@
|
|||
variables:
|
||||
GOBIN: "$(GOPATH)/bin" # Go binaries path
|
||||
GOROOT: "/usr/local/go1.13" # Go installation path
|
||||
GOPATH: "$(system.defaultWorkingDirectory)/gopath" # Go workspace path
|
||||
modulePath: "$(GOPATH)/src/github.com/Azure/ARO-RP" # Path to the module's code
|
|
@ -9,6 +9,14 @@
|
|||
revision = "b4cdc8d6eb508c4e74df26094d1adb678c87f818"
|
||||
version = "v0.51.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:112418214e9ccdfdf047b8f1099e5b57ef44365460050dad349ac00b32405d10"
|
||||
name = "github.com/AlekSi/gocov-xml"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "3a14fb1c4737b3995174c5f4d6d08a348b9b4180"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:e072b8b6e9ea2e1a1a119dc668f3e121b27b2a82b98f79d2b95006f78798462e"
|
||||
name = "github.com/Azure/azure-sdk-for-go"
|
||||
|
@ -176,6 +184,19 @@
|
|||
revision = "cd610ee76770524d9f1135bfdc241e1b60dfd00b"
|
||||
version = "v1.28.6"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:853f8d8dae66462f0479a2aff520f02af6d9003d98bd476085f117d5b8daa20a"
|
||||
name = "github.com/axw/gocov"
|
||||
packages = [
|
||||
".",
|
||||
"gocov",
|
||||
"gocov/internal/testflag",
|
||||
"gocovutil",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "b6eca663ebb7e7ef9798914d19f53ba2c6f74c96"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:d6afaeed1502aa28e80a4ed0981d570ad91b2579193404256ce672ed0a609e0d"
|
||||
name = "github.com/beorn7/perks"
|
||||
|
@ -594,6 +615,18 @@
|
|||
revision = "acfec88f7a0d5140ace3dcdbee10184e3684a9e1"
|
||||
version = "v1.1.9"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:076c531484852c227471112d49465873aaad47e5ad6e1aec3a5b092a436117ef"
|
||||
name = "github.com/jstemmer/go-junit-report"
|
||||
packages = [
|
||||
".",
|
||||
"formatter",
|
||||
"parser",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "cc1f095d5cc5eca2844f5c5ea7bb37f6b9bf6cac"
|
||||
version = "v0.9.1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:400e113a367b511b9b09ca642ee11d9885485a93838526d697033af334a2fdde"
|
||||
|
@ -1231,7 +1264,7 @@
|
|||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:835727a7ae21040542e69c98e140240055d2d264013b8fd41108826192e1b603"
|
||||
digest = "1:0b579a0502f74c473b7ac8beb5aacaaa093594eab7df242ea9cd683b48028360"
|
||||
name = "golang.org/x/net"
|
||||
packages = [
|
||||
"context",
|
||||
|
@ -1333,10 +1366,11 @@
|
|||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:3e7fbc517dc9fec1648669f7f171a362f4e25acac328a42788419e713dee8419"
|
||||
digest = "1:8bb1d2f050249349dbaeda1ce06e22d9f185acda87c0a5c5a4c43da45d46ed69"
|
||||
name = "golang.org/x/tools"
|
||||
packages = [
|
||||
"cmd/goimports",
|
||||
"cover",
|
||||
"go/ast/astutil",
|
||||
"go/gcexportdata",
|
||||
"go/internal/gcimporter",
|
||||
|
@ -1747,6 +1781,7 @@
|
|||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
input-imports = [
|
||||
"github.com/AlekSi/gocov-xml",
|
||||
"github.com/Azure/azure-sdk-for-go/services/authorization/mgmt/2015-07-01/authorization",
|
||||
"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-03-01/compute",
|
||||
"github.com/Azure/azure-sdk-for-go/services/cosmos-db/mgmt/2015-04-08/documentdb",
|
||||
|
@ -1773,6 +1808,7 @@
|
|||
"github.com/BurntSushi/toml",
|
||||
"github.com/alvaroloes/enumer",
|
||||
"github.com/apparentlymart/go-cidr/cidr",
|
||||
"github.com/axw/gocov/gocov",
|
||||
"github.com/containers/image/copy",
|
||||
"github.com/containers/image/docker",
|
||||
"github.com/containers/image/pkg/blobinfocache/memory",
|
||||
|
@ -1783,6 +1819,7 @@
|
|||
"github.com/golang/mock/mockgen",
|
||||
"github.com/gorilla/mux",
|
||||
"github.com/jim-minter/go-cosmosdb/cmd/gencosmosdb",
|
||||
"github.com/jstemmer/go-junit-report",
|
||||
"github.com/onsi/ginkgo",
|
||||
"github.com/onsi/gomega",
|
||||
"github.com/onsi/gomega/format",
|
||||
|
@ -1827,12 +1864,13 @@
|
|||
"golang.org/x/tools/go/ast/astutil",
|
||||
"golang.org/x/tools/go/packages",
|
||||
"k8s.io/api/core/v1",
|
||||
"k8s.io/apimachinery/pkg/api/errors",
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1",
|
||||
"k8s.io/apimachinery/pkg/util/wait",
|
||||
"k8s.io/client-go/kubernetes",
|
||||
"k8s.io/client-go/rest",
|
||||
"k8s.io/client-go/tools/clientcmd",
|
||||
"k8s.io/client-go/tools/clientcmd/api",
|
||||
"k8s.io/client-go/tools/clientcmd/api/latest",
|
||||
"k8s.io/client-go/tools/clientcmd/api/v1",
|
||||
"k8s.io/client-go/tools/metrics",
|
||||
"k8s.io/client-go/util/retry",
|
||||
|
|
|
@ -2,7 +2,10 @@ required = [
|
|||
"github.com/alvaroloes/enumer",
|
||||
"github.com/jim-minter/go-cosmosdb/cmd/gencosmosdb",
|
||||
"github.com/golang/mock/mockgen",
|
||||
"golang.org/x/tools/cmd/goimports"
|
||||
"golang.org/x/tools/cmd/goimports",
|
||||
"github.com/jstemmer/go-junit-report",
|
||||
"github.com/axw/gocov/gocov",
|
||||
"github.com/AlekSi/gocov-xml"
|
||||
]
|
||||
|
||||
[[constraint]]
|
||||
|
|
7
Makefile
7
Makefile
|
@ -40,7 +40,7 @@ client: generate
|
|||
--input-file=/swagger/redhatopenshift/resource-manager/Microsoft.RedHatOpenShift/preview/2019-12-31-preview/redhatopenshift.json \
|
||||
--output-folder=/python/client
|
||||
|
||||
sudo chown -R $(id -un):$(id -gn) pkg/client python/client
|
||||
sudo chown -R $$(id -un):$$(id -gn) pkg/client python/client
|
||||
sed -i -e 's|azure/aro-rp|Azure/ARO-RP|g' pkg/client/services/preview/redhatopenshift/mgmt/2019-12-31-preview/redhatopenshift/models.go pkg/client/services/preview/redhatopenshift/mgmt/2019-12-31-preview/redhatopenshift/redhatopenshiftapi/interfaces.go
|
||||
rm -rf python/client/azure/mgmt/redhatopenshift/v2019_12_31_preview/aio
|
||||
>python/client/__init__.py
|
||||
|
@ -81,7 +81,7 @@ secrets-update:
|
|||
oc create secret generic aro-v4-dev --from-file=secrets --dry-run -o yaml | oc apply -f -
|
||||
|
||||
e2e:
|
||||
go test ./test/e2e -timeout "60m" -v -ginkgo.v -tags e2e
|
||||
go test ./test/e2e -timeout 60m -v -ginkgo.v -tags e2e
|
||||
|
||||
test-go: generate
|
||||
go build ./...
|
||||
|
@ -93,9 +93,10 @@ test-go: generate
|
|||
@[ -z "$$(ls pkg/util/*.go 2>/dev/null)" ] || (echo error: go files are not allowed in pkg/util, use a subpackage; exit 1)
|
||||
@[ -z "$$(find -name "*:*")" ] || (echo error: filenames with colons are not allowed on Windows, please rename; exit 1)
|
||||
@sha256sum --quiet -c .sha256sum || (echo error: client library is stale, please run make client; exit 1)
|
||||
go test -tags e2e -run ^$$ ./test/e2e/...
|
||||
|
||||
go vet ./...
|
||||
go test ./...
|
||||
go test ./... -coverprofile cover.out | tee uts.txt
|
||||
|
||||
test-python: generate pyenv${PYTHON_VERSION}
|
||||
. pyenv${PYTHON_VERSION}/bin/activate && \
|
||||
|
|
|
@ -9,6 +9,15 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"component": {
|
||||
"type": "git",
|
||||
"git": {
|
||||
"commitHash": "3a14fb1c4737b3995174c5f4d6d08a348b9b4180",
|
||||
"repositoryUrl": "https://github.com/AlekSi/gocov-xml/"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"component": {
|
||||
"type": "git",
|
||||
|
@ -108,6 +117,15 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"component": {
|
||||
"type": "git",
|
||||
"git": {
|
||||
"commitHash": "b6eca663ebb7e7ef9798914d19f53ba2c6f74c96",
|
||||
"repositoryUrl": "https://github.com/axw/gocov/"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"component": {
|
||||
"type": "git",
|
||||
|
@ -450,6 +468,15 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"component": {
|
||||
"type": "git",
|
||||
"git": {
|
||||
"commitHash": "cc1f095d5cc5eca2844f5c5ea7bb37f6b9bf6cac",
|
||||
"repositoryUrl": "https://github.com/jstemmer/go-junit-report/"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"component": {
|
||||
"type": "git",
|
||||
|
|
|
@ -24,17 +24,17 @@ func monitor(ctx context.Context, log *logrus.Entry) error {
|
|||
return err
|
||||
}
|
||||
|
||||
db, err := database.NewDatabase(ctx, log.WithField("component", "database"), env, uuid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m, err := statsd.New(ctx, log.WithField("component", "metrics"), env)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer m.Close()
|
||||
|
||||
db, err := database.NewDatabase(ctx, log.WithField("component", "database"), env, m, uuid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mon := pkgmonitor.NewMonitor(log.WithField("component", "monitor"), env, db, m)
|
||||
|
||||
return mon.Run(ctx)
|
||||
|
|
|
@ -30,7 +30,13 @@ func rp(ctx context.Context, log *logrus.Entry) error {
|
|||
return err
|
||||
}
|
||||
|
||||
db, err := database.NewDatabase(ctx, log.WithField("component", "database"), env, uuid)
|
||||
m, err := statsd.New(ctx, log.WithField("component", "metrics"), env)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer m.Close()
|
||||
|
||||
db, err := database.NewDatabase(ctx, log.WithField("component", "database"), env, m, uuid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -40,12 +46,6 @@ func rp(ctx context.Context, log *logrus.Entry) error {
|
|||
done := make(chan struct{})
|
||||
signal.Notify(sigterm, syscall.SIGTERM)
|
||||
|
||||
m, err := statsd.New(ctx, log.WithField("component", "metrics"), env)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer m.Close()
|
||||
|
||||
f, err := frontend.NewFrontend(ctx, log.WithField("component", "frontend"), env, db, api.APIs, m)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
@ -1,16 +1,19 @@
|
|||
# Testing
|
||||
|
||||
* unit tests
|
||||
* Unit tests
|
||||
|
||||
```
|
||||
make test
|
||||
```
|
||||
|
||||
* e2e tests
|
||||
* E2e tests
|
||||
|
||||
1. start the RP
|
||||
1. run the e2e
|
||||
```
|
||||
make e2e
|
||||
```
|
||||
Note the test will tell you will what Environment variables to define
|
||||
1. Start the RP
|
||||
|
||||
1. Run the e2e:
|
||||
|
||||
```
|
||||
make e2e
|
||||
```
|
||||
|
||||
The test will tell you what environment variables need to be defined.
|
||||
|
|
|
@ -15,6 +15,7 @@ import (
|
|||
"github.com/Azure/ARO-RP/pkg/api"
|
||||
"github.com/Azure/ARO-RP/pkg/database"
|
||||
"github.com/Azure/ARO-RP/pkg/env"
|
||||
"github.com/Azure/ARO-RP/pkg/metrics/noop"
|
||||
utillog "github.com/Azure/ARO-RP/pkg/util/log"
|
||||
)
|
||||
|
||||
|
@ -28,7 +29,7 @@ func run(ctx context.Context, log *logrus.Entry) error {
|
|||
return err
|
||||
}
|
||||
|
||||
db, err := database.NewDatabase(ctx, log.WithField("component", "database"), env, "")
|
||||
db, err := database.NewDatabase(ctx, log.WithField("component", "database"), env, &noop.Noop{}, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -5,20 +5,19 @@ package main
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/Azure/go-autorest/autorest/azure"
|
||||
"github.com/Azure/go-autorest/autorest/azure/auth"
|
||||
"github.com/ugorji/go/codec"
|
||||
|
||||
"github.com/Azure/ARO-RP/pkg/api"
|
||||
"github.com/Azure/ARO-RP/pkg/util/azureclient/mgmt/redhatopenshift"
|
||||
utillog "github.com/Azure/ARO-RP/pkg/util/log"
|
||||
)
|
||||
|
||||
func writeKubeconfig(ctx context.Context, resourceid string) error {
|
||||
res, err := azure.ParseResourceID(resourceid)
|
||||
func writeKubeconfig(ctx context.Context, resourceID string) error {
|
||||
res, err := azure.ParseResourceID(resourceID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -27,47 +26,46 @@ func writeKubeconfig(ctx context.Context, resourceid string) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
openshiftclusters := redhatopenshift.NewOpenShiftClustersClient(res.SubscriptionID, authorizer)
|
||||
|
||||
oc, err := openshiftclusters.Get(ctx, res.ResourceGroup, res.ResourceName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
creds, err := openshiftclusters.ListCredentials(ctx, res.ResourceGroup, res.ResourceName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tokenURL, err := getTokenURLFromConsoleURL(*oc.Properties.ConsoleProfile.URL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
token, err := getAuthorizedToken(tokenURL, *creds.KubeadminUsername, *creds.KubeadminPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
adminKubeconfig, err := makeKubeconfig(strings.Replace(*oc.Properties.ApiserverProfile.URL, "https://", "", 1), *creds.KubeadminUsername, token, "kube-system")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
h := &codec.JsonHandle{
|
||||
Indent: 4,
|
||||
}
|
||||
|
||||
err = api.AddExtensions(&h.BasicHandle)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
adminKubeconfig := makeKubeconfig(strings.Replace(*oc.Properties.ApiserverProfile.URL, "https://", "", 1), *creds.KubeadminUsername, token, "kube-system")
|
||||
|
||||
return codec.NewEncoder(os.Stdout, h).Encode(adminKubeconfig)
|
||||
e := json.NewEncoder(os.Stdout)
|
||||
e.SetIndent("", " ")
|
||||
return e.Encode(adminKubeconfig)
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) != 2 {
|
||||
fmt.Printf("usage: %s resourceid\n", os.Args[0])
|
||||
os.Exit(2)
|
||||
}
|
||||
ctx := context.Background()
|
||||
log := utillog.GetLogger()
|
||||
|
||||
if len(os.Args) != 2 {
|
||||
log.Fatalf("usage: %s resourceid\n", os.Args[0])
|
||||
}
|
||||
|
||||
err := writeKubeconfig(ctx, os.Args[1])
|
||||
if err != nil {
|
||||
fmt.Printf("%v\n", err)
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import (
|
|||
v1 "k8s.io/client-go/tools/clientcmd/api/v1"
|
||||
)
|
||||
|
||||
func makeKubeconfig(endpoint, username, token, namespace string) (*v1.Config, error) {
|
||||
func makeKubeconfig(endpoint, username, token, namespace string) *v1.Config {
|
||||
clustername := strings.Replace(endpoint, ".", "-", -1)
|
||||
authinfoname := username + "/" + clustername
|
||||
contextname := namespace + "/" + clustername + "/" + username
|
||||
|
@ -45,5 +45,5 @@ func makeKubeconfig(endpoint, username, token, namespace string) (*v1.Config, er
|
|||
},
|
||||
},
|
||||
CurrentContext: contextname,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@ package main
|
|||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
@ -17,25 +16,28 @@ func parseTokenResponse(location string) (string, error) {
|
|||
return "", err
|
||||
}
|
||||
|
||||
for _, param := range strings.Split(locURL.Fragment, "&") {
|
||||
nameValue := strings.Split(param, "=")
|
||||
if nameValue[0] == "access_token" {
|
||||
return nameValue[1], nil
|
||||
}
|
||||
v, err := url.ParseQuery(locURL.Fragment)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return "", fmt.Errorf("token not found in response")
|
||||
|
||||
return v.Get("access_token"), nil
|
||||
}
|
||||
|
||||
func getTokenURLFromConsoleURL(consoleURL string) (*url.URL, error) {
|
||||
tokenURL, err := url.Parse(strings.Replace(consoleURL, "console-openshift-console.apps.", "oauth-openshift.apps.", 1))
|
||||
tokenURL, err := url.Parse(consoleURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tokenURL.Host = strings.Replace(tokenURL.Host, "console-openshift-console", "oauth-openshift", 1)
|
||||
tokenURL.Path = "/oauth/authorize"
|
||||
|
||||
q := tokenURL.Query()
|
||||
q.Set("response_type", "token")
|
||||
q.Set("client_id", "openshift-challenging-client")
|
||||
tokenURL.RawQuery = q.Encode()
|
||||
|
||||
return tokenURL, nil
|
||||
}
|
||||
|
||||
|
@ -44,24 +46,26 @@ func getAuthorizedToken(tokenURL *url.URL, username, password string) (string, e
|
|||
req.SetBasicAuth(username, password)
|
||||
req.Header.Add("X-CSRF-Token", "1")
|
||||
|
||||
resp, err := (&http.Client{
|
||||
cli := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
},
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }}).Do(req)
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := cli.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 302 {
|
||||
|
||||
if resp.StatusCode != http.StatusFound {
|
||||
return "", err
|
||||
}
|
||||
|
||||
loc := resp.Header["Location"]
|
||||
if loc == nil {
|
||||
return "", fmt.Errorf("no Location header found")
|
||||
}
|
||||
return parseTokenResponse(loc[0])
|
||||
return parseTokenResponse(resp.Header.Get("Location"))
|
||||
}
|
||||
|
|
|
@ -10,23 +10,22 @@ import (
|
|||
func TestGetTokenURLFromConsoleURL(t *testing.T) {
|
||||
got, err := getTokenURLFromConsoleURL("https://console-openshift-console.apps.zhz4qvm8.as-rp-v4.osadev.cloud")
|
||||
if err != nil {
|
||||
t.Errorf("GetTokenURLFromConsoleURL() error = %v", err)
|
||||
return
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.String() != "https://oauth-openshift.apps.zhz4qvm8.as-rp-v4.osadev.cloud/oauth/authorize?client_id=openshift-challenging-client&response_type=token" {
|
||||
t.Errorf("GetTokenURLFromConsoleURL() = %v", got)
|
||||
t.Error(got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseTokenResponse(t *testing.T) {
|
||||
location := "https://oauth-openshift.apps.zhz4qvm8.as-rp-v4.osadev.cloud/oauth/token/implicit#access_token=fIof3McZ5DVt1Uy6atsnUhis-y43dMctA5irrxH8ixk&expires_in=86400&scope=user%3Afull&token_type=Bearer"
|
||||
want := "fIof3McZ5DVt1Uy6atsnUhis-y43dMctA5irrxH8ixk"
|
||||
|
||||
got, err := parseTokenResponse(location)
|
||||
if err != nil {
|
||||
t.Errorf("parseTokenResponse() error = %v", err)
|
||||
return
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != want {
|
||||
t.Errorf("parseTokenResponse() = %v, want %v", got, want)
|
||||
t.Error(got)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,10 +15,15 @@ import (
|
|||
"github.com/Azure/ARO-RP/pkg/api"
|
||||
"github.com/Azure/ARO-RP/pkg/database/cosmosdb"
|
||||
"github.com/Azure/ARO-RP/pkg/env"
|
||||
"github.com/Azure/ARO-RP/pkg/metrics"
|
||||
dbmetrics "github.com/Azure/ARO-RP/pkg/metrics/statsd/cosmosdb"
|
||||
)
|
||||
|
||||
// Database represents a database
|
||||
type Database struct {
|
||||
log *logrus.Entry
|
||||
m metrics.Interface
|
||||
|
||||
AsyncOperations AsyncOperations
|
||||
Monitors Monitors
|
||||
OpenShiftClusters OpenShiftClusters
|
||||
|
@ -26,7 +31,7 @@ type Database struct {
|
|||
}
|
||||
|
||||
// NewDatabase returns a new Database
|
||||
func NewDatabase(ctx context.Context, log *logrus.Entry, env env.Interface, uuid string) (db *Database, err error) {
|
||||
func NewDatabase(ctx context.Context, log *logrus.Entry, env env.Interface, m metrics.Interface, uuid string) (db *Database, err error) {
|
||||
databaseAccount, masterKey := env.CosmosDB()
|
||||
|
||||
h := &codec.JsonHandle{
|
||||
|
@ -43,10 +48,11 @@ func NewDatabase(ctx context.Context, log *logrus.Entry, env env.Interface, uuid
|
|||
}
|
||||
|
||||
c := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
Transport: dbmetrics.New(log, &http.Transport{
|
||||
// disable HTTP/2 for now: https://github.com/golang/go/issues/36026
|
||||
TLSNextProto: map[string]func(string, *tls.Conn) http.RoundTripper{},
|
||||
},
|
||||
TLSNextProto: map[string]func(string, *tls.Conn) http.RoundTripper{},
|
||||
MaxIdleConnsPerHost: 20,
|
||||
}, m),
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
|
||||
|
@ -55,7 +61,10 @@ func NewDatabase(ctx context.Context, log *logrus.Entry, env env.Interface, uuid
|
|||
return nil, err
|
||||
}
|
||||
|
||||
db = &Database{}
|
||||
db = &Database{
|
||||
log: log,
|
||||
m: m,
|
||||
}
|
||||
|
||||
db.AsyncOperations, err = NewAsyncOperations(uuid, dbc, env.DatabaseName(), "AsyncOperations")
|
||||
if err != nil {
|
||||
|
@ -77,5 +86,7 @@ func NewDatabase(ctx context.Context, log *logrus.Entry, env env.Interface, uuid
|
|||
return nil, err
|
||||
}
|
||||
|
||||
go db.emitMetrics(ctx)
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
package database
|
||||
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the Apache License 2.0.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/Azure/ARO-RP/pkg/util/recover"
|
||||
)
|
||||
|
||||
func (db *Database) emitMetrics(ctx context.Context) {
|
||||
defer recover.Panic(db.log)
|
||||
t := time.NewTicker(time.Minute)
|
||||
defer t.Stop()
|
||||
|
||||
for range t.C {
|
||||
i, err := db.OpenShiftClusters.QueueLength(ctx, "OpenShiftClusters")
|
||||
if err != nil {
|
||||
db.log.Error(err)
|
||||
} else {
|
||||
db.m.EmitGauge("database.openshiftclusters.queue.length", int64(i), nil)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,8 +16,9 @@ import (
|
|||
)
|
||||
|
||||
type openShiftClusters struct {
|
||||
c cosmosdb.OpenShiftClusterDocumentClient
|
||||
uuid string
|
||||
c cosmosdb.OpenShiftClusterDocumentClient
|
||||
collc cosmosdb.CollectionClient
|
||||
uuid string
|
||||
}
|
||||
|
||||
// OpenShiftClusters is the database interface for OpenShiftClusterDocuments
|
||||
|
@ -25,6 +26,7 @@ type OpenShiftClusters interface {
|
|||
Create(context.Context, *api.OpenShiftClusterDocument) (*api.OpenShiftClusterDocument, error)
|
||||
ListAll(context.Context) (*api.OpenShiftClusterDocuments, error)
|
||||
Get(context.Context, string) (*api.OpenShiftClusterDocument, error)
|
||||
QueueLength(context.Context, string) (int, error)
|
||||
Patch(context.Context, string, func(*api.OpenShiftClusterDocument) error) (*api.OpenShiftClusterDocument, error)
|
||||
PatchWithLease(context.Context, string, func(*api.OpenShiftClusterDocument) error) (*api.OpenShiftClusterDocument, error)
|
||||
Update(context.Context, *api.OpenShiftClusterDocument) (*api.OpenShiftClusterDocument, error)
|
||||
|
@ -64,8 +66,9 @@ func NewOpenShiftClusters(ctx context.Context, uuid string, dbc cosmosdb.Databas
|
|||
}
|
||||
|
||||
return &openShiftClusters{
|
||||
c: cosmosdb.NewOpenShiftClusterDocumentClient(collc, collid),
|
||||
uuid: uuid,
|
||||
c: cosmosdb.NewOpenShiftClusterDocumentClient(collc, collid),
|
||||
collc: collc,
|
||||
uuid: uuid,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -126,6 +129,37 @@ func (c *openShiftClusters) Get(ctx context.Context, key string) (*api.OpenShift
|
|||
}
|
||||
}
|
||||
|
||||
// QueueLength returns OpenShiftClusters un-queued document count.
|
||||
// If error occurs, 0 is returned with error message
|
||||
func (c *openShiftClusters) QueueLength(ctx context.Context, collid string) (int, error) {
|
||||
partitions, err := c.collc.PartitionKeyRanges(ctx, collid)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var countTotal int
|
||||
for _, r := range partitions.PartitionKeyRanges {
|
||||
result := c.c.Query("", &cosmosdb.Query{
|
||||
Query: `SELECT VALUE COUNT(1) FROM OpenShiftClusters doc WHERE doc.openShiftCluster.properties.provisioningState IN ("Creating", "Deleting", "Updating") AND (doc.leaseExpires ?? 0) < GetCurrentTimestamp() / 1000`,
|
||||
}, &cosmosdb.Options{
|
||||
PartitionKeyRangeID: r.ID,
|
||||
})
|
||||
// because we aggregate count we don't expect pagination in this query result,
|
||||
// so we gonna call Next() only once per partition.
|
||||
var data struct {
|
||||
api.MissingFields
|
||||
Document []int `json:"Documents,omitempty"`
|
||||
}
|
||||
err := result.NextRaw(ctx, &data)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
countTotal = countTotal + data.Document[0]
|
||||
}
|
||||
return countTotal, nil
|
||||
}
|
||||
|
||||
func (c *openShiftClusters) Patch(ctx context.Context, key string, f func(*api.OpenShiftClusterDocument) error) (*api.OpenShiftClusterDocument, error) {
|
||||
return c.patch(ctx, key, f, nil)
|
||||
}
|
||||
|
@ -207,7 +241,7 @@ func (c *openShiftClusters) ListByPrefix(subscriptionID string, prefix string) (
|
|||
|
||||
func (c *openShiftClusters) Dequeue(ctx context.Context) (*api.OpenShiftClusterDocument, error) {
|
||||
i := c.c.Query("", &cosmosdb.Query{
|
||||
Query: `SELECT * FROM OpenShiftClusters doc WHERE NOT (doc.openShiftCluster.properties.provisioningState IN ("Succeeded", "Failed")) AND (doc.leaseExpires ?? 0) < GetCurrentTimestamp() / 1000`,
|
||||
Query: `SELECT * FROM OpenShiftClusters doc WHERE doc.openShiftCluster.properties.provisioningState IN ("Creating", "Deleting", "Updating") AND (doc.leaseExpires ?? 0) < GetCurrentTimestamp() / 1000`,
|
||||
}, nil)
|
||||
|
||||
for {
|
||||
|
|
|
@ -22,6 +22,11 @@ func (f *frontend) postOpenShiftClusterCredentials(w http.ResponseWriter, r *htt
|
|||
log := ctx.Value(middleware.ContextKeyLog).(*logrus.Entry)
|
||||
vars := mux.Vars(r)
|
||||
|
||||
if f.apis[vars["api-version"]].OpenShiftClusterCredentialsConverter == nil {
|
||||
api.WriteError(w, http.StatusNotFound, api.CloudErrorCodeInvalidResourceType, "", "The resource type '%s' could not be found in the namespace '%s' for api version '%s'.", vars["resourceType"], vars["resourceProviderNamespace"], vars["api-version"])
|
||||
return
|
||||
}
|
||||
|
||||
body := r.Context().Value(middleware.ContextKeyBody).([]byte)
|
||||
if len(body) > 0 && !json.Valid(body) {
|
||||
api.WriteError(w, http.StatusBadRequest, api.CloudErrorCodeInvalidRequestContent, "", "The request content was invalid and could not be deserialized.")
|
||||
|
|
|
@ -60,11 +60,20 @@ func TestPostOpenShiftClusterCredentials(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
apis := map[string]*api.Version{
|
||||
"2019-12-31-preview": api.APIs["2019-12-31-preview"],
|
||||
"no-credentials": {
|
||||
OpenShiftClusterConverter: api.APIs["2019-12-31-preview"].OpenShiftClusterConverter,
|
||||
OpenShiftClusterValidator: api.APIs["2019-12-31-preview"].OpenShiftClusterValidator,
|
||||
},
|
||||
}
|
||||
|
||||
mockSubID := "00000000-0000-0000-0000-000000000000"
|
||||
|
||||
type test struct {
|
||||
name string
|
||||
resourceID string
|
||||
apiVersion string
|
||||
mocks func(*test, *mock_database.MockOpenShiftClusters)
|
||||
wantStatusCode int
|
||||
wantResponse func(*test) *v20191231preview.OpenShiftClusterCredentials
|
||||
|
@ -101,6 +110,13 @@ func TestPostOpenShiftClusterCredentials(t *testing.T) {
|
|||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "credentials request is not allowed in the API version",
|
||||
resourceID: fmt.Sprintf("/subscriptions/%s/resourcegroups/resourceGroup/providers/Microsoft.RedHatOpenShift/openShiftClusters/resourceName", mockSubID),
|
||||
apiVersion: "no-credentials",
|
||||
wantStatusCode: http.StatusNotFound,
|
||||
wantError: `404: InvalidResourceType: : The resource type 'openshiftclusters' could not be found in the namespace 'microsoft.redhatopenshift' for api version 'no-credentials'.`,
|
||||
},
|
||||
{
|
||||
name: "cluster exists in db in creating state",
|
||||
resourceID: fmt.Sprintf("/subscriptions/%s/resourcegroups/resourceGroup/providers/Microsoft.RedHatOpenShift/openShiftClusters/resourceName", mockSubID),
|
||||
|
@ -239,30 +255,37 @@ func TestPostOpenShiftClusterCredentials(t *testing.T) {
|
|||
openshiftClusters := mock_database.NewMockOpenShiftClusters(controller)
|
||||
subscriptions := mock_database.NewMockSubscriptions(controller)
|
||||
|
||||
subscriptions.EXPECT().
|
||||
Get(gomock.Any(), mockSubID).
|
||||
Return(&api.SubscriptionDocument{
|
||||
Subscription: &api.Subscription{
|
||||
State: api.SubscriptionStateRegistered,
|
||||
Properties: &api.SubscriptionProperties{
|
||||
TenantID: "11111111-1111-1111-1111-111111111111",
|
||||
if tt.mocks != nil {
|
||||
subscriptions.EXPECT().
|
||||
Get(gomock.Any(), mockSubID).
|
||||
Return(&api.SubscriptionDocument{
|
||||
Subscription: &api.Subscription{
|
||||
State: api.SubscriptionStateRegistered,
|
||||
Properties: &api.SubscriptionProperties{
|
||||
TenantID: "11111111-1111-1111-1111-111111111111",
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
}, nil)
|
||||
|
||||
tt.mocks(tt, openshiftClusters)
|
||||
tt.mocks(tt, openshiftClusters)
|
||||
}
|
||||
|
||||
f, err := NewFrontend(ctx, logrus.NewEntry(logrus.StandardLogger()), env, &database.Database{
|
||||
OpenShiftClusters: openshiftClusters,
|
||||
Subscriptions: subscriptions,
|
||||
}, api.APIs, &noop.Noop{})
|
||||
}, apis, &noop.Noop{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
go f.Run(ctx, nil, nil)
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, "https://server"+tt.resourceID+"/listcredentials?api-version=2019-12-31-preview", nil)
|
||||
reqAPIVersion := "2019-12-31-preview"
|
||||
if tt.apiVersion != "" {
|
||||
reqAPIVersion = tt.apiVersion
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("https://server%s/listcredentials?api-version=%s", tt.resourceID, reqAPIVersion), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
package cosmodb
|
||||
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the Apache License 2.0.
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/Azure/ARO-RP/pkg/metrics"
|
||||
)
|
||||
|
||||
var _ http.RoundTripper = (*tracerRoundTripper)(nil)
|
||||
|
||||
type tracerRoundTripper struct {
|
||||
log *logrus.Entry
|
||||
m metrics.Interface
|
||||
tr http.RoundTripper
|
||||
}
|
||||
|
||||
func New(log *logrus.Entry, tr *http.Transport, m metrics.Interface) *tracerRoundTripper {
|
||||
return &tracerRoundTripper{
|
||||
log: log,
|
||||
m: m,
|
||||
tr: tr,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *tracerRoundTripper) RoundTrip(req *http.Request) (resp *http.Response, err error) {
|
||||
start := time.Now()
|
||||
|
||||
defer func() {
|
||||
if resp == nil {
|
||||
return
|
||||
}
|
||||
var ru float64
|
||||
// Sometimes we get request-charge="" because pkranges API is free
|
||||
// We log this on debug mode only and ignore
|
||||
requestCharge := strings.Trim(resp.Header.Get("x-ms-request-charge"), `"`)
|
||||
if requestCharge != "" {
|
||||
ru, err = strconv.ParseFloat(requestCharge, 64)
|
||||
if err != nil {
|
||||
// we don't want to kill all DB calls if this fails
|
||||
t.log.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
parts := strings.Split(req.URL.Path, "/")
|
||||
if len(parts) >= 2 && parts[len(parts)-2] == "docs" {
|
||||
parts[len(parts)-1] = "{id}"
|
||||
}
|
||||
path := strings.Join(parts, "/")
|
||||
|
||||
// emit RU only if we managed to parse RU value
|
||||
if err == nil {
|
||||
t.m.EmitFloat("client.cosmosdb.requestunits", ru, map[string]string{
|
||||
"code": strconv.Itoa(resp.StatusCode),
|
||||
"verb": req.Method,
|
||||
"path": path,
|
||||
})
|
||||
}
|
||||
|
||||
t.m.EmitGauge("client.cosmosdb.count", 1, map[string]string{
|
||||
"code": strconv.Itoa(resp.StatusCode),
|
||||
"verb": req.Method,
|
||||
"path": path,
|
||||
})
|
||||
|
||||
t.m.EmitFloat("client.cosmosdb.duration", time.Now().Sub(start).Seconds(), map[string]string{
|
||||
"code": strconv.Itoa(resp.StatusCode),
|
||||
"verb": req.Method,
|
||||
"path": path,
|
||||
})
|
||||
|
||||
}()
|
||||
|
||||
return t.tr.RoundTrip(req)
|
||||
}
|
|
@ -98,7 +98,12 @@ func validate(path string, v, w reflect.Value, ignoreCase bool) error {
|
|||
for i.Next() {
|
||||
k := i.Key()
|
||||
|
||||
err := validate(fmt.Sprintf("%s[%q]", path, k.Interface()), v.MapIndex(k), w.MapIndex(k), ignoreCase)
|
||||
mapW := w.MapIndex(k)
|
||||
if !mapW.IsValid() {
|
||||
return validationError(path)
|
||||
}
|
||||
|
||||
err := validate(fmt.Sprintf("%s[%q]", path, k.Interface()), v.MapIndex(k), mapW, ignoreCase)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -8,11 +8,12 @@ import (
|
|||
)
|
||||
|
||||
type ts struct {
|
||||
Mutable string `json:"mutable,omitempty" mutable:"true"` // should be able to change
|
||||
Case string `json:"case,omitempty" mutable:"case"` // should be case insensitive
|
||||
Empty string `json:"empty,omitempty" mutable:""` // default to immutable
|
||||
EmptyNoJSON string `mutable:"false"` // handle no json tag
|
||||
None string // default to immutable
|
||||
Mutable string `json:"mutable,omitempty" mutable:"true"` // should be able to change
|
||||
Case string `json:"case,omitempty" mutable:"case"` // should be case insensitive
|
||||
Empty string `json:"empty,omitempty" mutable:""` // default to immutable
|
||||
Map map[string]string `json:"map,omitempty"`
|
||||
EmptyNoJSON string `mutable:"false"` // handle no json tag
|
||||
None string // default to immutable
|
||||
}
|
||||
|
||||
func TestValidate(t *testing.T) {
|
||||
|
@ -22,6 +23,9 @@ func TestValidate(t *testing.T) {
|
|||
Empty: "before",
|
||||
EmptyNoJSON: "before",
|
||||
None: "before",
|
||||
Map: map[string]string{
|
||||
"key": "value",
|
||||
},
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
|
@ -57,6 +61,20 @@ func TestValidate(t *testing.T) {
|
|||
},
|
||||
wantErr: "400: PropertyChangeNotAllowed: empty: Changing property 'empty' is not allowed.",
|
||||
},
|
||||
{
|
||||
name: "can NOT replace a map",
|
||||
modify: func(s *ts) {
|
||||
s.Map = map[string]string{"new": "value"}
|
||||
},
|
||||
wantErr: "400: PropertyChangeNotAllowed: map: Changing property 'map' is not allowed.",
|
||||
},
|
||||
{
|
||||
name: "can NOT change a value in a map",
|
||||
modify: func(s *ts) {
|
||||
s.Map = map[string]string{"key": "new-value"}
|
||||
},
|
||||
wantErr: "400: PropertyChangeNotAllowed: map[\"key\"]: Changing property 'map[\"key\"]' is not allowed.",
|
||||
},
|
||||
{
|
||||
name: "can NOT change EmptyNoJSON",
|
||||
modify: func(s *ts) {
|
||||
|
|
|
@ -268,6 +268,21 @@ func (mr *MockOpenShiftClustersMockRecorder) PatchWithLease(arg0, arg1, arg2 int
|
|||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PatchWithLease", reflect.TypeOf((*MockOpenShiftClusters)(nil).PatchWithLease), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// QueueLength mocks base method
|
||||
func (m *MockOpenShiftClusters) QueueLength(arg0 context.Context, arg1 string) (int, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "QueueLength", arg0, arg1)
|
||||
ret0, _ := ret[0].(int)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// QueueLength indicates an expected call of QueueLength
|
||||
func (mr *MockOpenShiftClustersMockRecorder) QueueLength(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueueLength", reflect.TypeOf((*MockOpenShiftClusters)(nil).QueueLength), arg0, arg1)
|
||||
}
|
||||
|
||||
// Update mocks base method
|
||||
func (m *MockOpenShiftClusters) Update(arg0 context.Context, arg1 *api.OpenShiftClusterDocument) (*api.OpenShiftClusterDocument, error) {
|
||||
m.ctrl.T.Helper()
|
||||
|
|
|
@ -15,19 +15,21 @@ import (
|
|||
"github.com/Azure/ARO-RP/test/util/ready"
|
||||
)
|
||||
|
||||
var _ = Describe("Check the node count is correct [CheckNodeCount][EveryPR]", func() {
|
||||
It("should be possible to list nodes and confirm they are as expected", func() {
|
||||
By("Verifying that the expected number of nodes exist and are ready")
|
||||
var _ = Describe("Check nodes", func() {
|
||||
Specify("node count should match the cluster resource and nodes should be ready", func() {
|
||||
ctx := context.Background()
|
||||
cs, err := Clients.openshiftclusters.Get(ctx, os.Getenv("RESOURCEGROUP"), os.Getenv("CLUSTER"))
|
||||
|
||||
oc, err := Clients.OpenshiftClusters.Get(ctx, os.Getenv("RESOURCEGROUP"), os.Getenv("CLUSTER"))
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
var expectedNodeCount int32 = 3 // for masters
|
||||
for _, wp := range *cs.WorkerProfiles {
|
||||
for _, wp := range *oc.WorkerProfiles {
|
||||
expectedNodeCount += *wp.Count
|
||||
}
|
||||
|
||||
nodes, err := Clients.kubernetes.CoreV1().Nodes().List(metav1.ListOptions{})
|
||||
nodes, err := Clients.Kubernetes.CoreV1().Nodes().List(metav1.ListOptions{})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
var nodeCount int64
|
||||
for _, node := range nodes.Items {
|
||||
if ready.NodeIsReady(&node) {
|
||||
|
@ -38,6 +40,7 @@ var _ = Describe("Check the node count is correct [CheckNodeCount][EveryPR]", fu
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
Expect(nodeCount).To(Equal(expectedNodeCount))
|
||||
})
|
||||
})
|
||||
|
|
|
@ -4,9 +4,7 @@ package e2e
|
|||
// Licensed under the Apache License 2.0.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
|
@ -14,18 +12,14 @@ import (
|
|||
"github.com/Azure/go-autorest/autorest/azure/auth"
|
||||
"github.com/sirupsen/logrus"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
kapi "k8s.io/client-go/tools/clientcmd/api"
|
||||
"k8s.io/client-go/tools/clientcmd/api/latest"
|
||||
v1 "k8s.io/client-go/tools/clientcmd/api/v1"
|
||||
|
||||
"github.com/Azure/ARO-RP/pkg/util/azureclient/mgmt/redhatopenshift"
|
||||
)
|
||||
|
||||
type ClientSet struct {
|
||||
openshiftclusters redhatopenshift.OpenShiftClustersClient
|
||||
kubernetes kubernetes.Interface
|
||||
OpenshiftClusters redhatopenshift.OpenShiftClustersClient
|
||||
Kubernetes kubernetes.Interface
|
||||
}
|
||||
|
||||
var (
|
||||
|
@ -33,58 +27,45 @@ var (
|
|||
Clients *ClientSet
|
||||
)
|
||||
|
||||
func restConfigFromV1Config(kc *v1.Config) (*rest.Config, error) {
|
||||
var c kapi.Config
|
||||
err := latest.Scheme.Convert(kc, &c, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
kubeconfig := clientcmd.NewDefaultClientConfig(c, &clientcmd.ConfigOverrides{})
|
||||
return kubeconfig.ClientConfig()
|
||||
}
|
||||
|
||||
func newClientSet() (*ClientSet, error) {
|
||||
authorizer, err := auth.NewAuthorizerFromEnvironment()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
d, err := ioutil.ReadFile("../../admin.kubeconfig")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var config *v1.Config
|
||||
json.Unmarshal(d, &config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
restconfig, err := restConfigFromV1Config(config)
|
||||
kubeconfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
|
||||
clientcmd.NewDefaultClientConfigLoadingRules(),
|
||||
&clientcmd.ConfigOverrides{},
|
||||
)
|
||||
|
||||
restconfig, err := kubeconfig.ClientConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cli, err := kubernetes.NewForConfig(restconfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cs := &ClientSet{
|
||||
openshiftclusters: redhatopenshift.NewOpenShiftClustersClient(os.Getenv("AZURE_SUBSCRIPTION_ID"), authorizer),
|
||||
kubernetes: cli,
|
||||
}
|
||||
|
||||
return cs, nil
|
||||
return &ClientSet{
|
||||
OpenshiftClusters: redhatopenshift.NewOpenShiftClustersClient(os.Getenv("AZURE_SUBSCRIPTION_ID"), authorizer),
|
||||
Kubernetes: cli,
|
||||
}, nil
|
||||
}
|
||||
|
||||
var _ = BeforeSuite(func() {
|
||||
Log.Info("BeforeSuite")
|
||||
for _, key := range []string{
|
||||
"AZURE_SUBSCRIPTION_ID", "AZURE_TENANT_ID", "AZURE_CLIENT_ID", "AZURE_CLIENT_SECRET",
|
||||
"CLUSTER", "RESOURCEGROUP",
|
||||
"AZURE_SUBSCRIPTION_ID",
|
||||
"CLUSTER",
|
||||
"RESOURCEGROUP",
|
||||
} {
|
||||
if _, found := os.LookupEnv(key); !found {
|
||||
panic(fmt.Sprintf("environment variable %q unset", key))
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
Clients, err = newClientSet()
|
||||
if err != nil {
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
*.json
|
||||
coverage.xml
|
||||
|
||||
gocov-xml
|
|
@ -0,0 +1,19 @@
|
|||
Copyright (c) 2013 Alexey Palazhchenko
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -0,0 +1,12 @@
|
|||
all: fvb
|
||||
|
||||
prepare:
|
||||
go get -u github.com/axw/gocov/...
|
||||
go get -u github.com/gorilla/mux/...
|
||||
gocov test -v github.com/gorilla/mux > mux.json
|
||||
|
||||
fvb:
|
||||
gofmt -e -s -w .
|
||||
go vet .
|
||||
go run ./gocov-xml.go < mux.json > coverage.xml
|
||||
xmllint --valid --noout coverage.xml
|
|
@ -0,0 +1,62 @@
|
|||
# gocov XML
|
||||
|
||||
A tool to generate Go coverage in XML report for using with tools/plugins like Jenkins/Cobertura.
|
||||
|
||||
> Table of Contents
|
||||
|
||||
- [gocov XML](#gocov-xml)
|
||||
- [Installation](#installation)
|
||||
- [Usage](#usage)
|
||||
- [Examples](#examples)
|
||||
- [Generate coverage by passing `gocov` output as input to `gocov-xml`](#generate-coverage-by-passing-gocov-output-as-input-to-gocov-xml)
|
||||
- [Specifying optional source](#specifying-optional-source)
|
||||
- [Authors](#authors)
|
||||
|
||||
This is a simple helper tool for generating XML output in [Cobertura](http://cobertura.sourceforge.net/) format
|
||||
for CIs like [Jenkins](https://wiki.jenkins-ci.org/display/JENKINS/Cobertura+Plugin), [vsts](https://www.visualstudio.com/team-services) and others
|
||||
from [github.com/axw/gocov](https://github.com/axw/gocov) output.
|
||||
The generated XML output is in the latest [coverage-04.dtd](http://cobertura.sourceforge.net/xml/coverage-04.dtd) schema
|
||||
|
||||
## Installation
|
||||
|
||||
Just type the following to install the program and its dependencies:
|
||||
|
||||
```bash
|
||||
go get github.com/axw/gocov/...
|
||||
go get github.com/AlekSi/gocov-xml
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
> **NOTE**: `gocov-xml` reads data from the standard input.
|
||||
|
||||
```bash
|
||||
gocov [-source <absolute path to source>]
|
||||
```
|
||||
|
||||
Where,
|
||||
|
||||
- **`source`**: Absolute path to source. Defaults to the current working directory.
|
||||
|
||||
### Examples
|
||||
|
||||
#### Generate coverage by passing `gocov` output as input to `gocov-xml`
|
||||
|
||||
```bash
|
||||
gocov test github.com/gorilla/mux | gocov-xml > coverage.xml
|
||||
```
|
||||
|
||||
#### Specifying optional source
|
||||
|
||||
```bash
|
||||
gocov test github.com/gorilla/mux | gocov-xml -source /abs/path/to/source > coverage.xml
|
||||
```
|
||||
|
||||
## Authors
|
||||
|
||||
- [Alexey Palazhchenko (AlekSi)](https://github.com/AlekSi)
|
||||
- [Yukinari Toyota (t-yuki)](https://github.com/t-yuki)
|
||||
- [Marin Bek (marinbek)](https://github.com/marinbek)
|
||||
- [Alex Castle (acastle)](https://github.com/acastle)
|
||||
- [Billy Yao (yaoyaozong)](https://github.com/yaoyaozong)
|
||||
- [Abhijith DA (abhijithda)](https://github.com/abhijithda)
|
|
@ -0,0 +1,61 @@
|
|||
<!-- Portions (C) International Organization for Standardization 1986:
|
||||
Permission to copy in any form is granted for use with
|
||||
conforming SGML systems and applications as defined in
|
||||
ISO 8879, provided this notice is included in all copies.
|
||||
-->
|
||||
|
||||
<!ELEMENT coverage (sources?,packages)>
|
||||
<!ATTLIST coverage line-rate CDATA #REQUIRED>
|
||||
<!ATTLIST coverage branch-rate CDATA #REQUIRED>
|
||||
<!ATTLIST coverage lines-covered CDATA #REQUIRED>
|
||||
<!ATTLIST coverage lines-valid CDATA #REQUIRED>
|
||||
<!ATTLIST coverage branches-covered CDATA #REQUIRED>
|
||||
<!ATTLIST coverage branches-valid CDATA #REQUIRED>
|
||||
<!ATTLIST coverage complexity CDATA #REQUIRED>
|
||||
<!ATTLIST coverage version CDATA #REQUIRED>
|
||||
<!ATTLIST coverage timestamp CDATA #REQUIRED>
|
||||
|
||||
<!ELEMENT sources (source*)>
|
||||
|
||||
<!ELEMENT source (#PCDATA)>
|
||||
|
||||
<!ELEMENT packages (package*)>
|
||||
|
||||
<!ELEMENT package (classes)>
|
||||
<!ATTLIST package name CDATA #REQUIRED>
|
||||
<!ATTLIST package line-rate CDATA #REQUIRED>
|
||||
<!ATTLIST package branch-rate CDATA #REQUIRED>
|
||||
<!ATTLIST package complexity CDATA #REQUIRED>
|
||||
|
||||
<!ELEMENT classes (class*)>
|
||||
|
||||
<!ELEMENT class (methods,lines)>
|
||||
<!ATTLIST class name CDATA #REQUIRED>
|
||||
<!ATTLIST class filename CDATA #REQUIRED>
|
||||
<!ATTLIST class line-rate CDATA #REQUIRED>
|
||||
<!ATTLIST class branch-rate CDATA #REQUIRED>
|
||||
<!ATTLIST class complexity CDATA #REQUIRED>
|
||||
|
||||
<!ELEMENT methods (method*)>
|
||||
|
||||
<!ELEMENT method (lines)>
|
||||
<!ATTLIST method name CDATA #REQUIRED>
|
||||
<!ATTLIST method signature CDATA #REQUIRED>
|
||||
<!ATTLIST method line-rate CDATA #REQUIRED>
|
||||
<!ATTLIST method branch-rate CDATA #REQUIRED>
|
||||
<!ATTLIST method complexity CDATA #REQUIRED>
|
||||
|
||||
<!ELEMENT lines (line*)>
|
||||
|
||||
<!ELEMENT line (conditions*)>
|
||||
<!ATTLIST line number CDATA #REQUIRED>
|
||||
<!ATTLIST line hits CDATA #REQUIRED>
|
||||
<!ATTLIST line branch CDATA "false">
|
||||
<!ATTLIST line condition-coverage CDATA "100%">
|
||||
|
||||
<!ELEMENT conditions (condition*)>
|
||||
|
||||
<!ELEMENT condition EMPTY>
|
||||
<!ATTLIST condition number CDATA #REQUIRED>
|
||||
<!ATTLIST condition type CDATA #REQUIRED>
|
||||
<!ATTLIST condition coverage CDATA #REQUIRED>
|
179
vendor/github.com/AlekSi/gocov-xml/coverage-with-data.xml
сгенерированный
поставляемый
Normal file
179
vendor/github.com/AlekSi/gocov-xml/coverage-with-data.xml
сгенерированный
поставляемый
Normal file
|
@ -0,0 +1,179 @@
|
|||
<?xml version="1.0"?>
|
||||
<!--DOCTYPE coverage SYSTEM "http://cobertura.sourceforge.net/xml/coverage-03.dtd"-->
|
||||
|
||||
<coverage line-rate="0.9" branch-rate="0.75" version="1.9" timestamp="1187350905008">
|
||||
<sources>
|
||||
<source>C:/local/mvn-coverage-example/src/main/java</source>
|
||||
<source>--source</source>
|
||||
</sources>
|
||||
<packages>
|
||||
<package name="" line-rate="1.0" branch-rate="1.0" complexity="1.0">
|
||||
<classes>
|
||||
<class name="Main" filename="Main.java" line-rate="1.0" branch-rate="1.0" complexity="1.0">
|
||||
<methods>
|
||||
<method name="<init>" signature="()V" line-rate="1.0" branch-rate="1.0">
|
||||
<lines>
|
||||
<line number="10" hits="3" branch="false"/>
|
||||
</lines>
|
||||
</method>
|
||||
<method name="doSearch" signature="()V" line-rate="1.0" branch-rate="1.0">
|
||||
<lines>
|
||||
<line number="23" hits="3" branch="false"/>
|
||||
<line number="25" hits="3" branch="false"/>
|
||||
<line number="26" hits="3" branch="false"/>
|
||||
<line number="28" hits="3" branch="false"/>
|
||||
<line number="29" hits="3" branch="false"/>
|
||||
<line number="30" hits="3" branch="false"/>
|
||||
</lines>
|
||||
</method>
|
||||
<method name="main" signature="([Ljava/lang/String;)V" line-rate="1.0" branch-rate="1.0">
|
||||
<lines>
|
||||
<line number="16" hits="3" branch="false"/>
|
||||
<line number="17" hits="3" branch="false"/>
|
||||
<line number="18" hits="3" branch="false"/>
|
||||
<line number="19" hits="3" branch="false"/>
|
||||
</lines>
|
||||
</method>
|
||||
</methods>
|
||||
<lines>
|
||||
<line number="10" hits="3" branch="false"/>
|
||||
<line number="16" hits="3" branch="false"/>
|
||||
<line number="17" hits="3" branch="false"/>
|
||||
<line number="18" hits="3" branch="false"/>
|
||||
<line number="19" hits="3" branch="false"/>
|
||||
<line number="23" hits="3" branch="false"/>
|
||||
<line number="25" hits="3" branch="false"/>
|
||||
<line number="26" hits="3" branch="false"/>
|
||||
<line number="28" hits="3" branch="false"/>
|
||||
<line number="29" hits="3" branch="false"/>
|
||||
<line number="30" hits="3" branch="false"/>
|
||||
</lines>
|
||||
</class>
|
||||
</classes>
|
||||
</package>
|
||||
<package name="search" line-rate="0.8421052631578947" branch-rate="0.75" complexity="3.25">
|
||||
<classes>
|
||||
<class name="search.BinarySearch" filename="search/BinarySearch.java" line-rate="0.9166666666666666" branch-rate="0.8333333333333334" complexity="3.0">
|
||||
<methods>
|
||||
<method name="<init>" signature="()V" line-rate="1.0" branch-rate="1.0">
|
||||
<lines>
|
||||
<line number="12" hits="3" branch="false"/>
|
||||
</lines>
|
||||
</method>
|
||||
<method name="find" signature="([II)I" line-rate="0.9090909090909091" branch-rate="0.8333333333333334">
|
||||
<lines>
|
||||
<line number="16" hits="3" branch="false"/>
|
||||
<line number="18" hits="12" branch="true" condition-coverage="100% (2/2)">
|
||||
<conditions>
|
||||
<condition number="0" type="jump" coverage="100%"/>
|
||||
</conditions>
|
||||
</line>
|
||||
<line number="20" hits="9" branch="false"/>
|
||||
<line number="21" hits="9" branch="false"/>
|
||||
<line number="23" hits="9" branch="true" condition-coverage="50% (1/2)">
|
||||
<conditions>
|
||||
<condition number="0" type="jump" coverage="50%"/>
|
||||
</conditions>
|
||||
</line>
|
||||
<line number="24" hits="0" branch="false"/>
|
||||
<line number="25" hits="9" branch="true" condition-coverage="100% (2/2)">
|
||||
<conditions>
|
||||
<condition number="0" type="jump" coverage="100%"/>
|
||||
</conditions>
|
||||
</line>
|
||||
<line number="26" hits="6" branch="false"/>
|
||||
<line number="28" hits="3" branch="false"/>
|
||||
<line number="29" hits="9" branch="false"/>
|
||||
<line number="31" hits="3" branch="false"/>
|
||||
</lines>
|
||||
</method>
|
||||
</methods>
|
||||
<lines>
|
||||
<line number="12" hits="3" branch="false"/>
|
||||
<line number="16" hits="3" branch="false"/>
|
||||
<line number="18" hits="12" branch="true" condition-coverage="100% (2/2)">
|
||||
<conditions>
|
||||
<condition number="0" type="jump" coverage="100%"/>
|
||||
</conditions>
|
||||
</line>
|
||||
<line number="20" hits="9" branch="false"/>
|
||||
<line number="21" hits="9" branch="false"/>
|
||||
<line number="23" hits="9" branch="true" condition-coverage="50% (1/2)">
|
||||
<conditions>
|
||||
<condition number="0" type="jump" coverage="50%"/>
|
||||
</conditions>
|
||||
</line>
|
||||
<line number="24" hits="0" branch="false"/>
|
||||
<line number="25" hits="9" branch="true" condition-coverage="100% (2/2)">
|
||||
<conditions>
|
||||
<condition number="0" type="jump" coverage="100%"/>
|
||||
</conditions>
|
||||
</line>
|
||||
<line number="26" hits="6" branch="false"/>
|
||||
<line number="28" hits="3" branch="false"/>
|
||||
<line number="29" hits="9" branch="false"/>
|
||||
<line number="31" hits="3" branch="false"/>
|
||||
</lines>
|
||||
</class>
|
||||
<class name="search.ISortedArraySearch" filename="search/ISortedArraySearch.java" line-rate="1.0" branch-rate="1.0" complexity="1.0">
|
||||
<methods>
|
||||
</methods>
|
||||
<lines>
|
||||
</lines>
|
||||
</class>
|
||||
<class name="search.LinearSearch" filename="search/LinearSearch.java" line-rate="0.7142857142857143" branch-rate="0.6666666666666666" complexity="6.0">
|
||||
<methods>
|
||||
<method name="<init>" signature="()V" line-rate="1.0" branch-rate="1.0">
|
||||
<lines>
|
||||
<line number="9" hits="3" branch="false"/>
|
||||
</lines>
|
||||
</method>
|
||||
<method name="find" signature="([II)I" line-rate="0.6666666666666666" branch-rate="0.6666666666666666">
|
||||
<lines>
|
||||
<line number="13" hits="9" branch="true" condition-coverage="50% (1/2)">
|
||||
<conditions>
|
||||
<condition number="0" type="jump" coverage="50%"/>
|
||||
</conditions>
|
||||
</line>
|
||||
<line number="15" hits="9" branch="true" condition-coverage="100% (2/2)">
|
||||
<conditions>
|
||||
<condition number="0" type="jump" coverage="100%"/>
|
||||
</conditions>
|
||||
</line>
|
||||
<line number="16" hits="3" branch="false"/>
|
||||
<line number="17" hits="6" branch="true" condition-coverage="50% (1/2)">
|
||||
<conditions>
|
||||
<condition number="0" type="jump" coverage="50%"/>
|
||||
</conditions>
|
||||
</line>
|
||||
<line number="19" hits="0" branch="false"/>
|
||||
<line number="24" hits="0" branch="false"/>
|
||||
</lines>
|
||||
</method>
|
||||
</methods>
|
||||
<lines>
|
||||
<line number="9" hits="3" branch="false"/>
|
||||
<line number="13" hits="9" branch="true" condition-coverage="50% (1/2)">
|
||||
<conditions>
|
||||
<condition number="0" type="jump" coverage="50%"/>
|
||||
</conditions>
|
||||
</line>
|
||||
<line number="15" hits="9" branch="true" condition-coverage="100% (2/2)">
|
||||
<conditions>
|
||||
<condition number="0" type="jump" coverage="100%"/>
|
||||
</conditions>
|
||||
</line>
|
||||
<line number="16" hits="3" branch="false"/>
|
||||
<line number="17" hits="6" branch="true" condition-coverage="50% (1/2)">
|
||||
<conditions>
|
||||
<condition number="0" type="jump" coverage="50%"/>
|
||||
</conditions>
|
||||
</line>
|
||||
<line number="19" hits="0" branch="false"/>
|
||||
<line number="24" hits="0" branch="false"/>
|
||||
</lines>
|
||||
</class>
|
||||
</classes>
|
||||
</package>
|
||||
</packages>
|
||||
</coverage>
|
|
@ -0,0 +1,209 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"flag"
|
||||
"fmt"
|
||||
"go/token"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/axw/gocov"
|
||||
)
|
||||
|
||||
// Coverage information
|
||||
type Coverage struct {
|
||||
XMLName xml.Name `xml:"coverage"`
|
||||
LineRate float32 `xml:"line-rate,attr"`
|
||||
BranchRate float32 `xml:"branch-rate,attr"`
|
||||
LinesCovered float32 `xml:"lines-covered,attr"`
|
||||
LinesValid int64 `xml:"lines-valid,attr"`
|
||||
BranchesCovered int64 `xml:"branches-covered,attr"`
|
||||
BranchesValid int64 `xml:"branches-valid,attr"`
|
||||
Complexity float32 `xml:"complexity,attr"`
|
||||
Version string `xml:"version,attr"`
|
||||
Timestamp int64 `xml:"timestamp,attr"`
|
||||
Packages []Package `xml:"packages>package"`
|
||||
Sources []string `xml:"sources>source"`
|
||||
}
|
||||
|
||||
// Package information
|
||||
type Package struct {
|
||||
Name string `xml:"name,attr"`
|
||||
LineRate float32 `xml:"line-rate,attr"`
|
||||
BranchRate float32 `xml:"branch-rate,attr"`
|
||||
Complexity float32 `xml:"complexity,attr"`
|
||||
Classes []Class `xml:"classes>class"`
|
||||
LineCount int64 `xml:"line-count,attr"`
|
||||
LineHits int64 `xml:"line-hits,attr"`
|
||||
}
|
||||
|
||||
// Class information
|
||||
type Class struct {
|
||||
Name string `xml:"name,attr"`
|
||||
Filename string `xml:"filename,attr"`
|
||||
LineRate float32 `xml:"line-rate,attr"`
|
||||
BranchRate float32 `xml:"branch-rate,attr"`
|
||||
Complexity float32 `xml:"complexity,attr"`
|
||||
Methods []Method `xml:"methods>method"`
|
||||
Lines []Line `xml:"lines>line"`
|
||||
LineCount int64 `xml:"line-count,attr"`
|
||||
LineHits int64 `xml:"line-hits,attr"`
|
||||
}
|
||||
|
||||
// Method information
|
||||
type Method struct {
|
||||
Name string `xml:"name,attr"`
|
||||
Signature string `xml:"signature,attr"`
|
||||
LineRate float32 `xml:"line-rate,attr"`
|
||||
BranchRate float32 `xml:"branch-rate,attr"`
|
||||
Complexity float32 `xml:"complexity,attr"`
|
||||
Lines []Line `xml:"lines>line"`
|
||||
LineCount int64 `xml:"line-count,attr"`
|
||||
LineHits int64 `xml:"line-hits,attr"`
|
||||
}
|
||||
|
||||
// Line information
|
||||
type Line struct {
|
||||
Number int `xml:"number,attr"`
|
||||
Hits int64 `xml:"hits,attr"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
sourcePathPtr := flag.String(
|
||||
"source",
|
||||
"",
|
||||
"Absolute path to source. Defaults to current working directory.",
|
||||
)
|
||||
|
||||
flag.Parse()
|
||||
|
||||
// Parse the commandline arguments.
|
||||
var sourcePath string
|
||||
var err error
|
||||
if *sourcePathPtr != "" {
|
||||
sourcePath = *sourcePathPtr
|
||||
if !filepath.IsAbs(sourcePath) {
|
||||
panic(fmt.Sprintf("Source path is a relative path: %s", sourcePath))
|
||||
}
|
||||
} else {
|
||||
sourcePath, err = os.Getwd()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
sources := make([]string, 1)
|
||||
sources[0] = sourcePath
|
||||
var r struct{ Packages []gocov.Package }
|
||||
var totalLines, totalHits int64
|
||||
err = json.NewDecoder(os.Stdin).Decode(&r)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fset := token.NewFileSet()
|
||||
tokenFiles := make(map[string]*token.File)
|
||||
|
||||
// convert packages
|
||||
packages := make([]Package, len(r.Packages))
|
||||
for i, gPackage := range r.Packages {
|
||||
// group functions by filename and "class" (type)
|
||||
files := make(map[string]map[string]*Class)
|
||||
for _, gFunction := range gPackage.Functions {
|
||||
// get the releative path by base path.
|
||||
fpath, err := filepath.Rel(sourcePath, gFunction.File)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
classes := files[fpath]
|
||||
if classes == nil {
|
||||
// group functions by "class" (type) in a File
|
||||
classes = make(map[string]*Class)
|
||||
files[fpath] = classes
|
||||
}
|
||||
|
||||
s := strings.Split("-."+gFunction.Name, ".") // className is "-" for package-level functions
|
||||
className, methodName := s[len(s)-2], s[len(s)-1]
|
||||
class := classes[className]
|
||||
if class == nil {
|
||||
class = &Class{Name: className, Filename: fpath, Methods: []Method{}, Lines: []Line{}}
|
||||
classes[className] = class
|
||||
}
|
||||
|
||||
// from github.com/axw/gocov /gocov/annotate.go#printFunctionSource
|
||||
// Load the file for line information. Probably overkill, maybe
|
||||
// just compute the lines from offsets in here.
|
||||
setContent := false
|
||||
tokenFile := tokenFiles[gFunction.File]
|
||||
if tokenFile == nil {
|
||||
info, err := os.Stat(gFunction.File)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
tokenFile = fset.AddFile(gFunction.File, fset.Base(), int(info.Size()))
|
||||
setContent = true
|
||||
}
|
||||
|
||||
tokenData, err := ioutil.ReadFile(gFunction.File)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if setContent {
|
||||
// This processes the content and records line number info.
|
||||
tokenFile.SetLinesForContent(tokenData)
|
||||
}
|
||||
|
||||
// convert statements to lines
|
||||
lines := make([]Line, len(gFunction.Statements))
|
||||
var funcHits int
|
||||
for i, s := range gFunction.Statements {
|
||||
lineno := tokenFile.Line(tokenFile.Pos(s.Start))
|
||||
line := Line{Number: lineno, Hits: s.Reached}
|
||||
if int(s.Reached) > 0 {
|
||||
funcHits++
|
||||
}
|
||||
lines[i] = line
|
||||
class.Lines = append(class.Lines, line)
|
||||
}
|
||||
lineRate := float32(funcHits) / float32(len(gFunction.Statements))
|
||||
|
||||
class.Methods = append(class.Methods, Method{Name: methodName, Lines: lines, LineRate: lineRate})
|
||||
class.LineCount += int64(len(gFunction.Statements))
|
||||
class.LineHits += int64(funcHits)
|
||||
}
|
||||
|
||||
// fill package with "classes"
|
||||
p := Package{Name: gPackage.Name, Classes: []Class{}}
|
||||
for _, classes := range files {
|
||||
for _, class := range classes {
|
||||
p.LineCount += class.LineCount
|
||||
p.LineHits += class.LineHits
|
||||
class.LineRate = float32(class.LineHits) / float32(class.LineCount)
|
||||
p.Classes = append(p.Classes, *class)
|
||||
}
|
||||
p.LineRate = float32(p.LineHits) / float32(p.LineCount)
|
||||
}
|
||||
packages[i] = p
|
||||
totalLines += p.LineCount
|
||||
totalHits += p.LineHits
|
||||
}
|
||||
|
||||
coverage := Coverage{Sources: sources, Packages: packages, Timestamp: time.Now().UnixNano() / int64(time.Millisecond), LinesCovered: float32(totalHits), LinesValid: int64(totalLines), LineRate: float32(totalHits) / float32(totalLines)}
|
||||
|
||||
fmt.Printf(xml.Header)
|
||||
fmt.Printf("<!DOCTYPE coverage SYSTEM \"http://cobertura.sourceforge.net/xml/coverage-04.dtd\">\n")
|
||||
|
||||
encoder := xml.NewEncoder(os.Stdout)
|
||||
encoder.Indent("", "\t")
|
||||
err = encoder.Encode(coverage)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
_testmain.go
|
||||
|
||||
*.exe
|
|
@ -0,0 +1 @@
|
|||
language: go
|
|
@ -0,0 +1,5 @@
|
|||
# List of gocov authors.
|
||||
|
||||
Andrew Wilkins <axwalk@gmail.com>
|
||||
Dave Cheney <dave@cheney.net>
|
||||
Greg Ward <greg@gerg.ca>
|
|
@ -0,0 +1,50 @@
|
|||
Copyright (c) 2012 The Gocov Authors.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
Copyright (c) 2012 The Go Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
# gocov
|
||||
|
||||
Coverage reporting tool for The Go Programming Language
|
||||
|
||||
[![Build Status](https://travis-ci.org/axw/gocov.svg?branch=master)](https://travis-ci.org/axw/gocov)
|
||||
|
||||
## Installation
|
||||
|
||||
```go get github.com/axw/gocov/gocov```
|
||||
|
||||
## Usage
|
||||
|
||||
There are currently four gocov commands: ```test```, ```convert```, ```report``` and ```annotate```.
|
||||
|
||||
#### gocov test
|
||||
|
||||
Running `gocov test [args...]` will run `go test [args...]` with
|
||||
an implicit `-coverprofile` added, and then output the result of
|
||||
`gocov convert` with the profile.
|
||||
|
||||
#### gocov convert
|
||||
|
||||
Running `gocov convert <coverprofile>` will convert a coverage
|
||||
profile generated by `go tool cover` to gocov's JSON interchange
|
||||
format. For example:
|
||||
|
||||
go test -coverprofile=c.out
|
||||
gocov convert c.out | gocov annotate -
|
||||
|
||||
#### gocov report
|
||||
|
||||
Running `gocov report <coverage.json>` will generate a textual
|
||||
report from the coverage data output by `gocov convert`. It is
|
||||
assumed that the source code has not changed in between.
|
||||
|
||||
Output from ```gocov test``` is printed to stdout so users can
|
||||
pipe the output to ```gocov report``` to view a summary of the test
|
||||
coverage, for example: -
|
||||
|
||||
gocov test | gocov report
|
||||
|
||||
#### gocov annotate
|
||||
|
||||
Running `gocov annotate <coverage.json> <package[.receiver].function>`
|
||||
will generate a source listing of the specified function, annotating
|
||||
it with coverage information, such as which lines have been missed.
|
||||
|
||||
## Related tools and services
|
||||
|
||||
[GoCovGUI](http://github.com/nsf/gocovgui/):
|
||||
A simple GUI wrapper for the gocov coverage analysis tool.
|
||||
|
||||
[gocov-html](https://github.com/matm/gocov-html):
|
||||
A simple helper tool for generating HTML output from gocov.
|
||||
|
||||
[gocov-xml](https://github.com/AlekSi/gocov-xml):
|
||||
A simple helper tool for generating XML output in Cobertura format for CIs like Jenkins and others from gocov.
|
|
@ -0,0 +1,5 @@
|
|||
module github.com/axw/gocov
|
||||
|
||||
go 1.12
|
||||
|
||||
require golang.org/x/tools v0.0.0-20190617190820-da514acc4774
|
|
@ -0,0 +1,7 @@
|
|||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/tools v0.0.0-20190617190820-da514acc4774 h1:CQVOmarCBFzTx0kbOU0ru54Cvot8SdSrNYjZPhQl+gk=
|
||||
golang.org/x/tools v0.0.0-20190617190820-da514acc4774/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
|
@ -0,0 +1,115 @@
|
|||
// Copyright (c) 2012 The Gocov Authors.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to
|
||||
// deal in the Software without restriction, including without limitation the
|
||||
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
// sell copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
// IN THE SOFTWARE.
|
||||
|
||||
// Package gocov is a code coverage analysis tool for Go.
|
||||
package gocov
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type Package struct {
|
||||
// Name is the canonical path of the package.
|
||||
Name string
|
||||
|
||||
// Functions is a list of functions registered with this package.
|
||||
Functions []*Function
|
||||
}
|
||||
|
||||
type Function struct {
|
||||
// Name is the name of the function. If the function has a receiver, the
|
||||
// name will be of the form T.N, where T is the type and N is the name.
|
||||
Name string
|
||||
|
||||
// File is the full path to the file in which the function is defined.
|
||||
File string
|
||||
|
||||
// Start is the start offset of the function's signature.
|
||||
Start int
|
||||
|
||||
// End is the end offset of the function.
|
||||
End int
|
||||
|
||||
// statements registered with this function.
|
||||
Statements []*Statement
|
||||
}
|
||||
|
||||
type Statement struct {
|
||||
// Start is the start offset of the statement.
|
||||
Start int
|
||||
|
||||
// End is the end offset of the statement.
|
||||
End int
|
||||
|
||||
// Reached is the number of times the statement was reached.
|
||||
Reached int64
|
||||
}
|
||||
|
||||
// Accumulate will accumulate the coverage information from the provided
|
||||
// Package into this Package.
|
||||
func (p *Package) Accumulate(p2 *Package) error {
|
||||
if p.Name != p2.Name {
|
||||
return fmt.Errorf("Names do not match: %q != %q", p.Name, p2.Name)
|
||||
}
|
||||
if len(p.Functions) != len(p2.Functions) {
|
||||
return fmt.Errorf("Function counts do not match: %d != %d", len(p.Functions), len(p2.Functions))
|
||||
}
|
||||
for i, f := range p.Functions {
|
||||
err := f.Accumulate(p2.Functions[i])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Accumulate will accumulate the coverage information from the provided
|
||||
// Function into this Function.
|
||||
func (f *Function) Accumulate(f2 *Function) error {
|
||||
if f.Name != f2.Name {
|
||||
return fmt.Errorf("Names do not match: %q != %q", f.Name, f2.Name)
|
||||
}
|
||||
if f.File != f2.File {
|
||||
return fmt.Errorf("Files do not match: %q != %q", f.File, f2.File)
|
||||
}
|
||||
if f.Start != f2.Start || f.End != f2.End {
|
||||
return fmt.Errorf("Source ranges do not match: %d-%d != %d-%d", f.Start, f.End, f2.Start, f2.End)
|
||||
}
|
||||
if len(f.Statements) != len(f2.Statements) {
|
||||
return fmt.Errorf("Number of statements do not match: %d != %d", len(f.Statements), len(f2.Statements))
|
||||
}
|
||||
for i, s := range f.Statements {
|
||||
err := s.Accumulate(f2.Statements[i])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Accumulate will accumulate the coverage information from the provided
|
||||
// Statement into this Statement.
|
||||
func (s *Statement) Accumulate(s2 *Statement) error {
|
||||
if s.Start != s2.Start || s.End != s2.End {
|
||||
return fmt.Errorf("Source ranges do not match: %d-%d != %d-%d", s.Start, s.End, s2.Start, s2.End)
|
||||
}
|
||||
s.Reached += s2.Reached
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,238 @@
|
|||
// Copyright (c) 2012 The Gocov Authors.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to
|
||||
// deal in the Software without restriction, including without limitation the
|
||||
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
// sell copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
// IN THE SOFTWARE.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"go/token"
|
||||
"io/ioutil"
|
||||
"math"
|
||||
"os"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/axw/gocov"
|
||||
)
|
||||
|
||||
const (
|
||||
hitPrefix = " "
|
||||
missPrefix = "MISS"
|
||||
RED = "\x1b[31;1m"
|
||||
GREEN = "\x1b[32;1m"
|
||||
NONE = "\x1b[0m"
|
||||
)
|
||||
|
||||
var (
|
||||
annotateFlags = flag.NewFlagSet("annotate", flag.ExitOnError)
|
||||
annotateCeilingFlag = annotateFlags.Float64(
|
||||
"ceiling", 101,
|
||||
"Annotate only functions whose coverage is less than the specified percentage")
|
||||
annotateColorFlag = annotateFlags.Bool(
|
||||
"color", false,
|
||||
"Differentiate coverage with color")
|
||||
)
|
||||
|
||||
type packageList []*gocov.Package
|
||||
type functionList []*gocov.Function
|
||||
|
||||
func (l packageList) Len() int {
|
||||
return len(l)
|
||||
}
|
||||
|
||||
func (l packageList) Less(i, j int) bool {
|
||||
return l[i].Name < l[j].Name
|
||||
}
|
||||
|
||||
func (l packageList) Swap(i, j int) {
|
||||
l[i], l[j] = l[j], l[i]
|
||||
}
|
||||
|
||||
func (l functionList) Len() int {
|
||||
return len(l)
|
||||
}
|
||||
|
||||
func (l functionList) Less(i, j int) bool {
|
||||
return l[i].Name < l[j].Name
|
||||
}
|
||||
|
||||
func (l functionList) Swap(i, j int) {
|
||||
l[i], l[j] = l[j], l[i]
|
||||
}
|
||||
|
||||
type annotator struct {
|
||||
fset *token.FileSet
|
||||
files map[string]*token.File
|
||||
}
|
||||
|
||||
func percentReached(fn *gocov.Function) float64 {
|
||||
if len(fn.Statements) == 0 {
|
||||
return 0
|
||||
}
|
||||
var reached int
|
||||
for _, stmt := range fn.Statements {
|
||||
if stmt.Reached > 0 {
|
||||
reached++
|
||||
}
|
||||
}
|
||||
return float64(reached) / float64(len(fn.Statements)) * 100
|
||||
}
|
||||
|
||||
func annotateSource() (rc int) {
|
||||
annotateFlags.Parse(os.Args[2:])
|
||||
if annotateFlags.NArg() == 0 {
|
||||
fmt.Fprintf(os.Stderr, "missing coverage file\n")
|
||||
return 1
|
||||
}
|
||||
|
||||
var data []byte
|
||||
var err error
|
||||
if filename := annotateFlags.Arg(0); filename == "-" {
|
||||
data, err = ioutil.ReadAll(os.Stdin)
|
||||
} else {
|
||||
data, err = ioutil.ReadFile(filename)
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to read coverage file: %s\n", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
packages, err := unmarshalJson(data)
|
||||
if err != nil {
|
||||
fmt.Fprintf(
|
||||
os.Stderr, "failed to unmarshal coverage data: %s\n", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
// Sort packages, functions by name.
|
||||
sort.Sort(packageList(packages))
|
||||
for _, pkg := range packages {
|
||||
sort.Sort(functionList(pkg.Functions))
|
||||
}
|
||||
|
||||
a := &annotator{}
|
||||
a.fset = token.NewFileSet()
|
||||
a.files = make(map[string]*token.File)
|
||||
|
||||
var regexps []*regexp.Regexp
|
||||
for _, arg := range annotateFlags.Args()[1:] {
|
||||
re, err := regexp.Compile(arg)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "warning: failed to compile %q as a regular expression, ignoring\n", arg)
|
||||
} else {
|
||||
regexps = append(regexps, re)
|
||||
}
|
||||
}
|
||||
if len(regexps) == 0 {
|
||||
regexps = append(regexps, regexp.MustCompile("."))
|
||||
}
|
||||
for _, pkg := range packages {
|
||||
for _, fn := range pkg.Functions {
|
||||
if percentReached(fn) >= *annotateCeilingFlag {
|
||||
continue
|
||||
}
|
||||
name := pkg.Name + "/" + fn.Name
|
||||
for _, regexp := range regexps {
|
||||
if regexp.FindStringIndex(name) != nil {
|
||||
err := a.printFunctionSource(fn)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "warning: failed to annotate function %q\n", name)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (a *annotator) printFunctionSource(fn *gocov.Function) error {
|
||||
// Load the file for line information. Probably overkill, maybe
|
||||
// just compute the lines from offsets in here.
|
||||
setContent := false
|
||||
file := a.files[fn.File]
|
||||
if file == nil {
|
||||
info, err := os.Stat(fn.File)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
file = a.fset.AddFile(fn.File, a.fset.Base(), int(info.Size()))
|
||||
setContent = true
|
||||
}
|
||||
|
||||
data, err := ioutil.ReadFile(fn.File)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if setContent {
|
||||
// This processes the content and records line number info.
|
||||
file.SetLinesForContent(data)
|
||||
}
|
||||
|
||||
statements := fn.Statements[:]
|
||||
lineno := file.Line(file.Pos(fn.Start))
|
||||
lines := strings.Split(string(data)[fn.Start:fn.End], "\n")
|
||||
linenoWidth := int(math.Log10(float64(lineno+len(lines)))) + 1
|
||||
fmt.Println()
|
||||
for i, line := range lines {
|
||||
// Go through statements one at a time, seeing if we've hit
|
||||
// them or not.
|
||||
//
|
||||
// The prefix approach isn't perfect, as it doesn't
|
||||
// distinguish multiple statements per line. It'll have to
|
||||
// do for now. We could do fancy ANSI colouring later.
|
||||
lineno := lineno + i
|
||||
statementFound := false
|
||||
hit := false
|
||||
for j := 0; j < len(statements); j++ {
|
||||
start := file.Line(file.Pos(statements[j].Start))
|
||||
// FIXME instrumentation no longer records statements
|
||||
// in line order, as function literals are processed
|
||||
// after the body of a function. If/when that's changed,
|
||||
// we can go back to checking just the first statement
|
||||
// in each loop.
|
||||
if start == lineno {
|
||||
statementFound = true
|
||||
if !hit && statements[j].Reached > 0 {
|
||||
hit = true
|
||||
}
|
||||
statements = append(statements[:j], statements[j+1:]...)
|
||||
}
|
||||
}
|
||||
if *annotateColorFlag {
|
||||
color := NONE
|
||||
if statementFound && !hit {
|
||||
color = RED
|
||||
}
|
||||
fmt.Printf("%s%*d \t%s%s\n", color, linenoWidth, lineno, line, NONE)
|
||||
} else {
|
||||
hitmiss := hitPrefix
|
||||
if statementFound && !hit {
|
||||
hitmiss = missPrefix
|
||||
}
|
||||
fmt.Printf("%*d %s\t%s\n", linenoWidth, lineno, hitmiss, line)
|
||||
}
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,313 @@
|
|||
// Copyright (c) 2013 The Gocov Authors.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to
|
||||
// deal in the Software without restriction, including without limitation the
|
||||
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
// sell copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
// IN THE SOFTWARE.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/build"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"path/filepath"
|
||||
|
||||
"golang.org/x/tools/cover"
|
||||
|
||||
"github.com/axw/gocov"
|
||||
"github.com/axw/gocov/gocovutil"
|
||||
)
|
||||
|
||||
func convertProfiles(filenames ...string) error {
|
||||
var ps gocovutil.Packages
|
||||
for i := range filenames {
|
||||
converter := converter{
|
||||
packages: make(map[string]*gocov.Package),
|
||||
}
|
||||
profiles, err := cover.ParseProfiles(filenames[i])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, p := range profiles {
|
||||
if err := converter.convertProfile(p); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, pkg := range converter.packages {
|
||||
ps.AddPackage(pkg)
|
||||
}
|
||||
}
|
||||
bytes, err := marshalJson(ps)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(string(bytes))
|
||||
return nil
|
||||
}
|
||||
|
||||
type converter struct {
|
||||
packages map[string]*gocov.Package
|
||||
}
|
||||
|
||||
// wrapper for gocov.Statement
|
||||
type statement struct {
|
||||
*gocov.Statement
|
||||
*StmtExtent
|
||||
}
|
||||
|
||||
func (c *converter) convertProfile(p *cover.Profile) error {
|
||||
file, pkgpath, err := findFile(p.FileName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pkg := c.packages[pkgpath]
|
||||
if pkg == nil {
|
||||
pkg = &gocov.Package{Name: pkgpath}
|
||||
c.packages[pkgpath] = pkg
|
||||
}
|
||||
// Find function and statement extents; create corresponding
|
||||
// gocov.Functions and gocov.Statements, and keep a separate
|
||||
// slice of gocov.Statements so we can match them with profile
|
||||
// blocks.
|
||||
extents, err := findFuncs(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var stmts []statement
|
||||
for _, fe := range extents {
|
||||
f := &gocov.Function{
|
||||
Name: fe.name,
|
||||
File: file,
|
||||
Start: fe.startOffset,
|
||||
End: fe.endOffset,
|
||||
}
|
||||
for _, se := range fe.stmts {
|
||||
s := statement{
|
||||
Statement: &gocov.Statement{Start: se.startOffset, End: se.endOffset},
|
||||
StmtExtent: se,
|
||||
}
|
||||
f.Statements = append(f.Statements, s.Statement)
|
||||
stmts = append(stmts, s)
|
||||
}
|
||||
pkg.Functions = append(pkg.Functions, f)
|
||||
}
|
||||
// For each profile block in the file, find the statement(s) it
|
||||
// covers and increment the Reached field(s).
|
||||
blocks := p.Blocks
|
||||
for _, s := range stmts {
|
||||
for i, b := range blocks {
|
||||
if b.StartLine > s.endLine || (b.StartLine == s.endLine && b.StartCol >= s.endCol) {
|
||||
// Past the end of the statement
|
||||
blocks = blocks[i:]
|
||||
break
|
||||
}
|
||||
if b.EndLine < s.startLine || (b.EndLine == s.startLine && b.EndCol <= s.startCol) {
|
||||
// Before the beginning of the statement
|
||||
continue
|
||||
}
|
||||
s.Reached += int64(b.Count)
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// findFile finds the location of the named file in GOROOT, GOPATH etc.
|
||||
func findFile(file string) (filename string, pkgpath string, err error) {
|
||||
dir, file := filepath.Split(file)
|
||||
if dir != "" {
|
||||
dir = dir[:len(dir)-1] // drop trailing '/'
|
||||
}
|
||||
pkg, err := build.Import(dir, ".", build.FindOnly)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("can't find %q: %v", file, err)
|
||||
}
|
||||
return filepath.Join(pkg.Dir, file), pkg.ImportPath, nil
|
||||
}
|
||||
|
||||
// findFuncs parses the file and returns a slice of FuncExtent descriptors.
|
||||
func findFuncs(name string) ([]*FuncExtent, error) {
|
||||
fset := token.NewFileSet()
|
||||
parsedFile, err := parser.ParseFile(fset, name, nil, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
visitor := &FuncVisitor{fset: fset}
|
||||
ast.Walk(visitor, parsedFile)
|
||||
return visitor.funcs, nil
|
||||
}
|
||||
|
||||
type extent struct {
|
||||
startOffset int
|
||||
startLine int
|
||||
startCol int
|
||||
endOffset int
|
||||
endLine int
|
||||
endCol int
|
||||
}
|
||||
|
||||
// FuncExtent describes a function's extent in the source by file and position.
|
||||
type FuncExtent struct {
|
||||
extent
|
||||
name string
|
||||
stmts []*StmtExtent
|
||||
}
|
||||
|
||||
// StmtExtent describes a statements's extent in the source by file and position.
|
||||
type StmtExtent extent
|
||||
|
||||
// FuncVisitor implements the visitor that builds the function position list for a file.
|
||||
type FuncVisitor struct {
|
||||
fset *token.FileSet
|
||||
funcs []*FuncExtent
|
||||
}
|
||||
|
||||
// Visit implements the ast.Visitor interface.
|
||||
func (v *FuncVisitor) Visit(node ast.Node) ast.Visitor {
|
||||
var body *ast.BlockStmt
|
||||
var name string
|
||||
switch n := node.(type) {
|
||||
case *ast.FuncLit:
|
||||
body = n.Body
|
||||
case *ast.FuncDecl:
|
||||
body = n.Body
|
||||
name = n.Name.Name
|
||||
// Function name is prepended with "T." if there is a receiver, where
|
||||
// T is the type of the receiver, dereferenced if it is a pointer.
|
||||
if n.Recv != nil {
|
||||
field := n.Recv.List[0]
|
||||
switch recv := field.Type.(type) {
|
||||
case *ast.StarExpr:
|
||||
name = recv.X.(*ast.Ident).Name + "." + name
|
||||
case *ast.Ident:
|
||||
name = recv.Name + "." + name
|
||||
}
|
||||
}
|
||||
}
|
||||
if body != nil {
|
||||
start := v.fset.Position(node.Pos())
|
||||
end := v.fset.Position(node.End())
|
||||
if name == "" {
|
||||
name = fmt.Sprintf("@%d:%d", start.Line, start.Column)
|
||||
}
|
||||
fe := &FuncExtent{
|
||||
name: name,
|
||||
extent: extent{
|
||||
startOffset: start.Offset,
|
||||
startLine: start.Line,
|
||||
startCol: start.Column,
|
||||
endOffset: end.Offset,
|
||||
endLine: end.Line,
|
||||
endCol: end.Column,
|
||||
},
|
||||
}
|
||||
v.funcs = append(v.funcs, fe)
|
||||
sv := StmtVisitor{fset: v.fset, function: fe}
|
||||
sv.VisitStmt(body)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
type StmtVisitor struct {
|
||||
fset *token.FileSet
|
||||
function *FuncExtent
|
||||
}
|
||||
|
||||
func (v *StmtVisitor) VisitStmt(s ast.Stmt) {
|
||||
var statements *[]ast.Stmt
|
||||
switch s := s.(type) {
|
||||
case *ast.BlockStmt:
|
||||
statements = &s.List
|
||||
case *ast.CaseClause:
|
||||
statements = &s.Body
|
||||
case *ast.CommClause:
|
||||
statements = &s.Body
|
||||
case *ast.ForStmt:
|
||||
if s.Init != nil {
|
||||
v.VisitStmt(s.Init)
|
||||
}
|
||||
if s.Post != nil {
|
||||
v.VisitStmt(s.Post)
|
||||
}
|
||||
v.VisitStmt(s.Body)
|
||||
case *ast.IfStmt:
|
||||
if s.Init != nil {
|
||||
v.VisitStmt(s.Init)
|
||||
}
|
||||
v.VisitStmt(s.Body)
|
||||
if s.Else != nil {
|
||||
// Code copied from go.tools/cmd/cover, to deal with "if x {} else if y {}"
|
||||
const backupToElse = token.Pos(len("else ")) // The AST doesn't remember the else location. We can make an accurate guess.
|
||||
switch stmt := s.Else.(type) {
|
||||
case *ast.IfStmt:
|
||||
block := &ast.BlockStmt{
|
||||
Lbrace: stmt.If - backupToElse, // So the covered part looks like it starts at the "else".
|
||||
List: []ast.Stmt{stmt},
|
||||
Rbrace: stmt.End(),
|
||||
}
|
||||
s.Else = block
|
||||
case *ast.BlockStmt:
|
||||
stmt.Lbrace -= backupToElse // So the block looks like it starts at the "else".
|
||||
default:
|
||||
panic("unexpected node type in if")
|
||||
}
|
||||
v.VisitStmt(s.Else)
|
||||
}
|
||||
case *ast.LabeledStmt:
|
||||
v.VisitStmt(s.Stmt)
|
||||
case *ast.RangeStmt:
|
||||
v.VisitStmt(s.Body)
|
||||
case *ast.SelectStmt:
|
||||
v.VisitStmt(s.Body)
|
||||
case *ast.SwitchStmt:
|
||||
if s.Init != nil {
|
||||
v.VisitStmt(s.Init)
|
||||
}
|
||||
v.VisitStmt(s.Body)
|
||||
case *ast.TypeSwitchStmt:
|
||||
if s.Init != nil {
|
||||
v.VisitStmt(s.Init)
|
||||
}
|
||||
v.VisitStmt(s.Assign)
|
||||
v.VisitStmt(s.Body)
|
||||
}
|
||||
if statements == nil {
|
||||
return
|
||||
}
|
||||
for i := 0; i < len(*statements); i++ {
|
||||
s := (*statements)[i]
|
||||
switch s.(type) {
|
||||
case *ast.CaseClause, *ast.CommClause, *ast.BlockStmt:
|
||||
break
|
||||
default:
|
||||
start, end := v.fset.Position(s.Pos()), v.fset.Position(s.End())
|
||||
se := &StmtExtent{
|
||||
startOffset: start.Offset,
|
||||
startLine: start.Line,
|
||||
startCol: start.Column,
|
||||
endOffset: end.Offset,
|
||||
endLine: end.Line,
|
||||
endCol: end.Column,
|
||||
}
|
||||
v.function.stmts = append(v.function.stmts, se)
|
||||
}
|
||||
v.VisitStmt(s)
|
||||
}
|
||||
}
|
145
vendor/github.com/axw/gocov/gocov/internal/testflag/testflag.go
сгенерированный
поставляемый
Normal file
145
vendor/github.com/axw/gocov/gocov/internal/testflag/testflag.go
сгенерированный
поставляемый
Normal file
|
@ -0,0 +1,145 @@
|
|||
// Copyright (c) 2015 The Gocov Authors.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to
|
||||
// deal in the Software without restriction, including without limitation the
|
||||
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
// sell copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
// IN THE SOFTWARE.
|
||||
//
|
||||
// Parts of this taken from cmd/go/testflag.go and
|
||||
// cmd/go/build.go; adapted for simplicity.
|
||||
//
|
||||
// Copyright 2011 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package testflag
|
||||
|
||||
import "strings"
|
||||
|
||||
type testFlagSpec struct {
|
||||
name string
|
||||
isBool bool
|
||||
}
|
||||
|
||||
var testFlagDefn = []*testFlagSpec{
|
||||
// test-specific
|
||||
{name: "i", isBool: true},
|
||||
{name: "bench"},
|
||||
{name: "benchmem", isBool: true},
|
||||
{name: "benchtime"},
|
||||
{name: "covermode"},
|
||||
{name: "cpu"},
|
||||
{name: "cpuprofile"},
|
||||
{name: "memprofile"},
|
||||
{name: "memprofilerate"},
|
||||
{name: "blockprofile"},
|
||||
{name: "blockprofilerate"},
|
||||
{name: "parallel"},
|
||||
{name: "run"},
|
||||
{name: "short", isBool: true},
|
||||
{name: "timeout"},
|
||||
{name: "trace"},
|
||||
{name: "v", isBool: true},
|
||||
|
||||
// common build flags
|
||||
{name: "a", isBool: true},
|
||||
{name: "race", isBool: true},
|
||||
{name: "x", isBool: true},
|
||||
{name: "asmflags"},
|
||||
{name: "buildmode"},
|
||||
{name: "compiler"},
|
||||
{name: "gccgoflags"},
|
||||
{name: "gcflags"},
|
||||
{name: "ldflags"},
|
||||
{name: "linkshared", isBool: true},
|
||||
{name: "pkgdir"},
|
||||
{name: "tags"},
|
||||
{name: "toolexec"},
|
||||
}
|
||||
|
||||
// Split processes the arguments , separating flags and package
|
||||
// names as done by "go test".
|
||||
func Split(args []string) (packageNames, passToTest []string) {
|
||||
inPkg := false
|
||||
for i := 0; i < len(args); i++ {
|
||||
if !strings.HasPrefix(args[i], "-") {
|
||||
if !inPkg && packageNames == nil {
|
||||
// First package name we've seen.
|
||||
inPkg = true
|
||||
}
|
||||
if inPkg {
|
||||
packageNames = append(packageNames, args[i])
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if inPkg {
|
||||
// Found an argument beginning with "-"; end of package list.
|
||||
inPkg = false
|
||||
}
|
||||
|
||||
n := parseTestFlag(args, i)
|
||||
if n == 0 {
|
||||
// This is a flag we do not know; we must assume
|
||||
// that any args we see after this might be flag
|
||||
// arguments, not package names.
|
||||
inPkg = false
|
||||
if packageNames == nil {
|
||||
// make non-nil: we have seen the empty package list
|
||||
packageNames = []string{}
|
||||
}
|
||||
passToTest = append(passToTest, args[i])
|
||||
continue
|
||||
}
|
||||
|
||||
passToTest = append(passToTest, args[i:i+n]...)
|
||||
i += n - 1
|
||||
}
|
||||
return packageNames, passToTest
|
||||
}
|
||||
|
||||
// parseTestFlag sees if argument i is a known flag and returns its
|
||||
// definition, value, and whether it consumed an extra word.
|
||||
func parseTestFlag(args []string, i int) (n int) {
|
||||
arg := args[i]
|
||||
if strings.HasPrefix(arg, "--") { // reduce two minuses to one
|
||||
arg = arg[1:]
|
||||
}
|
||||
switch arg {
|
||||
case "-?", "-h", "-help":
|
||||
return 1
|
||||
}
|
||||
if arg == "" || arg[0] != '-' {
|
||||
return 0
|
||||
}
|
||||
name := arg[1:]
|
||||
// If there's already "test.", drop it for now.
|
||||
name = strings.TrimPrefix(name, "test.")
|
||||
equals := strings.Index(name, "=")
|
||||
if equals >= 0 {
|
||||
name = name[:equals]
|
||||
}
|
||||
for _, f := range testFlagDefn {
|
||||
if name == f.name {
|
||||
// Booleans are special because they have modes -x, -x=true, -x=false.
|
||||
if !f.isBool && equals < 0 {
|
||||
return 2
|
||||
}
|
||||
return 1
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
// Copyright (c) 2012 The Gocov Authors.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to
|
||||
// deal in the Software without restriction, including without limitation the
|
||||
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
// sell copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
// IN THE SOFTWARE.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/axw/gocov"
|
||||
)
|
||||
|
||||
func usage() {
|
||||
fmt.Fprintf(os.Stderr, "Usage:\n\n\tgocov command [arguments]\n\n")
|
||||
fmt.Fprintf(os.Stderr, "The commands are:\n\n")
|
||||
fmt.Fprintf(os.Stderr, "\tannotate\n")
|
||||
fmt.Fprintf(os.Stderr, "\tconvert\n")
|
||||
fmt.Fprintf(os.Stderr, "\treport\n")
|
||||
fmt.Fprintf(os.Stderr, "\ttest\n")
|
||||
fmt.Fprintf(os.Stderr, "\n")
|
||||
flag.PrintDefaults()
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
func marshalJson(packages []*gocov.Package) ([]byte, error) {
|
||||
return json.Marshal(struct{ Packages []*gocov.Package }{packages})
|
||||
}
|
||||
|
||||
func unmarshalJson(data []byte) (packages []*gocov.Package, err error) {
|
||||
result := &struct{ Packages []*gocov.Package }{}
|
||||
err = json.Unmarshal(data, result)
|
||||
if err == nil {
|
||||
packages = result.Packages
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Usage = usage
|
||||
flag.Parse()
|
||||
|
||||
command := ""
|
||||
if flag.NArg() > 0 {
|
||||
command = flag.Arg(0)
|
||||
switch command {
|
||||
case "convert":
|
||||
if flag.NArg() <= 1 {
|
||||
fmt.Fprintln(os.Stderr, "missing cover profile")
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := convertProfiles(flag.Args()[1:]...); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "error:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
case "annotate":
|
||||
os.Exit(annotateSource())
|
||||
case "report":
|
||||
os.Exit(reportCoverage())
|
||||
case "test":
|
||||
if err := runTests(flag.Args()[1:]); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "error:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "Unknown command: %#q\n\n", command)
|
||||
usage()
|
||||
}
|
||||
} else {
|
||||
usage()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,224 @@
|
|||
// Copyright (c) 2012 The Gocov Authors.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to
|
||||
// deal in the Software without restriction, including without limitation the
|
||||
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
// sell copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
// IN THE SOFTWARE.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/axw/gocov"
|
||||
)
|
||||
|
||||
type report struct {
|
||||
packages []*gocov.Package
|
||||
}
|
||||
|
||||
type reportFunction struct {
|
||||
*gocov.Function
|
||||
statementsReached int
|
||||
}
|
||||
|
||||
type reportFunctionList []reportFunction
|
||||
|
||||
func (l reportFunctionList) Len() int {
|
||||
return len(l)
|
||||
}
|
||||
|
||||
// TODO make sort method configurable?
|
||||
func (l reportFunctionList) Less(i, j int) bool {
|
||||
var left, right float64
|
||||
if len(l[i].Statements) > 0 {
|
||||
left = float64(l[i].statementsReached) / float64(len(l[i].Statements))
|
||||
}
|
||||
if len(l[j].Statements) > 0 {
|
||||
right = float64(l[j].statementsReached) / float64(len(l[j].Statements))
|
||||
}
|
||||
if left < right {
|
||||
return true
|
||||
}
|
||||
return left == right && len(l[i].Statements) < len(l[j].Statements)
|
||||
}
|
||||
|
||||
func (l reportFunctionList) Swap(i, j int) {
|
||||
l[i], l[j] = l[j], l[i]
|
||||
}
|
||||
|
||||
type reverse struct {
|
||||
sort.Interface
|
||||
}
|
||||
|
||||
func (r reverse) Less(i, j int) bool {
|
||||
return r.Interface.Less(j, i)
|
||||
}
|
||||
|
||||
// NewReport creates a new report.
|
||||
func newReport() (r *report) {
|
||||
r = &report{}
|
||||
return
|
||||
}
|
||||
|
||||
// AddPackage adds a package's coverage information to the report.
|
||||
func (r *report) addPackage(p *gocov.Package) {
|
||||
i := sort.Search(len(r.packages), func(i int) bool {
|
||||
return r.packages[i].Name >= p.Name
|
||||
})
|
||||
if i < len(r.packages) && r.packages[i].Name == p.Name {
|
||||
r.packages[i].Accumulate(p)
|
||||
} else {
|
||||
head := r.packages[:i]
|
||||
tail := append([]*gocov.Package{p}, r.packages[i:]...)
|
||||
r.packages = append(head, tail...)
|
||||
}
|
||||
}
|
||||
|
||||
// Clear clears the coverage information from the report.
|
||||
func (r *report) clear() {
|
||||
r.packages = nil
|
||||
}
|
||||
|
||||
// functionReports returns the packages functions as an array of
|
||||
// reportFunction objects with the statements reached calculated
|
||||
func functionReports(pkg *gocov.Package) reportFunctionList {
|
||||
functions := make(reportFunctionList, len(pkg.Functions))
|
||||
for i, fn := range pkg.Functions {
|
||||
reached := 0
|
||||
for _, stmt := range fn.Statements {
|
||||
if stmt.Reached > 0 {
|
||||
reached++
|
||||
}
|
||||
}
|
||||
functions[i] = reportFunction{fn, reached}
|
||||
}
|
||||
|
||||
return functions
|
||||
|
||||
}
|
||||
|
||||
// printTotalCoverage outputs the combined coverage for each
|
||||
// package
|
||||
func (r *report) printTotalCoverage(w io.Writer) {
|
||||
var totalStatements, totalReached int
|
||||
|
||||
for _, pkg := range r.packages {
|
||||
functions := functionReports(pkg)
|
||||
sort.Sort(reverse{functions})
|
||||
|
||||
for _, fn := range functions {
|
||||
reached := fn.statementsReached
|
||||
totalStatements += len(fn.Statements)
|
||||
totalReached += reached
|
||||
}
|
||||
}
|
||||
|
||||
coveragePercentage := float64(totalReached) / float64(totalStatements) * 100
|
||||
fmt.Fprintf(w, "Total Coverage: %.2f%% (%d/%d)", coveragePercentage, totalReached, totalStatements)
|
||||
fmt.Fprintln(w)
|
||||
}
|
||||
|
||||
// PrintReport prints a coverage report to the given writer.
|
||||
func printReport(w io.Writer, r *report) {
|
||||
w = tabwriter.NewWriter(w, 0, 8, 0, '\t', 0)
|
||||
//fmt.Fprintln(w, "Package\tFunction\tStatements\t")
|
||||
//fmt.Fprintln(w, "-------\t--------\t---------\t")
|
||||
for _, pkg := range r.packages {
|
||||
printPackage(w, pkg)
|
||||
fmt.Fprintln(w)
|
||||
}
|
||||
r.printTotalCoverage(w)
|
||||
}
|
||||
|
||||
func printPackage(w io.Writer, pkg *gocov.Package) {
|
||||
functions := functionReports(pkg)
|
||||
sort.Sort(reverse{functions})
|
||||
|
||||
var longestFunctionName int
|
||||
var totalStatements, totalReached int
|
||||
for _, fn := range functions {
|
||||
reached := fn.statementsReached
|
||||
totalStatements += len(fn.Statements)
|
||||
totalReached += reached
|
||||
var stmtPercent float64 = 0
|
||||
if len(fn.Statements) > 0 {
|
||||
stmtPercent = float64(reached) / float64(len(fn.Statements)) * 100
|
||||
}
|
||||
if len(fn.Name) > longestFunctionName {
|
||||
longestFunctionName = len(fn.Name)
|
||||
}
|
||||
fmt.Fprintf(w, "%s/%s\t %s\t %.2f%% (%d/%d)\n",
|
||||
pkg.Name, filepath.Base(fn.File), fn.Name, stmtPercent,
|
||||
reached, len(fn.Statements))
|
||||
}
|
||||
|
||||
var funcPercent float64
|
||||
if totalStatements > 0 {
|
||||
funcPercent = float64(totalReached) / float64(totalStatements) * 100
|
||||
}
|
||||
summaryLine := strings.Repeat("-", longestFunctionName)
|
||||
fmt.Fprintf(w, "%s\t %s\t %.2f%% (%d/%d)\n",
|
||||
pkg.Name, summaryLine, funcPercent,
|
||||
totalReached, totalStatements)
|
||||
}
|
||||
|
||||
func reportCoverage() (rc int) {
|
||||
files := make([]*os.File, 0, 1)
|
||||
if flag.NArg() > 1 {
|
||||
for _, name := range flag.Args()[1:] {
|
||||
file, err := os.Open(name)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to open file (%s): %s\n", name, err)
|
||||
} else {
|
||||
files = append(files, file)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
files = append(files, os.Stdin)
|
||||
}
|
||||
report := newReport()
|
||||
for _, file := range files {
|
||||
data, err := ioutil.ReadAll(file)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to read coverage file: %s\n", err)
|
||||
return 1
|
||||
}
|
||||
packages, err := unmarshalJson(data)
|
||||
if err != nil {
|
||||
fmt.Fprintf(
|
||||
os.Stderr, "failed to unmarshal coverage data: %s\n", err)
|
||||
return 1
|
||||
}
|
||||
for _, pkg := range packages {
|
||||
report.addPackage(pkg)
|
||||
}
|
||||
if file != os.Stdin {
|
||||
file.Close()
|
||||
}
|
||||
}
|
||||
fmt.Println()
|
||||
printReport(os.Stdout, report)
|
||||
return 0
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
// Copyright (c) 2013 The Gocov Authors.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to
|
||||
// deal in the Software without restriction, including without limitation the
|
||||
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
// sell copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
// IN THE SOFTWARE.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/axw/gocov/gocov/internal/testflag"
|
||||
)
|
||||
|
||||
// resolvePackages returns a slice of resolved package names, given a slice of
|
||||
// package names that could be relative or recursive.
|
||||
func resolvePackages(pkgs []string) ([]string, error) {
|
||||
var buf bytes.Buffer
|
||||
cmd := exec.Command("go", append([]string{"list", "-e"}, pkgs...)...)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = &buf
|
||||
cmd.Stderr = os.Stderr
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var resolvedPkgs []string
|
||||
lines := strings.Split(buf.String(), "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if len(line) > 0 {
|
||||
resolvedPkgs = append(resolvedPkgs, line)
|
||||
}
|
||||
}
|
||||
return resolvedPkgs, nil
|
||||
}
|
||||
|
||||
func runTests(args []string) error {
|
||||
pkgs, testFlags := testflag.Split(args)
|
||||
pkgs, err := resolvePackages(pkgs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tmpDir, err := ioutil.TempDir("", "gocov")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
err := os.RemoveAll(tmpDir)
|
||||
if err != nil {
|
||||
log.Printf("failed to clean up temp directory %q", tmpDir)
|
||||
}
|
||||
}()
|
||||
|
||||
// Unique -coverprofile file names are used so that all the files can be
|
||||
// later merged into a single file.
|
||||
for i, pkg := range pkgs {
|
||||
coverFile := filepath.Join(tmpDir, fmt.Sprintf("test%d.cov", i))
|
||||
cmdArgs := append([]string{"test", "-coverprofile", coverFile}, testFlags...)
|
||||
cmdArgs = append(cmdArgs, pkg)
|
||||
cmd := exec.Command("go", cmdArgs...)
|
||||
cmd.Stdin = nil
|
||||
// Write all test command output to stderr so as not to interfere with
|
||||
// the JSON coverage output.
|
||||
cmd.Stdout = os.Stderr
|
||||
cmd.Stderr = os.Stderr
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Packages without tests will not produce a coverprofile; only pick up the
|
||||
// ones that were created.
|
||||
files, err := filepath.Glob(filepath.Join(tmpDir, "test*.cov"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Merge the profiles.
|
||||
return convertProfiles(files...)
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
package gocovutil
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/axw/gocov"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// Packages represents a set of gocov.Package structures.
|
||||
// The "AddPackage" method may be used to merge package
|
||||
// coverage results into the set.
|
||||
type Packages []*gocov.Package
|
||||
|
||||
// AddPackage adds a package's coverage information to the
|
||||
func (ps *Packages) AddPackage(p *gocov.Package) {
|
||||
i := sort.Search(len(*ps), func(i int) bool {
|
||||
return (*ps)[i].Name >= p.Name
|
||||
})
|
||||
if i < len(*ps) && (*ps)[i].Name == p.Name {
|
||||
(*ps)[i].Accumulate(p)
|
||||
} else {
|
||||
head := (*ps)[:i]
|
||||
tail := append([]*gocov.Package{p}, (*ps)[i:]...)
|
||||
*ps = append(head, tail...)
|
||||
}
|
||||
}
|
||||
|
||||
// ReadPackages takes a list of filenames and parses their
|
||||
// contents as a Packages object.
|
||||
//
|
||||
// The special filename "-" may be used to indicate standard input.
|
||||
// Duplicate filenames are ignored.
|
||||
func ReadPackages(filenames []string) (ps Packages, err error) {
|
||||
copy_ := make([]string, len(filenames))
|
||||
copy(copy_, filenames)
|
||||
filenames = copy_
|
||||
sort.Strings(filenames)
|
||||
|
||||
// Eliminate duplicates.
|
||||
unique := []string{filenames[0]}
|
||||
if len(filenames) > 1 {
|
||||
for _, f := range filenames[1:] {
|
||||
if f != unique[len(unique)-1] {
|
||||
unique = append(unique, f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Open files.
|
||||
var files []*os.File
|
||||
for _, f := range filenames {
|
||||
if f == "-" {
|
||||
files = append(files, os.Stdin)
|
||||
} else {
|
||||
file, err := os.Open(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
files = append(files, os.Stdin)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the files, accumulate Packages.
|
||||
for _, file := range files {
|
||||
data, err := ioutil.ReadAll(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := &struct{ Packages []*gocov.Package }{}
|
||||
err = json.Unmarshal(data, result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, p := range result.Packages {
|
||||
ps.AddPackage(p)
|
||||
}
|
||||
}
|
||||
return ps, nil
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
go-junit-report
|
|
@ -0,0 +1,16 @@
|
|||
language: go
|
||||
|
||||
go:
|
||||
- tip
|
||||
- "1.13.x"
|
||||
- "1.12.x"
|
||||
- "1.11.x"
|
||||
- "1.10.x"
|
||||
- "1.9.x"
|
||||
- "1.8.x"
|
||||
- "1.7.x"
|
||||
- "1.6.x"
|
||||
- "1.5.x"
|
||||
- "1.4.x"
|
||||
- "1.3.x"
|
||||
- "1.2.x"
|
|
@ -0,0 +1,20 @@
|
|||
Copyright (c) 2012 Joel Stemmer
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@ -0,0 +1,49 @@
|
|||
# go-junit-report
|
||||
|
||||
Converts `go test` output to an xml report, suitable for applications that
|
||||
expect junit xml reports (e.g. [Jenkins](http://jenkins-ci.org)).
|
||||
|
||||
[![Build Status][travis-badge]][travis-link]
|
||||
[![Report Card][report-badge]][report-link]
|
||||
|
||||
## Installation
|
||||
|
||||
Go version 1.2 or higher is required. Install or update using the `go get`
|
||||
command:
|
||||
|
||||
```bash
|
||||
go get -u github.com/jstemmer/go-junit-report
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
go-junit-report reads the `go test` verbose output from standard in and writes
|
||||
junit compatible XML to standard out.
|
||||
|
||||
```bash
|
||||
go test -v 2>&1 | go-junit-report > report.xml
|
||||
```
|
||||
|
||||
Note that it also can parse benchmark output with `-bench` flag:
|
||||
```bash
|
||||
go test -v -bench . -count 5 2>&1 | go-junit-report > report.xml
|
||||
```
|
||||
|
||||
## Contribution
|
||||
|
||||
Create an Issue and discuss the fix or feature, then fork the package.
|
||||
Clone to github.com/jstemmer/go-junit-report. This is necessary because go import uses this path.
|
||||
Fix or implement feature. Test and then commit change.
|
||||
Specify #Issue and describe change in the commit message.
|
||||
Create Pull Request. It can be merged by owner or administrator then.
|
||||
|
||||
### Run Tests
|
||||
|
||||
```bash
|
||||
go test
|
||||
```
|
||||
|
||||
[travis-badge]: https://travis-ci.org/jstemmer/go-junit-report.svg
|
||||
[travis-link]: https://travis-ci.org/jstemmer/go-junit-report
|
||||
[report-badge]: https://goreportcard.com/badge/github.com/jstemmer/go-junit-report
|
||||
[report-link]: https://goreportcard.com/report/github.com/jstemmer/go-junit-report
|
182
vendor/github.com/jstemmer/go-junit-report/formatter/formatter.go
сгенерированный
поставляемый
Normal file
182
vendor/github.com/jstemmer/go-junit-report/formatter/formatter.go
сгенерированный
поставляемый
Normal file
|
@ -0,0 +1,182 @@
|
|||
package formatter
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jstemmer/go-junit-report/parser"
|
||||
)
|
||||
|
||||
// JUnitTestSuites is a collection of JUnit test suites.
|
||||
type JUnitTestSuites struct {
|
||||
XMLName xml.Name `xml:"testsuites"`
|
||||
Suites []JUnitTestSuite `xml:"testsuite"`
|
||||
}
|
||||
|
||||
// JUnitTestSuite is a single JUnit test suite which may contain many
|
||||
// testcases.
|
||||
type JUnitTestSuite struct {
|
||||
XMLName xml.Name `xml:"testsuite"`
|
||||
Tests int `xml:"tests,attr"`
|
||||
Failures int `xml:"failures,attr"`
|
||||
Time string `xml:"time,attr"`
|
||||
Name string `xml:"name,attr"`
|
||||
Properties []JUnitProperty `xml:"properties>property,omitempty"`
|
||||
TestCases []JUnitTestCase `xml:"testcase"`
|
||||
}
|
||||
|
||||
// JUnitTestCase is a single test case with its result.
|
||||
type JUnitTestCase struct {
|
||||
XMLName xml.Name `xml:"testcase"`
|
||||
Classname string `xml:"classname,attr"`
|
||||
Name string `xml:"name,attr"`
|
||||
Time string `xml:"time,attr"`
|
||||
SkipMessage *JUnitSkipMessage `xml:"skipped,omitempty"`
|
||||
Failure *JUnitFailure `xml:"failure,omitempty"`
|
||||
}
|
||||
|
||||
// JUnitSkipMessage contains the reason why a testcase was skipped.
|
||||
type JUnitSkipMessage struct {
|
||||
Message string `xml:"message,attr"`
|
||||
}
|
||||
|
||||
// JUnitProperty represents a key/value pair used to define properties.
|
||||
type JUnitProperty struct {
|
||||
Name string `xml:"name,attr"`
|
||||
Value string `xml:"value,attr"`
|
||||
}
|
||||
|
||||
// JUnitFailure contains data related to a failed test.
|
||||
type JUnitFailure struct {
|
||||
Message string `xml:"message,attr"`
|
||||
Type string `xml:"type,attr"`
|
||||
Contents string `xml:",chardata"`
|
||||
}
|
||||
|
||||
// JUnitReportXML writes a JUnit xml representation of the given report to w
|
||||
// in the format described at http://windyroad.org/dl/Open%20Source/JUnit.xsd
|
||||
func JUnitReportXML(report *parser.Report, noXMLHeader bool, goVersion string, w io.Writer) error {
|
||||
suites := JUnitTestSuites{}
|
||||
|
||||
// convert Report to JUnit test suites
|
||||
for _, pkg := range report.Packages {
|
||||
pkg.Benchmarks = mergeBenchmarks(pkg.Benchmarks)
|
||||
ts := JUnitTestSuite{
|
||||
Tests: len(pkg.Tests) + len(pkg.Benchmarks),
|
||||
Failures: 0,
|
||||
Time: formatTime(pkg.Duration),
|
||||
Name: pkg.Name,
|
||||
Properties: []JUnitProperty{},
|
||||
TestCases: []JUnitTestCase{},
|
||||
}
|
||||
|
||||
classname := pkg.Name
|
||||
if idx := strings.LastIndex(classname, "/"); idx > -1 && idx < len(pkg.Name) {
|
||||
classname = pkg.Name[idx+1:]
|
||||
}
|
||||
|
||||
// properties
|
||||
if goVersion == "" {
|
||||
// if goVersion was not specified as a flag, fall back to version reported by runtime
|
||||
goVersion = runtime.Version()
|
||||
}
|
||||
ts.Properties = append(ts.Properties, JUnitProperty{"go.version", goVersion})
|
||||
if pkg.CoveragePct != "" {
|
||||
ts.Properties = append(ts.Properties, JUnitProperty{"coverage.statements.pct", pkg.CoveragePct})
|
||||
}
|
||||
|
||||
// individual test cases
|
||||
for _, test := range pkg.Tests {
|
||||
testCase := JUnitTestCase{
|
||||
Classname: classname,
|
||||
Name: test.Name,
|
||||
Time: formatTime(test.Duration),
|
||||
Failure: nil,
|
||||
}
|
||||
|
||||
if test.Result == parser.FAIL {
|
||||
ts.Failures++
|
||||
testCase.Failure = &JUnitFailure{
|
||||
Message: "Failed",
|
||||
Type: "",
|
||||
Contents: strings.Join(test.Output, "\n"),
|
||||
}
|
||||
}
|
||||
|
||||
if test.Result == parser.SKIP {
|
||||
testCase.SkipMessage = &JUnitSkipMessage{strings.Join(test.Output, "\n")}
|
||||
}
|
||||
|
||||
ts.TestCases = append(ts.TestCases, testCase)
|
||||
}
|
||||
|
||||
// individual benchmarks
|
||||
for _, benchmark := range pkg.Benchmarks {
|
||||
benchmarkCase := JUnitTestCase{
|
||||
Classname: classname,
|
||||
Name: benchmark.Name,
|
||||
Time: formatBenchmarkTime(benchmark.Duration),
|
||||
}
|
||||
|
||||
ts.TestCases = append(ts.TestCases, benchmarkCase)
|
||||
}
|
||||
|
||||
suites.Suites = append(suites.Suites, ts)
|
||||
}
|
||||
|
||||
// to xml
|
||||
bytes, err := xml.MarshalIndent(suites, "", "\t")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
writer := bufio.NewWriter(w)
|
||||
|
||||
if !noXMLHeader {
|
||||
writer.WriteString(xml.Header)
|
||||
}
|
||||
|
||||
writer.Write(bytes)
|
||||
writer.WriteByte('\n')
|
||||
writer.Flush()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func mergeBenchmarks(benchmarks []*parser.Benchmark) []*parser.Benchmark {
|
||||
var merged []*parser.Benchmark
|
||||
benchmap := make(map[string][]*parser.Benchmark)
|
||||
for _, bm := range benchmarks {
|
||||
if _, ok := benchmap[bm.Name]; !ok {
|
||||
merged = append(merged, &parser.Benchmark{Name: bm.Name})
|
||||
}
|
||||
benchmap[bm.Name] = append(benchmap[bm.Name], bm)
|
||||
}
|
||||
|
||||
for _, bm := range merged {
|
||||
for _, b := range benchmap[bm.Name] {
|
||||
bm.Allocs += b.Allocs
|
||||
bm.Bytes += b.Bytes
|
||||
bm.Duration += b.Duration
|
||||
}
|
||||
n := len(benchmap[bm.Name])
|
||||
bm.Allocs /= n
|
||||
bm.Bytes /= n
|
||||
bm.Duration /= time.Duration(n)
|
||||
}
|
||||
|
||||
return merged
|
||||
}
|
||||
|
||||
func formatTime(d time.Duration) string {
|
||||
return fmt.Sprintf("%.3f", d.Seconds())
|
||||
}
|
||||
|
||||
func formatBenchmarkTime(d time.Duration) string {
|
||||
return fmt.Sprintf("%.9f", d.Seconds())
|
||||
}
|
45
vendor/github.com/jstemmer/go-junit-report/go-junit-report.go
сгенерированный
поставляемый
Normal file
45
vendor/github.com/jstemmer/go-junit-report/go-junit-report.go
сгенерированный
поставляемый
Normal file
|
@ -0,0 +1,45 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/jstemmer/go-junit-report/formatter"
|
||||
"github.com/jstemmer/go-junit-report/parser"
|
||||
)
|
||||
|
||||
var (
|
||||
noXMLHeader = flag.Bool("no-xml-header", false, "do not print xml header")
|
||||
packageName = flag.String("package-name", "", "specify a package name (compiled test have no package name in output)")
|
||||
goVersionFlag = flag.String("go-version", "", "specify the value to use for the go.version property in the generated XML")
|
||||
setExitCode = flag.Bool("set-exit-code", false, "set exit code to 1 if tests failed")
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
if flag.NArg() != 0 {
|
||||
fmt.Fprintf(os.Stderr, "%s does not accept positional arguments\n", os.Args[0])
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Read input
|
||||
report, err := parser.Parse(os.Stdin, *packageName)
|
||||
if err != nil {
|
||||
fmt.Printf("Error reading input: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Write xml
|
||||
err = formatter.JUnitReportXML(report, *noXMLHeader, *goVersionFlag, os.Stdout)
|
||||
if err != nil {
|
||||
fmt.Printf("Error writing XML: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if *setExitCode && report.Failures() > 0 {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
module github.com/jstemmer/go-junit-report
|
||||
|
||||
go 1.2
|
319
vendor/github.com/jstemmer/go-junit-report/parser/parser.go
сгенерированный
поставляемый
Normal file
319
vendor/github.com/jstemmer/go-junit-report/parser/parser.go
сгенерированный
поставляемый
Normal file
|
@ -0,0 +1,319 @@
|
|||
package parser
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Result represents a test result.
|
||||
type Result int
|
||||
|
||||
// Test result constants
|
||||
const (
|
||||
PASS Result = iota
|
||||
FAIL
|
||||
SKIP
|
||||
)
|
||||
|
||||
// Report is a collection of package tests.
|
||||
type Report struct {
|
||||
Packages []Package
|
||||
}
|
||||
|
||||
// Package contains the test results of a single package.
|
||||
type Package struct {
|
||||
Name string
|
||||
Duration time.Duration
|
||||
Tests []*Test
|
||||
Benchmarks []*Benchmark
|
||||
CoveragePct string
|
||||
|
||||
// Time is deprecated, use Duration instead.
|
||||
Time int // in milliseconds
|
||||
}
|
||||
|
||||
// Test contains the results of a single test.
|
||||
type Test struct {
|
||||
Name string
|
||||
Duration time.Duration
|
||||
Result Result
|
||||
Output []string
|
||||
|
||||
SubtestIndent string
|
||||
|
||||
// Time is deprecated, use Duration instead.
|
||||
Time int // in milliseconds
|
||||
}
|
||||
|
||||
// Benchmark contains the results of a single benchmark.
|
||||
type Benchmark struct {
|
||||
Name string
|
||||
Duration time.Duration
|
||||
// number of B/op
|
||||
Bytes int
|
||||
// number of allocs/op
|
||||
Allocs int
|
||||
}
|
||||
|
||||
var (
|
||||
regexStatus = regexp.MustCompile(`--- (PASS|FAIL|SKIP): (.+) \((\d+\.\d+)(?: seconds|s)\)`)
|
||||
regexIndent = regexp.MustCompile(`^([ \t]+)---`)
|
||||
regexCoverage = regexp.MustCompile(`^coverage:\s+(\d+\.\d+)%\s+of\s+statements(?:\sin\s.+)?$`)
|
||||
regexResult = regexp.MustCompile(`^(ok|FAIL)\s+([^ ]+)\s+(?:(\d+\.\d+)s|\(cached\)|(\[\w+ failed]))(?:\s+coverage:\s+(\d+\.\d+)%\sof\sstatements(?:\sin\s.+)?)?$`)
|
||||
// regexBenchmark captures 3-5 groups: benchmark name, number of times ran, ns/op (with or without decimal), B/op (optional), and allocs/op (optional).
|
||||
regexBenchmark = regexp.MustCompile(`^(Benchmark[^ -]+)(?:-\d+\s+|\s+)(\d+)\s+(\d+|\d+\.\d+)\sns/op(?:\s+(\d+)\sB/op)?(?:\s+(\d+)\sallocs/op)?`)
|
||||
regexOutput = regexp.MustCompile(`( )*\t(.*)`)
|
||||
regexSummary = regexp.MustCompile(`^(PASS|FAIL|SKIP)$`)
|
||||
regexPackageWithTest = regexp.MustCompile(`^# ([^\[\]]+) \[[^\]]+\]$`)
|
||||
)
|
||||
|
||||
// Parse parses go test output from reader r and returns a report with the
|
||||
// results. An optional pkgName can be given, which is used in case a package
|
||||
// result line is missing.
|
||||
func Parse(r io.Reader, pkgName string) (*Report, error) {
|
||||
reader := bufio.NewReader(r)
|
||||
|
||||
report := &Report{make([]Package, 0)}
|
||||
|
||||
// keep track of tests we find
|
||||
var tests []*Test
|
||||
|
||||
// keep track of benchmarks we find
|
||||
var benchmarks []*Benchmark
|
||||
|
||||
// sum of tests' time, use this if current test has no result line (when it is compiled test)
|
||||
var testsTime time.Duration
|
||||
|
||||
// current test
|
||||
var cur string
|
||||
|
||||
// coverage percentage report for current package
|
||||
var coveragePct string
|
||||
|
||||
// stores mapping between package name and output of build failures
|
||||
var packageCaptures = map[string][]string{}
|
||||
|
||||
// the name of the package which it's build failure output is being captured
|
||||
var capturedPackage string
|
||||
|
||||
// capture any non-test output
|
||||
var buffers = map[string][]string{}
|
||||
|
||||
// parse lines
|
||||
for {
|
||||
l, _, err := reader.ReadLine()
|
||||
if err != nil && err == io.EOF {
|
||||
break
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
line := string(l)
|
||||
|
||||
if strings.HasPrefix(line, "=== RUN ") {
|
||||
// new test
|
||||
cur = strings.TrimSpace(line[8:])
|
||||
tests = append(tests, &Test{
|
||||
Name: cur,
|
||||
Result: FAIL,
|
||||
Output: make([]string, 0),
|
||||
})
|
||||
|
||||
// clear the current build package, so output lines won't be added to that build
|
||||
capturedPackage = ""
|
||||
} else if matches := regexBenchmark.FindStringSubmatch(line); len(matches) == 6 {
|
||||
bytes, _ := strconv.Atoi(matches[4])
|
||||
allocs, _ := strconv.Atoi(matches[5])
|
||||
|
||||
benchmarks = append(benchmarks, &Benchmark{
|
||||
Name: matches[1],
|
||||
Duration: parseNanoseconds(matches[3]),
|
||||
Bytes: bytes,
|
||||
Allocs: allocs,
|
||||
})
|
||||
} else if strings.HasPrefix(line, "=== PAUSE ") {
|
||||
continue
|
||||
} else if strings.HasPrefix(line, "=== CONT ") {
|
||||
cur = strings.TrimSpace(line[8:])
|
||||
continue
|
||||
} else if matches := regexResult.FindStringSubmatch(line); len(matches) == 6 {
|
||||
if matches[5] != "" {
|
||||
coveragePct = matches[5]
|
||||
}
|
||||
if strings.HasSuffix(matches[4], "failed]") {
|
||||
// the build of the package failed, inject a dummy test into the package
|
||||
// which indicate about the failure and contain the failure description.
|
||||
tests = append(tests, &Test{
|
||||
Name: matches[4],
|
||||
Result: FAIL,
|
||||
Output: packageCaptures[matches[2]],
|
||||
})
|
||||
} else if matches[1] == "FAIL" && !containsFailures(tests) && len(buffers[cur]) > 0 {
|
||||
// This package didn't have any failing tests, but still it
|
||||
// failed with some output. Create a dummy test with the
|
||||
// output.
|
||||
tests = append(tests, &Test{
|
||||
Name: "Failure",
|
||||
Result: FAIL,
|
||||
Output: buffers[cur],
|
||||
})
|
||||
buffers[cur] = buffers[cur][0:0]
|
||||
}
|
||||
|
||||
// all tests in this package are finished
|
||||
report.Packages = append(report.Packages, Package{
|
||||
Name: matches[2],
|
||||
Duration: parseSeconds(matches[3]),
|
||||
Tests: tests,
|
||||
Benchmarks: benchmarks,
|
||||
CoveragePct: coveragePct,
|
||||
|
||||
Time: int(parseSeconds(matches[3]) / time.Millisecond), // deprecated
|
||||
})
|
||||
|
||||
buffers[cur] = buffers[cur][0:0]
|
||||
tests = make([]*Test, 0)
|
||||
benchmarks = make([]*Benchmark, 0)
|
||||
coveragePct = ""
|
||||
cur = ""
|
||||
testsTime = 0
|
||||
} else if matches := regexStatus.FindStringSubmatch(line); len(matches) == 4 {
|
||||
cur = matches[2]
|
||||
test := findTest(tests, cur)
|
||||
if test == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// test status
|
||||
if matches[1] == "PASS" {
|
||||
test.Result = PASS
|
||||
} else if matches[1] == "SKIP" {
|
||||
test.Result = SKIP
|
||||
} else {
|
||||
test.Result = FAIL
|
||||
}
|
||||
|
||||
if matches := regexIndent.FindStringSubmatch(line); len(matches) == 2 {
|
||||
test.SubtestIndent = matches[1]
|
||||
}
|
||||
|
||||
test.Output = buffers[cur]
|
||||
|
||||
test.Name = matches[2]
|
||||
test.Duration = parseSeconds(matches[3])
|
||||
testsTime += test.Duration
|
||||
|
||||
test.Time = int(test.Duration / time.Millisecond) // deprecated
|
||||
} else if matches := regexCoverage.FindStringSubmatch(line); len(matches) == 2 {
|
||||
coveragePct = matches[1]
|
||||
} else if matches := regexOutput.FindStringSubmatch(line); capturedPackage == "" && len(matches) == 3 {
|
||||
// Sub-tests start with one or more series of 4-space indents, followed by a hard tab,
|
||||
// followed by the test output
|
||||
// Top-level tests start with a hard tab.
|
||||
test := findTest(tests, cur)
|
||||
if test == nil {
|
||||
continue
|
||||
}
|
||||
test.Output = append(test.Output, matches[2])
|
||||
} else if strings.HasPrefix(line, "# ") {
|
||||
// indicates a capture of build output of a package. set the current build package.
|
||||
packageWithTestBinary := regexPackageWithTest.FindStringSubmatch(line)
|
||||
if packageWithTestBinary != nil {
|
||||
// Sometimes, the text after "# " shows the name of the test binary
|
||||
// ("<package>.test") in addition to the package
|
||||
// e.g.: "# package/name [package/name.test]"
|
||||
capturedPackage = packageWithTestBinary[1]
|
||||
} else {
|
||||
capturedPackage = line[2:]
|
||||
}
|
||||
} else if capturedPackage != "" {
|
||||
// current line is build failure capture for the current built package
|
||||
packageCaptures[capturedPackage] = append(packageCaptures[capturedPackage], line)
|
||||
} else if regexSummary.MatchString(line) {
|
||||
// unset current test name so any additional output after the
|
||||
// summary is captured separately.
|
||||
cur = ""
|
||||
} else {
|
||||
// buffer anything else that we didn't recognize
|
||||
buffers[cur] = append(buffers[cur], line)
|
||||
|
||||
// if we have a current test, also append to its output
|
||||
test := findTest(tests, cur)
|
||||
if test != nil {
|
||||
if strings.HasPrefix(line, test.SubtestIndent+" ") {
|
||||
test.Output = append(test.Output, strings.TrimPrefix(line, test.SubtestIndent+" "))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(tests) > 0 {
|
||||
// no result line found
|
||||
report.Packages = append(report.Packages, Package{
|
||||
Name: pkgName,
|
||||
Duration: testsTime,
|
||||
Time: int(testsTime / time.Millisecond),
|
||||
Tests: tests,
|
||||
Benchmarks: benchmarks,
|
||||
CoveragePct: coveragePct,
|
||||
})
|
||||
}
|
||||
|
||||
return report, nil
|
||||
}
|
||||
|
||||
func parseSeconds(t string) time.Duration {
|
||||
if t == "" {
|
||||
return time.Duration(0)
|
||||
}
|
||||
// ignore error
|
||||
d, _ := time.ParseDuration(t + "s")
|
||||
return d
|
||||
}
|
||||
|
||||
func parseNanoseconds(t string) time.Duration {
|
||||
// note: if input < 1 ns precision, result will be 0s.
|
||||
if t == "" {
|
||||
return time.Duration(0)
|
||||
}
|
||||
// ignore error
|
||||
d, _ := time.ParseDuration(t + "ns")
|
||||
return d
|
||||
}
|
||||
|
||||
func findTest(tests []*Test, name string) *Test {
|
||||
for i := len(tests) - 1; i >= 0; i-- {
|
||||
if tests[i].Name == name {
|
||||
return tests[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func containsFailures(tests []*Test) bool {
|
||||
for _, test := range tests {
|
||||
if test.Result == FAIL {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Failures counts the number of failed tests in this report
|
||||
func (r *Report) Failures() int {
|
||||
count := 0
|
||||
|
||||
for _, p := range r.Packages {
|
||||
for _, t := range p.Tests {
|
||||
if t.Result == FAIL {
|
||||
count++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return count
|
||||
}
|
|
@ -8,5 +8,3 @@ require (
|
|||
github.com/stretchr/testify v1.2.2
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894
|
||||
)
|
||||
|
||||
go 1.13
|
||||
|
|
|
@ -0,0 +1,256 @@
|
|||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package cover provides support for parsing coverage profiles
|
||||
// generated by "go test -coverprofile=cover.out".
|
||||
package cover // import "golang.org/x/tools/cover"
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Profile represents the profiling data for a specific file.
|
||||
type Profile struct {
|
||||
FileName string
|
||||
Mode string
|
||||
Blocks []ProfileBlock
|
||||
}
|
||||
|
||||
// ProfileBlock represents a single block of profiling data.
|
||||
type ProfileBlock struct {
|
||||
StartLine, StartCol int
|
||||
EndLine, EndCol int
|
||||
NumStmt, Count int
|
||||
}
|
||||
|
||||
type byFileName []*Profile
|
||||
|
||||
func (p byFileName) Len() int { return len(p) }
|
||||
func (p byFileName) Less(i, j int) bool { return p[i].FileName < p[j].FileName }
|
||||
func (p byFileName) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
|
||||
|
||||
// ParseProfiles parses profile data in the specified file and returns a
|
||||
// Profile for each source file described therein.
|
||||
func ParseProfiles(fileName string) ([]*Profile, error) {
|
||||
pf, err := os.Open(fileName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer pf.Close()
|
||||
|
||||
files := make(map[string]*Profile)
|
||||
buf := bufio.NewReader(pf)
|
||||
// First line is "mode: foo", where foo is "set", "count", or "atomic".
|
||||
// Rest of file is in the format
|
||||
// encoding/base64/base64.go:34.44,37.40 3 1
|
||||
// where the fields are: name.go:line.column,line.column numberOfStatements count
|
||||
s := bufio.NewScanner(buf)
|
||||
mode := ""
|
||||
for s.Scan() {
|
||||
line := s.Text()
|
||||
if mode == "" {
|
||||
const p = "mode: "
|
||||
if !strings.HasPrefix(line, p) || line == p {
|
||||
return nil, fmt.Errorf("bad mode line: %v", line)
|
||||
}
|
||||
mode = line[len(p):]
|
||||
continue
|
||||
}
|
||||
fn, b, err := parseLine(line)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("line %q doesn't match expected format: %v", line, err)
|
||||
}
|
||||
p := files[fn]
|
||||
if p == nil {
|
||||
p = &Profile{
|
||||
FileName: fn,
|
||||
Mode: mode,
|
||||
}
|
||||
files[fn] = p
|
||||
}
|
||||
p.Blocks = append(p.Blocks, b)
|
||||
}
|
||||
if err := s.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, p := range files {
|
||||
sort.Sort(blocksByStart(p.Blocks))
|
||||
// Merge samples from the same location.
|
||||
j := 1
|
||||
for i := 1; i < len(p.Blocks); i++ {
|
||||
b := p.Blocks[i]
|
||||
last := p.Blocks[j-1]
|
||||
if b.StartLine == last.StartLine &&
|
||||
b.StartCol == last.StartCol &&
|
||||
b.EndLine == last.EndLine &&
|
||||
b.EndCol == last.EndCol {
|
||||
if b.NumStmt != last.NumStmt {
|
||||
return nil, fmt.Errorf("inconsistent NumStmt: changed from %d to %d", last.NumStmt, b.NumStmt)
|
||||
}
|
||||
if mode == "set" {
|
||||
p.Blocks[j-1].Count |= b.Count
|
||||
} else {
|
||||
p.Blocks[j-1].Count += b.Count
|
||||
}
|
||||
continue
|
||||
}
|
||||
p.Blocks[j] = b
|
||||
j++
|
||||
}
|
||||
p.Blocks = p.Blocks[:j]
|
||||
}
|
||||
// Generate a sorted slice.
|
||||
profiles := make([]*Profile, 0, len(files))
|
||||
for _, profile := range files {
|
||||
profiles = append(profiles, profile)
|
||||
}
|
||||
sort.Sort(byFileName(profiles))
|
||||
return profiles, nil
|
||||
}
|
||||
|
||||
// parseLine parses a line from a coverage file.
|
||||
// It is equivalent to the regex
|
||||
// ^(.+):([0-9]+)\.([0-9]+),([0-9]+)\.([0-9]+) ([0-9]+) ([0-9]+)$
|
||||
//
|
||||
// However, it is much faster: https://golang.org/cl/179377
|
||||
func parseLine(l string) (fileName string, block ProfileBlock, err error) {
|
||||
end := len(l)
|
||||
|
||||
b := ProfileBlock{}
|
||||
b.Count, end, err = seekBack(l, ' ', end, "Count")
|
||||
if err != nil {
|
||||
return "", b, err
|
||||
}
|
||||
b.NumStmt, end, err = seekBack(l, ' ', end, "NumStmt")
|
||||
if err != nil {
|
||||
return "", b, err
|
||||
}
|
||||
b.EndCol, end, err = seekBack(l, '.', end, "EndCol")
|
||||
if err != nil {
|
||||
return "", b, err
|
||||
}
|
||||
b.EndLine, end, err = seekBack(l, ',', end, "EndLine")
|
||||
if err != nil {
|
||||
return "", b, err
|
||||
}
|
||||
b.StartCol, end, err = seekBack(l, '.', end, "StartCol")
|
||||
if err != nil {
|
||||
return "", b, err
|
||||
}
|
||||
b.StartLine, end, err = seekBack(l, ':', end, "StartLine")
|
||||
if err != nil {
|
||||
return "", b, err
|
||||
}
|
||||
fn := l[0:end]
|
||||
if fn == "" {
|
||||
return "", b, errors.New("a FileName cannot be blank")
|
||||
}
|
||||
return fn, b, nil
|
||||
}
|
||||
|
||||
// seekBack searches backwards from end to find sep in l, then returns the
|
||||
// value between sep and end as an integer.
|
||||
// If seekBack fails, the returned error will reference what.
|
||||
func seekBack(l string, sep byte, end int, what string) (value int, nextSep int, err error) {
|
||||
// Since we're seeking backwards and we know only ASCII is legal for these values,
|
||||
// we can ignore the possibility of non-ASCII characters.
|
||||
for start := end - 1; start >= 0; start-- {
|
||||
if l[start] == sep {
|
||||
i, err := strconv.Atoi(l[start+1 : end])
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("couldn't parse %q: %v", what, err)
|
||||
}
|
||||
if i < 0 {
|
||||
return 0, 0, fmt.Errorf("negative values are not allowed for %s, found %d", what, i)
|
||||
}
|
||||
return i, start, nil
|
||||
}
|
||||
}
|
||||
return 0, 0, fmt.Errorf("couldn't find a %s before %s", string(sep), what)
|
||||
}
|
||||
|
||||
type blocksByStart []ProfileBlock
|
||||
|
||||
func (b blocksByStart) Len() int { return len(b) }
|
||||
func (b blocksByStart) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
|
||||
func (b blocksByStart) Less(i, j int) bool {
|
||||
bi, bj := b[i], b[j]
|
||||
return bi.StartLine < bj.StartLine || bi.StartLine == bj.StartLine && bi.StartCol < bj.StartCol
|
||||
}
|
||||
|
||||
// Boundary represents the position in a source file of the beginning or end of a
|
||||
// block as reported by the coverage profile. In HTML mode, it will correspond to
|
||||
// the opening or closing of a <span> tag and will be used to colorize the source
|
||||
type Boundary struct {
|
||||
Offset int // Location as a byte offset in the source file.
|
||||
Start bool // Is this the start of a block?
|
||||
Count int // Event count from the cover profile.
|
||||
Norm float64 // Count normalized to [0..1].
|
||||
}
|
||||
|
||||
// Boundaries returns a Profile as a set of Boundary objects within the provided src.
|
||||
func (p *Profile) Boundaries(src []byte) (boundaries []Boundary) {
|
||||
// Find maximum count.
|
||||
max := 0
|
||||
for _, b := range p.Blocks {
|
||||
if b.Count > max {
|
||||
max = b.Count
|
||||
}
|
||||
}
|
||||
// Divisor for normalization.
|
||||
divisor := math.Log(float64(max))
|
||||
|
||||
// boundary returns a Boundary, populating the Norm field with a normalized Count.
|
||||
boundary := func(offset int, start bool, count int) Boundary {
|
||||
b := Boundary{Offset: offset, Start: start, Count: count}
|
||||
if !start || count == 0 {
|
||||
return b
|
||||
}
|
||||
if max <= 1 {
|
||||
b.Norm = 0.8 // Profile is in"set" mode; we want a heat map. Use cov8 in the CSS.
|
||||
} else if count > 0 {
|
||||
b.Norm = math.Log(float64(count)) / divisor
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
line, col := 1, 2 // TODO: Why is this 2?
|
||||
for si, bi := 0, 0; si < len(src) && bi < len(p.Blocks); {
|
||||
b := p.Blocks[bi]
|
||||
if b.StartLine == line && b.StartCol == col {
|
||||
boundaries = append(boundaries, boundary(si, true, b.Count))
|
||||
}
|
||||
if b.EndLine == line && b.EndCol == col || line > b.EndLine {
|
||||
boundaries = append(boundaries, boundary(si, false, 0))
|
||||
bi++
|
||||
continue // Don't advance through src; maybe the next block starts here.
|
||||
}
|
||||
if src[si] == '\n' {
|
||||
line++
|
||||
col = 0
|
||||
}
|
||||
col++
|
||||
si++
|
||||
}
|
||||
sort.Sort(boundariesByPos(boundaries))
|
||||
return
|
||||
}
|
||||
|
||||
type boundariesByPos []Boundary
|
||||
|
||||
func (b boundariesByPos) Len() int { return len(b) }
|
||||
func (b boundariesByPos) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
|
||||
func (b boundariesByPos) Less(i, j int) bool {
|
||||
if b[i].Offset == b[j].Offset {
|
||||
return !b[i].Start && b[j].Start
|
||||
}
|
||||
return b[i].Offset < b[j].Offset
|
||||
}
|
Загрузка…
Ссылка в новой задаче