Skip to content

Commit 15f2cbf

Browse files
dpinelaianlancetaylor
authored andcommitted
testing: allow marking subtest and subbenchmark functions as Helpers
Since subtests and subbenchmarks run in a separate goroutine, and thus a separate stack, this entails capturing the stack trace at the point tb.Run is called. The work of getting the file and line information from this stack is only done when needed, however. Continuing the search into the parent test also requires temporarily holding its mutex. Since Run does not hold it while waiting for the subtest to complete, there should be no risk of a deadlock due to this. Fixes golang#24128 Change-Id: If0bb169f3ac96bd48794624e619ade7edb599f83 Reviewed-on: https://go-review.googlesource.com/108658 Run-TryBot: Ian Lance Taylor <iant@golang.org> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
1 parent 05ca340 commit 15f2cbf

File tree

4 files changed

+81
-38
lines changed

4 files changed

+81
-38
lines changed

src/testing/benchmark.go

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -506,14 +506,17 @@ func (b *B) Run(name string, f func(b *B)) bool {
506506
if !ok {
507507
return true
508508
}
509+
var pc [maxStackLen]uintptr
510+
n := runtime.Callers(2, pc[:])
509511
sub := &B{
510512
common: common{
511-
signal: make(chan bool),
512-
name: benchName,
513-
parent: &b.common,
514-
level: b.level + 1,
515-
w: b.w,
516-
chatty: b.chatty,
513+
signal: make(chan bool),
514+
name: benchName,
515+
parent: &b.common,
516+
level: b.level + 1,
517+
creator: pc[:n],
518+
w: b.w,
519+
chatty: b.chatty,
517520
},
518521
importPath: b.importPath,
519522
benchFunc: f,

src/testing/helper_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,11 @@ helperfuncs_test.go:33: 1
2828
helperfuncs_test.go:21: 2
2929
helperfuncs_test.go:35: 3
3030
helperfuncs_test.go:42: 4
31-
helperfuncs_test.go:47: 5
3231
--- FAIL: Test/sub (?s)
33-
helperfuncs_test.go:50: 6
34-
helperfuncs_test.go:21: 7
35-
helperfuncs_test.go:53: 8
32+
helperfuncs_test.go:45: 5
33+
helperfuncs_test.go:21: 6
34+
helperfuncs_test.go:44: 7
35+
helperfuncs_test.go:56: 8
3636
`
3737
lines := strings.Split(buf.String(), "\n")
3838
durationRE := regexp.MustCompile(`\(.*\)$`)

src/testing/helperfuncs_test.go

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,17 +41,19 @@ func testHelper(t *T) {
4141
}
4242
fn("4")
4343

44-
// Check that calling Helper from inside this test entry function
45-
// doesn't have an effect.
46-
t.Helper()
47-
t.Error("5")
48-
4944
t.Run("sub", func(t *T) {
50-
helper(t, "6")
51-
notHelperCallingHelper(t, "7")
45+
helper(t, "5")
46+
notHelperCallingHelper(t, "6")
47+
// Check that calling Helper from inside a subtest entry function
48+
// works as if it were in an ordinary function call.
5249
t.Helper()
53-
t.Error("8")
50+
t.Error("7")
5451
})
52+
53+
// Check that calling Helper from inside a top-level test function
54+
// has no effect.
55+
t.Helper()
56+
t.Error("8")
5557
}
5658

5759
func parallelTestHelper(t *T) {

src/testing/testing.go

Lines changed: 58 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,10 @@ var (
281281
numFailed uint32 // number of test failures
282282
)
283283

284+
// The maximum number of stack frames to go through when skipping helper functions for
285+
// the purpose of decorating log messages.
286+
const maxStackLen = 50
287+
284288
// common holds the elements common between T and B and
285289
// captures common methods such as Errorf.
286290
type common struct {
@@ -301,6 +305,7 @@ type common struct {
301305

302306
parent *common
303307
level int // Nesting depth of test or benchmark.
308+
creator []uintptr // If level > 0, the stack trace at the point where the parent called t.Run.
304309
name string // Name of test or benchmark.
305310
start time.Time // Time test or benchmark started
306311
duration time.Duration
@@ -327,48 +332,75 @@ func Verbose() bool {
327332
}
328333

329334
// frameSkip searches, starting after skip frames, for the first caller frame
330-
// in a function not marked as a helper and returns the frames to skip
331-
// to reach that site. The search stops if it finds a tRunner function that
332-
// was the entry point into the test.
335+
// in a function not marked as a helper and returns that frame.
336+
// The search stops if it finds a tRunner function that
337+
// was the entry point into the test and the test is not a subtest.
333338
// This function must be called with c.mu held.
334-
func (c *common) frameSkip(skip int) int {
335-
if c.helpers == nil {
336-
return skip
337-
}
338-
var pc [50]uintptr
339+
func (c *common) frameSkip(skip int) runtime.Frame {
340+
// If the search continues into the parent test, we'll have to hold
341+
// its mu temporarily. If we then return, we need to unlock it.
342+
shouldUnlock := false
343+
defer func() {
344+
if shouldUnlock {
345+
c.mu.Unlock()
346+
}
347+
}()
348+
var pc [maxStackLen]uintptr
339349
// Skip two extra frames to account for this function
340350
// and runtime.Callers itself.
341351
n := runtime.Callers(skip+2, pc[:])
342352
if n == 0 {
343353
panic("testing: zero callers found")
344354
}
345355
frames := runtime.CallersFrames(pc[:n])
346-
var frame runtime.Frame
347-
more := true
348-
for i := 0; more; i++ {
356+
var firstFrame, prevFrame, frame runtime.Frame
357+
for more := true; more; prevFrame = frame {
349358
frame, more = frames.Next()
359+
if firstFrame.PC == 0 {
360+
firstFrame = frame
361+
}
350362
if frame.Function == c.runner {
351363
// We've gone up all the way to the tRunner calling
352364
// the test function (so the user must have
353365
// called tb.Helper from inside that test function).
354-
// Only skip up to the test function itself.
355-
return skip + i - 1
366+
// If this is a top-level test, only skip up to the test function itself.
367+
// If we're in a subtest, continue searching in the parent test,
368+
// starting from the point of the call to Run which created this subtest.
369+
if c.level > 1 {
370+
frames = runtime.CallersFrames(c.creator)
371+
parent := c.parent
372+
// We're no longer looking at the current c after this point,
373+
// so we should unlock its mu, unless it's the original receiver,
374+
// in which case our caller doesn't expect us to do that.
375+
if shouldUnlock {
376+
c.mu.Unlock()
377+
}
378+
c = parent
379+
// Remember to unlock c.mu when we no longer need it, either
380+
// because we went up another nesting level, or because we
381+
// returned.
382+
shouldUnlock = true
383+
c.mu.Lock()
384+
continue
385+
}
386+
return prevFrame
356387
}
357388
if _, ok := c.helpers[frame.Function]; !ok {
358389
// Found a frame that wasn't inside a helper function.
359-
return skip + i
390+
return frame
360391
}
361392
}
362-
return skip
393+
return firstFrame
363394
}
364395

365396
// decorate prefixes the string with the file and line of the call site
366397
// and inserts the final newline if needed and indentation tabs for formatting.
367398
// This function must be called with c.mu held.
368399
func (c *common) decorate(s string) string {
369-
skip := c.frameSkip(3) // decorate + log + public function.
370-
_, file, line, ok := runtime.Caller(skip)
371-
if ok {
400+
frame := c.frameSkip(3) // decorate + log + public function.
401+
file := frame.File
402+
line := frame.Line
403+
if file != "" {
372404
// Truncate file name at last file name separator.
373405
if index := strings.LastIndex(file, "/"); index >= 0 {
374406
file = file[index+1:]
@@ -377,6 +409,8 @@ func (c *common) decorate(s string) string {
377409
}
378410
} else {
379411
file = "???"
412+
}
413+
if line == 0 {
380414
line = 1
381415
}
382416
buf := new(strings.Builder)
@@ -642,8 +676,6 @@ func (c *common) Skipped() bool {
642676
// Helper marks the calling function as a test helper function.
643677
// When printing file and line information, that function will be skipped.
644678
// Helper may be called simultaneously from multiple goroutines.
645-
// Helper has no effect if it is called directly from a TestXxx/BenchmarkXxx
646-
// function or a subtest/sub-benchmark function.
647679
func (c *common) Helper() {
648680
c.mu.Lock()
649681
defer c.mu.Unlock()
@@ -810,13 +842,19 @@ func (t *T) Run(name string, f func(t *T)) bool {
810842
if !ok || shouldFailFast() {
811843
return true
812844
}
845+
// Record the stack trace at the point of this call so that if the subtest
846+
// function - which runs in a separate stack - is marked as a helper, we can
847+
// continue walking the stack into the parent test.
848+
var pc [maxStackLen]uintptr
849+
n := runtime.Callers(2, pc[:])
813850
t = &T{
814851
common: common{
815852
barrier: make(chan bool),
816853
signal: make(chan bool),
817854
name: testName,
818855
parent: &t.common,
819856
level: t.level + 1,
857+
creator: pc[:n],
820858
chatty: t.chatty,
821859
},
822860
context: t.context,

0 commit comments

Comments
 (0)