-
Notifications
You must be signed in to change notification settings - Fork 544
/
SnapshotTesting.swift
118 lines (107 loc) · 3.87 KB
/
SnapshotTesting.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
import XCTest
public var record = false
public func assertSnapshot(
matching any: Any,
named name: String? = nil,
pathExtension: String? = "txt",
record recording: Bool = SnapshotTesting.record,
file: StaticString = #file,
function: String = #function,
line: UInt = #line)
{
let snapshot: String = {
var string = ""
dump(any, to: &string)
return string
// Scrub NSObject pointers
.replacingOccurrences(of: ": 0x[\\da-f]+?(?=> #\\d+)", with: "", options: .regularExpression)
}()
assertSnapshot(
matching: snapshot,
named: name,
pathExtension: pathExtension,
record: recording,
file: file,
function: function,
line: line
)
}
public func assertSnapshot<S: Snapshot>(
matching snapshot: S,
named name: String? = nil,
pathExtension: String? = S.snapshotPathExtension,
record recording: Bool = SnapshotTesting.record,
file: StaticString = #file,
function: String = #function,
line: UInt = #line)
{
let snapshotDirectoryUrl: URL = {
let fileUrl = URL(fileURLWithPath: "\(file)")
let directoryUrl = fileUrl.deletingLastPathComponent()
return directoryUrl
.appendingPathComponent("__Snapshots__")
.appendingPathComponent(fileUrl.deletingPathExtension().lastPathComponent)
}()
let testName: String = {
let testIdentifier = "\(snapshotDirectoryUrl.absoluteString):\(function)"
counter[testIdentifier, default: 0] += 1
return "\(function.dropLast(2)).\(counter[testIdentifier]!)"
}()
let snapshotFileUrl = snapshotDirectoryUrl
.appendingPathComponent(name.map { "\(testName).\($0)" } ?? testName)
.appendingPathExtension(pathExtension ?? "")
let fileManager = FileManager.default
try! fileManager.createDirectory(at: snapshotDirectoryUrl, withIntermediateDirectories: true)
defer {
// NB: Linux doesn't have file manager enumeration capabilities, so we skip this work on Linux.
#if !os(Linux)
staleSnapshots[snapshotDirectoryUrl, default: Set(
try! fileManager.contentsOfDirectory(
at: snapshotDirectoryUrl, includingPropertiesForKeys: nil, options: .skipsHiddenFiles
)
)].remove(snapshotFileUrl)
_ = trackSnapshots
#endif
}
let format = snapshot.snapshotFormat
if !recording && fileManager.fileExists(atPath: snapshotFileUrl.path) {
let reference = S.Format.fromDiffableData(try! Data(contentsOf: snapshotFileUrl))
if let (failure, attachments) = S.Format.diffableDiff(reference, format) {
XCTFail(failure, file: file, line: line)
if !attachments.isEmpty {
// NB: Linux doesn't have XCTAttachment, and we don't even need it, so can skip all of this work.
#if !os(Linux)
XCTContext.runActivity(named: "Attached Failure Diff") { activity in
attachments.forEach {
$0.lifetime = .deleteOnSuccess
activity.add($0)
}
}
#endif
}
}
} else {
try! format.diffableData.write(to: snapshotFileUrl)
XCTFail(
"Recorded snapshot to \(snapshotFileUrl.path.debugDescription)"
+ (format.diffableDescription.map { ":\n\n\($0)" } ?? ""),
file: file,
line: line
)
}
}
/// Coeffect: global mutable state tracking the number of snapshots per test.
private var counter: [String: Int] = [:]
/// Coeffect: global mutable state tracking stale snapshots.
private var staleSnapshots: [URL: Set<URL>] = [:]
/// Prepares an `atexit` hook to print a list of any stale snapshots (those that were detected in
/// `__Snapshots__` directories but were not used in any assertions).
private var trackSnapshots = {
atexit {
let stale = staleSnapshots.flatMap { $1 }
let count = stale.count
guard count > 0 else { return }
let list = stale.map { " \($0.path.debugDescription)" }.sorted().joined(separator: " \\\n")
print("Found \(count) stale snapshot\(count == 1 ? "" : "s"):\n\n\(list)")
}
}()