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  }