Compare commits

...

6 Commits

Author SHA1 Message Date
Claire Wang 67fd260317 wip
Signed-off-by: Claire Wang <claire@tailscale.com>
2023-07-05 14:27:50 -04:00
Claire Wang e2dca1c8b4 wip
Signed-off-by: Claire Wang <claire@tailscale.com>
2023-07-05 13:20:23 -04:00
Claire Wang 0b71d2c11a wip
Signed-off-by: Claire Wang <claire@tailscale.com>
2023-07-03 18:24:17 -04:00
Claire Wang fb2db85b80 wip 2023-06-30 16:48:35 -04:00
Claire Wang eecdf161b8 wip
Signed-off-by: Claire Wang <claire@tailscale.com>
2023-06-30 16:48:28 -04:00
Claire Wang 0bde93060c cmd/testwrapper: Use testwrapper output in github workflows
Fixes #8493

Signed-off-by: Claire Wang <claire@tailscale.com>
2023-06-30 16:48:12 -04:00
2 changed files with 65 additions and 32 deletions

View File

@ -90,9 +90,16 @@ jobs:
- name: build test wrapper - name: build test wrapper
run: ./tool/go build -o /tmp/testwrapper ./cmd/testwrapper run: ./tool/go build -o /tmp/testwrapper ./cmd/testwrapper
- name: test all - name: test all
run: PATH=$PWD/tool:$PATH /tmp/testwrapper ./... ${{matrix.buildflags}} run: PATH=$PWD/tool:$PATH /tmp/testwrapper -json test_attempts.json ./... ${{matrix.buildflags}}
env: env:
GOARCH: ${{ matrix.goarch }} GOARCH: ${{ matrix.goarch }}
- name: upload test output
uses: actions/upload-artifact@v3
with:
name: ${{ github.job }}-${{ runner.os }}-${{ matrix.goarch }}-test-attempts.json
path: test_attempts.json
- name: remove upload test output
run: rm test_attempts.json
- name: bench all - name: bench all
run: PATH=$PWD/tool:$PATH /tmp/testwrapper ./... ${{matrix.buildflags}} -bench=. -benchtime=1x -run=^$ run: PATH=$PWD/tool:$PATH /tmp/testwrapper ./... ${{matrix.buildflags}} -bench=. -benchtime=1x -run=^$
env: env:

View File

@ -28,18 +28,21 @@ import (
const maxAttempts = 3 const maxAttempts = 3
// testAttempt keeps track of the test name, outcome, logs, and if the test is flakey.
// After running the tests, each testAttempt is written to a json file.
type testAttempt struct { type testAttempt struct {
name testName Name testName `json:",inline"`
outcome string // "pass", "fail", "skip" Outcome string `json:",omitempty"` // "pass", "fail", "skip"
logs bytes.Buffer logs bytes.Buffer
isMarkedFlaky bool // set if the test is marked as flaky IsMarkedFlaky bool `json:",omitempty"` // set if the test is marked as flaky
AttemptCount int `json:",omitempty"`
pkgFinished bool PkgFinished bool `json:",omitempty"`
} }
// testName keeps track of the test name and its package.
type testName struct { type testName struct {
pkg string // "tailscale.com/types/key" Pkg string `json:",omitempty"` // "tailscale.com/types/key"
name string // "TestFoo" Name string `json:",omitempty"` // "TestFoo"
} }
type packageTests struct { type packageTests struct {
@ -59,13 +62,17 @@ type goTestOutput struct {
Output string Output string
} }
type options struct {
w io.Writer // optional writer if the -w flag is set.
}
var debug = os.Getenv("TS_TESTWRAPPER_DEBUG") != "" var debug = os.Getenv("TS_TESTWRAPPER_DEBUG") != ""
// runTests runs the tests in pt and sends the results on ch. It sends a // 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 // testAttempt for each test and a final testAttempt per pkg with PkgFinished
// set to true. // set to true.
// It calls close(ch) when it's done. // It calls close(ch) when it's done.
func runTests(ctx context.Context, attempt int, pt *packageTests, otherArgs []string, ch chan<- *testAttempt) { func runTests(ctx context.Context, attempt int, pt *packageTests, otherArgs []string, ch chan<- *testAttempt, opt options) {
defer close(ch) defer close(ch)
args := []string{"test", "-json", pt.pattern} args := []string{"test", "-json", pt.pattern}
args = append(args, otherArgs...) args = append(args, otherArgs...)
@ -110,21 +117,21 @@ func runTests(ctx context.Context, attempt int, pt *packageTests, otherArgs []st
switch goOutput.Action { switch goOutput.Action {
case "fail", "pass", "skip": case "fail", "pass", "skip":
ch <- &testAttempt{ ch <- &testAttempt{
name: testName{ Name: testName{
pkg: goOutput.Package, Pkg: goOutput.Package,
}, },
outcome: goOutput.Action, Outcome: goOutput.Action,
pkgFinished: true, PkgFinished: true,
} }
} }
continue continue
} }
name := testName{ name := testName{
pkg: goOutput.Package, Pkg: goOutput.Package,
name: goOutput.Test, Name: goOutput.Test,
} }
if test, _, isSubtest := strings.Cut(goOutput.Test, "/"); isSubtest { if test, _, isSubtest := strings.Cut(goOutput.Test, "/"); isSubtest {
name.name = test name.Name = test
if goOutput.Action == "output" { if goOutput.Action == "output" {
resultMap[name].logs.WriteString(goOutput.Output) resultMap[name].logs.WriteString(goOutput.Output)
} }
@ -135,33 +142,43 @@ func runTests(ctx context.Context, attempt int, pt *packageTests, otherArgs []st
// ignore // ignore
case "run": case "run":
resultMap[name] = &testAttempt{ resultMap[name] = &testAttempt{
name: name, Name: name,
AttemptCount: attempt,
} }
case "skip", "pass", "fail": case "skip", "pass", "fail":
resultMap[name].outcome = goOutput.Action resultMap[name].Outcome = goOutput.Action
ch <- 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
} else { } else {
resultMap[name].logs.WriteString(goOutput.Output) resultMap[name].logs.WriteString(goOutput.Output)
} }
} }
} }
if opt.w != nil {
for _, result := range resultMap {
testAttemptJson, _ := json.Marshal(result)
_, err = opt.w.Write(testAttemptJson)
if err != nil {
log.Printf("error appending to test attempt json file: %v", err)
}
}
}
<-done <-done
} }
func main() { func main() {
ctx := context.Background() ctx := context.Background()
// We only need to parse the -v flag to figure out whether to print the logs // We need to parse the -v flag to figure out whether to print the logs
// for a test. We don't need to parse any other flags, so we just use the // for a test.
// flag package to parse the -v flag and then pass the rest of the args // The -json flag is to indicate whether we want to write the json test results to a provided file.
// through to 'go test'.
// We run `go test -json` which returns the same information as `go test -v`, // We run `go test -json` which returns the same information as `go test -v`,
// but in a machine-readable format. So this flag is only for testwrapper's // but in a machine-readable format. So this flag is only for testwrapper's
// output. // output.
v := flag.Bool("v", false, "verbose") v := flag.Bool("v", false, "verbose")
jsonFile := flag.String("json", "", "if set, the file to store all test results in")
flag.Usage = func() { flag.Usage = func() {
fmt.Println("usage: testwrapper [testwrapper-flags] [pattern] [build/test flags & test binary flags]") fmt.Println("usage: testwrapper [testwrapper-flags] [pattern] [build/test flags & test binary flags]")
@ -172,6 +189,7 @@ func main() {
fmt.Println("examples:") fmt.Println("examples:")
fmt.Println("\ttestwrapper -v ./... -count=1") fmt.Println("\ttestwrapper -v ./... -count=1")
fmt.Println("\ttestwrapper ./pkg/foo -run TestBar -count=1") fmt.Println("\ttestwrapper ./pkg/foo -run TestBar -count=1")
fmt.Println("\ttestwrapper -json=test_attempts.json ./pkg/foo -run TestBar -count=1")
fmt.Println() fmt.Println()
fmt.Println("Unlike 'go test', testwrapper requires a package pattern as the first positional argument and only supports a single pattern.") fmt.Println("Unlike 'go test', testwrapper requires a package pattern as the first positional argument and only supports a single pattern.")
} }
@ -217,7 +235,15 @@ func main() {
} }
fmt.Printf("%s\t%s\n", outcome, pkg) fmt.Printf("%s\t%s\n", outcome, pkg)
} }
opts := &options{}
if *jsonFile != "" {
f, err := os.Create(*jsonFile)
if err != nil {
log.Printf("error creating test attempt json file: %v", err)
}
defer f.Close()
opts.w = f
}
for len(toRun) > 0 { for len(toRun) > 0 {
var thisRun *nextRun var thisRun *nextRun
thisRun, toRun = toRun[0], toRun[1:] thisRun, toRun = toRun[0], toRun[1:]
@ -234,20 +260,20 @@ func main() {
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 {
ch := make(chan *testAttempt) ch := make(chan *testAttempt)
go runTests(ctx, thisRun.attempt, pt, otherArgs, ch) go runTests(ctx, thisRun.attempt, pt, otherArgs, ch, *opts)
for tr := range ch { for tr := range ch {
if tr.pkgFinished { if tr.PkgFinished {
printPkgOutcome(tr.name.pkg, tr.outcome, thisRun.attempt) printPkgOutcome(tr.Name.Pkg, tr.Outcome, thisRun.attempt)
continue continue
} }
if *v || tr.outcome == "fail" { if *v || tr.Outcome == "fail" {
io.Copy(os.Stdout, &tr.logs) io.Copy(os.Stdout, &tr.logs)
} }
if tr.outcome != "fail" { if tr.Outcome != "fail" {
continue continue
} }
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
} }