PureXML/Tests/NamespaceScaleTests.swift
Mihaela Mihaljevic 3405c9709d
perf(parsing): resolve namespaces in O(1), making parse linear in depth (#346)
Each open element resolved its name and attributes by looking up namespace
bindings through NamespaceContext.lookup, which walked a stack of scopes as
deep as the document. With one lookup per element over an O(depth) stack,
parsing a deeply-nested document was O(depth^2): a 40000-deep document took
~39s.

The context now maintains the current in-scope bindings incrementally: a
single prefix-to-URI map, plus a per-element undo list recording the
bindings each element overwrote so closing it restores the enclosing state.
A lookup is O(1) and entering/leaving an element is O(its xmlns
declarations), so parsing is O(n). The same document now parses in ~0.2s.

Namespace semantics are unchanged: the nearest in-scope binding still wins,
an empty default binding still undeclares, and 81 namespace tests plus the
full suite (1784) pass. Closes #345.
2026-06-26 17:44:27 +02:00

44 lines
2.3 KiB
Swift

import Testing
@testable import PureXML
/// Namespace resolution must not make parsing quadratic in nesting depth (#345).
/// Each open element resolves its name against the in-scope bindings; doing that
/// by walking a scope stack as deep as the document made parsing O(depth^2). The
/// bindings are now maintained incrementally, so a lookup is O(1) and parsing is
/// linear. The time limit turns a regression back to quadratic into a failure
/// rather than a silent interactive hang: at this depth the old behaviour took
/// minutes, the linear one takes well under a second.
@Suite("Namespace resolution scale (#345)")
struct NamespaceScaleTests {
#if os(WASI)
@Test("a deeply-nested document parses in linear time")
#else
@Test("a deeply-nested document parses in linear time", .timeLimit(.minutes(1)))
#endif
func test_deepNestingParsesLinearly() throws {
let depth = 100_000
let xml = String(repeating: "<a x=\"1\">", count: depth) + "leaf" + String(repeating: "</a>", count: depth)
let node = try PureXML.parse(xml, limits: .init(maxDepth: depth + 1))
// The parse produced the leaf at the bottom of the nesting.
#expect(PureXML.serialize(node, options: .compact).contains("leaf"))
}
@Test("nested default-namespace bindings still resolve to the nearest in scope")
func test_nearestBindingWins() throws {
let xml = """
<a xmlns="urn:outer"><b xmlns="urn:inner"><c/></b><d/></a>
"""
let node = try PureXML.parse(xml)
guard case let .document(roots) = node, let root = roots.first(where: { $0.element != nil })?.element else {
Issue.record("expected a document with a root element")
return
}
#expect(root.name.namespaceURI == "urn:outer") // a
let inner = root.children.first(where: { $0.element?.name.localName == "b" })?.element
let outerSibling = root.children.first(where: { $0.element?.name.localName == "d" })?.element
#expect(inner?.name.namespaceURI == "urn:inner") // b: nearer default binding
#expect(inner?.children.first(where: { $0.element?.name.localName == "c" })?.element?.name.namespaceURI == "urn:inner") // c inherits inner
#expect(outerSibling?.name.namespaceURI == "urn:outer") // d: inner binding popped, back to outer
}
}