port playground/test.sh to v23tests.

Change-Id: Ia994eeba356ce7eede7f207544e41871ae3279d9
diff --git a/go/src/playground/builder/main.go b/go/src/playground/builder/main.go
index 16b8c6d..a525533 100644
--- a/go/src/playground/builder/main.go
+++ b/go/src/playground/builder/main.go
@@ -32,6 +32,7 @@
 	"syscall"
 	"time"
 
+	vexec "v.io/core/veyron/lib/exec/consts"
 	"v.io/core/veyron/lib/flags/consts"
 
 	"playground/lib"
@@ -46,6 +47,8 @@
 	includeV23Env = flag.Bool("includeV23Env", false, "Whether to log the output of \"v23 env\" before compilation.")
 
 	// TODO(ivanpi): Separate out mounttable, proxy, wspr timeouts. Add compile timeout. Revise default.
+	// TODO(ivanpi): you can make this a time.Duration, implement the flag.Value
+	// interface as per veyron2/config settings.
 	runTimeout = flag.Int64("runTimeout", 3000, "Time limit for running user code, in milliseconds.")
 
 	// Sink for writing events (debug and run output) to stdout as JSON, one event per line.
@@ -410,6 +413,10 @@
 func main() {
 	flag.Parse()
 
+	// TODO(cnicolaou): remove this when the isse below is resolved:
+	// https://github.com/veyron/release-issues/issues/1157
+	os.Setenv(vexec.ExecVersionVariable, "")
+
 	out = event.NewJsonSink(os.Stdout, !*verbose)
 
 	r, err := parseRequest(os.Stdin)
@@ -436,6 +443,5 @@
 		panicOnError(out.Write(event.New("<compile>", "stderr", "Compilation error.")))
 		return
 	}
-
 	runFiles(r.Files)
 }
diff --git a/go/src/playground/builder/services.go b/go/src/playground/builder/services.go
index 6904a6d..00dc9a6 100644
--- a/go/src/playground/builder/services.go
+++ b/go/src/playground/builder/services.go
@@ -8,13 +8,11 @@
 	"bufio"
 	"fmt"
 	"io"
-	"math/rand"
 	"os"
 	"os/exec"
 	"path"
 	"regexp"
 	"strconv"
-	"syscall"
 	"time"
 
 	"v.io/core/veyron/lib/flags/consts"
@@ -26,35 +24,15 @@
 	proxyName = "proxy"
 )
 
-// Note: This was copied from release/go/src/v.io/core/veyron/tools/findunusedport.
-// I would like to be able to import that package directly, but it defines a
-// main(), so can't be imported.  An alternative solution would be to call the
-// 'findunusedport' binary, but that would require starting another process and
-// parsing the output.  It seemed simpler to just copy the function here.
-func findUnusedPort() (int, error) {
-	rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
-	for i := 0; i < 1000; i++ {
-		port := 1024 + rnd.Int31n(64512)
-		fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_STREAM, syscall.IPPROTO_TCP)
-		if err != nil {
-			continue
-		}
-		sa := &syscall.SockaddrInet4{Port: int(port)}
-		if err := syscall.Bind(fd, sa); err != nil {
-			continue
-		}
-		syscall.Close(fd)
-		return int(port), nil
-	}
-	return 0, fmt.Errorf("Can't find unused port.")
-}
+// TODO(ivanpi): this is all implemented in veyron/lib/modules/core, you
+// may be able to use that directly.
 
 // startMount starts a mounttabled process, and sets the NAMESPACE_ROOT env
 // variable to the mounttable's location.  We run one mounttabled process for
 // the entire environment.
 func startMount(timeLimit time.Duration) (proc *os.Process, err error) {
 	cmd := makeCmd("<mounttabled>", true, "mounttabled", "-veyron.tcp.address=127.0.0.1:0")
-	matches, err := startAndWaitFor(cmd, timeLimit, regexp.MustCompile("Mount table .+ endpoint: (.+)\n"))
+	matches, err := startAndWaitFor(cmd, timeLimit, regexp.MustCompile("NAME=(.*)"))
 	if err != nil {
 		return nil, fmt.Errorf("Error starting mounttabled: %v", err)
 	}
@@ -70,12 +48,11 @@
 func startProxy(timeLimit time.Duration) (proc *os.Process, err error) {
 	cmd := makeCmd("<proxyd>", true,
 		"proxyd",
-		// Verbose logging so we can watch the output for "Proxy listening" log line.
-		"-v=1",
+		"-log_dir=/tmp/logs",
 		"-name="+proxyName,
 		"-address=127.0.0.1:0",
 		"-http=")
-	if _, err := startAndWaitFor(cmd, timeLimit, regexp.MustCompile("Proxy listening")); err != nil {
+	if _, err := startAndWaitFor(cmd, timeLimit, regexp.MustCompile("NAME=(.*)")); err != nil {
 		return nil, fmt.Errorf("Error starting proxy: %v", err)
 	}
 	return cmd.Process, nil
@@ -84,17 +61,11 @@
 // startWspr starts a wsprd process. We run one wsprd process for each
 // javascript file being run.
 func startWspr(fileName, credentials string, timeLimit time.Duration) (proc *os.Process, port int, err error) {
-	port, err = findUnusedPort()
-	if err != nil {
-		return nil, port, err
-	}
 	cmd := makeCmd("<wsprd>:"+fileName, true,
 		"wsprd",
-		// Verbose logging so we can watch the output for "Listening" log line.
-		"-v=1",
 		"-veyron.proxy="+proxyName,
 		"-veyron.tcp.address=127.0.0.1:0",
-		"-port="+strconv.Itoa(port),
+		"-port=0",
 		// Retry RPC calls for 1 second. If a client makes an RPC call before the
 		// server is running, it won't immediately fail, but will retry while the
 		// server is starting.
@@ -106,9 +77,15 @@
 	if credentials != "" {
 		cmd.Env = append(cmd.Env, fmt.Sprintf("%v=%s", consts.VeyronCredentials, path.Join("credentials", credentials)))
 	}
-	if _, err := startAndWaitFor(cmd, timeLimit, regexp.MustCompile("Listening")); err != nil {
+	parts, err := startAndWaitFor(cmd, timeLimit, regexp.MustCompile(".*port: (.*)"))
+	if err != nil {
 		return nil, 0, fmt.Errorf("Error starting wspr: %v", err)
 	}
+	portstr := parts[1]
+	port, err = strconv.Atoi(portstr)
+	if err != nil {
+		return nil, 0, fmt.Errorf("Malformed port: %q: %v", portstr, err)
+	}
 	return cmd.Process, port, nil
 }
 
@@ -120,20 +97,16 @@
 // util function.
 func startAndWaitFor(cmd *exec.Cmd, timeout time.Duration, outputRegexp *regexp.Regexp) ([]string, error) {
 	reader, writer := io.Pipe()
-	// TODO(sadovsky): Why must we listen to both stdout and stderr? We should
-	// know which one produces the "Listening" log line...
 	cmd.Stdout.(*lib.MultiWriter).Add(writer)
-	cmd.Stderr.(*lib.MultiWriter).Add(writer)
-	err := cmd.Start()
-	if err != nil {
+	if err := cmd.Start(); err != nil {
 		return nil, err
 	}
 
-	buf := bufio.NewReader(reader)
-	t := time.After(timeout)
 	ch := make(chan []string)
 	go (func() {
-		for line, err := buf.ReadString('\n'); err == nil; line, err = buf.ReadString('\n') {
+		scanner := bufio.NewScanner(reader)
+		for scanner.Scan() {
+			line := scanner.Text()
 			if matches := outputRegexp.FindStringSubmatch(line); matches != nil {
 				ch <- matches
 			}
@@ -141,8 +114,8 @@
 		close(ch)
 	})()
 	select {
-	case <-t:
-		return nil, fmt.Errorf("Timeout starting service.")
+	case <-time.After(timeout):
+		return nil, fmt.Errorf("Timeout starting service: %v", cmd.Path)
 	case matches := <-ch:
 		return matches, nil
 	}
diff --git a/go/src/playground/playground_v23_test.go b/go/src/playground/playground_v23_test.go
new file mode 100644
index 0000000..854a7a3
--- /dev/null
+++ b/go/src/playground/playground_v23_test.go
@@ -0,0 +1,136 @@
+package playground_test
+
+import (
+	"os"
+	"path/filepath"
+
+	"v.io/core/veyron/lib/testutil/v23tests"
+	_ "v.io/core/veyron/profiles"
+)
+
+//go:generate v23 test generate
+
+var (
+	vanadiumRoot, playgroundRoot string
+)
+
+func init() {
+	vanadiumRoot = os.Getenv("VANADIUM_ROOT")
+	if len(vanadiumRoot) == 0 {
+		panic("VANADIUM_ROOT must be set")
+	}
+}
+
+func golist(i *v23tests.T, pkg string) string {
+	v23 := filepath.Join(vanadiumRoot, "bin/v23")
+	return i.Run(v23, "go", "list", "-f", "{{.Dir}}", pkg)
+}
+
+func npmLink(i *v23tests.T, dir, pkg string) {
+	npmBin := i.BinaryFromPath(filepath.Join(vanadiumRoot, "environment/cout/node/bin/npm"))
+	i.Pushd(dir)
+	npmBin.Run("link")
+	i.Popd()
+	npmBin.Run("link", pkg)
+}
+
+// Bundles a playground example and tests it using builder.
+// - dir is the root directory of example to test
+// - args are the arguments to call builder with
+func runPGExample(i *v23tests.T, dir string, args ...string) *v23tests.Invocation {
+	i.Run("./node_modules/.bin/pgbundle", dir)
+	tmp := i.NewTempDir()
+	cwd := i.Pushd(tmp)
+	old := filepath.Join(cwd, "node_modules")
+	if err := os.Symlink(old, filepath.Join(".", filepath.Base(old))); err != nil {
+		i.Fatalf("%s: symlink: failed: %v", i.Caller(2), err)
+	}
+	bundleName := filepath.Join(dir, "bundle.json")
+
+	stdin, err := os.Open(bundleName)
+	if err != nil {
+		i.Fatalf("%s: open(%s) failed: %v", i.Caller(2), bundleName, err)
+	}
+	// TODO(ivanpi): move this out so it only gets invoked once even though
+	// the binary is cached.
+	builderBin := i.BuildGoPkg("playground/builder")
+
+	PATH := "PATH=" + i.BinDir()
+	if path := os.Getenv("PATH"); len(path) > 0 {
+		PATH += ":" + path
+	}
+	defer i.Popd()
+	return builderBin.WithEnv(PATH).WithStdin(stdin).Start(args...)
+}
+
+// Sets up a directory with the given files, then runs builder.
+func testWithFiles(i *v23tests.T, pgRoot string, files ...string) *v23tests.Invocation {
+	testdataDir := filepath.Join(pgRoot, "testdata")
+	pgBundleDir := i.NewTempDir()
+	for _, f := range files {
+		fdir := filepath.Join(pgBundleDir, filepath.Dir(f))
+		if err := os.MkdirAll(fdir, 0755); err != nil {
+			i.Fatalf("%s: mkdir(%q): failed: %v", i.Caller(1), fdir, err)
+		}
+		i.Run("/bin/cp", filepath.Join(testdataDir, f), fdir)
+	}
+	return runPGExample(i, pgBundleDir, "-v=true", "--includeV23Env=true", "--runTimeout=5000")
+}
+
+func V23TestPlayground(i *v23tests.T) {
+	v23tests.RunRootMT(i, "--veyron.tcp.address=127.0.0.1:0")
+
+	i.BuildGoPkg("v.io/core/veyron/tools/principal")
+	i.BuildGoPkg("v.io/core/veyron/services/proxy/proxyd")
+	i.BuildGoPkg("v.io/core/veyron2/vdl/vdl")
+	i.BuildGoPkg("v.io/wspr/veyron/services/wsprd")
+
+	playgroundPkg := golist(i, "playground")
+	// strip last three directory components, much easier to read in
+	// errors than <path>/../../..
+	playgroundRoot = filepath.Dir(playgroundPkg)
+	playgroundRoot = filepath.Dir(playgroundRoot)
+	playgroundRoot = filepath.Dir(playgroundRoot)
+
+	npmLink(i, filepath.Join(vanadiumRoot, "release/javascript/core"), "veyron")
+	npmLink(i, filepath.Join(playgroundRoot, "pgbundle"), "pgbundle")
+
+	cases := []struct {
+		name  string
+		files []string
+	}{
+		{"basic ping (go -> go)",
+			[]string{"src/pong/pong.go", "src/ping/ping.go", "src/pingpong/wire.vdl"}},
+		{"basic ping (js -> js)",
+			[]string{"src/pong/pong.js", "src/ping/ping.js", "src/pingpong/wire.vdl"}},
+		{"basic ping (js -> go)",
+			[]string{"src/pong/pong.go", "src/ping/ping.js", "src/pingpong/wire.vdl"}},
+		{"basic ping (go -> js)",
+			[]string{"src/pong/pong.js", "src/ping/ping.go", "src/pingpong/wire.vdl"}},
+	}
+
+	runCases := func(authfile string, patterns []string) {
+		for _, c := range cases {
+			files := c.files
+			if len(authfile) > 0 {
+				files = append(files, authfile)
+			}
+			inv := testWithFiles(i, playgroundPkg, files...)
+			i.Logf("test: %s", c.name)
+			inv.ExpectSetEventuallyRE(patterns...)
+		}
+	}
+
+	i.Logf("Test as the same principal")
+	runCases("", []string{"PING", "PONG"})
+
+	i.Logf("Test with authorized blessings")
+	runCases("src/ids/authorized.id", []string{"PING", "PONG"})
+
+	i.Logf("Test with expired blessings")
+	runCases("src/ids/expired.id", []string{"not authorized"})
+
+	i.Logf("Test with unauthorized blessings")
+	runCases("src/ids/unauthorized.id", []string{"not authorized"})
+
+}
diff --git a/go/src/playground/v23_test.go b/go/src/playground/v23_test.go
new file mode 100644
index 0000000..789ef1a
--- /dev/null
+++ b/go/src/playground/v23_test.go
@@ -0,0 +1,21 @@
+// This file was auto-generated via go generate.
+// DO NOT UPDATE MANUALLY
+package playground_test
+
+import "testing"
+import "os"
+
+import "v.io/core/veyron/lib/testutil"
+import "v.io/core/veyron/lib/testutil/v23tests"
+
+func TestMain(m *testing.M) {
+	testutil.Init()
+	cleanup := v23tests.UseSharedBinDir()
+	r := m.Run()
+	cleanup()
+	os.Exit(r)
+}
+
+func TestV23Playground(t *testing.T) {
+	v23tests.RunTest(t, V23TestPlayground)
+}