swiftlang / swift-testing

A modern, expressive testing package for Swift
Apache License 2.0
1.77k stars 73 forks source link

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

Open schlossm opened 6 hours ago

schlossm commented 6 hours ago

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
}

}


2. 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 <private>.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
grynspan commented 5 hours ago

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