cmd/testwrapper: stream output results

Previously it would wait for all tests to run before printing anything,
instead stream the results over a channel so that they can be emitted
immediately.

Updates #8493

Signed-off-by: Maisem Ali <maisem@tailscale.com>
pull/8492/head^2
Maisem Ali 1 year ago committed by Maisem Ali
parent 2e4e7d6b9d
commit 1ca5dcce15

@ -23,7 +23,6 @@ import (
"time" "time"
"golang.org/x/exp/maps" "golang.org/x/exp/maps"
"golang.org/x/exp/slices"
"tailscale.com/cmd/testwrapper/flakytest" "tailscale.com/cmd/testwrapper/flakytest"
) )
@ -34,6 +33,8 @@ type testAttempt struct {
outcome string // "pass", "fail", "skip" outcome string // "pass", "fail", "skip"
logs bytes.Buffer logs bytes.Buffer
isMarkedFlaky bool // set if the test is marked as flaky isMarkedFlaky bool // set if the test is marked as flaky
pkgFinished bool
} }
type testName struct { type testName struct {
@ -60,7 +61,12 @@ type goTestOutput struct {
var debug = os.Getenv("TS_TESTWRAPPER_DEBUG") != "" var debug = os.Getenv("TS_TESTWRAPPER_DEBUG") != ""
func runTests(ctx context.Context, attempt int, pt *packageTests, otherArgs []string) []*testAttempt { // runTests runs the tests in pt and sends the results on ch. It sends a
// testAttempt for each test and a final testAttempt per pkg with pkgFinished
// set to true.
// It calls close(ch) when it's done.
func runTests(ctx context.Context, attempt int, pt *packageTests, otherArgs []string, ch chan<- *testAttempt) {
defer close(ch)
args := []string{"test", "-json", pt.pattern} args := []string{"test", "-json", pt.pattern}
args = append(args, otherArgs...) args = append(args, otherArgs...)
if len(pt.tests) > 0 { if len(pt.tests) > 0 {
@ -92,7 +98,6 @@ func runTests(ctx context.Context, attempt int, pt *packageTests, otherArgs []st
jd := json.NewDecoder(r) jd := json.NewDecoder(r)
resultMap := make(map[testName]*testAttempt) resultMap := make(map[testName]*testAttempt)
var out []*testAttempt
for { for {
var goOutput goTestOutput var goOutput goTestOutput
if err := jd.Decode(&goOutput); err != nil { if err := jd.Decode(&goOutput); err != nil {
@ -102,6 +107,16 @@ func runTests(ctx context.Context, attempt int, pt *packageTests, otherArgs []st
panic(err) panic(err)
} }
if goOutput.Test == "" { if goOutput.Test == "" {
switch goOutput.Action {
case "fail", "pass", "skip":
ch <- &testAttempt{
name: testName{
pkg: goOutput.Package,
},
outcome: goOutput.Action,
pkgFinished: true,
}
}
continue continue
} }
name := testName{ name := testName{
@ -124,7 +139,7 @@ func runTests(ctx context.Context, attempt int, pt *packageTests, otherArgs []st
} }
case "skip", "pass", "fail": case "skip", "pass", "fail":
resultMap[name].outcome = goOutput.Action resultMap[name].outcome = goOutput.Action
out = append(out, resultMap[name]) ch <- resultMap[name]
case "output": case "output":
if strings.TrimSpace(goOutput.Output) == flakytest.FlakyTestLogMessage { if strings.TrimSpace(goOutput.Output) == flakytest.FlakyTestLogMessage {
resultMap[name].isMarkedFlaky = true resultMap[name].isMarkedFlaky = true
@ -134,7 +149,6 @@ func runTests(ctx context.Context, attempt int, pt *packageTests, otherArgs []st
} }
} }
<-done <-done
return out
} }
func main() { func main() {
@ -186,13 +200,22 @@ func main() {
attempt: 1, attempt: 1,
}, },
} }
printPkgOutcome := func(pkg, outcome string, attempt int) {
printPkgStatus := func(pkgName string, failed bool) { if outcome == "skip" {
if failed { fmt.Printf("?\t%s [skipped/no tests] \n", pkg)
fmt.Println("FAIL\t", pkgName) return
} else { }
fmt.Println("ok\t", pkgName) if outcome == "pass" {
outcome = "ok"
}
if outcome == "fail" {
outcome = "FAIL"
}
if attempt > 1 {
fmt.Printf("%s\t%s [attempt=%d]\n", outcome, pkg, attempt)
return
} }
fmt.Printf("%s\t%s\n", outcome, pkg)
} }
for len(toRun) > 0 { for len(toRun) > 0 {
@ -210,25 +233,12 @@ func main() {
failed := false failed := false
toRetry := make(map[string][]string) // pkg -> tests to retry toRetry := make(map[string][]string) // pkg -> tests to retry
for _, pt := range thisRun.tests { for _, pt := range thisRun.tests {
output := runTests(ctx, thisRun.attempt, pt, otherArgs) ch := make(chan *testAttempt)
slices.SortFunc(output, func(i, j *testAttempt) bool { go runTests(ctx, thisRun.attempt, pt, otherArgs, ch)
if c := strings.Compare(i.name.pkg, j.name.pkg); c < 0 { for tr := range ch {
return true if tr.pkgFinished {
} else if c > 0 { printPkgOutcome(tr.name.pkg, tr.outcome, thisRun.attempt)
return false continue
}
return strings.Compare(i.name.name, j.name.name) <= 0
})
lastPkg := ""
lastPkgFailed := false
for _, tr := range output {
if lastPkg == "" {
lastPkg = tr.name.pkg
} else if lastPkg != tr.name.pkg {
printPkgStatus(lastPkg, lastPkgFailed)
lastPkg = tr.name.pkg
lastPkgFailed = false
} }
if *v || tr.outcome == "fail" { if *v || tr.outcome == "fail" {
io.Copy(os.Stdout, &tr.logs) io.Copy(os.Stdout, &tr.logs)
@ -236,14 +246,12 @@ func main() {
if tr.outcome != "fail" { if tr.outcome != "fail" {
continue continue
} }
lastPkgFailed = true
if tr.isMarkedFlaky { if tr.isMarkedFlaky {
toRetry[tr.name.pkg] = append(toRetry[tr.name.pkg], tr.name.name) toRetry[tr.name.pkg] = append(toRetry[tr.name.pkg], tr.name.name)
} else { } else {
failed = true failed = true
} }
} }
printPkgStatus(lastPkg, lastPkgFailed)
} }
if failed { if failed {
fmt.Println("\n\nNot retrying flaky tests because non-flaky tests failed.") fmt.Println("\n\nNot retrying flaky tests because non-flaky tests failed.")

Loading…
Cancel
Save