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 }