go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/configs/prjcfg/refresher/refresh_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 refresher 16 17 import ( 18 "context" 19 "testing" 20 "time" 21 22 "google.golang.org/protobuf/encoding/prototext" 23 "google.golang.org/protobuf/proto" 24 "google.golang.org/protobuf/types/known/durationpb" 25 26 "go.chromium.org/luci/common/clock/testclock" 27 "go.chromium.org/luci/common/logging" 28 "go.chromium.org/luci/common/logging/gologger" 29 "go.chromium.org/luci/config" 30 "go.chromium.org/luci/config/cfgclient" 31 cfgmemory "go.chromium.org/luci/config/impl/memory" 32 "go.chromium.org/luci/gae/filter/txndefer" 33 gaememory "go.chromium.org/luci/gae/impl/memory" 34 "go.chromium.org/luci/gae/service/datastore" 35 "go.chromium.org/luci/server/tq" 36 "go.chromium.org/luci/server/tq/tqtesting" 37 38 cfgpb "go.chromium.org/luci/cv/api/config/v2" 39 "go.chromium.org/luci/cv/internal/configs/prjcfg" 40 "go.chromium.org/luci/cv/internal/configs/srvcfg" 41 listenerpb "go.chromium.org/luci/cv/settings/listener" 42 43 . "github.com/smartystreets/goconvey/convey" 44 . "go.chromium.org/luci/common/testing/assertions" 45 ) 46 47 var testNow = testclock.TestTimeLocal.Round(1 * time.Millisecond) 48 var testCfg = &cfgpb.Config{ 49 DrainingStartTime: "2014-05-11T14:37:57Z", 50 SubmitOptions: &cfgpb.SubmitOptions{ 51 MaxBurst: 50, 52 BurstDelay: durationpb.New(2 * time.Second), 53 }, 54 CqStatusHost: "chromium-cq-status.appspot.com", 55 ConfigGroups: []*cfgpb.ConfigGroup{ 56 { 57 Name: "group_foo", 58 Gerrit: []*cfgpb.ConfigGroup_Gerrit{ 59 { 60 Url: "https://chromium-review.googlesource.com/", 61 Projects: []*cfgpb.ConfigGroup_Gerrit_Project{ 62 { 63 Name: "chromium/src", 64 RefRegexp: []string{"refs/heads/main"}, 65 }, 66 }, 67 }, 68 }, 69 }, 70 }, 71 } 72 73 func TestUpdateProject(t *testing.T) { 74 Convey("Update Project", t, func() { 75 ctx, testClock, _ := mkTestingCtx() 76 chromiumConfig := &cfgpb.Config{ 77 DrainingStartTime: "2017-12-23T15:47:58Z", 78 CqStatusHost: "chromium-cq-status.appspot.com", 79 SubmitOptions: &cfgpb.SubmitOptions{ 80 MaxBurst: 100, 81 BurstDelay: durationpb.New(1 * time.Second), 82 }, 83 ConfigGroups: []*cfgpb.ConfigGroup{ 84 { 85 Name: "branch_m100", 86 Gerrit: []*cfgpb.ConfigGroup_Gerrit{ 87 { 88 Url: "https://chromium-review.googlesource.com/", 89 Projects: []*cfgpb.ConfigGroup_Gerrit_Project{ 90 { 91 Name: "chromium/src", 92 RefRegexp: []string{"refs/heads/branch_m100"}, 93 }, 94 }, 95 }, 96 }, 97 }, 98 { 99 Name: "main", 100 Gerrit: []*cfgpb.ConfigGroup_Gerrit{ 101 { 102 Url: "https://chromium-review.googlesource.com/", 103 Projects: []*cfgpb.ConfigGroup_Gerrit_Project{ 104 { 105 Name: "chromium/src", 106 RefRegexp: []string{"refs/heads/main"}, 107 }, 108 }, 109 }, 110 }, 111 }, 112 }, 113 } 114 verifyEntitiesInDatastore := func(ctx context.Context, expectedEVersion int64) { 115 cfg, meta := &cfgpb.Config{}, &config.Meta{} 116 err := cfgclient.Get(ctx, config.MustProjectSet("chromium"), ConfigFileName, cfgclient.ProtoText(cfg), meta) 117 So(err, ShouldBeNil) 118 localHash := prjcfg.ComputeHash(cfg) 119 projKey := prjcfg.ProjectConfigKey(ctx, "chromium") 120 cgNames := make([]string, len(cfg.GetConfigGroups())) 121 // Verify ConfigGroups. 122 for i, cgpb := range cfg.GetConfigGroups() { 123 cgNames[i] = cgpb.GetName() 124 cg := prjcfg.ConfigGroup{ 125 ID: prjcfg.MakeConfigGroupID(localHash, cgNames[i]), 126 Project: projKey, 127 } 128 err := datastore.Get(ctx, &cg) 129 So(err, ShouldBeNil) 130 So(cg.DrainingStartTime, ShouldEqual, cfg.GetDrainingStartTime()) 131 So(cg.SubmitOptions, ShouldResembleProto, cfg.GetSubmitOptions()) 132 So(cg.Content, ShouldResembleProto, cfg.GetConfigGroups()[i]) 133 So(cg.CQStatusHost, ShouldResemble, cfg.GetCqStatusHost()) 134 } 135 // Verify ProjectConfig. 136 pc := prjcfg.ProjectConfig{Project: "chromium"} 137 err = datastore.Get(ctx, &pc) 138 So(err, ShouldBeNil) 139 So(pc, ShouldResemble, prjcfg.ProjectConfig{ 140 Project: "chromium", 141 SchemaVersion: prjcfg.SchemaVersion, 142 Enabled: true, 143 EVersion: expectedEVersion, 144 Hash: localHash, 145 ExternalHash: meta.ContentHash, 146 UpdateTime: datastore.RoundTime(testClock.Now()).UTC(), 147 ConfigGroupNames: cgNames, 148 }) 149 // The revision in the memory-based config fake is a fake 150 // 40-character sha256 hash digest. The particular value is 151 // internally determined by the memory-based implementation 152 // and isn't important here, so just assert that something 153 // that looks like a hash digest is filled in. 154 hashInfo := prjcfg.ConfigHashInfo{Hash: localHash, Project: projKey} 155 err = datastore.Get(ctx, &hashInfo) 156 So(err, ShouldBeNil) 157 So(len(hashInfo.GitRevision), ShouldEqual, 40) 158 hashInfo.GitRevision = "" 159 // Verify the rest of ConfigHashInfo. 160 So(hashInfo, ShouldResemble, prjcfg.ConfigHashInfo{ 161 Hash: localHash, 162 Project: projKey, 163 SchemaVersion: prjcfg.SchemaVersion, 164 ProjectEVersion: expectedEVersion, 165 UpdateTime: datastore.RoundTime(testClock.Now()).UTC(), 166 ConfigGroupNames: cgNames, 167 }) 168 } 169 170 notifyCalled := false 171 notify := func(context.Context) error { 172 notifyCalled = true 173 return nil 174 } 175 176 Convey("Creates new ProjectConfig", func() { 177 ctx = cfgclient.Use(ctx, cfgmemory.New(map[config.Set]cfgmemory.Files{ 178 config.MustProjectSet("chromium"): { 179 ConfigFileName: toProtoText(chromiumConfig), 180 }, 181 })) 182 err := UpdateProject(ctx, "chromium", notify) 183 So(err, ShouldBeNil) 184 verifyEntitiesInDatastore(ctx, 1) 185 So(notifyCalled, ShouldBeTrue) 186 187 notifyCalled = false 188 testClock.Add(10 * time.Minute) 189 190 Convey("Noop if config is up-to-date", func() { 191 err := UpdateProject(ctx, "chromium", notify) 192 So(err, ShouldBeNil) 193 pc := prjcfg.ProjectConfig{Project: "chromium"} 194 So(datastore.Get(ctx, &pc), ShouldBeNil) 195 So(pc.EVersion, ShouldEqual, 1) 196 prevUpdatedTime := testClock.Now().Add(-10 * time.Minute) 197 So(pc.UpdateTime, ShouldResemble, prevUpdatedTime.UTC()) 198 So(notifyCalled, ShouldBeFalse) 199 200 Convey("But not noop if SchemaVersion changed", func() { 201 old := pc // copy 202 old.SchemaVersion-- 203 So(datastore.Put(ctx, &old), ShouldBeNil) 204 205 err := UpdateProject(ctx, "chromium", notify) 206 So(err, ShouldBeNil) 207 So(notifyCalled, ShouldBeTrue) 208 So(datastore.Get(ctx, &pc), ShouldBeNil) 209 So(pc.EVersion, ShouldEqual, 2) 210 So(pc.SchemaVersion, ShouldEqual, prjcfg.SchemaVersion) 211 }) 212 }) 213 214 Convey("Update existing ProjectConfig", func() { 215 updatedConfig := proto.Clone(chromiumConfig).(*cfgpb.Config) 216 updatedConfig.ConfigGroups = append(updatedConfig.ConfigGroups, &cfgpb.ConfigGroup{ 217 Name: "experimental", 218 Gerrit: []*cfgpb.ConfigGroup_Gerrit{ 219 { 220 Url: "https://chromium-review.googlesource.com/", 221 Projects: []*cfgpb.ConfigGroup_Gerrit_Project{ 222 { 223 Name: "chromium/src/experimental", 224 RefRegexp: []string{"refs/heads/main"}, 225 }, 226 }, 227 }, 228 }, 229 }) 230 ctx = cfgclient.Use(ctx, cfgmemory.New(map[config.Set]cfgmemory.Files{ 231 config.MustProjectSet("chromium"): { 232 ConfigFileName: toProtoText(updatedConfig), 233 }, 234 })) 235 err := UpdateProject(ctx, "chromium", notify) 236 So(err, ShouldBeNil) 237 verifyEntitiesInDatastore(ctx, 2) 238 So(notifyCalled, ShouldBeTrue) 239 240 notifyCalled = false 241 testClock.Add(10 * time.Minute) 242 243 Convey("Roll back to previous version", func() { 244 ctx = cfgclient.Use(ctx, cfgmemory.New(map[config.Set]cfgmemory.Files{ 245 config.MustProjectSet("chromium"): { 246 ConfigFileName: toProtoText(chromiumConfig), 247 }, 248 })) 249 250 err := UpdateProject(ctx, "chromium", notify) 251 So(err, ShouldBeNil) 252 verifyEntitiesInDatastore(ctx, 3) 253 So(notifyCalled, ShouldBeTrue) 254 }) 255 256 Convey("Re-enables project even if config hash is the same", func() { 257 testClock.Add(10 * time.Minute) 258 So(DisableProject(ctx, "chromium", notify), ShouldBeNil) 259 before := prjcfg.ProjectConfig{Project: "chromium"} 260 So(datastore.Get(ctx, &before), ShouldBeNil) 261 // Delete config entities. 262 projKey := prjcfg.ProjectConfigKey(ctx, "chromium") 263 err := datastore.Delete(ctx, 264 &prjcfg.ConfigHashInfo{Hash: before.Hash, Project: projKey}, 265 &prjcfg.ConfigGroup{ 266 ID: prjcfg.MakeConfigGroupID(before.Hash, before.ConfigGroupNames[0]), 267 Project: projKey, 268 }, 269 ) 270 So(err, ShouldBeNil) 271 272 testClock.Add(10 * time.Minute) 273 So(UpdateProject(ctx, "chromium", notify), ShouldBeNil) 274 after := prjcfg.ProjectConfig{Project: "chromium"} 275 So(datastore.Get(ctx, &after), ShouldBeNil) 276 277 So(after.Enabled, ShouldBeTrue) 278 So(after.EVersion, ShouldEqual, before.EVersion+1) 279 So(after.Hash, ShouldResemble, before.Hash) 280 // Ensure deleted entities are re-created. 281 verifyEntitiesInDatastore(ctx, 4) 282 So(notifyCalled, ShouldBeTrue) 283 }) 284 }) 285 }) 286 }) 287 } 288 289 func TestDisableProject(t *testing.T) { 290 Convey("Disable", t, func() { 291 ctx, testClock, _ := mkTestingCtx() 292 writeProjectConfig := func(enabled bool) { 293 pc := prjcfg.ProjectConfig{ 294 Project: "chromium", 295 Enabled: enabled, 296 EVersion: 100, 297 Hash: "hash", 298 ExternalHash: "externalHash", 299 UpdateTime: datastore.RoundTime(testClock.Now()).UTC(), 300 ConfigGroupNames: []string{"default"}, 301 } 302 So(datastore.Put(ctx, &pc), ShouldBeNil) 303 testClock.Add(10 * time.Minute) 304 } 305 306 notifyCalled := false 307 notify := func(context.Context) error { 308 notifyCalled = true 309 return nil 310 } 311 312 Convey("currently enabled Project", func() { 313 writeProjectConfig(true) 314 err := DisableProject(ctx, "chromium", notify) 315 So(err, ShouldBeNil) 316 actual := prjcfg.ProjectConfig{Project: "chromium"} 317 So(datastore.Get(ctx, &actual), ShouldBeNil) 318 So(actual.Enabled, ShouldBeFalse) 319 So(actual.EVersion, ShouldEqual, 101) 320 So(actual.UpdateTime, ShouldResemble, datastore.RoundTime(testClock.Now()).UTC()) 321 So(notifyCalled, ShouldBeTrue) 322 }) 323 324 Convey("currently disabled Project", func() { 325 writeProjectConfig(false) 326 err := DisableProject(ctx, "chromium", notify) 327 So(err, ShouldBeNil) 328 actual := prjcfg.ProjectConfig{Project: "chromium"} 329 So(datastore.Get(ctx, &actual), ShouldBeNil) 330 So(actual.Enabled, ShouldBeFalse) 331 So(actual.EVersion, ShouldEqual, 100) 332 So(notifyCalled, ShouldBeFalse) 333 }) 334 335 Convey("non-existing Project", func() { 336 err := DisableProject(ctx, "non-existing", notify) 337 So(err, ShouldBeNil) 338 So(datastore.Get(ctx, &prjcfg.ProjectConfig{Project: "non-existing"}), ShouldErrLike, datastore.ErrNoSuchEntity) 339 So(notifyCalled, ShouldBeFalse) 340 }) 341 }) 342 } 343 344 func mkTestingCtx() (context.Context, testclock.TestClock, *tqtesting.Scheduler) { 345 ctx, clock := testclock.UseTime(context.Background(), testNow) 346 ctx = txndefer.FilterRDS(gaememory.Use(ctx)) 347 datastore.GetTestable(ctx).AutoIndex(true) 348 datastore.GetTestable(ctx).Consistent(true) 349 350 ctx, scheduler := tq.TestingContext(ctx, nil) 351 if err := srvcfg.SetTestListenerConfig(ctx, &listenerpb.Settings{}, nil); err != nil { 352 panic(err) 353 } 354 return ctx, clock, scheduler 355 } 356 357 func toProtoText(msg proto.Message) string { 358 bs, err := prototext.Marshal(msg) 359 So(err, ShouldBeNil) 360 return string(bs) 361 } 362 363 func TestPutConfigGroups(t *testing.T) { 364 t.Parallel() 365 366 Convey("PutConfigGroups", t, func() { 367 ctx := gaememory.Use(context.Background()) 368 if testing.Verbose() { 369 ctx = logging.SetLevel(gologger.StdConfig.Use(ctx), logging.Debug) 370 } 371 372 Convey("New Configs", func() { 373 hash := prjcfg.ComputeHash(testCfg) 374 err := putConfigGroups(ctx, testCfg, "chromium", hash) 375 So(err, ShouldBeNil) 376 stored := prjcfg.ConfigGroup{ 377 ID: prjcfg.MakeConfigGroupID(hash, "group_foo"), 378 Project: prjcfg.ProjectConfigKey(ctx, "chromium"), 379 } 380 So(datastore.Get(ctx, &stored), ShouldBeNil) 381 So(stored.DrainingStartTime, ShouldEqual, testCfg.GetDrainingStartTime()) 382 So(stored.SubmitOptions, ShouldResembleProto, testCfg.GetSubmitOptions()) 383 So(stored.Content, ShouldResembleProto, testCfg.GetConfigGroups()[0]) 384 So(stored.SchemaVersion, ShouldEqual, prjcfg.SchemaVersion) 385 386 Convey("Skip if already exists", func() { 387 ctx := datastore.AddRawFilters(ctx, func(_ context.Context, rds datastore.RawInterface) datastore.RawInterface { 388 return readOnlyFilter{rds} 389 }) 390 err := putConfigGroups(ctx, testCfg, "chromium", prjcfg.ComputeHash(testCfg)) 391 So(err, ShouldBeNil) 392 }) 393 394 Convey("Update existing due to SchemaVersion", func() { 395 old := stored // copy 396 old.SchemaVersion = prjcfg.SchemaVersion - 1 397 So(datastore.Put(ctx, &old), ShouldBeNil) 398 399 err := putConfigGroups(ctx, testCfg, "chromium", prjcfg.ComputeHash(testCfg)) 400 So(err, ShouldBeNil) 401 402 So(datastore.Get(ctx, &stored), ShouldBeNil) 403 So(stored.SchemaVersion, ShouldEqual, prjcfg.SchemaVersion) 404 }) 405 }) 406 }) 407 } 408 409 type readOnlyFilter struct{ datastore.RawInterface } 410 411 func (f readOnlyFilter) PutMulti(keys []*datastore.Key, vals []datastore.PropertyMap, cb datastore.NewKeyCB) error { 412 panic("write is not supported") 413 }