diff --git a/internal/utesting/utesting.go b/internal/utesting/utesting.go index 4de0ecf99aac..8260de1d7603 100644 --- a/internal/utesting/utesting.go +++ b/internal/utesting/utesting.go @@ -35,6 +35,7 @@ import ( type Test struct { Name string Fn func(*T) + Slow bool } // Result is the result of a test execution. @@ -63,32 +64,165 @@ func MatchTests(tests []Test, expr string) []Test { // RunTests executes all given tests in order and returns their results. // If the report writer is non-nil, a test report is written to it in real time. func RunTests(tests []Test, report io.Writer) []Result { - results := make([]Result, len(tests)) + if report == nil { + report = io.Discard + } + results := run(tests, newConsoleOutput(report)) + fails := CountFailures(results) + fmt.Fprintf(report, "%v/%v tests passed.\n", len(tests)-fails, len(tests)) + return results +} + +// RunTAP runs the given tests and writes Test Anything Protocol output +// to the report writer. +func RunTAP(tests []Test, report io.Writer) []Result { + return run(tests, newTAP(report, len(tests))) +} + +func run(tests []Test, output testOutput) []Result { + var results = make([]Result, len(tests)) for i, test := range tests { - var output io.Writer buffer := new(bytes.Buffer) - output = buffer - if report != nil { - output = io.MultiWriter(buffer, report) - } + logOutput := io.MultiWriter(buffer, output) + + output.testStart(test.Name) start := time.Now() results[i].Name = test.Name - results[i].Failed = run(test, output) + results[i].Failed = runTest(test, logOutput) results[i].Duration = time.Since(start) results[i].Output = buffer.String() - if report != nil { - printResult(results[i], report) - } + output.testResult(results[i]) } return results } -func printResult(r Result, w io.Writer) { +// testOutput is implemented by output formats. +type testOutput interface { + testStart(name string) + Write([]byte) (int, error) + testResult(Result) +} + +// consoleOutput prints test results similarly to go test. +type consoleOutput struct { + out io.Writer + indented *indentWriter + curTest string + wroteHeader bool +} + +func newConsoleOutput(w io.Writer) *consoleOutput { + return &consoleOutput{ + out: w, + indented: newIndentWriter(" ", w), + } +} + +// testStart signals the start of a new test. +func (c *consoleOutput) testStart(name string) { + c.curTest = name + c.wroteHeader = false +} + +// Write handles test log output. +func (c *consoleOutput) Write(b []byte) (int, error) { + if !c.wroteHeader { + // This is the first output line from the test. Print a "-- RUN" header. + fmt.Fprintln(c.out, "-- RUN", c.curTest) + c.wroteHeader = true + } + return c.indented.Write(b) +} + +// testResult prints the final test result line. +func (c *consoleOutput) testResult(r Result) { + c.indented.flush() pd := r.Duration.Truncate(100 * time.Microsecond) if r.Failed { - fmt.Fprintf(w, "-- FAIL %s (%v)\n", r.Name, pd) + fmt.Fprintf(c.out, "-- FAIL %s (%v)\n", r.Name, pd) } else { - fmt.Fprintf(w, "-- OK %s (%v)\n", r.Name, pd) + fmt.Fprintf(c.out, "-- OK %s (%v)\n", r.Name, pd) + } +} + +// tapOutput produces Test Anything Protocol v13 output. +type tapOutput struct { + out io.Writer + indented *indentWriter + counter int +} + +func newTAP(out io.Writer, numTests int) *tapOutput { + fmt.Fprintf(out, "1..%d\n", numTests) + return &tapOutput{ + out: out, + indented: newIndentWriter("# ", out), + } +} + +func (t *tapOutput) testStart(name string) { + t.counter++ +} + +// Write does nothing for TAP because there is no real-time output of test logs. +func (t *tapOutput) Write(b []byte) (int, error) { + return len(b), nil +} + +func (t *tapOutput) testResult(r Result) { + status := "ok" + if r.Failed { + status = "not ok" + } + fmt.Fprintln(t.out, status, t.counter, r.Name) + t.indented.Write([]byte(r.Output)) + t.indented.flush() +} + +// indentWriter indents all written text. +type indentWriter struct { + out io.Writer + indent string + inLine bool +} + +func newIndentWriter(indent string, out io.Writer) *indentWriter { + return &indentWriter{out: out, indent: indent} +} + +func (w *indentWriter) Write(b []byte) (n int, err error) { + for len(b) > 0 { + if !w.inLine { + if _, err = io.WriteString(w.out, w.indent); err != nil { + return n, err + } + w.inLine = true + } + + end := bytes.IndexByte(b, '\n') + if end == -1 { + nn, err := w.out.Write(b) + n += nn + return n, err + } + + line := b[:end+1] + nn, err := w.out.Write(line) + n += nn + if err != nil { + return n, err + } + b = b[end+1:] + w.inLine = false + } + return n, err +} + +// flush ensures the current line is terminated. +func (w *indentWriter) flush() { + if w.inLine { + fmt.Println(w.out) + w.inLine = false } } @@ -106,11 +240,11 @@ func CountFailures(rr []Result) int { // Run executes a single test. func Run(test Test) (bool, string) { output := new(bytes.Buffer) - failed := run(test, output) + failed := runTest(test, output) return failed, output.String() } -func run(test Test, output io.Writer) bool { +func runTest(test Test, output io.Writer) bool { t := &T{output: output} done := make(chan struct{}) go func() { @@ -137,6 +271,9 @@ type T struct { output io.Writer } +// Helper exists for compatibility with testing.T. +func (t *T) Helper() {} + // FailNow marks the test as having failed and stops its execution by calling // runtime.Goexit (which then runs all deferred calls in the current goroutine). func (t *T) FailNow() { diff --git a/internal/utesting/utesting_test.go b/internal/utesting/utesting_test.go new file mode 100644 index 000000000000..526d36bab1e3 --- /dev/null +++ b/internal/utesting/utesting_test.go @@ -0,0 +1,145 @@ +// Copyright 2020 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package utesting + +import ( + "bytes" + "regexp" + "strings" + "testing" +) + +func TestTest(t *testing.T) { + t.Parallel() + + tests := []Test{ + { + Name: "successful test", + Fn: func(t *T) {}, + }, + { + Name: "failing test", + Fn: func(t *T) { + t.Log("output") + t.Error("failed") + }, + }, + { + Name: "panicking test", + Fn: func(t *T) { + panic("oh no") + }, + }, + } + results := RunTests(tests, nil) + + if results[0].Failed || results[0].Output != "" { + t.Fatalf("wrong result for successful test: %#v", results[0]) + } + if !results[1].Failed || results[1].Output != "output\nfailed\n" { + t.Fatalf("wrong result for failing test: %#v", results[1]) + } + if !results[2].Failed || !strings.HasPrefix(results[2].Output, "panic: oh no\n") { + t.Fatalf("wrong result for panicking test: %#v", results[2]) + } +} + +var outputTests = []Test{ + { + Name: "TestWithLogs", + Fn: func(t *T) { + t.Log("output line 1") + t.Log("output line 2\noutput line 3") + }, + }, + { + Name: "TestNoLogs", + Fn: func(t *T) {}, + }, + { + Name: "FailWithLogs", + Fn: func(t *T) { + t.Log("output line 1") + t.Error("failed 1") + }, + }, + { + Name: "FailMessage", + Fn: func(t *T) { + t.Error("failed 2") + }, + }, + { + Name: "FailNoOutput", + Fn: func(t *T) { + t.Fail() + }, + }, +} + +func TestOutput(t *testing.T) { + t.Parallel() + + var buf bytes.Buffer + RunTests(outputTests, &buf) + + want := regexp.MustCompile(` +^-- RUN TestWithLogs + output line 1 + output line 2 + output line 3 +-- OK TestWithLogs \([^)]+\) +-- OK TestNoLogs \([^)]+\) +-- RUN FailWithLogs + output line 1 + failed 1 +-- FAIL FailWithLogs \([^)]+\) +-- RUN FailMessage + failed 2 +-- FAIL FailMessage \([^)]+\) +-- FAIL FailNoOutput \([^)]+\) +2/5 tests passed. +$`[1:]) + if !want.MatchString(buf.String()) { + t.Fatalf("output does not match: %q", buf.String()) + } +} + +func TestOutputTAP(t *testing.T) { + t.Parallel() + + var buf bytes.Buffer + RunTAP(outputTests, &buf) + + want := ` +1..5 +ok 1 TestWithLogs +# output line 1 +# output line 2 +# output line 3 +ok 2 TestNoLogs +not ok 3 FailWithLogs +# output line 1 +# failed 1 +not ok 4 FailMessage +# failed 2 +not ok 5 FailNoOutput +` + if buf.String() != want[1:] { + t.Fatalf("output does not match: %q", buf.String()) + } +}