Skip to content

Commit 3719427

Browse files
committed
Show output of failed tests
1 parent 9d7f2ba commit 3719427

File tree

5 files changed

+92
-41
lines changed

5 files changed

+92
-41
lines changed

README.md

+3-2
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ Some more advanced ways to use `gotestdox`:
5858

5959
## Exit status
6060

61-
If there are any test failures, `gotestdox` will report exit status 1.
61+
If there are any test failures, `gotestdox` will print the output messages from the offending test and report status 1 on exit.
6262

6363
## Colour
6464

@@ -94,7 +94,8 @@ github.com/octocat/mymodule/api:
9494
✔ NewServer returns a correctly configured server (0.00s)
9595
9696
github.com/octocat/mymodule/util:
97-
✔ LeftPad adds the correct number of leading spaces (0.00s)
97+
x LeftPad adds the correct number of leading spaces (0.00s)
98+
util_test.go:133: want " dummy", got " dummy"
9899
```
99100

100101
## Multi-word function names

gotestdox.go

+77-31
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,43 @@ import (
1414
"github.com/mattn/go-isatty"
1515
)
1616

17+
const Usage = `gotestdox is a command-line tool for turning Go test names into readable sentences.
18+
19+
Usage:
20+
21+
gotestdox [ARGS]
22+
23+
This will run 'go test -json [ARGS]' in the current directory and format the results in a readable
24+
way. You can use any arguments that 'go test -json' accepts, including a list of packages, for
25+
example.
26+
27+
If the standard input is not an interactive terminal, gotestdox will assume you want to pipe JSON
28+
data into it. For example:
29+
30+
go test -json |gotestdox
31+
32+
See https://github.com/bitfield/gotestdox for more information.`
33+
34+
// Main runs the command-line interface for gotestdox. The exit status for the
35+
// binary is 0 if the tests passed, or 1 if the tests failed, or there was some
36+
// error.
37+
func Main() int {
38+
if len(os.Args) > 1 && os.Args[1] == "-h" {
39+
fmt.Println(Usage)
40+
return 0
41+
}
42+
td := NewTestDoxer()
43+
if isatty.IsTerminal(os.Stdin.Fd()) {
44+
td.ExecGoTest(os.Args[1:])
45+
} else {
46+
td.Filter()
47+
}
48+
if !td.OK {
49+
return 1
50+
}
51+
return 0
52+
}
53+
1754
// TestDoxer holds the state and config associated with a particular invocation
1855
// of 'go test'.
1956
type TestDoxer struct {
@@ -72,6 +109,7 @@ func (td *TestDoxer) ExecGoTest(userArgs []string) {
72109
func (td *TestDoxer) Filter() {
73110
td.OK = true
74111
results := map[string][]Event{}
112+
outputs := map[string][]string{}
75113
scanner := bufio.NewScanner(td.Stdin)
76114
for scanner.Scan() {
77115
event, err := ParseJSON(scanner.Text())
@@ -80,27 +118,46 @@ func (td *TestDoxer) Filter() {
80118
fmt.Fprintln(td.Stderr, err)
81119
return
82120
}
83-
if event.Action == "fail" {
84-
td.OK = false
85-
}
86-
if event.IsPackageResult() {
121+
switch {
122+
case event.IsPackageResult():
87123
fmt.Fprintf(td.Stdout, "%s:\n", event.Package)
88124
tests := results[event.Package]
89125
sort.Slice(tests, func(i, j int) bool {
90126
return tests[i].Sentence < tests[j].Sentence
91127
})
92128
for _, r := range tests {
93129
fmt.Fprintln(td.Stdout, r.String())
130+
if r.Action == "fail" {
131+
for _, line := range outputs[r.Test] {
132+
fmt.Fprint(td.Stdout, line)
133+
}
134+
}
94135
}
95136
fmt.Fprintln(td.Stdout)
96-
}
97-
if event.Relevant() {
137+
case event.IsOutput():
138+
outputs[event.Test] = append(outputs[event.Test], event.Output)
139+
case event.IsTestResult():
98140
event.Sentence = Prettify(event.Test)
99141
results[event.Package] = append(results[event.Package], event)
142+
if event.Action == "fail" {
143+
td.OK = false
144+
}
100145
}
101146
}
102147
}
103148

149+
// ParseJSON takes a string representing a single JSON test record as emitted
150+
// by 'go test -json', and attempts to parse it into an [Event], returning any
151+
// parsing error encountered.
152+
func ParseJSON(line string) (Event, error) {
153+
event := Event{}
154+
err := json.Unmarshal([]byte(line), &event)
155+
if err != nil {
156+
return Event{}, fmt.Errorf("parsing JSON: %w\ninput: %s", err, line)
157+
}
158+
return event, nil
159+
}
160+
104161
// Event represents a Go test event as recorded by the 'go test -json' command.
105162
// It does not attempt to unmarshal all the data, only those fields it needs to
106163
// know about. It is based on the (unexported) 'event' struct used by Go's
@@ -110,6 +167,7 @@ type Event struct {
110167
Package string
111168
Test string
112169
Sentence string
170+
Output string
113171
Elapsed float64
114172
}
115173

@@ -132,11 +190,11 @@ func (e Event) String() string {
132190
return fmt.Sprintf(" %s %s (%.2fs)", status, e.Sentence, e.Elapsed)
133191
}
134192

135-
// Relevant determines whether or not the test event is one that we are
193+
// IsTestResult determines whether or not the test event is one that we are
136194
// interested in (namely, a pass or fail event on a test). Events on non-tests
137195
// (for example, examples) are ignored, and all events on tests other than pass
138196
// or fail events (for example, run or pause events) are also ignored.
139-
func (e Event) Relevant() bool {
197+
func (e Event) IsTestResult() bool {
140198
// Events on non-tests are irrelevant
141199
if !strings.HasPrefix(e.Test, "Test") {
142200
return false
@@ -160,30 +218,18 @@ func (e Event) IsPackageResult() bool {
160218
return false
161219
}
162220

163-
// ParseJSON takes a string representing a single JSON test record as emitted
164-
// by 'go test -json', and attempts to parse it into an [Event], returning any
165-
// parsing error encountered.
166-
func ParseJSON(line string) (Event, error) {
167-
event := Event{}
168-
err := json.Unmarshal([]byte(line), &event)
169-
if err != nil {
170-
return Event{}, fmt.Errorf("parsing JSON: %w\ninput: %s", err, line)
221+
// IsOutput determines whether or not the event is a test output (for example
222+
// from [testing.T.Error]), excluding status messages automatically generated
223+
// by 'go test' such as "--- FAIL: ..." or "=== RUN / PAUSE / CONT".
224+
func (e Event) IsOutput() bool {
225+
if e.Action != "output" {
226+
return false
171227
}
172-
return event, nil
173-
}
174-
175-
// Main runs the command-line interface for gotestdox. The exit status for the
176-
// binary is 0 if the tests passed, or 1 if the tests failed, or there was some
177-
// error.
178-
func Main() int {
179-
td := NewTestDoxer()
180-
if isatty.IsTerminal(os.Stdin.Fd()) {
181-
td.ExecGoTest(os.Args[1:])
182-
} else {
183-
td.Filter()
228+
if strings.HasPrefix(e.Output, "---") {
229+
return false
184230
}
185-
if !td.OK {
186-
return 1
231+
if strings.HasPrefix(e.Output, "===") {
232+
return false
187233
}
188-
return 0
234+
return true
189235
}

gotestdox_test.go

+8-8
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ func TestRelevantIsTrueForTestPassOrFailEvents(t *testing.T) {
8383
},
8484
}
8585
for _, event := range tcs {
86-
relevant := event.Relevant()
86+
relevant := event.IsTestResult()
8787
if !relevant {
8888
t.Errorf("false for relevant event %q on %q", event.Action, event.Test)
8989
}
@@ -115,7 +115,7 @@ func TestRelevantIsFalseForNonTestPassFailEvents(t *testing.T) {
115115
},
116116
}
117117
for _, event := range tcs {
118-
relevant := event.Relevant()
118+
relevant := event.IsTestResult()
119119
if relevant {
120120
t.Errorf("true for irrelevant event %q on %q", event.Action, event.Test)
121121
}
@@ -213,33 +213,33 @@ func ExampleEvent_String() {
213213
// ✔ It works (0.00s)
214214
}
215215

216-
func ExampleEvent_Relevant_true() {
216+
func ExampleEvent_IsTestResult_true() {
217217
event := gotestdox.Event{
218218
Action: "pass",
219219
Test: "TestItWorks",
220220
}
221-
fmt.Println(event.Relevant())
221+
fmt.Println(event.IsTestResult())
222222
// Output:
223223
// true
224224
}
225225

226-
func ExampleEvent_Relevant_false() {
226+
func ExampleEvent_IsTestResult_false() {
227227
event := gotestdox.Event{
228228
Action: "fail",
229229
Test: "ExampleIsIrrelevant",
230230
}
231-
fmt.Println(event.Relevant())
231+
fmt.Println(event.IsTestResult())
232232
// Output:
233233
// false
234234
}
235235

236236
func ExampleParseJSON() {
237-
input := `{"Action":"pass","Package":"demo","Test":"TestItWorks","Elapsed":0.2}`
237+
input := `{"Action":"pass","Package":"demo","Test":"TestItWorks","Output":"","Elapsed":0.2}`
238238
event, err := gotestdox.ParseJSON(input)
239239
if err != nil {
240240
log.Fatal(err)
241241
}
242242
fmt.Printf("%#v\n", event)
243243
// Output:
244-
// gotestdox.Event{Action:"pass", Package:"demo", Test:"TestItWorks", Sentence:"", Elapsed:0.2}
244+
// gotestdox.Event{Action:"pass", Package:"demo", Test:"TestItWorks", Sentence:"", Output:"", Elapsed:0.2}
245245
}

testdata/script/help_requested.txtar

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
exec gotestdox -h
2+
stdout 'Usage'

testdata/script/tests_fail.txtar

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ cmp stdout golden.txt
66
{"Action":"run","Package":"dummy","Test":"TestDummy"}
77
{"Action":"output","Package":"dummy","Test":"TestDummy","Output":"=== RUN TestDummy\n"}
88
{"Action":"output","Package":"dummy","Test":"TestDummy","Output":"--- FAIL: TestDummy (0.00s)\n"}
9+
{"Action":"output","Package":"dummy","Test":"TestDummy","Output":" dummy_test.go:23: oh no\n"}
910
{"Action":"fail","Package":"dummy","Test":"TestDummy"}
1011
{"Action":"output","Package":"dummy","Output":"FAIL\n"}
1112
{"Action":"output","Package":"dummy","Output":"exit status 1\n"}
@@ -14,4 +15,5 @@ cmp stdout golden.txt
1415
-- golden.txt --
1516
dummy:
1617
x Dummy (0.00s)
18+
dummy_test.go:23: oh no
1719

0 commit comments

Comments
 (0)