[canvaskit] Add task driver for running GMs on WASM/WebGL.

This loads in the known digests from Gold, starts the test
harness (which runs the GMs using puppeteer) and then uses
goldctl to upload the results to Gold when finished.

This will fail (and should not be landed) until
https://skia-review.googlesource.com/c/buildbot/+/328156
makes it into goldctl and the cipd build.

Bug: skia:10812
Change-Id: I89e5cf188d8f2adeba4ff676525d9bfbdcb46d5a
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/328380
Reviewed-by: Leandro Lovisolo <lovisolo@google.com>
Reviewed-by: Eric Boren <borenet@google.com>
diff --git a/infra/bots/cfg.json b/infra/bots/cfg.json
index 2313655..039e317 100644
--- a/infra/bots/cfg.json
+++ b/infra/bots/cfg.json
@@ -13,7 +13,8 @@
     "OpenCL",
     "SKQP",
     "TSAN",
-    "Valgrind"
+    "Valgrind",
+    "WasmGMTests"
   ],
   "service_account_canary": "skia-canary@skia-swarming-bots.iam.gserviceaccount.com",
   "service_account_compile": "skia-external-compile-tasks@skia-swarming-bots.iam.gserviceaccount.com",
diff --git a/infra/bots/gen_tasks_logic/gen_tasks_logic.go b/infra/bots/gen_tasks_logic/gen_tasks_logic.go
index 82e07f4..a5424fa 100644
--- a/infra/bots/gen_tasks_logic/gen_tasks_logic.go
+++ b/infra/bots/gen_tasks_logic/gen_tasks_logic.go
@@ -22,6 +22,7 @@
 	"strings"
 	"time"
 
+	"go.skia.org/infra/go/cipd"
 	"go.skia.org/infra/task_scheduler/go/specs"
 )
 
@@ -119,6 +120,8 @@
 	CIPD_PKGS_KITCHEN  = append(specs.CIPD_PKGS_KITCHEN[:2], specs.CIPD_PKGS_PYTHON[1])
 	CIPD_PKG_CPYTHON   = specs.CIPD_PKGS_PYTHON[0]
 
+	CIPD_PKGS_GOLDCTL = []*specs.CipdPackage{cipd.MustGetPackage("skia/tools/goldctl/${platform}")}
+
 	CIPD_PKGS_XCODE = []*specs.CipdPackage{
 		// https://chromium.googlesource.com/chromium/tools/build/+/e19b7d9390e2bb438b566515b141ed2b9ed2c7c2/scripts/slave/recipe_modules/ios/api.py#317
 		// This package is really just an installer for XCode.
@@ -1719,8 +1722,9 @@
 	})
 }
 
-// compileWasmGMTests uses a task driver to compile the GMs and unit tests for Web Assembly (WASM)
-// using WebGL if necessary.
+// compileWasmGMTests uses a task driver to compile the GMs and unit tests for Web Assembly (WASM).
+// We can use the same build for both CPU and GPU tests since the latter requires the code for the
+// former anyway.
 func (b *jobBuilder) compileWasmGMTests(compileName string) {
 	b.addTask(compileName, func(b *taskBuilder) {
 		b.attempts(1)
@@ -1749,3 +1753,49 @@
 		)
 	})
 }
+
+// compileWasmGMTests uses a task driver to compile the GMs and unit tests for Web Assembly (WASM).
+// We can use the same build for both CPU and GPU tests since the latter requires the code for the
+// former anyway.
+func (b *jobBuilder) runWasmGMTests() {
+	compileTaskName := b.compile()
+
+	b.addTask(b.Name, func(b *taskBuilder) {
+		b.attempts(1)
+		b.usesNode()
+		b.swarmDimensions()
+		b.cipd(CIPD_PKG_LUCI_AUTH)
+		b.cipd(CIPD_PKGS_GOLDCTL...)
+		b.dep(b.buildTaskDrivers())
+		b.dep(compileTaskName)
+		b.timeout(60 * time.Minute)
+		b.isolate("wasm_gm_tests.isolate")
+		b.serviceAccount(b.cfg.ServiceAccountUploadGM)
+		b.cmd(
+			"./run_wasm_gm_tests",
+			"--project_id", "skia-swarming-bots",
+			"--task_id", specs.PLACEHOLDER_TASK_ID,
+			"--task_name", b.Name,
+			"--test_harness_path", "./tools/run-wasm-gm-tests",
+			"--built_path", "./wasm_out",
+			"--node_bin_path", "./node/node/bin",
+			"--work_path", "./wasm_gm/work",
+			"--gold_ctl_path", "./cipd_bin_packages/goldctl",
+			"--git_commit", specs.PLACEHOLDER_REVISION,
+			"--changelist_id", specs.PLACEHOLDER_ISSUE,
+			"--patchset_order", specs.PLACEHOLDER_PATCHSET,
+			"--tryjob_id", specs.PLACEHOLDER_BUILDBUCKET_BUILD_ID,
+			// TODO(kjlubick, nifong) Make these not hard coded if we change the configs we test on.
+			"--webgl_version", "2", // 0 means CPU ; this flag controls cpu_or_gpu and extra_config
+			"--gold_key", "browser:Chrome",
+			"--gold_key", "alpha_type:Premul",
+			"--gold_key", "arch:wasm",
+			"--gold_key", "color_depth:8888",
+			"--gold_key", "configuration:Release",
+			"--gold_key", "cpu_or_gpu_value:QuadroP400",
+			"--gold_key", "model:Golo",
+			"--gold_key", "os:Ubuntu18",
+			"--alsologtostderr",
+		)
+	})
+}
diff --git a/infra/bots/gen_tasks_logic/job_builder.go b/infra/bots/gen_tasks_logic/job_builder.go
index d835758..6aada07 100644
--- a/infra/bots/gen_tasks_logic/job_builder.go
+++ b/infra/bots/gen_tasks_logic/job_builder.go
@@ -174,6 +174,10 @@
 
 	// Test bots.
 	if b.role("Test") {
+		if b.extraConfig("WasmGMTests") {
+			b.runWasmGMTests()
+			return
+		}
 		b.dm()
 		return
 	}
diff --git a/infra/bots/jobs.json b/infra/bots/jobs.json
index 8d70099..aa5edbc 100644
--- a/infra/bots/jobs.json
+++ b/infra/bots/jobs.json
@@ -537,9 +537,9 @@
   "Test-Ubuntu18-Clang-Golo-GPU-QuadroP400-x86_64-Debug-All-DDL1",
   "Test-Ubuntu18-Clang-Golo-GPU-QuadroP400-x86_64-Debug-All-DDL1_Vulkan",
   "Test-Ubuntu18-Clang-Golo-GPU-QuadroP400-x86_64-Debug-All-DDL3_ASAN",
-  "Test-Ubuntu18-Clang-Golo-GPU-QuadroP400-x86_64-Debug-All-OOPRDDL_Vulkan",
-  "Test-Ubuntu18-Clang-Golo-GPU-QuadroP400-x86_64-Debug-All-OOPRDDL_ASAN",
   "Test-Ubuntu18-Clang-Golo-GPU-QuadroP400-x86_64-Debug-All-DDL3_Vulkan",
+  "Test-Ubuntu18-Clang-Golo-GPU-QuadroP400-x86_64-Debug-All-OOPRDDL_ASAN",
+  "Test-Ubuntu18-Clang-Golo-GPU-QuadroP400-x86_64-Debug-All-OOPRDDL_Vulkan",
   "Test-Ubuntu18-Clang-Golo-GPU-QuadroP400-x86_64-Debug-All-PreAbandonGpuContext",
   "Test-Ubuntu18-Clang-Golo-GPU-QuadroP400-x86_64-Debug-All-Vulkan",
   "Test-Ubuntu18-Clang-Golo-GPU-QuadroP400-x86_64-Release-All",
@@ -553,6 +553,7 @@
   "Test-Ubuntu18-Clang-Golo-GPU-QuadroP400-x86_64-Release-All-Valgrind_PreAbandonGpuContext_SK_CPU_LIMIT_SSE41",
   "Test-Ubuntu18-Clang-Golo-GPU-QuadroP400-x86_64-Release-All-Valgrind_SK_CPU_LIMIT_SSE41",
   "Test-Ubuntu18-Clang-Golo-GPU-QuadroP400-x86_64-Release-All-Vulkan",
+  "Test-Ubuntu18-EMCC-Golo-GPU-QuadroP400-wasm-Release-All-WasmGMTests_WebGL2",
   "Test-Win10-Clang-AlphaR2-GPU-RadeonR9M470X-x86_64-Debug-All",
   "Test-Win10-Clang-AlphaR2-GPU-RadeonR9M470X-x86_64-Debug-All-ANGLE",
   "Test-Win10-Clang-AlphaR2-GPU-RadeonR9M470X-x86_64-Debug-All-Vulkan",
diff --git a/infra/bots/task_drivers/run_wasm_gm_tests/run_wasm_gm_tests.go b/infra/bots/task_drivers/run_wasm_gm_tests/run_wasm_gm_tests.go
new file mode 100644
index 0000000..041bf5b
--- /dev/null
+++ b/infra/bots/task_drivers/run_wasm_gm_tests/run_wasm_gm_tests.go
@@ -0,0 +1,277 @@
+// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package main
+
+import (
+	"context"
+	"encoding/json"
+	"flag"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"strconv"
+
+	"go.skia.org/infra/go/common"
+	"go.skia.org/infra/go/exec"
+	"go.skia.org/infra/go/httputils"
+	"go.skia.org/infra/go/skerr"
+	"go.skia.org/infra/task_driver/go/lib/os_steps"
+	"go.skia.org/infra/task_driver/go/td"
+)
+
+func main() {
+	var (
+		// Required properties for this task.
+		builtPath       = flag.String("built_path", "", "The directory where the built wasm/js code will be.")
+		gitCommit       = flag.String("git_commit", "", "The commit at which we are testing.")
+		goldCtlPath     = flag.String("gold_ctl_path", "", "Path to the goldctl binary")
+		goldKeys        = common.NewMultiStringFlag("gold_key", nil, "The keys that will tag this data")
+		nodeBinPath     = flag.String("node_bin_path", "", "Path to the node bin directory (should have npm also). This directory *must* be on the PATH when this executable is called, otherwise, the wrong node or npm version may be found (e.g. the one on the system), even if we are explicitly calling npm with the absolute path.")
+		projectID       = flag.String("project_id", "", "ID of the Google Cloud project.")
+		taskID          = flag.String("task_id", "", "task id this data was generated on")
+		taskName        = flag.String("task_name", "", "Name of the task.")
+		testHarnessPath = flag.String("test_harness_path", "", "Path to test harness folder (tools/run-wasm-gm-tests)")
+		webGLVersion    = flag.Int("webgl_version", 2, "The version of web gl to use. 0 means CPU")
+		workPath        = flag.String("work_path", "", "The directory to use to store temporary files (e.g. pngs and JSON)")
+
+		// Provided for tryjobs
+		changelistID  = flag.String("changelist_id", "", "The id the Gerrit CL. Omit for primary branch.")
+		tryjobID      = flag.String("tryjob_id", "", "The id of the Buildbucket job for tryjobs. Omit for primary branch.")
+		patchsetOrder = flag.Int("patchset_order", 0, "Represents if this is the nth patchset")
+
+		// Debugging flags.
+		local              = flag.Bool("local", false, "True if running locally (as opposed to on the bots)")
+		outputSteps        = flag.String("o", "", "If provided, dump a JSON blob of step data to the given file. Prints to stdout if '-' is given.")
+		serviceAccountPath = flag.String("service_account_path", "", "Used in local mode for authentication. Non-local mode uses Luci config.")
+	)
+
+	// Setup.
+	ctx := td.StartRun(projectID, taskID, taskName, outputSteps, local)
+	defer td.EndRun(ctx)
+
+	builtAbsPath := td.MustGetAbsolutePathOfFlag(ctx, *builtPath, "built_path")
+	goldctlAbsPath := td.MustGetAbsolutePathOfFlag(ctx, *goldCtlPath, "gold_ctl_path")
+	nodeBinAbsPath := td.MustGetAbsolutePathOfFlag(ctx, *nodeBinPath, "node_bin_path")
+	testHarnessAbsPath := td.MustGetAbsolutePathOfFlag(ctx, *testHarnessPath, "test_harness_path")
+	workAbsPath := td.MustGetAbsolutePathOfFlag(ctx, *workPath, "work_path")
+
+	goldctlWorkPath := filepath.Join(workAbsPath, "goldctl")
+	if err := os_steps.MkdirAll(ctx, goldctlWorkPath); err != nil {
+		td.Fatal(ctx, err)
+	}
+	testsWorkPath := filepath.Join(workAbsPath, "tests")
+	if err := os_steps.MkdirAll(ctx, testsWorkPath); err != nil {
+		td.Fatal(ctx, err)
+	}
+
+	keys := *goldKeys
+	switch *webGLVersion {
+	case 0:
+		keys = append(keys, "cpu_or_gpu:CPU")
+	case 1:
+		keys = append(keys, "cpu_or_gpu:GPU", "extra_config:WebGL1")
+	case 2:
+		keys = append(keys, "cpu_or_gpu:GPU", "extra_config:WebGL2")
+	default:
+		td.Fatalf(ctx, "Invalid value for webgl_version, must be 0, 1, 2 got %d", *webGLVersion)
+	}
+
+	// initialize goldctl
+	if err := setupGoldctl(ctx, *local, *gitCommit, *changelistID, *tryjobID, goldctlAbsPath, goldctlWorkPath,
+		*serviceAccountPath, keys, *patchsetOrder); err != nil {
+		td.Fatal(ctx, err)
+	}
+
+	if err := downloadKnownHashes(ctx, testsWorkPath); err != nil {
+		td.Fatal(ctx, err)
+	}
+	if err := setupTests(ctx, nodeBinAbsPath, testHarnessAbsPath); err != nil {
+		td.Fatal(ctx, skerr.Wrap(err))
+	}
+	// Run puppeteer tests. The input is a list of known hashes. The output will be a JSON array and
+	// any new images to be written to disk in the testsWorkPath. See WriteToDisk in DM for how that
+	// is done on the C++ side.
+	if err := runTests(ctx, builtAbsPath, nodeBinAbsPath, testHarnessAbsPath, testsWorkPath, *webGLVersion); err != nil {
+		td.Fatal(ctx, err)
+	}
+
+	// Parse JSON and call goldctl imgtest add them.
+	if err := processTestData(ctx, testsWorkPath, goldctlAbsPath, goldctlWorkPath); err != nil {
+		td.Fatal(ctx, err)
+	}
+
+	// call goldctl finalize to upload stuff.
+	if err := finalizeGoldctl(ctx, goldctlAbsPath, goldctlWorkPath); err != nil {
+		td.Fatal(ctx, err)
+	}
+}
+
+func setupGoldctl(ctx context.Context, local bool, gitCommit, gerritCLID, tryjobID, goldctlPath, workPath, serviceAccountPath string, keys []string, psOrder int) error {
+	ctx = td.StartStep(ctx, td.Props("setup goldctl").Infra())
+	defer td.EndStep(ctx)
+
+	args := []string{goldctlPath, "auth", "--work-dir", workPath}
+	if !local {
+		args = append(args, "--luci")
+	} else {
+		// When testing locally, it can also be handy to add in --dry-run here.
+		args = append(args, "--service-account", serviceAccountPath)
+	}
+
+	if _, err := exec.RunCwd(ctx, workPath, args...); err != nil {
+		return td.FailStep(ctx, skerr.Wrapf(err, "running %s", args))
+	}
+
+	args = []string{
+		goldctlPath, "imgtest", "init", "--work-dir", workPath, "--instance", "skia", "--corpus", "gm",
+		"--commit", gitCommit,
+	}
+	if gerritCLID != "" {
+		ps := strconv.Itoa(psOrder)
+		args = append(args, "--crs", "gerrit", "--changelist", gerritCLID, "--patchset", ps,
+			"--cis", "buildbucket", "--jobid", tryjobID)
+	}
+
+	for _, key := range keys {
+		args = append(args, "--key", key)
+	}
+
+	if _, err := exec.RunCwd(ctx, workPath, args...); err != nil {
+		return td.FailStep(ctx, skerr.Wrapf(err, "running %s", args))
+	}
+	return nil
+}
+
+const knownHashesURL = "https://storage.googleapis.com/skia-infra-gm/hash_files/gold-prod-hashes.txt"
+
+// downloadKnownHashes downloads the known hashes from Gold and stores it as a text file in
+// workPath/hashes.txt
+func downloadKnownHashes(ctx context.Context, workPath string) error {
+	ctx = td.StartStep(ctx, td.Props("download known hashes").Infra())
+	defer td.EndStep(ctx)
+
+	client := httputils.DefaultClientConfig().With2xxOnly().Client()
+	resp, err := client.Get(knownHashesURL)
+	if err != nil {
+		return td.FailStep(ctx, skerr.Wrapf(err, "downloading known hashes"))
+	}
+	defer resp.Body.Close()
+	data, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		return td.FailStep(ctx, skerr.Wrapf(err, "reading known hashes"))
+	}
+	return os_steps.WriteFile(ctx, filepath.Join(workPath, "hashes.txt"), data, 0666)
+}
+
+func setupTests(ctx context.Context, nodeBinPath string, testHarnessPath string) error {
+	ctx = td.StartStep(ctx, td.Props("setup npm").Infra())
+	defer td.EndStep(ctx)
+
+	if _, err := exec.RunCwd(ctx, testHarnessPath, filepath.Join(nodeBinPath, "npm"), "ci"); err != nil {
+		return td.FailStep(ctx, skerr.Wrap(err))
+	}
+	return nil
+}
+
+func runTests(ctx context.Context, builtPath, nodeBinPath, testHarnessPath, workPath string, webglVersion int) error {
+	ctx = td.StartStep(ctx, td.Props("run GMs and unit tests"))
+	defer td.EndStep(ctx)
+
+	// TODO(kjlubick) the test harness does not actually run unit tests yet.
+	err := td.Do(ctx, td.Props("Run GMs and Unit Tests"), func(ctx context.Context) error {
+		args := []string{filepath.Join(nodeBinPath, "node"),
+			"run-wasm-gm-tests",
+			"--js_file", filepath.Join(builtPath, "wasm_gm_tests.js"),
+			"--wasm_file", filepath.Join(builtPath, "wasm_gm_tests.wasm"),
+			"--known_hashes", filepath.Join(workPath, "hashes.txt"),
+			"--use_gpu", // TODO(kjlubick) use webglVersion and account for CPU
+			"--output", workPath,
+			"--timeout", "120", // 120 seconds per batch of 50 tests.
+		}
+
+		_, err := exec.RunCwd(ctx, testHarnessPath, args...)
+		if err != nil {
+			return skerr.Wrap(err)
+		}
+		return nil
+	})
+	if err != nil {
+		return td.FailStep(ctx, skerr.Wrap(err))
+	}
+	return nil
+}
+
+type goldResult struct {
+	TestName string `json:"name"`
+	MD5Hash  string `json:"digest"`
+}
+
+func processTestData(ctx context.Context, testOutputPath, goldctlPath, goldctlWorkPath string) error {
+	ctx = td.StartStep(ctx, td.Props("process test data").Infra())
+	defer td.EndStep(ctx)
+
+	// Read in the file, process it as []goldResult
+	var results []goldResult
+	resultFile := filepath.Join(testOutputPath, "gold_results.json")
+
+	err := td.Do(ctx, td.Props("Load results from "+resultFile), func(ctx context.Context) error {
+		b, err := os_steps.ReadFile(ctx, resultFile)
+		if err != nil {
+			return skerr.Wrap(err)
+		}
+		if err := json.Unmarshal(b, &results); err != nil {
+			return skerr.Wrap(err)
+		}
+		return nil
+	})
+	if err != nil {
+		return td.FailStep(ctx, skerr.Wrap(err))
+	}
+
+	err = td.Do(ctx, td.Props(fmt.Sprintf("Call goldtl on %d results", len(results))), func(ctx context.Context) error {
+		for _, result := range results {
+			// These args are the same regardless of if we need to upload the png file or not.
+			args := []string{goldctlPath, "imgtest", "add", "--work-dir", goldctlWorkPath,
+				"--test-name", result.TestName, "--png-digest", result.MD5Hash}
+			// check to see if there's an image we need to upload
+			potentialPNGFile := filepath.Join(testOutputPath, result.MD5Hash+".png")
+			_, err := os_steps.Stat(ctx, potentialPNGFile)
+			if os.IsNotExist(err) {
+				// PNG was not produced, we assume it is already uploaded to Gold and just say the digest
+				// we produced.
+				_, err = exec.RunCwd(ctx, goldctlWorkPath, args...)
+				if err != nil {
+					return skerr.Wrapf(err, "reporting result %#v to goldctl", result)
+				}
+				continue
+			} else if err != nil {
+				return skerr.Wrapf(err, "reading %s", potentialPNGFile)
+			}
+			// call goldctl with the png file
+			args = append(args, "--png-file", potentialPNGFile)
+			_, err = exec.RunCwd(ctx, goldctlWorkPath, args...)
+			if err != nil {
+				return skerr.Wrapf(err, "reporting result %#v to goldctl", result)
+			}
+		}
+		return nil
+	})
+	if err != nil {
+		return td.FailStep(ctx, skerr.Wrap(err))
+	}
+	return nil
+}
+
+func finalizeGoldctl(ctx context.Context, goldctlPath, workPath string) error {
+	ctx = td.StartStep(ctx, td.Props("finalize goldctl data").Infra())
+	defer td.EndStep(ctx)
+
+	_, err := exec.RunCwd(ctx, workPath, goldctlPath, "imgtest", "finalize", "--work-dir", workPath)
+	if err != nil {
+		return skerr.Wrapf(err, "Finalizing goldctl")
+	}
+	return nil
+}
diff --git a/infra/bots/tasks.json b/infra/bots/tasks.json
index cb9916b..1f11fb5 100755
--- a/infra/bots/tasks.json
+++ b/infra/bots/tasks.json
@@ -2795,6 +2795,11 @@
         "Upload-Test-Ubuntu18-Clang-Golo-GPU-QuadroP400-x86_64-Release-All-Vulkan"
       ]
     },
+    "Test-Ubuntu18-EMCC-Golo-GPU-QuadroP400-wasm-Release-All-WasmGMTests_WebGL2": {
+      "tasks": [
+        "Test-Ubuntu18-EMCC-Golo-GPU-QuadroP400-wasm-Release-All-WasmGMTests_WebGL2"
+      ]
+    },
     "Test-Win10-Clang-AlphaR2-GPU-RadeonR9M470X-x86_64-Debug-All": {
       "tasks": [
         "Upload-Test-Win10-Clang-AlphaR2-GPU-RadeonR9M470X-x86_64-Debug-All"
@@ -45392,6 +45397,90 @@
         "test"
       ]
     },
+    "Test-Ubuntu18-EMCC-Golo-GPU-QuadroP400-wasm-Release-All-WasmGMTests_WebGL2": {
+      "cipd_packages": [
+        {
+          "name": "infra/tools/luci-auth/${platform}",
+          "path": "cipd_bin_packages",
+          "version": "git_revision:ad5333b7045827af72359e889d75c5fd6cb764a8"
+        },
+        {
+          "name": "skia/bots/node",
+          "path": "node",
+          "version": "version:3"
+        },
+        {
+          "name": "skia/tools/goldctl/${platform}",
+          "path": "cipd_bin_packages",
+          "version": "git_revision:ad5333b7045827af72359e889d75c5fd6cb764a8"
+        }
+      ],
+      "command": [
+        "./run_wasm_gm_tests",
+        "--project_id",
+        "skia-swarming-bots",
+        "--task_id",
+        "<(TASK_ID)",
+        "--task_name",
+        "Test-Ubuntu18-EMCC-Golo-GPU-QuadroP400-wasm-Release-All-WasmGMTests_WebGL2",
+        "--test_harness_path",
+        "./tools/run-wasm-gm-tests",
+        "--built_path",
+        "./wasm_out",
+        "--node_bin_path",
+        "./node/node/bin",
+        "--work_path",
+        "./wasm_gm/work",
+        "--gold_ctl_path",
+        "./cipd_bin_packages/goldctl",
+        "--git_commit",
+        "<(REVISION)",
+        "--changelist_id",
+        "<(ISSUE)",
+        "--patchset_order",
+        "<(PATCHSET)",
+        "--tryjob_id",
+        "<(BUILDBUCKET_BUILD_ID)",
+        "--webgl_version",
+        "2",
+        "--gold_key",
+        "browser:Chrome",
+        "--gold_key",
+        "alpha_type:Premul",
+        "--gold_key",
+        "arch:wasm",
+        "--gold_key",
+        "color_depth:8888",
+        "--gold_key",
+        "configuration:Release",
+        "--gold_key",
+        "cpu_or_gpu_value:QuadroP400",
+        "--gold_key",
+        "model:Golo",
+        "--gold_key",
+        "os:Ubuntu18",
+        "--alsologtostderr"
+      ],
+      "dependencies": [
+        "Build-Debian10-EMCC-wasm-Release-WasmGMTests",
+        "Housekeeper-PerCommit-BuildTaskDrivers"
+      ],
+      "dimensions": [
+        "gpu:10de:1cb3-430.14",
+        "os:Ubuntu-18.04",
+        "pool:Skia"
+      ],
+      "env_prefixes": {
+        "PATH": [
+          "node/node/bin"
+        ]
+      },
+      "execution_timeout_ns": 3600000000000,
+      "io_timeout_ns": 3600000000000,
+      "isolate": "wasm_gm_tests.isolate",
+      "max_attempts": 1,
+      "service_account": "skia-external-gm-uploader@skia-swarming-bots.iam.gserviceaccount.com"
+    },
     "Test-Win10-Clang-AlphaR2-GPU-RadeonR9M470X-x86_64-Debug-All": {
       "caches": [
         {
diff --git a/infra/bots/wasm_gm_tests.isolate b/infra/bots/wasm_gm_tests.isolate
new file mode 100644
index 0000000..6df7699
--- /dev/null
+++ b/infra/bots/wasm_gm_tests.isolate
@@ -0,0 +1,7 @@
+{
+  'variables': {
+    'files': [
+      '../../tools/run-wasm-gm-tests',
+    ],
+  },
+}