From 41ad36154ea299e93f97776c35c4b345444774df Mon Sep 17 00:00:00 2001 From: Jamal Carvalho Date: Sat, 8 Jan 2022 21:27:15 +0000 Subject: [PATCH] {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 TryBot-Result: Gopher Robot Trust: Jamal Carvalho Reviewed-by: Jonathan Amsterdam --- cmd/screentest/main.go | 23 ++++++++++++++----- internal/screentest/screentest.go | 31 ++++++++++++++++++++++---- internal/screentest/screentest_test.go | 4 ++-- 3 files changed, 47 insertions(+), 11 deletions(-) diff --git a/cmd/screentest/main.go b/cmd/screentest/main.go index a628459e..615c3241 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 @@ import ( "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 @@ func main() { 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 7be25f7f..6aa2262b 100644 --- a/internal/screentest/screentest.go +++ b/internal/screentest/screentest.go @@ -130,7 +130,7 @@ import ( // 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 @@ func CheckHandler(glob string, update bool, vars map[string]string) error { 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 @@ func CheckHandler(glob string, update bool, vars map[string]string) error { 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 @@ func CheckHandler(glob string, update bool, vars map[string]string) error { } // 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 @@ func TestHandler(t *testing.T, glob string, update bool, vars map[string]string) 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 @@ func waitForEvent(eventName string) chromedp.ActionFunc { } } } + +// 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 c9b26801..96838acf 100644 --- a/internal/screentest/screentest_test.go +++ b/internal/screentest/screentest_test.go @@ -239,7 +239,7 @@ func TestCheckHandler(t *testing.T) { } 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 @@ func TestTestHandler(t *testing.T) { 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) {