veyron/tools/playground: Playground support for javascript.

Starts one WSPR instance per js file. Also has to start a single proxy
instance for the entire environment.

Tests that javascript client and server can communicate, and also that
they can communicate with go clients and servers.

Once javascript has support for identities and ACLs, I will add tests
that exercise those features.

Change-Id: I384e2220c68cb42c62caa2c38952f604bfcaa20f
diff --git a/tools/playground/builder/Dockerfile b/tools/playground/builder/Dockerfile
index b34ad18..218a248 100644
--- a/tools/playground/builder/Dockerfile
+++ b/tools/playground/builder/Dockerfile
@@ -1,25 +1,42 @@
 FROM ubuntu
+RUN /usr/sbin/useradd -d /home/playground -m playground
 RUN apt-get update
-RUN apt-get install -y curl git golang
+RUN apt-get install -y curl g++ git golang libc6-i386 make python
 ENV HOME /root
 ENV VEYRON_ROOT /usr/local/veyron
+ENV GOPATH /home/playground
+
+# Setup veyron and veyron profiles.
+# Note: This will be cached! If you want to re-build the docker image using
+# fresh veyron code, you must pass "--no-cache" to the docker build command.
+# See README.md.
 ADD netrc /root/.netrc
 RUN curl -u veyron:D6HT]P,LrJ7e https://www.envyor.com/noproxy/veyron-setup.sh | bash
-RUN /usr/local/veyron/bin/veyron profile setup core
 RUN rm /root/.netrc
+RUN /usr/local/veyron/bin/veyron profile setup core
+RUN /usr/local/veyron/bin/veyron profile setup web
 
-# Uncomment the following line to use the local copy of vbuild.go.  This is
-# useful when developing and testing local changes to vbuild.go.
-# ADD vbuild.go $VEYRON_ROOT/veyron/go/src/tools/playground/builder
+# Install the veyron.js library.
+# TODO(nlacasse): Change to "npm install -g veyron" once it's publicly published
+# in NPM.
+WORKDIR /usr/local/veyron/veyron.js/
+RUN $VEYRON_ROOT/environment/cout/node/bin/npm install --production
+RUN $VEYRON_ROOT/environment/cout/node/bin/npm link
+WORKDIR /home/playground
+RUN $VEYRON_ROOT/environment/cout/node/bin/npm link veyron
 
+# Install Veyron Go dependencies.
 WORKDIR /usr/local/veyron/veyron
 ENV PATH /usr/local/veyron/veyron/go/bin:/usr/local/bin:/usr/bin:/bin
-RUN $VEYRON_ROOT/veyron/scripts/build/go install veyron/tools/identity veyron/services/mounttable/mounttabled veyron2/vdl/vdl
-RUN $VEYRON_ROOT/veyron/scripts/build/go install veyron/tools/playground/builder veyron/tools/playground/compilerd
+RUN $VEYRON_ROOT/veyron/scripts/build/go install veyron/... veyron2/...
 
-RUN /usr/sbin/useradd -d /home/playground -m playground
+# Uncomment the following lines to install a version of the builder tool using
+# the local copy of vbuild.go.  This is useful when developing and testing
+# local changes to the builder tool.
+# RUN rm $VEYRON_ROOT/veyron/go/bin/builder
+# ADD vbuild.go /usr/local/veyron/veyron/go/src/veyron/tools/playground/builder/vbuild.go
+# RUN $VEYRON_ROOT/veyron/scripts/build/go install veyron/tools/playground/builder
 
 USER playground
 WORKDIR /home/playground
-ENV GOPATH /home/playground
 ENTRYPOINT /usr/local/veyron/veyron/go/bin/builder
diff --git a/tools/playground/builder/identity.go b/tools/playground/builder/identity.go
new file mode 100644
index 0000000..9420a00
--- /dev/null
+++ b/tools/playground/builder/identity.go
@@ -0,0 +1,76 @@
+// Functions to start services needed in the Veyron environment.
+package main
+
+import (
+	"os"
+	"path"
+)
+
+type Identity struct {
+	Name     string
+	Blesser  string
+	Duration string
+	Files    []string
+}
+
+func (id Identity) create() error {
+	if err := id.generate(); err != nil {
+		return err
+	}
+	if id.Blesser != "" || id.Duration != "" {
+		return id.bless()
+	}
+	return nil
+}
+
+func (id Identity) generate() error {
+	args := []string{"generate"}
+	if id.Blesser == "" && id.Duration == "" {
+		args = append(args, id.Name)
+	}
+	return runIdentity(args, path.Join("ids", id.Name))
+}
+
+func (id Identity) bless() error {
+	filename := path.Join("ids", id.Name)
+	var blesser string
+	if id.Blesser == "" {
+		blesser = filename
+	} else {
+		blesser = path.Join("ids", id.Blesser)
+	}
+	args := []string{"bless", "--with", blesser}
+	if id.Duration != "" {
+		args = append(args, "--for", id.Duration)
+	}
+	args = append(args, filename, id.Name)
+	tempfile := filename + ".tmp"
+	if err := runIdentity(args, tempfile); err != nil {
+		return err
+	}
+	return os.Rename(tempfile, filename)
+}
+
+func createIdentities(ids []Identity) error {
+	debug("Generating identities")
+	if err := os.MkdirAll("ids", 0777); err != nil {
+		return err
+	}
+	for _, id := range ids {
+		if err := id.create(); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func runIdentity(args []string, filename string) error {
+	cmd := makeCmd("identity", args...)
+	out, err := os.Create(filename)
+	if err != nil {
+		return err
+	}
+	defer out.Close()
+	cmd.Stdout = out
+	return cmd.Run()
+}
diff --git a/tools/playground/builder/services.go b/tools/playground/builder/services.go
new file mode 100644
index 0000000..a7e9dd8
--- /dev/null
+++ b/tools/playground/builder/services.go
@@ -0,0 +1,91 @@
+// Functions to start services needed by the Veyron playground.
+package main
+
+import (
+	"bufio"
+	"fmt"
+	"io"
+	"log"
+	"os"
+	"path"
+	"regexp"
+	"strconv"
+	"time"
+)
+
+var (
+	proxyPort    = 1234
+	proxyName    = "proxy"
+	wsprBasePort = 1235
+)
+
+// 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) {
+	reader, writer := io.Pipe()
+	cmd := makeCmd("mounttabled")
+	cmd.Stdout = writer
+	cmd.Stderr = cmd.Stdout
+	err = cmd.Start()
+	if err != nil {
+		return nil, err
+	}
+	buf := bufio.NewReader(reader)
+	// TODO(nlacasse): Find a better way to get the mounttable endpoint.
+	pat := regexp.MustCompile("Mount table .+ endpoint: (.+)\n")
+
+	timeout := time.After(timeLimit)
+	ch := make(chan string)
+	go (func() {
+		for line, err := buf.ReadString('\n'); err == nil; line, err = buf.ReadString('\n') {
+			if groups := pat.FindStringSubmatch(line); groups != nil {
+				ch <- groups[1]
+			}
+		}
+		close(ch)
+	})()
+	select {
+	case <-timeout:
+		log.Fatal("Timeout starting mounttabled")
+	case endpoint := <-ch:
+		if endpoint == "" {
+			log.Fatal("mounttable died")
+		}
+		return cmd.Process, os.Setenv("NAMESPACE_ROOT", endpoint)
+	}
+	return cmd.Process, err
+}
+
+// startProxy starts a proxyd process.  We run one proxyd process for the
+// entire environment.
+func startProxy() (proc *os.Process, err error) {
+	cmd := makeCmd("proxyd", "-name="+proxyName, "-address=:"+strconv.Itoa(proxyPort))
+	err = cmd.Start()
+	if err != nil {
+		return nil, err
+	}
+	return cmd.Process, err
+}
+
+// startWspr starts a wsprd process. We run one wsprd process for each
+// javascript file being run. The 'index' argument is used to pick a distinct
+// port for each wsprd process.
+func startWspr(index int, identity string) (proc *os.Process, port int, err error) {
+	port = wsprBasePort + index
+	cmd := makeCmd("wsprd",
+		"-v=-1",
+		"-vproxy="+proxyName,
+		"-port="+strconv.Itoa(port),
+		// The identd server won't be used, so pass a fake name.
+		"-identd=/unused")
+
+	if identity != "" {
+		cmd.Env = append(cmd.Env, fmt.Sprintf("VEYRON_IDENTITY=%s", path.Join("ids", identity)))
+	}
+	err = cmd.Start()
+	if err != nil {
+		return nil, 0, err
+	}
+	return cmd.Process, port, err
+}
diff --git a/tools/playground/builder/vbuild.go b/tools/playground/builder/vbuild.go
index b9decde..9c739db 100644
--- a/tools/playground/builder/vbuild.go
+++ b/tools/playground/builder/vbuild.go
@@ -4,7 +4,6 @@
 package main
 
 import (
-	"bufio"
 	"encoding/json"
 	"flag"
 	"fmt"
@@ -16,30 +15,37 @@
 	"os"
 	"os/exec"
 	"path"
-	"regexp"
+	"strconv"
 	"strings"
+	"sync"
 	"syscall"
 	"time"
 )
 
 const RUN_TIMEOUT = time.Second
 
-var debug = flag.Bool("v", false, "Verbose mode")
+var (
+	verbose = flag.Bool("v", false, "Verbose mode")
+
+	// Whether we have stopped execution of running files.
+	stopped = false
+
+	mu sync.Mutex
+)
 
 type CodeFile struct {
-	Name       string
-	Body       string
-	identity   string
-	pkg        string
-	executable bool
-	proc       *exec.Cmd
-}
-
-type Identity struct {
 	Name     string
-	Blesser  string
-	Duration string
-	Files    []string
+	Body     string
+	lang     string
+	identity string
+	pkg      string
+	// The executable flag denotes whether the file should be executed as
+	// part of the playground run. This is currently used only for
+	// javascript files, and go files with package "main".
+	executable bool
+	cmd        *exec.Cmd
+	subProcs   []*os.Process
+	index      int
 }
 
 // The input on STDIN should only contain Files.  We look for a file
@@ -56,24 +62,14 @@
 	err  error
 }
 
-func Log(args ...interface{}) {
-	if *debug {
+func debug(args ...interface{}) {
+	if *verbose {
 		log.Println(args...)
 	}
 }
 
-func MakeCmd(prog string, args ...string) *exec.Cmd {
-	Log("Running", prog, strings.Join(args, " "))
-	cmd := exec.Command(prog, args...)
-	// TODO(ribrdb): prefix output with the name of the binary
-	cmd.Stdout = os.Stdout
-	cmd.Stderr = os.Stderr
-	cmd.Env = os.Environ()
-	return cmd
-}
-
-func ParseRequest(in io.Reader) (r Request, err error) {
-	Log("Parsing input")
+func parseRequest(in io.Reader) (r Request, err error) {
+	debug("Parsing input")
 	data, err := ioutil.ReadAll(in)
 	if err == nil {
 		err = json.Unmarshal(data, &r)
@@ -81,6 +77,7 @@
 	m := make(map[string]*CodeFile)
 	for i := 0; i < len(r.Files); {
 		f := r.Files[i]
+		f.index = i
 		if path.Ext(f.Name) == ".id" {
 			err = json.Unmarshal([]byte(f.Body), &r.Identities)
 			if err != nil {
@@ -88,6 +85,22 @@
 			}
 			r.Files = append(r.Files[:i], r.Files[i+1:]...)
 		} else {
+			switch path.Ext(f.Name) {
+			case ".js":
+				// Javascript files are always executable.
+				f.lang = "js"
+				f.executable = true
+			case ".go":
+				// Go files will be marked as executable if
+				// their package name is "main". This happens
+				// in the "readPackage" function.
+				f.lang = "go"
+			case ".vdl":
+				f.lang = "vdl"
+			default:
+				return r, fmt.Errorf("Unknown file type %s", f.Name)
+			}
+
 			m[f.Name] = f
 			i++
 		}
@@ -101,7 +114,15 @@
 	} else {
 		for _, identity := range r.Identities {
 			for _, name := range identity.Files {
-				m[name].identity = identity.Name
+				// Check that the file associated with the
+				// identity exists.  We ignore cases where it
+				// doesn't because the test .id files get used
+				// for multiple different code files.  See
+				// testdata/ids/authorized.id, for example.
+				if m[name] != nil {
+					m[name].identity = identity.Name
+
+				}
 			}
 		}
 	}
@@ -111,65 +132,86 @@
 
 func main() {
 	flag.Parse()
-	r, err := ParseRequest(os.Stdin)
+	r, err := parseRequest(os.Stdin)
 	if err != nil {
 		log.Fatal(err)
 	}
 
-	err = CreateIdentities(r)
+	err = createIdentities(r.Identities)
 	if err != nil {
 		log.Fatal(err)
 	}
-	mt, err := StartMount()
-	if mt != nil {
-		defer mt.Kill()
-	}
+
+	mt, err := startMount(RUN_TIMEOUT)
 	if err != nil {
 		log.Fatal(err)
 	}
-	CompileAndRun(r)
+	defer mt.Kill()
+
+	proxy, err := startProxy()
+	if err != nil {
+		log.Fatal(err)
+	}
+	defer proxy.Kill()
+
+	if err := writeFiles(r.Files); err != nil {
+		log.Fatal(err)
+	}
+
+	if err := compileFiles(r.Files); err != nil {
+		log.Fatal(err)
+	}
+
+	runFiles(r.Files)
 }
 
-func CreateIdentities(r Request) error {
-	Log("Generating identities")
-	if err := os.MkdirAll("ids", 0777); err != nil {
-		return err
-	}
-	for _, id := range r.Identities {
-		if err := id.Create(); err != nil {
-			return err
+func writeFiles(files []*CodeFile) error {
+	debug("Writing files")
+	for _, f := range files {
+		if err := f.write(); err != nil {
+			return fmt.Errorf("Error writing %s: %v", f.Name, err)
 		}
 	}
 	return nil
 }
 
-func CompileAndRun(r Request) {
-	Log("Processing files")
+func compileFiles(files []*CodeFile) error {
+	debug("Compiling files")
+	var nonVdlFiles []*CodeFile
+
+	// Compile the vdl files first, since Go files may depend on *.vdl.go
+	// generated files.
+	for _, f := range files {
+		if f.lang != "vdl" {
+			nonVdlFiles = append(nonVdlFiles, f)
+		} else {
+			if err := f.compile(); err != nil {
+				return fmt.Errorf("Error compiling %s: %v", f.Name, err)
+			}
+		}
+	}
+
+	// Compile the non-vdl files
+	for _, f := range nonVdlFiles {
+		if err := f.compile(); err != nil {
+			return fmt.Errorf("Error compiling %s: %v", f.Name, err)
+		}
+	}
+	return nil
+}
+
+func runFiles(files []*CodeFile) {
+	debug("Running files")
 	exit := make(chan Exit)
 	running := 0
-
-	// TODO(ribrdb): Compile first, don't run anything if compilation fails.
-
-	for _, f := range r.Files {
-		var err error
-		if err = f.Write(); err != nil {
-			goto Error
-		}
-		if err = f.Compile(); err != nil {
-			goto Error
-		}
+	for _, f := range files {
 		if f.executable {
-			go f.Run(exit)
+			f.run(exit)
 			running++
 		}
-	Error:
-		if err != nil {
-			log.Printf("%s: %v\n", f.Name, err)
-		}
 	}
 
 	timeout := time.After(RUN_TIMEOUT)
-	killed := false
 
 	for running > 0 {
 		select {
@@ -182,51 +224,63 @@
 				log.Printf("%s exited with error %v", status.name, status.err)
 			}
 			running--
-			if !killed {
-				killed = true
-				for _, f := range r.Files {
-					if f.Name != status.name && f.proc != nil {
-						f.proc.Process.Signal(syscall.SIGTERM)
-					}
-				}
-			}
+			stopAll(files)
 		}
 	}
 }
 
-func (file *CodeFile) readPackage() error {
-	Log("Parsing package from ", file.Name)
-	f, err := parser.ParseFile(token.NewFileSet(), file.Name,
-		strings.NewReader(file.Body), parser.PackageClauseOnly)
+func stopAll(files []*CodeFile) {
+	mu.Lock()
+	defer mu.Unlock()
+	if !stopped {
+		stopped = true
+		for _, f := range files {
+			f.stop()
+		}
+	}
+}
+
+func (f *CodeFile) readPackage() error {
+	debug("Parsing package from ", f.Name)
+	file, err := parser.ParseFile(token.NewFileSet(), f.Name,
+		strings.NewReader(f.Body), parser.PackageClauseOnly)
 	if err != nil {
 		return err
 	}
-	file.pkg = f.Name.String()
-	if "main" == file.pkg {
-		file.executable = true
-		basename := path.Base(file.Name)
-		file.pkg = basename[:len(basename)-len(path.Ext(basename))]
+	f.pkg = file.Name.String()
+	if "main" == f.pkg {
+		f.executable = true
+		basename := path.Base(f.Name)
+		f.pkg = basename[:len(basename)-len(path.Ext(basename))]
 	}
 	return nil
 }
 
-func (f *CodeFile) Write() error {
-	if err := f.readPackage(); err != nil {
-		return err
+func (f *CodeFile) write() error {
+	debug("Writing file ", f.Name)
+	if f.lang == "go" || f.lang == "vdl" {
+		if err := f.readPackage(); err != nil {
+			return err
+		}
 	}
 	if err := os.MkdirAll(path.Join("src", f.pkg), 0777); err != nil {
 		return err
 	}
-	Log("Writing file ", f.Name)
 	return ioutil.WriteFile(path.Join("src", f.pkg, f.Name), []byte(f.Body), 0666)
 }
 
-func (f *CodeFile) Compile() error {
+func (f *CodeFile) compile() error {
+	debug("Compiling file ", f.Name)
 	var cmd *exec.Cmd
-	if path.Ext(f.Name) == ".vdl" {
-		cmd = MakeCmd("vdl", "generate", "--lang=go", f.pkg)
-	} else {
-		cmd = MakeCmd(path.Join(os.Getenv("VEYRON_ROOT"), "veyron/scripts/build/go"), "install", f.pkg)
+	switch f.lang {
+	case "js":
+		return nil
+	case "vdl":
+		cmd = makeCmd("vdl", "generate", "--lang=go", f.pkg)
+	case "go":
+		cmd = makeCmd(path.Join(os.Getenv("VEYRON_ROOT"), "veyron/scripts/build/go"), "install", f.pkg)
+	default:
+		return fmt.Errorf("Can't compile file %s with language %s.", f.Name, f.lang)
 	}
 	cmd.Stdout = cmd.Stderr
 	err := cmd.Run()
@@ -236,98 +290,83 @@
 	return err
 }
 
-func (f *CodeFile) Run(ch chan Exit) {
-	cmd := MakeCmd(path.Join("bin", f.pkg))
+func (f *CodeFile) startJs() error {
+	wsprProc, wsprPort, err := startWspr(f.index, f.identity)
+	if err != nil {
+		return fmt.Errorf("Error starting wspr: %v", err)
+	}
+	f.subProcs = append(f.subProcs, wsprProc)
+	os.Setenv("WSPR", "http://localhost:"+strconv.Itoa(wsprPort))
+	node := path.Join(os.Getenv("VEYRON_ROOT"), "/environment/cout/node/bin/node")
+	cmd := makeCmd(node, path.Join("src", f.Name))
+	f.cmd = cmd
+	return cmd.Start()
+}
+
+func (f *CodeFile) startGo() error {
+	cmd := makeCmd(path.Join("bin", f.pkg))
 	if f.identity != "" {
 		cmd.Env = append(cmd.Env, fmt.Sprintf("VEYRON_IDENTITY=%s", path.Join("ids", f.identity)))
 	}
-	f.proc = cmd
-	err := cmd.Run()
-	ch <- Exit{f.pkg, err}
+	f.cmd = cmd
+	return cmd.Start()
 }
 
-func (id Identity) Create() error {
-	if err := id.generate(); err != nil {
-		return err
+func (f *CodeFile) run(ch chan Exit) {
+	debug("Running", f.Name)
+	err := func() error {
+		mu.Lock()
+		defer mu.Unlock()
+		if stopped {
+			return fmt.Errorf("Execution already stopped, not running file %s", f.Name)
+		}
+
+		switch f.lang {
+		case "go":
+			return f.startGo()
+		case "js":
+			return f.startJs()
+		default:
+			return fmt.Errorf("Cannot run file: %v", f.Name)
+		}
+	}()
+	if err != nil {
+		debug("Error starting", f.Name)
+		ch <- Exit{f.Name, err}
+		return
 	}
-	if id.Blesser != "" || id.Duration != "" {
-		return id.bless()
-	}
-	return nil
+
+	// Wait for the process to exit and send result to channel.
+	go func() {
+		debug("Waiting for", f.Name)
+		err := f.cmd.Wait()
+		debug("Done waiting for", f.Name)
+		ch <- Exit{f.Name, err}
+	}()
 }
 
-func (id Identity) generate() error {
-	args := []string{"generate"}
-	if id.Blesser == "" && id.Duration == "" {
-		args = append(args, id.Name)
-	}
-	return runIdentity(args, path.Join("ids", id.Name))
-}
-
-func (id Identity) bless() error {
-	filename := path.Join("ids", id.Name)
-	var blesser string
-	if id.Blesser == "" {
-		blesser = filename
+func (f *CodeFile) stop() {
+	debug("Attempting to stop ", f.Name)
+	if f.cmd == nil {
+		debug("No cmd for", f.Name, "cannot stop.")
+	} else if f.cmd.Process == nil {
+		debug("f.cmd exists for", f.Name, "but f.cmd.Process is nil! Cannot stop!.")
 	} else {
-		blesser = path.Join("ids", id.Blesser)
+		debug("Sending SIGTERM to", f.Name)
+		f.cmd.Process.Signal(syscall.SIGTERM)
 	}
-	args := []string{"bless", "--with", blesser}
-	if id.Duration != "" {
-		args = append(args, "--for", id.Duration)
+
+	for _, subProc := range f.subProcs {
+		debug("Killing sub process for", f.Name)
+		subProc.Kill()
 	}
-	args = append(args, filename, id.Name)
-	tempfile := filename + ".tmp"
-	if err := runIdentity(args, tempfile); err != nil {
-		return err
-	}
-	return os.Rename(tempfile, filename)
 }
 
-func runIdentity(args []string, filename string) error {
-	cmd := MakeCmd("identity", args...)
-	out, err := os.Create(filename)
-	if err != nil {
-		return err
-	}
-	defer out.Close()
-	cmd.Stdout = out
-	return cmd.Run()
-}
-
-func StartMount() (proc *os.Process, err error) {
-	reader, writer := io.Pipe()
-	cmd := MakeCmd("mounttabled")
-	cmd.Stdout = writer
-	cmd.Stderr = cmd.Stdout
-	err = cmd.Start()
-	if err != nil {
-		return nil, err
-	}
-	buf := bufio.NewReader(reader)
-	pat := regexp.MustCompile("Mount table .+ endpoint: (.+)\n")
-
-	timeout := time.After(RUN_TIMEOUT)
-	ch := make(chan string)
-	go (func() {
-		for line, err := buf.ReadString('\n'); err == nil; line, err = buf.ReadString('\n') {
-			if groups := pat.FindStringSubmatch(line); groups != nil {
-				ch <- groups[1]
-			} else {
-				Log("mounttabld: %s", line)
-			}
-		}
-		close(ch)
-	})()
-	select {
-	case <-timeout:
-		log.Fatal("Timeout starting mounttabled")
-	case endpoint := <-ch:
-		if endpoint == "" {
-			log.Fatal("mounttable died")
-		}
-		Log("mount at ", endpoint)
-		return cmd.Process, os.Setenv("NAMESPACE_ROOT", endpoint)
-	}
-	return cmd.Process, err
+func makeCmd(prog string, args ...string) *exec.Cmd {
+	cmd := exec.Command(prog, args...)
+	// TODO(ribrdb): prefix output with the name of the binary
+	cmd.Stdout = os.Stdout
+	cmd.Stderr = os.Stderr
+	cmd.Env = os.Environ()
+	return cmd
 }
diff --git a/tools/playground/test.sh b/tools/playground/test.sh
index fa69c8e..420e8d1 100755
--- a/tools/playground/test.sh
+++ b/tools/playground/test.sh
@@ -38,20 +38,50 @@
 
   export GOPATH="$(pwd)"
   export PATH="$(pwd):$PATH"
-  test_with_files $DIR/pingpong/wire.vdl $DIR/pong/pong.go $DIR/ping/ping.go || shell_test::fail "line ${LINENO}: basic ping"
-  grep -q ping builder.out || shell_test::fail "line ${LINENO}: no ping"
-  grep -q pong builder.out || shell_test::fail "line ${LINENO}: no pong"
 
-  test_with_files $DIR/pingpong/wire.vdl $DIR/pong/pong.go $DIR/ping/ping.go $DIR/ids/authorized.id || shell_test::fail "line ${LINENO}: authorized id"
-  grep -q ping builder.out || shell_test::fail "line ${LINENO}: no ping"
-  grep -q pong builder.out || shell_test::fail "line ${LINENO}: no pong"
+  # Test without identities
 
-  test_with_files $DIR/pingpong/wire.vdl $DIR/pong/pong.go $DIR/ping/ping.go $DIR/ids/expired.id || shell_test::fail  "line ${LINENO}: failed to build with expired id"
+  test_with_files $DIR/pingpong/wire.vdl $DIR/pong/pong.go $DIR/ping/ping.go || shell_test::fail "line ${LINENO}: basic ping (go -> go)"
+  grep -q PING builder.out || shell_test::fail "line ${LINENO}: no PING"
+  grep -q PONG builder.out || shell_test::fail "line ${LINENO}: no PONG"
+
+  test_with_files $DIR/pong/pong.js $DIR/ping/ping.js || shell_test::fail "line ${LINENO}: basic ping (js -> js)"
+  grep -q PING builder.out || shell_test::fail "line ${LINENO}: no PING"
+  grep -q PONG builder.out || shell_test::fail "line ${LINENO}: no PONG"
+
+  test_with_files $DIR/pong/pong.go $DIR/ping/ping.js $DIR/pingpong/wire.vdl || shell_test::fail "line ${LINENO}: basic ping (js -> go)"
+  grep -q PING builder.out || shell_test::fail "line ${LINENO}: no PING"
+  grep -q PONG builder.out || shell_test::fail "line ${LINENO}: no PONG"
+
+  # Test with authorized identities
+
+  test_with_files $DIR/pong/pong.go $DIR/ping/ping.go $DIR/pingpong/wire.vdl $DIR/ids/authorized.id || shell_test::fail "line ${LINENO}: authorized id (go -> go)"
+  grep -q PING builder.out || shell_test::fail "line ${LINENO}: no PING"
+  grep -q PONG builder.out || shell_test::fail "line ${LINENO}: no PONG"
+
+  test_with_files $DIR/pong/pong.js $DIR/ping/ping.js $DIR/ids/authorized.id || shell_test::fail "line ${LINENO}: authorized id (js -> js)"
+  grep -q PING builder.out || shell_test::fail "line ${LINENO}: no PING"
+  grep -q PONG builder.out || shell_test::fail "line ${LINENO}: no PONG"
+
+  # Test with expired identities
+
+  test_with_files $DIR/pong/pong.go $DIR/ping/ping.go $DIR/pingpong/wire.vdl $DIR/ids/expired.id || shell_test::fail  "line ${LINENO}: failed to build with expired id (go -> go)"
   grep -q "ipc: not authorized" builder.out || shell_test::fail "line ${LINENO}: rpc with expired id succeeded"
 
-  test_with_files $DIR/pingpong/wire.vdl $DIR/pong/pong.go $DIR/ping/ping.go $DIR/ids/unauthorized.id || shell_test::fail  "line ${LINENO}: failed to build with unauthorized id"
+  test_with_files $DIR/pong/pong.js $DIR/ping/ping.js $DIR/ids/expired.id || shell_test::fail  "line ${LINENO}: failed to build with expired id (js -> js)"
+  # TODO(nlacasse): The error message in this case is very bad. Clean up the
+  # veyron.js errors and change this to something reasonable.
+  grep -q "error serving service:" builder.out || shell_test::fail "line ${LINENO}: rpc with expired id succeeded"
+
+  # Test with unauthorized identities
+
+  test_with_files $DIR/pong/pong.go $DIR/ping/ping.go $DIR/pingpong/wire.vdl $DIR/ids/unauthorized.id || shell_test::fail  "line ${LINENO}: failed to build with unauthorized id (go -> go)"
   grep -q "ipc: not authorized" builder.out || shell_test::fail "line ${LINENO}: rpc with unauthorized id succeeded"
 
+  # TODO(nlacasse): Write the javascript version of this test once the
+  # javascript implementation is capable of checking that an identity is
+  # authorized.
+
   shell_test::pass
 }
 
diff --git a/tools/playground/testdata/ids/authorized.id b/tools/playground/testdata/ids/authorized.id
index 8a1f950..cca29d1 100644
--- a/tools/playground/testdata/ids/authorized.id
+++ b/tools/playground/testdata/ids/authorized.id
@@ -5,11 +5,11 @@
 {
   "Name": "myserver",
   "Blesser": "myorg",
-  "Files": ["pong.go"]
+  "Files": ["pong.go", "pong.js"]
 },
 {
   "Name": "myclient",
   "Blesser": "myserver",
-  "Files": ["ping.go"]
+  "Files": ["ping.go", "ping.js"]
 }
-]
\ No newline at end of file
+]
diff --git a/tools/playground/testdata/ids/expired.id b/tools/playground/testdata/ids/expired.id
index 6ff368d..2fee1f3 100644
--- a/tools/playground/testdata/ids/expired.id
+++ b/tools/playground/testdata/ids/expired.id
@@ -2,6 +2,6 @@
 {
   "Name": "expired",
   "Duration": "0s",
-  "Files": ["ping.go", "pong.go"]
+  "Files": ["ping.go", "pong.go", "ping.js", "pong.js"]
 }
-]
\ No newline at end of file
+]
diff --git a/tools/playground/testdata/ids/unauthorized.id b/tools/playground/testdata/ids/unauthorized.id
index 544343b..cd39f52 100644
--- a/tools/playground/testdata/ids/unauthorized.id
+++ b/tools/playground/testdata/ids/unauthorized.id
@@ -5,11 +5,11 @@
 {
   "Name": "myserver",
   "Blesser": "myorg",
-  "Files": ["pong.go"]
+  "Files": ["pong.go", "pong.js"]
 },
 {
   "Name": "myclient",
   "Blesser": "myorg",
-  "Files": ["ping.go"]
+  "Files": ["ping.go", "ping.js"]
 }
-]
\ No newline at end of file
+]
diff --git a/tools/playground/testdata/ping/ping.go b/tools/playground/testdata/ping/ping.go
index 725c102..2372abe 100644
--- a/tools/playground/testdata/ping/ping.go
+++ b/tools/playground/testdata/ping/ping.go
@@ -16,7 +16,7 @@
 		log.Fatal("error binding to server: ", err)
 	}
 
-	pong, err := s.Ping(runtime.NewContext(), "ping")
+	pong, err := s.Ping(runtime.NewContext(), "PING")
 	if err != nil {
 		log.Fatal("error pinging: ", err)
 	}
diff --git a/tools/playground/testdata/ping/ping.js b/tools/playground/testdata/ping/ping.js
new file mode 100644
index 0000000..493ce2e
--- /dev/null
+++ b/tools/playground/testdata/ping/ping.js
@@ -0,0 +1,23 @@
+var veyron = require('veyron');
+
+veyron.init(function(err, rt){
+  if (err) throw err;
+
+  function bindAndSendPing(){
+    rt.bindTo('pingpong', function(err, s){
+      if (err) throw err;
+
+      s.ping('PING', function(err, pong){
+        if (err) throw err;
+
+        console.log(pong);
+        process.exit(0);
+      });
+    });
+  }
+
+  // Give the server some time to start.
+  // TODO(nlacasse): how are we going to handle this race condition in the
+  // actual playground?
+  setTimeout(bindAndSendPing, 500);
+});
diff --git a/tools/playground/testdata/pong/pong.go b/tools/playground/testdata/pong/pong.go
index fa961f8..d6c5a42 100644
--- a/tools/playground/testdata/pong/pong.go
+++ b/tools/playground/testdata/pong/pong.go
@@ -14,7 +14,7 @@
 
 func (f *pongd) Ping(_ ipc.ServerContext, message string) (result string, err error) {
 	fmt.Println(message)
-	return "pong", nil
+	return "PONG", nil
 }
 
 func main() {
diff --git a/tools/playground/testdata/pong/pong.js b/tools/playground/testdata/pong/pong.js
new file mode 100644
index 0000000..8d2b76c
--- /dev/null
+++ b/tools/playground/testdata/pong/pong.js
@@ -0,0 +1,16 @@
+var veyron = require('veyron');
+
+var pingPongService = {
+  ping: function(msg){
+    console.log(msg);
+    return 'PONG';
+  }
+};
+
+veyron.init(function(err, rt) {
+  if (err) throw err;
+
+  rt.serve('pingpong', pingPongService, function(err) {
+    if (err) throw err;
+  });
+});