PureXML/Tests/TreeNodeDepthSafetyTests.swift
Mihaela Mihaljevic a77ffbd952
perf(model): make the TreeNode tree type depth-safe (#343)
Building a TreeNode from a value Node, projecting it back via `node`, and
releasing it each recursed one native frame per tree level, so a deeply
nested document overflowed the call stack (signal 10 around 30-60k deep,
and far shallower on a constrained stack such as a Swift Task). None of
these is fixed by the serializer, which was already iterative.

- `TreeNode(_:)` and `node`/`projectedChildren` now drive explicit
  post-order work stacks (entity-reference splicing, doctype/namespace
  dropping, and the root special-cases are unchanged).
- `deinit` flattens the subtree into a heap worklist before releasing, so
  each node frees with empty children and the teardown stays flat. Only a
  node the teardown solely owns (isKnownUniquelyReferenced) is flattened,
  so a child still referenced elsewhere keeps its subtree.
- The validator walk and the XPath descendant axis were made iterative for
  the same reason.

A 60000-deep tree now releases in well under 0.1s; 1778 tests pass and no
accepted output changes. Part of #341; the value Node's own recursive
release and XPath result ordering/dedup remain depth-bounded and tracked
there.
2026-06-26 16:39:08 +02:00

60 lines
2.9 KiB
Swift

import Testing
@testable import PureXML
/// Depth safety for the mutable ``TreeNode`` tree type (#341): building a tree
/// from a value ``Node``, projecting it back, and releasing it must all run in
/// bounded native stack, so a deeply-nested document cannot overflow the call
/// stack through a recursive walk or a recursive chain of child releases.
///
/// The release test builds its tree bottom-up through the adopting initializer
/// rather than from a parsed value ``Node``: a value `Node` that deep still
/// releases recursively (its storage change is tracked separately in #341), so
/// using one as a fixture would itself overflow on teardown and mask what is
/// under test here.
@Suite("TreeNode depth safety")
struct TreeNodeDepthSafetyTests {
/// A chain of `depth` nested elements, built bottom-up so construction never
/// recurses and holds no deep value `Node`.
private func deepChain(_ depth: Int) -> PureXML.Model.TreeNode {
var node = PureXML.Model.TreeNode(adopting: .element, name: PureXML.Model.QualifiedName("a"), children: [])
for _ in 0 ..< depth {
node = PureXML.Model.TreeNode(adopting: .element, name: PureXML.Model.QualifiedName("a"), children: [node])
}
return node
}
@Test("releasing a 60k-deep tree does not overflow the stack")
func test_deepReleaseIsBounded() {
var root: PureXML.Model.TreeNode? = deepChain(60000)
root = nil
#expect(root == nil)
}
@Test("a held attached child keeps its subtree when the root is released")
func test_heldChildKeepsSubtreeOnRootRelease() {
let leaf = PureXML.Model.TreeNode.element("leaf")
let mid = PureXML.Model.TreeNode.element("mid", children: [leaf])
var root: PureXML.Model.TreeNode? = PureXML.Model.TreeNode.element("root", children: [mid])
root = nil
// The teardown only flattens nodes it solely owns; `mid` is still held, so
// its subtree must remain intact rather than be cleared.
#expect(mid.children.count == 1)
#expect(mid.children.first === leaf)
#expect(leaf.name?.description == "leaf")
}
@Test("Node to TreeNode and back round-trips through the iterative conversions")
func test_conversionRoundTrip() throws {
// Held at the default depth bound: a value `Node` this deep still releases
// recursively, and a Swift Task's stack (this test runs on one) overflows
// far shallower than the main thread, so a deeper fixture would crash on
// teardown for a reason unrelated to the conversions under test (#341).
let depth = 200
let xml = String(repeating: "<a x=\"1\">", count: depth) + "leaf" + String(repeating: "</a>", count: depth)
let node = try PureXML.parse(xml)
let tree = PureXML.Model.TreeNode(node)
let back = tree.node
#expect(PureXML.serialize(back, options: .compact) == PureXML.serialize(node, options: .compact))
}
}