PureXML/Tests/RelaxNGLocatedErrorTests.swift
Mihaela Mihaljevic ae271d65f6 feat(relaxng): located, recovering validation errors for editor use
Adds RelaxNG.errors(in:) reporting every way a document fails the schema
as individual located errors with coding paths, recovering past each
failure so all of a faulty document's problems surface at once instead of
only the first. Errors carry recovery hints naming the element types
accepted at the failure point, and distinguish unexpected elements,
invalid or missing attributes, invalid text/datatype content, and missing
required content. RelaxNG.validation() exposes the schema as a
Validation<Node, Void>, composing with the same framework as the XSD,
DTD, and Schematron rules. The boolean derivative engine remains the
authority on validity; the diagnostic walk only places and explains the
failures, recovering each step so it never cascades. Splits the engine
and the diagnostics into separate files.
2026-06-09 00:40:56 +02:00

112 lines
4.6 KiB
Swift

@testable import PureXML
import Testing
@Suite("RELAX NG located, recovering validation errors")
struct RelaxNGLocatedErrorTests {
private func errors(_ rng: String, _ xml: String) throws -> [PureXML.Validation.ValidationError] {
try PureXML.Schema.RelaxNG(rng).errors(in: xml)
}
private func reasons(_ rng: String, _ xml: String) throws -> [String] {
try errors(rng, xml).map(\.reason)
}
private func paths(_ rng: String, _ xml: String) throws -> [[String]] {
try errors(rng, xml).map { $0.codingPath.map(\.stringValue) }
}
private let twoChildren = """
<element name="root" xmlns="http://relaxng.org/ns/structure/1.0">
<element name="a"><element name="x"><empty/></element></element>
<element name="b"><element name="y"><empty/></element></element>
</element>
"""
@Test("A valid document yields no errors")
func test_valid() throws {
#expect(try errors(twoChildren, "<root><a><x/></a><b><y/></b></root>").isEmpty)
}
@Test("Two independent subtree errors are both reported (recovery, not one error)")
func test_multipleErrorsWithRecovery() throws {
let xml = "<root><a><wrong/></a><b><alsowrong/></b></root>"
let found = try errors(twoChildren, xml)
#expect(found.count == 2)
let reasons = found.map(\.reason)
#expect(reasons.contains { $0.contains("<wrong>") && $0.contains("<a>") })
#expect(reasons.contains { $0.contains("<alsowrong>") && $0.contains("<b>") })
}
@Test("Each error is located at the offending nested element")
func test_locatedPaths() throws {
let xml = "<root><a><wrong/></a><b><alsowrong/></b></root>"
let located = try paths(twoChildren, xml)
#expect(located.contains(["root", "a", "wrong"]))
#expect(located.contains(["root", "b", "alsowrong"]))
}
@Test("An unexpected element carries an expected-content recovery hint")
func test_expectedHint() throws {
let xml = "<root><a><wrong/></a><b><y/></b></root>"
let found = try reasons(twoChildren, xml)
#expect(found.count == 1)
#expect(found.first?.contains("expected <x>") == true)
}
@Test("Missing required content is reported with what was expected")
func test_missingContent() throws {
let xml = "<root><a><x/></a></root>"
let found = try reasons(twoChildren, xml)
#expect(found.contains { $0.contains("missing required content") && $0.contains("<b>") })
}
@Test("An extra trailing element is reported as unexpected")
func test_extraElement() throws {
let xml = "<root><a><x/></a><b><y/></b><c/></root>"
let found = try errors(twoChildren, xml)
#expect(found.count == 1)
#expect(found.first?.reason.contains("<c>") == true)
}
@Test("An invalid attribute value is located on its element")
func test_attribute() throws {
let rng = """
<element name="n" datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes" xmlns="http://relaxng.org/ns/structure/1.0">
<attribute name="count"><data type="integer"/></attribute>
</element>
"""
let found = try errors(rng, "<n count='abc'/>")
#expect(found.count == 1)
#expect(found.first?.reason.contains("@count") == true)
#expect(found.first?.codingPath.map(\.stringValue) == ["n", "@count"])
}
@Test("Invalid datatype content is located on the element")
func test_dataValue() throws {
let rng = """
<element name="n" datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes" xmlns="http://relaxng.org/ns/structure/1.0">
<data type="integer"/>
</element>
"""
let found = try reasons(rng, "<n>abc</n>")
#expect(found.contains { $0.contains("<n>") })
}
@Test("The validation() value composes with the framework like the other validators")
func test_validationValueForm() throws {
let schema = try PureXML.Schema.RelaxNG(twoChildren)
let node = try PureXML.parse("<root><a><wrong/></a><b><alsowrong/></b></root>")
let collected = PureXML.Validation.Validator<Void>.blank
.validating(schema.validation())
.errors(for: node, in: ())
#expect(collected.count == 2)
}
@Test("The same value-space datatypes still validate through located errors")
func test_compactSchema() throws {
let schema = try PureXML.Schema.RelaxNG(compact: "element greeting { \"hi\" }")
#expect(try schema.errors(in: "<greeting>hi</greeting>").isEmpty)
#expect(try !schema.errors(in: "<greeting>bye</greeting>").isEmpty)
}
}