go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/buildbucket/appengine/internal/search/query_test.go (about)

     1  // Copyright 2020 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package search
    16  
    17  import (
    18  	"container/heap"
    19  	"context"
    20  	"sort"
    21  	"testing"
    22  	"time"
    23  
    24  	"google.golang.org/grpc/codes"
    25  	"google.golang.org/protobuf/types/known/timestamppb"
    26  
    27  	"go.chromium.org/luci/auth/identity"
    28  	"go.chromium.org/luci/common/clock/testclock"
    29  	"go.chromium.org/luci/common/data/stringset"
    30  	"go.chromium.org/luci/common/data/strpair"
    31  	"go.chromium.org/luci/common/logging/memlogger"
    32  	"go.chromium.org/luci/gae/impl/memory"
    33  	"go.chromium.org/luci/gae/service/datastore"
    34  	"go.chromium.org/luci/server/auth"
    35  	"go.chromium.org/luci/server/auth/authtest"
    36  
    37  	bb "go.chromium.org/luci/buildbucket"
    38  	"go.chromium.org/luci/buildbucket/appengine/model"
    39  	"go.chromium.org/luci/buildbucket/bbperms"
    40  	pb "go.chromium.org/luci/buildbucket/proto"
    41  
    42  	. "github.com/smartystreets/goconvey/convey"
    43  
    44  	. "go.chromium.org/luci/common/testing/assertions"
    45  )
    46  
    47  const userID = identity.Identity("user:user@example.com")
    48  
    49  func TestNewSearchQuery(t *testing.T) {
    50  	t.Parallel()
    51  
    52  	Convey("NewQuery", t, func() {
    53  		Convey("valid input", func() {
    54  			gerritChanges := make([]*pb.GerritChange, 2)
    55  			gerritChanges[0] = &pb.GerritChange{
    56  				Host:     "a",
    57  				Project:  "b",
    58  				Change:   1,
    59  				Patchset: 1,
    60  			}
    61  			gerritChanges[1] = &pb.GerritChange{
    62  				Host:     "a",
    63  				Project:  "b",
    64  				Change:   2,
    65  				Patchset: 1,
    66  			}
    67  			tags := []*pb.StringPair{
    68  				{Key: "k1", Value: "v1"},
    69  			}
    70  			req := &pb.SearchBuildsRequest{
    71  				Predicate: &pb.BuildPredicate{
    72  					Builder: &pb.BuilderID{
    73  						Project: "infra",
    74  						Bucket:  "ci",
    75  						Builder: "test",
    76  					},
    77  					Status:        pb.Status_ENDED_MASK,
    78  					GerritChanges: gerritChanges,
    79  					CreatedBy:     "abc@test.com",
    80  					Tags:          tags,
    81  					CreateTime: &pb.TimeRange{
    82  						StartTime: &timestamppb.Timestamp{Seconds: 1592701200},
    83  						EndTime:   &timestamppb.Timestamp{Seconds: 1592704800},
    84  					},
    85  					Build: &pb.BuildRange{
    86  						StartBuildId: 200,
    87  						EndBuildId:   100,
    88  					},
    89  					Canary:       pb.Trinary_YES,
    90  					DescendantOf: 2,
    91  				},
    92  			}
    93  			query := NewQuery(req)
    94  
    95  			expectedStartTime := time.Unix(1592701200, 0).UTC()
    96  			expectedEndTime := time.Unix(1592704800, 0).UTC()
    97  			expectedTags := strpair.Map{
    98  				"k1":       []string{"v1"},
    99  				"buildset": []string{"patch/gerrit/a/1/1", "patch/gerrit/a/2/1"},
   100  			}
   101  			expectedBuilder := &pb.BuilderID{
   102  				Project: "infra",
   103  				Bucket:  "ci",
   104  				Builder: "test",
   105  			}
   106  
   107  			So(query, ShouldResemble, &Query{
   108  				Builder:   expectedBuilder,
   109  				Tags:      expectedTags,
   110  				Status:    pb.Status_ENDED_MASK,
   111  				CreatedBy: identity.Identity("user:abc@test.com"),
   112  				StartTime: expectedStartTime,
   113  				EndTime:   expectedEndTime,
   114  				ExperimentFilters: stringset.NewFromSlice(
   115  					"+"+bb.ExperimentBBCanarySoftware,
   116  					"-"+bb.ExperimentNonProduction,
   117  				),
   118  				BuildIDHigh:  201,
   119  				BuildIDLow:   99,
   120  				DescendantOf: 2,
   121  				PageSize:     100,
   122  				PageToken:    "",
   123  			})
   124  		})
   125  
   126  		Convey("empty req", func() {
   127  			So(NewQuery(&pb.SearchBuildsRequest{}), ShouldResemble, &Query{PageSize: 100})
   128  		})
   129  
   130  		Convey("empty predict", func() {
   131  			req := &pb.SearchBuildsRequest{
   132  				PageToken: "aa",
   133  				PageSize:  2,
   134  			}
   135  			query := NewQuery(req)
   136  
   137  			So(query, ShouldResemble, &Query{
   138  				PageSize:  2,
   139  				PageToken: "aa",
   140  			})
   141  		})
   142  
   143  		Convey("empty identity", func() {
   144  			req := &pb.SearchBuildsRequest{
   145  				Predicate: &pb.BuildPredicate{
   146  					CreatedBy: "",
   147  				},
   148  			}
   149  			query := NewQuery(req)
   150  
   151  			So(query.CreatedBy, ShouldEqual, identity.Identity(""))
   152  		})
   153  
   154  		Convey("invalid create time", func() {
   155  			req := &pb.SearchBuildsRequest{
   156  				Predicate: &pb.BuildPredicate{
   157  					CreatedBy: string(identity.AnonymousIdentity),
   158  					CreateTime: &pb.TimeRange{
   159  						StartTime: &timestamppb.Timestamp{Seconds: int64(253402300801)},
   160  					},
   161  				},
   162  			}
   163  			So(func() { NewQuery(req) }, ShouldPanic)
   164  		})
   165  	})
   166  }
   167  
   168  func TestFixPageSize(t *testing.T) {
   169  	t.Parallel()
   170  
   171  	Convey("normal page size", t, func() {
   172  		So(fixPageSize(200), ShouldEqual, 200)
   173  	})
   174  
   175  	Convey("default page size", t, func() {
   176  		So(fixPageSize(0), ShouldEqual, 100)
   177  	})
   178  
   179  	Convey("max page size", t, func() {
   180  		So(fixPageSize(1500), ShouldEqual, 1000)
   181  	})
   182  }
   183  
   184  func TestMustTimestamp(t *testing.T) {
   185  	t.Parallel()
   186  	Convey("normal timestamp", t, func() {
   187  		res := mustTimestamp(&timestamppb.Timestamp{Seconds: 1592701200})
   188  		So(res, ShouldEqual, time.Unix(1592701200, 0).UTC())
   189  	})
   190  	Convey("invalid timestamp", t, func() {
   191  		So(func() { mustTimestamp(&timestamppb.Timestamp{Seconds: 253402300801}) }, ShouldPanic)
   192  	})
   193  	Convey("nil timestamp", t, func() {
   194  		res := mustTimestamp(nil)
   195  		So(res.IsZero(), ShouldBeTrue)
   196  	})
   197  }
   198  
   199  func experiments(canary, experimental bool) (ret []string) {
   200  	if canary {
   201  		ret = append(ret, "+"+bb.ExperimentBBCanarySoftware)
   202  	} else {
   203  		ret = append(ret, "-"+bb.ExperimentBBCanarySoftware)
   204  	}
   205  
   206  	if experimental {
   207  		ret = append(ret, "+"+bb.ExperimentNonProduction)
   208  	}
   209  	return
   210  }
   211  
   212  func TestMainFetchFlow(t *testing.T) {
   213  	t.Parallel()
   214  
   215  	Convey("Fetch", t, func() {
   216  		ctx := memory.Use(context.Background())
   217  		ctx = memlogger.Use(ctx)
   218  		datastore.GetTestable(ctx).AutoIndex(true)
   219  		datastore.GetTestable(ctx).Consistent(true)
   220  
   221  		So(datastore.Put(
   222  			ctx,
   223  			&model.Bucket{
   224  				Parent: model.ProjectKey(ctx, "project"),
   225  				ID:     "bucket",
   226  				Proto:  &pb.Bucket{},
   227  			},
   228  			&model.Builder{
   229  				Parent: model.BucketKey(ctx, "project", "bucket"),
   230  				ID:     "builder",
   231  				Config: &pb.BuilderConfig{Name: "builder"},
   232  			},
   233  		), ShouldBeNil)
   234  
   235  		query := NewQuery(&pb.SearchBuildsRequest{
   236  			Predicate: &pb.BuildPredicate{
   237  				Builder: &pb.BuilderID{
   238  					Project: "project",
   239  					Bucket:  "bucket",
   240  					Builder: "builder",
   241  				},
   242  			},
   243  		})
   244  
   245  		Convey("No permission for requested bucketId", func() {
   246  			ctx = auth.WithState(ctx, &authtest.FakeState{
   247  				Identity: userID,
   248  			})
   249  			_, err := query.Fetch(ctx)
   250  			So(err, ShouldHaveAppStatus, codes.NotFound, "not found")
   251  		})
   252  
   253  		Convey("With read permission", func() {
   254  			ctx = auth.WithState(ctx, &authtest.FakeState{
   255  				Identity: userID,
   256  				FakeDB: authtest.NewFakeDB(
   257  					authtest.MockPermission(userID, "project:bucket", bbperms.BuildersList),
   258  					authtest.MockPermission(userID, "project:bucket", bbperms.BuildsList),
   259  				),
   260  			})
   261  
   262  			Convey("Fetch via TagIndex flow", func() {
   263  				query.Tags = strpair.ParseMap([]string{"buildset:1"})
   264  				actualRsp, err := query.Fetch(ctx)
   265  				So(err, ShouldBeNil)
   266  				So(actualRsp, ShouldResembleProto, &pb.SearchBuildsResponse{})
   267  			})
   268  
   269  			Convey("Fetch via Build flow", func() {
   270  				So(datastore.Put(ctx, &model.Build{
   271  					Proto: &pb.Build{
   272  						Id: 1,
   273  						Builder: &pb.BuilderID{
   274  							Project: "project",
   275  							Bucket:  "bucket",
   276  							Builder: "builder",
   277  						},
   278  					},
   279  					BucketID:    "project/bucket",
   280  					BuilderID:   "project/bucket/builder",
   281  					Experiments: experiments(false, false),
   282  				}), ShouldBeNil)
   283  
   284  				query := NewQuery(&pb.SearchBuildsRequest{})
   285  				rsp, err := query.Fetch(ctx)
   286  				So(err, ShouldBeNil)
   287  				expectedRsp := &pb.SearchBuildsResponse{
   288  					Builds: []*pb.Build{
   289  						{
   290  							Id: 1,
   291  							Builder: &pb.BuilderID{
   292  								Project: "project",
   293  								Bucket:  "bucket",
   294  								Builder: "builder",
   295  							},
   296  						},
   297  					},
   298  				}
   299  				So(rsp, ShouldResembleProto, expectedRsp)
   300  			})
   301  
   302  			Convey("Fallback to fetchOnBuild flow", func() {
   303  				So(datastore.Put(ctx, &model.TagIndex{
   304  					ID:         ":10:buildset:1",
   305  					Incomplete: true,
   306  					Entries:    nil,
   307  				}), ShouldBeNil)
   308  				So(datastore.Put(ctx, &model.Build{
   309  					Proto: &pb.Build{
   310  						Id: 1,
   311  						Builder: &pb.BuilderID{
   312  							Project: "project",
   313  							Bucket:  "bucket",
   314  							Builder: "builder",
   315  						},
   316  					},
   317  					BucketID:    "project/bucket",
   318  					BuilderID:   "project/bucket/builder",
   319  					Tags:        []string{"buildset:1"},
   320  					Experiments: experiments(false, false),
   321  				}), ShouldBeNil)
   322  
   323  				query.Tags = strpair.ParseMap([]string{"buildset:1"})
   324  				actualRsp, err := query.Fetch(ctx)
   325  				So(err, ShouldBeNil)
   326  				So(actualRsp, ShouldResembleProto, &pb.SearchBuildsResponse{
   327  					Builds: []*pb.Build{
   328  						{
   329  							Id: 1,
   330  							Builder: &pb.BuilderID{
   331  								Project: "project",
   332  								Bucket:  "bucket",
   333  								Builder: "builder",
   334  							},
   335  							Tags: []*pb.StringPair{
   336  								{
   337  									Key:   "buildset",
   338  									Value: "1",
   339  								},
   340  							},
   341  						},
   342  					},
   343  				})
   344  			})
   345  		})
   346  	})
   347  }
   348  
   349  func TestFetchOnBuild(t *testing.T) {
   350  	t.Parallel()
   351  
   352  	Convey("FetchOnBuild", t, func() {
   353  		ctx := memory.Use(context.Background())
   354  		ctx = auth.WithState(ctx, &authtest.FakeState{
   355  			Identity: userID,
   356  			FakeDB: authtest.NewFakeDB(
   357  				authtest.MockPermission(userID, "project:bucket", bbperms.BuildersList),
   358  				authtest.MockPermission(userID, "project:bucket", bbperms.BuildsList),
   359  			),
   360  		})
   361  		datastore.GetTestable(ctx).AutoIndex(true)
   362  		datastore.GetTestable(ctx).Consistent(true)
   363  
   364  		So(datastore.Put(ctx, &model.Bucket{
   365  			ID:     "bucket",
   366  			Parent: model.ProjectKey(ctx, "project"),
   367  			Proto:  &pb.Bucket{},
   368  		}), ShouldBeNil)
   369  		So(datastore.Put(ctx, &model.Build{
   370  			ID: 100,
   371  			Proto: &pb.Build{
   372  				Id: 100,
   373  				Builder: &pb.BuilderID{
   374  					Project: "project",
   375  					Bucket:  "bucket",
   376  					Builder: "builder1",
   377  				},
   378  				Status: pb.Status_SUCCESS,
   379  			},
   380  			Status:      pb.Status_SUCCESS,
   381  			Project:     "project",
   382  			BucketID:    "project/bucket",
   383  			BuilderID:   "project/bucket/builder1",
   384  			Tags:        []string{"k1:v1", "k2:v2"},
   385  			Experiments: experiments(false, false),
   386  		}), ShouldBeNil)
   387  		So(datastore.Put(ctx, &model.Build{
   388  			ID: 200,
   389  			Proto: &pb.Build{
   390  				Id: 200,
   391  				Builder: &pb.BuilderID{
   392  					Project: "project",
   393  					Bucket:  "bucket",
   394  					Builder: "builder2",
   395  				},
   396  				Status: pb.Status_CANCELED,
   397  			},
   398  			Status:      pb.Status_CANCELED,
   399  			Project:     "project",
   400  			BucketID:    "project/bucket",
   401  			BuilderID:   "project/bucket/builder2",
   402  			Experiments: experiments(false, false),
   403  		}), ShouldBeNil)
   404  
   405  		Convey("found by builder", func() {
   406  			req := &pb.SearchBuildsRequest{
   407  				Predicate: &pb.BuildPredicate{
   408  					Builder: &pb.BuilderID{
   409  						Project: "project",
   410  						Bucket:  "bucket",
   411  						Builder: "builder1",
   412  					},
   413  				},
   414  			}
   415  			query := NewQuery(req)
   416  			actualRsp, err := query.fetchOnBuild(ctx)
   417  			expectedRsp := &pb.SearchBuildsResponse{
   418  				Builds: []*pb.Build{
   419  					{
   420  						Id: 100,
   421  						Builder: &pb.BuilderID{
   422  							Project: "project",
   423  							Bucket:  "bucket",
   424  							Builder: "builder1",
   425  						},
   426  						Tags: []*pb.StringPair{
   427  							{Key: "k1", Value: "v1"},
   428  							{Key: "k2", Value: "v2"},
   429  						},
   430  						Status: pb.Status_SUCCESS,
   431  					},
   432  				},
   433  			}
   434  
   435  			So(err, ShouldBeNil)
   436  			So(actualRsp, ShouldResembleProto, expectedRsp)
   437  		})
   438  		Convey("found by tag", func() {
   439  			req := &pb.SearchBuildsRequest{
   440  				Predicate: &pb.BuildPredicate{
   441  					Status: pb.Status_SUCCESS,
   442  					Tags: []*pb.StringPair{
   443  						{Key: "k1", Value: "v1"},
   444  						{Key: "k2", Value: "v2"},
   445  					},
   446  				},
   447  			}
   448  			query := NewQuery(req)
   449  			actualRsp, err := query.fetchOnBuild(ctx)
   450  			expectedRsp := &pb.SearchBuildsResponse{
   451  				Builds: []*pb.Build{
   452  					{
   453  						Id: 100,
   454  						Builder: &pb.BuilderID{
   455  							Project: "project",
   456  							Bucket:  "bucket",
   457  							Builder: "builder1",
   458  						},
   459  						Tags: []*pb.StringPair{
   460  							{Key: "k1", Value: "v1"},
   461  							{Key: "k2", Value: "v2"},
   462  						},
   463  						Status: pb.Status_SUCCESS,
   464  					},
   465  				},
   466  			}
   467  
   468  			So(err, ShouldBeNil)
   469  			So(actualRsp, ShouldResembleProto, expectedRsp)
   470  		})
   471  		Convey("found by status", func() {
   472  			req := &pb.SearchBuildsRequest{
   473  				Predicate: &pb.BuildPredicate{
   474  					Status: pb.Status_SUCCESS,
   475  				},
   476  			}
   477  			query := NewQuery(req)
   478  			actualRsp, err := query.fetchOnBuild(ctx)
   479  			expectedRsp := &pb.SearchBuildsResponse{
   480  				Builds: []*pb.Build{
   481  					{
   482  						Id: 100,
   483  						Builder: &pb.BuilderID{
   484  							Project: "project",
   485  							Bucket:  "bucket",
   486  							Builder: "builder1",
   487  						},
   488  						Tags: []*pb.StringPair{
   489  							{Key: "k1", Value: "v1"},
   490  							{Key: "k2", Value: "v2"},
   491  						},
   492  						Status: pb.Status_SUCCESS,
   493  					},
   494  				},
   495  			}
   496  
   497  			So(err, ShouldBeNil)
   498  			So(actualRsp, ShouldResembleProto, expectedRsp)
   499  		})
   500  		Convey("found by build range", func() {
   501  			req := &pb.SearchBuildsRequest{
   502  				Predicate: &pb.BuildPredicate{
   503  					Build: &pb.BuildRange{
   504  						StartBuildId: 199,
   505  						EndBuildId:   99,
   506  					},
   507  				},
   508  			}
   509  			query := NewQuery(req)
   510  			actualRsp, err := query.fetchOnBuild(ctx)
   511  			expectedRsp := &pb.SearchBuildsResponse{
   512  				Builds: []*pb.Build{
   513  					{
   514  						Id: 100,
   515  						Builder: &pb.BuilderID{
   516  							Project: "project",
   517  							Bucket:  "bucket",
   518  							Builder: "builder1",
   519  						},
   520  						Tags: []*pb.StringPair{
   521  							{Key: "k1", Value: "v1"},
   522  							{Key: "k2", Value: "v2"},
   523  						},
   524  						Status: pb.Status_SUCCESS,
   525  					},
   526  				},
   527  			}
   528  
   529  			So(err, ShouldBeNil)
   530  			So(actualRsp, ShouldResembleProto, expectedRsp)
   531  		})
   532  		Convey("found by create time", func() {
   533  			So(datastore.Put(ctx, &model.Build{
   534  				ID: 8764414515958775808,
   535  				Proto: &pb.Build{
   536  					Builder: &pb.BuilderID{
   537  						Project: "project",
   538  						Bucket:  "bucket",
   539  						Builder: "builder5808",
   540  					},
   541  					Id: 8764414515958775808,
   542  				},
   543  				BucketID:    "project/bucket",
   544  				Experiments: experiments(false, false),
   545  			}), ShouldBeNil)
   546  			req := &pb.SearchBuildsRequest{
   547  				Predicate: &pb.BuildPredicate{
   548  					CreateTime: &pb.TimeRange{
   549  						StartTime: &timestamppb.Timestamp{Seconds: 1592701200},
   550  						EndTime:   &timestamppb.Timestamp{Seconds: 1700000000},
   551  					},
   552  				},
   553  			}
   554  			query := NewQuery(req)
   555  			actualRsp, err := query.fetchOnBuild(ctx)
   556  			expectedRsp := &pb.SearchBuildsResponse{
   557  				Builds: []*pb.Build{
   558  					{
   559  						Id: 8764414515958775808,
   560  						Builder: &pb.BuilderID{
   561  							Project: "project",
   562  							Bucket:  "bucket",
   563  							Builder: "builder5808",
   564  						},
   565  					},
   566  				},
   567  			}
   568  
   569  			So(err, ShouldBeNil)
   570  			So(actualRsp, ShouldResembleProto, expectedRsp)
   571  		})
   572  		Convey("found by created_by", func() {
   573  			So(datastore.Put(ctx, &model.Build{
   574  				ID: 1111,
   575  				Proto: &pb.Build{
   576  					Id:        1111,
   577  					CreatedBy: "project:infra",
   578  					Builder: &pb.BuilderID{
   579  						Project: "project",
   580  						Bucket:  "bucket",
   581  						Builder: "builder1111",
   582  					},
   583  				},
   584  				CreatedBy:   "project:infra",
   585  				BucketID:    "project/bucket",
   586  				Experiments: experiments(false, false),
   587  			}), ShouldBeNil)
   588  			req := &pb.SearchBuildsRequest{
   589  				Predicate: &pb.BuildPredicate{
   590  					CreatedBy: "project:infra",
   591  				},
   592  			}
   593  			query := NewQuery(req)
   594  			actualRsp, err := query.fetchOnBuild(ctx)
   595  			expectedRsp := &pb.SearchBuildsResponse{
   596  				Builds: []*pb.Build{
   597  					{
   598  						Id:        1111,
   599  						CreatedBy: "project:infra",
   600  						Builder: &pb.BuilderID{
   601  							Project: "project",
   602  							Bucket:  "bucket",
   603  							Builder: "builder1111",
   604  						},
   605  					},
   606  				},
   607  			}
   608  
   609  			So(err, ShouldBeNil)
   610  			So(actualRsp, ShouldResembleProto, expectedRsp)
   611  		})
   612  		Convey("found by ENDED_MASK", func() {
   613  			So(datastore.Put(ctx, &model.Build{
   614  				ID: 300,
   615  				Proto: &pb.Build{
   616  					Id: 300,
   617  					Builder: &pb.BuilderID{
   618  						Project: "project",
   619  						Bucket:  "bucket",
   620  						Builder: "builder3",
   621  					},
   622  					Status: pb.Status_STARTED,
   623  				},
   624  				Project:     "project",
   625  				BucketID:    "project/bucket",
   626  				BuilderID:   "project/bucket/builder3",
   627  				Status:      pb.Status_STARTED,
   628  				Experiments: experiments(false, false),
   629  			}), ShouldBeNil)
   630  
   631  			req := &pb.SearchBuildsRequest{
   632  				Predicate: &pb.BuildPredicate{
   633  					Status: pb.Status_ENDED_MASK,
   634  				},
   635  			}
   636  			query := NewQuery(req)
   637  			actualRsp, err := query.fetchOnBuild(ctx)
   638  			expectedRsp := &pb.SearchBuildsResponse{
   639  				Builds: []*pb.Build{
   640  					{
   641  						Id: 100,
   642  						Builder: &pb.BuilderID{
   643  							Project: "project",
   644  							Bucket:  "bucket",
   645  							Builder: "builder1",
   646  						},
   647  						Tags: []*pb.StringPair{
   648  							{Key: "k1", Value: "v1"},
   649  							{Key: "k2", Value: "v2"},
   650  						},
   651  						Status: pb.Status_SUCCESS,
   652  					},
   653  					{
   654  						Id: 200,
   655  						Builder: &pb.BuilderID{
   656  							Project: "project",
   657  							Bucket:  "bucket",
   658  							Builder: "builder2",
   659  						},
   660  						Status: pb.Status_CANCELED,
   661  					},
   662  				},
   663  			}
   664  
   665  			So(err, ShouldBeNil)
   666  			So(actualRsp, ShouldResembleProto, expectedRsp)
   667  		})
   668  		Convey("found by canary", func() {
   669  			req := &pb.SearchBuildsRequest{
   670  				Predicate: &pb.BuildPredicate{
   671  					Canary: pb.Trinary_YES,
   672  				},
   673  			}
   674  			So(datastore.Put(ctx, &model.Build{
   675  				ID: 321,
   676  				Proto: &pb.Build{
   677  					Id: 321,
   678  					Builder: &pb.BuilderID{
   679  						Project: "project",
   680  						Bucket:  "bucket",
   681  						Builder: "builder321",
   682  					},
   683  					Canary: true,
   684  				},
   685  				Project:     "project",
   686  				BucketID:    "project/bucket",
   687  				BuilderID:   "project/bucket/builder321",
   688  				Experiments: experiments(true, false),
   689  			}), ShouldBeNil)
   690  			query := NewQuery(req)
   691  			actualRsp, err := query.fetchOnBuild(ctx)
   692  			expectedRsp := &pb.SearchBuildsResponse{
   693  				Builds: []*pb.Build{
   694  					{
   695  						Id: 321,
   696  						Builder: &pb.BuilderID{
   697  							Project: "project",
   698  							Bucket:  "bucket",
   699  							Builder: "builder321",
   700  						},
   701  						Canary: true,
   702  					},
   703  				},
   704  			}
   705  
   706  			So(err, ShouldBeNil)
   707  			So(actualRsp, ShouldResembleProto, expectedRsp)
   708  		})
   709  		Convey("found only experimental", func() {
   710  			req := &pb.SearchBuildsRequest{
   711  				Predicate: &pb.BuildPredicate{
   712  					Experiments: []string{"+" + bb.ExperimentNonProduction},
   713  				},
   714  			}
   715  			So(datastore.Put(ctx, &model.Build{
   716  				ID: 321,
   717  				Proto: &pb.Build{
   718  					Id: 321,
   719  					Builder: &pb.BuilderID{
   720  						Project: "project",
   721  						Bucket:  "bucket",
   722  						Builder: "builder321",
   723  					},
   724  					Input: &pb.Build_Input{Experimental: true},
   725  				},
   726  				Project:     "project",
   727  				BucketID:    "project/bucket",
   728  				BuilderID:   "project/bucket/builder321",
   729  				Experiments: experiments(false, true),
   730  			}), ShouldBeNil)
   731  			So(datastore.Put(ctx, &model.Build{
   732  				ID: 123,
   733  				Proto: &pb.Build{
   734  					Id: 123,
   735  					Builder: &pb.BuilderID{
   736  						Project: "project",
   737  						Bucket:  "bucket",
   738  						Builder: "builder321",
   739  					},
   740  				},
   741  				Project:     "project",
   742  				BucketID:    "project/bucket",
   743  				BuilderID:   "project/bucket/builder123",
   744  				Experiments: experiments(false, false),
   745  			}), ShouldBeNil)
   746  			query := NewQuery(req)
   747  			actualRsp, err := query.fetchOnBuild(ctx)
   748  			expectedRsp := &pb.SearchBuildsResponse{
   749  				Builds: []*pb.Build{
   750  					{
   751  						Id: 321,
   752  						Builder: &pb.BuilderID{
   753  							Project: "project",
   754  							Bucket:  "bucket",
   755  							Builder: "builder321",
   756  						},
   757  						Input: &pb.Build_Input{Experimental: true},
   758  					},
   759  				},
   760  			}
   761  
   762  			So(err, ShouldBeNil)
   763  			So(actualRsp, ShouldResembleProto, expectedRsp)
   764  		})
   765  		Convey("found non experimental", func() {
   766  			req := &pb.SearchBuildsRequest{
   767  				Predicate: &pb.BuildPredicate{
   768  					Experiments: []string{"-" + bb.ExperimentNonProduction},
   769  				},
   770  			}
   771  			So(datastore.Put(ctx, &model.Build{
   772  				ID: 321,
   773  				Proto: &pb.Build{
   774  					Id: 321,
   775  					Builder: &pb.BuilderID{
   776  						Project: "project",
   777  						Bucket:  "bucket",
   778  						Builder: "builder321",
   779  					},
   780  					Input: &pb.Build_Input{Experimental: true},
   781  				},
   782  				Project:     "project",
   783  				BucketID:    "project/bucket",
   784  				BuilderID:   "project/bucket/builder321",
   785  				Experiments: experiments(false, true),
   786  			}), ShouldBeNil)
   787  			So(datastore.Put(ctx, &model.Build{
   788  				ID: 123,
   789  				Proto: &pb.Build{
   790  					Id: 123,
   791  					Builder: &pb.BuilderID{
   792  						Project: "project",
   793  						Bucket:  "bucket",
   794  						Builder: "builder123",
   795  					},
   796  				},
   797  				Project:     "project",
   798  				BucketID:    "project/bucket",
   799  				BuilderID:   "project/bucket/builder123",
   800  				Experiments: experiments(false, false),
   801  			}), ShouldBeNil)
   802  			query := NewQuery(req)
   803  			actualRsp, err := query.fetchOnBuild(ctx)
   804  			expectedRsp := &pb.SearchBuildsResponse{
   805  				Builds: []*pb.Build{
   806  					{
   807  						Id: 100,
   808  						Builder: &pb.BuilderID{
   809  							Project: "project",
   810  							Bucket:  "bucket",
   811  							Builder: "builder1",
   812  						},
   813  						Tags: []*pb.StringPair{
   814  							{Key: "k1", Value: "v1"},
   815  							{Key: "k2", Value: "v2"},
   816  						},
   817  						Status: pb.Status_SUCCESS,
   818  					},
   819  					{
   820  						Id: 123,
   821  						Builder: &pb.BuilderID{
   822  							Project: "project",
   823  							Bucket:  "bucket",
   824  							Builder: "builder123",
   825  						},
   826  					},
   827  					{
   828  						Id: 200,
   829  						Builder: &pb.BuilderID{
   830  							Project: "project",
   831  							Bucket:  "bucket",
   832  							Builder: "builder2",
   833  						},
   834  						Status: pb.Status_CANCELED,
   835  					},
   836  				},
   837  			}
   838  
   839  			So(err, ShouldBeNil)
   840  			So(actualRsp, ShouldResembleProto, expectedRsp)
   841  		})
   842  		Convey("found by ancestors", func() {
   843  			bIDs := func(rsp *pb.SearchBuildsResponse) []int {
   844  				ids := make([]int, 0, len(rsp.Builds))
   845  				for _, b := range rsp.Builds {
   846  					ids = append(ids, int(b.Id))
   847  				}
   848  				sort.Ints(ids)
   849  				return ids
   850  			}
   851  			So(datastore.Put(ctx, &model.Build{
   852  				ID: 1,
   853  				Proto: &pb.Build{
   854  					Id: 1,
   855  					Builder: &pb.BuilderID{
   856  						Project: "project",
   857  						Bucket:  "bucket",
   858  						Builder: "builder",
   859  					},
   860  				},
   861  			}), ShouldBeNil)
   862  			So(datastore.Put(ctx, &model.Build{
   863  				ID: 2,
   864  				Proto: &pb.Build{
   865  					Id: 2,
   866  					Builder: &pb.BuilderID{
   867  						Project: "project",
   868  						Bucket:  "bucket",
   869  						Builder: "builder",
   870  					},
   871  					AncestorIds: []int64{1},
   872  				},
   873  			}), ShouldBeNil)
   874  			So(datastore.Put(ctx, &model.Build{
   875  				ID: 3,
   876  				Proto: &pb.Build{
   877  					Id: 3,
   878  					Builder: &pb.BuilderID{
   879  						Project: "project",
   880  						Bucket:  "bucket",
   881  						Builder: "builder",
   882  					},
   883  					AncestorIds: []int64{1},
   884  				},
   885  			}), ShouldBeNil)
   886  			So(datastore.Put(ctx, &model.Build{
   887  				ID: 4,
   888  				Proto: &pb.Build{
   889  					Id: 4,
   890  					Builder: &pb.BuilderID{
   891  						Project: "project",
   892  						Bucket:  "bucket",
   893  						Builder: "builder",
   894  					},
   895  					AncestorIds: []int64{1, 2},
   896  				},
   897  			}), ShouldBeNil)
   898  			Convey("by ancestor_ids", func() {
   899  				req := &pb.SearchBuildsRequest{
   900  					Predicate: &pb.BuildPredicate{
   901  						DescendantOf: 1,
   902  					},
   903  				}
   904  				query := NewQuery(req)
   905  				actualRsp, err := query.fetchOnBuild(ctx)
   906  				So(err, ShouldBeNil)
   907  				So(bIDs(actualRsp), ShouldResemble, []int{2, 3, 4})
   908  			})
   909  			Convey("by parent_id", func() {
   910  				req := &pb.SearchBuildsRequest{
   911  					Predicate: &pb.BuildPredicate{
   912  						ChildOf: 1,
   913  					},
   914  				}
   915  				query := NewQuery(req)
   916  				actualRsp, err := query.fetchOnBuild(ctx)
   917  				So(err, ShouldBeNil)
   918  				So(bIDs(actualRsp), ShouldResemble, []int{2, 3})
   919  			})
   920  		})
   921  		Convey("empty request", func() {
   922  			req := &pb.SearchBuildsRequest{
   923  				Predicate: &pb.BuildPredicate{},
   924  			}
   925  			query := NewQuery(req)
   926  			actualRsp, err := query.fetchOnBuild(ctx)
   927  			expectedRsp := &pb.SearchBuildsResponse{
   928  				Builds: []*pb.Build{
   929  					{
   930  						Id: 100,
   931  						Builder: &pb.BuilderID{
   932  							Project: "project",
   933  							Bucket:  "bucket",
   934  							Builder: "builder1",
   935  						},
   936  						Tags: []*pb.StringPair{
   937  							{Key: "k1", Value: "v1"},
   938  							{Key: "k2", Value: "v2"},
   939  						},
   940  						Status: pb.Status_SUCCESS,
   941  					},
   942  					{
   943  						Id: 200,
   944  						Builder: &pb.BuilderID{
   945  							Project: "project",
   946  							Bucket:  "bucket",
   947  							Builder: "builder2",
   948  						},
   949  						Status: pb.Status_CANCELED,
   950  					},
   951  				},
   952  			}
   953  
   954  			So(err, ShouldBeNil)
   955  			So(actualRsp, ShouldResembleProto, expectedRsp)
   956  		})
   957  		Convey("pagination", func() {
   958  			So(datastore.Put(ctx, &model.Build{
   959  				ID: 300,
   960  				Proto: &pb.Build{
   961  					Id: 300,
   962  					Builder: &pb.BuilderID{
   963  						Project: "project",
   964  						Bucket:  "bucket",
   965  						Builder: "builder3",
   966  					},
   967  				},
   968  				Project:     "project",
   969  				BucketID:    "project/bucket",
   970  				BuilderID:   "project/bucket/builder3",
   971  				Experiments: experiments(false, false),
   972  			}), ShouldBeNil)
   973  
   974  			// this build can be fetched from db but not accessible by the user.
   975  			So(datastore.Put(ctx, &model.Build{
   976  				ID: 400,
   977  				Proto: &pb.Build{
   978  					Id: 400,
   979  					Builder: &pb.BuilderID{
   980  						Project: "project_no_access",
   981  						Bucket:  "bucket",
   982  						Builder: "builder",
   983  					},
   984  				},
   985  				Project:     "project_no_access",
   986  				BucketID:    "project_no_access/bucket",
   987  				BuilderID:   "project_no_access/bucket/builder",
   988  				Experiments: experiments(false, false),
   989  			}), ShouldBeNil)
   990  
   991  			req := &pb.SearchBuildsRequest{
   992  				PageSize: 2,
   993  			}
   994  
   995  			// fetch 1st page.
   996  			query := NewQuery(req)
   997  			actualRsp, err := query.fetchOnBuild(ctx)
   998  			expectedBuilds := []*pb.Build{
   999  				{
  1000  					Id: 100,
  1001  					Builder: &pb.BuilderID{
  1002  						Project: "project",
  1003  						Bucket:  "bucket",
  1004  						Builder: "builder1",
  1005  					},
  1006  					Tags: []*pb.StringPair{
  1007  						{Key: "k1", Value: "v1"},
  1008  						{Key: "k2", Value: "v2"},
  1009  					},
  1010  					Status: pb.Status_SUCCESS,
  1011  				},
  1012  				{
  1013  					Id: 200,
  1014  					Builder: &pb.BuilderID{
  1015  						Project: "project",
  1016  						Bucket:  "bucket",
  1017  						Builder: "builder2",
  1018  					},
  1019  					Status: pb.Status_CANCELED,
  1020  				},
  1021  			}
  1022  
  1023  			So(err, ShouldBeNil)
  1024  			So(actualRsp.Builds, ShouldResembleProto, expectedBuilds)
  1025  			So(actualRsp.NextPageToken, ShouldEqual, "id>200")
  1026  
  1027  			// fetch the following page (response should have a build with the ID - 400).
  1028  			req.PageToken = actualRsp.NextPageToken
  1029  			query = NewQuery(req)
  1030  			actualRsp, err = query.fetchOnBuild(ctx)
  1031  			expectedBuilds = []*pb.Build{
  1032  				{
  1033  					Id: 300,
  1034  					Builder: &pb.BuilderID{
  1035  						Project: "project",
  1036  						Bucket:  "bucket",
  1037  						Builder: "builder3",
  1038  					},
  1039  				},
  1040  			}
  1041  
  1042  			So(err, ShouldBeNil)
  1043  			So(actualRsp.Builds, ShouldResembleProto, expectedBuilds)
  1044  			So(actualRsp.NextPageToken, ShouldBeEmpty)
  1045  		})
  1046  		Convey("found by start build id and pagination", func() {
  1047  			req := &pb.SearchBuildsRequest{
  1048  				Predicate: &pb.BuildPredicate{
  1049  					Build: &pb.BuildRange{
  1050  						StartBuildId: 199,
  1051  					},
  1052  				},
  1053  				PageToken: "id>199",
  1054  			}
  1055  			query := NewQuery(req)
  1056  			actualRsp, err := query.fetchOnBuild(ctx)
  1057  			expectedRsp := &pb.SearchBuildsResponse{}
  1058  
  1059  			So(err, ShouldBeNil)
  1060  			So(actualRsp, ShouldResembleProto, expectedRsp)
  1061  		})
  1062  	})
  1063  }
  1064  
  1065  func TestIndexedTags(t *testing.T) {
  1066  	t.Parallel()
  1067  
  1068  	Convey("tags", t, func() {
  1069  		tags := strpair.Map{
  1070  			"a":        []string{"b"},
  1071  			"buildset": []string{"b1"},
  1072  		}
  1073  		result := IndexedTags(tags)
  1074  		So(result, ShouldResemble, []string{"buildset:b1"})
  1075  	})
  1076  
  1077  	Convey("duplicate tags", t, func() {
  1078  		tags := strpair.Map{
  1079  			"buildset":      []string{"b1", "b1"},
  1080  			"build_address": []string{"address"},
  1081  		}
  1082  		result := IndexedTags(tags)
  1083  		So(result, ShouldResemble, []string{"build_address:address", "buildset:b1"})
  1084  	})
  1085  
  1086  	Convey("empty tags", t, func() {
  1087  		tags := strpair.Map{}
  1088  		result := IndexedTags(tags)
  1089  		So(result, ShouldResemble, []string{})
  1090  	})
  1091  }
  1092  
  1093  func TestUpdateTagIndex(t *testing.T) {
  1094  	t.Parallel()
  1095  
  1096  	Convey("UpdateTagIndex", t, func() {
  1097  		ctx, _ := testclock.UseTime(memory.Use(context.Background()), testclock.TestRecentTimeUTC)
  1098  		datastore.GetTestable(ctx).AutoIndex(true)
  1099  		datastore.GetTestable(ctx).Consistent(true)
  1100  
  1101  		builds := []*model.Build{
  1102  			{
  1103  				ID: 1,
  1104  				Proto: &pb.Build{
  1105  					Builder: &pb.BuilderID{
  1106  						Project: "project",
  1107  						Bucket:  "bucket",
  1108  						Builder: "builder",
  1109  					},
  1110  					CreateTime: timestamppb.New(testclock.TestRecentTimeUTC),
  1111  				},
  1112  				Tags: []string{
  1113  					"a:b",
  1114  					"buildset:b1",
  1115  				},
  1116  				Experiments: experiments(false, false),
  1117  			},
  1118  			{
  1119  				ID: 2,
  1120  				Proto: &pb.Build{
  1121  					Builder: &pb.BuilderID{
  1122  						Project: "project",
  1123  						Bucket:  "bucket",
  1124  						Builder: "builder",
  1125  					},
  1126  					CreateTime: timestamppb.New(testclock.TestRecentTimeUTC),
  1127  				},
  1128  				Tags: []string{
  1129  					"a:b",
  1130  					"build_address:address",
  1131  					"buildset:b1",
  1132  				},
  1133  				Experiments: experiments(false, false),
  1134  			},
  1135  		}
  1136  		So(UpdateTagIndex(ctx, builds), ShouldBeNil)
  1137  
  1138  		idx, err := model.SearchTagIndex(ctx, "a", "b")
  1139  		So(err, ShouldBeNil)
  1140  		So(idx, ShouldBeNil)
  1141  
  1142  		idx, err = model.SearchTagIndex(ctx, "buildset", "b1")
  1143  		So(err, ShouldBeNil)
  1144  		So(idx, ShouldResemble, []*model.TagIndexEntry{
  1145  			{
  1146  				BuildID:     int64(1),
  1147  				BucketID:    "project/bucket",
  1148  				CreatedTime: datastore.RoundTime(testclock.TestRecentTimeUTC),
  1149  			},
  1150  			{
  1151  				BuildID:     int64(2),
  1152  				BucketID:    "project/bucket",
  1153  				CreatedTime: datastore.RoundTime(testclock.TestRecentTimeUTC),
  1154  			},
  1155  		})
  1156  
  1157  		idx, err = model.SearchTagIndex(ctx, "build_address", "address")
  1158  		So(err, ShouldBeNil)
  1159  		So(idx, ShouldResemble, []*model.TagIndexEntry{
  1160  			{
  1161  				BuildID:     int64(2),
  1162  				BucketID:    "project/bucket",
  1163  				CreatedTime: datastore.RoundTime(testclock.TestRecentTimeUTC),
  1164  			},
  1165  		})
  1166  	})
  1167  }
  1168  
  1169  func TestFetchOnTagIndex(t *testing.T) {
  1170  	t.Parallel()
  1171  
  1172  	Convey("FetchOnTagIndex", t, func() {
  1173  		ctx := memory.Use(context.Background())
  1174  		ctx = auth.WithState(ctx, &authtest.FakeState{
  1175  			Identity: userID,
  1176  			FakeDB: authtest.NewFakeDB(
  1177  				authtest.MockPermission(userID, "project:bucket", bbperms.BuildersList),
  1178  				authtest.MockPermission(userID, "project:bucket", bbperms.BuildsList),
  1179  			),
  1180  		})
  1181  		datastore.GetTestable(ctx).AutoIndex(true)
  1182  		datastore.GetTestable(ctx).Consistent(true)
  1183  
  1184  		So(datastore.Put(ctx, &model.Bucket{
  1185  			ID:     "bucket",
  1186  			Parent: model.ProjectKey(ctx, "project"),
  1187  			Proto:  &pb.Bucket{},
  1188  		}), ShouldBeNil)
  1189  		So(datastore.Put(ctx, &model.Build{
  1190  			ID: 100,
  1191  			Proto: &pb.Build{
  1192  				Id: 100,
  1193  				Builder: &pb.BuilderID{
  1194  					Project: "project",
  1195  					Bucket:  "bucket",
  1196  					Builder: "builder1",
  1197  				},
  1198  				Status: pb.Status_SUCCESS,
  1199  			},
  1200  			Status:      pb.Status_SUCCESS,
  1201  			Project:     "project",
  1202  			BucketID:    "project/bucket",
  1203  			BuilderID:   "project/bucket/builder1",
  1204  			Tags:        []string{"buildset:commit/git/abcd"},
  1205  			Experiments: experiments(false, false),
  1206  		}), ShouldBeNil)
  1207  		So(datastore.Put(ctx, &model.Build{
  1208  			ID: 200,
  1209  			Proto: &pb.Build{
  1210  				Id: 200,
  1211  				Builder: &pb.BuilderID{
  1212  					Project: "project",
  1213  					Bucket:  "bucket",
  1214  					Builder: "builder2",
  1215  				},
  1216  				Status: pb.Status_CANCELED,
  1217  			},
  1218  			Status:    pb.Status_CANCELED,
  1219  			Project:   "project",
  1220  			BucketID:  "project/bucket",
  1221  			BuilderID: "project/bucket/builder2",
  1222  			Tags:      []string{"buildset:commit/git/abcd"},
  1223  			// legacy; no Experiments, assumed to be prod
  1224  		}), ShouldBeNil)
  1225  		So(datastore.Put(ctx, &model.Build{
  1226  			ID: 300,
  1227  			Proto: &pb.Build{
  1228  				Id: 300,
  1229  				Builder: &pb.BuilderID{
  1230  					Project: "project",
  1231  					Bucket:  "bucket",
  1232  					Builder: "builder3",
  1233  				},
  1234  				Status: pb.Status_CANCELED,
  1235  				Input:  &pb.Build_Input{Experimental: true},
  1236  			},
  1237  			Status:      pb.Status_CANCELED,
  1238  			Project:     "project",
  1239  			BucketID:    "project/bucket",
  1240  			BuilderID:   "project/bucket/builder3",
  1241  			Tags:        []string{"buildset:commit/git/abcd"},
  1242  			Experiments: experiments(false, true),
  1243  		}), ShouldBeNil)
  1244  		So(datastore.Put(ctx, &model.TagIndex{
  1245  			ID: ":2:buildset:commit/git/abcd",
  1246  			Entries: []model.TagIndexEntry{
  1247  				{
  1248  					BuildID:  100,
  1249  					BucketID: "project/bucket",
  1250  				},
  1251  			},
  1252  		}), ShouldBeNil)
  1253  		So(datastore.Put(ctx, &model.TagIndex{
  1254  			ID: ":3:buildset:commit/git/abcd",
  1255  			Entries: []model.TagIndexEntry{
  1256  				{
  1257  					BuildID:  200,
  1258  					BucketID: "project/bucket",
  1259  				},
  1260  				{
  1261  					BuildID:  300,
  1262  					BucketID: "project/bucket",
  1263  				},
  1264  			},
  1265  		}), ShouldBeNil)
  1266  		req := &pb.SearchBuildsRequest{
  1267  			Predicate: &pb.BuildPredicate{
  1268  				Tags: []*pb.StringPair{
  1269  					{Key: "buildset", Value: "commit/git/abcd"},
  1270  				},
  1271  			},
  1272  		}
  1273  		Convey("filter only by an indexed tag", func() {
  1274  			query := NewQuery(req)
  1275  			actualRsp, err := query.fetchOnTagIndex(ctx)
  1276  			expectedRsp := &pb.SearchBuildsResponse{
  1277  				Builds: []*pb.Build{
  1278  					{
  1279  						Id: 100,
  1280  						Builder: &pb.BuilderID{
  1281  							Project: "project",
  1282  							Bucket:  "bucket",
  1283  							Builder: "builder1",
  1284  						},
  1285  						Tags: []*pb.StringPair{
  1286  							{Key: "buildset", Value: "commit/git/abcd"},
  1287  						},
  1288  						Status: pb.Status_SUCCESS,
  1289  					},
  1290  					{
  1291  						Id: 200,
  1292  						Builder: &pb.BuilderID{
  1293  							Project: "project",
  1294  							Bucket:  "bucket",
  1295  							Builder: "builder2",
  1296  						},
  1297  						Tags: []*pb.StringPair{
  1298  							{Key: "buildset", Value: "commit/git/abcd"},
  1299  						},
  1300  						Status: pb.Status_CANCELED,
  1301  					},
  1302  				},
  1303  			}
  1304  
  1305  			So(err, ShouldBeNil)
  1306  			So(actualRsp, ShouldResembleProto, expectedRsp)
  1307  		})
  1308  
  1309  		Convey("filter by status", func() {
  1310  			req.Predicate.Status = pb.Status_CANCELED
  1311  			query := NewQuery(req)
  1312  			actualRsp, err := query.fetchOnTagIndex(ctx)
  1313  			expectedRsp := &pb.SearchBuildsResponse{
  1314  				Builds: []*pb.Build{
  1315  					{
  1316  						Id: 200,
  1317  						Builder: &pb.BuilderID{
  1318  							Project: "project",
  1319  							Bucket:  "bucket",
  1320  							Builder: "builder2",
  1321  						},
  1322  						Tags: []*pb.StringPair{
  1323  							{Key: "buildset", Value: "commit/git/abcd"},
  1324  						},
  1325  						Status: pb.Status_CANCELED,
  1326  					},
  1327  				},
  1328  			}
  1329  			So(err, ShouldBeNil)
  1330  			So(actualRsp, ShouldResembleProto, expectedRsp)
  1331  		})
  1332  		Convey("filter by ENDED_MASK", func() {
  1333  			So(datastore.Put(ctx, &model.Build{
  1334  				ID: 999,
  1335  				Proto: &pb.Build{
  1336  					Id:     999,
  1337  					Status: pb.Status_STARTED,
  1338  					Builder: &pb.BuilderID{
  1339  						Project: "project",
  1340  						Bucket:  "bucket",
  1341  						Builder: "builder999",
  1342  					},
  1343  				},
  1344  				Project:     "project",
  1345  				BucketID:    "project/bucket",
  1346  				BuilderID:   "project/bucket/builder999",
  1347  				Status:      pb.Status_STARTED,
  1348  				Tags:        []string{"buildset:commit/git/abcd"},
  1349  				Experiments: experiments(false, false),
  1350  			}), ShouldBeNil)
  1351  			So(datastore.Put(ctx, &model.TagIndex{
  1352  				ID: ":4:buildset:commit/git/abcd",
  1353  				Entries: []model.TagIndexEntry{
  1354  					{
  1355  						BuildID:  999,
  1356  						BucketID: "project/bucket",
  1357  					},
  1358  				},
  1359  			}), ShouldBeNil)
  1360  			req.Predicate.Status = pb.Status_ENDED_MASK
  1361  			query := NewQuery(req)
  1362  			actualRsp, err := query.fetchOnTagIndex(ctx)
  1363  			expectedRsp := &pb.SearchBuildsResponse{
  1364  				Builds: []*pb.Build{
  1365  					{
  1366  						Id: 100,
  1367  						Builder: &pb.BuilderID{
  1368  							Project: "project",
  1369  							Bucket:  "bucket",
  1370  							Builder: "builder1",
  1371  						},
  1372  						Tags: []*pb.StringPair{
  1373  							{Key: "buildset", Value: "commit/git/abcd"},
  1374  						},
  1375  						Status: pb.Status_SUCCESS,
  1376  					},
  1377  					{
  1378  						Id: 200,
  1379  						Builder: &pb.BuilderID{
  1380  							Project: "project",
  1381  							Bucket:  "bucket",
  1382  							Builder: "builder2",
  1383  						},
  1384  						Tags: []*pb.StringPair{
  1385  							{Key: "buildset", Value: "commit/git/abcd"},
  1386  						},
  1387  						Status: pb.Status_CANCELED,
  1388  					},
  1389  				},
  1390  			}
  1391  			So(err, ShouldBeNil)
  1392  			So(actualRsp, ShouldResembleProto, expectedRsp)
  1393  		})
  1394  		Convey("filter by an indexed tag and a normal tag", func() {
  1395  			req.Predicate.Tags = append(req.Predicate.Tags, &pb.StringPair{Key: "k1", Value: "v1"})
  1396  			query := NewQuery(req)
  1397  			actualRsp, err := query.fetchOnTagIndex(ctx)
  1398  			So(err, ShouldBeNil)
  1399  			So(actualRsp, ShouldResembleProto, &pb.SearchBuildsResponse{})
  1400  		})
  1401  		Convey("filter by build range", func() {
  1402  			req.Predicate.Build = &pb.BuildRange{
  1403  				StartBuildId: 199,
  1404  				EndBuildId:   99,
  1405  			}
  1406  			query := NewQuery(req)
  1407  			actualRsp, err := query.fetchOnTagIndex(ctx)
  1408  			expectedRsp := &pb.SearchBuildsResponse{
  1409  				Builds: []*pb.Build{
  1410  					{
  1411  						Id: 100,
  1412  						Builder: &pb.BuilderID{
  1413  							Project: "project",
  1414  							Bucket:  "bucket",
  1415  							Builder: "builder1",
  1416  						},
  1417  						Tags: []*pb.StringPair{
  1418  							{Key: "buildset", Value: "commit/git/abcd"},
  1419  						},
  1420  						Status: pb.Status_SUCCESS,
  1421  					},
  1422  				},
  1423  			}
  1424  
  1425  			So(err, ShouldBeNil)
  1426  			So(actualRsp, ShouldResembleProto, expectedRsp)
  1427  		})
  1428  		Convey("filter by created_by", func() {
  1429  			req.Predicate.CreatedBy = "project:infra"
  1430  			query := NewQuery(req)
  1431  			actualRsp, err := query.fetchOnTagIndex(ctx)
  1432  			So(err, ShouldBeNil)
  1433  			So(actualRsp, ShouldResembleProto, &pb.SearchBuildsResponse{})
  1434  		})
  1435  		Convey("filter by canary", func() {
  1436  			req.Predicate.Canary = pb.Trinary_YES
  1437  			query := NewQuery(req)
  1438  			actualRsp, err := query.fetchOnTagIndex(ctx)
  1439  			So(err, ShouldBeNil)
  1440  			So(actualRsp, ShouldResembleProto, &pb.SearchBuildsResponse{})
  1441  		})
  1442  		Convey("filter by IncludeExperimental", func() {
  1443  			req.Predicate.IncludeExperimental = true
  1444  			query := NewQuery(req)
  1445  			actualRsp, err := query.fetchOnTagIndex(ctx)
  1446  			expectedRsp := &pb.SearchBuildsResponse{
  1447  				Builds: []*pb.Build{
  1448  					{
  1449  						Id: 100,
  1450  						Builder: &pb.BuilderID{
  1451  							Project: "project",
  1452  							Bucket:  "bucket",
  1453  							Builder: "builder1",
  1454  						},
  1455  						Tags: []*pb.StringPair{
  1456  							{Key: "buildset", Value: "commit/git/abcd"},
  1457  						},
  1458  						Status: pb.Status_SUCCESS,
  1459  					},
  1460  					{
  1461  						Id: 200,
  1462  						Builder: &pb.BuilderID{
  1463  							Project: "project",
  1464  							Bucket:  "bucket",
  1465  							Builder: "builder2",
  1466  						},
  1467  						Tags: []*pb.StringPair{
  1468  							{Key: "buildset", Value: "commit/git/abcd"},
  1469  						},
  1470  						Status: pb.Status_CANCELED,
  1471  					},
  1472  					{
  1473  						Id: 300,
  1474  						Builder: &pb.BuilderID{
  1475  							Project: "project",
  1476  							Bucket:  "bucket",
  1477  							Builder: "builder3",
  1478  						},
  1479  						Tags: []*pb.StringPair{
  1480  							{Key: "buildset", Value: "commit/git/abcd"},
  1481  						},
  1482  						Input:  &pb.Build_Input{Experimental: true},
  1483  						Status: pb.Status_CANCELED,
  1484  					},
  1485  				},
  1486  			}
  1487  
  1488  			So(err, ShouldBeNil)
  1489  			So(actualRsp, ShouldResembleProto, expectedRsp)
  1490  		})
  1491  		Convey("pagination", func() {
  1492  			req.PageSize = 1
  1493  			query := NewQuery(req)
  1494  			actualRsp, err := query.fetchOnTagIndex(ctx)
  1495  			expectedRsp := &pb.SearchBuildsResponse{
  1496  				Builds: []*pb.Build{
  1497  					{
  1498  						Id: 100,
  1499  						Builder: &pb.BuilderID{
  1500  							Project: "project",
  1501  							Bucket:  "bucket",
  1502  							Builder: "builder1",
  1503  						},
  1504  						Tags: []*pb.StringPair{
  1505  							{Key: "buildset", Value: "commit/git/abcd"},
  1506  						},
  1507  						Status: pb.Status_SUCCESS,
  1508  					},
  1509  				},
  1510  				NextPageToken: "id>100",
  1511  			}
  1512  
  1513  			So(err, ShouldBeNil)
  1514  			So(actualRsp, ShouldResembleProto, expectedRsp)
  1515  		})
  1516  		Convey("No permission on requested buckets", func() {
  1517  			ctx = auth.WithState(ctx, &authtest.FakeState{
  1518  				Identity: "user:none",
  1519  			})
  1520  			query := NewQuery(req)
  1521  			actualRsp, err := query.fetchOnTagIndex(ctx)
  1522  
  1523  			So(err, ShouldBeNil)
  1524  			So(actualRsp, ShouldResembleProto, &pb.SearchBuildsResponse{})
  1525  		})
  1526  	})
  1527  }
  1528  
  1529  func TestMinHeap(t *testing.T) {
  1530  	t.Parallel()
  1531  
  1532  	Convey("minHeap", t, func() {
  1533  		h := &minHeap{{BuildID: 2}, {BuildID: 1}, {BuildID: 5}}
  1534  
  1535  		heap.Init(h)
  1536  		heap.Push(h, &model.TagIndexEntry{BuildID: 3})
  1537  		var res []int64
  1538  		for h.Len() > 0 {
  1539  			res = append(res, heap.Pop(h).(*model.TagIndexEntry).BuildID)
  1540  		}
  1541  		So(res, ShouldResemble, []int64{1, 2, 3, 5})
  1542  	})
  1543  
  1544  }