diff --git a/README.md b/README.md index 463a082..5fcf5c9 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Pass | Fail By default, `tparse` will always return test failures and panics, if any, followed by a package-level summary table. -To get additional info on passed tests run `tparse` with `-pass` flag. Tests are grouped by package and sorted by elapsed time in descending order (longest to shortest). +To get additional info on passed tests run `tparse` with `-pass` flag. To display only failed tests, use the `-fail-only` flag. Tests are grouped by package and sorted by elapsed time in descending order (longest to shortest). ### [But why?!](#but-why) for more info. @@ -47,7 +47,7 @@ Tip: run `tparse -h` to get usage and options. `tparse` attempts to do just that; return failed tests and panics, if any, followed by a single package-level summary. No more searching for the literal string: "--- FAIL". -But, let's take it a bit further. With `-all` (`-pass` and `-skip` combined) you can get additional info, such as skipped tests and elapsed time of each passed test. +But, let's take it a bit further. With `-all` (`-pass` and `-skip` combined) you can get additional info, such as skipped tests and elapsed time of each passed test. For the opposite use case, use `-fail-only` to display only failed tests. `tparse` comes with a `-follow` flag to print raw output. Yep, go test pipes JSON, it's parsed and the output is printed back out as if you ran go test without `-json` flag. Eliminating the need for `tee /dev/tty` between pipes. diff --git a/internal/app/app.go b/internal/app/app.go index 852d077..b198601 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -134,7 +134,7 @@ func display(w io.Writer, summary *parse.GoTestSummary, option Options) { // Sort packages by name ASC. packages := summary.GetSortedPackages(option.Sorter) // Only print the tests table if either pass or skip is true. - if option.TestTableOptions.Pass || option.TestTableOptions.Skip { + if option.TestTableOptions.Pass || option.TestTableOptions.Skip || option.TestTableOptions.FailOnly { if option.Format == OutputFormatMarkdown { cw.testsTableMarkdown(packages, option.TestTableOptions) } else { diff --git a/internal/app/table_summary.go b/internal/app/table_summary.go index bb571c6..bc4b2f8 100644 --- a/internal/app/table_summary.go +++ b/internal/app/table_summary.go @@ -25,6 +25,9 @@ type SummaryTableOptions struct { // TrimPath is the path prefix to trim from the package name. TrimPath string + + // FailOnly will display only packages with failed tests. + FailOnly bool } func (c *consoleWriter) summaryTable( @@ -191,6 +194,9 @@ func (c *consoleWriter) summaryTable( return } for _, r := range passed { + if options.FailOnly && r.fail == "0" { + continue + } data.Append(r.toRow()) } @@ -203,7 +209,10 @@ func (c *consoleWriter) summaryTable( data.Append(r.toRow()) } } - + if options.FailOnly && data.Rows() == 0 { + fmt.Fprintln(c, "No tests failed.") + return + } fmt.Fprintln(c, tbl.Data(data).Render()) } diff --git a/internal/app/table_tests.go b/internal/app/table_tests.go index 54eb2e6..ec9a5ea 100644 --- a/internal/app/table_tests.go +++ b/internal/app/table_tests.go @@ -21,6 +21,9 @@ var ( type TestTableOptions struct { // Display passed or skipped tests. If both are true this is equivalent to all. Pass, Skip bool + // FailOnly will display only tests with failed status. + // If true, it takes precedence over Pass and Skip. + FailOnly bool // For narrow screens, trim long test identifiers vertically. Example: // TestNoVersioning/seed-up-down-to-zero // @@ -102,6 +105,9 @@ func (c *consoleWriter) testsTable(packages []*parse.Package, option TestTableOp data.Append(row.toRow()) } if i != (len(packages) - 1) { + if option.FailOnly && data.Rows() == 0 { + continue + } // Add a blank row between packages. data.Append(testRow{}.toRow()) } diff --git a/main.go b/main.go index fc3aad4..3eb3cf4 100644 --- a/main.go +++ b/main.go @@ -23,6 +23,7 @@ var ( allPtr = flag.Bool("all", false, "") passPtr = flag.Bool("pass", false, "") skipPtr = flag.Bool("skip", false, "") + failOnlyPtr = flag.Bool("fail-only", false, "") showNoTestsPtr = flag.Bool("notests", false, "") smallScreenPtr = flag.Bool("smallscreen", false, "") noColorPtr = flag.Bool("nocolor", false, "") @@ -54,6 +55,7 @@ Options: -all Display table event for pass and skip. (Failed items always displayed) -pass Display table for passed tests. -skip Display table for skipped tests. + -fail-only Display only failed tests. (overrides -pass, -skip and -all) -notests Display packages containing no test files or empty test files. -smallscreen Split subtest names vertically to fit on smaller screens. -slow Number of slowest tests to display. Default is 0, display all. @@ -120,7 +122,13 @@ func main() { return } - if *allPtr { + if *failOnlyPtr { + if *passPtr || *skipPtr || *allPtr { + fmt.Fprintln(os.Stdout, "warning: -fail-only takes precedence over -pass, -skip, and -all flags") + *passPtr = false + *skipPtr = false + } + } else if *allPtr { *passPtr = true *skipPtr = true } @@ -157,6 +165,7 @@ func main() { TestTableOptions: app.TestTableOptions{ Pass: *passPtr, Skip: *skipPtr, + FailOnly: *failOnlyPtr, Trim: *smallScreenPtr, TrimPath: *trimPathPtr, Slow: *slowPtr, @@ -164,6 +173,7 @@ func main() { SummaryTableOptions: app.SummaryTableOptions{ Trim: *smallScreenPtr, TrimPath: *trimPathPtr, + FailOnly: *failOnlyPtr, }, Format: format, Sorter: sorter, diff --git a/tests/failed_test.go b/tests/failed_test.go index 5ca8651..5d1072d 100644 --- a/tests/failed_test.go +++ b/tests/failed_test.go @@ -54,3 +54,45 @@ func TestFailedTestsTable(t *testing.T) { }) } } + +func TestFailOnly(t *testing.T) { + t.Parallel() + + base := filepath.Join("testdata", "fail-only") + + tt := []struct { + fileName string + exitCode int + }{ + {"test_01", 1}, + {"test_02", 0}, + } + + for _, tc := range tt { + t.Run(tc.fileName, func(t *testing.T) { + buf := bytes.NewBuffer(nil) + inputFile := filepath.Join(base, tc.fileName+".jsonl") + options := app.Options{ + FileName: inputFile, + Output: buf, + Sorter: parse.SortByPackageName, + TestTableOptions: app.TestTableOptions{ + FailOnly: true, + }, + SummaryTableOptions: app.SummaryTableOptions{ + FailOnly: true, + }, + } + gotExitCode, err := app.Run(options) + require.NoError(t, err) + assert.Equal(t, tc.exitCode, gotExitCode) + + goldenFile := filepath.Join(base, tc.fileName+".golden") + want, err := os.ReadFile(goldenFile) + if err != nil { + t.Fatal(err) + } + checkGolden(t, inputFile, goldenFile, buf.Bytes(), want) + }) + } +} diff --git a/tests/testdata/fail-only/test_01.golden b/tests/testdata/fail-only/test_01.golden new file mode 100644 index 0000000..d766499 --- /dev/null +++ b/tests/testdata/fail-only/test_01.golden @@ -0,0 +1,18 @@ +╭────────┬─────────┬──────────┬─────────────────────────────╮ +│ Status │ Elapsed │ Test │ Package │ +├────────┼─────────┼──────────┼─────────────────────────────┤ +│ FAIL │ 0.01 │ TestFail │ github.com/example/testpkg2 │ +╰────────┴─────────┴──────────┴─────────────────────────────╯ +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃ FAIL package: github.com/example/testpkg2 ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +--- FAIL: TestFail (0.01s) + + example_test.go:10: test failed + +╭────────┬─────────┬─────────────────────────────┬───────┬──────┬──────┬──────╮ +│ Status │ Elapsed │ Package │ Cover │ Pass │ Fail │ Skip │ +├────────┼─────────┼─────────────────────────────┼───────┼──────┼──────┼──────┤ +│ FAIL │ 0.03s │ github.com/example/testpkg2 │ -- │ 2 │ 1 │ 0 │ +╰────────┴─────────┴─────────────────────────────┴───────┴──────┴──────┴──────╯ diff --git a/tests/testdata/fail-only/test_01.jsonl b/tests/testdata/fail-only/test_01.jsonl new file mode 100644 index 0000000..2baac8d --- /dev/null +++ b/tests/testdata/fail-only/test_01.jsonl @@ -0,0 +1,27 @@ +{"Time":"2024-01-15T10:00:00Z","Action":"run","Package":"github.com/example/testpkg","Test":"TestPass1"} +{"Time":"2024-01-15T10:00:00.001Z","Action":"output","Package":"github.com/example/testpkg","Test":"TestPass1","Output":"=== RUN TestPass1\n"} +{"Time":"2024-01-15T10:00:00.010Z","Action":"output","Package":"github.com/example/testpkg","Test":"TestPass1","Output":"--- PASS: TestPass1 (0.01s)\n"} +{"Time":"2024-01-15T10:00:00.010Z","Action":"pass","Package":"github.com/example/testpkg","Test":"TestPass1","Elapsed":0.01} +{"Time":"2024-01-15T10:00:00.011Z","Action":"run","Package":"github.com/example/testpkg","Test":"TestPass2"} +{"Time":"2024-01-15T10:00:00.012Z","Action":"output","Package":"github.com/example/testpkg","Test":"TestPass2","Output":"=== RUN TestPass2\n"} +{"Time":"2024-01-15T10:00:00.020Z","Action":"output","Package":"github.com/example/testpkg","Test":"TestPass2","Output":"--- PASS: TestPass2 (0.01s)\n"} +{"Time":"2024-01-15T10:00:00.020Z","Action":"pass","Package":"github.com/example/testpkg","Test":"TestPass2","Elapsed":0.01} +{"Time":"2024-01-15T10:00:00.021Z","Action":"output","Package":"github.com/example/testpkg","Output":"PASS\n"} +{"Time":"2024-01-15T10:00:00.022Z","Action":"output","Package":"github.com/example/testpkg","Output":"ok \tgithub.com/example/testpkg\t0.022s\n"} +{"Time":"2024-01-15T10:00:00.022Z","Action":"pass","Package":"github.com/example/testpkg","Elapsed":0.022} +{"Time":"2024-01-15T10:00:00Z","Action":"run","Package":"github.com/example/testpkg2","Test":"TestPass1"} +{"Time":"2024-01-15T10:00:00.001Z","Action":"output","Package":"github.com/example/testpkg2","Test":"TestPass1","Output":"=== RUN TestPass1\n"} +{"Time":"2024-01-15T10:00:00.010Z","Action":"output","Package":"github.com/example/testpkg2","Test":"TestPass1","Output":"--- PASS: TestPass1 (0.01s)\n"} +{"Time":"2024-01-15T10:00:00.010Z","Action":"pass","Package":"github.com/example/testpkg2","Test":"TestPass1","Elapsed":0.01} +{"Time":"2024-01-15T10:00:00.011Z","Action":"run","Package":"github.com/example/testpkg2","Test":"TestPass2"} +{"Time":"2024-01-15T10:00:00.012Z","Action":"output","Package":"github.com/example/testpkg2","Test":"TestPass2","Output":"=== RUN TestPass2\n"} +{"Time":"2024-01-15T10:00:00.020Z","Action":"output","Package":"github.com/example/testpkg2","Test":"TestPass2","Output":"--- PASS: TestPass2 (0.01s)\n"} +{"Time":"2024-01-15T10:00:00.020Z","Action":"pass","Package":"github.com/example/testpkg2","Test":"TestPass2","Elapsed":0.01} +{"Time":"2024-01-15T10:00:00.021Z","Action":"run","Package":"github.com/example/testpkg2","Test":"TestFail"} +{"Time":"2024-01-15T10:00:00.022Z","Action":"output","Package":"github.com/example/testpkg2","Test":"TestFail","Output":"=== RUN TestFail\n"} +{"Time":"2024-01-15T10:00:00.023Z","Action":"output","Package":"github.com/example/testpkg2","Test":"TestFail","Output":" example_test.go:10: test failed\n"} +{"Time":"2024-01-15T10:00:00.030Z","Action":"output","Package":"github.com/example/testpkg2","Test":"TestFail","Output":"--- FAIL: TestFail (0.01s)\n"} +{"Time":"2024-01-15T10:00:00.030Z","Action":"fail","Package":"github.com/example/testpkg2","Test":"TestFail","Elapsed":0.01} +{"Time":"2024-01-15T10:00:00.031Z","Action":"output","Package":"github.com/example/testpkg2","Output":"FAIL\n"} +{"Time":"2024-01-15T10:00:00.032Z","Action":"output","Package":"github.com/example/testpkg2","Output":"FAIL\tgithub.com/example/testpkg2\t0.032s\n"} +{"Time":"2024-01-15T10:00:00.032Z","Action":"fail","Package":"github.com/example/testpkg2","Elapsed":0.032} diff --git a/tests/testdata/fail-only/test_02.golden b/tests/testdata/fail-only/test_02.golden new file mode 100644 index 0000000..ee83811 --- /dev/null +++ b/tests/testdata/fail-only/test_02.golden @@ -0,0 +1 @@ +No tests failed. diff --git a/tests/testdata/fail-only/test_02.jsonl b/tests/testdata/fail-only/test_02.jsonl new file mode 100644 index 0000000..ef8925c --- /dev/null +++ b/tests/testdata/fail-only/test_02.jsonl @@ -0,0 +1,22 @@ +{"Time":"2024-01-15T10:00:00Z","Action":"run","Package":"github.com/example/testpkg","Test":"TestPass1"} +{"Time":"2024-01-15T10:00:00.001Z","Action":"output","Package":"github.com/example/testpkg","Test":"TestPass1","Output":"=== RUN TestPass1\n"} +{"Time":"2024-01-15T10:00:00.010Z","Action":"output","Package":"github.com/example/testpkg","Test":"TestPass1","Output":"--- PASS: TestPass1 (0.01s)\n"} +{"Time":"2024-01-15T10:00:00.010Z","Action":"pass","Package":"github.com/example/testpkg","Test":"TestPass1","Elapsed":0.01} +{"Time":"2024-01-15T10:00:00.011Z","Action":"run","Package":"github.com/example/testpkg","Test":"TestPass2"} +{"Time":"2024-01-15T10:00:00.012Z","Action":"output","Package":"github.com/example/testpkg","Test":"TestPass2","Output":"=== RUN TestPass2\n"} +{"Time":"2024-01-15T10:00:00.020Z","Action":"output","Package":"github.com/example/testpkg","Test":"TestPass2","Output":"--- PASS: TestPass2 (0.01s)\n"} +{"Time":"2024-01-15T10:00:00.020Z","Action":"pass","Package":"github.com/example/testpkg","Test":"TestPass2","Elapsed":0.01} +{"Time":"2024-01-15T10:00:00.021Z","Action":"output","Package":"github.com/example/testpkg","Output":"PASS\n"} +{"Time":"2024-01-15T10:00:00.022Z","Action":"output","Package":"github.com/example/testpkg","Output":"ok \tgithub.com/example/testpkg\t0.022s\n"} +{"Time":"2024-01-15T10:00:00.022Z","Action":"pass","Package":"github.com/example/testpkg","Elapsed":0.022} +{"Time":"2024-01-15T10:00:00Z","Action":"run","Package":"github.com/example/testpkg2","Test":"TestPass1"} +{"Time":"2024-01-15T10:00:00.001Z","Action":"output","Package":"github.com/example/testpkg2","Test":"TestPass1","Output":"=== RUN TestPass1\n"} +{"Time":"2024-01-15T10:00:00.010Z","Action":"output","Package":"github.com/example/testpkg2","Test":"TestPass1","Output":"--- PASS: TestPass1 (0.01s)\n"} +{"Time":"2024-01-15T10:00:00.010Z","Action":"pass","Package":"github.com/example/testpkg2","Test":"TestPass1","Elapsed":0.01} +{"Time":"2024-01-15T10:00:00.011Z","Action":"run","Package":"github.com/example/testpkg2","Test":"TestPass2"} +{"Time":"2024-01-15T10:00:00.012Z","Action":"output","Package":"github.com/example/testpkg2","Test":"TestPass2","Output":"=== RUN TestPass2\n"} +{"Time":"2024-01-15T10:00:00.020Z","Action":"output","Package":"github.com/example/testpkg2","Test":"TestPass2","Output":"--- PASS: TestPass2 (0.01s)\n"} +{"Time":"2024-01-15T10:00:00.020Z","Action":"pass","Package":"github.com/example/testpkg2","Test":"TestPass2","Elapsed":0.01} +{"Time":"2024-01-15T10:00:00.021Z","Action":"output","Package":"github.com/example/testpkg2","Output":"PASS\n"} +{"Time":"2024-01-15T10:00:00.022Z","Action":"output","Package":"github.com/example/testpkg2","Output":"ok \tgithub.com/example/testpkg2\t0.022s\n"} +{"Time":"2024-01-15T10:00:00.022Z","Action":"pass","Package":"github.com/example/testpkg2","Elapsed":0.022}