go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/prjmanager/triager/runs_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  	"testing"
    19  
    20  	"go.chromium.org/luci/auth/identity"
    21  	gerritpb "go.chromium.org/luci/common/proto/gerrit"
    22  
    23  	cfgpb "go.chromium.org/luci/cv/api/config/v2"
    24  	"go.chromium.org/luci/cv/internal/changelist"
    25  	"go.chromium.org/luci/cv/internal/cvtesting"
    26  	gf "go.chromium.org/luci/cv/internal/gerrit/gerritfake"
    27  	"go.chromium.org/luci/cv/internal/gerrit/trigger"
    28  	"go.chromium.org/luci/cv/internal/prjmanager/prjpb"
    29  	"go.chromium.org/luci/cv/internal/run"
    30  
    31  	. "github.com/smartystreets/goconvey/convey"
    32  	. "go.chromium.org/luci/common/testing/assertions"
    33  )
    34  
    35  func TestFindImmediateHardDeps(t *testing.T) {
    36  	t.Parallel()
    37  
    38  	Convey("findImmediateHardDeps", t, func() {
    39  		ct := cvtesting.Test{}
    40  		_, cancel := ct.SetUp(t)
    41  		defer cancel()
    42  
    43  		cls := make(map[int64]*clInfo)
    44  		nextCLID := int64(1)
    45  		spm := &simplePMState{
    46  			pb: &prjpb.PState{},
    47  		}
    48  		newCL := func() *testCLInfo {
    49  			defer func() { nextCLID++ }()
    50  			tci := &testCLInfo{
    51  				pcl: &prjpb.PCL{
    52  					Clid: nextCLID,
    53  				},
    54  			}
    55  			cls[nextCLID] = (*clInfo)(tci)
    56  			spm.pb.Pcls = append(spm.pb.Pcls, tci.pcl)
    57  			return tci
    58  		}
    59  		rs := &runStage{
    60  			pm:  pmState{spm},
    61  			cls: cls,
    62  		}
    63  		CLIDs := func(pcls ...*prjpb.PCL) []int64 {
    64  			var ret []int64
    65  			for _, pcl := range pcls {
    66  				ret = append(ret, pcl.GetClid())
    67  			}
    68  			return ret
    69  		}
    70  
    71  		Convey("without deps", func() {
    72  			cl1 := newCL()
    73  			cl2 := newCL()
    74  			So(rs.findImmediateHardDeps(cl1.pcl), ShouldEqual, CLIDs())
    75  			So(rs.findImmediateHardDeps(cl2.pcl), ShouldEqual, CLIDs())
    76  		})
    77  
    78  		Convey("with HARD deps", func() {
    79  			Convey("one immediate parent", func() {
    80  				cl1 := newCL()
    81  				cl2 := newCL().Deps(cl1)
    82  				cl3 := newCL().Deps(cl1, cl2)
    83  
    84  				So(rs.findImmediateHardDeps(cl1.pcl), ShouldEqual, CLIDs())
    85  				So(rs.findImmediateHardDeps(cl2.pcl), ShouldEqual, CLIDs(cl1.pcl))
    86  				So(rs.findImmediateHardDeps(cl3.pcl), ShouldEqual, CLIDs(cl2.pcl))
    87  			})
    88  
    89  			Convey("more than one immediate parents", func() {
    90  				// stack 1
    91  				s1cl1 := newCL()
    92  				s1cl2 := newCL().Deps(s1cl1)
    93  				So(rs.findImmediateHardDeps(s1cl1.pcl), ShouldEqual, CLIDs())
    94  				So(rs.findImmediateHardDeps(s1cl2.pcl), ShouldEqual, CLIDs(s1cl1.pcl))
    95  				// stack 2
    96  				s2cl1 := newCL()
    97  				s2cl2 := newCL().Deps(s2cl1)
    98  				So(rs.findImmediateHardDeps(s2cl1.pcl), ShouldEqual, CLIDs())
    99  				So(rs.findImmediateHardDeps(s2cl2.pcl), ShouldEqual, CLIDs(s2cl1.pcl))
   100  
   101  				// cl3 belongs to both stack 1 and 2.
   102  				// both s1cl2 and s2cl2 should be its immediate parents.
   103  				cl3 := newCL().Deps(s1cl1, s1cl2, s2cl1, s2cl2)
   104  				So(rs.findImmediateHardDeps(cl3.pcl), ShouldEqual, CLIDs(s1cl2.pcl, s2cl2.pcl))
   105  			})
   106  		})
   107  
   108  		Convey("with SOFT deps", func() {
   109  			cl1 := newCL()
   110  			cl2 := newCL().SoftDeps(cl1)
   111  			cl3 := newCL().SoftDeps(cl1, cl2)
   112  
   113  			So(rs.findImmediateHardDeps(cl1.pcl), ShouldEqual, CLIDs())
   114  			So(rs.findImmediateHardDeps(cl2.pcl), ShouldEqual, CLIDs())
   115  			So(rs.findImmediateHardDeps(cl3.pcl), ShouldEqual, CLIDs())
   116  		})
   117  
   118  		Convey("with a mix of HARD and SOFT deps", func() {
   119  			cl1 := newCL()
   120  			clA := newCL()
   121  			cl2 := newCL().Deps(cl1)
   122  			cl3 := newCL().Deps(cl1, cl2).SoftDeps(clA)
   123  			cl4 := newCL().Deps(cl1, cl2, cl3)
   124  
   125  			So(rs.findImmediateHardDeps(cl1.pcl), ShouldEqual, CLIDs())
   126  			So(rs.findImmediateHardDeps(cl2.pcl), ShouldEqual, CLIDs(cl1.pcl))
   127  			So(rs.findImmediateHardDeps(cl3.pcl), ShouldEqual, CLIDs(cl2.pcl))
   128  			So(rs.findImmediateHardDeps(cl4.pcl), ShouldEqual, CLIDs(cl3.pcl))
   129  		})
   130  	})
   131  }
   132  
   133  func TestQuotaPayer(t *testing.T) {
   134  	t.Parallel()
   135  
   136  	Convey("quotaPayer", t, func() {
   137  		cl := &changelist.CL{
   138  			ID: 1234,
   139  			Snapshot: &changelist.Snapshot{
   140  				Kind: &changelist.Snapshot_Gerrit{
   141  					Gerrit: &changelist.Gerrit{
   142  						Info: &gerritpb.ChangeInfo{
   143  							Labels: make(map[string]*gerritpb.LabelInfo),
   144  						},
   145  					},
   146  				},
   147  			},
   148  		}
   149  		owner := identity.Identity("user:user-1@example.com")
   150  		trigg := identity.Identity("user:user-2@example.com")
   151  		tr := &run.Trigger{}
   152  
   153  		setAutoSubmit := func(cl *changelist.CL, val bool) {
   154  			labels := cl.Snapshot.GetGerrit().GetInfo().Labels
   155  			if !val {
   156  				delete(labels, trigger.AutoSubmitLabelName)
   157  			} else {
   158  				labels[trigger.AutoSubmitLabelName] = &gerritpb.LabelInfo{
   159  					All: []*gerritpb.ApprovalInfo{{
   160  						User:  gf.U(owner.Value()),
   161  						Value: 1,
   162  					}},
   163  				}
   164  			}
   165  		}
   166  
   167  		Convey("panics", func() {
   168  			var expected string
   169  			Convey("if owner is empty", func() {
   170  				owner = ""
   171  				expected = "empty owner"
   172  			})
   173  			Convey("if owner is annoymous", func() {
   174  				owner = identity.AnonymousIdentity
   175  				expected = "the CL owner is anonymous"
   176  			})
   177  			So(func() { quotaPayer(cl, owner, trigg, tr) }, ShouldPanicLike, expected)
   178  		})
   179  
   180  		Convey("with NewPatchsetRun", func() {
   181  			// must be the owner always.
   182  			tr.Mode = string(run.NewPatchsetRun)
   183  			// the owner is the triggerer in NPR.
   184  			trigg = owner
   185  
   186  			setAutoSubmit(cl, true)
   187  			So(quotaPayer(cl, owner, trigg, tr), ShouldEqual, trigg)
   188  			setAutoSubmit(cl, false)
   189  			So(quotaPayer(cl, owner, trigg, tr), ShouldEqual, trigg)
   190  		})
   191  
   192  		Convey("with Dry-Run", func() {
   193  			Convey("by the StandardMode with AS", func() {
   194  				tr.Mode = string(run.DryRun)
   195  				setAutoSubmit(cl, true)
   196  			})
   197  			Convey("by the StandardMode without AS", func() {
   198  				tr.Mode = string(run.DryRun)
   199  				setAutoSubmit(cl, false)
   200  			})
   201  			Convey("by the CustomMode with AS", func() {
   202  				tr.Mode = "custom-mode"
   203  				tr.ModeDefinition = &cfgpb.Mode{
   204  					Name:            "custom-mode",
   205  					CqLabelValue:    trigger.CQVoteByMode(run.DryRun),
   206  					TriggeringLabel: "custom-label",
   207  				}
   208  				setAutoSubmit(cl, true)
   209  			})
   210  			Convey("by the CustomMode without AS", func() {
   211  				tr.Mode = "custom-mode"
   212  				tr.ModeDefinition = &cfgpb.Mode{
   213  					Name:            "custom-mode",
   214  					CqLabelValue:    trigger.CQVoteByMode(run.DryRun),
   215  					TriggeringLabel: "custom-label",
   216  				}
   217  				setAutoSubmit(cl, false)
   218  			})
   219  			// Must be the triggerer always.
   220  			So(quotaPayer(cl, owner, trigg, tr), ShouldEqual, trigg)
   221  		})
   222  		Convey("with Full-Run", func() {
   223  			var expected identity.Identity
   224  			Convey("by the StandardMode with AS", func() {
   225  				tr.Mode = string(run.FullRun)
   226  				setAutoSubmit(cl, true)
   227  				expected = owner
   228  			})
   229  			Convey("by the StandardMode without AS", func() {
   230  				tr.Mode = string(run.FullRun)
   231  				setAutoSubmit(cl, false)
   232  				expected = trigg
   233  			})
   234  			Convey("by the CustomMode with AS", func() {
   235  				tr.Mode = "custom-mode"
   236  				tr.ModeDefinition = &cfgpb.Mode{
   237  					Name:            "custom-mode",
   238  					CqLabelValue:    trigger.CQVoteByMode(run.FullRun),
   239  					TriggeringLabel: "custom-label",
   240  				}
   241  				setAutoSubmit(cl, true)
   242  				expected = owner
   243  			})
   244  			Convey("by the CustomMode without AS", func() {
   245  				tr.Mode = "custom-mode"
   246  				tr.ModeDefinition = &cfgpb.Mode{
   247  					Name:            "custom-mode",
   248  					CqLabelValue:    trigger.CQVoteByMode(run.FullRun),
   249  					TriggeringLabel: "custom-label",
   250  				}
   251  				setAutoSubmit(cl, false)
   252  				expected = trigg
   253  			})
   254  			So(quotaPayer(cl, owner, trigg, tr), ShouldEqual, expected)
   255  		})
   256  	})
   257  }