hector-cli 0.5.0__tar.gz → 0.7.0__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.
- {hector_cli-0.5.0 → hector_cli-0.7.0}/PKG-INFO +35 -4
- {hector_cli-0.5.0 → hector_cli-0.7.0}/README.md +34 -3
- {hector_cli-0.5.0 → hector_cli-0.7.0}/hector/commands/base.py +8 -2
- {hector_cli-0.5.0 → hector_cli-0.7.0}/hector/core.py +50 -12
- {hector_cli-0.5.0 → hector_cli-0.7.0}/hector/docker.py +24 -15
- {hector_cli-0.5.0 → hector_cli-0.7.0}/hector/generator.py +9 -7
- {hector_cli-0.5.0 → hector_cli-0.7.0}/hector/mappings.py +2 -2
- {hector_cli-0.5.0 → hector_cli-0.7.0}/hector/modules.py +3 -3
- {hector_cli-0.5.0 → hector_cli-0.7.0}/hector/pipeline.py +48 -4
- {hector_cli-0.5.0 → hector_cli-0.7.0}/hector/runners.py +3 -3
- {hector_cli-0.5.0 → hector_cli-0.7.0}/hector/validator.py +8 -1
- {hector_cli-0.5.0 → hector_cli-0.7.0}/hector_cli.egg-info/PKG-INFO +35 -4
- {hector_cli-0.5.0 → hector_cli-0.7.0}/tests/test_core_paths.py +27 -0
- {hector_cli-0.5.0 → hector_cli-0.7.0}/tests/test_docker_argv.py +59 -6
- {hector_cli-0.5.0 → hector_cli-0.7.0}/tests/test_generator.py +17 -1
- {hector_cli-0.5.0 → hector_cli-0.7.0}/tests/test_pipeline_helpers.py +84 -0
- {hector_cli-0.5.0 → hector_cli-0.7.0}/tests/test_validator.py +16 -0
- {hector_cli-0.5.0 → hector_cli-0.7.0}/LICENSE +0 -0
- {hector_cli-0.5.0 → hector_cli-0.7.0}/hector/__init__.py +0 -0
- {hector_cli-0.5.0 → hector_cli-0.7.0}/hector/__main__.py +0 -0
- {hector_cli-0.5.0 → hector_cli-0.7.0}/hector/commands/__init__.py +0 -0
- {hector_cli-0.5.0 → hector_cli-0.7.0}/hector/commands/clean.py +0 -0
- {hector_cli-0.5.0 → hector_cli-0.7.0}/hector/commands/export.py +0 -0
- {hector_cli-0.5.0 → hector_cli-0.7.0}/hector/commands/init.py +0 -0
- {hector_cli-0.5.0 → hector_cli-0.7.0}/hector/commands/run.py +0 -0
- {hector_cli-0.5.0 → hector_cli-0.7.0}/hector/commands/test.py +0 -0
- {hector_cli-0.5.0 → hector_cli-0.7.0}/hector/commands/validate.py +0 -0
- {hector_cli-0.5.0 → hector_cli-0.7.0}/hector/connections.py +0 -0
- {hector_cli-0.5.0 → hector_cli-0.7.0}/hector/dependencies.py +0 -0
- {hector_cli-0.5.0 → hector_cli-0.7.0}/hector/hubs.py +0 -0
- {hector_cli-0.5.0 → hector_cli-0.7.0}/hector/peripherals.py +0 -0
- {hector_cli-0.5.0 → hector_cli-0.7.0}/hector/reporters.py +0 -0
- {hector_cli-0.5.0 → hector_cli-0.7.0}/hector/scaffold.py +0 -0
- {hector_cli-0.5.0 → hector_cli-0.7.0}/hector_cli.egg-info/SOURCES.txt +0 -0
- {hector_cli-0.5.0 → hector_cli-0.7.0}/hector_cli.egg-info/dependency_links.txt +0 -0
- {hector_cli-0.5.0 → hector_cli-0.7.0}/hector_cli.egg-info/entry_points.txt +0 -0
- {hector_cli-0.5.0 → hector_cli-0.7.0}/hector_cli.egg-info/requires.txt +0 -0
- {hector_cli-0.5.0 → hector_cli-0.7.0}/hector_cli.egg-info/top_level.txt +0 -0
- {hector_cli-0.5.0 → hector_cli-0.7.0}/setup.cfg +0 -0
- {hector_cli-0.5.0 → hector_cli-0.7.0}/setup.py +0 -0
- {hector_cli-0.5.0 → hector_cli-0.7.0}/tests/test_arguments.py +0 -0
- {hector_cli-0.5.0 → hector_cli-0.7.0}/tests/test_clean.py +0 -0
- {hector_cli-0.5.0 → hector_cli-0.7.0}/tests/test_cli_smoke.py +0 -0
- {hector_cli-0.5.0 → hector_cli-0.7.0}/tests/test_connections.py +0 -0
- {hector_cli-0.5.0 → hector_cli-0.7.0}/tests/test_core_registry.py +0 -0
- {hector_cli-0.5.0 → hector_cli-0.7.0}/tests/test_core_text.py +0 -0
- {hector_cli-0.5.0 → hector_cli-0.7.0}/tests/test_hubs.py +0 -0
- {hector_cli-0.5.0 → hector_cli-0.7.0}/tests/test_mappings.py +0 -0
- {hector_cli-0.5.0 → hector_cli-0.7.0}/tests/test_matrix.py +0 -0
- {hector_cli-0.5.0 → hector_cli-0.7.0}/tests/test_needs_renode_source.py +0 -0
- {hector_cli-0.5.0 → hector_cli-0.7.0}/tests/test_peripherals.py +0 -0
- {hector_cli-0.5.0 → hector_cli-0.7.0}/tests/test_reporters.py +0 -0
- {hector_cli-0.5.0 → hector_cli-0.7.0}/tests/test_runners.py +0 -0
- {hector_cli-0.5.0 → hector_cli-0.7.0}/tests/test_scaffold.py +0 -0
- {hector_cli-0.5.0 → hector_cli-0.7.0}/tests/test_version.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hector-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: YAML-driven Renode + Verilator simulation orchestrator for embedded hardware CI
|
|
5
5
|
Home-page: https://hector-ci.com
|
|
6
6
|
Author: Nemesis
|
|
@@ -206,10 +206,11 @@ the CLI natively with `pip` and let hector launch real per-step containers.
|
|
|
206
206
|
| `hector run --gather-execution-metrics boardA --gather-execution-metrics boardB` | Enable profiler for selected machines |
|
|
207
207
|
| `hector run --renode-version 1.16.1` | Override the Renode version from `.hector.yaml` |
|
|
208
208
|
| `hector run --renode-image my-renode:dev` | Use a specific (e.g. self-built) Renode Docker image |
|
|
209
|
+
| `hector run --docker-args '-v /opt/tools:/opt/tools'` | Add extra flags (mounts, env, …) to every `docker run` |
|
|
209
210
|
| `hector run --renode-dir /cache/renode` | Use/cache the Renode source checkout at this path (only fetched when needed) |
|
|
210
211
|
| `hector run --renode-integration-dir /cache/integration` | Likewise for the verilator-integration checkout |
|
|
211
212
|
| `hector run --no-docker` | Use locally installed `renode` / `renode-test` instead of Docker |
|
|
212
|
-
| `hector run --workspace-mount /mnt` |
|
|
213
|
+
| `hector run --workspace-mount /mnt` | Mount the hectorfile's folder at this container path and execute there |
|
|
213
214
|
| `hector run --snapshot path/to/snapshot.save` | Load a snapshot instead of booting from the resc |
|
|
214
215
|
| `hector run --snapshot snap.save --renode-version 1.16.1` | Load a snapshot without a `.hector.yaml` |
|
|
215
216
|
| `hector test` | Run the `tests:` section; exit 1 on any failure |
|
|
@@ -482,6 +483,32 @@ hector run --renode-args '--console'
|
|
|
482
483
|
hector run --renode-args '--hide-log --config my.conf'
|
|
483
484
|
```
|
|
484
485
|
|
|
486
|
+
#### Paths and working directory
|
|
487
|
+
|
|
488
|
+
Everything a hectorfile executes — `build:` steps, `shell` tests, Renode and `renode-test` — runs **in the hectorfile's folder**, and paths written in the file (`firmware:`, `platform:`, module `source:`, test `file:` / `snapshot:`, `file:` mappings, `artifacts:`, paths inside scripts and `commands:`) are **relative to that folder**. So `hector test -f boards/nucleo/.hector.yaml` behaves the same as running `hector test` inside `boards/nucleo/`.
|
|
489
|
+
|
|
490
|
+
The directory you run hector from is mounted into the container (at the same path), and the container's working directory (`-w`) is the hectorfile's folder. If the hectorfile lives **outside** the directory you run from (`hector test -f ../other/.hector.yaml`), its folder is mounted as well, at its own path. (With [`--workspace-mount`](#overriding-the-workspace-mount-path---workspace-mount) the hectorfile's folder is mounted at the given path instead.) A path like `../shared/board.repl` works as long as it lands inside one of those mounts — for anything else, add a mount with [`docker_args`](#extra-docker-flags---docker-args). Under `--no-docker` the processes simply run with the hectorfile's folder as their cwd.
|
|
491
|
+
|
|
492
|
+
Paths given on the **command line** (`--output`, `--snapshot`, `--test-file`, `--artifacts`, `--renode-dir`, …) stay relative to where you run hector, and `.hector/` is still created there.
|
|
493
|
+
|
|
494
|
+
#### Extra Docker flags (`--docker-args`)
|
|
495
|
+
|
|
496
|
+
Add your own flags to every `docker run` hector starts — Renode, robot tests, `shell` tests, `build:` steps and module builds — e.g. to mount more directories or set environment variables. Declare them in `.hector.yaml` as a string or a list:
|
|
497
|
+
|
|
498
|
+
```yaml
|
|
499
|
+
docker_args:
|
|
500
|
+
- -v /opt/toolchains:/opt/toolchains:ro
|
|
501
|
+
- -e LICENSE_SERVER=10.0.0.5
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
or pass them on the command line, where they are **appended** to the config's:
|
|
505
|
+
|
|
506
|
+
```bash
|
|
507
|
+
hector test --docker-args '-v /data/firmware:/data/firmware'
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
Use absolute host paths for `-v` (Docker treats a bare name as a named volume). Ignored under `--no-docker`.
|
|
511
|
+
|
|
485
512
|
#### Container networking
|
|
486
513
|
|
|
487
514
|
Every container hector starts — the Renode emulation (`run`), robot tests, `shell` tests, `build:` steps and module builds — runs with `--net=host`. GDB servers (`--debug`), `tcp:` mappings and anything a step listens on are reachable from the host on the same port, and steps can reach host services on `localhost`. No port publishing is involved.
|
|
@@ -545,12 +572,15 @@ An existing directory is reused as-is; a missing one is cloned into. The locatio
|
|
|
545
572
|
|
|
546
573
|
#### Overriding the workspace mount path (`--workspace-mount`)
|
|
547
574
|
|
|
548
|
-
By default
|
|
575
|
+
By default everything is bind-mounted into the container at the **same absolute path** as on the host (e.g. `/home/user/projects/myboard`), so paths look identical inside and outside the container. `--workspace-mount PATH` instead mounts the **hectorfile's folder** at `PATH`, and that's where every step, Renode and `renode-test` execute:
|
|
549
576
|
|
|
550
577
|
```bash
|
|
551
|
-
hector run --workspace-mount /workspace
|
|
578
|
+
hector run --workspace-mount /workspace # . → /workspace
|
|
579
|
+
hector test -f boards/nucleo/.hector.yaml --workspace-mount /workspace # boards/nucleo → /workspace
|
|
552
580
|
```
|
|
553
581
|
|
|
582
|
+
When you run from a different folder than the hectorfile's (the second line), that folder — where `.hector/` and `--output` live — is additionally mounted at its own path, unless it's already inside the hectorfile's folder.
|
|
583
|
+
|
|
554
584
|
**Cannot be combined with `--no-docker`** — if you are running locally there is no container mount to configure.
|
|
555
585
|
|
|
556
586
|
### Selecting a matrix combination (`--job`)
|
|
@@ -625,6 +655,7 @@ The configuration lives in `.hector.yaml`.
|
|
|
625
655
|
| `version` | No | Schema version (currently `"0.1"`). Warns if unrecognised. |
|
|
626
656
|
| `renode_version` | Yes¹ | Renode version, no leading `v` (e.g. `1.16.1`). Selects both the git tag for cloning and the Docker image tag. |
|
|
627
657
|
| `renode_image` | Yes¹ | Full Renode Docker image to use instead of `antmicro/renode:<renode_version>` (e.g. a self-built `my-renode:dev`). See [custom Renode image](#using-a-custom-renode-image---renode-image). |
|
|
658
|
+
| `docker_args` | No | Extra `docker run` flags (string or list) for every container, e.g. more `-v` mounts. See [extra Docker flags](#extra-docker-flags---docker-args). |
|
|
628
659
|
| `arguments` | No | Scalar defaults, overridable by env var or `--set`. |
|
|
629
660
|
| `build` | No | Pre-sim shell steps run once per job in containers (compile firmware, fetch/generate files, …). Interpolated with the job's arguments/matrix variables. |
|
|
630
661
|
| `matrix` | No | Cross-product job expansion. |
|
|
@@ -176,10 +176,11 @@ the CLI natively with `pip` and let hector launch real per-step containers.
|
|
|
176
176
|
| `hector run --gather-execution-metrics boardA --gather-execution-metrics boardB` | Enable profiler for selected machines |
|
|
177
177
|
| `hector run --renode-version 1.16.1` | Override the Renode version from `.hector.yaml` |
|
|
178
178
|
| `hector run --renode-image my-renode:dev` | Use a specific (e.g. self-built) Renode Docker image |
|
|
179
|
+
| `hector run --docker-args '-v /opt/tools:/opt/tools'` | Add extra flags (mounts, env, …) to every `docker run` |
|
|
179
180
|
| `hector run --renode-dir /cache/renode` | Use/cache the Renode source checkout at this path (only fetched when needed) |
|
|
180
181
|
| `hector run --renode-integration-dir /cache/integration` | Likewise for the verilator-integration checkout |
|
|
181
182
|
| `hector run --no-docker` | Use locally installed `renode` / `renode-test` instead of Docker |
|
|
182
|
-
| `hector run --workspace-mount /mnt` |
|
|
183
|
+
| `hector run --workspace-mount /mnt` | Mount the hectorfile's folder at this container path and execute there |
|
|
183
184
|
| `hector run --snapshot path/to/snapshot.save` | Load a snapshot instead of booting from the resc |
|
|
184
185
|
| `hector run --snapshot snap.save --renode-version 1.16.1` | Load a snapshot without a `.hector.yaml` |
|
|
185
186
|
| `hector test` | Run the `tests:` section; exit 1 on any failure |
|
|
@@ -452,6 +453,32 @@ hector run --renode-args '--console'
|
|
|
452
453
|
hector run --renode-args '--hide-log --config my.conf'
|
|
453
454
|
```
|
|
454
455
|
|
|
456
|
+
#### Paths and working directory
|
|
457
|
+
|
|
458
|
+
Everything a hectorfile executes — `build:` steps, `shell` tests, Renode and `renode-test` — runs **in the hectorfile's folder**, and paths written in the file (`firmware:`, `platform:`, module `source:`, test `file:` / `snapshot:`, `file:` mappings, `artifacts:`, paths inside scripts and `commands:`) are **relative to that folder**. So `hector test -f boards/nucleo/.hector.yaml` behaves the same as running `hector test` inside `boards/nucleo/`.
|
|
459
|
+
|
|
460
|
+
The directory you run hector from is mounted into the container (at the same path), and the container's working directory (`-w`) is the hectorfile's folder. If the hectorfile lives **outside** the directory you run from (`hector test -f ../other/.hector.yaml`), its folder is mounted as well, at its own path. (With [`--workspace-mount`](#overriding-the-workspace-mount-path---workspace-mount) the hectorfile's folder is mounted at the given path instead.) A path like `../shared/board.repl` works as long as it lands inside one of those mounts — for anything else, add a mount with [`docker_args`](#extra-docker-flags---docker-args). Under `--no-docker` the processes simply run with the hectorfile's folder as their cwd.
|
|
461
|
+
|
|
462
|
+
Paths given on the **command line** (`--output`, `--snapshot`, `--test-file`, `--artifacts`, `--renode-dir`, …) stay relative to where you run hector, and `.hector/` is still created there.
|
|
463
|
+
|
|
464
|
+
#### Extra Docker flags (`--docker-args`)
|
|
465
|
+
|
|
466
|
+
Add your own flags to every `docker run` hector starts — Renode, robot tests, `shell` tests, `build:` steps and module builds — e.g. to mount more directories or set environment variables. Declare them in `.hector.yaml` as a string or a list:
|
|
467
|
+
|
|
468
|
+
```yaml
|
|
469
|
+
docker_args:
|
|
470
|
+
- -v /opt/toolchains:/opt/toolchains:ro
|
|
471
|
+
- -e LICENSE_SERVER=10.0.0.5
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
or pass them on the command line, where they are **appended** to the config's:
|
|
475
|
+
|
|
476
|
+
```bash
|
|
477
|
+
hector test --docker-args '-v /data/firmware:/data/firmware'
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
Use absolute host paths for `-v` (Docker treats a bare name as a named volume). Ignored under `--no-docker`.
|
|
481
|
+
|
|
455
482
|
#### Container networking
|
|
456
483
|
|
|
457
484
|
Every container hector starts — the Renode emulation (`run`), robot tests, `shell` tests, `build:` steps and module builds — runs with `--net=host`. GDB servers (`--debug`), `tcp:` mappings and anything a step listens on are reachable from the host on the same port, and steps can reach host services on `localhost`. No port publishing is involved.
|
|
@@ -515,12 +542,15 @@ An existing directory is reused as-is; a missing one is cloned into. The locatio
|
|
|
515
542
|
|
|
516
543
|
#### Overriding the workspace mount path (`--workspace-mount`)
|
|
517
544
|
|
|
518
|
-
By default
|
|
545
|
+
By default everything is bind-mounted into the container at the **same absolute path** as on the host (e.g. `/home/user/projects/myboard`), so paths look identical inside and outside the container. `--workspace-mount PATH` instead mounts the **hectorfile's folder** at `PATH`, and that's where every step, Renode and `renode-test` execute:
|
|
519
546
|
|
|
520
547
|
```bash
|
|
521
|
-
hector run --workspace-mount /workspace
|
|
548
|
+
hector run --workspace-mount /workspace # . → /workspace
|
|
549
|
+
hector test -f boards/nucleo/.hector.yaml --workspace-mount /workspace # boards/nucleo → /workspace
|
|
522
550
|
```
|
|
523
551
|
|
|
552
|
+
When you run from a different folder than the hectorfile's (the second line), that folder — where `.hector/` and `--output` live — is additionally mounted at its own path, unless it's already inside the hectorfile's folder.
|
|
553
|
+
|
|
524
554
|
**Cannot be combined with `--no-docker`** — if you are running locally there is no container mount to configure.
|
|
525
555
|
|
|
526
556
|
### Selecting a matrix combination (`--job`)
|
|
@@ -595,6 +625,7 @@ The configuration lives in `.hector.yaml`.
|
|
|
595
625
|
| `version` | No | Schema version (currently `"0.1"`). Warns if unrecognised. |
|
|
596
626
|
| `renode_version` | Yes¹ | Renode version, no leading `v` (e.g. `1.16.1`). Selects both the git tag for cloning and the Docker image tag. |
|
|
597
627
|
| `renode_image` | Yes¹ | Full Renode Docker image to use instead of `antmicro/renode:<renode_version>` (e.g. a self-built `my-renode:dev`). See [custom Renode image](#using-a-custom-renode-image---renode-image). |
|
|
628
|
+
| `docker_args` | No | Extra `docker run` flags (string or list) for every container, e.g. more `-v` mounts. See [extra Docker flags](#extra-docker-flags---docker-args). |
|
|
598
629
|
| `arguments` | No | Scalar defaults, overridable by env var or `--set`. |
|
|
599
630
|
| `build` | No | Pre-sim shell steps run once per job in containers (compile firmware, fetch/generate files, …). Interpolated with the job's arguments/matrix variables. |
|
|
600
631
|
| `matrix` | No | Cross-product job expansion. |
|
|
@@ -59,6 +59,12 @@ def add_common_arguments(parser):
|
|
|
59
59
|
"image, e.g. a self-built one: my-renode:dev). Overrides renode_image / "
|
|
60
60
|
"renode_version from .hector.yaml.",
|
|
61
61
|
)
|
|
62
|
+
parser.add_argument(
|
|
63
|
+
"--docker-args", metavar="ARGS", dest="docker_args", default="",
|
|
64
|
+
help="Extra flags added to every `docker run` hector starts, e.g. more mounts "
|
|
65
|
+
"(--docker-args '-v /opt/tools:/opt/tools -e FOO=1'). Appended to "
|
|
66
|
+
"docker_args from .hector.yaml.",
|
|
67
|
+
)
|
|
62
68
|
parser.add_argument(
|
|
63
69
|
"--renode-dir", metavar="PATH", dest="renode_dir", default=None,
|
|
64
70
|
help="Location of the Renode source checkout (only fetched when a config builds "
|
|
@@ -104,8 +110,8 @@ def add_common_arguments(parser):
|
|
|
104
110
|
)
|
|
105
111
|
docker_mode.add_argument(
|
|
106
112
|
"--workspace-mount", metavar="PATH", dest="workspace_mount", default=None,
|
|
107
|
-
help="
|
|
108
|
-
"(default:
|
|
113
|
+
help="Mount the hectorfile's folder at this container path and execute there "
|
|
114
|
+
"(default: everything is mounted at its own host path).",
|
|
109
115
|
)
|
|
110
116
|
|
|
111
117
|
|
|
@@ -67,7 +67,7 @@ class Registry:
|
|
|
67
67
|
# ==========================================================================
|
|
68
68
|
# CONSTANTS
|
|
69
69
|
# ==========================================================================
|
|
70
|
-
TOOL_VERSION = "0.
|
|
70
|
+
TOOL_VERSION = "0.7.0"
|
|
71
71
|
SUPPORTED_SCHEMA_VERSIONS = {"0.1"}
|
|
72
72
|
|
|
73
73
|
RENODE_REPO_URL = "https://github.com/renode/renode.git"
|
|
@@ -78,8 +78,16 @@ ARTIFACTS_DIR = ".hector" # everything we generate/clone lives here (gi
|
|
|
78
78
|
# hosts we mirror the host cwd, so paths look identical in and out; on Windows the host
|
|
79
79
|
# path (C:\...) is not a valid Linux path, so use a fixed POSIX mount point and translate.
|
|
80
80
|
WORKSPACE_MOUNT = "/workspace" if os.name == "nt" else os.getcwd()
|
|
81
|
-
|
|
81
|
+
# Host directory bind-mounted at WORKSPACE_MOUNT. None = the cwd; --workspace-mount
|
|
82
|
+
# sets it to the config's directory, so the mount point is where execution happens.
|
|
83
|
+
WORKSPACE_ROOT = None
|
|
84
|
+
NO_DOCKER = False # when True, call renode/renode-test directly on host
|
|
82
85
|
EXTRA_MOUNTS = [] # dep dirs outside the workspace, bind-mounted 1:1 into every container
|
|
86
|
+
DOCKER_ARGS = [] # user flags (docker_args: + --docker-args) added to every `docker run`
|
|
87
|
+
# Host directory of the config file being executed. Paths written in the config resolve
|
|
88
|
+
# against it, and scripts / Renode run with it as their working directory. None = cwd
|
|
89
|
+
# (no config loaded, e.g. `run --snapshot` without one).
|
|
90
|
+
CONFIG_DIR = None
|
|
83
91
|
LIBOPENLIBM_NAME = "libopenlibm-Linux-x86_64.a"
|
|
84
92
|
|
|
85
93
|
DEFAULT_QUANTUM = "0.00001" # 10 us; applied when any hub is present
|
|
@@ -119,10 +127,20 @@ def host_uid_gid():
|
|
|
119
127
|
return None
|
|
120
128
|
|
|
121
129
|
|
|
130
|
+
def workspace_root():
|
|
131
|
+
"""Host directory mounted at WORKSPACE_MOUNT (see WORKSPACE_ROOT)."""
|
|
132
|
+
return os.path.realpath(WORKSPACE_ROOT or os.getcwd())
|
|
133
|
+
|
|
134
|
+
|
|
122
135
|
def to_container_path(host_path):
|
|
123
136
|
abs_path = os.path.realpath(host_path)
|
|
124
|
-
root =
|
|
137
|
+
root = workspace_root()
|
|
125
138
|
if os.path.commonpath([abs_path, root]) != root:
|
|
139
|
+
# Dirs outside the workspace registered in EXTRA_MOUNTS (the config's dir or the
|
|
140
|
+
# cwd, a shared Renode cache) are mounted 1:1, so their paths are valid as-is.
|
|
141
|
+
for m in EXTRA_MOUNTS:
|
|
142
|
+
if os.path.commonpath([abs_path, m]) == m:
|
|
143
|
+
return abs_path
|
|
126
144
|
raise ValueError(
|
|
127
145
|
f"Path '{host_path}' resolves outside the project root '{root}'. Everything used "
|
|
128
146
|
f"at runtime must live under the project folder (mounted at '{WORKSPACE_MOUNT}')."
|
|
@@ -130,7 +148,7 @@ def to_container_path(host_path):
|
|
|
130
148
|
# The container is always Linux: build a POSIX path under WORKSPACE_MOUNT (this is a
|
|
131
149
|
# no-op on Linux, where WORKSPACE_MOUNT is the cwd and os.sep is already '/').
|
|
132
150
|
rel = os.path.relpath(abs_path, root).replace(os.sep, "/")
|
|
133
|
-
return posixpath.join(WORKSPACE_MOUNT, rel)
|
|
151
|
+
return WORKSPACE_MOUNT if rel == "." else posixpath.join(WORKSPACE_MOUNT, rel)
|
|
134
152
|
|
|
135
153
|
|
|
136
154
|
def register_dependency_mount(host_dir):
|
|
@@ -140,13 +158,12 @@ def register_dependency_mount(host_dir):
|
|
|
140
158
|
Dirs under the workspace are already covered by the workspace mount, so they map via
|
|
141
159
|
to_container_path. An out-of-tree dir (e.g. a shared cache) is registered in
|
|
142
160
|
EXTRA_MOUNTS and used at its own absolute path inside the container."""
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
if abs_dir not in EXTRA_MOUNTS:
|
|
161
|
+
try:
|
|
162
|
+
return to_container_path(host_dir) # already reachable through some mount
|
|
163
|
+
except ValueError:
|
|
164
|
+
abs_dir = os.path.realpath(host_dir)
|
|
148
165
|
EXTRA_MOUNTS.append(abs_dir)
|
|
149
|
-
|
|
166
|
+
return abs_dir
|
|
150
167
|
|
|
151
168
|
|
|
152
169
|
def container_path_for(host_path):
|
|
@@ -166,13 +183,34 @@ def container_to_host(container_path):
|
|
|
166
183
|
if container_path != WORKSPACE_MOUNT and not container_path.startswith(WORKSPACE_MOUNT + "/"):
|
|
167
184
|
raise ValueError(f"'{container_path}' is not under {WORKSPACE_MOUNT}.")
|
|
168
185
|
rel = posixpath.relpath(container_path, WORKSPACE_MOUNT).replace("/", os.sep)
|
|
169
|
-
|
|
170
|
-
|
|
186
|
+
root = workspace_root()
|
|
187
|
+
host = os.path.realpath(os.path.join(root, rel))
|
|
171
188
|
if os.path.commonpath([host, root]) != root:
|
|
172
189
|
raise ValueError(f"'{container_path}' maps outside the project root.")
|
|
173
190
|
return host
|
|
174
191
|
|
|
175
192
|
|
|
193
|
+
def config_dir():
|
|
194
|
+
"""Host directory config-relative paths resolve against (see CONFIG_DIR)."""
|
|
195
|
+
return CONFIG_DIR or os.getcwd()
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def from_config(path):
|
|
199
|
+
"""Resolve a path written in the config against the config file's directory.
|
|
200
|
+
Absolute paths (host or container, e.g. an interpolated ${RENODE_DIR}) and URLs
|
|
201
|
+
are returned unchanged."""
|
|
202
|
+
if not path or os.path.isabs(path) or path.startswith("/") or "://" in path:
|
|
203
|
+
return path
|
|
204
|
+
return os.path.normpath(os.path.join(config_dir(), path))
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def execution_dir():
|
|
208
|
+
"""Working directory for everything hector executes (build/shell steps, Renode,
|
|
209
|
+
renode-test): the config's directory — as a container path under Docker, a host
|
|
210
|
+
path under --no-docker."""
|
|
211
|
+
return config_dir() if NO_DOCKER else to_container_path(config_dir())
|
|
212
|
+
|
|
213
|
+
|
|
176
214
|
def split_host_container(path):
|
|
177
215
|
if path == WORKSPACE_MOUNT or path.startswith(WORKSPACE_MOUNT + "/"):
|
|
178
216
|
return container_to_host(path), path
|
|
@@ -48,16 +48,17 @@ def _user_flags():
|
|
|
48
48
|
|
|
49
49
|
|
|
50
50
|
def _workspace_mount():
|
|
51
|
-
return ["-v", f"{
|
|
51
|
+
return ["-v", f"{core.workspace_root()}:{core.WORKSPACE_MOUNT}", "-w", core.execution_dir()]
|
|
52
52
|
|
|
53
53
|
|
|
54
54
|
def _extra_mounts():
|
|
55
|
-
"""Bind-mount out-of-tree
|
|
56
|
-
containers reach them at the same path they have on the host
|
|
55
|
+
"""Bind-mount out-of-tree dirs (the config's dir, a shared Renode source cache) 1:1,
|
|
56
|
+
so containers reach them at the same path they have on the host, then append the
|
|
57
|
+
user's own `docker run` flags (extra -v mounts, --env, ...). Empty for the common case."""
|
|
57
58
|
args = []
|
|
58
59
|
for d in core.EXTRA_MOUNTS:
|
|
59
60
|
args += ["-v", f"{d}:{d}"]
|
|
60
|
-
return args
|
|
61
|
+
return args + core.DOCKER_ARGS
|
|
61
62
|
|
|
62
63
|
|
|
63
64
|
# Every container shares the host's network stack: GDB servers, tcp: mappings and
|
|
@@ -93,7 +94,7 @@ def _runtime_flags(runtime):
|
|
|
93
94
|
def run_build(script, image):
|
|
94
95
|
"""Run a build script in the container as root (for apt-get + cmake)."""
|
|
95
96
|
if core.NO_DOCKER:
|
|
96
|
-
subprocess.run(["bash", "-c", script], check=True)
|
|
97
|
+
subprocess.run(["bash", "-c", script], check=True, cwd=core.execution_dir())
|
|
97
98
|
return
|
|
98
99
|
subprocess.run(
|
|
99
100
|
["docker", "run", "--rm", *_HOST_NETWORK, "-u", "0:0",
|
|
@@ -131,14 +132,17 @@ def run_in_container(script, image, live=False):
|
|
|
131
132
|
fail on the first error.
|
|
132
133
|
|
|
133
134
|
Backs the `shell` test type and the `build:` steps. Normally a one-off container of
|
|
134
|
-
`image`; under --no-docker the script runs directly on the host (the image is moot).
|
|
135
|
+
`image`; under --no-docker the script runs directly on the host (the image is moot).
|
|
136
|
+
Either way it runs in the config's directory (core.execution_dir)."""
|
|
137
|
+
cwd = None
|
|
135
138
|
if core.NO_DOCKER:
|
|
136
139
|
cmd = ["bash", "-c", f"set -ex\n{script}"]
|
|
140
|
+
cwd = core.execution_dir()
|
|
137
141
|
else:
|
|
138
142
|
cmd = ["docker", "run", "--rm", *_HOST_NETWORK, *_workspace_mount(),
|
|
139
143
|
*_extra_mounts(), *_user_flags(), image, "sh", "-c", f"set -ex\n{script}"]
|
|
140
144
|
if live:
|
|
141
|
-
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
|
|
145
|
+
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, cwd=cwd,
|
|
142
146
|
stderr=subprocess.STDOUT, text=True)
|
|
143
147
|
lines = []
|
|
144
148
|
for line in proc.stdout:
|
|
@@ -146,7 +150,8 @@ def run_in_container(script, image, live=False):
|
|
|
146
150
|
lines.append(line)
|
|
147
151
|
proc.wait()
|
|
148
152
|
return "".join(lines), proc.returncode == 0
|
|
149
|
-
proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True
|
|
153
|
+
proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,
|
|
154
|
+
cwd=cwd)
|
|
150
155
|
if proc.stdout:
|
|
151
156
|
print(proc.stdout, end="")
|
|
152
157
|
return proc.stdout, proc.returncode == 0
|
|
@@ -158,12 +163,13 @@ def build_renode_argv(resc_filename, image, extra_args=None, runtime=None):
|
|
|
158
163
|
|
|
159
164
|
Returning the command (instead of running it) is what lets the `export` command
|
|
160
165
|
print it verbatim and keeps run_renode a one-liner around this builder.
|
|
161
|
-
runtime: aggregated RuntimeOptions whose docker flags (caps/devices/root) are added.
|
|
166
|
+
runtime: aggregated RuntimeOptions whose docker flags (caps/devices/root) are added.
|
|
167
|
+
The resc path is made absolute: Renode runs in the config's directory, not the cwd."""
|
|
162
168
|
if core.NO_DOCKER:
|
|
163
|
-
return ["renode", resc_filename] + (extra_args or [])
|
|
169
|
+
return ["renode", os.path.abspath(resc_filename)] + (extra_args or [])
|
|
164
170
|
return ["docker", "run", "-it", "--rm", *_xdisplay_args(), *_HOST_NETWORK,
|
|
165
171
|
*_workspace_mount(), *_extra_mounts(), *_user_flags(), *_runtime_flags(runtime),
|
|
166
|
-
image, "renode", resc_filename] + (extra_args or [])
|
|
172
|
+
image, "renode", to_container_path(resc_filename)] + (extra_args or [])
|
|
167
173
|
|
|
168
174
|
|
|
169
175
|
def run_renode(resc_filename, image, extra_args=None, output_dir="results", runtime=None):
|
|
@@ -172,7 +178,8 @@ def run_renode(resc_filename, image, extra_args=None, output_dir="results", runt
|
|
|
172
178
|
runtime: aggregated RuntimeOptions whose docker flags (caps/devices/root) are added."""
|
|
173
179
|
if not core.NO_DOCKER:
|
|
174
180
|
os.makedirs(os.path.join(os.getcwd(), output_dir), exist_ok=True)
|
|
175
|
-
subprocess.run(build_renode_argv(resc_filename, image, extra_args, runtime)
|
|
181
|
+
subprocess.run(build_renode_argv(resc_filename, image, extra_args, runtime),
|
|
182
|
+
cwd=core.execution_dir() if core.NO_DOCKER else None)
|
|
176
183
|
|
|
177
184
|
|
|
178
185
|
def run_renode_test(robot_file_host, image, results_dir_host,
|
|
@@ -186,12 +193,14 @@ def run_renode_test(robot_file_host, image, results_dir_host,
|
|
|
186
193
|
"""
|
|
187
194
|
os.makedirs(results_dir_host, exist_ok=True)
|
|
188
195
|
if core.NO_DOCKER:
|
|
189
|
-
|
|
196
|
+
# Absolute paths: renode-test runs in the config's directory, not the cwd.
|
|
197
|
+
cmd = ["renode-test", os.path.abspath(robot_file_host),
|
|
198
|
+
"-r", os.path.abspath(results_dir_host)]
|
|
190
199
|
if resc_host_path:
|
|
191
|
-
cmd += ["--variable", f"RESC:{resc_host_path}"]
|
|
200
|
+
cmd += ["--variable", f"RESC:{os.path.abspath(resc_host_path)}"]
|
|
192
201
|
if extra_args:
|
|
193
202
|
cmd += extra_args
|
|
194
|
-
result = subprocess.run(cmd)
|
|
203
|
+
result = subprocess.run(cmd, cwd=core.execution_dir())
|
|
195
204
|
return result.returncode == 0
|
|
196
205
|
flags = ["-it"] if interactive else []
|
|
197
206
|
display = _xdisplay_args() if interactive else []
|
|
@@ -262,7 +262,7 @@ def snapshot_resc(snapshot_container_path, debug_config=None, no_start=False):
|
|
|
262
262
|
# ARTIFACT COLLECTION
|
|
263
263
|
# ==========================================================================
|
|
264
264
|
|
|
265
|
-
def collect_artifacts(patterns, output_dir, job_index=None):
|
|
265
|
+
def collect_artifacts(patterns, output_dir, job_index=None, base_dir="."):
|
|
266
266
|
"""Sweep the global artifact glob patterns and copy matches into
|
|
267
267
|
<output_dir>/artifacts/ so a single directory can be uploaded by CI / read by a
|
|
268
268
|
dashboard.
|
|
@@ -270,6 +270,8 @@ def collect_artifacts(patterns, output_dir, job_index=None):
|
|
|
270
270
|
patterns : list of glob strings (top-level 'artifacts:' or --artifacts override)
|
|
271
271
|
output_dir : the run output directory (--output)
|
|
272
272
|
job_index : 1-based job number; prefixes copied filenames to avoid cross-job clashes
|
|
273
|
+
base_dir : directory relative patterns resolve against (the config's directory
|
|
274
|
+
for 'artifacts:', the cwd for --artifacts)
|
|
273
275
|
|
|
274
276
|
Returns the list of collected destination paths (relative to cwd).
|
|
275
277
|
"""
|
|
@@ -283,7 +285,7 @@ def collect_artifacts(patterns, output_dir, job_index=None):
|
|
|
283
285
|
collected, seen = [], set()
|
|
284
286
|
header_shown = False
|
|
285
287
|
for pattern in patterns:
|
|
286
|
-
for src in sorted(_match_artifacts(pattern, dest_root)):
|
|
288
|
+
for src in sorted(_match_artifacts(pattern, dest_root, base_dir)):
|
|
287
289
|
real = os.path.realpath(src)
|
|
288
290
|
if real in seen or not os.path.isfile(real):
|
|
289
291
|
continue
|
|
@@ -308,19 +310,19 @@ def collect_artifacts(patterns, output_dir, job_index=None):
|
|
|
308
310
|
return collected
|
|
309
311
|
|
|
310
312
|
|
|
311
|
-
def _match_artifacts(pattern, dest_root):
|
|
312
|
-
"""Resolve one artifact glob pattern to a list of host paths.
|
|
313
|
+
def _match_artifacts(pattern, dest_root, base_dir="."):
|
|
314
|
+
"""Resolve one artifact glob pattern (relative to base_dir) to a list of host paths.
|
|
313
315
|
|
|
314
|
-
A bare filename pattern (no '/', e.g. '*.xml') searches the whole
|
|
316
|
+
A bare filename pattern (no '/', e.g. '*.xml') searches the whole base_dir tree
|
|
315
317
|
recursively, pruning hidden dirs (.hector, .git, …) and the artifacts output dir —
|
|
316
318
|
so 'results/junit.xml' is found without sweeping the bundled Renode clone.
|
|
317
319
|
A pattern with a path component ('results/*.bin', 'logs/**/*.log') is passed to
|
|
318
320
|
glob as-is, so explicit directories and '**' behave normally.
|
|
319
321
|
"""
|
|
320
322
|
if "/" in pattern or os.sep in pattern:
|
|
321
|
-
return glob.glob(pattern, recursive=True)
|
|
323
|
+
return glob.glob(os.path.join(base_dir, pattern), recursive=True)
|
|
322
324
|
matches = []
|
|
323
|
-
for root, dirs, files in os.walk(
|
|
325
|
+
for root, dirs, files in os.walk(base_dir):
|
|
324
326
|
dirs[:] = [d for d in dirs
|
|
325
327
|
if not d.startswith(".")
|
|
326
328
|
and os.path.realpath(os.path.join(root, d)) != dest_root]
|
|
@@ -33,7 +33,7 @@ YAML then accepts: mappings: |\n boardA.eth0 -> tap:tap0
|
|
|
33
33
|
from dataclasses import dataclass, field
|
|
34
34
|
from typing import List
|
|
35
35
|
|
|
36
|
-
from .core import Registry, RuntimeOptions, parse_endpoint, to_container_path
|
|
36
|
+
from .core import Registry, RuntimeOptions, from_config, parse_endpoint, to_container_path
|
|
37
37
|
from .generator import queue_command
|
|
38
38
|
|
|
39
39
|
MAPPING_BACKENDS = Registry("mapping backend")
|
|
@@ -55,7 +55,7 @@ def file_backend(_node_name, peripheral, param, _node_config):
|
|
|
55
55
|
if not param:
|
|
56
56
|
raise ValueError("'file' backend requires a path (file:<path>).")
|
|
57
57
|
return MappingResult(
|
|
58
|
-
machine_cmd=f"{peripheral} CreateFileBackend @{to_container_path(param)} true",
|
|
58
|
+
machine_cmd=f"{peripheral} CreateFileBackend @{to_container_path(from_config(param))} true",
|
|
59
59
|
)
|
|
60
60
|
|
|
61
61
|
|
|
@@ -14,7 +14,7 @@ import shutil
|
|
|
14
14
|
from dataclasses import dataclass
|
|
15
15
|
|
|
16
16
|
from .core import (ARTIFACTS_DIR, LIBOPENLIBM_NAME, Registry,
|
|
17
|
-
container_path_for, host_uid_gid, split_host_container,
|
|
17
|
+
container_path_for, from_config, host_uid_gid, split_host_container,
|
|
18
18
|
to_container_path)
|
|
19
19
|
from .docker import run_build
|
|
20
20
|
|
|
@@ -46,7 +46,7 @@ def build_renode_verilator(module_name, module_config, ctx):
|
|
|
46
46
|
if not source:
|
|
47
47
|
raise ValueError(f"Module '{module_name}' is missing required 'source'.")
|
|
48
48
|
|
|
49
|
-
source_host, source_container = split_host_container(source)
|
|
49
|
+
source_host, source_container = split_host_container(from_config(source))
|
|
50
50
|
if not os.path.isdir(source_host):
|
|
51
51
|
raise FileNotFoundError(f"Module '{module_name}': source dir not found: {source} ({source_host})")
|
|
52
52
|
|
|
@@ -113,7 +113,7 @@ def build_csharp(module_name, module_config, ctx):
|
|
|
113
113
|
if not source:
|
|
114
114
|
raise ValueError(f"Module '{module_name}' is missing required 'source'.")
|
|
115
115
|
|
|
116
|
-
source_host, _ = split_host_container(source)
|
|
116
|
+
source_host, _ = split_host_container(from_config(source))
|
|
117
117
|
|
|
118
118
|
# ---- raw .cs: Renode compiles it at load time (bundled Roslyn) — no SDK, no build ----
|
|
119
119
|
if source_host.endswith(".cs"):
|
|
@@ -47,6 +47,9 @@ from .validator import print_issues, validate_config
|
|
|
47
47
|
# The config file every command reads/writes. Overridable with -f/--file; set by
|
|
48
48
|
# apply_global_flags before any config is loaded.
|
|
49
49
|
CONFIG_PATH = ".hector.yaml"
|
|
50
|
+
# --workspace-mount given: mount the config's directory (where execution happens) at
|
|
51
|
+
# that path, instead of the cwd. Set by apply_global_flags, applied once the config loads.
|
|
52
|
+
MOUNT_CONFIG_DIR = False
|
|
50
53
|
|
|
51
54
|
|
|
52
55
|
def apply_global_flags(args):
|
|
@@ -60,7 +63,9 @@ def apply_global_flags(args):
|
|
|
60
63
|
global CONFIG_PATH
|
|
61
64
|
CONFIG_PATH = path
|
|
62
65
|
if getattr(args, "workspace_mount", None):
|
|
66
|
+
global MOUNT_CONFIG_DIR
|
|
63
67
|
_core.WORKSPACE_MOUNT = args.workspace_mount
|
|
68
|
+
MOUNT_CONFIG_DIR = True
|
|
64
69
|
if getattr(args, "no_docker", False) or os.environ.get("HECTOR_NO_DOCKER"):
|
|
65
70
|
_core.NO_DOCKER = True
|
|
66
71
|
_core.WORKSPACE_MOUNT = os.getcwd()
|
|
@@ -104,9 +109,26 @@ def load_config_or_exit():
|
|
|
104
109
|
if v is not None and str(v) not in SUPPORTED_SCHEMA_VERSIONS:
|
|
105
110
|
print(f"[WARN] .hector.yaml 'version: {v}' is not in supported versions "
|
|
106
111
|
f"({', '.join(sorted(SUPPORTED_SCHEMA_VERSIONS))}). Proceeding anyway.")
|
|
112
|
+
_set_config_dir(config_path)
|
|
107
113
|
return config
|
|
108
114
|
|
|
109
115
|
|
|
116
|
+
def _set_config_dir(config_path):
|
|
117
|
+
"""Make the config's directory the base for its paths and the working directory of
|
|
118
|
+
everything executed (see core.CONFIG_DIR), and make sure both it and the cwd (where
|
|
119
|
+
.hector/ and --output live) are reachable from the container.
|
|
120
|
+
|
|
121
|
+
By default the cwd is the workspace mount and a config dir outside it is mounted
|
|
122
|
+
1:1 as well. With --workspace-mount, the config dir is mounted at that path instead
|
|
123
|
+
(so execution happens there) and a cwd outside it is the one mounted 1:1."""
|
|
124
|
+
_core.CONFIG_DIR = os.path.realpath(os.path.dirname(os.path.abspath(config_path)))
|
|
125
|
+
if MOUNT_CONFIG_DIR:
|
|
126
|
+
_core.WORKSPACE_ROOT = _core.CONFIG_DIR
|
|
127
|
+
_core.register_dependency_mount(os.getcwd())
|
|
128
|
+
else:
|
|
129
|
+
_core.register_dependency_mount(_core.CONFIG_DIR)
|
|
130
|
+
|
|
131
|
+
|
|
110
132
|
_VERSION_TAG_RE = re.compile(r"^v?\d+\.\d+\.\d+$")
|
|
111
133
|
|
|
112
134
|
|
|
@@ -138,6 +160,16 @@ def resolve_renode_image(config, args):
|
|
|
138
160
|
return (f"v{normalized}" if normalized else None), image
|
|
139
161
|
|
|
140
162
|
|
|
163
|
+
def apply_docker_args(config, args):
|
|
164
|
+
"""Collect the user's extra `docker run` flags — the config's `docker_args:` (a
|
|
165
|
+
string or a list of strings) followed by --docker-args — into core.DOCKER_ARGS."""
|
|
166
|
+
raw = config.get("docker_args") or []
|
|
167
|
+
flags = []
|
|
168
|
+
for item in ([raw] if isinstance(raw, str) else raw):
|
|
169
|
+
flags += shlex.split(str(item))
|
|
170
|
+
_core.DOCKER_ARGS = flags + shlex.split(getattr(args, "docker_args", "") or "")
|
|
171
|
+
|
|
172
|
+
|
|
141
173
|
def _resolve_set_overrides(args):
|
|
142
174
|
overrides = {}
|
|
143
175
|
for kv in (getattr(args, "set_args", None) or []):
|
|
@@ -166,6 +198,7 @@ def build_job_config(action, config, args, *, test_mode=False):
|
|
|
166
198
|
"""Assemble a JobConfig from parsed args — the shared run / test / export path:
|
|
167
199
|
pin the image, prepare the framework (lazily), resolve arguments + artifacts."""
|
|
168
200
|
git_tag, docker_image = resolve_renode_image(config, args)
|
|
201
|
+
apply_docker_args(config, args)
|
|
169
202
|
|
|
170
203
|
# Clone Renode only when the config actually needs the source; otherwise skip it
|
|
171
204
|
# entirely (a ~200 MB clone) and leave the framework vars empty.
|
|
@@ -186,8 +219,14 @@ def build_job_config(action, config, args, *, test_mode=False):
|
|
|
186
219
|
|
|
187
220
|
resolved_args = resolve_arguments(config.get("arguments", {}),
|
|
188
221
|
overrides=_resolve_set_overrides(args))
|
|
189
|
-
|
|
190
|
-
|
|
222
|
+
# --artifacts globs are relative to where hector runs; the config's own 'artifacts:'
|
|
223
|
+
# to the config's directory (like every other path written in the config).
|
|
224
|
+
if getattr(args, "artifacts", None):
|
|
225
|
+
artifacts_cfg, artifacts_base = args.artifacts, os.getcwd()
|
|
226
|
+
else:
|
|
227
|
+
artifacts_cfg = as_lines(config.get("artifacts", []))
|
|
228
|
+
artifacts_base = _core.config_dir()
|
|
229
|
+
test_file = getattr(args, "test_file", None)
|
|
191
230
|
return JobConfig(
|
|
192
231
|
config=config,
|
|
193
232
|
fw_vars=fw_vars,
|
|
@@ -196,7 +235,8 @@ def build_job_config(action, config, args, *, test_mode=False):
|
|
|
196
235
|
action=action,
|
|
197
236
|
test_mode=test_mode,
|
|
198
237
|
debug_args=getattr(args, "debug", None) or [],
|
|
199
|
-
|
|
238
|
+
# Absolute so it isn't re-resolved against the config's directory.
|
|
239
|
+
test_file=os.path.abspath(test_file) if test_file else None,
|
|
200
240
|
reporter_names=_resolve_reporters(getattr(args, "reporters", None)),
|
|
201
241
|
renode_args=shlex.split(getattr(args, "renode_args", "") or ""),
|
|
202
242
|
renode_test_args=shlex.split(getattr(args, "renode_test_args", "") or ""),
|
|
@@ -208,6 +248,7 @@ def build_job_config(action, config, args, *, test_mode=False):
|
|
|
208
248
|
output_dir=getattr(args, "output", "results"),
|
|
209
249
|
gather_metrics=getattr(args, "gather_execution_metrics", None),
|
|
210
250
|
artifacts=artifacts_cfg,
|
|
251
|
+
artifacts_base=artifacts_base,
|
|
211
252
|
)
|
|
212
253
|
|
|
213
254
|
|
|
@@ -241,6 +282,7 @@ def snapshot_fastpath(args):
|
|
|
241
282
|
print(f"[ERROR] Snapshot not found: {args.snapshot}", file=sys.stderr)
|
|
242
283
|
sys.exit(1)
|
|
243
284
|
_, docker_image = resolve_renode_image({}, args)
|
|
285
|
+
apply_docker_args({}, args)
|
|
244
286
|
resc_root = os.path.join(ARTIFACTS_DIR, "resc")
|
|
245
287
|
os.makedirs(resc_root, exist_ok=True)
|
|
246
288
|
# No .hector.yaml here, so debug machine names come straight from --debug.
|
|
@@ -485,6 +527,7 @@ class JobConfig:
|
|
|
485
527
|
output_dir: str = "results"
|
|
486
528
|
gather_metrics: list = None
|
|
487
529
|
artifacts: list = field(default_factory=list)
|
|
530
|
+
artifacts_base: str = "." # directory the artifact globs resolve against
|
|
488
531
|
|
|
489
532
|
|
|
490
533
|
@dataclass
|
|
@@ -757,7 +800,8 @@ def run_job(i, total_jobs, job_context, cfg):
|
|
|
757
800
|
# Global glob patterns (top-level 'artifacts:' or --artifacts), interpolated
|
|
758
801
|
# with this job's context so matrix variables resolve.
|
|
759
802
|
artifact_patterns = as_lines(interpolate(list(cfg.artifacts), context))
|
|
760
|
-
collect_artifacts(artifact_patterns, cfg.output_dir, i + 1
|
|
803
|
+
collect_artifacts(artifact_patterns, cfg.output_dir, i + 1,
|
|
804
|
+
base_dir=cfg.artifacts_base)
|
|
761
805
|
|
|
762
806
|
except Exception as exc:
|
|
763
807
|
result["passed"] = False
|
|
@@ -20,7 +20,7 @@ import time
|
|
|
20
20
|
from dataclasses import dataclass, field
|
|
21
21
|
|
|
22
22
|
from . import core as _core
|
|
23
|
-
from .core import ARTIFACTS_DIR, Registry, to_container_path
|
|
23
|
+
from .core import ARTIFACTS_DIR, Registry, from_config, to_container_path
|
|
24
24
|
from .docker import resolve_shell_image, run_in_container, run_renode_test
|
|
25
25
|
from .reporters import StepResult
|
|
26
26
|
|
|
@@ -170,7 +170,7 @@ def generate_robot_file(test_name, steps, resc_host_path, job_index, test_index,
|
|
|
170
170
|
def _run_robot_test(test, steps, ctx, test_index):
|
|
171
171
|
"""Run a robot test. Returns a single StepResult for the whole test."""
|
|
172
172
|
test_name = test.get("name", f"test_{test_index}")
|
|
173
|
-
effective_snapshot = ctx.snapshot or test.get("snapshot", "")
|
|
173
|
+
effective_snapshot = ctx.snapshot or from_config(test.get("snapshot", ""))
|
|
174
174
|
if effective_snapshot and not os.path.isfile(effective_snapshot):
|
|
175
175
|
raise FileNotFoundError(f"Snapshot not found: {effective_snapshot}")
|
|
176
176
|
|
|
@@ -190,7 +190,7 @@ def _run_robot_test(test, steps, ctx, test_index):
|
|
|
190
190
|
if len(steps) == 1 and steps[0].get("file"):
|
|
191
191
|
if effective_snapshot:
|
|
192
192
|
print(" [WARN] snapshot ignored for file-based robot tests.")
|
|
193
|
-
robot_file = steps[0]["file"]
|
|
193
|
+
robot_file = from_config(steps[0]["file"])
|
|
194
194
|
else:
|
|
195
195
|
if any(s.get("file") for s in steps):
|
|
196
196
|
raise ValueError(f"Test '{test_name}': mixing file: and script: steps is not supported.")
|
|
@@ -28,7 +28,7 @@ _TRIGGER_TOP_KEYS = {"when", "cron", "reporters", "jobs", "fail_fast", "timeout"
|
|
|
28
28
|
|
|
29
29
|
# Top-level keys defined by this schema version; anything else is flagged.
|
|
30
30
|
_KNOWN_TOP_KEYS = {
|
|
31
|
-
"version", "renode_version", "renode_image", "arguments", "matrix",
|
|
31
|
+
"version", "renode_version", "renode_image", "docker_args", "arguments", "matrix",
|
|
32
32
|
"modules", "hubs", "machines", "connections", "mappings", "build", "tests", "quantum",
|
|
33
33
|
"artifacts",
|
|
34
34
|
} | _TRIGGER_TOP_KEYS
|
|
@@ -91,6 +91,13 @@ def validate_config(config) -> Tuple[List[Issue], List[Issue]]:
|
|
|
91
91
|
if ri and not isinstance(ri, str):
|
|
92
92
|
err("renode_image", f"Must be a string like 'my-renode:dev', got {type(ri).__name__}.")
|
|
93
93
|
|
|
94
|
+
# ---- docker_args ----
|
|
95
|
+
da = config.get("docker_args")
|
|
96
|
+
if da is not None and not (isinstance(da, str) or
|
|
97
|
+
(isinstance(da, list) and all(isinstance(x, str) for x in da))):
|
|
98
|
+
err("docker_args", "Must be a string or a list of strings "
|
|
99
|
+
"(e.g. ['-v /opt/tools:/opt/tools', '-e FOO=1']).")
|
|
100
|
+
|
|
94
101
|
# ---- unknown / renamed top-level keys ----
|
|
95
102
|
for k in config:
|
|
96
103
|
if k in _RENAMED_TOP_KEYS:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hector-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: YAML-driven Renode + Verilator simulation orchestrator for embedded hardware CI
|
|
5
5
|
Home-page: https://hector-ci.com
|
|
6
6
|
Author: Nemesis
|
|
@@ -206,10 +206,11 @@ the CLI natively with `pip` and let hector launch real per-step containers.
|
|
|
206
206
|
| `hector run --gather-execution-metrics boardA --gather-execution-metrics boardB` | Enable profiler for selected machines |
|
|
207
207
|
| `hector run --renode-version 1.16.1` | Override the Renode version from `.hector.yaml` |
|
|
208
208
|
| `hector run --renode-image my-renode:dev` | Use a specific (e.g. self-built) Renode Docker image |
|
|
209
|
+
| `hector run --docker-args '-v /opt/tools:/opt/tools'` | Add extra flags (mounts, env, …) to every `docker run` |
|
|
209
210
|
| `hector run --renode-dir /cache/renode` | Use/cache the Renode source checkout at this path (only fetched when needed) |
|
|
210
211
|
| `hector run --renode-integration-dir /cache/integration` | Likewise for the verilator-integration checkout |
|
|
211
212
|
| `hector run --no-docker` | Use locally installed `renode` / `renode-test` instead of Docker |
|
|
212
|
-
| `hector run --workspace-mount /mnt` |
|
|
213
|
+
| `hector run --workspace-mount /mnt` | Mount the hectorfile's folder at this container path and execute there |
|
|
213
214
|
| `hector run --snapshot path/to/snapshot.save` | Load a snapshot instead of booting from the resc |
|
|
214
215
|
| `hector run --snapshot snap.save --renode-version 1.16.1` | Load a snapshot without a `.hector.yaml` |
|
|
215
216
|
| `hector test` | Run the `tests:` section; exit 1 on any failure |
|
|
@@ -482,6 +483,32 @@ hector run --renode-args '--console'
|
|
|
482
483
|
hector run --renode-args '--hide-log --config my.conf'
|
|
483
484
|
```
|
|
484
485
|
|
|
486
|
+
#### Paths and working directory
|
|
487
|
+
|
|
488
|
+
Everything a hectorfile executes — `build:` steps, `shell` tests, Renode and `renode-test` — runs **in the hectorfile's folder**, and paths written in the file (`firmware:`, `platform:`, module `source:`, test `file:` / `snapshot:`, `file:` mappings, `artifacts:`, paths inside scripts and `commands:`) are **relative to that folder**. So `hector test -f boards/nucleo/.hector.yaml` behaves the same as running `hector test` inside `boards/nucleo/`.
|
|
489
|
+
|
|
490
|
+
The directory you run hector from is mounted into the container (at the same path), and the container's working directory (`-w`) is the hectorfile's folder. If the hectorfile lives **outside** the directory you run from (`hector test -f ../other/.hector.yaml`), its folder is mounted as well, at its own path. (With [`--workspace-mount`](#overriding-the-workspace-mount-path---workspace-mount) the hectorfile's folder is mounted at the given path instead.) A path like `../shared/board.repl` works as long as it lands inside one of those mounts — for anything else, add a mount with [`docker_args`](#extra-docker-flags---docker-args). Under `--no-docker` the processes simply run with the hectorfile's folder as their cwd.
|
|
491
|
+
|
|
492
|
+
Paths given on the **command line** (`--output`, `--snapshot`, `--test-file`, `--artifacts`, `--renode-dir`, …) stay relative to where you run hector, and `.hector/` is still created there.
|
|
493
|
+
|
|
494
|
+
#### Extra Docker flags (`--docker-args`)
|
|
495
|
+
|
|
496
|
+
Add your own flags to every `docker run` hector starts — Renode, robot tests, `shell` tests, `build:` steps and module builds — e.g. to mount more directories or set environment variables. Declare them in `.hector.yaml` as a string or a list:
|
|
497
|
+
|
|
498
|
+
```yaml
|
|
499
|
+
docker_args:
|
|
500
|
+
- -v /opt/toolchains:/opt/toolchains:ro
|
|
501
|
+
- -e LICENSE_SERVER=10.0.0.5
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
or pass them on the command line, where they are **appended** to the config's:
|
|
505
|
+
|
|
506
|
+
```bash
|
|
507
|
+
hector test --docker-args '-v /data/firmware:/data/firmware'
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
Use absolute host paths for `-v` (Docker treats a bare name as a named volume). Ignored under `--no-docker`.
|
|
511
|
+
|
|
485
512
|
#### Container networking
|
|
486
513
|
|
|
487
514
|
Every container hector starts — the Renode emulation (`run`), robot tests, `shell` tests, `build:` steps and module builds — runs with `--net=host`. GDB servers (`--debug`), `tcp:` mappings and anything a step listens on are reachable from the host on the same port, and steps can reach host services on `localhost`. No port publishing is involved.
|
|
@@ -545,12 +572,15 @@ An existing directory is reused as-is; a missing one is cloned into. The locatio
|
|
|
545
572
|
|
|
546
573
|
#### Overriding the workspace mount path (`--workspace-mount`)
|
|
547
574
|
|
|
548
|
-
By default
|
|
575
|
+
By default everything is bind-mounted into the container at the **same absolute path** as on the host (e.g. `/home/user/projects/myboard`), so paths look identical inside and outside the container. `--workspace-mount PATH` instead mounts the **hectorfile's folder** at `PATH`, and that's where every step, Renode and `renode-test` execute:
|
|
549
576
|
|
|
550
577
|
```bash
|
|
551
|
-
hector run --workspace-mount /workspace
|
|
578
|
+
hector run --workspace-mount /workspace # . → /workspace
|
|
579
|
+
hector test -f boards/nucleo/.hector.yaml --workspace-mount /workspace # boards/nucleo → /workspace
|
|
552
580
|
```
|
|
553
581
|
|
|
582
|
+
When you run from a different folder than the hectorfile's (the second line), that folder — where `.hector/` and `--output` live — is additionally mounted at its own path, unless it's already inside the hectorfile's folder.
|
|
583
|
+
|
|
554
584
|
**Cannot be combined with `--no-docker`** — if you are running locally there is no container mount to configure.
|
|
555
585
|
|
|
556
586
|
### Selecting a matrix combination (`--job`)
|
|
@@ -625,6 +655,7 @@ The configuration lives in `.hector.yaml`.
|
|
|
625
655
|
| `version` | No | Schema version (currently `"0.1"`). Warns if unrecognised. |
|
|
626
656
|
| `renode_version` | Yes¹ | Renode version, no leading `v` (e.g. `1.16.1`). Selects both the git tag for cloning and the Docker image tag. |
|
|
627
657
|
| `renode_image` | Yes¹ | Full Renode Docker image to use instead of `antmicro/renode:<renode_version>` (e.g. a self-built `my-renode:dev`). See [custom Renode image](#using-a-custom-renode-image---renode-image). |
|
|
658
|
+
| `docker_args` | No | Extra `docker run` flags (string or list) for every container, e.g. more `-v` mounts. See [extra Docker flags](#extra-docker-flags---docker-args). |
|
|
628
659
|
| `arguments` | No | Scalar defaults, overridable by env var or `--set`. |
|
|
629
660
|
| `build` | No | Pre-sim shell steps run once per job in containers (compile firmware, fetch/generate files, …). Interpolated with the job's arguments/matrix variables. |
|
|
630
661
|
| `matrix` | No | Cross-product job expansion. |
|
|
@@ -84,6 +84,33 @@ def test_container_path_for_prefers_extra_mount(project, tmp_path_factory):
|
|
|
84
84
|
assert core.container_path_for(str(inner)) == str((cache / "renode" / "x.so").resolve())
|
|
85
85
|
|
|
86
86
|
|
|
87
|
+
def test_from_config_resolves_against_config_dir(project, monkeypatch):
|
|
88
|
+
monkeypatch.setattr(core, "CONFIG_DIR", str(project / "board"))
|
|
89
|
+
assert core.from_config("fw/app.elf") == str(project / "board" / "fw" / "app.elf")
|
|
90
|
+
assert core.from_config("../shared/x.repl") == str(project / "shared" / "x.repl")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_from_config_leaves_absolute_and_urls(project, monkeypatch):
|
|
94
|
+
monkeypatch.setattr(core, "CONFIG_DIR", str(project / "board"))
|
|
95
|
+
for p in ("/opt/renode/x.repl", "https://example.com/fw.elf", "", None):
|
|
96
|
+
assert core.from_config(p) == p
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_from_config_defaults_to_cwd(project, monkeypatch):
|
|
100
|
+
monkeypatch.setattr(core, "CONFIG_DIR", None)
|
|
101
|
+
assert core.from_config("fw.elf") == str(project / "fw.elf")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_execution_dir_is_container_path_of_config_dir(project, monkeypatch):
|
|
105
|
+
(project / "board").mkdir()
|
|
106
|
+
monkeypatch.setattr(core, "NO_DOCKER", False)
|
|
107
|
+
monkeypatch.setattr(core, "WORKSPACE_MOUNT", "/workspace")
|
|
108
|
+
monkeypatch.setattr(core, "CONFIG_DIR", str(project / "board"))
|
|
109
|
+
assert core.execution_dir() == "/workspace/board"
|
|
110
|
+
monkeypatch.setattr(core, "NO_DOCKER", True)
|
|
111
|
+
assert core.execution_dir() == str(project / "board")
|
|
112
|
+
|
|
113
|
+
|
|
87
114
|
def test_host_uid_gid_on_posix():
|
|
88
115
|
ids = core.host_uid_gid()
|
|
89
116
|
if hasattr(os, "getuid"):
|
|
@@ -14,6 +14,8 @@ def clean_env(monkeypatch, tmp_path):
|
|
|
14
14
|
monkeypatch.setattr(core, "WORKSPACE_MOUNT", str(tmp_path))
|
|
15
15
|
monkeypatch.setattr(core, "EXTRA_MOUNTS", [])
|
|
16
16
|
monkeypatch.setattr(core, "NO_DOCKER", False)
|
|
17
|
+
monkeypatch.setattr(core, "CONFIG_DIR", None)
|
|
18
|
+
monkeypatch.setattr(core, "DOCKER_ARGS", [])
|
|
17
19
|
# Neutralise X11 forwarding so argv is deterministic across hosts.
|
|
18
20
|
monkeypatch.delenv("XAUTHORITY", raising=False)
|
|
19
21
|
monkeypatch.delenv("HOME", raising=False)
|
|
@@ -21,20 +23,71 @@ def clean_env(monkeypatch, tmp_path):
|
|
|
21
23
|
|
|
22
24
|
# ---- build_renode_argv ----
|
|
23
25
|
|
|
24
|
-
def test_no_docker_is_bare_renode(monkeypatch):
|
|
26
|
+
def test_no_docker_is_bare_renode(monkeypatch, tmp_path):
|
|
25
27
|
monkeypatch.setattr(core, "NO_DOCKER", True)
|
|
28
|
+
# Absolute: renode runs in the config's directory, not the cwd.
|
|
26
29
|
assert docker.build_renode_argv("job.resc", "img", extra_args=["--console"]) == [
|
|
27
|
-
"renode", "job.resc", "--console"]
|
|
30
|
+
"renode", str(tmp_path / "job.resc"), "--console"]
|
|
28
31
|
|
|
29
32
|
|
|
30
|
-
def test_docker_argv_core_tokens():
|
|
33
|
+
def test_docker_argv_core_tokens(tmp_path):
|
|
31
34
|
argv = docker.build_renode_argv("job.resc", "antmicro/renode:1.16.1")
|
|
32
35
|
assert argv[:3] == ["docker", "run", "-it"]
|
|
33
36
|
assert "--net=host" in argv
|
|
34
37
|
assert "antmicro/renode:1.16.1" in argv
|
|
35
38
|
# The image must be immediately followed by the renode invocation.
|
|
36
39
|
i = argv.index("antmicro/renode:1.16.1")
|
|
37
|
-
assert argv[i + 1:i + 3] == ["renode", "job.resc"]
|
|
40
|
+
assert argv[i + 1:i + 3] == ["renode", str(tmp_path / "job.resc")]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_workdir_is_cwd_without_config(tmp_path):
|
|
44
|
+
argv = docker.build_renode_argv("j.resc", "img")
|
|
45
|
+
assert argv[argv.index("-w") + 1] == str(tmp_path)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_workdir_is_config_dir_mount_unchanged(monkeypatch, tmp_path):
|
|
49
|
+
(tmp_path / "sub").mkdir()
|
|
50
|
+
monkeypatch.setattr(core, "CONFIG_DIR", str(tmp_path / "sub"))
|
|
51
|
+
argv = docker.build_renode_argv("j.resc", "img")
|
|
52
|
+
assert argv[argv.index("-v") + 1] == f"{tmp_path}:{tmp_path}"
|
|
53
|
+
assert argv[argv.index("-w") + 1] == str(tmp_path / "sub")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_user_docker_args_reach_every_container(monkeypatch):
|
|
57
|
+
monkeypatch.setattr(core, "DOCKER_ARGS", ["-v", "/opt/tools:/opt/tools", "-e", "FOO=1"])
|
|
58
|
+
calls = []
|
|
59
|
+
|
|
60
|
+
class _Proc:
|
|
61
|
+
returncode, stdout = 0, ""
|
|
62
|
+
|
|
63
|
+
monkeypatch.setattr(docker.subprocess, "run",
|
|
64
|
+
lambda cmd, **kw: calls.append(cmd) or _Proc())
|
|
65
|
+
monkeypatch.setattr(docker.os, "makedirs", lambda *a, **kw: None)
|
|
66
|
+
docker.run_build("true", "img")
|
|
67
|
+
docker.run_in_container("true", "img")
|
|
68
|
+
docker.run_renode_test("t.robot", "img", "res")
|
|
69
|
+
docker.run_renode("j.resc", "img")
|
|
70
|
+
for cmd in calls:
|
|
71
|
+
i = cmd.index("/opt/tools:/opt/tools")
|
|
72
|
+
assert cmd[i - 1] == "-v" and cmd[i + 1:i + 3] == ["-e", "FOO=1"]
|
|
73
|
+
assert i < cmd.index("img") # flags must precede the image
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_no_docker_steps_run_in_config_dir(monkeypatch, tmp_path):
|
|
77
|
+
monkeypatch.setattr(core, "NO_DOCKER", True)
|
|
78
|
+
monkeypatch.setattr(core, "CONFIG_DIR", str(tmp_path / "sub"))
|
|
79
|
+
cwds = []
|
|
80
|
+
|
|
81
|
+
class _Proc:
|
|
82
|
+
returncode, stdout = 0, ""
|
|
83
|
+
|
|
84
|
+
monkeypatch.setattr(docker.subprocess, "run",
|
|
85
|
+
lambda cmd, **kw: cwds.append(kw.get("cwd")) or _Proc())
|
|
86
|
+
docker.run_build("true", "img")
|
|
87
|
+
docker.run_in_container("true", "img")
|
|
88
|
+
docker.run_renode_test("t.robot", "img", "res")
|
|
89
|
+
docker.run_renode("j.resc", "img")
|
|
90
|
+
assert cwds == [str(tmp_path / "sub")] * 4
|
|
38
91
|
|
|
39
92
|
|
|
40
93
|
def test_docker_argv_does_not_publish_ports():
|
|
@@ -60,9 +113,9 @@ def test_every_container_uses_host_network(monkeypatch):
|
|
|
60
113
|
assert all("--net=host" in cmd for cmd in calls)
|
|
61
114
|
|
|
62
115
|
|
|
63
|
-
def test_docker_argv_appends_extra_args_after_resc():
|
|
116
|
+
def test_docker_argv_appends_extra_args_after_resc(tmp_path):
|
|
64
117
|
argv = docker.build_renode_argv("j.resc", "img", extra_args=["--console", "--hide-log"])
|
|
65
|
-
assert argv[-3:] == ["j.resc", "--console", "--hide-log"]
|
|
118
|
+
assert argv[-3:] == [str(tmp_path / "j.resc"), "--console", "--hide-log"]
|
|
66
119
|
|
|
67
120
|
|
|
68
121
|
# ---- _runtime_flags ----
|
|
@@ -6,7 +6,7 @@ import os
|
|
|
6
6
|
import pytest
|
|
7
7
|
|
|
8
8
|
from hector.generator import (NodeDescriptor, PeripheralDef, build_repl_fragment,
|
|
9
|
-
finalize_commands, generate_emulation_resc,
|
|
9
|
+
collect_artifacts, finalize_commands, generate_emulation_resc,
|
|
10
10
|
generate_node_repl, repl_value, snapshot_resc,
|
|
11
11
|
write_inline_repl)
|
|
12
12
|
|
|
@@ -194,3 +194,19 @@ def test_snapshot_resc_reopens_gdb_after_load():
|
|
|
194
194
|
# A GDB server is never part of saved state, so it must be (re)started post-Load.
|
|
195
195
|
assert text.index("Load @") < text.index("StartGdbServer 4444")
|
|
196
196
|
assert 'mach set "mcu"' in text
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# ---- collect_artifacts ----
|
|
200
|
+
|
|
201
|
+
def test_collect_artifacts_resolves_against_base_dir(tmp_path, monkeypatch):
|
|
202
|
+
# Config in board/: its 'artifacts:' globs resolve there, not against the cwd.
|
|
203
|
+
monkeypatch.chdir(tmp_path)
|
|
204
|
+
(tmp_path / "board" / "build").mkdir(parents=True)
|
|
205
|
+
(tmp_path / "board" / "build" / "fw.bin").write_text("x")
|
|
206
|
+
(tmp_path / "board" / "uart.log").write_text("x")
|
|
207
|
+
(tmp_path / "other.log").write_text("x")
|
|
208
|
+
collected = collect_artifacts(["build/*.bin", "*.log"], "results", 1,
|
|
209
|
+
base_dir=str(tmp_path / "board"))
|
|
210
|
+
assert sorted(collected) == [
|
|
211
|
+
os.path.join("results", "artifacts", "job_1", "board", "build", "fw.bin"),
|
|
212
|
+
os.path.join("results", "artifacts", "job_1", "board", "uart.log")]
|
|
@@ -36,6 +36,90 @@ def test_config_path_returns_requested_when_neither_exists(tmp_path, monkeypatch
|
|
|
36
36
|
assert resolve_config_path("missing.yaml") == "missing.yaml"
|
|
37
37
|
|
|
38
38
|
|
|
39
|
+
# ---- load_config_or_exit: the config's directory ----
|
|
40
|
+
|
|
41
|
+
@pytest.fixture
|
|
42
|
+
def config_in_subdir(tmp_path, monkeypatch):
|
|
43
|
+
from hector import core, pipeline
|
|
44
|
+
monkeypatch.chdir(tmp_path)
|
|
45
|
+
monkeypatch.setattr(core, "WORKSPACE_MOUNT", str(tmp_path))
|
|
46
|
+
monkeypatch.setattr(core, "NO_DOCKER", False)
|
|
47
|
+
monkeypatch.setattr(core, "CONFIG_DIR", None)
|
|
48
|
+
monkeypatch.setattr(core, "EXTRA_MOUNTS", [])
|
|
49
|
+
monkeypatch.setattr(core, "WORKSPACE_ROOT", None)
|
|
50
|
+
monkeypatch.setattr(pipeline, "MOUNT_CONFIG_DIR", False)
|
|
51
|
+
(tmp_path / "board").mkdir()
|
|
52
|
+
(tmp_path / "board" / ".hector.yaml").write_text("renode_version: '1.16.1'\n")
|
|
53
|
+
return pipeline, core, tmp_path
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_load_config_sets_config_dir(config_in_subdir, monkeypatch):
|
|
57
|
+
pipeline, core, root = config_in_subdir
|
|
58
|
+
monkeypatch.setattr(pipeline, "CONFIG_PATH", "board/.hector.yaml")
|
|
59
|
+
pipeline.load_config_or_exit()
|
|
60
|
+
assert core.CONFIG_DIR == str(root / "board")
|
|
61
|
+
assert core.execution_dir() == str(root / "board")
|
|
62
|
+
assert core.EXTRA_MOUNTS == [] # inside the cwd mount already
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_load_config_outside_cwd_is_mounted_too(config_in_subdir, monkeypatch):
|
|
66
|
+
# Run from a sibling dir: the cwd stays the workspace mount, and ../board is
|
|
67
|
+
# bind-mounted 1:1 so the config's paths and working dir are reachable.
|
|
68
|
+
pipeline, core, root = config_in_subdir
|
|
69
|
+
(root / "elsewhere").mkdir()
|
|
70
|
+
monkeypatch.setattr(core, "WORKSPACE_MOUNT", str(root / "elsewhere"))
|
|
71
|
+
monkeypatch.chdir(root / "elsewhere")
|
|
72
|
+
monkeypatch.setattr(pipeline, "CONFIG_PATH", "../board/.hector.yaml")
|
|
73
|
+
pipeline.load_config_or_exit()
|
|
74
|
+
board = str((root / "board").resolve())
|
|
75
|
+
assert core.EXTRA_MOUNTS == [board]
|
|
76
|
+
assert core.execution_dir() == board
|
|
77
|
+
assert core.to_container_path(core.from_config("fw.elf")) == f"{board}/fw.elf"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_workspace_mount_mounts_config_dir_there(config_in_subdir, monkeypatch):
|
|
81
|
+
# Run from the parent with --workspace-mount /workspace: the config's dir is what's
|
|
82
|
+
# mounted there and where execution happens; the cwd (.hector/, --output) is 1:1.
|
|
83
|
+
pipeline, core, root = config_in_subdir
|
|
84
|
+
monkeypatch.setattr(core, "WORKSPACE_MOUNT", "/workspace")
|
|
85
|
+
monkeypatch.setattr(pipeline, "MOUNT_CONFIG_DIR", True)
|
|
86
|
+
monkeypatch.setattr(pipeline, "CONFIG_PATH", "board/.hector.yaml")
|
|
87
|
+
pipeline.load_config_or_exit()
|
|
88
|
+
board, cwd = str((root / "board").resolve()), str(root.resolve())
|
|
89
|
+
assert core.workspace_root() == board
|
|
90
|
+
assert core.execution_dir() == "/workspace"
|
|
91
|
+
assert core.to_container_path(core.from_config("fw/app.elf")) == "/workspace/fw/app.elf"
|
|
92
|
+
assert core.EXTRA_MOUNTS == [cwd]
|
|
93
|
+
assert core.to_container_path(".hector/resc/job_1.resc") == f"{cwd}/.hector/resc/job_1.resc"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_workspace_mount_from_config_dir_needs_no_extra_mount(config_in_subdir, monkeypatch):
|
|
97
|
+
pipeline, core, root = config_in_subdir
|
|
98
|
+
monkeypatch.chdir(root / "board")
|
|
99
|
+
monkeypatch.setattr(core, "WORKSPACE_MOUNT", "/workspace")
|
|
100
|
+
monkeypatch.setattr(pipeline, "MOUNT_CONFIG_DIR", True)
|
|
101
|
+
monkeypatch.setattr(pipeline, "CONFIG_PATH", ".hector.yaml")
|
|
102
|
+
pipeline.load_config_or_exit()
|
|
103
|
+
assert core.execution_dir() == "/workspace"
|
|
104
|
+
assert core.EXTRA_MOUNTS == []
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# ---- apply_docker_args ----
|
|
108
|
+
|
|
109
|
+
@pytest.mark.parametrize("cfg,flag,expected", [
|
|
110
|
+
({}, "", []),
|
|
111
|
+
({"docker_args": "-v /a:/a -e X=1"}, "", ["-v", "/a:/a", "-e", "X=1"]),
|
|
112
|
+
({"docker_args": ["-v /a:/a", "--env 'Y=two words'"]}, "",
|
|
113
|
+
["-v", "/a:/a", "--env", "Y=two words"]),
|
|
114
|
+
({"docker_args": "-v /a:/a"}, "-v /b:/b", ["-v", "/a:/a", "-v", "/b:/b"]), # flag appends
|
|
115
|
+
])
|
|
116
|
+
def test_apply_docker_args(monkeypatch, cfg, flag, expected):
|
|
117
|
+
from hector import core, pipeline
|
|
118
|
+
monkeypatch.setattr(core, "DOCKER_ARGS", [])
|
|
119
|
+
pipeline.apply_docker_args(cfg, SimpleNamespace(docker_args=flag))
|
|
120
|
+
assert core.DOCKER_ARGS == expected
|
|
121
|
+
|
|
122
|
+
|
|
39
123
|
# ---- resolve_renode_image ----
|
|
40
124
|
|
|
41
125
|
def test_image_from_config():
|
|
@@ -39,6 +39,22 @@ def test_missing_renode_version_is_error():
|
|
|
39
39
|
assert any(i.path == "renode_version" for i in errors)
|
|
40
40
|
|
|
41
41
|
|
|
42
|
+
def test_renode_image_alone_satisfies_requirement():
|
|
43
|
+
errors, _ = validate_config({"renode_image": "my-renode:dev"})
|
|
44
|
+
assert errors == []
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@pytest.mark.parametrize("value", ["-v /a:/a", ["-v /a:/a", "-e X=1"]])
|
|
48
|
+
def test_docker_args_accepts_string_or_list(value):
|
|
49
|
+
errors, warnings = validate_config({"renode_version": "1.16.1", "docker_args": value})
|
|
50
|
+
assert errors == [] and not any(w.path == "docker_args" for w in warnings)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_docker_args_wrong_type_is_error():
|
|
54
|
+
errors, _ = validate_config({"renode_version": "1.16.1", "docker_args": {"v": "/a"}})
|
|
55
|
+
assert any(i.path == "docker_args" for i in errors)
|
|
56
|
+
|
|
57
|
+
|
|
42
58
|
def test_unknown_module_kind_is_error():
|
|
43
59
|
cfg = {"renode_version": "1.16.1",
|
|
44
60
|
"modules": {"u": {"kind": "bogus", "type": "X", "source": "s"}}}
|
|
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
|
|
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
|