Skip to content

Commit d85fd67

Browse files
committed
[feat]: add swift-log dep & some more logging
1 parent 0061902 commit d85fd67

File tree

6 files changed

+101
-24
lines changed

6 files changed

+101
-24
lines changed

Package.swift

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,21 +61,28 @@ let package = Package(
6161
.package(url: "https://github.com/pointfreeco/swift-macro-testing", from: "0.4.0"),
6262
.package(url: "https://github.com/pointfreeco/swift-perception", "1.5.0" ..< "3.0.0"),
6363
.package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.3.3"),
64+
// 'xctest-dynamic-overlay' is actually for "IssueReporting"
6465
.package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.0.0"),
66+
.package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"),
6567
],
6668
targets: [
6769
// MARK: Workflow
6870

6971
.target(
7072
name: "Workflow",
71-
dependencies: ["ReactiveSwift"],
73+
dependencies: [
74+
.product(name: "IssueReporting", package: "xctest-dynamic-overlay"),
75+
.product(name: "Logging", package: "swift-log"),
76+
.product(name: "ReactiveSwift", package: "ReactiveSwift"),
77+
],
7278
path: "Workflow/Sources"
7379
),
7480
.target(
7581
name: "WorkflowTesting",
7682
dependencies: [
7783
"Workflow",
7884
.product(name: "CustomDump", package: "swift-custom-dump"),
85+
.product(name: "IssueReporting", package: "xctest-dynamic-overlay"),
7986
],
8087
path: "WorkflowTesting/Sources",
8188
linkerSettings: [.linkedFramework("XCTest")]
@@ -125,7 +132,10 @@ let package = Package(
125132

126133
.target(
127134
name: "WorkflowReactiveSwift",
128-
dependencies: ["ReactiveSwift", "Workflow"],
135+
dependencies: [
136+
"Workflow",
137+
.product(name: "ReactiveSwift", package: "ReactiveSwift"),
138+
],
129139
path: "WorkflowReactiveSwift/Sources"
130140
),
131141
.target(
@@ -139,7 +149,10 @@ let package = Package(
139149

140150
.target(
141151
name: "WorkflowRxSwift",
142-
dependencies: ["RxSwift", "Workflow"],
152+
dependencies: [
153+
"Workflow",
154+
.product(name: "RxSwift", package: "RxSwift"),
155+
],
143156
path: "WorkflowRxSwift/Sources"
144157
),
145158
.target(

Samples/Tuist/Package.resolved

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Workflow/Sources/RenderContext.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,10 @@ public class RenderContext<WorkflowType: Workflow>: RenderContextType {
153153
}
154154

155155
private func assertStillValid() {
156-
assert(isValid, "A `RenderContext` instance was used outside of the workflow's `render` method. It is a programmer error to capture a context in a closure or otherwise cause it to be used outside of the `render` method.")
156+
if !isValid {
157+
WorkflowLogger.logExternal(as: .error, "A `RenderContext` instance was used outside of the workflow's `render` method. It is a programmer error to capture a context in a closure or otherwise cause it to be used outside of the `render` method.")
158+
assertionFailure("A `RenderContext` instance was used outside of the workflow's `render` method. It is a programmer error to capture a context in a closure or otherwise cause it to be used outside of the `render` method.")
159+
}
157160
}
158161
}
159162
}

Workflow/Sources/SubtreeManager.swift

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616

1717
import Dispatch
1818

19+
#if DEBUG
20+
import IssueReporting
21+
#endif
22+
1923
extension WorkflowNode {
2024
/// Manages the subtree of a workflow. Specifically, this type encapsulates the logic required to update and manage
2125
/// the lifecycle of nested workflows across multiple render passes.
@@ -76,6 +80,18 @@ extension WorkflowNode {
7680

7781
wrapped.invalidate()
7882

83+
#if DEBUG
84+
var contextEscapeCheck = consume wrapped
85+
if !isKnownUniquelyReferenced(&contextEscapeCheck) {
86+
if let _ = TestContext.current {
87+
reportIssue("Detected escape of a RenderContext<\(WorkflowType.self)> instance from a `render()` method. A RenderContext is only valid to use within render(); Allowing it to escape can lead to incorrect behavior.")
88+
} else {
89+
assertionFailure("Detected escape of a RenderContext<\(WorkflowType.self)> instance from a `render()` method. A RenderContext is only valid to use within render(); Allowing it to escape can lead to incorrect behavior.")
90+
}
91+
}
92+
_ = consume contextEscapeCheck
93+
#endif
94+
7995
/// After the render is complete, assign children using *only the children that were used during the render
8096
/// pass.* This means that any pre-existing children that were *not* used during the render pass are removed
8197
/// as a result of this call to `render`.
@@ -409,27 +425,28 @@ extension WorkflowNode.SubtreeManager {
409425
case .valid(let handler):
410426
handler(event)
411427

412-
case .invalid:
428+
case .invalid where isReentrantCall:
413429
#if DEBUG
414430
// Reentrancy seems to often be due to UIKit behaviors over
415431
// which we have little control (e.g. synchronous resignation
416432
// of first responder after a new Rendering is assigned). Emit
417433
// some debug info in these cases.
418-
if isReentrantCall {
419-
print("[\(WorkflowType.self)]: ℹ️ Sink sent another action after it was invalidated but before its original action handling was resolved. This new action will be ignored. If this is unexpected, set a Swift error breakpoint on `\(InvalidSinkSentAction.self)` to debug.")
420-
}
421-
422-
do {
423-
throw InvalidSinkSentAction()
424-
} catch {}
434+
print("[\(WorkflowType.self)]: ℹ️ Sink sent another action after it was invalidated but before its original action handling was resolved. This new action will be ignored. If this is unexpected, set a Swift error breakpoint on `\(InvalidSinkSentAction.self)` to debug.")
435+
do { throw InvalidSinkSentAction() } catch {}
425436
#endif
426437

438+
case .invalid:
427439
// If we're invalid and this is the first time `handle()` has
428440
// been called, then it's likely we've somehow been inadvertently
429441
// retained from the 'outside world'. Fail more loudly in this case.
430-
assert(isReentrantCall, """
431-
[\(WorkflowType.self)]: Sink sent an action after it was invalidated. This action will be ignored.
432-
""")
442+
if !isReentrantCall {
443+
WorkflowLogger.logExternal(as: .warning, """
444+
[\(WorkflowType.self)]: Sink sent an action after it was invalidated. This action will be ignored.
445+
""")
446+
assertionFailure("""
447+
[\(WorkflowType.self)]: Sink sent an action after it was invalidated. This action will be ignored.
448+
""")
449+
}
433450
}
434451
}
435452

Workflow/Sources/WorkflowLogger.swift

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616

1717
import Foundation
18+
import Logging
1819
import os.signpost
1920

2021
extension OSLog {
@@ -207,3 +208,33 @@ enum WorkflowLogger {
207208
}
208209
}
209210
}
211+
212+
// MARK: - External Logging
213+
214+
typealias EternalLogger = Logging.Logger
215+
216+
extension WorkflowLogger {
217+
static func logExternal(
218+
as level: EternalLogger.Level,
219+
_ message: @autoclosure () -> EternalLogger.Message,
220+
metadata: @autoclosure () -> EternalLogger.Metadata? = nil,
221+
source: @autoclosure () -> String? = nil,
222+
file: String = #fileID,
223+
function: String = #function,
224+
line: UInt = #line
225+
) {
226+
EternalLogger.workflow.log(
227+
level: level,
228+
message(),
229+
metadata: metadata(),
230+
source: source(),
231+
file: file,
232+
function: function,
233+
line: line
234+
)
235+
}
236+
}
237+
238+
extension EternalLogger {
239+
static let workflow = Logger(label: "com.squareup.workflow")
240+
}

Workflow/Tests/SubtreeManagerTests.swift

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@
1414
* limitations under the License.
1515
*/
1616

17+
import IssueReporting
1718
import ReactiveSwift
1819
import XCTest
20+
1921
@testable import Workflow
2022

2123
final class SubtreeManagerTests: XCTestCase {
@@ -126,16 +128,18 @@ final class SubtreeManagerTests: XCTestCase {
126128

127129
var escapingContext: RenderContext<ParentWorkflow>!
128130

129-
_ = manager.render { context -> TestViewModel in
130-
XCTAssertTrue(context.isValid)
131-
escapingContext = context
132-
return context.render(
133-
workflow: TestWorkflow(),
134-
key: "",
135-
outputMap: { _ in AnyWorkflowAction.noAction }
136-
)
131+
withExpectedIssue {
132+
_ = manager.render { context -> TestViewModel in
133+
XCTAssertTrue(context.isValid)
134+
escapingContext = context
135+
return context.render(
136+
workflow: TestWorkflow(),
137+
key: "",
138+
outputMap: { _ in AnyWorkflowAction.noAction }
139+
)
140+
}
141+
manager.enableEvents()
137142
}
138-
manager.enableEvents()
139143

140144
XCTAssertFalse(escapingContext.isValid)
141145
}

0 commit comments

Comments
 (0)