hector-cli 0.4.0__tar.gz → 0.6.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.4.0 → hector_cli-0.6.0}/PKG-INFO +33 -1
- {hector_cli-0.4.0 → hector_cli-0.6.0}/README.md +32 -0
- {hector_cli-0.4.0 → hector_cli-0.6.0}/hector/commands/base.py +6 -0
- {hector_cli-0.4.0 → hector_cli-0.6.0}/hector/core.py +33 -2
- {hector_cli-0.4.0 → hector_cli-0.6.0}/hector/docker.py +42 -30
- {hector_cli-0.4.0 → hector_cli-0.6.0}/hector/generator.py +9 -7
- {hector_cli-0.4.0 → hector_cli-0.6.0}/hector/mappings.py +2 -2
- {hector_cli-0.4.0 → hector_cli-0.6.0}/hector/modules.py +3 -3
- {hector_cli-0.4.0 → hector_cli-0.6.0}/hector/pipeline.py +36 -8
- {hector_cli-0.4.0 → hector_cli-0.6.0}/hector/runners.py +3 -3
- {hector_cli-0.4.0 → hector_cli-0.6.0}/hector/validator.py +8 -1
- {hector_cli-0.4.0 → hector_cli-0.6.0}/hector_cli.egg-info/PKG-INFO +33 -1
- {hector_cli-0.4.0 → hector_cli-0.6.0}/tests/test_core_paths.py +27 -0
- hector_cli-0.6.0/tests/test_docker_argv.py +171 -0
- {hector_cli-0.4.0 → hector_cli-0.6.0}/tests/test_generator.py +17 -1
- {hector_cli-0.4.0 → hector_cli-0.6.0}/tests/test_pipeline_helpers.py +55 -0
- {hector_cli-0.4.0 → hector_cli-0.6.0}/tests/test_validator.py +16 -0
- hector_cli-0.4.0/tests/test_docker_argv.py +0 -101
- {hector_cli-0.4.0 → hector_cli-0.6.0}/LICENSE +0 -0
- {hector_cli-0.4.0 → hector_cli-0.6.0}/hector/__init__.py +0 -0
- {hector_cli-0.4.0 → hector_cli-0.6.0}/hector/__main__.py +0 -0
- {hector_cli-0.4.0 → hector_cli-0.6.0}/hector/commands/__init__.py +0 -0
- {hector_cli-0.4.0 → hector_cli-0.6.0}/hector/commands/clean.py +0 -0
- {hector_cli-0.4.0 → hector_cli-0.6.0}/hector/commands/export.py +0 -0
- {hector_cli-0.4.0 → hector_cli-0.6.0}/hector/commands/init.py +0 -0
- {hector_cli-0.4.0 → hector_cli-0.6.0}/hector/commands/run.py +0 -0
- {hector_cli-0.4.0 → hector_cli-0.6.0}/hector/commands/test.py +0 -0
- {hector_cli-0.4.0 → hector_cli-0.6.0}/hector/commands/validate.py +0 -0
- {hector_cli-0.4.0 → hector_cli-0.6.0}/hector/connections.py +0 -0
- {hector_cli-0.4.0 → hector_cli-0.6.0}/hector/dependencies.py +0 -0
- {hector_cli-0.4.0 → hector_cli-0.6.0}/hector/hubs.py +0 -0
- {hector_cli-0.4.0 → hector_cli-0.6.0}/hector/peripherals.py +0 -0
- {hector_cli-0.4.0 → hector_cli-0.6.0}/hector/reporters.py +0 -0
- {hector_cli-0.4.0 → hector_cli-0.6.0}/hector/scaffold.py +0 -0
- {hector_cli-0.4.0 → hector_cli-0.6.0}/hector_cli.egg-info/SOURCES.txt +0 -0
- {hector_cli-0.4.0 → hector_cli-0.6.0}/hector_cli.egg-info/dependency_links.txt +0 -0
- {hector_cli-0.4.0 → hector_cli-0.6.0}/hector_cli.egg-info/entry_points.txt +0 -0
- {hector_cli-0.4.0 → hector_cli-0.6.0}/hector_cli.egg-info/requires.txt +0 -0
- {hector_cli-0.4.0 → hector_cli-0.6.0}/hector_cli.egg-info/top_level.txt +0 -0
- {hector_cli-0.4.0 → hector_cli-0.6.0}/setup.cfg +0 -0
- {hector_cli-0.4.0 → hector_cli-0.6.0}/setup.py +0 -0
- {hector_cli-0.4.0 → hector_cli-0.6.0}/tests/test_arguments.py +0 -0
- {hector_cli-0.4.0 → hector_cli-0.6.0}/tests/test_clean.py +0 -0
- {hector_cli-0.4.0 → hector_cli-0.6.0}/tests/test_cli_smoke.py +0 -0
- {hector_cli-0.4.0 → hector_cli-0.6.0}/tests/test_connections.py +0 -0
- {hector_cli-0.4.0 → hector_cli-0.6.0}/tests/test_core_registry.py +0 -0
- {hector_cli-0.4.0 → hector_cli-0.6.0}/tests/test_core_text.py +0 -0
- {hector_cli-0.4.0 → hector_cli-0.6.0}/tests/test_hubs.py +0 -0
- {hector_cli-0.4.0 → hector_cli-0.6.0}/tests/test_mappings.py +0 -0
- {hector_cli-0.4.0 → hector_cli-0.6.0}/tests/test_matrix.py +0 -0
- {hector_cli-0.4.0 → hector_cli-0.6.0}/tests/test_needs_renode_source.py +0 -0
- {hector_cli-0.4.0 → hector_cli-0.6.0}/tests/test_peripherals.py +0 -0
- {hector_cli-0.4.0 → hector_cli-0.6.0}/tests/test_reporters.py +0 -0
- {hector_cli-0.4.0 → hector_cli-0.6.0}/tests/test_runners.py +0 -0
- {hector_cli-0.4.0 → hector_cli-0.6.0}/tests/test_scaffold.py +0 -0
- {hector_cli-0.4.0 → hector_cli-0.6.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.6.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,6 +206,7 @@ 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 |
|
|
@@ -482,6 +483,36 @@ 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. 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
|
+
|
|
512
|
+
#### Container networking
|
|
513
|
+
|
|
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.
|
|
515
|
+
|
|
485
516
|
#### Running without Docker (`--no-docker`)
|
|
486
517
|
|
|
487
518
|
Call the locally installed `renode` and `renode-test` binaries directly instead of launching a Docker container. `build:` steps and `shell` tests also run on the host (their `image:` is ignored, with a warning), as do module builds.
|
|
@@ -621,6 +652,7 @@ The configuration lives in `.hector.yaml`.
|
|
|
621
652
|
| `version` | No | Schema version (currently `"0.1"`). Warns if unrecognised. |
|
|
622
653
|
| `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. |
|
|
623
654
|
| `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). |
|
|
655
|
+
| `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). |
|
|
624
656
|
| `arguments` | No | Scalar defaults, overridable by env var or `--set`. |
|
|
625
657
|
| `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. |
|
|
626
658
|
| `matrix` | No | Cross-product job expansion. |
|
|
@@ -176,6 +176,7 @@ 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 |
|
|
@@ -452,6 +453,36 @@ 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. 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
|
+
|
|
482
|
+
#### Container networking
|
|
483
|
+
|
|
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.
|
|
485
|
+
|
|
455
486
|
#### Running without Docker (`--no-docker`)
|
|
456
487
|
|
|
457
488
|
Call the locally installed `renode` and `renode-test` binaries directly instead of launching a Docker container. `build:` steps and `shell` tests also run on the host (their `image:` is ignored, with a warning), as do module builds.
|
|
@@ -591,6 +622,7 @@ The configuration lives in `.hector.yaml`.
|
|
|
591
622
|
| `version` | No | Schema version (currently `"0.1"`). Warns if unrecognised. |
|
|
592
623
|
| `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. |
|
|
593
624
|
| `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). |
|
|
625
|
+
| `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). |
|
|
594
626
|
| `arguments` | No | Scalar defaults, overridable by env var or `--set`. |
|
|
595
627
|
| `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. |
|
|
596
628
|
| `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 "
|
|
@@ -67,7 +67,7 @@ class Registry:
|
|
|
67
67
|
# ==========================================================================
|
|
68
68
|
# CONSTANTS
|
|
69
69
|
# ==========================================================================
|
|
70
|
-
TOOL_VERSION = "0.
|
|
70
|
+
TOOL_VERSION = "0.6.0"
|
|
71
71
|
SUPPORTED_SCHEMA_VERSIONS = {"0.1"}
|
|
72
72
|
|
|
73
73
|
RENODE_REPO_URL = "https://github.com/renode/renode.git"
|
|
@@ -80,6 +80,11 @@ ARTIFACTS_DIR = ".hector" # everything we generate/clone lives here (gi
|
|
|
80
80
|
WORKSPACE_MOUNT = "/workspace" if os.name == "nt" else os.getcwd()
|
|
81
81
|
NO_DOCKER = False # when True, call renode/renode-test directly on host
|
|
82
82
|
EXTRA_MOUNTS = [] # dep dirs outside the workspace, bind-mounted 1:1 into every container
|
|
83
|
+
DOCKER_ARGS = [] # user flags (docker_args: + --docker-args) added to every `docker run`
|
|
84
|
+
# Host directory of the config file being executed. Paths written in the config resolve
|
|
85
|
+
# against it, and scripts / Renode run with it as their working directory. None = cwd
|
|
86
|
+
# (no config loaded, e.g. `run --snapshot` without one).
|
|
87
|
+
CONFIG_DIR = None
|
|
83
88
|
LIBOPENLIBM_NAME = "libopenlibm-Linux-x86_64.a"
|
|
84
89
|
|
|
85
90
|
DEFAULT_QUANTUM = "0.00001" # 10 us; applied when any hub is present
|
|
@@ -123,6 +128,11 @@ def to_container_path(host_path):
|
|
|
123
128
|
abs_path = os.path.realpath(host_path)
|
|
124
129
|
root = os.path.realpath(os.getcwd())
|
|
125
130
|
if os.path.commonpath([abs_path, root]) != root:
|
|
131
|
+
# Out-of-tree dirs registered in EXTRA_MOUNTS (the config's dir, a shared
|
|
132
|
+
# Renode cache) are mounted 1:1, so their paths are valid as-is.
|
|
133
|
+
for m in EXTRA_MOUNTS:
|
|
134
|
+
if os.path.commonpath([abs_path, m]) == m:
|
|
135
|
+
return abs_path
|
|
126
136
|
raise ValueError(
|
|
127
137
|
f"Path '{host_path}' resolves outside the project root '{root}'. Everything used "
|
|
128
138
|
f"at runtime must live under the project folder (mounted at '{WORKSPACE_MOUNT}')."
|
|
@@ -130,7 +140,7 @@ def to_container_path(host_path):
|
|
|
130
140
|
# The container is always Linux: build a POSIX path under WORKSPACE_MOUNT (this is a
|
|
131
141
|
# no-op on Linux, where WORKSPACE_MOUNT is the cwd and os.sep is already '/').
|
|
132
142
|
rel = os.path.relpath(abs_path, root).replace(os.sep, "/")
|
|
133
|
-
return posixpath.join(WORKSPACE_MOUNT, rel)
|
|
143
|
+
return WORKSPACE_MOUNT if rel == "." else posixpath.join(WORKSPACE_MOUNT, rel)
|
|
134
144
|
|
|
135
145
|
|
|
136
146
|
def register_dependency_mount(host_dir):
|
|
@@ -173,6 +183,27 @@ def container_to_host(container_path):
|
|
|
173
183
|
return host
|
|
174
184
|
|
|
175
185
|
|
|
186
|
+
def config_dir():
|
|
187
|
+
"""Host directory config-relative paths resolve against (see CONFIG_DIR)."""
|
|
188
|
+
return CONFIG_DIR or os.getcwd()
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def from_config(path):
|
|
192
|
+
"""Resolve a path written in the config against the config file's directory.
|
|
193
|
+
Absolute paths (host or container, e.g. an interpolated ${RENODE_DIR}) and URLs
|
|
194
|
+
are returned unchanged."""
|
|
195
|
+
if not path or os.path.isabs(path) or path.startswith("/") or "://" in path:
|
|
196
|
+
return path
|
|
197
|
+
return os.path.normpath(os.path.join(config_dir(), path))
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def execution_dir():
|
|
201
|
+
"""Working directory for everything hector executes (build/shell steps, Renode,
|
|
202
|
+
renode-test): the config's directory — as a container path under Docker, a host
|
|
203
|
+
path under --no-docker."""
|
|
204
|
+
return config_dir() if NO_DOCKER else to_container_path(config_dir())
|
|
205
|
+
|
|
206
|
+
|
|
176
207
|
def split_host_container(path):
|
|
177
208
|
if path == WORKSPACE_MOUNT or path.startswith(WORKSPACE_MOUNT + "/"):
|
|
178
209
|
return container_to_host(path), path
|
|
@@ -48,16 +48,24 @@ def _user_flags():
|
|
|
48
48
|
|
|
49
49
|
|
|
50
50
|
def _workspace_mount():
|
|
51
|
-
return ["-v", f"{os.getcwd()}:{core.WORKSPACE_MOUNT}", "-w", core.
|
|
51
|
+
return ["-v", f"{os.getcwd()}:{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
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# Every container shares the host's network stack: GDB servers, tcp: mappings and
|
|
65
|
+
# anything a build/test step listens on are reachable from the host as-is, and steps
|
|
66
|
+
# can reach host services (e.g. a local registry or a TCP-mapped UART) on localhost.
|
|
67
|
+
# No -p publishing is needed (Docker ignores it in this mode anyway).
|
|
68
|
+
_HOST_NETWORK = ["--net=host"]
|
|
61
69
|
|
|
62
70
|
|
|
63
71
|
def _runtime_flags(runtime):
|
|
@@ -86,10 +94,10 @@ def _runtime_flags(runtime):
|
|
|
86
94
|
def run_build(script, image):
|
|
87
95
|
"""Run a build script in the container as root (for apt-get + cmake)."""
|
|
88
96
|
if core.NO_DOCKER:
|
|
89
|
-
subprocess.run(["bash", "-c", script], check=True)
|
|
97
|
+
subprocess.run(["bash", "-c", script], check=True, cwd=core.execution_dir())
|
|
90
98
|
return
|
|
91
99
|
subprocess.run(
|
|
92
|
-
["docker", "run", "--rm", "-u", "0:0",
|
|
100
|
+
["docker", "run", "--rm", *_HOST_NETWORK, "-u", "0:0",
|
|
93
101
|
*_workspace_mount(), *_extra_mounts(), image, "bash", "-c", script],
|
|
94
102
|
check=True,
|
|
95
103
|
)
|
|
@@ -124,14 +132,17 @@ def run_in_container(script, image, live=False):
|
|
|
124
132
|
fail on the first error.
|
|
125
133
|
|
|
126
134
|
Backs the `shell` test type and the `build:` steps. Normally a one-off container of
|
|
127
|
-
`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
|
|
128
138
|
if core.NO_DOCKER:
|
|
129
139
|
cmd = ["bash", "-c", f"set -ex\n{script}"]
|
|
140
|
+
cwd = core.execution_dir()
|
|
130
141
|
else:
|
|
131
|
-
cmd = ["docker", "run", "--rm", *
|
|
132
|
-
*_user_flags(), image, "sh", "-c", f"set -ex\n{script}"]
|
|
142
|
+
cmd = ["docker", "run", "--rm", *_HOST_NETWORK, *_workspace_mount(),
|
|
143
|
+
*_extra_mounts(), *_user_flags(), image, "sh", "-c", f"set -ex\n{script}"]
|
|
133
144
|
if live:
|
|
134
|
-
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
|
|
145
|
+
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, cwd=cwd,
|
|
135
146
|
stderr=subprocess.STDOUT, text=True)
|
|
136
147
|
lines = []
|
|
137
148
|
for line in proc.stdout:
|
|
@@ -139,35 +150,36 @@ def run_in_container(script, image, live=False):
|
|
|
139
150
|
lines.append(line)
|
|
140
151
|
proc.wait()
|
|
141
152
|
return "".join(lines), proc.returncode == 0
|
|
142
|
-
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)
|
|
143
155
|
if proc.stdout:
|
|
144
156
|
print(proc.stdout, end="")
|
|
145
157
|
return proc.stdout, proc.returncode == 0
|
|
146
158
|
|
|
147
159
|
|
|
148
|
-
def build_renode_argv(resc_filename, image,
|
|
160
|
+
def build_renode_argv(resc_filename, image, extra_args=None, runtime=None):
|
|
149
161
|
"""Build the exact argv that would run an emulation resc — the local `renode`
|
|
150
162
|
invocation under --no-docker, otherwise the full `docker run ... renode` command.
|
|
151
163
|
|
|
152
164
|
Returning the command (instead of running it) is what lets the `export` command
|
|
153
165
|
print it verbatim and keeps run_renode a one-liner around this builder.
|
|
154
|
-
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."""
|
|
155
168
|
if core.NO_DOCKER:
|
|
156
|
-
return ["renode", resc_filename] + (extra_args or [])
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
cmd += ["-p", f"{port}:{port}"]
|
|
161
|
-
cmd += [image, "renode", resc_filename] + (extra_args or [])
|
|
162
|
-
return cmd
|
|
169
|
+
return ["renode", os.path.abspath(resc_filename)] + (extra_args or [])
|
|
170
|
+
return ["docker", "run", "-it", "--rm", *_xdisplay_args(), *_HOST_NETWORK,
|
|
171
|
+
*_workspace_mount(), *_extra_mounts(), *_user_flags(), *_runtime_flags(runtime),
|
|
172
|
+
image, "renode", to_container_path(resc_filename)] + (extra_args or [])
|
|
163
173
|
|
|
164
174
|
|
|
165
|
-
def run_renode(resc_filename, image,
|
|
166
|
-
"""Run an emulation resc interactively.
|
|
175
|
+
def run_renode(resc_filename, image, extra_args=None, output_dir="results", runtime=None):
|
|
176
|
+
"""Run an emulation resc interactively. GDB ports (--debug) are reachable from the
|
|
177
|
+
host through the host network.
|
|
167
178
|
runtime: aggregated RuntimeOptions whose docker flags (caps/devices/root) are added."""
|
|
168
179
|
if not core.NO_DOCKER:
|
|
169
180
|
os.makedirs(os.path.join(os.getcwd(), output_dir), exist_ok=True)
|
|
170
|
-
subprocess.run(build_renode_argv(resc_filename, image,
|
|
181
|
+
subprocess.run(build_renode_argv(resc_filename, image, extra_args, runtime),
|
|
182
|
+
cwd=core.execution_dir() if core.NO_DOCKER else None)
|
|
171
183
|
|
|
172
184
|
|
|
173
185
|
def run_renode_test(robot_file_host, image, results_dir_host,
|
|
@@ -177,22 +189,22 @@ def run_renode_test(robot_file_host, image, results_dir_host,
|
|
|
177
189
|
resc_host_path: passed as --variable RESC:<path> so .robot files can load
|
|
178
190
|
the generated emulation script via ${RESC}.
|
|
179
191
|
extra_args: optional list of additional flags forwarded verbatim to renode-test.
|
|
180
|
-
interactive: attach stdin/tty and
|
|
181
|
-
or when TCP ports need to be reachable from the host during the test).
|
|
192
|
+
interactive: attach stdin/tty and X display (useful with Pause Execution).
|
|
182
193
|
"""
|
|
183
194
|
os.makedirs(results_dir_host, exist_ok=True)
|
|
184
195
|
if core.NO_DOCKER:
|
|
185
|
-
|
|
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)]
|
|
186
199
|
if resc_host_path:
|
|
187
|
-
cmd += ["--variable", f"RESC:{resc_host_path}"]
|
|
200
|
+
cmd += ["--variable", f"RESC:{os.path.abspath(resc_host_path)}"]
|
|
188
201
|
if extra_args:
|
|
189
202
|
cmd += extra_args
|
|
190
|
-
result = subprocess.run(cmd)
|
|
203
|
+
result = subprocess.run(cmd, cwd=core.execution_dir())
|
|
191
204
|
return result.returncode == 0
|
|
192
205
|
flags = ["-it"] if interactive else []
|
|
193
|
-
net = ["--net=host"] if interactive else []
|
|
194
206
|
display = _xdisplay_args() if interactive else []
|
|
195
|
-
cmd = ["docker", "run", "--rm", *flags, *
|
|
207
|
+
cmd = ["docker", "run", "--rm", *flags, *_HOST_NETWORK, *display,
|
|
196
208
|
*_workspace_mount(), *_extra_mounts(), *_user_flags(), *_runtime_flags(runtime),
|
|
197
209
|
image, "renode-test", to_container_path(robot_file_host),
|
|
198
210
|
"-r", to_container_path(results_dir_host)]
|
|
@@ -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"):
|
|
@@ -104,9 +104,18 @@ def load_config_or_exit():
|
|
|
104
104
|
if v is not None and str(v) not in SUPPORTED_SCHEMA_VERSIONS:
|
|
105
105
|
print(f"[WARN] .hector.yaml 'version: {v}' is not in supported versions "
|
|
106
106
|
f"({', '.join(sorted(SUPPORTED_SCHEMA_VERSIONS))}). Proceeding anyway.")
|
|
107
|
+
_set_config_dir(config_path)
|
|
107
108
|
return config
|
|
108
109
|
|
|
109
110
|
|
|
111
|
+
def _set_config_dir(config_path):
|
|
112
|
+
"""Make the config's directory the base for its paths and the working directory of
|
|
113
|
+
everything executed (see core.CONFIG_DIR). The cwd stays the workspace mount; a
|
|
114
|
+
config outside it gets its directory bind-mounted 1:1 as well, so it's reachable."""
|
|
115
|
+
_core.CONFIG_DIR = os.path.realpath(os.path.dirname(os.path.abspath(config_path)))
|
|
116
|
+
_core.register_dependency_mount(_core.CONFIG_DIR)
|
|
117
|
+
|
|
118
|
+
|
|
110
119
|
_VERSION_TAG_RE = re.compile(r"^v?\d+\.\d+\.\d+$")
|
|
111
120
|
|
|
112
121
|
|
|
@@ -138,6 +147,16 @@ def resolve_renode_image(config, args):
|
|
|
138
147
|
return (f"v{normalized}" if normalized else None), image
|
|
139
148
|
|
|
140
149
|
|
|
150
|
+
def apply_docker_args(config, args):
|
|
151
|
+
"""Collect the user's extra `docker run` flags — the config's `docker_args:` (a
|
|
152
|
+
string or a list of strings) followed by --docker-args — into core.DOCKER_ARGS."""
|
|
153
|
+
raw = config.get("docker_args") or []
|
|
154
|
+
flags = []
|
|
155
|
+
for item in ([raw] if isinstance(raw, str) else raw):
|
|
156
|
+
flags += shlex.split(str(item))
|
|
157
|
+
_core.DOCKER_ARGS = flags + shlex.split(getattr(args, "docker_args", "") or "")
|
|
158
|
+
|
|
159
|
+
|
|
141
160
|
def _resolve_set_overrides(args):
|
|
142
161
|
overrides = {}
|
|
143
162
|
for kv in (getattr(args, "set_args", None) or []):
|
|
@@ -166,6 +185,7 @@ def build_job_config(action, config, args, *, test_mode=False):
|
|
|
166
185
|
"""Assemble a JobConfig from parsed args — the shared run / test / export path:
|
|
167
186
|
pin the image, prepare the framework (lazily), resolve arguments + artifacts."""
|
|
168
187
|
git_tag, docker_image = resolve_renode_image(config, args)
|
|
188
|
+
apply_docker_args(config, args)
|
|
169
189
|
|
|
170
190
|
# Clone Renode only when the config actually needs the source; otherwise skip it
|
|
171
191
|
# entirely (a ~200 MB clone) and leave the framework vars empty.
|
|
@@ -186,8 +206,14 @@ def build_job_config(action, config, args, *, test_mode=False):
|
|
|
186
206
|
|
|
187
207
|
resolved_args = resolve_arguments(config.get("arguments", {}),
|
|
188
208
|
overrides=_resolve_set_overrides(args))
|
|
189
|
-
|
|
190
|
-
|
|
209
|
+
# --artifacts globs are relative to where hector runs; the config's own 'artifacts:'
|
|
210
|
+
# to the config's directory (like every other path written in the config).
|
|
211
|
+
if getattr(args, "artifacts", None):
|
|
212
|
+
artifacts_cfg, artifacts_base = args.artifacts, os.getcwd()
|
|
213
|
+
else:
|
|
214
|
+
artifacts_cfg = as_lines(config.get("artifacts", []))
|
|
215
|
+
artifacts_base = _core.config_dir()
|
|
216
|
+
test_file = getattr(args, "test_file", None)
|
|
191
217
|
return JobConfig(
|
|
192
218
|
config=config,
|
|
193
219
|
fw_vars=fw_vars,
|
|
@@ -196,7 +222,8 @@ def build_job_config(action, config, args, *, test_mode=False):
|
|
|
196
222
|
action=action,
|
|
197
223
|
test_mode=test_mode,
|
|
198
224
|
debug_args=getattr(args, "debug", None) or [],
|
|
199
|
-
|
|
225
|
+
# Absolute so it isn't re-resolved against the config's directory.
|
|
226
|
+
test_file=os.path.abspath(test_file) if test_file else None,
|
|
200
227
|
reporter_names=_resolve_reporters(getattr(args, "reporters", None)),
|
|
201
228
|
renode_args=shlex.split(getattr(args, "renode_args", "") or ""),
|
|
202
229
|
renode_test_args=shlex.split(getattr(args, "renode_test_args", "") or ""),
|
|
@@ -208,6 +235,7 @@ def build_job_config(action, config, args, *, test_mode=False):
|
|
|
208
235
|
output_dir=getattr(args, "output", "results"),
|
|
209
236
|
gather_metrics=getattr(args, "gather_execution_metrics", None),
|
|
210
237
|
artifacts=artifacts_cfg,
|
|
238
|
+
artifacts_base=artifacts_base,
|
|
211
239
|
)
|
|
212
240
|
|
|
213
241
|
|
|
@@ -241,6 +269,7 @@ def snapshot_fastpath(args):
|
|
|
241
269
|
print(f"[ERROR] Snapshot not found: {args.snapshot}", file=sys.stderr)
|
|
242
270
|
sys.exit(1)
|
|
243
271
|
_, docker_image = resolve_renode_image({}, args)
|
|
272
|
+
apply_docker_args({}, args)
|
|
244
273
|
resc_root = os.path.join(ARTIFACTS_DIR, "resc")
|
|
245
274
|
os.makedirs(resc_root, exist_ok=True)
|
|
246
275
|
# No .hector.yaml here, so debug machine names come straight from --debug.
|
|
@@ -251,8 +280,7 @@ def snapshot_fastpath(args):
|
|
|
251
280
|
no_start=getattr(args, "no_start", False)))
|
|
252
281
|
print(f"[SNAP] {args.snapshot}"
|
|
253
282
|
+ (f" (+GDB: {debug_config})" if debug_config else ""))
|
|
254
|
-
run_renode(snap_resc, docker_image,
|
|
255
|
-
extra_args=shlex.split(args.renode_args or ""), output_dir=args.output)
|
|
283
|
+
run_renode(snap_resc, docker_image, extra_args=shlex.split(args.renode_args or ""), output_dir=args.output)
|
|
256
284
|
|
|
257
285
|
|
|
258
286
|
def _run_validate():
|
|
@@ -486,6 +514,7 @@ class JobConfig:
|
|
|
486
514
|
output_dir: str = "results"
|
|
487
515
|
gather_metrics: list = None
|
|
488
516
|
artifacts: list = field(default_factory=list)
|
|
517
|
+
artifacts_base: str = "." # directory the artifact globs resolve against
|
|
489
518
|
|
|
490
519
|
|
|
491
520
|
@dataclass
|
|
@@ -524,7 +553,6 @@ def action_simulate(state, cfg, result):
|
|
|
524
553
|
if not state.resc_filename:
|
|
525
554
|
raise ValueError("`hector run` needs at least one machine in 'machines:'.")
|
|
526
555
|
run_renode(_resolve_active_resc(state, cfg), cfg.docker_image,
|
|
527
|
-
ports=list(state.debug_config.values()) or None,
|
|
528
556
|
extra_args=cfg.renode_args, output_dir=cfg.output_dir,
|
|
529
557
|
runtime=state.runtime)
|
|
530
558
|
|
|
@@ -536,7 +564,6 @@ def action_export(state, cfg, result):
|
|
|
536
564
|
if not state.resc_filename:
|
|
537
565
|
raise ValueError("`hector export` needs at least one machine in 'machines:'.")
|
|
538
566
|
argv = build_renode_argv(_resolve_active_resc(state, cfg), cfg.docker_image,
|
|
539
|
-
ports=list(state.debug_config.values()) or None,
|
|
540
567
|
extra_args=cfg.renode_args, runtime=state.runtime)
|
|
541
568
|
line = shlex.join(argv)
|
|
542
569
|
print(line)
|
|
@@ -760,7 +787,8 @@ def run_job(i, total_jobs, job_context, cfg):
|
|
|
760
787
|
# Global glob patterns (top-level 'artifacts:' or --artifacts), interpolated
|
|
761
788
|
# with this job's context so matrix variables resolve.
|
|
762
789
|
artifact_patterns = as_lines(interpolate(list(cfg.artifacts), context))
|
|
763
|
-
collect_artifacts(artifact_patterns, cfg.output_dir, i + 1
|
|
790
|
+
collect_artifacts(artifact_patterns, cfg.output_dir, i + 1,
|
|
791
|
+
base_dir=cfg.artifacts_base)
|
|
764
792
|
|
|
765
793
|
except Exception as exc:
|
|
766
794
|
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.6.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,6 +206,7 @@ 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 |
|
|
@@ -482,6 +483,36 @@ 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. 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
|
+
|
|
512
|
+
#### Container networking
|
|
513
|
+
|
|
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.
|
|
515
|
+
|
|
485
516
|
#### Running without Docker (`--no-docker`)
|
|
486
517
|
|
|
487
518
|
Call the locally installed `renode` and `renode-test` binaries directly instead of launching a Docker container. `build:` steps and `shell` tests also run on the host (their `image:` is ignored, with a warning), as do module builds.
|
|
@@ -621,6 +652,7 @@ The configuration lives in `.hector.yaml`.
|
|
|
621
652
|
| `version` | No | Schema version (currently `"0.1"`). Warns if unrecognised. |
|
|
622
653
|
| `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. |
|
|
623
654
|
| `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). |
|
|
655
|
+
| `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). |
|
|
624
656
|
| `arguments` | No | Scalar defaults, overridable by env var or `--set`. |
|
|
625
657
|
| `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. |
|
|
626
658
|
| `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"):
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Unit tests for hector.docker command construction. We assert on the argv that
|
|
2
|
+
*would* run — never invoking Docker or Renode. build_renode_argv is the seam the
|
|
3
|
+
`export` command prints verbatim."""
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from hector import core, docker
|
|
8
|
+
from hector.core import RuntimeOptions
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.fixture(autouse=True)
|
|
12
|
+
def clean_env(monkeypatch, tmp_path):
|
|
13
|
+
monkeypatch.chdir(tmp_path)
|
|
14
|
+
monkeypatch.setattr(core, "WORKSPACE_MOUNT", str(tmp_path))
|
|
15
|
+
monkeypatch.setattr(core, "EXTRA_MOUNTS", [])
|
|
16
|
+
monkeypatch.setattr(core, "NO_DOCKER", False)
|
|
17
|
+
monkeypatch.setattr(core, "CONFIG_DIR", None)
|
|
18
|
+
monkeypatch.setattr(core, "DOCKER_ARGS", [])
|
|
19
|
+
# Neutralise X11 forwarding so argv is deterministic across hosts.
|
|
20
|
+
monkeypatch.delenv("XAUTHORITY", raising=False)
|
|
21
|
+
monkeypatch.delenv("HOME", raising=False)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# ---- build_renode_argv ----
|
|
25
|
+
|
|
26
|
+
def test_no_docker_is_bare_renode(monkeypatch, tmp_path):
|
|
27
|
+
monkeypatch.setattr(core, "NO_DOCKER", True)
|
|
28
|
+
# Absolute: renode runs in the config's directory, not the cwd.
|
|
29
|
+
assert docker.build_renode_argv("job.resc", "img", extra_args=["--console"]) == [
|
|
30
|
+
"renode", str(tmp_path / "job.resc"), "--console"]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_docker_argv_core_tokens(tmp_path):
|
|
34
|
+
argv = docker.build_renode_argv("job.resc", "antmicro/renode:1.16.1")
|
|
35
|
+
assert argv[:3] == ["docker", "run", "-it"]
|
|
36
|
+
assert "--net=host" in argv
|
|
37
|
+
assert "antmicro/renode:1.16.1" in argv
|
|
38
|
+
# The image must be immediately followed by the renode invocation.
|
|
39
|
+
i = argv.index("antmicro/renode:1.16.1")
|
|
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
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_docker_argv_does_not_publish_ports():
|
|
94
|
+
# Host networking exposes GDB ports directly; -p would be ignored by Docker.
|
|
95
|
+
argv = docker.build_renode_argv("j.resc", "img")
|
|
96
|
+
assert "-p" not in argv
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_every_container_uses_host_network(monkeypatch):
|
|
100
|
+
calls = []
|
|
101
|
+
|
|
102
|
+
class _Proc:
|
|
103
|
+
returncode, stdout = 0, ""
|
|
104
|
+
|
|
105
|
+
monkeypatch.setattr(docker.subprocess, "run",
|
|
106
|
+
lambda cmd, **kw: calls.append(cmd) or _Proc())
|
|
107
|
+
monkeypatch.setattr(docker.os, "makedirs", lambda *a, **kw: None)
|
|
108
|
+
docker.run_build("true", "img")
|
|
109
|
+
docker.run_in_container("true", "img")
|
|
110
|
+
docker.run_renode_test("t.robot", "img", "res")
|
|
111
|
+
docker.run_renode("j.resc", "img")
|
|
112
|
+
assert len(calls) == 4
|
|
113
|
+
assert all("--net=host" in cmd for cmd in calls)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_docker_argv_appends_extra_args_after_resc(tmp_path):
|
|
117
|
+
argv = docker.build_renode_argv("j.resc", "img", extra_args=["--console", "--hide-log"])
|
|
118
|
+
assert argv[-3:] == [str(tmp_path / "j.resc"), "--console", "--hide-log"]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# ---- _runtime_flags ----
|
|
122
|
+
|
|
123
|
+
def test_runtime_flags_none():
|
|
124
|
+
assert docker._runtime_flags(None) == []
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_runtime_flags_root_and_caps():
|
|
128
|
+
flags = docker._runtime_flags(RuntimeOptions(root=True, capabilities={"NET_ADMIN"}))
|
|
129
|
+
assert "-u" in flags and "0:0" in flags
|
|
130
|
+
assert flags[flags.index("--cap-add") + 1] == "NET_ADMIN"
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def test_runtime_flags_device_present(monkeypatch):
|
|
134
|
+
monkeypatch.setattr(docker.os.path, "exists", lambda p: p == "/dev/net/tun")
|
|
135
|
+
flags = docker._runtime_flags(RuntimeOptions(devices={"/dev/net/tun"}))
|
|
136
|
+
assert flags[flags.index("--device") + 1] == "/dev/net/tun"
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_runtime_flags_all_devices_absent_yields_nothing(monkeypatch):
|
|
140
|
+
# A requested device missing on the host must not fail the run, and (per the
|
|
141
|
+
# docstring) no flags at all are applied so it drops to unprivileged dummy mode.
|
|
142
|
+
monkeypatch.setattr(docker.os.path, "exists", lambda p: False)
|
|
143
|
+
assert docker._runtime_flags(
|
|
144
|
+
RuntimeOptions(devices={"/dev/net/tun"}, root=True)) == []
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# ---- _user_flags ----
|
|
148
|
+
|
|
149
|
+
def test_user_flags_maps_host_uid(monkeypatch):
|
|
150
|
+
monkeypatch.setattr(core, "host_uid_gid", lambda: (1000, 1000))
|
|
151
|
+
assert docker._user_flags() == ["-u", "1000:1000"]
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def test_user_flags_empty_without_uid(monkeypatch):
|
|
155
|
+
monkeypatch.setattr(core, "host_uid_gid", lambda: None)
|
|
156
|
+
assert docker._user_flags() == []
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# ---- resolve_shell_image ----
|
|
160
|
+
|
|
161
|
+
def test_shell_image_explicit_wins():
|
|
162
|
+
assert docker.resolve_shell_image({"image": "alpine"}, "default:img") == "alpine"
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def test_shell_image_falls_back_to_default():
|
|
166
|
+
assert docker.resolve_shell_image({}, "default:img") == "default:img"
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def test_shell_image_no_docker_uses_default(monkeypatch):
|
|
170
|
+
monkeypatch.setattr(core, "NO_DOCKER", True)
|
|
171
|
+
assert docker.resolve_shell_image({"image": "alpine"}, "default:img") == "alpine"
|
|
@@ -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,61 @@ 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
|
+
(tmp_path / "board").mkdir()
|
|
50
|
+
(tmp_path / "board" / ".hector.yaml").write_text("renode_version: '1.16.1'\n")
|
|
51
|
+
return pipeline, core, tmp_path
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_load_config_sets_config_dir(config_in_subdir, monkeypatch):
|
|
55
|
+
pipeline, core, root = config_in_subdir
|
|
56
|
+
monkeypatch.setattr(pipeline, "CONFIG_PATH", "board/.hector.yaml")
|
|
57
|
+
pipeline.load_config_or_exit()
|
|
58
|
+
assert core.CONFIG_DIR == str(root / "board")
|
|
59
|
+
assert core.execution_dir() == str(root / "board")
|
|
60
|
+
assert core.EXTRA_MOUNTS == [] # inside the cwd mount already
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_load_config_outside_cwd_is_mounted_too(config_in_subdir, monkeypatch):
|
|
64
|
+
# Run from a sibling dir: the cwd stays the workspace mount, and ../board is
|
|
65
|
+
# bind-mounted 1:1 so the config's paths and working dir are reachable.
|
|
66
|
+
pipeline, core, root = config_in_subdir
|
|
67
|
+
(root / "elsewhere").mkdir()
|
|
68
|
+
monkeypatch.setattr(core, "WORKSPACE_MOUNT", str(root / "elsewhere"))
|
|
69
|
+
monkeypatch.chdir(root / "elsewhere")
|
|
70
|
+
monkeypatch.setattr(pipeline, "CONFIG_PATH", "../board/.hector.yaml")
|
|
71
|
+
pipeline.load_config_or_exit()
|
|
72
|
+
board = str((root / "board").resolve())
|
|
73
|
+
assert core.EXTRA_MOUNTS == [board]
|
|
74
|
+
assert core.execution_dir() == board
|
|
75
|
+
assert core.to_container_path(core.from_config("fw.elf")) == f"{board}/fw.elf"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# ---- apply_docker_args ----
|
|
79
|
+
|
|
80
|
+
@pytest.mark.parametrize("cfg,flag,expected", [
|
|
81
|
+
({}, "", []),
|
|
82
|
+
({"docker_args": "-v /a:/a -e X=1"}, "", ["-v", "/a:/a", "-e", "X=1"]),
|
|
83
|
+
({"docker_args": ["-v /a:/a", "--env 'Y=two words'"]}, "",
|
|
84
|
+
["-v", "/a:/a", "--env", "Y=two words"]),
|
|
85
|
+
({"docker_args": "-v /a:/a"}, "-v /b:/b", ["-v", "/a:/a", "-v", "/b:/b"]), # flag appends
|
|
86
|
+
])
|
|
87
|
+
def test_apply_docker_args(monkeypatch, cfg, flag, expected):
|
|
88
|
+
from hector import core, pipeline
|
|
89
|
+
monkeypatch.setattr(core, "DOCKER_ARGS", [])
|
|
90
|
+
pipeline.apply_docker_args(cfg, SimpleNamespace(docker_args=flag))
|
|
91
|
+
assert core.DOCKER_ARGS == expected
|
|
92
|
+
|
|
93
|
+
|
|
39
94
|
# ---- resolve_renode_image ----
|
|
40
95
|
|
|
41
96
|
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"}}}
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
"""Unit tests for hector.docker command construction. We assert on the argv that
|
|
2
|
-
*would* run — never invoking Docker or Renode. build_renode_argv is the seam the
|
|
3
|
-
`export` command prints verbatim."""
|
|
4
|
-
|
|
5
|
-
import pytest
|
|
6
|
-
|
|
7
|
-
from hector import core, docker
|
|
8
|
-
from hector.core import RuntimeOptions
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
@pytest.fixture(autouse=True)
|
|
12
|
-
def clean_env(monkeypatch, tmp_path):
|
|
13
|
-
monkeypatch.chdir(tmp_path)
|
|
14
|
-
monkeypatch.setattr(core, "WORKSPACE_MOUNT", str(tmp_path))
|
|
15
|
-
monkeypatch.setattr(core, "EXTRA_MOUNTS", [])
|
|
16
|
-
monkeypatch.setattr(core, "NO_DOCKER", False)
|
|
17
|
-
# Neutralise X11 forwarding so argv is deterministic across hosts.
|
|
18
|
-
monkeypatch.delenv("XAUTHORITY", raising=False)
|
|
19
|
-
monkeypatch.delenv("HOME", raising=False)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
# ---- build_renode_argv ----
|
|
23
|
-
|
|
24
|
-
def test_no_docker_is_bare_renode(monkeypatch):
|
|
25
|
-
monkeypatch.setattr(core, "NO_DOCKER", True)
|
|
26
|
-
assert docker.build_renode_argv("job.resc", "img", extra_args=["--console"]) == [
|
|
27
|
-
"renode", "job.resc", "--console"]
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def test_docker_argv_core_tokens():
|
|
31
|
-
argv = docker.build_renode_argv("job.resc", "antmicro/renode:1.16.1")
|
|
32
|
-
assert argv[:3] == ["docker", "run", "-it"]
|
|
33
|
-
assert "--net=host" in argv
|
|
34
|
-
assert "antmicro/renode:1.16.1" in argv
|
|
35
|
-
# The image must be immediately followed by the renode invocation.
|
|
36
|
-
i = argv.index("antmicro/renode:1.16.1")
|
|
37
|
-
assert argv[i + 1:i + 3] == ["renode", "job.resc"]
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def test_docker_argv_publishes_ports():
|
|
41
|
-
argv = docker.build_renode_argv("j.resc", "img", ports=[3333, 3334])
|
|
42
|
-
joined = " ".join(argv)
|
|
43
|
-
assert "-p 3333:3333" in joined and "-p 3334:3334" in joined
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def test_docker_argv_appends_extra_args_after_resc():
|
|
47
|
-
argv = docker.build_renode_argv("j.resc", "img", extra_args=["--console", "--hide-log"])
|
|
48
|
-
assert argv[-3:] == ["j.resc", "--console", "--hide-log"]
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
# ---- _runtime_flags ----
|
|
52
|
-
|
|
53
|
-
def test_runtime_flags_none():
|
|
54
|
-
assert docker._runtime_flags(None) == []
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def test_runtime_flags_root_and_caps():
|
|
58
|
-
flags = docker._runtime_flags(RuntimeOptions(root=True, capabilities={"NET_ADMIN"}))
|
|
59
|
-
assert "-u" in flags and "0:0" in flags
|
|
60
|
-
assert flags[flags.index("--cap-add") + 1] == "NET_ADMIN"
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def test_runtime_flags_device_present(monkeypatch):
|
|
64
|
-
monkeypatch.setattr(docker.os.path, "exists", lambda p: p == "/dev/net/tun")
|
|
65
|
-
flags = docker._runtime_flags(RuntimeOptions(devices={"/dev/net/tun"}))
|
|
66
|
-
assert flags[flags.index("--device") + 1] == "/dev/net/tun"
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
def test_runtime_flags_all_devices_absent_yields_nothing(monkeypatch):
|
|
70
|
-
# A requested device missing on the host must not fail the run, and (per the
|
|
71
|
-
# docstring) no flags at all are applied so it drops to unprivileged dummy mode.
|
|
72
|
-
monkeypatch.setattr(docker.os.path, "exists", lambda p: False)
|
|
73
|
-
assert docker._runtime_flags(
|
|
74
|
-
RuntimeOptions(devices={"/dev/net/tun"}, root=True)) == []
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
# ---- _user_flags ----
|
|
78
|
-
|
|
79
|
-
def test_user_flags_maps_host_uid(monkeypatch):
|
|
80
|
-
monkeypatch.setattr(core, "host_uid_gid", lambda: (1000, 1000))
|
|
81
|
-
assert docker._user_flags() == ["-u", "1000:1000"]
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
def test_user_flags_empty_without_uid(monkeypatch):
|
|
85
|
-
monkeypatch.setattr(core, "host_uid_gid", lambda: None)
|
|
86
|
-
assert docker._user_flags() == []
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
# ---- resolve_shell_image ----
|
|
90
|
-
|
|
91
|
-
def test_shell_image_explicit_wins():
|
|
92
|
-
assert docker.resolve_shell_image({"image": "alpine"}, "default:img") == "alpine"
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def test_shell_image_falls_back_to_default():
|
|
96
|
-
assert docker.resolve_shell_image({}, "default:img") == "default:img"
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
def test_shell_image_no_docker_uses_default(monkeypatch):
|
|
100
|
-
monkeypatch.setattr(core, "NO_DOCKER", True)
|
|
101
|
-
assert docker.resolve_shell_image({"image": "alpine"}, "default:img") == "alpine"
|
|
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
|