{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>
This commit is contained in:
Jamal Carvalho 2022-01-08 21:27:15 +00:00 коммит произвёл Jamal Carvalho
Родитель 180886a91e
Коммит 41ad36154e
3 изменённых файлов: 47 добавлений и 11 удалений

Просмотреть файл

@ -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)
}
}

Просмотреть файл

@ -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{}{}
}
}

Просмотреть файл

@ -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) {