blob: b8bf851b36246fbbb78c0575aa1adc4d4cd2b669 [file] [log] [blame]
/*Package testsum provides functions for parsing `go test -v` output and returning
a summary of the test run.
Build the executable:
go build -o gotestsum ./testsum/cmd
Usage:
go test -v ./... | gotestsum
Example output:
=== RUN TestPass
--- PASS: TestPass (0.00s)
=== RUN TestSkip
--- SKIP: TestSkip (0.00s)
example_test.go:11:
=== RUN TestFail
Some test output
--- FAIL: TestFail (0.00s)
example_test.go:22: some log output
FAIL
exit status 1
FAIL example.com/gotestyourself/testpkg 0.002s
======== 3 tests, 1 skipped, 1 failed in 2.28 seconds ========
--- FAIL: TestFail
Some test output
example_test.go:22: some log output
*/
package testsum
import (
"bufio"
"bytes"
"fmt"
"io"
"strings"
"time"
"github.com/pkg/errors"
)
// Failure test including output
type Failure struct {
name string
output string
logs string
}
func (f Failure) String() string {
buf := bytes.NewBufferString("--- FAIL: " + f.name)
if f.output != "" {
buf.WriteString("\n" + f.output)
}
buf.WriteString("\n" + f.logs + "\n")
return buf.String()
}
// Summary information from a `go test -v` run. Includes counts of tests and
// a list of failed tests with the test output
type Summary struct {
Total int
Skipped int
Elapsed time.Duration
Failures []Failure
}
// FormatLine returns a line with counts of tests, skipped, and failed
func (s *Summary) FormatLine() string {
bar := "========"
buf := bytes.NewBufferString(fmt.Sprintf(bar+" %d tests", s.Total))
if s.Skipped != 0 {
buf.WriteString(fmt.Sprintf(", %d skipped", s.Skipped))
}
if len(s.Failures) > 0 {
buf.WriteString(fmt.Sprintf(", %d failed", len(s.Failures)))
}
buf.WriteString(fmt.Sprintf(" in %0.2f seconds", s.Elapsed.Seconds()))
buf.WriteString(" " + bar)
return buf.String()
}
// FormatFailures returns a string with all the test failure and the test output.
// Returns a empty string if there are no failures.
func (s *Summary) FormatFailures() string {
formatted := []string{}
for _, failure := range s.Failures {
formatted = append(formatted, failure.String())
}
return strings.Join(formatted, "\n")
}
func (s *Summary) addFailure(failure *Failure) {
if failure != nil {
s.Failures = append(s.Failures, *failure)
}
}
// Scan reads lines from the reader, echos them to the writer, and parses the
// lines for `go test -v` output. It returns a summary of the test run.
func Scan(in io.Reader, out io.Writer) (*Summary, error) {
summary := &Summary{}
state := newScanState()
start := time.Now()
scanner := bufio.NewScanner(in)
for scanner.Scan() {
line := scanner.Text()
if _, err := out.Write([]byte(line + "\n")); err != nil {
return summary, errors.Wrapf(err, "failed to echo output")
}
parseLine(summary, state, line)
}
if err := scanner.Err(); err != nil {
return summary, errors.Wrapf(err, "failed to scan input")
}
summary.Elapsed = time.Since(start)
return summary, nil
}
type state int
const (
stateNone = iota
stateRun
stateFail
)
type scanState struct {
buffer *bytes.Buffer
currentFail *Failure
state state
}
func newScanState() *scanState {
return &scanState{buffer: new(bytes.Buffer)}
}
func (s *scanState) end() {
s.state = stateNone
s.currentFail = nil
s.buffer.Reset()
}
func (s *scanState) addLine(line string) {
s.buffer.WriteString(line + "\n")
}
func (s *scanState) start(name string) {
s.state = stateRun
s.currentFail = &Failure{name: name}
}
func (s *scanState) getFailure() *Failure {
defer s.end()
if s.state != stateFail {
return nil
}
failure := s.currentFail
failure.logs = s.buffer.String()
return failure
}
func (s *scanState) setFailed() {
s.state = stateFail
s.currentFail.output = s.buffer.String()
s.buffer.Reset()
}
var runPrefixLength = len("=== RUN ")
func parseLine(summary *Summary, state *scanState, line string) {
switch {
// Nested tests start with the same line prefix so only start a new test
// if not already in a run state
case state.state != stateRun && strings.HasPrefix(line, "=== RUN "):
summary.addFailure(state.getFailure())
state.start(strings.TrimSpace(line[runPrefixLength:]))
summary.Total++
case strings.HasPrefix(line, "--- PASS: "):
state.end()
case strings.HasPrefix(line, "--- SKIP: "):
summary.Skipped++
state.end()
case strings.HasPrefix(line, "--- FAIL: "):
state.setFailed()
case isEndOfTestRun(line):
summary.addFailure(state.getFailure())
default:
state.addLine(line)
}
}
func isEndOfTestRun(line string) bool {
return line == "FAIL" || line == "PASS"
}