import Testing
@testable import PureXML
/// The recovering reader (`PureXML.read`) never throws and never crashes: it
/// returns the maximal best-effort tree plus one located diagnostic per problem.
/// Its recovery is deterministic, so every assertion here pins an exact result.
@Suite("Recovering reader")
struct RecoveringReaderTests {
@Test("Well-formed input matches the strict parser with no diagnostics")
func test_wellFormed() throws {
let xml = "x"
let result = PureXML.read(xml)
#expect(result.diagnostics.isEmpty)
#expect(try result.node == PureXML.parse(xml))
}
@Test("Truncated input is closed into a best-effort tree with one diagnostic")
func test_truncated() throws {
let result = PureXML.read("x")
#expect(try result.node == PureXML.parse("x"))
#expect(result.diagnostics.count == 1)
#expect(result.diagnostics[0].mark != nil)
#expect(result.diagnostics[0].message.contains("unexpected end of input"))
}
@Test("A mismatched end tag is reported and the content is salvaged")
func test_mismatchedEndTag() throws {
let result = PureXML.read("x")
#expect(try result.node == PureXML.parse("x"))
#expect(result.diagnostics.count == 2)
#expect(result.diagnostics.allSatisfy { $0.message.contains("expected ") })
}
@Test("A duplicate attribute is dropped but the element is kept")
func test_duplicateAttributeKeepsElement() throws {
let result = PureXML.read("y")
#expect(try result.node == PureXML.parse("y"))
#expect(result.diagnostics.count == 1)
#expect(result.diagnostics[0].message.contains("duplicate attribute 'id'"))
}
@Test("A mismatched end tag pops to the matching ancestor, closing nested elements")
func test_popToMatch() throws {
let result = PureXML.read("x")
#expect(try result.node == PureXML.parse("x"))
#expect(result.diagnostics.count == 1)
#expect(result.diagnostics[0].message.contains("expected but found "))
}
@Test("Junk around the root is dropped and the element is kept")
func test_strayJunk() throws {
let result = PureXML.read("garbagexz")
#expect(try result.node == PureXML.parse("x"))
#expect(!result.diagnostics.isEmpty)
}
@Test("Empty input reads as an empty document without crashing")
func test_empty() {
let result = PureXML.read("")
#expect(result.node == .document([]))
#expect(result.diagnostics.isEmpty)
}
@Test("Reading is deterministic: the same bytes give the same result")
func test_deterministic() {
for xml in ["x", "<<>>", "z", "deep"] {
#expect(PureXML.read(xml) == PureXML.read(xml))
}
}
@Test("Arbitrary and truncated input never crashes and stays deterministic")
func test_fuzzNeverCrashes() {
// A seeded generator (no Date/random) makes the sweep reproducible.
var state: UInt64 = 0x9E37_79B9_7F4A_7C15
func nextByte() -> UInt8 {
state = state &* 6_364_136_223_846_793_005 &+ 1_442_695_040_888_963_407
return UInt8((state >> 33) & 0xFF)
}
let alphabet = Array("<>/&;\"'= \nabcxyz![]-?CDATA")
let seed = "text"
for iteration in 0 ..< 400 {
// Mix random characters with truncations of a valid document.
var input = ""
let length = Int(nextByte()) % 60
for _ in 0 ..< length {
input.append(alphabet[Int(nextByte()) % alphabet.count])
}
if iteration.isMultiple(of: 3) {
input += String(seed.prefix(Int(nextByte()) % seed.count + 1))
}
let first = PureXML.read(input)
let second = PureXML.read(input)
// Completing without trapping is the no-crash assertion; also check the
// result is a document and the read is deterministic.
guard case .document = first.node else {
Issue.record("read did not return a document for input \(input.debugDescription)")
continue
}
#expect(first == second)
}
}
}