go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/gerrit/trigger/trigger_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 trigger
    16  
    17  import (
    18  	"testing"
    19  	"time"
    20  
    21  	"google.golang.org/protobuf/types/known/timestamppb"
    22  
    23  	"go.chromium.org/luci/common/clock/testclock"
    24  	gerritpb "go.chromium.org/luci/common/proto/gerrit"
    25  
    26  	cfgpb "go.chromium.org/luci/cv/api/config/v2"
    27  	"go.chromium.org/luci/cv/internal/changelist"
    28  	"go.chromium.org/luci/cv/internal/gerrit/botdata"
    29  	gf "go.chromium.org/luci/cv/internal/gerrit/gerritfake"
    30  	"go.chromium.org/luci/cv/internal/run"
    31  
    32  	c "github.com/smartystreets/goconvey/convey"
    33  	la "go.chromium.org/luci/common/testing/assertions"
    34  )
    35  
    36  func TestHasAutoSubmit(t *testing.T) {
    37  	t.Parallel()
    38  
    39  	c.Convey("HasAutoSubmit", t, func() {
    40  		now := testclock.TestRecentTimeUTC
    41  		ci := &gerritpb.ChangeInfo{
    42  			Status:          gerritpb.ChangeStatus_NEW,
    43  			CurrentRevision: "deadbeef~1",
    44  			Revisions: map[string]*gerritpb.RevisionInfo{
    45  				"deadbeef~1": {
    46  					Number:  2,
    47  					Created: timestamppb.New(now.Add(-30 * time.Minute)),
    48  				},
    49  				"deadbeef~2": {
    50  					Number:  1,
    51  					Created: timestamppb.New(now.Add(-1 * time.Hour)),
    52  				},
    53  			},
    54  			Labels: map[string]*gerritpb.LabelInfo{
    55  				AutoSubmitLabelName: {
    56  					All: nil, // set in tests.
    57  				},
    58  			},
    59  		}
    60  		c.So(HasAutoSubmit(ci), c.ShouldBeFalse)
    61  		ci.Labels[AutoSubmitLabelName].All = []*gerritpb.ApprovalInfo{{
    62  			User:  gf.U("u-1"),
    63  			Value: modeToVote[run.FullRun],
    64  			Date:  timestamppb.New(now.Add(-15 * time.Minute)),
    65  		}}
    66  		c.So(HasAutoSubmit(ci), c.ShouldBeTrue)
    67  	})
    68  }
    69  
    70  func TestFindCQTrigger(t *testing.T) {
    71  	t.Parallel()
    72  
    73  	user1 := gf.U("u-1")
    74  	user2 := gf.U("u-2")
    75  	user3 := gf.U("u-3")
    76  	user4 := gf.U("u-4")
    77  	const dryRunVote = 1
    78  	const fullRunVote = 2
    79  	cg := &cfgpb.ConfigGroup{}
    80  	c.Convey("findCQTrigger", t, func() {
    81  		now := testclock.TestRecentTimeUTC
    82  		ci := &gerritpb.ChangeInfo{
    83  			Status:          gerritpb.ChangeStatus_NEW,
    84  			CurrentRevision: "deadbeef~1",
    85  			Revisions: map[string]*gerritpb.RevisionInfo{
    86  				"deadbeef~1": {
    87  					Number:  2,
    88  					Created: timestamppb.New(now.Add(-30 * time.Minute)),
    89  				},
    90  				"deadbeef~2": {
    91  					Number:  1,
    92  					Created: timestamppb.New(now.Add(-1 * time.Hour)),
    93  				},
    94  			},
    95  			Labels: map[string]*gerritpb.LabelInfo{
    96  				CQLabelName: {
    97  					All: nil, // set in tests.
    98  				},
    99  			},
   100  		}
   101  
   102  		c.Convey("Abandoned CL", func() {
   103  			ci.Status = gerritpb.ChangeStatus_ABANDONED
   104  			c.So(findCQTrigger(&FindInput{ChangeInfo: ci, ConfigGroup: cg}), c.ShouldBeNil)
   105  		})
   106  		c.Convey("Merged CL", func() {
   107  			ci.Status = gerritpb.ChangeStatus_MERGED
   108  			c.So(findCQTrigger(&FindInput{ChangeInfo: ci, ConfigGroup: cg}), c.ShouldBeNil)
   109  		})
   110  		c.Convey("No votes", func() {
   111  			c.So(findCQTrigger(&FindInput{ChangeInfo: ci, ConfigGroup: cg}), c.ShouldBeNil)
   112  		})
   113  		c.Convey("No Commit-Queue label info", func() {
   114  			ci.Labels = nil
   115  			c.So(findCQTrigger(&FindInput{ChangeInfo: ci, ConfigGroup: cg}), c.ShouldBeNil)
   116  		})
   117  		c.Convey("Single vote", func() {
   118  			ci.Labels[CQLabelName].All = []*gerritpb.ApprovalInfo{{
   119  				User:  user1,
   120  				Value: dryRunVote,
   121  				Date:  timestamppb.New(now.Add(-15 * time.Minute)),
   122  			}}
   123  			t := findCQTrigger(&FindInput{ChangeInfo: ci, ConfigGroup: cg})
   124  			c.So(t, la.ShouldResembleProto, &run.Trigger{
   125  				Time:            timestamppb.New(now.Add(-15 * time.Minute)),
   126  				Mode:            string(run.DryRun),
   127  				GerritAccountId: user1.GetAccountId(),
   128  				Email:           user1.GetEmail(),
   129  			})
   130  		})
   131  		c.Convey(">CQ+2 is clamped to CQ+2", func() {
   132  			ci.Labels[CQLabelName].All = []*gerritpb.ApprovalInfo{{
   133  				User:  user1,
   134  				Value: 3,
   135  				Date:  timestamppb.New(now.Add(-15 * time.Minute)),
   136  			}}
   137  			t := findCQTrigger(&FindInput{ChangeInfo: ci, ConfigGroup: cg})
   138  			c.So(t, la.ShouldResembleProto, &run.Trigger{
   139  				Time:            timestamppb.New(now.Add(-15 * time.Minute)),
   140  				Mode:            string(run.FullRun),
   141  				GerritAccountId: user1.GetAccountId(),
   142  				Email:           user1.GetEmail(),
   143  			})
   144  		})
   145  		c.Convey("Earliest votes wins", func() {
   146  			ci.Labels[CQLabelName].All = []*gerritpb.ApprovalInfo{
   147  				{
   148  					User:  user1,
   149  					Value: dryRunVote,
   150  					Date:  timestamppb.New(now.Add(-15 * time.Minute)),
   151  				},
   152  				{
   153  					User:  user2,
   154  					Value: dryRunVote,
   155  					Date:  timestamppb.New(now.Add(-5 * time.Minute)),
   156  				},
   157  			}
   158  			t := findCQTrigger(&FindInput{ChangeInfo: ci, ConfigGroup: cg})
   159  			c.So(t, la.ShouldResembleProto, &run.Trigger{
   160  				Time:            timestamppb.New(now.Add(-15 * time.Minute)),
   161  				Mode:            string(run.DryRun),
   162  				GerritAccountId: user1.GetAccountId(),
   163  				Email:           user1.GetEmail(),
   164  			})
   165  			c.Convey("except when some later run was canceled via botdata message", func() {
   166  				cancelMsg, err := botdata.Append("", botdata.BotData{
   167  					Action:      botdata.Cancel,
   168  					TriggeredAt: now.Add(-15 * time.Minute),
   169  					Revision:    ci.GetCurrentRevision(),
   170  				})
   171  				c.So(err, c.ShouldBeNil)
   172  				ci.Messages = append(ci.Messages, &gerritpb.ChangeMessageInfo{Message: cancelMsg})
   173  				t := findCQTrigger(&FindInput{ChangeInfo: ci, ConfigGroup: cg})
   174  				c.So(t, la.ShouldResembleProto, &run.Trigger{
   175  					Time:            timestamppb.New(now.Add(-5 * time.Minute)),
   176  					Mode:            string(run.DryRun),
   177  					GerritAccountId: user2.GetAccountId(),
   178  					Email:           user2.GetEmail(),
   179  				})
   180  			})
   181  		})
   182  		c.Convey("Earliest CQ+2 vote wins against even earlier CQ+1", func() {
   183  			ci.Labels[CQLabelName].All = []*gerritpb.ApprovalInfo{
   184  				{
   185  					User:  user1,
   186  					Value: dryRunVote,
   187  					Date:  timestamppb.New(now.Add(-15 * time.Minute)),
   188  				},
   189  				{
   190  					User:  user2,
   191  					Value: fullRunVote,
   192  					Date:  timestamppb.New(now.Add(-10 * time.Minute)),
   193  				},
   194  				{
   195  					User:  user3,
   196  					Value: dryRunVote,
   197  					Date:  timestamppb.New(now.Add(-5 * time.Minute)),
   198  				},
   199  				{
   200  					User:  user4,
   201  					Value: fullRunVote,
   202  					Date:  timestamppb.New(now.Add(-1 * time.Minute)),
   203  				},
   204  			}
   205  			t := findCQTrigger(&FindInput{ChangeInfo: ci, ConfigGroup: cg})
   206  			c.So(t, la.ShouldResembleProto, &run.Trigger{
   207  				Time:            timestamppb.New(now.Add(-10 * time.Minute)),
   208  				Mode:            string(run.FullRun),
   209  				GerritAccountId: user2.GetAccountId(),
   210  				Email:           user2.GetEmail(),
   211  			})
   212  		})
   213  		c.Convey("Sticky Vote bumps trigger time to cur revision creation time", func() {
   214  			ci.Labels[CQLabelName].All = []*gerritpb.ApprovalInfo{{
   215  				User:  user1,
   216  				Value: fullRunVote,
   217  				Date:  timestamppb.New(now.Add(-15 * time.Minute)),
   218  			}}
   219  			ci.CurrentRevision = "deadbeef"
   220  			ci.Revisions["deadbeef"] = &gerritpb.RevisionInfo{
   221  				Number:  3,
   222  				Created: timestamppb.New(now.Add(-10 * time.Minute)),
   223  			}
   224  			t := findCQTrigger(&FindInput{ChangeInfo: ci, ConfigGroup: cg})
   225  			c.So(t, la.ShouldResembleProto, &run.Trigger{
   226  				Time:            timestamppb.New(now.Add(-10 * time.Minute)),
   227  				Mode:            string(run.FullRun),
   228  				GerritAccountId: user1.GetAccountId(),
   229  				Email:           user1.GetEmail(),
   230  			})
   231  		})
   232  		c.Convey("Additional modes", func() {
   233  			const customLabel = "Custom"
   234  			const customRunMode = "CUSTOM_RUN"
   235  			cg.AdditionalModes = []*cfgpb.Mode{
   236  				{
   237  					Name:            customRunMode,
   238  					CqLabelValue:    +1,
   239  					TriggeringLabel: customLabel,
   240  					TriggeringValue: +1,
   241  				},
   242  			}
   243  			ci.Labels[CQLabelName].All = []*gerritpb.ApprovalInfo{
   244  				{
   245  					User:  user1,
   246  					Value: dryRunVote,
   247  					Date:  timestamppb.New(now.Add(-15 * time.Minute)),
   248  				},
   249  			}
   250  			ci.Labels[customLabel] = &gerritpb.LabelInfo{All: []*gerritpb.ApprovalInfo{
   251  				{
   252  					User:  user1,
   253  					Value: +1,
   254  					Date:  timestamppb.New(now.Add(-15 * time.Minute)),
   255  				},
   256  			}}
   257  			c.Convey("Simplest possible custom run", func() {
   258  				t := findCQTrigger(&FindInput{ChangeInfo: ci, ConfigGroup: cg})
   259  				c.So(t, la.ShouldResembleProto, &run.Trigger{
   260  					Time:            timestamppb.New(now.Add(-15 * time.Minute)),
   261  					Mode:            customRunMode,
   262  					ModeDefinition:  cg.AdditionalModes[0],
   263  					GerritAccountId: user1.GetAccountId(),
   264  					Email:           user1.GetEmail(),
   265  				})
   266  			})
   267  			c.Convey("Custom run despite other users' votes", func() {
   268  				ci.Labels[CQLabelName].All = []*gerritpb.ApprovalInfo{
   269  					{
   270  						User:  user1,
   271  						Value: dryRunVote,
   272  						Date:  timestamppb.New(now.Add(-15 * time.Minute)),
   273  					},
   274  					{
   275  						User:  user2,
   276  						Value: dryRunVote,
   277  						Date:  timestamppb.New(now.Add(-10 * time.Minute)),
   278  					},
   279  				}
   280  				ci.Labels[customLabel] = &gerritpb.LabelInfo{All: []*gerritpb.ApprovalInfo{
   281  					{
   282  						User:  user2,
   283  						Value: +2,
   284  						Date:  timestamppb.New(now.Add(-20 * time.Minute)),
   285  					},
   286  					{
   287  						User:  user1,
   288  						Value: +1,
   289  						Date:  timestamppb.New(now.Add(-15 * time.Minute)),
   290  					},
   291  				}}
   292  				t := findCQTrigger(&FindInput{ChangeInfo: ci, ConfigGroup: cg})
   293  				c.So(t.GetMode(), c.ShouldEqual, customRunMode)
   294  			})
   295  			c.Convey("Not applicable cases", func() {
   296  				c.Convey("Additional vote must have the same timestamp", func() {
   297  					c.Convey("before", func() {
   298  						ci.Labels[customLabel].GetAll()[0].Date.Seconds++
   299  						t := findCQTrigger(&FindInput{ChangeInfo: ci, ConfigGroup: cg})
   300  						c.So(t.GetMode(), c.ShouldEqual, string(run.DryRun))
   301  					})
   302  					c.Convey("after", func() {
   303  						ci.Labels[customLabel].GetAll()[0].Date.Seconds--
   304  						t := findCQTrigger(&FindInput{ChangeInfo: ci, ConfigGroup: cg})
   305  						c.So(t.GetMode(), c.ShouldEqual, string(run.DryRun))
   306  					})
   307  				})
   308  				c.Convey("Additional vote be from the same account", func() {
   309  					ci.Labels[customLabel].GetAll()[0].User = user2
   310  					t := findCQTrigger(&FindInput{ChangeInfo: ci, ConfigGroup: cg})
   311  					c.So(t.GetMode(), c.ShouldEqual, string(run.DryRun))
   312  				})
   313  				c.Convey("Additional vote must have expected value", func() {
   314  					ci.Labels[customLabel].GetAll()[0].Value = 100
   315  					t := findCQTrigger(&FindInput{ChangeInfo: ci, ConfigGroup: cg})
   316  					c.So(t.GetMode(), c.ShouldEqual, string(run.DryRun))
   317  				})
   318  				c.Convey("Additional vote must be for the correct label", func() {
   319  					ci.Labels[customLabel+"-Other"] = ci.Labels[customLabel]
   320  					delete(ci.Labels, customLabel)
   321  					t := findCQTrigger(&FindInput{ChangeInfo: ci, ConfigGroup: cg})
   322  					c.So(t.GetMode(), c.ShouldEqual, string(run.DryRun))
   323  				})
   324  				c.Convey("CQ vote must have correct value", func() {
   325  					ci.Labels[CQLabelName].GetAll()[0].Value = fullRunVote
   326  					t := findCQTrigger(&FindInput{ChangeInfo: ci, ConfigGroup: cg})
   327  					c.So(t.GetMode(), c.ShouldEqual, string(run.FullRun))
   328  				})
   329  			})
   330  		})
   331  	})
   332  }
   333  
   334  func TestFindNewPatchsetRunTrigger(t *testing.T) {
   335  	t.Parallel()
   336  	owner := gf.U("owner")
   337  	uploader1 := gf.U("ul-1")
   338  	uploader2 := gf.U("ul-2")
   339  	ts1 := testclock.TestRecentTimeUTC
   340  	ts2 := ts1.Add(30 * time.Minute)
   341  	// Create change info.
   342  	ci := &gerritpb.ChangeInfo{
   343  		Owner:           owner,
   344  		Status:          gerritpb.ChangeStatus_NEW,
   345  		CurrentRevision: "deadbeef~2",
   346  		Revisions: map[string]*gerritpb.RevisionInfo{
   347  			"deadbeef~1": {
   348  				Number:   1,
   349  				Uploader: uploader1,
   350  				Created:  timestamppb.New(ts1),
   351  			},
   352  			"deadbeef~2": {
   353  				Number:   2,
   354  				Uploader: uploader2,
   355  				Created:  timestamppb.New(ts2),
   356  			},
   357  		},
   358  	}
   359  
   360  	// Create empty config.
   361  	cg := &cfgpb.ConfigGroup{}
   362  
   363  	// Create CLEntity.
   364  	cle := &changelist.CL{
   365  		TriggerNewPatchsetRunAfterPS: 1,
   366  	}
   367  
   368  	c.Convey("findNewPatchsetRun", t, func() {
   369  		c.Convey("Not configured to trigger new patchset runs", func() {
   370  			c.So(
   371  				findNewPatchsetRunTrigger(&FindInput{ChangeInfo: ci, ConfigGroup: cg, TriggerNewPatchsetRunAfterPS: cle.TriggerNewPatchsetRunAfterPS}),
   372  				c.ShouldBeNil,
   373  			)
   374  		})
   375  		c.Convey("Configured", func() {
   376  			// Add npr builder to config.
   377  			cg.Verifiers = &cfgpb.Verifiers{
   378  				Tryjob: &cfgpb.Verifiers_Tryjob{
   379  					Builders: []*cfgpb.Verifiers_Tryjob_Builder{
   380  						{
   381  							Name:          "builder_name",
   382  							ModeAllowlist: []string{string(run.NewPatchsetRun)},
   383  						},
   384  					},
   385  				},
   386  			}
   387  			c.Convey("Current patchset not ended", func() {
   388  				c.So(
   389  					findNewPatchsetRunTrigger(&FindInput{ChangeInfo: ci, ConfigGroup: cg, TriggerNewPatchsetRunAfterPS: cle.TriggerNewPatchsetRunAfterPS}),
   390  					la.ShouldResembleProto,
   391  					&run.Trigger{
   392  						Mode:            string(run.NewPatchsetRun),
   393  						Time:            timestamppb.New(ts2),
   394  						Email:           owner.GetEmail(),
   395  						GerritAccountId: owner.GetAccountId(),
   396  					},
   397  				)
   398  			})
   399  			c.Convey("Current patchset already ended", func() {
   400  				// Set CLEntity NewPatchsetUploaded
   401  				cle.TriggerNewPatchsetRunAfterPS = 2
   402  				c.So(
   403  					findNewPatchsetRunTrigger(&FindInput{ChangeInfo: ci, ConfigGroup: cg, TriggerNewPatchsetRunAfterPS: cle.TriggerNewPatchsetRunAfterPS}),
   404  					c.ShouldBeNil,
   405  				)
   406  			})
   407  		})
   408  	})
   409  }