testwrapper_test.go 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. // Copyright (c) Tailscale Inc & contributors
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. package main_test
  4. import (
  5. "bytes"
  6. "errors"
  7. "fmt"
  8. "os"
  9. "os/exec"
  10. "path/filepath"
  11. "regexp"
  12. "runtime"
  13. "strings"
  14. "sync"
  15. "testing"
  16. )
  17. var (
  18. buildPath string
  19. buildErr error
  20. buildOnce sync.Once
  21. )
  22. func cmdTestwrapper(t *testing.T, args ...string) *exec.Cmd {
  23. buildOnce.Do(func() {
  24. buildPath, buildErr = buildTestWrapper()
  25. })
  26. if buildErr != nil {
  27. t.Fatalf("building testwrapper: %s", buildErr)
  28. }
  29. return exec.Command(buildPath, args...)
  30. }
  31. func buildTestWrapper() (string, error) {
  32. dir, err := os.MkdirTemp("", "testwrapper")
  33. if err != nil {
  34. return "", fmt.Errorf("making temp dir: %w", err)
  35. }
  36. _, err = exec.Command("go", "build", "-o", dir, ".").Output()
  37. if err != nil {
  38. return "", fmt.Errorf("go build: %w", err)
  39. }
  40. return filepath.Join(dir, "testwrapper"), nil
  41. }
  42. func TestRetry(t *testing.T) {
  43. t.Parallel()
  44. testfile := filepath.Join(t.TempDir(), "retry_test.go")
  45. code := []byte(`package retry_test
  46. import (
  47. "os"
  48. "testing"
  49. "tailscale.com/cmd/testwrapper/flakytest"
  50. )
  51. func TestOK(t *testing.T) {}
  52. func TestFlakeRun(t *testing.T) {
  53. flakytest.Mark(t, "https://github.com/tailscale/tailscale/issues/0") // random issue
  54. e := os.Getenv(flakytest.FlakeAttemptEnv)
  55. if e == "" {
  56. t.Skip("not running in testwrapper")
  57. }
  58. if e == "1" {
  59. t.Fatal("First run in testwrapper, failing so that test is retried. This is expected.")
  60. }
  61. }
  62. `)
  63. if err := os.WriteFile(testfile, code, 0o644); err != nil {
  64. t.Fatalf("writing package: %s", err)
  65. }
  66. out, err := cmdTestwrapper(t, "-v", testfile).CombinedOutput()
  67. if err != nil {
  68. t.Fatalf("go run . %s: %s with output:\n%s", testfile, err, out)
  69. }
  70. // Replace the unpredictable timestamp with "0.00s".
  71. out = regexp.MustCompile(`\t\d+\.\d\d\ds\t`).ReplaceAll(out, []byte("\t0.00s\t"))
  72. want := []byte("ok\t" + testfile + "\t0.00s\t[attempt=2]")
  73. if !bytes.Contains(out, want) {
  74. t.Fatalf("wanted output containing %q but got:\n%s", want, out)
  75. }
  76. if okRuns := bytes.Count(out, []byte("=== RUN TestOK")); okRuns != 1 {
  77. t.Fatalf("expected TestOK to be run once but was run %d times in output:\n%s", okRuns, out)
  78. }
  79. if flakeRuns := bytes.Count(out, []byte("=== RUN TestFlakeRun")); flakeRuns != 2 {
  80. t.Fatalf("expected TestFlakeRun to be run twice but was run %d times in output:\n%s", flakeRuns, out)
  81. }
  82. if testing.Verbose() {
  83. t.Logf("success - output:\n%s", out)
  84. }
  85. }
  86. func TestNoRetry(t *testing.T) {
  87. t.Parallel()
  88. testfile := filepath.Join(t.TempDir(), "noretry_test.go")
  89. code := []byte(`package noretry_test
  90. import (
  91. "testing"
  92. "tailscale.com/cmd/testwrapper/flakytest"
  93. )
  94. func TestFlakeRun(t *testing.T) {
  95. flakytest.Mark(t, "https://github.com/tailscale/tailscale/issues/0") // random issue
  96. t.Error("shouldn't be retried")
  97. }
  98. func TestAlwaysError(t *testing.T) {
  99. t.Error("error")
  100. }
  101. `)
  102. if err := os.WriteFile(testfile, code, 0o644); err != nil {
  103. t.Fatalf("writing package: %s", err)
  104. }
  105. out, err := cmdTestwrapper(t, "-v", testfile).Output()
  106. if err == nil {
  107. t.Fatalf("go run . %s: expected error but it succeeded with output:\n%s", testfile, out)
  108. }
  109. if code, ok := errExitCode(err); ok && code != 1 {
  110. t.Fatalf("expected exit code 1 but got %d", code)
  111. }
  112. want := []byte("Not retrying flaky tests because non-flaky tests failed.")
  113. if !bytes.Contains(out, want) {
  114. t.Fatalf("wanted output containing %q but got:\n%s", want, out)
  115. }
  116. if flakeRuns := bytes.Count(out, []byte("=== RUN TestFlakeRun")); flakeRuns != 1 {
  117. t.Fatalf("expected TestFlakeRun to be run once but was run %d times in output:\n%s", flakeRuns, out)
  118. }
  119. if testing.Verbose() {
  120. t.Logf("success - output:\n%s", out)
  121. }
  122. }
  123. func TestBuildError(t *testing.T) {
  124. t.Parallel()
  125. // Construct our broken package.
  126. testfile := filepath.Join(t.TempDir(), "builderror_test.go")
  127. code := []byte("package builderror_test\n\nderp")
  128. err := os.WriteFile(testfile, code, 0o644)
  129. if err != nil {
  130. t.Fatalf("writing package: %s", err)
  131. }
  132. wantErr := "builderror_test.go:3:1: expected declaration, found derp\nFAIL"
  133. // Confirm `go test` exits with code 1.
  134. goOut, err := exec.Command("go", "test", testfile).CombinedOutput()
  135. if code, ok := errExitCode(err); !ok || code != 1 {
  136. t.Fatalf("go test %s: got exit code %d, want 1 (err: %v)", testfile, code, err)
  137. }
  138. if !strings.Contains(string(goOut), wantErr) {
  139. t.Fatalf("go test %s: got output %q, want output containing %q", testfile, goOut, wantErr)
  140. }
  141. // Confirm `testwrapper` exits with code 1.
  142. twOut, err := cmdTestwrapper(t, testfile).CombinedOutput()
  143. if code, ok := errExitCode(err); !ok || code != 1 {
  144. t.Fatalf("testwrapper %s: got exit code %d, want 1 (err: %v)", testfile, code, err)
  145. }
  146. if !strings.Contains(string(twOut), wantErr) {
  147. t.Fatalf("testwrapper %s: got output %q, want output containing %q", testfile, twOut, wantErr)
  148. }
  149. if testing.Verbose() {
  150. t.Logf("success - output:\n%s", twOut)
  151. }
  152. }
  153. func TestTimeout(t *testing.T) {
  154. t.Parallel()
  155. // Construct our broken package.
  156. testfile := filepath.Join(t.TempDir(), "timeout_test.go")
  157. code := []byte(`package noretry_test
  158. import (
  159. "testing"
  160. "time"
  161. )
  162. func TestTimeout(t *testing.T) {
  163. time.Sleep(500 * time.Millisecond)
  164. }
  165. `)
  166. err := os.WriteFile(testfile, code, 0o644)
  167. if err != nil {
  168. t.Fatalf("writing package: %s", err)
  169. }
  170. out, err := cmdTestwrapper(t, testfile, "-timeout=20ms").CombinedOutput()
  171. if code, ok := errExitCode(err); !ok || code != 1 {
  172. t.Fatalf("testwrapper %s: expected error with exit code 0 but got: %v; output was:\n%s", testfile, err, out)
  173. }
  174. if want := "panic: test timed out after 20ms"; !bytes.Contains(out, []byte(want)) {
  175. t.Fatalf("testwrapper %s: expected build error containing %q but got:\n%s", testfile, buildErr, out)
  176. }
  177. if testing.Verbose() {
  178. t.Logf("success - output:\n%s", out)
  179. }
  180. }
  181. func TestCached(t *testing.T) {
  182. t.Parallel()
  183. // Construct our trivial package.
  184. pkgDir := t.TempDir()
  185. goMod := fmt.Sprintf(`module example.com
  186. go %s
  187. `, runtime.Version()[2:]) // strip leading "go"
  188. test := `package main
  189. import "testing"
  190. func TestCached(t *testing.T) {}
  191. `
  192. for f, c := range map[string]string{
  193. "go.mod": goMod,
  194. "cached_test.go": test,
  195. } {
  196. err := os.WriteFile(filepath.Join(pkgDir, f), []byte(c), 0o644)
  197. if err != nil {
  198. t.Fatalf("writing package: %s", err)
  199. }
  200. }
  201. for name, args := range map[string][]string{
  202. "without_flags": {"./..."},
  203. "with_short": {"./...", "-short"},
  204. "with_coverprofile": {"./...", "-coverprofile=" + filepath.Join(t.TempDir(), "coverage.out")},
  205. } {
  206. t.Run(name, func(t *testing.T) {
  207. var (
  208. out []byte
  209. err error
  210. )
  211. for range 2 {
  212. cmd := cmdTestwrapper(t, args...)
  213. cmd.Dir = pkgDir
  214. out, err = cmd.CombinedOutput()
  215. if err != nil {
  216. t.Fatalf("testwrapper ./...: expected no error but got: %v; output was:\n%s", err, out)
  217. }
  218. }
  219. want := []byte("ok\texample.com\t(cached)")
  220. if !bytes.Contains(out, want) {
  221. t.Fatalf("wanted output containing %q but got:\n%s", want, out)
  222. }
  223. if testing.Verbose() {
  224. t.Logf("success - output:\n%s", out)
  225. }
  226. })
  227. }
  228. }
  229. func errExitCode(err error) (int, bool) {
  230. var exit *exec.ExitError
  231. if errors.As(err, &exit) {
  232. return exit.ExitCode(), true
  233. }
  234. return 0, false
  235. }