go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/prjmanager/state/partition_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 state
    16  
    17  import (
    18  	"testing"
    19  	"time"
    20  
    21  	"go.chromium.org/luci/cv/internal/changelist"
    22  	"go.chromium.org/luci/cv/internal/common"
    23  	"go.chromium.org/luci/cv/internal/cvtesting"
    24  	"go.chromium.org/luci/cv/internal/prjmanager/prjpb"
    25  	"go.chromium.org/luci/cv/internal/run"
    26  	"google.golang.org/protobuf/types/known/timestamppb"
    27  
    28  	. "github.com/smartystreets/goconvey/convey"
    29  	. "go.chromium.org/luci/common/testing/assertions"
    30  )
    31  
    32  func TestRepartition(t *testing.T) {
    33  	t.Parallel()
    34  
    35  	Convey("repartition works", t, func() {
    36  		state := &State{PB: &prjpb.PState{
    37  			RepartitionRequired: true,
    38  		}}
    39  		cat := &categorizedCLs{
    40  			active:   common.CLIDsSet{},
    41  			deps:     common.CLIDsSet{},
    42  			unused:   common.CLIDsSet{},
    43  			unloaded: common.CLIDsSet{},
    44  		}
    45  
    46  		defer func() {
    47  			// Assert guarantees of repartition()
    48  			So(state.PB.GetRepartitionRequired(), ShouldBeFalse)
    49  			So(state.PB.GetCreatedPruns(), ShouldBeNil)
    50  			actual := state.pclIndex
    51  			state.pclIndex = nil
    52  			state.ensurePCLIndex()
    53  			So(actual, ShouldResemble, state.pclIndex)
    54  		}()
    55  
    56  		Convey("nothing to do, except resetting RepartitionRequired", func() {
    57  			Convey("totally empty", func() {
    58  				state.repartition(cat)
    59  				So(state.PB, ShouldResembleProto, &prjpb.PState{})
    60  			})
    61  			Convey("1 active CL in 1 component", func() {
    62  				cat.active.ResetI64(1)
    63  				state.PB.Components = []*prjpb.Component{{Clids: []int64{1}}}
    64  				state.PB.Pcls = []*prjpb.PCL{{Clid: 1}}
    65  				pb := backupPB(state)
    66  
    67  				state.repartition(cat)
    68  				pb.RepartitionRequired = false
    69  				So(state.PB, ShouldResembleProto, pb)
    70  			})
    71  			Convey("1 active CL in 1 component needing triage with 1 Run", func() {
    72  				cat.active.ResetI64(1)
    73  				state.PB.Components = []*prjpb.Component{{
    74  					Clids:          []int64{1},
    75  					Pruns:          []*prjpb.PRun{{Clids: []int64{1}, Id: "id"}},
    76  					TriageRequired: true,
    77  				}}
    78  				state.PB.Pcls = []*prjpb.PCL{{Clid: 1}}
    79  				pb := backupPB(state)
    80  
    81  				state.repartition(cat)
    82  				pb.RepartitionRequired = false
    83  				So(state.PB, ShouldResembleProto, pb)
    84  			})
    85  		})
    86  
    87  		Convey("Compacts out unused PCLs", func() {
    88  			Convey("no existing components", func() {
    89  				cat.active.ResetI64(1, 3)
    90  				cat.unused.ResetI64(2)
    91  				state.PB.Pcls = []*prjpb.PCL{
    92  					{Clid: 1},
    93  					{Clid: 2},
    94  					{Clid: 3, Deps: []*changelist.Dep{{Clid: 1}}},
    95  				}
    96  
    97  				state.repartition(cat)
    98  				So(state.PB, ShouldResembleProto, &prjpb.PState{
    99  					Pcls: []*prjpb.PCL{
   100  						{Clid: 1},
   101  						{Clid: 3, Deps: []*changelist.Dep{{Clid: 1}}},
   102  					},
   103  					Components: []*prjpb.Component{{
   104  						Clids:          []int64{1, 3},
   105  						TriageRequired: true,
   106  					}},
   107  				})
   108  			})
   109  			Convey("wipes out existing component, too", func() {
   110  				cat.unused.ResetI64(1, 2, 3)
   111  				state.PB.Pcls = []*prjpb.PCL{
   112  					{Clid: 1},
   113  					{Clid: 2},
   114  					{Clid: 3},
   115  				}
   116  				state.PB.Components = []*prjpb.Component{
   117  					{Clids: []int64{1}},
   118  					{Clids: []int64{2, 3}},
   119  				}
   120  				state.repartition(cat)
   121  				So(state.PB, ShouldResembleProto, &prjpb.PState{
   122  					Pcls:       nil,
   123  					Components: nil,
   124  				})
   125  			})
   126  			Convey("shrinks existing component, too", func() {
   127  				cat.active.ResetI64(1)
   128  				cat.unused.ResetI64(2)
   129  				state.PB.Pcls = []*prjpb.PCL{
   130  					{Clid: 1},
   131  					{Clid: 2},
   132  				}
   133  				state.PB.Components = []*prjpb.Component{{
   134  					Clids: []int64{1, 2},
   135  				}}
   136  				state.repartition(cat)
   137  				So(state.PB, ShouldResembleProto, &prjpb.PState{
   138  					Pcls: []*prjpb.PCL{
   139  						{Clid: 1},
   140  					},
   141  					Components: []*prjpb.Component{{
   142  						Clids:          []int64{1},
   143  						TriageRequired: true,
   144  					}},
   145  				})
   146  			})
   147  		})
   148  
   149  		Convey("Creates new components", func() {
   150  			Convey("1 active CL converted into 1 new component needing triage", func() {
   151  				cat.active.ResetI64(1)
   152  				state.PB.Pcls = []*prjpb.PCL{{Clid: 1}}
   153  
   154  				state.repartition(cat)
   155  				So(state.PB, ShouldResembleProto, &prjpb.PState{
   156  					Pcls: []*prjpb.PCL{{Clid: 1}},
   157  					Components: []*prjpb.Component{{
   158  						Clids:          []int64{1},
   159  						TriageRequired: true,
   160  					}},
   161  				})
   162  			})
   163  			Convey("Deps respected during conversion", func() {
   164  				cat.active.ResetI64(1, 2, 3)
   165  				state.PB.Pcls = []*prjpb.PCL{
   166  					{Clid: 1},
   167  					{Clid: 2},
   168  					{Clid: 3, Deps: []*changelist.Dep{{Clid: 1}}},
   169  				}
   170  				orig := backupPB(state)
   171  
   172  				state.repartition(cat)
   173  				sortByFirstCL(state.PB.Components)
   174  				So(state.PB, ShouldResembleProto, &prjpb.PState{
   175  					Pcls: orig.Pcls,
   176  					Components: []*prjpb.Component{
   177  						{
   178  							Clids:          []int64{1, 3},
   179  							TriageRequired: true,
   180  						},
   181  						{
   182  							Clids:          []int64{2},
   183  							TriageRequired: true,
   184  						},
   185  					},
   186  				})
   187  			})
   188  		})
   189  
   190  		Convey("Components splitting works", func() {
   191  			Convey("Crossing-over 12, 34 => 13, 24", func() {
   192  				cat.active.ResetI64(1, 2, 3, 4)
   193  				state.PB.Pcls = []*prjpb.PCL{
   194  					{Clid: 1},
   195  					{Clid: 2},
   196  					{Clid: 3, Deps: []*changelist.Dep{{Clid: 1}}},
   197  					{Clid: 4, Deps: []*changelist.Dep{{Clid: 2}}},
   198  				}
   199  				state.PB.Components = []*prjpb.Component{
   200  					{Clids: []int64{1, 2}},
   201  					{Clids: []int64{3, 4}},
   202  				}
   203  				orig := backupPB(state)
   204  
   205  				state.repartition(cat)
   206  				sortByFirstCL(state.PB.Components)
   207  				So(state.PB, ShouldResembleProto, &prjpb.PState{
   208  					Pcls: orig.Pcls,
   209  					Components: []*prjpb.Component{
   210  						{Clids: []int64{1, 3}, TriageRequired: true},
   211  						{Clids: []int64{2, 4}, TriageRequired: true},
   212  					},
   213  				})
   214  			})
   215  			Convey("Loaded and unloaded deps can be shared by several components", func() {
   216  				cat.active.ResetI64(1, 2, 3)
   217  				cat.deps.ResetI64(4, 5)
   218  				cat.unloaded.ResetI64(5)
   219  				state.PB.Pcls = []*prjpb.PCL{
   220  					{Clid: 1, Deps: []*changelist.Dep{{Clid: 3}, {Clid: 4}, {Clid: 5}}},
   221  					{Clid: 2, Deps: []*changelist.Dep{{Clid: 4}, {Clid: 5}}},
   222  					{Clid: 3},
   223  					{Clid: 4},
   224  				}
   225  				orig := backupPB(state)
   226  
   227  				state.repartition(cat)
   228  				sortByFirstCL(state.PB.Components)
   229  				So(state.PB, ShouldResembleProto, &prjpb.PState{
   230  					Pcls: orig.Pcls,
   231  					Components: []*prjpb.Component{
   232  						{Clids: []int64{1, 3}, TriageRequired: true},
   233  						{Clids: []int64{2}, TriageRequired: true},
   234  					},
   235  				})
   236  			})
   237  		})
   238  
   239  		Convey("CreatedRuns are moved into components", func() {
   240  			Convey("Simple", func() {
   241  				cat.active.ResetI64(1, 2)
   242  				state.PB.Pcls = []*prjpb.PCL{
   243  					{Clid: 1},
   244  					{Clid: 2, Deps: []*changelist.Dep{{Clid: 1}}},
   245  				}
   246  				state.PB.CreatedPruns = []*prjpb.PRun{{Clids: []int64{1, 2}, Id: "id"}}
   247  				orig := backupPB(state)
   248  
   249  				state.repartition(cat)
   250  				So(state.PB, ShouldResembleProto, &prjpb.PState{
   251  					CreatedPruns: nil,
   252  					Pcls:         orig.Pcls,
   253  					Components: []*prjpb.Component{
   254  						{
   255  							Clids:          []int64{1, 2},
   256  							Pruns:          []*prjpb.PRun{{Clids: []int64{1, 2}, Id: "id"}},
   257  							TriageRequired: true,
   258  						},
   259  					},
   260  				})
   261  			})
   262  			Convey("Force-merge 2 existing components", func() {
   263  				cat.active.ResetI64(1, 2)
   264  				state.PB.Pcls = []*prjpb.PCL{
   265  					{Clid: 1},
   266  					{Clid: 2},
   267  				}
   268  				state.PB.Components = []*prjpb.Component{
   269  					{Clids: []int64{1}, Pruns: []*prjpb.PRun{{Clids: []int64{1}, Id: "1"}}},
   270  					{Clids: []int64{2}, Pruns: []*prjpb.PRun{{Clids: []int64{2}, Id: "2"}}},
   271  				}
   272  				state.PB.CreatedPruns = []*prjpb.PRun{{Clids: []int64{1, 2}, Id: "12"}}
   273  				orig := backupPB(state)
   274  
   275  				state.repartition(cat)
   276  				sortByFirstCL(state.PB.Components)
   277  				So(state.PB, ShouldResembleProto, &prjpb.PState{
   278  					CreatedPruns: nil,
   279  					Pcls:         orig.Pcls,
   280  					Components: []*prjpb.Component{
   281  						{
   282  							Clids: []int64{1, 2},
   283  							Pruns: []*prjpb.PRun{ // must be sorted by ID
   284  								{Clids: []int64{1}, Id: "1"},
   285  								{Clids: []int64{1, 2}, Id: "12"},
   286  								{Clids: []int64{2}, Id: "2"},
   287  							},
   288  							TriageRequired: true,
   289  						},
   290  					},
   291  				})
   292  			})
   293  		})
   294  
   295  		Convey("Does all at once", func() {
   296  			// This test adds more test coverage for a busy project where components
   297  			// are created, split, merged, and CreatedRuns are incorporated during
   298  			// repartition(), especially likely after a config update.
   299  			cat.active.ResetI64(1, 2, 4, 5, 6)
   300  			cat.deps.ResetI64(7)
   301  			cat.unused.ResetI64(3)
   302  			cat.unloaded.ResetI64(7)
   303  			state.PB.Pcls = []*prjpb.PCL{
   304  				{Clid: 1},
   305  				{Clid: 2, Deps: []*changelist.Dep{{Clid: 1}}},
   306  				{Clid: 3, Deps: []*changelist.Dep{{Clid: 1}, {Clid: 2}}}, // but unused
   307  				{Clid: 4},
   308  				{Clid: 5, Deps: []*changelist.Dep{{Clid: 4}}},
   309  				{Clid: 6, Deps: []*changelist.Dep{{Clid: 7}}},
   310  			}
   311  			state.PB.Components = []*prjpb.Component{
   312  				{Clids: []int64{1, 2, 3}, Pruns: []*prjpb.PRun{{Clids: []int64{1}, Id: "1"}}},
   313  				{Clids: []int64{4}, Pruns: []*prjpb.PRun{{Clids: []int64{4}, Id: "4"}}},
   314  				{Clids: []int64{5}, Pruns: []*prjpb.PRun{{Clids: []int64{5}, Id: "5"}}},
   315  			}
   316  			state.PB.CreatedPruns = []*prjpb.PRun{
   317  				{Clids: []int64{4, 5}, Id: "45"}, // so, merge component with {4}, {5}.
   318  				{Clids: []int64{6}, Id: "6"},
   319  			}
   320  
   321  			state.repartition(cat)
   322  			sortByFirstCL(state.PB.Components)
   323  			So(state.PB, ShouldResembleProto, &prjpb.PState{
   324  				Pcls: []*prjpb.PCL{
   325  					{Clid: 1},
   326  					{Clid: 2, Deps: []*changelist.Dep{{Clid: 1}}},
   327  					// 3 was deleted
   328  					{Clid: 4},
   329  					{Clid: 5, Deps: []*changelist.Dep{{Clid: 4}}},
   330  					{Clid: 6, Deps: []*changelist.Dep{{Clid: 7}}},
   331  				},
   332  				Components: []*prjpb.Component{
   333  					{Clids: []int64{1, 2}, TriageRequired: true, Pruns: []*prjpb.PRun{{Clids: []int64{1}, Id: "1"}}},
   334  					{Clids: []int64{4, 5}, TriageRequired: true, Pruns: []*prjpb.PRun{
   335  						{Clids: []int64{4}, Id: "4"},
   336  						{Clids: []int64{4, 5}, Id: "45"},
   337  						{Clids: []int64{5}, Id: "5"},
   338  					}},
   339  					{Clids: []int64{6}, TriageRequired: true, Pruns: []*prjpb.PRun{{Clids: []int64{6}, Id: "6"}}},
   340  				},
   341  			})
   342  		})
   343  	})
   344  }
   345  
   346  func TestPartitionSpecialCases(t *testing.T) {
   347  	t.Parallel()
   348  
   349  	Convey("Special cases of partitioning", t, func() {
   350  		ct := cvtesting.Test{}
   351  		ctx, cancel := ct.SetUp(t)
   352  		defer cancel()
   353  		epoch := ct.Clock.Now().Truncate(time.Hour)
   354  
   355  		Convey("crbug/1217775", func() {
   356  			s0 := &State{PB: &prjpb.PState{
   357  				// PCLs form a stack 11 <- 12 <- 13.
   358  				Pcls: []*prjpb.PCL{
   359  					{
   360  						Clid: 10,
   361  						// ConfigGroupIndexes: []int32{0},
   362  						Status: prjpb.PCL_OK,
   363  						Triggers: &run.Triggers{CqVoteTrigger: &run.Trigger{
   364  							Time: timestamppb.New(epoch.Add(1 * time.Minute)),
   365  							Mode: string(run.DryRun),
   366  						}},
   367  					},
   368  					{
   369  						Clid: 11,
   370  						// ConfigGroupIndexes: []int32{0},
   371  						Deps: []*changelist.Dep{
   372  							{Clid: 10, Kind: changelist.DepKind_HARD},
   373  						},
   374  						Status:   prjpb.PCL_OK,
   375  						Triggers: nil, // no longer triggered, because its DryRun has just finished.
   376  					},
   377  					{
   378  						Clid: 12,
   379  						// ConfigGroupIndexes: []int32{0},
   380  						Deps: []*changelist.Dep{
   381  							{Clid: 10, Kind: changelist.DepKind_SOFT},
   382  							{Clid: 11, Kind: changelist.DepKind_HARD},
   383  						},
   384  						Status: prjpb.PCL_OK,
   385  						Triggers: &run.Triggers{CqVoteTrigger: &run.Trigger{
   386  							Time: timestamppb.New(epoch.Add(2 * time.Minute)),
   387  							Mode: string(run.DryRun),
   388  						}},
   389  					},
   390  				},
   391  
   392  				Components: []*prjpb.Component{
   393  					{
   394  						Clids: []int64{11},
   395  						// Associated DryRun has just been completed and removed and
   396  						// TriageRequired set to true.
   397  						TriageRequired: true,
   398  					},
   399  				},
   400  
   401  				CreatedPruns: []*prjpb.PRun{
   402  					{Id: "chromium/8999-1-aa10", Clids: []int64{10}},
   403  					{Id: "chromium/8999-1-aa12", Clids: []int64{12}},
   404  				},
   405  			}}
   406  
   407  			cat := s0.categorizeCLs(ctx)
   408  			So(cat.active, ShouldResemble, common.CLIDsSet{10: {}, 12: {}})
   409  			So(cat.deps, ShouldResemble, common.CLIDsSet{11: {}})
   410  			So(cat.unused, ShouldBeEmpty)
   411  			So(cat.unloaded, ShouldBeEmpty)
   412  
   413  			s1 := s0.cloneShallow()
   414  			s1.repartition(cat)
   415  
   416  			// All PCLs are still used.
   417  			So(s1.PB.GetPcls(), ShouldResembleProto, s0.PB.GetPcls())
   418  			// But CreatedPruns must be moved into components.
   419  			So(s1.PB.GetCreatedPruns(), ShouldBeEmpty)
   420  			// Because CLs are related, there should be just 1 component remaining with
   421  			// both Runs.
   422  			So(s1.PB.GetComponents(), ShouldResembleProto, []*prjpb.Component{
   423  				{
   424  					Clids: []int64{10, 12},
   425  					Pruns: []*prjpb.PRun{
   426  						{Id: "chromium/8999-1-aa10", Clids: []int64{10}},
   427  						{Id: "chromium/8999-1-aa12", Clids: []int64{12}},
   428  					},
   429  					TriageRequired: true,
   430  				},
   431  			})
   432  		})
   433  	})
   434  }