Skip to content

Commit 5df638d

Browse files
Add Python golden history replay fixtures
Issue: zorporation/durable-workflow#443 Loop-ID: build-01
1 parent db10086 commit 5df638d

2 files changed

Lines changed: 212 additions & 0 deletions

File tree

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
[
2+
{
3+
"name": "single_activity_completion",
4+
"workflow_type": "golden.single-activity",
5+
"start_input": ["Ada"],
6+
"history": [
7+
{
8+
"event_type": "ActivityCompleted",
9+
"payload": {
10+
"result": "\"hello Ada\""
11+
}
12+
}
13+
],
14+
"expected": {
15+
"command_type": "CompleteWorkflow",
16+
"result": "hello Ada"
17+
}
18+
},
19+
{
20+
"name": "signal_satisfies_wait_condition",
21+
"workflow_type": "golden.signal-wait",
22+
"start_input": [],
23+
"history": [
24+
{
25+
"event_type": "ConditionWaitOpened",
26+
"payload": {
27+
"condition_wait_id": "wait-approval-1",
28+
"condition_key": "approval",
29+
"timeout_seconds": 30
30+
}
31+
},
32+
{
33+
"event_type": "SignalReceived",
34+
"payload": {
35+
"signal_name": "approve",
36+
"arguments": "[\"alice\"]"
37+
}
38+
},
39+
{
40+
"event_type": "ConditionWaitSatisfied",
41+
"payload": {
42+
"condition_wait_id": "wait-approval-1"
43+
}
44+
},
45+
{
46+
"event_type": "ActivityCompleted",
47+
"payload": {
48+
"result": "\"notified\""
49+
}
50+
}
51+
],
52+
"expected": {
53+
"command_type": "CompleteWorkflow",
54+
"result": {
55+
"approved_by": "alice",
56+
"activity_result": "notified"
57+
}
58+
}
59+
},
60+
{
61+
"name": "wait_condition_timeout",
62+
"workflow_type": "golden.timeout-wait",
63+
"start_input": [],
64+
"history": [
65+
{
66+
"event_type": "ConditionWaitOpened",
67+
"payload": {
68+
"condition_wait_id": "wait-timeout-1",
69+
"condition_key": "approval",
70+
"timeout_seconds": 5
71+
}
72+
},
73+
{
74+
"event_type": "ConditionWaitTimedOut",
75+
"payload": {
76+
"condition_wait_id": "wait-timeout-1"
77+
}
78+
}
79+
],
80+
"expected": {
81+
"command_type": "CompleteWorkflow",
82+
"result": "timed-out"
83+
}
84+
},
85+
{
86+
"name": "version_marker_new_path",
87+
"workflow_type": "golden.version-marker",
88+
"start_input": [],
89+
"history": [
90+
{
91+
"event_type": "VersionMarkerRecorded",
92+
"payload": {
93+
"sequence": 1,
94+
"change_id": "golden-version",
95+
"version": 2,
96+
"min_supported": 1,
97+
"max_supported": 2
98+
}
99+
}
100+
],
101+
"expected": {
102+
"command_type": "ScheduleActivity",
103+
"activity_type": "golden.version-new-path",
104+
"arguments": []
105+
}
106+
}
107+
]
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
from __future__ import annotations
2+
3+
import json
4+
from pathlib import Path
5+
from typing import Any
6+
7+
import pytest
8+
9+
from durable_workflow import Replayer, workflow
10+
from durable_workflow.workflow import CompleteWorkflow, ScheduleActivity, WorkflowContext
11+
12+
FIXTURE_PATH = Path(__file__).parent / "fixtures" / "golden_history" / "python-sdk-v0.json"
13+
14+
15+
@workflow.defn(name="golden.single-activity")
16+
class GoldenSingleActivityWorkflow:
17+
def run(self, ctx: WorkflowContext, name: str): # type: ignore[no-untyped-def]
18+
return (yield ctx.schedule_activity("golden.greet", [name]))
19+
20+
21+
@workflow.defn(name="golden.signal-wait")
22+
class GoldenSignalWaitWorkflow:
23+
def __init__(self) -> None:
24+
self.approved_by: str | None = None
25+
26+
@workflow.signal("approve")
27+
def approve(self, approved_by: str) -> None:
28+
self.approved_by = approved_by
29+
30+
def run(self, ctx: WorkflowContext): # type: ignore[no-untyped-def]
31+
satisfied = yield ctx.wait_condition(
32+
lambda: self.approved_by is not None,
33+
key="approval",
34+
timeout=30,
35+
)
36+
if not satisfied:
37+
return "timed-out"
38+
39+
activity_result = yield ctx.schedule_activity("golden.notify", [self.approved_by])
40+
return {
41+
"approved_by": self.approved_by,
42+
"activity_result": activity_result,
43+
}
44+
45+
46+
@workflow.defn(name="golden.timeout-wait")
47+
class GoldenTimeoutWaitWorkflow:
48+
def run(self, ctx: WorkflowContext): # type: ignore[no-untyped-def]
49+
if not (yield ctx.wait_condition(lambda: False, key="approval", timeout=5)):
50+
return "timed-out"
51+
52+
return "satisfied"
53+
54+
55+
@workflow.defn(name="golden.version-marker")
56+
class GoldenVersionMarkerWorkflow:
57+
def run(self, ctx: WorkflowContext): # type: ignore[no-untyped-def]
58+
version = yield ctx.get_version("golden-version", 1, 2)
59+
activity = "golden.version-new-path" if version >= 2 else "golden.version-old-path"
60+
return (yield ctx.schedule_activity(activity, []))
61+
62+
63+
def _golden_cases() -> list[dict[str, Any]]:
64+
with FIXTURE_PATH.open() as handle:
65+
cases = json.load(handle)
66+
67+
assert isinstance(cases, list)
68+
return cases
69+
70+
71+
@pytest.mark.parametrize("case", _golden_cases(), ids=lambda case: str(case["name"]))
72+
def test_golden_history_replay_contract(case: dict[str, Any]) -> None:
73+
replayer = Replayer(
74+
workflows=[
75+
GoldenSingleActivityWorkflow,
76+
GoldenSignalWaitWorkflow,
77+
GoldenTimeoutWaitWorkflow,
78+
GoldenVersionMarkerWorkflow,
79+
],
80+
)
81+
82+
outcome = replayer.replay(
83+
case["history"],
84+
case["start_input"],
85+
workflow_type=case["workflow_type"],
86+
)
87+
88+
assert len(outcome.commands) == 1
89+
expected = case["expected"]
90+
command = outcome.commands[0]
91+
92+
if expected["command_type"] == "CompleteWorkflow":
93+
assert isinstance(command, CompleteWorkflow)
94+
assert command.result == expected["result"]
95+
96+
return
97+
98+
if expected["command_type"] == "ScheduleActivity":
99+
assert isinstance(command, ScheduleActivity)
100+
assert command.activity_type == expected["activity_type"]
101+
assert command.arguments == expected["arguments"]
102+
103+
return
104+
105+
raise AssertionError(f"Unsupported golden expected command type: {expected['command_type']}")

0 commit comments

Comments
 (0)