Skip to content

Commit 2828dc4

Browse files
authored
Runtime Warning Finesse (#1023)
1 parent 1efde6e commit 2828dc4

File tree

7 files changed

+49
-53
lines changed

7 files changed

+49
-53
lines changed

Sources/ComposableArchitecture/Debugging/ReducerInstrumentation.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,8 @@ func debugCaseOutput(_ value: Any) -> String {
110110
}
111111
}
112112

113-
return "\(type(of: value))\(debugCaseOutputHelp(value))"
113+
return (value as? CustomDebugStringConvertible)?.debugDescription
114+
?? "\(type(of: value))\(debugCaseOutputHelp(value))"
114115
}
115116

116117
private func isUnlabeledArgument(_ label: String) -> Bool {

Sources/ComposableArchitecture/Internal/Deprecations.swift

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,6 @@ import Combine
33
import SwiftUI
44
import XCTestDynamicOverlay
55

6-
#if DEBUG
7-
import os
8-
#endif
9-
106
// NB: Deprecated after 0.31.0:
117

128
extension Reducer {
@@ -548,8 +544,7 @@ extension Reducer {
548544
}
549545
if index >= globalState[keyPath: toLocalState].endIndex {
550546
#if DEBUG
551-
os_log(
552-
.fault, dso: rw.dso, log: rw.log,
547+
runtimeWarning(
553548
"""
554549
A "forEach" reducer at "%@:%d" received an action when state contained no element at \
555550
that index. …
@@ -563,21 +558,21 @@ extension Reducer {
563558
reasons:
564559
565560
• This "forEach" reducer was combined with or run from another reducer that removed \
566-
the element at this index when it handled this action. To fix this make sure that \
567-
this "forEach" reducer is run before any other reducers that can move or remove \
568-
elements from state. This ensures that "forEach" reducers can handle their actions \
569-
for the element at the intended index.
561+
the element at this index when it handled this action. To fix this make sure that this \
562+
"forEach" reducer is run before any other reducers that can move or remove elements \
563+
from state. This ensures that "forEach" reducers can handle their actions for the \
564+
element at the intended index.
570565
571566
• An in-flight effect emitted this action while state contained no element at this \
572567
index. While it may be perfectly reasonable to ignore this action, you may want to \
573568
cancel the associated effect when moving or removing an element. If your "forEach" \
574-
reducer returns any long-living effects, you should use the identifier-based \
575-
"forEach" instead.
569+
reducer returns any long-living effects, you should use the identifier-based "forEach" \
570+
instead.
576571
577-
• This action was sent to the store while its state contained no element at this \
578-
index. To fix this make sure that actions for this reducer can only be sent to a \
579-
view store when its state contains an element at this index. In SwiftUI \
580-
applications, use "ForEachStore".
572+
• This action was sent to the store while its state contained no element at this index \
573+
To fix this make sure that actions for this reducer can only be sent to a view store \
574+
when its state contains an element at this index. In SwiftUI applications, use \
575+
"ForEachStore".
581576
""",
582577
"\(file)",
583578
line,

Sources/ComposableArchitecture/Internal/RuntimeWarnings.swift

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
#if DEBUG
22
import os
3+
import XCTestDynamicOverlay
34

45
// NB: Xcode runtime warnings offer a much better experience than traditional assertions and
56
// breakpoints, but Apple provides no means of creating custom runtime warnings ourselves.
67
// To work around this, we hook into SwiftUI's runtime issue delivery mechanism, instead.
78
//
89
// Feedback filed: https://gist.github.com/stephencelis/a8d06383ed6ccde3e5ef5d1b3ad52bbc
9-
let rw = (
10+
private let rw = (
1011
dso: { () -> UnsafeMutableRawPointer in
1112
let count = _dyld_image_count()
1213
for i in 0..<count {
@@ -23,4 +24,15 @@
2324
}(),
2425
log: OSLog(subsystem: "com.apple.runtime-issues", category: "ComposableArchitecture")
2526
)
27+
28+
func runtimeWarning(
29+
_ message: StaticString,
30+
_ args: CVarArg...
31+
) {
32+
unsafeBitCast(
33+
os_log as (OSLogType, UnsafeRawPointer, OSLog, StaticString, CVarArg...) -> Void,
34+
to: ((OSLogType, UnsafeRawPointer, OSLog, StaticString, [CVarArg]) -> Void).self
35+
)(.fault, rw.dso, rw.log, message, args)
36+
XCTFail(String(format: "\(message)", arguments: args))
37+
}
2638
#endif

Sources/ComposableArchitecture/Reducer.swift

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
import CasePaths
22
import Combine
33

4-
#if DEBUG
5-
import os
6-
#endif
7-
84
/// A reducer describes how to evolve the current state of an application to the next state, given
95
/// an action, and describes what ``Effect``s should be executed later by the store, if any.
106
///
@@ -468,8 +464,7 @@ public struct Reducer<State, Action, Environment> {
468464

469465
guard var localState = toLocalState.extract(from: globalState) else {
470466
#if DEBUG
471-
os_log(
472-
.fault, dso: rw.dso, log: rw.log,
467+
runtimeWarning(
473468
"""
474469
A reducer pulled back from "%@:%d" received an action when local state was \
475470
unavailable. …
@@ -678,8 +673,7 @@ public struct Reducer<State, Action, Environment> {
678673
.init { state, action, environment in
679674
guard state != nil else {
680675
#if DEBUG
681-
os_log(
682-
.fault, dso: rw.dso, log: rw.log,
676+
runtimeWarning(
683677
"""
684678
An "optional" reducer at "%@:%d" received an action when state was "nil". …
685679
@@ -689,8 +683,8 @@ public struct Reducer<State, Action, Environment> {
689683
This is generally considered an application logic error, and can happen for a few \
690684
reasons:
691685
692-
• The optional reducer was combined with or run from another reducer that set \
693-
"%@" to "nil" before the optional reducer ran. Combine or run optional reducers before \
686+
• The optional reducer was combined with or run from another reducer that set "%@" to \
687+
"nil" before the optional reducer ran. Combine or run optional reducers before \
694688
reducers that can set their state to "nil". This ensures that optional reducers can \
695689
handle their actions while their state is still non-"nil".
696690
@@ -762,8 +756,7 @@ public struct Reducer<State, Action, Environment> {
762756
guard let (id, localAction) = toLocalAction.extract(from: globalAction) else { return .none }
763757
if globalState[keyPath: toLocalState][id: id] == nil {
764758
#if DEBUG
765-
os_log(
766-
.fault, dso: rw.dso, log: rw.log,
759+
runtimeWarning(
767760
"""
768761
A "forEach" reducer at "%@:%d" received an action when state contained no element with \
769762
that id. …
@@ -837,8 +830,7 @@ public struct Reducer<State, Action, Environment> {
837830

838831
if globalState[keyPath: toLocalState][key] == nil {
839832
#if DEBUG
840-
os_log(
841-
.fault, dso: rw.dso, log: rw.log,
833+
runtimeWarning(
842834
"""
843835
A "forEach" reducer at "%@:%d" received an action when state contained no value at \
844836
that key. …

Sources/ComposableArchitecture/Store.swift

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
import Combine
22
import Foundation
33

4-
#if DEBUG
5-
import os
6-
#endif
7-
84
/// A store represents the runtime that powers the application. It is the object that you will pass
95
/// around to views that need to interact with the application.
106
///
@@ -420,8 +416,7 @@ public final class Store<State, Action> {
420416

421417
switch status {
422418
case let .effectCompletion(action):
423-
os_log(
424-
.fault, dso: rw.dso, log: rw.log,
419+
runtimeWarning(
425420
"""
426421
An effect completed on a non-main thread. …
427422
@@ -440,8 +435,7 @@ public final class Store<State, Action> {
440435
)
441436

442437
case .`init`:
443-
os_log(
444-
.fault, dso: rw.dso, log: rw.log,
438+
runtimeWarning(
445439
"""
446440
A store initialized on a non-main thread. …
447441
@@ -455,8 +449,7 @@ public final class Store<State, Action> {
455449
)
456450

457451
case .scope:
458-
os_log(
459-
.fault, dso: rw.dso, log: rw.log,
452+
runtimeWarning(
460453
"""
461454
"Store.scope" was called on a non-main thread. …
462455
@@ -470,8 +463,7 @@ public final class Store<State, Action> {
470463
)
471464

472465
case let .send(action, originatingAction: nil):
473-
os_log(
474-
.fault, dso: rw.dso, log: rw.log,
466+
runtimeWarning(
475467
"""
476468
"ViewStore.send" was called on a non-main thread with: %@ …
477469
@@ -486,8 +478,7 @@ public final class Store<State, Action> {
486478
)
487479

488480
case let .send(action, originatingAction: .some(originatingAction)):
489-
os_log(
490-
.fault, dso: rw.dso, log: rw.log,
481+
runtimeWarning(
491482
"""
492483
An effect published an action on a non-main thread. …
493484

Sources/ComposableArchitecture/SwiftUI/SwitchStore.swift

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
11
import SwiftUI
22

3-
#if DEBUG
4-
import os
5-
#endif
6-
73
/// A view that can switch over a store of enum state and handle each case.
84
///
95
/// An application may model parts of its state with enums. For example, app state may differ if a
@@ -1201,8 +1197,7 @@ public struct _ExhaustivityCheckView<State, Action>: View {
12011197
.background(Color.red.edgesIgnoringSafeArea(.all))
12021198
.onAppear {
12031199
#if DEBUG
1204-
os_log(
1205-
.fault, dso: rw.dso, log: rw.log,
1200+
runtimeWarning(
12061201
"""
12071202
SwitchStore@%@:%d does not handle the current case. …
12081203

Sources/ComposableArchitecture/TestSupport/TestStore.swift

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,7 @@
288288
}
289289
}
290290

291-
private struct TestAction {
291+
private struct TestAction: CustomDebugStringConvertible {
292292
let origin: Origin
293293
let file: StaticString
294294
let line: UInt
@@ -297,6 +297,16 @@
297297
case send(LocalAction)
298298
case receive(Action)
299299
}
300+
301+
var debugDescription: String {
302+
switch self.origin {
303+
case let .send(action):
304+
return debugCaseOutput(action)
305+
306+
case let .receive(action):
307+
return debugCaseOutput(action)
308+
}
309+
}
300310
}
301311
}
302312

0 commit comments

Comments
 (0)