From d4b0fe77d24c177ca0861fa989b27e86cdeedd36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linas=20Med=C5=BEi=C5=ABnas?= Date: Sat, 4 Nov 2023 06:27:17 +0200 Subject: [PATCH] [query] Fix matching of disjunction including empty string (#4258) --- src/m3ninx/index/regexp.go | 23 +++--- src/m3ninx/index/regexp_prop_test.go | 5 +- src/m3ninx/index/regexp_test.go | 91 +++++++++++---------- src/query/storage/index.go | 81 ++++++++++++++----- src/query/storage/index_test.go | 66 +++++++++++++++ src/x/regexp/empty_matcher.go | 115 +++++++++++++++++++++++++++ src/x/regexp/empty_matcher_test.go | 90 +++++++++++++++++++++ 7 files changed, 386 insertions(+), 85 deletions(-) create mode 100644 src/x/regexp/empty_matcher.go create mode 100644 src/x/regexp/empty_matcher_test.go diff --git a/src/m3ninx/index/regexp.go b/src/m3ninx/index/regexp.go index c8114db734..6aa250d595 100644 --- a/src/m3ninx/index/regexp.go +++ b/src/m3ninx/index/regexp.go @@ -21,7 +21,6 @@ package index import ( - "context" "fmt" re "regexp" "regexp/syntax" @@ -37,7 +36,6 @@ var ( // dotStartCompiledRegex is a CompileRegex that matches any input. // NB: It can be accessed through DotStartCompiledRegex(). dotStarCompiledRegex CompiledRegex - cacheContext = context.Background() ) func init() { @@ -146,7 +144,7 @@ func CompileRegex(r []byte) (CompiledRegex, error) { // Issue (a): Vellum does not allow regexps which use characters '^', or '$'. // To address this issue, we strip these characters from appropriate locations in the parsed syntax.Regexp // for Vellum's RE. - vellumRe, err := ensureRegexpUnanchored(reAst) + vellumRe, err := EnsureRegexpUnanchored(reAst) if err != nil { return CompiledRegex{}, fmt.Errorf("unable to create FST re: %v", err) } @@ -154,10 +152,7 @@ func CompileRegex(r []byte) (CompiledRegex, error) { // Issue (b): Vellum treats every regular expression as anchored, where as the map-backed segment does not. // To address this issue, we ensure that every incoming regular expression is modified to be anchored // when querying the map-backed segment, and isn't anchored when querying Vellum's RE. - simpleRe, err := ensureRegexpAnchored(vellumRe) - if err != nil { - return CompiledRegex{}, fmt.Errorf("unable to create map re: %v", err) - } + simpleRe := EnsureRegexpAnchored(vellumRe) simpleRE, err := re.Compile(simpleRe.String()) if err != nil { @@ -191,10 +186,10 @@ func parseRegexp(re string) (*syntax.Regexp, error) { return syntax.Parse(re, syntax.Perl) } -// ensureRegexpAnchored adds '^' and '$' characters to appropriate locations in the parsed syntax.Regexp, -// to ensure every input regular expression is converted to it's equivalent anchored regular expression. +// EnsureRegexpAnchored adds '^' and '$' characters to appropriate locations in the parsed syntax.Regexp, +// to ensure every input regular expression is converted to its equivalent anchored regular expression. // NB: assumes input regexp AST is un-anchored. -func ensureRegexpAnchored(unanchoredRegexp *syntax.Regexp) (*syntax.Regexp, error) { +func EnsureRegexpAnchored(unanchoredRegexp *syntax.Regexp) *syntax.Regexp { ast := &syntax.Regexp{ Op: syntax.OpConcat, Flags: syntax.Perl, @@ -210,13 +205,13 @@ func ensureRegexpAnchored(unanchoredRegexp *syntax.Regexp) (*syntax.Regexp, erro }, }, } - return simplify(ast.Simplify()), nil + return simplify(ast.Simplify()) } -// ensureRegexpUnanchored strips '^' and '$' characters from appropriate locations in the parsed syntax.Regexp, -// to ensure every input regular expression is converted to it's equivalent un-anchored regular expression +// EnsureRegexpUnanchored strips '^' and '$' characters from appropriate locations in the parsed syntax.Regexp, +// to ensure every input regular expression is converted to its equivalent un-anchored regular expression // assuming the entire input is matched. -func ensureRegexpUnanchored(parsed *syntax.Regexp) (*syntax.Regexp, error) { +func EnsureRegexpUnanchored(parsed *syntax.Regexp) (*syntax.Regexp, error) { r, _, err := ensureRegexpUnanchoredHelper(parsed, true, true) if err != nil { return nil, err diff --git a/src/m3ninx/index/regexp_prop_test.go b/src/m3ninx/index/regexp_prop_test.go index 1d884faaff..444dd7a196 100644 --- a/src/m3ninx/index/regexp_prop_test.go +++ b/src/m3ninx/index/regexp_prop_test.go @@ -77,10 +77,9 @@ func TestRegexpCompilationProperty(t *testing.T) { func compileRegexp(x string, t *testing.T) *regexp.Regexp { ast, err := parseRegexp(x) require.NoError(t, err) - astp, err := ensureRegexpUnanchored(ast) - require.NoError(t, err) - ast2p, err := ensureRegexpAnchored(astp) + astp, err := EnsureRegexpUnanchored(ast) require.NoError(t, err) + ast2p := EnsureRegexpAnchored(astp) re, err := regexp.Compile(ast2p.String()) require.NoError(t, err) return re diff --git a/src/m3ninx/index/regexp_test.go b/src/m3ninx/index/regexp_test.go index fbe9641024..e8ba1f0a8a 100644 --- a/src/m3ninx/index/regexp_test.go +++ b/src/m3ninx/index/regexp_test.go @@ -57,162 +57,162 @@ func TestEnsureRegexpUnachoredee(t *testing.T) { func TestEnsureRegexpUnachored(t *testing.T) { testCases := []testCase{ - testCase{ + { name: "naked ^", input: "^", expectedOutput: "emp{}", }, - testCase{ + { name: "naked $", input: "$", expectedOutput: "emp{}", }, - testCase{ + { name: "empty string ^$", input: "^$", expectedOutput: "cat{}", }, - testCase{ + { name: "invalid naked concat ^$", input: "$^", expectedOutput: "cat{eot{}bot{}}", }, - testCase{ + { name: "simple case of ^", input: "^abc", expectedOutput: "str{abc}", }, - testCase{ + { name: "simple case of $", input: "abc$", expectedOutput: "str{abc}", }, - testCase{ + { name: "simple case of both ^ & $", input: "^abc$", expectedOutput: "str{abc}", }, - testCase{ + { name: "weird case of internal ^", input: "^a^bc$", expectedOutput: "cat{lit{a}bot{}str{bc}}", }, - testCase{ + { name: "weird case of internal $", input: "^a$bc$", expectedOutput: "cat{lit{a}eot{}str{bc}}", }, - testCase{ + { name: "alternate of sub expressions with only legal ^ and $", input: "(?:^abc$)|(?:^xyz$)", expectedOutput: "alt{str{abc}str{xyz}}", }, - testCase{ + { name: "concat of sub expressions with only legal ^ and $", input: "(^abc$)(?:^xyz$)", expectedOutput: "cat{cap{cat{str{abc}eot{}}}bot{}str{xyz}}", }, - testCase{ + { name: "alternate of sub expressions with illegal ^ and $", input: "(?:^a$bc$)|(?:^xyz$)", expectedOutput: "alt{cat{lit{a}eot{}str{bc}}str{xyz}}", }, - testCase{ + { name: "concat of sub expressions with illegal ^ and $", input: "(?:^a$bc$)(?:^xyz$)", expectedOutput: "cat{lit{a}eot{}str{bc}eot{}bot{}str{xyz}}", }, - testCase{ + { name: "question mark case both boundaries success", input: "(?:^abc$)?", expectedOutput: "que{str{abc}}", }, - testCase{ + { name: "question mark case only ^", input: "(?:^abc)?", expectedOutput: "que{str{abc}}", }, - testCase{ + { name: "question mark case only $", input: "(?:abc$)?", expectedOutput: "que{str{abc}}", }, - testCase{ + { name: "question concat case $", input: "abc$?", expectedOutput: "str{abc}", }, - testCase{ + { name: "star mark case both boundaries success", input: "(?:^abc$)*", expectedOutput: "cat{que{str{abc}}star{cat{bot{}str{abc}eot{}}}}", }, - testCase{ + { name: "star mark case only ^", input: "(?:^abc)*", expectedOutput: "cat{que{str{abc}}star{cat{bot{}str{abc}}}}", }, - testCase{ + { name: "star mark case only $", input: "(?:abc$)*", expectedOutput: "cat{que{str{abc}}star{cat{str{abc}eot{}}}}", }, - testCase{ + { name: "star concat case $", input: "abc$*", expectedOutput: "cat{str{abc}star{eot{}}}", }, - testCase{ + { name: "star concat case ^", input: "^*abc", expectedOutput: "cat{star{bot{}}str{abc}}", }, - testCase{ + { name: "plus mark case both boundaries success", input: "(?:^abc$)+", expectedOutput: "cat{str{abc}star{cat{bot{}str{abc}eot{}}}}", }, - testCase{ + { name: "plus mark case with capturing group", input: "(^abc$)+", expectedOutput: "cat{cap{str{abc}}star{cap{cat{bot{}str{abc}eot{}}}}}", }, - testCase{ + { name: "plus mark case only ^", input: "(?:^abc)+", expectedOutput: "cat{str{abc}star{cat{bot{}str{abc}}}}", }, - testCase{ + { name: "plus mark case only $", input: "(?:abc$)+", expectedOutput: "cat{str{abc}star{cat{str{abc}eot{}}}}", }, - testCase{ + { name: "plus concat case $", input: "abc$+", expectedOutput: "cat{str{abc}star{eot{}}}", }, - testCase{ + { name: "plus concat case ^", input: "^+abc", expectedOutput: "cat{star{bot{}}str{abc}}", }, - testCase{ + { name: "repeat case both boundaries success", input: "(?:^abc$){3,4}", expectedOutput: "cat{str{abc}rep{2,3 cat{bot{}str{abc}eot{}}}}", }, - testCase{ + { name: "repeat case unbounded max", input: "(?:^abc$){3,}", expectedOutput: "cat{str{abc}rep{2,-1 cat{bot{}str{abc}eot{}}}}", }, - testCase{ + { name: "repeat case unbounded max with 1 min", input: "(?:^abc$){1,2}", expectedOutput: "cat{str{abc}rep{0,1 cat{bot{}str{abc}eot{}}}}", }, - testCase{ + { name: "repeat case unbounded max with 0 min", input: "(?:^abc$){0,2}", expectedOutput: "rep{0,2 cat{bot{}str{abc}eot{}}}", @@ -222,7 +222,7 @@ func TestEnsureRegexpUnachored(t *testing.T) { t.Run(tc.name, func(t *testing.T) { re, err := parseRegexp(tc.input) require.NoError(t, err) - parsed, err := ensureRegexpUnanchored(re) + parsed, err := EnsureRegexpUnanchored(re) require.NoError(t, err) assert.Equal(t, tc.expectedOutput, dumpRegexp(parsed)) }) @@ -231,57 +231,57 @@ func TestEnsureRegexpUnachored(t *testing.T) { func TestEnsureRegexpAnchored(t *testing.T) { testCases := []testCase{ - testCase{ + { name: "naked ^", input: "(?:)", expectedOutput: "cat{bot{}eot{\\z}}", }, - testCase{ + { name: "invalid naked concat ^$", input: "$^", expectedOutput: "cat{bot{}eot{}bot{}eot{\\z}}", }, - testCase{ + { name: "simple case of literal", input: "abc", expectedOutput: "cat{bot{}str{abc}eot{\\z}}", }, - testCase{ + { name: "weird case of internal ^", input: "a^bc", expectedOutput: "cat{bot{}lit{a}bot{}str{bc}eot{\\z}}", }, - testCase{ + { name: "weird case of internal $", input: "a$bc", expectedOutput: "cat{bot{}lit{a}eot{}str{bc}eot{\\z}}", }, - testCase{ + { name: "alternate of sub expressions with only legal ^ and $", input: "abc|xyz", expectedOutput: "cat{bot{}alt{str{abc}str{xyz}}eot{\\z}}", }, - testCase{ + { name: "concat of sub expressions with only legal ^ and $", input: "(?:abc)(?:xyz)", expectedOutput: "cat{bot{}str{abcxyz}eot{\\z}}", }, - testCase{ + { name: "question mark case both boundaries success", input: "(?:abc)?", expectedOutput: "cat{bot{}que{str{abc}}eot{\\z}}", }, - testCase{ + { name: "star mark case both boundaries success", input: "(?:abc)*", expectedOutput: "cat{bot{}star{str{abc}}eot{\\z}}", }, - testCase{ + { name: "plus mark case both boundaries success", input: "(?:abc)+", expectedOutput: "cat{bot{}plus{str{abc}}eot{\\z}}", }, - testCase{ + { name: "repeat case both boundaries success", input: "(?:abc){3,4}", expectedOutput: "cat{bot{}str{abc}str{abc}str{abc}que{str{abc}}eot{\\z}}", @@ -291,8 +291,7 @@ func TestEnsureRegexpAnchored(t *testing.T) { t.Run(tc.name, func(t *testing.T) { re, err := parseRegexp(tc.input) require.NoError(t, err) - parsed, err := ensureRegexpAnchored(re) - require.NoError(t, err) + parsed := EnsureRegexpAnchored(re) assert.Equal(t, tc.expectedOutput, dumpRegexp(parsed)) }) } diff --git a/src/query/storage/index.go b/src/query/storage/index.go index 72c0d073ca..826db03e8f 100644 --- a/src/query/storage/index.go +++ b/src/query/storage/index.go @@ -21,6 +21,7 @@ package storage import ( + "bytes" "fmt" "time" @@ -30,13 +31,13 @@ import ( "github.com/m3db/m3/src/query/storage/m3/consolidators" xerrors "github.com/m3db/m3/src/x/errors" "github.com/m3db/m3/src/x/ident" + "github.com/m3db/m3/src/x/regexp" xtime "github.com/m3db/m3/src/x/time" ) -const ( - dot = byte('.') - plus = byte('+') - star = byte('*') +var ( + dotStar = []byte(".*") + dotPlus = []byte(".+") ) // FromM3IdentToMetric converts an M3 ident metric to a coordinator metric. @@ -176,7 +177,10 @@ func FetchQueryToM3Query( // Optimization for single matcher case. if len(matchers) == 1 { - specialCase := isSpecialCaseMatcher(matchers[0]) + specialCase, err := isSpecialCaseMatcher(matchers[0]) + if err != nil { + return index.Query{}, err + } if specialCase.skip { // NB: only matcher has no effect; this is synonymous to an AllQuery. return index.Query{ @@ -198,7 +202,10 @@ func FetchQueryToM3Query( idxQueries := make([]idx.Query, 0, len(matchers)) for _, matcher := range matchers { - specialCase := isSpecialCaseMatcher(matcher) + specialCase, err := isSpecialCaseMatcher(matcher) + if err != nil { + return index.Query{}, err + } if specialCase.skip { continue } @@ -227,55 +234,81 @@ type specialCase struct { skip bool } -func isSpecialCaseMatcher(matcher models.Matcher) specialCase { +func isSpecialCaseMatcher(matcher models.Matcher) (specialCase, error) { if len(matcher.Value) == 0 { if matcher.Type == models.MatchNotRegexp || matcher.Type == models.MatchNotEqual { query := idx.NewFieldQuery(matcher.Name) - return specialCase{query: query, isSpecial: true} + return specialCase{query: query, isSpecial: true}, nil } if matcher.Type == models.MatchRegexp || matcher.Type == models.MatchEqual { query := idx.NewNegationQuery(idx.NewFieldQuery(matcher.Name)) - return specialCase{query: query, isSpecial: true} + return specialCase{query: query, isSpecial: true}, nil } - return specialCase{} + return specialCase{}, nil } - // NB: no special case for regex / not regex here. + // NB: no special case except for regex / notRegex here. isNegatedRegex := matcher.Type == models.MatchNotRegexp isRegex := matcher.Type == models.MatchRegexp if !isNegatedRegex && !isRegex { - return specialCase{} + return specialCase{}, nil } - if len(matcher.Value) != 2 || matcher.Value[0] != dot { - return specialCase{} - } - - if matcher.Value[1] == star { + if bytes.Equal(matcher.Value, dotStar) { if isNegatedRegex { // NB: This should match no results. query := idx.NewNegationQuery(idx.NewAllQuery()) - return specialCase{query: query, isSpecial: true} + return specialCase{query: query, isSpecial: true}, nil } // NB: this matcher should not affect query results. - return specialCase{skip: true} + return specialCase{skip: true}, nil } - if matcher.Value[1] == plus { + if bytes.Equal(matcher.Value, dotPlus) { query := idx.NewFieldQuery(matcher.Name) if isNegatedRegex { query = idx.NewNegationQuery(query) } - return specialCase{query: query, isSpecial: true} + return specialCase{query: query, isSpecial: true}, nil + } + + matchesEmpty, err := regexp.MatchesEmptyValue(matcher.Value) + if err != nil { + return specialCase{}, regexError(err) } - return specialCase{} + if matchesEmpty { + regexpQuery, err := idx.NewRegexpQuery(matcher.Name, matcher.Value) + if err != nil { + return specialCase{}, err + } + + if isNegatedRegex { + return specialCase{ + query: idx.NewConjunctionQuery( + idx.NewNegationQuery(regexpQuery), + idx.NewFieldQuery(matcher.Name), + ), + isSpecial: true, + }, nil + } + + return specialCase{ + query: idx.NewDisjunctionQuery( + regexpQuery, + idx.NewNegationQuery(idx.NewFieldQuery(matcher.Name)), + ), + isSpecial: true, + }, nil + } + + return specialCase{}, nil } func matcherToQuery(matcher models.Matcher) (idx.Query, error) { @@ -335,3 +368,7 @@ func matcherToQuery(matcher models.Matcher) (idx.Query, error) { return idx.Query{}, fmt.Errorf("unsupported query type: %v", matcher) } } + +func regexError(err error) error { + return xerrors.NewInvalidParamsError(xerrors.Wrap(err, "regex error")) +} diff --git a/src/query/storage/index_test.go b/src/query/storage/index_test.go index 25a4f8745b..3d8931815a 100644 --- a/src/query/storage/index_test.go +++ b/src/query/storage/index_test.go @@ -236,6 +236,72 @@ func TestFetchQueryToM3Query(t *testing.T) { }, }, }, + { + name: "disjunction with empty (no field) match, no parens", + expected: "disjunction(negation(field(env)), regexp(env,one|))", + matchers: models.Matchers{ + { + Type: models.MatchRegexp, + Name: []byte("env"), + Value: []byte("one|"), + }, + }, + }, + { + name: "disjunction with empty (no field) match", + expected: "disjunction(negation(field(env)), regexp(env,(|one|two)))", + matchers: models.Matchers{ + { + Type: models.MatchRegexp, + Name: []byte("env"), + Value: []byte("(|one|two)"), + }, + }, + }, + { + name: "disjunction with non trivial empty (no field) match", + expected: "disjunction(negation(field(env)), regexp(env,\\d*|one))", + matchers: models.Matchers{ + { + Type: models.MatchRegexp, + Name: []byte("env"), + Value: []byte("\\d*|one"), + }, + }, + }, + { + name: "disjunction with both empty (no field) matches", + expected: "disjunction(negation(field(env)), regexp(env,(|)))", + matchers: models.Matchers{ + { + Type: models.MatchRegexp, + Name: []byte("env"), + Value: []byte("(|)"), + }, + }, + }, + { + name: "negated disjunction with empty (no field) match", + expected: "conjunction(field(env),negation(regexp(env,(|one))))", + matchers: models.Matchers{ + { + Type: models.MatchNotRegexp, + Name: []byte("env"), + Value: []byte("(|one)"), + }, + }, + }, + { + name: "negated disjunction with both empty (no field) matches", + expected: "conjunction(field(env),negation(regexp(env,(|))))", + matchers: models.Matchers{ + { + Type: models.MatchNotRegexp, + Name: []byte("env"), + Value: []byte("(|)"), + }, + }, + }, } for _, test := range tests { diff --git a/src/x/regexp/empty_matcher.go b/src/x/regexp/empty_matcher.go new file mode 100644 index 0000000000..7fa0416c9b --- /dev/null +++ b/src/x/regexp/empty_matcher.go @@ -0,0 +1,115 @@ +// Copyright (c) 2023 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +// Package regexp contains regexp processing related utilities. +package regexp + +import ( + "regexp" + "regexp/syntax" + + "github.com/m3db/m3/src/m3ninx/index" +) + +// MatchesEmptyValue returns true if the given regexp would match an empty value. +func MatchesEmptyValue(expr []byte) (bool, error) { + parsed, err := syntax.Parse(string(expr), syntax.Perl) + if err != nil { + return false, err //nolint:propagate_error + } + + switch matchesEmptyValueAnalytically(parsed) { + case yes: + return true, nil + case no: + return false, nil + default: // unknown - only now we resort to compilation and actual attempt to match the regexp + return matchesEmptyValueEmpirically(parsed) + } +} + +func matchesEmptyValueAnalytically(r *syntax.Regexp) threeValuedLogic { + switch r.Op { + case syntax.OpEmptyMatch: + return yes + + case syntax.OpLiteral: + if len(r.Rune) == 0 { + return yes + } + return no + + case syntax.OpCharClass: + return no + + case syntax.OpStar: + return yes + + case syntax.OpCapture, syntax.OpPlus: + return matchesEmptyValueAnalytically(r.Sub[0]) + + case syntax.OpConcat: + var res = yes + for _, s := range r.Sub { + if m := matchesEmptyValueAnalytically(s); m == no { + return no + } else if m == unknown { + res = unknown + } + } + return res + + case syntax.OpAlternate: + var res = no + for _, s := range r.Sub { + if m := matchesEmptyValueAnalytically(s); m == yes { + return yes + } else if m == unknown { + res = unknown + } + } + return res + + default: + // If we even hit this case then we should fall back to + // compiling and running the regexp against an empty string. + return unknown + } +} + +// matchesEmptyValueEmpirically follows the logic of index.CompileRegex(expr). +func matchesEmptyValueEmpirically(r *syntax.Regexp) (bool, error) { + unanchored, err := index.EnsureRegexpUnanchored(r) + if err != nil { + return false, err //nolint:propagate_error + } + + anchored := index.EnsureRegexpAnchored(unanchored) + + return regexp.Match(anchored.String(), nil) +} + +type threeValuedLogic uint8 + +const ( + no threeValuedLogic = iota + yes + unknown +) diff --git a/src/x/regexp/empty_matcher_test.go b/src/x/regexp/empty_matcher_test.go new file mode 100644 index 0000000000..80b4d57c61 --- /dev/null +++ b/src/x/regexp/empty_matcher_test.go @@ -0,0 +1,90 @@ +// Copyright (c) 2023 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package regexp + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestMatchesEmptyValue(t *testing.T) { + tests := []struct { + given string + want bool + }{ + {given: "", want: true}, + {given: "x", want: false}, + {given: ".*", want: true}, + {given: ".+", want: false}, + {given: "x*", want: true}, + {given: "x+", want: false}, + {given: "|", want: true}, + {given: "\\|", want: false}, + {given: "[|]", want: false}, + {given: "(|)", want: true}, + {given: "a|b", want: false}, + {given: "a||b", want: true}, + {given: "a|", want: true}, + {given: "|a", want: true}, + {given: "|a|", want: true}, + {given: "a|b|c", want: false}, + {given: "||a|||b||||c||||", want: true}, + {given: "()", want: true}, + {given: "()*", want: true}, + {given: "()+", want: true}, + {given: "(ab)", want: false}, + {given: "(ab)+", want: false}, + {given: "(a|b)", want: false}, + {given: "(a||b)", want: true}, + {given: "(a|)", want: true}, + {given: "(|a)", want: true}, + {given: "(\\|a)", want: false}, + {given: "([|])", want: false}, + {given: "([|]a)", want: false}, + {given: "([|a])", want: false}, + {given: ".*|a", want: true}, + {given: ".+|a", want: false}, + {given: ".*|.+", want: true}, + {given: ".*|.*", want: true}, + {given: ".+|.+", want: false}, + {given: "a(|)", want: false}, + {given: "a(|)b", want: false}, + {given: "(|)(|)", want: true}, + {given: "(|)a(|)", want: false}, + {given: "(|).*(|)", want: true}, + {given: "(|).+(|)", want: false}, + {given: "\\d*", want: true}, + {given: "\\d+", want: false}, + {given: "(\\d*|a)", want: true}, + {given: "(a|\\d*)", want: true}, + {given: "(|\\d*)", want: true}, + {given: "a(\\d*)", want: false}, + {given: "(\\d*)a", want: false}, + } + for _, tt := range tests { + t.Run(tt.given, func(t *testing.T) { + res, err := MatchesEmptyValue([]byte(tt.given)) + require.NoError(t, err) + require.Equal(t, tt.want, res) + }) + } +}