diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 545fb86..5325631 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Select Xcode - run: sudo xcode-select -s /Applications/Xcode_15.2.app + run: sudo xcode-select -s /Applications/Xcode_15.4.app - name: Build run: make build-all - name: Test diff --git a/Tests/OneWayTests/EffectTests.swift b/Tests/OneWayTests/EffectTests.swift index d892189..8c513c5 100644 --- a/Tests/OneWayTests/EffectTests.swift +++ b/Tests/OneWayTests/EffectTests.swift @@ -333,18 +333,18 @@ final class EffectTests: XCTestCase { let clock = TestClock() let values = Effects.Create { continuation in - Task { @MainActor in + Task { try! await clock.sleep(for: .seconds(100)) continuation.yield(Action.first) continuation.yield(Action.second) } - Task { @MainActor in + Task { try! await clock.sleep(for: .seconds(200)) continuation.yield(Action.third) continuation.yield(Action.fourth) continuation.yield(Action.fifth) } - Task { @MainActor in + Task { try! await clock.sleep(for: .seconds(300)) continuation.finish() } diff --git a/Tests/OneWayTests/StoreTests.swift b/Tests/OneWayTests/StoreTests.swift index 4e22c26..b47c2b3 100644 --- a/Tests/OneWayTests/StoreTests.swift +++ b/Tests/OneWayTests/StoreTests.swift @@ -23,7 +23,7 @@ final class StoreTests: XCTestCase { self.clock = clock sut = Store( reducer: TestReducer(clock: clock), - state: .init(count: 0, text: "") + state: TestReducer.State(count: 0, text: "") ) } @@ -103,10 +103,10 @@ final class StoreTests: XCTestCase { // https://forums.swift.org/t/how-to-use-combine-publisher-with-swift-concurrency-publisher-values-could-miss-events/67193 Task { try! await Task.sleep(nanoseconds: NSEC_PER_MSEC) - textPublisher.send("first") - numberPublisher.send(1) - textPublisher.send("second") - numberPublisher.send(2) + testPublisher.text.send("first") + testPublisher.number.send(1) + testPublisher.text.send("second") + testPublisher.number.send(2) } let states = await sut.states @@ -259,13 +259,14 @@ final class StoreTests: XCTestCase { } #if canImport(Combine) -private let textPublisher = PassthroughSubject() -private let numberPublisher = PassthroughSubject() +/// Just for testing +private struct TestPublisher: @unchecked Sendable { + let text = PassthroughSubject() + let number = PassthroughSubject() +} +private let testPublisher = TestPublisher() #endif -@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) -private var _clock = TestClock() - @available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) private struct TestReducer: Reducer { enum Action: Sendable { @@ -371,12 +372,12 @@ private struct TestReducer: Reducer { func bind() -> AnyEffect { return .merge( .sequence { send in - for await text in textPublisher.stream { + for await text in testPublisher.text.stream { send(Action.response(text)) } }, .sequence { send in - for await number in numberPublisher.stream { + for await number in testPublisher.number.stream { send(Action.response(String(number))) } } diff --git a/Tests/OneWayTests/TestHelper/XCTestCase+Expect.swift b/Tests/OneWayTests/TestHelper/XCTestCase+Expect.swift index d6743f9..fbc52aa 100644 --- a/Tests/OneWayTests/TestHelper/XCTestCase+Expect.swift +++ b/Tests/OneWayTests/TestHelper/XCTestCase+Expect.swift @@ -8,6 +8,27 @@ import XCTest extension XCTestCase { + func expect( + compare: () async -> Bool, + timeout seconds: UInt64 = 1, + description: String = #function + ) async { + let limit = NSEC_PER_SEC * seconds + let start = DispatchTime.now().uptimeNanoseconds + while true { + guard start.distance(to: DispatchTime.now().uptimeNanoseconds) < limit else { + XCTFail("Exceeded timeout of \(seconds) seconds") + break + } + if await compare() { + XCTAssert(true) + break + } else { + await Task.yield() + } + } + } + func sendableExpect( compare: @Sendable () async -> Bool, timeout seconds: UInt64 = 1, @@ -28,9 +49,10 @@ extension XCTestCase { } } } - - func expect( - compare: () async -> Bool, + + @MainActor + func sendableExpectWithMainActor( + compare: @Sendable () async -> Bool, timeout seconds: UInt64 = 1, description: String = #function ) async { diff --git a/Tests/OneWayTests/ViewStoreTests.swift b/Tests/OneWayTests/ViewStoreTests.swift index a9ea105..834b2cb 100644 --- a/Tests/OneWayTests/ViewStoreTests.swift +++ b/Tests/OneWayTests/ViewStoreTests.swift @@ -9,23 +9,26 @@ import OneWay import XCTest #if !os(Linux) -@MainActor final class ViewStoreTests: XCTestCase { + @MainActor private var sut: ViewStore! + @MainActor override func setUp() { super.setUp() sut = ViewStore( reducer: TestReducer(), - state: .init(count: 0) + state: TestReducer.State(count: 0) ) } + @MainActor override func tearDown() { super.tearDown() sut = nil } + @MainActor func test_initialState() async { XCTAssertEqual(sut.initialState, TestReducer.State(count: 0)) XCTAssertEqual(sut.state.count, 0) @@ -37,16 +40,21 @@ final class ViewStoreTests: XCTestCase { } } +#if swift(>=5.10) + @MainActor func test_sendSeveralActions() async { sut.send(.increment) sut.send(.increment) sut.send(.twice) - await sendableExpect { await sut.state.count == 4 } + nonisolated(unsafe) let sut = sut! + await sendableExpectWithMainActor { await sut.state.count == 4 } } +#endif + @MainActor func test_triggeredState() async { - actor Result { + actor TestResult { var counts: [Int] = [] var triggeredCounts: [Int] = [] func appendCount(_ count: Int) { @@ -56,7 +64,7 @@ final class ViewStoreTests: XCTestCase { triggeredCounts.append(count) } } - let result = Result() + let result = TestResult() Task { @MainActor in for await state in sut.states { @@ -73,12 +81,13 @@ final class ViewStoreTests: XCTestCase { sut.send(.setTriggeredCount(10)) sut.send(.setTriggeredCount(10)) - await sendableExpect { await result.counts == [0, 0, 0, 0] } - await sendableExpect { await result.triggeredCounts == [0, 10, 10, 10] } + await sendableExpectWithMainActor { await result.counts == [0, 0, 0, 0] } + await sendableExpectWithMainActor { await result.triggeredCounts == [0, 10, 10, 10] } } + @MainActor func test_ignoredState() async { - actor Result { + actor TestResult { var counts: [Int] = [] var ignoredCounts: [Int] = [] func appendCount(_ count: Int) { @@ -88,7 +97,7 @@ final class ViewStoreTests: XCTestCase { ignoredCounts.append(count) } } - let result = Result() + let result = TestResult() Task { @MainActor in for await state in sut.states { @@ -106,10 +115,11 @@ final class ViewStoreTests: XCTestCase { sut.send(.setIgnoredCount(30)) // only initial value - await sendableExpect { await result.counts == [0] } - await sendableExpect { await result.ignoredCounts == [0] } + await sendableExpectWithMainActor { await result.counts == [0] } + await sendableExpectWithMainActor { await result.ignoredCounts == [0] } } + @MainActor func test_asyncViewStateSequence() async { sut.send(.concat) @@ -122,15 +132,30 @@ final class ViewStoreTests: XCTestCase { XCTAssertEqual(result, [0, 1, 2, 3, 4]) } +#if swift(>=5.10) + @MainActor func test_asyncViewStateSequenceForMultipleConsumers() async { let expectation = expectation(description: #function) - let result = Result(expectation, expectedCount: 15) + nonisolated(unsafe) let sut = sut! + let result = TestResult(expectation, expectedCount: 15) Task { @MainActor in await withTaskGroup(of: Void.self) { group in - group.addTask { await self.consumeAsyncViewStateSequence1(result) } - group.addTask { await self.consumeAsyncViewStateSequence2(result) } - group.addTask { await self.consumeAsyncViewStateSequence3(result) } + group.addTask { @MainActor in + for await state in sut.states { + await result.insert(state.count) + } + } + group.addTask { @MainActor in + for await count in sut.states.count { + await result.insert(count) + } + } + group.addTask { @MainActor in + for await count in sut.states.count { + await result.insert(count) + } + } } } @@ -151,26 +176,7 @@ final class ViewStoreTests: XCTestCase { ] ) } -} - -extension ViewStoreTests { - private func consumeAsyncViewStateSequence1(_ result: Result) async { - for await state in sut.states { - await result.insert(state.count) - } - } - - private func consumeAsyncViewStateSequence2(_ result: Result) async { - for await count in sut.states.count { - await result.insert(count) - } - } - - private func consumeAsyncViewStateSequence3(_ result: Result) async { - for await count in sut.states.count { - await result.insert(count) - } - } +#endif } private struct TestReducer: Reducer { @@ -224,7 +230,7 @@ private struct TestReducer: Reducer { } } -private actor Result { +private actor TestResult { let expectation: XCTestExpectation let expectedCount: Int var values: [Int] = [] {