pyworkflow-engine 0.1.7__py3-none-any.whl → 0.1.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyworkflow/__init__.py +10 -1
- pyworkflow/celery/tasks.py +272 -24
- pyworkflow/cli/__init__.py +4 -1
- pyworkflow/cli/commands/runs.py +4 -4
- pyworkflow/cli/commands/setup.py +203 -4
- pyworkflow/cli/utils/config_generator.py +76 -3
- pyworkflow/cli/utils/docker_manager.py +232 -0
- pyworkflow/context/__init__.py +13 -0
- pyworkflow/context/base.py +26 -0
- pyworkflow/context/local.py +80 -0
- pyworkflow/context/step_context.py +295 -0
- pyworkflow/core/registry.py +6 -1
- pyworkflow/core/step.py +141 -0
- pyworkflow/core/workflow.py +56 -0
- pyworkflow/engine/events.py +30 -0
- pyworkflow/engine/replay.py +39 -0
- pyworkflow/primitives/child_workflow.py +1 -1
- pyworkflow/runtime/local.py +1 -1
- pyworkflow/storage/__init__.py +14 -0
- pyworkflow/storage/base.py +35 -0
- pyworkflow/storage/cassandra.py +1747 -0
- pyworkflow/storage/config.py +69 -0
- pyworkflow/storage/dynamodb.py +31 -2
- pyworkflow/storage/file.py +28 -0
- pyworkflow/storage/memory.py +18 -0
- pyworkflow/storage/mysql.py +1159 -0
- pyworkflow/storage/postgres.py +27 -2
- pyworkflow/storage/schemas.py +4 -3
- pyworkflow/storage/sqlite.py +25 -2
- {pyworkflow_engine-0.1.7.dist-info → pyworkflow_engine-0.1.9.dist-info}/METADATA +7 -4
- pyworkflow_engine-0.1.9.dist-info/RECORD +91 -0
- pyworkflow_engine-0.1.9.dist-info/top_level.txt +1 -0
- dashboard/backend/app/__init__.py +0 -1
- dashboard/backend/app/config.py +0 -32
- dashboard/backend/app/controllers/__init__.py +0 -6
- dashboard/backend/app/controllers/run_controller.py +0 -86
- dashboard/backend/app/controllers/workflow_controller.py +0 -33
- dashboard/backend/app/dependencies/__init__.py +0 -5
- dashboard/backend/app/dependencies/storage.py +0 -50
- dashboard/backend/app/repositories/__init__.py +0 -6
- dashboard/backend/app/repositories/run_repository.py +0 -80
- dashboard/backend/app/repositories/workflow_repository.py +0 -27
- dashboard/backend/app/rest/__init__.py +0 -8
- dashboard/backend/app/rest/v1/__init__.py +0 -12
- dashboard/backend/app/rest/v1/health.py +0 -33
- dashboard/backend/app/rest/v1/runs.py +0 -133
- dashboard/backend/app/rest/v1/workflows.py +0 -41
- dashboard/backend/app/schemas/__init__.py +0 -23
- dashboard/backend/app/schemas/common.py +0 -16
- dashboard/backend/app/schemas/event.py +0 -24
- dashboard/backend/app/schemas/hook.py +0 -25
- dashboard/backend/app/schemas/run.py +0 -54
- dashboard/backend/app/schemas/step.py +0 -28
- dashboard/backend/app/schemas/workflow.py +0 -31
- dashboard/backend/app/server.py +0 -87
- dashboard/backend/app/services/__init__.py +0 -6
- dashboard/backend/app/services/run_service.py +0 -240
- dashboard/backend/app/services/workflow_service.py +0 -155
- dashboard/backend/main.py +0 -18
- docs/concepts/cancellation.mdx +0 -362
- docs/concepts/continue-as-new.mdx +0 -434
- docs/concepts/events.mdx +0 -266
- docs/concepts/fault-tolerance.mdx +0 -370
- docs/concepts/hooks.mdx +0 -552
- docs/concepts/limitations.mdx +0 -167
- docs/concepts/schedules.mdx +0 -775
- docs/concepts/sleep.mdx +0 -312
- docs/concepts/steps.mdx +0 -301
- docs/concepts/workflows.mdx +0 -255
- docs/guides/cli.mdx +0 -942
- docs/guides/configuration.mdx +0 -560
- docs/introduction.mdx +0 -155
- docs/quickstart.mdx +0 -279
- examples/__init__.py +0 -1
- examples/celery/__init__.py +0 -1
- examples/celery/durable/docker-compose.yml +0 -55
- examples/celery/durable/pyworkflow.config.yaml +0 -12
- examples/celery/durable/workflows/__init__.py +0 -122
- examples/celery/durable/workflows/basic.py +0 -87
- examples/celery/durable/workflows/batch_processing.py +0 -102
- examples/celery/durable/workflows/cancellation.py +0 -273
- examples/celery/durable/workflows/child_workflow_patterns.py +0 -240
- examples/celery/durable/workflows/child_workflows.py +0 -202
- examples/celery/durable/workflows/continue_as_new.py +0 -260
- examples/celery/durable/workflows/fault_tolerance.py +0 -210
- examples/celery/durable/workflows/hooks.py +0 -211
- examples/celery/durable/workflows/idempotency.py +0 -112
- examples/celery/durable/workflows/long_running.py +0 -99
- examples/celery/durable/workflows/retries.py +0 -101
- examples/celery/durable/workflows/schedules.py +0 -209
- examples/celery/transient/01_basic_workflow.py +0 -91
- examples/celery/transient/02_fault_tolerance.py +0 -257
- examples/celery/transient/__init__.py +0 -20
- examples/celery/transient/pyworkflow.config.yaml +0 -25
- examples/local/__init__.py +0 -1
- examples/local/durable/01_basic_workflow.py +0 -94
- examples/local/durable/02_file_storage.py +0 -132
- examples/local/durable/03_retries.py +0 -169
- examples/local/durable/04_long_running.py +0 -119
- examples/local/durable/05_event_log.py +0 -145
- examples/local/durable/06_idempotency.py +0 -148
- examples/local/durable/07_hooks.py +0 -334
- examples/local/durable/08_cancellation.py +0 -233
- examples/local/durable/09_child_workflows.py +0 -198
- examples/local/durable/10_child_workflow_patterns.py +0 -265
- examples/local/durable/11_continue_as_new.py +0 -249
- examples/local/durable/12_schedules.py +0 -198
- examples/local/durable/__init__.py +0 -1
- examples/local/transient/01_quick_tasks.py +0 -87
- examples/local/transient/02_retries.py +0 -130
- examples/local/transient/03_sleep.py +0 -141
- examples/local/transient/__init__.py +0 -1
- pyworkflow_engine-0.1.7.dist-info/RECORD +0 -196
- pyworkflow_engine-0.1.7.dist-info/top_level.txt +0 -5
- tests/examples/__init__.py +0 -0
- tests/integration/__init__.py +0 -0
- tests/integration/test_cancellation.py +0 -330
- tests/integration/test_child_workflows.py +0 -439
- tests/integration/test_continue_as_new.py +0 -428
- tests/integration/test_dynamodb_storage.py +0 -1146
- tests/integration/test_fault_tolerance.py +0 -369
- tests/integration/test_schedule_storage.py +0 -484
- tests/unit/__init__.py +0 -0
- tests/unit/backends/__init__.py +0 -1
- tests/unit/backends/test_dynamodb_storage.py +0 -1554
- tests/unit/backends/test_postgres_storage.py +0 -1281
- tests/unit/backends/test_sqlite_storage.py +0 -1460
- tests/unit/conftest.py +0 -41
- tests/unit/test_cancellation.py +0 -364
- tests/unit/test_child_workflows.py +0 -680
- tests/unit/test_continue_as_new.py +0 -441
- tests/unit/test_event_limits.py +0 -316
- tests/unit/test_executor.py +0 -320
- tests/unit/test_fault_tolerance.py +0 -334
- tests/unit/test_hooks.py +0 -495
- tests/unit/test_registry.py +0 -261
- tests/unit/test_replay.py +0 -420
- tests/unit/test_schedule_schemas.py +0 -285
- tests/unit/test_schedule_utils.py +0 -286
- tests/unit/test_scheduled_workflow.py +0 -274
- tests/unit/test_step.py +0 -353
- tests/unit/test_workflow.py +0 -243
- {pyworkflow_engine-0.1.7.dist-info → pyworkflow_engine-0.1.9.dist-info}/WHEEL +0 -0
- {pyworkflow_engine-0.1.7.dist-info → pyworkflow_engine-0.1.9.dist-info}/entry_points.txt +0 -0
- {pyworkflow_engine-0.1.7.dist-info → pyworkflow_engine-0.1.9.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,1460 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Unit tests for SQLite storage backend.
|
|
3
|
-
|
|
4
|
-
These tests verify the SQLiteStorageBackend implementation.
|
|
5
|
-
For integration tests with a real SQLite database, see tests/integration/.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from contextlib import asynccontextmanager
|
|
9
|
-
from datetime import UTC, datetime
|
|
10
|
-
from pathlib import Path
|
|
11
|
-
from unittest.mock import AsyncMock, MagicMock, patch
|
|
12
|
-
|
|
13
|
-
import pytest
|
|
14
|
-
|
|
15
|
-
from pyworkflow.engine.events import Event, EventType
|
|
16
|
-
from pyworkflow.storage.schemas import (
|
|
17
|
-
Hook,
|
|
18
|
-
HookStatus,
|
|
19
|
-
OverlapPolicy,
|
|
20
|
-
RunStatus,
|
|
21
|
-
Schedule,
|
|
22
|
-
ScheduleSpec,
|
|
23
|
-
ScheduleStatus,
|
|
24
|
-
StepExecution,
|
|
25
|
-
StepStatus,
|
|
26
|
-
WorkflowRun,
|
|
27
|
-
)
|
|
28
|
-
from pyworkflow.storage.sqlite import SQLiteStorageBackend
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
@pytest.fixture
|
|
32
|
-
def mock_backend(tmp_path):
|
|
33
|
-
"""Create a backend with mocked connection for testing."""
|
|
34
|
-
backend = SQLiteStorageBackend(db_path=str(tmp_path / "test.db"))
|
|
35
|
-
mock_conn = MagicMock()
|
|
36
|
-
backend._db = mock_conn
|
|
37
|
-
backend._initialized = True
|
|
38
|
-
return backend, mock_conn
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def create_mock_cursor(fetchone_result=None, fetchall_result=None):
|
|
42
|
-
"""Helper to create a mock cursor that works as async context manager."""
|
|
43
|
-
mock_cursor = AsyncMock()
|
|
44
|
-
mock_cursor.fetchone = AsyncMock(return_value=fetchone_result)
|
|
45
|
-
mock_cursor.fetchall = AsyncMock(return_value=fetchall_result or [])
|
|
46
|
-
return mock_cursor
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
class TestSQLiteStorageBackendInit:
|
|
50
|
-
"""Test SQLiteStorageBackend initialization."""
|
|
51
|
-
|
|
52
|
-
def test_init_with_default_path(self):
|
|
53
|
-
"""Test initialization with default database path."""
|
|
54
|
-
backend = SQLiteStorageBackend()
|
|
55
|
-
|
|
56
|
-
assert backend.db_path == Path("./pyworkflow_data/pyworkflow.db")
|
|
57
|
-
assert backend._db is None
|
|
58
|
-
assert backend._initialized is False
|
|
59
|
-
|
|
60
|
-
def test_init_with_custom_path(self, tmp_path):
|
|
61
|
-
"""Test initialization with custom database path."""
|
|
62
|
-
custom_path = tmp_path / "custom" / "path" / "db.sqlite"
|
|
63
|
-
backend = SQLiteStorageBackend(db_path=str(custom_path))
|
|
64
|
-
|
|
65
|
-
assert backend.db_path == custom_path
|
|
66
|
-
assert backend._db is None
|
|
67
|
-
assert backend._initialized is False
|
|
68
|
-
|
|
69
|
-
def test_init_creates_parent_directory(self, tmp_path):
|
|
70
|
-
"""Test that initialization creates parent directories."""
|
|
71
|
-
nested_path = tmp_path / "nested" / "dir" / "test.db"
|
|
72
|
-
_backend = SQLiteStorageBackend(db_path=str(nested_path)) # noqa: F841
|
|
73
|
-
|
|
74
|
-
# Parent should be created during __init__
|
|
75
|
-
assert nested_path.parent.exists()
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
class TestSQLiteStorageBackendConnection:
|
|
79
|
-
"""Test connection management."""
|
|
80
|
-
|
|
81
|
-
@pytest.mark.asyncio
|
|
82
|
-
async def test_ensure_connected_raises_when_not_connected(self):
|
|
83
|
-
"""Test that _ensure_connected raises when connection is None."""
|
|
84
|
-
backend = SQLiteStorageBackend()
|
|
85
|
-
|
|
86
|
-
with pytest.raises(RuntimeError, match="Database not connected"):
|
|
87
|
-
backend._ensure_connected()
|
|
88
|
-
|
|
89
|
-
@pytest.mark.asyncio
|
|
90
|
-
async def test_connect_creates_connection(self, tmp_path):
|
|
91
|
-
"""Test that connect creates a database connection."""
|
|
92
|
-
backend = SQLiteStorageBackend(db_path=str(tmp_path / "test.db"))
|
|
93
|
-
|
|
94
|
-
mock_conn = AsyncMock()
|
|
95
|
-
|
|
96
|
-
async def mock_connect(*args, **kwargs):
|
|
97
|
-
return mock_conn
|
|
98
|
-
|
|
99
|
-
with patch("aiosqlite.connect", side_effect=mock_connect) as mock_aiosqlite:
|
|
100
|
-
# Mock the schema initialization
|
|
101
|
-
backend._initialize_schema = AsyncMock()
|
|
102
|
-
|
|
103
|
-
await backend.connect()
|
|
104
|
-
|
|
105
|
-
mock_aiosqlite.assert_called_once()
|
|
106
|
-
assert backend._db is not None
|
|
107
|
-
assert backend._initialized is True
|
|
108
|
-
|
|
109
|
-
@pytest.mark.asyncio
|
|
110
|
-
async def test_disconnect_closes_connection(self):
|
|
111
|
-
"""Test that disconnect closes the connection."""
|
|
112
|
-
backend = SQLiteStorageBackend()
|
|
113
|
-
mock_conn = AsyncMock()
|
|
114
|
-
backend._db = mock_conn
|
|
115
|
-
backend._initialized = True
|
|
116
|
-
|
|
117
|
-
await backend.disconnect()
|
|
118
|
-
|
|
119
|
-
mock_conn.close.assert_called_once()
|
|
120
|
-
assert backend._db is None
|
|
121
|
-
assert backend._initialized is False
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
class TestRowConversion:
|
|
125
|
-
"""Test row to object conversion methods."""
|
|
126
|
-
|
|
127
|
-
def test_row_to_workflow_run(self):
|
|
128
|
-
"""Test converting database row to WorkflowRun."""
|
|
129
|
-
backend = SQLiteStorageBackend()
|
|
130
|
-
|
|
131
|
-
# SQLite returns tuples in column order
|
|
132
|
-
row = (
|
|
133
|
-
"run_123", # 0: run_id
|
|
134
|
-
"test_workflow", # 1: workflow_name
|
|
135
|
-
"running", # 2: status
|
|
136
|
-
"2024-01-01T12:00:00+00:00", # 3: created_at
|
|
137
|
-
"2024-01-01T12:00:01+00:00", # 4: updated_at
|
|
138
|
-
"2024-01-01T12:00:00+00:00", # 5: started_at
|
|
139
|
-
None, # 6: completed_at
|
|
140
|
-
"[]", # 7: input_args
|
|
141
|
-
'{"key": "value"}', # 8: input_kwargs
|
|
142
|
-
None, # 9: result
|
|
143
|
-
None, # 10: error
|
|
144
|
-
"idem_123", # 11: idempotency_key
|
|
145
|
-
"1h", # 12: max_duration
|
|
146
|
-
'{"foo": "bar"}', # 13: metadata
|
|
147
|
-
0, # 14: recovery_attempts
|
|
148
|
-
3, # 15: max_recovery_attempts
|
|
149
|
-
1, # 16: recover_on_worker_loss (SQLite stores as int)
|
|
150
|
-
None, # 17: parent_run_id
|
|
151
|
-
0, # 18: nesting_depth
|
|
152
|
-
None, # 19: continued_from_run_id
|
|
153
|
-
None, # 20: continued_to_run_id
|
|
154
|
-
)
|
|
155
|
-
|
|
156
|
-
run = backend._row_to_workflow_run(row)
|
|
157
|
-
|
|
158
|
-
assert run.run_id == "run_123"
|
|
159
|
-
assert run.workflow_name == "test_workflow"
|
|
160
|
-
assert run.status == RunStatus.RUNNING
|
|
161
|
-
assert run.idempotency_key == "idem_123"
|
|
162
|
-
assert run.metadata == {"foo": "bar"}
|
|
163
|
-
assert run.recover_on_worker_loss is True
|
|
164
|
-
|
|
165
|
-
def test_row_to_event(self):
|
|
166
|
-
"""Test converting database row to Event."""
|
|
167
|
-
backend = SQLiteStorageBackend()
|
|
168
|
-
|
|
169
|
-
row = (
|
|
170
|
-
"event_123", # 0: event_id
|
|
171
|
-
"run_123", # 1: run_id
|
|
172
|
-
5, # 2: sequence
|
|
173
|
-
"step.completed", # 3: type
|
|
174
|
-
"2024-01-01T12:00:00+00:00", # 4: timestamp
|
|
175
|
-
'{"step_id": "step_1"}', # 5: data
|
|
176
|
-
)
|
|
177
|
-
|
|
178
|
-
event = backend._row_to_event(row)
|
|
179
|
-
|
|
180
|
-
assert event.event_id == "event_123"
|
|
181
|
-
assert event.run_id == "run_123"
|
|
182
|
-
assert event.sequence == 5
|
|
183
|
-
assert event.type == EventType.STEP_COMPLETED
|
|
184
|
-
assert event.data == {"step_id": "step_1"}
|
|
185
|
-
|
|
186
|
-
def test_row_to_step_execution(self):
|
|
187
|
-
"""Test converting database row to StepExecution."""
|
|
188
|
-
backend = SQLiteStorageBackend()
|
|
189
|
-
|
|
190
|
-
row = (
|
|
191
|
-
"step_123", # 0: step_id
|
|
192
|
-
"run_123", # 1: run_id
|
|
193
|
-
"process_data", # 2: step_name
|
|
194
|
-
"completed", # 3: status
|
|
195
|
-
"2024-01-01T12:00:00+00:00", # 4: created_at
|
|
196
|
-
"2024-01-01T12:00:01+00:00", # 5: started_at
|
|
197
|
-
"2024-01-01T12:00:05+00:00", # 6: completed_at
|
|
198
|
-
"[]", # 7: input_args
|
|
199
|
-
"{}", # 8: input_kwargs
|
|
200
|
-
'"success"', # 9: result
|
|
201
|
-
None, # 10: error
|
|
202
|
-
2, # 11: retry_count (0-based in DB)
|
|
203
|
-
)
|
|
204
|
-
|
|
205
|
-
step = backend._row_to_step_execution(row)
|
|
206
|
-
|
|
207
|
-
assert step.step_id == "step_123"
|
|
208
|
-
assert step.step_name == "process_data"
|
|
209
|
-
assert step.status == StepStatus.COMPLETED
|
|
210
|
-
# retry_count 2 -> attempt 3 (1-based)
|
|
211
|
-
assert step.attempt == 3
|
|
212
|
-
|
|
213
|
-
def test_row_to_hook(self):
|
|
214
|
-
"""Test converting database row to Hook."""
|
|
215
|
-
backend = SQLiteStorageBackend()
|
|
216
|
-
|
|
217
|
-
row = (
|
|
218
|
-
"hook_123", # 0: hook_id
|
|
219
|
-
"run_123", # 1: run_id
|
|
220
|
-
"token_abc", # 2: token
|
|
221
|
-
"2024-01-01T12:00:00+00:00", # 3: created_at
|
|
222
|
-
None, # 4: received_at
|
|
223
|
-
"2024-01-02T12:00:00+00:00", # 5: expires_at
|
|
224
|
-
"pending", # 6: status
|
|
225
|
-
None, # 7: payload
|
|
226
|
-
'{"webhook": true}', # 8: metadata
|
|
227
|
-
)
|
|
228
|
-
|
|
229
|
-
hook = backend._row_to_hook(row)
|
|
230
|
-
|
|
231
|
-
assert hook.hook_id == "hook_123"
|
|
232
|
-
assert hook.token == "token_abc"
|
|
233
|
-
assert hook.status == HookStatus.PENDING
|
|
234
|
-
assert hook.metadata == {"webhook": True}
|
|
235
|
-
|
|
236
|
-
def test_row_to_schedule(self):
|
|
237
|
-
"""Test converting database row to Schedule."""
|
|
238
|
-
backend = SQLiteStorageBackend()
|
|
239
|
-
|
|
240
|
-
row = (
|
|
241
|
-
"sched_123", # 0: schedule_id
|
|
242
|
-
"daily_report", # 1: workflow_name
|
|
243
|
-
"0 9 * * *", # 2: spec (cron expression)
|
|
244
|
-
"cron", # 3: spec_type
|
|
245
|
-
"UTC", # 4: timezone
|
|
246
|
-
"[]", # 5: input_args
|
|
247
|
-
"{}", # 6: input_kwargs
|
|
248
|
-
"active", # 7: status
|
|
249
|
-
"skip", # 8: overlap_policy
|
|
250
|
-
"2024-01-02T09:00:00+00:00", # 9: next_run_time
|
|
251
|
-
"2024-01-01T09:00:00+00:00", # 10: last_run_time
|
|
252
|
-
'["run_1", "run_2"]', # 11: running_run_ids
|
|
253
|
-
"{}", # 12: metadata
|
|
254
|
-
"2024-01-01T00:00:00+00:00", # 13: created_at
|
|
255
|
-
"2024-01-01T09:00:00+00:00", # 14: updated_at
|
|
256
|
-
None, # 15: paused_at
|
|
257
|
-
None, # 16: deleted_at
|
|
258
|
-
)
|
|
259
|
-
|
|
260
|
-
schedule = backend._row_to_schedule(row)
|
|
261
|
-
|
|
262
|
-
assert schedule.schedule_id == "sched_123"
|
|
263
|
-
assert schedule.workflow_name == "daily_report"
|
|
264
|
-
assert schedule.spec.cron == "0 9 * * *"
|
|
265
|
-
assert schedule.spec.timezone == "UTC"
|
|
266
|
-
assert schedule.status == ScheduleStatus.ACTIVE
|
|
267
|
-
assert schedule.overlap_policy == OverlapPolicy.SKIP
|
|
268
|
-
assert schedule.running_run_ids == ["run_1", "run_2"]
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
class TestSQLiteStorageBackendConfig:
|
|
272
|
-
"""Test storage configuration integration."""
|
|
273
|
-
|
|
274
|
-
def test_storage_to_config(self, tmp_path):
|
|
275
|
-
"""Test serializing backend to config."""
|
|
276
|
-
from pyworkflow.storage.config import storage_to_config
|
|
277
|
-
|
|
278
|
-
db_path = tmp_path / "db.sqlite"
|
|
279
|
-
backend = SQLiteStorageBackend(db_path=str(db_path))
|
|
280
|
-
config = storage_to_config(backend)
|
|
281
|
-
|
|
282
|
-
assert config["type"] == "sqlite"
|
|
283
|
-
# Config module serializes db_path as base_path
|
|
284
|
-
assert config["base_path"] == str(db_path)
|
|
285
|
-
|
|
286
|
-
def test_config_to_storage(self, tmp_path):
|
|
287
|
-
"""Test creating backend from config."""
|
|
288
|
-
from pyworkflow.storage.config import config_to_storage
|
|
289
|
-
|
|
290
|
-
db_path = tmp_path / "db.sqlite"
|
|
291
|
-
# Config module uses base_path key for SQLite
|
|
292
|
-
config = {"type": "sqlite", "base_path": str(db_path)}
|
|
293
|
-
backend = config_to_storage(config)
|
|
294
|
-
|
|
295
|
-
assert isinstance(backend, SQLiteStorageBackend)
|
|
296
|
-
assert str(backend.db_path) == str(db_path)
|
|
297
|
-
|
|
298
|
-
def test_storage_to_config_with_default_path(self, tmp_path):
|
|
299
|
-
"""Test serializing backend with default path to config."""
|
|
300
|
-
from pyworkflow.storage.config import storage_to_config
|
|
301
|
-
|
|
302
|
-
db_path = tmp_path / "default.db"
|
|
303
|
-
backend = SQLiteStorageBackend(db_path=str(db_path))
|
|
304
|
-
config = storage_to_config(backend)
|
|
305
|
-
|
|
306
|
-
assert config["type"] == "sqlite"
|
|
307
|
-
# Config module uses base_path key
|
|
308
|
-
assert "base_path" in config
|
|
309
|
-
|
|
310
|
-
def test_config_to_storage_with_default(self):
|
|
311
|
-
"""Test creating backend from minimal config."""
|
|
312
|
-
from pyworkflow.storage.config import config_to_storage
|
|
313
|
-
|
|
314
|
-
config = {"type": "sqlite"}
|
|
315
|
-
backend = config_to_storage(config)
|
|
316
|
-
|
|
317
|
-
assert isinstance(backend, SQLiteStorageBackend)
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
class TestWorkflowRunOperations:
|
|
321
|
-
"""Test workflow run CRUD operations."""
|
|
322
|
-
|
|
323
|
-
@pytest.mark.asyncio
|
|
324
|
-
async def test_create_run(self, mock_backend):
|
|
325
|
-
"""Test creating a workflow run."""
|
|
326
|
-
backend, mock_conn = mock_backend
|
|
327
|
-
mock_conn.execute = AsyncMock()
|
|
328
|
-
mock_conn.commit = AsyncMock()
|
|
329
|
-
|
|
330
|
-
run = WorkflowRun(
|
|
331
|
-
run_id="run_123",
|
|
332
|
-
workflow_name="test_workflow",
|
|
333
|
-
status=RunStatus.PENDING,
|
|
334
|
-
)
|
|
335
|
-
|
|
336
|
-
await backend.create_run(run)
|
|
337
|
-
|
|
338
|
-
mock_conn.execute.assert_called_once()
|
|
339
|
-
call_args = mock_conn.execute.call_args
|
|
340
|
-
assert "INSERT INTO workflow_runs" in call_args[0][0]
|
|
341
|
-
assert call_args[0][1][0] == "run_123"
|
|
342
|
-
mock_conn.commit.assert_called_once()
|
|
343
|
-
|
|
344
|
-
@pytest.mark.asyncio
|
|
345
|
-
async def test_get_run_found(self, mock_backend):
|
|
346
|
-
"""Test retrieving an existing workflow run."""
|
|
347
|
-
backend, mock_conn = mock_backend
|
|
348
|
-
|
|
349
|
-
row = (
|
|
350
|
-
"run_123", # run_id
|
|
351
|
-
"test_workflow", # workflow_name
|
|
352
|
-
"running", # status
|
|
353
|
-
"2024-01-01T12:00:00+00:00", # created_at
|
|
354
|
-
"2024-01-01T12:00:01+00:00", # updated_at
|
|
355
|
-
None, # started_at
|
|
356
|
-
None, # completed_at
|
|
357
|
-
"[]", # input_args
|
|
358
|
-
"{}", # input_kwargs
|
|
359
|
-
None, # result
|
|
360
|
-
None, # error
|
|
361
|
-
None, # idempotency_key
|
|
362
|
-
None, # max_duration
|
|
363
|
-
"{}", # metadata
|
|
364
|
-
0, # recovery_attempts
|
|
365
|
-
3, # max_recovery_attempts
|
|
366
|
-
1, # recover_on_worker_loss
|
|
367
|
-
None, # parent_run_id
|
|
368
|
-
0, # nesting_depth
|
|
369
|
-
None, # continued_from_run_id
|
|
370
|
-
None, # continued_to_run_id
|
|
371
|
-
)
|
|
372
|
-
|
|
373
|
-
mock_cursor = create_mock_cursor(fetchone_result=row)
|
|
374
|
-
|
|
375
|
-
@asynccontextmanager
|
|
376
|
-
async def mock_execute(*args, **kwargs):
|
|
377
|
-
yield mock_cursor
|
|
378
|
-
|
|
379
|
-
mock_conn.execute = mock_execute
|
|
380
|
-
|
|
381
|
-
run = await backend.get_run("run_123")
|
|
382
|
-
|
|
383
|
-
assert run is not None
|
|
384
|
-
assert run.run_id == "run_123"
|
|
385
|
-
assert run.status == RunStatus.RUNNING
|
|
386
|
-
mock_cursor.fetchone.assert_called_once()
|
|
387
|
-
|
|
388
|
-
@pytest.mark.asyncio
|
|
389
|
-
async def test_get_run_not_found(self, mock_backend):
|
|
390
|
-
"""Test retrieving a non-existent workflow run."""
|
|
391
|
-
backend, mock_conn = mock_backend
|
|
392
|
-
|
|
393
|
-
mock_cursor = create_mock_cursor(fetchone_result=None)
|
|
394
|
-
|
|
395
|
-
@asynccontextmanager
|
|
396
|
-
async def mock_execute(*args, **kwargs):
|
|
397
|
-
yield mock_cursor
|
|
398
|
-
|
|
399
|
-
mock_conn.execute = mock_execute
|
|
400
|
-
|
|
401
|
-
run = await backend.get_run("nonexistent")
|
|
402
|
-
|
|
403
|
-
assert run is None
|
|
404
|
-
|
|
405
|
-
@pytest.mark.asyncio
|
|
406
|
-
async def test_get_run_by_idempotency_key(self, mock_backend):
|
|
407
|
-
"""Test retrieving workflow run by idempotency key."""
|
|
408
|
-
backend, mock_conn = mock_backend
|
|
409
|
-
|
|
410
|
-
row = (
|
|
411
|
-
"run_123",
|
|
412
|
-
"test_workflow",
|
|
413
|
-
"completed",
|
|
414
|
-
"2024-01-01T12:00:00+00:00",
|
|
415
|
-
"2024-01-01T12:00:01+00:00",
|
|
416
|
-
None,
|
|
417
|
-
None,
|
|
418
|
-
"[]",
|
|
419
|
-
"{}",
|
|
420
|
-
None,
|
|
421
|
-
None,
|
|
422
|
-
"idem_key_123",
|
|
423
|
-
None,
|
|
424
|
-
"{}",
|
|
425
|
-
0,
|
|
426
|
-
3,
|
|
427
|
-
1,
|
|
428
|
-
None,
|
|
429
|
-
0,
|
|
430
|
-
None,
|
|
431
|
-
None,
|
|
432
|
-
)
|
|
433
|
-
|
|
434
|
-
mock_cursor = create_mock_cursor(fetchone_result=row)
|
|
435
|
-
|
|
436
|
-
@asynccontextmanager
|
|
437
|
-
async def mock_execute(*args, **kwargs):
|
|
438
|
-
yield mock_cursor
|
|
439
|
-
|
|
440
|
-
mock_conn.execute = mock_execute
|
|
441
|
-
|
|
442
|
-
run = await backend.get_run_by_idempotency_key("idem_key_123")
|
|
443
|
-
|
|
444
|
-
assert run is not None
|
|
445
|
-
assert run.idempotency_key == "idem_key_123"
|
|
446
|
-
|
|
447
|
-
@pytest.mark.asyncio
|
|
448
|
-
async def test_update_run_status(self, mock_backend):
|
|
449
|
-
"""Test updating workflow run status."""
|
|
450
|
-
backend, mock_conn = mock_backend
|
|
451
|
-
mock_conn.execute = AsyncMock()
|
|
452
|
-
mock_conn.commit = AsyncMock()
|
|
453
|
-
|
|
454
|
-
await backend.update_run_status("run_123", RunStatus.COMPLETED, result='"done"', error=None)
|
|
455
|
-
|
|
456
|
-
mock_conn.execute.assert_called_once()
|
|
457
|
-
call_args = mock_conn.execute.call_args
|
|
458
|
-
assert "UPDATE workflow_runs" in call_args[0][0]
|
|
459
|
-
assert "status" in call_args[0][0]
|
|
460
|
-
mock_conn.commit.assert_called_once()
|
|
461
|
-
|
|
462
|
-
@pytest.mark.asyncio
|
|
463
|
-
async def test_update_run_recovery_attempts(self, mock_backend):
|
|
464
|
-
"""Test updating recovery attempts counter."""
|
|
465
|
-
backend, mock_conn = mock_backend
|
|
466
|
-
mock_conn.execute = AsyncMock()
|
|
467
|
-
mock_conn.commit = AsyncMock()
|
|
468
|
-
|
|
469
|
-
await backend.update_run_recovery_attempts("run_123", 2)
|
|
470
|
-
|
|
471
|
-
mock_conn.execute.assert_called_once()
|
|
472
|
-
call_args = mock_conn.execute.call_args
|
|
473
|
-
assert "recovery_attempts" in call_args[0][0]
|
|
474
|
-
mock_conn.commit.assert_called_once()
|
|
475
|
-
|
|
476
|
-
@pytest.mark.asyncio
|
|
477
|
-
async def test_list_runs(self, mock_backend):
|
|
478
|
-
"""Test listing workflow runs."""
|
|
479
|
-
backend, mock_conn = mock_backend
|
|
480
|
-
|
|
481
|
-
row = (
|
|
482
|
-
"run_1",
|
|
483
|
-
"test_workflow",
|
|
484
|
-
"completed",
|
|
485
|
-
"2024-01-01T12:00:00+00:00",
|
|
486
|
-
"2024-01-01T12:00:01+00:00",
|
|
487
|
-
None,
|
|
488
|
-
None,
|
|
489
|
-
"[]",
|
|
490
|
-
"{}",
|
|
491
|
-
None,
|
|
492
|
-
None,
|
|
493
|
-
None,
|
|
494
|
-
None,
|
|
495
|
-
"{}",
|
|
496
|
-
0,
|
|
497
|
-
3,
|
|
498
|
-
1,
|
|
499
|
-
None,
|
|
500
|
-
0,
|
|
501
|
-
None,
|
|
502
|
-
None,
|
|
503
|
-
)
|
|
504
|
-
|
|
505
|
-
mock_cursor = create_mock_cursor(fetchall_result=[row])
|
|
506
|
-
|
|
507
|
-
@asynccontextmanager
|
|
508
|
-
async def mock_execute(*args, **kwargs):
|
|
509
|
-
yield mock_cursor
|
|
510
|
-
|
|
511
|
-
mock_conn.execute = mock_execute
|
|
512
|
-
|
|
513
|
-
runs, cursor = await backend.list_runs(limit=10)
|
|
514
|
-
|
|
515
|
-
assert len(runs) == 1
|
|
516
|
-
assert runs[0].run_id == "run_1"
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
class TestEventOperations:
|
|
520
|
-
"""Test event log operations."""
|
|
521
|
-
|
|
522
|
-
@pytest.mark.asyncio
|
|
523
|
-
async def test_record_event(self, mock_backend):
|
|
524
|
-
"""Test recording an event."""
|
|
525
|
-
backend, mock_conn = mock_backend
|
|
526
|
-
|
|
527
|
-
# Mock for sequence fetch - this is used with async with
|
|
528
|
-
mock_cursor_seq = create_mock_cursor(fetchone_result=(0,))
|
|
529
|
-
|
|
530
|
-
# Track which call is being made
|
|
531
|
-
call_count = [0]
|
|
532
|
-
execute_calls = []
|
|
533
|
-
|
|
534
|
-
# Create a mock that works both as context manager and awaitable
|
|
535
|
-
class MockExecuteResult:
|
|
536
|
-
def __init__(self, cursor):
|
|
537
|
-
self._cursor = cursor
|
|
538
|
-
|
|
539
|
-
async def __aenter__(self):
|
|
540
|
-
return self._cursor
|
|
541
|
-
|
|
542
|
-
async def __aexit__(self, *args):
|
|
543
|
-
pass
|
|
544
|
-
|
|
545
|
-
def __await__(self):
|
|
546
|
-
async def _noop():
|
|
547
|
-
return None
|
|
548
|
-
|
|
549
|
-
return _noop().__await__()
|
|
550
|
-
|
|
551
|
-
def mock_execute(sql, params=None):
|
|
552
|
-
call_count[0] += 1
|
|
553
|
-
execute_calls.append((sql, params))
|
|
554
|
-
if "SELECT" in sql:
|
|
555
|
-
return MockExecuteResult(mock_cursor_seq)
|
|
556
|
-
else:
|
|
557
|
-
return MockExecuteResult(AsyncMock())
|
|
558
|
-
|
|
559
|
-
mock_conn.execute = mock_execute
|
|
560
|
-
mock_conn.commit = AsyncMock()
|
|
561
|
-
|
|
562
|
-
event = Event(
|
|
563
|
-
event_id="event_123",
|
|
564
|
-
run_id="run_123",
|
|
565
|
-
type=EventType.WORKFLOW_STARTED,
|
|
566
|
-
timestamp=datetime.now(UTC),
|
|
567
|
-
data={"key": "value"},
|
|
568
|
-
)
|
|
569
|
-
|
|
570
|
-
await backend.record_event(event)
|
|
571
|
-
|
|
572
|
-
mock_conn.commit.assert_called_once()
|
|
573
|
-
# Should have called execute twice: once for SELECT, once for INSERT
|
|
574
|
-
assert len(execute_calls) == 2
|
|
575
|
-
assert "INSERT INTO events" in execute_calls[1][0]
|
|
576
|
-
|
|
577
|
-
@pytest.mark.asyncio
|
|
578
|
-
async def test_get_events(self, mock_backend):
|
|
579
|
-
"""Test retrieving events for a workflow run."""
|
|
580
|
-
backend, mock_conn = mock_backend
|
|
581
|
-
|
|
582
|
-
rows = [
|
|
583
|
-
(
|
|
584
|
-
"event_1",
|
|
585
|
-
"run_123",
|
|
586
|
-
0,
|
|
587
|
-
"workflow.started",
|
|
588
|
-
"2024-01-01T12:00:00+00:00",
|
|
589
|
-
"{}",
|
|
590
|
-
),
|
|
591
|
-
(
|
|
592
|
-
"event_2",
|
|
593
|
-
"run_123",
|
|
594
|
-
1,
|
|
595
|
-
"step.completed",
|
|
596
|
-
"2024-01-01T12:00:01+00:00",
|
|
597
|
-
'{"step_id": "step_1"}',
|
|
598
|
-
),
|
|
599
|
-
]
|
|
600
|
-
|
|
601
|
-
mock_cursor = create_mock_cursor(fetchall_result=rows)
|
|
602
|
-
|
|
603
|
-
@asynccontextmanager
|
|
604
|
-
async def mock_execute(*args, **kwargs):
|
|
605
|
-
yield mock_cursor
|
|
606
|
-
|
|
607
|
-
mock_conn.execute = mock_execute
|
|
608
|
-
|
|
609
|
-
events = await backend.get_events("run_123")
|
|
610
|
-
|
|
611
|
-
assert len(events) == 2
|
|
612
|
-
assert events[0].type == EventType.WORKFLOW_STARTED
|
|
613
|
-
assert events[1].type == EventType.STEP_COMPLETED
|
|
614
|
-
|
|
615
|
-
@pytest.mark.asyncio
|
|
616
|
-
async def test_get_latest_event(self, mock_backend):
|
|
617
|
-
"""Test retrieving the latest event."""
|
|
618
|
-
backend, mock_conn = mock_backend
|
|
619
|
-
|
|
620
|
-
row = (
|
|
621
|
-
"event_5",
|
|
622
|
-
"run_123",
|
|
623
|
-
5,
|
|
624
|
-
"step.completed",
|
|
625
|
-
"2024-01-01T12:00:05+00:00",
|
|
626
|
-
"{}",
|
|
627
|
-
)
|
|
628
|
-
|
|
629
|
-
mock_cursor = create_mock_cursor(fetchone_result=row)
|
|
630
|
-
|
|
631
|
-
@asynccontextmanager
|
|
632
|
-
async def mock_execute(*args, **kwargs):
|
|
633
|
-
yield mock_cursor
|
|
634
|
-
|
|
635
|
-
mock_conn.execute = mock_execute
|
|
636
|
-
|
|
637
|
-
event = await backend.get_latest_event("run_123")
|
|
638
|
-
|
|
639
|
-
assert event is not None
|
|
640
|
-
assert event.sequence == 5
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
class TestStepOperations:
|
|
644
|
-
"""Test step execution operations."""
|
|
645
|
-
|
|
646
|
-
@pytest.mark.asyncio
|
|
647
|
-
async def test_create_step(self, mock_backend):
|
|
648
|
-
"""Test creating a step execution record."""
|
|
649
|
-
backend, mock_conn = mock_backend
|
|
650
|
-
mock_conn.execute = AsyncMock()
|
|
651
|
-
mock_conn.commit = AsyncMock()
|
|
652
|
-
|
|
653
|
-
step = StepExecution(
|
|
654
|
-
step_id="step_123",
|
|
655
|
-
run_id="run_123",
|
|
656
|
-
step_name="process_data",
|
|
657
|
-
status=StepStatus.PENDING,
|
|
658
|
-
)
|
|
659
|
-
|
|
660
|
-
await backend.create_step(step)
|
|
661
|
-
|
|
662
|
-
mock_conn.execute.assert_called_once()
|
|
663
|
-
call_args = mock_conn.execute.call_args
|
|
664
|
-
assert "INSERT INTO steps" in call_args[0][0]
|
|
665
|
-
mock_conn.commit.assert_called_once()
|
|
666
|
-
|
|
667
|
-
@pytest.mark.asyncio
|
|
668
|
-
async def test_get_step_found(self, mock_backend):
|
|
669
|
-
"""Test retrieving an existing step."""
|
|
670
|
-
backend, mock_conn = mock_backend
|
|
671
|
-
|
|
672
|
-
row = (
|
|
673
|
-
"step_123",
|
|
674
|
-
"run_123",
|
|
675
|
-
"process_data",
|
|
676
|
-
"completed",
|
|
677
|
-
"2024-01-01T12:00:00+00:00",
|
|
678
|
-
"2024-01-01T12:00:01+00:00",
|
|
679
|
-
"2024-01-01T12:00:05+00:00",
|
|
680
|
-
"[]",
|
|
681
|
-
"{}",
|
|
682
|
-
'"success"',
|
|
683
|
-
None,
|
|
684
|
-
1, # retry_count (0-based)
|
|
685
|
-
)
|
|
686
|
-
|
|
687
|
-
mock_cursor = create_mock_cursor(fetchone_result=row)
|
|
688
|
-
|
|
689
|
-
@asynccontextmanager
|
|
690
|
-
async def mock_execute(*args, **kwargs):
|
|
691
|
-
yield mock_cursor
|
|
692
|
-
|
|
693
|
-
mock_conn.execute = mock_execute
|
|
694
|
-
|
|
695
|
-
step = await backend.get_step("step_123")
|
|
696
|
-
|
|
697
|
-
assert step is not None
|
|
698
|
-
assert step.step_id == "step_123"
|
|
699
|
-
assert step.status == StepStatus.COMPLETED
|
|
700
|
-
|
|
701
|
-
@pytest.mark.asyncio
|
|
702
|
-
async def test_get_step_not_found(self, mock_backend):
|
|
703
|
-
"""Test retrieving a non-existent step."""
|
|
704
|
-
backend, mock_conn = mock_backend
|
|
705
|
-
|
|
706
|
-
mock_cursor = create_mock_cursor(fetchone_result=None)
|
|
707
|
-
|
|
708
|
-
@asynccontextmanager
|
|
709
|
-
async def mock_execute(*args, **kwargs):
|
|
710
|
-
yield mock_cursor
|
|
711
|
-
|
|
712
|
-
mock_conn.execute = mock_execute
|
|
713
|
-
|
|
714
|
-
step = await backend.get_step("nonexistent")
|
|
715
|
-
|
|
716
|
-
assert step is None
|
|
717
|
-
|
|
718
|
-
@pytest.mark.asyncio
|
|
719
|
-
async def test_update_step_status(self, mock_backend):
|
|
720
|
-
"""Test updating step execution status."""
|
|
721
|
-
backend, mock_conn = mock_backend
|
|
722
|
-
mock_conn.execute = AsyncMock()
|
|
723
|
-
mock_conn.commit = AsyncMock()
|
|
724
|
-
|
|
725
|
-
await backend.update_step_status("step_123", StepStatus.COMPLETED, result='"done"')
|
|
726
|
-
|
|
727
|
-
mock_conn.execute.assert_called_once()
|
|
728
|
-
call_args = mock_conn.execute.call_args
|
|
729
|
-
assert "UPDATE steps" in call_args[0][0]
|
|
730
|
-
mock_conn.commit.assert_called_once()
|
|
731
|
-
|
|
732
|
-
@pytest.mark.asyncio
|
|
733
|
-
async def test_list_steps(self, mock_backend):
|
|
734
|
-
"""Test listing steps for a workflow run."""
|
|
735
|
-
backend, mock_conn = mock_backend
|
|
736
|
-
|
|
737
|
-
row = (
|
|
738
|
-
"step_1",
|
|
739
|
-
"run_123",
|
|
740
|
-
"step_one",
|
|
741
|
-
"completed",
|
|
742
|
-
"2024-01-01T12:00:00+00:00",
|
|
743
|
-
None,
|
|
744
|
-
None,
|
|
745
|
-
"[]",
|
|
746
|
-
"{}",
|
|
747
|
-
None,
|
|
748
|
-
None,
|
|
749
|
-
1,
|
|
750
|
-
)
|
|
751
|
-
|
|
752
|
-
mock_cursor = create_mock_cursor(fetchall_result=[row])
|
|
753
|
-
|
|
754
|
-
@asynccontextmanager
|
|
755
|
-
async def mock_execute(*args, **kwargs):
|
|
756
|
-
yield mock_cursor
|
|
757
|
-
|
|
758
|
-
mock_conn.execute = mock_execute
|
|
759
|
-
|
|
760
|
-
steps = await backend.list_steps("run_123")
|
|
761
|
-
|
|
762
|
-
assert len(steps) == 1
|
|
763
|
-
assert steps[0].step_id == "step_1"
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
class TestHookOperations:
|
|
767
|
-
"""Test hook operations."""
|
|
768
|
-
|
|
769
|
-
@pytest.mark.asyncio
|
|
770
|
-
async def test_create_hook(self, mock_backend):
|
|
771
|
-
"""Test creating a hook record."""
|
|
772
|
-
backend, mock_conn = mock_backend
|
|
773
|
-
mock_conn.execute = AsyncMock()
|
|
774
|
-
mock_conn.commit = AsyncMock()
|
|
775
|
-
|
|
776
|
-
hook = Hook(
|
|
777
|
-
hook_id="hook_123",
|
|
778
|
-
run_id="run_123",
|
|
779
|
-
token="token_abc",
|
|
780
|
-
)
|
|
781
|
-
|
|
782
|
-
await backend.create_hook(hook)
|
|
783
|
-
|
|
784
|
-
mock_conn.execute.assert_called_once()
|
|
785
|
-
call_args = mock_conn.execute.call_args
|
|
786
|
-
assert "INSERT INTO hooks" in call_args[0][0]
|
|
787
|
-
mock_conn.commit.assert_called_once()
|
|
788
|
-
|
|
789
|
-
@pytest.mark.asyncio
|
|
790
|
-
async def test_get_hook_found(self, mock_backend):
|
|
791
|
-
"""Test retrieving an existing hook."""
|
|
792
|
-
backend, mock_conn = mock_backend
|
|
793
|
-
|
|
794
|
-
row = (
|
|
795
|
-
"hook_123",
|
|
796
|
-
"run_123",
|
|
797
|
-
"token_abc",
|
|
798
|
-
"2024-01-01T12:00:00+00:00",
|
|
799
|
-
None,
|
|
800
|
-
None,
|
|
801
|
-
"pending",
|
|
802
|
-
None,
|
|
803
|
-
"{}",
|
|
804
|
-
)
|
|
805
|
-
|
|
806
|
-
mock_cursor = create_mock_cursor(fetchone_result=row)
|
|
807
|
-
|
|
808
|
-
@asynccontextmanager
|
|
809
|
-
async def mock_execute(*args, **kwargs):
|
|
810
|
-
yield mock_cursor
|
|
811
|
-
|
|
812
|
-
mock_conn.execute = mock_execute
|
|
813
|
-
|
|
814
|
-
hook = await backend.get_hook("hook_123")
|
|
815
|
-
|
|
816
|
-
assert hook is not None
|
|
817
|
-
assert hook.hook_id == "hook_123"
|
|
818
|
-
assert hook.status == HookStatus.PENDING
|
|
819
|
-
|
|
820
|
-
@pytest.mark.asyncio
|
|
821
|
-
async def test_get_hook_not_found(self, mock_backend):
|
|
822
|
-
"""Test retrieving a non-existent hook."""
|
|
823
|
-
backend, mock_conn = mock_backend
|
|
824
|
-
|
|
825
|
-
mock_cursor = create_mock_cursor(fetchone_result=None)
|
|
826
|
-
|
|
827
|
-
@asynccontextmanager
|
|
828
|
-
async def mock_execute(*args, **kwargs):
|
|
829
|
-
yield mock_cursor
|
|
830
|
-
|
|
831
|
-
mock_conn.execute = mock_execute
|
|
832
|
-
|
|
833
|
-
hook = await backend.get_hook("nonexistent")
|
|
834
|
-
|
|
835
|
-
assert hook is None
|
|
836
|
-
|
|
837
|
-
@pytest.mark.asyncio
|
|
838
|
-
async def test_get_hook_by_token(self, mock_backend):
|
|
839
|
-
"""Test retrieving a hook by token."""
|
|
840
|
-
backend, mock_conn = mock_backend
|
|
841
|
-
|
|
842
|
-
row = (
|
|
843
|
-
"hook_123",
|
|
844
|
-
"run_123",
|
|
845
|
-
"token_abc",
|
|
846
|
-
"2024-01-01T12:00:00+00:00",
|
|
847
|
-
None,
|
|
848
|
-
None,
|
|
849
|
-
"pending",
|
|
850
|
-
None,
|
|
851
|
-
"{}",
|
|
852
|
-
)
|
|
853
|
-
|
|
854
|
-
mock_cursor = create_mock_cursor(fetchone_result=row)
|
|
855
|
-
|
|
856
|
-
@asynccontextmanager
|
|
857
|
-
async def mock_execute(*args, **kwargs):
|
|
858
|
-
yield mock_cursor
|
|
859
|
-
|
|
860
|
-
mock_conn.execute = mock_execute
|
|
861
|
-
|
|
862
|
-
hook = await backend.get_hook_by_token("token_abc")
|
|
863
|
-
|
|
864
|
-
assert hook is not None
|
|
865
|
-
assert hook.token == "token_abc"
|
|
866
|
-
|
|
867
|
-
@pytest.mark.asyncio
|
|
868
|
-
async def test_update_hook_status(self, mock_backend):
|
|
869
|
-
"""Test updating hook status."""
|
|
870
|
-
backend, mock_conn = mock_backend
|
|
871
|
-
mock_conn.execute = AsyncMock()
|
|
872
|
-
mock_conn.commit = AsyncMock()
|
|
873
|
-
|
|
874
|
-
await backend.update_hook_status(
|
|
875
|
-
"hook_123", HookStatus.RECEIVED, payload='{"data": "test"}'
|
|
876
|
-
)
|
|
877
|
-
|
|
878
|
-
mock_conn.execute.assert_called_once()
|
|
879
|
-
call_args = mock_conn.execute.call_args
|
|
880
|
-
assert "UPDATE hooks" in call_args[0][0]
|
|
881
|
-
mock_conn.commit.assert_called_once()
|
|
882
|
-
|
|
883
|
-
@pytest.mark.asyncio
|
|
884
|
-
async def test_list_hooks(self, mock_backend):
|
|
885
|
-
"""Test listing hooks."""
|
|
886
|
-
backend, mock_conn = mock_backend
|
|
887
|
-
|
|
888
|
-
row = (
|
|
889
|
-
"hook_1",
|
|
890
|
-
"run_123",
|
|
891
|
-
"token_1",
|
|
892
|
-
"2024-01-01T12:00:00+00:00",
|
|
893
|
-
None,
|
|
894
|
-
None,
|
|
895
|
-
"pending",
|
|
896
|
-
None,
|
|
897
|
-
"{}",
|
|
898
|
-
)
|
|
899
|
-
|
|
900
|
-
mock_cursor = create_mock_cursor(fetchall_result=[row])
|
|
901
|
-
|
|
902
|
-
@asynccontextmanager
|
|
903
|
-
async def mock_execute(*args, **kwargs):
|
|
904
|
-
yield mock_cursor
|
|
905
|
-
|
|
906
|
-
mock_conn.execute = mock_execute
|
|
907
|
-
|
|
908
|
-
hooks = await backend.list_hooks(run_id="run_123")
|
|
909
|
-
|
|
910
|
-
assert len(hooks) == 1
|
|
911
|
-
assert hooks[0].hook_id == "hook_1"
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
class TestCancellationOperations:
|
|
915
|
-
"""Test cancellation flag operations."""
|
|
916
|
-
|
|
917
|
-
@pytest.mark.asyncio
|
|
918
|
-
async def test_set_cancellation_flag(self, mock_backend):
|
|
919
|
-
"""Test setting a cancellation flag."""
|
|
920
|
-
backend, mock_conn = mock_backend
|
|
921
|
-
mock_conn.execute = AsyncMock()
|
|
922
|
-
mock_conn.commit = AsyncMock()
|
|
923
|
-
|
|
924
|
-
await backend.set_cancellation_flag("run_123")
|
|
925
|
-
|
|
926
|
-
mock_conn.execute.assert_called_once()
|
|
927
|
-
call_args = mock_conn.execute.call_args
|
|
928
|
-
assert "INSERT OR IGNORE INTO cancellation_flags" in call_args[0][0]
|
|
929
|
-
mock_conn.commit.assert_called_once()
|
|
930
|
-
|
|
931
|
-
@pytest.mark.asyncio
|
|
932
|
-
async def test_check_cancellation_flag_set(self, mock_backend):
|
|
933
|
-
"""Test checking a set cancellation flag."""
|
|
934
|
-
backend, mock_conn = mock_backend
|
|
935
|
-
|
|
936
|
-
mock_cursor = create_mock_cursor(fetchone_result=(1,))
|
|
937
|
-
|
|
938
|
-
@asynccontextmanager
|
|
939
|
-
async def mock_execute(*args, **kwargs):
|
|
940
|
-
yield mock_cursor
|
|
941
|
-
|
|
942
|
-
mock_conn.execute = mock_execute
|
|
943
|
-
|
|
944
|
-
result = await backend.check_cancellation_flag("run_123")
|
|
945
|
-
|
|
946
|
-
assert result is True
|
|
947
|
-
|
|
948
|
-
@pytest.mark.asyncio
|
|
949
|
-
async def test_check_cancellation_flag_not_set(self, mock_backend):
|
|
950
|
-
"""Test checking when cancellation flag is not set."""
|
|
951
|
-
backend, mock_conn = mock_backend
|
|
952
|
-
|
|
953
|
-
mock_cursor = create_mock_cursor(fetchone_result=None)
|
|
954
|
-
|
|
955
|
-
@asynccontextmanager
|
|
956
|
-
async def mock_execute(*args, **kwargs):
|
|
957
|
-
yield mock_cursor
|
|
958
|
-
|
|
959
|
-
mock_conn.execute = mock_execute
|
|
960
|
-
|
|
961
|
-
result = await backend.check_cancellation_flag("run_123")
|
|
962
|
-
|
|
963
|
-
assert result is False
|
|
964
|
-
|
|
965
|
-
@pytest.mark.asyncio
|
|
966
|
-
async def test_clear_cancellation_flag(self, mock_backend):
|
|
967
|
-
"""Test clearing a cancellation flag."""
|
|
968
|
-
backend, mock_conn = mock_backend
|
|
969
|
-
mock_conn.execute = AsyncMock()
|
|
970
|
-
mock_conn.commit = AsyncMock()
|
|
971
|
-
|
|
972
|
-
await backend.clear_cancellation_flag("run_123")
|
|
973
|
-
|
|
974
|
-
mock_conn.execute.assert_called_once()
|
|
975
|
-
call_args = mock_conn.execute.call_args
|
|
976
|
-
assert "DELETE FROM cancellation_flags" in call_args[0][0]
|
|
977
|
-
mock_conn.commit.assert_called_once()
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
class TestContinueAsNewOperations:
|
|
981
|
-
"""Test continue-as-new chain operations."""
|
|
982
|
-
|
|
983
|
-
@pytest.mark.asyncio
|
|
984
|
-
async def test_update_run_continuation(self, mock_backend):
|
|
985
|
-
"""Test updating continuation link."""
|
|
986
|
-
backend, mock_conn = mock_backend
|
|
987
|
-
mock_conn.execute = AsyncMock()
|
|
988
|
-
mock_conn.commit = AsyncMock()
|
|
989
|
-
|
|
990
|
-
await backend.update_run_continuation("run_1", "run_2")
|
|
991
|
-
|
|
992
|
-
mock_conn.execute.assert_called_once()
|
|
993
|
-
call_args = mock_conn.execute.call_args
|
|
994
|
-
assert "continued_to_run_id" in call_args[0][0]
|
|
995
|
-
mock_conn.commit.assert_called_once()
|
|
996
|
-
|
|
997
|
-
@pytest.mark.asyncio
|
|
998
|
-
async def test_get_workflow_chain(self, mock_backend):
|
|
999
|
-
"""Test retrieving workflow chain."""
|
|
1000
|
-
backend, mock_conn = mock_backend
|
|
1001
|
-
|
|
1002
|
-
# Mock get_run for chain traversal
|
|
1003
|
-
with patch.object(
|
|
1004
|
-
backend,
|
|
1005
|
-
"get_run",
|
|
1006
|
-
return_value=WorkflowRun(
|
|
1007
|
-
run_id="run_1",
|
|
1008
|
-
workflow_name="test_workflow",
|
|
1009
|
-
status=RunStatus.COMPLETED,
|
|
1010
|
-
continued_to_run_id=None,
|
|
1011
|
-
),
|
|
1012
|
-
):
|
|
1013
|
-
# Mock the chain traversal query (finding first run)
|
|
1014
|
-
mock_cursor = create_mock_cursor(fetchone_result=None)
|
|
1015
|
-
|
|
1016
|
-
@asynccontextmanager
|
|
1017
|
-
async def mock_execute(*args, **kwargs):
|
|
1018
|
-
yield mock_cursor
|
|
1019
|
-
|
|
1020
|
-
mock_conn.execute = mock_execute
|
|
1021
|
-
|
|
1022
|
-
runs = await backend.get_workflow_chain("run_1")
|
|
1023
|
-
|
|
1024
|
-
assert len(runs) == 1
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
class TestChildWorkflowOperations:
|
|
1028
|
-
"""Test child workflow operations."""
|
|
1029
|
-
|
|
1030
|
-
@pytest.mark.asyncio
|
|
1031
|
-
async def test_get_children(self, mock_backend):
|
|
1032
|
-
"""Test retrieving child workflows."""
|
|
1033
|
-
backend, mock_conn = mock_backend
|
|
1034
|
-
|
|
1035
|
-
row = (
|
|
1036
|
-
"child_1",
|
|
1037
|
-
"child_workflow",
|
|
1038
|
-
"completed",
|
|
1039
|
-
"2024-01-01T12:00:00+00:00",
|
|
1040
|
-
"2024-01-01T12:00:01+00:00",
|
|
1041
|
-
None,
|
|
1042
|
-
None,
|
|
1043
|
-
"[]",
|
|
1044
|
-
"{}",
|
|
1045
|
-
None,
|
|
1046
|
-
None,
|
|
1047
|
-
None,
|
|
1048
|
-
None,
|
|
1049
|
-
"{}",
|
|
1050
|
-
0,
|
|
1051
|
-
3,
|
|
1052
|
-
1,
|
|
1053
|
-
"parent_123",
|
|
1054
|
-
1,
|
|
1055
|
-
None,
|
|
1056
|
-
None,
|
|
1057
|
-
)
|
|
1058
|
-
|
|
1059
|
-
mock_cursor = create_mock_cursor(fetchall_result=[row])
|
|
1060
|
-
|
|
1061
|
-
@asynccontextmanager
|
|
1062
|
-
async def mock_execute(*args, **kwargs):
|
|
1063
|
-
yield mock_cursor
|
|
1064
|
-
|
|
1065
|
-
mock_conn.execute = mock_execute
|
|
1066
|
-
|
|
1067
|
-
children = await backend.get_children("parent_123")
|
|
1068
|
-
|
|
1069
|
-
assert len(children) == 1
|
|
1070
|
-
assert children[0].parent_run_id == "parent_123"
|
|
1071
|
-
|
|
1072
|
-
@pytest.mark.asyncio
|
|
1073
|
-
async def test_get_parent_found(self, mock_backend):
|
|
1074
|
-
"""Test retrieving parent workflow."""
|
|
1075
|
-
backend, mock_conn = mock_backend
|
|
1076
|
-
|
|
1077
|
-
child_row = (
|
|
1078
|
-
"child_1",
|
|
1079
|
-
"child_workflow",
|
|
1080
|
-
"running",
|
|
1081
|
-
"2024-01-01T12:00:00+00:00",
|
|
1082
|
-
"2024-01-01T12:00:01+00:00",
|
|
1083
|
-
None,
|
|
1084
|
-
None,
|
|
1085
|
-
"[]",
|
|
1086
|
-
"{}",
|
|
1087
|
-
None,
|
|
1088
|
-
None,
|
|
1089
|
-
None,
|
|
1090
|
-
None,
|
|
1091
|
-
"{}",
|
|
1092
|
-
0,
|
|
1093
|
-
3,
|
|
1094
|
-
1,
|
|
1095
|
-
"parent_123",
|
|
1096
|
-
1,
|
|
1097
|
-
None,
|
|
1098
|
-
None,
|
|
1099
|
-
)
|
|
1100
|
-
|
|
1101
|
-
parent_row = (
|
|
1102
|
-
"parent_123",
|
|
1103
|
-
"parent_workflow",
|
|
1104
|
-
"running",
|
|
1105
|
-
"2024-01-01T12:00:00+00:00",
|
|
1106
|
-
"2024-01-01T12:00:01+00:00",
|
|
1107
|
-
None,
|
|
1108
|
-
None,
|
|
1109
|
-
"[]",
|
|
1110
|
-
"{}",
|
|
1111
|
-
None,
|
|
1112
|
-
None,
|
|
1113
|
-
None,
|
|
1114
|
-
None,
|
|
1115
|
-
"{}",
|
|
1116
|
-
0,
|
|
1117
|
-
3,
|
|
1118
|
-
1,
|
|
1119
|
-
None,
|
|
1120
|
-
0,
|
|
1121
|
-
None,
|
|
1122
|
-
None,
|
|
1123
|
-
)
|
|
1124
|
-
|
|
1125
|
-
call_count = [0]
|
|
1126
|
-
rows = [child_row, parent_row]
|
|
1127
|
-
|
|
1128
|
-
def create_cursor_for_call():
|
|
1129
|
-
mock_cursor = create_mock_cursor(fetchone_result=rows[call_count[0]])
|
|
1130
|
-
call_count[0] += 1
|
|
1131
|
-
return mock_cursor
|
|
1132
|
-
|
|
1133
|
-
@asynccontextmanager
|
|
1134
|
-
async def mock_execute(*args, **kwargs):
|
|
1135
|
-
yield create_cursor_for_call()
|
|
1136
|
-
|
|
1137
|
-
mock_conn.execute = mock_execute
|
|
1138
|
-
|
|
1139
|
-
parent = await backend.get_parent("child_1")
|
|
1140
|
-
|
|
1141
|
-
assert parent is not None
|
|
1142
|
-
assert parent.run_id == "parent_123"
|
|
1143
|
-
|
|
1144
|
-
@pytest.mark.asyncio
|
|
1145
|
-
async def test_get_parent_not_found(self, mock_backend):
|
|
1146
|
-
"""Test get_parent when run has no parent."""
|
|
1147
|
-
backend, mock_conn = mock_backend
|
|
1148
|
-
|
|
1149
|
-
row = (
|
|
1150
|
-
"run_1",
|
|
1151
|
-
"test_workflow",
|
|
1152
|
-
"running",
|
|
1153
|
-
"2024-01-01T12:00:00+00:00",
|
|
1154
|
-
"2024-01-01T12:00:01+00:00",
|
|
1155
|
-
None,
|
|
1156
|
-
None,
|
|
1157
|
-
"[]",
|
|
1158
|
-
"{}",
|
|
1159
|
-
None,
|
|
1160
|
-
None,
|
|
1161
|
-
None,
|
|
1162
|
-
None,
|
|
1163
|
-
"{}",
|
|
1164
|
-
0,
|
|
1165
|
-
3,
|
|
1166
|
-
1,
|
|
1167
|
-
None, # No parent_run_id
|
|
1168
|
-
0,
|
|
1169
|
-
None,
|
|
1170
|
-
None,
|
|
1171
|
-
)
|
|
1172
|
-
|
|
1173
|
-
mock_cursor = create_mock_cursor(fetchone_result=row)
|
|
1174
|
-
|
|
1175
|
-
@asynccontextmanager
|
|
1176
|
-
async def mock_execute(*args, **kwargs):
|
|
1177
|
-
yield mock_cursor
|
|
1178
|
-
|
|
1179
|
-
mock_conn.execute = mock_execute
|
|
1180
|
-
|
|
1181
|
-
parent = await backend.get_parent("run_1")
|
|
1182
|
-
|
|
1183
|
-
assert parent is None
|
|
1184
|
-
|
|
1185
|
-
@pytest.mark.asyncio
|
|
1186
|
-
async def test_get_nesting_depth(self, mock_backend):
|
|
1187
|
-
"""Test getting nesting depth."""
|
|
1188
|
-
backend, mock_conn = mock_backend
|
|
1189
|
-
|
|
1190
|
-
row = (
|
|
1191
|
-
"run_1",
|
|
1192
|
-
"test_workflow",
|
|
1193
|
-
"running",
|
|
1194
|
-
"2024-01-01T12:00:00+00:00",
|
|
1195
|
-
"2024-01-01T12:00:01+00:00",
|
|
1196
|
-
None,
|
|
1197
|
-
None,
|
|
1198
|
-
"[]",
|
|
1199
|
-
"{}",
|
|
1200
|
-
None,
|
|
1201
|
-
None,
|
|
1202
|
-
None,
|
|
1203
|
-
None,
|
|
1204
|
-
"{}",
|
|
1205
|
-
0,
|
|
1206
|
-
3,
|
|
1207
|
-
1,
|
|
1208
|
-
None,
|
|
1209
|
-
2, # nesting_depth
|
|
1210
|
-
None,
|
|
1211
|
-
None,
|
|
1212
|
-
)
|
|
1213
|
-
|
|
1214
|
-
mock_cursor = create_mock_cursor(fetchone_result=row)
|
|
1215
|
-
|
|
1216
|
-
@asynccontextmanager
|
|
1217
|
-
async def mock_execute(*args, **kwargs):
|
|
1218
|
-
yield mock_cursor
|
|
1219
|
-
|
|
1220
|
-
mock_conn.execute = mock_execute
|
|
1221
|
-
|
|
1222
|
-
depth = await backend.get_nesting_depth("run_1")
|
|
1223
|
-
|
|
1224
|
-
assert depth == 2
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
class TestScheduleOperations:
|
|
1228
|
-
"""Test schedule operations."""
|
|
1229
|
-
|
|
1230
|
-
@pytest.mark.asyncio
|
|
1231
|
-
async def test_create_schedule(self, mock_backend):
|
|
1232
|
-
"""Test creating a schedule."""
|
|
1233
|
-
backend, mock_conn = mock_backend
|
|
1234
|
-
mock_conn.execute = AsyncMock()
|
|
1235
|
-
mock_conn.commit = AsyncMock()
|
|
1236
|
-
|
|
1237
|
-
schedule = Schedule(
|
|
1238
|
-
schedule_id="sched_123",
|
|
1239
|
-
workflow_name="daily_report",
|
|
1240
|
-
spec=ScheduleSpec(cron="0 9 * * *"),
|
|
1241
|
-
)
|
|
1242
|
-
|
|
1243
|
-
await backend.create_schedule(schedule)
|
|
1244
|
-
|
|
1245
|
-
mock_conn.execute.assert_called_once()
|
|
1246
|
-
call_args = mock_conn.execute.call_args
|
|
1247
|
-
assert "INSERT INTO schedules" in call_args[0][0]
|
|
1248
|
-
mock_conn.commit.assert_called_once()
|
|
1249
|
-
|
|
1250
|
-
@pytest.mark.asyncio
|
|
1251
|
-
async def test_get_schedule_found(self, mock_backend):
|
|
1252
|
-
"""Test retrieving an existing schedule."""
|
|
1253
|
-
backend, mock_conn = mock_backend
|
|
1254
|
-
|
|
1255
|
-
row = (
|
|
1256
|
-
"sched_123",
|
|
1257
|
-
"daily_report",
|
|
1258
|
-
"0 9 * * *",
|
|
1259
|
-
"cron",
|
|
1260
|
-
"UTC",
|
|
1261
|
-
"[]",
|
|
1262
|
-
"{}",
|
|
1263
|
-
"active",
|
|
1264
|
-
"skip",
|
|
1265
|
-
"2024-01-02T09:00:00+00:00",
|
|
1266
|
-
None,
|
|
1267
|
-
"[]",
|
|
1268
|
-
"{}",
|
|
1269
|
-
"2024-01-01T00:00:00+00:00",
|
|
1270
|
-
None,
|
|
1271
|
-
None,
|
|
1272
|
-
None,
|
|
1273
|
-
)
|
|
1274
|
-
|
|
1275
|
-
mock_cursor = create_mock_cursor(fetchone_result=row)
|
|
1276
|
-
|
|
1277
|
-
@asynccontextmanager
|
|
1278
|
-
async def mock_execute(*args, **kwargs):
|
|
1279
|
-
yield mock_cursor
|
|
1280
|
-
|
|
1281
|
-
mock_conn.execute = mock_execute
|
|
1282
|
-
|
|
1283
|
-
schedule = await backend.get_schedule("sched_123")
|
|
1284
|
-
|
|
1285
|
-
assert schedule is not None
|
|
1286
|
-
assert schedule.schedule_id == "sched_123"
|
|
1287
|
-
assert schedule.spec.cron == "0 9 * * *"
|
|
1288
|
-
|
|
1289
|
-
@pytest.mark.asyncio
|
|
1290
|
-
async def test_get_schedule_not_found(self, mock_backend):
|
|
1291
|
-
"""Test retrieving a non-existent schedule."""
|
|
1292
|
-
backend, mock_conn = mock_backend
|
|
1293
|
-
|
|
1294
|
-
mock_cursor = create_mock_cursor(fetchone_result=None)
|
|
1295
|
-
|
|
1296
|
-
@asynccontextmanager
|
|
1297
|
-
async def mock_execute(*args, **kwargs):
|
|
1298
|
-
yield mock_cursor
|
|
1299
|
-
|
|
1300
|
-
mock_conn.execute = mock_execute
|
|
1301
|
-
|
|
1302
|
-
schedule = await backend.get_schedule("nonexistent")
|
|
1303
|
-
|
|
1304
|
-
assert schedule is None
|
|
1305
|
-
|
|
1306
|
-
@pytest.mark.asyncio
|
|
1307
|
-
async def test_update_schedule(self, mock_backend):
|
|
1308
|
-
"""Test updating a schedule."""
|
|
1309
|
-
backend, mock_conn = mock_backend
|
|
1310
|
-
mock_conn.execute = AsyncMock()
|
|
1311
|
-
mock_conn.commit = AsyncMock()
|
|
1312
|
-
|
|
1313
|
-
schedule = Schedule(
|
|
1314
|
-
schedule_id="sched_123",
|
|
1315
|
-
workflow_name="daily_report",
|
|
1316
|
-
spec=ScheduleSpec(cron="0 10 * * *"),
|
|
1317
|
-
)
|
|
1318
|
-
|
|
1319
|
-
await backend.update_schedule(schedule)
|
|
1320
|
-
|
|
1321
|
-
mock_conn.execute.assert_called_once()
|
|
1322
|
-
call_args = mock_conn.execute.call_args
|
|
1323
|
-
assert "UPDATE schedules" in call_args[0][0]
|
|
1324
|
-
mock_conn.commit.assert_called_once()
|
|
1325
|
-
|
|
1326
|
-
@pytest.mark.asyncio
|
|
1327
|
-
async def test_delete_schedule(self, mock_backend):
|
|
1328
|
-
"""Test deleting (soft delete) a schedule."""
|
|
1329
|
-
backend, mock_conn = mock_backend
|
|
1330
|
-
mock_conn.execute = AsyncMock()
|
|
1331
|
-
mock_conn.commit = AsyncMock()
|
|
1332
|
-
|
|
1333
|
-
await backend.delete_schedule("sched_123")
|
|
1334
|
-
|
|
1335
|
-
mock_conn.execute.assert_called_once()
|
|
1336
|
-
call_args = mock_conn.execute.call_args
|
|
1337
|
-
assert "UPDATE schedules" in call_args[0][0]
|
|
1338
|
-
assert "deleted_at" in call_args[0][0]
|
|
1339
|
-
mock_conn.commit.assert_called_once()
|
|
1340
|
-
|
|
1341
|
-
@pytest.mark.asyncio
|
|
1342
|
-
async def test_list_schedules(self, mock_backend):
|
|
1343
|
-
"""Test listing schedules."""
|
|
1344
|
-
backend, mock_conn = mock_backend
|
|
1345
|
-
|
|
1346
|
-
row = (
|
|
1347
|
-
"sched_1",
|
|
1348
|
-
"daily_report",
|
|
1349
|
-
"0 9 * * *",
|
|
1350
|
-
"cron",
|
|
1351
|
-
"UTC",
|
|
1352
|
-
"[]",
|
|
1353
|
-
"{}",
|
|
1354
|
-
"active",
|
|
1355
|
-
"skip",
|
|
1356
|
-
"2024-01-02T09:00:00+00:00",
|
|
1357
|
-
None,
|
|
1358
|
-
"[]",
|
|
1359
|
-
"{}",
|
|
1360
|
-
"2024-01-01T00:00:00+00:00",
|
|
1361
|
-
None,
|
|
1362
|
-
None,
|
|
1363
|
-
None,
|
|
1364
|
-
)
|
|
1365
|
-
|
|
1366
|
-
mock_cursor = create_mock_cursor(fetchall_result=[row])
|
|
1367
|
-
|
|
1368
|
-
@asynccontextmanager
|
|
1369
|
-
async def mock_execute(*args, **kwargs):
|
|
1370
|
-
yield mock_cursor
|
|
1371
|
-
|
|
1372
|
-
mock_conn.execute = mock_execute
|
|
1373
|
-
|
|
1374
|
-
schedules = await backend.list_schedules()
|
|
1375
|
-
|
|
1376
|
-
assert len(schedules) == 1
|
|
1377
|
-
assert schedules[0].schedule_id == "sched_1"
|
|
1378
|
-
|
|
1379
|
-
@pytest.mark.asyncio
|
|
1380
|
-
async def test_get_due_schedules(self, mock_backend):
|
|
1381
|
-
"""Test getting schedules that are due to run."""
|
|
1382
|
-
backend, mock_conn = mock_backend
|
|
1383
|
-
|
|
1384
|
-
row = (
|
|
1385
|
-
"sched_1",
|
|
1386
|
-
"daily_report",
|
|
1387
|
-
"0 9 * * *",
|
|
1388
|
-
"cron",
|
|
1389
|
-
"UTC",
|
|
1390
|
-
"[]",
|
|
1391
|
-
"{}",
|
|
1392
|
-
"active",
|
|
1393
|
-
"skip",
|
|
1394
|
-
"2024-01-01T09:00:00+00:00",
|
|
1395
|
-
None,
|
|
1396
|
-
"[]",
|
|
1397
|
-
"{}",
|
|
1398
|
-
"2024-01-01T00:00:00+00:00",
|
|
1399
|
-
None,
|
|
1400
|
-
None,
|
|
1401
|
-
None,
|
|
1402
|
-
)
|
|
1403
|
-
|
|
1404
|
-
mock_cursor = create_mock_cursor(fetchall_result=[row])
|
|
1405
|
-
|
|
1406
|
-
@asynccontextmanager
|
|
1407
|
-
async def mock_execute(*args, **kwargs):
|
|
1408
|
-
yield mock_cursor
|
|
1409
|
-
|
|
1410
|
-
mock_conn.execute = mock_execute
|
|
1411
|
-
|
|
1412
|
-
now = datetime(2024, 1, 1, 9, 1, 0, tzinfo=UTC)
|
|
1413
|
-
schedules = await backend.get_due_schedules(now)
|
|
1414
|
-
|
|
1415
|
-
assert len(schedules) == 1
|
|
1416
|
-
|
|
1417
|
-
@pytest.mark.asyncio
|
|
1418
|
-
async def test_add_running_run(self, mock_backend):
|
|
1419
|
-
"""Test adding a run_id to schedule's running_run_ids."""
|
|
1420
|
-
backend, mock_conn = mock_backend
|
|
1421
|
-
|
|
1422
|
-
schedule = Schedule(
|
|
1423
|
-
schedule_id="sched_123",
|
|
1424
|
-
workflow_name="daily_report",
|
|
1425
|
-
spec=ScheduleSpec(cron="0 9 * * *"),
|
|
1426
|
-
running_run_ids=["run_1"],
|
|
1427
|
-
)
|
|
1428
|
-
|
|
1429
|
-
with (
|
|
1430
|
-
patch.object(backend, "get_schedule", return_value=schedule),
|
|
1431
|
-
patch.object(backend, "update_schedule") as mock_update,
|
|
1432
|
-
):
|
|
1433
|
-
await backend.add_running_run("sched_123", "run_2")
|
|
1434
|
-
|
|
1435
|
-
mock_update.assert_called_once()
|
|
1436
|
-
updated_schedule = mock_update.call_args[0][0]
|
|
1437
|
-
assert "run_2" in updated_schedule.running_run_ids
|
|
1438
|
-
|
|
1439
|
-
@pytest.mark.asyncio
|
|
1440
|
-
async def test_remove_running_run(self, mock_backend):
|
|
1441
|
-
"""Test removing a run_id from schedule's running_run_ids."""
|
|
1442
|
-
backend, mock_conn = mock_backend
|
|
1443
|
-
|
|
1444
|
-
schedule = Schedule(
|
|
1445
|
-
schedule_id="sched_123",
|
|
1446
|
-
workflow_name="daily_report",
|
|
1447
|
-
spec=ScheduleSpec(cron="0 9 * * *"),
|
|
1448
|
-
running_run_ids=["run_1", "run_2"],
|
|
1449
|
-
)
|
|
1450
|
-
|
|
1451
|
-
with (
|
|
1452
|
-
patch.object(backend, "get_schedule", return_value=schedule),
|
|
1453
|
-
patch.object(backend, "update_schedule") as mock_update,
|
|
1454
|
-
):
|
|
1455
|
-
await backend.remove_running_run("sched_123", "run_1")
|
|
1456
|
-
|
|
1457
|
-
mock_update.assert_called_once()
|
|
1458
|
-
updated_schedule = mock_update.call_args[0][0]
|
|
1459
|
-
assert "run_1" not in updated_schedule.running_run_ids
|
|
1460
|
-
assert "run_2" in updated_schedule.running_run_ids
|