lib/testutil/integration: a first pass at implementing IO redirection
Change-Id: I180d0208469c3336a11d0499d595f5b2b90a1060
diff --git a/lib/testutil/v23tests/v23tests.go b/lib/testutil/v23tests/v23tests.go
index b85afb6..947cc49 100644
--- a/lib/testutil/v23tests/v23tests.go
+++ b/lib/testutil/v23tests/v23tests.go
@@ -107,13 +107,6 @@
//
// TODO(sjr): document all of the methods.
//
-// TODO(sjr): we need I/O redirection and piping. This is one of the
-// conveniences of the shell based tests that we've lost here. There's
-// current no way to redirect I/O and it won't always be possible
-// to change the command to take a command line arg. Similarly,
-// we should provide support for piping output from one command to
-// to another, or for creating process pipelines directly.
-//
// TODO(sjr): need more testing of this core package, especially wrt to
// process cleanup, making sure debug output is captured correctly, etc.
//
@@ -196,9 +189,15 @@
// Path returns the path to the binary.
Path() string
- // Returns a copy of this binary that, when Start is called, will use
- // the given environment variables.
+ // WithEnv returns a copy of this binary that, when Start is called,
+ // will use the given environment variables.
WithEnv(env []string) TestBinary
+
+ // 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
+ // Invocation.CloseStdin).
+ WithStdin(r io.Reader) TestBinary
}
// Session mirrors veyron/lib/expect is used to allow us to embed all of
@@ -229,6 +228,11 @@
Session
Stdin() io.Writer
+
+ // CloseStdin closes the write-side of the pipe to the invocation's
+ // standard input.
+ CloseStdin()
+
Stdout() io.Reader
// Output reads the invocation's stdout until EOF and then returns what
@@ -299,6 +303,9 @@
// The cleanup function to run when the binary exits.
cleanupFunc func()
+
+ // The reader who is supplying the bytes we're going to send to our stdin.
+ inputReader io.Reader
}
type testBinaryInvocation struct {
@@ -337,6 +344,10 @@
return i.handle.Stdin()
}
+func (i *testBinaryInvocation) CloseStdin() {
+ i.handle.CloseStdin()
+}
+
func (i *testBinaryInvocation) Stdout() io.Reader {
return i.handle.Stdout()
}
@@ -418,9 +429,33 @@
Session: expect.NewSession(b.env, handle.Stdout(), 5*time.Minute),
}
b.env.appendInvocation(inv)
+ if b.inputReader != nil {
+ // This goroutine makes a best-effort attempt to copy bytes
+ // from b.inputReader to inv.Stdin() using io.Copy. When Copy
+ // returns (successfully or not), inv.CloseStdin() is called.
+ // This is always safe, even if inv has been shutdown.
+ //
+ // This goroutine's lifespan will be limited to that of the
+ // environment to which 'inv' is attached. This is because the
+ // environment will take care to kill all remaining invocations
+ // upon Cleanup, which will in turn cause Copy to fail and
+ // therefore this goroutine will exit.
+ go func() {
+ if _, err := io.Copy(inv.Stdin(), b.inputReader); err != nil {
+ vlog.Infof("Copy failed: %v", err)
+ }
+ inv.CloseStdin()
+ }()
+ }
return inv
}
+func (b *testBinary) WithStdin(r io.Reader) TestBinary {
+ newBin := *b
+ newBin.inputReader = r
+ return &newBin
+}
+
func (b *testBinary) WithEnv(env []string) TestBinary {
newBin := *b
newBin.envVars = env
diff --git a/lib/testutil/v23tests/v23tests_test.go b/lib/testutil/v23tests/v23tests_test.go
index 8c0a3d8..8b58360 100644
--- a/lib/testutil/v23tests/v23tests_test.go
+++ b/lib/testutil/v23tests/v23tests_test.go
@@ -2,6 +2,7 @@
import (
"bytes"
+ "crypto/sha1"
"fmt"
"io"
"regexp"
@@ -137,3 +138,43 @@
vlog.Infof("Child\n=============\n%s", stderr.String())
vlog.Infof("-----------------")
}
+
+func TestInputRedirection(t *testing.T) {
+ env := v23tests.New(t)
+ defer env.Cleanup()
+
+ echo := env.BinaryFromPath("/bin/echo")
+ cat := env.BinaryFromPath("/bin/cat")
+
+ if want, got := "Hello, world!\n", cat.WithStdin(echo.Start("Hello, world!").Stdout()).Start().Output(); want != got {
+ t.Fatalf("unexpected output, got %s, want %s", got, want)
+ }
+
+ // Read something from a file.
+ {
+ want := "Hello from a file!"
+ f := env.TempFile()
+ f.WriteString(want)
+ f.Seek(0, 0)
+ if got := cat.WithStdin(f).Start().Output(); want != got {
+ t.Fatalf("unexpected output, got %s, want %s", got, want)
+ }
+ }
+
+ // Try it again with 1Mb.
+ {
+ want := testutil.RandomBytes(1 << 20)
+ expectedSum := sha1.Sum(want)
+ f := env.TempFile()
+ f.Write(want)
+ f.Seek(0, 0)
+ got := cat.WithStdin(f).Start().Output()
+ if len(got) != len(want) {
+ t.Fatalf("length mismatch, got %d but wanted %d", len(want), len(got))
+ }
+ actualSum := sha1.Sum([]byte(got))
+ if actualSum != expectedSum {
+ t.Fatalf("SHA-1 mismatch, got %x but wanted %x", actualSum, expectedSum)
+ }
+ }
+}
diff --git a/tools/naming/simulator/simulator_test.go b/tools/naming/simulator/simulator_test.go
index 0e01268..2e92777 100644
--- a/tools/naming/simulator/simulator_test.go
+++ b/tools/naming/simulator/simulator_test.go
@@ -35,7 +35,11 @@
if testing.Verbose() {
fmt.Fprintf(os.Stderr, "Script %v\n", script)
}
- invocation := binary.Start("--file", script)
+ scriptFile, err := os.Open(script)
+ if err != nil {
+ t.Fatalf("Open(%q) failed: %v", script, err)
+ }
+ invocation := binary.WithStdin(scriptFile).Start()
var stdout, stderr bytes.Buffer
if err := invocation.Wait(&stdout, &stderr); err != nil {
fmt.Fprintf(os.Stderr, "Script %v failed\n", script)