Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,16 @@ 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
run: make test-swift
run: make test

linux:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- name: Test
run: make test-swift
run: make test
6 changes: 3 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ PLATFORM_VISIONOS = visionOS Simulator,name=Apple Vision Pro
PLATFORM_WATCHOS = watchOS Simulator,name=Apple Watch Series 9 (45mm)
CONFIG = debug

default: test-swift
default: test

build-all:
CONFIG=debug make build
Expand All @@ -25,8 +25,8 @@ build:
-destination platform="$$platform" || exit 1; \
done;

test-swift:
test:
swift test -c debug
swift test -c release

.PHONY: build-all build test-swift
.PHONY: build-all build test
6 changes: 3 additions & 3 deletions Tests/OneWayTests/EffectTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down
25 changes: 13 additions & 12 deletions Tests/OneWayTests/StoreTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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: "")
)
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -259,13 +259,14 @@ final class StoreTests: XCTestCase {
}

#if canImport(Combine)
private let textPublisher = PassthroughSubject<String, Never>()
private let numberPublisher = PassthroughSubject<Int, Never>()
/// Just for testing
private struct TestPublisher: @unchecked Sendable {
let text = PassthroughSubject<String, Never>()
let number = PassthroughSubject<Int, Never>()
}
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 {
Expand Down Expand Up @@ -371,12 +372,12 @@ private struct TestReducer: Reducer {
func bind() -> AnyEffect<Action> {
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)))
}
}
Expand Down
28 changes: 25 additions & 3 deletions Tests/OneWayTests/TestHelper/XCTestCase+Expect.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 {
Expand Down
78 changes: 42 additions & 36 deletions Tests/OneWayTests/ViewStoreTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,26 @@ import OneWay
import XCTest

#if !os(Linux)
@MainActor
final class ViewStoreTests: XCTestCase {
@MainActor
private var sut: ViewStore<TestReducer>!

@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)
Expand All @@ -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) {
Expand All @@ -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 {
Expand All @@ -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) {
Expand All @@ -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 {
Expand All @@ -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)

Expand All @@ -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)
}
}
}
}

Expand All @@ -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 {
Expand Down Expand Up @@ -224,7 +230,7 @@ private struct TestReducer: Reducer {
}
}

private actor Result {
private actor TestResult {
let expectation: XCTestExpectation
let expectedCount: Int
var values: [Int] = [] {
Expand Down