PureXML/Tests/ByteFastPathTests.swift
Mihaela Mihaljevic 380160cc2b refactor(validation): decompose XSD instance validation into composable rules
The two monolithic XSD content/identity rules only satisfied the OpenAPIKit
validation idiom at the wrong granularity: no per-constraint removability,
no isolable failing/near-miss tests. Split them into 32 named rules, each
finding tagged at its origin and cached per XSDContext so the walk still
runs once per document (the #101 decision against duplicating XSD's
recursive type resolution still holds). Wired the streaming path's ID/IDREF
and identity checks through the same framework instead of calling the
validators imperatively, and added the unwrapAndLookup combinator the rule
already named as required.

Also includes the mandated repo-wide swiftformat/swiftlint pass.
2026-07-12 14:12:17 +02:00

160 lines
6.8 KiB
Swift

import Testing
@testable import PureXML
/// Locks the parser's byte-level fast paths against their Character-path
/// fallbacks. Each case is chosen to land exactly on a boundary where the byte
/// scanner must hand off to the scalar scanner (a non-ASCII byte, a carriage
/// return, an entity reference) so that a future change to either path cannot
/// silently diverge. The expected values are the spec-correct results,
/// independent of which path produced them.
@Suite("Byte fast-path boundaries")
struct ByteFastPathTests {
@Test("An all-ASCII name uses the byte path; a non-ASCII name falls back")
func test_nameFallback() throws {
let node = try PureXML.parse("<caf\u{e9} na\u{ef}ve=\"x\">y</caf\u{e9}>")
let element = node.firstElement
#expect(element?.name.localName == "caf\u{e9}")
#expect(element?.attributes == [.init("na\u{ef}ve", "x")])
#expect(element?.text == "y")
}
@Test("A name whose ASCII prefix precedes a non-ASCII byte stays whole")
func test_mixedNameStaysWhole() throws {
// The byte scanner consumes "ab", meets the non-ASCII byte, returns nil,
// and the Character path re-scans the entire "ab\u{e9}" as one name.
let node = try PureXML.parse("<ab\u{e9}/>")
#expect(node.firstElement?.name.localName == "ab\u{e9}")
}
@Test("A prefixed name splits at the colon on the byte path")
func test_prefixedName() throws {
let node = try PureXML.parse("<m:row xmlns:m=\"urn:x\" m:k=\"v\"/>")
let element = node.firstElement
#expect(element?.name.prefix == "m")
#expect(element?.name.localName == "row")
#expect(element?.attributes.contains { $0.name.localName == "k" && $0.value == "v" } == true)
}
@Test("Literal whitespace in an attribute value normalizes to single spaces")
func test_attributeWhitespaceNormalizes() throws {
// Tab and line feed each become one space per 3.3.3; these run through
// the byte scanner's whitespace transform.
let node = try PureXML.parse("<a v=\"x\ty\nz\"/>")
#expect(node.firstElement?.attributes == [.init("v", "x y z")])
}
@Test("A CRLF in an attribute value collapses to a single space")
func test_attributeCRLF() throws {
// 2.11 folds CRLF to one LF before 3.3.3 turns it into one space. The
// byte scanner defers any carriage return to the Character path so the
// pair is not double-counted as two spaces.
let node = try PureXML.parse("<a v=\"x\r\ny\"/>")
#expect(node.firstElement?.attributes == [.init("v", "x y")])
}
@Test("A character reference in an attribute value survives normalization")
func test_attributeCharReferenceSurvives() throws {
// A referenced line feed is NOT subject to whitespace normalization: it
// stays a line feed while a literal line feed would have become a space.
let node = try PureXML.parse("<a v=\"x&#10;y\"/>")
#expect(node.firstElement?.attributes == [.init("v", "x\ny")])
}
@Test("Entities and bare ampersands in text decode through the slow path")
func test_textEntities() throws {
let node = try PureXML.parse("<p>plain text then 1 &lt; 2 &amp; ok</p>")
#expect(node.firstElement?.text == "plain text then 1 < 2 & ok")
}
@Test("Ampersand-free text is returned verbatim")
func test_textWithoutEntities() throws {
let node = try PureXML.parse("<p>just plain ascii content, no references</p>")
#expect(node.firstElement?.text == "just plain ascii content, no references")
}
@Test("A CRLF inside text content folds to a single line feed")
func test_textCRLF() throws {
let node = try PureXML.parse("<p>line one\r\nline two</p>")
#expect(node.firstElement?.text == "line one\nline two")
}
@Test("Non-ASCII text content round-trips unchanged")
func test_nonASCIIText() throws {
let node = try PureXML.parse("<p>caf\u{e9} na\u{ef}ve r\u{e9}sum\u{e9}</p>")
#expect(node.firstElement?.text == "caf\u{e9} na\u{ef}ve r\u{e9}sum\u{e9}")
}
@Test("An over-long ASCII name is rejected, not silently truncated")
func test_longNameRejected() {
let limits = PureXML.Parsing.Limits(maxNameLength: 4)
#expect(throws: PureXML.Parsing.ParseError.self) {
try PureXML.parse("<abcdefgh/>", limits: limits)
}
}
@Test("A raw less-than inside an attribute value is rejected")
func test_rawLessThanInAttribute() {
#expect(throws: PureXML.Parsing.ParseError.self) {
try PureXML.parse("<a v=\"x<y\"/>")
}
}
@Test("An empty attribute value parses on the byte fast path")
func test_emptyAttributeValue() throws {
// attributeRunBytes returns nil immediately (empty run), so the
// closing-quote check must fire on the very first byte peek.
let node = try PureXML.parse("<a v=\"\"/>")
#expect(node.firstElement?.attributes == [.init("v", "")])
}
@Test("A single-quoted attribute value uses the byte fast path")
func test_singleQuotedAttributeValue() throws {
let node = try PureXML.parse("<a v='x'/>")
#expect(node.firstElement?.attributes == [.init("v", "x")])
}
@Test("A double quote is legal content inside a single-quoted value")
func test_doubleQuoteInsideSingleQuotedValue() throws {
let node = try PureXML.parse("<a v='it\"s'/>")
#expect(node.firstElement?.attributes == [.init("v", "it\"s")])
}
@Test("A single quote is legal content inside a double-quoted value")
func test_singleQuoteInsideDoubleQuotedValue() throws {
let node = try PureXML.parse("<a v=\"it's\"/>")
#expect(node.firstElement?.attributes == [.init("v", "it's")])
}
@Test("An attribute value unterminated at end of input is rejected")
func test_unterminatedAttributeValueAtEndOfInput() {
#expect(throws: PureXML.Parsing.ParseError.self) {
try PureXML.parse("<a v=\"x")
}
}
@Test("An attribute value that ends exactly at end of input with no closing quote is rejected")
func test_attributeValueMissingClosingQuoteAtVeryEnd() {
#expect(throws: PureXML.Parsing.ParseError.self) {
try PureXML.parse("<a v=\"")
}
}
@Test("A leading greater-than in text content is preserved")
func test_textLeadingGreaterThan() throws {
let node = try PureXML.parse("<p>a > b</p>")
#expect(node.firstElement?.text == "a > b")
}
}
private extension PureXML.Model.Node {
var firstElement: PureXML.Model.Element? {
guard case let .document(children) = self else { return element }
for child in children {
if case let .element(element) = child {
return element
}
}
return nil
}
}