Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
55022eb50This file is pinned to the selected release. Download the original to open it in your design tool.
# Copyright (c) 2026 TiaC Systems
# SPDX-License-Identifier: Apache-2.0
"""Unit: cli — the frozen front door.
Two contracts live here, both cli.py's own: the frozen argv surface
(`expand <rig_yml>` + the nine options, asserted in-process via
build_parser()/main(), no subprocess) and the loud-refusal behaviour of
unimplemented paths (exit 3, single-line `rigc: not implemented: <what>`
on stderr, never a traceback, never exit 1, never a silent accept).
Every `main([...])` call below passes an EXPLICIT `--shield-dir` pointing
at an empty/nonexistent directory (`_no_shields`) -- shield-dir scanning
runs unconditionally, before rig.yml even opens, and
the CLI's own bare-invocation fallback is the vendored PRODUCTION shield
library (`loader/library.py`'s `SHIELDS_DIR`, direct-API/test use only per
its own docstring). Omitting `--shield-dir` here would make these unit
tests silently scan and cpp-parse real repo shield content -- a
subprocess call and a hermeticity violation both, so every call site
supplies one (`glob.glob` on a directory that does not exist just returns
`[]`, no error -- the directory need not exist, only be named).
"""
from __future__ import annotations
import argparse
import logging
import re
from pathlib import Path
from textwrap import dedent
import pytest
from rigc import cli
from rigc.cli import _configure_logging, build_parser, main
def _no_shields(tmp_path: Path) -> list[str]:
"""Point EVERY library root that has a production fallback at an
empty directory -- --shield-dir alone is not enough: --connector-dir
has the identical None-falls-back-to-the-real-tree shape
(cli._expand -> load_types -> registry.BINDINGS), so omitting it
would let the unit suite read production connector bindings and
headers. A unit test touches NO production data."""
empty = tmp_path / "no_library_here"
return ["--shield-dir", str(empty), "--connector-dir", str(empty), "--include-dir", str(empty)]
def _parse(extra: list[str]) -> argparse.Namespace:
return build_parser().parse_args(["expand", "rig.yml", "--out-dir", "out", *extra])
# --------------------------------------------------------- the argv surface
def test_positional_rig_and_out_dir() -> None:
args = _parse([])
assert args.command == "expand"
assert args.rig == "rig.yml"
assert args.out_dir == "out"
def test_defaults_are_none() -> None:
args = _parse([])
assert args.shield_dirs is None
assert args.board is None
assert args.board_dts is None
assert args.build_info is None
assert args.bindings_dirs is None
assert args.include_dirs is None
assert args.connector_dirs is None
assert args.revision is None
assert args.variant is None
assert args.verbose == 0
assert args.promote is None
# ------------------------------------------- --promote / positional exclusion
def test_promote_and_positional_rig_are_mutually_exclusive() -> None:
with pytest.raises(SystemExit) as e:
build_parser().parse_args(
["expand", "rig.yml", "--promote", "some_shield", "--out-dir", "out"]
)
assert e.value.code == 2
def test_neither_rig_nor_promote_given_is_a_usage_error() -> None:
with pytest.raises(SystemExit) as e:
build_parser().parse_args(["expand", "--out-dir", "out"])
assert e.value.code == 2
def test_promote_alone_parses() -> None:
args = build_parser().parse_args(
["expand", "--promote", "adafruit_data_logger", "--out-dir", "out"]
)
assert args.rig is None
assert args.promote == "adafruit_data_logger"
def test_repeatable_options_accumulate_in_order() -> None:
args = _parse(
[
"--shield-dir",
"s1",
"--shield-dir",
"s2",
"--bindings-dir",
"b1",
"--bindings-dir",
"b2",
"--include-dir",
"i1",
"--include-dir",
"i2",
"--connector-dir",
"c1",
"--connector-dir",
"c2",
]
)
assert args.shield_dirs == ["s1", "s2"]
assert args.bindings_dirs == ["b1", "b2"]
assert args.include_dirs == ["i1", "i2"]
assert args.connector_dirs == ["c1", "c2"]
def test_single_valued_options() -> None:
args = _parse(
[
"--board",
"some_board/soc/rig",
"--board-dts",
"b.dts",
"--build-info",
"bi.yml",
"--revision",
"2",
"--variant",
"b",
]
)
assert args.board == "some_board/soc/rig"
assert args.board_dts == "b.dts"
assert args.build_info == "bi.yml"
assert args.revision == "2"
assert args.variant == "b"
def test_verbose_counts_repeats() -> None:
assert _parse([]).verbose == 0
assert _parse(["-v"]).verbose == 1
assert _parse(["-vv"]).verbose == 2
assert _parse(["-v", "-v"]).verbose == 2
assert _parse(["--verbose", "--verbose", "--verbose"]).verbose == 3
def test_out_dir_is_required() -> None:
with pytest.raises(SystemExit) as e:
build_parser().parse_args(["expand", "rig.yml"])
assert e.value.code == 2
def test_subcommand_is_required() -> None:
with pytest.raises(SystemExit) as e:
build_parser().parse_args([])
assert e.value.code == 2
def test_unknown_option_is_a_usage_error() -> None:
with pytest.raises(SystemExit) as e:
_parse(["--no-such-flag"])
assert e.value.code == 2
def test_unknown_command_is_a_usage_error() -> None:
with pytest.raises(SystemExit) as e:
build_parser().parse_args(["translate", "rig.yml"])
assert e.value.code == 2
def test_main_is_callable_in_process(tmp_path: Path) -> None:
"""main(argv) -> int -- returns rather than raises for every
non-usage outcome (here: an unimplemented path, exit code 3)."""
ret = main(
[
"expand",
str(tmp_path / "no-such-rig.yml"),
"--out-dir",
str(tmp_path / "out"),
*_no_shields(tmp_path),
]
)
assert isinstance(ret, int)
assert ret == 3
def test_board_reading_options_are_now_live(tmp_path: Path) -> None:
"""--board-dts/--build-info/--bindings-dir wire into the board
reader. A rig with no board-resolution problem of its own (loader
accepts cleanly) but naming a --board-dts that does not exist on
disk must be rejected (exit 1, phys-board) -- proving the option
actually reaches load_board rather than being parsed and discarded.
(A rig the LOADER itself rejects first -- e.g. unreadable -- still
exits 3 regardless of these options: see test_recipe_resolved_lazily
below.) --board is given here too: rig.yml carries no board: of its
own, so with no board injected, cli.py's own board-empty check would
reject BEFORE ever reaching --board-dts, which is not what this test
means to exercise."""
(tmp_path / "rig.yml").write_text(
dedent("""\
rig:
name: r
""")
)
(tmp_path / "r.yml").write_text(
dedent("""\
instances: []
""")
)
ret = main(
[
"expand",
str(tmp_path / "rig.yml"),
"--out-dir",
str(tmp_path / "out"),
*_no_shields(tmp_path),
"--board",
"some_board/soc/rig",
"--board-dts",
str(tmp_path / "no-such-board.dts"),
]
)
assert ret == 1
def test_board_option_reaches_load_board_with_the_given_name(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
"""--board threads straight to `rig.board` -- proven the same way as
the option above (a --board-dts that does not exist forces a
phys-board rejection), but checking that the GIVEN name is what
load_board's own diagnostic embeds. rig.yml declares no board: of
its own -- --board is the only source."""
(tmp_path / "rig.yml").write_text(
dedent("""\
rig:
name: r
""")
)
(tmp_path / "r.yml").write_text(
dedent("""\
instances: []
""")
)
ret = main(
[
"expand",
str(tmp_path / "rig.yml"),
"--out-dir",
str(tmp_path / "out"),
*_no_shields(tmp_path),
"--board",
"given_board/soc/rig",
"--board-dts",
str(tmp_path / "no-such-board.dts"),
]
)
assert ret == 1
err = capsys.readouterr().err
assert "given_board/soc/rig" in err
def test_board_option_absent_is_a_clean_phys_board_reject(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
"""The negative control: omitting --board, with no board: declaration
in rig.yml to fall back to, must still be a clean phys-board
rejection -- never a crash, and never load_board's own confusing
"unknown board ''" (this fixture's --board-dts, which would
otherwise force exactly that, proves cli.py's own board-empty check
fires FIRST: --board-dts is never even reached)."""
(tmp_path / "rig.yml").write_text(
dedent("""\
rig:
name: r
""")
)
(tmp_path / "r.yml").write_text(
dedent("""\
instances: []
""")
)
ret = main(
[
"expand",
str(tmp_path / "rig.yml"),
"--out-dir",
str(tmp_path / "out"),
*_no_shields(tmp_path),
"--board-dts",
str(tmp_path / "no-such-board.dts"),
]
)
assert ret == 1
err = capsys.readouterr().err
assert "[phys-board]" in err
assert "no board given" in err
assert "no-such-board.dts" not in err
def test_recipe_resolved_lazily(tmp_path: Path) -> None:
"""A bogus --build-info path must never crash the run when the rig
itself is rejected first (here: unreadable) -- the recipe is a
board-reading concern, resolved only once the loader has already
accepted, never eagerly alongside the other inputs (cli.py's own
docstring at the _resolve_recipe call site). `open()`-ing a
nonexistent --build-info path must never surface as an unhandled
FileNotFoundError -- a traceback is never an acceptable outcome."""
ret = main(
[
"expand",
str(tmp_path / "no-such-rig.yml"),
"--out-dir",
str(tmp_path / "out"),
*_no_shields(tmp_path),
"--build-info",
str(tmp_path / "no-such-build-info.yml"),
]
)
assert ret == 3
# ------------------------------------------------- loud, distinct refusals
def _run(capsys: pytest.CaptureFixture[str], argv: list[str]) -> tuple[int, str]:
ret = main(argv)
captured = capsys.readouterr()
assert captured.out == "" # diagnostics and refusals: stderr only
return ret, captured.err
def test_unreadable_rig_refuses(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
ret, err = _run(
capsys,
[
"expand",
str(tmp_path / "absent.yml"),
"--out-dir",
str(tmp_path / "out"),
*_no_shields(tmp_path),
],
)
assert ret == 3
assert err.startswith("rigc: not implemented: ")
assert len(err.splitlines()) == 1 # one line -- never a traceback
def test_out_of_scope_feature_refuses(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
"""params:/config: are fully implemented -- a YAML parse failure is
the capability that stays Unimplemented: no frozen golden covers
lang-parse wording, so Unimplemented remains the deliberate,
always-acceptable choice."""
(tmp_path / "rig.yml").write_text(
dedent("""\
rig: [this is not, valid: yaml
""")
)
ret, err = _run(
capsys,
[
"expand",
str(tmp_path / "rig.yml"),
"--out-dir",
str(tmp_path / "out"),
*_no_shields(tmp_path),
],
)
assert ret == 3
assert err.startswith("rigc: not implemented: ")
def _write_zero_instance_rig(tmp_path: Path) -> None:
"""A rig with zero instances needs no cpp from the emitter (no params
to resolve, no shield,param-includes header to probe) -- the shape
every accept-path test below reuses so board reading is the only
thing left to stub.
Carries no `board:` of its own -- every caller must pass `--board`
itself, the only source."""
(tmp_path / "rig.yml").write_text(
dedent("""\
rig:
name: r
""")
)
(tmp_path / "r.yml").write_text(
dedent("""\
instances: []
""")
)
def _stub_board_reading(monkeypatch: pytest.MonkeyPatch) -> None:
"""Board reading is integration-only by construction (the same
cpp/unit-test seam that applies to the shield side) -- stubbed
rather than exercised via a real board .dts + cpp."""
from rigc.model import Board
def fake_load_board(name: str, workdir: str, board_dts=None, recipe=None):
return Board(name=name, sockets={}), [], frozenset()
monkeypatch.setattr(cli, "load_board", fake_load_board)
def test_accept_path_now_accepts_and_writes_artifacts(
tmp_path: Path, capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch
) -> None:
"""Input the loader/analyzer find nothing wrong with exits 0 and
writes the rig artifacts + context.cmake -- proving the full accept
path (emit, context.render, the one writer) without a subprocess."""
_write_zero_instance_rig(tmp_path)
_stub_board_reading(monkeypatch)
out_dir = tmp_path / "out"
ret, err = _run(
capsys,
[
"expand",
str(tmp_path / "rig.yml"),
"--out-dir",
str(out_dir),
*_no_shields(tmp_path),
"--board",
"some_board/soc/rig",
],
)
assert ret == 0
assert err == "" # no diagnostics at all -- not even a warning
for fname in ("rig-gen.overlay", "config-sheet.md", "expectations.yml", "context.cmake"):
assert (out_dir / fname).is_file(), fname
assert not (out_dir / "rig-gen-includes.dtsi").exists() # no params: at all
assert not (out_dir / "rig-gen.conf").exists() # never emitted
context_text = (out_dir / "context.cmake").read_text()
assert 'set(RIG_NAME "r")' in context_text
assert 'set(RIG_BOARD "some_board/soc/rig")' in context_text
assert 'set(RIG_SHIELDS "")' in context_text
# ----------------------------------------------------------- the workdir
def _workdir_of(out_dir: Path) -> Path:
"""Where cli.py puts its workdir for a given --out-dir. Derived from
cli.WORKDIR_NAME rather than spelled again, so a rename cannot leave
these tests asserting a path nothing writes.
The location is a property of the code under test, so these tests
assert it directly -- and nothing can leak into real /tmp, because a
workdir inside --out-dir is inside tmp_path by construction."""
return out_dir / cli.WORKDIR_NAME
def test_accept_path_keeps_the_workdir(
tmp_path: Path, capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch
) -> None:
"""An ACCEPTED run is the one that produces an overlay somebody
later doubts, so its intermediates -- the shield
`.dts` the loader wrote, the cpp-preprocessed `.pre` of each, a
promoted shield's synthesized pair -- are exactly the evidence that
doubt needs, and success is no reason to burn them.
The control against "then it never wrote anything at all": the
accept path's own artifacts must still have landed in --out-dir,
which is a different directory."""
_write_zero_instance_rig(tmp_path)
_stub_board_reading(monkeypatch)
out_dir = tmp_path / "out"
ret, _err = _run(
capsys,
[
"expand",
str(tmp_path / "rig.yml"),
"--out-dir",
str(out_dir),
*_no_shields(tmp_path),
"--board",
"some_board/soc/rig",
],
)
assert ret == 0
assert _workdir_of(out_dir).is_dir()
assert (out_dir / "context.cmake").is_file()
def test_reject_path_keeps_the_workdir(
tmp_path: Path, capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch
) -> None:
"""The other exit, kept for the same reason: a rejected board-dts is
real evidence a cpp failure's own rendered diagnostic points at
(e.g. `param-missing-header`, which embeds this very path inside
gcc's stderr text), so the directory must survive this exit too.
Held as a SEPARATE test rather than folded into the accept one: the
two exits reach the end of `_expand` by different routes (this one
returns early from `_reject`, the other falls off the end), and one
route surviving proves nothing about the other."""
_write_zero_instance_rig(tmp_path)
out_dir = tmp_path / "out"
ret, _err = _run(
capsys,
[
"expand",
str(tmp_path / "rig.yml"),
"--out-dir",
str(out_dir),
*_no_shields(tmp_path),
"--board",
"some_board/soc/rig",
"--board-dts",
str(tmp_path / "no-such-board.dts"),
],
)
assert ret == 1
assert _workdir_of(out_dir).is_dir()
def test_entry_wipe_clears_a_previous_runs_workdir(
tmp_path: Path, capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch
) -> None:
"""The one deletion `_expand` still does, and the negative control
for the two retention tests above: "keep the workdir" must not decay
into "accumulate every run's intermediates in one directory".
A stale file left in the workdir by a previous run into the same
--out-dir is GONE after the next run, because a `.pre` that no
longer corresponds to the overlay sitting next to it is worse
evidence than no `.pre` at all. Planted under a name nothing in the
pipeline writes, so its survival could only mean the entry wipe
stopped happening."""
_write_zero_instance_rig(tmp_path)
_stub_board_reading(monkeypatch)
out_dir = tmp_path / "out"
stale = _workdir_of(out_dir) / "a-previous-runs-leftover.dts.pre"
stale.parent.mkdir(parents=True)
stale.write_text("/* the run before this one */\n")
ret, _err = _run(
capsys,
[
"expand",
str(tmp_path / "rig.yml"),
"--out-dir",
str(out_dir),
*_no_shields(tmp_path),
"--board",
"some_board/soc/rig",
],
)
assert ret == 0
assert _workdir_of(out_dir).is_dir() # kept, per the ruling
assert not stale.exists() # but not this run's content
# --------------------------------------------------------------- --promote
def test_promote_writes_promote_shields_own_documents_verbatim(
tmp_path: Path, capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch
) -> None:
"""There is exactly ONE desugaring: --promote's materialized
rig.yml/content pair is byte-identical to promote.promote_shield's
own return, for the exact name/filename it chose. No --board is
given, so this run rejects at cli.py's own board-empty check: the
loader itself no longer needs a board to accept a promoted rig's
topology, so this is the first and only place a missing one still
matters. The workdir is kept on that reject, which is what lets
this test read the two files back off disk."""
from rigc.promote import promote_shield
out_dir = tmp_path / "out"
ret, _err = _run(
capsys,
[
"expand",
"--promote",
"adafruit_data_logger",
"--out-dir",
str(out_dir),
*_no_shields(tmp_path),
],
)
assert ret == 1
workdir = _workdir_of(out_dir)
expected = promote_shield("adafruit_data_logger")
assert (workdir / "rig.yml").read_text() == expected.rig_yml
assert (workdir / expected.content_name).read_text() == expected.content
def test_promote_with_revision_bakes_the_shields_own_revision(
tmp_path: Path, capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch
) -> None:
"""--revision alongside --promote means the SHIELD's own revision --
baked into the synthesized content file's `shield:` reference, never
forwarded to loader.load as a rig-level axis selection (a promoted
rig declares no revisions: of its own). Reused promote_shield
directly as the oracle, same as the test above."""
from rigc.promote import promote_shield
out_dir = tmp_path / "out"
ret, _err = _run(
capsys,
[
"expand",
"--promote",
"i2c_sensor",
"--revision",
"2",
"--out-dir",
str(out_dir),
*_no_shields(tmp_path),
],
)
assert ret == 1
workdir = _workdir_of(out_dir)
expected = promote_shield("i2c_sensor", revision="2")
assert (workdir / "rig.yml").read_text() == expected.rig_yml
assert (workdir / expected.content_name).read_text() == expected.content
assert "shield: i2c_sensor@2" in expected.content
def test_promote_with_a_dotted_config_opt_writes_a_config_block_verbatim(
tmp_path: Path, capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch
) -> None:
"""The end-to-end `--promote <shield>:config.<label>=<value>` path,
threaded through cli.py's own `--promote` branch
(args.promote.partition(":") -> parse_promotion_opts ->
promote_shield) rather than calling promote_shield directly:
proves `ParsedPromotionOpts.config` actually reaches the printed
text via THIS module's own plumbing, not merely via promote.py's own
printer in isolation. A MUTATION that routed `config.<label>=` to
`params` instead (or dropped `config=opts.config or None` from this
module's own promote_shield call) would make this fail on the
PRINTED BLOCK itself -- either as a `params:` block quoting `config`
as a device label, or as a missing `config:` block entirely."""
from rigc.promote import promote_shield
out_dir = tmp_path / "out"
ret, _err = _run(
capsys,
[
"expand",
"--promote",
"adafruit_winc1500:config.w_irq_jmp=D2",
"--out-dir",
str(out_dir),
*_no_shields(tmp_path),
],
)
assert ret == 1
workdir = _workdir_of(out_dir)
expected = promote_shield("adafruit_winc1500", config={"w_irq_jmp": "D2"})
assert (workdir / expected.content_name).read_text() == expected.content
assert " config:\n w_irq_jmp: D2\n" in expected.content
assert (workdir / "rig.yml").read_text() == expected.rig_yml
# --------------------------------------------------------------- logging
def test_stderr_carries_only_renderer_bytes_when_rigc_log_is_unset(
tmp_path: Path,
capsys: pytest.CaptureFixture[str],
caplog: pytest.LogCaptureFixture,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""The stderr-purity discipline test: with RIGC_LOG unset, the package
root's NullHandler (rigc/__init__.py) is the only handler on the
`rigc` tree, so a full main() run over a REJECTING input puts ONLY
the renderer's own bytes on stderr -- while caplog, which taps
Python's real root logger independently of our own (silent) handler,
still observes that the pipeline actually emitted records, proving
this is a "nothing configured" outcome rather than "nothing
happened"."""
monkeypatch.delenv("RIGC_LOG", raising=False)
caplog.set_level(logging.DEBUG, logger="rigc")
(tmp_path / "rig.yml").write_text("not-rig: {}\n")
ret, err = _run(
capsys,
[
"expand",
str(tmp_path / "rig.yml"),
"--out-dir",
str(tmp_path / "out"),
*_no_shields(tmp_path),
],
)
assert ret == 1
assert "error[lang-schema]" in err
# ONLY renderer bytes -- none of our own log formatting leaked in.
assert "INFO" not in err
assert "DEBUG" not in err
assert "rigc.cli" not in err
assert "rigc.loader" not in err
# ...yet the pipeline really did log: caplog observed the records via
# propagation, entirely independent of the (silent) handler above.
assert any("argv" in m for m in caplog.messages)
assert any(r.name == "rigc.loader" for r in caplog.records)
def test_rigc_log_env_attaches_a_real_stderr_handler(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""RIGC_LOG=<level> is one way a real handler reaches the `rigc`
logger tree (the other is -v/-vv, see below) -- read fresh at the top
of every main() call (not at import time), so this is observable
without a subprocess."""
monkeypatch.setenv("RIGC_LOG", "debug")
main(
[
"expand",
str(tmp_path / "no-such-rig.yml"),
"--out-dir",
str(tmp_path / "out"),
*_no_shields(tmp_path),
]
)
root = logging.getLogger("rigc")
real_handlers = [
h
for h in root.handlers
if isinstance(h, logging.StreamHandler) and not isinstance(h, logging.NullHandler)
]
assert len(real_handlers) == 1
assert root.getEffectiveLevel() == logging.DEBUG
# Deterministic cleanup -- a repeated main() call always re-derives
# its handler state from the CURRENT environment, so unsetting and
# calling _configure_logging() again leaves the logger tree exactly
# as every OTHER test in this module expects to find it.
monkeypatch.delenv("RIGC_LOG")
_configure_logging()
def _assert_verbose_flag_attaches_a_real_stderr_handler(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, flag: str, level: int
) -> None:
"""-v/-vv reach `_configure_logging` the same way RIGC_LOG does, with
no environment variable involved at all. A plain helper, not a
parametrized test -- tests/unit/ bans pytest markers outright
(test_layer_discipline.py), directory-classified only."""
monkeypatch.delenv("RIGC_LOG", raising=False)
main(
[
"expand",
str(tmp_path / "no-such-rig.yml"),
"--out-dir",
str(tmp_path / "out"),
*_no_shields(tmp_path),
flag,
]
)
root = logging.getLogger("rigc")
real_handlers = [
h
for h in root.handlers
if isinstance(h, logging.StreamHandler) and not isinstance(h, logging.NullHandler)
]
assert len(real_handlers) == 1
assert root.getEffectiveLevel() == level
_configure_logging() # same deterministic cleanup as the test above
def test_dash_v_attaches_a_real_stderr_handler_at_info(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
_assert_verbose_flag_attaches_a_real_stderr_handler(tmp_path, monkeypatch, "-v", logging.INFO)
def test_dash_vv_attaches_a_real_stderr_handler_at_debug(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
_assert_verbose_flag_attaches_a_real_stderr_handler(tmp_path, monkeypatch, "-vv", logging.DEBUG)
def test_verbose_flag_overrides_rigc_log(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""The CLI flag is the more explicit, per-invocation request, so it
wins when both are present -- here RIGC_LOG asks for DEBUG but -v
(INFO) is what the run actually gets."""
monkeypatch.setenv("RIGC_LOG", "debug")
main(
[
"expand",
str(tmp_path / "no-such-rig.yml"),
"--out-dir",
str(tmp_path / "out"),
*_no_shields(tmp_path),
"-v",
]
)
assert logging.getLogger("rigc").getEffectiveLevel() == logging.INFO
monkeypatch.delenv("RIGC_LOG")
_configure_logging()
def test_log_format_carries_a_timestamp_and_the_emitting_function(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
) -> None:
"""Every enabled record must be traceable to WHEN it fired and WHICH
function emitted it -- both are missing from a plain level+name
format and neither is recoverable after the fact."""
monkeypatch.delenv("RIGC_LOG", raising=False)
main(
[
"expand",
str(tmp_path / "no-such-rig.yml"),
"--out-dir",
str(tmp_path / "out"),
*_no_shields(tmp_path),
"-v",
]
)
err = capsys.readouterr().err
assert "rigc.cli:main" in err # module:function
assert re.search(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}", err)
_configure_logging()