import Testing @testable import PureXML /// A differential oracle for the parser's byte-level fast paths. The byte path /// (a reader built from a `String`, which owns its UTF-8 and scans straight off /// the bytes) and the streaming Character path (a reader built from a pulling /// closure, whose `storage` is nil so every byte method returns nil) are two /// implementations of the same grammar. They must therefore emit the identical /// event stream AND, on malformed input, fail at the identical source mark, for /// every input. Any divergence is a fast-path bug. The corpus is chosen to land /// on the boundaries where the two paths differ in mechanism: carriage-return /// folding, attribute-value normalization, multi-line position tracking, the /// `]]>` guard, name limits, and the ASCII/non-ASCII handoff. @Suite("Parser byte/Character path differential") struct ParserPathDifferentialTests { private func drain(_ reader: inout PureXML.Parsing.EventReader) -> String { var out = "" do { while let event = try reader.next() { let mark = reader.eventStart out += "\(event)@\(mark.line):\(mark.column):\(mark.offset)|" } out += "EOF" } catch { out += "ERR:\(error)" } return out } private func bytePath(_ input: String, _ limits: PureXML.Parsing.Limits) -> String { var reader = PureXML.Parsing.EventReader(input, limits: limits) return drain(&reader) } private func characterPath(_ input: String, _ limits: PureXML.Parsing.Limits) -> String { var iterator = input.makeIterator() var reader = PureXML.Parsing.EventReader(pulling: { iterator.next() }, limits: limits) return drain(&reader) } private func expectAgreement(_ label: String, _ input: String, limits: PureXML.Parsing.Limits = .default) { let viaBytes = bytePath(input, limits) let viaCharacters = characterPath(input, limits) #expect( viaBytes == viaCharacters, "byte and Character paths diverged for \(label) on \(input.debugDescription)\n bytes: \(viaBytes)\n chars: \(viaCharacters)", ) } @Test("Carriage-return and line-feed folding in content agree on both paths") func test_contentLineEndings() { expectAgreement("lone CR", "x\ry") expectAgreement("CRLF", "x\r\ny") expectAgreement("CR then CRLF", "x\r\r\ny") expectAgreement("only CR", "\r") expectAgreement("CR then element", "x\r") expectAgreement("leading CRs then content and element", "\r\r\rxy") expectAgreement("multi-line then element mark", "line1\nline2\nline3") expectAgreement("blank lines then element mark", "\n\n\n") } @Test("Attribute-value normalization agrees on both paths") func test_attributeNormalization() { expectAgreement("lone CR attr", "") expectAgreement("only CR attr", "") expectAgreement("CR then CRLF attr", "") expectAgreement("two lone CR attr", "") expectAgreement("mixed whitespace attr", "") expectAgreement("char-ref CR survives", "") expectAgreement("char-ref LF survives", "") expectAgreement("literal tab plus ref tab", "") expectAgreement("multi-line attr then attr", "") expectAgreement("empty value", "") expectAgreement("single-quoted value", "") expectAgreement("double quote inside single-quoted value", "") expectAgreement("single quote inside double-quoted value", "") expectAgreement("empty then non-empty value", "") expectAgreement("unterminated value at end of input", "]]") expectAgreement("]]> error mark", "x]]>y") expectAgreement("] then ]]>", "]x]]>y") expectAgreement("triple ] gt", "]]]>x") expectAgreement("scattered brackets", "a]b]c") expectAgreement("newline then ]]>", "line1\n]]>x") expectAgreement("leading greater-than", "
a > b
") } @Test("Name boundaries and the ASCII/non-ASCII handoff agree on both paths") func test_nameBoundaries() { let short = PureXML.Parsing.Limits(maxNameLength: 3) expectAgreement("over-long name", "1 < 2 & ok
") expectAgreement("undefined entity mark", "&unknown;") expectAgreement("entity after run", "text & more &bad;") expectAgreement("ampersand truncation after newlines", "\n\nbad &x") expectAgreement("whitespace-only content", " ") expectAgreement("tabs then element", "\t\t") } @Test("Attribute-value ampersand handling agrees on both paths") func test_attributeAmpersands() { // The byte path skips reference decoding when no '&' is seen; the runs // and the Character fallback must still agree on the decoded value. expectAgreement("ampersand-free attr", #""#) expectAgreement("entity in attr", #""#) expectAgreement("char ref in attr", #""#) expectAgreement("ampersand-free with whitespace", "") expectAgreement("entity after whitespace run", "") expectAgreement("undefined entity in attr", #""#) } }