blob: d241fcaa14c81ce548d776018829daa8fa8d8e1a [file] [log] [blame]
// Copyright 2021 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package run
import (
"context"
"encoding/hex"
"fmt"
"testing"
"time"
"go.chromium.org/luci/common/clock/testclock"
"go.chromium.org/luci/common/data/stringset"
"go.chromium.org/luci/gae/service/datastore"
"go.chromium.org/luci/grpc/appstatus"
"google.golang.org/grpc/codes"
cfgpb "go.chromium.org/luci/cv/api/config/v2"
"go.chromium.org/luci/cv/internal/common"
"go.chromium.org/luci/cv/internal/configs/prjcfg/prjcfgtest"
"go.chromium.org/luci/cv/internal/cvtesting"
. "github.com/smartystreets/goconvey/convey"
)
func TestCLQueryBuilder(t *testing.T) {
t.Parallel()
Convey("CLQueryBuilder works", t, func() {
ct := cvtesting.Test{}
ctx, cancel := ct.SetUp()
defer cancel()
// getAll asserts that LoadRuns returns Runs with the given RunIDs.
getAll := func(q CLQueryBuilder) common.RunIDs {
keys, err := q.GetAllRunKeys(ctx)
So(err, ShouldBeNil)
// They keys may be different than the Runs because some Runs
// may be filtered out (by isSatisfied).
runs, pageToken, err := q.LoadRuns(ctx)
So(err, ShouldBeNil)
ids := idsOf(runs)
assertCorrectPageToken(q, keys, pageToken)
return ids
}
// makeRun puts a Run and returns the RunID.
makeRun := func(proj string, delay time.Duration, clids ...common.CLID) common.RunID {
createdAt := ct.Clock.Now().Add(delay)
runID := common.MakeRunID(proj, createdAt, 1, []byte{0, byte(delay / time.Millisecond)})
So(datastore.Put(ctx, &Run{ID: runID, CLs: clids}), ShouldBeNil)
for _, clid := range clids {
So(datastore.Put(ctx, &RunCL{
Run: datastore.MakeKey(ctx, common.RunKind, string(runID)),
ID: clid,
IndexedID: clid,
}), ShouldBeNil)
}
return runID
}
clA, clB, clZ := common.CLID(1), common.CLID(2), common.CLID(3)
// The below runs are sorted by RunID; by project then by time from
// latest to earliest.
bond9 := makeRun("bond", 9*time.Millisecond, clA)
bond4 := makeRun("bond", 4*time.Millisecond, clA, clB)
bond2 := makeRun("bond", 2*time.Millisecond, clA)
dart5 := makeRun("dart", 5*time.Millisecond, clA)
dart3 := makeRun("dart", 3*time.Millisecond, clA)
rust1 := makeRun("rust", 1*time.Millisecond, clA, clB)
xero7 := makeRun("xero", 7*time.Millisecond, clA)
Convey("CL without Runs", func() {
qb := CLQueryBuilder{CLID: clZ}
So(getAll(qb), ShouldResemble, common.RunIDs(nil))
})
Convey("CL with some Runs", func() {
qb := CLQueryBuilder{CLID: clB}
So(getAll(qb), ShouldResemble, common.RunIDs{bond4, rust1})
})
Convey("CL with all Runs", func() {
qb := CLQueryBuilder{CLID: clA}
So(getAll(qb), ShouldResemble, common.RunIDs{bond9, bond4, bond2, dart5, dart3, rust1, xero7})
})
Convey("Two CLs, with some Runs", func() {
qb := CLQueryBuilder{CLID: clB, AdditionalCLIDs: common.MakeCLIDsSet(int64(clA))}
So(getAll(qb), ShouldResemble, common.RunIDs{bond4, rust1})
})
Convey("Two CLs with some Runs, other order", func() {
qb := CLQueryBuilder{CLID: clA, AdditionalCLIDs: common.MakeCLIDsSet(int64(clB))}
So(getAll(qb), ShouldResemble, common.RunIDs{bond4, rust1})
})
Convey("Two CLs, with no Runs", func() {
qb := CLQueryBuilder{CLID: clA, AdditionalCLIDs: common.MakeCLIDsSet(int64(clZ))}
So(getAll(qb), ShouldBeEmpty)
})
Convey("Filter by Project", func() {
qb := CLQueryBuilder{CLID: clA, Project: "bond"}
So(getAll(qb), ShouldResemble, common.RunIDs{bond9, bond4, bond2})
})
Convey("Filtering by Project and Min with diff project", func() {
qb := CLQueryBuilder{CLID: clA, Project: "dart", MinExcl: bond4}
So(getAll(qb), ShouldResemble, common.RunIDs{dart5, dart3})
qb = CLQueryBuilder{CLID: clA, Project: "dart", MinExcl: rust1}
_, err := qb.BuildKeysOnly(ctx).Finalize()
So(err, ShouldEqual, datastore.ErrNullQuery)
})
Convey("Filtering by Project and Max with diff project", func() {
qb := CLQueryBuilder{CLID: clA, Project: "dart", MaxExcl: xero7}
So(getAll(qb), ShouldResemble, common.RunIDs{dart5, dart3})
qb = CLQueryBuilder{CLID: clA, Project: "dart", MaxExcl: bond4}
_, err := qb.BuildKeysOnly(ctx).Finalize()
So(err, ShouldEqual, datastore.ErrNullQuery)
})
Convey("Before", func() {
qb := CLQueryBuilder{CLID: clA}.BeforeInProject(bond9)
So(getAll(qb), ShouldResemble, common.RunIDs{bond4, bond2})
qb = CLQueryBuilder{CLID: clA}.BeforeInProject(bond4)
So(getAll(qb), ShouldResemble, common.RunIDs{bond2})
qb = CLQueryBuilder{CLID: clA}.BeforeInProject(bond2)
So(getAll(qb), ShouldResemble, common.RunIDs(nil))
})
Convey("After", func() {
qb := CLQueryBuilder{CLID: clA}.AfterInProject(bond2)
So(getAll(qb), ShouldResemble, common.RunIDs{bond9, bond4})
qb = CLQueryBuilder{CLID: clA}.AfterInProject(bond4)
So(getAll(qb), ShouldResemble, common.RunIDs{bond9})
qb = CLQueryBuilder{CLID: clA}.AfterInProject(bond9)
So(getAll(qb), ShouldResemble, common.RunIDs(nil))
})
Convey("Obeys limit and returns correct page token", func() {
qb := CLQueryBuilder{CLID: clA, Limit: 1}.AfterInProject(bond2)
runs1, pageToken1, err := qb.LoadRuns(ctx)
So(err, ShouldBeNil)
So(idsOf(runs1), ShouldResemble, common.RunIDs{bond9})
So(pageToken1, ShouldNotBeNil)
qb = qb.PageToken(pageToken1)
So(qb.MinExcl, ShouldResemble, bond9)
runs2, pageToken2, err := qb.LoadRuns(ctx)
So(err, ShouldBeNil)
So(idsOf(runs2), ShouldResemble, common.RunIDs{bond4})
So(pageToken2, ShouldNotBeNil)
qb = qb.PageToken(pageToken2)
So(qb.MinExcl, ShouldResemble, bond4)
runs3, pageToken3, err := qb.LoadRuns(ctx)
So(err, ShouldBeNil)
So(runs3, ShouldBeEmpty)
So(pageToken3, ShouldBeNil)
})
Convey("After and Before", func() {
qb := CLQueryBuilder{CLID: clA}.AfterInProject(bond2).BeforeInProject(bond9)
So(getAll(qb), ShouldResemble, common.RunIDs{bond4})
})
Convey("Invalid usage panics", func() {
So(func() { CLQueryBuilder{CLID: clA, Project: "dart"}.BeforeInProject(bond2) }, ShouldPanic)
So(func() { CLQueryBuilder{CLID: clA, Project: "dart"}.AfterInProject(bond2) }, ShouldPanic)
So(func() { CLQueryBuilder{CLID: clA}.AfterInProject(dart3).BeforeInProject(xero7) }, ShouldPanic)
})
})
}
func TestProjectQueryBuilder(t *testing.T) {
t.Parallel()
Convey("ProjectQueryBuilder works", t, func() {
ct := cvtesting.Test{}
ctx, cancel := ct.SetUp()
defer cancel()
getAll := func(qb ProjectQueryBuilder) common.RunIDs {
return execQueryInTestSameRunsAndKeys(ctx, qb)
}
makeRun := func(proj string, delay time.Duration, s Status) common.RunID {
createdAt := ct.Clock.Now().Add(delay)
runID := common.MakeRunID(proj, createdAt, 1, []byte{0, byte(delay / time.Millisecond)})
So(datastore.Put(ctx, &Run{ID: runID, Status: s}), ShouldBeNil)
return runID
}
// RunID below are ordered lexicographically.
bond9 := makeRun("bond", 9*time.Millisecond, Status_RUNNING)
bond4 := makeRun("bond", 4*time.Millisecond, Status_FAILED)
bond2 := makeRun("bond", 2*time.Millisecond, Status_CANCELLED)
xero7 := makeRun("xero", 7*time.Millisecond, Status_RUNNING)
xero6 := makeRun("xero", 6*time.Millisecond, Status_RUNNING)
xero5 := makeRun("xero", 5*time.Millisecond, Status_SUCCEEDED)
Convey("Project without Runs", func() {
qb := ProjectQueryBuilder{Project: "missing"}
So(getAll(qb), ShouldBeEmpty)
})
Convey("Project with some Runs", func() {
qb := ProjectQueryBuilder{Project: "bond"}
So(getAll(qb), ShouldResemble, common.RunIDs{bond9, bond4, bond2})
})
Convey("Obeys limit and returns correct page token", func() {
qb := ProjectQueryBuilder{Project: "bond", Limit: 2}
runs1, pageToken1, err := qb.LoadRuns(ctx)
So(err, ShouldBeNil)
So(idsOf(runs1), ShouldResemble, common.RunIDs{bond9, bond4})
So(pageToken1, ShouldNotBeNil)
qb = qb.PageToken(pageToken1)
So(qb.MinExcl, ShouldResemble, bond4)
runs2, pageToken2, err := qb.LoadRuns(ctx)
So(err, ShouldBeNil)
So(idsOf(runs2), ShouldResemble, common.RunIDs{bond2})
So(pageToken2, ShouldBeNil)
})
Convey("Filters by Status", func() {
Convey("Simple", func() {
qb := ProjectQueryBuilder{Project: "xero", Status: Status_RUNNING}
So(getAll(qb), ShouldResemble, common.RunIDs{xero7, xero6})
qb = ProjectQueryBuilder{Project: "xero", Status: Status_SUCCEEDED}
So(getAll(qb), ShouldResemble, common.RunIDs{xero5})
})
Convey("Status_ENDED_MASK", func() {
qb := ProjectQueryBuilder{Project: "bond", Status: Status_ENDED_MASK}
So(getAll(qb), ShouldResemble, common.RunIDs{bond4, bond2})
Convey("Obeys limit", func() {
qb.Limit = 1
So(getAll(qb), ShouldResemble, common.RunIDs{bond4})
})
})
})
Convey("Min", func() {
qb := ProjectQueryBuilder{Project: "bond", MinExcl: bond9}
So(getAll(qb), ShouldResemble, common.RunIDs{bond4, bond2})
Convey("same as Before", func() {
qb2 := ProjectQueryBuilder{}.Before(bond9)
So(qb, ShouldResemble, qb2)
})
})
Convey("Max", func() {
qb := ProjectQueryBuilder{Project: "bond", MaxExcl: bond2}
So(getAll(qb), ShouldResemble, common.RunIDs{bond9, bond4})
Convey("same as After", func() {
qb2 := ProjectQueryBuilder{}.After(bond2)
So(qb, ShouldResemble, qb2)
})
})
Convey("After .. Before", func() {
Convey("Some", func() {
qb := ProjectQueryBuilder{}.After(bond2).Before(bond9)
So(getAll(qb), ShouldResemble, common.RunIDs{bond4})
})
Convey("Empty", func() {
qb := ProjectQueryBuilder{Project: "bond"}.After(bond4).Before(bond9)
So(getAll(qb), ShouldHaveLength, 0)
})
Convey("Overconstrained", func() {
qb := ProjectQueryBuilder{Project: "bond"}.After(bond9).Before(bond2)
_, err := qb.BuildKeysOnly(ctx).Finalize()
So(err, ShouldEqual, datastore.ErrNullQuery)
})
Convey("With status", func() {
qb := ProjectQueryBuilder{Status: Status_FAILED}.After(bond2).Before(bond9)
So(getAll(qb), ShouldResemble, common.RunIDs{bond4})
qb = ProjectQueryBuilder{Status: Status_SUCCEEDED}.After(bond2).Before(bond9)
So(getAll(qb), ShouldHaveLength, 0)
})
})
Convey("Invalid usage panics", func() {
So(func() { ProjectQueryBuilder{}.BuildKeysOnly(ctx) }, ShouldPanic)
So(func() { ProjectQueryBuilder{Project: "not-bond", MinExcl: bond4}.BuildKeysOnly(ctx) }, ShouldPanic)
So(func() { ProjectQueryBuilder{Project: "not-bond", MaxExcl: bond4}.BuildKeysOnly(ctx) }, ShouldPanic)
So(func() { ProjectQueryBuilder{Project: "not-bond"}.Before(bond4) }, ShouldPanic)
So(func() { ProjectQueryBuilder{Project: "not-bond"}.After(bond4) }, ShouldPanic)
So(func() { ProjectQueryBuilder{}.After(bond4).Before(xero7) }, ShouldPanic)
})
})
}
func TestRecentQueryBuilder(t *testing.T) {
t.Parallel()
Convey("RecentQueryBuilder works", t, func() {
ct := cvtesting.Test{}
ctx, cancel := ct.SetUp()
defer cancel()
// checkOrder verifies this order:
// * DESC Created (== ASC InverseTS, or latest first)
// * ASC Project
// * ASC RunID (the remaining part of RunID)
checkOrder := func(runs []*Run) {
for i := range runs {
if i == 0 {
continue
}
switch l, r := runs[i-1], runs[i]; {
case !l.CreateTime.Equal(r.CreateTime):
So(l.CreateTime, ShouldHappenAfter, r.CreateTime)
case l.ID.LUCIProject() != r.ID.LUCIProject():
So(l.ID.LUCIProject(), ShouldBeLessThan, r.ID.LUCIProject())
default:
// Same CreateTime and Project.
So(l.ID, ShouldBeLessThan, r.ID)
}
}
}
getAllWithPageToken := func(qb RecentQueryBuilder) (common.RunIDs, *PageToken) {
keys, runs, pt := execQueryInTest(ctx, qb)
checkOrder(runs)
// Check that loading Runs returns the same values in the same order.
So(idsOf(runs), ShouldResemble, idsOfKeys(keys))
assertCorrectPageToken(qb, keys, pt)
return idsOfKeys(keys), pt
}
getAll := func(qb RecentQueryBuilder) common.RunIDs {
out, _ := getAllWithPageToken(qb)
return out
}
// Choose epoch such that inverseTS of Run ID has zeros at the end for
// ease of debugging.
epoch := testclock.TestRecentTimeUTC.Truncate(time.Millisecond).Add(498490844 * time.Millisecond)
makeRun := func(project, createdAfter, remainder int) *Run {
remBytes, err := hex.DecodeString(fmt.Sprintf("%02d", remainder))
if err != nil {
panic(err)
}
createTime := epoch.Add(time.Duration(createdAfter) * time.Millisecond)
id := common.MakeRunID(
fmt.Sprintf("p%02d", project),
createTime,
1,
remBytes,
)
return &Run{ID: id, CreateTime: createTime}
}
placeRuns := func(runs ...*Run) common.RunIDs {
ids := make(common.RunIDs, len(runs))
So(datastore.Put(ctx, runs), ShouldBeNil)
projects := stringset.New(10)
for i, r := range runs {
projects.Add(r.ID.LUCIProject())
ids[i] = r.ID
}
for p := range projects {
prjcfgtest.Create(ctx, p, &cfgpb.Config{})
}
return ids
}
Convey("just one project", func() {
expIDs := placeRuns(
// project, creationDelay, hash.
makeRun(1, 90, 11),
makeRun(1, 80, 12),
makeRun(1, 70, 13),
makeRun(1, 70, 14),
makeRun(1, 70, 15),
makeRun(1, 60, 11),
makeRun(1, 60, 12),
)
Convey("without paging", func() {
So(getAll(RecentQueryBuilder{Limit: 128}), ShouldResemble, expIDs)
})
Convey("with paging", func() {
page, next := getAllWithPageToken(RecentQueryBuilder{Limit: 3})
So(page, ShouldResemble, expIDs[:3])
So(getAll(RecentQueryBuilder{Limit: 3}.PageToken(next)), ShouldResemble, expIDs[3:6])
})
Convey("without read access to project", func() {
runs, pageToken, err := RecentQueryBuilder{
CheckProjectAccess: func(ctx context.Context, proj string) (bool, error) {
if proj == expIDs[0].LUCIProject() {
return false, nil
}
return true, nil
},
}.LoadRuns(ctx)
So(err, ShouldBeNil)
So(pageToken, ShouldBeNil)
So(runs, ShouldBeEmpty)
})
})
Convey("two projects with overlapping timestaps", func() {
expIDs := placeRuns(
// project, creationDelay, hash.
makeRun(1, 90, 11),
makeRun(1, 80, 12), // same creationDelay, but smaller project
makeRun(2, 80, 12),
makeRun(2, 80, 13), // later hash
makeRun(2, 70, 13),
makeRun(1, 60, 12),
)
Convey("without paging", func() {
So(getAll(RecentQueryBuilder{Limit: 128}), ShouldResemble, expIDs)
})
Convey("with paging", func() {
page, next := getAllWithPageToken(RecentQueryBuilder{Limit: 2})
So(page, ShouldResemble, expIDs[:2])
So(getAll(RecentQueryBuilder{Limit: 4}.PageToken(next)), ShouldResemble, expIDs[2:6])
})
Convey("without read access to one project", func() {
prj1 := expIDs[0].LUCIProject()
prj2 := expIDs[2].LUCIProject()
runs, pageToken, err := RecentQueryBuilder{
CheckProjectAccess: func(ctx context.Context, proj string) (bool, error) {
if proj == prj1 {
return false, nil
}
return true, nil
},
}.LoadRuns(ctx)
So(err, ShouldBeNil)
So(pageToken, ShouldBeNil)
checkOrder(runs)
for _, r := range runs {
So(r.ID.LUCIProject(), ShouldResemble, prj2)
}
So(idsOf(runs), ShouldResemble, expIDs[2:5])
})
Convey("without read access to any project", func() {
prj1 := expIDs[0].LUCIProject()
prj2 := expIDs[2].LUCIProject()
runs, pageToken, err := RecentQueryBuilder{
CheckProjectAccess: func(ctx context.Context, proj string) (bool, error) {
switch proj {
case prj1, prj2:
return false, nil
default:
return true, nil
}
},
}.LoadRuns(ctx)
So(err, ShouldBeNil)
So(pageToken, ShouldBeNil)
So(runs, ShouldBeEmpty)
})
})
Convey("large scale", func() {
var runs []*Run
for p := 50; p < 60; p++ {
// Distribute # of Runs unevenly across projects.
for c := p - 49; c > 0; c-- {
// Create some Runs with the same start timestamp.
for r := 90; r <= 90+c%3; r++ {
runs = append(runs, makeRun(p, c, r))
}
}
}
placeRuns(runs...)
So(len(runs), ShouldBeLessThan, 128)
_, _ = Println("without paging")
all := getAll(RecentQueryBuilder{Limit: 128})
So(len(all), ShouldEqual, len(runs))
_, _ = Println("with paging")
page, next := getAllWithPageToken(RecentQueryBuilder{Limit: 13})
So(page, ShouldResemble, all[:13])
So(getAll(RecentQueryBuilder{Limit: 7}.PageToken(next)), ShouldResemble, all[13:20])
})
})
}
// TestLoadRunsFromQuery provides additional coverage to loadRunsFromQuery
// which isn't achieved in other tests in this file, where loadRunsFromQuery is
// tested indirectly as part of ...QueryBuilder.LoadRuns().
func TestLoadRunsFromQuery(t *testing.T) {
t.Parallel()
Convey("loadRunsFromQuery", t, func() {
ct := cvtesting.Test{}
ctx, cancel := ct.SetUp()
defer cancel()
makeRun := func(proj string, delay time.Duration, clids ...common.CLID) common.RunID {
createdAt := ct.Clock.Now().Add(delay)
runID := common.MakeRunID(proj, createdAt, 1, []byte{0, byte(delay / time.Millisecond)})
So(datastore.Put(ctx, &Run{ID: runID, CLs: clids}), ShouldBeNil)
for _, clid := range clids {
So(datastore.Put(ctx, &RunCL{
Run: datastore.MakeKey(ctx, common.RunKind, string(runID)),
ID: clid,
IndexedID: clid,
}), ShouldBeNil)
}
return runID
}
clA, clB, clZ := common.CLID(1), common.CLID(2), common.CLID(3)
// RunID below are ordered lexicographically.
bond9 := makeRun("bond", 9*time.Millisecond, clA)
bond4 := makeRun("bond", 4*time.Millisecond, clA, clB)
bond2 := makeRun("bond", 2*time.Millisecond, clA) // ignored by aclChecker
dart5 := makeRun("dart", 5*time.Millisecond, clA)
dart3 := makeRun("dart", 3*time.Millisecond, clA) // ignored by aclChecker
rust8 := makeRun("rust", 8*time.Millisecond, clA, clB) // ignored by aclChecker
rust1 := makeRun("rust", 1*time.Millisecond, clA, clB)
xero7 := makeRun("xero", 7*time.Millisecond, clA)
errNotFound := appstatus.Error(codes.NotFound, "but really, no permission")
aclChecker := &fakeRunChecker{
before: map[common.RunID]error{
bond2: errNotFound,
rust8: errNotFound,
},
after: map[common.RunID]error{
dart3: errNotFound,
},
}
Convey("If there is no limit, page token must not be returned but ACLs must be obeyed", func() {
q := CLQueryBuilder{CLID: clA}
runs, pt, err := loadRunsFromQuery(ctx, q, aclChecker)
So(err, ShouldBeNil)
So(idsOf(runs), ShouldResemble, common.RunIDs{bond9, bond4, dart5, rust1, xero7})
So(pt, ShouldBeNil)
})
Convey("Obeys and provides exactly the limit of Runs when available", func() {
Convey("without ACLs filtering", func() {
q := CLQueryBuilder{CLID: clA, Limit: 3}
runs, pt, err := loadRunsFromQuery(ctx, q)
So(err, ShouldBeNil)
So(idsOf(runs), ShouldResemble, common.RunIDs{bond9, bond4, bond2})
So(pt, ShouldNotBeNil)
})
Convey("even with ACLs filtering", func() {
Convey("limit=3", func() {
q := CLQueryBuilder{CLID: clA, Limit: 3}
runs, pt, err := loadRunsFromQuery(ctx, q, aclChecker)
So(err, ShouldBeNil)
So(idsOf(runs), ShouldResemble, common.RunIDs{bond9, bond4, dart5})
_, _ = Println("and chooses pagetoken to maximally avoid redundant work")
// NOTE: the second behind-the-scenes query should have fetched
// keys for {dart5,dart3,rust8}.
So(pt.GetRun(), ShouldResemble, string(rust8))
q = q.PageToken(pt)
runs, pt, err = loadRunsFromQuery(ctx, q, aclChecker)
So(err, ShouldBeNil)
So(idsOf(runs), ShouldResemble, common.RunIDs{rust1, xero7})
So(pt, ShouldBeNil)
})
Convey("limit=4", func() {
q := CLQueryBuilder{CLID: clA, Limit: 4}
runs, pt, err := loadRunsFromQuery(ctx, q, aclChecker)
So(err, ShouldBeNil)
So(idsOf(runs), ShouldResemble, common.RunIDs{bond9, bond4, dart5, rust1})
_, _ = Println("and chooses pagetoken to maximally avoid redundant work")
// NOTE: the second behind-the-scenes query should have fetched
// keys for {dart3,rust8,rust1,xero7} but xero7 didn't fit into
// `runs`, thus next time it's sufficient to start with >rust1.
So(pt.GetRun(), ShouldResemble, string(rust1))
q = q.PageToken(pt)
runs, pt, err = loadRunsFromQuery(ctx, q, aclChecker)
So(err, ShouldBeNil)
So(idsOf(runs), ShouldResemble, common.RunIDs{xero7})
So(pt, ShouldBeNil)
})
Convey("limit=5", func() {
q := CLQueryBuilder{CLID: clA, Limit: 5}
runs, pt, err := loadRunsFromQuery(ctx, q, aclChecker)
So(err, ShouldBeNil)
So(idsOf(runs), ShouldResemble, common.RunIDs{bond9, bond4, dart5, rust1, xero7})
// NOTE: the second behind-the-scenes query should have fetched
// keys for {rust8,rust1,xero7}, and thus exhausted the search.
So(pt, ShouldBeNil)
})
})
})
Convey("If there are exactly `limit` Runs, page token may be returned", func() {
q := CLQueryBuilder{CLID: clB, Limit: 3}
runs, pt, err := loadRunsFromQuery(ctx, q, aclChecker)
So(err, ShouldBeNil)
So(idsOf(runs), ShouldResemble, common.RunIDs{bond4, rust1})
if pt != nil {
// If page token is returned, then next page must be empty.
q = q.PageToken(pt)
runs, pt, err = loadRunsFromQuery(ctx, q, aclChecker)
So(err, ShouldBeNil)
So(runs, ShouldBeEmpty)
So(pt, ShouldBeNil)
}
})
Convey("If there are no Runs, then limit is not obeyed", func() {
q := CLQueryBuilder{CLID: clZ, Limit: 1}
runs, pt, err := loadRunsFromQuery(ctx, q, aclChecker)
So(err, ShouldBeNil)
So(runs, ShouldBeEmpty)
So(pt, ShouldBeNil)
})
Convey("Limits looping when most Runs are filtered out, returning partial page instead", func() {
// Create batches Runs referencing the same CL with every batch having 1
// visible Run and many not visible ones.
// It's important that each batch has a shared project name prefix,
// s.t. that CLQueryBuilder iterates Runs in the order of batches.
const batchesN = queryStopAfterIterations + 2
const invisibleN = 7
const visibleSuffix = "visible"
const invisibleSuffix = "zzz-no-access" // must be after visibleSuffix lexicographically
var visible, invisible common.RunIDs
visibleSet := stringset.New(batchesN)
clX := common.CLID(25)
for i := 1; i < batchesN; i++ {
creationDelay := time.Duration(i) * time.Millisecond
id := makeRun(fmt.Sprintf("%d-%s", i, visibleSuffix), creationDelay, clX)
visible = append(visible, id)
visibleSet.Add(string(id))
for j := 1; j <= invisibleN; j++ {
id := makeRun(fmt.Sprintf("%d-%s-%02d", i, invisibleSuffix, j), creationDelay, clX)
invisible = append(invisible, id)
}
}
// Simulate extremely slow ACL checks.
const aclCheckDuration = queryStopAfterDuration / 20
// Quick check test setup: the queryStopAfterDuration must be hit before
// queryStopAfterIterations are done.
So(aclCheckDuration*(invisibleN+1)*queryStopAfterIterations, ShouldBeGreaterThan, queryStopAfterDuration)
aclChecker.beforeFunc = func(id common.RunID) error {
ct.Clock.Add(aclCheckDuration)
if visibleSet.Has(string(id)) {
return nil
}
return errNotFound
}
tStart := ct.Clock.Now()
// Run the query s.t. every iteration it loads an entire batch.
q := CLQueryBuilder{CLID: clX, Limit: invisibleN + 1}
runs, pt, err := loadRunsFromQuery(ctx, q, aclChecker)
took := ct.Clock.Now().Sub(tStart)
So(err, ShouldBeNil)
// Shouldn't have terminated prematurely.
So(took, ShouldBeGreaterThanOrEqualTo, queryStopAfterDuration)
// It should have loaded `queryStopAfterIterations` of batches, each
// having 1 visible Run.
So(idsOf(runs), ShouldResemble, visible[:queryStopAfterIterations])
// Which must be less than the requested limit.
So(len(runs), ShouldBeLessThan, q.Limit)
// Quick check test assumption.
So(invisibleSuffix, ShouldBeGreaterThan, visibleSuffix)
// Thus the page token must point to the last Run among invisible ones.
So(pt.GetRun(), ShouldResemble, string(invisible[(queryStopAfterIterations*invisibleN)-1]))
})
})
}
type projQueryInTest interface {
runKeysQuery
LoadRuns(context.Context, ...LoadRunChecker) ([]*Run, *PageToken, error)
}
// execQueryInTest calls GetAllRunKeys and LoadRuns and returns the Run keys,
// Runs and page token.
func execQueryInTest(ctx context.Context, q projQueryInTest, checkers ...LoadRunChecker) ([]*datastore.Key, []*Run, *PageToken) {
keys, err := q.GetAllRunKeys(ctx)
So(err, ShouldBeNil)
runs, pageToken, err := q.LoadRuns(ctx, checkers...)
So(err, ShouldBeNil)
return keys, runs, pageToken
}
// execQueryInTestSameRunsAndKeysWithPageToken asserts that the Run keys and
// Runs match; then returns the IDs and page token.
func execQueryInTestSameRunsAndKeysWithPageToken(ctx context.Context, q projQueryInTest, checkers ...LoadRunChecker) (common.RunIDs, *PageToken) {
keys, runs, pageToken := execQueryInTest(ctx, q, checkers...)
ids := idsOfKeys(keys)
So(ids, ShouldResemble, idsOf(runs))
assertCorrectPageToken(q, keys, pageToken)
return ids, pageToken
}
// execQueryInTestSameRunsAndKeys asserts that the Run keys and Runs match;
// then returns just the IDs.
func execQueryInTestSameRunsAndKeys(ctx context.Context, q projQueryInTest) common.RunIDs {
ids, _ := execQueryInTestSameRunsAndKeysWithPageToken(ctx, q)
return ids
}
// assertCorrectPageToken asserts that page token is as expected.
//
// That is, page token should be nil if the number of keys is less than the
// limit (or if there's no limit); and page token should have the correct value
// when the number of keys is equal to the limit.
func assertCorrectPageToken(q runKeysQuery, keys []*datastore.Key, pageToken *PageToken) {
if l := len(keys); q.qLimit() <= 0 || l < int(q.qLimit()) {
So(pageToken, ShouldBeNil)
} else {
So(pageToken.GetRun(), ShouldResemble, keys[l-1].StringID())
}
}
func idsOf(runs []*Run) common.RunIDs {
if len(runs) == 0 {
return nil
}
out := make(common.RunIDs, len(runs))
for i, r := range runs {
out[i] = r.ID
}
return out
}
func idsOfKeys(keys []*datastore.Key) common.RunIDs {
if len(keys) == 0 {
return nil
}
out := make(common.RunIDs, len(keys))
for i, k := range keys {
out[i] = common.RunID(k.StringID())
}
return out
}