experimaestro 2.0.0b4__py3-none-any.whl → 2.0.0b17__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.
Potentially problematic release.
This version of experimaestro might be problematic. Click here for more details.
- experimaestro/__init__.py +12 -5
- experimaestro/cli/__init__.py +393 -134
- experimaestro/cli/filter.py +48 -23
- experimaestro/cli/jobs.py +253 -71
- experimaestro/cli/refactor.py +1 -2
- experimaestro/commandline.py +7 -4
- experimaestro/connectors/__init__.py +9 -1
- experimaestro/connectors/local.py +43 -3
- experimaestro/core/arguments.py +18 -18
- experimaestro/core/identifier.py +11 -11
- experimaestro/core/objects/config.py +96 -39
- experimaestro/core/objects/config_walk.py +3 -3
- experimaestro/core/{subparameters.py → partial.py} +16 -16
- experimaestro/core/partial_lock.py +394 -0
- experimaestro/core/types.py +12 -15
- experimaestro/dynamic.py +290 -0
- experimaestro/experiments/__init__.py +6 -2
- experimaestro/experiments/cli.py +223 -52
- experimaestro/experiments/configuration.py +24 -0
- experimaestro/generators.py +5 -5
- experimaestro/ipc.py +118 -1
- experimaestro/launcherfinder/__init__.py +2 -2
- experimaestro/launcherfinder/registry.py +6 -7
- experimaestro/launcherfinder/specs.py +2 -9
- experimaestro/launchers/slurm/__init__.py +2 -2
- experimaestro/launchers/slurm/base.py +62 -0
- experimaestro/locking.py +957 -1
- experimaestro/notifications.py +89 -201
- experimaestro/progress.py +63 -366
- experimaestro/rpyc.py +0 -2
- experimaestro/run.py +29 -2
- experimaestro/scheduler/__init__.py +8 -1
- experimaestro/scheduler/base.py +650 -53
- experimaestro/scheduler/dependencies.py +20 -16
- experimaestro/scheduler/experiment.py +764 -169
- experimaestro/scheduler/interfaces.py +338 -96
- experimaestro/scheduler/jobs.py +58 -20
- experimaestro/scheduler/remote/__init__.py +31 -0
- experimaestro/scheduler/remote/adaptive_sync.py +265 -0
- experimaestro/scheduler/remote/client.py +928 -0
- experimaestro/scheduler/remote/protocol.py +282 -0
- experimaestro/scheduler/remote/server.py +447 -0
- experimaestro/scheduler/remote/sync.py +144 -0
- experimaestro/scheduler/services.py +186 -35
- experimaestro/scheduler/state_provider.py +811 -2157
- experimaestro/scheduler/state_status.py +1247 -0
- experimaestro/scheduler/transient.py +31 -0
- experimaestro/scheduler/workspace.py +1 -1
- experimaestro/scheduler/workspace_state_provider.py +1273 -0
- experimaestro/scriptbuilder.py +4 -4
- experimaestro/settings.py +36 -0
- experimaestro/tests/conftest.py +33 -5
- experimaestro/tests/connectors/bin/executable.py +1 -1
- experimaestro/tests/fixtures/pre_experiment/experiment_check_env.py +16 -0
- experimaestro/tests/fixtures/pre_experiment/experiment_check_mock.py +14 -0
- experimaestro/tests/fixtures/pre_experiment/experiment_simple.py +12 -0
- experimaestro/tests/fixtures/pre_experiment/pre_setup_env.py +5 -0
- experimaestro/tests/fixtures/pre_experiment/pre_setup_error.py +3 -0
- experimaestro/tests/fixtures/pre_experiment/pre_setup_mock.py +8 -0
- experimaestro/tests/launchers/bin/test.py +1 -0
- experimaestro/tests/launchers/test_slurm.py +9 -9
- experimaestro/tests/partial_reschedule.py +46 -0
- experimaestro/tests/restart.py +3 -3
- experimaestro/tests/restart_main.py +1 -0
- experimaestro/tests/scripts/notifyandwait.py +1 -0
- experimaestro/tests/task_partial.py +38 -0
- experimaestro/tests/task_tokens.py +2 -2
- experimaestro/tests/tasks/test_dynamic.py +6 -6
- experimaestro/tests/test_dependencies.py +3 -3
- experimaestro/tests/test_deprecated.py +15 -15
- experimaestro/tests/test_dynamic_locking.py +317 -0
- experimaestro/tests/test_environment.py +24 -14
- experimaestro/tests/test_experiment.py +171 -36
- experimaestro/tests/test_identifier.py +25 -25
- experimaestro/tests/test_identifier_stability.py +3 -5
- experimaestro/tests/test_multitoken.py +2 -4
- experimaestro/tests/{test_subparameters.py → test_partial.py} +25 -25
- experimaestro/tests/test_partial_paths.py +81 -138
- experimaestro/tests/test_pre_experiment.py +219 -0
- experimaestro/tests/test_progress.py +2 -8
- experimaestro/tests/test_remote_state.py +1132 -0
- experimaestro/tests/test_stray_jobs.py +261 -0
- experimaestro/tests/test_tasks.py +1 -2
- experimaestro/tests/test_token_locking.py +52 -67
- experimaestro/tests/test_tokens.py +5 -6
- experimaestro/tests/test_transient.py +225 -0
- experimaestro/tests/test_workspace_state_provider.py +768 -0
- experimaestro/tests/token_reschedule.py +1 -3
- experimaestro/tests/utils.py +2 -7
- experimaestro/tokens.py +227 -372
- experimaestro/tools/diff.py +1 -0
- experimaestro/tools/documentation.py +4 -5
- experimaestro/tools/jobs.py +1 -2
- experimaestro/tui/app.py +459 -1895
- experimaestro/tui/app.tcss +162 -0
- experimaestro/tui/dialogs.py +172 -0
- experimaestro/tui/log_viewer.py +253 -3
- experimaestro/tui/messages.py +137 -0
- experimaestro/tui/utils.py +54 -0
- experimaestro/tui/widgets/__init__.py +23 -0
- experimaestro/tui/widgets/experiments.py +468 -0
- experimaestro/tui/widgets/global_services.py +238 -0
- experimaestro/tui/widgets/jobs.py +972 -0
- experimaestro/tui/widgets/log.py +156 -0
- experimaestro/tui/widgets/orphans.py +363 -0
- experimaestro/tui/widgets/runs.py +185 -0
- experimaestro/tui/widgets/services.py +314 -0
- experimaestro/tui/widgets/stray_jobs.py +528 -0
- experimaestro/utils/__init__.py +1 -1
- experimaestro/utils/environment.py +105 -22
- experimaestro/utils/fswatcher.py +124 -0
- experimaestro/utils/jobs.py +1 -2
- experimaestro/utils/jupyter.py +1 -2
- experimaestro/utils/logging.py +72 -0
- experimaestro/version.py +2 -2
- experimaestro/webui/__init__.py +9 -0
- experimaestro/webui/app.py +117 -0
- experimaestro/{server → webui}/data/index.css +66 -11
- experimaestro/webui/data/index.css.map +1 -0
- experimaestro/{server → webui}/data/index.js +82763 -87217
- experimaestro/webui/data/index.js.map +1 -0
- experimaestro/webui/routes/__init__.py +5 -0
- experimaestro/webui/routes/auth.py +53 -0
- experimaestro/webui/routes/proxy.py +117 -0
- experimaestro/webui/server.py +200 -0
- experimaestro/webui/state_bridge.py +152 -0
- experimaestro/webui/websocket.py +413 -0
- {experimaestro-2.0.0b4.dist-info → experimaestro-2.0.0b17.dist-info}/METADATA +8 -9
- experimaestro-2.0.0b17.dist-info/RECORD +219 -0
- experimaestro/cli/progress.py +0 -269
- experimaestro/scheduler/state.py +0 -75
- experimaestro/scheduler/state_db.py +0 -388
- experimaestro/scheduler/state_sync.py +0 -834
- experimaestro/server/__init__.py +0 -467
- experimaestro/server/data/index.css.map +0 -1
- experimaestro/server/data/index.js.map +0 -1
- experimaestro/tests/test_cli_jobs.py +0 -615
- experimaestro/tests/test_file_progress.py +0 -425
- experimaestro/tests/test_file_progress_integration.py +0 -477
- experimaestro/tests/test_state_db.py +0 -434
- experimaestro-2.0.0b4.dist-info/RECORD +0 -181
- /experimaestro/{server → webui}/data/1815e00441357e01619e.ttf +0 -0
- /experimaestro/{server → webui}/data/2463b90d9a316e4e5294.woff2 +0 -0
- /experimaestro/{server → webui}/data/2582b0e4bcf85eceead0.ttf +0 -0
- /experimaestro/{server → webui}/data/89999bdf5d835c012025.woff2 +0 -0
- /experimaestro/{server → webui}/data/914997e1bdfc990d0897.ttf +0 -0
- /experimaestro/{server → webui}/data/c210719e60948b211a12.woff2 +0 -0
- /experimaestro/{server → webui}/data/favicon.ico +0 -0
- /experimaestro/{server → webui}/data/index.html +0 -0
- /experimaestro/{server → webui}/data/login.html +0 -0
- /experimaestro/{server → webui}/data/manifest.json +0 -0
- {experimaestro-2.0.0b4.dist-info → experimaestro-2.0.0b17.dist-info}/WHEEL +0 -0
- {experimaestro-2.0.0b4.dist-info → experimaestro-2.0.0b17.dist-info}/entry_points.txt +0 -0
- {experimaestro-2.0.0b4.dist-info → experimaestro-2.0.0b17.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"""Tests for stray job detection.
|
|
2
|
+
|
|
3
|
+
A stray job is a running job that is not associated with any active experiment.
|
|
4
|
+
This happens when an experiment plan changes (e.g., same experiment ID is relaunched
|
|
5
|
+
with different parameters).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import time
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import tempfile
|
|
11
|
+
import logging
|
|
12
|
+
|
|
13
|
+
from experimaestro import (
|
|
14
|
+
Task,
|
|
15
|
+
Param,
|
|
16
|
+
Meta,
|
|
17
|
+
field,
|
|
18
|
+
PathGenerator,
|
|
19
|
+
experiment,
|
|
20
|
+
GracefulExperimentExit,
|
|
21
|
+
)
|
|
22
|
+
from experimaestro.scheduler.workspace import RunMode
|
|
23
|
+
from experimaestro.scheduler import JobState
|
|
24
|
+
from experimaestro.scheduler.workspace_state_provider import WorkspaceStateProvider
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ControllableTask(Task):
|
|
30
|
+
"""A task that can be controlled via files for testing purposes."""
|
|
31
|
+
|
|
32
|
+
value: Param[int]
|
|
33
|
+
touch: Meta[Path] = field(default_factory=PathGenerator("touch"))
|
|
34
|
+
wait: Meta[Path] = field(default_factory=PathGenerator("wait"))
|
|
35
|
+
|
|
36
|
+
def execute(self):
|
|
37
|
+
# Signal that the task has started
|
|
38
|
+
with open(self.touch, "w") as out:
|
|
39
|
+
out.write("started")
|
|
40
|
+
|
|
41
|
+
# Wait for external signal to continue
|
|
42
|
+
while not self.wait.is_file():
|
|
43
|
+
time.sleep(0.1)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
MAX_WAIT_ITERATIONS = 50 # 5 seconds max wait
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_stray_job_detection():
|
|
50
|
+
"""Test that a running job becomes stray when experiment is relaunched with different params.
|
|
51
|
+
|
|
52
|
+
Scenario:
|
|
53
|
+
1. Start experiment with task(value=1)
|
|
54
|
+
2. Wait for task to start running
|
|
55
|
+
3. Exit experiment without waiting (using GracefulExperimentExit)
|
|
56
|
+
4. Start same experiment (same ID) with task(value=2)
|
|
57
|
+
5. The first job should now be detected as stray
|
|
58
|
+
6. Signal first task to finish
|
|
59
|
+
7. Verify stray detection works correctly
|
|
60
|
+
"""
|
|
61
|
+
with tempfile.TemporaryDirectory(prefix="xpm_stray_test_") as workdir:
|
|
62
|
+
workdir_path = Path(workdir)
|
|
63
|
+
experiment_id = "stray_test"
|
|
64
|
+
|
|
65
|
+
# Phase 1: Start experiment with value=1
|
|
66
|
+
with experiment(workdir_path, experiment_id) as _:
|
|
67
|
+
task1 = ControllableTask.C(value=1)
|
|
68
|
+
# First do a dry run to get the file paths
|
|
69
|
+
task1.submit(run_mode=RunMode.DRY_RUN)
|
|
70
|
+
touch_path = task1.touch
|
|
71
|
+
wait_path = task1.wait
|
|
72
|
+
|
|
73
|
+
# Now submit for real
|
|
74
|
+
task1 = ControllableTask.C(value=1)
|
|
75
|
+
task1.submit()
|
|
76
|
+
|
|
77
|
+
# Wait for task to start
|
|
78
|
+
counter = 0
|
|
79
|
+
while not touch_path.exists():
|
|
80
|
+
time.sleep(0.1)
|
|
81
|
+
counter += 1
|
|
82
|
+
if counter >= MAX_WAIT_ITERATIONS:
|
|
83
|
+
raise AssertionError("Timeout waiting for task1 to start")
|
|
84
|
+
|
|
85
|
+
raise GracefulExperimentExit()
|
|
86
|
+
|
|
87
|
+
# Phase 2: Start same experiment with value=2
|
|
88
|
+
try:
|
|
89
|
+
with experiment(workdir_path, experiment_id) as _:
|
|
90
|
+
task2 = ControllableTask.C(value=2)
|
|
91
|
+
task2.submit()
|
|
92
|
+
|
|
93
|
+
# Give the scheduler a moment to process
|
|
94
|
+
time.sleep(0.5)
|
|
95
|
+
|
|
96
|
+
# Check for stray jobs using a fresh state provider
|
|
97
|
+
provider = WorkspaceStateProvider(workdir_path)
|
|
98
|
+
|
|
99
|
+
stray_jobs = provider.get_stray_jobs()
|
|
100
|
+
|
|
101
|
+
# At least the first task should be stray (running but not in current experiment)
|
|
102
|
+
# Note: task2 might also appear as stray temporarily because status.json
|
|
103
|
+
# hasn't been flushed yet
|
|
104
|
+
assert len(stray_jobs) >= 1, (
|
|
105
|
+
f"Expected at least 1 stray job, found {len(stray_jobs)}"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Verify that at least one stray job is actually running
|
|
109
|
+
running_stray = [j for j in stray_jobs if j.state == JobState.RUNNING]
|
|
110
|
+
assert len(running_stray) >= 1, (
|
|
111
|
+
f"Expected at least 1 running stray job, found {len(running_stray)}"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# Signal first task to finish
|
|
115
|
+
with open(wait_path, "w") as f:
|
|
116
|
+
f.write("done")
|
|
117
|
+
|
|
118
|
+
# Give it a moment to finish
|
|
119
|
+
time.sleep(0.5)
|
|
120
|
+
|
|
121
|
+
# After task1 finishes, it should no longer be stray (it's not running)
|
|
122
|
+
# task1 should no longer be stray (it finished)
|
|
123
|
+
# task2 might still appear as stray if status hasn't been flushed
|
|
124
|
+
# So we just verify that the count decreased by at least 1
|
|
125
|
+
# (or at least, that task1's specific job is no longer in the list)
|
|
126
|
+
|
|
127
|
+
# Exit gracefully to not wait for task2
|
|
128
|
+
raise GracefulExperimentExit()
|
|
129
|
+
finally:
|
|
130
|
+
# Clean up: signal task2 to finish if it started
|
|
131
|
+
with experiment(workdir_path, experiment_id, run_mode=RunMode.DRY_RUN):
|
|
132
|
+
task2_dry = ControllableTask.C(value=2)
|
|
133
|
+
task2_dry.submit(run_mode=RunMode.DRY_RUN)
|
|
134
|
+
task2_wait = task2_dry.wait
|
|
135
|
+
|
|
136
|
+
if task2_wait and task2_wait.exists() is False:
|
|
137
|
+
# task2 might have started
|
|
138
|
+
task2_touch = task2_dry.touch
|
|
139
|
+
if task2_touch.exists():
|
|
140
|
+
with open(task2_wait, "w") as f:
|
|
141
|
+
f.write("done")
|
|
142
|
+
time.sleep(0.5)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def test_running_state_detection():
|
|
146
|
+
"""Test that running jobs are correctly detected from PID files."""
|
|
147
|
+
with tempfile.TemporaryDirectory(prefix="xpm_running_test_") as workdir:
|
|
148
|
+
workdir_path = Path(workdir)
|
|
149
|
+
experiment_id = "running_test"
|
|
150
|
+
|
|
151
|
+
with experiment(workdir_path, experiment_id) as _:
|
|
152
|
+
task = ControllableTask.C(value=1)
|
|
153
|
+
task.submit(run_mode=RunMode.DRY_RUN)
|
|
154
|
+
touch_path = task.touch
|
|
155
|
+
wait_path = task.wait
|
|
156
|
+
|
|
157
|
+
task = ControllableTask.C(value=1)
|
|
158
|
+
task.submit()
|
|
159
|
+
|
|
160
|
+
# Wait for task to start
|
|
161
|
+
counter = 0
|
|
162
|
+
while not touch_path.exists():
|
|
163
|
+
time.sleep(0.1)
|
|
164
|
+
counter += 1
|
|
165
|
+
if counter >= MAX_WAIT_ITERATIONS:
|
|
166
|
+
raise AssertionError("Timeout waiting for task to start")
|
|
167
|
+
|
|
168
|
+
# Create a fresh provider to check the running state
|
|
169
|
+
provider = WorkspaceStateProvider(workdir_path)
|
|
170
|
+
|
|
171
|
+
# Get all jobs on disk
|
|
172
|
+
jobs_base = workdir_path / "jobs"
|
|
173
|
+
job_paths = list(jobs_base.glob("*/*"))
|
|
174
|
+
assert len(job_paths) == 1, (
|
|
175
|
+
f"Expected 1 job on disk, found {len(job_paths)}"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# Check that the job is detected as running
|
|
179
|
+
job_path = job_paths[0]
|
|
180
|
+
task_id = job_path.parent.name
|
|
181
|
+
job_id = job_path.name
|
|
182
|
+
|
|
183
|
+
mock_job = provider._create_mock_job_from_path(job_path, task_id, job_id)
|
|
184
|
+
assert mock_job.state == JobState.RUNNING, (
|
|
185
|
+
f"Expected job state RUNNING, got {mock_job.state}"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# Signal task to finish
|
|
189
|
+
with open(wait_path, "w") as f:
|
|
190
|
+
f.write("done")
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def test_completed_job_not_stray():
|
|
194
|
+
"""Test that completed jobs are not detected as stray.
|
|
195
|
+
|
|
196
|
+
Scenario:
|
|
197
|
+
1. Start experiment, run and complete a job
|
|
198
|
+
2. Start a new run of the same experiment with a different job
|
|
199
|
+
3. The first job should be orphan (not in the new run) but NOT stray (not running)
|
|
200
|
+
"""
|
|
201
|
+
with tempfile.TemporaryDirectory(prefix="xpm_completed_test_") as workdir:
|
|
202
|
+
workdir_path = Path(workdir)
|
|
203
|
+
experiment_id = "completed_test"
|
|
204
|
+
|
|
205
|
+
# Phase 1: Create and complete a job
|
|
206
|
+
with experiment(workdir_path, experiment_id) as _:
|
|
207
|
+
task = ControllableTask.C(value=1)
|
|
208
|
+
task.submit(run_mode=RunMode.DRY_RUN)
|
|
209
|
+
touch_path = task.touch
|
|
210
|
+
wait_path = task.wait
|
|
211
|
+
|
|
212
|
+
task = ControllableTask.C(value=1)
|
|
213
|
+
task.submit()
|
|
214
|
+
|
|
215
|
+
# Wait for task to start
|
|
216
|
+
counter = 0
|
|
217
|
+
while not touch_path.exists():
|
|
218
|
+
time.sleep(0.1)
|
|
219
|
+
counter += 1
|
|
220
|
+
if counter >= MAX_WAIT_ITERATIONS:
|
|
221
|
+
raise AssertionError("Timeout waiting for task to start")
|
|
222
|
+
|
|
223
|
+
# Signal to finish immediately
|
|
224
|
+
with open(wait_path, "w") as f:
|
|
225
|
+
f.write("done")
|
|
226
|
+
|
|
227
|
+
# Phase 2: Start a new run with a different job
|
|
228
|
+
# This makes the old job an orphan (not in current run)
|
|
229
|
+
with experiment(workdir_path, experiment_id) as _:
|
|
230
|
+
task2 = ControllableTask.C(value=2)
|
|
231
|
+
task2.submit(run_mode=RunMode.DRY_RUN)
|
|
232
|
+
touch_path2 = task2.touch
|
|
233
|
+
wait_path2 = task2.wait
|
|
234
|
+
|
|
235
|
+
task2 = ControllableTask.C(value=2)
|
|
236
|
+
task2.submit()
|
|
237
|
+
|
|
238
|
+
# Wait for task2 to start
|
|
239
|
+
counter = 0
|
|
240
|
+
while not touch_path2.exists():
|
|
241
|
+
time.sleep(0.1)
|
|
242
|
+
counter += 1
|
|
243
|
+
if counter >= MAX_WAIT_ITERATIONS:
|
|
244
|
+
raise AssertionError("Timeout waiting for task2 to start")
|
|
245
|
+
|
|
246
|
+
# Now check that the first job is NOT stray (it's completed)
|
|
247
|
+
provider = WorkspaceStateProvider(workdir_path)
|
|
248
|
+
|
|
249
|
+
stray_jobs = provider.get_stray_jobs()
|
|
250
|
+
|
|
251
|
+
# Task1 is not running, so it should not be stray
|
|
252
|
+
# Task2 might appear as stray because status.json hasn't been flushed
|
|
253
|
+
# Filter to check that no DONE jobs are stray
|
|
254
|
+
done_stray = [j for j in stray_jobs if j.state == JobState.DONE]
|
|
255
|
+
assert len(done_stray) == 0, (
|
|
256
|
+
f"Expected 0 completed stray jobs, found {len(done_stray)}"
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
# Signal task2 to finish
|
|
260
|
+
with open(wait_path2, "w") as f:
|
|
261
|
+
f.write("done")
|
|
@@ -132,14 +132,13 @@ def test_restart(terminate):
|
|
|
132
132
|
def test_submitted_twice():
|
|
133
133
|
"""Check that a job cannot be submitted twice within the same experiment"""
|
|
134
134
|
with TemporaryExperiment("duplicate", maxwait=20):
|
|
135
|
-
|
|
136
135
|
task1 = SimpleTask.C(x=1)
|
|
137
136
|
o1 = task1.submit()
|
|
138
137
|
|
|
139
138
|
task2 = SimpleTask.C(x=1)
|
|
140
139
|
o2 = task2.submit()
|
|
141
140
|
|
|
142
|
-
print(o1)
|
|
141
|
+
print(o1) # noqa: T201
|
|
143
142
|
assert o1.task is not o2.task
|
|
144
143
|
assert task1.__xpm__.job is task2.__xpm__.job, f"{id(task1)} != {id(task2)}"
|
|
145
144
|
|
|
@@ -9,6 +9,7 @@ import pytest
|
|
|
9
9
|
import tempfile
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
import time
|
|
12
|
+
import hashlib
|
|
12
13
|
|
|
13
14
|
from experimaestro.tokens import CounterToken
|
|
14
15
|
from experimaestro.locking import LockError
|
|
@@ -16,22 +17,54 @@ from experimaestro.locking import LockError
|
|
|
16
17
|
pytestmark = pytest.mark.anyio
|
|
17
18
|
|
|
18
19
|
|
|
20
|
+
class MockIdentifier:
|
|
21
|
+
"""Mock identifier with hex() method."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, value: str):
|
|
24
|
+
self._hex = hashlib.sha256(value.encode()).hexdigest()
|
|
25
|
+
|
|
26
|
+
def hex(self):
|
|
27
|
+
return self._hex
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class MockXPM:
|
|
31
|
+
"""Mock __xpm__ object."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, name: str):
|
|
34
|
+
self.identifier = type("Identifier", (), {"main": MockIdentifier(name)})()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class MockConfig:
|
|
38
|
+
"""Mock config object."""
|
|
39
|
+
|
|
40
|
+
def __init__(self, name: str):
|
|
41
|
+
self.__xpm__ = MockXPM(name)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def create_mock_job(name: str, tmpdir: str):
|
|
45
|
+
"""Create a mock job with all required attributes."""
|
|
46
|
+
|
|
47
|
+
class MockJob:
|
|
48
|
+
task_id = "mock-task"
|
|
49
|
+
config = MockConfig(name)
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def identifier(self):
|
|
53
|
+
return f"mock-job-{name}"
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def basepath(self):
|
|
57
|
+
return Path(tmpdir) / name
|
|
58
|
+
|
|
59
|
+
return MockJob()
|
|
60
|
+
|
|
61
|
+
|
|
19
62
|
async def test_token_acquire_release():
|
|
20
63
|
"""Test basic token acquire and release"""
|
|
21
64
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
22
65
|
token = CounterToken("test-basic", Path(tmpdir) / "token", count=1)
|
|
23
66
|
|
|
24
|
-
|
|
25
|
-
class MockJob:
|
|
26
|
-
@property
|
|
27
|
-
def identifier(self):
|
|
28
|
-
return "mock-job-1"
|
|
29
|
-
|
|
30
|
-
@property
|
|
31
|
-
def basepath(self):
|
|
32
|
-
return Path(tmpdir) / "job1"
|
|
33
|
-
|
|
34
|
-
job = MockJob()
|
|
67
|
+
job = create_mock_job("1", tmpdir)
|
|
35
68
|
|
|
36
69
|
# Create dependency
|
|
37
70
|
dep = token.dependency(1)
|
|
@@ -52,20 +85,8 @@ async def test_token_blocking():
|
|
|
52
85
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
53
86
|
token = CounterToken("test-blocking", Path(tmpdir) / "token", count=1)
|
|
54
87
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
self.name = name
|
|
58
|
-
|
|
59
|
-
@property
|
|
60
|
-
def identifier(self):
|
|
61
|
-
return f"mock-job-{self.name}"
|
|
62
|
-
|
|
63
|
-
@property
|
|
64
|
-
def basepath(self):
|
|
65
|
-
return Path(tmpdir) / self.name
|
|
66
|
-
|
|
67
|
-
job1 = MockJob("1")
|
|
68
|
-
job2 = MockJob("2")
|
|
88
|
+
job1 = create_mock_job("1", tmpdir)
|
|
89
|
+
job2 = create_mock_job("2", tmpdir)
|
|
69
90
|
|
|
70
91
|
dep1 = token.dependency(1)
|
|
71
92
|
dep1.target = job1
|
|
@@ -99,20 +120,8 @@ async def test_token_notification():
|
|
|
99
120
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
100
121
|
token = CounterToken("test-notify", Path(tmpdir) / "token", count=1)
|
|
101
122
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
self.name = name
|
|
105
|
-
|
|
106
|
-
@property
|
|
107
|
-
def identifier(self):
|
|
108
|
-
return f"mock-job-{self.name}"
|
|
109
|
-
|
|
110
|
-
@property
|
|
111
|
-
def basepath(self):
|
|
112
|
-
return Path(tmpdir) / self.name
|
|
113
|
-
|
|
114
|
-
job1 = MockJob("1")
|
|
115
|
-
job2 = MockJob("2")
|
|
123
|
+
job1 = create_mock_job("1", tmpdir)
|
|
124
|
+
job2 = create_mock_job("2", tmpdir)
|
|
116
125
|
|
|
117
126
|
dep1 = token.dependency(1)
|
|
118
127
|
dep1.target = job1
|
|
@@ -151,20 +160,8 @@ async def test_token_multiple_waiting():
|
|
|
151
160
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
152
161
|
token = CounterToken("test-multiple", Path(tmpdir) / "token", count=1)
|
|
153
162
|
|
|
154
|
-
class MockJob:
|
|
155
|
-
def __init__(self, name):
|
|
156
|
-
self.name = name
|
|
157
|
-
|
|
158
|
-
@property
|
|
159
|
-
def identifier(self):
|
|
160
|
-
return f"mock-job-{self.name}"
|
|
161
|
-
|
|
162
|
-
@property
|
|
163
|
-
def basepath(self):
|
|
164
|
-
return Path(tmpdir) / self.name
|
|
165
|
-
|
|
166
163
|
# Acquire the token
|
|
167
|
-
job1 =
|
|
164
|
+
job1 = create_mock_job("1", tmpdir)
|
|
168
165
|
dep1 = token.dependency(1)
|
|
169
166
|
dep1.target = job1
|
|
170
167
|
lock1 = await dep1.aio_lock(timeout=0.5)
|
|
@@ -173,7 +170,7 @@ async def test_token_multiple_waiting():
|
|
|
173
170
|
acquired_order = []
|
|
174
171
|
|
|
175
172
|
async def acquire_task(name):
|
|
176
|
-
job =
|
|
173
|
+
job = create_mock_job(name, tmpdir)
|
|
177
174
|
dep = token.dependency(1)
|
|
178
175
|
dep.target = job
|
|
179
176
|
lock = await dep.aio_lock(timeout=10.0)
|
|
@@ -206,20 +203,8 @@ async def test_token_timeout_zero():
|
|
|
206
203
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
207
204
|
token = CounterToken("test-timeout-zero", Path(tmpdir) / "token", count=1)
|
|
208
205
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
self.name = name
|
|
212
|
-
|
|
213
|
-
@property
|
|
214
|
-
def identifier(self):
|
|
215
|
-
return f"mock-job-{self.name}"
|
|
216
|
-
|
|
217
|
-
@property
|
|
218
|
-
def basepath(self):
|
|
219
|
-
return Path(tmpdir) / self.name
|
|
220
|
-
|
|
221
|
-
job1 = MockJob("1")
|
|
222
|
-
job2 = MockJob("2")
|
|
206
|
+
job1 = create_mock_job("1", tmpdir)
|
|
207
|
+
job2 = create_mock_job("2", tmpdir)
|
|
223
208
|
|
|
224
209
|
dep1 = token.dependency(1)
|
|
225
210
|
dep1.target = job1
|
|
@@ -8,7 +8,7 @@ from pathlib import Path
|
|
|
8
8
|
|
|
9
9
|
import subprocess
|
|
10
10
|
from experimaestro import Task, Param
|
|
11
|
-
from experimaestro.tokens import CounterToken,
|
|
11
|
+
from experimaestro.tokens import CounterToken, TokenLockFile
|
|
12
12
|
from experimaestro.scheduler import JobState
|
|
13
13
|
from .utils import (
|
|
14
14
|
TemporaryExperiment,
|
|
@@ -101,7 +101,7 @@ def test_token_cleanup():
|
|
|
101
101
|
# Just lock directly (but without process)
|
|
102
102
|
# The absence of process should be detected right away
|
|
103
103
|
logging.info("Lock without process")
|
|
104
|
-
|
|
104
|
+
TokenLockFile.from_dependency(dependency)
|
|
105
105
|
task2 = dummy_task.C(x=2)
|
|
106
106
|
task2.add_dependencies(token.dependency(1)).submit()
|
|
107
107
|
xp.wait()
|
|
@@ -111,7 +111,7 @@ def test_token_cleanup():
|
|
|
111
111
|
job = dependency.target
|
|
112
112
|
with fasteners.InterProcessLock(job.lockpath):
|
|
113
113
|
logging.info("Creating dependency %s", dependency)
|
|
114
|
-
|
|
114
|
+
TokenLockFile.from_dependency(dependency)
|
|
115
115
|
lockingpath = job.path / "testtoken.signal"
|
|
116
116
|
command = [
|
|
117
117
|
sys.executable,
|
|
@@ -145,7 +145,7 @@ def test_token_monitor():
|
|
|
145
145
|
)
|
|
146
146
|
return task
|
|
147
147
|
|
|
148
|
-
with TemporaryExperiment("tokens1", maxwait=20
|
|
148
|
+
with TemporaryExperiment("tokens1", maxwait=20) as xp1:
|
|
149
149
|
# Use the same workspace for both experiments
|
|
150
150
|
with TemporaryExperiment(
|
|
151
151
|
"tokens2", workdir=xp1.workspace.path, maxwait=20
|
|
@@ -203,8 +203,7 @@ def test_token_reschedule():
|
|
|
203
203
|
|
|
204
204
|
# Create the locking path
|
|
205
205
|
logging.info(
|
|
206
|
-
"Both processes are ready:"
|
|
207
|
-
"allowing tasks to finish by writing in %s",
|
|
206
|
+
"Both processes are ready:allowing tasks to finish by writing in %s",
|
|
208
207
|
lockingpath,
|
|
209
208
|
)
|
|
210
209
|
lockingpath.write_text("Let's go")
|