PureXML/Tests/RelaxNGLocatedErrorTests.swift
Mihaela Mihaljevic d80718c530 feat(relaxng): distinguish undeclared attribute from invalid value
A RELAX NG attribute failure now reports an undeclared attribute
("attribute @b is not allowed on <n>") separately from a declared
attribute carrying a bad value ("attribute @count on <n>: 'abc' is not a
valid integer"), quoting the value and naming its datatype by reusing the
text detail. The declared value pattern is found by matching the
attribute name through the residual.
2026-06-09 01:11:54 +02:00

141 lines
5.9 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)
// The declared attribute's bad value is quoted and the type named.
#expect(found.first?.reason.contains("'abc'") == true)
#expect(found.first?.reason.contains("integer") == true)
#expect(found.first?.codingPath.map(\.stringValue) == ["n", "@count"])
}
@Test("An undeclared attribute is distinguished from a bad value")
func test_undeclaredAttribute() throws {
let rng = """
<element name="n" xmlns="http://relaxng.org/ns/structure/1.0">
<attribute name="a"><text/></attribute>
</element>
"""
let found = try errors(rng, "<n a='x' b='y'/>")
#expect(found.count == 1)
#expect(found.first?.reason.contains("@b") == true)
#expect(found.first?.reason.contains("is not allowed") == true)
}
@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)
}
}