| package goldclient |
| |
| import ( |
| "bytes" |
| "context" |
| "encoding/hex" |
| "encoding/json" |
| "errors" |
| "image" |
| "image/png" |
| "io" |
| "net/http" |
| "os" |
| "path/filepath" |
| "testing" |
| "time" |
| |
| "github.com/stretchr/testify/assert" |
| "github.com/stretchr/testify/mock" |
| "github.com/stretchr/testify/require" |
| |
| "go.skia.org/infra/go/deepequal/assertdeep" |
| "go.skia.org/infra/go/fileutil" |
| "go.skia.org/infra/go/now" |
| "go.skia.org/infra/go/paramtools" |
| "go.skia.org/infra/go/skerr" |
| "go.skia.org/infra/go/testutils" |
| "go.skia.org/infra/go/util" |
| "go.skia.org/infra/gold-client/go/imgmatching" |
| "go.skia.org/infra/gold-client/go/mocks" |
| "go.skia.org/infra/golden/go/diff" |
| "go.skia.org/infra/golden/go/expectations" |
| "go.skia.org/infra/golden/go/image/text" |
| "go.skia.org/infra/golden/go/jsonio" |
| "go.skia.org/infra/golden/go/sql" |
| one_by_five "go.skia.org/infra/golden/go/testutils/data_one_by_five" |
| "go.skia.org/infra/golden/go/tiling" |
| "go.skia.org/infra/golden/go/types" |
| "go.skia.org/infra/golden/go/web/frontend" |
| ) |
| |
| // test data processing of the known hashes input. |
| func TestLoadKnownHashes(t *testing.T) { |
| |
| wd := t.TempDir() |
| |
| ctx, httpClient, uploader, _ := makeMocks() |
| defer httpClient.AssertExpectations(t) |
| defer uploader.AssertExpectations(t) |
| |
| hashesResp := httpResponse(mockHashesTxt, "200 OK", http.StatusOK) |
| httpClient.On("Get", testutils.AnyContext, "https://testing-gold.skia.org/json/v1/hashes").Return(hashesResp, nil) |
| |
| exp := httpResponse("{}", "200 OK", http.StatusOK) |
| httpClient.On("Get", testutils.AnyContext, "https://testing-gold.skia.org/json/v2/expectations?issue=867&crs=gerrit").Return(exp, nil) |
| |
| goldClient, err := makeGoldClient(false /*=passFail*/, false /*=uploadOnly*/, wd) |
| assert.NoError(t, err) |
| err = goldClient.SetSharedConfig(ctx, makeTestSharedConfig(), false) |
| assert.NoError(t, err) |
| // Check that the baseline was loaded correctly |
| baseline := goldClient.resultState.Expectations |
| assert.Empty(t, baseline, "No expectations loaded") |
| |
| knownHashes := goldClient.resultState.KnownHashes |
| assert.Len(t, knownHashes, 4, "4 hashes loaded") |
| // spot check |
| assert.Contains(t, knownHashes, types.Digest("a9e1481ebc45c1c4f6720d1119644c20")) |
| assert.NotContains(t, knownHashes, "notInThere") |
| } |
| |
| // TestLoadBaseline loads a baseline for an issue (testSharedConfig defaults to being |
| // an configured for a tryjob). |
| func TestLoadBaseline(t *testing.T) { |
| |
| wd := t.TempDir() |
| |
| ctx, httpClient, uploader, _ := makeMocks() |
| defer httpClient.AssertExpectations(t) |
| defer uploader.AssertExpectations(t) |
| |
| hashesResp := httpResponse("none", "200 OK", http.StatusOK) |
| httpClient.On("Get", testutils.AnyContext, "https://testing-gold.skia.org/json/v1/hashes").Return(hashesResp, nil) |
| |
| exp := httpResponse(mockBaselineJSON, "200 OK", http.StatusOK) |
| httpClient.On("Get", testutils.AnyContext, "https://testing-gold.skia.org/json/v2/expectations?issue=867&crs=gerrit").Return(exp, nil) |
| |
| goldClient, err := makeGoldClient(false /*=passFail*/, false /*=uploadOnly*/, wd) |
| assert.NoError(t, err) |
| err = goldClient.SetSharedConfig(ctx, makeTestSharedConfig(), false) |
| assert.NoError(t, err) |
| |
| // Check that the baseline was loaded correctly |
| bl := goldClient.resultState.Expectations |
| assert.Len(t, bl, 1, "only one test") |
| digests := bl["ThisIsTheOnlyTest"] |
| assert.Len(t, digests, 2, "two previously seen images") |
| assert.Equal(t, expectations.Negative, digests["badbadbad1325855590527db196112e0"]) |
| assert.Equal(t, expectations.Positive, digests["beef00d3a1527db19619ec12a4e0df68"]) |
| |
| assert.Equal(t, testIssueID, goldClient.resultState.SharedConfig.ChangelistID) |
| |
| knownHashes := goldClient.resultState.KnownHashes |
| assert.Empty(t, knownHashes, "No hashes loaded") |
| } |
| |
| // TestLoadBaselineMaster loads the baseline for the master branch. |
| func TestLoadBaselineMaster(t *testing.T) { |
| |
| wd := t.TempDir() |
| |
| ctx, httpClient, uploader, _ := makeMocks() |
| defer httpClient.AssertExpectations(t) |
| defer uploader.AssertExpectations(t) |
| |
| hashesResp := httpResponse("none", "200 OK", http.StatusOK) |
| httpClient.On("Get", testutils.AnyContext, "https://testing-gold.skia.org/json/v1/hashes").Return(hashesResp, nil) |
| |
| exp := httpResponse(mockBaselineJSON, "200 OK", http.StatusOK) |
| httpClient.On("Get", testutils.AnyContext, "https://testing-gold.skia.org/json/v2/expectations").Return(exp, nil) |
| |
| goldClient, err := makeGoldClient(false /*=passFail*/, false /*=uploadOnly*/, wd) |
| assert.NoError(t, err) |
| err = goldClient.SetSharedConfig(ctx, jsonio.GoldResults{ |
| GitHash: "abcd1234", |
| Key: map[string]string{ |
| "os": "WinTest", |
| "gpu": "GPUTest", |
| }, |
| // defaults to master branch |
| }, false) |
| assert.NoError(t, err) |
| |
| // Check that the baseline was loaded correctly |
| bl := goldClient.resultState.Expectations |
| assert.Len(t, bl, 1, "only one test") |
| digests := bl["ThisIsTheOnlyTest"] |
| assert.Len(t, digests, 2, "two previously seen images") |
| assert.Equal(t, expectations.Negative, digests["badbadbad1325855590527db196112e0"]) |
| assert.Equal(t, expectations.Positive, digests["beef00d3a1527db19619ec12a4e0df68"]) |
| |
| assert.Equal(t, "", goldClient.resultState.SharedConfig.ChangelistID) |
| |
| knownHashes := goldClient.resultState.KnownHashes |
| assert.Empty(t, knownHashes, "No hashes loaded") |
| } |
| |
| // Test that the working dir has the correct JSON after initializing. |
| // This is effectively a test for "goldctl imgtest init" |
| func TestInit(t *testing.T) { |
| // This test reads and writes a small amount of data from/to disk |
| |
| wd := t.TempDir() |
| |
| ctx, httpClient, uploader, _ := makeMocks() |
| defer httpClient.AssertExpectations(t) |
| defer uploader.AssertExpectations(t) |
| |
| hashesResp := httpResponse(mockHashesTxt, "200 OK", http.StatusOK) |
| httpClient.On("Get", testutils.AnyContext, "https://testing-gold.skia.org/json/v1/hashes").Return(hashesResp, nil) |
| |
| exp := httpResponse(mockBaselineJSON, "200 OK", http.StatusOK) |
| httpClient.On("Get", testutils.AnyContext, "https://testing-gold.skia.org/json/v2/expectations?issue=867&crs=gerrit").Return(exp, nil) |
| |
| // no uploader calls |
| |
| goldClient, err := makeGoldClient(true /*=passFail*/, false /*=uploadOnly*/, wd) |
| assert.NoError(t, err) |
| err = goldClient.SetSharedConfig(ctx, makeTestSharedConfig(), false) |
| assert.NoError(t, err) |
| |
| outFile := filepath.Join(wd, stateFile) |
| assert.True(t, fileutil.FileExists(outFile)) |
| |
| state, err := loadStateFromJSON(outFile) |
| assert.NoError(t, err) |
| assert.True(t, state.PerTestPassFail) |
| assert.False(t, state.UploadOnly) |
| assert.Equal(t, "testing", state.InstanceID) |
| assert.Equal(t, "https://testing-gold.skia.org", state.GoldURL) |
| assert.Equal(t, "skia-gold-testing", state.Bucket) |
| assert.Len(t, state.KnownHashes, 4) // these should be saved to disk |
| assert.Len(t, state.Expectations, 1) |
| assert.Len(t, state.Expectations["ThisIsTheOnlyTest"], 2) |
| assert.Equal(t, makeTestSharedConfig(), state.SharedConfig) |
| |
| _, err = loadStateFromJSON("/tmp/some-file-guaranteed-not-to-exist") |
| assert.Error(t, err) |
| } |
| |
| // TestInitInvalidKeys fails if the SharedConfig would not pass validation (e.g. keys are malformed) |
| func TestInitInvalidKeys(t *testing.T) { |
| |
| wd := t.TempDir() |
| |
| ctx, _, _, _ := makeMocks() |
| |
| goldClient, err := makeGoldClient(true /*=passFail*/, false /*=uploadOnly*/, wd) |
| assert.NoError(t, err) |
| conf := makeTestSharedConfig() |
| conf.Key["blank"] = "" |
| err = goldClient.SetSharedConfig(ctx, conf, false) |
| assert.Error(t, err) |
| assert.Contains(t, err.Error(), `invalid configuration`) |
| } |
| |
| // Test that the client does not fetch from the server if UploadOnly is set. |
| // This is effectively a test for "goldctl imgtest init --upload-only" |
| func TestInitUploadOnly(t *testing.T) { |
| // This test reads and writes a small amount of data from/to disk |
| |
| wd := t.TempDir() |
| |
| ctx, httpClient, uploader, _ := makeMocks() |
| defer httpClient.AssertExpectations(t) |
| defer uploader.AssertExpectations(t) |
| |
| // no calls of any kind |
| |
| config := GoldClientConfig{ |
| InstanceID: "fuchsia", |
| WorkDir: wd, |
| PassFailStep: false, |
| UploadOnly: true, |
| } |
| |
| goldClient, err := NewCloudClient(config) |
| assert.NoError(t, err) |
| err = goldClient.SetSharedConfig(ctx, makeTestSharedConfig(), false) |
| assert.NoError(t, err) |
| |
| outFile := filepath.Join(wd, stateFile) |
| assert.True(t, fileutil.FileExists(outFile)) |
| |
| state, err := loadStateFromJSON(outFile) |
| assert.NoError(t, err) |
| assert.False(t, state.PerTestPassFail) |
| assert.True(t, state.UploadOnly) |
| assert.Equal(t, "fuchsia", state.InstanceID) |
| assert.Equal(t, "https://fuchsia-gold.corp.goog", state.GoldURL) |
| assert.Equal(t, "skia-gold-fuchsia", state.Bucket) |
| } |
| |
| func TestAddResult_Success(t *testing.T) { |
| |
| const expectedTraceID = "673031cf116813b91be9c4ac14d62412" |
| _, tb := sql.SerializeMap(map[string]string{"alpha": "beta", "gamma": "delta", "name": "my_test", "source_type": "my_corpus"}) |
| require.Equal(t, expectedTraceID, hex.EncodeToString(tb)) |
| |
| goldClient := CloudClient{ |
| resultState: &resultState{ |
| InstanceID: "my_instance", // Should be ignored. |
| SharedConfig: jsonio.GoldResults{ |
| Key: map[string]string{ |
| "alpha": "beta", |
| types.CorpusField: "my_corpus", |
| }, |
| }, |
| }, |
| } |
| |
| traceParams, traceID := goldClient.addResult("my_test", "9d0568469d206c1aedf1b71f12f474bc", map[string]string{"gamma": "delta"}, map[string]string{"epsilon": "zeta"}) |
| assert.Equal(t, paramtools.Params{ |
| types.CorpusField: "my_corpus", |
| "name": "my_test", |
| "alpha": "beta", |
| "gamma": "delta", |
| }, traceParams) |
| assert.Equal(t, []jsonio.Result{ |
| { |
| Digest: "9d0568469d206c1aedf1b71f12f474bc", |
| Key: map[string]string{ |
| "gamma": "delta", |
| "name": "my_test", |
| }, |
| Options: map[string]string{ |
| "epsilon": "zeta", |
| "ext": "png", |
| }, |
| }, |
| }, goldClient.resultState.SharedConfig.Results) |
| assert.Equal(t, tiling.TraceIDV2(expectedTraceID), traceID) |
| } |
| |
| func TestAddResult_NoCorpusSpecified_UsesInstanceIdAsCorpus_Success(t *testing.T) { |
| |
| const expectedTraceID = "b8bb20640d45f2fa4f2b52d1acb11abd" |
| _, tb := sql.SerializeMap(map[string]string{"alpha": "beta", "gamma": "delta", "name": "my_test", "source_type": "my_instance"}) |
| require.Equal(t, expectedTraceID, hex.EncodeToString(tb)) |
| |
| goldClient := CloudClient{ |
| resultState: &resultState{ |
| InstanceID: "my_instance", |
| SharedConfig: jsonio.GoldResults{ |
| Key: map[string]string{ |
| "alpha": "beta", |
| // No corpus specified, therefore the instance name is used as the default. |
| }, |
| }, |
| }, |
| } |
| |
| traceParams, traceID := goldClient.addResult("my_test", "9d0568469d206c1aedf1b71f12f474bc", map[string]string{"gamma": "delta"}, map[string]string{"epsilon": "zeta"}) |
| assert.Equal(t, paramtools.Params{ |
| types.CorpusField: "my_instance", |
| "name": "my_test", |
| "alpha": "beta", |
| "gamma": "delta", |
| }, traceParams) |
| assert.Equal(t, []jsonio.Result{ |
| { |
| Digest: "9d0568469d206c1aedf1b71f12f474bc", |
| Key: map[string]string{ |
| "gamma": "delta", |
| "name": "my_test", |
| "source_type": "my_instance", |
| }, |
| Options: map[string]string{ |
| "epsilon": "zeta", |
| "ext": "png", |
| }, |
| }, |
| }, goldClient.resultState.SharedConfig.Results) |
| assert.Equal(t, tiling.TraceIDV2(expectedTraceID), traceID) |
| } |
| |
| // Report an image that does not match any previous digests. |
| // This is effectively a test for "goldctl imgtest add" |
| func TestNewReportNormal(t *testing.T) { |
| |
| wd := t.TempDir() |
| |
| imgData := []byte("some bytes") |
| imgHash := types.Digest("9d0568469d206c1aedf1b71f12f474bc") |
| |
| ctx, httpClient, uploader, _ := makeMocks() |
| defer httpClient.AssertExpectations(t) |
| defer uploader.AssertExpectations(t) |
| |
| hashesResp := httpResponse("none", "200 OK", http.StatusOK) |
| httpClient.On("Get", testutils.AnyContext, "https://testing-gold.skia.org/json/v1/hashes").Return(hashesResp, nil) |
| |
| exp := httpResponse("{}", "200 OK", http.StatusOK) |
| httpClient.On("Get", testutils.AnyContext, "https://testing-gold.skia.org/json/v2/expectations?issue=867&crs=gerrit").Return(exp, nil) |
| |
| groupingsResp := httpResponse(`{"grouping_param_keys_by_corpus": {"testing": ["name", "source_type"]}}`, "200 OK", http.StatusOK) |
| httpClient.On("Get", testutils.AnyContext, "https://testing-gold.skia.org/json/v1/groupings").Return(groupingsResp, nil) |
| |
| expectedUploadPath := string("gs://skia-gold-testing/dm-images-v1/" + imgHash + ".png") |
| uploader.On("UploadBytes", testutils.AnyContext, imgData, testImgPath, expectedUploadPath).Return(nil) |
| |
| // Notice the JSON is not uploaded if we are not in passfail mode - a client |
| // would need to call finalize first. |
| goldClient, err := makeGoldClient(false /*=passFail*/, false /*=uploadOnly*/, wd) |
| assert.NoError(t, err) |
| err = goldClient.SetSharedConfig(ctx, makeTestSharedConfig(), false) |
| assert.NoError(t, err) |
| |
| overrideLoadAndHashImage(goldClient, func(path string) ([]byte, types.Digest, error) { |
| assert.Equal(t, testImgPath, path) |
| return imgData, imgHash, nil |
| }) |
| |
| pass, err := goldClient.Test(ctx, "first-test", testImgPath, "", nil, nil) |
| assert.NoError(t, err) |
| // true is always returned if we are not on passFail mode. |
| assert.True(t, pass) |
| } |
| |
| // Report an image using the supplied digest |
| // This is effectively a test for "goldctl imgtest add --png-digest ... --png-file ..." |
| func TestNewReportDigestAndImageSupplied(t *testing.T) { |
| |
| wd := t.TempDir() |
| |
| imgData := []byte("some bytes") |
| // This is the digest the client has calculated |
| const precalculatedDigest = types.Digest("00000000000000111111111111111222") |
| |
| ctx, httpClient, uploader, _ := makeMocks() |
| defer httpClient.AssertExpectations(t) |
| defer uploader.AssertExpectations(t) |
| |
| hashesResp := httpResponse("none", "200 OK", http.StatusOK) |
| httpClient.On("Get", testutils.AnyContext, "https://testing-gold.skia.org/json/v1/hashes").Return(hashesResp, nil) |
| |
| exp := httpResponse("{}", "200 OK", http.StatusOK) |
| httpClient.On("Get", testutils.AnyContext, "https://testing-gold.skia.org/json/v2/expectations?issue=867&crs=gerrit").Return(exp, nil) |
| |
| groupingsResp := httpResponse(`{"grouping_param_keys_by_corpus": {"testing": ["name", "source_type"]}}`, "200 OK", http.StatusOK) |
| httpClient.On("Get", testutils.AnyContext, "https://testing-gold.skia.org/json/v1/groupings").Return(groupingsResp, nil) |
| |
| expectedUploadPath := string("gs://skia-gold-testing/dm-images-v1/" + precalculatedDigest + ".png") |
| uploader.On("UploadBytes", testutils.AnyContext, imgData, testImgPath, expectedUploadPath).Return(nil) |
| |
| // Notice the JSON is not uploaded if we are not in passfail mode - a client |
| // would need to call finalize first. |
| goldClient, err := makeGoldClient(false /*=passFail*/, false /*=uploadOnly*/, wd) |
| assert.NoError(t, err) |
| err = goldClient.SetSharedConfig(ctx, makeTestSharedConfig(), false) |
| assert.NoError(t, err) |
| |
| // This returns the "wrong" hash, i.e. the one we don't want to use. |
| overrideLoadAndHashImage(goldClient, func(path string) ([]byte, types.Digest, error) { |
| assert.Equal(t, testImgPath, path) |
| return imgData, "ffffffffffffffffffffffffffffffff", nil |
| }) |
| |
| pass, err := goldClient.Test(ctx, "first-test", testImgPath, precalculatedDigest, nil, nil) |
| assert.NoError(t, err) |
| // true is always returned if we are not on passFail mode. |
| assert.True(t, pass) |
| } |
| |
| // Report an image using only the supplied digest (no png to upload). |
| // This is effectively a test for "goldctl imgtest add --png-digest ..." |
| func TestNewReportDigestSupplied(t *testing.T) { |
| |
| wd := t.TempDir() |
| |
| // This is the digest the client has calculated |
| const precalculatedDigest = "00000000000000111111111111111222" |
| |
| ctx, httpClient, uploader, _ := makeMocks() |
| defer httpClient.AssertExpectations(t) |
| defer uploader.AssertExpectations(t) |
| |
| hashesResp := httpResponse(precalculatedDigest, "200 OK", http.StatusOK) |
| httpClient.On("Get", testutils.AnyContext, "https://testing-gold.skia.org/json/v1/hashes").Return(hashesResp, nil) |
| |
| exp := httpResponse("{}", "200 OK", http.StatusOK) |
| httpClient.On("Get", testutils.AnyContext, "https://testing-gold.skia.org/json/v2/expectations?issue=867&crs=gerrit").Return(exp, nil) |
| |
| groupingsResp := httpResponse(`{"grouping_param_keys_by_corpus": {"testing": ["name", "source_type"]}}`, "200 OK", http.StatusOK) |
| httpClient.On("Get", testutils.AnyContext, "https://testing-gold.skia.org/json/v1/groupings").Return(groupingsResp, nil) |
| |
| // Notice the JSON is not uploaded if we are not in passfail mode - a client |
| // would need to call finalize first. |
| goldClient, err := makeGoldClient(false /*=passFail*/, false /*=uploadOnly*/, wd) |
| assert.NoError(t, err) |
| err = goldClient.SetSharedConfig(ctx, makeTestSharedConfig(), false) |
| assert.NoError(t, err) |
| |
| overrideLoadAndHashImage(goldClient, func(path string) ([]byte, types.Digest, error) { |
| assert.Fail(t, "call not expected") |
| return nil, "", errors.New("not expected") |
| }) |
| |
| pass, err := goldClient.Test(ctx, "first-test", "", precalculatedDigest, nil, nil) |
| assert.NoError(t, err) |
| // true is always returned if we are not on passFail mode. |
| assert.True(t, pass) |
| } |
| |
| // TestNewReportNormalBadKeys tests the case when bad keys are passed in, which should not upload |
| // because the jsonio.GoldResults would be invalid. |
| func TestNewReportNormalBadKeys(t *testing.T) { |
| |
| wd := t.TempDir() |
| |
| imgData := []byte("some bytes") |
| imgHash := types.Digest("9d0568469d206c1aedf1b71f12f474bc") |
| |
| ctx, httpClient, _, _ := makeMocks() |
| defer httpClient.AssertExpectations(t) |
| |
| hashesResp := httpResponse("none", "200 OK", http.StatusOK) |
| httpClient.On("Get", testutils.AnyContext, "https://testing-gold.skia.org/json/v1/hashes").Return(hashesResp, nil) |
| |
| exp := httpResponse("{}", "200 OK", http.StatusOK) |
| httpClient.On("Get", testutils.AnyContext, "https://testing-gold.skia.org/json/v2/expectations?issue=867&crs=gerrit").Return(exp, nil) |
| |
| groupingsResp := httpResponse(`{"grouping_param_keys_by_corpus": {"testing": ["name", "source_type"]}}`, "200 OK", http.StatusOK) |
| httpClient.On("Get", testutils.AnyContext, "https://testing-gold.skia.org/json/v1/groupings").Return(groupingsResp, nil) |
| |
| // Notice the JSON is not uploaded if we are not in passfail mode - a client |
| // would need to call finalize first. |
| goldClient, err := makeGoldClient(false /*=passFail*/, false /*=uploadOnly*/, wd) |
| assert.NoError(t, err) |
| err = goldClient.SetSharedConfig(ctx, makeTestSharedConfig(), false) |
| assert.NoError(t, err) |
| |
| overrideLoadAndHashImage(goldClient, func(path string) ([]byte, types.Digest, error) { |
| assert.Equal(t, testImgPath, path) |
| return imgData, imgHash, nil |
| }) |
| |
| _, err = goldClient.Test(ctx, "first-test", testImgPath, "", map[string]string{"empty": ""}, nil) |
| assert.Error(t, err) |
| assert.Contains(t, err.Error(), "invalid test config") |
| } |
| |
| // Test the uploading of JSON after two tests/images have been seen. |
| // This is effectively a test for "goldctl imgtest finalize" |
| func TestFinalizeNormal(t *testing.T) { |
| // This test reads and writes a small amount of data from/to disk |
| |
| wd := t.TempDir() |
| |
| ctx, httpClient, uploader, _ := makeMocks() |
| defer httpClient.AssertExpectations(t) |
| defer uploader.AssertExpectations(t) |
| |
| // handcrafted state that has two tests in it |
| j := resultState{ |
| PerTestPassFail: false, |
| InstanceID: "testing", |
| GoldURL: "https://testing-gold.skia.org", |
| Bucket: "skia-gold-testing", |
| SharedConfig: jsonio.GoldResults{ |
| GitHash: "cadbed23562", |
| TryJobID: "Test-Z80-Debug", |
| Key: map[string]string{ |
| "os": "TestOS", |
| "cpu": "z80", |
| }, |
| Results: []jsonio.Result{ |
| { |
| Key: map[string]string{ |
| "name": "first-test", |
| "source_type": "default", |
| }, |
| Options: map[string]string{ |
| "ext": "png", |
| }, |
| Digest: "9d0568469d206c1aedf1b71f12f474bc", |
| }, |
| { |
| Key: map[string]string{ |
| "name": "second-test", |
| "optional_key": "frobulator", |
| "source_type": "default", |
| }, |
| Options: map[string]string{ |
| "ext": "png", |
| }, |
| Digest: "94ba66590d3da0f9ea3a2f2c43132464", |
| }, |
| }, |
| }, |
| } |
| |
| // no calls to httpclient because expectations and baseline should be |
| // loaded from disk. |
| |
| expectedJSONPath := "skia-gold-testing/dm-json-v1/2019/04/02/19/cadbed23562/Test-Z80-Debug/dm-1554234843000000000.json" |
| grm := mock.MatchedBy(func(gr jsonio.GoldResults) bool { |
| assertdeep.Equal(t, j.SharedConfig, gr) |
| return true |
| }) |
| uploader.On("UploadJSON", testutils.AnyContext, grm, filepath.Join(wd, jsonTempFile), expectedJSONPath).Return(nil) |
| |
| jsonToWrite := testutils.MarshalJSON(t, &j) |
| testutils.WriteFile(t, filepath.Join(wd, stateFile), jsonToWrite) |
| |
| goldClient, err := LoadCloudClient(wd) |
| assert.NoError(t, err) |
| |
| // We don't need to call SetSharedConfig because the state should be |
| // loaded from disk |
| |
| err = goldClient.Finalize(ctx) |
| assert.NoError(t, err) |
| } |
| |
| // "End to End" test of the non-pass-fail mode |
| // We init the setup, write a test, re-load the client from disk, write a test, re-load |
| // the client from disk and then finalize. |
| // |
| // This is essentially a test for |
| // |
| // goldctl imgtest init |
| // goldctl imgtest add |
| // goldctl imgtest add |
| // goldctl imgtest finalize |
| func TestInitAddFinalize(t *testing.T) { |
| // We read and write to disk a little |
| |
| wd := t.TempDir() |
| |
| imgData := []byte("some bytes") |
| const firstHash = types.Digest("9d0568469d206c1aedf1b71f12f474bc") |
| const secondHash = types.Digest("29d0568469d206c1aedf1b71f12f474b") |
| |
| ctx, httpClient, uploader, _ := makeMocks() |
| defer httpClient.AssertExpectations(t) |
| defer uploader.AssertExpectations(t) |
| |
| httpClient.On("Get", testutils.AnyContext, "https://testing-gold.skia.org/json/v1/groupings").Return(func(_ context.Context, url string) *http.Response { |
| // Return a fresh response every time the RPC is called. |
| return httpResponse(`{"grouping_param_keys_by_corpus": {"testing": ["name", "source_type"]}}`, "200 OK", http.StatusOK) |
| }, nil).Twice() |
| |
| expectedUploadPath := string("gs://skia-gold-testing/dm-images-v1/" + firstHash + ".png") |
| uploader.On("UploadBytes", testutils.AnyContext, imgData, testImgPath, expectedUploadPath).Return(nil).Once() |
| expectedUploadPath = string("gs://skia-gold-testing/dm-images-v1/" + secondHash + ".png") |
| uploader.On("UploadBytes", testutils.AnyContext, imgData, testImgPath, expectedUploadPath).Return(nil).Once() |
| |
| // Notice the JSON is not uploaded if we are not in passfail mode - a client |
| // would need to call finalize first. |
| goldClient, err := makeGoldClient(false /*=passFail*/, true /*=uploadOnly*/, wd) |
| assert.NoError(t, err) |
| err = goldClient.SetSharedConfig(ctx, makeTestSharedConfig(), false) |
| assert.NoError(t, err) |
| |
| overrideLoadAndHashImage(goldClient, func(path string) ([]byte, types.Digest, error) { |
| assert.Equal(t, testImgPath, path) |
| return imgData, firstHash, nil |
| }) |
| |
| pass, err := goldClient.Test(ctx, "first-test", testImgPath, "", map[string]string{ |
| "config": "canvas", |
| }, map[string]string{ |
| "alpha_type": "Premul", |
| }) |
| assert.NoError(t, err) |
| // true is always returned if we are not on passFail mode. |
| assert.True(t, pass) |
| |
| // Check that the goldClient's in-memory representation is good |
| results := goldClient.resultState.SharedConfig.Results |
| assert.Len(t, results, 1) |
| r := results[0] |
| assert.Equal(t, "first-test", r.Key["name"]) |
| assert.Equal(t, "canvas", r.Key["config"]) |
| assert.Equal(t, "testing", r.Key[types.CorpusField]) |
| assert.Equal(t, "Premul", r.Options["alpha_type"]) |
| assert.Equal(t, firstHash, r.Digest) |
| |
| // Now read the state from disk to make sure results are still there |
| goldClient, err = LoadCloudClient(wd) |
| assert.NoError(t, err) |
| |
| results = goldClient.resultState.SharedConfig.Results |
| assert.Len(t, results, 1) |
| r = results[0] |
| assert.Equal(t, "first-test", r.Key["name"]) |
| assert.Equal(t, firstHash, r.Digest) |
| |
| // Add a second test with the same hash |
| overrideLoadAndHashImage(goldClient, func(path string) ([]byte, types.Digest, error) { |
| assert.Equal(t, testImgPath, path) |
| return imgData, secondHash, nil |
| }) |
| pass, err = goldClient.Test(ctx, "second-test", testImgPath, "", map[string]string{ |
| "config": "svg", |
| }, nil) |
| assert.NoError(t, err) |
| // true is always returned if we are not on passFail mode. |
| assert.True(t, pass) |
| |
| // Now read the state again from disk to make sure results are still there |
| goldClient, err = LoadCloudClient(wd) |
| assert.NoError(t, err) |
| |
| expectedJSONPath := "skia-gold-testing/trybot/dm-json-v1/2019/04/02/19/867__5309/117/dm-1554234843000000000.json" |
| grm := mock.MatchedBy(func(gr jsonio.GoldResults) bool { |
| assert.Len(t, gr.Results, 2) |
| r := gr.Results[0] |
| assert.Equal(t, "first-test", r.Key["name"]) |
| assert.Equal(t, firstHash, r.Digest) |
| assert.Equal(t, "canvas", r.Key["config"]) |
| assert.Equal(t, "testing", r.Key[types.CorpusField]) |
| assert.Equal(t, "Premul", r.Options["alpha_type"]) |
| r = gr.Results[1] |
| assert.Equal(t, "second-test", r.Key["name"]) |
| assert.Equal(t, secondHash, r.Digest) |
| assert.Equal(t, "svg", r.Key["config"]) |
| assert.Equal(t, "testing", r.Key[types.CorpusField]) |
| return true |
| }) |
| uploader.On("UploadJSON", testutils.AnyContext, grm, filepath.Join(wd, jsonTempFile), expectedJSONPath).Return(nil) |
| |
| err = goldClient.Finalize(ctx) |
| assert.NoError(t, err) |
| } |
| |
| // TestNewReportPassFail ensures that a brand new test/digest returns false in pass-fail mode. |
| func TestNewReportPassFail(t *testing.T) { |
| |
| wd := t.TempDir() |
| |
| imgData := []byte("some bytes") |
| imgHash := types.Digest("9d0568469d206c1aedf1b71f12f474bc") |
| testName := types.TestName("TestNotSeenBefore") |
| |
| ctx, httpClient, uploader, _ := makeMocks() |
| defer httpClient.AssertExpectations(t) |
| defer uploader.AssertExpectations(t) |
| |
| hashesResp := httpResponse("none", "200 OK", http.StatusOK) |
| httpClient.On("Get", testutils.AnyContext, "https://testing-gold.skia.org/json/v1/hashes").Return(hashesResp, nil) |
| |
| exp := httpResponse("{}", "200 OK", http.StatusOK) |
| httpClient.On("Get", testutils.AnyContext, "https://testing-gold.skia.org/json/v2/expectations?issue=867&crs=gerrit").Return(exp, nil) |
| |
| groupingsResp := httpResponse(`{"grouping_param_keys_by_corpus": {"testing": ["name", "source_type"]}}`, "200 OK", http.StatusOK) |
| httpClient.On("Get", testutils.AnyContext, "https://testing-gold.skia.org/json/v1/groupings").Return(groupingsResp, nil) |
| |
| expectedUploadPath := string("gs://skia-gold-testing/dm-images-v1/" + imgHash + ".png") |
| uploader.On("UploadBytes", testutils.AnyContext, imgData, testImgPath, expectedUploadPath).Return(nil) |
| |
| expectedJSONPath := "skia-gold-testing/trybot/dm-json-v1/2019/04/02/19/867__5309/117/dm-1554234843000000000.json" |
| checkResults := func(g jsonio.GoldResults) bool { |
| // spot check some of the properties |
| assert.Equal(t, "abcd1234", g.GitHash) |
| assert.Equal(t, testBuildBucketID, g.TryJobID) |
| assert.Equal(t, map[string]string{ |
| "os": "WinTest", |
| "gpu": "GPUTest", |
| }, g.Key) |
| |
| results := g.Results |
| assert.Len(t, results, 1) |
| r := results[0] |
| assert.Equal(t, jsonio.Result{ |
| Digest: imgHash, |
| Options: map[string]string{ |
| "ext": "png", |
| }, |
| Key: map[string]string{ |
| "name": string(testName), |
| // Since we did not specify a source_type it defaults to the instance name, which is |
| // "testing" |
| "source_type": "testing", |
| }, |
| }, r) |
| return true |
| } |
| |
| uploader.On("UploadJSON", testutils.AnyContext, mock.MatchedBy(checkResults), filepath.Join(wd, jsonTempFile), expectedJSONPath).Return(nil) |
| |
| goldClient, err := makeGoldClient(true /*=passFail*/, false /*=uploadOnly*/, wd) |
| assert.NoError(t, err) |
| err = goldClient.SetSharedConfig(ctx, makeTestSharedConfig(), false) |
| assert.NoError(t, err) |
| |
| overrideLoadAndHashImage(goldClient, func(path string) ([]byte, types.Digest, error) { |
| assert.Equal(t, testImgPath, path) |
| return imgData, imgHash, nil |
| }) |
| |
| pass, err := goldClient.Test(ctx, testName, testImgPath, "", nil, nil) |
| assert.NoError(t, err) |
| // Returns false because the test name has never been seen before |
| // (and the digest is brand new) |
| assert.False(t, pass) |
| |
| b, err := os.ReadFile(filepath.Join(wd, failureLog)) |
| assert.NoError(t, err) |
| assert.Equal(t, "https://testing-gold.skia.org/detail?grouping=name%3DTestNotSeenBefore%26source_type%3Dtesting&digest=9d0568469d206c1aedf1b71f12f474bc&changelist_id=867&crs=gerrit\n", string(b)) |
| } |
| |
| // TestReportPassFailPassWithCorpus test that when we set the corpus via the initial config |
| // // it properly gets overridden. |
| func TestReportPassFailPassWithCorpusInInit(t *testing.T) { |
| |
| wd := t.TempDir() |
| |
| imgData := []byte("some bytes") |
| // These are defined in mockBaselineJSON |
| const imgHash = "beef00d3a1527db19619ec12a4e0df68" |
| const testName = types.TestName("ThisIsTheOnlyTest") |
| |
| const overRiddenCorpus = "gtest-pixeltests" |
| |
| ctx, httpClient, uploader, _ := makeMocks() |
| defer httpClient.AssertExpectations(t) |
| defer uploader.AssertExpectations(t) |
| |
| hashesResp := httpResponse(imgHash, "200 OK", http.StatusOK) |
| httpClient.On("Get", testutils.AnyContext, "https://testing-gold.skia.org/json/v1/hashes").Return(hashesResp, nil) |
| |
| exp := httpResponse(mockBaselineJSON, "200 OK", http.StatusOK) |
| httpClient.On("Get", testutils.AnyContext, "https://testing-gold.skia.org/json/v2/expectations?issue=867&crs=gerrit").Return(exp, nil) |
| |
| groupingsResp := httpResponse(`{"grouping_param_keys_by_corpus": {"gtest-pixeltests": ["name", "source_type"]}}`, "200 OK", http.StatusOK) |
| httpClient.On("Get", testutils.AnyContext, "https://testing-gold.skia.org/json/v1/groupings").Return(groupingsResp, nil) |
| |
| expectedJSONPath := "skia-gold-testing/trybot/dm-json-v1/2019/04/02/19/867__5309/117/dm-1554234843000000000.json" |
| checkResults := func(g jsonio.GoldResults) bool { |
| // spot check some of the properties |
| assert.Equal(t, "abcd1234", g.GitHash) |
| assert.Equal(t, testBuildBucketID, g.TryJobID) |
| assert.Equal(t, map[string]string{ |
| "os": "WinTest", |
| "gpu": "GPUTest", |
| "source_type": overRiddenCorpus, |
| }, g.Key) |
| |
| results := g.Results |
| assert.Len(t, results, 1) |
| r := results[0] |
| assert.Equal(t, jsonio.Result{ |
| Digest: imgHash, |
| Options: map[string]string{ |
| "ext": "png", |
| }, |
| Key: map[string]string{ |
| "name": string(testName), |
| "another_notch": "emeril", |
| }, |
| }, r) |
| return true |
| } |
| |
| uploader.On("UploadJSON", testutils.AnyContext, mock.MatchedBy(checkResults), filepath.Join(wd, jsonTempFile), expectedJSONPath).Return(nil) |
| |
| goldClient, err := makeGoldClient(true /*=passFail*/, false /*=uploadOnly*/, wd) |
| assert.NoError(t, err) |
| config := makeTestSharedConfig() |
| config.Key[types.CorpusField] = overRiddenCorpus |
| err = goldClient.SetSharedConfig(ctx, config, false) |
| assert.NoError(t, err) |
| |
| overrideLoadAndHashImage(goldClient, func(path string) ([]byte, types.Digest, error) { |
| assert.Equal(t, testImgPath, path) |
| return imgData, imgHash, nil |
| }) |
| |
| extraKeys := map[string]string{ |
| "another_notch": "emeril", |
| } |
| |
| pass, err := goldClient.Test(ctx, testName, testImgPath, "", extraKeys, nil) |
| assert.NoError(t, err) |
| // Returns true because the test has been seen before and marked positive. |
| assert.True(t, pass) |
| } |
| |
| // TestReportPassFailPassWithCorpusInKeys test that when we set the corpus via additional keys, |
| // it properly gets overridden. |
| func TestReportPassFailPassWithCorpusInKeys(t *testing.T) { |
| |
| wd := t.TempDir() |
| |
| imgData := []byte("some bytes") |
| // These are defined in mockBaselineJSON |
| const imgHash = "beef00d3a1527db19619ec12a4e0df68" |
| const testName = types.TestName("ThisIsTheOnlyTest") |
| |
| const overRiddenCorpus = "gtest-pixeltests" |
| |
| ctx, httpClient, uploader, _ := makeMocks() |
| |
| defer httpClient.AssertExpectations(t) |
| defer uploader.AssertExpectations(t) |
| |
| hashesResp := httpResponse(imgHash, "200 OK", http.StatusOK) |
| httpClient.On("Get", testutils.AnyContext, "https://testing-gold.skia.org/json/v1/hashes").Return(hashesResp, nil) |
| |
| exp := httpResponse(mockBaselineJSON, "200 OK", http.StatusOK) |
| httpClient.On("Get", testutils.AnyContext, "https://testing-gold.skia.org/json/v2/expectations?issue=867&crs=gerrit").Return(exp, nil) |
| |
| groupingsResp := httpResponse(`{"grouping_param_keys_by_corpus": {"gtest-pixeltests": ["name", "source_type"]}}`, "200 OK", http.StatusOK) |
| httpClient.On("Get", testutils.AnyContext, "https://testing-gold.skia.org/json/v1/groupings").Return(groupingsResp, nil) |
| |
| expectedJSONPath := "skia-gold-testing/trybot/dm-json-v1/2019/04/02/19/867__5309/117/dm-1554234843000000000.json" |
| checkResults := func(g jsonio.GoldResults) bool { |
| // spot check some of the properties |
| assert.Equal(t, "abcd1234", g.GitHash) |
| assert.Equal(t, testBuildBucketID, g.TryJobID) |
| assert.Equal(t, map[string]string{ |
| "os": "WinTest", |
| "gpu": "GPUTest", |
| }, g.Key) |
| |
| results := g.Results |
| assert.Len(t, results, 1) |
| r := results[0] |
| assert.Equal(t, jsonio.Result{ |
| Digest: imgHash, |
| Options: map[string]string{ |
| "ext": "png", |
| }, |
| Key: map[string]string{ |
| "name": string(testName), |
| "source_type": overRiddenCorpus, |
| "another_notch": "emeril", |
| }, |
| }, r) |
| return true |
| } |
| |
| uploader.On("UploadJSON", testutils.AnyContext, mock.MatchedBy(checkResults), filepath.Join(wd, jsonTempFile), expectedJSONPath).Return(nil) |
| |
| goldClient, err := makeGoldClient(true /*=passFail*/, false /*=uploadOnly*/, wd) |
| assert.NoError(t, err) |
| err = goldClient.SetSharedConfig(ctx, makeTestSharedConfig(), false) |
| assert.NoError(t, err) |
| |
| overrideLoadAndHashImage(goldClient, func(path string) ([]byte, types.Digest, error) { |
| assert.Equal(t, testImgPath, path) |
| return imgData, imgHash, nil |
| }) |
| |
| extraKeys := map[string]string{ |
| "source_type": overRiddenCorpus, |
| "another_notch": "emeril", |
| } |
| |
| pass, err := goldClient.Test(ctx, testName, testImgPath, "", extraKeys, nil) |
| assert.NoError(t, err) |
| // Returns true because the test has been seen before and marked positive. |
| assert.True(t, pass) |
| } |
| |
| // TestReportPassFailPassWithFuzzyMatching tests that a non-exact image matching algorithm is used |
| // when one is specified via the optional keys. Specifically, the user adds an image with digest |
| // "111...", which is deemed to be an approximate match to the latest positive digest "222...". |
| // Because it is deemed to be a match, we expect to see an RPC to triage "111..." as positive. |
| func TestReportPassFailPassWithFuzzyMatching(t *testing.T) { |
| |
| wd := t.TempDir() |
| |
| // The test name is defined in mockBaselineJSON. The image hashes below are not. |
| testName := types.TestName("ThisIsTheOnlyTest") |
| |
| latestPositiveImageBytes := imageToPngBytes(t, text.MustToNRGBA(`! SKTEXTSIMPLE |
| 2 2 |
| 0x00000000 0x00000000 |
| 0x00000000 0x00000000`)) |
| const latestPositiveImageHash = types.Digest("22222222222222222222222222222222") |
| |
| // New image differs from the latest positive image by one pixel, but the difference is below the |
| // fuzzy matching thresholds. |
| newImageBytes := imageToPngBytes(t, text.MustToNRGBA(`! SKTEXTSIMPLE |
| 2 2 |
| 0xFFFFFFFF 0x00000000 |
| 0x00000000 0x00000000`)) |
| const newImageHash = "11111111111111111111111111111111" |
| |
| // Fuzzy matching with big thresholds to ensure the images above are always deemed equivalent. |
| const imageMatchingAlgorithm = imgmatching.FuzzyMatching |
| const maxDifferentPixels = "99999999" |
| const pixelDeltaThreshold = "1020" |
| |
| overRiddenCorpus := "gtest-pixeltests" |
| |
| ctx, httpClient, uploader, downloader := makeMocks() |
| defer httpClient.AssertExpectations(t) |
| defer uploader.AssertExpectations(t) |
| defer downloader.AssertExpectations(t) |
| |
| // Mock out getting the list of known hashes. |
| hashesResp := httpResponse(newImageHash, "200 OK", http.StatusOK) |
| httpClient.On("Get", testutils.AnyContext, "https://testing-gold.skia.org/json/v1/hashes").Return(hashesResp, nil) |
| |
| // Mock out getting the test baselines. |
| exp := httpResponse(mockBaselineJSON, "200 OK", http.StatusOK) |
| httpClient.On("Get", testutils.AnyContext, "https://testing-gold.skia.org/json/v2/expectations?issue=867&crs=gerrit").Return(exp, nil) |
| |
| groupingsResp := httpResponse(`{"grouping_param_keys_by_corpus": {"gtest-pixeltests": ["name", "source_type"]}}`, "200 OK", http.StatusOK) |
| httpClient.On("Get", testutils.AnyContext, "https://testing-gold.skia.org/json/v1/groupings").Return(groupingsResp, nil) |
| |
| // Mock out retrieving the latest positive image hash for ThisIsTheOnlyTest. |
| // This hash comes from an MD5 hash of the key/values in the trace |
| const latestPositiveDigestRpcUrl = "https://testing-gold.skia.org/json/v2/latestpositivedigest/84c1168e85de827b0b958c8994485e83" |
| const latestPositiveDigestResponse = `{"digest":"` + string(latestPositiveImageHash) + `"}` |
| httpClient.On("Get", testutils.AnyContext, latestPositiveDigestRpcUrl).Return(httpResponse(latestPositiveDigestResponse, "200 OK", http.StatusOK), nil) |
| |
| // Mock out downloading the latest positive digest returned by the previous mocked RPC. |
| downloader.On("DownloadImage", testutils.AnyContext, "https://testing-gold.skia.org", latestPositiveImageHash).Return(latestPositiveImageBytes, nil) |
| |
| // Mock out RPC to automatically triage the new image as positive. |
| bodyMatcher := mock.MatchedBy(func(r io.Reader) bool { |
| b, err := io.ReadAll(r) |
| assert.NoError(t, err) |
| if len(b) == 0 { |
| // This matcher can get called a second time during AssertExpectations. This check makes sure |
| // we don't erroniously fail. |
| return false |
| } |
| tr := frontend.TriageRequestV2{} |
| assert.NoError(t, json.Unmarshal(b, &tr)) |
| assert.Equal(t, frontend.TriageRequestV2{ |
| TestDigestStatus: map[types.TestName]map[types.Digest]expectations.Label{ |
| "ThisIsTheOnlyTest": { |
| newImageHash: expectations.Positive, |
| }, |
| }, |
| ChangelistID: "867", |
| CodeReviewSystem: "gerrit", |
| ImageMatchingAlgorithm: "fuzzy", |
| }, tr) |
| return true |
| }) |
| httpClient.On("Post", testutils.AnyContext, "https://testing-gold.skia.org/json/v2/triage", "application/json", bodyMatcher).Return(httpResponse("", "200 OK", http.StatusOK), nil) |
| |
| // Mock out uploading the JSON file with the test results to Gold. |
| expectedJSONPath := "skia-gold-testing/trybot/dm-json-v1/2019/04/02/19/867__5309/117/dm-1554234843000000000.json" |
| checkResults := func(g jsonio.GoldResults) bool { |
| // spot check some of the properties |
| assert.Equal(t, "abcd1234", g.GitHash) |
| assert.Equal(t, testBuildBucketID, g.TryJobID) |
| assert.Equal(t, map[string]string{ |
| "os": "WinTest", |
| "gpu": "GPUTest", |
| "source_type": overRiddenCorpus, |
| }, g.Key) |
| |
| results := g.Results |
| assert.Len(t, results, 1) |
| r := results[0] |
| assert.Equal(t, jsonio.Result{ |
| Digest: newImageHash, |
| Options: map[string]string{ |
| "ext": "png", |
| imgmatching.AlgorithmNameOptKey: string(imageMatchingAlgorithm), |
| string(imgmatching.MaxDifferentPixels): maxDifferentPixels, |
| string(imgmatching.PixelDeltaThreshold): pixelDeltaThreshold, |
| }, |
| Key: map[string]string{ |
| "name": string(testName), |
| "another_notch": "emeril", |
| }, |
| }, r) |
| return true |
| } |
| uploader.On("UploadJSON", testutils.AnyContext, mock.MatchedBy(checkResults), filepath.Join(wd, jsonTempFile), expectedJSONPath).Return(nil) |
| |
| goldClient, err := makeGoldClient(true /*=passFail*/, false /*=uploadOnly*/, wd) |
| assert.NoError(t, err) |
| config := makeTestSharedConfig() |
| config.Key[types.CorpusField] = overRiddenCorpus |
| err = goldClient.SetSharedConfig(ctx, config, false) |
| assert.NoError(t, err) |
| |
| overrideLoadAndHashImage(goldClient, func(path string) ([]byte, types.Digest, error) { |
| assert.Equal(t, testImgPath, path) |
| return newImageBytes, newImageHash, nil |
| }) |
| |
| extraKeys := map[string]string{ |
| "another_notch": "emeril", |
| } |
| |
| optionalKeys := map[string]string{ |
| imgmatching.AlgorithmNameOptKey: string(imageMatchingAlgorithm), |
| string(imgmatching.MaxDifferentPixels): maxDifferentPixels, |
| string(imgmatching.PixelDeltaThreshold): pixelDeltaThreshold, |
| } |
| |
| pass, err := goldClient.Test(ctx, testName, testImgPath, "", extraKeys, optionalKeys) |
| assert.NoError(t, err) |
| // Returns true because the test has been seen before and marked positive. |
| assert.True(t, pass) |
| } |
| |
| // TestNegativePassFail ensures that a digest marked negative returns false in pass-fail mode. |
| func TestNegativePassFail(t *testing.T) { |
| |
| wd := t.TempDir() |
| |
| imgData := []byte("some bytes") |
| // These are defined in mockBaselineJSON |
| const imgHash = "badbadbad1325855590527db196112e0" |
| const testName = types.TestName("ThisIsTheOnlyTest") |
| |
| ctx, httpClient, uploader, _ := makeMocks() |
| defer httpClient.AssertExpectations(t) |
| defer uploader.AssertExpectations(t) |
| |
| hashesResp := httpResponse(imgHash, "200 OK", http.StatusOK) |
| httpClient.On("Get", testutils.AnyContext, "https://testing-gold.skia.org/json/v1/hashes").Return(hashesResp, nil) |
| |
| exp := httpResponse(mockBaselineJSON, "200 OK", http.StatusOK) |
| httpClient.On("Get", testutils.AnyContext, "https://testing-gold.skia.org/json/v2/expectations?issue=867&crs=gerrit").Return(exp, nil) |
| |
| groupingsResp := httpResponse(`{"grouping_param_keys_by_corpus": {"testing": ["name", "source_type"]}}`, "200 OK", http.StatusOK) |
| httpClient.On("Get", testutils.AnyContext, "https://testing-gold.skia.org/json/v1/groupings").Return(groupingsResp, nil) |
| |
| // No upload expected because the bytes were already seen in json/hashes. |
| |
| expectedJSONPath := "skia-gold-testing/trybot/dm-json-v1/2019/04/02/19/867__5309/117/dm-1554234843000000000.json" |
| uploader.On("UploadJSON", testutils.AnyContext, mock.AnythingOfType("jsonio.GoldResults"), filepath.Join(wd, jsonTempFile), expectedJSONPath).Return(nil) |
| |
| goldClient, err := makeGoldClient(true /*=passFail*/, false /*=uploadOnly*/, wd) |
| assert.NoError(t, err) |
| err = goldClient.SetSharedConfig(ctx, makeTestSharedConfig(), false) |
| assert.NoError(t, err) |
| |
| overrideLoadAndHashImage(goldClient, func(path string) ([]byte, types.Digest, error) { |
| assert.Equal(t, testImgPath, path) |
| return imgData, imgHash, nil |
| }) |
| |
| pass, err := goldClient.Test(ctx, testName, testImgPath, "", nil, nil) |
| assert.NoError(t, err) |
| // Returns false because the test is negative |
| assert.False(t, pass) |
| |
| // Run it again to make sure the failure log isn't truncated |
| pass, err = goldClient.Test(ctx, testName, testImgPath, "", nil, nil) |
| assert.NoError(t, err) |
| // Returns false because the test is negative |
| assert.False(t, pass) |
| |
| b, err := os.ReadFile(filepath.Join(wd, failureLog)) |
| assert.NoError(t, err) |
| assert.Equal(t, `https://testing-gold.skia.org/detail?grouping=name%3DThisIsTheOnlyTest%26source_type%3Dtesting&digest=badbadbad1325855590527db196112e0&changelist_id=867&crs=gerrit |
| https://testing-gold.skia.org/detail?grouping=name%3DThisIsTheOnlyTest%26source_type%3Dtesting&digest=badbadbad1325855590527db196112e0&changelist_id=867&crs=gerrit |
| `, string(b)) |
| } |
| |
| // TestPositivePassFail ensures that a positively marked digest returns true in pass-fail mode. |
| func TestPositivePassFail(t *testing.T) { |
| |
| wd := t.TempDir() |
| |
| imgData := []byte("some bytes") |
| // These are defined in mockBaselineJSON |
| const imgHash = "beef00d3a1527db19619ec12a4e0df68" |
| const testName = types.TestName("ThisIsTheOnlyTest") |
| |
| ctx, httpClient, uploader, _ := makeMocks() |
| defer httpClient.AssertExpectations(t) |
| defer uploader.AssertExpectations(t) |
| |
| hashesResp := httpResponse(imgHash, "200 OK", http.StatusOK) |
| httpClient.On("Get", testutils.AnyContext, "https://testing-gold.skia.org/json/v1/hashes").Return(hashesResp, nil) |
| |
| exp := httpResponse(mockBaselineJSON, "200 OK", http.StatusOK) |
| httpClient.On("Get", testutils.AnyContext, "https://testing-gold.skia.org/json/v2/expectations?issue=867&crs=gerrit").Return(exp, nil) |
| |
| groupingsResp := httpResponse(`{"grouping_param_keys_by_corpus": {"testing": ["name", "source_type"]}}`, "200 OK", http.StatusOK) |
| httpClient.On("Get", testutils.AnyContext, "https://testing-gold.skia.org/json/v1/groupings").Return(groupingsResp, nil) |
| |
| // No upload expected because the bytes were already seen in json/hashes. |
| |
| expectedJSONPath := "skia-gold-testing/trybot/dm-json-v1/2019/04/02/19/867__5309/117/dm-1554234843000000000.json" |
| uploader.On("UploadJSON", testutils.AnyContext, mock.AnythingOfType("jsonio.GoldResults"), filepath.Join(wd, jsonTempFile), expectedJSONPath).Return(nil) |
| |
| goldClient, err := makeGoldClient(true /*=passFail*/, false /*=uploadOnly*/, wd) |
| assert.NoError(t, err) |
| err = goldClient.SetSharedConfig(ctx, makeTestSharedConfig(), false) |
| assert.NoError(t, err) |
| |
| overrideLoadAndHashImage(goldClient, func(path string) ([]byte, types.Digest, error) { |
| assert.Equal(t, testImgPath, path) |
| return imgData, imgHash, nil |
| }) |
| |
| pass, err := goldClient.Test(ctx, testName, testImgPath, "", nil, nil) |
| assert.NoError(t, err) |
| // Returns true because this test has been seen before and the digest was |
| // previously triaged positive. |
| assert.True(t, pass) |
| } |
| |
| // TestCheckSunnyDay emulates running goldctl auth; goldctl imgtest check ... where the |
| // passed in image matches something on the baseline |
| func TestCheckSunnyDay(t *testing.T) { |
| |
| wd := t.TempDir() |
| |
| imgData := []byte("some bytes") |
| // These are defined in mockBaselineJSON |
| const imgHash = "beef00d3a1527db19619ec12a4e0df68" |
| const testName = types.TestName("ThisIsTheOnlyTest") |
| |
| ctx, httpClient, _, _ := makeMocks() |
| defer httpClient.AssertExpectations(t) |
| |
| hashesResp := httpResponse(imgHash, "200 OK", http.StatusOK) |
| httpClient.On("Get", testutils.AnyContext, "https://testing-gold.skia.org/json/v1/hashes").Return(hashesResp, nil) |
| |
| exp := httpResponse(mockBaselineJSON, "200 OK", http.StatusOK) |
| httpClient.On("Get", testutils.AnyContext, "https://testing-gold.skia.org/json/v2/expectations").Return(exp, nil) |
| |
| config := GoldClientConfig{ |
| WorkDir: wd, |
| InstanceID: "testing", |
| } |
| goldClient, err := NewCloudClient(config) |
| assert.NoError(t, err) |
| |
| overrideLoadAndHashImage(goldClient, func(path string) ([]byte, types.Digest, error) { |
| assert.Equal(t, testImgPath, path) |
| return imgData, imgHash, nil |
| }) |
| |
| pass, err := goldClient.Check(ctx, testName, testImgPath, nil, nil) |
| assert.NoError(t, err) |
| assert.True(t, pass) |
| |
| baselineBytes, err := os.ReadFile(goldClient.getResultStatePath()) |
| assert.NoError(t, err) |
| // spot check that the expectations were written to disk |
| assert.Contains(t, string(baselineBytes), imgHash) |
| assert.Contains(t, string(baselineBytes), "badbadbad1325855590527db196112e0") |
| } |
| |
| // TestCheckIssue emulates running goldctl auth; goldctl imgtest check ... where the |
| // passed in image matches something on the baseline for a changelist. |
| func TestCheckIssue(t *testing.T) { |
| |
| wd := t.TempDir() |
| |
| imgData := []byte("some bytes") |
| // These are defined in mockBaselineJSON |
| const imgHash = "beef00d3a1527db19619ec12a4e0df68" |
| const testName = types.TestName("ThisIsTheOnlyTest") |
| const githubCRS = "github" |
| const changelistID = "abc" |
| |
| ctx, httpClient, _, _ := makeMocks() |
| defer httpClient.AssertExpectations(t) |
| |
| hashesResp := httpResponse(imgHash, "200 OK", http.StatusOK) |
| httpClient.On("Get", testutils.AnyContext, "https://testing-gold.skia.org/json/v1/hashes").Return(hashesResp, nil) |
| |
| exp := httpResponse(mockBaselineJSON, "200 OK", http.StatusOK) |
| httpClient.On("Get", testutils.AnyContext, "https://testing-gold.skia.org/json/v2/expectations?issue=abc&crs=github").Return(exp, nil) |
| |
| config := GoldClientConfig{ |
| WorkDir: wd, |
| InstanceID: "testing", |
| } |
| goldClient, err := NewCloudClient(config) |
| assert.NoError(t, err) |
| |
| gr := jsonio.GoldResults{ |
| CodeReviewSystem: githubCRS, |
| ChangelistID: changelistID, |
| GitHash: "HEAD", |
| } |
| err = goldClient.SetSharedConfig(ctx, gr, true) |
| assert.NoError(t, err) |
| |
| overrideLoadAndHashImage(goldClient, func(path string) ([]byte, types.Digest, error) { |
| assert.Equal(t, testImgPath, path) |
| return imgData, imgHash, nil |
| }) |
| |
| pass, err := goldClient.Check(ctx, testName, testImgPath, nil, nil) |
| assert.NoError(t, err) |
| assert.True(t, pass) |
| |
| baselineBytes, err := os.ReadFile(goldClient.getResultStatePath()) |
| assert.NoError(t, err) |
| // spot check that the expectations were written to disk |
| assert.Contains(t, string(baselineBytes), imgHash) |
| assert.Contains(t, string(baselineBytes), "badbadbad1325855590527db196112e0") |
| } |
| |
| // TestCheckSunnyDayNegative emulates running goldctl auth; goldctl imgtest check ... where the |
| // passed in image does not match something on the baseline. |
| func TestCheckSunnyDayNegative(t *testing.T) { |
| |
| wd := t.TempDir() |
| |
| imgData := []byte("some bytes") |
| // imgHash is not seen in expectations |
| const imgHash = "4043142d1ec36177e8c6c4d31af0c6de" |
| const testName = types.TestName("ThisIsTheOnlyTest") |
| |
| ctx, httpClient, _, _ := makeMocks() |
| defer httpClient.AssertExpectations(t) |
| |
| hashesResp := httpResponse(imgHash, "200 OK", http.StatusOK) |
| httpClient.On("Get", testutils.AnyContext, "https://testing-gold.skia.org/json/v1/hashes").Return(hashesResp, nil) |
| |
| exp := httpResponse(mockBaselineJSON, "200 OK", http.StatusOK) |
| httpClient.On("Get", testutils.AnyContext, "https://testing-gold.skia.org/json/v2/expectations").Return(exp, nil) |
| |
| config := GoldClientConfig{ |
| WorkDir: wd, |
| InstanceID: "testing", |
| } |
| goldClient, err := NewCloudClient(config) |
| assert.NoError(t, err) |
| |
| overrideLoadAndHashImage(goldClient, func(path string) ([]byte, types.Digest, error) { |
| assert.Equal(t, testImgPath, path) |
| return imgData, imgHash, nil |
| }) |
| |
| pass, err := goldClient.Check(ctx, testName, testImgPath, nil, nil) |
| assert.NoError(t, err) |
| assert.False(t, pass) |
| } |
| |
| // TestCheckLoad emulates running goldctl auth; goldctl imgtest check ...; goldctl imgtest check... |
| // specifically focusing on loading from disk after the first check and not querying the |
| // backend every time. |
| func TestCheckLoad(t *testing.T) { |
| |
| wd := t.TempDir() |
| |
| imgData := []byte("some bytes") |
| // These are defined in mockBaselineJSON |
| const imgHash = "beef00d3a1527db19619ec12a4e0df68" |
| const testName = types.TestName("ThisIsTheOnlyTest") |
| |
| ctx, httpClient, _, _ := makeMocks() |
| defer httpClient.AssertExpectations(t) |
| |
| hashesResp := httpResponse(imgHash, "200 OK", http.StatusOK) |
| httpClient.On("Get", testutils.AnyContext, "https://testing-gold.skia.org/json/v1/hashes").Return(hashesResp, nil).Once() |
| |
| exp := httpResponse(mockBaselineJSON, "200 OK", http.StatusOK) |
| httpClient.On("Get", testutils.AnyContext, "https://testing-gold.skia.org/json/v2/expectations").Return(exp, nil).Once() |
| |
| config := GoldClientConfig{ |
| WorkDir: wd, |
| InstanceID: "testing", |
| } |
| goldClient, err := NewCloudClient(config) |
| assert.NoError(t, err) |
| |
| overrideLoadAndHashImage(goldClient, func(path string) ([]byte, types.Digest, error) { |
| assert.Equal(t, testImgPath, path) |
| return imgData, imgHash, nil |
| }) |
| |
| pass, err := goldClient.Check(ctx, testName, testImgPath, nil, nil) |
| assert.NoError(t, err) |
| assert.True(t, pass) |
| |
| // Reload saved state from disk |
| goldClient, err = LoadCloudClient(wd) |
| assert.NoError(t, err) |
| overrideLoadAndHashImage(goldClient, func(path string) ([]byte, types.Digest, error) { |
| assert.Equal(t, testImgPath, path) |
| return imgData, imgHash, nil |
| }) |
| pass, err = goldClient.Check(ctx, testName, testImgPath, nil, nil) |
| assert.NoError(t, err) |
| assert.True(t, pass) |
| } |
| |
| // TestCheckLoadFails make sure that if we load from an empty directory, we fail to initialize |
| // a GoldClient. |
| func TestCheckLoadFails(t *testing.T) { |
| |
| wd := t.TempDir() |
| |
| // This should not work |
| _, err := LoadCloudClient(wd) |
| assert.Error(t, err) |
| assert.Contains(t, err.Error(), "from disk") |
| } |
| |
| // TestDiffSunnyDay shows a scenario in which the user wants to identify the closest known image |
| // for their given image. It simulates the Gold server having two digests for the given test, |
| // and makes sure that the goldClient downloads those and correctly identifies the closest image. |
| // It asserts that the given output directory has the original image, the closest image and |
| // the computed diff. |
| func TestDiffSunnyDay(t *testing.T) { |
| |
| const corpus = "This Has spaces" |
| const testName = types.TestName("This IsTheOnly Test") |
| // This hash is the real computed hash of the bytes from image1 |
| const leftHash = "f81a3bb94c02596e06e74c84d1076fff" |
| // rightHash is the closest of the two images compared against. It is arbitrary. |
| const rightHash = "bbb0dc56d0429ef3586629787666ce09" |
| // otherHash is a digest that should be compared against, but is not the closest. |
| // It is arbitrary. |
| const otherHash = "ccc2912653148661835084a809fee263" |
| |
| wd := t.TempDir() |
| outDir := filepath.Join(wd, "out") |
| |
| inputPath := filepath.Join(wd, "input.png") |
| input, err := os.Create(inputPath) |
| require.NoError(t, err) |
| require.NoError(t, png.Encode(input, image1)) |
| require.NoError(t, input.Close()) |
| |
| ctx, httpClient, _, dlr := makeMocks() |
| defer httpClient.AssertExpectations(t) |
| defer dlr.AssertExpectations(t) |
| |
| digests := httpResponse(mockDigestsJSON, "200 OK", http.StatusOK) |
| httpClient.On("Get", testutils.AnyContext, "https://testing-gold.skia.org/json/v2/digests?grouping=name%3DThis%2BIsTheOnly%2BTest%26source_type%3DThis%2BHas%2Bspaces").Return(digests, nil) |
| |
| img2 := imageToPngBytes(t, image2) |
| dlr.On("DownloadImage", testutils.AnyContext, "https://testing-gold.skia.org", types.Digest(rightHash)).Return(img2, nil) |
| img3 := imageToPngBytes(t, image3) |
| dlr.On("DownloadImage", testutils.AnyContext, "https://testing-gold.skia.org", types.Digest(otherHash)).Return(img3, nil) |
| |
| config := GoldClientConfig{ |
| WorkDir: wd, |
| InstanceID: "testing", |
| } |
| goldClient, err := NewCloudClient(config) |
| require.NoError(t, err) |
| |
| grouping := paramtools.Params{ |
| types.CorpusField: corpus, |
| types.PrimaryKeyField: string(testName), |
| } |
| err = goldClient.Diff(ctx, grouping, inputPath, outDir) |
| require.NoError(t, err) |
| |
| leftImg, err := openNRGBAFromFile(filepath.Join(outDir, "input-"+leftHash+".png")) |
| require.NoError(t, err) |
| assert.Equal(t, leftImg, image1) |
| |
| rightImg, err := openNRGBAFromFile(filepath.Join(outDir, "closest-"+rightHash+".png")) |
| require.NoError(t, err) |
| assert.Equal(t, rightImg, image2) |
| |
| diffImg, err := openNRGBAFromFile(filepath.Join(outDir, "diff.png")) |
| require.NoError(t, err) |
| assert.Equal(t, diffImg, diff12) |
| } |
| |
| // TestDiffCaching makes sure we cache the images we download from GCS and only download them |
| // once if we try calling Diff multiple times. |
| func TestDiffCaching(t *testing.T) { |
| |
| const corpus = "whatever" |
| const testName = types.TestName("ThisIsTheOnlyTest") |
| const rightHash = types.Digest("bbb0dc56d0429ef3586629787666ce09") |
| const otherHash = types.Digest("ccc2912653148661835084a809fee263") |
| |
| wd := t.TempDir() |
| outDir := filepath.Join(wd, "out") |
| |
| inputPath := filepath.Join(wd, "input.png") |
| input, err := os.Create(inputPath) |
| require.NoError(t, err) |
| require.NoError(t, png.Encode(input, image1)) |
| require.NoError(t, input.Close()) |
| |
| ctx, httpClient, _, dlr := makeMocks() |
| defer httpClient.AssertExpectations(t) |
| defer dlr.AssertExpectations(t) |
| |
| httpClient.On("Get", testutils.AnyContext, "https://testing-gold.skia.org/json/v2/digests?grouping=name%3DThisIsTheOnlyTest%26source_type%3Dwhatever").Return(func(_ context.Context, _ string) *http.Response { |
| // return a fresh response each time Diff is called |
| return httpResponse(mockDigestsJSON, "200 OK", http.StatusOK) |
| }, nil).Twice() |
| |
| img2 := imageToPngBytes(t, image2) |
| dlr.On("DownloadImage", testutils.AnyContext, "https://testing-gold.skia.org", rightHash).Return(img2, nil) |
| img3 := imageToPngBytes(t, image3) |
| dlr.On("DownloadImage", testutils.AnyContext, "https://testing-gold.skia.org", otherHash).Return(img3, nil) |
| |
| config := GoldClientConfig{ |
| WorkDir: wd, |
| InstanceID: "testing", |
| } |
| goldClient, err := NewCloudClient(config) |
| require.NoError(t, err) |
| |
| grouping := paramtools.Params{ |
| types.CorpusField: corpus, |
| types.PrimaryKeyField: string(testName), |
| } |
| |
| err = goldClient.Diff(ctx, grouping, inputPath, outDir) |
| require.NoError(t, err) |
| |
| // Call it twice to make sure we only hit GCS once per file |
| err = goldClient.Diff(ctx, grouping, inputPath, outDir) |
| require.NoError(t, err) |
| } |
| |
| func TestMakeResultKeyAndTraceId_Success(t *testing.T) { |
| |
| const instanceId = "my_instance" |
| const testName = types.TestName("my_test") |
| |
| tests := []struct { |
| name string |
| sharedConfig jsonio.GoldResults |
| additionalKeys map[string]string |
| expectedResultKey map[string]string |
| expectedTrace paramtools.Params |
| }{ |
| { |
| name: "no additional keys, nil shared config, corpus set to instance ID", |
| expectedResultKey: map[string]string{ |
| types.PrimaryKeyField: "my_test", |
| types.CorpusField: instanceId, |
| }, |
| expectedTrace: paramtools.Params{"name": "my_test", "source_type": instanceId}, |
| }, |
| { |
| name: "no additional keys, empty shared config, corpus set to instance ID", |
| sharedConfig: jsonio.GoldResults{}, |
| expectedResultKey: map[string]string{ |
| types.PrimaryKeyField: "my_test", |
| types.CorpusField: instanceId, |
| }, |
| expectedTrace: paramtools.Params{"name": "my_test", "source_type": instanceId}, |
| }, |
| { |
| name: "no additional keys, shared config with corpus, uses corpus from shared config", |
| sharedConfig: jsonio.GoldResults{ |
| Key: map[string]string{types.CorpusField: "my_corpus"}, |
| }, |
| expectedResultKey: map[string]string{ |
| types.PrimaryKeyField: "my_test", |
| }, |
| expectedTrace: paramtools.Params{"name": "my_test", "source_type": "my_corpus"}, |
| }, |
| { |
| name: "additional keys with corpus, empty shared config, uses corpus from additional keys", |
| sharedConfig: jsonio.GoldResults{}, |
| additionalKeys: map[string]string{types.CorpusField: "my_corpus"}, |
| expectedResultKey: map[string]string{ |
| types.PrimaryKeyField: "my_test", |
| types.CorpusField: "my_corpus", |
| }, |
| expectedTrace: paramtools.Params{"name": "my_test", "source_type": "my_corpus"}, |
| }, |
| { |
| name: "additional keys with corpus, shared config with corpus, uses corpus from additional keys", |
| sharedConfig: jsonio.GoldResults{ |
| Key: map[string]string{types.CorpusField: "corpus_from_shared_config"}, |
| }, |
| additionalKeys: map[string]string{types.CorpusField: "my_corpus"}, |
| expectedResultKey: map[string]string{ |
| types.PrimaryKeyField: "my_test", |
| types.CorpusField: "my_corpus", |
| }, |
| expectedTrace: paramtools.Params{"name": "my_test", "source_type": "my_corpus"}, |
| }, |
| { |
| name: "overlapping shared and additional keys, additional keys take precedence", |
| sharedConfig: jsonio.GoldResults{ |
| Key: map[string]string{ |
| types.CorpusField: "corpus_from_shared_config", |
| "overlapping_key": "alpha", |
| "shared_key": "foo", |
| }, |
| }, |
| additionalKeys: map[string]string{ |
| types.CorpusField: "my_corpus", |
| "overlapping_key": "beta", |
| "additional_key": "bar", |
| }, |
| expectedResultKey: map[string]string{ |
| types.PrimaryKeyField: "my_test", |
| types.CorpusField: "my_corpus", |
| "overlapping_key": "beta", |
| "additional_key": "bar", |
| }, |
| expectedTrace: paramtools.Params{ |
| types.PrimaryKeyField: "my_test", |
| types.CorpusField: "my_corpus", |
| "shared_key": "foo", |
| "overlapping_key": "beta", |
| "additional_key": "bar", |
| }, |
| }, |
| } |
| |
| for _, tc := range tests { |
| t.Run(tc.name, func(t *testing.T) { |
| goldClient := CloudClient{ |
| resultState: newResultState(tc.sharedConfig, &GoldClientConfig{ |
| InstanceID: instanceId, |
| }), |
| } |
| |
| resultKey, _, traceID := goldClient.makeResultKeyAndTraceParamsAndID(testName, tc.additionalKeys) |
| assert.Equal(t, tc.expectedResultKey, resultKey) |
| _, tb := sql.SerializeMap(tc.expectedTrace) |
| expectedTraceID := tiling.TraceIDV2(hex.EncodeToString(tb)) |
| assert.Equal(t, expectedTraceID, traceID) |
| }) |
| } |
| } |
| |
| func TestCloudClient_MatchImageAgainstBaseline_NoAlgorithmSpecified_DefaultsToExactMatching_Success(t *testing.T) { |
| const testName = types.TestName("my_test") |
| const digest = types.Digest("11111111111111111111111111111111") |
| const unlabeled = expectations.Label("unlabeled") // Sentinel value. |
| |
| test := func(name string, label expectations.Label, want bool) { |
| t.Run(name, func(t *testing.T) { |
| goldClient, ctx, _, _ := makeGoldClientForMatchImageAgainstBaselineTests(t) |
| |
| if label != unlabeled { |
| goldClient.resultState.Expectations = expectations.Baseline{ |
| testName: { |
| digest: label, |
| }, |
| } |
| } |
| |
| // Parameters traceId and imageBytes are not used in exact matching. |
| got, algorithmName, err := goldClient.matchImageAgainstBaseline(ctx, testName, "" /* =traceId */, []byte{} /* =imageBytes */, digest, nil /* =optionalKeys */) |
| |
| assert.NoError(t, err) |
| assert.Equal(t, imgmatching.ExactMatching, algorithmName) |
| assert.Equal(t, want, got) |
| }) |
| } |
| |
| test("image label positive, returns true", expectations.Positive, true) |
| test("image label negative, returns false", expectations.Negative, false) |
| test("image label untriaged, returns false", expectations.Untriaged, false) |
| test("image unlabeled, returns false", unlabeled, false) |
| } |
| |
| func TestCloudClient_MatchImageAgainstBaseline_ExactMatching_Success(t *testing.T) { |
| const testName = types.TestName("my_test") |
| const digest = types.Digest("11111111111111111111111111111111") |
| const unlabeled = expectations.Label("unlabeled") // Sentinel value. |
| |
| test := func(name string, label expectations.Label, want bool) { |
| t.Run(name, func(t *testing.T) { |
| goldClient, ctx, _, _ := makeGoldClientForMatchImageAgainstBaselineTests(t) |
| |
| if label != unlabeled { |
| goldClient.resultState.Expectations = expectations.Baseline{ |
| testName: { |
| digest: label, |
| }, |
| } |
| } |
| |
| optionalKeys := map[string]string{ |
| imgmatching.AlgorithmNameOptKey: string(imgmatching.ExactMatching), |
| } |
| |
| // Parameters traceId and imageBytes are not used in exact matching. |
| got, algorithmName, err := goldClient.matchImageAgainstBaseline(ctx, testName, "" /* =traceId */, []byte{} /* =imageBytes */, digest, optionalKeys) |
| |
| assert.NoError(t, err) |
| assert.Equal(t, imgmatching.ExactMatching, algorithmName) |
| assert.Equal(t, want, got) |
| }) |
| } |
| |
| test("image labeled positive, returns true", expectations.Positive, true) |
| test("image labeled negative, returns false", expectations.Negative, false) |
| test("image labeled untriaged, returns false", expectations.Untriaged, false) |
| test("image unlabeled, returns false", unlabeled, false) |
| } |
| |
| func TestCloudClient_MatchImageAgainstBaseline_FuzzyMatching_ImageAlreadyLabeled_Success(t *testing.T) { |
| test := func(name string, label expectations.Label, want bool) { |
| t.Run(name, func(t *testing.T) { |
| goldClient, ctx, _, _ := makeGoldClientForMatchImageAgainstBaselineTests(t) |
| |
| const testName = types.TestName("my_test") |
| const digest = types.Digest("11111111111111111111111111111111") |
| optionalKeys := map[string]string{ |
| imgmatching.AlgorithmNameOptKey: string(imgmatching.FuzzyMatching), |
| // These optionalKeys do not matter because the algorithm is not exercised by this test. |
| string(imgmatching.MaxDifferentPixels): "0", |
| string(imgmatching.PixelDeltaThreshold): "0", |
| } |
| |
| goldClient.resultState.Expectations = expectations.Baseline{ |
| testName: { |
| digest: label, |
| }, |
| } |
| |
| got, algorithmName, err := goldClient.matchImageAgainstBaseline(ctx, testName, "" /* =traceId */, nil /* =imageBytes */, digest, optionalKeys) |
| assert.NoError(t, err) |
| assert.Equal(t, imgmatching.ExactMatching, algorithmName) |
| assert.Equal(t, want, got) |
| }) |
| } |
| |
| test("labeled positive, returns true", expectations.Positive, true) |
| test("labeled negative, returns false", expectations.Negative, false) |
| } |
| |
| func TestCloudClient_MatchImageAgainstBaseline_FuzzyMatching_UntriagedImage_Success(t *testing.T) { |
| const testName = types.TestName("my_test") |
| const traceId = tiling.TraceIDV2("1234567890abcdef1234567890abcdef") |
| const digest = types.Digest("11111111111111111111111111111111") |
| |
| const latestPositiveDigestRpcUrl = "https://testing-gold.skia.org/json/v2/latestpositivedigest/1234567890abcdef1234567890abcdef" |
| const latestPositiveDigestResponse = `{"digest":"22222222222222222222222222222222"}` |
| const latestPositiveDigest = types.Digest("22222222222222222222222222222222") |
| latestPositiveImageBytes := imageToPngBytes(t, text.MustToNRGBA(`! SKTEXTSIMPLE |
| 2 2 |
| 0x00000000 0x00000000 |
| 0x00000000 0x00000000`)) |
| |
| test := func(name string, imageBytes []byte, expected bool) { |
| t.Run(name, func(t *testing.T) { |
| goldClient, ctx, httpClient, dlr := makeGoldClientForMatchImageAgainstBaselineTests(t) |
| defer httpClient.AssertExpectations(t) |
| defer dlr.AssertExpectations(t) |
| |
| httpClient.On("Get", testutils.AnyContext, latestPositiveDigestRpcUrl).Return(httpResponse(latestPositiveDigestResponse, "200 OK", http.StatusOK), nil) |
| dlr.On("DownloadImage", testutils.AnyContext, "https://testing-gold.skia.org", latestPositiveDigest).Return(latestPositiveImageBytes, nil) |
| |
| optionalKeys := map[string]string{ |
| imgmatching.AlgorithmNameOptKey: string(imgmatching.FuzzyMatching), |
| string(imgmatching.MaxDifferentPixels): "2", |
| string(imgmatching.PixelDeltaThreshold): "10", |
| } |
| |
| actual, algorithmName, err := goldClient.matchImageAgainstBaseline(ctx, testName, traceId, imageBytes, digest, optionalKeys) |
| assert.NoError(t, err) |
| assert.Equal(t, imgmatching.FuzzyMatching, algorithmName) |
| assert.Equal(t, expected, actual) |
| }) |
| } |
| |
| test( |
| "identical images, returns true", |
| imageToPngBytes(t, text.MustToNRGBA(`! SKTEXTSIMPLE |
| 2 2 |
| 0x00000000 0x00000000 |
| 0x00000000 0x00000000`)), |
| true) |
| test( |
| "images different below threshold, returns true", |
| imageToPngBytes(t, text.MustToNRGBA(`! SKTEXTSIMPLE |
| 2 2 |
| 0x00000505 0x00000001 |
| 0x00000000 0x00000000`)), |
| true) |
| test( |
| "images different above threshold, returns false", |
| imageToPngBytes(t, text.MustToNRGBA(`! SKTEXTSIMPLE |
| 2 2 |
| 0x00000506 0x00000001 |
| 0x00000001 0x00000000`)), |
| false) |
| } |
| |
| func TestCloudClient_MatchImageAgainstBaseline_FuzzyMatching_InvalidParameters_ReturnsError(t *testing.T) { |
| test := func(name string, optionalKeys map[string]string, expectedError string) { |
| t.Run(name, func(t *testing.T) { |
| goldClient, ctx, _, _ := makeGoldClientForMatchImageAgainstBaselineTests(t) |
| |
| _, _, err := goldClient.matchImageAgainstBaseline(ctx, "my_test", "" /* =traceId */, nil /* =imageBytes */, "11111111111111111111111111111111", optionalKeys) |
| assert.Error(t, err) |
| assert.Contains(t, err.Error(), expectedError) |
| }) |
| } |
| |
| test( |
| "insufficient parameters: no parameter specified", |
| map[string]string{ |
| imgmatching.AlgorithmNameOptKey: string(imgmatching.FuzzyMatching), |
| }, |
| "required image matching parameter not found") |
| test( |
| "invalid parameters", |
| map[string]string{ |
| imgmatching.AlgorithmNameOptKey: string(imgmatching.FuzzyMatching), |
| string(imgmatching.MaxDifferentPixels): "not a number", |
| string(imgmatching.PixelDeltaThreshold): "not a number", |
| string(imgmatching.PixelPerChannelDeltaThreshold): "not a number", |
| }, |
| "parsing integer value") |
| } |
| |
| func TestCloudClient_MatchImageAgainstBaseline_FuzzyMatching_NoRecentPositiveDigests_ReturnsFalse(t *testing.T) { |
| const testName = types.TestName("my_test") |
| const traceId = tiling.TraceIDV2("1234567890abcdef1234567890abcdef") |
| const digest = types.Digest("11111111111111111111111111111111") |
| imageBytes := imageToPngBytes(t, text.MustToNRGBA(`! SKTEXTSIMPLE |
| 1 1 |
| 0x00000000`)) |
| |
| const latestPositiveDigestRpcUrl = "https://testing-gold.skia.org/json/v2/latestpositivedigest/1234567890abcdef1234567890abcdef" |
| const latestPositiveDigestResponse = `{"digest":""}` |
| |
| goldClient, ctx, httpClient, _ := makeGoldClientForMatchImageAgainstBaselineTests(t) |
| defer httpClient.AssertExpectations(t) |
| |
| httpClient.On("Get", testutils.AnyContext, latestPositiveDigestRpcUrl).Return(httpResponse(latestPositiveDigestResponse, "200 OK", http.StatusOK), nil) |
| |
| optionalKeys := map[string]string{ |
| imgmatching.AlgorithmNameOptKey: string(imgmatching.FuzzyMatching), |
| string(imgmatching.MaxDifferentPixels): "0", |
| string(imgmatching.PixelDeltaThreshold): "0", |
| string(imgmatching.PixelPerChannelDeltaThreshold): "0", |
| } |
| |
| matched, algorithmName, err := goldClient.matchImageAgainstBaseline(ctx, testName, traceId, imageBytes, digest, optionalKeys) |
| assert.NoError(t, err) |
| assert.False(t, matched) |
| assert.Equal(t, imgmatching.FuzzyMatching, algorithmName) |
| } |
| |
| func TestCloudClient_MatchImageAgainstBaseline_PositiveIfOnlyImageMatching_UntriagedImage_Success(t *testing.T) { |
| const testName = types.TestName("my_test") |
| const traceId = tiling.TraceIDV2("1234567890abcdef1234567890abcdef") |
| const digest = types.Digest("11111111111111111111111111111111") |
| |
| const latestPositiveDigestRpcUrl = "https://testing-gold.skia.org/json/v2/latestpositivedigest/1234567890abcdef1234567890abcdef" |
| const latestPositiveDigestResponse = `{"digest":"22222222222222222222222222222222"}` |
| const latestPositiveDigest = types.Digest("22222222222222222222222222222222") |
| latestPositiveImageBytes := imageToPngBytes(t, text.MustToNRGBA(`! SKTEXTSIMPLE |
| 2 2 |
| 0x00000000 0x00000000 |
| 0x00000000 0x00000000`)) |
| |
| test := func(name string, imageBytes []byte, expected bool) { |
| t.Run(name, func(t *testing.T) { |
| goldClient, ctx, httpClient, dlr := makeGoldClientForMatchImageAgainstBaselineTests(t) |
| defer httpClient.AssertExpectations(t) |
| defer dlr.AssertExpectations(t) |
| |
| httpClient.On("Get", testutils.AnyContext, latestPositiveDigestRpcUrl).Return(httpResponse(latestPositiveDigestResponse, "200 OK", http.StatusOK), nil) |
| dlr.On("DownloadImage", testutils.AnyContext, "https://testing-gold.skia.org", latestPositiveDigest).Return(latestPositiveImageBytes, nil) |
| |
| optionalKeys := map[string]string{ |
| imgmatching.AlgorithmNameOptKey: string(imgmatching.PositiveIfOnlyImageMatching), |
| } |
| |
| actual, algorithmName, err := goldClient.matchImageAgainstBaseline(ctx, testName, traceId, imageBytes, digest, optionalKeys) |
| assert.NoError(t, err) |
| assert.Equal(t, imgmatching.PositiveIfOnlyImageMatching, algorithmName) |
| assert.Equal(t, expected, actual) |
| }) |
| } |
| |
| test( |
| "identical images, returns true", |
| imageToPngBytes(t, text.MustToNRGBA(`! SKTEXTSIMPLE |
| 2 2 |
| 0x00000000 0x00000000 |
| 0x00000000 0x00000000`)), |
| true) |
| test( |
| "different images, returns false", |
| imageToPngBytes(t, text.MustToNRGBA(`! SKTEXTSIMPLE |
| 2 2 |
| 0x00000000 0x00000001 |
| 0x00000000 0x00000000`)), |
| false) |
| } |
| |
| func TestCloudClient_MatchImageAgainstBaseline_PositiveIfOnlyImageMatching_NoRecentPositiveDigests_ReturnsTrue(t *testing.T) { |
| const testName = types.TestName("my_test") |
| const traceId = tiling.TraceIDV2("1234567890abcdef1234567890abcdef") |
| const digest = types.Digest("11111111111111111111111111111111") |
| imageBytes := imageToPngBytes(t, text.MustToNRGBA(`! SKTEXTSIMPLE |
| 1 1 |
| 0x00000000`)) |
| |
| const latestPositiveDigestRpcUrl = "https://testing-gold.skia.org/json/v2/latestpositivedigest/1234567890abcdef1234567890abcdef" |
| const latestPositiveDigestResponse = `{"digest":""}` |
| |
| goldClient, ctx, httpClient, _ := makeGoldClientForMatchImageAgainstBaselineTests(t) |
| defer httpClient.AssertExpectations(t) |
| |
| httpClient.On("Get", testutils.AnyContext, latestPositiveDigestRpcUrl).Return(httpResponse(latestPositiveDigestResponse, "200 OK", http.StatusOK), nil) |
| |
| optionalKeys := map[string]string{ |
| imgmatching.AlgorithmNameOptKey: string(imgmatching.PositiveIfOnlyImageMatching), |
| } |
| |
| matched, algorithmName, err := goldClient.matchImageAgainstBaseline(ctx, testName, traceId, imageBytes, digest, optionalKeys) |
| assert.NoError(t, err) |
| assert.True(t, matched) |
| assert.Equal(t, imgmatching.PositiveIfOnlyImageMatching, algorithmName) |
| } |
| |
| func TestCloudClient_MatchImageAgainstBaseline_SampleAreaMatching_ImageAlreadyLabeled_Success(t *testing.T) { |
| test := func(name string, label expectations.Label, want bool) { |
| t.Run(name, func(t *testing.T) { |
| goldClient, ctx, _, _ := makeGoldClientForMatchImageAgainstBaselineTests(t) |
| |
| const testName = types.TestName("my_test") |
| const digest = types.Digest("11111111111111111111111111111111") |
| optionalKeys := map[string]string{ |
| imgmatching.AlgorithmNameOptKey: string(imgmatching.SampleAreaMatching), |
| // These optionalKeys do not matter because the algorithm is not exercised by this test. |
| string(imgmatching.SampleAreaWidth): "0", |
| string(imgmatching.MaxDifferentPixelsPerArea): "0", |
| string(imgmatching.SampleAreaChannelDeltaThreshold): "0", |
| } |
| |
| goldClient.resultState.Expectations = expectations.Baseline{ |
| testName: { |
| digest: label, |
| }, |
| } |
| |
| got, algorithmName, err := goldClient.matchImageAgainstBaseline(ctx, testName, "" /* =traceId */, nil /* =imageBytes */, digest, optionalKeys) |
| assert.NoError(t, err) |
| assert.Equal(t, imgmatching.ExactMatching, algorithmName) |
| assert.Equal(t, want, got) |
| }) |
| } |
| |
| test("labeled positive, returns true", expectations.Positive, true) |
| test("labeled negative, returns false", expectations.Negative, false) |
| } |
| |
| func TestCloudClient_MatchImageAgainstBaseline_SampleAreaMatching_UntriagedImage_Success(t *testing.T) { |
| const testName = types.TestName("my_test") |
| const traceId = tiling.TraceIDV2("1234567890abcdef1234567890abcdef") |
| const digest = types.Digest("11111111111111111111111111111111") |
| |
| const latestPositiveDigestRpcUrl = "https://testing-gold.skia.org/json/v2/latestpositivedigest/1234567890abcdef1234567890abcdef" |
| const latestPositiveDigestResponse = `{"digest":"22222222222222222222222222222222"}` |
| const latestPositiveDigest = types.Digest("22222222222222222222222222222222") |
| latestPositiveImageBytes := imageToPngBytes(t, text.MustToNRGBA(`! SKTEXTSIMPLE |
| 8 8 |
| 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF |
| 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF |
| 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF |
| 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF |
| 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF |
| 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF |
| 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF |
| 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF`)) |
| |
| test := func(name string, imageBytes []byte, expected bool) { |
| t.Run(name, func(t *testing.T) { |
| goldClient, ctx, httpClient, dlr := makeGoldClientForMatchImageAgainstBaselineTests(t) |
| defer httpClient.AssertExpectations(t) |
| defer dlr.AssertExpectations(t) |
| |
| httpClient.On("Get", testutils.AnyContext, latestPositiveDigestRpcUrl).Return(httpResponse(latestPositiveDigestResponse, "200 OK", http.StatusOK), nil) |
| dlr.On("DownloadImage", testutils.AnyContext, "https://testing-gold.skia.org", latestPositiveDigest).Return(latestPositiveImageBytes, nil) |
| |
| optionalKeys := map[string]string{ |
| imgmatching.AlgorithmNameOptKey: string(imgmatching.SampleAreaMatching), |
| string(imgmatching.SampleAreaWidth): "2", |
| string(imgmatching.MaxDifferentPixelsPerArea): "1", |
| string(imgmatching.SampleAreaChannelDeltaThreshold): "0", |
| } |
| |
| actual, algorithmName, err := goldClient.matchImageAgainstBaseline(ctx, testName, traceId, imageBytes, digest, optionalKeys) |
| assert.NoError(t, err) |
| assert.Equal(t, imgmatching.SampleAreaMatching, algorithmName) |
| assert.Equal(t, expected, actual) |
| }) |
| } |
| |
| test( |
| "identical images, returns true", |
| imageToPngBytes(t, text.MustToNRGBA(`! SKTEXTSIMPLE |
| 8 8 |
| 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF |
| 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF |
| 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF |
| 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF |
| 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF |
| 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF |
| 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF |
| 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF`)), |
| true) |
| test( |
| "small differences, returns true", |
| imageToPngBytes(t, text.MustToNRGBA(`! SKTEXTSIMPLE |
| 8 8 |
| 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF |
| 0xFF 0x00 0xFF 0xFF 0xFF 0x00 0xFF 0xFF |
| 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF |
| 0xFF 0xFF 0xFF 0x00 0xFF 0xFF 0xFF 0xFF |
| 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF |
| 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0x00 |
| 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF |
| 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF`)), |
| true) |
| test( |
| "large differences, returns false", |
| imageToPngBytes(t, text.MustToNRGBA(`! SKTEXTSIMPLE |
| 8 8 |
| 0xFF 0x00 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF |
| 0xFF 0x00 0xFF 0xFF 0x00 0x00 0xFF 0xFF |
| 0xFF 0xFF 0xFF 0xFF 0xFF 0x00 0xFF 0xFF |
| 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF |
| 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF |
| 0xFF 0xFF 0xFF 0x00 0x00 0xFF 0xFF 0xFF |
| 0xFF 0xFF 0xFF 0x00 0xFF 0xFF 0xFF 0xFF |
| 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF`)), |
| false) |
| test( |
| "different size images, returns false", |
| imageToPngBytes(t, text.MustToNRGBA(`! SKTEXTSIMPLE |
| 2 2 |
| 0xFF 0xFF |
| 0xFF 0xFF`)), |
| false) |
| } |
| |
| func TestCloudClient_MatchImageAgainstBaseline_SampleAreaMatching_InvalidParameters_ReturnsError(t *testing.T) { |
| test := func(name string, optionalKeys map[string]string, expectedError string) { |
| t.Run(name, func(t *testing.T) { |
| goldClient, ctx, _, _ := makeGoldClientForMatchImageAgainstBaselineTests(t) |
| |
| _, _, err := goldClient.matchImageAgainstBaseline(ctx, "my_test", "" /* =traceId */, nil /* =imageBytes */, "11111111111111111111111111111111", optionalKeys) |
| assert.Error(t, err) |
| assert.Contains(t, err.Error(), expectedError) |
| }) |
| } |
| |
| test( |
| "insufficient parameters: no parameter specified", |
| map[string]string{ |
| imgmatching.AlgorithmNameOptKey: string(imgmatching.SampleAreaMatching), |
| }, |
| "required image matching parameter not found") |
| test( |
| "invalid parameters", |
| map[string]string{ |
| imgmatching.AlgorithmNameOptKey: string(imgmatching.SampleAreaMatching), |
| string(imgmatching.SampleAreaWidth): "not a number", |
| string(imgmatching.MaxDifferentPixelsPerArea): "not a number", |
| string(imgmatching.SampleAreaChannelDeltaThreshold): "not a number", |
| }, |
| "parsing integer value") |
| } |
| |
| func TestCloudClient_MatchImageAgainstBaseline_SampleAreaMatching_NoRecentPositiveDigests_ReturnsFalse(t *testing.T) { |
| const testName = types.TestName("my_test") |
| const traceId = tiling.TraceIDV2("1234567890abcdef1234567890abcdef") |
| const digest = types.Digest("11111111111111111111111111111111") |
| imageBytes := imageToPngBytes(t, text.MustToNRGBA(`! SKTEXTSIMPLE |
| 1 1 |
| 0x00000000`)) |
| |
| const latestPositiveDigestRpcUrl = "https://testing-gold.skia.org/json/v2/latestpositivedigest/1234567890abcdef1234567890abcdef" |
| const latestPositiveDigestResponse = `{"digest":""}` |
| |
| goldClient, ctx, httpClient, _ := makeGoldClientForMatchImageAgainstBaselineTests(t) |
| defer httpClient.AssertExpectations(t) |
| |
| httpClient.On("Get", testutils.AnyContext, latestPositiveDigestRpcUrl).Return(httpResponse(latestPositiveDigestResponse, "200 OK", http.StatusOK), nil) |
| |
| optionalKeys := map[string]string{ |
| imgmatching.AlgorithmNameOptKey: string(imgmatching.SampleAreaMatching), |
| string(imgmatching.SampleAreaWidth): "2", |
| string(imgmatching.MaxDifferentPixelsPerArea): "1", |
| string(imgmatching.SampleAreaChannelDeltaThreshold): "0", |
| } |
| |
| matched, algorithmName, err := goldClient.matchImageAgainstBaseline(ctx, testName, traceId, imageBytes, digest, optionalKeys) |
| assert.NoError(t, err) |
| assert.False(t, matched) |
| assert.Equal(t, imgmatching.SampleAreaMatching, algorithmName) |
| } |
| |
| func TestCloudClient_MatchImageAgainstBaseline_SobelFuzzyMatching_ImageAlreadyLabeled_Success(t *testing.T) { |
| test := func(name string, label expectations.Label, want bool) { |
| t.Run(name, func(t *testing.T) { |
| goldClient, ctx, _, _ := makeGoldClientForMatchImageAgainstBaselineTests(t) |
| |
| const testName = types.TestName("my_test") |
| const digest = types.Digest("11111111111111111111111111111111") |
| optionalKeys := map[string]string{ |
| imgmatching.AlgorithmNameOptKey: string(imgmatching.SobelFuzzyMatching), |
| // These optionalKeys do not matter because the algorithm is not exercised by this test. |
| string(imgmatching.EdgeThreshold): "0", |
| string(imgmatching.MaxDifferentPixels): "0", |
| string(imgmatching.PixelDeltaThreshold): "0", |
| } |
| |
| goldClient.resultState.Expectations = expectations.Baseline{ |
| testName: { |
| digest: label, |
| }, |
| } |
| |
| got, algorithmName, err := goldClient.matchImageAgainstBaseline(ctx, testName, "" /* =traceId */, nil /* =imageBytes */, digest, optionalKeys) |
| assert.NoError(t, err) |
| assert.Equal(t, imgmatching.ExactMatching, algorithmName) |
| assert.Equal(t, want, got) |
| }) |
| } |
| |
| test("labeled positive, returns true", expectations.Positive, true) |
| test("labeled negative, returns false", expectations.Negative, false) |
| } |
| |
| func TestCloudClient_MatchImageAgainstBaseline_SobelFuzzyMatching_UntriagedImage_Success(t *testing.T) { |
| const testName = types.TestName("my_test") |
| const traceId = tiling.TraceIDV2("1234567890abcdef1234567890abcdef") |
| const digest = types.Digest("11111111111111111111111111111111") |
| testImageBytes := imageToPngBytes(t, text.MustToNRGBA(`! SKTEXTSIMPLE |
| 8 8 |
| 0x44 0x44 0x44 0x44 0x44 0x44 0x49 0x83 |
| 0x44 0x44 0x44 0x44 0x44 0x49 0x83 0x88 |
| 0x44 0x44 0x47 0x49 0x55 0x83 0x88 0x88 |
| 0x44 0x44 0x49 0x4D 0x7F 0x87 0x88 0x88 |
| 0x44 0x44 0x55 0x7F 0x88 0x88 0x88 0x88 |
| 0x44 0x49 0x83 0x87 0x88 0x88 0x88 0x88 |
| 0x49 0x83 0x88 0x88 0x88 0x88 0x88 0x88 |
| 0x83 0x88 0x88 0x88 0x88 0x88 0x88 0x88`)) |
| |
| const latestPositiveDigestRpcUrl = "https://testing-gold.skia.org/json/v2/latestpositivedigest/1234567890abcdef1234567890abcdef" |
| const latestPositiveDigestResponse = `{"digest":"22222222222222222222222222222222"}` |
| const latestPositiveDigest = types.Digest("22222222222222222222222222222222") |
| latestPositiveImageBytes := imageToPngBytes(t, text.MustToNRGBA(`! SKTEXTSIMPLE |
| 8 8 |
| 0x44 0x44 0x44 0x44 0x44 0x44 0x49 0x83 |
| 0x44 0x44 0x44 0x44 0x44 0x49 0x83 0x88 |
| 0x44 0x44 0x44 0x44 0x49 0x83 0x88 0x88 |
| 0x44 0x44 0x44 0x49 0x83 0x88 0x88 0x88 |
| 0x44 0x44 0x49 0x83 0x88 0x88 0x88 0x88 |
| 0x44 0x49 0x83 0x88 0x88 0x88 0x88 0x88 |
| 0x49 0x83 0x88 0x88 0x88 0x88 0x88 0x88 |
| 0x83 0x88 0x88 0x88 0x88 0x88 0x88 0x88`)) |
| |
| test := func(name, edgeThreshold string, expected bool) { |
| t.Run(name, func(t *testing.T) { |
| goldClient, ctx, httpClient, dlr := makeGoldClientForMatchImageAgainstBaselineTests(t) |
| defer httpClient.AssertExpectations(t) |
| defer dlr.AssertExpectations(t) |
| |
| httpClient.On("Get", testutils.AnyContext, latestPositiveDigestRpcUrl).Return(httpResponse(latestPositiveDigestResponse, "200 OK", http.StatusOK), nil) |
| dlr.On("DownloadImage", testutils.AnyContext, "https://testing-gold.skia.org", latestPositiveDigest).Return(latestPositiveImageBytes, nil) |
| |
| optionalKeys := map[string]string{ |
| imgmatching.AlgorithmNameOptKey: string(imgmatching.SobelFuzzyMatching), |
| string(imgmatching.MaxDifferentPixels): "2", |
| string(imgmatching.PixelDeltaThreshold): "10", |
| string(imgmatching.EdgeThreshold): edgeThreshold, |
| } |
| |
| actual, algorithmName, err := goldClient.matchImageAgainstBaseline(ctx, testName, traceId, testImageBytes, digest, optionalKeys) |
| assert.NoError(t, err) |
| assert.Equal(t, imgmatching.SobelFuzzyMatching, algorithmName) |
| assert.Equal(t, expected, actual) |
| }) |
| } |
| |
| test("differences under threshold, returns true", "0x66", true) |
| test("differences above threshold, returns false", "0xAA", false) |
| } |
| |
| func TestCloudClient_MatchImageAgainstBaseline_SobelFuzzyMatching_InvalidParameters_ReturnsError(t *testing.T) { |
| test := func(name string, optionalKeys map[string]string, expectedError string) { |
| t.Run(name, func(t *testing.T) { |
| goldClient, ctx, _, _ := makeGoldClientForMatchImageAgainstBaselineTests(t) |
| |
| _, _, err := goldClient.matchImageAgainstBaseline(ctx, "my_test", "" /* =traceId */, nil /* =imageBytes */, "11111111111111111111111111111111", optionalKeys) |
| assert.Error(t, err) |
| assert.Contains(t, err.Error(), expectedError) |
| }) |
| } |
| |
| test( |
| "insufficient parameters: no parameter specified", |
| map[string]string{ |
| imgmatching.AlgorithmNameOptKey: string(imgmatching.SobelFuzzyMatching), |
| }, |
| "required image matching parameter not found") |
| test( |
| "insufficient parameters: only SobelFuzzyMatching-specific parameter specified", |
| map[string]string{ |
| imgmatching.AlgorithmNameOptKey: string(imgmatching.SobelFuzzyMatching), |
| string(imgmatching.EdgeThreshold): "0", |
| }, |
| `required image matching parameter not found: "fuzzy_max_different_pixels"`) |
| test( |
| "insufficient parameters: only FuzzyMatching-specific parameter specified", |
| map[string]string{ |
| imgmatching.AlgorithmNameOptKey: string(imgmatching.SobelFuzzyMatching), |
| string(imgmatching.MaxDifferentPixels): "0", |
| string(imgmatching.PixelDeltaThreshold): "0", |
| string(imgmatching.PixelPerChannelDeltaThreshold): "0", |
| }, |
| "required image matching parameter not found") |
| test( |
| "invalid parameters", |
| map[string]string{ |
| imgmatching.AlgorithmNameOptKey: string(imgmatching.SobelFuzzyMatching), |
| string(imgmatching.EdgeThreshold): "not a number", |
| string(imgmatching.MaxDifferentPixels): "not a number", |
| string(imgmatching.PixelDeltaThreshold): "not a number", |
| string(imgmatching.PixelPerChannelDeltaThreshold): "not a number", |
| }, |
| "parsing integer value") |
| } |
| |
| func TestCloudClient_MatchImageAgainstBaseline_SobelFuzzyMatching_NoRecentPositiveDigests_ReturnsFalse(t *testing.T) { |
| const testName = types.TestName("my_test") |
| const traceId = tiling.TraceIDV2("1234567890abcdef1234567890abcdef") |
| const digest = types.Digest("11111111111111111111111111111111") |
| imageBytes := imageToPngBytes(t, text.MustToNRGBA(`! SKTEXTSIMPLE |
| 1 1 |
| 0x00000000`)) |
| |
| const latestPositiveDigestRpcUrl = "https://testing-gold.skia.org/json/v2/latestpositivedigest/1234567890abcdef1234567890abcdef" |
| const latestPositiveDigestResponse = `{"digest":""}` |
| |
| goldClient, ctx, httpClient, _ := makeGoldClientForMatchImageAgainstBaselineTests(t) |
| defer httpClient.AssertExpectations(t) |
| |
| httpClient.On("Get", testutils.AnyContext, latestPositiveDigestRpcUrl).Return(httpResponse(latestPositiveDigestResponse, "200 OK", http.StatusOK), nil) |
| |
| optionalKeys := map[string]string{ |
| imgmatching.AlgorithmNameOptKey: string(imgmatching.SobelFuzzyMatching), |
| // These optionalKeys do not matter because the comparison image will be nil. |
| string(imgmatching.EdgeThreshold): "0xAA", |
| string(imgmatching.MaxDifferentPixels): "2", |
| string(imgmatching.PixelDeltaThreshold): "10", |
| } |
| |
| matched, algorithmName, err := goldClient.matchImageAgainstBaseline(ctx, testName, traceId, imageBytes, digest, optionalKeys) |
| assert.NoError(t, err) |
| assert.False(t, matched) |
| assert.Equal(t, imgmatching.SobelFuzzyMatching, algorithmName) |
| } |
| |
| func TestCloudClient_MatchImageAgainstBaseline_UnknownAlgorithm_ReturnsError(t *testing.T) { |
| goldClient, ctx, _, _ := makeGoldClientForMatchImageAgainstBaselineTests(t) |
| |
| optionalKeys := map[string]string{ |
| imgmatching.AlgorithmNameOptKey: "unknown algorithm", |
| } |
| |
| _, _, err := goldClient.matchImageAgainstBaseline(ctx, "" /* =testName */, "" /* =traceId */, nil /* =imageBytes */, "" /* =digest */, optionalKeys) |
| assert.Error(t, err) |
| assert.Contains(t, err.Error(), "unrecognized image matching algorithm") |
| } |
| |
| func TestCloudClient_GetDigestFromCacheOrGCS_NotInCache_DownloadsImageFromGCS_Success(t *testing.T) { |
| wd := t.TempDir() |
| |
| ctx, _, _, gcsDownloader := makeMocks() |
| gcsDownloader.AssertExpectations(t) |
| |
| goldClient, err := NewCloudClient(GoldClientConfig{ |
| WorkDir: wd, |
| InstanceID: "testing", |
| }) |
| assert.NoError(t, err) |
| |
| const digest = types.Digest("11111111111111111111111111111111") |
| digestImage := image1 |
| digestBytes := imageToPngBytes(t, image1) |
| |
| gcsDownloader.On("DownloadImage", testutils.AnyContext, "https://testing-gold.skia.org", digest).Return(digestBytes, nil) |
| |
| actualImage, actualBytes, err := goldClient.getDigestFromCacheOrGCS(ctx, digest) |
| assert.NoError(t, err) |
| assert.Equal(t, digestImage, actualImage) |
| assert.Equal(t, digestBytes, actualBytes) |
| } |
| |
| func TestCloudClient_GetDigestFromCacheOrGCS_NotInCache_DownloadsCorruptedImageFromGCS_Failure(t *testing.T) { |
| wd := t.TempDir() |
| |
| ctx, _, _, gcsDownloader := makeMocks() |
| gcsDownloader.AssertExpectations(t) |
| |
| goldClient, err := NewCloudClient(GoldClientConfig{ |
| WorkDir: wd, |
| InstanceID: "testing", |
| }) |
| assert.NoError(t, err) |
| |
| const digest = types.Digest("11111111111111111111111111111111") |
| digestBytes := []byte("corrupted image") |
| |
| gcsDownloader.On("DownloadImage", testutils.AnyContext, "https://testing-gold.skia.org", digest).Return(digestBytes, nil) |
| |
| _, _, err = goldClient.getDigestFromCacheOrGCS(ctx, digest) |
| assert.Error(t, err) |
| assert.Contains(t, err.Error(), "decoding PNG file at "+filepath.Join(wd, digestsDirectory, string(digest))+".png") |
| } |
| |
| func TestCloudClient_GetDigestFromCacheOrGCS_InCache_ReadsImageFromDisk_Success(t *testing.T) { |
| wd := t.TempDir() |
| |
| ctx, _, _, _ := makeMocks() |
| |
| goldClient, err := NewCloudClient(GoldClientConfig{ |
| WorkDir: wd, |
| InstanceID: "testing", |
| }) |
| assert.NoError(t, err) |
| |
| const digest = types.Digest("11111111111111111111111111111111") |
| digestImage := image1 |
| digestBytes := imageToPngBytes(t, image1) |
| |
| // Make cache directory that will contain the cached digest. |
| err = os.MkdirAll(filepath.Join(wd, digestsDirectory), os.ModePerm) |
| assert.NoError(t, err) |
| |
| // Write cached digest to disk. |
| err = os.WriteFile(filepath.Join(wd, digestsDirectory, string(digest)+".png"), digestBytes, os.ModePerm) |
| assert.NoError(t, err) |
| |
| actualImage, actualBytes, err := goldClient.getDigestFromCacheOrGCS(ctx, digest) |
| assert.NoError(t, err) |
| assert.Equal(t, digestImage, actualImage) |
| assert.Equal(t, digestBytes, actualBytes) |
| } |
| |
| func TestCloudClient_GetDigestFromCacheOrGCS_InCache_ReadsCorruptedImageFromDisk_Failure(t *testing.T) { |
| wd := t.TempDir() |
| |
| ctx, _, _, _ := makeMocks() |
| |
| goldClient, err := NewCloudClient(GoldClientConfig{ |
| WorkDir: wd, |
| InstanceID: "testing", |
| }) |
| assert.NoError(t, err) |
| |
| const digest = types.Digest("11111111111111111111111111111111") |
| digestBytes := []byte("corrupted image") |
| |
| // Make cache directory that will contain the cached digest. |
| err = os.MkdirAll(filepath.Join(wd, digestsDirectory), os.ModePerm) |
| assert.NoError(t, err) |
| |
| // Write cached digest to disk. |
| err = os.WriteFile(filepath.Join(wd, digestsDirectory, string(digest)+".png"), digestBytes, os.ModePerm) |
| assert.NoError(t, err) |
| |
| _, _, err = goldClient.getDigestFromCacheOrGCS(ctx, digest) |
| assert.Error(t, err) |
| assert.Contains(t, err.Error(), "decoding PNG file at "+filepath.Join(wd, digestsDirectory, string(digest))+".png") |
| } |
| |
| func TestCloudClient_Whoami_Success(t *testing.T) { |
| // This test reads and writes a small amount of data from/to disk. |
| |
| wd := t.TempDir() |
| |
| ctx, httpClient, _, _ := makeMocks() |
| defer httpClient.AssertExpectations(t) |
| |
| config := GoldClientConfig{ |
| WorkDir: wd, |
| InstanceID: "testing", |
| } |
| goldClient, err := NewCloudClient(config) |
| assert.NoError(t, err) |
| |
| url := "https://testing-gold.skia.org/json/v1/whoami" |
| response := `{"whoami": "test@example.com"}` |
| httpClient.On("Get", testutils.AnyContext, url).Return(httpResponse(response, "200 OK", http.StatusOK), nil) |
| |
| email, err := goldClient.Whoami(ctx) |
| assert.NoError(t, err) |
| assert.Equal(t, "test@example.com", email) |
| } |
| |
| func TestCloudClient_Whoami_InternalServerError_Failure(t *testing.T) { |
| // This test takes >30 seconds to retry a bunch. |
| |
| wd := t.TempDir() |
| |
| ctx, httpClient, _, _ := makeMocks() |
| defer httpClient.AssertExpectations(t) |
| |
| config := GoldClientConfig{ |
| WorkDir: wd, |
| InstanceID: "testing", |
| } |
| goldClient, err := NewCloudClient(config) |
| assert.NoError(t, err) |
| |
| url := "https://testing-gold.skia.org/json/v1/whoami" |
| httpClient.On("Get", testutils.AnyContext, url).Return(httpResponse("", "500 Internal Server Error", http.StatusInternalServerError), nil) |
| |
| _, err = goldClient.Whoami(ctx) |
| assert.Error(t, err) |
| assert.Contains(t, err.Error(), "500") |
| } |
| |
| func TestCloudClient_TriageAsPositive_NoCL_Success(t *testing.T) { |
| // This test reads and writes a small amount of data from/to disk. |
| |
| wd := t.TempDir() |
| |
| // Pretend "goldctl imgtest init" was called. |
| j := resultState{ |
| GoldURL: "https://testing-gold.skia.org", |
| SharedConfig: jsonio.GoldResults{}, |
| } |
| jsonToWrite := testutils.MarshalJSON(t, &j) |
| testutils.WriteFile(t, filepath.Join(wd, stateFile), jsonToWrite) |
| |
| ctx, httpClient, _, _ := makeMocks() |
| defer httpClient.AssertExpectations(t) |
| |
| goldClient, err := LoadCloudClient(wd) |
| assert.NoError(t, err) |
| |
| url := "https://testing-gold.skia.org/json/v2/triage" |
| contentType := "application/json" |
| bodyMatcher := mock.MatchedBy(func(r io.Reader) bool { |
| b, err := io.ReadAll(r) |
| assert.NoError(t, err) |
| if len(b) == 0 { |
| // This matcher can get called a second time during AssertExpectations. This check makes sure |
| // we don't erroniously fail. |
| return false |
| } |
| tr := frontend.TriageRequestV2{} |
| assert.NoError(t, json.Unmarshal(b, &tr)) |
| assert.Equal(t, frontend.TriageRequestV2{ |
| TestDigestStatus: map[types.TestName]map[types.Digest]expectations.Label{ |
| "MyTest": { |
| "deadbeefcafefe771d61bf0ed3d84bc2": expectations.Positive, |
| }, |
| }, |
| ImageMatchingAlgorithm: "fuzzy", |
| }, tr) |
| return true |
| }) |
| httpClient.On("Post", testutils.AnyContext, url, contentType, bodyMatcher).Return(httpResponse("", "200 OK", http.StatusOK), nil) |
| |
| err = goldClient.TriageAsPositive(ctx, "MyTest", "deadbeefcafefe771d61bf0ed3d84bc2", "fuzzy") |
| assert.NoError(t, err) |
| } |
| |
| func TestCloudClient_TriageAsPositive_WithCL_Success(t *testing.T) { |
| // This test reads and writes a small amount of data from/to disk. |
| |
| wd := t.TempDir() |
| |
| // Pretend "goldctl imgtest init" was called. |
| j := resultState{ |
| GoldURL: "https://testing-gold.skia.org", |
| SharedConfig: jsonio.GoldResults{ |
| CodeReviewSystem: "gerrit", |
| ChangelistID: "123456", |
| }, |
| } |
| jsonToWrite := testutils.MarshalJSON(t, &j) |
| testutils.WriteFile(t, filepath.Join(wd, stateFile), jsonToWrite) |
| |
| ctx, httpClient, _, _ := makeMocks() |
| defer httpClient.AssertExpectations(t) |
| |
| goldClient, err := LoadCloudClient(wd) |
| assert.NoError(t, err) |
| |
| url := "https://testing-gold.skia.org/json/v2/triage" |
| contentType := "application/json" |
| bodyMatcher := mock.MatchedBy(func(r io.Reader) bool { |
| b, err := io.ReadAll(r) |
| assert.NoError(t, err) |
| if len(b) == 0 { |
| // This matcher can get called a second time during AssertExpectations. This check makes sure |
| // we don't erroniously fail. |
| return false |
| } |
| tr := frontend.TriageRequestV2{} |
| assert.NoError(t, json.Unmarshal(b, &tr)) |
| assert.Equal(t, frontend.TriageRequestV2{ |
| TestDigestStatus: map[types.TestName]map[types.Digest]expectations.Label{ |
| "MyTest": { |
| "deadbeefcafefe771d61bf0ed3d84bc2": expectations.Positive, |
| }, |
| }, |
| CodeReviewSystem: "gerrit", |
| ChangelistID: "123456", |
| ImageMatchingAlgorithm: "fuzzy", |
| }, tr) |
| return true |
| }) |
| httpClient.On("Post", testutils.AnyContext, url, contentType, bodyMatcher).Return(httpResponse("", "200 OK", http.StatusOK), nil) |
| |
| err = goldClient.TriageAsPositive(ctx, "MyTest", "deadbeefcafefe771d61bf0ed3d84bc2", "fuzzy") |
| assert.NoError(t, err) |
| } |
| |
| func TestCloudClient_TriageAsPositive_InternalServerError_Failure(t *testing.T) { |
| // This test takes >30 seconds to retry a bunch. |
| |
| wd := t.TempDir() |
| |
| // Pretend "goldctl imgtest init" was called. |
| j := resultState{ |
| GoldURL: "https://testing-gold.skia.org", |
| SharedConfig: jsonio.GoldResults{}, |
| } |
| jsonToWrite := testutils.MarshalJSON(t, &j) |
| testutils.WriteFile(t, filepath.Join(wd, stateFile), jsonToWrite) |
| |
| ctx, httpClient, _, _ := makeMocks() |
| defer httpClient.AssertExpectations(t) |
| |
| goldClient, err := LoadCloudClient(wd) |
| assert.NoError(t, err) |
| |
| url := "https://testing-gold.skia.org/json/v2/triage" |
| contentType := "application/json" |
| httpClient.On("Post", testutils.AnyContext, url, contentType, mock.Anything).Return(httpResponse("", "500 Internal Server Error", http.StatusInternalServerError), nil) |
| |
| err = goldClient.TriageAsPositive(ctx, "MyTest", "deadbeefcafefe771d61bf0ed3d84bc2", "fuzzy") |
| assert.Error(t, err) |
| assert.Contains(t, err.Error(), "500") |
| } |
| |
| func TestCloudClient_MostRecentPositiveDigest_Success(t *testing.T) { |
| // This test reads and writes a small amount of data from/to disk. |
| |
| wd := t.TempDir() |
| |
| ctx, httpClient, _, _ := makeMocks() |
| defer httpClient.AssertExpectations(t) |
| |
| config := GoldClientConfig{ |
| WorkDir: wd, |
| InstanceID: "testing", |
| } |
| goldClient, err := NewCloudClient(config) |
| assert.NoError(t, err) |
| |
| const traceId = tiling.TraceIDV2("1234567890abcdef1234567890abcdef") |
| const url = "https://testing-gold.skia.org/json/v2/latestpositivedigest/1234567890abcdef1234567890abcdef" |
| const response = `{"digest":"deadbeefcafefe771d61bf0ed3d84bc2"}` |
| const expectedDigest = types.Digest("deadbeefcafefe771d61bf0ed3d84bc2") |
| |
| httpClient.On("Get", testutils.AnyContext, url).Return(httpResponse(response, "200 OK", http.StatusOK), nil) |
| |
| actualDigest, err := goldClient.MostRecentPositiveDigest(ctx, traceId) |
| assert.NoError(t, err) |
| assert.Equal(t, expectedDigest, actualDigest) |
| } |
| |
| func TestCloudClient_MostRecentPositiveDigest_NonJSONResponse_Failure(t *testing.T) { |
| // This test reads and writes a small amount of data from/to disk. |
| |
| wd := t.TempDir() |
| |
| ctx, httpClient, _, _ := makeMocks() |
| defer httpClient.AssertExpectations(t) |
| |
| config := GoldClientConfig{ |
| WorkDir: wd, |
| InstanceID: "testing", |
| } |
| goldClient, err := NewCloudClient(config) |
| assert.NoError(t, err) |
| |
| const traceId = tiling.TraceIDV2("1234567890abcdef1234567890abcdef") |
| const url = "https://testing-gold.skia.org/json/v2/latestpositivedigest/1234567890abcdef1234567890abcdef" |
| const response = "Not JSON" |
| |
| httpClient.On("Get", testutils.AnyContext, url).Return(httpResponse(response, "200 OK", http.StatusOK), nil) |
| |
| _, err = goldClient.MostRecentPositiveDigest(ctx, traceId) |
| assert.Error(t, err) |
| assert.Contains(t, err.Error(), "unmarshalling JSON response") |
| } |
| |
| func TestCloudClient_MostRecentPositiveDigest_InternalServerError_Failure(t *testing.T) { |
| // This test takes a while to deal with retries |
| |
| wd := t.TempDir() |
| |
| ctx, httpClient, _, _ := makeMocks() |
| defer httpClient.AssertExpectations(t) |
| |
| config := GoldClientConfig{ |
| WorkDir: wd, |
| InstanceID: "testing", |
| } |
| goldClient, err := NewCloudClient(config) |
| assert.NoError(t, err) |
| |
| const traceId = tiling.TraceIDV2("1234567890abcdef1234567890abcdef") |
| const url = "https://testing-gold.skia.org/json/v2/latestpositivedigest/1234567890abcdef1234567890abcdef" |
| |
| httpClient.On("Get", testutils.AnyContext, url).Return(httpResponse("", "500 Internal Server Error", http.StatusInternalServerError), nil) |
| |
| _, err = goldClient.MostRecentPositiveDigest(ctx, traceId) |
| assert.Error(t, err) |
| assert.Contains(t, err.Error(), "500") |
| } |
| |
| func TestCloudClient_GroupingForTrace_ValidTraceParams_Success(t *testing.T) { |
| wd := t.TempDir() |
| |
| ctx, httpClient, _, _ := makeMocks() |
| defer httpClient.AssertExpectations(t) |
| |
| hashesResp := httpResponse("none", "200 OK", http.StatusOK) |
| httpClient.On("Get", testutils.AnyContext, "https://testing-gold.skia.org/json/v1/hashes").Return(hashesResp, nil) |
| |
| expectationsResp := httpResponse("{}", "200 OK", http.StatusOK) |
| httpClient.On("Get", testutils.AnyContext, "https://testing-gold.skia.org/json/v2/expectations?issue=867&crs=gerrit").Return(expectationsResp, nil) |
| |
| groupingsResp := httpResponse(`{"grouping_param_keys_by_corpus": {"my-corpus": ["name", "source_type"]}}`, "200 OK", http.StatusOK) |
| httpClient.On("Get", testutils.AnyContext, "https://testing-gold.skia.org/json/v1/groupings").Return(groupingsResp, nil) |
| |
| goldClient, err := makeGoldClient(true /*=passFail*/, false /*=uploadOnly*/, wd) |
| assert.NoError(t, err) |
| err = goldClient.SetSharedConfig(ctx, makeTestSharedConfig(), false) |
| assert.NoError(t, err) |
| |
| traceParams := paramtools.Params{ |
| "name": "my-test", |
| "source_type": "my-corpus", |
| "alpha": "beta", |
| } |
| expectedGrouping := paramtools.Params{ |
| "name": "my-test", |
| "source_type": "my-corpus", |
| } |
| |
| grouping, err := goldClient.groupingForTrace(ctx, traceParams) |
| require.NoError(t, err) |
| assert.Equal(t, expectedGrouping, grouping) |
| numberOfCalls := 3 |
| httpClient.AssertNumberOfCalls(t, "Get", numberOfCalls) |
| |
| // Assert that the /json/v1/groupings RPC is only called once. |
| grouping, err = goldClient.groupingForTrace(ctx, traceParams) |
| require.NoError(t, err) |
| assert.Equal(t, expectedGrouping, grouping) // Same response as before. |
| httpClient.AssertNumberOfCalls(t, "Get", numberOfCalls) // No new RPC calls. |
| } |
| |
| func TestCloudClient_GroupingForTrace_InvalidTraceParams_Error(t *testing.T) { |
| test := func(name string, traceParams paramtools.Params, callsGroupingRPC bool, expectedError string) { |
| t.Run(name, func(t *testing.T) { |
| wd := t.TempDir() |
| |
| ctx, httpClient, _, _ := makeMocks() |
| defer httpClient.AssertExpectations(t) |
| |
| hashesResp := httpResponse("none", "200 OK", http.StatusOK) |
| httpClient.On("Get", testutils.AnyContext, "https://testing-gold.skia.org/json/v1/hashes").Return(hashesResp, nil) |
| |
| expectationsResp := httpResponse("{}", "200 OK", http.StatusOK) |
| httpClient.On("Get", testutils.AnyContext, "https://testing-gold.skia.org/json/v2/expectations?issue=867&crs=gerrit").Return(expectationsResp, nil) |
| |
| if callsGroupingRPC { |
| groupingsResp := httpResponse(`{"grouping_param_keys_by_corpus": {"my-corpus": ["name", "source_type"]}}`, "200 OK", http.StatusOK) |
| httpClient.On("Get", testutils.AnyContext, "https://testing-gold.skia.org/json/v1/groupings").Return(groupingsResp, nil) |
| } |
| |
| goldClient, err := makeGoldClient(true /*=passFail*/, false /*=uploadOnly*/, wd) |
| assert.NoError(t, err) |
| err = goldClient.SetSharedConfig(ctx, makeTestSharedConfig(), false) |
| assert.NoError(t, err) |
| |
| _, err = goldClient.groupingForTrace(ctx, traceParams) |
| require.Error(t, err) |
| assert.Contains(t, err.Error(), expectedError) |
| }) |
| } |
| |
| test( |
| "no corpus", |
| paramtools.Params{ |
| "name": "my-test", |
| "alpha": "beta", |
| }, |
| false, /* =callsGroupingRPC */ |
| `trace params must include key "source_type"`) |
| test( |
| "unknown corpus", |
| paramtools.Params{ |
| "name": "my-test", |
| "source_type": "unknown-corpus", |
| "alpha": "beta", |
| }, |
| true, /* =callsGroupingRPC */ |
| `grouping params for corpus "unknown-corpus" are unknown; known grouping params: map[my-corpus:[name source_type]]`) |
| test( |
| "no test name", |
| paramtools.Params{ |
| "source_type": "my-corpus", |
| "alpha": "beta", |
| }, |
| true, /* =callsGroupingRPC */ |
| `trace params must include key "name"`) |
| } |
| |
| func makeMocks() (context.Context, *mocks.HTTPClient, *mocks.GCSUploader, *mocks.ImageDownloader) { |
| mh := &mocks.HTTPClient{} |
| mg := &mocks.GCSUploader{} |
| md := &mocks.ImageDownloader{} |
| fakeNow := time.Date(2019, time.April, 2, 19, 54, 3, 0, time.UTC) |
| ctx := WithContext(context.Background(), mg, mh, md) |
| ctx = context.WithValue(ctx, now.ContextKey, fakeNow) |
| return ctx, mh, mg, md |
| } |
| |
| // makeGoldClient will create new cloud client from scratch (using a |
| // set configuration), and return it. |
| func makeGoldClient(passFail bool, uploadOnly bool, workDir string) (*CloudClient, error) { |
| config := GoldClientConfig{ |
| InstanceID: testInstanceID, |
| WorkDir: workDir, |
| FailureFile: filepath.Join(workDir, failureLog), |
| PassFailStep: passFail, |
| UploadOnly: uploadOnly, |
| } |
| |
| return NewCloudClient(config) |
| } |
| |
| // makeGoldClientForMatchImageAgainstBaselineTests returns a new CloudClient to be used in |
| // CloudClient#matchImageAgainstBaseline() tests. |
| func makeGoldClientForMatchImageAgainstBaselineTests(t *testing.T) (*CloudClient, context.Context, *mocks.HTTPClient, *mocks.ImageDownloader) { |
| wd := t.TempDir() |
| ctx, httpClient, _, gcsDownloader := makeMocks() |
| goldClient, err := NewCloudClient(GoldClientConfig{ |
| WorkDir: wd, |
| InstanceID: "testing", |
| }) |
| require.NoError(t, err) |
| return goldClient, ctx, httpClient, gcsDownloader |
| } |
| |
| func overrideLoadAndHashImage(c *CloudClient, testFn func(path string) ([]byte, types.Digest, error)) { |
| c.loadAndHashImage = testFn |
| } |
| |
| const ( |
| testInstanceID = "testing" |
| testIssueID = "867" |
| testPatchsetOrder = 5309 |
| testBuildBucketID = "117" |
| testImgPath = "/path/to/images/fake.png" |
| |
| failureLog = "failures.log" |
| ) |
| |
| // These images (of type *image.NRGBA) are assumed to be used in a read-only manner |
| // throughout the tests. |
| var image1 = text.MustToNRGBA(one_by_five.ImageOne) |
| var image2 = text.MustToNRGBA(one_by_five.ImageTwo) |
| var image3 = text.MustToNRGBA(one_by_five.ImageSix) |
| var diff12 = text.MustToNRGBA(one_by_five.DiffImageOneAndTwo) |
| |
| // An example baseline that has a single test at a single commit with a good |
| // image and a bad image. |
| const mockBaselineJSON = ` |
| { |
| "md5": "7e4081337b3258555906970002a04a59", |
| "primary": { |
| "ThisIsTheOnlyTest": { |
| "beef00d3a1527db19619ec12a4e0df68": "positive", |
| "badbadbad1325855590527db196112e0": "negative" |
| } |
| }, |
| "Issue": -1 |
| }` |
| |
| const mockHashesTxt = `a9e1481ebc45c1c4f6720d1119644c20 |
| c156c5e4b634a3b8cc96e16055197f8b |
| 4a434407218e198faf2054645fe0ff73 |
| 303a5fd488361214f246004530e24273` |
| |
| const mockDigestsJSON = ` |
| { |
| "digests": ["bbb0dc56d0429ef3586629787666ce09", "ccc2912653148661835084a809fee263"] |
| }` |
| |
| func makeTestSharedConfig() jsonio.GoldResults { |
| return jsonio.GoldResults{ |
| GitHash: "abcd1234", |
| Key: map[string]string{ |
| "os": "WinTest", |
| "gpu": "GPUTest", |
| }, |
| ChangelistID: testIssueID, |
| PatchsetOrder: testPatchsetOrder, |
| CodeReviewSystem: "gerrit", |
| TryJobID: testBuildBucketID, |
| ContinuousIntegrationSystem: "buildbucket", |
| } |
| } |
| |
| func imageToPngBytes(t *testing.T, img image.Image) []byte { |
| var buf bytes.Buffer |
| require.NoError(t, png.Encode(&buf, img)) |
| return buf.Bytes() |
| } |
| |
| // openNRGBAFromFile opens the given file path to a PNG file and returns the image as image.NRGBA. |
| func openNRGBAFromFile(fileName string) (*image.NRGBA, error) { |
| var img *image.NRGBA |
| err := util.WithReadFile(fileName, func(r io.Reader) error { |
| im, err := png.Decode(r) |
| if err != nil { |
| return err |
| } |
| img = diff.GetNRGBA(im) |
| return nil |
| }) |
| if err != nil { |
| return nil, skerr.Wrap(err) |
| } |
| return img, nil |
| } |