Skip to content

testing: add Output #71575

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 35 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
cf4506e
Add a new output writer and use it in t.Log
Feb 5, 2025
dfd2396
Refactor output writer implementation
Feb 12, 2025
a233f16
Fix output indentation
Feb 18, 2025
7ecfb1a
Remove call site from outputWriter
Feb 19, 2025
521b634
Add outputWriter as a common field
Feb 20, 2025
cecfa65
Add outputWriter tests
Mar 5, 2025
a90fc68
Build line as bytes instead of string
Mar 6, 2025
f556750
Name outputWriter fields when initialising
Mar 10, 2025
7785719
Refactor chatty case of writeLine
Mar 10, 2025
6ed9fbd
Address feedback
Mar 10, 2025
53cc952
Refactor outputWriter.Write
Mar 10, 2025
2b3bd9a
Test outputWriter directly
Mar 10, 2025
6f9d54a
Make indent global, rename line length in outputWriter.Write
Mar 10, 2025
2386803
Remove unnecessary variable in outputWriter.Write
Mar 11, 2025
010277f
Improve variable naming
Mar 13, 2025
da48f4d
Use ReplaceAll, TrimSuffix in log
Mar 13, 2025
4a5b4a7
Convert indent to string
Mar 13, 2025
8dda922
Lock common mutex before appending to buffer
Mar 13, 2025
cdd09ba
Add common method setOutputWriter
Mar 14, 2025
90996a3
Refactor writeLine signature
Mar 14, 2025
d4b63d6
Check if test is done in log method
Apr 10, 2025
91934a0
Refactor selection of nesting level for test output
Apr 10, 2025
0eb55d7
Flush outputWriter's buffer when test finishes
Apr 18, 2025
afa16e4
Add outputWriter chatty and json test cases
Apr 21, 2025
d8ebbea
Add Output method
Apr 24, 2025
f9bd701
Select outputWriter in Output
Apr 25, 2025
9972e60
Improve documentation and field naming of outputWriter
Apr 29, 2025
80ed937
Use os.PathSeparator in callSite
Apr 29, 2025
7143d3f
Refactor outputWriter Write method
Apr 29, 2025
9ef20be
Refactor addition of indentation to line
Apr 29, 2025
6818a54
Refactor writeLine
Apr 29, 2025
29df600
Fix setting of indentation in log
Apr 29, 2025
15c7a59
Replace outputWriter with t.Output in some tests
Apr 30, 2025
3a0cc20
Add release notes
Apr 30, 2025
6ea1cd0
Add check for test completion when calling Write
Apr 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions api/next/59928.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pkg testing, method (*B) Output() io.Writer #59928
pkg testing, method (*F) Output() io.Writer #59928
pkg testing, method (*T) Output() io.Writer #59928
3 changes: 3 additions & 0 deletions doc/next/6-stdlib/99-minor/testing/59928.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<!-- go.dev/issue/59928 -->

The new [Output] method of [testing.T], [testing.B] and [testing.F] provides a Writer that writes to the same test output stream as [TB.Log].
3 changes: 3 additions & 0 deletions src/testing/benchmark.go
Original file line number Diff line number Diff line change
Expand Up @@ -755,6 +755,7 @@ func (s *benchState) processBench(b *B) {
benchFunc: b.benchFunc,
benchTime: b.benchTime,
}
b.setOutputWriter()
b.run1()
}
r := b.doBench()
Expand Down Expand Up @@ -831,6 +832,7 @@ func (b *B) Run(name string, f func(b *B)) bool {
benchTime: b.benchTime,
bstate: b.bstate,
}
sub.setOutputWriter()
if partial {
// Partial name match, like -bench=X/Y matching BenchmarkX.
// Only process sub-benchmarks, if any.
Expand Down Expand Up @@ -1007,6 +1009,7 @@ func Benchmark(f func(b *B)) BenchmarkResult {
benchFunc: f,
benchTime: benchTime,
}
b.setOutputWriter()
if b.run1() {
b.run()
}
Expand Down
2 changes: 2 additions & 0 deletions src/testing/fuzz.go
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,7 @@ func (f *F) Fuzz(ff any) {
t.parent.w = captureOut
}
t.w = indenter{&t.common}
t.setOutputWriter()
if t.chatty != nil {
t.chatty.Updatef(t.name, "=== RUN %s\n", t.name)
}
Expand Down Expand Up @@ -529,6 +530,7 @@ func runFuzzTests(deps testDeps, fuzzTests []InternalFuzzTarget, deadline time.T
fstate: fstate,
}
f.w = indenter{&f.common}
f.setOutputWriter()
if f.chatty != nil {
f.chatty.Updatef(f.name, "=== RUN %s\n", f.name)
}
Expand Down
229 changes: 229 additions & 0 deletions src/testing/sub_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -989,3 +989,232 @@ func TestNestedCleanup(t *T) {
t.Errorf("unexpected cleanup count: got %d want 3", ranCleanup)
}
}

func TestOutput(t *T) {
o := t.Output()
if o != t.o {
t.Errorf("outputWriter:\ngot:\n%+v\nwant:\n%+v", o, t.o)
}

testCases := []struct {
in string
out string
buf string
}{{
in: "a",
out: "",
buf: "a",
}, {
in: "b",
out: "",
buf: "ab",
}, {
in: "\n",
out: " ab\n",
buf: "",
}, {
in: "\nc",
out: " ab\n \n",
buf: "c",
}, {
in: "d",
out: " ab\n \n",
buf: "cd",
}}
for _, tc := range testCases {
o.Write([]byte(tc.in))
if string(t.output) != tc.out {
t.Errorf("output:\ngot:\n%s\nwant:\n%s", t.output, tc.out)
}
if string(t.o.partial) != tc.buf {
t.Errorf("buffer:\ngot:\n%s\nwant:\n%s", t.o.partial, tc.buf)
}
}
}

func TestOutputFlushing(t *T) {
testCases := []struct {
desc string
chatty bool
json bool
output string
f func(*T)
}{{
desc: "buffered output gets flushed at test end",
output: `
--- FAIL: buffered output gets flushed at test end (0.00s)
--- FAIL: buffered output gets flushed at test end/#00 (0.00s)
a
b`,
f: func(t *T) {
t.Run("", func(t *T) {
o := t.Output()
o.Write([]byte("a\n"))
o.Write([]byte("b"))
t.Fail()
})
},
}, {
desc: "with chatty",
chatty: true,
output: `
=== RUN with chatty
=== RUN with chatty/#00
a
b
--- PASS: with chatty (0.00s)
--- PASS: with chatty/#00 (0.00s)`,
f: func(t *T) {
t.Run("", func(t *T) {
o := t.Output()
o.Write([]byte("a\n"))
o.Write([]byte("b"))
})
},
}, {
desc: "with chatty and json",
chatty: true,
json: true,
output: `
^V=== RUN with chatty and json
^V=== RUN with chatty and json/#00
a
b
^V--- PASS: with chatty and json/#00 (0.00s)
^V=== NAME with chatty and json
^V--- PASS: with chatty and json (0.00s)
^V=== NAME
`,
f: func(t *T) {
t.Run("", func(t *T) {
o := t.Output()
o.Write([]byte("a\n"))
o.Write([]byte("b"))
})
},
}}
for _, tc := range testCases {
t.Run(tc.desc, func(t *T) {
tstate := newTestState(1, allMatcher())
buf := &strings.Builder{}
root := &T{
common: common{
w: buf,
},
tstate: tstate,
}

if tc.chatty {
root.chatty = newChattyPrinter(root.w)
root.chatty.json = tc.json
}

root.Run(tc.desc, tc.f)

got := strings.TrimSpace(buf.String())

want := strings.TrimSpace(tc.output)
re := makeRegexp(want)
if ok, err := regexp.MatchString(re, got); !ok || err != nil {
t.Errorf("output:\ngot:\n%s\nwant:\n%s", got, want)
}
})
}
}

func TestOutputAfterComplete(t *T) {
tstate := newTestState(1, allMatcher())
var buf bytes.Buffer
t1 := &T{
common: common{
// Use a buffered channel so that tRunner can write
// to it although nothing is reading from it.
signal: make(chan bool, 1),
w: &buf,
},
tstate: tstate,
}

c1 := make(chan bool)
c2 := make(chan string)
tRunner(t1, func(t *T) {
t.Run("TestLateOutput", func(t *T) {
go func() {
defer close(c2)
defer func() {
p := recover()
if p == nil {
c2 <- "subtest did not panic"
return
}
s, ok := p.(string)
if !ok {
c2 <- fmt.Sprintf("subtest panic with unexpected value %v", p)
return
}
const want = "Output called after TestLateOutput has completed"
if !strings.Contains(s, want) {
c2 <- fmt.Sprintf("subtest panic %q does not contain %q", s, want)
}
}()

<-c1
t.Output()
}()
})
})
close(c1)

if s := <-c2; s != "" {
t.Error(s)
}
}

func TestWriteAfterComplete(t *T) {
tstate := newTestState(1, allMatcher())
var buf bytes.Buffer
t1 := &T{
common: common{
// Use a buffered channel so that tRunner can write
// to it although nothing is reading from it.
signal: make(chan bool, 1),
w: &buf,
},
tstate: tstate,
}

c1 := make(chan bool)
c2 := make(chan string)
tRunner(t1, func(t *T) {
t.Run("TestLateWrite", func(t *T) {
o := t.Output()
go func() {
defer close(c2)
defer func() {
p := recover()
if p == nil {
c2 <- "subtest did not panic"
return
}
s, ok := p.(string)
if !ok {
c2 <- fmt.Sprintf("subtest panic with unexpected value %v", p)
return
}
const want = "Write called after TestLateWrite has completed"
if !strings.Contains(s, want) {
c2 <- fmt.Sprintf("subtest panic %q does not contain %q", s, want)
}
}()

<-c1
o.Write([]byte("write after test"))
}()
})
})
close(c1)

if s := <-c2; s != "" {
t.Error(s)
}
}
Loading