• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2022 Google LLC
2//
3// Use of this source code is governed by a BSD-style license that can be
4// found in the LICENSE file.
5//
6// This executable builds and tests CanvasKit. The tests produce images (aka gms) and these
7// are uploaded to Gold.
8// It requires unzip to be installed (which Bazel already requires).
9package main
10
11import (
12	"context"
13	"flag"
14	"fmt"
15	"os"
16	"path/filepath"
17	"strconv"
18	"strings"
19
20	sk_exec "go.skia.org/infra/go/exec"
21	"go.skia.org/infra/go/skerr"
22	"go.skia.org/infra/task_driver/go/lib/bazel"
23	"go.skia.org/infra/task_driver/go/lib/os_steps"
24	"go.skia.org/infra/task_driver/go/td"
25)
26
27// This value is arbitrarily selected. It is smaller than our maximum RBE pool size.
28const rbeJobs = 100
29
30var (
31	// Required properties for this task.
32	projectId  = flag.String("project_id", "", "ID of the Google Cloud project.")
33	taskId     = flag.String("task_id", "", "ID of this task.")
34	taskName   = flag.String("task_name", "", "Name of the task.")
35	workdir    = flag.String("workdir", ".", "Working directory, the root directory of a full Skia checkout")
36	testConfig = flag.String("test_config", "", "The config name (defined in //bazel/buildrc), which indicates how CanvasKit should be compiled and tested.")
37	cross      = flag.String("cross", "", "[not yet supported] For use with cross-compiling.")
38	// goldctl data
39	goldctlPath      = flag.String("goldctl_path", "", "The path to the golctl binary on disk.")
40	gitCommit        = flag.String("git_commit", "", "The git hash to which the data should be associated. This will be used when changelist_id and patchset_order are not set to report data to Gold that belongs on the primary branch.")
41	changelistID     = flag.String("changelist_id", "", "Should be non-empty only when run on the CQ.")
42	patchsetOrderStr = flag.String("patchset_order", "", "Should be non-zero only when run on the CQ.")
43	tryjobID         = flag.String("tryjob_id", "", "Should be non-zero only when run on the CQ.")
44	// goldctl keys
45	browser         = flag.String("browser", "Chrome", "The browser running the tests")
46	compilationMode = flag.String("compilation_mode", "Release", "How the binary was compiled")
47	cpuOrGPU        = flag.String("cpu_or_gpu", "GPU", "The render backend")
48	cpuOrGPUValue   = flag.String("cpu_or_gpu_value", "WebGL2", "What variant of the render backend")
49
50	// Optional flags.
51	bazelCacheDir = flag.String("bazel_cache_dir", "/mnt/pd0/bazel_cache", "Override the Bazel cache directory with this path")
52	expungeCache  = flag.Bool("expunge_cache", false, "If set, the Bazel cache will be cleaned with --expunge before execution. We should only have to set this rarely, if something gets messed up.")
53	local         = flag.Bool("local", false, "True if running locally (as opposed to on the CI/CQ)")
54	output        = flag.String("o", "", "If provided, dump a JSON blob of step data to the given file. Prints to stdout if '-' is given.")
55)
56
57func main() {
58	ctx := td.StartRun(projectId, taskId, taskName, output, local)
59	defer td.EndRun(ctx)
60
61	goldctlAbsPath := td.MustGetAbsolutePathOfFlag(ctx, *goldctlPath, "gold_ctl_path")
62	wd := td.MustGetAbsolutePathOfFlag(ctx, *workdir, "workdir")
63	skiaDir := filepath.Join(wd, "skia")
64	patchsetOrder := 0
65	if *patchsetOrderStr != "" {
66		var err error
67		patchsetOrder, err = strconv.Atoi(*patchsetOrderStr)
68		if err != nil {
69			fmt.Println("Non-integer value passed in to --patchset_order")
70			td.Fatal(ctx, err)
71		}
72	}
73	if *testConfig == "" {
74		td.Fatal(ctx, skerr.Fmt("Must specify --test_config"))
75	}
76
77	opts := bazel.BazelOptions{
78		// We want the cache to be on a bigger disk than default. The root disk, where the home
79		// directory (and default Bazel cache) lives, is only 15 GB on our GCE VMs.
80		CachePath: *bazelCacheDir,
81	}
82	if err := bazel.EnsureBazelRCFile(ctx, opts); err != nil {
83		td.Fatal(ctx, err)
84	}
85	if *cross != "" {
86		fmt.Println("Saw --cross, but don't know what to do with that yet.")
87	}
88
89	if *expungeCache {
90		if err := bazelClean(ctx, skiaDir); err != nil {
91			td.Fatal(ctx, err)
92		}
93	}
94
95	if err := bazelTest(ctx, skiaDir, "//modules/canvaskit:canvaskit_js_tests", *testConfig,
96		"--config=linux_rbe", "--test_output=streamed", "--jobs="+strconv.Itoa(rbeJobs)); err != nil {
97		td.Fatal(ctx, err)
98	}
99
100	conf := goldctlConfig{
101		goldctlPath:   goldctlAbsPath,
102		gitCommit:     *gitCommit,
103		changelistID:  *changelistID,
104		patchsetOrder: patchsetOrder,
105		tryjobID:      *tryjobID,
106		corpus:        "canvaskit",
107		keys: map[string]string{
108			"arch":             "wasm32", // https://github.com/bazelbuild/platforms/blob/da5541f26b7de1dc8e04c075c99df5351742a4a2/cpu/BUILD#L109
109			"configuration":    *testConfig,
110			"browser":          *browser,
111			"compilation_mode": *compilationMode,
112			"cpu_or_gpu":       *cpuOrGPU,
113			"cpu_or_gpu_value": *cpuOrGPUValue,
114		},
115	}
116	if err := uploadDataToGold(ctx, skiaDir, conf); err != nil {
117		td.Fatal(ctx, err)
118	}
119}
120
121func bazelTest(ctx context.Context, checkoutDir, label, config string, args ...string) error {
122	step := fmt.Sprintf("Running Test %s with config %s and %d extra flags", label, config, len(args))
123	return td.Do(ctx, td.Props(step), func(ctx context.Context) error {
124		runCmd := &sk_exec.Command{
125			Name: "bazelisk",
126			Args: append([]string{"test",
127				label,
128				"--config=" + config, // Should be defined in //bazel/buildrc
129			}, args...),
130			InheritEnv: true, // Makes sure bazelisk is on PATH
131			Dir:        checkoutDir,
132			LogStdout:  true,
133			LogStderr:  true,
134		}
135		_, err := sk_exec.RunCommand(ctx, runCmd)
136		if err != nil {
137			return err
138		}
139		return nil
140	})
141}
142
143type goldctlConfig struct {
144	goldctlPath   string
145	gitCommit     string
146	changelistID  string
147	patchsetOrder int
148	tryjobID      string
149	corpus        string
150	keys          map[string]string
151}
152
153func uploadDataToGold(ctx context.Context, checkoutDir string, cfg goldctlConfig) error {
154	return td.Do(ctx, td.Props("Upload to Gold"), func(ctx context.Context) error {
155		zipExtractDir, err := os_steps.TempDir(ctx, "", "gold_outputs")
156		if err != nil {
157			return err
158		}
159		if err := extractZip(ctx, filepath.Join(checkoutDir, "bazel-testlogs", "modules", "canvaskit",
160			"canvaskit_js_tests", "test.outputs", "outputs.zip"), zipExtractDir); err != nil {
161			return err
162		}
163
164		goldWorkDir, err := os_steps.TempDir(ctx, "", "gold_workdir")
165		if err != nil {
166			return err
167		}
168
169		if err := setupGoldctl(ctx, cfg, goldWorkDir); err != nil {
170			return err
171		}
172
173		if err := addAllGoldImages(ctx, cfg.goldctlPath, zipExtractDir, goldWorkDir); err != nil {
174			return err
175		}
176
177		if err := finalizeGoldctl(ctx, cfg.goldctlPath, goldWorkDir); err != nil {
178			return err
179		}
180		return nil
181	})
182}
183
184func extractZip(ctx context.Context, zipPath, targetDir string) error {
185	runCmd := &sk_exec.Command{
186		Name:      "unzip",
187		Args:      []string{zipPath, "-d", targetDir},
188		LogStdout: true,
189		LogStderr: true,
190	}
191	_, err := sk_exec.RunCommand(ctx, runCmd)
192	if err != nil {
193		return err
194	}
195	return nil
196}
197
198func setupGoldctl(ctx context.Context, cfg goldctlConfig, workDir string) error {
199	authCmd := &sk_exec.Command{
200		Name:      cfg.goldctlPath,
201		Args:      []string{"auth", "--work-dir=" + workDir, "--luci"},
202		LogStdout: true,
203		LogStderr: true,
204	}
205	if _, err := sk_exec.RunCommand(ctx, authCmd); err != nil {
206		return err
207	}
208
209	initArgs := []string{"imgtest", "init", "--work-dir", workDir,
210		"--instance", "skia", "--corpus", cfg.corpus,
211		"--commit", cfg.gitCommit, "--url", "https://gold.skia.org", "--bucket", "skia-infra-gm"}
212
213	if cfg.changelistID != "" {
214		ps := strconv.Itoa(cfg.patchsetOrder)
215		initArgs = append(initArgs, "--crs", "gerrit", "--changelist", cfg.changelistID,
216			"--patchset", ps, "--cis", "buildbucket", "--jobid", cfg.tryjobID)
217	}
218
219	for key, value := range cfg.keys {
220		initArgs = append(initArgs, "--key="+key+":"+value)
221	}
222
223	initCmd := &sk_exec.Command{
224		Name:      cfg.goldctlPath,
225		Args:      initArgs,
226		LogStdout: true,
227		LogStderr: true,
228	}
229	if _, err := sk_exec.RunCommand(ctx, initCmd); err != nil {
230		return err
231	}
232	return nil
233}
234
235func addAllGoldImages(ctx context.Context, goldctlPath, pngsDir, workDir string) error {
236	pngFiles, err := os.ReadDir(pngsDir)
237	if err != nil {
238		return err
239	}
240	return td.Do(ctx, td.Props(fmt.Sprintf("Upload %d images to Gold", len(pngFiles))), func(ctx context.Context) error {
241		for _, entry := range pngFiles {
242			// We expect the filename to be testname.optional_config.png
243			baseName := filepath.Base(entry.Name())
244			parts := strings.Split(baseName, ".")
245			testName := parts[0]
246			addArgs := []string{
247				"imgtest", "add",
248				"--work-dir", workDir,
249				"--png-file", filepath.Join(pngsDir, filepath.Base(entry.Name())),
250				"--test-name", testName,
251			}
252			if len(parts) == 3 {
253				// There was a config specified.
254				addArgs = append(addArgs, "--add-test-key=config:"+parts[1])
255			}
256
257			addCmd := &sk_exec.Command{
258				Name:      goldctlPath,
259				Args:      addArgs,
260				LogStdout: true,
261				LogStderr: true,
262			}
263			if _, err := sk_exec.RunCommand(ctx, addCmd); err != nil {
264				return err
265			}
266		}
267		return nil
268	})
269}
270
271// finalizeGoldctl uploads the JSON file created from adding all the test PNGs. Then, Gold begins
272// ingesting the data.
273func finalizeGoldctl(ctx context.Context, goldctlPath, workDir string) error {
274	finalizeCmd := &sk_exec.Command{
275		Name:      goldctlPath,
276		Args:      []string{"imgtest", "finalize", "--work-dir=" + workDir},
277		LogStdout: true,
278		LogStderr: true,
279	}
280	if _, err := sk_exec.RunCommand(ctx, finalizeCmd); err != nil {
281		return err
282	}
283	return nil
284}
285
286// bazelClean cleans the bazel cache and the external directory via the --expunge flag.
287func bazelClean(ctx context.Context, checkoutDir string) error {
288	return td.Do(ctx, td.Props("Cleaning cache with --expunge"), func(ctx context.Context) error {
289		runCmd := &sk_exec.Command{
290			Name:       "bazelisk",
291			Args:       append([]string{"clean", "--expunge"}),
292			InheritEnv: true, // Makes sure bazelisk is on PATH
293			Dir:        checkoutDir,
294			LogStdout:  true,
295			LogStderr:  true,
296		}
297		_, err := sk_exec.RunCommand(ctx, runCmd)
298		if err != nil {
299			return err
300		}
301		return nil
302	})
303}
304