hector-cli 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
hector/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-or-later
2
+ from .core import TOOL_VERSION
3
+
4
+ __version__ = TOOL_VERSION
hector/__main__.py ADDED
@@ -0,0 +1,7 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-or-later
2
+ """Enable `python -m hector`, equivalent to the `hector` console script."""
3
+
4
+ from hector.pipeline import main
5
+
6
+ if __name__ == "__main__":
7
+ main()
@@ -0,0 +1,35 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-or-later
2
+ """hector's command surface: one `hector <name>` subcommand per module here.
3
+
4
+ Importing this package registers every command into COMMANDS; build_parser() then
5
+ wires them all into a single argparse parser. To add a command, create a module
6
+ (see run.py for the template) and add it to the import list below.
7
+ """
8
+
9
+ import argparse
10
+
11
+ from ..core import TOOL_VERSION
12
+ from .base import COMMANDS, Command, add_common_arguments, add_simulation_arguments
13
+
14
+ # Importing each module runs its @COMMANDS.register(...) decorator.
15
+ from . import run, test, export, init, validate # noqa: E402,F401
16
+
17
+ __all__ = ["COMMANDS", "Command", "build_parser",
18
+ "add_common_arguments", "add_simulation_arguments"]
19
+
20
+
21
+ def build_parser():
22
+ """Assemble the top-level `hector` parser with one subparser per registered command."""
23
+ parser = argparse.ArgumentParser(
24
+ prog="hector",
25
+ description="YAML-driven Renode + Verilator simulation orchestrator",
26
+ )
27
+ parser.add_argument("--version", action="version", version=f"hector {TOOL_VERSION}")
28
+
29
+ sub = parser.add_subparsers(dest="command", required=True, metavar="<command>")
30
+ for name in sorted(COMMANDS.keys()):
31
+ cmd = COMMANDS.get(name)() # the registry holds classes; instantiate
32
+ sp = sub.add_parser(cmd.name, help=cmd.help, description=cmd.help)
33
+ cmd.add_arguments(sp)
34
+ sp.set_defaults(_command=cmd)
35
+ return parser
@@ -0,0 +1,111 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-or-later
2
+ """Command framework: a tiny registry + base class so each subcommand is a
3
+ self-contained module.
4
+
5
+ To add a command, drop a module in this package that subclasses Command, fills in
6
+ name/help, declares its flags in add_arguments(), does its work in execute(), and
7
+ registers itself with @COMMANDS.register("<name>"). Import it from __init__.py and
8
+ `hector <name>` exists — no central switch to edit.
9
+ """
10
+
11
+ from ..core import Registry
12
+
13
+ COMMANDS = Registry("command")
14
+
15
+
16
+ class Command:
17
+ """One `hector <name>` subcommand."""
18
+ name = "" # the subcommand word, e.g. "run"
19
+ help = "" # one-line description shown in `hector --help`
20
+
21
+ def add_arguments(self, parser):
22
+ """Declare this command's flags on its argparse subparser."""
23
+
24
+ def execute(self, args):
25
+ """Do the work. Return a process exit code (0 = success, falsy == 0)."""
26
+ raise NotImplementedError
27
+
28
+
29
+ # ---------------------------------------------------------------------------
30
+ # Reusable flag groups (shared so each command's surface stays consistent)
31
+ # ---------------------------------------------------------------------------
32
+
33
+ def add_common_arguments(parser):
34
+ """Flags shared by the config-driven commands (run / test / export): they all
35
+ read .hector.yaml, pin a Renode version, expand the matrix, and collect output."""
36
+ parser.add_argument(
37
+ "--set", action="append", metavar="KEY=VALUE", dest="set_args",
38
+ help="Override a config argument. Repeatable. Takes precedence over env vars "
39
+ "and config defaults. Example: --set BIN=fw.elf --set BOARD=nucleo.",
40
+ )
41
+ parser.add_argument(
42
+ "--renode-version", metavar="VERSION", dest="renode_version",
43
+ help="Renode version to use (e.g. 1.16.1). Overrides renode_version from "
44
+ ".hector.yaml.",
45
+ )
46
+ parser.add_argument(
47
+ "--renode-dir", metavar="PATH", dest="renode_dir", default=None,
48
+ help="Location of the Renode source checkout (only fetched when a config builds "
49
+ "'modules:' or uses ${RENODE_DIR}). Reused if it exists, cloned there if "
50
+ "not. Default: .hector/renode. Point it at a cache to avoid re-cloning.",
51
+ )
52
+ parser.add_argument(
53
+ "--renode-integration-dir", metavar="PATH", dest="renode_integration_dir",
54
+ default=None,
55
+ help="Location of the renode-verilator-integration checkout (see --renode-dir). "
56
+ "Default: .hector/renode-verilator-integration.",
57
+ )
58
+ parser.add_argument(
59
+ "--output", metavar="DIR", default="results",
60
+ help="Directory for test results, artifacts, and simulation output files "
61
+ "(default: results).",
62
+ )
63
+ parser.add_argument(
64
+ "--artifacts", action="append", metavar="GLOB", dest="artifacts",
65
+ help="Glob pattern(s) for output files to collect into <output>/artifacts/ "
66
+ "after each job. Repeatable. Overrides the top-level 'artifacts:' in "
67
+ ".hector.yaml when given.",
68
+ )
69
+ parser.add_argument(
70
+ "--gather-execution-metrics", action="append", nargs="?", const=None,
71
+ metavar="NODE", dest="gather_execution_metrics",
72
+ help="Enable Renode's CPU execution profiler. With no argument, enables it for "
73
+ "all machines. Pass a node name to target one machine. Repeatable. Writes "
74
+ "one binary file per node to the output directory.",
75
+ )
76
+ parser.add_argument(
77
+ "--job", metavar="KEY=VALUE",
78
+ help="Select the matrix combination to run, e.g. --job BOARD=stm32f7 "
79
+ "(repeat keys with commas: --job BOARD=stm32f7,FW=release.elf). Required "
80
+ "when a matrix is defined — hector runs one combination per invocation and "
81
+ "does not expand the matrix itself (that's the CI's job).",
82
+ )
83
+ docker_mode = parser.add_mutually_exclusive_group()
84
+ docker_mode.add_argument(
85
+ "--no-docker", action="store_true", dest="no_docker",
86
+ help="Call the locally installed renode / renode-test binaries directly "
87
+ "instead of launching a Docker container.",
88
+ )
89
+ docker_mode.add_argument(
90
+ "--workspace-mount", metavar="PATH", dest="workspace_mount", default=None,
91
+ help="Container path where the project root is bind-mounted "
92
+ "(default: same absolute path as on the host).",
93
+ )
94
+
95
+
96
+ def add_simulation_arguments(parser):
97
+ """Flags shared by commands that assemble/boot a single emulation (run / export)."""
98
+ parser.add_argument(
99
+ "--debug", action="append", metavar="NODE:PORT",
100
+ help="Halt the CPU and open a GDB server for each named node. Repeatable: "
101
+ "--debug boardA:3333 --debug boardB:3334.",
102
+ )
103
+ parser.add_argument(
104
+ "--snapshot", metavar="PATH", dest="snapshot",
105
+ help="Load a Renode snapshot instead of booting from the generated resc.",
106
+ )
107
+ parser.add_argument(
108
+ "--renode-args", metavar="ARGS", dest="renode_args", default="",
109
+ help="Extra arguments passed verbatim to Renode "
110
+ "(e.g. --renode-args '--console --hide-log').",
111
+ )
@@ -0,0 +1,26 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-or-later
2
+ """`hector export` — generate the emulation files but, instead of running, print
3
+ the command that would run them.
4
+
5
+ Emits the `docker run ... renode ...` invocation (or the bare `renode ...` command
6
+ under --no-docker) for each matrix job, so you can inspect, copy, or wrap it.
7
+ """
8
+
9
+ from .. import pipeline
10
+ from .base import COMMANDS, Command, add_common_arguments, add_simulation_arguments
11
+
12
+
13
+ @COMMANDS.register("export")
14
+ class ExportCommand(Command):
15
+ name = "export"
16
+ help = "Generate the files and print the run command instead of executing it."
17
+
18
+ def add_arguments(self, parser):
19
+ add_common_arguments(parser)
20
+ add_simulation_arguments(parser)
21
+
22
+ def execute(self, args):
23
+ pipeline.apply_global_flags(args)
24
+ config = pipeline.load_config_or_exit()
25
+ cfg = pipeline.build_job_config(pipeline.action_export, config, args)
26
+ return pipeline.execute_jobs(cfg, args)
@@ -0,0 +1,15 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-or-later
2
+ """`hector init` — scaffold a new .hector.yaml in the current directory."""
3
+
4
+ from ..scaffold import scaffold_init
5
+ from .base import COMMANDS, Command
6
+
7
+
8
+ @COMMANDS.register("init")
9
+ class InitCommand(Command):
10
+ name = "init"
11
+ help = "Scaffold a new .hector.yaml in the current directory."
12
+
13
+ def execute(self, args):
14
+ scaffold_init()
15
+ return 0
hector/commands/run.py ADDED
@@ -0,0 +1,27 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-or-later
2
+ """`hector run` — boot the emulation interactively in the Renode monitor."""
3
+
4
+ from .. import pipeline
5
+ from .base import COMMANDS, Command, add_common_arguments, add_simulation_arguments
6
+
7
+
8
+ @COMMANDS.register("run")
9
+ class RunCommand(Command):
10
+ name = "run"
11
+ help = "Build the emulation and run it interactively in Renode."
12
+
13
+ def add_arguments(self, parser):
14
+ add_common_arguments(parser)
15
+ add_simulation_arguments(parser)
16
+
17
+ def execute(self, args):
18
+ pipeline.apply_global_flags(args)
19
+
20
+ # Convenience fast path: a snapshot + pinned version needs no .hector.yaml.
21
+ if args.snapshot and args.renode_version:
22
+ pipeline.snapshot_fastpath(args)
23
+ return 0
24
+
25
+ config = pipeline.load_config_or_exit()
26
+ cfg = pipeline.build_job_config(pipeline.action_simulate, config, args)
27
+ return pipeline.execute_jobs(cfg, args)
@@ -0,0 +1,52 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-or-later
2
+ """`hector test` — run the `tests:` section; exit 1 on any failure."""
3
+
4
+ from .. import pipeline
5
+ from .base import COMMANDS, Command, add_common_arguments
6
+
7
+
8
+ @COMMANDS.register("test")
9
+ class TestCommand(Command):
10
+ name = "test"
11
+ help = "Run the 'tests:' section against the generated emulation; exit 1 on failure."
12
+
13
+ def add_arguments(self, parser):
14
+ add_common_arguments(parser)
15
+ parser.add_argument(
16
+ "--test-file", metavar="FILE", dest="test_file",
17
+ help="Run a specific .robot file against the generated resc instead of the "
18
+ "YAML-defined tests. ${RESC} expands to the resc path.",
19
+ )
20
+ parser.add_argument(
21
+ "--test-name", metavar="NAME", dest="test_name", action="append",
22
+ help="Run only tests whose name contains NAME. Repeatable.",
23
+ )
24
+ parser.add_argument(
25
+ "--reporters", action="append", metavar="REPORTER", dest="reporters",
26
+ help="Reporter to call after tests. Repeatable (default: junit). "
27
+ "Example: --reporters junit --reporters json.",
28
+ )
29
+ parser.add_argument(
30
+ "--live", action="store_true",
31
+ help="Stream bash test output line-by-line and run robot tests verbosely.",
32
+ )
33
+ parser.add_argument(
34
+ "--snapshot", metavar="PATH", dest="snapshot",
35
+ help="Load a Renode snapshot at the start of every test instead of booting "
36
+ "from the resc. Overrides per-test 'snapshot:' fields.",
37
+ )
38
+ parser.add_argument(
39
+ "--renode-test-args", metavar="ARGS", dest="renode_test_args", default="",
40
+ help="Extra arguments passed verbatim to renode-test "
41
+ "(e.g. --renode-test-args '--loglevel DEBUG').",
42
+ )
43
+ parser.add_argument(
44
+ "--fail-fast", action="store_true", dest="fail_fast",
45
+ help="Stop after the first failing test instead of running them all.",
46
+ )
47
+
48
+ def execute(self, args):
49
+ pipeline.apply_global_flags(args)
50
+ config = pipeline.load_config_or_exit()
51
+ cfg = pipeline.build_job_config(pipeline.action_test, config, args, test_mode=True)
52
+ return pipeline.execute_jobs(cfg, args)
@@ -0,0 +1,15 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-or-later
2
+ """`hector validate` — check .hector.yaml without running anything; exit 1 on errors."""
3
+
4
+ from .. import pipeline
5
+ from .base import COMMANDS, Command
6
+
7
+
8
+ @COMMANDS.register("validate")
9
+ class ValidateCommand(Command):
10
+ name = "validate"
11
+ help = "Validate .hector.yaml without running anything; exit 1 on errors."
12
+
13
+ def execute(self, args):
14
+ # _run_validate prints a report and exits with the right code itself.
15
+ pipeline._run_validate()
hector/connections.py ADDED
@@ -0,0 +1,130 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-or-later
2
+ """
3
+ Connection resolution. Generic over the HUBS registry, so adding a hub type needs no
4
+ change here.
5
+
6
+ intra-machine (same node, '->') -> repl updating entry (GPIO/IRQ wire)
7
+ periph <-> hub / port@pin -> hub -> cross-machine link via a declared hub
8
+ """
9
+
10
+ import re
11
+
12
+ from .core import parse_endpoint
13
+ from .hubs import HUBS
14
+
15
+
16
+ def is_hub_line(line, hubs_registry):
17
+ """Return True if any token in the line is a declared hub name."""
18
+ for tok in re.split(r"<->|->", line):
19
+ base = re.split(r"[@\[]", tok.strip())[0]
20
+ if "." not in base and base in hubs_registry:
21
+ return True
22
+ return False
23
+
24
+
25
+ def _expand_chains(connections):
26
+ """Expand inline hub-chain syntax into two separate endpoint↔hub lines.
27
+
28
+ Symmetric: A <-> hub <-> B → A <-> hub + B <-> hub
29
+ Directional: A -> hub -> B → A -> hub + hub -> B
30
+ """
31
+ result = []
32
+ for line in connections:
33
+ parts = line.split("<->")
34
+ if len(parts) == 3:
35
+ a, hub, b = (p.strip() for p in parts)
36
+ result += [f"{a} <-> {hub}", f"{b} <-> {hub}"]
37
+ continue
38
+ if "<->" not in line:
39
+ parts = line.split("->")
40
+ if len(parts) == 3:
41
+ a, hub, b = (p.strip() for p in parts)
42
+ result += [f"{a} -> {hub}", f"{hub} -> {b}"]
43
+ continue
44
+ result.append(line)
45
+ return result
46
+
47
+
48
+ def qualify_node_connection(line, node_name, hubs_registry, known_nodes=None):
49
+ """Qualify bare peripheral names in a per-node connections block with the node name.
50
+ Hub names and already-qualified 'node.x' tokens are left unchanged.
51
+ 'periph.Signal' tokens (where periph is not a node) are qualified as 'node.periph.Signal'."""
52
+ known_nodes = known_nodes or set()
53
+ def q(tok):
54
+ tok = tok.strip()
55
+ base = tok.split("@", 1)[0]
56
+ if "." in base:
57
+ prefix = base.split(".", 1)[0]
58
+ if prefix in known_nodes or prefix in hubs_registry:
59
+ return tok
60
+ return f"{node_name}.{tok}"
61
+ return tok if base in hubs_registry else f"{node_name}.{tok}"
62
+ parts = line.split("<->")
63
+ if len(parts) == 3:
64
+ a, hub, b = (p.strip() for p in parts)
65
+ return f"{q(a)} <-> {hub} <-> {q(b)}"
66
+ if "<->" not in line:
67
+ parts = line.split("->")
68
+ if len(parts) == 3:
69
+ a, hub, b = (p.strip() for p in parts)
70
+ return f"{q(a)} -> {hub} -> {q(b)}"
71
+ if "<->" in line:
72
+ a, b = line.split("<->", 1); return f"{q(a)} <-> {q(b)}"
73
+ if "->" in line:
74
+ a, b = line.split("->", 1); return f"{q(a)} -> {q(b)}"
75
+ raise ValueError(f"Connection '{line}' (node '{node_name}') has no '->' or '<->'")
76
+
77
+
78
+ def resolve_connections(connections, concrete_nodes, hubs_registry):
79
+ """Return cross_connection_specs for hub-connected endpoints.
80
+ All intra-machine wires must have been injected into _repl_signals before calling this.
81
+ `hubs_registry` maps a hub NAME to its declared type string."""
82
+ def check_node(node, line):
83
+ if node not in concrete_nodes:
84
+ raise ValueError(f"Connection '{line}': unknown node '{node}'.")
85
+ if concrete_nodes[node].get("backend", "renode") != "renode":
86
+ raise ValueError(f"Connection '{line}': '{node}' is not a renode node.")
87
+
88
+ specs = []
89
+ hub_specs = {}
90
+ for line in _expand_chains(connections):
91
+ if "<->" in line: a, b = line.split("<->", 1); op = "<->"
92
+ elif "->" in line: a, b = line.split("->", 1); op = "->"
93
+ else: raise ValueError(f"Connection '{line}' has no '->' or '<->'")
94
+ a, b = a.strip(), b.strip()
95
+ a_hub = is_hub_line(a, hubs_registry)
96
+ b_hub = is_hub_line(b, hubs_registry)
97
+
98
+ if not a_hub and not b_hub:
99
+ raise ValueError(
100
+ f"Connection '{line}': non-hub connection reached resolve_connections. "
101
+ "Cross-machine links require a hub declared under 'hubs:'. "
102
+ "Intra-machine wires belong in the per-node 'connections:' block.")
103
+
104
+ if a_hub and b_hub:
105
+ raise ValueError(f"Connection '{line}': both sides are hubs.")
106
+
107
+ hub_name = (a if a_hub else b).split("@", 1)[0]
108
+ ep = parse_endpoint(b if a_hub else a)
109
+ check_node(ep["node"], line)
110
+ hub_cls = HUBS.get(hubs_registry[hub_name])
111
+
112
+ if hub_cls.symmetric and op != "<->":
113
+ raise ValueError(f"Connection '{line}': '{hub_name}' is symmetric; use '<->'.")
114
+ if not hub_cls.symmetric and op != "->":
115
+ raise ValueError(f"Connection '{line}': '{hub_name}' is directional; use '->'.")
116
+
117
+ spec = hub_cls.build_spec(ep["node"], ep["name"], hub_name, ep,
118
+ hub_on_left=a_hub, line=line)
119
+ specs.append(spec)
120
+ hub_specs.setdefault(hub_name, (hub_cls, []))[1].append(spec)
121
+
122
+ for hub_name, (hub_cls, hspecs) in hub_specs.items():
123
+ if hub_cls.symmetric and len(hspecs) == 2:
124
+ a = f'{hspecs[0]["node"]}.{hspecs[0]["periph"]}'
125
+ b = f'{hspecs[1]["node"]}.{hspecs[1]["periph"]}'
126
+ print(f"[XCONN] {a} <-> {hub_name} <-> {b}")
127
+ else:
128
+ for spec in hspecs:
129
+ print(f"[XCONN] {hub_cls.describe(spec)}")
130
+ return specs