testwrapper.go 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. // Copyright (c) Tailscale Inc & AUTHORS
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. // testwrapper is a wrapper for retrying flaky tests. It is an alternative to
  4. // `go test` and re-runs failed marked flaky tests (using the flakytest pkg). It
  5. // takes different arguments than go test and requires the first positional
  6. // argument to be the pattern to test.
  7. package main
  8. import (
  9. "bytes"
  10. "context"
  11. "encoding/json"
  12. "errors"
  13. "flag"
  14. "fmt"
  15. "io"
  16. "log"
  17. "os"
  18. "os/exec"
  19. "sort"
  20. "strings"
  21. "time"
  22. "golang.org/x/exp/maps"
  23. "tailscale.com/cmd/testwrapper/flakytest"
  24. )
  25. const maxAttempts = 3
  26. type testAttempt struct {
  27. name testName
  28. outcome string // "pass", "fail", "skip"
  29. logs bytes.Buffer
  30. isMarkedFlaky bool // set if the test is marked as flaky
  31. pkgFinished bool
  32. }
  33. type testName struct {
  34. pkg string // "tailscale.com/types/key"
  35. name string // "TestFoo"
  36. }
  37. type packageTests struct {
  38. // pattern is the package pattern to run.
  39. // Must be a single pattern, not a list of patterns.
  40. pattern string // "./...", "./types/key"
  41. // tests is a list of tests to run. If empty, all tests in the package are
  42. // run.
  43. tests []string // ["TestFoo", "TestBar"]
  44. }
  45. type goTestOutput struct {
  46. Time time.Time
  47. Action string
  48. Package string
  49. Test string
  50. Output string
  51. }
  52. var debug = os.Getenv("TS_TESTWRAPPER_DEBUG") != ""
  53. // runTests runs the tests in pt and sends the results on ch. It sends a
  54. // testAttempt for each test and a final testAttempt per pkg with pkgFinished
  55. // set to true.
  56. // It calls close(ch) when it's done.
  57. func runTests(ctx context.Context, attempt int, pt *packageTests, otherArgs []string, ch chan<- *testAttempt) {
  58. defer close(ch)
  59. args := []string{"test", "-json", pt.pattern}
  60. args = append(args, otherArgs...)
  61. if len(pt.tests) > 0 {
  62. runArg := strings.Join(pt.tests, "|")
  63. args = append(args, "-run", runArg)
  64. }
  65. if debug {
  66. fmt.Println("running", strings.Join(args, " "))
  67. }
  68. cmd := exec.CommandContext(ctx, "go", args...)
  69. r, err := cmd.StdoutPipe()
  70. if err != nil {
  71. log.Printf("error creating stdout pipe: %v", err)
  72. }
  73. cmd.Stderr = os.Stderr
  74. cmd.Env = os.Environ()
  75. cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%d", flakytest.FlakeAttemptEnv, attempt))
  76. if err := cmd.Start(); err != nil {
  77. log.Printf("error starting test: %v", err)
  78. os.Exit(1)
  79. }
  80. done := make(chan struct{})
  81. go func() {
  82. defer close(done)
  83. cmd.Wait()
  84. }()
  85. jd := json.NewDecoder(r)
  86. resultMap := make(map[testName]*testAttempt)
  87. for {
  88. var goOutput goTestOutput
  89. if err := jd.Decode(&goOutput); err != nil {
  90. if errors.Is(err, io.EOF) || errors.Is(err, os.ErrClosed) {
  91. break
  92. }
  93. panic(err)
  94. }
  95. if goOutput.Test == "" {
  96. switch goOutput.Action {
  97. case "fail", "pass", "skip":
  98. ch <- &testAttempt{
  99. name: testName{
  100. pkg: goOutput.Package,
  101. },
  102. outcome: goOutput.Action,
  103. pkgFinished: true,
  104. }
  105. }
  106. continue
  107. }
  108. name := testName{
  109. pkg: goOutput.Package,
  110. name: goOutput.Test,
  111. }
  112. if test, _, isSubtest := strings.Cut(goOutput.Test, "/"); isSubtest {
  113. name.name = test
  114. if goOutput.Action == "output" {
  115. resultMap[name].logs.WriteString(goOutput.Output)
  116. }
  117. continue
  118. }
  119. switch goOutput.Action {
  120. case "start":
  121. // ignore
  122. case "run":
  123. resultMap[name] = &testAttempt{
  124. name: name,
  125. }
  126. case "skip", "pass", "fail":
  127. resultMap[name].outcome = goOutput.Action
  128. ch <- resultMap[name]
  129. case "output":
  130. if strings.TrimSpace(goOutput.Output) == flakytest.FlakyTestLogMessage {
  131. resultMap[name].isMarkedFlaky = true
  132. } else {
  133. resultMap[name].logs.WriteString(goOutput.Output)
  134. }
  135. }
  136. }
  137. <-done
  138. }
  139. func main() {
  140. ctx := context.Background()
  141. // We only need to parse the -v flag to figure out whether to print the logs
  142. // for a test. We don't need to parse any other flags, so we just use the
  143. // flag package to parse the -v flag and then pass the rest of the args
  144. // through to 'go test'.
  145. // We run `go test -json` which returns the same information as `go test -v`,
  146. // but in a machine-readable format. So this flag is only for testwrapper's
  147. // output.
  148. v := flag.Bool("v", false, "verbose")
  149. flag.Usage = func() {
  150. fmt.Println("usage: testwrapper [testwrapper-flags] [pattern] [build/test flags & test binary flags]")
  151. fmt.Println()
  152. fmt.Println("testwrapper-flags:")
  153. flag.CommandLine.PrintDefaults()
  154. fmt.Println()
  155. fmt.Println("examples:")
  156. fmt.Println("\ttestwrapper -v ./... -count=1")
  157. fmt.Println("\ttestwrapper ./pkg/foo -run TestBar -count=1")
  158. fmt.Println()
  159. fmt.Println("Unlike 'go test', testwrapper requires a package pattern as the first positional argument and only supports a single pattern.")
  160. }
  161. flag.Parse()
  162. args := flag.Args()
  163. if len(args) < 1 || strings.HasPrefix(args[0], "-") {
  164. fmt.Println("no pattern specified")
  165. flag.Usage()
  166. os.Exit(1)
  167. } else if len(args) > 1 && !strings.HasPrefix(args[1], "-") {
  168. fmt.Println("expected single pattern")
  169. flag.Usage()
  170. os.Exit(1)
  171. }
  172. pattern, otherArgs := args[0], args[1:]
  173. type nextRun struct {
  174. tests []*packageTests
  175. attempt int
  176. }
  177. toRun := []*nextRun{
  178. {
  179. tests: []*packageTests{{pattern: pattern}},
  180. attempt: 1,
  181. },
  182. }
  183. printPkgOutcome := func(pkg, outcome string, attempt int) {
  184. if outcome == "skip" {
  185. fmt.Printf("?\t%s [skipped/no tests] \n", pkg)
  186. return
  187. }
  188. if outcome == "pass" {
  189. outcome = "ok"
  190. }
  191. if outcome == "fail" {
  192. outcome = "FAIL"
  193. }
  194. if attempt > 1 {
  195. fmt.Printf("%s\t%s [attempt=%d]\n", outcome, pkg, attempt)
  196. return
  197. }
  198. fmt.Printf("%s\t%s\n", outcome, pkg)
  199. }
  200. for len(toRun) > 0 {
  201. var thisRun *nextRun
  202. thisRun, toRun = toRun[0], toRun[1:]
  203. if thisRun.attempt >= maxAttempts {
  204. fmt.Println("max attempts reached")
  205. os.Exit(1)
  206. }
  207. if thisRun.attempt > 1 {
  208. fmt.Printf("\n\nAttempt #%d: Retrying flaky tests:\n\n", thisRun.attempt)
  209. }
  210. failed := false
  211. toRetry := make(map[string][]string) // pkg -> tests to retry
  212. for _, pt := range thisRun.tests {
  213. ch := make(chan *testAttempt)
  214. go runTests(ctx, thisRun.attempt, pt, otherArgs, ch)
  215. for tr := range ch {
  216. if tr.pkgFinished {
  217. printPkgOutcome(tr.name.pkg, tr.outcome, thisRun.attempt)
  218. continue
  219. }
  220. if *v || tr.outcome == "fail" {
  221. io.Copy(os.Stdout, &tr.logs)
  222. }
  223. if tr.outcome != "fail" {
  224. continue
  225. }
  226. if tr.isMarkedFlaky {
  227. toRetry[tr.name.pkg] = append(toRetry[tr.name.pkg], tr.name.name)
  228. } else {
  229. failed = true
  230. }
  231. }
  232. }
  233. if failed {
  234. fmt.Println("\n\nNot retrying flaky tests because non-flaky tests failed.")
  235. os.Exit(1)
  236. }
  237. if len(toRetry) == 0 {
  238. continue
  239. }
  240. pkgs := maps.Keys(toRetry)
  241. sort.Strings(pkgs)
  242. nextRun := &nextRun{
  243. attempt: thisRun.attempt + 1,
  244. }
  245. for _, pkg := range pkgs {
  246. tests := toRetry[pkg]
  247. sort.Strings(tests)
  248. nextRun.tests = append(nextRun.tests, &packageTests{
  249. pattern: pkg,
  250. tests: tests,
  251. })
  252. }
  253. toRun = append(toRun, nextRun)
  254. }
  255. }