tools: Add --fxc option to test-runner

And add a reasonable timeout to tests.

Change-Id: I362d44f2a799562d236a80f5c35d50d93e8d094a
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/56769
Kokoro: Kokoro <noreply+kokoro@google.com>
Commit-Queue: Ben Clayton <bclayton@google.com>
Reviewed-by: David Neto <dneto@google.com>
diff --git a/tools/src/cmd/test-runner/main.go b/tools/src/cmd/test-runner/main.go
index 8d4f855..f30025a 100644
--- a/tools/src/cmd/test-runner/main.go
+++ b/tools/src/cmd/test-runner/main.go
@@ -16,6 +16,7 @@
 package main
 
 import (
+	"context"
 	"flag"
 	"fmt"
 	"io/ioutil"
@@ -25,6 +26,7 @@
 	"runtime"
 	"sort"
 	"strings"
+	"time"
 	"unicode/utf8"
 
 	"dawn.googlesource.com/tint/tools/src/fileutils"
@@ -36,6 +38,8 @@
 type outputFormat string
 
 const (
+	testTimeout = 30 * time.Second
+
 	wgsl   = outputFormat("wgsl")
 	spvasm = outputFormat("spvasm")
 	msl    = outputFormat("msl")
@@ -68,11 +72,12 @@
 func run() error {
 	var formatList, filter, dxcPath, xcrunPath string
 	numCPU := runtime.NumCPU()
-	verbose, generateExpected, generateSkip := false, false, false
+	fxc, verbose, generateExpected, generateSkip := false, false, false, false
 	flag.StringVar(&formatList, "format", "all", "comma separated list of formats to emit. Possible values are: all, wgsl, spvasm, msl, hlsl")
 	flag.StringVar(&filter, "filter", "**.wgsl, **.spvasm, **.spv", "comma separated list of glob patterns for test files")
 	flag.StringVar(&dxcPath, "dxc", "", "path to DXC executable for validating HLSL output")
 	flag.StringVar(&xcrunPath, "xcrun", "", "path to xcrun executable for validating MSL output")
+	flag.BoolVar(&fxc, "fxc", false, "validate with FXC instead of DXC")
 	flag.BoolVar(&verbose, "verbose", false, "print all run tests, including rows that all pass")
 	flag.BoolVar(&generateExpected, "generate-expected", false, "create or update all expected outputs")
 	flag.BoolVar(&generateSkip, "generate-skip", false, "create or update all expected outputs that fail with SKIP")
@@ -186,12 +191,12 @@
 		fmt.Printf("%-4s", tool.lang)
 		color.Unset()
 		fmt.Printf(" validation ")
-		if *tool.path == "" {
-			color.Set(color.FgRed)
-			fmt.Printf("DISABLED")
-		} else {
+		if *tool.path != "" || (fxc && tool.lang == "hlsl") {
 			color.Set(color.FgGreen)
 			fmt.Printf("ENABLED")
+		} else {
+			color.Set(color.FgRed)
+			fmt.Printf("DISABLED")
 		}
 		color.Unset()
 		fmt.Println()
@@ -215,7 +220,7 @@
 	for cpu := 0; cpu < numCPU; cpu++ {
 		go func() {
 			for job := range pendingJobs {
-				job.run(dir, exe, dxcPath, xcrunPath, generateExpected, generateSkip)
+				job.run(dir, exe, fxc, dxcPath, xcrunPath, generateExpected, generateSkip)
 			}
 		}()
 	}
@@ -451,7 +456,7 @@
 	result chan status
 }
 
-func (j job) run(wd, exe, dxcPath, xcrunPath string, generateExpected, generateSkip bool) {
+func (j job) run(wd, exe string, fxc bool, dxcPath, xcrunPath string, generateExpected, generateSkip bool) {
 	j.result <- func() status {
 		// Is there an expected output?
 		expected := loadExpectedFile(j.file, j.format)
@@ -485,7 +490,10 @@
 			args = append(args, "--validate") // spirv-val is statically linked, always available
 			validate = true
 		case hlsl:
-			if dxcPath != "" {
+			if fxc {
+				args = append(args, "--fxc")
+				validate = true
+			} else if dxcPath != "" {
 				args = append(args, "--dxc", dxcPath)
 				validate = true
 			}
@@ -644,11 +652,17 @@
 
 // invoke runs the executable 'exe' with the provided arguments.
 func invoke(wd, exe string, args ...string) (ok bool, output string) {
-	cmd := exec.Command(exe, args...)
+	ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
+	defer cancel()
+
+	cmd := exec.CommandContext(ctx, exe, args...)
 	cmd.Dir = wd
 	out, err := cmd.CombinedOutput()
 	str := string(out)
 	if err != nil {
+		if ctx.Err() == context.DeadlineExceeded {
+			return false, fmt.Sprintf("test timed out after %v", testTimeout)
+		}
 		if str != "" {
 			return false, str
 		}