{cmd,internal}/screentest: add config for remote debugger url

To have test runs output consistent screenshots across
different systems we allow for the use of a remote
instance of Chrome by providing a url to its debugger
as a flag. For example:

  go run ./cmd/screentest -d ws://localhost:9222

This is useful in a CI/CD environment to control the
chrome version and OS in which screenshots are taken.

Change-Id: I09b2e2d8e3056d7229808a57653d641677fc8cc3
Reviewed-on: https://go-review.googlesource.com/c/website/+/378475
Run-TryBot: Jamal Carvalho <jamal@golang.org>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Jonathan Amsterdam <jba@google.com>
Trust: Jamal Carvalho <jamalcarvalho@google.com>
diff --git a/cmd/screentest/main.go b/cmd/screentest/main.go
index 615c324..a2a2129 100644
--- a/cmd/screentest/main.go
+++ b/cmd/screentest/main.go
@@ -13,6 +13,8 @@
     variables provided to script templates as comma separated KEY:VALUE pairs
   -c
     number of testcases to run concurrently
+	-d
+		chrome debugger url
 */
 package main
 
@@ -32,6 +34,7 @@
 	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")
+	debuggerURL = flag.String("d", "", "chrome debugger url")
 )
 
 func main() {
@@ -60,7 +63,13 @@
 			parsedVars[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
 		}
 	}
-	if err := screentest.CheckHandler(glob, *update, *concurrency, parsedVars); err != nil {
+	opts := screentest.CheckOptions{
+		Update:         *update,
+		MaxConcurrency: *concurrency,
+		Vars:           parsedVars,
+		DebuggerURL:    *debuggerURL,
+	}
+	if err := screentest.CheckHandler(glob, opts); err != nil {
 		log.Fatal(err)
 	}
 }
diff --git a/internal/screentest/screentest.go b/internal/screentest/screentest.go
index 6aa2262..bfa8bf1 100644
--- a/internal/screentest/screentest.go
+++ b/internal/screentest/screentest.go
@@ -128,9 +128,28 @@
 	"google.golang.org/api/iterator"
 )
 
+type CheckOptions struct {
+	// Update is true if cached screenshots should be udpated.
+	Update bool
+
+	// MaxConcurrency is the maximum number of testcases to run in parallel.
+	MaxConcurrency int
+
+	// Vars are accessible as values in the test script templates.
+	Vars map[string]string
+
+	// DebuggerURL is the URL to a chrome websocket debugger. If left empty
+	// screentest tries to find the Chrome exectuable on the system and starts
+	// a new instance.
+	DebuggerURL string
+}
+
 // 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, maxConcurrency int, vars map[string]string) error {
+func CheckHandler(glob string, opts CheckOptions) error {
+	if opts.MaxConcurrency < 1 {
+		opts.MaxConcurrency = 1
+	}
 	now := time.Now()
 	ctx := context.Background()
 	files, err := filepath.Glob(glob)
@@ -140,14 +159,19 @@
 	if len(files) == 0 {
 		return fmt.Errorf("no files match %q", glob)
 	}
-	ctx, cancel := chromedp.NewExecAllocator(ctx, append(
-		chromedp.DefaultExecAllocatorOptions[:],
-		chromedp.WindowSize(browserWidth, browserHeight),
-	)...)
+	var cancel context.CancelFunc
+	if opts.DebuggerURL != "" {
+		ctx, cancel = chromedp.NewRemoteAllocator(ctx, opts.DebuggerURL)
+	} else {
+		ctx, cancel = chromedp.NewExecAllocator(ctx, append(
+			chromedp.DefaultExecAllocatorOptions[:],
+			chromedp.WindowSize(browserWidth, browserHeight),
+		)...)
+	}
 	defer cancel()
 	var buf bytes.Buffer
 	for _, file := range files {
-		tests, err := readTests(file, vars)
+		tests, err := readTests(file, opts.Vars)
 		if err != nil {
 			return fmt.Errorf("readTestdata(%q): %w", file, err)
 		}
@@ -160,9 +184,9 @@
 		ctx, cancel = chromedp.NewContext(ctx, chromedp.WithLogf(log.Printf))
 		defer cancel()
 		var hdr bool
-		runConcurrently(len(tests), maxConcurrency, func(i int) {
+		runConcurrently(len(tests), opts.MaxConcurrency, func(i int) {
 			tc := tests[i]
-			if err := tc.run(ctx, update); err != nil {
+			if err := tc.run(ctx, opts.Update); err != nil {
 				if !hdr {
 					fmt.Fprintf(&buf, "%s\n\n", file)
 					hdr = true
@@ -180,8 +204,19 @@
 	return nil
 }
 
+type TestOpts struct {
+	// Update is true if cached screenshots should be udpated.
+	Update bool
+
+	// Parallel runs t.Parallel for each testcase.
+	Parallel bool
+
+	// Vars are accessible as values in the test script templates.
+	Vars map[string]string
+}
+
 // TestHandler runs the test script files matched by glob.
-func TestHandler(t *testing.T, glob string, update, parallel bool, vars map[string]string) {
+func TestHandler(t *testing.T, glob string, opts TestOpts) {
 	ctx := context.Background()
 	files, err := filepath.Glob(glob)
 	if err != nil {
@@ -196,7 +231,7 @@
 	)...)
 	defer cancel()
 	for _, file := range files {
-		tests, err := readTests(file, vars)
+		tests, err := readTests(file, opts.Vars)
 		if err != nil {
 			t.Fatal(err)
 		}
@@ -207,10 +242,10 @@
 		defer cancel()
 		for _, tc := range tests {
 			t.Run(tc.name, func(t *testing.T) {
-				if parallel {
+				if opts.Parallel {
 					t.Parallel()
 				}
-				if err := tc.run(ctx, update); err != nil {
+				if err := tc.run(ctx, opts.Update); err != nil {
 					t.Fatal(err)
 				}
 			})
diff --git a/internal/screentest/screentest_test.go b/internal/screentest/screentest_test.go
index 96838ac..fac5d55 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, 1, nil); (err != nil) != tt.wantErr {
+			if err := CheckHandler(tt.args.glob, CheckOptions{}); (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, false, nil)
+	TestHandler(t, "testdata/pass.txt", TestOpts{})
 }
 
 func TestHeaders(t *testing.T) {