PureXML/Tests/SchematronTests.swift
Mihaela Mihaljevic d682f85daf refactor(validation): migrate Schematron, retire Issue
Slice 5 of #92. The last validator moves onto the framework and the legacy
Issue type is removed, so all validation now flows through one error model.

- ValidationError gains a severity (error by default, warning for advisory
  findings), chosen so Schematron's assert/report distinction survives in the
  unified type.
- Schematron is a Validation<Node, Void> over the document root; a failed assert
  is an error finding, a matched report a warning. Each finding is located at the
  context node its rule fired on (built from the element chain, with a sibling
  index only when a name repeats), not the document root.
- Schematron.validate returns [ValidationError] and runs through a Validator.
- Validation.Issue and its Severity-based result are deleted; Severity now backs
  ValidationError.

Every validation layer (structural, DTD, XSD content and identity, Schematron)
now produces located ValidationError values through the shared framework.
2026-06-08 12:49:19 +02:00

78 lines
2.9 KiB
Swift

@testable import PureXML
import Testing
@Suite("Schematron")
struct SchematronTests {
private let schema = """
<schema xmlns="http://purl.oclc.org/dsdl/schematron">
<pattern>
<rule context="book">
<assert test="title">a book must have a title</assert>
<assert test="@isbn">a book must have an isbn</assert>
<report test="count(author) &gt; 2">more than two authors</report>
</rule>
</pattern>
</schema>
"""
private func validate(_ xml: String) throws -> [PureXML.Validation.ValidationError] {
try PureXML.Validation.Schematron(schema: schema).validate(xml)
}
@Test("A conforming document produces no issues")
func test_valid() throws {
let xml = "<library><book isbn=\"1\"><title>T</title><author>A</author></book></library>"
#expect(try validate(xml).isEmpty)
}
@Test("A failed assert is reported as an error located at its context node")
func test_failedAssert() throws {
let xml = "<library><book isbn=\"1\"><author>A</author></book></library>"
let issues = try validate(xml)
#expect(issues.count == 1)
#expect(issues.first?.severity == .error)
#expect(issues.first?.reason == "a book must have a title")
#expect(String(describing: issues[0]) == "a book must have a title at path: library/book")
}
@Test("Multiple failed asserts each report")
func test_multipleAsserts() throws {
let xml = "<library><book><author>A</author></book></library>"
let messages = try validate(xml).map(\.reason)
#expect(messages.contains("a book must have a title"))
#expect(messages.contains("a book must have an isbn"))
}
@Test("A matched report is a warning")
func test_report() throws {
let xml = "<library><book isbn=\"1\"><title>T</title>"
+ "<author>A</author><author>B</author><author>C</author></book></library>"
let issues = try validate(xml)
#expect(issues.count == 1)
#expect(issues.first?.severity == .warning)
#expect(issues.first?.reason == "more than two authors")
}
@Test("Rules fire once per matching node across the document")
func test_perNode() throws {
let xml = "<library>"
+ "<book isbn=\"1\"><title>T</title></book>"
+ "<book><author>A</author></book>"
+ "</library>"
// The second book is missing both title and isbn.
let issues = try validate(xml)
#expect(issues.count == 2)
#expect(issues.allSatisfy { $0.severity == .error })
}
@Test("An invalid test expression is rejected at compile time")
func test_invalidSchema() {
let bad = """
<schema><pattern><rule context="book"><assert test="(">x</assert></rule></pattern></schema>
"""
#expect(throws: Error.self) {
_ = try PureXML.Validation.Schematron(schema: bad)
}
}
}