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)