blob: dc16f700c03845d5737ef0a0f14b1f59a72b465d [file] [log] [blame]
// 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"
"encoding/json"
"flag"
"fmt"
"io"
"net"
"net/http"
"os"
"os/exec"
"os/signal"
"strings"
"syscall"
"time"
"v.io/v23"
"v.io/v23/options"
"v.io/v23/rpc"
"v.io/v23/security"
"v.io/v23/security/access"
"v.io/x/ref/envvar"
"v.io/x/ref/lib/signals"
"v.io/x/ref/profiles"
"v.io/x/ref/services/identity/identitylib"
"v.io/x/ref/services/mounttable/mounttablelib"
"v.io/x/ref/test/expect"
"v.io/x/ref/test/modules"
"v.io/x/browser/sample/sampleworld"
)
const (
SampleWorldCommand = "sampleWorld" // The modules library command.
RunMTCommand = "runMT"
stdoutLog = "tmp/runner.stdout.log" // Used as stdout drain when shutting down.
stderrLog = "tmp/runner.stderr.log" // Used as stderr drain when shutting down.
)
var (
// Flags used as input to this program.
runSample bool
serveHTTP bool
portHTTP string
rootHTTP string
runTests bool
runTestsWatch bool
)
func init() {
modules.RegisterChild(SampleWorldCommand, "desc", sampleWorld)
modules.RegisterChild(RunMTCommand, "", runMT)
flag.BoolVar(&runSample, "runSample", false, "if true, runs sample services")
flag.BoolVar(&serveHTTP, "serveHTTP", false, "if true, serves HTTP")
flag.StringVar(&portHTTP, "portHTTP", "9001", "default 9001, the port to serve HTTP on")
flag.StringVar(&rootHTTP, "rootHTTP", ".", "default '.', the root HTTP folder path")
flag.BoolVar(&runTests, "runTests", false, "if true, runs the namespace browser tests")
flag.BoolVar(&runTestsWatch, "runTestsWatch", false, "if true && runTests, runs the tests in watch mode")
}
func runMT(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
ctx, shutdown := v23.Init()
defer shutdown()
lspec := v23.GetListenSpec(ctx)
server, err := v23.NewServer(ctx, options.ServesMountTable(true))
if err != nil {
return fmt.Errorf("root failed: %v", err)
}
mp := args[0]
mt, err := mounttablelib.NewMountTableDispatcher("", "", "mounttable")
if err != nil {
return fmt.Errorf("mounttablelib.NewMountTableDispatcher failed: %s", err)
}
eps, err := server.Listen(lspec)
if err != nil {
return fmt.Errorf("server.Listen failed: %s", err)
}
if err := server.ServeDispatcher(mp, mt); err != nil {
return fmt.Errorf("root failed: %s", err)
}
fmt.Fprintf(stdout, "PID=%d\n", os.Getpid())
for _, ep := range eps {
fmt.Fprintf(stdout, "MT_NAME=%s\n", ep.Name())
}
modules.WaitForEOF(stdin)
return nil
}
// Helper function to simply print an error and then exit.
func exitOnError(err error, desc string) {
if err != nil {
fmt.Fprintln(os.Stderr, desc, err)
os.Exit(1)
}
}
// updateVars captures the vars from the given Handle's stdout and adds them to
// the given vars map, overwriting existing entries.
func updateVars(h modules.Handle, vars map[string]string, varNames ...string) error {
varsToAdd := map[string]bool{}
for _, v := range varNames {
varsToAdd[v] = true
}
numLeft := len(varsToAdd)
s := expect.NewSession(nil, h.Stdout(), 30*time.Second)
for {
l := s.ReadLine()
if err := s.OriginalError(); err != nil {
return err // EOF or otherwise
}
parts := strings.Split(l, "=")
if len(parts) != 2 {
return fmt.Errorf("Unexpected line: %s", l)
}
if _, ok := varsToAdd[parts[0]]; ok {
numLeft--
vars[parts[0]] = parts[1]
if numLeft == 0 {
break
}
}
}
return nil
}
// The module command for running the sample world.
func sampleWorld(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
ctx, shutdown := v23.Init()
defer shutdown()
sampleworld.RunSampleWorld(ctx, func() {
modules.WaitForEOF(stdin)
})
return nil
}
func main() {
if modules.IsModulesChildProcess() {
exitOnError(modules.Dispatch(), "Failed to dispatch module")
return
}
// If we ever get a SIGHUP (terminal closes), then end the program.
signalChannel := make(chan os.Signal)
signal.Notify(signalChannel, syscall.SIGHUP)
go func() {
sig := <-signalChannel
switch sig {
case syscall.SIGHUP:
os.Exit(1)
}
}()
// Try running the program; on failure, exit with error status code.
if !run() {
os.Exit(1)
}
}
// Returns the first ipv4 address found or an error
func getFirstIPv4Address() (string, error) {
ifaces, err := net.Interfaces()
if err != nil {
return "", fmt.Errorf("No net interfaces found")
}
for _, i := range ifaces {
addrs, err := i.Addrs()
if err != nil {
continue
}
for _, addr := range addrs {
var ip net.IP
if v, ok := addr.(*net.IPNet); ok {
ip = v.IP
}
if ip == nil || ip.IsLoopback() {
continue
}
ip = ip.To4()
if ip == nil {
continue // not an ipv4 address
}
return ip.String(), nil
}
}
return "", fmt.Errorf("no ipv4 addresses were found")
}
// Runs the services and cleans up afterwards.
// Returns true if the run was successful.
func run() bool {
ctx, shutdown := v23.Init()
defer shutdown()
// In order to prevent conflicts, tests and webapp use different mounttable ports.
port := 8884
cottagePort := 8885
housePort := 8886
host := "localhost"
if !runTests {
// Get the IP address to serve at, since this is external-facing.
sampleHost, err := getFirstIPv4Address()
exitOnError(err, "Could not get host IP address")
fmt.Printf("Using host %s\n", sampleHost)
host = sampleHost
}
// Start a new shell module.
vars := map[string]string{}
sh, err := modules.NewShell(ctx, nil, false, nil)
if err != nil {
panic(fmt.Sprintf("modules.NewShell: %s", err))
}
// Collect the output of this shell on termination.
err = os.MkdirAll("tmp", 0750)
exitOnError(err, "Could not make temp directory")
outFile, err := os.Create(stdoutLog)
exitOnError(err, "Could not open stdout log file")
defer outFile.Close()
errFile, err := os.Create(stderrLog)
exitOnError(err, "Could not open stderr log file")
defer errFile.Close()
defer sh.Cleanup(outFile, errFile)
// ns.dev.v.io Mounttable only allows one to publish under users/<name>
// for a user that poses the blessing /dev.v.io/root/users/<name>
// therefore to find a <name> we can publish under, we remove /dev.v.io/root/users/
// from the default blessing name.
blessing := string(security.DefaultBlessingPatterns(v23.GetPrincipal(ctx))[0])
name := strings.TrimPrefix(blessing, "dev.v.io/root/users/")
nsPrefix := fmt.Sprintf("/ns.dev.v.io:8101/users/%s", name)
exitOnError(err, "Failed to obtain hostname")
rootName := fmt.Sprintf("%s/sample-world", nsPrefix)
fmt.Printf("Publishing under %s\n", rootName)
if runTests {
// Run a mounttable for tests
hRoot, err := sh.Start(RunMTCommand, nil, "--v23.tcp.protocol=wsh", fmt.Sprintf("--v23.tcp.address=%s:%d", host, port), "root")
exitOnError(err, "Failed to start root mount table")
exitOnError(updateVars(hRoot, vars, "MT_NAME"), "Failed to get MT_NAME")
defer hRoot.Shutdown(outFile, errFile)
// Set envvar.NamespacePrefix env var, consumed downstream.
sh.SetVar(envvar.NamespacePrefix, vars["MT_NAME"])
v23.GetNamespace(ctx).SetRoots(vars["MT_NAME"])
// Run the cottage mounttable at host/cottage.
hCottage, err := sh.Start(RunMTCommand, nil, "--v23.tcp.protocol=wsh", fmt.Sprintf("--v23.tcp.address=%s:%d", host, cottagePort), "cottage")
exitOnError(err, "Failed to start cottage mount table")
expect.NewSession(nil, hCottage.Stdout(), 30*time.Second)
defer hCottage.Shutdown(outFile, errFile)
// run the house mounttable at host/house.
hHouse, err := sh.Start(RunMTCommand, nil, "--v23.tcp.protocol=wsh", fmt.Sprintf("--v23.tcp.address=%s:%d", host, housePort), "house")
exitOnError(err, "Failed to start house mount table")
expect.NewSession(nil, hHouse.Stdout(), 30*time.Second)
defer hHouse.Shutdown(outFile, errFile)
} else {
sh.SetVar(envvar.NamespacePrefix, rootName)
v23.GetNamespace(ctx).SetRoots(rootName)
}
// Possibly run the sample world.
if runSample {
authorize := security.DefaultBlessingPatterns(v23.GetPrincipal(ctx))[0]
fmt.Println("Running Sample World")
hSample, err := sh.Start(SampleWorldCommand, nil, "--v23.tcp.protocol=wsh", fmt.Sprintf("--v23.tcp.address=%s:0", host), fmt.Sprintf("--authorize=%s", authorize))
exitOnError(err, "Failed to start sample world")
expect.NewSession(nil, hSample.Stdout(), 30*time.Second)
defer hSample.Shutdown(outFile, errFile)
}
// Possibly serve the public bundle at the portHTTP.
if serveHTTP {
fmt.Printf("Also serving HTTP at %s for %s\n", portHTTP, rootHTTP)
http.ListenAndServe(":"+portHTTP, http.FileServer(http.Dir(rootHTTP)))
}
// Just print out the collected variables. This is for debugging purposes.
bytes, err := json.Marshal(vars)
exitOnError(err, "Failed to marshal the collected variables")
fmt.Println(string(bytes))
// Possibly run the tests in Prova.
if runTests {
// Also set HOUSE_MOUNTTABLE (used in the tests)
os.Setenv("HOUSE_MOUNTTABLE", fmt.Sprintf("/%s:%d", host, housePort))
lspec := v23.GetListenSpec(ctx)
lspec.Addrs = rpc.ListenAddrs{{"wsh", ":0"}}
// Allow all processes started by this runner to use the proxy.
proxyACL := access.AccessList{In: security.DefaultBlessingPatterns(v23.GetPrincipal(ctx))}
proxyShutdown, proxyEndpoint, err := profiles.NewProxy(ctx, lspec, proxyACL, "test/proxy")
exitOnError(err, "Failed to start proxy")
defer proxyShutdown()
vars["PROXY_NAME"] = proxyEndpoint.Name()
hIdentityd, err := sh.Start(identitylib.TestIdentitydCommand, nil, "--v23.tcp.protocol=wsh", "--v23.tcp.address=:0", "--v23.proxy=test/proxy", "--http-addr=localhost:0")
exitOnError(err, "Failed to start identityd")
exitOnError(updateVars(hIdentityd, vars, "TEST_IDENTITYD_NAME", "TEST_IDENTITYD_HTTP_ADDR"), "Failed to obtain identityd address")
defer hIdentityd.Shutdown(outFile, errFile)
// Setup a lot of environment variables; these are used for the tests and building the test extension.
os.Setenv(envvar.NamespacePrefix, vars["MT_NAME"])
os.Setenv("PROXY_ADDR", vars["PROXY_NAME"])
os.Setenv("IDENTITYD", fmt.Sprintf("%s/google", vars["TEST_IDENTITYD_NAME"]))
os.Setenv("IDENTITYD_BLESSING_URL", fmt.Sprintf("%s/auth/blessing-root", vars["TEST_IDENTITYD_HTTP_ADDR"]))
os.Setenv("DEBUG", "false")
testsOk := runProva()
fmt.Println("Cleaning up launched services...")
return testsOk
}
// Not in a test, so run until the program is killed.
<-signals.ShutdownOnSignals(ctx)
return true
}
// Run the prova tests and convert its tap output to xunit.
func runProva() bool {
// This is also useful information for routing the test output.
V23_ROOT := os.Getenv("V23_ROOT")
VANADIUM_JS := fmt.Sprintf("%s/release/javascript/core", V23_ROOT)
VANADIUM_BROWSER := fmt.Sprintf("%s/release/projects/browser", V23_ROOT)
TAP_XUNIT := fmt.Sprintf("%s/node_modules/.bin/tap-xunit", VANADIUM_BROWSER)
XUNIT_OUTPUT_FILE := os.Getenv("XUNIT_OUTPUT_FILE")
if XUNIT_OUTPUT_FILE == "" {
XUNIT_OUTPUT_FILE = fmt.Sprintf("%s/test_output.xml", os.Getenv("TMPDIR"))
}
TAP_XUNIT_OPTIONS := " --package=namespace-browser"
// Make sure we're in the right folder when we run make test-extension.
vbroot, err := os.Open(VANADIUM_BROWSER)
exitOnError(err, "Failed to open vanadium browser dir")
err = vbroot.Chdir()
exitOnError(err, "Failed to change to vanadium browser dir")
// Make the test-extension, this should also remove the old one.
fmt.Println("Rebuilding test extension...")
cmdExtensionClean := exec.Command("rm", "-fr", fmt.Sprintf("%s/extension/build-test", VANADIUM_JS))
err = cmdExtensionClean.Run()
exitOnError(err, "Failed to clean test extension")
cmdExtensionBuild := exec.Command("make", "-C", fmt.Sprintf("%s/extension", VANADIUM_JS), "build-test")
err = cmdExtensionBuild.Run()
exitOnError(err, "Failed to build test extension")
// These are the basic prova options.
options := []string{
"test/**/*.js",
"--browser",
"--includeFilenameAsPackage",
"--launch",
"chrome",
"--plugin",
"proxyquireify/plugin",
"--transform",
"envify,./main-transform",
"--log",
"tmp/chrome.log",
fmt.Sprintf("--options=--load-extension=%s/extension/build-test/,--ignore-certificate-errors,--enable-logging=stderr", VANADIUM_JS),
}
// Normal tests have a few more options and a different port from the watch tests.
var PROVA_PORT int
if !runTestsWatch {
PROVA_PORT = 8893
options = append(options, "--headless", "--quit", "--progress", "--tap")
fmt.Printf("\033[34m-Executing tests. See %s for test xunit output.\033[0m\n", XUNIT_OUTPUT_FILE)
} else {
PROVA_PORT = 8894
fmt.Println("\033[34m-Running tests in watch mode.\033[0m")
}
options = append(options, "--port", fmt.Sprintf("%d", PROVA_PORT))
// This is the prova command.
cmdProva := exec.Command(
fmt.Sprintf("%s/node_modules/.bin/prova", VANADIUM_BROWSER),
options...,
)
fmt.Printf("\033[34m-Go to \033[32mhttp://0.0.0.0:%d\033[34m to see tests running.\033[0m\n", PROVA_PORT)
fmt.Println(cmdProva)
// Collect the prova stdout. This information needs to be sent to xunit.
provaOut, err := cmdProva.StdoutPipe()
exitOnError(err, "Failed to get prova stdout pipe")
// Setup the tap to xunit command. It uses Prova's stdout as input.
// The output will got the xunit output file.
cmdTap := exec.Command(TAP_XUNIT, TAP_XUNIT_OPTIONS)
cmdTap.Stdin = io.TeeReader(provaOut, os.Stdout) // Tee the prova output to see it on the console too.
outfile, err := os.Create(XUNIT_OUTPUT_FILE)
exitOnError(err, "Failed to create xunit output file")
defer outfile.Close()
bufferedWriter := bufio.NewWriter(outfile)
cmdTap.Stdout = bufferedWriter
defer bufferedWriter.Flush() // Ensure that the full xunit output is written.
// We start the tap command...
err = cmdTap.Start()
exitOnError(err, "Failed to start tap to xunit command")
// Meanwhile, run Prova to completion. If there was an error, print ERROR, otherwise PASS.
err = cmdProva.Run()
testsOk := true
if err != nil {
fmt.Println(err)
fmt.Println("\033[31m\033[1mERROR\033[0m")
testsOk = false
} else {
fmt.Println("\033[32m\033[1mPASS\033[0m")
}
// Wait for tap to xunit to finish itself off. This file will be ready for reading by Jenkins.
fmt.Println("Converting Tap output to XUnit")
err = cmdTap.Wait()
exitOnError(err, "Failed tap to xunit conversion")
return testsOk
}