XSD 1.0 has no year zero (the year before 0001 is -0001), so 0000-01-01T00:00:00 and the other 0000 forms are not valid values. DateTimeParser.parseYearOnly now rejects a zero magnitude for both signs, so negative years like -0001 still parse. Closes the XSTS dateTime011 false negative: invalid-instances-accepted 31 -> 30, no other bucket moved (valid-schemas-rejected and valid-instances-rejected held at 0). 1449 unit tests pass; the full XSTS suite settles at 0/43/0/30.
141 lines
6.3 KiB
Swift
141 lines
6.3 KiB
Swift
import Testing
|
|
@testable import PureXML
|
|
|
|
@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("XSD 1.0 has no year zero: 0000 is rejected across the year-bearing types")
|
|
func test_yearZero() {
|
|
// Rejected: the prohibited year 0000 (and -0000) wherever a year appears.
|
|
#expect(!valid("0000-01-01T00:00:00", .dateTime))
|
|
#expect(!valid("0000-01-01", .date))
|
|
#expect(!valid("0000-01", .gYearMonth))
|
|
#expect(!valid("0000", .gYear) && !valid("-0000", .gYear))
|
|
// Accepted: the smallest magnitudes either side of zero, and a normal year.
|
|
#expect(valid("0001-01-01T00:00:00", .dateTime) && valid("-0001-12-31", .date))
|
|
#expect(valid("0001", .gYear) && valid("2026", .gYear) && valid("-0044-03", .gYearMonth))
|
|
}
|
|
|
|
@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"))
|
|
}
|
|
}
|