go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/prjmanager/triager/cls_test.go (about)

     1  // Copyright 2021 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 triager
    16  
    17  import (
    18  	"fmt"
    19  	"strings"
    20  	"testing"
    21  	"time"
    22  
    23  	"google.golang.org/protobuf/proto"
    24  	"google.golang.org/protobuf/types/known/timestamppb"
    25  
    26  	"go.chromium.org/luci/common/clock/testclock"
    27  
    28  	cfgpb "go.chromium.org/luci/cv/api/config/v2"
    29  	"go.chromium.org/luci/cv/internal/changelist"
    30  	"go.chromium.org/luci/cv/internal/common"
    31  	"go.chromium.org/luci/cv/internal/configs/prjcfg"
    32  	"go.chromium.org/luci/cv/internal/cvtesting"
    33  	"go.chromium.org/luci/cv/internal/prjmanager/prjpb"
    34  	"go.chromium.org/luci/cv/internal/run"
    35  
    36  	. "github.com/smartystreets/goconvey/convey"
    37  	. "go.chromium.org/luci/common/testing/assertions"
    38  )
    39  
    40  func shouldResembleTriagedCL(actual any, expected ...any) string {
    41  	if len(expected) != 1 {
    42  		return fmt.Sprintf("expected 1 value, got %d", len(expected))
    43  	}
    44  	exp := expected[0] // this may be nil
    45  	a, ok := actual.(*clInfo)
    46  	if !ok {
    47  		return fmt.Sprintf("Wrong actual type %T, must be %T", actual, a)
    48  	}
    49  	if err := ShouldHaveSameTypeAs(actual, exp); err != "" {
    50  		return err
    51  	}
    52  	b := exp.(*clInfo)
    53  	switch {
    54  	case a == b:
    55  		return ""
    56  	case a == nil:
    57  		return "actual is nil, but non-nil was expected"
    58  	case b == nil:
    59  		return "actual is not-nil, but nil was expected"
    60  	}
    61  
    62  	buf := strings.Builder{}
    63  	for _, err := range []string{
    64  		ShouldResemble(a.cqReady, b.cqReady),
    65  		ShouldResemble(a.nprReady, b.nprReady),
    66  		ShouldResemble(a.runIndexes, b.runIndexes),
    67  		cvtesting.SafeShouldResemble(a.deps, b.deps),
    68  		ShouldResembleProto(a.pcl, b.pcl),
    69  		ShouldResembleProto(a.purgingCL, b.purgingCL),
    70  		ShouldResembleProto(a.purgeReasons, b.purgeReasons),
    71  		ShouldResembleProto(a.triggeringCLDeps, b.triggeringCLDeps),
    72  	} {
    73  		if err != "" {
    74  			buf.WriteRune(' ')
    75  			buf.WriteString(err)
    76  		}
    77  	}
    78  	return strings.TrimSpace(buf.String())
    79  }
    80  
    81  func TestCLsTriage(t *testing.T) {
    82  	t.Parallel()
    83  
    84  	Convey("Component's PCL deps triage", t, func() {
    85  		ct := cvtesting.Test{}
    86  		ctx, cancel := ct.SetUp(t)
    87  		defer cancel()
    88  
    89  		// Truncate start time point s.t. easy to see diff in test failures.
    90  		epoch := testclock.TestRecentTimeUTC.Truncate(10000 * time.Second)
    91  		dryRun := func(t time.Time) *run.Trigger {
    92  			return &run.Trigger{Mode: string(run.DryRun), Time: timestamppb.New(t)}
    93  		}
    94  		fullRun := func(t time.Time) *run.Trigger {
    95  			return &run.Trigger{Mode: string(run.FullRun), Time: timestamppb.New(t)}
    96  		}
    97  		newPatchsetTrigger := func(t time.Time) *run.Trigger {
    98  			return &run.Trigger{Mode: string(run.NewPatchsetRun), Time: timestamppb.New(t)}
    99  		}
   100  
   101  		sup := &simplePMState{
   102  			pb: &prjpb.PState{},
   103  			cgs: []*prjcfg.ConfigGroup{
   104  				{ID: "hash/singular", Content: &cfgpb.ConfigGroup{}},
   105  				{ID: "hash/combinable", Content: &cfgpb.ConfigGroup{CombineCls: &cfgpb.CombineCLs{}}},
   106  				{ID: "hash/another", Content: &cfgpb.ConfigGroup{}},
   107  				{ID: "hash/npr", Content: &cfgpb.ConfigGroup{
   108  					Verifiers: &cfgpb.Verifiers{
   109  						Tryjob: &cfgpb.Verifiers_Tryjob{
   110  							Builders: []*cfgpb.Verifiers_Tryjob_Builder{
   111  								{Name: "nprBuilder", ModeAllowlist: []string{string(run.NewPatchsetRun)}},
   112  							},
   113  						},
   114  					},
   115  				}},
   116  			},
   117  		}
   118  		pm := pmState{sup}
   119  		const singIdx, combIdx, anotherIdx, nprIdx = 0, 1, 2, 3
   120  
   121  		do := func(c *prjpb.Component) map[int64]*clInfo {
   122  			sup.pb.Components = []*prjpb.Component{c} // include it in backup
   123  			backup := prjpb.PState{}
   124  			proto.Merge(&backup, sup.pb)
   125  
   126  			cls := triageCLs(ctx, c, pm)
   127  			So(sup.pb, ShouldResembleProto, &backup) // must not be modified
   128  			return cls
   129  		}
   130  
   131  		Convey("Typical 1 CL component without deps", func() {
   132  			sup.pb.Pcls = []*prjpb.PCL{{
   133  				Clid:               1,
   134  				ConfigGroupIndexes: []int32{singIdx},
   135  				Status:             prjpb.PCL_OK,
   136  				Triggers:           &run.Triggers{CqVoteTrigger: dryRun(epoch)},
   137  				Submitted:          false,
   138  				Deps:               nil,
   139  			}}
   140  
   141  			Convey("Ready without runs", func() {
   142  				cls := do(&prjpb.Component{Clids: []int64{1}})
   143  				So(cls, ShouldHaveLength, 1)
   144  				expected := &clInfo{
   145  					pcl:        pm.MustPCL(1),
   146  					runIndexes: nil,
   147  					purgingCL:  nil,
   148  
   149  					triagedCL: triagedCL{
   150  						purgeReasons: nil,
   151  						cqReady:      true,
   152  						deps:         &triagedDeps{},
   153  					},
   154  				}
   155  				So(cls[1], shouldResembleTriagedCL, expected)
   156  
   157  				Convey("ready may also be in 1+ Runs", func() {
   158  					cls := do(&prjpb.Component{
   159  						Clids: []int64{1},
   160  						Pruns: []*prjpb.PRun{{Id: "r1", Clids: []int64{1}, Mode: string(run.DryRun)}},
   161  					})
   162  					So(cls, ShouldHaveLength, 1)
   163  					expected.runIndexes = []int32{0}
   164  					So(cls[1], shouldResembleTriagedCL, expected)
   165  				})
   166  			})
   167  
   168  			Convey("CL already with Errors is not ready", func() {
   169  				sup.pb.Pcls[0].PurgeReasons = []*prjpb.PurgeReason{
   170  					{
   171  						ClError: &changelist.CLError{
   172  							Kind: &changelist.CLError_OwnerLacksEmail{OwnerLacksEmail: true},
   173  						},
   174  						ApplyTo: &prjpb.PurgeReason_AllActiveTriggers{AllActiveTriggers: true},
   175  					},
   176  					{
   177  						ClError: &changelist.CLError{
   178  							Kind: &changelist.CLError_UnsupportedMode{UnsupportedMode: "CUSTOM_RUN"},
   179  						},
   180  						ApplyTo: &prjpb.PurgeReason_AllActiveTriggers{AllActiveTriggers: true},
   181  					},
   182  				}
   183  				cls := do(&prjpb.Component{Clids: []int64{1}})
   184  				So(cls, ShouldHaveLength, 1)
   185  				expected := &clInfo{
   186  					pcl:        pm.MustPCL(1),
   187  					runIndexes: nil,
   188  					purgingCL:  nil,
   189  
   190  					triagedCL: triagedCL{
   191  						purgeReasons: sup.pb.Pcls[0].GetPurgeReasons(),
   192  					},
   193  				}
   194  				So(cls[1], shouldResembleTriagedCL, expected)
   195  			})
   196  
   197  			Convey("Already purged is never ready", func() {
   198  				sup.pb.PurgingCls = []*prjpb.PurgingCL{{
   199  					Clid:    1,
   200  					ApplyTo: &prjpb.PurgingCL_AllActiveTriggers{AllActiveTriggers: true},
   201  				}}
   202  				cls := do(&prjpb.Component{Clids: []int64{1}})
   203  				So(cls, ShouldHaveLength, 1)
   204  				expected := &clInfo{
   205  					pcl:        pm.MustPCL(1),
   206  					runIndexes: nil,
   207  					purgingCL:  pm.PurgingCL(1),
   208  
   209  					triagedCL: triagedCL{
   210  						purgeReasons: nil,
   211  						cqReady:      false,
   212  						deps:         &triagedDeps{},
   213  					},
   214  				}
   215  				So(cls[1], shouldResembleTriagedCL, expected)
   216  
   217  				Convey("not even if inside 1+ Runs", func() {
   218  					cls := do(&prjpb.Component{
   219  						Clids: []int64{1},
   220  						Pruns: []*prjpb.PRun{{Id: "r1", Clids: []int64{1}, Mode: string(run.DryRun)}},
   221  					})
   222  					So(cls, ShouldHaveLength, 1)
   223  					expected.runIndexes = []int32{0}
   224  					So(cls[1], shouldResembleTriagedCL, expected)
   225  				})
   226  			})
   227  
   228  			Convey("CL matching several config groups is never ready", func() {
   229  				sup.PCL(1).ConfigGroupIndexes = []int32{singIdx, anotherIdx}
   230  				cls := do(&prjpb.Component{Clids: []int64{1}})
   231  				So(cls, ShouldHaveLength, 1)
   232  				expected := &clInfo{
   233  					pcl:        pm.MustPCL(1),
   234  					runIndexes: nil,
   235  					purgingCL:  nil,
   236  
   237  					triagedCL: triagedCL{
   238  						purgeReasons: []*prjpb.PurgeReason{{
   239  							ClError: &changelist.CLError{
   240  								Kind: &changelist.CLError_WatchedByManyConfigGroups_{
   241  									WatchedByManyConfigGroups: &changelist.CLError_WatchedByManyConfigGroups{
   242  										ConfigGroups: []string{"singular", "another"},
   243  									},
   244  								},
   245  							},
   246  							ApplyTo: &prjpb.PurgeReason_Triggers{Triggers: &run.Triggers{CqVoteTrigger: dryRun(epoch)}},
   247  						}},
   248  						cqReady: false,
   249  						deps:    nil, // not checked.
   250  					},
   251  				}
   252  				So(cls[1], shouldResembleTriagedCL, expected)
   253  
   254  				Convey("not even if inside 1+ Runs, but Run protects from purging", func() {
   255  					cls := do(&prjpb.Component{
   256  						Clids: []int64{1},
   257  						Pruns: []*prjpb.PRun{{Id: "r1", Clids: []int64{1}, Mode: string(run.DryRun)}},
   258  					})
   259  					So(cls, ShouldHaveLength, 1)
   260  					expected.runIndexes = []int32{0}
   261  					expected.purgeReasons = nil
   262  					So(cls[1], shouldResembleTriagedCL, expected)
   263  				})
   264  			})
   265  		})
   266  
   267  		Convey("Typical 1 CL component with new patchset run enabled", func() {
   268  			sup.pb.Pcls = []*prjpb.PCL{{
   269  				Clid:               1,
   270  				ConfigGroupIndexes: []int32{nprIdx},
   271  				Status:             prjpb.PCL_OK,
   272  				Triggers: &run.Triggers{
   273  					CqVoteTrigger:         dryRun(epoch),
   274  					NewPatchsetRunTrigger: newPatchsetTrigger(epoch),
   275  				},
   276  				Submitted: false,
   277  				Deps:      nil,
   278  			}}
   279  			Convey("new patchset upload on CL with CQ vote run being purged", func() {
   280  				sup.pb.PurgingCls = append(sup.pb.PurgingCls, &prjpb.PurgingCL{
   281  					Clid: 1,
   282  					ApplyTo: &prjpb.PurgingCL_Triggers{
   283  						Triggers: &run.Triggers{
   284  							CqVoteTrigger: dryRun(epoch),
   285  						},
   286  					},
   287  				})
   288  				expected := &clInfo{
   289  					pcl:        pm.MustPCL(1),
   290  					runIndexes: nil,
   291  					purgingCL: &prjpb.PurgingCL{
   292  						Clid: 1,
   293  						ApplyTo: &prjpb.PurgingCL_Triggers{
   294  							Triggers: &run.Triggers{
   295  								CqVoteTrigger: dryRun(epoch),
   296  							},
   297  						},
   298  					},
   299  
   300  					triagedCL: triagedCL{
   301  						purgeReasons: nil,
   302  						cqReady:      false,
   303  						nprReady:     true,
   304  						deps:         &triagedDeps{},
   305  					},
   306  				}
   307  
   308  				cls := do(&prjpb.Component{Clids: []int64{1}})
   309  				So(cls, ShouldHaveLength, 1)
   310  				So(cls[1], shouldResembleTriagedCL, expected)
   311  			})
   312  
   313  			Convey("new patch upload on CL with NPR being purged", func() {
   314  				sup.pb.PurgingCls = append(sup.pb.PurgingCls, &prjpb.PurgingCL{
   315  					Clid: 1,
   316  					ApplyTo: &prjpb.PurgingCL_Triggers{
   317  						Triggers: &run.Triggers{
   318  							NewPatchsetRunTrigger: newPatchsetTrigger(epoch),
   319  						},
   320  					},
   321  				})
   322  				expected := &clInfo{
   323  					pcl:        pm.MustPCL(1),
   324  					runIndexes: nil,
   325  					purgingCL: &prjpb.PurgingCL{
   326  						Clid: 1,
   327  						ApplyTo: &prjpb.PurgingCL_Triggers{
   328  							Triggers: &run.Triggers{
   329  								NewPatchsetRunTrigger: newPatchsetTrigger(epoch),
   330  							},
   331  						},
   332  					},
   333  					triagedCL: triagedCL{
   334  						purgeReasons: nil,
   335  						cqReady:      true,
   336  						nprReady:     false,
   337  						deps:         &triagedDeps{},
   338  					},
   339  				}
   340  				cls := do(&prjpb.Component{Clids: []int64{1}})
   341  				So(cls, ShouldHaveLength, 1)
   342  				So(cls[1], shouldResembleTriagedCL, expected)
   343  
   344  			})
   345  		})
   346  		Convey("Triage with ongoing New Pachset Run", func() {
   347  			sup.pb.Pcls = []*prjpb.PCL{{
   348  				Clid:               1,
   349  				ConfigGroupIndexes: []int32{nprIdx},
   350  				Status:             prjpb.PCL_OK,
   351  				Triggers: &run.Triggers{
   352  					NewPatchsetRunTrigger: newPatchsetTrigger(epoch),
   353  				},
   354  				Submitted: false,
   355  				Deps:      nil,
   356  			}}
   357  			expected := &clInfo{
   358  				pcl:        pm.MustPCL(1),
   359  				runIndexes: []int32{0},
   360  				triagedCL: triagedCL{
   361  					purgeReasons: nil,
   362  					nprReady:     true,
   363  				},
   364  			}
   365  			cls := do(&prjpb.Component{
   366  				Clids: []int64{1},
   367  				Pruns: []*prjpb.PRun{{Id: "r1", Clids: []int64{1}, Mode: string(run.NewPatchsetRun)}},
   368  			})
   369  			So(cls, ShouldHaveLength, 1)
   370  			So(cls[1], shouldResembleTriagedCL, expected)
   371  		})
   372  		Convey("Single CL Runs: typical CL stack", func() {
   373  			// CL 3 depends on 2, which in turn depends 1.
   374  			// Start configuration is each one is Dry-run triggered.
   375  			sup.pb.Pcls = []*prjpb.PCL{
   376  				{
   377  					Clid:               1,
   378  					ConfigGroupIndexes: []int32{singIdx},
   379  					Status:             prjpb.PCL_OK,
   380  					Triggers:           &run.Triggers{CqVoteTrigger: dryRun(epoch)},
   381  					Submitted:          false,
   382  					Deps:               nil,
   383  				},
   384  				{
   385  					Clid:               2,
   386  					ConfigGroupIndexes: []int32{singIdx},
   387  					Status:             prjpb.PCL_OK,
   388  					Triggers:           &run.Triggers{CqVoteTrigger: dryRun(epoch)},
   389  					Submitted:          false,
   390  					Deps:               []*changelist.Dep{{Clid: 1, Kind: changelist.DepKind_HARD}},
   391  				},
   392  				{
   393  					Clid:               3,
   394  					ConfigGroupIndexes: []int32{singIdx},
   395  					Status:             prjpb.PCL_OK,
   396  					Triggers:           &run.Triggers{CqVoteTrigger: dryRun(epoch)},
   397  					Submitted:          false,
   398  					Deps:               []*changelist.Dep{{Clid: 1, Kind: changelist.DepKind_SOFT}, {Clid: 2, Kind: changelist.DepKind_HARD}},
   399  				},
   400  			}
   401  
   402  			Convey("Dry run everywhere is OK", func() {
   403  				cls := do(&prjpb.Component{Clids: []int64{1, 2, 3}})
   404  				So(cls, ShouldHaveLength, 3)
   405  				for _, info := range cls {
   406  					So(info.cqReady, ShouldBeTrue)
   407  					So(info.deps.OK(), ShouldBeTrue)
   408  					So(info.lastCQVoteTriggered(), ShouldResemble, epoch)
   409  				}
   410  			})
   411  
   412  			Convey("Full run at the bottom (CL1) and dry run elsewhere is also OK", func() {
   413  				sup.PCL(1).Triggers = &run.Triggers{CqVoteTrigger: fullRun(epoch)}
   414  				cls := do(&prjpb.Component{Clids: []int64{1, 2, 3}})
   415  				So(cls, ShouldHaveLength, 3)
   416  				for _, info := range cls {
   417  					So(info.cqReady, ShouldBeTrue)
   418  					So(info.deps.OK(), ShouldBeTrue)
   419  				}
   420  			})
   421  
   422  			Convey("Full Run on #3 is purged if its deps aren't submitted, but NPR is not affected", func() {
   423  				sup.PCL(1).Triggers = &run.Triggers{CqVoteTrigger: dryRun(epoch), NewPatchsetRunTrigger: newPatchsetTrigger(epoch)}
   424  				sup.PCL(2).Triggers = &run.Triggers{CqVoteTrigger: dryRun(epoch), NewPatchsetRunTrigger: newPatchsetTrigger(epoch)}
   425  				sup.PCL(3).Triggers = &run.Triggers{CqVoteTrigger: fullRun(epoch), NewPatchsetRunTrigger: newPatchsetTrigger(epoch)}
   426  				cls := do(&prjpb.Component{Clids: []int64{1, 2, 3}})
   427  				So(cls[1].cqReady, ShouldBeTrue)
   428  				So(cls[2].cqReady, ShouldBeTrue)
   429  				So(cls[1].nprReady, ShouldBeTrue)
   430  				So(cls[2].nprReady, ShouldBeTrue)
   431  				So(cls[3].nprReady, ShouldBeTrue)
   432  				So(cls[3], shouldResembleTriagedCL, &clInfo{
   433  					pcl: sup.PCL(3),
   434  					triagedCL: triagedCL{
   435  						cqReady:  false,
   436  						nprReady: true,
   437  						purgeReasons: []*prjpb.PurgeReason{{
   438  							ClError: &changelist.CLError{
   439  								Kind: &changelist.CLError_InvalidDeps_{
   440  									InvalidDeps: &changelist.CLError_InvalidDeps{
   441  										SingleFullDeps: sup.PCL(3).GetDeps(),
   442  									},
   443  								},
   444  							},
   445  							ApplyTo: &prjpb.PurgeReason_Triggers{
   446  								Triggers: &run.Triggers{
   447  									CqVoteTrigger: fullRun(epoch),
   448  								},
   449  							},
   450  						}},
   451  						deps: &triagedDeps{
   452  							lastCQVoteTriggered: epoch,
   453  							invalidDeps: &changelist.CLError_InvalidDeps{
   454  								SingleFullDeps: sup.PCL(3).GetDeps(),
   455  							},
   456  						},
   457  					},
   458  				})
   459  			})
   460  
   461  			Convey("CL1 submitted but still with Run, CL2 CQ+1 is OK, CL3 CQ+2 is purged", func() {
   462  				sup.PCL(1).Triggers = nil
   463  				sup.PCL(1).Submitted = true
   464  				// PCL(2) is still not submitted.
   465  				sup.PCL(3).Triggers = &run.Triggers{CqVoteTrigger: fullRun(epoch)}
   466  				cls := do(&prjpb.Component{
   467  					Clids: []int64{1, 2, 3},
   468  					Pruns: []*prjpb.PRun{{Id: "r1", Clids: []int64{1}, Mode: string(run.FullRun)}},
   469  				})
   470  				So(cls[2].cqReady, ShouldBeTrue)
   471  				So(cls[2].deps, cvtesting.SafeShouldResemble, &triagedDeps{
   472  					submitted: []*changelist.Dep{{Clid: 1, Kind: changelist.DepKind_HARD}},
   473  				})
   474  				So(cls[3], shouldResembleTriagedCL, &clInfo{
   475  					pcl: sup.PCL(3),
   476  					triagedCL: triagedCL{
   477  						cqReady: false,
   478  						purgeReasons: []*prjpb.PurgeReason{{
   479  							ClError: &changelist.CLError{
   480  								Kind: &changelist.CLError_InvalidDeps_{
   481  									InvalidDeps: &changelist.CLError_InvalidDeps{
   482  										SingleFullDeps: []*changelist.Dep{{Clid: 2, Kind: changelist.DepKind_HARD}},
   483  									},
   484  								},
   485  							},
   486  							ApplyTo: &prjpb.PurgeReason_Triggers{
   487  								Triggers: &run.Triggers{
   488  									CqVoteTrigger: fullRun(epoch),
   489  								},
   490  							},
   491  						}},
   492  						deps: &triagedDeps{
   493  							lastCQVoteTriggered: epoch.UTC(),
   494  							submitted:           []*changelist.Dep{{Clid: 1, Kind: changelist.DepKind_SOFT}},
   495  							invalidDeps: &changelist.CLError_InvalidDeps{
   496  								SingleFullDeps: []*changelist.Dep{{Clid: 2, Kind: changelist.DepKind_HARD}},
   497  							},
   498  						},
   499  					},
   500  				})
   501  			})
   502  		})
   503  
   504  		Convey("Multiple CL Runs: 1<->2 and 3 depending on both", func() {
   505  			// CL 3 depends on 1 and 2, while 1 and 2 depend on each other (e.g. via
   506  			// CQ-Depend).  Start configuration is each one is Dry-run triggered.
   507  			sup.pb.Pcls = []*prjpb.PCL{
   508  				{
   509  					Clid:               1,
   510  					ConfigGroupIndexes: []int32{combIdx},
   511  					Status:             prjpb.PCL_OK,
   512  					Triggers:           &run.Triggers{CqVoteTrigger: dryRun(epoch)},
   513  					Submitted:          false,
   514  					Deps:               []*changelist.Dep{{Clid: 2, Kind: changelist.DepKind_SOFT}},
   515  				},
   516  				{
   517  					Clid:               2,
   518  					ConfigGroupIndexes: []int32{combIdx},
   519  					Status:             prjpb.PCL_OK,
   520  					Triggers:           &run.Triggers{CqVoteTrigger: dryRun(epoch)},
   521  					Submitted:          false,
   522  					Deps:               []*changelist.Dep{{Clid: 1, Kind: changelist.DepKind_SOFT}},
   523  				},
   524  				{
   525  					Clid:               3,
   526  					ConfigGroupIndexes: []int32{combIdx},
   527  					Status:             prjpb.PCL_OK,
   528  					Triggers:           &run.Triggers{CqVoteTrigger: dryRun(epoch)},
   529  					Submitted:          false,
   530  					Deps:               []*changelist.Dep{{Clid: 1, Kind: changelist.DepKind_SOFT}, {Clid: 2, Kind: changelist.DepKind_SOFT}},
   531  				},
   532  			}
   533  
   534  			Convey("Happy case: all are ready", func() {
   535  				cls := do(&prjpb.Component{Clids: []int64{1, 2, 3}})
   536  				So(cls, ShouldHaveLength, 3)
   537  				for _, info := range cls {
   538  					So(info.cqReady, ShouldBeTrue)
   539  					So(info.deps.OK(), ShouldBeTrue)
   540  				}
   541  			})
   542  
   543  			Convey("Full Run on #1 and #2 can co-exist, but Dry run on #3 is purged", func() {
   544  				// This scenario documents current CQDaemon behavior. This isn't desired
   545  				// long term though.
   546  				sup.PCL(1).Triggers = &run.Triggers{CqVoteTrigger: fullRun(epoch)}
   547  				sup.PCL(2).Triggers = &run.Triggers{CqVoteTrigger: fullRun(epoch)}
   548  				cls := do(&prjpb.Component{Clids: []int64{1, 2, 3}})
   549  				So(cls[1].cqReady, ShouldBeTrue)
   550  				So(cls[2].cqReady, ShouldBeTrue)
   551  				So(cls[3], shouldResembleTriagedCL, &clInfo{
   552  					pcl: sup.PCL(3),
   553  					triagedCL: triagedCL{
   554  						cqReady: false,
   555  						purgeReasons: []*prjpb.PurgeReason{{
   556  							ClError: &changelist.CLError{
   557  								Kind: &changelist.CLError_InvalidDeps_{
   558  									InvalidDeps: &changelist.CLError_InvalidDeps{
   559  										CombinableMismatchedMode: sup.PCL(3).GetDeps(),
   560  									},
   561  								},
   562  							},
   563  							ApplyTo: &prjpb.PurgeReason_Triggers{
   564  								Triggers: &run.Triggers{
   565  									CqVoteTrigger: dryRun(epoch),
   566  								},
   567  							},
   568  						}},
   569  						deps: &triagedDeps{
   570  							lastCQVoteTriggered: epoch,
   571  							invalidDeps: &changelist.CLError_InvalidDeps{
   572  								CombinableMismatchedMode: sup.PCL(3).GetDeps(),
   573  							},
   574  						},
   575  					},
   576  				})
   577  			})
   578  
   579  			Convey("Dependencies in diff config groups are not allowed", func() {
   580  				sup.PCL(1).ConfigGroupIndexes = []int32{combIdx}    // depends on 2
   581  				sup.PCL(2).ConfigGroupIndexes = []int32{anotherIdx} // depends on 1
   582  				sup.PCL(3).ConfigGroupIndexes = []int32{combIdx}    // depends on 1(OK) and 2.
   583  				cls := do(&prjpb.Component{Clids: []int64{1, 2, 3}})
   584  				for _, info := range cls {
   585  					So(info.cqReady, ShouldBeFalse)
   586  					So(info.purgeReasons, ShouldResembleProto, []*prjpb.PurgeReason{{
   587  						ClError: &changelist.CLError{
   588  							Kind: &changelist.CLError_InvalidDeps_{
   589  								InvalidDeps: info.triagedCL.deps.invalidDeps,
   590  							},
   591  						},
   592  						ApplyTo: &prjpb.PurgeReason_Triggers{
   593  							Triggers: &run.Triggers{
   594  								CqVoteTrigger: dryRun(epoch),
   595  							},
   596  						},
   597  					}})
   598  				}
   599  
   600  				Convey("unless dependency is already submitted", func() {
   601  					sup.PCL(2).Triggers = nil
   602  					sup.PCL(2).Submitted = true
   603  
   604  					cls := do(&prjpb.Component{Clids: []int64{1, 3}})
   605  					for _, info := range cls {
   606  						So(info.cqReady, ShouldBeTrue)
   607  						So(info.purgeReasons, ShouldBeNil)
   608  						So(info.deps.submitted, ShouldResembleProto, []*changelist.Dep{{Clid: 2, Kind: changelist.DepKind_SOFT}})
   609  					}
   610  				})
   611  			})
   612  		})
   613  
   614  		Convey("Ready CLs can have not yet loaded dependencies", func() {
   615  			sup.pb.Pcls = []*prjpb.PCL{
   616  				{
   617  					Clid:   1,
   618  					Status: prjpb.PCL_UNKNOWN,
   619  				},
   620  				{
   621  					Clid:               2,
   622  					ConfigGroupIndexes: []int32{combIdx},
   623  					Status:             prjpb.PCL_OK,
   624  					Triggers:           &run.Triggers{CqVoteTrigger: dryRun(epoch)},
   625  					Deps:               []*changelist.Dep{{Clid: 1, Kind: changelist.DepKind_SOFT}},
   626  				},
   627  			}
   628  			cls := do(&prjpb.Component{Clids: []int64{2}})
   629  			So(cls[2], shouldResembleTriagedCL, &clInfo{
   630  				pcl: sup.PCL(2),
   631  				triagedCL: triagedCL{
   632  					cqReady: true,
   633  					deps:    &triagedDeps{notYetLoaded: sup.PCL(2).GetDeps()},
   634  				},
   635  			})
   636  		})
   637  
   638  		Convey("Multiple CL Runs with chained CQ votes", func() {
   639  			const clid1, clid2, clid3, clid4 = 1, 2, 3, 4
   640  
   641  			newCL := func(clid int64, deps ...*changelist.Dep) *prjpb.PCL {
   642  				return &prjpb.PCL{
   643  					Clid:               clid,
   644  					ConfigGroupIndexes: []int32{singIdx},
   645  					Status:             prjpb.PCL_OK,
   646  					Submitted:          false,
   647  					Deps:               deps,
   648  				}
   649  			}
   650  			Dep := func(clid int64) *changelist.Dep {
   651  				return &changelist.Dep{Clid: clid, Kind: changelist.DepKind_HARD}
   652  			}
   653  			voter := "test@example.org"
   654  			ct.AddMember(voter, common.MCEDogfooderGroup)
   655  			sup.pb.Pcls = []*prjpb.PCL{
   656  				newCL(clid1),
   657  				newCL(clid2, Dep(clid1)),
   658  				newCL(clid3, Dep(clid1), Dep(clid2)),
   659  				newCL(clid4, Dep(clid1), Dep(clid2), Dep(clid3)),
   660  			}
   661  
   662  			Convey("CQ vote on a child CL", func() {
   663  				// Trigger CQ on the CL 3 only.
   664  				sup.pb.Pcls[2].Triggers = &run.Triggers{CqVoteTrigger: fullRun(epoch)}
   665  				sup.pb.Pcls[2].Triggers.CqVoteTrigger.Email = voter
   666  				cls := do(&prjpb.Component{Clids: []int64{clid1, clid2, clid3, clid4}})
   667  				So(cls, ShouldHaveLength, 4)
   668  
   669  				// - all CLs should be not-cq-ready.
   670  				So(cls[clid1].cqReady, ShouldBeFalse)
   671  				So(cls[clid2].cqReady, ShouldBeFalse)
   672  				So(cls[clid3].cqReady, ShouldBeFalse)
   673  				So(cls[clid4].cqReady, ShouldBeFalse)
   674  				// - CL3 should have CL1, and CL2 in needToTrigger, whereas
   675  				// the others shouldn't have any, because only CL3 has
   676  				// the CQ vote. Deps are not triaged, unless a given CL has
   677  				// a CQ vote.
   678  				So(cls[clid1].deps, ShouldBeNil)
   679  				So(cls[clid2].deps, ShouldBeNil)
   680  				So(cls[clid3].deps.needToTrigger, ShouldResembleProto, []*changelist.Dep{
   681  					Dep(clid1), Dep(clid2),
   682  				})
   683  				So(cls[clid4].deps, ShouldBeNil)
   684  			})
   685  
   686  			Convey("CQ vote on multi CLs", func() {
   687  				// Trigger CQ on the CL 2 and 4.
   688  				sup.pb.Pcls[1].Triggers = &run.Triggers{CqVoteTrigger: fullRun(epoch)}
   689  				sup.pb.Pcls[1].Triggers.CqVoteTrigger.Email = voter
   690  				sup.pb.Pcls[3].Triggers = &run.Triggers{CqVoteTrigger: fullRun(epoch)}
   691  				sup.pb.Pcls[3].Triggers.CqVoteTrigger.Email = voter
   692  				cls := do(&prjpb.Component{Clids: []int64{clid1, clid2, clid3, clid4}})
   693  				So(cls, ShouldHaveLength, 4)
   694  
   695  				// - all CLs should not be cq-ready.
   696  				So(cls[clid1].cqReady, ShouldBeFalse)
   697  				So(cls[clid2].cqReady, ShouldBeFalse)
   698  				So(cls[clid3].cqReady, ShouldBeFalse)
   699  				So(cls[clid4].cqReady, ShouldBeFalse)
   700  				// - CL3 should have CL1, and CL2 in needToTrigger, whereas
   701  				// the others shouldn't have any, because only CL3 has
   702  				// the CQ vote. Deps are not triaged, unless a given CL has
   703  				// a CQ vote.
   704  				So(cls[clid1].deps, ShouldBeNil)
   705  				So(cls[clid2].deps.needToTrigger, ShouldResembleProto, []*changelist.Dep{
   706  					Dep(clid1),
   707  				})
   708  				So(cls[clid3].deps, ShouldBeNil)
   709  				So(cls[clid4].deps.needToTrigger, ShouldResembleProto, []*changelist.Dep{
   710  					// Should NOT have clid4 in needToTrigger, as it is already
   711  					// voted.
   712  					Dep(clid1), Dep(clid3),
   713  				})
   714  			})
   715  
   716  			Convey("CqReady if all voted", func() {
   717  				// Vote on all the CLs.
   718  				for i := 0; i < 4; i++ {
   719  					sup.pb.Pcls[i].Triggers = &run.Triggers{CqVoteTrigger: fullRun(epoch)}
   720  					sup.pb.Pcls[i].Triggers.CqVoteTrigger.Email = voter
   721  				}
   722  				cls := do(&prjpb.Component{Clids: []int64{clid1, clid2, clid3, clid4}})
   723  				So(cls, ShouldHaveLength, 4)
   724  
   725  				// They all should be cq-ready.
   726  				So(cls[clid1].cqReady, ShouldBeTrue)
   727  				So(cls[clid2].cqReady, ShouldBeTrue)
   728  				So(cls[clid3].cqReady, ShouldBeTrue)
   729  				So(cls[clid4].cqReady, ShouldBeTrue)
   730  
   731  				Convey("unless there is an inflight TriggeringCLDeps{}", func() {
   732  					sup.pb.TriggeringClDeps, _ = sup.pb.COWTriggeringCLDeps(nil, []*prjpb.TriggeringCLDeps{
   733  						{OperationId: "op-1", OriginClid: clid4, DepClids: []int64{1, 2, 3}},
   734  					})
   735  					cls := do(&prjpb.Component{Clids: []int64{clid1, clid2, clid3, clid4}})
   736  					So(cls, ShouldHaveLength, 4)
   737  
   738  					// They all should not be cq-ready.
   739  					So(cls[clid1].cqReady, ShouldBeFalse)
   740  					So(cls[clid2].cqReady, ShouldBeFalse)
   741  					So(cls[clid3].cqReady, ShouldBeFalse)
   742  					So(cls[clid4].cqReady, ShouldBeFalse)
   743  				})
   744  			})
   745  		})
   746  	})
   747  }