Skip to content

Commit 16b4700

Browse files
authored
Use @_spi to test more internals in release. (#1456)
1 parent ef13780 commit 16b4700

File tree

8 files changed

+167
-192
lines changed

8 files changed

+167
-192
lines changed

Sources/ComposableArchitecture/Effects/Cancellation.swift

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -42,33 +42,33 @@ extension Effect {
4242
AnyPublisher<Action, Failure>, PassthroughSubject<Void, Never>
4343
>
4444
> in
45-
cancellablesLock.lock()
46-
defer { cancellablesLock.unlock() }
45+
_cancellablesLock.lock()
46+
defer { _cancellablesLock.unlock() }
4747

48-
let id = CancelToken(id: id)
48+
let id = _CancelToken(id: id)
4949
if cancelInFlight {
50-
cancellationCancellables[id]?.forEach { $0.cancel() }
50+
_cancellationCancellables[id]?.forEach { $0.cancel() }
5151
}
5252

5353
let cancellationSubject = PassthroughSubject<Void, Never>()
5454

5555
var cancellationCancellable: AnyCancellable!
5656
cancellationCancellable = AnyCancellable {
57-
cancellablesLock.sync {
57+
_cancellablesLock.sync {
5858
cancellationSubject.send(())
5959
cancellationSubject.send(completion: .finished)
60-
cancellationCancellables[id]?.remove(cancellationCancellable)
61-
if cancellationCancellables[id]?.isEmpty == .some(true) {
62-
cancellationCancellables[id] = nil
60+
_cancellationCancellables[id]?.remove(cancellationCancellable)
61+
if _cancellationCancellables[id]?.isEmpty == .some(true) {
62+
_cancellationCancellables[id] = nil
6363
}
6464
}
6565
}
6666

6767
return publisher.prefix(untilOutputFrom: cancellationSubject)
6868
.handleEvents(
6969
receiveSubscription: { _ in
70-
_ = cancellablesLock.sync {
71-
cancellationCancellables[id, default: []].insert(
70+
_ = _cancellablesLock.sync {
71+
_cancellationCancellables[id, default: []].insert(
7272
cancellationCancellable
7373
)
7474
}
@@ -112,8 +112,8 @@ extension Effect {
112112
/// identifier.
113113
public static func cancel(id: AnyHashable) -> Self {
114114
.fireAndForget {
115-
cancellablesLock.sync {
116-
cancellationCancellables[.init(id: id)]?.forEach { $0.cancel() }
115+
_cancellablesLock.sync {
116+
_cancellationCancellables[.init(id: id)]?.forEach { $0.cancel() }
117117
}
118118
}
119119
}
@@ -196,21 +196,21 @@ public func withTaskCancellation<T: Sendable>(
196196
cancelInFlight: Bool = false,
197197
operation: @Sendable @escaping () async throws -> T
198198
) async rethrows -> T {
199-
let id = CancelToken(id: id)
200-
let (cancellable, task) = cancellablesLock.sync { () -> (AnyCancellable, Task<T, Error>) in
199+
let id = _CancelToken(id: id)
200+
let (cancellable, task) = _cancellablesLock.sync { () -> (AnyCancellable, Task<T, Error>) in
201201
if cancelInFlight {
202-
cancellationCancellables[id]?.forEach { $0.cancel() }
202+
_cancellationCancellables[id]?.forEach { $0.cancel() }
203203
}
204204
let task = Task { try await operation() }
205205
let cancellable = AnyCancellable { task.cancel() }
206-
cancellationCancellables[id, default: []].insert(cancellable)
206+
_cancellationCancellables[id, default: []].insert(cancellable)
207207
return (cancellable, task)
208208
}
209209
defer {
210-
cancellablesLock.sync {
211-
cancellationCancellables[id]?.remove(cancellable)
212-
if cancellationCancellables[id]?.isEmpty == .some(true) {
213-
cancellationCancellables[id] = nil
210+
_cancellablesLock.sync {
211+
_cancellationCancellables[id]?.remove(cancellable)
212+
if _cancellationCancellables[id]?.isEmpty == .some(true) {
213+
_cancellationCancellables[id] = nil
214214
}
215215
}
216216
}
@@ -250,7 +250,7 @@ extension Task where Success == Never, Failure == Never {
250250
///
251251
/// - Parameter id: An identifier.
252252
public static func cancel<ID: Hashable & Sendable>(id: ID) {
253-
cancellablesLock.sync { cancellationCancellables[.init(id: id)]?.forEach { $0.cancel() } }
253+
_cancellablesLock.sync { _cancellationCancellables[.init(id: id)]?.forEach { $0.cancel() } }
254254
}
255255

256256
/// Cancel any currently in-flight operation with the given identifier.
@@ -264,18 +264,18 @@ extension Task where Success == Never, Failure == Never {
264264
}
265265
}
266266

267-
struct CancelToken: Hashable {
267+
@_spi(Internals) public struct _CancelToken: Hashable {
268268
let id: AnyHashable
269269
let discriminator: ObjectIdentifier
270270

271-
init(id: AnyHashable) {
271+
public init(id: AnyHashable) {
272272
self.id = id
273273
self.discriminator = ObjectIdentifier(type(of: id.base))
274274
}
275275
}
276276

277-
var cancellationCancellables: [CancelToken: Set<AnyCancellable>] = [:]
278-
let cancellablesLock = NSRecursiveLock()
277+
@_spi(Internals) public var _cancellationCancellables: [_CancelToken: Set<AnyCancellable>] = [:]
278+
@_spi(Internals) public let _cancellablesLock = NSRecursiveLock()
279279

280280
@rethrows
281281
private protocol _ErrorMechanism {

Sources/ComposableArchitecture/Internal/Locking.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ extension UnsafeMutablePointer where Pointee == os_unfair_lock_s {
1111

1212
extension NSRecursiveLock {
1313
@inlinable @discardableResult
14-
func sync<R>(work: () -> R) -> R {
14+
@_spi(Internals) public func sync<R>(work: () -> R) -> R {
1515
self.lock()
1616
defer { self.unlock() }
1717
return work()

Sources/ComposableArchitecture/Internal/TaskCancellableValue.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
extension Task where Failure == Error {
2-
var cancellableValue: Success {
2+
@_spi(Internals) public var cancellableValue: Success {
33
get async throws {
44
try await withTaskCancellationHandler {
55
try await self.value

Sources/ComposableArchitecture/Store.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ import Foundation
122122
/// store are also checked to make sure that work is performed on the main thread.
123123
public final class Store<State, Action> {
124124
private var bufferedActions: [Action] = []
125-
var effectCancellables: [UUID: AnyCancellable] = [:]
125+
@_spi(Internals) public var effectCancellables: [UUID: AnyCancellable] = [:]
126126
private var isSending = false
127127
var parentCancellable: AnyCancellable?
128128
#if swift(>=5.7)
@@ -317,7 +317,7 @@ public final class Store<State, Action> {
317317
self.scope(state: toChildState, action: { $0 })
318318
}
319319

320-
func send(
320+
@_spi(Internals) public func send(
321321
_ action: Action,
322322
originatingFrom originatingAction: Action? = nil
323323
) -> Task<Void, Never>? {

Sources/ComposableArchitecture/TestStore.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ public final class TestStore<State, Action, ScopedState, ScopedAction, Environme
237237
private let fromScopedAction: (ScopedAction) -> Action
238238
private var line: UInt
239239
let reducer: TestReducer<State, Action>
240-
private var store: Store<State, TestReducer<State, Action>.TestAction>!
240+
private let store: Store<State, TestReducer<State, Action>.TestAction>
241241
private let toScopedState: (State) -> ScopedState
242242

243243
public init<Reducer: ReducerProtocol>(
@@ -1129,7 +1129,7 @@ class TestReducer<State, Action>: ReducerProtocol {
11291129
}
11301130

11311131
extension Task where Success == Never, Failure == Never {
1132-
static func megaYield(count: Int = 10) async {
1132+
@_spi(Internals) public static func megaYield(count: Int = 10) async {
11331133
for _ in 1...count {
11341134
await Task<Void, Never>.detached(priority: .low) { await Task.yield() }.value
11351135
}

Tests/ComposableArchitectureTests/EffectCancellationTests.swift

Lines changed: 82 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
11
import Combine
2+
@_spi(Internals) import ComposableArchitecture
23
import XCTest
34

4-
#if DEBUG
5-
@testable import ComposableArchitecture
6-
#else
7-
import ComposableArchitecture
8-
#endif
9-
105
final class EffectCancellationTests: XCTestCase {
116
struct CancelID: Hashable {}
127
var cancellables: Set<AnyCancellable> = []
@@ -112,39 +107,35 @@ final class EffectCancellationTests: XCTestCase {
112107
XCTAssertEqual(value, nil)
113108
}
114109

115-
#if DEBUG
116-
func testCancellablesCleanUp_OnComplete() {
117-
let id = UUID()
110+
func testCancellablesCleanUp_OnComplete() {
111+
let id = UUID()
118112

119-
Just(1)
120-
.eraseToEffect()
121-
.cancellable(id: id)
122-
.sink(receiveValue: { _ in })
123-
.store(in: &self.cancellables)
113+
Just(1)
114+
.eraseToEffect()
115+
.cancellable(id: id)
116+
.sink(receiveValue: { _ in })
117+
.store(in: &self.cancellables)
124118

125-
XCTAssertNil(cancellationCancellables[CancelToken(id: id)])
126-
}
127-
#endif
119+
XCTAssertNil(_cancellationCancellables[_CancelToken(id: id)])
120+
}
128121

129-
#if DEBUG
130-
func testCancellablesCleanUp_OnCancel() {
131-
let id = UUID()
122+
func testCancellablesCleanUp_OnCancel() {
123+
let id = UUID()
132124

133-
let mainQueue = DispatchQueue.test
134-
Just(1)
135-
.delay(for: 1, scheduler: mainQueue)
136-
.eraseToEffect()
137-
.cancellable(id: id)
138-
.sink(receiveValue: { _ in })
139-
.store(in: &self.cancellables)
125+
let mainQueue = DispatchQueue.test
126+
Just(1)
127+
.delay(for: 1, scheduler: mainQueue)
128+
.eraseToEffect()
129+
.cancellable(id: id)
130+
.sink(receiveValue: { _ in })
131+
.store(in: &self.cancellables)
140132

141-
Effect<Int, Never>.cancel(id: id)
142-
.sink(receiveValue: { _ in })
143-
.store(in: &self.cancellables)
133+
Effect<Int, Never>.cancel(id: id)
134+
.sink(receiveValue: { _ in })
135+
.store(in: &self.cancellables)
144136

145-
XCTAssertNil(cancellationCancellables[CancelToken(id: id)])
146-
}
147-
#endif
137+
XCTAssertNil(_cancellationCancellables[_CancelToken(id: id)])
138+
}
148139

149140
func testDoubleCancellation() {
150141
var values: [Int] = []
@@ -194,77 +185,73 @@ final class EffectCancellationTests: XCTestCase {
194185
XCTAssertEqual(values, [1])
195186
}
196187

197-
#if DEBUG
198-
func testConcurrentCancels() {
199-
let queues = [
200-
DispatchQueue.main,
201-
DispatchQueue.global(qos: .background),
202-
DispatchQueue.global(qos: .default),
203-
DispatchQueue.global(qos: .unspecified),
204-
DispatchQueue.global(qos: .userInitiated),
205-
DispatchQueue.global(qos: .userInteractive),
206-
DispatchQueue.global(qos: .utility),
207-
]
208-
let ids = (1...10).map { _ in UUID() }
209-
210-
let effect = Effect.merge(
211-
(1...1_000).map { idx -> Effect<Int, Never> in
212-
let id = ids[idx % 10]
213-
214-
return Effect.merge(
215-
Just(idx)
216-
.delay(
217-
for: .milliseconds(Int.random(in: 1...100)), scheduler: queues.randomElement()!
218-
)
219-
.eraseToEffect()
220-
.cancellable(id: id),
221-
222-
Just(())
223-
.delay(
224-
for: .milliseconds(Int.random(in: 1...100)), scheduler: queues.randomElement()!
225-
)
226-
.flatMap { Effect.cancel(id: id) }
227-
.eraseToEffect()
228-
)
229-
}
230-
)
231-
232-
let expectation = self.expectation(description: "wait")
233-
effect
234-
.sink(receiveCompletion: { _ in expectation.fulfill() }, receiveValue: { _ in })
235-
.store(in: &self.cancellables)
236-
self.wait(for: [expectation], timeout: 999)
237-
238-
for id in ids {
239-
XCTAssertNil(
240-
cancellationCancellables[CancelToken(id: id)],
241-
"cancellationCancellables should not contain id \(id)"
188+
func testConcurrentCancels() {
189+
let queues = [
190+
DispatchQueue.main,
191+
DispatchQueue.global(qos: .background),
192+
DispatchQueue.global(qos: .default),
193+
DispatchQueue.global(qos: .unspecified),
194+
DispatchQueue.global(qos: .userInitiated),
195+
DispatchQueue.global(qos: .userInteractive),
196+
DispatchQueue.global(qos: .utility),
197+
]
198+
let ids = (1...10).map { _ in UUID() }
199+
200+
let effect = Effect.merge(
201+
(1...1_000).map { idx -> Effect<Int, Never> in
202+
let id = ids[idx % 10]
203+
204+
return Effect.merge(
205+
Just(idx)
206+
.delay(
207+
for: .milliseconds(Int.random(in: 1...100)), scheduler: queues.randomElement()!
208+
)
209+
.eraseToEffect()
210+
.cancellable(id: id),
211+
212+
Just(())
213+
.delay(
214+
for: .milliseconds(Int.random(in: 1...100)), scheduler: queues.randomElement()!
215+
)
216+
.flatMap { Effect.cancel(id: id) }
217+
.eraseToEffect()
242218
)
243219
}
220+
)
221+
222+
let expectation = self.expectation(description: "wait")
223+
effect
224+
.sink(receiveCompletion: { _ in expectation.fulfill() }, receiveValue: { _ in })
225+
.store(in: &self.cancellables)
226+
self.wait(for: [expectation], timeout: 999)
227+
228+
for id in ids {
229+
XCTAssertNil(
230+
_cancellationCancellables[_CancelToken(id: id)],
231+
"cancellationCancellables should not contain id \(id)"
232+
)
244233
}
245-
#endif
234+
}
246235

247-
#if DEBUG
248-
func testNestedCancels() {
249-
let id = UUID()
236+
func testNestedCancels() {
237+
let id = UUID()
250238

251-
var effect = Empty<Void, Never>(completeImmediately: false)
252-
.eraseToEffect()
253-
.cancellable(id: 1)
239+
var effect = Empty<Void, Never>(completeImmediately: false)
240+
.eraseToEffect()
241+
.cancellable(id: 1)
254242

255-
for _ in 1...1_000 {
256-
effect = effect.cancellable(id: id)
257-
}
243+
for _ in 1...1_000 {
244+
effect = effect.cancellable(id: id)
245+
}
258246

259-
effect
260-
.sink(receiveValue: { _ in })
261-
.store(in: &cancellables)
247+
effect
248+
.sink(receiveValue: { _ in })
249+
.store(in: &cancellables)
262250

263-
cancellables.removeAll()
251+
cancellables.removeAll()
264252

265-
XCTAssertNil(cancellationCancellables[CancelToken(id: id)])
266-
}
267-
#endif
253+
XCTAssertNil(_cancellationCancellables[_CancelToken(id: id)])
254+
}
268255

269256
func testSharedId() {
270257
let mainQueue = DispatchQueue.test

0 commit comments

Comments
 (0)