blob: 443299aead195415714867b4c44b442ba8b1ced7 [file] [log] [blame]
package worker
import (
"bytes"
"context"
"errors"
"io/ioutil"
"path/filepath"
"testing"
"time"
"go.skia.org/infra/golden/go/sql/databuilder"
"go.skia.org/infra/go/repo_root"
"github.com/jackc/pgx/v4/pgxpool"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.skia.org/infra/go/now"
"go.skia.org/infra/go/paramtools"
"go.skia.org/infra/go/testutils"
"go.skia.org/infra/go/testutils/unittest"
"go.skia.org/infra/golden/go/diff/mocks"
"go.skia.org/infra/golden/go/sql"
dks "go.skia.org/infra/golden/go/sql/datakitchensink"
"go.skia.org/infra/golden/go/sql/schema"
"go.skia.org/infra/golden/go/sql/sqltest"
"go.skia.org/infra/golden/go/types"
)
func TestWorkerImpl_CalculateDiffs_NoExistingData_Success(t *testing.T) {
unittest.LargeTest(t)
fakeNow := time.Date(2021, time.February, 1, 1, 1, 1, 0, time.UTC)
ctx := context.WithValue(context.Background(), now.ContextKey, fakeNow)
db := sqltest.NewCockroachDBForTestsWithProductionSchema(ctx, t)
waitForSystemTime()
w := newWorker2UsingImagesFromKitchenSink(t, db)
grouping := paramtools.Params{
types.CorpusField: "not used",
types.PrimaryKeyField: "not used",
}
imagesToCalculateDiffsFor := []types.Digest{dks.DigestA01Pos, dks.DigestA02Pos, dks.DigestA04Unt, dks.DigestA05Unt}
require.NoError(t, w.CalculateDiffs(ctx, grouping, imagesToCalculateDiffsFor))
actualMetrics := getAllDiffMetricRows(t, db)
assert.Equal(t, []schema.DiffMetricRow{
expectedFromKS(t, dks.DigestA01Pos, dks.DigestA02Pos, fakeNow),
expectedFromKS(t, dks.DigestA01Pos, dks.DigestA04Unt, fakeNow),
expectedFromKS(t, dks.DigestA01Pos, dks.DigestA05Unt, fakeNow),
expectedFromKS(t, dks.DigestA02Pos, dks.DigestA01Pos, fakeNow),
expectedFromKS(t, dks.DigestA02Pos, dks.DigestA04Unt, fakeNow),
expectedFromKS(t, dks.DigestA02Pos, dks.DigestA05Unt, fakeNow),
expectedFromKS(t, dks.DigestA04Unt, dks.DigestA01Pos, fakeNow),
expectedFromKS(t, dks.DigestA04Unt, dks.DigestA02Pos, fakeNow),
expectedFromKS(t, dks.DigestA04Unt, dks.DigestA05Unt, fakeNow),
expectedFromKS(t, dks.DigestA05Unt, dks.DigestA01Pos, fakeNow),
expectedFromKS(t, dks.DigestA05Unt, dks.DigestA02Pos, fakeNow),
expectedFromKS(t, dks.DigestA05Unt, dks.DigestA04Unt, fakeNow),
}, actualMetrics)
assert.Empty(t, getAllProblemImageRows(t, db))
}
func TestWorkerImpl_CalculateDiffs_ReadFromPrimaryBranch_Success(t *testing.T) {
unittest.LargeTest(t)
fakeNow := time.Date(2021, time.February, 1, 1, 1, 1, 0, time.UTC)
ctx := context.WithValue(context.Background(), now.ContextKey, fakeNow)
db := sqltest.NewCockroachDBForTestsWithProductionSchema(ctx, t)
existingData := dks.Build()
// Remove existing diffs, so the ones for triangle test can be recomputed.
existingData.DiffMetrics = nil
require.NoError(t, sqltest.BulkInsertDataTables(ctx, db, existingData))
waitForSystemTime()
w := newWorker2UsingImagesFromKitchenSink(t, db)
grouping := paramtools.Params{
types.CorpusField: dks.CornersCorpus,
types.PrimaryKeyField: dks.TriangleTest,
}
require.NoError(t, w.CalculateDiffs(ctx, grouping, nil))
actualMetrics := getAllDiffMetricRows(t, db)
assert.Equal(t, []schema.DiffMetricRow{
expectedFromKS(t, dks.DigestBlank, dks.DigestB01Pos, fakeNow),
expectedFromKS(t, dks.DigestBlank, dks.DigestB02Pos, fakeNow),
expectedFromKS(t, dks.DigestBlank, dks.DigestB03Neg, fakeNow),
expectedFromKS(t, dks.DigestBlank, dks.DigestB04Neg, fakeNow),
expectedFromKS(t, dks.DigestB01Pos, dks.DigestBlank, fakeNow),
expectedFromKS(t, dks.DigestB01Pos, dks.DigestB02Pos, fakeNow),
expectedFromKS(t, dks.DigestB01Pos, dks.DigestB03Neg, fakeNow),
expectedFromKS(t, dks.DigestB01Pos, dks.DigestB04Neg, fakeNow),
expectedFromKS(t, dks.DigestB02Pos, dks.DigestBlank, fakeNow),
expectedFromKS(t, dks.DigestB02Pos, dks.DigestB01Pos, fakeNow),
expectedFromKS(t, dks.DigestB02Pos, dks.DigestB03Neg, fakeNow),
expectedFromKS(t, dks.DigestB02Pos, dks.DigestB04Neg, fakeNow),
expectedFromKS(t, dks.DigestB03Neg, dks.DigestBlank, fakeNow),
expectedFromKS(t, dks.DigestB03Neg, dks.DigestB01Pos, fakeNow),
expectedFromKS(t, dks.DigestB03Neg, dks.DigestB02Pos, fakeNow),
expectedFromKS(t, dks.DigestB03Neg, dks.DigestB04Neg, fakeNow),
expectedFromKS(t, dks.DigestB04Neg, dks.DigestBlank, fakeNow),
expectedFromKS(t, dks.DigestB04Neg, dks.DigestB01Pos, fakeNow),
expectedFromKS(t, dks.DigestB04Neg, dks.DigestB02Pos, fakeNow),
expectedFromKS(t, dks.DigestB04Neg, dks.DigestB03Neg, fakeNow),
}, actualMetrics)
assert.Empty(t, getAllProblemImageRows(t, db))
}
func TestWorkerImpl_CalculateDiffs_DiffSubset_Success(t *testing.T) {
unittest.LargeTest(t)
fakeNow := time.Date(2021, time.February, 1, 1, 1, 1, 0, time.UTC)
ctx := context.WithValue(context.Background(), now.ContextKey, fakeNow)
db := sqltest.NewCockroachDBForTestsWithProductionSchema(ctx, t)
existingData := dks.Build()
// Remove existing diffs, so the ones for triangle test can be recomputed.
existingData.DiffMetrics = nil
require.NoError(t, sqltest.BulkInsertDataTables(ctx, db, existingData))
waitForSystemTime()
w := newWorker2UsingImagesFromKitchenSink(t, db)
grouping := paramtools.Params{
types.CorpusField: dks.CornersCorpus,
types.PrimaryKeyField: dks.TriangleTest,
}
// By adding in extra digests, we make sure to hit the calculateDiffSubset branch
var extraDigests []types.Digest
for i := 0; i < computeTotalGridCutoff+1; i++ {
extraDigests = append(extraDigests, dks.DigestBlank)
}
require.NoError(t, w.CalculateDiffs(ctx, grouping, extraDigests))
actualMetrics := getAllDiffMetricRows(t, db)
assert.Equal(t, []schema.DiffMetricRow{
expectedFromKS(t, dks.DigestBlank, dks.DigestB01Pos, fakeNow),
expectedFromKS(t, dks.DigestBlank, dks.DigestB02Pos, fakeNow),
expectedFromKS(t, dks.DigestBlank, dks.DigestB03Neg, fakeNow),
expectedFromKS(t, dks.DigestBlank, dks.DigestB04Neg, fakeNow),
expectedFromKS(t, dks.DigestB01Pos, dks.DigestBlank, fakeNow),
expectedFromKS(t, dks.DigestB01Pos, dks.DigestB02Pos, fakeNow),
expectedFromKS(t, dks.DigestB01Pos, dks.DigestB03Neg, fakeNow),
expectedFromKS(t, dks.DigestB01Pos, dks.DigestB04Neg, fakeNow),
expectedFromKS(t, dks.DigestB02Pos, dks.DigestBlank, fakeNow),
expectedFromKS(t, dks.DigestB02Pos, dks.DigestB01Pos, fakeNow),
expectedFromKS(t, dks.DigestB02Pos, dks.DigestB03Neg, fakeNow),
expectedFromKS(t, dks.DigestB02Pos, dks.DigestB04Neg, fakeNow),
expectedFromKS(t, dks.DigestB03Neg, dks.DigestBlank, fakeNow),
expectedFromKS(t, dks.DigestB03Neg, dks.DigestB01Pos, fakeNow),
expectedFromKS(t, dks.DigestB03Neg, dks.DigestB02Pos, fakeNow),
expectedFromKS(t, dks.DigestB03Neg, dks.DigestB04Neg, fakeNow),
expectedFromKS(t, dks.DigestB04Neg, dks.DigestBlank, fakeNow),
expectedFromKS(t, dks.DigestB04Neg, dks.DigestB01Pos, fakeNow),
expectedFromKS(t, dks.DigestB04Neg, dks.DigestB02Pos, fakeNow),
expectedFromKS(t, dks.DigestB04Neg, dks.DigestB03Neg, fakeNow),
}, actualMetrics)
assert.Empty(t, getAllProblemImageRows(t, db))
}
func TestWorkerImpl_CalculateDiffs_ReadFromPrimaryBranch_SparseData_Success(t *testing.T) {
unittest.LargeTest(t)
fakeNow := time.Date(2021, time.February, 1, 1, 1, 1, 0, time.UTC)
ctx := context.WithValue(context.Background(), now.ContextKey, fakeNow)
db := sqltest.NewCockroachDBForTestsWithProductionSchema(ctx, t)
// Only C03, C04, C05 will be in the last 3 commits for this data.
sparseData := makeSparseData()
require.NoError(t, sqltest.BulkInsertDataTables(ctx, db, sparseData))
waitForSystemTime()
w := New(db, &fsImageSource{root: kitchenSinkRoot(t)}, 3)
grouping := paramtools.Params{
types.CorpusField: dks.RoundCorpus,
types.PrimaryKeyField: dks.CircleTest,
}
require.NoError(t, w.CalculateDiffs(ctx, grouping, []types.Digest{dks.DigestC06Pos_CL}))
actualMetrics := getAllDiffMetricRows(t, db)
assert.Equal(t, []schema.DiffMetricRow{
expectedFromKS(t, dks.DigestC03Unt, dks.DigestC04Unt, fakeNow),
expectedFromKS(t, dks.DigestC03Unt, dks.DigestC05Unt, fakeNow),
expectedFromKS(t, dks.DigestC03Unt, dks.DigestC06Pos_CL, fakeNow),
expectedFromKS(t, dks.DigestC04Unt, dks.DigestC03Unt, fakeNow),
expectedFromKS(t, dks.DigestC04Unt, dks.DigestC05Unt, fakeNow),
expectedFromKS(t, dks.DigestC04Unt, dks.DigestC06Pos_CL, fakeNow),
expectedFromKS(t, dks.DigestC05Unt, dks.DigestC03Unt, fakeNow),
expectedFromKS(t, dks.DigestC05Unt, dks.DigestC04Unt, fakeNow),
expectedFromKS(t, dks.DigestC05Unt, dks.DigestC06Pos_CL, fakeNow),
expectedFromKS(t, dks.DigestC06Pos_CL, dks.DigestC03Unt, fakeNow),
expectedFromKS(t, dks.DigestC06Pos_CL, dks.DigestC04Unt, fakeNow),
expectedFromKS(t, dks.DigestC06Pos_CL, dks.DigestC05Unt, fakeNow),
}, actualMetrics)
assert.Empty(t, getAllProblemImageRows(t, db))
}
func TestWorkerImpl_CalculateDiffs_ImageNotFound_PartialData(t *testing.T) {
unittest.LargeTest(t)
fakeNow := time.Date(2021, time.February, 1, 1, 1, 1, 0, time.UTC)
ctx := context.WithValue(context.Background(), now.ContextKey, fakeNow)
db := sqltest.NewCockroachDBForTestsWithProductionSchema(ctx, t)
waitForSystemTime()
// Set up an image source that can download A01 and A02, but returns error on A04
b01, err := ioutil.ReadFile(filepath.Join(kitchenSinkRoot(t), string(dks.DigestA01Pos)+".png"))
require.NoError(t, err)
b02, err := ioutil.ReadFile(filepath.Join(kitchenSinkRoot(t), string(dks.DigestA02Pos)+".png"))
require.NoError(t, err)
mis := &mocks.ImageSource{}
mis.On("GetImage", testutils.AnyContext, dks.DigestA01Pos).Return(b01, nil)
mis.On("GetImage", testutils.AnyContext, dks.DigestA02Pos).Return(b02, nil)
mis.On("GetImage", testutils.AnyContext, dks.DigestA04Unt).Return(nil, errors.New("not found"))
w := New(db, mis, 2)
grouping := paramtools.Params{
types.CorpusField: "not used",
types.PrimaryKeyField: "not used",
}
imagesToCalculateDiffsFor := []types.Digest{dks.DigestA01Pos, dks.DigestA02Pos, dks.DigestA04Unt}
require.NoError(t, w.CalculateDiffs(ctx, grouping, imagesToCalculateDiffsFor))
actualMetrics := getAllDiffMetricRows(t, db)
// We should see partial success
assert.Equal(t, []schema.DiffMetricRow{
expectedFromKS(t, dks.DigestA01Pos, dks.DigestA02Pos, fakeNow),
expectedFromKS(t, dks.DigestA02Pos, dks.DigestA01Pos, fakeNow),
}, actualMetrics)
actualProblemImageRows := getAllProblemImageRows(t, db)
require.Len(t, actualProblemImageRows, 1)
problem := actualProblemImageRows[0]
assert.Equal(t, string(dks.DigestA04Unt), problem.Digest)
// could be more than 1 because of multiple goroutines running
assert.True(t, problem.NumErrors >= 1)
assert.Contains(t, problem.LatestError, "not found")
assert.Equal(t, fakeNow, problem.ErrorTS)
}
func TestWorkerImpl_CalculateDiffs_CorruptedImage_PartialData(t *testing.T) {
unittest.LargeTest(t)
fakeNow := time.Date(2021, time.February, 2, 2, 2, 2, 0, time.UTC)
ctx := context.WithValue(context.Background(), now.ContextKey, fakeNow)
db := sqltest.NewCockroachDBForTestsWithProductionSchema(ctx, t)
// Write a sentinel error for A04. We expect this to be updated.
existingProblem := schema.Tables{ProblemImages: []schema.ProblemImageRow{{
Digest: string(dks.DigestA04Unt),
NumErrors: 100,
LatestError: "not found",
ErrorTS: time.Date(2020, time.January, 1, 1, 1, 1, 0, time.UTC),
}}}
require.NoError(t, sqltest.BulkInsertDataTables(ctx, db, existingProblem))
waitForSystemTime()
// Set up an image source that can download A01 and A02, but invalid PNG data for A04
b01, err := ioutil.ReadFile(filepath.Join(kitchenSinkRoot(t), string(dks.DigestA01Pos)+".png"))
require.NoError(t, err)
b02, err := ioutil.ReadFile(filepath.Join(kitchenSinkRoot(t), string(dks.DigestA02Pos)+".png"))
require.NoError(t, err)
mis := &mocks.ImageSource{}
mis.On("GetImage", testutils.AnyContext, dks.DigestA01Pos).Return(b01, nil)
mis.On("GetImage", testutils.AnyContext, dks.DigestA02Pos).Return(b02, nil)
mis.On("GetImage", testutils.AnyContext, dks.DigestA04Unt).Return([]byte(`not a png`), nil)
w := New(db, mis, 2)
grouping := paramtools.Params{
types.CorpusField: "not used",
types.PrimaryKeyField: "not used",
}
imagesToCalculateDiffsFor := []types.Digest{dks.DigestA01Pos, dks.DigestA02Pos, dks.DigestA04Unt}
require.NoError(t, w.CalculateDiffs(ctx, grouping, imagesToCalculateDiffsFor))
actualMetrics := getAllDiffMetricRows(t, db)
// We should see partial success
assert.Equal(t, []schema.DiffMetricRow{
expectedFromKS(t, dks.DigestA01Pos, dks.DigestA02Pos, fakeNow),
expectedFromKS(t, dks.DigestA02Pos, dks.DigestA01Pos, fakeNow),
}, actualMetrics)
actualProblemImageRows := getAllProblemImageRows(t, db)
require.Len(t, actualProblemImageRows, 1)
problem := actualProblemImageRows[0]
assert.Equal(t, string(dks.DigestA04Unt), problem.Digest)
// The sentinel value is 100. This should be greater than that because of the new error.
assert.True(t, problem.NumErrors >= 101)
assert.Contains(t, problem.LatestError, "png: invalid format: not a PNG file")
assert.Equal(t, fakeNow, problem.ErrorTS)
}
func TestWorkerImpl_GetTriagedDigests_Success(t *testing.T) {
unittest.LargeTest(t)
ctx := context.Background()
db := sqltest.NewCockroachDBForTestsWithProductionSchema(ctx, t)
require.NoError(t, sqltest.BulkInsertDataTables(ctx, db, dks.Build()))
waitForSystemTime()
w := New(db, nil, 100)
squareGrouping := paramtools.Params{types.CorpusField: dks.CornersCorpus, types.PrimaryKeyField: dks.SquareTest}
triangleGrouping := paramtools.Params{types.CorpusField: dks.CornersCorpus, types.PrimaryKeyField: dks.TriangleTest}
circleGrouping := paramtools.Params{types.CorpusField: dks.RoundCorpus, types.PrimaryKeyField: dks.CircleTest}
actualSquare, err := w.getTriagedDigests(ctx, squareGrouping, 0)
require.NoError(t, err)
assert.ElementsMatch(t, []schema.DigestBytes{
d(dks.DigestA01Pos), d(dks.DigestA02Pos), d(dks.DigestA03Pos), d(dks.DigestA07Pos),
d(dks.DigestA08Pos), d(dks.DigestA09Neg),
}, actualSquare)
actualTriangle, err := w.getTriagedDigests(ctx, triangleGrouping, 0)
require.NoError(t, err)
assert.ElementsMatch(t, []schema.DigestBytes{
d(dks.DigestB01Pos), d(dks.DigestB02Pos), d(dks.DigestB03Neg), d(dks.DigestB04Neg),
}, actualTriangle)
actualCircle, err := w.getTriagedDigests(ctx, circleGrouping, 0)
require.NoError(t, err)
assert.ElementsMatch(t, []schema.DigestBytes{
d(dks.DigestC01Pos), d(dks.DigestC02Pos),
}, actualCircle)
}
func TestWorkerImpl_GetCommonAndRecentDigests_SmokeTest(t *testing.T) {
unittest.LargeTest(t)
ctx := context.Background()
db := sqltest.NewCockroachDBForTestsWithProductionSchema(ctx, t)
require.NoError(t, sqltest.BulkInsertDataTables(ctx, db, dks.Build()))
waitForSystemTime()
w := New(db, nil, 100)
squareGrouping := paramtools.Params{types.CorpusField: dks.CornersCorpus, types.PrimaryKeyField: dks.SquareTest}
actualSquare, err := w.getCommonAndRecentDigests(ctx, squareGrouping)
require.NoError(t, err)
assert.ElementsMatch(t, []schema.DigestBytes{
d(dks.DigestA01Pos), d(dks.DigestA02Pos), d(dks.DigestA03Pos), d(dks.DigestA04Unt),
d(dks.DigestA05Unt), d(dks.DigestA06Unt), d(dks.DigestA07Pos), d(dks.DigestA08Pos),
d(dks.DigestA09Neg),
}, actualSquare)
}
func newWorker2UsingImagesFromKitchenSink(t *testing.T, db *pgxpool.Pool) *WorkerImpl {
return New(db, &fsImageSource{root: kitchenSinkRoot(t)}, 200)
}
var kitchenSinkData = dks.Build()
// expectedFromKS returns the computed diff metric from the kitchen sink data. It replaces the
// default timestamp with the provided timestamp.
func expectedFromKS(t *testing.T, left types.Digest, right types.Digest, ts time.Time) schema.DiffMetricRow {
leftB := d(left)
rightB := d(right)
for _, row := range kitchenSinkData.DiffMetrics {
if bytes.Equal(leftB, row.LeftDigest) && bytes.Equal(rightB, row.RightDigest) {
row.Timestamp = ts
return row
}
}
require.Fail(t, "Could not find diff metrics for %s-%s", left, right)
return schema.DiffMetricRow{}
}
func makeSparseData() schema.Tables {
b := databuilder.TablesBuilder{TileWidth: 1}
// Make a few commits, each on their own tile
b.CommitsWithData().
Insert("337", "whomever", "commit 337", "2020-12-01T00:00:01Z").
Insert("437", "whomever", "commit 437", "2020-12-01T00:00:02Z").
Insert("537", "whomever", "commit 537", "2020-12-01T00:00:03Z").
Insert("637", "whomever", "commit 637", "2020-12-01T00:00:04Z").
Insert("687", "whomever", "commit 687", "2020-12-01T00:00:05Z")
b.SetDigests(map[rune]types.Digest{
// All of these will be untriaged because diffs don't care about triage status
'a': dks.DigestC01Pos,
'b': dks.DigestC02Pos,
'c': dks.DigestC03Unt,
'd': dks.DigestC04Unt,
'e': dks.DigestC05Unt,
})
b.SetGroupingKeys(types.CorpusField, types.PrimaryKeyField)
b.AddTracesWithCommonKeys(paramtools.Params{
types.CorpusField: dks.RoundCorpus,
}).History("abcde").Keys([]paramtools.Params{{types.PrimaryKeyField: dks.CircleTest}}).
OptionsAll(paramtools.Params{"ext": "png"}).
IngestedFrom([]string{"file1", "file2", "file3", "file4", "file5"}, // not used in this test
[]string{"2020-12-01T00:00:05Z", "2020-12-01T00:00:05Z", "2020-12-01T00:00:05Z", "2020-12-01T00:00:05Z", "2020-12-01T00:00:05Z"})
b.AddIgnoreRule("userOne", "userOne", "2020-12-02T0:00:00Z", "nop ignore",
paramtools.ParamSet{"matches": []string{"nothing"}})
rv := b.Build()
rv.DiffMetrics = nil
return rv
}
func d(d types.Digest) schema.DigestBytes {
b, err := sql.DigestToBytes(d)
if err != nil {
panic(err)
}
return b
}
func getAllDiffMetricRows(t *testing.T, db *pgxpool.Pool) []schema.DiffMetricRow {
rows := sqltest.GetAllRows(context.Background(), t, db, "DiffMetrics", &schema.DiffMetricRow{}).([]schema.DiffMetricRow)
for _, r := range rows {
// spot check that we handle arrays correctly.
assert.NotEqual(t, [4]int{0, 0, 0, 0}, r.MaxRGBADiffs)
}
return rows
}
func getAllProblemImageRows(t *testing.T, db *pgxpool.Pool) []schema.ProblemImageRow {
return sqltest.GetAllRows(context.Background(), t, db, "ProblemImages", &schema.ProblemImageRow{}).([]schema.ProblemImageRow)
}
// waitForSystemTime waits for a time greater than the duration mentioned in "AS OF SYSTEM TIME"
// clauses in queries. This way, the queries will be accurate.
func waitForSystemTime() {
time.Sleep(150 * time.Millisecond)
}
// fsImageSource returns an image from the local file system, looking in a given root directory.
type fsImageSource struct {
root string
}
func (f fsImageSource) GetImage(_ context.Context, digest types.Digest) ([]byte, error) {
p := filepath.Join(f.root, string(digest)+".png")
return ioutil.ReadFile(p)
}
func kitchenSinkRoot(t *testing.T) string {
root, err := repo_root.Get()
if err != nil {
require.NoError(t, err)
}
return filepath.Join(root, "golden", "go", "sql", "datakitchensink", "img")
}