Skip to content
This repository has been archived by the owner on Dec 11, 2019. It is now read-only.

Commit

Permalink
Rewrite to take test2json input
Browse files Browse the repository at this point in the history
This executes on the plan outlined in:

https://github.com/cockroachdb/go-test-teamcity/issues/5#issuecomment-422494665

by making go-test-teamcity take test2json input. More precisely, and
unfortunately, it realistically has to be taking the input of

    make test TESTFLAGS='-v -json'

which isn't pure JSON but contains a bunch of `make` and `go build`
output (requiring a bit of massaging to pluck the relevant JSON from
the input stream).

I retained the old functionality and introduced a new `-json` flag
that triggers the new code. This important because our CI scripts
are versioned, and so we need the updated code here to work with
the non-updated CI pipeline.

I initially tried to jsonify the old test cases, but they're basically
garbage and don't correspond to real test output in some cases. Instead,
I generated a new corpus from actual CI runs in the main repo from which
I copied the artifacts file:

cockroachdb/cockroach#30406

I think it's worth checking in that PR in suitable form because it'll
allow us to iterate on this parser and extend the corpus much more
easily in the future.

Two caveats:

1. the code isn't creating TeamCity Test Suite tags. I was never married
to those and have found it unfortunate that they mess up TeamCity's test
duration counting (as far as I can remember). Adding them is probably
possible, but with parallel tests (which we don't use heavily) and such
I'm pretty sure that any naive implementation would be incorrect.
2. Tests which aren't explicitly ended (due to premature exit/crash of
the package test binary) are now output at the very end of the log.
Since the input contains *all tests*, this can be a little unintuitive.
However, these final tests contain basically no information anyway, so
that seems fine. This could be improved by flushing out the tests
whenever a package-level PASS/FAIL event is encountered, but I'm not
sure it's worth doing so.

The next step, assuming the code is in good order, is to merge it and
update the CI pipeline in the main repo to pass the `-json` flags both
to `go test` and `go-test-teamcity`.
  • Loading branch information
tbg committed Sep 19, 2018
1 parent 9a36ab1 commit 4e48730
Show file tree
Hide file tree
Showing 6 changed files with 2,331 additions and 27 deletions.
119 changes: 118 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@ package main

import (
"bufio"
"encoding/json"
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"regexp"
"sort"
"strings"
"time"
)
Expand All @@ -24,13 +28,15 @@ type Test struct {
Status string
Race bool
Suite bool
Package string
}

var (
input = os.Stdin
output = os.Stdout

additionalTestName = ""
useJSON = false

run = regexp.MustCompile("^=== RUN\\s+([a-zA-Z_]\\S*)")
end = regexp.MustCompile("^(\\s*)--- (PASS|SKIP|FAIL):\\s+([a-zA-Z_]\\S*) \\((-?[\\.\\ds]+)\\)")
Expand All @@ -39,6 +45,7 @@ var (
)

func init() {
flag.BoolVar(&useJSON, "json", false, "Parse input from JSON (as emitted from go tool test2json)")
flag.StringVar(&additionalTestName, "name", "", "Add prefix to test name")
}

Expand Down Expand Up @@ -78,6 +85,14 @@ func outputTest(w io.Writer, test *Test) {
now, testName, escapeLines(test.Details))
case "PASS":
// ignore
case "UNKNOWN":
// This can happen when a data race is detected, in which case the test binary
// exits apruptly assuming GORACE="halt_on_error=1" is specified.
// CockroachDB CI does this at the time of writing:
// https://github.com/cockroachdb/cockroach/pull/14590
fmt.Fprintf(w, "##teamcity[testIgnored timestamp='%s' name='%s' message='"+
"Test framework exited prematurely. Likely another test panicked or encountered a data race']\n",
now, testName)
default:
fmt.Fprintf(w, "##teamcity[testFailed timestamp='%s' name='%s' message='Test ended in panic.' details='%s']\n",
now, testName, escapeLines(test.Details))
Expand Down Expand Up @@ -195,5 +210,107 @@ func main() {

reader := bufio.NewReader(input)

processReader(reader, output)
if useJSON {
processJSON(reader, output)
} else {
processReader(reader, output)
}
}

// TestEvent is a message as emitted by `go tool test2json`.
type TestEvent struct {
Time time.Time // encodes as an RFC3339-format string
Action string
Package string
Test string
Elapsed float64 // seconds
Output string
}

func processJSON(r *bufio.Reader, w io.Writer) {
openTests := map[string]*Test{}
output := func(name string) {
test := openTests[name]
delete(openTests, name)
outputTest(w, test)
}

defer func() {
sorted := make([]*Test, 0, len(openTests))
for _, test := range openTests {
sorted = append(sorted, test)
}
sort.Slice(sorted, func(i, j int) bool {
return sorted[i].Name < sorted[j].Name
})
for _, test := range sorted {
test.Output += "(test not terminated explicitly)\n"
test.Status = "UNKNOWN"
outputTest(w, test)
}
}()

dec := json.NewDecoder(r)
dec.DisallowUnknownFields()

for dec.More() {
var event TestEvent
if err := dec.Decode(&event); err != nil {
buffered, err := ioutil.ReadAll(dec.Buffered())
if err != nil {
log.Fatal(err)
}
fmt.Fprint(w, string(buffered))
line, err := r.ReadString('\n')
dec = json.NewDecoder(r)
if err != nil {
if err == io.EOF {
continue
}
log.Fatal(err)
}
fmt.Fprint(w, string(line))
continue
}

if openTests[event.Test] == nil {
if event.Test == "" {
// We're about to start a new test, but this line doesn't correspond to one.
// It's probably a package-level info (coverage etc).
fmt.Fprint(w, event.Output)
continue
}
openTests[event.Test] = &Test{}
}

test := openTests[event.Test]
if test.Name == "" {
test.Name = event.Test
}
test.Output += event.Output
test.Race = test.Race || race.MatchString(event.Output)
test.Duration += time.Duration(event.Elapsed * 1E9)
if test.Package == "" {
test.Package = event.Package
}

switch event.Action {
case "run":
case "pause":
case "cont":
case "bench":
case "output":
case "skip":
test.Status = "SKIP"
output(event.Test)
case "pass":
test.Status = "PASS"
output(event.Test)
case "fail":
test.Status = "FAIL"
output(event.Test)
default:
log.Fatalf("unknown event type: %+v", event)
}
}
}
72 changes: 46 additions & 26 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,52 +3,72 @@ package main
import (
"bufio"
"bytes"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strings"
"testing"
)

var (
inDir = "testdata/input/"
outDir = "testdata/output/"
inDir = "testdata/input"
outDir = "testdata/output"

timestampRegexp = regexp.MustCompile(`timestamp='.*?'`)
timestampReplacement = `timestamp='2017-01-02T04:05:06.789'`
)

func TestProcessReader(t *testing.T) {
files, err := ioutil.ReadDir(inDir)
if err != nil {
t.Error(err)
}
for _, file := range files {
t.Run(file.Name(), func(t *testing.T) {
inpath := inDir + file.Name()
f, err := os.Open(inpath)
if err != nil {
t.Error(err)
for _, json := range []bool{false, true} {
t.Run(fmt.Sprintf("json=%t", json), func(t *testing.T) {
sep := ""
if json {
sep = ".json"
}
in := bufio.NewReader(f)

out := &bytes.Buffer{}
processReader(in, out)
actual := out.String()
actual = timestampRegexp.ReplaceAllString(actual, timestampReplacement)

outpath := outDir + file.Name()
t.Logf("input: %s", inpath)
t.Logf("output: %s", outpath)
expectedBytes, err := ioutil.ReadFile(outpath)
files, err := ioutil.ReadDir(inDir + sep)
if err != nil {
t.Error(err)
}
expected := string(expectedBytes)
expected = timestampRegexp.ReplaceAllString(expected, timestampReplacement)
for _, file := range files {
t.Run(file.Name(), func(t *testing.T) {
inpath := filepath.Join(inDir+sep, file.Name())
f, err := os.Open(inpath)
if err != nil {
t.Error(err)
}
in := bufio.NewReader(f)

out := &bytes.Buffer{}
if json {
processJSON(in, out)
} else {
processReader(in, out)
}
actual := out.String()
actual = timestampRegexp.ReplaceAllString(actual, timestampReplacement)

outpath := filepath.Join(outDir+sep, file.Name())
t.Logf("input: %s", inpath)
t.Logf("output: %s", outpath)
expectedBytes, err := ioutil.ReadFile(outpath)
if err != nil {
t.Error(err)
}
expected := string(expectedBytes)
expected = timestampRegexp.ReplaceAllString(expected, timestampReplacement)

const rewriteOutput = true
if rewriteOutput {
_ = ioutil.WriteFile(outpath, []byte(actual), 0644)
}

if strings.Compare(expected, actual) != 0 {
t.Errorf("expected:\n\n%s\nbut got:\n\n%s\n", expected, actual)
if strings.Compare(expected, actual) != 0 {
t.Errorf("expected:\n\n%s\nbut got:\n\n%s\n", expected, actual)
}
})
}
})
}
Expand Down
Loading

0 comments on commit 4e48730

Please sign in to comment.