From dfab0b6e62e2282b2f3197ed87198e46577baaf9 Mon Sep 17 00:00:00 2001 From: Robert MacEachern Date: Tue, 19 Aug 2025 09:48:24 -0500 Subject: [PATCH 1/3] Bump perception to 2.0 --- Package.swift | 2 +- Samples/Tuist/Package.resolved | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Package.swift b/Package.swift index 931130ecc..080eac2e9 100644 --- a/Package.swift +++ b/Package.swift @@ -59,7 +59,7 @@ let package = Package( .package(url: "https://github.com/pointfreeco/swift-case-paths", from: "1.5.5"), .package(url: "https://github.com/pointfreeco/swift-identified-collections", from: "1.0.0"), .package(url: "https://github.com/pointfreeco/swift-macro-testing", from: "0.4.0"), - .package(url: "https://github.com/pointfreeco/swift-perception", from: "1.5.0"), + .package(url: "https://github.com/pointfreeco/swift-perception", from: "2.0.0"), .package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.2.1"), .package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.0.0"), ], diff --git a/Samples/Tuist/Package.resolved b/Samples/Tuist/Package.resolved index 378ee86bf..22e3b0eb7 100644 --- a/Samples/Tuist/Package.resolved +++ b/Samples/Tuist/Package.resolved @@ -68,8 +68,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-perception", "state" : { - "revision" : "21811d6230a625fa0f2e6ffa85be857075cc02c4", - "version" : "1.5.0" + "revision" : "7d3509c7f4de78ad3eb3d804e036fb62e3585141", + "version" : "2.0.5" } }, { @@ -86,8 +86,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", "state" : { - "revision" : "96beb108a57f24c8476ae1f309239270772b2940", - "version" : "1.2.5" + "revision" : "b2ed9eabefe56202ee4939dd9fc46b6241c88317", + "version" : "1.6.1" } } ], From a954911a3a4e3d9409e081311e051bc4aec6d8d6 Mon Sep 17 00:00:00 2001 From: Robert MacEachern Date: Fri, 22 Aug 2025 11:14:35 -0500 Subject: [PATCH 2/3] `tuist install --update --path Samples` to update Sample resolved dependencies --- Samples/Tuist/Package.resolved | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/Samples/Tuist/Package.resolved b/Samples/Tuist/Package.resolved index 22e3b0eb7..c56d54a62 100644 --- a/Samples/Tuist/Package.resolved +++ b/Samples/Tuist/Package.resolved @@ -14,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/ReactiveCocoa/ReactiveSwift.git", "state" : { - "revision" : "40c465af19b993344e84355c00669ba2022ca3cd", - "version" : "7.1.1" + "revision" : "a44317ce38b5bcce173b03f5d36cfdc939e1768b", + "version" : "7.2.1" } }, { @@ -23,8 +23,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/ReactiveX/RxSwift.git", "state" : { - "revision" : "b06a8c8596e4c3e8e7788e08e720e3248563ce6a", - "version" : "6.7.1" + "revision" : "5dd1907d64f0d36f158f61a466bab75067224893", + "version" : "6.9.0" } }, { @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-case-paths", "state" : { - "revision" : "41b89b8b68d8c56c622dbb7132258f1a3e638b25", - "version" : "1.7.0" + "revision" : "9810c8d6c2914de251e072312f01d3bf80071852", + "version" : "1.7.1" } }, { @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-collections", "state" : { - "revision" : "3d2dc41a01f9e49d84f0a3925fb858bed64f702d", - "version" : "1.1.2" + "revision" : "8c0c0a8b49e080e54e5e328cc552821ff07cd341", + "version" : "1.2.1" } }, { @@ -59,8 +59,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-identified-collections", "state" : { - "revision" : "2f5ab6e091dd032b63dacbda052405756010dc3b", - "version" : "1.1.0" + "revision" : "322d9ffeeba85c9f7c4984b39422ec7cc3c56597", + "version" : "1.1.1" } }, { @@ -77,8 +77,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/swiftlang/swift-syntax", "state" : { - "revision" : "2bc86522d115234d1f588efe2bcb4ce4be8f8b82", - "version" : "510.0.3" + "revision" : "0687f71944021d616d34d922343dcef086855920", + "version" : "600.0.1" } }, { From 19e8b7bc42eeb03fc1f92cc3db95f3c33a21e096 Mon Sep 17 00:00:00 2001 From: Robert MacEachern Date: Mon, 25 Aug 2025 11:36:57 -0500 Subject: [PATCH 3/3] Don't fail tests when `onChange` is invoked after the test body has finished --- WorkflowSwiftUI/Tests/CompletionTracker.swift | 11 +++++ .../Tests/Derived/ObservableStateTests.swift | 44 +++++++++++++++---- WorkflowSwiftUI/Tests/NestedStoreTests.swift | 6 ++- WorkflowSwiftUI/Tests/StoreTests.swift | 43 +++++++++++++++--- 4 files changed, 88 insertions(+), 16 deletions(-) create mode 100644 WorkflowSwiftUI/Tests/CompletionTracker.swift diff --git a/WorkflowSwiftUI/Tests/CompletionTracker.swift b/WorkflowSwiftUI/Tests/CompletionTracker.swift new file mode 100644 index 000000000..778dd02bc --- /dev/null +++ b/WorkflowSwiftUI/Tests/CompletionTracker.swift @@ -0,0 +1,11 @@ +/// Tracks a completion flag during a test or during a portion of a larger test. +/// +/// This can be useful during tests in which object deinitialization can have side-effects that you may +/// want to filter out when making assertions. +class CompletionTracker { + private(set) var isComplete: Bool = false + + func complete() { + isComplete = true + } +} diff --git a/WorkflowSwiftUI/Tests/Derived/ObservableStateTests.swift b/WorkflowSwiftUI/Tests/Derived/ObservableStateTests.swift index 844e10f9e..72ce74fbd 100644 --- a/WorkflowSwiftUI/Tests/Derived/ObservableStateTests.swift +++ b/WorkflowSwiftUI/Tests/Derived/ObservableStateTests.swift @@ -24,6 +24,7 @@ final class ObservableStateTests: XCTestCase { } func testChildCountMutation() async { + let tracker = CompletionTracker() var state = ParentState() let childCountDidChange = expectation(description: "child.count.didChange") @@ -35,15 +36,19 @@ final class ObservableStateTests: XCTestCase { withPerceptionTracking { _ = state.child } onChange: { - XCTFail("state.child should not change.") + if tracker.isComplete == false { + XCTFail("state.child should not change.") + } } state.child.count += 1 await fulfillment(of: [childCountDidChange], timeout: 0) XCTAssertEqual(state.child.count, 1) + tracker.complete() } func testChildReset() async { + let tracker = CompletionTracker() var state = ParentState() let childDidChange = expectation(description: "child.didChange") @@ -51,7 +56,9 @@ final class ObservableStateTests: XCTestCase { withPerceptionTracking { _ = child.count } onChange: { - XCTFail("child.count should not change.") + if tracker.isComplete == false { + XCTFail("child.count should not change.") + } } withPerceptionTracking { _ = state.child @@ -62,6 +69,7 @@ final class ObservableStateTests: XCTestCase { state.child = ChildState(count: 42) await fulfillment(of: [childDidChange], timeout: 0) XCTAssertEqual(state.child.count, 42) + tracker.complete() } func testReplaceChild() async { @@ -139,16 +147,20 @@ final class ObservableStateTests: XCTestCase { // nil -> nil do { + let tracker = CompletionTracker() var state = ParentState(optional: nil) withPerceptionTracking { _ = state.optional } onChange: { - XCTFail("Optional should not change") + if tracker.isComplete == false { + XCTFail("Optional should not change") + } } state.optional = nil XCTAssertNil(state.optional) + tracker.complete() } // value -> nil @@ -169,13 +181,16 @@ final class ObservableStateTests: XCTestCase { } func testMutateOptional() async { + let tracker = CompletionTracker() var state = ParentState(optional: ChildState()) let optionalCountDidChange = expectation(description: "optional.count.didChange") withPerceptionTracking { _ = state.optional } onChange: { - XCTFail("Optional should not change") + if tracker.isComplete == false { + XCTFail("Optional should not change") + } } let optional = state.optional withPerceptionTracking { @@ -187,6 +202,7 @@ final class ObservableStateTests: XCTestCase { state.optional?.count += 1 await fulfillment(of: [optionalCountDidChange], timeout: 0) XCTAssertEqual(state.optional?.count, 1) + tracker.complete() } func testReplaceWithCopy() async { @@ -225,6 +241,7 @@ final class ObservableStateTests: XCTestCase { } func testIdentifiedArray_MutateElement() { + let tracker = CompletionTracker() var state = ParentState(rows: [ ChildState(), ChildState(), @@ -234,12 +251,16 @@ final class ObservableStateTests: XCTestCase { withPerceptionTracking { _ = state.rows } onChange: { - XCTFail("rows should not change") + if tracker.isComplete == false { + XCTFail("rows should not change") + } } withPerceptionTracking { _ = state.rows[0] } onChange: { - XCTFail("rows[0] should not change") + if tracker.isComplete == false { + XCTFail("rows[0] should not change") + } } withPerceptionTracking { _ = state.rows[0].count @@ -249,12 +270,15 @@ final class ObservableStateTests: XCTestCase { withPerceptionTracking { _ = state.rows[1].count } onChange: { - XCTFail("rows[1].count should not change") + if tracker.isComplete == false { + XCTFail("rows[1].count should not change") + } } state.rows[0].count += 1 XCTAssertEqual(state.rows[0].count, 1) wait(for: [firstRowCountDidChange], timeout: 0) + tracker.complete() } func testCopy() { @@ -289,15 +313,19 @@ final class ObservableStateTests: XCTestCase { } func testArrayMutate() { + let tracker = CompletionTracker() var state = ParentState(children: [ChildState()]) withPerceptionTracking { _ = state.children } onChange: { - XCTFail("children should not change") + if tracker.isComplete == false { + XCTFail("children should not change") + } } state.children[0].count += 1 + tracker.complete() } } diff --git a/WorkflowSwiftUI/Tests/NestedStoreTests.swift b/WorkflowSwiftUI/Tests/NestedStoreTests.swift index 5f013117b..05964a7b5 100644 --- a/WorkflowSwiftUI/Tests/NestedStoreTests.swift +++ b/WorkflowSwiftUI/Tests/NestedStoreTests.swift @@ -120,6 +120,7 @@ final class NestedStoreTests: XCTestCase { // nil to nil do { + let tracker = CompletionTracker() var state = State(optional: nil) func makeModel() -> StateAccessor { StateAccessor(state: state) { update in @@ -131,11 +132,14 @@ final class NestedStoreTests: XCTestCase { withPerceptionTracking { _ = store.scope(keyPath: \.optional)?.name } onChange: { - XCTFail("optional should not change") + if tracker.isComplete == false { + XCTFail("optional should not change") + } } state.optional = nil setModel(makeModel()) + tracker.complete() } } diff --git a/WorkflowSwiftUI/Tests/StoreTests.swift b/WorkflowSwiftUI/Tests/StoreTests.swift index 781c68bd9..b07d33bf4 100644 --- a/WorkflowSwiftUI/Tests/StoreTests.swift +++ b/WorkflowSwiftUI/Tests/StoreTests.swift @@ -8,6 +8,7 @@ import XCTest final class StoreTests: XCTestCase { func test_stateRead() { + let tracker = CompletionTracker() var state = State() let model = StateAccessor(state: state) { update in update(&state) @@ -17,11 +18,16 @@ final class StoreTests: XCTestCase { withPerceptionTracking { XCTAssertEqual(store.count, 0) } onChange: { - XCTFail("State should not have been mutated") + if tracker.isComplete == false { + XCTFail("State should not have been mutated") + } } + + tracker.complete() } func test_stateMutation() async { + let tracker = CompletionTracker() var state = State() let model = StateAccessor(state: state) { update in update(&state) @@ -39,15 +45,19 @@ final class StoreTests: XCTestCase { withPerceptionTracking { _ = store.child.name } onChange: { - XCTFail("child.name should not change") + if tracker.isComplete == false { + XCTFail("child.name should not change") + } } store.count = 1 await fulfillment(of: [countDidChange], timeout: 0) XCTAssertEqual(state.count, 1) + tracker.complete() } func test_childStateMutation() async { + let tracker = CompletionTracker() var state = State() let model = StateAccessor(state: state) { update in update(&state) @@ -59,7 +69,9 @@ final class StoreTests: XCTestCase { withPerceptionTracking { _ = store.count } onChange: { - XCTFail("count should not change") + if tracker.isComplete == false { + XCTFail("count should not change") + } } withPerceptionTracking { @@ -72,6 +84,7 @@ final class StoreTests: XCTestCase { await fulfillment(of: [childNameDidChange], timeout: 0) XCTAssertEqual(state.count, 0) XCTAssertEqual(state.child.name, "foo") + tracker.complete() } func test_stateReplacement() async { @@ -369,6 +382,7 @@ final class StoreTests: XCTestCase { // some to some do { + let tracker = CompletionTracker() var childState = ParentModel.ChildState(age: 0) let childModel = StateAccessor(state: childState) { update in update(&childState) @@ -382,7 +396,9 @@ final class StoreTests: XCTestCase { withPerceptionTracking { _ = store.optional } onChange: { - XCTFail("optional should not change") + if tracker.isComplete == false { + XCTFail("optional should not change") + } } let optionalAgeDidChange = expectation(description: "optional.age.didChange") @@ -403,10 +419,12 @@ final class StoreTests: XCTestCase { setModel(model) await fulfillment(of: [optionalAgeDidChange], timeout: 0) + tracker.complete() } // nil to nil do { + let tracker = CompletionTracker() var model = makeModel() let (store, setModel) = Store.make(model: model) @@ -416,11 +434,14 @@ final class StoreTests: XCTestCase { withPerceptionTracking { _ = store.optional } onChange: { - XCTFail("optional should not change") + if tracker.isComplete == false { + XCTFail("optional should not change") + } } model.optional = nil setModel(model) + tracker.complete() } } @@ -502,6 +523,7 @@ final class StoreTests: XCTestCase { // reorder do { + let tracker = CompletionTracker() let childStates = makeChildStates() let childModels = makeChildModels(childStates: childStates) var model = makeModel() @@ -513,7 +535,9 @@ final class StoreTests: XCTestCase { withPerceptionTracking { _ = store.array } onChange: { - XCTFail("array should not change") + if tracker.isComplete == false { + XCTFail("array should not change") + } } let array0AgeDidChange = expectation(description: "array[0].age.didChange") @@ -527,6 +551,7 @@ final class StoreTests: XCTestCase { setModel(model) await fulfillment(of: [array0AgeDidChange], timeout: 0) + tracker.complete() } } @@ -612,6 +637,7 @@ final class StoreTests: XCTestCase { // reorder do { + let tracker = CompletionTracker() var childStates = makeChildStates() let childModels = IdentifiedArray( uniqueElements: zip(childStates.indices, childStates).map { index, state in @@ -629,7 +655,9 @@ final class StoreTests: XCTestCase { withPerceptionTracking { _ = store.identified } onChange: { - XCTFail("identified should not change") + if tracker.isComplete == false { + XCTFail("identified should not change") + } } let identified0AgeDidChange = expectation(description: "identified[0].age.didChange") @@ -643,6 +671,7 @@ final class StoreTests: XCTestCase { setModel(model) await fulfillment(of: [identified0AgeDidChange], timeout: 0) + tracker.complete() } }