testwrapper_test.go 5.8 KB

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