Repository: gotestyourself/gotestsum
Pull request #362 (2023-08-21 09:42:52)

Add post-run-command notify implementation for linux
Using `notify-send` instead of `terminal-notifier`, and icons instead of emojis.

Renamed the "notify-macos" to the proper "notify_darwin", so the build works.

Sample notification:

![gotestsum-notify-linux-pass](https://github.com/gotestyourself/gotestsum/assets/10364051/6bd96e9d-6511-4fc7-a479-63a3a90e18c1)

![gotestsum-notify-linux-fail](https://github.com/gotestyourself/gotestsum/assets/10364051/dda0eb6c-8c01-4c25-a587-b30bfb798698)

Issue #116

* https://github.com/gotestyourself/gotestsum/issues/74#issuecomment-549133324


Patch:
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
deleted file mode 100644
index 010c666..0000000
--- a/.github/workflows/ci.yaml
+++ /dev/null
@@ -1,17 +0,0 @@
-name: ci
-
-on:
-  pull_request:
-  push:
-
-jobs:
-  build:
-    runs-on: ubuntu-latest
-    steps:
-      - uses: actions/checkout@v3
-      - uses: actions/setup-go@v3
-        with:
-          go-version: '1.20'
-          cache: true
-      - run: go build .
-      - run: ./gotestsum -f testname
diff --git a/.project/golangci-lint.yml b/.project/golangci-lint.yml
index 43d400f..5ef5f79 100644
--- a/.project/golangci-lint.yml
+++ b/.project/golangci-lint.yml
@@ -31,6 +31,7 @@ linters:
     - deadcode
     - depguard
     - errcheck
+    - gocognit
     - goconst
     - gofmt
     - goimports
diff --git a/README.md b/README.md
index ed86817..b4fe867 100644
--- a/README.md
+++ b/README.md
@@ -139,14 +139,14 @@ test run has completed. The binary will be run with the following environment
 variables set:
 
 ```
-GOTESTSUM_ELAPSED       # test run time in seconds (ex: 2.45s)
-GOTESTSUM_FORMAT        # gotestsum format (ex: pkgname)
+GOTESTSUM_FORMAT        # gotestsum format (ex: short)
 GOTESTSUM_JSONFILE      # path to the jsonfile, empty if no file path was given
 GOTESTSUM_JUNITFILE     # path to the junit.xml file, empty if no file path was given
 TESTS_ERRORS            # number of errors
 TESTS_FAILED            # number of failed tests
 TESTS_SKIPPED           # number of skipped tests
 TESTS_TOTAL             # number of tests run
+TESTS_ELAPSED           # test time elapsed, in seconds
 ```
 
 To get more details about the test run, such as failure messages or the full list of failed
@@ -159,9 +159,15 @@ package may be used to parse the JSON file output.
 
 First install the example notification command with `go get gotest.tools/gotestsum/contrib/notify`.
 The command will be downloaded to `$GOPATH/bin` as `notify`. Note that this
-example `notify` command only works on macOS with
+example `notify` command only works on Linux with `notify-send` and on macOS with
 [terminal-notifer](https://github.com/julienXX/terminal-notifier) installed.
 
+On Linux, you need to have some "test-pass" and "test-fail" icons installed in your icon theme.
+Some sample icons can be found in `contrib/notify`, and can be installed with `make install`.
+
+On Windows, you can install [notify-send.exe](https://github.com/vaskovsky/notify-send)
+but it does not support custom icons so will have to use the basic "info" and "error".
+
 ```
 gotestsum --post-run-command notify
 ```
diff --git a/cmd/handler.go b/cmd/handler.go
index c5159a5..b2d74f9 100644
--- a/cmd/handler.go
+++ b/cmd/handler.go
@@ -161,6 +161,7 @@ func postRunHook(opts *options, execution *testjson.Execution) error {
 	if len(command) == 0 {
 		return nil
 	}
+	log.Debugf("exec: %s", command)
 
 	cmd := exec.Command(command[0], command[1:]...)
 	cmd.Stdout = opts.stdout
@@ -170,11 +171,11 @@ func postRunHook(opts *options, execution *testjson.Execution) error {
 		"GOTESTSUM_JSONFILE="+opts.jsonFile,
 		"GOTESTSUM_JSONFILE_TIMING_EVENTS="+opts.jsonFileTimingEvents,
 		"GOTESTSUM_JUNITFILE="+opts.junitFile,
-		fmt.Sprintf("GOTESTSUM_ELAPSED=%.3fs", execution.Elapsed().Seconds()),
 		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())),
+		fmt.Sprintf("TESTS_ELAPSED=%.3f", execution.Elapsed().Seconds()),
 	)
 	return cmd.Run()
 }
diff --git a/cmd/main.go b/cmd/main.go
index 86df767..7cbbf65 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -56,9 +56,8 @@ func setupFlags(name string) (*pflag.FlagSet, *options) {
 	flags.Usage = func() {
 		usage(os.Stdout, name, flags)
 	}
-
 	flags.StringVarP(&opts.format, "format", "f",
-		lookEnvWithDefault("GOTESTSUM_FORMAT", "pkgname"),
+		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")
@@ -140,7 +139,6 @@ Formats:
     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
-    github-actions           testname format with github actions log grouping
     standard-quiet           standard go test format
     standard-verbose         standard go test -v format
 
diff --git a/contrib/notify/.gitignore b/contrib/notify/.gitignore
new file mode 100644
index 0000000..f3b695d
--- /dev/null
+++ b/contrib/notify/.gitignore
@@ -0,0 +1,1 @@
+notify
diff --git a/contrib/notify/Makefile b/contrib/notify/Makefile
new file mode 100644
index 0000000..73771c6
--- /dev/null
+++ b/contrib/notify/Makefile
@@ -0,0 +1,13 @@
+GO = go
+INSTALL = install
+
+ICONS = icons/test-pass.svg icons/test-fail.svg
+ICONDIR = $(HOME)/.icons  # or /usr/share/icons
+
+build:
+	$(GO) build
+
+install: $(ICONS)
+	$(GO) install
+	$(INSTALL) -d $(ICONDIR)
+	$(INSTALL) $^ $(ICONDIR)
diff --git a/contrib/notify/icons/README.html b/contrib/notify/icons/README.html
new file mode 100644
index 0000000..4512a24
--- /dev/null
+++ b/contrib/notify/icons/README.html
@@ -0,0 +1,1 @@
+<a href="https://www.vecteezy.com/free-vector/pass-fail">Pass Fail Vectors by Vecteezy</a>
diff --git a/contrib/notify/notify-macos.go b/contrib/notify/notify_darwin.go
similarity index 85%
rename from contrib/notify/notify-macos.go
rename to contrib/notify/notify_darwin.go
index 3f6822c..ad68389 100644
--- a/contrib/notify/notify-macos.go
+++ b/contrib/notify/notify_darwin.go
@@ -43,9 +43,11 @@ func main() {
 		"-group", "gotestsum",
 		"-subtitle", subtitle,
 	}
-	log.Printf("terminal-notifier %#v", args)
-	err := exec.Command("terminal-notifier", args...).Run()
-	if err != nil {
+	cmd := exec.Command("terminal-notifier", args...)
+	log.Printf("%#v", cmd.Args)
+	cmd.Stdout = os.Stdout
+	cmd.Stderr = os.Stderr
+	if err := cmd.Run(); err != nil {
 		log.Fatalf("Failed to exec: %v", err)
 	}
 }
diff --git a/contrib/notify/notify_linux.go b/contrib/notify/notify_linux.go
new file mode 100644
index 0000000..8da6c3e
--- /dev/null
+++ b/contrib/notify/notify_linux.go
@@ -0,0 +1,57 @@
+package main
+
+import (
+	"fmt"
+	"log"
+	"os"
+	"os/exec"
+	"strconv"
+)
+
+func main() {
+	total := envInt("TOTAL")
+	skipped := envInt("SKIPPED")
+	failed := envInt("FAILED")
+	errors := envInt("ERRORS")
+
+	icon := "test-pass"
+	title := "Passed"
+	switch {
+	case errors > 0:
+		icon = "dialog-warning"
+		title = "Errored"
+	case failed > 0:
+		icon = "test-fail"
+		title = "Failed"
+	case skipped > 0:
+		title = "Passed with skipped"
+	}
+
+	subtitle := fmt.Sprintf("%d Tests Run", total)
+	if errors > 0 {
+		subtitle += fmt.Sprintf(", %d Errored", errors)
+	}
+	if failed > 0 {
+		subtitle += fmt.Sprintf(", %d Failed", failed)
+	}
+	if skipped > 0 {
+		subtitle += fmt.Sprintf(", %d Skipped", skipped)
+	}
+
+	cmd := exec.Command("notify-send", "--icon", icon, title, subtitle)
+	log.Printf("%#v", cmd.Args)
+	cmd.Stdout = os.Stdout
+	cmd.Stderr = os.Stderr
+	if err := cmd.Run(); err != nil {
+		log.Fatalf("Failed to exec: %v", err)
+	}
+}
+
+func envInt(name string) int {
+	val := os.Getenv("TESTS_" + name)
+	n, err := strconv.Atoi(val)
+	if err != nil {
+		return 0
+	}
+	return n
+}
diff --git a/contrib/notify/notify_windows.go b/contrib/notify/notify_windows.go
new file mode 100644
index 0000000..f3d384d
--- /dev/null
+++ b/contrib/notify/notify_windows.go
@@ -0,0 +1,57 @@
+package main
+
+import (
+	"fmt"
+	"log"
+	"os"
+	"os/exec"
+	"strconv"
+)
+
+func main() {
+	total := envInt("TOTAL")
+	skipped := envInt("SKIPPED")
+	failed := envInt("FAILED")
+	errors := envInt("ERRORS")
+
+	icon := "info" // Info 🛈
+	title := "Passed"
+	switch {
+	case errors > 0:
+		icon = "important" // Warning ⚠
+		title = "Errored"
+	case failed > 0:
+		icon = "error" // Error ⮾
+		title = "Failed"
+	case skipped > 0:
+		title = "Passed with skipped"
+	}
+
+	subtitle := fmt.Sprintf("%d Tests Run", total)
+	if errors > 0 {
+		subtitle += fmt.Sprintf(", %d Errored", errors)
+	}
+	if failed > 0 {
+		subtitle += fmt.Sprintf(", %d Failed", failed)
+	}
+	if skipped > 0 {
+		subtitle += fmt.Sprintf(", %d Skipped", skipped)
+	}
+
+	cmd := exec.Command("notify-send.exe", "-i", icon, title, subtitle)
+	log.Printf("%#v", cmd.Args)
+	cmd.Stdout = os.Stdout
+	cmd.Stderr = os.Stderr
+	if err := cmd.Run(); err != nil {
+		log.Fatalf("Failed to exec: %v", err)
+	}
+}
+
+func envInt(name string) int {
+	val := os.Getenv("TESTS_" + name)
+	n, err := strconv.Atoi(val)
+	if err != nil {
+		return 0
+	}
+	return n
+}


Test patch:
diff --git a/cmd/handler_test.go b/cmd/handler_test.go
index 30fa786..dabefd2 100644
--- a/cmd/handler_test.go
+++ b/cmd/handler_test.go
@@ -39,10 +39,9 @@ func TestPostRunHook(t *testing.T) {
 	assert.NilError(t, err)
 
 	actual := text.ProcessLines(t, buf, func(line string) string {
-		if strings.HasPrefix(line, "GOTESTSUM_ELAPSED=0.0") &&
-			strings.HasSuffix(line, "s") {
+		if strings.HasPrefix(line, "TESTS_ELAPSED=0.0") {
 			i := strings.Index(line, "=")
-			return line[:i] + "=0.000s"
+			return line[:i] + "=0.000"
 		}
 		return line
 	})
@@ -72,8 +71,6 @@ func (bufferCloser) Close() error { return nil }
 func (bufferCloser) Sync() error { return nil }
 
 func TestEventHandler_Event_WithMissingActionFail(t *testing.T) {
-	t.Setenv("GITHUB_ACTIONS", "no")
-
 	buf := new(bufferCloser)
 	errBuf := new(bytes.Buffer)
 	format := testjson.NewEventFormatter(errBuf, "testname", testjson.FormatOptions{})
diff --git a/cmd/main_e2e_test.go b/cmd/main_e2e_test.go
index 4478600..932cfe0 100644
--- a/cmd/main_e2e_test.go
+++ b/cmd/main_e2e_test.go
@@ -31,7 +31,6 @@ func TestE2E_RerunFails(t *testing.T) {
 	if testing.Short() {
 		t.Skip("too slow for short run")
 	}
-	t.Setenv("GITHUB_ACTIONS", "no")
 
 	type testCase struct {
 		name        string
@@ -221,7 +220,6 @@ func TestE2E_MaxFails_EndTestRun(t *testing.T) {
 	envVars["TEST_SEEDFILE"] = tmpFile.Path()
 	env.PatchAll(t, envVars)
 
-	t.Setenv("GOTESTSUM_FORMAT", "pkgname")
 	flags, opts := setupFlags("gotestsum")
 	args := []string{"--max-fails=2", "--packages=./testdata/e2e/flaky/", "--", "-tags=testdata"}
 	assert.NilError(t, flags.Parse(args))
@@ -246,7 +244,6 @@ func TestE2E_IgnoresWarnings(t *testing.T) {
 	if testing.Short() {
 		t.Skip("too slow for short run")
 	}
-	t.Setenv("GITHUB_ACTIONS", "no")
 
 	flags, opts := setupFlags("gotestsum")
 	args := []string{
diff --git a/cmd/testdata/gotestsum-help-text b/cmd/testdata/gotestsum-help-text
index 4ff8982..048054d 100644
--- a/cmd/testdata/gotestsum-help-text
+++ b/cmd/testdata/gotestsum-help-text
@@ -6,7 +6,7 @@ See https://pkg.go.dev/gotest.tools/gotestsum#section-readme for detailed docume
 
 Flags:
       --debug                                       enabled debug logging
-  -f, --format string                               print format of test input (default "pkgname")
+  -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)
@@ -36,7 +36,6 @@ Formats:
     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
-    github-actions           testname format with github actions log grouping
     standard-quiet           standard go test format
     standard-verbose         standard go test -v format
 
diff --git a/cmd/testdata/post-run-hook-expected b/cmd/testdata/post-run-hook-expected
index 7e5fd9a..55ea6b5 100644
--- a/cmd/testdata/post-run-hook-expected
+++ b/cmd/testdata/post-run-hook-expected
@@ -1,8 +1,8 @@
-GOTESTSUM_ELAPSED=0.000s
 GOTESTSUM_FORMAT=short
 GOTESTSUM_JSONFILE=events.json
 GOTESTSUM_JSONFILE_TIMING_EVENTS=timing.json
 GOTESTSUM_JUNITFILE=junit.xml
+TESTS_ELAPSED=0.000
 TESTS_ERRORS=0
 TESTS_FAILED=13
 TESTS_SKIPPED=5
diff --git a/contrib/notify/icons/test-fail.svg b/contrib/notify/icons/test-fail.svg
new file mode 100644
index 0000000..f6b97c7
--- /dev/null
+++ b/contrib/notify/icons/test-fail.svg
@@ -0,0 +1,1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="1333.951" height="1333.957" viewBox="0 0 352.941 352.943"><path d="M12574 0c-2054.7 0-3726.32 1671.57-3726.32 3726.24S10519.3 7452.55 12574 7452.55c2054.7 0 3726.2-1671.64 3726.2-3726.31S14628.7 0 12574 0" style="fill:#ed1d24;fill-opacity:1;fill-rule:nonzero;stroke:none" transform="matrix(.04736 0 0 -.04736 -419.014 352.943)"/><path d="m16300 5971.77-520.5 520.45-1841.2-1841.13-1841.1 1841.13-520.5-520.45 1841.1-1841.22-1841.1-1841.13 520.5-520.54 1841.1 1841.22 1841.2-1841.22 520.5 520.54-1841.1 1841.13L16300 5971.77" style="fill:#fff;fill-opacity:1;fill-rule:nonzero;stroke:none" transform="matrix(.04272 0 0 -.04272 -419.014 352.943)"/></svg>
\ No newline at end of file
diff --git a/contrib/notify/icons/test-pass.svg b/contrib/notify/icons/test-pass.svg
new file mode 100644
index 0000000..2e96bda
--- /dev/null
+++ b/contrib/notify/icons/test-pass.svg
@@ -0,0 +1,1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="1333.951" height="1333.948" viewBox="0 0 352.941 352.94"><path d="M12574 8544.8c-2054.7 0-3726.32 1671.6-3726.32 3726.3 0 2054.7 1671.62 3726.2 3726.32 3726.2 2054.7 0 3726.2-1671.5 3726.2-3726.2S14628.7 8544.8 12574 8544.8" style="fill:#6cc62e;fill-opacity:1;fill-rule:nonzero;stroke:none" transform="matrix(.04736 0 0 -.04736 -419.014 757.61)"/><path d="m12683.6 12354.6-1021.9 1022-512.5-512.6 1534.4-1534.5 256.3 256.3 256.3 256.3 3103.8 3103.8-512.6 512.5-3103.8-3103.8" style="fill:#fff;fill-opacity:1;fill-rule:nonzero;stroke:none" transform="matrix(.04339 0 0 -.04339 -419.014 757.61)"/></svg>
\ No newline at end of file
diff --git a/testjson/execution.go b/testjson/execution.go
index 27e041c..517b7ec 100644
--- a/testjson/execution.go
+++ b/testjson/execution.go
@@ -63,6 +63,11 @@ func (e TestEvent) PackageEvent() bool {
 	return e.Test == ""
 }
 
+// ElapsedFormatted returns Elapsed formatted in the go test format, ex (0.00s).
+func (e TestEvent) ElapsedFormatted() string {
+	return fmt.Sprintf("(%.2fs)", e.Elapsed)
+}
+
 // Bytes returns the serialized JSON bytes that were parsed to create the event.
 func (e TestEvent) Bytes() []byte {
 	return e.raw
@@ -194,6 +199,7 @@ func (p *Package) addOutput(id int, output string) {
 	if strings.HasPrefix(output, "panic: ") {
 		p.panicked = true
 	}
+	// TODO: limit size of buffered test output
 	p.output[id] = append(p.output[id], output)
 }
 
diff --git a/testjson/format.go b/testjson/format.go
index f298eb0..180086e 100644
--- a/testjson/format.go
+++ b/testjson/format.go
@@ -4,7 +4,6 @@ import (
 	"bufio"
 	"fmt"
 	"io"
-	"os"
 	"strings"
 
 	"github.com/fatih/color"
@@ -74,22 +73,18 @@ func standardJSONFormat(out io.Writer) EventFormatter {
 	})
 }
 
-func testNameFormatTestEvent(out io.Writer, event TestEvent) {
-	pkgPath := RelativePackagePath(event.Package)
-
-	fmt.Fprintf(out, "%s %s%s %s\n",
-		colorEvent(event)(strings.ToUpper(string(event.Action))),
-		joinPkgToTestName(pkgPath, event.Test),
-		formatRunID(event.RunID),
-		fmt.Sprintf("(%.2fs)", event.Elapsed))
-}
-
 func testNameFormat(out io.Writer) EventFormatter {
 	buf := bufio.NewWriter(out)
 	// nolint:errcheck
 	return eventFormatterFunc(func(event TestEvent, exec *Execution) error {
 		formatTest := func() error {
-			testNameFormatTestEvent(buf, event)
+			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()
 		}
 
@@ -106,7 +101,6 @@ func testNameFormat(out io.Writer) EventFormatter {
 			result := colorEvent(event)(strings.ToUpper(string(event.Action)))
 			pkg := exec.Package(event.Package)
 			if event.Action == ActionSkip || (event.Action == ActionPass && pkg.Total == 0) {
-				event.Action = ActionSkip // always color these as skip actions
 				result = colorEvent(event)("EMPTY")
 			}
 
@@ -122,7 +116,7 @@ func testNameFormat(out io.Writer) EventFormatter {
 			pkg.WriteOutputTo(buf, tc.ID)
 			return formatTest()
 
-		case event.Action == ActionPass || event.Action == ActionSkip:
+		case event.Action == ActionPass:
 			return formatTest()
 		}
 		return nil
@@ -313,77 +307,12 @@ func NewEventFormatter(out io.Writer, format string, formatOpts FormatOptions) E
 	case "dots-v2":
 		return newDotFormatter(out, formatOpts)
 	case "testname", "short-verbose":
-		if os.Getenv("GITHUB_ACTIONS") == "true" {
-			return githubActionsFormat(out)
-		}
 		return testNameFormat(out)
 	case "pkgname", "short":
 		return pkgNameFormat(out, formatOpts)
 	case "pkgname-and-test-fails", "short-with-failures":
 		return pkgNameWithFailuresFormat(out, formatOpts)
-	case "github-actions", "github-action":
-		return githubActionsFormat(out)
 	default:
 		return nil
 	}
 }
-
-func githubActionsFormat(out io.Writer) EventFormatter {
-	buf := bufio.NewWriter(out)
-
-	type name struct {
-		Package string
-		Test    string
-	}
-	output := map[name][]string{}
-
-	return eventFormatterFunc(func(event TestEvent, exec *Execution) error {
-		key := name{Package: event.Package, Test: event.Test}
-
-		// test case output
-		if event.Test != "" && event.Action == ActionOutput {
-			if !isFramingLine(event.Output, event.Test) {
-				output[key] = append(output[key], event.Output)
-			}
-			return nil
-		}
-
-		// test case end event
-		if event.Test != "" && event.Action.IsTerminal() {
-			if len(output[key]) > 0 {
-				buf.WriteString("::group::")
-			} else {
-				buf.WriteString("  ")
-			}
-			testNameFormatTestEvent(buf, event)
-
-			for _, item := range output[key] {
-				buf.WriteString(item)
-			}
-			if len(output[key]) > 0 {
-				buf.WriteString("\n::endgroup::\n")
-			}
-			delete(output, key)
-			return buf.Flush()
-		}
-
-		// package event
-		if !event.Action.IsTerminal() {
-			return nil
-		}
-
-		result := colorEvent(event)(strings.ToUpper(string(event.Action)))
-		pkg := exec.Package(event.Package)
-		if event.Action == ActionSkip || (event.Action == ActionPass && pkg.Total == 0) {
-			event.Action = ActionSkip // always color these as skip actions
-			result = colorEvent(event)("EMPTY")
-		}
-
-		buf.WriteString("  ")
-		buf.WriteString(result)
-		buf.WriteString(" Package ")
-		buf.WriteString(packageLine(event, exec.Package(event.Package)))
-		buf.WriteString("\n")
-		return buf.Flush()
-	})
-}
diff --git a/testjson/format_test.go b/testjson/format_test.go
index b2b4605..552ae22 100644
--- a/testjson/format_test.go
+++ b/testjson/format_test.go
@@ -132,11 +132,6 @@ func TestFormats_DefaultGoTestJson(t *testing.T) {
 			format:      standardJSONFormat,
 			expectedOut: "input/go-test-json.out",
 		},
-		{
-			name:        "github-actions",
-			format:      githubActionsFormat,
-			expectedOut: "format/github-actions.out",
-		},
 	}
 
 	for _, tc := range testCases {
diff --git a/testjson/summary.go b/testjson/summary.go
index a06865b..0bbbee0 100644
--- a/testjson/summary.go
+++ b/testjson/summary.go
@@ -178,7 +178,7 @@ func writeTestCaseSummary(out io.Writer, execution executionSummary, conf testCa
 			formatRunID(tc.RunID),
 			FormatDurationAsSeconds(tc.Elapsed, 2))
 		for _, line := range execution.OutputLines(tc) {
-			if isFramingLine(line, tc.Test.Name()) {
+			if isFramingLine(line) || conf.filter(tc.Test.Name(), line) {
 				continue
 			}
 			fmt.Fprint(out, line)
@@ -192,6 +192,7 @@ func writeTestCaseSummary(out io.Writer, execution executionSummary, conf testCa
 type testCaseFormatConfig struct {
 	header string
 	prefix string
+	filter func(testName string, line string) bool
 	getter func(executionSummary) []TestCase
 }
 
@@ -200,6 +201,9 @@ func formatFailed() testCaseFormatConfig {
 	return testCaseFormatConfig{
 		header: withColor("Failed"),
 		prefix: withColor("FAIL"),
+		filter: func(testName string, line string) bool {
+			return strings.HasPrefix(line, "--- FAIL: "+testName+" ")
+		},
 		getter: func(execution executionSummary) []TestCase {
 			return execution.Failed()
 		},
@@ -211,17 +215,17 @@ func formatSkipped() testCaseFormatConfig {
 	return testCaseFormatConfig{
 		header: withColor("Skipped"),
 		prefix: withColor("SKIP"),
+		filter: func(testName string, line string) bool {
+			return strings.HasPrefix(line, "--- SKIP: "+testName+" ")
+		},
 		getter: func(execution executionSummary) []TestCase {
 			return execution.Skipped()
 		},
 	}
 }
 
-func isFramingLine(line string, testName string) bool {
+func isFramingLine(line string) bool {
 	return strings.HasPrefix(line, "=== RUN   Test") ||
 		strings.HasPrefix(line, "=== PAUSE Test") ||
-		strings.HasPrefix(line, "=== CONT  Test") ||
-		strings.HasPrefix(line, "--- FAIL: "+testName+" ") ||
-		strings.HasPrefix(line, "--- SKIP: "+testName+" ") ||
-		strings.HasPrefix(line, "--- PASS: "+testName+" ")
+		strings.HasPrefix(line, "=== CONT  Test")
 }
diff --git a/testjson/testdata/format/github-actions.out b/testjson/testdata/format/github-actions.out
deleted file mode 100644
index 0e6f325..0000000
--- a/testjson/testdata/format/github-actions.out
+++ /dev/null
@@ -1,213 +0,0 @@
-  FAIL Package testjson/internal/badmain (1ms)
-
-  EMPTY Package testjson/internal/empty (cached)
-
-  PASS testjson/internal/good.TestPassed (0.00s)
-::group::PASS testjson/internal/good.TestPassedWithLog (0.00s)
-    good_test.go:15: this is a log
-
-::endgroup::
-::group::PASS testjson/internal/good.TestPassedWithStdout (0.00s)
-this is a Print
-
-::endgroup::
-::group::SKIP testjson/internal/good.TestSkipped (0.00s)
-    good_test.go:23: 
-
-::endgroup::
-::group::SKIP testjson/internal/good.TestSkippedWitLog (0.00s)
-    good_test.go:27: the skip message
-
-::endgroup::
-::group::PASS testjson/internal/good.TestWithStderr (0.00s)
-this is stderr
-
-::endgroup::
-::group::PASS testjson/internal/good.TestNestedSuccess/a/sub (0.00s)
-        --- PASS: TestNestedSuccess/a/sub (0.00s)
-
-::endgroup::
-::group::PASS testjson/internal/good.TestNestedSuccess/a (0.00s)
-    --- PASS: TestNestedSuccess/a (0.00s)
-
-::endgroup::
-::group::PASS testjson/internal/good.TestNestedSuccess/b/sub (0.00s)
-        --- PASS: TestNestedSuccess/b/sub (0.00s)
-
-::endgroup::
-::group::PASS testjson/internal/good.TestNestedSuccess/b (0.00s)
-    --- PASS: TestNestedSuccess/b (0.00s)
-
-::endgroup::
-::group::PASS testjson/internal/good.TestNestedSuccess/c/sub (0.00s)
-        --- PASS: TestNestedSuccess/c/sub (0.00s)
-
-::endgroup::
-::group::PASS testjson/internal/good.TestNestedSuccess/c (0.00s)
-    --- PASS: TestNestedSuccess/c (0.00s)
-
-::endgroup::
-::group::PASS testjson/internal/good.TestNestedSuccess/d/sub (0.00s)
-        --- PASS: TestNestedSuccess/d/sub (0.00s)
-
-::endgroup::
-::group::PASS testjson/internal/good.TestNestedSuccess/d (0.00s)
-    --- PASS: TestNestedSuccess/d (0.00s)
-
-::endgroup::
-  PASS testjson/internal/good.TestNestedSuccess (0.00s)
-  PASS testjson/internal/good.TestParallelTheFirst (0.01s)
-  PASS testjson/internal/good.TestParallelTheThird (0.00s)
-  PASS testjson/internal/good.TestParallelTheSecond (0.01s)
-  PASS Package testjson/internal/good (cached)
-
-  PASS testjson/internal/parallelfails.TestPassed (0.00s)
-::group::PASS testjson/internal/parallelfails.TestPassedWithLog (0.00s)
-    fails_test.go:15: this is a log
-
-::endgroup::
-::group::PASS testjson/internal/parallelfails.TestPassedWithStdout (0.00s)
-this is a Print
-
-::endgroup::
-::group::PASS testjson/internal/parallelfails.TestWithStderr (0.00s)
-this is stderr
-
-::endgroup::
-::group::FAIL testjson/internal/parallelfails.TestNestedParallelFailures/a (0.00s)
-    fails_test.go:50: failed sub a
-    --- FAIL: TestNestedParallelFailures/a (0.00s)
-
-::endgroup::
-::group::FAIL testjson/internal/parallelfails.TestNestedParallelFailures/d (0.00s)
-    fails_test.go:50: failed sub d
-    --- FAIL: TestNestedParallelFailures/d (0.00s)
-
-::endgroup::
-::group::FAIL testjson/internal/parallelfails.TestNestedParallelFailures/c (0.00s)
-    fails_test.go:50: failed sub c
-    --- FAIL: TestNestedParallelFailures/c (0.00s)
-
-::endgroup::
-::group::FAIL testjson/internal/parallelfails.TestNestedParallelFailures/b (0.00s)
-    fails_test.go:50: failed sub b
-    --- FAIL: TestNestedParallelFailures/b (0.00s)
-
-::endgroup::
-  FAIL testjson/internal/parallelfails.TestNestedParallelFailures (0.00s)
-::group::FAIL testjson/internal/parallelfails.TestParallelTheFirst (0.01s)
-    fails_test.go:29: failed the first
-
-::endgroup::
-::group::FAIL testjson/internal/parallelfails.TestParallelTheThird (0.00s)
-    fails_test.go:41: failed the third
-
-::endgroup::
-::group::FAIL testjson/internal/parallelfails.TestParallelTheSecond (0.01s)
-    fails_test.go:35: failed the second
-
-::endgroup::
-  FAIL Package testjson/internal/parallelfails (20ms)
-
-  PASS testjson/internal/withfails.TestPassed (0.00s)
-::group::PASS testjson/internal/withfails.TestPassedWithLog (0.00s)
-    fails_test.go:18: this is a log
-
-::endgroup::
-::group::PASS testjson/internal/withfails.TestPassedWithStdout (0.00s)
-this is a Print
-
-::endgroup::
-::group::SKIP testjson/internal/withfails.TestSkipped (0.00s)
-    fails_test.go:26: 
-
-::endgroup::
-::group::SKIP testjson/internal/withfails.TestSkippedWitLog (0.00s)
-    fails_test.go:30: the skip message
-
-::endgroup::
-::group::FAIL testjson/internal/withfails.TestFailed (0.00s)
-    fails_test.go:34: this failed
-
-::endgroup::
-::group::PASS testjson/internal/withfails.TestWithStderr (0.00s)
-this is stderr
-
-::endgroup::
-::group::FAIL testjson/internal/withfails.TestFailedWithStderr (0.00s)
-this is stderr
-    fails_test.go:43: also failed
-
-::endgroup::
-::group::PASS testjson/internal/withfails.TestNestedWithFailure/a/sub (0.00s)
-        --- PASS: TestNestedWithFailure/a/sub (0.00s)
-
-::endgroup::
-::group::PASS testjson/internal/withfails.TestNestedWithFailure/a (0.00s)
-    --- PASS: TestNestedWithFailure/a (0.00s)
-
-::endgroup::
-::group::PASS testjson/internal/withfails.TestNestedWithFailure/b/sub (0.00s)
-        --- PASS: TestNestedWithFailure/b/sub (0.00s)
-
-::endgroup::
-::group::PASS testjson/internal/withfails.TestNestedWithFailure/b (0.00s)
-    --- PASS: TestNestedWithFailure/b (0.00s)
-
-::endgroup::
-::group::FAIL testjson/internal/withfails.TestNestedWithFailure/c (0.00s)
-    fails_test.go:65: failed
-    --- FAIL: TestNestedWithFailure/c (0.00s)
-
-::endgroup::
-::group::PASS testjson/internal/withfails.TestNestedWithFailure/d/sub (0.00s)
-        --- PASS: TestNestedWithFailure/d/sub (0.00s)
-
-::endgroup::
-::group::PASS testjson/internal/withfails.TestNestedWithFailure/d (0.00s)
-    --- PASS: TestNestedWithFailure/d (0.00s)
-
-::endgroup::
-  FAIL testjson/internal/withfails.TestNestedWithFailure (0.00s)
-::group::PASS testjson/internal/withfails.TestNestedSuccess/a/sub (0.00s)
-        --- PASS: TestNestedSuccess/a/sub (0.00s)
-
-::endgroup::
-::group::PASS testjson/internal/withfails.TestNestedSuccess/a (0.00s)
-    --- PASS: TestNestedSuccess/a (0.00s)
-
-::endgroup::
-::group::PASS testjson/internal/withfails.TestNestedSuccess/b/sub (0.00s)
-        --- PASS: TestNestedSuccess/b/sub (0.00s)
-
-::endgroup::
-::group::PASS testjson/internal/withfails.TestNestedSuccess/b (0.00s)
-    --- PASS: TestNestedSuccess/b (0.00s)
-
-::endgroup::
-::group::PASS testjson/internal/withfails.TestNestedSuccess/c/sub (0.00s)
-        --- PASS: TestNestedSuccess/c/sub (0.00s)
-
-::endgroup::
-::group::PASS testjson/internal/withfails.TestNestedSuccess/c (0.00s)
-    --- PASS: TestNestedSuccess/c (0.00s)
-
-::endgroup::
-::group::PASS testjson/internal/withfails.TestNestedSuccess/d/sub (0.00s)
-        --- PASS: TestNestedSuccess/d/sub (0.00s)
-
-::endgroup::
-::group::PASS testjson/internal/withfails.TestNestedSuccess/d (0.00s)
-    --- PASS: TestNestedSuccess/d (0.00s)
-
-::endgroup::
-  PASS testjson/internal/withfails.TestNestedSuccess (0.00s)
-::group::SKIP testjson/internal/withfails.TestTimeout (0.00s)
-    timeout_test.go:13: skipping slow test
-
-::endgroup::
-  PASS testjson/internal/withfails.TestParallelTheFirst (0.01s)
-  PASS testjson/internal/withfails.TestParallelTheThird (0.00s)
-  PASS testjson/internal/withfails.TestParallelTheSecond (0.01s)
-  FAIL Package testjson/internal/withfails (20ms)
-
diff --git a/testjson/testdata/format/testname-coverage.out b/testjson/testdata/format/testname-coverage.out
index 0cac2b4..4364ae4 100644
--- a/testjson/testdata/format/testname-coverage.out
+++ b/testjson/testdata/format/testname-coverage.out
@@ -3,8 +3,6 @@ FAIL gotestsum/testjson/internal/badmain
 PASS gotestsum/testjson/internal/good.TestPassed (0.00s)
 PASS gotestsum/testjson/internal/good.TestPassedWithLog (0.00s)
 PASS gotestsum/testjson/internal/good.TestPassedWithStdout (0.00s)
-SKIP gotestsum/testjson/internal/good.TestSkipped (0.00s)
-SKIP gotestsum/testjson/internal/good.TestSkippedWitLog (0.00s)
 PASS gotestsum/testjson/internal/good.TestWithStderr (0.00s)
 PASS gotestsum/testjson/internal/good.TestNestedSuccess/a/sub (0.00s)
 PASS gotestsum/testjson/internal/good.TestNestedSuccess/a (0.00s)
@@ -23,8 +21,6 @@ PASS gotestsum/testjson/internal/good (coverage: 0.0% of statements)
 PASS gotestsum/testjson/internal/stub.TestPassed (0.00s)
 PASS gotestsum/testjson/internal/stub.TestPassedWithLog (0.00s)
 PASS gotestsum/testjson/internal/stub.TestPassedWithStdout (0.00s)
-SKIP gotestsum/testjson/internal/stub.TestSkipped (0.00s)
-SKIP gotestsum/testjson/internal/stub.TestSkippedWitLog (0.00s)
 === RUN   TestFailed
 --- FAIL: TestFailed (0.00s)
     stub_test.go:34: this failed
diff --git a/testjson/testdata/format/testname-shuffle.out b/testjson/testdata/format/testname-shuffle.out
index 8d80b36..58a0233 100644
--- a/testjson/testdata/format/testname-shuffle.out
+++ b/testjson/testdata/format/testname-shuffle.out
@@ -1,7 +1,6 @@
 sometimes main can exit 2
 FAIL testjson/internal/badmain
 PASS testjson/internal/good.TestPassedWithLog (0.00s)
-SKIP testjson/internal/good.TestSkippedWitLog (0.00s)
 PASS testjson/internal/good.TestPassedWithStdout (0.00s)
 PASS testjson/internal/good.TestPassed (0.00s)
 PASS testjson/internal/good.TestNestedSuccess/a/sub (0.00s)
@@ -14,7 +13,6 @@ PASS testjson/internal/good.TestNestedSuccess/d/sub (0.00s)
 PASS testjson/internal/good.TestNestedSuccess/d (0.00s)
 PASS testjson/internal/good.TestNestedSuccess (0.00s)
 PASS testjson/internal/good.TestWithStderr (0.00s)
-SKIP testjson/internal/good.TestSkipped (0.00s)
 PASS testjson/internal/good.TestParallelTheSecond (0.01s)
 PASS testjson/internal/good.TestParallelTheFirst (0.01s)
 PASS testjson/internal/good.TestParallelTheThird (0.00s)
@@ -70,7 +68,6 @@ FAIL testjson/internal/parallelfails.TestParallelTheFirst (0.01s)
 FAIL testjson/internal/parallelfails.TestParallelTheThird (0.00s)
 FAIL testjson/internal/parallelfails (-test.shuffle 123456)
 PASS testjson/internal/withfails.TestPassedWithStdout (0.00s)
-SKIP testjson/internal/withfails.TestSkipped (0.00s)
 PASS testjson/internal/withfails.TestNestedWithFailure/a/sub (0.00s)
 PASS testjson/internal/withfails.TestNestedWithFailure/a (0.00s)
 PASS testjson/internal/withfails.TestNestedWithFailure/b/sub (0.00s)
@@ -86,7 +83,6 @@ PASS testjson/internal/withfails.TestNestedWithFailure/d (0.00s)
 FAIL testjson/internal/withfails.TestNestedWithFailure (0.00s)
 PASS testjson/internal/withfails.TestWithStderr (0.00s)
 PASS testjson/internal/withfails.TestPassed (0.00s)
-SKIP testjson/internal/withfails.TestSkippedWitLog (0.00s)
 PASS testjson/internal/withfails.TestNestedSuccess/a/sub (0.00s)
 PASS testjson/internal/withfails.TestNestedSuccess/a (0.00s)
 PASS testjson/internal/withfails.TestNestedSuccess/b/sub (0.00s)
@@ -97,7 +93,6 @@ PASS testjson/internal/withfails.TestNestedSuccess/d/sub (0.00s)
 PASS testjson/internal/withfails.TestNestedSuccess/d (0.00s)
 PASS testjson/internal/withfails.TestNestedSuccess (0.00s)
 PASS testjson/internal/withfails.TestPassedWithLog (0.00s)
-SKIP testjson/internal/withfails.TestTimeout (0.00s)
 === RUN   TestFailedWithStderr
 this is stderr
     fails_test.go:43: also failed
diff --git a/testjson/testdata/format/testname.out b/testjson/testdata/format/testname.out
index e39dcd4..5eaedca 100644
--- a/testjson/testdata/format/testname.out
+++ b/testjson/testdata/format/testname.out
@@ -4,8 +4,6 @@ EMPTY testjson/internal/empty (cached)
 PASS testjson/internal/good.TestPassed (0.00s)
 PASS testjson/internal/good.TestPassedWithLog (0.00s)
 PASS testjson/internal/good.TestPassedWithStdout (0.00s)
-SKIP testjson/internal/good.TestSkipped (0.00s)
-SKIP testjson/internal/good.TestSkippedWitLog (0.00s)
 PASS testjson/internal/good.TestWithStderr (0.00s)
 PASS testjson/internal/good.TestNestedSuccess/a/sub (0.00s)
 PASS testjson/internal/good.TestNestedSuccess/a (0.00s)
@@ -73,8 +71,6 @@ FAIL testjson/internal/parallelfails
 PASS testjson/internal/withfails.TestPassed (0.00s)
 PASS testjson/internal/withfails.TestPassedWithLog (0.00s)
 PASS testjson/internal/withfails.TestPassedWithStdout (0.00s)
-SKIP testjson/internal/withfails.TestSkipped (0.00s)
-SKIP testjson/internal/withfails.TestSkippedWitLog (0.00s)
 === RUN   TestFailed
     fails_test.go:34: this failed
 --- FAIL: TestFailed (0.00s)
@@ -107,7 +103,6 @@ PASS testjson/internal/withfails.TestNestedSuccess/c (0.00s)
 PASS testjson/internal/withfails.TestNestedSuccess/d/sub (0.00s)
 PASS testjson/internal/withfails.TestNestedSuccess/d (0.00s)
 PASS testjson/internal/withfails.TestNestedSuccess (0.00s)
-SKIP testjson/internal/withfails.TestTimeout (0.00s)
 PASS testjson/internal/withfails.TestParallelTheFirst (0.01s)
 PASS testjson/internal/withfails.TestParallelTheThird (0.00s)
 PASS testjson/internal/withfails.TestParallelTheSecond (0.01s)