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.
129 lines
5.6 KiB
Swift
129 lines
5.6 KiB
Swift
@testable import PureXML
|
|
import Testing
|
|
|
|
@Suite("XSD datatypes")
|
|
struct XSDDatatypeTests {
|
|
private typealias Schema = PureXML.Schema
|
|
|
|
private func valid(_ value: String, _ type: Schema.BuiltinType) -> Bool {
|
|
Schema.isValid(value, type: type)
|
|
}
|
|
|
|
// MARK: Primitives
|
|
|
|
@Test("boolean accepts the four lexical forms")
|
|
func test_boolean() {
|
|
#expect(valid("true", .boolean) && valid("false", .boolean))
|
|
#expect(valid("0", .boolean) && valid("1", .boolean))
|
|
#expect(!valid("True", .boolean) && !valid("yes", .boolean))
|
|
}
|
|
|
|
@Test("decimal and integer lexical spaces")
|
|
func test_decimalInteger() {
|
|
#expect(valid("-12.345", .decimal) && valid("+.5", .decimal) && valid("100", .decimal))
|
|
#expect(!valid("1.2.3", .decimal) && !valid("1e3", .decimal))
|
|
#expect(valid("-42", .integer) && !valid("4.0", .integer))
|
|
}
|
|
|
|
@Test("double accepts exponents and the special values")
|
|
func test_double() {
|
|
#expect(valid("1.5e-9", .double) && valid("INF", .double) && valid("-INF", .double) && valid("NaN", .double))
|
|
#expect(!valid("1.5e", .double) && !valid("inf", .double))
|
|
}
|
|
|
|
@Test("bounded integer types enforce their ranges")
|
|
func test_integerBounds() {
|
|
#expect(valid("127", .byte) && !valid("128", .byte))
|
|
#expect(valid("-128", .byte) && !valid("-129", .byte))
|
|
#expect(valid("255", .unsignedByte) && !valid("-1", .unsignedByte))
|
|
#expect(valid("1", .positiveInteger) && !valid("0", .positiveInteger))
|
|
#expect(valid("0", .nonNegativeInteger) && !valid("-1", .nonNegativeInteger))
|
|
}
|
|
|
|
// MARK: Dates
|
|
|
|
@Test("date validates field ranges and leap years")
|
|
func test_date() {
|
|
#expect(valid("2024-02-29", .date) && !valid("2023-02-29", .date))
|
|
#expect(!valid("2024-02-30", .date) && !valid("2024-13-01", .date))
|
|
#expect(valid("-0044-03-15", .date))
|
|
}
|
|
|
|
@Test("dateTime accepts 24:00:00 but not 24:00:01, and timezones")
|
|
func test_dateTime() {
|
|
#expect(valid("2026-06-08T24:00:00", .dateTime))
|
|
#expect(!valid("2026-06-08T24:00:01", .dateTime))
|
|
#expect(valid("2026-06-08T12:30:00Z", .dateTime) && valid("2026-06-08T12:30:00+05:30", .dateTime))
|
|
#expect(!valid("2026-06-08T12:30:00+15:00", .dateTime))
|
|
}
|
|
|
|
@Test("the g* date types parse their reduced forms")
|
|
func test_gTypes() {
|
|
#expect(valid("--02-29", .gMonthDay) && !valid("--02-30", .gMonthDay))
|
|
#expect(valid("---31", .gDay) && valid("--12", .gMonth) && valid("2026", .gYear))
|
|
// gMonth accepts both the XSD 1.0 `--MM--` form and the `--MM` erratum form,
|
|
// with an optional timezone, and still rejects malformed or out-of-range ones.
|
|
#expect(valid("--02--", .gMonth) && valid("--02--Z", .gMonth) && valid("--02-05:00", .gMonth))
|
|
#expect(!valid("--13--", .gMonth) && !valid("--02-", .gMonth) && !valid("--02--x", .gMonth))
|
|
}
|
|
|
|
// MARK: Other primitives
|
|
|
|
@Test("hexBinary, base64Binary, and names")
|
|
func test_binaryAndNames() {
|
|
#expect(valid("0fB7", .hexBinary) && !valid("0fB", .hexBinary))
|
|
#expect(valid("YW55", .base64Binary) && valid("YW4=", .base64Binary))
|
|
#expect(valid("ns:local", .qName) && !valid("ns:1bad", .qName))
|
|
#expect(valid("Hello", .ncName) && !valid("a:b", .ncName))
|
|
#expect(valid("en-US", .language) && !valid("english-", .language))
|
|
#expect(valid("P1Y2M3DT4H", .duration) && !valid("P", .duration) && !valid("PT", .duration))
|
|
}
|
|
|
|
// MARK: Facets
|
|
|
|
@Test("whiteSpace collapse is applied before validation")
|
|
func test_whiteSpace() {
|
|
#expect(Schema.SimpleType(base: .token).isValid(" a b "))
|
|
#expect(Schema.SimpleType(base: .int).isValid(" 42 "))
|
|
}
|
|
|
|
@Test("length, minLength, and maxLength count characters")
|
|
func test_lengthFacets() {
|
|
let type = Schema.SimpleType(base: .string, facets: .init(minLength: 2, maxLength: 4))
|
|
#expect(type.isValid("abc") && !type.isValid("a") && !type.isValid("abcde"))
|
|
}
|
|
|
|
@Test("pattern facets use the regex engine")
|
|
func test_patternFacet() {
|
|
let type = Schema.SimpleType(base: .string, facets: .init(patternGroups: [["[A-Z]{3}-\\d{2}"]]))
|
|
#expect(type.isValid("XYZ-42") && !type.isValid("xyz-42"))
|
|
}
|
|
|
|
@Test("enumeration compares in value space")
|
|
func test_enumerationFacet() {
|
|
let type = Schema.SimpleType(base: .decimal, facets: .init(enumeration: ["1.0", "2.5"]))
|
|
#expect(type.isValid("1.00") && type.isValid("2.5") && !type.isValid("3"))
|
|
}
|
|
|
|
@Test("inclusive and exclusive ranges")
|
|
func test_rangeFacets() {
|
|
let inclusive = Schema.SimpleType(base: .integer, facets: .init(minInclusive: "1", maxInclusive: "10"))
|
|
#expect(inclusive.isValid("1") && inclusive.isValid("10") && !inclusive.isValid("11"))
|
|
let exclusive = Schema.SimpleType(base: .integer, facets: .init(minExclusive: "1", maxExclusive: "10"))
|
|
#expect(!exclusive.isValid("1") && exclusive.isValid("2") && !exclusive.isValid("10"))
|
|
}
|
|
|
|
@Test("totalDigits and fractionDigits")
|
|
func test_digitFacets() {
|
|
let type = Schema.SimpleType(base: .decimal, facets: .init(totalDigits: 4, fractionDigits: 2))
|
|
#expect(type.isValid("12.34") && type.isValid("12") && !type.isValid("123.45") && !type.isValid("1.234"))
|
|
}
|
|
|
|
@Test("exact decimal comparison avoids floating-point error")
|
|
func test_exactDecimal() {
|
|
// 0.1 + 0.2 != 0.3 in binary; exact comparison handles boundary values.
|
|
let type = Schema.SimpleType(base: .decimal, facets: .init(maxInclusive: "0.3"))
|
|
#expect(type.isValid("0.3") && type.isValid("0.30") && !type.isValid("0.30000000001"))
|
|
}
|
|
}
|