Skip to content

Commit 4c1aff8

Browse files
committed
testing: gracefully handle subtest failing parent’s T
Don’t panic if a subtest inadvertently calls FailNow on a parent’s T. Instead, report the offending subtest while still reporting the error with the ancestor test and keep exiting goroutines�. Note that this implementation has a race if parallel subtests are failing the parent concurrently. This is fine: Calling FailNow on a parent is considered an error in principle, at the moment, and is reported if it is detected. Having the race allows the race detector to detect the error as well. Fixes #22882 Change-Id: Ifa6d5e55bb88f6bcbb562fc8c99f1f77e320015a Reviewed-on: https://go-review.googlesource.com/97635 Run-TryBot: Marcel van Lohuizen <[email protected]> Reviewed-by: Kunpei Sakai <[email protected]> Reviewed-by: Ian Lance Taylor <[email protected]> TryBot-Result: Gobot Gobot <[email protected]>
1 parent c9438cb commit 4c1aff8

File tree

2 files changed

+95
-4
lines changed

2 files changed

+95
-4
lines changed

src/testing/sub_test.go

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,81 @@ func TestTRun(t *T) {
315315
f: func(t *T) {
316316
t.Skip()
317317
},
318+
}, {
319+
desc: "subtest calls error on parent",
320+
ok: false,
321+
output: `
322+
--- FAIL: subtest calls error on parent (N.NNs)
323+
sub_test.go:NNN: first this
324+
sub_test.go:NNN: and now this!
325+
sub_test.go:NNN: oh, and this too`,
326+
maxPar: 1,
327+
f: func(t *T) {
328+
t.Errorf("first this")
329+
outer := t
330+
t.Run("", func(t *T) {
331+
outer.Errorf("and now this!")
332+
})
333+
t.Errorf("oh, and this too")
334+
},
335+
}, {
336+
desc: "subtest calls fatal on parent",
337+
ok: false,
338+
output: `
339+
--- FAIL: subtest calls fatal on parent (N.NNs)
340+
sub_test.go:NNN: first this
341+
sub_test.go:NNN: and now this!
342+
--- FAIL: subtest calls fatal on parent/#00 (N.NNs)
343+
testing.go:NNN: test executed panic(nil) or runtime.Goexit: subtest may have called FailNow on a parent test`,
344+
maxPar: 1,
345+
f: func(t *T) {
346+
outer := t
347+
t.Errorf("first this")
348+
t.Run("", func(t *T) {
349+
outer.Fatalf("and now this!")
350+
})
351+
t.Errorf("Should not reach here.")
352+
},
353+
}, {
354+
desc: "subtest calls error on ancestor",
355+
ok: false,
356+
output: `
357+
--- FAIL: subtest calls error on ancestor (N.NNs)
358+
sub_test.go:NNN: Report to ancestor
359+
--- FAIL: subtest calls error on ancestor/#00 (N.NNs)
360+
sub_test.go:NNN: Still do this
361+
sub_test.go:NNN: Also do this`,
362+
maxPar: 1,
363+
f: func(t *T) {
364+
outer := t
365+
t.Run("", func(t *T) {
366+
t.Run("", func(t *T) {
367+
outer.Errorf("Report to ancestor")
368+
})
369+
t.Errorf("Still do this")
370+
})
371+
t.Errorf("Also do this")
372+
},
373+
}, {
374+
desc: "subtest calls fatal on ancestor",
375+
ok: false,
376+
output: `
377+
--- FAIL: subtest calls fatal on ancestor (N.NNs)
378+
sub_test.go:NNN: Nope`,
379+
maxPar: 1,
380+
f: func(t *T) {
381+
outer := t
382+
t.Run("", func(t *T) {
383+
for i := 0; i < 4; i++ {
384+
t.Run("", func(t *T) {
385+
outer.Fatalf("Nope")
386+
})
387+
t.Errorf("Don't do this")
388+
}
389+
t.Errorf("And neither do this")
390+
})
391+
t.Errorf("Nor this")
392+
},
318393
}, {
319394
desc: "panic on goroutine fail after test exit",
320395
ok: false,
@@ -518,8 +593,9 @@ func TestBRun(t *T) {
518593
}
519594

520595
func makeRegexp(s string) string {
596+
s = regexp.QuoteMeta(s)
521597
s = strings.Replace(s, ":NNN:", `:\d\d\d:`, -1)
522-
s = strings.Replace(s, "(N.NNs)", `\(\d*\.\d*s\)`, -1)
598+
s = strings.Replace(s, "N\\.NNs", `\d*\.\d*s`, -1)
523599
return s
524600
}
525601

src/testing/testing.go

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -718,6 +718,8 @@ type InternalTest struct {
718718
F func(*T)
719719
}
720720

721+
var errNilPanicOrGoexit = errors.New("test executed panic(nil) or runtime.Goexit")
722+
721723
func tRunner(t *T, fn func(t *T)) {
722724
t.runner = callerName(0)
723725

@@ -733,8 +735,17 @@ func tRunner(t *T, fn func(t *T)) {
733735
t.duration += time.Since(t.start)
734736
// If the test panicked, print any test output before dying.
735737
err := recover()
738+
signal := true
736739
if !t.finished && err == nil {
737-
err = fmt.Errorf("test executed panic(nil) or runtime.Goexit")
740+
err = errNilPanicOrGoexit
741+
for p := t.parent; p != nil; p = p.parent {
742+
if p.finished {
743+
t.Errorf("%v: subtest may have called FailNow on a parent test", err)
744+
err = nil
745+
signal = false
746+
break
747+
}
748+
}
738749
}
739750
if err != nil {
740751
t.Fail()
@@ -769,7 +780,7 @@ func tRunner(t *T, fn func(t *T)) {
769780
if t.parent != nil && atomic.LoadInt32(&t.hasSub) == 0 {
770781
t.setRan()
771782
}
772-
t.signal <- true
783+
t.signal <- signal
773784
}()
774785

775786
t.start = time.Now()
@@ -822,7 +833,11 @@ func (t *T) Run(name string, f func(t *T)) bool {
822833
// without being preempted, even when their parent is a parallel test. This
823834
// may especially reduce surprises if *parallel == 1.
824835
go tRunner(t, f)
825-
<-t.signal
836+
if !<-t.signal {
837+
// At this point, it is likely that FailNow was called on one of the
838+
// parent tests by one of the subtests. Continue aborting up the chain.
839+
runtime.Goexit()
840+
}
826841
return !t.failed
827842
}
828843

0 commit comments

Comments
 (0)