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