Skip to content

Commit

Permalink
Fixed runSideEffect key in WorkflowConcurrency Worker (#202)
Browse files Browse the repository at this point in the history
* Fixed runSideEffect key in Worker

Changed WorkerWorkflow to use it’s state as the key for the runSideEffect call so that if the state changes the worker is re-run.
Updated unit tests to test for this issue of the side effect not running.
Added unit tests for testing that updating the key in the render call causes the worker to run.

* Changed initial state to be more obvious

Per CR changed intial state of the updated workflow to be a more obvious state change
  • Loading branch information
mjohnson12 authored Apr 5, 2023
1 parent 3e703ce commit 5292ee8
Show file tree
Hide file tree
Showing 2 changed files with 130 additions and 9 deletions.
2 changes: 1 addition & 1 deletion WorkflowConcurrency/Sources/Worker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ struct WorkerWorkflow<WorkerType: Worker>: Workflow {
func render(state: State, context: RenderContext<WorkerWorkflow>) -> Rendering {
let logger = WorkerLogger<WorkerType>()
let sink = context.makeOutputSink()
context.runSideEffect(key: "") { lifetime in
context.runSideEffect(key: state) { lifetime in
let send: @MainActor(Output) -> Void = sink.send
let task = Task {
logger.logStarted()
Expand Down
137 changes: 129 additions & 8 deletions WorkflowConcurrency/Tests/WorkerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import XCTest
class WorkerTests: XCTestCase {
func testWorkerOutput() {
let host = WorkflowHost(
workflow: TaskTestWorkerWorkflow(key: "")
workflow: TaskTestWorkerWorkflow(key: "", initialState: 0)
)

let expectation = XCTestExpectation()
Expand All @@ -38,8 +38,120 @@ class WorkerTests: XCTestCase {
disposable?.dispose()
}

func testWorkflowUpdate() {
// Create the workflow which causes the TaskTestWorker to run.
let host = WorkflowHost(
workflow: TaskTestWorkerWorkflow(key: "", initialState: 0)
)

var expectation = XCTestExpectation()
// Set to observe renderings
// This expectation should be called after the TaskTestWorker runs and
// updates the state.
var disposable = host.rendering.signal.observeValues { rendering in
expectation.fulfill()
}

// Test to make sure the initial state of the workflow is correct.
XCTAssertEqual(0, host.rendering.value)

// Wait for the worker to run.
wait(for: [expectation], timeout: 1.0)
// Test to make sure the rendering after the worker runs is correct.
XCTAssertEqual(1, host.rendering.value)

disposable?.dispose()

expectation = XCTestExpectation()
// Set to observe renderings
// This expectation should be called after the workflow is updated.
// After the host is updated with a new workflow instance the
// initial state should be 1.
disposable = host.rendering.signal.observeValues { rendering in
expectation.fulfill()
}

// Updated the workflow to a new initial state.
host.update(workflow: TaskTestWorkerWorkflow(key: "", initialState: 7))

// Wait for the workflow to render after being updated.
wait(for: [expectation], timeout: 1.0)
// Test to make sure the rendering matches the initial state.
XCTAssertEqual(7, host.rendering.value)

expectation = XCTestExpectation()
// Set to observe renderings
// This expectation should be called when the worker runs.
// The worker isEquivalent is false because we have changed the initialState.
disposable = host.rendering.signal.observeValues { rendering in
expectation.fulfill()
}

// Wait for the worker to trigger a rendering.
wait(for: [expectation], timeout: 1.0)
// Check to make sure the rendering is correct.
XCTAssertEqual(8, host.rendering.value)
}

func testWorkflowKeyChange() {
// Create the workflow which causes the TaskTestWorker to run.
let host = WorkflowHost(
workflow: TaskTestWorkerWorkflow(key: "", initialState: 0)
)

var expectation = XCTestExpectation()
// Set to observe renderings
// This expectation should be called after the TaskTestWorker runs and
// updates the state.
var disposable = host.rendering.signal.observeValues { rendering in
expectation.fulfill()
}

// Test to make sure the initial state of the workflow is correct.
XCTAssertEqual(0, host.rendering.value)

// Wait for the worker to run.
wait(for: [expectation], timeout: 1.0)
// Test to make sure the rendering after the worker runs is correct.
XCTAssertEqual(1, host.rendering.value)

disposable?.dispose()

expectation = XCTestExpectation()
// Set to observe renderings
// This expectation should be called after the workflow is updated.
// After the host is updated with a new workflow instance the
// initial state should be 1.
disposable = host.rendering.signal.observeValues { rendering in
expectation.fulfill()
}

// Update the workflow to a new key which should force the worker to run.
host.update(workflow: TaskTestWorkerWorkflow(key: "key", initialState: 0))

// Wait for the workflow to render after being updated.
wait(for: [expectation], timeout: 1.0)
// Test to make sure the rendering matches the existing state
// since the inititalState didn't change.
XCTAssertEqual(1, host.rendering.value)

expectation = XCTestExpectation()
// Set to observe renderings
// This expectation should be called when the worker runs.
// The worker should run because the key was changed for the workflow.
disposable = host.rendering.signal.observeValues { rendering in
expectation.fulfill()
}

// Wait for the worker to trigger a rendering.
wait(for: [expectation], timeout: 1.0)
// Check to make sure the rendering is correct.
// The worker adds one to the initialState so this should be 1.
XCTAssertEqual(1, host.rendering.value)
}

func testExpectedWorker() {
TaskTestWorkerWorkflow(key: "123")
TaskTestWorkerWorkflow(key: "123", initialState: 0)
.renderTester()
.expectWorkflow(
type: WorkerWorkflow<TaskTestWorker>.self,
Expand Down Expand Up @@ -127,11 +239,11 @@ private struct TaskTestWorkerWorkflow: Workflow {
typealias Rendering = Int

let key: String

func makeInitialState() -> Int { 0 }
let initialState: Int
func makeInitialState() -> Int { initialState }

func render(state: Int, context: RenderContext<TaskTestWorkerWorkflow>) -> Int {
TaskTestWorker()
TaskTestWorker(initialState: initialState)
.mapOutput { output in
AnyWorkflowAction { state in
state = output
Expand All @@ -141,18 +253,27 @@ private struct TaskTestWorkerWorkflow: Workflow {
.running(in: context, key: key)
return state
}

func workflowDidChange(from previousWorkflow: TaskTestWorkerWorkflow, state: inout Int) {
if previousWorkflow.initialState != initialState {
state = initialState
}
}
}

private struct TaskTestWorker: Worker {
typealias Output = Int

let initialState: Int

func run() async -> Int {
do {
try await Task.sleep(nanoseconds: 10000000)
} catch {}

return 1
return initialState + 1
}

func isEquivalent(to otherWorker: TaskTestWorker) -> Bool { true }
func isEquivalent(to otherWorker: TaskTestWorker) -> Bool {
return otherWorker.initialState == initialState
}
}

0 comments on commit 5292ee8

Please sign in to comment.