blob: ca6480b3d5055785b7700c873c67f3d2e894b1a5 [file] [log] [blame]
package blame
import (
"testing"
assert "github.com/stretchr/testify/require"
"go.skia.org/infra/go/testutils/unittest"
three_devices "go.skia.org/infra/golden/go/testutils/data_three_devices"
)
func TestBlamerGetBlamesForTestThreeDevices(t *testing.T) {
unittest.SmallTest(t)
blamer := blamerWithCalculate(t)
bd := blamer.GetBlamesForTest(three_devices.AlphaTest)
assert.Len(t, bd, 1)
b := bd[0]
assert.NotNil(t, b)
// The AlphaTest becomes untriaged in exactly the third commit for exactly
// one trace, so blame is able to identify that author as the one
// and only culprit.
assert.Equal(t, WeightedBlame{
Author: three_devices.ThirdCommitAuthor,
Prob: 1,
}, *b)
bd = blamer.GetBlamesForTest(three_devices.BetaTest)
assert.Len(t, bd, 1)
b = bd[0]
assert.NotNil(t, b)
// The BetaTest has an untriaged digest in the first commit and is missing
// data after that, so this is the best that blamer can do.
assert.Equal(t, WeightedBlame{
Author: three_devices.FirstCommitAuthor,
Prob: 1,
}, *b)
bd = blamer.GetBlamesForTest("test_that_does_not_exist")
assert.Len(t, bd, 0)
}
func TestBlamerGetBlameThreeDevices(t *testing.T) {
unittest.SmallTest(t)
blamer := blamerWithCalculate(t)
commits := three_devices.MakeTestCommits()
// In the first two commits, this untriaged image doesn't show up
// so GetBlame should return empty.
bd := blamer.GetBlame(three_devices.AlphaTest, three_devices.AlphaUntriaged1Digest, commits[0:2])
assert.NotNil(t, bd)
assert.Equal(t, BlameDistribution{
Freq: []int{},
Old: false,
}, *bd)
// Searching in the whole range should indicates that
// the commit with index 2 (i.e. the third and last one)
// has the blame
bd = blamer.GetBlame(three_devices.AlphaTest, three_devices.AlphaUntriaged1Digest, commits[0:3])
assert.NotNil(t, bd)
assert.Equal(t, BlameDistribution{
Freq: []int{2},
Old: false,
}, *bd)
// The BetaUntriaged1Digest only shows up in the first commit (index 0)
bd = blamer.GetBlame(three_devices.BetaTest, three_devices.BetaUntriaged1Digest, commits[0:3])
assert.NotNil(t, bd)
assert.Equal(t, BlameDistribution{
Freq: []int{0},
Old: false,
}, *bd)
// Good digests have no blame ever
bd = blamer.GetBlame(three_devices.BetaTest, three_devices.BetaGood1Digest, commits[0:3])
assert.NotNil(t, bd)
assert.Equal(t, BlameDistribution{
Freq: []int{},
Old: false,
}, *bd)
// Negative digests have no blame ever
bd = blamer.GetBlame(three_devices.AlphaTest, three_devices.AlphaBad1Digest, commits[0:3])
assert.NotNil(t, bd)
assert.Equal(t, BlameDistribution{
Freq: []int{},
Old: false,
}, *bd)
}
// Returns a Blamer filled out with the data from three_devices.
func blamerWithCalculate(t *testing.T) Blamer {
exp := three_devices.MakeTestExpectations()
blamer, err := New(three_devices.MakeTestTile(), exp)
assert.NoError(t, err)
return blamer
}
const (
// Directory with testdata.
TEST_DATA_DIR = "./testdata"
// Local file location of the test data.
TEST_DATA_PATH = TEST_DATA_DIR + "/goldentile.json.zip"
// Folder in the testdata bucket. See go/testutils for details.
TEST_DATA_STORAGE_PATH = "gold-testdata/goldentile.json.gz"
)
// TestBlamerWithSyntheticData tests a lot of the functionality of
// blamer. TODO(kjlubick) I think it tests too much and should
// be broken up into smaller pieces. Additionally, explaining why
// the data ends up the way it does would aid in readability.
// func TestBlamerWithSyntheticData(t *testing.T) {
// unittest.SmallTest(t)
// start := time.Now().Unix()
// commits := []*tiling.Commit{
// {CommitTime: start + 10, Hash: "h1", Author: "John Doe 1"},
// {CommitTime: start + 20, Hash: "h2", Author: "John Doe 2"},
// {CommitTime: start + 30, Hash: "h3", Author: "John Doe 3"},
// {CommitTime: start + 40, Hash: "h4", Author: "John Doe 4"},
// {CommitTime: start + 50, Hash: "h5", Author: "John Doe 5"},
// }
// params := []map[string]string{
// {types.PRIMARY_KEY_FIELD: "foo", "config": "8888", types.CORPUS_FIELD: "gm"},
// {types.PRIMARY_KEY_FIELD: "foo", "config": "565", types.CORPUS_FIELD: "gm"},
// {types.PRIMARY_KEY_FIELD: "foo", "config": "gpu", types.CORPUS_FIELD: "gm"},
// {types.PRIMARY_KEY_FIELD: "bar", "config": "8888", types.CORPUS_FIELD: "gm"},
// {types.PRIMARY_KEY_FIELD: "bar", "config": "565", types.CORPUS_FIELD: "gm"},
// {types.PRIMARY_KEY_FIELD: "bar", "config": "gpu", types.CORPUS_FIELD: "gm"},
// {types.PRIMARY_KEY_FIELD: "baz", "config": "565", types.CORPUS_FIELD: "gm"},
// {types.PRIMARY_KEY_FIELD: "baz", "config": "gpu", types.CORPUS_FIELD: "gm"},
// }
// DI_1, DI_2, DI_3 := "digest1", "digest2", "digest3"
// DI_4, DI_5, DI_6 := "digest4", "digest5", "digest6"
// DI_7, DI_8, DI_9 := "digest7", "digest8", "digest9"
// MISS := types.MISSING_DIGEST
// digests := [][]string{
// {MISS, MISS, DI_1, MISS, MISS},
// {MISS, DI_1, DI_1, DI_2, MISS},
// {DI_3, MISS, MISS, MISS, MISS},
// {DI_5, DI_4, DI_5, DI_5, DI_5},
// {DI_6, MISS, DI_4, MISS, MISS},
// {MISS, MISS, MISS, MISS, MISS},
// {DI_7, DI_7, MISS, DI_8, MISS},
// {DI_7, MISS, DI_7, DI_8, MISS},
// }
// // Make sure the data are consistent and create a mock TileStore.
// assert.Equal(t, len(commits), len(digests[0]))
// assert.Equal(t, len(digests), len(params))
// eventBus := eventbus.New()
// storages := &storage.Storage{
// ExpectationsStore: expstorage.NewMemExpectationsStore(eventBus),
// MasterTileBuilder: mocks.NewMockTileBuilder(t, digests, params, commits),
// EventBus: eventBus,
// }
// cpxTile, err := storages.GetLastTileTrimmed()
// exp, err := storages.ExpectationsStore.Get()
// assert.NoError(t, err)
// blamer, err := New(cpxTile.GetTile(false), exp)
// assert.NoError(t, err)
// storages.EventBus.SubscribeAsync(expstorage.EV_EXPSTORAGE_CHANGED, func(e interface{}) {
// if err := blamer.Calculate(cpxTile.GetTile(false)); err != nil {
// assert.Fail(t, "Async calculate failed")
// }
// })
// // Check when completely untriaged
// blameLists, _ := blamer.GetAllBlameLists()
// assert.NotNil(t, blameLists)
// assert.Equal(t, 3, len(blameLists))
// assert.Equal(t, 3, len(blameLists["foo"]))
// assert.Equal(t, []int{1, 0, 0, 0}, blameLists["foo"][DI_1].Freq)
// assert.Equal(t, []int{1, 0}, blameLists["foo"][DI_2].Freq)
// assert.Equal(t, []int{1, 0, 0, 0, 0}, blameLists["foo"][DI_3].Freq)
// assert.Equal(t, 3, len(blameLists["bar"]))
// assert.Equal(t, []int{2, 0, 0, 0}, blameLists["bar"][DI_4].Freq)
// assert.Equal(t, []int{1, 0, 0, 0, 0}, blameLists["bar"][DI_5].Freq)
// assert.Equal(t, []int{1, 0, 0, 0, 0}, blameLists["bar"][DI_6].Freq)
// assert.Equal(t, &BlameDistribution{Freq: []int{1}}, blamer.GetBlame("foo", DI_1, commits))
// assert.Equal(t, &BlameDistribution{Freq: []int{3}}, blamer.GetBlame("foo", DI_2, commits))
// assert.Equal(t, &BlameDistribution{Freq: []int{0}}, blamer.GetBlame("foo", DI_3, commits))
// assert.Equal(t, &BlameDistribution{Freq: []int{1}}, blamer.GetBlame("bar", DI_4, commits))
// assert.Equal(t, &BlameDistribution{Freq: []int{0}}, blamer.GetBlame("bar", DI_5, commits))
// assert.Equal(t, &BlameDistribution{Freq: []int{0}}, blamer.GetBlame("bar", DI_6, commits))
// // Classify some digests and re-calculate.
// changes := types.Expectations{
// "foo": map[string]types.Label{DI_1: types.POSITIVE, DI_2: types.NEGATIVE},
// "bar": map[string]types.Label{DI_4: types.POSITIVE, DI_6: types.NEGATIVE},
// }
// assert.NoError(t, storages.ExpectationsStore.AddChange(changes, ""))
// // Wait for the change to propagate.
// waitForChange(t, blamer, blameLists)
// blameLists, _ = blamer.GetAllBlameLists()
// assert.Equal(t, 3, len(blameLists))
// assert.Equal(t, 1, len(blameLists["foo"]))
// assert.Equal(t, []int{1, 0, 0, 0, 0}, blameLists["foo"][DI_3].Freq)
// assert.Equal(t, 1, len(blameLists["bar"]))
// assert.Equal(t, []int{1, 0, 0, 0, 0}, blameLists["bar"][DI_5].Freq)
// assert.Equal(t, []int{1, 2, 0}, blameLists["baz"][DI_8].Freq)
// assert.Equal(t, &BlameDistribution{Freq: []int{0}}, blamer.GetBlame("foo", DI_3, commits))
// assert.Equal(t, &BlameDistribution{Freq: []int{0}}, blamer.GetBlame("bar", DI_5, commits))
// assert.Equal(t, &BlameDistribution{Freq: []int{3}}, blamer.GetBlame("baz", DI_8, commits))
// // Change the underlying tile and trigger with another change.
// tile := storages.MasterTileBuilder.GetTile()
// // Get the trace for the last parameters and set a value.
// gTrace := tile.Traces[mocks.TraceKey(params[5])].(*types.GoldenTrace)
// gTrace.Digests[2] = DI_9
// assert.NoError(t, storages.ExpectationsStore.AddChange(changes, ""))
// // Wait for the change to propagate.
// waitForChange(t, blamer, blameLists)
// blameLists, _ = blamer.GetAllBlameLists()
// assert.Equal(t, 3, len(blameLists))
// assert.Equal(t, 1, len(blameLists["foo"]))
// assert.Equal(t, 2, len(blameLists["bar"]))
// assert.Equal(t, []int{1, 0, 0}, blameLists["bar"][DI_9].Freq)
// assert.Equal(t, &BlameDistribution{Freq: []int{2}}, blamer.GetBlame("bar", DI_9, commits))
// // Simulate the case where the digest is not found in digest store.
// assert.NoError(t, storages.ExpectationsStore.AddChange(changes, ""))
// time.Sleep(10 * time.Millisecond)
// blameLists, _ = blamer.GetAllBlameLists()
// assert.Equal(t, 3, len(blameLists))
// assert.Equal(t, 1, len(blameLists["foo"]))
// assert.Equal(t, 2, len(blameLists["bar"]))
// assert.Equal(t, []int{1, 0, 0}, blameLists["bar"][DI_9].Freq)
// assert.Equal(t, &BlameDistribution{Freq: []int{2}}, blamer.GetBlame("bar", DI_9, commits))
// assert.Equal(t, &BlameDistribution{Freq: []int{1}}, blamer.GetBlame("bar", DI_9, commits[1:4]))
// assert.Equal(t, &BlameDistribution{Freq: []int{}}, blamer.GetBlame("bar", DI_9, commits[0:2]))
// }
// These tests are not ideal in that they depend on blamer being stateful and not
// a stateless helper. TODO(kjlubick): replace them with some functionality
// that tests similar behavior.
// func BenchmarkBlamer(b *testing.B) {
// ctx := context.Background()
// tileBuilder := mocks.GetTileBuilderFromEnv(b, ctx)
// // Get a tile to make sure it's cached.
// tileBuilder.GetTile()
// b.ResetTimer()
// testBlamerWithLiveData(b, tileBuilder)
// }
// func TestBlamerWithLiveData(t *testing.T) {
// unittest.LargeTest(t)
// err := gcs_testutils.DownloadTestDataFile(t, gcs_testutils.TEST_DATA_BUCKET, TEST_DATA_STORAGE_PATH, TEST_DATA_PATH)
// assert.NoError(t, err, "Unable to download testdata.")
// defer testutils.RemoveAll(t, TEST_DATA_DIR)
// tileStore := mocks.NewMockTileBuilderFromJson(t, TEST_DATA_PATH)
// testBlamerWithLiveData(t, tileStore)
// }
// func testBlamerWithLiveData(t assert.TestingT, tileBuilder tracedb.MasterTileBuilder) {
// eventBus := eventbus.New()
// storages := &storage.Storage{
// ExpectationsStore: expstorage.NewMemExpectationsStore(eventBus),
// MasterTileBuilder: tileBuilder,
// EventBus: eventBus,
// }
// blamer := New(storages)
// cpxTile, err := storages.GetLastTileTrimmed()
// assert.NoError(t, err)
// err = blamer.Calculate(cpxTile.GetTile(false))
// assert.NoError(t, err)
// storages.EventBus.SubscribeAsync(expstorage.EV_EXPSTORAGE_CHANGED, func(e interface{}) {
// if err := blamer.Calculate(cpxTile.GetTile(false)); err != nil {
// assert.Fail(t, "Async calculate failed")
// }
// })
// // Wait until we have a blamelist.
// var blameLists map[string]map[string]*BlameDistribution
// for {
// blameLists, _ = blamer.GetAllBlameLists()
// if blameLists != nil {
// break
// }
// }
// tile := storages.MasterTileBuilder.GetTile()
// // Since we set the 'First' timestamp of all digest info entries
// // to Now. We should get a non-empty blamelist of all digests.
// oneTestName := ""
// oneDigest := ""
// forEachTestDigestDo(tile, func(testName, digest string) {
// assert.NotNil(t, blameLists[testName][digest])
// assert.True(t, len(blameLists[testName][digest].Freq) > 0)
// // Remember the last one for later.
// oneTestName, oneDigest = testName, digest
// })
// // Change the classification of one test and trigger the recalculation.
// changes := types.Expectations{
// oneTestName: map[string]types.Label{oneDigest: types.POSITIVE},
// }
// assert.NoError(t, storages.ExpectationsStore.AddChange(changes, ""))
// // Wait for change to propagate.
// waitForChange(t, blamer, blameLists)
// blameLists, _ = blamer.GetAllBlameLists()
// // Assert the correctness of the blamelists.
// forEachTestDigestDo(tile, func(testName, digest string) {
// if (testName == oneTestName) && (digest == oneDigest) {
// assert.Nil(t, blameLists[testName][digest])
// } else {
// assert.NotNil(t, blameLists[testName][digest])
// assert.True(t, len(blameLists[testName][digest].Freq) > 0)
// }
// })
// blameLists, _ = blamer.GetAllBlameLists()
// // Randomly assign labels to the different digests and make sure
// // that the blamelists are correct.
// changes = types.Expectations{}
// choices := []types.Label{types.POSITIVE, types.NEGATIVE, types.UNTRIAGED}
// forEachTestDigestDo(tile, func(testName, digest string) {
// // Randomly skip some digests.
// label := choices[rand.Int()%len(choices)]
// if label != types.UNTRIAGED {
// changes.AddDigest(testName, digest, label)
// }
// })
// // Add the labels and wait for the recalculation.
// assert.NoError(t, storages.ExpectationsStore.AddChange(changes, ""))
// waitForChange(t, blamer, blameLists)
// blameLists, commits := blamer.GetAllBlameLists()
// expectations, err := storages.ExpectationsStore.Get()
// assert.NoError(t, err)
// // Verify that the results are plausible.
// forEachTestTraceDo(tile, func(testName string, values []string) {
// for idx, digest := range values {
// if digest != types.MISSING_DIGEST {
// label := expectations.Classification(testName, digest)
// if label == types.UNTRIAGED {
// bl := blameLists[testName][digest]
// assert.NotNil(t, bl)
// freq := bl.Freq
// assert.True(t, len(freq) > 0)
// startIdx := len(commits) - len(freq)
// assert.True(t, (startIdx >= 0) && (startIdx <= idx), fmt.Sprintf("Expected (%s): Smaller than %d but got %d.", digest, startIdx, idx))
// }
// }
// }
// })
// }
// func waitForChange(t assert.TestingT, blamer Blamer, oldBlameLists map[string]map[string]*BlameDistribution) {
// assert.NoError(t, testutils.EventuallyConsistent(time.Second*10, func() error {
// blameLists, _ := blamer.GetAllBlameLists()
// if !reflect.DeepEqual(blameLists, oldBlameLists) {
// return nil
// }
// return testutils.TryAgainErr
// }))
// }
// func forEachTestDigestDo(tile *tiling.Tile, fn func(string, string)) {
// for _, trace := range tile.Traces {
// gTrace := trace.(*types.GoldenTrace)
// testName := gTrace.Params()[types.PRIMARY_KEY_FIELD]
// for _, digest := range gTrace.Digests {
// if digest != types.MISSING_DIGEST {
// fn(testName, digest)
// }
// }
// }
// }
// func forEachTestTraceDo(tile *tiling.Tile, fn func(string, []string)) {
// tileLen := tile.LastCommitIndex() + 1
// for _, trace := range tile.Traces {
// gTrace := trace.(*types.GoldenTrace)
// testName := gTrace.Params()[types.PRIMARY_KEY_FIELD]
// fn(testName, gTrace.Digests[:tileLen])
// }
// }