Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
46 changes: 17 additions & 29 deletions Sources/ComposableArchitecture/Internal/DispatchQueue.swift
Original file line number Diff line number Diff line change
@@ -1,36 +1,24 @@
import Dispatch
#if compiler(<6.2)
import Dispatch

func mainActorNow<R: Sendable>(execute block: @MainActor @Sendable () -> R) -> R {
if DispatchQueue.getSpecific(key: key) == value {
return MainActor._assumeIsolated {
block()
}
} else {
return DispatchQueue.main.sync {
MainActor._assumeIsolated {
func mainActorNow<R: Sendable>(execute block: @MainActor @Sendable () -> R) -> R {
if DispatchQueue.getSpecific(key: key) == value {
return MainActor._assumeIsolated {
block()
}
}
}
}

func mainActorASAP(execute block: @escaping @MainActor @Sendable () -> Void) {
if DispatchQueue.getSpecific(key: key) == value {
MainActor._assumeIsolated {
block()
}
} else {
DispatchQueue.main.async {
MainActor._assumeIsolated {
block()
} else {
return DispatchQueue.main.sync {
MainActor._assumeIsolated {
block()
}
}
}
}
}

private let key: DispatchSpecificKey<UInt8> = {
let key = DispatchSpecificKey<UInt8>()
DispatchQueue.main.setSpecific(key: key, value: value)
return key
}()
private let value: UInt8 = 0
private let key: DispatchSpecificKey<UInt8> = {
let key = DispatchSpecificKey<UInt8>()
DispatchQueue.main.setSpecific(key: key, value: value)
return key
}()
private let value: UInt8 = 0
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -100,25 +100,25 @@ extension BindingAction {
self.isInvalidated = isInvalidated
}
deinit {
let isInvalidated = mainActorNow(execute: isInvalidated)
guard !isInvalidated else { return }
guard wasCalled.value else {
guard !wasCalled.value
else { return }
Task { @MainActor [value, isInvalidated] in
guard !isInvalidated() else { return }
var valueDump: String {
var valueDump = ""
customDump(self.value, to: &valueDump, maxDepth: 0)
customDump(value, to: &valueDump, maxDepth: 0)
return valueDump
}
reportIssue(
"""
A binding action sent from a store was not handled. …

Action:
\(typeName(Action.self)).binding(.set(_, \(valueDump)))

To fix this, invoke "BindingReducer()" from your feature reducer's "body".
"""
)
return
}
}
}
Expand Down
21 changes: 13 additions & 8 deletions Sources/ComposableArchitecture/SwiftUI/Binding.swift
Original file line number Diff line number Diff line change
Expand Up @@ -770,22 +770,28 @@ extension WithViewStore where ViewState: Equatable, Content: View {
}

deinit {
let isInvalidated = mainActorNow(execute: isInvalidated)
guard !isInvalidated else { return }
guard self.wasCalled.value else {
guard !self.wasCalled.value
else { return }

Task {
@MainActor [
context, fileID, filePath, line, column, value, bindableActionType, isInvalidated
] in
let tmp = isInvalidated()
guard !tmp else { return }
var valueDump: String {
var valueDump = ""
customDump(self.value, to: &valueDump, maxDepth: 0)
customDump(value, to: &valueDump, maxDepth: 0)
return valueDump
}
reportIssue(
"""
A binding action sent from a store \
\(self.context == .bindingState ? "for binding state defined " : "")at \
"\(self.fileID):\(self.line)" was not handled. …
\(context == .bindingState ? "for binding state defined " : "")at \
"\(fileID):\(line)" was not handled. …

Action:
\(typeName(self.bindableActionType)).binding(.set(_, \(valueDump)))
\(typeName(bindableActionType)).binding(.set(_, \(valueDump)))

To fix this, invoke "BindingReducer()" from your feature reducer's "body".
""",
Expand All @@ -794,7 +800,6 @@ extension WithViewStore where ViewState: Equatable, Content: View {
line: line,
column: column
)
return
}
}
}
Expand Down
54 changes: 40 additions & 14 deletions Sources/ComposableArchitecture/TestStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,7 @@ import IssueReporting
#if swift(<5.10)
@MainActor(unsafe)
#else
@preconcurrency@MainActor
@preconcurrency @MainActor
#endif
public final class TestStore<State: Equatable, Action> {
/// The current dependencies of the test store.
Expand Down Expand Up @@ -576,7 +576,11 @@ public final class TestStore<State: Equatable, Action> {
column: UInt = #column
) async {
await self.finish(
timeout: duration.nanoseconds, fileID: fileID, file: filePath, line: line, column: column
timeout: duration.nanoseconds,
fileID: fileID,
file: filePath,
line: line,
column: column
)
}

Expand Down Expand Up @@ -642,14 +646,24 @@ public final class TestStore<State: Equatable, Action> {
self.assertNoSharedChanges(fileID: fileID, filePath: filePath, line: line, column: column)
}

deinit {
uncheckedUseMainSerialExecutor = self.originalUseMainSerialExecutor
mainActorNow { self.completed() }
}
#if compiler(>=6.2)
isolated deinit {
uncheckedUseMainSerialExecutor = originalUseMainSerialExecutor
completed()
}
#else
deinit {
uncheckedUseMainSerialExecutor = self.originalUseMainSerialExecutor
mainActorNow { self.completed() }
}
#endif

func completed() {
self.assertNoReceivedActions(
fileID: self.fileID, filePath: self.filePath, line: self.line, column: self.column
fileID: self.fileID,
filePath: self.filePath,
line: self.line,
column: self.column
)
Task.cancel(id: OnFirstAppearID())
for effect in self.reducer.inFlightEffects {
Expand Down Expand Up @@ -983,7 +997,11 @@ extension TestStore {
let previousStackElementID = self.reducer.dependencies.stackElementID.incrementingCopy()
let task = self.store.send(
.init(
origin: .send(action), fileID: fileID, filePath: filePath, line: line, column: column
origin: .send(action),
fileID: fileID,
filePath: filePath,
line: line,
column: column
)
)
if uncheckedUseMainSerialExecutor {
Expand Down Expand Up @@ -2379,7 +2397,11 @@ extension TestStore {
await Task.megaYield()
_ = {
self._skipReceivedActions(
strict: strict, fileID: fileID, file: filePath, line: line, column: column
strict: strict,
fileID: fileID,
file: filePath,
line: line,
column: column
)
}()
}
Expand Down Expand Up @@ -2464,7 +2486,11 @@ extension TestStore {
await Task.megaYield()
_ = {
self._skipInFlightEffects(
strict: strict, fileID: fileID, filePath: filePath, line: line, column: column
strict: strict,
fileID: fileID,
filePath: filePath,
line: line,
column: column
)
}()
}
Expand Down Expand Up @@ -2526,7 +2552,7 @@ extension TestStore {
switch exhaustivity {
case .on:
reportIssue(message, fileID: fileID, filePath: filePath, line: line, column: column)
case let .off(showSkippedAssertions):
case .off(let showSkippedAssertions):
if showSkippedAssertions {
withExpectedIssue {
reportIssue(
Expand Down Expand Up @@ -2833,11 +2859,11 @@ class TestReducer<State: Equatable, Action>: Reducer {

let effects: Effect<Action>
switch action.origin {
case let .send(action):
case .send(let action):
effects = reducer.reduce(into: &state, action: action)
self.state = state

case let .receive(action):
case .receive(let action):
effects = reducer.reduce(into: &state, action: action)
self.receivedActions.append((action, state))
}
Expand Down Expand Up @@ -2908,7 +2934,7 @@ class TestReducer<State: Equatable, Action>: Reducer {
case send(Action)
fileprivate var action: Action {
switch self {
case let .receive(action), let .send(action):
case .receive(let action), .send(let action):
return action
}
}
Expand Down
29 changes: 19 additions & 10 deletions Tests/ComposableArchitectureTests/RuntimeWarningTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

final class RuntimeWarningTests: BaseTCATestCase {
@MainActor
func testBindingUnhandledAction() {
func testBindingUnhandledAction() async throws {
let line = #line + 2
struct State: Equatable {
@BindingState var value = 0
Expand All @@ -16,8 +16,6 @@
let store = Store<State, Action>(initialState: State()) {}

XCTExpectFailure {
ViewStore(store, observe: { $0 }).$value.wrappedValue = 42
} issueMatcher: {
$0.compactDescription == """
failed - A binding action sent from a store for binding state defined at \
"\(#fileID):\(line)" was not handled. …
Expand All @@ -28,23 +26,26 @@
To fix this, invoke "BindingReducer()" from your feature reducer's "body".
"""
}

let viewStore = ViewStore(store, observe: { $0 })
viewStore.$value.wrappedValue = 42
try await Task.sleep(nanoseconds: 1_000_000)

}

@ObservableState
struct TestObservableBindingUnhandledActionState: Equatable {
var count = 0
}
@MainActor
func testObservableBindingUnhandledAction() {
func testObservableBindingUnhandledAction() async throws {
typealias State = TestObservableBindingUnhandledActionState
enum Action: BindableAction, Equatable {
case binding(BindingAction<State>)
}
let store = Store<State, Action>(initialState: State()) {}

XCTExpectFailure {
store.count = 42
} issueMatcher: {
$0.compactDescription == """
failed - A binding action sent from a store was not handled. …

Expand All @@ -54,10 +55,13 @@
To fix this, invoke "BindingReducer()" from your feature reducer's "body".
"""
}

store.count = 42
try await Task.sleep(nanoseconds: 1_000_000)
}

@MainActor
func testBindingUnhandledAction_BindingState() {
func testBindingUnhandledAction_BindingState() async throws {
struct State: Equatable {
@BindingState var value = 0
}
Expand All @@ -68,8 +72,6 @@
let store = Store<State, Action>(initialState: State()) {}

XCTExpectFailure {
ViewStore(store, observe: { $0 }).$value.wrappedValue = 42
} issueMatcher: {
$0.compactDescription == """
failed - A binding action sent from a store for binding state defined at \
"\(#fileID):\(line)" was not handled. …
Expand All @@ -80,6 +82,10 @@
To fix this, invoke "BindingReducer()" from your feature reducer's "body".
"""
}

let viewStore = ViewStore(store, observe: { $0 })
viewStore.$value.wrappedValue = 42
try await Task.sleep(nanoseconds: 1_000_000)
}

@Reducer
Expand All @@ -100,7 +106,10 @@

XCTExpectFailure {
store.scope(state: \.path, action: \.path)[
fileID: "file.swift", filePath: "/file.swift", line: 1, column: 1
fileID: "file.swift",
filePath: "/file.swift",
line: 1,
column: 1
] = .init()
} issueMatcher: {
$0.compactDescription == """
Expand Down
Loading