{cmd,internal}/screentest: testcases run concurrently
Testcases for screentest will run concurrently
with a configurable max concurrency setting that
defaults to half of number of CPUs on a system.
Change-Id: I07e7ffd8d3867c47b709c160110a58ac60ee357c
Reviewed-on: https://go-review.googlesource.com/c/website/+/377256
Run-TryBot: Jamal Carvalho <jamal@golang.org>
TryBot-Result: Gopher Robot <gobot@golang.org>
Trust: Jamal Carvalho <jamalcarvalho@google.com>
Reviewed-by: Jonathan Amsterdam <jba@google.com>
diff --git a/cmd/screentest/main.go b/cmd/screentest/main.go
index a628459..615c324 100644
--- a/cmd/screentest/main.go
+++ b/cmd/screentest/main.go
@@ -2,7 +2,18 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-// Command screentest runs the screentest check for a set of scripts.
+/*
+Command screentest runs the screentest check for a set of scripts.
+ Usage: screentest [flags] [glob]
+
+The flags are:
+ -u
+ update cached screenshots
+ -v
+ variables provided to script templates as comma separated KEY:VALUE pairs
+ -c
+ number of testcases to run concurrently
+*/
package main
import (
@@ -11,19 +22,21 @@
"log"
"os"
"path/filepath"
+ "runtime"
"strings"
"golang.org/x/website/internal/screentest"
)
var (
- update = flag.Bool("update", false, "update cached screenshots")
- vars = flag.String("vars", "", "provide variables to the script template as comma separated KEY:VALUE pairs")
+ update = flag.Bool("u", false, "update cached screenshots")
+ vars = flag.String("v", "", "variables provided to script templates as comma separated KEY:VALUE pairs")
+ concurrency = flag.Int("c", (runtime.NumCPU()+1)/2, "number of testcases to run concurrently")
)
func main() {
flag.Usage = func() {
- fmt.Printf("Usage: screentest [OPTIONS] glob\n")
+ fmt.Printf("Usage: screentest [flags] [glob]\n")
flag.PrintDefaults()
}
flag.Parse()
@@ -47,7 +60,7 @@
parsedVars[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
}
}
- if err := screentest.CheckHandler(glob, *update, parsedVars); err != nil {
+ if err := screentest.CheckHandler(glob, *update, *concurrency, parsedVars); err != nil {
log.Fatal(err)
}
}
diff --git a/internal/screentest/screentest.go b/internal/screentest/screentest.go
index 7be25f7..6aa2262 100644
--- a/internal/screentest/screentest.go
+++ b/internal/screentest/screentest.go
@@ -130,7 +130,7 @@
// CheckHandler runs the test scripts matched by glob. If any errors are
// encountered, CheckHandler returns an error listing the problems.
-func CheckHandler(glob string, update bool, vars map[string]string) error {
+func CheckHandler(glob string, update bool, maxConcurrency int, vars map[string]string) error {
now := time.Now()
ctx := context.Background()
files, err := filepath.Glob(glob)
@@ -160,7 +160,8 @@
ctx, cancel = chromedp.NewContext(ctx, chromedp.WithLogf(log.Printf))
defer cancel()
var hdr bool
- for _, tc := range tests {
+ runConcurrently(len(tests), maxConcurrency, func(i int) {
+ tc := tests[i]
if err := tc.run(ctx, update); err != nil {
if !hdr {
fmt.Fprintf(&buf, "%s\n\n", file)
@@ -170,7 +171,7 @@
fmt.Fprintf(&buf, "inspect diff at %s\n\n", tc.outDiff)
}
fmt.Println(tc.output.String())
- }
+ })
}
fmt.Printf("finished in %s\n\n", time.Since(now).Truncate(time.Millisecond))
if buf.Len() > 0 {
@@ -180,7 +181,7 @@
}
// TestHandler runs the test script files matched by glob.
-func TestHandler(t *testing.T, glob string, update bool, vars map[string]string) {
+func TestHandler(t *testing.T, glob string, update, parallel bool, vars map[string]string) {
ctx := context.Background()
files, err := filepath.Glob(glob)
if err != nil {
@@ -206,6 +207,9 @@
defer cancel()
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
+ if parallel {
+ t.Parallel()
+ }
if err := tc.run(ctx, update); err != nil {
t.Fatal(err)
}
@@ -788,3 +792,22 @@
}
}
}
+
+// runConcurrently calls f on each integer from 0 to n-1,
+// with at most max invocations active at once.
+// It waits for all invocations to complete.
+func runConcurrently(n, max int, f func(int)) {
+ tokens := make(chan struct{}, max)
+ for i := 0; i < n; i++ {
+ i := i
+ tokens <- struct{}{} // wait until the number of goroutines is below the limit
+ go func() {
+ f(i)
+ <-tokens // let another goroutine run
+ }()
+ }
+ // Wait for all goroutines to finish.
+ for i := 0; i < cap(tokens); i++ {
+ tokens <- struct{}{}
+ }
+}
diff --git a/internal/screentest/screentest_test.go b/internal/screentest/screentest_test.go
index c9b2680..96838ac 100644
--- a/internal/screentest/screentest_test.go
+++ b/internal/screentest/screentest_test.go
@@ -239,7 +239,7 @@
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- if err := CheckHandler(tt.args.glob, false, nil); (err != nil) != tt.wantErr {
+ if err := CheckHandler(tt.args.glob, false, 1, nil); (err != nil) != tt.wantErr {
t.Fatalf("CheckHandler() error = %v, wantErr %v", err, tt.wantErr)
}
if len(tt.wantFiles) != 0 {
@@ -262,7 +262,7 @@
if err != nil {
t.Skip()
}
- TestHandler(t, "testdata/pass.txt", false, nil)
+ TestHandler(t, "testdata/pass.txt", false, false, nil)
}
func TestHeaders(t *testing.T) {