-
Notifications
You must be signed in to change notification settings - Fork 95
/
Copy pathExpectDifference.swift
164 lines (158 loc) · 4.88 KB
/
ExpectDifference.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
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
import IssueReporting
/// Expects that a value has a set of changes.
///
/// This function evaluates a given expression before and after a given operation and then compares
/// the results. The comparison is done by invoking the `changes` closure with a mutable version of
/// the initial value, and then asserting that the modifications made match the final value using
/// ``expectNoDifference``.
///
/// For example, given a very simple counter structure, we can write a test against its incrementing
/// functionality:
/// `
/// ```swift
/// struct Counter {
/// var count = 0
/// var isOdd = false
/// mutating func increment() {
/// self.count += 1
/// self.isOdd.toggle()
/// }
/// }
///
/// var counter = Counter()
/// expectDifference(counter) {
/// counter.increment()
/// } changes: {
/// $0.count = 1
/// $0.isOdd = true
/// }
/// ```
///
/// If the `changes` does not exhaustively describe all changed fields, the assertion will fail.
///
/// By omitting the operation you can write a "non-exhaustive" assertion against a value by
/// describing just the fields you want to assert against in the `changes` closure:
///
/// ```swift
/// counter.increment()
/// expectDifference(counter) {
/// $0.count = 1
/// // Don't need to further describe how `isOdd` has changed
/// }
/// ```
///
/// - Parameters:
/// - expression: An expression that is evaluated before and after `operation`, and then compared.
/// - message: An optional description of a failure.
/// - operation: An optional operation that is performed in between an initial and final
/// evaluation of `operation`. By omitting this operation, you can write a "non-exhaustive"
/// assertion against an already-changed value by describing just the fields you want to assert
/// against in the `changes` closure.
/// - updateExpectingResult: A closure that asserts how the expression changed by supplying a
/// mutable version of the initial value. This value must be modified to match the final value.
public func expectDifference<T: Equatable>(
_ expression: @autoclosure () throws -> T,
_ message: @autoclosure () -> String? = nil,
operation: () throws -> Void = {},
changes updateExpectingResult: (inout T) throws -> Void,
fileID: StaticString = #fileID,
filePath: StaticString = #filePath,
line: UInt = #line,
column: UInt = #column
) {
do {
var expression1 = try expression()
try updateExpectingResult(&expression1)
try operation()
let expression2 = try expression()
guard expression1 != expression2 else { return }
let format = DiffFormat.proportional
guard let difference = diff(expression1, expression2, format: format)
else {
reportIssue(
"""
("\(expression1)" is not equal to ("\(expression2)"), but no difference was detected.
""",
fileID: fileID,
filePath: filePath,
line: line,
column: column
)
return
}
reportIssue(
"""
\(message()?.appending(" - ") ?? "")Difference: …
\(difference.indenting(by: 2))
(Expected: \(format.first), Actual: \(format.second))
""",
fileID: fileID,
filePath: filePath,
line: line,
column: column
)
} catch {
reportIssue(
error,
fileID: fileID,
filePath: filePath,
line: line,
column: column
)
}
}
/// Expects that a value has a set of changes.
///
/// An async version of
/// ``expectDifference(_:_:operation:changes:fileID:filePath:line:column:)-5fu8q``.
public func expectDifference<T: Equatable & Sendable>(
_ expression: @autoclosure @Sendable () throws -> T,
_ message: @autoclosure @Sendable () -> String? = nil,
operation: @Sendable () async throws -> Void = {},
changes updateExpectingResult: @Sendable (inout T) throws -> Void,
fileID: StaticString = #fileID,
filePath: StaticString = #filePath,
line: UInt = #line,
column: UInt = #column
) async {
do {
var expression1 = try expression()
try updateExpectingResult(&expression1)
try await operation()
let expression2 = try expression()
guard expression1 != expression2 else { return }
let format = DiffFormat.proportional
guard let difference = diff(expression1, expression2, format: format)
else {
reportIssue(
"""
("\(expression1)" is not equal to ("\(expression2)"), but no difference was detected.
""",
fileID: fileID,
filePath: filePath,
line: line,
column: column
)
return
}
reportIssue(
"""
\(message()?.appending(" - ") ?? "")Difference: …
\(difference.indenting(by: 2))
(Expected: \(format.first), Actual: \(format.second))
""",
fileID: fileID,
filePath: filePath,
line: line,
column: column
)
} catch {
reportIssue(
error,
fileID: fileID,
filePath: filePath,
line: line,
column: column
)
}
}