go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/buildbucket/appengine/rpc/schedule_build_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 rpc
    16  
    17  import (
    18  	"context"
    19  	"math/rand"
    20  	"sort"
    21  	"strconv"
    22  	"strings"
    23  	"testing"
    24  	"time"
    25  
    26  	"github.com/golang/mock/gomock"
    27  	"google.golang.org/grpc/codes"
    28  	"google.golang.org/grpc/metadata"
    29  	grpcStatus "google.golang.org/grpc/status"
    30  	"google.golang.org/protobuf/types/known/durationpb"
    31  	"google.golang.org/protobuf/types/known/fieldmaskpb"
    32  	"google.golang.org/protobuf/types/known/structpb"
    33  	"google.golang.org/protobuf/types/known/timestamppb"
    34  
    35  	"go.chromium.org/luci/auth/identity"
    36  	"go.chromium.org/luci/common/clock/testclock"
    37  	"go.chromium.org/luci/common/data/rand/mathrand"
    38  	"go.chromium.org/luci/common/data/stringset"
    39  	"go.chromium.org/luci/common/errors"
    40  	"go.chromium.org/luci/common/logging"
    41  	"go.chromium.org/luci/common/logging/memlogger"
    42  	luciCmProto "go.chromium.org/luci/common/proto"
    43  	"go.chromium.org/luci/common/tsmon"
    44  	"go.chromium.org/luci/gae/filter/txndefer"
    45  	"go.chromium.org/luci/gae/impl/memory"
    46  	"go.chromium.org/luci/gae/service/datastore"
    47  	rdbPb "go.chromium.org/luci/resultdb/proto/v1"
    48  	"go.chromium.org/luci/server/auth"
    49  	"go.chromium.org/luci/server/auth/authtest"
    50  	"go.chromium.org/luci/server/caching"
    51  	"go.chromium.org/luci/server/caching/cachingtest"
    52  	"go.chromium.org/luci/server/tq"
    53  	"go.chromium.org/luci/server/tq/tqtesting"
    54  
    55  	bb "go.chromium.org/luci/buildbucket"
    56  	"go.chromium.org/luci/buildbucket/appengine/internal/buildtoken"
    57  	"go.chromium.org/luci/buildbucket/appengine/internal/clients"
    58  	"go.chromium.org/luci/buildbucket/appengine/internal/config"
    59  	"go.chromium.org/luci/buildbucket/appengine/internal/metrics"
    60  	"go.chromium.org/luci/buildbucket/appengine/internal/resultdb"
    61  	"go.chromium.org/luci/buildbucket/appengine/model"
    62  	"go.chromium.org/luci/buildbucket/appengine/rpc/testutil"
    63  	taskdefs "go.chromium.org/luci/buildbucket/appengine/tasks/defs"
    64  	"go.chromium.org/luci/buildbucket/bbperms"
    65  	pb "go.chromium.org/luci/buildbucket/proto"
    66  
    67  	. "github.com/smartystreets/goconvey/convey"
    68  	. "go.chromium.org/luci/common/testing/assertions"
    69  )
    70  
    71  func fv(vs ...any) []any {
    72  	ret := []any{"luci.project.bucket", "builder"}
    73  	return append(ret, vs...)
    74  }
    75  
    76  func TestScheduleBuild(t *testing.T) {
    77  	t.Parallel()
    78  
    79  	// Note: request deduplication IDs depend on a hash of this value.
    80  	const userID = identity.Identity("user:caller@example.com")
    81  
    82  	Convey("builderMatches", t, func() {
    83  		Convey("nil", func() {
    84  			So(builderMatches("", nil), ShouldBeTrue)
    85  			So(builderMatches("project/bucket/builder", nil), ShouldBeTrue)
    86  		})
    87  
    88  		Convey("empty", func() {
    89  			p := &pb.BuilderPredicate{}
    90  			So(builderMatches("", p), ShouldBeTrue)
    91  			So(builderMatches("project/bucket/builder", p), ShouldBeTrue)
    92  		})
    93  
    94  		Convey("regex", func() {
    95  			p := &pb.BuilderPredicate{
    96  				Regex: []string{
    97  					"project/bucket/.+",
    98  				},
    99  			}
   100  			So(builderMatches("", p), ShouldBeFalse)
   101  			So(builderMatches("project/bucket/builder", p), ShouldBeTrue)
   102  			So(builderMatches("project/other/builder", p), ShouldBeFalse)
   103  		})
   104  
   105  		Convey("regex exclude", func() {
   106  			p := &pb.BuilderPredicate{
   107  				RegexExclude: []string{
   108  					"project/bucket/.+",
   109  				},
   110  			}
   111  			So(builderMatches("", p), ShouldBeTrue)
   112  			So(builderMatches("project/bucket/builder", p), ShouldBeFalse)
   113  			So(builderMatches("project/other/builder", p), ShouldBeTrue)
   114  		})
   115  
   116  		Convey("regex exclude > regex", func() {
   117  			p := &pb.BuilderPredicate{
   118  				Regex: []string{
   119  					"project/bucket/.+",
   120  				},
   121  				RegexExclude: []string{
   122  					"project/bucket/builder",
   123  				},
   124  			}
   125  			So(builderMatches("", p), ShouldBeFalse)
   126  			So(builderMatches("project/bucket/builder", p), ShouldBeFalse)
   127  			So(builderMatches("project/bucket/other", p), ShouldBeTrue)
   128  		})
   129  	})
   130  
   131  	Convey("fetchBuilderConfigs", t, func() {
   132  		ctx := metrics.WithServiceInfo(memory.Use(context.Background()), "svc", "job", "ins")
   133  		datastore.GetTestable(ctx).AutoIndex(true)
   134  		datastore.GetTestable(ctx).Consistent(true)
   135  
   136  		testutil.PutBuilder(ctx, "project", "bucket 1", "builder 1", "")
   137  		testutil.PutBuilder(ctx, "project", "bucket 1", "builder 2", "")
   138  		testutil.PutBucket(ctx, "project", "bucket 1", &pb.Bucket{
   139  			Swarming: &pb.Swarming{},
   140  			Shadow:   "bucket 2",
   141  		})
   142  		testutil.PutBucket(ctx, "project", "bucket 2", &pb.Bucket{DynamicBuilderTemplate: &pb.Bucket_DynamicBuilderTemplate{}})
   143  
   144  		Convey("bucket not found", func() {
   145  			bldrIDs := []*pb.BuilderID{
   146  				{
   147  					Project: "project",
   148  					Bucket:  "bucket 3",
   149  					Builder: "builder 1",
   150  				},
   151  			}
   152  			bldrs, _, _, err := fetchBuilderConfigs(ctx, bldrIDs)
   153  			So(len(err.(errors.MultiError)), ShouldEqual, len(bldrIDs))
   154  			So(err, ShouldErrLike, "bucket not found")
   155  			So(bldrs["project/bucket 3"]["builder 1"], ShouldBeNil)
   156  		})
   157  
   158  		Convey("builder not found", func() {
   159  			bldrIDs := []*pb.BuilderID{
   160  				{
   161  					Project: "project",
   162  					Bucket:  "bucket 1",
   163  					Builder: "builder 3",
   164  				},
   165  			}
   166  			bldrs, _, _, err := fetchBuilderConfigs(ctx, bldrIDs)
   167  			So(len(err.(errors.MultiError)), ShouldEqual, len(bldrIDs))
   168  			So(err, ShouldErrLike, "builder not found")
   169  			So(bldrs["project/bucket 3"]["builder 1"], ShouldBeNil)
   170  		})
   171  
   172  		Convey("one found and the other not found", func() {
   173  			bldrIDs := []*pb.BuilderID{
   174  				{
   175  					Project: "project",
   176  					Bucket:  "bucket 1",
   177  					Builder: "builder 1",
   178  				},
   179  				{
   180  					Project: "project",
   181  					Bucket:  "bucket 1",
   182  					Builder: "builder 100",
   183  				},
   184  			}
   185  			bldrs, _, shadowMap, err := fetchBuilderConfigs(ctx, bldrIDs)
   186  			So(err.(errors.MultiError)[1], ShouldErrLike, "builder not found")
   187  			So(bldrs["project/bucket 3"]["builder 1"], ShouldBeNil)
   188  			So(bldrs["project/bucket 1"]["builder 1"], ShouldResembleProto, &pb.BuilderConfig{
   189  				Name:         "builder 1",
   190  				SwarmingHost: "host",
   191  			})
   192  			So(shadowMap, ShouldResemble, map[string]string{"project/bucket 1": "bucket 2"})
   193  		})
   194  
   195  		Convey("dynamic", func() {
   196  			bldrIDs := []*pb.BuilderID{
   197  				{
   198  					Project: "project",
   199  					Bucket:  "bucket 2",
   200  					Builder: "builder 1",
   201  				},
   202  			}
   203  			bldrs, dynamicBuckets, shadowMap, err := fetchBuilderConfigs(ctx, bldrIDs)
   204  			So(err, ShouldBeNil)
   205  			So(bldrs["project/bucket 2"]["builder 1"], ShouldBeNil)
   206  			So(len(dynamicBuckets), ShouldEqual, 1)
   207  			So(dynamicBuckets["project/bucket 2"], ShouldResembleProto, &pb.Bucket{
   208  				Name:                   "bucket 2",
   209  				DynamicBuilderTemplate: &pb.Bucket_DynamicBuilderTemplate{},
   210  			})
   211  			So(shadowMap, ShouldResemble, map[string]string{"project/bucket 2": ""})
   212  		})
   213  
   214  		Convey("one", func() {
   215  			bldrIDs := []*pb.BuilderID{
   216  				{
   217  					Project: "project",
   218  					Bucket:  "bucket 1",
   219  					Builder: "builder 1",
   220  				},
   221  			}
   222  			bldrs, _, _, err := fetchBuilderConfigs(ctx, bldrIDs)
   223  			So(err, ShouldBeNil)
   224  			So(bldrs["project/bucket 1"]["builder 1"], ShouldResembleProto, &pb.BuilderConfig{
   225  				Name:         "builder 1",
   226  				SwarmingHost: "host",
   227  			})
   228  		})
   229  
   230  		Convey("many", func() {
   231  			bldrIDs := []*pb.BuilderID{
   232  				{
   233  					Project: "project",
   234  					Bucket:  "bucket 1",
   235  					Builder: "builder 1",
   236  				},
   237  				{
   238  					Project: "project",
   239  					Bucket:  "bucket 1",
   240  					Builder: "builder 2",
   241  				},
   242  				{
   243  					Project: "project",
   244  					Bucket:  "bucket 2",
   245  					Builder: "builder 1",
   246  				},
   247  			}
   248  			bldrs, _, _, err := fetchBuilderConfigs(ctx, bldrIDs)
   249  			So(err, ShouldBeNil)
   250  			So(bldrs["project/bucket 1"]["builder 1"], ShouldResembleProto, &pb.BuilderConfig{
   251  				Name:         "builder 1",
   252  				SwarmingHost: "host",
   253  			})
   254  			So(bldrs["project/bucket 1"]["builder 2"], ShouldResembleProto, &pb.BuilderConfig{
   255  				Name:         "builder 2",
   256  				SwarmingHost: "host",
   257  			})
   258  			So(bldrs["project/bucket 2"]["builder 1"], ShouldBeNil)
   259  		})
   260  	})
   261  
   262  	Convey("generateBuildNumbers", t, func() {
   263  		ctx := metrics.WithServiceInfo(memory.Use(context.Background()), "svc", "job", "ins")
   264  		datastore.GetTestable(ctx).AutoIndex(true)
   265  		datastore.GetTestable(ctx).Consistent(true)
   266  
   267  		Convey("one", func() {
   268  			blds := []*model.Build{
   269  				{
   270  					Proto: &pb.Build{
   271  						Builder: &pb.BuilderID{
   272  							Project: "project",
   273  							Bucket:  "bucket",
   274  							Builder: "builder",
   275  						},
   276  					},
   277  				},
   278  			}
   279  			err := generateBuildNumbers(ctx, blds)
   280  			So(err, ShouldBeNil)
   281  			So(blds, ShouldResemble, []*model.Build{
   282  				{
   283  					Proto: &pb.Build{
   284  						Builder: &pb.BuilderID{
   285  							Project: "project",
   286  							Bucket:  "bucket",
   287  							Builder: "builder",
   288  						},
   289  						Number: 1,
   290  					},
   291  					Tags: []string{
   292  						"build_address:luci.project.bucket/builder/1",
   293  					},
   294  				},
   295  			})
   296  		})
   297  
   298  		Convey("many", func() {
   299  			blds := []*model.Build{
   300  				{
   301  					Proto: &pb.Build{
   302  						Builder: &pb.BuilderID{
   303  							Project: "project",
   304  							Bucket:  "bucket",
   305  							Builder: "builder1",
   306  						},
   307  					},
   308  				},
   309  				{
   310  					Proto: &pb.Build{
   311  						Builder: &pb.BuilderID{
   312  							Project: "project",
   313  							Bucket:  "bucket",
   314  							Builder: "builder2",
   315  						},
   316  					},
   317  				},
   318  				{
   319  					Proto: &pb.Build{
   320  						Builder: &pb.BuilderID{
   321  							Project: "project",
   322  							Bucket:  "bucket",
   323  							Builder: "builder1",
   324  						},
   325  					},
   326  				},
   327  			}
   328  			err := generateBuildNumbers(ctx, blds)
   329  			So(err, ShouldBeNil)
   330  			So(blds, ShouldResemble, []*model.Build{
   331  				{
   332  					Proto: &pb.Build{
   333  						Builder: &pb.BuilderID{
   334  							Project: "project",
   335  							Bucket:  "bucket",
   336  							Builder: "builder1",
   337  						},
   338  						Number: 1,
   339  					},
   340  					Tags: []string{
   341  						"build_address:luci.project.bucket/builder1/1",
   342  					},
   343  				},
   344  				{
   345  					Proto: &pb.Build{
   346  						Builder: &pb.BuilderID{
   347  							Project: "project",
   348  							Bucket:  "bucket",
   349  							Builder: "builder2",
   350  						},
   351  						Number: 1,
   352  					},
   353  					Tags: []string{
   354  						"build_address:luci.project.bucket/builder2/1",
   355  					},
   356  				},
   357  				{
   358  					Proto: &pb.Build{
   359  						Builder: &pb.BuilderID{
   360  							Project: "project",
   361  							Bucket:  "bucket",
   362  							Builder: "builder1",
   363  						},
   364  						Number: 2,
   365  					},
   366  					Tags: []string{
   367  						"build_address:luci.project.bucket/builder1/2",
   368  					},
   369  				},
   370  			})
   371  		})
   372  	})
   373  
   374  	Convey("scheduleBuilds", t, func() {
   375  		ctx := txndefer.FilterRDS(memory.Use(context.Background()))
   376  		ctx = metrics.WithServiceInfo(ctx, "svc", "job", "ins")
   377  		ctx = mathrand.Set(ctx, rand.New(rand.NewSource(0)))
   378  		ctx, _ = testclock.UseTime(ctx, testclock.TestRecentTimeUTC)
   379  		ctx, sch := tq.TestingContext(ctx, nil)
   380  		datastore.GetTestable(ctx).AutoIndex(true)
   381  		datastore.GetTestable(ctx).Consistent(true)
   382  		ctx, _ = tsmon.WithDummyInMemory(ctx)
   383  		ctx = installTestSecret(ctx)
   384  
   385  		store := tsmon.Store(ctx)
   386  		globalCfg := &pb.SettingsCfg{
   387  			Resultdb: &pb.ResultDBSettings{
   388  				Hostname: "rdbHost",
   389  			},
   390  			Swarming: &pb.SwarmingSettings{
   391  				BbagentPackage: &pb.SwarmingSettings_Package{
   392  					PackageName: "bbagent",
   393  					Version:     "bbagent-version",
   394  				},
   395  				KitchenPackage: &pb.SwarmingSettings_Package{
   396  					PackageName: "kitchen",
   397  					Version:     "kitchen-version",
   398  				},
   399  			},
   400  		}
   401  
   402  		// stripProtos strips the Proto field from each of the given *model.Builds,
   403  		// returning a slice whose ith index is the stripped *pb.Build value.
   404  		// Needed because model.Build.Proto can only be compared with ShouldResembleProto
   405  		// while model.Build can only be compared with ShouldResemble.
   406  		stripProtos := func(builds []*model.Build) []*pb.Build {
   407  			ret := make([]*pb.Build, len(builds))
   408  			for i, b := range builds {
   409  				if b == nil {
   410  					ret[i] = nil
   411  				} else {
   412  					ret[i] = b.Proto
   413  					b.Proto = nil
   414  				}
   415  			}
   416  			return ret
   417  		}
   418  
   419  		Convey("builder not found", func() {
   420  			Convey("error", func() {
   421  				req := &pb.ScheduleBuildRequest{
   422  					Builder: &pb.BuilderID{
   423  						Project: "project",
   424  						Bucket:  "bucket",
   425  						Builder: "builder",
   426  					},
   427  				}
   428  
   429  				blds, err := scheduleBuilds(ctx, globalCfg, req)
   430  				So(err, ShouldHaveLength, 1)
   431  				So(err.(errors.MultiError), ShouldErrLike, "error fetching builders")
   432  				So(blds, ShouldHaveLength, 1)
   433  				So(blds[0], ShouldBeNil)
   434  				So(sch.Tasks(), ShouldBeEmpty)
   435  				So(store.Get(ctx, metrics.V1.BuildCountCreated, time.Time{}, fv("")), ShouldBeNil)
   436  			})
   437  
   438  			Convey("dynamic", func() {
   439  				testutil.PutBucket(ctx, "project", "bucket", nil)
   440  				req := &pb.ScheduleBuildRequest{
   441  					Builder: &pb.BuilderID{
   442  						Project: "project",
   443  						Bucket:  "bucket",
   444  						Builder: "builder",
   445  					},
   446  					DryRun: true,
   447  				}
   448  
   449  				blds, err := scheduleBuilds(ctx, globalCfg, req)
   450  				So(err, ShouldBeNil)
   451  				So(stripProtos(blds), ShouldResembleProto, []*pb.Build{
   452  					{
   453  						Builder: &pb.BuilderID{
   454  							Project: "project",
   455  							Bucket:  "bucket",
   456  							Builder: "builder",
   457  						},
   458  						Exe: &pb.Executable{
   459  							Cmd: []string{"recipes"},
   460  						},
   461  						ExecutionTimeout: &durationpb.Duration{
   462  							Seconds: 10800,
   463  						},
   464  						GracePeriod: &durationpb.Duration{
   465  							Seconds: 30,
   466  						},
   467  						Infra: &pb.BuildInfra{
   468  							Bbagent: &pb.BuildInfra_BBAgent{
   469  								CacheDir:    "cache",
   470  								PayloadPath: "kitchen-checkout",
   471  							},
   472  							Buildbucket: &pb.BuildInfra_Buildbucket{
   473  								Hostname: "app.appspot.com",
   474  								Agent: &pb.BuildInfra_Buildbucket_Agent{
   475  									Input: &pb.BuildInfra_Buildbucket_Agent_Input{},
   476  									Purposes: map[string]pb.BuildInfra_Buildbucket_Agent_Purpose{
   477  										"kitchen-checkout": pb.BuildInfra_Buildbucket_Agent_PURPOSE_EXE_PAYLOAD,
   478  									},
   479  								},
   480  							},
   481  							Logdog: &pb.BuildInfra_LogDog{
   482  								Project: "project",
   483  							},
   484  							Resultdb: &pb.BuildInfra_ResultDB{
   485  								Hostname: "rdbHost",
   486  							},
   487  							Swarming: &pb.BuildInfra_Swarming{
   488  								Caches: []*pb.BuildInfra_Swarming_CacheEntry{
   489  									{
   490  										Name: "builder_1809c38861a9996b1748e4640234fbd089992359f6f23f62f68deb98528f5f2b_v2",
   491  										Path: "builder",
   492  										WaitForWarmCache: &durationpb.Duration{
   493  											Seconds: 240,
   494  										},
   495  									},
   496  								},
   497  								Priority: 30,
   498  							},
   499  						},
   500  						Input: &pb.Build_Input{
   501  							Properties: &structpb.Struct{},
   502  						},
   503  						SchedulingTimeout: &durationpb.Duration{
   504  							Seconds: 21600,
   505  						},
   506  						Tags: []*pb.StringPair{
   507  							{
   508  								Key:   "builder",
   509  								Value: "builder",
   510  							},
   511  						},
   512  					},
   513  				})
   514  				So(sch.Tasks(), ShouldBeEmpty)
   515  			})
   516  		})
   517  
   518  		Convey("dry run", func() {
   519  			testutil.PutBuilder(ctx, "project", "bucket", "builder", "")
   520  			testutil.PutBucket(ctx, "project", "bucket", nil)
   521  
   522  			Convey("mixed", func() {
   523  				reqs := []*pb.ScheduleBuildRequest{
   524  					{
   525  						Builder: &pb.BuilderID{
   526  							Project: "project",
   527  							Bucket:  "bucket",
   528  							Builder: "builder",
   529  						},
   530  					},
   531  					{
   532  						Builder: &pb.BuilderID{
   533  							Project: "project",
   534  							Bucket:  "bucket",
   535  							Builder: "builder",
   536  						},
   537  						DryRun: true,
   538  					},
   539  					{
   540  						Builder: &pb.BuilderID{
   541  							Project: "project",
   542  							Bucket:  "bucket",
   543  							Builder: "builder",
   544  						},
   545  						DryRun: false,
   546  					},
   547  				}
   548  
   549  				blds, err := scheduleBuilds(ctx, globalCfg, reqs...)
   550  				_, ok := err.(errors.MultiError)
   551  				So(ok, ShouldBeFalse)
   552  				So(err, ShouldErrLike, "all requests must have the same dry_run value")
   553  				So(blds, ShouldBeNil)
   554  				So(sch.Tasks(), ShouldBeEmpty)
   555  
   556  				// dry-run should not increase the build creation counter metric.
   557  				So(store.Get(ctx, metrics.V1.BuildCountCreated, time.Time{}, fv("")), ShouldBeNil)
   558  			})
   559  
   560  			Convey("one", func() {
   561  				req := &pb.ScheduleBuildRequest{
   562  					Builder: &pb.BuilderID{
   563  						Project: "project",
   564  						Bucket:  "bucket",
   565  						Builder: "builder",
   566  					},
   567  					DryRun: true,
   568  				}
   569  
   570  				blds, err := scheduleBuilds(ctx, globalCfg, req)
   571  				So(err, ShouldBeNil)
   572  				So(stripProtos(blds), ShouldResembleProto, []*pb.Build{
   573  					{
   574  						Builder: &pb.BuilderID{
   575  							Project: "project",
   576  							Bucket:  "bucket",
   577  							Builder: "builder",
   578  						},
   579  						Exe: &pb.Executable{
   580  							Cmd: []string{"recipes"},
   581  						},
   582  						ExecutionTimeout: &durationpb.Duration{
   583  							Seconds: 10800,
   584  						},
   585  						GracePeriod: &durationpb.Duration{
   586  							Seconds: 30,
   587  						},
   588  						Infra: &pb.BuildInfra{
   589  							Bbagent: &pb.BuildInfra_BBAgent{
   590  								CacheDir:    "cache",
   591  								PayloadPath: "kitchen-checkout",
   592  							},
   593  							Buildbucket: &pb.BuildInfra_Buildbucket{
   594  								Hostname: "app.appspot.com",
   595  								Agent: &pb.BuildInfra_Buildbucket_Agent{
   596  									Input: &pb.BuildInfra_Buildbucket_Agent_Input{},
   597  									Purposes: map[string]pb.BuildInfra_Buildbucket_Agent_Purpose{
   598  										"kitchen-checkout": pb.BuildInfra_Buildbucket_Agent_PURPOSE_EXE_PAYLOAD,
   599  									},
   600  								},
   601  							},
   602  							Logdog: &pb.BuildInfra_LogDog{
   603  								Project: "project",
   604  							},
   605  							Resultdb: &pb.BuildInfra_ResultDB{
   606  								Hostname: "rdbHost",
   607  							},
   608  							Swarming: &pb.BuildInfra_Swarming{
   609  								Hostname: "host",
   610  								Caches: []*pb.BuildInfra_Swarming_CacheEntry{
   611  									{
   612  										Name: "builder_1809c38861a9996b1748e4640234fbd089992359f6f23f62f68deb98528f5f2b_v2",
   613  										Path: "builder",
   614  										WaitForWarmCache: &durationpb.Duration{
   615  											Seconds: 240,
   616  										},
   617  									},
   618  								},
   619  								Priority: 30,
   620  							},
   621  						},
   622  						Input: &pb.Build_Input{
   623  							Properties: &structpb.Struct{},
   624  						},
   625  						SchedulingTimeout: &durationpb.Duration{
   626  							Seconds: 21600,
   627  						},
   628  						Tags: []*pb.StringPair{
   629  							{
   630  								Key:   "builder",
   631  								Value: "builder",
   632  							},
   633  						},
   634  					},
   635  				})
   636  				So(sch.Tasks(), ShouldBeEmpty)
   637  			})
   638  		})
   639  
   640  		Convey("zero", func() {
   641  			blds, err := scheduleBuilds(ctx, nil)
   642  			So(err, ShouldBeNil)
   643  			So(blds, ShouldBeEmpty)
   644  			So(sch.Tasks(), ShouldBeEmpty)
   645  		})
   646  
   647  		Convey("one", func() {
   648  			req := &pb.ScheduleBuildRequest{
   649  				Builder: &pb.BuilderID{
   650  					Project: "project",
   651  					Bucket:  "bucket",
   652  					Builder: "builder",
   653  				},
   654  				Notify: &pb.NotificationConfig{
   655  					PubsubTopic: "topic",
   656  					UserData:    []byte("data"),
   657  				},
   658  				Tags: []*pb.StringPair{
   659  					{
   660  						Key:   "buildset",
   661  						Value: "buildset",
   662  					},
   663  					{
   664  						Key:   "user_agent",
   665  						Value: "gerrit",
   666  					},
   667  				},
   668  			}
   669  			testutil.PutBuilder(ctx, "project", "bucket", "builder", "")
   670  			testutil.PutBucket(ctx, "project", "bucket", nil)
   671  
   672  			blds, err := scheduleBuilds(ctx, globalCfg, req)
   673  			So(err, ShouldBeNil)
   674  			So(store.Get(ctx, metrics.V1.BuildCountCreated, time.Time{}, fv("gerrit")), ShouldEqual, 1)
   675  			So(stripProtos(blds), ShouldResembleProto, []*pb.Build{
   676  				{
   677  					Builder: &pb.BuilderID{
   678  						Project: "project",
   679  						Bucket:  "bucket",
   680  						Builder: "builder",
   681  					},
   682  					CreatedBy:  "anonymous:anonymous",
   683  					CreateTime: timestamppb.New(testclock.TestRecentTimeUTC),
   684  					UpdateTime: timestamppb.New(testclock.TestRecentTimeUTC),
   685  					Exe: &pb.Executable{
   686  						Cmd: []string{"recipes"},
   687  					},
   688  					ExecutionTimeout: &durationpb.Duration{
   689  						Seconds: 10800,
   690  					},
   691  					GracePeriod: &durationpb.Duration{
   692  						Seconds: 30,
   693  					},
   694  					Id: 9021868963221667745,
   695  					Infra: &pb.BuildInfra{
   696  						Bbagent: &pb.BuildInfra_BBAgent{
   697  							CacheDir:    "cache",
   698  							PayloadPath: "kitchen-checkout",
   699  						},
   700  						Buildbucket: &pb.BuildInfra_Buildbucket{
   701  							Hostname: "app.appspot.com",
   702  							Agent: &pb.BuildInfra_Buildbucket_Agent{
   703  								Input: &pb.BuildInfra_Buildbucket_Agent_Input{},
   704  								Purposes: map[string]pb.BuildInfra_Buildbucket_Agent_Purpose{
   705  									"kitchen-checkout": pb.BuildInfra_Buildbucket_Agent_PURPOSE_EXE_PAYLOAD,
   706  								},
   707  							},
   708  						},
   709  						Logdog: &pb.BuildInfra_LogDog{
   710  							Prefix:  "buildbucket/app/9021868963221667745",
   711  							Project: "project",
   712  						},
   713  						Resultdb: &pb.BuildInfra_ResultDB{
   714  							Hostname: "rdbHost",
   715  						},
   716  						Swarming: &pb.BuildInfra_Swarming{
   717  							Hostname: "host",
   718  							Caches: []*pb.BuildInfra_Swarming_CacheEntry{
   719  								{
   720  									Name: "builder_1809c38861a9996b1748e4640234fbd089992359f6f23f62f68deb98528f5f2b_v2",
   721  									Path: "builder",
   722  									WaitForWarmCache: &durationpb.Duration{
   723  										Seconds: 240,
   724  									},
   725  								},
   726  							},
   727  							Priority: 30,
   728  						},
   729  					},
   730  					Input: &pb.Build_Input{
   731  						Properties: &structpb.Struct{},
   732  					},
   733  					SchedulingTimeout: &durationpb.Duration{
   734  						Seconds: 21600,
   735  					},
   736  					Status: pb.Status_SCHEDULED,
   737  					Tags: []*pb.StringPair{
   738  						{
   739  							Key:   "builder",
   740  							Value: "builder",
   741  						},
   742  						{
   743  							Key:   "buildset",
   744  							Value: "buildset",
   745  						},
   746  						{
   747  							Key:   "user_agent",
   748  							Value: "gerrit",
   749  						},
   750  					},
   751  				},
   752  			})
   753  			So(blds, ShouldResemble, []*model.Build{
   754  				{
   755  					ID:                9021868963221667745,
   756  					BucketID:          "project/bucket",
   757  					BuilderID:         "project/bucket/builder",
   758  					CreatedBy:         "anonymous:anonymous",
   759  					CreateTime:        testclock.TestRecentTimeUTC,
   760  					StatusChangedTime: testclock.TestRecentTimeUTC,
   761  					Experiments:       nil,
   762  					Incomplete:        true,
   763  					IsLuci:            true,
   764  					Status:            pb.Status_SCHEDULED,
   765  					Tags: []string{
   766  						"builder:builder",
   767  						"buildset:buildset",
   768  						"user_agent:gerrit",
   769  					},
   770  					Project: "project",
   771  					PubSubCallback: model.PubSubCallback{
   772  						Topic:    "topic",
   773  						UserData: []byte("data"),
   774  					},
   775  					LegacyProperties: model.LegacyProperties{
   776  						Status: model.Scheduled,
   777  					},
   778  				},
   779  			})
   780  			tasks := sch.Tasks()
   781  			So(tasks, ShouldHaveLength, 4)
   782  			sortTasksByClassName(tasks)
   783  			So(tasks.Payloads()[0], ShouldResembleProto, &taskdefs.CreateSwarmingTask{
   784  				BuildId: 9021868963221667745,
   785  			})
   786  			// for `builds` topic.
   787  			So(tasks.Payloads()[1], ShouldResembleProto, &taskdefs.NotifyPubSub{
   788  				BuildId: 9021868963221667745,
   789  			})
   790  			// for topic in build.PubSubCallback.topic field.
   791  			So(tasks.Payloads()[2], ShouldResembleProto, &taskdefs.NotifyPubSubGo{
   792  				BuildId: 9021868963221667745,
   793  				Topic: &pb.BuildbucketCfg_Topic{
   794  					Name: "topic",
   795  				},
   796  				Callback: true,
   797  			})
   798  			// for `bulids_v2` topic
   799  			So(tasks.Payloads()[3], ShouldResembleProto, &taskdefs.NotifyPubSubGoProxy{
   800  				BuildId: 9021868963221667745,
   801  				Project: "project",
   802  			})
   803  
   804  			So(datastore.Get(ctx, blds), ShouldBeNil)
   805  
   806  			ind, err := model.SearchTagIndex(ctx, "buildset", "buildset")
   807  			So(err, ShouldBeNil)
   808  			So(ind, ShouldResemble, []*model.TagIndexEntry{
   809  				{
   810  					BuildID:     9021868963221667745,
   811  					BucketID:    "project/bucket",
   812  					CreatedTime: datastore.RoundTime(testclock.TestRecentTimeUTC),
   813  				},
   814  			})
   815  		})
   816  
   817  		Convey("many", func() {
   818  			reqs := []*pb.ScheduleBuildRequest{
   819  				{
   820  					Builder: &pb.BuilderID{
   821  						Project: "project",
   822  						Bucket:  "static bucket",
   823  						Builder: "static builder",
   824  					},
   825  					Critical:  pb.Trinary_UNSET,
   826  					Retriable: pb.Trinary_UNSET,
   827  				},
   828  				{
   829  					Builder: &pb.BuilderID{
   830  						Project: "project",
   831  						Bucket:  "static bucket",
   832  						Builder: "static builder",
   833  					},
   834  					Critical:  pb.Trinary_YES,
   835  					Retriable: pb.Trinary_YES,
   836  				},
   837  				{
   838  					Builder: &pb.BuilderID{
   839  						Project: "project",
   840  						Bucket:  "dynamic bucket",
   841  						Builder: "dynamic builder",
   842  					},
   843  					Critical:  pb.Trinary_NO,
   844  					Retriable: pb.Trinary_NO,
   845  				},
   846  			}
   847  			testutil.PutBuilder(ctx, "project", "static bucket", "static builder", "")
   848  			testutil.PutBucket(ctx, "project", "static bucket", &pb.Bucket{Swarming: &pb.Swarming{}})
   849  			testutil.PutBucket(ctx, "project", "dynamic bucket", &pb.Bucket{DynamicBuilderTemplate: &pb.Bucket_DynamicBuilderTemplate{}})
   850  
   851  			blds, err := scheduleBuilds(ctx, globalCfg, reqs...)
   852  			So(err, ShouldBeNil)
   853  
   854  			fvs := []any{"luci.project.static bucket", "static builder", ""}
   855  			So(store.Get(ctx, metrics.V1.BuildCountCreated, time.Time{}, fvs), ShouldEqual, 2)
   856  			fvs = []any{"luci.project.dynamic bucket", "dynamic builder", ""}
   857  			So(store.Get(ctx, metrics.V1.BuildCountCreated, time.Time{}, fvs), ShouldEqual, 1)
   858  
   859  			So(stripProtos(blds), ShouldResembleProto, []*pb.Build{
   860  				{
   861  					Builder: &pb.BuilderID{
   862  						Project: "project",
   863  						Bucket:  "static bucket",
   864  						Builder: "static builder",
   865  					},
   866  					CreatedBy:  "anonymous:anonymous",
   867  					CreateTime: timestamppb.New(testclock.TestRecentTimeUTC),
   868  					UpdateTime: timestamppb.New(testclock.TestRecentTimeUTC),
   869  					Exe: &pb.Executable{
   870  						Cmd: []string{"recipes"},
   871  					},
   872  					ExecutionTimeout: &durationpb.Duration{
   873  						Seconds: 10800,
   874  					},
   875  					GracePeriod: &durationpb.Duration{
   876  						Seconds: 30,
   877  					},
   878  					Id: 9021868963221610337,
   879  					Infra: &pb.BuildInfra{
   880  						Bbagent: &pb.BuildInfra_BBAgent{
   881  							CacheDir:    "cache",
   882  							PayloadPath: "kitchen-checkout",
   883  						},
   884  						Buildbucket: &pb.BuildInfra_Buildbucket{
   885  							Hostname: "app.appspot.com",
   886  							Agent: &pb.BuildInfra_Buildbucket_Agent{
   887  								Input: &pb.BuildInfra_Buildbucket_Agent_Input{},
   888  								Purposes: map[string]pb.BuildInfra_Buildbucket_Agent_Purpose{
   889  									"kitchen-checkout": pb.BuildInfra_Buildbucket_Agent_PURPOSE_EXE_PAYLOAD,
   890  								},
   891  							},
   892  						},
   893  						Logdog: &pb.BuildInfra_LogDog{
   894  							Prefix:  "buildbucket/app/9021868963221610337",
   895  							Project: "project",
   896  						},
   897  						Resultdb: &pb.BuildInfra_ResultDB{
   898  							Hostname: "rdbHost",
   899  						},
   900  						Swarming: &pb.BuildInfra_Swarming{
   901  							Hostname: "host",
   902  							Caches: []*pb.BuildInfra_Swarming_CacheEntry{
   903  								{
   904  									Name: "builder_943d53aa636f1497a9367662af111471018b08dcd116ae5405ff9fab3b2d5682_v2",
   905  									Path: "builder",
   906  									WaitForWarmCache: &durationpb.Duration{
   907  										Seconds: 240,
   908  									},
   909  								},
   910  							},
   911  							Priority: 30,
   912  						},
   913  					},
   914  					Input: &pb.Build_Input{
   915  						Properties: &structpb.Struct{},
   916  					},
   917  					SchedulingTimeout: &durationpb.Duration{
   918  						Seconds: 21600,
   919  					},
   920  					Status: pb.Status_SCHEDULED,
   921  					Tags: []*pb.StringPair{
   922  						{
   923  							Key:   "builder",
   924  							Value: "static builder",
   925  						},
   926  					},
   927  				},
   928  				{
   929  					Builder: &pb.BuilderID{
   930  						Project: "project",
   931  						Bucket:  "static bucket",
   932  						Builder: "static builder",
   933  					},
   934  					CreatedBy:  "anonymous:anonymous",
   935  					CreateTime: timestamppb.New(testclock.TestRecentTimeUTC),
   936  					UpdateTime: timestamppb.New(testclock.TestRecentTimeUTC),
   937  					Exe: &pb.Executable{
   938  						Cmd: []string{"recipes"},
   939  					},
   940  					Critical:  pb.Trinary_YES,
   941  					Retriable: pb.Trinary_YES,
   942  					ExecutionTimeout: &durationpb.Duration{
   943  						Seconds: 10800,
   944  					},
   945  					GracePeriod: &durationpb.Duration{
   946  						Seconds: 30,
   947  					},
   948  					Id: 9021868963221610321,
   949  					Infra: &pb.BuildInfra{
   950  						Bbagent: &pb.BuildInfra_BBAgent{
   951  							CacheDir:    "cache",
   952  							PayloadPath: "kitchen-checkout",
   953  						},
   954  						Buildbucket: &pb.BuildInfra_Buildbucket{
   955  							Hostname: "app.appspot.com",
   956  							Agent: &pb.BuildInfra_Buildbucket_Agent{
   957  								Input: &pb.BuildInfra_Buildbucket_Agent_Input{},
   958  								Purposes: map[string]pb.BuildInfra_Buildbucket_Agent_Purpose{
   959  									"kitchen-checkout": pb.BuildInfra_Buildbucket_Agent_PURPOSE_EXE_PAYLOAD,
   960  								},
   961  							},
   962  						},
   963  						Logdog: &pb.BuildInfra_LogDog{
   964  							Prefix:  "buildbucket/app/9021868963221610321",
   965  							Project: "project",
   966  						},
   967  						Resultdb: &pb.BuildInfra_ResultDB{
   968  							Hostname: "rdbHost",
   969  						},
   970  						Swarming: &pb.BuildInfra_Swarming{
   971  							Hostname: "host",
   972  							Caches: []*pb.BuildInfra_Swarming_CacheEntry{
   973  								{
   974  									Name: "builder_943d53aa636f1497a9367662af111471018b08dcd116ae5405ff9fab3b2d5682_v2",
   975  									Path: "builder",
   976  									WaitForWarmCache: &durationpb.Duration{
   977  										Seconds: 240,
   978  									},
   979  								},
   980  							},
   981  							Priority: 30,
   982  						},
   983  					},
   984  					Input: &pb.Build_Input{
   985  						Properties: &structpb.Struct{},
   986  					},
   987  					SchedulingTimeout: &durationpb.Duration{
   988  						Seconds: 21600,
   989  					},
   990  					Status: pb.Status_SCHEDULED,
   991  					Tags: []*pb.StringPair{
   992  						{
   993  							Key:   "builder",
   994  							Value: "static builder",
   995  						},
   996  					},
   997  				},
   998  				{
   999  					Builder: &pb.BuilderID{
  1000  						Project: "project",
  1001  						Bucket:  "dynamic bucket",
  1002  						Builder: "dynamic builder",
  1003  					},
  1004  					CreatedBy:  "anonymous:anonymous",
  1005  					CreateTime: timestamppb.New(testclock.TestRecentTimeUTC),
  1006  					UpdateTime: timestamppb.New(testclock.TestRecentTimeUTC),
  1007  					Exe: &pb.Executable{
  1008  						Cmd: []string{"recipes"},
  1009  					},
  1010  					Critical:  pb.Trinary_NO,
  1011  					Retriable: pb.Trinary_NO,
  1012  					ExecutionTimeout: &durationpb.Duration{
  1013  						Seconds: 10800,
  1014  					},
  1015  					GracePeriod: &durationpb.Duration{
  1016  						Seconds: 30,
  1017  					},
  1018  					Id: 9021868963221610305,
  1019  					Infra: &pb.BuildInfra{
  1020  						Bbagent: &pb.BuildInfra_BBAgent{
  1021  							CacheDir:    "cache",
  1022  							PayloadPath: "kitchen-checkout",
  1023  						},
  1024  						Buildbucket: &pb.BuildInfra_Buildbucket{
  1025  							Hostname: "app.appspot.com",
  1026  							Agent: &pb.BuildInfra_Buildbucket_Agent{
  1027  								Input: &pb.BuildInfra_Buildbucket_Agent_Input{},
  1028  								Purposes: map[string]pb.BuildInfra_Buildbucket_Agent_Purpose{
  1029  									"kitchen-checkout": pb.BuildInfra_Buildbucket_Agent_PURPOSE_EXE_PAYLOAD,
  1030  								},
  1031  							},
  1032  						},
  1033  						Logdog: &pb.BuildInfra_LogDog{
  1034  							Prefix:  "buildbucket/app/9021868963221610305",
  1035  							Project: "project",
  1036  						},
  1037  						Resultdb: &pb.BuildInfra_ResultDB{
  1038  							Hostname: "rdbHost",
  1039  						},
  1040  						Swarming: &pb.BuildInfra_Swarming{
  1041  							Caches: []*pb.BuildInfra_Swarming_CacheEntry{
  1042  								{
  1043  									Name: "builder_e229fa0169afaeb5fa8340560ffb3c5fe529169e0207f7378bd115cd74977bd2_v2",
  1044  									Path: "builder",
  1045  									WaitForWarmCache: &durationpb.Duration{
  1046  										Seconds: 240,
  1047  									},
  1048  								},
  1049  							},
  1050  							Priority: 30,
  1051  						},
  1052  					},
  1053  					Input: &pb.Build_Input{
  1054  						Properties: &structpb.Struct{},
  1055  					},
  1056  					SchedulingTimeout: &durationpb.Duration{
  1057  						Seconds: 21600,
  1058  					},
  1059  					Status: pb.Status_SCHEDULED,
  1060  					Tags: []*pb.StringPair{
  1061  						{
  1062  							Key:   "builder",
  1063  							Value: "dynamic builder",
  1064  						},
  1065  					},
  1066  				},
  1067  			})
  1068  
  1069  			So(blds, ShouldResemble, []*model.Build{
  1070  				{
  1071  					ID:                9021868963221610337,
  1072  					BucketID:          "project/static bucket",
  1073  					BuilderID:         "project/static bucket/static builder",
  1074  					CreatedBy:         "anonymous:anonymous",
  1075  					CreateTime:        testclock.TestRecentTimeUTC,
  1076  					StatusChangedTime: testclock.TestRecentTimeUTC,
  1077  					Incomplete:        true,
  1078  					IsLuci:            true,
  1079  					Status:            pb.Status_SCHEDULED,
  1080  					Tags: []string{
  1081  						"builder:static builder",
  1082  					},
  1083  					Project: "project",
  1084  					LegacyProperties: model.LegacyProperties{
  1085  						Status: model.Scheduled,
  1086  					},
  1087  				},
  1088  				{
  1089  					ID:                9021868963221610321,
  1090  					BucketID:          "project/static bucket",
  1091  					BuilderID:         "project/static bucket/static builder",
  1092  					CreatedBy:         "anonymous:anonymous",
  1093  					CreateTime:        testclock.TestRecentTimeUTC,
  1094  					StatusChangedTime: testclock.TestRecentTimeUTC,
  1095  					Incomplete:        true,
  1096  					IsLuci:            true,
  1097  					Status:            pb.Status_SCHEDULED,
  1098  					Tags: []string{
  1099  						"builder:static builder",
  1100  					},
  1101  					Project: "project",
  1102  					LegacyProperties: model.LegacyProperties{
  1103  						Status: model.Scheduled,
  1104  					},
  1105  				},
  1106  				{
  1107  					ID:                9021868963221610305,
  1108  					BucketID:          "project/dynamic bucket",
  1109  					BuilderID:         "project/dynamic bucket/dynamic builder",
  1110  					CreatedBy:         "anonymous:anonymous",
  1111  					CreateTime:        testclock.TestRecentTimeUTC,
  1112  					StatusChangedTime: testclock.TestRecentTimeUTC,
  1113  					Incomplete:        true,
  1114  					IsLuci:            true,
  1115  					Status:            pb.Status_SCHEDULED,
  1116  					Tags: []string{
  1117  						"builder:dynamic builder",
  1118  					},
  1119  					Project: "project",
  1120  					LegacyProperties: model.LegacyProperties{
  1121  						Status: model.Scheduled,
  1122  					},
  1123  				},
  1124  			})
  1125  
  1126  			So(sch.Tasks(), ShouldHaveLength, 6)
  1127  			So(datastore.Get(ctx, blds), ShouldBeNil)
  1128  		})
  1129  
  1130  		Convey("one success and one failure", func() {
  1131  			reqs := []*pb.ScheduleBuildRequest{
  1132  				{
  1133  					Builder: &pb.BuilderID{
  1134  						Project: "project",
  1135  						Bucket:  "bucket",
  1136  						Builder: "builder",
  1137  					},
  1138  					Notify: &pb.NotificationConfig{
  1139  						PubsubTopic: "topic",
  1140  						UserData:    []byte("data"),
  1141  					},
  1142  					Tags: []*pb.StringPair{
  1143  						{
  1144  							Key:   "buildset",
  1145  							Value: "buildset",
  1146  						},
  1147  						{
  1148  							Key:   "user_agent",
  1149  							Value: "gerrit",
  1150  						},
  1151  					},
  1152  				},
  1153  				{
  1154  					RequestId: "dupReqIdWithoutBuildAssociated",
  1155  					Builder: &pb.BuilderID{
  1156  						Project: "project",
  1157  						Bucket:  "bucket",
  1158  						Builder: "builder",
  1159  					},
  1160  					Notify: &pb.NotificationConfig{
  1161  						PubsubTopic: "topic",
  1162  						UserData:    []byte("data"),
  1163  					},
  1164  					Tags: []*pb.StringPair{
  1165  						{
  1166  							Key:   "buildset",
  1167  							Value: "buildset",
  1168  						},
  1169  						{
  1170  							Key:   "user_agent",
  1171  							Value: "gerrit",
  1172  						},
  1173  					},
  1174  				},
  1175  			}
  1176  			r := model.NewRequestID(ctx, 0, time.Time{}, "dupReqIdWithoutBuildAssociated")
  1177  			So(datastore.Put(ctx,
  1178  				r,
  1179  				&model.Builder{
  1180  					Parent: model.BucketKey(ctx, "project", "bucket"),
  1181  					ID:     "builder",
  1182  					Config: &pb.BuilderConfig{
  1183  						Name:         "builder",
  1184  						SwarmingHost: "host",
  1185  					},
  1186  				}), ShouldBeNil)
  1187  			testutil.PutBucket(ctx, "project", "bucket", nil)
  1188  
  1189  			blds, err := scheduleBuilds(ctx, globalCfg, reqs...)
  1190  			So(err.(errors.MultiError), ShouldHaveLength, 2)
  1191  			So(err.(errors.MultiError)[1], ShouldErrLike, "failed to fetch deduplicated build")
  1192  			So(store.Get(ctx, metrics.V1.BuildCountCreated, time.Time{}, fv("gerrit")), ShouldEqual, 1)
  1193  			So(stripProtos(blds), ShouldResembleProto, []*pb.Build{
  1194  				{
  1195  					Builder: &pb.BuilderID{
  1196  						Project: "project",
  1197  						Bucket:  "bucket",
  1198  						Builder: "builder",
  1199  					},
  1200  					CreatedBy:  "anonymous:anonymous",
  1201  					CreateTime: timestamppb.New(testclock.TestRecentTimeUTC),
  1202  					UpdateTime: timestamppb.New(testclock.TestRecentTimeUTC),
  1203  					Exe: &pb.Executable{
  1204  						Cmd: []string{"recipes"},
  1205  					},
  1206  					ExecutionTimeout: &durationpb.Duration{
  1207  						Seconds: 10800,
  1208  					},
  1209  					GracePeriod: &durationpb.Duration{
  1210  						Seconds: 30,
  1211  					},
  1212  					Id: 9021868963222163313,
  1213  					Infra: &pb.BuildInfra{
  1214  						Bbagent: &pb.BuildInfra_BBAgent{
  1215  							CacheDir:    "cache",
  1216  							PayloadPath: "kitchen-checkout",
  1217  						},
  1218  						Buildbucket: &pb.BuildInfra_Buildbucket{
  1219  							Hostname: "app.appspot.com",
  1220  							Agent: &pb.BuildInfra_Buildbucket_Agent{
  1221  								Input: &pb.BuildInfra_Buildbucket_Agent_Input{},
  1222  								Purposes: map[string]pb.BuildInfra_Buildbucket_Agent_Purpose{
  1223  									"kitchen-checkout": pb.BuildInfra_Buildbucket_Agent_PURPOSE_EXE_PAYLOAD,
  1224  								},
  1225  							},
  1226  						},
  1227  						Logdog: &pb.BuildInfra_LogDog{
  1228  							Prefix:  "buildbucket/app/9021868963222163313",
  1229  							Project: "project",
  1230  						},
  1231  						Resultdb: &pb.BuildInfra_ResultDB{
  1232  							Hostname: "rdbHost",
  1233  						},
  1234  						Swarming: &pb.BuildInfra_Swarming{
  1235  							Hostname: "host",
  1236  							Caches: []*pb.BuildInfra_Swarming_CacheEntry{
  1237  								{
  1238  									Name: "builder_1809c38861a9996b1748e4640234fbd089992359f6f23f62f68deb98528f5f2b_v2",
  1239  									Path: "builder",
  1240  									WaitForWarmCache: &durationpb.Duration{
  1241  										Seconds: 240,
  1242  									},
  1243  								},
  1244  							},
  1245  							Priority: 30,
  1246  						},
  1247  					},
  1248  					Input: &pb.Build_Input{
  1249  						Properties: &structpb.Struct{},
  1250  					},
  1251  					SchedulingTimeout: &durationpb.Duration{
  1252  						Seconds: 21600,
  1253  					},
  1254  					Status: pb.Status_SCHEDULED,
  1255  					Tags: []*pb.StringPair{
  1256  						{
  1257  							Key:   "builder",
  1258  							Value: "builder",
  1259  						},
  1260  						{
  1261  							Key:   "buildset",
  1262  							Value: "buildset",
  1263  						},
  1264  						{
  1265  							Key:   "user_agent",
  1266  							Value: "gerrit",
  1267  						},
  1268  					},
  1269  				},
  1270  				nil,
  1271  			})
  1272  			So(blds, ShouldResemble, []*model.Build{
  1273  				{
  1274  					ID:                9021868963222163313,
  1275  					BucketID:          "project/bucket",
  1276  					BuilderID:         "project/bucket/builder",
  1277  					CreatedBy:         "anonymous:anonymous",
  1278  					CreateTime:        testclock.TestRecentTimeUTC,
  1279  					StatusChangedTime: testclock.TestRecentTimeUTC,
  1280  					Incomplete:        true,
  1281  					IsLuci:            true,
  1282  					Status:            pb.Status_SCHEDULED,
  1283  					Tags: []string{
  1284  						"builder:builder",
  1285  						"buildset:buildset",
  1286  						"user_agent:gerrit",
  1287  					},
  1288  					Project: "project",
  1289  					PubSubCallback: model.PubSubCallback{
  1290  						Topic:    "topic",
  1291  						UserData: []byte("data"),
  1292  					},
  1293  					LegacyProperties: model.LegacyProperties{
  1294  						Status: model.Scheduled,
  1295  					},
  1296  				},
  1297  				nil,
  1298  			})
  1299  			So(sch.Tasks(), ShouldHaveLength, 4)
  1300  			So(datastore.Get(ctx, blds[0]), ShouldBeNil)
  1301  
  1302  			ind, err := model.SearchTagIndex(ctx, "buildset", "buildset")
  1303  			So(err, ShouldBeNil)
  1304  			// TagIndexEntry for the 2nd req should exist but its build entity shouldn't.
  1305  			// Because an error was thrown in the build creation transaction which is after the TagIndex update.
  1306  			So(ind, ShouldResemble, []*model.TagIndexEntry{
  1307  				{
  1308  					BuildID:     9021868963222163313,
  1309  					BucketID:    "project/bucket",
  1310  					CreatedTime: datastore.RoundTime(testclock.TestRecentTimeUTC),
  1311  				},
  1312  				{
  1313  					BuildID:     9021868963222163297,
  1314  					BucketID:    "project/bucket",
  1315  					CreatedTime: datastore.RoundTime(testclock.TestRecentTimeUTC),
  1316  				},
  1317  			})
  1318  			So(datastore.Get(ctx, &model.Build{ID: 9021868963222163297}), ShouldErrLike, "no such entity")
  1319  		})
  1320  
  1321  		Convey("one with parent", func() {
  1322  			tk, err := buildtoken.GenerateToken(ctx, 1, pb.TokenBody_BUILD)
  1323  			So(err, ShouldBeNil)
  1324  
  1325  			testutil.PutBuilder(ctx, "project", "bucket", "builder", "")
  1326  			testutil.PutBuilder(ctx, "project", "bucket", "builder", "")
  1327  			testutil.PutBucket(ctx, "project", "bucket", nil)
  1328  
  1329  			So(datastore.Put(ctx, &model.Build{
  1330  				Proto: &pb.Build{
  1331  					Id: 1,
  1332  					Builder: &pb.BuilderID{
  1333  						Project: "project",
  1334  						Bucket:  "bucket",
  1335  						Builder: "builder",
  1336  					},
  1337  					Status:      pb.Status_STARTED,
  1338  					AncestorIds: []int64{2, 3},
  1339  				},
  1340  				UpdateToken: tk,
  1341  			}), ShouldBeNil)
  1342  
  1343  			bld := &model.Build{ID: 1}
  1344  			So(datastore.Get(ctx, bld), ShouldBeNil)
  1345  
  1346  			key := datastore.KeyForObj(ctx, bld)
  1347  			So(datastore.Put(ctx, &model.BuildInfra{
  1348  				Build: key,
  1349  				Proto: &pb.BuildInfra{
  1350  					Swarming: &pb.BuildInfra_Swarming{
  1351  						TaskId: "544239050",
  1352  					},
  1353  				},
  1354  			}), ShouldBeNil)
  1355  
  1356  			req := &pb.ScheduleBuildRequest{
  1357  				Builder: &pb.BuilderID{
  1358  					Project: "project",
  1359  					Bucket:  "bucket",
  1360  					Builder: "builder",
  1361  				},
  1362  				Notify: &pb.NotificationConfig{
  1363  					PubsubTopic: "topic",
  1364  					UserData:    []byte("data"),
  1365  				},
  1366  				Tags: []*pb.StringPair{
  1367  					{
  1368  						Key:   "buildset",
  1369  						Value: "buildset",
  1370  					},
  1371  					{
  1372  						Key:   "buildset",
  1373  						Value: "buildset",
  1374  					},
  1375  					{
  1376  						Key:   "user_agent",
  1377  						Value: "gerrit",
  1378  					},
  1379  				},
  1380  				CanOutliveParent: pb.Trinary_NO,
  1381  			}
  1382  			ctx := metadata.NewIncomingContext(ctx, metadata.Pairs(bb.BuildbucketTokenHeader, tk))
  1383  			blds, err := scheduleBuilds(ctx, globalCfg, req)
  1384  			So(err, ShouldBeNil)
  1385  			So(store.Get(ctx, metrics.V1.BuildCountCreated, time.Time{}, fv("gerrit")), ShouldEqual, 1)
  1386  			So(stripProtos(blds), ShouldResembleProto, []*pb.Build{
  1387  				{
  1388  					Builder: &pb.BuilderID{
  1389  						Project: "project",
  1390  						Bucket:  "bucket",
  1391  						Builder: "builder",
  1392  					},
  1393  					CreatedBy:  "anonymous:anonymous",
  1394  					CreateTime: timestamppb.New(testclock.TestRecentTimeUTC),
  1395  					UpdateTime: timestamppb.New(testclock.TestRecentTimeUTC),
  1396  					Exe: &pb.Executable{
  1397  						Cmd: []string{"recipes"},
  1398  					},
  1399  					ExecutionTimeout: &durationpb.Duration{
  1400  						Seconds: 10800,
  1401  					},
  1402  					GracePeriod: &durationpb.Duration{
  1403  						Seconds: 30,
  1404  					},
  1405  					Id: 9021868963221667745,
  1406  					Infra: &pb.BuildInfra{
  1407  						Bbagent: &pb.BuildInfra_BBAgent{
  1408  							CacheDir:    "cache",
  1409  							PayloadPath: "kitchen-checkout",
  1410  						},
  1411  						Buildbucket: &pb.BuildInfra_Buildbucket{
  1412  							Hostname: "app.appspot.com",
  1413  							Agent: &pb.BuildInfra_Buildbucket_Agent{
  1414  								Input: &pb.BuildInfra_Buildbucket_Agent_Input{},
  1415  								Purposes: map[string]pb.BuildInfra_Buildbucket_Agent_Purpose{
  1416  									"kitchen-checkout": pb.BuildInfra_Buildbucket_Agent_PURPOSE_EXE_PAYLOAD,
  1417  								},
  1418  							},
  1419  						},
  1420  						Logdog: &pb.BuildInfra_LogDog{
  1421  							Prefix:  "buildbucket/app/9021868963221667745",
  1422  							Project: "project",
  1423  						},
  1424  						Resultdb: &pb.BuildInfra_ResultDB{
  1425  							Hostname: "rdbHost",
  1426  						},
  1427  						Swarming: &pb.BuildInfra_Swarming{
  1428  							Hostname: "host",
  1429  							Caches: []*pb.BuildInfra_Swarming_CacheEntry{
  1430  								{
  1431  									Name: "builder_1809c38861a9996b1748e4640234fbd089992359f6f23f62f68deb98528f5f2b_v2",
  1432  									Path: "builder",
  1433  									WaitForWarmCache: &durationpb.Duration{
  1434  										Seconds: 240,
  1435  									},
  1436  								},
  1437  							},
  1438  							Priority: 30,
  1439  						},
  1440  					},
  1441  					Input: &pb.Build_Input{
  1442  						Properties: &structpb.Struct{},
  1443  					},
  1444  					SchedulingTimeout: &durationpb.Duration{
  1445  						Seconds: 21600,
  1446  					},
  1447  					Status: pb.Status_SCHEDULED,
  1448  					Tags: []*pb.StringPair{
  1449  						{
  1450  							Key:   "builder",
  1451  							Value: "builder",
  1452  						},
  1453  						{
  1454  							Key:   "buildset",
  1455  							Value: "buildset",
  1456  						},
  1457  						{
  1458  							Key:   "buildset",
  1459  							Value: "buildset",
  1460  						},
  1461  						{
  1462  							Key:   "parent_task_id",
  1463  							Value: "544239051",
  1464  						},
  1465  						{
  1466  							Key:   "user_agent",
  1467  							Value: "gerrit",
  1468  						},
  1469  					},
  1470  					CanOutliveParent: false,
  1471  					AncestorIds:      []int64{2, 3, 1},
  1472  				},
  1473  			})
  1474  			So(blds, ShouldResemble, []*model.Build{
  1475  				{
  1476  					ID:                9021868963221667745,
  1477  					BucketID:          "project/bucket",
  1478  					BuilderID:         "project/bucket/builder",
  1479  					CreatedBy:         "anonymous:anonymous",
  1480  					CreateTime:        testclock.TestRecentTimeUTC,
  1481  					StatusChangedTime: testclock.TestRecentTimeUTC,
  1482  					Experiments:       nil,
  1483  					Incomplete:        true,
  1484  					IsLuci:            true,
  1485  					Status:            pb.Status_SCHEDULED,
  1486  					Tags: []string{
  1487  						"builder:builder",
  1488  						"buildset:buildset",
  1489  						"parent_task_id:544239051",
  1490  						"user_agent:gerrit",
  1491  					},
  1492  					Project: "project",
  1493  					PubSubCallback: model.PubSubCallback{
  1494  						Topic:    "topic",
  1495  						UserData: []byte("data"),
  1496  					},
  1497  					LegacyProperties: model.LegacyProperties{
  1498  						Status: model.Scheduled,
  1499  					},
  1500  					AncestorIds: []int64{2, 3, 1},
  1501  					ParentID:    1,
  1502  				},
  1503  			})
  1504  			So(sch.Tasks(), ShouldHaveLength, 4)
  1505  			So(datastore.Get(ctx, blds), ShouldBeNil)
  1506  		})
  1507  
  1508  		Convey("one shadow, one original, and one with no shadow bucket", func() {
  1509  			testutil.PutBucket(ctx, "project", "bucket", &pb.Bucket{Swarming: &pb.Swarming{}, Shadow: "bucket.shadow"})
  1510  			testutil.PutBucket(ctx, "project", "bucket.shadow", &pb.Bucket{DynamicBuilderTemplate: &pb.Bucket_DynamicBuilderTemplate{}})
  1511  			So(datastore.Put(ctx, &model.Builder{
  1512  				Parent: model.BucketKey(ctx, "project", "bucket"),
  1513  				ID:     "builder",
  1514  				Config: &pb.BuilderConfig{
  1515  					Name:           "builder",
  1516  					ServiceAccount: "sa@chops-service-accounts.iam.gserviceaccount.com",
  1517  					Dimensions:     []string{"pool:pool1"},
  1518  					Properties:     `{"a":"b","b":"b"}`,
  1519  					ShadowBuilderAdjustments: &pb.BuilderConfig_ShadowBuilderAdjustments{
  1520  						ServiceAccount: "shadow@chops-service-accounts.iam.gserviceaccount.com",
  1521  						Pool:           "pool2",
  1522  						Properties:     `{"a":"b2","c":"c"}`,
  1523  						Dimensions: []string{
  1524  							"pool:pool2",
  1525  						},
  1526  					},
  1527  				},
  1528  			}), ShouldBeNil)
  1529  
  1530  			tk, err := buildtoken.GenerateToken(ctx, 1, pb.TokenBody_BUILD)
  1531  			ctx := metadata.NewIncomingContext(ctx, metadata.Pairs(bb.BuildbucketTokenHeader, tk))
  1532  			So(err, ShouldBeNil)
  1533  			// This is the parent led build of the newly requested led build.
  1534  			So(datastore.Put(ctx, &model.Build{
  1535  				Proto: &pb.Build{
  1536  					Id: 1,
  1537  					Builder: &pb.BuilderID{
  1538  						Project: "project",
  1539  						Bucket:  "bucket.shadow",
  1540  						Builder: "builder",
  1541  					},
  1542  					Status: pb.Status_STARTED,
  1543  					Exe: &pb.Executable{
  1544  						Cmd: []string{"recipes"},
  1545  					},
  1546  				},
  1547  				UpdateToken: tk,
  1548  			}), ShouldBeNil)
  1549  			So(datastore.Put(ctx, &model.BuildInfra{
  1550  				Build: datastore.MakeKey(ctx, "Build", 1),
  1551  				Proto: &pb.BuildInfra{
  1552  					Buildbucket: &pb.BuildInfra_Buildbucket{
  1553  						Agent: &pb.BuildInfra_Buildbucket_Agent{
  1554  							Input: &pb.BuildInfra_Buildbucket_Agent_Input{
  1555  								Data: map[string]*pb.InputDataRef{
  1556  									"cipd_bin_packages": {
  1557  										DataType: &pb.InputDataRef_Cipd{
  1558  											Cipd: &pb.InputDataRef_CIPD{
  1559  												Server: "cipd server",
  1560  												Specs: []*pb.InputDataRef_CIPD_PkgSpec{
  1561  													{Package: "include", Version: "canary-version"},
  1562  													{Package: "include_experiment", Version: "version"},
  1563  												},
  1564  											},
  1565  										},
  1566  										OnPath: []string{"cipd_bin_packages", "cipd_bin_packages/bin"},
  1567  									},
  1568  									"kitchen-checkout": {
  1569  										DataType: &pb.InputDataRef_Cas{
  1570  											Cas: &pb.InputDataRef_CAS{
  1571  												CasInstance: "projects/project/instances/instance",
  1572  												Digest: &pb.InputDataRef_CAS_Digest{
  1573  													Hash:      "hash",
  1574  													SizeBytes: 1,
  1575  												},
  1576  											},
  1577  										},
  1578  									},
  1579  								},
  1580  							},
  1581  							Source: &pb.BuildInfra_Buildbucket_Agent_Source{
  1582  								DataType: &pb.BuildInfra_Buildbucket_Agent_Source_Cipd{
  1583  									Cipd: &pb.BuildInfra_Buildbucket_Agent_Source_CIPD{
  1584  										Package: "infra/tools/luci/bbagent/${platform}",
  1585  										Version: "canary-version",
  1586  										Server:  "cipd server",
  1587  									},
  1588  								},
  1589  							},
  1590  							Purposes: map[string]pb.BuildInfra_Buildbucket_Agent_Purpose{
  1591  								"kitchen-checkout": pb.BuildInfra_Buildbucket_Agent_PURPOSE_EXE_PAYLOAD,
  1592  							},
  1593  						},
  1594  					},
  1595  				},
  1596  			}), ShouldBeNil)
  1597  			reqs := []*pb.ScheduleBuildRequest{
  1598  				{
  1599  					Builder: &pb.BuilderID{
  1600  						Project: "project",
  1601  						Bucket:  "bucket",
  1602  						Builder: "builder",
  1603  					},
  1604  					ShadowInput: &pb.ScheduleBuildRequest_ShadowInput{},
  1605  				},
  1606  				{
  1607  					Builder: &pb.BuilderID{
  1608  						Project: "project",
  1609  						Bucket:  "bucket",
  1610  						Builder: "builder",
  1611  					},
  1612  				},
  1613  				{
  1614  					Builder: &pb.BuilderID{
  1615  						Project: "project",
  1616  						Bucket:  "bucket.shadow",
  1617  						Builder: "builder",
  1618  					},
  1619  					ShadowInput: &pb.ScheduleBuildRequest_ShadowInput{},
  1620  				},
  1621  			}
  1622  			blds, err := scheduleBuilds(ctx, globalCfg, reqs...)
  1623  			So(err, ShouldNotBeNil)
  1624  			So(err, ShouldErrLike, "scheduling a shadow build in the original bucket is not allowed")
  1625  			So(stripProtos(blds), ShouldResembleProto, []*pb.Build{
  1626  				{
  1627  					Builder: &pb.BuilderID{
  1628  						Project: "project",
  1629  						Bucket:  "bucket.shadow",
  1630  						Builder: "builder",
  1631  					},
  1632  					CreatedBy:  "anonymous:anonymous",
  1633  					CreateTime: timestamppb.New(testclock.TestRecentTimeUTC),
  1634  					UpdateTime: timestamppb.New(testclock.TestRecentTimeUTC),
  1635  					Exe: &pb.Executable{
  1636  						Cmd: []string{"recipes"},
  1637  					},
  1638  					ExecutionTimeout: &durationpb.Duration{
  1639  						Seconds: 10800,
  1640  					},
  1641  					GracePeriod: &durationpb.Duration{
  1642  						Seconds: 30,
  1643  					},
  1644  					Id: 9021868963221610337,
  1645  					Infra: &pb.BuildInfra{
  1646  						Bbagent: &pb.BuildInfra_BBAgent{
  1647  							CacheDir:    "cache",
  1648  							PayloadPath: "kitchen-checkout",
  1649  						},
  1650  						Buildbucket: &pb.BuildInfra_Buildbucket{
  1651  							Hostname: "app.appspot.com",
  1652  							Agent: &pb.BuildInfra_Buildbucket_Agent{
  1653  								// Inherited from its parent.
  1654  								Input: &pb.BuildInfra_Buildbucket_Agent_Input{
  1655  									Data: map[string]*pb.InputDataRef{
  1656  										"cipd_bin_packages": {
  1657  											DataType: &pb.InputDataRef_Cipd{
  1658  												Cipd: &pb.InputDataRef_CIPD{
  1659  													Server: "cipd server",
  1660  													Specs: []*pb.InputDataRef_CIPD_PkgSpec{
  1661  														{Package: "include", Version: "canary-version"},
  1662  														{Package: "include_experiment", Version: "version"},
  1663  													},
  1664  												},
  1665  											},
  1666  											OnPath: []string{"cipd_bin_packages", "cipd_bin_packages/bin"},
  1667  										},
  1668  										"kitchen-checkout": {
  1669  											DataType: &pb.InputDataRef_Cas{
  1670  												Cas: &pb.InputDataRef_CAS{
  1671  													CasInstance: "projects/project/instances/instance",
  1672  													Digest: &pb.InputDataRef_CAS_Digest{
  1673  														Hash:      "hash",
  1674  														SizeBytes: 1,
  1675  													},
  1676  												},
  1677  											},
  1678  										},
  1679  									},
  1680  								},
  1681  								Purposes: map[string]pb.BuildInfra_Buildbucket_Agent_Purpose{
  1682  									"kitchen-checkout": pb.BuildInfra_Buildbucket_Agent_PURPOSE_EXE_PAYLOAD,
  1683  								},
  1684  								Source: &pb.BuildInfra_Buildbucket_Agent_Source{
  1685  									DataType: &pb.BuildInfra_Buildbucket_Agent_Source_Cipd{
  1686  										Cipd: &pb.BuildInfra_Buildbucket_Agent_Source_CIPD{
  1687  											Package: "infra/tools/luci/bbagent/${platform}",
  1688  											Version: "canary-version",
  1689  											Server:  "cipd server",
  1690  										},
  1691  									},
  1692  								},
  1693  								CipdPackagesCache: &pb.CacheEntry{
  1694  									Name: "cipd_cache_60bbd3834a15dabe356b6b277007f73bc1b4bdb8dff69da7db09d155463f8f75",
  1695  									Path: "cipd_cache",
  1696  								},
  1697  							},
  1698  						},
  1699  						Logdog: &pb.BuildInfra_LogDog{
  1700  							Prefix:  "buildbucket/app/9021868963221610337",
  1701  							Project: "project",
  1702  						},
  1703  						Resultdb: &pb.BuildInfra_ResultDB{
  1704  							Hostname: "rdbHost",
  1705  						},
  1706  						Swarming: &pb.BuildInfra_Swarming{
  1707  							Caches: []*pb.BuildInfra_Swarming_CacheEntry{
  1708  								{
  1709  									Name: "builder_1809c38861a9996b1748e4640234fbd089992359f6f23f62f68deb98528f5f2b_v2",
  1710  									Path: "builder",
  1711  									WaitForWarmCache: &durationpb.Duration{
  1712  										Seconds: 240,
  1713  									},
  1714  								},
  1715  							},
  1716  							Priority:           30,
  1717  							TaskServiceAccount: "shadow@chops-service-accounts.iam.gserviceaccount.com",
  1718  							TaskDimensions: []*pb.RequestedDimension{
  1719  								{
  1720  									Key:   "pool",
  1721  									Value: "pool2",
  1722  								},
  1723  							},
  1724  						},
  1725  						Led: &pb.BuildInfra_Led{
  1726  							ShadowedBucket: "bucket",
  1727  						},
  1728  					},
  1729  					Input: &pb.Build_Input{
  1730  						Properties: &structpb.Struct{
  1731  							Fields: map[string]*structpb.Value{
  1732  								"$recipe_engine/led": {
  1733  									Kind: &structpb.Value_StructValue{
  1734  										StructValue: &structpb.Struct{
  1735  											Fields: map[string]*structpb.Value{
  1736  												"shadowed_bucket": {
  1737  													Kind: &structpb.Value_StringValue{
  1738  														StringValue: "bucket",
  1739  													},
  1740  												},
  1741  											},
  1742  										},
  1743  									},
  1744  								},
  1745  								"a": {
  1746  									Kind: &structpb.Value_StringValue{
  1747  										StringValue: "b2",
  1748  									},
  1749  								},
  1750  								"b": {
  1751  									Kind: &structpb.Value_StringValue{
  1752  										StringValue: "b",
  1753  									},
  1754  								},
  1755  								"c": {
  1756  									Kind: &structpb.Value_StringValue{
  1757  										StringValue: "c",
  1758  									},
  1759  								},
  1760  							},
  1761  						},
  1762  					},
  1763  					SchedulingTimeout: &durationpb.Duration{
  1764  						Seconds: 21600,
  1765  					},
  1766  					Status: pb.Status_SCHEDULED,
  1767  					Tags: []*pb.StringPair{
  1768  						{
  1769  							Key:   "builder",
  1770  							Value: "builder",
  1771  						},
  1772  					},
  1773  					AncestorIds:      []int64{1},
  1774  					CanOutliveParent: true,
  1775  				},
  1776  				{
  1777  					Builder: &pb.BuilderID{
  1778  						Project: "project",
  1779  						Bucket:  "bucket",
  1780  						Builder: "builder",
  1781  					},
  1782  					CreatedBy:  "anonymous:anonymous",
  1783  					CreateTime: timestamppb.New(testclock.TestRecentTimeUTC),
  1784  					UpdateTime: timestamppb.New(testclock.TestRecentTimeUTC),
  1785  					Exe: &pb.Executable{
  1786  						Cmd: []string{"recipes"},
  1787  					},
  1788  					ExecutionTimeout: &durationpb.Duration{
  1789  						Seconds: 10800,
  1790  					},
  1791  					GracePeriod: &durationpb.Duration{
  1792  						Seconds: 30,
  1793  					},
  1794  					Id: 9021868963221610321,
  1795  					Infra: &pb.BuildInfra{
  1796  						Bbagent: &pb.BuildInfra_BBAgent{
  1797  							CacheDir:    "cache",
  1798  							PayloadPath: "kitchen-checkout",
  1799  						},
  1800  						Buildbucket: &pb.BuildInfra_Buildbucket{
  1801  							Hostname: "app.appspot.com",
  1802  							Agent: &pb.BuildInfra_Buildbucket_Agent{
  1803  								Input: &pb.BuildInfra_Buildbucket_Agent_Input{},
  1804  								Purposes: map[string]pb.BuildInfra_Buildbucket_Agent_Purpose{
  1805  									"kitchen-checkout": pb.BuildInfra_Buildbucket_Agent_PURPOSE_EXE_PAYLOAD,
  1806  								},
  1807  							},
  1808  						},
  1809  						Logdog: &pb.BuildInfra_LogDog{
  1810  							Prefix:  "buildbucket/app/9021868963221610321",
  1811  							Project: "project",
  1812  						},
  1813  						Resultdb: &pb.BuildInfra_ResultDB{
  1814  							Hostname: "rdbHost",
  1815  						},
  1816  						Swarming: &pb.BuildInfra_Swarming{
  1817  							Caches: []*pb.BuildInfra_Swarming_CacheEntry{
  1818  								{
  1819  									Name: "builder_1809c38861a9996b1748e4640234fbd089992359f6f23f62f68deb98528f5f2b_v2",
  1820  									Path: "builder",
  1821  									WaitForWarmCache: &durationpb.Duration{
  1822  										Seconds: 240,
  1823  									},
  1824  								},
  1825  							},
  1826  							Priority:           30,
  1827  							TaskServiceAccount: "sa@chops-service-accounts.iam.gserviceaccount.com",
  1828  							TaskDimensions: []*pb.RequestedDimension{
  1829  								{
  1830  									Key:   "pool",
  1831  									Value: "pool1",
  1832  								},
  1833  							},
  1834  						},
  1835  					},
  1836  					Input: &pb.Build_Input{
  1837  						Properties: &structpb.Struct{
  1838  							Fields: map[string]*structpb.Value{
  1839  								"a": {
  1840  									Kind: &structpb.Value_StringValue{
  1841  										StringValue: "b",
  1842  									},
  1843  								},
  1844  								"b": {
  1845  									Kind: &structpb.Value_StringValue{
  1846  										StringValue: "b",
  1847  									},
  1848  								},
  1849  							},
  1850  						},
  1851  					},
  1852  					SchedulingTimeout: &durationpb.Duration{
  1853  						Seconds: 21600,
  1854  					},
  1855  					Status: pb.Status_SCHEDULED,
  1856  					Tags: []*pb.StringPair{
  1857  						{
  1858  							Key:   "builder",
  1859  							Value: "builder",
  1860  						},
  1861  					},
  1862  					AncestorIds:      []int64{1},
  1863  					CanOutliveParent: true,
  1864  				},
  1865  				nil,
  1866  			})
  1867  		})
  1868  	})
  1869  
  1870  	Convey("scheduleRequestFromTemplate", t, func() {
  1871  		ctx := metrics.WithServiceInfo(memory.Use(context.Background()), "svc", "job", "ins")
  1872  		datastore.GetTestable(ctx).AutoIndex(true)
  1873  		datastore.GetTestable(ctx).Consistent(true)
  1874  		ctx = auth.WithState(ctx, &authtest.FakeState{
  1875  			Identity: userID,
  1876  			FakeDB: authtest.NewFakeDB(
  1877  				authtest.MockPermission(userID, "project:bucket", bbperms.BuildsGet),
  1878  			),
  1879  		})
  1880  
  1881  		testutil.PutBucket(ctx, "project", "bucket", nil)
  1882  
  1883  		Convey("nil", func() {
  1884  			ret, err := scheduleRequestFromTemplate(ctx, nil)
  1885  			So(err, ShouldBeNil)
  1886  			So(ret, ShouldBeNil)
  1887  		})
  1888  
  1889  		Convey("empty", func() {
  1890  			req := &pb.ScheduleBuildRequest{}
  1891  			ret, err := scheduleRequestFromTemplate(ctx, req)
  1892  			So(err, ShouldBeNil)
  1893  			So(req, ShouldResembleProto, &pb.ScheduleBuildRequest{})
  1894  			So(ret, ShouldResembleProto, &pb.ScheduleBuildRequest{})
  1895  		})
  1896  
  1897  		Convey("not found", func() {
  1898  			req := &pb.ScheduleBuildRequest{
  1899  				TemplateBuildId: 1,
  1900  			}
  1901  			ret, err := scheduleRequestFromTemplate(ctx, req)
  1902  			So(err, ShouldErrLike, "not found")
  1903  			So(req, ShouldResembleProto, &pb.ScheduleBuildRequest{
  1904  				TemplateBuildId: 1,
  1905  			})
  1906  			So(ret, ShouldBeNil)
  1907  		})
  1908  
  1909  		Convey("permission denied", func() {
  1910  			ctx = auth.WithState(ctx, &authtest.FakeState{
  1911  				Identity: "user:unauthorized@example.com",
  1912  			})
  1913  			So(datastore.Put(ctx, &model.Build{
  1914  				Proto: &pb.Build{
  1915  					Id: 1,
  1916  					Builder: &pb.BuilderID{
  1917  						Project: "project",
  1918  						Bucket:  "bucket",
  1919  						Builder: "builder",
  1920  					},
  1921  				},
  1922  			}), ShouldBeNil)
  1923  			req := &pb.ScheduleBuildRequest{
  1924  				TemplateBuildId: 1,
  1925  			}
  1926  			ret, err := scheduleRequestFromTemplate(ctx, req)
  1927  			So(err, ShouldErrLike, "not found")
  1928  			So(req, ShouldResembleProto, &pb.ScheduleBuildRequest{
  1929  				TemplateBuildId: 1,
  1930  			})
  1931  			So(ret, ShouldBeNil)
  1932  		})
  1933  
  1934  		Convey("canary", func() {
  1935  			Convey("false default", func() {
  1936  				So(datastore.Put(ctx, &model.Build{
  1937  					Proto: &pb.Build{
  1938  						Id: 1,
  1939  						Builder: &pb.BuilderID{
  1940  							Project: "project",
  1941  							Bucket:  "bucket",
  1942  							Builder: "builder",
  1943  						},
  1944  					},
  1945  					Experiments: []string{
  1946  						"-" + bb.ExperimentBBCanarySoftware,
  1947  					},
  1948  				}), ShouldBeNil)
  1949  
  1950  				Convey("merge", func() {
  1951  					req := &pb.ScheduleBuildRequest{
  1952  						TemplateBuildId: 1,
  1953  						Experiments:     map[string]bool{bb.ExperimentBBCanarySoftware: true},
  1954  					}
  1955  					ret, err := scheduleRequestFromTemplate(ctx, req)
  1956  					So(err, ShouldBeNil)
  1957  					So(req, ShouldResembleProto, &pb.ScheduleBuildRequest{
  1958  						TemplateBuildId: 1,
  1959  						Experiments:     map[string]bool{bb.ExperimentBBCanarySoftware: true},
  1960  					})
  1961  					So(ret, ShouldResembleProto, &pb.ScheduleBuildRequest{
  1962  						Builder: &pb.BuilderID{
  1963  							Project: "project",
  1964  							Bucket:  "bucket",
  1965  							Builder: "builder",
  1966  						},
  1967  						Experiments: map[string]bool{
  1968  							bb.ExperimentBBCanarySoftware: true,
  1969  							bb.ExperimentNonProduction:    false,
  1970  						},
  1971  					})
  1972  				})
  1973  
  1974  				Convey("ok", func() {
  1975  					req := &pb.ScheduleBuildRequest{
  1976  						TemplateBuildId: 1,
  1977  					}
  1978  					ret, err := scheduleRequestFromTemplate(ctx, req)
  1979  					So(err, ShouldBeNil)
  1980  					So(req, ShouldResembleProto, &pb.ScheduleBuildRequest{
  1981  						TemplateBuildId: 1,
  1982  					})
  1983  					So(ret, ShouldResembleProto, &pb.ScheduleBuildRequest{
  1984  						Builder: &pb.BuilderID{
  1985  							Project: "project",
  1986  							Bucket:  "bucket",
  1987  							Builder: "builder",
  1988  						},
  1989  						Experiments: map[string]bool{
  1990  							bb.ExperimentBBCanarySoftware: false,
  1991  							bb.ExperimentNonProduction:    false,
  1992  						},
  1993  					})
  1994  				})
  1995  			})
  1996  
  1997  			Convey("true default", func() {
  1998  				So(datastore.Put(ctx, &model.Build{
  1999  					Proto: &pb.Build{
  2000  						Id: 1,
  2001  						Builder: &pb.BuilderID{
  2002  							Project: "project",
  2003  							Bucket:  "bucket",
  2004  							Builder: "builder",
  2005  						},
  2006  					},
  2007  					Experiments: []string{
  2008  						"+" + bb.ExperimentBBCanarySoftware,
  2009  					},
  2010  				}), ShouldBeNil)
  2011  
  2012  				Convey("merge", func() {
  2013  					req := &pb.ScheduleBuildRequest{
  2014  						TemplateBuildId: 1,
  2015  						Experiments: map[string]bool{
  2016  							bb.ExperimentBBCanarySoftware: false,
  2017  						},
  2018  					}
  2019  					ret, err := scheduleRequestFromTemplate(ctx, req)
  2020  					So(err, ShouldBeNil)
  2021  					So(req, ShouldResembleProto, &pb.ScheduleBuildRequest{
  2022  						TemplateBuildId: 1,
  2023  						Experiments: map[string]bool{
  2024  							bb.ExperimentBBCanarySoftware: false,
  2025  						},
  2026  					})
  2027  					So(ret, ShouldResembleProto, &pb.ScheduleBuildRequest{
  2028  						Builder: &pb.BuilderID{
  2029  							Project: "project",
  2030  							Bucket:  "bucket",
  2031  							Builder: "builder",
  2032  						},
  2033  						Experiments: map[string]bool{
  2034  							bb.ExperimentBBCanarySoftware: false,
  2035  							bb.ExperimentNonProduction:    false,
  2036  						},
  2037  					})
  2038  				})
  2039  
  2040  				Convey("ok", func() {
  2041  					req := &pb.ScheduleBuildRequest{
  2042  						TemplateBuildId: 1,
  2043  					}
  2044  					ret, err := scheduleRequestFromTemplate(ctx, req)
  2045  					So(err, ShouldBeNil)
  2046  					So(req, ShouldResembleProto, &pb.ScheduleBuildRequest{
  2047  						TemplateBuildId: 1,
  2048  					})
  2049  					So(ret, ShouldResembleProto, &pb.ScheduleBuildRequest{
  2050  						Builder: &pb.BuilderID{
  2051  							Project: "project",
  2052  							Bucket:  "bucket",
  2053  							Builder: "builder",
  2054  						},
  2055  						Experiments: map[string]bool{
  2056  							bb.ExperimentBBCanarySoftware: true,
  2057  							bb.ExperimentNonProduction:    false,
  2058  						},
  2059  					})
  2060  				})
  2061  			})
  2062  		})
  2063  
  2064  		Convey("critical", func() {
  2065  			So(datastore.Put(ctx, &model.Build{
  2066  				Proto: &pb.Build{
  2067  					Id: 1,
  2068  					Builder: &pb.BuilderID{
  2069  						Project: "project",
  2070  						Bucket:  "bucket",
  2071  						Builder: "builder",
  2072  					},
  2073  					Critical: pb.Trinary_YES,
  2074  				},
  2075  				Experiments: []string{"-" + bb.ExperimentBBCanarySoftware},
  2076  			}), ShouldBeNil)
  2077  
  2078  			Convey("merge", func() {
  2079  				req := &pb.ScheduleBuildRequest{
  2080  					TemplateBuildId: 1,
  2081  					Critical:        pb.Trinary_NO,
  2082  				}
  2083  				ret, err := scheduleRequestFromTemplate(ctx, req)
  2084  				So(err, ShouldBeNil)
  2085  				So(req, ShouldResembleProto, &pb.ScheduleBuildRequest{
  2086  					TemplateBuildId: 1,
  2087  					Critical:        pb.Trinary_NO,
  2088  				})
  2089  				So(ret, ShouldResembleProto, &pb.ScheduleBuildRequest{
  2090  					Builder: &pb.BuilderID{
  2091  						Project: "project",
  2092  						Bucket:  "bucket",
  2093  						Builder: "builder",
  2094  					},
  2095  					Critical: pb.Trinary_NO,
  2096  					Experiments: map[string]bool{
  2097  						bb.ExperimentBBCanarySoftware: false,
  2098  						bb.ExperimentNonProduction:    false,
  2099  					},
  2100  				})
  2101  			})
  2102  
  2103  			Convey("ok", func() {
  2104  				req := &pb.ScheduleBuildRequest{
  2105  					TemplateBuildId: 1,
  2106  				}
  2107  				ret, err := scheduleRequestFromTemplate(ctx, req)
  2108  				So(err, ShouldBeNil)
  2109  				So(req, ShouldResembleProto, &pb.ScheduleBuildRequest{
  2110  					TemplateBuildId: 1,
  2111  				})
  2112  				So(ret, ShouldResembleProto, &pb.ScheduleBuildRequest{
  2113  					Builder: &pb.BuilderID{
  2114  						Project: "project",
  2115  						Bucket:  "bucket",
  2116  						Builder: "builder",
  2117  					},
  2118  					Critical: pb.Trinary_YES,
  2119  					Experiments: map[string]bool{
  2120  						bb.ExperimentBBCanarySoftware: false,
  2121  						bb.ExperimentNonProduction:    false,
  2122  					},
  2123  				})
  2124  			})
  2125  		})
  2126  
  2127  		Convey("exe", func() {
  2128  			So(datastore.Put(ctx, &model.Build{
  2129  				Proto: &pb.Build{
  2130  					Id: 1,
  2131  					Builder: &pb.BuilderID{
  2132  						Project: "project",
  2133  						Bucket:  "bucket",
  2134  						Builder: "builder",
  2135  					},
  2136  					Exe: &pb.Executable{
  2137  						CipdPackage: "package",
  2138  						CipdVersion: "version",
  2139  					},
  2140  				},
  2141  				Experiments: []string{"-" + bb.ExperimentBBCanarySoftware},
  2142  			}), ShouldBeNil)
  2143  
  2144  			Convey("merge", func() {
  2145  				Convey("empty", func() {
  2146  					req := &pb.ScheduleBuildRequest{
  2147  						TemplateBuildId: 1,
  2148  						Exe:             &pb.Executable{},
  2149  					}
  2150  					ret, err := scheduleRequestFromTemplate(ctx, req)
  2151  					So(err, ShouldBeNil)
  2152  					So(req, ShouldResembleProto, &pb.ScheduleBuildRequest{
  2153  						TemplateBuildId: 1,
  2154  						Exe:             &pb.Executable{},
  2155  					})
  2156  					So(ret, ShouldResembleProto, &pb.ScheduleBuildRequest{
  2157  						Builder: &pb.BuilderID{
  2158  							Project: "project",
  2159  							Bucket:  "bucket",
  2160  							Builder: "builder",
  2161  						},
  2162  						Exe: &pb.Executable{},
  2163  						Experiments: map[string]bool{
  2164  							bb.ExperimentBBCanarySoftware: false,
  2165  							bb.ExperimentNonProduction:    false,
  2166  						},
  2167  					})
  2168  				})
  2169  
  2170  				Convey("non-empty", func() {
  2171  					req := &pb.ScheduleBuildRequest{
  2172  						TemplateBuildId: 1,
  2173  						Exe: &pb.Executable{
  2174  							CipdPackage: "package",
  2175  							CipdVersion: "new",
  2176  						},
  2177  					}
  2178  					ret, err := scheduleRequestFromTemplate(ctx, req)
  2179  					So(err, ShouldBeNil)
  2180  					So(req, ShouldResembleProto, &pb.ScheduleBuildRequest{
  2181  						TemplateBuildId: 1,
  2182  						Exe: &pb.Executable{
  2183  							CipdPackage: "package",
  2184  							CipdVersion: "new",
  2185  						},
  2186  					})
  2187  					So(ret, ShouldResembleProto, &pb.ScheduleBuildRequest{
  2188  						Builder: &pb.BuilderID{
  2189  							Project: "project",
  2190  							Bucket:  "bucket",
  2191  							Builder: "builder",
  2192  						},
  2193  						Experiments: map[string]bool{
  2194  							bb.ExperimentBBCanarySoftware: false,
  2195  							bb.ExperimentNonProduction:    false,
  2196  						},
  2197  						Exe: &pb.Executable{
  2198  							CipdPackage: "package",
  2199  							CipdVersion: "new",
  2200  						},
  2201  					})
  2202  				})
  2203  			})
  2204  
  2205  			Convey("ok", func() {
  2206  				req := &pb.ScheduleBuildRequest{
  2207  					TemplateBuildId: 1,
  2208  				}
  2209  				ret, err := scheduleRequestFromTemplate(ctx, req)
  2210  				So(err, ShouldBeNil)
  2211  				So(req, ShouldResembleProto, &pb.ScheduleBuildRequest{
  2212  					TemplateBuildId: 1,
  2213  				})
  2214  				So(ret, ShouldResembleProto, &pb.ScheduleBuildRequest{
  2215  					Builder: &pb.BuilderID{
  2216  						Project: "project",
  2217  						Bucket:  "bucket",
  2218  						Builder: "builder",
  2219  					},
  2220  					Experiments: map[string]bool{
  2221  						bb.ExperimentBBCanarySoftware: false,
  2222  						bb.ExperimentNonProduction:    false,
  2223  					},
  2224  					Exe: &pb.Executable{
  2225  						CipdPackage: "package",
  2226  						CipdVersion: "version",
  2227  					},
  2228  				})
  2229  			})
  2230  		})
  2231  
  2232  		Convey("gerrit changes", func() {
  2233  			So(datastore.Put(ctx, &model.Build{
  2234  				Proto: &pb.Build{
  2235  					Id: 1,
  2236  					Builder: &pb.BuilderID{
  2237  						Project: "project",
  2238  						Bucket:  "bucket",
  2239  						Builder: "builder",
  2240  					},
  2241  					Input: &pb.Build_Input{
  2242  						GerritChanges: []*pb.GerritChange{
  2243  							{
  2244  								Host:     "example.com",
  2245  								Project:  "project",
  2246  								Change:   1,
  2247  								Patchset: 1,
  2248  							},
  2249  						},
  2250  					},
  2251  				},
  2252  				Experiments: []string{"-" + bb.ExperimentBBCanarySoftware},
  2253  			}), ShouldBeNil)
  2254  
  2255  			Convey("merge", func() {
  2256  				Convey("empty", func() {
  2257  					req := &pb.ScheduleBuildRequest{
  2258  						TemplateBuildId: 1,
  2259  						GerritChanges:   []*pb.GerritChange{},
  2260  					}
  2261  					ret, err := scheduleRequestFromTemplate(ctx, req)
  2262  					So(err, ShouldBeNil)
  2263  					So(req, ShouldResembleProto, &pb.ScheduleBuildRequest{
  2264  						TemplateBuildId: 1,
  2265  						GerritChanges:   []*pb.GerritChange{},
  2266  					})
  2267  					So(ret, ShouldResembleProto, &pb.ScheduleBuildRequest{
  2268  						Builder: &pb.BuilderID{
  2269  							Project: "project",
  2270  							Bucket:  "bucket",
  2271  							Builder: "builder",
  2272  						},
  2273  						Experiments: map[string]bool{
  2274  							bb.ExperimentBBCanarySoftware: false,
  2275  							bb.ExperimentNonProduction:    false,
  2276  						},
  2277  						GerritChanges: []*pb.GerritChange{
  2278  							{
  2279  								Host:     "example.com",
  2280  								Project:  "project",
  2281  								Change:   1,
  2282  								Patchset: 1,
  2283  							},
  2284  						},
  2285  					})
  2286  				})
  2287  
  2288  				Convey("non-empty", func() {
  2289  					req := &pb.ScheduleBuildRequest{
  2290  						TemplateBuildId: 1,
  2291  						GerritChanges: []*pb.GerritChange{
  2292  							{
  2293  								Host:     "example.com",
  2294  								Project:  "project",
  2295  								Change:   1,
  2296  								Patchset: 2,
  2297  							},
  2298  						},
  2299  					}
  2300  					ret, err := scheduleRequestFromTemplate(ctx, req)
  2301  					So(err, ShouldBeNil)
  2302  					So(req, ShouldResembleProto, &pb.ScheduleBuildRequest{
  2303  						TemplateBuildId: 1,
  2304  						GerritChanges: []*pb.GerritChange{
  2305  							{
  2306  								Host:     "example.com",
  2307  								Project:  "project",
  2308  								Change:   1,
  2309  								Patchset: 2,
  2310  							},
  2311  						},
  2312  					})
  2313  					So(ret, ShouldResembleProto, &pb.ScheduleBuildRequest{
  2314  						Builder: &pb.BuilderID{
  2315  							Project: "project",
  2316  							Bucket:  "bucket",
  2317  							Builder: "builder",
  2318  						},
  2319  						Experiments: map[string]bool{
  2320  							bb.ExperimentBBCanarySoftware: false,
  2321  							bb.ExperimentNonProduction:    false,
  2322  						},
  2323  						GerritChanges: []*pb.GerritChange{
  2324  							{
  2325  								Host:     "example.com",
  2326  								Project:  "project",
  2327  								Change:   1,
  2328  								Patchset: 2,
  2329  							},
  2330  						},
  2331  					})
  2332  				})
  2333  			})
  2334  
  2335  			Convey("ok", func() {
  2336  				req := &pb.ScheduleBuildRequest{
  2337  					TemplateBuildId: 1,
  2338  				}
  2339  				ret, err := scheduleRequestFromTemplate(ctx, req)
  2340  				So(err, ShouldBeNil)
  2341  				So(req, ShouldResembleProto, &pb.ScheduleBuildRequest{
  2342  					TemplateBuildId: 1,
  2343  				})
  2344  				So(ret, ShouldResembleProto, &pb.ScheduleBuildRequest{
  2345  					Builder: &pb.BuilderID{
  2346  						Project: "project",
  2347  						Bucket:  "bucket",
  2348  						Builder: "builder",
  2349  					},
  2350  					Experiments: map[string]bool{
  2351  						bb.ExperimentBBCanarySoftware: false,
  2352  						bb.ExperimentNonProduction:    false,
  2353  					},
  2354  					GerritChanges: []*pb.GerritChange{
  2355  						{
  2356  							Host:     "example.com",
  2357  							Project:  "project",
  2358  							Change:   1,
  2359  							Patchset: 1,
  2360  						},
  2361  					},
  2362  				})
  2363  			})
  2364  		})
  2365  
  2366  		Convey("gitiles commit", func() {
  2367  			So(datastore.Put(ctx, &model.Build{
  2368  				Proto: &pb.Build{
  2369  					Id: 1,
  2370  					Builder: &pb.BuilderID{
  2371  						Project: "project",
  2372  						Bucket:  "bucket",
  2373  						Builder: "builder",
  2374  					},
  2375  					Input: &pb.Build_Input{
  2376  						GitilesCommit: &pb.GitilesCommit{
  2377  							Host:    "example.com",
  2378  							Project: "project",
  2379  							Ref:     "refs/heads/master",
  2380  						},
  2381  					},
  2382  				},
  2383  				Experiments: []string{"-" + bb.ExperimentBBCanarySoftware},
  2384  			}), ShouldBeNil)
  2385  			req := &pb.ScheduleBuildRequest{
  2386  				TemplateBuildId: 1,
  2387  			}
  2388  			ret, err := scheduleRequestFromTemplate(ctx, req)
  2389  			So(err, ShouldBeNil)
  2390  			So(req, ShouldResembleProto, &pb.ScheduleBuildRequest{
  2391  				TemplateBuildId: 1,
  2392  			})
  2393  			So(ret, ShouldResembleProto, &pb.ScheduleBuildRequest{
  2394  				Builder: &pb.BuilderID{
  2395  					Project: "project",
  2396  					Bucket:  "bucket",
  2397  					Builder: "builder",
  2398  				},
  2399  				Experiments: map[string]bool{
  2400  					bb.ExperimentBBCanarySoftware: false,
  2401  					bb.ExperimentNonProduction:    false,
  2402  				},
  2403  				GitilesCommit: &pb.GitilesCommit{
  2404  					Host:    "example.com",
  2405  					Project: "project",
  2406  					Ref:     "refs/heads/master",
  2407  				},
  2408  			})
  2409  		})
  2410  
  2411  		Convey("input properties", func() {
  2412  			So(datastore.Put(ctx, &model.Build{
  2413  				Proto: &pb.Build{
  2414  					Id: 1,
  2415  					Builder: &pb.BuilderID{
  2416  						Project: "project",
  2417  						Bucket:  "bucket",
  2418  						Builder: "builder",
  2419  					},
  2420  				},
  2421  				Experiments: []string{"-" + bb.ExperimentBBCanarySoftware},
  2422  			}), ShouldBeNil)
  2423  
  2424  			Convey("empty", func() {
  2425  				So(datastore.Put(ctx, &model.BuildInputProperties{
  2426  					Build: datastore.MakeKey(ctx, "Build", 1),
  2427  				}), ShouldBeNil)
  2428  
  2429  				Convey("merge", func() {
  2430  					req := &pb.ScheduleBuildRequest{
  2431  						TemplateBuildId: 1,
  2432  						Properties: &structpb.Struct{
  2433  							Fields: map[string]*structpb.Value{
  2434  								"input": {
  2435  									Kind: &structpb.Value_StringValue{
  2436  										StringValue: "input value",
  2437  									},
  2438  								},
  2439  							},
  2440  						},
  2441  					}
  2442  					ret, err := scheduleRequestFromTemplate(ctx, req)
  2443  					So(err, ShouldBeNil)
  2444  					So(req, ShouldResembleProto, &pb.ScheduleBuildRequest{
  2445  						TemplateBuildId: 1,
  2446  						Properties: &structpb.Struct{
  2447  							Fields: map[string]*structpb.Value{
  2448  								"input": {
  2449  									Kind: &structpb.Value_StringValue{
  2450  										StringValue: "input value",
  2451  									},
  2452  								},
  2453  							},
  2454  						},
  2455  					})
  2456  					So(ret, ShouldResembleProto, &pb.ScheduleBuildRequest{
  2457  						Builder: &pb.BuilderID{
  2458  							Project: "project",
  2459  							Bucket:  "bucket",
  2460  							Builder: "builder",
  2461  						},
  2462  						Experiments: map[string]bool{
  2463  							bb.ExperimentBBCanarySoftware: false,
  2464  							bb.ExperimentNonProduction:    false,
  2465  						},
  2466  						Properties: &structpb.Struct{
  2467  							Fields: map[string]*structpb.Value{
  2468  								"input": {
  2469  									Kind: &structpb.Value_StringValue{
  2470  										StringValue: "input value",
  2471  									},
  2472  								},
  2473  							},
  2474  						},
  2475  					})
  2476  				})
  2477  
  2478  				Convey("ok", func() {
  2479  					req := &pb.ScheduleBuildRequest{
  2480  						TemplateBuildId: 1,
  2481  					}
  2482  					ret, err := scheduleRequestFromTemplate(ctx, req)
  2483  					So(err, ShouldBeNil)
  2484  					So(req, ShouldResembleProto, &pb.ScheduleBuildRequest{
  2485  						TemplateBuildId: 1,
  2486  					})
  2487  					So(ret, ShouldResembleProto, &pb.ScheduleBuildRequest{
  2488  						Builder: &pb.BuilderID{
  2489  							Project: "project",
  2490  							Bucket:  "bucket",
  2491  							Builder: "builder",
  2492  						},
  2493  						Experiments: map[string]bool{
  2494  							bb.ExperimentBBCanarySoftware: false,
  2495  							bb.ExperimentNonProduction:    false,
  2496  						},
  2497  					})
  2498  				})
  2499  			})
  2500  
  2501  			Convey("non-empty", func() {
  2502  				So(datastore.Put(ctx, &model.BuildInputProperties{
  2503  					Build: datastore.MakeKey(ctx, "Build", 1),
  2504  					Proto: &structpb.Struct{
  2505  						Fields: map[string]*structpb.Value{
  2506  							"input": {
  2507  								Kind: &structpb.Value_StringValue{
  2508  									StringValue: "input value",
  2509  								},
  2510  							},
  2511  						},
  2512  					},
  2513  				}), ShouldBeNil)
  2514  
  2515  				Convey("merge", func() {
  2516  					Convey("empty", func() {
  2517  						req := &pb.ScheduleBuildRequest{
  2518  							TemplateBuildId: 1,
  2519  							Properties:      &structpb.Struct{},
  2520  						}
  2521  						ret, err := scheduleRequestFromTemplate(ctx, req)
  2522  						So(err, ShouldBeNil)
  2523  						So(req, ShouldResembleProto, &pb.ScheduleBuildRequest{
  2524  							TemplateBuildId: 1,
  2525  							Properties:      &structpb.Struct{},
  2526  						})
  2527  						So(ret, ShouldResembleProto, &pb.ScheduleBuildRequest{
  2528  							Builder: &pb.BuilderID{
  2529  								Project: "project",
  2530  								Bucket:  "bucket",
  2531  								Builder: "builder",
  2532  							},
  2533  							Experiments: map[string]bool{
  2534  								bb.ExperimentBBCanarySoftware: false,
  2535  								bb.ExperimentNonProduction:    false,
  2536  							},
  2537  							Properties: &structpb.Struct{},
  2538  						})
  2539  					})
  2540  
  2541  					Convey("non-empty", func() {
  2542  						req := &pb.ScheduleBuildRequest{
  2543  							TemplateBuildId: 1,
  2544  							Properties: &structpb.Struct{
  2545  								Fields: map[string]*structpb.Value{
  2546  									"other": {
  2547  										Kind: &structpb.Value_StringValue{
  2548  											StringValue: "other value",
  2549  										},
  2550  									},
  2551  								},
  2552  							},
  2553  						}
  2554  						ret, err := scheduleRequestFromTemplate(ctx, req)
  2555  						So(err, ShouldBeNil)
  2556  						So(req, ShouldResembleProto, &pb.ScheduleBuildRequest{
  2557  							TemplateBuildId: 1,
  2558  							Properties: &structpb.Struct{
  2559  								Fields: map[string]*structpb.Value{
  2560  									"other": {
  2561  										Kind: &structpb.Value_StringValue{
  2562  											StringValue: "other value",
  2563  										},
  2564  									},
  2565  								},
  2566  							},
  2567  						})
  2568  						So(ret, ShouldResembleProto, &pb.ScheduleBuildRequest{
  2569  							Builder: &pb.BuilderID{
  2570  								Project: "project",
  2571  								Bucket:  "bucket",
  2572  								Builder: "builder",
  2573  							},
  2574  							Experiments: map[string]bool{
  2575  								bb.ExperimentBBCanarySoftware: false,
  2576  								bb.ExperimentNonProduction:    false,
  2577  							},
  2578  							Properties: &structpb.Struct{
  2579  								Fields: map[string]*structpb.Value{
  2580  									"other": {
  2581  										Kind: &structpb.Value_StringValue{
  2582  											StringValue: "other value",
  2583  										},
  2584  									},
  2585  								},
  2586  							},
  2587  						})
  2588  					})
  2589  				})
  2590  
  2591  				Convey("ok", func() {
  2592  					req := &pb.ScheduleBuildRequest{
  2593  						TemplateBuildId: 1,
  2594  					}
  2595  					ret, err := scheduleRequestFromTemplate(ctx, req)
  2596  					So(err, ShouldBeNil)
  2597  					So(req, ShouldResembleProto, &pb.ScheduleBuildRequest{
  2598  						TemplateBuildId: 1,
  2599  					})
  2600  					So(ret, ShouldResembleProto, &pb.ScheduleBuildRequest{
  2601  						Builder: &pb.BuilderID{
  2602  							Project: "project",
  2603  							Bucket:  "bucket",
  2604  							Builder: "builder",
  2605  						},
  2606  						Experiments: map[string]bool{
  2607  							bb.ExperimentBBCanarySoftware: false,
  2608  							bb.ExperimentNonProduction:    false,
  2609  						},
  2610  						Properties: &structpb.Struct{
  2611  							Fields: map[string]*structpb.Value{
  2612  								"input": {
  2613  									Kind: &structpb.Value_StringValue{
  2614  										StringValue: "input value",
  2615  									},
  2616  								},
  2617  							},
  2618  						},
  2619  					})
  2620  				})
  2621  			})
  2622  		})
  2623  
  2624  		Convey("tags", func() {
  2625  			So(datastore.Put(ctx, &model.Build{
  2626  				Proto: &pb.Build{
  2627  					Id: 1,
  2628  					Builder: &pb.BuilderID{
  2629  						Project: "project",
  2630  						Bucket:  "bucket",
  2631  						Builder: "builder",
  2632  					},
  2633  				},
  2634  				Tags: []string{
  2635  					"key:value",
  2636  				},
  2637  				Experiments: []string{"-" + bb.ExperimentBBCanarySoftware},
  2638  			}), ShouldBeNil)
  2639  
  2640  			Convey("merge", func() {
  2641  				Convey("empty", func() {
  2642  					req := &pb.ScheduleBuildRequest{
  2643  						TemplateBuildId: 1,
  2644  						Tags:            []*pb.StringPair{},
  2645  					}
  2646  					ret, err := scheduleRequestFromTemplate(ctx, req)
  2647  					So(err, ShouldBeNil)
  2648  					So(req, ShouldResembleProto, &pb.ScheduleBuildRequest{
  2649  						TemplateBuildId: 1,
  2650  						Tags:            []*pb.StringPair{},
  2651  					})
  2652  					So(ret, ShouldResembleProto, &pb.ScheduleBuildRequest{
  2653  						Builder: &pb.BuilderID{
  2654  							Project: "project",
  2655  							Bucket:  "bucket",
  2656  							Builder: "builder",
  2657  						},
  2658  						Experiments: map[string]bool{
  2659  							bb.ExperimentBBCanarySoftware: false,
  2660  							bb.ExperimentNonProduction:    false,
  2661  						},
  2662  						Tags: []*pb.StringPair{
  2663  							{
  2664  								Key:   "key",
  2665  								Value: "value",
  2666  							},
  2667  						},
  2668  					})
  2669  				})
  2670  
  2671  				Convey("non-empty", func() {
  2672  					req := &pb.ScheduleBuildRequest{
  2673  						TemplateBuildId: 1,
  2674  						Tags: []*pb.StringPair{
  2675  							{
  2676  								Key:   "other",
  2677  								Value: "other",
  2678  							},
  2679  						},
  2680  					}
  2681  					ret, err := scheduleRequestFromTemplate(ctx, req)
  2682  					So(err, ShouldBeNil)
  2683  					So(req, ShouldResembleProto, &pb.ScheduleBuildRequest{
  2684  						TemplateBuildId: 1,
  2685  						Tags: []*pb.StringPair{
  2686  							{
  2687  								Key:   "other",
  2688  								Value: "other",
  2689  							},
  2690  						},
  2691  					})
  2692  					So(ret, ShouldResembleProto, &pb.ScheduleBuildRequest{
  2693  						Builder: &pb.BuilderID{
  2694  							Project: "project",
  2695  							Bucket:  "bucket",
  2696  							Builder: "builder",
  2697  						},
  2698  						Experiments: map[string]bool{
  2699  							bb.ExperimentBBCanarySoftware: false,
  2700  							bb.ExperimentNonProduction:    false,
  2701  						},
  2702  						Tags: []*pb.StringPair{
  2703  							{
  2704  								Key:   "other",
  2705  								Value: "other",
  2706  							},
  2707  						},
  2708  					})
  2709  				})
  2710  			})
  2711  
  2712  			Convey("ok", func() {
  2713  				req := &pb.ScheduleBuildRequest{
  2714  					TemplateBuildId: 1,
  2715  				}
  2716  				ret, err := scheduleRequestFromTemplate(ctx, req)
  2717  				So(err, ShouldBeNil)
  2718  				So(req, ShouldResembleProto, &pb.ScheduleBuildRequest{
  2719  					TemplateBuildId: 1,
  2720  				})
  2721  				So(ret, ShouldResembleProto, &pb.ScheduleBuildRequest{
  2722  					Builder: &pb.BuilderID{
  2723  						Project: "project",
  2724  						Bucket:  "bucket",
  2725  						Builder: "builder",
  2726  					},
  2727  					Experiments: map[string]bool{
  2728  						bb.ExperimentBBCanarySoftware: false,
  2729  						bb.ExperimentNonProduction:    false,
  2730  					},
  2731  					Tags: []*pb.StringPair{
  2732  						{
  2733  							Key:   "key",
  2734  							Value: "value",
  2735  						},
  2736  					},
  2737  				})
  2738  			})
  2739  		})
  2740  
  2741  		Convey("requested dimensions", func() {
  2742  			So(datastore.Put(ctx, &model.Build{
  2743  				Proto: &pb.Build{
  2744  					Id: 1,
  2745  					Builder: &pb.BuilderID{
  2746  						Project: "project",
  2747  						Bucket:  "bucket",
  2748  						Builder: "builder",
  2749  					},
  2750  				},
  2751  				Experiments: []string{"-" + bb.ExperimentBBCanarySoftware},
  2752  			}), ShouldBeNil)
  2753  			So(datastore.Put(ctx, &model.BuildInfra{
  2754  				Build: datastore.MakeKey(ctx, "Build", 1),
  2755  				Proto: &pb.BuildInfra{
  2756  					Buildbucket: &pb.BuildInfra_Buildbucket{
  2757  						Hostname: "app.appspot.com",
  2758  						RequestedDimensions: []*pb.RequestedDimension{
  2759  							{Key: "key_in_db", Value: "value_in_db"},
  2760  						},
  2761  					},
  2762  				},
  2763  			}), ShouldBeNil)
  2764  
  2765  			Convey("ok", func() {
  2766  				req := &pb.ScheduleBuildRequest{
  2767  					TemplateBuildId: 1,
  2768  				}
  2769  				ret, err := scheduleRequestFromTemplate(ctx, req)
  2770  				So(err, ShouldBeNil)
  2771  				So(ret, ShouldResembleProto, &pb.ScheduleBuildRequest{
  2772  					Builder: &pb.BuilderID{
  2773  						Project: "project",
  2774  						Bucket:  "bucket",
  2775  						Builder: "builder",
  2776  					},
  2777  					Dimensions: []*pb.RequestedDimension{
  2778  						{Key: "key_in_db", Value: "value_in_db"},
  2779  					},
  2780  					Experiments: map[string]bool{
  2781  						bb.ExperimentBBCanarySoftware: false,
  2782  						bb.ExperimentNonProduction:    false,
  2783  					},
  2784  				})
  2785  			})
  2786  
  2787  			Convey("override", func() {
  2788  				req := &pb.ScheduleBuildRequest{
  2789  					TemplateBuildId: 1,
  2790  					Dimensions: []*pb.RequestedDimension{
  2791  						{Key: "key_in_req", Value: "value_in_req"},
  2792  					},
  2793  				}
  2794  				ret, err := scheduleRequestFromTemplate(ctx, req)
  2795  				So(err, ShouldBeNil)
  2796  				So(ret, ShouldResembleProto, &pb.ScheduleBuildRequest{
  2797  					Builder: &pb.BuilderID{
  2798  						Project: "project",
  2799  						Bucket:  "bucket",
  2800  						Builder: "builder",
  2801  					},
  2802  					Dimensions: []*pb.RequestedDimension{
  2803  						{Key: "key_in_req", Value: "value_in_req"},
  2804  					},
  2805  					Experiments: map[string]bool{
  2806  						bb.ExperimentBBCanarySoftware: false,
  2807  						bb.ExperimentNonProduction:    false,
  2808  					},
  2809  				})
  2810  			})
  2811  		})
  2812  
  2813  		Convey("priority", func() {
  2814  			So(datastore.Put(ctx, &model.Build{
  2815  				Proto: &pb.Build{
  2816  					Id: 1,
  2817  					Builder: &pb.BuilderID{
  2818  						Project: "project",
  2819  						Bucket:  "bucket",
  2820  						Builder: "builder",
  2821  					},
  2822  				},
  2823  				Experiments: []string{"-" + bb.ExperimentBBCanarySoftware},
  2824  			}), ShouldBeNil)
  2825  			So(datastore.Put(ctx, &model.BuildInfra{
  2826  				Build: datastore.MakeKey(ctx, "Build", 1),
  2827  				Proto: &pb.BuildInfra{
  2828  					Swarming: &pb.BuildInfra_Swarming{
  2829  						Priority: int32(30),
  2830  					},
  2831  				},
  2832  			}), ShouldBeNil)
  2833  
  2834  			Convey("ok", func() {
  2835  				req := &pb.ScheduleBuildRequest{
  2836  					TemplateBuildId: 1,
  2837  				}
  2838  				ret, err := scheduleRequestFromTemplate(ctx, req)
  2839  				So(err, ShouldBeNil)
  2840  				So(ret, ShouldResembleProto, &pb.ScheduleBuildRequest{
  2841  					Builder: &pb.BuilderID{
  2842  						Project: "project",
  2843  						Bucket:  "bucket",
  2844  						Builder: "builder",
  2845  					},
  2846  					Priority: int32(30),
  2847  					Experiments: map[string]bool{
  2848  						bb.ExperimentBBCanarySoftware: false,
  2849  						bb.ExperimentNonProduction:    false,
  2850  					},
  2851  				})
  2852  			})
  2853  
  2854  			Convey("override", func() {
  2855  				req := &pb.ScheduleBuildRequest{
  2856  					TemplateBuildId: 1,
  2857  					Priority:        int32(25),
  2858  				}
  2859  				ret, err := scheduleRequestFromTemplate(ctx, req)
  2860  				So(err, ShouldBeNil)
  2861  				So(ret, ShouldResembleProto, &pb.ScheduleBuildRequest{
  2862  					Builder: &pb.BuilderID{
  2863  						Project: "project",
  2864  						Bucket:  "bucket",
  2865  						Builder: "builder",
  2866  					},
  2867  					Priority: int32(25),
  2868  					Experiments: map[string]bool{
  2869  						bb.ExperimentBBCanarySoftware: false,
  2870  						bb.ExperimentNonProduction:    false,
  2871  					},
  2872  				})
  2873  			})
  2874  		})
  2875  
  2876  		Convey("ok", func() {
  2877  			So(datastore.Put(ctx, &model.Build{
  2878  				Proto: &pb.Build{
  2879  					Id: 1,
  2880  					Builder: &pb.BuilderID{
  2881  						Project: "project",
  2882  						Bucket:  "bucket",
  2883  						Builder: "builder",
  2884  					},
  2885  				},
  2886  				Experiments: []string{"-" + bb.ExperimentBBCanarySoftware},
  2887  			}), ShouldBeNil)
  2888  			req := &pb.ScheduleBuildRequest{
  2889  				TemplateBuildId: 1,
  2890  			}
  2891  			ret, err := scheduleRequestFromTemplate(ctx, req)
  2892  			So(err, ShouldBeNil)
  2893  			So(req, ShouldResembleProto, &pb.ScheduleBuildRequest{
  2894  				TemplateBuildId: 1,
  2895  			})
  2896  			So(ret, ShouldResembleProto, &pb.ScheduleBuildRequest{
  2897  				Builder: &pb.BuilderID{
  2898  					Project: "project",
  2899  					Bucket:  "bucket",
  2900  					Builder: "builder",
  2901  				},
  2902  				Experiments: map[string]bool{
  2903  					bb.ExperimentBBCanarySoftware: false,
  2904  					bb.ExperimentNonProduction:    false,
  2905  				},
  2906  			})
  2907  		})
  2908  	})
  2909  
  2910  	Convey("setDimensions", t, func() {
  2911  		Convey("config", func() {
  2912  			Convey("omit", func() {
  2913  				cfg := &pb.BuilderConfig{
  2914  					Dimensions: []string{
  2915  						"key:",
  2916  					},
  2917  				}
  2918  				b := &pb.Build{
  2919  					Infra: &pb.BuildInfra{
  2920  						Swarming: &pb.BuildInfra_Swarming{},
  2921  					},
  2922  				}
  2923  
  2924  				setDimensions(nil, cfg, b, false)
  2925  				So(b.Infra.Swarming, ShouldResembleProto, &pb.BuildInfra_Swarming{})
  2926  			})
  2927  
  2928  			Convey("simple", func() {
  2929  				cfg := &pb.BuilderConfig{
  2930  					Dimensions: []string{
  2931  						"key:value",
  2932  					},
  2933  				}
  2934  				b := &pb.Build{
  2935  					Infra: &pb.BuildInfra{
  2936  						Swarming: &pb.BuildInfra_Swarming{},
  2937  					},
  2938  				}
  2939  
  2940  				setDimensions(nil, cfg, b, false)
  2941  				So(b.Infra.Swarming, ShouldResembleProto, &pb.BuildInfra_Swarming{
  2942  					TaskDimensions: []*pb.RequestedDimension{
  2943  						{
  2944  							Key:   "key",
  2945  							Value: "value",
  2946  						},
  2947  					},
  2948  				})
  2949  			})
  2950  
  2951  			Convey("expiration", func() {
  2952  				cfg := &pb.BuilderConfig{
  2953  					Dimensions: []string{
  2954  						"1:key:value",
  2955  					},
  2956  				}
  2957  				b := &pb.Build{
  2958  					Infra: &pb.BuildInfra{
  2959  						Swarming: &pb.BuildInfra_Swarming{},
  2960  					},
  2961  				}
  2962  
  2963  				setDimensions(nil, cfg, b, false)
  2964  				So(b.Infra.Swarming, ShouldResembleProto, &pb.BuildInfra_Swarming{
  2965  					TaskDimensions: []*pb.RequestedDimension{
  2966  						{
  2967  							Expiration: &durationpb.Duration{
  2968  								Seconds: 1,
  2969  							},
  2970  							Key:   "key",
  2971  							Value: "value",
  2972  						},
  2973  					},
  2974  				})
  2975  			})
  2976  
  2977  			Convey("many", func() {
  2978  				cfg := &pb.BuilderConfig{
  2979  					Dimensions: []string{
  2980  						"key:",
  2981  						"key:value",
  2982  						"key:value:",
  2983  						"key:val:ue",
  2984  						"0:key:",
  2985  						"0:key:value",
  2986  						"0:key:value:",
  2987  						"0:key:val:ue",
  2988  						"1:key:",
  2989  						"1:key:value",
  2990  						"1:key:value:",
  2991  						"1:key:val:ue",
  2992  					},
  2993  				}
  2994  				b := &pb.Build{
  2995  					Infra: &pb.BuildInfra{
  2996  						Swarming: &pb.BuildInfra_Swarming{},
  2997  					},
  2998  				}
  2999  
  3000  				setDimensions(nil, cfg, b, false)
  3001  				So(b.Infra, ShouldResembleProto, &pb.BuildInfra{
  3002  					Swarming: &pb.BuildInfra_Swarming{
  3003  						TaskDimensions: []*pb.RequestedDimension{
  3004  							{
  3005  								Key:   "key",
  3006  								Value: "value",
  3007  							},
  3008  							{
  3009  								Key:   "key",
  3010  								Value: "value:",
  3011  							},
  3012  							{
  3013  								Key:   "key",
  3014  								Value: "val:ue",
  3015  							},
  3016  							{
  3017  								Key:   "key",
  3018  								Value: "value",
  3019  							},
  3020  							{
  3021  								Key:   "key",
  3022  								Value: "value:",
  3023  							},
  3024  							{
  3025  								Key:   "key",
  3026  								Value: "val:ue",
  3027  							},
  3028  							{
  3029  								Expiration: &durationpb.Duration{
  3030  									Seconds: 1,
  3031  								},
  3032  								Key:   "key",
  3033  								Value: "value",
  3034  							},
  3035  							{
  3036  								Expiration: &durationpb.Duration{
  3037  									Seconds: 1,
  3038  								},
  3039  								Key:   "key",
  3040  								Value: "value:",
  3041  							},
  3042  							{
  3043  								Expiration: &durationpb.Duration{
  3044  									Seconds: 1,
  3045  								},
  3046  								Key:   "key",
  3047  								Value: "val:ue",
  3048  							},
  3049  						},
  3050  					},
  3051  				})
  3052  			})
  3053  
  3054  			Convey("auto builder", func() {
  3055  				cfg := &pb.BuilderConfig{
  3056  					AutoBuilderDimension: pb.Toggle_YES,
  3057  					Name:                 "builder",
  3058  				}
  3059  				b := &pb.Build{
  3060  					Infra: &pb.BuildInfra{
  3061  						Swarming: &pb.BuildInfra_Swarming{},
  3062  					},
  3063  				}
  3064  
  3065  				setDimensions(nil, cfg, b, false)
  3066  				So(b.Infra.Swarming, ShouldResembleProto, &pb.BuildInfra_Swarming{
  3067  					TaskDimensions: []*pb.RequestedDimension{
  3068  						{
  3069  							Key:   "builder",
  3070  							Value: "builder",
  3071  						},
  3072  					},
  3073  				})
  3074  			})
  3075  
  3076  			Convey("builder > auto builder", func() {
  3077  				cfg := &pb.BuilderConfig{
  3078  					AutoBuilderDimension: pb.Toggle_YES,
  3079  					Dimensions: []string{
  3080  						"1:builder:cfg builder",
  3081  					},
  3082  					Name: "auto builder",
  3083  				}
  3084  				b := &pb.Build{
  3085  					Infra: &pb.BuildInfra{
  3086  						Swarming: &pb.BuildInfra_Swarming{},
  3087  					},
  3088  				}
  3089  
  3090  				setDimensions(nil, cfg, b, false)
  3091  				So(b.Infra.Swarming, ShouldResembleProto, &pb.BuildInfra_Swarming{
  3092  					TaskDimensions: []*pb.RequestedDimension{
  3093  						{
  3094  							Expiration: &durationpb.Duration{
  3095  								Seconds: 1,
  3096  							},
  3097  							Key:   "builder",
  3098  							Value: "cfg builder",
  3099  						},
  3100  					},
  3101  				})
  3102  			})
  3103  
  3104  			Convey("omit builder > auto builder", func() {
  3105  				cfg := &pb.BuilderConfig{
  3106  					AutoBuilderDimension: pb.Toggle_YES,
  3107  					Dimensions: []string{
  3108  						"builder:",
  3109  					},
  3110  					Name: "auto builder",
  3111  				}
  3112  				b := &pb.Build{
  3113  					Infra: &pb.BuildInfra{
  3114  						Swarming: &pb.BuildInfra_Swarming{},
  3115  					},
  3116  				}
  3117  
  3118  				setDimensions(nil, cfg, b, false)
  3119  				So(b.Infra.Swarming, ShouldResembleProto, &pb.BuildInfra_Swarming{})
  3120  			})
  3121  		})
  3122  
  3123  		Convey("request", func() {
  3124  			req := &pb.ScheduleBuildRequest{
  3125  				Dimensions: []*pb.RequestedDimension{
  3126  					{
  3127  						Expiration: &durationpb.Duration{
  3128  							Seconds: 1,
  3129  						},
  3130  						Key:   "key",
  3131  						Value: "value",
  3132  					},
  3133  				},
  3134  			}
  3135  			b := &pb.Build{
  3136  				Infra: &pb.BuildInfra{
  3137  					Swarming: &pb.BuildInfra_Swarming{},
  3138  				},
  3139  			}
  3140  
  3141  			setDimensions(req, nil, b, false)
  3142  			So(b.Infra.Swarming, ShouldResembleProto, &pb.BuildInfra_Swarming{
  3143  				TaskDimensions: []*pb.RequestedDimension{
  3144  					{
  3145  						Expiration: &durationpb.Duration{
  3146  							Seconds: 1,
  3147  						},
  3148  						Key:   "key",
  3149  						Value: "value",
  3150  					},
  3151  				},
  3152  			})
  3153  		})
  3154  
  3155  		Convey("request > config", func() {
  3156  			req := &pb.ScheduleBuildRequest{
  3157  				Dimensions: []*pb.RequestedDimension{
  3158  					{
  3159  						Expiration: &durationpb.Duration{
  3160  							Seconds: 1,
  3161  						},
  3162  						Key:   "req only",
  3163  						Value: "req value",
  3164  					},
  3165  					{
  3166  						Key:   "req only",
  3167  						Value: "req value",
  3168  					},
  3169  					{
  3170  						Key:   "key",
  3171  						Value: "req value",
  3172  					},
  3173  					{
  3174  						Key:   "key_to_exclude",
  3175  						Value: "",
  3176  					},
  3177  				},
  3178  			}
  3179  			cfg := &pb.BuilderConfig{
  3180  				AutoBuilderDimension: pb.Toggle_YES,
  3181  				Dimensions: []string{
  3182  					"1:cfg only:cfg value",
  3183  					"cfg only:cfg value",
  3184  					"cfg only:",
  3185  					"1:key:cfg value",
  3186  					"1:key_to_exclude:cfg value",
  3187  				},
  3188  				Name: "auto builder",
  3189  			}
  3190  			b := &pb.Build{
  3191  				Infra: &pb.BuildInfra{
  3192  					Swarming: &pb.BuildInfra_Swarming{},
  3193  				},
  3194  			}
  3195  
  3196  			setDimensions(req, cfg, b, false)
  3197  			So(b.Infra.Swarming, ShouldResembleProto, &pb.BuildInfra_Swarming{
  3198  				TaskDimensions: []*pb.RequestedDimension{
  3199  					{
  3200  						Key:   "builder",
  3201  						Value: "auto builder",
  3202  					},
  3203  					{
  3204  						Key:   "cfg only",
  3205  						Value: "cfg value",
  3206  					},
  3207  					{
  3208  						Expiration: &durationpb.Duration{
  3209  							Seconds: 1,
  3210  						},
  3211  						Key:   "cfg only",
  3212  						Value: "cfg value",
  3213  					},
  3214  					{
  3215  						Key:   "key",
  3216  						Value: "req value",
  3217  					},
  3218  					{
  3219  						Key:   "req only",
  3220  						Value: "req value",
  3221  					},
  3222  					{
  3223  						Expiration: &durationpb.Duration{
  3224  							Seconds: 1,
  3225  						},
  3226  						Key:   "req only",
  3227  						Value: "req value",
  3228  					},
  3229  				},
  3230  			})
  3231  		})
  3232  	})
  3233  
  3234  	Convey("setExecutable", t, func() {
  3235  		Convey("nil", func() {
  3236  			b := &pb.Build{}
  3237  
  3238  			setExecutable(nil, nil, b)
  3239  			So(b.Exe, ShouldResembleProto, &pb.Executable{})
  3240  		})
  3241  
  3242  		Convey("request only", func() {
  3243  			req := &pb.ScheduleBuildRequest{
  3244  				Exe: &pb.Executable{
  3245  					CipdPackage: "package",
  3246  					CipdVersion: "version",
  3247  					Cmd:         []string{"command"},
  3248  				},
  3249  			}
  3250  			b := &pb.Build{}
  3251  
  3252  			setExecutable(req, nil, b)
  3253  			So(b.Exe, ShouldResembleProto, &pb.Executable{
  3254  				CipdVersion: "version",
  3255  			})
  3256  		})
  3257  
  3258  		Convey("config only", func() {
  3259  			Convey("exe", func() {
  3260  				cfg := &pb.BuilderConfig{
  3261  					Exe: &pb.Executable{
  3262  						CipdPackage: "package",
  3263  						CipdVersion: "version",
  3264  						Cmd:         []string{"command"},
  3265  					},
  3266  				}
  3267  				b := &pb.Build{}
  3268  
  3269  				setExecutable(nil, cfg, b)
  3270  				So(b.Exe, ShouldResembleProto, &pb.Executable{
  3271  					CipdPackage: "package",
  3272  					CipdVersion: "version",
  3273  					Cmd:         []string{"command"},
  3274  				})
  3275  			})
  3276  
  3277  			Convey("recipe", func() {
  3278  				cfg := &pb.BuilderConfig{
  3279  					Exe: &pb.Executable{
  3280  						CipdPackage: "package 1",
  3281  						CipdVersion: "version 1",
  3282  						Cmd:         []string{"command"},
  3283  					},
  3284  					Recipe: &pb.BuilderConfig_Recipe{
  3285  						CipdPackage: "package 2",
  3286  						CipdVersion: "version 2",
  3287  					},
  3288  				}
  3289  				b := &pb.Build{}
  3290  
  3291  				setExecutable(nil, cfg, b)
  3292  				So(b.Exe, ShouldResembleProto, &pb.Executable{
  3293  					CipdPackage: "package 2",
  3294  					CipdVersion: "version 2",
  3295  					Cmd:         []string{"command"},
  3296  				})
  3297  			})
  3298  		})
  3299  
  3300  		Convey("request > config", func() {
  3301  			req := &pb.ScheduleBuildRequest{
  3302  				Exe: &pb.Executable{
  3303  					CipdPackage: "package 1",
  3304  					CipdVersion: "version 1",
  3305  					Cmd:         []string{"command 1"},
  3306  				},
  3307  			}
  3308  			cfg := &pb.BuilderConfig{
  3309  				Exe: &pb.Executable{
  3310  					CipdPackage: "package 2",
  3311  					CipdVersion: "version 2",
  3312  					Cmd:         []string{"command 2"},
  3313  				},
  3314  			}
  3315  			b := &pb.Build{}
  3316  
  3317  			setExecutable(req, cfg, b)
  3318  			So(b.Exe, ShouldResembleProto, &pb.Executable{
  3319  				CipdPackage: "package 2",
  3320  				CipdVersion: "version 1",
  3321  				Cmd:         []string{"command 2"},
  3322  			})
  3323  		})
  3324  	})
  3325  
  3326  	Convey("setExperiments", t, func() {
  3327  		ctx := mathrand.Set(memory.Use(context.Background()), rand.New(rand.NewSource(1)))
  3328  		ctx = metrics.WithServiceInfo(ctx, "svc", "job", "ins")
  3329  
  3330  		// settings.cfg
  3331  		gCfg := &pb.SettingsCfg{
  3332  			Experiment: &pb.ExperimentSettings{},
  3333  		}
  3334  
  3335  		// builder config
  3336  		cfg := &pb.BuilderConfig{
  3337  			Experiments: map[string]int32{},
  3338  		}
  3339  
  3340  		// base datastore entity (and embedded Build Proto)
  3341  		ent := &model.Build{
  3342  			Proto: &pb.Build{
  3343  				Builder: &pb.BuilderID{
  3344  					Project: "project",
  3345  					Bucket:  "bucket",
  3346  					Builder: "builder",
  3347  				},
  3348  				Exe: &pb.Executable{},
  3349  				Infra: &pb.BuildInfra{
  3350  					Buildbucket: &pb.BuildInfra_Buildbucket{
  3351  						Hostname: "app.appspot.com",
  3352  					},
  3353  				},
  3354  				Input: &pb.Build_Input{},
  3355  			},
  3356  		}
  3357  
  3358  		expect := &model.Build{
  3359  			Proto: &pb.Build{
  3360  				Builder: &pb.BuilderID{
  3361  					Project: "project",
  3362  					Bucket:  "bucket",
  3363  					Builder: "builder",
  3364  				},
  3365  				Exe: &pb.Executable{
  3366  					Cmd: []string{"recipes"},
  3367  				},
  3368  				Infra: &pb.BuildInfra{
  3369  					Buildbucket: &pb.BuildInfra_Buildbucket{
  3370  						Hostname: "app.appspot.com",
  3371  					},
  3372  				},
  3373  				Input: &pb.Build_Input{},
  3374  			},
  3375  		}
  3376  
  3377  		req := &pb.ScheduleBuildRequest{
  3378  			Experiments: map[string]bool{},
  3379  		}
  3380  
  3381  		setExps := func() {
  3382  			normalizeSchedule(req)
  3383  			setExperiments(ctx, req, cfg, gCfg, ent.Proto)
  3384  			setExperimentsFromProto(ent)
  3385  		}
  3386  		initReasons := func() map[string]pb.BuildInfra_Buildbucket_ExperimentReason {
  3387  			er := make(map[string]pb.BuildInfra_Buildbucket_ExperimentReason)
  3388  			expect.Proto.Infra.Buildbucket.ExperimentReasons = er
  3389  			return er
  3390  		}
  3391  
  3392  		Convey("nil", func() {
  3393  			setExps()
  3394  			So(ent, ShouldResemble, expect)
  3395  		})
  3396  
  3397  		Convey("dice rolling works", func() {
  3398  			for i := 0; i < 100; i += 10 {
  3399  				cfg.Experiments["exp"+strconv.Itoa(i)] = int32(i)
  3400  			}
  3401  			setExps()
  3402  
  3403  			So(ent.Proto.Input.Experiments, ShouldResemble, []string{
  3404  				"exp60", "exp70", "exp80", "exp90",
  3405  			})
  3406  		})
  3407  
  3408  		Convey("command", func() {
  3409  			Convey("recipes", func() {
  3410  				req.Experiments[bb.ExperimentBBAgent] = false
  3411  				setExps()
  3412  
  3413  				So(ent.Proto.Exe, ShouldResembleProto, &pb.Executable{
  3414  					Cmd: []string{"recipes"},
  3415  				})
  3416  				So(ent.Proto.Infra.Buildbucket.ExperimentReasons[bb.ExperimentBBAgent],
  3417  					ShouldEqual, pb.BuildInfra_Buildbucket_EXPERIMENT_REASON_REQUESTED)
  3418  			})
  3419  
  3420  			Convey("recipes (explicit)", func() {
  3421  				ent.Proto.Exe.Cmd = []string{"recipes"}
  3422  				req.Experiments[bb.ExperimentBBAgent] = false
  3423  				setExps()
  3424  
  3425  				So(ent.Proto.Exe, ShouldResembleProto, &pb.Executable{
  3426  					Cmd: []string{"recipes"},
  3427  				})
  3428  				So(ent.Proto.Input.Experiments, ShouldBeEmpty)
  3429  				So(ent.Proto.Infra.Buildbucket.ExperimentReasons[bb.ExperimentBBAgent],
  3430  					ShouldEqual, pb.BuildInfra_Buildbucket_EXPERIMENT_REASON_BUILDER_CONFIG)
  3431  			})
  3432  
  3433  			Convey("luciexe (experiment)", func() {
  3434  				req.Experiments[bb.ExperimentBBAgent] = true
  3435  				setExps()
  3436  
  3437  				So(ent.Proto.Exe, ShouldResembleProto, &pb.Executable{
  3438  					Cmd: []string{"luciexe"},
  3439  				})
  3440  				So(ent.Proto.Input.Experiments, ShouldContain, bb.ExperimentBBAgent)
  3441  				So(ent.Experiments, ShouldContain, "+"+bb.ExperimentBBAgent)
  3442  				So(ent.Proto.Infra.Buildbucket.ExperimentReasons[bb.ExperimentBBAgent],
  3443  					ShouldEqual, pb.BuildInfra_Buildbucket_EXPERIMENT_REASON_REQUESTED)
  3444  			})
  3445  
  3446  			Convey("luciexe (explicit)", func() {
  3447  				ent.Proto.Exe.Cmd = []string{"luciexe"}
  3448  				setExps()
  3449  
  3450  				So(ent.Proto.Exe, ShouldResembleProto, &pb.Executable{
  3451  					Cmd: []string{"luciexe"},
  3452  				})
  3453  				So(ent.Proto.Input.Experiments, ShouldContain, bb.ExperimentBBAgent)
  3454  				So(ent.Proto.Infra.Buildbucket.ExperimentReasons[bb.ExperimentBBAgent],
  3455  					ShouldEqual, pb.BuildInfra_Buildbucket_EXPERIMENT_REASON_BUILDER_CONFIG)
  3456  			})
  3457  
  3458  			Convey("cmd > experiment", func() {
  3459  				req.Experiments[bb.ExperimentBBAgent] = false
  3460  				ent.Proto.Exe.Cmd = []string{"command"}
  3461  				setExps()
  3462  
  3463  				So(ent.Proto.Exe, ShouldResembleProto, &pb.Executable{
  3464  					Cmd: []string{"command"},
  3465  				})
  3466  				So(ent.Proto.Input.Experiments, ShouldContain, bb.ExperimentBBAgent)
  3467  				So(ent.Proto.Infra.Buildbucket.ExperimentReasons[bb.ExperimentBBAgent],
  3468  					ShouldEqual, pb.BuildInfra_Buildbucket_EXPERIMENT_REASON_BUILDER_CONFIG)
  3469  			})
  3470  		})
  3471  
  3472  		Convey("request only", func() {
  3473  			req.Experiments["experiment1"] = true
  3474  			req.Experiments["experiment2"] = false
  3475  			setExps()
  3476  
  3477  			expect.Experiments = []string{
  3478  				"+experiment1",
  3479  				"-experiment2",
  3480  			}
  3481  			expect.Proto.Input.Experiments = []string{"experiment1"}
  3482  			er := initReasons()
  3483  			er["experiment1"] = pb.BuildInfra_Buildbucket_EXPERIMENT_REASON_REQUESTED
  3484  			er["experiment2"] = pb.BuildInfra_Buildbucket_EXPERIMENT_REASON_REQUESTED
  3485  
  3486  			So(ent, ShouldResemble, expect)
  3487  		})
  3488  
  3489  		Convey("legacy only", func() {
  3490  			req.Canary = pb.Trinary_YES
  3491  			req.Experimental = pb.Trinary_NO
  3492  			setExps()
  3493  
  3494  			expect.Canary = true
  3495  			expect.Experiments = []string{
  3496  				"+" + bb.ExperimentBBCanarySoftware,
  3497  				"-" + bb.ExperimentNonProduction,
  3498  			}
  3499  			expect.Proto.Canary = true
  3500  			expect.Proto.Input.Experiments = []string{bb.ExperimentBBCanarySoftware}
  3501  			er := initReasons()
  3502  			er[bb.ExperimentBBCanarySoftware] = pb.BuildInfra_Buildbucket_EXPERIMENT_REASON_REQUESTED
  3503  			er[bb.ExperimentNonProduction] = pb.BuildInfra_Buildbucket_EXPERIMENT_REASON_REQUESTED
  3504  
  3505  			So(ent, ShouldResemble, expect)
  3506  		})
  3507  
  3508  		Convey("config only", func() {
  3509  			cfg.Experiments["experiment1"] = 100
  3510  			cfg.Experiments["experiment2"] = 0
  3511  			setExps()
  3512  
  3513  			expect.Experiments = []string{
  3514  				"+experiment1",
  3515  				"-experiment2",
  3516  			}
  3517  			expect.Proto.Input.Experiments = []string{"experiment1"}
  3518  			er := initReasons()
  3519  			er["experiment1"] = pb.BuildInfra_Buildbucket_EXPERIMENT_REASON_BUILDER_CONFIG
  3520  			er["experiment2"] = pb.BuildInfra_Buildbucket_EXPERIMENT_REASON_BUILDER_CONFIG
  3521  
  3522  			So(ent, ShouldResemble, expect)
  3523  		})
  3524  
  3525  		Convey("override", func() {
  3526  			Convey("request > legacy", func() {
  3527  				req.Canary = pb.Trinary_YES
  3528  				req.Experimental = pb.Trinary_NO
  3529  				req.Experiments[bb.ExperimentBBCanarySoftware] = false
  3530  				req.Experiments[bb.ExperimentNonProduction] = true
  3531  				setExps()
  3532  
  3533  				expect.Experiments = []string{
  3534  					"+" + bb.ExperimentNonProduction,
  3535  					"-" + bb.ExperimentBBCanarySoftware,
  3536  				}
  3537  				expect.Experimental = true
  3538  				expect.Proto.Input.Experimental = true
  3539  				expect.Proto.Input.Experiments = []string{bb.ExperimentNonProduction}
  3540  				er := initReasons()
  3541  				er[bb.ExperimentNonProduction] = pb.BuildInfra_Buildbucket_EXPERIMENT_REASON_REQUESTED
  3542  				er[bb.ExperimentBBCanarySoftware] = pb.BuildInfra_Buildbucket_EXPERIMENT_REASON_REQUESTED
  3543  
  3544  				So(ent, ShouldResemble, expect)
  3545  			})
  3546  
  3547  			Convey("legacy > config", func() {
  3548  				req.Canary = pb.Trinary_YES
  3549  				req.Experimental = pb.Trinary_NO
  3550  				cfg.Experiments[bb.ExperimentBBCanarySoftware] = 0
  3551  				cfg.Experiments[bb.ExperimentNonProduction] = 100
  3552  				setExps()
  3553  
  3554  				expect.Experiments = []string{
  3555  					"+" + bb.ExperimentBBCanarySoftware,
  3556  					"-" + bb.ExperimentNonProduction,
  3557  				}
  3558  				expect.Canary = true
  3559  				expect.Proto.Canary = true
  3560  				expect.Proto.Input.Experiments = []string{bb.ExperimentBBCanarySoftware}
  3561  				er := initReasons()
  3562  				er[bb.ExperimentNonProduction] = pb.BuildInfra_Buildbucket_EXPERIMENT_REASON_REQUESTED
  3563  				er[bb.ExperimentBBCanarySoftware] = pb.BuildInfra_Buildbucket_EXPERIMENT_REASON_REQUESTED
  3564  
  3565  				So(ent, ShouldResemble, expect)
  3566  			})
  3567  
  3568  			Convey("request > config", func() {
  3569  				req.Experiments["experiment1"] = true
  3570  				req.Experiments["experiment2"] = false
  3571  				cfg.Experiments["experiment1"] = 0
  3572  				cfg.Experiments["experiment2"] = 100
  3573  				setExps()
  3574  
  3575  				expect.Experiments = []string{
  3576  					"+experiment1",
  3577  					"-experiment2",
  3578  				}
  3579  				expect.Proto.Input.Experiments = []string{"experiment1"}
  3580  				er := initReasons()
  3581  				er["experiment1"] = pb.BuildInfra_Buildbucket_EXPERIMENT_REASON_REQUESTED
  3582  				er["experiment2"] = pb.BuildInfra_Buildbucket_EXPERIMENT_REASON_REQUESTED
  3583  
  3584  				So(ent, ShouldResemble, expect)
  3585  			})
  3586  
  3587  			Convey("request > legacy > config", func() {
  3588  				req.Canary = pb.Trinary_YES
  3589  				req.Experimental = pb.Trinary_NO
  3590  				req.Experiments[bb.ExperimentBBCanarySoftware] = false
  3591  				req.Experiments[bb.ExperimentNonProduction] = true
  3592  				req.Experiments["experiment1"] = true
  3593  				req.Experiments["experiment2"] = false
  3594  				cfg.Experiments[bb.ExperimentBBCanarySoftware] = 100
  3595  				cfg.Experiments[bb.ExperimentNonProduction] = 100
  3596  				cfg.Experiments["experiment1"] = 0
  3597  				cfg.Experiments["experiment2"] = 0
  3598  				setExps()
  3599  
  3600  				expect.Experiments = []string{
  3601  					"+experiment1",
  3602  					"+" + bb.ExperimentNonProduction,
  3603  					"-experiment2",
  3604  					"-" + bb.ExperimentBBCanarySoftware,
  3605  				}
  3606  				expect.Experimental = true
  3607  				expect.Proto.Input.Experimental = true
  3608  				expect.Proto.Input.Experiments = []string{
  3609  					"experiment1",
  3610  					bb.ExperimentNonProduction,
  3611  				}
  3612  				er := initReasons()
  3613  				er["experiment1"] = pb.BuildInfra_Buildbucket_EXPERIMENT_REASON_REQUESTED
  3614  				er["experiment2"] = pb.BuildInfra_Buildbucket_EXPERIMENT_REASON_REQUESTED
  3615  				er[bb.ExperimentBBCanarySoftware] = pb.BuildInfra_Buildbucket_EXPERIMENT_REASON_REQUESTED
  3616  				er[bb.ExperimentNonProduction] = pb.BuildInfra_Buildbucket_EXPERIMENT_REASON_REQUESTED
  3617  
  3618  				So(ent, ShouldResemble, expect)
  3619  			})
  3620  		})
  3621  
  3622  		Convey("global configuration", func() {
  3623  			addExp := func(name string, dflt, min int32, inactive bool, b *pb.BuilderPredicate) {
  3624  				gCfg.Experiment.Experiments = append(gCfg.Experiment.Experiments, &pb.ExperimentSettings_Experiment{
  3625  					Name:         name,
  3626  					DefaultValue: dflt,
  3627  					MinimumValue: min,
  3628  					Builders:     b,
  3629  					Inactive:     inactive,
  3630  				})
  3631  			}
  3632  
  3633  			Convey("default always", func() {
  3634  				addExp("always", 100, 0, false, nil)
  3635  
  3636  				Convey("will fill in if unset", func() {
  3637  					setExps()
  3638  
  3639  					So(ent.Proto.Input.Experiments, ShouldResemble, []string{"always"})
  3640  					So(ent.Proto.Infra.Buildbucket.ExperimentReasons, ShouldResemble, map[string]pb.BuildInfra_Buildbucket_ExperimentReason{
  3641  						"always": pb.BuildInfra_Buildbucket_EXPERIMENT_REASON_GLOBAL_DEFAULT,
  3642  					})
  3643  				})
  3644  
  3645  				Convey("can be overridden from request", func() {
  3646  					req.Experiments["always"] = false
  3647  					setExps()
  3648  
  3649  					So(ent.Proto.Input.Experiments, ShouldBeEmpty)
  3650  					So(ent.Proto.Infra.Buildbucket.ExperimentReasons, ShouldResemble, map[string]pb.BuildInfra_Buildbucket_ExperimentReason{
  3651  						"always": pb.BuildInfra_Buildbucket_EXPERIMENT_REASON_REQUESTED,
  3652  					})
  3653  				})
  3654  			})
  3655  
  3656  			Convey("per builder", func() {
  3657  				addExp("per.builder", 100, 0, false, &pb.BuilderPredicate{
  3658  					Regex: []string{"project/bucket/builder"},
  3659  				})
  3660  				addExp("other.builder", 100, 0, false, &pb.BuilderPredicate{
  3661  					Regex: []string{"project/bucket/other"},
  3662  				})
  3663  				setExps()
  3664  
  3665  				So(ent.Proto.Input.Experiments, ShouldResemble, []string{"per.builder"})
  3666  				So(ent.Experiments, ShouldContain, "-other.builder")
  3667  				So(ent.Proto.Infra.Buildbucket.ExperimentReasons, ShouldResemble, map[string]pb.BuildInfra_Buildbucket_ExperimentReason{
  3668  					"per.builder":   pb.BuildInfra_Buildbucket_EXPERIMENT_REASON_GLOBAL_DEFAULT,
  3669  					"other.builder": pb.BuildInfra_Buildbucket_EXPERIMENT_REASON_GLOBAL_DEFAULT,
  3670  				})
  3671  			})
  3672  
  3673  			Convey("min value", func() {
  3674  				// note that default == 0, min == 100 is a bit silly, but works for this
  3675  				// test.
  3676  				addExp("min.value", 0, 100, false, nil)
  3677  
  3678  				Convey("overrides builder config", func() {
  3679  					cfg.Experiments["min.value"] = 0
  3680  					setExps()
  3681  
  3682  					So(ent.Proto.Input.Experiments, ShouldResemble, []string{"min.value"})
  3683  					So(ent.Proto.Infra.Buildbucket.ExperimentReasons, ShouldResemble, map[string]pb.BuildInfra_Buildbucket_ExperimentReason{
  3684  						"min.value": pb.BuildInfra_Buildbucket_EXPERIMENT_REASON_GLOBAL_MINIMUM,
  3685  					})
  3686  				})
  3687  
  3688  				Convey("can be overridden from request", func() {
  3689  					req.Experiments["min.value"] = false
  3690  					setExps()
  3691  
  3692  					So(ent.Proto.Input.Experiments, ShouldBeEmpty)
  3693  					So(ent.Proto.Infra.Buildbucket.ExperimentReasons, ShouldResemble, map[string]pb.BuildInfra_Buildbucket_ExperimentReason{
  3694  						"min.value": pb.BuildInfra_Buildbucket_EXPERIMENT_REASON_REQUESTED,
  3695  					})
  3696  				})
  3697  			})
  3698  
  3699  			Convey("inactive", func() {
  3700  				addExp("inactive", 30, 30, true, nil)
  3701  				addExp("other_inactive", 30, 30, true, nil)
  3702  				cfg.Experiments["inactive"] = 100
  3703  				setExps()
  3704  
  3705  				So(ent.Proto.Input.Experiments, ShouldBeEmpty)
  3706  				So(ent.Proto.Infra.Buildbucket.ExperimentReasons, ShouldResemble, map[string]pb.BuildInfra_Buildbucket_ExperimentReason{
  3707  					"inactive": pb.BuildInfra_Buildbucket_EXPERIMENT_REASON_GLOBAL_INACTIVE,
  3708  					// Note that other_inactive wasn't requested in the build so it's
  3709  					// absent here.
  3710  				})
  3711  			})
  3712  		})
  3713  	})
  3714  
  3715  	Convey("buildFromScheduleRequest", t, func() {
  3716  		ctx := memory.Use(context.Background())
  3717  		Convey("backend is enabled", func() {
  3718  			s := &pb.SettingsCfg{
  3719  				Backends: []*pb.BackendSetting{
  3720  					{
  3721  						Target:   "swarming://chromium-swarm",
  3722  						Hostname: "chromium-swarm.appspot.com",
  3723  					},
  3724  				},
  3725  				Cipd: &pb.CipdSettings{
  3726  					Server: "cipd_server",
  3727  				},
  3728  				Swarming: &pb.SwarmingSettings{
  3729  					BbagentPackage: &pb.SwarmingSettings_Package{
  3730  						PackageName: "cipd_pkg/${platform}",
  3731  						Version:     "cipd_vers",
  3732  					},
  3733  				},
  3734  			}
  3735  			bldrCfg := &pb.BuilderConfig{
  3736  				Dimensions: []string{
  3737  					"key:value",
  3738  				},
  3739  				ServiceAccount: "account",
  3740  				Backend: &pb.BuilderConfig_Backend{
  3741  					Target: "swarming://chromium-swarm",
  3742  				},
  3743  				Experiments: map[string]int32{
  3744  					bb.ExperimentBackendAlt: 100,
  3745  				},
  3746  			}
  3747  			req := &pb.ScheduleBuildRequest{
  3748  				Builder: &pb.BuilderID{
  3749  					Bucket:  "bucket",
  3750  					Builder: "builder",
  3751  					Project: "project",
  3752  				},
  3753  				RequestId: "request_id",
  3754  				Priority:  100,
  3755  			}
  3756  
  3757  			buildResult := buildFromScheduleRequest(ctx, req, nil, "", bldrCfg, s)
  3758  			expectedBackendConfig := &structpb.Struct{}
  3759  			expectedBackendConfig.Fields = make(map[string]*structpb.Value)
  3760  			expectedBackendConfig.Fields["priority"] = &structpb.Value{Kind: &structpb.Value_NumberValue{NumberValue: 100}}
  3761  			expectedBackendConfig.Fields["service_account"] = &structpb.Value{Kind: &structpb.Value_StringValue{StringValue: "account"}}
  3762  			expectedBackendConfig.Fields["agent_binary_cipd_pkg"] = &structpb.Value{Kind: &structpb.Value_StringValue{StringValue: "cipd_pkg/${platform}"}}
  3763  			expectedBackendConfig.Fields["agent_binary_cipd_vers"] = &structpb.Value{Kind: &structpb.Value_StringValue{StringValue: "cipd_vers"}}
  3764  			expectedBackendConfig.Fields["agent_binary_cipd_server"] = &structpb.Value{Kind: &structpb.Value_StringValue{StringValue: "cipd_server"}}
  3765  			expectedBackendConfig.Fields["agent_binary_cipd_filename"] = &structpb.Value{Kind: &structpb.Value_StringValue{StringValue: "bbagent${EXECUTABLE_SUFFIX}"}}
  3766  
  3767  			So(buildResult.Infra.Backend, ShouldResembleProto, &pb.BuildInfra_Backend{
  3768  				Caches: []*pb.CacheEntry{
  3769  					{
  3770  						Name:             "builder_1809c38861a9996b1748e4640234fbd089992359f6f23f62f68deb98528f5f2b_v2",
  3771  						Path:             "builder",
  3772  						WaitForWarmCache: &durationpb.Duration{Seconds: 240},
  3773  					},
  3774  				},
  3775  				Config:   expectedBackendConfig,
  3776  				Hostname: "chromium-swarm.appspot.com",
  3777  				Task: &pb.Task{
  3778  					Id: &pb.TaskID{
  3779  						Target: "swarming://chromium-swarm",
  3780  					},
  3781  				},
  3782  				TaskDimensions: []*pb.RequestedDimension{
  3783  					{
  3784  						Key:   "key",
  3785  						Value: "value",
  3786  					},
  3787  				},
  3788  			})
  3789  		})
  3790  	})
  3791  
  3792  	Convey("setInfra", t, func() {
  3793  		ctx := mathrand.Set(memory.Use(context.Background()), rand.New(rand.NewSource(1)))
  3794  		ctx = metrics.WithServiceInfo(ctx, "svc", "job", "ins")
  3795  		Convey("nil", func() {
  3796  			b := &pb.Build{
  3797  				Builder: &pb.BuilderID{
  3798  					Project: "project",
  3799  					Bucket:  "bucket",
  3800  					Builder: "builder",
  3801  				},
  3802  			}
  3803  
  3804  			setInfra(ctx, nil, nil, b, nil)
  3805  			So(b.Infra, ShouldResembleProto, &pb.BuildInfra{
  3806  				Bbagent: &pb.BuildInfra_BBAgent{
  3807  					CacheDir:    "cache",
  3808  					PayloadPath: "kitchen-checkout",
  3809  				},
  3810  				Buildbucket: &pb.BuildInfra_Buildbucket{
  3811  					Hostname: "app.appspot.com",
  3812  				},
  3813  				Logdog: &pb.BuildInfra_LogDog{
  3814  					Project: "project",
  3815  				},
  3816  				Resultdb: &pb.BuildInfra_ResultDB{},
  3817  			})
  3818  		})
  3819  
  3820  		Convey("bbagent", func() {
  3821  			b := &pb.Build{
  3822  				Builder: &pb.BuilderID{
  3823  					Project: "project",
  3824  					Bucket:  "bucket",
  3825  					Builder: "builder",
  3826  				},
  3827  			}
  3828  			s := &pb.SettingsCfg{
  3829  				KnownPublicGerritHosts: []string{
  3830  					"host",
  3831  				},
  3832  			}
  3833  
  3834  			setInfra(ctx, nil, nil, b, s)
  3835  			So(b.Infra, ShouldResembleProto, &pb.BuildInfra{
  3836  				Bbagent: &pb.BuildInfra_BBAgent{
  3837  					CacheDir:    "cache",
  3838  					PayloadPath: "kitchen-checkout",
  3839  				},
  3840  				Buildbucket: &pb.BuildInfra_Buildbucket{
  3841  					Hostname: "app.appspot.com",
  3842  					KnownPublicGerritHosts: []string{
  3843  						"host",
  3844  					},
  3845  				},
  3846  				Logdog: &pb.BuildInfra_LogDog{
  3847  					Project: "project",
  3848  				},
  3849  				Resultdb: &pb.BuildInfra_ResultDB{},
  3850  			})
  3851  		})
  3852  
  3853  		Convey("logdog", func() {
  3854  			b := &pb.Build{
  3855  				Builder: &pb.BuilderID{
  3856  					Project: "project",
  3857  					Bucket:  "bucket",
  3858  					Builder: "builder",
  3859  				},
  3860  			}
  3861  			s := &pb.SettingsCfg{
  3862  				Logdog: &pb.LogDogSettings{
  3863  					Hostname: "host",
  3864  				},
  3865  			}
  3866  
  3867  			setInfra(ctx, nil, nil, b, s)
  3868  			So(b.Infra, ShouldResembleProto, &pb.BuildInfra{
  3869  				Bbagent: &pb.BuildInfra_BBAgent{
  3870  					CacheDir:    "cache",
  3871  					PayloadPath: "kitchen-checkout",
  3872  				},
  3873  				Buildbucket: &pb.BuildInfra_Buildbucket{
  3874  					Hostname: "app.appspot.com",
  3875  				},
  3876  				Logdog: &pb.BuildInfra_LogDog{
  3877  					Hostname: "host",
  3878  					Project:  "project",
  3879  				},
  3880  				Resultdb: &pb.BuildInfra_ResultDB{},
  3881  			})
  3882  		})
  3883  
  3884  		Convey("resultdb", func() {
  3885  			b := &pb.Build{
  3886  				Builder: &pb.BuilderID{
  3887  					Project: "project",
  3888  					Bucket:  "bucket",
  3889  					Builder: "builder",
  3890  				},
  3891  				Id: 1,
  3892  			}
  3893  			s := &pb.SettingsCfg{
  3894  				Resultdb: &pb.ResultDBSettings{
  3895  					Hostname: "host",
  3896  				},
  3897  			}
  3898  
  3899  			bqExports := []*rdbPb.BigQueryExport{}
  3900  			cfg := &pb.BuilderConfig{
  3901  				Resultdb: &pb.BuilderConfig_ResultDB{
  3902  					Enable:    true,
  3903  					BqExports: bqExports,
  3904  				},
  3905  			}
  3906  
  3907  			setInfra(ctx, nil, cfg, b, s)
  3908  			So(b.Infra, ShouldResembleProto, &pb.BuildInfra{
  3909  				Bbagent: &pb.BuildInfra_BBAgent{
  3910  					PayloadPath: "kitchen-checkout",
  3911  					CacheDir:    "cache",
  3912  				},
  3913  				Buildbucket: &pb.BuildInfra_Buildbucket{
  3914  					Hostname: "app.appspot.com",
  3915  				},
  3916  				Logdog: &pb.BuildInfra_LogDog{
  3917  					Hostname: "",
  3918  					Project:  "project",
  3919  				},
  3920  				Resultdb: &pb.BuildInfra_ResultDB{
  3921  					Hostname:  "host",
  3922  					Enable:    true,
  3923  					BqExports: bqExports,
  3924  				},
  3925  			})
  3926  		})
  3927  
  3928  		Convey("config", func() {
  3929  			Convey("recipe", func() {
  3930  				cfg := &pb.BuilderConfig{
  3931  					Recipe: &pb.BuilderConfig_Recipe{
  3932  						CipdPackage: "package",
  3933  						Name:        "name",
  3934  					},
  3935  				}
  3936  				b := &pb.Build{
  3937  					Builder: &pb.BuilderID{
  3938  						Project: "project",
  3939  						Bucket:  "bucket",
  3940  						Builder: "builder",
  3941  					},
  3942  				}
  3943  
  3944  				setInfra(ctx, nil, cfg, b, nil)
  3945  				So(b.Infra, ShouldResembleProto, &pb.BuildInfra{
  3946  					Bbagent: &pb.BuildInfra_BBAgent{
  3947  						CacheDir:    "cache",
  3948  						PayloadPath: "kitchen-checkout",
  3949  					},
  3950  					Buildbucket: &pb.BuildInfra_Buildbucket{
  3951  						Hostname: "app.appspot.com",
  3952  					},
  3953  					Logdog: &pb.BuildInfra_LogDog{
  3954  						Project: "project",
  3955  					},
  3956  					Recipe: &pb.BuildInfra_Recipe{
  3957  						CipdPackage: "package",
  3958  						Name:        "name",
  3959  					},
  3960  					Resultdb: &pb.BuildInfra_ResultDB{},
  3961  				})
  3962  			})
  3963  		})
  3964  
  3965  		Convey("request", func() {
  3966  			Convey("dimensions", func() {
  3967  				req := &pb.ScheduleBuildRequest{
  3968  					Dimensions: []*pb.RequestedDimension{
  3969  						{
  3970  							Expiration: &durationpb.Duration{
  3971  								Seconds: 1,
  3972  							},
  3973  							Key:   "key",
  3974  							Value: "value",
  3975  						},
  3976  					},
  3977  				}
  3978  				b := &pb.Build{
  3979  					Builder: &pb.BuilderID{
  3980  						Project: "project",
  3981  						Bucket:  "bucket",
  3982  						Builder: "builder",
  3983  					},
  3984  				}
  3985  
  3986  				setInfra(ctx, req, nil, b, nil)
  3987  				So(b.Infra, ShouldResembleProto, &pb.BuildInfra{
  3988  					Bbagent: &pb.BuildInfra_BBAgent{
  3989  						CacheDir:    "cache",
  3990  						PayloadPath: "kitchen-checkout",
  3991  					},
  3992  					Buildbucket: &pb.BuildInfra_Buildbucket{
  3993  						Hostname: "app.appspot.com",
  3994  						RequestedDimensions: []*pb.RequestedDimension{
  3995  							{
  3996  								Expiration: &durationpb.Duration{
  3997  									Seconds: 1,
  3998  								},
  3999  								Key:   "key",
  4000  								Value: "value",
  4001  							},
  4002  						},
  4003  					},
  4004  					Logdog: &pb.BuildInfra_LogDog{
  4005  						Project: "project",
  4006  					},
  4007  					Resultdb: &pb.BuildInfra_ResultDB{},
  4008  				})
  4009  			})
  4010  
  4011  			Convey("properties", func() {
  4012  				req := &pb.ScheduleBuildRequest{
  4013  					Properties: &structpb.Struct{
  4014  						Fields: map[string]*structpb.Value{
  4015  							"key": {
  4016  								Kind: &structpb.Value_StringValue{
  4017  									StringValue: "value",
  4018  								},
  4019  							},
  4020  						},
  4021  					},
  4022  				}
  4023  				b := &pb.Build{
  4024  					Builder: &pb.BuilderID{
  4025  						Project: "project",
  4026  						Bucket:  "bucket",
  4027  						Builder: "builder",
  4028  					},
  4029  				}
  4030  
  4031  				setInfra(ctx, req, nil, b, nil)
  4032  				So(b.Infra, ShouldResembleProto, &pb.BuildInfra{
  4033  					Bbagent: &pb.BuildInfra_BBAgent{
  4034  						CacheDir:    "cache",
  4035  						PayloadPath: "kitchen-checkout",
  4036  					},
  4037  					Buildbucket: &pb.BuildInfra_Buildbucket{
  4038  						Hostname: "app.appspot.com",
  4039  						RequestedProperties: &structpb.Struct{
  4040  							Fields: map[string]*structpb.Value{
  4041  								"key": {
  4042  									Kind: &structpb.Value_StringValue{
  4043  										StringValue: "value",
  4044  									},
  4045  								},
  4046  							},
  4047  						},
  4048  					},
  4049  					Logdog: &pb.BuildInfra_LogDog{
  4050  						Project: "project",
  4051  					},
  4052  					Resultdb: &pb.BuildInfra_ResultDB{},
  4053  				})
  4054  			})
  4055  		})
  4056  	})
  4057  
  4058  	Convey("setSwarmingOrBackend", t, func() {
  4059  		ctx := mathrand.Set(memory.Use(context.Background()), rand.New(rand.NewSource(1)))
  4060  		ctx = metrics.WithServiceInfo(ctx, "svc", "job", "ins")
  4061  		Convey("nil", func() {
  4062  			b := &pb.Build{
  4063  				Builder: &pb.BuilderID{
  4064  					Project: "project",
  4065  					Bucket:  "bucket",
  4066  					Builder: "builder",
  4067  				},
  4068  				Infra: &pb.BuildInfra{
  4069  					Bbagent: &pb.BuildInfra_BBAgent{
  4070  						CacheDir:    "cache",
  4071  						PayloadPath: "kitchen-checkout",
  4072  					},
  4073  					Buildbucket: &pb.BuildInfra_Buildbucket{
  4074  						Hostname: "app.appspot.com",
  4075  					},
  4076  					Logdog: &pb.BuildInfra_LogDog{
  4077  						Hostname: "host",
  4078  						Project:  "project",
  4079  					},
  4080  					Resultdb: &pb.BuildInfra_ResultDB{},
  4081  				},
  4082  			}
  4083  
  4084  			setSwarmingOrBackend(ctx, nil, nil, b, nil)
  4085  			So(b.Infra, ShouldResembleProto, &pb.BuildInfra{
  4086  				Bbagent: &pb.BuildInfra_BBAgent{
  4087  					CacheDir:    "cache",
  4088  					PayloadPath: "kitchen-checkout",
  4089  				},
  4090  				Buildbucket: &pb.BuildInfra_Buildbucket{
  4091  					Hostname: "app.appspot.com",
  4092  				},
  4093  				Logdog: &pb.BuildInfra_LogDog{
  4094  					Hostname: "host",
  4095  					Project:  "project",
  4096  				},
  4097  				Resultdb: &pb.BuildInfra_ResultDB{},
  4098  				Swarming: &pb.BuildInfra_Swarming{
  4099  					Caches: []*pb.BuildInfra_Swarming_CacheEntry{
  4100  						{
  4101  							Name: "builder_1809c38861a9996b1748e4640234fbd089992359f6f23f62f68deb98528f5f2b_v2",
  4102  							Path: "builder",
  4103  							WaitForWarmCache: &durationpb.Duration{
  4104  								Seconds: 240,
  4105  							},
  4106  						},
  4107  					},
  4108  					Priority: 30,
  4109  				},
  4110  			})
  4111  		})
  4112  		Convey("priority", func() {
  4113  			b := &pb.Build{
  4114  				Builder: &pb.BuilderID{
  4115  					Project: "project",
  4116  					Bucket:  "bucket",
  4117  					Builder: "builder",
  4118  				},
  4119  				Infra: &pb.BuildInfra{
  4120  					Bbagent: &pb.BuildInfra_BBAgent{
  4121  						CacheDir:    "cache",
  4122  						PayloadPath: "kitchen-checkout",
  4123  					},
  4124  					Buildbucket: &pb.BuildInfra_Buildbucket{
  4125  						Hostname: "app.appspot.com",
  4126  					},
  4127  					Logdog: &pb.BuildInfra_LogDog{
  4128  						Hostname: "host",
  4129  						Project:  "project",
  4130  					},
  4131  					Resultdb: &pb.BuildInfra_ResultDB{},
  4132  				},
  4133  				Input: &pb.Build_Input{
  4134  					Experiments: []string{},
  4135  				},
  4136  			}
  4137  			Convey("default production", func() {
  4138  				setSwarmingOrBackend(ctx, nil, nil, b, nil)
  4139  				So(b.Infra.Swarming.Priority, ShouldEqual, 30)
  4140  				So(b.Input.Experimental, ShouldBeFalse)
  4141  			})
  4142  
  4143  			Convey("non-production", func() {
  4144  				b.Input.Experiments = append(b.Input.Experiments, bb.ExperimentNonProduction)
  4145  				setSwarmingOrBackend(ctx, nil, nil, b, nil)
  4146  				So(b.Infra.Swarming.Priority, ShouldEqual, 255)
  4147  				So(b.Input.Experimental, ShouldBeFalse)
  4148  			})
  4149  
  4150  			Convey("req > experiment", func() {
  4151  				b.Input.Experiments = append(b.Input.Experiments, bb.ExperimentNonProduction)
  4152  				req := &pb.ScheduleBuildRequest{
  4153  					Priority: 1,
  4154  				}
  4155  				setSwarmingOrBackend(ctx, req, nil, b, nil)
  4156  				So(b.Infra.Swarming.Priority, ShouldEqual, 1)
  4157  			})
  4158  		})
  4159  
  4160  		Convey("swarming", func() {
  4161  			Convey("no dimensions", func() {
  4162  				cfg := &pb.BuilderConfig{
  4163  					Priority:       1,
  4164  					ServiceAccount: "account",
  4165  					SwarmingHost:   "host",
  4166  				}
  4167  				b := &pb.Build{
  4168  					Builder: &pb.BuilderID{
  4169  						Project: "project",
  4170  						Bucket:  "bucket",
  4171  						Builder: "builder",
  4172  					},
  4173  					Infra: &pb.BuildInfra{
  4174  						Bbagent: &pb.BuildInfra_BBAgent{
  4175  							PayloadPath: "kitchen-checkout",
  4176  							CacheDir:    "cache",
  4177  						},
  4178  						Buildbucket: &pb.BuildInfra_Buildbucket{
  4179  							Hostname: "app.appspot.com",
  4180  						},
  4181  						Logdog: &pb.BuildInfra_LogDog{
  4182  							Project: "project",
  4183  						},
  4184  						Resultdb: &pb.BuildInfra_ResultDB{},
  4185  					},
  4186  				}
  4187  
  4188  				setSwarmingOrBackend(ctx, nil, cfg, b, nil)
  4189  				So(b.Infra, ShouldResembleProto, &pb.BuildInfra{
  4190  					Bbagent: &pb.BuildInfra_BBAgent{
  4191  						PayloadPath: "kitchen-checkout",
  4192  						CacheDir:    "cache",
  4193  					},
  4194  					Buildbucket: &pb.BuildInfra_Buildbucket{
  4195  						Hostname: "app.appspot.com",
  4196  					},
  4197  					Logdog: &pb.BuildInfra_LogDog{
  4198  						Project: "project",
  4199  					},
  4200  					Resultdb: &pb.BuildInfra_ResultDB{},
  4201  					Swarming: &pb.BuildInfra_Swarming{
  4202  						Caches: []*pb.BuildInfra_Swarming_CacheEntry{
  4203  							{
  4204  								Name: "builder_1809c38861a9996b1748e4640234fbd089992359f6f23f62f68deb98528f5f2b_v2",
  4205  								Path: "builder",
  4206  								WaitForWarmCache: &durationpb.Duration{
  4207  									Seconds: 240,
  4208  								},
  4209  							},
  4210  						},
  4211  						Hostname:           "host",
  4212  						Priority:           1,
  4213  						TaskServiceAccount: "account",
  4214  					},
  4215  				})
  4216  			})
  4217  
  4218  			Convey("caches", func() {
  4219  				Convey("nil", func() {
  4220  					b := &pb.Build{
  4221  						Builder: &pb.BuilderID{
  4222  							Project: "project",
  4223  							Bucket:  "bucket",
  4224  							Builder: "builder",
  4225  						},
  4226  						Infra: &pb.BuildInfra{
  4227  							Bbagent: &pb.BuildInfra_BBAgent{
  4228  								PayloadPath: "kitchen-checkout",
  4229  								CacheDir:    "cache",
  4230  							},
  4231  							Buildbucket: &pb.BuildInfra_Buildbucket{
  4232  								Hostname: "app.appspot.com",
  4233  							},
  4234  							Logdog: &pb.BuildInfra_LogDog{
  4235  								Project: "project",
  4236  							},
  4237  							Resultdb: &pb.BuildInfra_ResultDB{},
  4238  						},
  4239  					}
  4240  
  4241  					setSwarmingOrBackend(ctx, nil, nil, b, nil)
  4242  					So(b.Infra, ShouldResembleProto, &pb.BuildInfra{
  4243  						Bbagent: &pb.BuildInfra_BBAgent{
  4244  							CacheDir:    "cache",
  4245  							PayloadPath: "kitchen-checkout",
  4246  						},
  4247  						Buildbucket: &pb.BuildInfra_Buildbucket{
  4248  							Hostname: "app.appspot.com",
  4249  						},
  4250  						Logdog: &pb.BuildInfra_LogDog{
  4251  							Project: "project",
  4252  						},
  4253  						Resultdb: &pb.BuildInfra_ResultDB{},
  4254  						Swarming: &pb.BuildInfra_Swarming{
  4255  							Caches: []*pb.BuildInfra_Swarming_CacheEntry{
  4256  								{
  4257  									Name: "builder_1809c38861a9996b1748e4640234fbd089992359f6f23f62f68deb98528f5f2b_v2",
  4258  									Path: "builder",
  4259  									WaitForWarmCache: &durationpb.Duration{
  4260  										Seconds: 240,
  4261  									},
  4262  								},
  4263  							},
  4264  							Priority: 30,
  4265  						},
  4266  					})
  4267  				})
  4268  
  4269  				Convey("global", func() {
  4270  					b := &pb.Build{
  4271  						Builder: &pb.BuilderID{
  4272  							Project: "project",
  4273  							Bucket:  "bucket",
  4274  							Builder: "builder",
  4275  						},
  4276  						Infra: &pb.BuildInfra{
  4277  							Bbagent: &pb.BuildInfra_BBAgent{
  4278  								PayloadPath: "kitchen-checkout",
  4279  								CacheDir:    "cache",
  4280  							},
  4281  							Buildbucket: &pb.BuildInfra_Buildbucket{
  4282  								Hostname: "app.appspot.com",
  4283  							},
  4284  							Logdog: &pb.BuildInfra_LogDog{
  4285  								Project: "project",
  4286  							},
  4287  							Resultdb: &pb.BuildInfra_ResultDB{},
  4288  						},
  4289  					}
  4290  					s := &pb.SettingsCfg{
  4291  						Swarming: &pb.SwarmingSettings{
  4292  							GlobalCaches: []*pb.BuilderConfig_CacheEntry{
  4293  								{
  4294  									Path: "cache",
  4295  								},
  4296  							},
  4297  						},
  4298  					}
  4299  
  4300  					setSwarmingOrBackend(ctx, nil, nil, b, s)
  4301  					So(b.Infra, ShouldResembleProto, &pb.BuildInfra{
  4302  						Bbagent: &pb.BuildInfra_BBAgent{
  4303  							CacheDir:    "cache",
  4304  							PayloadPath: "kitchen-checkout",
  4305  						},
  4306  						Buildbucket: &pb.BuildInfra_Buildbucket{
  4307  							Hostname: "app.appspot.com",
  4308  						},
  4309  						Logdog: &pb.BuildInfra_LogDog{
  4310  							Project: "project",
  4311  						},
  4312  						Resultdb: &pb.BuildInfra_ResultDB{},
  4313  						Swarming: &pb.BuildInfra_Swarming{
  4314  							Caches: []*pb.BuildInfra_Swarming_CacheEntry{
  4315  								{
  4316  									Name: "builder_1809c38861a9996b1748e4640234fbd089992359f6f23f62f68deb98528f5f2b_v2",
  4317  									Path: "builder",
  4318  									WaitForWarmCache: &durationpb.Duration{
  4319  										Seconds: 240,
  4320  									},
  4321  								},
  4322  								{
  4323  									Name: "cache",
  4324  									Path: "cache",
  4325  								},
  4326  							},
  4327  							Priority: 30,
  4328  						},
  4329  					})
  4330  				})
  4331  
  4332  				Convey("config", func() {
  4333  					cfg := &pb.BuilderConfig{
  4334  						Caches: []*pb.BuilderConfig_CacheEntry{
  4335  							{
  4336  								Path: "cache",
  4337  							},
  4338  						},
  4339  					}
  4340  					b := &pb.Build{
  4341  						Builder: &pb.BuilderID{
  4342  							Project: "project",
  4343  							Bucket:  "bucket",
  4344  							Builder: "builder",
  4345  						},
  4346  						Infra: &pb.BuildInfra{
  4347  							Bbagent: &pb.BuildInfra_BBAgent{
  4348  								PayloadPath: "kitchen-checkout",
  4349  								CacheDir:    "cache",
  4350  							},
  4351  							Buildbucket: &pb.BuildInfra_Buildbucket{
  4352  								Hostname: "app.appspot.com",
  4353  							},
  4354  							Logdog: &pb.BuildInfra_LogDog{
  4355  								Project: "project",
  4356  							},
  4357  							Resultdb: &pb.BuildInfra_ResultDB{},
  4358  						},
  4359  					}
  4360  
  4361  					setSwarmingOrBackend(ctx, nil, cfg, b, nil)
  4362  					So(b.Infra, ShouldResembleProto, &pb.BuildInfra{
  4363  						Bbagent: &pb.BuildInfra_BBAgent{
  4364  							CacheDir:    "cache",
  4365  							PayloadPath: "kitchen-checkout",
  4366  						},
  4367  						Buildbucket: &pb.BuildInfra_Buildbucket{
  4368  							Hostname: "app.appspot.com",
  4369  						},
  4370  						Logdog: &pb.BuildInfra_LogDog{
  4371  							Project: "project",
  4372  						},
  4373  						Resultdb: &pb.BuildInfra_ResultDB{},
  4374  						Swarming: &pb.BuildInfra_Swarming{
  4375  							Caches: []*pb.BuildInfra_Swarming_CacheEntry{
  4376  								{
  4377  									Name: "builder_1809c38861a9996b1748e4640234fbd089992359f6f23f62f68deb98528f5f2b_v2",
  4378  									Path: "builder",
  4379  									WaitForWarmCache: &durationpb.Duration{
  4380  										Seconds: 240,
  4381  									},
  4382  								},
  4383  								{
  4384  									Name: "cache",
  4385  									Path: "cache",
  4386  								},
  4387  							},
  4388  							Priority: 30,
  4389  						},
  4390  					})
  4391  				})
  4392  
  4393  				Convey("config > global", func() {
  4394  					cfg := &pb.BuilderConfig{
  4395  						Caches: []*pb.BuilderConfig_CacheEntry{
  4396  							{
  4397  								Name: "builder only name",
  4398  								Path: "builder only path",
  4399  							},
  4400  							{
  4401  								Name: "name",
  4402  								Path: "builder path",
  4403  							},
  4404  							{
  4405  								Name: "builder name",
  4406  								Path: "path",
  4407  							},
  4408  							{
  4409  								EnvVar: "builder env",
  4410  								Path:   "env",
  4411  							},
  4412  						},
  4413  					}
  4414  					b := &pb.Build{
  4415  						Builder: &pb.BuilderID{
  4416  							Project: "project",
  4417  							Bucket:  "bucket",
  4418  							Builder: "builder",
  4419  						},
  4420  						Infra: &pb.BuildInfra{
  4421  							Bbagent: &pb.BuildInfra_BBAgent{
  4422  								PayloadPath: "kitchen-checkout",
  4423  								CacheDir:    "cache",
  4424  							},
  4425  							Buildbucket: &pb.BuildInfra_Buildbucket{
  4426  								Hostname: "app.appspot.com",
  4427  							},
  4428  							Logdog: &pb.BuildInfra_LogDog{
  4429  								Project: "project",
  4430  							},
  4431  							Resultdb: &pb.BuildInfra_ResultDB{},
  4432  						},
  4433  					}
  4434  					s := &pb.SettingsCfg{
  4435  						Swarming: &pb.SwarmingSettings{
  4436  							GlobalCaches: []*pb.BuilderConfig_CacheEntry{
  4437  								{
  4438  									Name: "global only name",
  4439  									Path: "global only path",
  4440  								},
  4441  								{
  4442  									Name: "name",
  4443  									Path: "global path",
  4444  								},
  4445  								{
  4446  									Name: "global name",
  4447  									Path: "path",
  4448  								},
  4449  								{
  4450  									EnvVar: "global env",
  4451  									Path:   "path",
  4452  								},
  4453  							},
  4454  						},
  4455  					}
  4456  
  4457  					setSwarmingOrBackend(ctx, nil, cfg, b, s)
  4458  					So(b.Infra, ShouldResembleProto, &pb.BuildInfra{
  4459  						Bbagent: &pb.BuildInfra_BBAgent{
  4460  							CacheDir:    "cache",
  4461  							PayloadPath: "kitchen-checkout",
  4462  						},
  4463  						Buildbucket: &pb.BuildInfra_Buildbucket{
  4464  							Hostname: "app.appspot.com",
  4465  						},
  4466  						Logdog: &pb.BuildInfra_LogDog{
  4467  							Project: "project",
  4468  						},
  4469  						Resultdb: &pb.BuildInfra_ResultDB{},
  4470  						Swarming: &pb.BuildInfra_Swarming{
  4471  							Caches: []*pb.BuildInfra_Swarming_CacheEntry{
  4472  								{
  4473  									Name: "builder_1809c38861a9996b1748e4640234fbd089992359f6f23f62f68deb98528f5f2b_v2",
  4474  									Path: "builder",
  4475  									WaitForWarmCache: &durationpb.Duration{
  4476  										Seconds: 240,
  4477  									},
  4478  								},
  4479  								{
  4480  									Name: "builder only name",
  4481  									Path: "builder only path",
  4482  								},
  4483  								{
  4484  									Name: "name",
  4485  									Path: "builder path",
  4486  								},
  4487  								{
  4488  									EnvVar: "builder env",
  4489  									Name:   "env",
  4490  									Path:   "env",
  4491  								},
  4492  								{
  4493  									Name: "global only name",
  4494  									Path: "global only path",
  4495  								},
  4496  								{
  4497  									Name: "builder name",
  4498  									Path: "path",
  4499  								},
  4500  							},
  4501  							Priority: 30,
  4502  						},
  4503  					})
  4504  				})
  4505  			})
  4506  
  4507  			Convey("parent run id", func() {
  4508  				req := &pb.ScheduleBuildRequest{
  4509  					Swarming: &pb.ScheduleBuildRequest_Swarming{
  4510  						ParentRunId: "id",
  4511  					},
  4512  				}
  4513  				b := &pb.Build{
  4514  					Builder: &pb.BuilderID{
  4515  						Project: "project",
  4516  						Bucket:  "bucket",
  4517  						Builder: "builder",
  4518  					},
  4519  					Infra: &pb.BuildInfra{
  4520  						Bbagent: &pb.BuildInfra_BBAgent{
  4521  							PayloadPath: "kitchen-checkout",
  4522  							CacheDir:    "cache",
  4523  						},
  4524  						Buildbucket: &pb.BuildInfra_Buildbucket{
  4525  							Hostname: "app.appspot.com",
  4526  						},
  4527  						Logdog: &pb.BuildInfra_LogDog{
  4528  							Project: "project",
  4529  						},
  4530  						Resultdb: &pb.BuildInfra_ResultDB{},
  4531  					},
  4532  				}
  4533  
  4534  				setSwarmingOrBackend(ctx, req, nil, b, nil)
  4535  				So(b.Infra, ShouldResembleProto, &pb.BuildInfra{
  4536  					Bbagent: &pb.BuildInfra_BBAgent{
  4537  						CacheDir:    "cache",
  4538  						PayloadPath: "kitchen-checkout",
  4539  					},
  4540  					Buildbucket: &pb.BuildInfra_Buildbucket{
  4541  						Hostname: "app.appspot.com",
  4542  					},
  4543  					Logdog: &pb.BuildInfra_LogDog{
  4544  						Project: "project",
  4545  					},
  4546  					Resultdb: &pb.BuildInfra_ResultDB{},
  4547  					Swarming: &pb.BuildInfra_Swarming{
  4548  						Caches: []*pb.BuildInfra_Swarming_CacheEntry{
  4549  							{
  4550  								Name: "builder_1809c38861a9996b1748e4640234fbd089992359f6f23f62f68deb98528f5f2b_v2",
  4551  								Path: "builder",
  4552  								WaitForWarmCache: &durationpb.Duration{
  4553  									Seconds: 240,
  4554  								},
  4555  							},
  4556  						},
  4557  						ParentRunId: "id",
  4558  						Priority:    30,
  4559  					},
  4560  				})
  4561  			})
  4562  
  4563  			Convey("priority", func() {
  4564  				req := &pb.ScheduleBuildRequest{
  4565  					Priority: 1,
  4566  				}
  4567  				b := &pb.Build{
  4568  					Builder: &pb.BuilderID{
  4569  						Project: "project",
  4570  						Bucket:  "bucket",
  4571  						Builder: "builder",
  4572  					},
  4573  					Infra: &pb.BuildInfra{
  4574  						Bbagent: &pb.BuildInfra_BBAgent{
  4575  							PayloadPath: "kitchen-checkout",
  4576  							CacheDir:    "cache",
  4577  						},
  4578  						Buildbucket: &pb.BuildInfra_Buildbucket{
  4579  							Hostname: "app.appspot.com",
  4580  						},
  4581  						Logdog: &pb.BuildInfra_LogDog{
  4582  							Project: "project",
  4583  						},
  4584  						Resultdb: &pb.BuildInfra_ResultDB{},
  4585  					},
  4586  				}
  4587  
  4588  				setSwarmingOrBackend(ctx, req, nil, b, nil)
  4589  				So(b.Infra, ShouldResembleProto, &pb.BuildInfra{
  4590  					Bbagent: &pb.BuildInfra_BBAgent{
  4591  						CacheDir:    "cache",
  4592  						PayloadPath: "kitchen-checkout",
  4593  					},
  4594  					Buildbucket: &pb.BuildInfra_Buildbucket{
  4595  						Hostname: "app.appspot.com",
  4596  					},
  4597  					Logdog: &pb.BuildInfra_LogDog{
  4598  						Project: "project",
  4599  					},
  4600  					Resultdb: &pb.BuildInfra_ResultDB{},
  4601  					Swarming: &pb.BuildInfra_Swarming{
  4602  						Caches: []*pb.BuildInfra_Swarming_CacheEntry{
  4603  							{
  4604  								Name: "builder_1809c38861a9996b1748e4640234fbd089992359f6f23f62f68deb98528f5f2b_v2",
  4605  								Path: "builder",
  4606  								WaitForWarmCache: &durationpb.Duration{
  4607  									Seconds: 240,
  4608  								},
  4609  							},
  4610  						},
  4611  						Priority: 1,
  4612  					},
  4613  				})
  4614  			})
  4615  		})
  4616  
  4617  		Convey("backend", func() {
  4618  			b := &pb.Build{
  4619  				Builder: &pb.BuilderID{
  4620  					Project: "project",
  4621  					Bucket:  "bucket",
  4622  					Builder: "builder",
  4623  				},
  4624  				Infra: &pb.BuildInfra{
  4625  					Bbagent: &pb.BuildInfra_BBAgent{
  4626  						CacheDir:    "cache",
  4627  						PayloadPath: "kitchen-checkout",
  4628  					},
  4629  					Buildbucket: &pb.BuildInfra_Buildbucket{
  4630  						Hostname: "app.appspot.com",
  4631  					},
  4632  					Logdog: &pb.BuildInfra_LogDog{
  4633  						Hostname: "host",
  4634  						Project:  "project",
  4635  					},
  4636  					Resultdb: &pb.BuildInfra_ResultDB{},
  4637  				},
  4638  			}
  4639  			s := &pb.SettingsCfg{
  4640  				Backends: []*pb.BackendSetting{
  4641  					{
  4642  						Target:   "swarming://chromium-swarm",
  4643  						Hostname: "chromium-swarm.appspot.com",
  4644  					},
  4645  				},
  4646  			}
  4647  			bldrCfg := &pb.BuilderConfig{
  4648  				ServiceAccount: "account",
  4649  				Priority:       200,
  4650  				Backend: &pb.BuilderConfig_Backend{
  4651  					Target: "swarming://chromium-swarm",
  4652  				},
  4653  				Experiments: map[string]int32{
  4654  					bb.ExperimentBackendAlt: 100,
  4655  				},
  4656  			}
  4657  
  4658  			// Need these to be set so that setSwarmingOrBackend can be set.
  4659  			setExecutable(nil, bldrCfg, b)
  4660  			setInput(ctx, nil, bldrCfg, b)
  4661  			setExperiments(ctx, nil, bldrCfg, s, b)
  4662  
  4663  			Convey("use builder Priority and ServiceAccount", func() {
  4664  
  4665  				setSwarmingOrBackend(ctx, nil, bldrCfg, b, s)
  4666  
  4667  				expectedBackendConfig := &structpb.Struct{}
  4668  				expectedBackendConfig.Fields = make(map[string]*structpb.Value)
  4669  				expectedBackendConfig.Fields["priority"] = &structpb.Value{Kind: &structpb.Value_NumberValue{NumberValue: 200}}
  4670  				expectedBackendConfig.Fields["service_account"] = &structpb.Value{Kind: &structpb.Value_StringValue{StringValue: "account"}}
  4671  
  4672  				So(b.Infra.Backend, ShouldResembleProto, &pb.BuildInfra_Backend{
  4673  					Caches: []*pb.CacheEntry{
  4674  						{
  4675  							Name:             "builder_1809c38861a9996b1748e4640234fbd089992359f6f23f62f68deb98528f5f2b_v2",
  4676  							Path:             "builder",
  4677  							WaitForWarmCache: &durationpb.Duration{Seconds: 240},
  4678  						},
  4679  					},
  4680  					Config:   expectedBackendConfig,
  4681  					Hostname: "chromium-swarm.appspot.com",
  4682  					Task: &pb.Task{
  4683  						Id: &pb.TaskID{
  4684  							Target: "swarming://chromium-swarm",
  4685  						},
  4686  					},
  4687  				})
  4688  			})
  4689  
  4690  			Convey("use backend priority and ServiceAccount", func() {
  4691  				bldrCfg.Backend.ConfigJson = "{\"priority\": 2, \"service_account\": \"service_account\"}"
  4692  				setSwarmingOrBackend(ctx, nil, bldrCfg, b, s)
  4693  
  4694  				expectedBackendConfig := &structpb.Struct{}
  4695  				expectedBackendConfig.Fields = make(map[string]*structpb.Value)
  4696  				expectedBackendConfig.Fields["priority"] = &structpb.Value{Kind: &structpb.Value_NumberValue{NumberValue: 2}}
  4697  				expectedBackendConfig.Fields["service_account"] = &structpb.Value{Kind: &structpb.Value_StringValue{StringValue: "service_account"}}
  4698  
  4699  				So(b.Infra.Backend, ShouldResembleProto, &pb.BuildInfra_Backend{
  4700  					Caches: []*pb.CacheEntry{
  4701  						{
  4702  							Name:             "builder_1809c38861a9996b1748e4640234fbd089992359f6f23f62f68deb98528f5f2b_v2",
  4703  							Path:             "builder",
  4704  							WaitForWarmCache: &durationpb.Duration{Seconds: 240},
  4705  						},
  4706  					},
  4707  					Config:   expectedBackendConfig,
  4708  					Hostname: "chromium-swarm.appspot.com",
  4709  					Task: &pb.Task{
  4710  						Id: &pb.TaskID{
  4711  							Target: "swarming://chromium-swarm",
  4712  						},
  4713  					},
  4714  				})
  4715  			})
  4716  
  4717  			Convey("use user requested priority", func() {
  4718  				req := &pb.ScheduleBuildRequest{Priority: 22}
  4719  				setSwarmingOrBackend(ctx, req, bldrCfg, b, s)
  4720  
  4721  				expectedBackendConfig := &structpb.Struct{}
  4722  				expectedBackendConfig.Fields = make(map[string]*structpb.Value)
  4723  				expectedBackendConfig.Fields["priority"] = &structpb.Value{Kind: &structpb.Value_NumberValue{NumberValue: 22}}
  4724  				expectedBackendConfig.Fields["service_account"] = &structpb.Value{Kind: &structpb.Value_StringValue{StringValue: "account"}}
  4725  
  4726  				So(b.Infra.Backend, ShouldResembleProto, &pb.BuildInfra_Backend{
  4727  					Caches: []*pb.CacheEntry{
  4728  						{
  4729  							Name:             "builder_1809c38861a9996b1748e4640234fbd089992359f6f23f62f68deb98528f5f2b_v2",
  4730  							Path:             "builder",
  4731  							WaitForWarmCache: &durationpb.Duration{Seconds: 240},
  4732  						},
  4733  					},
  4734  					Config:   expectedBackendConfig,
  4735  					Hostname: "chromium-swarm.appspot.com",
  4736  					Task: &pb.Task{
  4737  						Id: &pb.TaskID{
  4738  							Target: "swarming://chromium-swarm",
  4739  						},
  4740  					},
  4741  				})
  4742  			})
  4743  
  4744  			Convey("backend alt is used", func() {
  4745  				bldrCfg.BackendAlt = &pb.BuilderConfig_Backend{
  4746  					Target: "swarming://chromium-swarm-alt",
  4747  				}
  4748  
  4749  				setSwarmingOrBackend(ctx, nil, bldrCfg, b, s)
  4750  
  4751  				expectedBackendConfig := &structpb.Struct{}
  4752  				expectedBackendConfig.Fields = make(map[string]*structpb.Value)
  4753  				expectedBackendConfig.Fields["priority"] = &structpb.Value{Kind: &structpb.Value_NumberValue{NumberValue: 200}}
  4754  				expectedBackendConfig.Fields["service_account"] = &structpb.Value{Kind: &structpb.Value_StringValue{StringValue: "account"}}
  4755  
  4756  				So(b.Infra.Backend, ShouldResembleProto, &pb.BuildInfra_Backend{
  4757  					Caches: []*pb.CacheEntry{
  4758  						{
  4759  							Name:             "builder_1809c38861a9996b1748e4640234fbd089992359f6f23f62f68deb98528f5f2b_v2",
  4760  							Path:             "builder",
  4761  							WaitForWarmCache: &durationpb.Duration{Seconds: 240},
  4762  						},
  4763  					},
  4764  					Config: expectedBackendConfig,
  4765  					Task: &pb.Task{
  4766  						Id: &pb.TaskID{
  4767  							Target: "swarming://chromium-swarm-alt",
  4768  						},
  4769  					},
  4770  				})
  4771  			})
  4772  
  4773  			Convey("backend_alt exp is true, derive backend from swarming", func() {
  4774  				bldrCfg := &pb.BuilderConfig{
  4775  					ServiceAccount: "account",
  4776  					Priority:       200,
  4777  					Experiments: map[string]int32{
  4778  						bb.ExperimentBackendAlt: 100,
  4779  					},
  4780  					SwarmingHost: "chromium-swarming.appspot.com",
  4781  				}
  4782  
  4783  				s.SwarmingBackends = map[string]string{
  4784  					"chromium-swarming.appspot.com": "swarming://chromium-swarm",
  4785  				}
  4786  				setSwarmingOrBackend(ctx, nil, bldrCfg, b, s)
  4787  
  4788  				expectedBackendConfig := &structpb.Struct{}
  4789  				expectedBackendConfig.Fields = make(map[string]*structpb.Value)
  4790  				expectedBackendConfig.Fields["priority"] = &structpb.Value{Kind: &structpb.Value_NumberValue{NumberValue: 200}}
  4791  				expectedBackendConfig.Fields["service_account"] = &structpb.Value{Kind: &structpb.Value_StringValue{StringValue: "account"}}
  4792  
  4793  				So(b.Infra.Swarming, ShouldBeNil)
  4794  				So(b.Infra.Backend, ShouldResembleProto, &pb.BuildInfra_Backend{
  4795  					Caches: []*pb.CacheEntry{
  4796  						{
  4797  							Name:             "builder_1809c38861a9996b1748e4640234fbd089992359f6f23f62f68deb98528f5f2b_v2",
  4798  							Path:             "builder",
  4799  							WaitForWarmCache: &durationpb.Duration{Seconds: 240},
  4800  						},
  4801  					},
  4802  					Config:   expectedBackendConfig,
  4803  					Hostname: "chromium-swarm.appspot.com",
  4804  					Task: &pb.Task{
  4805  						Id: &pb.TaskID{
  4806  							Target: "swarming://chromium-swarm",
  4807  						},
  4808  					},
  4809  				})
  4810  			})
  4811  
  4812  			Convey("backend_alt exp is true but no swarming to backend mapping, so use swarming", func() {
  4813  				bldrCfg := &pb.BuilderConfig{
  4814  					ServiceAccount: "account",
  4815  					Priority:       200,
  4816  					Experiments: map[string]int32{
  4817  						bb.ExperimentBackendAlt: 100,
  4818  					},
  4819  				}
  4820  
  4821  				setSwarmingOrBackend(ctx, nil, bldrCfg, b, s)
  4822  
  4823  				expectedBackendConfig := &structpb.Struct{}
  4824  				expectedBackendConfig.Fields = make(map[string]*structpb.Value)
  4825  				expectedBackendConfig.Fields["priority"] = &structpb.Value{Kind: &structpb.Value_NumberValue{NumberValue: 200}}
  4826  				expectedBackendConfig.Fields["service_account"] = &structpb.Value{Kind: &structpb.Value_StringValue{StringValue: "account"}}
  4827  
  4828  				So(b.Infra.Backend, ShouldBeNil)
  4829  				So(b.Infra.Swarming, ShouldResembleProto, &pb.BuildInfra_Swarming{
  4830  					TaskServiceAccount: "account",
  4831  					Priority:           200,
  4832  					Caches: []*pb.BuildInfra_Swarming_CacheEntry{
  4833  						{
  4834  							Name:             "builder_1809c38861a9996b1748e4640234fbd089992359f6f23f62f68deb98528f5f2b_v2",
  4835  							Path:             "builder",
  4836  							WaitForWarmCache: &durationpb.Duration{Seconds: 240},
  4837  						},
  4838  					},
  4839  				})
  4840  			})
  4841  
  4842  			Convey("swarming is used", func() {
  4843  				bldrCfg := &pb.BuilderConfig{
  4844  					ServiceAccount: "account",
  4845  					Priority:       200,
  4846  				}
  4847  
  4848  				setSwarmingOrBackend(ctx, nil, bldrCfg, b, s)
  4849  
  4850  				expectedBackendConfig := &structpb.Struct{}
  4851  				expectedBackendConfig.Fields = make(map[string]*structpb.Value)
  4852  				expectedBackendConfig.Fields["priority"] = &structpb.Value{Kind: &structpb.Value_NumberValue{NumberValue: 200}}
  4853  				expectedBackendConfig.Fields["service_account"] = &structpb.Value{Kind: &structpb.Value_StringValue{StringValue: "account"}}
  4854  
  4855  				So(b.Infra.Backend, ShouldBeNil)
  4856  				So(b.Infra.Swarming, ShouldResembleProto, &pb.BuildInfra_Swarming{
  4857  					TaskServiceAccount: "account",
  4858  					Priority:           200,
  4859  					Caches: []*pb.BuildInfra_Swarming_CacheEntry{
  4860  						{
  4861  							Name:             "builder_1809c38861a9996b1748e4640234fbd089992359f6f23f62f68deb98528f5f2b_v2",
  4862  							Path:             "builder",
  4863  							WaitForWarmCache: &durationpb.Duration{Seconds: 240},
  4864  						},
  4865  					},
  4866  				})
  4867  			})
  4868  		})
  4869  	})
  4870  
  4871  	Convey("setInput", t, func() {
  4872  		ctx := memlogger.Use(context.Background())
  4873  
  4874  		Convey("nil", func() {
  4875  			b := &pb.Build{}
  4876  
  4877  			setInput(ctx, nil, nil, b)
  4878  			So(b.Input, ShouldResembleProto, &pb.Build_Input{
  4879  				Properties: &structpb.Struct{},
  4880  			})
  4881  		})
  4882  
  4883  		Convey("request", func() {
  4884  			Convey("properties", func() {
  4885  				Convey("empty", func() {
  4886  					req := &pb.ScheduleBuildRequest{}
  4887  					b := &pb.Build{}
  4888  
  4889  					setInput(ctx, req, nil, b)
  4890  					So(b.Input, ShouldResembleProto, &pb.Build_Input{
  4891  						Properties: &structpb.Struct{},
  4892  					})
  4893  				})
  4894  
  4895  				Convey("non-empty", func() {
  4896  					req := &pb.ScheduleBuildRequest{
  4897  						Properties: &structpb.Struct{
  4898  							Fields: map[string]*structpb.Value{
  4899  								"int": {
  4900  									Kind: &structpb.Value_NumberValue{
  4901  										NumberValue: 1,
  4902  									},
  4903  								},
  4904  								"str": {
  4905  									Kind: &structpb.Value_StringValue{
  4906  										StringValue: "value",
  4907  									},
  4908  								},
  4909  							},
  4910  						},
  4911  					}
  4912  					b := &pb.Build{}
  4913  
  4914  					setInput(ctx, req, nil, b)
  4915  					So(b.Input, ShouldResembleProto, &pb.Build_Input{
  4916  						Properties: &structpb.Struct{
  4917  							Fields: map[string]*structpb.Value{
  4918  								"int": {
  4919  									Kind: &structpb.Value_NumberValue{
  4920  										NumberValue: 1,
  4921  									},
  4922  								},
  4923  								"str": {
  4924  									Kind: &structpb.Value_StringValue{
  4925  										StringValue: "value",
  4926  									},
  4927  								},
  4928  							},
  4929  						},
  4930  					})
  4931  				})
  4932  			})
  4933  		})
  4934  
  4935  		Convey("config", func() {
  4936  			Convey("properties", func() {
  4937  				cfg := &pb.BuilderConfig{
  4938  					Properties: "{\"int\": 1, \"str\": \"value\"}",
  4939  				}
  4940  				b := &pb.Build{
  4941  					Builder: &pb.BuilderID{
  4942  						Project: "project",
  4943  						Bucket:  "bucket",
  4944  						Builder: "builder",
  4945  					},
  4946  				}
  4947  
  4948  				setInput(ctx, nil, cfg, b)
  4949  				So(b.Input, ShouldResembleProto, &pb.Build_Input{
  4950  					Properties: &structpb.Struct{
  4951  						Fields: map[string]*structpb.Value{
  4952  							"int": {
  4953  								Kind: &structpb.Value_NumberValue{
  4954  									NumberValue: 1,
  4955  								},
  4956  							},
  4957  							"str": {
  4958  								Kind: &structpb.Value_StringValue{
  4959  									StringValue: "value",
  4960  								},
  4961  							},
  4962  						},
  4963  					},
  4964  				})
  4965  			})
  4966  
  4967  			Convey("recipe", func() {
  4968  				Convey("empty", func() {
  4969  					cfg := &pb.BuilderConfig{
  4970  						Recipe: &pb.BuilderConfig_Recipe{},
  4971  					}
  4972  					b := &pb.Build{
  4973  						Builder: &pb.BuilderID{
  4974  							Project: "project",
  4975  							Bucket:  "bucket",
  4976  							Builder: "builder",
  4977  						},
  4978  					}
  4979  
  4980  					setInput(ctx, nil, cfg, b)
  4981  					So(b.Input, ShouldResembleProto, &pb.Build_Input{
  4982  						Properties: &structpb.Struct{
  4983  							Fields: map[string]*structpb.Value{
  4984  								"recipe": {
  4985  									Kind: &structpb.Value_StringValue{},
  4986  								},
  4987  							},
  4988  						},
  4989  					})
  4990  				})
  4991  
  4992  				Convey("properties", func() {
  4993  					cfg := &pb.BuilderConfig{
  4994  						Recipe: &pb.BuilderConfig_Recipe{
  4995  							Properties: []string{
  4996  								"key:value",
  4997  							},
  4998  						},
  4999  					}
  5000  					b := &pb.Build{
  5001  						Builder: &pb.BuilderID{
  5002  							Project: "project",
  5003  							Bucket:  "bucket",
  5004  							Builder: "builder",
  5005  						},
  5006  					}
  5007  
  5008  					setInput(ctx, nil, cfg, b)
  5009  					So(b.Input, ShouldResembleProto, &pb.Build_Input{
  5010  						Properties: &structpb.Struct{
  5011  							Fields: map[string]*structpb.Value{
  5012  								"key": {
  5013  									Kind: &structpb.Value_StringValue{
  5014  										StringValue: "value",
  5015  									},
  5016  								},
  5017  								"recipe": {
  5018  									Kind: &structpb.Value_StringValue{
  5019  										StringValue: "",
  5020  									},
  5021  								},
  5022  							},
  5023  						},
  5024  					})
  5025  				})
  5026  
  5027  				Convey("properties json", func() {
  5028  					cfg := &pb.BuilderConfig{
  5029  						Recipe: &pb.BuilderConfig_Recipe{
  5030  							PropertiesJ: []string{
  5031  								"str:\"value\"",
  5032  								"int:1",
  5033  							},
  5034  						},
  5035  					}
  5036  					b := &pb.Build{
  5037  						Builder: &pb.BuilderID{
  5038  							Project: "project",
  5039  							Bucket:  "bucket",
  5040  							Builder: "builder",
  5041  						},
  5042  					}
  5043  
  5044  					setInput(ctx, nil, cfg, b)
  5045  					So(b.Input, ShouldResembleProto, &pb.Build_Input{
  5046  						Properties: &structpb.Struct{
  5047  							Fields: map[string]*structpb.Value{
  5048  								"int": {
  5049  									Kind: &structpb.Value_NumberValue{
  5050  										NumberValue: 1,
  5051  									},
  5052  								},
  5053  								"recipe": {
  5054  									Kind: &structpb.Value_StringValue{},
  5055  								},
  5056  								"str": {
  5057  									Kind: &structpb.Value_StringValue{
  5058  										StringValue: "value",
  5059  									},
  5060  								},
  5061  							},
  5062  						},
  5063  					})
  5064  				})
  5065  
  5066  				Convey("recipe", func() {
  5067  					cfg := &pb.BuilderConfig{
  5068  						Recipe: &pb.BuilderConfig_Recipe{
  5069  							Name: "recipe",
  5070  						},
  5071  					}
  5072  					b := &pb.Build{
  5073  						Builder: &pb.BuilderID{
  5074  							Project: "project",
  5075  							Bucket:  "bucket",
  5076  							Builder: "builder",
  5077  						},
  5078  					}
  5079  
  5080  					setInput(ctx, nil, cfg, b)
  5081  					So(b.Input, ShouldResembleProto, &pb.Build_Input{
  5082  						Properties: &structpb.Struct{
  5083  							Fields: map[string]*structpb.Value{
  5084  								"recipe": {
  5085  									Kind: &structpb.Value_StringValue{
  5086  										StringValue: "recipe",
  5087  									},
  5088  								},
  5089  							},
  5090  						},
  5091  					})
  5092  				})
  5093  
  5094  				Convey("properties json > properties", func() {
  5095  					cfg := &pb.BuilderConfig{
  5096  						Recipe: &pb.BuilderConfig_Recipe{
  5097  							Properties: []string{
  5098  								"key:value",
  5099  							},
  5100  							PropertiesJ: []string{
  5101  								"key:1",
  5102  							},
  5103  						},
  5104  					}
  5105  					b := &pb.Build{
  5106  						Builder: &pb.BuilderID{
  5107  							Project: "project",
  5108  							Bucket:  "bucket",
  5109  							Builder: "builder",
  5110  						},
  5111  					}
  5112  
  5113  					setInput(ctx, nil, cfg, b)
  5114  					So(b.Input, ShouldResembleProto, &pb.Build_Input{
  5115  						Properties: &structpb.Struct{
  5116  							Fields: map[string]*structpb.Value{
  5117  								"key": {
  5118  									Kind: &structpb.Value_NumberValue{
  5119  										NumberValue: 1,
  5120  									},
  5121  								},
  5122  								"recipe": {
  5123  									Kind: &structpb.Value_StringValue{
  5124  										StringValue: "",
  5125  									},
  5126  								},
  5127  							},
  5128  						},
  5129  					})
  5130  				})
  5131  
  5132  				Convey("recipe > properties", func() {
  5133  					cfg := &pb.BuilderConfig{
  5134  						Recipe: &pb.BuilderConfig_Recipe{
  5135  							Name: "recipe",
  5136  							Properties: []string{
  5137  								"recipe:value",
  5138  							},
  5139  						},
  5140  					}
  5141  					b := &pb.Build{
  5142  						Builder: &pb.BuilderID{
  5143  							Project: "project",
  5144  							Bucket:  "bucket",
  5145  							Builder: "builder",
  5146  						},
  5147  					}
  5148  
  5149  					setInput(ctx, nil, cfg, b)
  5150  					So(b.Input, ShouldResembleProto, &pb.Build_Input{
  5151  						Properties: &structpb.Struct{
  5152  							Fields: map[string]*structpb.Value{
  5153  								"recipe": {
  5154  									Kind: &structpb.Value_StringValue{
  5155  										StringValue: "recipe",
  5156  									},
  5157  								},
  5158  							},
  5159  						},
  5160  					})
  5161  				})
  5162  
  5163  				Convey("recipe > properties json", func() {
  5164  					cfg := &pb.BuilderConfig{
  5165  						Recipe: &pb.BuilderConfig_Recipe{
  5166  							Name: "recipe",
  5167  							PropertiesJ: []string{
  5168  								"recipe:\"value\"",
  5169  							},
  5170  						},
  5171  					}
  5172  					b := &pb.Build{
  5173  						Builder: &pb.BuilderID{
  5174  							Project: "project",
  5175  							Bucket:  "bucket",
  5176  							Builder: "builder",
  5177  						},
  5178  					}
  5179  
  5180  					setInput(ctx, nil, cfg, b)
  5181  					So(b.Input, ShouldResembleProto, &pb.Build_Input{
  5182  						Properties: &structpb.Struct{
  5183  							Fields: map[string]*structpb.Value{
  5184  								"recipe": {
  5185  									Kind: &structpb.Value_StringValue{
  5186  										StringValue: "recipe",
  5187  									},
  5188  								},
  5189  							},
  5190  						},
  5191  					})
  5192  				})
  5193  			})
  5194  		})
  5195  
  5196  		Convey("request > config", func() {
  5197  			req := &pb.ScheduleBuildRequest{
  5198  				Properties: &structpb.Struct{
  5199  					Fields: map[string]*structpb.Value{
  5200  						"allowed": {
  5201  							Kind: &structpb.Value_StringValue{
  5202  								StringValue: "I'm alright",
  5203  							},
  5204  						},
  5205  						"override": {
  5206  							Kind: &structpb.Value_StringValue{
  5207  								StringValue: "req value",
  5208  							},
  5209  						},
  5210  						"req key": {
  5211  							Kind: &structpb.Value_StringValue{
  5212  								StringValue: "req value",
  5213  							},
  5214  						},
  5215  					},
  5216  				},
  5217  			}
  5218  			cfg := &pb.BuilderConfig{
  5219  				Properties:               "{\"override\": \"cfg value\", \"allowed\": \"stuff\", \"cfg key\": \"cfg value\"}",
  5220  				AllowedPropertyOverrides: []string{"allowed"},
  5221  			}
  5222  			b := &pb.Build{
  5223  				Builder: &pb.BuilderID{
  5224  					Project: "project",
  5225  					Bucket:  "bucket",
  5226  					Builder: "builder",
  5227  				},
  5228  			}
  5229  
  5230  			setInput(ctx, req, cfg, b)
  5231  			So(b.Input, ShouldResembleProto, &pb.Build_Input{
  5232  				Properties: &structpb.Struct{
  5233  					Fields: map[string]*structpb.Value{
  5234  						"allowed": {
  5235  							Kind: &structpb.Value_StringValue{
  5236  								StringValue: "I'm alright",
  5237  							},
  5238  						},
  5239  						"cfg key": {
  5240  							Kind: &structpb.Value_StringValue{
  5241  								StringValue: "cfg value",
  5242  							},
  5243  						},
  5244  						"override": {
  5245  							Kind: &structpb.Value_StringValue{
  5246  								StringValue: "req value",
  5247  							},
  5248  						},
  5249  						"req key": {
  5250  							Kind: &structpb.Value_StringValue{
  5251  								StringValue: "req value",
  5252  							},
  5253  						},
  5254  					},
  5255  				},
  5256  			})
  5257  			So(ctx, memlogger.ShouldHaveLog, logging.Warning, "ScheduleBuild: Unpermitted Override for property \"override\"")
  5258  			So(ctx, memlogger.ShouldNotHaveLog, logging.Warning, "ScheduleBuild: Unpermitted Override for property \"allowed\"")
  5259  			So(ctx, memlogger.ShouldNotHaveLog, logging.Warning, "ScheduleBuild: Unpermitted Override for property \"cfg key\"")
  5260  			So(ctx, memlogger.ShouldNotHaveLog, logging.Warning, "ScheduleBuild: Unpermitted Override for property \"req key\"")
  5261  		})
  5262  	})
  5263  
  5264  	Convey("setTags", t, func() {
  5265  		Convey("nil", func() {
  5266  			b := &pb.Build{}
  5267  
  5268  			setTags(nil, b, "")
  5269  			So(b.Tags, ShouldResemble, []*pb.StringPair{})
  5270  		})
  5271  
  5272  		Convey("request", func() {
  5273  			req := &pb.ScheduleBuildRequest{
  5274  				Tags: []*pb.StringPair{
  5275  					{
  5276  						Key:   "key2",
  5277  						Value: "value2",
  5278  					},
  5279  					{
  5280  						Key:   "key1",
  5281  						Value: "value1",
  5282  					},
  5283  				},
  5284  			}
  5285  			normalizeSchedule(req)
  5286  			b := &pb.Build{}
  5287  
  5288  			setTags(req, b, "")
  5289  			So(b.Tags, ShouldResemble, []*pb.StringPair{
  5290  				{
  5291  					Key:   "key1",
  5292  					Value: "value1",
  5293  				},
  5294  				{
  5295  					Key:   "key2",
  5296  					Value: "value2",
  5297  				},
  5298  			})
  5299  		})
  5300  
  5301  		Convey("builder", func() {
  5302  			req := &pb.ScheduleBuildRequest{
  5303  				Builder: &pb.BuilderID{
  5304  					Project: "project",
  5305  					Bucket:  "bucket",
  5306  					Builder: "builder",
  5307  				},
  5308  			}
  5309  			normalizeSchedule(req)
  5310  			b := &pb.Build{}
  5311  
  5312  			setTags(req, b, "")
  5313  			So(b.Tags, ShouldResemble, []*pb.StringPair{
  5314  				{
  5315  					Key:   "builder",
  5316  					Value: "builder",
  5317  				},
  5318  			})
  5319  		})
  5320  
  5321  		Convey("gitiles commit", func() {
  5322  			req := &pb.ScheduleBuildRequest{
  5323  				GitilesCommit: &pb.GitilesCommit{
  5324  					Host:    "host",
  5325  					Project: "project",
  5326  					Id:      "id",
  5327  					Ref:     "ref",
  5328  				},
  5329  			}
  5330  			normalizeSchedule(req)
  5331  			b := &pb.Build{}
  5332  
  5333  			setTags(req, b, "")
  5334  			So(b.Tags, ShouldResemble, []*pb.StringPair{
  5335  				{
  5336  					Key:   "buildset",
  5337  					Value: "commit/gitiles/host/project/+/id",
  5338  				},
  5339  				{
  5340  					Key:   "gitiles_ref",
  5341  					Value: "ref",
  5342  				},
  5343  			})
  5344  		})
  5345  
  5346  		Convey("partial gitiles commit", func() {
  5347  			req := &pb.ScheduleBuildRequest{
  5348  				GitilesCommit: &pb.GitilesCommit{
  5349  					Host:    "host",
  5350  					Project: "project",
  5351  					Ref:     "ref",
  5352  				},
  5353  			}
  5354  			normalizeSchedule(req)
  5355  			b := &pb.Build{}
  5356  
  5357  			setTags(req, b, "")
  5358  			So(b.Tags, ShouldResemble, []*pb.StringPair{
  5359  				{
  5360  					Key:   "gitiles_ref",
  5361  					Value: "ref",
  5362  				},
  5363  			})
  5364  		})
  5365  
  5366  		Convey("gerrit changes", func() {
  5367  			Convey("one", func() {
  5368  				req := &pb.ScheduleBuildRequest{
  5369  					GerritChanges: []*pb.GerritChange{
  5370  						{
  5371  							Host:     "host",
  5372  							Change:   1,
  5373  							Patchset: 2,
  5374  						},
  5375  					},
  5376  				}
  5377  				normalizeSchedule(req)
  5378  				b := &pb.Build{}
  5379  
  5380  				setTags(req, b, "")
  5381  				So(b.Tags, ShouldResemble, []*pb.StringPair{
  5382  					{
  5383  						Key:   "buildset",
  5384  						Value: "patch/gerrit/host/1/2",
  5385  					},
  5386  				})
  5387  			})
  5388  
  5389  			Convey("many", func() {
  5390  				req := &pb.ScheduleBuildRequest{
  5391  					GerritChanges: []*pb.GerritChange{
  5392  						{
  5393  							Host:     "host",
  5394  							Change:   3,
  5395  							Patchset: 4,
  5396  						},
  5397  						{
  5398  							Host:     "host",
  5399  							Change:   1,
  5400  							Patchset: 2,
  5401  						},
  5402  					},
  5403  				}
  5404  				normalizeSchedule(req)
  5405  				b := &pb.Build{}
  5406  
  5407  				setTags(req, b, "")
  5408  				So(b.Tags, ShouldResemble, []*pb.StringPair{
  5409  					{
  5410  						Key:   "buildset",
  5411  						Value: "patch/gerrit/host/1/2",
  5412  					},
  5413  					{
  5414  						Key:   "buildset",
  5415  						Value: "patch/gerrit/host/3/4",
  5416  					},
  5417  				})
  5418  			})
  5419  		})
  5420  
  5421  		Convey("various", func() {
  5422  			req := &pb.ScheduleBuildRequest{
  5423  				Builder: &pb.BuilderID{
  5424  					Project: "project",
  5425  					Bucket:  "bucket",
  5426  					Builder: "builder",
  5427  				},
  5428  				GerritChanges: []*pb.GerritChange{
  5429  					{
  5430  						Host:     "host",
  5431  						Change:   3,
  5432  						Patchset: 4,
  5433  					},
  5434  					{
  5435  						Host:     "host",
  5436  						Change:   1,
  5437  						Patchset: 2,
  5438  					},
  5439  				},
  5440  				GitilesCommit: &pb.GitilesCommit{
  5441  					Host:    "host",
  5442  					Project: "project",
  5443  					Id:      "id",
  5444  					Ref:     "ref",
  5445  				},
  5446  				Tags: []*pb.StringPair{
  5447  					{
  5448  						Key:   "key2",
  5449  						Value: "value2",
  5450  					},
  5451  					{
  5452  						Key:   "key1",
  5453  						Value: "value1",
  5454  					},
  5455  				},
  5456  			}
  5457  			normalizeSchedule(req)
  5458  			b := &pb.Build{}
  5459  
  5460  			setTags(req, b, "")
  5461  			So(b.Tags, ShouldResemble, []*pb.StringPair{
  5462  				{
  5463  					Key:   "builder",
  5464  					Value: "builder",
  5465  				},
  5466  				{
  5467  					Key:   "buildset",
  5468  					Value: "commit/gitiles/host/project/+/id",
  5469  				},
  5470  				{
  5471  					Key:   "buildset",
  5472  					Value: "patch/gerrit/host/1/2",
  5473  				},
  5474  				{
  5475  					Key:   "buildset",
  5476  					Value: "patch/gerrit/host/3/4",
  5477  				},
  5478  				{
  5479  					Key:   "gitiles_ref",
  5480  					Value: "ref",
  5481  				},
  5482  				{
  5483  					Key:   "key1",
  5484  					Value: "value1",
  5485  				},
  5486  				{
  5487  					Key:   "key2",
  5488  					Value: "value2",
  5489  				},
  5490  			})
  5491  		})
  5492  	})
  5493  
  5494  	Convey("setTimeouts", t, func() {
  5495  		Convey("nil", func() {
  5496  			b := &pb.Build{}
  5497  
  5498  			setTimeouts(nil, nil, b)
  5499  			So(b.ExecutionTimeout, ShouldResembleProto, &durationpb.Duration{
  5500  				Seconds: 10800,
  5501  			})
  5502  			So(b.GracePeriod, ShouldResembleProto, &durationpb.Duration{
  5503  				Seconds: 30,
  5504  			})
  5505  			So(b.SchedulingTimeout, ShouldResembleProto, &durationpb.Duration{
  5506  				Seconds: 21600,
  5507  			})
  5508  		})
  5509  
  5510  		Convey("request only", func() {
  5511  			req := &pb.ScheduleBuildRequest{
  5512  				ExecutionTimeout: &durationpb.Duration{
  5513  					Seconds: 1,
  5514  				},
  5515  				GracePeriod: &durationpb.Duration{
  5516  					Seconds: 2,
  5517  				},
  5518  				SchedulingTimeout: &durationpb.Duration{
  5519  					Seconds: 3,
  5520  				},
  5521  			}
  5522  			normalizeSchedule(req)
  5523  			b := &pb.Build{}
  5524  
  5525  			setTimeouts(req, nil, b)
  5526  			So(b.ExecutionTimeout, ShouldResembleProto, &durationpb.Duration{
  5527  				Seconds: 1,
  5528  			})
  5529  			So(b.GracePeriod, ShouldResembleProto, &durationpb.Duration{
  5530  				Seconds: 2,
  5531  			})
  5532  			So(b.SchedulingTimeout, ShouldResembleProto, &durationpb.Duration{
  5533  				Seconds: 3,
  5534  			})
  5535  		})
  5536  
  5537  		Convey("config only", func() {
  5538  			cfg := &pb.BuilderConfig{
  5539  				ExecutionTimeoutSecs: 1,
  5540  				ExpirationSecs:       3,
  5541  				GracePeriod: &durationpb.Duration{
  5542  					Seconds: 2,
  5543  				},
  5544  			}
  5545  			b := &pb.Build{}
  5546  
  5547  			setTimeouts(nil, cfg, b)
  5548  			So(b.ExecutionTimeout, ShouldResembleProto, &durationpb.Duration{
  5549  				Seconds: 1,
  5550  			})
  5551  			So(b.GracePeriod, ShouldResembleProto, &durationpb.Duration{
  5552  				Seconds: 2,
  5553  			})
  5554  			So(b.SchedulingTimeout, ShouldResembleProto, &durationpb.Duration{
  5555  				Seconds: 3,
  5556  			})
  5557  		})
  5558  
  5559  		Convey("override", func() {
  5560  			req := &pb.ScheduleBuildRequest{
  5561  				ExecutionTimeout: &durationpb.Duration{
  5562  					Seconds: 1,
  5563  				},
  5564  				GracePeriod: &durationpb.Duration{
  5565  					Seconds: 2,
  5566  				},
  5567  				SchedulingTimeout: &durationpb.Duration{
  5568  					Seconds: 3,
  5569  				},
  5570  			}
  5571  			normalizeSchedule(req)
  5572  			cfg := &pb.BuilderConfig{
  5573  				ExecutionTimeoutSecs: 4,
  5574  				ExpirationSecs:       6,
  5575  				GracePeriod: &durationpb.Duration{
  5576  					Seconds: 5,
  5577  				},
  5578  			}
  5579  			b := &pb.Build{}
  5580  
  5581  			setTimeouts(req, cfg, b)
  5582  			So(b.ExecutionTimeout, ShouldResembleProto, &durationpb.Duration{
  5583  				Seconds: 1,
  5584  			})
  5585  			So(b.GracePeriod, ShouldResembleProto, &durationpb.Duration{
  5586  				Seconds: 2,
  5587  			})
  5588  			So(b.SchedulingTimeout, ShouldResembleProto, &durationpb.Duration{
  5589  				Seconds: 3,
  5590  			})
  5591  		})
  5592  	})
  5593  
  5594  	Convey("ScheduleBuild", t, func() {
  5595  		srv := &Builds{}
  5596  		ctx := txndefer.FilterRDS(memory.Use(context.Background()))
  5597  		ctx = metrics.WithServiceInfo(ctx, "svc", "job", "ins")
  5598  		ctx = mathrand.Set(ctx, rand.New(rand.NewSource(0)))
  5599  		ctx, _ = testclock.UseTime(ctx, testclock.TestRecentTimeUTC)
  5600  		ctx, sch := tq.TestingContext(ctx, nil)
  5601  		datastore.GetTestable(ctx).AutoIndex(true)
  5602  		datastore.GetTestable(ctx).Consistent(true)
  5603  		ctx = auth.WithState(ctx, &authtest.FakeState{
  5604  			Identity: userID,
  5605  		})
  5606  
  5607  		So(config.SetTestSettingsCfg(ctx, &pb.SettingsCfg{
  5608  			Resultdb: &pb.ResultDBSettings{
  5609  				Hostname: "rdbHost",
  5610  			},
  5611  			Swarming: &pb.SwarmingSettings{
  5612  				BbagentPackage: &pb.SwarmingSettings_Package{
  5613  					PackageName: "bbagent",
  5614  					Version:     "bbagent-version",
  5615  				},
  5616  				KitchenPackage: &pb.SwarmingSettings_Package{
  5617  					PackageName: "kitchen",
  5618  					Version:     "kitchen-version",
  5619  				},
  5620  			},
  5621  			Backends: []*pb.BackendSetting{
  5622  				{
  5623  					Target:   "lite://foo-lite",
  5624  					Hostname: "foo_hostname",
  5625  					Mode:     &pb.BackendSetting_LiteMode_{},
  5626  				},
  5627  			},
  5628  		}), ShouldBeNil)
  5629  
  5630  		Convey("builder", func() {
  5631  			Convey("not found", func() {
  5632  				req := &pb.ScheduleBuildRequest{
  5633  					Builder: &pb.BuilderID{
  5634  						Project: "project",
  5635  						Bucket:  "bucket",
  5636  						Builder: "builder",
  5637  					},
  5638  				}
  5639  				rsp, err := srv.ScheduleBuild(ctx, req)
  5640  				So(err, ShouldErrLike, "not found")
  5641  				So(rsp, ShouldBeNil)
  5642  				So(sch.Tasks(), ShouldBeEmpty)
  5643  			})
  5644  
  5645  			Convey("permission denied", func() {
  5646  				So(datastore.Put(ctx, &model.Build{
  5647  					Proto: &pb.Build{
  5648  						Id: 1,
  5649  						Builder: &pb.BuilderID{
  5650  							Project: "project",
  5651  							Bucket:  "bucket",
  5652  							Builder: "builder",
  5653  						},
  5654  					},
  5655  				}), ShouldBeNil)
  5656  				req := &pb.ScheduleBuildRequest{
  5657  					Builder: &pb.BuilderID{
  5658  						Project: "project",
  5659  						Bucket:  "bucket",
  5660  						Builder: "builder",
  5661  					},
  5662  				}
  5663  				rsp, err := srv.ScheduleBuild(ctx, req)
  5664  				So(err, ShouldErrLike, "not found")
  5665  				So(rsp, ShouldBeNil)
  5666  				So(sch.Tasks(), ShouldBeEmpty)
  5667  			})
  5668  
  5669  			Convey("directly from dynamic", func() {
  5670  				ctx = auth.WithState(ctx, &authtest.FakeState{
  5671  					Identity: userID,
  5672  					FakeDB: authtest.NewFakeDB(
  5673  						authtest.MockPermission(userID, "project:bucket", bbperms.BuildsAdd),
  5674  					),
  5675  				})
  5676  
  5677  				Convey("no template in dynamic_builder_template", func() {
  5678  					testutil.PutBucket(ctx, "project", "bucket", &pb.Bucket{DynamicBuilderTemplate: &pb.Bucket_DynamicBuilderTemplate{}})
  5679  					req := &pb.ScheduleBuildRequest{
  5680  						Builder: &pb.BuilderID{
  5681  							Project: "project",
  5682  							Bucket:  "bucket",
  5683  							Builder: "builder",
  5684  						},
  5685  					}
  5686  
  5687  					rsp, err := srv.ScheduleBuild(ctx, req)
  5688  					So(err, ShouldBeNil)
  5689  					So(rsp, ShouldResembleProto, &pb.Build{
  5690  						Builder: &pb.BuilderID{
  5691  							Project: "project",
  5692  							Bucket:  "bucket",
  5693  							Builder: "builder",
  5694  						},
  5695  						CreatedBy:  string(userID),
  5696  						CreateTime: timestamppb.New(testclock.TestRecentTimeUTC),
  5697  						UpdateTime: timestamppb.New(testclock.TestRecentTimeUTC),
  5698  						Id:         9021868963221667745,
  5699  						Input:      &pb.Build_Input{},
  5700  						Status:     pb.Status_SCHEDULED,
  5701  					})
  5702  					So(sch.Tasks(), ShouldBeEmpty)
  5703  				})
  5704  
  5705  				Convey("has template in dynamic_builder_template", func() {
  5706  					testutil.PutBucket(ctx, "project", "bucket", &pb.Bucket{
  5707  						DynamicBuilderTemplate: &pb.Bucket_DynamicBuilderTemplate{
  5708  							Template: &pb.BuilderConfig{
  5709  								Backend: &pb.BuilderConfig_Backend{
  5710  									Target: "lite://foo-lite",
  5711  								},
  5712  								Experiments: map[string]int32{
  5713  									"luci.buildbucket.backend_alt": 100,
  5714  								},
  5715  							},
  5716  						},
  5717  					})
  5718  					req := &pb.ScheduleBuildRequest{
  5719  						Builder: &pb.BuilderID{
  5720  							Project: "project",
  5721  							Bucket:  "bucket",
  5722  							Builder: "builder",
  5723  						},
  5724  					}
  5725  
  5726  					rsp, err := srv.ScheduleBuild(ctx, req)
  5727  					So(err, ShouldBeNil)
  5728  					So(rsp, ShouldResembleProto, &pb.Build{
  5729  						Builder: &pb.BuilderID{
  5730  							Project: "project",
  5731  							Bucket:  "bucket",
  5732  							Builder: "builder",
  5733  						},
  5734  						CreatedBy:  string(userID),
  5735  						CreateTime: timestamppb.New(testclock.TestRecentTimeUTC),
  5736  						UpdateTime: timestamppb.New(testclock.TestRecentTimeUTC),
  5737  						Id:         9021868963221667745,
  5738  						Input:      &pb.Build_Input{},
  5739  						Status:     pb.Status_SCHEDULED,
  5740  					})
  5741  
  5742  					buildInDB := &model.Build{ID: 9021868963221667745}
  5743  					bInfra := &model.BuildInfra{Build: datastore.KeyForObj(ctx, buildInDB)}
  5744  					So(datastore.Get(ctx, buildInDB, bInfra), ShouldBeNil)
  5745  					So(bInfra.Proto.Backend, ShouldResembleProto, &pb.BuildInfra_Backend{
  5746  						Caches: []*pb.CacheEntry{
  5747  							{
  5748  								Name:             "builder_1809c38861a9996b1748e4640234fbd089992359f6f23f62f68deb98528f5f2b_v2",
  5749  								Path:             "builder",
  5750  								WaitForWarmCache: &durationpb.Duration{Seconds: 240},
  5751  							},
  5752  						},
  5753  						Config: &structpb.Struct{
  5754  							Fields: map[string]*structpb.Value{
  5755  								"priority": structpb.NewNumberValue(30),
  5756  							},
  5757  						},
  5758  						Hostname: "foo_hostname",
  5759  						Task: &pb.Task{
  5760  							Id: &pb.TaskID{
  5761  								Target: "lite://foo-lite",
  5762  							},
  5763  						},
  5764  					})
  5765  
  5766  					tasks := sch.Tasks()
  5767  					So(tasks, ShouldHaveLength, 3)
  5768  					sortTasksByClassName(tasks)
  5769  					backendTask, ok := tasks.Payloads()[0].(*taskdefs.CreateBackendBuildTask)
  5770  					So(ok, ShouldBeTrue)
  5771  					So(backendTask.BuildId, ShouldEqual, 9021868963221667745)
  5772  					So(tasks.Payloads()[1], ShouldResembleProto, &taskdefs.NotifyPubSub{
  5773  						BuildId: 9021868963221667745,
  5774  					})
  5775  					So(tasks.Payloads()[2], ShouldResembleProto, &taskdefs.NotifyPubSubGoProxy{
  5776  						BuildId: 9021868963221667745,
  5777  						Project: "project",
  5778  					})
  5779  				})
  5780  			})
  5781  
  5782  			Convey("static", func() {
  5783  				ctx = auth.WithState(ctx, &authtest.FakeState{
  5784  					Identity: userID,
  5785  					FakeDB: authtest.NewFakeDB(
  5786  						authtest.MockPermission(userID, "project:bucket", bbperms.BuildsAdd),
  5787  					),
  5788  				})
  5789  
  5790  				testutil.PutBucket(ctx, "project", "bucket", &pb.Bucket{
  5791  					Swarming: &pb.Swarming{},
  5792  				})
  5793  				req := &pb.ScheduleBuildRequest{
  5794  					Builder: &pb.BuilderID{
  5795  						Project: "project",
  5796  						Bucket:  "bucket",
  5797  						Builder: "builder",
  5798  					},
  5799  				}
  5800  
  5801  				Convey("not found", func() {
  5802  					rsp, err := srv.ScheduleBuild(ctx, req)
  5803  					So(err, ShouldErrLike, "error fetching builders")
  5804  					So(rsp, ShouldBeNil)
  5805  					So(sch.Tasks(), ShouldBeEmpty)
  5806  				})
  5807  
  5808  				Convey("exists", func() {
  5809  					testutil.PutBuilder(ctx, "project", "bucket", "builder", "")
  5810  					So(datastore.Put(ctx, &model.Build{
  5811  						ID: 9021868963221667745,
  5812  						Proto: &pb.Build{
  5813  							Id: 9021868963221667745,
  5814  							Builder: &pb.BuilderID{
  5815  								Project: "project",
  5816  								Bucket:  "bucket",
  5817  								Builder: "builder",
  5818  							},
  5819  						},
  5820  					}), ShouldBeNil)
  5821  
  5822  					rsp, err := srv.ScheduleBuild(ctx, req)
  5823  					So(err, ShouldErrLike, "build already exists")
  5824  					So(rsp, ShouldBeNil)
  5825  					So(sch.Tasks(), ShouldBeEmpty)
  5826  				})
  5827  
  5828  				Convey("ok without backend_go exp", func() {
  5829  					So(datastore.Put(ctx, &model.Builder{
  5830  						Parent: model.BucketKey(ctx, "project", "bucket"),
  5831  						ID:     "builder",
  5832  						Config: &pb.BuilderConfig{
  5833  							BuildNumbers: pb.Toggle_YES,
  5834  							Name:         "builder",
  5835  							SwarmingHost: "host",
  5836  						},
  5837  					}), ShouldBeNil)
  5838  					rsp, err := srv.ScheduleBuild(ctx, req)
  5839  					So(err, ShouldBeNil)
  5840  					So(rsp, ShouldResembleProto, &pb.Build{
  5841  						Builder: &pb.BuilderID{
  5842  							Project: "project",
  5843  							Bucket:  "bucket",
  5844  							Builder: "builder",
  5845  						},
  5846  						CreatedBy:  string(userID),
  5847  						CreateTime: timestamppb.New(testclock.TestRecentTimeUTC),
  5848  						UpdateTime: timestamppb.New(testclock.TestRecentTimeUTC),
  5849  						Id:         9021868963221667745,
  5850  						Input:      &pb.Build_Input{},
  5851  						Number:     1,
  5852  						Status:     pb.Status_SCHEDULED,
  5853  					})
  5854  					tasks := sch.Tasks()
  5855  					So(tasks, ShouldHaveLength, 3)
  5856  					sortTasksByClassName(tasks)
  5857  					So(tasks.Payloads()[0], ShouldResembleProto, &taskdefs.CreateSwarmingTask{
  5858  						BuildId: 9021868963221667745,
  5859  					})
  5860  					So(tasks.Payloads()[1], ShouldResembleProto, &taskdefs.NotifyPubSub{
  5861  						BuildId: 9021868963221667745,
  5862  					})
  5863  					So(tasks.Payloads()[2], ShouldResembleProto, &taskdefs.NotifyPubSubGoProxy{
  5864  						BuildId: 9021868963221667745,
  5865  						Project: "project",
  5866  					})
  5867  				})
  5868  
  5869  				Convey("ok with backend_go exp", func() {
  5870  					So(datastore.Put(ctx, &model.Builder{
  5871  						Parent: model.BucketKey(ctx, "project", "bucket"),
  5872  						ID:     "builder",
  5873  						Config: &pb.BuilderConfig{
  5874  							BuildNumbers: pb.Toggle_YES,
  5875  							Name:         "builder",
  5876  							Experiments:  map[string]int32{bb.ExperimentBackendGo: 100},
  5877  							SwarmingHost: "host",
  5878  						},
  5879  					}), ShouldBeNil)
  5880  
  5881  					req.Properties = &structpb.Struct{
  5882  						Fields: map[string]*structpb.Value{
  5883  							"input key": {
  5884  								Kind: &structpb.Value_StringValue{
  5885  									StringValue: "input value",
  5886  								},
  5887  							},
  5888  						},
  5889  					}
  5890  					rsp, err := srv.ScheduleBuild(ctx, req)
  5891  					So(err, ShouldBeNil)
  5892  					So(rsp, ShouldResembleProto, &pb.Build{
  5893  						Builder: &pb.BuilderID{
  5894  							Project: "project",
  5895  							Bucket:  "bucket",
  5896  							Builder: "builder",
  5897  						},
  5898  						CreatedBy:  string(userID),
  5899  						CreateTime: timestamppb.New(testclock.TestRecentTimeUTC),
  5900  						UpdateTime: timestamppb.New(testclock.TestRecentTimeUTC),
  5901  						Id:         9021868963221667745,
  5902  						Input:      &pb.Build_Input{},
  5903  						Number:     1,
  5904  						Status:     pb.Status_SCHEDULED,
  5905  					})
  5906  
  5907  					// check input.properties and infra are stored in their own Datastore
  5908  					// entities and not in Build entity.
  5909  					buildInDB := &model.Build{ID: 9021868963221667745}
  5910  					So(datastore.Get(ctx, buildInDB), ShouldBeNil)
  5911  					So(buildInDB.Proto.Input.Properties, ShouldBeNil)
  5912  					So(buildInDB.Proto.Infra, ShouldBeNil)
  5913  					inProp := &model.BuildInputProperties{Build: datastore.KeyForObj(ctx, buildInDB)}
  5914  					bInfra := &model.BuildInfra{Build: datastore.KeyForObj(ctx, buildInDB)}
  5915  					bs := &model.BuildStatus{Build: datastore.KeyForObj(ctx, buildInDB)}
  5916  					So(datastore.Get(ctx, inProp, bInfra, bs), ShouldBeNil)
  5917  					So(inProp.Proto, ShouldResembleProto, &structpb.Struct{
  5918  						Fields: map[string]*structpb.Value{
  5919  							"input key": {
  5920  								Kind: &structpb.Value_StringValue{
  5921  									StringValue: "input value",
  5922  								},
  5923  							},
  5924  						},
  5925  					})
  5926  					So(bInfra.Proto, ShouldNotBeNil)
  5927  					So(bs.BuildAddress, ShouldEqual, "project/bucket/builder/1")
  5928  					So(bs.Status, ShouldEqual, pb.Status_SCHEDULED)
  5929  
  5930  					tasks := sch.Tasks()
  5931  					So(tasks, ShouldHaveLength, 3)
  5932  					sortTasksByClassName(tasks)
  5933  					So(tasks.Payloads()[0], ShouldResembleProto, &taskdefs.CreateSwarmingBuildTask{
  5934  						BuildId: 9021868963221667745,
  5935  					})
  5936  					So(tasks.Payloads()[1], ShouldResembleProto, &taskdefs.NotifyPubSub{
  5937  						BuildId: 9021868963221667745,
  5938  					})
  5939  					So(tasks.Payloads()[2], ShouldResembleProto, &taskdefs.NotifyPubSubGoProxy{
  5940  						BuildId: 9021868963221667745,
  5941  						Project: "project",
  5942  					})
  5943  				})
  5944  
  5945  				Convey("dry_run", func() {
  5946  					So(datastore.Put(ctx, &model.Builder{
  5947  						Parent: model.BucketKey(ctx, "project", "bucket"),
  5948  						ID:     "builder",
  5949  						Config: &pb.BuilderConfig{
  5950  							BuildNumbers: pb.Toggle_YES,
  5951  							Name:         "builder",
  5952  						},
  5953  					}), ShouldBeNil)
  5954  
  5955  					req.DryRun = true
  5956  					rsp, err := srv.ScheduleBuild(ctx, req)
  5957  					So(err, ShouldBeNil)
  5958  					So(rsp, ShouldResembleProto, &pb.Build{
  5959  						Builder: &pb.BuilderID{
  5960  							Project: "project",
  5961  							Bucket:  "bucket",
  5962  							Builder: "builder",
  5963  						},
  5964  						Input: &pb.Build_Input{
  5965  							Properties: &structpb.Struct{},
  5966  						},
  5967  						Tags: []*pb.StringPair{
  5968  							{
  5969  								Key:   "builder",
  5970  								Value: "builder",
  5971  							},
  5972  						},
  5973  						Infra: &pb.BuildInfra{
  5974  							Bbagent: &pb.BuildInfra_BBAgent{
  5975  								CacheDir:    "cache",
  5976  								PayloadPath: "kitchen-checkout",
  5977  							},
  5978  							Buildbucket: &pb.BuildInfra_Buildbucket{
  5979  								Hostname: "app.appspot.com",
  5980  								Agent: &pb.BuildInfra_Buildbucket_Agent{
  5981  									Input: &pb.BuildInfra_Buildbucket_Agent_Input{},
  5982  									Purposes: map[string]pb.BuildInfra_Buildbucket_Agent_Purpose{
  5983  										"kitchen-checkout": pb.BuildInfra_Buildbucket_Agent_PURPOSE_EXE_PAYLOAD,
  5984  									},
  5985  								},
  5986  								BuildNumber: true,
  5987  							},
  5988  							Logdog: &pb.BuildInfra_LogDog{
  5989  								Project: "project",
  5990  							},
  5991  							Resultdb: &pb.BuildInfra_ResultDB{
  5992  								Hostname: "rdbHost",
  5993  							},
  5994  							Swarming: &pb.BuildInfra_Swarming{
  5995  								Caches: []*pb.BuildInfra_Swarming_CacheEntry{
  5996  									{
  5997  										Name: "builder_1809c38861a9996b1748e4640234fbd089992359f6f23f62f68deb98528f5f2b_v2",
  5998  										Path: "builder",
  5999  										WaitForWarmCache: &durationpb.Duration{
  6000  											Seconds: 240,
  6001  										},
  6002  									},
  6003  								},
  6004  								Priority: 30,
  6005  							},
  6006  						},
  6007  						Exe:               &pb.Executable{Cmd: []string{"recipes"}},
  6008  						SchedulingTimeout: &durationpb.Duration{Seconds: 21600},
  6009  						ExecutionTimeout:  &durationpb.Duration{Seconds: 10800},
  6010  						GracePeriod:       &durationpb.Duration{Seconds: 30},
  6011  					})
  6012  					So(sch.Tasks(), ShouldBeEmpty)
  6013  				})
  6014  
  6015  				Convey("request ID", func() {
  6016  					req := &pb.ScheduleBuildRequest{
  6017  						Builder: &pb.BuilderID{
  6018  							Project: "project",
  6019  							Bucket:  "bucket",
  6020  							Builder: "builder",
  6021  						},
  6022  						RequestId: "id",
  6023  					}
  6024  					testutil.PutBuilder(ctx, "project", "bucket", "builder", "")
  6025  
  6026  					Convey("deduplication", func() {
  6027  						So(datastore.Put(ctx, &model.RequestID{
  6028  							ID:      "6d03f5c780125e74ac6cb0f25c5e0b6467ff96c96d98bfb41ba382863ba7707a",
  6029  							BuildID: 1,
  6030  						}), ShouldBeNil)
  6031  
  6032  						Convey("not found", func() {
  6033  							rsp, err := srv.ScheduleBuild(ctx, req)
  6034  							So(err, ShouldErrLike, "no such entity")
  6035  							So(rsp, ShouldBeNil)
  6036  							So(sch.Tasks(), ShouldBeEmpty)
  6037  						})
  6038  
  6039  						Convey("ok", func() {
  6040  							So(datastore.Put(ctx, &model.Build{
  6041  								ID: 1,
  6042  								Proto: &pb.Build{
  6043  									Builder: &pb.BuilderID{
  6044  										Project: "project",
  6045  										Bucket:  "bucket",
  6046  										Builder: "builder",
  6047  									},
  6048  									Id: 1,
  6049  								},
  6050  							}), ShouldBeNil)
  6051  
  6052  							rsp, err := srv.ScheduleBuild(ctx, req)
  6053  							So(err, ShouldBeNil)
  6054  							So(rsp, ShouldResembleProto, &pb.Build{
  6055  								Builder: &pb.BuilderID{
  6056  									Project: "project",
  6057  									Bucket:  "bucket",
  6058  									Builder: "builder",
  6059  								},
  6060  								Id:    1,
  6061  								Input: &pb.Build_Input{},
  6062  							})
  6063  							So(sch.Tasks(), ShouldBeEmpty)
  6064  						})
  6065  					})
  6066  
  6067  					Convey("ok", func() {
  6068  						rsp, err := srv.ScheduleBuild(ctx, req)
  6069  						So(err, ShouldBeNil)
  6070  						So(rsp, ShouldResembleProto, &pb.Build{
  6071  							Builder: &pb.BuilderID{
  6072  								Project: "project",
  6073  								Bucket:  "bucket",
  6074  								Builder: "builder",
  6075  							},
  6076  							CreatedBy:  string(userID),
  6077  							CreateTime: timestamppb.New(testclock.TestRecentTimeUTC),
  6078  							UpdateTime: timestamppb.New(testclock.TestRecentTimeUTC),
  6079  							Id:         9021868963221667745,
  6080  							Input:      &pb.Build_Input{},
  6081  							Status:     pb.Status_SCHEDULED,
  6082  						})
  6083  						So(sch.Tasks(), ShouldHaveLength, 3)
  6084  
  6085  						r := &model.RequestID{
  6086  							ID: "6d03f5c780125e74ac6cb0f25c5e0b6467ff96c96d98bfb41ba382863ba7707a",
  6087  						}
  6088  						So(datastore.Get(ctx, r), ShouldBeNil)
  6089  						So(r, ShouldResemble, &model.RequestID{
  6090  							ID:         "6d03f5c780125e74ac6cb0f25c5e0b6467ff96c96d98bfb41ba382863ba7707a",
  6091  							BuildID:    9021868963221667745,
  6092  							CreatedBy:  userID,
  6093  							CreateTime: datastore.RoundTime(testclock.TestRecentTimeUTC),
  6094  							RequestID:  "id",
  6095  						})
  6096  					})
  6097  
  6098  					Convey("builder description", func() {
  6099  						So(datastore.Put(ctx, &model.Builder{
  6100  							Parent: model.BucketKey(ctx, "project", "bucket"),
  6101  							ID:     "builder",
  6102  							Config: &pb.BuilderConfig{
  6103  								BuildNumbers:    pb.Toggle_YES,
  6104  								Name:            "builder",
  6105  								SwarmingHost:    "host",
  6106  								DescriptionHtml: "test builder description",
  6107  							},
  6108  						}), ShouldBeNil)
  6109  						req.Mask = &pb.BuildMask{
  6110  							Fields: &fieldmaskpb.FieldMask{
  6111  								Paths: []string{
  6112  									"builder_info",
  6113  								},
  6114  							},
  6115  						}
  6116  						rsp, err := srv.ScheduleBuild(ctx, req)
  6117  						So(err, ShouldBeNil)
  6118  						So(rsp.BuilderInfo.Description, ShouldEqual, "test builder description")
  6119  					})
  6120  				})
  6121  			})
  6122  		})
  6123  
  6124  		Convey("template build ID", func() {
  6125  			Convey("not found", func() {
  6126  				req := &pb.ScheduleBuildRequest{
  6127  					TemplateBuildId: 1000,
  6128  				}
  6129  				rsp, err := srv.ScheduleBuild(ctx, req)
  6130  				So(err, ShouldErrLike, "not found")
  6131  				So(rsp, ShouldBeNil)
  6132  				So(sch.Tasks(), ShouldBeEmpty)
  6133  			})
  6134  
  6135  			Convey("permission denied", func() {
  6136  				So(datastore.Put(ctx, &model.Build{
  6137  					ID: 1000,
  6138  					Proto: &pb.Build{
  6139  						Id: 1000,
  6140  						Builder: &pb.BuilderID{
  6141  							Project: "project",
  6142  							Bucket:  "bucket",
  6143  							Builder: "builder",
  6144  						},
  6145  					},
  6146  				}), ShouldBeNil)
  6147  				req := &pb.ScheduleBuildRequest{
  6148  					TemplateBuildId: 1,
  6149  				}
  6150  				rsp, err := srv.ScheduleBuild(ctx, req)
  6151  				So(err, ShouldErrLike, "not found")
  6152  				So(rsp, ShouldBeNil)
  6153  				So(sch.Tasks(), ShouldBeEmpty)
  6154  			})
  6155  
  6156  			Convey("not retriable", func() {
  6157  				ctx = auth.WithState(ctx, &authtest.FakeState{
  6158  					Identity: userID,
  6159  					FakeDB: authtest.NewFakeDB(
  6160  						authtest.MockPermission(userID, "project:bucket", bbperms.BuildsGet),
  6161  						authtest.MockPermission(userID, "project:bucket", bbperms.BuildsAdd),
  6162  					),
  6163  				})
  6164  				testutil.PutBucket(ctx, "project", "bucket", &pb.Bucket{
  6165  					Name:     "bucket",
  6166  					Swarming: &pb.Swarming{},
  6167  				})
  6168  				So(datastore.Put(ctx, &model.Build{
  6169  					ID: 1000,
  6170  					Proto: &pb.Build{
  6171  						Id: 1000,
  6172  						Builder: &pb.BuilderID{
  6173  							Project: "project",
  6174  							Bucket:  "bucket",
  6175  							Builder: "builder",
  6176  						},
  6177  						Retriable: pb.Trinary_NO,
  6178  					},
  6179  				}), ShouldBeNil)
  6180  				So(datastore.Put(ctx, &model.Builder{
  6181  					Parent: model.BucketKey(ctx, "project", "bucket"),
  6182  					ID:     "builder",
  6183  					Config: &pb.BuilderConfig{
  6184  						BuildNumbers: pb.Toggle_YES,
  6185  						Name:         "builder",
  6186  						SwarmingHost: "host",
  6187  					},
  6188  				}), ShouldBeNil)
  6189  				req := &pb.ScheduleBuildRequest{
  6190  					TemplateBuildId: 1000,
  6191  				}
  6192  
  6193  				rsp, err := srv.ScheduleBuild(ctx, req)
  6194  				So(err, ShouldErrLike, "build 1000 is not retriable")
  6195  				So(rsp, ShouldBeNil)
  6196  				So(sch.Tasks(), ShouldBeEmpty)
  6197  			})
  6198  
  6199  			Convey("ok", func() {
  6200  				ctx = auth.WithState(ctx, &authtest.FakeState{
  6201  					Identity: userID,
  6202  					FakeDB: authtest.NewFakeDB(
  6203  						authtest.MockPermission(userID, "project:bucket", bbperms.BuildsGet),
  6204  						authtest.MockPermission(userID, "project:bucket", bbperms.BuildsAdd),
  6205  					),
  6206  				})
  6207  				testutil.PutBucket(ctx, "project", "bucket", &pb.Bucket{
  6208  					Name:     "bucket",
  6209  					Swarming: &pb.Swarming{},
  6210  				})
  6211  				So(datastore.Put(ctx, &model.Build{
  6212  					ID: 1000,
  6213  					Proto: &pb.Build{
  6214  						Id: 1000,
  6215  						Builder: &pb.BuilderID{
  6216  							Project: "project",
  6217  							Bucket:  "bucket",
  6218  							Builder: "builder",
  6219  						},
  6220  					},
  6221  				}), ShouldBeNil)
  6222  				req := &pb.ScheduleBuildRequest{
  6223  					TemplateBuildId: 1000,
  6224  				}
  6225  
  6226  				Convey("not found", func() {
  6227  					rsp, err := srv.ScheduleBuild(ctx, req)
  6228  					So(err, ShouldErrLike, "error fetching builders")
  6229  					So(rsp, ShouldBeNil)
  6230  					So(sch.Tasks(), ShouldBeEmpty)
  6231  				})
  6232  
  6233  				Convey("ok", func() {
  6234  					So(datastore.Put(ctx, &model.Builder{
  6235  						Parent: model.BucketKey(ctx, "project", "bucket"),
  6236  						ID:     "builder",
  6237  						Config: &pb.BuilderConfig{
  6238  							BuildNumbers: pb.Toggle_YES,
  6239  							Name:         "builder",
  6240  							SwarmingHost: "host",
  6241  						},
  6242  					}), ShouldBeNil)
  6243  					rsp, err := srv.ScheduleBuild(ctx, req)
  6244  					So(err, ShouldBeNil)
  6245  					So(rsp, ShouldResembleProto, &pb.Build{
  6246  						Builder: &pb.BuilderID{
  6247  							Project: "project",
  6248  							Bucket:  "bucket",
  6249  							Builder: "builder",
  6250  						},
  6251  						CreatedBy:  string(userID),
  6252  						CreateTime: timestamppb.New(testclock.TestRecentTimeUTC),
  6253  						UpdateTime: timestamppb.New(testclock.TestRecentTimeUTC),
  6254  						Id:         9021868963221667745,
  6255  						Input:      &pb.Build_Input{},
  6256  						Number:     1,
  6257  						Status:     pb.Status_SCHEDULED,
  6258  					})
  6259  					So(sch.Tasks(), ShouldHaveLength, 3)
  6260  				})
  6261  			})
  6262  		})
  6263  	})
  6264  
  6265  	Convey("scheduleBuilds", t, func() {
  6266  		srv := &Builds{}
  6267  		ctx := txndefer.FilterRDS(memory.Use(context.Background()))
  6268  		ctx = metrics.WithServiceInfo(ctx, "svc", "job", "ins")
  6269  		ctx = mathrand.Set(ctx, rand.New(rand.NewSource(0)))
  6270  		ctx, _ = testclock.UseTime(ctx, testclock.TestRecentTimeUTC)
  6271  		ctx, sch := tq.TestingContext(ctx, nil)
  6272  		datastore.GetTestable(ctx).AutoIndex(true)
  6273  		datastore.GetTestable(ctx).Consistent(true)
  6274  		ctx = auth.WithState(ctx, &authtest.FakeState{
  6275  			Identity: userID,
  6276  			FakeDB: authtest.NewFakeDB(
  6277  				authtest.MockPermission(userID, "project:bucket", bbperms.BuildsGet),
  6278  				authtest.MockPermission(userID, "project:bucket", bbperms.BuildsAdd),
  6279  			),
  6280  		})
  6281  		globalCfg := &pb.SettingsCfg{
  6282  			Resultdb: &pb.ResultDBSettings{
  6283  				Hostname: "rdbHost",
  6284  			},
  6285  			Swarming: &pb.SwarmingSettings{
  6286  				BbagentPackage: &pb.SwarmingSettings_Package{
  6287  					PackageName: "bbagent",
  6288  					Version:     "bbagent-version",
  6289  				},
  6290  				KitchenPackage: &pb.SwarmingSettings_Package{
  6291  					PackageName: "kitchen",
  6292  					Version:     "kitchen-version",
  6293  				},
  6294  			},
  6295  		}
  6296  
  6297  		testutil.PutBucket(ctx, "project", "bucket", &pb.Bucket{
  6298  			Name:     "bucket",
  6299  			Swarming: &pb.Swarming{},
  6300  		})
  6301  		So(datastore.Put(ctx, &model.Build{
  6302  			ID: 1000,
  6303  			Proto: &pb.Build{
  6304  				Id: 1000,
  6305  				Builder: &pb.BuilderID{
  6306  					Project: "project",
  6307  					Bucket:  "bucket",
  6308  					Builder: "builder",
  6309  				},
  6310  			},
  6311  		}), ShouldBeNil)
  6312  		So(datastore.Put(ctx, &model.Builder{
  6313  			Parent: model.BucketKey(ctx, "project", "bucket"),
  6314  			ID:     "builder",
  6315  			Config: &pb.BuilderConfig{
  6316  				BuildNumbers: pb.Toggle_YES,
  6317  				Name:         "builder",
  6318  				SwarmingHost: "host",
  6319  			},
  6320  		}), ShouldBeNil)
  6321  
  6322  		Convey("one", func() {
  6323  			reqs := []*pb.ScheduleBuildRequest{
  6324  				{
  6325  					TemplateBuildId: 1000,
  6326  					Tags: []*pb.StringPair{
  6327  						{
  6328  							Key:   "buildset",
  6329  							Value: "buildset",
  6330  						},
  6331  					},
  6332  				},
  6333  			}
  6334  
  6335  			rsp, merr := srv.scheduleBuilds(ctx, globalCfg, reqs)
  6336  			So(merr, ShouldBeNil)
  6337  			So(rsp, ShouldHaveLength, 1)
  6338  			So(rsp[0], ShouldResembleProto, &pb.Build{
  6339  				Builder: &pb.BuilderID{
  6340  					Project: "project",
  6341  					Bucket:  "bucket",
  6342  					Builder: "builder",
  6343  				},
  6344  				CreatedBy:  string(userID),
  6345  				CreateTime: timestamppb.New(testclock.TestRecentTimeUTC),
  6346  				UpdateTime: timestamppb.New(testclock.TestRecentTimeUTC),
  6347  				Id:         9021868963221667745,
  6348  				Input:      &pb.Build_Input{},
  6349  				Number:     1,
  6350  				Status:     pb.Status_SCHEDULED,
  6351  			})
  6352  			So(sch.Tasks(), ShouldHaveLength, 3)
  6353  
  6354  			ind, err := model.SearchTagIndex(ctx, "buildset", "buildset")
  6355  			So(err, ShouldBeNil)
  6356  			So(ind, ShouldResemble, []*model.TagIndexEntry{
  6357  				{
  6358  					BuildID:     9021868963221667745,
  6359  					BucketID:    "project/bucket",
  6360  					CreatedTime: datastore.RoundTime(testclock.TestRecentTimeUTC),
  6361  				},
  6362  			})
  6363  		})
  6364  
  6365  		Convey("many", func() {
  6366  			Convey("one of TemplateBuildId builds not found", func() {
  6367  				reqs := []*pb.ScheduleBuildRequest{
  6368  					{
  6369  						TemplateBuildId: 1000,
  6370  						Tags: []*pb.StringPair{
  6371  							{
  6372  								Key:   "buildset",
  6373  								Value: "buildset",
  6374  							},
  6375  						},
  6376  					},
  6377  					{
  6378  						TemplateBuildId: 1001,
  6379  						Tags: []*pb.StringPair{
  6380  							{
  6381  								Key:   "buildset",
  6382  								Value: "buildset",
  6383  							},
  6384  						},
  6385  					},
  6386  					{
  6387  						TemplateBuildId: 1000,
  6388  						Tags: []*pb.StringPair{
  6389  							{
  6390  								Key:   "buildset",
  6391  								Value: "buildset",
  6392  							},
  6393  						},
  6394  					},
  6395  				}
  6396  
  6397  				rsp, err := srv.scheduleBuilds(ctx, globalCfg, reqs)
  6398  				So(err, ShouldNotBeNil)
  6399  				So(err[0], ShouldBeNil)
  6400  				So(err[1], ShouldErrLike, `requested resource not found or "user:caller@example.com" does not have permission to view it`)
  6401  				So(err[2], ShouldBeNil)
  6402  				So(rsp, ShouldResembleProto, []*pb.Build{
  6403  					{
  6404  						Id:         9021868963222163313,
  6405  						Builder:    &pb.BuilderID{Project: "project", Bucket: "bucket", Builder: "builder"},
  6406  						Number:     1,
  6407  						CreatedBy:  string(userID),
  6408  						Status:     pb.Status_SCHEDULED,
  6409  						CreateTime: timestamppb.New(testclock.TestRecentTimeUTC),
  6410  						UpdateTime: timestamppb.New(testclock.TestRecentTimeUTC),
  6411  						Input:      &pb.Build_Input{},
  6412  					},
  6413  					nil,
  6414  					{
  6415  						Id:         9021868963222163297,
  6416  						Builder:    &pb.BuilderID{Project: "project", Bucket: "bucket", Builder: "builder"},
  6417  						Number:     2,
  6418  						CreatedBy:  string(userID),
  6419  						Status:     pb.Status_SCHEDULED,
  6420  						CreateTime: timestamppb.New(testclock.TestRecentTimeUTC),
  6421  						UpdateTime: timestamppb.New(testclock.TestRecentTimeUTC),
  6422  						Input:      &pb.Build_Input{},
  6423  					},
  6424  				})
  6425  			})
  6426  
  6427  			Convey("one of builds missing builderCfg", func() {
  6428  				So(datastore.Put(ctx, &model.Build{
  6429  					ID: 1010,
  6430  					Proto: &pb.Build{
  6431  						Id: 1010,
  6432  						Builder: &pb.BuilderID{
  6433  							Project: "project",
  6434  							Bucket:  "bucket",
  6435  							Builder: "miss_builder_cfg",
  6436  						},
  6437  					},
  6438  				}), ShouldBeNil)
  6439  
  6440  				reqs := []*pb.ScheduleBuildRequest{
  6441  					{
  6442  						TemplateBuildId: 1000,
  6443  						Tags: []*pb.StringPair{
  6444  							{
  6445  								Key:   "buildset",
  6446  								Value: "buildset",
  6447  							},
  6448  						},
  6449  					},
  6450  					{
  6451  						TemplateBuildId: 1010,
  6452  					},
  6453  					{
  6454  						TemplateBuildId: 1000,
  6455  						Tags: []*pb.StringPair{
  6456  							{
  6457  								Key:   "buildset",
  6458  								Value: "buildset",
  6459  							},
  6460  						},
  6461  					},
  6462  				}
  6463  
  6464  				rsp, err := srv.scheduleBuilds(ctx, globalCfg, reqs)
  6465  				So(err, ShouldNotBeNil)
  6466  				So(err[0], ShouldBeNil)
  6467  				So(err[1], ShouldErrLike, `builder not found: "miss_builder_cfg"`)
  6468  				So(err[2], ShouldBeNil)
  6469  				So(rsp, ShouldResembleProto, []*pb.Build{
  6470  					{
  6471  						Id:         9021868963222163313,
  6472  						Builder:    &pb.BuilderID{Project: "project", Bucket: "bucket", Builder: "builder"},
  6473  						Number:     1,
  6474  						CreatedBy:  string(userID),
  6475  						Status:     pb.Status_SCHEDULED,
  6476  						CreateTime: timestamppb.New(testclock.TestRecentTimeUTC),
  6477  						UpdateTime: timestamppb.New(testclock.TestRecentTimeUTC),
  6478  						Input:      &pb.Build_Input{},
  6479  					},
  6480  					nil,
  6481  					{
  6482  						Id:         9021868963222163297,
  6483  						Builder:    &pb.BuilderID{Project: "project", Bucket: "bucket", Builder: "builder"},
  6484  						Number:     2,
  6485  						CreatedBy:  string(userID),
  6486  						Status:     pb.Status_SCHEDULED,
  6487  						CreateTime: timestamppb.New(testclock.TestRecentTimeUTC),
  6488  						UpdateTime: timestamppb.New(testclock.TestRecentTimeUTC),
  6489  						Input:      &pb.Build_Input{},
  6490  					},
  6491  				})
  6492  
  6493  			})
  6494  
  6495  			Convey("one of builds failed in `createBuilds` part", func() {
  6496  				So(datastore.Put(ctx, &model.Build{
  6497  					ID: 1011,
  6498  					Proto: &pb.Build{
  6499  						Id: 1011,
  6500  						Builder: &pb.BuilderID{
  6501  							Project: "project",
  6502  							Bucket:  "bucket",
  6503  							Builder: "builder_with_rdb",
  6504  						},
  6505  					},
  6506  				}), ShouldBeNil)
  6507  				bqExports := []*rdbPb.BigQueryExport{}
  6508  				So(datastore.Put(ctx, &model.Builder{
  6509  					Parent: model.BucketKey(ctx, "project", "bucket"),
  6510  					ID:     "builder_with_rdb",
  6511  					Config: &pb.BuilderConfig{
  6512  						BuildNumbers: pb.Toggle_YES,
  6513  						Name:         "builder_with_rdb",
  6514  						SwarmingHost: "host",
  6515  						Resultdb: &pb.BuilderConfig_ResultDB{
  6516  							Enable:    true,
  6517  							BqExports: bqExports,
  6518  						},
  6519  					},
  6520  				}), ShouldBeNil)
  6521  
  6522  				ctl := gomock.NewController(t)
  6523  				defer ctl.Finish()
  6524  				mockRdbClient := rdbPb.NewMockRecorderClient(ctl)
  6525  				ctx = resultdb.SetMockRecorder(ctx, mockRdbClient)
  6526  				deadline := testclock.TestRecentTimeUTC.Add(time.Second * 10800).Add(time.Second * 21600)
  6527  				ctx, _ = testclock.UseTime(ctx, testclock.TestRecentTimeUTC)
  6528  				mockRdbClient.EXPECT().CreateInvocation(gomock.Any(), luciCmProto.MatcherEqual(
  6529  					&rdbPb.CreateInvocationRequest{
  6530  						InvocationId: "build-9021868963221610321",
  6531  						Invocation: &rdbPb.Invocation{
  6532  							BigqueryExports:  bqExports,
  6533  							ProducerResource: "//app.appspot.com/builds/9021868963221610321",
  6534  							Realm:            "project:bucket",
  6535  							Deadline:         timestamppb.New(deadline),
  6536  						},
  6537  						RequestId: "build-9021868963221610321",
  6538  					}), gomock.Any()).Return(nil, grpcStatus.Error(codes.Internal, "internal error"))
  6539  
  6540  				reqs := []*pb.ScheduleBuildRequest{
  6541  					{
  6542  						TemplateBuildId: 1000,
  6543  						Tags: []*pb.StringPair{
  6544  							{
  6545  								Key:   "buildset",
  6546  								Value: "buildset",
  6547  							},
  6548  						},
  6549  					},
  6550  					{
  6551  						TemplateBuildId: 1011,
  6552  						Tags: []*pb.StringPair{
  6553  							{
  6554  								Key:   "buildset",
  6555  								Value: "buildset",
  6556  							},
  6557  						},
  6558  					},
  6559  					{
  6560  						TemplateBuildId: 1000,
  6561  						Tags: []*pb.StringPair{
  6562  							{
  6563  								Key:   "buildset",
  6564  								Value: "buildset",
  6565  							},
  6566  						},
  6567  					},
  6568  				}
  6569  
  6570  				rsp, err := srv.scheduleBuilds(ctx, globalCfg, reqs)
  6571  				So(err, ShouldNotBeNil)
  6572  				So(err[0], ShouldBeNil)
  6573  				So(err[1], ShouldErrLike, "failed to create the invocation for build id: 9021868963221610321: rpc error: code = Internal desc = internal error")
  6574  				So(err[2], ShouldBeNil)
  6575  				So(rsp, ShouldResembleProto, []*pb.Build{
  6576  					{
  6577  						Id:         9021868963221610337,
  6578  						Builder:    &pb.BuilderID{Project: "project", Bucket: "bucket", Builder: "builder"},
  6579  						Number:     1,
  6580  						CreatedBy:  string(userID),
  6581  						Status:     pb.Status_SCHEDULED,
  6582  						CreateTime: timestamppb.New(testclock.TestRecentTimeUTC),
  6583  						UpdateTime: timestamppb.New(testclock.TestRecentTimeUTC),
  6584  						Input:      &pb.Build_Input{},
  6585  					},
  6586  					nil,
  6587  					{
  6588  						Id:         9021868963221610305,
  6589  						Builder:    &pb.BuilderID{Project: "project", Bucket: "bucket", Builder: "builder"},
  6590  						Number:     2,
  6591  						CreatedBy:  string(userID),
  6592  						Status:     pb.Status_SCHEDULED,
  6593  						CreateTime: timestamppb.New(testclock.TestRecentTimeUTC),
  6594  						UpdateTime: timestamppb.New(testclock.TestRecentTimeUTC),
  6595  						Input:      &pb.Build_Input{},
  6596  					},
  6597  				})
  6598  			})
  6599  
  6600  			Convey("ok", func() {
  6601  				reqs := []*pb.ScheduleBuildRequest{
  6602  					{
  6603  						TemplateBuildId: 1000,
  6604  						Tags: []*pb.StringPair{
  6605  							{
  6606  								Key:   "buildset",
  6607  								Value: "buildset",
  6608  							},
  6609  						},
  6610  					},
  6611  					{
  6612  						TemplateBuildId: 1000,
  6613  						Tags: []*pb.StringPair{
  6614  							{
  6615  								Key:   "buildset",
  6616  								Value: "buildset",
  6617  							},
  6618  						},
  6619  					},
  6620  				}
  6621  
  6622  				rsp, merr := srv.scheduleBuilds(ctx, globalCfg, reqs)
  6623  				So(merr, ShouldBeNil)
  6624  				So(rsp, ShouldHaveLength, 2)
  6625  				So(rsp[0], ShouldResembleProto, &pb.Build{
  6626  					Builder: &pb.BuilderID{
  6627  						Project: "project",
  6628  						Bucket:  "bucket",
  6629  						Builder: "builder",
  6630  					},
  6631  					CreatedBy:  string(userID),
  6632  					UpdateTime: timestamppb.New(testclock.TestRecentTimeUTC),
  6633  					CreateTime: timestamppb.New(testclock.TestRecentTimeUTC),
  6634  					Id:         9021868963222163313,
  6635  					Input:      &pb.Build_Input{},
  6636  					Number:     1,
  6637  					Status:     pb.Status_SCHEDULED,
  6638  				})
  6639  				So(rsp[1], ShouldResembleProto, &pb.Build{
  6640  					Builder: &pb.BuilderID{
  6641  						Project: "project",
  6642  						Bucket:  "bucket",
  6643  						Builder: "builder",
  6644  					},
  6645  					CreatedBy:  string(userID),
  6646  					UpdateTime: timestamppb.New(testclock.TestRecentTimeUTC),
  6647  					CreateTime: timestamppb.New(testclock.TestRecentTimeUTC),
  6648  					Id:         9021868963222163297,
  6649  					Input:      &pb.Build_Input{},
  6650  					Number:     2,
  6651  					Status:     pb.Status_SCHEDULED,
  6652  				})
  6653  				So(sch.Tasks(), ShouldHaveLength, 6)
  6654  
  6655  				ind, err := model.SearchTagIndex(ctx, "buildset", "buildset")
  6656  				So(err, ShouldBeNil)
  6657  				So(ind, ShouldResemble, []*model.TagIndexEntry{
  6658  					{
  6659  						BuildID:     9021868963222163313,
  6660  						BucketID:    "project/bucket",
  6661  						CreatedTime: datastore.RoundTime(testclock.TestRecentTimeUTC),
  6662  					},
  6663  					{
  6664  						BuildID:     9021868963222163297,
  6665  						BucketID:    "project/bucket",
  6666  						CreatedTime: datastore.RoundTime(testclock.TestRecentTimeUTC),
  6667  					},
  6668  				})
  6669  			})
  6670  		})
  6671  
  6672  		Convey("schedule in shadow", func() {
  6673  			testutil.PutBucket(ctx, "project", "bucket", &pb.Bucket{Swarming: &pb.Swarming{}, Shadow: "bucket.shadow"})
  6674  			testutil.PutBucket(ctx, "project", "bucket.shadow", &pb.Bucket{DynamicBuilderTemplate: &pb.Bucket_DynamicBuilderTemplate{}})
  6675  			So(datastore.Put(ctx, &model.Builder{
  6676  				Parent: model.BucketKey(ctx, "project", "bucket"),
  6677  				ID:     "builder",
  6678  				Config: &pb.BuilderConfig{
  6679  					Name:           "builder",
  6680  					ServiceAccount: "sa@chops-service-accounts.iam.gserviceaccount.com",
  6681  					Dimensions:     []string{"pool:pool1"},
  6682  					Properties:     `{"a":"b","b":"b"}`,
  6683  					ShadowBuilderAdjustments: &pb.BuilderConfig_ShadowBuilderAdjustments{
  6684  						ServiceAccount: "shadow@chops-service-accounts.iam.gserviceaccount.com",
  6685  						Pool:           "pool2",
  6686  						Properties:     `{"a":"b2","c":"c"}`,
  6687  						Dimensions: []string{
  6688  							"pool:pool2",
  6689  						},
  6690  					},
  6691  				},
  6692  			}), ShouldBeNil)
  6693  			Convey("no permission", func() {
  6694  				ctx = auth.WithState(ctx, &authtest.FakeState{
  6695  					Identity: userID,
  6696  					FakeDB: authtest.NewFakeDB(
  6697  						authtest.MockPermission(userID, "project:bucket", bbperms.BuildsGet),
  6698  						authtest.MockPermission(userID, "project:bucket", bbperms.BuildsAdd),
  6699  						authtest.MockPermission(userID, "project:bucket.shadow", bbperms.BuildersGet),
  6700  					),
  6701  				})
  6702  				reqs := []*pb.ScheduleBuildRequest{
  6703  					{
  6704  						Builder: &pb.BuilderID{
  6705  							Project: "project",
  6706  							Bucket:  "bucket",
  6707  							Builder: "builder",
  6708  						},
  6709  						ShadowInput: &pb.ScheduleBuildRequest_ShadowInput{},
  6710  					},
  6711  				}
  6712  				_, err := srv.scheduleBuilds(ctx, globalCfg, reqs)
  6713  				So(err, ShouldErrLike, `does not have permission "buildbucket.builds.add"`)
  6714  			})
  6715  
  6716  			Convey("one shadow, one original, and one with no shadow bucket", func() {
  6717  				ctx = auth.WithState(ctx, &authtest.FakeState{
  6718  					Identity: userID,
  6719  					FakeDB: authtest.NewFakeDB(
  6720  						authtest.MockPermission(userID, "project:bucket", bbperms.BuildsGet),
  6721  						authtest.MockPermission(userID, "project:bucket", bbperms.BuildsAdd),
  6722  						authtest.MockPermission(userID, "project:bucket.shadow", bbperms.BuildsGet),
  6723  						authtest.MockPermission(userID, "project:bucket.shadow", bbperms.BuildsAdd),
  6724  					),
  6725  				})
  6726  				reqs := []*pb.ScheduleBuildRequest{
  6727  					{
  6728  						Builder: &pb.BuilderID{
  6729  							Project: "project",
  6730  							Bucket:  "bucket",
  6731  							Builder: "builder",
  6732  						},
  6733  						ShadowInput: &pb.ScheduleBuildRequest_ShadowInput{},
  6734  					},
  6735  					{
  6736  						Builder: &pb.BuilderID{
  6737  							Project: "project",
  6738  							Bucket:  "bucket",
  6739  							Builder: "builder",
  6740  						},
  6741  					},
  6742  					{
  6743  						Builder: &pb.BuilderID{
  6744  							Project: "project",
  6745  							Bucket:  "bucket.shadow",
  6746  							Builder: "builder",
  6747  						},
  6748  						ShadowInput: &pb.ScheduleBuildRequest_ShadowInput{},
  6749  					},
  6750  				}
  6751  				blds, err := srv.scheduleBuilds(ctx, globalCfg, reqs)
  6752  				So(err, ShouldNotBeNil)
  6753  				So(err, ShouldErrLike, "scheduling a shadow build in the original bucket is not allowed")
  6754  				So(len(blds), ShouldEqual, 3)
  6755  				So(blds[2], ShouldBeNil)
  6756  			})
  6757  		})
  6758  
  6759  	})
  6760  
  6761  	Convey("structContains", t, func() {
  6762  		Convey("nil", func() {
  6763  			So(structContains(nil, nil), ShouldBeTrue)
  6764  		})
  6765  
  6766  		Convey("nil struct", func() {
  6767  			path := []string{"path"}
  6768  			So(structContains(nil, path), ShouldBeFalse)
  6769  		})
  6770  
  6771  		Convey("nil path", func() {
  6772  			s := &structpb.Struct{}
  6773  			So(structContains(s, nil), ShouldBeTrue)
  6774  		})
  6775  
  6776  		Convey("one component", func() {
  6777  			s := &structpb.Struct{
  6778  				Fields: map[string]*structpb.Value{
  6779  					"key": {
  6780  						Kind: &structpb.Value_StringValue{
  6781  							StringValue: "value",
  6782  						},
  6783  					},
  6784  				},
  6785  			}
  6786  			path := []string{"key"}
  6787  			So(structContains(s, path), ShouldBeTrue)
  6788  		})
  6789  
  6790  		Convey("many components", func() {
  6791  			s := &structpb.Struct{
  6792  				Fields: map[string]*structpb.Value{
  6793  					"key1": {
  6794  						Kind: &structpb.Value_StructValue{
  6795  							StructValue: &structpb.Struct{
  6796  								Fields: map[string]*structpb.Value{
  6797  									"key2": {
  6798  										Kind: &structpb.Value_StructValue{
  6799  											StructValue: &structpb.Struct{
  6800  												Fields: map[string]*structpb.Value{
  6801  													"key3": {
  6802  														Kind: &structpb.Value_StringValue{
  6803  															StringValue: "value",
  6804  														},
  6805  													},
  6806  												},
  6807  											},
  6808  										},
  6809  									},
  6810  								},
  6811  							},
  6812  						},
  6813  					},
  6814  				},
  6815  			}
  6816  			path := []string{"key1", "key2", "key3"}
  6817  			So(structContains(s, path), ShouldBeTrue)
  6818  		})
  6819  
  6820  		Convey("excess component", func() {
  6821  			s := &structpb.Struct{
  6822  				Fields: map[string]*structpb.Value{
  6823  					"key1": {
  6824  						Kind: &structpb.Value_StructValue{
  6825  							StructValue: &structpb.Struct{
  6826  								Fields: map[string]*structpb.Value{
  6827  									"key2": {
  6828  										Kind: &structpb.Value_StringValue{
  6829  											StringValue: "value",
  6830  										},
  6831  									},
  6832  								},
  6833  							},
  6834  						},
  6835  					},
  6836  				},
  6837  			}
  6838  			path := []string{"key1"}
  6839  			So(structContains(s, path), ShouldBeTrue)
  6840  		})
  6841  	})
  6842  
  6843  	Convey("validateSchedule", t, func() {
  6844  		ctx := memory.Use(context.Background())
  6845  		ctx = installTestSecret(ctx)
  6846  
  6847  		Convey("nil", func() {
  6848  			err := validateSchedule(ctx, nil, nil, nil)
  6849  			So(err, ShouldErrLike, "builder or template_build_id is required")
  6850  		})
  6851  
  6852  		Convey("empty", func() {
  6853  			req := &pb.ScheduleBuildRequest{}
  6854  			err := validateSchedule(ctx, req, nil, nil)
  6855  			So(err, ShouldErrLike, "builder or template_build_id is required")
  6856  		})
  6857  
  6858  		Convey("request ID", func() {
  6859  			req := &pb.ScheduleBuildRequest{
  6860  				RequestId:       "request/id",
  6861  				TemplateBuildId: 1,
  6862  			}
  6863  			err := validateSchedule(ctx, req, nil, nil)
  6864  			So(err, ShouldErrLike, "request_id cannot contain")
  6865  		})
  6866  
  6867  		Convey("builder ID", func() {
  6868  			req := &pb.ScheduleBuildRequest{
  6869  				Builder: &pb.BuilderID{},
  6870  			}
  6871  			err := validateSchedule(ctx, req, nil, nil)
  6872  			So(err, ShouldErrLike, "project must match")
  6873  		})
  6874  
  6875  		Convey("dimensions", func() {
  6876  			Convey("empty", func() {
  6877  				req := &pb.ScheduleBuildRequest{
  6878  					Dimensions: []*pb.RequestedDimension{
  6879  						{},
  6880  					},
  6881  					TemplateBuildId: 1,
  6882  				}
  6883  				err := validateSchedule(ctx, req, nil, nil)
  6884  				So(err, ShouldErrLike, "dimensions")
  6885  			})
  6886  
  6887  			Convey("expiration", func() {
  6888  				Convey("empty", func() {
  6889  					req := &pb.ScheduleBuildRequest{
  6890  						Dimensions: []*pb.RequestedDimension{
  6891  							{
  6892  								Expiration: &durationpb.Duration{},
  6893  								Key:        "key",
  6894  								Value:      "value",
  6895  							},
  6896  						},
  6897  						TemplateBuildId: 1,
  6898  					}
  6899  					err := validateSchedule(ctx, req, nil, nil)
  6900  					So(err, ShouldBeNil)
  6901  				})
  6902  
  6903  				Convey("nanos", func() {
  6904  					req := &pb.ScheduleBuildRequest{
  6905  						Dimensions: []*pb.RequestedDimension{
  6906  							{
  6907  								Expiration: &durationpb.Duration{
  6908  									Nanos: 1,
  6909  								},
  6910  								Key:   "key",
  6911  								Value: "value",
  6912  							},
  6913  						},
  6914  						TemplateBuildId: 1,
  6915  					}
  6916  					err := validateSchedule(ctx, req, nil, nil)
  6917  					So(err, ShouldErrLike, "nanos must not be specified")
  6918  				})
  6919  
  6920  				Convey("seconds", func() {
  6921  					Convey("negative", func() {
  6922  						req := &pb.ScheduleBuildRequest{
  6923  							Dimensions: []*pb.RequestedDimension{
  6924  								{
  6925  									Expiration: &durationpb.Duration{
  6926  										Seconds: -60,
  6927  									},
  6928  									Key:   "key",
  6929  									Value: "value",
  6930  								},
  6931  							},
  6932  							TemplateBuildId: 1,
  6933  						}
  6934  						err := validateSchedule(ctx, req, nil, nil)
  6935  						So(err, ShouldErrLike, "seconds must not be negative")
  6936  					})
  6937  
  6938  					Convey("whole minute", func() {
  6939  						req := &pb.ScheduleBuildRequest{
  6940  							Dimensions: []*pb.RequestedDimension{
  6941  								{
  6942  									Expiration: &durationpb.Duration{
  6943  										Seconds: 1,
  6944  									},
  6945  									Key:   "key",
  6946  									Value: "value",
  6947  								},
  6948  							},
  6949  							TemplateBuildId: 1,
  6950  						}
  6951  						err := validateSchedule(ctx, req, nil, nil)
  6952  						So(err, ShouldErrLike, "seconds must be a multiple of 60")
  6953  					})
  6954  				})
  6955  
  6956  				Convey("ok", func() {
  6957  					req := &pb.ScheduleBuildRequest{
  6958  						Dimensions: []*pb.RequestedDimension{
  6959  							{
  6960  								Expiration: &durationpb.Duration{
  6961  									Seconds: 60,
  6962  								},
  6963  								Key:   "key",
  6964  								Value: "value",
  6965  							},
  6966  						},
  6967  						TemplateBuildId: 1,
  6968  					}
  6969  					err := validateSchedule(ctx, req, nil, nil)
  6970  					So(err, ShouldBeNil)
  6971  				})
  6972  			})
  6973  
  6974  			Convey("key", func() {
  6975  				Convey("empty", func() {
  6976  					req := &pb.ScheduleBuildRequest{
  6977  						Dimensions: []*pb.RequestedDimension{
  6978  							{
  6979  								Value: "value",
  6980  							},
  6981  						},
  6982  						TemplateBuildId: 1,
  6983  					}
  6984  					err := validateSchedule(ctx, req, nil, nil)
  6985  					So(err, ShouldErrLike, "key must be specified")
  6986  				})
  6987  
  6988  				Convey("caches", func() {
  6989  					req := &pb.ScheduleBuildRequest{
  6990  						Dimensions: []*pb.RequestedDimension{
  6991  							{
  6992  								Key:   "caches",
  6993  								Value: "value",
  6994  							},
  6995  						},
  6996  						TemplateBuildId: 1,
  6997  					}
  6998  					err := validateSchedule(ctx, req, nil, nil)
  6999  					So(err, ShouldErrLike, "caches may only be specified in builder configs")
  7000  				})
  7001  
  7002  				Convey("pool", func() {
  7003  					req := &pb.ScheduleBuildRequest{
  7004  						Dimensions: []*pb.RequestedDimension{
  7005  							{
  7006  								Key:   "pool",
  7007  								Value: "value",
  7008  							},
  7009  						},
  7010  						TemplateBuildId: 1,
  7011  					}
  7012  					err := validateSchedule(ctx, req, nil, nil)
  7013  					So(err, ShouldErrLike, "pool may only be specified in builder configs")
  7014  				})
  7015  
  7016  				Convey("ok", func() {
  7017  					req := &pb.ScheduleBuildRequest{
  7018  						Dimensions: []*pb.RequestedDimension{
  7019  							{
  7020  								Key:   "key",
  7021  								Value: "value",
  7022  							},
  7023  							{
  7024  								Key: "key1",
  7025  							},
  7026  						},
  7027  						TemplateBuildId: 1,
  7028  					}
  7029  					err := validateSchedule(ctx, req, nil, nil)
  7030  					So(err, ShouldBeNil)
  7031  				})
  7032  			})
  7033  
  7034  			Convey("parent", func() {
  7035  				Convey("missing parent", func() {
  7036  					req := &pb.ScheduleBuildRequest{
  7037  						Dimensions: []*pb.RequestedDimension{
  7038  							{
  7039  								Key:   "key",
  7040  								Value: "value",
  7041  							},
  7042  						},
  7043  						TemplateBuildId:  1,
  7044  						CanOutliveParent: pb.Trinary_NO,
  7045  					}
  7046  					err := validateSchedule(ctx, req, nil, nil)
  7047  					So(err, ShouldErrLike, "can_outlive_parent is specified without parent build token")
  7048  				})
  7049  
  7050  				Convey("schedule no parent build", func() {
  7051  					req := &pb.ScheduleBuildRequest{
  7052  						Dimensions: []*pb.RequestedDimension{
  7053  							{
  7054  								Key:   "key",
  7055  								Value: "value",
  7056  							},
  7057  						},
  7058  						TemplateBuildId:  1,
  7059  						CanOutliveParent: pb.Trinary_UNSET,
  7060  					}
  7061  					err := validateSchedule(ctx, req, nil, nil)
  7062  					So(err, ShouldBeNil)
  7063  				})
  7064  
  7065  				tk, err := buildtoken.GenerateToken(ctx, 1, pb.TokenBody_BUILD)
  7066  				So(err, ShouldBeNil)
  7067  				Convey("ended parent", func() {
  7068  					testutil.PutBucket(ctx, "project", "bucket", nil)
  7069  
  7070  					So(datastore.Put(ctx, &model.Build{
  7071  						Proto: &pb.Build{
  7072  							Id: 1,
  7073  							Builder: &pb.BuilderID{
  7074  								Project: "project",
  7075  								Bucket:  "bucket",
  7076  								Builder: "builder",
  7077  							},
  7078  							Status: pb.Status_SUCCESS,
  7079  						},
  7080  						UpdateToken: tk,
  7081  					}), ShouldBeNil)
  7082  
  7083  					ctx := metadata.NewIncomingContext(ctx, metadata.Pairs(bb.BuildbucketTokenHeader, tk))
  7084  					_, err := validateParent(ctx)
  7085  					So(err, ShouldErrLike, "1 has ended, cannot add child to it")
  7086  				})
  7087  
  7088  				Convey("OK", func() {
  7089  					testutil.PutBucket(ctx, "project", "bucket", nil)
  7090  					So(datastore.Put(ctx, &model.Build{
  7091  						Proto: &pb.Build{
  7092  							Id: 1,
  7093  							Builder: &pb.BuilderID{
  7094  								Project: "project",
  7095  								Bucket:  "bucket",
  7096  								Builder: "builder",
  7097  							},
  7098  							Status: pb.Status_STARTED,
  7099  						},
  7100  						UpdateToken: tk,
  7101  					}), ShouldBeNil)
  7102  
  7103  					ctx := metadata.NewIncomingContext(ctx, metadata.Pairs(bb.BuildbucketTokenHeader, tk))
  7104  					b, err := validateParent(ctx)
  7105  					So(err, ShouldBeNil)
  7106  					So(b.Proto.Id, ShouldEqual, 1)
  7107  				})
  7108  			})
  7109  
  7110  			Convey("ok", func() {
  7111  				req := &pb.ScheduleBuildRequest{
  7112  					Dimensions: []*pb.RequestedDimension{
  7113  						{
  7114  							Key:   "key",
  7115  							Value: "value",
  7116  						},
  7117  					},
  7118  					TemplateBuildId: 1,
  7119  				}
  7120  				err := validateSchedule(ctx, req, nil, nil)
  7121  				So(err, ShouldBeNil)
  7122  			})
  7123  
  7124  			Convey("empty value & non-value", func() {
  7125  				req := &pb.ScheduleBuildRequest{
  7126  					Dimensions: []*pb.RequestedDimension{
  7127  						{
  7128  							Key:   "req_key",
  7129  							Value: "value",
  7130  						},
  7131  						{
  7132  							Key: "req_key",
  7133  						},
  7134  					},
  7135  					TemplateBuildId: 1,
  7136  				}
  7137  				err := validateSchedule(ctx, req, nil, nil)
  7138  				So(err, ShouldErrLike, `dimensions: contain both empty and non-empty value for the same key - "req_key"`)
  7139  			})
  7140  		})
  7141  
  7142  		Convey("exe", func() {
  7143  			Convey("empty", func() {
  7144  				req := &pb.ScheduleBuildRequest{
  7145  					Exe:             &pb.Executable{},
  7146  					TemplateBuildId: 1,
  7147  				}
  7148  				err := validateSchedule(ctx, req, nil, nil)
  7149  				So(err, ShouldBeNil)
  7150  			})
  7151  
  7152  			Convey("package", func() {
  7153  				req := &pb.ScheduleBuildRequest{
  7154  					Exe: &pb.Executable{
  7155  						CipdPackage: "package",
  7156  					},
  7157  					TemplateBuildId: 1,
  7158  				}
  7159  				err := validateSchedule(ctx, req, nil, nil)
  7160  				So(err, ShouldErrLike, "cipd_package must not be specified")
  7161  			})
  7162  
  7163  			Convey("version", func() {
  7164  				Convey("invalid", func() {
  7165  					req := &pb.ScheduleBuildRequest{
  7166  						Exe: &pb.Executable{
  7167  							CipdVersion: "invalid!",
  7168  						},
  7169  						TemplateBuildId: 1,
  7170  					}
  7171  					err := validateSchedule(ctx, req, nil, nil)
  7172  					So(err, ShouldErrLike, "cipd_version")
  7173  				})
  7174  
  7175  				Convey("valid", func() {
  7176  					req := &pb.ScheduleBuildRequest{
  7177  						Exe: &pb.Executable{
  7178  							CipdVersion: "valid",
  7179  						},
  7180  						TemplateBuildId: 1,
  7181  					}
  7182  					err := validateSchedule(ctx, req, nil, nil)
  7183  					So(err, ShouldBeNil)
  7184  				})
  7185  			})
  7186  		})
  7187  
  7188  		Convey("gerrit changes", func() {
  7189  			Convey("empty", func() {
  7190  				req := &pb.ScheduleBuildRequest{
  7191  					GerritChanges:   []*pb.GerritChange{},
  7192  					TemplateBuildId: 1,
  7193  				}
  7194  				err := validateSchedule(ctx, req, nil, nil)
  7195  				So(err, ShouldBeNil)
  7196  			})
  7197  
  7198  			Convey("unspecified", func() {
  7199  				req := &pb.ScheduleBuildRequest{
  7200  					GerritChanges: []*pb.GerritChange{
  7201  						{},
  7202  					},
  7203  					TemplateBuildId: 1,
  7204  				}
  7205  				err := validateSchedule(ctx, req, nil, nil)
  7206  				So(err, ShouldErrLike, "gerrit_changes")
  7207  			})
  7208  
  7209  			Convey("change", func() {
  7210  				req := &pb.ScheduleBuildRequest{
  7211  					GerritChanges: []*pb.GerritChange{
  7212  						{
  7213  							Host:     "host",
  7214  							Patchset: 1,
  7215  							Project:  "project",
  7216  						},
  7217  					},
  7218  					TemplateBuildId: 1,
  7219  				}
  7220  				err := validateSchedule(ctx, req, nil, nil)
  7221  				So(err, ShouldErrLike, "change must be specified")
  7222  			})
  7223  
  7224  			Convey("host", func() {
  7225  				Convey("not specified", func() {
  7226  					req := &pb.ScheduleBuildRequest{
  7227  						GerritChanges: []*pb.GerritChange{
  7228  							{
  7229  								Change:   1,
  7230  								Patchset: 1,
  7231  								Project:  "project",
  7232  							},
  7233  						},
  7234  						TemplateBuildId: 1,
  7235  					}
  7236  					err := validateSchedule(ctx, req, nil, nil)
  7237  					So(err, ShouldErrLike, "host must be specified")
  7238  				})
  7239  				Convey("invalid", func() {
  7240  					req := &pb.ScheduleBuildRequest{
  7241  						GerritChanges: []*pb.GerritChange{
  7242  							{
  7243  								Change:   1,
  7244  								Host:     "https://somehost", // host should not include the protocol.
  7245  								Patchset: 1,
  7246  								Project:  "project",
  7247  							},
  7248  						},
  7249  						TemplateBuildId: 1,
  7250  					}
  7251  					err := validateSchedule(ctx, req, nil, nil)
  7252  					So(err, ShouldErrLike, "host does not match pattern")
  7253  				})
  7254  				Convey("too long", func() {
  7255  					req := &pb.ScheduleBuildRequest{
  7256  						GerritChanges: []*pb.GerritChange{
  7257  							{
  7258  								Change:   1,
  7259  								Host:     strings.Repeat("h", 256),
  7260  								Patchset: 1,
  7261  								Project:  "project",
  7262  							},
  7263  						},
  7264  						TemplateBuildId: 1,
  7265  					}
  7266  					err := validateSchedule(ctx, req, nil, nil)
  7267  					So(err, ShouldErrLike, "host must not exceed 255 characters")
  7268  				})
  7269  			})
  7270  
  7271  			Convey("patchset", func() {
  7272  				req := &pb.ScheduleBuildRequest{
  7273  					GerritChanges: []*pb.GerritChange{
  7274  						{
  7275  							Change:  1,
  7276  							Host:    "host",
  7277  							Project: "project",
  7278  						},
  7279  					},
  7280  					TemplateBuildId: 1,
  7281  				}
  7282  				err := validateSchedule(ctx, req, nil, nil)
  7283  				So(err, ShouldErrLike, "patchset must be specified")
  7284  			})
  7285  
  7286  			Convey("project", func() {
  7287  				req := &pb.ScheduleBuildRequest{
  7288  					GerritChanges: []*pb.GerritChange{
  7289  						{
  7290  							Change:   1,
  7291  							Host:     "host",
  7292  							Patchset: 1,
  7293  						},
  7294  					},
  7295  					TemplateBuildId: 1,
  7296  				}
  7297  				err := validateSchedule(ctx, req, nil, nil)
  7298  				So(err, ShouldErrLike, "project must be specified")
  7299  			})
  7300  
  7301  			Convey("ok", func() {
  7302  				req := &pb.ScheduleBuildRequest{
  7303  					GerritChanges: []*pb.GerritChange{
  7304  						{
  7305  							Change:   1,
  7306  							Host:     "host",
  7307  							Patchset: 1,
  7308  							Project:  "project",
  7309  						},
  7310  					},
  7311  					TemplateBuildId: 1,
  7312  				}
  7313  				err := validateSchedule(ctx, req, nil, nil)
  7314  				So(err, ShouldBeNil)
  7315  			})
  7316  		})
  7317  
  7318  		Convey("gitiles commit", func() {
  7319  			req := &pb.ScheduleBuildRequest{
  7320  				GitilesCommit: &pb.GitilesCommit{
  7321  					Host: "example.com",
  7322  				},
  7323  				TemplateBuildId: 1,
  7324  			}
  7325  			err := validateSchedule(ctx, req, nil, nil)
  7326  			So(err, ShouldErrLike, "gitiles_commit")
  7327  		})
  7328  
  7329  		Convey("notify", func() {
  7330  			ctx, psserver, psclient, err := clients.SetupTestPubsub(ctx, "project")
  7331  			So(err, ShouldBeNil)
  7332  			defer func() {
  7333  				psclient.Close()
  7334  				psserver.Close()
  7335  			}()
  7336  			tpc, err := psclient.CreateTopic(ctx, "topic")
  7337  			tpc.IAM()
  7338  			So(err, ShouldBeNil)
  7339  			ctx = cachingtest.WithGlobalCache(ctx, map[string]caching.BlobCache{
  7340  				"has_perm_on_pubsub_callback_topic": cachingtest.NewBlobCache(),
  7341  			})
  7342  			Convey("empty", func() {
  7343  				req := &pb.ScheduleBuildRequest{
  7344  					Notify:          &pb.NotificationConfig{},
  7345  					TemplateBuildId: 1,
  7346  				}
  7347  				err := validateSchedule(ctx, req, nil, nil)
  7348  				So(err, ShouldErrLike, "notify")
  7349  			})
  7350  
  7351  			Convey("pubsub topic", func() {
  7352  				req := &pb.ScheduleBuildRequest{
  7353  					Notify: &pb.NotificationConfig{
  7354  						UserData: []byte("user data"),
  7355  					},
  7356  					TemplateBuildId: 1,
  7357  				}
  7358  				err := validateSchedule(ctx, req, nil, nil)
  7359  				So(err, ShouldErrLike, "pubsub_topic")
  7360  			})
  7361  
  7362  			Convey("user data", func() {
  7363  				req := &pb.ScheduleBuildRequest{
  7364  					Notify: &pb.NotificationConfig{
  7365  						PubsubTopic: "projects/project/topics/topic",
  7366  						UserData:    make([]byte, 4097),
  7367  					},
  7368  					TemplateBuildId: 1,
  7369  				}
  7370  				err := validateSchedule(ctx, req, nil, nil)
  7371  				So(err, ShouldErrLike, "user_data")
  7372  			})
  7373  
  7374  			Convey("ok - pubsub topic perm cached", func() {
  7375  				cache := caching.GlobalCache(ctx, "has_perm_on_pubsub_callback_topic")
  7376  				err := cache.Set(ctx, "projects/project/topics/topic", []byte{1}, 10*time.Hour)
  7377  				So(err, ShouldBeNil)
  7378  				req := &pb.ScheduleBuildRequest{
  7379  					Notify: &pb.NotificationConfig{
  7380  						PubsubTopic: "projects/project/topics/topic",
  7381  						UserData:    []byte("user data"),
  7382  					},
  7383  					TemplateBuildId: 1,
  7384  				}
  7385  				err = validateSchedule(ctx, req, nil, nil)
  7386  				So(err, ShouldBeNil)
  7387  			})
  7388  
  7389  			Convey("ok - pubsub topic perm not cached", func() {
  7390  				req := &pb.ScheduleBuildRequest{
  7391  					Notify: &pb.NotificationConfig{
  7392  						PubsubTopic: "projects/project/topics/topic",
  7393  						UserData:    []byte("user data"),
  7394  					},
  7395  					TemplateBuildId: 1,
  7396  				}
  7397  				err := validateSchedule(ctx, req, nil, nil)
  7398  				// "cloud.google.com/go/pubsub/pstest" lib doesn't expose a way to mock
  7399  				// IAM policy check. Therefore, only check if our `validateSchedule`
  7400  				// tries to call `topic.IAM().TestPermissions()` and get the expected
  7401  				// `Unimplemented` err msg.
  7402  				So(err, ShouldErrLike, "Unimplemented desc = unknown service google.iam.v1.IAMPolicy")
  7403  				// The bad result should not be cached.
  7404  				cache := caching.GlobalCache(ctx, "has_perm_on_pubsub_callback_topic")
  7405  				_, err = cache.Get(ctx, "projects/project/topics/topic")
  7406  				So(err, ShouldErrLike, caching.ErrCacheMiss)
  7407  			})
  7408  		})
  7409  
  7410  		Convey("priority", func() {
  7411  			Convey("negative", func() {
  7412  				req := &pb.ScheduleBuildRequest{
  7413  					Priority:        -1,
  7414  					TemplateBuildId: 1,
  7415  				}
  7416  				err := validateSchedule(ctx, req, nil, nil)
  7417  				So(err, ShouldErrLike, "priority must be in")
  7418  			})
  7419  
  7420  			Convey("excessive", func() {
  7421  				req := &pb.ScheduleBuildRequest{
  7422  					Priority:        256,
  7423  					TemplateBuildId: 1,
  7424  				}
  7425  				err := validateSchedule(ctx, req, nil, nil)
  7426  				So(err, ShouldErrLike, "priority must be in")
  7427  			})
  7428  		})
  7429  
  7430  		Convey("properties", func() {
  7431  			Convey("prohibited", func() {
  7432  				req := &pb.ScheduleBuildRequest{
  7433  					Properties: &structpb.Struct{
  7434  						Fields: map[string]*structpb.Value{
  7435  							"buildbucket": {
  7436  								Kind: &structpb.Value_StringValue{},
  7437  							},
  7438  						},
  7439  					},
  7440  					TemplateBuildId: 1,
  7441  				}
  7442  				err := validateSchedule(ctx, req, nil, nil)
  7443  				So(err, ShouldErrLike, "must not be specified")
  7444  			})
  7445  
  7446  			Convey("ok", func() {
  7447  				req := &pb.ScheduleBuildRequest{
  7448  					Properties: &structpb.Struct{
  7449  						Fields: map[string]*structpb.Value{
  7450  							"key": {
  7451  								Kind: &structpb.Value_StringValue{},
  7452  							},
  7453  						},
  7454  					},
  7455  					TemplateBuildId: 1,
  7456  				}
  7457  				err := validateSchedule(ctx, req, nil, nil)
  7458  				So(err, ShouldBeNil)
  7459  			})
  7460  		})
  7461  
  7462  		Convey("tags", func() {
  7463  			req := &pb.ScheduleBuildRequest{
  7464  				Tags: []*pb.StringPair{
  7465  					{
  7466  						Key: "key:value",
  7467  					},
  7468  				},
  7469  				TemplateBuildId: 1,
  7470  			}
  7471  			err := validateSchedule(ctx, req, nil, nil)
  7472  			So(err, ShouldErrLike, "tags")
  7473  		})
  7474  
  7475  		Convey("experiments", func() {
  7476  			Convey("ok", func() {
  7477  				req := &pb.ScheduleBuildRequest{
  7478  					TemplateBuildId: 1,
  7479  					Experiments: map[string]bool{
  7480  						bb.ExperimentBBAgent:    true,
  7481  						"cool.experiment_thing": true,
  7482  					},
  7483  				}
  7484  				So(validateSchedule(ctx, req, stringset.NewFromSlice(bb.ExperimentBBAgent), nil), ShouldBeNil)
  7485  			})
  7486  
  7487  			Convey("bad name", func() {
  7488  				req := &pb.ScheduleBuildRequest{
  7489  					TemplateBuildId: 1,
  7490  					Experiments: map[string]bool{
  7491  						"bad name": true,
  7492  					},
  7493  				}
  7494  				So(validateSchedule(ctx, req, nil, nil), ShouldErrLike, "does not match")
  7495  			})
  7496  
  7497  			Convey("bad reserved", func() {
  7498  				req := &pb.ScheduleBuildRequest{
  7499  					TemplateBuildId: 1,
  7500  					Experiments: map[string]bool{
  7501  						"luci.use_ralms": true,
  7502  					},
  7503  				}
  7504  				So(validateSchedule(ctx, req, nil, nil), ShouldErrLike, "unknown experiment has reserved prefix")
  7505  			})
  7506  		})
  7507  	})
  7508  
  7509  	Convey("setInfraAgent", t, func() {
  7510  		Convey("bbagent+userpackages", func() {
  7511  			b := &pb.Build{
  7512  				Builder: &pb.BuilderID{
  7513  					Project: "project",
  7514  					Bucket:  "bucket",
  7515  					Builder: "builder",
  7516  				},
  7517  				Canary: true,
  7518  				Exe: &pb.Executable{
  7519  					CipdPackage: "exe",
  7520  					CipdVersion: "exe-version",
  7521  				},
  7522  				Infra: &pb.BuildInfra{
  7523  					Buildbucket: &pb.BuildInfra_Buildbucket{
  7524  						Hostname: "app.appspot.com",
  7525  					},
  7526  				},
  7527  				Input: &pb.Build_Input{
  7528  					Experiments: []string{"omit", "include"},
  7529  				},
  7530  			}
  7531  			cfg := &pb.SettingsCfg{
  7532  				Swarming: &pb.SwarmingSettings{
  7533  					BbagentPackage: &pb.SwarmingSettings_Package{
  7534  						PackageName:   "infra/tools/luci/bbagent/${platform}",
  7535  						Version:       "version",
  7536  						VersionCanary: "canary-version",
  7537  					},
  7538  					UserPackages: []*pb.SwarmingSettings_Package{
  7539  						{
  7540  							PackageName:   "include",
  7541  							Version:       "version",
  7542  							VersionCanary: "canary-version",
  7543  						},
  7544  						{
  7545  							Builders: &pb.BuilderPredicate{
  7546  								RegexExclude: []string{
  7547  									".*",
  7548  								},
  7549  							},
  7550  							PackageName:   "exclude",
  7551  							Version:       "version",
  7552  							VersionCanary: "canary-version",
  7553  						},
  7554  						{
  7555  							Builders: &pb.BuilderPredicate{
  7556  								Regex: []string{
  7557  									".*",
  7558  								},
  7559  							},
  7560  							PackageName:   "subdir",
  7561  							Subdir:        "subdir",
  7562  							Version:       "version",
  7563  							VersionCanary: "canary-version",
  7564  						},
  7565  						{
  7566  							PackageName:         "include_experiment",
  7567  							Version:             "version",
  7568  							IncludeOnExperiment: []string{"include"},
  7569  						},
  7570  						{
  7571  							PackageName:         "not_include_experiment",
  7572  							Version:             "version",
  7573  							IncludeOnExperiment: []string{"not_include"},
  7574  						},
  7575  						{
  7576  							PackageName:      "omit_experiment",
  7577  							Version:          "version",
  7578  							OmitOnExperiment: []string{"omit"},
  7579  						},
  7580  					},
  7581  				},
  7582  				Cipd: &pb.CipdSettings{
  7583  					Server: "cipd server",
  7584  					Source: &pb.CipdSettings_Source{
  7585  						PackageName:   "the/offical/cipd/package/${platform}",
  7586  						Version:       "1",
  7587  						VersionCanary: "1canary",
  7588  					},
  7589  				},
  7590  			}
  7591  			err := setInfraAgent(b, cfg)
  7592  			So(err, ShouldBeNil)
  7593  			So(b.Infra.Buildbucket.Agent, ShouldResembleProto, &pb.BuildInfra_Buildbucket_Agent{
  7594  				Source: &pb.BuildInfra_Buildbucket_Agent_Source{
  7595  					DataType: &pb.BuildInfra_Buildbucket_Agent_Source_Cipd{
  7596  						Cipd: &pb.BuildInfra_Buildbucket_Agent_Source_CIPD{
  7597  							Package: "infra/tools/luci/bbagent/${platform}",
  7598  							Version: "canary-version",
  7599  							Server:  "cipd server",
  7600  						},
  7601  					},
  7602  				},
  7603  				Input: &pb.BuildInfra_Buildbucket_Agent_Input{
  7604  					CipdSource: map[string]*pb.InputDataRef{
  7605  						"cipd": &pb.InputDataRef{
  7606  							DataType: &pb.InputDataRef_Cipd{
  7607  								Cipd: &pb.InputDataRef_CIPD{
  7608  									Server: "cipd server",
  7609  									Specs: []*pb.InputDataRef_CIPD_PkgSpec{
  7610  										{
  7611  											Package: "the/offical/cipd/package/${platform}",
  7612  											Version: "1canary",
  7613  										},
  7614  									},
  7615  								},
  7616  							},
  7617  							OnPath: []string{"cipd", "cipd/bin"},
  7618  						},
  7619  					},
  7620  					Data: map[string]*pb.InputDataRef{
  7621  						"cipd_bin_packages": {
  7622  							DataType: &pb.InputDataRef_Cipd{
  7623  								Cipd: &pb.InputDataRef_CIPD{
  7624  									Server: "cipd server",
  7625  									Specs: []*pb.InputDataRef_CIPD_PkgSpec{
  7626  										{Package: "include", Version: "canary-version"},
  7627  										{Package: "include_experiment", Version: "version"},
  7628  									},
  7629  								},
  7630  							},
  7631  							OnPath: []string{"cipd_bin_packages", "cipd_bin_packages/bin"},
  7632  						},
  7633  						"cipd_bin_packages/subdir": {
  7634  							DataType: &pb.InputDataRef_Cipd{
  7635  								Cipd: &pb.InputDataRef_CIPD{
  7636  									Server: "cipd server",
  7637  									Specs: []*pb.InputDataRef_CIPD_PkgSpec{
  7638  										{Package: "subdir", Version: "canary-version"},
  7639  									},
  7640  								},
  7641  							},
  7642  							OnPath: []string{"cipd_bin_packages/subdir", "cipd_bin_packages/subdir/bin"},
  7643  						},
  7644  						"kitchen-checkout": {
  7645  							DataType: &pb.InputDataRef_Cipd{
  7646  								Cipd: &pb.InputDataRef_CIPD{
  7647  									Server: "cipd server",
  7648  									Specs: []*pb.InputDataRef_CIPD_PkgSpec{
  7649  										{Package: "exe", Version: "exe-version"},
  7650  									},
  7651  								},
  7652  							},
  7653  						},
  7654  					},
  7655  				},
  7656  				Purposes: map[string]pb.BuildInfra_Buildbucket_Agent_Purpose{
  7657  					"kitchen-checkout": pb.BuildInfra_Buildbucket_Agent_PURPOSE_EXE_PAYLOAD,
  7658  				},
  7659  				CipdClientCache: &pb.CacheEntry{
  7660  					Name: "cipd_client_c3bb9331ecf2d9dfe25df9012569bcc1278974c87ea33a56b2f4aa2761078578",
  7661  					Path: "cipd_client",
  7662  				},
  7663  				CipdPackagesCache: &pb.CacheEntry{
  7664  					Name: "cipd_cache_e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
  7665  					Path: "cipd_cache",
  7666  				},
  7667  			})
  7668  		})
  7669  
  7670  		Convey("bad bbagent cfg", func() {
  7671  			b := &pb.Build{
  7672  				Builder: &pb.BuilderID{
  7673  					Project: "project",
  7674  					Bucket:  "bucket",
  7675  					Builder: "builder",
  7676  				},
  7677  				Exe: &pb.Executable{
  7678  					CipdPackage: "exe",
  7679  					CipdVersion: "exe-version",
  7680  				},
  7681  				Infra: &pb.BuildInfra{
  7682  					Buildbucket: &pb.BuildInfra_Buildbucket{
  7683  						Hostname: "app.appspot.com",
  7684  					},
  7685  				},
  7686  			}
  7687  			cfg := &pb.SettingsCfg{
  7688  				Swarming: &pb.SwarmingSettings{
  7689  					BbagentPackage: &pb.SwarmingSettings_Package{
  7690  						PackageName: "infra/tools/luci/bbagent/${bad}",
  7691  						Version:     "bbagent-version",
  7692  					},
  7693  				},
  7694  				Cipd: &pb.CipdSettings{
  7695  					Server: "cipd server",
  7696  					Source: &pb.CipdSettings_Source{
  7697  						PackageName: "the/offical/cipd/package/${platform}",
  7698  						Version:     "1",
  7699  					},
  7700  				},
  7701  			}
  7702  			err := setInfraAgent(b, cfg)
  7703  			So(err, ShouldErrLike, "bad settings: bbagent package name must end with '/${platform}'")
  7704  			So(b.Infra.Buildbucket.Agent.Source, ShouldBeNil)
  7705  		})
  7706  
  7707  		Convey("empty settings", func() {
  7708  			b := &pb.Build{
  7709  				Builder: &pb.BuilderID{
  7710  					Project: "project",
  7711  					Bucket:  "bucket",
  7712  					Builder: "builder",
  7713  				},
  7714  				Infra: &pb.BuildInfra{
  7715  					Buildbucket: &pb.BuildInfra_Buildbucket{
  7716  						Hostname: "app.appspot.com",
  7717  					},
  7718  				},
  7719  			}
  7720  			err := setInfraAgent(b, &pb.SettingsCfg{})
  7721  			So(err, ShouldBeNil)
  7722  			So(b.Infra.Buildbucket.Agent.Source, ShouldBeNil)
  7723  			So(b.Infra.Buildbucket.Agent.Input.Data, ShouldBeEmpty)
  7724  		})
  7725  
  7726  		Convey("bbagent alternative", func() {
  7727  			b := &pb.Build{
  7728  				Builder: &pb.BuilderID{
  7729  					Project: "project",
  7730  					Bucket:  "bucket",
  7731  					Builder: "builder",
  7732  				},
  7733  				Canary: true,
  7734  				Infra: &pb.BuildInfra{
  7735  					Buildbucket: &pb.BuildInfra_Buildbucket{
  7736  						Hostname: "app.appspot.com",
  7737  					},
  7738  				},
  7739  				Input: &pb.Build_Input{
  7740  					Experiments: []string{"omit", "include"},
  7741  				},
  7742  			}
  7743  			Convey("cannot decide bbagent", func() {
  7744  				cfg := &pb.SettingsCfg{
  7745  					Swarming: &pb.SwarmingSettings{
  7746  						BbagentPackage: &pb.SwarmingSettings_Package{
  7747  							PackageName:   "infra/tools/luci/bbagent/${platform}",
  7748  							Version:       "version",
  7749  							VersionCanary: "canary-version",
  7750  						},
  7751  						AlternativeAgentPackages: []*pb.SwarmingSettings_Package{
  7752  							{
  7753  								PackageName:         "bbagent_alternative/${platform}",
  7754  								Version:             "version",
  7755  								IncludeOnExperiment: []string{"include"},
  7756  							},
  7757  							{
  7758  								PackageName:         "bbagent_alternative_2/${platform}",
  7759  								Version:             "version",
  7760  								IncludeOnExperiment: []string{"include"},
  7761  							},
  7762  						},
  7763  					},
  7764  					Cipd: &pb.CipdSettings{
  7765  						Server: "cipd server",
  7766  					},
  7767  				}
  7768  				err := setInfraAgent(b, cfg)
  7769  				So(err, ShouldErrLike, "cannot decide buildbucket agent source")
  7770  			})
  7771  			Convey("pass", func() {
  7772  				cfg := &pb.SettingsCfg{
  7773  					Swarming: &pb.SwarmingSettings{
  7774  						BbagentPackage: &pb.SwarmingSettings_Package{
  7775  							PackageName:   "infra/tools/luci/bbagent/${platform}",
  7776  							Version:       "version",
  7777  							VersionCanary: "canary-version",
  7778  						},
  7779  						AlternativeAgentPackages: []*pb.SwarmingSettings_Package{
  7780  							{
  7781  								PackageName:         "bbagent_alternative/${platform}",
  7782  								Version:             "version",
  7783  								IncludeOnExperiment: []string{"include"},
  7784  							},
  7785  						},
  7786  					},
  7787  					Cipd: &pb.CipdSettings{
  7788  						Server: "cipd server",
  7789  						Source: &pb.CipdSettings_Source{
  7790  							PackageName: "the/offical/cipd/package/${platform}",
  7791  							Version:     "1",
  7792  						},
  7793  					},
  7794  				}
  7795  				err := setInfraAgent(b, cfg)
  7796  				So(err, ShouldBeNil)
  7797  				So(b.Infra.Buildbucket.Agent, ShouldResembleProto, &pb.BuildInfra_Buildbucket_Agent{
  7798  					Source: &pb.BuildInfra_Buildbucket_Agent_Source{
  7799  						DataType: &pb.BuildInfra_Buildbucket_Agent_Source_Cipd{
  7800  							Cipd: &pb.BuildInfra_Buildbucket_Agent_Source_CIPD{
  7801  								Package: "bbagent_alternative/${platform}",
  7802  								Version: "version",
  7803  								Server:  "cipd server",
  7804  							},
  7805  						},
  7806  					},
  7807  					Input: &pb.BuildInfra_Buildbucket_Agent_Input{
  7808  						CipdSource: map[string]*pb.InputDataRef{
  7809  							"cipd": &pb.InputDataRef{
  7810  								DataType: &pb.InputDataRef_Cipd{
  7811  									Cipd: &pb.InputDataRef_CIPD{
  7812  										Server: "cipd server",
  7813  										Specs: []*pb.InputDataRef_CIPD_PkgSpec{
  7814  											{
  7815  												Package: "the/offical/cipd/package/${platform}",
  7816  												Version: "1",
  7817  											},
  7818  										},
  7819  									},
  7820  								},
  7821  								OnPath: []string{"cipd", "cipd/bin"},
  7822  							},
  7823  						},
  7824  					},
  7825  					Purposes: map[string]pb.BuildInfra_Buildbucket_Agent_Purpose{
  7826  						"kitchen-checkout": pb.BuildInfra_Buildbucket_Agent_PURPOSE_EXE_PAYLOAD,
  7827  					},
  7828  					CipdClientCache: &pb.CacheEntry{
  7829  						Name: "cipd_client_6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b",
  7830  						Path: "cipd_client",
  7831  					},
  7832  				})
  7833  			})
  7834  		})
  7835  
  7836  		Convey("bbagent_utilility_packages", func() {
  7837  			b := &pb.Build{
  7838  				Builder: &pb.BuilderID{
  7839  					Project: "project",
  7840  					Bucket:  "bucket",
  7841  					Builder: "builder",
  7842  				},
  7843  				Canary: true,
  7844  				Infra: &pb.BuildInfra{
  7845  					Buildbucket: &pb.BuildInfra_Buildbucket{
  7846  						Hostname: "app.appspot.com",
  7847  					},
  7848  				},
  7849  				Input: &pb.Build_Input{
  7850  					Experiments: []string{"omit", "include"},
  7851  				},
  7852  			}
  7853  			cfg := &pb.SettingsCfg{
  7854  				Swarming: &pb.SwarmingSettings{
  7855  					BbagentPackage: &pb.SwarmingSettings_Package{
  7856  						PackageName:   "infra/tools/luci/bbagent/${platform}",
  7857  						Version:       "version",
  7858  						VersionCanary: "canary-version",
  7859  					},
  7860  					BbagentUtilityPackages: []*pb.SwarmingSettings_Package{
  7861  						{
  7862  							PackageName:   "include",
  7863  							Version:       "version",
  7864  							VersionCanary: "canary-version",
  7865  						},
  7866  						{
  7867  							Builders: &pb.BuilderPredicate{
  7868  								RegexExclude: []string{
  7869  									".*",
  7870  								},
  7871  							},
  7872  							PackageName:   "exclude",
  7873  							Version:       "version",
  7874  							VersionCanary: "canary-version",
  7875  						},
  7876  						{
  7877  							Builders: &pb.BuilderPredicate{
  7878  								Regex: []string{
  7879  									".*",
  7880  								},
  7881  							},
  7882  							PackageName:   "subdir",
  7883  							Subdir:        "subdir",
  7884  							Version:       "version",
  7885  							VersionCanary: "canary-version",
  7886  						},
  7887  						{
  7888  							PackageName:         "include_experiment",
  7889  							Version:             "version",
  7890  							IncludeOnExperiment: []string{"include"},
  7891  						},
  7892  						{
  7893  							PackageName:         "not_include_experiment",
  7894  							Version:             "version",
  7895  							IncludeOnExperiment: []string{"not_include"},
  7896  						},
  7897  						{
  7898  							PackageName:      "omit_experiment",
  7899  							Version:          "version",
  7900  							OmitOnExperiment: []string{"omit"},
  7901  						},
  7902  					},
  7903  				},
  7904  				Cipd: &pb.CipdSettings{
  7905  					Server: "cipd server",
  7906  					Source: &pb.CipdSettings_Source{
  7907  						PackageName: "the/offical/cipd/package/${platform}",
  7908  						Version:     "1",
  7909  					},
  7910  				},
  7911  			}
  7912  			err := setInfraAgent(b, cfg)
  7913  			So(err, ShouldBeNil)
  7914  			So(b.Infra.Buildbucket.Agent, ShouldResembleProto, &pb.BuildInfra_Buildbucket_Agent{
  7915  				Source: &pb.BuildInfra_Buildbucket_Agent_Source{
  7916  					DataType: &pb.BuildInfra_Buildbucket_Agent_Source_Cipd{
  7917  						Cipd: &pb.BuildInfra_Buildbucket_Agent_Source_CIPD{
  7918  							Package: "infra/tools/luci/bbagent/${platform}",
  7919  							Version: "canary-version",
  7920  							Server:  "cipd server",
  7921  						},
  7922  					},
  7923  				},
  7924  				Input: &pb.BuildInfra_Buildbucket_Agent_Input{
  7925  					CipdSource: map[string]*pb.InputDataRef{
  7926  						"cipd": &pb.InputDataRef{
  7927  							DataType: &pb.InputDataRef_Cipd{
  7928  								Cipd: &pb.InputDataRef_CIPD{
  7929  									Server: "cipd server",
  7930  									Specs: []*pb.InputDataRef_CIPD_PkgSpec{
  7931  										{
  7932  											Package: "the/offical/cipd/package/${platform}",
  7933  											Version: "1",
  7934  										},
  7935  									},
  7936  								},
  7937  							},
  7938  							OnPath: []string{"cipd", "cipd/bin"},
  7939  						},
  7940  					},
  7941  					Data: map[string]*pb.InputDataRef{
  7942  						"bbagent_utility_packages": {
  7943  							DataType: &pb.InputDataRef_Cipd{
  7944  								Cipd: &pb.InputDataRef_CIPD{
  7945  									Server: "cipd server",
  7946  									Specs: []*pb.InputDataRef_CIPD_PkgSpec{
  7947  										{Package: "include", Version: "canary-version"},
  7948  										{Package: "include_experiment", Version: "version"},
  7949  									},
  7950  								},
  7951  							},
  7952  							OnPath: []string{"bbagent_utility_packages", "bbagent_utility_packages/bin"},
  7953  						},
  7954  						"bbagent_utility_packages/subdir": {
  7955  							DataType: &pb.InputDataRef_Cipd{
  7956  								Cipd: &pb.InputDataRef_CIPD{
  7957  									Server: "cipd server",
  7958  									Specs: []*pb.InputDataRef_CIPD_PkgSpec{
  7959  										{Package: "subdir", Version: "canary-version"},
  7960  									},
  7961  								},
  7962  							},
  7963  							OnPath: []string{"bbagent_utility_packages/subdir", "bbagent_utility_packages/subdir/bin"},
  7964  						},
  7965  					},
  7966  				},
  7967  				Purposes: map[string]pb.BuildInfra_Buildbucket_Agent_Purpose{
  7968  					"kitchen-checkout":                pb.BuildInfra_Buildbucket_Agent_PURPOSE_EXE_PAYLOAD,
  7969  					"bbagent_utility_packages":        pb.BuildInfra_Buildbucket_Agent_PURPOSE_BBAGENT_UTILITY,
  7970  					"bbagent_utility_packages/subdir": pb.BuildInfra_Buildbucket_Agent_PURPOSE_BBAGENT_UTILITY,
  7971  				},
  7972  				CipdClientCache: &pb.CacheEntry{
  7973  					Name: "cipd_client_6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b",
  7974  					Path: "cipd_client",
  7975  				},
  7976  				CipdPackagesCache: &pb.CacheEntry{
  7977  					Name: "cipd_cache_e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
  7978  					Path: "cipd_cache",
  7979  				},
  7980  			})
  7981  		})
  7982  	})
  7983  }
  7984  
  7985  func sortTasksByClassName(tasks tqtesting.TaskList) {
  7986  	sort.Slice(tasks, func(i, j int) bool {
  7987  		return tasks[i].Class < tasks[j].Class
  7988  	})
  7989  }