Skip to content

Commit 41e5159

Browse files
OguzYuukselOguz Yukselmbrandonw
authored
Handle circular reference in snap function (#942)
* handle circular references during reccursive child search * add tests for circular reference snapshotting as dump * fixes --------- Co-authored-by: Oguz Yuksel <[email protected]> Co-authored-by: Brandon Williams <[email protected]> Co-authored-by: Brandon Williams <[email protected]>
1 parent d69b3df commit 41e5159

File tree

5 files changed

+54
-9
lines changed

5 files changed

+54
-9
lines changed

Sources/SnapshotTesting/AssertSnapshot.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -502,6 +502,8 @@ func sanitizePathComponent(_ string: String) -> String {
502502
}
503503

504504
#if !os(Linux) && !os(Windows)
505+
import CoreServices
506+
505507
func uniformTypeIdentifier(fromExtension pathExtension: String) -> String? {
506508
// This can be much cleaner in macOS 11+ using UTType
507509
let unmanagedString = UTTypeCreatePreferredIdentifierForTag(

Sources/SnapshotTesting/Snapshotting/Any.swift

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -66,23 +66,29 @@ extension Snapshotting where Format == String {
6666
}
6767
}
6868

69-
private func snap<T>(_ value: T, name: String? = nil, indent: Int = 0) -> String {
69+
private func snap<T>(
70+
_ value: T,
71+
name: String? = nil,
72+
indent: Int = 0,
73+
visitedValues: Set<ObjectIdentifier> = .init()
74+
) -> String {
7075
let indentation = String(repeating: " ", count: indent)
7176
let mirror = Mirror(reflecting: value)
7277
var children = mirror.children
7378
let count = children.count
7479
let bullet = count == 0 ? "-" : ""
80+
var visitedValues = visitedValues
7581

7682
let description: String
7783
switch (value, mirror.displayStyle) {
7884
case (_, .collection?):
7985
description = count == 1 ? "1 element" : "\(count) elements"
8086
case (_, .dictionary?):
8187
description = count == 1 ? "1 key/value pair" : "\(count) key/value pairs"
82-
children = sort(children)
88+
children = sort(children, visitedValues: visitedValues)
8389
case (_, .set?):
8490
description = count == 1 ? "1 member" : "\(count) members"
85-
children = sort(children)
91+
children = sort(children, visitedValues: visitedValues)
8692
case (_, .tuple?):
8793
description = count == 1 ? "(1 element)" : "(\(count) elements)"
8894
case (_, .optional?):
@@ -95,10 +101,19 @@ private func snap<T>(_ value: T, name: String? = nil, indent: Int = 0) -> String
95101
return "\(indentation)- \(name.map { "\($0): " } ?? "")\(value.snapshotDescription)\n"
96102
case (let value as CustomStringConvertible, _):
97103
description = value.description
98-
case (_, .class?), (_, .struct?):
104+
case let (value as AnyObject, .class?):
105+
let objectID = ObjectIdentifier(value)
106+
if visitedValues.contains(objectID) {
107+
return "\(indentation)\(bullet) \(name ?? "value") (circular reference detected)\n"
108+
}
109+
visitedValues.insert(objectID)
110+
description = String(describing: mirror.subjectType)
111+
.replacingOccurrences(of: " #\\d+", with: "", options: .regularExpression)
112+
children = sort(children, visitedValues: visitedValues)
113+
case (_, .struct?):
99114
description = String(describing: mirror.subjectType)
100115
.replacingOccurrences(of: " #\\d+", with: "", options: .regularExpression)
101-
children = sort(children)
116+
children = sort(children, visitedValues: visitedValues)
102117
case (_, .enum?):
103118
let subjectType = String(describing: mirror.subjectType)
104119
.replacingOccurrences(of: " #\\d+", with: "", options: .regularExpression)
@@ -109,15 +124,15 @@ private func snap<T>(_ value: T, name: String? = nil, indent: Int = 0) -> String
109124

110125
let lines =
111126
["\(indentation)\(bullet) \(name.map { "\($0): " } ?? "")\(description)\n"]
112-
+ children.map { snap($1, name: $0, indent: indent + 2) }
127+
+ children.map { snap($1, name: $0, indent: indent + 2, visitedValues: visitedValues) }
113128

114129
return lines.joined()
115130
}
116131

117-
private func sort(_ children: Mirror.Children) -> Mirror.Children {
132+
private func sort(_ children: Mirror.Children, visitedValues: Set<ObjectIdentifier>) -> Mirror.Children {
118133
return .init(
119134
children
120-
.map({ (child: $0, snap: snap($0)) })
135+
.map({ (child: $0, snap: snap($0, visitedValues: visitedValues)) })
121136
.sorted(by: { $0.snap < $1.snap })
122137
.map({ $0.child })
123138
)

Tests/SnapshotTestingTests/SnapshotTestingTests.swift

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import XCTest
1414
import SwiftUI
1515
#endif
1616
#if canImport(WebKit)
17-
import WebKit
17+
@preconcurrency import WebKit
1818
#endif
1919
#if canImport(UIKit)
2020
import UIKit.UIView
@@ -36,6 +36,26 @@ final class SnapshotTestingTests: XCTestCase {
3636
assertSnapshot(of: user, as: .dump)
3737
}
3838

39+
func testRecursion() {
40+
withSnapshotTesting {
41+
class Father {
42+
var child: Child?
43+
init(_ child: Child? = nil) { self.child = child }
44+
}
45+
class Child {
46+
let father: Father
47+
init(_ father: Father) {
48+
self.father = father
49+
father.child = self
50+
}
51+
}
52+
let father = Father()
53+
let child = Child(father)
54+
assertSnapshot(of: father, as: .dump)
55+
assertSnapshot(of: child, as: .dump)
56+
}
57+
}
58+
3959
@available(macOS 10.13, tvOS 11.0, *)
4060
func testAnyAsJson() throws {
4161
struct User: Encodable { let id: Int, name: String, bio: String }
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
▿ Father
2+
▿ child: Optional<Child>
3+
▿ some: Child
4+
▿ father (circular reference detected)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
▿ Child
2+
▿ father: Father
3+
▿ child: Optional<Child>
4+
▿ some (circular reference detected)

0 commit comments

Comments
 (0)