The instance-validation differential treated "PureXML rejects, libxml2 accepts"
as a PureXML false positive. That is unsound: libxml2's own content validation is
lenient and accepts some INVALID instances. A DIFF_SEED sweep surfaced exactly
such a case (seed 13371337): the model ( (a*|c*){1,3}, b? ){1,3} and the instance
<doc><b/><b/><a/><b/><c/><a/></doc>. libxml2 accepts it, but it is genuinely
INVALID (the three b's anchor three (choice,b?) segments, leaving the trailing
'ca' with no fourth segment), so PureXML's rejection is correct, not a false
positive. Verified against an external regex engine and exhaustive enumeration.
Add ContentModelOracle, an independent brute-force membership matcher over element
names, implemented separately from PureXML's matcher. The generated instances are
empty xs:string elements, so an instance is valid iff its element-name sequence is
in the model's language, which this oracle decides exactly. The generator now
emits the structured ContentModel alongside the schema string (group references
inlined), and the differential classifies a false positive / false negative
against the ORACLE as ground truth; libxml2 is kept as a secondary cross-check
whose disagreements with the oracle are counted (its leniency) but never charged
to PureXML.
The oracle is unit-tested (ContentModelOracleTests), including the libxml2-leniency
case it exists to catch. Over a sweep of seeds the differential now reports zero
oracle-adjudicated false positives and zero false negatives, so PureXML's content
matcher agrees with the independent oracle on every generated case; the default
seed is unchanged. Test-only; no library behaviour change.
61 lines
3.2 KiB
Swift
61 lines
3.2 KiB
Swift
import Testing
|
|
@testable import PureXML
|
|
|
|
/// The independent content-model membership oracle that adjudicates instance-
|
|
/// differential disagreements. Pins the cases that matter, including the libxml2
|
|
/// leniency case the oracle exists to catch: a model libxml2 wrongly accepts an
|
|
/// invalid instance for, which the oracle (and PureXML) correctly reject.
|
|
@Suite("Content-model membership oracle")
|
|
struct ContentModelOracleTests {
|
|
private func tokens(_ string: String) -> [String] {
|
|
string.map(String.init)
|
|
}
|
|
|
|
/// `( (a*|c*){1,3} b? ){1,3}`, the model from the DIFF_SEED=13371337 case.
|
|
private var nestedNullable: ContentModel {
|
|
let choice = ContentModel.occurs(
|
|
.choice([.occurs(.element("a"), 0, nil), .occurs(.sequence([.occurs(.element("c"), 0, nil)]), 1, nil)]),
|
|
1, 3,
|
|
)
|
|
return .occurs(.sequence([choice, .occurs(.element("b"), 0, 1)]), 1, 3)
|
|
}
|
|
|
|
@Test("the nested nullable-repetition model matches exactly its language")
|
|
func test_nestedNullable() {
|
|
// 'bbabca' is the instance libxml2 LENIENTLY accepts but is NOT in the
|
|
// language: three b's anchor three (choice,b?) segments, leaving the
|
|
// trailing 'ca' with no fourth segment. The oracle (like PureXML) rejects it.
|
|
#expect(!ContentModelOracle.matches(nestedNullable, tokens("bbabca")))
|
|
#expect(!ContentModelOracle.matches(nestedNullable, tokens("bbabcab")))
|
|
// Members, cross-checked against an external regex engine.
|
|
#expect(ContentModelOracle.matches(nestedNullable, tokens("bbab")))
|
|
#expect(ContentModelOracle.matches(nestedNullable, tokens("aca")))
|
|
#expect(ContentModelOracle.matches(nestedNullable, tokens("abcabc")))
|
|
#expect(ContentModelOracle.matches(nestedNullable, tokens("b")))
|
|
#expect(ContentModelOracle.matches(nestedNullable, tokens(""))) // one empty outer iteration
|
|
}
|
|
|
|
@Test("sequence, choice, occurrence, and wildcard match as specified")
|
|
func test_basics() {
|
|
let seq = ContentModel.sequence([.element("a"), .occurs(.element("b"), 0, 1), .element("c")])
|
|
#expect(ContentModelOracle.matches(seq, tokens("abc")))
|
|
#expect(ContentModelOracle.matches(seq, tokens("ac")))
|
|
#expect(!ContentModelOracle.matches(seq, tokens("abbc")))
|
|
let choice = ContentModel.occurs(.choice([.element("a"), .element("c")]), 1, 3)
|
|
#expect(ContentModelOracle.matches(choice, tokens("aca")))
|
|
#expect(!ContentModelOracle.matches(choice, tokens("acac")))
|
|
let wildcard = ContentModel.occurs(.any, 0, nil)
|
|
#expect(ContentModelOracle.matches(wildcard, tokens("abc")))
|
|
}
|
|
|
|
@Test("an all group matches its members in any order, requiring the non-optional ones")
|
|
func test_allGroup() {
|
|
let all = ContentModel.all([.occurs(.element("a"), 1, 1), .occurs(.element("b"), 0, 1)])
|
|
#expect(ContentModelOracle.matches(all, tokens("a")))
|
|
#expect(ContentModelOracle.matches(all, tokens("ab")))
|
|
#expect(ContentModelOracle.matches(all, tokens("ba")))
|
|
#expect(!ContentModelOracle.matches(all, tokens("b"))) // required 'a' absent
|
|
#expect(!ContentModelOracle.matches(all, tokens("aa"))) // 'a' is max 1
|
|
}
|
|
}
|