go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/analysis/internal/clustering/rules/cache/cache_test.go (about) 1 // Copyright 2022 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 cache 16 17 import ( 18 "sort" 19 "testing" 20 "time" 21 22 "go.chromium.org/luci/common/clock/testclock" 23 "go.chromium.org/luci/server/caching" 24 25 "go.chromium.org/luci/analysis/internal/bugs" 26 "go.chromium.org/luci/analysis/internal/clustering/rules" 27 "go.chromium.org/luci/analysis/internal/testutil" 28 29 . "github.com/smartystreets/goconvey/convey" 30 . "go.chromium.org/luci/common/testing/assertions" 31 ) 32 33 var cache = caching.RegisterLRUCache[string, *Ruleset](50) 34 35 func TestRulesCache(t *testing.T) { 36 Convey(`With Spanner Test Database`, t, func() { 37 ctx := testutil.IntegrationTestContext(t) 38 ctx, tc := testclock.UseTime(ctx, testclock.TestRecentTimeUTC) 39 ctx = caching.WithEmptyProcessCache(ctx) 40 41 rc := NewRulesCache(cache) 42 err := rules.SetForTesting(ctx, nil) 43 So(err, ShouldBeNil) 44 45 test := func(minimumPredicatesVerison time.Time, expectedRules []*rules.Entry, expectedVersion rules.Version) { 46 // Tests the content of the cache is as expected. 47 ruleset, err := rc.Ruleset(ctx, "myproject", minimumPredicatesVerison) 48 So(err, ShouldBeNil) 49 So(ruleset.Version, ShouldResemble, expectedVersion) 50 51 activeRules := 0 52 for _, e := range expectedRules { 53 if e.IsActive { 54 activeRules++ 55 } 56 } 57 So(len(ruleset.ActiveRulesSorted), ShouldEqual, activeRules) 58 So(len(ruleset.ActiveRulesByID), ShouldEqual, activeRules) 59 60 sortedExpectedRules := sortRulesByPredicateLastUpdated(expectedRules) 61 62 actualRuleIndex := 0 63 for _, e := range sortedExpectedRules { 64 if e.IsActive { 65 a := ruleset.ActiveRulesSorted[actualRuleIndex] 66 So(a.Rule, ShouldResembleProto, *e) 67 // Technically (*lang.Expr).String() may not get us 68 // back the original rule if RuleDefinition didn't use 69 // normalised formatting. But for this test, we use 70 // normalised formatting, so that is not an issue. 71 So(a.Expr, ShouldNotBeNil) 72 So(a.Expr.String(), ShouldEqual, e.RuleDefinition) 73 actualRuleIndex++ 74 75 a2, ok := ruleset.ActiveRulesByID[a.Rule.RuleID] 76 So(ok, ShouldBeTrue) 77 So(a2.Rule, ShouldResembleProto, *e) 78 } 79 } 80 So(len(ruleset.ActiveRulesWithPredicateUpdatedSince(rules.StartingEpoch)), ShouldEqual, activeRules) 81 So(len(ruleset.ActiveRulesWithPredicateUpdatedSince(time.Date(2100, time.January, 1, 1, 0, 0, 0, time.UTC))), ShouldEqual, 0) 82 } 83 84 Convey(`Initially Empty`, func() { 85 err := rules.SetForTesting(ctx, nil) 86 So(err, ShouldBeNil) 87 test(rules.StartingEpoch, nil, rules.StartingVersion) 88 89 Convey(`Then Empty`, func() { 90 // Test cache. 91 test(rules.StartingEpoch, nil, rules.StartingVersion) 92 93 tc.Add(refreshInterval) 94 95 test(rules.StartingEpoch, nil, rules.StartingVersion) 96 test(rules.StartingEpoch, nil, rules.StartingVersion) 97 }) 98 Convey(`Then Non-Empty`, func() { 99 // Spanner commit timestamps are in microsecond 100 // (not nanosecond) granularity, and some Spanner timestamp 101 // operators truncates to microseconds. For this 102 // reason, we use microsecond resolution timestamps 103 // when testing. 104 reference := time.Date(2020, 1, 2, 3, 4, 5, 6000, time.UTC) 105 106 rs := []*rules.Entry{ 107 rules.NewRule(100). 108 WithLastUpdateTime(reference.Add(-1 * time.Hour)). 109 WithPredicateLastUpdateTime(reference.Add(-2 * time.Hour)). 110 Build(), 111 rules.NewRule(101).WithActive(false). 112 WithLastUpdateTime(reference.Add(1 * time.Hour)). 113 WithPredicateLastUpdateTime(reference). 114 Build(), 115 } 116 err := rules.SetForTesting(ctx, rs) 117 So(err, ShouldBeNil) 118 119 expectedRulesVersion := rules.Version{ 120 Total: reference.Add(1 * time.Hour), 121 Predicates: reference, 122 } 123 124 Convey(`By Strong Read`, func() { 125 test(StrongRead, rs, expectedRulesVersion) 126 test(StrongRead, rs, expectedRulesVersion) 127 }) 128 Convey(`By Requesting Version`, func() { 129 test(expectedRulesVersion.Predicates, rs, expectedRulesVersion) 130 }) 131 Convey(`By Cache Expiry`, func() { 132 // Test cache is working and still returning the old value. 133 tc.Add(refreshInterval / 2) 134 test(rules.StartingEpoch, nil, rules.StartingVersion) 135 136 tc.Add(refreshInterval) 137 138 test(rules.StartingEpoch, rs, expectedRulesVersion) 139 test(rules.StartingEpoch, rs, expectedRulesVersion) 140 }) 141 }) 142 }) 143 Convey(`Initially Non-Empty`, func() { 144 reference := time.Date(2021, 1, 2, 3, 4, 5, 6000, time.UTC) 145 146 ruleOne := rules.NewRule(100). 147 WithLastUpdateTime(reference.Add(-2 * time.Hour)). 148 WithPredicateLastUpdateTime(reference.Add(-3 * time.Hour)) 149 ruleTwo := rules.NewRule(101). 150 WithLastUpdateTime(reference.Add(-2 * time.Hour)). 151 WithPredicateLastUpdateTime(reference.Add(-3 * time.Hour)) 152 ruleThree := rules.NewRule(102).WithActive(false). 153 WithLastUpdateTime(reference). 154 WithPredicateLastUpdateTime(reference.Add(-1 * time.Hour)) 155 156 rs := []*rules.Entry{ 157 ruleOne.Build(), 158 ruleTwo.Build(), 159 ruleThree.Build(), 160 } 161 err := rules.SetForTesting(ctx, rs) 162 So(err, ShouldBeNil) 163 164 expectedRulesVersion := rules.Version{ 165 Total: reference, 166 Predicates: reference.Add(-1 * time.Hour), 167 } 168 test(rules.StartingEpoch, rs, expectedRulesVersion) 169 170 Convey(`Then Empty`, func() { 171 // Mark all rules inactive. 172 newRules := []*rules.Entry{ 173 ruleOne.WithActive(false). 174 WithLastUpdateTime(reference.Add(4 * time.Hour)). 175 WithPredicateLastUpdateTime(reference.Add(3 * time.Hour)). 176 Build(), 177 ruleTwo.WithActive(false). 178 WithLastUpdateTime(reference.Add(2 * time.Hour)). 179 WithPredicateLastUpdateTime(reference.Add(1 * time.Hour)). 180 Build(), 181 ruleThree.WithActive(false). 182 WithLastUpdateTime(reference.Add(2 * time.Hour)). 183 WithPredicateLastUpdateTime(reference.Add(1 * time.Hour)). 184 Build(), 185 } 186 err := rules.SetForTesting(ctx, newRules) 187 So(err, ShouldBeNil) 188 189 oldRulesVersion := expectedRulesVersion 190 expectedRulesVersion := rules.Version{ 191 Total: reference.Add(4 * time.Hour), 192 Predicates: reference.Add(3 * time.Hour), 193 } 194 195 Convey(`By Strong Read`, func() { 196 test(StrongRead, newRules, expectedRulesVersion) 197 test(StrongRead, newRules, expectedRulesVersion) 198 }) 199 Convey(`By Requesting Version`, func() { 200 test(expectedRulesVersion.Predicates, newRules, expectedRulesVersion) 201 }) 202 Convey(`By Cache Expiry`, func() { 203 // Test cache is working and still returning the old value. 204 tc.Add(refreshInterval / 2) 205 test(rules.StartingEpoch, rs, oldRulesVersion) 206 207 tc.Add(refreshInterval) 208 209 test(rules.StartingEpoch, newRules, expectedRulesVersion) 210 test(rules.StartingEpoch, newRules, expectedRulesVersion) 211 }) 212 }) 213 Convey(`Then Non-Empty`, func() { 214 newRules := []*rules.Entry{ 215 // Mark an existing rule inactive. 216 ruleOne.WithActive(false). 217 WithLastUpdateTime(reference.Add(time.Hour)). 218 WithPredicateLastUpdateTime(reference.Add(time.Hour)). 219 Build(), 220 // Make a non-predicate change on an active rule. 221 ruleTwo. 222 WithBug(bugs.BugID{System: "monorail", ID: "project/123"}). 223 WithLastUpdateTime(reference.Add(time.Hour)). 224 Build(), 225 // Make an existing rule active. 226 ruleThree.WithActive(true). 227 WithLastUpdateTime(reference.Add(time.Hour)). 228 WithPredicateLastUpdateTime(reference.Add(time.Hour)). 229 Build(), 230 // Add a new active rule. 231 rules.NewRule(103). 232 WithPredicateLastUpdateTime(reference.Add(time.Hour)). 233 WithLastUpdateTime(reference.Add(time.Hour)). 234 Build(), 235 // Add a new inactive rule. 236 rules.NewRule(104).WithActive(false). 237 WithPredicateLastUpdateTime(reference.Add(2 * time.Hour)). 238 WithLastUpdateTime(reference.Add(3 * time.Hour)). 239 Build(), 240 } 241 err := rules.SetForTesting(ctx, newRules) 242 So(err, ShouldBeNil) 243 244 oldRulesVersion := expectedRulesVersion 245 expectedRulesVersion := rules.Version{ 246 Total: reference.Add(3 * time.Hour), 247 Predicates: reference.Add(2 * time.Hour), 248 } 249 250 Convey(`By Strong Read`, func() { 251 test(StrongRead, newRules, expectedRulesVersion) 252 test(StrongRead, newRules, expectedRulesVersion) 253 }) 254 Convey(`By Forced Eviction`, func() { 255 test(expectedRulesVersion.Predicates, newRules, expectedRulesVersion) 256 }) 257 Convey(`By Cache Expiry`, func() { 258 // Test cache is working and still returning the old value. 259 tc.Add(refreshInterval / 2) 260 test(rules.StartingEpoch, rs, oldRulesVersion) 261 262 tc.Add(refreshInterval) 263 264 test(rules.StartingEpoch, newRules, expectedRulesVersion) 265 test(rules.StartingEpoch, newRules, expectedRulesVersion) 266 }) 267 }) 268 }) 269 }) 270 } 271 272 func sortRulesByPredicateLastUpdated(rs []*rules.Entry) []*rules.Entry { 273 result := make([]*rules.Entry, len(rs)) 274 copy(result, rs) 275 sort.Slice(result, func(i, j int) bool { 276 if result[i].PredicateLastUpdateTime.Equal(result[j].PredicateLastUpdateTime) { 277 return result[i].RuleID < result[j].RuleID 278 } 279 return result[i].PredicateLastUpdateTime.After(result[j].PredicateLastUpdateTime) 280 }) 281 return result 282 }