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

     1  // Copyright 2023 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  	"testing"
    20  
    21  	cfgpb "go.chromium.org/luci/cv/api/config/v2"
    22  	"go.chromium.org/luci/cv/internal/changelist"
    23  	"go.chromium.org/luci/cv/internal/configs/prjcfg"
    24  	"go.chromium.org/luci/cv/internal/cvtesting"
    25  	"go.chromium.org/luci/cv/internal/prjmanager/prjpb"
    26  	"go.chromium.org/luci/cv/internal/run"
    27  
    28  	. "github.com/smartystreets/goconvey/convey"
    29  	. "go.chromium.org/luci/common/testing/assertions"
    30  )
    31  
    32  type testCLInfo clInfo
    33  
    34  func (ci *testCLInfo) Deps(deps ...*testCLInfo) *testCLInfo {
    35  	for _, dep := range deps {
    36  		ci.pcl.Deps = append(ci.pcl.Deps, &changelist.Dep{
    37  			Clid: dep.Clid(),
    38  			Kind: changelist.DepKind_HARD,
    39  		})
    40  	}
    41  	return ci
    42  }
    43  
    44  func (ci *testCLInfo) SoftDeps(deps ...*testCLInfo) *testCLInfo {
    45  	for _, dep := range deps {
    46  		ci.pcl.Deps = append(ci.pcl.Deps, &changelist.Dep{
    47  			Clid: dep.Clid(),
    48  			Kind: changelist.DepKind_SOFT,
    49  		})
    50  	}
    51  	return ci
    52  }
    53  
    54  func (ci *testCLInfo) CQ(val int) *testCLInfo {
    55  	switch val {
    56  	case 0:
    57  		ci.pcl.Triggers = nil
    58  	case 1:
    59  		ci.pcl.Triggers = &run.Triggers{
    60  			CqVoteTrigger: &run.Trigger{
    61  				Mode: string(run.DryRun),
    62  			},
    63  		}
    64  	case 2:
    65  		ci.pcl.Triggers = &run.Triggers{
    66  			CqVoteTrigger: &run.Trigger{
    67  				Mode: string(run.FullRun),
    68  			},
    69  		}
    70  	default:
    71  		panic(fmt.Errorf("unsupported CQ value"))
    72  	}
    73  	return ci
    74  }
    75  
    76  func (ci *testCLInfo) triageDeps(cls map[int64]*clInfo) {
    77  	mode := ci.pcl.GetTriggers().GetCqVoteTrigger().GetMode()
    78  	if mode != string(run.FullRun) {
    79  		return
    80  	}
    81  	ci.deps.needToTrigger = ci.deps.needToTrigger[:0]
    82  	for _, dep := range ci.pcl.GetDeps() {
    83  		dci, ok := cls[dep.GetClid()]
    84  		if !ok {
    85  			ci.deps.notYetLoaded = append(ci.deps.notYetLoaded, &changelist.Dep{
    86  				Clid: dep.GetClid(),
    87  				Kind: changelist.DepKind_HARD,
    88  			})
    89  			continue
    90  		}
    91  		depMode := dci.pcl.GetTriggers().GetCqVoteTrigger().GetMode()
    92  		if mode != depMode {
    93  			ci.deps.needToTrigger = append(ci.deps.needToTrigger, &changelist.Dep{
    94  				Clid: dep.GetClid(),
    95  				Kind: changelist.DepKind_HARD,
    96  			})
    97  		}
    98  	}
    99  }
   100  
   101  func (ci *testCLInfo) Clid() int64 {
   102  	return ci.pcl.GetClid()
   103  }
   104  
   105  func (ci *testCLInfo) NeedToTrigger() []int64 {
   106  	var ret []int64
   107  	if ci.deps == nil {
   108  		return ret
   109  	}
   110  	for _, dep := range ci.deps.needToTrigger {
   111  		ret = append(ret, dep.GetClid())
   112  	}
   113  	return ret
   114  }
   115  
   116  func (ci *testCLInfo) SetPurgingCL() *testCLInfo {
   117  	ci.purgingCL = &prjpb.PurgingCL{}
   118  	return ci
   119  }
   120  
   121  func (ci *testCLInfo) SetPurgeReasons() *testCLInfo {
   122  	ci.purgeReasons = []*prjpb.PurgeReason{{}}
   123  	return ci
   124  }
   125  
   126  func (ci *testCLInfo) SetIncompleteRun(m run.Mode) *testCLInfo {
   127  	ci.runIndexes = []int32{1}
   128  	ci.runCountByMode[m]++
   129  	return ci
   130  }
   131  
   132  func (ci *testCLInfo) SetTriggeringCLDeps() *testCLInfo {
   133  	ci.triggeringCLDeps = &prjpb.TriggeringCLDeps{}
   134  	return ci
   135  }
   136  
   137  func (ci *testCLInfo) Outdated() *testCLInfo {
   138  	ci.pcl.Outdated = &changelist.Snapshot_Outdated{}
   139  	return ci
   140  }
   141  
   142  func TestStageTriggerCLDeps(t *testing.T) {
   143  	t.Parallel()
   144  
   145  	Convey("stargeTriggerCLDeps", t, func() {
   146  		ct := cvtesting.Test{}
   147  		ctx, cancel := ct.SetUp(t)
   148  		defer cancel()
   149  
   150  		cq2 := &run.Trigger{Mode: string(run.FullRun)}
   151  		cls := make(map[int64]*clInfo)
   152  		nextCLID := int64(1)
   153  		newCL := func() *testCLInfo {
   154  			defer func() { nextCLID++ }()
   155  			tci := &testCLInfo{
   156  				pcl: &prjpb.PCL{
   157  					Clid:               nextCLID,
   158  					ConfigGroupIndexes: []int32{0},
   159  				},
   160  				triagedCL: triagedCL{
   161  					deps: &triagedDeps{},
   162  				},
   163  				runCountByMode: make(map[run.Mode]int),
   164  			}
   165  			cls[nextCLID] = (*clInfo)(tci)
   166  			return tci
   167  		}
   168  		triageDeps := func(cis ...*testCLInfo) {
   169  			for _, ci := range cis {
   170  				ci.triageDeps(cls)
   171  			}
   172  		}
   173  		sup := &simplePMState{
   174  			pb: &prjpb.PState{},
   175  			cgs: []*prjcfg.ConfigGroup{
   176  				{ID: "hash/cg1", Content: &cfgpb.ConfigGroup{}},
   177  			},
   178  		}
   179  		pm := pmState{sup}
   180  
   181  		Convey("CLs without deps", func() {
   182  			cl1 := newCL().CQ(0)
   183  			cl2 := newCL().CQ(+2)
   184  			triageDeps(cl1, cl2)
   185  			So(cl1.NeedToTrigger(), ShouldBeNil)
   186  			So(cl2.NeedToTrigger(), ShouldBeNil)
   187  			So(stageTriggerCLDeps(ctx, cls, pm), ShouldHaveLength, 0)
   188  		})
   189  
   190  		Convey("CL with deps", func() {
   191  			cl1 := newCL()
   192  			cl2 := newCL().Deps(cl1)
   193  			cl3 := newCL().Deps(cl1, cl2)
   194  
   195  			Convey("no deps have CQ vote", func() {
   196  				cl3 = cl3.CQ(+2)
   197  				triageDeps(cl1, cl2, cl3)
   198  				So(cl3.NeedToTrigger(), ShouldEqual, []int64{cl1.Clid(), cl2.Clid()})
   199  				So(stageTriggerCLDeps(ctx, cls, pm), ShouldResembleProto, []*prjpb.TriggeringCLDeps{
   200  					{
   201  						OriginClid:      cl3.Clid(),
   202  						DepClids:        []int64{cl1.Clid(), cl2.Clid()},
   203  						Trigger:         cq2,
   204  						ConfigGroupName: "cg1",
   205  					},
   206  				})
   207  
   208  				Convey("unless outdated", func() {
   209  					Convey("the origin CL", func() {
   210  						cl3 = cl3.Outdated()
   211  					})
   212  					Convey("a dep CL", func() {
   213  						cl2 = cl2.Outdated()
   214  					})
   215  					triageDeps(cl1, cl2, cl3)
   216  					So(cl3.NeedToTrigger(), ShouldEqual, []int64{cl1.Clid(), cl2.Clid()})
   217  					So(stageTriggerCLDeps(ctx, cls, pm), ShouldBeNil)
   218  				})
   219  
   220  				Convey("retriaging it should be noop", func() {
   221  					// Now, cl3 has TriggeringCLDeps, created by the previous
   222  					// stageTriggerCLDeps(), and let's say that cl2 has voted.
   223  					cl2 = cl2.CQ(+2)
   224  					cl3 = cl3.SetTriggeringCLDeps()
   225  					triageDeps(cl1, cl2, cl3)
   226  
   227  					// Now, triageDeps declares that both cl2 and cl3 have
   228  					// unvoted deps, but none of them should schedule a new
   229  					// task.
   230  					So(cl2.NeedToTrigger(), ShouldEqual, []int64{cl1.Clid()})
   231  					So(cl3.NeedToTrigger(), ShouldEqual, []int64{cl1.Clid()})
   232  					So(stageTriggerCLDeps(ctx, cls, pm), ShouldBeNil)
   233  				})
   234  
   235  				Convey("unless a dep was not loaded yet", func() {
   236  					delete(cls, cl1.pcl.GetClid())
   237  					triageDeps(cl1, cl2, cl3)
   238  					So(stageTriggerCLDeps(ctx, cls, pm), ShouldHaveLength, 0)
   239  				})
   240  			})
   241  			Convey("all deps have CQ vote", func() {
   242  				cl1 = cl1.CQ(+2)
   243  				cl2 = cl2.CQ(+2)
   244  				cl3 = cl3.CQ(+2)
   245  				triageDeps(cl1, cl2, cl3)
   246  				So(cl3.NeedToTrigger(), ShouldBeNil)
   247  				So(stageTriggerCLDeps(ctx, cls, pm), ShouldHaveLength, 0)
   248  			})
   249  			Convey("some deps have and some others don't have CQ votes", func() {
   250  				cl2 = cl2.CQ(+2)
   251  				cl3 = cl3.CQ(+2)
   252  				triageDeps(cl1, cl2, cl3)
   253  				// Both cl2.deps and cl3.deps have cl1 in needToTrigger, but
   254  				// TriggerCLDeps{} should be created for cl3 only.
   255  				So(cl2.NeedToTrigger(), ShouldEqual, []int64{cl1.Clid()})
   256  				So(cl3.NeedToTrigger(), ShouldEqual, []int64{cl1.Clid()})
   257  				So(stageTriggerCLDeps(ctx, cls, pm), ShouldResembleProto, []*prjpb.TriggeringCLDeps{
   258  					{
   259  						OriginClid:      cl3.Clid(),
   260  						DepClids:        []int64{cl1.Clid()},
   261  						Trigger:         cq2,
   262  						ConfigGroupName: "cg1",
   263  					},
   264  				})
   265  			})
   266  		})
   267  
   268  		Convey("with inflight purges", func() {
   269  			cl1 := newCL()
   270  			cl2 := newCL().Deps(cl1)
   271  			cl3 := newCL().Deps(cl1, cl2)
   272  
   273  			Convey("PurgingCL on the originating CL", func() {
   274  				cl3 = cl3.CQ(+2).SetPurgingCL()
   275  				triageDeps(cl1, cl2, cl3)
   276  				So(cl2.NeedToTrigger(), ShouldBeNil)
   277  				So(cl3.NeedToTrigger(), ShouldEqual, []int64{cl1.Clid(), cl2.Clid()})
   278  			})
   279  			Convey("PurgingCL on a parent CL", func() {
   280  				cl2 = cl2.CQ(+2).SetPurgingCL()
   281  				cl3 = cl3.CQ(+2)
   282  				triageDeps(cl1, cl2, cl3)
   283  				So(cl2.NeedToTrigger(), ShouldEqual, []int64{cl1.Clid()})
   284  				So(cl3.NeedToTrigger(), ShouldEqual, []int64{cl1.Clid()})
   285  			})
   286  			Convey("purgeReasons on the originating CL", func() {
   287  				cl3 = cl3.CQ(+2).SetPurgeReasons()
   288  				triageDeps(cl1, cl2, cl3)
   289  				So(cl2.NeedToTrigger(), ShouldBeNil)
   290  				So(cl3.NeedToTrigger(), ShouldEqual, []int64{cl1.Clid(), cl2.Clid()})
   291  			})
   292  			Convey("purgeReasons on a parent CL", func() {
   293  				cl2 = cl2.CQ(+2).SetPurgeReasons()
   294  				cl3 = cl3.CQ(+2)
   295  				triageDeps(cl1, cl2, cl3)
   296  				So(cl2.NeedToTrigger(), ShouldEqual, []int64{cl1.Clid()})
   297  				So(cl3.NeedToTrigger(), ShouldEqual, []int64{cl1.Clid()})
   298  			})
   299  			So(stageTriggerCLDeps(ctx, cls, pm), ShouldHaveLength, 0)
   300  		})
   301  
   302  		Convey("with inflight TriggeringCLDeps", func() {
   303  			cl1 := newCL()
   304  			cl2 := newCL().Deps(cl1)
   305  			cl3 := newCL().Deps(cl1, cl2)
   306  
   307  			Convey("TriggeringCLDeps on the originating CL", func() {
   308  				cl3 = cl3.CQ(+2).SetTriggeringCLDeps()
   309  				triageDeps(cl1, cl2, cl3)
   310  				So(cl2.NeedToTrigger(), ShouldBeNil)
   311  				So(cl3.NeedToTrigger(), ShouldEqual, []int64{cl1.Clid(), cl2.Clid()})
   312  			})
   313  			Convey("TriggeringCLDeps on a parent CL", func() {
   314  				cl2 = cl2.CQ(+2).SetTriggeringCLDeps()
   315  				cl3 = cl3.CQ(+2)
   316  				triageDeps(cl1, cl2, cl3)
   317  				So(cl2.NeedToTrigger(), ShouldEqual, []int64{cl1.Clid()})
   318  				So(cl3.NeedToTrigger(), ShouldEqual, []int64{cl1.Clid()})
   319  			})
   320  			So(stageTriggerCLDeps(ctx, cls, pm), ShouldHaveLength, 0)
   321  		})
   322  
   323  		Convey("with incomplete run", func() {
   324  			cl1 := newCL()
   325  			cl2 := newCL().Deps(cl1)
   326  			cl3 := newCL().Deps(cl1, cl2)
   327  			cl4 := newCL().Deps(cl1, cl2, cl3)
   328  
   329  			Convey("incomplete run with the same CQ vote in all the CLs", func() {
   330  				cl1 = cl1.CQ(+2).SetIncompleteRun(run.FullRun)
   331  				cl2 = cl2.CQ(+2).SetIncompleteRun(run.FullRun)
   332  				cl3 = cl3.CQ(+2).SetIncompleteRun(run.FullRun)
   333  
   334  				triageDeps(cl1, cl2, cl3, cl4)
   335  				So(cl1.NeedToTrigger(), ShouldBeNil)
   336  				So(cl2.NeedToTrigger(), ShouldBeNil)
   337  				So(cl3.NeedToTrigger(), ShouldBeNil)
   338  				So(cl4.NeedToTrigger(), ShouldBeNil)
   339  				So(stageTriggerCLDeps(ctx, cls, pm), ShouldHaveLength, 0)
   340  			})
   341  
   342  			Convey("incomplete run with different CQVotes in deps", func() {
   343  				cl1 = cl1.CQ(+0).SetIncompleteRun(run.NewPatchsetRun)
   344  				cl2 = cl2.CQ(+1).SetIncompleteRun(run.DryRun)
   345  				cl3 = cl3.CQ(+2).SetIncompleteRun(run.FullRun)
   346  
   347  				triageDeps(cl1, cl2, cl3, cl4)
   348  				So(cl1.NeedToTrigger(), ShouldBeNil)
   349  				So(cl2.NeedToTrigger(), ShouldBeNil)
   350  				So(cl3.NeedToTrigger(), ShouldEqual, []int64{cl1.pcl.GetClid(), cl2.pcl.GetClid()})
   351  				So(cl4.NeedToTrigger(), ShouldBeNil)
   352  				So(stageTriggerCLDeps(ctx, cls, pm), ShouldHaveLength, 0)
   353  			})
   354  
   355  			Convey("incomplete run on parent CLs", func() {
   356  				// This happen, where a child CL receives CQ+2, while its
   357  				// parents are running.
   358  				cl1 = cl1.CQ(+2).SetIncompleteRun(run.FullRun)
   359  				cl2 = cl2.CQ(+2).SetIncompleteRun(run.FullRun)
   360  				cl3 = cl3.CQ(+2)
   361  				cl4 = cl3.CQ(0)
   362  
   363  				triageDeps(cl1, cl2, cl3, cl4)
   364  				So(cl1.NeedToTrigger(), ShouldBeNil)
   365  				So(cl2.NeedToTrigger(), ShouldBeNil)
   366  				So(cl3.NeedToTrigger(), ShouldBeNil)
   367  				So(cl4.NeedToTrigger(), ShouldBeNil)
   368  				So(stageTriggerCLDeps(ctx, cls, pm), ShouldHaveLength, 0)
   369  			})
   370  
   371  			Convey("MCE over MCE", func() {
   372  				// Similar to "incomplete run on parent CLs", but with another
   373  				// CL between.
   374  				cl1 = cl1.CQ(+2).SetIncompleteRun(run.FullRun)
   375  				cl2 = cl2.CQ(+2).SetIncompleteRun(run.FullRun)
   376  				cl3 = cl3.CQ(0)
   377  				cl4 = cl4.CQ(+2)
   378  
   379  				triageDeps(cl1, cl2, cl3, cl4)
   380  				So(cl1.NeedToTrigger(), ShouldBeNil)
   381  				So(cl2.NeedToTrigger(), ShouldBeNil)
   382  				So(cl3.NeedToTrigger(), ShouldBeNil)
   383  				So(cl4.NeedToTrigger(), ShouldEqual, []int64{cl3.Clid()})
   384  				So(stageTriggerCLDeps(ctx, cls, pm), ShouldResembleProto, []*prjpb.TriggeringCLDeps{
   385  					{
   386  						OriginClid:      cl4.Clid(),
   387  						DepClids:        []int64{cl3.Clid()},
   388  						Trigger:         cq2,
   389  						ConfigGroupName: "cg1",
   390  					},
   391  				})
   392  			})
   393  
   394  			Convey("MCE over MCE with a mix of incomplete and complete runs", func() {
   395  				// Similar to "incomplete run on parent CLs", but with another
   396  				// CL between.
   397  				cl1 = cl1.CQ(+2)
   398  				cl2 = cl2.CQ(+2).SetIncompleteRun(run.FullRun)
   399  				cl3 = cl3.CQ(0)
   400  				cl4 = cl4.CQ(+2)
   401  
   402  				triageDeps(cl1, cl2, cl3, cl4)
   403  				So(cl1.NeedToTrigger(), ShouldBeNil)
   404  				So(cl2.NeedToTrigger(), ShouldBeNil)
   405  				So(cl3.NeedToTrigger(), ShouldBeNil)
   406  				So(cl4.NeedToTrigger(), ShouldEqual, []int64{cl3.Clid()})
   407  				So(stageTriggerCLDeps(ctx, cls, pm), ShouldResembleProto, []*prjpb.TriggeringCLDeps{
   408  					{
   409  						OriginClid:      cl4.Clid(),
   410  						DepClids:        []int64{cl3.Clid()},
   411  						Trigger:         cq2,
   412  						ConfigGroupName: "cg1",
   413  					},
   414  				})
   415  			})
   416  		})
   417  	})
   418  }