From 72d6b002882a99f7c12d47a5f4a7533b302e51c0 Mon Sep 17 00:00:00 2001 From: "fox.cpp" Date: Tue, 30 Jan 2024 02:59:14 +0300 Subject: [PATCH] Implement relational extension --- README.md | 7 +- interp/dovecot_testsuite.go | 2 + interp/load.go | 1 + interp/load_dovecot.go | 20 +++++- interp/load_generic.go | 2 +- interp/load_test.go | 8 ++- interp/matchertest.go | 69 +++++++++++++++++- interp/relational.go | 94 +++++++++++++++++++++++++ interp/test.go | 50 ++++++++++--- interp/test_string.go | 135 +++++++++++++++++++++++++----------- interp/variables.go | 13 ++++ lexer/lex.go | 2 + lexer/token.go | 4 ++ tests/compile_test.go | 124 +++++++++++++++++++++++++++++++-- tests/relational_test.go | 37 ++++++++++ tests/run.go | 34 +++++++++ tests/variables_test.go | 2 - 17 files changed, 540 insertions(+), 64 deletions(-) create mode 100644 interp/relational.go create mode 100644 tests/relational_test.go diff --git a/README.md b/README.md index d14df0d..a4ee738 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ implementation in Go. - encoded-character ([RFC 5228]) - imap4flags ([RFC 5232]) - variables ([RFC 5229]) +- relational ([RFC 5231]) ## Example @@ -18,8 +19,12 @@ See ./cmd/sieve-run. ## Known issues -- `:matches` `*` is greedy (RFC 5229 requires non-greedy matching). +- Some invalid scripts are accepted as valid (see tests/compile_test.go) +- Comments in addresses are not ignored when testing equality, etc. +- Source routes in addresses are not ignored when testing equality, etc. [RFC 5228]: https://datatracker.ietf.org/doc/html/rfc5228 [RFC 5229]: https://datatracker.ietf.org/doc/html/rfc5229 [RFC 5232]: https://datatracker.ietf.org/doc/html/rfc5232 +[RFC 5231]: https://datatracker.ietf.org/doc/html/rfc5231 + diff --git a/interp/dovecot_testsuite.go b/interp/dovecot_testsuite.go index 7f06cbf..b40f80c 100644 --- a/interp/dovecot_testsuite.go +++ b/interp/dovecot_testsuite.go @@ -150,6 +150,7 @@ func (t TestDovecotCompile) Check(_ context.Context, d *RuntimeData) (bool, erro } toks, err := lexer.Lex(bytes.NewReader(svScript), &lexer.Options{ + Filename: t.ScriptPath, MaxTokens: 5000, }) if err != nil { @@ -197,6 +198,7 @@ func (t TestDovecotRun) Check(ctx context.Context, d *RuntimeData) (bool, error) } type TestDovecotTestError struct { + matcherTest } func (t TestDovecotTestError) Check(_ context.Context, _ *RuntimeData) (bool, error) { diff --git a/interp/load.go b/interp/load.go index bcf7fe2..7f32a45 100644 --- a/interp/load.go +++ b/interp/load.go @@ -20,6 +20,7 @@ var supportedRequires = map[string]struct{}{ "imap4flags": {}, "variables": {}, + "relational": {}, } var ( diff --git a/interp/load_dovecot.go b/interp/load_dovecot.go index 8670c0a..d9f40f0 100644 --- a/interp/load_dovecot.go +++ b/interp/load_dovecot.go @@ -158,7 +158,23 @@ func loadDovecotRun(s *Script, test parser.Test) (Test, error) { } func loadDovecotError(s *Script, test parser.Test) (Test, error) { - loaded := TestDovecotTestError{} - err := LoadSpec(s, &Spec{}, test.Position, test.Args, test.Tests, nil) + loaded := TestDovecotTestError{matcherTest: newMatcherTest()} + err := LoadSpec(s, loaded.addSpecTags(&Spec{ + Tags: map[string]SpecTag{ + "index": { + NeedsValue: true, + MinStrCount: 1, + MaxStrCount: 1, + NoVariables: true, + MatchNum: func(val int) {}, + }, + }, + Pos: []SpecPosArg{ + { + MatchStr: func(val []string) {}, + MinStrCount: 1, + }, + }, + }), test.Position, test.Args, test.Tests, nil) return loaded, err } diff --git a/interp/load_generic.go b/interp/load_generic.go index 37fee1f..d08ce07 100644 --- a/interp/load_generic.go +++ b/interp/load_generic.go @@ -182,7 +182,7 @@ func LoadSpec(s *Script, spec *Spec, position lexer.Position, args []parser.Arg, } tag, ok := spec.Tags[strings.ToLower(a.Value)] if !ok { - return lexer.ErrorAt(a, "LoadSpec: unknown tagged argument: %v", a) + return lexer.ErrorAt(a, "LoadSpec: unknown tagged argument: %v", a.Value) } if tag.NeedsValue { lastTag = &tag diff --git a/interp/load_test.go b/interp/load_test.go index 3c8570b..2f1690c 100644 --- a/interp/load_test.go +++ b/interp/load_test.go @@ -63,11 +63,13 @@ if envelope :is "from" "test@example.org" { `, []Cmd{ CmdIf{ Test: EnvelopeTest{ - Comparator: ComparatorASCIICaseMap, - Match: MatchIs, + matcherTest: matcherTest{ + comparator: ComparatorASCIICaseMap, + match: MatchIs, + key: []string{"test@example.org"}, + }, AddressPart: All, Field: []string{"from"}, - Key: []string{"test@example.org"}, }, Block: []Cmd{ CmdFileInto{Mailbox: "hell"}, diff --git a/interp/matchertest.go b/interp/matchertest.go index 08e69ea..a43fcde 100644 --- a/interp/matchertest.go +++ b/interp/matchertest.go @@ -2,6 +2,7 @@ package interp import ( "fmt" + "strconv" ) // matcherTest contains code shared between tests @@ -11,9 +12,10 @@ import ( type matcherTest struct { comparator Comparator match Match + relational Relational key []string - // Used for keys without + // Used for keys without variables. keyCompiled []CompiledMatcher matchCnt int @@ -37,6 +39,7 @@ func (t *matcherTest) addSpecTags(s *Spec) *Spec { MatchStr: func(val []string) { t.comparator = Comparator(val[0]) }, + NoVariables: true, } s.Tags["is"] = SpecTag{ MatchBool: func() { @@ -56,6 +59,28 @@ func (t *matcherTest) addSpecTags(s *Spec) *Spec { t.matchCnt++ }, } + s.Tags["value"] = SpecTag{ + NeedsValue: true, + MinStrCount: 1, + MaxStrCount: 1, + NoVariables: true, + MatchStr: func(val []string) { + t.match = MatchValue + t.matchCnt++ + t.relational = Relational(val[0]) + }, + } + s.Tags["count"] = SpecTag{ + NeedsValue: true, + MinStrCount: 1, + MaxStrCount: 1, + NoVariables: true, + MatchStr: func(val []string) { + t.match = MatchCount + t.matchCnt++ + t.relational = Relational(val[0]) + }, + } return s } @@ -66,6 +91,19 @@ func (t *matcherTest) setKey(s *Script, k []string) error { return fmt.Errorf("multiple match-types are not allowed") } + if t.match == MatchCount || t.match == MatchValue { + if !s.RequiresExtension("relational") { + return fmt.Errorf("missing require 'relational'") + } + switch t.relational { + case RelGreaterThan, RelGreaterOrEqual, + RelLessThan, RelLessOrEqual, RelEqual, + RelNotEqual: + default: + return fmt.Errorf("unknown relational operator: %v", t.relational) + } + } + caseFold := false octet := false switch t.comparator { @@ -96,9 +134,36 @@ func (t *matcherTest) setKey(s *Script, k []string) error { } } + if t.match == MatchCount && t.comparator != ComparatorASCIINumeric { + return fmt.Errorf("non-numeric comparators cannot be used with :count") + } + return nil } +func (t *matcherTest) isCount() bool { + return t.match == MatchCount +} + +func (t *matcherTest) countMatches(d *RuntimeData, value uint64) bool { + if !t.isCount() { + panic("countMatches can be called only with MatchCount matcher") + } + + for _, k := range t.key { + kNum, err := strconv.ParseUint(expandVars(d, k), 10, 64) + if err != nil { + continue + } + + if t.relational.CompareUint64(value, kNum) { + return true + } + } + + return false +} + func (t *matcherTest) tryMatch(d *RuntimeData, source string) (bool, error) { for i, key := range t.key { var ( @@ -110,7 +175,7 @@ func (t *matcherTest) tryMatch(d *RuntimeData, source string) (bool, error) { ok, matches, err = t.keyCompiled[i](source) } else { key = expandVars(d, key) - ok, matches, err = testString(t.comparator, t.match, source, expandVars(d, key)) + ok, matches, err = testString(t.comparator, t.match, t.relational, source, expandVars(d, key)) } if err != nil { return false, err diff --git a/interp/relational.go b/interp/relational.go new file mode 100644 index 0000000..4e167c7 --- /dev/null +++ b/interp/relational.go @@ -0,0 +1,94 @@ +package interp + +type Relational string + +const ( + RelGreaterThan Relational = "gt" + RelGreaterOrEqual Relational = "ge" + RelLessThan Relational = "lt" + RelLessOrEqual Relational = "le" + RelEqual Relational = "eq" + RelNotEqual Relational = "ne" +) + +func (r Relational) CompareString(lhs, rhs string) bool { + switch r { + case RelGreaterThan: + return lhs > rhs + case RelGreaterOrEqual: + return lhs >= rhs + case RelLessThan: + return lhs < rhs + case RelLessOrEqual: + return lhs <= rhs + case RelEqual: + return lhs == rhs + case RelNotEqual: + return lhs != rhs + } + return false +} + +func (r Relational) CompareUint64(lhs, rhs uint64) bool { + switch r { + case RelGreaterThan: + return lhs > rhs + case RelGreaterOrEqual: + return lhs >= rhs + case RelLessThan: + return lhs < rhs + case RelLessOrEqual: + return lhs <= rhs + case RelEqual: + return lhs == rhs + case RelNotEqual: + return lhs != rhs + } + return false +} + +func (r Relational) CompareNumericValue(lhs, rhs *uint64) bool { + // https://www.rfc-editor.org/rfc/rfc4790.html#section-9.1 + // nil (string not starting with a digit) + // represents positive infinity. inf == inf. inf > any integer. + + switch r { + case RelGreaterThan: + if lhs == nil { + if rhs == nil { + return false + } + return true + } + if rhs == nil { + return false + } + return *lhs > *rhs + case RelGreaterOrEqual: + return !RelLessThan.CompareNumericValue(lhs, rhs) + case RelLessThan: + if rhs == nil { + if lhs == nil { + return false + } + return true + } + if lhs == nil { + return false + } + return *lhs < *rhs + case RelLessOrEqual: + return !RelGreaterThan.CompareNumericValue(lhs, rhs) + case RelEqual: + if lhs == nil && rhs == nil { + return true + } + if lhs != nil && rhs != nil { + return *lhs == *rhs + } + return false + case RelNotEqual: + return !RelEqual.CompareNumericValue(lhs, rhs) + } + return false +} diff --git a/interp/test.go b/interp/test.go index 0efc5d7..c181039 100644 --- a/interp/test.go +++ b/interp/test.go @@ -58,6 +58,7 @@ var allowedAddrHeaders = map[string]struct{}{ } func (a AddressTest) Check(_ context.Context, d *RuntimeData) (bool, error) { + entryCount := uint64(0) for _, hdr := range a.Header { hdr = strings.ToLower(hdr) hdr = expandVars(d, hdr) @@ -77,15 +78,27 @@ func (a AddressTest) Check(_ context.Context, d *RuntimeData) (bool, error) { return false, nil } - ok, err := testAddress(d, a.matcherTest, a.AddressPart, addrList) - if err != nil { - return false, err - } - if ok { - return true, nil + for _, addr := range addrList { + if a.isCount() { + entryCount++ + continue + } + + ok, err := testAddress(d, a.matcherTest, a.AddressPart, addr.Address) + if err != nil { + return false, err + } + if ok { + return true, nil + } } } } + + if a.isCount() { + return a.countMatches(d, entryCount), nil + } + return false, nil } @@ -131,6 +144,7 @@ type EnvelopeTest struct { } func (e EnvelopeTest) Check(_ context.Context, d *RuntimeData) (bool, error) { + entryCount := uint64(0) for _, field := range e.Field { var value string switch strings.ToLower(expandVars(d, field)) { @@ -143,10 +157,14 @@ func (e EnvelopeTest) Check(_ context.Context, d *RuntimeData) (bool, error) { default: return false, fmt.Errorf("envelope: unsupported envelope-part: %v", field) } + if e.isCount() { + if value != "" { + entryCount++ + } + continue + } - ok, err := testAddress(d, e.matcherTest, e.AddressPart, []*mail.Address{ - {Address: value}, - }) + ok, err := testAddress(d, e.matcherTest, e.AddressPart, value) if err != nil { return false, err } @@ -154,6 +172,9 @@ func (e EnvelopeTest) Check(_ context.Context, d *RuntimeData) (bool, error) { return true, nil } } + if e.isCount() { + return e.countMatches(d, entryCount), nil + } return false, nil } @@ -193,6 +214,7 @@ type HeaderTest struct { } func (h HeaderTest) Check(_ context.Context, d *RuntimeData) (bool, error) { + entryCount := uint64(0) for _, hdr := range h.Header { values, err := d.Msg.HeaderGet(expandVars(d, hdr)) if err != nil { @@ -200,6 +222,11 @@ func (h HeaderTest) Check(_ context.Context, d *RuntimeData) (bool, error) { } for _, value := range values { + if h.isCount() { + entryCount++ + continue + } + ok, err := h.matcherTest.tryMatch(d, value) if err != nil { return false, err @@ -209,6 +236,11 @@ func (h HeaderTest) Check(_ context.Context, d *RuntimeData) (bool, error) { } } } + + if h.isCount() { + return h.countMatches(d, entryCount), nil + } + return false, nil } diff --git a/interp/test_string.go b/interp/test_string.go index 481c3c1..96851c6 100644 --- a/interp/test_string.go +++ b/interp/test_string.go @@ -3,7 +3,6 @@ package interp import ( "errors" "fmt" - "net/mail" "strconv" "strings" "unicode" @@ -15,6 +14,8 @@ const ( MatchContains Match = "contains" MatchIs Match = "is" MatchMatches Match = "matches" + MatchValue Match = "value" + MatchCount Match = "count" ) type Comparator string @@ -59,6 +60,8 @@ func split(addr string) (mailbox, domain string, err error) { var ErrComparatorMatchUnsupported = fmt.Errorf("match-comparator combination not supported") func numericValue(s string) *uint64 { + // https://www.rfc-editor.org/rfc/rfc4790.html#section-9.1 + if len(s) == 0 { return nil } @@ -70,13 +73,20 @@ func numericValue(s string) *uint64 { for i, r := range runes { if !unicode.IsDigit(r) { sl = string(runes[:i]) + break } } - digit, _ := strconv.ParseUint(sl, 10, 64) + if sl == "" { + sl = s + } + digit, err := strconv.ParseUint(sl, 10, 64) + if err != nil { + return nil + } return &digit } -func testString(comparator Comparator, match Match, value, key string) (bool, []string, error) { +func testString(comparator Comparator, match Match, rel Relational, value, key string) (bool, []string, error) { switch comparator { case ComparatorOctet: switch match { @@ -86,6 +96,10 @@ func testString(comparator Comparator, match Match, value, key string) (bool, [] return value == key, nil, nil case MatchMatches: return matchOctet(key, value, false) + case MatchValue: + return rel.CompareString(value, key), nil, nil + case MatchCount: + panic("testString should not be used with MatchCount") } case ComparatorASCIINumeric: switch match { @@ -94,25 +108,34 @@ func testString(comparator Comparator, match Match, value, key string) (bool, [] case MatchIs: lhsNum := numericValue(value) rhsNum := numericValue(key) - if lhsNum == nil || rhsNum == nil { - return false, nil, nil - } - return *lhsNum == *rhsNum, nil, nil + return RelEqual.CompareNumericValue(lhsNum, rhsNum), nil, nil case MatchMatches: return false, nil, ErrComparatorMatchUnsupported + case MatchValue: + lhsNum := numericValue(value) + rhsNum := numericValue(key) + return rel.CompareNumericValue(lhsNum, rhsNum), nil, nil + case MatchCount: + panic("testString should not be used with MatchCount") } case ComparatorASCIICaseMap: switch match { case MatchContains: - value = strings.ToLower(value) - key = strings.ToLower(key) + value = toLowerASCII(value) + key = toLowerASCII(key) return strings.Contains(value, key), nil, nil case MatchIs: - value = strings.ToLower(value) - key = strings.ToLower(key) + value = toLowerASCII(value) + key = toLowerASCII(key) return value == key, nil, nil case MatchMatches: return matchOctet(key, value, true) + case MatchValue: + value = toLowerASCII(value) + key = toLowerASCII(key) + return rel.CompareString(value, key), nil, nil + case MatchCount: + panic("testString should not be used with MatchCount") } case ComparatorUnicodeCaseMap: switch match { @@ -124,44 +147,76 @@ func testString(comparator Comparator, match Match, value, key string) (bool, [] return strings.EqualFold(value, key), nil, nil case MatchMatches: return matchUnicode(key, value, true) + case MatchValue: + value = toLowerASCII(value) + key = toLowerASCII(key) + return rel.CompareString(value, key), nil, nil + case MatchCount: + panic("testString should not be used with MatchCount") } } return false, nil, nil } -func testAddress(d *RuntimeData, matcher matcherTest, part AddressPart, headerVal []*mail.Address) (bool, error) { - for _, addr := range headerVal { - if addr.Address == "<>" { - addr.Address = "" - } +func testAddress(d *RuntimeData, matcher matcherTest, part AddressPart, address string) (bool, error) { + if address == "<>" { + address = "" + } - var valueToCompare string - if addr.Address != "" { - switch part { - case LocalPart: - localPart, _, err := split(addr.Address) - if err != nil { - continue - } - valueToCompare = localPart - case Domain: - _, domain, err := split(addr.Address) - if err != nil { - continue - } - valueToCompare = domain - case All: - valueToCompare = addr.Address + var valueToCompare string + if address != "" { + switch part { + case LocalPart: + localPart, _, err := split(address) + if err != nil { + return false, nil + } + valueToCompare = localPart + case Domain: + _, domain, err := split(address) + if err != nil { + return false, nil } + valueToCompare = domain + case All: + valueToCompare = address } + } - ok, err := matcher.tryMatch(d, valueToCompare) - if err != nil { - return false, err - } - if ok { - return true, nil + ok, err := matcher.tryMatch(d, valueToCompare) + if err != nil { + return false, err + } + return ok, nil +} + +func toLowerASCII(s string) string { + hasUpper := false + for i := 0; i < len(s); i++ { + c := s[i] + hasUpper = hasUpper || ('A' <= c && c <= 'Z') + } + if !hasUpper { + return s + } + var ( + b strings.Builder + pos int + ) + b.Grow(len(s)) + for i := 0; i < len(s); i++ { + c := s[i] + if 'A' <= c && c <= 'Z' { + c += 'a' - 'A' + if pos < i { + b.WriteString(s[pos:i]) + } + b.WriteByte(c) + pos = i + 1 } } - return false, nil + if pos < len(s) { + b.WriteString(s[pos:]) + } + return b.String() } diff --git a/interp/variables.go b/interp/variables.go index ec3cb17..71f127a 100644 --- a/interp/variables.go +++ b/interp/variables.go @@ -96,9 +96,17 @@ type TestString struct { } func (t TestString) Check(_ context.Context, d *RuntimeData) (bool, error) { + entryCount := uint64(0) for _, source := range t.Source { source = expandVars(d, source) + if t.isCount() { + if source != "" { + entryCount++ + } + continue + } + ok, err := t.matcherTest.tryMatch(d, source) if err != nil { return false, err @@ -107,5 +115,10 @@ func (t TestString) Check(_ context.Context, d *RuntimeData) (bool, error) { return true, nil } } + + if t.isCount() { + return t.countMatches(d, entryCount), nil + } + return false, nil } diff --git a/lexer/lex.go b/lexer/lex.go index e8a69e4..0a147ee 100644 --- a/lexer/lex.go +++ b/lexer/lex.go @@ -10,6 +10,7 @@ import ( ) type Options struct { + Filename string NoPosition bool MaxTokens int } @@ -59,6 +60,7 @@ type lexerState struct { func tokenStream(r *bufio.Reader, opts *Options) ([]Token, error) { res := []Token{} state := &lexerState{} + state.File = opts.Filename state.Line = 1 for { b, err := r.ReadByte() diff --git a/lexer/token.go b/lexer/token.go index eb28d80..4c8149c 100644 --- a/lexer/token.go +++ b/lexer/token.go @@ -6,11 +6,15 @@ import ( ) type Position struct { + File string Line int Col int } func (l Position) String() string { + if l.File != "" { + return l.File + ":" + strconv.Itoa(l.Line) + ":" + strconv.Itoa(l.Col) + } return strconv.Itoa(l.Line) + ":" + strconv.Itoa(l.Col) } diff --git a/tests/compile_test.go b/tests/compile_test.go index 676ebd2..3a96d4f 100644 --- a/tests/compile_test.go +++ b/tests/compile_test.go @@ -13,13 +13,129 @@ func TestCompile(t *testing.T) { // tests to check whether any invalid scripts are not loaded as valid. func TestCompileErrors(t *testing.T) { - t.Skip("requires relational extension") - RunDovecotTest(t, filepath.Join("pigeonhole", "tests", "compile", "errors.svtest")) + t.Skip("FIXME: Non-conforming compilation") + // Stripped test_error calls from errors.svtest. + RunDovecotTestInline(t, filepath.Join("pigeonhole", "tests", "compile"), ` +require "vnd.dovecot.testsuite"; +test "Lexer errors (FIXME: count only)" { + if test_script_compile "errors/lexer.sieve" { + test_fail "compile should have failed."; + } +} +test "Parser errors (FIXME: count only)" { + if test_script_compile "errors/parser.sieve" { + test_fail "compile should have failed."; + } +} +test "Header errors" { + if test_script_compile "errors/header.sieve" { + test_fail "compile should have failed."; + } +} +test "Address errors" { + if test_script_compile "errors/address.sieve" { + test_fail "compile should have failed."; + } +} +test "If errors (FIXME: count only)" { + if test_script_compile "errors/if.sieve" { + test_fail "compile should have failed."; + } +} +test "Require errors (FIXME: count only)" { + if test_script_compile "errors/require.sieve" { + test_fail "compile should have failed."; + } +} +test "Size errors (FIXME: count only)" { + if test_script_compile "errors/size.sieve" { + test_fail "compile should have failed."; + } +} +test "Envelope errors (FIXME: count only)" { + if test_script_compile "errors/envelope.sieve" { + test_fail "compile should have failed."; + } +} +test "Stop errors (FIXME: count only)" { + if test_script_compile "errors/stop.sieve" { + test_fail "compile should have failed."; + } +} +test "Keep errors (FIXME: count only)" { + if test_script_compile "errors/keep.sieve" { + test_fail "compile should have failed."; + } +} +test "Fileinto errors (FIXME: count only)" { + if test_script_compile "errors/fileinto.sieve" { + test_fail "compile should have failed."; + } +} +test "COMPARATOR errors (FIXME: count only)" { + if test_script_compile "errors/comparator.sieve" { + test_fail "compile should have failed."; + } +} +test "ADDRESS-PART errors (FIXME: count only)" { + if test_script_compile "errors/address-part.sieve" { + test_fail "compile should have failed."; + } +} +test "MATCH-TYPE errors (FIXME: count only)" { + if test_script_compile "errors/match-type.sieve" { + test_fail "compile should have failed."; + } +} +test "Encoded-character errors (FIXME: count only)" { + if test_script_compile "errors/encoded-character.sieve" { + test_fail "compile should have failed."; + } +} +test "Outgoing address errors (FIXME: count only)" { + if test_script_compile "errors/out-address.sieve" { + test_fail "compile should have failed."; + } +} +test "Tagged argument errors (FIXME: count only)" { + if test_script_compile "errors/tag.sieve" { + test_fail "compile should have failed."; + } +} +test "Typos" { + if test_script_compile "errors/typos.sieve" { + test_fail "compile should have failed."; + } +} +test "Unsupported language features (FIXME: count only)" { + if test_script_compile "errors/unsupported.sieve" { + test_fail "compile should have failed."; + } +} +`) + //RunDovecotTest(t, filepath.Join("pigeonhole", "tests", "compile", "errors.svtest")) } func TestCompileRecover(t *testing.T) { - t.Skip("requires relational extension") - RunDovecotTest(t, filepath.Join("pigeonhole", "tests", "compile", "recover.svtest")) + t.Skip("FIXME: Non-conforming compilation") + RunDovecotTestInline(t, filepath.Join("pigeonhole", "tests", "compile"), ` +require "vnd.dovecot.testsuite"; +test "Missing semicolons" { + if test_script_compile "recover/commands-semicolon.sieve" { + test_fail "compile should have failed."; + } +} +test "Missing semicolon at end of block" { + if test_script_compile "recover/commands-endblock.sieve" { + test_fail "compile should have failed."; + } +} +test "Spurious comma at end of test list" { + if test_script_compile "recover/tests-endcomma.sieve" { + test_fail "compile should have failed."; + } +} +`) } func TestCompileWarnings(t *testing.T) { diff --git a/tests/relational_test.go b/tests/relational_test.go new file mode 100644 index 0000000..ca9cad4 --- /dev/null +++ b/tests/relational_test.go @@ -0,0 +1,37 @@ +package tests + +import ( + "path/filepath" + "testing" +) + +func TestRelationalBasic(t *testing.T) { + RunDovecotTest(t, filepath.Join("pigeonhole", "tests", "extensions", "relational", "basic.svtest")) +} + +func TestRelationalComparators(t *testing.T) { + RunDovecotTest(t, filepath.Join("pigeonhole", "tests", "extensions", "relational", "comparators.svtest")) +} + +func TestRelationalErrors(t *testing.T) { + // Stripped test_error calls. + RunDovecotTestInline(t, filepath.Join("pigeonhole", "tests", "extensions", "relational"), ` +require "vnd.dovecot.testsuite"; +test "Syntax errors" { + if test_script_compile "errors/syntax.sieve" { + test_fail "compile should have failed"; + } +} +test "Validation errors" { + if test_script_compile "errors/validation.sieve" { + test_fail "compile should have failed"; + } +} +`) + + //RunDovecotTest(t, filepath.Join("pigeonhole", "tests", "extensions", "relational", "errors.svtest")) +} + +func TestRelationalRFC(t *testing.T) { + RunDovecotTest(t, filepath.Join("pigeonhole", "tests", "extensions", "relational", "rfc.svtest")) +} diff --git a/tests/run.go b/tests/run.go index 5dfde5e..b026fa1 100644 --- a/tests/run.go +++ b/tests/run.go @@ -5,12 +5,45 @@ import ( "context" "os" "path/filepath" + "strings" "testing" "github.com/foxcpp/go-sieve" "github.com/foxcpp/go-sieve/interp" ) +func RunDovecotTestInline(t *testing.T, baseDir string, scriptText string) { + opts := sieve.DefaultOptions() + opts.Lexer.Filename = "inline" + opts.Interp.T = t + + script, err := sieve.Load(strings.NewReader(scriptText), opts) + if err != nil { + t.Fatal(err) + } + + ctx := context.Background() + + // Empty data. + data := sieve.NewRuntimeData(script, interp.DummyPolicy{}, + interp.EnvelopeStatic{}, interp.MessageStatic{}) + + if baseDir == "" { + wd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + data.Namespace = os.DirFS(wd) + } else { + data.Namespace = os.DirFS(baseDir) + } + + err = script.Execute(ctx, data) + if err != nil { + t.Fatal(err) + } +} + func RunDovecotTestWithout(t *testing.T, path string, disabledTests []string) { svScript, err := os.ReadFile(path) if err != nil { @@ -18,6 +51,7 @@ func RunDovecotTestWithout(t *testing.T, path string, disabledTests []string) { } opts := sieve.DefaultOptions() + opts.Lexer.Filename = filepath.Base(path) opts.Interp.T = t opts.Interp.DisabledTests = disabledTests diff --git a/tests/variables_test.go b/tests/variables_test.go index 9d5185c..5c44b3b 100644 --- a/tests/variables_test.go +++ b/tests/variables_test.go @@ -10,7 +10,6 @@ func TestExtensionsVariablesBasic(t *testing.T) { } func TestExtensionsVariablesErrors(t *testing.T) { - t.Skip("requires relational extension") RunDovecotTest(t, filepath.Join("pigeonhole", "tests", "extensions", "variables", "errors.svtest")) } @@ -36,6 +35,5 @@ func TestExtensionsVariablesRegex(t *testing.T) { } func TestExtensionsVariablesString(t *testing.T) { - t.Skip("requires relational extension") RunDovecotTest(t, filepath.Join("pigeonhole", "tests", "extensions", "variables", "string.svtest")) }