diff --git a/Sources/Testing/SourceAttribution/Expression.swift b/Sources/Testing/SourceAttribution/Expression.swift index 01cd69fb5..3f6a7e716 100644 --- a/Sources/Testing/SourceAttribution/Expression.swift +++ b/Sources/Testing/SourceAttribution/Expression.swift @@ -190,9 +190,9 @@ public struct __Expression: Sendable { /// This is used to halt further recursion if a previously-seen object /// is encountered again. private init( - _reflecting subject: Any, - label: String?, - seenObjects: inout [ObjectIdentifier: AnyObject] + _reflecting subject: Any, + label: String?, + seenObjects: inout [ObjectIdentifier: AnyObject] ) { let mirror = Mirror(reflecting: subject) @@ -214,7 +214,7 @@ public struct __Expression: Sendable { // `type(of:)`, which is inexpensive. The object itself is stored as the // value in the dictionary to ensure it is retained for the duration of // the recursion. - var objectIdentifierTeRemove: ObjectIdentifier? + var objectIdentifierToRemove: ObjectIdentifier? var shouldIncludeChildren = true if mirror.displayStyle == .class, type(of: subject) is AnyObject.Type { let object = subject as AnyObject @@ -222,16 +222,17 @@ public struct __Expression: Sendable { let oldValue = seenObjects.updateValue(object, forKey: objectIdentifier) if oldValue != nil { shouldIncludeChildren = false + } else { + objectIdentifierToRemove = objectIdentifier } - objectIdentifierTeRemove = objectIdentifier } defer { - if let objectIdentifierTeRemove { + if let objectIdentifierToRemove { // Remove the object from the set of previously-seen objects after // (potentially) recursing to reflect children. This is so that // repeated references to the same object are still included multiple // times; only _cyclic_ object references should be avoided. - seenObjects[objectIdentifierTeRemove] = nil + seenObjects[objectIdentifierToRemove] = nil } } diff --git a/Tests/TestingTests/Expression.ValueTests.swift b/Tests/TestingTests/Expression.ValueTests.swift index e7a7b4c37..c9929f59c 100644 --- a/Tests/TestingTests/Expression.ValueTests.swift +++ b/Tests/TestingTests/Expression.ValueTests.swift @@ -166,4 +166,50 @@ struct Expression_ValueTests { #expect(oneChildChildrenOptionalChild.children == nil) } + @Test("Value reflecting an object with two back-references to itself", + .bug("https://github.com/swiftlang/swift-testing/issues/785#issuecomment-2440222995")) + func multipleSelfReferences() { + class A { + weak var one: A? + weak var two: A? + } + + let a = A() + a.one = a + a.two = a + + let value = Expression.Value(reflecting: a) + #expect(value.children?.count == 2) + } + + @Test("Value reflecting an object in a complex graph which includes back-references", + .bug("https://github.com/swiftlang/swift-testing/issues/785")) + func complexObjectGraphWithCyclicReferences() throws { + class A { + var c1: C! + var c2: C! + var b: B! + } + class B { + weak var a: A! + var c: C! + } + class C { + weak var a: A! + } + + let a = A() + let b = B() + let c = C() + a.c1 = c + a.c2 = c + a.b = b + b.a = a + b.c = c + c.a = a + + let value = Expression.Value(reflecting: a) + #expect(value.children?.count == 3) + } + }