From 0cf697c4ba0de846ac603df7d9a83d12997261ed Mon Sep 17 00:00:00 2001 From: Mihaela Mihaljevic Date: Wed, 10 Jun 2026 02:30:56 +0200 Subject: [PATCH] test(conformance): add an XPath axes/predicates corpus Grow the Tier-2 conformance corpus with 13 XPath navigation cases (descendant and all-element counts, positional and last() predicates, ancestor and ancestor-or-self axes, following/preceding-sibling, the attribute axis, an attribute predicate, union, the parent abbreviation, and position()) over a fixed tree, checked through the validation-driven harness. Own suite. Part of #105 (Tier 2) --- CHANGELOG.md | 2 +- README.md | 2 +- Tests/ConformanceXPathAxisTests.swift | 44 +++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 Tests/ConformanceXPathAxisTests.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b24ca2..204a373 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) an XPath 1.0 core-function corpus, and an XSD datatype/facet corpus (length, range, pattern, enumeration, lexical space), a RELAX NG pattern corpus (text, optional, repetition, choice, ordered group, attribute, interleave, empty), an XSLT 1.0 transformation corpus (value-of, for-each, if, choose, sort, variable, format-number, apply-templates, call-template), an HTML5 tree-construction corpus (void elements, implied tag closing, case normalization, the adoption agency, entities), a Schematron corpus (assert, report, rule context), and a C14N exclusive/2.0 corpus (unused-namespace dropping, render-at-use, whitespace trimming). The HTML corpus surfaced its first divergence (the fragment parser does not reconstruct active formatting elements like the document parser, tracked as #109). 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) an XPath 1.0 core-function corpus, and an XSD datatype/facet corpus (length, range, pattern, enumeration, lexical space), a RELAX NG pattern corpus (text, optional, repetition, choice, ordered group, attribute, interleave, empty), an XSLT 1.0 transformation corpus (value-of, for-each, if, choose, sort, variable, format-number, apply-templates, call-template), an HTML5 tree-construction corpus (void elements, implied tag closing, case normalization, the adoption agency, entities), a Schematron corpus (assert, report, rule context), a C14N exclusive/2.0 corpus (unused-namespace dropping, render-at-use, whitespace trimming), and an XPath axes/predicates corpus (descendant, ancestor, sibling axes, positional and attribute predicates, union). The HTML corpus surfaced its first divergence (the fragment parser does not reconstruct active formatting elements like the document parser, tracked as #109). 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 35626d5..009f3c2 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 **818 tests in 127 suites** (`swift test`). +currently runs **819 tests in 128 suites** (`swift test`). ### Shipped (libxml2-aligned surface) diff --git a/Tests/ConformanceXPathAxisTests.swift b/Tests/ConformanceXPathAxisTests.swift new file mode 100644 index 0000000..ec27595 --- /dev/null +++ b/Tests/ConformanceXPathAxisTests.swift @@ -0,0 +1,44 @@ +@testable import PureXML +import Testing + +/// XPath 1.0 axis and predicate conformance, driven through the validation +/// framework (its own suite to keep the main corpus under the cap). +@Suite("Conformance corpus: XPath axes and predicates") +struct ConformanceXPathAxisTests { + private struct AxisSpec { + let name: String + let expression: String + let expected: String + } + + /// Axis navigation and positional predicates over a fixed tree, checked + /// against the node-set the XPath 1.0 spec selects (coerced to a string). + private func corpus() throws -> [PureXML.Validation.ConformanceCase] { + let document = try PureXML.parse("xyz") + let specs = [ + AxisSpec(name: "descendant-count", expression: "count(//b)", expected: "2"), + AxisSpec(name: "all-elements-count", expression: "count(//*)", expected: "6"), + AxisSpec(name: "positional-predicate", expression: "//b[2]", expected: "y"), + AxisSpec(name: "last-predicate", expression: "//a[last()]/c", expected: "z"), + AxisSpec(name: "ancestor-axis", expression: "count(//c/ancestor::a)", expected: "1"), + AxisSpec(name: "ancestor-or-self", expression: "count(//c/ancestor-or-self::*)", expected: "3"), + AxisSpec(name: "following-sibling", expression: "count(//a[1]/following-sibling::a)", expected: "1"), + AxisSpec(name: "preceding-sibling", expression: "count(//a[2]/preceding-sibling::a)", expected: "1"), + AxisSpec(name: "attribute-axis", expression: "//a[2]/@id", expected: "2"), + AxisSpec(name: "attribute-predicate", expression: "count(//a[@id='1'])", expected: "1"), + AxisSpec(name: "union", expression: "count(//b | //c)", expected: "3"), + AxisSpec(name: "parent-abbreviation", expression: "//c/../@id", expected: "2"), + AxisSpec(name: "position-function", expression: "count(//a[position()=1])", expected: "1"), + ] + 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 XPath axes/predicates conformance corpus passes with no located failures") + func test_axisCorpusConforms() throws { + let failures = try PureXML.Validation.Conformance.failures(in: corpus()) + #expect(failures.isEmpty, "\(failures.map(\.reason))") + } +}