Group @testable imports at the bottom, per public/rules-swift's swiftformat. A behavior-preserving import-ordering reformat across 252 files (mostly tests). --trailingclosures was deliberately not adopted (cosmetic, and swiftformat flags it as caution-worthy).
271 lines
16 KiB
Swift
271 lines
16 KiB
Swift
import Testing
|
|
@testable import PureXML
|
|
|
|
/// A first Tier-2 conformance corpus, driven through the validation framework:
|
|
/// each case runs a PureXML subsystem and the `Conformance` validation rule
|
|
/// reports any divergence from the spec-authoritative expected output.
|
|
@Suite("Conformance corpus (validation-driven)")
|
|
struct ConformanceCorpusTests {
|
|
private struct Spec {
|
|
let name: String
|
|
let input: String
|
|
let expected: String
|
|
}
|
|
|
|
/// Canonical XML 1.0 (inclusive, no comments) conformance points, with the
|
|
/// expected canonical form taken from the specification's rules.
|
|
private func canonicalCorpus() throws -> [PureXML.Validation.ConformanceCase] {
|
|
let specs = [
|
|
Spec(name: "attribute-ordering", input: "<e b=\"2\" a=\"1\"></e>", expected: "<e a=\"1\" b=\"2\"></e>"),
|
|
Spec(name: "empty-element-expanded", input: "<e/>", expected: "<e></e>"),
|
|
Spec(name: "comments-removed", input: "<e><!--c-->x</e>", expected: "<e>x</e>"),
|
|
Spec(name: "cdata-becomes-escaped-text", input: "<e><![CDATA[<x>&]]></e>", expected: "<e><x>&</e>"),
|
|
Spec(name: "processing-instruction", input: "<e><?pi data?></e>", expected: "<e><?pi data?></e>"),
|
|
Spec(name: "namespace-rendered", input: "<e xmlns=\"urn:x\"></e>", expected: "<e xmlns=\"urn:x\"></e>"),
|
|
]
|
|
return try specs.map { spec in
|
|
let actual = try PureXML.Canonical.canonicalize(PureXML.parse(spec.input))
|
|
return PureXML.Validation.ConformanceCase(name: spec.name, actual: actual, expected: spec.expected)
|
|
}
|
|
}
|
|
|
|
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("<r><item/><item/><item/></r>")
|
|
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)
|
|
}
|
|
}
|
|
|
|
private struct DatatypeSpec {
|
|
let name: String
|
|
let base: String
|
|
let facets: String
|
|
let value: String
|
|
let valid: Bool
|
|
}
|
|
|
|
/// XSD datatype and facet conformance: an instance is validated against an
|
|
/// element whose simple type restricts `base` with `facets`, and the verdict
|
|
/// (valid / invalid) is checked against the specification.
|
|
private func datatypeCorpus() throws -> [PureXML.Validation.ConformanceCase] {
|
|
let specs = [
|
|
DatatypeSpec(name: "integer-valid", base: "xs:integer", facets: "", value: "42", valid: true),
|
|
DatatypeSpec(name: "integer-rejects-decimal", base: "xs:integer", facets: "", value: "4.2", valid: false),
|
|
DatatypeSpec(name: "minInclusive-ok", base: "xs:int", facets: "<xs:minInclusive value=\"0\"/>", value: "5", valid: true),
|
|
DatatypeSpec(name: "minInclusive-fail", base: "xs:int", facets: "<xs:minInclusive value=\"0\"/>", value: "-1", valid: false),
|
|
DatatypeSpec(name: "maxInclusive-fail", base: "xs:int", facets: "<xs:maxInclusive value=\"10\"/>", value: "11", valid: false),
|
|
DatatypeSpec(name: "length-ok", base: "xs:string", facets: "<xs:length value=\"3\"/>", value: "abc", valid: true),
|
|
DatatypeSpec(name: "length-fail", base: "xs:string", facets: "<xs:length value=\"3\"/>", value: "ab", valid: false),
|
|
DatatypeSpec(name: "pattern-ok", base: "xs:string", facets: "<xs:pattern value=\"[a-z]+\"/>", value: "abc", valid: true),
|
|
DatatypeSpec(name: "pattern-fail", base: "xs:string", facets: "<xs:pattern value=\"[a-z]+\"/>", value: "ab1", valid: false),
|
|
DatatypeSpec(name: "enumeration-ok", base: "xs:string", facets: "<xs:enumeration value=\"red\"/><xs:enumeration value=\"green\"/>", value: "red", valid: true),
|
|
DatatypeSpec(name: "enumeration-fail", base: "xs:string", facets: "<xs:enumeration value=\"red\"/><xs:enumeration value=\"green\"/>", value: "blue", valid: false),
|
|
DatatypeSpec(name: "boolean-ok", base: "xs:boolean", facets: "", value: "true", valid: true),
|
|
DatatypeSpec(name: "boolean-fail", base: "xs:boolean", facets: "", value: "yes", valid: false),
|
|
]
|
|
return try specs.map { spec in
|
|
let xsd = """
|
|
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
|
<xs:element name="v"><xs:simpleType><xs:restriction base="\(spec.base)">\(spec.facets)</xs:restriction></xs:simpleType></xs:element>
|
|
</xs:schema>
|
|
"""
|
|
let errors = try PureXML.Schema.Document(xsd).validate("<v>\(spec.value)</v>")
|
|
return PureXML.Validation.ConformanceCase(
|
|
name: spec.name,
|
|
actual: errors.isEmpty ? "valid" : "invalid",
|
|
expected: spec.valid ? "valid" : "invalid",
|
|
)
|
|
}
|
|
}
|
|
|
|
private struct RelaxNGSpec {
|
|
let name: String
|
|
let schema: String
|
|
let xml: String
|
|
let valid: Bool
|
|
}
|
|
|
|
/// RELAX NG pattern conformance: an instance is validated against a compact
|
|
/// schema, and the verdict is checked against the pattern's semantics.
|
|
private func relaxNGCorpus() throws -> [PureXML.Validation.ConformanceCase] {
|
|
let specs = [
|
|
RelaxNGSpec(name: "text-ok", schema: "start = element a { text }", xml: "<a>hi</a>", valid: true),
|
|
RelaxNGSpec(name: "text-rejects-child", schema: "start = element a { text }", xml: "<a><b/></a>", valid: false),
|
|
RelaxNGSpec(name: "optional-present", schema: "start = element a { element b { text }? }", xml: "<a><b>x</b></a>", valid: true),
|
|
RelaxNGSpec(name: "optional-absent", schema: "start = element a { element b { text }? }", xml: "<a></a>", valid: true),
|
|
RelaxNGSpec(name: "oneOrMore-fail", schema: "start = element a { element b { text }+ }", xml: "<a></a>", valid: false),
|
|
RelaxNGSpec(name: "choice-ok", schema: "start = element a { element b { text } | element c { text } }", xml: "<a><c>y</c></a>", valid: true),
|
|
RelaxNGSpec(name: "group-order-fail", schema: "start = element a { element b { text }, element c { text } }", xml: "<a><c>y</c><b>x</b></a>", valid: false),
|
|
RelaxNGSpec(name: "attribute-required", schema: "start = element a { attribute id { text } }", xml: "<a></a>", valid: false),
|
|
RelaxNGSpec(name: "attribute-ok", schema: "start = element a { attribute id { text } }", xml: "<a id='1'></a>", valid: true),
|
|
RelaxNGSpec(name: "interleave-order-independent", schema: "start = element a { element b { text } & element c { text } }", xml: "<a><c>y</c><b>x</b></a>", valid: true),
|
|
RelaxNGSpec(name: "empty-ok", schema: "start = element a { empty }", xml: "<a></a>", valid: true),
|
|
RelaxNGSpec(name: "empty-rejects-text", schema: "start = element a { empty }", xml: "<a>x</a>", valid: false),
|
|
]
|
|
return try specs.map { spec in
|
|
let conforms = try PureXML.Schema.RelaxNG(compact: spec.schema).validate(spec.xml)
|
|
return PureXML.Validation.ConformanceCase(
|
|
name: spec.name,
|
|
actual: conforms ? "valid" : "invalid",
|
|
expected: spec.valid ? "valid" : "invalid",
|
|
)
|
|
}
|
|
}
|
|
|
|
private struct XSLTSpec {
|
|
let name: String
|
|
let stylesheet: String
|
|
let source: String
|
|
let expected: String
|
|
}
|
|
|
|
/// A text-output stylesheet wrapping a single `match="/"` template body, so
|
|
/// the result is exactly the body's output with no stray whitespace.
|
|
private func textSheet(_ body: String) -> String {
|
|
"<xsl:stylesheet version=\"1.0\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\">"
|
|
+ "<xsl:output method=\"text\"/><xsl:template match=\"/\">\(body)</xsl:template></xsl:stylesheet>"
|
|
}
|
|
|
|
/// XSLT 1.0 transformation conformance: a stylesheet over a source, with the
|
|
/// text result checked against what the spec prescribes.
|
|
private func xsltCorpus() throws -> [PureXML.Validation.ConformanceCase] {
|
|
let multiTemplate = "<xsl:stylesheet version=\"1.0\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\">"
|
|
+ "<xsl:output method=\"text\"/>"
|
|
+ "<xsl:template match=\"/\"><xsl:apply-templates select=\"/r/i\"/></xsl:template>"
|
|
+ "<xsl:template match=\"i\">[<xsl:value-of select=\".\"/>]</xsl:template></xsl:stylesheet>"
|
|
let calling = "<xsl:stylesheet version=\"1.0\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\">"
|
|
+ "<xsl:output method=\"text\"/>"
|
|
+ "<xsl:template match=\"/\"><xsl:call-template name=\"greet\"><xsl:with-param name=\"who\" select=\"'world'\"/></xsl:call-template></xsl:template>"
|
|
+ "<xsl:template name=\"greet\"><xsl:param name=\"who\"/>hi <xsl:value-of select=\"$who\"/></xsl:template></xsl:stylesheet>"
|
|
let specs = [
|
|
XSLTSpec(name: "value-of", stylesheet: textSheet("<xsl:value-of select=\"/r/x\"/>"), source: "<r><x>hi</x></r>", expected: "hi"),
|
|
XSLTSpec(
|
|
name: "for-each",
|
|
stylesheet: textSheet("<xsl:for-each select=\"/r/i\"><xsl:value-of select=\".\"/></xsl:for-each>"),
|
|
source: "<r><i>a</i><i>b</i></r>",
|
|
expected: "ab",
|
|
),
|
|
XSLTSpec(name: "if", stylesheet: textSheet("<xsl:if test=\"/r/x\">yes</xsl:if>"), source: "<r><x>1</x></r>", expected: "yes"),
|
|
XSLTSpec(
|
|
name: "choose-otherwise",
|
|
stylesheet: textSheet("<xsl:choose><xsl:when test=\"1=2\">no</xsl:when><xsl:otherwise>else</xsl:otherwise></xsl:choose>"),
|
|
source: "<x/>",
|
|
expected: "else",
|
|
),
|
|
XSLTSpec(
|
|
name: "sort",
|
|
stylesheet: textSheet("<xsl:for-each select=\"/r/i\"><xsl:sort select=\".\"/><xsl:value-of select=\".\"/></xsl:for-each>"),
|
|
source: "<r><i>3</i><i>1</i><i>2</i></r>",
|
|
expected: "123",
|
|
),
|
|
XSLTSpec(name: "variable", stylesheet: textSheet("<xsl:variable name=\"v\" select=\"'x'\"/><xsl:value-of select=\"$v\"/>"), source: "<x/>", expected: "x"),
|
|
XSLTSpec(name: "format-number", stylesheet: textSheet("<xsl:value-of select=\"format-number(1234.5,'#,##0.00')\"/>"), source: "<x/>", expected: "1,234.50"),
|
|
XSLTSpec(name: "concat", stylesheet: textSheet("<xsl:value-of select=\"concat(/r/a,'-',/r/b)\"/>"), source: "<r><a>x</a><b>y</b></r>", expected: "x-y"),
|
|
XSLTSpec(name: "apply-templates", stylesheet: multiTemplate, source: "<r><i>a</i><i>b</i></r>", expected: "[a][b]"),
|
|
XSLTSpec(name: "call-template-param", stylesheet: calling, source: "<x/>", expected: "hi world"),
|
|
]
|
|
return try specs.map { spec in
|
|
try PureXML.Validation.ConformanceCase(
|
|
name: spec.name,
|
|
actual: PureXML.XSLT.transform(stylesheet: spec.stylesheet, source: spec.source),
|
|
expected: spec.expected,
|
|
)
|
|
}
|
|
}
|
|
|
|
/// HTML5 tree-construction conformance: the parsed-then-serialized fragment,
|
|
/// against the normalized form the HTML5 parsing algorithm prescribes. The
|
|
/// fragment parser now reconstructs active formatting elements (#109 fixed).
|
|
private func htmlCorpus() -> [PureXML.Validation.ConformanceCase] {
|
|
let specs = [
|
|
Spec(name: "void-elements", input: "<p>a<br>b<img src=\"x\">c</p>", expected: "<p>a<br>b<img src=\"x\">c</p>"),
|
|
Spec(name: "implied-li-close", input: "<ul><li>one<li>two</ul>", expected: "<ul><li>one</li><li>two</li></ul>"),
|
|
Spec(name: "implied-p-close", input: "<p>first<p>second", expected: "<p>first</p><p>second</p>"),
|
|
Spec(name: "tag-case-normalized", input: "<DIV><SPAN>x</SPAN></DIV>", expected: "<div><span>x</span></div>"),
|
|
Spec(name: "adoption-agency", input: "<b><i></b></i>", expected: "<b><i></i></b>"),
|
|
Spec(name: "adoption-agency-with-text", input: "<b><i></b>X</i>", expected: "<b><i></i></b><i>X</i>"),
|
|
Spec(name: "entities-decoded", input: "<p>a & b < c A</p>", expected: "<p>a & b < c A</p>"),
|
|
Spec(name: "stray-end-tag-ignored", input: "<b>bold</i></b>", expected: "<b>bold</b>"),
|
|
]
|
|
return specs.map { spec in
|
|
PureXML.Validation.ConformanceCase(
|
|
name: spec.name,
|
|
actual: PureXML.HTML.serialize(PureXML.HTML.parse(spec.input)),
|
|
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("The XSD datatype/facet conformance corpus passes with no located failures")
|
|
func test_datatypeCorpusConforms() throws {
|
|
let failures = try PureXML.Validation.Conformance.failures(in: datatypeCorpus())
|
|
#expect(failures.isEmpty, "\(failures.map(\.reason))")
|
|
}
|
|
|
|
@Test("The RELAX NG pattern conformance corpus passes with no located failures")
|
|
func test_relaxNGCorpusConforms() throws {
|
|
let failures = try PureXML.Validation.Conformance.failures(in: relaxNGCorpus())
|
|
#expect(failures.isEmpty, "\(failures.map(\.reason))")
|
|
}
|
|
|
|
@Test("The XSLT transformation conformance corpus passes with no located failures")
|
|
func test_xsltCorpusConforms() throws {
|
|
let failures = try PureXML.Validation.Conformance.failures(in: xsltCorpus())
|
|
#expect(failures.isEmpty, "\(failures.map(\.reason))")
|
|
}
|
|
|
|
@Test("The HTML5 tree-construction conformance corpus passes with no located failures")
|
|
func test_htmlCorpusConforms() {
|
|
let failures = PureXML.Validation.Conformance.failures(in: htmlCorpus())
|
|
#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: "<a></a>", expected: "<b></b>")
|
|
let failures = PureXML.Validation.Conformance.failures(in: [bad])
|
|
#expect(failures.count == 1)
|
|
#expect(failures.first?.codingPath.map(\.stringValue) == ["wrong"])
|
|
#expect(failures.first?.reason.contains("case 'wrong'") == true)
|
|
}
|
|
}
|