blob: 3f938664a8e517a5eedc6c0dc679ab8a8b79c56c [file] [log] [blame]
// Copyright 2022 Google LLC
//
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//
// This executable builds and tests CanvasKit. The tests produce images (aka gms) and these
// are uploaded to Gold.
// It requires unzip to be installed (which Bazel already requires).
package main
import (
"context"
"flag"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
sk_exec "go.skia.org/infra/go/exec"
"go.skia.org/infra/go/skerr"
"go.skia.org/infra/task_driver/go/lib/bazel"
"go.skia.org/infra/task_driver/go/lib/os_steps"
"go.skia.org/infra/task_driver/go/td"
)
// This value is arbitrarily selected. It is smaller than our maximum RBE pool size.
const rbeJobs = 100
var (
// Required properties for this task.
projectId = flag.String("project_id", "", "ID of the Google Cloud project.")
taskId = flag.String("task_id", "", "ID of this task.")
taskName = flag.String("task_name", "", "Name of the task.")
workdir = flag.String("workdir", ".", "Working directory, the root directory of a full Skia checkout")
testConfig = flag.String("test_config", "", "The config name (defined in //bazel/buildrc), which indicates how CanvasKit should be compiled and tested.")
cross = flag.String("cross", "", "[not yet supported] For use with cross-compiling.")
// goldctl data
goldctlPath = flag.String("goldctl_path", "", "The path to the golctl binary on disk.")
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.")
changelistID = flag.String("changelist_id", "", "Should be non-empty only when run on the CQ.")
patchsetOrderStr = flag.String("patchset_order", "", "Should be non-zero only when run on the CQ.")
tryjobID = flag.String("tryjob_id", "", "Should be non-zero only when run on the CQ.")
// goldctl keys
browser = flag.String("browser", "Chrome", "The browser running the tests")
compilationMode = flag.String("compilation_mode", "Release", "How the binary was compiled")
cpuOrGPU = flag.String("cpu_or_gpu", "GPU", "The render backend")
cpuOrGPUValue = flag.String("cpu_or_gpu_value", "WebGL2", "What variant of the render backend")
// Optional flags.
bazelCacheDir = flag.String("bazel_cache_dir", "/mnt/pd0/bazel_cache", "Override the Bazel cache directory with this path")
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.")
local = flag.Bool("local", false, "True if running locally (as opposed to on the CI/CQ)")
output = flag.String("o", "", "If provided, dump a JSON blob of step data to the given file. Prints to stdout if '-' is given.")
)
func main() {
ctx := td.StartRun(projectId, taskId, taskName, output, local)
defer td.EndRun(ctx)
goldctlAbsPath := td.MustGetAbsolutePathOfFlag(ctx, *goldctlPath, "gold_ctl_path")
wd := td.MustGetAbsolutePathOfFlag(ctx, *workdir, "workdir")
skiaDir := filepath.Join(wd, "skia")
patchsetOrder := 0
if *patchsetOrderStr != "" {
var err error
patchsetOrder, err = strconv.Atoi(*patchsetOrderStr)
if err != nil {
fmt.Println("Non-integer value passed in to --patchset_order")
td.Fatal(ctx, err)
}
}
if *testConfig == "" {
td.Fatal(ctx, skerr.Fmt("Must specify --test_config"))
}
opts := bazel.BazelOptions{
// We want the cache to be on a bigger disk than default. The root disk, where the home
// directory (and default Bazel cache) lives, is only 15 GB on our GCE VMs.
CachePath: *bazelCacheDir,
}
if err := bazel.EnsureBazelRCFile(ctx, opts); err != nil {
td.Fatal(ctx, err)
}
if *cross != "" {
fmt.Println("Saw --cross, but don't know what to do with that yet.")
}
if *expungeCache {
if err := bazelClean(ctx, skiaDir); err != nil {
td.Fatal(ctx, err)
}
}
if err := bazelTest(ctx, skiaDir, "//modules/canvaskit:canvaskit_js_tests", *testConfig,
"--config=linux_rbe", "--test_output=streamed", "--jobs="+strconv.Itoa(rbeJobs)); err != nil {
td.Fatal(ctx, err)
}
conf := goldctlConfig{
goldctlPath: goldctlAbsPath,
gitCommit: *gitCommit,
changelistID: *changelistID,
patchsetOrder: patchsetOrder,
tryjobID: *tryjobID,
corpus: "canvaskit",
keys: map[string]string{
"arch": "wasm32", // https://github.com/bazelbuild/platforms/blob/da5541f26b7de1dc8e04c075c99df5351742a4a2/cpu/BUILD#L109
"configuration": *testConfig,
"browser": *browser,
"compilation_mode": *compilationMode,
"cpu_or_gpu": *cpuOrGPU,
"cpu_or_gpu_value": *cpuOrGPUValue,
},
}
if err := uploadDataToGold(ctx, skiaDir, conf); err != nil {
td.Fatal(ctx, err)
}
}
func bazelTest(ctx context.Context, checkoutDir, label, config string, args ...string) error {
step := fmt.Sprintf("Running Test %s with config %s and %d extra flags", label, config, len(args))
return td.Do(ctx, td.Props(step), func(ctx context.Context) error {
runCmd := &sk_exec.Command{
Name: "bazelisk",
Args: append([]string{"test",
label,
"--config=" + config, // Should be defined in //bazel/buildrc
}, args...),
InheritEnv: true, // Makes sure bazelisk is on PATH
Dir: checkoutDir,
LogStdout: true,
LogStderr: true,
}
_, err := sk_exec.RunCommand(ctx, runCmd)
if err != nil {
return err
}
return nil
})
}
type goldctlConfig struct {
goldctlPath string
gitCommit string
changelistID string
patchsetOrder int
tryjobID string
corpus string
keys map[string]string
}
func uploadDataToGold(ctx context.Context, checkoutDir string, cfg goldctlConfig) error {
return td.Do(ctx, td.Props("Upload to Gold"), func(ctx context.Context) error {
zipExtractDir, err := os_steps.TempDir(ctx, "", "gold_outputs")
if err != nil {
return err
}
if err := extractZip(ctx, filepath.Join(checkoutDir, "bazel-testlogs", "modules", "canvaskit",
"canvaskit_js_tests", "test.outputs", "outputs.zip"), zipExtractDir); err != nil {
return err
}
goldWorkDir, err := os_steps.TempDir(ctx, "", "gold_workdir")
if err != nil {
return err
}
if err := setupGoldctl(ctx, cfg, goldWorkDir); err != nil {
return err
}
if err := addAllGoldImages(ctx, cfg.goldctlPath, zipExtractDir, goldWorkDir); err != nil {
return err
}
if err := finalizeGoldctl(ctx, cfg.goldctlPath, goldWorkDir); err != nil {
return err
}
return nil
})
}
func extractZip(ctx context.Context, zipPath, targetDir string) error {
runCmd := &sk_exec.Command{
Name: "unzip",
Args: []string{zipPath, "-d", targetDir},
LogStdout: true,
LogStderr: true,
}
_, err := sk_exec.RunCommand(ctx, runCmd)
if err != nil {
return err
}
return nil
}
func setupGoldctl(ctx context.Context, cfg goldctlConfig, workDir string) error {
authCmd := &sk_exec.Command{
Name: cfg.goldctlPath,
Args: []string{"auth", "--work-dir=" + workDir, "--luci"},
LogStdout: true,
LogStderr: true,
}
if _, err := sk_exec.RunCommand(ctx, authCmd); err != nil {
return err
}
initArgs := []string{"imgtest", "init", "--work-dir", workDir,
"--instance", "skia", "--corpus", cfg.corpus,
"--commit", cfg.gitCommit, "--url", "https://gold.skia.org", "--bucket", "skia-infra-gm"}
if cfg.changelistID != "" {
ps := strconv.Itoa(cfg.patchsetOrder)
initArgs = append(initArgs, "--crs", "gerrit", "--changelist", cfg.changelistID,
"--patchset", ps, "--cis", "buildbucket", "--jobid", cfg.tryjobID)
}
for key, value := range cfg.keys {
initArgs = append(initArgs, "--key="+key+":"+value)
}
initCmd := &sk_exec.Command{
Name: cfg.goldctlPath,
Args: initArgs,
LogStdout: true,
LogStderr: true,
}
if _, err := sk_exec.RunCommand(ctx, initCmd); err != nil {
return err
}
return nil
}
func addAllGoldImages(ctx context.Context, goldctlPath, pngsDir, workDir string) error {
pngFiles, err := os.ReadDir(pngsDir)
if err != nil {
return err
}
return td.Do(ctx, td.Props(fmt.Sprintf("Upload %d images to Gold", len(pngFiles))), func(ctx context.Context) error {
for _, entry := range pngFiles {
// We expect the filename to be testname.optional_config.png
baseName := filepath.Base(entry.Name())
parts := strings.Split(baseName, ".")
testName := parts[0]
addArgs := []string{
"imgtest", "add",
"--work-dir", workDir,
"--png-file", filepath.Join(pngsDir, filepath.Base(entry.Name())),
"--test-name", testName,
}
if len(parts) == 3 {
// There was a config specified.
addArgs = append(addArgs, "--add-test-key=config:"+parts[1])
}
addCmd := &sk_exec.Command{
Name: goldctlPath,
Args: addArgs,
LogStdout: true,
LogStderr: true,
}
if _, err := sk_exec.RunCommand(ctx, addCmd); err != nil {
return err
}
}
return nil
})
}
// finalizeGoldctl uploads the JSON file created from adding all the test PNGs. Then, Gold begins
// ingesting the data.
func finalizeGoldctl(ctx context.Context, goldctlPath, workDir string) error {
finalizeCmd := &sk_exec.Command{
Name: goldctlPath,
Args: []string{"imgtest", "finalize", "--work-dir=" + workDir},
LogStdout: true,
LogStderr: true,
}
if _, err := sk_exec.RunCommand(ctx, finalizeCmd); err != nil {
return err
}
return nil
}
// bazelClean cleans the bazel cache and the external directory via the --expunge flag.
func bazelClean(ctx context.Context, checkoutDir string) error {
return td.Do(ctx, td.Props("Cleaning cache with --expunge"), func(ctx context.Context) error {
runCmd := &sk_exec.Command{
Name: "bazelisk",
Args: append([]string{"clean", "--expunge"}),
InheritEnv: true, // Makes sure bazelisk is on PATH
Dir: checkoutDir,
LogStdout: true,
LogStderr: true,
}
_, err := sk_exec.RunCommand(ctx, runCmd)
if err != nil {
return err
}
return nil
})
}