Merge pull request #57 from danw/testrunner

Add a test runner
diff --git a/Blueprints b/Blueprints
index 70d57db..df3ad60 100644
--- a/Blueprints
+++ b/Blueprints
@@ -125,6 +125,11 @@
 )
 
 bootstrap_core_go_binary(
+    name = "gotestrunner",
+    srcs = ["gotestrunner/gotestrunner.go"],
+)
+
+bootstrap_core_go_binary(
     name = "choosestage",
     srcs = ["choosestage/choosestage.go"],
 )
diff --git a/bootstrap/bootstrap.go b/bootstrap/bootstrap.go
index dbc4710..ed5debc 100644
--- a/bootstrap/bootstrap.go
+++ b/bootstrap/bootstrap.go
@@ -30,6 +30,7 @@
 	pctx = blueprint.NewPackageContext("github.com/google/blueprint/bootstrap")
 
 	goTestMainCmd   = pctx.StaticVariable("goTestMainCmd", filepath.Join(bootstrapDir, "bin", "gotestmain"))
+	goTestRunnerCmd = pctx.StaticVariable("goTestRunnerCmd", filepath.Join(bootstrapDir, "bin", "gotestrunner"))
 	chooseStageCmd  = pctx.StaticVariable("chooseStageCmd", filepath.Join(bootstrapDir, "bin", "choosestage"))
 	pluginGenSrcCmd = pctx.StaticVariable("pluginGenSrcCmd", filepath.Join(bootstrapDir, "bin", "loadplugins"))
 
@@ -64,7 +65,7 @@
 
 	test = pctx.StaticRule("test",
 		blueprint.RuleParams{
-			Command:     "(cd $pkgSrcDir && $$OLDPWD/$in -test.short) && touch $out",
+			Command:     "$goTestRunnerCmd -p $pkgSrcDir -f $out -- $in -test.short",
 			Description: "test $pkg",
 		},
 		"pkg", "pkgSrcDir")
@@ -111,7 +112,7 @@
 
 	docsDir = filepath.Join(bootstrapDir, "docs")
 
-	bootstrapDir = filepath.Join("$buildDir", bootstrapSubDir)
+	bootstrapDir     = filepath.Join("$buildDir", bootstrapSubDir)
 	miniBootstrapDir = filepath.Join("$buildDir", miniBootstrapSubDir)
 )
 
@@ -553,9 +554,10 @@
 	})
 
 	ctx.Build(pctx, blueprint.BuildParams{
-		Rule:    test,
-		Outputs: []string{testPassed},
-		Inputs:  []string{testFile},
+		Rule:      test,
+		Outputs:   []string{testPassed},
+		Inputs:    []string{testFile},
+		Implicits: []string{"$goTestRunnerCmd"},
 		Args: map[string]string{
 			"pkg":       pkgPath,
 			"pkgSrcDir": filepath.Dir(testFiles[0]),
diff --git a/build.ninja.in b/build.ninja.in
index a782142..751dec0 100644
--- a/build.ninja.in
+++ b/build.ninja.in
@@ -190,7 +190,7 @@
 # Variant:
 # Type:    bootstrap_core_go_binary
 # Factory: github.com/google/blueprint/bootstrap.func·005
-# Defined: Blueprints:127:1
+# Defined: Blueprints:132:1
 
 build ${g.bootstrap.buildDir}/.bootstrap/choosestage/obj/choosestage.a: $
         g.bootstrap.compile ${g.bootstrap.srcDir}/choosestage/choosestage.go | $
@@ -230,6 +230,28 @@
 default ${g.bootstrap.BinDir}/gotestmain
 
 # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
+# Module:  gotestrunner
+# Variant:
+# Type:    bootstrap_core_go_binary
+# Factory: github.com/google/blueprint/bootstrap.func·005
+# Defined: Blueprints:127:1
+
+build ${g.bootstrap.buildDir}/.bootstrap/gotestrunner/obj/gotestrunner.a: $
+        g.bootstrap.compile ${g.bootstrap.srcDir}/gotestrunner/gotestrunner.go $
+        | ${g.bootstrap.compileCmd}
+    pkgPath = gotestrunner
+default ${g.bootstrap.buildDir}/.bootstrap/gotestrunner/obj/gotestrunner.a
+
+build ${g.bootstrap.buildDir}/.bootstrap/gotestrunner/obj/a.out: $
+        g.bootstrap.link $
+        ${g.bootstrap.buildDir}/.bootstrap/gotestrunner/obj/gotestrunner.a | $
+        ${g.bootstrap.linkCmd}
+default ${g.bootstrap.buildDir}/.bootstrap/gotestrunner/obj/a.out
+build ${g.bootstrap.BinDir}/gotestrunner: g.bootstrap.cp $
+        ${g.bootstrap.buildDir}/.bootstrap/gotestrunner/obj/a.out
+default ${g.bootstrap.BinDir}/gotestrunner
+
+# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
 # Module:  minibp
 # Variant:
 # Type:    bootstrap_core_go_binary
@@ -279,7 +301,8 @@
         ${g.bootstrap.buildDir}/.bootstrap/primary.ninja.in.timestamp: $
         s.bootstrap.primarybp ${g.bootstrap.srcDir}/Blueprints | $
         ${g.bootstrap.BinDir}/choosestage ${g.bootstrap.BinDir}/gotestmain $
-        ${g.bootstrap.BinDir}/minibp ${g.bootstrap.srcDir}/Blueprints
+        ${g.bootstrap.BinDir}/gotestrunner ${g.bootstrap.BinDir}/minibp $
+        ${g.bootstrap.srcDir}/Blueprints
     outfile = ${g.bootstrap.buildDir}/.bootstrap/primary.ninja.in
     timestamp = ${g.bootstrap.buildDir}/.bootstrap/primary.ninja.in.timestamp
     timestampdep = ${g.bootstrap.buildDir}/.bootstrap/primary.ninja.in.timestamp.d
diff --git a/gotestrunner/gotestrunner.go b/gotestrunner/gotestrunner.go
new file mode 100644
index 0000000..20fbe1c
--- /dev/null
+++ b/gotestrunner/gotestrunner.go
@@ -0,0 +1,103 @@
+// Copyright 2015 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package main
+
+import (
+	"bufio"
+	"bytes"
+	"flag"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"syscall"
+)
+
+var (
+	chdir = flag.String("p", "", "Change to a path before executing test")
+	touch = flag.String("f", "", "Write a file on success")
+)
+
+// This will copy the stdout from the test process to our stdout
+// unless it only contains "PASS\n".
+func handleStdout(stdout io.Reader) {
+	reader := bufio.NewReader(stdout)
+
+	// This is intentionally 6 instead of 5 to check for EOF
+	buf, _ := reader.Peek(6)
+	if bytes.Equal(buf, []byte("PASS\n")) {
+		return
+	}
+
+	io.Copy(os.Stdout, reader)
+}
+
+func main() {
+	flag.Parse()
+
+	if flag.NArg() == 0 {
+		fmt.Fprintln(os.Stderr, "error: must pass at least one test executable")
+		os.Exit(1)
+	}
+
+	test, err := filepath.Abs(flag.Arg(0))
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "error: Failed to locate test binary: %s", err)
+	}
+
+	cmd := exec.Command(test, flag.Args()[1:]...)
+	if *chdir != "" {
+		cmd.Dir = *chdir
+	}
+
+	cmd.Stderr = os.Stderr
+	stdout, err := cmd.StdoutPipe()
+	if err != nil {
+		fmt.Fprintln(os.Stderr, err)
+		os.Exit(1)
+	}
+
+	err = cmd.Start()
+	if err != nil {
+		fmt.Fprintln(os.Stderr, err)
+		os.Exit(1)
+	}
+
+	handleStdout(stdout)
+
+	if err = cmd.Wait(); err != nil {
+		if e, ok := err.(*exec.ExitError); ok {
+			if status, ok := e.Sys().(syscall.WaitStatus); ok && status.Exited() {
+				os.Exit(status.ExitStatus())
+			} else if status.Signaled() {
+				fmt.Fprintf(os.Stderr, "test got signal %s\n", status.Signal())
+				os.Exit(1)
+			}
+		}
+		fmt.Fprintln(os.Stderr, err)
+		os.Exit(1)
+	}
+
+	if *touch != "" {
+		err = ioutil.WriteFile(*touch, []byte{}, 0666)
+		if err != nil {
+			panic(err)
+		}
+	}
+
+	os.Exit(0)
+}