testwrapper_test.go 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. // Copyright (c) Tailscale Inc & AUTHORS
  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. "sync"
  13. "testing"
  14. )
  15. var (
  16. buildPath string
  17. buildErr error
  18. buildOnce sync.Once
  19. )
  20. func cmdTestwrapper(t *testing.T, args ...string) *exec.Cmd {
  21. buildOnce.Do(func() {
  22. buildPath, buildErr = buildTestWrapper()
  23. })
  24. if buildErr != nil {
  25. t.Fatalf("building testwrapper: %s", buildErr)
  26. }
  27. return exec.Command(buildPath, args...)
  28. }
  29. func buildTestWrapper() (string, error) {
  30. dir, err := os.MkdirTemp("", "testwrapper")
  31. if err != nil {
  32. return "", fmt.Errorf("making temp dir: %w", err)
  33. }
  34. _, err = exec.Command("go", "build", "-o", dir, ".").Output()
  35. if err != nil {
  36. return "", fmt.Errorf("go build: %w", err)
  37. }
  38. return filepath.Join(dir, "testwrapper"), nil
  39. }
  40. func TestRetry(t *testing.T) {
  41. t.Parallel()
  42. testfile := filepath.Join(t.TempDir(), "retry_test.go")
  43. code := []byte(`package retry_test
  44. import (
  45. "os"
  46. "testing"
  47. "tailscale.com/cmd/testwrapper/flakytest"
  48. )
  49. func TestOK(t *testing.T) {}
  50. func TestFlakeRun(t *testing.T) {
  51. flakytest.Mark(t, "https://github.com/tailscale/tailscale/issues/0") // random issue
  52. e := os.Getenv(flakytest.FlakeAttemptEnv)
  53. if e == "" {
  54. t.Skip("not running in testwrapper")
  55. }
  56. if e == "1" {
  57. t.Fatal("First run in testwrapper, failing so that test is retried. This is expected.")
  58. }
  59. }
  60. `)
  61. if err := os.WriteFile(testfile, code, 0o644); err != nil {
  62. t.Fatalf("writing package: %s", err)
  63. }
  64. out, err := cmdTestwrapper(t, "-v", testfile).CombinedOutput()
  65. if err != nil {
  66. t.Fatalf("go run . %s: %s with output:\n%s", testfile, err, out)
  67. }
  68. // Replace the unpredictable timestamp with "0.00s".
  69. out = regexp.MustCompile(`\t\d+\.\d\d\ds\t`).ReplaceAll(out, []byte("\t0.00s\t"))
  70. want := []byte("ok\t" + testfile + "\t0.00s\t[attempt=2]")
  71. if !bytes.Contains(out, want) {
  72. t.Fatalf("wanted output containing %q but got:\n%s", want, out)
  73. }
  74. if okRuns := bytes.Count(out, []byte("=== RUN TestOK")); okRuns != 1 {
  75. t.Fatalf("expected TestOK to be run once but was run %d times in output:\n%s", okRuns, out)
  76. }
  77. if flakeRuns := bytes.Count(out, []byte("=== RUN TestFlakeRun")); flakeRuns != 2 {
  78. t.Fatalf("expected TestFlakeRun to be run twice but was run %d times in output:\n%s", flakeRuns, out)
  79. }
  80. if testing.Verbose() {
  81. t.Logf("success - output:\n%s", out)
  82. }
  83. }
  84. func TestNoRetry(t *testing.T) {
  85. t.Parallel()
  86. testfile := filepath.Join(t.TempDir(), "noretry_test.go")
  87. code := []byte(`package noretry_test
  88. import (
  89. "testing"
  90. "tailscale.com/cmd/testwrapper/flakytest"
  91. )
  92. func TestFlakeRun(t *testing.T) {
  93. flakytest.Mark(t, "https://github.com/tailscale/tailscale/issues/0") // random issue
  94. t.Error("shouldn't be retried")
  95. }
  96. func TestAlwaysError(t *testing.T) {
  97. t.Error("error")
  98. }
  99. `)
  100. if err := os.WriteFile(testfile, code, 0o644); err != nil {
  101. t.Fatalf("writing package: %s", err)
  102. }
  103. out, err := cmdTestwrapper(t, "-v", testfile).Output()
  104. if err == nil {
  105. t.Fatalf("go run . %s: expected error but it succeeded with output:\n%s", testfile, out)
  106. }
  107. if code, ok := errExitCode(err); ok && code != 1 {
  108. t.Fatalf("expected exit code 1 but got %d", code)
  109. }
  110. want := []byte("Not retrying flaky tests because non-flaky tests failed.")
  111. if !bytes.Contains(out, want) {
  112. t.Fatalf("wanted output containing %q but got:\n%s", want, out)
  113. }
  114. if flakeRuns := bytes.Count(out, []byte("=== RUN TestFlakeRun")); flakeRuns != 1 {
  115. t.Fatalf("expected TestFlakeRun to be run once but was run %d times in output:\n%s", flakeRuns, out)
  116. }
  117. if testing.Verbose() {
  118. t.Logf("success - output:\n%s", out)
  119. }
  120. }
  121. func TestBuildError(t *testing.T) {
  122. t.Parallel()
  123. // Construct our broken package.
  124. testfile := filepath.Join(t.TempDir(), "builderror_test.go")
  125. code := []byte("package builderror_test\n\nderp")
  126. err := os.WriteFile(testfile, code, 0o644)
  127. if err != nil {
  128. t.Fatalf("writing package: %s", err)
  129. }
  130. buildErr := []byte("builderror_test.go:3:1: expected declaration, found derp\nFAIL command-line-arguments [setup failed]")
  131. // Confirm `go test` exits with code 1.
  132. goOut, err := exec.Command("go", "test", testfile).CombinedOutput()
  133. if code, ok := errExitCode(err); !ok || code != 1 {
  134. t.Fatalf("go test %s: expected error with exit code 0 but got: %v", testfile, err)
  135. }
  136. if !bytes.Contains(goOut, buildErr) {
  137. t.Fatalf("go test %s: expected build error containing %q but got:\n%s", testfile, buildErr, goOut)
  138. }
  139. // Confirm `testwrapper` exits with code 1.
  140. twOut, err := cmdTestwrapper(t, testfile).CombinedOutput()
  141. if code, ok := errExitCode(err); !ok || code != 1 {
  142. t.Fatalf("testwrapper %s: expected error with exit code 0 but got: %v", testfile, err)
  143. }
  144. if !bytes.Contains(twOut, buildErr) {
  145. t.Fatalf("testwrapper %s: expected build error containing %q but got:\n%s", testfile, buildErr, twOut)
  146. }
  147. if testing.Verbose() {
  148. t.Logf("success - output:\n%s", twOut)
  149. }
  150. }
  151. func TestTimeout(t *testing.T) {
  152. t.Parallel()
  153. // Construct our broken package.
  154. testfile := filepath.Join(t.TempDir(), "timeout_test.go")
  155. code := []byte(`package noretry_test
  156. import (
  157. "testing"
  158. "time"
  159. )
  160. func TestTimeout(t *testing.T) {
  161. time.Sleep(500 * time.Millisecond)
  162. }
  163. `)
  164. err := os.WriteFile(testfile, code, 0o644)
  165. if err != nil {
  166. t.Fatalf("writing package: %s", err)
  167. }
  168. out, err := cmdTestwrapper(t, testfile, "-timeout=20ms").CombinedOutput()
  169. if code, ok := errExitCode(err); !ok || code != 1 {
  170. t.Fatalf("testwrapper %s: expected error with exit code 0 but got: %v; output was:\n%s", testfile, err, out)
  171. }
  172. if want := "panic: test timed out after 20ms"; !bytes.Contains(out, []byte(want)) {
  173. t.Fatalf("testwrapper %s: expected build error containing %q but got:\n%s", testfile, buildErr, out)
  174. }
  175. if testing.Verbose() {
  176. t.Logf("success - output:\n%s", out)
  177. }
  178. }
  179. func errExitCode(err error) (int, bool) {
  180. var exit *exec.ExitError
  181. if errors.As(err, &exit) {
  182. return exit.ExitCode(), true
  183. }
  184. return 0, false
  185. }