-
Notifications
You must be signed in to change notification settings - Fork 3
[core] Improve, test and document the DSL #35
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
ad362f9
[foreign_testing] orchestration: make sure the project_path exists
kpouget c7f6a46
[foreign_testing] orchestration: add dynamic configuration of the pro…
kpouget 122c2bf
specs: 008-toolbox-dsl: new spec
kpouget 53ea77f
[core] Improve, test and document the DSL
kpouget 22798ca
[core] Never use the logging package directly
kpouget 653fc49
[fournos_launcher] Never use the logging package directly
kpouget 860ca9b
[cluster] toolbox: rebuild_image: don't use the logging package directly
kpouget 23ee00c
[skeleton] orchestration: prepare_skeleton: don't use the logging pac…
kpouget 7b9ba1a
[core] dsl: runtime: don't swallow the (multiple) always-tasks except…
kpouget 5353d91
[fournos_launcher] orchestration: submit: strip the display name
kpouget File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| # Unit tests for projects/core/dsl (task decorators, execute_tasks, failure/always/skip). | ||
| name: Toolbox DSL tests | ||
|
|
||
| on: | ||
| pull_request: | ||
| branches: [main] | ||
| push: | ||
| branches: [main] | ||
| workflow_dispatch: | ||
|
|
||
| concurrency: | ||
| group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} | ||
| cancel-in-progress: true | ||
|
|
||
| jobs: | ||
| pytest: | ||
| runs-on: ubuntu-latest | ||
| env: | ||
| PYTHONPATH: ${{ github.workspace }} | ||
|
|
||
| steps: | ||
| - uses: actions/checkout@v4 | ||
|
|
||
| - uses: actions/setup-python@v5 | ||
| with: | ||
| python-version: "3.12" | ||
|
|
||
| - name: Install test dependencies | ||
| run: | | ||
| set -o errexit | ||
| python -m pip install --upgrade pip | ||
| python -m pip install pytest pyyaml jinja2 | ||
|
|
||
| - name: Run projects/core/tests | ||
| run: | | ||
| set -o errexit | ||
| # Tree + docstrings (what is being tested), then execute with one line per test + result. | ||
| python -m pytest --collect-only -vv -o addopts='-ra --strict-markers --strict-config' | ||
| echo "" | ||
| python -m pytest -vv -o addopts='-ra --strict-markers --strict-config' |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,136 @@ | ||
| # Toolbox DSL | ||
|
|
||
| The FORGE toolbox uses a small Python **domain-specific language** (DSL) so each command is a standalone script: a **public entrypoint** (plain function with typed parameters), a **linear list of tasks** (`@task` functions), and **verbose logging** under an artifact directory for post-mortem review. | ||
|
|
||
| ## Command entrypoint and CLI | ||
|
|
||
| The public API for orchestration (or humans) is a single function, for example `run(...)`, documented with an `Args:` section in its docstring. | ||
|
|
||
| - **From Python**: call that function; it typically ends with `return execute_tasks(locals())` so every `@task` defined in the same module runs in registration order. | ||
| - **From the shell**: use `projects.core.dsl.toolbox.create_toolbox_main(run)` so `argparse` options are generated from the function signature (`toolbox.run_toolbox_command`). | ||
|
|
||
| Tasks are **not** the entrypoint; they are internal steps registered at import time. | ||
|
|
||
| ## Tasks (`@task`) | ||
|
|
||
| Each task is a function `(args, ctx) -> value` (or return `None`). | ||
|
|
||
| - **`args`**: read-only view of the command parameters plus `artifact_dir` (see `projects.core.dsl.context.ReadOnlyArgs`). | ||
| - **`ctx`**: mutable per-task context that is merged back into a shared namespace after each task so later steps can read values previous tasks set on `ctx`. | ||
|
|
||
| Registration order in the file is execution order (see `ScriptManager`). | ||
|
|
||
| ```python | ||
| @task | ||
| def ensure_project(args, ctx): | ||
| """Describe the step; shown in logs.""" | ||
| ctx.project_ready = True | ||
| return "ok" | ||
| ``` | ||
|
|
||
| ## Conditional tasks (`@when`) | ||
|
|
||
| `@when` takes a **zero-argument** callable (usually `lambda:` …) evaluated when the task is reached. If it returns a falsy value, the task is skipped (logged as skipped). | ||
|
|
||
| **Decorator order:** write `@when(...)` on the line **above** `@task` (same as `@retry`). Python applies the bottom decorator first, so `@task` registers the step, then `@when` attaches the condition and updates the script registry entry so `execute_tasks` sees it. | ||
|
|
||
| ```python | ||
| @when(lambda: some_other_task.status.return_value is True) | ||
| @task | ||
| def follow_up(args, ctx): | ||
| ... | ||
| ``` | ||
|
|
||
| Because the condition is called with **no arguments**, anything dynamic must come from a **closure**, module-level state, or another task’s `.status.return_value` (see `TaskResult` in `script_manager.py`). | ||
|
|
||
| ## Retries (`@retry`) | ||
|
|
||
| **Order:** `@retry(...)` above `@task` above `def` (same pattern as waiting for OpenShift resources). | ||
|
|
||
| By default, retries apply when the task **returns a falsy** value (`False`, `None`, `[]`, …). Each attempt runs the full `@task` wrapper (logging, result capture). Delays use `time.sleep`. | ||
|
|
||
| Parameters: | ||
|
|
||
| | Parameter | Meaning | | ||
| |-----------|---------| | ||
| | `attempts` | Maximum attempts | | ||
| | `delay` | Initial sleep in seconds before the next attempt | | ||
| | `backoff` | Multiplier for the delay after each retry | | ||
| | `retry_on_exceptions` | If `True`, **also** retry when the task raises (never retries on `KeyboardInterrupt`) | | ||
|
|
||
| ```python | ||
| @retry(attempts=60, delay=30, backoff=1.0) | ||
| @task | ||
| def wait_until_ready(args, ctx): | ||
| ... | ||
| return False # try again after delay | ||
| ``` | ||
|
|
||
| ```python | ||
| @retry(attempts=5, delay=2, backoff=1.5, retry_on_exceptions=True) | ||
| @task | ||
| def call_flaky_api(args, ctx): | ||
| ... | ||
| ``` | ||
|
|
||
| If all attempts fail, `RetryFailure` is raised (wrapped in `TaskExecutionError` during `execute_tasks`, with the underlying `RetryFailure` available as `TaskExecutionError.__cause__` when that applies). | ||
|
|
||
| ## Always tasks (`@always`) | ||
|
|
||
| Mark cleanup or artifact steps that must run **after a failure** in the main sequence. `@always` may appear **above or below** `@task` on the same function (see `always()` in `task.py`). | ||
|
|
||
| If a normal task raises, remaining non-`@always` tasks are **skipped** (each pending non-always task is logged as skipped; its body is not run). Pending `@always` tasks still run in file order. The original error is re-raised after always-tasks finish (unless an always-task fails and becomes the primary error when there was none). | ||
|
|
||
| Place `@always` tasks **after** the main pipeline so they behave as teardown (see toolbox scripts under `projects/*/toolbox/`). | ||
|
|
||
| ## Execution driver (`execute_tasks`) | ||
|
|
||
| `execute_tasks(locals())` (or a filtered dict of parameters): | ||
|
|
||
| - Opens a nested artifact directory (`env.NextArtifactDir`). | ||
| - Writes metadata (`_meta/metadata.yaml`, `_meta/restart.sh`) and `task.log`. | ||
| - Runs tasks from the **calling file** only (`ScriptManager` path must match `Path(__file__).relative_to(FORGE_HOME)` vs `os.path.relpath` at task definition — run commands from the repository root as the toolbox does). | ||
|
|
||
| Interrupts (`KeyboardInterrupt`, `SignalError`) stop execution and still emit completion banners where implemented (not covered by `test_dsl_toolbox.py`; see `runtime.py`). | ||
|
|
||
| ### Trace and artifacts (post-mortem) | ||
|
|
||
| Each run is intended to be reviewable without re-executing the command: | ||
|
|
||
| | Output | Role | | ||
| |--------|------| | ||
| | `task.log` | Full DSL log stream for the run | | ||
| | `_meta/metadata.yaml` | Timestamp, command file, artifact dir, serialized arguments | | ||
| | `_meta/restart.sh` | Replay helper with the same CLI-style flags | | ||
| | Console / DSL logger | Step headers, skip lines (`==> SKIPPING TASK: …` when pending steps are skipped after a failure), retry banners | | ||
|
|
||
| Keep secrets out of entrypoint parameters where possible so they appear safely in metadata (follow project norms for redaction if you add any). | ||
|
|
||
| ### Standalone parameters (entrypoint contract) | ||
|
|
||
| Declare orchestration and operator inputs on the **public entrypoint** (typed parameters and an `Args:` section in the docstring). Prefer those parameters (and values derived in tasks) over ad hoc reads of undeclared environment variables, so the command stays **standalone** and reviewable—except where FORGE already documents global conventions (for example `FORGE_HOME`, artifact layout). | ||
|
|
||
| ## Related modules | ||
|
|
||
| - `projects.core.dsl.task` — `@task`, `@when`, `@retry`, `@always`, `RetryFailure` | ||
| - `projects.core.dsl.runtime` — `execute_tasks`, `TaskExecutionError` | ||
| - `projects.core.dsl.toolbox` — CLI wrapper | ||
| - `projects.core.dsl.shell`, `template`, … — helpers used inside tasks | ||
|
|
||
| ## Tests | ||
|
|
||
| `projects/core/tests/test_dsl_toolbox.py` exercises the behaviors below. Run: `python -m pytest projects/core/tests/test_dsl_toolbox.py -v`. | ||
|
|
||
| | Area | What is asserted | | ||
| |------|------------------| | ||
| | Task order | `first` / `second` run in **source definition order** when all succeed. | | ||
| | Failure → skip | After a task raises, **later non-`@always`** tasks do not run: **`task.log`** has `SKIPPING TASK: …` and the “not @always” line; unique return markers for pending steps **do not** appear in the log. | | ||
| | Failure → `@always` | After a task raises, a **later** `@always` task **still runs** (event order); **`task.log`** contains that task’s return value; the failure re-raised is **`TaskExecutionError`** with the original **`RuntimeError`** as **`__cause__`**. | | ||
| | `@when` | If the predicate is falsy, the task body does not run. | | ||
| | `@retry` (falsy → success) | Falsy return values are retried until a truthy result. | | ||
| | `@retry` (falsy exhausted) | If every attempt returns falsy, **`RetryFailure`** is raised and wrapped so **`TaskExecutionError.__cause__`** is **`RetryFailure`**. | | ||
| | `@retry` (exceptions) | With `retry_on_exceptions=True`, exceptions are retried until success; if every attempt raises, **`TaskExecutionError.__cause__`** is **`RetryFailure`**. | | ||
| | Decorator stack | `@retry` / `@when` **without** `@task` raise **`TypeError` at definition time** with the “Put `@task` BELOW …” message. | | ||
| | Success return | `execute_tasks` returns **`shared_context`** with task attributes and **`artifact_dir`** set. | | ||
|
|
||
| Not in that file: interrupt handling (`KeyboardInterrupt` / `SignalError`), and CLI wiring (`create_toolbox_main` / `run_toolbox_command`)—those are documented above but not exercised by these unit tests. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.