go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/configs/prjcfg/refresher/cron_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 "math/rand" 20 "testing" 21 "time" 22 23 "go.chromium.org/luci/common/data/rand/mathrand" 24 "go.chromium.org/luci/config" 25 "go.chromium.org/luci/config/cfgclient" 26 cfgmemory "go.chromium.org/luci/config/impl/memory" 27 "go.chromium.org/luci/gae/service/datastore" 28 "go.chromium.org/luci/server/quota/quotapb" 29 "go.chromium.org/luci/server/tq/tqtesting" 30 "google.golang.org/protobuf/reflect/protoreflect" 31 32 "go.chromium.org/luci/cv/internal/configs/prjcfg" 33 "go.chromium.org/luci/cv/internal/cvtesting" 34 35 . "github.com/smartystreets/goconvey/convey" 36 . "go.chromium.org/luci/common/testing/assertions" 37 ) 38 39 func TestConfigRefreshCron(t *testing.T) { 40 t.Parallel() 41 42 Convey("Config refresh cron works", t, func() { 43 ct := cvtesting.Test{} 44 ctx, cancel := ct.SetUp(t) 45 defer cancel() 46 47 pm := mockPM{} 48 qm := mockQM{} 49 pcr := NewRefresher(ct.TQDispatcher, &pm, &qm, ct.Env) 50 51 Convey("for a new project", func() { 52 ctx = cfgclient.Use(ctx, cfgmemory.New(map[config.Set]cfgmemory.Files{ 53 config.MustProjectSet("chromium"): {ConfigFileName: ""}, 54 })) 55 // Project chromium doesn't exist in datastore. 56 err := pcr.SubmitRefreshTasks(ctx) 57 So(err, ShouldBeNil) 58 So(ct.TQ.Tasks().Payloads(), ShouldResembleProto, []protoreflect.ProtoMessage{ 59 &RefreshProjectConfigTask{Project: "chromium"}, 60 }) 61 ct.TQ.Run(ctx, tqtesting.StopAfterTask("refresh-project-config")) 62 So(pm.updates, ShouldResemble, []string{"chromium"}) 63 So(qm.writes, ShouldResemble, []string{"chromium"}) 64 }) 65 66 Convey("for an existing project", func() { 67 ctx = cfgclient.Use(ctx, cfgmemory.New(map[config.Set]cfgmemory.Files{ 68 config.MustProjectSet("chromium"): {ConfigFileName: ""}, 69 })) 70 So(datastore.Put(ctx, &prjcfg.ProjectConfig{ 71 Project: "chromium", 72 Enabled: true, 73 }), ShouldBeNil) 74 So(pcr.SubmitRefreshTasks(ctx), ShouldBeNil) 75 So(ct.TQ.Tasks().Payloads(), ShouldResembleProto, []protoreflect.ProtoMessage{ 76 &RefreshProjectConfigTask{Project: "chromium"}, 77 }) 78 ct.TQ.Run(ctx, tqtesting.StopAfterTask("refresh-project-config")) 79 So(pm.updates, ShouldResemble, []string{"chromium"}) 80 So(qm.writes, ShouldResemble, []string{"chromium"}) 81 pm.updates = nil 82 qm.writes = nil 83 84 Convey("randomly pokes existing projects even if there are no updates", func() { 85 // Simulate cron runs every 1 minute and expect PM to be poked at least 86 // once per pokePMInterval. 87 ctx = mathrand.Set(ctx, rand.New(rand.NewSource(1234))) 88 pokeBefore := ct.Clock.Now().Add(pokePMInterval) 89 for ct.Clock.Now().Before(pokeBefore) { 90 ct.Clock.Add(time.Minute) 91 So(pcr.SubmitRefreshTasks(ctx), ShouldBeNil) 92 ct.TQ.Run(ctx, tqtesting.StopAfterTask("refresh-project-config")) 93 So(pm.updates, ShouldBeEmpty) 94 So(qm.writes, ShouldResemble, []string{"chromium"}) 95 if len(pm.pokes) > 0 { 96 break 97 } 98 } 99 So(pm.pokes, ShouldResemble, []string{"chromium"}) 100 }) 101 }) 102 103 Convey("Disable project", func() { 104 Convey("that doesn't have CV config", func() { 105 ctx = cfgclient.Use(ctx, cfgmemory.New(map[config.Set]cfgmemory.Files{ 106 config.MustProjectSet("chromium"): {"other.cfg": ""}, 107 })) 108 So(datastore.Put(ctx, &prjcfg.ProjectConfig{ 109 Project: "chromium", 110 Enabled: true, 111 }), ShouldBeNil) 112 err := pcr.SubmitRefreshTasks(ctx) 113 So(err, ShouldBeNil) 114 So(ct.TQ.Tasks().Payloads(), ShouldResembleProto, []protoreflect.ProtoMessage{ 115 &RefreshProjectConfigTask{Project: "chromium", Disable: true}, 116 }) 117 ct.TQ.Run(ctx, tqtesting.StopAfterTask("refresh-project-config")) 118 So(pm.updates, ShouldResemble, []string{"chromium"}) 119 So(qm.writes, ShouldResemble, []string{"chromium"}) 120 }) 121 Convey("that doesn't exist in LUCI Config", func() { 122 ctx = cfgclient.Use(ctx, cfgmemory.New(map[config.Set]cfgmemory.Files{})) 123 So(datastore.Put(ctx, &prjcfg.ProjectConfig{ 124 Project: "chromium", 125 Enabled: true, 126 }), ShouldBeNil) 127 err := pcr.SubmitRefreshTasks(ctx) 128 So(err, ShouldBeNil) 129 So(ct.TQ.Tasks().Payloads(), ShouldResembleProto, []protoreflect.ProtoMessage{ 130 &RefreshProjectConfigTask{Project: "chromium", Disable: true}, 131 }) 132 ct.TQ.Run(ctx, tqtesting.StopAfterTask("refresh-project-config")) 133 So(pm.updates, ShouldResemble, []string{"chromium"}) 134 So(qm.writes, ShouldResemble, []string{"chromium"}) 135 }) 136 Convey("Skip already disabled Project", func() { 137 ctx = cfgclient.Use(ctx, cfgmemory.New(map[config.Set]cfgmemory.Files{})) 138 So(datastore.Put(ctx, &prjcfg.ProjectConfig{ 139 Project: "foo", 140 Enabled: false, 141 }), ShouldBeNil) 142 err := pcr.SubmitRefreshTasks(ctx) 143 So(err, ShouldBeNil) 144 So(ct.TQ.Tasks(), ShouldBeEmpty) 145 }) 146 }) 147 }) 148 } 149 150 type mockPM struct { 151 pokes []string 152 updates []string 153 } 154 155 func (m *mockPM) Poke(ctx context.Context, luciProject string) error { 156 m.pokes = append(m.pokes, luciProject) 157 return nil 158 } 159 160 func (m *mockPM) UpdateConfig(ctx context.Context, luciProject string) error { 161 m.updates = append(m.updates, luciProject) 162 return nil 163 } 164 165 type mockQM struct { 166 writes []string 167 } 168 169 func (qm *mockQM) WritePolicy(ctx context.Context, project string) (*quotapb.PolicyConfigID, error) { 170 qm.writes = append(qm.writes, project) 171 return nil, nil 172 }