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&#xA;FAIL&#x9;gotest.tools/gotestsum/testjson/internal/badmain&#x9;0.001s&#xA;</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&#xA;    good_test.go:23: &#xA;--- SKIP: TestSkipped (0.00s)&#xA;"></skipped>
+		</testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/good" name="TestSkippedWitLog" time="0.000000">
+			<skipped message="=== RUN   TestSkippedWitLog&#xA;    good_test.go:27: the skip message&#xA;--- SKIP: TestSkippedWitLog (0.00s)&#xA;"></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&#xA;=== PAUSE TestNestedParallelFailures/a&#xA;=== CONT  TestNestedParallelFailures/a&#xA;    fails_test.go:50: failed sub a&#xA;    --- FAIL: TestNestedParallelFailures/a (0.00s)&#xA;</failure>
+		</testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/parallelfails" name="TestNestedParallelFailures/d" time="0.000000">
+			<failure message="Failed" type="">=== RUN   TestNestedParallelFailures/d&#xA;=== PAUSE TestNestedParallelFailures/d&#xA;=== CONT  TestNestedParallelFailures/d&#xA;    fails_test.go:50: failed sub d&#xA;    --- FAIL: TestNestedParallelFailures/d (0.00s)&#xA;</failure>
+		</testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/parallelfails" name="TestNestedParallelFailures/c" time="0.000000">
+			<failure message="Failed" type="">=== RUN   TestNestedParallelFailures/c&#xA;=== PAUSE TestNestedParallelFailures/c&#xA;=== CONT  TestNestedParallelFailures/c&#xA;    fails_test.go:50: failed sub c&#xA;    --- FAIL: TestNestedParallelFailures/c (0.00s)&#xA;</failure>
+		</testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/parallelfails" name="TestNestedParallelFailures/b" time="0.000000">
+			<failure message="Failed" type="">=== RUN   TestNestedParallelFailures/b&#xA;=== PAUSE TestNestedParallelFailures/b&#xA;=== CONT  TestNestedParallelFailures/b&#xA;    fails_test.go:50: failed sub b&#xA;    --- FAIL: TestNestedParallelFailures/b (0.00s)&#xA;</failure>
+		</testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/parallelfails" name="TestNestedParallelFailures" time="0.000000">
+			<failure message="Failed" type="">=== RUN   TestNestedParallelFailures&#xA;--- FAIL: TestNestedParallelFailures (0.00s)&#xA;</failure>
+		</testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/parallelfails" name="TestParallelTheFirst" time="0.010000">
+			<failure message="Failed" type="">=== RUN   TestParallelTheFirst&#xA;=== PAUSE TestParallelTheFirst&#xA;=== CONT  TestParallelTheFirst&#xA;    fails_test.go:29: failed the first&#xA;--- FAIL: TestParallelTheFirst (0.01s)&#xA;</failure>
+		</testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/parallelfails" name="TestParallelTheThird" time="0.000000">
+			<failure message="Failed" type="">=== RUN   TestParallelTheThird&#xA;=== PAUSE TestParallelTheThird&#xA;=== CONT  TestParallelTheThird&#xA;    fails_test.go:41: failed the third&#xA;--- FAIL: TestParallelTheThird (0.00s)&#xA;</failure>
+		</testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/parallelfails" name="TestParallelTheSecond" time="0.010000">
+			<failure message="Failed" type="">=== RUN   TestParallelTheSecond&#xA;=== PAUSE TestParallelTheSecond&#xA;=== CONT  TestParallelTheSecond&#xA;    fails_test.go:35: failed the second&#xA;--- FAIL: TestParallelTheSecond (0.01s)&#xA;</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&#xA;    fails_test.go:34: this failed&#xA;--- FAIL: TestFailed (0.00s)&#xA;</failure>
+		</testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestFailedWithStderr" time="0.000000">
+			<failure message="Failed" type="">=== RUN   TestFailedWithStderr&#xA;this is stderr&#xA;    fails_test.go:43: also failed&#xA;--- FAIL: TestFailedWithStderr (0.00s)&#xA;</failure>
+		</testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestNestedWithFailure/c" time="0.000000">
+			<failure message="Failed" type="">=== RUN   TestNestedWithFailure/c&#xA;    fails_test.go:65: failed&#xA;    --- FAIL: TestNestedWithFailure/c (0.00s)&#xA;</failure>
+		</testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestNestedWithFailure" time="0.000000">
+			<failure message="Failed" type="">=== RUN   TestNestedWithFailure&#xA;--- FAIL: TestNestedWithFailure (0.00s)&#xA;</failure>
+		</testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestSkipped" time="0.000000">
+			<skipped message="=== RUN   TestSkipped&#xA;    fails_test.go:26: &#xA;--- SKIP: TestSkipped (0.00s)&#xA;"></skipped>
+		</testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestSkippedWitLog" time="0.000000">
+			<skipped message="=== RUN   TestSkippedWitLog&#xA;    fails_test.go:30: the skip message&#xA;--- SKIP: TestSkippedWitLog (0.00s)&#xA;"></skipped>
+		</testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestTimeout" time="0.000000">
+			<skipped message="=== RUN   TestTimeout&#xA;    timeout_test.go:13: skipping slow test&#xA;--- SKIP: TestTimeout (0.00s)&#xA;"></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

More details

Full run details

Historical runs