Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions ebuild/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
# Copyright (c) 2026 EoS Project

"""Allow running ebuild as a module: python -m ebuild."""
# The integration commands are registered on the group inside
# ebuild.cli.commands, so `python -m ebuild` and the installed `ebuild`
# console script expose the same command set. Importing `cli` is enough.
from ebuild.cli.commands import cli
from ebuild.cli.integration import register_commands

register_commands(cli)

if __name__ == "__main__":
cli()
24 changes: 19 additions & 5 deletions ebuild/cli/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from ebuild import __version__
from ebuild.build.ninja_backend import NinjaBackend, PackagePaths
from ebuild.build.toolchain import resolve_toolchain
from ebuild.cli.integration import register_commands as _register_integration_commands
from ebuild.cli.logger import Logger
from ebuild.core.config import ConfigError, load_config, ProjectConfig
from ebuild.core.graph import CycleError, DependencyGraph, build_dependency_graph
Expand Down Expand Up @@ -2143,6 +2144,8 @@ def test(log: Logger, config_path: str, build_dir: str,
"""
log.header("ebuild — Test")

build_path = Path(build_dir)

try:
log.step("Loading configuration...")
cfg = load_config(config_path)
Expand All @@ -2157,8 +2160,6 @@ def test(log: Logger, config_path: str, build_dir: str,
log.error(f"Configuration error: {e}")
raise SystemExit(1)

build_path = _resolve_build_dir(build_dir, cfg)

native = [t for t in cfg.targets if t.target_type == "test"]
if native:
_run_native_tests(cfg, native, build_path, log, name_filter)
Expand Down Expand Up @@ -2227,9 +2228,7 @@ def _run_native_tests(
+ ["-f", str(build_path / "build.ninja")]
+ [str(build_path / t.name) for t in selected]
)
# Run from the project directory, as `ebuild build` does: the source
# paths recorded in build.ninja are relative to it.
result = subprocess.run(argv, cwd=str(cfg.source_dir))
result = subprocess.run(argv)
if result.returncode != 0:
log.error("Test targets failed to build.")
raise SystemExit(result.returncode)
Expand Down Expand Up @@ -2366,3 +2365,18 @@ def _serial_ports() -> List[str]:
for pattern in ("/dev/ttyUSB*", "/dev/ttyACM*", "/dev/tty.usb*"):
found.extend(sorted(glob.glob(pattern)))
return found


# ═════════════════════════════════════════════════════════════
# Integration commands
# ═════════════════════════════════════════════════════════════
# `integration`, `qemu`, `sdk`, `package` and `models` live in
# ebuild/cli/integration.py and are attached to the group by
# register_commands(). That call used to live only in ebuild/__main__.py,
# which runs for `python -m ebuild` and not for the `ebuild` console script
# that pyproject.toml's [project.scripts] installs on PATH. The five
# commands were therefore missing from the entry point that every user and
# every doc actually invokes. Registering here attaches them to the group
# itself, so both entry points -- and anything that imports `cli` -- see
# the same CLI.
_register_integration_commands(cli)
82 changes: 82 additions & 0 deletions tests/unit/test_integration_commands_registered.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# SPDX-License-Identifier: MIT
# Copyright (c) 2026 EoS Project

"""The documented integration commands must exist on the installed CLI.

`integration`, `qemu`, `sdk`, `package` and `models` are defined in
ebuild/cli/integration.py and attached to the group by register_commands().
That call used to live only in ebuild/__main__.py, so the five commands
existed under `python -m ebuild` and were absent from the `ebuild` console
script that pyproject.toml installs on PATH -- the invocation the README,
docs/architecture.md, docs/qms/quality_management_system.md and
examples/eradar360/eos.yaml all use.

These tests assert against the `cli` object named by [project.scripts], which
is the thing the entry point actually runs, so the gap cannot come back.
"""

from pathlib import Path

import pytest
from click.testing import CliRunner

from ebuild.cli.commands import cli


# Defined in ebuild/cli/integration.py and referenced by the docs.
INTEGRATION_COMMANDS = ["integration", "qemu", "sdk", "package", "models"]


@pytest.mark.ebuild
class TestIntegrationCommandsRegistered:
"""Every integration command must be on the console-script group."""

@pytest.mark.parametrize("name", INTEGRATION_COMMANDS)
def test_command_is_registered(self, name):
assert name in cli.commands, (
f"'ebuild {name}' is defined in ebuild/cli/integration.py and "
f"documented, but is not registered on the group that "
f"[project.scripts] points at"
)

@pytest.mark.parametrize("name", INTEGRATION_COMMANDS)
def test_command_help_runs(self, name):
result = CliRunner().invoke(cli, [name, "--help"])
assert result.exit_code == 0, result.output

def test_entry_point_and_module_expose_the_same_commands(self):
"""`ebuild <cmd>` and `python -m ebuild <cmd>` must not diverge.

__main__.py imports the same group object, so this asserts that
importing it adds nothing the console script does not already have.
"""
console_script_commands = set(cli.commands)

import ebuild.__main__ as module_entry_point

assert set(module_entry_point.cli.commands) == console_script_commands

def test_documented_commands_are_reachable(self):
"""`ebuild --help` must list the commands the docs tell users to run."""
result = CliRunner().invoke(cli, ["--help"])
assert result.exit_code == 0, result.output
for name in INTEGRATION_COMMANDS:
assert name in result.output, (
f"'{name}' is missing from `ebuild --help`"
)


@pytest.mark.ebuild
def test_entry_point_target_is_the_registered_group():
"""[project.scripts] must keep pointing at the group under test.

If the entry point is ever re-pointed somewhere else, these tests would
keep passing while the installed `ebuild` command lost the commands
again. Pin the target so that change is caught here.
"""
pyproject = Path(__file__).resolve().parents[2] / "pyproject.toml"
content = pyproject.read_text(encoding="utf-8")
assert 'ebuild = "ebuild.cli.commands:cli"' in content, (
"the console script no longer points at ebuild.cli.commands:cli, so "
"these tests no longer cover the installed command"
)