├── tests ├── testdata │ ├── follow │ │ └── test_01.golden │ ├── follow-verbose │ │ ├── test_04.golden │ │ ├── test_06.golden │ │ ├── test_05.golden │ │ ├── test_06.jsonl │ │ ├── test_04.jsonl │ │ ├── test_01.golden │ │ ├── test_02.golden │ │ ├── test_05.jsonl │ │ └── test_01.jsonl │ ├── outcome │ │ ├── test_07.jsonl │ │ ├── test_08.jsonl │ │ ├── test_06.jsonl │ │ ├── test_01.jsonl │ │ ├── test_05.jsonl │ │ ├── test_04.jsonl │ │ └── test_02.jsonl │ ├── prescan │ │ ├── test_04.txt │ │ ├── test_03.txt │ │ ├── test_01.txt │ │ └── test_02.txt │ ├── elapsed_test.jsonl │ ├── failed │ │ ├── test_02.golden │ │ ├── test_04.golden │ │ ├── test_02.jsonl │ │ ├── test_01.golden │ │ └── test_04.jsonl │ ├── race │ │ ├── test_06.jsonl │ │ ├── test_03.jsonl │ │ ├── test_01.jsonl │ │ └── test_04.jsonl │ └── panic │ │ ├── test_04.jsonl │ │ ├── test_03.jsonl │ │ └── test_05.jsonl ├── prescan_test.go ├── failed_test.go ├── cached_test.go ├── cover_test.go ├── package_start_test.go ├── panic_test.go ├── race_test.go ├── follow_test.go ├── summary_counts_test.go ├── outcome_test.go └── sort_test.go ├── .github └── workflows │ ├── lint.yaml │ ├── release.yaml │ └── ci.yaml ├── go.mod ├── .goreleaser.yaml ├── LICENSE ├── parse ├── process_options.go ├── package_slice.go ├── test.go └── package.go ├── internal ├── app │ ├── table.go │ ├── console_writer.go │ ├── app.go │ ├── table_failed.go │ ├── table_tests.go │ └── table_summary.go └── utils │ ├── utils.go │ └── utils_test.go ├── scripts └── release-notes.sh ├── Makefile ├── .golangci.yaml ├── README.md ├── go.sum ├── CHANGELOG.md └── main.go /tests/testdata/follow/test_01.golden: -------------------------------------------------------------------------------- 1 | fmt_test.go:1457: skipping; GOMAXPROCS>1 2 | ok fmt 0.143s 3 | -------------------------------------------------------------------------------- /tests/testdata/follow-verbose/test_04.golden: -------------------------------------------------------------------------------- 1 | === RUN TestFollow 2 | --- PASS: TestFollow (0.00s) 3 | testing: warning: no tests to run 4 | PASS 5 | ok github.com/mfridman/tparse/tests (cached) [no tests to run] 6 | -------------------------------------------------------------------------------- /tests/testdata/outcome/test_07.jsonl: -------------------------------------------------------------------------------- 1 | {"Time":"2018-10-20T13:32:27.452736-04:00","Action":"output","Package":"debug/errorcause","Output":"? \tdebug/errorcause\t[no test files]\n"} 2 | {"Time":"2018-10-20T13:32:27.452995-04:00","Action":"skip","Package":"debug/errorcause","Elapsed":0} -------------------------------------------------------------------------------- /tests/testdata/follow-verbose/test_06.golden: -------------------------------------------------------------------------------- 1 | # github.com/marco-m/tparse-bugs [github.com/marco-m/tparse-bugs.test] 2 | ./a_test.go:6:2: undefined: hello 3 | FAIL github.com/marco-m/tparse-bugs [build failed] 4 | === RUN TestB 5 | --- PASS: TestB (0.00s) 6 | PASS 7 | ok github.com/marco-m/tparse-bugs/b 0.098s 8 | -------------------------------------------------------------------------------- /tests/testdata/outcome/test_08.jsonl: -------------------------------------------------------------------------------- 1 | {"Time":"2018-10-27T23:15:21.909874-04:00","Action":"output","Package":"github.com/awesome/pkg","Output":"PASS\n"} 2 | {"Time":"2018-10-27T23:15:21.914955-04:00","Action":"output","Package":"github.com/awesome/pkg","Output":"ok \tgithub.com/awesome/pkg\t4.543s [no tests to run]\n"} 3 | {"Time":"2018-10-27T23:15:21.914992-04:00","Action":"pass","Package":"github.com/awesome/pkg","Elapsed":4.543} -------------------------------------------------------------------------------- /tests/testdata/outcome/test_06.jsonl: -------------------------------------------------------------------------------- 1 | {"Time":"2018-10-17T22:01:48.798229-04:00","Action":"output","Package":"fmt","Output":"testing: warning: no tests to run\n"} 2 | {"Time":"2018-10-17T22:01:48.798553-04:00","Action":"output","Package":"fmt","Output":"PASS\n"} 3 | {"Time":"2018-10-17T22:01:48.798617-04:00","Action":"output","Package":"fmt","Output":"ok \tfmt\t0.011s [no tests to run]\n"} 4 | {"Time":"2018-10-17T22:01:48.798647-04:00","Action":"pass","Package":"fmt","Elapsed":0.011} 5 | -------------------------------------------------------------------------------- /tests/testdata/follow-verbose/test_05.golden: -------------------------------------------------------------------------------- 1 | It 2 | is 3 | a 4 | long 5 | established 6 | fact 7 | that 8 | a 9 | reader 10 | will 11 | be 12 | distracted 13 | by 14 | the 15 | readable 16 | content 17 | of 18 | a 19 | page 20 | when 21 | looking 22 | at 23 | its 24 | layout. 25 | The 26 | point 27 | of 28 | using 29 | Lorem 30 | Ipsum 31 | is 32 | that 33 | it 34 | has 35 | a 36 | more-or-less 37 | normal 38 | distribution 39 | of 40 | letters, 41 | as 42 | opposed 43 | to 44 | using 45 | 'Content 46 | here, 47 | content 48 | here', 49 | making 50 | it 51 | -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | name: golangci 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.ref }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | golangci: 15 | name: lint 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout code 19 | uses: actions/checkout@v5 20 | - uses: actions/setup-go@v5 21 | with: 22 | go-version: 'stable' 23 | - name: golangci-lint 24 | uses: golangci/golangci-lint-action@v7 25 | with: 26 | version: latest 27 | github-token: ${{ secrets.GITHUB_TOKEN }} 28 | args: --timeout=2m --verbose 29 | annotations: false 30 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | on: 3 | push: 4 | tags: 5 | - '*' 6 | permissions: 7 | contents: write 8 | jobs: 9 | goreleaser: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v5 13 | with: 14 | fetch-depth: 0 15 | - run: git fetch --force --tags 16 | - uses: actions/setup-go@v5 17 | with: 18 | go-version: 'stable' 19 | - name: Generate release notes 20 | continue-on-error: true 21 | run: ./scripts/release-notes.sh ${{github.ref_name}} > ${{runner.temp}}/release_notes.txt 22 | - name: Run GoReleaser 23 | uses: goreleaser/goreleaser-action@v6 24 | with: 25 | distribution: goreleaser 26 | version: "~> v2" 27 | args: release --clean --release-notes=${{runner.temp}}/release_notes.txt 28 | env: 29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | -------------------------------------------------------------------------------- /tests/testdata/prescan/test_04.txt: -------------------------------------------------------------------------------- 1 | Previous write at 0x00c000090090 by goroutine 6: 2 | debug/tparse-24.TestRace() 3 | /Users/michael.fridman/go/src/debug/tparse-24/some_test.go:8 +0x88 4 | testing.tRunner() 5 | /usr/local/go/src/testing/testing.go:827 +0x162 6 | 7 | Goroutine 7 (running) created at: 8 | debug/tparse-24.TestRace() 9 | /Users/michael.fridman/go/src/debug/tparse-24/some_test.go:7 +0x7a 10 | testing.tRunner() 11 | /usr/local/go/src/testing/testing.go:827 +0x162 12 | 13 | Goroutine 6 (running) created at: 14 | testing.(*T).Run() 15 | /usr/local/go/src/testing/testing.go:878 +0x650 16 | testing.runTests.func1() 17 | /usr/local/go/src/testing/testing.go:1119 +0xa8 18 | testing.tRunner() 19 | /usr/local/go/src/testing/testing.go:827 +0x162 20 | testing.runTests() 21 | /usr/local/go/src/testing/testing.go:1117 +0x4ee 22 | testing.(*M).Run() 23 | /usr/local/go/src/testing/testing.go:1034 +0x2ee 24 | main.main() 25 | _testmain.go:42 +0x221 26 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mfridman/tparse 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/charmbracelet/lipgloss v1.1.0 7 | github.com/muesli/termenv v0.16.0 8 | github.com/stretchr/testify v1.11.1 9 | golang.org/x/term v0.32.0 10 | ) 11 | 12 | require ( 13 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 14 | github.com/charmbracelet/colorprofile v0.3.1 // indirect 15 | github.com/charmbracelet/x/ansi v0.9.2 // indirect 16 | github.com/charmbracelet/x/cellbuf v0.0.13 // indirect 17 | github.com/charmbracelet/x/term v0.2.1 // indirect 18 | github.com/davecgh/go-spew v1.1.1 // indirect 19 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 20 | github.com/mattn/go-isatty v0.0.20 // indirect 21 | github.com/mattn/go-runewidth v0.0.16 // indirect 22 | github.com/pmezard/go-difflib v1.0.0 // indirect 23 | github.com/rivo/uniseg v0.4.7 // indirect 24 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect 25 | golang.org/x/sys v0.33.0 // indirect 26 | gopkg.in/yaml.v3 v3.0.1 // indirect 27 | ) 28 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://goreleaser.com/static/schema.json 2 | # 3 | # See https://goreleaser.com/customization/ for more information. 4 | version: 2 5 | project_name: tparse 6 | 7 | before: 8 | hooks: 9 | - go mod tidy 10 | builds: 11 | - env: 12 | - CGO_ENABLED=0 13 | binary: tparse 14 | main: main.go 15 | goos: 16 | - linux 17 | - darwin 18 | # - windows 19 | goarch: 20 | - amd64 21 | - arm64 22 | ldflags: 23 | # The v prefix is stripped by goreleaser, so we need to add it back. 24 | # https://goreleaser.com/customization/templates/#fnref:version-prefix 25 | - "-s -w -X main.version=v{{ .Version }}" 26 | 27 | archives: 28 | - format: binary 29 | name_template: >- 30 | {{ .ProjectName }}_{{- tolower .Os }}_{{- if eq .Arch "amd64" }}x86_64{{- else }}{{ .Arch }}{{ end }} 31 | checksum: 32 | name_template: "checksums.txt" 33 | snapshot: 34 | name_template: "{{ incpatch .Version }}-next" 35 | changelog: 36 | use: github-native 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Michael Fridman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/testdata/follow-verbose/test_06.jsonl: -------------------------------------------------------------------------------- 1 | # github.com/marco-m/tparse-bugs [github.com/marco-m/tparse-bugs.test] 2 | ./a_test.go:6:2: undefined: hello 3 | FAIL github.com/marco-m/tparse-bugs [build failed] 4 | {"Time":"2022-05-25T23:11:20.775252-04:00","Action":"run","Package":"github.com/marco-m/tparse-bugs/b","Test":"TestB"} 5 | {"Time":"2022-05-25T23:11:20.775449-04:00","Action":"output","Package":"github.com/marco-m/tparse-bugs/b","Test":"TestB","Output":"=== RUN TestB\n"} 6 | {"Time":"2022-05-25T23:11:20.775462-04:00","Action":"output","Package":"github.com/marco-m/tparse-bugs/b","Test":"TestB","Output":"--- PASS: TestB (0.00s)\n"} 7 | {"Time":"2022-05-25T23:11:20.775464-04:00","Action":"pass","Package":"github.com/marco-m/tparse-bugs/b","Test":"TestB","Elapsed":0} 8 | {"Time":"2022-05-25T23:11:20.775467-04:00","Action":"output","Package":"github.com/marco-m/tparse-bugs/b","Output":"PASS\n"} 9 | {"Time":"2022-05-25T23:11:20.7755-04:00","Action":"output","Package":"github.com/marco-m/tparse-bugs/b","Output":"ok \tgithub.com/marco-m/tparse-bugs/b\t0.098s\n"} 10 | {"Time":"2022-05-25T23:11:20.77551-04:00","Action":"pass","Package":"github.com/marco-m/tparse-bugs/b","Elapsed":0.098} -------------------------------------------------------------------------------- /parse/process_options.go: -------------------------------------------------------------------------------- 1 | package parse 2 | 3 | import ( 4 | "io" 5 | ) 6 | 7 | type progressWriter interface { 8 | io.Writer 9 | FormatAction(Action) string 10 | } 11 | 12 | type options struct { 13 | w io.Writer 14 | follow bool 15 | followVerbose bool 16 | debug bool 17 | 18 | progress bool 19 | progressOutput progressWriter 20 | 21 | includeTimestamp bool 22 | } 23 | 24 | type OptionsFunc func(o *options) 25 | 26 | func WithFollowOutput(b bool) OptionsFunc { 27 | return func(o *options) { o.follow = b } 28 | } 29 | 30 | func WithFollowVersboseOutput(b bool) OptionsFunc { 31 | return func(o *options) { o.followVerbose = b } 32 | } 33 | 34 | func WithWriter(w io.Writer) OptionsFunc { 35 | return func(o *options) { o.w = w } 36 | } 37 | 38 | func WithDebug() OptionsFunc { 39 | return func(o *options) { o.debug = true } 40 | } 41 | 42 | func WithProgress(b bool) OptionsFunc { 43 | return func(o *options) { o.progress = b } 44 | } 45 | 46 | func WithProgressOutput(w progressWriter) OptionsFunc { 47 | return func(o *options) { o.progressOutput = w } 48 | } 49 | 50 | func WithIncludeTimestamp(b bool) OptionsFunc { 51 | return func(o *options) { o.includeTimestamp = b } 52 | } 53 | -------------------------------------------------------------------------------- /tests/testdata/follow-verbose/test_04.jsonl: -------------------------------------------------------------------------------- 1 | {"Time":"2022-05-15T22:44:38.670279-04:00","Action":"run","Package":"github.com/mfridman/tparse/tests","Test":"TestFollow"} 2 | {"Time":"2022-05-15T22:44:38.670387-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestFollow","Output":"=== RUN TestFollow\n"} 3 | {"Time":"2022-05-15T22:44:38.670399-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestFollow","Output":"--- PASS: TestFollow (0.00s)\n"} 4 | {"Time":"2022-05-15T22:44:38.670403-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestFollow","Output":"testing: warning: no tests to run\n"} 5 | {"Time":"2022-05-15T22:44:38.670406-04:00","Action":"pass","Package":"github.com/mfridman/tparse/tests","Test":"TestFollow","Elapsed":0} 6 | {"Time":"2022-05-15T22:44:38.67041-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Output":"PASS\n"} 7 | {"Time":"2022-05-15T22:44:38.670413-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Output":"ok \tgithub.com/mfridman/tparse/tests\t(cached) [no tests to run]\n"} 8 | {"Time":"2022-05-15T22:44:38.67042-04:00","Action":"pass","Package":"github.com/mfridman/tparse/tests","Elapsed":0} 9 | -------------------------------------------------------------------------------- /tests/prescan_test.go: -------------------------------------------------------------------------------- 1 | package parsetest 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | 10 | "github.com/mfridman/tparse/parse" 11 | ) 12 | 13 | func TestPrescan(t *testing.T) { 14 | t.Parallel() 15 | 16 | base := filepath.Join("testdata", "prescan") 17 | 18 | tt := []struct { 19 | fileName string 20 | desc string 21 | err error 22 | }{ 23 | {"test_01.txt", "want err", nil}, 24 | {"test_02.txt", "want failure after reading >50 lines of non-parsable events", parse.ErrNotParsable}, 25 | // logic: unparsable event(s), good event(s), at least one event = fail. 26 | // Once we get a good event, we expect only good events to follow until EOF. 27 | {"test_03.txt", "want failure when stream contains a bad event(s) -> good event(s) -> bad event", parse.ErrNotParsable}, 28 | {"test_04.txt", "want failure reading <50 lines of non-parsable events", parse.ErrNotParsable}, 29 | } 30 | 31 | for _, tc := range tt { 32 | t.Run(tc.fileName, func(t *testing.T) { 33 | inputFile := filepath.Join(base, tc.fileName) 34 | f, err := os.Open(inputFile) 35 | require.NoError(t, err) 36 | 37 | _, err = parse.Process(f) 38 | if tc.err != nil { 39 | require.ErrorIs(t, err, tc.err) 40 | return 41 | } 42 | require.NoError(t, err) 43 | }) 44 | 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /internal/app/table.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "github.com/charmbracelet/lipgloss" 5 | "github.com/charmbracelet/lipgloss/table" 6 | ) 7 | 8 | func newTable( 9 | format OutputFormat, 10 | override func(style lipgloss.Style, row, col int) lipgloss.Style, 11 | ) *table.Table { 12 | tbl := table.New() 13 | switch format { 14 | case OutputFormatPlain: 15 | tbl.Border(lipgloss.HiddenBorder()).BorderTop(false).BorderBottom(false) 16 | case OutputFormatMarkdown: 17 | tbl.Border(markdownBorder).BorderBottom(false).BorderTop(false) 18 | case OutputFormatBasic: 19 | tbl.Border(lipgloss.RoundedBorder()) 20 | } 21 | return tbl.StyleFunc(func(row, col int) lipgloss.Style { 22 | // Default style, may be overridden. 23 | style := lipgloss.NewStyle().PaddingLeft(1).PaddingRight(1).Align(lipgloss.Center) 24 | if override != nil { 25 | style = override(style, row, col) 26 | } 27 | return style 28 | }) 29 | } 30 | 31 | var markdownBorder = lipgloss.Border{ 32 | Top: "-", 33 | Bottom: "-", 34 | Left: "|", 35 | Right: "|", 36 | TopLeft: "", // empty for markdown 37 | TopRight: "", // empty for markdown 38 | BottomLeft: "", // empty for markdown 39 | BottomRight: "", // empty for markdown 40 | MiddleLeft: "|", 41 | MiddleRight: "|", 42 | Middle: "|", 43 | MiddleTop: "|", 44 | MiddleBottom: "|", 45 | } 46 | -------------------------------------------------------------------------------- /parse/package_slice.go: -------------------------------------------------------------------------------- 1 | package parse 2 | 3 | import ( 4 | "sort" 5 | ) 6 | 7 | type PackageSorter func([]*Package) sort.Interface 8 | type PackageSlice []*Package 9 | 10 | type byCoverage struct{ PackageSlice } 11 | type byElapsed struct{ PackageSlice } 12 | 13 | // SortByPackageName sorts packages in ascending alphabetical order. 14 | func SortByPackageName(packages []*Package) sort.Interface { return PackageSlice(packages) } 15 | func (packages PackageSlice) Len() int { return len(packages) } 16 | func (packages PackageSlice) Swap(i, j int) { 17 | packages[i], packages[j] = packages[j], packages[i] 18 | } 19 | func (packages PackageSlice) Less(i, j int) bool { 20 | return packages[i].Summary.Package < packages[j].Summary.Package 21 | } 22 | 23 | // SortByCoverage sorts packages in descending order of code coverage. 24 | func SortByCoverage(packages []*Package) sort.Interface { return byCoverage{packages} } 25 | func (packages byCoverage) Less(i, j int) bool { 26 | return packages.PackageSlice[i].Coverage > packages.PackageSlice[j].Coverage 27 | } 28 | 29 | // SortByElapsed sorts packages in descending order of elapsed time per package. 30 | func SortByElapsed(packages []*Package) sort.Interface { return byElapsed{packages} } 31 | func (packages byElapsed) Less(i, j int) bool { 32 | return packages.PackageSlice[i].Summary.Elapsed > packages.PackageSlice[j].Summary.Elapsed 33 | } 34 | -------------------------------------------------------------------------------- /parse/test.go: -------------------------------------------------------------------------------- 1 | package parse 2 | 3 | import ( 4 | "sort" 5 | ) 6 | 7 | // Test represents a single, unique, package test. 8 | type Test struct { 9 | Name string 10 | Package string 11 | Events []*Event 12 | } 13 | 14 | // Elapsed indicates how long a given test ran (in seconds), by scanning for the largest 15 | // elapsed value from all events. 16 | func (t *Test) Elapsed() float64 { 17 | var f float64 18 | for _, e := range t.Events { 19 | if e.Elapsed > f { 20 | f = e.Elapsed 21 | } 22 | } 23 | return f 24 | } 25 | 26 | // Status reports the outcome of the test represented as a single Action: pass, fail or skip. 27 | func (t *Test) Status() Action { 28 | // sort by time and scan for an action in reverse order. 29 | // The first action we come across (in reverse order) is 30 | // the outcome of the test, which will be one of pass|fail|skip. 31 | t.SortEvents() 32 | for i := len(t.Events) - 1; i >= 0; i-- { 33 | switch t.Events[i].Action { 34 | case ActionPass: 35 | return ActionPass 36 | case ActionSkip: 37 | return ActionSkip 38 | case ActionFail: 39 | return ActionFail 40 | } 41 | } 42 | return ActionFail 43 | } 44 | 45 | // SortEvents sorts test events by elapsed time in ascending order, i.e., oldest to newest. 46 | func (t *Test) SortEvents() { 47 | sort.Slice(t.Events, func(i, j int) bool { 48 | return t.Events[i].Time.Before(t.Events[j].Time) 49 | }) 50 | } 51 | -------------------------------------------------------------------------------- /tests/testdata/elapsed_test.jsonl: -------------------------------------------------------------------------------- 1 | {"Time":"2018-11-24T21:54:39.153656-05:00","Action":"run","Package":"strings","Test":"TestCompareStrings"} 2 | {"Time":"2018-11-24T21:54:39.154006-05:00","Action":"output","Package":"strings","Test":"TestCompareStrings","Output":"=== RUN TestCompareStrings\n"} 3 | {"Time":"2018-11-24T21:54:42.644837-05:00","Action":"output","Package":"strings","Test":"TestCompareStrings","Output":"--- PASS: TestCompareStrings (3.49s)\n"} 4 | {"Time":"2018-11-24T21:54:42.644875-05:00","Action":"pass","Package":"strings","Test":"TestCompareStrings","Elapsed":3.49} 5 | {"Time":"2018-11-24T21:54:42.644896-05:00","Action":"run","Package":"strings","Test":"TestCaseConsistency"} 6 | {"Time":"2018-11-24T21:54:42.644906-05:00","Action":"output","Package":"strings","Test":"TestCaseConsistency","Output":"=== RUN TestCaseConsistency\n"} 7 | {"Time":"2018-11-24T21:54:42.818776-05:00","Action":"output","Package":"strings","Test":"TestCaseConsistency","Output":"--- PASS: TestCaseConsistency (0.17s)\n"} 8 | {"Time":"2018-11-24T21:54:42.818824-05:00","Action":"pass","Package":"strings","Test":"TestCaseConsistency","Elapsed":0.17} 9 | {"Time":"2018-11-24T21:54:42.818878-05:00","Action":"output","Package":"strings","Output":"PASS\n"} 10 | {"Time":"2018-11-24T21:54:42.824353-05:00","Action":"output","Package":"strings","Output":"ok \tstrings\t3.775s\n"} 11 | {"Time":"2018-11-24T21:54:42.824402-05:00","Action":"pass","Package":"strings","Elapsed":3.775} -------------------------------------------------------------------------------- /tests/failed_test.go: -------------------------------------------------------------------------------- 1 | package parsetest 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | 12 | "github.com/mfridman/tparse/internal/app" 13 | "github.com/mfridman/tparse/parse" 14 | ) 15 | 16 | func TestFailedTestsTable(t *testing.T) { 17 | t.Parallel() 18 | 19 | base := filepath.Join("testdata", "failed") 20 | 21 | tt := []struct { 22 | fileName string 23 | exitCode int 24 | }{ 25 | {"test_01", 1}, 26 | {"test_02", 1}, 27 | {"test_03", 1}, 28 | {"test_04", 1}, 29 | } 30 | 31 | for _, tc := range tt { 32 | t.Run(tc.fileName, func(t *testing.T) { 33 | buf := bytes.NewBuffer(nil) 34 | inputFile := filepath.Join(base, tc.fileName+".jsonl") 35 | options := app.Options{ 36 | FileName: inputFile, 37 | Output: buf, 38 | Sorter: parse.SortByPackageName, 39 | TestTableOptions: app.TestTableOptions{ 40 | Pass: true, // Enable test table output 41 | Skip: true, // Also show skipped tests 42 | }, 43 | } 44 | gotExitCode, err := app.Run(options) 45 | require.NoError(t, err) 46 | assert.Equal(t, tc.exitCode, gotExitCode) 47 | 48 | goldenFile := filepath.Join(base, tc.fileName+".golden") 49 | want, err := os.ReadFile(goldenFile) 50 | if err != nil { 51 | t.Fatal(err) 52 | } 53 | checkGolden(t, inputFile, goldenFile, buf.Bytes(), want) 54 | }) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/testdata/follow-verbose/test_01.golden: -------------------------------------------------------------------------------- 1 | === RUN TestRace 2 | ================== 3 | WARNING: DATA RACE 4 | Write at 0x00c000090090 by goroutine 7: 5 | debug/tparse-24.TestRace.func1() 6 | /Users/michael.fridman/go/src/debug/tparse-24/some_test.go:7 +0x38 7 | 8 | Previous write at 0x00c000090090 by goroutine 6: 9 | debug/tparse-24.TestRace() 10 | /Users/michael.fridman/go/src/debug/tparse-24/some_test.go:8 +0x88 11 | testing.tRunner() 12 | /usr/local/go/src/testing/testing.go:827 +0x162 13 | 14 | Goroutine 7 (running) created at: 15 | debug/tparse-24.TestRace() 16 | /Users/michael.fridman/go/src/debug/tparse-24/some_test.go:7 +0x7a 17 | testing.tRunner() 18 | /usr/local/go/src/testing/testing.go:827 +0x162 19 | 20 | Goroutine 6 (running) created at: 21 | testing.(*T).Run() 22 | /usr/local/go/src/testing/testing.go:878 +0x650 23 | testing.runTests.func1() 24 | /usr/local/go/src/testing/testing.go:1119 +0xa8 25 | testing.tRunner() 26 | /usr/local/go/src/testing/testing.go:827 +0x162 27 | testing.runTests() 28 | /usr/local/go/src/testing/testing.go:1117 +0x4ee 29 | testing.(*M).Run() 30 | /usr/local/go/src/testing/testing.go:1034 +0x2ee 31 | main.main() 32 | _testmain.go:42 +0x221 33 | ================== 34 | --- FAIL: TestRace (0.00s) 35 | some_test.go:9: 64 36 | testing.go:771: race detected during execution of test 37 | FAIL 38 | exit status 1 39 | FAIL debug/tparse-24 0.020s 40 | -------------------------------------------------------------------------------- /internal/utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "io" 5 | "sort" 6 | "strings" 7 | ) 8 | 9 | // FindLongestCommonPrefix finds the longest common path prefix of a set of paths. For example, 10 | // given the following: 11 | // 12 | // github.com/owner/repo/cmd/foo 13 | // github.com/owner/repo/cmd/bar 14 | // 15 | // The longest common prefix is: github.com/owner/repo/cmd/ (note the trailing slash is included). 16 | func FindLongestCommonPrefix(paths []string) string { 17 | if len(paths) < 2 { 18 | return "" 19 | } 20 | // Sort the paths to optimize comparison. 21 | sort.Strings(paths) 22 | 23 | first, last := paths[0], paths[len(paths)-1] 24 | if first == last { 25 | return first 26 | } 27 | 28 | // Find the common prefix between the first and last sorted paths. 29 | commonPrefixLength := 0 30 | minLength := min(len(first), len(last)) 31 | for commonPrefixLength < minLength && first[commonPrefixLength] == last[commonPrefixLength] { 32 | commonPrefixLength++ 33 | } 34 | 35 | // Ensure the common prefix ends at a boundary. 36 | commonPrefix := first[:commonPrefixLength] 37 | if n := strings.LastIndex(commonPrefix, "/"); n != -1 { 38 | return commonPrefix[:n+1] 39 | } 40 | return "" 41 | } 42 | 43 | // DiscardCloser is an io.Writer that implements io.Closer by doing nothing. 44 | // 45 | // https://github.com/golang/go/issues/22823 46 | type WriteNopCloser struct { 47 | io.Writer 48 | } 49 | 50 | func (WriteNopCloser) Close() error { 51 | return nil 52 | } 53 | -------------------------------------------------------------------------------- /tests/testdata/failed/test_02.golden: -------------------------------------------------------------------------------- 1 | ╭────────┬─────────┬──────────────┬──────────────────────────────────────╮ 2 | │ Status │ Elapsed │ Test │ Package │ 3 | ├────────┼─────────┼──────────────┼──────────────────────────────────────┤ 4 | │ FAIL │ 1.00 │ TestWhatever │ github.com/mfridman/debug-go/testing │ 5 | ╰────────┴─────────┴──────────────┴──────────────────────────────────────╯ 6 | ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ 7 | ┃ FAIL package: github.com/mfridman/debug-go/testing ┃ 8 | ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ 9 | 10 | --- FAIL: TestWhatever (1.00s) 11 | 12 | main_test.go:13: assert error 13 | main_test.go:14: 14 | Error Trace: main_test.go:14 15 | Error: "does not contain" does not contain "ostriche" 16 | Test: TestWhatever 17 | main_test.go:15: 18 | Error Trace: main_test.go:15 19 | Error: Received unexpected error: 20 | make the errorss.. 21 | Test: TestWhatever 22 | main_test.go:17: skdhjfg 23 | 24 | ╭────────┬─────────┬──────────────────────────────────────┬───────┬──────┬──────┬──────╮ 25 | │ Status │ Elapsed │ Package │ Cover │ Pass │ Fail │ Skip │ 26 | ├────────┼─────────┼──────────────────────────────────────┼───────┼──────┼──────┼──────┤ 27 | │ FAIL │ 1.11s │ github.com/mfridman/debug-go/testing │ -- │ 0 │ 1 │ 0 │ 28 | ╰────────┴─────────┴──────────────────────────────────────┴───────┴──────┴──────┴──────╯ 29 | -------------------------------------------------------------------------------- /scripts/release-notes.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | # Check if the required argument is provided 6 | if [ $# -lt 1 ]; then 7 | echo "Usage: $0 []" 8 | exit 1 9 | fi 10 | 11 | version="$1" 12 | changelog_file="${2:-CHANGELOG.md}" 13 | 14 | # Check if the changelog file exists 15 | if [ ! -f "$changelog_file" ]; then 16 | echo "Error: $changelog_file does not exist" 17 | exit 1 18 | fi 19 | 20 | CAPTURE=0 21 | items="" 22 | # Read the changelog file line by line 23 | while IFS= read -r LINE; do 24 | # Stop capturing when we reach the next version sections 25 | if [[ "${LINE}" == "##"* ]] && [[ "${CAPTURE}" -eq 1 ]]; then 26 | break 27 | fi 28 | # Stop capturing when we reach the Unreleased section 29 | if [[ "${LINE}" == "[Unreleased]"* ]]; then 30 | break 31 | fi 32 | # Start capturing when we reach the specified version section 33 | if [[ "${LINE}" == "## [${version}]"* ]] && [[ "${CAPTURE}" -eq 0 ]]; then 34 | CAPTURE=1 35 | continue 36 | fi 37 | # Capture the lines between the specified version and the next version 38 | if [[ "${CAPTURE}" -eq 1 ]]; then 39 | # Ignore empty lines 40 | if [[ -z "${LINE}" ]]; then 41 | continue 42 | fi 43 | items+="$(echo "${LINE}" | xargs -0)" 44 | # Add a newline between each item 45 | if [[ -n "$items" ]]; then 46 | items+=$'\n' 47 | fi 48 | fi 49 | done <"${changelog_file}" 50 | 51 | if [[ -n "$items" ]]; then 52 | cat </dev/null 2>&1 || \ 16 | (echo "Installing latest golangci-lint" && \ 17 | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | \ 18 | sh -s -- -b "$(TOOLS_BIN)") 19 | 20 | .PHONY: tools-update 21 | tools-update: 22 | @echo "Updating golangci-lint to latest version" 23 | @rm -f "$(TOOLS_BIN)/golangci-lint" 24 | @curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | \ 25 | sh -s -- -b "$(TOOLS_BIN)" 26 | @echo "golangci-lint updated successfully to latest version" 27 | 28 | .PHONY: tools-version 29 | tools-version: 30 | @echo "Current tool versions:" 31 | @echo "golangci-lint: $$(golangci-lint --version 2>/dev/null || echo 'not installed')" 32 | 33 | .PHONY: release 34 | release: 35 | @goreleaser --rm-dist 36 | 37 | .PHONY: build 38 | build: 39 | @go build -o $$GOBIN/tparse ./ 40 | 41 | .PHONY: clean 42 | clean: 43 | @find . -type f -name '*.FAIL' -delete 44 | 45 | .PHONY: test 46 | test: 47 | @go test -count=1 ./... 48 | 49 | test-tparse: 50 | @go test -race -count=1 ./internal/... -json -cover | go run main.go -trimpath=auto -sort=elapsed 51 | @go test -race -count=1 ./tests/... -json -cover -coverpkg=./parse | go run main.go -trimpath=github.com/mfridman/tparse/ -sort=elapsed 52 | 53 | # dogfooding :) 54 | test-tparse-full: 55 | go test -race -count=1 -v ./... -json | go run main.go -all -smallscreen -notests -sort=elapsed 56 | 57 | coverage: 58 | go test ./tests/... -coverpkg=./parse -covermode=count -coverprofile=count.out 59 | go tool cover -html=count.out 60 | 61 | search-todo: 62 | @echo "Searching for TODOs in Go files..." 63 | @rg '// TODO\(mf\):' --glob '*.go' || echo "No TODOs found." 64 | 65 | -------------------------------------------------------------------------------- /tests/testdata/outcome/test_01.jsonl: -------------------------------------------------------------------------------- 1 | {"Time":"2018-10-17T22:05:12.535482-04:00","Action":"run","Package":"github.com/mfridman/tparse/tests","Test":"TestStatus"} 2 | {"Time":"2018-10-17T22:05:12.535868-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestStatus","Output":"=== RUN TestStatus\n"} 3 | {"Time":"2018-10-17T22:05:12.535902-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestStatus","Output":"=== PAUSE TestStatus\n"} 4 | {"Time":"2018-10-17T22:05:12.535919-04:00","Action":"pause","Package":"github.com/mfridman/tparse/tests","Test":"TestStatus"} 5 | {"Time":"2018-10-17T22:05:12.535942-04:00","Action":"cont","Package":"github.com/mfridman/tparse/tests","Test":"TestStatus"} 6 | {"Time":"2018-10-17T22:05:12.535959-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestStatus","Output":"=== CONT TestStatus\n"} 7 | {"Time":"2018-10-17T22:05:12.541357-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestStatus","Output":"--- FAIL: TestStatus (0.01s)\n"} 8 | {"Time":"2018-10-17T22:05:12.54139-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestStatus","Output":" status_test.go:56: log: file: package_skip02.json\n"} 9 | {"Time":"2018-10-17T22:05:12.541412-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestStatus","Output":" status_test.go:57: failed package summary action: got \"FAIL\", want \"PASS\"\n"} 10 | {"Time":"2018-10-17T22:05:12.541432-04:00","Action":"fail","Package":"github.com/mfridman/tparse/tests","Test":"TestStatus","Elapsed":0.01} 11 | {"Time":"2018-10-17T22:05:12.541458-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Output":"FAIL\n"} 12 | {"Time":"2018-10-17T22:05:12.541857-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Output":"FAIL\tgithub.com/mfridman/tparse/tests\t0.017s\n"} 13 | {"Time":"2018-10-17T22:05:12.541908-04:00","Action":"fail","Package":"github.com/mfridman/tparse/tests","Elapsed":0.017} 14 | -------------------------------------------------------------------------------- /tests/cached_test.go: -------------------------------------------------------------------------------- 1 | package parsetest 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | 11 | "github.com/mfridman/tparse/parse" 12 | ) 13 | 14 | func TestPackageCache(t *testing.T) { 15 | t.Parallel() 16 | 17 | base := filepath.Join("testdata", "cached") 18 | 19 | // Where bool indicates whether the package is expected to be marked as cached. 20 | type expected map[string]bool 21 | 22 | // Note, remember to clean the cache before running: 23 | // go clean -testcache 24 | tt := []struct { 25 | fileName string 26 | expected 27 | }{ 28 | { 29 | // go test strings fmt -json 30 | // go test strings fmt time mime -json 31 | "test_01", 32 | expected{ 33 | "strings": true, 34 | "fmt": true, 35 | "time": false, 36 | "mime": false, 37 | }, 38 | }, 39 | { 40 | // go test log mime sort strings -json 41 | // go test bufio bytes crypto fmt log mime sort strings time -json 42 | "test_02", 43 | expected{ 44 | "bufio": false, 45 | "bytes": false, 46 | "crypto": false, 47 | "fmt": false, 48 | "log": true, 49 | "mime": true, 50 | "sort": true, 51 | "strings": true, 52 | "time": false, 53 | }, 54 | }, 55 | } 56 | 57 | for _, tc := range tt { 58 | t.Run(tc.fileName, func(t *testing.T) { 59 | inputFile := filepath.Join(base, tc.fileName+".jsonl") 60 | f, err := os.Open(inputFile) 61 | require.NoError(t, err) 62 | 63 | summary, err := parse.Process(f) 64 | require.NoError(t, err) 65 | assert.Len(t, summary.Packages, len(tc.expected)) 66 | 67 | for name, pkg := range summary.Packages { 68 | t.Run(name, func(t *testing.T) { 69 | wantCached, ok := tc.expected[name] 70 | if !ok { 71 | t.Fatalf("got unexpected package name: %q", name) 72 | } 73 | if pkg.Cached != wantCached { 74 | t.Fatalf("got %t, want package %q to have cached field marked %t", pkg.Cached, name, wantCached) 75 | } 76 | }) 77 | } 78 | }) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | types: [opened, synchronize, reopened] 9 | workflow_dispatch: 10 | 11 | jobs: 12 | build: 13 | name: Build and test 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | # go-version: ['oldstable', 'stable', '1.23.0-rc.2'] 18 | go-version: ['oldstable', 'stable'] 19 | env: 20 | VERBOSE: 1 21 | 22 | steps: 23 | - name: Checkout code 24 | uses: actions/checkout@v5 25 | - name: Set up Go 26 | uses: actions/setup-go@v5 27 | with: 28 | go-version: ${{ matrix.go-version }} 29 | - name: Build 30 | run: go build -v . 31 | - name: Run tests with GITHUB_STEP_SUMMARY 32 | shell: bash 33 | # Note the use of || true. This so the job doesn't fail at that line. We want to preserve -follow 34 | # as part of the test output, but not output it to the summary page, which is done in the proceeding 35 | # command when we parse the output.jsonl file. 36 | run: | 37 | go test -v -count=1 -race ./... -json -coverpkg github.com/mfridman/tparse/parse \ 38 | | tee output.jsonl | ./tparse -notests -follow -all || true 39 | ./tparse -format markdown -file output.jsonl -all -slow 20 > $GITHUB_STEP_SUMMARY 40 | - name: Run tparse w/ std lib 41 | run: go test -count=1 fmt strings bytes bufio crypto log mime sort slices -json -cover | ./tparse -follow -all 42 | - name: Install GoReleaser 43 | if: github.event_name == 'push' && github.ref == 'refs/heads/main' && matrix.go-version == 'stable' 44 | uses: goreleaser/goreleaser-action@v6 45 | with: 46 | install-only: true 47 | distribution: goreleaser 48 | version: "~> v2" 49 | - name: Gorelease dry-run 50 | if: github.event_name == 'push' && github.ref == 'refs/heads/main' && matrix.go-version == 'stable' 51 | run: | 52 | goreleaser release --skip=publish --snapshot --fail-fast --clean 53 | -------------------------------------------------------------------------------- /tests/testdata/follow-verbose/test_02.golden: -------------------------------------------------------------------------------- 1 | ? github.com/mfridman/rover [no test files] 2 | ? github.com/mfridman/rover/cmd/assets [no test files] 3 | ? github.com/mfridman/rover/cmd/roverd [no test files] 4 | === RUN TestStringPager 5 | === RUN TestStringPager/PageableWithLimit 6 | === RUN TestStringPager/NonPageable 7 | --- PASS: TestStringPager (0.00s) 8 | --- PASS: TestStringPager/PageableWithLimit (0.00s) 9 | --- PASS: TestStringPager/NonPageable (0.00s) 10 | PASS 11 | coverage: 100.0% of statements 12 | ok github.com/mfridman/rover/pkg/paging/stringpager 0.033s coverage: 100.0% of statements 13 | ? github.com/mfridman/rover/pkg/web [no test files] 14 | ? github.com/mfridman/rover/rpc/mailer [no test files] 15 | ? github.com/mfridman/rover/smtp [no test files] 16 | ? github.com/mfridman/rover/smtp/smtpserver [no test files] 17 | ? github.com/mfridman/rover/storage [no test files] 18 | ? github.com/mfridman/rover/storage/badger [no test files] 19 | === RUN TestNameFormat 20 | --- PASS: TestNameFormat (0.00s) 21 | PASS 22 | coverage: 0.0% of statements 23 | ok github.com/mfridman/rover/tests/helper 0.030s coverage: 0.0% of statements 24 | 2018/12/16 20:40:12 Replaying from value pointer: {Fid:0 Len:0 Offset:0} 25 | 2018/12/16 20:40:12 Iterating file id: 0 26 | 2018/12/16 20:40:12 Iteration took: 80.538µs 27 | 2018/12/16 20:40:12 Replaying from value pointer: {Fid:0 Len:0 Offset:0} 28 | 2018/12/16 20:40:12 Iterating file id: 0 29 | 2018/12/16 20:40:12 Iteration took: 43.759µs 30 | === RUN TestAddEmail 31 | === PAUSE TestAddEmail 32 | === RUN TestDeleteEmail 33 | === PAUSE TestDeleteEmail 34 | === RUN TestMarkAsRead 35 | === PAUSE TestMarkAsRead 36 | === RUN TestMarkAsUnRead 37 | === PAUSE TestMarkAsUnRead 38 | === CONT TestAddEmail 39 | === CONT TestMarkAsUnRead 40 | === CONT TestMarkAsRead 41 | === CONT TestDeleteEmail 42 | === RUN TestAddEmail/retrieveEmail 43 | --- PASS: TestAddEmail (0.00s) 44 | --- PASS: TestAddEmail/retrieveEmail (0.00s) 45 | --- PASS: TestDeleteEmail (0.00s) 46 | --- PASS: TestMarkAsRead (0.00s) 47 | --- PASS: TestMarkAsUnRead (0.00s) 48 | PASS 49 | coverage: 0.0% of statements 50 | server closed 51 | ok github.com/mfridman/rover/tests 0.046s coverage: 0.0% of statements 52 | -------------------------------------------------------------------------------- /tests/testdata/outcome/test_05.jsonl: -------------------------------------------------------------------------------- 1 | {"Time":"2018-10-15T23:00:27.929094-04:00","Action":"output","Package":"github.com/astromail/rover/tests","Output":"2018/10/15 23:00:27 Replaying from value pointer: {Fid:0 Len:0 Offset:0}\n"} 2 | {"Time":"2018-10-15T23:00:27.929492-04:00","Action":"output","Package":"github.com/astromail/rover/tests","Output":"2018/10/15 23:00:27 Iterating file id: 0\n"} 3 | {"Time":"2018-10-15T23:00:27.929529-04:00","Action":"output","Package":"github.com/astromail/rover/tests","Output":"2018/10/15 23:00:27 Replaying from value pointer: {Fid:0 Len:0 Offset:0}\n"} 4 | {"Time":"2018-10-15T23:00:27.929543-04:00","Action":"output","Package":"github.com/astromail/rover/tests","Output":"2018/10/15 23:00:27 Iterating file id: 0\n"} 5 | {"Time":"2018-10-15T23:00:27.929572-04:00","Action":"output","Package":"github.com/astromail/rover/tests","Output":"2018/10/15 23:00:27 Iteration took: 73.332µs\n"} 6 | {"Time":"2018-10-15T23:00:27.929588-04:00","Action":"output","Package":"github.com/astromail/rover/tests","Output":"2018/10/15 23:00:27 Iteration took: 39.372µs\n"} 7 | {"Time":"2018-10-15T23:00:28.430669-04:00","Action":"run","Package":"github.com/astromail/rover/tests","Test":"TestNameFormat"} 8 | {"Time":"2018-10-15T23:00:28.430728-04:00","Action":"output","Package":"github.com/astromail/rover/tests","Test":"TestNameFormat","Output":"=== RUN TestNameFormat\n"} 9 | {"Time":"2018-10-15T23:00:28.43076-04:00","Action":"output","Package":"github.com/astromail/rover/tests","Test":"TestNameFormat","Output":"--- SKIP: TestNameFormat (0.00s)\n"} 10 | {"Time":"2018-10-15T23:00:28.430781-04:00","Action":"output","Package":"github.com/astromail/rover/tests","Test":"TestNameFormat","Output":" testutil_test.go:9: \n"} 11 | {"Time":"2018-10-15T23:00:28.430798-04:00","Action":"skip","Package":"github.com/astromail/rover/tests","Test":"TestNameFormat","Elapsed":0} 12 | {"Time":"2018-10-15T23:00:28.430825-04:00","Action":"output","Package":"github.com/astromail/rover/tests","Output":"PASS\n"} 13 | {"Time":"2018-10-15T23:00:28.432239-04:00","Action":"output","Package":"github.com/astromail/rover/tests","Output":"ok \tgithub.com/astromail/rover/tests\t0.530s\n"} 14 | {"Time":"2018-10-15T23:00:28.436039-04:00","Action":"pass","Package":"github.com/astromail/rover/tests","Elapsed":0.534} 15 | -------------------------------------------------------------------------------- /internal/utils/utils_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "testing" 4 | 5 | func TestFindLongestCommonPrefix(t *testing.T) { 6 | t.Parallel() 7 | 8 | tests := []struct { 9 | paths []string 10 | want string 11 | }{ 12 | { 13 | paths: []string{}, 14 | want: "", 15 | }, 16 | { 17 | paths: []string{ 18 | "github.com/user/project/pkg", 19 | }, 20 | want: "", 21 | }, 22 | { 23 | paths: []string{ 24 | "github.com/user/project/pkg", 25 | "github.com/user/project/pkg", 26 | "github.com/user/project/pkg", 27 | }, 28 | want: "github.com/user/project/pkg", 29 | }, 30 | { 31 | paths: []string{ 32 | "github.com/user/project/pkg", 33 | "github.com/user/project/cmd", 34 | }, 35 | want: "github.com/user/project/", 36 | }, 37 | { 38 | paths: []string{ 39 | "github.com/user/project/pkg", 40 | "bitbucket.org/user/project/cmd", 41 | }, 42 | want: "", 43 | }, 44 | { 45 | paths: []string{ 46 | "github.com/user/project/pkg", 47 | "github.com/user/project/cmd", 48 | "github.com/user/project/cmd/subcmd", 49 | "github.com/nonuser/project/cmd/subcmd", 50 | }, 51 | want: "github.com/", 52 | }, 53 | { 54 | paths: []string{ 55 | "github.com/foo/bar/baz/qux", 56 | "github.com/foo/bar/baz", 57 | "github.com/foo/bar/baz/qux/quux", 58 | "github.com/foo/bar/baz/qux/quux/corge", 59 | "github.com/foo/bar/baz/foo", 60 | "github.com/foo/bar/baz/foo/bar", 61 | }, 62 | want: "github.com/foo/bar/", 63 | }, 64 | { 65 | paths: []string{ 66 | "/", 67 | }, 68 | want: "", 69 | }, 70 | { 71 | paths: []string{ 72 | "/", 73 | "/", 74 | }, 75 | want: "/", 76 | }, 77 | { 78 | paths: []string{ 79 | "/abc", 80 | "/abc", 81 | }, 82 | want: "/abc", 83 | }, 84 | { 85 | paths: []string{ 86 | "foo/bar/foo", 87 | "foo/foo/foo", 88 | }, 89 | want: "foo/", 90 | }, 91 | } 92 | for _, tt := range tests { 93 | t.Run("", func(t *testing.T) { 94 | actual := FindLongestCommonPrefix(tt.paths) 95 | if actual != tt.want { 96 | t.Errorf("want %s, got %s", tt.want, actual) 97 | } 98 | }) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /tests/testdata/follow-verbose/test_05.jsonl: -------------------------------------------------------------------------------- 1 | It 2 | is 3 | a 4 | long 5 | established 6 | fact 7 | that 8 | a 9 | reader 10 | will 11 | be 12 | distracted 13 | by 14 | the 15 | readable 16 | content 17 | of 18 | a 19 | page 20 | when 21 | looking 22 | at 23 | its 24 | layout. 25 | The 26 | point 27 | of 28 | using 29 | Lorem 30 | Ipsum 31 | is 32 | that 33 | it 34 | has 35 | a 36 | more-or-less 37 | normal 38 | distribution 39 | of 40 | letters, 41 | as 42 | opposed 43 | to 44 | using 45 | 'Content 46 | here, 47 | content 48 | here', 49 | making 50 | it 51 | look 52 | like 53 | readable 54 | English. 55 | Many 56 | desktop 57 | publishing 58 | packages 59 | and 60 | web 61 | page 62 | editors 63 | now 64 | use 65 | Lorem 66 | Ipsum 67 | as 68 | their 69 | default 70 | model 71 | text, 72 | and 73 | a 74 | search 75 | for 76 | 'lorem 77 | ipsum' 78 | will 79 | uncover 80 | many 81 | web 82 | sites 83 | still 84 | in 85 | their 86 | infancy. 87 | Various 88 | versions 89 | have 90 | evolved 91 | over 92 | the 93 | years, 94 | sometimes 95 | by 96 | accident, 97 | sometimes 98 | on 99 | purpose 100 | (injected 101 | humour 102 | and 103 | the 104 | like). 105 | It 106 | is 107 | a 108 | long 109 | established 110 | fact 111 | that 112 | a 113 | reader 114 | will 115 | be 116 | distracted 117 | by 118 | the 119 | readable 120 | content 121 | of 122 | a 123 | page 124 | when 125 | looking 126 | at 127 | its 128 | layout. 129 | The 130 | point 131 | of 132 | using 133 | Lorem 134 | Ipsum 135 | is 136 | that 137 | it 138 | has 139 | a 140 | more-or-less 141 | normal 142 | distribution 143 | of 144 | letters, 145 | as 146 | opposed 147 | to 148 | using 149 | 'Content 150 | here, 151 | content 152 | here', 153 | making 154 | it 155 | look 156 | like 157 | readable 158 | English. 159 | Many 160 | desktop 161 | publishing 162 | packages 163 | and 164 | web 165 | page 166 | editors 167 | now 168 | use 169 | Lorem 170 | Ipsum 171 | as 172 | their 173 | default 174 | model 175 | text, 176 | and 177 | a 178 | search 179 | for 180 | 'lorem 181 | ipsum' 182 | will 183 | uncover 184 | many 185 | web 186 | sites 187 | still 188 | in 189 | their 190 | infancy. 191 | Various 192 | versions 193 | have 194 | evolved 195 | over 196 | the 197 | years, 198 | sometimes 199 | by 200 | accident, 201 | sometimes 202 | on 203 | purpose 204 | (injected 205 | humour 206 | and 207 | the 208 | like). 209 | -------------------------------------------------------------------------------- /tests/testdata/prescan/test_03.txt: -------------------------------------------------------------------------------- 1 | git.checkout 2 | 0.55s$ git clone --depth=50 --branch=master https://github.com/mfridman/tparse.git mfridman/tparse 3 | Cloning into 'mfridman/tparse'... 4 | remote: Enumerating objects: 485, done. 5 | remote: Counting objects: 100% (485/485), done. 6 | {"Time":"2018-10-24T13:13:35.447209-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestStack","Output":"=== RUN TestStack\n"} 7 | remote: Compressing objects: 100% (310/310), done. 8 | {"Time":"2018-10-24T13:13:35.447317-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestStack", 9 | remote: Total 485 (delta 209), reused 442 (delta 168), pack-reused 0 10 | Receiving objects: 100% (485/485), 1.17 MiB | 20.58 MiB/s, done. 11 | Resolving deltas: 100% (209/209), done. 12 | $ cd mfridman/tparse 13 | $ git checkout -qf d7a97f658463e3abd90357d9988575e8138d9a86 14 | {"Time":"2018-10-24T13:13:35.445816-04:00","Action":"run","Package":"github.com/mfridman/tparse/tests","Test":"TestStack"} 15 | {"Time":"2018-10-24T13:13:35.447209-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestStack","Output":"=== RUN TestStack\n"} 16 | {"Time":"2018-10-24T13:13:35.44729-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestStack","Output":"=== PAUSE TestStack\n"} 17 | {"Time":"2018-10-24T13:13:35.447302-04:00","Action":"pause","Package":"github.com/mfridman/tparse/tests","Test":"TestStack"} 18 | {"Time":"2018-10-24T13:13:35.447311-04:00","Action":"cont","Package":"github.com/mfridman/tparse/tests","Test":"TestStack"} 19 | {"Time":"2018-10-24T13:13:35.447317-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestStack","Output":"=== CONT TestStack\n"} 20 | {"Time":"2018-10-24T13:13:35.447348-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestStack","Output":"--- PASS: TestStack (0.00s)\n"} 21 | {"Time":"2018-10-24T13:13:35.447394-04:00","Action":"pass","Package":"github.com/mfridman/tparse/tests","Test":"TestStack","Elapsed":0} 22 | {"Time":"2018-10-24T13:13:35.447481-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Output":"PASS\n"} 23 | {"Time":"2018-10-24T13:13:35.447759-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Output":"ok \tgithub.com/mfridman/tparse/tests\t0.014s\n"} 24 | {"Time":"2018-10-24T13:13:35.454438-04:00","Action":"pass","Package":"github.com/mfridman/tparse/tests","Elapsed":0.021} -------------------------------------------------------------------------------- /tests/cover_test.go: -------------------------------------------------------------------------------- 1 | package parsetest 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | 11 | "github.com/mfridman/tparse/parse" 12 | ) 13 | 14 | func Test(t *testing.T) { 15 | t.Parallel() 16 | 17 | base := filepath.Join("testdata", "cover") 18 | 19 | // expected package name and corresponding cover % 20 | // Note, these numbers will vary between on go versions. 21 | type expected map[string]float64 22 | 23 | tt := []struct { 24 | fileName string 25 | expected 26 | }{ 27 | { 28 | // go test -count=1 bytes log sort -json -cover 29 | "test_01", 30 | expected{"log": 68.0, "bytes": 86.7, "sort": 60.8}, 31 | }, 32 | { 33 | // go test -count=1 bufio bytes crypto fmt log mime net sort strings time -json -cover 34 | "test_02", 35 | expected{ 36 | "bufio": 93.3, 37 | "bytes": 95.6, 38 | "crypto": 5.9, 39 | "fmt": 95.2, 40 | "log": 68.0, 41 | "mime": 93.8, 42 | "net": 81.2, 43 | "sort": 60.8, 44 | "strings": 98.1, 45 | "time": 91.8, 46 | }, 47 | }, 48 | { 49 | // This is run without the -cover flag. Expecting 0.0 for all packages. 50 | // go test -count=1 crypto fmt log strings -json -cover 51 | "test_03", 52 | expected{ 53 | "crypto": 0.0, 54 | "fmt": 0.0, 55 | "log": 0.0, 56 | "strings": 0.0, 57 | }, 58 | }, 59 | } 60 | for _, tc := range tt { 61 | t.Run(tc.fileName, func(t *testing.T) { 62 | inputFile := filepath.Join(base, tc.fileName+".jsonl") 63 | f, err := os.Open(inputFile) 64 | require.NoError(t, err) 65 | 66 | summary, err := parse.Process(f) 67 | require.NoError(t, err) 68 | assert.Len(t, summary.Packages, len(tc.expected)) 69 | 70 | for name, pkg := range summary.Packages { 71 | t.Run(name, func(t *testing.T) { 72 | wantCover, ok := tc.expected[name] 73 | if !ok { 74 | t.Fatalf("got unexpected package name: %q", name) 75 | } 76 | if pkg.Coverage != wantCover { 77 | t.Fatalf("got cover: %v, want package %q cover: %v", pkg.Coverage, name, wantCover) 78 | } 79 | var f float64 80 | if pkg.Coverage > f && !pkg.Cover { 81 | t.Fatalf("got %v, want package %q to have cover field marked as true when coverage %v>%v", 82 | pkg.Cover, 83 | name, 84 | pkg.Coverage, 85 | f, 86 | ) 87 | } 88 | }) 89 | } 90 | }) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /tests/package_start_test.go: -------------------------------------------------------------------------------- 1 | package parsetest 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | 11 | "github.com/mfridman/tparse/parse" 12 | ) 13 | 14 | func TestPackageStartTime(t *testing.T) { 15 | t.Parallel() 16 | 17 | // This test depends on go120_start_action.jsonl, which contains test output from go1.20 18 | 19 | expected := map[string]string{ 20 | "github.com/pressly/goose/v4": "2023-05-28T18:36:01.280967-04:00", 21 | "github.com/pressly/goose/v4/internal/check": "2023-05-28T18:36:01.281088-04:00", 22 | "github.com/pressly/goose/v4/internal/cli": "2023-05-28T18:36:01.281147-04:00", 23 | "github.com/pressly/goose/v4/internal/dialectadapter": "2023-05-28T18:36:01.281218-04:00", 24 | "github.com/pressly/goose/v4/internal/dialectadapter/dialectquery": "2023-05-28T18:36:01.281253-04:00", 25 | "github.com/pressly/goose/v4/internal/migration": "2023-05-28T18:36:01.281269-04:00", 26 | "github.com/pressly/goose/v4/internal/migrationstats": "2023-05-28T18:36:01.281381-04:00", 27 | "github.com/pressly/goose/v4/internal/migrationstats/migrationstatsos": "2023-05-28T18:36:01.281426-04:00", 28 | "github.com/pressly/goose/v4/internal/normalizedsn": "2023-05-28T18:36:01.281465-04:00", 29 | "github.com/pressly/goose/v4/internal/sqlparser": "2023-05-28T18:36:01.446915-04:00", 30 | "github.com/pressly/goose/v4/internal/testdb": "2023-05-28T18:36:01.446973-04:00", 31 | } 32 | 33 | fileName := "./testdata/go120_start_action.jsonl" 34 | f, err := os.Open(fileName) 35 | require.NoError(t, err) 36 | defer f.Close() 37 | 38 | summary, err := parse.Process(f) 39 | require.NoError(t, err) 40 | assert.Len(t, summary.Packages, len(expected)) 41 | 42 | for _, p := range summary.Packages { 43 | if p.StartTime.IsZero() { 44 | t.Fatalf("package %q cannot contain zero start time", p.Summary.Package) 45 | } 46 | unparsed, ok := expected[p.Summary.Package] 47 | if !ok { 48 | t.Fatalf("package %q not found in expected map", p.Summary.Package) 49 | } 50 | want, err := time.Parse(time.RFC3339, unparsed) 51 | require.NoError(t, err) 52 | if !p.StartTime.Equal(want) { 53 | t.Fatalf("package %q start time got %q want %q", p.Summary.Package, p.StartTime, want) 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | enable: 4 | - dupword 5 | - errorlint 6 | - mirror 7 | - misspell 8 | - nolintlint 9 | - revive 10 | - testifylint 11 | - thelper 12 | - modernize 13 | settings: 14 | dupword: 15 | ignore: 16 | - FAIL 17 | nolintlint: 18 | require-explanation: true 19 | require-specific: true 20 | allow-unused: true 21 | revive: 22 | rules: 23 | - name: bare-return 24 | - name: blank-imports 25 | - name: comment-spacings 26 | - name: context-as-argument 27 | arguments: 28 | - allowTypesBefore: '*testing.T' 29 | - name: context-keys-type 30 | - name: defer 31 | arguments: 32 | - - call-chain 33 | - loop 34 | - name: dot-imports 35 | - name: early-return 36 | - name: empty-block 37 | - name: error-return 38 | - name: error-strings 39 | - name: error-naming 40 | - name: errorf 41 | - name: exported 42 | arguments: 43 | - checkPrivateReceivers 44 | - sayRepetitiveInsteadOfStutters 45 | - name: if-return 46 | - name: import-shadowing 47 | - name: increment-decrement 48 | - name: indent-error-flow 49 | - name: exported 50 | - name: var-declaration 51 | - name: package-comments 52 | - name: range 53 | - name: receiver-naming 54 | - name: redefines-builtin-id 55 | - name: superfluous-else 56 | - name: time-naming 57 | - name: time-equal 58 | - name: unexported-return 59 | - name: use-any 60 | - name: unreachable-code 61 | - name: unhandled-error 62 | arguments: 63 | - fmt.Print.* 64 | - fmt.Fprint.* 65 | - bytes.Buffer.Write.* 66 | - strings.Builder.Write.* 67 | - name: unused-parameter 68 | - name: unused-receiver 69 | - name: useless-break 70 | exclusions: 71 | generated: lax 72 | presets: 73 | - comments 74 | - common-false-positives 75 | - legacy 76 | - std-error-handling 77 | paths: 78 | - third_party$ 79 | - builtin$ 80 | - examples$ 81 | formatters: 82 | enable: 83 | - gci 84 | settings: 85 | gci: 86 | sections: 87 | - standard 88 | - default 89 | - prefix(github.com/mfridman) 90 | exclusions: 91 | generated: lax 92 | paths: 93 | - third_party$ 94 | - builtin$ 95 | - examples$ 96 | -------------------------------------------------------------------------------- /tests/panic_test.go: -------------------------------------------------------------------------------- 1 | package parsetest 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | 11 | "github.com/mfridman/tparse/parse" 12 | ) 13 | 14 | func TestPanic(t *testing.T) { 15 | t.Parallel() 16 | 17 | // Key is the package name, bool reports whether the packages is expected to be marked as panic. 18 | type expected map[string]bool 19 | 20 | base := filepath.Join("testdata", "panic") 21 | 22 | tt := []struct { 23 | fileName string 24 | expected 25 | }{ 26 | { 27 | "test_01.jsonl", expected{ 28 | "github.com/pressly/goose/v3/tests/e2e": true, 29 | }, 30 | }, 31 | { 32 | "test_02.jsonl", expected{ 33 | "github.com/mfridman/tparse/parse": true, 34 | }, 35 | }, 36 | { 37 | "test_03.jsonl", expected{ 38 | "github.com/mfridman/tparse/tests": true, 39 | }, 40 | }, 41 | { 42 | "test_04.jsonl", expected{ 43 | "github.com/mfridman/tparse/tests": true, 44 | "github.com/mfridman/tparse/ignore": false, 45 | "github.com/mfridman/tparse/parse": false, 46 | "github.com/mfridman/tparse": false, 47 | }, 48 | }, 49 | { 50 | "test_05.jsonl", expected{ 51 | "github.com/mfridman/tparse/tests": true, 52 | "github.com/mfridman/tparse/parse": false, 53 | "github.com/mfridman/tparse": false, 54 | "github.com/mfridman/tparse/ignore": false, 55 | }, 56 | }, 57 | { 58 | "test_06.jsonl", expected{ 59 | "github.com/mfridman/tparse/tests": false, 60 | "github.com/mfridman/tparse/parse": true, 61 | "github.com/mfridman/tparse": false, 62 | "github.com/mfridman/tparse/ignore": false, 63 | }, 64 | }, 65 | } 66 | 67 | for _, tc := range tt { 68 | t.Run(tc.fileName, func(t *testing.T) { 69 | f, err := os.Open(filepath.Join(base, tc.fileName)) 70 | require.NoError(t, err) 71 | 72 | summary, err := parse.Process(f) 73 | require.NoError(t, err) 74 | assert.Equal(t, 1, summary.ExitCode()) 75 | 76 | for name, pkg := range summary.Packages { 77 | want, ok := tc.expected[name] 78 | if !ok { 79 | t.Log("currently registered packages:") 80 | for k := range tc.expected { 81 | t.Log(k) 82 | } 83 | t.Fatalf("got unmapped package name %q. Check input file and record all unique package names in registry", name) 84 | } 85 | if pkg.HasPanic != want { 86 | t.Log("package: ", name) 87 | t.Logf("summary: %+v", pkg.Summary) 88 | t.Fatal("got no panic, expecting package to be marked as has panic") 89 | } 90 | } 91 | }) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /tests/race_test.go: -------------------------------------------------------------------------------- 1 | package parsetest 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | 11 | "github.com/mfridman/tparse/parse" 12 | ) 13 | 14 | func TestRaceDetected(t *testing.T) { 15 | t.Parallel() 16 | 17 | // Key is the package name, and the value may be zero, one or more test name(s). 18 | // Not all data races may be associated with a test. 19 | type expected map[string][]string 20 | 21 | base := filepath.Join("testdata", "race") 22 | 23 | var tt = []struct { 24 | fileName string 25 | expected 26 | }{ 27 | { 28 | "test_01", expected{"command-line-arguments": {"TestA"}}, 29 | }, 30 | { 31 | "test_02", expected{"github.com/mfridman/tparse/parse": {"TestB", "TestElapsed"}}, 32 | }, 33 | { 34 | "test_03", expected{"debug/tparse-24": {}}, 35 | }, 36 | // This is a race directly from Test only. 37 | { 38 | "test_04", expected{"github.com/mfridman/debug-go/testing": {"TestRace"}}, 39 | }, 40 | // This is a race directly from TestMain with other tests that have failed. 41 | { 42 | "test_05", expected{"github.com/mfridman/debug-go/testing": {}}, 43 | }, 44 | // This is a race directly from TestMain only. 45 | { 46 | "test_06", expected{"github.com/mfridman/debug-go/testing": {}}, 47 | }, 48 | // This is a race from a Test that calls into a package that has a race condition. (failed assertion) 49 | { 50 | "test_07", expected{"github.com/mfridman/debug-go/testing": {"TestRace"}}, 51 | }, 52 | // This is a race from a Test that calls into a package that has a race condition. (passed assertion) 53 | { 54 | "test_08", expected{"github.com/mfridman/debug-go/testing": {"TestRace"}}, 55 | }, 56 | } 57 | 58 | for _, tc := range tt { 59 | t.Run(tc.fileName, func(t *testing.T) { 60 | inputFile := filepath.Join(base, tc.fileName+".jsonl") 61 | f, err := os.Open(inputFile) 62 | require.NoError(t, err) 63 | defer f.Close() 64 | 65 | summary, err := parse.Process(f) 66 | require.NoError(t, err) 67 | 68 | if summary.ExitCode() == 0 { 69 | t.Fatalf("expecting non-zero exit code") 70 | } 71 | for name, pkg := range summary.Packages { 72 | wantTestName, ok := tc.expected[name] 73 | if !ok { 74 | t.Fatalf("failed to find package: %q", name) 75 | } 76 | assert.Len(t, pkg.DataRaceTests, len(wantTestName)) 77 | if len(pkg.DataRaceTests) > 0 { 78 | for i := range pkg.DataRaceTests { 79 | assert.Equal(t, pkg.DataRaceTests[i], wantTestName[i]) 80 | } 81 | } 82 | } 83 | }) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /tests/testdata/failed/test_04.golden: -------------------------------------------------------------------------------- 1 | ╭────────┬─────────┬────────────────────────────────┬────────────────────────╮ 2 | │ Status │ Elapsed │ Test │ Package │ 3 | ├────────┼─────────┼────────────────────────────────┼────────────────────────┤ 4 | │ FAIL │ 1.00 │ TestWhatever │ command-line-arguments │ 5 | │ FAIL │ 0.00 │ TestWhatever/foo │ command-line-arguments │ 6 | │ FAIL │ 0.00 │ TestWhatever/foo/bar │ command-line-arguments │ 7 | │ FAIL │ 0.00 │ TestWhatever/foo/baz │ command-line-arguments │ 8 | │ FAIL │ 0.00 │ TestWhatever/foo/bar/inner-bar │ command-line-arguments │ 9 | │ FAIL │ 0.00 │ TestWhatever/foo/baz/inner-baz │ command-line-arguments │ 10 | ╰────────┴─────────┴────────────────────────────────┴────────────────────────╯ 11 | ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ 12 | ┃ FAIL package: command-line-arguments ┃ 13 | ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ 14 | 15 | --- FAIL: TestWhatever (1.00s) 16 | 17 | main_test.go:12: assert error 18 | main_test.go:13: 19 | Error Trace: main_test.go:13 20 | Error: "does not contain" does not contain "ostriche" 21 | Test: TestWhatever 22 | main_test.go:35: 23 | Error Trace: main_test.go:35 24 | Error: Not equal: 25 | expected: 7823456 26 | actual : 1 27 | Test: TestWhatever 28 | Messages: not what I was expecting 29 | 30 |  --- FAIL: TestWhatever/foo (0.00s) 31 | 32 | main_test.go:17: some random output from foo only 33 | 34 |  --- FAIL: TestWhatever/foo/bar (0.00s) 35 | 36 | main_test.go:20: some random output from bar only 37 | 38 |  --- FAIL: TestWhatever/foo/bar/inner-bar (0.00s) 39 | 40 | main_test.go:23: another inner-bar 41 | 42 |  --- FAIL: TestWhatever/foo/baz (0.00s) 43 |  --- FAIL: TestWhatever/foo/baz/inner-baz (0.00s) 44 | 45 | main_test.go:30: some inner-baz error 46 | 47 | ╭────────┬─────────┬────────────────────────┬───────┬──────┬──────┬──────╮ 48 | │ Status │ Elapsed │ Package │ Cover │ Pass │ Fail │ Skip │ 49 | ├────────┼─────────┼────────────────────────┼───────┼──────┼──────┼──────┤ 50 | │ FAIL │ 1.13s │ command-line-arguments │ -- │ 0 │ 6 │ 0 │ 51 | ╰────────┴─────────┴────────────────────────┴───────┴──────┴──────┴──────╯ 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tparse [![Actions](https://github.com/mfridman/tparse/workflows/CI/badge.svg)](https://github.com/mfridman/tparse) 2 | 3 | A command line tool for analyzing and summarizing `go test` output. 4 | 5 | > [!TIP] 6 | > 7 | > Don't forget to run `go test` with the `-json` flag. 8 | 9 | Pass | Fail 10 | :-------------------------:|:-------------------------: 11 | | 12 | 13 | By default, `tparse` will always return test failures and panics, if any, followed by a package-level summary table. 14 | 15 | 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). 16 | 17 | ### [But why?!](#but-why) for more info. 18 | 19 | ## Installation 20 | 21 | go install github.com/mfridman/tparse@latest 22 | 23 | Or download the latest pre-built binary [here](https://github.com/mfridman/tparse/releases/latest). 24 | 25 | ## Usage 26 | 27 | Once `tparse` is installed there are 2 ways to use it: 28 | 29 | 1. Run `go test` as normal, but add `-json` flag and pipe output to `tparse`. 30 | 31 | ``` 32 | set -o pipefail && go test fmt -json | tparse -all 33 | ``` 34 | 35 | 2. Save the output of `go test` with `-json` flag into a file and call `tparse` with `-file` option. 36 | 37 | ``` 38 | go test fmt -json > fmt.out 39 | tparse -all -file=fmt.out 40 | ``` 41 | 42 | Tip: run `tparse -h` to get usage and options. 43 | 44 | ## But why?! 45 | 46 | `go test` is awesome, but verbose. Sometimes you just want readily available failures, grouped by package, printed with a dash of color. 47 | 48 | `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". 49 | 50 | 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. 51 | 52 | `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. 53 | 54 | The default print order is: 55 | - `go test` output (if adding `-follow` flag) 56 | - passed/skipped table (if adding `-all`, `-skip` or `-pass` flag) 57 | - failed tests and panics 58 | - summary 59 | 60 | For narrow displays the `-smallscreen` flag may be useful, dividing a long test name and making it vertical heavy: 61 | 62 | ``` 63 | TestSubtests/an_awesome_but_long/subtest_for_the/win 64 | 65 | TestSubtests 66 | /an_awesome_but_long 67 | /subtest_for_the 68 | /win 69 | ``` 70 | 71 | `tparse` aims to be a simple alternative to one-liner bash functions. 72 | -------------------------------------------------------------------------------- /internal/app/console_writer.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "io" 5 | "strings" 6 | 7 | "github.com/charmbracelet/lipgloss" 8 | "github.com/muesli/termenv" 9 | 10 | "github.com/mfridman/tparse/parse" 11 | ) 12 | 13 | type OutputFormat int 14 | 15 | const ( 16 | // OutputFormatBasic is a normal table without a border 17 | OutputFormatPlain OutputFormat = iota + 1 18 | // OutputFormatBasic is a normal table with border 19 | OutputFormatBasic 20 | // OutputFormatBasic is a markdown-rendered table 21 | OutputFormatMarkdown 22 | ) 23 | 24 | type consoleWriter struct { 25 | io.Writer 26 | format OutputFormat 27 | 28 | red colorOptionFunc 29 | green colorOptionFunc 30 | yellow colorOptionFunc 31 | } 32 | 33 | type colorOptionFunc func(s string) string 34 | 35 | // newColor is a helper function to set the base color. 36 | func newColor(color lipgloss.TerminalColor) colorOptionFunc { 37 | return func(text string) string { 38 | return lipgloss.NewStyle().Foreground(color).Render(text) 39 | } 40 | } 41 | 42 | // newMarkdownColor is a helper function to set the base color for markdown. 43 | func newMarkdownColor(s string) colorOptionFunc { 44 | return func(text string) string { 45 | return s + " " + text 46 | } 47 | } 48 | 49 | func noColor() colorOptionFunc { 50 | return func(text string) string { return text } 51 | } 52 | 53 | func newConsoleWriter(w io.Writer, format OutputFormat, disableColor bool) *consoleWriter { 54 | if format == 0 { 55 | format = OutputFormatBasic 56 | } 57 | cw := &consoleWriter{ 58 | Writer: w, 59 | format: format, 60 | } 61 | cw.red = noColor() 62 | cw.green = noColor() 63 | cw.yellow = noColor() 64 | 65 | if !disableColor { 66 | // NOTE(mf): GitHub Actions CI env (and probably others) do not have an 67 | // interactive TTY, and tparse through termenv will degrade to the 68 | // "best available option" .. which is no colors. We can work around this by 69 | // setting a color profile explicitly instead of relying on termenv to auto-detect. 70 | // Ref: https://github.com/charmbracelet/lipgloss/issues/74 71 | // Ref: https://github.com/mfridman/tparse/issues/76 72 | lipgloss.SetColorProfile(termenv.TrueColor) 73 | 74 | switch format { 75 | case OutputFormatMarkdown: 76 | cw.green = newMarkdownColor("🟢") 77 | cw.yellow = newMarkdownColor("🟡") 78 | cw.red = newMarkdownColor("🔴") 79 | default: 80 | cw.green = newColor(lipgloss.Color("10")) 81 | cw.yellow = newColor(lipgloss.Color("11")) 82 | cw.red = newColor(lipgloss.Color("9")) 83 | } 84 | } 85 | return cw 86 | } 87 | 88 | func (w *consoleWriter) FormatAction(action parse.Action) string { 89 | s := strings.ToUpper(action.String()) 90 | switch action { 91 | case parse.ActionPass: 92 | return w.green(s) 93 | case parse.ActionSkip: 94 | return w.yellow(s) 95 | case parse.ActionFail: 96 | return w.red(s) 97 | default: 98 | return s 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /tests/testdata/failed/test_02.jsonl: -------------------------------------------------------------------------------- 1 | {"Time":"2022-05-20T09:24:29.849233-04:00","Action":"run","Package":"github.com/mfridman/debug-go/testing","Test":"TestWhatever"} 2 | {"Time":"2022-05-20T09:24:29.849381-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Test":"TestWhatever","Output":"=== RUN TestWhatever\n"} 3 | {"Time":"2022-05-20T09:24:29.849391-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Test":"TestWhatever","Output":" main_test.go:13: assert error\n"} 4 | {"Time":"2022-05-20T09:24:29.8494-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Test":"TestWhatever","Output":" main_test.go:14: \n"} 5 | {"Time":"2022-05-20T09:24:29.849402-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Test":"TestWhatever","Output":" \tError Trace:\tmain_test.go:14\n"} 6 | {"Time":"2022-05-20T09:24:29.849404-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Test":"TestWhatever","Output":" \tError: \t\"does not contain\" does not contain \"ostriche\"\n"} 7 | {"Time":"2022-05-20T09:24:29.849412-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Test":"TestWhatever","Output":" \tTest: \tTestWhatever\n"} 8 | {"Time":"2022-05-20T09:24:29.849435-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Test":"TestWhatever","Output":" main_test.go:15: \n"} 9 | {"Time":"2022-05-20T09:24:29.849556-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Test":"TestWhatever","Output":" \tError Trace:\tmain_test.go:15\n"} 10 | {"Time":"2022-05-20T09:24:29.849562-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Test":"TestWhatever","Output":" \tError: \tReceived unexpected error:\n"} 11 | {"Time":"2022-05-20T09:24:29.849569-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Test":"TestWhatever","Output":" \t \tmake the errorss..\n"} 12 | {"Time":"2022-05-20T09:24:29.849571-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Test":"TestWhatever","Output":" \tTest: \tTestWhatever\n"} 13 | {"Time":"2022-05-20T09:24:30.850912-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Test":"TestWhatever","Output":" main_test.go:17: skdhjfg\n"} 14 | {"Time":"2022-05-20T09:24:30.851026-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Test":"TestWhatever","Output":"--- FAIL: TestWhatever (1.00s)\n"} 15 | {"Time":"2022-05-20T09:24:30.851526-04:00","Action":"fail","Package":"github.com/mfridman/debug-go/testing","Test":"TestWhatever","Elapsed":1} 16 | {"Time":"2022-05-20T09:24:30.851551-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Output":"FAIL\n"} 17 | {"Time":"2022-05-20T09:24:30.854596-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Output":"FAIL\tgithub.com/mfridman/debug-go/testing\t1.113s\n"} 18 | {"Time":"2022-05-20T09:24:30.854827-04:00","Action":"fail","Package":"github.com/mfridman/debug-go/testing","Elapsed":1.114} -------------------------------------------------------------------------------- /parse/package.go: -------------------------------------------------------------------------------- 1 | package parse 2 | 3 | import "time" 4 | 5 | // Package is the representation of a single package being tested. The 6 | // summary field is an event that contains all relevant information about the 7 | // package, namely Package (name), Elapsed and Action (big pass or fail). 8 | type Package struct { 9 | Summary *Event 10 | Tests []*Test 11 | 12 | // StartTime is the time the package started running. This is only available 13 | // in go1.20 and above. 14 | StartTime time.Time 15 | 16 | // NoTestFiles indicates whether the package contains tests: [no test files] 17 | // This only occurs at the package level 18 | NoTestFiles bool 19 | 20 | // NoTests indicates a package contains one or more files with no tests. This doesn't 21 | // necessarily mean the file is empty or that the package doesn't have any tests. 22 | // Unfortunately go test marks the package summary with [no tests to run]. 23 | NoTests bool 24 | // NoTestSlice holds events that contain "testing: warning: no tests to run" and 25 | // a non-empty test name. 26 | NoTestSlice []*Event 27 | 28 | // Cached indicates whether the test result was obtained from the cache. 29 | Cached bool 30 | 31 | // Cover reports whether the package contains coverage (go test run with -cover) 32 | Cover bool 33 | Coverage float64 34 | 35 | // HasPanic marks the entire package as panicked. Game over. 36 | HasPanic bool 37 | // Once a package has been marked HasPanic all subsequent events are added to PanicEvents. 38 | PanicEvents []*Event 39 | 40 | // HasDataRace marks the entire package as having a data race. 41 | HasDataRace bool 42 | // DataRaceTests captures an individual test names as having a data race. 43 | DataRaceTests []string 44 | 45 | // HasFailedBuildOrSetup marks the package as having a failed build or setup. 46 | // Example: [build failed] or [setup failed] 47 | HasFailedBuildOrSetup bool 48 | } 49 | 50 | // newPackage initializes and returns a Package. 51 | func newPackage() *Package { 52 | return &Package{ 53 | Summary: &Event{}, 54 | Tests: []*Test{}, 55 | } 56 | } 57 | 58 | // AddEvent adds the event to a test based on test name. 59 | func (p *Package) AddEvent(event *Event) { 60 | var t *Test 61 | if t = p.GetTest(event.Test); t == nil { 62 | // Test does not exist, add it to pkg. 63 | t = &Test{ 64 | Name: event.Test, 65 | Package: event.Package, 66 | } 67 | p.Tests = append(p.Tests, t) 68 | } 69 | 70 | t.Events = append(t.Events, event) 71 | } 72 | 73 | // GetTest returns a test based on given name, if no test is found 74 | // return nil 75 | func (p *Package) GetTest(name string) *Test { 76 | for _, t := range p.Tests { 77 | if t.Name == name { 78 | return t 79 | } 80 | } 81 | return nil 82 | } 83 | 84 | // TestsByAction returns all tests that identify as one of the following 85 | // actions: pass, skip or fail. 86 | // 87 | // An empty slice if returned if there are no tests. 88 | func (p *Package) TestsByAction(action Action) []*Test { 89 | var tests []*Test 90 | for _, t := range p.Tests { 91 | if t.Status() == action { 92 | tests = append(tests, t) 93 | } 94 | } 95 | return tests 96 | } 97 | -------------------------------------------------------------------------------- /tests/testdata/outcome/test_04.jsonl: -------------------------------------------------------------------------------- 1 | {"Time":"2018-10-15T22:57:27.729679-04:00","Action":"output","Package":"github.com/astromail/rover/tests","Output":"2018/10/15 22:57:27 Replaying from value pointer: {Fid:0 Len:0 Offset:0}\n"} 2 | {"Time":"2018-10-15T22:57:27.730067-04:00","Action":"output","Package":"github.com/astromail/rover/tests","Output":"2018/10/15 22:57:27 Iterating file id: 0\n"} 3 | {"Time":"2018-10-15T22:57:27.730109-04:00","Action":"output","Package":"github.com/astromail/rover/tests","Output":"2018/10/15 22:57:27 Iteration took: 49.09µs\n"} 4 | {"Time":"2018-10-15T22:57:27.732221-04:00","Action":"output","Package":"github.com/astromail/rover/tests","Output":"2018/10/15 22:57:27 Replaying from value pointer: {Fid:0 Len:0 Offset:0}\n"} 5 | {"Time":"2018-10-15T22:57:27.732263-04:00","Action":"output","Package":"github.com/astromail/rover/tests","Output":"2018/10/15 22:57:27 Iterating file id: 0\n"} 6 | {"Time":"2018-10-15T22:57:27.732285-04:00","Action":"output","Package":"github.com/astromail/rover/tests","Output":"2018/10/15 22:57:27 Iteration took: 63.3µs\n"} 7 | {"Time":"2018-10-15T22:57:28.228957-04:00","Action":"run","Package":"github.com/astromail/rover/tests","Test":"TestCatch"} 8 | {"Time":"2018-10-15T22:57:28.229033-04:00","Action":"output","Package":"github.com/astromail/rover/tests","Test":"TestCatch","Output":"=== RUN TestCatch\n"} 9 | {"Time":"2018-10-15T22:57:28.229083-04:00","Action":"run","Package":"github.com/astromail/rover/tests","Test":"TestCatch/catchAndRetrieve"} 10 | {"Time":"2018-10-15T22:57:28.229112-04:00","Action":"output","Package":"github.com/astromail/rover/tests","Test":"TestCatch/catchAndRetrieve","Output":"=== RUN TestCatch/catchAndRetrieve\n"} 11 | {"Time":"2018-10-15T22:57:28.236421-04:00","Action":"output","Package":"github.com/astromail/rover/tests","Test":"TestCatch","Output":"--- PASS: TestCatch (0.01s)\n"} 12 | {"Time":"2018-10-15T22:57:28.236461-04:00","Action":"output","Package":"github.com/astromail/rover/tests","Test":"TestCatch/catchAndRetrieve","Output":" --- PASS: TestCatch/catchAndRetrieve (0.01s)\n"} 13 | {"Time":"2018-10-15T22:57:28.236492-04:00","Action":"pass","Package":"github.com/astromail/rover/tests","Test":"TestCatch/catchAndRetrieve","Elapsed":0.01} 14 | {"Time":"2018-10-15T22:57:28.236516-04:00","Action":"pass","Package":"github.com/astromail/rover/tests","Test":"TestCatch","Elapsed":0.01} 15 | {"Time":"2018-10-15T22:57:28.236568-04:00","Action":"run","Package":"github.com/astromail/rover/tests","Test":"TestNameFormat"} 16 | {"Time":"2018-10-15T22:57:28.236597-04:00","Action":"output","Package":"github.com/astromail/rover/tests","Test":"TestNameFormat","Output":"=== RUN TestNameFormat\n"} 17 | {"Time":"2018-10-15T22:57:28.236633-04:00","Action":"output","Package":"github.com/astromail/rover/tests","Test":"TestNameFormat","Output":"--- PASS: TestNameFormat (0.00s)\n"} 18 | {"Time":"2018-10-15T22:57:28.236657-04:00","Action":"pass","Package":"github.com/astromail/rover/tests","Test":"TestNameFormat","Elapsed":0} 19 | {"Time":"2018-10-15T22:57:28.236673-04:00","Action":"output","Package":"github.com/astromail/rover/tests","Output":"PASS\n"} 20 | {"Time":"2018-10-15T22:57:28.237956-04:00","Action":"output","Package":"github.com/astromail/rover/tests","Output":"ok \tgithub.com/astromail/rover/tests\t0.582s\n"} 21 | {"Time":"2018-10-15T22:57:28.23799-04:00","Action":"pass","Package":"github.com/astromail/rover/tests","Elapsed":0.582} 22 | -------------------------------------------------------------------------------- /tests/follow_test.go: -------------------------------------------------------------------------------- 1 | package parsetest 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "os" 7 | "path/filepath" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | 12 | "github.com/mfridman/tparse/internal/app" 13 | "github.com/mfridman/tparse/internal/utils" 14 | "github.com/mfridman/tparse/parse" 15 | ) 16 | 17 | func TestFollow(t *testing.T) { 18 | t.Parallel() 19 | 20 | t.Run("follow_verbose", func(t *testing.T) { 21 | base := filepath.Join("testdata", "follow-verbose") 22 | 23 | tt := []struct { 24 | fileName string 25 | err error 26 | exitCode int 27 | }{ 28 | // race detected 29 | {"test_01", nil, 1}, 30 | {"test_02", nil, 0}, 31 | {"test_03", nil, 0}, 32 | {"test_04", nil, 0}, 33 | {"test_05", parse.ErrNotParsable, 1}, 34 | // build failure in one package 35 | {"test_06", nil, 2}, 36 | } 37 | for _, tc := range tt { 38 | t.Run(tc.fileName, func(t *testing.T) { 39 | buf := bytes.NewBuffer(nil) 40 | inputFile := filepath.Join(base, tc.fileName+".jsonl") 41 | options := app.Options{ 42 | FileName: inputFile, 43 | FollowOutput: true, 44 | FollowOutputWriter: utils.WriteNopCloser{Writer: buf}, 45 | FollowOutputVerbose: true, 46 | DisableTableOutput: true, 47 | } 48 | gotExitCode, err := app.Run(options) 49 | if err != nil && !errors.Is(err, tc.err) { 50 | t.Fatal(err) 51 | } 52 | assert.Equal(t, gotExitCode, tc.exitCode) 53 | goldenFile := filepath.Join(base, tc.fileName+".golden") 54 | want, err := os.ReadFile(goldenFile) 55 | if err != nil { 56 | t.Fatal(err) 57 | } 58 | checkGolden(t, inputFile, goldenFile, buf.Bytes(), want) 59 | }) 60 | } 61 | }) 62 | 63 | t.Run("follow_no_verbose", func(t *testing.T) { 64 | base := filepath.Join("testdata", "follow") 65 | 66 | tt := []struct { 67 | fileName string 68 | err error 69 | exitCode int 70 | }{ 71 | {"test_01", nil, 0}, 72 | } 73 | for _, tc := range tt { 74 | t.Run(tc.fileName, func(t *testing.T) { 75 | buf := bytes.NewBuffer(nil) 76 | inputFile := filepath.Join(base, tc.fileName+".jsonl") 77 | options := app.Options{ 78 | FileName: inputFile, 79 | FollowOutput: true, 80 | FollowOutputWriter: utils.WriteNopCloser{Writer: buf}, 81 | DisableTableOutput: true, 82 | } 83 | gotExitCode, err := app.Run(options) 84 | if err != nil && !errors.Is(err, tc.err) { 85 | t.Fatal(err) 86 | } 87 | assert.Equal(t, gotExitCode, tc.exitCode) 88 | goldenFile := filepath.Join(base, tc.fileName+".golden") 89 | want, err := os.ReadFile(goldenFile) 90 | if err != nil { 91 | t.Fatal(err) 92 | } 93 | checkGolden(t, inputFile, goldenFile, buf.Bytes(), want) 94 | }) 95 | } 96 | }) 97 | } 98 | 99 | func checkGolden( 100 | t *testing.T, 101 | inputFile, goldenFile string, 102 | got, want []byte, 103 | ) { 104 | t.Helper() 105 | if !bytes.Equal(got, want) { 106 | t.Error("input does not match expected output; diff files in follow dir suffixed with .FAIL to debug") 107 | t.Logf("diff %v %v", 108 | "tests/"+goldenFile+".FAIL", 109 | "tests/"+inputFile+".FAIL", 110 | ) 111 | if err := os.WriteFile(goldenFile+".FAIL", got, 0644); err != nil { 112 | t.Fatal(err) 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /tests/testdata/prescan/test_01.txt: -------------------------------------------------------------------------------- 1 | git.checkout 2 | 0.55s$ git clone --depth=50 --branch=master https://github.com/mfridman/tparse.git mfridman/tparse 3 | Cloning into 'mfridman/tparse'... 4 | remote: Enumerating objects: 485, done. 5 | remote: Counting objects: 100% (485/485), done. 6 | remote: Compressing objects: 100% (310/310), done. 7 | remote: Total 485 (delta 209), reused 442 (delta 168), pack-reused 0 8 | Receiving objects: 100% (485/485), 1.17 MiB | 20.58 MiB/s, done. 9 | Resolving deltas: 100% (209/209), done. 10 | $ cd mfridman/tparse 11 | $ git checkout -qf d7a97f658463e3abd90357d9988575e8138d9a86 12 | 4.38s$ GIMME_OUTPUT="$(gimme 1.10 | tee -a ${TRAVIS_HOME}/.bashrc)" && eval "$GIMME_OUTPUT" 13 | go version go1.10 linux/amd64 14 | $ export GOPATH=${TRAVIS_HOME}/gopath 15 | $ export PATH=${TRAVIS_HOME}/gopath/bin:$PATH 16 | $ mkdir -p ${TRAVIS_HOME}/gopath/src/github.com/mfridman/tparse 17 | $ tar -Pczf ${TRAVIS_TMPDIR}/src_archive.tar.gz -C ${TRAVIS_BUILD_DIR} . && tar -Pxzf ${TRAVIS_TMPDIR}/src_archive.tar.gz -C ${TRAVIS_HOME}/gopath/src/github.com/mfridman/tparse 18 | $ export TRAVIS_BUILD_DIR=${TRAVIS_HOME}/gopath/src/github.com/mfridman/tparse 19 | $ cd ${TRAVIS_HOME}/gopath/src/github.com/mfridman/tparse 20 | 0.00s 21 | $ gimme version 22 | v1.5.3 23 | $ go version 24 | go version go1.10 linux/amd64 25 | go.env 26 | $ go env 27 | GOARCH="amd64" 28 | GOBIN="" 29 | GOCACHE="/home/travis/.cache/go-build" 30 | GOEXE="" 31 | GOHOSTARCH="amd64" 32 | GOHOSTOS="linux" 33 | GOOS="linux" 34 | GOPATH="/home/travis/gopath" 35 | GORACE="" 36 | GOROOT="/home/travis/.gimme/versions/go1.10.linux.amd64" 37 | GOTMPDIR="" 38 | GOTOOLDIR="/home/travis/.gimme/versions/go1.10.linux.amd64/pkg/tool/linux_amd64" 39 | GCCGO="gccgo" 40 | CC="gcc" 41 | CXX="g++" 42 | CGO_ENABLED="1" 43 | CGO_CFLAGS="-g -O2" 44 | CGO_CPPFLAGS="" 45 | CGO_CXXFLAGS="-g -O2" 46 | CGO_FFLAGS="-g -O2" 47 | CGO_LDFLAGS="-g -O2" 48 | PKG_CONFIG="pkg-config" 49 | GOGCCFLAGS="-fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build714003296=/tmp/go-build -gno-record-gcc-switches" 50 | before_install 51 | {"Time":"2018-10-24T13:13:35.445816-04:00","Action":"run","Package":"github.com/mfridman/tparse/tests","Test":"TestStack"} 52 | {"Time":"2018-10-24T13:13:35.447209-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestStack","Output":"=== RUN TestStack\n"} 53 | {"Time":"2018-10-24T13:13:35.44729-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestStack","Output":"=== PAUSE TestStack\n"} 54 | {"Time":"2018-10-24T13:13:35.447302-04:00","Action":"pause","Package":"github.com/mfridman/tparse/tests","Test":"TestStack"} 55 | {"Time":"2018-10-24T13:13:35.447311-04:00","Action":"cont","Package":"github.com/mfridman/tparse/tests","Test":"TestStack"} 56 | {"Time":"2018-10-24T13:13:35.447317-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestStack","Output":"=== CONT TestStack\n"} 57 | {"Time":"2018-10-24T13:13:35.447348-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestStack","Output":"--- PASS: TestStack (0.00s)\n"} 58 | {"Time":"2018-10-24T13:13:35.447394-04:00","Action":"pass","Package":"github.com/mfridman/tparse/tests","Test":"TestStack","Elapsed":0} 59 | {"Time":"2018-10-24T13:13:35.447481-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Output":"PASS\n"} 60 | {"Time":"2018-10-24T13:13:35.447759-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Output":"ok \tgithub.com/mfridman/tparse/tests\t0.014s\n"} 61 | {"Time":"2018-10-24T13:13:35.454438-04:00","Action":"pass","Package":"github.com/mfridman/tparse/tests","Elapsed":0.021} 62 | -------------------------------------------------------------------------------- /tests/testdata/prescan/test_02.txt: -------------------------------------------------------------------------------- 1 | git.checkout 2 | 0.55s$ git clone --depth=50 --branch=master https://github.com/mfridman/tparse.git mfridman/tparse 3 | Cloning into 'mfridman/tparse'... 4 | remote: Enumerating objects: 485, done. 5 | remote: Counting objects: 100% (485/485), done. 6 | remote: Compressing objects: 100% (310/310), done. 7 | remote: Total 485 (delta 209), reused 442 (delta 168), pack-reused 0 8 | Receiving objects: 100% (485/485), 1.17 MiB | 20.58 MiB/s, done. 9 | Resolving deltas: 100% (209/209), done. 10 | $ cd mfridman/tparse 11 | $ git checkout -qf d7a97f658463e3abd90357d9988575e8138d9a86 12 | 4.38s$ GIMME_OUTPUT="$(gimme 1.10 | tee -a ${TRAVIS_HOME}/.bashrc)" && eval "$GIMME_OUTPUT" 13 | go version go1.10 linux/amd64 14 | $ export GOPATH=${TRAVIS_HOME}/gopath 15 | $ export PATH=${TRAVIS_HOME}/gopath/bin:$PATH 16 | $ mkdir -p ${TRAVIS_HOME}/gopath/src/github.com/mfridman/tparse 17 | $ tar -Pczf ${TRAVIS_TMPDIR}/src_archive.tar.gz -C ${TRAVIS_BUILD_DIR} . && tar -Pxzf ${TRAVIS_TMPDIR}/src_archive.tar.gz -C ${TRAVIS_HOME}/gopath/src/github.com/mfridman/tparse 18 | $ export TRAVIS_BUILD_DIR=${TRAVIS_HOME}/gopath/src/github.com/mfridman/tparse 19 | $ cd ${TRAVIS_HOME}/gopath/src/github.com/mfridman/tparse 20 | 0.00s 21 | $ gimme version 22 | v1.5.3 23 | $ go version 24 | go version go1.10 linux/amd64 25 | go.env 26 | $ go env 27 | GOARCH="amd64" 28 | GOBIN="" 29 | GOCACHE="/home/travis/.cache/go-build" 30 | GOEXE="" 31 | GOHOSTARCH="amd64" 32 | GOHOSTOS="linux" 33 | GOOS="linux" 34 | GOPATH="/home/travis/gopath" 35 | GORACE="" 36 | GOROOT="/home/travis/.gimme/versions/go1.10.linux.amd64" 37 | GOTMPDIR="" 38 | GOTOOLDIR="/home/travis/.gimme/versions/go1.10.linux.amd64/pkg/tool/linux_amd64" 39 | GCCGO="gccgo" 40 | CC="gcc" 41 | CXX="g++" 42 | CGO_ENABLED="1" 43 | CGO_CFLAGS="-g -O2" 44 | CGO_CPPFLAGS="" 45 | CGO_CXXFLAGS="-g -O2" 46 | CGO_FFLAGS="-g -O2" 47 | CGO_LDFLAGS="-g -O2" 48 | PKG_CONFIG="pkg-config" 49 | GOGCCFLAGS="-fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build714003296=/tmp/go-build -gno-record-gcc-switches" 50 | before_install 51 | pirate ipsum! 52 | {"Time":"2018-10-24T13:13:35.445816-04:00","Action":"run","Package":"github.com/mfridman/tparse/tests","Test":"TestStack"} 53 | {"Time":"2018-10-24T13:13:35.447209-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestStack","Output":"=== RUN TestStack\n"} 54 | {"Time":"2018-10-24T13:13:35.44729-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestStack","Output":"=== PAUSE TestStack\n"} 55 | {"Time":"2018-10-24T13:13:35.447302-04:00","Action":"pause","Package":"github.com/mfridman/tparse/tests","Test":"TestStack"} 56 | {"Time":"2018-10-24T13:13:35.447311-04:00","Action":"cont","Package":"github.com/mfridman/tparse/tests","Test":"TestStack"} 57 | {"Time":"2018-10-24T13:13:35.447317-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestStack","Output":"=== CONT TestStack\n"} 58 | {"Time":"2018-10-24T13:13:35.447348-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestStack","Output":"--- PASS: TestStack (0.00s)\n"} 59 | {"Time":"2018-10-24T13:13:35.447394-04:00","Action":"pass","Package":"github.com/mfridman/tparse/tests","Test":"TestStack","Elapsed":0} 60 | {"Time":"2018-10-24T13:13:35.447481-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Output":"PASS\n"} 61 | {"Time":"2018-10-24T13:13:35.447759-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Output":"ok \tgithub.com/mfridman/tparse/tests\t0.014s\n"} 62 | {"Time":"2018-10-24T13:13:35.454438-04:00","Action":"pass","Package":"github.com/mfridman/tparse/tests","Elapsed":0.021} -------------------------------------------------------------------------------- /tests/summary_counts_test.go: -------------------------------------------------------------------------------- 1 | package parsetest 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | 10 | "github.com/mfridman/tparse/parse" 11 | ) 12 | 13 | func TestSummaryCounts(t *testing.T) { 14 | t.Parallel() 15 | 16 | // This test depends on metrics_test.jsonl, which contains the output of 9 std lib packages: 17 | // go test -count=1 fmt strings bytes bufio crypto log mime sort time -json 18 | 19 | fileName := "./testdata/metrics_test.jsonl" 20 | f, err := os.Open(fileName) 21 | require.NoError(t, err) 22 | 23 | summary, err := parse.Process(f) 24 | require.NoError(t, err) 25 | assert.Len(t, summary.Packages, 9) 26 | 27 | for name, pkg := range summary.Packages { 28 | if pkg.Summary == nil { 29 | t.Fatalf("package %q cannot contain nil summary", name) 30 | } 31 | if pkg.Summary.Action != parse.ActionPass { 32 | t.Logf("failed pkg: %v", name) 33 | t.Fatalf("unexpected action %q, want %q", pkg.Summary.Action, parse.ActionPass) 34 | } 35 | } 36 | 37 | tests := []struct { 38 | name string 39 | total, passed, skipped, failed int 40 | elapsed float64 41 | }{ 42 | {"fmt", 59, 58, 1, 0, 0.22}, 43 | {"strings", 107, 107, 0, 0, 5.494}, 44 | {"bytes", 123, 123, 0, 0, 3.5380000000000003}, 45 | {"bufio", 69, 69, 0, 0, 0.07}, 46 | {"crypto", 5, 5, 0, 0, 0.016}, 47 | {"log", 8, 8, 0, 0, 0.085}, 48 | {"mime", 20, 20, 0, 0, 0.025}, 49 | {"sort", 37, 36, 1, 0, 3.117}, 50 | {"time", 118, 117, 1, 0, 7.157}, 51 | } 52 | 53 | for _, test := range tests { 54 | t.Run(test.name+"_test", func(t *testing.T) { 55 | pkg := summary.Packages[test.name] 56 | if len(pkg.Tests) != test.total { 57 | t.Fatalf("got %d total tests in package %q, want %d total tests", len(pkg.Tests), test.name, test.total) 58 | } 59 | 60 | pa := pkg.TestsByAction(parse.ActionPass) 61 | if len(pa) != test.passed { 62 | t.Fatalf("got %d passed tests in package %q, want %d passed tests", len(pa), test.name, test.passed) 63 | } 64 | 65 | sk := pkg.TestsByAction(parse.ActionSkip) 66 | if len(sk) != test.skipped { 67 | t.Fatalf("got %d passed tests in package %q, want %d passed tests", len(sk), test.name, test.skipped) 68 | } 69 | 70 | fa := pkg.TestsByAction(parse.ActionFail) 71 | if len(fa) != test.failed { 72 | t.Fatalf("got %d failed tests in package %q, want %d failed tests", len(fa), test.name, test.failed) 73 | } 74 | 75 | if pkg.Summary.Elapsed != test.elapsed { 76 | t.Fatalf("got elapsed time %f for package %q, want %f", pkg.Summary.Elapsed, test.name, test.elapsed) 77 | } 78 | }) 79 | } 80 | } 81 | 82 | func TestElapsed(t *testing.T) { 83 | t.Parallel() 84 | 85 | // This test depends on elapsed_test.jsonl, which contains the output of 2 std lib tests 86 | // with known elapsed time. 87 | // go test -count=1 strings -run="^(TestCompareStrings|TestCaseConsistency$)" -json -cover 88 | 89 | expected := map[string]float64{ 90 | "TestCompareStrings": 3.49, 91 | "TestCaseConsistency": 0.17, 92 | } 93 | 94 | fileName := "./testdata/elapsed_test.jsonl" 95 | f, err := os.Open(fileName) 96 | require.NoError(t, err) 97 | defer f.Close() 98 | 99 | summary, err := parse.Process(f) 100 | require.NoError(t, err) 101 | assert.Len(t, summary.Packages, 1) 102 | 103 | pkg, ok := summary.Packages["strings"] 104 | if !ok { 105 | t.Fatalf(`got unexpected pkg: %v\nwant "strings"`, pkg) 106 | } 107 | assert.Len(t, pkg.Tests, 2) 108 | 109 | for _, test := range pkg.Tests { 110 | wantElapsed, ok := expected[test.Name] 111 | if !ok { 112 | t.Errorf("got unknown test name %q", test.Name) 113 | } 114 | if test.Elapsed() != wantElapsed { 115 | t.Errorf("got %v elapsed time for test: %q, want %v", test.Elapsed(), test.Name, wantElapsed) 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= 2 | github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= 3 | github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= 4 | github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= 5 | github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40= 6 | github.com/charmbracelet/colorprofile v0.3.1/go.mod h1:/GkGusxNs8VB/RSOh3fu0TJmQ4ICMMPApIIVn0KszZ0= 7 | github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= 8 | github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= 9 | github.com/charmbracelet/x/ansi v0.9.2 h1:92AGsQmNTRMzuzHEYfCdjQeUzTrgE1vfO5/7fEVoXdY= 10 | github.com/charmbracelet/x/ansi v0.9.2/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= 11 | github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k= 12 | github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= 13 | github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30= 14 | github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= 15 | github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= 16 | github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= 17 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 18 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 19 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 20 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 21 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 22 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 23 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 24 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 25 | github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= 26 | github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= 27 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 28 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 29 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 30 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 31 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 32 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 33 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 34 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= 35 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= 36 | golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= 37 | golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= 38 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 39 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 40 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 41 | golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= 42 | golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= 43 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 44 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 45 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 46 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 47 | -------------------------------------------------------------------------------- /tests/testdata/race/test_06.jsonl: -------------------------------------------------------------------------------- 1 | {"Time":"2022-05-23T10:22:27.087816-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Output":"testing: warning: no tests to run\n"} 2 | {"Time":"2022-05-23T10:22:27.088274-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Output":"PASS\n"} 3 | {"Time":"2022-05-23T10:22:27.088645-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Output":"2\n"} 4 | {"Time":"2022-05-23T10:22:27.089042-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Output":"==================\n"} 5 | {"Time":"2022-05-23T10:22:27.089065-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Output":"WARNING: DATA RACE\n"} 6 | {"Time":"2022-05-23T10:22:27.08908-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Output":"Read at 0x00c0000b0178 by goroutine 7:\n"} 7 | {"Time":"2022-05-23T10:22:27.089102-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Output":" github.com/mfridman/debug-go/testing_test.TestMain.func1()\n"} 8 | {"Time":"2022-05-23T10:22:27.089119-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Output":" /Users/mfridman/src/github.com/mfridman/debug-go/testing/main_test.go:16 +0x3c\n"} 9 | {"Time":"2022-05-23T10:22:27.089138-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Output":"\n"} 10 | {"Time":"2022-05-23T10:22:27.089151-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Output":"Previous write at 0x00c0000b0178 by main goroutine:\n"} 11 | {"Time":"2022-05-23T10:22:27.089163-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Output":" github.com/mfridman/debug-go/testing_test.TestMain()\n"} 12 | {"Time":"2022-05-23T10:22:27.089175-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Output":" /Users/mfridman/src/github.com/mfridman/debug-go/testing/main_test.go:14 +0x98\n"} 13 | {"Time":"2022-05-23T10:22:27.089187-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Output":" main.main()\n"} 14 | {"Time":"2022-05-23T10:22:27.089198-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Output":" _testmain.go:47 +0x304\n"} 15 | {"Time":"2022-05-23T10:22:27.08921-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Output":"\n"} 16 | {"Time":"2022-05-23T10:22:27.089222-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Output":"Goroutine 7 (running) created at:\n"} 17 | {"Time":"2022-05-23T10:22:27.089256-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Output":" github.com/mfridman/debug-go/testing_test.TestMain()\n"} 18 | {"Time":"2022-05-23T10:22:27.089269-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Output":" /Users/mfridman/src/github.com/mfridman/debug-go/testing/main_test.go:15 +0x80\n"} 19 | {"Time":"2022-05-23T10:22:27.089282-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Output":" main.main()\n"} 20 | {"Time":"2022-05-23T10:22:27.089293-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Output":" _testmain.go:47 +0x304\n"} 21 | {"Time":"2022-05-23T10:22:27.089305-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Output":"==================\n"} 22 | {"Time":"2022-05-23T10:22:27.089317-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Output":"2\n"} 23 | {"Time":"2022-05-23T10:22:27.089338-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Output":"2\n"} 24 | {"Time":"2022-05-23T10:22:27.08935-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Output":"4\n"} 25 | {"Time":"2022-05-23T10:22:27.089364-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Output":"5\n"} 26 | {"Time":"2022-05-23T10:22:27.10081-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Output":"Found 1 data race(s)\n"} 27 | {"Time":"2022-05-23T10:22:27.101697-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Output":"FAIL\tgithub.com/mfridman/debug-go/testing\t0.173s\n"} 28 | {"Time":"2022-05-23T10:22:27.101757-04:00","Action":"fail","Package":"github.com/mfridman/debug-go/testing","Elapsed":0.173} 29 | -------------------------------------------------------------------------------- /tests/outcome_test.go: -------------------------------------------------------------------------------- 1 | package parsetest 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | 11 | "github.com/mfridman/tparse/parse" 12 | ) 13 | 14 | func TestFinalOutcome(t *testing.T) { 15 | t.Parallel() 16 | // key is the package name, action reports the final outcome of the package. 17 | type registry map[string]parse.Action 18 | 19 | base := filepath.Join("testdata", "outcome") 20 | 21 | tt := []struct { 22 | fileName string 23 | exitCode int 24 | registry 25 | }{ 26 | {"test_01.jsonl", 1, registry{ 27 | "github.com/mfridman/tparse/tests": parse.ActionFail, 28 | }}, 29 | {"test_02.jsonl", 1, registry{ 30 | "github.com/astromail/rover/tests": parse.ActionFail, 31 | "github.com/astromail/rover/cmd/roverd": parse.ActionPass, 32 | "github.com/astromail/rover/smtp": parse.ActionPass, 33 | "github.com/astromail/rover/storage": parse.ActionPass, 34 | "github.com/astromail/rover/errors": parse.ActionPass, 35 | "github.com/astromail/rover/storage/badger": parse.ActionPass, 36 | "github.com/astromail/rover": parse.ActionPass, 37 | }}, 38 | {"test_03.jsonl", 0, registry{ 39 | "fmt": parse.ActionPass, 40 | }}, 41 | {"test_04.jsonl", 0, registry{ 42 | "github.com/astromail/rover/tests": parse.ActionPass, 43 | }}, 44 | {"test_05.jsonl", 0, registry{ 45 | "github.com/astromail/rover/tests": parse.ActionPass, 46 | }}, 47 | {"test_06.jsonl", 0, registry{ 48 | "fmt": parse.ActionPass, 49 | }}, 50 | {"test_07.jsonl", 0, registry{ 51 | "debug/errorcause": parse.ActionPass, 52 | }}, 53 | {"test_08.jsonl", 0, registry{ 54 | "github.com/awesome/pkg": parse.ActionPass, 55 | }}, 56 | } 57 | for _, tc := range tt { 58 | t.Run(tc.fileName, func(t *testing.T) { 59 | f, err := os.Open(filepath.Join(base, tc.fileName)) 60 | if err != nil { 61 | t.Fatal(err) 62 | } 63 | summary, err := parse.Process(f) 64 | require.NoError(t, err) 65 | assert.Len(t, summary.Packages, len(tc.registry)) 66 | assert.Equal(t, summary.ExitCode(), tc.exitCode) 67 | 68 | for name, pkg := range summary.Packages { 69 | want, ok := tc.registry[name] 70 | if !ok { 71 | t.Log("currently registered packages:") 72 | for k := range tc.registry { 73 | t.Log(k) 74 | } 75 | t.Fatalf("got unmapped package name %q. Check input file and record all unique package names in registry", name) 76 | } 77 | if pkg.Summary.Action != want { 78 | t.Fatalf("failed package summary action: got: %q, want: %q", pkg.Summary.Action, want) 79 | } 80 | if len(pkg.Tests) == 0 && pkg.Summary.Action != parse.ActionPass { 81 | t.Fatalf("zero test should always return pass: got: %q, want: %q", pkg.Summary.Action, parse.ActionPass) 82 | } 83 | if (pkg.NoTestFiles || pkg.NoTests) && pkg.Summary.Action != parse.ActionPass { 84 | t.Fatalf("packages marked as [no tests to run] or [no test files] should always return pass: got: %q, want: %q", 85 | pkg.Summary.Action, 86 | parse.ActionPass, 87 | ) 88 | } 89 | 90 | // As a sanity check, iterate over the tests and make sure all tests actually 91 | // reflect the package outcome. 92 | 93 | switch pkg.Summary.Action { 94 | case parse.ActionPass: 95 | // One or more tests must be marked as either pass or skip. 96 | // A single skipped test will still yield a pass package outcome. 97 | for _, test := range pkg.Tests { 98 | switch test.Status() { 99 | case parse.ActionPass, parse.ActionSkip: 100 | continue 101 | default: 102 | t.Fatalf("all tests within a passed package should have a status of pass or skip: got: %q", test.Status()) 103 | } 104 | } 105 | case parse.ActionFail: 106 | // One or more tests must be marked as failed. 107 | var failed bool 108 | for _, tc := range pkg.Tests { 109 | if tc.Status() == parse.ActionFail { 110 | failed = true 111 | break 112 | } 113 | } 114 | if !failed { 115 | t.Fatalf("got no failed tests, want one or more tests to be marked as: %q", parse.ActionFail) 116 | } 117 | default: 118 | // Catch all, should never get this. 119 | t.Fatalf("got package summary action %q, want pass or fail", pkg.Summary.Action) 120 | } 121 | } 122 | }) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /internal/app/app.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "os" 8 | 9 | "github.com/mfridman/tparse/parse" 10 | ) 11 | 12 | type Options struct { 13 | // Output is used to write the final output, such as the tables, summary, etc. 14 | Output io.Writer 15 | // DisableColor will disable all colors. 16 | DisableColor bool 17 | // Format will set the output format for tables. 18 | Format OutputFormat 19 | // Sorter will set the sort order for the table. 20 | Sorter parse.PackageSorter 21 | // ShowNoTests will display packages containing no test files or empty test files. 22 | ShowNoTests bool 23 | // FileName will read test output from a file. 24 | FileName string 25 | 26 | // Test table options 27 | TestTableOptions TestTableOptions 28 | SummaryTableOptions SummaryTableOptions 29 | 30 | // FollowOutput will follow the raw output as go test is running. 31 | FollowOutput bool // Output to stdout 32 | FollowOutputWriter io.WriteCloser // Output to a file, takes precedence over FollowOutput 33 | FollowOutputVerbose bool 34 | 35 | // Progress will print a single summary line for each package once the package has completed. 36 | // Useful for long running test suites. Maybe used with FollowOutput or on its own. 37 | // 38 | // This will output to stdout. 39 | Progress bool 40 | ProgressOutput io.Writer 41 | 42 | // DisableTableOutput will disable all table output. This is used for testing. 43 | DisableTableOutput bool 44 | 45 | // 46 | // Experimental 47 | // 48 | 49 | // Compare includes a diff of a previous test output file in the summary table. 50 | Compare string 51 | 52 | // Used with FollowOutput, when enabled it would include timestamp with log lines 53 | IncludeTimestamp bool 54 | } 55 | 56 | func Run(option Options) (int, error) { 57 | var reader io.ReadCloser 58 | var err error 59 | if option.FileName != "" { 60 | if reader, err = os.Open(option.FileName); err != nil { 61 | return 1, err 62 | } 63 | } else { 64 | if reader, err = newPipeReader(); err != nil { 65 | return 1, errors.New("stdin must be a pipe, or use -file to open a go test output file") 66 | } 67 | } 68 | defer reader.Close() 69 | 70 | if option.FollowOutputWriter != nil { 71 | defer option.FollowOutputWriter.Close() 72 | } 73 | 74 | progressWriter := newConsoleWriter(option.Output, option.Format, option.DisableColor) 75 | summary, err := parse.Process( 76 | reader, 77 | parse.WithFollowOutput(option.FollowOutput), 78 | parse.WithFollowVersboseOutput(option.FollowOutputVerbose), 79 | parse.WithWriter(option.FollowOutputWriter), 80 | parse.WithProgress(option.Progress), 81 | parse.WithProgressOutput(progressWriter), 82 | parse.WithIncludeTimestamp(option.IncludeTimestamp), 83 | ) 84 | if err != nil { 85 | return 1, err 86 | } 87 | if len(summary.Packages) == 0 { 88 | return 1, fmt.Errorf("found no go test packages") 89 | } 90 | // Useful for tests that don't need tparse table output. Very useful for testing output from 91 | // [parse.Process] 92 | if !option.DisableTableOutput { 93 | display(option.Output, summary, option) 94 | } 95 | return summary.ExitCode(), nil 96 | } 97 | 98 | func newPipeReader() (io.ReadCloser, error) { 99 | finfo, err := os.Stdin.Stat() 100 | if err != nil { 101 | return nil, err 102 | } 103 | // Check file mode bits to test for named pipe as stdin. 104 | if finfo.Mode()&os.ModeNamedPipe != 0 { 105 | return os.Stdin, nil 106 | } 107 | return nil, errors.New("stdin must be a pipe") 108 | } 109 | 110 | func display(w io.Writer, summary *parse.GoTestSummary, option Options) { 111 | // Best effort to open the compare against file, if it exists. 112 | var warnings []string 113 | defer func() { 114 | for _, w := range warnings { 115 | fmt.Fprintf(os.Stderr, "warning: %s\n", w) 116 | } 117 | }() 118 | var against *parse.GoTestSummary 119 | if option.Compare != "" { 120 | // TODO(mf): cleanup, this is messy. 121 | f, err := os.Open(option.Compare) 122 | if err != nil { 123 | warnings = append(warnings, fmt.Sprintf("failed to open against file: %s", option.Compare)) 124 | } else { 125 | defer f.Close() 126 | against, err = parse.Process(f) 127 | if err != nil { 128 | warnings = append(warnings, fmt.Sprintf("failed to parse against file: %s", option.Compare)) 129 | } 130 | } 131 | } 132 | 133 | cw := newConsoleWriter(w, option.Format, option.DisableColor) 134 | // Sort packages by name ASC. 135 | packages := summary.GetSortedPackages(option.Sorter) 136 | // Only print the tests table if either pass or skip is true. 137 | if option.TestTableOptions.Pass || option.TestTableOptions.Skip { 138 | if option.Format == OutputFormatMarkdown { 139 | cw.testsTableMarkdown(packages, option.TestTableOptions) 140 | } else { 141 | cw.testsTable(packages, option.TestTableOptions) 142 | } 143 | } 144 | // Failures (if any) and summary table are always printed. 145 | cw.printFailed(packages) 146 | cw.summaryTable(packages, option.ShowNoTests, option.SummaryTableOptions, against) 147 | } 148 | -------------------------------------------------------------------------------- /tests/testdata/panic/test_04.jsonl: -------------------------------------------------------------------------------- 1 | {"Time":"2018-10-21T23:42:51.496472-04:00","Action":"output","Package":"github.com/mfridman/tparse","Output":"? \tgithub.com/mfridman/tparse\t[no test files]\n"} 2 | {"Time":"2018-10-21T23:42:51.496734-04:00","Action":"skip","Package":"github.com/mfridman/tparse","Elapsed":0} 3 | {"Time":"2018-10-21T23:42:51.49677-04:00","Action":"output","Package":"github.com/mfridman/tparse/ignore","Output":"? \tgithub.com/mfridman/tparse/ignore\t[no test files]\n"} 4 | {"Time":"2018-10-21T23:42:51.496782-04:00","Action":"skip","Package":"github.com/mfridman/tparse/ignore","Elapsed":0} 5 | {"Time":"2018-10-21T23:42:51.496805-04:00","Action":"output","Package":"github.com/mfridman/tparse/parse","Output":"? \tgithub.com/mfridman/tparse/parse\t[no test files]\n"} 6 | {"Time":"2018-10-21T23:42:51.496813-04:00","Action":"skip","Package":"github.com/mfridman/tparse/parse","Elapsed":0} 7 | {"Time":"2018-10-21T23:42:51.673696-04:00","Action":"run","Package":"github.com/mfridman/tparse/tests","Test":"TestNewEvent"} 8 | {"Time":"2018-10-21T23:42:51.673742-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestNewEvent","Output":"=== RUN TestNewEvent\n"} 9 | {"Time":"2018-10-21T23:42:51.673772-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestNewEvent","Output":"=== PAUSE TestNewEvent\n"} 10 | {"Time":"2018-10-21T23:42:51.673795-04:00","Action":"pause","Package":"github.com/mfridman/tparse/tests","Test":"TestNewEvent"} 11 | {"Time":"2018-10-21T23:42:51.673815-04:00","Action":"run","Package":"github.com/mfridman/tparse/tests","Test":"TestPanicEvent"} 12 | {"Time":"2018-10-21T23:42:51.673834-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestPanicEvent","Output":"=== RUN TestPanicEvent\n"} 13 | {"Time":"2018-10-21T23:42:51.674274-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestPanicEvent","Output":"--- PASS: TestPanicEvent (0.00s)\n"} 14 | {"Time":"2018-10-21T23:42:51.674295-04:00","Action":"pass","Package":"github.com/mfridman/tparse/tests","Test":"TestPanicEvent","Elapsed":0} 15 | {"Time":"2018-10-21T23:42:51.674307-04:00","Action":"run","Package":"github.com/mfridman/tparse/tests","Test":"TestStack"} 16 | {"Time":"2018-10-21T23:42:51.674314-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestStack","Output":"=== RUN TestStack\n"} 17 | {"Time":"2018-10-21T23:42:51.674328-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestStack","Output":"--- FAIL: TestStack (0.00s)\n"} 18 | {"Time":"2018-10-21T23:42:51.676397-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestStack","Output":"panic: oops [recovered]\n"} 19 | {"Time":"2018-10-21T23:42:51.676427-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestStack","Output":"\tpanic: oops\n"} 20 | {"Time":"2018-10-21T23:42:51.676437-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestStack","Output":"\n"} 21 | {"Time":"2018-10-21T23:42:51.676453-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestStack","Output":"goroutine 20 [running]:\n"} 22 | {"Time":"2018-10-21T23:42:51.676462-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestStack","Output":"testing.tRunner.func1(0xc0000b4600)\n"} 23 | {"Time":"2018-10-21T23:42:51.676489-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestStack","Output":"\t/usr/local/go/src/testing/testing.go:792 +0x387\n"} 24 | {"Time":"2018-10-21T23:42:51.676501-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestStack","Output":"panic(0x112ad60, 0x1182f90)\n"} 25 | {"Time":"2018-10-21T23:42:51.67651-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestStack","Output":"\t/usr/local/go/src/runtime/panic.go:513 +0x1b9\n"} 26 | {"Time":"2018-10-21T23:42:51.676523-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestStack","Output":"github.com/mfridman/tparse/tests_test.TestStack(0xc0000b4600)\n"} 27 | {"Time":"2018-10-21T23:42:51.676542-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestStack","Output":"\t/Users/michael.fridman/go/src/github.com/mfridman/tparse/tests/stack_test.go:12 +0x39\n"} 28 | {"Time":"2018-10-21T23:42:51.676555-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestStack","Output":"testing.tRunner(0xc0000b4600, 0x116a730)\n"} 29 | {"Time":"2018-10-21T23:42:51.676585-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestStack","Output":"\t/usr/local/go/src/testing/testing.go:827 +0xbf\n"} 30 | {"Time":"2018-10-21T23:42:51.676596-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestStack","Output":"created by testing.(*T).Run\n"} 31 | {"Time":"2018-10-21T23:42:51.67666-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestStack","Output":"\t/usr/local/go/src/testing/testing.go:878 +0x353\n"} 32 | {"Time":"2018-10-21T23:42:51.676943-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestStack","Output":"FAIL\tgithub.com/mfridman/tparse/tests\t0.016s\n"} 33 | {"Time":"2018-10-21T23:42:51.676966-04:00","Action":"fail","Package":"github.com/mfridman/tparse/tests","Test":"TestStack","Elapsed":0.016} -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project 6 | adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | - Update dependencies to latest versions 11 | - Include failed test names in table (#149) 12 | - Colorize the status in `--progress` output (#150) 13 | 14 | ## [v0.18.0] - 2025-08-24 15 | 16 | - Wrap panic messages at the terminal width (#142) 17 | - Do not include packages with no coverage in the output (#144) 18 | 19 | ## [v0.17.0] 20 | 21 | - Deprecate github.com/mfridman/buildversion, and use std lib `debug.ReadBuildInfo()` instead. In 22 | go1.24 this is handled automatically, from the [release notes](https://go.dev/doc/go1.24): 23 | 24 | > The go build command now sets the main module’s version in the compiled binary based on the 25 | > version control system tag and/or commit. A +dirty suffix will be appended if there are 26 | > uncommitted changes. Use the -buildvcs=false flag to omit version control information from the 27 | > binary. 28 | 29 | - Handle changes in go1.24 related to build output. `tparse` will pipe the build output to stderr 30 | 31 | > Furthermore, `go test -json` now reports build output and failures in JSON, interleaved with 32 | > test result JSON. These are distinguished by new Action types, but if they cause problems in a 33 | > test integration system, you can revert to the text build output with GODEBUG setting 34 | > gotestjsonbuildtext=1. 35 | 36 | ## [v0.16.0] 37 | 38 | - Add a `-follow-output` flag to allow writing go test output directly into a file. This will be 39 | useful (especially in CI jobs) for outputting overly verbose testing output into a file instead of 40 | the standard stream. (#134) 41 | 42 | | flag combination | `go test` output destination | 43 | | ------------------------ | ---------------------------- | 44 | | No flags | Discard output | 45 | | `-follow` | Write to stdout | 46 | | `-follow-output` | Write to file | 47 | | `-follow -follow-output` | Write to file | 48 | 49 | - Use [charmbracelet/lipgloss](https://github.com/charmbracelet/lipgloss) for table rendering. 50 | - This will allow for more control over the output and potentially more features in the future. 51 | (#136) 52 | - Minor changes to the output format are expected, but the overall content should remain the same. 53 | If you have any feedback, please let me know. 54 | 55 | ## [v0.15.0] 56 | 57 | - Add `-trimpath` flag, which removes the path prefix from package names in the output, simplifying 58 | their display. See #128 for examples. 59 | - There's a special case for `-trimpath=auto` which will automatically determine the prefix based 60 | on the longest common prefix of all package paths. 61 | 62 | ## [v0.14.0] 63 | 64 | - Modify `--follow` behavior by minimizing noisy output. (#122) 65 | 66 | > [!TIP] 67 | > 68 | > If you want the existing behavior, I added a `--follow-verbose` flag. But please do let me know if 69 | > this affected you, as I plan to remove this before cutting a `v1.0.0`. Thank you! 70 | 71 | ## [v0.13.3] 72 | 73 | - General housekeeping and dependency updates. 74 | 75 | ## [v0.13.2] 76 | 77 | - Add partial support for `-compare`. A feature that displays the coverage difference against a 78 | previous run. See description for more details 79 | https://github.com/mfridman/tparse/pull/101#issue-1857786730 and the initial issue #92. 80 | - Fix unstable common package prefix logic #104 81 | 82 | ## [v0.13.1] - 2023-08-04 83 | 84 | - Fix failing GoReleaser GitHub action (release notes location). 85 | 86 | Summary from [v0.13.0](https://github.com/mfridman/tparse/releases/tag/v0.13.0) 87 | 88 | - Start a [CHANGELOG.md](https://github.com/mfridman/tparse/blob/main/CHANGELOG.md) for user-facing 89 | change. 90 | - Add [GoReleaser](https://goreleaser.com/) to automate the release process. Pre-built binaries are 91 | available for each release, currently Linux and macOS. If there is demand, can also add Windows. 92 | 93 | ## [v0.13.0] - 2023-08-04 94 | 95 | - Start a [CHANGELOG.md](https://github.com/mfridman/tparse/blob/main/CHANGELOG.md) for user-facing 96 | change. 97 | - Add [GoReleaser](https://goreleaser.com/) to automate the release process. Pre-built binaries are 98 | available for each release, currently Linux and macOS. If there is demand, can also add Windows. 99 | 100 | [Unreleased]: https://github.com/mfridman/tparse/compare/v0.18.0...HEAD 101 | [v0.18.0]: https://github.com/mfridman/tparse/compare/v0.17.0...v0.18.0 102 | [v0.17.0]: https://github.com/mfridman/tparse/compare/v0.16.0...v0.17.0 103 | [v0.16.0]: https://github.com/mfridman/tparse/compare/v0.15.0...v0.16.0 104 | [v0.15.0]: https://github.com/mfridman/tparse/compare/v0.14.0...v0.15.0 105 | [v0.14.0]: https://github.com/mfridman/tparse/compare/v0.13.3...v0.14.0 106 | [v0.13.3]: https://github.com/mfridman/tparse/compare/v0.13.2...v0.13.3 107 | [v0.13.2]: https://github.com/mfridman/tparse/compare/v0.13.1...v0.13.2 108 | [v0.13.1]: https://github.com/mfridman/tparse/compare/v0.13.0...v0.13.1 109 | [v0.13.0]: https://github.com/mfridman/tparse/releases/tag/v0.13.0 110 | -------------------------------------------------------------------------------- /tests/testdata/panic/test_03.jsonl: -------------------------------------------------------------------------------- 1 | {"Time":"2018-10-21T22:15:24.47322-04:00","Action":"run","Package":"github.com/mfridman/tparse/tests","Test":"TestStatus"} 2 | {"Time":"2018-10-21T22:15:24.473515-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestStatus","Output":"=== RUN TestStatus\n"} 3 | {"Time":"2018-10-21T22:15:24.473542-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestStatus","Output":"=== PAUSE TestStatus\n"} 4 | {"Time":"2018-10-21T22:15:24.47355-04:00","Action":"pause","Package":"github.com/mfridman/tparse/tests","Test":"TestStatus"} 5 | {"Time":"2018-10-21T22:15:24.473565-04:00","Action":"cont","Package":"github.com/mfridman/tparse/tests","Test":"TestStatus"} 6 | {"Time":"2018-10-21T22:15:24.473573-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestStatus","Output":"=== CONT TestStatus\n"} 7 | {"Time":"2018-10-21T22:15:24.473588-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestStatus","Output":"--- FAIL: TestStatus (0.00s)\n"} 8 | {"Time":"2018-10-21T22:15:24.47549-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestStatus","Output":"panic: runtime error: invalid memory address or nil pointer dereference [recovered]\n"} 9 | {"Time":"2018-10-21T22:15:24.475513-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestStatus","Output":"\tpanic: runtime error: invalid memory address or nil pointer dereference\n"} 10 | {"Time":"2018-10-21T22:15:24.475532-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestStatus","Output":"[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x1112389]\n"} 11 | {"Time":"2018-10-21T22:15:24.47554-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestStatus","Output":"\n"} 12 | {"Time":"2018-10-21T22:15:24.475549-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestStatus","Output":"goroutine 18 [running]:\n"} 13 | {"Time":"2018-10-21T22:15:24.475559-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestStatus","Output":"testing.tRunner.func1(0xc0000b6300)\n"} 14 | {"Time":"2018-10-21T22:15:24.475567-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestStatus","Output":"\t/usr/local/go/src/testing/testing.go:792 +0x387\n"} 15 | {"Time":"2018-10-21T22:15:24.475581-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestStatus","Output":"panic(0x1137980, 0x1262100)\n"} 16 | {"Time":"2018-10-21T22:15:24.475651-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestStatus","Output":"\t/usr/local/go/src/runtime/panic.go:513 +0x1b9\n"} 17 | {"Time":"2018-10-21T22:15:24.475682-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestStatus","Output":"github.com/mfridman/tparse/tests_test.TestStatus.func1(0x116177e, 0xe, 0x1185120, 0xc00006c820, 0x0, 0x0, 0x0, 0xc00002e6c0)\n"} 18 | {"Time":"2018-10-21T22:15:24.475695-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestStatus","Output":"\t/Users/michael.fridman/go/src/github.com/mfridman/tparse/tests/status_test.go:26 +0x69\n"} 19 | {"Time":"2018-10-21T22:15:24.475749-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestStatus","Output":"path/filepath.walk(0x116177e, 0xe, 0x1185120, 0xc00006c820, 0xc0000666a0, 0x0, 0x10)\n"} 20 | {"Time":"2018-10-21T22:15:24.475773-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestStatus","Output":"\t/usr/local/go/src/path/filepath/path.go:362 +0xf6\n"} 21 | {"Time":"2018-10-21T22:15:24.475781-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestStatus","Output":"path/filepath.Walk(0x116177e, 0xe, 0xc0000666a0, 0x1c338b20, 0xf815f)\n"} 22 | {"Time":"2018-10-21T22:15:24.475788-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestStatus","Output":"\t/usr/local/go/src/path/filepath/path.go:404 +0x105\n"} 23 | {"Time":"2018-10-21T22:15:24.475798-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestStatus","Output":"github.com/mfridman/tparse/tests_test.TestStatus(0xc0000b6300)\n"} 24 | {"Time":"2018-10-21T22:15:24.475936-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestStatus","Output":"\t/Users/michael.fridman/go/src/github.com/mfridman/tparse/tests/status_test.go:19 +0x7e\n"} 25 | {"Time":"2018-10-21T22:15:24.475945-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestStatus","Output":"testing.tRunner(0xc0000b6300, 0x116ab18)\n"} 26 | {"Time":"2018-10-21T22:15:24.475952-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestStatus","Output":"\t/usr/local/go/src/testing/testing.go:827 +0xbf\n"} 27 | {"Time":"2018-10-21T22:15:24.475959-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestStatus","Output":"created by testing.(*T).Run\n"} 28 | {"Time":"2018-10-21T22:15:24.475975-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestStatus","Output":"\t/usr/local/go/src/testing/testing.go:878 +0x353\n"} 29 | {"Time":"2018-10-21T22:15:24.476216-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestStatus","Output":"FAIL\tgithub.com/mfridman/tparse/tests\t0.014s\n"} 30 | {"Time":"2018-10-21T22:15:24.476261-04:00","Action":"fail","Package":"github.com/mfridman/tparse/tests","Test":"TestStatus","Elapsed":0.014} -------------------------------------------------------------------------------- /tests/sort_test.go: -------------------------------------------------------------------------------- 1 | package parsetest 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | 11 | "github.com/mfridman/tparse/parse" 12 | ) 13 | 14 | func TestSortName(t *testing.T) { 15 | t.Parallel() 16 | 17 | base := filepath.Join("testdata", "cover") 18 | 19 | // expected package name and corresponding cover % 20 | // Note, these numbers will vary between on go versions. 21 | type expected []string 22 | 23 | tt := []struct { 24 | fileName string 25 | expected 26 | }{ 27 | { 28 | // go test -count=1 bytes log sort -json -cover | tparse -sort name 29 | "test_01", 30 | expected{ 31 | "bytes", 32 | "log", 33 | "sort", 34 | }, 35 | }, 36 | { 37 | // go test -count=1 bufio bytes crypto fmt log mime net sort strings time -json -cover | tparse -sort name 38 | "test_02", 39 | expected{ 40 | "bufio", 41 | "bytes", 42 | "crypto", 43 | "fmt", 44 | "log", 45 | "mime", 46 | "net", 47 | "sort", 48 | "strings", 49 | "time", 50 | }, 51 | }, 52 | } 53 | for _, tc := range tt { 54 | t.Run(tc.fileName, func(t *testing.T) { 55 | inputFile := filepath.Join(base, tc.fileName+".jsonl") 56 | f, err := os.Open(inputFile) 57 | require.NoError(t, err) 58 | 59 | summary, err := parse.Process(f) 60 | require.NoError(t, err) 61 | assert.Len(t, summary.Packages, len(tc.expected)) 62 | packages := summary.GetSortedPackages(parse.SortByPackageName) 63 | 64 | for i, pkg := range packages { 65 | t.Run(pkg.Summary.Package, func(t *testing.T) { 66 | wantName := tc.expected[i] 67 | if pkg.Summary.Package != wantName { 68 | t.Fatalf("got name: %s, want name: %s", pkg.Summary.Package, wantName) 69 | } 70 | }) 71 | } 72 | }) 73 | } 74 | } 75 | 76 | func TestSortCoverage(t *testing.T) { 77 | t.Parallel() 78 | 79 | base := filepath.Join("testdata", "cover") 80 | 81 | // expected package name and corresponding cover % 82 | // Note, these numbers will vary between on go versions. 83 | type expected []float64 84 | 85 | tt := []struct { 86 | fileName string 87 | expected 88 | }{ 89 | { 90 | // go test -count=1 bytes log sort -json -cover | tparse -sort cover 91 | "test_01", 92 | expected{ 93 | 86.7, // "bytes" 94 | 68.0, // "log" 95 | 60.8, // "sort" 96 | }, 97 | }, 98 | { 99 | // go test -count=1 bufio bytes crypto fmt log mime net sort strings time -json -cover | tparse -sort cover 100 | "test_02", 101 | expected{ 102 | 98.1, // "strings" 103 | 95.6, // "bytes" 104 | 95.2, // "fmt" 105 | 93.8, // "mime" 106 | 93.3, // "bufio" 107 | 91.8, // "time" 108 | 81.2, // "net" 109 | 68.0, // "log" 110 | 60.8, // "sort" 111 | 5.9, // "crypto" 112 | }, 113 | }, 114 | } 115 | for _, tc := range tt { 116 | t.Run(tc.fileName, func(t *testing.T) { 117 | inputFile := filepath.Join(base, tc.fileName+".jsonl") 118 | f, err := os.Open(inputFile) 119 | require.NoError(t, err) 120 | 121 | summary, err := parse.Process(f) 122 | require.NoError(t, err) 123 | assert.Len(t, summary.Packages, len(tc.expected)) 124 | packages := summary.GetSortedPackages(parse.SortByCoverage) 125 | 126 | for i, pkg := range packages { 127 | t.Run(pkg.Summary.Package, func(t *testing.T) { 128 | wantCover := tc.expected[i] 129 | if pkg.Coverage != wantCover { 130 | t.Fatalf("got cover: %v(%s), want cover: %v", pkg.Coverage, pkg.Summary.Package, wantCover) 131 | } 132 | }) 133 | } 134 | }) 135 | } 136 | } 137 | 138 | func TestSortElapsed(t *testing.T) { 139 | t.Parallel() 140 | 141 | base := filepath.Join("testdata", "cached") 142 | 143 | // expected package name and corresponding cover % 144 | // Note, these numbers will vary between on go versions. 145 | type expected []float64 146 | 147 | tt := []struct { 148 | fileName string 149 | expected 150 | }{ 151 | { 152 | // go test -count=1 fmt mime strings time -json | tparse -sort elapsed 153 | "test_01", 154 | expected{ 155 | 7.168, // "time" 156 | 0.020, // "mime" 157 | 0.007, // "strings" 158 | 0.003, // "fmt" 159 | }, 160 | }, 161 | { 162 | // go test -count=1 bufio bytes crypto fmt log mime sort strings time -json | tparse -sort elapsed 163 | "test_02", 164 | expected{ 165 | 7.641, // "time", 166 | 1.176, // "bytes", 167 | 0.220, // "fmt", 168 | 0.134, // "bufio", 169 | 0.070, // "crypto", 170 | 0.002, // "strings", 171 | 0.001, // "mime", 172 | 0.001, // "sort", 173 | 0.000, // "log", 174 | }, 175 | }, 176 | } 177 | for _, tc := range tt { 178 | t.Run(tc.fileName, func(t *testing.T) { 179 | inputFile := filepath.Join(base, tc.fileName+".jsonl") 180 | f, err := os.Open(inputFile) 181 | require.NoError(t, err) 182 | 183 | summary, err := parse.Process(f) 184 | require.NoError(t, err) 185 | assert.Len(t, summary.Packages, len(tc.expected)) 186 | packages := summary.GetSortedPackages(parse.SortByElapsed) 187 | 188 | for i, pkg := range packages { 189 | t.Run(pkg.Summary.Package, func(t *testing.T) { 190 | wantElapsed := tc.expected[i] 191 | if pkg.Summary.Elapsed != wantElapsed { 192 | t.Fatalf("got elapsed: %v (%s), want elapsed: %v", pkg.Summary.Elapsed, pkg.Summary.Package, wantElapsed) 193 | } 194 | }) 195 | } 196 | }) 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /tests/testdata/outcome/test_02.jsonl: -------------------------------------------------------------------------------- 1 | {"Time":"2018-10-28T00:06:53.478265-04:00","Action":"output","Package":"github.com/astromail/rover","Output":"? \tgithub.com/astromail/rover\t[no test files]\n"} 2 | {"Time":"2018-10-28T00:06:53.478512-04:00","Action":"skip","Package":"github.com/astromail/rover","Elapsed":0} 3 | {"Time":"2018-10-28T00:06:53.511804-04:00","Action":"output","Package":"github.com/astromail/rover/cmd/roverd","Output":"? \tgithub.com/astromail/rover/cmd/roverd\t[no test files]\n"} 4 | {"Time":"2018-10-28T00:06:53.511862-04:00","Action":"skip","Package":"github.com/astromail/rover/cmd/roverd","Elapsed":0} 5 | {"Time":"2018-10-28T00:06:53.511882-04:00","Action":"output","Package":"github.com/astromail/rover/errors","Output":"? \tgithub.com/astromail/rover/errors\t[no test files]\n"} 6 | {"Time":"2018-10-28T00:06:53.511891-04:00","Action":"skip","Package":"github.com/astromail/rover/errors","Elapsed":0} 7 | {"Time":"2018-10-28T00:06:53.511907-04:00","Action":"output","Package":"github.com/astromail/rover/smtp","Output":"? \tgithub.com/astromail/rover/smtp\t[no test files]\n"} 8 | {"Time":"2018-10-28T00:06:53.511916-04:00","Action":"skip","Package":"github.com/astromail/rover/smtp","Elapsed":0} 9 | {"Time":"2018-10-28T00:06:53.511933-04:00","Action":"output","Package":"github.com/astromail/rover/storage","Output":"? \tgithub.com/astromail/rover/storage\t[no test files]\n"} 10 | {"Time":"2018-10-28T00:06:53.511942-04:00","Action":"skip","Package":"github.com/astromail/rover/storage","Elapsed":0} 11 | {"Time":"2018-10-28T00:06:53.511957-04:00","Action":"output","Package":"github.com/astromail/rover/storage/badger","Output":"? \tgithub.com/astromail/rover/storage/badger\t[no test files]\n"} 12 | {"Time":"2018-10-28T00:06:53.511969-04:00","Action":"skip","Package":"github.com/astromail/rover/storage/badger","Elapsed":0} 13 | {"Time":"2018-10-28T00:06:54.007207-04:00","Action":"output","Package":"github.com/astromail/rover/tests","Output":"2018/10/28 00:06:54 Replaying from value pointer: {Fid:0 Len:0 Offset:0}\n"} 14 | {"Time":"2018-10-28T00:06:54.007282-04:00","Action":"output","Package":"github.com/astromail/rover/tests","Output":"2018/10/28 00:06:54 Iterating file id: 0\n"} 15 | {"Time":"2018-10-28T00:06:54.007321-04:00","Action":"output","Package":"github.com/astromail/rover/tests","Output":"2018/10/28 00:06:54 Replaying from value pointer: {Fid:0 Len:0 Offset:0}\n"} 16 | {"Time":"2018-10-28T00:06:54.00733-04:00","Action":"output","Package":"github.com/astromail/rover/tests","Output":"2018/10/28 00:06:54 Iterating file id: 0\n"} 17 | {"Time":"2018-10-28T00:06:54.007389-04:00","Action":"output","Package":"github.com/astromail/rover/tests","Output":"2018/10/28 00:06:54 Iteration took: 144.649µs\n"} 18 | {"Time":"2018-10-28T00:06:54.007462-04:00","Action":"output","Package":"github.com/astromail/rover/tests","Output":"2018/10/28 00:06:54 Iteration took: 259.316µs\n"} 19 | {"Time":"2018-10-28T00:06:54.503762-04:00","Action":"run","Package":"github.com/astromail/rover/tests","Test":"TestCatch"} 20 | {"Time":"2018-10-28T00:06:54.503795-04:00","Action":"output","Package":"github.com/astromail/rover/tests","Test":"TestCatch","Output":"=== RUN TestCatch\n"} 21 | {"Time":"2018-10-28T00:06:54.503829-04:00","Action":"run","Package":"github.com/astromail/rover/tests","Test":"TestCatch/catchAndRetrieve"} 22 | {"Time":"2018-10-28T00:06:54.503838-04:00","Action":"output","Package":"github.com/astromail/rover/tests","Test":"TestCatch/catchAndRetrieve","Output":"=== RUN TestCatch/catchAndRetrieve\n"} 23 | {"Time":"2018-10-28T00:06:54.507445-04:00","Action":"output","Package":"github.com/astromail/rover/tests","Test":"TestCatch","Output":"--- FAIL: TestCatch (0.00s)\n"} 24 | {"Time":"2018-10-28T00:06:54.507485-04:00","Action":"output","Package":"github.com/astromail/rover/tests","Test":"TestCatch/catchAndRetrieve","Output":" --- FAIL: TestCatch/catchAndRetrieve (0.00s)\n"} 25 | {"Time":"2018-10-28T00:06:54.507507-04:00","Action":"output","Package":"github.com/astromail/rover/tests","Test":"TestCatch/catchAndRetrieve","Output":" catch_test.go:29: got id \"ad0892h\", want empty id\n"} 26 | {"Time":"2018-10-28T00:06:54.507517-04:00","Action":"output","Package":"github.com/astromail/rover/tests","Test":"TestCatch/catchAndRetrieve","Output":" catch_test.go:37: email id does not match: got \"69c47b65-0ad5-47d5-9346-f4f8bd22c56e\", want \"oops@example.com\"\n"} 27 | {"Time":"2018-10-28T00:06:54.507528-04:00","Action":"output","Package":"github.com/astromail/rover/tests","Test":"TestCatch/catchAndRetrieve","Output":" catch_test.go:41: failed to mark email as read: failed to mark email id \"123\" as read: PUT \"http://localhost:8026/api/v1/emails/123/read\": expecting valid status code, got 500 Internal Server Error\n"} 28 | {"Time":"2018-10-28T00:06:54.507536-04:00","Action":"fail","Package":"github.com/astromail/rover/tests","Test":"TestCatch/catchAndRetrieve","Elapsed":0} 29 | {"Time":"2018-10-28T00:06:54.507544-04:00","Action":"fail","Package":"github.com/astromail/rover/tests","Test":"TestCatch","Elapsed":0} 30 | {"Time":"2018-10-28T00:06:54.507549-04:00","Action":"run","Package":"github.com/astromail/rover/tests","Test":"TestNameFormat"} 31 | {"Time":"2018-10-28T00:06:54.507554-04:00","Action":"output","Package":"github.com/astromail/rover/tests","Test":"TestNameFormat","Output":"=== RUN TestNameFormat\n"} 32 | {"Time":"2018-10-28T00:06:54.507571-04:00","Action":"output","Package":"github.com/astromail/rover/tests","Test":"TestNameFormat","Output":"--- PASS: TestNameFormat (0.00s)\n"} 33 | {"Time":"2018-10-28T00:06:54.507578-04:00","Action":"pass","Package":"github.com/astromail/rover/tests","Test":"TestNameFormat","Elapsed":0} 34 | {"Time":"2018-10-28T00:06:54.507583-04:00","Action":"output","Package":"github.com/astromail/rover/tests","Output":"FAIL\n"} 35 | {"Time":"2018-10-28T00:06:54.50967-04:00","Action":"output","Package":"github.com/astromail/rover/tests","Output":"FAIL\tgithub.com/astromail/rover/tests\t0.532s\n"} 36 | {"Time":"2018-10-28T00:06:54.509705-04:00","Action":"fail","Package":"github.com/astromail/rover/tests","Elapsed":0.532} -------------------------------------------------------------------------------- /tests/testdata/follow-verbose/test_01.jsonl: -------------------------------------------------------------------------------- 1 | {"Time":"2018-12-04T22:42:29.634612-05:00","Action":"run","Package":"debug/tparse-24","Test":"TestRace"} 2 | {"Time":"2018-12-04T22:42:29.634912-05:00","Action":"output","Package":"debug/tparse-24","Test":"TestRace","Output":"=== RUN TestRace\n"} 3 | {"Time":"2018-12-04T22:42:29.635129-05:00","Action":"output","Package":"debug/tparse-24","Test":"TestRace","Output":"==================\n"} 4 | {"Time":"2018-12-04T22:42:29.635163-05:00","Action":"output","Package":"debug/tparse-24","Test":"TestRace","Output":"WARNING: DATA RACE\n"} 5 | {"Time":"2018-12-04T22:42:29.635172-05:00","Action":"output","Package":"debug/tparse-24","Test":"TestRace","Output":"Write at 0x00c000090090 by goroutine 7:\n"} 6 | {"Time":"2018-12-04T22:42:29.635205-05:00","Action":"output","Package":"debug/tparse-24","Test":"TestRace","Output":" debug/tparse-24.TestRace.func1()\n"} 7 | {"Time":"2018-12-04T22:42:29.635213-05:00","Action":"output","Package":"debug/tparse-24","Test":"TestRace","Output":" /Users/michael.fridman/go/src/debug/tparse-24/some_test.go:7 +0x38\n"} 8 | {"Time":"2018-12-04T22:42:29.635226-05:00","Action":"output","Package":"debug/tparse-24","Test":"TestRace","Output":"\n"} 9 | {"Time":"2018-12-04T22:42:29.635236-05:00","Action":"output","Package":"debug/tparse-24","Test":"TestRace","Output":"Previous write at 0x00c000090090 by goroutine 6:\n"} 10 | {"Time":"2018-12-04T22:42:29.635254-05:00","Action":"output","Package":"debug/tparse-24","Test":"TestRace","Output":" debug/tparse-24.TestRace()\n"} 11 | {"Time":"2018-12-04T22:42:29.63527-05:00","Action":"output","Package":"debug/tparse-24","Test":"TestRace","Output":" /Users/michael.fridman/go/src/debug/tparse-24/some_test.go:8 +0x88\n"} 12 | {"Time":"2018-12-04T22:42:29.635283-05:00","Action":"output","Package":"debug/tparse-24","Test":"TestRace","Output":" testing.tRunner()\n"} 13 | {"Time":"2018-12-04T22:42:29.63529-05:00","Action":"output","Package":"debug/tparse-24","Test":"TestRace","Output":" /usr/local/go/src/testing/testing.go:827 +0x162\n"} 14 | {"Time":"2018-12-04T22:42:29.635302-05:00","Action":"output","Package":"debug/tparse-24","Test":"TestRace","Output":"\n"} 15 | {"Time":"2018-12-04T22:42:29.635322-05:00","Action":"output","Package":"debug/tparse-24","Test":"TestRace","Output":"Goroutine 7 (running) created at:\n"} 16 | {"Time":"2018-12-04T22:42:29.635334-05:00","Action":"output","Package":"debug/tparse-24","Test":"TestRace","Output":" debug/tparse-24.TestRace()\n"} 17 | {"Time":"2018-12-04T22:42:29.635341-05:00","Action":"output","Package":"debug/tparse-24","Test":"TestRace","Output":" /Users/michael.fridman/go/src/debug/tparse-24/some_test.go:7 +0x7a\n"} 18 | {"Time":"2018-12-04T22:42:29.635348-05:00","Action":"output","Package":"debug/tparse-24","Test":"TestRace","Output":" testing.tRunner()\n"} 19 | {"Time":"2018-12-04T22:42:29.635354-05:00","Action":"output","Package":"debug/tparse-24","Test":"TestRace","Output":" /usr/local/go/src/testing/testing.go:827 +0x162\n"} 20 | {"Time":"2018-12-04T22:42:29.635361-05:00","Action":"output","Package":"debug/tparse-24","Test":"TestRace","Output":"\n"} 21 | {"Time":"2018-12-04T22:42:29.635367-05:00","Action":"output","Package":"debug/tparse-24","Test":"TestRace","Output":"Goroutine 6 (running) created at:\n"} 22 | {"Time":"2018-12-04T22:42:29.635376-05:00","Action":"output","Package":"debug/tparse-24","Test":"TestRace","Output":" testing.(*T).Run()\n"} 23 | {"Time":"2018-12-04T22:42:29.635382-05:00","Action":"output","Package":"debug/tparse-24","Test":"TestRace","Output":" /usr/local/go/src/testing/testing.go:878 +0x650\n"} 24 | {"Time":"2018-12-04T22:42:29.635388-05:00","Action":"output","Package":"debug/tparse-24","Test":"TestRace","Output":" testing.runTests.func1()\n"} 25 | {"Time":"2018-12-04T22:42:29.635397-05:00","Action":"output","Package":"debug/tparse-24","Test":"TestRace","Output":" /usr/local/go/src/testing/testing.go:1119 +0xa8\n"} 26 | {"Time":"2018-12-04T22:42:29.635403-05:00","Action":"output","Package":"debug/tparse-24","Test":"TestRace","Output":" testing.tRunner()\n"} 27 | {"Time":"2018-12-04T22:42:29.635409-05:00","Action":"output","Package":"debug/tparse-24","Test":"TestRace","Output":" /usr/local/go/src/testing/testing.go:827 +0x162\n"} 28 | {"Time":"2018-12-04T22:42:29.635415-05:00","Action":"output","Package":"debug/tparse-24","Test":"TestRace","Output":" testing.runTests()\n"} 29 | {"Time":"2018-12-04T22:42:29.635422-05:00","Action":"output","Package":"debug/tparse-24","Test":"TestRace","Output":" /usr/local/go/src/testing/testing.go:1117 +0x4ee\n"} 30 | {"Time":"2018-12-04T22:42:29.635448-05:00","Action":"output","Package":"debug/tparse-24","Test":"TestRace","Output":" testing.(*M).Run()\n"} 31 | {"Time":"2018-12-04T22:42:29.635465-05:00","Action":"output","Package":"debug/tparse-24","Test":"TestRace","Output":" /usr/local/go/src/testing/testing.go:1034 +0x2ee\n"} 32 | {"Time":"2018-12-04T22:42:29.635472-05:00","Action":"output","Package":"debug/tparse-24","Test":"TestRace","Output":" main.main()\n"} 33 | {"Time":"2018-12-04T22:42:29.635478-05:00","Action":"output","Package":"debug/tparse-24","Test":"TestRace","Output":" _testmain.go:42 +0x221\n"} 34 | {"Time":"2018-12-04T22:42:29.635485-05:00","Action":"output","Package":"debug/tparse-24","Test":"TestRace","Output":"==================\n"} 35 | {"Time":"2018-12-04T22:42:29.635807-05:00","Action":"output","Package":"debug/tparse-24","Test":"TestRace","Output":"--- FAIL: TestRace (0.00s)\n"} 36 | {"Time":"2018-12-04T22:42:29.635821-05:00","Action":"output","Package":"debug/tparse-24","Test":"TestRace","Output":" some_test.go:9: 64\n"} 37 | {"Time":"2018-12-04T22:42:29.635829-05:00","Action":"output","Package":"debug/tparse-24","Test":"TestRace","Output":" testing.go:771: race detected during execution of test\n"} 38 | {"Time":"2018-12-04T22:42:29.636065-05:00","Action":"fail","Package":"debug/tparse-24","Test":"TestRace","Elapsed":0} 39 | {"Time":"2018-12-04T22:42:29.636085-05:00","Action":"output","Package":"debug/tparse-24","Output":"FAIL\n"} 40 | {"Time":"2018-12-04T22:42:29.63706-05:00","Action":"output","Package":"debug/tparse-24","Output":"exit status 1\n"} 41 | {"Time":"2018-12-04T22:42:29.637098-05:00","Action":"output","Package":"debug/tparse-24","Output":"FAIL\tdebug/tparse-24\t0.020s\n"} 42 | {"Time":"2018-12-04T22:42:29.637108-05:00","Action":"fail","Package":"debug/tparse-24","Elapsed":0.02} -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | "fmt" 7 | "io" 8 | "log" 9 | "os" 10 | "runtime/debug" 11 | 12 | "github.com/mfridman/tparse/internal/app" 13 | "github.com/mfridman/tparse/internal/utils" 14 | "github.com/mfridman/tparse/parse" 15 | ) 16 | 17 | // Flags. 18 | var ( 19 | vPtr = flag.Bool("v", false, "") 20 | versionPtr = flag.Bool("version", false, "") 21 | hPtr = flag.Bool("h", false, "") 22 | helpPtr = flag.Bool("help", false, "") 23 | allPtr = flag.Bool("all", false, "") 24 | passPtr = flag.Bool("pass", false, "") 25 | skipPtr = flag.Bool("skip", false, "") 26 | showNoTestsPtr = flag.Bool("notests", false, "") 27 | smallScreenPtr = flag.Bool("smallscreen", false, "") 28 | noColorPtr = flag.Bool("nocolor", false, "") 29 | slowPtr = flag.Int("slow", 0, "") 30 | fileNamePtr = flag.String("file", "", "") 31 | formatPtr = flag.String("format", "", "") 32 | followPtr = flag.Bool("follow", false, "") 33 | followOutputPtr = flag.String("follow-output", "", "") 34 | sortPtr = flag.String("sort", "name", "") 35 | progressPtr = flag.Bool("progress", false, "") 36 | comparePtr = flag.String("compare", "", "") 37 | trimPathPtr = flag.String("trimpath", "", "") 38 | // Undocumented flags 39 | followVerbosePtr = flag.Bool("follow-verbose", false, "") 40 | includeTimestamp = flag.Bool("include-timestamp", false, "include timestamps in follow output") 41 | 42 | // Legacy flags 43 | noBordersPtr = flag.Bool("noborders", false, "") 44 | ) 45 | 46 | var usage = `Usage: 47 | go test ./... -json | tparse [options...] 48 | go test [packages...] -json | tparse [options...] 49 | go test [packages...] -json > pkgs.out ; tparse [options...] -file pkgs.out 50 | 51 | Options: 52 | -h Show help. 53 | -v Show version. 54 | -all Display table event for pass and skip. (Failed items always displayed) 55 | -pass Display table for passed tests. 56 | -skip Display table for skipped tests. 57 | -notests Display packages containing no test files or empty test files. 58 | -smallscreen Split subtest names vertically to fit on smaller screens. 59 | -slow Number of slowest tests to display. Default is 0, display all. 60 | -sort Sort table output by attribute [name, elapsed, cover]. Default is name. 61 | -nocolor Disable all colors. (NO_COLOR also supported) 62 | -format The output format for tables [basic, plain, markdown]. Default is basic. 63 | -file Read test output from a file. 64 | -follow Follow raw output from go test to stdout. 65 | -follow-output Write raw output from go test to a file (takes precedence over -follow). 66 | -include-timestamp Include timestamps in follow output. 67 | -progress Print a single summary line for each package. Useful for long running test suites. 68 | -compare Compare against a previous test output file. (experimental) 69 | -trimpath Remove path prefix from package names in output, simplifying their display. 70 | ` 71 | 72 | var version string 73 | 74 | func main() { 75 | log.SetFlags(0) 76 | flag.Usage = func() { 77 | fmt.Fprint(flag.CommandLine.Output(), usage) 78 | } 79 | flag.Parse() 80 | 81 | if *vPtr || *versionPtr { 82 | if info, ok := debug.ReadBuildInfo(); ok { 83 | version = info.Main.Version 84 | } 85 | fmt.Fprintf(os.Stdout, "tparse version: %s\n", version) 86 | return 87 | } 88 | if *hPtr || *helpPtr { 89 | fmt.Print(usage) 90 | return 91 | } 92 | var format app.OutputFormat 93 | switch *formatPtr { 94 | case "basic": 95 | format = app.OutputFormatBasic 96 | case "plain": 97 | format = app.OutputFormatPlain 98 | case "markdown": 99 | format = app.OutputFormatMarkdown 100 | case "": 101 | // This was an existing flag, let's try to avoid breaking users. 102 | format = app.OutputFormatBasic 103 | if *noBordersPtr { 104 | format = app.OutputFormatPlain 105 | } 106 | default: 107 | fmt.Fprintf(os.Stderr, "invalid option:%q. The -format flag must be one of: basic, plain or markdown\n", *formatPtr) 108 | return 109 | } 110 | var sorter parse.PackageSorter 111 | switch *sortPtr { 112 | case "name": 113 | sorter = parse.SortByPackageName 114 | case "elapsed": 115 | sorter = parse.SortByElapsed 116 | case "cover": 117 | sorter = parse.SortByCoverage 118 | default: 119 | fmt.Fprintf(os.Stderr, "invalid option:%q. The -sort flag must be one of: name, elapsed or cover\n", *sortPtr) 120 | return 121 | } 122 | 123 | if *allPtr { 124 | *passPtr = true 125 | *skipPtr = true 126 | } 127 | // Show colors by default. 128 | var disableColor bool 129 | if _, ok := os.LookupEnv("NO_COLOR"); ok || *noColorPtr { 130 | disableColor = true 131 | } 132 | 133 | var followOutput io.WriteCloser 134 | switch { 135 | case *followOutputPtr != "": 136 | var err error 137 | followOutput, err = os.Create(*followOutputPtr) 138 | if err != nil { 139 | fmt.Fprintln(os.Stderr, err) 140 | return 141 | } 142 | *followPtr = true 143 | case *followPtr, *followVerbosePtr: 144 | followOutput = os.Stdout 145 | default: 146 | // If no follow flags are set, we should not write to followOutput. 147 | followOutput = utils.WriteNopCloser{Writer: io.Discard} 148 | } 149 | // TODO(mf): we should marry the options with the flags to avoid having to do this. 150 | options := app.Options{ 151 | Output: os.Stdout, 152 | DisableColor: disableColor, 153 | FollowOutput: *followPtr, 154 | FollowOutputWriter: followOutput, 155 | FollowOutputVerbose: *followVerbosePtr, 156 | FileName: *fileNamePtr, 157 | TestTableOptions: app.TestTableOptions{ 158 | Pass: *passPtr, 159 | Skip: *skipPtr, 160 | Trim: *smallScreenPtr, 161 | TrimPath: *trimPathPtr, 162 | Slow: *slowPtr, 163 | }, 164 | SummaryTableOptions: app.SummaryTableOptions{ 165 | Trim: *smallScreenPtr, 166 | TrimPath: *trimPathPtr, 167 | }, 168 | Format: format, 169 | Sorter: sorter, 170 | ShowNoTests: *showNoTestsPtr, 171 | Progress: *progressPtr, 172 | ProgressOutput: os.Stdout, 173 | Compare: *comparePtr, 174 | IncludeTimestamp: *includeTimestamp, 175 | 176 | // Do not expose publicly. 177 | DisableTableOutput: false, 178 | } 179 | exitCode, err := app.Run(options) 180 | if err != nil { 181 | msg := err.Error() 182 | if errors.Is(err, parse.ErrNotParsable) { 183 | msg = "no parsable events: Make sure to run go test with -json flag" 184 | } 185 | fmt.Fprintln(os.Stderr, msg) 186 | } 187 | os.Exit(exitCode) 188 | } 189 | -------------------------------------------------------------------------------- /tests/testdata/race/test_03.jsonl: -------------------------------------------------------------------------------- 1 | {"Time":"2018-12-16T21:21:52.633949-05:00","Action":"run","Package":"debug/tparse-24","Test":"TestRace1"} 2 | {"Time":"2018-12-16T21:21:52.63424-05:00","Action":"output","Package":"debug/tparse-24","Test":"TestRace1","Output":"=== RUN TestRace1\n"} 3 | {"Time":"2018-12-16T21:21:52.634262-05:00","Action":"output","Package":"debug/tparse-24","Test":"TestRace1","Output":"=== PAUSE TestRace1\n"} 4 | {"Time":"2018-12-16T21:21:52.63427-05:00","Action":"pause","Package":"debug/tparse-24","Test":"TestRace1"} 5 | {"Time":"2018-12-16T21:21:52.634277-05:00","Action":"run","Package":"debug/tparse-24","Test":"TestA"} 6 | {"Time":"2018-12-16T21:21:52.634283-05:00","Action":"output","Package":"debug/tparse-24","Test":"TestA","Output":"=== RUN TestA\n"} 7 | {"Time":"2018-12-16T21:21:52.63429-05:00","Action":"output","Package":"debug/tparse-24","Test":"TestA","Output":"=== PAUSE TestA\n"} 8 | {"Time":"2018-12-16T21:21:52.634296-05:00","Action":"pause","Package":"debug/tparse-24","Test":"TestA"} 9 | {"Time":"2018-12-16T21:21:52.634392-05:00","Action":"cont","Package":"debug/tparse-24","Test":"TestA"} 10 | {"Time":"2018-12-16T21:21:52.634498-05:00","Action":"output","Package":"debug/tparse-24","Test":"TestA","Output":"=== CONT TestA\n"} 11 | {"Time":"2018-12-16T21:21:52.634516-05:00","Action":"cont","Package":"debug/tparse-24","Test":"TestRace1"} 12 | {"Time":"2018-12-16T21:21:52.634522-05:00","Action":"output","Package":"debug/tparse-24","Test":"TestRace1","Output":"=== CONT TestRace1\n"} 13 | {"Time":"2018-12-16T21:21:52.634537-05:00","Action":"output","Package":"debug/tparse-24","Test":"TestA","Output":"--- PASS: TestA (0.00s)\n"} 14 | {"Time":"2018-12-16T21:21:52.635006-05:00","Action":"pass","Package":"debug/tparse-24","Test":"TestA","Elapsed":0} 15 | {"Time":"2018-12-16T21:21:52.635058-05:00","Action":"output","Package":"debug/tparse-24","Test":"TestRace1","Output":"--- PASS: TestRace1 (0.00s)\n"} 16 | {"Time":"2018-12-16T21:21:52.635074-05:00","Action":"output","Package":"debug/tparse-24","Test":"TestRace1","Output":" some_test.go:10: 64\n"} 17 | {"Time":"2018-12-16T21:21:52.635111-05:00","Action":"pass","Package":"debug/tparse-24","Test":"TestRace1","Elapsed":0} 18 | {"Time":"2018-12-16T21:21:52.63512-05:00","Action":"output","Package":"debug/tparse-24","Output":"PASS\n"} 19 | {"Time":"2018-12-16T21:21:52.635128-05:00","Action":"output","Package":"debug/tparse-24","Output":"==================\n"} 20 | {"Time":"2018-12-16T21:21:52.635145-05:00","Action":"output","Package":"debug/tparse-24","Output":"WARNING: DATA RACE\n"} 21 | {"Time":"2018-12-16T21:21:52.635154-05:00","Action":"output","Package":"debug/tparse-24","Output":"Write at 0x00c0000c8010 by goroutine 9:\n"} 22 | {"Time":"2018-12-16T21:21:52.635161-05:00","Action":"output","Package":"debug/tparse-24","Output":" debug/tparse-24.TestRace1.func1()\n"} 23 | {"Time":"2018-12-16T21:21:52.635168-05:00","Action":"output","Package":"debug/tparse-24","Output":" /Users/michael.fridman/go/src/debug/tparse-24/some_test.go:8 +0x38\n"} 24 | {"Time":"2018-12-16T21:21:52.635178-05:00","Action":"output","Package":"debug/tparse-24","Output":"\n"} 25 | {"Time":"2018-12-16T21:21:52.635184-05:00","Action":"output","Package":"debug/tparse-24","Output":"Previous write at 0x00c0000c8010 by goroutine 6:\n"} 26 | {"Time":"2018-12-16T21:21:52.63519-05:00","Action":"output","Package":"debug/tparse-24","Output":" debug/tparse-24.TestRace1()\n"} 27 | {"Time":"2018-12-16T21:21:52.635201-05:00","Action":"output","Package":"debug/tparse-24","Output":" /Users/michael.fridman/go/src/debug/tparse-24/some_test.go:9 +0x96\n"} 28 | {"Time":"2018-12-16T21:21:52.635208-05:00","Action":"output","Package":"debug/tparse-24","Output":" testing.tRunner()\n"} 29 | {"Time":"2018-12-16T21:21:52.635217-05:00","Action":"output","Package":"debug/tparse-24","Output":" /usr/local/go/src/testing/testing.go:827 +0x162\n"} 30 | {"Time":"2018-12-16T21:21:52.635223-05:00","Action":"output","Package":"debug/tparse-24","Output":"\n"} 31 | {"Time":"2018-12-16T21:21:52.635228-05:00","Action":"output","Package":"debug/tparse-24","Output":"Goroutine 9 (running) created at:\n"} 32 | {"Time":"2018-12-16T21:21:52.635237-05:00","Action":"output","Package":"debug/tparse-24","Output":" debug/tparse-24.TestRace1()\n"} 33 | {"Time":"2018-12-16T21:21:52.635242-05:00","Action":"output","Package":"debug/tparse-24","Output":" /Users/michael.fridman/go/src/debug/tparse-24/some_test.go:8 +0x88\n"} 34 | {"Time":"2018-12-16T21:21:52.635248-05:00","Action":"output","Package":"debug/tparse-24","Output":" testing.tRunner()\n"} 35 | {"Time":"2018-12-16T21:21:52.635253-05:00","Action":"output","Package":"debug/tparse-24","Output":" /usr/local/go/src/testing/testing.go:827 +0x162\n"} 36 | {"Time":"2018-12-16T21:21:52.635276-05:00","Action":"output","Package":"debug/tparse-24","Output":"\n"} 37 | {"Time":"2018-12-16T21:21:52.635285-05:00","Action":"output","Package":"debug/tparse-24","Output":"Goroutine 6 (finished) created at:\n"} 38 | {"Time":"2018-12-16T21:21:52.635291-05:00","Action":"output","Package":"debug/tparse-24","Output":" testing.(*T).Run()\n"} 39 | {"Time":"2018-12-16T21:21:52.635298-05:00","Action":"output","Package":"debug/tparse-24","Output":" /usr/local/go/src/testing/testing.go:878 +0x650\n"} 40 | {"Time":"2018-12-16T21:21:52.635304-05:00","Action":"output","Package":"debug/tparse-24","Output":" testing.runTests.func1()\n"} 41 | {"Time":"2018-12-16T21:21:52.635309-05:00","Action":"output","Package":"debug/tparse-24","Output":" /usr/local/go/src/testing/testing.go:1119 +0xa8\n"} 42 | {"Time":"2018-12-16T21:21:52.635319-05:00","Action":"output","Package":"debug/tparse-24","Output":" testing.tRunner()\n"} 43 | {"Time":"2018-12-16T21:21:52.635329-05:00","Action":"output","Package":"debug/tparse-24","Output":" /usr/local/go/src/testing/testing.go:827 +0x162\n"} 44 | {"Time":"2018-12-16T21:21:52.635335-05:00","Action":"output","Package":"debug/tparse-24","Output":" testing.runTests()\n"} 45 | {"Time":"2018-12-16T21:21:52.635344-05:00","Action":"output","Package":"debug/tparse-24","Output":" /usr/local/go/src/testing/testing.go:1117 +0x4ee\n"} 46 | {"Time":"2018-12-16T21:21:52.63535-05:00","Action":"output","Package":"debug/tparse-24","Output":" testing.(*M).Run()\n"} 47 | {"Time":"2018-12-16T21:21:52.635356-05:00","Action":"output","Package":"debug/tparse-24","Output":" /usr/local/go/src/testing/testing.go:1034 +0x2ee\n"} 48 | {"Time":"2018-12-16T21:21:52.635362-05:00","Action":"output","Package":"debug/tparse-24","Output":" main.main()\n"} 49 | {"Time":"2018-12-16T21:21:52.63537-05:00","Action":"output","Package":"debug/tparse-24","Output":" _testmain.go:44 +0x221\n"} 50 | {"Time":"2018-12-16T21:21:52.635378-05:00","Action":"output","Package":"debug/tparse-24","Output":"==================\n"} 51 | {"Time":"2018-12-16T21:21:53.635797-05:00","Action":"output","Package":"debug/tparse-24","Output":"Found 1 data race(s)\n"} 52 | {"Time":"2018-12-16T21:21:53.637742-05:00","Action":"output","Package":"debug/tparse-24","Output":"FAIL\tdebug/tparse-24\t1.017s\n"} 53 | {"Time":"2018-12-16T21:21:53.637783-05:00","Action":"pass","Package":"debug/tparse-24","Elapsed":1.018} -------------------------------------------------------------------------------- /internal/app/table_failed.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "sort" 7 | "strings" 8 | 9 | "github.com/charmbracelet/lipgloss" 10 | "golang.org/x/term" 11 | 12 | "github.com/mfridman/tparse/parse" 13 | ) 14 | 15 | const ( 16 | defaultWidth = 96 17 | ) 18 | 19 | // printFailed prints all failed tests, grouping them by package. Packages are sorted. 20 | // Panic is an exception. 21 | func (c *consoleWriter) printFailed(packages []*parse.Package) { 22 | width, _, err := term.GetSize(int(os.Stdout.Fd())) 23 | if err != nil { 24 | width = defaultWidth 25 | } 26 | 27 | for _, pkg := range packages { 28 | if pkg.HasPanic { 29 | // TODO(mf): document why panics are handled separately. A panic may or may 30 | // not be associated with tests, so we print it at the package level. 31 | output := c.prepareStyledPanic(pkg.Summary.Package, pkg.Summary.Test, pkg.PanicEvents, width) 32 | fmt.Fprintln(c, output) 33 | continue 34 | } 35 | failedTests := pkg.TestsByAction(parse.ActionFail) 36 | if len(failedTests) == 0 { 37 | continue 38 | } 39 | styledPackageHeader := c.styledHeader( 40 | pkg.Summary.Action.String(), 41 | pkg.Summary.Package, 42 | ) 43 | fmt.Fprintln(c, styledPackageHeader) 44 | fmt.Fprintln(c) 45 | /* 46 | Failed tests are all the individual tests, where the subtests are not separated. 47 | 48 | We need to sort the tests by name to ensure they are grouped together 49 | */ 50 | sort.Slice(failedTests, func(i, j int) bool { 51 | return failedTests[i].Name < failedTests[j].Name 52 | }) 53 | 54 | divider := lipgloss.NewStyle(). 55 | BorderStyle(lipgloss.NormalBorder()). 56 | BorderTop(true). 57 | Faint(c.format != OutputFormatMarkdown). 58 | Width(width) 59 | 60 | /* 61 | Note, some output such as the "--- FAIL: " line is prefixed 62 | with spaces. Unfortunately when dumping this in markdown format 63 | it renders as an code block. 64 | 65 | "To produce a code block in Markdown, simply indent every line of the 66 | block by at least 4 spaces or 1 tab." 67 | Ref. https://daringfireball.net/projects/markdown/syntax 68 | 69 | Example: 70 | --- FAIL: Test (0.05s) 71 | --- FAIL: Test/test_01 (0.01s) 72 | --- FAIL: Test/test_01/sort (0.00s) 73 | 74 | This is why we wrap the entire test output in a code block. 75 | */ 76 | 77 | if c.format == OutputFormatMarkdown { 78 | fmt.Fprintln(c, fencedCodeBlock) 79 | } 80 | var key string 81 | for i, t := range failedTests { 82 | // Add top divider to all tests except first one. 83 | base, _, _ := cut(t.Name, "/") 84 | if i > 0 && key != base { 85 | fmt.Fprintln(c, divider.String()) 86 | } 87 | key = base 88 | fmt.Fprintln(c, c.prepareStyledTest(t)) 89 | } 90 | if c.format == OutputFormatMarkdown { 91 | fmt.Fprint(c, fencedCodeBlock+"\n\n") 92 | } 93 | } 94 | } 95 | 96 | const ( 97 | fencedCodeBlock string = "```" 98 | ) 99 | 100 | // copied directly from strings.Cut (go1.18) to support older Go versions. 101 | // In the future, replace this with the upstream function. 102 | func cut(s, sep string) (before, after string, found bool) { 103 | if i := strings.Index(s, sep); i >= 0 { 104 | return s[:i], s[i+len(sep):], true 105 | } 106 | return s, "", false 107 | } 108 | 109 | func (c *consoleWriter) prepareStyledPanic( 110 | packageName string, 111 | testName string, 112 | panicEvents []*parse.Event, 113 | width int, 114 | ) string { 115 | if testName != "" { 116 | packageName = packageName + " • " + testName 117 | } 118 | styledPackageHeader := c.styledHeader("PANIC", packageName) 119 | // TODO(mf): can we pass this panic stack to another package and either by default, 120 | // or optionally, build human-readable panic output with: 121 | // https://github.com/maruel/panicparse 122 | var rows strings.Builder 123 | for _, e := range panicEvents { 124 | if e.Output == "" { 125 | continue 126 | } 127 | rows.WriteString(e.Output) 128 | } 129 | content := lipgloss.NewStyle().Width(width).Render(rows.String()) 130 | return lipgloss.JoinVertical(lipgloss.Left, styledPackageHeader, content) 131 | } 132 | 133 | func (c *consoleWriter) styledHeader(status, packageName string) string { 134 | status = c.red(strings.ToUpper(status)) 135 | packageName = strings.TrimSpace(packageName) 136 | 137 | if c.format == OutputFormatMarkdown { 138 | msg := fmt.Sprintf("## %s • %s", status, packageName) 139 | return msg 140 | // TODO(mf): an alternative implementation is to add 2 horizontal lines above and below 141 | // the package header output. 142 | // 143 | // var divider string 144 | // for i := 0; i < len(msg); i++ { 145 | // divider += "─" 146 | // } 147 | // return fmt.Sprintf("%s\n%s\n%s", divider, msg, divider) 148 | } 149 | /* 150 | Need to rethink how to best support multiple output formats across 151 | CI, local terminal development and markdown 152 | 153 | See https://github.com/mfridman/tparse/issues/71 154 | */ 155 | headerStyle := lipgloss.NewStyle(). 156 | BorderStyle(lipgloss.ThickBorder()). 157 | BorderForeground(lipgloss.Color("103")) 158 | statusStyle := lipgloss.NewStyle(). 159 | PaddingLeft(3). 160 | PaddingRight(2). 161 | Foreground(lipgloss.Color("9")) 162 | packageNameStyle := lipgloss.NewStyle(). 163 | PaddingRight(3) 164 | headerRow := lipgloss.JoinHorizontal( 165 | lipgloss.Left, 166 | statusStyle.Render(status), 167 | packageNameStyle.Render("package: "+packageName), 168 | ) 169 | return headerStyle.Render(headerRow) 170 | } 171 | 172 | const ( 173 | failLine = "--- FAIL: " 174 | ) 175 | 176 | func (c *consoleWriter) prepareStyledTest(t *parse.Test) string { 177 | t.SortEvents() 178 | 179 | var rows, headerRows strings.Builder 180 | for _, e := range t.Events { 181 | // Only add events that have output information. Skip everything else. 182 | // Note, since we know about all the output, we can bubble "--- Fail" to the top 183 | // of the output so it's trivial to spot the failing test name and elapsed time. 184 | if e.Action != parse.ActionOutput { 185 | continue 186 | } 187 | if strings.Contains(e.Output, failLine) { 188 | header := strings.TrimSuffix(e.Output, "\n") 189 | // go test prefixes too much padding to the "--- FAIL: " output lines. 190 | // Let's cut the padding by half, being careful to preserve the fail 191 | // line and the proceeding output. 192 | before, after, ok := cut(header, failLine) 193 | var pad string 194 | if ok { 195 | var n int 196 | for _, r := range before { 197 | if r == 32 { 198 | n++ 199 | } 200 | } 201 | if n/2 > 0 { 202 | pad = strings.Repeat(" ", n/2) 203 | } 204 | } 205 | header = pad + failLine + after 206 | 207 | // Avoid colorizing markdown output so it renders properly, otherwise add a subtle 208 | // red color to the test headers. 209 | if c.format != OutputFormatMarkdown { 210 | header = lipgloss.NewStyle().Foreground(lipgloss.Color("1")).Render(header) 211 | } 212 | headerRows.WriteString(header) 213 | continue 214 | } 215 | 216 | if e.Output != "" { 217 | rows.WriteString(e.Output) 218 | } 219 | } 220 | out := headerRows.String() 221 | if rows.Len() > 0 { 222 | out += "\n\n" + rows.String() 223 | } 224 | return out 225 | } 226 | -------------------------------------------------------------------------------- /tests/testdata/failed/test_01.golden: -------------------------------------------------------------------------------- 1 | ╭────────┬─────────┬───────────────────────────────────────┬───────────────────────────────────────╮ 2 | │ Status │ Elapsed │ Test │ Package │ 3 | ├────────┼─────────┼───────────────────────────────────────┼───────────────────────────────────────┤ 4 | │ PASS │ 5.45 │ TestMigrateUpWithReset │ github.com/pressly/goose/v3/tests/e2e │ 5 | │ PASS │ 5.39 │ TestMigrateUpTo │ github.com/pressly/goose/v3/tests/e2e │ 6 | │ PASS │ 4.96 │ TestMigrateUpByOne │ github.com/pressly/goose/v3/tests/e2e │ 7 | │ PASS │ 3.37 │ TestMigrateFull │ github.com/pressly/goose/v3/tests/e2e │ 8 | │ PASS │ 2.53 │ TestMigrateUpWithRedo │ github.com/pressly/goose/v3/tests/e2e │ 9 | │ PASS │ 2.32 │ TestNoVersioning │ github.com/pressly/goose/v3/tests/e2e │ 10 | │ PASS │ 0.02 │ TestNoVersioning/seed-up-down-to-zero │ github.com/pressly/goose/v3/tests/e2e │ 11 | │ PASS │ 0.02 │ TestNoVersioning/test-seed-up-reset │ github.com/pressly/goose/v3/tests/e2e │ 12 | │ PASS │ 0.01 │ TestNoVersioning/test-seed-up-redo │ github.com/pressly/goose/v3/tests/e2e │ 13 | │ FAIL │ 3.71 │ TestNotAllowMissing │ github.com/pressly/goose/v3/tests/e2e │ 14 | │ FAIL │ 2.55 │ TestAllowMissingUpWithRedo │ github.com/pressly/goose/v3/tests/e2e │ 15 | │ FAIL │ 3.16 │ TestNowAllowMissingUpByOne │ github.com/pressly/goose/v3/tests/e2e │ 16 | │ FAIL │ 4.13 │ TestAllowMissingUpWithReset │ github.com/pressly/goose/v3/tests/e2e │ 17 | │ FAIL │ 3.15 │ TestAllowMissingUpByOne │ github.com/pressly/goose/v3/tests/e2e │ 18 | │ FAIL │ 5.11 │ TestMigrateAllowMissingDown │ github.com/pressly/goose/v3/tests/e2e │ 19 | ╰────────┴─────────┴───────────────────────────────────────┴───────────────────────────────────────╯ 20 | ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ 21 | ┃ FAIL package: github.com/pressly/goose/v3/tests/e2e ┃ 22 | ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ 23 | 24 | --- FAIL: TestAllowMissingUpByOne (3.15s) 25 | 26 | allow_missing_test.go:329: unexpected number value: got:0 want:1 27 | 2022/05/19 22:03:07 OK 00001_a.sql 28 | 2022/05/19 22:03:07 OK 00002_b.sql 29 | 2022/05/19 22:03:07 OK 00003_c.sql 30 | 2022/05/19 22:03:07 OK 00004_d.sql 31 | 2022/05/19 22:03:07 OK 00005_e.sql 32 | 2022/05/19 22:03:07 OK 00006_f.sql 33 | 2022/05/19 22:03:07 OK 00007_g.sql 34 | 2022/05/19 22:03:07 OK 00008_h.sql 35 | 2022/05/19 22:03:07 OK 00009_i.sql 36 | 2022/05/19 22:03:07 OK 00010_j.sql 37 | 2022/05/19 22:03:07 OK 00011_k.sql 38 | 2022/05/19 22:03:07 goose: no migrations to run. current version: 11 39 | 2022/05/19 22:03:07 OK 00011_k.sql 40 | 2022/05/19 22:03:07 OK 00010_j.sql 41 | 42 | ──────────────────────────────────────────────────────────────────────────────────────────────── 43 |  44 | --- FAIL: TestAllowMissingUpWithRedo (2.55s) 45 | 46 | allow_missing_test.go:329: unexpected number value: got:0 want:1 47 | 48 | ──────────────────────────────────────────────────────────────────────────────────────────────── 49 |  50 | --- FAIL: TestAllowMissingUpWithReset (4.13s) 51 | 52 | allow_missing_test.go:329: unexpected number value: got:0 want:1 53 | 2022/05/19 22:03:08 OK 00001_a.sql 54 | 2022/05/19 22:03:08 OK 00002_b.sql 55 | 2022/05/19 22:03:08 OK 00003_c.sql 56 | 2022/05/19 22:03:08 OK 00004_d.sql 57 | 2022/05/19 22:03:08 OK 00005_e.sql 58 | 2022/05/19 22:03:08 OK 00006_f.sql 59 | 2022/05/19 22:03:08 OK 00007_g.sql 60 | 2022/05/19 22:03:08 OK 00008_h.sql 61 | 2022/05/19 22:03:08 OK 00009_i.sql 62 | 2022/05/19 22:03:08 OK 00010_j.sql 63 | 2022/05/19 22:03:08 OK 00011_k.sql 64 | 2022/05/19 22:03:08 goose: no migrations to run. current version: 11 65 | 66 | ──────────────────────────────────────────────────────────────────────────────────────────────── 67 |  68 | --- FAIL: TestMigrateAllowMissingDown (5.11s) 69 | 70 | allow_missing_test.go:329: unexpected number value: got:0 want:1 71 | 2022/05/19 22:03:09 OK 00005_e.sql 72 | 2022/05/19 22:03:09 OK 00006_f.sql 73 | 2022/05/19 22:03:09 OK 00007_g.sql 74 | 2022/05/19 22:03:09 OK 00008_h.sql 75 | 2022/05/19 22:03:09 OK 00009_i.sql 76 | 2022/05/19 22:03:09 OK 00010_j.sql 77 | 2022/05/19 22:03:09 OK 00001_a.sql 78 | 2022/05/19 22:03:09 OK 00001_a.sql 79 | 2022/05/19 22:03:09 OK 00011_k.sql 80 | 2022/05/19 22:03:09 goose: no migrations to run. current version: 11 81 | 2022/05/19 22:03:09 OK 00001_a.sql 82 | 2022/05/19 22:03:09 OK 00011_k.sql 83 | 2022/05/19 22:03:09 OK 00002_b.sql 84 | 2022/05/19 22:03:09 OK 00010_j.sql 85 | 2022/05/19 22:03:09 OK 00009_i.sql 86 | 2022/05/19 22:03:09 OK 00002_b.sql 87 | 2022/05/19 22:03:09 OK 00008_h.sql 88 | 2022/05/19 22:03:09 OK 00001_a.sql 89 | 2022/05/19 22:03:09 OK 00002_b.sql 90 | 2022/05/19 22:03:09 OK 00003_c.sql 91 | 2022/05/19 22:03:09 OK 00007_g.sql 92 | 2022/05/19 22:03:09 OK 00002_b.sql 93 | 2022/05/19 22:03:09 goose: no migrations to run. current version: 2 94 | 2022/05/19 22:03:09 OK 00006_f.sql 95 | 2022/05/19 22:03:09 OK 00003_c.sql 96 | 2022/05/19 22:03:09 OK 00005_e.sql 97 | 2022/05/19 22:03:09 OK 00003_c.sql 98 | 2022/05/19 22:03:09 OK 00004_d.sql 99 | 2022/05/19 22:03:09 OK 00004_d.sql 100 | 2022/05/19 22:03:09 OK 00003_c.sql 101 | 2022/05/19 22:03:09 OK 00004_d.sql 102 | 2022/05/19 22:03:09 OK 00002_b.sql 103 | 2022/05/19 22:03:09 OK 00004_d.sql 104 | 2022/05/19 22:03:09 OK 00001_a.sql 105 | 2022/05/19 22:03:09 OK 00005_e.sql 106 | 2022/05/19 22:03:09 OK 00005_e.sql 107 | 2022/05/19 22:03:09 OK 00005_e.sql 108 | 2022/05/19 22:03:09 OK 00006_f.sql 109 | 2022/05/19 22:03:09 OK 00006_f.sql 110 | 2022/05/19 22:03:09 OK 00006_f.sql 111 | 2022/05/19 22:03:09 OK 00007_g.sql 112 | 2022/05/19 22:03:09 OK 00007_g.sql 113 | 2022/05/19 22:03:09 OK 00007_g.sql 114 | 2022/05/19 22:03:09 OK 00008_h.sql 115 | 116 | ──────────────────────────────────────────────────────────────────────────────────────────────── 117 |  118 | --- FAIL: TestNotAllowMissing (3.71s) 119 | 120 | allow_missing_test.go:329: unexpected number value: got:0 want:1 121 | 122 | ──────────────────────────────────────────────────────────────────────────────────────────────── 123 |  124 | --- FAIL: TestNowAllowMissingUpByOne (3.16s) 125 | 126 | allow_missing_test.go:329: unexpected number value: got:0 want:1 127 | 128 | ╭────────┬─────────┬───────────────────────────────────────┬───────┬──────┬──────┬──────╮ 129 | │ Status │ Elapsed │ Package │ Cover │ Pass │ Fail │ Skip │ 130 | ├────────┼─────────┼───────────────────────────────────────┼───────┼──────┼──────┼──────┤ 131 | │ FAIL │ 9.00s │ github.com/pressly/goose/v3/tests/e2e │ -- │ 9 │ 6 │ 0 │ 132 | ╰────────┴─────────┴───────────────────────────────────────┴───────┴──────┴──────┴──────╯ 133 | -------------------------------------------------------------------------------- /tests/testdata/panic/test_05.jsonl: -------------------------------------------------------------------------------- 1 | {"Time":"2018-10-24T14:32:01.310248-04:00","Action":"output","Package":"github.com/mfridman/tparse","Output":"? \tgithub.com/mfridman/tparse\t[no test files]\n"} 2 | {"Time":"2018-10-24T14:32:01.31052-04:00","Action":"skip","Package":"github.com/mfridman/tparse","Elapsed":0} 3 | {"Time":"2018-10-24T14:32:01.31056-04:00","Action":"output","Package":"github.com/mfridman/tparse/ignore","Output":"? \tgithub.com/mfridman/tparse/ignore\t[no test files]\n"} 4 | {"Time":"2018-10-24T14:32:01.310588-04:00","Action":"skip","Package":"github.com/mfridman/tparse/ignore","Elapsed":0} 5 | {"Time":"2018-10-24T14:32:01.310626-04:00","Action":"output","Package":"github.com/mfridman/tparse/parse","Output":"? \tgithub.com/mfridman/tparse/parse\t[no test files]\n"} 6 | {"Time":"2018-10-24T14:32:01.310638-04:00","Action":"skip","Package":"github.com/mfridman/tparse/parse","Elapsed":0} 7 | {"Time":"2018-10-24T14:32:01.499657-04:00","Action":"run","Package":"github.com/mfridman/tparse/tests","Test":"TestNewEvent"} 8 | {"Time":"2018-10-24T14:32:01.499717-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestNewEvent","Output":"=== RUN TestNewEvent\n"} 9 | {"Time":"2018-10-24T14:32:01.499741-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestNewEvent","Output":"=== PAUSE TestNewEvent\n"} 10 | {"Time":"2018-10-24T14:32:01.499758-04:00","Action":"pause","Package":"github.com/mfridman/tparse/tests","Test":"TestNewEvent"} 11 | {"Time":"2018-10-24T14:32:01.499771-04:00","Action":"run","Package":"github.com/mfridman/tparse/tests","Test":"TestCachedPackage"} 12 | {"Time":"2018-10-24T14:32:01.499781-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestCachedPackage","Output":"=== RUN TestCachedPackage\n"} 13 | {"Time":"2018-10-24T14:32:01.499789-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestCachedPackage","Output":"=== PAUSE TestCachedPackage\n"} 14 | {"Time":"2018-10-24T14:32:01.499799-04:00","Action":"pause","Package":"github.com/mfridman/tparse/tests","Test":"TestCachedPackage"} 15 | {"Time":"2018-10-24T14:32:01.499809-04:00","Action":"run","Package":"github.com/mfridman/tparse/tests","Test":"TestCover"} 16 | {"Time":"2018-10-24T14:32:01.499816-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestCover","Output":"=== RUN TestCover\n"} 17 | {"Time":"2018-10-24T14:32:01.499829-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestCover","Output":"=== PAUSE TestCover\n"} 18 | {"Time":"2018-10-24T14:32:01.499836-04:00","Action":"pause","Package":"github.com/mfridman/tparse/tests","Test":"TestCover"} 19 | {"Time":"2018-10-24T14:32:01.499843-04:00","Action":"run","Package":"github.com/mfridman/tparse/tests","Test":"TestPanicEvent"} 20 | {"Time":"2018-10-24T14:32:01.499849-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestPanicEvent","Output":"=== RUN TestPanicEvent\n"} 21 | {"Time":"2018-10-24T14:32:01.500824-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestPanicEvent","Output":"--- PASS: TestPanicEvent (0.00s)\n"} 22 | {"Time":"2018-10-24T14:32:01.500848-04:00","Action":"pass","Package":"github.com/mfridman/tparse/tests","Test":"TestPanicEvent","Elapsed":0} 23 | {"Time":"2018-10-24T14:32:01.500859-04:00","Action":"run","Package":"github.com/mfridman/tparse/tests","Test":"TestPrescan"} 24 | {"Time":"2018-10-24T14:32:01.500869-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestPrescan","Output":"=== RUN TestPrescan\n"} 25 | {"Time":"2018-10-24T14:32:01.50088-04:00","Action":"run","Package":"github.com/mfridman/tparse/tests","Test":"TestPrescan/pass"} 26 | {"Time":"2018-10-24T14:32:01.50089-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestPrescan/pass","Output":"=== RUN TestPrescan/pass\n"} 27 | {"Time":"2018-10-24T14:32:01.501224-04:00","Action":"run","Package":"github.com/mfridman/tparse/tests","Test":"TestPrescan/fail01"} 28 | {"Time":"2018-10-24T14:32:01.501234-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestPrescan/fail01","Output":"=== RUN TestPrescan/fail01\n"} 29 | {"Time":"2018-10-24T14:32:01.503535-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestPrescan/fail01","Output":"panic: runtime error: invalid memory address or nil pointer dereference [recovered]\n"} 30 | {"Time":"2018-10-24T14:32:01.503561-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestPrescan/fail01","Output":"\tpanic: runtime error: invalid memory address or nil pointer dereference\n"} 31 | {"Time":"2018-10-24T14:32:01.503574-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestPrescan/fail01","Output":"[signal SIGSEGV: segmentation violation code=0x1 addr=0x40 pc=0x110f2f1]\n"} 32 | {"Time":"2018-10-24T14:32:01.503582-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestPrescan/fail01","Output":"\n"} 33 | {"Time":"2018-10-24T14:32:01.503592-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestPrescan/fail01","Output":"goroutine 11 [running]:\n"} 34 | {"Time":"2018-10-24T14:32:01.5036-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestPrescan/fail01","Output":"testing.tRunner.func1(0xc0000a0b00)\n"} 35 | {"Time":"2018-10-24T14:32:01.503612-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestPrescan/fail01","Output":"\t/usr/local/go/src/testing/testing.go:792 +0x387\n"} 36 | {"Time":"2018-10-24T14:32:01.503664-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestPrescan/fail01","Output":"panic(0x1138e80, 0x126a100)\n"} 37 | {"Time":"2018-10-24T14:32:01.50368-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestPrescan/fail01","Output":"\t/usr/local/go/src/runtime/panic.go:513 +0x1b9\n"} 38 | {"Time":"2018-10-24T14:32:01.503689-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestPrescan/fail01","Output":"github.com/mfridman/tparse/parse.Start(0x118b220, 0xc0000fb8a0, 0x5bd0baa1, 0xc000034798, 0x106e0b6)\n"} 39 | {"Time":"2018-10-24T14:32:01.503697-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestPrescan/fail01","Output":"\t/Users/michael.fridman/go/src/github.com/mfridman/tparse/parse/package.go:136 +0x151\n"} 40 | {"Time":"2018-10-24T14:32:01.503707-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestPrescan/fail01","Output":"github.com/mfridman/tparse/tests_test.TestPrescan.func2(0xc0000a0b00)\n"} 41 | {"Time":"2018-10-24T14:32:01.503779-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestPrescan/fail01","Output":"\t/Users/michael.fridman/go/src/github.com/mfridman/tparse/tests/prescan_test.go:144 +0x7a\n"} 42 | {"Time":"2018-10-24T14:32:01.503791-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestPrescan/fail01","Output":"testing.tRunner(0xc0000a0b00, 0x1171820)\n"} 43 | {"Time":"2018-10-24T14:32:01.503798-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestPrescan/fail01","Output":"\t/usr/local/go/src/testing/testing.go:827 +0xbf\n"} 44 | {"Time":"2018-10-24T14:32:01.503805-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestPrescan/fail01","Output":"created by testing.(*T).Run\n"} 45 | {"Time":"2018-10-24T14:32:01.503813-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestPrescan/fail01","Output":"\t/usr/local/go/src/testing/testing.go:878 +0x353\n"} 46 | {"Time":"2018-10-24T14:32:01.504138-04:00","Action":"output","Package":"github.com/mfridman/tparse/tests","Test":"TestPrescan/fail01","Output":"FAIL\tgithub.com/mfridman/tparse/tests\t0.016s\n"} 47 | {"Time":"2018-10-24T14:32:01.50418-04:00","Action":"fail","Package":"github.com/mfridman/tparse/tests","Test":"TestPrescan/fail01","Elapsed":0.016} -------------------------------------------------------------------------------- /tests/testdata/race/test_01.jsonl: -------------------------------------------------------------------------------- 1 | {"Time":"2018-12-16T20:28:29.256021-05:00","Action":"run","Package":"command-line-arguments","Test":"TestRace1"} 2 | {"Time":"2018-12-16T20:28:29.256317-05:00","Action":"output","Package":"command-line-arguments","Test":"TestRace1","Output":"=== RUN TestRace1\n"} 3 | {"Time":"2018-12-16T20:28:29.256347-05:00","Action":"output","Package":"command-line-arguments","Test":"TestRace1","Output":"=== PAUSE TestRace1\n"} 4 | {"Time":"2018-12-16T20:28:29.256355-05:00","Action":"pause","Package":"command-line-arguments","Test":"TestRace1"} 5 | {"Time":"2018-12-16T20:28:29.256363-05:00","Action":"run","Package":"command-line-arguments","Test":"TestA"} 6 | {"Time":"2018-12-16T20:28:29.25637-05:00","Action":"output","Package":"command-line-arguments","Test":"TestA","Output":"=== RUN TestA\n"} 7 | {"Time":"2018-12-16T20:28:29.256377-05:00","Action":"output","Package":"command-line-arguments","Test":"TestA","Output":"=== PAUSE TestA\n"} 8 | {"Time":"2018-12-16T20:28:29.256384-05:00","Action":"pause","Package":"command-line-arguments","Test":"TestA"} 9 | {"Time":"2018-12-16T20:28:29.256538-05:00","Action":"cont","Package":"command-line-arguments","Test":"TestA"} 10 | {"Time":"2018-12-16T20:28:29.256559-05:00","Action":"output","Package":"command-line-arguments","Test":"TestA","Output":"=== CONT TestA\n"} 11 | {"Time":"2018-12-16T20:28:29.256572-05:00","Action":"cont","Package":"command-line-arguments","Test":"TestRace1"} 12 | {"Time":"2018-12-16T20:28:29.256579-05:00","Action":"output","Package":"command-line-arguments","Test":"TestRace1","Output":"=== CONT TestRace1\n"} 13 | {"Time":"2018-12-16T20:28:29.256742-05:00","Action":"output","Package":"command-line-arguments","Test":"TestA","Output":"--- PASS: TestA (0.00s)\n"} 14 | {"Time":"2018-12-16T20:28:29.256827-05:00","Action":"output","Package":"command-line-arguments","Test":"TestA","Output":"==================\n"} 15 | {"Time":"2018-12-16T20:28:29.256859-05:00","Action":"output","Package":"command-line-arguments","Test":"TestA","Output":"WARNING: DATA RACE\n"} 16 | {"Time":"2018-12-16T20:28:29.256868-05:00","Action":"output","Package":"command-line-arguments","Test":"TestA","Output":"Write at 0x00c0000be010 by goroutine 9:\n"} 17 | {"Time":"2018-12-16T20:28:29.25689-05:00","Action":"output","Package":"command-line-arguments","Test":"TestA","Output":" command-line-arguments.TestRace1.func1()\n"} 18 | {"Time":"2018-12-16T20:28:29.256899-05:00","Action":"output","Package":"command-line-arguments","Test":"TestA","Output":" /Users/michael.fridman/go/src/debug/tparse-24/some_test.go:8 +0x38\n"} 19 | {"Time":"2018-12-16T20:28:29.256915-05:00","Action":"output","Package":"command-line-arguments","Test":"TestA","Output":"\n"} 20 | {"Time":"2018-12-16T20:28:29.256929-05:00","Action":"output","Package":"command-line-arguments","Test":"TestA","Output":"Previous write at 0x00c0000be010 by goroutine 6:\n"} 21 | {"Time":"2018-12-16T20:28:29.256936-05:00","Action":"output","Package":"command-line-arguments","Test":"TestA","Output":" command-line-arguments.TestRace1()\n"} 22 | {"Time":"2018-12-16T20:28:29.256942-05:00","Action":"output","Package":"command-line-arguments","Test":"TestA","Output":" /Users/michael.fridman/go/src/debug/tparse-24/some_test.go:9 +0x96\n"} 23 | {"Time":"2018-12-16T20:28:29.256948-05:00","Action":"output","Package":"command-line-arguments","Test":"TestA","Output":" testing.tRunner()\n"} 24 | {"Time":"2018-12-16T20:28:29.256961-05:00","Action":"output","Package":"command-line-arguments","Test":"TestA","Output":" /usr/local/go/src/testing/testing.go:827 +0x162\n"} 25 | {"Time":"2018-12-16T20:28:29.256968-05:00","Action":"output","Package":"command-line-arguments","Test":"TestA","Output":"\n"} 26 | {"Time":"2018-12-16T20:28:29.256974-05:00","Action":"output","Package":"command-line-arguments","Test":"TestA","Output":"Goroutine 9 (running) created at:\n"} 27 | {"Time":"2018-12-16T20:28:29.25698-05:00","Action":"output","Package":"command-line-arguments","Test":"TestA","Output":" command-line-arguments.TestRace1()\n"} 28 | {"Time":"2018-12-16T20:28:29.256986-05:00","Action":"output","Package":"command-line-arguments","Test":"TestA","Output":" /Users/michael.fridman/go/src/debug/tparse-24/some_test.go:8 +0x88\n"} 29 | {"Time":"2018-12-16T20:28:29.256993-05:00","Action":"output","Package":"command-line-arguments","Test":"TestA","Output":" testing.tRunner()\n"} 30 | {"Time":"2018-12-16T20:28:29.257034-05:00","Action":"output","Package":"command-line-arguments","Test":"TestA","Output":" /usr/local/go/src/testing/testing.go:827 +0x162\n"} 31 | {"Time":"2018-12-16T20:28:29.257044-05:00","Action":"output","Package":"command-line-arguments","Test":"TestA","Output":"\n"} 32 | {"Time":"2018-12-16T20:28:29.257097-05:00","Action":"output","Package":"command-line-arguments","Test":"TestA","Output":"Goroutine 6 (running) created at:\n"} 33 | {"Time":"2018-12-16T20:28:29.257111-05:00","Action":"output","Package":"command-line-arguments","Test":"TestA","Output":" testing.(*T).Run()\n"} 34 | {"Time":"2018-12-16T20:28:29.257121-05:00","Action":"output","Package":"command-line-arguments","Test":"TestA","Output":" /usr/local/go/src/testing/testing.go:878 +0x650\n"} 35 | {"Time":"2018-12-16T20:28:29.257127-05:00","Action":"output","Package":"command-line-arguments","Test":"TestA","Output":" testing.runTests.func1()\n"} 36 | {"Time":"2018-12-16T20:28:29.257134-05:00","Action":"output","Package":"command-line-arguments","Test":"TestA","Output":" /usr/local/go/src/testing/testing.go:1119 +0xa8\n"} 37 | {"Time":"2018-12-16T20:28:29.25714-05:00","Action":"output","Package":"command-line-arguments","Test":"TestA","Output":" testing.tRunner()\n"} 38 | {"Time":"2018-12-16T20:28:29.257146-05:00","Action":"output","Package":"command-line-arguments","Test":"TestA","Output":" /usr/local/go/src/testing/testing.go:827 +0x162\n"} 39 | {"Time":"2018-12-16T20:28:29.257153-05:00","Action":"output","Package":"command-line-arguments","Test":"TestA","Output":" testing.runTests()\n"} 40 | {"Time":"2018-12-16T20:28:29.257162-05:00","Action":"output","Package":"command-line-arguments","Test":"TestA","Output":" /usr/local/go/src/testing/testing.go:1117 +0x4ee\n"} 41 | {"Time":"2018-12-16T20:28:29.25717-05:00","Action":"output","Package":"command-line-arguments","Test":"TestA","Output":" testing.(*M).Run()\n"} 42 | {"Time":"2018-12-16T20:28:29.257187-05:00","Action":"output","Package":"command-line-arguments","Test":"TestA","Output":" /usr/local/go/src/testing/testing.go:1034 +0x2ee\n"} 43 | {"Time":"2018-12-16T20:28:29.257196-05:00","Action":"output","Package":"command-line-arguments","Test":"TestA","Output":" main.main()\n"} 44 | {"Time":"2018-12-16T20:28:29.257209-05:00","Action":"output","Package":"command-line-arguments","Test":"TestA","Output":" _testmain.go:44 +0x221\n"} 45 | {"Time":"2018-12-16T20:28:29.257216-05:00","Action":"output","Package":"command-line-arguments","Test":"TestA","Output":"==================\n"} 46 | {"Time":"2018-12-16T20:28:29.257467-05:00","Action":"pass","Package":"command-line-arguments","Test":"TestA","Elapsed":0} 47 | {"Time":"2018-12-16T20:28:29.257487-05:00","Action":"output","Package":"command-line-arguments","Test":"TestRace1","Output":"--- FAIL: TestRace1 (0.00s)\n"} 48 | {"Time":"2018-12-16T20:28:29.257498-05:00","Action":"output","Package":"command-line-arguments","Test":"TestRace1","Output":" some_test.go:10: 64\n"} 49 | {"Time":"2018-12-16T20:28:29.257508-05:00","Action":"output","Package":"command-line-arguments","Test":"TestRace1","Output":" testing.go:771: race detected during execution of test\n"} 50 | {"Time":"2018-12-16T20:28:29.257518-05:00","Action":"fail","Package":"command-line-arguments","Test":"TestRace1","Elapsed":0} 51 | {"Time":"2018-12-16T20:28:29.257524-05:00","Action":"output","Package":"command-line-arguments","Output":"FAIL\n"} 52 | {"Time":"2018-12-16T20:28:29.258518-05:00","Action":"output","Package":"command-line-arguments","Output":"FAIL\tcommand-line-arguments\t0.016s\n"} 53 | {"Time":"2018-12-16T20:28:29.258549-05:00","Action":"fail","Package":"command-line-arguments","Elapsed":0.017} -------------------------------------------------------------------------------- /internal/app/table_tests.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "sort" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/charmbracelet/lipgloss" 11 | "github.com/charmbracelet/lipgloss/table" 12 | 13 | "github.com/mfridman/tparse/internal/utils" 14 | "github.com/mfridman/tparse/parse" 15 | ) 16 | 17 | var ( 18 | versionMajorRe = regexp.MustCompile(`(?m)v[0-9]+`) 19 | ) 20 | 21 | type TestTableOptions struct { 22 | // Display passed or skipped tests. If both are true this is equivalent to all. 23 | Pass, Skip bool 24 | // For narrow screens, trim long test identifiers vertically. Example: 25 | // TestNoVersioning/seed-up-down-to-zero 26 | // 27 | // TestNoVersioning 28 | // /seed-up-down-to-zero 29 | Trim bool 30 | 31 | // TrimPath is the path prefix to trim from the package name. 32 | TrimPath string 33 | 34 | // Display up to N slow tests for each package, tests are sorted by 35 | // calculated the elapsed time for the given test. 36 | Slow int 37 | } 38 | 39 | type packageTests struct { 40 | skippedCount int 41 | skipped []*parse.Test 42 | passedCount int 43 | passed []*parse.Test 44 | failed []*parse.Test 45 | failedCount int 46 | } 47 | 48 | func (c *consoleWriter) testsTable(packages []*parse.Package, option TestTableOptions) { 49 | // Print passed tests, sorted by elapsed DESC. Grouped by alphabetically sorted packages. 50 | tbl := newTable(c.format, func(style lipgloss.Style, row, col int) lipgloss.Style { 51 | switch row { 52 | case table.HeaderRow: 53 | default: 54 | if col == 2 || col == 3 { 55 | // Test name and package name 56 | style = style.Align(lipgloss.Left) 57 | } 58 | } 59 | return style 60 | }) 61 | header := testRow{ 62 | status: "Status", 63 | elapsed: "Elapsed", 64 | testName: "Test", 65 | packageName: "Package", 66 | } 67 | tbl.Headers(header.toRow()...) 68 | data := table.NewStringData() 69 | 70 | names := make([]string, 0, len(packages)) 71 | for _, pkg := range packages { 72 | names = append(names, pkg.Summary.Package) 73 | } 74 | packagePrefix := utils.FindLongestCommonPrefix(names) 75 | 76 | for i, pkg := range packages { 77 | // Discard packages where we cannot generate a sensible test summary. 78 | if pkg.NoTestFiles || pkg.NoTests || pkg.HasPanic { 79 | continue 80 | } 81 | pkgTests := getTestsFromPackages(pkg, option) 82 | all := make([]*parse.Test, 0, len(pkgTests.passed)+len(pkgTests.skipped)+len(pkgTests.failed)) 83 | all = append(all, pkgTests.passed...) 84 | all = append(all, pkgTests.skipped...) 85 | all = append(all, pkgTests.failed...) 86 | 87 | for _, t := range all { 88 | // TODO(mf): why are we sorting this? 89 | t.SortEvents() 90 | 91 | testName := shortenTestName(t.Name, option.Trim, 32) 92 | 93 | status := c.FormatAction(t.Status()) 94 | packageName := shortenPackageName(t.Package, packagePrefix, 16, option.Trim, option.TrimPath) 95 | 96 | row := testRow{ 97 | status: status, 98 | elapsed: strconv.FormatFloat(t.Elapsed(), 'f', 2, 64), 99 | testName: testName, 100 | packageName: packageName, 101 | } 102 | data.Append(row.toRow()) 103 | } 104 | if i != (len(packages) - 1) { 105 | // Add a blank row between packages. 106 | data.Append(testRow{}.toRow()) 107 | } 108 | } 109 | 110 | if data.Rows() > 0 { 111 | fmt.Fprintln(c, tbl.Data(data).Render()) 112 | } 113 | } 114 | 115 | func (c *consoleWriter) testsTableMarkdown(packages []*parse.Package, option TestTableOptions) { 116 | for _, pkg := range packages { 117 | // Print passed tests, sorted by elapsed DESC. Grouped by alphabetically sorted packages. 118 | tbl := newTable(c.format, func(style lipgloss.Style, row, col int) lipgloss.Style { 119 | switch row { 120 | case table.HeaderRow: 121 | default: 122 | if col == 2 { 123 | // Test name 124 | style = style.Align(lipgloss.Left) 125 | } 126 | } 127 | return style 128 | }) 129 | header := []string{ 130 | "Status", 131 | "Elapsed", 132 | "Test", 133 | } 134 | tbl.Headers(header...) 135 | data := table.NewStringData() 136 | 137 | // Discard packages where we cannot generate a sensible test summary. 138 | if pkg.NoTestFiles || pkg.NoTests || pkg.HasPanic { 139 | continue 140 | } 141 | pkgTests := getTestsFromPackages(pkg, option) 142 | all := make([]*parse.Test, 0, len(pkgTests.passed)+len(pkgTests.skipped)+len(pkgTests.failed)) 143 | all = append(all, pkgTests.passed...) 144 | all = append(all, pkgTests.skipped...) 145 | all = append(all, pkgTests.failed...) 146 | 147 | for _, t := range all { 148 | // TODO(mf): why are we sorting this? 149 | t.SortEvents() 150 | 151 | testName := shortenTestName(t.Name, option.Trim, 32) 152 | 153 | status := c.FormatAction(t.Status()) 154 | data.Append([]string{ 155 | status, 156 | strconv.FormatFloat(t.Elapsed(), 'f', 2, 64), 157 | testName, 158 | }) 159 | } 160 | if data.Rows() > 0 { 161 | fmt.Fprintf(c, "## 📦 Package **`%s`**\n", pkg.Summary.Package) 162 | fmt.Fprintln(c) 163 | 164 | msg := fmt.Sprintf("Tests: ✓ %d passed | %d skipped | %d failed\n", 165 | pkgTests.passedCount, 166 | pkgTests.skippedCount, 167 | pkgTests.failedCount, 168 | ) 169 | if option.Slow > 0 && option.Slow < pkgTests.passedCount { 170 | msg += fmt.Sprintf("↓ Slowest %d passed tests shown (of %d)\n", 171 | option.Slow, 172 | pkgTests.passedCount, 173 | ) 174 | } 175 | fmt.Fprint(c, msg) 176 | 177 | fmt.Fprintln(c) 178 | fmt.Fprintln(c, "
") 179 | fmt.Fprintln(c) 180 | fmt.Fprintln(c, "Click for test summary") 181 | fmt.Fprintln(c) 182 | fmt.Fprintln(c, tbl.Data(data).Render()) 183 | fmt.Fprintln(c, "
") 184 | fmt.Fprintln(c) 185 | } 186 | fmt.Fprintln(c) 187 | } 188 | } 189 | 190 | func getTestsFromPackages(pkg *parse.Package, option TestTableOptions) *packageTests { 191 | tests := &packageTests{} 192 | skipped := pkg.TestsByAction(parse.ActionSkip) 193 | tests.skippedCount = len(skipped) 194 | passed := pkg.TestsByAction(parse.ActionPass) 195 | tests.passedCount = len(passed) 196 | failed := pkg.TestsByAction(parse.ActionFail) 197 | tests.failedCount = len(failed) 198 | if option.Skip { 199 | tests.skipped = append(tests.skipped, skipped...) 200 | } 201 | if option.Pass { 202 | tests.passed = append(tests.passed, passed...) 203 | // Order passed tests within a package by elapsed time DESC (longest on top). 204 | sort.Slice(tests.passed, func(i, j int) bool { 205 | return tests.passed[i].Elapsed() > tests.passed[j].Elapsed() 206 | }) 207 | // Optional, display only the slowest N tests by elapsed time. 208 | if option.Slow > 0 && len(tests.passed) > option.Slow { 209 | tests.passed = tests.passed[:option.Slow] 210 | } 211 | } 212 | tests.failed = append(tests.failed, failed...) 213 | return tests 214 | } 215 | 216 | func shortenTestName(s string, trim bool, maxLength int) string { 217 | var testName strings.Builder 218 | testName.WriteString(s) 219 | if trim && testName.Len() > maxLength && strings.Count(testName.String(), "/") > 0 { 220 | testName.Reset() 221 | ss := strings.Split(s, "/") 222 | testName.WriteString(ss[0] + "\n") 223 | for i, s := range ss[1:] { 224 | testName.WriteString(" /") 225 | for len(s) > maxLength { 226 | testName.WriteString(s[:maxLength-2] + " …\n ") 227 | s = s[maxLength-2:] 228 | } 229 | testName.WriteString(s) 230 | if i != len(ss[1:])-1 { 231 | testName.WriteString("\n") 232 | } 233 | } 234 | } 235 | return testName.String() 236 | } 237 | 238 | type testRow struct { 239 | status string 240 | elapsed string 241 | testName string 242 | packageName string 243 | } 244 | 245 | func (r testRow) toRow() []string { 246 | return []string{ 247 | r.status, 248 | r.elapsed, 249 | r.testName, 250 | r.packageName, 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /internal/app/table_summary.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "fmt" 5 | "path" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/charmbracelet/lipgloss" 10 | "github.com/charmbracelet/lipgloss/table" 11 | 12 | "github.com/mfridman/tparse/internal/utils" 13 | "github.com/mfridman/tparse/parse" 14 | ) 15 | 16 | type SummaryTableOptions struct { 17 | // For narrow screens, remove common prefix and trim long package names vertically. Example: 18 | // github.com/mfridman/tparse/app 19 | // github.com/mfridman/tparse/internal/seed-up-down-to-zero 20 | // 21 | // tparse/app 22 | // tparse 23 | // /seed-up-down-to-zero 24 | Trim bool 25 | 26 | // TrimPath is the path prefix to trim from the package name. 27 | TrimPath string 28 | } 29 | 30 | func (c *consoleWriter) summaryTable( 31 | packages []*parse.Package, 32 | showNoTests bool, 33 | options SummaryTableOptions, 34 | against *parse.GoTestSummary, 35 | ) { 36 | tbl := newTable(c.format, func(style lipgloss.Style, row, col int) lipgloss.Style { 37 | switch row { 38 | case table.HeaderRow: 39 | default: 40 | if col == 2 { 41 | // Package name 42 | style = style.Align(lipgloss.Left) 43 | } 44 | } 45 | return style 46 | }) 47 | header := summaryRow{ 48 | status: "Status", 49 | elapsed: "Elapsed", 50 | packageName: "Package", 51 | cover: "Cover", 52 | pass: "Pass", 53 | fail: "Fail", 54 | skip: "Skip", 55 | } 56 | tbl.Headers(header.toRow()...) 57 | data := table.NewStringData() 58 | 59 | // Capture as separate slices because notests are optional when passed tests are available. 60 | // The only exception is if passed=0 and notests=1, then we display them regardless. This 61 | // is almost always the user matching on the wrong package. 62 | var passed, notests []summaryRow 63 | 64 | names := make([]string, 0, len(packages)) 65 | for _, pkg := range packages { 66 | names = append(names, pkg.Summary.Package) 67 | } 68 | packagePrefix := utils.FindLongestCommonPrefix(names) 69 | 70 | for _, pkg := range packages { 71 | elapsed := strconv.FormatFloat(pkg.Summary.Elapsed, 'f', 2, 64) + "s" 72 | if pkg.Cached { 73 | elapsed = "(cached)" 74 | } 75 | packageName := pkg.Summary.Package 76 | packageName = shortenPackageName(packageName, packagePrefix, 32, options.Trim, options.TrimPath) 77 | if pkg.HasPanic { 78 | row := summaryRow{ 79 | status: c.red("PANIC"), 80 | elapsed: elapsed, 81 | packageName: packageName, 82 | cover: "--", pass: "--", fail: "--", skip: "--", 83 | } 84 | data.Append(row.toRow()) 85 | continue 86 | } 87 | if pkg.HasFailedBuildOrSetup { 88 | row := summaryRow{ 89 | status: c.red("FAIL"), 90 | elapsed: elapsed, 91 | packageName: packageName + "\n[" + pkg.Summary.Output + "]", 92 | cover: "--", pass: "--", fail: "--", skip: "--", 93 | } 94 | data.Append(row.toRow()) 95 | continue 96 | } 97 | if pkg.NoTestFiles { 98 | row := summaryRow{ 99 | status: c.yellow("NOTEST"), 100 | elapsed: elapsed, 101 | packageName: packageName + "\n[no test files]", 102 | cover: "--", pass: "--", fail: "--", skip: "--", 103 | } 104 | notests = append(notests, row) 105 | continue 106 | } 107 | if pkg.NoTests { 108 | // This should capture cases where packages truly have no tests, but empty files. 109 | if len(pkg.NoTestSlice) == 0 { 110 | row := summaryRow{ 111 | status: c.yellow("NOTEST"), 112 | elapsed: elapsed, 113 | packageName: packageName + "\n[no tests to run]", 114 | cover: "--", pass: "--", fail: "--", skip: "--", 115 | } 116 | notests = append(notests, row) 117 | continue 118 | } 119 | // This should capture cases where packages have a mixture of empty and non-empty test files. 120 | var ss []string 121 | for i, t := range pkg.NoTestSlice { 122 | i++ 123 | ss = append(ss, fmt.Sprintf("%d.%s", i, t.Test)) 124 | } 125 | packageName := fmt.Sprintf("%s\n[no tests to run]\n%s", packageName, strings.Join(ss, "\n")) 126 | row := summaryRow{ 127 | status: c.yellow("NOTEST"), 128 | elapsed: elapsed, 129 | packageName: packageName, 130 | cover: "--", pass: "--", fail: "--", skip: "--", 131 | } 132 | notests = append(notests, row) 133 | 134 | if len(pkg.TestsByAction(parse.ActionPass)) == len(pkg.NoTestSlice) { 135 | continue 136 | } 137 | } 138 | // TODO(mf): refactor this 139 | // Separate cover colorization from the delta output. 140 | coverage := "--" 141 | if pkg.Cover { 142 | coverage = fmt.Sprintf("%.1f%%", pkg.Coverage) 143 | if against != nil { 144 | againstP, ok := against.Packages[pkg.Summary.Package] 145 | if ok { 146 | var sign string 147 | if pkg.Coverage > againstP.Coverage { 148 | sign = "+" 149 | } 150 | coverage = fmt.Sprintf("%s (%s)", coverage, sign+strconv.FormatFloat(pkg.Coverage-againstP.Coverage, 'f', 1, 64)+"%") 151 | } else { 152 | coverage = fmt.Sprintf("%s (-)", coverage) 153 | } 154 | } 155 | // Showing coverage for a package that failed is a bit odd. 156 | // 157 | // Only colorize the coverage when everything passed AND the output is not markdown. 158 | if pkg.Summary.Action == parse.ActionPass && c.format != OutputFormatMarkdown { 159 | switch cover := pkg.Coverage; { 160 | case cover > 0.0 && cover <= 50.0: 161 | coverage = c.red(coverage) 162 | case pkg.Coverage > 50.0 && pkg.Coverage < 80.0: 163 | coverage = c.yellow(coverage) 164 | case pkg.Coverage >= 80.0: 165 | coverage = c.green(coverage) 166 | } 167 | } 168 | } 169 | 170 | status := c.FormatAction(pkg.Summary.Action) 171 | 172 | // Skip packages with no coverage to mimic nocoverageredesign behavior (changed in github.com/golang/go/issues/24570) 173 | totalTests := len(pkg.TestsByAction(parse.ActionPass)) + len(pkg.TestsByAction(parse.ActionFail)) + len(pkg.TestsByAction(parse.ActionSkip)) 174 | if pkg.Cover && pkg.Coverage == 0.0 && totalTests == 0 { 175 | continue 176 | } 177 | 178 | row := summaryRow{ 179 | status: status, 180 | elapsed: elapsed, 181 | packageName: packageName, 182 | cover: coverage, 183 | pass: strconv.Itoa(len(pkg.TestsByAction(parse.ActionPass))), 184 | fail: strconv.Itoa(len(pkg.TestsByAction(parse.ActionFail))), 185 | skip: strconv.Itoa(len(pkg.TestsByAction(parse.ActionSkip))), 186 | } 187 | passed = append(passed, row) 188 | } 189 | 190 | if data.Rows() == 0 && len(passed) == 0 && len(notests) == 0 { 191 | return 192 | } 193 | for _, r := range passed { 194 | data.Append(r.toRow()) 195 | } 196 | 197 | // Only display the "no tests to run" cases if users want to see them when passed 198 | // tests are available. 199 | // An exception is made if there are no passed tests and only a single no test files 200 | // package. This is almost always because the user forgot to match one or more packages. 201 | if showNoTests || (len(passed) == 0 && len(notests) == 1) { 202 | for _, r := range notests { 203 | data.Append(r.toRow()) 204 | } 205 | } 206 | 207 | fmt.Fprintln(c, tbl.Data(data).Render()) 208 | } 209 | 210 | type summaryRow struct { 211 | status string 212 | elapsed string 213 | packageName string 214 | cover string 215 | pass string 216 | fail string 217 | skip string 218 | } 219 | 220 | func (r summaryRow) toRow() []string { 221 | return []string{ 222 | r.status, 223 | r.elapsed, 224 | r.packageName, 225 | r.cover, 226 | r.pass, 227 | r.fail, 228 | r.skip, 229 | } 230 | } 231 | 232 | func shortenPackageName( 233 | name string, 234 | prefix string, 235 | maxLength int, 236 | trim bool, 237 | trimPath string, 238 | ) string { 239 | if trimPath == "auto" { 240 | name = strings.TrimPrefix(name, prefix) 241 | } else if trimPath != "" { 242 | name = strings.TrimPrefix(name, trimPath) 243 | } 244 | if !trim { 245 | return name 246 | } 247 | 248 | if prefix == "" { 249 | dir, name := path.Split(name) 250 | // For SIV-style imports show the last non-versioned path identifier. 251 | // Example: github.com/foo/bar/helper/v3 returns helper/v3 252 | if dir != "" && versionMajorRe.MatchString(name) { 253 | _, subpath := path.Split(path.Clean(dir)) 254 | name = path.Join(subpath, name) 255 | } 256 | return name 257 | } 258 | 259 | name = strings.TrimPrefix(name, prefix) 260 | name = strings.TrimLeft(name, "/") 261 | name = shortenTestName(name, true, maxLength) 262 | 263 | return name 264 | } 265 | -------------------------------------------------------------------------------- /tests/testdata/race/test_04.jsonl: -------------------------------------------------------------------------------- 1 | {"Time":"2022-05-23T10:24:01.895489-04:00","Action":"run","Package":"github.com/mfridman/debug-go/testing","Test":"TestRace"} 2 | {"Time":"2022-05-23T10:24:01.895869-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Test":"TestRace","Output":"=== RUN TestRace\n"} 3 | {"Time":"2022-05-23T10:24:01.895882-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Test":"TestRace","Output":"2\n"} 4 | {"Time":"2022-05-23T10:24:01.895886-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Test":"TestRace","Output":"3\n"} 5 | {"Time":"2022-05-23T10:24:01.896021-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Test":"TestRace","Output":"==================\n"} 6 | {"Time":"2022-05-23T10:24:01.896034-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Test":"TestRace","Output":"WARNING: DATA RACE\n"} 7 | {"Time":"2022-05-23T10:24:01.896039-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Test":"TestRace","Output":"Read at 0x00c0000b0188 by goroutine 8:\n"} 8 | {"Time":"2022-05-23T10:24:01.896047-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Test":"TestRace","Output":" github.com/mfridman/debug-go/testing_test.TestRace.func1()\n"} 9 | {"Time":"2022-05-23T10:24:01.896053-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Test":"TestRace","Output":" /Users/mfridman/src/github.com/mfridman/debug-go/testing/main_test.go:55 +0x3c\n"} 10 | {"Time":"2022-05-23T10:24:01.896059-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Test":"TestRace","Output":"\n"} 11 | {"Time":"2022-05-23T10:24:01.896063-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Test":"TestRace","Output":"Previous write at 0x00c0000b0188 by goroutine 7:\n"} 12 | {"Time":"2022-05-23T10:24:01.896066-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Test":"TestRace","Output":" github.com/mfridman/debug-go/testing_test.TestRace()\n"} 13 | {"Time":"2022-05-23T10:24:01.896069-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Test":"TestRace","Output":" /Users/mfridman/src/github.com/mfridman/debug-go/testing/main_test.go:53 +0x88\n"} 14 | {"Time":"2022-05-23T10:24:01.896075-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Test":"TestRace","Output":" testing.tRunner()\n"} 15 | {"Time":"2022-05-23T10:24:01.896082-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Test":"TestRace","Output":" /opt/homebrew/Cellar/go/1.18.1/libexec/src/testing/testing.go:1439 +0x18c\n"} 16 | {"Time":"2022-05-23T10:24:01.896088-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Test":"TestRace","Output":" testing.(*T).Run.func1()\n"} 17 | {"Time":"2022-05-23T10:24:01.896091-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Test":"TestRace","Output":" /opt/homebrew/Cellar/go/1.18.1/libexec/src/testing/testing.go:1486 +0x44\n"} 18 | {"Time":"2022-05-23T10:24:01.896094-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Test":"TestRace","Output":"\n"} 19 | {"Time":"2022-05-23T10:24:01.896097-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Test":"TestRace","Output":"Goroutine 8 (running) created at:\n"} 20 | {"Time":"2022-05-23T10:24:01.8961-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Test":"TestRace","Output":" github.com/mfridman/debug-go/testing_test.TestRace()\n"} 21 | {"Time":"2022-05-23T10:24:01.896103-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Test":"TestRace","Output":" /Users/mfridman/src/github.com/mfridman/debug-go/testing/main_test.go:54 +0x70\n"} 22 | {"Time":"2022-05-23T10:24:01.896106-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Test":"TestRace","Output":" testing.tRunner()\n"} 23 | {"Time":"2022-05-23T10:24:01.896109-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Test":"TestRace","Output":" /opt/homebrew/Cellar/go/1.18.1/libexec/src/testing/testing.go:1439 +0x18c\n"} 24 | {"Time":"2022-05-23T10:24:01.89612-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Test":"TestRace","Output":" testing.(*T).Run.func1()\n"} 25 | {"Time":"2022-05-23T10:24:01.896123-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Test":"TestRace","Output":" /opt/homebrew/Cellar/go/1.18.1/libexec/src/testing/testing.go:1486 +0x44\n"} 26 | {"Time":"2022-05-23T10:24:01.896125-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Test":"TestRace","Output":"\n"} 27 | {"Time":"2022-05-23T10:24:01.896128-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Test":"TestRace","Output":"Goroutine 7 (running) created at:\n"} 28 | {"Time":"2022-05-23T10:24:01.896131-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Test":"TestRace","Output":" testing.(*T).Run()\n"} 29 | {"Time":"2022-05-23T10:24:01.896134-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Test":"TestRace","Output":" /opt/homebrew/Cellar/go/1.18.1/libexec/src/testing/testing.go:1486 +0x560\n"} 30 | {"Time":"2022-05-23T10:24:01.896137-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Test":"TestRace","Output":" testing.runTests.func1()\n"} 31 | {"Time":"2022-05-23T10:24:01.89614-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Test":"TestRace","Output":" /opt/homebrew/Cellar/go/1.18.1/libexec/src/testing/testing.go:1839 +0x94\n"} 32 | {"Time":"2022-05-23T10:24:01.896143-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Test":"TestRace","Output":" testing.tRunner()\n"} 33 | {"Time":"2022-05-23T10:24:01.896146-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Test":"TestRace","Output":" /opt/homebrew/Cellar/go/1.18.1/libexec/src/testing/testing.go:1439 +0x18c\n"} 34 | {"Time":"2022-05-23T10:24:01.896149-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Test":"TestRace","Output":" testing.runTests()\n"} 35 | {"Time":"2022-05-23T10:24:01.896152-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Test":"TestRace","Output":" /opt/homebrew/Cellar/go/1.18.1/libexec/src/testing/testing.go:1837 +0x6c8\n"} 36 | {"Time":"2022-05-23T10:24:01.896155-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Test":"TestRace","Output":" testing.(*M).Run()\n"} 37 | {"Time":"2022-05-23T10:24:01.896157-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Test":"TestRace","Output":" /opt/homebrew/Cellar/go/1.18.1/libexec/src/testing/testing.go:1719 +0x878\n"} 38 | {"Time":"2022-05-23T10:24:01.89616-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Test":"TestRace","Output":" main.main()\n"} 39 | {"Time":"2022-05-23T10:24:01.896163-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Test":"TestRace","Output":" _testmain.go:47 +0x2fc\n"} 40 | {"Time":"2022-05-23T10:24:01.896166-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Test":"TestRace","Output":"==================\n"} 41 | {"Time":"2022-05-23T10:24:01.896169-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Test":"TestRace","Output":"3\n"} 42 | {"Time":"2022-05-23T10:24:01.896171-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Test":"TestRace","Output":"3\n"} 43 | {"Time":"2022-05-23T10:24:01.896178-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Test":"TestRace","Output":"5\n"} 44 | {"Time":"2022-05-23T10:24:01.896181-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Test":"TestRace","Output":" testing.go:1312: race detected during execution of test\n"} 45 | {"Time":"2022-05-23T10:24:01.896199-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Test":"TestRace","Output":"--- FAIL: TestRace (0.00s)\n"} 46 | {"Time":"2022-05-23T10:24:01.896221-04:00","Action":"fail","Package":"github.com/mfridman/debug-go/testing","Test":"TestRace","Elapsed":0} 47 | {"Time":"2022-05-23T10:24:01.896233-04:00","Action":"cont","Package":"github.com/mfridman/debug-go/testing"} 48 | {"Time":"2022-05-23T10:24:01.896243-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Output":"=== CONT \n"} 49 | {"Time":"2022-05-23T10:24:01.896248-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Output":" testing.go:1312: race detected during execution of test\n"} 50 | {"Time":"2022-05-23T10:24:01.896251-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Output":"FAIL\n"} 51 | {"Time":"2022-05-23T10:24:01.896704-04:00","Action":"output","Package":"github.com/mfridman/debug-go/testing","Output":"FAIL\tgithub.com/mfridman/debug-go/testing\t0.158s\n"} 52 | {"Time":"2022-05-23T10:24:01.896714-04:00","Action":"fail","Package":"github.com/mfridman/debug-go/testing","Elapsed":0.158} 53 | -------------------------------------------------------------------------------- /tests/testdata/failed/test_04.jsonl: -------------------------------------------------------------------------------- 1 | {"Time":"2022-05-21T09:04:11.074034-04:00","Action":"run","Package":"command-line-arguments","Test":"TestWhatever"} 2 | {"Time":"2022-05-21T09:04:11.074252-04:00","Action":"output","Package":"command-line-arguments","Test":"TestWhatever","Output":"=== RUN TestWhatever\n"} 3 | {"Time":"2022-05-21T09:04:11.074268-04:00","Action":"output","Package":"command-line-arguments","Test":"TestWhatever","Output":" main_test.go:12: assert error\n"} 4 | {"Time":"2022-05-21T09:04:11.074275-04:00","Action":"output","Package":"command-line-arguments","Test":"TestWhatever","Output":" main_test.go:13: \n"} 5 | {"Time":"2022-05-21T09:04:11.07428-04:00","Action":"output","Package":"command-line-arguments","Test":"TestWhatever","Output":" \tError Trace:\tmain_test.go:13\n"} 6 | {"Time":"2022-05-21T09:04:11.074284-04:00","Action":"output","Package":"command-line-arguments","Test":"TestWhatever","Output":" \tError: \t\"does not contain\" does not contain \"ostriche\"\n"} 7 | {"Time":"2022-05-21T09:04:11.07429-04:00","Action":"output","Package":"command-line-arguments","Test":"TestWhatever","Output":" \tTest: \tTestWhatever\n"} 8 | {"Time":"2022-05-21T09:04:12.075418-04:00","Action":"run","Package":"command-line-arguments","Test":"TestWhatever/foo"} 9 | {"Time":"2022-05-21T09:04:12.075597-04:00","Action":"output","Package":"command-line-arguments","Test":"TestWhatever/foo","Output":"=== RUN TestWhatever/foo\n"} 10 | {"Time":"2022-05-21T09:04:12.075641-04:00","Action":"output","Package":"command-line-arguments","Test":"TestWhatever/foo","Output":" main_test.go:17: some random output from foo only\n"} 11 | {"Time":"2022-05-21T09:04:12.075689-04:00","Action":"run","Package":"command-line-arguments","Test":"TestWhatever/foo/bar"} 12 | {"Time":"2022-05-21T09:04:12.075718-04:00","Action":"output","Package":"command-line-arguments","Test":"TestWhatever/foo/bar","Output":"=== RUN TestWhatever/foo/bar\n"} 13 | {"Time":"2022-05-21T09:04:12.075748-04:00","Action":"output","Package":"command-line-arguments","Test":"TestWhatever/foo/bar","Output":"=== PAUSE TestWhatever/foo/bar\n"} 14 | {"Time":"2022-05-21T09:04:12.075774-04:00","Action":"pause","Package":"command-line-arguments","Test":"TestWhatever/foo/bar"} 15 | {"Time":"2022-05-21T09:04:12.075799-04:00","Action":"run","Package":"command-line-arguments","Test":"TestWhatever/foo/baz"} 16 | {"Time":"2022-05-21T09:04:12.075823-04:00","Action":"output","Package":"command-line-arguments","Test":"TestWhatever/foo/baz","Output":"=== RUN TestWhatever/foo/baz\n"} 17 | {"Time":"2022-05-21T09:04:12.075849-04:00","Action":"output","Package":"command-line-arguments","Test":"TestWhatever/foo/baz","Output":"=== PAUSE TestWhatever/foo/baz\n"} 18 | {"Time":"2022-05-21T09:04:12.075873-04:00","Action":"pause","Package":"command-line-arguments","Test":"TestWhatever/foo/baz"} 19 | {"Time":"2022-05-21T09:04:12.075935-04:00","Action":"cont","Package":"command-line-arguments","Test":"TestWhatever/foo/bar"} 20 | {"Time":"2022-05-21T09:04:12.075964-04:00","Action":"output","Package":"command-line-arguments","Test":"TestWhatever/foo/bar","Output":"=== CONT TestWhatever/foo/bar\n"} 21 | {"Time":"2022-05-21T09:04:12.07599-04:00","Action":"output","Package":"command-line-arguments","Test":"TestWhatever/foo/bar","Output":" main_test.go:20: some random output from bar only\n"} 22 | {"Time":"2022-05-21T09:04:12.076047-04:00","Action":"run","Package":"command-line-arguments","Test":"TestWhatever/foo/bar/inner-bar"} 23 | {"Time":"2022-05-21T09:04:12.076076-04:00","Action":"output","Package":"command-line-arguments","Test":"TestWhatever/foo/bar/inner-bar","Output":"=== RUN TestWhatever/foo/bar/inner-bar\n"} 24 | {"Time":"2022-05-21T09:04:12.076103-04:00","Action":"output","Package":"command-line-arguments","Test":"TestWhatever/foo/bar/inner-bar","Output":"=== PAUSE TestWhatever/foo/bar/inner-bar\n"} 25 | {"Time":"2022-05-21T09:04:12.076127-04:00","Action":"pause","Package":"command-line-arguments","Test":"TestWhatever/foo/bar/inner-bar"} 26 | {"Time":"2022-05-21T09:04:12.076152-04:00","Action":"cont","Package":"command-line-arguments","Test":"TestWhatever/foo/bar/inner-bar"} 27 | {"Time":"2022-05-21T09:04:12.076177-04:00","Action":"output","Package":"command-line-arguments","Test":"TestWhatever/foo/bar/inner-bar","Output":"=== CONT TestWhatever/foo/bar/inner-bar\n"} 28 | {"Time":"2022-05-21T09:04:12.076291-04:00","Action":"output","Package":"command-line-arguments","Test":"TestWhatever/foo/bar/inner-bar","Output":" main_test.go:23: another inner-bar\n"} 29 | {"Time":"2022-05-21T09:04:12.076317-04:00","Action":"cont","Package":"command-line-arguments","Test":"TestWhatever/foo/baz"} 30 | {"Time":"2022-05-21T09:04:12.076336-04:00","Action":"output","Package":"command-line-arguments","Test":"TestWhatever/foo/baz","Output":"=== CONT TestWhatever/foo/baz\n"} 31 | {"Time":"2022-05-21T09:04:12.076355-04:00","Action":"run","Package":"command-line-arguments","Test":"TestWhatever/foo/baz/inner-baz"} 32 | {"Time":"2022-05-21T09:04:12.076373-04:00","Action":"output","Package":"command-line-arguments","Test":"TestWhatever/foo/baz/inner-baz","Output":"=== RUN TestWhatever/foo/baz/inner-baz\n"} 33 | {"Time":"2022-05-21T09:04:12.076393-04:00","Action":"output","Package":"command-line-arguments","Test":"TestWhatever/foo/baz/inner-baz","Output":"=== PAUSE TestWhatever/foo/baz/inner-baz\n"} 34 | {"Time":"2022-05-21T09:04:12.07641-04:00","Action":"pause","Package":"command-line-arguments","Test":"TestWhatever/foo/baz/inner-baz"} 35 | {"Time":"2022-05-21T09:04:12.076428-04:00","Action":"cont","Package":"command-line-arguments","Test":"TestWhatever/foo/baz/inner-baz"} 36 | {"Time":"2022-05-21T09:04:12.076446-04:00","Action":"output","Package":"command-line-arguments","Test":"TestWhatever/foo/baz/inner-baz","Output":"=== CONT TestWhatever/foo/baz/inner-baz\n"} 37 | {"Time":"2022-05-21T09:04:12.076465-04:00","Action":"output","Package":"command-line-arguments","Test":"TestWhatever/foo/baz/inner-baz","Output":" main_test.go:30: some inner-baz error\n"} 38 | {"Time":"2022-05-21T09:04:12.076504-04:00","Action":"cont","Package":"command-line-arguments","Test":"TestWhatever"} 39 | {"Time":"2022-05-21T09:04:12.076524-04:00","Action":"output","Package":"command-line-arguments","Test":"TestWhatever","Output":"=== CONT TestWhatever\n"} 40 | {"Time":"2022-05-21T09:04:12.07657-04:00","Action":"output","Package":"command-line-arguments","Test":"TestWhatever","Output":" main_test.go:35: \n"} 41 | {"Time":"2022-05-21T09:04:12.076594-04:00","Action":"output","Package":"command-line-arguments","Test":"TestWhatever","Output":" \tError Trace:\tmain_test.go:35\n"} 42 | {"Time":"2022-05-21T09:04:12.076614-04:00","Action":"output","Package":"command-line-arguments","Test":"TestWhatever","Output":" \tError: \tNot equal: \n"} 43 | {"Time":"2022-05-21T09:04:12.076632-04:00","Action":"output","Package":"command-line-arguments","Test":"TestWhatever","Output":" \t \texpected: 7823456\n"} 44 | {"Time":"2022-05-21T09:04:12.076651-04:00","Action":"output","Package":"command-line-arguments","Test":"TestWhatever","Output":" \t \tactual : 1\n"} 45 | {"Time":"2022-05-21T09:04:12.076669-04:00","Action":"output","Package":"command-line-arguments","Test":"TestWhatever","Output":" \tTest: \tTestWhatever\n"} 46 | {"Time":"2022-05-21T09:04:12.076688-04:00","Action":"output","Package":"command-line-arguments","Test":"TestWhatever","Output":" \tMessages: \tnot what I was expecting\n"} 47 | {"Time":"2022-05-21T09:04:12.076716-04:00","Action":"output","Package":"command-line-arguments","Test":"TestWhatever","Output":"--- FAIL: TestWhatever (1.00s)\n"} 48 | {"Time":"2022-05-21T09:04:12.076737-04:00","Action":"output","Package":"command-line-arguments","Test":"TestWhatever/foo","Output":" --- FAIL: TestWhatever/foo (0.00s)\n"} 49 | {"Time":"2022-05-21T09:04:12.07676-04:00","Action":"output","Package":"command-line-arguments","Test":"TestWhatever/foo/bar","Output":" --- FAIL: TestWhatever/foo/bar (0.00s)\n"} 50 | {"Time":"2022-05-21T09:04:12.076781-04:00","Action":"output","Package":"command-line-arguments","Test":"TestWhatever/foo/bar/inner-bar","Output":" --- FAIL: TestWhatever/foo/bar/inner-bar (0.00s)\n"} 51 | {"Time":"2022-05-21T09:04:12.076825-04:00","Action":"fail","Package":"command-line-arguments","Test":"TestWhatever/foo/bar/inner-bar","Elapsed":0} 52 | {"Time":"2022-05-21T09:04:12.076874-04:00","Action":"fail","Package":"command-line-arguments","Test":"TestWhatever/foo/bar","Elapsed":0} 53 | {"Time":"2022-05-21T09:04:12.076892-04:00","Action":"output","Package":"command-line-arguments","Test":"TestWhatever/foo/baz","Output":" --- FAIL: TestWhatever/foo/baz (0.00s)\n"} 54 | {"Time":"2022-05-21T09:04:12.076912-04:00","Action":"output","Package":"command-line-arguments","Test":"TestWhatever/foo/baz/inner-baz","Output":" --- FAIL: TestWhatever/foo/baz/inner-baz (0.00s)\n"} 55 | {"Time":"2022-05-21T09:04:12.076934-04:00","Action":"fail","Package":"command-line-arguments","Test":"TestWhatever/foo/baz/inner-baz","Elapsed":0} 56 | {"Time":"2022-05-21T09:04:12.076952-04:00","Action":"fail","Package":"command-line-arguments","Test":"TestWhatever/foo/baz","Elapsed":0} 57 | {"Time":"2022-05-21T09:04:12.07697-04:00","Action":"fail","Package":"command-line-arguments","Test":"TestWhatever/foo","Elapsed":0} 58 | {"Time":"2022-05-21T09:04:12.076986-04:00","Action":"fail","Package":"command-line-arguments","Test":"TestWhatever","Elapsed":1} 59 | {"Time":"2022-05-21T09:04:12.077054-04:00","Action":"output","Package":"command-line-arguments","Output":"FAIL\n"} 60 | {"Time":"2022-05-21T09:04:12.078217-04:00","Action":"output","Package":"command-line-arguments","Output":"FAIL\tcommand-line-arguments\t1.127s\n"} 61 | {"Time":"2022-05-21T09:04:12.078255-04:00","Action":"fail","Package":"command-line-arguments","Elapsed":1.127} 62 | --------------------------------------------------------------------------------