diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d08644..1cdb5e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Conformance corpus harness (#105, Tier 2): the start of conformance-suite checking, expressed through the validation framework (the OpenAPIKit idiom). A `PureXML.Validation.ConformanceCase` is a `Validatable` named expectation (actual vs spec-authoritative expected output), and `Conformance.matchesExpected` is a `Validation` rule that emits one located `ValidationError` per diverging case, so a whole suite reports every failure at once with the case name as its coding path. Seeded with a Canonical XML 1.0 corpus (attribute ordering, empty-element expansion, comment removal, CDATA-to-escaped-text, processing instructions, namespace rendering). Growing the corpora against the official W3C/OASIS suites is the ongoing Tier-2 work. +- Conformance corpus harness (#105, Tier 2): the start of conformance-suite checking, expressed through the validation framework (the OpenAPIKit idiom). A `PureXML.Validation.ConformanceCase` is a `Validatable` named expectation (actual vs spec-authoritative expected output), and `Conformance.matchesExpected` is a `Validation` rule that emits one located `ValidationError` per diverging case, so a whole suite reports every failure at once with the case name as its coding path. Seeded with a Canonical XML 1.0 corpus (attribute ordering, empty-element expansion, comment removal, CDATA-to-escaped-text, processing instructions, namespace rendering) and an XPath 1.0 core-function corpus (the string, number, boolean, and node functions against their spec-mandated results, including the `Infinity`/`NaN` division cases). Growing the corpora against the official W3C/OASIS suites is the ongoing Tier-2 work. - EXSLT extension functions (#105, Tier 3): the `common`, `math`, and `sets` EXSLT modules, dispatched by resolving a function's prefix to its namespace (so any prefix bound to an EXSLT namespace works). `math:min`, `math:max`, `math:highest`, `math:lowest`, `math:abs`, `math:sqrt` (with the EXSLT NaN rule for non-numeric nodes); `set:distinct`, `set:difference`, `set:intersection`, `set:has-same-node`, `set:leading`, `set:trailing` (by node identity and document order); `exsl:object-type`. `math:power` and the other transcendentals are omitted because they need a C math library, which the pure-Swift, Foundation-free target excludes; `exsl:node-set` awaits result-tree-fragment support. - Canonical XML 1.1 (#105, Tier 3): a `Canonical.Options.canonical11` preset and a `mergeInheritedBase` option. When canonicalizing a document subset, the `xml:base` values of the apex's omitted ancestors are merged into the apex by RFC 3986 reference resolution (vs. 1.0's nearest-ancestor rule), the apex's own relative `xml:base` is resolved against that chain, and `xml:id` is no longer inherited; `xml:lang` and `xml:space` still inherit the nearest. A self-contained RFC 3986 resolver (scheme/authority/path parsing, path merging, dot-segment removal) backs the merge. - Single-byte legacy encodings (#97): the byte decoder gains a table-based single-byte path and the full set of single-byte legacy encodings, selected by the XML declaration's `encoding` name for both the whole-buffer and streaming decoders: the complete ISO-8859 family (parts 2 through 16), Windows-1250/1251/1253/1254/1255/1256/1257/1258, and KOI8-R/U. Most are vendored verbatim from the authoritative `unicode.org` mapping files; ISO-8859-5/9/15 are derived exactly; Windows-1254 composes from Windows-1252. Multi-byte CJK encodings (#99) are tracked separately. diff --git a/README.md b/README.md index 18e7d58..dc9dca9 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ flowchart TB PureXML is a working, dependency-free XML library today: parse, emit, validate, query, and stream documents on macOS, Linux, Windows, and WASM. The test suite -currently runs **809 tests in 125 suites** (`swift test`). +currently runs **810 tests in 125 suites** (`swift test`). ### Shipped (libxml2-aligned surface) diff --git a/Tests/ConformanceCorpusTests.swift b/Tests/ConformanceCorpusTests.swift index 5cdda1c..d0120f8 100644 --- a/Tests/ConformanceCorpusTests.swift +++ b/Tests/ConformanceCorpusTests.swift @@ -29,12 +29,52 @@ struct ConformanceCorpusTests { } } + private struct XPathSpec { + let name: String + let expression: String + let expected: String + } + + /// XPath 1.0 core-function conformance points: the function's result coerced + /// to a string, against the value the specification mandates. + private func xpathCorpus() throws -> [PureXML.Validation.ConformanceCase] { + let document = try PureXML.parse("") + let specs = [ + XPathSpec(name: "concat", expression: "concat('a','b','c')", expected: "abc"), + XPathSpec(name: "substring-length", expression: "substring('12345',2,3)", expected: "234"), + XPathSpec(name: "substring-open", expression: "substring('12345',2)", expected: "2345"), + XPathSpec(name: "string-length", expression: "string-length('hello')", expected: "5"), + XPathSpec(name: "normalize-space", expression: "normalize-space(' a b ')", expected: "a b"), + XPathSpec(name: "translate", expression: "translate('bar','abc','ABC')", expected: "BAr"), + XPathSpec(name: "substring-before", expression: "substring-before('a/b','/')", expected: "a"), + XPathSpec(name: "substring-after", expression: "substring-after('a/b','/')", expected: "b"), + XPathSpec(name: "contains", expression: "contains('hello','ell')", expected: "true"), + XPathSpec(name: "starts-with", expression: "starts-with('hello','he')", expected: "true"), + XPathSpec(name: "round-half-up", expression: "round(2.5)", expected: "3"), + XPathSpec(name: "floor", expression: "floor(2.9)", expected: "2"), + XPathSpec(name: "ceiling", expression: "ceiling(2.1)", expected: "3"), + XPathSpec(name: "count", expression: "count(//item)", expected: "3"), + XPathSpec(name: "div-by-zero-infinity", expression: "1 div 0", expected: "Infinity"), + XPathSpec(name: "zero-div-zero-nan", expression: "0 div 0", expected: "NaN"), + ] + return try specs.map { spec in + let actual = try PureXML.XPath.Query(spec.expression).string(over: document) + return PureXML.Validation.ConformanceCase(name: spec.name, actual: actual, expected: spec.expected) + } + } + @Test("The C14N conformance corpus passes with no located failures") func test_corpusConforms() throws { let failures = try PureXML.Validation.Conformance.failures(in: canonicalCorpus()) #expect(failures.isEmpty, "\(failures.map(\.reason))") } + @Test("The XPath core-function conformance corpus passes with no located failures") + func test_xpathCorpusConforms() throws { + let failures = try PureXML.Validation.Conformance.failures(in: xpathCorpus()) + #expect(failures.isEmpty, "\(failures.map(\.reason))") + } + @Test("A divergent case is reported as a located conformance failure") func test_divergenceReported() { let bad = PureXML.Validation.ConformanceCase(name: "wrong", actual: "", expected: "")