|
| 1 | +import XCTestDynamicOverlay |
| 2 | + |
| 3 | +/// Asserts that a value has a set of changes. |
| 4 | +/// |
| 5 | +/// This function evaluates a given expression before and after a given operation and then compares |
| 6 | +/// the results. The comparison is done by invoking the `changes` closure with a mutable version of |
| 7 | +/// the initial value, and then asserting that the modifications made match the final value using |
| 8 | +/// ``XCTAssertNoDifference``. |
| 9 | +/// |
| 10 | +/// For example, given a very simple counter structure, we can write a test against its incrementing |
| 11 | +/// functionality: |
| 12 | +/// ` |
| 13 | +/// ```swift |
| 14 | +/// struct Counter { |
| 15 | +/// var count = 0 |
| 16 | +/// var isOdd = false |
| 17 | +/// mutating func increment() { |
| 18 | +/// self.count += 1 |
| 19 | +/// self.isOdd.toggle() |
| 20 | +/// } |
| 21 | +/// } |
| 22 | +/// |
| 23 | +/// var counter = Counter() |
| 24 | +/// XCTAssertDifference(counter) { |
| 25 | +/// counter.increment() |
| 26 | +/// } changes: { |
| 27 | +/// $0.count = 1 |
| 28 | +/// $0.isOdd = true |
| 29 | +/// } |
| 30 | +/// ``` |
| 31 | +/// |
| 32 | +/// If the `changes` does not exhaustively describe all changed fields, the assertion will fail. |
| 33 | +/// |
| 34 | +/// By omitting the operation you can write a "non-exhaustive" assertion against a value by |
| 35 | +/// describing just the fields you want to assert against in the `changes` closure: |
| 36 | +/// |
| 37 | +/// ```swift |
| 38 | +/// counter.increment() |
| 39 | +/// XCTAssertDifference(counter) { |
| 40 | +/// $0.count = 1 |
| 41 | +/// // Don't need to further describe how `isOdd` has changed |
| 42 | +/// } |
| 43 | +/// ``` |
| 44 | +/// |
| 45 | +/// - Parameters: |
| 46 | +/// - expression: An expression that is evaluated before and after `operation`, and then compared. |
| 47 | +/// - message: An optional description of a failure. |
| 48 | +/// - operation: An optional operation that is performed in between an initial and final |
| 49 | +/// evaluation of `operation`. By omitting this operation, you can write a "non-exhaustive" |
| 50 | +/// assertion against an already-changed value by describing just the fields you want to assert |
| 51 | +/// against in the `changes` closure. |
| 52 | +/// - updateExpectingResult: A closure that asserts how the expression changed by supplying a |
| 53 | +/// mutable version of the initial value. This value must be modified to match the final value. |
| 54 | +@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) |
| 55 | +public func XCTAssertDifference<T>( |
| 56 | + _ expression: @autoclosure () throws -> T, |
| 57 | + _ message: @autoclosure () -> String = "", |
| 58 | + operation: () throws -> Void = {}, |
| 59 | + changes updateExpectingResult: (inout T) throws -> Void, |
| 60 | + file: StaticString = #filePath, |
| 61 | + line: UInt = #line |
| 62 | +) where T: Equatable { |
| 63 | + do { |
| 64 | + var expression1 = try expression() |
| 65 | + try updateExpectingResult(&expression1) |
| 66 | + try operation() |
| 67 | + let expression2 = try expression() |
| 68 | + let message = message() |
| 69 | + guard expression1 != expression2 else { return } |
| 70 | + let format = DiffFormat.proportional |
| 71 | + guard let difference = diff(expression1, expression2, format: format) |
| 72 | + else { |
| 73 | + XCTFail( |
| 74 | + """ |
| 75 | + XCTAssertDifference failed: ("\(expression1)" is not equal to ("\(expression2)"), but no \ |
| 76 | + difference was detected. |
| 77 | + """, |
| 78 | + file: file, |
| 79 | + line: line |
| 80 | + ) |
| 81 | + return |
| 82 | + } |
| 83 | + let failure = """ |
| 84 | + XCTAssertDifference failed: … |
| 85 | +
|
| 86 | + \(difference.indenting(by: 2)) |
| 87 | + |
| 88 | + (Expected: \(format.first), Actual: \(format.second)) |
| 89 | + """ |
| 90 | + XCTFail( |
| 91 | + "\(failure)\(message.isEmpty ? "" : " - \(message)")", |
| 92 | + file: file, |
| 93 | + line: line |
| 94 | + ) |
| 95 | + } catch { |
| 96 | + XCTFail( |
| 97 | + """ |
| 98 | + XCTAssertDifference failed: threw error "\(error)" |
| 99 | + """, |
| 100 | + file: file, |
| 101 | + line: line |
| 102 | + ) |
| 103 | + } |
| 104 | +} |
| 105 | + |
| 106 | +/// Asserts that a value has a set of changes. |
| 107 | +/// |
| 108 | +/// An async version of ``XCTAssertDifference(_:_:operation:changes:)``. |
| 109 | +@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) |
| 110 | +public func XCTAssertDifference<T: Sendable>( |
| 111 | + _ expression: @autoclosure @Sendable () throws -> T, |
| 112 | + _ message: @autoclosure @Sendable () -> String = "", |
| 113 | + operation: @Sendable () async throws -> Void = {}, |
| 114 | + changes updateExpectingResult: @Sendable (inout T) throws -> Void, |
| 115 | + file: StaticString = #filePath, |
| 116 | + line: UInt = #line |
| 117 | +) async where T: Equatable { |
| 118 | + do { |
| 119 | + var expression1 = try expression() |
| 120 | + try updateExpectingResult(&expression1) |
| 121 | + try await operation() |
| 122 | + let expression2 = try expression() |
| 123 | + let message = message() |
| 124 | + guard expression1 != expression2 else { return } |
| 125 | + let format = DiffFormat.proportional |
| 126 | + guard let difference = diff(expression1, expression2, format: format) |
| 127 | + else { |
| 128 | + XCTFail( |
| 129 | + """ |
| 130 | + XCTAssertDifference failed: ("\(expression1)" is not equal to ("\(expression2)"), but no \ |
| 131 | + difference was detected. |
| 132 | + """, |
| 133 | + file: file, |
| 134 | + line: line |
| 135 | + ) |
| 136 | + return |
| 137 | + } |
| 138 | + let failure = """ |
| 139 | + XCTAssertDifference failed: … |
| 140 | +
|
| 141 | + \(difference.indenting(by: 2)) |
| 142 | +
|
| 143 | + (Expected: \(format.first), Actual: \(format.second)) |
| 144 | + """ |
| 145 | + XCTFail( |
| 146 | + "\(failure)\(message.isEmpty ? "" : " - \(message)")", |
| 147 | + file: file, |
| 148 | + line: line |
| 149 | + ) |
| 150 | + } catch { |
| 151 | + XCTFail( |
| 152 | + """ |
| 153 | + XCTAssertDifference failed: threw error "\(error)" |
| 154 | + """, |
| 155 | + file: file, |
| 156 | + line: line |
| 157 | + ) |
| 158 | + } |
| 159 | +} |
0 commit comments