go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/analysis/internal/clustering/algorithms/failurereason/failurereason_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 failurereason 16 17 import ( 18 "testing" 19 20 "go.chromium.org/luci/analysis/internal/clustering" 21 "go.chromium.org/luci/analysis/internal/clustering/rules/lang" 22 "go.chromium.org/luci/analysis/internal/config/compiledcfg" 23 configpb "go.chromium.org/luci/analysis/proto/config" 24 pb "go.chromium.org/luci/analysis/proto/v1" 25 26 . "github.com/smartystreets/goconvey/convey" 27 ) 28 29 func TestAlgorithm(t *testing.T) { 30 cfgpb := &configpb.ProjectConfig{ 31 Clustering: &configpb.Clustering{ 32 ReasonMaskPatterns: []string{ 33 `(?:^\[Fixture failure\] )([a-zA-Z0-9_]+)[:]`, 34 }, 35 }, 36 } 37 Convey(`Name`, t, func() { 38 // Algorithm name should be valid. 39 a := &Algorithm{} 40 So(clustering.AlgorithmRe.MatchString(a.Name()), ShouldBeTrue) 41 }) 42 Convey(`Cluster`, t, func() { 43 a := &Algorithm{} 44 cfg, err := compiledcfg.NewConfig(cfgpb) 45 So(err, ShouldBeNil) 46 47 Convey(`Does not cluster test result without failure reason`, func() { 48 id := a.Cluster(cfg, &clustering.Failure{}) 49 So(id, ShouldBeNil) 50 }) 51 Convey(`ID of appropriate length`, func() { 52 id := a.Cluster(cfg, &clustering.Failure{ 53 Reason: &pb.FailureReason{PrimaryErrorMessage: "abcd this is a test failure message"}, 54 }) 55 // IDs may be 16 bytes at most. 56 So(len(id), ShouldBeGreaterThan, 0) 57 So(len(id), ShouldBeLessThanOrEqualTo, clustering.MaxClusterIDBytes) 58 }) 59 Convey(`Same ID for same cluster with different numbers`, func() { 60 id1 := a.Cluster(cfg, &clustering.Failure{ 61 Reason: &pb.FailureReason{PrimaryErrorMessage: "Null pointer exception at ip 0x45637271"}, 62 }) 63 id2 := a.Cluster(cfg, &clustering.Failure{ 64 Reason: &pb.FailureReason{PrimaryErrorMessage: "Null pointer exception at ip 0x12345678"}, 65 }) 66 So(id2, ShouldResemble, id1) 67 }) 68 Convey(`Different ID for different clusters`, func() { 69 id1 := a.Cluster(cfg, &clustering.Failure{ 70 Reason: &pb.FailureReason{PrimaryErrorMessage: "Exception in TestMethod"}, 71 }) 72 id2 := a.Cluster(cfg, &clustering.Failure{ 73 Reason: &pb.FailureReason{PrimaryErrorMessage: "Exception in MethodUnderTest"}, 74 }) 75 So(id2, ShouldNotResemble, id1) 76 }) 77 }) 78 Convey(`Failure Association Rule`, t, func() { 79 a := &Algorithm{} 80 cfg, err := compiledcfg.NewConfig(cfgpb) 81 So(err, ShouldBeNil) 82 83 test := func(failure *clustering.Failure, expectedRule string) { 84 rule := a.FailureAssociationRule(cfg, failure) 85 So(rule, ShouldEqual, expectedRule) 86 87 // Test the rule is valid syntax and matches at least the example failure. 88 expr, err := lang.Parse(rule) 89 So(err, ShouldBeNil) 90 So(expr.Evaluate(failure), ShouldBeTrue) 91 } 92 Convey(`Hexadecimal`, func() { 93 failure := &clustering.Failure{ 94 Reason: &pb.FailureReason{PrimaryErrorMessage: "Null pointer exception at ip 0x45637271"}, 95 } 96 test(failure, `reason LIKE "Null pointer exception at ip %"`) 97 }) 98 Convey(`Numeric`, func() { 99 failure := &clustering.Failure{ 100 Reason: &pb.FailureReason{PrimaryErrorMessage: "Could not connect to 127.1.2.1: connection refused"}, 101 } 102 test(failure, `reason LIKE "Could not connect to %.%.%.%: connection refused"`) 103 }) 104 Convey(`Base64`, func() { 105 failure := &clustering.Failure{ 106 Reason: &pb.FailureReason{PrimaryErrorMessage: "Received unexpected response: AdafdxAAD17917+/="}, 107 } 108 test(failure, `reason LIKE "Received unexpected response: %"`) 109 }) 110 Convey(`Escaping`, func() { 111 failure := &clustering.Failure{ 112 Reason: &pb.FailureReason{PrimaryErrorMessage: `_%"'+[]|` + "\u0000\r\n\v\u202E\u2066 AdafdxAAD17917+/="}, 113 } 114 test(failure, `reason LIKE "\\_\\%\"'+[]|\x00\r\n\v\u202e\u2066 %"`) 115 }) 116 Convey(`Multiline`, func() { 117 failure := &clustering.Failure{ 118 Reason: &pb.FailureReason{ 119 // Previously "ce\n ... Ac" matched the hexadecimal format 120 // for hexadecimal strings of 16 characters or more. 121 PrimaryErrorMessage: "Expected: to be called once\n Actual: never called", 122 }, 123 } 124 test(failure, `reason LIKE "Expected: to be called once\n Actual: never called"`) 125 }) 126 Convey(`User-defined masking`, func() { 127 failure := &clustering.Failure{ 128 Reason: &pb.FailureReason{PrimaryErrorMessage: `[Fixture failure] crostiniBuster: Failed to install Crostini: 3 is not 5`}, 129 } 130 test(failure, `reason LIKE "[Fixture failure] %: Failed to install Crostini: % is not %"`) 131 }) 132 }) 133 Convey(`Cluster Title`, t, func() { 134 a := &Algorithm{} 135 cfg, err := compiledcfg.NewConfig(cfgpb) 136 So(err, ShouldBeNil) 137 138 Convey(`Baseline`, func() { 139 failure := &clustering.Failure{ 140 Reason: &pb.FailureReason{PrimaryErrorMessage: "Null pointer exception at ip 0x45637271"}, 141 } 142 title := a.ClusterTitle(cfg, failure) 143 So(title, ShouldEqual, `Null pointer exception at ip %`) 144 }) 145 Convey(`Escaping`, func() { 146 failure := &clustering.Failure{ 147 Reason: &pb.FailureReason{PrimaryErrorMessage: `_%"'+[]|` + "\u0000\r\n\v\u202E\u2066 AdafdxAAD17917+/="}, 148 } 149 title := a.ClusterTitle(cfg, failure) 150 So(title, ShouldEqual, `_%\"'+[]|\x00\r\n\v\u202e\u2066 %`) 151 }) 152 Convey(`User-defined masking`, func() { 153 failure := &clustering.Failure{ 154 Reason: &pb.FailureReason{PrimaryErrorMessage: `[Fixture failure] crostiniBuster: Failed to install Crostini: 3 is not 5`}, 155 } 156 title := a.ClusterTitle(cfg, failure) 157 So(title, ShouldEqual, `[Fixture failure] %: Failed to install Crostini: % is not %`) 158 }) 159 }) 160 Convey(`Cluster Description`, t, func() { 161 a := &Algorithm{} 162 cfg, err := compiledcfg.NewConfig(cfgpb) 163 So(err, ShouldBeNil) 164 165 Convey(`Baseline`, func() { 166 failure := &clustering.ClusterSummary{ 167 Example: clustering.Failure{ 168 Reason: &pb.FailureReason{PrimaryErrorMessage: "Null pointer exception at ip 0x45637271"}, 169 }, 170 TopTests: []string{ 171 "ninja://test_one", 172 "ninja://test_two", 173 "ninja://test_three", 174 }, 175 } 176 description, err := a.ClusterDescription(cfg, failure) 177 So(err, ShouldBeNil) 178 So(description.Title, ShouldEqual, `Null pointer exception at ip 0x45637271`) 179 So(description.Description, ShouldContainSubstring, `Null pointer exception at ip 0x45637271`) 180 So(description.Description, ShouldContainSubstring, `- ninja://test_one`) 181 So(description.Description, ShouldContainSubstring, `- ninja://test_three`) 182 So(description.Description, ShouldContainSubstring, `- ninja://test_three`) 183 }) 184 Convey(`Escaping`, func() { 185 summary := &clustering.ClusterSummary{ 186 Example: clustering.Failure{ 187 Reason: &pb.FailureReason{PrimaryErrorMessage: `_%"'+[]|` + "\u0000\r\n\v\u202E\u2066 AdafdxAAD17917+/="}, 188 }, 189 TopTests: []string{ 190 "\u2066\u202E\v\n\r\u0000", 191 }, 192 } 193 description, err := a.ClusterDescription(cfg, summary) 194 So(err, ShouldBeNil) 195 So(description.Title, ShouldEqual, `_%\"'+[]|\x00\r\n\v\u202e\u2066 AdafdxAAD17917+/=`) 196 So(description.Description, ShouldContainSubstring, `_%\"'+[]|\x00\r\n\v\u202e\u2066 AdafdxAAD17917+/=`) 197 So(description.Description, ShouldContainSubstring, `- \u2066\u202e\v\n\r\x00`) 198 }) 199 }) 200 }