diff --git a/Makefile b/Makefile index c4ad3914556..f3f415842e9 100644 --- a/Makefile +++ b/Makefile @@ -71,13 +71,13 @@ all: build ################################################## ##@ Test -.PHONY: install-test-deps -install-test-deps: - go install github.com/jstemmer/go-junit-report/v2@latest +# .PHONY: install-test-deps +# install-test-deps: +# go install github.com/jstemmer/go-junit-report/v2@latest .PHONY: test -test: manifests generate fmt vet envtest install-test-deps ## Run tests and export the result to junit format. - KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) -p path)" go test -v 2>&1 ./... -coverprofile cover.out | go-junit-report -iocopy -set-exit-code -out report.xml +test: manifests generate fmt vet envtest go-junit-report ## Run tests and export the result to junit format. + KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) -p path)" go test -v 2>&1 ./... -coverprofile cover.out | $(GO_JUNIT_REPORT) -iocopy -set-exit-code -out report.xml .PHONY: az-login: @@ -321,6 +321,7 @@ ENVTEST ?= $(LOCALBIN)/setup-envtest MOCKGEN ?= $(LOCALBIN)/mockgen PROTOCGEN ?= $(LOCALBIN)/protoc-gen-go PROTOCGEN_GRPC ?= $(LOCALBIN)/protoc-gen-go-grpc +GO_JUNIT_REPORT ?= $(LOCALBIN)/go-junit-report .PHONY: controller-gen controller-gen: $(CONTROLLER_GEN) ## Install controller-gen from vendor dir if necessary. @@ -349,6 +350,10 @@ $(PROTOCGEN): $(LOCALBIN) $(PROTOCGEN_GRPC): $(LOCALBIN) GOBIN=$(LOCALBIN) go install google.golang.org/grpc/cmd/protoc-gen-go-grpc +.PHONY: go-junit-report +go-junit-report: $(GO_JUNIT_REPORT) ## Install go-junit-report from vendor dir if necessary. +$(GO_JUNIT_REPORT): $(LOCALBIN) + test -s $(LOCALBIN)/go-junit-report || GOBIN=$(LOCALBIN) go install github.com/jstemmer/go-junit-report/v2 ################################################## # General # diff --git a/go.mod b/go.mod index 33702d41af0..b9b95dcf704 100644 --- a/go.mod +++ b/go.mod @@ -59,6 +59,7 @@ require ( github.com/influxdata/influxdb-client-go/v2 v2.13.0 github.com/jackc/pgx/v5 v5.5.5 github.com/joho/godotenv v1.5.1 + github.com/jstemmer/go-junit-report/v2 v2.1.0 github.com/microsoft/ApplicationInsights-Go v0.4.4 github.com/microsoft/azure-devops-go-api/azuredevops v1.0.0-b5 github.com/mitchellh/hashstructure v1.1.0 diff --git a/go.sum b/go.sum index 44f33841dfc..52b6356ba09 100644 --- a/go.sum +++ b/go.sum @@ -2023,6 +2023,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jstemmer/go-junit-report/v2 v2.1.0 h1:X3+hPYlSczH9IMIpSC9CQSZA0L+BipYafciZUWHEmsc= +github.com/jstemmer/go-junit-report/v2 v2.1.0/go.mod h1:mgHVr7VUo5Tn8OLVr1cKnLuEy0M92wdRntM99h7RkgQ= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= diff --git a/hack/tools.go b/hack/tools.go index 0fd4fa773de..7bb02fc94ca 100644 --- a/hack/tools.go +++ b/hack/tools.go @@ -6,6 +6,7 @@ package tools import ( // Import code-generator to use in build tools + _ "github.com/jstemmer/go-junit-report/v2" _ "go.uber.org/mock/mockgen" _ "google.golang.org/grpc/cmd/protoc-gen-go-grpc" _ "google.golang.org/protobuf/cmd/protoc-gen-go" diff --git a/pkg/metricsservice/api/metrics.pb.go b/pkg/metricsservice/api/metrics.pb.go index d1069a12a34..a4f0372aa1a 100644 --- a/pkg/metricsservice/api/metrics.pb.go +++ b/pkg/metricsservice/api/metrics.pb.go @@ -16,7 +16,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.34.1 -// protoc v5.26.1 +// protoc v5.27.0 // source: metrics.proto package api diff --git a/pkg/metricsservice/api/metrics_grpc.pb.go b/pkg/metricsservice/api/metrics_grpc.pb.go index 79e23afc5f6..ad6b36d807e 100644 --- a/pkg/metricsservice/api/metrics_grpc.pb.go +++ b/pkg/metricsservice/api/metrics_grpc.pb.go @@ -16,7 +16,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.3.0 -// - protoc v5.26.1 +// - protoc v5.27.0 // source: metrics.proto package api diff --git a/pkg/scalers/externalscaler/externalscaler.pb.go b/pkg/scalers/externalscaler/externalscaler.pb.go index 732b6f6340c..b7dad6dd09c 100644 --- a/pkg/scalers/externalscaler/externalscaler.pb.go +++ b/pkg/scalers/externalscaler/externalscaler.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.34.1 -// protoc v5.26.1 +// protoc v5.27.0 // source: externalscaler.proto package externalscaler diff --git a/pkg/scalers/externalscaler/externalscaler_grpc.pb.go b/pkg/scalers/externalscaler/externalscaler_grpc.pb.go index 9a56ba097d5..a61a3010aaf 100644 --- a/pkg/scalers/externalscaler/externalscaler_grpc.pb.go +++ b/pkg/scalers/externalscaler/externalscaler_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.3.0 -// - protoc v5.26.1 +// - protoc v5.27.0 // source: externalscaler.proto package externalscaler diff --git a/pkg/scalers/liiklus/LiiklusService.pb.go b/pkg/scalers/liiklus/LiiklusService.pb.go index 661b840eddb..b50ab47173b 100644 --- a/pkg/scalers/liiklus/LiiklusService.pb.go +++ b/pkg/scalers/liiklus/LiiklusService.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.34.1 -// protoc v5.26.1 +// protoc v5.27.0 // source: LiiklusService.proto package liiklus diff --git a/pkg/scalers/liiklus/LiiklusService_grpc.pb.go b/pkg/scalers/liiklus/LiiklusService_grpc.pb.go index 737f6979e7e..e24be98970f 100644 --- a/pkg/scalers/liiklus/LiiklusService_grpc.pb.go +++ b/pkg/scalers/liiklus/LiiklusService_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.3.0 -// - protoc v5.26.1 +// - protoc v5.27.0 // source: LiiklusService.proto package liiklus diff --git a/vendor/github.com/jstemmer/go-junit-report/v2/.gitignore b/vendor/github.com/jstemmer/go-junit-report/v2/.gitignore new file mode 100644 index 00000000000..7216c087e16 --- /dev/null +++ b/vendor/github.com/jstemmer/go-junit-report/v2/.gitignore @@ -0,0 +1,2 @@ +go-junit-report +build/ diff --git a/vendor/github.com/jstemmer/go-junit-report/v2/CONTRIBUTING.md b/vendor/github.com/jstemmer/go-junit-report/v2/CONTRIBUTING.md new file mode 100644 index 00000000000..125d350e116 --- /dev/null +++ b/vendor/github.com/jstemmer/go-junit-report/v2/CONTRIBUTING.md @@ -0,0 +1,21 @@ +# Contributing + +## Bug reports + +- Before reporting a bug, have a look at the [issue + list](https://github.com/jstemmer/go-junit-report/issues) to see if an issue + already exists for your problem. +- Include as much information as you can in the bug report, e.g.: the versions + of go-junit-report and the Go compiler, how go-junit-report was called, what + input was given to go-junit-report, what the actual output was, was the + expected output was. + +## Pull requests + +- Before sending a pull request for new features, open an issue to discuss it. +- Run `go fmt` to format your code. +- Add test coverage and run all tests. +- Prefer small PRs, avoid making unrelated changes in the same PR. +- Limit the first line of the commit message to 72 characters. +- Write commit messages in the imperative mood ("Fix bug", not "Fixed bug" or + "Fixes bug") . diff --git a/vendor/github.com/jstemmer/go-junit-report/v2/LICENSE b/vendor/github.com/jstemmer/go-junit-report/v2/LICENSE new file mode 100644 index 00000000000..f346564cefd --- /dev/null +++ b/vendor/github.com/jstemmer/go-junit-report/v2/LICENSE @@ -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. diff --git a/vendor/github.com/jstemmer/go-junit-report/v2/Makefile b/vendor/github.com/jstemmer/go-junit-report/v2/Makefile new file mode 100644 index 00000000000..8a2adf0ef4f --- /dev/null +++ b/vendor/github.com/jstemmer/go-junit-report/v2/Makefile @@ -0,0 +1,27 @@ +VERSION=$(shell git describe --match="v*") +REVISION=$(shell git rev-parse HEAD) +TIMESTAMP=$(shell date +%FT%T) + +test: + go test ./... + +build/go-junit-report build/go-junit-report.exe: clean + go build --ldflags "-s -X main.Version=$(VERSION) -X main.Revision=$(REVISION) -X main.BuildTime=$(TIMESTAMP)" -o $@ + +build/go-junit-report-$(VERSION)-$(GOOS)-$(GOARCH).tar.gz: build/go-junit-report + tar czf $@ -C build go-junit-report + +build/go-junit-report-$(VERSION)-windows-amd64.zip: build/go-junit-report.exe + zip -j $@ build/go-junit-report.exe + +release: test + $(MAKE) GOOS=linux GOARCH=amd64 build/go-junit-report-$(VERSION)-linux-amd64.tar.gz + $(MAKE) GOOS=windows GOARCH=amd64 build/go-junit-report-$(VERSION)-windows-amd64.zip + $(MAKE) GOOS=darwin GOARCH=amd64 build/go-junit-report-$(VERSION)-darwin-amd64.tar.gz + $(MAKE) GOOS=darwin GOARCH=arm64 build/go-junit-report-$(VERSION)-darwin-arm64.tar.gz + +clean: + rm -f build/go-junit-report + rm -f build/go-junit-report.exe + +.PHONY: build clean release test diff --git a/vendor/github.com/jstemmer/go-junit-report/v2/README.md b/vendor/github.com/jstemmer/go-junit-report/v2/README.md new file mode 100644 index 00000000000..c70b67120b2 --- /dev/null +++ b/vendor/github.com/jstemmer/go-junit-report/v2/README.md @@ -0,0 +1,138 @@ +# go-junit-report + +go-junit-report is a tool that converts [`go test`] output to a JUnit compatible +XML report, suitable for use with applications such as [Jenkins]. + +[![Build status](https://github.com/jstemmer/go-junit-report/actions/workflows/main.yml/badge.svg)](https://github.com/jstemmer/go-junit-report/actions) +[![Go Reference](https://pkg.go.dev/badge/github.com/jstemmer/go-junit-report/v2.svg)](https://pkg.go.dev/github.com/jstemmer/go-junit-report/v2) +[![Go Report Card](https://goreportcard.com/badge/github.com/jstemmer/go-junit-report/v2)](https://goreportcard.com/report/github.com/jstemmer/go-junit-report/v2) + +## Install from package (recommended) + +Pre-built packages for Windows, macOS and Linux are found on the [Releases] +page. + +## Install from source + +Download and install the latest stable version from source by running: + +```bash +go install github.com/jstemmer/go-junit-report/v2@latest +``` + +## Usage + +By default, go-junit-report reads `go test -v` output generated by the standard +library [testing] package from `stdin` and writes a JUnit XML report to +`stdout`. + +Go build and runtime errors are also supported, but this requires that `stderr` +is redirected to go-junit-report as well. + +Typical use looks like this: + +```bash +go test -v 2>&1 ./... | go-junit-report -set-exit-code > report.xml +``` + +### More examples + +JSON produced by `go test -json` is supported by the `gojson` parser. Note that +`stderr` still needs to be redirected to go-junit-report in order for build +errors to be detected. For example: + +```bash +go test -json 2>&1 | go-junit-report -parser gojson > report.xml +``` + +Go benchmark output is also supported. The following example runs benchmarks for +the package in the current directory and uses the `-out` flag to write the +output to a file called `report.xml`. + +```bash +go test -v -bench . -count 5 2>&1 | go-junit-report -out report.xml +``` + +The `-iocopy` flag copies `stdin` directly to `stdout`, which is helpful if you +want to see what was sent to go-junit-report. The following example reads test +input from a file called `tests.txt`, copies the input to `stdout` and writes +the output to a file called `report.xml`. + +```bash +go-junit-report -in tests.txt -iocopy -out report.xml +``` + +### Flags + +Run `go-junit-report -help` for a list of all supported flags. + +| Flag | Description | +| -------------------- | ----------- | +| `-in file` | read go test log from `file` | +| `-iocopy` | copy input to stdout; can only be used in conjunction with -out | +| `-no-xml-header` | do not print xml header | +| `-out file` | write XML report to `file` | +| `-package-name name` | specify a default package name to use if output does not contain a package name | +| `-parser parser` | specify the parser to use, available parsers are: `gotest` (default), `gojson` | +| `-p key=value` | add property to generated report; properties should be specified as `key=value` | +| `-set-exit-code` | set exit code to 1 if tests failed | +| `-subtest-mode` | set subtest `mode`, modes are: `ignore-parent-results`, `exclude-parents` | +| `-version` | print version and exit | + +## Go packages + +The test output parser and JUnit XML report generator are also available as Go +packages. This can be helpful if you want to use the `go test` output parser or +create your own custom JUnit reports for example. See the package documentation +on pkg.go.dev for more information: + +- [github.com/jstemmer/go-junit-report/v2/parser/gotest] +- [github.com/jstemmer/go-junit-report/v2/junit] + +## Changelog + +### v2.1.0 + +- Fix #147: Make timestamps in generated report more accurate. +- Fix #140: Escape illegal XML characters in junit output. +- Fix #145: Handle build errors in test packages with the `_test` suffix. +- Fix #145: Don't ignore build errors that did not belong to a package. +- Fix #134: Json test output was not parsed correctly when using the `-race` flag in `go test`. +- Add support for `=== NAME` lines introduced in Go1.20 +- junit: Add File attribute to `testsuite`. +- junit: Allow multiple properties with the same name. +- junit: Add the `Testsuites.WriteXML` convenience method. + +### v2.0.0 + +- Support for parsing `go test -json` output. +- Distinguish between build/runtime errors and test failures. +- JUnit report now includes output for all tests and benchmarks, and global output that doesn't belong to any test. +- Use full Go package name in generated report instead of only last path segment. +- Add support for reading skipped/failed benchmarks. +- Add `-subtest-mode` flag to exclude or ignore results of subtest parent tests. +- Add `-in` and `-out` flags for specifying input and output files respectively. +- Add `-iocopy` flag to copy stdin directly to stdout. +- Add `-prop` flags to set key/value properties in generated report. +- Add `-parser` flag to switch between regular `go test` (default) and `go test -json` parsing. +- Output in JUnit XML is written in `` tags for improved readability. +- Add `hostname`, `timestamp` and `id` attributes to JUnit XML. +- Improve accuracy of benchmark time calculation and update formatting in report. +- No longer strip leading whitespace from test output. +- The `formatter` and `parser` packages have been replaced with `junit` and `parser/gotest` packages respectively. +- Add support for parsing lines longer than 64KiB. +- The JUnit errors/failures attributes are now required fields. +- Drop support for parsing pre-Go1.13 test output. +- Deprecate `-go-version` flag. + +## Contributing + +See [CONTRIBUTING.md]. + +[`go test`]: https://pkg.go.dev/cmd/go#hdr-Test_packages +[Jenkins]: https://www.jenkins.io/ +[github.com/jstemmer/go-junit-report/v2/parser/gotest]: https://pkg.go.dev/github.com/jstemmer/go-junit-report/v2/parser/gotest +[github.com/jstemmer/go-junit-report/v2/junit]: https://pkg.go.dev/github.com/jstemmer/go-junit-report/v2/junit +[Releases]: https://github.com/jstemmer/go-junit-report/releases +[testing]: https://pkg.go.dev/testing +[CONTRIBUTING.md]: https://github.com/jstemmer/go-junit-report/blob/master/CONTRIBUTING.md diff --git a/vendor/github.com/jstemmer/go-junit-report/v2/gtr/gtr.go b/vendor/github.com/jstemmer/go-junit-report/v2/gtr/gtr.go new file mode 100644 index 00000000000..e4f2d25d73c --- /dev/null +++ b/vendor/github.com/jstemmer/go-junit-report/v2/gtr/gtr.go @@ -0,0 +1,145 @@ +// Package gtr defines a standard test report format and provides convenience +// methods to create and convert reports. +package gtr + +import ( + "strings" + "time" +) + +// Result is the result of a test. +type Result int + +// Test results. +const ( + Unknown Result = iota + Pass + Fail + Skip +) + +func (r Result) String() string { + switch r { + case Unknown: + return "UNKNOWN" + case Pass: + return "PASS" + case Fail: + return "FAIL" + case Skip: + return "SKIP" + default: + panic("invalid Result") + } +} + +// Report contains the build and test results of a collection of packages. +type Report struct { + Packages []Package +} + +// IsSuccessful returns true if none of the packages in this report have build +// or runtime errors and all tests passed without failures or were skipped. +func (r *Report) IsSuccessful() bool { + for _, pkg := range r.Packages { + if pkg.BuildError.Name != "" || pkg.RunError.Name != "" { + return false + } + for _, t := range pkg.Tests { + if t.Result != Pass && t.Result != Skip { + return false + } + } + } + return true +} + +// Package contains build and test results for a single package. +type Package struct { + Name string + Timestamp time.Time + Duration time.Duration + Coverage float64 + Output []string + Properties []Property + + Tests []Test + + BuildError Error + RunError Error +} + +// SetProperty stores a key/value property in the current package. If a +// property with the given key already exists, its old value will be +// overwritten with the given value. +func (p *Package) SetProperty(key, value string) { + // TODO(jstemmer): Delete this method in the next major release. + // Delete all the properties whose name is the specified key, + // then add the specified key-value property. + i := 0 + for _, prop := range p.Properties { + if key != prop.Name { + p.Properties[i] = prop + i++ + } + } + p.Properties = p.Properties[:i] + p.AddProperty(key, value) +} + +// AddProperty appends a name/value property in the current package. +func (p *Package) AddProperty(name, value string) { + p.Properties = append(p.Properties, Property{Name: name, Value: value}) +} + +// Property is a name/value property. +type Property struct { + Name, Value string +} + +// Test contains the results of a single test. +type Test struct { + ID int + Name string + Duration time.Duration + Result Result + Level int + Output []string + Data map[string]interface{} +} + +// NewTest creates a new Test with the given id and name. +func NewTest(id int, name string) Test { + return Test{ID: id, Name: name, Data: make(map[string]interface{})} +} + +// Error contains details of a build or runtime error. +type Error struct { + ID int + Name string + Duration time.Duration + Cause string + Output []string +} + +// TrimPrefixSpaces trims the leading whitespace of the given line using the +// indentation level of the test. Printing logs in a Go test is typically +// prepended by blocks of 4 spaces to align it with the rest of the test +// output. TrimPrefixSpaces intends to only trim the whitespace added by the Go +// test command, without inadvertently trimming whitespace added by the test +// author. +func TrimPrefixSpaces(line string, indent int) string { + // We only want to trim the whitespace prefix if it was part of the test + // output. Test output is usually prefixed by a series of 4-space indents, + // so we'll check for that to decide whether this output was likely to be + // from a test. + prefixLen := strings.IndexFunc(line, func(r rune) bool { return r != ' ' }) + if prefixLen%4 == 0 { + // Use the subtest level to trim a consistently sized prefix from the + // output lines. + for i := 0; i <= indent; i++ { + line = strings.TrimPrefix(line, " ") + } + } + return strings.TrimPrefix(line, "\t") +} diff --git a/vendor/github.com/jstemmer/go-junit-report/v2/internal/gojunitreport/go-junit-report.go b/vendor/github.com/jstemmer/go-junit-report/v2/internal/gojunitreport/go-junit-report.go new file mode 100644 index 00000000000..df28d93fd2a --- /dev/null +++ b/vendor/github.com/jstemmer/go-junit-report/v2/internal/gojunitreport/go-junit-report.go @@ -0,0 +1,90 @@ +package gojunitreport + +import ( + "encoding/json" + "encoding/xml" + "fmt" + "io" + "os" + "time" + + "github.com/jstemmer/go-junit-report/v2/gtr" + "github.com/jstemmer/go-junit-report/v2/junit" + "github.com/jstemmer/go-junit-report/v2/parser/gotest" +) + +type parser interface { + Parse(r io.Reader) (gtr.Report, error) + Events() []gotest.Event +} + +// Config contains the go-junit-report command configuration. +type Config struct { + Parser string + Hostname string + PackageName string + SkipXMLHeader bool + SubtestMode gotest.SubtestMode + Properties map[string]string + TimestampFunc func() time.Time + + // For debugging + PrintEvents bool +} + +// Run runs the go-junit-report command and returns the generated report. +func (c Config) Run(input io.Reader, output io.Writer) (*gtr.Report, error) { + var p parser + switch c.Parser { + case "gotest": + p = gotest.NewParser(c.gotestOptions()...) + case "gojson": + p = gotest.NewJSONParser(c.gotestOptions()...) + default: + return nil, fmt.Errorf("invalid parser: %s", c.Parser) + } + + report, err := p.Parse(input) + if err != nil { + return nil, fmt.Errorf("error parsing input: %w", err) + } + + if c.PrintEvents { + enc := json.NewEncoder(os.Stderr) + for _, event := range p.Events() { + if err := enc.Encode(event); err != nil { + return nil, err + } + } + } + + for i := range report.Packages { + for k, v := range c.Properties { + report.Packages[i].SetProperty(k, v) + } + } + + if err = c.writeJunitXML(output, report); err != nil { + return nil, err + } + return &report, nil +} + +func (c Config) writeJunitXML(w io.Writer, report gtr.Report) error { + testsuites := junit.CreateFromReport(report, c.Hostname) + if !c.SkipXMLHeader { + _, err := fmt.Fprintf(w, xml.Header) + if err != nil { + return err + } + } + return testsuites.WriteXML(w) +} + +func (c Config) gotestOptions() []gotest.Option { + return []gotest.Option{ + gotest.PackageName(c.PackageName), + gotest.SetSubtestMode(c.SubtestMode), + gotest.TimestampFunc(c.TimestampFunc), + } +} diff --git a/vendor/github.com/jstemmer/go-junit-report/v2/junit/junit.go b/vendor/github.com/jstemmer/go-junit-report/v2/junit/junit.go new file mode 100644 index 00000000000..e87b6ed0ad5 --- /dev/null +++ b/vendor/github.com/jstemmer/go-junit-report/v2/junit/junit.go @@ -0,0 +1,277 @@ +// Package junit defines a JUnit XML report and includes convenience methods +// for working with these reports. +package junit + +import ( + "encoding/xml" + "fmt" + "io" + "strings" + "time" + + "github.com/jstemmer/go-junit-report/v2/gtr" +) + +// Testsuites is a collection of JUnit testsuites. +type Testsuites struct { + XMLName xml.Name `xml:"testsuites"` + + Name string `xml:"name,attr,omitempty"` + Time string `xml:"time,attr,omitempty"` // total duration in seconds + Tests int `xml:"tests,attr,omitempty"` + Errors int `xml:"errors,attr,omitempty"` + Failures int `xml:"failures,attr,omitempty"` + Skipped int `xml:"skipped,attr,omitempty"` + Disabled int `xml:"disabled,attr,omitempty"` + + Suites []Testsuite `xml:"testsuite,omitempty"` +} + +// AddSuite adds a Testsuite and updates this testssuites' totals. +func (t *Testsuites) AddSuite(ts Testsuite) { + t.Suites = append(t.Suites, ts) + t.Tests += ts.Tests + t.Errors += ts.Errors + t.Failures += ts.Failures + t.Skipped += ts.Skipped + t.Disabled += ts.Disabled +} + +// WriteXML writes the XML representation of Testsuites t to writer w. +func (t *Testsuites) WriteXML(w io.Writer) error { + enc := xml.NewEncoder(w) + enc.Indent("", "\t") + if err := enc.Encode(t); err != nil { + return err + } + if err := enc.Flush(); err != nil { + return err + } + _, err := fmt.Fprintf(w, "\n") + return err +} + +// Testsuite is a single JUnit testsuite containing testcases. +type Testsuite struct { + // required attributes + Name string `xml:"name,attr"` + Tests int `xml:"tests,attr"` + Failures int `xml:"failures,attr"` + Errors int `xml:"errors,attr"` + ID int `xml:"id,attr"` + + // optional attributes + Disabled int `xml:"disabled,attr,omitempty"` + Hostname string `xml:"hostname,attr,omitempty"` + Package string `xml:"package,attr,omitempty"` + Skipped int `xml:"skipped,attr,omitempty"` + Time string `xml:"time,attr"` // duration in seconds + Timestamp string `xml:"timestamp,attr,omitempty"` // date and time in ISO8601 + File string `xml:"file,attr,omitempty"` + + Properties *[]Property `xml:"properties>property,omitempty"` + Testcases []Testcase `xml:"testcase,omitempty"` + SystemOut *Output `xml:"system-out,omitempty"` + SystemErr *Output `xml:"system-err,omitempty"` +} + +// AddProperty adds a property with the given name and value to this Testsuite. +func (t *Testsuite) AddProperty(name, value string) { + prop := Property{Name: name, Value: value} + if t.Properties == nil { + t.Properties = &[]Property{prop} + return + } + props := append(*t.Properties, prop) + t.Properties = &props +} + +// AddTestcase adds Testcase tc to this Testsuite. +func (t *Testsuite) AddTestcase(tc Testcase) { + t.Testcases = append(t.Testcases, tc) + t.Tests++ + + if tc.Error != nil { + t.Errors++ + } + + if tc.Failure != nil { + t.Failures++ + } + + if tc.Skipped != nil { + t.Skipped++ + } +} + +// SetTimestamp sets the timestamp in this Testsuite. +func (t *Testsuite) SetTimestamp(timestamp time.Time) { + t.Timestamp = timestamp.Format(time.RFC3339) +} + +// Testcase represents a single test with its results. +type Testcase struct { + // required attributes + Name string `xml:"name,attr"` + Classname string `xml:"classname,attr"` + + // optional attributes + Time string `xml:"time,attr,omitempty"` // duration in seconds + Status string `xml:"status,attr,omitempty"` + + Skipped *Result `xml:"skipped,omitempty"` + Error *Result `xml:"error,omitempty"` + Failure *Result `xml:"failure,omitempty"` + SystemOut *Output `xml:"system-out,omitempty"` + SystemErr *Output `xml:"system-err,omitempty"` +} + +// Property represents a key/value pair. +type Property struct { + Name string `xml:"name,attr"` + Value string `xml:"value,attr"` +} + +// Result represents the result of a single test. +type Result struct { + Message string `xml:"message,attr"` + Type string `xml:"type,attr,omitempty"` + Data string `xml:",cdata"` +} + +// Output represents output written to stdout or sderr. +type Output struct { + Data string `xml:",cdata"` +} + +// CreateFromReport creates a JUnit representation of the given gtr.Report. +func CreateFromReport(report gtr.Report, hostname string) Testsuites { + var suites Testsuites + for _, pkg := range report.Packages { + var duration time.Duration + suite := Testsuite{ + Name: pkg.Name, + Hostname: hostname, + ID: len(suites.Suites), + } + + if !pkg.Timestamp.IsZero() { + suite.SetTimestamp(pkg.Timestamp) + } + + for _, p := range pkg.Properties { + suite.AddProperty(p.Name, p.Value) + } + + if len(pkg.Output) > 0 { + suite.SystemOut = &Output{Data: formatOutput(pkg.Output)} + } + + if pkg.Coverage > 0 { + suite.AddProperty("coverage.statements.pct", fmt.Sprintf("%.2f", pkg.Coverage)) + } + + for _, test := range pkg.Tests { + duration += test.Duration + suite.AddTestcase(createTestcaseForTest(pkg.Name, test)) + } + + // JUnit doesn't have a good way of dealing with build or runtime + // errors that happen before a test has started, so we create a single + // failing test that contains the build error details. + if pkg.BuildError.Name != "" { + tc := Testcase{ + Classname: pkg.BuildError.Name, + Name: pkg.BuildError.Cause, + Time: formatDuration(0), + Error: &Result{ + Message: "Build error", + Data: strings.Join(pkg.BuildError.Output, "\n"), + }, + } + suite.AddTestcase(tc) + } + + if pkg.RunError.Name != "" { + tc := Testcase{ + Classname: pkg.RunError.Name, + Name: "Failure", + Time: formatDuration(0), + Error: &Result{ + Message: "Runtime error", + Data: strings.Join(pkg.RunError.Output, "\n"), + }, + } + suite.AddTestcase(tc) + } + + if (pkg.Duration) == 0 { + suite.Time = formatDuration(duration) + } else { + suite.Time = formatDuration(pkg.Duration) + } + suites.AddSuite(suite) + } + return suites +} + +func createTestcaseForTest(pkgName string, test gtr.Test) Testcase { + tc := Testcase{ + Classname: pkgName, + Name: test.Name, + Time: formatDuration(test.Duration), + } + + if test.Result == gtr.Fail { + tc.Failure = &Result{ + Message: "Failed", + Data: formatOutput(test.Output), + } + } else if test.Result == gtr.Skip { + tc.Skipped = &Result{ + Message: "Skipped", + Data: formatOutput(test.Output), + } + } else if test.Result == gtr.Unknown { + tc.Error = &Result{ + Message: "No test result found", + Data: formatOutput(test.Output), + } + } else if len(test.Output) > 0 { + tc.SystemOut = &Output{Data: formatOutput(test.Output)} + } + return tc +} + +// formatDuration returns the JUnit string representation of the given +// duration. +func formatDuration(d time.Duration) string { + return fmt.Sprintf("%.3f", d.Seconds()) +} + +// formatOutput combines the lines from the given output into a single string. +func formatOutput(output []string) string { + return escapeIllegalChars(strings.Join(output, "\n")) +} + +func escapeIllegalChars(str string) string { + return strings.Map(func(r rune) rune { + if isInCharacterRange(r) { + return r + } + return '\uFFFD' + }, str) +} + +// Decide whether the given rune is in the XML Character Range, per +// the Char production of https://www.xml.com/axml/testaxml.htm, +// Section 2.2 Characters. +// From: encoding/xml/xml.go +func isInCharacterRange(r rune) (inrange bool) { + return r == 0x09 || + r == 0x0A || + r == 0x0D || + r >= 0x20 && r <= 0xD7FF || + r >= 0xE000 && r <= 0xFFFD || + r >= 0x10000 && r <= 0x10FFFF +} diff --git a/vendor/github.com/jstemmer/go-junit-report/v2/main.go b/vendor/github.com/jstemmer/go-junit-report/v2/main.go new file mode 100644 index 00000000000..947d9498975 --- /dev/null +++ b/vendor/github.com/jstemmer/go-junit-report/v2/main.go @@ -0,0 +1,149 @@ +// go-junit-report converts `go test` output to a JUnit compatible XML report. +// +// See README.md for more information and usage examples. +package main + +import ( + "flag" + "fmt" + "io" + "os" + "strings" + + "github.com/jstemmer/go-junit-report/v2/internal/gojunitreport" + "github.com/jstemmer/go-junit-report/v2/parser/gotest" +) + +// Current release information printed by the -version flag. +var ( + Version = "v2.1.0-dev" + Revision = "HEAD" + BuildTime string +) + +var ( + noXMLHeader = flag.Bool("no-xml-header", false, "do not print xml header") + packageName = flag.String("package-name", "", "specify a default package `name` to use if output does not contain a package name") + setExitCode = flag.Bool("set-exit-code", false, "set exit code to 1 if tests failed") + version = flag.Bool("version", false, "print version") + input = flag.String("in", "", "read go test log from `file`") + output = flag.String("out", "", "write XML report to `file`") + iocopy = flag.Bool("iocopy", false, "copy input to stdout; can only be used in conjunction with -out") + properties = make(keyValueFlag) + parser = flag.String("parser", "gotest", "set input parser: gotest, gojson") + mode = flag.String("subtest-mode", "", "set subtest `mode`: ignore-parent-results (subtest parents always pass), exclude-parents (subtest parents are excluded from the report)") + + // debug flags + printEvents = flag.Bool("debug.print-events", false, "print events generated by the go test parser") + + // deprecated flags + goVersionFlag = flag.String("go-version", "", "(deprecated, use -prop) the value to use for the go.version property in the generated XML") +) + +func main() { + flag.Var(&properties, "p", "add `key=value` property to generated report; repeat this flag to add multiple properties.") + flag.Parse() + + if *iocopy && *output == "" { + exitf("you must specify an output file with -out when using -iocopy") + } + + if *version { + fmt.Printf("go-junit-report %s %s (%s)\n", Version, BuildTime, Revision) + return + } + + if *goVersionFlag != "" { + fmt.Fprintf(os.Stderr, "the -go-version flag is deprecated and will be removed in the future, use the -p flag instead.\n") + properties["go.version"] = *goVersionFlag + } + + subtestMode := gotest.SubtestModeDefault + if *mode != "" { + var err error + if subtestMode, err = gotest.ParseSubtestMode(*mode); err != nil { + exitf("invalid value for -subtest-mode: %s\n", err) + } + } + + if flag.NArg() != 0 { + fmt.Fprintf(os.Stderr, "invalid argument(s): %s\n", strings.Join(flag.Args(), " ")) + fmt.Fprintf(os.Stderr, "%s does not accept positional arguments\n", os.Args[0]) + flag.Usage() + exitf("") + } + + var in io.Reader = os.Stdin + if *input != "" { + f, err := os.Open(*input) + if err != nil { + exitf("error opening input file: %v", err) + } + defer f.Close() + in = f + } + + var out io.Writer = os.Stdout + if *output != "" { + f, err := os.Create(*output) + if err != nil { + exitf("error creating output file: %v", err) + } + defer f.Close() + out = f + } + + if *iocopy { + in = io.TeeReader(in, os.Stdout) + } + + hostname, _ := os.Hostname() // ignore error + + config := gojunitreport.Config{ + Parser: *parser, + Hostname: hostname, + PackageName: *packageName, + SkipXMLHeader: *noXMLHeader, + SubtestMode: subtestMode, + Properties: properties, + PrintEvents: *printEvents, + } + report, err := config.Run(in, out) + if err != nil { + exitf("error: %v\n", err) + } + + if *setExitCode && !report.IsSuccessful() { + os.Exit(1) + } +} + +func exitf(msg string, args ...interface{}) { + if msg != "" { + fmt.Fprintf(os.Stderr, msg+"\n", args...) + } + os.Exit(2) +} + +type keyValueFlag map[string]string + +func (f *keyValueFlag) String() string { + if f != nil { + var pairs []string + for k, v := range *f { + pairs = append(pairs, fmt.Sprintf("%s=%s", k, v)) + } + return strings.Join(pairs, ",") + } + return "" +} + +func (f *keyValueFlag) Set(value string) error { + idx := strings.IndexByte(value, '=') + if idx == -1 { + return fmt.Errorf("%v is not specified as \"key=value\"", value) + } + k, v := value[:idx], value[idx+1:] + (*f)[k] = v + return nil +} diff --git a/vendor/github.com/jstemmer/go-junit-report/v2/parser/gotest/benchmark.go b/vendor/github.com/jstemmer/go-junit-report/v2/parser/gotest/benchmark.go new file mode 100644 index 00000000000..e0568e5c5f8 --- /dev/null +++ b/vendor/github.com/jstemmer/go-junit-report/v2/parser/gotest/benchmark.go @@ -0,0 +1,48 @@ +package gotest + +import ( + "time" + + "github.com/jstemmer/go-junit-report/v2/gtr" +) + +const ( + key = "gotest.benchmark" +) + +// Benchmark contains benchmark results and is intended to be used as extra +// data in a gtr.Test. +type Benchmark struct { + Iterations int64 + NsPerOp float64 + MBPerSec float64 + BytesPerOp int64 + AllocsPerOp int64 +} + +// ApproximateDuration returns the duration calculated by multiplying the +// iterations and average time per iteration (NsPerOp). +func (b Benchmark) ApproximateDuration() time.Duration { + return time.Duration(float64(b.Iterations)*b.NsPerOp) * time.Nanosecond +} + +// GetBenchmarkData is a helper function that returns the benchmark contained +// in the data field of the given gtr.Test t. If no (valid) benchmark is +// present, ok will be set to false. +func GetBenchmarkData(t gtr.Test) (b Benchmark, ok bool) { + if t.Data != nil { + if data, exists := t.Data[key]; exists { + bm, ok := data.(Benchmark) + return bm, ok + } + } + return Benchmark{}, false +} + +// SetBenchmarkData is a helper function that writes the benchmark b to the +// data field of the given gtr.Test t. +func SetBenchmarkData(t *gtr.Test, b Benchmark) { + if t.Data != nil { + t.Data[key] = b + } +} diff --git a/vendor/github.com/jstemmer/go-junit-report/v2/parser/gotest/event.go b/vendor/github.com/jstemmer/go-junit-report/v2/parser/gotest/event.go new file mode 100644 index 00000000000..6802c79a53f --- /dev/null +++ b/vendor/github.com/jstemmer/go-junit-report/v2/parser/gotest/event.go @@ -0,0 +1,37 @@ +package gotest + +import ( + "time" + + "github.com/jstemmer/go-junit-report/v2/parser/gotest/internal/reader" +) + +// Event is a single event in a Go test or benchmark. +type Event struct { + Type string `json:"type"` + + Name string `json:"name,omitempty"` + Package string `json:"pkg,omitempty"` + Result string `json:"result,omitempty"` + Duration time.Duration `json:"duration,omitempty"` + Data string `json:"data,omitempty"` + Indent int `json:"indent,omitempty"` + + // Code coverage + CovPct float64 `json:"coverage_percentage,omitempty"` + CovPackages []string `json:"coverage_packages,omitempty"` + + // Benchmarks + Iterations int64 `json:"benchmark_iterations,omitempty"` + NsPerOp float64 `json:"benchmark_ns_per_op,omitempty"` + MBPerSec float64 `json:"benchmark_mb_per_sec,omitempty"` + BytesPerOp int64 `json:"benchmark_bytes_per_op,omitempty"` + AllocsPerOp int64 `json:"benchmark_allocs_per_op,omitempty"` +} + +func (e *Event) applyMetadata(m *reader.Metadata) { + if e == nil || m == nil { + return + } + e.Package = m.Package +} diff --git a/vendor/github.com/jstemmer/go-junit-report/v2/parser/gotest/gotest.go b/vendor/github.com/jstemmer/go-junit-report/v2/parser/gotest/gotest.go new file mode 100644 index 00000000000..1894758aaac --- /dev/null +++ b/vendor/github.com/jstemmer/go-junit-report/v2/parser/gotest/gotest.go @@ -0,0 +1,347 @@ +// Package gotest is a standard Go test output parser. +package gotest + +import ( + "bufio" + "fmt" + "io" + "regexp" + "strconv" + "strings" + "time" + + "github.com/jstemmer/go-junit-report/v2/gtr" + "github.com/jstemmer/go-junit-report/v2/parser/gotest/internal/reader" +) + +const ( + // maxLineSize is the maximum amount of bytes we'll read for a single line. + // Lines longer than maxLineSize will be truncated. + maxLineSize = 4 * 1024 * 1024 +) + +var ( + // regexBenchInfo captures 3-5 groups: benchmark name, number of times ran, ns/op (with or without decimal), MB/sec (optional), B/op (optional), and allocs/op (optional). + regexBenchmark = regexp.MustCompile(`^(Benchmark[^ -]+)$`) + regexBenchSummary = regexp.MustCompile(`^(Benchmark[^ -]+)(?:-\d+\s+|\s+)(\d+)\s+(\d+|\d+\.\d+)\sns\/op(?:\s+(\d+|\d+\.\d+)\sMB\/s)?(?:\s+(\d+)\sB\/op)?(?:\s+(\d+)\sallocs/op)?`) + regexCoverage = regexp.MustCompile(`^coverage:\s+(\d+|\d+\.\d+)%\s+of\s+statements(?:\sin\s(.+))?$`) + regexEndBenchmark = regexp.MustCompile(`^--- (BENCH|FAIL|SKIP): (Benchmark[^ -]+)(?:-\d+)?$`) + regexEndTest = regexp.MustCompile(`((?: )*)--- (PASS|FAIL|SKIP): ([^ ]+) \((\d+\.\d+)(?: seconds|s)\)`) + regexStatus = regexp.MustCompile(`^(PASS|FAIL|SKIP)$`) + regexSummary = regexp.MustCompile(`` + + // 1: result + `^(\?|ok|FAIL)` + + // 2: package name + `\s+([^ \t]+)` + + // 3: duration (optional) + `(?:\s+(\d+\.\d+)s)?` + + // 4: cached indicator (optional) + `(?:\s+(\(cached\)))?` + + // 5: coverage percentage (optional) + // 6: coverage package list (optional) + `(?:\s+coverage:\s+(?:\[no\sstatements\]|(\d+\.\d+)%\sof\sstatements(?:\sin\s(.+))?))?` + + // 7: [status message] (optional) + `(?:\s+(\[[^\]]+\]))?` + + `$`) +) + +// Option defines options that can be passed to gotest.New. +type Option func(*Parser) + +// PackageName is an Option that sets the default package name to use when it +// cannot be determined from the test output. +func PackageName(name string) Option { + return func(p *Parser) { + p.packageName = name + } +} + +// TimestampFunc is an Option that sets the timestamp function that is used to +// determine the current time when creating the Report. This can be used to +// override the default behaviour of using time.Now(). +func TimestampFunc(f func() time.Time) Option { + return func(p *Parser) { + p.timestampFunc = f + } +} + +// SubtestMode configures how Go subtests should be handled by the parser. +type SubtestMode string + +const ( + // SubtestModeDefault is the default subtest mode. It treats tests with + // subtests as any other tests. + SubtestModeDefault SubtestMode = "" + + // IgnoreParentResults ignores test results for tests with subtests. Use + // this mode if you use subtest parents for common setup/teardown, but are + // not interested in counting them as failed tests. Ignoring their results + // still preserves these tests and their captured output in the report. + IgnoreParentResults SubtestMode = "ignore-parent-results" + + // ExcludeParents excludes tests that contain subtests from the report. + // Note that the subtests themselves are not removed. Use this mode if you + // use subtest parents for common setup/teardown, but are not actually + // interested in their presence in the created report. If output was + // captured for tests that are removed, the output is preserved in the + // global report output. + ExcludeParents SubtestMode = "exclude-parents" +) + +// ParseSubtestMode returns a SubtestMode for the given string. +func ParseSubtestMode(in string) (SubtestMode, error) { + switch in { + case string(IgnoreParentResults): + return IgnoreParentResults, nil + case string(ExcludeParents): + return ExcludeParents, nil + default: + return SubtestModeDefault, fmt.Errorf("unknown subtest mode: %v", in) + } +} + +// SetSubtestMode is an Option to change how the parser handles tests with +// subtests. See the documentation for the individual SubtestModes for more +// information. +func SetSubtestMode(mode SubtestMode) Option { + return func(p *Parser) { + p.subtestMode = mode + } +} + +// Parser is a Go test output Parser. +type Parser struct { + packageName string + subtestMode SubtestMode + + timestampFunc func() time.Time + + events []Event +} + +// NewParser returns a new Go test output parser. +func NewParser(options ...Option) *Parser { + p := &Parser{} + for _, option := range options { + option(p) + } + return p +} + +// Parse parses Go test output from the given io.Reader r and returns +// gtr.Report. +func (p *Parser) Parse(r io.Reader) (gtr.Report, error) { + return p.parse(reader.NewLimitedLineReader(r, maxLineSize)) +} + +func (p *Parser) parse(r reader.LineReader) (gtr.Report, error) { + p.events = nil + + rb := newReportBuilder() + rb.packageName = p.packageName + rb.subtestMode = p.subtestMode + if p.timestampFunc != nil { + rb.timestampFunc = p.timestampFunc + } + + for { + line, metadata, err := r.ReadLine() + if err == io.EOF { + break + } else if err != nil { + return gtr.Report{}, err + } + + var evs []Event + + // Lines that exceed bufio.MaxScanTokenSize are not expected to contain + // any relevant test infrastructure output, so instead of parsing them + // we treat them as regular output to increase performance. + // + // Parser used a bufio.Scanner in the past, which only supported + // reading lines up to bufio.MaxScanTokenSize in length. Since this + // turned out to be fine in almost all cases, it seemed an appropriate + // value to use to decide whether or not to attempt parsing this line. + if len(line) > bufio.MaxScanTokenSize { + evs = p.output(line) + } else { + evs = p.parseLine(line) + } + + for _, ev := range evs { + ev.applyMetadata(metadata) + rb.ProcessEvent(ev) + p.events = append(p.events, ev) + } + } + return rb.Build(), nil +} + +// Events returns the events created by the parser. +func (p *Parser) Events() []Event { + events := make([]Event, len(p.events)) + copy(events, p.events) + return events +} + +func (p *Parser) parseLine(line string) (events []Event) { + if strings.HasPrefix(line, "=== RUN ") { + return p.runTest(strings.TrimSpace(line[8:])) + } else if strings.HasPrefix(line, "=== PAUSE ") { + return p.pauseTest(strings.TrimSpace(line[10:])) + } else if strings.HasPrefix(line, "=== CONT ") { + return p.contTest(strings.TrimSpace(line[9:])) + } else if strings.HasPrefix(line, "=== NAME ") { + // for compatibility with gotest 1.20+ https://go-review.git.corp.google.com/c/go/+/443596 + return p.contTest(strings.TrimSpace(line[9:])) + } else if matches := regexEndTest.FindStringSubmatch(line); len(matches) == 5 { + return p.endTest(line, matches[1], matches[2], matches[3], matches[4]) + } else if matches := regexStatus.FindStringSubmatch(line); len(matches) == 2 { + return p.status(matches[1]) + } else if matches := regexSummary.FindStringSubmatch(line); len(matches) == 8 { + return p.summary(matches[1], matches[2], matches[3], matches[4], matches[7], matches[5], matches[6]) + } else if matches := regexCoverage.FindStringSubmatch(line); len(matches) == 3 { + return p.coverage(matches[1], matches[2]) + } else if matches := regexBenchmark.FindStringSubmatch(line); len(matches) == 2 { + return p.runBench(matches[1]) + } else if matches := regexBenchSummary.FindStringSubmatch(line); len(matches) == 7 { + return p.benchSummary(matches[1], matches[2], matches[3], matches[4], matches[5], matches[6]) + } else if matches := regexEndBenchmark.FindStringSubmatch(line); len(matches) == 3 { + return p.endBench(matches[1], matches[2]) + } else if strings.HasPrefix(line, "# ") { + fields := strings.Fields(strings.TrimPrefix(line, "# ")) + if len(fields) == 1 || len(fields) == 2 { + return p.buildOutput(fields[0]) + } + } + return p.output(line) +} + +func (p *Parser) runTest(name string) []Event { + return []Event{{Type: "run_test", Name: name}} +} + +func (p *Parser) pauseTest(name string) []Event { + return []Event{{Type: "pause_test", Name: name}} +} + +func (p *Parser) contTest(name string) []Event { + return []Event{{Type: "cont_test", Name: name}} +} + +func (p *Parser) endTest(line, indent, result, name, duration string) []Event { + var events []Event + if idx := strings.Index(line, fmt.Sprintf("%s--- %s:", indent, result)); idx > 0 { + events = append(events, p.output(line[:idx])...) + } + _, n := stripIndent(indent) + events = append(events, Event{ + Type: "end_test", + Name: name, + Result: result, + Indent: n, + Duration: parseSeconds(duration), + }) + return events +} + +func (p *Parser) status(result string) []Event { + return []Event{{Type: "status", Result: result}} +} + +func (p *Parser) summary(result, name, duration, cached, status, covpct, packages string) []Event { + return []Event{{ + Type: "summary", + Result: result, + Name: name, + Duration: parseSeconds(duration), + Data: strings.TrimSpace(cached + " " + status), + CovPct: parseFloat(covpct), + CovPackages: parsePackages(packages), + }} +} + +func (p *Parser) coverage(percent, packages string) []Event { + return []Event{{ + Type: "coverage", + CovPct: parseFloat(percent), + CovPackages: parsePackages(packages), + }} +} + +func (p *Parser) runBench(name string) []Event { + return []Event{{ + Type: "run_benchmark", + Name: name, + }} +} + +func (p *Parser) benchSummary(name, iterations, nsPerOp, mbPerSec, bytesPerOp, allocsPerOp string) []Event { + return []Event{{ + Type: "benchmark", + Name: name, + Iterations: parseInt(iterations), + NsPerOp: parseFloat(nsPerOp), + MBPerSec: parseFloat(mbPerSec), + BytesPerOp: parseInt(bytesPerOp), + AllocsPerOp: parseInt(allocsPerOp), + }} +} + +func (p *Parser) endBench(result, name string) []Event { + return []Event{{ + Type: "end_benchmark", + Name: name, + Result: result, + }} +} + +func (p *Parser) buildOutput(packageName string) []Event { + return []Event{{ + Type: "build_output", + Name: packageName, + }} +} + +func (p *Parser) output(line string) []Event { + return []Event{{Type: "output", Data: line}} +} + +func parseSeconds(s string) time.Duration { + if s == "" { + return time.Duration(0) + } + // ignore error + d, _ := time.ParseDuration(s + "s") + return d +} + +func parseFloat(s string) float64 { + if s == "" { + return 0 + } + // ignore error + pct, _ := strconv.ParseFloat(s, 64) + return pct +} + +func parsePackages(pkgList string) []string { + if len(pkgList) == 0 { + return nil + } + return strings.Split(pkgList, ", ") +} + +func parseInt(s string) int64 { + // ignore error + n, _ := strconv.ParseInt(s, 10, 64) + return n +} + +func stripIndent(line string) (string, int) { + var indent int + for indent = 0; strings.HasPrefix(line, " "); indent++ { + line = line[4:] + } + return line, indent +} diff --git a/vendor/github.com/jstemmer/go-junit-report/v2/parser/gotest/internal/collector/collector.go b/vendor/github.com/jstemmer/go-junit-report/v2/parser/gotest/internal/collector/collector.go new file mode 100644 index 00000000000..92d5e1ffe23 --- /dev/null +++ b/vendor/github.com/jstemmer/go-junit-report/v2/parser/gotest/internal/collector/collector.go @@ -0,0 +1,97 @@ +// Package collector collects output lines grouped by id and provides ways to +// retrieve and merge output ordered by the time each line was added. +package collector + +import ( + "sort" + "time" +) + +// line is a single line of output captured at some point in time. +type line struct { + Timestamp time.Time + Text string +} + +// Output stores output lines grouped by id. Output can be retrieved for one or +// more ids and output for different ids can be merged together, while +// preserving their insertion original order based on the time it was +// collected. +// Output also tracks the active id, so you can append output without providing +// an id. +type Output struct { + m map[int][]line + id int // active id +} + +// New returns a new output collector. +func New() *Output { + return &Output{m: make(map[int][]line)} +} + +// Clear deletes all output for the given id. +func (o *Output) Clear(id int) { + delete(o.m, id) +} + +// Append appends the given line of text to the output of the currently active +// id. +func (o *Output) Append(text string) { + o.m[o.id] = append(o.m[o.id], line{time.Now(), text}) +} + +// AppendToID appends the given line of text to the output of the given id. +func (o *Output) AppendToID(id int, text string) { + o.m[id] = append(o.m[id], line{time.Now(), text}) +} + +// Contains returns true if any output lines were collected for the given id. +func (o *Output) Contains(id int) bool { + return len(o.m[id]) > 0 +} + +// Get returns the output lines for the given id. +func (o *Output) Get(id int) []string { + var lines []string + for _, line := range o.m[id] { + lines = append(lines, line.Text) + } + return lines +} + +// GetAll returns the output lines for all ids sorted by the collection +// timestamp of each line of output. +func (o *Output) GetAll(ids ...int) []string { + var output []line + for _, id := range ids { + output = append(output, o.m[id]...) + } + sort.Slice(output, func(i, j int) bool { + return output[i].Timestamp.Before(output[j].Timestamp) + }) + var lines []string + for _, line := range output { + lines = append(lines, line.Text) + } + return lines +} + +// Merge merges the output lines from fromID into intoID, and sorts the output +// by the collection timestamp of each line of output. +func (o *Output) Merge(fromID, intoID int) { + var merged []line + for _, id := range []int{fromID, intoID} { + merged = append(merged, o.m[id]...) + } + sort.Slice(merged, func(i, j int) bool { + return merged[i].Timestamp.Before(merged[j].Timestamp) + }) + o.m[intoID] = merged + delete(o.m, fromID) +} + +// SetActiveID sets the active id. Text appended to this output will be +// associated with the active id. +func (o *Output) SetActiveID(id int) { + o.id = id +} diff --git a/vendor/github.com/jstemmer/go-junit-report/v2/parser/gotest/internal/reader/reader.go b/vendor/github.com/jstemmer/go-junit-report/v2/parser/gotest/internal/reader/reader.go new file mode 100644 index 00000000000..81ff2ded5fa --- /dev/null +++ b/vendor/github.com/jstemmer/go-junit-report/v2/parser/gotest/internal/reader/reader.go @@ -0,0 +1,122 @@ +package reader + +import ( + "bufio" + "bytes" + "encoding/json" + "io" + "strings" + "time" +) + +// LineReader is an interface to read lines with optional Metadata. +type LineReader interface { + ReadLine() (string, *Metadata, error) +} + +// Metadata contains metadata that belongs to a line. +type Metadata struct { + Package string +} + +// LimitedLineReader reads lines from an io.Reader object with a configurable +// line size limit. Lines exceeding the limit will be truncated, but read +// completely from the underlying io.Reader. +type LimitedLineReader struct { + r *bufio.Reader + limit int +} + +var _ LineReader = &LimitedLineReader{} + +// NewLimitedLineReader returns a LimitedLineReader to read lines from r with a +// maximum line size of limit. +func NewLimitedLineReader(r io.Reader, limit int) *LimitedLineReader { + return &LimitedLineReader{r: bufio.NewReader(r), limit: limit} +} + +// ReadLine returns the next line from the underlying reader. The length of the +// line will not exceed the configured limit. ReadLine either returns a line or +// it returns an error, never both. +func (r *LimitedLineReader) ReadLine() (string, *Metadata, error) { + line, isPrefix, err := r.r.ReadLine() + if err != nil { + return "", nil, err + } + + if !isPrefix { + return string(line), nil, nil + } + + // Line is incomplete, keep reading until we reach the end of the line. + var buf bytes.Buffer + buf.Write(line) // ignore err, always nil + for isPrefix { + line, isPrefix, err = r.r.ReadLine() + if err != nil { + return "", nil, err + } + + if buf.Len() >= r.limit { + // Stop writing to buf if we exceed the limit. We continue reading + // however to make sure we consume the entire line. + continue + } + + buf.Write(line) // ignore err, always nil + } + + if buf.Len() > r.limit { + buf.Truncate(r.limit) + } + return buf.String(), nil, nil +} + +// Event represents a JSON event emitted by `go test -json`. +type Event struct { + Time time.Time + Action string + Package string + Test string + Elapsed float64 // seconds + Output string +} + +// JSONEventReader reads JSON events from an io.Reader object. +type JSONEventReader struct { + r *LimitedLineReader +} + +var _ LineReader = &JSONEventReader{} + +// jsonLineLimit is the maximum size of a single JSON line emitted by `go test +// -json`. +const jsonLineLimit = 64 * 1024 + +// NewJSONEventReader returns a JSONEventReader to read the data in JSON +// events from r. +func NewJSONEventReader(r io.Reader) *JSONEventReader { + return &JSONEventReader{NewLimitedLineReader(r, jsonLineLimit)} +} + +// ReadLine returns the next line from the underlying reader. +func (r *JSONEventReader) ReadLine() (string, *Metadata, error) { + for { + line, _, err := r.r.ReadLine() + if err != nil { + return "", nil, err + } + if len(line) == 0 || line[0] != '{' { + return line, nil, nil + } + event := &Event{} + if err := json.Unmarshal([]byte(line), event); err != nil { + return "", nil, err + } + if event.Output == "" { + // Skip events without output + continue + } + return strings.TrimSuffix(event.Output, "\n"), &Metadata{Package: event.Package}, nil + } +} diff --git a/vendor/github.com/jstemmer/go-junit-report/v2/parser/gotest/json.go b/vendor/github.com/jstemmer/go-junit-report/v2/parser/gotest/json.go new file mode 100644 index 00000000000..bb0ccece52a --- /dev/null +++ b/vendor/github.com/jstemmer/go-junit-report/v2/parser/gotest/json.go @@ -0,0 +1,29 @@ +package gotest + +import ( + "io" + + "github.com/jstemmer/go-junit-report/v2/gtr" + "github.com/jstemmer/go-junit-report/v2/parser/gotest/internal/reader" +) + +// NewJSONParser returns a new Go test json output parser. +func NewJSONParser(options ...Option) *JSONParser { + return &JSONParser{gp: NewParser(options...)} +} + +// JSONParser is a `go test -json` output Parser. +type JSONParser struct { + gp *Parser +} + +// Parse parses Go test json output from the given io.Reader r and returns +// gtr.Report. +func (p *JSONParser) Parse(r io.Reader) (gtr.Report, error) { + return p.gp.parse(reader.NewJSONEventReader(r)) +} + +// Events returns the events created by the parser. +func (p *JSONParser) Events() []Event { + return p.gp.Events() +} diff --git a/vendor/github.com/jstemmer/go-junit-report/v2/parser/gotest/report_builder.go b/vendor/github.com/jstemmer/go-junit-report/v2/parser/gotest/report_builder.go new file mode 100644 index 00000000000..5369d8bc2fa --- /dev/null +++ b/vendor/github.com/jstemmer/go-junit-report/v2/parser/gotest/report_builder.go @@ -0,0 +1,499 @@ +package gotest + +import ( + "fmt" + "sort" + "strings" + "time" + + "github.com/jstemmer/go-junit-report/v2/gtr" + "github.com/jstemmer/go-junit-report/v2/parser/gotest/internal/collector" +) + +const ( + globalID = 0 +) + +// reportBuilder helps build a test Report from a collection of events. +// +// The reportBuilder delegates to the packageBuilder for creating packages from +// basic test events, but keeps track of build errors itself. The reportBuilder +// is also responsible for generating unique test id's. +// +// Test output is collected by the output collector, which also keeps track of +// the currently active test so output is automatically associated with the +// correct test. +type reportBuilder struct { + packageBuilders map[string]*packageBuilder + buildErrors map[int]gtr.Error + + nextID int // next free unused id + output *collector.Output // output collected for each id + packages []gtr.Package // completed packages + + // options + packageName string + subtestMode SubtestMode + timestampFunc func() time.Time +} + +// newReportBuilder creates a new reportBuilder. +func newReportBuilder() *reportBuilder { + return &reportBuilder{ + packageBuilders: make(map[string]*packageBuilder), + buildErrors: make(map[int]gtr.Error), + nextID: 1, + output: collector.New(), + timestampFunc: time.Now, + } +} + +// getPackageBuilder returns the packageBuilder for the given packageName. If +// no packageBuilder exists for the given package, a new one is created. +func (b *reportBuilder) getPackageBuilder(packageName string) *packageBuilder { + pb, ok := b.packageBuilders[packageName] + if !ok { + output := b.output + if packageName != "" { + output = collector.New() + } + pb = newPackageBuilder(b.generateID, output) + b.packageBuilders[packageName] = pb + } + return pb +} + +// ProcessEvent takes a test event and adds it to the report. +func (b *reportBuilder) ProcessEvent(ev Event) { + switch ev.Type { + case "run_test": + b.getPackageBuilder(ev.Package).CreateTest(ev.Name) + case "pause_test": + b.getPackageBuilder(ev.Package).PauseTest(ev.Name) + case "cont_test": + b.getPackageBuilder(ev.Package).ContinueTest(ev.Name) + case "end_test": + b.getPackageBuilder(ev.Package).EndTest(ev.Name, ev.Result, ev.Duration, ev.Indent) + case "run_benchmark": + b.getPackageBuilder(ev.Package).CreateTest(ev.Name) + case "benchmark": + b.getPackageBuilder(ev.Package).BenchmarkResult(ev.Name, ev.Iterations, ev.NsPerOp, ev.MBPerSec, ev.BytesPerOp, ev.AllocsPerOp) + case "end_benchmark": + b.getPackageBuilder(ev.Package).EndTest(ev.Name, ev.Result, 0, 0) + case "status": + b.getPackageBuilder(ev.Package).End() + case "summary": + // The summary marks the end of a package. We can now create the actual + // package from all the events we've processed so far for this package. + b.packages = append(b.packages, b.CreatePackage(ev.Package, ev.Name, ev.Result, ev.Duration, ev.Data)) + case "coverage": + b.getPackageBuilder(ev.Package).Coverage(ev.CovPct, ev.CovPackages) + case "build_output": + b.CreateBuildError(ev.Name) + case "output": + if ev.Package != "" { + b.getPackageBuilder(ev.Package).Output(ev.Data) + } else { + b.output.Append(ev.Data) + } + default: + // This shouldn't happen, but just in case print a warning and ignore + // this event. + fmt.Printf("reportBuilder: unhandled event type: %v\n", ev.Type) + } +} + +// newID returns a new unique id. +func (b *reportBuilder) generateID() int { + id := b.nextID + b.nextID++ + return id +} + +// Build returns the new Report containing all the tests, build errors and +// their output created from the processed events. +func (b *reportBuilder) Build() gtr.Report { + // Create packages for any leftover package builders. + for name, pb := range b.packageBuilders { + if pb.IsEmpty() { + continue + } + b.packages = append(b.packages, b.CreatePackage(name, b.packageName, "", 0, "")) + } + + // Create packages for any leftover build errors. + for _, buildErr := range b.buildErrors { + b.packages = append(b.packages, b.CreatePackage("", buildErr.Name, "", 0, "")) + } + return gtr.Report{Packages: b.packages} +} + +// CreateBuildError creates a new build error and marks it as active. +func (b *reportBuilder) CreateBuildError(packageName string) { + id := b.generateID() + b.output.SetActiveID(id) + b.buildErrors[id] = gtr.Error{ID: id, Name: packageName} +} + +// CreatePackage returns a new package containing all the build errors, output, +// tests and benchmarks created so far. The optional packageName is used to +// find the correct reportBuilder. The newPackageName is the actual package +// name that will be given to the returned package, which should be used in +// case the packageName was unknown until this point. +func (b *reportBuilder) CreatePackage(packageName, newPackageName, result string, duration time.Duration, data string) gtr.Package { + pkg := gtr.Package{ + Name: newPackageName, + Duration: duration, + Timestamp: b.timestampFunc(), + } + + // First check if this package contained a build error. If that's the case, + // we won't find any tests in this package. + for id, buildErr := range b.buildErrors { + if buildErr.Name == newPackageName || strings.TrimSuffix(buildErr.Name, "_test") == newPackageName { + pkg.BuildError = buildErr + pkg.BuildError.ID = id + pkg.BuildError.Duration = duration + pkg.BuildError.Cause = data + pkg.BuildError.Output = b.output.Get(id) + + delete(b.buildErrors, id) + b.output.SetActiveID(0) + return pkg + } + } + + // Get the packageBuilder for this package and make sure it's deleted, so + // future events for this package will use a new packageBuilder. + pb := b.getPackageBuilder(packageName) + delete(b.packageBuilders, packageName) + pb.output.SetActiveID(0) + + // If the packageBuilder is empty, we never received any events for this + // package so there's no need to continue. + if pb.IsEmpty() { + // However, we should at least report an error if the result says we + // failed. + if parseResult(result) == gtr.Fail { + pkg.RunError = gtr.Error{ + Name: newPackageName, + } + } + return pkg + } + + // If we've collected output, but there were no tests, then this package + // had a runtime error or it simply didn't have any tests. + if pb.output.Contains(globalID) && len(pb.tests) == 0 { + if parseResult(result) == gtr.Fail { + pkg.RunError = gtr.Error{ + Name: newPackageName, + Output: pb.output.Get(globalID), + } + } else { + pkg.Output = pb.output.Get(globalID) + } + pb.output.Clear(globalID) + return pkg + } + + // If the summary result says we failed, but there were no failing tests + // then something else must have failed. + if parseResult(result) == gtr.Fail && len(pb.tests) > 0 && !pb.containsFailures() { + pkg.RunError = gtr.Error{ + Name: newPackageName, + Output: pb.output.Get(globalID), + } + pb.output.Clear(globalID) + } + + // Collect tests for this package + var tests []gtr.Test + for id, t := range pb.tests { + if pb.isParent(id) { + if b.subtestMode == IgnoreParentResults { + t.Result = gtr.Pass + } else if b.subtestMode == ExcludeParents { + pb.output.Merge(id, globalID) + continue + } + } + t.Output = pb.output.Get(id) + tests = append(tests, t) + } + tests = groupBenchmarksByName(tests, b.output) + + // Sort packages by id to ensure we maintain insertion order. + sort.Slice(tests, func(i, j int) bool { + return tests[i].ID < tests[j].ID + }) + + pkg.Tests = groupBenchmarksByName(tests, pb.output) + pkg.Coverage = pb.coverage + pkg.Output = pb.output.Get(globalID) + pb.output.Clear(globalID) + return pkg +} + +// parseResult returns a gtr.Result for the given result string r. +func parseResult(r string) gtr.Result { + switch r { + case "PASS": + return gtr.Pass + case "FAIL": + return gtr.Fail + case "SKIP": + return gtr.Skip + case "BENCH": + return gtr.Pass + default: + return gtr.Unknown + } +} + +// groupBenchmarksByName groups tests with the Benchmark prefix if they have +// the same name and combines their output. +func groupBenchmarksByName(tests []gtr.Test, output *collector.Output) []gtr.Test { + if len(tests) == 0 { + return nil + } + + var grouped []gtr.Test + byName := make(map[string][]gtr.Test) + for _, test := range tests { + if !strings.HasPrefix(test.Name, "Benchmark") { + // If this test is not a benchmark, we won't group it by name but + // just add it to the final result. + grouped = append(grouped, test) + continue + } + if _, ok := byName[test.Name]; !ok { + grouped = append(grouped, gtr.NewTest(test.ID, test.Name)) + } + byName[test.Name] = append(byName[test.Name], test) + } + + for i, group := range grouped { + if !strings.HasPrefix(group.Name, "Benchmark") { + continue + } + var ( + ids []int + total Benchmark + count int + ) + for _, test := range byName[group.Name] { + ids = append(ids, test.ID) + if test.Result != gtr.Pass { + continue + } + + if bench, ok := GetBenchmarkData(test); ok { + total.Iterations += bench.Iterations + total.NsPerOp += bench.NsPerOp + total.MBPerSec += bench.MBPerSec + total.BytesPerOp += bench.BytesPerOp + total.AllocsPerOp += bench.AllocsPerOp + count++ + } + } + + group.Duration = combinedDuration(byName[group.Name]) + group.Result = groupResults(byName[group.Name]) + group.Output = output.GetAll(ids...) + if count > 0 { + total.Iterations /= int64(count) + total.NsPerOp /= float64(count) + total.MBPerSec /= float64(count) + total.BytesPerOp /= int64(count) + total.AllocsPerOp /= int64(count) + SetBenchmarkData(&group, total) + } + grouped[i] = group + } + return grouped +} + +// combinedDuration returns the sum of the durations of the given tests. +func combinedDuration(tests []gtr.Test) time.Duration { + var total time.Duration + for _, test := range tests { + total += test.Duration + } + return total +} + +// groupResults returns the result we should use for a collection of tests. +func groupResults(tests []gtr.Test) gtr.Result { + var result gtr.Result + for _, test := range tests { + if test.Result == gtr.Fail { + return gtr.Fail + } + if result != gtr.Pass { + result = test.Result + } + } + return result +} + +// packageBuilder helps build a gtr.Package from a collection of test events. +type packageBuilder struct { + generateID func() int + output *collector.Output + + tests map[int]gtr.Test + parentIDs map[int]struct{} // set of test id's that contain subtests + coverage float64 // coverage percentage +} + +// newPackageBuilder creates a new packageBuilder. New tests will be assigned +// an ID returned by the generateID function. The activeIDSetter is called to +// set or reset the active test id. +func newPackageBuilder(generateID func() int, output *collector.Output) *packageBuilder { + return &packageBuilder{ + generateID: generateID, + output: output, + tests: make(map[int]gtr.Test), + parentIDs: make(map[int]struct{}), + } +} + +// IsEmpty returns true if this package builder does not have any tests and has +// not collected any global output. +func (b packageBuilder) IsEmpty() bool { + return len(b.tests) == 0 && !b.output.Contains(0) +} + +// CreateTest adds a test with the given name to the package, marks it as +// active and returns its generated id. +func (b *packageBuilder) CreateTest(name string) int { + if parentID, ok := b.findTestParentID(name); ok { + b.parentIDs[parentID] = struct{}{} + } + id := b.generateID() + b.output.SetActiveID(id) + b.tests[id] = gtr.NewTest(id, name) + return id +} + +// PauseTest marks the test with the given name no longer active. Any results +// or output added to the package after calling PauseTest will no longer be +// associated with this test. +func (b *packageBuilder) PauseTest(name string) { + b.output.SetActiveID(0) +} + +// ContinueTest finds the test with the given name and marks it as active. If +// more than one test exist with this name, the most recently created test will +// be used. +func (b *packageBuilder) ContinueTest(name string) { + id, _ := b.findTest(name) + b.output.SetActiveID(id) +} + +// EndTest finds the test with the given name, sets the result, duration and +// level. If more than one test exists with this name, the most recently +// created test will be used. If no test exists with this name, a new test is +// created. The test is then marked as no longer active. +func (b *packageBuilder) EndTest(name, result string, duration time.Duration, level int) { + id, ok := b.findTest(name) + if !ok { + // test did not exist, create one + // TODO: Likely reason is that the user ran go test without the -v + // flag, should we report this somewhere? + id = b.CreateTest(name) + } + + t := b.tests[id] + t.Result = parseResult(result) + t.Duration = duration + t.Level = level + b.tests[id] = t + b.output.SetActiveID(0) +} + +// End resets the active test. +func (b *packageBuilder) End() { + b.output.SetActiveID(0) +} + +// BenchmarkResult updates an existing or adds a new test with the given +// results and marks it as active. If an existing test with this name exists +// but without result, then that one is updated. Otherwise a new one is added +// to the report. +func (b *packageBuilder) BenchmarkResult(name string, iterations int64, nsPerOp, mbPerSec float64, bytesPerOp, allocsPerOp int64) { + id, ok := b.findTest(name) + if !ok || b.tests[id].Result != gtr.Unknown { + id = b.CreateTest(name) + } + b.output.SetActiveID(id) + + benchmark := Benchmark{iterations, nsPerOp, mbPerSec, bytesPerOp, allocsPerOp} + test := gtr.NewTest(id, name) + test.Result = gtr.Pass + test.Duration = benchmark.ApproximateDuration() + SetBenchmarkData(&test, benchmark) + b.tests[id] = test +} + +// Coverage sets the code coverage percentage. +func (b *packageBuilder) Coverage(pct float64, packages []string) { + b.coverage = pct +} + +// Output appends data to the output of this package. +func (b *packageBuilder) Output(data string) { + b.output.Append(data) +} + +// findTest returns the id of the most recently created test with the given +// name if it exists. +func (b *packageBuilder) findTest(name string) (int, bool) { + var maxid int + for id, test := range b.tests { + if maxid < id && test.Name == name { + maxid = id + } + } + return maxid, maxid > 0 +} + +// findTestParentID searches the existing tests in this package for a parent of +// the test with the given name, and returns its id if one is found. +func (b *packageBuilder) findTestParentID(name string) (int, bool) { + parent := dropLastSegment(name) + for parent != "" { + if id, ok := b.findTest(parent); ok { + return id, true + } + parent = dropLastSegment(parent) + } + return 0, false +} + +// isParent returns true if the test with the given id has sub tests. +func (b *packageBuilder) isParent(id int) bool { + _, ok := b.parentIDs[id] + return ok +} + +// dropLastSegment strips the last `/` and everything following it from the +// given name. If no `/` was found, the empty string is returned. +func dropLastSegment(name string) string { + if idx := strings.LastIndexByte(name, '/'); idx >= 0 { + return name[:idx] + } + return "" +} + +// containsFailures return true if this package contains at least one failing +// test or a test with an unknown result. +func (b *packageBuilder) containsFailures() bool { + for _, test := range b.tests { + if test.Result == gtr.Fail || test.Result == gtr.Unknown { + return true + } + } + return false +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 2c6536d547a..c35508e77a1 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -983,6 +983,15 @@ github.com/jpillora/backoff # github.com/json-iterator/go v1.1.12 ## explicit; go 1.12 github.com/json-iterator/go +# github.com/jstemmer/go-junit-report/v2 v2.1.0 +## explicit; go 1.13 +github.com/jstemmer/go-junit-report/v2 +github.com/jstemmer/go-junit-report/v2/gtr +github.com/jstemmer/go-junit-report/v2/internal/gojunitreport +github.com/jstemmer/go-junit-report/v2/junit +github.com/jstemmer/go-junit-report/v2/parser/gotest +github.com/jstemmer/go-junit-report/v2/parser/gotest/internal/collector +github.com/jstemmer/go-junit-report/v2/parser/gotest/internal/reader # github.com/klauspost/compress v1.17.7 ## explicit; go 1.20 github.com/klauspost/compress