Skip to content

A cyclic graph of 3 or more items causes an infinite recursion #785

Closed
@schlossm

Description

@schlossm

Description

Initializing an argument that contains 3 or more cyclical classes causes an infinite recursion within __Expression.Value's private init:

private init(
        _reflecting subject: Any,
        label: String?,
        seenObjects: inout [ObjectIdentifier: AnyObject]
    )

I tracked it down to the attempt to prevent cyclical dependencies. Using the sample code provided in Steps to Reproduce, I was able to maybe fix this by removing the defer and adding a filter onto the children:

    private init(
        _reflecting subject: Any,
        label: String?,
        seenObjects: inout [ObjectIdentifier: AnyObject]
    ) {
        ...

        if shouldIncludeChildren, !mirror.children.isEmpty || isCollection {
            children = mirror.children.**filter { child in
                !seenObjects.contains(ObjectIdentifier(child as AnyObject))
            }**.map { child in
                Self(_reflecting: child.value, label: child.label, seenObjects: &seenObjects)
            }
        }
    }

But I'm unsure if this has any unintended side effects (specifically, the comment callout about multiple references to the same object in the same subject). Logging a Value(reflecting: a) in the makeA() method returns me a Value instance that looks to properly stop traversing future references of the same type after it traverses first one.

Expected behavior

Cyclical graphs of any complexity should resolve their reflection correctly

Actual behavior

Once 3 classes are introduced into the graph, the initializer enters an infinite loop

Steps to reproduce

  1. Attempt to run the following Test:
class A {
    private var c: C!
    private var c2: C!
    private var b: B!

    func setup(b: B, c: C) {
        self.b = b
        self.c = c
        c2 = c
    }
}

class B {
    private var a: A!
    private var c: C!

    func setup(a: A, c: C) {
        self.a = a
        self.c = c
    }
}

class C {
    private var a: A!
    private var b: B!

    func setup(a: A, b: B) {
        self.a = a
        self.b = b
    }
}

@Suite
struct TestingAppTests {
    @Test(arguments: [makeA()]) func example(foo _: A) async throws {
        // Write your test here and use APIs like `#expect(...)` to check expected conditions.
        #expect(true)
    }

    static func makeA() -> A {
        let a = A()
        let b = B()
        let c = C()

        a.setup(b: b, c: c)
        b.setup(a: a, c: c)
        c.setup(a: a, b: b)

        return a
    }
}
  1. Observe the process crashes after the return of makeA() and before the main test begins

swift-testing version/commit hash

Xcode's version of swift-testing

Swift & OS version (output of swift --version ; uname -a)

swift-driver version: 1.115 Apple Swift version 6.0.2 (swiftlang-6.0.2.1.2 clang-1600.0.26.4)
Target: arm64-apple-macosx15.0
Darwin .local 24.2.0 Darwin Kernel Version 24.2.0: Tue Oct 15 18:15:36 PDT 2024; root:xnu-11215.60.364.501.5~3/RELEASE_ARM64_T6000 arm64

Metadata

Metadata

Assignees

Labels

bug🪲 Something isn't workingissue-handlingRelated to Issue handling within the testing library

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions