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/task_driver/go/lib/bazel" 22 "go.skia.org/infra/task_driver/go/lib/os_steps" 23 "go.skia.org/infra/task_driver/go/td" 24 "go.skia.org/skia/infra/bots/task_drivers/common" 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 // goldctl data 37 goldctlPath = flag.String("goldctl_path", "", "The path to the golctl binary on disk.") 38 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.") 39 changelistID = flag.String("changelist_id", "", "Should be non-empty only when run on the CQ.") 40 patchsetOrderStr = flag.String("patchset_order", "", "Should be non-zero only when run on the CQ.") 41 tryjobID = flag.String("tryjob_id", "", "Should be non-zero only when run on the CQ.") 42 // goldctl keys 43 browser = flag.String("browser", "Chrome", "The browser running the tests") 44 compilationMode = flag.String("compilation_mode", "Release", "How the binary was compiled") 45 cpuOrGPU = flag.String("cpu_or_gpu", "GPU", "The render backend") 46 cpuOrGPUValue = flag.String("cpu_or_gpu_value", "WebGL2", "What variant of the render backend") 47 48 // Optional flags. 49 local = flag.Bool("local", false, "True if running locally (as opposed to on the CI/CQ)") 50 output = flag.String("o", "", "If provided, dump a JSON blob of step data to the given file. Prints to stdout if '-' is given.") 51) 52 53func main() { 54 bazelFlags := common.MakeBazelFlags(common.MakeBazelFlagsOpts{ 55 Label: true, 56 Config: true, 57 }) 58 59 ctx := td.StartRun(projectId, taskId, taskName, output, local) 60 defer td.EndRun(ctx) 61 62 bazelFlags.Validate(ctx) 63 64 goldctlAbsPath := td.MustGetAbsolutePathOfFlag(ctx, *goldctlPath, "gold_ctl_path") 65 wd := td.MustGetAbsolutePathOfFlag(ctx, *workdir, "workdir") 66 skiaDir := filepath.Join(wd, "skia") 67 patchsetOrder := 0 68 if *patchsetOrderStr != "" { 69 var err error 70 patchsetOrder, err = strconv.Atoi(*patchsetOrderStr) 71 if err != nil { 72 fmt.Println("Non-integer value passed in to --patchset_order") 73 td.Fatal(ctx, err) 74 } 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: *bazelFlags.CacheDir, 81 } 82 if err := bazel.EnsureBazelRCFile(ctx, opts); err != nil { 83 td.Fatal(ctx, err) 84 } 85 86 if err := bazelTest(ctx, skiaDir, *bazelFlags.Label, *bazelFlags.Config, 87 "--config=linux_rbe", "--test_output=streamed", "--jobs="+strconv.Itoa(rbeJobs)); err != nil { 88 td.Fatal(ctx, err) 89 } 90 91 conf := goldctlConfig{ 92 goldctlPath: goldctlAbsPath, 93 gitCommit: *gitCommit, 94 changelistID: *changelistID, 95 patchsetOrder: patchsetOrder, 96 tryjobID: *tryjobID, 97 corpus: "canvaskit", 98 keys: map[string]string{ 99 "arch": "wasm32", // https://github.com/bazelbuild/platforms/blob/da5541f26b7de1dc8e04c075c99df5351742a4a2/cpu/BUILD#L109 100 "configuration": *bazelFlags.Config, 101 "browser": *browser, 102 "compilation_mode": *compilationMode, 103 "cpu_or_gpu": *cpuOrGPU, 104 "cpu_or_gpu_value": *cpuOrGPUValue, 105 }, 106 } 107 if err := uploadDataToGold(ctx, *bazelFlags.Label, skiaDir, conf); err != nil { 108 td.Fatal(ctx, err) 109 } 110 111 if !*local { 112 if err := common.BazelCleanIfLowDiskSpace(ctx, *bazelFlags.CacheDir, skiaDir, "bazelisk"); err != nil { 113 td.Fatal(ctx, err) 114 } 115 } 116} 117 118func bazelTest(ctx context.Context, checkoutDir, label, config string, args ...string) error { 119 step := fmt.Sprintf("Running Test %s with config %s and %d extra flags", label, config, len(args)) 120 return td.Do(ctx, td.Props(step), func(ctx context.Context) error { 121 runCmd := &sk_exec.Command{ 122 Name: "bazelisk", 123 Args: append([]string{"test", 124 label, 125 "--config=" + config, // Should be defined in //bazel/buildrc 126 }, args...), 127 InheritEnv: true, // Makes sure bazelisk is on PATH 128 Dir: checkoutDir, 129 LogStdout: true, 130 LogStderr: true, 131 } 132 _, err := sk_exec.RunCommand(ctx, runCmd) 133 if err != nil { 134 return err 135 } 136 return nil 137 }) 138} 139 140type goldctlConfig struct { 141 goldctlPath string 142 gitCommit string 143 changelistID string 144 patchsetOrder int 145 tryjobID string 146 corpus string 147 keys map[string]string 148} 149 150func uploadDataToGold(ctx context.Context, label, checkoutDir string, cfg goldctlConfig) error { 151 return td.Do(ctx, td.Props("Upload to Gold"), func(ctx context.Context) error { 152 zipExtractDir, err := os_steps.TempDir(ctx, "", "gold_outputs") 153 if err != nil { 154 return err 155 } 156 157 // Turn "//path/to:target" into "path/to/target". 158 labelBazelTestlogsPath := strings.ReplaceAll(label, "//", "") 159 labelBazelTestlogsPath = strings.ReplaceAll(label, ":", "/") 160 161 if err := extractZip(ctx, filepath.Join(checkoutDir, "bazel-testlogs", labelBazelTestlogsPath, "test.outputs", "outputs.zip"), zipExtractDir); err != nil { 162 return err 163 } 164 165 goldWorkDir, err := os_steps.TempDir(ctx, "", "gold_workdir") 166 if err != nil { 167 return err 168 } 169 170 if err := setupGoldctl(ctx, cfg, goldWorkDir); err != nil { 171 return err 172 } 173 174 if err := addAllGoldImages(ctx, cfg.goldctlPath, zipExtractDir, goldWorkDir); err != nil { 175 return err 176 } 177 178 if err := finalizeGoldctl(ctx, cfg.goldctlPath, goldWorkDir); err != nil { 179 return err 180 } 181 return nil 182 }) 183} 184 185func extractZip(ctx context.Context, zipPath, targetDir string) error { 186 runCmd := &sk_exec.Command{ 187 Name: "unzip", 188 Args: []string{zipPath, "-d", targetDir}, 189 LogStdout: true, 190 LogStderr: true, 191 } 192 _, err := sk_exec.RunCommand(ctx, runCmd) 193 if err != nil { 194 return err 195 } 196 return nil 197} 198 199func setupGoldctl(ctx context.Context, cfg goldctlConfig, workDir string) error { 200 authCmd := &sk_exec.Command{ 201 Name: cfg.goldctlPath, 202 Args: []string{"auth", "--work-dir=" + workDir, "--luci"}, 203 LogStdout: true, 204 LogStderr: true, 205 } 206 if _, err := sk_exec.RunCommand(ctx, authCmd); err != nil { 207 return err 208 } 209 210 initArgs := []string{"imgtest", "init", "--work-dir", workDir, 211 "--instance", "skia", "--corpus", cfg.corpus, 212 "--commit", cfg.gitCommit, "--url", "https://gold.skia.org", "--bucket", "skia-infra-gm"} 213 214 if cfg.changelistID != "" { 215 ps := strconv.Itoa(cfg.patchsetOrder) 216 initArgs = append(initArgs, "--crs", "gerrit", "--changelist", cfg.changelistID, 217 "--patchset", ps, "--cis", "buildbucket", "--jobid", cfg.tryjobID) 218 } 219 220 for key, value := range cfg.keys { 221 initArgs = append(initArgs, "--key="+key+":"+value) 222 } 223 224 initCmd := &sk_exec.Command{ 225 Name: cfg.goldctlPath, 226 Args: initArgs, 227 LogStdout: true, 228 LogStderr: true, 229 } 230 if _, err := sk_exec.RunCommand(ctx, initCmd); err != nil { 231 return err 232 } 233 return nil 234} 235 236func addAllGoldImages(ctx context.Context, goldctlPath, pngsDir, workDir string) error { 237 pngFiles, err := os.ReadDir(pngsDir) 238 if err != nil { 239 return err 240 } 241 return td.Do(ctx, td.Props(fmt.Sprintf("Upload %d images to Gold", len(pngFiles))), func(ctx context.Context) error { 242 for _, entry := range pngFiles { 243 // We expect the filename to be testname.optional_config.png 244 baseName := filepath.Base(entry.Name()) 245 parts := strings.Split(baseName, ".") 246 testName := parts[0] 247 addArgs := []string{ 248 "imgtest", "add", 249 "--work-dir", workDir, 250 "--png-file", filepath.Join(pngsDir, filepath.Base(entry.Name())), 251 "--test-name", testName, 252 } 253 if len(parts) == 3 { 254 // There was a config specified. 255 addArgs = append(addArgs, "--add-test-key=config:"+parts[1]) 256 } 257 258 addCmd := &sk_exec.Command{ 259 Name: goldctlPath, 260 Args: addArgs, 261 LogStdout: true, 262 LogStderr: true, 263 } 264 if _, err := sk_exec.RunCommand(ctx, addCmd); err != nil { 265 return err 266 } 267 } 268 return nil 269 }) 270} 271 272// finalizeGoldctl uploads the JSON file created from adding all the test PNGs. Then, Gold begins 273// ingesting the data. 274func finalizeGoldctl(ctx context.Context, goldctlPath, workDir string) error { 275 finalizeCmd := &sk_exec.Command{ 276 Name: goldctlPath, 277 Args: []string{"imgtest", "finalize", "--work-dir=" + workDir}, 278 LogStdout: true, 279 LogStderr: true, 280 } 281 if _, err := sk_exec.RunCommand(ctx, finalizeCmd); err != nil { 282 return err 283 } 284 return nil 285} 286