services/device/internal/impl: shard device unit tests (2)

This is CL 2 of n to shard device manager unit tests into multiple
files and packages to permit concurrent test execution. This CL moves
module bodies into a utility package so that they can be called from
different test packages.

Change-Id: If936d109768adcccc3db28983dadb290b6411351
diff --git a/services/device/internal/impl/debug_perms_test.go b/services/device/internal/impl/debug_perms_test.go
index bd9c18a..7bba065 100644
--- a/services/device/internal/impl/debug_perms_test.go
+++ b/services/device/internal/impl/debug_perms_test.go
@@ -49,7 +49,7 @@
 	utiltest.ClaimDevice(t, ctx, "claimable", "dm", "mydevice", noPairingToken)
 
 	// Create the local server that the app uses to let us know it's ready.
-	pingCh, cleanup := setupPingServer(t, ctx)
+	pingCh, cleanup := utiltest.SetupPingServer(t, ctx)
 	defer cleanup()
 	utiltest.Resolve(t, ctx, "pingserver", 1)
 
@@ -72,7 +72,7 @@
 
 	// Bob starts an instance of the app.
 	bobApp := utiltest.LaunchApp(t, bobCtx, appID)
-	verifyPingArgs(t, pingCh, userName(t), "default", "")
+	pingCh.VerifyPingArgs(t, userName(t), "default", "")
 
 	// Bob permits Alice to read from his app.
 	updateAccessList(t, bobCtx, "root/alice/$", string(access.Read), "dm/apps", appID, bobApp)
diff --git a/services/device/internal/impl/impl_test.go b/services/device/internal/impl/impl_test.go
index ed14858..201a844 100644
--- a/services/device/internal/impl/impl_test.go
+++ b/services/device/internal/impl/impl_test.go
@@ -13,13 +13,11 @@
 	"crypto/md5"
 	"encoding/base64"
 	"encoding/hex"
-	"encoding/json"
 	"flag"
 	"fmt"
 	"io"
 	"io/ioutil"
 	"os"
-	goexec "os/exec"
 	"os/user"
 	"path"
 	"path/filepath"
@@ -34,7 +32,6 @@
 	"v.io/v23"
 	"v.io/v23/context"
 	"v.io/v23/naming"
-	"v.io/v23/rpc"
 	"v.io/v23/security"
 	"v.io/v23/security/access"
 	"v.io/v23/services/application"
@@ -44,11 +41,9 @@
 
 	"v.io/x/ref/envvar"
 	"v.io/x/ref/lib/mgmt"
-	"v.io/x/ref/lib/signals"
 	"v.io/x/ref/services/device/internal/config"
 	"v.io/x/ref/services/device/internal/impl"
 	"v.io/x/ref/services/device/internal/impl/utiltest"
-	"v.io/x/ref/services/device/internal/starter"
 	"v.io/x/ref/services/device/internal/suid"
 	"v.io/x/ref/services/internal/binarylib"
 	"v.io/x/ref/services/internal/servicetest"
@@ -74,8 +69,6 @@
 	// V23 prefix is necessary to pass the env filtering.
 	testEnvVarName = "V23_RANDOM_ENV_VALUE"
 
-	redirectEnv = "DEVICE_MANAGER_DONT_REDIRECT_STDOUT_STDERR"
-
 	noPairingToken = ""
 )
 
@@ -114,206 +107,23 @@
 
 // execScript launches the script passed as argument.
 func execScript(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
-	if want, got := 1, len(args); want != got {
-		vlog.Fatalf("execScript expected %d arguments, got %d instead", want, got)
-	}
-	script := args[0]
-	osenv := []string{redirectEnv + "=1"}
-	if env["PAUSE_BEFORE_STOP"] == "1" {
-		osenv = append(osenv, "PAUSE_BEFORE_STOP=1")
-	}
-
-	cmd := goexec.Cmd{
-		Path:   script,
-		Env:    osenv,
-		Stdin:  stdin,
-		Stderr: stderr,
-		Stdout: stdout,
-	}
-
-	return cmd.Run()
+	return utiltest.ExecScript(stdin, stdout, stderr, env, args...)
 }
 
 // deviceManager sets up a device manager server.  It accepts the name to
 // publish the server under as an argument.  Additional arguments can optionally
 // specify device manager config settings.
 func deviceManager(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
-	ctx, shutdown := test.InitForTest()
-	if len(args) == 0 {
-		vlog.Fatalf("deviceManager expected at least an argument")
-	}
-	publishName := args[0]
-	args = args[1:]
-	defer fmt.Fprintf(stdout, "%v terminated\n", publishName)
-	defer vlog.VI(1).Infof("%v terminated", publishName)
-	defer shutdown()
-	v23.GetNamespace(ctx).CacheCtl(naming.DisableCache(true))
-
-	// Satisfy the contract described in doc.go by passing the config state
-	// through to the device manager dispatcher constructor.
-	configState, err := config.Load()
-	if err != nil {
-		vlog.Fatalf("Failed to decode config state: %v", err)
-	}
-
-	// This exemplifies how to override or set specific config fields, if,
-	// for example, the device manager is invoked 'by hand' instead of via a
-	// script prepared by a previous version of the device manager.
-	var pairingToken string
-	if len(args) > 0 {
-		if want, got := 4, len(args); want > got {
-			vlog.Fatalf("expected atleast %d additional arguments, got %d instead: %q", want, got, args)
-		}
-		configState.Root, configState.Helper, configState.Origin, configState.CurrentLink = args[0], args[1], args[2], args[3]
-		if len(args) > 4 {
-			pairingToken = args[4]
-		}
-	}
-	// We grab the shutdown channel at this point in order to ensure that we
-	// register a listener for the app cycle manager Stop before we start
-	// running the device manager service.  Otherwise, any device manager
-	// method that calls Stop on the app cycle manager (e.g. the Stop RPC)
-	// will precipitate an immediate process exit.
-	shutdownChan := signals.ShutdownOnSignals(ctx)
-	claimableName, stop, err := starter.Start(ctx, starter.Args{
-		Namespace: starter.NamespaceArgs{
-			ListenSpec: rpc.ListenSpec{Addrs: rpc.ListenAddrs{{"tcp", "127.0.0.1:0"}}},
-		},
-		Device: starter.DeviceArgs{
-			Name:            publishName,
-			ListenSpec:      rpc.ListenSpec{Addrs: rpc.ListenAddrs{{"tcp", "127.0.0.1:0"}}},
-			ConfigState:     configState,
-			TestMode:        strings.HasSuffix(fmt.Sprint(v23.GetPrincipal(ctx).BlessingStore().Default()), "/testdm"),
-			RestartCallback: func() { fmt.Println("restart handler") },
-			PairingToken:    pairingToken,
-		},
-		// TODO(rthellend): Wire up the local mounttable like the real device
-		// manager, i.e. mount the device manager and the apps on it, and mount
-		// the local mounttable in the global namespace.
-		// MountGlobalNamespaceInLocalNamespace: true,
-	})
-	if err != nil {
-		vlog.Errorf("starter.Start failed: %v", err)
-		return err
-	}
-	defer stop()
-	// Update the namespace roots to remove the server blessing from the
-	// endpoints.  This is needed to be able to publish into the 'global'
-	// mounttable before we have compatible credentials.
-	ctx, err = utiltest.SetNamespaceRootsForUnclaimedDevice(ctx)
-	if err != nil {
-		return err
-	}
-	// Manually mount the claimable service in the 'global' mounttable.
-	v23.GetNamespace(ctx).Mount(ctx, "claimable", claimableName, 0)
-	fmt.Fprintf(stdout, "ready:%d\n", os.Getpid())
-
-	<-shutdownChan
-	if val, present := env["PAUSE_BEFORE_STOP"]; present && val == "1" {
-		modules.WaitForEOF(stdin)
-	}
-	// TODO(ashankar): Figure out a way to incorporate this check in the test.
-	// if impl.DispatcherLeaking(dispatcher) {
-	//	vlog.Fatalf("device manager leaking resources")
-	// }
-	return nil
+	return utiltest.DeviceManager(stdin, stdout, stderr, env, args...)
 }
 
 // This is the same as deviceManager above, except that it has a different major version number
 func deviceManagerV10(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
-	impl.CurrentVersion = impl.Version{10, 0} // Set the version number to 10.0
-	return deviceManager(stdin, stdout, stderr, env, args...)
-}
-
-// appService defines a test service that the test app should be running.
-// TODO(caprita): Use this to make calls to the app and verify how Kill
-// interacts with an active service.
-type appService struct{}
-
-func (appService) Echo(_ *context.T, _ rpc.ServerCall, message string) (string, error) {
-	return message, nil
-}
-
-func (appService) Cat(_ *context.T, _ rpc.ServerCall, file string) (string, error) {
-	if file == "" || file[0] == filepath.Separator || file[0] == '.' {
-		return "", fmt.Errorf("illegal file name: %q", file)
-	}
-	bytes, err := ioutil.ReadFile(file)
-	if err != nil {
-		return "", err
-	}
-	return string(bytes), nil
-}
-
-type pingArgs struct {
-	Username, FlagValue, EnvValue string
-	Pid                           int
-}
-
-func ping(ctx *context.T) {
-	helperEnv := os.Getenv(suid.SavedArgs)
-	d := json.NewDecoder(strings.NewReader(helperEnv))
-	var savedArgs suid.ArgsSavedForTest
-	if err := d.Decode(&savedArgs); err != nil {
-		vlog.Fatalf("Failed to decode preserved argument %v: %v", helperEnv, err)
-	}
-	args := &pingArgs{
-		// TODO(rjkroege): Consider validating additional parameters
-		// from helper.
-		Username:  savedArgs.Uname,
-		FlagValue: *flagValue,
-		EnvValue:  os.Getenv(testEnvVarName),
-		Pid:       os.Getpid(),
-	}
-	client := v23.GetClient(ctx)
-	if call, err := client.StartCall(ctx, "pingserver", "Ping", []interface{}{args}); err != nil {
-		vlog.Fatalf("StartCall failed: %v", err)
-	} else if err := call.Finish(); err != nil {
-		vlog.Fatalf("Finish failed: %v", err)
-	}
-}
-
-func cat(ctx *context.T, name, file string) (string, error) {
-	ctx, cancel := context.WithTimeout(ctx, time.Minute)
-	defer cancel()
-	client := v23.GetClient(ctx)
-	call, err := client.StartCall(ctx, name, "Cat", []interface{}{file})
-	if err != nil {
-		return "", err
-	}
-	var content string
-	if err := call.Finish(&content); err != nil {
-		return "", err
-	}
-	return content, nil
+	return utiltest.DeviceManagerV10(stdin, stdout, stderr, env, args...)
 }
 
 func app(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
-	ctx, shutdown := test.InitForTest()
-	defer shutdown()
-
-	v23.GetNamespace(ctx).CacheCtl(naming.DisableCache(true))
-
-	if expected, got := 1, len(args); expected != got {
-		vlog.Fatalf("Unexpected number of arguments: expected %d, got %d", expected, got)
-	}
-	publishName := args[0]
-
-	server, _ := servicetest.NewServer(ctx)
-	defer server.Stop()
-	if err := server.Serve(publishName, new(appService), nil); err != nil {
-		vlog.Fatalf("Serve(%v) failed: %v", publishName, err)
-	}
-	// Some of our tests look for log files, so make sure they are flushed
-	// to ensure that at least the files exist.
-	vlog.FlushLog()
-	ping(ctx)
-
-	<-signals.ShutdownOnSignals(ctx)
-	if err := ioutil.WriteFile("testfile", []byte("goodbye world"), 0600); err != nil {
-		vlog.Fatalf("Failed to write testfile: %v", err)
-	}
-	return nil
+	return utiltest.App(stdin, stdout, stderr, env, flagValue, args...)
 }
 
 // Same as app, except that it does not exit properly after being stopped
@@ -557,32 +367,6 @@
 	dmh.ExpectEOF()
 }
 
-type pingServer chan<- pingArgs
-
-// TODO(caprita): Set the timeout in a more principled manner.
-const pingTimeout = 60 * time.Second
-
-func (p pingServer) Ping(_ *context.T, _ rpc.ServerCall, arg pingArgs) error {
-	p <- arg
-	return nil
-}
-
-// setupPingServer creates a server listening for a ping from a child app; it
-// returns a channel on which the app's ping message is returned, and a cleanup
-// function.
-func setupPingServer(t *testing.T, ctx *context.T) (<-chan pingArgs, func()) {
-	server, _ := servicetest.NewServer(ctx)
-	pingCh := make(chan pingArgs, 1)
-	if err := server.Serve("pingserver", pingServer(pingCh), security.AllowEveryone()); err != nil {
-		t.Fatalf("Serve(%q, <dispatcher>) failed: %v", "pingserver", err)
-	}
-	return pingCh, func() {
-		if err := server.Stop(); err != nil {
-			t.Fatalf("Stop() failed: %v", err)
-		}
-	}
-}
-
 func instanceDirForApp(root, appID, instanceID string) string {
 	applicationDirName := func(title string) string {
 		h := md5.New()
@@ -612,29 +396,6 @@
 	// END HACK
 }
 
-func receivePingArgs(t *testing.T, pingCh <-chan pingArgs) pingArgs {
-	var args pingArgs
-	select {
-	case args = <-pingCh:
-	case <-time.After(pingTimeout):
-		t.Fatalf(testutil.FormatLogLine(2, "failed to get ping"))
-	}
-	return args
-}
-
-func verifyPingArgs(t *testing.T, pingCh <-chan pingArgs, username, flagValue, envValue string) {
-	args := receivePingArgs(t, pingCh)
-	wantArgs := pingArgs{
-		Username:  username,
-		FlagValue: flagValue,
-		EnvValue:  envValue,
-		Pid:       args.Pid, // We are not checking for a value of Pid
-	}
-	if !reflect.DeepEqual(args, wantArgs) {
-		t.Fatalf(testutil.FormatLogLine(2, "got ping args %q, expected %q", args, wantArgs))
-	}
-}
-
 // TestLifeOfAnApp installs an app, instantiates, runs, kills, and deletes
 // several instances, and performs updates.
 func TestLifeOfAnApp(t *testing.T) {
@@ -664,13 +425,13 @@
 	utiltest.ClaimDevice(t, ctx, "claimable", "dm", "mydevice", noPairingToken)
 
 	// Create the local server that the app uses to let us know it's ready.
-	pingCh, cleanup := setupPingServer(t, ctx)
+	pingCh, cleanup := utiltest.SetupPingServer(t, ctx)
 	defer cleanup()
 
 	utiltest.Resolve(t, ctx, "pingserver", 1)
 
 	// Create an envelope for a first version of the app.
-	*envelope = utiltest.EnvelopeFromShell(sh, []string{testEnvVarName + "=env-val-envelope"}, appCmd, "google naps", fmt.Sprintf("--%s=flag-val-envelope", testFlagName), "appV1")
+	*envelope = utiltest.EnvelopeFromShell(sh, []string{utiltest.TestEnvVarName + "=env-val-envelope"}, appCmd, "google naps", fmt.Sprintf("--%s=flag-val-envelope", testFlagName), "appV1")
 
 	// Install the app.  The config-specified flag value for testFlagName
 	// should override the value specified in the envelope above, and the
@@ -718,7 +479,7 @@
 	}
 
 	// Wait until the app pings us that it's ready.
-	verifyPingArgs(t, pingCh, userName(t), "flag-val-install", "env-val-envelope")
+	pingCh.VerifyPingArgs(t, userName(t), "flag-val-install", "env-val-envelope")
 
 	v1EP1 := utiltest.Resolve(t, ctx, "appV1", 1)[0]
 
@@ -729,7 +490,7 @@
 
 	utiltest.RunApp(t, ctx, appID, instance1ID)
 	utiltest.VerifyState(t, ctx, device.InstanceStateRunning, appID, instance1ID)
-	verifyPingArgs(t, pingCh, userName(t), "flag-val-install", "env-val-envelope") // Wait until the app pings us that it's ready.
+	pingCh.VerifyPingArgs(t, userName(t), "flag-val-install", "env-val-envelope") // Wait until the app pings us that it's ready.
 	oldV1EP1 := v1EP1
 	if v1EP1 = utiltest.Resolve(t, ctx, "appV1", 1)[0]; v1EP1 == oldV1EP1 {
 		t.Fatalf("Expected a new endpoint for the app after kill/run")
@@ -737,7 +498,7 @@
 
 	// Start a second instance.
 	instance2ID := utiltest.LaunchApp(t, ctx, appID)
-	verifyPingArgs(t, pingCh, userName(t), "flag-val-install", "env-val-envelope") // Wait until the app pings us that it's ready.
+	pingCh.VerifyPingArgs(t, userName(t), "flag-val-install", "env-val-envelope") // Wait until the app pings us that it's ready.
 
 	// There should be two endpoints mounted as "appV1", one for each
 	// instance of the app.
@@ -771,7 +532,7 @@
 	utiltest.UpdateAppExpectError(t, ctx, appID, impl.ErrAppTitleMismatch.ID)
 
 	// Create a second version of the app and update the app to it.
-	*envelope = utiltest.EnvelopeFromShell(sh, []string{testEnvVarName + "=env-val-envelope"}, appCmd, "google naps", "appV2")
+	*envelope = utiltest.EnvelopeFromShell(sh, []string{utiltest.TestEnvVarName + "=env-val-envelope"}, appCmd, "google naps", "appV2")
 
 	utiltest.UpdateApp(t, ctx, appID)
 
@@ -793,7 +554,7 @@
 	if v := utiltest.VerifyState(t, ctx, device.InstanceStateRunning, appID, instance1ID); v != v1 {
 		t.Fatalf("Instance version expected to be %v, got %v instead", v1, v)
 	}
-	verifyPingArgs(t, pingCh, userName(t), "flag-val-install", "env-val-envelope") // Wait until the app pings us that it's ready.
+	pingCh.VerifyPingArgs(t, userName(t), "flag-val-install", "env-val-envelope") // Wait until the app pings us that it's ready.
 	// Both instances should still be running the first version of the app.
 	// Check that the mounttable contains two endpoints, one of which is
 	// v1EP2.
@@ -821,7 +582,7 @@
 	}
 	// Resume the first instance and verify it's running v2 now.
 	utiltest.RunApp(t, ctx, appID, instance1ID)
-	verifyPingArgs(t, pingCh, userName(t), "flag-val-install", "env-val-envelope")
+	pingCh.VerifyPingArgs(t, userName(t), "flag-val-install", "env-val-envelope")
 	utiltest.Resolve(t, ctx, "appV1", 1)
 	utiltest.Resolve(t, ctx, "appV2", 1)
 
@@ -836,7 +597,7 @@
 		t.Fatalf("Instance version expected to be %v, got %v instead", v2, v)
 	}
 	// Wait until the app pings us that it's ready.
-	verifyPingArgs(t, pingCh, userName(t), "flag-val-install", "env-val-envelope")
+	pingCh.VerifyPingArgs(t, userName(t), "flag-val-install", "env-val-envelope")
 
 	utiltest.Resolve(t, ctx, "appV2", 1)
 
@@ -859,7 +620,7 @@
 	if v := utiltest.VerifyState(t, ctx, device.InstanceStateRunning, appID, instance4ID); v != v1 {
 		t.Fatalf("Instance version expected to be %v, got %v instead", v1, v)
 	}
-	verifyPingArgs(t, pingCh, userName(t), "flag-val-install", "env-val-envelope") // Wait until the app pings us that it's ready.
+	pingCh.VerifyPingArgs(t, userName(t), "flag-val-install", "env-val-envelope") // Wait until the app pings us that it's ready.
 	utiltest.Resolve(t, ctx, "appV1", 1)
 	utiltest.TerminateApp(t, ctx, appID, instance4ID)
 	utiltest.ResolveExpectNotFound(t, ctx, "appV1")
@@ -887,7 +648,7 @@
 	*envelope = utiltest.EnvelopeFromShell(sh, nil, hangingAppCmd, "hanging ap", "hAppV1")
 	hAppID := utiltest.InstallApp(t, ctx)
 	hInstanceID := utiltest.LaunchApp(t, ctx, hAppID)
-	hangingPid := receivePingArgs(t, pingCh).Pid
+	hangingPid := pingCh.WaitForPingArgs(t).Pid
 	if err := syscall.Kill(hangingPid, 0); err != nil && err != syscall.EPERM {
 		t.Fatalf("Pid of hanging app (%v) is not live", hangingPid)
 	}
@@ -1010,18 +771,14 @@
 	utiltest.InstallAppExpectError(t, octx, verror.ErrNoAccess.ID)
 
 	// Create the local server that the app uses to let us know it's ready.
-	pingCh, cleanup := setupPingServer(t, claimantCtx)
+	pingCh, cleanup := utiltest.SetupPingServer(t, claimantCtx)
 	defer cleanup()
 
 	// Start an instance of the app.
 	instanceID := utiltest.LaunchApp(t, claimantCtx, appID)
 
 	// Wait until the app pings us that it's ready.
-	select {
-	case <-pingCh:
-	case <-time.After(pingTimeout):
-		t.Fatalf("failed to get ping")
-	}
+	pingCh.WaitForPingArgs(t)
 	utiltest.Resolve(t, ctx, "trapp", 1)
 	utiltest.KillApp(t, claimantCtx, appID, instanceID)
 
@@ -1154,8 +911,8 @@
 	utiltest.ResolveExpectNotFound(t, ctx, "dm")
 	// Start the device manager.
 	stdout := make(simpleRW, 100)
-	defer os.Setenv(redirectEnv, os.Getenv(redirectEnv))
-	os.Setenv(redirectEnv, "1")
+	defer os.Setenv(utiltest.RedirectEnv, os.Getenv(utiltest.RedirectEnv))
+	os.Setenv(utiltest.RedirectEnv, "1")
 	if err := impl.Start(dmDir, os.Stderr, stdout); err != nil {
 		t.Fatalf("Start failed: %v", err)
 	}
@@ -1211,7 +968,7 @@
 	defer syscall.Kill(pid, syscall.SIGINT)
 
 	// Create the local server that the app uses to let us know it's ready.
-	pingCh, cleanup := setupPingServer(t, ctx)
+	pingCh, cleanup := utiltest.SetupPingServer(t, ctx)
 	defer cleanup()
 
 	// Create the envelope for the first version of the app.
@@ -1228,11 +985,7 @@
 	defer utiltest.TerminateApp(t, ctx, appID, instance1ID)
 
 	// Wait until the app pings us that it's ready.
-	select {
-	case <-pingCh:
-	case <-time.After(pingTimeout):
-		t.Fatalf("failed to get ping")
-	}
+	pingCh.WaitForPingArgs(t)
 
 	app2ID := utiltest.InstallApp(t, ctx)
 	install2ID := path.Base(app2ID)
@@ -1344,7 +1097,7 @@
 	defer utiltest.VerifyNoRunningProcesses(t)
 
 	// Create the local server that the app uses to let us know it's ready.
-	pingCh, cleanup := setupPingServer(t, ctx)
+	pingCh, cleanup := utiltest.SetupPingServer(t, ctx)
 	defer cleanup()
 
 	// Create the envelope for the first version of the app.
@@ -1382,11 +1135,7 @@
 	defer utiltest.TerminateApp(t, ctx, appID, instance1ID)
 
 	// Wait until the app pings us that it's ready.
-	select {
-	case <-pingCh:
-	case <-time.After(pingTimeout):
-		t.Fatalf("failed to get ping")
-	}
+	pingCh.WaitForPingArgs(t)
 
 	for _, c := range []struct {
 		path, content string
@@ -1411,9 +1160,9 @@
 		// Ask the app to cat the file.
 		file := filepath.Join("packages", c.path)
 		name := "appV1"
-		content, err := cat(ctx, name, file)
+		content, err := utiltest.Cat(ctx, name, file)
 		if err != nil {
-			t.Errorf("cat(%q, %q) failed: %v", name, file, err)
+			t.Errorf("utiltest.Cat(%q, %q) failed: %v", name, file, err)
 		}
 		if expected := c.content; content != expected {
 			t.Errorf("unexpected content: expected %q, got %q", expected, content)
@@ -1568,11 +1317,11 @@
 
 	// Create the local server that the app uses to tell us which system
 	// name the device manager wished to run it as.
-	pingCh, cleanup := setupPingServer(t, ctx)
+	pingCh, cleanup := utiltest.SetupPingServer(t, ctx)
 	defer cleanup()
 
 	// Create an envelope for a first version of the app.
-	*envelope = utiltest.EnvelopeFromShell(sh, []string{testEnvVarName + "=env-var"}, appCmd, "google naps", fmt.Sprintf("--%s=flag-val-envelope", testFlagName), "appV1")
+	*envelope = utiltest.EnvelopeFromShell(sh, []string{utiltest.TestEnvVarName + "=env-var"}, appCmd, "google naps", fmt.Sprintf("--%s=flag-val-envelope", testFlagName), "appV1")
 
 	// Install and start the app as root/self.
 	appID := utiltest.InstallApp(t, selfCtx)
@@ -1600,7 +1349,7 @@
 	}
 
 	instance1ID := utiltest.LaunchApp(t, selfCtx, appID)
-	verifyPingArgs(t, pingCh, testUserName, "flag-val-envelope", "env-var") // Wait until the app pings us that it's ready.
+	pingCh.VerifyPingArgs(t, testUserName, "flag-val-envelope", "env-var") // Wait until the app pings us that it's ready.
 	utiltest.TerminateApp(t, selfCtx, appID, instance1ID)
 
 	vlog.VI(2).Infof("other attempting to run an app without access. Should fail.")
@@ -1640,7 +1389,7 @@
 
 	vlog.VI(2).Infof("other attempting to run an app with access. Should succeed.")
 	instance2ID := utiltest.LaunchApp(t, otherCtx, appID)
-	verifyPingArgs(t, pingCh, testUserName, "flag-val-envelope", "env-var") // Wait until the app pings us that it's ready.
+	pingCh.VerifyPingArgs(t, testUserName, "flag-val-envelope", "env-var") // Wait until the app pings us that it's ready.
 
 	vlog.VI(2).Infof("Validate that created instance has the right permissions.")
 	expected = make(access.Permissions)
@@ -1660,7 +1409,7 @@
 
 	vlog.VI(2).Infof("Verify that Run with the same systemName works.")
 	utiltest.RunApp(t, otherCtx, appID, instance2ID)
-	verifyPingArgs(t, pingCh, testUserName, "flag-val-envelope", "env-var") // Wait until the app pings us that it's ready.
+	pingCh.VerifyPingArgs(t, testUserName, "flag-val-envelope", "env-var") // Wait until the app pings us that it's ready.
 	utiltest.KillApp(t, otherCtx, appID, instance2ID)
 
 	vlog.VI(2).Infof("Verify that other can install and run applications.")
@@ -1668,7 +1417,7 @@
 
 	vlog.VI(2).Infof("other attempting to run an app that other installed. Should succeed.")
 	instance4ID := utiltest.LaunchApp(t, otherCtx, otherAppID)
-	verifyPingArgs(t, pingCh, testUserName, "flag-val-envelope", "env-var") // Wait until the app pings us that it's ready.
+	pingCh.VerifyPingArgs(t, testUserName, "flag-val-envelope", "env-var") // Wait until the app pings us that it's ready.
 
 	// Clean up.
 	utiltest.TerminateApp(t, otherCtx, otherAppID, instance4ID)
@@ -1686,7 +1435,7 @@
 
 	vlog.VI(2).Infof("Show that Start with different systemName works.")
 	instance3ID := utiltest.LaunchApp(t, otherCtx, appID)
-	verifyPingArgs(t, pingCh, anotherTestUserName, "flag-val-envelope", "env-var") // Wait until the app pings us that it's ready.
+	pingCh.VerifyPingArgs(t, anotherTestUserName, "flag-val-envelope", "env-var") // Wait until the app pings us that it's ready.
 
 	// Clean up.
 	utiltest.TerminateApp(t, otherCtx, appID, instance3ID)
diff --git a/services/device/internal/impl/instance_reaping_test.go b/services/device/internal/impl/instance_reaping_test.go
index 4b34b81..a563484 100644
--- a/services/device/internal/impl/instance_reaping_test.go
+++ b/services/device/internal/impl/instance_reaping_test.go
@@ -34,7 +34,7 @@
 	utiltest.ClaimDevice(t, ctx, "claimable", "dm", "mydevice", noPairingToken)
 
 	// Create the local server that the app uses to let us know it's ready.
-	pingCh, cleanup := setupPingServer(t, ctx)
+	pingCh, cleanup := utiltest.SetupPingServer(t, ctx)
 	defer cleanup()
 
 	utiltest.Resolve(t, ctx, "pingserver", 1)
@@ -50,7 +50,7 @@
 	instance1ID := utiltest.LaunchApp(t, ctx, appID)
 
 	// Wait until the app pings us that it's ready.
-	verifyPingArgs(t, pingCh, userName(t), "default", "")
+	pingCh.VerifyPingArgs(t, userName(t), "default", "")
 
 	// Get application pid.
 	name := naming.Join("dm", "apps/"+appID+"/"+instance1ID+"/stats/system/pid")
@@ -69,7 +69,7 @@
 
 	// Start a second instance of the app which will force polling to happen.
 	instance2ID := utiltest.LaunchApp(t, ctx, appID)
-	verifyPingArgs(t, pingCh, userName(t), "default", "")
+	pingCh.VerifyPingArgs(t, userName(t), "default", "")
 
 	utiltest.VerifyState(t, ctx, device.InstanceStateRunning, appID, instance2ID)
 
@@ -114,7 +114,7 @@
 	utiltest.ClaimDevice(t, ctx, "claimable", "dm", "mydevice", noPairingToken)
 
 	// Create the local server that the app uses to let us know it's ready.
-	pingCh, cleanup := setupPingServer(t, ctx)
+	pingCh, cleanup := utiltest.SetupPingServer(t, ctx)
 	defer cleanup()
 	utiltest.Resolve(t, ctx, "pingserver", 1)
 
@@ -128,7 +128,7 @@
 	instances := make([]string, 3)
 	for i, _ := range instances {
 		instances[i] = utiltest.LaunchApp(t, ctx, appID)
-		verifyPingArgs(t, pingCh, userName(t), "default", "")
+		pingCh.VerifyPingArgs(t, userName(t), "default", "")
 	}
 
 	// Get pid of instance[0]
@@ -168,7 +168,7 @@
 	// Start instance[0] over-again to show that an app marked not running
 	// by reconciliation can be restarted.
 	utiltest.RunApp(t, ctx, appID, instances[0])
-	verifyPingArgs(t, pingCh, userName(t), "default", "")
+	pingCh.VerifyPingArgs(t, userName(t), "default", "")
 
 	// Kill instance[1]
 	pid = getPid(t, ctx, appID, instances[1])
@@ -177,7 +177,7 @@
 	// Make a fourth instance. This forces a polling of processes so that
 	// the state is updated.
 	instances = append(instances, utiltest.LaunchApp(t, ctx, appID))
-	verifyPingArgs(t, pingCh, userName(t), "default", "")
+	pingCh.VerifyPingArgs(t, userName(t), "default", "")
 
 	// Stop the fourth instance to make sure that there's no way we could
 	// still be running the polling loop before doing the below.
diff --git a/services/device/internal/impl/utiltest/app.go b/services/device/internal/impl/utiltest/app.go
new file mode 100644
index 0000000..2b7ec13
--- /dev/null
+++ b/services/device/internal/impl/utiltest/app.go
@@ -0,0 +1,181 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package utiltest
+
+import (
+	"encoding/json"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"reflect"
+	"strings"
+	"testing"
+	"time"
+
+	"v.io/v23"
+	"v.io/v23/context"
+	"v.io/v23/naming"
+	"v.io/v23/rpc"
+	"v.io/v23/security"
+
+	"v.io/x/lib/vlog"
+
+	"v.io/x/ref/lib/signals"
+	"v.io/x/ref/services/device/internal/suid"
+	"v.io/x/ref/services/internal/servicetest"
+	"v.io/x/ref/test"
+	"v.io/x/ref/test/testutil"
+)
+
+// appService defines a test service that the test app should be running.
+// TODO(caprita): Use this to make calls to the app and verify how Kill
+// interacts with an active service.
+type appService struct{}
+
+func (appService) Echo(_ *context.T, _ rpc.ServerCall, message string) (string, error) {
+	return message, nil
+}
+
+func (appService) Cat(_ *context.T, _ rpc.ServerCall, file string) (string, error) {
+	if file == "" || file[0] == filepath.Separator || file[0] == '.' {
+		return "", fmt.Errorf("illegal file name: %q", file)
+	}
+	bytes, err := ioutil.ReadFile(file)
+	if err != nil {
+		return "", err
+	}
+	return string(bytes), nil
+}
+
+type PingArgs struct {
+	Username, FlagValue, EnvValue string
+	Pid                           int
+}
+
+// ping makes a RPC from the App back to the invoking device manager
+// carrying a PingArgs instance.
+func ping(ctx *context.T, flagValue string) {
+
+	vlog.Errorf("ping flagValue: %s", flagValue)
+
+	helperEnv := os.Getenv(suid.SavedArgs)
+	d := json.NewDecoder(strings.NewReader(helperEnv))
+	var savedArgs suid.ArgsSavedForTest
+	if err := d.Decode(&savedArgs); err != nil {
+		vlog.Fatalf("Failed to decode preserved argument %v: %v", helperEnv, err)
+	}
+	args := &PingArgs{
+		// TODO(rjkroege): Consider validating additional parameters
+		// from helper.
+		Username:  savedArgs.Uname,
+		FlagValue: flagValue,
+		EnvValue:  os.Getenv(TestEnvVarName),
+		Pid:       os.Getpid(),
+	}
+	client := v23.GetClient(ctx)
+	if call, err := client.StartCall(ctx, "pingserver", "Ping", []interface{}{args}); err != nil {
+		vlog.Fatalf("StartCall failed: %v", err)
+	} else if err := call.Finish(); err != nil {
+		vlog.Fatalf("Finish failed: %v", err)
+	}
+}
+
+// Cat is an RPC invoked from the test harness process to the application process.
+func Cat(ctx *context.T, name, file string) (string, error) {
+	ctx, cancel := context.WithTimeout(ctx, time.Minute)
+	defer cancel()
+	client := v23.GetClient(ctx)
+	call, err := client.StartCall(ctx, name, "Cat", []interface{}{file})
+	if err != nil {
+		return "", err
+	}
+	var content string
+	if err := call.Finish(&content); err != nil {
+		return "", err
+	}
+	return content, nil
+}
+
+// App is a test application. It pings the invoking device manager with state information.
+func App(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, flagValue *string, args ...string) error {
+	ctx, shutdown := test.InitForTest()
+	defer shutdown()
+
+	v23.GetNamespace(ctx).CacheCtl(naming.DisableCache(true))
+
+	if expected, got := 1, len(args); expected != got {
+		vlog.Fatalf("Unexpected number of arguments: expected %d, got %d", expected, got)
+	}
+	publishName := args[0]
+
+	server, _ := servicetest.NewServer(ctx)
+	defer server.Stop()
+	if err := server.Serve(publishName, new(appService), nil); err != nil {
+		vlog.Fatalf("Serve(%v) failed: %v", publishName, err)
+	}
+	// Some of our tests look for log files, so make sure they are flushed
+	// to ensure that at least the files exist.
+	vlog.FlushLog()
+	ping(ctx, *flagValue)
+
+	<-signals.ShutdownOnSignals(ctx)
+	if err := ioutil.WriteFile("testfile", []byte("goodbye world"), 0600); err != nil {
+		vlog.Fatalf("Failed to write testfile: %v", err)
+	}
+	return nil
+}
+
+type PingServer struct {
+	ing chan PingArgs
+}
+
+// TODO(caprita): Set the timeout in a more principled manner.
+const pingTimeout = 60 * time.Second
+
+func (p PingServer) Ping(_ *context.T, _ rpc.ServerCall, arg PingArgs) error {
+	p.ing <- arg
+	return nil
+}
+
+// SetupPingServer creates a server listening for a ping from a child app; it
+// returns a channel on which the app's ping message is returned, and a cleanup
+// function.
+func SetupPingServer(t *testing.T, ctx *context.T) (PingServer, func()) {
+	server, _ := servicetest.NewServer(ctx)
+	pingCh := make(chan PingArgs, 1)
+	if err := server.Serve("pingserver", PingServer{pingCh}, security.AllowEveryone()); err != nil {
+		t.Fatalf("Serve(%q, <dispatcher>) failed: %v", "pingserver", err)
+	}
+	return PingServer{pingCh}, func() {
+		if err := server.Stop(); err != nil {
+			t.Fatalf("Stop() failed: %v", err)
+		}
+	}
+}
+
+func (p PingServer) WaitForPingArgs(t *testing.T) PingArgs {
+	var args PingArgs
+	select {
+	case args = <-p.ing:
+	case <-time.After(pingTimeout):
+		t.Fatalf(testutil.FormatLogLine(2, "failed to get ping"))
+	}
+	return args
+}
+
+func (p PingServer) VerifyPingArgs(t *testing.T, username, flagValue, envValue string) {
+	args := p.WaitForPingArgs(t)
+	wantArgs := PingArgs{
+		Username:  username,
+		FlagValue: flagValue,
+		EnvValue:  envValue,
+		Pid:       args.Pid, // We are not checking for a value of Pid
+	}
+	if !reflect.DeepEqual(args, wantArgs) {
+		t.Fatalf(testutil.FormatLogLine(2, "got ping args %q, expected %q", args, wantArgs))
+	}
+}
diff --git a/services/device/internal/impl/utiltest/modules.go b/services/device/internal/impl/utiltest/modules.go
new file mode 100644
index 0000000..65852ed
--- /dev/null
+++ b/services/device/internal/impl/utiltest/modules.go
@@ -0,0 +1,144 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package utiltest
+
+import (
+	"fmt"
+	"io"
+	"os"
+	goexec "os/exec"
+	"strings"
+
+	"v.io/v23"
+	"v.io/v23/naming"
+	"v.io/v23/rpc"
+
+	"v.io/x/lib/vlog"
+
+	"v.io/x/ref/lib/signals"
+	"v.io/x/ref/services/device/internal/config"
+	"v.io/x/ref/services/device/internal/impl"
+	"v.io/x/ref/services/device/internal/starter"
+	"v.io/x/ref/test"
+	"v.io/x/ref/test/modules"
+)
+
+const (
+	RedirectEnv    = "DEVICE_MANAGER_DONT_REDIRECT_STDOUT_STDERR"
+	TestEnvVarName = "V23_RANDOM_ENV_VALUE"
+)
+
+// ExecScript launches the script passed as argument.
+func ExecScript(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
+	if want, got := 1, len(args); want != got {
+		vlog.Fatalf("execScript expected %d arguments, got %d instead", want, got)
+	}
+	script := args[0]
+	osenv := []string{RedirectEnv + "=1"}
+	if env["PAUSE_BEFORE_STOP"] == "1" {
+		osenv = append(osenv, "PAUSE_BEFORE_STOP=1")
+	}
+
+	cmd := goexec.Cmd{
+		Path:   script,
+		Env:    osenv,
+		Stdin:  stdin,
+		Stderr: stderr,
+		Stdout: stdout,
+	}
+
+	return cmd.Run()
+}
+
+// DeviceManager sets up a device manager server.  It accepts the name to
+// publish the server under as an argument.  Additional arguments can optionally
+// specify device manager config settings.
+func DeviceManager(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
+	ctx, shutdown := test.InitForTest()
+	if len(args) == 0 {
+		vlog.Fatalf("deviceManager expected at least an argument")
+	}
+	publishName := args[0]
+	args = args[1:]
+	defer fmt.Fprintf(stdout, "%v terminated\n", publishName)
+	defer vlog.VI(1).Infof("%v terminated", publishName)
+	defer shutdown()
+	v23.GetNamespace(ctx).CacheCtl(naming.DisableCache(true))
+
+	// Satisfy the contract described in doc.go by passing the config state
+	// through to the device manager dispatcher constructor.
+	configState, err := config.Load()
+	if err != nil {
+		vlog.Fatalf("Failed to decode config state: %v", err)
+	}
+
+	// This exemplifies how to override or set specific config fields, if,
+	// for example, the device manager is invoked 'by hand' instead of via a
+	// script prepared by a previous version of the device manager.
+	var pairingToken string
+	if len(args) > 0 {
+		if want, got := 4, len(args); want > got {
+			vlog.Fatalf("expected atleast %d additional arguments, got %d instead: %q", want, got, args)
+		}
+		configState.Root, configState.Helper, configState.Origin, configState.CurrentLink = args[0], args[1], args[2], args[3]
+		if len(args) > 4 {
+			pairingToken = args[4]
+		}
+	}
+	// We grab the shutdown channel at this point in order to ensure that we
+	// register a listener for the app cycle manager Stop before we start
+	// running the device manager service.  Otherwise, any device manager
+	// method that calls Stop on the app cycle manager (e.g. the Stop RPC)
+	// will precipitate an immediate process exit.
+	shutdownChan := signals.ShutdownOnSignals(ctx)
+	claimableName, stop, err := starter.Start(ctx, starter.Args{
+		Namespace: starter.NamespaceArgs{
+			ListenSpec: rpc.ListenSpec{Addrs: rpc.ListenAddrs{{"tcp", "127.0.0.1:0"}}},
+		},
+		Device: starter.DeviceArgs{
+			Name:            publishName,
+			ListenSpec:      rpc.ListenSpec{Addrs: rpc.ListenAddrs{{"tcp", "127.0.0.1:0"}}},
+			ConfigState:     configState,
+			TestMode:        strings.HasSuffix(fmt.Sprint(v23.GetPrincipal(ctx).BlessingStore().Default()), "/testdm"),
+			RestartCallback: func() { fmt.Println("restart handler") },
+			PairingToken:    pairingToken,
+		},
+		// TODO(rthellend): Wire up the local mounttable like the real device
+		// manager, i.e. mount the device manager and the apps on it, and mount
+		// the local mounttable in the global namespace.
+		// MountGlobalNamespaceInLocalNamespace: true,
+	})
+	if err != nil {
+		vlog.Errorf("starter.Start failed: %v", err)
+		return err
+	}
+	defer stop()
+	// Update the namespace roots to remove the server blessing from the
+	// endpoints.  This is needed to be able to publish into the 'global'
+	// mounttable before we have compatible credentials.
+	ctx, err = SetNamespaceRootsForUnclaimedDevice(ctx)
+	if err != nil {
+		return err
+	}
+	// Manually mount the claimable service in the 'global' mounttable.
+	v23.GetNamespace(ctx).Mount(ctx, "claimable", claimableName, 0)
+	fmt.Fprintf(stdout, "ready:%d\n", os.Getpid())
+
+	<-shutdownChan
+	if val, present := env["PAUSE_BEFORE_STOP"]; present && val == "1" {
+		modules.WaitForEOF(stdin)
+	}
+	// TODO(ashankar): Figure out a way to incorporate this check in the test.
+	// if impl.DispatcherLeaking(dispatcher) {
+	//	vlog.Fatalf("device manager leaking resources")
+	// }
+	return nil
+}
+
+// This is the same as DeviceManager above, except that it has a different major version number
+func DeviceManagerV10(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
+	impl.CurrentVersion = impl.Version{10, 0} // Set the version number to 10.0
+	return DeviceManager(stdin, stdout, stderr, env, args...)
+}