// Copyright 2023 Google LLC // // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. package common import ( "context" "os" "path/filepath" "testing" exec_testutils "go.skia.org/infra/go/exec/testutils" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.skia.org/infra/go/exec" "go.skia.org/infra/task_driver/go/lib/os_steps" "go.skia.org/infra/task_driver/go/td" "go.skia.org/skia/infra/bots/task_drivers/testutils" ) func TestUploadToGold_NoOutputsZIPOrDir_NoGoldctlInvocations(t *testing.T) { test := func(name string, utgArgs UploadToGoldArgs) { t.Run(name, func(t *testing.T) { commandCollector := exec.CommandCollector{} res := td.RunTestSteps(t, false, func(ctx context.Context) error { ctx = td.WithExecRunFn(ctx, commandCollector.Run) err := UploadToGold(ctx, utgArgs, "/path/to/skia/bazel-testlogs/some/test/target/test.outputs/outputs.zip") assert.NoError(t, err) return err }) require.Empty(t, res.Errors) require.Empty(t, res.Exceptions) testutils.AssertStepNames(t, res, "Test did not produce an undeclared test outputs ZIP file or directory; nothing to upload to Gold", ) assert.Empty(t, commandCollector.Commands()) }) } test("post-submit task", UploadToGoldArgs{ TestOnlyAllowAnyBazelLabel: true, BazelLabel: "//some/test:target", DeviceSpecificBazelConfig: "Pixel5", GoldctlPath: "/path/to/goldctl", GitCommit: "ff99ff99ff99ff99ff99ff99ff99ff99ff99ff99", }) test("CL task", UploadToGoldArgs{ TestOnlyAllowAnyBazelLabel: true, BazelLabel: "//some/test:target", DeviceSpecificBazelConfig: "Pixel5", GoldctlPath: "/path/to/goldctl", GitCommit: "ff99ff99ff99ff99ff99ff99ff99ff99ff99ff99", // These arguments are only used when there are images to upload, and are therefore // ignored by the task driver under this test. ChangelistID: "changelist-id", PatchsetOrder: "1", TryjobID: "tryjob-id", }) } func TestUploadToGold_WithOutputsZIPOrDir_NoValidImages_NoGoldctlInvocations(t *testing.T) { test := func(name string, zip bool, utgArgs UploadToGoldArgs) { t.Run(name, func(t *testing.T) { undeclaredTestOutputs := map[string]string{ // The contents of PNG files does not matter for this test. "image-with-invalid-json-file.png": "fake PNG", "image-with-invalid-json-file.json": `{ "invalid JSON file": "This JSON file should be ignored" }`, "image-with-no-json-file.png": "fake PNG", "json-file-with-no-image.json": `{ "md5": "00000000000000000000000000000000", "keys": { "name": "no-image", "source_type": "no-corpus" } }`, "not-an-image-nor-json-file.txt": "I'm neither a PNG nor a JSON file.", } // Write undeclared test outputs to disk. outputsZIPOrDir := "" if zip { outputsZIPOrDir = filepath.Join(t.TempDir(), "bazel-testlogs", "some", "test", "target", "test.outputs", "outputs.zip") require.NoError(t, os.MkdirAll(filepath.Dir(outputsZIPOrDir), 0700)) testutils.MakeZIP(t, outputsZIPOrDir, undeclaredTestOutputs) } else { outputsZIPOrDir = t.TempDir() testutils.PopulateDir(t, outputsZIPOrDir, undeclaredTestOutputs) } // Will be returned by the mocked os_steps.TempDir() when the task driver tries to create a // directory in which to extract the undeclared outputs ZIP archive. outputsZIPExtractionDir := t.TempDir() commandCollector := exec.CommandCollector{} res := td.RunTestSteps(t, false, func(ctx context.Context) error { ctx = td.WithExecRunFn(ctx, commandCollector.Run) if zip { // Mock os_steps.TempDir() only for the case where outpusZIPOrDir is a ZIP archive. We // don't need to assert the exact number of times that os_steps.TempDir() is called // because said function produces a "Creating TempDir" task driver step, and we check the // exact set of steps produced. ctx = context.WithValue(ctx, os_steps.TempDirContextKey, testutils.MakeTempDirMockFn(t, outputsZIPExtractionDir)) } err := UploadToGold(ctx, utgArgs, outputsZIPOrDir) assert.NoError(t, err) return err }) require.Empty(t, res.Errors) require.Empty(t, res.Exceptions) expectedSteps := []string{} if zip { expectedSteps = append(expectedSteps, "Creating TempDir", "Extract undeclared outputs archive "+outputsZIPOrDir+" into "+outputsZIPExtractionDir, "Extracting file: image-with-invalid-json-file.json", "Extracting file: image-with-invalid-json-file.png", "Extracting file: image-with-no-json-file.png", "Extracting file: json-file-with-no-image.json", "Not extracting non-PNG / non-JSON file: not-an-image-nor-json-file.txt", ) } expectedSteps = append(expectedSteps, "Gather JSON and PNG files produced by GMs", "Ignoring \"image-with-invalid-json-file.json\": field \"md5\" not found", "Ignoring \"json-file-with-no-image.json\": file \"json-file-with-no-image.png\" not found", "Undeclared test outputs ZIP file or directory contains no GM outputs; nothing to upload to Gold", ) testutils.AssertStepNames(t, res, expectedSteps...) assert.Empty(t, commandCollector.Commands()) }) } postSubmitTaskArgs := UploadToGoldArgs{ TestOnlyAllowAnyBazelLabel: true, BazelLabel: "//some/test:target", DeviceSpecificBazelConfig: "Pixel5", GoldctlPath: "/path/to/goldctl", GitCommit: "ff99ff99ff99ff99ff99ff99ff99ff99ff99ff99", } test("post-submit task, ZIP file", true /* =zip */, postSubmitTaskArgs) test("post-submit task, directory", false /* =zip */, postSubmitTaskArgs) clTaskArgs := UploadToGoldArgs{ TestOnlyAllowAnyBazelLabel: true, BazelLabel: "//some/test:target", DeviceSpecificBazelConfig: "Pixel5", GoldctlPath: "/path/to/goldctl", GitCommit: "ff99ff99ff99ff99ff99ff99ff99ff99ff99ff99", // These arguments are only used when there are images to upload, and are therefore // ignored by the task driver under this test. ChangelistID: "changelist-id", PatchsetOrder: "1", TryjobID: "tryjob-id", } test("CL task, ZIP file", true /* =zip */, clTaskArgs) test("CL task, directory", false /* =zip */, clTaskArgs) } func TestUploadToGold_WithOutputsZIPOrDir_ValidImages_ImagesUploadedToGold(t *testing.T) { test := func(name string, zip bool, utgArgs UploadToGoldArgs, goldctlWorkDir, goldctlImgtestInitStepName string, goldctlImgtestInitArgs []string) { t.Run(name, func(t *testing.T) { undeclaredTestOutputs := map[string]string{ // The contents of PNG files does not matter for this test. "alfa.png": "fake PNG", "alfa.json": `{ "md5": "a01a01a01a01a01a01a01a01a01a01a0", "keys": { "build_system": "bazel", "name": "alfa", "source_type": "gm" } }`, "beta.png": "fake PNG", "beta.json": `{ "md5": "b02b02b02b02b02b02b02b02b02b02b0", "keys": { "build_system": "bazel", "name": "beta", "source_type": "gm" } }`, "image-with-invalid-json-file.png": "fake PNG", "image-with-invalid-json-file.json": `{ "invalid JSON file": "This JSON file should be ignored" }`, "image-with-no-json-file.png": "fake PNG", "json-file-with-no-image.json": `{ "md5": "00000000000000000000000000000000", "keys": { "name": "no-image", "source_type": "no-corpus" } }`, "not-an-image-nor-json-file.txt": "I'm neither a PNG nor a JSON file.", } // Write undeclared test outputs to disk. outpusZIPOrDir := "" if zip { outpusZIPOrDir = filepath.Join(t.TempDir(), "bazel-testlogs", "some", "test", "target", "test.outputs", "outputs.zip") require.NoError(t, os.MkdirAll(filepath.Dir(outpusZIPOrDir), 0700)) testutils.MakeZIP(t, outpusZIPOrDir, undeclaredTestOutputs) } else { outpusZIPOrDir = t.TempDir() testutils.PopulateDir(t, outpusZIPOrDir, undeclaredTestOutputs) } // Will be returned by the mocked os_steps.TempDir() when the task driver tries to create a // directory in which to extract the undeclared outputs ZIP archive. outputsZIPExtractionDir := t.TempDir() commandCollector := exec.CommandCollector{} res := td.RunTestSteps(t, false, func(ctx context.Context) error { ctx = td.WithExecRunFn(ctx, commandCollector.Run) tempDirMockFn := testutils.MakeTempDirMockFn(t, goldctlWorkDir) if zip { tempDirMockFn = testutils.MakeTempDirMockFn(t, outputsZIPExtractionDir, goldctlWorkDir) } // We don't need to assert the exact number of times that os_steps.TempDir() is called // because said function produces a "Creating TempDir" task driver step, and we check the // exact set of steps produced. ctx = context.WithValue(ctx, os_steps.TempDirContextKey, tempDirMockFn) err := UploadToGold(ctx, utgArgs, outpusZIPOrDir) assert.NoError(t, err) return err }) require.Empty(t, res.Errors) require.Empty(t, res.Exceptions) dirWithTestOutputs := outpusZIPOrDir if zip { dirWithTestOutputs = outputsZIPExtractionDir } expectedSteps := []string{} if zip { expectedSteps = append(expectedSteps, "Creating TempDir", "Extract undeclared outputs archive "+outpusZIPOrDir+" into "+outputsZIPExtractionDir, "Extracting file: alfa.json", "Extracting file: alfa.png", "Extracting file: beta.json", "Extracting file: beta.png", "Extracting file: image-with-invalid-json-file.json", "Extracting file: image-with-invalid-json-file.png", "Extracting file: image-with-no-json-file.png", "Extracting file: json-file-with-no-image.json", "Not extracting non-PNG / non-JSON file: not-an-image-nor-json-file.txt", ) } expectedSteps = append(expectedSteps, "Gather JSON and PNG files produced by GMs", "Gather \"alfa.png\"", "Gather \"beta.png\"", "Ignoring \"image-with-invalid-json-file.json\": field \"md5\" not found", "Ignoring \"json-file-with-no-image.json\": file \"json-file-with-no-image.png\" not found", "Upload GM outputs to Gold", "Creating TempDir", "/path/to/goldctl auth --work-dir "+goldctlWorkDir+" --luci", goldctlImgtestInitStepName, "/path/to/goldctl imgtest add --work-dir "+goldctlWorkDir+" --test-name alfa --png-file "+dirWithTestOutputs+"/alfa.png --png-digest a01a01a01a01a01a01a01a01a01a01a0 --add-test-key build_system:bazel --add-test-key name:alfa --add-test-key source_type:gm", "/path/to/goldctl imgtest add --work-dir "+goldctlWorkDir+" --test-name beta --png-file "+dirWithTestOutputs+"/beta.png --png-digest b02b02b02b02b02b02b02b02b02b02b0 --add-test-key build_system:bazel --add-test-key name:beta --add-test-key source_type:gm", "/path/to/goldctl imgtest finalize --work-dir "+goldctlWorkDir, ) testutils.AssertStepNames(t, res, expectedSteps...) exec_testutils.AssertCommandsMatch(t, [][]string{ { "/path/to/goldctl", "auth", "--work-dir", goldctlWorkDir, "--luci", }, append([]string{"/path/to/goldctl"}, goldctlImgtestInitArgs..., ), { "/path/to/goldctl", "imgtest", "add", "--work-dir", goldctlWorkDir, "--test-name", "alfa", "--png-file", dirWithTestOutputs + "/alfa.png", "--png-digest", "a01a01a01a01a01a01a01a01a01a01a0", "--add-test-key", "build_system:bazel", "--add-test-key", "name:alfa", "--add-test-key", "source_type:gm", }, { "/path/to/goldctl", "imgtest", "add", "--work-dir", goldctlWorkDir, "--test-name", "beta", "--png-file", dirWithTestOutputs + "/beta.png", "--png-digest", "b02b02b02b02b02b02b02b02b02b02b0", "--add-test-key", "build_system:bazel", "--add-test-key", "name:beta", "--add-test-key", "source_type:gm", }, { "/path/to/goldctl", "imgtest", "finalize", "--work-dir", goldctlWorkDir, }, }, commandCollector.Commands()) }) } goldctlWorkDir := t.TempDir() postSubmitTaskArgs := UploadToGoldArgs{ TestOnlyAllowAnyBazelLabel: true, BazelLabel: "//some/test:target", DeviceSpecificBazelConfig: "Pixel5", GoldctlPath: "/path/to/goldctl", GitCommit: "ff99ff99ff99ff99ff99ff99ff99ff99ff99ff99", } postSubmitTaskGoldctlImgtestInitStep := "/path/to/goldctl imgtest init --work-dir " + goldctlWorkDir + " --instance skia --url https://gold.skia.org --bucket skia-infra-gm --git_hash ff99ff99ff99ff99ff99ff99ff99ff99ff99ff99 --key arch:arm64 --key model:Pixel5 --key os:Android" postSubmitTaskGoldctlImgtestInitArgs := []string{ "imgtest", "init", "--work-dir", goldctlWorkDir, "--instance", "skia", "--url", "https://gold.skia.org", "--bucket", "skia-infra-gm", "--git_hash", "ff99ff99ff99ff99ff99ff99ff99ff99ff99ff99", "--key", "arch:arm64", "--key", "model:Pixel5", "--key", "os:Android", } test("post-submit task, ZIP file", true /* =zip */, postSubmitTaskArgs, goldctlWorkDir, postSubmitTaskGoldctlImgtestInitStep, postSubmitTaskGoldctlImgtestInitArgs) test("post-submit task, directory", false /* =zip */, postSubmitTaskArgs, goldctlWorkDir, postSubmitTaskGoldctlImgtestInitStep, postSubmitTaskGoldctlImgtestInitArgs) goldctlWorkDir = t.TempDir() clTaskArgs := UploadToGoldArgs{ TestOnlyAllowAnyBazelLabel: true, BazelLabel: "//some/test:target", DeviceSpecificBazelConfig: "Pixel5", GoldctlPath: "/path/to/goldctl", GitCommit: "ff99ff99ff99ff99ff99ff99ff99ff99ff99ff99", ChangelistID: "changelist-id", PatchsetOrder: "1", TryjobID: "tryjob-id", } clTaskGoldctlImgtestInitStep := "/path/to/goldctl imgtest init --work-dir " + goldctlWorkDir + " --instance skia --url https://gold.skia.org --bucket skia-infra-gm --git_hash ff99ff99ff99ff99ff99ff99ff99ff99ff99ff99 --crs gerrit --cis buildbucket --changelist changelist-id --patchset 1 --jobid tryjob-id --key arch:arm64 --key model:Pixel5 --key os:Android" clTaskGoldctlImgtestInitArgs := []string{ "imgtest", "init", "--work-dir", goldctlWorkDir, "--instance", "skia", "--url", "https://gold.skia.org", "--bucket", "skia-infra-gm", "--git_hash", "ff99ff99ff99ff99ff99ff99ff99ff99ff99ff99", "--crs", "gerrit", "--cis", "buildbucket", "--changelist", "changelist-id", "--patchset", "1", "--jobid", "tryjob-id", "--key", "arch:arm64", "--key", "model:Pixel5", "--key", "os:Android", } test("CL task, ZIP file", true /* =zip */, clTaskArgs, goldctlWorkDir, clTaskGoldctlImgtestInitStep, clTaskGoldctlImgtestInitArgs) test("CL task, directory", false /* =zip */, clTaskArgs, goldctlWorkDir, clTaskGoldctlImgtestInitStep, clTaskGoldctlImgtestInitArgs) }