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: "", expected: ""),
Spec(name: "empty-element-expanded", input: "", expected: ""),
Spec(name: "comments-removed", input: "x", expected: "x"),
Spec(name: "cdata-becomes-escaped-text", input: "&]]>", expected: "<x>&"),
Spec(name: "processing-instruction", input: "", expected: ""),
Spec(name: "namespace-rendered", input: "", expected: ""),
]
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(" ")
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: "", value: "5", valid: true),
DatatypeSpec(name: "minInclusive-fail", base: "xs:int", facets: "", value: "-1", valid: false),
DatatypeSpec(name: "maxInclusive-fail", base: "xs:int", facets: "", value: "11", valid: false),
DatatypeSpec(name: "length-ok", base: "xs:string", facets: "", value: "abc", valid: true),
DatatypeSpec(name: "length-fail", base: "xs:string", facets: "", value: "ab", valid: false),
DatatypeSpec(name: "pattern-ok", base: "xs:string", facets: "", value: "abc", valid: true),
DatatypeSpec(name: "pattern-fail", base: "xs:string", facets: "", value: "ab1", valid: false),
DatatypeSpec(name: "enumeration-ok", base: "xs:string", facets: "", value: "red", valid: true),
DatatypeSpec(name: "enumeration-fail", base: "xs:string", facets: "", 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 = """
\(spec.facets)
"""
let errors = try PureXML.Schema.Document(xsd).validate("\(spec.value)")
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: "hi", valid: true),
RelaxNGSpec(name: "text-rejects-child", schema: "start = element a { text }", xml: "", valid: false),
RelaxNGSpec(name: "optional-present", schema: "start = element a { element b { text }? }", xml: "x", valid: true),
RelaxNGSpec(name: "optional-absent", schema: "start = element a { element b { text }? }", xml: "", valid: true),
RelaxNGSpec(name: "oneOrMore-fail", schema: "start = element a { element b { text }+ }", xml: "", valid: false),
RelaxNGSpec(name: "choice-ok", schema: "start = element a { element b { text } | element c { text } }", xml: "y", valid: true),
RelaxNGSpec(name: "group-order-fail", schema: "start = element a { element b { text }, element c { text } }", xml: "yx", valid: false),
RelaxNGSpec(name: "attribute-required", schema: "start = element a { attribute id { text } }", xml: "", valid: false),
RelaxNGSpec(name: "attribute-ok", schema: "start = element a { attribute id { text } }", xml: "", valid: true),
RelaxNGSpec(name: "interleave-order-independent", schema: "start = element a { element b { text } & element c { text } }", xml: "yx", valid: true),
RelaxNGSpec(name: "empty-ok", schema: "start = element a { empty }", xml: "", valid: true),
RelaxNGSpec(name: "empty-rejects-text", schema: "start = element a { empty }", xml: "x", 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 {
""
+ "\(body)"
}
/// 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 = ""
+ ""
+ ""
+ "[]"
let calling = ""
+ ""
+ ""
+ "hi "
let specs = [
XSLTSpec(name: "value-of", stylesheet: textSheet(""), source: "hi", expected: "hi"),
XSLTSpec(
name: "for-each",
stylesheet: textSheet(""),
source: "ab",
expected: "ab",
),
XSLTSpec(name: "if", stylesheet: textSheet("yes"), source: "1", expected: "yes"),
XSLTSpec(
name: "choose-otherwise",
stylesheet: textSheet("noelse"),
source: "",
expected: "else",
),
XSLTSpec(
name: "sort",
stylesheet: textSheet(""),
source: "312",
expected: "123",
),
XSLTSpec(name: "variable", stylesheet: textSheet(""), source: "", expected: "x"),
XSLTSpec(name: "format-number", stylesheet: textSheet(""), source: "", expected: "1,234.50"),
XSLTSpec(name: "concat", stylesheet: textSheet(""), source: "xy", expected: "x-y"),
XSLTSpec(name: "apply-templates", stylesheet: multiTemplate, source: "ab", expected: "[a][b]"),
XSLTSpec(name: "call-template-param", stylesheet: calling, source: "", 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: "a
b
c
", expected: "a
b
c
"),
Spec(name: "implied-li-close", input: "", expected: ""),
Spec(name: "implied-p-close", input: "first
second", expected: "
first
second
"),
Spec(name: "tag-case-normalized", input: "x
", expected: "x
"),
Spec(name: "adoption-agency", input: "", expected: ""),
Spec(name: "adoption-agency-with-text", input: "X", expected: "X"),
Spec(name: "entities-decoded", input: "a & b < c A
", expected: "a & b < c A
"),
Spec(name: "stray-end-tag-ignored", input: "bold", expected: "bold"),
]
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: "", expected: "")
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)
}
}