test-runner: Add option to limit number of threads

Fixed: tint:830
Change-Id: I46696d0f72760549743f95a35ea21fef96269146
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/51924
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 0267d15..c7afb39 100644
--- a/tools/src/cmd/test-runner/main.go
+++ b/tools/src/cmd/test-runner/main.go
@@ -22,6 +22,7 @@
 	"os"
 	"os/exec"
 	"path/filepath"
+	"runtime"
 	"sort"
 	"strconv"
 	"strings"
@@ -68,10 +69,12 @@
 
 func run() error {
 	var formatList, filter string
+	numCPU := runtime.NumCPU()
 	generateExpected := 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.BoolVar(&generateExpected, "generate-expected", false, "create or update all expected outputs")
+	flag.IntVar(&numCPU, "j", numCPU, "maximum number of concurrent threads to run tests")
 	flag.Usage = showUsage
 	flag.Parse()
 
@@ -144,82 +147,41 @@
 		}
 	}
 
-	// Structures to hold the results of the tests
-	type statusCode string
-	const (
-		fail statusCode = "FAIL"
-		pass statusCode = "PASS"
-		skip statusCode = "SKIP"
-	)
-	type status struct {
-		code statusCode
-		err  error
-	}
-	type result map[outputFormat]status
-	results := make([]result, len(files))
+	results := make([]map[outputFormat]*status, len(files))
+	jobs := make(chan job, 256)
 
-	// In parallel...
+	// Spawn numCPU job runners...
 	wg := sync.WaitGroup{}
-	wg.Add(len(files))
-	for i, file := range files { // For each test file...
-		i, file := i, filepath.Join(dir, file)
+	wg.Add(numCPU)
+	for cpu := 0; cpu < numCPU; cpu++ {
 		go func() {
 			defer wg.Done()
-			r := result{}
-			for _, format := range formats { // For each output format...
-
-				// Is there an expected output?
-				expected := loadExpectedFile(file, format)
-				if strings.HasPrefix(expected, "SKIP") { // Special SKIP token
-					r[format] = status{code: skip}
-					continue
-				}
-
-				// Invoke the compiler...
-				var err error
-				if ok, out := invoke(exe, file, "--format", string(format), "--dawn-validation"); ok {
-					if generateExpected {
-						// If --generate-expected was passed, write out the output
-						err = saveExpectedFile(file, format, out)
-					} else if expected != "" && expected != out {
-						// Expected output did not match
-						dmp := diffmatchpatch.New()
-						diff := dmp.DiffPrettyText(dmp.DiffMain(expected, out, true))
-						err = fmt.Errorf(`Output was not as expected
-
---------------------------------------------------------------------------------
--- Expected:                                                                  --
---------------------------------------------------------------------------------
-%s
-
---------------------------------------------------------------------------------
--- Got:                                                                       --
---------------------------------------------------------------------------------
-%s
-
---------------------------------------------------------------------------------
--- Diff:                                                                      --
---------------------------------------------------------------------------------
-%s`,
-							expected, out, diff)
-					}
-				} else {
-					// Compiler returned a non-zero exit code
-					err = fmt.Errorf("%s", out)
-				}
-
-				if err != nil {
-					r[format] = status{code: fail, err: err}
-				} else {
-					r[format] = status{code: pass}
-				}
+			for job := range jobs {
+				job.run(exe, generateExpected)
 			}
-			results[i] = r
 		}()
 	}
+
+	// Issue the jobs...
+	for i, file := range files { // For each test file...
+		file := filepath.Join(dir, file)
+		fileResults := map[outputFormat]*status{}
+		for _, format := range formats { // For each output format...
+			result := &status{}
+			jobs <- job{
+				file:   file,
+				format: format,
+				result: result,
+			}
+			fileResults[format] = result
+		}
+		results[i] = fileResults
+	}
+
+	// Wait for the jobs to all finish...
+	close(jobs)
 	wg.Wait()
 
-	// At this point all the tests have been run
 	// Time to print the outputs
 
 	// Start by printing the error message for any file x format combinations
@@ -331,6 +293,74 @@
 	return nil
 }
 
+// Structures to hold the results of the tests
+type statusCode string
+
+const (
+	fail statusCode = "FAIL"
+	pass statusCode = "PASS"
+	skip statusCode = "SKIP"
+)
+
+type status struct {
+	code statusCode
+	err  error
+}
+
+type job struct {
+	file   string
+	format outputFormat
+	result *status
+}
+
+func (j job) run(exe string, generateExpected bool) {
+	// Is there an expected output?
+	expected := loadExpectedFile(j.file, j.format)
+	if strings.HasPrefix(expected, "SKIP") { // Special SKIP token
+		*j.result = status{code: skip}
+		return
+	}
+
+	// Invoke the compiler...
+	var err error
+	if ok, out := invoke(exe, j.file, "--format", string(j.format), "--dawn-validation"); ok {
+		if generateExpected {
+			// If --generate-expected was passed, write out the output
+			err = saveExpectedFile(j.file, j.format, out)
+		} else if expected != "" && expected != out {
+			// Expected output did not match
+			dmp := diffmatchpatch.New()
+			diff := dmp.DiffPrettyText(dmp.DiffMain(expected, out, true))
+			err = fmt.Errorf(`Output was not as expected
+
+--------------------------------------------------------------------------------
+-- Expected:                                                                  --
+--------------------------------------------------------------------------------
+%s
+
+--------------------------------------------------------------------------------
+-- Got:                                                                       --
+--------------------------------------------------------------------------------
+%s
+
+--------------------------------------------------------------------------------
+-- Diff:                                                                      --
+--------------------------------------------------------------------------------
+%s`,
+				expected, out, diff)
+		}
+	} else {
+		// Compiler returned a non-zero exit code
+		err = fmt.Errorf("%s", out)
+	}
+
+	if err != nil {
+		*j.result = status{code: fail, err: err}
+	} else {
+		*j.result = status{code: pass}
+	}
+}
+
 // loadExpectedFile loads the expected output file for the test file at 'path'
 // and the output format 'format'. If the file does not exist, or cannot be
 // read, then an empty string is returned.