PureXML/Tests/ContentMatcherDifferentialTests.swift
Mihaela Mihaljevic 68273205f6 feat(schema): full NameAndTypeOK type derivation in element restriction
Complete the element-vs-element type clause of Particle Valid (Restriction):
the restricting element's type must be, or derive by restriction from, the base
element's type. Previously only names were compared, so an unrelated, wider, or
ur-type element type was accepted under restriction.

Enrich the content-model particle: Term.element carries the element type's
derivation identity (the resolved type name, anyType for an absent type, nil for
an inline anonymous type). Record named simpleType restriction derivations in the
derivation table alongside complex ones. The element-vs-element check walks the
user restriction chain and then the built-in lattice to decide derivation by
name; an ur-type base, a union base (membership is not a restriction chain), and
an inline anonymous type are permitted, so no valid restriction is rejected.

XSTS: invalid schemas accepted 616 to 594, invalid instances accepted 165 to 160
(more invalid schemas rejected up front), valid schemas rejected held at 72,
valid instances rejected held at 214.
2026-06-14 13:47:18 +02:00

77 lines
3.6 KiB
Swift

@testable import PureXML
import Testing
/// Locks the two content-model matchers together (#115): the DTD matcher
/// (positional set closure) and the XSD matcher (Thompson NFA) solve the same
/// problem with different algorithms, so equivalent models must agree on every
/// child sequence.
@Suite("DTD vs XSD content-matcher differential")
struct ContentMatcherDifferentialTests {
private struct ModelCase {
let dtdModel: String
let xsdParticle: PureXML.Schema.Particle
let sequences: [(names: [String], valid: Bool)]
}
private func element(_ name: String, min: Int = 1, max: Int? = 1) -> PureXML.Schema.Particle {
.init(minOccurs: min, maxOccurs: max, term: .element(name: .init(name), type: nil, typeName: nil))
}
private func group(_ compositor: PureXML.Schema.Compositor, _ particles: [PureXML.Schema.Particle], min: Int = 1, max: Int? = 1) -> PureXML.Schema.Particle {
.init(minOccurs: min, maxOccurs: max, term: .group(.init(compositor: compositor, particles: particles)))
}
private var cases: [ModelCase] {
[
ModelCase(
dtdModel: "(a,b)",
xsdParticle: group(.sequence, [element("a"), element("b")]),
sequences: [(["a", "b"], true), (["b", "a"], false), (["a"], false), ([], false)],
),
ModelCase(
dtdModel: "(a|b)",
xsdParticle: group(.choice, [element("a"), element("b")]),
sequences: [(["a"], true), (["b"], true), (["a", "b"], false), ([], false)],
),
ModelCase(
dtdModel: "(a)*",
xsdParticle: group(.sequence, [element("a")], min: 0, max: nil),
sequences: [([], true), (["a"], true), (["a", "a", "a"], true), (["b"], false)],
),
ModelCase(
dtdModel: "(a)+",
xsdParticle: group(.sequence, [element("a")], min: 1, max: nil),
sequences: [([], false), (["a"], true), (["a", "a"], true)],
),
ModelCase(
dtdModel: "(a)?",
xsdParticle: group(.sequence, [element("a")], min: 0, max: 1),
sequences: [([], true), (["a"], true), (["a", "a"], false)],
),
ModelCase(
dtdModel: "((a,b)|c)",
xsdParticle: group(.choice, [group(.sequence, [element("a"), element("b")]), element("c")]),
sequences: [(["a", "b"], true), (["c"], true), (["a", "c"], false), (["a", "b", "c"], false)],
),
]
}
@Test("Equivalent DTD and XSD content models agree on every child sequence")
func test_matchersAgree() {
for modelCase in cases {
guard case let .children(particle) = PureXML.Validation.ContentModelParser.parse(modelCase.dtdModel) else {
Issue.record("DTD model '\(modelCase.dtdModel)' did not parse to element content")
continue
}
let nfa = PureXML.Schema.ContentNFABuilder.build(modelCase.xsdParticle)
for sequence in modelCase.sequences {
let dtd = PureXML.Validation.ContentModelMatcher.matchesChildren(particle, sequence.names)
let xsd = nfa.matchesWhole(sequence.names.map { PureXML.Model.QualifiedName($0) })
#expect(dtd == sequence.valid, "DTD '\(modelCase.dtdModel)' wrong for \(sequence.names)")
#expect(xsd == sequence.valid, "XSD '\(modelCase.dtdModel)' wrong for \(sequence.names)")
#expect(dtd == xsd, "matchers disagree on '\(modelCase.dtdModel)' for \(sequence.names)")
}
}
}
}