Skip to content

Commit 51c0bdc

Browse files
author
Bryan C. Mills
committed
testing: flush test summaries to stdout atomically when streaming output
While debugging #40771, I realized that the chatty printer should only ever print to a single io.Writer (normally os.Stdout). The other Writer implementations in the chain write to local buffers, but if we wrote a test's output to a local buffer, then we did *not* write it to stdout and we should not store it as the most recently logged test. Because the chatty printer should only ever print to one place, it shouldn't receive an io.Writer as an argument — rather, it shouldn't be used at all for destinations other than the main output stream. On the other hand, when we flush the output buffer to stdout in the top-level flushToParent call, it is important that we not allow some other test's output to intrude between the test summary header and the remainder of the test's output. cmd/test2json doesn't know how to parse such an intrusion, and it's confusing to humans too. No test because I couldn't reproduce the user-reported error without modifying the testing package. (This behavior seems to be very sensitive to output size and/or goroutine scheduling.) Fixes #40771 Updates #38458 Change-Id: Ic19bf1d535672b096ba1c8583a3b74aab6d6d766 Reviewed-on: https://go-review.googlesource.com/c/go/+/249026 Run-TryBot: Bryan C. Mills <bcmills@google.com> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Jay Conrod <jayconrod@google.com> Reviewed-by: Ian Lance Taylor <iant@golang.org>
1 parent e6583dc commit 51c0bdc

File tree

3 files changed

+88
-87
lines changed

3 files changed

+88
-87
lines changed

src/testing/benchmark.go

+10-8
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ func (b *B) run1() bool {
242242
if b.skipped {
243243
tag = "SKIP"
244244
}
245-
if b.chatty && (len(b.output) > 0 || b.finished) {
245+
if b.chatty != nil && (len(b.output) > 0 || b.finished) {
246246
b.trimOutput()
247247
fmt.Fprintf(b.w, "--- %s: %s\n%s", tag, b.name, b.output)
248248
}
@@ -523,10 +523,9 @@ func runBenchmarks(importPath string, matchString func(pat, str string) (bool, e
523523
}
524524
main := &B{
525525
common: common{
526-
name: "Main",
527-
w: os.Stdout,
528-
chatty: *chatty,
529-
bench: true,
526+
name: "Main",
527+
w: os.Stdout,
528+
bench: true,
530529
},
531530
importPath: importPath,
532531
benchFunc: func(b *B) {
@@ -537,6 +536,9 @@ func runBenchmarks(importPath string, matchString func(pat, str string) (bool, e
537536
benchTime: benchTime,
538537
context: ctx,
539538
}
539+
if Verbose() {
540+
main.chatty = newChattyPrinter(main.w)
541+
}
540542
main.runN(1)
541543
return !main.failed
542544
}
@@ -549,7 +551,7 @@ func (ctx *benchContext) processBench(b *B) {
549551
benchName := benchmarkName(b.name, procs)
550552

551553
// If it's chatty, we've already printed this information.
552-
if !b.chatty {
554+
if b.chatty == nil {
553555
fmt.Fprintf(b.w, "%-*s\t", ctx.maxLen, benchName)
554556
}
555557
// Recompute the running time for all but the first iteration.
@@ -576,7 +578,7 @@ func (ctx *benchContext) processBench(b *B) {
576578
continue
577579
}
578580
results := r.String()
579-
if b.chatty {
581+
if b.chatty != nil {
580582
fmt.Fprintf(b.w, "%-*s\t", ctx.maxLen, benchName)
581583
}
582584
if *benchmarkMemory || b.showAllocResult {
@@ -639,7 +641,7 @@ func (b *B) Run(name string, f func(b *B)) bool {
639641
atomic.StoreInt32(&sub.hasSub, 1)
640642
}
641643

642-
if b.chatty {
644+
if b.chatty != nil {
643645
labelsOnce.Do(func() {
644646
fmt.Printf("goos: %s\n", runtime.GOOS)
645647
fmt.Printf("goarch: %s\n", runtime.GOARCH)

src/testing/sub_test.go

+14-5
Original file line numberDiff line numberDiff line change
@@ -483,10 +483,12 @@ func TestTRun(t *T) {
483483
signal: make(chan bool),
484484
name: "Test",
485485
w: buf,
486-
chatty: tc.chatty,
487486
},
488487
context: ctx,
489488
}
489+
if tc.chatty {
490+
root.chatty = newChattyPrinter(root.w)
491+
}
490492
ok := root.Run(tc.desc, tc.f)
491493
ctx.release()
492494

@@ -665,11 +667,13 @@ func TestBRun(t *T) {
665667
signal: make(chan bool),
666668
name: "root",
667669
w: buf,
668-
chatty: tc.chatty,
669670
},
670671
benchFunc: func(b *B) { ok = b.Run("test", tc.f) }, // Use Run to catch failure.
671672
benchTime: benchTimeFlag{d: 1 * time.Microsecond},
672673
}
674+
if tc.chatty {
675+
root.chatty = newChattyPrinter(root.w)
676+
}
673677
root.runN(1)
674678
if ok != !tc.failed {
675679
t.Errorf("%s:ok: got %v; want %v", tc.desc, ok, !tc.failed)
@@ -741,9 +745,13 @@ func TestParallelSub(t *T) {
741745
}
742746
}
743747

744-
type funcWriter func([]byte) (int, error)
748+
type funcWriter struct {
749+
write func([]byte) (int, error)
750+
}
745751

746-
func (fw funcWriter) Write(b []byte) (int, error) { return fw(b) }
752+
func (fw *funcWriter) Write(b []byte) (int, error) {
753+
return fw.write(b)
754+
}
747755

748756
func TestRacyOutput(t *T) {
749757
var runs int32 // The number of running Writes
@@ -761,9 +769,10 @@ func TestRacyOutput(t *T) {
761769

762770
var wg sync.WaitGroup
763771
root := &T{
764-
common: common{w: funcWriter(raceDetector), chatty: true},
772+
common: common{w: &funcWriter{raceDetector}},
765773
context: newTestContext(1, newMatcher(regexp.MatchString, "", "")),
766774
}
775+
root.chatty = newChattyPrinter(root.w)
767776
root.Run("", func(t *T) {
768777
for i := 0; i < 100; i++ {
769778
wg.Add(1)

src/testing/testing.go

+64-74
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,6 @@ var (
327327
cpuListStr *string
328328
parallel *int
329329
testlog *string
330-
printer *testPrinter
331330

332331
haveExamples bool // are there examples?
333332

@@ -337,55 +336,45 @@ var (
337336
numFailed uint32 // number of test failures
338337
)
339338

340-
type testPrinter struct {
341-
chatty bool
342-
339+
type chattyPrinter struct {
340+
w io.Writer
343341
lastNameMu sync.Mutex // guards lastName
344342
lastName string // last printed test name in chatty mode
345343
}
346344

347-
func newTestPrinter(chatty bool) *testPrinter {
348-
return &testPrinter{
349-
chatty: chatty,
350-
}
345+
func newChattyPrinter(w io.Writer) *chattyPrinter {
346+
return &chattyPrinter{w: w}
351347
}
352348

353-
func (p *testPrinter) Print(testName, out string) {
354-
p.Fprint(os.Stdout, testName, out)
349+
// Updatef prints a message about the status of the named test to w.
350+
//
351+
// The formatted message must include the test name itself.
352+
func (p *chattyPrinter) Updatef(testName, format string, args ...interface{}) {
353+
p.lastNameMu.Lock()
354+
defer p.lastNameMu.Unlock()
355+
356+
// Since the message already implies an association with a specific new test,
357+
// we don't need to check what the old test name was or log an extra CONT line
358+
// for it. (We're updating it anyway, and the current message already includes
359+
// the test name.)
360+
p.lastName = testName
361+
fmt.Fprintf(p.w, format, args...)
355362
}
356363

357-
func (p *testPrinter) Fprint(w io.Writer, testName, out string) {
364+
// Printf prints a message, generated by the named test, that does not
365+
// necessarily mention that tests's name itself.
366+
func (p *chattyPrinter) Printf(testName, format string, args ...interface{}) {
358367
p.lastNameMu.Lock()
359368
defer p.lastNameMu.Unlock()
360369

361-
if !p.chatty ||
362-
strings.HasPrefix(out, "--- PASS: ") ||
363-
strings.HasPrefix(out, "--- FAIL: ") ||
364-
strings.HasPrefix(out, "--- SKIP: ") ||
365-
strings.HasPrefix(out, "=== RUN ") ||
366-
strings.HasPrefix(out, "=== CONT ") ||
367-
strings.HasPrefix(out, "=== PAUSE ") {
368-
// If we're buffering test output (!p.chatty), we don't really care which
369-
// test is emitting which line so long as they are serialized.
370-
//
371-
// If the message already implies an association with a specific new test,
372-
// we don't need to check what the old test name was or log an extra CONT
373-
// line for it. (We're updating it anyway, and the current message already
374-
// includes the test name.)
375-
p.lastName = testName
376-
fmt.Fprint(w, out)
377-
return
378-
}
379-
380370
if p.lastName == "" {
381371
p.lastName = testName
382372
} else if p.lastName != testName {
383-
// Always printed as-is, with 0 decoration or indentation. So, we skip
384-
// printing to w.
385-
fmt.Printf("=== CONT %s\n", testName)
373+
fmt.Fprintf(p.w, "=== CONT %s\n", testName)
386374
p.lastName = testName
387375
}
388-
fmt.Fprint(w, out)
376+
377+
fmt.Fprintf(p.w, format, args...)
389378
}
390379

391380
// The maximum number of stack frames to go through when skipping helper functions for
@@ -407,12 +396,12 @@ type common struct {
407396
cleanupName string // Name of the cleanup function.
408397
cleanupPc []uintptr // The stack trace at the point where Cleanup was called.
409398

410-
chatty bool // A copy of the chatty flag.
411-
bench bool // Whether the current test is a benchmark.
412-
finished bool // Test function has completed.
413-
hasSub int32 // Written atomically.
414-
raceErrors int // Number of races detected during test.
415-
runner string // Function name of tRunner running the test.
399+
chatty *chattyPrinter // A copy of chattyPrinter, if the chatty flag is set.
400+
bench bool // Whether the current test is a benchmark.
401+
finished bool // Test function has completed.
402+
hasSub int32 // Written atomically.
403+
raceErrors int // Number of races detected during test.
404+
runner string // Function name of tRunner running the test.
416405

417406
parent *common
418407
level int // Nesting depth of test or benchmark.
@@ -574,12 +563,31 @@ func (c *common) flushToParent(testName, format string, args ...interface{}) {
574563
p.mu.Lock()
575564
defer p.mu.Unlock()
576565

577-
printer.Fprint(p.w, testName, fmt.Sprintf(format, args...))
578-
579566
c.mu.Lock()
580567
defer c.mu.Unlock()
581-
io.Copy(p.w, bytes.NewReader(c.output))
582-
c.output = c.output[:0]
568+
569+
if len(c.output) > 0 {
570+
format += "%s"
571+
args = append(args[:len(args):len(args)], c.output)
572+
c.output = c.output[:0] // but why?
573+
}
574+
575+
if c.chatty != nil && p.w == c.chatty.w {
576+
// We're flushing to the actual output, so track that this output is
577+
// associated with a specific test (and, specifically, that the next output
578+
// is *not* associated with that test).
579+
//
580+
// Moreover, if c.output is non-empty it is important that this write be
581+
// atomic with respect to the output of other tests, so that we don't end up
582+
// with confusing '=== CONT' lines in the middle of our '--- PASS' block.
583+
// Neither humans nor cmd/test2json can parse those easily.
584+
// (See https://golang.org/issue/40771.)
585+
c.chatty.Updatef(testName, format, args...)
586+
} else {
587+
// We're flushing to the output buffer of the parent test, which will
588+
// itself follow a test-name header when it is finally flushed to stdout.
589+
fmt.Fprintf(p.w, format, args...)
590+
}
583591
}
584592

585593
type indenter struct {
@@ -748,13 +756,13 @@ func (c *common) logDepth(s string, depth int) {
748756
}
749757
panic("Log in goroutine after " + c.name + " has completed")
750758
} else {
751-
if c.chatty {
759+
if c.chatty != nil {
752760
if c.bench {
753761
// Benchmarks don't print === CONT, so we should skip the test
754762
// printer and just print straight to stdout.
755763
fmt.Print(c.decorate(s, depth+1))
756764
} else {
757-
printer.Print(c.name, c.decorate(s, depth+1))
765+
c.chatty.Printf(c.name, "%s", c.decorate(s, depth+1))
758766
}
759767

760768
return
@@ -1003,34 +1011,22 @@ func (t *T) Parallel() {
10031011
t.parent.sub = append(t.parent.sub, t)
10041012
t.raceErrors += race.Errors()
10051013

1006-
if t.chatty {
1007-
// Print directly to root's io.Writer so there is no delay.
1008-
root := t.parent
1009-
for ; root.parent != nil; root = root.parent {
1010-
}
1011-
root.mu.Lock()
1014+
if t.chatty != nil {
10121015
// Unfortunately, even though PAUSE indicates that the named test is *no
10131016
// longer* running, cmd/test2json interprets it as changing the active test
10141017
// for the purpose of log parsing. We could fix cmd/test2json, but that
10151018
// won't fix existing deployments of third-party tools that already shell
10161019
// out to older builds of cmd/test2json — so merely fixing cmd/test2json
10171020
// isn't enough for now.
1018-
printer.Fprint(root.w, t.name, fmt.Sprintf("=== PAUSE %s\n", t.name))
1019-
root.mu.Unlock()
1021+
t.chatty.Updatef(t.name, "=== PAUSE %s\n", t.name)
10201022
}
10211023

10221024
t.signal <- true // Release calling test.
10231025
<-t.parent.barrier // Wait for the parent test to complete.
10241026
t.context.waitParallel()
10251027

1026-
if t.chatty {
1027-
// Print directly to root's io.Writer so there is no delay.
1028-
root := t.parent
1029-
for ; root.parent != nil; root = root.parent {
1030-
}
1031-
root.mu.Lock()
1032-
printer.Fprint(root.w, t.name, fmt.Sprintf("=== CONT %s\n", t.name))
1033-
root.mu.Unlock()
1028+
if t.chatty != nil {
1029+
t.chatty.Updatef(t.name, "=== CONT %s\n", t.name)
10341030
}
10351031

10361032
t.start = time.Now()
@@ -1181,14 +1177,8 @@ func (t *T) Run(name string, f func(t *T)) bool {
11811177
}
11821178
t.w = indenter{&t.common}
11831179

1184-
if t.chatty {
1185-
// Print directly to root's io.Writer so there is no delay.
1186-
root := t.parent
1187-
for ; root.parent != nil; root = root.parent {
1188-
}
1189-
root.mu.Lock()
1190-
printer.Fprint(root.w, t.name, fmt.Sprintf("=== RUN %s\n", t.name))
1191-
root.mu.Unlock()
1180+
if t.chatty != nil {
1181+
t.chatty.Updatef(t.name, "=== RUN %s\n", t.name)
11921182
}
11931183
// Instead of reducing the running count of this test before calling the
11941184
// tRunner and increasing it afterwards, we rely on tRunner keeping the
@@ -1355,8 +1345,6 @@ func (m *M) Run() (code int) {
13551345
flag.Parse()
13561346
}
13571347

1358-
printer = newTestPrinter(Verbose())
1359-
13601348
if *parallel < 1 {
13611349
fmt.Fprintln(os.Stderr, "testing: -parallel can only be given a positive integer")
13621350
flag.Usage()
@@ -1401,7 +1389,7 @@ func (t *T) report() {
14011389
format := "--- %s: %s (%s)\n"
14021390
if t.Failed() {
14031391
t.flushToParent(t.name, format, "FAIL", t.name, dstr)
1404-
} else if t.chatty {
1392+
} else if t.chatty != nil {
14051393
if t.Skipped() {
14061394
t.flushToParent(t.name, format, "SKIP", t.name, dstr)
14071395
} else {
@@ -1462,10 +1450,12 @@ func runTests(matchString func(pat, str string) (bool, error), tests []InternalT
14621450
signal: make(chan bool),
14631451
barrier: make(chan bool),
14641452
w: os.Stdout,
1465-
chatty: *chatty,
14661453
},
14671454
context: ctx,
14681455
}
1456+
if Verbose() {
1457+
t.chatty = newChattyPrinter(t.w)
1458+
}
14691459
tRunner(t, func(t *T) {
14701460
for _, test := range tests {
14711461
t.Run(test.Name, test.F)

0 commit comments

Comments
 (0)