From 3c3080acf88fe209d878275294288d7af2398c66 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Sat, 19 Aug 2023 22:51:23 -0700 Subject: [PATCH 1/7] Add `XCTAssertDifference` for testing changes to values --- Sources/CustomDump/XCTAssertDifference.swift | 106 ++++++++++++++++++ .../XCTAssertDifferenceTests.swift | 57 ++++++++++ 2 files changed, 163 insertions(+) create mode 100644 Sources/CustomDump/XCTAssertDifference.swift create mode 100644 Tests/CustomDumpTests/XCTAssertDifferenceTests.swift diff --git a/Sources/CustomDump/XCTAssertDifference.swift b/Sources/CustomDump/XCTAssertDifference.swift new file mode 100644 index 0000000..b4021f9 --- /dev/null +++ b/Sources/CustomDump/XCTAssertDifference.swift @@ -0,0 +1,106 @@ +import XCTestDynamicOverlay + +@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +public func XCTAssertDifference( + _ expression: @autoclosure () throws -> T, + _ message: @autoclosure () -> String = "", + operation: () throws -> Void = {}, + changes updateExpectingResult: (inout T) throws -> Void, + file: StaticString = #filePath, + line: UInt = #line +) where T: Equatable { + do { + var expression1 = try expression() + try updateExpectingResult(&expression1) + try operation() + let expression2 = try expression() + let message = message() + guard expression1 != expression2 else { return } + let format = DiffFormat.proportional + guard let difference = diff(expression1, expression2, format: format) + else { + XCTFail( + """ + XCTAssertDifference failed: ("\(expression1)" is not equal to ("\(expression2)"), but no \ + difference was detected. + """, + file: file, + line: line + ) + return + } + let failure = """ + XCTAssertDifference failed: … + + \(difference.indenting(by: 2)) + + (Expected: \(format.first), Actual: \(format.second)) + """ + XCTFail( + "\(failure)\(message.isEmpty ? "" : " - \(message)")", + file: file, + line: line + ) + } catch { + XCTFail( + """ + XCTAssertDifference failed: threw error "\(error)" + """, + file: file, + line: line + ) + } +} + +// TODO: Somehow share logic between above and below without `reasync`. +@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +public func XCTAssertDifference( + _ expression: @autoclosure () throws -> T, + _ message: @autoclosure () -> String = "", + operation: () async throws -> Void = {}, + changes updateExpectingResult: (inout T) throws -> Void, + file: StaticString = #filePath, + line: UInt = #line +) async where T: Equatable { + do { + var expression1 = try expression() + try updateExpectingResult(&expression1) + try await operation() + let expression2 = try expression() + let message = message() + guard expression1 != expression2 else { return } + let format = DiffFormat.proportional + guard let difference = diff(expression1, expression2, format: format) + else { + XCTFail( + """ + XCTAssertDifference failed: ("\(expression1)" is not equal to ("\(expression2)"), but no \ + difference was detected. + """, + file: file, + line: line + ) + return + } + let failure = """ + XCTAssertDifference failed: … + + \(difference.indenting(by: 2)) + + (Expected: \(format.first), Actual: \(format.second)) + """ + XCTFail( + "\(failure)\(message.isEmpty ? "" : " - \(message)")", + file: file, + line: line + ) + } catch { + XCTFail( + """ + XCTAssertDifference failed: threw error "\(error)" + """, + file: file, + line: line + ) + } +} diff --git a/Tests/CustomDumpTests/XCTAssertDifferenceTests.swift b/Tests/CustomDumpTests/XCTAssertDifferenceTests.swift new file mode 100644 index 0000000..24f2698 --- /dev/null +++ b/Tests/CustomDumpTests/XCTAssertDifferenceTests.swift @@ -0,0 +1,57 @@ +import CustomDump +import XCTest + +@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +class XCTAssertDifferencesTests: XCTestCase { + func testXCTAssertDifference() { + var user = User(id: 42, name: "Blob") + func increment(_ root: inout Value, at keyPath: WritableKeyPath) { + root[keyPath: keyPath] += 1 + } + + XCTAssertDifference(user) { + increment(&user, at: \.id) + } changes: { + $0.id = 43 + } + } + + func testXCTAssertDifference_NonExhaustive() { + let user = User(id: 42, name: "Blob") + func increment(_ root: inout Value, at keyPath: WritableKeyPath) { + root[keyPath: keyPath] += 1 + } + + XCTAssertDifference(user) { + $0.id = 42 + $0.name = "Blob" + } + } + + #if compiler(>=5.4) && (os(iOS) || os(macOS) || os(tvOS) || os(watchOS)) + func testXCTAssertDifference_Failure() { + var user = User(id: 42, name: "Blob") + func increment(_ root: inout Value, at keyPath: WritableKeyPath) { + root[keyPath: keyPath] += 1 + } + + XCTExpectFailure() + + XCTAssertDifference(user) { + increment(&user, at: \.id) + } changes: { + $0.id = 44 + } + } + + func testXCTAssertNoDifference() { + XCTExpectFailure() + + let user = User(id: 42, name: "Blob") + var other = user + other.name += " Sr." + + XCTAssertNoDifference(user, other) + } + #endif +} From 5243d1eb10788c9a95da636f05cb7127a669ebda Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Sat, 19 Aug 2023 22:59:45 -0700 Subject: [PATCH 2/7] wip --- Sources/CustomDump/XCTAssertDifference.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/CustomDump/XCTAssertDifference.swift b/Sources/CustomDump/XCTAssertDifference.swift index b4021f9..ca288dd 100644 --- a/Sources/CustomDump/XCTAssertDifference.swift +++ b/Sources/CustomDump/XCTAssertDifference.swift @@ -55,10 +55,10 @@ public func XCTAssertDifference( // TODO: Somehow share logic between above and below without `reasync`. @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) public func XCTAssertDifference( - _ expression: @autoclosure () throws -> T, - _ message: @autoclosure () -> String = "", - operation: () async throws -> Void = {}, - changes updateExpectingResult: (inout T) throws -> Void, + _ expression: @autoclosure @Sendable () throws -> T, + _ message: @autoclosure @Sendable () -> String = "", + operation: @Sendable () async throws -> Void = {}, + changes updateExpectingResult: @Sendable (inout T) throws -> Void, file: StaticString = #filePath, line: UInt = #line ) async where T: Equatable { From e196284d10c6071ddd03471fda99ebb5bacb89bd Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Sat, 19 Aug 2023 23:01:58 -0700 Subject: [PATCH 3/7] wip --- Sources/CustomDump/XCTAssertDifference.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/CustomDump/XCTAssertDifference.swift b/Sources/CustomDump/XCTAssertDifference.swift index ca288dd..6e19efa 100644 --- a/Sources/CustomDump/XCTAssertDifference.swift +++ b/Sources/CustomDump/XCTAssertDifference.swift @@ -54,7 +54,7 @@ public func XCTAssertDifference( // TODO: Somehow share logic between above and below without `reasync`. @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) -public func XCTAssertDifference( +public func XCTAssertDifference( _ expression: @autoclosure @Sendable () throws -> T, _ message: @autoclosure @Sendable () -> String = "", operation: @Sendable () async throws -> Void = {}, From 6740502665bb00249c59107d0834ae29932d3afb Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Tue, 22 Aug 2023 11:30:23 -0700 Subject: [PATCH 4/7] Docs --- Sources/CustomDump/XCTAssertDifference.swift | 55 +++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/Sources/CustomDump/XCTAssertDifference.swift b/Sources/CustomDump/XCTAssertDifference.swift index 6e19efa..080e20b 100644 --- a/Sources/CustomDump/XCTAssertDifference.swift +++ b/Sources/CustomDump/XCTAssertDifference.swift @@ -1,5 +1,56 @@ import XCTestDynamicOverlay +/// Asserts 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 +/// ``XCTAssertNoDifference``. +/// +/// 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() +/// XCTAssertDifference(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() +/// XCTAssertDifference(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. @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) public func XCTAssertDifference( _ expression: @autoclosure () throws -> T, @@ -52,7 +103,9 @@ public func XCTAssertDifference( } } -// TODO: Somehow share logic between above and below without `reasync`. +/// Asserts that a value has a set of changes. +/// +/// An async version of ``XCTAssertDifference(_:_:operation:changes:)``. @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) public func XCTAssertDifference( _ expression: @autoclosure @Sendable () throws -> T, From a0adb6423ca1845f79cd4d8cac928655070374d7 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Tue, 22 Aug 2023 15:30:31 -0700 Subject: [PATCH 5/7] Update Tests/CustomDumpTests/XCTAssertDifferenceTests.swift Co-authored-by: Brandon Williams <135203+mbrandonw@users.noreply.github.com> --- Tests/CustomDumpTests/XCTAssertDifferenceTests.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/Tests/CustomDumpTests/XCTAssertDifferenceTests.swift b/Tests/CustomDumpTests/XCTAssertDifferenceTests.swift index 24f2698..2bfd407 100644 --- a/Tests/CustomDumpTests/XCTAssertDifferenceTests.swift +++ b/Tests/CustomDumpTests/XCTAssertDifferenceTests.swift @@ -18,9 +18,6 @@ class XCTAssertDifferencesTests: XCTestCase { func testXCTAssertDifference_NonExhaustive() { let user = User(id: 42, name: "Blob") - func increment(_ root: inout Value, at keyPath: WritableKeyPath) { - root[keyPath: keyPath] += 1 - } XCTAssertDifference(user) { $0.id = 42 From 93e8e8d79f9900d7534b10db11e1518031b44bcf Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Tue, 22 Aug 2023 15:32:26 -0700 Subject: [PATCH 6/7] wip --- Tests/CustomDumpTests/XCTAssertDifferenceTests.swift | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/Tests/CustomDumpTests/XCTAssertDifferenceTests.swift b/Tests/CustomDumpTests/XCTAssertDifferenceTests.swift index 2bfd407..a48ec35 100644 --- a/Tests/CustomDumpTests/XCTAssertDifferenceTests.swift +++ b/Tests/CustomDumpTests/XCTAssertDifferenceTests.swift @@ -40,15 +40,5 @@ class XCTAssertDifferencesTests: XCTestCase { $0.id = 44 } } - - func testXCTAssertNoDifference() { - XCTExpectFailure() - - let user = User(id: 42, name: "Blob") - var other = user - other.name += " Sr." - - XCTAssertNoDifference(user, other) - } #endif } From 10e0b37957786d39f32d6d928c4ec4d751196942 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Tue, 22 Aug 2023 16:15:29 -0700 Subject: [PATCH 7/7] wip --- Tests/CustomDumpTests/XCTAssertDifferenceTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/CustomDumpTests/XCTAssertDifferenceTests.swift b/Tests/CustomDumpTests/XCTAssertDifferenceTests.swift index a48ec35..77d3ab8 100644 --- a/Tests/CustomDumpTests/XCTAssertDifferenceTests.swift +++ b/Tests/CustomDumpTests/XCTAssertDifferenceTests.swift @@ -25,7 +25,7 @@ class XCTAssertDifferencesTests: XCTestCase { } } - #if compiler(>=5.4) && (os(iOS) || os(macOS) || os(tvOS) || os(watchOS)) + #if DEBUG && compiler(>=5.4) && (os(iOS) || os(macOS) || os(tvOS) || os(watchOS)) func testXCTAssertDifference_Failure() { var user = User(id: 42, name: "Blob") func increment(_ root: inout Value, at keyPath: WritableKeyPath) {