cook-build 0.6.3__tar.gz → 0.6.4__tar.gz
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.
- {cook_build-0.6.3/src/cook_build.egg-info → cook_build-0.6.4}/PKG-INFO +1 -1
- {cook_build-0.6.3 → cook_build-0.6.4}/pyproject.toml +1 -1
- {cook_build-0.6.3 → cook_build-0.6.4}/src/cook/__main__.py +9 -1
- {cook_build-0.6.3 → cook_build-0.6.4}/src/cook/controller.py +77 -43
- {cook_build-0.6.3 → cook_build-0.6.4/src/cook_build.egg-info}/PKG-INFO +1 -1
- {cook_build-0.6.3 → cook_build-0.6.4}/tests/test_main.py +63 -0
- {cook_build-0.6.3 → cook_build-0.6.4}/LICENSE +0 -0
- {cook_build-0.6.3 → cook_build-0.6.4}/README.md +0 -0
- {cook_build-0.6.3 → cook_build-0.6.4}/README.rst +0 -0
- {cook_build-0.6.3 → cook_build-0.6.4}/setup.cfg +0 -0
- {cook_build-0.6.3 → cook_build-0.6.4}/src/cook/__init__.py +0 -0
- {cook_build-0.6.3 → cook_build-0.6.4}/src/cook/actions.py +0 -0
- {cook_build-0.6.3 → cook_build-0.6.4}/src/cook/contexts.py +0 -0
- {cook_build-0.6.3 → cook_build-0.6.4}/src/cook/manager.py +0 -0
- {cook_build-0.6.3 → cook_build-0.6.4}/src/cook/task.py +0 -0
- {cook_build-0.6.3 → cook_build-0.6.4}/src/cook/util.py +0 -0
- {cook_build-0.6.3 → cook_build-0.6.4}/src/cook_build.egg-info/SOURCES.txt +0 -0
- {cook_build-0.6.3 → cook_build-0.6.4}/src/cook_build.egg-info/dependency_links.txt +0 -0
- {cook_build-0.6.3 → cook_build-0.6.4}/src/cook_build.egg-info/entry_points.txt +0 -0
- {cook_build-0.6.3 → cook_build-0.6.4}/src/cook_build.egg-info/requires.txt +0 -0
- {cook_build-0.6.3 → cook_build-0.6.4}/src/cook_build.egg-info/top_level.txt +0 -0
- {cook_build-0.6.3 → cook_build-0.6.4}/tests/test_actions.py +0 -0
- {cook_build-0.6.3 → cook_build-0.6.4}/tests/test_contexts.py +0 -0
- {cook_build-0.6.3 → cook_build-0.6.4}/tests/test_controller.py +0 -0
- {cook_build-0.6.3 → cook_build-0.6.4}/tests/test_examples.py +0 -0
- {cook_build-0.6.3 → cook_build-0.6.4}/tests/test_manager.py +0 -0
- {cook_build-0.6.3 → cook_build-0.6.4}/tests/test_util.py +0 -0
|
@@ -122,6 +122,7 @@ class Command:
|
|
|
122
122
|
|
|
123
123
|
class ExecArgs(Args):
|
|
124
124
|
jobs: int
|
|
125
|
+
dry_run: bool
|
|
125
126
|
|
|
126
127
|
|
|
127
128
|
class ExecCommand(Command):
|
|
@@ -135,11 +136,18 @@ class ExecCommand(Command):
|
|
|
135
136
|
parser.add_argument(
|
|
136
137
|
"--jobs", "-j", help="number of concurrent jobs", type=int, default=1
|
|
137
138
|
)
|
|
139
|
+
parser.add_argument(
|
|
140
|
+
"--dry-run",
|
|
141
|
+
"-n",
|
|
142
|
+
help="show what \033[1mwould\033[0m be executed without running tasks",
|
|
143
|
+
action="store_true",
|
|
144
|
+
dest="dry_run",
|
|
145
|
+
)
|
|
138
146
|
super().configure_parser(parser)
|
|
139
147
|
|
|
140
148
|
def execute(self, controller: Controller, args: ExecArgs) -> None: # pyright: ignore[reportIncompatibleMethodOverride]
|
|
141
149
|
tasks = self.discover_tasks(controller, args)
|
|
142
|
-
controller.execute(tasks, num_concurrent=args.jobs)
|
|
150
|
+
controller.execute(tasks, num_concurrent=args.jobs, dry_run=args.dry_run)
|
|
143
151
|
|
|
144
152
|
|
|
145
153
|
class LsArgs(Args):
|
|
@@ -233,7 +233,11 @@ class Controller:
|
|
|
233
233
|
return False
|
|
234
234
|
|
|
235
235
|
def execute(
|
|
236
|
-
self,
|
|
236
|
+
self,
|
|
237
|
+
tasks: "Task | list[Task]",
|
|
238
|
+
num_concurrent: int = 1,
|
|
239
|
+
interval: float = 1,
|
|
240
|
+
dry_run: bool = False,
|
|
237
241
|
) -> None:
|
|
238
242
|
"""
|
|
239
243
|
Execute one or more tasks.
|
|
@@ -241,6 +245,8 @@ class Controller:
|
|
|
241
245
|
Args:
|
|
242
246
|
tasks: Tasks to execute.
|
|
243
247
|
num_concurrent: Number of concurrent threads to run.
|
|
248
|
+
interval: Interval for checking stop events.
|
|
249
|
+
dry_run: If True, show what would execute without running tasks.
|
|
244
250
|
"""
|
|
245
251
|
if not isinstance(tasks, Sequence):
|
|
246
252
|
tasks = [tasks]
|
|
@@ -256,7 +262,7 @@ class Controller:
|
|
|
256
262
|
thread = threading.Thread(
|
|
257
263
|
target=self._target,
|
|
258
264
|
name=f"cook-thread-{i}",
|
|
259
|
-
args=(stop, input_queue, output_queue),
|
|
265
|
+
args=(stop, input_queue, output_queue, dry_run),
|
|
260
266
|
daemon=True,
|
|
261
267
|
)
|
|
262
268
|
thread.start()
|
|
@@ -288,30 +294,35 @@ class Controller:
|
|
|
288
294
|
# Unpack the results.
|
|
289
295
|
if event.kind == "fail":
|
|
290
296
|
# Update the status in the database.
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
+
if not dry_run:
|
|
298
|
+
params = {
|
|
299
|
+
"name": event.task.name,
|
|
300
|
+
"last_failed": event.timestamp,
|
|
301
|
+
}
|
|
302
|
+
self.connection.execute(QUERIES["upsert_task_failed"], params)
|
|
303
|
+
self.connection.commit()
|
|
297
304
|
ex = event.exc_info[1]
|
|
298
305
|
raise util.FailedTaskError(ex, task=event.task) from ex
|
|
299
306
|
elif event.kind == "complete":
|
|
300
307
|
# Update the status in the database.
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
+
if not dry_run:
|
|
309
|
+
params = {
|
|
310
|
+
"name": event.task.name,
|
|
311
|
+
"digest": event.digest,
|
|
312
|
+
"last_completed": event.timestamp,
|
|
313
|
+
}
|
|
314
|
+
self.connection.execute(
|
|
315
|
+
QUERIES["upsert_task_completed"], params
|
|
316
|
+
)
|
|
317
|
+
self.connection.commit()
|
|
308
318
|
elif event.kind == "start":
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
319
|
+
if not dry_run:
|
|
320
|
+
params = {
|
|
321
|
+
"name": event.task.name,
|
|
322
|
+
"last_started": event.timestamp,
|
|
323
|
+
}
|
|
324
|
+
self.connection.execute(QUERIES["upsert_task_started"], params)
|
|
325
|
+
self.connection.commit()
|
|
315
326
|
continue
|
|
316
327
|
else:
|
|
317
328
|
raise ValueError(event) # pragma: no cover
|
|
@@ -345,7 +356,11 @@ class Controller:
|
|
|
345
356
|
raise RuntimeError(f"thread {thread} failed to join")
|
|
346
357
|
|
|
347
358
|
def _target(
|
|
348
|
-
self,
|
|
359
|
+
self,
|
|
360
|
+
stop: util.StopEvent,
|
|
361
|
+
input_queue: Queue,
|
|
362
|
+
output_queue: Queue,
|
|
363
|
+
dry_run: bool = False,
|
|
349
364
|
) -> None:
|
|
350
365
|
LOGGER.debug(f"started thread `{threading.current_thread().name}`")
|
|
351
366
|
while not stop.is_set():
|
|
@@ -365,12 +380,28 @@ class Controller:
|
|
|
365
380
|
|
|
366
381
|
start = datetime.now()
|
|
367
382
|
try:
|
|
368
|
-
# Execute the task.
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
383
|
+
# Execute or simulate the task.
|
|
384
|
+
if dry_run:
|
|
385
|
+
LOGGER.log(
|
|
386
|
+
logging.DEBUG if task.name.startswith("_") else logging.INFO,
|
|
387
|
+
"would execute %s",
|
|
388
|
+
task,
|
|
389
|
+
)
|
|
390
|
+
if task.action:
|
|
391
|
+
LOGGER.log(
|
|
392
|
+
logging.DEBUG
|
|
393
|
+
if task.name.startswith("_")
|
|
394
|
+
else logging.INFO,
|
|
395
|
+
" action: %s",
|
|
396
|
+
task.action,
|
|
397
|
+
)
|
|
398
|
+
else:
|
|
399
|
+
LOGGER.log(
|
|
400
|
+
logging.DEBUG if task.name.startswith("_") else logging.INFO,
|
|
401
|
+
"executing %s ...",
|
|
402
|
+
task,
|
|
403
|
+
)
|
|
404
|
+
|
|
374
405
|
output_queue.put(
|
|
375
406
|
Event(
|
|
376
407
|
kind="start",
|
|
@@ -380,15 +411,17 @@ class Controller:
|
|
|
380
411
|
exc_info=(None, None, None),
|
|
381
412
|
)
|
|
382
413
|
)
|
|
383
|
-
task.execute(stop)
|
|
384
414
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
)
|
|
391
|
-
|
|
415
|
+
if not dry_run:
|
|
416
|
+
task.execute(stop)
|
|
417
|
+
|
|
418
|
+
# Check that all targets were created.
|
|
419
|
+
for target in task.targets:
|
|
420
|
+
if not target.is_file():
|
|
421
|
+
raise FileNotFoundError(
|
|
422
|
+
f"task {task} did not create target {target}"
|
|
423
|
+
)
|
|
424
|
+
LOGGER.debug("%s created `%s`", task, target)
|
|
392
425
|
|
|
393
426
|
# Add the result to the output queue and report success.
|
|
394
427
|
output_queue.put(
|
|
@@ -400,13 +433,14 @@ class Controller:
|
|
|
400
433
|
exc_info=(None, None, None),
|
|
401
434
|
)
|
|
402
435
|
)
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
436
|
+
if not dry_run:
|
|
437
|
+
delta = util.format_timedelta(datetime.now() - start)
|
|
438
|
+
LOGGER.log(
|
|
439
|
+
logging.DEBUG if task.name.startswith("_") else logging.INFO,
|
|
440
|
+
"completed %s in %s",
|
|
441
|
+
task,
|
|
442
|
+
delta,
|
|
443
|
+
)
|
|
410
444
|
except: # noqa: E722
|
|
411
445
|
exc_info = sys.exc_info()
|
|
412
446
|
delta = util.format_timedelta(datetime.now() - start)
|
|
@@ -127,3 +127,66 @@ def test_terrible_recipe(caplog: pytest.LogCaptureFixture) -> None:
|
|
|
127
127
|
with pytest.raises(SystemExit), caplog.at_level("CRITICAL"):
|
|
128
128
|
__main__(["--recipe", str(RECIPES / "terrible.not-py"), "ls"])
|
|
129
129
|
assert "failed to load recipe" in caplog.text
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def test_dry_run_shows_tasks(tmp_wd: Path, caplog: pytest.LogCaptureFixture) -> None:
|
|
133
|
+
"""Dry-run should show what would execute without running tasks."""
|
|
134
|
+
with caplog.at_level(logging.INFO):
|
|
135
|
+
__main__(["--recipe", str(RECIPES / "blah.py"), "exec", "--dry-run", "run"])
|
|
136
|
+
|
|
137
|
+
# Should show "would execute" messages
|
|
138
|
+
assert "would execute <task `create_source`" in caplog.text
|
|
139
|
+
assert "would execute <task `compile`" in caplog.text
|
|
140
|
+
assert "would execute <task `link`" in caplog.text
|
|
141
|
+
assert "would execute <task `run`" in caplog.text
|
|
142
|
+
|
|
143
|
+
# Should show actions
|
|
144
|
+
assert "action:" in caplog.text
|
|
145
|
+
|
|
146
|
+
# Should NOT show "executing" or "completed" messages
|
|
147
|
+
assert "executing <task" not in caplog.text
|
|
148
|
+
assert "completed <task" not in caplog.text
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def test_dry_run_does_not_create_files(tmp_wd: Path) -> None:
|
|
152
|
+
"""Dry-run should not create target files."""
|
|
153
|
+
__main__(["--recipe", str(RECIPES / "blah.py"), "exec", "--dry-run", "link"])
|
|
154
|
+
|
|
155
|
+
# Files should NOT be created
|
|
156
|
+
assert not (tmp_wd / "blah.c").exists()
|
|
157
|
+
assert not (tmp_wd / "blah.o").exists()
|
|
158
|
+
assert not (tmp_wd / "blah").exists()
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def test_dry_run_does_not_update_database(tmp_wd: Path) -> None:
|
|
162
|
+
"""Dry-run should not update the database."""
|
|
163
|
+
# First, run normally to create database entry
|
|
164
|
+
__main__(["--recipe", str(RECIPES / "blah.py"), "exec", "link"])
|
|
165
|
+
|
|
166
|
+
# Check the database was updated
|
|
167
|
+
__main__(["--recipe", str(RECIPES / "blah.py"), "info", "link"])
|
|
168
|
+
|
|
169
|
+
# Reset the task
|
|
170
|
+
__main__(["--recipe", str(RECIPES / "blah.py"), "reset", "link"])
|
|
171
|
+
|
|
172
|
+
# Now dry-run should not update last_completed
|
|
173
|
+
__main__(["--recipe", str(RECIPES / "blah.py"), "exec", "--dry-run", "link"])
|
|
174
|
+
|
|
175
|
+
# Task should still be stale (database not updated)
|
|
176
|
+
__main__(["--recipe", str(RECIPES / "blah.py"), "info", "--stale", "link"])
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def test_dry_run_with_no_stale_tasks(
|
|
180
|
+
tmp_wd: Path, caplog: pytest.LogCaptureFixture
|
|
181
|
+
) -> None:
|
|
182
|
+
"""Dry-run with no stale tasks should be silent."""
|
|
183
|
+
# First, run normally
|
|
184
|
+
__main__(["--recipe", str(RECIPES / "blah.py"), "exec", "link"])
|
|
185
|
+
|
|
186
|
+
# Now dry-run with nothing stale
|
|
187
|
+
caplog.clear()
|
|
188
|
+
with caplog.at_level(logging.INFO):
|
|
189
|
+
__main__(["--recipe", str(RECIPES / "blah.py"), "exec", "--dry-run", "link"])
|
|
190
|
+
|
|
191
|
+
# Should be silent (no "would execute" messages)
|
|
192
|
+
assert "would execute" not in caplog.text
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|