go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/analysis/internal/clustering/algorithms/rulesalgorithm/rules_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 rulesalgorithm 16 17 import ( 18 "sort" 19 "testing" 20 "time" 21 22 "go.chromium.org/luci/analysis/internal/clustering" 23 "go.chromium.org/luci/analysis/internal/clustering/rules" 24 "go.chromium.org/luci/analysis/internal/clustering/rules/cache" 25 pb "go.chromium.org/luci/analysis/proto/v1" 26 27 . "github.com/smartystreets/goconvey/convey" 28 ) 29 30 func TestAlgorithm(t *testing.T) { 31 Convey(`Name`, t, func() { 32 // Algorithm name should be valid. 33 So(clustering.AlgorithmRe.MatchString(AlgorithmName), ShouldBeTrue) 34 }) 35 Convey(`Cluster from scratch`, t, func() { 36 a := &Algorithm{} 37 existingRulesVersion := rules.StartingEpoch 38 ids := make(map[string]struct{}) 39 Convey(`Empty Rules`, func() { 40 ruleset := &cache.Ruleset{} 41 a.Cluster(ruleset, existingRulesVersion, ids, &clustering.Failure{ 42 Reason: &pb.FailureReason{PrimaryErrorMessage: "Null pointer exception at ip 0x45637271"}, 43 }) 44 So(ids, ShouldBeEmpty) 45 }) 46 Convey(`With Rules`, func() { 47 rule1, err := cache.NewCachedRule( 48 rules.NewRule(100). 49 WithRuleDefinition(`test = "ninja://test_name_one/"`). 50 Build()) 51 So(err, ShouldBeNil) 52 rule2, err := cache.NewCachedRule( 53 rules.NewRule(101). 54 WithRuleDefinition(`reason LIKE "failed to connect to %.%.%.%"`). 55 Build()) 56 So(err, ShouldBeNil) 57 58 rulesVersion := rules.Version{ 59 Predicates: time.Now(), 60 } 61 lastUpdated := time.Now() 62 rules := []*cache.CachedRule{rule1, rule2} 63 ruleset := cache.NewRuleset("myproject", rules, rulesVersion, lastUpdated) 64 65 Convey(`Without failure reason`, func() { 66 Convey(`Matching`, func() { 67 a.Cluster(ruleset, existingRulesVersion, ids, &clustering.Failure{ 68 TestID: "ninja://test_name_one/", 69 }) 70 So(ids, ShouldResemble, map[string]struct{}{ 71 rule1.Rule.RuleID: {}, 72 }) 73 }) 74 Convey(`Non-matcing`, func() { 75 a.Cluster(ruleset, existingRulesVersion, ids, &clustering.Failure{ 76 TestID: "ninja://test_name_two/", 77 }) 78 So(ids, ShouldBeEmpty) 79 }) 80 }) 81 Convey(`Matches one`, func() { 82 a.Cluster(ruleset, existingRulesVersion, ids, &clustering.Failure{ 83 TestID: "ninja://test_name_three/", 84 Reason: &pb.FailureReason{ 85 PrimaryErrorMessage: "failed to connect to 192.168.0.1", 86 }, 87 }) 88 So(ids, ShouldResemble, map[string]struct{}{ 89 rule2.Rule.RuleID: {}, 90 }) 91 }) 92 Convey(`Matches multiple`, func() { 93 a.Cluster(ruleset, existingRulesVersion, ids, &clustering.Failure{ 94 TestID: "ninja://test_name_one/", 95 Reason: &pb.FailureReason{ 96 PrimaryErrorMessage: "failed to connect to 192.168.0.1", 97 }, 98 }) 99 expectedIDs := []string{rule1.Rule.RuleID, rule2.Rule.RuleID} 100 sort.Strings(expectedIDs) 101 So(ids, ShouldResemble, map[string]struct{}{ 102 rule1.Rule.RuleID: {}, 103 rule2.Rule.RuleID: {}, 104 }) 105 }) 106 }) 107 }) 108 Convey(`Cluster incrementally`, t, func() { 109 a := &Algorithm{} 110 originalRulesVersion := time.Date(2020, time.January, 1, 1, 0, 0, 0, time.UTC) 111 testFailure := &clustering.Failure{ 112 TestID: "ninja://test_name_one/", 113 Reason: &pb.FailureReason{ 114 PrimaryErrorMessage: "failed to connect to 192.168.0.1", 115 }, 116 } 117 118 // The ruleset we are incrementally clustering with has a new rule 119 // (rule 3) and no longer has rule 2. We silently set the definition 120 // of rule1 to FALSE without changing its last updated time (this 121 // should never happen in reality) to check it is never evaluated. 122 rule1, err := cache.NewCachedRule( 123 rules.NewRule(100).WithRuleDefinition(`FALSE`). 124 WithPredicateLastUpdateTime(originalRulesVersion).Build()) 125 So(err, ShouldBeNil) 126 rule3, err := cache.NewCachedRule( 127 rules.NewRule(102). 128 WithRuleDefinition(`reason LIKE "failed to connect to %"`). 129 WithPredicateLastUpdateTime(originalRulesVersion.Add(time.Hour)).Build()) 130 So(err, ShouldBeNil) 131 132 rs := []*cache.CachedRule{rule1, rule3} 133 newRulesVersion := rules.Version{ 134 Predicates: originalRulesVersion.Add(time.Hour), 135 } 136 lastUpdated := time.Now() 137 secondRuleset := cache.NewRuleset("myproject", rs, newRulesVersion, lastUpdated) 138 139 ids := map[string]struct{}{ 140 rule1.Rule.RuleID: {}, 141 "rule2-id": {}, 142 } 143 144 // Test incrementally clustering leads to the correct outcome, 145 // matching rule 3 and unmatching rule 2. 146 a.Cluster(secondRuleset, originalRulesVersion, ids, testFailure) 147 So(ids, ShouldResemble, map[string]struct{}{ 148 rule1.Rule.RuleID: {}, 149 rule3.Rule.RuleID: {}, 150 }) 151 }) 152 }