cmd/screentest: compare image sizes

The imgdiff package we use assumes the images are the same size.
So check the sizes first.

Also:
- Fix a bug where we retry instead of failing immediately.

- Make the default concurrency 1. Screen tests are flaky enough
  without adding concurrency.

- Remove the check in sleep for integers. time.ParseDuration already
  does that.

Change-Id: I63442d29c0846e3b6648eef993e8a113f4019bda
Reviewed-on: https://go-review.googlesource.com/c/website/+/634078
Reviewed-by: Hyang-Ah Hana Kim <hyangah@gmail.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
diff --git a/cmd/screentest/main.go b/cmd/screentest/main.go
index 7998f97..b1edfe8 100644
--- a/cmd/screentest/main.go
+++ b/cmd/screentest/main.go
@@ -168,7 +168,6 @@
 	"fmt"
 	"log"
 	"os"
-	"runtime"
 )
 
 var flags options
@@ -176,7 +175,7 @@
 func init() {
 	flag.BoolVar(&flags.update, "update", false, "update want with test")
 	flag.StringVar(&flags.vars, "v", "", "variables provided to script templates as comma separated KEY:VALUE pairs")
-	flag.IntVar(&flags.maxConcurrency, "c", (runtime.NumCPU()+1)/2, "number of test cases to run concurrently")
+	flag.IntVar(&flags.maxConcurrency, "c", 1, "number of test cases to run concurrently")
 	flag.StringVar(&flags.debuggerURL, "d", "", "chrome debugger URL")
 	flag.StringVar(&flags.outputDirURL, "o", "", "path for output: file path or URL with 'file' or 'gs' scheme")
 	flag.StringVar(&flags.headers, "headers", "", "HTTP headers: comma-separated list of name:value")
diff --git a/cmd/screentest/screentest.go b/cmd/screentest/screentest.go
index 74c6c2b..44018d3 100644
--- a/cmd/screentest/screentest.go
+++ b/cmd/screentest/screentest.go
@@ -134,7 +134,7 @@
 		log.Print("no tests to run")
 		return nil
 	}
-	log.Printf("ran %d tests in %s\n\n", nTests, time.Since(start).Truncate(time.Millisecond))
+	log.Printf("ran %d tests in %s\n", nTests, time.Since(start).Truncate(time.Millisecond))
 	if summary.Len() > 0 {
 		os.Stdout.Write(summary.Bytes())
 		if len(failedTests) > 0 {
@@ -440,9 +440,6 @@
 			if test == nil {
 				return nil, errors.New("directive must be in a test")
 			}
-			if ns, err := strconv.Atoi(args); err == nil {
-				return nil, fmt.Errorf("sleep argument of %d is in nanoseconds; did you mean %[1]ds?", ns)
-			}
 			dur, err := time.ParseDuration(args)
 			if err != nil {
 				return nil, err
@@ -623,6 +620,7 @@
 		testScreen, wantScreen image.Image
 	)
 	fmt.Fprintf(&tc.output, "test %s ", tc.name)
+	var failReason string
 	for try := 0; try < maxRetries; try++ {
 		g, gctx := errgroup.WithContext(ctx)
 		g.Go(func() error {
@@ -646,6 +644,19 @@
 			return tc.wantImageReadWriter.writeImage(ctx, tc.wantPath, testScreen)
 		}
 
+		// Expect the images to start at (0, 0).
+		if p := testScreen.Bounds().Min; p != image.ZP {
+			return fmt.Errorf("test image starts at %s, not (0, 0)", p)
+		}
+		if p := wantScreen.Bounds().Min; p != image.ZP {
+			return fmt.Errorf("want image starts at %s, not (0, 0)", p)
+		}
+		// If the images are different sizes, don't even compare them. imgdiff does
+		// not handle differently sized images properly.
+		if tmax, wmax := testScreen.Bounds().Max, wantScreen.Bounds().Max; tmax != wmax {
+			failReason = fmt.Sprintf("test image is %s but want image is %s", tmax, wmax)
+			break
+		}
 		result = imgdiff.Diff(testScreen, wantScreen, &imgdiff.Options{
 			Threshold: 0.1,
 			DiffImage: true,
@@ -655,16 +666,20 @@
 			fmt.Fprintf(&tc.output, "(%s)", since)
 			return nil
 		}
-		if result.DiffPixelsCount <= uint64(tc.retryPixels) {
-			fmt.Fprintf(&tc.output, "difference is <= %d pixels\n", tc.retryPixels)
+		failReason = fmt.Sprintf("%d pixels differ", result.DiffPixelsCount)
+		if result.DiffPixelsCount > uint64(tc.retryPixels) {
+			break
 		}
+		fmt.Fprintf(&tc.output, "difference is <= %d pixels\n", tc.retryPixels)
 	}
-	fmt.Fprintf(&tc.output, "(%s)\n    FAIL %s != %s (%d pixels differ)\n",
-		since, tc.testOrigin(), tc.wantOrigin(), result.DiffPixelsCount)
+	fmt.Fprintf(&tc.output, "(%s)\n    FAIL %s != %s: %s\n",
+		since, tc.testOrigin(), tc.wantOrigin(), failReason)
 	g, gctx := errgroup.WithContext(ctx)
 	g.Go(func() error { return tc.failImageWriter.writeImage(gctx, tc.testPath, testScreen) })
 	g.Go(func() error { return tc.failImageWriter.writeImage(gctx, tc.wantPath, wantScreen) })
-	g.Go(func() error { return tc.failImageWriter.writeImage(gctx, tc.diffPath, result.Image) })
+	if result != nil && result.Image != nil {
+		g.Go(func() error { return tc.failImageWriter.writeImage(gctx, tc.diffPath, result.Image) })
+	}
 	if err := g.Wait(); err != nil {
 		return err
 	}