Skip to content

Commit f4e6a96

Browse files
committed
[PlaygroundLogger] Implemented support for capping the depth of log entries.
This pulls over behavior matching the legacy PlaygroundLogger, which capped log entries at a maximum depth of 2. This includes commits for a small handful of cases. More to be added later.
1 parent 14243c5 commit f4e6a96

File tree

3 files changed

+280
-16
lines changed

3 files changed

+280
-16
lines changed

PlaygroundLogger/PlaygroundLogger/LogEntry+Reflection.swift

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,16 @@ fileprivate let emptyNameString = ""
2121

2222
extension LogEntry {
2323
init(describing instance: Any, name: String? = nil, policy: LogPolicy) {
24-
self = .init(describing: instance, name: name ?? emptyNameString, typeName: nil, summary: nil, policy: policy)
24+
self = .init(describing: instance, name: name ?? emptyNameString, typeName: nil, summary: nil, policy: policy, currentDepth: 0)
2525
}
2626

27-
private init(describing instance: Any, name: String, typeName passedInTypeName: String?, summary passedInSummary: String?, policy: LogPolicy) {
28-
// TODO: need to handle optionals better (e.g. implicitly unwrap optionality, I think)
29-
27+
fileprivate init(describing instance: Any, name: String, typeName passedInTypeName: String?, summary passedInSummary: String?, policy: LogPolicy, currentDepth: Int) {
28+
guard currentDepth <= policy.maximumDepth else {
29+
// We're trying to log an instance that is "too deep"; as a result, we need to just return a gap.
30+
self = .gap
31+
return
32+
}
33+
3034
// Returns either the passed-in type name/summary or the type name/summary of `instance`.
3135
var typeName: String { return passedInTypeName ?? normalizedName(of: type(of: instance)) }
3236
var summary: String { return passedInSummary ?? String(describing: instance) }
@@ -47,7 +51,7 @@ extension LogEntry {
4751

4852
// If a type implements the `debugQuickLookObject()` Objective-C method, then get their debug quick look object and use that for logging (by passing it back through this initializer).
4953
else if let debugQuickLookObjectMethod = (instance as AnyObject).debugQuickLookObject, let debugQuickLookObject = debugQuickLookObjectMethod() {
50-
self = .init(describing: debugQuickLookObject, name: name, typeName: typeName, summary: nil, policy: policy)
54+
self = .init(describing: debugQuickLookObject, name: name, typeName: typeName, summary: nil, policy: policy, currentDepth: currentDepth)
5155
}
5256

5357
// Otherwise, first check if this is an interesting CF type before logging structure.
@@ -68,11 +72,11 @@ extension LogEntry {
6872

6973
if mirror.displayStyle == .optional && mirror.children.count == 1 {
7074
// If the mirror displays as an Optional and has exactly one child, then we want to unwrap the optionality and generate a log entry for the child.
71-
self = .init(describing: mirror.children.first!.value, name: name, typeName: typeName, summary: nil, policy: policy)
75+
self = .init(describing: mirror.children.first!.value, name: name, typeName: typeName, summary: nil, policy: policy, currentDepth: currentDepth)
7276
}
7377
else {
7478
// Otherwise, we want to generate a log entry with the structure from the mirror.
75-
self = .init(structureFrom: mirror, name: name, typeName: typeName, summary: summary, policy: policy)
79+
self = .init(structureFrom: mirror, name: name, typeName: typeName, summary: summary, policy: policy, currentDepth: currentDepth)
7680
}
7781
}
7882
}
@@ -85,12 +89,12 @@ extension LogEntry {
8589

8690
fileprivate static let superclassLogEntryName = "super"
8791

88-
fileprivate init(structureFrom mirror: Mirror, name: String, typeName: String, summary: String, policy: LogPolicy) {
92+
fileprivate init(structureFrom mirror: Mirror, name: String, typeName: String, summary: String, policy: LogPolicy, currentDepth: Int) {
8993
self = .structured(name: name,
9094
typeName: typeName,
9195
summary: summary,
9296
totalChildrenCount: mirror.totalChildCount,
93-
children: mirror.childEntries(using: policy),
97+
children: mirror.childEntries(using: policy, currentDepth: currentDepth),
9498
disposition: .init(displayStyle: mirror.displayStyle)
9599
)
96100
}
@@ -134,7 +138,7 @@ extension Mirror {
134138
}
135139
}
136140

137-
fileprivate func childEntries(using policy: LogPolicy) -> [LogEntry] {
141+
fileprivate func childEntries(using policy: LogPolicy, currentDepth: Int) -> [LogEntry] {
138142
let childPolicy: LogPolicy.ChildPolicy = {
139143
switch self.displayStyle ?? .struct {
140144
case .class, .struct, .tuple, .enum:
@@ -144,14 +148,26 @@ extension Mirror {
144148
}
145149
}()
146150

151+
let childDepth: Int = {
152+
switch self.displayStyle ?? .struct {
153+
case .optional, .dictionary:
154+
// We don't consume a level of depth for optionals or dictionaries.
155+
// We don't want optional to count as a level of depth as we would quickly end up with gaps.
156+
// We don't want dictionary to count as a level of depth as dictionary is modeled as a collection of (key, value) pairs, and we don't want to lose a level due to the pairs themselves consuming a level, so for ease of bookkeeping the dictionary level is counted as not consuming a level.
157+
return currentDepth
158+
case .class, .struct, .tuple, .enum, .collection, .set:
159+
return currentDepth + 1
160+
}
161+
}()
162+
147163
func logEntry(forChild child: Mirror.Child) -> LogEntry {
148-
return LogEntry(describing: child.value, name: child.label, policy: policy)
164+
return LogEntry(describing: child.value, name: child.label ?? emptyNameString, typeName: nil, summary: nil, policy: policy, currentDepth: childDepth)
149165
}
150166

151167
func logEntriesForAllChildren() -> [LogEntry] {
152168
let childEntries = children.map(logEntry(forChild:))
153169
if let superclassMirror = superclassMirror {
154-
return [superclassMirror.logEntry(named: LogEntry.superclassLogEntryName, usingPolicy: policy)] + childEntries
170+
return [superclassMirror.logEntry(named: LogEntry.superclassLogEntryName, usingPolicy: policy, depth: childDepth)] + childEntries
155171
}
156172
else {
157173
return childEntries
@@ -162,7 +178,7 @@ extension Mirror {
162178
let numberOfChildren: Int
163179
let superclassEntries: [LogEntry]
164180
if let superclassMirror = superclassMirror {
165-
superclassEntries = [superclassMirror.logEntry(named: LogEntry.superclassLogEntryName, usingPolicy: policy)]
181+
superclassEntries = [superclassMirror.logEntry(named: LogEntry.superclassLogEntryName, usingPolicy: policy, depth: childDepth)]
166182
numberOfChildren = count - 1
167183
}
168184
else {
@@ -183,6 +199,12 @@ extension Mirror {
183199
return children[start..<max].map(logEntry(forChild:))
184200
}
185201

202+
// Ensure that our children are loggable (i.e. their depth is not prohibited by our current policy).
203+
// If our children **are** too deep, then simply return a single gap as our children.
204+
guard childDepth <= policy.maximumDepth else {
205+
return [.gap]
206+
}
207+
186208
switch childPolicy {
187209
case .all:
188210
return logEntriesForAllChildren()
@@ -203,8 +225,8 @@ extension Mirror {
203225
}
204226
}
205227

206-
fileprivate func logEntry(named name: String, usingPolicy policy: LogPolicy) -> LogEntry {
228+
fileprivate func logEntry(named name: String, usingPolicy policy: LogPolicy, depth: Int) -> LogEntry {
207229
let subjectTypeName = normalizedName(of: self.subjectType)
208-
return LogEntry(structureFrom: self, name: name, typeName: subjectTypeName, summary: subjectTypeName, policy: policy)
230+
return LogEntry(structureFrom: self, name: name, typeName: subjectTypeName, summary: subjectTypeName, policy: policy, currentDepth: depth)
209231
}
210232
}

PlaygroundLogger/PlaygroundLogger/LogPolicy.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
struct LogPolicy {
1414
static let `default`: LogPolicy = LogPolicy()
1515

16+
/// The policy for the maximum depth level for log entries.
17+
var maximumDepth: Int
18+
1619
enum ChildPolicy {
1720
/// Indicates that all children should be logged.
1821
case all
@@ -32,10 +35,13 @@ struct LogPolicy {
3235

3336
/// Initializes a new `LogPolicy`.
3437
///
38+
/// - parameter maximumDepth: The maximum depth level for logging children of children. Defaults to 2.
3539
/// - parameter aggregateChildPolicy: The policy to use for logging children of aggregates. Defaults to logging no more than the first 10,000 children.
3640
/// - parameter containerChildPolicy: The policy to use for logging children of collections. Defaults to logging no more than the first 80 children plus the last 20 children.
37-
init(aggregateChildPolicy: ChildPolicy = .head(count: 10_000),
41+
init(maximumDepth: Int = 2,
42+
aggregateChildPolicy: ChildPolicy = .head(count: 10_000),
3843
containerChildPolicy: ChildPolicy = .headTail(headCount: 80, tailCount: 20)) {
44+
self.maximumDepth = maximumDepth
3945
self.aggregateChildPolicy = aggregateChildPolicy
4046
self.containerChildPolicy = containerChildPolicy
4147
}

PlaygroundLogger/PlaygroundLoggerTests/LogPolicyTests.swift

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,243 @@ fileprivate struct TestStruct {
2323
init() {}
2424
}
2525

26+
fileprivate class TestClass {
27+
let a: Int = 1
28+
init() {}
29+
}
30+
31+
fileprivate class TestSubclass: TestClass {
32+
let b: Int = 2
33+
override init() { super.init() }
34+
}
35+
36+
fileprivate class TestSubsubclass: TestSubclass {
37+
let c: Int = 3
38+
override init() { super.init() }
39+
}
40+
2641
class LogPolicyTests: XCTestCase {
42+
func testMaximumDepthLimitZero() {
43+
let testPolicy = LogPolicy(maximumDepth: 0)
44+
45+
let logEntry = LogEntry(describing: TestStruct(), name: "testStruct", policy: testPolicy)
46+
47+
guard case let .structured(name, _, _, totalChildrenCount, children, disposition) = logEntry else {
48+
XCTFail("Expected a structured log entry for a struct")
49+
return
50+
}
51+
52+
XCTAssertEqual(name, "testStruct")
53+
XCTAssertEqual(disposition, .struct)
54+
XCTAssertEqual(totalChildrenCount, 5)
55+
56+
guard children.count == 1 else {
57+
XCTFail("Expected the struct to have exactly one child")
58+
return
59+
}
60+
61+
guard case .gap = children[0] else {
62+
XCTFail("Expected the struct's only child to be a gap")
63+
return
64+
}
65+
}
66+
67+
func testMaximumDepthLimitOne() {
68+
let testPolicy = LogPolicy(maximumDepth: 1)
69+
70+
let logEntry = LogEntry(describing: TestStruct(), name: "testStruct", policy: testPolicy)
71+
72+
guard case let .structured(name, _, _, totalChildrenCount, children, disposition) = logEntry else {
73+
XCTFail("Expected a structured log entry for a struct")
74+
return
75+
}
76+
77+
XCTAssertEqual(name, "testStruct")
78+
XCTAssertEqual(disposition, .struct)
79+
XCTAssertEqual(totalChildrenCount, 5)
80+
XCTAssertEqual(children.count, 5)
81+
82+
for (index, child) in children.enumerated() {
83+
guard case let .opaque(_, typeName, _, _, representation) = child else {
84+
XCTFail("Expected an opaque log entry for an item in the array")
85+
continue
86+
}
87+
88+
XCTAssertEqual(typeName, "Int")
89+
90+
guard let integer = representation as? Int64 else {
91+
XCTFail("Expected an Int64 as the representation for an Int")
92+
return
93+
}
94+
95+
XCTAssertEqual(integer, Int64(index + 1))
96+
}
97+
}
98+
99+
func testMaximumDepthLimitTwoWithSuperclasses() {
100+
let testPolicy = LogPolicy(maximumDepth: 2)
101+
102+
check_TestClass: do {
103+
let logEntry = LogEntry(describing: TestClass(), name: "testClass", policy: testPolicy)
104+
105+
guard case let .structured(name, _, _, totalChildrenCount, children, disposition) = logEntry else {
106+
XCTFail("Expected a structured log entry for a class")
107+
return
108+
}
109+
110+
XCTAssertEqual(name, "testClass")
111+
XCTAssertEqual(disposition, .class)
112+
XCTAssertEqual(totalChildrenCount, 1)
113+
114+
guard children.count == 1 else {
115+
XCTFail("Expected TestClass to have exactly one child, but it had \(children.count)")
116+
break check_TestClass
117+
}
118+
119+
check_child: do {
120+
guard case let .opaque(childName, childTypeName, _, _, childRepresentation) = children[0] else {
121+
XCTFail("Expected an opaque log entry for the first child")
122+
break check_TestClass
123+
}
124+
125+
XCTAssertEqual(childName, "a")
126+
XCTAssertEqual(childTypeName, "Int")
127+
XCTAssertEqual(childRepresentation as? Int64, 1 as Int64)
128+
}
129+
}
130+
131+
check_TestSubclass: do {
132+
let logEntry = LogEntry(describing: TestSubclass(), name: "testSubclass", policy: testPolicy)
133+
134+
guard case let .structured(name, _, _, totalChildrenCount, children, disposition) = logEntry else {
135+
XCTFail("Expected a structured log entry for a class")
136+
return
137+
}
138+
139+
XCTAssertEqual(name, "testSubclass")
140+
XCTAssertEqual(disposition, .class)
141+
XCTAssertEqual(totalChildrenCount, 2)
142+
143+
guard children.count == 2 else {
144+
XCTFail("Expected TestSubclass to have exactly two children, but it had \(children.count)")
145+
break check_TestSubclass
146+
}
147+
148+
check_superclassChild: do {
149+
guard case let .structured(superclassName, _, _, superclassChildrenCount, superclassChildren, superclassDisposition) = children[0] else {
150+
XCTFail("Expected a structured log entry for the first child (superclass)")
151+
break check_TestSubclass
152+
}
153+
154+
XCTAssertEqual(superclassName, "super")
155+
XCTAssertEqual(superclassChildrenCount, 1)
156+
XCTAssertEqual(superclassDisposition, .class)
157+
158+
guard superclassChildren.count == 1 else {
159+
XCTFail("Expected exactly one child of the superclass")
160+
break check_superclassChild
161+
}
162+
163+
guard case let .opaque(childName, childTypeName, _, _, childRepresentation) = superclassChildren[0] else {
164+
XCTFail("Expected an opaque log entry for the first child")
165+
break check_superclassChild
166+
}
167+
168+
XCTAssertEqual(childName, "a")
169+
XCTAssertEqual(childTypeName, "Int")
170+
XCTAssertEqual(childRepresentation as? Int64, 1 as Int64)
171+
}
172+
173+
check_child: do {
174+
guard case let .opaque(childName, childTypeName, _, _, childRepresentation) = children[1] else {
175+
XCTFail("Expected an opaque log entry for the second child")
176+
break check_TestSubclass
177+
}
178+
179+
XCTAssertEqual(childName, "b")
180+
XCTAssertEqual(childTypeName, "Int")
181+
XCTAssertEqual(childRepresentation as? Int64, 2 as Int64)
182+
}
183+
}
184+
185+
check_TestSubsubclass: do {
186+
let logEntry = LogEntry(describing: TestSubsubclass(), name: "testSubsubclass", policy: testPolicy)
187+
188+
guard case let .structured(name, _, _, totalChildrenCount, children, disposition) = logEntry else {
189+
XCTFail("Expected a structured log entry for a class")
190+
return
191+
}
192+
193+
XCTAssertEqual(name, "testSubsubclass")
194+
XCTAssertEqual(disposition, .class)
195+
XCTAssertEqual(totalChildrenCount, 2)
196+
197+
guard children.count == 2 else {
198+
XCTFail("Expected TestSubsubclass to have exactly two children, but it had \(children.count)")
199+
break check_TestSubsubclass
200+
}
201+
202+
check_superclass: do {
203+
guard case let .structured(superclassName, _, _, superclassChildrenCount, superclassChildren, superclassDisposition) = children[0] else {
204+
XCTFail("Expected a structured log entry for the first child (superclass)")
205+
break check_superclass
206+
}
207+
208+
XCTAssertEqual(superclassName, "super")
209+
XCTAssertEqual(superclassChildrenCount, 2)
210+
XCTAssertEqual(superclassDisposition, .class)
211+
212+
guard superclassChildren.count == 2 else {
213+
XCTFail("Expected exactly two children for the superclass")
214+
break check_superclass
215+
}
216+
217+
check_doubleSuperclass: do {
218+
guard case let .structured(doubleSuperclassName, _, _, doubleSuperclassChildrenCount, doubleSuperclassChildren, doubleSuperclassDisposition) = superclassChildren[0] else {
219+
XCTFail("Expected a structured log entry for the superclass's first child (double-superclass)")
220+
break check_doubleSuperclass
221+
}
222+
223+
XCTAssertEqual(doubleSuperclassName, "super")
224+
XCTAssertEqual(doubleSuperclassChildrenCount, 1)
225+
XCTAssertEqual(doubleSuperclassDisposition, .class)
226+
227+
guard doubleSuperclassChildren.count == 1 else {
228+
XCTFail("Expected exactly one child for the double-superclass")
229+
break check_doubleSuperclass
230+
}
231+
232+
guard case .gap = doubleSuperclassChildren[0] else {
233+
XCTFail("Expected the double-superclass's child to be a gap")
234+
break check_doubleSuperclass
235+
}
236+
}
237+
238+
check_superclassChild: do {
239+
guard case let .opaque(superclassChildName, superclassChildTypeName, _, _, superclassChildRepresentation) = superclassChildren[1] else {
240+
XCTFail("Expected an opaque log entry for the superclass's second child")
241+
break check_superclassChild
242+
}
243+
244+
XCTAssertEqual(superclassChildName, "b")
245+
XCTAssertEqual(superclassChildTypeName, "Int")
246+
XCTAssertEqual(superclassChildRepresentation as? Int64, 2 as Int64)
247+
}
248+
}
249+
250+
check_child: do {
251+
guard case let .opaque(childName, childTypeName, _, _, childRepresentation) = children[1] else {
252+
XCTFail("Expected an opaque log entry for the second child")
253+
break check_child
254+
}
255+
256+
XCTAssertEqual(childName, "c")
257+
XCTAssertEqual(childTypeName, "Int")
258+
XCTAssertEqual(childRepresentation as? Int64, 3 as Int64)
259+
}
260+
}
261+
}
262+
27263
func testContainerChildPolicyAll() {
28264
let testPolicy = LogPolicy(containerChildPolicy: .all)
29265

0 commit comments

Comments
 (0)