Merge "v.io/core: another step in eliminating verror1, and making verror2.E be a struct"
diff --git a/lib/testutil/init.go b/lib/testutil/init.go
index d451c27..9dd4630 100644
--- a/lib/testutil/init.go
+++ b/lib/testutil/init.go
@@ -56,12 +56,15 @@
 
 var Rand *Random
 var once sync.Once
-var RunIntegrationTests bool
+var IntegrationTestsEnabled bool
+var IntegrationTestsDebugShellOnError bool
 
 const IntegrationTestsFlag = "v23.tests"
+const IntegrationTestsDebugShellOnErrorFlag = "v23.tests.shell-on-fail"
 
 func init() {
-	flag.BoolVar(&RunIntegrationTests, IntegrationTestsFlag, false, "Run integration tests.")
+	flag.BoolVar(&IntegrationTestsEnabled, IntegrationTestsFlag, false, "Run integration tests.")
+	flag.BoolVar(&IntegrationTestsDebugShellOnError, IntegrationTestsDebugShellOnErrorFlag, false, "Drop into a debug shell if an integration test fails.")
 }
 
 // Init sets up state for running tests: Adjusting GOMAXPROCS,
diff --git a/lib/testutil/integration/testdata/integration_test.go b/lib/testutil/integration/testdata/integration_test.go
deleted file mode 100644
index eadb7be..0000000
--- a/lib/testutil/integration/testdata/integration_test.go
+++ /dev/null
@@ -1,32 +0,0 @@
-package testdata
-
-import (
-	"bufio"
-	"bytes"
-	"testing"
-
-	"v.io/core/veyron/lib/modules"
-	"v.io/core/veyron/lib/testutil/integration"
-	_ "v.io/core/veyron/profiles"
-)
-
-func TestHelperProcess(t *testing.T) {
-	modules.DispatchInTest()
-}
-
-func TestBinaryFromPath(t *testing.T) {
-	env := integration.New(t)
-	defer env.Cleanup()
-
-	bash := env.BinaryFromPath("/bin/bash")
-	if want, got := "hello world\n", bash.Start("-c", "echo hello world").Output(); want != got {
-		t.Fatalf("unexpected output, want %s, got %s", want, got)
-	}
-
-	inv := bash.Start("-c", "echo hello world 1>&2")
-	var buf bytes.Buffer
-	inv.WaitOrDie(nil, bufio.NewWriter(&buf))
-	if want, got := "hello world\n", string(buf.Bytes()); want != got {
-		t.Fatalf("unexpected output, want %s, got %s", want, got)
-	}
-}
diff --git a/lib/testutil/integration/util.go b/lib/testutil/integration/util.go
deleted file mode 100644
index 906dc3e..0000000
--- a/lib/testutil/integration/util.go
+++ /dev/null
@@ -1,546 +0,0 @@
-// This package provides support for writing end-to-end style tests. The
-// TestEnvironment type is the root of the API, you can use this type to set up
-// your test environment and to perform operations within the environment. To
-// create a new test environment, use the NewTestEnvironment method, e.g.
-//
-//   func TestFoo(t *testing.T) {
-//     env := integration.NewTestEnvironment(t)
-//     defer env.Cleanup()
-//
-//     ...
-//   }
-//
-// The methods in this API typically do not return error in the case of
-// failure. Instead, the current test will fail with an appropriate error
-// message. This alleviates the need to handle errors in the test itself.
-//
-// End-to-end style tests may involve several communicating processes. These
-// kinds of tests can be hard to debug using Go alone. The TestEnvironment
-// interface provides a DebugShell() to assist in test debugging. This method
-// will pause the current test and spawn a new shell that can be used to
-// manually inspect and interact with the test environment.
-package integration
-
-import (
-	"bytes"
-	"fmt"
-	"io"
-	"io/ioutil"
-	"os"
-	"os/exec"
-	"path"
-	"path/filepath"
-	"runtime"
-	"strings"
-	"syscall"
-	"time"
-
-	"v.io/core/veyron/lib/expect"
-	"v.io/core/veyron/lib/modules"
-	"v.io/core/veyron/lib/modules/core"
-	"v.io/core/veyron/lib/testutil"
-
-	tsecurity "v.io/core/veyron/lib/testutil/security"
-	"v.io/core/veyron2"
-	"v.io/core/veyron2/security"
-)
-
-// Test represents the currently running test. In a local end-to-end test
-// environment obtained though New, this interface will be
-// implemented by Go's standard testing.T.
-//
-// We are planning to implement a regression testing environment that does not
-// depend on Go's testing framework. In this case, users of this interface will
-// ideally not have to change their code to run on the new environment.
-type Test 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 a test environment. Typically, an end-to-end
-// test will begin with:
-//   func TestFoo(t *testing.T) {
-//     env := integration.New(t)
-//     defer env.Cleanup()
-//
-//     ...
-//   }
-type T interface {
-	Test
-
-	// Cleanup cleans up the environment and deletes all its artifacts.
-	Cleanup()
-
-	// BinaryFromPath returns a new TestBinary that, when started, will
-	// execute the executable or script at the given path.
-	//
-	// E.g. env.BinaryFromPath("/bin/bash").Start("-c", "echo hello world").Output() -> "hello world"
-	BinaryFromPath(path string) TestBinary
-
-	// BuildGoPkg expects a Go package path that identifies a "main"
-	// package and returns a TestBinary representing the newly built
-	// binary.
-	BuildGoPkg(path string) TestBinary
-
-	// RootMT returns the endpoint to the root mounttable for this test
-	// environment.
-	RootMT() string
-
-	// Principal returns the security principal of this environment.
-	Principal() security.Principal
-
-	// DebugShell drops the user into a debug shell. If there is no
-	// controlling TTY, DebugShell will emit a warning message and take no
-	// futher action.
-	DebugShell()
-
-	// TempFile creates a temporary file. Temporary files will be deleted
-	// by Cleanup.
-	TempFile() *os.File
-
-	// TempDir creates a temporary directory. Temporary directories and
-	// their contents will be deleted by Cleanup.
-	TempDir() string
-}
-
-type TestBinary interface {
-	// Start starts the given binary with the given arguments.
-	Start(args ...string) Invocation
-
-	// 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(env []string) TestBinary
-}
-
-// Invocation represents a single invocation of a TestBinary.
-//
-// Any bytes written by the invocation to its standard error may be recovered
-// using the Wait or WaitOrDie functions.
-//
-// For example:
-//   bin := env.BinaryFromPath("/bin/bash")
-//   inv := bin.Start("-c", "echo hello world 1>&2")
-//   var stderr bytes.Buffer
-//   inv.WaitOrDie(nil, &stderr)
-//   // stderr.Bytes() now contains 'hello world\n'
-type Invocation interface {
-	Stdin() io.Writer
-	Stdout() io.Reader
-
-	// Output reads the invocation's stdout until EOF and then returns what
-	// was read as a string.
-	Output() string
-
-	// Sends the given signal to this invocation. It is up to the test
-	// author to decide whether failure to deliver the signal is fatal to
-	// the test.
-	Kill(syscall.Signal) error
-
-	// Wait waits for this invocation to finish. If either stdout or stderr
-	// is non-nil, any remaining unread output from those sources will be
-	// written to the corresponding writer. The returned error represents
-	// the exit status of the underlying command.
-	Wait(stdout, stderr io.Writer) error
-
-	// Wait waits for this invocation to finish. If either stdout or stderr
-	// is non-nil, any remaining unread output from those sources will be
-	// written to the corresponding writer. If the underlying command
-	// exited with anything but success (exit status 0), this function will
-	// cause the current test to fail.
-	WaitOrDie(stdout, stderr io.Writer)
-}
-
-type integrationTestEnvironment struct {
-	// The testing framework.
-	Test
-
-	// The function to shutdown the context used to create the environment.
-	shutdown veyron2.Shutdown
-
-	// The shell to use to start commands.
-	shell *modules.Shell
-
-	// The environment's root security principal.
-	principal security.Principal
-
-	// Maps path to TestBinary.
-	builtBinaries map[string]*integrationTestBinary
-
-	mtHandle   modules.Handle
-	mtEndpoint string
-
-	tempFiles []*os.File
-	tempDirs  []string
-}
-
-type integrationTestBinary struct {
-	// The environment to which this binary belongs.
-	env *integrationTestEnvironment
-
-	// The path to the binary.
-	path string
-
-	// Environment variables that will be used when creating invocations
-	// via Start.
-	envVars []string
-
-	// The cleanup function to run when the binary exits.
-	cleanupFunc func()
-}
-
-type integrationTestBinaryInvocation struct {
-	// The environment to which this invocation belongs.
-	env *integrationTestEnvironment
-
-	// The handle to the process that was run when this invocation was started.
-	handle *modules.Handle
-}
-
-func (i *integrationTestBinaryInvocation) Stdin() io.Writer {
-	return (*i.handle).Stdin()
-}
-
-func (i *integrationTestBinaryInvocation) Stdout() io.Reader {
-	return (*i.handle).Stdout()
-}
-
-func (i *integrationTestBinaryInvocation) Kill(sig syscall.Signal) error {
-	// TODO(sjr): consider using vexec to manage subprocesses.
-	pid := (*i.handle).Pid()
-	i.env.Logf("sending signal %v to PID %d", sig, pid)
-	err := syscall.Kill(pid, sig)
-	(*i.handle).Shutdown(nil, nil)
-	return err
-}
-
-func readerToString(t Test, r io.Reader) string {
-	buf := bytes.Buffer{}
-	_, err := buf.ReadFrom(r)
-	if err != nil {
-		t.Fatalf("ReadFrom() failed: %v", err)
-	}
-	return buf.String()
-}
-
-func (i *integrationTestBinaryInvocation) Output() string {
-	return readerToString(i.env, i.Stdout())
-}
-
-func (i *integrationTestBinaryInvocation) Wait(stdout, stderr io.Writer) error {
-	return (*i.handle).Shutdown(stdout, stderr)
-}
-
-func (i *integrationTestBinaryInvocation) WaitOrDie(stdout, stderr io.Writer) {
-	if err := i.Wait(stdout, stderr); err != nil {
-		i.env.Fatalf("FATAL: Wait() for pid %d failed: %v", (*i.handle).Pid(), err)
-	}
-}
-
-func (b *integrationTestBinary) cleanup() {
-	binaryDir := path.Dir(b.path)
-	b.env.Logf("cleaning up %s", binaryDir)
-	if err := os.RemoveAll(binaryDir); err != nil {
-		b.env.Logf("WARNING: RemoveAll(%s) failed (%v)", binaryDir, err)
-	}
-}
-
-func (b *integrationTestBinary) Path() string {
-	return b.path
-}
-
-func (b *integrationTestBinary) Start(args ...string) Invocation {
-	locationString := ""
-	if _, file, line, ok := runtime.Caller(1); ok {
-		locationString = fmt.Sprintf("(requested at %s:%d) ", filepath.Base(file), line)
-	}
-	b.env.Logf("%sstarting %s %s", locationString, b.Path(), strings.Join(args, " "))
-	handle, err := b.env.shell.StartExternalCommand(b.envVars, append([]string{b.Path()}, args...)...)
-	if err != nil {
-		b.env.Fatalf("StartExternalCommand(%v, %v) failed: %v", b.Path(), strings.Join(args, ", "), err)
-	}
-	b.env.Logf("started PID %d\n", handle.Pid())
-	return &integrationTestBinaryInvocation{
-		env:    b.env,
-		handle: &handle,
-	}
-}
-
-func (b *integrationTestBinary) WithEnv(env []string) TestBinary {
-	newBin := *b
-	newBin.envVars = env
-	return &newBin
-}
-
-func (e *integrationTestEnvironment) RootMT() string {
-	return e.mtEndpoint
-}
-
-func (e *integrationTestEnvironment) Principal() security.Principal {
-	return e.principal
-}
-
-func (e *integrationTestEnvironment) Cleanup() {
-	for _, binary := range e.builtBinaries {
-		binary.cleanupFunc()
-	}
-
-	for _, tempFile := range e.tempFiles {
-		e.Logf("cleaning up %s", tempFile.Name())
-		if err := tempFile.Close(); err != nil {
-			e.Logf("WARNING: Close(%q) failed: %v", tempFile.Name(), err)
-		}
-		if err := os.RemoveAll(tempFile.Name()); err != nil {
-			e.Logf("WARNING: RemoveAll(%q) failed: %v", tempFile.Name(), err)
-		}
-	}
-
-	for _, tempDir := range e.tempDirs {
-		e.Logf("cleaning up %s", tempDir)
-		if err := os.RemoveAll(tempDir); err != nil {
-			e.Logf("WARNING: RemoveAll(%q) failed: %v", tempDir, err)
-		}
-	}
-
-	if err := e.shell.Cleanup(os.Stdout, os.Stderr); err != nil {
-		e.Fatalf("WARNING: could not clean up shell (%v)", err)
-	}
-	e.mtHandle.Shutdown(os.Stdout, os.Stderr)
-	e.shutdown()
-}
-
-func writeStringOrDie(t Test, f *os.File, s string) {
-	if _, err := f.WriteString(s); err != nil {
-		t.Fatalf("Write() failed: %v", err)
-	}
-}
-
-func (e *integrationTestEnvironment) DebugShell() {
-	// Get the current working directory.
-	cwd, err := os.Getwd()
-	if err != nil {
-		e.Fatalf("Getwd() failed: %v", err)
-	}
-
-	// Transfer stdin, stdout, and stderr to the new process
-	// and also set target directory for the shell to start in.
-	dev := "/dev/tty"
-	fd, err := syscall.Open(dev, syscall.O_RDWR, 0)
-	if err != nil {
-		e.Logf("WARNING: Open(%v) failed, was asked to create a debug shell but cannot: %v", dev, err)
-		return
-	}
-	file := os.NewFile(uintptr(fd), dev)
-	attr := os.ProcAttr{
-		Files: []*os.File{file, file, file},
-		Dir:   cwd,
-	}
-
-	// Start up a new shell.
-	writeStringOrDie(e, file, ">> Starting a new interactive shell\n")
-	writeStringOrDie(e, file, "Hit CTRL-D to resume the test\n")
-	if len(e.builtBinaries) > 0 {
-		writeStringOrDie(e, file, "Built binaries:\n")
-		for _, value := range e.builtBinaries {
-			writeStringOrDie(e, file, "\t"+value.Path()+"\n")
-		}
-	}
-	writeStringOrDie(e, file, fmt.Sprintf("Root mounttable endpoint: %s\n", e.RootMT()))
-
-	shellPath := "/bin/sh"
-	if shellPathFromEnv := os.Getenv("SHELL"); shellPathFromEnv != "" {
-		shellPath = shellPathFromEnv
-	}
-	proc, err := os.StartProcess(shellPath, []string{}, &attr)
-	if err != nil {
-		e.Fatalf("StartProcess(%q) failed: %v", shellPath, err)
-	}
-
-	// Wait until user exits the shell
-	state, err := proc.Wait()
-	if err != nil {
-		e.Fatalf("Wait(%v) failed: %v", shellPath, err)
-	}
-
-	writeStringOrDie(e, file, fmt.Sprintf("<< Exited shell: %s\n", state.String()))
-}
-
-func (e *integrationTestEnvironment) BinaryFromPath(path string) TestBinary {
-	return &integrationTestBinary{
-		env:         e,
-		envVars:     nil,
-		path:        path,
-		cleanupFunc: func() {},
-	}
-}
-
-func (e *integrationTestEnvironment) BuildGoPkg(binary_path string) TestBinary {
-	e.Logf("building %s...", binary_path)
-	if cached_binary := e.builtBinaries[binary_path]; cached_binary != nil {
-		e.Logf("using cached binary for %s at %s.", binary_path, cached_binary.Path())
-		return cached_binary
-	}
-	built_path, cleanup, err := buildPkg(binary_path)
-	if err != nil {
-		e.Fatalf("buildPkg() failed: %v", err)
-		return nil
-	}
-	output_path := path.Join(built_path, path.Base(binary_path))
-	e.Logf("done building %s, written to %s.", binary_path, output_path)
-	binary := &integrationTestBinary{
-		env:         e,
-		envVars:     nil,
-		path:        output_path,
-		cleanupFunc: cleanup,
-	}
-	e.builtBinaries[binary_path] = binary
-	return binary
-}
-
-func (e *integrationTestEnvironment) TempFile() *os.File {
-	f, err := ioutil.TempFile("", "")
-	if err != nil {
-		e.Fatalf("TempFile() failed: %v", err)
-	}
-	e.Logf("created temporary file at %s", f.Name())
-	e.tempFiles = append(e.tempFiles, f)
-	return f
-}
-
-func (e *integrationTestEnvironment) TempDir() string {
-	f, err := ioutil.TempDir("", "")
-	if err != nil {
-		e.Fatalf("TempDir() failed: %v", err)
-	}
-	e.Logf("created temporary directory at %s", f)
-	e.tempDirs = append(e.tempDirs, f)
-	return f
-}
-
-// Creates a new local testing environment. A local testing environment has a
-// root mounttable endpoint at RootMT() and a security principle available via
-// Principal().
-//
-// You should clean up the returned environment using the env.Cleanup() method.
-// A typical end-to-end test will begin like:
-//
-//   func TestFoo(t *testing.T) {
-//     env := integration.NewTestEnvironment(t)
-//     defer env.Cleanup()
-//
-//     ...
-//   }
-func New(t Test) T {
-	ctx, shutdown := veyron2.Init()
-
-	t.Log("creating root principal")
-	principal := tsecurity.NewPrincipal("root")
-	ctx, err := veyron2.SetPrincipal(ctx, principal)
-	if err != nil {
-		t.Fatalf("failed to set principal: %v", err)
-	}
-
-	shell, err := modules.NewShell(ctx, principal)
-	if err != nil {
-		t.Fatalf("NewShell() failed: %v", err)
-	}
-	shell.SetWaitTimeout(5 * time.Minute)
-	t.Log("starting root mounttable...")
-	mtHandle, mtEndpoint, err := startRootMT(shell)
-	if err != nil {
-		t.Fatalf("startRootMT() failed: %v", err)
-	}
-	shell.Forget(mtHandle)
-	t.Logf("mounttable available at %s", mtEndpoint)
-
-	return &integrationTestEnvironment{
-		Test:          t,
-		principal:     principal,
-		builtBinaries: make(map[string]*integrationTestBinary),
-		shell:         shell,
-		mtHandle:      mtHandle,
-		mtEndpoint:    mtEndpoint,
-		tempFiles:     []*os.File{},
-		tempDirs:      []string{},
-		shutdown:      shutdown,
-	}
-}
-
-// RunTest runs a single Vanadium integration test.
-func RunTest(t Test, fn func(i T)) {
-	if !testutil.RunIntegrationTests {
-		t.Skip()
-	}
-	i := New(t)
-	fn(i)
-	i.Cleanup()
-}
-
-// BuildPkg returns a path to a directory that contains the built binary for
-// the given packages and a function that should be invoked to clean up the
-// build artifacts. Note that the clients of this function should not modify
-// the contents of this directory directly and instead defer to the cleanup
-// function.
-func buildPkg(pkg string) (string, func(), error) {
-	// The VEYRON_INTEGRATION_BIN_DIR environment variable can be
-	// used to identify a directory that multiple integration
-	// tests can use to share binaries. Whoever sets this
-	// environment variable is responsible for cleaning up the
-	// directory it points to.
-	binDir, cleanupFn := os.Getenv("VEYRON_INTEGRATION_BIN_DIR"), func() {}
-	if binDir == "" {
-		// If the aforementioned environment variable is not
-		// set, the given packages are built in a temporary
-		// directory, which the cleanup function removes.
-		tmpDir, err := ioutil.TempDir("", "")
-		if err != nil {
-			return "", nil, fmt.Errorf("TempDir() failed: %v", err)
-		}
-		binDir, cleanupFn = tmpDir, func() { os.RemoveAll(tmpDir) }
-	}
-	binFile := filepath.Join(binDir, path.Base(pkg))
-	if _, err := os.Stat(binFile); err != nil {
-		if !os.IsNotExist(err) {
-			return "", nil, err
-		}
-		cmd := exec.Command("v23", "go", "build", "-o", filepath.Join(binDir, path.Base(pkg)), pkg)
-		if err := cmd.Run(); err != nil {
-			return "", nil, err
-		}
-	}
-	return binDir, cleanupFn, nil
-}
-
-// startRootMT uses the given shell to start a root mount table and
-// returns a handle for the started command along with the object name
-// of the mount table.
-func startRootMT(shell *modules.Shell) (modules.Handle, string, error) {
-	handle, err := shell.Start(core.RootMTCommand, nil, "--veyron.tcp.address=127.0.0.1:0")
-	if err != nil {
-		return nil, "", err
-	}
-	s := expect.NewSession(nil, handle.Stdout(), 10*time.Second)
-	s.ExpectVar("PID")
-	if err := s.Error(); err != nil {
-		return nil, "", err
-	}
-	name := s.ExpectVar("MT_NAME")
-	if err := s.Error(); err != nil {
-		return nil, "", err
-	}
-
-	return handle, name, nil
-}
diff --git a/lib/testutil/v23tests/v23tests.go b/lib/testutil/v23tests/v23tests.go
new file mode 100644
index 0000000..b85afb6
--- /dev/null
+++ b/lib/testutil/v23tests/v23tests.go
@@ -0,0 +1,736 @@
+// Package v23tests provides support for writing end-to-end style integration
+// tests. In particular, support is provided for building binaries, running
+// processes, making assertions about their output/state and ensuring that
+// no processes or files are left behind on exit. Since such tests are often
+// difficult to debug facilities are provided to help do so.
+//
+// The preferred usage of this integration test framework is via the v23
+// tool which generates supporting code. The primary reason for doing so is
+// to cleanly separate integration tests, which can be very expensive to run,
+// from normal unit tests which are intended to be fast and used constantly.
+// However, it still beneficial to be able to always compile the integration
+// test code with the normal test code, just not to run it. Similarly, it
+// is beneficial to share as much of the existing go test infrastructure as
+// possible, so the generated code uses a flag and a naming convention to
+// separate the tests. Integration tests may be run in addition to unit tests
+// by supplying the --v23.tests flag; the -run flag can be used
+// to avoid running unit tests by specifying a prefix of TestV23 since
+// the generate test functions always. Thus:
+//
+// v23 go test -v <pkgs> --v23.test  // runs both unit and integration tests
+// v23 go test -v -run=TestV23 <pkgs> --v23.test // runs just integration tests
+//
+// The go generate mechanism is used to generate the test code, thus the
+// comment:
+//
+// //go:generate v23 integration generate
+//
+// will generate the files v23_test.go and internal_v23_test.go for the
+// package in which it occurs. Run v23 integration generate help for full
+// details and options. In short, any function in an external
+// (i.e. <pgk>_test) test package of the following form:
+//
+// V23Test<x>(t integration.T)
+//
+// will be invoked as integration test if the --v23.tests flag is used.
+//
+// The generated code makes use of the RunTest function, documented below.
+//
+// The test environment is implemented by an instance of the interface T.
+// It is constructed with an instance of another interface Test, which is
+// generally implemented by testing.T. Thus, the integration test environment
+// directly as follows:
+//
+//   func TestFoo(t *testing.T) {
+//     env := v23tests.New(t)
+//     defer env.Cleanup()
+//
+//     ...
+//   }
+//
+// The methods in this API typically do not return error in the case of
+// failure. Instead, the current test will fail with an appropriate error
+// message. This avoids the need to handle errors inline the test itself.
+//
+// The test environment manages all built packages, subprocesses and a
+// set of environment variables that are passed to subprocesses.
+//
+// Debugging is supported as follows:
+// 1. The DebugShell method creates an interative shell at that point in
+//    the tests execution that has access to all of the running processes
+//    and environment of those processes. The developer can interact with
+//    those processes to determine the state of the test.
+// 2. Calls to methods on Test (e.g. FailNow, Fatalf) that fail the test
+//    cause the Cleanup method to print out the status of all invocations.
+// 3. Similarly, if the --v23.tests.shell-on-error flag is set then the
+//    cleanup method will invoke a DebugShell on a test failure allowing
+//    the developer to inspect the state of the test.
+// 4. The implementation of this package uses filenames that start with v23test
+//    to allow for easy tracing with --vmodule=v23test*=2 for example.
+//
+package v23tests
+
+import (
+	"bytes"
+	"container/list"
+	"errors"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"os"
+	"os/exec"
+	"path"
+	"path/filepath"
+	"strings"
+	"syscall"
+	"time"
+
+	"v.io/core/veyron2"
+	"v.io/core/veyron2/security"
+	"v.io/core/veyron2/vlog"
+
+	"v.io/core/veyron/lib/expect"
+	"v.io/core/veyron/lib/modules"
+	"v.io/core/veyron/lib/testutil"
+	tsecurity "v.io/core/veyron/lib/testutil/security"
+)
+
+// TODO(cnicolaou): need to enable running under the agent as per the old shell tests,
+// via the shell_test::enable_agent "$@" mechanism.
+//
+//
+// TODO(sjr,cnicolaou): caching of binaries is per test environment -
+// it should be in a file system somewhere and should handle all tests run
+// from a single invocation of v23.
+//
+// TODO(sjr): make this thread safe.
+//
+// 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.
+//
+// TODO(sjr): provide a utility function to retry an operation for a specific
+// time before failing. Useful for synchronising across process startups etc.
+//
+// Test represents the currently running test. In a local end-to-end test
+// environment obtained though New, this interface will be
+// typically be implemented by Go's standard testing.T.
+type Test 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 interface {
+	Test
+
+	// Cleanup cleans up the environment, deletes all its artifacts and
+	// kills all subprocesses. It will kill subprocesses in LIFO order.
+	// Cleanup checks to see if the test has failed and logs information
+	// as to the state of the processes it was asked to invoke up to that
+	// point and optionally, if the --v23.tests.shell-on-fail flag is set
+	// then it will run a debug shell before cleaning up its state.
+	Cleanup()
+
+	// BinaryFromPath returns a new TestBinary that, when started, will
+	// execute the executable or script at the given path.
+	//
+	// E.g. env.BinaryFromPath("/bin/bash").Start("-c", "echo hello world").Output() -> "hello world"
+	BinaryFromPath(path string) TestBinary
+
+	// BuildGoPkg expects a Go package path that identifies a "main"
+	// package and returns a TestBinary representing the newly built
+	// binary.
+	BuildGoPkg(path string) TestBinary
+
+	// Principal returns the security principal of this environment.
+	Principal() security.Principal
+
+	// DebugShell drops the user into a debug shell. If there is no
+	// controlling TTY, DebugShell will emit a warning message and take no
+	// futher action.
+	DebugShell()
+
+	// TempFile creates a temporary file. Temporary files will be deleted
+	// by Cleanup.
+	TempFile() *os.File
+
+	// TempDir creates a temporary directory. Temporary directories and
+	// their contents will be deleted by Cleanup.
+	TempDir() string
+
+	// SetVar sets the value to be associated with key.
+	SetVar(key, value string)
+
+	// GetVar returns the variable associated with the specified key
+	// and an indication of whether it is defined or not.
+	GetVar(key string) (string, bool)
+
+	// ClearVar removes the speficied variable from the Shell's environment
+	ClearVar(key string)
+}
+
+type TestBinary interface {
+	// Start starts the given binary with the given arguments.
+	Start(args ...string) Invocation
+
+	// 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(env []string) TestBinary
+}
+
+// Session mirrors veyron/lib/expect is used to allow us to embed all of
+// expect.Session's methods in Invocation below.
+type Session interface {
+	Expect(expected string)
+	Expectf(format string, args ...interface{})
+	ExpectRE(pattern string, n int) [][]string
+	ExpectVar(name string) string
+	ExpectSetRE(expected ...string) [][]string
+	ExpectSetEventuallyRE(expected ...string) [][]string
+	ReadLine() string
+	ExpectEOF() error
+}
+
+// Invocation represents a single invocation of a TestBinary.
+//
+// Any bytes written by the invocation to its standard error may be recovered
+// using the Wait or WaitOrDie functions.
+//
+// For example:
+//   bin := env.BinaryFromPath("/bin/bash")
+//   inv := bin.Start("-c", "echo hello world 1>&2")
+//   var stderr bytes.Buffer
+//   inv.WaitOrDie(nil, &stderr)
+//   // stderr.Bytes() now contains 'hello world\n'
+type Invocation interface {
+	Session
+
+	Stdin() io.Writer
+	Stdout() io.Reader
+
+	// Output reads the invocation's stdout until EOF and then returns what
+	// was read as a string.
+	Output() string
+
+	// Sends the given signal to this invocation. It is up to the test
+	// author to decide whether failure to deliver the signal is fatal to
+	// the test.
+	Kill(syscall.Signal) error
+
+	// Exists returns true if the invocation still exists.
+	Exists() bool
+
+	// Wait waits for this invocation to finish. If either stdout or stderr
+	// is non-nil, any remaining unread output from those sources will be
+	// written to the corresponding writer. The returned error represents
+	// the exit status of the underlying command.
+	Wait(stdout, stderr io.Writer) error
+
+	// Wait waits for this invocation to finish. If either stdout or stderr
+	// is non-nil, any remaining unread output from those sources will be
+	// written to the corresponding writer. If the underlying command
+	// exited with anything but success (exit status 0), this function will
+	// cause the current test to fail.
+	WaitOrDie(stdout, stderr io.Writer)
+
+	// Environment returns the instance of the test environment that this
+	// invocation was from.
+	Environment() T
+
+	// Path returns the path to the binary that was used for this invocation.
+	Path() string
+}
+
+type testEnvironment struct {
+	// The testing framework.
+	Test
+
+	// The function to shutdown the context used to create the environment.
+	shutdown veyron2.Shutdown
+
+	// The shell to use to start commands.
+	shell *modules.Shell
+
+	// The environment's root security principal.
+	principal security.Principal
+
+	// Maps path to TestBinary.
+	builtBinaries map[string]*testBinary
+
+	tempFiles []*os.File
+	tempDirs  []string
+
+	invocations []*testBinaryInvocation
+}
+
+type testBinary struct {
+	// The environment to which this binary belongs.
+	env *testEnvironment
+
+	// The path to the binary.
+	path string
+
+	// Environment variables that will be used when creating invocations
+	// via Start.
+	envVars []string
+
+	// The cleanup function to run when the binary exits.
+	cleanupFunc func()
+}
+
+type testBinaryInvocation struct {
+	// The embedded Session
+	Session
+
+	// The environment to which this invocation belongs.
+	env *testEnvironment
+
+	// The handle to the process that was run when this invocation was started.
+	handle modules.Handle
+
+	// The element representing this invocation in the list of
+	// invocations stored in the environment
+	el *list.Element
+
+	// The path of the binary used for this invocation.
+	path string
+
+	// The args the binary was started with
+	args []string
+
+	// True if the process has been shutdown
+	hasShutdown bool
+
+	// The error, if any, as determined when the invocation was
+	// shutdown. It must be set to a default initial value of
+	// errNotShutdown rather than nil to allow us to distinguish between
+	// a successful shutdown or an error.
+	shutdownErr error
+}
+
+var errNotShutdown = errors.New("has not been shutdown")
+
+func (i *testBinaryInvocation) Stdin() io.Writer {
+	return i.handle.Stdin()
+}
+
+func (i *testBinaryInvocation) Stdout() io.Reader {
+	return i.handle.Stdout()
+}
+
+func (i *testBinaryInvocation) Path() string {
+	return i.path
+}
+
+func (i *testBinaryInvocation) Exists() bool {
+	return syscall.Kill(i.handle.Pid(), 0) == nil
+}
+
+func (i *testBinaryInvocation) 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)
+}
+
+func readerToString(t Test, r io.Reader) string {
+	buf := bytes.Buffer{}
+	_, err := buf.ReadFrom(r)
+	if err != nil {
+		t.Fatalf("ReadFrom() failed: %v", err)
+	}
+	return buf.String()
+}
+
+func (i *testBinaryInvocation) Output() string {
+	return readerToString(i.env, i.Stdout())
+}
+
+func (i *testBinaryInvocation) Wait(stdout, stderr io.Writer) error {
+	err := i.handle.Shutdown(stdout, stderr)
+	i.hasShutdown = true
+	i.shutdownErr = err
+	return err
+}
+
+func (i *testBinaryInvocation) WaitOrDie(stdout, stderr io.Writer) {
+	if err := i.Wait(stdout, stderr); err != nil {
+		i.env.Fatalf("FATAL: Wait() for pid %d failed: %v", i.handle.Pid(), err)
+	}
+}
+
+func (i *testBinaryInvocation) Environment() T {
+	return i.env
+}
+
+func (b *testBinary) cleanup() {
+	binaryDir := path.Dir(b.path)
+	vlog.Infof("cleaning up %s", binaryDir)
+	if err := os.RemoveAll(binaryDir); err != nil {
+		vlog.Infof("WARNING: RemoveAll(%s) failed (%v)", binaryDir, err)
+	}
+}
+
+func (b *testBinary) Path() string {
+	return b.path
+}
+
+func (b *testBinary) Start(args ...string) Invocation {
+	depth := testutil.DepthToExternalCaller()
+	vlog.Infof(testutil.FormatLogLine(depth, "starting %s %s", b.Path(), strings.Join(args, " ")))
+	handle, err := b.env.shell.StartExternalCommand(b.envVars, append([]string{b.Path()}, args...)...)
+	if err != nil {
+		b.env.Fatalf("StartExternalCommand(%v, %v) failed: %v", b.Path(), strings.Join(args, ", "), err)
+	}
+	vlog.Infof("started PID %d\n", handle.Pid())
+	inv := &testBinaryInvocation{
+		env:         b.env,
+		handle:      handle,
+		path:        b.path,
+		args:        args,
+		shutdownErr: errNotShutdown,
+		Session:     expect.NewSession(b.env, handle.Stdout(), 5*time.Minute),
+	}
+	b.env.appendInvocation(inv)
+	return inv
+}
+
+func (b *testBinary) WithEnv(env []string) TestBinary {
+	newBin := *b
+	newBin.envVars = env
+	return &newBin
+}
+
+func (e *testEnvironment) Principal() security.Principal {
+	return e.principal
+}
+
+func (e *testEnvironment) Cleanup() {
+	if e.Failed() {
+		if testutil.IntegrationTestsDebugShellOnError {
+			e.DebugShell()
+		}
+		// Print out a summary of the invocations and their status.
+		for i, inv := range e.invocations {
+			if inv.hasShutdown && inv.Exists() {
+				m := fmt.Sprintf("%d: %s has been shutdown but still exists: %v", i, inv.path, inv.shutdownErr)
+				e.Log(m)
+				vlog.VI(1).Info(m)
+				vlog.VI(2).Infof("%d: %s %v", i, inv.path, inv.args)
+				continue
+			}
+			m := fmt.Sprintf("%d: %s: shutdown status: %v", i, inv.path, inv.shutdownErr)
+			e.Log(m)
+			vlog.VI(1).Info(m)
+			vlog.VI(2).Infof("%d: %s %v", i, inv.path, inv.args)
+		}
+	}
+
+	vlog.VI(1).Infof("V23Test.Cleanup")
+	// Shut down all processes before attempting to delete any
+	// files/directories to avoid potential 'file system busy' problems
+	// on non-unix systems.
+	for _, inv := range e.invocations {
+		if inv.hasShutdown {
+			vlog.VI(1).Infof("V23Test.Cleanup: %q has been shutdown", inv.Path())
+			continue
+		}
+		vlog.VI(1).Infof("V23Test.Cleanup: Kill: %q", inv.Path())
+		err := inv.Kill(syscall.SIGTERM)
+		inv.Wait(os.Stdout, os.Stderr)
+		vlog.VI(1).Infof("V23Test.Cleanup: Killed: %q: %v", inv.Path(), err)
+	}
+	vlog.VI(1).Infof("V23Test.Cleanup: all invocations taken care of.")
+
+	if err := e.shell.Cleanup(os.Stdout, os.Stderr); err != nil {
+		e.Fatalf("WARNING: could not clean up shell (%v)", err)
+	}
+
+	vlog.VI(1).Infof("V23Test.Cleanup: cleaning up binaries & files")
+
+	for _, binary := range e.builtBinaries {
+		binary.cleanupFunc()
+	}
+
+	for _, tempFile := range e.tempFiles {
+		vlog.VI(1).Infof("V23Test.Cleanup: cleaning up %s", tempFile.Name())
+		if err := tempFile.Close(); err != nil {
+			vlog.Errorf("WARNING: Close(%q) failed: %v", tempFile.Name(), err)
+		}
+		if err := os.RemoveAll(tempFile.Name()); err != nil {
+			vlog.Errorf("WARNING: RemoveAll(%q) failed: %v", tempFile.Name(), err)
+		}
+	}
+
+	for _, tempDir := range e.tempDirs {
+		vlog.VI(1).Infof("V23Test.Cleanup: cleaning up %s", tempDir)
+		if err := os.RemoveAll(tempDir); err != nil {
+			vlog.Errorf("WARNING: RemoveAll(%q) failed: %v", tempDir, err)
+		}
+	}
+
+	// shutdown the runtime
+	e.shutdown()
+}
+
+func (e *testEnvironment) GetVar(key string) (string, bool) {
+	return e.shell.GetVar(key)
+}
+
+func (e *testEnvironment) SetVar(key, value string) {
+	e.shell.SetVar(key, value)
+}
+
+func (e *testEnvironment) ClearVar(key string) {
+	e.shell.ClearVar(key)
+}
+
+func writeStringOrDie(t Test, f *os.File, s string) {
+	if _, err := f.WriteString(s); err != nil {
+		t.Fatalf("Write() failed: %v", err)
+	}
+}
+
+func (e *testEnvironment) DebugShell() {
+	// Get the current working directory.
+	cwd, err := os.Getwd()
+	if err != nil {
+		e.Fatalf("Getwd() failed: %v", err)
+	}
+
+	// Transfer stdin, stdout, and stderr to the new process
+	// and also set target directory for the shell to start in.
+	dev := "/dev/tty"
+	fd, err := syscall.Open(dev, syscall.O_RDWR, 0)
+	if err != nil {
+		vlog.Errorf("WARNING: Open(%v) failed, was asked to create a debug shell but cannot: %v", dev, err)
+		return
+	}
+	file := os.NewFile(uintptr(fd), dev)
+	attr := os.ProcAttr{
+		Files: []*os.File{file, file, file},
+		Dir:   cwd,
+	}
+
+	// Set up environment for Child.
+	if ns, ok := e.GetVar("NAMESPACE_ROOT"); ok {
+		attr.Env = append(attr.Env, "NAMESPACE_ROOT="+ns)
+	}
+	// TODO(sjr): talk to Ankur about how to do this properly/safely
+	// using either the agent, or a file descriptor inherited by the shell
+	// and its children. This is preferable since it avoids compiling and
+	// running the agent which can be an overhead in tests.
+	dir, _ := tsecurity.ForkCredentials(e.principal, "debugShell")
+	attr.Env = append(attr.Env, "VEYRON_CRED="+dir)
+
+	// Start up a new shell.
+	writeStringOrDie(e, file, ">> Starting a new interactive shell\n")
+	writeStringOrDie(e, file, "Hit CTRL-D to resume the test\n")
+	if len(e.builtBinaries) > 0 {
+		writeStringOrDie(e, file, "Built binaries:\n")
+		for _, value := range e.builtBinaries {
+			writeStringOrDie(e, file, "\t"+value.Path()+"\n")
+		}
+	}
+
+	shellPath := "/bin/sh"
+	if shellPathFromEnv := os.Getenv("SHELL"); shellPathFromEnv != "" {
+		shellPath = shellPathFromEnv
+	}
+	proc, err := os.StartProcess(shellPath, []string{}, &attr)
+	if err != nil {
+		e.Fatalf("StartProcess(%q) failed: %v", shellPath, err)
+	}
+
+	// Wait until user exits the shell
+	state, err := proc.Wait()
+	if err != nil {
+		e.Fatalf("Wait(%v) failed: %v", shellPath, err)
+	}
+
+	writeStringOrDie(e, file, fmt.Sprintf("<< Exited shell: %s\n", state.String()))
+}
+
+func (e *testEnvironment) BinaryFromPath(path string) TestBinary {
+	return &testBinary{
+		env:         e,
+		envVars:     nil,
+		path:        path,
+		cleanupFunc: func() {},
+	}
+}
+
+func (e *testEnvironment) BuildGoPkg(binary_path string) TestBinary {
+	vlog.Infof("building %s...", binary_path)
+	if cached_binary := e.builtBinaries[binary_path]; cached_binary != nil {
+		vlog.Infof("using cached binary for %s at %s.", binary_path, cached_binary.Path())
+		return cached_binary
+	}
+	built_path, cleanup, err := buildPkg(binary_path)
+	if err != nil {
+		e.Fatalf("buildPkg() failed: %v", err)
+		return nil
+	}
+	output_path := path.Join(built_path, path.Base(binary_path))
+	vlog.Infof("done building %s, written to %s.", binary_path, output_path)
+	binary := &testBinary{
+		env:         e,
+		envVars:     nil,
+		path:        output_path,
+		cleanupFunc: cleanup,
+	}
+	e.builtBinaries[binary_path] = binary
+	return binary
+}
+
+func (e *testEnvironment) TempFile() *os.File {
+	f, err := ioutil.TempFile("", "")
+	if err != nil {
+		e.Fatalf("TempFile() failed: %v", err)
+	}
+	vlog.Infof("created temporary file at %s", f.Name())
+	e.tempFiles = append(e.tempFiles, f)
+	return f
+}
+
+func (e *testEnvironment) TempDir() string {
+	f, err := ioutil.TempDir("", "")
+	if err != nil {
+		e.Fatalf("TempDir() failed: %v", err)
+	}
+	vlog.Infof("created temporary directory at %s", f)
+	e.tempDirs = append(e.tempDirs, f)
+	return f
+}
+
+func (e *testEnvironment) appendInvocation(inv *testBinaryInvocation) {
+	e.invocations = append(e.invocations, inv)
+}
+
+// Creates a new local testing environment. A local testing environment has a
+// a security principle available via Principal().
+//
+// You should clean up the returned environment using the env.Cleanup() method.
+// A typical end-to-end test will begin like:
+//
+//   func TestFoo(t *testing.T) {
+//     env := integration.NewTestEnvironment(t)
+//     defer env.Cleanup()
+//
+//     ...
+//   }
+func New(t Test) T {
+	ctx, shutdown := veyron2.Init()
+
+	vlog.Infof("creating root principal")
+	principal := tsecurity.NewPrincipal("root")
+	ctx, err := veyron2.SetPrincipal(ctx, principal)
+	if err != nil {
+		t.Fatalf("failed to set principal: %v", err)
+	}
+
+	shell, err := modules.NewShell(ctx, principal)
+	if err != nil {
+		t.Fatalf("NewShell() failed: %v", err)
+	}
+	shell.SetStartTimeout(1 * time.Minute)
+	shell.SetWaitTimeout(5 * time.Minute)
+
+	return &testEnvironment{
+		Test:          t,
+		principal:     principal,
+		builtBinaries: make(map[string]*testBinary),
+		shell:         shell,
+		tempFiles:     []*os.File{},
+		tempDirs:      []string{},
+		shutdown:      shutdown,
+	}
+}
+
+// BuildPkg returns a path to a directory that contains the built binary for
+// the given packages and a function that should be invoked to clean up the
+// build artifacts. Note that the clients of this function should not modify
+// the contents of this directory directly and instead defer to the cleanup
+// function.
+func buildPkg(pkg string) (string, func(), error) {
+	// The VEYRON_INTEGRATION_BIN_DIR environment variable can be
+	// used to identify a directory that multiple integration
+	// tests can use to share binaries. Whoever sets this
+	// environment variable is responsible for cleaning up the
+	// directory it points to.
+	binDir, cleanupFn := os.Getenv("VEYRON_INTEGRATION_BIN_DIR"), func() {}
+	if binDir == "" {
+		// If the aforementioned environment variable is not
+		// set, the given packages are built in a temporary
+		// directory, which the cleanup function removes.
+		tmpDir, err := ioutil.TempDir("", "")
+		if err != nil {
+			return "", nil, fmt.Errorf("TempDir() failed: %v", err)
+		}
+		binDir, cleanupFn = tmpDir, func() { os.RemoveAll(tmpDir) }
+	}
+	binFile := filepath.Join(binDir, path.Base(pkg))
+	if _, err := os.Stat(binFile); err != nil {
+		if !os.IsNotExist(err) {
+			return "", nil, err
+		}
+		cmd := exec.Command("v23", "go", "build", "-o", filepath.Join(binDir, path.Base(pkg)), pkg)
+		if err := cmd.Run(); err != nil {
+			return "", nil, err
+		}
+	}
+	return binDir, cleanupFn, nil
+}
+
+// RunTest runs a single Vanadium 'v23 style' integration test.
+func RunTest(t Test, fn func(i T)) {
+	if !testutil.IntegrationTestsEnabled {
+		t.Skip()
+	}
+	i := New(t)
+	// defer the Cleanup method so that it will be called even if
+	// t.Fatalf/FailNow etc are called and can print out useful information.
+	defer i.Cleanup()
+	fn(i)
+}
+
+// RunRootMT builds and runs a root mount table instance. It populates
+// the NAMESPACE_ROOT variable in the test environment so that all subsequent
+// invocations will access this root mount table.
+func RunRootMT(t T, args ...string) (TestBinary, Invocation) {
+	b := t.BuildGoPkg("v.io/core/veyron/services/mounttable/mounttabled")
+	i := b.Start(args...)
+	s := expect.NewSession(t, i.Stdout(), time.Minute)
+	name := s.ExpectVar("NAME")
+	i.Environment().SetVar("NAMESPACE_ROOT", name)
+	return b, i
+}
+
+// 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
new file mode 100644
index 0000000..8c0a3d8
--- /dev/null
+++ b/lib/testutil/v23tests/v23tests_test.go
@@ -0,0 +1,139 @@
+package v23tests_test
+
+import (
+	"bytes"
+	"fmt"
+	"io"
+	"regexp"
+	"strings"
+	"syscall"
+	"testing"
+	"time"
+
+	"v.io/core/veyron2/naming"
+	"v.io/core/veyron2/vlog"
+
+	"v.io/core/veyron/lib/expect"
+	"v.io/core/veyron/lib/modules"
+	"v.io/core/veyron/lib/testutil"
+	"v.io/core/veyron/lib/testutil/v23tests"
+	_ "v.io/core/veyron/profiles"
+)
+
+// TODO(sjr): add more unit tests, especially for errors cases.
+// TODO(sjr): need to make sure processes don't get left lying around.
+
+func TestBinaryFromPath(t *testing.T) {
+	env := v23tests.New(t)
+	defer env.Cleanup()
+
+	bash := env.BinaryFromPath("/bin/bash")
+	if want, got := "hello world\n", bash.Start("-c", "echo hello world").Output(); want != got {
+		t.Fatalf("unexpected output, want %s, got %s", want, got)
+	}
+
+	inv := bash.Start("-c", "echo hello world")
+	var buf bytes.Buffer
+	inv.WaitOrDie(&buf, nil)
+	if want, got := "hello world\n", buf.String(); want != got {
+		t.Fatalf("unexpected output, want %s, got %s", want, got)
+	}
+}
+
+func TestMountTable(t *testing.T) {
+	env := v23tests.New(t)
+	defer env.Cleanup()
+
+	v23tests.RunRootMT(env, "--veyron.tcp.address=127.0.0.1:0")
+	proxyBin := env.BuildGoPkg("v.io/core/veyron/services/proxy/proxyd")
+	nsBin := env.BuildGoPkg("v.io/core/veyron/tools/namespace")
+
+	mt, ok := env.GetVar("NAMESPACE_ROOT")
+	if !ok || len(mt) == 0 {
+		t.Fatalf("expected a mount table name")
+	}
+
+	proxy := proxyBin.Start("--address=127.0.0.1:0", "-name=proxyd")
+	proxyName := proxy.ExpectVar("NAME")
+	proxyAddress, _ := naming.SplitAddressName(proxyName)
+
+	re := regexp.MustCompile("proxyd (.*) \\(.*")
+	for i := 0; i < 10; i++ {
+		time.Sleep(100 * time.Millisecond)
+		inv := nsBin.Start("glob", "...")
+		parts := re.FindStringSubmatch(inv.ReadLine())
+		if len(parts) == 2 {
+			if got, want := proxyAddress, parts[1]; got != want {
+				t.Fatalf("got: %v, want: %v", got, want)
+			} else {
+				break
+			}
+		}
+	}
+	if got, want := proxy.Exists(), true; got != want {
+		t.Fatalf("got: %v, want: %v", got, want)
+	}
+}
+
+// The next set of tests are a complicated dance to test the correct
+// detection and logging of failed integration tests. The complication
+// is that we need to run these tests in a child process so that we
+// can examine their output, but not in the parent process. We use the
+// modules framework to do so, with the added twist that we need access
+// to an instance of testing.T which we obtain via a global variable.
+// TODO(cnicolaou): this will need to change once we switch to using
+// TestMain.
+func IntegrationTestInChild(i v23tests.T) {
+	fmt.Println("Hello")
+	sleep := i.BinaryFromPath("/bin/sleep")
+	sleep.Start("3600")
+	s2 := sleep.Start("6400")
+	sleep.Start("21600")
+	s2.Kill(syscall.SIGTERM)
+	s2.Wait(nil, nil)
+	i.FailNow()
+	panic("should never get here")
+}
+
+var globalT *testing.T
+
+func TestHelperProcess(t *testing.T) {
+	globalT = t
+	modules.DispatchInTest()
+}
+
+func RunIntegrationTestInChild(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
+	v23tests.RunTest(globalT, IntegrationTestInChild)
+	return nil
+}
+
+func init() {
+	testutil.Init()
+	modules.RegisterChild("RunIntegrationTestInChild", "", RunIntegrationTestInChild)
+}
+
+func TestDeferHandling(t *testing.T) {
+	sh, _ := modules.NewShell(nil, nil)
+	child, err := sh.Start("RunIntegrationTestInChild", nil, "--v23.tests")
+	if err != nil {
+		t.Fatal(err)
+	}
+	s := expect.NewSession(t, child.Stdout(), time.Minute)
+	s.Expect("Hello")
+	s.ExpectRE("--- FAIL: TestHelperProcess", -1)
+	for _, e := range []string{
+		".* 0: /bin/sleep: shutdown status: has not been shutdown",
+		".* 1: /bin/sleep: shutdown status: signal: terminated",
+		".* 2: /bin/sleep: shutdown status: has not been shutdown",
+	} {
+		s.ExpectRE(e, -1)
+	}
+	var stderr bytes.Buffer
+	if err := child.Shutdown(nil, &stderr); err != nil {
+		if !strings.Contains(err.Error(), "exit status 1") {
+			t.Fatal(err)
+		}
+	}
+	vlog.Infof("Child\n=============\n%s", stderr.String())
+	vlog.Infof("-----------------")
+}
diff --git a/services/identity/server/identityd.go b/services/identity/server/identityd.go
index 3416ef8..9ef16cb 100644
--- a/services/identity/server/identityd.go
+++ b/services/identity/server/identityd.go
@@ -186,6 +186,12 @@
 		dischargerService:   services.DischargerServer(discharger.NewDischarger()),
 		oauthBlesserService: blesser.NewOAuthBlesserServer(blesserParams),
 	})
+	// Set up the glob invoker.
+	var children []string
+	for k, _ := range d {
+		children = append(children, k)
+	}
+	d[""] = ipc.ChildrenGlobberInvoker(children...)
 	return d
 }
 
diff --git a/services/mgmt/application/applicationd/testdata/integration_test.go b/services/mgmt/application/applicationd/testdata/integration_test.go
index 6140f73..697b482 100644
--- a/services/mgmt/application/applicationd/testdata/integration_test.go
+++ b/services/mgmt/application/applicationd/testdata/integration_test.go
@@ -4,12 +4,10 @@
 	"encoding/json"
 	"os"
 	"strings"
-	"syscall"
 	"testing"
 
-	"v.io/core/veyron/lib/modules"
-	"v.io/core/veyron/lib/testutil/integration"
 	libsecurity "v.io/core/veyron/lib/testutil/security"
+	"v.io/core/veyron/lib/testutil/v23tests"
 	_ "v.io/core/veyron/profiles"
 	vsecurity "v.io/core/veyron/security"
 	"v.io/core/veyron2/naming"
@@ -21,8 +19,8 @@
 	"v.io/core/veyron/tools/application",
 }
 
-func helper(t *testing.T, env integration.T, clientBin integration.TestBinary, expectError bool, credentials, cmd string, args ...string) string {
-	args = append([]string{"-veyron.credentials=" + credentials, "-veyron.namespace.root=" + env.RootMT(), cmd}, args...)
+func helper(t *testing.T, env v23tests.T, clientBin v23tests.TestBinary, expectError bool, credentials, cmd string, args ...string) string {
+	args = append([]string{"-veyron.credentials=" + credentials, cmd}, args...)
 	inv := clientBin.Start(args...)
 	out := inv.Output()
 	err := inv.Wait(os.Stdout, os.Stderr)
@@ -36,26 +34,24 @@
 
 }
 
-func matchEnvelope(t *testing.T, env integration.T, clientBin integration.TestBinary, expectError bool, credentials, name, suffix string) string {
+func matchEnvelope(t *testing.T, env v23tests.T, clientBin v23tests.TestBinary, expectError bool, credentials, name, suffix string) string {
 	return helper(t, env, clientBin, expectError, credentials, "match", naming.Join(name, suffix), "test-profile")
 }
 
-func putEnvelope(t *testing.T, env integration.T, clientBin integration.TestBinary, credentials, name, suffix, envelope string) string {
+func putEnvelope(t *testing.T, env v23tests.T, clientBin v23tests.TestBinary, credentials, name, suffix, envelope string) string {
 	return helper(t, env, clientBin, false, credentials, "put", naming.Join(name, suffix), "test-profile", envelope)
 }
 
-func removeEnvelope(t *testing.T, env integration.T, clientBin integration.TestBinary, credentials, name, suffix string) string {
+func removeEnvelope(t *testing.T, env v23tests.T, clientBin v23tests.TestBinary, credentials, name, suffix string) string {
 	return helper(t, env, clientBin, false, credentials, "remove", naming.Join(name, suffix), "test-profile")
 }
 
-func TestHelperProcess(t *testing.T) {
-	modules.DispatchInTest()
-}
-
 func TestApplicationRepository(t *testing.T) {
-	env := integration.New(t)
+	env := v23tests.New(t)
 	defer env.Cleanup()
+	v23tests.RunRootMT(env, "--veyron.tcp.address=127.0.0.1:0")
 
+	// TODO(sjr): talk to caprita about the necessity/correctness of these.
 	// Generate credentials.
 	serverCred, serverPrin := libsecurity.NewCredentials("server")
 	defer os.RemoveAll(serverCred)
@@ -68,13 +64,11 @@
 	args := []string{
 		"-name=" + appRepoName,
 		"-store=" + appRepoStore,
+		"-v=2",
 		"-veyron.tcp.address=127.0.0.1:0",
 		"-veyron.credentials=" + serverCred,
-		"-veyron.namespace.root=" + env.RootMT(),
 	}
-	serverBin := env.BuildGoPkg("v.io/core/veyron/services/mgmt/application/applicationd")
-	serverProcess := serverBin.Start(args...)
-	defer serverProcess.Kill(syscall.SIGTERM)
+	env.BuildGoPkg("v.io/core/veyron/services/mgmt/application/applicationd").Start(args...)
 
 	// Build the client binary.
 	clientBin := env.BuildGoPkg("v.io/core/veyron/tools/application")
diff --git a/services/mgmt/binary/binaryd/testdata/integration_test.go b/services/mgmt/binary/binaryd/testdata/integration_test.go
index c1f61df..80e39c1 100644
--- a/services/mgmt/binary/binaryd/testdata/integration_test.go
+++ b/services/mgmt/binary/binaryd/testdata/integration_test.go
@@ -8,13 +8,11 @@
 	"os"
 	"os/exec"
 	"strings"
-	"syscall"
 	"testing"
 
-	"v.io/core/veyron/lib/modules"
 	"v.io/core/veyron/lib/testutil"
-	"v.io/core/veyron/lib/testutil/integration"
 	"v.io/core/veyron/lib/testutil/security"
+	"v.io/core/veyron/lib/testutil/v23tests"
 	_ "v.io/core/veyron/profiles"
 	"v.io/core/veyron2/naming"
 )
@@ -50,19 +48,17 @@
 	}
 }
 
-func deleteFile(env integration.T, clientBin integration.TestBinary, credentials, name, suffix string) {
+func deleteFile(env v23tests.T, clientBin v23tests.TestBinary, credentials, name, suffix string) {
 	deleteArgs := []string{
 		"-veyron.credentials=" + credentials,
-		"-veyron.namespace.root=" + env.RootMT(),
 		"delete", naming.Join(name, suffix),
 	}
 	clientBin.Start(deleteArgs...).WaitOrDie(nil, nil)
 }
 
-func downloadFile(t *testing.T, env integration.T, clientBin integration.TestBinary, expectError bool, credentials, name, path, suffix string) {
+func downloadFile(t *testing.T, env v23tests.T, clientBin v23tests.TestBinary, expectError bool, credentials, name, path, suffix string) {
 	downloadArgs := []string{
 		"-veyron.credentials=" + credentials,
-		"-veyron.namespace.root=" + env.RootMT(),
 		"download", naming.Join(name, suffix), path,
 	}
 	err := clientBin.Start(downloadArgs...).Wait(os.Stdout, os.Stderr)
@@ -90,31 +86,26 @@
 	}
 }
 
-func rootURL(t *testing.T, env integration.T, clientBin integration.TestBinary, credentials, name string) string {
+func rootURL(t *testing.T, env v23tests.T, clientBin v23tests.TestBinary, credentials, name string) string {
 	rootArgs := []string{
 		"-veyron.credentials=" + credentials,
-		"-veyron.namespace.root=" + env.RootMT(),
 		"url", name,
 	}
 	return strings.TrimSpace(clientBin.Start(rootArgs...).Output())
 }
 
-func uploadFile(t *testing.T, env integration.T, clientBin integration.TestBinary, credentials, name, path, suffix string) {
+func uploadFile(t *testing.T, env v23tests.T, clientBin v23tests.TestBinary, credentials, name, path, suffix string) {
 	uploadArgs := []string{
 		"-veyron.credentials=" + credentials,
-		"-veyron.namespace.root=" + env.RootMT(),
 		"upload", naming.Join(name, suffix), path,
 	}
 	clientBin.Start(uploadArgs...).WaitOrDie(os.Stdout, os.Stderr)
 }
 
-func TestHelperProcess(t *testing.T) {
-	modules.DispatchInTest()
-}
-
 func TestBinaryRepositoryIntegration(t *testing.T) {
-	env := integration.New(t)
+	env := v23tests.New(t)
 	defer env.Cleanup()
+	v23tests.RunRootMT(env, "--veyron.tcp.address=127.0.0.1:0")
 
 	// Build the required binaries.
 	binaryRepoBin := env.BuildGoPkg("v.io/core/veyron/services/mgmt/binary/binaryd")
@@ -133,11 +124,9 @@
 		"-http=127.0.0.1:0",
 		"-veyron.tcp.address=127.0.0.1:0",
 		"-veyron.credentials=" + serverCred,
-		"-veyron.namespace.root=" + env.RootMT(),
 	}
 
-	server := binaryRepoBin.Start(args...)
-	defer server.Kill(syscall.SIGTERM)
+	binaryRepoBin.Start(args...)
 
 	// Upload a random binary file.
 	binFile := env.TempFile()
diff --git a/services/mgmt/binary/impl/fs_utils.go b/services/mgmt/binary/impl/fs_utils.go
index 9468fb6..02808c0 100644
--- a/services/mgmt/binary/impl/fs_utils.go
+++ b/services/mgmt/binary/impl/fs_utils.go
@@ -13,9 +13,11 @@
 )
 
 const (
-	checksum = "checksum"
-	data     = "data"
-	lock     = "lock"
+	checksumFileName  = "checksum"
+	dataFileName      = "data"
+	lockFileName      = "lock"
+	nameFileName      = "name"
+	mediaInfoFileName = "mediainfo"
 )
 
 // checksumExists checks whether the given part path is valid and
@@ -31,7 +33,7 @@
 		vlog.Errorf("Stat(%v) failed: %v", path, err)
 		return verror.Make(ErrOperationFailed, nil, path)
 	}
-	checksumFile := filepath.Join(path, checksum)
+	checksumFile := filepath.Join(path, checksumFileName)
 	_, err := os.Stat(checksumFile)
 	switch {
 	case os.IsNotExist(err):
@@ -80,7 +82,7 @@
 			}
 			result[idx] = filepath.Join(path, partName)
 		} else {
-			if info.Name() == "name" || info.Name() == "mediainfo" {
+			if info.Name() == nameFileName || info.Name() == mediaInfoFileName {
 				continue
 			}
 			// The only entries should correspond to the part dirs.
@@ -97,7 +99,7 @@
 	for d := 0; d < i.state.depth; d++ {
 		pattern = filepath.Join(pattern, "*")
 	}
-	pattern = filepath.Join(pattern, "*", "name")
+	pattern = filepath.Join(pattern, "*", nameFileName)
 	matches, err := filepath.Glob(pattern)
 	if err != nil {
 		return nil
diff --git a/services/mgmt/binary/impl/http.go b/services/mgmt/binary/impl/http.go
index 1b34812..7f672d5 100644
--- a/services/mgmt/binary/impl/http.go
+++ b/services/mgmt/binary/impl/http.go
@@ -39,7 +39,7 @@
 		if err := checksumExists(part); err != nil {
 			return nil, err
 		}
-		dataPath := filepath.Join(part, data)
+		dataPath := filepath.Join(part, dataFileName)
 		var err error
 		if partFiles[i], err = os.Open(dataPath); err != nil {
 			vlog.Errorf("Open(%v) failed: %v", dataPath, err)
diff --git a/services/mgmt/binary/impl/service.go b/services/mgmt/binary/impl/service.go
index 718e9e2..9b659da 100644
--- a/services/mgmt/binary/impl/service.go
+++ b/services/mgmt/binary/impl/service.go
@@ -12,6 +12,8 @@
 // name
 // acls/data
 // acls/sig
+// mediainfo
+// name
 // <part_1>/checksum
 // <part_1>/data
 // ...
@@ -115,7 +117,7 @@
 		vlog.Errorf("TempDir(%v, %v) failed: %v", parent, prefix, err)
 		return verror.Make(ErrOperationFailed, context.Context())
 	}
-	nameFile := filepath.Join(tmpDir, "name")
+	nameFile := filepath.Join(tmpDir, nameFileName)
 	if err := ioutil.WriteFile(nameFile, []byte(i.suffix), os.FileMode(0600)); err != nil {
 		vlog.Errorf("WriteFile(%q) failed: %v", nameFile)
 		return verror.Make(ErrOperationFailed, context.Context())
@@ -128,7 +130,7 @@
 		return verror.Make(ErrOperationFailed, context.Context())
 	}
 
-	infoFile := filepath.Join(tmpDir, "mediainfo")
+	infoFile := filepath.Join(tmpDir, mediaInfoFileName)
 	jInfo, err := json.Marshal(mediaInfo)
 	if err != nil {
 		vlog.Errorf("json.Marshal(%v) failed: %v", mediaInfo, err)
@@ -209,7 +211,7 @@
 	if err := checksumExists(path); err != nil {
 		return err
 	}
-	dataPath := filepath.Join(path, data)
+	dataPath := filepath.Join(path, dataFileName)
 	file, err := os.Open(dataPath)
 	if err != nil {
 		vlog.Errorf("Open(%v) failed: %v", dataPath, err)
@@ -250,7 +252,7 @@
 		return []binary.PartInfo{}, repository.MediaInfo{}, err
 	}
 	for _, part := range parts {
-		checksumFile := filepath.Join(part, checksum)
+		checksumFile := filepath.Join(part, checksumFileName)
 		bytes, err := ioutil.ReadFile(checksumFile)
 		if err != nil {
 			if os.IsNotExist(err) {
@@ -260,7 +262,7 @@
 			vlog.Errorf("ReadFile(%v) failed: %v", checksumFile, err)
 			return []binary.PartInfo{}, repository.MediaInfo{}, verror.Make(ErrOperationFailed, context.Context())
 		}
-		dataFile := filepath.Join(part, data)
+		dataFile := filepath.Join(part, dataFileName)
 		fi, err := os.Stat(dataFile)
 		if err != nil {
 			if os.IsNotExist(err) {
@@ -272,7 +274,7 @@
 		}
 		result = append(result, binary.PartInfo{Checksum: string(bytes), Size: fi.Size()})
 	}
-	infoFile := filepath.Join(i.path, "mediainfo")
+	infoFile := filepath.Join(i.path, mediaInfoFileName)
 	jInfo, err := ioutil.ReadFile(infoFile)
 	if err != nil {
 		vlog.Errorf("ReadFile(%q) failed: %v", infoFile)
@@ -296,7 +298,7 @@
 		return err
 	}
 	// Use os.OpenFile() to resolve races.
-	lockPath, flags, perm := filepath.Join(path, lock), os.O_CREATE|os.O_WRONLY|os.O_EXCL, os.FileMode(0600)
+	lockPath, flags, perm := filepath.Join(path, lockFileName), os.O_CREATE|os.O_WRONLY|os.O_EXCL, os.FileMode(0600)
 	lockFile, err := os.OpenFile(lockPath, flags, perm)
 	if err != nil {
 		if os.IsExist(err) {
@@ -336,7 +338,7 @@
 	}
 
 	hash := hex.EncodeToString(h.Sum(nil))
-	checksumFile, perm := filepath.Join(path, checksum), os.FileMode(0600)
+	checksumFile, perm := filepath.Join(path, checksumFileName), os.FileMode(0600)
 	if err := ioutil.WriteFile(checksumFile, []byte(hash), perm); err != nil {
 		vlog.Errorf("WriteFile(%v, %v, %v) failed: %v", checksumFile, hash, perm, err)
 		if err := os.Remove(file.Name()); err != nil {
@@ -344,7 +346,7 @@
 		}
 		return verror.Make(ErrOperationFailed, context.Context())
 	}
-	dataFile := filepath.Join(path, data)
+	dataFile := filepath.Join(path, dataFileName)
 	if err := os.Rename(file.Name(), dataFile); err != nil {
 		vlog.Errorf("Rename(%v, %v) failed: %v", file.Name(), dataFile, err)
 		if err := os.Remove(file.Name()); err != nil {
diff --git a/services/mgmt/build/buildd/testdata/integration_test.go b/services/mgmt/build/buildd/testdata/integration_test.go
index f913b35..96c0e12 100644
--- a/services/mgmt/build/buildd/testdata/integration_test.go
+++ b/services/mgmt/build/buildd/testdata/integration_test.go
@@ -8,12 +8,11 @@
 	"path/filepath"
 	"runtime"
 	"strings"
-	"syscall"
 	"testing"
 
 	"v.io/core/veyron/lib/modules"
-	"v.io/core/veyron/lib/testutil/integration"
 	"v.io/core/veyron/lib/testutil/security"
+	"v.io/core/veyron/lib/testutil/v23tests"
 	_ "v.io/core/veyron/profiles"
 )
 
@@ -29,8 +28,9 @@
 }
 
 func TestBuildServerIntegration(t *testing.T) {
-	env := integration.New(t)
+	env := v23tests.New(t)
 	defer env.Cleanup()
+	v23tests.RunRootMT(env, "--veyron.tcp.address=127.0.0.1:0")
 
 	// Generate credentials.
 	serverCred, serverPrin := security.NewCredentials("server")
@@ -50,10 +50,8 @@
 		"-name=" + buildServerName, "-gobin=" + goBin, "-goroot=" + goRoot,
 		"-veyron.tcp.address=127.0.0.1:0",
 		"-veyron.credentials=" + serverCred,
-		"-veyron.namespace.root=" + env.RootMT(),
 	}
-	serverProcess := buildServerBin.Start(args...)
-	defer serverProcess.Kill(syscall.SIGTERM)
+	buildServerBin.Start(args...)
 
 	// Create and build a test source file.
 	testGoPath := env.TempDir()
@@ -72,7 +70,6 @@
 	}
 	buildArgs := []string{
 		"-veyron.credentials=" + clientCred,
-		"-veyron.namespace.root=" + env.RootMT(),
 		"build", buildServerName, "test",
 	}
 	buildEnv := []string{"GOPATH=" + testGoPath, "GOROOT=" + goRoot, "TMPDIR=" + testBinDir}
diff --git a/services/mgmt/profile/profiled/testdata/integration_test.go b/services/mgmt/profile/profiled/testdata/integration_test.go
index 9c4f6ac..1d76a11 100644
--- a/services/mgmt/profile/profiled/testdata/integration_test.go
+++ b/services/mgmt/profile/profiled/testdata/integration_test.go
@@ -3,18 +3,15 @@
 import (
 	"os"
 	"strings"
-	"syscall"
 	"testing"
 
-	"v.io/core/veyron/lib/modules"
-	"v.io/core/veyron/lib/testutil/integration"
+	"v.io/core/veyron/lib/testutil/v23tests"
 	_ "v.io/core/veyron/profiles"
 	"v.io/core/veyron2/naming"
 )
 
-func profileCommandOutput(t *testing.T, env integration.T, profileBin integration.TestBinary, expectError bool, command, name, suffix string) string {
+func profileCommandOutput(t *testing.T, env v23tests.T, profileBin v23tests.TestBinary, expectError bool, command, name, suffix string) string {
 	labelArgs := []string{
-		"-veyron.namespace.root=" + env.RootMT(),
 		command, naming.Join(name, suffix),
 	}
 	labelCmd := profileBin.Start(labelArgs...)
@@ -29,29 +26,24 @@
 	return strings.TrimSpace(out)
 }
 
-func putProfile(t *testing.T, env integration.T, profileBin integration.TestBinary, name, suffix string) {
+func putProfile(t *testing.T, env v23tests.T, profileBin v23tests.TestBinary, name, suffix string) {
 	putArgs := []string{
-		"-veyron.namespace.root=" + env.RootMT(),
 		"put", naming.Join(name, suffix),
 	}
 	profileBin.Start(putArgs...).WaitOrDie(os.Stdout, os.Stderr)
 }
 
-func removeProfile(t *testing.T, env integration.T, profileBin integration.TestBinary, name, suffix string) {
+func removeProfile(t *testing.T, env v23tests.T, profileBin v23tests.TestBinary, name, suffix string) {
 	removeArgs := []string{
-		"-veyron.namespace.root=" + env.RootMT(),
 		"remove", naming.Join(name, suffix),
 	}
 	profileBin.Start(removeArgs...).WaitOrDie(os.Stdout, os.Stderr)
 }
 
-func TestHelperProcess(t *testing.T) {
-	modules.DispatchInTest()
-}
-
 func TestProfileRepository(t *testing.T) {
-	env := integration.New(t)
+	env := v23tests.New(t)
 	defer env.Cleanup()
+	v23tests.RunRootMT(env, "--veyron.tcp.address=127.0.0.1:0")
 
 	// Start the profile repository.
 	profileRepoName := "test-profile-repo"
@@ -59,11 +51,8 @@
 	args := []string{
 		"-name=" + profileRepoName, "-store=" + profileRepoStore,
 		"-veyron.tcp.address=127.0.0.1:0",
-		"-veyron.namespace.root=" + env.RootMT(),
 	}
-	serverBin := env.BuildGoPkg("v.io/core/veyron/services/mgmt/profile/profiled")
-	serverInv := serverBin.Start(args...)
-	defer serverInv.Kill(syscall.SIGTERM)
+	env.BuildGoPkg("v.io/core/veyron/services/mgmt/profile/profiled").Start(args...)
 
 	clientBin := env.BuildGoPkg("v.io/core/veyron/tools/profile")
 
diff --git a/services/mounttable/lib/servers.go b/services/mounttable/lib/servers.go
index 6edc5e3..1f844e7 100644
--- a/services/mounttable/lib/servers.go
+++ b/services/mounttable/lib/servers.go
@@ -67,7 +67,11 @@
 			return "", nil, err
 		}
 
-		nh, err := NewLoopbackNeighborhoodDispatcher(nhName, mtName)
+		addresses := []string{}
+		for _, ep := range mtEndpoints {
+			addresses = append(addresses, ep.Name())
+		}
+		nh, err := NewNeighborhoodDispatcher(nhName, addresses...)
 		if err != nil {
 			vlog.Errorf("NewNeighborhoodServer failed: %v", err)
 			stop()
diff --git a/services/mounttable/mounttabled/mounttable.go b/services/mounttable/mounttabled/mounttable.go
index 62017a3..307cb33 100644
--- a/services/mounttable/mounttabled/mounttable.go
+++ b/services/mounttable/mounttabled/mounttable.go
@@ -3,6 +3,7 @@
 
 import (
 	"flag"
+	"fmt"
 	"os"
 
 	"v.io/core/veyron2"
@@ -23,13 +24,17 @@
 	ctx, shutdown := veyron2.Init()
 	defer shutdown()
 
-	_, stop, err := mounttable.StartServers(ctx, veyron2.GetListenSpec(ctx), *mountName, *nhName, *aclFile)
+	name, stop, err := mounttable.StartServers(ctx, veyron2.GetListenSpec(ctx), *mountName, *nhName, *aclFile)
 	if err != nil {
 		vlog.Errorf("mounttable.StartServers failed: %v", err)
 		os.Exit(1)
 	}
 	defer stop()
 
+	// Print out a directly accessible name of the mount table so that
+	// integration tests can reliably read it from stdout.
+	fmt.Printf("NAME=%s\n", name)
+
 	// Wait until signal is received.
 	vlog.Info("Received signal ", <-signals.ShutdownOnSignals(ctx))
 }
diff --git a/services/mounttable/mounttabled/testdata/integration_test.go b/services/mounttable/mounttabled/testdata/integration_test.go
index 7b48cfe..4c9dd9d 100644
--- a/services/mounttable/mounttabled/testdata/integration_test.go
+++ b/services/mounttable/mounttabled/testdata/integration_test.go
@@ -4,31 +4,12 @@
 	"fmt"
 	"os"
 	"regexp"
-	"strings"
-	"syscall"
 	"testing"
-	"time"
 
-	"v.io/core/veyron/lib/expect"
-	"v.io/core/veyron/lib/modules"
-	"v.io/core/veyron/lib/testutil"
-	"v.io/core/veyron/lib/testutil/integration"
+	"v.io/core/veyron/lib/testutil/v23tests"
 	_ "v.io/core/veyron/profiles/static"
 )
 
-func TestHelperProcess(t *testing.T) {
-	modules.DispatchInTest()
-}
-
-func pickUnusedPort(t *testing.T) int {
-	if port, err := testutil.FindUnusedPort(); err != nil {
-		t.Fatalf("FindUnusedPort failed: %v", err)
-		return 0
-	} else {
-		return port
-	}
-}
-
 func getHostname(t *testing.T) string {
 	if hostname, err := os.Hostname(); err != nil {
 		t.Fatalf("Hostname() failed: %v", err)
@@ -39,53 +20,43 @@
 }
 
 func TestMount(t *testing.T) {
-	env := integration.New(t)
+	env := v23tests.New(t)
 	defer env.Cleanup()
 
-	// Build and run mounttabled. The test environment already gives us
-	// one, but we want to run it with our own options (e.g.
-	// --neighborhood_name), and currently the test environment does not
-	// let us customize the execution of the automatically-provided
-	// mounttabled.
-	serverBin := env.BuildGoPkg("v.io/core/veyron/services/mounttable/mounttabled")
-	serverPort := pickUnusedPort(t)
-	address := fmt.Sprintf("127.0.0.1:%d", serverPort)
-	endpoint := fmt.Sprintf("/%s", address)
 	neighborhood := fmt.Sprintf("test-%s-%d", getHostname(t), os.Getpid())
-	inv := serverBin.Start(fmt.Sprintf("--veyron.tcp.address=127.0.0.1:%d", serverPort), "--neighborhood_name="+neighborhood)
-	defer inv.Kill(syscall.SIGTERM)
+	v23tests.RunRootMT(env, "--veyron.tcp.address=127.0.0.1:0", "--neighborhood_name="+neighborhood)
+
+	name, _ := env.GetVar("NAMESPACE_ROOT")
 
 	clientBin := env.BuildGoPkg("v.io/core/veyron/tools/mounttable")
 
 	// Get the neighborhood endpoint from the mounttable.
-	e := expect.NewSession(t, clientBin.Start("glob", endpoint, "nh").Stdout(), 10*time.Second)
-	neighborhoodEndpoint := e.ExpectSetEventuallyRE(`^nh (.*) \(TTL .*\)$`)[0][1]
+	neighborhoodEndpoint := clientBin.Start("glob", name, "nh").ExpectSetEventuallyRE(`^nh (.*) \(TTL .*\)$`)[0][1]
 
-	if clientBin.Start("mount", endpoint+"/myself", endpoint, "5m").Wait(os.Stdout, os.Stderr) != nil {
+	if clientBin.Start("mount", name+"/myself", name, "5m").Wait(os.Stdout, os.Stderr) != nil {
 		t.Fatalf("failed to mount the mounttable on itself")
 	}
-	if clientBin.Start("mount", endpoint+"/google", "/www.google.com:80", "5m").Wait(os.Stdout, os.Stderr) != nil {
+	if clientBin.Start("mount", name+"/google", "/www.google.com:80", "5m").Wait(os.Stdout, os.Stderr) != nil {
 		t.Fatalf("failed to mount www.google.com")
 	}
 
 	// Test glob output. We expect three entries (two we mounted plus the
 	// neighborhood). The 'myself' entry should be the IP:port we
 	// specified for the mounttable.
-	e = expect.NewSession(t, clientBin.Start("glob", endpoint, "*").Stdout(), 10*time.Second)
-	matches := e.ExpectSetEventuallyRE(
+	glob := clientBin.Start("glob", name, "*")
+	matches := glob.ExpectSetEventuallyRE(
 		`^google /www\.google\.com:80 \(TTL .*\)$`,
 		`^myself (.*) \(TTL .*\)$`,
 		`^nh `+regexp.QuoteMeta(neighborhoodEndpoint)+` \(TTL .*\)$`)
-	if matches[1][1] != endpoint {
-		t.Fatalf("expected 'myself' entry to be %s, but was %s", endpoint, matches[1][1])
+	if matches[1][1] != name {
+		t.Fatalf("expected 'myself' entry to be %q, but was %q", name, matches[1][1])
 	}
 
-	// Test globbing on the neighborhood name. Its endpoint should contain
-	// the IP:port we specified for the mounttable.
-	e = expect.NewSession(t, clientBin.Start("glob", "/"+neighborhoodEndpoint, neighborhood).Stdout(), 10*time.Second)
-	matches = e.ExpectSetEventuallyRE("^" + regexp.QuoteMeta(neighborhood) + ` (.*) \(TTL .*\)$`)
-	if want := "@" + address + "@"; !strings.Contains(matches[0][1], want) {
-		t.Fatalf("expected neighborhood endpoint to contain %s, but did not. Was: %s", want, matches[0][1])
+	// Test globbing on the neighborhood name. Its endpoint should be the
+	// endpoint of the mount table.
+	glob = clientBin.Start("glob", "/"+neighborhoodEndpoint, neighborhood)
+	matches = glob.ExpectSetEventuallyRE("^" + regexp.QuoteMeta(neighborhood) + ` (.*) \(TTL .*\)$`)
+	if matches[0][1] != name {
+		t.Fatalf("expected endpoint of mount table for name %s", neighborhood)
 	}
-
 }
diff --git a/services/proxy/proxyd/main.go b/services/proxy/proxyd/main.go
index 63bf175..a4945a6 100644
--- a/services/proxy/proxyd/main.go
+++ b/services/proxy/proxyd/main.go
@@ -4,6 +4,7 @@
 
 import (
 	"flag"
+	"fmt"
 	"net/http"
 	_ "net/http/pprof"
 	"time"
@@ -49,6 +50,9 @@
 		defer publisher.Stop()
 		publisher.AddServer(proxy.Endpoint().String(), false)
 		publisher.AddName(*name)
+		// Print out a directly accessible name for the proxy table so
+		// that integration tests can reliably read it from stdout.
+		fmt.Printf("NAME=%s\n", proxy.Endpoint().Name())
 	}
 
 	if len(*healthzAddr) != 0 {
diff --git a/tools/binary/doc.go b/tools/binary/doc.go
index cac49eb..31712d3 100644
--- a/tools/binary/doc.go
+++ b/tools/binary/doc.go
@@ -10,7 +10,7 @@
 The binary commands are:
    delete      Delete a binary
    download    Download a binary
-   upload      Upload a binary
+   upload      Upload a binary or directory archive
    url         Fetch a download URL
    help        Display help for commands or topics
 Run "binary help [command]" for command usage.
@@ -78,13 +78,14 @@
 Binary Upload
 
 Upload connects to the binary repository and uploads the binary of the specified
-file. When successful, it writes the name of the new binary to stdout.
+file or archive of the specified directory. When successful, it writes the name
+of the new binary to stdout.
 
 Usage:
    binary upload <von> <filename>
 
 <von> is the veyron object name of the binary to upload <filename> is the name
-of the file to upload
+of the file or directory to upload
 
 Binary Url
 
diff --git a/tools/binary/impl.go b/tools/binary/impl.go
index cfd78f9..9d9f56a 100644
--- a/tools/binary/impl.go
+++ b/tools/binary/impl.go
@@ -59,15 +59,15 @@
 var cmdUpload = &cmdline.Command{
 	Run:   runUpload,
 	Name:  "upload",
-	Short: "Upload a binary",
+	Short: "Upload a binary or directory archive",
 	Long: `
 Upload connects to the binary repository and uploads the binary of the specified
-file. When successful, it writes the name of the new binary to stdout.
+file or archive of the specified directory. When successful, it writes the name of the new binary to stdout.
 `,
 	ArgsName: "<von> <filename>",
 	ArgsLong: `
 <von> is the veyron object name of the binary to upload
-<filename> is the name of the file to upload
+<filename> is the name of the file or directory to upload
 `,
 }
 
diff --git a/tools/debug/impl.go b/tools/debug/impl.go
index 4d423ba..c8a9406 100644
--- a/tools/debug/impl.go
+++ b/tools/debug/impl.go
@@ -161,6 +161,10 @@
 			if !ok {
 				return lastErr
 			}
+			if me.Error != nil {
+				fmt.Fprintf(cmd.Stderr(), "Error: %s: %v\n", me.Name, me.Error)
+				continue
+			}
 			fmt.Fprint(cmd.Stdout(), me.Name)
 			for _, s := range me.Servers {
 				fmt.Fprintf(cmd.Stdout(), " %s (Expires %s)", s.Server, s.Expires)
diff --git a/tools/debug/testdata/integration_test.go b/tools/debug/testdata/integration_test.go
index 1c23518..0a09142 100644
--- a/tools/debug/testdata/integration_test.go
+++ b/tools/debug/testdata/integration_test.go
@@ -8,30 +8,29 @@
 	"regexp"
 	"strconv"
 	"strings"
-	"syscall"
 	"testing"
 	"time"
 
-	"v.io/core/veyron/lib/modules"
-	"v.io/core/veyron/lib/testutil/integration"
-
+	"v.io/core/veyron/lib/testutil/v23tests"
 	_ "v.io/core/veyron/profiles/static"
 )
 
-func TestHelperProcess(t *testing.T) {
-	modules.DispatchInTest()
-}
+// TODO(sjr): consolidate some of these tests to amortize the cost
+// of the build/setup times.
 
+// TODO(sjr): caching of binaries is limited to a single instance of
+// of the integration environment which makes this test very slow.
 func TestDebugGlob(t *testing.T) {
-	env := integration.New(t)
+	env := v23tests.New(t)
 	defer env.Cleanup()
+	v23tests.RunRootMT(env, "--veyron.tcp.address=127.0.0.1:0")
 
 	binary := env.BuildGoPkg("v.io/core/veyron/tools/debug")
-	inv := binary.Start("glob", env.RootMT()+"/__debug/*")
+	inv := binary.Start("glob", "__debug/*")
 
 	var want string
 	for _, entry := range []string{"logs", "pprof", "stats", "vtrace"} {
-		want += env.RootMT() + "/__debug/" + entry + "\n"
+		want += "__debug/" + entry + "\n"
 	}
 	if got := inv.Output(); got != want {
 		t.Fatalf("unexpected output, want %s, got %s", want, got)
@@ -39,12 +38,13 @@
 }
 
 func TestDebugGlobLogs(t *testing.T) {
-	env := integration.New(t)
+	env := v23tests.New(t)
 	defer env.Cleanup()
+	v23tests.RunRootMT(env, "--veyron.tcp.address=127.0.0.1:0")
 
 	fileName := filepath.Base(env.TempFile().Name())
 	binary := env.BuildGoPkg("v.io/core/veyron/tools/debug")
-	output := binary.Start("glob", env.RootMT()+"/__debug/logs/*").Output()
+	output := binary.Start("glob", "__debug/logs/*").Output()
 
 	// The output should contain the filename.
 	want := "/logs/" + fileName
@@ -54,10 +54,11 @@
 }
 
 func TestReadHostname(t *testing.T) {
-	env := integration.New(t)
+	env := v23tests.New(t)
 	defer env.Cleanup()
+	v23tests.RunRootMT(env, "--veyron.tcp.address=127.0.0.1:0")
 
-	path := env.RootMT() + "/__debug/stats/system/hostname"
+	path := "__debug/stats/system/hostname"
 	binary := env.BuildGoPkg("v.io/core/veyron/tools/debug")
 	got := binary.Start("stats", "read", path).Output()
 	hostname, err := os.Hostname()
@@ -69,7 +70,7 @@
 	}
 }
 
-func createTestLogFile(t *testing.T, env integration.T, content string) *os.File {
+func createTestLogFile(t *testing.T, env v23tests.T, content string) *os.File {
 	file := env.TempFile()
 	_, err := file.Write([]byte(content))
 	if err != nil {
@@ -79,15 +80,16 @@
 }
 
 func TestLogSize(t *testing.T) {
-	env := integration.New(t)
+	env := v23tests.New(t)
 	defer env.Cleanup()
+	v23tests.RunRootMT(env, "--veyron.tcp.address=127.0.0.1:0")
 
 	binary := env.BuildGoPkg("v.io/core/veyron/tools/debug")
 	testLogData := "This is a test log file"
 	file := createTestLogFile(t, env, testLogData)
 
 	// Check to ensure the file size is accurate
-	str := strings.TrimSpace(binary.Start("logs", "size", env.RootMT()+"/__debug/logs/"+filepath.Base(file.Name())).Output())
+	str := strings.TrimSpace(binary.Start("logs", "size", "__debug/logs/"+filepath.Base(file.Name())).Output())
 	got, err := strconv.Atoi(str)
 	if err != nil {
 		t.Fatalf("Atoi(\"%q\") failed", str)
@@ -99,8 +101,9 @@
 }
 
 func TestStatsRead(t *testing.T) {
-	env := integration.New(t)
+	env := v23tests.New(t)
 	defer env.Cleanup()
+	v23tests.RunRootMT(env, "--veyron.tcp.address=127.0.0.1:0")
 
 	binary := env.BuildGoPkg("v.io/core/veyron/tools/debug")
 	testLogData := "This is a test log file\n"
@@ -108,9 +111,11 @@
 	logName := filepath.Base(file.Name())
 	runCount := 12
 	for i := 0; i < runCount; i++ {
-		binary.Start("logs", "read", env.RootMT()+"/__debug/logs/"+logName).WaitOrDie(nil, nil)
+		binary.Start("logs", "read", "__debug/logs/"+logName).WaitOrDie(nil, nil)
 	}
-	got := binary.Start("stats", "read", env.RootMT()+"/__debug/stats/ipc/server/routing-id/*/methods/ReadLog/latency-ms").Output()
+
+	got := binary.Start("stats", "read", "__debug/stats/ipc/server/routing-id/*/methods/ReadLog/latency-ms").Output()
+
 	want := fmt.Sprintf("Count: %d", runCount)
 	if !strings.Contains(got, want) {
 		t.Fatalf("expected output to contain %s, but did not\n", want, got)
@@ -118,16 +123,17 @@
 }
 
 func TestStatsWatch(t *testing.T) {
-	env := integration.New(t)
+	env := v23tests.New(t)
 	defer env.Cleanup()
+	v23tests.RunRootMT(env, "--veyron.tcp.address=127.0.0.1:0")
 
 	binary := env.BuildGoPkg("v.io/core/veyron/tools/debug")
 	testLogData := "This is a test log file\n"
 	file := createTestLogFile(t, env, testLogData)
 	logName := filepath.Base(file.Name())
-	binary.Start("logs", "read", env.RootMT()+"/__debug/logs/"+logName).WaitOrDie(nil, nil)
+	binary.Start("logs", "read", "__debug/logs/"+logName).WaitOrDie(nil, nil)
 
-	inv := binary.Start("stats", "watch", "-raw", env.RootMT()+"/__debug/stats/ipc/server/routing-id/*/methods/ReadLog/latency-ms")
+	inv := binary.Start("stats", "watch", "-raw", "__debug/stats/ipc/server/routing-id/*/methods/ReadLog/latency-ms")
 
 	lineChan := make(chan string)
 	// Go off and read the invocation's stdout.
@@ -151,23 +157,20 @@
 			t.Errorf("wanted but could not find %q in output\n%s", want, got)
 		}
 	}
-
-	// TODO(sjr): make env cleanup take care of invocations that are still
-	// running at the end of the test.
-	inv.Kill(syscall.SIGTERM)
 }
 
-func performTracedRead(debugBinary integration.TestBinary, path string) string {
+func performTracedRead(debugBinary v23tests.TestBinary, path string) string {
 	return debugBinary.Start("--veyron.vtrace.sample_rate=1", "logs", "read", path).Output()
 }
 
 func TestVTrace(t *testing.T) {
-	env := integration.New(t)
+	env := v23tests.New(t)
 	defer env.Cleanup()
+	v23tests.RunRootMT(env, "--veyron.tcp.address=127.0.0.1:0")
 
 	binary := env.BuildGoPkg("v.io/core/veyron/tools/debug")
 	logContent := "Hello, world!\n"
-	logPath := env.RootMT() + "/__debug/logs/" + filepath.Base(createTestLogFile(t, env, logContent).Name())
+	logPath := "__debug/logs/" + filepath.Base(createTestLogFile(t, env, logContent).Name())
 	// Create a log file with tracing, read it and check that the resulting trace exists.
 	got := performTracedRead(binary, logPath)
 	if logContent != got {
@@ -175,7 +178,7 @@
 	}
 
 	// Grab the ID of the first and only trace.
-	want, traceContent := 1, binary.Start("vtrace", env.RootMT()+"/__debug/vtrace").Output()
+	want, traceContent := 1, binary.Start("vtrace", "__debug/vtrace").Output()
 	if count := strings.Count(traceContent, "Trace -"); count != want {
 		t.Fatalf("unexpected trace count, want %d, got %d\n%s", want, count, traceContent)
 	}
@@ -194,14 +197,14 @@
 	performTracedRead(binary, logPath)
 
 	// Read vtrace, we should have 2 traces now.
-	want, output := 2, binary.Start("vtrace", env.RootMT()+"/__debug/vtrace").Output()
+	want, output := 2, binary.Start("vtrace", "__debug/vtrace").Output()
 	if count := strings.Count(output, "Trace -"); count != want {
 		t.Fatalf("unexpected trace count, want %d, got %d\n%s", want, count, output)
 	}
 
 	// Now ask for a particular trace. The output should contain exactly
 	// one trace whose ID is equal to the one we asked for.
-	want, got = 1, binary.Start("vtrace", env.RootMT()+"/__debug/vtrace", traceId).Output()
+	want, got = 1, binary.Start("vtrace", "__debug/vtrace", traceId).Output()
 	if count := strings.Count(got, "Trace -"); count != want {
 		t.Fatalf("unexpected trace count, want %d, got %d\n%s", want, count, got)
 	}
@@ -216,11 +219,12 @@
 }
 
 func TestPprof(t *testing.T) {
-	env := integration.New(t)
+	env := v23tests.New(t)
 	defer env.Cleanup()
+	v23tests.RunRootMT(env, "--veyron.tcp.address=127.0.0.1:0")
 
 	binary := env.BuildGoPkg("v.io/core/veyron/tools/debug")
-	inv := binary.Start("pprof", "run", env.RootMT()+"/__debug/pprof", "heap", "--text")
+	inv := binary.Start("pprof", "run", "__debug/pprof", "heap", "--text")
 
 	// Assert that a profile indicating the heap size was written out.
 	want, got := "(.*) of (.*) total", inv.Output()
diff --git a/tools/mgmt/device/doc.go b/tools/mgmt/device/doc.go
index c8f4780..26d2164 100644
--- a/tools/mgmt/device/doc.go
+++ b/tools/mgmt/device/doc.go
@@ -91,14 +91,16 @@
 Install the given application, specified using a local path.
 
 Usage:
-   device install-local [flags] <device> <title> [ENV=VAL ...] binary [--flag=val ...]
+   device install-local [flags] <device> <title> [ENV=VAL ...] binary [--flag=val ...] [PACKAGES path ...]
 
 <device> is the veyron object name of the device manager's app service.
 
 <title> is the app title.
 
 This is followed by an arbitrary number of environment variable settings, the
-local path for the binary to install, and arbitrary flag settings.
+local path for the binary to install, and arbitrary flag settings and args.
+Optionally, this can be followed by 'PACKAGES' and a list of local files and
+directories to be installed as packages for the app
 
 The device install-local flags are:
  -config={}
diff --git a/tools/mgmt/device/impl/devicemanager_mock_test.go b/tools/mgmt/device/impl/devicemanager_mock_test.go
index d0da827..254d370 100644
--- a/tools/mgmt/device/impl/devicemanager_mock_test.go
+++ b/tools/mgmt/device/impl/devicemanager_mock_test.go
@@ -1,7 +1,11 @@
 package impl_test
 
 import (
+	"fmt"
+	"io/ioutil"
 	"log"
+	"os"
+	"path/filepath"
 	"testing"
 
 	"v.io/core/veyron2"
@@ -17,6 +21,7 @@
 	"v.io/core/veyron2/vlog"
 
 	binlib "v.io/core/veyron/services/mgmt/lib/binary"
+	"v.io/core/veyron/services/mgmt/lib/packages"
 )
 
 type mockDeviceInvoker struct {
@@ -79,11 +84,14 @@
 
 // Mock Install
 type InstallStimulus struct {
-	fun        string
-	appName    string
-	config     device.Config
-	envelope   application.Envelope
-	binarySize int64
+	fun      string
+	appName  string
+	config   device.Config
+	envelope application.Envelope
+	// files holds a map from file or package name to file or package size.
+	// The app binary has the key "binary". Each of the packages will have
+	// the key "package/<package name>".
+	files map[string]int64
 }
 
 type InstallResponse struct {
@@ -103,8 +111,28 @@
 	binaryNameAfterFetch = "binary-fetched"
 )
 
+func packageSize(pkgPath string) int64 {
+	info, err := os.Stat(pkgPath)
+	if err != nil {
+		return -1
+	}
+	if info.IsDir() {
+		infos, err := ioutil.ReadDir(pkgPath)
+		if err != nil {
+			return -1
+		}
+		var size int64
+		for _, i := range infos {
+			size += i.Size()
+		}
+		return size
+	} else {
+		return info.Size()
+	}
+}
+
 func (mni *mockDeviceInvoker) Install(call ipc.ServerContext, appName string, config device.Config) (string, error) {
-	is := InstallStimulus{"Install", appName, config, application.Envelope{}, 0}
+	is := InstallStimulus{"Install", appName, config, application.Envelope{}, nil}
 	if appName != appNameNoFetch {
 		// Fetch the envelope and record it in the stimulus.
 		envelope, err := repository.ApplicationClient(appName).Match(call.Context(), []string{"test"})
@@ -113,14 +141,38 @@
 		}
 		binaryName := envelope.Binary
 		envelope.Binary = binaryNameAfterFetch
-		is.envelope = envelope
 		is.appName = appNameAfterFetch
+		is.files = make(map[string]int64)
 		// Fetch the binary and record its size in the stimulus.
-		data, _, err := binlib.Download(call.Context(), binaryName)
+		data, mediaInfo, err := binlib.Download(call.Context(), binaryName)
 		if err != nil {
 			return "", err
 		}
-		is.binarySize = int64(len(data))
+		is.files["binary"] = int64(len(data))
+		if mediaInfo.Type != "application/octet-stream" {
+			return "", fmt.Errorf("unexpected media type: %v", mediaInfo)
+		}
+		// Iterate over the packages, download them, compute the size of
+		// the file(s) that make up each package, and record that in the
+		// stimulus.
+		for pkgLocalName, pkgVON := range envelope.Packages {
+			dir, err := ioutil.TempDir("", "package")
+			if err != nil {
+				return "", fmt.Errorf("failed to create temp package dir: %v", err)
+			}
+			defer os.RemoveAll(dir)
+			tmpFile := filepath.Join(dir, pkgLocalName)
+			if err := binlib.DownloadToFile(call.Context(), pkgVON, tmpFile); err != nil {
+				return "", fmt.Errorf("DownloadToFile failed: %v", err)
+			}
+			dst := filepath.Join(dir, "install")
+			if err := packages.Install(tmpFile, dst); err != nil {
+				return "", fmt.Errorf("packages.Install failed: %v", err)
+			}
+			is.files[naming.Join("packages", pkgLocalName)] = packageSize(dst)
+		}
+		envelope.Packages = nil
+		is.envelope = envelope
 	}
 	r := mni.tape.Record(is).(InstallResponse)
 	return r.appId, r.err
diff --git a/tools/mgmt/device/impl/impl_test.go b/tools/mgmt/device/impl/impl_test.go
index 09e0a83..3a14a76 100644
--- a/tools/mgmt/device/impl/impl_test.go
+++ b/tools/mgmt/device/impl/impl_test.go
@@ -215,14 +215,14 @@
 			nil,
 			false,
 			InstallResponse{appId, nil},
-			InstallStimulus{"Install", appNameNoFetch, nil, application.Envelope{}, 0},
+			InstallStimulus{"Install", appNameNoFetch, nil, application.Envelope{}, nil},
 		},
 		{
 			[]string{deviceName, appNameNoFetch},
 			cfg,
 			false,
 			InstallResponse{appId, nil},
-			InstallStimulus{"Install", appNameNoFetch, cfg, application.Envelope{}, 0},
+			InstallStimulus{"Install", appNameNoFetch, cfg, application.Envelope{}, nil},
 		},
 	} {
 		tape.SetResponses([]interface{}{c.tapeResponse})
diff --git a/tools/mgmt/device/impl/local_install.go b/tools/mgmt/device/impl/local_install.go
index 1598559..b6a926d 100644
--- a/tools/mgmt/device/impl/local_install.go
+++ b/tools/mgmt/device/impl/local_install.go
@@ -7,6 +7,7 @@
 	"io"
 	"io/ioutil"
 	"os"
+	"path/filepath"
 	"strings"
 
 	"v.io/core/veyron2"
@@ -20,6 +21,8 @@
 	"v.io/core/veyron2/services/mgmt/repository"
 	"v.io/core/veyron2/services/security/access"
 	"v.io/core/veyron2/uniqueid"
+
+	"v.io/core/veyron/services/mgmt/lib/packages"
 	"v.io/lib/cmdline"
 )
 
@@ -28,15 +31,16 @@
 	Name:     "install-local",
 	Short:    "Install the given application from the local system.",
 	Long:     "Install the given application, specified using a local path.",
-	ArgsName: "<device> <title> [ENV=VAL ...] binary [--flag=val ...]",
+	ArgsName: "<device> <title> [ENV=VAL ...] binary [--flag=val ...] [PACKAGES path ...]",
 	ArgsLong: `
 <device> is the veyron object name of the device manager's app service.
 
 <title> is the app title.
 
 This is followed by an arbitrary number of environment variable settings, the
-local path for the binary to install, and arbitrary flag settings.`,
-}
+local path for the binary to install, and arbitrary flag settings and args.
+Optionally, this can be followed by 'PACKAGES' and a list of local files and
+directories to be installed as packages for the app`}
 
 func init() {
 	cmdInstallLocal.Flags.Var(&configOverride, "config", "JSON-encoded device.Config object, of the form: '{\"flag1\":\"value1\",\"flag2\":\"value2\"}'")
@@ -152,7 +156,7 @@
 	}
 	h.Write(bytes)
 	part := binary.PartInfo{Checksum: hex.EncodeToString(h.Sum(nil)), Size: int64(len(bytes))}
-	return []binary.PartInfo{part}, repository.MediaInfo{Type: "application/octet-stream"}, nil
+	return []binary.PartInfo{part}, packages.MediaInfoForFileName(fileName), nil
 }
 
 func (binaryInvoker) Upload(repository.BinaryUploadContext, int32) error {
@@ -213,7 +217,17 @@
 		return cmd.UsageErrorf("install-local: missing binary")
 	}
 	binary := args[0]
-	envelope.Args = args[1:]
+	args = args[1:]
+	firstNonArg, firstPackage := len(args), len(args)
+	for i, arg := range args {
+		if arg == "PACKAGES" {
+			firstNonArg = i
+			firstPackage = i + 1
+			break
+		}
+	}
+	envelope.Args = args[:firstNonArg]
+	pkgs := args[firstPackage:]
 	if _, err := os.Stat(binary); err != nil {
 		return fmt.Errorf("binary %v not found: %v", binary, err)
 	}
@@ -225,6 +239,43 @@
 	defer cancel()
 	envelope.Binary = naming.Join(name, "binary")
 
+	// For each package dir/file specified in the arguments list, set up an
+	// object in the binary service to serve that package, and add the
+	// object name to the envelope's Packages map.
+	var tmpZipDir string
+	for _, p := range pkgs {
+		if envelope.Packages == nil {
+			envelope.Packages = make(map[string]string)
+		}
+		info, err := os.Stat(p)
+		if os.IsNotExist(err) {
+			return fmt.Errorf("%v not found: %v", p, err)
+		} else if err != nil {
+			return fmt.Errorf("Stat(%v) failed: %v", p, err)
+		}
+		pkgName := naming.Join("packages", info.Name())
+		if _, ok := objects[pkgName]; ok {
+			return fmt.Errorf("can't have more than one package with name %v", info.Name())
+		}
+		fileName := p
+		// Directory packages first get zip'ped.
+		if info.IsDir() {
+			if tmpZipDir == "" {
+				tmpZipDir, err = ioutil.TempDir("", "packages")
+				if err != nil {
+					return fmt.Errorf("failed to create a temp dir for zip packages: %v", err)
+				}
+				defer os.RemoveAll(tmpZipDir)
+			}
+			fileName = filepath.Join(tmpZipDir, info.Name()+".zip")
+			if err := packages.CreateZip(fileName, p); err != nil {
+				return err
+			}
+		}
+		objects[pkgName] = repository.BinaryServer(binaryInvoker(fileName))
+		envelope.Packages[info.Name()] = naming.Join(name, pkgName)
+	}
+
 	objects["application"] = repository.ApplicationServer(envelopeInvoker(envelope))
 	appName := naming.Join(name, "application")
 	appID, err := device.ApplicationClient(deviceName).Install(gctx, appName, device.Config(configOverride))
diff --git a/tools/mgmt/device/impl/local_install_test.go b/tools/mgmt/device/impl/local_install_test.go
index 1029ec1..4c8b5b5 100644
--- a/tools/mgmt/device/impl/local_install_test.go
+++ b/tools/mgmt/device/impl/local_install_test.go
@@ -4,7 +4,9 @@
 	"bytes"
 	"encoding/json"
 	"fmt"
+	"io/ioutil"
 	"os"
+	"path/filepath"
 	"reflect"
 	"strings"
 	"testing"
@@ -17,6 +19,12 @@
 	"v.io/core/veyron/tools/mgmt/device/impl"
 )
 
+func createFile(t *testing.T, path string, contents string) {
+	if err := ioutil.WriteFile(path, []byte(contents), 0700); err != nil {
+		t.Fatalf("Failed to create %v: %v", path, err)
+	}
+}
+
 func TestInstallLocalCommand(t *testing.T) {
 	shutdown := initTest()
 	defer shutdown()
@@ -32,7 +40,13 @@
 	var stdout, stderr bytes.Buffer
 	cmd.Init(nil, &stdout, &stderr)
 	deviceName := naming.JoinAddressName(endpoint.String(), "")
-	appTitle := "Appo di tutti Appi"
+	const appTitle = "Appo di tutti Appi"
+	binary := os.Args[0]
+	fi, err := os.Stat(binary)
+	if err != nil {
+		t.Fatalf("Failed to stat %v: %v", binary, err)
+	}
+	binarySize := fi.Size()
 	for i, c := range []struct {
 		args         []string
 		stderrSubstr string
@@ -49,6 +63,9 @@
 		{
 			[]string{deviceName, appTitle, "foo"}, "binary foo not found",
 		},
+		{
+			[]string{deviceName, appTitle, binary, "PACKAGES", "foo"}, "foo not found",
+		},
 	} {
 		c.args = append([]string{"install-local"}, c.args...)
 		if err := cmd.Execute(c.args); err == nil {
@@ -66,16 +83,27 @@
 		stdout.Reset()
 		stderr.Reset()
 	}
-	appId := "myBestAppID"
-	binary := os.Args[0]
-	fi, err := os.Stat(binary)
-	if err != nil {
-		t.Fatalf("Failed to stat %v: %v", binary, err)
-	}
 	emptySig := security.Signature{Purpose: []uint8{}, Hash: "", R: []uint8{}, S: []uint8{}}
 	emptyBlessings := security.WireBlessings{}
-	binarySize := fi.Size()
 	cfg := device.Config{"someflag": "somevalue"}
+
+	testPackagesDir, err := ioutil.TempDir("", "testdir")
+	if err != nil {
+		t.Fatalf("Failed to create temp dir: %v", err)
+	}
+	defer os.RemoveAll(testPackagesDir)
+	pkgFile1 := filepath.Join(testPackagesDir, "file1.txt")
+	createFile(t, pkgFile1, "1234567")
+	pkgFile2 := filepath.Join(testPackagesDir, "file2")
+	createFile(t, pkgFile2, string([]byte{0x01, 0x02, 0x03, 0x04}))
+	pkgDir := filepath.Join(testPackagesDir, "dir")
+	if err := os.Mkdir(pkgDir, 0700); err != nil {
+		t.Fatalf("Failed to create dir: %v", err)
+	}
+	createFile(t, filepath.Join(pkgDir, "f1"), "123")
+	createFile(t, filepath.Join(pkgDir, "f2"), "456")
+	createFile(t, filepath.Join(pkgDir, "f3"), "7890")
+
 	for i, c := range []struct {
 		args         []string
 		config       device.Config
@@ -84,19 +112,68 @@
 		{
 			[]string{deviceName, appTitle, binary},
 			nil,
-			InstallStimulus{"Install", appNameAfterFetch, nil, application.Envelope{Title: appTitle, Binary: binaryNameAfterFetch, Signature: emptySig, Publisher: emptyBlessings}, binarySize},
+			InstallStimulus{
+				"Install",
+				appNameAfterFetch,
+				nil,
+				application.Envelope{
+					Title:     appTitle,
+					Binary:    binaryNameAfterFetch,
+					Signature: emptySig,
+					Publisher: emptyBlessings,
+				},
+				map[string]int64{"binary": binarySize}},
 		},
 		{
 			[]string{deviceName, appTitle, binary},
 			cfg,
-			InstallStimulus{"Install", appNameAfterFetch, cfg, application.Envelope{Title: appTitle, Binary: binaryNameAfterFetch, Signature: emptySig, Publisher: emptyBlessings}, binarySize},
+			InstallStimulus{
+				"Install",
+				appNameAfterFetch,
+				cfg,
+				application.Envelope{
+					Title:     appTitle,
+					Binary:    binaryNameAfterFetch,
+					Signature: emptySig,
+					Publisher: emptyBlessings,
+				},
+				map[string]int64{"binary": binarySize}},
 		},
 		{
 			[]string{deviceName, appTitle, "ENV1=V1", "ENV2=V2", binary, "FLAG1=V1", "FLAG2=V2"},
 			nil,
-			InstallStimulus{"Install", appNameAfterFetch, nil, application.Envelope{Title: appTitle, Binary: binaryNameAfterFetch, Signature: emptySig, Publisher: emptyBlessings, Env: []string{"ENV1=V1", "ENV2=V2"}, Args: []string{"FLAG1=V1", "FLAG2=V2"}}, binarySize},
+			InstallStimulus{
+				"Install",
+				appNameAfterFetch,
+				nil,
+				application.Envelope{
+					Title:     appTitle,
+					Binary:    binaryNameAfterFetch,
+					Signature: emptySig,
+					Publisher: emptyBlessings,
+					Env:       []string{"ENV1=V1", "ENV2=V2"},
+					Args:      []string{"FLAG1=V1", "FLAG2=V2"},
+				},
+				map[string]int64{"binary": binarySize}},
+		},
+		{
+			[]string{deviceName, appTitle, "ENV=V", binary, "FLAG=V", "PACKAGES", pkgFile1, pkgFile2, pkgDir},
+			nil,
+			InstallStimulus{"Install",
+				appNameAfterFetch,
+				nil,
+				application.Envelope{
+					Title:     appTitle,
+					Binary:    binaryNameAfterFetch,
+					Signature: emptySig,
+					Publisher: emptyBlessings,
+					Env:       []string{"ENV=V"},
+					Args:      []string{"FLAG=V"},
+				},
+				map[string]int64{"binary": binarySize, "packages/file1.txt": 7, "packages/file2": 4, "packages/dir": 10}},
 		},
 	} {
+		const appId = "myBestAppID"
 		tape.SetResponses([]interface{}{InstallResponse{appId, nil}})
 		if c.config != nil {
 			jsonConfig, err := json.Marshal(c.config)
diff --git a/tools/namespace/impl.go b/tools/namespace/impl.go
index 53f73ae..a80902f 100644
--- a/tools/namespace/impl.go
+++ b/tools/namespace/impl.go
@@ -40,14 +40,14 @@
 		return err
 	}
 	for res := range c {
+		if res.Error != nil {
+			fmt.Fprintf(cmd.Stderr(), "Error: %s: %v\n", res.Name, res.Error)
+			continue
+		}
 		fmt.Fprint(cmd.Stdout(), res.Name)
 		for _, s := range res.Servers {
 			fmt.Fprintf(cmd.Stdout(), " %s (Expires %s)", s.Server, s.Expires)
 		}
-		if res.Error != nil {
-			fmt.Fprintln(cmd.Stdout())
-			fmt.Fprintf(cmd.Stdout(), "result error: %v", res.Error)
-		}
 		fmt.Fprintln(cmd.Stdout())
 	}
 	return nil
diff --git a/tools/naming/simulator/simulator_test.go b/tools/naming/simulator/simulator_test.go
index fae47ca..0e01268 100644
--- a/tools/naming/simulator/simulator_test.go
+++ b/tools/naming/simulator/simulator_test.go
@@ -11,13 +11,14 @@
 	"testing"
 
 	"v.io/core/veyron/lib/modules"
-	"v.io/core/veyron/lib/testutil/integration"
+	"v.io/core/veyron/lib/testutil/v23tests"
 )
 
 func TestHelperProcess(t *testing.T) {
 	modules.DispatchInTest()
 }
-func V23TestSimulator(t integration.T) {
+
+func V23TestSimulator(t v23tests.T) {
 	binary := t.BuildGoPkg("v.io/core/veyron/tools/naming/simulator")
 	files, err := ioutil.ReadDir("./testdata")
 	if err != nil {
diff --git a/tools/naming/simulator/testdata/integration_test.go b/tools/naming/simulator/testdata/integration_test.go
index 77c5ddb..12c8576 100644
--- a/tools/naming/simulator/testdata/integration_test.go
+++ b/tools/naming/simulator/testdata/integration_test.go
@@ -9,8 +9,7 @@
 	"testing"
 
 	"v.io/core/veyron/lib/modules"
-	"v.io/core/veyron/lib/testutil/integration"
-
+	"v.io/core/veyron/lib/testutil/v23tests"
 	_ "v.io/core/veyron/profiles/static"
 )
 
@@ -19,7 +18,7 @@
 }
 
 func TestSimulator(t *testing.T) {
-	env := integration.New(t)
+	env := v23tests.New(t)
 	defer env.Cleanup()
 	binary := env.BuildGoPkg("v.io/core/veyron/tools/naming/simulator")
 	files, err := ioutil.ReadDir(".")
diff --git a/tools/naming/simulator/v23_test.go b/tools/naming/simulator/v23_test.go
index dfde460..be63305 100644
--- a/tools/naming/simulator/v23_test.go
+++ b/tools/naming/simulator/v23_test.go
@@ -6,7 +6,7 @@
 import "os"
 
 import "v.io/core/veyron/lib/testutil"
-import "v.io/core/veyron/lib/testutil/integration"
+import "v.io/core/veyron/lib/testutil/v23tests"
 
 func TestMain(m *testing.M) {
 	testutil.Init()
@@ -15,5 +15,5 @@
 }
 
 func TestV23Simulator(t *testing.T) {
-	integration.RunTest(t, V23TestSimulator)
+	v23tests.RunTest(t, V23TestSimulator)
 }
diff --git a/tools/principal/principal_test.go b/tools/principal/principal_test.go
index 302a698..e38b017 100644
--- a/tools/principal/principal_test.go
+++ b/tools/principal/principal_test.go
@@ -8,7 +8,7 @@
 	"testing"
 
 	"v.io/core/veyron/lib/modules"
-	"v.io/core/veyron/lib/testutil/integration"
+	"v.io/core/veyron/lib/testutil/v23tests"
 )
 
 //go:generate v23 integration generate
@@ -18,13 +18,13 @@
 
 // redirect redirects the stdout of the given invocation to the file at the
 // given path.
-func redirect(t integration.T, inv integration.Invocation, path string) {
+func redirect(t v23tests.T, inv v23tests.Invocation, path string) {
 	if err := ioutil.WriteFile(path, []byte(inv.Output()), 0600); err != nil {
 		t.Fatalf("WriteFile(%q) failed: %v\n", path, err)
 	}
 }
 
-func V23TestBlessSelf(t integration.T) {
+func V23TestBlessSelf(t v23tests.T) {
 	var (
 		outputDir         = t.TempDir()
 		aliceDir          = filepath.Join(outputDir, "alice")
diff --git a/tools/principal/testdata/integration_test.go b/tools/principal/testdata/integration_test.go
index 030dd17..a1d454b 100644
--- a/tools/principal/testdata/integration_test.go
+++ b/tools/principal/testdata/integration_test.go
@@ -8,7 +8,7 @@
 	"testing"
 
 	"v.io/core/veyron/lib/modules"
-	"v.io/core/veyron/lib/testutil/integration"
+	"v.io/core/veyron/lib/testutil/v23tests"
 	_ "v.io/core/veyron/profiles"
 )
 
@@ -18,14 +18,14 @@
 
 // redirect redirects the stdout of the given invocation to the file at the
 // given path.
-func redirect(t *testing.T, inv integration.Invocation, path string) {
+func redirect(t *testing.T, inv v23tests.Invocation, path string) {
 	if err := ioutil.WriteFile(path, []byte(inv.Output()), 0600); err != nil {
 		t.Fatalf("WriteFile(%q) failed: %v\n", path, err)
 	}
 }
 
 func TestBlessSelf(t *testing.T) {
-	env := integration.New(t)
+	env := v23tests.New(t)
 	defer env.Cleanup()
 
 	var (
diff --git a/tools/principal/v23_test.go b/tools/principal/v23_test.go
index f421ae1..4e12189 100644
--- a/tools/principal/v23_test.go
+++ b/tools/principal/v23_test.go
@@ -6,7 +6,7 @@
 import "os"
 
 import "v.io/core/veyron/lib/testutil"
-import "v.io/core/veyron/lib/testutil/integration"
+import "v.io/core/veyron/lib/testutil/v23tests"
 
 func TestMain(m *testing.M) {
 	testutil.Init()
@@ -15,5 +15,5 @@
 }
 
 func TestV23BlessSelf(t *testing.T) {
-	integration.RunTest(t, V23TestBlessSelf)
+	v23tests.RunTest(t, V23TestBlessSelf)
 }