| // Convenience utilities for testing. |
| package testutils |
| |
| import ( |
| "bytes" |
| "context" |
| "encoding/json" |
| "errors" |
| "fmt" |
| "io" |
| "io/ioutil" |
| "os" |
| "path" |
| "runtime" |
| "strings" |
| "sync" |
| "text/template" |
| "time" |
| |
| "github.com/google/uuid" |
| "github.com/stretchr/testify/mock" |
| "github.com/stretchr/testify/require" |
| "go.skia.org/infra/go/repo_root" |
| "go.skia.org/infra/go/sktest" |
| ) |
| |
| var ( |
| // TryAgainErr use used by TryUntil. |
| TryAgainErr = errors.New("Trying Again") |
| ) |
| |
| // 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.ReadCloser, 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 |
| } |
| |
| // ReadFileBytes reads a file from the caller's testdata directory and returns its contents as a |
| // slice of bytes. |
| func ReadFileBytes(filename string) ([]byte, error) { |
| f, err := readFile(filename) |
| if err != nil { |
| return nil, err |
| } |
| b, err := ioutil.ReadAll(f) |
| if err != nil { |
| return nil, fmt.Errorf("Could not read %s: %v", filename, err) |
| } |
| return b, nil |
| } |
| |
| // ReadFile reads a file from the caller's testdata directory. |
| func ReadFile(filename string) (string, error) { |
| b, err := ReadFileBytes(filename) |
| if err != nil { |
| return "", err |
| } |
| return string(b), nil |
| } |
| |
| // MustGetReader reads a file from the caller's testdata directory and panics on |
| // error. |
| func MustGetReader(filename string) io.ReadCloser { |
| r, err := readFile(filename) |
| if err != nil { |
| panic(err) |
| } |
| return r |
| } |
| |
| // MustReadFile returns 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 sktest.TestingT, filename, contents string) { |
| require.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 sktest.TestingT, c io.Closer) { |
| require.NoError(t, c.Close()) |
| } |
| |
| // Remove attempts to remove the given file and asserts that no error is returned. |
| func Remove(t sktest.TestingT, fp string) { |
| require.NoError(t, os.Remove(fp)) |
| } |
| |
| // RemoveAll attempts to remove the given directory and asserts that no error is returned. |
| func RemoveAll(t sktest.TestingT, fp string) { |
| require.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 sktest.TestingT) (string, func()) { |
| d, err := ioutil.TempDir("", "testutils") |
| require.NoError(t, err) |
| return d, func() { |
| RemoveAll(t, d) |
| } |
| } |
| |
| // MarshalJSON encodes the given interface to a JSON string. |
| func MarshalJSON(t sktest.TestingT, i interface{}) string { |
| b, err := json.Marshal(i) |
| require.NoError(t, err) |
| return string(b) |
| } |
| |
| // MarshalIndentJSON encodes the given interface to an indented JSON string. |
| func MarshalIndentJSON(t sktest.TestingT, i interface{}) string { |
| b, err := json.MarshalIndent(i, "", " ") |
| require.NoError(t, err) |
| return string(b) |
| } |
| |
| // AssertErrorContains asserts that the given error contains the given string. |
| func AssertErrorContains(t sktest.TestingT, err error, substr string) { |
| require.NotNil(t, err) |
| require.True(t, strings.Contains(err.Error(), substr)) |
| } |
| |
| // Return the path to the root of the checkout. |
| func GetRepoRoot(t sktest.TestingT) string { |
| root, err := repo_root.Get() |
| require.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.") |
| } |
| |
| // MockTestingT implements sktest.TestingT by saving calls to Log and Fail. MockTestingT can |
| // be used to test a test helper function. See also AssertFails. |
| // The methods Helper, Name, Skip, SkipNow, Skipf, and Skipped are unimplemented. |
| // This type is not safe for concurrent use. |
| type MockTestingT struct { |
| LogMsgs []string |
| IsFailed bool |
| } |
| |
| func (m *MockTestingT) Error(args ...interface{}) { |
| m.Log(args...) |
| m.Fail() |
| } |
| func (m *MockTestingT) Errorf(format string, args ...interface{}) { |
| m.Logf(format, args...) |
| m.Fail() |
| } |
| func (m *MockTestingT) Fail() { |
| m.IsFailed = true |
| } |
| func (m *MockTestingT) FailNow() { |
| m.Fail() |
| runtime.Goexit() |
| } |
| func (m *MockTestingT) Failed() bool { |
| return m.IsFailed |
| } |
| func (m *MockTestingT) Fatal(args ...interface{}) { |
| m.Log(args...) |
| m.FailNow() |
| } |
| func (m *MockTestingT) Fatalf(format string, args ...interface{}) { |
| m.Logf(format, args...) |
| m.FailNow() |
| } |
| func (m *MockTestingT) Helper() {} |
| func (m *MockTestingT) Log(args ...interface{}) { |
| m.LogMsgs = append(m.LogMsgs, fmt.Sprintln(args...)) |
| } |
| func (m *MockTestingT) Logf(format string, args ...interface{}) { |
| m.LogMsgs = append(m.LogMsgs, fmt.Sprintf(format, args...)) |
| } |
| func (m *MockTestingT) Name() string { |
| return "" |
| } |
| func (m *MockTestingT) Skip(args ...interface{}) { |
| m.Log(args...) |
| m.SkipNow() |
| } |
| func (m *MockTestingT) SkipNow() { |
| panic("SkipNow is not implemented.") |
| } |
| func (m *MockTestingT) Skipf(format string, args ...interface{}) { |
| m.Logf(format, args...) |
| m.SkipNow() |
| } |
| func (m *MockTestingT) Skipped() bool { |
| return false |
| } |
| |
| // Assert that MockTestingT implements the sktest.TestingT interface: |
| var _ sktest.TestingT = (*MockTestingT)(nil) |
| |
| // AssertFails runs testfn with a MockTestingT and asserts that the test fails and the first failure |
| // logged matches the regexp. The sktest.TestingT passed to testfn is not safe for concurrent use. |
| func AssertFails(parent sktest.TestingT, regexp string, testfn func(sktest.TestingT)) { |
| mock := MockTestingT{} |
| wg := sync.WaitGroup{} |
| wg.Add(1) |
| go func() { |
| defer wg.Done() |
| testfn(&mock) |
| }() |
| wg.Wait() |
| require.True(parent, mock.Failed(), "In AssertFails, the test function did not fail.") |
| require.True(parent, len(mock.LogMsgs) > 0, "In AssertFails, the test function did not produce any failure messages.") |
| require.Regexp(parent, regexp, mock.LogMsgs[0]) |
| } |
| |
| // AnyContext can be used to match any Context objects e.g. |
| // m.On("Foo", testutils.AnyContext).Return(...) |
| // This is better than trying to used mock.AnythingOfTypeArgument |
| // because that only works for concrete types, which could be brittle |
| // (e.g. a "normal" context is *context.emptyCtx, but one modified by |
| // trace.StartSpan() could be a *context.valueCtx) |
| var AnyContext = mock.MatchedBy(func(c context.Context) bool { |
| // if the passed in parameter does not implement the context.Context interface, the |
| // wrapping MatchedBy will panic - so we can simply return true, since we |
| // know it's a context.Context if execution flow makes it here. |
| return true |
| }) |
| |
| // ExecTemplate parses the given string as a text template, executes it using |
| // the given data, and returns the result as a string. |
| func ExecTemplate(t sktest.TestingT, tmpl string, data interface{}) string { |
| template, err := template.New(uuid.New().String()).Parse(tmpl) |
| require.NoError(t, err) |
| var buf bytes.Buffer |
| require.NoError(t, template.Execute(&buf, data)) |
| return buf.String() |
| } |