flakytest.go 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
  1. // Copyright (c) Tailscale Inc & contributors
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. // Package flakytest contains test helpers for marking a test as flaky. For
  4. // tests run using cmd/testwrapper, a failed flaky test will cause tests to be
  5. // re-run a few time until they succeed or exceed our iteration limit.
  6. package flakytest
  7. import (
  8. "fmt"
  9. "os"
  10. "path"
  11. "regexp"
  12. "strconv"
  13. "sync"
  14. "testing"
  15. "tailscale.com/util/mak"
  16. )
  17. // FlakyTestLogMessage is a sentinel value that is printed to stderr when a
  18. // flaky test is marked. This is used by cmd/testwrapper to detect flaky tests
  19. // and retry them.
  20. const FlakyTestLogMessage = "flakytest: this is a known flaky test"
  21. // FlakeAttemptEnv is an environment variable that is set by cmd/testwrapper
  22. // when a flaky test is being (re)tried. It contains the attempt number,
  23. // starting at 1.
  24. const FlakeAttemptEnv = "TS_TESTWRAPPER_ATTEMPT"
  25. var issueRegexp = regexp.MustCompile(`\Ahttps://github\.com/[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+/issues/\d+\z`)
  26. var (
  27. rootFlakesMu sync.Mutex
  28. rootFlakes map[string]bool
  29. )
  30. // Mark sets the current test as a flaky test, such that if it fails, it will
  31. // be retried a few times on failure. issue must be a GitHub issue that tracks
  32. // the status of the flaky test being marked, of the format:
  33. //
  34. // https://github.com/tailscale/myRepo-H3re/issues/12345
  35. func Mark(t testing.TB, issue string) {
  36. if !issueRegexp.MatchString(issue) {
  37. t.Fatalf("bad issue format: %q", issue)
  38. }
  39. if _, ok := os.LookupEnv(FlakeAttemptEnv); ok {
  40. // We're being run under cmd/testwrapper so send our sentinel message
  41. // to stderr. (We avoid doing this when the env is absent to avoid
  42. // spamming people running tests without the wrapper)
  43. fmt.Fprintf(os.Stderr, "%s: %s\n", FlakyTestLogMessage, issue)
  44. }
  45. t.Attr("flaky-test-issue-url", issue)
  46. // The Attr method above also emits human-readable output, so this t.Logf
  47. // is somewhat redundant, but we keep it for compatibility with
  48. // old test runs, so cmd/testwrapper doesn't need to be modified.
  49. // TODO(bradfitz): switch testwrapper to look for Action "attr"
  50. // instead:
  51. // "Action":"attr","Package":"tailscale.com/cmd/testwrapper/flakytest","Test":"TestMarked_Root","Key":"flaky-test-issue-url","Value":"https://github.com/tailscale/tailscale/issues/0"}
  52. // And then remove this Logf a month or so after that.
  53. t.Logf("flakytest: issue tracking this flaky test: %s", issue)
  54. if boolEnv("TS_SKIP_FLAKY_TESTS") {
  55. t.Skipf("skipping due to TS_SKIP_FLAKY_TESTS")
  56. }
  57. // Record the root test name as flakey.
  58. rootFlakesMu.Lock()
  59. defer rootFlakesMu.Unlock()
  60. mak.Set(&rootFlakes, t.Name(), true)
  61. }
  62. // Marked reports whether the current test or one of its parents was marked flaky.
  63. func Marked(t testing.TB) bool {
  64. n := t.Name()
  65. for {
  66. if rootFlakes[n] {
  67. return true
  68. }
  69. n = path.Dir(n)
  70. if n == "." || n == "/" {
  71. break
  72. }
  73. }
  74. return false
  75. }
  76. func boolEnv(k string) bool {
  77. s := os.Getenv(k)
  78. if s == "" {
  79. return false
  80. }
  81. v, _ := strconv.ParseBool(s)
  82. return v
  83. }