diff --git a/internal/junitxml/report.go b/internal/junitxml/report.go index f821e2e0..9857f6ba 100644 --- a/internal/junitxml/report.go +++ b/internal/junitxml/report.go @@ -3,6 +3,7 @@ package junitxml import ( + "bytes" "encoding/xml" "fmt" "io" @@ -172,10 +173,12 @@ func packageTestCases(pkg *testjson.Package, formatClassname FormatFunc) []JUnit cases := []JUnitTestCase{} if pkg.TestMainFailed() { + var buf bytes.Buffer + pkg.WriteOutputTo(&buf, 0) //nolint:errcheck jtc := newJUnitTestCase(testjson.TestCase{Test: "TestMain"}, formatClassname) jtc.Failure = &JUnitFailure{ Message: "Failed", - Contents: pkg.Output(0), + Contents: buf.String(), } cases = append(cases, jtc) } diff --git a/testjson/dotformat.go b/testjson/dotformat.go index fd8a2936..625cd87a 100644 --- a/testjson/dotformat.go +++ b/testjson/dotformat.go @@ -1,6 +1,7 @@ package testjson import ( + "bufio" "fmt" "io" "os" @@ -13,15 +14,21 @@ import ( "gotest.tools/gotestsum/internal/log" ) -func dotsFormatV1(event TestEvent, exec *Execution) string { - pkg := exec.Package(event.Package) - switch { - case event.PackageEvent(): - return "" - case event.Action == ActionRun && pkg.Total == 1: - return "[" + RelativePackagePath(event.Package) + "]" - } - return fmtDot(event) +func dotsFormatV1(out io.Writer) EventFormatter { + buf := bufio.NewWriter(out) + // nolint:errcheck + return eventFormatterFunc(func(event TestEvent, exec *Execution) error { + pkg := exec.Package(event.Package) + switch { + case event.PackageEvent(): + return nil + case event.Action == ActionRun && pkg.Total == 1: + buf.WriteString("[" + RelativePackagePath(event.Package) + "]") + return buf.Flush() + } + buf.WriteString(fmtDot(event)) + return buf.Flush() + }) } func fmtDot(event TestEvent) string { @@ -72,7 +79,7 @@ func newDotFormatter(out io.Writer, opts FormatOptions) EventFormatter { w, _, err := term.GetSize(int(os.Stdout.Fd())) if err != nil || w == 0 { log.Warnf("Failed to detect terminal width for dots format, error: %v", err) - return &formatAdapter{format: dotsFormatV1, out: out} + return dotsFormatV1(out) } return &dotFormatter{ pkgs: make(map[string]*dotLine), diff --git a/testjson/execution.go b/testjson/execution.go index ae9fbae0..49111d30 100644 --- a/testjson/execution.go +++ b/testjson/execution.go @@ -151,13 +151,24 @@ func (p *Package) LastFailedByName(name string) TestCase { return TestCase{} } -// Output returns the full test output for a test. +// Output returns the full test output for a test. Unlike OutputLines() it does +// not return lines from subtests in some cases. // -// Unlike OutputLines() it does not return lines from subtests in some cases. +// Deprecated: use WriteOutputTo to avoid lots of allocation func (p *Package) Output(id int) string { return strings.Join(p.output[id], "") } +// WriteOutputTo writes the output for TestCase with id to out. +func (p *Package) WriteOutputTo(out io.StringWriter, id int) error { + for _, v := range p.output[id] { + if _, err := out.WriteString(v); err != nil { + return err + } + } + return nil +} + // OutputLines returns the full test output for a test as a slice of strings. // // As a workaround for test output being attributed to the wrong subtest, if: diff --git a/testjson/format.go b/testjson/format.go index 8b052cbd..6fd0dcee 100644 --- a/testjson/format.go +++ b/testjson/format.go @@ -9,37 +9,48 @@ import ( "github.com/fatih/color" ) -func debugFormat(event TestEvent, _ *Execution) string { - return fmt.Sprintf("%s %s %s (%.3f) [%d] %s\n", - event.Package, - event.Test, - event.Action, - event.Elapsed, - event.Time.Unix(), - event.Output) +func debugFormat(out io.Writer) eventFormatterFunc { + return func(event TestEvent, _ *Execution) error { + _, err := fmt.Fprintf(out, "%s %s %s (%.3f) [%d] %s\n", + event.Package, + event.Test, + event.Action, + event.Elapsed, + event.Time.Unix(), + event.Output) + return err + } } // go test -v -func standardVerboseFormat(event TestEvent, _ *Execution) string { - if event.Action == ActionOutput { - return event.Output - } - return "" +func standardVerboseFormat(out io.Writer) EventFormatter { + buf := bufio.NewWriter(out) + return eventFormatterFunc(func(event TestEvent, _ *Execution) error { + if event.Action == ActionOutput { + _, _ = buf.WriteString(event.Output) + return buf.Flush() + } + return nil + }) } // go test -func standardQuietFormat(event TestEvent, _ *Execution) string { - if !event.PackageEvent() { - return "" - } - if event.Output == "PASS\n" || isCoverageOutput(event.Output) { - return "" - } - if isWarningNoTestsToRunOutput(event.Output) { - return "" - } +func standardQuietFormat(out io.Writer) EventFormatter { + buf := bufio.NewWriter(out) + return eventFormatterFunc(func(event TestEvent, _ *Execution) error { + if !event.PackageEvent() { + return nil + } + if event.Output == "PASS\n" || isCoverageOutput(event.Output) { + return nil + } + if isWarningNoTestsToRunOutput(event.Output) { + return nil + } - return event.Output + _, _ = buf.WriteString(event.Output) + return buf.Flush() + }) } // go test -json @@ -53,43 +64,54 @@ func standardJSONFormat(out io.Writer) EventFormatter { }) } -func testNameFormat(event TestEvent, exec *Execution) string { - result := colorEvent(event)(strings.ToUpper(string(event.Action))) - formatTest := func() string { - pkgPath := RelativePackagePath(event.Package) +func testNameFormat(out io.Writer) EventFormatter { + buf := bufio.NewWriter(out) + // nolint:errcheck + return eventFormatterFunc(func(event TestEvent, exec *Execution) error { + formatTest := func() error { + pkgPath := RelativePackagePath(event.Package) + + fmt.Fprintf(buf, "%s %s%s %s\n", + colorEvent(event)(strings.ToUpper(string(event.Action))), + joinPkgToTestName(pkgPath, event.Test), + formatRunID(event.RunID), + event.ElapsedFormatted()) + return buf.Flush() + } - return fmt.Sprintf("%s %s%s %s\n", - result, - joinPkgToTestName(pkgPath, event.Test), - formatRunID(event.RunID), - event.ElapsedFormatted()) - } + switch { + case isPkgFailureOutput(event): + buf.WriteString(event.Output) + return buf.Flush() - switch { - case isPkgFailureOutput(event): - return event.Output + case event.PackageEvent(): + if !event.Action.IsTerminal() { + return nil + } - case event.PackageEvent(): - if !event.Action.IsTerminal() { - return "" - } - pkg := exec.Package(event.Package) - if event.Action == ActionSkip || (event.Action == ActionPass && pkg.Total == 0) { - result = colorEvent(event)("EMPTY") - } + result := colorEvent(event)(strings.ToUpper(string(event.Action))) + pkg := exec.Package(event.Package) + if event.Action == ActionSkip || (event.Action == ActionPass && pkg.Total == 0) { + result = colorEvent(event)("EMPTY") + } - event.Elapsed = 0 // hide elapsed for now, for backwards compat - return result + " " + packageLine(event, exec.Package(event.Package)) + event.Elapsed = 0 // hide elapsed for now, for backwards compat + buf.WriteString(result) + buf.WriteRune(' ') + buf.WriteString(packageLine(event, exec.Package(event.Package))) + return buf.Flush() - case event.Action == ActionFail: - pkg := exec.Package(event.Package) - tc := pkg.LastFailedByName(event.Test) - return pkg.Output(tc.ID) + formatTest() + case event.Action == ActionFail: + pkg := exec.Package(event.Package) + tc := pkg.LastFailedByName(event.Test) + pkg.WriteOutputTo(buf, tc.ID) + return formatTest() - case event.Action == ActionPass: - return formatTest() - } - return "" + case event.Action == ActionPass: + return formatTest() + } + return nil + }) } // joinPkgToTestName for formatting. @@ -140,12 +162,14 @@ func all(cond ...bool) bool { return true } -func pkgNameFormat(opts FormatOptions) func(event TestEvent, exec *Execution) string { - return func(event TestEvent, exec *Execution) string { +func pkgNameFormat(out io.Writer, opts FormatOptions) eventFormatterFunc { + buf := bufio.NewWriter(out) + return func(event TestEvent, exec *Execution) error { if !event.PackageEvent() { - return "" + return nil } - return shortFormatPackageEvent(opts, event, exec) + _, _ = buf.WriteString(shortFormatPackageEvent(opts, event, exec)) + return buf.Flush() } } @@ -210,17 +234,20 @@ func packageLine(event TestEvent, pkg *Package) string { return buf.String() } -func pkgNameWithFailuresFormat(opts FormatOptions) func(event TestEvent, exec *Execution) string { - return func(event TestEvent, exec *Execution) string { +func pkgNameWithFailuresFormat(out io.Writer, opts FormatOptions) eventFormatterFunc { + buf := bufio.NewWriter(out) + return func(event TestEvent, exec *Execution) error { if !event.PackageEvent() { if event.Action == ActionFail { pkg := exec.Package(event.Package) tc := pkg.LastFailedByName(event.Test) - return pkg.Output(tc.ID) + pkg.WriteOutputTo(buf, tc.ID) // nolint:errcheck + return buf.Flush() } - return "" + return nil } - return shortFormatPackageEvent(opts, event, exec) + buf.WriteString(shortFormatPackageEvent(opts, event, exec)) // nolint:errcheck + return buf.Flush() } } @@ -259,35 +286,24 @@ func NewEventFormatter(out io.Writer, format string, formatOpts FormatOptions) E case "none": return eventFormatterFunc(func(TestEvent, *Execution) error { return nil }) case "debug": - return &formatAdapter{out, debugFormat} + return debugFormat(out) case "standard-json": return standardJSONFormat(out) case "standard-verbose": - return &formatAdapter{out, standardVerboseFormat} + return standardVerboseFormat(out) case "standard-quiet": - return &formatAdapter{out, standardQuietFormat} + return standardQuietFormat(out) case "dots", "dots-v1": - return &formatAdapter{out, dotsFormatV1} + return dotsFormatV1(out) case "dots-v2": return newDotFormatter(out, formatOpts) case "testname", "short-verbose": - return &formatAdapter{out, testNameFormat} + return testNameFormat(out) case "pkgname", "short": - return &formatAdapter{out, pkgNameFormat(formatOpts)} + return pkgNameFormat(out, formatOpts) case "pkgname-and-test-fails", "short-with-failures": - return &formatAdapter{out, pkgNameWithFailuresFormat(formatOpts)} + return pkgNameWithFailuresFormat(out, formatOpts) default: return nil } } - -type formatAdapter struct { - out io.Writer - format func(TestEvent, *Execution) string -} - -func (f *formatAdapter) Format(event TestEvent, exec *Execution) error { - o := f.format(event, exec) - _, err := f.out.Write([]byte(o)) - return err -} diff --git a/testjson/format_test.go b/testjson/format_test.go index e11cdd9a..10a755fa 100644 --- a/testjson/format_test.go +++ b/testjson/format_test.go @@ -27,7 +27,6 @@ import ( type fakeHandler struct { inputName string formatter EventFormatter - out *bytes.Buffer err *bytes.Buffer } @@ -39,19 +38,6 @@ func (s *fakeHandler) Config(t *testing.T) ScanConfig { } } -func newFakeHandlerWithAdapter( - format func(event TestEvent, output *Execution) string, - inputName string, -) *fakeHandler { - out := new(bytes.Buffer) - return &fakeHandler{ - inputName: inputName, - formatter: &formatAdapter{out: out, format: format}, - out: out, - err: new(bytes.Buffer), - } -} - func newFakeHandler(formatter EventFormatter, inputName string) *fakeHandler { return &fakeHandler{ inputName: inputName, @@ -77,12 +63,6 @@ func patchPkgPathPrefix(t *testing.T, val string) { }) } -func withAdapter(format func(TestEvent, *Execution) string) func(io.Writer) EventFormatter { - return func(out io.Writer) EventFormatter { - return &formatAdapter{out: out, format: format} - } -} - func TestFormats_DefaultGoTestJson(t *testing.T) { type testCase struct { name string @@ -108,37 +88,43 @@ func TestFormats_DefaultGoTestJson(t *testing.T) { testCases := []testCase{ { name: "testname", - format: withAdapter(testNameFormat), + format: testNameFormat, expectedOut: "format/testname.out", }, { name: "dots-v1", - format: withAdapter(dotsFormatV1), + format: dotsFormatV1, expectedOut: "format/dots-v1.out", }, { - name: "pkgname", - format: withAdapter(pkgNameFormat(FormatOptions{})), + name: "pkgname", + format: func(out io.Writer) EventFormatter { + return pkgNameFormat(out, FormatOptions{}) + }, expectedOut: "format/pkgname.out", }, { - name: "pkgname-hivis", - format: withAdapter(pkgNameFormat(FormatOptions{UseHiVisibilityIcons: true})), + name: "pkgname with hivis", + format: func(out io.Writer) EventFormatter { + return pkgNameFormat(out, FormatOptions{UseHiVisibilityIcons: true}) + }, expectedOut: "format/pkgname-hivis.out", }, { - name: "pkgname", - format: withAdapter(pkgNameFormat(FormatOptions{HideEmptyPackages: true})), + name: "pkgname with hide-empty", + format: func(out io.Writer) EventFormatter { + return pkgNameFormat(out, FormatOptions{HideEmptyPackages: true}) + }, expectedOut: "format/pkgname-hide-empty.out", }, { name: "standard-verbose", - format: withAdapter(standardVerboseFormat), + format: standardVerboseFormat, expectedOut: "format/standard-verbose.out", }, { name: "standard-quiet", - format: withAdapter(standardQuietFormat), + format: standardQuietFormat, expectedOut: "format/standard-quiet.out", }, { @@ -158,18 +144,20 @@ func TestFormats_DefaultGoTestJson(t *testing.T) { func TestFormats_Coverage(t *testing.T) { type testCase struct { name string - format func(event TestEvent, exec *Execution) string + format func(writer io.Writer) EventFormatter expectedOut string expected func(t *testing.T, exec *Execution) } run := func(t *testing.T, tc testCase) { patchPkgPathPrefix(t, "gotest.tools") - shim := newFakeHandlerWithAdapter(tc.format, "input/go-test-json-with-cover") + out := new(bytes.Buffer) + + shim := newFakeHandler(tc.format(out), "input/go-test-json-with-cover") exec, err := ScanTestOutput(shim.Config(t)) assert.NilError(t, err) - golden.Assert(t, shim.out.String(), tc.expectedOut) + golden.Assert(t, out.String(), tc.expectedOut) golden.Assert(t, shim.err.String(), "go-test.err") if tc.expected != nil { @@ -184,8 +172,10 @@ func TestFormats_Coverage(t *testing.T) { expectedOut: "format/testname-coverage.out", }, { - name: "pkgname", - format: pkgNameFormat(FormatOptions{}), + name: "pkgname", + format: func(out io.Writer) EventFormatter { + return pkgNameFormat(out, FormatOptions{}) + }, expectedOut: "format/pkgname-coverage.out", }, { @@ -210,17 +200,18 @@ func TestFormats_Coverage(t *testing.T) { func TestFormats_Shuffle(t *testing.T) { type testCase struct { name string - format func(event TestEvent, exec *Execution) string + format func(io.Writer) EventFormatter expectedOut string expected func(t *testing.T, exec *Execution) } run := func(t *testing.T, tc testCase) { - shim := newFakeHandlerWithAdapter(tc.format, "input/go-test-json-with-shuffle") + out := new(bytes.Buffer) + shim := newFakeHandler(tc.format(out), "input/go-test-json-with-shuffle") exec, err := ScanTestOutput(shim.Config(t)) assert.NilError(t, err) - golden.Assert(t, shim.out.String(), tc.expectedOut) + golden.Assert(t, out.String(), tc.expectedOut) golden.Assert(t, shim.err.String(), "go-test.err") if tc.expected != nil { @@ -235,8 +226,10 @@ func TestFormats_Shuffle(t *testing.T) { expectedOut: "format/testname-shuffle.out", }, { - name: "pkgname", - format: pkgNameFormat(FormatOptions{}), + name: "pkgname", + format: func(out io.Writer) EventFormatter { + return pkgNameFormat(out, FormatOptions{}) + }, expectedOut: "format/pkgname-shuffle.out", }, {