| // Convenience utilities for testing. |
| package testutils |
| |
| import ( |
| "encoding/json" |
| "errors" |
| "flag" |
| "fmt" |
| "io" |
| "io/ioutil" |
| "os" |
| "path" |
| "runtime" |
| "strings" |
| "time" |
| |
| "cloud.google.com/go/compute/metadata" |
| assert "github.com/stretchr/testify/require" |
| "go.skia.org/infra/go/repo_root" |
| ) |
| |
| const ( |
| SMALL_TEST = "small" |
| MEDIUM_TEST = "medium" |
| LARGE_TEST = "large" |
| ) |
| |
| var ( |
| small = flag.Bool(SMALL_TEST, false, "Whether or not to run small tests.") |
| medium = flag.Bool(MEDIUM_TEST, false, "Whether or not to run medium tests.") |
| large = flag.Bool(LARGE_TEST, false, "Whether or not to run large tests.") |
| uncategorized = flag.Bool("uncategorized", false, "Only run uncategorized tests.") |
| |
| // DEFAULT_RUN indicates whether the given test type runs by default |
| // when no filter flag is specified. |
| DEFAULT_RUN = map[string]bool{ |
| SMALL_TEST: true, |
| MEDIUM_TEST: true, |
| LARGE_TEST: true, |
| } |
| |
| TIMEOUT_SMALL = "4s" |
| TIMEOUT_MEDIUM = "15s" |
| TIMEOUT_LARGE = "4m" |
| |
| TIMEOUT_RACE = "5m" |
| |
| // TEST_TYPES lists all of the types of tests. |
| TEST_TYPES = []string{ |
| SMALL_TEST, |
| MEDIUM_TEST, |
| LARGE_TEST, |
| } |
| |
| // TryAgainErr use used by TryUntil. |
| TryAgainErr = errors.New("Trying Again") |
| ) |
| |
| // TestingT is an interface which is compatible with testing.T and testing.B, |
| // used so that we don't have to import the "testing" package except in _test.go |
| // files. |
| type TestingT interface { |
| Error(...interface{}) |
| Errorf(string, ...interface{}) |
| Fail() |
| FailNow() |
| Failed() bool |
| Fatal(...interface{}) |
| Fatalf(string, ...interface{}) |
| Helper() |
| Log(...interface{}) |
| Logf(string, ...interface{}) |
| Name() string |
| Skip(...interface{}) |
| SkipNow() |
| Skipf(string, ...interface{}) |
| Skipped() bool |
| } |
| |
| // ShouldRun determines whether the test should run based on the provided flags. |
| func ShouldRun(testType string) bool { |
| if *uncategorized { |
| return false |
| } |
| |
| // Fallback if no test filter is specified. |
| if !*small && !*medium && !*large { |
| return DEFAULT_RUN[testType] |
| } |
| |
| switch testType { |
| case SMALL_TEST: |
| return *small |
| case MEDIUM_TEST: |
| return *medium |
| case LARGE_TEST: |
| return *large |
| } |
| return false |
| } |
| |
| // SmallTest is a function which should be called at the beginning of a small |
| // test: A test (under 2 seconds) with no dependencies on external databases, |
| // networks, etc. |
| func SmallTest(t TestingT) { |
| if !ShouldRun(SMALL_TEST) { |
| t.Skip("Not running small tests.") |
| } |
| } |
| |
| // MediumTest is a function which should be called at the beginning of an |
| // medium-sized test: a test (2-15 seconds) which has dependencies on external |
| // databases, networks, etc. |
| func MediumTest(t TestingT) { |
| if !ShouldRun(MEDIUM_TEST) { |
| t.Skip("Not running medium tests.") |
| } |
| } |
| |
| // LargeTest is a function which should be called at the beginning of a large |
| // test: a test (> 15 seconds) with significant reliance on external |
| // dependencies which makes it too slow or flaky to run as part of the normal |
| // test suite. |
| func LargeTest(t TestingT) { |
| if !ShouldRun(LARGE_TEST) { |
| t.Skip("Not running large tests.") |
| } |
| } |
| |
| // ManualTest is a function which should be called at the beginning of tests |
| // which shouldn't run on the bots due to excessive running time, external |
| // requirements, etc. |
| func ManualTest(t TestingT) { |
| if os.Getenv("SWARMING_HEADLESS") == "1" { |
| t.Skip("Not running manual tests.") |
| } |
| } |
| |
| // TestDataDir returns the path to the caller's testdata directory, which |
| // is assumed to be "<path to caller dir>/testdata". |
| func TestDataDir() (string, error) { |
| _, thisFile, _, ok := runtime.Caller(0) |
| if !ok { |
| return "", fmt.Errorf("Could not find test data dir: runtime.Caller() failed.") |
| } |
| for skip := 0; ; skip++ { |
| _, file, _, ok := runtime.Caller(skip) |
| if !ok { |
| return "", fmt.Errorf("Could not find test data dir: runtime.Caller() failed.") |
| } |
| if file != thisFile { |
| return path.Join(path.Dir(file), "testdata"), nil |
| } |
| } |
| } |
| |
| func readFile(filename string) (io.Reader, error) { |
| dir, err := TestDataDir() |
| if err != nil { |
| return nil, fmt.Errorf("Could not read %s: %v", filename, err) |
| } |
| f, err := os.Open(path.Join(dir, filename)) |
| if err != nil { |
| return nil, fmt.Errorf("Could not read %s: %v", filename, err) |
| } |
| return f, nil |
| } |
| |
| // ReadFile reads a file from the caller's testdata directory. |
| func ReadFile(filename string) (string, error) { |
| f, err := readFile(filename) |
| if err != nil { |
| return "", err |
| } |
| b, err := ioutil.ReadAll(f) |
| if err != nil { |
| return "", fmt.Errorf("Could not read %s: %v", filename, err) |
| } |
| return string(b), nil |
| } |
| |
| // MustReadFile reads a file from the caller's testdata directory and panics on |
| // error. |
| func MustReadFile(filename string) string { |
| s, err := ReadFile(filename) |
| if err != nil { |
| panic(err) |
| } |
| return s |
| } |
| |
| // ReadJsonFile reads a JSON file from the caller's testdata directory into the |
| // given interface. |
| func ReadJsonFile(filename string, dest interface{}) error { |
| f, err := readFile(filename) |
| if err != nil { |
| return err |
| } |
| return json.NewDecoder(f).Decode(dest) |
| } |
| |
| // MustReadJsonFile reads a JSON file from the caller's testdata directory into |
| // the given interface and panics on error. |
| func MustReadJsonFile(filename string, dest interface{}) { |
| if err := ReadJsonFile(filename, dest); err != nil { |
| panic(err) |
| } |
| } |
| |
| // WriteFile writes the given contents to the given file path, reporting any |
| // error. |
| func WriteFile(t TestingT, filename, contents string) { |
| assert.NoErrorf(t, ioutil.WriteFile(filename, []byte(contents), os.ModePerm), "Unable to write to file %s", filename) |
| } |
| |
| // AssertCloses takes an ioutil.Closer and asserts that it closes. E.g.: |
| // frobber := NewFrobber() |
| // defer testutils.AssertCloses(t, frobber) |
| func AssertCloses(t TestingT, c io.Closer) { |
| assert.NoError(t, c.Close()) |
| } |
| |
| // Remove attempts to remove the given file and asserts that no error is returned. |
| func Remove(t TestingT, fp string) { |
| assert.NoError(t, os.Remove(fp)) |
| } |
| |
| // RemoveAll attempts to remove the given directory and asserts that no error is returned. |
| func RemoveAll(t TestingT, fp string) { |
| assert.NoError(t, os.RemoveAll(fp)) |
| } |
| |
| // TempDir is a wrapper for ioutil.TempDir. Returns the path to the directory and a cleanup |
| // function to defer. |
| func TempDir(t TestingT) (string, func()) { |
| d, err := ioutil.TempDir("", "testutils") |
| assert.NoError(t, err) |
| return d, func() { |
| RemoveAll(t, d) |
| } |
| } |
| |
| // MarshalJSON encodes the given interface to a JSON string. |
| func MarshalJSON(t TestingT, i interface{}) string { |
| b, err := json.Marshal(i) |
| assert.NoError(t, err) |
| return string(b) |
| } |
| |
| // MarshalIndentJSON encodes the given interface to an indented JSON string. |
| func MarshalIndentJSON(t TestingT, i interface{}) string { |
| b, err := json.MarshalIndent(i, "", " ") |
| assert.NoError(t, err) |
| return string(b) |
| } |
| |
| // AssertErrorContains asserts that the given error contains the given string. |
| func AssertErrorContains(t TestingT, err error, substr string) { |
| assert.NotNil(t, err) |
| assert.True(t, strings.Contains(err.Error(), substr)) |
| } |
| |
| // Return the path to the root of the checkout. |
| func GetRepoRoot(t TestingT) string { |
| root, err := repo_root.Get() |
| assert.NoError(t, err) |
| return root |
| } |
| |
| // EventuallyConsistent tries a test repeatedly until either the test passes |
| // or time expires, and is used when tests are written to expect |
| // non-eventual consistency. |
| // |
| // Use this function sparingly. |
| // |
| // duration - The amount of time to keep trying. |
| // f - The func to run the tests, should return TryAgainErr if |
| // we should keep trying, otherwise TryUntil will return |
| // with the err that f() returns. |
| func EventuallyConsistent(duration time.Duration, f func() error) error { |
| begin := time.Now() |
| for time.Now().Sub(begin) < duration { |
| if err := f(); err != TryAgainErr { |
| return err |
| } |
| } |
| return fmt.Errorf("Failed to pass test in allotted time.") |
| } |
| |
| // LocalOnlyTest will skip the test if it is bein run on GCE. |
| func LocalOnlyTest(t TestingT) { |
| if metadata.OnGCE() { |
| t.Skipf("Test is only run locally.") |
| } |
| } |