experimaestro 1.11.1__py3-none-any.whl → 2.0.0rc0__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.

Files changed (37) hide show
  1. experimaestro/annotations.py +1 -1
  2. experimaestro/cli/__init__.py +10 -11
  3. experimaestro/cli/progress.py +269 -0
  4. experimaestro/core/identifier.py +11 -2
  5. experimaestro/core/objects/config.py +64 -94
  6. experimaestro/core/types.py +35 -57
  7. experimaestro/launcherfinder/registry.py +3 -3
  8. experimaestro/mkdocs/base.py +6 -8
  9. experimaestro/notifications.py +12 -3
  10. experimaestro/progress.py +406 -0
  11. experimaestro/settings.py +4 -2
  12. experimaestro/tests/launchers/common.py +2 -2
  13. experimaestro/tests/restart.py +1 -1
  14. experimaestro/tests/test_checkers.py +2 -2
  15. experimaestro/tests/test_dependencies.py +12 -12
  16. experimaestro/tests/test_experiment.py +3 -3
  17. experimaestro/tests/test_file_progress.py +425 -0
  18. experimaestro/tests/test_file_progress_integration.py +477 -0
  19. experimaestro/tests/test_generators.py +61 -0
  20. experimaestro/tests/test_identifier.py +90 -81
  21. experimaestro/tests/test_instance.py +9 -9
  22. experimaestro/tests/test_objects.py +9 -32
  23. experimaestro/tests/test_outputs.py +6 -6
  24. experimaestro/tests/test_param.py +14 -14
  25. experimaestro/tests/test_progress.py +4 -4
  26. experimaestro/tests/test_serializers.py +5 -5
  27. experimaestro/tests/test_tags.py +15 -15
  28. experimaestro/tests/test_tasks.py +40 -36
  29. experimaestro/tests/test_tokens.py +8 -6
  30. experimaestro/tests/test_types.py +10 -10
  31. experimaestro/tests/test_validation.py +19 -19
  32. experimaestro/tests/token_reschedule.py +1 -1
  33. {experimaestro-1.11.1.dist-info → experimaestro-2.0.0rc0.dist-info}/METADATA +1 -1
  34. {experimaestro-1.11.1.dist-info → experimaestro-2.0.0rc0.dist-info}/RECORD +37 -32
  35. {experimaestro-1.11.1.dist-info → experimaestro-2.0.0rc0.dist-info}/LICENSE +0 -0
  36. {experimaestro-1.11.1.dist-info → experimaestro-2.0.0rc0.dist-info}/WHEEL +0 -0
  37. {experimaestro-1.11.1.dist-info → experimaestro-2.0.0rc0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,425 @@
1
+ """Tests for the file-based progress tracking system"""
2
+
3
+ import json
4
+ import tempfile
5
+ from pathlib import Path
6
+ from unittest.mock import patch
7
+
8
+ import pytest
9
+
10
+ from experimaestro.progress import (
11
+ ProgressEntry,
12
+ ProgressFileWriter,
13
+ ProgressFileReader,
14
+ FileBasedProgressReporter,
15
+ )
16
+
17
+
18
+ class TestProgressEntry:
19
+ """Test ProgressEntry dataclass"""
20
+
21
+ def test_to_dict(self):
22
+ entry = ProgressEntry(
23
+ timestamp=1234567890.0, level=1, progress=0.5, desc="Test description"
24
+ )
25
+
26
+ expected = {
27
+ "timestamp": 1234567890.0,
28
+ "level": 1,
29
+ "progress": 0.5,
30
+ "desc": "Test description",
31
+ }
32
+
33
+ assert entry.to_dict() == expected
34
+
35
+ def test_from_dict(self):
36
+ data = {
37
+ "timestamp": 1234567890.0,
38
+ "level": 1,
39
+ "progress": 0.5,
40
+ "desc": "Test description",
41
+ }
42
+
43
+ entry = ProgressEntry.from_dict(data)
44
+
45
+ assert entry.timestamp == 1234567890.0
46
+ assert entry.level == 1
47
+ assert entry.progress == 0.5
48
+ assert entry.desc == "Test description"
49
+
50
+ def test_from_dict_minimal(self):
51
+ data = {"timestamp": 1234567890.0, "level": 0, "progress": 1.0}
52
+
53
+ entry = ProgressEntry.from_dict(data)
54
+
55
+ assert entry.timestamp == 1234567890.0
56
+ assert entry.level == 0
57
+ assert entry.progress == 1.0
58
+ assert entry.desc is None
59
+
60
+
61
+ class TestProgressFileWriter:
62
+ """Test ProgressFileWriter class"""
63
+
64
+ def test_init_creates_directory(self):
65
+ with tempfile.TemporaryDirectory() as tmpdir:
66
+ task_path = Path(tmpdir)
67
+ writer = ProgressFileWriter(task_path)
68
+
69
+ assert writer.progress_dir.exists()
70
+ assert writer.progress_dir == task_path / ".experimaestro"
71
+
72
+ def test_write_single_progress(self):
73
+ with tempfile.TemporaryDirectory() as tmpdir:
74
+ task_path = Path(tmpdir)
75
+ writer = ProgressFileWriter(task_path)
76
+
77
+ writer.write_progress(0, 0.5, "Test progress")
78
+
79
+ # Check file was created
80
+ progress_file = writer.progress_dir / "progress-0000.jsonl"
81
+ assert progress_file.exists()
82
+
83
+ # Check symlink was created
84
+ latest_link = writer.progress_dir / "progress-latest.jsonl"
85
+ assert latest_link.exists()
86
+ assert latest_link.is_symlink()
87
+ assert latest_link.resolve().name == progress_file.name
88
+
89
+ def test_write_multiple_progress(self):
90
+ with tempfile.TemporaryDirectory() as tmpdir:
91
+ task_path = Path(tmpdir)
92
+ writer = ProgressFileWriter(task_path)
93
+
94
+ # Write multiple progress entries
95
+ writer.write_progress(0, 0.1, "Step 1")
96
+ writer.write_progress(0, 0.5, "Step 2")
97
+ writer.write_progress(1, 0.3, "Substep")
98
+ writer.write_progress(0, 1.0, "Complete")
99
+
100
+ progress_file = writer.progress_dir / "progress-0000.jsonl"
101
+ assert progress_file.exists()
102
+
103
+ # Read and verify entries
104
+ lines = progress_file.read_text().strip().split("\n")
105
+ assert len(lines) == 4
106
+
107
+ # Check first entry
108
+ entry1 = json.loads(lines[0])
109
+ assert entry1["level"] == 0
110
+ assert entry1["progress"] == 0.1
111
+ assert entry1["desc"] == "Step 1"
112
+
113
+ def test_file_rotation(self):
114
+ with tempfile.TemporaryDirectory() as tmpdir:
115
+ task_path = Path(tmpdir)
116
+ # Set small max entries for testing rotation
117
+ writer = ProgressFileWriter(task_path, max_entries_per_file=2)
118
+
119
+ # Write 3 entries to trigger rotation
120
+ writer.write_progress(0, 0.1, "Entry 1")
121
+ writer.write_progress(0, 0.2, "Entry 2")
122
+ writer.write_progress(0, 0.3, "Entry 3") # Should trigger rotation
123
+
124
+ # Check both files exist
125
+ file1 = writer.progress_dir / "progress-0000.jsonl"
126
+ file2 = writer.progress_dir / "progress-0001.jsonl"
127
+
128
+ assert file1.exists()
129
+ assert file2.exists()
130
+
131
+ # Check file1 has 2 entries
132
+ lines1 = file1.read_text().strip().split("\n")
133
+ assert len(lines1) == 2
134
+
135
+ # Check file2 has 1 entry
136
+ lines2 = file2.read_text().strip().split("\n")
137
+ assert len(lines2) == 1
138
+
139
+ # Check symlink points to latest file
140
+ latest_link = writer.progress_dir / "progress-latest.jsonl"
141
+ assert latest_link.resolve().name == file2.name
142
+
143
+ def test_resume_from_existing_files(self):
144
+ with tempfile.TemporaryDirectory() as tmpdir:
145
+ task_path = Path(tmpdir)
146
+
147
+ # Create first writer and write some entries
148
+ writer1 = ProgressFileWriter(task_path, max_entries_per_file=2)
149
+ writer1.write_progress(0, 0.1, "Entry 1")
150
+ writer1.write_progress(0, 0.2, "Entry 2")
151
+
152
+ # Create second writer (simulating restart)
153
+ writer2 = ProgressFileWriter(task_path, max_entries_per_file=2)
154
+
155
+ # Should resume from existing state
156
+ assert writer2.current_file_index == 0
157
+ assert writer2.current_file_entries == 2
158
+
159
+ # Writing one more should trigger rotation
160
+ writer2.write_progress(0, 0.3, "Entry 3")
161
+
162
+ file2 = writer2.progress_dir / "progress-0001.jsonl"
163
+ assert file2.exists()
164
+
165
+
166
+ class TestProgressFileReader:
167
+ """Test ProgressFileReader class"""
168
+
169
+ def test_read_entries_from_file(self):
170
+ with tempfile.TemporaryDirectory() as tmpdir:
171
+ task_path = Path(tmpdir)
172
+
173
+ # Write some test data
174
+ writer = ProgressFileWriter(task_path)
175
+ writer.write_progress(0, 0.5, "Test")
176
+ writer.write_progress(1, 0.3, "Nested")
177
+
178
+ # Read it back
179
+ reader = ProgressFileReader(task_path)
180
+ progress_file = reader.get_progress_files()[0]
181
+ entries = list(reader.read_entries(progress_file))
182
+
183
+ assert len(entries) == 2
184
+ assert entries[0].level == 0
185
+ assert entries[0].progress == 0.5
186
+ assert entries[0].desc == "Test"
187
+ assert entries[1].level == 1
188
+ assert entries[1].progress == 0.3
189
+ assert entries[1].desc == "Nested"
190
+
191
+ def test_read_all_entries(self):
192
+ with tempfile.TemporaryDirectory() as tmpdir:
193
+ task_path = Path(tmpdir)
194
+
195
+ # Write entries across multiple files
196
+ writer = ProgressFileWriter(task_path, max_entries_per_file=2)
197
+ writer.write_progress(0, 0.1, "Entry 1")
198
+ writer.write_progress(0, 0.2, "Entry 2")
199
+ writer.write_progress(0, 0.3, "Entry 3") # Triggers rotation
200
+ writer.write_progress(0, 0.4, "Entry 4")
201
+
202
+ # Read all entries
203
+ reader = ProgressFileReader(task_path)
204
+ entries = list(reader.read_all_entries())
205
+
206
+ assert len(entries) == 4
207
+ assert entries[0].desc == "Entry 1"
208
+ assert entries[1].desc == "Entry 2"
209
+ assert entries[2].desc == "Entry 3"
210
+ assert entries[3].desc == "Entry 4"
211
+
212
+ def test_read_latest_entries(self):
213
+ with tempfile.TemporaryDirectory() as tmpdir:
214
+ task_path = Path(tmpdir)
215
+
216
+ # Write many entries
217
+ writer = ProgressFileWriter(task_path, max_entries_per_file=3)
218
+ for i in range(10):
219
+ writer.write_progress(0, i / 10.0, f"Entry {i}")
220
+
221
+ # Read latest 5 entries
222
+ reader = ProgressFileReader(task_path)
223
+ latest = reader.read_latest_entries(5)
224
+
225
+ assert len(latest) == 5
226
+ # Should be entries 5-9 in chronological order
227
+ assert latest[0].desc == "Entry 5"
228
+ assert latest[4].desc == "Entry 9"
229
+
230
+ def test_get_current_progress(self):
231
+ with tempfile.TemporaryDirectory() as tmpdir:
232
+ task_path = Path(tmpdir)
233
+
234
+ # Write progress for multiple levels
235
+ writer = ProgressFileWriter(task_path)
236
+ writer.write_progress(0, 0.1, "Level 0 start")
237
+ writer.write_progress(1, 0.5, "Level 1 progress")
238
+ writer.write_progress(0, 0.5, "Level 0 update")
239
+ writer.write_progress(1, 1.0, "Level 1 complete")
240
+ writer.write_progress(0, 1.0, "Level 0 complete")
241
+
242
+ # Get current progress
243
+ reader = ProgressFileReader(task_path)
244
+ current = reader.get_current_progress()
245
+
246
+ assert len(current) == 2
247
+ assert current[0].progress == 1.0
248
+ assert current[0].desc == "Level 0 complete"
249
+ assert current[1].progress == 1.0
250
+ assert current[1].desc == "Level 1 complete"
251
+
252
+ def test_get_latest_file_via_symlink(self):
253
+ with tempfile.TemporaryDirectory() as tmpdir:
254
+ task_path = Path(tmpdir)
255
+
256
+ # Write some entries
257
+ writer = ProgressFileWriter(task_path, max_entries_per_file=2)
258
+ writer.write_progress(0, 0.1, "Entry 1")
259
+ writer.write_progress(0, 0.2, "Entry 2")
260
+ writer.write_progress(0, 0.3, "Entry 3") # Triggers rotation
261
+
262
+ # Get latest file
263
+ reader = ProgressFileReader(task_path)
264
+ latest_file = reader.get_latest_file()
265
+
266
+ expected_file = task_path / ".experimaestro" / "progress-0001.jsonl"
267
+ assert latest_file.name == expected_file.name
268
+
269
+ def test_no_progress_files(self):
270
+ with tempfile.TemporaryDirectory() as tmpdir:
271
+ task_path = Path(tmpdir)
272
+
273
+ reader = ProgressFileReader(task_path)
274
+
275
+ assert reader.get_progress_files() == []
276
+ assert reader.get_latest_file() is None
277
+ assert list(reader.read_all_entries()) == []
278
+ assert reader.read_latest_entries(10) == []
279
+ assert reader.get_current_progress() == {}
280
+
281
+
282
+ class TestFileBasedProgressReporter:
283
+ """Test FileBasedProgressReporter class"""
284
+
285
+ def test_set_progress_writes_to_file(self):
286
+ with tempfile.TemporaryDirectory() as tmpdir:
287
+ task_path = Path(tmpdir)
288
+
289
+ reporter = FileBasedProgressReporter(task_path)
290
+ reporter.set_progress(0.5, 0, "Test progress")
291
+
292
+ # Verify file was written
293
+ progress_file = task_path / ".experimaestro" / "progress-0000.jsonl"
294
+ assert progress_file.exists()
295
+
296
+ # Read and verify content
297
+ reader = ProgressFileReader(task_path)
298
+ entries = list(reader.read_all_entries())
299
+
300
+ assert len(entries) == 1
301
+ assert entries[0].level == 0
302
+ assert entries[0].progress == 0.5
303
+ assert entries[0].desc == "Test progress"
304
+
305
+ def test_set_progress_threshold(self):
306
+ with tempfile.TemporaryDirectory() as tmpdir:
307
+ task_path = Path(tmpdir)
308
+
309
+ reporter = FileBasedProgressReporter(task_path)
310
+
311
+ # First progress
312
+ reporter.set_progress(0.5, 0, "Test")
313
+
314
+ # Small change (should not write)
315
+ reporter.set_progress(0.505, 0, "Test")
316
+
317
+ # Larger change (should write)
318
+ reporter.set_progress(0.6, 0, "Test")
319
+
320
+ # Read entries
321
+ reader = ProgressFileReader(task_path)
322
+ entries = list(reader.read_all_entries())
323
+
324
+ # Should only have 2 entries (first and third)
325
+ assert len(entries) == 2
326
+ assert entries[0].progress == 0.5
327
+ assert entries[1].progress == 0.6
328
+
329
+ def test_set_progress_description_change(self):
330
+ with tempfile.TemporaryDirectory() as tmpdir:
331
+ task_path = Path(tmpdir)
332
+
333
+ reporter = FileBasedProgressReporter(task_path)
334
+
335
+ # Same progress, different description
336
+ reporter.set_progress(0.5, 0, "Description 1")
337
+ reporter.set_progress(0.5, 0, "Description 2")
338
+
339
+ # Read entries
340
+ reader = ProgressFileReader(task_path)
341
+ entries = list(reader.read_all_entries())
342
+
343
+ # Should have both entries due to description change
344
+ assert len(entries) == 2
345
+ assert entries[0].desc == "Description 1"
346
+ assert entries[1].desc == "Description 2"
347
+
348
+ def test_eoj_writes_marker(self):
349
+ with tempfile.TemporaryDirectory() as tmpdir:
350
+ task_path = Path(tmpdir)
351
+
352
+ reporter = FileBasedProgressReporter(task_path)
353
+ reporter.set_progress(1.0, 0, "Complete")
354
+ reporter.eoj()
355
+
356
+ # Read entries
357
+ reader = ProgressFileReader(task_path)
358
+ entries = list(reader.read_all_entries())
359
+
360
+ assert len(entries) == 2
361
+ assert entries[0].level == 0
362
+ assert entries[0].progress == 1.0
363
+ assert entries[1].level == -1 # EOJ marker
364
+ assert entries[1].progress == 1.0
365
+ assert entries[1].desc == "EOJ"
366
+
367
+ def test_multiple_levels(self):
368
+ with tempfile.TemporaryDirectory() as tmpdir:
369
+ task_path = Path(tmpdir)
370
+
371
+ reporter = FileBasedProgressReporter(task_path)
372
+
373
+ # Progress at different levels
374
+ reporter.set_progress(0.1, 0, "Main task")
375
+ reporter.set_progress(0.5, 1, "Subtask")
376
+ reporter.set_progress(0.3, 2, "Sub-subtask")
377
+ reporter.set_progress(0.5, 0, "Main task update")
378
+
379
+ # Read current progress
380
+ reader = ProgressFileReader(task_path)
381
+ current = reader.get_current_progress()
382
+
383
+ assert len(current) == 3
384
+ assert current[0].progress == 0.5
385
+ assert current[0].desc == "Main task update"
386
+ assert current[1].progress == 0.5
387
+ assert current[1].desc == "Subtask"
388
+ assert current[2].progress == 0.3
389
+ assert current[2].desc == "Sub-subtask"
390
+
391
+
392
+ class TestIntegrationWithNotifications:
393
+ """Test integration with existing notification system"""
394
+
395
+ @patch("experimaestro.taskglobals.Env.instance")
396
+ def test_progress_function_writes_to_file(self, mock_env):
397
+ """Test that the progress() function writes to file system"""
398
+ with tempfile.TemporaryDirectory() as tmpdir:
399
+ task_path = Path(tmpdir)
400
+
401
+ # Mock the task environment
402
+ mock_env.return_value.taskpath = task_path
403
+ mock_env.return_value.slave = False
404
+
405
+ # Import and call progress function
406
+ from experimaestro.notifications import progress
407
+
408
+ progress(0.5, level=0, desc="Test progress")
409
+
410
+ # Verify file was written
411
+ progress_file = task_path / ".experimaestro" / "progress-0000.jsonl"
412
+ assert progress_file.exists()
413
+
414
+ # Read and verify
415
+ reader = ProgressFileReader(task_path)
416
+ entries = list(reader.read_all_entries())
417
+
418
+ assert len(entries) == 1
419
+ assert entries[0].level == 0
420
+ assert entries[0].progress == 0.5
421
+ assert entries[0].desc == "Test progress"
422
+
423
+
424
+ if __name__ == "__main__":
425
+ pytest.main([__file__])