v23proxy: test v23proxy between two mojo shells

Change-Id: I2ab9f3e8fb894a351901a530e94d3e0a8d1ac729
diff --git a/Makefile b/Makefile
index bd7d3a8..335c55c 100644
--- a/Makefile
+++ b/Makefile
@@ -59,8 +59,12 @@
 
 build-dart-examples: gen/echo.mojom.dart gen/fortune.mojom.dart
 
+.PHONY: test
+test: test-unit test-integration
+
 # Go-based unit tests
-test: $(MOJO_SHARED_LIB) gen/go/src/mojom/tests/transcoder_testcases/transcoder_testcases.mojom.go gen-vdl
+.PHONY: test-unit
+test-unit: $(MOJO_SHARED_LIB) gen/go/src/mojom/tests/transcoder_testcases/transcoder_testcases.mojom.go gen-vdl
 	$(call MOGO_TEST,v.io/x/mojo/transcoder/...)
 
 # Note:This file is needed to compile v23proxy.mojom, so we're symlinking it in from $MOJO_SDK.
@@ -90,6 +94,16 @@
 $(BUILD_DIR)/echo_server.mojo: gen/go/src/mojom/examples/echo/echo.mojom.go
 	$(call MOGO_BUILD,examples/echo/server,$@)
 
+.PHONY: test-integration
+test-integration: $(BUILD_DIR)/test_client.mojo $(BUILD_DIR)/test_server.mojo $(BUILD_DIR)/v23proxy.mojo
+	GOPATH=$(PWD)/go:$(PWD)/gen/go jiri go -profiles=base,$(MOJO_PROFILE) run go/src/v.io/x/mojo/tests/cmd/runtest.go
+
+$(BUILD_DIR)/test_client.mojo: go/src/v.io/x/mojo/tests/client/test_client.go gen/go/src/mojom/tests/end_to_end_test/end_to_end_test.mojom.go gen/go/src/mojom/v23proxy/v23proxy.mojom.go
+	$(call MOGO_BUILD,v.io/x/mojo/tests/client,$@)
+
+$(BUILD_DIR)/test_server.mojo: go/src/v.io/x/mojo/tests/server/test_server.go gen/go/src/mojom/tests/end_to_end_test/end_to_end_test.mojom.go
+	$(call MOGO_BUILD,v.io/x/mojo/tests/server,$@)
+
 gen/go/src/mojom/examples/echo/echo.mojom.go: mojom/mojom/examples/echo.mojom | mojo-env-check
 	$(call MOJOM_GEN,$<,mojom,gen,go)
 	gofmt -w $@
@@ -110,7 +124,7 @@
 gen/fortune.mojom.dart: mojom/mojom/examples/fortune.mojom | mojo-env-check
 	$(call MOJOM_GEN,$<,mojom,dart-examples/fortune/lib/gen,dart)
 
-$(BUILD_DIR)/v23proxy.mojo: gen/go/src/mojom/v23proxy/v23proxy.mojom.go | mojo-env-check
+$(BUILD_DIR)/v23proxy.mojo: $(shell find $(PWD)/go/src/v.io/x/mojo/proxy -name *.go) | mojo-env-check
 	$(call MOGO_BUILD,v.io/x/mojo/proxy,$@)
 
 gen/go/src/mojo/public/interfaces/bindings/mojom_types/mojom_types.mojom.go: mojom/mojo/public/interfaces/bindings/mojom_types.mojom | mojo-env-check
@@ -128,6 +142,10 @@
 	$(call MOJOM_GEN,$<,mojom,gen,go)
 	gofmt -w $@
 
+gen/go/src/mojom/tests/end_to_end_test/end_to_end_test.mojom.go: mojom/mojom/tests/end_to_end_test.mojom | mojo-env-check
+	$(call MOJOM_GEN,$<,mojom,gen,go)
+	gofmt -w $@
+
 gen/v23proxy.mojom.dart: mojom/mojom/v23proxy.mojom packages gen/mojo/public/interfaces/bindings/mojom_types/mojom_types.mojom.dart | mojo-env-check
 	$(call MOJOM_GEN,$<,mojom,lib/gen,dart)
 	# TODO(nlacasse): mojom_bindings_generator creates bad symlinks on dart
diff --git a/go/src/v.io/x/mojo/proxy/fake_service.go b/go/src/v.io/x/mojo/proxy/fake_service.go
index 78a86df..85b21b2 100644
--- a/go/src/v.io/x/mojo/proxy/fake_service.go
+++ b/go/src/v.io/x/mojo/proxy/fake_service.go
@@ -9,19 +9,17 @@
 	"log"
 	"strings"
 
-	"v.io/v23/context"
-	"v.io/v23/rpc"
-
 	"mojo/public/go/application"
 	"mojo/public/go/bindings"
 	"mojo/public/go/system"
+	"mojo/public/interfaces/bindings/mojom_types"
+	"mojo/public/interfaces/bindings/service_describer"
 
+	"v.io/v23/context"
+	"v.io/v23/rpc"
 	"v.io/v23/vdl"
 	"v.io/v23/vdlroot/signature"
 	"v.io/x/mojo/transcoder"
-
-	"mojo/public/interfaces/bindings/mojom_types"
-	"mojo/public/interfaces/bindings/service_describer"
 )
 
 // As long as fakeService meets the Invoker interface, it is allowed to pass as
diff --git a/go/src/v.io/x/mojo/proxy/proxy.go b/go/src/v.io/x/mojo/proxy/proxy.go
index c0d1179..3550746 100644
--- a/go/src/v.io/x/mojo/proxy/proxy.go
+++ b/go/src/v.io/x/mojo/proxy/proxy.go
@@ -7,20 +7,19 @@
 import (
 	"fmt"
 
+	"mojo/public/go/application"
+	"mojo/public/go/bindings"
+	"mojo/public/go/system"
+	"mojo/public/interfaces/bindings/mojom_types"
+
+	"mojom/v23proxy"
+
 	"v.io/v23"
 	"v.io/v23/context"
 	"v.io/v23/options"
 	"v.io/v23/rpc"
 	"v.io/v23/security"
 	"v.io/v23/vdl"
-
-	"mojom/v23proxy"
-
-	"mojo/public/go/application"
-	"mojo/public/go/bindings"
-	"mojo/public/go/system"
-	"mojo/public/interfaces/bindings/mojom_types"
-
 	"v.io/x/mojo/transcoder"
 	_ "v.io/x/ref/runtime/factories/roaming"
 )
diff --git a/go/src/v.io/x/mojo/tests/client/test_client.go b/go/src/v.io/x/mojo/tests/client/test_client.go
new file mode 100644
index 0000000..603c6cc
--- /dev/null
+++ b/go/src/v.io/x/mojo/tests/client/test_client.go
@@ -0,0 +1,163 @@
+// 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 main
+
+import (
+	"flag"
+	"fmt"
+	"log"
+	"os"
+	"reflect"
+	"runtime"
+	"strings"
+	"testing"
+
+	"mojo/public/go/application"
+	"mojo/public/go/bindings"
+	"mojo/public/go/system"
+
+	"mojom/tests/end_to_end_test"
+
+	v23 "v.io/x/mojo/client"
+	"v.io/x/mojo/tests/expected"
+)
+
+//#include "mojo/public/c/system/types.h"
+import "C"
+
+func init() {
+	// Add flag placeholders to suppress warnings on unhandled mojo flags.
+	flag.String("child-connection-id", "", "")
+	flag.String("platform-channel-handle-info", "", "")
+}
+
+func TestSimple(t *testing.T, ctx application.Context) {
+	proxy := createProxy(ctx)
+	defer proxy.Close_Proxy()
+
+	value, err := proxy.Simple(expected.SimpleRequestA)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if value != expected.SimpleResponseValue {
+		t.Errorf("expected %v, but got %v", expected.SimpleResponseValue, value)
+	}
+}
+
+func TestMultiArgs(t *testing.T, ctx application.Context) {
+	proxy := createProxy(ctx)
+	defer proxy.Close_Proxy()
+
+	x, y, err := proxy.MultiArgs(expected.MultiArgsRequestA, expected.MultiArgsRequestB, expected.MultiArgsRequestC, expected.MultiArgsRequestD)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if !reflect.DeepEqual(x, expected.MultiArgsResponseX) {
+		t.Errorf("expected %v, but got %v", expected.MultiArgsResponseX, x)
+	}
+	if y != expected.MultiArgsResponseY {
+		t.Errorf("expected %v, but got %v", expected.MultiArgsResponseY, y)
+	}
+}
+
+func TestNoReturn(t *testing.T, ctx application.Context) {
+	proxy := createProxy(ctx)
+	defer proxy.Close_Proxy()
+
+	err := proxy.NoReturn()
+	if err != nil {
+		t.Fatal(err)
+	}
+}
+
+func TestReuseProxy(t *testing.T, ctx application.Context) {
+	fmt.Printf("in test reuse\n")
+	proxy := createProxy(ctx)
+	defer proxy.Close_Proxy()
+
+	value, err := proxy.Simple(expected.SimpleRequestA)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if value != expected.SimpleResponseValue {
+		t.Errorf("expected %v, but got %v", expected.SimpleResponseValue, value)
+	}
+	fmt.Printf("about to call second f\n")
+	x, y, err := proxy.MultiArgs(expected.MultiArgsRequestA, expected.MultiArgsRequestB, expected.MultiArgsRequestC, expected.MultiArgsRequestD)
+	if err != nil {
+		t.Fatal(err)
+	}
+	fmt.Printf("called second f\n")
+	if !reflect.DeepEqual(x, expected.MultiArgsResponseX) {
+		t.Errorf("expected %v, but got %v", expected.MultiArgsResponseX, x)
+	}
+	if y != expected.MultiArgsResponseY {
+		t.Errorf("expected %v, but got %v", expected.MultiArgsResponseY, y)
+	}
+}
+
+func createProxy(ctx application.Context) *end_to_end_test.V23ProxyTest_Proxy {
+	// Parse arguments. Note: May panic if not enough args are given.
+	remoteName := ctx.Args()[1]
+
+	r, p := end_to_end_test.CreateMessagePipeForV23ProxyTest()
+	v23.ConnectToRemoteService(ctx, &r, remoteName)
+	return end_to_end_test.NewV23ProxyTestProxy(p, bindings.GetAsyncWaiter())
+}
+
+type TestClientDelegate struct{}
+
+func funcName(f func(*testing.T, application.Context)) string {
+	qualified := runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name()
+	return qualified[strings.LastIndex(qualified, ".")+1:]
+}
+func convertTests(testFuncs []func(*testing.T, application.Context), ctx application.Context) []testing.InternalTest {
+	tests := make([]testing.InternalTest, len(testFuncs))
+	for i, _ := range testFuncs {
+		f := testFuncs[i]
+		tests[i] = testing.InternalTest{
+			Name: funcName(f),
+			F:    func(t *testing.T) { f(t, ctx) },
+		}
+	}
+	return tests
+}
+
+func (delegate *TestClientDelegate) Initialize(ctx application.Context) {
+	log.Printf("TestClientDelegate.Initialize...")
+
+	tests := []func(*testing.T, application.Context){
+		TestSimple, TestMultiArgs, TestNoReturn, TestReuseProxy,
+	}
+
+	matchAllTests := func(pat, str string) (bool, error) { return true, nil }
+	exitCode := testing.MainStart(matchAllTests, convertTests(tests, ctx), nil, nil).Run()
+	if exitCode == 0 {
+		fmt.Printf("%s\n", expected.SuccessMessage)
+	} else {
+		fmt.Printf("%s\n", expected.FailureMessage)
+	}
+
+	ctx.Close()
+	os.Exit(exitCode)
+}
+
+func (delegate *TestClientDelegate) AcceptConnection(connection *application.Connection) {
+	log.Printf("TestClientDelegate.AcceptConnection...")
+	connection.Close()
+}
+
+func (delegate *TestClientDelegate) Quit() {
+	log.Printf("TestClientDelegate.Quit...")
+}
+
+//export MojoMain
+func MojoMain(handle C.MojoHandle) C.MojoResult {
+	application.Run(&TestClientDelegate{}, system.MojoHandle(handle))
+	return C.MOJO_RESULT_OK
+}
+
+func main() {
+}
diff --git a/go/src/v.io/x/mojo/tests/cmd/runtest.go b/go/src/v.io/x/mojo/tests/cmd/runtest.go
new file mode 100644
index 0000000..8b694db
--- /dev/null
+++ b/go/src/v.io/x/mojo/tests/cmd/runtest.go
@@ -0,0 +1,74 @@
+// 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 main
+
+import (
+	"bufio"
+	"bytes"
+	"fmt"
+	"io"
+	"os"
+	"strings"
+
+	"v.io/x/mojo/tests/expected"
+	"v.io/x/mojo/tests/util"
+)
+
+func main() {
+	wd, err := os.Getwd()
+	if err != nil {
+		panic(err)
+	}
+	v23proxy, err := util.StartV23Proxy(wd)
+	if err != nil {
+		panic(err)
+	}
+	endpoint, err := v23proxy.Endpoint()
+	if err != nil {
+		panic(err)
+	}
+	name := endpoint + "//https://mojo.v.io/test_server.mojo/mojo::v23proxy::tests::V23ProxyTest"
+	if err := runTestClient(wd, name); err != nil {
+		fmt.Fprintf(os.Stderr, "%s", err)
+		os.Exit(2)
+	}
+	if err := v23proxy.Stop(); err != nil {
+		panic(err)
+	}
+}
+
+func runTestClient(v23ProxyRoot, endpoint string) error {
+	cmd := util.RunMojoShellForV23ProxyTests("test_client.mojo", v23ProxyRoot, []string{endpoint})
+	stdout, err := cmd.StdoutPipe()
+	if err != nil {
+		return err
+	}
+	var stdoutBuf bytes.Buffer
+	go func() {
+		io.Copy(os.Stdout, io.TeeReader(stdout, &stdoutBuf))
+	}()
+	stderr, err := cmd.StderrPipe()
+	if err != nil {
+		return err
+	}
+	go func() {
+		io.Copy(os.Stderr, stderr)
+	}()
+
+	if err := cmd.Run(); err != nil {
+		return err
+	}
+
+	scanner := bufio.NewScanner(bytes.NewReader(stdoutBuf.Bytes()))
+	for scanner.Scan() {
+		if strings.HasSuffix(scanner.Text(), expected.SuccessMessage) {
+			return nil
+		}
+	}
+	if err := scanner.Err(); err != nil {
+		return err
+	}
+	return fmt.Errorf("TESTS FAILED")
+}
diff --git a/go/src/v.io/x/mojo/tests/expected/expected.go b/go/src/v.io/x/mojo/tests/expected/expected.go
new file mode 100644
index 0000000..9ba0188
--- /dev/null
+++ b/go/src/v.io/x/mojo/tests/expected/expected.go
@@ -0,0 +1,24 @@
+// 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 expected
+
+import (
+	"mojom/tests/end_to_end_test"
+)
+
+var (
+	SimpleRequestA      int32 = 123
+	SimpleResponseValue       = "TheValue"
+
+	MultiArgsRequestA  = true
+	MultiArgsRequestB  = []float32{1, 2, 3}
+	MultiArgsRequestC  = map[string]uint8{"X": 1, "Y": 2}
+	MultiArgsRequestD  = end_to_end_test.AStruct{3, 300, 129}
+	MultiArgsResponseX = &end_to_end_test.AUnionB{Value: "TheUnion"}
+	MultiArgsResponseY = "yresponse"
+
+	SuccessMessage = "ALL TESTS PASSED"
+	FailureMessage = "Failed"
+)
diff --git a/go/src/v.io/x/mojo/tests/server/test_server.go b/go/src/v.io/x/mojo/tests/server/test_server.go
new file mode 100644
index 0000000..9462829
--- /dev/null
+++ b/go/src/v.io/x/mojo/tests/server/test_server.go
@@ -0,0 +1,104 @@
+// 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 main
+
+import (
+	"fmt"
+	"log"
+	"reflect"
+
+	"mojo/public/go/application"
+	"mojo/public/go/bindings"
+	"mojo/public/go/system"
+
+	"mojom/tests/end_to_end_test"
+
+	"v.io/x/mojo/tests/expected"
+)
+
+//#include "mojo/public/c/system/types.h"
+import "C"
+
+type V23ProxyTestImpl struct{}
+
+func (i *V23ProxyTestImpl) Simple(a int32) (value string, err error) {
+	if a != expected.SimpleRequestA {
+		return "", fmt.Errorf("expected %v, but got %v", expected.SimpleRequestA, a)
+	}
+	return expected.SimpleResponseValue, nil
+}
+
+func (i *V23ProxyTestImpl) MultiArgs(a bool, b []float32, c map[string]uint8, d end_to_end_test.AStruct) (x end_to_end_test.AUnion, y string, err error) {
+	if a != expected.MultiArgsRequestA {
+		return nil, "", fmt.Errorf("expected %v, but got %v", expected.MultiArgsRequestA, a)
+	}
+	if !reflect.DeepEqual(b, expected.MultiArgsRequestB) {
+		return nil, "", fmt.Errorf("expected %v, but got %v", expected.MultiArgsRequestB, b)
+	}
+	if !reflect.DeepEqual(c, expected.MultiArgsRequestC) {
+		return nil, "", fmt.Errorf("expected %v, but got %v", expected.MultiArgsRequestC, c)
+	}
+	if !reflect.DeepEqual(d, expected.MultiArgsRequestD) {
+		return nil, "", fmt.Errorf("expected %v, but got %v", expected.MultiArgsRequestD, d)
+	}
+	return expected.MultiArgsResponseX, expected.MultiArgsResponseY, nil
+}
+
+func (i *V23ProxyTestImpl) NoReturn() error {
+	// TODO(bprosnitz) The test should fail if the message is not received.
+	return nil
+}
+
+type V23ProxyTestServerDelegate struct {
+	factory V23ProxyTestFactory
+}
+
+type V23ProxyTestFactory struct {
+	stubs []*bindings.Stub
+}
+
+func (delegate *V23ProxyTestServerDelegate) Initialize(context application.Context) {
+	log.Printf("V23ProxyTestServerDelegate.Initialize...")
+}
+
+func (factory *V23ProxyTestFactory) Create(request end_to_end_test.V23ProxyTest_Request) {
+	log.Printf("V23ProxyTestServer's V23ProxyTestFactory.Create...")
+	stub := end_to_end_test.NewV23ProxyTestStub(request, &V23ProxyTestImpl{}, bindings.GetAsyncWaiter())
+	factory.stubs = append(factory.stubs, stub)
+	go func() {
+		for {
+			if err := stub.ServeRequest(); err != nil {
+				connectionError, ok := err.(*bindings.ConnectionError)
+				if !ok || !connectionError.Closed() {
+					log.Println(err)
+				}
+				break
+			}
+		}
+	}()
+}
+
+func (delegate *V23ProxyTestServerDelegate) AcceptConnection(connection *application.Connection) {
+	log.Printf("RemoteEchoServerDelegate.AcceptConnection...")
+	connection.ProvideServicesWithDescriber(
+		&end_to_end_test.V23ProxyTest_ServiceFactory{&delegate.factory},
+	)
+}
+
+func (delegate *V23ProxyTestServerDelegate) Quit() {
+	log.Printf("V23ProxyTestServerDelegate.Quit...")
+	for _, stub := range delegate.factory.stubs {
+		stub.Close()
+	}
+}
+
+//export MojoMain
+func MojoMain(handle C.MojoHandle) C.MojoResult {
+	application.Run(&V23ProxyTestServerDelegate{}, system.MojoHandle(handle))
+	return C.MOJO_RESULT_OK
+}
+
+func main() {
+}
diff --git a/go/src/v.io/x/mojo/tests/util/mojo_shell_runner.go b/go/src/v.io/x/mojo/tests/util/mojo_shell_runner.go
new file mode 100644
index 0000000..8cee582
--- /dev/null
+++ b/go/src/v.io/x/mojo/tests/util/mojo_shell_runner.go
@@ -0,0 +1,84 @@
+// 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 util
+
+import (
+	"fmt"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"strings"
+
+	"v.io/jiri/jiri"
+	"v.io/jiri/profiles"
+	"v.io/x/lib/cmdline"
+	"v.io/x/lib/timing"
+)
+
+func RunMojoShell(mojoUrl, configFile string, configAliases map[string]string, argsFor map[string][]string, target profiles.Target) *exec.Cmd {
+	// ensure the profiles are loaded
+	jirix, err := jiri.NewX(&cmdline.Env{Timer: timing.NewTimer("root")})
+	if err != nil {
+		panic(err)
+	}
+	_, err = profiles.NewConfigHelper(jirix, profiles.UseProfiles, filepath.Join(jirix.Root, ".jiri_v23_profiles"))
+	if err != nil {
+		panic(err)
+	}
+
+	env := profiles.EnvFromProfile(target, mojoProfileName())
+	var mojoDevtools, mojoShell, mojoServices string
+	for _, e := range env {
+		parts := strings.SplitN(e, "=", 2)
+		switch parts[0] {
+		case "MOJO_DEVTOOLS":
+			mojoDevtools = parts[1]
+		case "MOJO_SHELL":
+			mojoShell = parts[1]
+		case "MOJO_SERVICES":
+			mojoServices = parts[1]
+		}
+	}
+	args := []string{
+		mojoUrl,
+		"--config-file", configFile,
+		"--shell-path", mojoShell,
+		"--enable-multiprocess"}
+	if target.OS() == "android" {
+		args = append(args, "--android")
+		args = append(args, "--origin", mojoServices)
+	}
+	for alias, value := range configAliases {
+		args = append(args, "--config-alias", fmt.Sprintf("%s=%s", alias, value))
+	}
+	for key, value := range argsFor {
+		args = append(args, fmt.Sprintf("--args-for=%s %s", key, strings.Join(value, " ")))
+	}
+	return exec.Command(filepath.Join(mojoDevtools, "mojo_run"), args...)
+}
+
+func RunMojoShellForV23ProxyTests(mojoFile, v23ProxyRoot string, args []string) *exec.Cmd {
+	configFile := filepath.Join(v23ProxyRoot, "mojoconfig")
+	mojoUrl := fmt.Sprintf("https://mojo.v.io/%s", mojoFile)
+	buildDir := filepath.Join(v23ProxyRoot, "gen", "mojo", "linux_amd64")
+	configAliases := map[string]string{
+		"V23PROXY_DIR":       v23ProxyRoot,
+		"V23PROXY_BUILD_DIR": buildDir,
+	}
+	argsFor := map[string][]string{
+		mojoUrl:                     args,
+		"mojo:dart_content_handler": []string{"--enable-strict-mode"},
+	}
+	target := profiles.DefaultTarget()
+	return RunMojoShell(mojoUrl, configFile, configAliases, argsFor, target)
+}
+
+func mojoProfileName() string {
+	if val, ok := os.LookupEnv("USE_MOJO_DEV_PROFILE"); ok && val == "true" {
+		fmt.Printf("Using dev profile\n")
+		return "mojo-dev"
+	}
+	return "mojo"
+}
diff --git a/go/src/v.io/x/mojo/tests/util/v23_proxy_controller.go b/go/src/v.io/x/mojo/tests/util/v23_proxy_controller.go
new file mode 100644
index 0000000..e27c033
--- /dev/null
+++ b/go/src/v.io/x/mojo/tests/util/v23_proxy_controller.go
@@ -0,0 +1,118 @@
+// 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 util
+
+import (
+	"bufio"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"os"
+	"os/exec"
+	"strconv"
+	"strings"
+	"sync"
+	"syscall"
+)
+
+func StartV23Proxy(v23ProxyRoot string) (*V23ProxyController, error) {
+	cmd := RunMojoShellForV23ProxyTests("v23proxy.mojo", v23ProxyRoot, []string{})
+	stdout, err := cmd.StdoutPipe()
+	if err != nil {
+		return nil, err
+	}
+	stderr, err := cmd.StderrPipe()
+	if err != nil {
+		return nil, err
+	}
+	go func() {
+		io.Copy(os.Stderr, stderr)
+	}()
+	// A lock is put in home for the url response cache. Change HOME for v23proxy, since
+	// two mojo shells will be run.
+	tempHome, err := ioutil.TempDir("", "")
+	if err != nil {
+		return nil, err
+	}
+	cmd.Env = append(cmd.Env, "HOME="+tempHome)
+	if err := cmd.Start(); err != nil {
+		return nil, err
+	}
+	return &V23ProxyController{
+		cmd:      cmd,
+		stdout:   stdout,
+		tempHome: tempHome,
+	}, nil
+}
+
+type V23ProxyController struct {
+	cmd          *exec.Cmd
+	stdout       io.ReadCloser
+	endpointLock sync.Mutex
+	endpoint     string // empty until the endpoint is read
+	tempHome     string
+}
+
+func (v *V23ProxyController) Stop() error {
+	os.Remove(v.tempHome)
+	childPids, err := getChildProcessPids(v.cmd.Process.Pid)
+	if err != nil {
+		return err
+	}
+	if err := v.cmd.Process.Signal(syscall.SIGTERM); err != nil {
+		return err
+	}
+	for _, pid := range childPids {
+		syscall.Kill(pid, syscall.SIGTERM)
+	}
+	return nil
+}
+
+func (v *V23ProxyController) Endpoint() (string, error) {
+	v.endpointLock.Lock()
+	defer v.endpointLock.Unlock()
+
+	if v.endpoint != "" {
+		return v.endpoint, nil
+	}
+
+	scanner := bufio.NewScanner(v.stdout)
+	const prefix = "Listening at: "
+	for scanner.Scan() {
+		if strings.HasPrefix(scanner.Text(), prefix) {
+			v.endpoint = strings.TrimPrefix(scanner.Text(), prefix)
+			return v.endpoint, nil
+		}
+	}
+	if err := scanner.Err(); err != nil {
+		return "", err
+	}
+
+	return "", fmt.Errorf("unexpected EOF when looking for endpoint")
+}
+
+func getChildProcessPids(parentPid int) ([]int, error) {
+	cmd := exec.Command("ps", []string{"h", "--ppid", fmt.Sprintf("%d", parentPid), "-o", "pid"}...)
+	stdout, err := cmd.StdoutPipe()
+	if err != nil {
+		return nil, err
+	}
+	scanner := bufio.NewScanner(stdout)
+	var pids []int
+	if err := cmd.Start(); err != nil {
+		return nil, err
+	}
+	for scanner.Scan() {
+		if len(scanner.Text()) == 0 {
+			continue
+		}
+		pid, err := strconv.ParseInt(strings.Trim(scanner.Text(), " "), 10, 64)
+		if err != nil {
+			return nil, err
+		}
+		pids = append(pids, int(pid))
+	}
+	return pids, cmd.Wait()
+}
diff --git a/mojom/mojom/tests/end_to_end_test.mojom b/mojom/mojom/tests/end_to_end_test.mojom
new file mode 100644
index 0000000..a5b3a1d
--- /dev/null
+++ b/mojom/mojom/tests/end_to_end_test.mojom
@@ -0,0 +1,23 @@
+// 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.
+
+module mojo.v23proxy.tests;
+
+struct AStruct{
+ uint8 x;
+ int16 y;
+ uint8 z;
+};
+
+union AUnion{
+ uint8 a;
+ string b;
+};
+
+[ServiceName="mojo::v23proxy::tests::V23ProxyTest"]
+interface V23ProxyTest {
+  Simple(int32 a) => (string value);
+  MultiArgs(bool a, array<float> b, map<string, uint8> c, AStruct d) => (AUnion x, string y);
+  NoReturn();
+};