Gracefully handle recursive state debug output (#117)

* Gracefully handle recursive state debug output

* Fix
This commit is contained in:
Stephen Celis
2020-05-19 13:22:40 -04:00
committed by GitHub
parent 6f3749bfbb
commit 2af6ac2307
2 changed files with 154 additions and 95 deletions

View File

@@ -1,104 +1,147 @@
import Foundation
func debugOutput(_ value: Any, indent: Int = 0) -> String {
let mirror = Mirror(reflecting: value)
switch (value, mirror.displayStyle) {
case let (value as CustomDebugOutputConvertible, _):
return value.debugOutput.indent(by: indent)
case (_, .collection?):
return """
[
\(mirror.children.map { "\(debugOutput($0.value, indent: 2)),\n" }.joined())]
"""
.indent(by: indent)
case (_, .dictionary?):
let pairs = mirror.children.map { label, value -> String in
let pair = value as! (key: AnyHashable, value: Any)
return "\("\(debugOutput(pair.key.base)): \(debugOutput(pair.value)),".indent(by: 2))\n"
}
return """
[
\(pairs.sorted().joined())]
"""
.indent(by: indent)
case (_, .set?):
return """
Set([
\(mirror.children.map { "\(debugOutput($0.value, indent: 2)),\n" }.sorted().joined())])
"""
.indent(by: indent)
case (_, .optional?):
return mirror.children.isEmpty
? "nil".indent(by: indent)
: debugOutput(mirror.children.first!.value, indent: indent)
case (_, .enum?) where !mirror.children.isEmpty:
let child = mirror.children.first!
let childMirror = Mirror(reflecting: child.value)
let elements =
childMirror.displayStyle != .tuple
? debugOutput(child.value, indent: 2)
: childMirror.children.map { child -> String in
let label = child.label!
return "\(label.hasPrefix(".") ? "" : "\(label): ")\(debugOutput(child.value))"
var visitedItems: Set<ObjectIdentifier> = []
func debugOutputHelp(_ value: Any, indent: Int = 0) -> String {
let mirror = Mirror(reflecting: value)
switch (value, mirror.displayStyle) {
case let (value as CustomDebugOutputConvertible, _):
return value.debugOutput.indent(by: indent)
case (_, .collection?):
return """
[
\(mirror.children.map { "\(debugOutput($0.value, indent: 2)),\n" }.joined())]
"""
.indent(by: indent)
case (_, .dictionary?):
let pairs = mirror.children.map { label, value -> String in
let pair = value as! (key: AnyHashable, value: Any)
return
"\("\(debugOutputHelp(pair.key.base)): \(debugOutputHelp(pair.value)),".indent(by: 2))\n"
}
.joined(separator: ",\n")
.indent(by: 2)
return """
\(mirror.subjectType).\(child.label!)(
\(elements)
)
"""
.indent(by: indent)
case (_, .enum?):
return """
\(mirror.subjectType).\(value)
"""
.indent(by: indent)
case (_, .struct?) where !mirror.children.isEmpty, (_, .class?) where !mirror.children.isEmpty:
let elements = mirror.children
.map { "\($0.label.map { "\($0): " } ?? "")\(debugOutput($0.value))".indent(by: 2) }
.joined(separator: ",\n")
return """
\(mirror.subjectType)(
\(elements)
)
"""
.indent(by: indent)
case let (value as CustomStringConvertible, .class?):
return value.description
.replacingOccurrences(of: #"^<([^:]+): 0x[^>]+>$"#, with: "$1()", options: .regularExpression)
.indent(by: indent)
case let (value as CustomDebugStringConvertible, _):
return value.debugDescription
.replacingOccurrences(of: #"^<([^:]+): 0x[^>]+>$"#, with: "$1()", options: .regularExpression)
.indent(by: indent)
case let (value as CustomStringConvertible, _):
return value.description
.indent(by: indent)
case (_, .struct?), (_, .class?):
return "\(mirror.subjectType)()"
.indent(by: indent)
case (_, .tuple?) where mirror.children.isEmpty:
return "()"
.indent(by: indent)
case (_, .tuple?):
let elements = mirror.children.map { child -> String in
let label = child.label!
return "\(label.hasPrefix(".") ? "" : "\(label): ")\(debugOutput(child.value))".indent(by: 2)
return """
[
\(pairs.sorted().joined())]
"""
.indent(by: indent)
case (_, .set?):
return """
Set([
\(mirror.children.map { "\(debugOutputHelp($0.value, indent: 2)),\n" }.sorted().joined())])
"""
.indent(by: indent)
case (_, .optional?):
return mirror.children.isEmpty
? "nil".indent(by: indent)
: debugOutputHelp(mirror.children.first!.value, indent: indent)
case (_, .enum?) where !mirror.children.isEmpty:
let child = mirror.children.first!
let childMirror = Mirror(reflecting: child.value)
let elements =
childMirror.displayStyle != .tuple
? debugOutputHelp(child.value, indent: 2)
: childMirror.children.map { child -> String in
let label = child.label!
return "\(label.hasPrefix(".") ? "" : "\(label): ")\(debugOutputHelp(child.value))"
}
.joined(separator: ",\n")
.indent(by: 2)
return """
\(mirror.subjectType).\(child.label!)(
\(elements)
)
"""
.indent(by: indent)
case (_, .enum?):
return """
\(mirror.subjectType).\(value)
"""
.indent(by: indent)
case (_, .struct?) where !mirror.children.isEmpty:
let elements = mirror.children
.map { "\($0.label.map { "\($0): " } ?? "")\(debugOutputHelp($0.value))".indent(by: 2) }
.joined(separator: ",\n")
return """
\(mirror.subjectType)(
\(elements)
)
"""
.indent(by: indent)
case let (value as AnyObject, .class?)
where !mirror.children.isEmpty && !visitedItems.contains(ObjectIdentifier(value)):
visitedItems.insert(ObjectIdentifier(value))
let elements = mirror.children
.map { "\($0.label.map { "\($0): " } ?? "")\(debugOutputHelp($0.value))".indent(by: 2) }
.joined(separator: ",\n")
return """
\(mirror.subjectType)(
\(elements)
)
"""
.indent(by: indent)
case let (value as AnyObject, .class?)
where !mirror.children.isEmpty && visitedItems.contains(ObjectIdentifier(value)):
return "\(mirror.subjectType)(↩︎)"
case let (value as CustomStringConvertible, .class?):
return value.description
.replacingOccurrences(
of: #"^<([^:]+): 0x[^>]+>$"#, with: "$1()", options: .regularExpression
)
.indent(by: indent)
case let (value as CustomDebugStringConvertible, _):
return value.debugDescription
.replacingOccurrences(
of: #"^<([^:]+): 0x[^>]+>$"#, with: "$1()", options: .regularExpression
)
.indent(by: indent)
case let (value as CustomStringConvertible, _):
return value.description
.indent(by: indent)
case (_, .struct?), (_, .class?):
return "\(mirror.subjectType)()"
.indent(by: indent)
case (_, .tuple?) where mirror.children.isEmpty:
return "()"
.indent(by: indent)
case (_, .tuple?):
let elements = mirror.children.map { child -> String in
let label = child.label!
return "\(label.hasPrefix(".") ? "" : "\(label): ")\(debugOutputHelp(child.value))"
.indent(by: 2)
}
return """
(
\(elements.joined(separator: ",\n"))
)
"""
.indent(by: indent)
case (_, nil):
return "\(value)"
.indent(by: indent)
@unknown default:
return "\(value)"
.indent(by: indent)
}
return """
(
\(elements.joined(separator: ",\n"))
)
"""
.indent(by: indent)
case (_, nil):
return "\(value)"
.indent(by: indent)
@unknown default:
return "\(value)"
.indent(by: indent)
}
return debugOutputHelp(value, indent: indent)
}
func debugDiff<T>(_ before: T, _ after: T, printer: (T) -> String = { debugOutput($0) }) -> String?

View File

@@ -373,6 +373,22 @@ final class DebugTests: XCTestCase {
)
}
func testRecursiveOutput() {
class Foo {
var foo: Foo?
}
let foo = Foo()
foo.foo = foo
XCTAssertEqual(
debugOutput(foo),
"""
Foo(
foo: Foo(↩︎)
)
"""
)
}
func testEffectOutput() {
// XCTAssertEqual(
// Effect<Int, Never>(value: 42)