Consolidates the in-progress #146 work; valid-instances-rejected reaches 4 on the 2006-11-06 XSTS archive (buckets now 1/262/4/147). Regex: match Unicode scalars rather than grapheme clusters; the empty pattern matches only the empty string; pin the category repertoire to Unicode 3.2, the version the corpus is consistent at, so code points added in 3.2 (Tagalog, Cyrillic Komi) are word characters while later additions (the Tamil digit zero) stay unassigned and match \D. New XSDCategory and a 3.2 assignment table derived from DerivedAge, replacing the old Unicode31 files. Content model: assess each child against the particle the content automaton matched, not a by-name lookup (#180). Two same-named particles keep their own fixed values, and an element matching a skip wildcard is no longer validated against its global declaration. Selection is made reproducible when an ambiguous schema slips past the UPA check. XSTS runner: compile and merge every schemaDocument in a schemaTest, not just the first; this also surfaced one previously-masked invalid-instance acceptance (recorded in the baselines). Also folds in attribute-group anyAttribute inheritance, extension wildcard union, namespace-qualified type tables for multi-document xsi:schemaLocation, per-container elementFormDefault, strict and lax attribute wildcard validation, built-in type resolution, and xs:redefine simpleType overrides.
148 lines
6.1 KiB
Swift
148 lines
6.1 KiB
Swift
@testable import PureXML
|
|
import Testing
|
|
|
|
@Suite("Regex")
|
|
struct RegexTests {
|
|
private func matches(_ pattern: String, _ string: String) throws -> Bool {
|
|
try PureXML.Regex.matches(pattern, string)
|
|
}
|
|
|
|
@Test("Literal patterns match the whole string")
|
|
func test_literal() throws {
|
|
#expect(try matches("abc", "abc"))
|
|
#expect(try !matches("abc", "abcd"))
|
|
#expect(try !matches("abc", "ab"))
|
|
}
|
|
|
|
@Test("The dot matches any character except line breaks")
|
|
func test_dot() throws {
|
|
#expect(try matches("a.c", "axc"))
|
|
#expect(try !matches("a.c", "a\nc"))
|
|
}
|
|
|
|
@Test("Quantifiers ? * + bound repetition")
|
|
func test_quantifiers() throws {
|
|
#expect(try matches("ab?c", "ac"))
|
|
#expect(try matches("ab?c", "abc"))
|
|
#expect(try matches("ab*c", "abbbc"))
|
|
#expect(try matches("ab+c", "abc"))
|
|
#expect(try !matches("ab+c", "ac"))
|
|
}
|
|
|
|
@Test("Bounded quantifiers {n}, {n,}, {n,m}")
|
|
func test_boundedQuantifiers() throws {
|
|
#expect(try matches("a{3}", "aaa"))
|
|
#expect(try !matches("a{3}", "aa"))
|
|
#expect(try matches("a{2,}", "aaaa"))
|
|
#expect(try matches("a{2,4}", "aaa"))
|
|
#expect(try !matches("a{2,4}", "aaaaa"))
|
|
}
|
|
|
|
@Test("Alternation and grouping")
|
|
func test_alternationGrouping() throws {
|
|
#expect(try matches("cat|dog", "dog"))
|
|
#expect(try matches("(ab)+", "ababab"))
|
|
#expect(try matches("gr(a|e)y", "grey"))
|
|
#expect(try !matches("gr(a|e)y", "groy"))
|
|
}
|
|
|
|
@Test("Character classes with ranges and negation")
|
|
func test_charClasses() throws {
|
|
#expect(try matches("[a-z]+", "hello"))
|
|
#expect(try !matches("[a-z]+", "Hello"))
|
|
#expect(try matches("[^0-9]+", "abc"))
|
|
#expect(try matches("[abc]{2}", "ca"))
|
|
}
|
|
|
|
@Test("A '-[' opens class subtraction even after a class member")
|
|
func test_classSubtractionAfterMember() throws {
|
|
// `[abcd-[d]]` is {a,b,c,d} minus {d} = {a,b,c}; the `d-[` must read as
|
|
// subtraction, not a `d`..`[` range that traps on reversed bounds (#129,
|
|
// XSTS RegexTest_322).
|
|
#expect(try matches("[abcd-[d]]+", "abcabc"))
|
|
#expect(try !matches("[abcd-[d]]+", "dddaabbccddd"))
|
|
#expect(try matches("[a-z-[aeiou]]+", "bcdfg"))
|
|
#expect(try !matches("[a-z-[aeiou]]+", "bcde"))
|
|
}
|
|
|
|
@Test("A reversed range is rejected, not a trap")
|
|
func test_reversedRangeRejected() {
|
|
#expect(throws: PureXML.Regex.RegexError.self) { _ = try PureXML.Regex.Pattern("[z-a]") }
|
|
}
|
|
|
|
@Test("Class escapes \\d \\w \\s and their negations")
|
|
func test_classEscapes() throws {
|
|
#expect(try matches("\\d{4}", "2026"))
|
|
#expect(try !matches("\\d{4}", "20x6"))
|
|
#expect(try matches("\\w+", "abc1"))
|
|
// XSD \w is the complement of \p{P}\p{Z}\p{C}: symbols and marks are word
|
|
// characters, while the connector `_` (Pc) and whitespace are not.
|
|
#expect(try matches("\\w", "+"))
|
|
#expect(try !matches("\\w", "_"))
|
|
#expect(try !matches("\\w", " "))
|
|
#expect(try matches("a\\sb", "a b"))
|
|
#expect(try matches("\\D+", "abc"))
|
|
// XSTS reT51: Tamil digit U+0BE6 was unassigned in Unicode 3.1, so it matches \D.
|
|
#expect(try matches("\\D", "\u{0BE6}"))
|
|
// XSTS reZ003v: ceiling brackets were Sm in Unicode 3.1, so they match `\w`.
|
|
#expect(try matches("\\w", "\u{2308}"))
|
|
#expect(try matches("[\\w]", "\u{2308}"))
|
|
#expect(try matches("[\\w]", "\u{2309}"))
|
|
// XSTS reZ004v: decimal digits from other scripts match `\d` via Nd.
|
|
#expect(try matches("\\d", "\u{0661}"))
|
|
}
|
|
|
|
@Test("Escaped metacharacters are literal")
|
|
func test_escapes() throws {
|
|
#expect(try matches("a\\.b", "a.b"))
|
|
#expect(try !matches("a\\.b", "axb"))
|
|
#expect(try matches("\\(\\)", "()"))
|
|
}
|
|
|
|
@Test("A realistic ISBN-like pattern")
|
|
func test_realistic() throws {
|
|
let pattern = "\\d{3}-\\d{1,5}-\\d{1,7}-\\d{1,6}-[0-9X]"
|
|
#expect(try matches(pattern, "978-0-13-468599-1"))
|
|
#expect(try !matches(pattern, "abc-0-13-468599-1"))
|
|
}
|
|
|
|
@Test("Malformed patterns are rejected")
|
|
func test_errors() {
|
|
#expect(throws: PureXML.Regex.RegexError.self) { _ = try PureXML.Regex.Pattern("a(b") }
|
|
#expect(throws: PureXML.Regex.RegexError.self) { _ = try PureXML.Regex.Pattern("*ab") }
|
|
}
|
|
|
|
@Test("The empty pattern is valid and matches only the empty string")
|
|
func test_emptyPattern() throws {
|
|
// XSD grammar (Datatypes Appendix F): branch ::= piece*, so a branch
|
|
// may have zero pieces. The empty regex is valid and matches "".
|
|
#expect(try matches("", ""))
|
|
#expect(try !matches("", "a"))
|
|
}
|
|
|
|
@Test("Pathological quantifiers compile bounded instead of exhausting memory")
|
|
func test_quantifierExplosionBounded() throws {
|
|
// `a{1000000000}` would unroll a billion automaton states without a
|
|
// ceiling; the nested form multiplies. Both must build and answer
|
|
// without the OOM that killed the XSTS msMeta Regex set (#129).
|
|
_ = try matches("a{1000000000}", String(repeating: "a", count: 64))
|
|
_ = try matches("(a{100000}){100000}", "aaaa")
|
|
// Ordinary bounded quantifiers keep their exact semantics.
|
|
#expect(try matches("a{2,4}", "aaa"))
|
|
#expect(try !matches("a{2,4}", "a"))
|
|
#expect(try !matches("a{2,4}", "aaaaa"))
|
|
}
|
|
|
|
@Test("Unicode block escapes match within their block and reject outside it")
|
|
func test_unicodeBlocks() throws {
|
|
// \u{0531} Armenian, \u{0B85} Tamil, \u{1100} Hangul Jamo.
|
|
#expect(try matches(#"\p{IsArmenian}+"#, "\u{0531}\u{0561}"))
|
|
#expect(try !matches(#"\p{IsArmenian}+"#, "abc"))
|
|
#expect(try matches(#"\p{IsTamil}+"#, "\u{0B85}"))
|
|
#expect(try matches(#"\p{IsHangulJamo}+"#, "\u{1100}"))
|
|
#expect(try !matches(#"\p{IsTamil}+"#, "\u{0531}")) // Armenian is not Tamil
|
|
// An unknown block name is still a compile error.
|
|
#expect(throws: PureXML.Regex.RegexError.self) { _ = try PureXML.Regex.Pattern(#"\p{IsNotARealBlock}"#) }
|
|
}
|
|
}
|