PureXML/Tests/XSDDatatypeTests.swift
Mihaela Mihaljevic 01650a454e fix(schema): reject year 0000 in dateTime, date, gYearMonth, gYear
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.
2026-06-20 01:31:14 +02:00

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"))
}
}