blob: b7728bd85139f335bd2e5fe02d188fec4fe1375c [file] [log] [blame]
package main
import (
"bufio"
"flag"
"fmt"
"io"
"log"
"os"
"regexp"
"sort"
"strings"
"text/template"
)
const (
version = "0.3.0"
// gotest regular expressions
// === RUN TestAdd
gt_startRE = "^=== RUN:? ([a-zA-Z_][^[:space:]]*)"
// --- PASS: TestSub (0.00 seconds)
// --- FAIL: TestSubFail (0.00 seconds)
// --- SKIP: TestSubSkip (0.00 seconds)
gt_endRE = "^--- (PASS|FAIL|SKIP): ([a-zA-Z_][^[:space:]]*) \\((\\d+(.\\d+)?)"
// FAIL _/home/miki/Projects/goroot/src/xunit 0.004s
// ok _/home/miki/Projects/goroot/src/anotherTest 0.000s
gt_suiteRE = "^(ok|FAIL)[ \t]+([^ \t]+)[ \t]+(\\d+.\\d+)"
// ? alipay [no test files]
gt_noFiles = "^\\?.*\\[no test files\\]$"
// FAIL node/config [build failed]
gt_buildFailed = `^FAIL.*\[(build|setup) failed\]$`
// gocheck regular expressions
// START: mmath_test.go:16: MySuite.TestAdd
gc_startRE = "START: [^:]+:[^:]+: ([A-Za-z_][[:word:]]*).([A-Za-z_][[:word:]]*)"
// PASS: mmath_test.go:16: MySuite.TestAdd 0.000s
// FAIL: mmath_test.go:35: MySuite.TestDiv
gc_endRE = "(PASS|FAIL): [^:]+:[^:]+: ([A-Za-z_][[:word:]]*).([A-Za-z_][[:word:]]*)([[:space:]]+([0-9]+.[0-9]+))?"
)
var (
failOnRace = false
)
type Test struct {
Name, Time, Message string
Failed bool
Skipped bool
}
type Suite struct {
Name string
Time string
Status string
Tests []*Test
}
type TestResults struct {
Suites []*Suite
Multi bool
}
func (suite *Suite) NumFailed() int {
count := 0
for _, test := range suite.Tests {
if test.Failed {
count++
}
}
return count
}
func (suite *Suite) NumSkipped() int {
count := 0
for _, test := range suite.Tests {
if test.Skipped {
count++
}
}
return count
}
func (suite *Suite) Count() int {
return len(suite.Tests)
}
func hasDatarace(lines []string) bool {
has_datarace := regexp.MustCompile("^WARNING: DATA RACE$").MatchString
for _, line := range lines {
if has_datarace(line) {
return true
}
}
return false
}
func gt_Parse(rd io.Reader) ([]*Suite, error) {
find_start := regexp.MustCompile(gt_startRE).FindStringSubmatch
find_end := regexp.MustCompile(gt_endRE).FindStringSubmatch
find_suite := regexp.MustCompile(gt_suiteRE).FindStringSubmatch
is_nofiles := regexp.MustCompile(gt_noFiles).MatchString
is_buildFailed := regexp.MustCompile(gt_buildFailed).MatchString
is_exit := regexp.MustCompile("^exit status -?\\d+").MatchString
suites := []*Suite{}
var curTest *Test
var curSuite *Suite
var out []string
// Handles a test that ended with a panic.
handlePanic := func() {
curTest.Failed = true
curTest.Skipped = false
curTest.Time = "N/A"
curSuite.Tests = append(curSuite.Tests, curTest)
curTest = nil
}
// Appends output to the last test.
appendError := func() error {
if len(out) > 0 && curSuite != nil && len(curSuite.Tests) > 0 {
message := strings.Join(out, "\n")
if curSuite.Tests[len(curSuite.Tests)-1].Message == "" {
curSuite.Tests[len(curSuite.Tests)-1].Message = message
} else {
curSuite.Tests[len(curSuite.Tests)-1].Message += "\n" + message
}
}
out = []string{}
return nil
}
scanner := bufio.NewScanner(rd)
for lnum := 1; scanner.Scan(); lnum++ {
line := scanner.Text()
// TODO: Only outside a suite/test, report as empty suite?
if is_nofiles(line) {
continue
}
if is_buildFailed(line) {
return nil, fmt.Errorf("%d: package build failed: %s", lnum, line)
}
if curSuite == nil {
curSuite = &Suite{}
}
tokens := find_start(line)
if tokens != nil {
if curTest != nil {
// This occurs when the last test ended with a panic.
handlePanic()
}
if e := appendError(); e != nil {
return nil, e
}
curTest = &Test{
Name: tokens[1],
}
continue
}
tokens = find_end(line)
if tokens != nil {
if curTest == nil {
return nil, fmt.Errorf("%d: orphan end test", lnum)
}
if tokens[2] != curTest.Name {
return nil, fmt.Errorf("%d: name mismatch", lnum)
}
curTest.Failed = (tokens[1] == "FAIL") || (failOnRace && hasDatarace(out))
curTest.Skipped = (tokens[1] == "SKIP")
curTest.Time = tokens[3]
curTest.Message = strings.Join(out, "\n")
curSuite.Tests = append(curSuite.Tests, curTest)
curTest = nil
out = []string{}
continue
}
tokens = find_suite(line)
if tokens != nil {
if curTest != nil {
// This occurs when the last test ended with a panic.
handlePanic()
}
if e := appendError(); e != nil {
return nil, e
}
curSuite.Name = tokens[2]
curSuite.Time = tokens[3]
suites = append(suites, curSuite)
curSuite = nil
continue
}
if is_exit(line) || (line == "FAIL") || (line == "PASS") {
continue
}
out = append(out, line)
}
if err := scanner.Err(); err != nil {
return nil, err
}
return suites, nil
}
func map2arr(m map[string]*Suite) []*Suite {
arr := []*Suite{}
keys := []string{}
for key, _ := range m {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
/* FIXME:
suite.Status =
suite.Time =
*/
arr = append(arr, m[key])
}
return arr
}
// gc_Parse parses output of "go test -gocheck.vv", returns a list of tests
// See data/gocheck.out for an example
func gc_Parse(rd io.Reader) ([]*Suite, error) {
find_start := regexp.MustCompile(gc_startRE).FindStringSubmatch
find_end := regexp.MustCompile(gc_endRE).FindStringSubmatch
scanner := bufio.NewScanner(rd)
var test *Test
var suites = make(map[string]*Suite)
var suiteName string
var out []string
for lnum := 1; scanner.Scan(); lnum++ {
line := scanner.Text()
tokens := find_start(line)
if len(tokens) > 0 {
if test != nil {
return nil, fmt.Errorf("%d: start in middle\n", lnum)
}
suiteName = tokens[1]
test = &Test{Name: tokens[2]}
out = []string{}
continue
}
tokens = find_end(line)
if len(tokens) > 0 {
if test == nil {
return nil, fmt.Errorf("%d: orphan end", lnum)
}
if (tokens[2] != suiteName) || (tokens[3] != test.Name) {
return nil, fmt.Errorf("%d: suite/name mismatch", lnum)
}
test.Message = strings.Join(out, "\n")
test.Time = tokens[4]
test.Failed = (tokens[1] == "FAIL")
suite, ok := suites[suiteName]
if !ok {
suite = &Suite{Name: suiteName}
}
suite.Tests = append(suite.Tests, test)
suites[suiteName] = suite
test = nil
suiteName = ""
out = []string{}
continue
}
if test != nil {
out = append(out, line)
}
}
if err := scanner.Err(); err != nil {
return nil, err
}
return map2arr(suites), nil
}
func hasFailures(suites []*Suite) bool {
for _, suite := range suites {
if suite.NumFailed() > 0 {
return true
}
}
return false
}
var xmlTemplate string = `<?xml version="1.0" encoding="utf-8"?>
{{if .Multi}}<testsuites>{{end}}
{{range $suite := .Suites}} <testsuite name="{{.Name}}" tests="{{.Count}}" errors="0" failures="{{.NumFailed}}" skip="{{.NumSkipped}}">
{{range $test := $suite.Tests}} <testcase classname="{{$suite.Name}}" name="{{$test.Name}}" time="{{$test.Time}}">
{{if $test.Skipped }} <skipped/> {{end}}
{{if $test.Failed }} <failure type="go.error" message="error">
<![CDATA[{{$test.Message}}]]>
</failure>{{end}} </testcase>
{{end}} </testsuite>
{{end}}{{if .Multi}}</testsuites>{{end}}
`
// writeXML exits xunit XML of tests to out
func writeXML(suites []*Suite, out io.Writer, bamboo bool) {
testsResult := TestResults{
Suites: suites,
Multi: bamboo || (len(suites) > 1),
}
t := template.New("test template")
t, err := t.Parse(xmlTemplate)
if err != nil {
fmt.Println("Error en parse %v", err)
return
}
err = t.Execute(out, testsResult)
if err != nil {
fmt.Println("Error en execute %v", err)
return
}
}
// getInput return input io.Reader from file name, if file name is - it will
// return os.Stdin
func getInput(filename string) (io.Reader, error) {
if filename == "-" || filename == "" {
return os.Stdin, nil
}
return os.Open(filename)
}
// getInput return output io.Writer from file name, if file name is - it will
// return os.Stdout
func getOutput(filename string) (io.Writer, error) {
if filename == "-" || filename == "" {
return os.Stdout, nil
}
return os.Create(filename)
}
// getIO returns input and output streams from file names
func getIO(inputFile, outputFile string) (io.Reader, io.Writer, error) {
input, err := getInput(inputFile)
if err != nil {
return nil, nil, fmt.Errorf("can't open %s for reading: %s", inputFile, err)
}
output, err := getOutput(outputFile)
if err != nil {
return nil, nil, fmt.Errorf("can't open %s for writing: %s", outputFile, err)
}
return input, output, nil
}
func main() {
inputFile := flag.String("input", "", "input file (default to stdin)")
outputFile := flag.String("output", "", "output file (default to stdout)")
fail := flag.Bool("fail", false, "fail (non zero exit) if any test failed")
showVersion := flag.Bool("version", false, "print version and exit")
bamboo := flag.Bool("bamboo", false, "xml compatible with Atlassian's Bamboo")
is_gocheck := flag.Bool("gocheck", false, "parse gocheck output")
flag.BoolVar(&failOnRace, "fail-on-race", false, "mark test as failing if it exposes a data race")
flag.Parse()
if *showVersion {
fmt.Println(version)
os.Exit(0)
}
// No time ... prefix for error messages
log.SetFlags(0)
if flag.NArg() > 0 {
log.Fatalf("error: %s does not take parameters (did you mean -input?)", os.Args[0])
}
input, output, err := getIO(*inputFile, *outputFile)
if err != nil {
log.Fatalf("error: %s", err)
}
var parse func(rd io.Reader) ([]*Suite, error)
if *is_gocheck {
parse = gc_Parse
} else {
parse = gt_Parse
}
suites, err := parse(input)
if err != nil {
log.Fatalf("error: %s", err)
}
if len(suites) == 0 {
log.Fatalf("error: no tests found")
os.Exit(1)
}
writeXML(suites, output, *bamboo)
if *fail && hasFailures(suites) {
os.Exit(1)
}
}