go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/analysis/internal/clustering/rules/lang/lang_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 lang 16 17 import ( 18 "fmt" 19 "strings" 20 "testing" 21 22 "go.chromium.org/luci/analysis/internal/clustering" 23 analysispb "go.chromium.org/luci/analysis/proto/v1" 24 25 . "github.com/smartystreets/goconvey/convey" 26 ) 27 28 func TestRules(t *testing.T) { 29 Convey(`Syntax Parsing`, t, func() { 30 parse := func(input string) error { 31 expr, err := Parse(input) 32 if err != nil { 33 So(expr, ShouldBeNil) 34 } else { 35 So(expr, ShouldNotBeNil) 36 } 37 return err 38 } 39 Convey(`Valid inputs`, func() { 40 validInputs := []string{ 41 `false`, 42 `true`, 43 `true or true and not true`, 44 `(((true)))`, 45 `"" = "foo"`, 46 `"" = "'"`, 47 `"" = "\a\b\f\n\r\t\v\"\101\x42\u0042\U00000042"`, 48 `"" = test`, 49 `"" = TesT`, 50 `test = "foo"`, 51 `test != "foo"`, 52 `test <> "foo"`, 53 `test in ("foo", "bar", reason)`, 54 `test not in ("foo", "bar", reason)`, 55 `not test in ("foo", "bar", reason)`, 56 `test like "%arc%"`, 57 `test not like "%arc%"`, 58 `not test like "%arc%"`, 59 `regexp_contains (test, "^arc\\.")`, 60 `not regexp_contains(test, "^arc\\.")`, 61 `test = "arc.Boot" AND reason LIKE "%failed%"`, 62 } 63 for _, v := range validInputs { 64 So(parse(v), ShouldBeNil) 65 } 66 }) 67 Convey(`Invalid inputs`, func() { 68 invalidInputs := []string{ 69 `'' = 'foo'`, // Uses single quotes. 70 `"" = "\ud800"`, // Illegal Unicode surrogate code point (D800-DFFF). 71 `"" = "\U00110000"`, // Above maximum Unicode code point (10FFFF). 72 `"" = "\c"`, // Illegal escape sequence. 73 `"" = foo`, // Bad identifier. 74 `"" = ?`, // Bad identifier. 75 `test $ "foo"`, // Invalid operator. 76 `test like build`, // Use of non-constant like pattern. 77 `regexp_contains(test, "[")`, // bad regexp. 78 `reason like "foo\\"`, // invalid trailing "\" escape sequence in LIKE pattern. 79 `reason like "foo\\a"`, // invalid escape sequence "\a" in LIKE pattern. 80 `regexp_contains(test, test)`, // Use of non-constant regexp pattern. 81 `regexp_contains(test)`, // Incorrect argument count. 82 `bad_func(test, test)`, // Undeclared function. 83 `reason NOTLIKE "%failed%"`, // Bad operator. 84 } 85 for _, v := range invalidInputs { 86 So(parse(v), ShouldNotBeNil) 87 } 88 }) 89 }) 90 Convey(`Semantics`, t, func() { 91 eval := func(input string, failure *clustering.Failure) bool { 92 eval, err := Parse(input) 93 So(err, ShouldBeNil) 94 return eval.eval(failure) 95 } 96 boot := &clustering.Failure{ 97 TestID: "tast.arc.Boot", 98 Reason: &analysispb.FailureReason{PrimaryErrorMessage: "annotation 1: annotation 2: failure"}, 99 } 100 dbus := &clustering.Failure{ 101 TestID: "tast.example.DBus", 102 Reason: &analysispb.FailureReason{PrimaryErrorMessage: "true was not true"}, 103 } 104 Convey(`String Expression`, func() { 105 So(eval(`test = "tast.arc.Boot"`, boot), ShouldBeTrue) 106 So(eval(`test = "tast.arc.Boot"`, dbus), ShouldBeFalse) 107 So(eval(`test = test`, dbus), ShouldBeTrue) 108 escaping := &clustering.Failure{ 109 TestID: "\a\b\f\n\r\t\v\"\101\x42\u0042\U00000042", 110 } 111 So(eval(`test = "\a\b\f\n\r\t\v\"\101\x42\u0042\U00000042"`, escaping), ShouldBeTrue) 112 }) 113 Convey(`Boolean Constants`, func() { 114 So(eval(`TRUE`, boot), ShouldBeTrue) 115 So(eval(`tRue`, boot), ShouldBeTrue) 116 So(eval(`FALSE`, boot), ShouldBeFalse) 117 }) 118 Convey(`Boolean Item`, func() { 119 So(eval(`(((TRUE)))`, boot), ShouldBeTrue) 120 So(eval(`(FALSE)`, boot), ShouldBeFalse) 121 }) 122 Convey(`Boolean Predicate`, func() { 123 Convey(`Comp`, func() { 124 So(eval(`test = "tast.arc.Boot"`, boot), ShouldBeTrue) 125 So(eval(`test = "tast.arc.Boot"`, dbus), ShouldBeFalse) 126 So(eval(`test <> "tast.arc.Boot"`, boot), ShouldBeFalse) 127 So(eval(`test <> "tast.arc.Boot"`, dbus), ShouldBeTrue) 128 So(eval(`test != "tast.arc.Boot"`, boot), ShouldBeFalse) 129 So(eval(`test != "tast.arc.Boot"`, dbus), ShouldBeTrue) 130 }) 131 Convey(`Negatable`, func() { 132 So(eval(`test NOT LIKE "tast.arc.%"`, boot), ShouldBeFalse) 133 So(eval(`test NOT LIKE "tast.arc.%"`, dbus), ShouldBeTrue) 134 So(eval(`test LIKE "tast.arc.%"`, boot), ShouldBeTrue) 135 So(eval(`test LIKE "tast.arc.%"`, dbus), ShouldBeFalse) 136 }) 137 Convey(`Like`, func() { 138 So(eval(`test LIKE "tast.arc.%"`, boot), ShouldBeTrue) 139 So(eval(`test LIKE "tast.arc.%"`, dbus), ShouldBeFalse) 140 So(eval(`test LIKE "arc.%"`, boot), ShouldBeFalse) 141 So(eval(`test LIKE ".Boot"`, boot), ShouldBeFalse) 142 So(eval(`test LIKE "%arc.%"`, boot), ShouldBeTrue) 143 So(eval(`test LIKE "%.Boot"`, boot), ShouldBeTrue) 144 So(eval(`test LIKE "tast.%.Boot"`, boot), ShouldBeTrue) 145 146 escapeTest := &clustering.Failure{ 147 TestID: "a\\.+*?()|[]{}^$a", 148 } 149 So(eval(`test LIKE "\\\\.+*?()|[]{}^$a"`, escapeTest), ShouldBeFalse) 150 So(eval(`test LIKE "a\\\\.+*?()|[]{}^$"`, escapeTest), ShouldBeFalse) 151 So(eval(`test LIKE "a\\\\.+*?()|[]{}^$a"`, escapeTest), ShouldBeTrue) 152 So(eval(`test LIKE "a\\\\.+*?()|[]{}^$_"`, escapeTest), ShouldBeTrue) 153 So(eval(`test LIKE "a\\\\.+*?()|[]{}^$%"`, escapeTest), ShouldBeTrue) 154 155 escapeTest2 := &clustering.Failure{ 156 TestID: "a\\.+*?()|[]{}^$_", 157 } 158 So(eval(`test LIKE "a\\\\.+*?()|[]{}^$\\_"`, escapeTest), ShouldBeFalse) 159 So(eval(`test LIKE "a\\\\.+*?()|[]{}^$\\_"`, escapeTest2), ShouldBeTrue) 160 161 escapeTest3 := &clustering.Failure{ 162 TestID: "a\\.+*?()|[]{}^$%", 163 } 164 165 So(eval(`test LIKE "a\\\\.+*?()|[]{}^$\\%"`, escapeTest), ShouldBeFalse) 166 So(eval(`test LIKE "a\\\\.+*?()|[]{}^$\\%"`, escapeTest3), ShouldBeTrue) 167 168 escapeTest4 := &clustering.Failure{ 169 Reason: &analysispb.FailureReason{ 170 PrimaryErrorMessage: "a\nb", 171 }, 172 } 173 So(eval(`reason LIKE "a"`, escapeTest4), ShouldBeFalse) 174 So(eval(`reason LIKE "%"`, escapeTest4), ShouldBeTrue) 175 So(eval(`reason LIKE "a%b"`, escapeTest4), ShouldBeTrue) 176 So(eval(`reason LIKE "a_b"`, escapeTest4), ShouldBeTrue) 177 }) 178 Convey(`In`, func() { 179 So(eval(`test IN ("tast.arc.Boot")`, boot), ShouldBeTrue) 180 So(eval(`test IN ("tast.arc.Clipboard", "tast.arc.Boot")`, boot), ShouldBeTrue) 181 So(eval(`test IN ("tast.arc.Clipboard", "tast.arc.Boot")`, dbus), ShouldBeFalse) 182 }) 183 }) 184 Convey(`Boolean Function`, func() { 185 So(eval(`REGEXP_CONTAINS(test, "tast\\.arc\\..*")`, boot), ShouldBeTrue) 186 So(eval(`REGEXP_CONTAINS(test, "tast\\.arc\\..*")`, dbus), ShouldBeFalse) 187 }) 188 Convey(`Boolean Factor`, func() { 189 So(eval(`NOT TRUE`, boot), ShouldBeFalse) 190 So(eval(`NOT FALSE`, boot), ShouldBeTrue) 191 So(eval(`NOT REGEXP_CONTAINS(test, "tast\\.arc\\..*")`, boot), ShouldBeFalse) 192 So(eval(`NOT REGEXP_CONTAINS(test, "tast\\.arc\\..*")`, dbus), ShouldBeTrue) 193 }) 194 Convey(`Boolean Term`, func() { 195 So(eval(`TRUE AND TRUE`, boot), ShouldBeTrue) 196 So(eval(`TRUE AND FALSE`, boot), ShouldBeFalse) 197 So(eval(`NOT FALSE AND NOT FALSE`, boot), ShouldBeTrue) 198 So(eval(`NOT FALSE AND NOT FALSE AND NOT FALSE`, boot), ShouldBeTrue) 199 }) 200 Convey(`Boolean Expression`, func() { 201 So(eval(`TRUE OR FALSE`, boot), ShouldBeTrue) 202 So(eval(`FALSE AND FALSE OR TRUE`, boot), ShouldBeTrue) 203 So(eval(`FALSE AND TRUE OR FALSE OR FALSE AND TRUE`, boot), ShouldBeFalse) 204 }) 205 }) 206 Convey(`Formatting`, t, func() { 207 roundtrip := func(input string) string { 208 eval, err := Parse(input) 209 So(err, ShouldBeNil) 210 return eval.String() 211 } 212 // The following statements should be formatted exactly the same when they are printed. 213 inputs := []string{ 214 `FALSE`, 215 `TRUE`, 216 `TRUE OR TRUE AND NOT TRUE`, 217 `(((TRUE)))`, 218 `"" = "foo"`, 219 `"" = "'"`, 220 `"" = "\a\b\f\n\r\t\v\"\101\x42\u0042\U00000042"`, 221 `"" = test`, 222 `test = "foo"`, 223 `test != "foo"`, 224 `test <> "foo"`, 225 `test IN ("foo", "bar", reason)`, 226 `test NOT IN ("foo", "bar", reason)`, 227 `NOT test IN ("foo", "bar", reason)`, 228 `test LIKE "%arc%"`, 229 `test NOT LIKE "%arc%"`, 230 `NOT test LIKE "%arc%"`, 231 `regexp_contains(test, "^arc\\.")`, 232 `NOT regexp_contains(test, "^arc\\.")`, 233 `test = "arc.Boot" AND reason LIKE "%failed%"`, 234 } 235 for _, input := range inputs { 236 So(roundtrip(input), ShouldEqual, input) 237 } 238 }) 239 } 240 241 // On my machine, I get the following reuslts: 242 // cpu: Intel(R) Xeon(R) CPU @ 2.00GHz 243 // BenchmarkRules-48 51 22406568 ns/op 481 B/op 0 allocs/op 244 func BenchmarkRules(b *testing.B) { 245 // Setup 1000 rules. 246 var rules []*Expr 247 for i := 0; i < 1000; i++ { 248 rule := `test LIKE "%arc.Boot` + fmt.Sprintf("%v", i) + `.%" AND reason LIKE "%failed` + fmt.Sprintf("%v", i) + `.%"` 249 expr, err := Parse(rule) 250 if err != nil { 251 b.Error(err) 252 } 253 rules = append(rules, expr) 254 } 255 var testText strings.Builder 256 var reasonText strings.Builder 257 for j := 0; j < 100; j++ { 258 testText.WriteString("blah") 259 reasonText.WriteString("blah") 260 } 261 testText.WriteString("arc.Boot0.") 262 reasonText.WriteString("failed0.") 263 for j := 0; j < 100; j++ { 264 testText.WriteString("blah") 265 reasonText.WriteString("blah") 266 } 267 data := &clustering.Failure{ 268 TestID: testText.String(), 269 Reason: &analysispb.FailureReason{PrimaryErrorMessage: reasonText.String()}, 270 } 271 272 // Start benchmark. 273 b.ResetTimer() 274 for n := 0; n < b.N; n++ { 275 for j, r := range rules { 276 matches := r.Evaluate(data) 277 shouldMatch := j == 0 278 if matches != shouldMatch { 279 b.Errorf("Unexpected result at %v: got %v, want %v", j, matches, shouldMatch) 280 } 281 } 282 } 283 }