Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
schlossm opened this issue Oct 26, 2024 · 4 comments · Fixed by #787
Closed

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

schlossm opened this issue Oct 26, 2024 · 4 comments · Fixed by #787
Assignees
Labels
bug 🪲 Something isn't working issue-handling Related to Issue handling within the testing library
Milestone

Comments

@schlossm
Copy link

schlossm commented Oct 26, 2024

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

@schlossm schlossm added the bug 🪲 Something isn't working label Oct 26, 2024
@grynspan
Copy link
Contributor

@stmontgomery, I think you may have a radar tracking this internally too?

@emilykfox
Copy link

emilykfox commented Oct 27, 2024

I believe I ran into the same bug, except it happens when attempting to describe the objects involved in a failed #expect. Here's a small code sample to reproduce the issue. The first call to #expect just prints the type of object twice as should happen. The second call after the additional self-reference causes infinite recursion and a crash.

import Testing

class SelfReferential {
  var optionalRefA: SelfReferential? = nil
  var optionalRefB: SelfReferential? = nil
}

@Test func crasher() {
  let object = SelfReferential()

  object.optionalRefA = object
  #expect(object !== object)

  object.optionalRefB = object
  #expect(object !== object)
}

Swift and OS Version

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 Emilys-MacBook-Pro-3.local 24.1.0 Darwin Kernel Version 24.1.0: Thu Oct 10 21:03:11 PDT 2024; root:xnu-11215.41.3~2/RELEASE_ARM64_T6020 arm64

@stmontgomery
Copy link
Contributor

@stmontgomery, I think you may have a radar tracking this internally too?

I don't believe we have a Radar for this particular symptom (a crash due to infinite recursion), but I have a fix nearly ready.

@stmontgomery stmontgomery added this to the Swift 6.1 milestone Oct 28, 2024
@grynspan grynspan added the issue-handling Related to Issue handling within the testing library label Oct 28, 2024
stmontgomery added a commit to stmontgomery/swift-testing that referenced this issue Oct 28, 2024
@stmontgomery
Copy link
Contributor

Tracked internally as rdar://138708651

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug 🪲 Something isn't working issue-handling Related to Issue handling within the testing library
Projects
None yet
4 participants