import Testing
@testable import PureXML
/// The validity constraints added with the Sun-section burn-down: declaration-
/// level checks (reported at the document root), the root/DOCTYPE name match,
/// lexical checks on the ID family, CDATA as character data in element
/// content, and the strict-mode every-attribute-declared rule.
@Suite("DTD validity constraints")
struct DTDValidityConstraintTests {
private func errors(_ xml: String, strict: Bool = false) throws -> [String] {
try PureXML.validateAgainstInternalDTD(xml, strict: strict).map(\.reason)
}
@Test("The root element must match the DOCTYPE name")
func test_rootElementType() throws {
let xml = "]>\n"
#expect(try errors(xml).contains { $0.contains("does not match the DOCTYPE name") })
let matching = "]>\n"
#expect(try errors(matching).isEmpty)
}
@Test("An element type declared twice is reported")
func test_uniqueElementDeclaration() throws {
let xml = "]>\n"
#expect(try errors(xml).contains { $0.contains("declared more than once") })
}
@Test("Declaration findings carry the declaration's coding path")
func test_findingsAreLocated() throws {
let xml = "]>\n"
let rendered = try PureXML.validateAgainstInternalDTD(xml).map { String(describing: $0) }
#expect(rendered.contains { $0.hasSuffix("at path: x") })
#expect(rendered.contains { $0.hasSuffix("at path: r/@a") })
#expect(!rendered.contains { $0.contains("at root of document") })
}
@Test("Mixed content may not repeat a name")
func test_mixedDuplicates() throws {
let xml = "]>\n"
#expect(try errors(xml).contains { $0.contains("repeats 'x'") })
}
@Test("An element type may declare at most one ID attribute")
func test_oneIDPerElement() throws {
let xml = """
]>
"""
#expect(try errors(xml).contains { $0.contains("more than one ID attribute") })
}
@Test("NOTATION lists and NDATA entities must name declared notations")
func test_notationsDeclared() throws {
let list = "]>\n"
#expect(try errors(list).contains { $0.contains("lists undeclared notation 'b'") })
let ndata = "]>\n"
#expect(try errors(ndata).contains { $0.contains("names undeclared notation 'missing'") })
}
@Test("Attribute defaults must be legal for their type, and IDs default-free")
func test_attributeDefaultLegal() throws {
let id = "]>\n"
#expect(try errors(id).contains { $0.contains("must be #IMPLIED or #REQUIRED") })
let idref = "]>\n"
#expect(try errors(idref).contains { $0.contains("IDREF default") })
let nmtokens = "]>\n"
#expect(try errors(nmtokens).contains { $0.contains("NMTOKENS default") })
let entities = "]>\n"
#expect(try errors(entities).contains { $0.contains("ENTITIES default") })
let enumerated = "]>\n"
#expect(try errors(enumerated).contains { $0.contains("outside its enumeration") })
}
@Test("ID and IDREF values must be lexical Names")
func test_idLexicalForm() throws {
let xml = "]>\n"
#expect(try errors(xml).contains { $0.contains("not a valid ID") })
}
@Test("A CDATA section is character data in element content")
func test_cdataInElementContent() throws {
let xml = "]>\n"
#expect(try errors(xml).contains { $0.contains("contains character data") })
}
@Test("Strict mode requires every attribute to be declared")
func test_undeclaredAttribute() throws {
let xml = "]>\n"
#expect(try errors(xml, strict: true).contains { $0.contains("is not declared") })
#expect(try errors(xml).isEmpty)
}
}