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.
60 lines
2.9 KiB
Swift
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))
|
|
}
|
|
}
|