Skip to content

Commit 26c6b51

Browse files
Bryan C. Millsdmitshur
Bryan C. Mills
authored andcommitted
[release-branch.go1.14] 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 #40880 Updates #40771 Updates #38458 Change-Id: Ic19bf1d535672b096ba1c8583a3b74aab6d6d766 Reviewed-on: https://go-review.googlesource.com/c/go/+/249026 Run-TryBot: Bryan C. Mills <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Jay Conrod <[email protected]> Reviewed-by: Ian Lance Taylor <[email protected]> (cherry picked from commit 51c0bdc) Reviewed-on: https://go-review.googlesource.com/c/go/+/252638 TryBot-Result: Go Bot <[email protected]> Trust: Bryan C. Mills <[email protected]>
1 parent fe30482 commit 26c6b51

File tree

3 files changed

+88
-87
lines changed

3 files changed

+88
-87
lines changed

src/testing/benchmark.go

Lines changed: 10 additions & 8 deletions
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

Lines changed: 14 additions & 5 deletions
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

Lines changed: 64 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,6 @@ var (
320320
cpuListStr *string
321321
parallel *int
322322
testlog *string
323-
printer *testPrinter
324323

325324
haveExamples bool // are there examples?
326325

@@ -330,55 +329,45 @@ var (
330329
numFailed uint32 // number of test failures
331330
)
332331

333-
type testPrinter struct {
334-
chatty bool
335-
332+
type chattyPrinter struct {
333+
w io.Writer
336334
lastNameMu sync.Mutex // guards lastName
337335
lastName string // last printed test name in chatty mode
338336
}
339337

340-
func newTestPrinter(chatty bool) *testPrinter {
341-
return &testPrinter{
342-
chatty: chatty,
343-
}
338+
func newChattyPrinter(w io.Writer) *chattyPrinter {
339+
return &chattyPrinter{w: w}
344340
}
345341

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

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

354-
if !p.chatty ||
355-
strings.HasPrefix(out, "--- PASS: ") ||
356-
strings.HasPrefix(out, "--- FAIL: ") ||
357-
strings.HasPrefix(out, "--- SKIP: ") ||
358-
strings.HasPrefix(out, "=== RUN ") ||
359-
strings.HasPrefix(out, "=== CONT ") ||
360-
strings.HasPrefix(out, "=== PAUSE ") {
361-
// If we're buffering test output (!p.chatty), we don't really care which
362-
// test is emitting which line so long as they are serialized.
363-
//
364-
// If the message already implies an association with a specific new test,
365-
// we don't need to check what the old test name was or log an extra CONT
366-
// line for it. (We're updating it anyway, and the current message already
367-
// includes the test name.)
368-
p.lastName = testName
369-
fmt.Fprint(w, out)
370-
return
371-
}
372-
373363
if p.lastName == "" {
374364
p.lastName = testName
375365
} else if p.lastName != testName {
376-
// Always printed as-is, with 0 decoration or indentation. So, we skip
377-
// printing to w.
378-
fmt.Printf("=== CONT %s\n", testName)
366+
fmt.Fprintf(p.w, "=== CONT %s\n", testName)
379367
p.lastName = testName
380368
}
381-
fmt.Fprint(w, out)
369+
370+
fmt.Fprintf(p.w, format, args...)
382371
}
383372

384373
// The maximum number of stack frames to go through when skipping helper functions for
@@ -398,12 +387,12 @@ type common struct {
398387
helpers map[string]struct{} // functions to be skipped when writing file/line info
399388
cleanup func() // optional function to be called at the end of the test
400389

401-
chatty bool // A copy of the chatty flag.
402-
bench bool // Whether the current test is a benchmark.
403-
finished bool // Test function has completed.
404-
hasSub int32 // Written atomically.
405-
raceErrors int // Number of races detected during test.
406-
runner string // Function name of tRunner running the test.
390+
chatty *chattyPrinter // A copy of chattyPrinter, if the chatty flag is set.
391+
bench bool // Whether the current test is a benchmark.
392+
finished bool // Test function has completed.
393+
hasSub int32 // Written atomically.
394+
raceErrors int // Number of races detected during test.
395+
runner string // Function name of tRunner running the test.
407396

408397
parent *common
409398
level int // Nesting depth of test or benchmark.
@@ -556,12 +545,31 @@ func (c *common) flushToParent(testName, format string, args ...interface{}) {
556545
p.mu.Lock()
557546
defer p.mu.Unlock()
558547

559-
printer.Fprint(p.w, testName, fmt.Sprintf(format, args...))
560-
561548
c.mu.Lock()
562549
defer c.mu.Unlock()
563-
io.Copy(p.w, bytes.NewReader(c.output))
564-
c.output = c.output[:0]
550+
551+
if len(c.output) > 0 {
552+
format += "%s"
553+
args = append(args[:len(args):len(args)], c.output)
554+
c.output = c.output[:0] // but why?
555+
}
556+
557+
if c.chatty != nil && p.w == c.chatty.w {
558+
// We're flushing to the actual output, so track that this output is
559+
// associated with a specific test (and, specifically, that the next output
560+
// is *not* associated with that test).
561+
//
562+
// Moreover, if c.output is non-empty it is important that this write be
563+
// atomic with respect to the output of other tests, so that we don't end up
564+
// with confusing '=== CONT' lines in the middle of our '--- PASS' block.
565+
// Neither humans nor cmd/test2json can parse those easily.
566+
// (See https://golang.org/issue/40771.)
567+
c.chatty.Updatef(testName, format, args...)
568+
} else {
569+
// We're flushing to the output buffer of the parent test, which will
570+
// itself follow a test-name header when it is finally flushed to stdout.
571+
fmt.Fprintf(p.w, format, args...)
572+
}
565573
}
566574

567575
type indenter struct {
@@ -729,13 +737,13 @@ func (c *common) logDepth(s string, depth int) {
729737
}
730738
panic("Log in goroutine after " + c.name + " has completed")
731739
} else {
732-
if c.chatty {
740+
if c.chatty != nil {
733741
if c.bench {
734742
// Benchmarks don't print === CONT, so we should skip the test
735743
// printer and just print straight to stdout.
736744
fmt.Print(c.decorate(s, depth+1))
737745
} else {
738-
printer.Print(c.name, c.decorate(s, depth+1))
746+
c.chatty.Printf(c.name, "%s", c.decorate(s, depth+1))
739747
}
740748

741749
return
@@ -910,34 +918,22 @@ func (t *T) Parallel() {
910918
t.parent.sub = append(t.parent.sub, t)
911919
t.raceErrors += race.Errors()
912920

913-
if t.chatty {
914-
// Print directly to root's io.Writer so there is no delay.
915-
root := t.parent
916-
for ; root.parent != nil; root = root.parent {
917-
}
918-
root.mu.Lock()
921+
if t.chatty != nil {
919922
// Unfortunately, even though PAUSE indicates that the named test is *no
920923
// longer* running, cmd/test2json interprets it as changing the active test
921924
// for the purpose of log parsing. We could fix cmd/test2json, but that
922925
// won't fix existing deployments of third-party tools that already shell
923926
// out to older builds of cmd/test2json — so merely fixing cmd/test2json
924927
// isn't enough for now.
925-
printer.Fprint(root.w, t.name, fmt.Sprintf("=== PAUSE %s\n", t.name))
926-
root.mu.Unlock()
928+
t.chatty.Updatef(t.name, "=== PAUSE %s\n", t.name)
927929
}
928930

929931
t.signal <- true // Release calling test.
930932
<-t.parent.barrier // Wait for the parent test to complete.
931933
t.context.waitParallel()
932934

933-
if t.chatty {
934-
// Print directly to root's io.Writer so there is no delay.
935-
root := t.parent
936-
for ; root.parent != nil; root = root.parent {
937-
}
938-
root.mu.Lock()
939-
printer.Fprint(root.w, t.name, fmt.Sprintf("=== CONT %s\n", t.name))
940-
root.mu.Unlock()
935+
if t.chatty != nil {
936+
t.chatty.Updatef(t.name, "=== CONT %s\n", t.name)
941937
}
942938

943939
t.start = time.Now()
@@ -1088,14 +1084,8 @@ func (t *T) Run(name string, f func(t *T)) bool {
10881084
}
10891085
t.w = indenter{&t.common}
10901086

1091-
if t.chatty {
1092-
// Print directly to root's io.Writer so there is no delay.
1093-
root := t.parent
1094-
for ; root.parent != nil; root = root.parent {
1095-
}
1096-
root.mu.Lock()
1097-
printer.Fprint(root.w, t.name, fmt.Sprintf("=== RUN %s\n", t.name))
1098-
root.mu.Unlock()
1087+
if t.chatty != nil {
1088+
t.chatty.Updatef(t.name, "=== RUN %s\n", t.name)
10991089
}
11001090
// Instead of reducing the running count of this test before calling the
11011091
// tRunner and increasing it afterwards, we rely on tRunner keeping the
@@ -1242,8 +1232,6 @@ func (m *M) Run() int {
12421232
flag.Parse()
12431233
}
12441234

1245-
printer = newTestPrinter(Verbose())
1246-
12471235
if *parallel < 1 {
12481236
fmt.Fprintln(os.Stderr, "testing: -parallel can only be given a positive integer")
12491237
flag.Usage()
@@ -1284,7 +1272,7 @@ func (t *T) report() {
12841272
format := "--- %s: %s (%s)\n"
12851273
if t.Failed() {
12861274
t.flushToParent(t.name, format, "FAIL", t.name, dstr)
1287-
} else if t.chatty {
1275+
} else if t.chatty != nil {
12881276
if t.Skipped() {
12891277
t.flushToParent(t.name, format, "SKIP", t.name, dstr)
12901278
} else {
@@ -1340,10 +1328,12 @@ func runTests(matchString func(pat, str string) (bool, error), tests []InternalT
13401328
signal: make(chan bool),
13411329
barrier: make(chan bool),
13421330
w: os.Stdout,
1343-
chatty: *chatty,
13441331
},
13451332
context: ctx,
13461333
}
1334+
if Verbose() {
1335+
t.chatty = newChattyPrinter(t.w)
1336+
}
13471337
tRunner(t, func(t *T) {
13481338
for _, test := range tests {
13491339
t.Run(test.Name, test.F)

0 commit comments

Comments
 (0)