v23tests: usability improvements.
Change-Id: Ib3cf900cfe23ce95c05eea13deb275e1f608aa63
diff --git a/lib/testutil/v23tests/v23tests.go b/lib/testutil/v23tests/v23tests.go
index e93850f..9b11b5a 100644
--- a/lib/testutil/v23tests/v23tests.go
+++ b/lib/testutil/v23tests/v23tests.go
@@ -98,21 +98,28 @@
"v.io/core/veyron/security/agent"
)
-// TODO(sjr): make this thread safe.
-//
-// TODO(sjr): document all of the methods.
-//
-// TODO(sjr): need more testing of this core package, especially wrt to
-// process cleanup, making sure debug output is captured correctly, etc.
-//
-// TODO(sjr): provide a utility function to retry an operation for a specific
-// time before failing. Useful for synchronising across process startups etc.
-//
+// TB is a mirror of testing.TB but without the private method
+// preventing its implementation from outside of the go testing package.
+type TB interface {
+ Error(args ...interface{})
+ Errorf(format string, args ...interface{})
+ Fail()
+ FailNow()
+ Failed() bool
+ Fatal(args ...interface{})
+ Fatalf(format string, args ...interface{})
+ Log(args ...interface{})
+ Logf(format string, args ...interface{})
+ Skip(args ...interface{})
+ SkipNow()
+ Skipf(format string, args ...interface{})
+ Skipped() bool
+}
// T represents an integration test environment.
type T struct {
- // The embedded testing.T
- *testing.T
+ // The embedded TB
+ TB
// The function to shutdown the context used to create the environment.
shutdown veyron2.Shutdown
@@ -129,6 +136,7 @@
tempFiles []*os.File
tempDirs []string
cachedBinDir string
+ dirStack []string
invocations []*Invocation
}
@@ -230,10 +238,6 @@
// author to decide whether failure to deliver the signal is fatal to
// the test.
func (i *Invocation) Kill(sig syscall.Signal) error {
- // TODO(sjr): consider using vexec to manage subprocesses.
- // TODO(sjr): if we use vexec, will want to tee stderr reliably to a file
- // as well as a stderr stream, maintain an 'enviroment' here.
- // We'll also want to maintain an environment, as per modules.Shell
pid := i.handle.Pid()
vlog.VI(1).Infof("sending signal %v to PID %d", sig, pid)
return syscall.Kill(pid, sig)
@@ -300,15 +304,18 @@
// Start starts the given binary with the given arguments.
func (b *Binary) Start(args ...string) *Invocation {
- loc := Caller(testutil.DepthToExternalCaller())
- vlog.Infof("%s: starting %s %s", loc, b.Path(), strings.Join(args, " "))
+ return b.start(1, args...)
+}
+
+func (b *Binary) start(skip int, args ...string) *Invocation {
+ vlog.Infof("%s: starting %s %s", Caller(skip+1), b.Path(), strings.Join(args, " "))
handle, err := b.env.shell.StartExternalCommand(b.envVars, append([]string{b.Path()}, args...)...)
if err != nil {
// TODO(cnicolaou): calling Fatalf etc from a goroutine often leads
// to deadlock. Need to make sure that we handle this here. Maybe
// it's best to just return an error? Or provide a StartWithError
// call for use from goroutines.
- b.env.Fatalf("%s: StartExternalCommand(%v, %v) failed: %v", loc, b.Path(), strings.Join(args, ", "), err)
+ b.env.Fatalf("%s: StartExternalCommand(%v, %v) failed: %v", Caller(skip+1), b.Path(), strings.Join(args, ", "), err)
}
vlog.Infof("started PID %d\n", handle.Pid())
inv := &Invocation{
@@ -333,7 +340,7 @@
// therefore this goroutine will exit.
go func() {
if _, err := io.Copy(inv.Stdin(), b.inputReader); err != nil {
- vlog.Infof("%s: Copy failed: %v", loc, err)
+ vlog.Infof("%s: Copy failed: %v", Caller(skip+2), err)
}
inv.CloseStdin()
}()
@@ -341,6 +348,125 @@
return inv
}
+func (b *Binary) run(args ...string) string {
+ inv := b.start(2, args...)
+ var stdout, stderr bytes.Buffer
+ err := inv.Wait(&stdout, &stderr)
+ if err != nil {
+ a := strings.Join(args, ", ")
+ b.env.Fatalf("%s: Run(%s): failed: %v: \n%s\n", Caller(2), a, err, stderr.String())
+ }
+ return strings.TrimRight(stdout.String(), "\n")
+}
+
+// Run runs the binary with the specified arguments to completion. On
+// success it returns the contents of stdout, on failure it terminates the
+// test with an error message containing the error and the contents of
+// stderr.
+func (b *Binary) Run(args ...string) string {
+ return b.run(args...)
+}
+
+// Run constructs a Binary for path and invokes Run on it.
+func (e *T) Run(path string, args ...string) string {
+ return e.BinaryFromPath(path).run(args...)
+}
+
+// WaitFunc is the type of the functions to be used in conjunction
+// with WaitFor and WaitForAsync. It should return a value or an error
+// when it wants those functions to terminate, returning a nil value
+// and nil error will result in it being called again after the specified
+// delay time specified in the calls to WaitFor and WaitForAsync.
+type WaitFunc func() (interface{}, error)
+
+// WaitFor calls fn at least once with the specified delay value
+// between iterations until the first of the following is encountered:
+// 1. fn returns a non-nil value.
+// 2. fn returns an error value
+// 3. fn is executed at least once and the specified timeout is exceeded.
+//
+// WaitFor returns the non-nil value for the first case and calls e.Fatalf for
+// the other two cases.
+// WaitFor will always run fn at least once to completion and hence it will
+// hang if that first iteration of fn hangs. If this behaviour is not
+// appropriate, then WaitForAsync should be used.
+func (e *T) WaitFor(fn WaitFunc, delay, timeout time.Duration) interface{} {
+ deadline := time.Now().Add(timeout)
+ for {
+ val, err := fn()
+ if val != nil {
+ return val
+ }
+ if err != nil {
+ e.Fatalf("%s: the WaitFunc returned an error: %v", Caller(1), err)
+ }
+ if time.Now().After(deadline) {
+ e.Fatalf("%s: timedout after %s", Caller(1), timeout)
+ }
+ time.Sleep(delay)
+ }
+}
+
+// WaitForAsync is like WaitFor except that it calls fn in a goroutine
+// and can timeout during the execution fn.
+func (e *T) WaitForAsync(fn WaitFunc, delay, timeout time.Duration) interface{} {
+ resultCh := make(chan interface{})
+ errCh := make(chan interface{})
+ go func() {
+ for {
+ val, err := fn()
+ if val != nil {
+ resultCh <- val
+ return
+ }
+ if err != nil {
+ errCh <- err
+ return
+ }
+ time.Sleep(delay)
+ }
+ }()
+ select {
+ case err := <-errCh:
+ e.Fatalf("%s: the WaitFunc returned error: %v", Caller(1), err)
+ case result := <-resultCh:
+ return result
+ case <-time.After(timeout):
+ e.Fatalf("%s: timedout after %s", Caller(1), timeout)
+ }
+ return nil
+}
+
+// Pushd pushes the current working directory to the stack of
+// directories, returning it as its result, and changes the working
+// directory to dir.
+func (e *T) Pushd(dir string) string {
+ cwd, err := os.Getwd()
+ if err != nil {
+ e.Fatalf("%s: Getwd failed: %s", Caller(1), err)
+ }
+ if err := os.Chdir(dir); err != nil {
+ e.Fatalf("%s: Chdir(%s) failed: %s", Caller(1), dir, err)
+ }
+ e.dirStack = append(e.dirStack, cwd)
+ return cwd
+}
+
+// Popd pops the most recent entry from the directory stack and changes
+// the working directory to that directory. It returns the new working
+// directory as its result.
+func (e *T) Popd() string {
+ if len(e.dirStack) == 0 {
+ e.Fatalf("%s: directory stack empty", Caller(1))
+ }
+ dir := e.dirStack[len(e.dirStack)-1]
+ e.dirStack = e.dirStack[:len(e.dirStack)-1]
+ if err := os.Chdir(dir); err != nil {
+ e.Fatalf("%s: Chdir(%s) failed: %s", Caller(1), dir, err)
+ }
+ return dir
+}
+
// WithStdin returns a copy of this binary that, when Start is called,
// will read its input from the given reader. Once the reader returns
// EOF, the returned invocation's standard input will be closed (see
@@ -635,7 +761,7 @@
//
// ...
// }
-func New(t *testing.T) *T {
+func New(t TB) *T {
ctx, shutdown := veyron2.Init()
vlog.Infof("creating root principal")
@@ -659,7 +785,7 @@
// directory it points to.
cachedBinDir := os.Getenv("V23_BIN_DIR")
return &T{
- T: t,
+ TB: t,
principal: principal,
builtBinaries: make(map[string]*Binary),
shell: shell,
@@ -720,7 +846,7 @@
// invocations will access this root mount table.
func RunRootMT(i *T, args ...string) (*Binary, *Invocation) {
b := i.BuildGoPkg("v.io/core/veyron/services/mounttable/mounttabled")
- inv := b.Start(args...)
+ inv := b.start(1, args...)
name := inv.ExpectVar("NAME")
inv.Environment().SetVar("NAMESPACE_ROOT", name)
vlog.Infof("Running root mount table: %q", name)
@@ -746,6 +872,3 @@
}
return func() {}
}
-
-// TODO(sjr): provided convenience wrapper for dealing with credentials if
-// necessary.
diff --git a/lib/testutil/v23tests/v23tests_test.go b/lib/testutil/v23tests/v23tests_test.go
index 1ab0f52..b823b3a 100644
--- a/lib/testutil/v23tests/v23tests_test.go
+++ b/lib/testutil/v23tests/v23tests_test.go
@@ -5,6 +5,7 @@
"crypto/sha1"
"fmt"
"io"
+ "os"
"regexp"
"strings"
"syscall"
@@ -178,3 +179,194 @@
}
}
}
+
+func TestDirStack(t *testing.T) {
+ env := v23tests.New(t)
+ defer env.Cleanup()
+
+ home := os.Getenv("HOME")
+ if len(home) == 0 {
+ t.Fatalf("failed to read HOME environment variable")
+ }
+
+ cwd, _ := os.Getwd()
+ if got, want := env.Pushd("/"), cwd; got != want {
+ t.Fatalf("got %v, want %v", got, want)
+ }
+ if got, want := env.Pushd(home), "/"; got != want {
+ t.Fatalf("got %v, want %v", got, want)
+ }
+ tcwd, _ := os.Getwd()
+ if got, want := tcwd, home; got != want {
+ t.Fatalf("got %v, want %v", got, want)
+ }
+ if got, want := env.Popd(), "/"; got != want {
+ t.Fatalf("got %v, want %v", got, want)
+ }
+ if got, want := env.Popd(), cwd; got != want {
+ t.Fatalf("got %v, want %v", got, want)
+ }
+ ncwd, _ := os.Getwd()
+ if got, want := ncwd, cwd; got != want {
+ t.Fatalf("got %v, want %v", got, want)
+ }
+}
+
+func TestRun(t *testing.T) {
+ env := v23tests.New(t)
+ defer env.Cleanup()
+
+ if got, want := env.Run("/bin/echo", "hello world"), "hello world"; got != want {
+ t.Fatalf("got %v, want %v", got, want)
+ }
+
+ echo := env.BinaryFromPath("/bin/echo")
+ if got, want := echo.Run("hello", "world"), "hello world"; got != want {
+ t.Fatalf("got %v, want %v", got, want)
+ }
+}
+
+type mockT struct {
+ msg string
+ failed bool
+}
+
+func (m *mockT) Error(args ...interface{}) {
+ m.msg = fmt.Sprint(args...)
+ m.failed = true
+}
+
+func (m *mockT) Errorf(format string, args ...interface{}) {
+ m.msg = fmt.Sprintf(format, args...)
+ m.failed = true
+}
+
+func (m *mockT) Fail() { panic("Fail") }
+
+func (m *mockT) FailNow() { panic("FailNow") }
+
+func (m *mockT) Failed() bool { return m.failed }
+
+func (m *mockT) Fatal(args ...interface{}) {
+ panic(fmt.Sprint(args...))
+}
+
+func (m *mockT) Fatalf(format string, args ...interface{}) {
+ panic(fmt.Sprintf(format, args...))
+}
+
+func (m *mockT) Log(args ...interface{}) {}
+
+func (m *mockT) Logf(format string, args ...interface{}) {}
+
+func (m *mockT) Skip(args ...interface{}) {}
+
+func (m *mockT) SkipNow() {}
+
+func (m *mockT) Skipf(format string, args ...interface{}) {}
+
+func (m *mockT) Skipped() bool { return false }
+
+func TestRunFailFromPath(t *testing.T) {
+ mock := &mockT{}
+ env := v23tests.New(mock)
+ defer env.Cleanup()
+
+ defer func() {
+ msg := recover().(string)
+ if got, want := msg, "v23tests_test.go:284"; !strings.Contains(got, want) {
+ t.Fatalf("%q does not contain %q", got, want)
+ }
+ if got, want := msg, "fork/exec /bin/echox: no such file or directory"; !strings.Contains(got, want) {
+ t.Fatalf("%q does not contain %q", got, want)
+ }
+ }()
+ env.Run("/bin/echox", "hello world")
+}
+
+func TestRunFail(t *testing.T) {
+ mock := &mockT{}
+ env := v23tests.New(mock)
+ defer env.Cleanup()
+
+ _, inv := v23tests.RunRootMT(env, "--xxveyron.tcp.address=127.0.0.1:0")
+ defer func() {
+ msg := recover().(string)
+ if got, want := msg, "v23tests_test.go:302"; !strings.Contains(got, want) {
+ t.Fatalf("%q does not contain %q", got, want)
+ }
+ if got, want := msg, "exit status 2"; !strings.Contains(got, want) {
+ t.Fatalf("%q does not contain %q", got, want)
+ }
+ }()
+ inv.WaitOrDie(nil, nil)
+}
+
+func TestWaitTimeout(t *testing.T) {
+ env := v23tests.New(&mockT{})
+ defer env.Cleanup()
+
+ iterations := 0
+ sleeper := func() (interface{}, error) {
+ iterations++
+ return nil, nil
+ }
+
+ defer func() {
+ if iterations == 0 {
+ t.Fatalf("our sleeper didn't get to run")
+ }
+ if got, want := recover().(string), "v23tests_test.go:324: timedout"; !strings.Contains(got, want) {
+ t.Fatalf("%q does not contain %q", got, want)
+ }
+ }()
+
+ env.WaitFor(sleeper, time.Millisecond, 50*time.Millisecond)
+
+}
+
+func TestWaitAsyncTimeout(t *testing.T) {
+ env := v23tests.New(&mockT{})
+ defer env.Cleanup()
+
+ iterations := 0
+ sleeper := func() (interface{}, error) {
+ time.Sleep(time.Minute)
+ iterations++
+ return nil, nil
+ }
+
+ defer func() {
+ if iterations != 0 {
+ t.Fatalf("our sleeper got to run")
+ }
+ if got, want := recover().(string), "v23tests_test.go:348: timedout"; !strings.Contains(got, want) {
+ t.Fatalf("%q does not contain %q", got, want)
+ }
+ }()
+
+ env.WaitForAsync(sleeper, time.Millisecond, 50*time.Millisecond)
+}
+
+func TestWaitFor(t *testing.T) {
+ env := v23tests.New(t)
+ defer env.Cleanup()
+ iterations := 0
+ countIn5s := func() (interface{}, error) {
+ iterations++
+ if iterations%5 == 0 {
+ return iterations, nil
+ }
+ return nil, nil
+ }
+
+ r := env.WaitFor(countIn5s, time.Millisecond, 50*time.Millisecond)
+ if got, want := r.(int), 5; got != want {
+ env.Fatalf("got %d, want %d", got, want)
+ }
+
+ r = env.WaitForAsync(countIn5s, time.Millisecond, 50*time.Millisecond)
+ if got, want := r.(int), 10; got != want {
+ env.Fatalf("got %d, want %d", got, want)
+ }
+}