New Upstream Release - gotestsum
Ready changes
Summary
Merged new upstream version: 1.10.1 (was: 1.8.2).
Diff
diff --git a/.circleci/config.yml b/.circleci/config.yml
index d518f4c..124d857 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -7,32 +7,25 @@ workflows:
ci:
jobs:
- go/test:
- name: test-go-1.16
- gotestsum-format: testname
- executor:
- name: go/golang
- tag: 1.16-alpine
-
- - go/test:
- name: test-go-1.17
+ name: test-go-1.18
gotestsum-format: testname
executor:
name: go/golang
- tag: 1.17-alpine
+ tag: 1.18-alpine
- go/test:
- name: test-go-1.18
+ name: test-go-1.19
gotestsum-format: testname
executor:
name: go/golang
- tag: 1.18-alpine
+ tag: 1.19-alpine
- go/test:
- name: test-go-1.19
+ name: test-go-1.20
gotestsum-format: testname
executor:
name: go/golang
- tag: 1.19-alpine
+ tag: 1.20-alpine
- go/test:
name: test-windows
@@ -75,8 +68,8 @@ commands:
- run:
name: Install goreleaser
command: |
- wget https://github.com/goreleaser/goreleaser/releases/download/v1.7.0/goreleaser_Linux_x86_64.tar.gz
- echo "e74934e7571991522324642ac7b032310f04baf192ce2a54db1dc323b97bcd7d goreleaser_Linux_x86_64.tar.gz" > checksum.txt
+ wget https://github.com/goreleaser/goreleaser/releases/download/v1.17.0/goreleaser_Linux_x86_64.tar.gz
+ echo "9fb13d0b9611794da8d71688a50b1f2ea221fcd5f2f4ad529f8b45ee909b2371 goreleaser_Linux_x86_64.tar.gz" > checksum.txt
sha256sum -c checksum.txt
tar -xf goreleaser_Linux_x86_64.tar.gz
mkdir -p ./bin
@@ -92,7 +85,7 @@ jobs:
default: false
executor:
name: go/golang
- tag: 1.19-alpine
+ tag: 1.20-alpine
steps:
- go/install: {package: git}
- go/install-ssh
@@ -105,19 +98,21 @@ jobs:
steps:
run:
name: build binaries
- command: bin/goreleaser --rm-dist --snapshot --config .project/goreleaser.yml
+ command: bin/goreleaser --clean --snapshot --config .project/goreleaser.yml
- when:
condition: << parameters.publish >>
steps:
run:
name: build and publish binaries
- command: bin/goreleaser --rm-dist --skip-validate --config .project/goreleaser.yml
+ command: bin/goreleaser --clean --skip-validate --config .project/goreleaser.yml
- store_artifacts:
path: ./dist
destination: dist
run:
- executor: go/golang
+ executor:
+ name: go/golang
+ tag: 1.20-alpine
steps:
- go/install: {package: git}
- go/install-ssh
@@ -131,7 +126,7 @@ jobs:
lint:
executor:
name: go/golang
- tag: 1.19-alpine
+ tag: 1.20-alpine
steps:
- checkout
- run: go mod download
@@ -141,7 +136,7 @@ jobs:
mkdir -p /go/bin
download=https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh
- wget -O- -q "$download" | sh -s -- -b /go/bin/ v1.48.0
+ wget -O- -q "$download" | sh -s -- -b /go/bin/ v1.51.1
- run:
name: Lint
command: |
diff --git a/.project/golangci-lint.yml b/.project/golangci-lint.yml
index 24ef6a4..5ef5f79 100644
--- a/.project/golangci-lint.yml
+++ b/.project/golangci-lint.yml
@@ -1,6 +1,4 @@
linters-settings:
- gocyclo:
- min-complexity: 12
goconst:
min-len: 2
min-occurrences: 4
@@ -12,6 +10,8 @@ issues:
exclude-rules:
- linters: [revive]
text: 'should have comment .*or be unexported'
+ - linters: [revive]
+ text: 'package-comments: should have a package comment'
- linters: [stylecheck]
text: 'ST1000: at least one file in a package should have a package comment'
- linters: [errcheck]
@@ -33,7 +33,6 @@ linters:
- errcheck
- gocognit
- goconst
- - gocyclo
- gofmt
- goimports
- gosimple
diff --git a/.project/goreleaser.yml b/.project/goreleaser.yml
index 4eea173..3a4cf50 100644
--- a/.project/goreleaser.yml
+++ b/.project/goreleaser.yml
@@ -10,6 +10,7 @@ builds:
- binary: gotestsum
goos:
- darwin
+ - freebsd
- windows
- linux
goarch:
@@ -25,6 +26,12 @@ builds:
goarch: s390x
- goos: darwin
goarch: ppc64le
+ - goos: freebsd
+ goarch: s390x
+ - goos: freebsd
+ goarch: ppc64le
+ - goos: freebsd
+ goarch: arm
- goos: windows
goarch: s390x
- goos: windows
diff --git a/README.md b/README.md
index 04763a6..0373c9e 100644
--- a/README.md
+++ b/README.md
@@ -2,29 +2,36 @@
`gotestsum` runs tests using `go test -json`, prints formatted test output, and a summary of the test run.
It is designed to work well for both local development, and for automation like CI.
+`gotestsum` is [used by](#who-uses-gotestsum) some of the most popular Go projects.
## Install
Download a binary from [releases](https://github.com/gotestyourself/gotestsum/releases), or build from
-source with `go install gotest.tools/gotestsum@latest`. With `go` version before 1.17, use `go get gotest.tools/gotestsum`.
+source with `go install gotest.tools/gotestsum@latest`. To run without installing use
+`go run gotest.tools/gotestsum@latest`.
## Documentation
**Core features**
-- [Output Format](#output-format) from compact to verbose, with color highlighting.
-- [Summary](#summary) of the test run.
-- [Add `go test` flags](#custom-go-test-command), or
- [run a compiled test binary](#executing-a-compiled-test-binary).
+- Change the [test output format](#output-format), from compact to verbose with color highlighting.
+- Print a [summary](#summary) of the test run after running all the tests.
+- Use any [`go test` flag](#custom-go-test-command),
+ run a script with [`--raw-command`](#custom-go-test-command),
+ or [run a compiled test binary](#executing-a-compiled-test-binary).
**CI and Automation**
- [`--junitfile`](#junit-xml-output) - write a JUnit XML file for integration with CI systems.
-- [`--jsonfile`](#json-file-output) - write the `test2json` output in a file.
-- [`--rerun-fails`](#re-running-failed-tests) - run failed tests again to save time when dealing with flaky test suites.
+- [`--jsonfile`](#json-file-output) - write all the [test2json](https://pkg.go.dev/cmd/test2json) input received by `gotestsum` to a file. The file
+ can be used as input to [`gotestsum tool slowest`](#finding-and-skipping-slow-tests), or as a way to
+ store the full verbose output of tests when less verbose output is printed to stdout using a compact [`--format`](#output-format).
+- [`--rerun-fails`](#re-running-failed-tests) - run failed (possibly flaky) tests again to avoid re-running the
+ entire suite. Re-running individual tests can save significant time when working with flaky test suites.
**Local Development**
-- [`--watch`](#run-tests-when-a-file-is-saved) - when a file is saved, run the tests for the package that includes the file.
-- [`--post-run-command`](#post-run-command) - run a command after the tests, can be used for desktop notification.
-- [`gotestsum tool slowest`](#finding-and-skipping-slow-tests) - find the slowest tests, also update slow tests to be skipepd with `-short`.
+- [`--watch`](#run-tests-when-a-file-is-saved) - every time a `.go` file is saved run the tests for the package that changed.
+- [`--post-run-command`](#post-run-command) - run a command after the tests, can be used for desktop notification of the test run.
+- [`gotestsum tool slowest`](#finding-and-skipping-slow-tests) - find the slowest tests, or automatically update the source code of
+ the slowest tests to add a conditional `t.Skip` statements. This statement allows you to skip the slowest tests using `gotestsum -- -short ./...`.
### Output Format
@@ -33,6 +40,9 @@ The `--format` flag or `GOTESTSUM_FORMAT` environment variable set the format th
is used to print the test names, and possibly test output, as the tests run. Most
outputs use color to highlight pass, fail, or skip.
+The `--format-hivis` flag changes the icons used by `pkgname` formats to higher
+visiblity unicode characters.
+
Commonly used formats (see `--help` for a full list):
* `dots` - print a character for each test.
@@ -164,6 +174,20 @@ quoting the whole command.
gotestsum --post-run-command "notify me --date"
```
+**Example: printing slowest tests**
+
+The post-run command can be combined with other `gotestsum` commands and tools to provide
+a more detailed summary. This example uses `gotestsum tool slowest` to print the
+slowest 10 tests after the summary.
+
+```
+gotestsum \
+ --jsonfile tmp.json.log \
+ --post-run-command "bash -c '
+ echo; echo Slowest tests;
+ gotestsum tool slowest --num 10 --jsonfile tmp.json.log'"
+```
+
### Re-running failed tests
When the `--rerun-fails` flag is set, `gotestsum` will re-run any failed tests.
@@ -344,15 +368,22 @@ The next time tests are run using `--short` all the slow tests will be skipped.
When the `--watch` flag is set, `gotestsum` will watch directories using
[file system notifications](https://pkg.go.dev/github.com/fsnotify/fsnotify).
When a Go file in one of those directories is modified, `gotestsum` will run the
-tests for the package which contains the changed file. By default all
-directories with at least one file with a `.go` extension, under the current
-directory will be watched. Use the `--packages` flag to specify a different list.
+tests for the package that contains the changed file. By default all
+directories under the current
+directory with at least one `.go` file will be watched.
+Use the `--packages` flag to specify a different list.
If `--watch` is used with a command line that includes the name of one or more
packages as command line arguments (ex: `gotestsum --watch -- ./...` or
`gotestsum --watch -- ./extrapkg`), the
tests in those packages will also be run when any file changes.
+With the `--watch-chdir` flag, `gotestsum` will change the working directory
+to the directory with the modified file before running tests. Changing the
+directory is primarily useful when the project contains multiple Go modules.
+Without this flag, `go test` will refuse to run tests for any package outside
+of the main Go module.
+
While in watch mode, pressing some keys will perform an action:
* `r` will run tests for the previous event.
@@ -383,12 +414,41 @@ Note that [delve] must be installed in order to use debug (`d`).
gotestsum --watch --format testname
```
+## Who uses gotestsum?
+
+The projects below use (or have used) gotestsum.
+
+* [kubernetes](https://github.com/kubernetes/kubernetes/blob/master/hack/tools/tools.go)
+* [moby](https://github.com/moby/moby/blob/master/hack/test/unit) (aka Docker)
+* [etcd](https://github.com/etcd-io/etcd/blob/main/tools/mod/tools.go)
+* [hashicorp/vault](https://github.com/hashicorp/vault/blob/main/tools/tools.go)
+* [hashicorp/consul](https://github.com/hashicorp/consul/blob/main/.github/workflows/reusable-unit.yml)
+* [prometheus](https://github.com/prometheus/prometheus/blob/main/Makefile.common)
+* [minikube](https://github.com/kubernetes/minikube/blob/master/hack/jenkins/common.ps1)
+* [influxdb](https://github.com/influxdata/influxdb/blob/master/scripts/ci/build-tests.sh)
+* [pulumi](https://github.com/pulumi/pulumi/blob/master/.github/workflows/ci.yml)
+* [grafana/k6](https://github.com/grafana/k6/issues/1986#issuecomment-996625874)
+* [grafana/loki](https://github.com/grafana/loki/blob/main/loki-build-image/Dockerfile)
+* [telegraf](https://github.com/influxdata/telegraf/blob/master/.circleci/config.yml)
+* [containerd](https://github.com/containerd/containerd/blob/main/.cirrus.yml)
+* [linkerd2](https://github.com/linkerd/linkerd2/blob/main/justfile)
+* [elastic/go-elasticsearch](https://github.com/elastic/go-elasticsearch/blob/main/Makefile)
+* [microsoft/hcsshim](https://github.com/microsoft/hcsshim/blob/main/.github/workflows/ci.yml)
+* [pingcap/tidb](https://github.com/pingcap/tidb/blob/master/Makefile)
+* [dex](https://github.com/dexidp/dex/blob/master/Makefile)
+* [coder](https://github.com/coder/coder/blob/main/Makefile)
+* [docker/cli](https://github.com/docker/cli/blob/master/Makefile)
+
+Please open a GitHub issue or pull request to add or remove projects from this list.
+
## Development
[![Godoc](https://godoc.org/gotest.tools/gotestsum?status.svg)](https://pkg.go.dev/gotest.tools/gotestsum?tab=subdirectories)
[![CircleCI](https://circleci.com/gh/gotestyourself/gotestsum/tree/main.svg?style=shield)](https://circleci.com/gh/gotestyourself/gotestsum/tree/main)
+[![Go Recipes](https://raw.githubusercontent.com/nikolaydubina/go-recipes/main/badge.svg?raw=true)](https://github.com/nikolaydubina/go-recipes)
[![Go Reportcard](https://goreportcard.com/badge/gotest.tools/gotestsum)](https://goreportcard.com/report/gotest.tools/gotestsum)
+
Pull requests and bug reports are welcome! Please open an issue first for any
big changes.
diff --git a/cmd/cmd.go b/cmd/cmd.go
deleted file mode 100644
index d539b29..0000000
--- a/cmd/cmd.go
+++ /dev/null
@@ -1,13 +0,0 @@
-package cmd
-
-// Next splits args into the next positional argument and any remaining args.
-func Next(args []string) (string, []string) {
- switch len(args) {
- case 0:
- return "", nil
- case 1:
- return args[0], nil
- default:
- return args[0], args[1:]
- }
-}
diff --git a/cmd/flags.go b/cmd/flags.go
index a5d811b..dc45a99 100644
--- a/cmd/flags.go
+++ b/cmd/flags.go
@@ -132,3 +132,11 @@ func (s *stringSlice) Set(raw string) error {
func (s *stringSlice) Type() string {
return "list"
}
+
+func truthyFlag(s string) bool {
+ switch strings.ToLower(s) {
+ case "true", "yes", "1":
+ return true
+ }
+ return false
+}
diff --git a/cmd/handler.go b/cmd/handler.go
index d67284f..8c47205 100644
--- a/cmd/handler.go
+++ b/cmd/handler.go
@@ -1,6 +1,7 @@
package cmd
import (
+ "bufio"
"fmt"
"io"
"os"
@@ -13,23 +14,33 @@ import (
)
type eventHandler struct {
- formatter testjson.EventFormatter
- err io.Writer
- jsonFile io.WriteCloser
- maxFails int
+ formatter testjson.EventFormatter
+ err *bufio.Writer
+ jsonFile writeSyncer
+ jsonFileTimingEvents writeSyncer
+ maxFails int
}
+type writeSyncer interface {
+ io.WriteCloser
+ Sync() error
+}
+
+// nolint:errcheck
func (h *eventHandler) Err(text string) error {
- _, _ = h.err.Write([]byte(text + "\n"))
+ h.err.WriteString(text)
+ h.err.WriteRune('\n')
+ h.err.Flush()
// always return nil, no need to stop scanning if the stderr write fails
return nil
}
func (h *eventHandler) Event(event testjson.TestEvent, execution *testjson.Execution) error {
- // ignore artificial events with no raw Bytes()
- if h.jsonFile != nil && len(event.Bytes()) > 0 {
- _, err := h.jsonFile.Write(append(event.Bytes(), '\n'))
- if err != nil {
+ if err := writeWithNewline(h.jsonFile, event.Bytes()); err != nil {
+ return fmt.Errorf("failed to write JSON file: %w", err)
+ }
+ if event.Action.IsTerminal() {
+ if err := writeWithNewline(h.jsonFileTimingEvents, event.Bytes()); err != nil {
return fmt.Errorf("failed to write JSON file: %w", err)
}
}
@@ -45,25 +56,55 @@ func (h *eventHandler) Event(event testjson.TestEvent, execution *testjson.Execu
return nil
}
+func writeWithNewline(out io.Writer, b []byte) error {
+ // ignore artificial events that have len(b) == 0
+ if out == nil || len(b) == 0 {
+ return nil
+ }
+ if _, err := out.Write(b); err != nil {
+ return err
+ }
+ _, err := out.Write([]byte{'\n'})
+ return err
+}
+
+func (h *eventHandler) Flush() {
+ if h.jsonFile != nil {
+ if err := h.jsonFile.Sync(); err != nil {
+ log.Errorf("Failed to sync JSON file: %v", err)
+ }
+ }
+ if h.jsonFileTimingEvents != nil {
+ if err := h.jsonFileTimingEvents.Sync(); err != nil {
+ log.Errorf("Failed to sync JSON file: %v", err)
+ }
+ }
+}
+
func (h *eventHandler) Close() error {
if h.jsonFile != nil {
if err := h.jsonFile.Close(); err != nil {
log.Errorf("Failed to close JSON file: %v", err)
}
}
+ if h.jsonFileTimingEvents != nil {
+ if err := h.jsonFileTimingEvents.Close(); err != nil {
+ log.Errorf("Failed to close JSON file: %v", err)
+ }
+ }
return nil
}
var _ testjson.EventHandler = &eventHandler{}
func newEventHandler(opts *options) (*eventHandler, error) {
- formatter := testjson.NewEventFormatter(opts.stdout, opts.format)
+ formatter := testjson.NewEventFormatter(opts.stdout, opts.format, opts.formatOptions)
if formatter == nil {
return nil, fmt.Errorf("unknown format %s", opts.format)
}
handler := &eventHandler{
formatter: formatter,
- err: opts.stderr,
+ err: bufio.NewWriter(opts.stderr),
maxFails: opts.maxFails,
}
var err error
@@ -71,7 +112,14 @@ func newEventHandler(opts *options) (*eventHandler, error) {
_ = os.MkdirAll(filepath.Dir(opts.jsonFile), 0o755)
handler.jsonFile, err = os.Create(opts.jsonFile)
if err != nil {
- return handler, fmt.Errorf("failed to open JSON file: %w", err)
+ return handler, fmt.Errorf("failed to create file: %w", err)
+ }
+ }
+ if opts.jsonFileTimingEvents != "" {
+ _ = os.MkdirAll(filepath.Dir(opts.jsonFileTimingEvents), 0o755)
+ handler.jsonFileTimingEvents, err = os.Create(opts.jsonFileTimingEvents)
+ if err != nil {
+ return handler, fmt.Errorf("failed to create file: %w", err)
}
}
return handler, nil
@@ -96,6 +144,7 @@ func writeJUnitFile(opts *options, execution *testjson.Execution) error {
ProjectName: opts.junitProjectName,
FormatTestSuiteName: opts.junitTestSuiteNameFormat.Value(),
FormatTestCaseClassname: opts.junitTestCaseClassnameFormat.Value(),
+ HideEmptyPackages: opts.junitHideEmptyPackages,
})
}
@@ -111,12 +160,12 @@ func postRunHook(opts *options, execution *testjson.Execution) error {
cmd.Env = append(
os.Environ(),
"GOTESTSUM_JSONFILE="+opts.jsonFile,
+ "GOTESTSUM_JSONFILE_TIMING_EVENTS="+opts.jsonFileTimingEvents,
"GOTESTSUM_JUNITFILE="+opts.junitFile,
fmt.Sprintf("TESTS_TOTAL=%d", execution.Total()),
fmt.Sprintf("TESTS_FAILED=%d", len(execution.Failed())),
fmt.Sprintf("TESTS_SKIPPED=%d", len(execution.Skipped())),
fmt.Sprintf("TESTS_ERRORS=%d", len(execution.Errors())),
)
- // TODO: send a more detailed report to stdin?
return cmd.Run()
}
diff --git a/cmd/handler_test.go b/cmd/handler_test.go
index 07ed874..48322de 100644
--- a/cmd/handler_test.go
+++ b/cmd/handler_test.go
@@ -22,10 +22,11 @@ func TestPostRunHook(t *testing.T) {
buf := new(bytes.Buffer)
opts := &options{
- postRunHookCmd: command,
- jsonFile: "events.json",
- junitFile: "junit.xml",
- stdout: buf,
+ postRunHookCmd: command,
+ jsonFile: "events.json",
+ jsonFileTimingEvents: "timing.json",
+ junitFile: "junit.xml",
+ stdout: buf,
}
env.Patch(t, "GOTESTSUM_FORMAT", "short")
@@ -56,10 +57,12 @@ type bufferCloser struct {
func (bufferCloser) Close() error { return nil }
+func (bufferCloser) Sync() error { return nil }
+
func TestEventHandler_Event_WithMissingActionFail(t *testing.T) {
buf := new(bufferCloser)
errBuf := new(bytes.Buffer)
- format := testjson.NewEventFormatter(errBuf, "testname")
+ format := testjson.NewEventFormatter(errBuf, "testname", testjson.FormatOptions{})
source := golden.Get(t, "../../testjson/testdata/input/go-test-json-missing-test-fail.out")
cfg := testjson.ScanConfig{
@@ -76,7 +79,7 @@ func TestEventHandler_Event_WithMissingActionFail(t *testing.T) {
}
func TestEventHandler_Event_MaxFails(t *testing.T) {
- format := testjson.NewEventFormatter(ioutil.Discard, "testname")
+ format := testjson.NewEventFormatter(ioutil.Discard, "testname", testjson.FormatOptions{})
source := golden.Get(t, "../../testjson/testdata/input/go-test-json.out")
cfg := testjson.ScanConfig{
diff --git a/cmd/main.go b/cmd/main.go
index 553cad5..57d5bca 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -59,6 +59,10 @@ func setupFlags(name string) (*pflag.FlagSet, *options) {
flags.StringVarP(&opts.format, "format", "f",
lookEnvWithDefault("GOTESTSUM_FORMAT", "short"),
"print format of test input")
+ flags.BoolVar(&opts.formatOptions.HideEmptyPackages, "format-hide-empty-pkg",
+ false, "do not print empty packages in compact formats")
+ flags.BoolVar(&opts.formatOptions.UseHiVisibilityIcons, "format-hivis",
+ false, "use high visibility characters in some formats")
flags.BoolVar(&opts.rawCommand, "raw-command", false,
"don't prepend 'go test -json' to the 'go test' command")
flags.BoolVar(&opts.ignoreNonJSONOutputLines, "ignore-non-json-output-lines", false,
@@ -67,6 +71,9 @@ func setupFlags(name string) (*pflag.FlagSet, *options) {
flags.StringVar(&opts.jsonFile, "jsonfile",
lookEnvWithDefault("GOTESTSUM_JSONFILE", ""),
"write all TestEvents to file")
+ flags.StringVar(&opts.jsonFileTimingEvents, "jsonfile-timing-events",
+ lookEnvWithDefault("GOTESTSUM_JSONFILE_TIMING_EVENTS", ""),
+ "write only the pass, skip, and fail TestEvents to the file")
flags.BoolVar(&opts.noColor, "no-color", defaultNoColor, "disable color output")
flags.Var(opts.hideSummary, "no-summary",
@@ -78,6 +85,8 @@ func setupFlags(name string) (*pflag.FlagSet, *options) {
"command to run after the tests have completed")
flags.BoolVar(&opts.watch, "watch", false,
"watch go files, and run tests when a file is modified")
+ flags.BoolVar(&opts.watchChdir, "watch-chdir", false,
+ "in watch mode change the working directory to the directory with the modified file before running tests")
flags.IntVar(&opts.maxFails, "max-fails", 0,
"end the test run after this number of failures")
@@ -91,9 +100,12 @@ func setupFlags(name string) (*pflag.FlagSet, *options) {
flags.StringVar(&opts.junitProjectName, "junitfile-project-name",
lookEnvWithDefault("GOTESTSUM_JUNITFILE_PROJECT_NAME", ""),
"name of the project used in the junit.xml file")
+ flags.BoolVar(&opts.junitHideEmptyPackages, "junitfile-hide-empty-pkg",
+ truthyFlag(lookEnvWithDefault("GOTESTSUM_JUNIT_HIDE_EMPTY_PKG", "")),
+ "omit packages with no tests from the junit.xml file")
flags.IntVar(&opts.rerunFailsMaxAttempts, "rerun-fails", 0,
- "rerun failed tests until they all pass, or attempts exceeds maximum. Defaults to max 2 reruns when enabled.")
+ "rerun failed tests until they all pass, or attempts exceeds maximum. Defaults to max 2 reruns when enabled")
flags.Lookup("rerun-fails").NoOptDefVal = "2"
flags.IntVar(&opts.rerunFailsMaxInitialFailures, "rerun-fails-max-failures", 10,
"do not rerun any tests if the initial run has more than this number of failures")
@@ -101,9 +113,8 @@ func setupFlags(name string) (*pflag.FlagSet, *options) {
"space separated list of package to test")
flags.StringVar(&opts.rerunFailsReportFile, "rerun-fails-report", "",
"write a report to the file, of the tests that were rerun")
- flags.BoolVar(&opts.rerunFailsOnlyRootCases, "rerun-fails-only-root-testcases", false,
- "rerun only root testcaes, instead of only subtests")
- flags.Lookup("rerun-fails-only-root-testcases").Hidden = true
+ flags.BoolVar(&opts.rerunFailsRunRootCases, "rerun-fails-run-root-test", false,
+ "rerun the entire root testcase when any of its subtests fail, instead of only the failed subtest")
flags.BoolVar(&opts.debug, "debug", false, "enabled debug logging")
flags.BoolVar(&opts.version, "version", false, "show version and exit")
@@ -115,24 +126,26 @@ func usage(out io.Writer, name string, flags *pflag.FlagSet) {
%[1]s [flags] [--] [go test flags]
%[1]s [command]
+See https://pkg.go.dev/gotest.tools/gotestsum#section-readme for detailed documentation.
+
Flags:
`, name)
flags.SetOutput(out)
flags.PrintDefaults()
- fmt.Fprint(out, `
+ fmt.Fprintf(out, `
Formats:
- dots print a character for each test
- dots-v2 experimental dots format, one package per line
- pkgname print a line for each package
- pkgname-and-test-fails print a line for each package and failed test output
- testname print a line for each test and package
- standard-quiet standard go test format
- standard-verbose standard go test -v format
+ dots print a character for each test
+ dots-v2 experimental dots format, one package per line
+ pkgname print a line for each package
+ pkgname-and-test-fails print a line for each package and failed test output
+ testname print a line for each test and package
+ standard-quiet standard go test format
+ standard-verbose standard go test -v format
Commands:
- tool tools for working with test2json output
- help print this help next
-`)
+ %[1]s tool slowest find or skip the slowest tests
+ %[1]s help print this help next
+`, name)
}
func lookEnvWithDefault(key, defValue string) string {
@@ -145,10 +158,12 @@ func lookEnvWithDefault(key, defValue string) string {
type options struct {
args []string
format string
+ formatOptions testjson.FormatOptions
debug bool
rawCommand bool
ignoreNonJSONOutputLines bool
jsonFile string
+ jsonFileTimingEvents string
junitFile string
postRunHookCmd *commandValue
noColor bool
@@ -156,12 +171,14 @@ type options struct {
junitTestSuiteNameFormat *junitFieldFormatValue
junitTestCaseClassnameFormat *junitFieldFormatValue
junitProjectName string
+ junitHideEmptyPackages bool
rerunFailsMaxAttempts int
rerunFailsMaxInitialFailures int
rerunFailsReportFile string
- rerunFailsOnlyRootCases bool
+ rerunFailsRunRootCases bool
packages []string
watch bool
+ watchChdir bool
maxFails int
version bool
@@ -173,14 +190,42 @@ type options struct {
func (o options) Validate() error {
if o.rerunFailsMaxAttempts > 0 && len(o.args) > 0 && !o.rawCommand && len(o.packages) == 0 {
return fmt.Errorf(
- "when go test args are used with --rerun-fails-max-attempts " +
+ "when go test args are used with --rerun-fails " +
"the list of packages to test must be specified by the --packages flag")
}
+ if o.rerunFailsMaxAttempts > 0 && boolArgIndex("failfast", o.args) > -1 {
+ return fmt.Errorf("-failfast can not be used with --rerun-fails " +
+ "because not all test cases will run")
+ }
return nil
}
var defaultNoColor = func() bool {
- if os.Getenv("GITHUB_ACTIONS") == "true" {
+ // fatih/color will only output color when stdout is a terminal which is not
+ // true for many CI environments which support color output. So instead, we
+ // try to detect these CI environments via their environment variables.
+ // This code is based on https://github.com/jwalton/go-supportscolor
+ if _, exists := os.LookupEnv("CI"); exists {
+ var ciEnvNames = []string{
+ "APPVEYOR",
+ "BUILDKITE",
+ "CIRCLECI",
+ "DRONE",
+ "GITEA_ACTIONS",
+ "GITHUB_ACTIONS",
+ "GITLAB_CI",
+ "TRAVIS",
+ }
+ for _, ciEnvName := range ciEnvNames {
+ if _, exists := os.LookupEnv(ciEnvName); exists {
+ return false
+ }
+ }
+ if os.Getenv("CI_NAME") == "codeship" {
+ return false
+ }
+ }
+ if _, exists := os.LookupEnv("TEAMCITY_VERSION"); exists {
return false
}
return color.NoColor
@@ -201,7 +246,7 @@ func run(opts *options) error {
return err
}
- goTestProc, err := startGoTestFn(ctx, goTestCmdArgs(opts, rerunOpts{}))
+ goTestProc, err := startGoTestFn(ctx, "", goTestCmdArgs(opts, rerunOpts{}))
if err != nil {
return err
}
@@ -219,9 +264,11 @@ func run(opts *options) error {
IgnoreNonJSONOutputLines: opts.ignoreNonJSONOutputLines,
}
exec, err := testjson.ScanTestOutput(cfg)
+ handler.Flush()
if err != nil {
return finishRun(opts, exec, err)
}
+
exitErr := goTestProc.cmd.Wait()
if signum := atomic.LoadInt32(&goTestProc.signal); signum != 0 {
return finishRun(opts, exec, exitError{num: signalExitCode + int(signum)})
@@ -243,6 +290,7 @@ func run(opts *options) error {
cfg = testjson.ScanConfig{Execution: exec, Handler: handler}
exitErr = rerunFailed(ctx, opts, cfg)
+ handler.Flush()
if err := writeRerunFailsReport(opts, exec); err != nil {
return err
}
@@ -361,13 +409,15 @@ type waiter interface {
Wait() error
}
-func startGoTest(ctx context.Context, args []string) (*proc, error) {
+func startGoTest(ctx context.Context, dir string, args []string) (*proc, error) {
if len(args) == 0 {
return nil, errors.New("missing command to run")
}
cmd := exec.CommandContext(ctx, args[0], args[1:]...)
cmd.Stdin = os.Stdin
+ cmd.Dir = dir
+
p := proc{cmd: cmd}
log.Debugf("exec: %s", cmd.Args)
var err error
diff --git a/cmd/main_e2e_test.go b/cmd/main_e2e_test.go
index b4d6343..1d40f77 100644
--- a/cmd/main_e2e_test.go
+++ b/cmd/main_e2e_test.go
@@ -120,26 +120,17 @@ func osEnviron() map[string]string {
func expectedFilename(name string) string {
ver := runtime.Version()
switch {
- case isPreGo114(ver):
- return name + "-go1.13"
+ case isPreGo120(ver):
+ return name + "-go1.19"
default:
return name
}
}
-// go1.14.6 changed how it prints messages from tests. go1.14.{0-5} used a format
-// that was different from both go1.14.6 and previous versions of Go. These tests
-// no longer support that format.
-func isPreGo114(ver string) bool {
- prefix := "go1.1"
- if !strings.HasPrefix(ver, prefix) || len(ver) < len(prefix)+1 {
- return false
- }
- switch ver[len(prefix)] {
- case '0', '1', '2', '3':
- return true
- }
- return false
+// go1.20.0 changed how it prints messages from subtests. It seems the output
+// has changed back to match the output from go1.14 and earlier.
+func isPreGo120(ver string) bool {
+ return strings.HasPrefix(ver, "go1.1")
}
var binaryFixture pkgFixtureFile
diff --git a/cmd/main_test.go b/cmd/main_test.go
index ae06dc3..526f2ce 100644
--- a/cmd/main_test.go
+++ b/cmd/main_test.go
@@ -5,6 +5,8 @@ import (
"encoding/json"
"os"
"os/exec"
+ "path/filepath"
+ "runtime"
"strings"
"testing"
@@ -13,6 +15,7 @@ import (
"gotest.tools/v3/assert/cmp"
"gotest.tools/v3/env"
"gotest.tools/v3/golden"
+ "gotest.tools/v3/skip"
)
func TestUsage_WithFlagsFromSetupFlags(t *testing.T) {
@@ -70,6 +73,11 @@ func TestOptions_Validate_FromFlags(t *testing.T) {
name: "rerun flag, no go-test args, with packages flag",
args: []string{"--rerun-fails", "--packages", "./..."},
},
+ {
+ name: "rerun-fails with failfast",
+ args: []string{"--rerun-fails", "--packages=./...", "--", "-failfast"},
+ expected: "-failfast can not be used with --rerun-fails",
+ },
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
@@ -440,3 +448,75 @@ func TestRun_InputFromStdin(t *testing.T) {
assert.NilError(t, err)
assert.Assert(t, cmp.Contains(stdout.String(), "DONE 1"))
}
+
+func TestRun_JsonFileIsSyncedBeforePostRunCommand(t *testing.T) {
+ skip.If(t, runtime.GOOS == "windows")
+
+ input := golden.Get(t, "../../testjson/testdata/input/go-test-json.out")
+
+ fn := func(args []string) *proc {
+ return &proc{
+ cmd: fakeWaiter{},
+ stdout: bytes.NewReader(input),
+ stderr: bytes.NewReader(nil),
+ }
+ }
+ reset := patchStartGoTestFn(fn)
+ defer reset()
+
+ tmp := t.TempDir()
+ jsonFile := filepath.Join(tmp, "json.log")
+
+ out := new(bytes.Buffer)
+ opts := &options{
+ rawCommand: true,
+ args: []string{"./test.test"},
+ format: "none",
+ stdout: out,
+ stderr: os.Stderr,
+ hideSummary: &hideSummaryValue{value: testjson.SummarizeNone},
+ jsonFile: jsonFile,
+ postRunHookCmd: &commandValue{
+ command: []string{"cat", jsonFile},
+ },
+ }
+ err := run(opts)
+ assert.NilError(t, err)
+ expected := string(input)
+ _, actual, _ := strings.Cut(out.String(), "s\n") // remove the DONE line
+ assert.Equal(t, actual, expected)
+}
+
+func TestRun_JsonFileTimingEvents(t *testing.T) {
+ input := golden.Get(t, "../../testjson/testdata/input/go-test-json.out")
+
+ fn := func(args []string) *proc {
+ return &proc{
+ cmd: fakeWaiter{},
+ stdout: bytes.NewReader(input),
+ stderr: bytes.NewReader(nil),
+ }
+ }
+ reset := patchStartGoTestFn(fn)
+ defer reset()
+
+ tmp := t.TempDir()
+ jsonFileTiming := filepath.Join(tmp, "json.log")
+
+ out := new(bytes.Buffer)
+ opts := &options{
+ rawCommand: true,
+ args: []string{"./test.test"},
+ format: "none",
+ stdout: out,
+ stderr: os.Stderr,
+ hideSummary: &hideSummaryValue{value: testjson.SummarizeNone},
+ jsonFileTimingEvents: jsonFileTiming,
+ }
+ err := run(opts)
+ assert.NilError(t, err)
+
+ raw, err := os.ReadFile(jsonFileTiming)
+ assert.NilError(t, err)
+ golden.Assert(t, string(raw), "expected-jsonfile-timing-events")
+}
diff --git a/cmd/rerunfails.go b/cmd/rerunfails.go
index 4676301..72fdbcc 100644
--- a/cmd/rerunfails.go
+++ b/cmd/rerunfails.go
@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"os"
+ "regexp"
"sort"
"gotest.tools/gotestsum/testjson"
@@ -35,9 +36,15 @@ func newRerunOptsFromTestCase(tc testjson.TestCase) rerunOpts {
type testCaseFilter func([]testjson.TestCase) []testjson.TestCase
func rerunFailsFilter(o *options) testCaseFilter {
- if o.rerunFailsOnlyRootCases {
+ if o.rerunFailsRunRootCases {
return func(tcs []testjson.TestCase) []testjson.TestCase {
- return tcs
+ var result []testjson.TestCase
+ for _, tc := range tcs {
+ if !tc.Test.IsSubTest() {
+ result = append(result, tc)
+ }
+ }
+ return result
}
}
return testjson.FilterFailedUnique
@@ -55,7 +62,7 @@ func rerunFailed(ctx context.Context, opts *options, scanConfig testjson.ScanCon
nextRec := newFailureRecorder(scanConfig.Handler)
for _, tc := range tcFilter(rec.failures) {
- goTestProc, err := startGoTestFn(ctx, goTestCmdArgs(opts, newRerunOptsFromTestCase(tc)))
+ goTestProc, err := startGoTestFn(ctx, "", goTestCmdArgs(opts, newRerunOptsFromTestCase(tc)))
if err != nil {
return err
}
@@ -131,9 +138,9 @@ func (r *failureRecorder) count() int {
func goTestRunFlagForTestCase(test testjson.TestName) string {
if test.IsSubTest() {
root, sub := test.Split()
- return "-test.run=^" + root + "$/^" + sub + "$"
+ return "-test.run=^" + regexp.QuoteMeta(root) + "$/^" + regexp.QuoteMeta(sub) + "$"
}
- return "-test.run=^" + test.Name() + "$"
+ return "-test.run=^" + regexp.QuoteMeta(test.Name()) + "$"
}
func writeRerunFailsReport(opts *options, exec *testjson.Execution) error {
diff --git a/cmd/rerunfails_test.go b/cmd/rerunfails_test.go
index 0d63c08..d2904c2 100644
--- a/cmd/rerunfails_test.go
+++ b/cmd/rerunfails_test.go
@@ -77,6 +77,10 @@ func TestGoTestRunFlagFromTestCases(t *testing.T) {
input: "TestOne/SubtestA",
expected: "-test.run=^TestOne$/^SubtestA$",
},
+ "sub test case with special characters": {
+ input: "TestOne/Subtest(A)[100]",
+ expected: `-test.run=^TestOne$/^Subtest\(A\)\[100\]$`,
+ },
}
for name := range testCases {
@@ -138,7 +142,7 @@ func TestRerunFailed_ReturnsAnErrorWhenTheLastTestIsSuccessful(t *testing.T) {
func patchStartGoTestFn(f func(args []string) *proc) func() {
orig := startGoTestFn
- startGoTestFn = func(ctx context.Context, args []string) (*proc, error) {
+ startGoTestFn = func(ctx context.Context, dir string, args []string) (*proc, error) {
return f(args), nil
}
return func() {
diff --git a/cmd/testdata/e2e/expected/TestE2E_RerunFails/first_run_has_errors,_abort_rerun-go1.13 b/cmd/testdata/e2e/expected/TestE2E_RerunFails/first_run_has_errors,_abort_rerun-go1.19
similarity index 100%
rename from cmd/testdata/e2e/expected/TestE2E_RerunFails/first_run_has_errors,_abort_rerun-go1.13
rename to cmd/testdata/e2e/expected/TestE2E_RerunFails/first_run_has_errors,_abort_rerun-go1.19
diff --git a/cmd/testdata/e2e/expected/TestE2E_RerunFails/reruns_continues_to_fail-go1.13 b/cmd/testdata/e2e/expected/TestE2E_RerunFails/reruns_continues_to_fail-go1.13
deleted file mode 100644
index 2b4d9ea..0000000
--- a/cmd/testdata/e2e/expected/TestE2E_RerunFails/reruns_continues_to_fail-go1.13
+++ /dev/null
@@ -1,83 +0,0 @@
-PASS cmd/testdata/e2e/flaky.TestAlwaysPasses
-=== RUN TestFailsRarely
-SEED: 0
---- FAIL: TestFailsRarely
- flaky_test.go:51: not this time
-FAIL cmd/testdata/e2e/flaky.TestFailsRarely
-=== RUN TestFailsSometimes
-SEED: 0
---- FAIL: TestFailsSometimes
- flaky_test.go:58: not this time
-FAIL cmd/testdata/e2e/flaky.TestFailsSometimes
-PASS cmd/testdata/e2e/flaky.TestFailsOften/subtest_always_passes
-=== RUN TestFailsOften/subtest_may_fail
- --- FAIL: TestFailsOften/subtest_may_fail
- flaky_test.go:68: not this time
-FAIL cmd/testdata/e2e/flaky.TestFailsOften/subtest_may_fail
-=== RUN TestFailsOften
-SEED: 0
---- FAIL: TestFailsOften
-FAIL cmd/testdata/e2e/flaky.TestFailsOften
-PASS cmd/testdata/e2e/flaky.TestFailsOftenDoesNotPrefixMatch
-PASS cmd/testdata/e2e/flaky.TestFailsSometimesDoesNotPrefixMatch
-FAIL cmd/testdata/e2e/flaky
-
-DONE 8 tests, 4 failures
-
-PASS cmd/testdata/e2e/flaky.TestFailsRarely (re-run 1)
-PASS cmd/testdata/e2e/flaky
-PASS cmd/testdata/e2e/flaky.TestFailsSometimes (re-run 1)
-PASS cmd/testdata/e2e/flaky
-=== RUN TestFailsOften/subtest_may_fail
- --- FAIL: TestFailsOften/subtest_may_fail
- flaky_test.go:68: not this time
-FAIL cmd/testdata/e2e/flaky.TestFailsOften/subtest_may_fail (re-run 1)
-=== RUN TestFailsOften
-SEED: 3
---- FAIL: TestFailsOften
-FAIL cmd/testdata/e2e/flaky.TestFailsOften (re-run 1)
-FAIL cmd/testdata/e2e/flaky
-
-DONE 2 runs, 12 tests, 6 failures
-
-=== RUN TestFailsOften/subtest_may_fail
- --- FAIL: TestFailsOften/subtest_may_fail
- flaky_test.go:68: not this time
-FAIL cmd/testdata/e2e/flaky.TestFailsOften/subtest_may_fail (re-run 2)
-=== RUN TestFailsOften
-SEED: 4
---- FAIL: TestFailsOften
-FAIL cmd/testdata/e2e/flaky.TestFailsOften (re-run 2)
-FAIL cmd/testdata/e2e/flaky
-
-=== Failed
-=== FAIL: cmd/testdata/e2e/flaky TestFailsRarely
-SEED: 0
- flaky_test.go:51: not this time
-
-=== FAIL: cmd/testdata/e2e/flaky TestFailsSometimes
-SEED: 0
- flaky_test.go:58: not this time
-
-=== FAIL: cmd/testdata/e2e/flaky TestFailsOften/subtest_may_fail
- --- FAIL: TestFailsOften/subtest_may_fail
- flaky_test.go:68: not this time
-
-=== FAIL: cmd/testdata/e2e/flaky TestFailsOften
-SEED: 0
-
-=== FAIL: cmd/testdata/e2e/flaky TestFailsOften/subtest_may_fail (re-run 1)
- --- FAIL: TestFailsOften/subtest_may_fail
- flaky_test.go:68: not this time
-
-=== FAIL: cmd/testdata/e2e/flaky TestFailsOften (re-run 1)
-SEED: 3
-
-=== FAIL: cmd/testdata/e2e/flaky TestFailsOften/subtest_may_fail (re-run 2)
- --- FAIL: TestFailsOften/subtest_may_fail
- flaky_test.go:68: not this time
-
-=== FAIL: cmd/testdata/e2e/flaky TestFailsOften (re-run 2)
-SEED: 4
-
-DONE 3 runs, 14 tests, 8 failures
diff --git a/cmd/testdata/e2e/expected/TestE2E_RerunFails/reruns_continues_to_fail b/cmd/testdata/e2e/expected/TestE2E_RerunFails/reruns_continues_to_fail-go1.19
similarity index 100%
rename from cmd/testdata/e2e/expected/TestE2E_RerunFails/reruns_continues_to_fail
rename to cmd/testdata/e2e/expected/TestE2E_RerunFails/reruns_continues_to_fail-go1.19
diff --git a/cmd/testdata/e2e/expected/TestE2E_RerunFails/reruns_until_success-go1.13 b/cmd/testdata/e2e/expected/TestE2E_RerunFails/reruns_until_success-go1.13
deleted file mode 100644
index 52b2b2a..0000000
--- a/cmd/testdata/e2e/expected/TestE2E_RerunFails/reruns_until_success-go1.13
+++ /dev/null
@@ -1,108 +0,0 @@
-PASS cmd/testdata/e2e/flaky.TestAlwaysPasses
-=== RUN TestFailsRarely
-SEED: 0
---- FAIL: TestFailsRarely
- flaky_test.go:51: not this time
-FAIL cmd/testdata/e2e/flaky.TestFailsRarely
-=== RUN TestFailsSometimes
-SEED: 0
---- FAIL: TestFailsSometimes
- flaky_test.go:58: not this time
-FAIL cmd/testdata/e2e/flaky.TestFailsSometimes
-PASS cmd/testdata/e2e/flaky.TestFailsOften/subtest_always_passes
-=== RUN TestFailsOften/subtest_may_fail
- --- FAIL: TestFailsOften/subtest_may_fail
- flaky_test.go:68: not this time
-FAIL cmd/testdata/e2e/flaky.TestFailsOften/subtest_may_fail
-=== RUN TestFailsOften
-SEED: 0
---- FAIL: TestFailsOften
-FAIL cmd/testdata/e2e/flaky.TestFailsOften
-PASS cmd/testdata/e2e/flaky.TestFailsOftenDoesNotPrefixMatch
-PASS cmd/testdata/e2e/flaky.TestFailsSometimesDoesNotPrefixMatch
-FAIL cmd/testdata/e2e/flaky
-
-DONE 8 tests, 4 failures
-
-PASS cmd/testdata/e2e/flaky.TestFailsRarely (re-run 1)
-PASS cmd/testdata/e2e/flaky
-PASS cmd/testdata/e2e/flaky.TestFailsSometimes (re-run 1)
-PASS cmd/testdata/e2e/flaky
-=== RUN TestFailsOften/subtest_may_fail
- --- FAIL: TestFailsOften/subtest_may_fail
- flaky_test.go:68: not this time
-FAIL cmd/testdata/e2e/flaky.TestFailsOften/subtest_may_fail (re-run 1)
-=== RUN TestFailsOften
-SEED: 3
---- FAIL: TestFailsOften
-FAIL cmd/testdata/e2e/flaky.TestFailsOften (re-run 1)
-FAIL cmd/testdata/e2e/flaky
-
-DONE 2 runs, 12 tests, 6 failures
-
-=== RUN TestFailsOften/subtest_may_fail
- --- FAIL: TestFailsOften/subtest_may_fail
- flaky_test.go:68: not this time
-FAIL cmd/testdata/e2e/flaky.TestFailsOften/subtest_may_fail (re-run 2)
-=== RUN TestFailsOften
-SEED: 4
---- FAIL: TestFailsOften
-FAIL cmd/testdata/e2e/flaky.TestFailsOften (re-run 2)
-FAIL cmd/testdata/e2e/flaky
-
-DONE 3 runs, 14 tests, 8 failures
-
-=== RUN TestFailsOften/subtest_may_fail
- --- FAIL: TestFailsOften/subtest_may_fail
- flaky_test.go:68: not this time
-FAIL cmd/testdata/e2e/flaky.TestFailsOften/subtest_may_fail (re-run 3)
-=== RUN TestFailsOften
-SEED: 5
---- FAIL: TestFailsOften
-FAIL cmd/testdata/e2e/flaky.TestFailsOften (re-run 3)
-FAIL cmd/testdata/e2e/flaky
-
-DONE 4 runs, 16 tests, 10 failures
-
-PASS cmd/testdata/e2e/flaky.TestFailsOften/subtest_may_fail (re-run 4)
-PASS cmd/testdata/e2e/flaky.TestFailsOften (re-run 4)
-PASS cmd/testdata/e2e/flaky
-
-=== Failed
-=== FAIL: cmd/testdata/e2e/flaky TestFailsRarely
-SEED: 0
- flaky_test.go:51: not this time
-
-=== FAIL: cmd/testdata/e2e/flaky TestFailsSometimes
-SEED: 0
- flaky_test.go:58: not this time
-
-=== FAIL: cmd/testdata/e2e/flaky TestFailsOften/subtest_may_fail
- --- FAIL: TestFailsOften/subtest_may_fail
- flaky_test.go:68: not this time
-
-=== FAIL: cmd/testdata/e2e/flaky TestFailsOften
-SEED: 0
-
-=== FAIL: cmd/testdata/e2e/flaky TestFailsOften/subtest_may_fail (re-run 1)
- --- FAIL: TestFailsOften/subtest_may_fail
- flaky_test.go:68: not this time
-
-=== FAIL: cmd/testdata/e2e/flaky TestFailsOften (re-run 1)
-SEED: 3
-
-=== FAIL: cmd/testdata/e2e/flaky TestFailsOften/subtest_may_fail (re-run 2)
- --- FAIL: TestFailsOften/subtest_may_fail
- flaky_test.go:68: not this time
-
-=== FAIL: cmd/testdata/e2e/flaky TestFailsOften (re-run 2)
-SEED: 4
-
-=== FAIL: cmd/testdata/e2e/flaky TestFailsOften/subtest_may_fail (re-run 3)
- --- FAIL: TestFailsOften/subtest_may_fail
- flaky_test.go:68: not this time
-
-=== FAIL: cmd/testdata/e2e/flaky TestFailsOften (re-run 3)
-SEED: 5
-
-DONE 5 runs, 18 tests, 10 failures
diff --git a/cmd/testdata/e2e/expected/TestE2E_RerunFails/reruns_until_success b/cmd/testdata/e2e/expected/TestE2E_RerunFails/reruns_until_success-go1.19
similarity index 100%
rename from cmd/testdata/e2e/expected/TestE2E_RerunFails/reruns_until_success
rename to cmd/testdata/e2e/expected/TestE2E_RerunFails/reruns_until_success-go1.19
diff --git a/cmd/testdata/expected-jsonfile-timing-events b/cmd/testdata/expected-jsonfile-timing-events
new file mode 100644
index 0000000..b9031a1
--- /dev/null
+++ b/cmd/testdata/expected-jsonfile-timing-events
@@ -0,0 +1,64 @@
+{"Time":"2022-06-19T13:44:44.851087257-04:00","Action":"fail","Package":"gotest.tools/gotestsum/testjson/internal/badmain","Elapsed":0.001}
+{"Time":"2022-06-19T13:44:44.855151131-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/empty","Elapsed":0}
+{"Time":"2022-06-19T13:44:44.859699224-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestPassed","Elapsed":0}
+{"Time":"2022-06-19T13:44:44.859712195-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestPassedWithLog","Elapsed":0}
+{"Time":"2022-06-19T13:44:44.859724262-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestPassedWithStdout","Elapsed":0}
+{"Time":"2022-06-19T13:44:44.859741082-04:00","Action":"skip","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestSkipped","Elapsed":0}
+{"Time":"2022-06-19T13:44:44.859753158-04:00","Action":"skip","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestSkippedWitLog","Elapsed":0}
+{"Time":"2022-06-19T13:44:44.859765298-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestWithStderr","Elapsed":0}
+{"Time":"2022-06-19T13:44:44.859850991-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestNestedSuccess/a/sub","Elapsed":0}
+{"Time":"2022-06-19T13:44:44.859853265-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestNestedSuccess/a","Elapsed":0}
+{"Time":"2022-06-19T13:44:44.859862788-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestNestedSuccess/b/sub","Elapsed":0}
+{"Time":"2022-06-19T13:44:44.85986508-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestNestedSuccess/b","Elapsed":0}
+{"Time":"2022-06-19T13:44:44.859872517-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestNestedSuccess/c/sub","Elapsed":0}
+{"Time":"2022-06-19T13:44:44.859874632-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestNestedSuccess/c","Elapsed":0}
+{"Time":"2022-06-19T13:44:44.859881961-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestNestedSuccess/d/sub","Elapsed":0}
+{"Time":"2022-06-19T13:44:44.859884191-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestNestedSuccess/d","Elapsed":0}
+{"Time":"2022-06-19T13:44:44.859886372-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestNestedSuccess","Elapsed":0}
+{"Time":"2022-06-19T13:44:44.859895611-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestParallelTheFirst","Elapsed":0.01}
+{"Time":"2022-06-19T13:44:44.859913525-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestParallelTheThird","Elapsed":0}
+{"Time":"2022-06-19T13:44:44.859918336-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestParallelTheSecond","Elapsed":0.01}
+{"Time":"2022-06-19T13:44:44.859926497-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/good","Elapsed":0}
+{"Time":"2022-06-19T13:44:44.914336528-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestPassed","Elapsed":0}
+{"Time":"2022-06-19T13:44:44.914351368-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestPassedWithLog","Elapsed":0}
+{"Time":"2022-06-19T13:44:44.914364274-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestPassedWithStdout","Elapsed":0}
+{"Time":"2022-06-19T13:44:44.914382623-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestWithStderr","Elapsed":0}
+{"Time":"2022-06-19T13:44:44.914503606-04:00","Action":"fail","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestNestedParallelFailures/a","Elapsed":0}
+{"Time":"2022-06-19T13:44:44.914508601-04:00","Action":"fail","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestNestedParallelFailures/d","Elapsed":0}
+{"Time":"2022-06-19T13:44:44.914513457-04:00","Action":"fail","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestNestedParallelFailures/c","Elapsed":0}
+{"Time":"2022-06-19T13:44:44.914518402-04:00","Action":"fail","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestNestedParallelFailures/b","Elapsed":0}
+{"Time":"2022-06-19T13:44:44.914520636-04:00","Action":"fail","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestNestedParallelFailures","Elapsed":0}
+{"Time":"2022-06-19T13:44:44.924699091-04:00","Action":"fail","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestParallelTheFirst","Elapsed":0.01}
+{"Time":"2022-06-19T13:44:44.926895283-04:00","Action":"fail","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestParallelTheThird","Elapsed":0}
+{"Time":"2022-06-19T13:44:44.933108555-04:00","Action":"fail","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestParallelTheSecond","Elapsed":0.01}
+{"Time":"2022-06-19T13:44:44.933277617-04:00","Action":"fail","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Elapsed":0.02}
+{"Time":"2022-06-19T13:44:44.988321998-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestPassed","Elapsed":0}
+{"Time":"2022-06-19T13:44:44.988339579-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestPassedWithLog","Elapsed":0}
+{"Time":"2022-06-19T13:44:44.988360671-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestPassedWithStdout","Elapsed":0}
+{"Time":"2022-06-19T13:44:44.988373636-04:00","Action":"skip","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestSkipped","Elapsed":0}
+{"Time":"2022-06-19T13:44:44.988385879-04:00","Action":"skip","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestSkippedWitLog","Elapsed":0}
+{"Time":"2022-06-19T13:44:44.988400233-04:00","Action":"fail","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestFailed","Elapsed":0}
+{"Time":"2022-06-19T13:44:44.988412375-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestWithStderr","Elapsed":0}
+{"Time":"2022-06-19T13:44:44.988429392-04:00","Action":"fail","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestFailedWithStderr","Elapsed":0}
+{"Time":"2022-06-19T13:44:44.988523195-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedWithFailure/a/sub","Elapsed":0}
+{"Time":"2022-06-19T13:44:44.988525729-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedWithFailure/a","Elapsed":0}
+{"Time":"2022-06-19T13:44:44.988533344-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedWithFailure/b/sub","Elapsed":0}
+{"Time":"2022-06-19T13:44:44.988535616-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedWithFailure/b","Elapsed":0}
+{"Time":"2022-06-19T13:44:44.988540575-04:00","Action":"fail","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedWithFailure/c","Elapsed":0}
+{"Time":"2022-06-19T13:44:44.98855307-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedWithFailure/d/sub","Elapsed":0}
+{"Time":"2022-06-19T13:44:44.988555926-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedWithFailure/d","Elapsed":0}
+{"Time":"2022-06-19T13:44:44.988558303-04:00","Action":"fail","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedWithFailure","Elapsed":0}
+{"Time":"2022-06-19T13:44:44.988613572-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedSuccess/a/sub","Elapsed":0}
+{"Time":"2022-06-19T13:44:44.98861593-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedSuccess/a","Elapsed":0}
+{"Time":"2022-06-19T13:44:44.988623564-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedSuccess/b/sub","Elapsed":0}
+{"Time":"2022-06-19T13:44:44.988625803-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedSuccess/b","Elapsed":0}
+{"Time":"2022-06-19T13:44:44.98863313-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedSuccess/c/sub","Elapsed":0}
+{"Time":"2022-06-19T13:44:44.988635513-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedSuccess/c","Elapsed":0}
+{"Time":"2022-06-19T13:44:44.988643545-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedSuccess/d/sub","Elapsed":0}
+{"Time":"2022-06-19T13:44:44.988645759-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedSuccess/d","Elapsed":0}
+{"Time":"2022-06-19T13:44:44.988647935-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedSuccess","Elapsed":0}
+{"Time":"2022-06-19T13:44:44.988663887-04:00","Action":"skip","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestTimeout","Elapsed":0}
+{"Time":"2022-06-19T13:44:44.998850256-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestParallelTheFirst","Elapsed":0.01}
+{"Time":"2022-06-19T13:44:45.000983481-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestParallelTheThird","Elapsed":0}
+{"Time":"2022-06-19T13:44:45.007374647-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestParallelTheSecond","Elapsed":0.01}
+{"Time":"2022-06-19T13:44:45.00795073-04:00","Action":"fail","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Elapsed":0.02}
diff --git a/cmd/testdata/gotestsum-help-text b/cmd/testdata/gotestsum-help-text
index 9e46882..048054d 100644
--- a/cmd/testdata/gotestsum-help-text
+++ b/cmd/testdata/gotestsum-help-text
@@ -2,35 +2,43 @@ Usage:
gotestsum [flags] [--] [go test flags]
gotestsum [command]
+See https://pkg.go.dev/gotest.tools/gotestsum#section-readme for detailed documentation.
+
Flags:
--debug enabled debug logging
-f, --format string print format of test input (default "short")
+ --format-hide-empty-pkg do not print empty packages in compact formats
+ --format-hivis use high visibility characters in some formats
--hide-summary summary hide sections of the summary: skipped,failed,errors,output (default none)
--jsonfile string write all TestEvents to file
+ --jsonfile-timing-events string write only the pass, skip, and fail TestEvents to the file
--junitfile string write a JUnit XML file
+ --junitfile-hide-empty-pkg omit packages with no tests from the junit.xml file
--junitfile-project-name string name of the project used in the junit.xml file
--junitfile-testcase-classname field-format format the testcase classname field as: full, relative, short (default full)
--junitfile-testsuite-name field-format format the testsuite name field as: full, relative, short (default full)
--max-fails int end the test run after this number of failures
- --no-color disable color output (default true)
+ --no-color disable color output
--packages list space separated list of package to test
--post-run-command command command to run after the tests have completed
--raw-command don't prepend 'go test -json' to the 'go test' command
- --rerun-fails int[=2] rerun failed tests until they all pass, or attempts exceeds maximum. Defaults to max 2 reruns when enabled.
+ --rerun-fails int[=2] rerun failed tests until they all pass, or attempts exceeds maximum. Defaults to max 2 reruns when enabled
--rerun-fails-max-failures int do not rerun any tests if the initial run has more than this number of failures (default 10)
--rerun-fails-report string write a report to the file, of the tests that were rerun
+ --rerun-fails-run-root-test rerun the entire root testcase when any of its subtests fail, instead of only the failed subtest
--version show version and exit
--watch watch go files, and run tests when a file is modified
+ --watch-chdir in watch mode change the working directory to the directory with the modified file before running tests
Formats:
- dots print a character for each test
- dots-v2 experimental dots format, one package per line
- pkgname print a line for each package
- pkgname-and-test-fails print a line for each package and failed test output
- testname print a line for each test and package
- standard-quiet standard go test format
- standard-verbose standard go test -v format
+ dots print a character for each test
+ dots-v2 experimental dots format, one package per line
+ pkgname print a line for each package
+ pkgname-and-test-fails print a line for each package and failed test output
+ testname print a line for each test and package
+ standard-quiet standard go test format
+ standard-verbose standard go test -v format
Commands:
- tool tools for working with test2json output
- help print this help next
+ gotestsum tool slowest find or skip the slowest tests
+ gotestsum help print this help next
diff --git a/cmd/testdata/post-run-hook-expected b/cmd/testdata/post-run-hook-expected
index 8826f12..5c1ebcf 100644
--- a/cmd/testdata/post-run-hook-expected
+++ b/cmd/testdata/post-run-hook-expected
@@ -1,5 +1,6 @@
GOTESTSUM_FORMAT=short
GOTESTSUM_JSONFILE=events.json
+GOTESTSUM_JSONFILE_TIMING_EVENTS=timing.json
GOTESTSUM_JUNITFILE=junit.xml
TESTS_ERRORS=0
TESTS_FAILED=13
diff --git a/cmd/tool/cmd.go b/cmd/tool/cmd.go
deleted file mode 100644
index 1c4c146..0000000
--- a/cmd/tool/cmd.go
+++ /dev/null
@@ -1,33 +0,0 @@
-package tool
-
-import (
- "fmt"
- "os"
-
- "gotest.tools/gotestsum/cmd"
- "gotest.tools/gotestsum/cmd/tool/slowest"
-)
-
-// Run one of the tool commands.
-func Run(name string, args []string) error {
- next, rest := cmd.Next(args)
- switch next {
- case "":
- fmt.Println(usage(name))
- return nil
- case "slowest":
- return slowest.Run(name+" "+next, rest)
- default:
- fmt.Fprintln(os.Stderr, usage(name))
- return fmt.Errorf("invalid command: %v %v", name, next)
- }
-}
-
-func usage(name string) string {
- return fmt.Sprintf(`Usage: %s COMMAND [flags]
-
-Commands: slowest
-
-Use '%s COMMAND --help' for command specific help.
-`, name, name)
-}
diff --git a/cmd/tool/matrix/matrix.go b/cmd/tool/matrix/matrix.go
new file mode 100644
index 0000000..a41752c
--- /dev/null
+++ b/cmd/tool/matrix/matrix.go
@@ -0,0 +1,274 @@
+package matrix
+
+import (
+ "bufio"
+ "encoding/json"
+ "fmt"
+ "io"
+ "math"
+ "os"
+ "path/filepath"
+ "sort"
+ "strings"
+ "time"
+
+ "github.com/dnephin/pflag"
+ "gotest.tools/gotestsum/internal/log"
+ "gotest.tools/gotestsum/testjson"
+)
+
+func Run(name string, args []string) error {
+ flags, opts := setupFlags(name)
+ switch err := flags.Parse(args); {
+ case err == pflag.ErrHelp:
+ return nil
+ case err != nil:
+ usage(os.Stderr, name, flags)
+ return err
+ }
+ opts.stdin = os.Stdin
+ opts.stdout = os.Stdout
+ return run(*opts)
+}
+
+type options struct {
+ numPartitions uint
+ timingFilesPattern string
+ debug bool
+
+ // shims for testing
+ stdin io.Reader
+ stdout io.Writer
+}
+
+func setupFlags(name string) (*pflag.FlagSet, *options) {
+ opts := &options{}
+ flags := pflag.NewFlagSet(name, pflag.ContinueOnError)
+ flags.SetInterspersed(false)
+ flags.Usage = func() {
+ usage(os.Stdout, name, flags)
+ }
+ flags.UintVar(&opts.numPartitions, "partitions", 0,
+ "number of parallel partitions to create in the test matrix")
+ flags.StringVar(&opts.timingFilesPattern, "timing-files", "",
+ "glob pattern to match files that contain test2json events, ex: ./logs/*.log")
+ flags.BoolVar(&opts.debug, "debug", false,
+ "enable debug logging")
+ return flags, opts
+}
+
+func usage(out io.Writer, name string, flags *pflag.FlagSet) {
+ fmt.Fprintf(out, `Usage:
+ %[1]s [flags]
+
+Read a list of packages from stdin and output a GitHub Actions matrix strategy
+that splits the packages by previous run times to minimize overall CI runtime.
+
+ echo -n "matrix=" >> $GITHUB_OUTPUT
+ go list ./... | %[1]s --timing-files ./*.log --partitions 4 >> $GITHUB_OUTPUT
+
+The output of the command is a JSON object that can be used as the matrix
+strategy for a test job.
+
+
+Flags:
+`, name)
+ flags.SetOutput(out)
+ flags.PrintDefaults()
+}
+
+func run(opts options) error {
+ log.SetLevel(log.InfoLevel)
+ if opts.debug {
+ log.SetLevel(log.DebugLevel)
+ }
+ if opts.numPartitions < 2 {
+ return fmt.Errorf("--partitions must be atleast 2")
+ }
+ if opts.timingFilesPattern == "" {
+ return fmt.Errorf("--timing-files is required")
+ }
+
+ pkgs, err := readPackages(opts.stdin)
+ if err != nil {
+ return fmt.Errorf("failed to read packages from stdin: %v", err)
+ }
+
+ files, err := readTimingReports(opts)
+ if err != nil {
+ return fmt.Errorf("failed to read or delete timing files: %v", err)
+ }
+ defer closeFiles(files)
+
+ pkgTiming, err := packageTiming(files)
+ if err != nil {
+ return err
+ }
+
+ buckets := bucketPackages(packagePercentile(pkgTiming), pkgs, opts.numPartitions)
+ return writeMatrix(opts.stdout, buckets)
+}
+
+func readPackages(stdin io.Reader) ([]string, error) {
+ var packages []string
+ scan := bufio.NewScanner(stdin)
+ for scan.Scan() {
+ packages = append(packages, scan.Text())
+ }
+ return packages, scan.Err()
+}
+
+func readTimingReports(opts options) ([]*os.File, error) {
+ fileNames, err := filepath.Glob(opts.timingFilesPattern)
+ if err != nil {
+ return nil, err
+ }
+
+ files := make([]*os.File, 0, len(fileNames))
+ for _, fileName := range fileNames {
+ fh, err := os.Open(fileName)
+ if err != nil {
+ return nil, err
+ }
+ files = append(files, fh)
+ }
+
+ log.Infof("Found %v timing files in %v", len(files), opts.timingFilesPattern)
+ return files, nil
+}
+
+func parseEvent(reader io.Reader) (testjson.TestEvent, error) {
+ event := testjson.TestEvent{}
+ err := json.NewDecoder(reader).Decode(&event)
+ return event, err
+}
+
+func packageTiming(files []*os.File) (map[string][]time.Duration, error) {
+ timing := make(map[string][]time.Duration)
+ for _, fh := range files {
+ exec, err := testjson.ScanTestOutput(testjson.ScanConfig{Stdout: fh})
+ if err != nil {
+ return nil, fmt.Errorf("failed to read events from %v: %v", fh.Name(), err)
+ }
+
+ for _, pkg := range exec.Packages() {
+ timing[pkg] = append(timing[pkg], exec.Package(pkg).Elapsed())
+ }
+ }
+ return timing, nil
+}
+
+func packagePercentile(timing map[string][]time.Duration) map[string]time.Duration {
+ result := make(map[string]time.Duration)
+ for pkg, times := range timing {
+ lenTimes := len(times)
+ if lenTimes == 0 {
+ result[pkg] = 0
+ continue
+ }
+
+ sort.Slice(times, func(i, j int) bool {
+ return times[i] < times[j]
+ })
+
+ r := int(math.Ceil(0.85 * float64(lenTimes)))
+ if r == 0 {
+ result[pkg] = times[0]
+ continue
+ }
+ result[pkg] = times[r-1]
+ }
+ return result
+}
+
+func closeFiles(files []*os.File) {
+ for _, fh := range files {
+ _ = fh.Close()
+ }
+}
+
+func bucketPackages(timing map[string]time.Duration, packages []string, n uint) []bucket {
+ sort.SliceStable(packages, func(i, j int) bool {
+ return timing[packages[i]] >= timing[packages[j]]
+ })
+
+ buckets := make([]bucket, n)
+ for _, pkg := range packages {
+ i := minBucket(buckets)
+ buckets[i].Total += timing[pkg]
+ buckets[i].Packages = append(buckets[i].Packages, pkg)
+ log.Debugf("adding %v (%v) to bucket %v with total %v",
+ pkg, timing[pkg], i, buckets[i].Total)
+ }
+ return buckets
+}
+
+func minBucket(buckets []bucket) int {
+ var n int
+ var min time.Duration = -1
+ for i, b := range buckets {
+ switch {
+ case min < 0 || b.Total < min:
+ min = b.Total
+ n = i
+ case b.Total == min && len(buckets[i].Packages) < len(buckets[n].Packages):
+ n = i
+ }
+ }
+ return n
+}
+
+type bucket struct {
+ Total time.Duration
+ Packages []string
+}
+
+type matrix struct {
+ Include []Partition `json:"include"`
+}
+
+type Partition struct {
+ ID int `json:"id"`
+ EstimatedRuntime string `json:"estimatedRuntime"`
+ Packages string `json:"packages"`
+ Description string `json:"description"`
+}
+
+func writeMatrix(out io.Writer, buckets []bucket) error {
+ m := matrix{Include: make([]Partition, len(buckets))}
+ for i, bucket := range buckets {
+ p := Partition{
+ ID: i,
+ EstimatedRuntime: bucket.Total.String(),
+ Packages: strings.Join(bucket.Packages, " "),
+ }
+ if len(bucket.Packages) > 0 {
+ var extra string
+ if len(bucket.Packages) > 1 {
+ extra = fmt.Sprintf(" and %d others", len(bucket.Packages)-1)
+ }
+ p.Description = fmt.Sprintf("%d - %v%v",
+ p.ID, testjson.RelativePackagePath(bucket.Packages[0]), extra)
+ }
+
+ m.Include[i] = p
+ }
+
+ log.Debugf("%v\n", debugMatrix(m))
+
+ err := json.NewEncoder(out).Encode(m)
+ if err != nil {
+ return fmt.Errorf("failed to json encode output: %v", err)
+ }
+ return nil
+}
+
+type debugMatrix matrix
+
+func (d debugMatrix) String() string {
+ raw, err := json.MarshalIndent(d, "", " ")
+ if err != nil {
+ return fmt.Sprintf("failed to marshal: %v", err.Error())
+ }
+ return string(raw)
+}
diff --git a/cmd/tool/matrix/matrix_test.go b/cmd/tool/matrix/matrix_test.go
new file mode 100644
index 0000000..f4a9b3c
--- /dev/null
+++ b/cmd/tool/matrix/matrix_test.go
@@ -0,0 +1,258 @@
+package matrix
+
+import (
+ "bytes"
+ "encoding/json"
+ "io"
+ "os"
+ "strconv"
+ "strings"
+ "testing"
+ "time"
+
+ "gotest.tools/gotestsum/testjson"
+ "gotest.tools/v3/assert"
+ "gotest.tools/v3/fs"
+)
+
+func TestPackagePercentile(t *testing.T) {
+ ms := time.Millisecond
+ timing := map[string][]time.Duration{
+ "none": {},
+ "one": {time.Second},
+ "two": {4 * ms, 2 * ms},
+ "three": {2 * ms, 3 * ms, 5 * ms},
+ "four": {4 * ms, 3 * ms, ms, 2 * ms},
+ "five": {6 * ms, 2 * ms, 3 * ms, 4 * ms, 9 * ms},
+ "nine": {6 * ms, 2 * ms, 3 * ms, 4 * ms, 9 * ms, 1 * ms, 5 * ms, 7 * ms, 8 * ms},
+ "ten": {6 * ms, 2 * ms, 3 * ms, 4 * ms, 9 * ms, 5 * ms, 7 * ms, 8 * ms, ms, ms},
+ "twenty": {
+ 6 * ms, 2 * ms, 3 * ms, 4 * ms, 9 * ms, 5 * ms, 7 * ms, 8 * ms, ms, ms,
+ 100, 200, 600, 700, 800, 900, 200, 300, 400, 500,
+ },
+ }
+
+ out := packagePercentile(timing)
+ expected := map[string]time.Duration{
+ "none": 0,
+ "one": time.Second,
+ "two": 4 * ms,
+ "three": 5 * ms,
+ "four": 4 * ms,
+ "five": 9 * ms,
+ "nine": 8 * ms,
+ "ten": 8 * ms,
+ "twenty": 6 * ms,
+ }
+ assert.DeepEqual(t, out, expected)
+}
+
+func TestBucketPackages(t *testing.T) {
+ ms := time.Millisecond
+ timing := map[string]time.Duration{
+ "one": 190 * ms,
+ "two": 200 * ms,
+ "three": 3800 * ms,
+ "four": 4000 * ms,
+ "five": 50 * ms,
+ "six": 606 * ms,
+ "rm1": time.Second,
+ "rm2": time.Second,
+ }
+ packages := []string{"new1", "new2", "one", "two", "three", "four", "five", "six"}
+
+ type testCase struct {
+ n uint
+ expected []bucket
+ }
+
+ run := func(t *testing.T, tc testCase) {
+ buckets := bucketPackages(timing, packages, tc.n)
+ assert.DeepEqual(t, buckets, tc.expected)
+ }
+
+ testCases := []testCase{
+ {
+ n: 2,
+ expected: []bucket{
+ 0: {Total: 4440 * ms, Packages: []string{"four", "two", "one", "five"}},
+ 1: {Total: 4406 * ms, Packages: []string{"three", "six", "new2", "new1"}},
+ },
+ },
+ {
+ n: 3,
+ expected: []bucket{
+ 0: {Total: 4000 * ms, Packages: []string{"four"}},
+ 1: {Total: 3800 * ms, Packages: []string{"three"}},
+ 2: {Total: 1046 * ms, Packages: []string{"six", "two", "one", "five", "new1", "new2"}},
+ },
+ },
+ {
+ n: 4,
+ expected: []bucket{
+ 0: {Total: 4000 * ms, Packages: []string{"four"}},
+ 1: {Total: 3800 * ms, Packages: []string{"three"}},
+ 2: {Total: 606 * ms, Packages: []string{"six"}},
+ 3: {Total: 440 * ms, Packages: []string{"two", "one", "five", "new2", "new1"}},
+ },
+ },
+ {
+ n: 8,
+ expected: []bucket{
+ 0: {Total: 4000 * ms, Packages: []string{"four"}},
+ 1: {Total: 3800 * ms, Packages: []string{"three"}},
+ 2: {Total: 606 * ms, Packages: []string{"six"}},
+ 3: {Total: 200 * ms, Packages: []string{"two"}},
+ 4: {Total: 190 * ms, Packages: []string{"one"}},
+ 5: {Total: 50 * ms, Packages: []string{"five"}},
+ 6: {Packages: []string{"new1"}},
+ 7: {Packages: []string{"new2"}},
+ },
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(strconv.FormatUint(uint64(tc.n), 10), func(t *testing.T) {
+ run(t, tc)
+ })
+ }
+}
+
+func TestReadTimingReports(t *testing.T) {
+ events := func(t *testing.T, start time.Time) string {
+ t.Helper()
+ var buf bytes.Buffer
+ encoder := json.NewEncoder(&buf)
+ for _, i := range []int{0, 1, 2} {
+ assert.NilError(t, encoder.Encode(testjson.TestEvent{
+ Time: start.Add(time.Duration(i) * time.Second),
+ Action: testjson.ActionRun,
+ Package: "pkg" + strconv.Itoa(i),
+ }))
+ }
+ return buf.String()
+ }
+
+ now := time.Now()
+ dir := fs.NewDir(t, "timing-files",
+ fs.WithFile("report1.log", events(t, now.Add(-time.Hour))),
+ fs.WithFile("report2.log", events(t, now.Add(-47*time.Hour))),
+ fs.WithFile("report3.log", events(t, now.Add(-49*time.Hour))),
+ fs.WithFile("report4.log", events(t, now.Add(-101*time.Hour))))
+
+ t.Run("match files", func(t *testing.T) {
+ opts := options{
+ timingFilesPattern: dir.Join("*.log"),
+ }
+
+ files, err := readTimingReports(opts)
+ assert.NilError(t, err)
+ defer closeFiles(files)
+ assert.Equal(t, len(files), 4)
+
+ for _, fh := range files {
+ // check the files are properly seeked to 0
+ event, err := parseEvent(fh)
+ assert.NilError(t, err)
+ assert.Equal(t, event.Package, "pkg0")
+ }
+
+ actual, err := os.ReadDir(dir.Path())
+ assert.NilError(t, err)
+ assert.Equal(t, len(actual), 4)
+ })
+
+ t.Run("no glob match, func", func(t *testing.T) {
+ opts := options{
+ timingFilesPattern: dir.Join("*.json"),
+ }
+
+ files, err := readTimingReports(opts)
+ assert.NilError(t, err)
+ assert.Equal(t, len(files), 0)
+ })
+}
+
+func TestRun(t *testing.T) {
+ events := func(t *testing.T) string {
+ t.Helper()
+ var buf bytes.Buffer
+ encoder := json.NewEncoder(&buf)
+ for _, i := range []int{0, 1, 2} {
+ elapsed := time.Duration(i+1) * 2 * time.Second
+ end := time.Now().Add(-5 * time.Second)
+ start := end.Add(-elapsed)
+
+ assert.NilError(t, encoder.Encode(testjson.TestEvent{
+ Time: start,
+ Action: testjson.ActionRun,
+ Package: "pkg" + strconv.Itoa(i),
+ }))
+ assert.NilError(t, encoder.Encode(testjson.TestEvent{
+ Time: end,
+ Action: testjson.ActionPass,
+ Package: "pkg" + strconv.Itoa(i),
+ Elapsed: elapsed.Seconds(),
+ }))
+ }
+ return buf.String()
+ }
+
+ dir := fs.NewDir(t, "timing-files",
+ fs.WithFile("report1.log", events(t)),
+ fs.WithFile("report2.log", events(t)),
+ fs.WithFile("report3.log", events(t)),
+ fs.WithFile("report4.log", events(t)),
+ fs.WithFile("report5.log", events(t)))
+
+ stdout := new(bytes.Buffer)
+ opts := options{
+ numPartitions: 3,
+ timingFilesPattern: dir.Join("*.log"),
+ debug: true,
+ stdout: stdout,
+ stdin: strings.NewReader("pkg0\npkg1\npkg2\nother"),
+ }
+ err := run(opts)
+ assert.NilError(t, err)
+ assert.Equal(t, strings.Count(stdout.String(), "\n"), 1,
+ "the output should be a single line")
+
+ assert.Equal(t, formatJSON(t, stdout), expectedMatrix)
+}
+
+// expectedMatrix can be automatically updated by running tests with -update
+// nolint:lll
+var expectedMatrix = `{
+ "include": [
+ {
+ "description": "0 - pkg2",
+ "estimatedRuntime": "6s",
+ "id": 0,
+ "packages": "pkg2"
+ },
+ {
+ "description": "1 - pkg1",
+ "estimatedRuntime": "4s",
+ "id": 1,
+ "packages": "pkg1"
+ },
+ {
+ "description": "2 - pkg0 and 1 others",
+ "estimatedRuntime": "2s",
+ "id": 2,
+ "packages": "pkg0 other"
+ }
+ ]
+}`
+
+func formatJSON(t *testing.T, v io.Reader) string {
+ t.Helper()
+ raw := map[string]interface{}{}
+ err := json.NewDecoder(v).Decode(&raw)
+ assert.NilError(t, err)
+
+ formatted, err := json.MarshalIndent(raw, "", " ")
+ assert.NilError(t, err)
+ return string(formatted)
+}
diff --git a/cmd/tool/slowest/slowest.go b/cmd/tool/slowest/slowest.go
index a2c1327..5dce14d 100644
--- a/cmd/tool/slowest/slowest.go
+++ b/cmd/tool/slowest/slowest.go
@@ -37,6 +37,8 @@ func setupFlags(name string) (*pflag.FlagSet, *options) {
"path to test2json output, defaults to stdin")
flags.DurationVar(&opts.threshold, "threshold", 100*time.Millisecond,
"test cases with elapsed time greater than threshold are slow tests")
+ flags.IntVar(&opts.topN, "num", 0,
+ "print at most num slowest tests, instead of all tests above the threshold")
flags.StringVar(&opts.skipStatement, "skip-stmt", "",
"add this go statement to slow tests, instead of printing the list of slow tests")
flags.BoolVar(&opts.debug, "debug", false,
@@ -94,6 +96,7 @@ Flags:
type options struct {
threshold time.Duration
+ topN int
jsonfile string
skipStatement string
debug bool
@@ -118,7 +121,7 @@ func run(opts *options) error {
return fmt.Errorf("failed to scan testjson: %v", err)
}
- tcs := aggregate.Slowest(exec, opts.threshold)
+ tcs := aggregate.Slowest(exec, opts.threshold, opts.topN)
if opts.skipStatement != "" {
skipStmt, err := parseSkipStatement(opts.skipStatement)
if err != nil {
diff --git a/cmd/tool/slowest/testdata/cmd-flags-help-text b/cmd/tool/slowest/testdata/cmd-flags-help-text
index 38bf4cd..06a0449 100644
--- a/cmd/tool/slowest/testdata/cmd-flags-help-text
+++ b/cmd/tool/slowest/testdata/cmd-flags-help-text
@@ -42,5 +42,6 @@ https://golang.org/cmd/go/#hdr-Environment_variables.
Flags:
--debug enable debug logging.
--jsonfile string path to test2json output, defaults to stdin
+ --num int print at most num slowest tests, instead of all tests above the threshold
--skip-stmt string add this go statement to slow tests, instead of printing the list of slow tests
--threshold duration test cases with elapsed time greater than threshold are slow tests (default 100ms)
diff --git a/cmd/watch.go b/cmd/watch.go
index a6b6e3a..ec7fcd1 100644
--- a/cmd/watch.go
+++ b/cmd/watch.go
@@ -43,13 +43,18 @@ func (w *watchRuns) run(event filewatcher.Event) error {
return nil
}
+ var dir string
+ if w.opts.watchChdir {
+ dir, event.PkgPath = event.PkgPath, "./"
+ }
+
opts := w.opts // shallow copy opts
opts.packages = append([]string{}, opts.packages...)
opts.packages = append(opts.packages, event.PkgPath)
opts.packages = append(opts.packages, event.Args...)
var err error
- if w.prevExec, err = runSingle(&opts); !IsExitCoder(err) {
+ if w.prevExec, err = runSingle(&opts, dir); !IsExitCoder(err) {
return err
}
return nil
@@ -58,7 +63,7 @@ func (w *watchRuns) run(event filewatcher.Event) error {
// runSingle is similar to run. It doesn't support rerun-fails. It may be
// possible to share runSingle with run, but the defer close on the handler
// would require at least 3 return values, so for now it is a copy.
-func runSingle(opts *options) (*testjson.Execution, error) {
+func runSingle(opts *options, dir string) (*testjson.Execution, error) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
@@ -66,7 +71,7 @@ func runSingle(opts *options) (*testjson.Execution, error) {
return nil, err
}
- goTestProc, err := startGoTestFn(ctx, goTestCmdArgs(opts, rerunOpts{}))
+ goTestProc, err := startGoTestFn(ctx, dir, goTestCmdArgs(opts, rerunOpts{}))
if err != nil {
return nil, err
}
@@ -83,6 +88,7 @@ func runSingle(opts *options) (*testjson.Execution, error) {
Stop: cancel,
}
exec, err := testjson.ScanTestOutput(cfg)
+ handler.Flush()
if err != nil {
return exec, finishRun(opts, exec, err)
}
diff --git a/debian/changelog b/debian/changelog
index 7c15e5a..503b5c0 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+gotestsum (1.10.1-1) UNRELEASED; urgency=low
+
+ * New upstream release.
+
+ -- Debian Janitor <janitor@jelmer.uk> Wed, 23 Aug 2023 04:28:43 -0000
+
gotestsum (1.8.2-1) unstable; urgency=medium
* Team upload
diff --git a/debian/patches/0001-Use-spf13-pflag.patch b/debian/patches/0001-Use-spf13-pflag.patch
index 56240ba..40e835e 100644
--- a/debian/patches/0001-Use-spf13-pflag.patch
+++ b/debian/patches/0001-Use-spf13-pflag.patch
@@ -10,10 +10,10 @@ the dnephin/pflag is not packaged.
cmd/tool/slowest/slowest.go | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
-diff --git a/cmd/flags.go b/cmd/flags.go
-index a5d811b..d584279 100644
---- a/cmd/flags.go
-+++ b/cmd/flags.go
+Index: gotestsum.git/cmd/flags.go
+===================================================================
+--- gotestsum.git.orig/cmd/flags.go
++++ gotestsum.git/cmd/flags.go
@@ -6,7 +6,7 @@ import (
"path"
"strings"
@@ -23,10 +23,10 @@ index a5d811b..d584279 100644
"github.com/google/shlex"
"gotest.tools/gotestsum/internal/junitxml"
"gotest.tools/gotestsum/testjson"
-diff --git a/cmd/main.go b/cmd/main.go
-index 553cad5..2f45028 100644
---- a/cmd/main.go
-+++ b/cmd/main.go
+Index: gotestsum.git/cmd/main.go
+===================================================================
+--- gotestsum.git.orig/cmd/main.go
++++ gotestsum.git/cmd/main.go
@@ -12,7 +12,7 @@ import (
"sync/atomic"
"syscall"
@@ -36,10 +36,10 @@ index 553cad5..2f45028 100644
"github.com/fatih/color"
"gotest.tools/gotestsum/internal/log"
"gotest.tools/gotestsum/testjson"
-diff --git a/cmd/tool/slowest/slowest.go b/cmd/tool/slowest/slowest.go
-index a2c1327..6c53a4c 100644
---- a/cmd/tool/slowest/slowest.go
-+++ b/cmd/tool/slowest/slowest.go
+Index: gotestsum.git/cmd/tool/slowest/slowest.go
+===================================================================
+--- gotestsum.git.orig/cmd/tool/slowest/slowest.go
++++ gotestsum.git/cmd/tool/slowest/slowest.go
@@ -7,7 +7,7 @@ import (
"os"
"time"
diff --git a/internal/aggregate/slowest.go b/internal/aggregate/slowest.go
index 43165d0..94c33ee 100644
--- a/internal/aggregate/slowest.go
+++ b/internal/aggregate/slowest.go
@@ -13,8 +13,8 @@ import (
//
// If there are multiple runs of a TestCase, all of them will be represented
// by a single TestCase with the median elapsed time in the returned slice.
-func Slowest(exec *testjson.Execution, threshold time.Duration) []testjson.TestCase {
- if threshold == 0 {
+func Slowest(exec *testjson.Execution, threshold time.Duration, num int) []testjson.TestCase {
+ if threshold == 0 && num == 0 {
return nil
}
pkgs := exec.Packages()
@@ -26,6 +26,13 @@ func Slowest(exec *testjson.Execution, threshold time.Duration) []testjson.TestC
sort.Slice(tests, func(i, j int) bool {
return tests[i].Elapsed > tests[j].Elapsed
})
+ if num >= len(tests) {
+ return tests
+ }
+ if num > 0 {
+ return tests[:num]
+ }
+
end := sort.Search(len(tests), func(i int) bool {
return tests[i].Elapsed < threshold
})
diff --git a/internal/aggregate/slowest_test.go b/internal/aggregate/slowest_test.go
index 010007b..c5389a4 100644
--- a/internal/aggregate/slowest_test.go
+++ b/internal/aggregate/slowest_test.go
@@ -1,15 +1,115 @@
package aggregate
import (
+ "bytes"
+ "encoding/json"
"strings"
"testing"
"time"
+ "github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"gotest.tools/gotestsum/testjson"
"gotest.tools/v3/assert"
)
+func TestSlowest(t *testing.T) {
+ newEvent := func(pkg, test string, elapsed float64) testjson.TestEvent {
+ return testjson.TestEvent{
+ Package: pkg,
+ Test: test,
+ Action: testjson.ActionPass,
+ Elapsed: elapsed,
+ }
+ }
+
+ exec := newExecutionFromEvents(t,
+ newEvent("one", "TestOmega", 22.2),
+ newEvent("one", "TestOmega", 1.5),
+ newEvent("one", "TestOmega", 0.6),
+ newEvent("one", "TestOnion", 0.5),
+ newEvent("two", "TestTents", 2.5),
+ newEvent("two", "TestTin", 0.3),
+ newEvent("two", "TestTunnel", 1.1))
+
+ cmpCasesShallow := cmp.Comparer(func(x, y testjson.TestCase) bool {
+ return x.Package == y.Package && x.Test == y.Test
+ })
+
+ type testCase struct {
+ name string
+ threshold time.Duration
+ num int
+ expected []testjson.TestCase
+ }
+
+ run := func(t *testing.T, tc testCase) {
+ actual := Slowest(exec, tc.threshold, tc.num)
+ assert.DeepEqual(t, actual, tc.expected, cmpCasesShallow)
+ }
+
+ testCases := []testCase{
+ {
+ name: "threshold only",
+ threshold: time.Second,
+ expected: []testjson.TestCase{
+ {Package: "two", Test: "TestTents"},
+ {Package: "one", Test: "TestOmega"},
+ {Package: "two", Test: "TestTunnel"},
+ },
+ },
+ {
+ name: "threshold only 2s",
+ threshold: 2 * time.Second,
+ expected: []testjson.TestCase{
+ {Package: "two", Test: "TestTents"},
+ },
+ },
+ {
+ name: "threshold and num",
+ threshold: 400 * time.Millisecond,
+ num: 2,
+ expected: []testjson.TestCase{
+ {Package: "two", Test: "TestTents"},
+ {Package: "one", Test: "TestOmega"},
+ },
+ },
+ {
+ name: "num only",
+ num: 4,
+ expected: []testjson.TestCase{
+ {Package: "two", Test: "TestTents"},
+ {Package: "one", Test: "TestOmega"},
+ {Package: "two", Test: "TestTunnel"},
+ {Package: "one", Test: "TestOnion"},
+ },
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ run(t, tc)
+ })
+ }
+}
+
+func newExecutionFromEvents(t *testing.T, events ...testjson.TestEvent) *testjson.Execution {
+ t.Helper()
+
+ buf := new(bytes.Buffer)
+ encoder := json.NewEncoder(buf)
+ for i, event := range events {
+ assert.NilError(t, encoder.Encode(event), "event %d", i)
+ }
+
+ exec, err := testjson.ScanTestOutput(testjson.ScanConfig{
+ Stdout: buf,
+ Stderr: strings.NewReader(""),
+ })
+ assert.NilError(t, err)
+ return exec
+}
+
func TestByElapsed_WithMedian(t *testing.T) {
cases := []testjson.TestCase{
{Test: "TestOne", Package: "pkg", Elapsed: time.Second},
diff --git a/internal/filewatcher/term_zos.go b/internal/filewatcher/term_zos.go
new file mode 100644
index 0000000..736d67b
--- /dev/null
+++ b/internal/filewatcher/term_zos.go
@@ -0,0 +1,6 @@
+package filewatcher
+
+import "golang.org/x/sys/unix"
+
+const tcGet = unix.TCGETS
+const tcSet = unix.TCSETS
diff --git a/internal/filewatcher/watch.go b/internal/filewatcher/watch.go
index e639f27..837ec44 100644
--- a/internal/filewatcher/watch.go
+++ b/internal/filewatcher/watch.go
@@ -236,7 +236,7 @@ type fsEventHandler struct {
var floodThreshold = 250 * time.Millisecond
func (h *fsEventHandler) handleEvent(event fsnotify.Event) error {
- if event.Op&(fsnotify.Write|fsnotify.Create) == 0 {
+ if event.Op&(fsnotify.Write|fsnotify.Create|fsnotify.Rename) == 0 {
return nil
}
diff --git a/internal/filewatcher/watch_test.go b/internal/filewatcher/watch_test.go
index d0e5af0..fcf2376 100644
--- a/internal/filewatcher/watch_test.go
+++ b/internal/filewatcher/watch_test.go
@@ -38,8 +38,9 @@ func TestFSEventHandler_HandleEvent(t *testing.T) {
var testCases = []testCase{
{
- name: "Op is rename",
- event: fsnotify.Event{Op: fsnotify.Rename, Name: "file_test.go"},
+ name: "Op is rename",
+ event: fsnotify.Event{Op: fsnotify.Rename, Name: "file_test.go"},
+ expectedRun: true,
},
{
name: "Op is remove",
diff --git a/internal/junitxml/report.go b/internal/junitxml/report.go
index cd23b1f..9857f6b 100644
--- a/internal/junitxml/report.go
+++ b/internal/junitxml/report.go
@@ -3,6 +3,7 @@
package junitxml
import (
+ "bytes"
"encoding/xml"
"fmt"
"io"
@@ -72,6 +73,7 @@ type Config struct {
ProjectName string
FormatTestSuiteName FormatFunc
FormatTestCaseClassname FormatFunc
+ HideEmptyPackages bool
// This is used for tests to have a consistent timestamp
customTimestamp string
customElapsed string
@@ -104,6 +106,9 @@ func generate(exec *testjson.Execution, cfg Config) JUnitTestSuites {
}
for _, pkgname := range exec.Packages() {
pkg := exec.Package(pkgname)
+ if cfg.HideEmptyPackages && pkg.IsEmpty() {
+ continue
+ }
junitpkg := JUnitTestSuite{
Name: cfg.FormatTestSuiteName(pkgname),
Tests: pkg.Total,
@@ -168,10 +173,12 @@ func packageTestCases(pkg *testjson.Package, formatClassname FormatFunc) []JUnit
cases := []JUnitTestCase{}
if pkg.TestMainFailed() {
+ var buf bytes.Buffer
+ pkg.WriteOutputTo(&buf, 0) //nolint:errcheck
jtc := newJUnitTestCase(testjson.TestCase{Test: "TestMain"}, formatClassname)
jtc.Failure = &JUnitFailure{
Message: "Failed",
- Contents: pkg.Output(0),
+ Contents: buf.String(),
}
cases = append(cases, jtc)
}
diff --git a/internal/junitxml/report_test.go b/internal/junitxml/report_test.go
index 14e8afb..38d309c 100644
--- a/internal/junitxml/report_test.go
+++ b/internal/junitxml/report_test.go
@@ -29,6 +29,21 @@ func TestWrite(t *testing.T) {
golden.Assert(t, out.String(), "junitxml-report.golden")
}
+func TestWrite_HideEmptyPackages(t *testing.T) {
+ out := new(bytes.Buffer)
+ exec := createExecution(t)
+
+ env.Patch(t, "GOVERSION", "go7.7.7")
+ err := Write(out, exec, Config{
+ ProjectName: "test",
+ HideEmptyPackages: true,
+ customTimestamp: new(time.Time).Format(time.RFC3339),
+ customElapsed: "2.1",
+ })
+ assert.NilError(t, err)
+ golden.Assert(t, out.String(), "junitxml-report-skip-empty.golden")
+}
+
func createExecution(t *testing.T) *testjson.Execution {
exec, err := testjson.ScanTestOutput(testjson.ScanConfig{
Stdout: readTestData(t, "out"),
diff --git a/internal/junitxml/testdata/junitxml-report-skip-empty.golden b/internal/junitxml/testdata/junitxml-report-skip-empty.golden
new file mode 100644
index 0000000..432accd
--- /dev/null
+++ b/internal/junitxml/testdata/junitxml-report-skip-empty.golden
@@ -0,0 +1,119 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<testsuites name="test" tests="59" failures="13" errors="1" time="2.1">
+ <testsuite tests="0" failures="0" time="0.001000" name="gotest.tools/gotestsum/testjson/internal/badmain" timestamp="0001-01-01T00:00:00Z">
+ <properties>
+ <property name="go.version" value="go7.7.7"></property>
+ </properties>
+ <testcase classname="" name="TestMain" time="0.000000">
+ <failure message="Failed" type="">sometimes main can exit 2
FAIL	gotest.tools/gotestsum/testjson/internal/badmain	0.001s
</failure>
+ </testcase>
+ </testsuite>
+ <testsuite tests="18" failures="0" time="0.000000" name="gotest.tools/gotestsum/testjson/internal/good" timestamp="0001-01-01T00:00:00Z">
+ <properties>
+ <property name="go.version" value="go7.7.7"></property>
+ </properties>
+ <testcase classname="gotest.tools/gotestsum/testjson/internal/good" name="TestSkipped" time="0.000000">
+ <skipped message="=== RUN TestSkipped
 good_test.go:23: 
--- SKIP: TestSkipped (0.00s)
"></skipped>
+ </testcase>
+ <testcase classname="gotest.tools/gotestsum/testjson/internal/good" name="TestSkippedWitLog" time="0.000000">
+ <skipped message="=== RUN TestSkippedWitLog
 good_test.go:27: the skip message
--- SKIP: TestSkippedWitLog (0.00s)
"></skipped>
+ </testcase>
+ <testcase classname="gotest.tools/gotestsum/testjson/internal/good" name="TestPassed" time="0.000000"></testcase>
+ <testcase classname="gotest.tools/gotestsum/testjson/internal/good" name="TestPassedWithLog" time="0.000000"></testcase>
+ <testcase classname="gotest.tools/gotestsum/testjson/internal/good" name="TestPassedWithStdout" time="0.000000"></testcase>
+ <testcase classname="gotest.tools/gotestsum/testjson/internal/good" name="TestWithStderr" time="0.000000"></testcase>
+ <testcase classname="gotest.tools/gotestsum/testjson/internal/good" name="TestNestedSuccess/a/sub" time="0.000000"></testcase>
+ <testcase classname="gotest.tools/gotestsum/testjson/internal/good" name="TestNestedSuccess/a" time="0.000000"></testcase>
+ <testcase classname="gotest.tools/gotestsum/testjson/internal/good" name="TestNestedSuccess/b/sub" time="0.000000"></testcase>
+ <testcase classname="gotest.tools/gotestsum/testjson/internal/good" name="TestNestedSuccess/b" time="0.000000"></testcase>
+ <testcase classname="gotest.tools/gotestsum/testjson/internal/good" name="TestNestedSuccess/c/sub" time="0.000000"></testcase>
+ <testcase classname="gotest.tools/gotestsum/testjson/internal/good" name="TestNestedSuccess/c" time="0.000000"></testcase>
+ <testcase classname="gotest.tools/gotestsum/testjson/internal/good" name="TestNestedSuccess/d/sub" time="0.000000"></testcase>
+ <testcase classname="gotest.tools/gotestsum/testjson/internal/good" name="TestNestedSuccess/d" time="0.000000"></testcase>
+ <testcase classname="gotest.tools/gotestsum/testjson/internal/good" name="TestNestedSuccess" time="0.000000"></testcase>
+ <testcase classname="gotest.tools/gotestsum/testjson/internal/good" name="TestParallelTheFirst" time="0.010000"></testcase>
+ <testcase classname="gotest.tools/gotestsum/testjson/internal/good" name="TestParallelTheThird" time="0.000000"></testcase>
+ <testcase classname="gotest.tools/gotestsum/testjson/internal/good" name="TestParallelTheSecond" time="0.010000"></testcase>
+ </testsuite>
+ <testsuite tests="12" failures="8" time="0.020000" name="gotest.tools/gotestsum/testjson/internal/parallelfails" timestamp="0001-01-01T00:00:00Z">
+ <properties>
+ <property name="go.version" value="go7.7.7"></property>
+ </properties>
+ <testcase classname="gotest.tools/gotestsum/testjson/internal/parallelfails" name="TestNestedParallelFailures/a" time="0.000000">
+ <failure message="Failed" type="">=== RUN TestNestedParallelFailures/a
=== PAUSE TestNestedParallelFailures/a
=== CONT TestNestedParallelFailures/a
 fails_test.go:50: failed sub a
 --- FAIL: TestNestedParallelFailures/a (0.00s)
</failure>
+ </testcase>
+ <testcase classname="gotest.tools/gotestsum/testjson/internal/parallelfails" name="TestNestedParallelFailures/d" time="0.000000">
+ <failure message="Failed" type="">=== RUN TestNestedParallelFailures/d
=== PAUSE TestNestedParallelFailures/d
=== CONT TestNestedParallelFailures/d
 fails_test.go:50: failed sub d
 --- FAIL: TestNestedParallelFailures/d (0.00s)
</failure>
+ </testcase>
+ <testcase classname="gotest.tools/gotestsum/testjson/internal/parallelfails" name="TestNestedParallelFailures/c" time="0.000000">
+ <failure message="Failed" type="">=== RUN TestNestedParallelFailures/c
=== PAUSE TestNestedParallelFailures/c
=== CONT TestNestedParallelFailures/c
 fails_test.go:50: failed sub c
 --- FAIL: TestNestedParallelFailures/c (0.00s)
</failure>
+ </testcase>
+ <testcase classname="gotest.tools/gotestsum/testjson/internal/parallelfails" name="TestNestedParallelFailures/b" time="0.000000">
+ <failure message="Failed" type="">=== RUN TestNestedParallelFailures/b
=== PAUSE TestNestedParallelFailures/b
=== CONT TestNestedParallelFailures/b
 fails_test.go:50: failed sub b
 --- FAIL: TestNestedParallelFailures/b (0.00s)
</failure>
+ </testcase>
+ <testcase classname="gotest.tools/gotestsum/testjson/internal/parallelfails" name="TestNestedParallelFailures" time="0.000000">
+ <failure message="Failed" type="">=== RUN TestNestedParallelFailures
--- FAIL: TestNestedParallelFailures (0.00s)
</failure>
+ </testcase>
+ <testcase classname="gotest.tools/gotestsum/testjson/internal/parallelfails" name="TestParallelTheFirst" time="0.010000">
+ <failure message="Failed" type="">=== RUN TestParallelTheFirst
=== PAUSE TestParallelTheFirst
=== CONT TestParallelTheFirst
 fails_test.go:29: failed the first
--- FAIL: TestParallelTheFirst (0.01s)
</failure>
+ </testcase>
+ <testcase classname="gotest.tools/gotestsum/testjson/internal/parallelfails" name="TestParallelTheThird" time="0.000000">
+ <failure message="Failed" type="">=== RUN TestParallelTheThird
=== PAUSE TestParallelTheThird
=== CONT TestParallelTheThird
 fails_test.go:41: failed the third
--- FAIL: TestParallelTheThird (0.00s)
</failure>
+ </testcase>
+ <testcase classname="gotest.tools/gotestsum/testjson/internal/parallelfails" name="TestParallelTheSecond" time="0.010000">
+ <failure message="Failed" type="">=== RUN TestParallelTheSecond
=== PAUSE TestParallelTheSecond
=== CONT TestParallelTheSecond
 fails_test.go:35: failed the second
--- FAIL: TestParallelTheSecond (0.01s)
</failure>
+ </testcase>
+ <testcase classname="gotest.tools/gotestsum/testjson/internal/parallelfails" name="TestPassed" time="0.000000"></testcase>
+ <testcase classname="gotest.tools/gotestsum/testjson/internal/parallelfails" name="TestPassedWithLog" time="0.000000"></testcase>
+ <testcase classname="gotest.tools/gotestsum/testjson/internal/parallelfails" name="TestPassedWithStdout" time="0.000000"></testcase>
+ <testcase classname="gotest.tools/gotestsum/testjson/internal/parallelfails" name="TestWithStderr" time="0.000000"></testcase>
+ </testsuite>
+ <testsuite tests="29" failures="4" time="0.020000" name="gotest.tools/gotestsum/testjson/internal/withfails" timestamp="0001-01-01T00:00:00Z">
+ <properties>
+ <property name="go.version" value="go7.7.7"></property>
+ </properties>
+ <testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestFailed" time="0.000000">
+ <failure message="Failed" type="">=== RUN TestFailed
 fails_test.go:34: this failed
--- FAIL: TestFailed (0.00s)
</failure>
+ </testcase>
+ <testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestFailedWithStderr" time="0.000000">
+ <failure message="Failed" type="">=== RUN TestFailedWithStderr
this is stderr
 fails_test.go:43: also failed
--- FAIL: TestFailedWithStderr (0.00s)
</failure>
+ </testcase>
+ <testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestNestedWithFailure/c" time="0.000000">
+ <failure message="Failed" type="">=== RUN TestNestedWithFailure/c
 fails_test.go:65: failed
 --- FAIL: TestNestedWithFailure/c (0.00s)
</failure>
+ </testcase>
+ <testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestNestedWithFailure" time="0.000000">
+ <failure message="Failed" type="">=== RUN TestNestedWithFailure
--- FAIL: TestNestedWithFailure (0.00s)
</failure>
+ </testcase>
+ <testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestSkipped" time="0.000000">
+ <skipped message="=== RUN TestSkipped
 fails_test.go:26: 
--- SKIP: TestSkipped (0.00s)
"></skipped>
+ </testcase>
+ <testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestSkippedWitLog" time="0.000000">
+ <skipped message="=== RUN TestSkippedWitLog
 fails_test.go:30: the skip message
--- SKIP: TestSkippedWitLog (0.00s)
"></skipped>
+ </testcase>
+ <testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestTimeout" time="0.000000">
+ <skipped message="=== RUN TestTimeout
 timeout_test.go:13: skipping slow test
--- SKIP: TestTimeout (0.00s)
"></skipped>
+ </testcase>
+ <testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestPassed" time="0.000000"></testcase>
+ <testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestPassedWithLog" time="0.000000"></testcase>
+ <testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestPassedWithStdout" time="0.000000"></testcase>
+ <testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestWithStderr" time="0.000000"></testcase>
+ <testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestNestedWithFailure/a/sub" time="0.000000"></testcase>
+ <testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestNestedWithFailure/a" time="0.000000"></testcase>
+ <testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestNestedWithFailure/b/sub" time="0.000000"></testcase>
+ <testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestNestedWithFailure/b" time="0.000000"></testcase>
+ <testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestNestedWithFailure/d/sub" time="0.000000"></testcase>
+ <testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestNestedWithFailure/d" time="0.000000"></testcase>
+ <testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestNestedSuccess/a/sub" time="0.000000"></testcase>
+ <testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestNestedSuccess/a" time="0.000000"></testcase>
+ <testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestNestedSuccess/b/sub" time="0.000000"></testcase>
+ <testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestNestedSuccess/b" time="0.000000"></testcase>
+ <testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestNestedSuccess/c/sub" time="0.000000"></testcase>
+ <testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestNestedSuccess/c" time="0.000000"></testcase>
+ <testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestNestedSuccess/d/sub" time="0.000000"></testcase>
+ <testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestNestedSuccess/d" time="0.000000"></testcase>
+ <testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestNestedSuccess" time="0.000000"></testcase>
+ <testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestParallelTheFirst" time="0.010000"></testcase>
+ <testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestParallelTheThird" time="0.000000"></testcase>
+ <testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestParallelTheSecond" time="0.010000"></testcase>
+ </testsuite>
+</testsuites>
\ No newline at end of file
diff --git a/internal/log/log.go b/internal/log/log.go
index 9a3e85f..d68955b 100644
--- a/internal/log/log.go
+++ b/internal/log/log.go
@@ -11,6 +11,7 @@ type Level uint8
const (
ErrorLevel Level = iota
WarnLevel
+ InfoLevel
DebugLevel
)
@@ -43,6 +44,15 @@ func Debugf(format string, args ...interface{}) {
fmt.Fprint(out, "\n")
}
+// Infof prints the message to stderr, with no prefix.
+func Infof(format string, args ...interface{}) {
+ if level < InfoLevel {
+ return
+ }
+ fmt.Fprintf(out, format, args...)
+ fmt.Fprint(out, "\n")
+}
+
// Errorf prints the message to stderr, with a red ERROR prefix.
func Errorf(format string, args ...interface{}) {
if level < ErrorLevel {
diff --git a/internal/text/testing.go b/internal/text/testing.go
index 521385d..ba8366a 100644
--- a/internal/text/testing.go
+++ b/internal/text/testing.go
@@ -4,15 +4,19 @@ import (
"bufio"
"io"
"strings"
- "testing"
"gotest.tools/v3/assert"
)
+type TestingT interface {
+ Helper()
+ assert.TestingT
+}
+
// ProcessLines from the Reader by passing each one to ops. The output of each
// op is passed to the next. Returns the string created by joining all the
// processed lines.
-func ProcessLines(t *testing.T, r io.Reader, ops ...func(string) string) string {
+func ProcessLines(t TestingT, r io.Reader, ops ...func(string) string) string {
t.Helper()
out := new(strings.Builder)
scan := bufio.NewScanner(r)
diff --git a/main.go b/main.go
index df3e790..f8af75e 100644
--- a/main.go
+++ b/main.go
@@ -1,10 +1,12 @@
package main
import (
+ "fmt"
"os"
"gotest.tools/gotestsum/cmd"
- "gotest.tools/gotestsum/cmd/tool"
+ "gotest.tools/gotestsum/cmd/tool/matrix"
+ "gotest.tools/gotestsum/cmd/tool/slowest"
"gotest.tools/gotestsum/internal/log"
)
@@ -25,13 +27,52 @@ func main() {
func route(args []string) error {
name := args[0]
- next, rest := cmd.Next(args[1:])
+ next, rest := nextArg(args[1:])
switch next {
case "help", "?":
return cmd.Run(name, []string{"--help"})
case "tool":
- return tool.Run(name+" "+next, rest)
+ return toolRun(name+" "+next, rest)
default:
return cmd.Run(name, args[1:])
}
}
+
+// nextArg splits args into the next positional argument and any remaining args.
+func nextArg(args []string) (string, []string) {
+ switch len(args) {
+ case 0:
+ return "", nil
+ case 1:
+ return args[0], nil
+ default:
+ return args[0], args[1:]
+ }
+}
+
+func toolRun(name string, args []string) error {
+ usage := func(name string) string {
+ return fmt.Sprintf(`Usage: %[1]s COMMAND [flags]
+
+Commands:
+ %[1]s slowest find or skip the slowest tests
+ %[1]s ci-matrix use previous test runtime to place packages into optimal buckets
+
+Use '%[1]s COMMAND --help' for command specific help.
+`, name)
+ }
+
+ next, rest := nextArg(args)
+ switch next {
+ case "", "help", "?":
+ fmt.Println(usage(name))
+ return nil
+ case "slowest":
+ return slowest.Run(name+" "+next, rest)
+ case "ci-matrix":
+ return matrix.Run(name+" "+next, rest)
+ default:
+ fmt.Fprintln(os.Stderr, usage(name))
+ return fmt.Errorf("invalid command: %v %v", name, next)
+ }
+}
diff --git a/testjson/dotformat.go b/testjson/dotformat.go
index fa00d36..625cd87 100644
--- a/testjson/dotformat.go
+++ b/testjson/dotformat.go
@@ -1,6 +1,7 @@
package testjson
import (
+ "bufio"
"fmt"
"io"
"os"
@@ -13,15 +14,21 @@ import (
"gotest.tools/gotestsum/internal/log"
)
-func dotsFormatV1(event TestEvent, exec *Execution) string {
- pkg := exec.Package(event.Package)
- switch {
- case event.PackageEvent():
- return ""
- case event.Action == ActionRun && pkg.Total == 1:
- return "[" + RelativePackagePath(event.Package) + "]"
- }
- return fmtDot(event)
+func dotsFormatV1(out io.Writer) EventFormatter {
+ buf := bufio.NewWriter(out)
+ // nolint:errcheck
+ return eventFormatterFunc(func(event TestEvent, exec *Execution) error {
+ pkg := exec.Package(event.Package)
+ switch {
+ case event.PackageEvent():
+ return nil
+ case event.Action == ActionRun && pkg.Total == 1:
+ buf.WriteString("[" + RelativePackagePath(event.Package) + "]")
+ return buf.Flush()
+ }
+ buf.WriteString(fmtDot(event))
+ return buf.Flush()
+ })
}
func fmtDot(event TestEvent) string {
@@ -41,6 +48,7 @@ type dotFormatter struct {
pkgs map[string]*dotLine
order []string
writer *dotwriter.Writer
+ opts FormatOptions
termWidth int
}
@@ -67,16 +75,17 @@ func (l *dotLine) checkWidth(prefix, terminal int) {
}
}
-func newDotFormatter(out io.Writer) EventFormatter {
+func newDotFormatter(out io.Writer, opts FormatOptions) EventFormatter {
w, _, err := term.GetSize(int(os.Stdout.Fd()))
if err != nil || w == 0 {
log.Warnf("Failed to detect terminal width for dots format, error: %v", err)
- return &formatAdapter{format: dotsFormatV1, out: out}
+ return dotsFormatV1(out)
}
return &dotFormatter{
pkgs: make(map[string]*dotLine),
writer: dotwriter.New(out),
termWidth: w,
+ opts: opts,
}
}
@@ -101,6 +110,10 @@ func (d *dotFormatter) Format(event TestEvent, exec *Execution) error {
sort.Slice(d.order, d.orderByLastUpdated)
for _, pkg := range d.order {
+ if d.opts.HideEmptyPackages && exec.Package(pkg).IsEmpty() {
+ continue
+ }
+
line := d.pkgs[pkg]
pkgname := RelativePackagePath(pkg) + " "
prefix := fmtDotElapsed(exec.Package(pkg))
diff --git a/testjson/dotformat_test.go b/testjson/dotformat_test.go
index 8d72390..e3f0c2c 100644
--- a/testjson/dotformat_test.go
+++ b/testjson/dotformat_test.go
@@ -129,7 +129,7 @@ func TestFmtDotElapsed_RuneCountProperty(t *testing.T) {
func TestNewDotFormatter(t *testing.T) {
buf := new(bytes.Buffer)
- ef := newDotFormatter(buf)
+ ef := newDotFormatter(buf, FormatOptions{})
d, ok := ef.(*dotFormatter)
skip.If(t, !ok, "no terminal width")
diff --git a/testjson/execution.go b/testjson/execution.go
index dd6e507..51cac7d 100644
--- a/testjson/execution.go
+++ b/testjson/execution.go
@@ -109,6 +109,11 @@ type Package struct {
// shuffleSeed is the seed used to shuffle the tests. The value is set when
// tests are run with -shuffle
shuffleSeed string
+
+ // testTimeoutPanicInTest stores the name of a test that received the panic
+ // output caused by a test timeout. This is necessary to work around a race
+ // condition in test2json. See https://github.com/golang/go/issues/57305.
+ testTimeoutPanicInTest string
}
// Result returns if the package passed, failed, or was skipped because there
@@ -146,13 +151,24 @@ func (p *Package) LastFailedByName(name string) TestCase {
return TestCase{}
}
-// Output returns the full test output for a test.
+// Output returns the full test output for a test. Unlike OutputLines() it does
+// not return lines from subtests in some cases.
//
-// Unlike OutputLines() it does not return lines from subtests in some cases.
+// Deprecated: use WriteOutputTo to avoid lots of allocation
func (p *Package) Output(id int) string {
return strings.Join(p.output[id], "")
}
+// WriteOutputTo writes the output for TestCase with id to out.
+func (p *Package) WriteOutputTo(out io.StringWriter, id int) error {
+ for _, v := range p.output[id] {
+ if _, err := out.WriteString(v); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
// OutputLines returns the full test output for a test as a slice of strings.
//
// As a workaround for test output being attributed to the wrong subtest, if:
@@ -232,6 +248,11 @@ func (p *Package) TestMainFailed() bool {
return p.action == ActionFail && len(p.Failed) == 0
}
+// IsEmpty returns true if this package contains no tests.
+func (p *Package) IsEmpty() bool {
+ return p.Total == 0 && !p.TestMainFailed()
+}
+
const neverFinished time.Duration = -1
// end adds any tests that were missing an ActionFail TestEvent to the list of
@@ -344,8 +365,8 @@ func (p *Package) addEvent(event TestEvent) {
p.action = event.Action
p.elapsed = elapsedDuration(event.Elapsed)
case ActionOutput:
- if isCoverageOutput(event.Output) {
- p.coverage = strings.TrimRight(event.Output, "\n")
+ if coverage, ok := isCoverageOutput(event.Output); ok {
+ p.coverage = coverage
}
if strings.Contains(event.Output, "\t(cached)") {
p.cached = true
@@ -393,6 +414,14 @@ func (p *Package) addTestEvent(event TestEvent) {
switch event.Action {
case ActionOutput, ActionBench:
+ if strings.HasPrefix(event.Output, "panic: test timed out") {
+ p.testTimeoutPanicInTest = event.Test
+ }
+ if p.testTimeoutPanicInTest == event.Test {
+ p.addOutput(0, event.Output)
+ return
+ }
+
tc := p.running[event.Test]
p.addOutput(tc.ID, event.Output)
return
@@ -437,10 +466,22 @@ func elapsedDuration(elapsed float64) time.Duration {
return time.Duration(elapsed*1000) * time.Millisecond
}
-func isCoverageOutput(output string) bool {
- return all(
- strings.HasPrefix(output, "coverage:"),
- strings.Contains(output, "% of statements"))
+func isCoverageOutput(output string) (string, bool) {
+ start := strings.Index(output, "coverage:")
+ if start < 0 {
+ return "", false
+ }
+
+ if !strings.Contains(output[start:], "% of statements") {
+ return "", false
+ }
+
+ return strings.TrimRight(output[start:], "\n"), true
+}
+
+func isCoverageOutputPreGo119(output string) bool {
+ return strings.HasPrefix(output, "coverage:") &&
+ strings.HasSuffix(output, "% of statements\n")
}
func isShuffleSeedOutput(output string) bool {
@@ -486,8 +527,10 @@ func (e *Execution) Failed() []TestCase {
for _, name := range sortedKeys(e.packages) {
pkg := e.packages[name]
- // Add package-level failure output if there were no failed tests.
- if pkg.TestMainFailed() {
+ // Add package-level failure output if there were no failed tests, or
+ // if the test timeout was reached (because we now have to store that
+ // output on the package).
+ if pkg.TestMainFailed() || pkg.testTimeoutPanicInTest != "" {
failed = append(failed, TestCase{Package: name})
}
failed = append(failed, pkg.Failed...)
@@ -703,7 +746,7 @@ func readStderr(config ScanConfig, execution *Execution) error {
if err := config.Handler.Err(line); err != nil {
return fmt.Errorf("failed to handle stderr: %v", err)
}
- if isGoModuleOutput(line) {
+ if isGoModuleOutput(line) || isGoDebugOutput(line) {
continue
}
if strings.HasPrefix(line, "warning:") {
@@ -711,6 +754,7 @@ func readStderr(config ScanConfig, execution *Execution) error {
}
execution.addError(line)
}
+
if err := scanner.Err(); err != nil {
return fmt.Errorf("failed to scan stderr: %v", err)
}
@@ -734,6 +778,20 @@ func isGoModuleOutput(scannerText string) bool {
return false
}
+func isGoDebugOutput(scannerText string) bool {
+ prefixes := []string{
+ "HASH", // Printed when tests are running with `GODEBUG=gocachehash=1`.
+ "testcache:", // Printed when tests are running with `GODEBUG=gocachetest=1`.
+ }
+
+ for _, prefix := range prefixes {
+ if strings.HasPrefix(scannerText, prefix) {
+ return true
+ }
+ }
+ return false
+}
+
func parseEvent(raw []byte) (TestEvent, error) {
// TODO: this seems to be a bug in the `go test -json` output
if bytes.HasPrefix(raw, []byte("FAIL")) {
diff --git a/testjson/execution_test.go b/testjson/execution_test.go
index 8cc39fa..8c7566e 100644
--- a/testjson/execution_test.go
+++ b/testjson/execution_test.go
@@ -3,6 +3,7 @@ package testjson
import (
"bytes"
"fmt"
+ "strings"
"testing"
"time"
@@ -217,6 +218,24 @@ func TestScanOutput_WithNonJSONLines(t *testing.T) {
}
}
+func TestScanOutput_WithGODEBUG(t *testing.T) {
+ goDebugSource := `HASH[moduleIndex]
+HASH[moduleIndex]: "go1.20.4"
+HASH /usr/lib/go/src/runtime/debuglog_off.go: d6f147198
+testcache: package: input list not found: ...`
+
+ handler := &captureHandler{}
+ cfg := ScanConfig{
+ Stdout: bytes.NewReader(nil),
+ Stderr: strings.NewReader(goDebugSource),
+ Handler: handler,
+ }
+ exec, err := ScanTestOutput(cfg)
+ assert.NilError(t, err)
+ assert.DeepEqual(t, handler.errs, strings.Split(goDebugSource, "\n"))
+ assert.DeepEqual(t, exec.Errors(), []string(nil))
+}
+
type captureHandler struct {
events []TestEvent
errs []string
@@ -229,5 +248,5 @@ func (s *captureHandler) Event(event TestEvent, _ *Execution) error {
func (s *captureHandler) Err(text string) error {
s.errs = append(s.errs, text)
- return fmt.Errorf(text)
+ return nil
}
diff --git a/testjson/format.go b/testjson/format.go
index 8d4faf7..180086e 100644
--- a/testjson/format.go
+++ b/testjson/format.go
@@ -1,6 +1,7 @@
package testjson
import (
+ "bufio"
"fmt"
"io"
"strings"
@@ -8,76 +9,118 @@ import (
"github.com/fatih/color"
)
-func debugFormat(event TestEvent, _ *Execution) string {
- return fmt.Sprintf("%s %s %s (%.3f) [%d] %s\n",
- event.Package,
- event.Test,
- event.Action,
- event.Elapsed,
- event.Time.Unix(),
- event.Output)
+func debugFormat(out io.Writer) eventFormatterFunc {
+ return func(event TestEvent, _ *Execution) error {
+ _, err := fmt.Fprintf(out, "%s %s %s (%.3f) [%d] %s\n",
+ event.Package,
+ event.Test,
+ event.Action,
+ event.Elapsed,
+ event.Time.Unix(),
+ event.Output)
+ return err
+ }
}
// go test -v
-func standardVerboseFormat(event TestEvent, _ *Execution) string {
- if event.Action == ActionOutput {
- return event.Output
- }
- return ""
+func standardVerboseFormat(out io.Writer) EventFormatter {
+ buf := bufio.NewWriter(out)
+ return eventFormatterFunc(func(event TestEvent, _ *Execution) error {
+ if event.Action == ActionOutput {
+ _, _ = buf.WriteString(event.Output)
+ return buf.Flush()
+ }
+ return nil
+ })
}
// go test
-func standardQuietFormat(event TestEvent, _ *Execution) string {
- if !event.PackageEvent() {
- return ""
- }
- if event.Output == "PASS\n" || isCoverageOutput(event.Output) {
- return ""
- }
- if isWarningNoTestsToRunOutput(event.Output) {
- return ""
- }
+func standardQuietFormat(out io.Writer) EventFormatter {
+ buf := bufio.NewWriter(out)
+ return eventFormatterFunc(func(event TestEvent, _ *Execution) error {
+ if !event.PackageEvent() {
+ return nil
+ }
+ if event.Output == "PASS\n" {
+ return nil
+ }
- return event.Output
-}
+ // Coverage line go1.20+
+ if strings.Contains(event.Output, event.Package+"\tcoverage:") {
+ return nil
+ }
+ if isCoverageOutputPreGo119(event.Output) {
+ return nil
+ }
-func testNameFormat(event TestEvent, exec *Execution) string {
- result := colorEvent(event)(strings.ToUpper(string(event.Action)))
- formatTest := func() string {
- pkgPath := RelativePackagePath(event.Package)
+ if isWarningNoTestsToRunOutput(event.Output) {
+ return nil
+ }
- return fmt.Sprintf("%s %s%s %s\n",
- result,
- joinPkgToTestName(pkgPath, event.Test),
- formatRunID(event.RunID),
- event.ElapsedFormatted())
- }
+ _, _ = buf.WriteString(event.Output)
+ return buf.Flush()
+ })
+}
- switch {
- case isPkgFailureOutput(event):
- return event.Output
+// go test -json
+func standardJSONFormat(out io.Writer) EventFormatter {
+ buf := bufio.NewWriter(out)
+ // nolint:errcheck // errors are returned by Flush
+ return eventFormatterFunc(func(event TestEvent, _ *Execution) error {
+ buf.Write(event.raw)
+ buf.WriteRune('\n')
+ return buf.Flush()
+ })
+}
- case event.PackageEvent():
- if !event.Action.IsTerminal() {
- return ""
- }
- pkg := exec.Package(event.Package)
- if event.Action == ActionSkip || (event.Action == ActionPass && pkg.Total == 0) {
- result = colorEvent(event)("EMPTY")
+func testNameFormat(out io.Writer) EventFormatter {
+ buf := bufio.NewWriter(out)
+ // nolint:errcheck
+ return eventFormatterFunc(func(event TestEvent, exec *Execution) error {
+ formatTest := func() error {
+ pkgPath := RelativePackagePath(event.Package)
+
+ fmt.Fprintf(buf, "%s %s%s %s\n",
+ colorEvent(event)(strings.ToUpper(string(event.Action))),
+ joinPkgToTestName(pkgPath, event.Test),
+ formatRunID(event.RunID),
+ event.ElapsedFormatted())
+ return buf.Flush()
}
- event.Elapsed = 0 // hide elapsed for now, for backwards compat
- return result + " " + packageLine(event, exec)
+ switch {
+ case isPkgFailureOutput(event):
+ buf.WriteString(event.Output)
+ return buf.Flush()
- case event.Action == ActionFail:
- pkg := exec.Package(event.Package)
- tc := pkg.LastFailedByName(event.Test)
- return pkg.Output(tc.ID) + formatTest()
+ case event.PackageEvent():
+ if !event.Action.IsTerminal() {
+ return nil
+ }
- case event.Action == ActionPass:
- return formatTest()
- }
- return ""
+ result := colorEvent(event)(strings.ToUpper(string(event.Action)))
+ pkg := exec.Package(event.Package)
+ if event.Action == ActionSkip || (event.Action == ActionPass && pkg.Total == 0) {
+ result = colorEvent(event)("EMPTY")
+ }
+
+ event.Elapsed = 0 // hide elapsed for now, for backwards compat
+ buf.WriteString(result)
+ buf.WriteRune(' ')
+ buf.WriteString(packageLine(event, exec.Package(event.Package)))
+ return buf.Flush()
+
+ case event.Action == ActionFail:
+ pkg := exec.Package(event.Package)
+ tc := pkg.LastFailedByName(event.Test)
+ pkg.WriteOutputTo(buf, tc.ID)
+ return formatTest()
+
+ case event.Action == ActionPass:
+ return formatTest()
+ }
+ return nil
+ })
}
// joinPkgToTestName for formatting.
@@ -128,37 +171,56 @@ func all(cond ...bool) bool {
return true
}
-func pkgNameFormat(event TestEvent, exec *Execution) string {
- if !event.PackageEvent() {
- return ""
+func pkgNameFormat(out io.Writer, opts FormatOptions) eventFormatterFunc {
+ buf := bufio.NewWriter(out)
+ return func(event TestEvent, exec *Execution) error {
+ if !event.PackageEvent() {
+ return nil
+ }
+ _, _ = buf.WriteString(shortFormatPackageEvent(opts, event, exec))
+ return buf.Flush()
}
- return shortFormatPackageEvent(event, exec)
}
-func shortFormatPackageEvent(event TestEvent, exec *Execution) string {
+func shortFormatPackageEvent(opts FormatOptions, event TestEvent, exec *Execution) string {
pkg := exec.Package(event.Package)
+ var iconSkipped, iconSuccess, iconFailure string
+ if opts.UseHiVisibilityIcons {
+ iconSkipped = "➖"
+ iconSuccess = "✅"
+ iconFailure = "❌"
+ } else {
+ iconSkipped = "∅"
+ iconSuccess = "✓"
+ iconFailure = "✖"
+ }
+
fmtEvent := func(action string) string {
- return action + " " + packageLine(event, exec)
+ return action + " " + packageLine(event, exec.Package(event.Package))
}
withColor := colorEvent(event)
switch event.Action {
case ActionSkip:
- return fmtEvent(withColor("∅"))
+ if opts.HideEmptyPackages {
+ return ""
+ }
+ return fmtEvent(withColor(iconSkipped))
case ActionPass:
if pkg.Total == 0 {
- return fmtEvent(withColor("∅"))
+ if opts.HideEmptyPackages {
+ return ""
+ }
+ return fmtEvent(withColor(iconSkipped))
}
- return fmtEvent(withColor("✓"))
+ return fmtEvent(withColor(iconSuccess))
case ActionFail:
- return fmtEvent(withColor("✖"))
+ return fmtEvent(withColor(iconFailure))
}
return ""
}
-func packageLine(event TestEvent, exec *Execution) string {
- pkg := exec.Package(event.Package)
-
+func packageLine(event TestEvent, pkg *Package) string {
var buf strings.Builder
buf.WriteString(RelativePackagePath(event.Package))
@@ -181,16 +243,21 @@ func packageLine(event TestEvent, exec *Execution) string {
return buf.String()
}
-func pkgNameWithFailuresFormat(event TestEvent, exec *Execution) string {
- if !event.PackageEvent() {
- if event.Action == ActionFail {
- pkg := exec.Package(event.Package)
- tc := pkg.LastFailedByName(event.Test)
- return pkg.Output(tc.ID)
+func pkgNameWithFailuresFormat(out io.Writer, opts FormatOptions) eventFormatterFunc {
+ buf := bufio.NewWriter(out)
+ return func(event TestEvent, exec *Execution) error {
+ if !event.PackageEvent() {
+ if event.Action == ActionFail {
+ pkg := exec.Package(event.Package)
+ tc := pkg.LastFailedByName(event.Test)
+ pkg.WriteOutputTo(buf, tc.ID) // nolint:errcheck
+ return buf.Flush()
+ }
+ return nil
}
- return ""
+ buf.WriteString(shortFormatPackageEvent(opts, event, exec)) // nolint:errcheck
+ return buf.Flush()
}
- return shortFormatPackageEvent(event, exec)
}
func colorEvent(event TestEvent) func(format string, a ...interface{}) string {
@@ -211,37 +278,41 @@ type EventFormatter interface {
Format(event TestEvent, output *Execution) error
}
+type eventFormatterFunc func(event TestEvent, output *Execution) error
+
+func (e eventFormatterFunc) Format(event TestEvent, output *Execution) error {
+ return e(event, output)
+}
+
+type FormatOptions struct {
+ HideEmptyPackages bool
+ UseHiVisibilityIcons bool
+}
+
// NewEventFormatter returns a formatter for printing events.
-func NewEventFormatter(out io.Writer, format string) EventFormatter {
+func NewEventFormatter(out io.Writer, format string, formatOpts FormatOptions) EventFormatter {
switch format {
+ case "none":
+ return eventFormatterFunc(func(TestEvent, *Execution) error { return nil })
case "debug":
- return &formatAdapter{out, debugFormat}
+ return debugFormat(out)
+ case "standard-json":
+ return standardJSONFormat(out)
case "standard-verbose":
- return &formatAdapter{out, standardVerboseFormat}
+ return standardVerboseFormat(out)
case "standard-quiet":
- return &formatAdapter{out, standardQuietFormat}
+ return standardQuietFormat(out)
case "dots", "dots-v1":
- return &formatAdapter{out, dotsFormatV1}
+ return dotsFormatV1(out)
case "dots-v2":
- return newDotFormatter(out)
+ return newDotFormatter(out, formatOpts)
case "testname", "short-verbose":
- return &formatAdapter{out, testNameFormat}
+ return testNameFormat(out)
case "pkgname", "short":
- return &formatAdapter{out, pkgNameFormat}
+ return pkgNameFormat(out, formatOpts)
case "pkgname-and-test-fails", "short-with-failures":
- return &formatAdapter{out, pkgNameWithFailuresFormat}
+ return pkgNameWithFailuresFormat(out, formatOpts)
default:
return nil
}
}
-
-type formatAdapter struct {
- out io.Writer
- format func(TestEvent, *Execution) string
-}
-
-func (f *formatAdapter) Format(event TestEvent, exec *Execution) error {
- o := f.format(event, exec)
- _, err := f.out.Write([]byte(o))
- return err
-}
diff --git a/testjson/format_test.go b/testjson/format_test.go
index aefd375..552ae22 100644
--- a/testjson/format_test.go
+++ b/testjson/format_test.go
@@ -2,6 +2,7 @@ package testjson
import (
"bytes"
+ "io"
"testing"
"gotest.tools/v3/assert"
@@ -26,7 +27,6 @@ import (
type fakeHandler struct {
inputName string
formatter EventFormatter
- out *bytes.Buffer
err *bytes.Buffer
}
@@ -38,24 +38,10 @@ func (s *fakeHandler) Config(t *testing.T) ScanConfig {
}
}
-func newFakeHandlerWithAdapter(
- format func(event TestEvent, output *Execution) string,
- inputName string,
-) *fakeHandler {
- out := new(bytes.Buffer)
- return &fakeHandler{
- inputName: inputName,
- formatter: &formatAdapter{out: out, format: format},
- out: out,
- err: new(bytes.Buffer),
- }
-}
-
func newFakeHandler(formatter EventFormatter, inputName string) *fakeHandler {
return &fakeHandler{
inputName: inputName,
formatter: formatter,
- out: new(bytes.Buffer),
err: new(bytes.Buffer),
}
}
@@ -80,17 +66,18 @@ func patchPkgPathPrefix(t *testing.T, val string) {
func TestFormats_DefaultGoTestJson(t *testing.T) {
type testCase struct {
name string
- format func(event TestEvent, exec *Execution) string
+ format func(io.Writer) EventFormatter
expectedOut string
expected func(t *testing.T, exec *Execution)
}
run := func(t *testing.T, tc testCase) {
- shim := newFakeHandlerWithAdapter(tc.format, "input/go-test-json")
+ out := new(bytes.Buffer)
+ shim := newFakeHandler(tc.format(out), "input/go-test-json")
exec, err := ScanTestOutput(shim.Config(t))
assert.NilError(t, err)
- golden.Assert(t, shim.out.String(), tc.expectedOut)
+ golden.Assert(t, out.String(), tc.expectedOut)
golden.Assert(t, shim.err.String(), "input/go-test-json.err")
if tc.expected != nil {
@@ -110,10 +97,26 @@ func TestFormats_DefaultGoTestJson(t *testing.T) {
expectedOut: "format/dots-v1.out",
},
{
- name: "pkgname",
- format: pkgNameFormat,
+ name: "pkgname",
+ format: func(out io.Writer) EventFormatter {
+ return pkgNameFormat(out, FormatOptions{})
+ },
expectedOut: "format/pkgname.out",
},
+ {
+ name: "pkgname with hivis",
+ format: func(out io.Writer) EventFormatter {
+ return pkgNameFormat(out, FormatOptions{UseHiVisibilityIcons: true})
+ },
+ expectedOut: "format/pkgname-hivis.out",
+ },
+ {
+ name: "pkgname with hide-empty",
+ format: func(out io.Writer) EventFormatter {
+ return pkgNameFormat(out, FormatOptions{HideEmptyPackages: true})
+ },
+ expectedOut: "format/pkgname-hide-empty.out",
+ },
{
name: "standard-verbose",
format: standardVerboseFormat,
@@ -124,6 +127,11 @@ func TestFormats_DefaultGoTestJson(t *testing.T) {
format: standardQuietFormat,
expectedOut: "format/standard-quiet.out",
},
+ {
+ name: "standard-json",
+ format: standardJSONFormat,
+ expectedOut: "input/go-test-json.out",
+ },
}
for _, tc := range testCases {
@@ -136,18 +144,25 @@ func TestFormats_DefaultGoTestJson(t *testing.T) {
func TestFormats_Coverage(t *testing.T) {
type testCase struct {
name string
- format func(event TestEvent, exec *Execution) string
+ format func(writer io.Writer) EventFormatter
+ input string
expectedOut string
expected func(t *testing.T, exec *Execution)
}
run := func(t *testing.T, tc testCase) {
patchPkgPathPrefix(t, "gotest.tools")
- shim := newFakeHandlerWithAdapter(tc.format, "input/go-test-json-with-cover")
+ out := new(bytes.Buffer)
+
+ if tc.input == "" {
+ tc.input = "input/go-test-json-with-cover"
+ }
+
+ shim := newFakeHandler(tc.format(out), tc.input)
exec, err := ScanTestOutput(shim.Config(t))
assert.NilError(t, err)
- golden.Assert(t, shim.out.String(), tc.expectedOut)
+ golden.Assert(t, out.String(), tc.expectedOut)
golden.Assert(t, shim.err.String(), "go-test.err")
if tc.expected != nil {
@@ -162,8 +177,10 @@ func TestFormats_Coverage(t *testing.T) {
expectedOut: "format/testname-coverage.out",
},
{
- name: "pkgname",
- format: pkgNameFormat,
+ name: "pkgname go1.19-",
+ format: func(out io.Writer) EventFormatter {
+ return pkgNameFormat(out, FormatOptions{})
+ },
expectedOut: "format/pkgname-coverage.out",
},
{
@@ -172,10 +189,24 @@ func TestFormats_Coverage(t *testing.T) {
expectedOut: "format/standard-verbose-coverage.out",
},
{
- name: "standard-quiet",
+ name: "standard-quiet go1.19-",
format: standardQuietFormat,
expectedOut: "format/standard-quiet-coverage.out",
},
+ {
+ name: "pkgname go1.20+",
+ format: func(out io.Writer) EventFormatter {
+ return pkgNameFormat(out, FormatOptions{})
+ },
+ input: "input/go-test-json-with-cover-go1.20",
+ expectedOut: "format/pkgname-coverage-go1.20.out",
+ },
+ {
+ name: "standard-quiet go.20+",
+ format: standardQuietFormat,
+ input: "input/go-test-json-with-cover-go1.20",
+ expectedOut: "format/standard-quiet-coverage-go1.20.out",
+ },
}
for _, tc := range testCases {
@@ -188,17 +219,18 @@ func TestFormats_Coverage(t *testing.T) {
func TestFormats_Shuffle(t *testing.T) {
type testCase struct {
name string
- format func(event TestEvent, exec *Execution) string
+ format func(io.Writer) EventFormatter
expectedOut string
expected func(t *testing.T, exec *Execution)
}
run := func(t *testing.T, tc testCase) {
- shim := newFakeHandlerWithAdapter(tc.format, "input/go-test-json-with-shuffle")
+ out := new(bytes.Buffer)
+ shim := newFakeHandler(tc.format(out), "input/go-test-json-with-shuffle")
exec, err := ScanTestOutput(shim.Config(t))
assert.NilError(t, err)
- golden.Assert(t, shim.out.String(), tc.expectedOut)
+ golden.Assert(t, out.String(), tc.expectedOut)
golden.Assert(t, shim.err.String(), "go-test.err")
if tc.expected != nil {
@@ -213,8 +245,10 @@ func TestFormats_Shuffle(t *testing.T) {
expectedOut: "format/testname-shuffle.out",
},
{
- name: "pkgname",
- format: pkgNameFormat,
+ name: "pkgname",
+ format: func(out io.Writer) EventFormatter {
+ return pkgNameFormat(out, FormatOptions{})
+ },
expectedOut: "format/pkgname-shuffle.out",
},
{
diff --git a/testjson/internal/good/good.go b/testjson/internal/good/good.go
new file mode 100644
index 0000000..5bff55e
--- /dev/null
+++ b/testjson/internal/good/good.go
@@ -0,0 +1,11 @@
+//go:build stubpkg
+// +build stubpkg
+
+package good
+
+func Something() int {
+ for i := 0; i < 10; i++ {
+ return i
+ }
+ return 0
+}
diff --git a/testjson/internal/good/good_test.go b/testjson/internal/good/good_test.go
index f5cf31a..968e752 100644
--- a/testjson/internal/good/good_test.go
+++ b/testjson/internal/good/good_test.go
@@ -1,3 +1,4 @@
+//go:build stubpkg
// +build stubpkg
package good
@@ -12,6 +13,7 @@ import (
func TestPassed(t *testing.T) {}
func TestPassedWithLog(t *testing.T) {
+ Something()
t.Log("this is a log")
}
diff --git a/testjson/summary_test.go b/testjson/summary_test.go
index 2c64191..e9b184c 100644
--- a/testjson/summary_test.go
+++ b/testjson/summary_test.go
@@ -7,6 +7,7 @@ import (
"testing"
"time"
+ "gotest.tools/gotestsum/internal/text"
"gotest.tools/v3/assert"
"gotest.tools/v3/golden"
)
@@ -319,3 +320,19 @@ func scanConfigFromGolden(filename string) func(t *testing.T) ScanConfig {
return ScanConfig{Stdout: bytes.NewReader(golden.Get(t, filename))}
}
}
+
+func TestSummary_TestTimeoutPanicRace(t *testing.T) {
+ source := golden.Get(t, "input/go-test-json-panic-race.out")
+ cfg := ScanConfig{
+ Stdout: bytes.NewReader(source),
+ Handler: &captureHandler{},
+ }
+ exec, err := ScanTestOutput(cfg)
+ assert.NilError(t, err)
+
+ out := new(bytes.Buffer)
+ PrintSummary(out, exec, SummarizeAll)
+
+ actual := text.ProcessLines(t, out, text.OpRemoveSummaryLineElapsedTime)
+ golden.Assert(t, actual, "summary/test-timeout-panic-race")
+}
diff --git a/testjson/testdata/format/pkgname-coverage-go1.20.out b/testjson/testdata/format/pkgname-coverage-go1.20.out
new file mode 100644
index 0000000..63c73c1
--- /dev/null
+++ b/testjson/testdata/format/pkgname-coverage-go1.20.out
@@ -0,0 +1,5 @@
+✖ gotestsum/testjson/internal/badmain (1ms)
+∅ gotestsum/testjson/internal/empty (cached)
+✓ gotestsum/testjson/internal/good (20ms) (coverage: 66.7% of statements)
+✖ gotestsum/testjson/internal/parallelfails (21ms)
+✖ gotestsum/testjson/internal/withfails (20ms)
diff --git a/testjson/testdata/format/pkgname-hide-empty.out b/testjson/testdata/format/pkgname-hide-empty.out
new file mode 100644
index 0000000..83cd3d7
--- /dev/null
+++ b/testjson/testdata/format/pkgname-hide-empty.out
@@ -0,0 +1,4 @@
+✖ testjson/internal/badmain (1ms)
+✓ testjson/internal/good (cached)
+✖ testjson/internal/parallelfails (20ms)
+✖ testjson/internal/withfails (20ms)
diff --git a/testjson/testdata/format/pkgname-hivis.out b/testjson/testdata/format/pkgname-hivis.out
new file mode 100644
index 0000000..c704987
--- /dev/null
+++ b/testjson/testdata/format/pkgname-hivis.out
@@ -0,0 +1,5 @@
+❌ testjson/internal/badmain (1ms)
+➖ testjson/internal/empty (cached)
+✅ testjson/internal/good (cached)
+❌ testjson/internal/parallelfails (20ms)
+❌ testjson/internal/withfails (20ms)
diff --git a/testjson/testdata/format/standard-quiet-coverage-go1.20.out b/testjson/testdata/format/standard-quiet-coverage-go1.20.out
new file mode 100644
index 0000000..f1f2f0b
--- /dev/null
+++ b/testjson/testdata/format/standard-quiet-coverage-go1.20.out
@@ -0,0 +1,11 @@
+sometimes main can exit 2
+program not built with -cover
+FAIL gotest.tools/gotestsum/testjson/internal/badmain 0.001s
+ok gotest.tools/gotestsum/testjson/internal/empty (cached) coverage: [no statements] [no tests to run]
+ok gotest.tools/gotestsum/testjson/internal/good 0.020s coverage: 66.7% of statements
+FAIL
+coverage: [no statements]
+FAIL gotest.tools/gotestsum/testjson/internal/parallelfails 0.021s
+FAIL
+coverage: [no statements]
+FAIL gotest.tools/gotestsum/testjson/internal/withfails 0.020s
diff --git a/testjson/testdata/input/go-test-json-panic-race.out b/testjson/testdata/input/go-test-json-panic-race.out
new file mode 100644
index 0000000..6d4e220
--- /dev/null
+++ b/testjson/testdata/input/go-test-json-panic-race.out
@@ -0,0 +1,39 @@
+{"Time":"2022-12-14T09:49:01.562401799Z","Action":"start","Package":"github.com/mafredri/test"}
+{"Time":"2022-12-14T09:49:01.569546938Z","Action":"run","Package":"github.com/mafredri/test","Test":"TestHello"}
+{"Time":"2022-12-14T09:49:01.569700427Z","Action":"output","Package":"github.com/mafredri/test","Test":"TestHello","Output":"=== RUN TestHello\n"}
+{"Time":"2022-12-14T09:49:02.569759117Z","Action":"output","Package":"github.com/mafredri/test","Test":"TestHello","Output":" main_test.go:11: Hello\n"}
+{"Time":"2022-12-14T09:49:02.56982657Z","Action":"output","Package":"github.com/mafredri/test","Test":"TestHello","Output":"--- PASS: TestHello (1.00s)\n"}
+{"Time":"2022-12-14T09:49:02.572963923Z","Action":"output","Package":"github.com/mafredri/test","Test":"TestHello","Output":"panic: test timed out after 1s\n"}
+{"Time":"2022-12-14T09:49:02.572982687Z","Action":"output","Package":"github.com/mafredri/test","Test":"TestHello","Output":"running tests:\n"}
+{"Time":"2022-12-14T09:49:02.572992095Z","Action":"output","Package":"github.com/mafredri/test","Test":"TestHello","Output":"\tTestHello (1s)\n"}
+{"Time":"2022-12-14T09:49:02.573000907Z","Action":"output","Package":"github.com/mafredri/test","Test":"TestHello","Output":"\n"}
+{"Time":"2022-12-14T09:49:02.573019868Z","Action":"output","Package":"github.com/mafredri/test","Test":"TestHello","Output":"goroutine 33 [running]:\n"}
+{"Time":"2022-12-14T09:49:02.573029067Z","Action":"output","Package":"github.com/mafredri/test","Test":"TestHello","Output":"testing.(*M).startAlarm.func1()\n"}
+{"Time":"2022-12-14T09:49:02.573038878Z","Action":"output","Package":"github.com/mafredri/test","Test":"TestHello","Output":"\t/home/mafredri/sdk/go1.20rc1/src/testing/testing.go:2240 +0x3b9\n"}
+{"Time":"2022-12-14T09:49:02.573064315Z","Action":"output","Package":"github.com/mafredri/test","Test":"TestHello","Output":"created by time.goFunc\n"}
+{"Time":"2022-12-14T09:49:02.573079975Z","Action":"output","Package":"github.com/mafredri/test","Test":"TestHello","Output":"\t/home/mafredri/sdk/go1.20rc1/src/time/sleep.go:176 +0x32\n"}
+{"Time":"2022-12-14T09:49:02.573097493Z","Action":"output","Package":"github.com/mafredri/test","Test":"TestHello","Output":"\n"}
+{"Time":"2022-12-14T09:49:02.573119064Z","Action":"output","Package":"github.com/mafredri/test","Test":"TestHello","Output":"goroutine 1 [runnable]:\n"}
+{"Time":"2022-12-14T09:49:02.573141104Z","Action":"output","Package":"github.com/mafredri/test","Test":"TestHello","Output":"testing.(*T).Run(0xc000083040, {0x5be88c?, 0x4ce6c5?}, 0x6072a0)\n"}
+{"Time":"2022-12-14T09:49:02.573162696Z","Action":"output","Package":"github.com/mafredri/test","Test":"TestHello","Output":"\t/home/mafredri/sdk/go1.20rc1/src/testing/testing.go:1629 +0x405\n"}
+{"Time":"2022-12-14T09:49:02.573178743Z","Action":"output","Package":"github.com/mafredri/test","Test":"TestHello","Output":"testing.runTests.func1(0x7438e0?)\n"}
+{"Time":"2022-12-14T09:49:02.573203585Z","Action":"output","Package":"github.com/mafredri/test","Test":"TestHello","Output":"\t/home/mafredri/sdk/go1.20rc1/src/testing/testing.go:2035 +0x45\n"}
+{"Time":"2022-12-14T09:49:02.57321895Z","Action":"output","Package":"github.com/mafredri/test","Test":"TestHello","Output":"testing.tRunner(0xc000083040, 0xc00025fc88)\n"}
+{"Time":"2022-12-14T09:49:02.573239542Z","Action":"output","Package":"github.com/mafredri/test","Test":"TestHello","Output":"\t/home/mafredri/sdk/go1.20rc1/src/testing/testing.go:1575 +0x10b\n"}
+{"Time":"2022-12-14T09:49:02.573342015Z","Action":"output","Package":"github.com/mafredri/test","Test":"TestHello","Output":"testing.runTests(0xc0000c0500?, {0x739320, 0x2, 0x2}, {0x0?, 0x100c0000ab938?, 0x743080?})\n"}
+{"Time":"2022-12-14T09:49:02.573376752Z","Action":"output","Package":"github.com/mafredri/test","Test":"TestHello","Output":"\t/home/mafredri/sdk/go1.20rc1/src/testing/testing.go:2033 +0x489\n"}
+{"Time":"2022-12-14T09:49:02.573403856Z","Action":"output","Package":"github.com/mafredri/test","Test":"TestHello","Output":"testing.(*M).Run(0xc0000c0500)\n"}
+{"Time":"2022-12-14T09:49:02.573433691Z","Action":"output","Package":"github.com/mafredri/test","Test":"TestHello","Output":"\t/home/mafredri/sdk/go1.20rc1/src/testing/testing.go:1905 +0x63a\n"}
+{"Time":"2022-12-14T09:49:02.573456763Z","Action":"output","Package":"github.com/mafredri/test","Test":"TestHello","Output":"main.main()\n"}
+{"Time":"2022-12-14T09:49:02.573483156Z","Action":"output","Package":"github.com/mafredri/test","Test":"TestHello","Output":"\t_testmain.go:49 +0x1aa\n"}
+{"Time":"2022-12-14T09:49:02.573503088Z","Action":"output","Package":"github.com/mafredri/test","Test":"TestHello","Output":"\n"}
+{"Time":"2022-12-14T09:49:02.573520911Z","Action":"output","Package":"github.com/mafredri/test","Test":"TestHello","Output":"goroutine 20 [runnable]:\n"}
+{"Time":"2022-12-14T09:49:02.573539195Z","Action":"output","Package":"github.com/mafredri/test","Test":"TestHello","Output":"runtime.goexit1()\n"}
+{"Time":"2022-12-14T09:49:02.573576101Z","Action":"output","Package":"github.com/mafredri/test","Test":"TestHello","Output":"\t/home/mafredri/sdk/go1.20rc1/src/runtime/proc.go:3616 +0x54\n"}
+{"Time":"2022-12-14T09:49:02.573596375Z","Action":"output","Package":"github.com/mafredri/test","Test":"TestHello","Output":"runtime.goexit()\n"}
+{"Time":"2022-12-14T09:49:02.573620424Z","Action":"output","Package":"github.com/mafredri/test","Test":"TestHello","Output":"\t/home/mafredri/sdk/go1.20rc1/src/runtime/asm_amd64.s:1599 +0x6\n"}
+{"Time":"2022-12-14T09:49:02.573637148Z","Action":"output","Package":"github.com/mafredri/test","Test":"TestHello","Output":"created by testing.(*T).Run\n"}
+{"Time":"2022-12-14T09:49:02.573690092Z","Action":"output","Package":"github.com/mafredri/test","Test":"TestHello","Output":"\t/home/mafredri/sdk/go1.20rc1/src/testing/testing.go:1628 +0x3ea\n"}
+{"Time":"2022-12-14T09:49:02.574702109Z","Action":"pass","Package":"github.com/mafredri/test","Test":"TestHello","Elapsed":1}
+{"Time":"2022-12-14T09:49:02.57473959Z","Action":"output","Package":"github.com/mafredri/test","Output":"FAIL\tgithub.com/mafredri/test\t1.012s\n"}
+{"Time":"2022-12-14T09:49:02.574754586Z","Action":"fail","Package":"github.com/mafredri/test","Elapsed":1.012}
diff --git a/testjson/testdata/input/go-test-json-with-cover-go1.20.err b/testjson/testdata/input/go-test-json-with-cover-go1.20.err
new file mode 100644
index 0000000..a19e6f3
--- /dev/null
+++ b/testjson/testdata/input/go-test-json-with-cover-go1.20.err
@@ -0,0 +1,2 @@
+# gotest.tools/gotestsum/testjson/internal/broken
+internal/broken/broken.go:5:21: undefined: somepackage
diff --git a/testjson/testdata/input/go-test-json-with-cover-go1.20.out b/testjson/testdata/input/go-test-json-with-cover-go1.20.out
new file mode 100644
index 0000000..4d9e2cd
--- /dev/null
+++ b/testjson/testdata/input/go-test-json-with-cover-go1.20.out
@@ -0,0 +1,339 @@
+{"Time":"2023-04-13T15:56:10.779258675-04:00","Action":"start","Package":"gotest.tools/gotestsum/testjson/internal/badmain"}
+{"Time":"2023-04-13T15:56:10.779971821-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/badmain","Output":"sometimes main can exit 2\n"}
+{"Time":"2023-04-13T15:56:10.779990294-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/badmain","Output":"program not built with -cover\n"}
+{"Time":"2023-04-13T15:56:10.780090632-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/badmain","Output":"FAIL\tgotest.tools/gotestsum/testjson/internal/badmain\t0.001s\n"}
+{"Time":"2023-04-13T15:56:10.780096536-04:00","Action":"fail","Package":"gotest.tools/gotestsum/testjson/internal/badmain","Elapsed":0.001}
+{"Time":"2023-04-13T15:56:10.784991272-04:00","Action":"start","Package":"gotest.tools/gotestsum/testjson/internal/empty"}
+{"Time":"2023-04-13T15:56:10.784998843-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/empty","Output":"testing: warning: no tests to run\n"}
+{"Time":"2023-04-13T15:56:10.785000834-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/empty","Output":"PASS\n"}
+{"Time":"2023-04-13T15:56:10.785021934-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/empty","Output":"\tgotest.tools/gotestsum/testjson/internal/empty\tcoverage: [no statements]\n"}
+{"Time":"2023-04-13T15:56:10.785023498-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/empty","Output":"ok \tgotest.tools/gotestsum/testjson/internal/empty\t(cached)\tcoverage: [no statements] [no tests to run]\n"}
+{"Time":"2023-04-13T15:56:10.785025461-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/empty","Elapsed":0}
+{"Time":"2023-04-13T15:56:10.844427956-04:00","Action":"start","Package":"gotest.tools/gotestsum/testjson/internal/good"}
+{"Time":"2023-04-13T15:56:10.84524858-04:00","Action":"run","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestPassed"}
+{"Time":"2023-04-13T15:56:10.845261979-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestPassed","Output":"=== RUN TestPassed\n"}
+{"Time":"2023-04-13T15:56:10.845268781-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestPassed","Output":"--- PASS: TestPassed (0.00s)\n"}
+{"Time":"2023-04-13T15:56:10.845272111-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestPassed","Elapsed":0}
+{"Time":"2023-04-13T15:56:10.845276245-04:00","Action":"run","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestPassedWithLog"}
+{"Time":"2023-04-13T15:56:10.845278286-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestPassedWithLog","Output":"=== RUN TestPassedWithLog\n"}
+{"Time":"2023-04-13T15:56:10.845280416-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestPassedWithLog","Output":" good_test.go:17: this is a log\n"}
+{"Time":"2023-04-13T15:56:10.845291145-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestPassedWithLog","Output":"--- PASS: TestPassedWithLog (0.00s)\n"}
+{"Time":"2023-04-13T15:56:10.845293831-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestPassedWithLog","Elapsed":0}
+{"Time":"2023-04-13T15:56:10.845297572-04:00","Action":"run","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestPassedWithStdout"}
+{"Time":"2023-04-13T15:56:10.845299493-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestPassedWithStdout","Output":"=== RUN TestPassedWithStdout\n"}
+{"Time":"2023-04-13T15:56:10.845301616-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestPassedWithStdout","Output":"this is a Print\n"}
+{"Time":"2023-04-13T15:56:10.845304167-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestPassedWithStdout","Output":"--- PASS: TestPassedWithStdout (0.00s)\n"}
+{"Time":"2023-04-13T15:56:10.845306564-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestPassedWithStdout","Elapsed":0}
+{"Time":"2023-04-13T15:56:10.845308891-04:00","Action":"run","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestSkipped"}
+{"Time":"2023-04-13T15:56:10.845310607-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestSkipped","Output":"=== RUN TestSkipped\n"}
+{"Time":"2023-04-13T15:56:10.84531274-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestSkipped","Output":" good_test.go:25: \n"}
+{"Time":"2023-04-13T15:56:10.845315106-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestSkipped","Output":"--- SKIP: TestSkipped (0.00s)\n"}
+{"Time":"2023-04-13T15:56:10.845316911-04:00","Action":"skip","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestSkipped","Elapsed":0}
+{"Time":"2023-04-13T15:56:10.84531892-04:00","Action":"run","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestSkippedWitLog"}
+{"Time":"2023-04-13T15:56:10.845320736-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestSkippedWitLog","Output":"=== RUN TestSkippedWitLog\n"}
+{"Time":"2023-04-13T15:56:10.845322745-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestSkippedWitLog","Output":" good_test.go:29: the skip message\n"}
+{"Time":"2023-04-13T15:56:10.845325268-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestSkippedWitLog","Output":"--- SKIP: TestSkippedWitLog (0.00s)\n"}
+{"Time":"2023-04-13T15:56:10.845327467-04:00","Action":"skip","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestSkippedWitLog","Elapsed":0}
+{"Time":"2023-04-13T15:56:10.845329688-04:00","Action":"run","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestWithStderr"}
+{"Time":"2023-04-13T15:56:10.845331357-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestWithStderr","Output":"=== RUN TestWithStderr\n"}
+{"Time":"2023-04-13T15:56:10.845333339-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestWithStderr","Output":"this is stderr\n"}
+{"Time":"2023-04-13T15:56:10.845336034-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestWithStderr","Output":"--- PASS: TestWithStderr (0.00s)\n"}
+{"Time":"2023-04-13T15:56:10.845339494-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestWithStderr","Elapsed":0}
+{"Time":"2023-04-13T15:56:10.845341837-04:00","Action":"run","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestParallelTheFirst"}
+{"Time":"2023-04-13T15:56:10.845343697-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestParallelTheFirst","Output":"=== RUN TestParallelTheFirst\n"}
+{"Time":"2023-04-13T15:56:10.845346168-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestParallelTheFirst","Output":"=== PAUSE TestParallelTheFirst\n"}
+{"Time":"2023-04-13T15:56:10.845348098-04:00","Action":"pause","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestParallelTheFirst"}
+{"Time":"2023-04-13T15:56:10.845352349-04:00","Action":"run","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestParallelTheSecond"}
+{"Time":"2023-04-13T15:56:10.845356344-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestParallelTheSecond","Output":"=== RUN TestParallelTheSecond\n"}
+{"Time":"2023-04-13T15:56:10.845359084-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestParallelTheSecond","Output":"=== PAUSE TestParallelTheSecond\n"}
+{"Time":"2023-04-13T15:56:10.845361049-04:00","Action":"pause","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestParallelTheSecond"}
+{"Time":"2023-04-13T15:56:10.845363416-04:00","Action":"run","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestParallelTheThird"}
+{"Time":"2023-04-13T15:56:10.845365217-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestParallelTheThird","Output":"=== RUN TestParallelTheThird\n"}
+{"Time":"2023-04-13T15:56:10.845367585-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestParallelTheThird","Output":"=== PAUSE TestParallelTheThird\n"}
+{"Time":"2023-04-13T15:56:10.845369619-04:00","Action":"pause","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestParallelTheThird"}
+{"Time":"2023-04-13T15:56:10.845371858-04:00","Action":"run","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestNestedSuccess"}
+{"Time":"2023-04-13T15:56:10.845373639-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestNestedSuccess","Output":"=== RUN TestNestedSuccess\n"}
+{"Time":"2023-04-13T15:56:10.845375857-04:00","Action":"run","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestNestedSuccess/a"}
+{"Time":"2023-04-13T15:56:10.845377612-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestNestedSuccess/a","Output":"=== RUN TestNestedSuccess/a\n"}
+{"Time":"2023-04-13T15:56:10.845380565-04:00","Action":"run","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestNestedSuccess/a/sub"}
+{"Time":"2023-04-13T15:56:10.845382581-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestNestedSuccess/a/sub","Output":"=== RUN TestNestedSuccess/a/sub\n"}
+{"Time":"2023-04-13T15:56:10.845385514-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestNestedSuccess/a/sub","Output":"--- PASS: TestNestedSuccess/a/sub (0.00s)\n"}
+{"Time":"2023-04-13T15:56:10.845388513-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestNestedSuccess/a/sub","Elapsed":0}
+{"Time":"2023-04-13T15:56:10.845391465-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestNestedSuccess/a","Output":"--- PASS: TestNestedSuccess/a (0.00s)\n"}
+{"Time":"2023-04-13T15:56:10.8453938-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestNestedSuccess/a","Elapsed":0}
+{"Time":"2023-04-13T15:56:10.845395869-04:00","Action":"run","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestNestedSuccess/b"}
+{"Time":"2023-04-13T15:56:10.8453977-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestNestedSuccess/b","Output":"=== RUN TestNestedSuccess/b\n"}
+{"Time":"2023-04-13T15:56:10.845399964-04:00","Action":"run","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestNestedSuccess/b/sub"}
+{"Time":"2023-04-13T15:56:10.845401957-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestNestedSuccess/b/sub","Output":"=== RUN TestNestedSuccess/b/sub\n"}
+{"Time":"2023-04-13T15:56:10.845404575-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestNestedSuccess/b/sub","Output":"--- PASS: TestNestedSuccess/b/sub (0.00s)\n"}
+{"Time":"2023-04-13T15:56:10.84540687-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestNestedSuccess/b/sub","Elapsed":0}
+{"Time":"2023-04-13T15:56:10.845409485-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestNestedSuccess/b","Output":"--- PASS: TestNestedSuccess/b (0.00s)\n"}
+{"Time":"2023-04-13T15:56:10.845413587-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestNestedSuccess/b","Elapsed":0}
+{"Time":"2023-04-13T15:56:10.845416072-04:00","Action":"run","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestNestedSuccess/c"}
+{"Time":"2023-04-13T15:56:10.845417893-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestNestedSuccess/c","Output":"=== RUN TestNestedSuccess/c\n"}
+{"Time":"2023-04-13T15:56:10.845444674-04:00","Action":"run","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestNestedSuccess/c/sub"}
+{"Time":"2023-04-13T15:56:10.845446734-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestNestedSuccess/c/sub","Output":"=== RUN TestNestedSuccess/c/sub\n"}
+{"Time":"2023-04-13T15:56:10.845449348-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestNestedSuccess/c/sub","Output":"--- PASS: TestNestedSuccess/c/sub (0.00s)\n"}
+{"Time":"2023-04-13T15:56:10.845451418-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestNestedSuccess/c/sub","Elapsed":0}
+{"Time":"2023-04-13T15:56:10.845454187-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestNestedSuccess/c","Output":"--- PASS: TestNestedSuccess/c (0.00s)\n"}
+{"Time":"2023-04-13T15:56:10.845456465-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestNestedSuccess/c","Elapsed":0}
+{"Time":"2023-04-13T15:56:10.845458696-04:00","Action":"run","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestNestedSuccess/d"}
+{"Time":"2023-04-13T15:56:10.8454605-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestNestedSuccess/d","Output":"=== RUN TestNestedSuccess/d\n"}
+{"Time":"2023-04-13T15:56:10.845462837-04:00","Action":"run","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestNestedSuccess/d/sub"}
+{"Time":"2023-04-13T15:56:10.845464631-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestNestedSuccess/d/sub","Output":"=== RUN TestNestedSuccess/d/sub\n"}
+{"Time":"2023-04-13T15:56:10.845467294-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestNestedSuccess/d/sub","Output":"--- PASS: TestNestedSuccess/d/sub (0.00s)\n"}
+{"Time":"2023-04-13T15:56:10.845469552-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestNestedSuccess/d/sub","Elapsed":0}
+{"Time":"2023-04-13T15:56:10.845472098-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestNestedSuccess/d","Output":"--- PASS: TestNestedSuccess/d (0.00s)\n"}
+{"Time":"2023-04-13T15:56:10.845491455-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestNestedSuccess/d","Elapsed":0}
+{"Time":"2023-04-13T15:56:10.845494041-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestNestedSuccess","Output":"--- PASS: TestNestedSuccess (0.00s)\n"}
+{"Time":"2023-04-13T15:56:10.845496414-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestNestedSuccess","Elapsed":0}
+{"Time":"2023-04-13T15:56:10.845498486-04:00","Action":"cont","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestParallelTheFirst"}
+{"Time":"2023-04-13T15:56:10.845520659-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestParallelTheFirst","Output":"=== CONT TestParallelTheFirst\n"}
+{"Time":"2023-04-13T15:56:10.855599932-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestParallelTheFirst","Output":"--- PASS: TestParallelTheFirst (0.01s)\n"}
+{"Time":"2023-04-13T15:56:10.855605874-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestParallelTheFirst","Elapsed":0.01}
+{"Time":"2023-04-13T15:56:10.855609334-04:00","Action":"cont","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestParallelTheThird"}
+{"Time":"2023-04-13T15:56:10.85561133-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestParallelTheThird","Output":"=== CONT TestParallelTheThird\n"}
+{"Time":"2023-04-13T15:56:10.857755303-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestParallelTheThird","Output":"--- PASS: TestParallelTheThird (0.00s)\n"}
+{"Time":"2023-04-13T15:56:10.857761229-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestParallelTheThird","Elapsed":0}
+{"Time":"2023-04-13T15:56:10.857763829-04:00","Action":"cont","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestParallelTheSecond"}
+{"Time":"2023-04-13T15:56:10.857765878-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestParallelTheSecond","Output":"=== CONT TestParallelTheSecond\n"}
+{"Time":"2023-04-13T15:56:10.863973379-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestParallelTheSecond","Output":"--- PASS: TestParallelTheSecond (0.01s)\n"}
+{"Time":"2023-04-13T15:56:10.863979047-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/good","Test":"TestParallelTheSecond","Elapsed":0.01}
+{"Time":"2023-04-13T15:56:10.863982423-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/good","Output":"PASS\n"}
+{"Time":"2023-04-13T15:56:10.864149159-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/good","Output":"\tgotest.tools/gotestsum/testjson/internal/good\tcoverage: 66.7% of statements\n"}
+{"Time":"2023-04-13T15:56:10.864404892-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/good","Output":"ok \tgotest.tools/gotestsum/testjson/internal/good\t0.020s\tcoverage: 66.7% of statements\n"}
+{"Time":"2023-04-13T15:56:10.864638477-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/good","Elapsed":0.02}
+{"Time":"2023-04-13T15:56:10.923523155-04:00","Action":"start","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails"}
+{"Time":"2023-04-13T15:56:10.924268028-04:00","Action":"run","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestPassed"}
+{"Time":"2023-04-13T15:56:10.924274941-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestPassed","Output":"=== RUN TestPassed\n"}
+{"Time":"2023-04-13T15:56:10.924280242-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestPassed","Output":"--- PASS: TestPassed (0.00s)\n"}
+{"Time":"2023-04-13T15:56:10.924282489-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestPassed","Elapsed":0}
+{"Time":"2023-04-13T15:56:10.92428507-04:00","Action":"run","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestPassedWithLog"}
+{"Time":"2023-04-13T15:56:10.924286209-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestPassedWithLog","Output":"=== RUN TestPassedWithLog\n"}
+{"Time":"2023-04-13T15:56:10.924287455-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestPassedWithLog","Output":" fails_test.go:15: this is a log\n"}
+{"Time":"2023-04-13T15:56:10.924289248-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestPassedWithLog","Output":"--- PASS: TestPassedWithLog (0.00s)\n"}
+{"Time":"2023-04-13T15:56:10.924291132-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestPassedWithLog","Elapsed":0}
+{"Time":"2023-04-13T15:56:10.924292691-04:00","Action":"run","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestPassedWithStdout"}
+{"Time":"2023-04-13T15:56:10.924293812-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestPassedWithStdout","Output":"=== RUN TestPassedWithStdout\n"}
+{"Time":"2023-04-13T15:56:10.924295003-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestPassedWithStdout","Output":"this is a Print\n"}
+{"Time":"2023-04-13T15:56:10.924297096-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestPassedWithStdout","Output":"--- PASS: TestPassedWithStdout (0.00s)\n"}
+{"Time":"2023-04-13T15:56:10.924298449-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestPassedWithStdout","Elapsed":0}
+{"Time":"2023-04-13T15:56:10.924300119-04:00","Action":"run","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestWithStderr"}
+{"Time":"2023-04-13T15:56:10.924301281-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestWithStderr","Output":"=== RUN TestWithStderr\n"}
+{"Time":"2023-04-13T15:56:10.924303003-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestWithStderr","Output":"this is stderr\n"}
+{"Time":"2023-04-13T15:56:10.924307743-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestWithStderr","Output":"--- PASS: TestWithStderr (0.00s)\n"}
+{"Time":"2023-04-13T15:56:10.924309762-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestWithStderr","Elapsed":0}
+{"Time":"2023-04-13T15:56:10.924313237-04:00","Action":"run","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestParallelTheFirst"}
+{"Time":"2023-04-13T15:56:10.924314389-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestParallelTheFirst","Output":"=== RUN TestParallelTheFirst\n"}
+{"Time":"2023-04-13T15:56:10.92432712-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestParallelTheFirst","Output":"=== PAUSE TestParallelTheFirst\n"}
+{"Time":"2023-04-13T15:56:10.924330217-04:00","Action":"pause","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestParallelTheFirst"}
+{"Time":"2023-04-13T15:56:10.924332637-04:00","Action":"run","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestParallelTheSecond"}
+{"Time":"2023-04-13T15:56:10.924334038-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestParallelTheSecond","Output":"=== RUN TestParallelTheSecond\n"}
+{"Time":"2023-04-13T15:56:10.924336004-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestParallelTheSecond","Output":"=== PAUSE TestParallelTheSecond\n"}
+{"Time":"2023-04-13T15:56:10.924337066-04:00","Action":"pause","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestParallelTheSecond"}
+{"Time":"2023-04-13T15:56:10.924339932-04:00","Action":"run","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestParallelTheThird"}
+{"Time":"2023-04-13T15:56:10.924341132-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestParallelTheThird","Output":"=== RUN TestParallelTheThird\n"}
+{"Time":"2023-04-13T15:56:10.924349059-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestParallelTheThird","Output":"=== PAUSE TestParallelTheThird\n"}
+{"Time":"2023-04-13T15:56:10.924351958-04:00","Action":"pause","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestParallelTheThird"}
+{"Time":"2023-04-13T15:56:10.924353871-04:00","Action":"run","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestNestedParallelFailures"}
+{"Time":"2023-04-13T15:56:10.924355125-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestNestedParallelFailures","Output":"=== RUN TestNestedParallelFailures\n"}
+{"Time":"2023-04-13T15:56:10.924433868-04:00","Action":"run","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestNestedParallelFailures/a"}
+{"Time":"2023-04-13T15:56:10.924439008-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestNestedParallelFailures/a","Output":"=== RUN TestNestedParallelFailures/a\n"}
+{"Time":"2023-04-13T15:56:10.924441456-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestNestedParallelFailures/a","Output":"=== PAUSE TestNestedParallelFailures/a\n"}
+{"Time":"2023-04-13T15:56:10.924442689-04:00","Action":"pause","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestNestedParallelFailures/a"}
+{"Time":"2023-04-13T15:56:10.924444414-04:00","Action":"run","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestNestedParallelFailures/b"}
+{"Time":"2023-04-13T15:56:10.92444554-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestNestedParallelFailures/b","Output":"=== RUN TestNestedParallelFailures/b\n"}
+{"Time":"2023-04-13T15:56:10.924446982-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestNestedParallelFailures/b","Output":"=== PAUSE TestNestedParallelFailures/b\n"}
+{"Time":"2023-04-13T15:56:10.924448088-04:00","Action":"pause","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestNestedParallelFailures/b"}
+{"Time":"2023-04-13T15:56:10.924449467-04:00","Action":"run","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestNestedParallelFailures/c"}
+{"Time":"2023-04-13T15:56:10.924450494-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestNestedParallelFailures/c","Output":"=== RUN TestNestedParallelFailures/c\n"}
+{"Time":"2023-04-13T15:56:10.924451838-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestNestedParallelFailures/c","Output":"=== PAUSE TestNestedParallelFailures/c\n"}
+{"Time":"2023-04-13T15:56:10.924453048-04:00","Action":"pause","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestNestedParallelFailures/c"}
+{"Time":"2023-04-13T15:56:10.924454352-04:00","Action":"run","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestNestedParallelFailures/d"}
+{"Time":"2023-04-13T15:56:10.924455399-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestNestedParallelFailures/d","Output":"=== RUN TestNestedParallelFailures/d\n"}
+{"Time":"2023-04-13T15:56:10.924461186-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestNestedParallelFailures/d","Output":"=== PAUSE TestNestedParallelFailures/d\n"}
+{"Time":"2023-04-13T15:56:10.924462546-04:00","Action":"pause","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestNestedParallelFailures/d"}
+{"Time":"2023-04-13T15:56:10.924463909-04:00","Action":"cont","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestNestedParallelFailures/a"}
+{"Time":"2023-04-13T15:56:10.924465075-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestNestedParallelFailures/a","Output":"=== CONT TestNestedParallelFailures/a\n"}
+{"Time":"2023-04-13T15:56:10.924466302-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestNestedParallelFailures/a","Output":" fails_test.go:50: failed sub a\n"}
+{"Time":"2023-04-13T15:56:10.924468352-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestNestedParallelFailures/a","Output":"--- FAIL: TestNestedParallelFailures/a (0.00s)\n"}
+{"Time":"2023-04-13T15:56:10.924471441-04:00","Action":"fail","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestNestedParallelFailures/a","Elapsed":0}
+{"Time":"2023-04-13T15:56:10.924472839-04:00","Action":"cont","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestNestedParallelFailures/c"}
+{"Time":"2023-04-13T15:56:10.924473874-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestNestedParallelFailures/c","Output":"=== CONT TestNestedParallelFailures/c\n"}
+{"Time":"2023-04-13T15:56:10.924475576-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestNestedParallelFailures/c","Output":" fails_test.go:50: failed sub c\n"}
+{"Time":"2023-04-13T15:56:10.924477274-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestNestedParallelFailures/c","Output":"--- FAIL: TestNestedParallelFailures/c (0.00s)\n"}
+{"Time":"2023-04-13T15:56:10.924478709-04:00","Action":"fail","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestNestedParallelFailures/c","Elapsed":0}
+{"Time":"2023-04-13T15:56:10.924479784-04:00","Action":"cont","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestNestedParallelFailures/d"}
+{"Time":"2023-04-13T15:56:10.924480798-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestNestedParallelFailures/d","Output":"=== CONT TestNestedParallelFailures/d\n"}
+{"Time":"2023-04-13T15:56:10.924483086-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestNestedParallelFailures/d","Output":" fails_test.go:50: failed sub d\n"}
+{"Time":"2023-04-13T15:56:10.924485331-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestNestedParallelFailures/d","Output":"--- FAIL: TestNestedParallelFailures/d (0.00s)\n"}
+{"Time":"2023-04-13T15:56:10.924487194-04:00","Action":"fail","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestNestedParallelFailures/d","Elapsed":0}
+{"Time":"2023-04-13T15:56:10.924488358-04:00","Action":"cont","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestNestedParallelFailures/b"}
+{"Time":"2023-04-13T15:56:10.924489375-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestNestedParallelFailures/b","Output":"=== CONT TestNestedParallelFailures/b\n"}
+{"Time":"2023-04-13T15:56:10.924490997-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestNestedParallelFailures/b","Output":" fails_test.go:50: failed sub b\n"}
+{"Time":"2023-04-13T15:56:10.924504382-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestNestedParallelFailures/b","Output":"--- FAIL: TestNestedParallelFailures/b (0.00s)\n"}
+{"Time":"2023-04-13T15:56:10.924507691-04:00","Action":"fail","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestNestedParallelFailures/b","Elapsed":0}
+{"Time":"2023-04-13T15:56:10.92450908-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestNestedParallelFailures","Output":"--- FAIL: TestNestedParallelFailures (0.00s)\n"}
+{"Time":"2023-04-13T15:56:10.924511874-04:00","Action":"fail","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestNestedParallelFailures","Elapsed":0}
+{"Time":"2023-04-13T15:56:10.924513277-04:00","Action":"cont","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestParallelTheFirst"}
+{"Time":"2023-04-13T15:56:10.924514387-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestParallelTheFirst","Output":"=== CONT TestParallelTheFirst\n"}
+{"Time":"2023-04-13T15:56:10.934774578-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestParallelTheFirst","Output":" fails_test.go:29: failed the first\n"}
+{"Time":"2023-04-13T15:56:10.934781349-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestParallelTheFirst","Output":"--- FAIL: TestParallelTheFirst (0.01s)\n"}
+{"Time":"2023-04-13T15:56:10.934783268-04:00","Action":"fail","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestParallelTheFirst","Elapsed":0.01}
+{"Time":"2023-04-13T15:56:10.934785074-04:00","Action":"cont","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestParallelTheThird"}
+{"Time":"2023-04-13T15:56:10.934786259-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestParallelTheThird","Output":"=== CONT TestParallelTheThird\n"}
+{"Time":"2023-04-13T15:56:10.936869124-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestParallelTheThird","Output":" fails_test.go:41: failed the third\n"}
+{"Time":"2023-04-13T15:56:10.936876228-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestParallelTheThird","Output":"--- FAIL: TestParallelTheThird (0.00s)\n"}
+{"Time":"2023-04-13T15:56:10.93687819-04:00","Action":"fail","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestParallelTheThird","Elapsed":0}
+{"Time":"2023-04-13T15:56:10.936879704-04:00","Action":"cont","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestParallelTheSecond"}
+{"Time":"2023-04-13T15:56:10.936880896-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestParallelTheSecond","Output":"=== CONT TestParallelTheSecond\n"}
+{"Time":"2023-04-13T15:56:10.943203668-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestParallelTheSecond","Output":" fails_test.go:35: failed the second\n"}
+{"Time":"2023-04-13T15:56:10.943242132-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestParallelTheSecond","Output":"--- FAIL: TestParallelTheSecond (0.01s)\n"}
+{"Time":"2023-04-13T15:56:10.943255977-04:00","Action":"fail","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Test":"TestParallelTheSecond","Elapsed":0.01}
+{"Time":"2023-04-13T15:56:10.943267489-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Output":"FAIL\n"}
+{"Time":"2023-04-13T15:56:10.943521769-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Output":"coverage: [no statements]\n"}
+{"Time":"2023-04-13T15:56:10.944225722-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Output":"FAIL\tgotest.tools/gotestsum/testjson/internal/parallelfails\t0.021s\n"}
+{"Time":"2023-04-13T15:56:10.944268594-04:00","Action":"fail","Package":"gotest.tools/gotestsum/testjson/internal/parallelfails","Elapsed":0.021}
+{"Time":"2023-04-13T15:56:11.005751009-04:00","Action":"start","Package":"gotest.tools/gotestsum/testjson/internal/withfails"}
+{"Time":"2023-04-13T15:56:11.006476255-04:00","Action":"run","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestPassed"}
+{"Time":"2023-04-13T15:56:11.006492467-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestPassed","Output":"=== RUN TestPassed\n"}
+{"Time":"2023-04-13T15:56:11.006502638-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestPassed","Output":"--- PASS: TestPassed (0.00s)\n"}
+{"Time":"2023-04-13T15:56:11.006506052-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestPassed","Elapsed":0}
+{"Time":"2023-04-13T15:56:11.006511998-04:00","Action":"run","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestPassedWithLog"}
+{"Time":"2023-04-13T15:56:11.006514213-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestPassedWithLog","Output":"=== RUN TestPassedWithLog\n"}
+{"Time":"2023-04-13T15:56:11.006517609-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestPassedWithLog","Output":" fails_test.go:18: this is a log\n"}
+{"Time":"2023-04-13T15:56:11.006521146-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestPassedWithLog","Output":"--- PASS: TestPassedWithLog (0.00s)\n"}
+{"Time":"2023-04-13T15:56:11.006523375-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestPassedWithLog","Elapsed":0}
+{"Time":"2023-04-13T15:56:11.006526139-04:00","Action":"run","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestPassedWithStdout"}
+{"Time":"2023-04-13T15:56:11.006528089-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestPassedWithStdout","Output":"=== RUN TestPassedWithStdout\n"}
+{"Time":"2023-04-13T15:56:11.006530485-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestPassedWithStdout","Output":"this is a Print\n"}
+{"Time":"2023-04-13T15:56:11.006533003-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestPassedWithStdout","Output":"--- PASS: TestPassedWithStdout (0.00s)\n"}
+{"Time":"2023-04-13T15:56:11.006537055-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestPassedWithStdout","Elapsed":0}
+{"Time":"2023-04-13T15:56:11.006539442-04:00","Action":"run","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestSkipped"}
+{"Time":"2023-04-13T15:56:11.006541694-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestSkipped","Output":"=== RUN TestSkipped\n"}
+{"Time":"2023-04-13T15:56:11.006543829-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestSkipped","Output":" fails_test.go:26: \n"}
+{"Time":"2023-04-13T15:56:11.006546648-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestSkipped","Output":"--- SKIP: TestSkipped (0.00s)\n"}
+{"Time":"2023-04-13T15:56:11.00654895-04:00","Action":"skip","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestSkipped","Elapsed":0}
+{"Time":"2023-04-13T15:56:11.006551396-04:00","Action":"run","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestSkippedWitLog"}
+{"Time":"2023-04-13T15:56:11.00655336-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestSkippedWitLog","Output":"=== RUN TestSkippedWitLog\n"}
+{"Time":"2023-04-13T15:56:11.006555478-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestSkippedWitLog","Output":" fails_test.go:30: the skip message\n"}
+{"Time":"2023-04-13T15:56:11.006559982-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestSkippedWitLog","Output":"--- SKIP: TestSkippedWitLog (0.00s)\n"}
+{"Time":"2023-04-13T15:56:11.006562434-04:00","Action":"skip","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestSkippedWitLog","Elapsed":0}
+{"Time":"2023-04-13T15:56:11.006564733-04:00","Action":"run","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestFailed"}
+{"Time":"2023-04-13T15:56:11.006567047-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestFailed","Output":"=== RUN TestFailed\n"}
+{"Time":"2023-04-13T15:56:11.006569113-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestFailed","Output":" fails_test.go:34: this failed\n"}
+{"Time":"2023-04-13T15:56:11.006571797-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestFailed","Output":"--- FAIL: TestFailed (0.00s)\n"}
+{"Time":"2023-04-13T15:56:11.006573836-04:00","Action":"fail","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestFailed","Elapsed":0}
+{"Time":"2023-04-13T15:56:11.006576065-04:00","Action":"run","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestWithStderr"}
+{"Time":"2023-04-13T15:56:11.00657794-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestWithStderr","Output":"=== RUN TestWithStderr\n"}
+{"Time":"2023-04-13T15:56:11.006580048-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestWithStderr","Output":"this is stderr\n"}
+{"Time":"2023-04-13T15:56:11.00658421-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestWithStderr","Output":"--- PASS: TestWithStderr (0.00s)\n"}
+{"Time":"2023-04-13T15:56:11.00658682-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestWithStderr","Elapsed":0}
+{"Time":"2023-04-13T15:56:11.006589902-04:00","Action":"run","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestFailedWithStderr"}
+{"Time":"2023-04-13T15:56:11.00659181-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestFailedWithStderr","Output":"=== RUN TestFailedWithStderr\n"}
+{"Time":"2023-04-13T15:56:11.006593778-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestFailedWithStderr","Output":"this is stderr\n"}
+{"Time":"2023-04-13T15:56:11.006595889-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestFailedWithStderr","Output":" fails_test.go:43: also failed\n"}
+{"Time":"2023-04-13T15:56:11.006598568-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestFailedWithStderr","Output":"--- FAIL: TestFailedWithStderr (0.00s)\n"}
+{"Time":"2023-04-13T15:56:11.006600911-04:00","Action":"fail","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestFailedWithStderr","Elapsed":0}
+{"Time":"2023-04-13T15:56:11.006603066-04:00","Action":"run","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestParallelTheFirst"}
+{"Time":"2023-04-13T15:56:11.00660502-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestParallelTheFirst","Output":"=== RUN TestParallelTheFirst\n"}
+{"Time":"2023-04-13T15:56:11.006608251-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestParallelTheFirst","Output":"=== PAUSE TestParallelTheFirst\n"}
+{"Time":"2023-04-13T15:56:11.006610161-04:00","Action":"pause","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestParallelTheFirst"}
+{"Time":"2023-04-13T15:56:11.006612656-04:00","Action":"run","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestParallelTheSecond"}
+{"Time":"2023-04-13T15:56:11.006614432-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestParallelTheSecond","Output":"=== RUN TestParallelTheSecond\n"}
+{"Time":"2023-04-13T15:56:11.006617009-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestParallelTheSecond","Output":"=== PAUSE TestParallelTheSecond\n"}
+{"Time":"2023-04-13T15:56:11.006618779-04:00","Action":"pause","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestParallelTheSecond"}
+{"Time":"2023-04-13T15:56:11.006622139-04:00","Action":"run","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestParallelTheThird"}
+{"Time":"2023-04-13T15:56:11.00662409-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestParallelTheThird","Output":"=== RUN TestParallelTheThird\n"}
+{"Time":"2023-04-13T15:56:11.006626677-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestParallelTheThird","Output":"=== PAUSE TestParallelTheThird\n"}
+{"Time":"2023-04-13T15:56:11.006628683-04:00","Action":"pause","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestParallelTheThird"}
+{"Time":"2023-04-13T15:56:11.006631006-04:00","Action":"run","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedWithFailure"}
+{"Time":"2023-04-13T15:56:11.006632924-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedWithFailure","Output":"=== RUN TestNestedWithFailure\n"}
+{"Time":"2023-04-13T15:56:11.0066359-04:00","Action":"run","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedWithFailure/a"}
+{"Time":"2023-04-13T15:56:11.006637618-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedWithFailure/a","Output":"=== RUN TestNestedWithFailure/a\n"}
+{"Time":"2023-04-13T15:56:11.006639999-04:00","Action":"run","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedWithFailure/a/sub"}
+{"Time":"2023-04-13T15:56:11.00664208-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedWithFailure/a/sub","Output":"=== RUN TestNestedWithFailure/a/sub\n"}
+{"Time":"2023-04-13T15:56:11.006644825-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedWithFailure/a/sub","Output":"--- PASS: TestNestedWithFailure/a/sub (0.00s)\n"}
+{"Time":"2023-04-13T15:56:11.006647059-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedWithFailure/a/sub","Elapsed":0}
+{"Time":"2023-04-13T15:56:11.006650182-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedWithFailure/a","Output":"--- PASS: TestNestedWithFailure/a (0.00s)\n"}
+{"Time":"2023-04-13T15:56:11.006652869-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedWithFailure/a","Elapsed":0}
+{"Time":"2023-04-13T15:56:11.006654975-04:00","Action":"run","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedWithFailure/b"}
+{"Time":"2023-04-13T15:56:11.006656968-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedWithFailure/b","Output":"=== RUN TestNestedWithFailure/b\n"}
+{"Time":"2023-04-13T15:56:11.006661121-04:00","Action":"run","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedWithFailure/b/sub"}
+{"Time":"2023-04-13T15:56:11.006663273-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedWithFailure/b/sub","Output":"=== RUN TestNestedWithFailure/b/sub\n"}
+{"Time":"2023-04-13T15:56:11.006666126-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedWithFailure/b/sub","Output":"--- PASS: TestNestedWithFailure/b/sub (0.00s)\n"}
+{"Time":"2023-04-13T15:56:11.006668656-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedWithFailure/b/sub","Elapsed":0}
+{"Time":"2023-04-13T15:56:11.006671419-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedWithFailure/b","Output":"--- PASS: TestNestedWithFailure/b (0.00s)\n"}
+{"Time":"2023-04-13T15:56:11.006673906-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedWithFailure/b","Elapsed":0}
+{"Time":"2023-04-13T15:56:11.00667627-04:00","Action":"run","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedWithFailure/c"}
+{"Time":"2023-04-13T15:56:11.006678134-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedWithFailure/c","Output":"=== RUN TestNestedWithFailure/c\n"}
+{"Time":"2023-04-13T15:56:11.006680932-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedWithFailure/c","Output":" fails_test.go:65: failed\n"}
+{"Time":"2023-04-13T15:56:11.00668368-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedWithFailure/c","Output":"--- FAIL: TestNestedWithFailure/c (0.00s)\n"}
+{"Time":"2023-04-13T15:56:11.006686215-04:00","Action":"fail","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedWithFailure/c","Elapsed":0}
+{"Time":"2023-04-13T15:56:11.006688688-04:00","Action":"run","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedWithFailure/d"}
+{"Time":"2023-04-13T15:56:11.006690736-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedWithFailure/d","Output":"=== RUN TestNestedWithFailure/d\n"}
+{"Time":"2023-04-13T15:56:11.006693767-04:00","Action":"run","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedWithFailure/d/sub"}
+{"Time":"2023-04-13T15:56:11.006695565-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedWithFailure/d/sub","Output":"=== RUN TestNestedWithFailure/d/sub\n"}
+{"Time":"2023-04-13T15:56:11.006698474-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedWithFailure/d/sub","Output":"--- PASS: TestNestedWithFailure/d/sub (0.00s)\n"}
+{"Time":"2023-04-13T15:56:11.006700782-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedWithFailure/d/sub","Elapsed":0}
+{"Time":"2023-04-13T15:56:11.006703547-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedWithFailure/d","Output":"--- PASS: TestNestedWithFailure/d (0.00s)\n"}
+{"Time":"2023-04-13T15:56:11.006705858-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedWithFailure/d","Elapsed":0}
+{"Time":"2023-04-13T15:56:11.006708538-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedWithFailure","Output":"--- FAIL: TestNestedWithFailure (0.00s)\n"}
+{"Time":"2023-04-13T15:56:11.006710819-04:00","Action":"fail","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedWithFailure","Elapsed":0}
+{"Time":"2023-04-13T15:56:11.006713038-04:00","Action":"run","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedSuccess"}
+{"Time":"2023-04-13T15:56:11.006714764-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedSuccess","Output":"=== RUN TestNestedSuccess\n"}
+{"Time":"2023-04-13T15:56:11.00671703-04:00","Action":"run","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedSuccess/a"}
+{"Time":"2023-04-13T15:56:11.0067187-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedSuccess/a","Output":"=== RUN TestNestedSuccess/a\n"}
+{"Time":"2023-04-13T15:56:11.006721024-04:00","Action":"run","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedSuccess/a/sub"}
+{"Time":"2023-04-13T15:56:11.006722863-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedSuccess/a/sub","Output":"=== RUN TestNestedSuccess/a/sub\n"}
+{"Time":"2023-04-13T15:56:11.00672549-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedSuccess/a/sub","Output":"--- PASS: TestNestedSuccess/a/sub (0.00s)\n"}
+{"Time":"2023-04-13T15:56:11.006727807-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedSuccess/a/sub","Elapsed":0}
+{"Time":"2023-04-13T15:56:11.006730437-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedSuccess/a","Output":"--- PASS: TestNestedSuccess/a (0.00s)\n"}
+{"Time":"2023-04-13T15:56:11.006732605-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedSuccess/a","Elapsed":0}
+{"Time":"2023-04-13T15:56:11.006734892-04:00","Action":"run","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedSuccess/b"}
+{"Time":"2023-04-13T15:56:11.006737517-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedSuccess/b","Output":"=== RUN TestNestedSuccess/b\n"}
+{"Time":"2023-04-13T15:56:11.006740718-04:00","Action":"run","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedSuccess/b/sub"}
+{"Time":"2023-04-13T15:56:11.006744553-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedSuccess/b/sub","Output":"=== RUN TestNestedSuccess/b/sub\n"}
+{"Time":"2023-04-13T15:56:11.0067474-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedSuccess/b/sub","Output":"--- PASS: TestNestedSuccess/b/sub (0.00s)\n"}
+{"Time":"2023-04-13T15:56:11.006749829-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedSuccess/b/sub","Elapsed":0}
+{"Time":"2023-04-13T15:56:11.006752598-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedSuccess/b","Output":"--- PASS: TestNestedSuccess/b (0.00s)\n"}
+{"Time":"2023-04-13T15:56:11.006754772-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedSuccess/b","Elapsed":0}
+{"Time":"2023-04-13T15:56:11.006757025-04:00","Action":"run","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedSuccess/c"}
+{"Time":"2023-04-13T15:56:11.006758838-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedSuccess/c","Output":"=== RUN TestNestedSuccess/c\n"}
+{"Time":"2023-04-13T15:56:11.006761212-04:00","Action":"run","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedSuccess/c/sub"}
+{"Time":"2023-04-13T15:56:11.006763019-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedSuccess/c/sub","Output":"=== RUN TestNestedSuccess/c/sub\n"}
+{"Time":"2023-04-13T15:56:11.006765855-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedSuccess/c/sub","Output":"--- PASS: TestNestedSuccess/c/sub (0.00s)\n"}
+{"Time":"2023-04-13T15:56:11.0067687-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedSuccess/c/sub","Elapsed":0}
+{"Time":"2023-04-13T15:56:11.006771508-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedSuccess/c","Output":"--- PASS: TestNestedSuccess/c (0.00s)\n"}
+{"Time":"2023-04-13T15:56:11.006773772-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedSuccess/c","Elapsed":0}
+{"Time":"2023-04-13T15:56:11.006776083-04:00","Action":"run","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedSuccess/d"}
+{"Time":"2023-04-13T15:56:11.006777869-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedSuccess/d","Output":"=== RUN TestNestedSuccess/d\n"}
+{"Time":"2023-04-13T15:56:11.006780067-04:00","Action":"run","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedSuccess/d/sub"}
+{"Time":"2023-04-13T15:56:11.006781976-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedSuccess/d/sub","Output":"=== RUN TestNestedSuccess/d/sub\n"}
+{"Time":"2023-04-13T15:56:11.006784628-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedSuccess/d/sub","Output":"--- PASS: TestNestedSuccess/d/sub (0.00s)\n"}
+{"Time":"2023-04-13T15:56:11.006786827-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedSuccess/d/sub","Elapsed":0}
+{"Time":"2023-04-13T15:56:11.006789399-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedSuccess/d","Output":"--- PASS: TestNestedSuccess/d (0.00s)\n"}
+{"Time":"2023-04-13T15:56:11.006791712-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedSuccess/d","Elapsed":0}
+{"Time":"2023-04-13T15:56:11.006794187-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedSuccess","Output":"--- PASS: TestNestedSuccess (0.00s)\n"}
+{"Time":"2023-04-13T15:56:11.006796399-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestNestedSuccess","Elapsed":0}
+{"Time":"2023-04-13T15:56:11.0067988-04:00","Action":"run","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestTimeout"}
+{"Time":"2023-04-13T15:56:11.006800666-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestTimeout","Output":"=== RUN TestTimeout\n"}
+{"Time":"2023-04-13T15:56:11.006804847-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestTimeout","Output":" timeout_test.go:13: skipping slow test\n"}
+{"Time":"2023-04-13T15:56:11.006807827-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestTimeout","Output":"--- SKIP: TestTimeout (0.00s)\n"}
+{"Time":"2023-04-13T15:56:11.006810123-04:00","Action":"skip","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestTimeout","Elapsed":0}
+{"Time":"2023-04-13T15:56:11.006812257-04:00","Action":"cont","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestParallelTheFirst"}
+{"Time":"2023-04-13T15:56:11.006814082-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestParallelTheFirst","Output":"=== CONT TestParallelTheFirst\n"}
+{"Time":"2023-04-13T15:56:11.016941648-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestParallelTheFirst","Output":"--- PASS: TestParallelTheFirst (0.01s)\n"}
+{"Time":"2023-04-13T15:56:11.01694787-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestParallelTheFirst","Elapsed":0.01}
+{"Time":"2023-04-13T15:56:11.016950903-04:00","Action":"cont","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestParallelTheThird"}
+{"Time":"2023-04-13T15:56:11.016952937-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestParallelTheThird","Output":"=== CONT TestParallelTheThird\n"}
+{"Time":"2023-04-13T15:56:11.019083027-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestParallelTheThird","Output":"--- PASS: TestParallelTheThird (0.00s)\n"}
+{"Time":"2023-04-13T15:56:11.019089024-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestParallelTheThird","Elapsed":0}
+{"Time":"2023-04-13T15:56:11.019091617-04:00","Action":"cont","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestParallelTheSecond"}
+{"Time":"2023-04-13T15:56:11.019093476-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestParallelTheSecond","Output":"=== CONT TestParallelTheSecond\n"}
+{"Time":"2023-04-13T15:56:11.025222383-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestParallelTheSecond","Output":"--- PASS: TestParallelTheSecond (0.01s)\n"}
+{"Time":"2023-04-13T15:56:11.025236153-04:00","Action":"pass","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Test":"TestParallelTheSecond","Elapsed":0.01}
+{"Time":"2023-04-13T15:56:11.025240694-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Output":"FAIL\n"}
+{"Time":"2023-04-13T15:56:11.025264597-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Output":"coverage: [no statements]\n"}
+{"Time":"2023-04-13T15:56:11.025516547-04:00","Action":"output","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Output":"FAIL\tgotest.tools/gotestsum/testjson/internal/withfails\t0.020s\n"}
+{"Time":"2023-04-13T15:56:11.025522005-04:00","Action":"fail","Package":"gotest.tools/gotestsum/testjson/internal/withfails","Elapsed":0.02}
diff --git a/testjson/testdata/summary/test-timeout-panic-race b/testjson/testdata/summary/test-timeout-panic-race
new file mode 100644
index 0000000..4305999
--- /dev/null
+++ b/testjson/testdata/summary/test-timeout-panic-race
@@ -0,0 +1,37 @@
+
+=== Failed
+=== FAIL: github.com/mafredri/test (0.00s)
+panic: test timed out after 1s
+running tests:
+ TestHello (1s)
+
+goroutine 33 [running]:
+testing.(*M).startAlarm.func1()
+ /home/mafredri/sdk/go1.20rc1/src/testing/testing.go:2240 +0x3b9
+created by time.goFunc
+ /home/mafredri/sdk/go1.20rc1/src/time/sleep.go:176 +0x32
+
+goroutine 1 [runnable]:
+testing.(*T).Run(0xc000083040, {0x5be88c?, 0x4ce6c5?}, 0x6072a0)
+ /home/mafredri/sdk/go1.20rc1/src/testing/testing.go:1629 +0x405
+testing.runTests.func1(0x7438e0?)
+ /home/mafredri/sdk/go1.20rc1/src/testing/testing.go:2035 +0x45
+testing.tRunner(0xc000083040, 0xc00025fc88)
+ /home/mafredri/sdk/go1.20rc1/src/testing/testing.go:1575 +0x10b
+testing.runTests(0xc0000c0500?, {0x739320, 0x2, 0x2}, {0x0?, 0x100c0000ab938?, 0x743080?})
+ /home/mafredri/sdk/go1.20rc1/src/testing/testing.go:2033 +0x489
+testing.(*M).Run(0xc0000c0500)
+ /home/mafredri/sdk/go1.20rc1/src/testing/testing.go:1905 +0x63a
+main.main()
+ _testmain.go:49 +0x1aa
+
+goroutine 20 [runnable]:
+runtime.goexit1()
+ /home/mafredri/sdk/go1.20rc1/src/runtime/proc.go:3616 +0x54
+runtime.goexit()
+ /home/mafredri/sdk/go1.20rc1/src/runtime/asm_amd64.s:1599 +0x6
+created by testing.(*T).Run
+ /home/mafredri/sdk/go1.20rc1/src/testing/testing.go:1628 +0x3ea
+FAIL github.com/mafredri/test 1.012s
+
+DONE 1 tests, 1 failure