blob: deebf105c88cd6aee0b539a2d8f72e9618bdf408 [file] [log] [blame]
package tryjobstore
import (
"context"
"errors"
"fmt"
"sort"
"testing"
"time"
assert "github.com/stretchr/testify/require"
"go.skia.org/infra/go/deepequal"
"go.skia.org/infra/go/ds"
"go.skia.org/infra/go/ds/testutil"
"go.skia.org/infra/go/eventbus"
"go.skia.org/infra/go/testutils"
"go.skia.org/infra/go/testutils/unittest"
"go.skia.org/infra/go/util"
"go.skia.org/infra/golden/go/expstorage"
"go.skia.org/infra/golden/go/expstorage/ds_expstore"
"go.skia.org/infra/golden/go/types"
)
func TestCloudTryjobStore(t *testing.T) {
unittest.LargeTest(t)
// Otherwise try and connect to a locally running emulator.
cleanup := testutil.InitDatastore(t,
ds.ISSUE,
ds.TRYJOB,
ds.TRYJOB_RESULT,
ds.TRYJOB_EXP_CHANGE,
ds.TEST_DIGEST_EXP)
defer cleanup()
eventBus := eventbus.New()
estore, err := ds_expstore.DeprecatedNew(ds.DS, eventBus)
assert.NoError(t, err)
store, err := NewCloudTryjobStore(ds.DS, eventBus)
assert.NoError(t, err)
testTryjobStore(t, store, estore)
}
func testTryjobStore(t *testing.T, store TryjobStore, estore expstorage.ExpectationsStore) {
// Add the issue and two tryjobs to the store.
issueID := int64(99)
patchsetID := int64(1099)
buildBucketID := int64(30099)
ctx := context.Background()
// Note: Cloud datastore only stores up to microseconds correctly, so if we
// kept the time down to nanoseconds the test would fail. So we drop everything
// smaller than a second.
nowSec := time.Unix(time.Now().Unix(), 0)
tryjob_1 := &Tryjob{
IssueID: issueID,
PatchsetID: patchsetID,
Builder: "Test-Builder-1",
BuildBucketID: buildBucketID,
Status: TRYJOB_RUNNING,
Updated: nowSec,
}
patchsetID_2 := int64(1200)
buildBucketID_2 := int64(30199)
tryjob_2 := &Tryjob{
IssueID: issueID,
PatchsetID: patchsetID_2,
Builder: "Test-Builder-2",
BuildBucketID: buildBucketID_2,
Status: TRYJOB_COMPLETE,
Updated: nowSec,
}
buildBucketID_3 := int64(40199)
tryjob_3 := &Tryjob{
IssueID: issueID,
PatchsetID: patchsetID_2,
Builder: "Test-Builder-2",
BuildBucketID: buildBucketID_3,
Status: TRYJOB_COMPLETE,
Updated: nowSec.Add(-time.Hour),
}
// Delete the tryjobs from the datastore.
issue := &Issue{
ID: issueID,
Subject: "Test issue",
Owner: "jdoe@example.com",
Updated: time.Now(),
Status: "",
PatchsetDetails: []*PatchsetDetail{
{ID: patchsetID},
{ID: patchsetID_2},
},
}
assert.NoError(t, store.UpdateIssue(issue, nil))
// Insert the tryjobs into the datastore.
assert.NoError(t, store.UpdateTryjob(0, tryjob_1, nil))
found, err := store.GetTryjob(issueID, buildBucketID)
assert.NoError(t, err)
found.Key = nil
assert.Equal(t, tryjob_1.Updated, found.Updated)
assert.Equal(t, tryjob_1, found)
assert.NoError(t, store.UpdateTryjob(0, tryjob_2, nil))
expTryjobs := []*Tryjob{tryjob_1, tryjob_2}
foundTryjobs := []*Tryjob{}
assert.NoError(t, testutils.EventuallyConsistent(5*time.Second, func() error {
foundIssue, err := store.GetIssue(issueID, true)
assert.NoError(t, err)
assert.NotNil(t, foundIssue)
for _, ps := range foundIssue.PatchsetDetails {
for _, tj := range ps.Tryjobs {
tj.Key = nil
}
foundTryjobs = append(foundTryjobs, ps.Tryjobs...)
}
if len(foundTryjobs) != len(expTryjobs) {
return testutils.TryAgainErr
}
return nil
}))
deepequal.AssertDeepEqual(t, expTryjobs, foundTryjobs)
listedIssues, total, err := store.ListIssues(0, 1000)
assert.NoError(t, err)
assert.Equal(t, 1, len(listedIssues))
assert.Equal(t, 1, total)
checkEqualIssue(t, issue, listedIssues[0])
// Generate instances of results
allTryjobs := []*Tryjob{tryjob_1, tryjob_2}
tryjobResults := make([][]*TryjobResult, len(allTryjobs))
for idx, tj := range allTryjobs {
digestStart := int64((idx + 1) * 1000)
results := []*TryjobResult{}
for i := 0; i < 5; i++ {
digestStr := fmt.Sprintf("%010d", digestStart+int64(i))
testName := fmt.Sprintf("test-%d", i%5)
results = append(results, &TryjobResult{
BuildBucketID: tj.BuildBucketID,
Digest: types.Digest("digest-" + digestStr),
TestName: types.TestName(testName),
Params: map[string][]string{
"name": {testName},
"param-1": {"value-1-1-" + digestStr, "value-1-2-" + digestStr},
"param-2": {"value-2-1" + digestStr, "value-2-2-" + digestStr},
},
})
}
assert.NoError(t, store.UpdateTryjobResult(results))
tryjobResults[idx] = results
}
var foundTJs []*Tryjob
var foundTJResults [][]*TryjobResult
assert.NoError(t, testutils.EventuallyConsistent(3*time.Second, func() error {
foundTJs, foundTJResults, err = store.GetTryjobs(issueID, []int64{patchsetID, patchsetID_2}, false, true)
if err != nil {
return err
}
if len(allTryjobs) != len(foundTJs) {
return testutils.TryAgainErr
}
return nil
}))
for idx := range allTryjobs {
foundTJs[idx].Key = nil
assert.Equal(t, allTryjobs[idx], foundTJs[idx])
tjr := foundTJResults[idx]
sort.Slice(tjr, func(i, j int) bool { return tjr[i].Digest < tjr[j].Digest })
assert.Equal(t, tryjobResults[idx], tjr)
}
// Add a redundant Tryjob make sure it's not filtered out.
assert.NoError(t, store.UpdateTryjob(0, tryjob_3, nil))
assert.NoError(t, testutils.EventuallyConsistent(10*time.Second, func() error {
foundTJs, _, err := store.GetTryjobs(issueID, []int64{patchsetID, patchsetID_2}, false, false)
assert.NoError(t, err)
if len(foundTJs) == len(allTryjobs)+1 {
return nil
}
return testutils.TryAgainErr
}))
foundTJs, _, err = store.GetTryjobs(issueID, []int64{patchsetID, patchsetID_2}, false, false)
assert.NoError(t, err)
assert.Equal(t, len(allTryjobs)+1, len(foundTJs))
// Filter out duplicates
foundTJs, _, err = store.GetTryjobs(issueID, []int64{patchsetID, patchsetID_2}, true, false)
assert.NoError(t, err)
assert.Equal(t, len(allTryjobs), len(foundTJs))
for idx := range allTryjobs {
foundTJs[idx].Key = nil
assert.Equal(t, allTryjobs[idx], foundTJs[idx])
}
// Add changes to the issue
allChanges := types.Expectations{}
// TODO(kjlubick): assert something with expLogEntries - it is only added to.
expLogEntries := []expstorage.TriageLogEntry{}
userName := "jdoe@example.com"
expStore := estore.ForIssue(issueID)
for i := 0; i < 5; i++ {
triageDetails := []expstorage.TriageDetail{}
changes := types.Expectations{}
for testCount := 0; testCount < 5; testCount++ {
testName := types.TestName(fmt.Sprintf("test-%04d", testCount))
for digestCount := 0; digestCount < 5; digestCount++ {
digest := types.Digest(fmt.Sprintf("digest-%04d-%04d", testCount, digestCount))
label := types.Label((i + testCount + digestCount) % 3)
changes.AddDigest(testName, digest, label)
triageDetails = append(triageDetails, expstorage.TriageDetail{
TestName: testName, Digest: digest, Label: label.String(),
})
}
}
assert.NoError(t, expStore.AddChange(ctx, changes, userName))
allChanges.MergeExpectations(changes)
expLogEntries = append(expLogEntries, expstorage.TriageLogEntry{
Name: userName, ChangeCount: len(triageDetails), Details: triageDetails,
})
}
var foundTestExp types.Expectations
assert.NoError(t, testutils.EventuallyConsistent(10*time.Second, func() error {
foundTestExp, err = expStore.Get()
assert.NoError(t, err)
if !deepequal.DeepEqual(allChanges, foundTestExp) {
return testutils.TryAgainErr
}
return nil
}))
assert.Equal(t, allChanges, foundTestExp)
logEntries, total, err := expStore.QueryLog(ctx, 0, -1, false)
assert.NoError(t, err)
assert.Equal(t, 5, total)
assert.Equal(t, 5, len(logEntries))
// Flip all expectations to untriaged.
for _, digests := range foundTestExp {
for digest := range digests {
digests[digest] = types.UNTRIAGED
}
}
assert.NoError(t, expStore.AddChange(ctx, foundTestExp, userName))
var untriagedTestExp types.Expectations
assert.NoError(t, testutils.EventuallyConsistent(3*time.Second, func() error {
untriagedTestExp, err = expStore.Get()
assert.NoError(t, err)
for testName, digests := range foundTestExp {
for digest, label := range digests {
if label != untriagedTestExp[testName][digest] {
return testutils.TryAgainErr
}
}
}
return nil
}))
// Test commiting where the commit fails.
assert.Error(t, store.CommitIssueExp(issueID, func() error {
return errors.New("Write failed")
}))
foundIssue, err := store.GetIssue(issueID, false)
assert.NoError(t, err)
assert.False(t, foundIssue.Committed)
// Test commiting the changes.
assert.NoError(t, store.CommitIssueExp(issueID, func() error {
// Assume that writing the master baseline works.
return nil
}))
foundIssue, err = store.GetIssue(issueID, false)
assert.NoError(t, err)
assert.True(t, foundIssue.Committed)
}
func checkEqualIssue(t *testing.T, exp *Issue, actual *Issue) {
expCp := *exp
actCp := *actual
expCp.Updated = normalizeTimeToMs(expCp.Updated)
actCp.Updated = normalizeTimeToMs(actCp.Updated)
assert.Equal(t, &expCp, &actCp)
}
func normalizeTimeToMs(t time.Time) time.Time {
unixNano := t.UnixNano()
secs := unixNano / int64(time.Second)
newNanoRemainder := ((unixNano % int64(time.Second)) / int64(time.Millisecond)) * int64(time.Millisecond)
return time.Unix(secs, newNanoRemainder)
}
func TestTryjobJsonCodec(t *testing.T) {
unittest.SmallTest(t)
tryjob_1 := &Tryjob{
IssueID: 12345,
PatchsetID: 9,
Builder: "Test-Builder-1",
BuildBucketID: 45409309403,
Status: TRYJOB_RUNNING,
}
// Create a codec like we do for firing events and test the round trip.
codec := util.JSONCodec(&Tryjob{})
jsonBytes, err := codec.Encode(tryjob_1)
assert.NoError(t, err)
foundInterface, err := codec.Decode(jsonBytes)
assert.NoError(t, err)
assert.IsType(t, foundInterface, &Tryjob{})
assert.Equal(t, tryjob_1, foundInterface.(*Tryjob))
}