PureXML/Tests/RelaxNGLocatedErrorTests.swift
Mihaela Mihaljevic 3be6832c23 feat(relaxng): quote the offending value and name the datatype in text errors
A RELAX NG text/datatype mismatch now reports the rich datatype reason
(quoting the value and naming the type, e.g. 'abc' is not a valid
integer) and a value mismatch quotes both the actual text and the
required literal, instead of a generic "text content is not valid". The
detail is peeled from the content pattern at the failure point through
choice, group, list, and ref.
2026-06-09 01:09:29 +02:00

125 lines
5.2 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>") })
// The message quotes the offending value and names the expected datatype.
#expect(found.contains { $0.contains("'abc'") && $0.contains("integer") })
}
@Test("A value mismatch quotes both the actual text and the required literal")
func test_valueMismatchDetail() throws {
let rng = """
<element name="flag" xmlns="http://relaxng.org/ns/structure/1.0">
<value>on</value>
</element>
"""
let found = try reasons(rng, "<flag>off</flag>")
#expect(found.contains { $0.contains("'off'") && $0.contains("'on'") })
}
@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)
}
}