experimaestro 2.0.0b8__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 +239 -126
- 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 +217 -50
- 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 +629 -53
- experimaestro/scheduler/dependencies.py +20 -16
- experimaestro/scheduler/experiment.py +732 -167
- experimaestro/scheduler/interfaces.py +316 -101
- experimaestro/scheduler/jobs.py +58 -20
- experimaestro/scheduler/remote/adaptive_sync.py +265 -0
- experimaestro/scheduler/remote/client.py +171 -117
- experimaestro/scheduler/remote/protocol.py +8 -193
- experimaestro/scheduler/remote/server.py +95 -71
- experimaestro/scheduler/services.py +53 -28
- experimaestro/scheduler/state_provider.py +663 -2430
- 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 +560 -99
- 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 +438 -1966
- 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.0b8.dist-info → experimaestro-2.0.0b17.dist-info}/METADATA +5 -6
- 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 -437
- experimaestro/scheduler/state_sync.py +0 -891
- 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.0b8.dist-info/RECORD +0 -187
- /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.0b8.dist-info → experimaestro-2.0.0b17.dist-info}/WHEEL +0 -0
- {experimaestro-2.0.0b8.dist-info → experimaestro-2.0.0b17.dist-info}/entry_points.txt +0 -0
- {experimaestro-2.0.0b8.dist-info → experimaestro-2.0.0b17.dist-info}/licenses/LICENSE +0 -0
|
@@ -30,10 +30,13 @@ from experimaestro.scheduler.remote.protocol import (
|
|
|
30
30
|
create_notification,
|
|
31
31
|
serialize_datetime,
|
|
32
32
|
deserialize_datetime,
|
|
33
|
-
serialize_job,
|
|
34
|
-
serialize_experiment,
|
|
35
|
-
serialize_run,
|
|
36
33
|
)
|
|
34
|
+
from experimaestro.scheduler.state_provider import (
|
|
35
|
+
MockJob,
|
|
36
|
+
MockExperiment,
|
|
37
|
+
MockService,
|
|
38
|
+
)
|
|
39
|
+
from experimaestro.notifications import LevelInformation
|
|
37
40
|
|
|
38
41
|
|
|
39
42
|
# =============================================================================
|
|
@@ -111,13 +114,17 @@ class TestProtocolMessages:
|
|
|
111
114
|
def test_notification_creation(self):
|
|
112
115
|
"""Test creating a JSON-RPC notification"""
|
|
113
116
|
notif_json = create_notification(
|
|
114
|
-
NotificationMethod.
|
|
117
|
+
NotificationMethod.STATE_EVENT,
|
|
118
|
+
{"event_type": "JobStateChangedEvent", "data": {"job_id": "job1"}},
|
|
115
119
|
)
|
|
116
120
|
data = json.loads(notif_json)
|
|
117
121
|
|
|
118
122
|
assert data["jsonrpc"] == JSONRPC_VERSION
|
|
119
|
-
assert data["method"] == "notification.
|
|
120
|
-
assert data["params"] == {
|
|
123
|
+
assert data["method"] == "notification.state_event"
|
|
124
|
+
assert data["params"] == {
|
|
125
|
+
"event_type": "JobStateChangedEvent",
|
|
126
|
+
"data": {"job_id": "job1"},
|
|
127
|
+
}
|
|
121
128
|
assert "id" not in data
|
|
122
129
|
|
|
123
130
|
def test_notification_parsing(self):
|
|
@@ -187,90 +194,231 @@ class TestDatetimeSerialization:
|
|
|
187
194
|
|
|
188
195
|
|
|
189
196
|
class TestJobSerialization:
|
|
190
|
-
"""Test job serialization"""
|
|
197
|
+
"""Test job serialization using state_dict"""
|
|
191
198
|
|
|
192
199
|
def test_serialize_mock_job(self):
|
|
193
|
-
"""Test serializing a MockJob
|
|
194
|
-
|
|
195
|
-
|
|
200
|
+
"""Test serializing a MockJob using state_dict()"""
|
|
201
|
+
# Note: tags, experiment_id, run_id are not part of MockJob
|
|
202
|
+
# as they are experiment-specific
|
|
196
203
|
job = MockJob(
|
|
197
204
|
identifier="job123",
|
|
198
205
|
task_id="task.MyTask",
|
|
199
|
-
locator="job123",
|
|
200
206
|
path=Path("/tmp/jobs/job123"),
|
|
201
207
|
state="running",
|
|
202
208
|
submittime=1704067200.0,
|
|
203
209
|
starttime=1704067300.0,
|
|
204
210
|
endtime=None,
|
|
205
211
|
progress=[],
|
|
206
|
-
tags={"tag1": "value1"},
|
|
207
|
-
experiment_id="exp1",
|
|
208
|
-
run_id="run1",
|
|
209
212
|
updated_at="2024-01-01T00:00:00",
|
|
210
213
|
)
|
|
211
214
|
|
|
212
|
-
result =
|
|
215
|
+
result = job.state_dict()
|
|
213
216
|
|
|
214
|
-
assert result["
|
|
217
|
+
assert result["job_id"] == "job123"
|
|
215
218
|
assert result["task_id"] == "task.MyTask"
|
|
216
219
|
assert result["path"] == "/tmp/jobs/job123"
|
|
217
220
|
# State is serialized from JobState enum - case may vary
|
|
218
221
|
assert result["state"].upper() == "RUNNING"
|
|
219
|
-
assert result["tags"] == {"tag1": "value1"}
|
|
220
|
-
assert result["experiment_id"] == "exp1"
|
|
221
|
-
assert result["run_id"] == "run1"
|
|
222
222
|
|
|
223
223
|
|
|
224
224
|
class TestExperimentSerialization:
|
|
225
|
-
"""Test experiment serialization"""
|
|
225
|
+
"""Test experiment serialization using state_dict"""
|
|
226
226
|
|
|
227
227
|
def test_serialize_mock_experiment(self):
|
|
228
|
-
"""Test serializing a MockExperiment
|
|
229
|
-
from experimaestro.scheduler.
|
|
228
|
+
"""Test serializing a MockExperiment using state_dict()"""
|
|
229
|
+
from experimaestro.scheduler.interfaces import ExperimentStatus
|
|
230
230
|
|
|
231
|
+
# New layout: experiments/{experiment_id}/{run_id}
|
|
231
232
|
exp = MockExperiment(
|
|
232
|
-
workdir=Path("/tmp/
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
finished_jobs=5,
|
|
236
|
-
failed_jobs=1,
|
|
237
|
-
updated_at="2024-01-01T12:00:00",
|
|
233
|
+
workdir=Path("/tmp/experiments/myexp/run_20240101"),
|
|
234
|
+
run_id="run_20240101",
|
|
235
|
+
status=ExperimentStatus.RUNNING,
|
|
238
236
|
started_at=1704067200.0,
|
|
239
237
|
ended_at=None,
|
|
240
238
|
hostname="server1",
|
|
241
239
|
)
|
|
242
240
|
|
|
243
|
-
result =
|
|
241
|
+
result = exp.state_dict()
|
|
244
242
|
|
|
245
243
|
assert result["experiment_id"] == "myexp"
|
|
246
|
-
assert result["
|
|
247
|
-
assert result["
|
|
248
|
-
assert result["total_jobs"] == 10
|
|
249
|
-
assert result["finished_jobs"] == 5
|
|
250
|
-
assert result["failed_jobs"] == 1
|
|
251
|
-
assert result["hostname"] == "server1"
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
class TestRunSerialization:
|
|
255
|
-
"""Test run serialization"""
|
|
256
|
-
|
|
257
|
-
def test_serialize_run_dict(self):
|
|
258
|
-
"""Test serializing a run dictionary"""
|
|
259
|
-
run_dict = {
|
|
260
|
-
"run_id": "run_20240101",
|
|
261
|
-
"experiment_id": "exp1",
|
|
262
|
-
"hostname": "server1",
|
|
263
|
-
"started_at": "2024-01-01T10:00:00",
|
|
264
|
-
"ended_at": None,
|
|
265
|
-
"status": "active",
|
|
266
|
-
}
|
|
244
|
+
assert result["run_id"] == "run_20240101"
|
|
245
|
+
assert result["status"] == "running"
|
|
267
246
|
|
|
268
|
-
result = serialize_run(run_dict)
|
|
269
247
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
248
|
+
class TestServiceSerialization:
|
|
249
|
+
"""Test service serialization using state_dict"""
|
|
250
|
+
|
|
251
|
+
def test_serialize_mock_service(self):
|
|
252
|
+
"""Test serializing a MockService using full_state_dict()
|
|
253
|
+
|
|
254
|
+
MockService.full_state_dict() preserves the original service class name
|
|
255
|
+
(not MockService's class name) to enable proper round-trip serialization.
|
|
256
|
+
"""
|
|
257
|
+
service = MockService(
|
|
258
|
+
service_id="svc123",
|
|
259
|
+
description_text="Test service",
|
|
260
|
+
state_dict_data={"port": 8080},
|
|
261
|
+
service_class="mymodule.MyService",
|
|
262
|
+
experiment_id="exp1",
|
|
263
|
+
run_id="run1",
|
|
264
|
+
url="http://localhost:8080",
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
result = service.full_state_dict()
|
|
268
|
+
|
|
269
|
+
assert result["service_id"] == "svc123"
|
|
270
|
+
assert result["description"] == "Test service"
|
|
271
|
+
# Preserves original service class, not MockService's class name
|
|
272
|
+
assert result["class"] == "mymodule.MyService"
|
|
273
|
+
assert result["state_dict"] == {"port": 8080}
|
|
274
|
+
|
|
275
|
+
def test_serialize_mock_service_no_class(self):
|
|
276
|
+
"""Test serializing a MockService with service_class=None"""
|
|
277
|
+
service = MockService(
|
|
278
|
+
service_id="svc123",
|
|
279
|
+
description_text="Test service",
|
|
280
|
+
state_dict_data={"port": 8080},
|
|
281
|
+
service_class=None,
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
result = service.full_state_dict()
|
|
285
|
+
|
|
286
|
+
# class is always present, even when None
|
|
287
|
+
assert result["class"] is None
|
|
288
|
+
assert result["service_id"] == "svc123"
|
|
289
|
+
assert result["description"] == "Test service"
|
|
290
|
+
assert result["state_dict"] == {"port": 8080}
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
# =============================================================================
|
|
294
|
+
# SSH Round-Trip Tests (Mock → full_state_dict → client → Mock)
|
|
295
|
+
# =============================================================================
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
class MockSSHClient:
|
|
299
|
+
"""Minimal mock of SSHStateProviderClient for testing deserialization"""
|
|
300
|
+
|
|
301
|
+
def __init__(self, remote_workspace: str, local_cache_dir: Path):
|
|
302
|
+
self.remote_workspace = remote_workspace
|
|
303
|
+
self.local_cache_dir = local_cache_dir
|
|
304
|
+
|
|
305
|
+
def _parse_datetime_to_timestamp(self, value) -> float | None:
|
|
306
|
+
"""Convert datetime value to Unix timestamp"""
|
|
307
|
+
if value is None:
|
|
308
|
+
return None
|
|
309
|
+
if isinstance(value, (int, float)):
|
|
310
|
+
return float(value)
|
|
311
|
+
if isinstance(value, str):
|
|
312
|
+
try:
|
|
313
|
+
dt = datetime.fromisoformat(value)
|
|
314
|
+
return dt.timestamp()
|
|
315
|
+
except ValueError:
|
|
316
|
+
return None
|
|
317
|
+
if isinstance(value, datetime):
|
|
318
|
+
return value.timestamp()
|
|
319
|
+
return None
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
class TestSSHRoundTrip:
|
|
323
|
+
"""Test SSH round-trip serialization: Mock → state_dict → from_state_dict"""
|
|
324
|
+
|
|
325
|
+
def test_mockjob_ssh_roundtrip(self, tmp_path: Path):
|
|
326
|
+
"""Test MockJob round-trip through SSH serialization path"""
|
|
327
|
+
from experimaestro.scheduler.remote.client import SSHStateProviderClient
|
|
328
|
+
|
|
329
|
+
workspace_path = tmp_path / "workspace"
|
|
330
|
+
workspace_path.mkdir()
|
|
331
|
+
job_path = workspace_path / "jobs" / "my.Task" / "abc123"
|
|
332
|
+
|
|
333
|
+
# Create original MockJob
|
|
334
|
+
# Note: tags, experiment_id, run_id are not part of MockJob
|
|
335
|
+
original = MockJob(
|
|
336
|
+
identifier="abc123",
|
|
337
|
+
task_id="my.Task",
|
|
338
|
+
path=job_path,
|
|
339
|
+
state="running",
|
|
340
|
+
submittime=1234567890.0,
|
|
341
|
+
starttime=1234567891.0,
|
|
342
|
+
endtime=None,
|
|
343
|
+
progress=[LevelInformation(level=0, progress=0.5, desc="halfway")],
|
|
344
|
+
updated_at="2024-01-01T00:00:00",
|
|
345
|
+
exit_code=None,
|
|
346
|
+
retry_count=2,
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
# Server-side: serialize using state_dict
|
|
350
|
+
serialized = original.state_dict()
|
|
351
|
+
|
|
352
|
+
# Client-side: deserialize using _dict_to_job (which uses from_state_dict)
|
|
353
|
+
mock_client = MockSSHClient(
|
|
354
|
+
remote_workspace=str(workspace_path),
|
|
355
|
+
local_cache_dir=workspace_path,
|
|
356
|
+
)
|
|
357
|
+
restored = SSHStateProviderClient._dict_to_job(mock_client, serialized)
|
|
358
|
+
|
|
359
|
+
# Verify equality by comparing state_dict outputs
|
|
360
|
+
assert original.state_dict() == restored.state_dict()
|
|
361
|
+
|
|
362
|
+
def test_mockexperiment_ssh_roundtrip(self, tmp_path: Path):
|
|
363
|
+
"""Test MockExperiment round-trip through SSH serialization path"""
|
|
364
|
+
from experimaestro.scheduler.interfaces import ExperimentStatus
|
|
365
|
+
from experimaestro.scheduler.remote.client import SSHStateProviderClient
|
|
366
|
+
|
|
367
|
+
workspace_path = tmp_path / "workspace"
|
|
368
|
+
workspace_path.mkdir()
|
|
369
|
+
# New layout: experiments/{exp-id}/{run-id}/
|
|
370
|
+
(workspace_path / "experiments" / "test_exp" / "run_001").mkdir(parents=True)
|
|
371
|
+
|
|
372
|
+
# Create original MockExperiment
|
|
373
|
+
original = MockExperiment(
|
|
374
|
+
workdir=workspace_path / "experiments" / "test_exp" / "run_001",
|
|
375
|
+
run_id="run_001",
|
|
376
|
+
status=ExperimentStatus.RUNNING,
|
|
377
|
+
started_at=1234567890.0,
|
|
378
|
+
ended_at=None,
|
|
379
|
+
hostname="testhost",
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
# Server-side: serialize using state_dict
|
|
383
|
+
serialized = original.state_dict()
|
|
384
|
+
|
|
385
|
+
# Client-side: deserialize using _dict_to_experiment
|
|
386
|
+
mock_client = MockSSHClient(
|
|
387
|
+
remote_workspace=str(workspace_path),
|
|
388
|
+
local_cache_dir=workspace_path,
|
|
389
|
+
)
|
|
390
|
+
restored = SSHStateProviderClient._dict_to_experiment(mock_client, serialized)
|
|
391
|
+
|
|
392
|
+
# Verify equality by comparing state_dict outputs
|
|
393
|
+
assert original.state_dict() == restored.state_dict()
|
|
394
|
+
|
|
395
|
+
def test_mockservice_ssh_roundtrip(self, tmp_path: Path):
|
|
396
|
+
"""Test MockService round-trip through SSH serialization path"""
|
|
397
|
+
from experimaestro.scheduler.remote.client import SSHStateProviderClient
|
|
398
|
+
|
|
399
|
+
# Create original MockService (no service_class to avoid Service recreation)
|
|
400
|
+
original = MockService(
|
|
401
|
+
service_id="svc_123",
|
|
402
|
+
description_text="Test service description",
|
|
403
|
+
state_dict_data={"port": 8080, "host": "localhost"},
|
|
404
|
+
service_class=None,
|
|
405
|
+
experiment_id="exp1",
|
|
406
|
+
run_id="run1",
|
|
407
|
+
url="http://localhost:8080",
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
# Server-side: serialize using full_state_dict
|
|
411
|
+
serialized = original.full_state_dict()
|
|
412
|
+
|
|
413
|
+
# Client-side: deserialize using _dict_to_service
|
|
414
|
+
mock_client = MockSSHClient(
|
|
415
|
+
remote_workspace="/tmp/workspace",
|
|
416
|
+
local_cache_dir=tmp_path,
|
|
417
|
+
)
|
|
418
|
+
restored = SSHStateProviderClient._dict_to_service(mock_client, serialized)
|
|
419
|
+
|
|
420
|
+
# Verify equality by comparing full_state_dict outputs
|
|
421
|
+
assert original.full_state_dict() == restored.full_state_dict()
|
|
274
422
|
|
|
275
423
|
|
|
276
424
|
# =============================================================================
|
|
@@ -312,15 +460,13 @@ class TestServerRequestHandling:
|
|
|
312
460
|
|
|
313
461
|
def test_handle_get_experiments(self, server_with_mock, mock_state_provider):
|
|
314
462
|
"""Test handling get_experiments request"""
|
|
315
|
-
from experimaestro.scheduler.
|
|
463
|
+
from experimaestro.scheduler.interfaces import ExperimentStatus
|
|
316
464
|
|
|
465
|
+
# New layout: experiments/{exp-id}/{run-id}
|
|
317
466
|
mock_exp = MockExperiment(
|
|
318
|
-
workdir=Path("/tmp/
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
finished_jobs=3,
|
|
322
|
-
failed_jobs=0,
|
|
323
|
-
updated_at="2024-01-01T00:00:00",
|
|
467
|
+
workdir=Path("/tmp/experiments/exp1/run1"),
|
|
468
|
+
run_id="run1",
|
|
469
|
+
status=ExperimentStatus.RUNNING,
|
|
324
470
|
)
|
|
325
471
|
mock_state_provider.get_experiments.return_value = [mock_exp]
|
|
326
472
|
|
|
@@ -330,23 +476,67 @@ class TestServerRequestHandling:
|
|
|
330
476
|
assert result[0]["experiment_id"] == "exp1"
|
|
331
477
|
mock_state_provider.get_experiments.assert_called_once_with(since=None)
|
|
332
478
|
|
|
479
|
+
def test_handle_get_experiment(self, server_with_mock, mock_state_provider):
|
|
480
|
+
"""Test handling get_experiment request"""
|
|
481
|
+
from experimaestro.scheduler.interfaces import ExperimentStatus
|
|
482
|
+
|
|
483
|
+
# New layout: experiments/{exp-id}/{run-id}
|
|
484
|
+
mock_exp = MockExperiment(
|
|
485
|
+
workdir=Path("/tmp/experiments/exp1/run1"),
|
|
486
|
+
run_id="run1",
|
|
487
|
+
status=ExperimentStatus.RUNNING,
|
|
488
|
+
)
|
|
489
|
+
mock_state_provider.get_experiment.return_value = mock_exp
|
|
490
|
+
|
|
491
|
+
result = server_with_mock._handle_get_experiment({"experiment_id": "exp1"})
|
|
492
|
+
|
|
493
|
+
assert result["experiment_id"] == "exp1"
|
|
494
|
+
assert result["run_id"] == "run1"
|
|
495
|
+
mock_state_provider.get_experiment.assert_called_once_with("exp1")
|
|
496
|
+
|
|
497
|
+
def test_handle_get_experiment_not_found(
|
|
498
|
+
self, server_with_mock, mock_state_provider
|
|
499
|
+
):
|
|
500
|
+
"""Test handling get_experiment when experiment not found"""
|
|
501
|
+
mock_state_provider.get_experiment.return_value = None
|
|
502
|
+
|
|
503
|
+
result = server_with_mock._handle_get_experiment(
|
|
504
|
+
{"experiment_id": "nonexistent"}
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
assert result is None
|
|
508
|
+
|
|
509
|
+
def test_handle_get_experiment_runs(self, server_with_mock, mock_state_provider):
|
|
510
|
+
"""Test handling get_experiment_runs request"""
|
|
511
|
+
from experimaestro.scheduler.interfaces import ExperimentStatus
|
|
512
|
+
|
|
513
|
+
mock_run = MockExperiment(
|
|
514
|
+
workdir=Path("/tmp/experiments/exp1/run1"),
|
|
515
|
+
run_id="run1",
|
|
516
|
+
status=ExperimentStatus.DONE,
|
|
517
|
+
hostname="server1",
|
|
518
|
+
started_at=1704067200.0,
|
|
519
|
+
ended_at=1704070800.0,
|
|
520
|
+
)
|
|
521
|
+
mock_state_provider.get_experiment_runs.return_value = [mock_run]
|
|
522
|
+
|
|
523
|
+
result = server_with_mock._handle_get_experiment_runs({"experiment_id": "exp1"})
|
|
524
|
+
|
|
525
|
+
assert len(result) == 1
|
|
526
|
+
assert result[0]["run_id"] == "run1"
|
|
527
|
+
assert result[0]["status"] == "done"
|
|
528
|
+
|
|
333
529
|
def test_handle_get_jobs(self, server_with_mock, mock_state_provider):
|
|
334
530
|
"""Test handling get_jobs request"""
|
|
335
|
-
from experimaestro.scheduler.state_provider import MockJob
|
|
336
|
-
|
|
337
531
|
mock_job = MockJob(
|
|
338
532
|
identifier="job1",
|
|
339
533
|
task_id="task.Test",
|
|
340
|
-
locator="job1",
|
|
341
534
|
path=Path("/tmp/jobs/job1"),
|
|
342
535
|
state="done",
|
|
343
536
|
submittime=None,
|
|
344
537
|
starttime=None,
|
|
345
538
|
endtime=None,
|
|
346
539
|
progress=[],
|
|
347
|
-
tags={},
|
|
348
|
-
experiment_id="exp1",
|
|
349
|
-
run_id="run1",
|
|
350
540
|
updated_at="",
|
|
351
541
|
)
|
|
352
542
|
mock_state_provider.get_jobs.return_value = [mock_job]
|
|
@@ -359,7 +549,196 @@ class TestServerRequestHandling:
|
|
|
359
549
|
)
|
|
360
550
|
|
|
361
551
|
assert len(result) == 1
|
|
362
|
-
assert result[0]["
|
|
552
|
+
assert result[0]["job_id"] == "job1"
|
|
553
|
+
|
|
554
|
+
def test_handle_get_job(self, server_with_mock, mock_state_provider):
|
|
555
|
+
"""Test handling get_job request"""
|
|
556
|
+
mock_job = MockJob(
|
|
557
|
+
identifier="job1",
|
|
558
|
+
task_id="task.Test",
|
|
559
|
+
path=Path("/tmp/jobs/job1"),
|
|
560
|
+
state="running",
|
|
561
|
+
submittime=1704067200.0,
|
|
562
|
+
starttime=1704067300.0,
|
|
563
|
+
endtime=None,
|
|
564
|
+
progress=[],
|
|
565
|
+
updated_at="2024-01-01T00:00:00",
|
|
566
|
+
)
|
|
567
|
+
mock_state_provider.get_job.return_value = mock_job
|
|
568
|
+
|
|
569
|
+
result = server_with_mock._handle_get_job(
|
|
570
|
+
{"job_id": "job1", "experiment_id": "exp1", "run_id": "run1"}
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
assert result["job_id"] == "job1"
|
|
574
|
+
assert result["task_id"] == "task.Test"
|
|
575
|
+
|
|
576
|
+
def test_handle_get_job_not_found(self, server_with_mock, mock_state_provider):
|
|
577
|
+
"""Test handling get_job when job not found"""
|
|
578
|
+
mock_state_provider.get_job.return_value = None
|
|
579
|
+
|
|
580
|
+
result = server_with_mock._handle_get_job(
|
|
581
|
+
{"job_id": "nonexistent", "experiment_id": "exp1", "run_id": "run1"}
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
assert result is None
|
|
585
|
+
|
|
586
|
+
def test_handle_get_all_jobs(self, server_with_mock, mock_state_provider):
|
|
587
|
+
"""Test handling get_all_jobs request"""
|
|
588
|
+
mock_job1 = MockJob(
|
|
589
|
+
identifier="job1",
|
|
590
|
+
task_id="task.Test1",
|
|
591
|
+
path=Path("/tmp/jobs/job1"),
|
|
592
|
+
state="done",
|
|
593
|
+
submittime=None,
|
|
594
|
+
starttime=None,
|
|
595
|
+
endtime=None,
|
|
596
|
+
progress=[],
|
|
597
|
+
updated_at="",
|
|
598
|
+
)
|
|
599
|
+
mock_job2 = MockJob(
|
|
600
|
+
identifier="job2",
|
|
601
|
+
task_id="task.Test2",
|
|
602
|
+
path=Path("/tmp/jobs/job2"),
|
|
603
|
+
state="running",
|
|
604
|
+
submittime=None,
|
|
605
|
+
starttime=None,
|
|
606
|
+
endtime=None,
|
|
607
|
+
progress=[],
|
|
608
|
+
updated_at="",
|
|
609
|
+
)
|
|
610
|
+
mock_state_provider.get_all_jobs.return_value = [mock_job1, mock_job2]
|
|
611
|
+
|
|
612
|
+
result = server_with_mock._handle_get_all_jobs({"state": None, "tags": None})
|
|
613
|
+
|
|
614
|
+
assert len(result) == 2
|
|
615
|
+
assert result[0]["job_id"] == "job1"
|
|
616
|
+
assert result[1]["job_id"] == "job2"
|
|
617
|
+
|
|
618
|
+
def test_handle_get_services(self, server_with_mock, mock_state_provider):
|
|
619
|
+
"""Test handling get_services request"""
|
|
620
|
+
mock_service = MockService(
|
|
621
|
+
service_id="svc1",
|
|
622
|
+
description_text="Test service",
|
|
623
|
+
state_dict_data={"port": 8080},
|
|
624
|
+
service_class="mymodule.MyService",
|
|
625
|
+
experiment_id="exp1",
|
|
626
|
+
run_id="run1",
|
|
627
|
+
url="http://localhost:8080",
|
|
628
|
+
)
|
|
629
|
+
mock_state_provider.get_services.return_value = [mock_service]
|
|
630
|
+
|
|
631
|
+
result = server_with_mock._handle_get_services(
|
|
632
|
+
{"experiment_id": "exp1", "run_id": "run1"}
|
|
633
|
+
)
|
|
634
|
+
|
|
635
|
+
assert len(result) == 1
|
|
636
|
+
assert result[0]["service_id"] == "svc1"
|
|
637
|
+
assert result[0]["description"] == "Test service"
|
|
638
|
+
|
|
639
|
+
def test_handle_get_tags_map(self, server_with_mock, mock_state_provider):
|
|
640
|
+
"""Test handling get_tags_map request"""
|
|
641
|
+
mock_state_provider.get_tags_map.return_value = {
|
|
642
|
+
"job1": {"model": "bert", "dataset": "squad"},
|
|
643
|
+
"job2": {"model": "gpt"},
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
result = server_with_mock._handle_get_tags_map(
|
|
647
|
+
{"experiment_id": "exp1", "run_id": "run1"}
|
|
648
|
+
)
|
|
649
|
+
|
|
650
|
+
assert result["job1"]["model"] == "bert"
|
|
651
|
+
assert result["job2"]["model"] == "gpt"
|
|
652
|
+
|
|
653
|
+
def test_handle_get_tags_map_missing_experiment(self, server_with_mock):
|
|
654
|
+
"""Test get_tags_map raises TypeError when experiment_id missing"""
|
|
655
|
+
with pytest.raises(TypeError, match="experiment_id is required"):
|
|
656
|
+
server_with_mock._handle_get_tags_map({"run_id": "run1"})
|
|
657
|
+
|
|
658
|
+
def test_handle_get_dependencies_map(self, server_with_mock, mock_state_provider):
|
|
659
|
+
"""Test handling get_dependencies_map request"""
|
|
660
|
+
mock_state_provider.get_dependencies_map.return_value = {
|
|
661
|
+
"job2": ["job1"],
|
|
662
|
+
"job3": ["job1", "job2"],
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
result = server_with_mock._handle_get_dependencies_map(
|
|
666
|
+
{"experiment_id": "exp1", "run_id": "run1"}
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
assert result["job2"] == ["job1"]
|
|
670
|
+
assert result["job3"] == ["job1", "job2"]
|
|
671
|
+
|
|
672
|
+
def test_handle_get_dependencies_map_missing_experiment(self, server_with_mock):
|
|
673
|
+
"""Test get_dependencies_map raises TypeError when experiment_id missing"""
|
|
674
|
+
with pytest.raises(TypeError, match="experiment_id is required"):
|
|
675
|
+
server_with_mock._handle_get_dependencies_map({"run_id": "run1"})
|
|
676
|
+
|
|
677
|
+
def test_handle_kill_job(self, server_with_mock, mock_state_provider):
|
|
678
|
+
"""Test handling kill_job request"""
|
|
679
|
+
mock_job = MockJob(
|
|
680
|
+
identifier="job1",
|
|
681
|
+
task_id="task.Test",
|
|
682
|
+
path=Path("/tmp/jobs/job1"),
|
|
683
|
+
state="running",
|
|
684
|
+
submittime=None,
|
|
685
|
+
starttime=None,
|
|
686
|
+
endtime=None,
|
|
687
|
+
progress=[],
|
|
688
|
+
updated_at="",
|
|
689
|
+
)
|
|
690
|
+
mock_state_provider.get_job.return_value = mock_job
|
|
691
|
+
mock_state_provider.kill_job.return_value = True
|
|
692
|
+
|
|
693
|
+
result = server_with_mock._handle_kill_job(
|
|
694
|
+
{"job_id": "job1", "experiment_id": "exp1", "run_id": "run1"}
|
|
695
|
+
)
|
|
696
|
+
|
|
697
|
+
assert result["success"] is True
|
|
698
|
+
|
|
699
|
+
def test_handle_kill_job_not_found(self, server_with_mock, mock_state_provider):
|
|
700
|
+
"""Test handling kill_job when job not found"""
|
|
701
|
+
mock_state_provider.get_job.return_value = None
|
|
702
|
+
|
|
703
|
+
result = server_with_mock._handle_kill_job(
|
|
704
|
+
{"job_id": "nonexistent", "experiment_id": "exp1", "run_id": "run1"}
|
|
705
|
+
)
|
|
706
|
+
|
|
707
|
+
assert result["success"] is False
|
|
708
|
+
assert "error" in result
|
|
709
|
+
|
|
710
|
+
def test_handle_clean_job(self, server_with_mock, mock_state_provider):
|
|
711
|
+
"""Test handling clean_job request"""
|
|
712
|
+
mock_job = MockJob(
|
|
713
|
+
identifier="job1",
|
|
714
|
+
task_id="task.Test",
|
|
715
|
+
path=Path("/tmp/jobs/job1"),
|
|
716
|
+
state="done",
|
|
717
|
+
submittime=None,
|
|
718
|
+
starttime=None,
|
|
719
|
+
endtime=None,
|
|
720
|
+
progress=[],
|
|
721
|
+
updated_at="",
|
|
722
|
+
)
|
|
723
|
+
mock_state_provider.get_job.return_value = mock_job
|
|
724
|
+
mock_state_provider.clean_job.return_value = True
|
|
725
|
+
|
|
726
|
+
result = server_with_mock._handle_clean_job(
|
|
727
|
+
{"job_id": "job1", "experiment_id": "exp1", "run_id": "run1"}
|
|
728
|
+
)
|
|
729
|
+
|
|
730
|
+
assert result["success"] is True
|
|
731
|
+
|
|
732
|
+
def test_handle_clean_job_not_found(self, server_with_mock, mock_state_provider):
|
|
733
|
+
"""Test handling clean_job when job not found"""
|
|
734
|
+
mock_state_provider.get_job.return_value = None
|
|
735
|
+
|
|
736
|
+
result = server_with_mock._handle_clean_job(
|
|
737
|
+
{"job_id": "nonexistent", "experiment_id": "exp1", "run_id": "run1"}
|
|
738
|
+
)
|
|
739
|
+
|
|
740
|
+
assert result["success"] is False
|
|
741
|
+
assert "error" in result
|
|
363
742
|
|
|
364
743
|
def test_handle_get_sync_info(self, server_with_mock):
|
|
365
744
|
"""Test handling get_sync_info request"""
|
|
@@ -412,22 +791,112 @@ class TestClientServerIntegration:
|
|
|
412
791
|
assert isinstance(resp_msg, RPCResponse)
|
|
413
792
|
assert resp_msg.result == []
|
|
414
793
|
|
|
415
|
-
def
|
|
416
|
-
"""Test notification message handling"""
|
|
794
|
+
def test_notification_job_updated(self):
|
|
795
|
+
"""Test job_updated notification message handling via STATE_EVENT"""
|
|
417
796
|
notification = create_notification(
|
|
418
|
-
NotificationMethod.
|
|
797
|
+
NotificationMethod.STATE_EVENT,
|
|
419
798
|
{
|
|
420
|
-
"
|
|
421
|
-
"
|
|
422
|
-
|
|
423
|
-
|
|
799
|
+
"event_type": "JobStateChangedEvent",
|
|
800
|
+
"data": {
|
|
801
|
+
"job_id": "job1",
|
|
802
|
+
"experiment_id": "exp1",
|
|
803
|
+
"run_id": "run1",
|
|
804
|
+
},
|
|
424
805
|
},
|
|
425
806
|
)
|
|
426
807
|
|
|
427
808
|
msg = parse_message(notification)
|
|
428
809
|
assert isinstance(msg, RPCNotification)
|
|
429
|
-
assert msg.method == "notification.
|
|
430
|
-
assert msg.params["
|
|
810
|
+
assert msg.method == "notification.state_event"
|
|
811
|
+
assert msg.params["event_type"] == "JobStateChangedEvent"
|
|
812
|
+
assert msg.params["data"]["job_id"] == "job1"
|
|
813
|
+
|
|
814
|
+
def test_notification_experiment_updated(self):
|
|
815
|
+
"""Test experiment_updated notification message handling via STATE_EVENT"""
|
|
816
|
+
notification = create_notification(
|
|
817
|
+
NotificationMethod.STATE_EVENT,
|
|
818
|
+
{
|
|
819
|
+
"event_type": "ExperimentUpdatedEvent",
|
|
820
|
+
"data": {
|
|
821
|
+
"experiment_id": "exp1",
|
|
822
|
+
},
|
|
823
|
+
},
|
|
824
|
+
)
|
|
825
|
+
|
|
826
|
+
msg = parse_message(notification)
|
|
827
|
+
assert isinstance(msg, RPCNotification)
|
|
828
|
+
assert msg.method == "notification.state_event"
|
|
829
|
+
assert msg.params["event_type"] == "ExperimentUpdatedEvent"
|
|
830
|
+
assert msg.params["data"]["experiment_id"] == "exp1"
|
|
831
|
+
|
|
832
|
+
def test_notification_run_updated(self):
|
|
833
|
+
"""Test run_updated notification message handling via STATE_EVENT"""
|
|
834
|
+
notification = create_notification(
|
|
835
|
+
NotificationMethod.STATE_EVENT,
|
|
836
|
+
{
|
|
837
|
+
"event_type": "RunUpdatedEvent",
|
|
838
|
+
"data": {
|
|
839
|
+
"experiment_id": "exp1",
|
|
840
|
+
"run_id": "run1",
|
|
841
|
+
},
|
|
842
|
+
},
|
|
843
|
+
)
|
|
844
|
+
|
|
845
|
+
msg = parse_message(notification)
|
|
846
|
+
assert isinstance(msg, RPCNotification)
|
|
847
|
+
assert msg.method == "notification.state_event"
|
|
848
|
+
assert msg.params["event_type"] == "RunUpdatedEvent"
|
|
849
|
+
assert msg.params["data"]["run_id"] == "run1"
|
|
850
|
+
|
|
851
|
+
def test_notification_service_updated(self):
|
|
852
|
+
"""Test service_updated notification message handling via STATE_EVENT"""
|
|
853
|
+
notification = create_notification(
|
|
854
|
+
NotificationMethod.STATE_EVENT,
|
|
855
|
+
{
|
|
856
|
+
"event_type": "ServiceAddedEvent",
|
|
857
|
+
"data": {
|
|
858
|
+
"experiment_id": "exp1",
|
|
859
|
+
"run_id": "run1",
|
|
860
|
+
"service_id": "svc1",
|
|
861
|
+
},
|
|
862
|
+
},
|
|
863
|
+
)
|
|
864
|
+
|
|
865
|
+
msg = parse_message(notification)
|
|
866
|
+
assert isinstance(msg, RPCNotification)
|
|
867
|
+
assert msg.method == "notification.state_event"
|
|
868
|
+
assert msg.params["event_type"] == "ServiceAddedEvent"
|
|
869
|
+
assert msg.params["data"]["service_id"] == "svc1"
|
|
870
|
+
|
|
871
|
+
def test_notification_file_changed(self):
|
|
872
|
+
"""Test file_changed notification message handling"""
|
|
873
|
+
notification = create_notification(
|
|
874
|
+
NotificationMethod.FILE_CHANGED,
|
|
875
|
+
{
|
|
876
|
+
"path": "/workspace/logs/job.log",
|
|
877
|
+
"event_type": "modified",
|
|
878
|
+
},
|
|
879
|
+
)
|
|
880
|
+
|
|
881
|
+
msg = parse_message(notification)
|
|
882
|
+
assert isinstance(msg, RPCNotification)
|
|
883
|
+
assert msg.method == "notification.file_changed"
|
|
884
|
+
assert msg.params["path"] == "/workspace/logs/job.log"
|
|
885
|
+
|
|
886
|
+
def test_notification_shutdown(self):
|
|
887
|
+
"""Test shutdown notification message handling"""
|
|
888
|
+
notification = create_notification(
|
|
889
|
+
NotificationMethod.SHUTDOWN,
|
|
890
|
+
{
|
|
891
|
+
"reason": "server_stop",
|
|
892
|
+
"code": 0,
|
|
893
|
+
},
|
|
894
|
+
)
|
|
895
|
+
|
|
896
|
+
msg = parse_message(notification)
|
|
897
|
+
assert isinstance(msg, RPCNotification)
|
|
898
|
+
assert msg.method == "notification.shutdown"
|
|
899
|
+
assert msg.params["reason"] == "server_stop"
|
|
431
900
|
|
|
432
901
|
|
|
433
902
|
# =============================================================================
|
|
@@ -456,19 +925,17 @@ class TestClientDataConversion:
|
|
|
456
925
|
|
|
457
926
|
def test_dict_to_job(self, client, tmp_path):
|
|
458
927
|
"""Test converting dictionary to MockJob"""
|
|
928
|
+
# Note: tags, experiment_id, run_id are not part of MockJob
|
|
929
|
+
# as they are experiment-specific
|
|
459
930
|
job_dict = {
|
|
460
|
-
"
|
|
931
|
+
"job_id": "job123",
|
|
461
932
|
"task_id": "task.MyTask",
|
|
462
|
-
"locator": "job123",
|
|
463
933
|
"path": "/remote/workspace/jobs/job123",
|
|
464
934
|
"state": "running",
|
|
465
|
-
"
|
|
466
|
-
"
|
|
467
|
-
"
|
|
935
|
+
"submitted_time": "2024-01-01T10:00:00",
|
|
936
|
+
"started_time": "2024-01-01T10:01:00",
|
|
937
|
+
"ended_time": None,
|
|
468
938
|
"progress": [],
|
|
469
|
-
"tags": {"key": "value"},
|
|
470
|
-
"experiment_id": "exp1",
|
|
471
|
-
"run_id": "run1",
|
|
472
939
|
}
|
|
473
940
|
|
|
474
941
|
job = client._dict_to_job(job_dict)
|
|
@@ -477,18 +944,15 @@ class TestClientDataConversion:
|
|
|
477
944
|
assert job.task_id == "task.MyTask"
|
|
478
945
|
# Path should be mapped to local cache
|
|
479
946
|
assert job.path == tmp_path / "jobs/job123"
|
|
480
|
-
assert job.tags == {"key": "value"}
|
|
481
947
|
|
|
482
948
|
def test_dict_to_experiment(self, client, tmp_path):
|
|
483
949
|
"""Test converting dictionary to MockExperiment"""
|
|
950
|
+
# New layout: experiments/{experiment_id}/{run_id}
|
|
484
951
|
exp_dict = {
|
|
485
952
|
"experiment_id": "myexp",
|
|
486
|
-
"workdir": "/remote/workspace/
|
|
487
|
-
"
|
|
488
|
-
"
|
|
489
|
-
"finished_jobs": 5,
|
|
490
|
-
"failed_jobs": 1,
|
|
491
|
-
"updated_at": "2024-01-01T12:00:00",
|
|
953
|
+
"workdir": "/remote/workspace/experiments/myexp/run1",
|
|
954
|
+
"run_id": "run1",
|
|
955
|
+
"status": "running",
|
|
492
956
|
"hostname": "server1",
|
|
493
957
|
}
|
|
494
958
|
|
|
@@ -496,20 +960,17 @@ class TestClientDataConversion:
|
|
|
496
960
|
|
|
497
961
|
assert exp.experiment_id == "myexp"
|
|
498
962
|
# Path should be mapped to local cache
|
|
499
|
-
assert exp.workdir == tmp_path / "
|
|
500
|
-
assert exp.total_jobs == 10
|
|
963
|
+
assert exp.workdir == tmp_path / "experiments/myexp/run1"
|
|
501
964
|
assert exp.hostname == "server1"
|
|
502
965
|
|
|
503
|
-
def test_path_mapping_outside_workspace(self, client
|
|
966
|
+
def test_path_mapping_outside_workspace(self, client):
|
|
504
967
|
"""Test path mapping for paths outside remote workspace"""
|
|
505
968
|
job_dict = {
|
|
506
|
-
"
|
|
969
|
+
"job_id": "job123",
|
|
507
970
|
"task_id": "task.MyTask",
|
|
508
|
-
"locator": "job123",
|
|
509
971
|
"path": "/other/path/job123", # Not under remote_workspace
|
|
510
972
|
"state": "done",
|
|
511
973
|
"progress": [],
|
|
512
|
-
"tags": {},
|
|
513
974
|
}
|
|
514
975
|
|
|
515
976
|
job = client._dict_to_job(job_dict)
|