Files
C.E.L_Slide_test2/tests/test_phase_z2_cli_overrides.py

349 lines
11 KiB
Python

"""IMP-51 (#79) u5 — focused tests for the ``--override-image`` CLI surface.
Stage 2 u5 scope (per the Exit Report):
- Successful parse: single flag + multiple flags accumulate.
- Forwarding: parsed mapping reaches ``run_phase_z2_mvp1`` as
``override_image_overrides={image_id: {"x", "y", "w", "h"}}``.
- Empty payload: omitting ``--override-image`` forwards ``None``
(CLI ``or None`` collapse, sibling pattern to other axes).
- Hard-error cases (each must ``sys.exit(2)`` with a stderr message):
* missing ``=``
* empty ``IMAGE_ID``
* duplicate ``IMAGE_ID``
* wrong float count (not 4)
* non-numeric float component
The harness mirrors ``tests/test_user_overrides_pipeline_fallback.py`` —
the ``if __name__ == "__main__"`` block of ``src.phase_z2_pipeline`` is
exec'd inside the module's namespace after monkeypatching
``run_phase_z2_mvp1`` with a recording stub. This exercises the actual
production parser without invoking the real pipeline.
The persistence fallback is silenced by redirecting
``src.user_overrides_io.DEFAULT_OVERRIDES_ROOT`` to a clean tmp directory
so persisted state from prior runs cannot bleed into the parser-only
assertions here.
"""
from __future__ import annotations
import ast
import sys
from pathlib import Path
from typing import Any
import pytest
import src.phase_z2_pipeline as _pz2
import src.user_overrides_io as _io
# -- harness ---------------------------------------------------------------
def _exec_main_block(
captured: dict[str, Any], argv: list[str], monkeypatch
) -> None:
"""Run the ``__main__`` body of phase_z2_pipeline.py with a fake
``run_phase_z2_mvp1`` so its kwargs are observable."""
def _fake_run(
mdx_path,
run_id,
*,
override_layout=None,
override_frames=None,
override_zone_geometries=None,
override_section_assignments=None,
override_image_overrides=None,
):
captured["mdx_path"] = mdx_path
captured["run_id"] = run_id
captured["override_layout"] = override_layout
captured["override_frames"] = override_frames
captured["override_zone_geometries"] = override_zone_geometries
captured["override_section_assignments"] = override_section_assignments
captured["override_image_overrides"] = override_image_overrides
monkeypatch.setattr(_pz2, "run_phase_z2_mvp1", _fake_run)
monkeypatch.setattr(sys, "argv", argv)
src_path = Path(_pz2.__file__)
source = src_path.read_text(encoding="utf-8")
tree = ast.parse(source)
for node in tree.body:
if (
isinstance(node, ast.If)
and isinstance(node.test, ast.Compare)
and isinstance(node.test.left, ast.Name)
and node.test.left.id == "__name__"
):
block = ast.Module(body=node.body, type_ignores=[])
exec(compile(block, str(src_path), "exec"), _pz2.__dict__)
return
raise AssertionError("no `if __name__ == '__main__'` block found")
def _redirect_overrides_root(tmp_path: Path, monkeypatch) -> None:
"""Isolate the persistence fallback so file state never leaks in."""
monkeypatch.setattr(_io, "DEFAULT_OVERRIDES_ROOT", tmp_path)
# -- success paths --------------------------------------------------------
def test_no_image_override_forwards_none(tmp_path, monkeypatch):
"""When ``--override-image`` is omitted, the kwarg must be ``None``
(the parser's accumulator stays empty → ``overrides_images or None``)."""
_redirect_overrides_root(tmp_path, monkeypatch)
captured: dict[str, Any] = {}
_exec_main_block(captured, ["src.phase_z2_pipeline", "03.mdx"], monkeypatch)
assert captured["override_image_overrides"] is None
def test_single_image_override_parses_and_forwards(tmp_path, monkeypatch):
_redirect_overrides_root(tmp_path, monkeypatch)
captured: dict[str, Any] = {}
_exec_main_block(
captured,
[
"src.phase_z2_pipeline",
"03.mdx",
"--override-image",
"img-abc=10,15,30.5,25",
],
monkeypatch,
)
assert captured["override_image_overrides"] == {
"img-abc": {"x": 10.0, "y": 15.0, "w": 30.5, "h": 25.0},
}
def test_multiple_image_overrides_accumulate(tmp_path, monkeypatch):
_redirect_overrides_root(tmp_path, monkeypatch)
captured: dict[str, Any] = {}
_exec_main_block(
captured,
[
"src.phase_z2_pipeline",
"03.mdx",
"--override-image",
"img-abc=10,15,30,25",
"--override-image",
"img-def=50,15,40,40",
],
monkeypatch,
)
assert captured["override_image_overrides"] == {
"img-abc": {"x": 10.0, "y": 15.0, "w": 30.0, "h": 25.0},
"img-def": {"x": 50.0, "y": 15.0, "w": 40.0, "h": 40.0},
}
def test_image_override_strips_whitespace_in_image_id(tmp_path, monkeypatch):
"""``iid.strip()`` is intentional — match sibling --override-frame and
--override-zone-geometry leniency on surrounding whitespace."""
_redirect_overrides_root(tmp_path, monkeypatch)
captured: dict[str, Any] = {}
_exec_main_block(
captured,
[
"src.phase_z2_pipeline",
"03.mdx",
"--override-image",
" img-pad =5,5,10,10",
],
monkeypatch,
)
assert captured["override_image_overrides"] == {
"img-pad": {"x": 5.0, "y": 5.0, "w": 10.0, "h": 10.0},
}
# -- hard-error paths -----------------------------------------------------
def test_image_override_missing_equals_exits(tmp_path, monkeypatch, capsys):
_redirect_overrides_root(tmp_path, monkeypatch)
captured: dict[str, Any] = {}
with pytest.raises(SystemExit) as excinfo:
_exec_main_block(
captured,
[
"src.phase_z2_pipeline",
"03.mdx",
"--override-image",
"img-abc10,15,30,25",
],
monkeypatch,
)
assert excinfo.value.code == 2
err = capsys.readouterr().err
assert "--override-image must be IMAGE_ID=X,Y,W,H" in err
def test_image_override_empty_image_id_exits(tmp_path, monkeypatch, capsys):
_redirect_overrides_root(tmp_path, monkeypatch)
captured: dict[str, Any] = {}
with pytest.raises(SystemExit) as excinfo:
_exec_main_block(
captured,
[
"src.phase_z2_pipeline",
"03.mdx",
"--override-image",
"=10,15,30,25",
],
monkeypatch,
)
assert excinfo.value.code == 2
err = capsys.readouterr().err
assert "IMAGE_ID must be non-empty" in err
def test_image_override_whitespace_only_image_id_exits(
tmp_path, monkeypatch, capsys
):
"""``iid.strip()`` must collapse whitespace-only IDs into the empty-ID
error path (otherwise a spurious key would land in the mapping)."""
_redirect_overrides_root(tmp_path, monkeypatch)
captured: dict[str, Any] = {}
with pytest.raises(SystemExit) as excinfo:
_exec_main_block(
captured,
[
"src.phase_z2_pipeline",
"03.mdx",
"--override-image",
" =10,15,30,25",
],
monkeypatch,
)
assert excinfo.value.code == 2
err = capsys.readouterr().err
assert "IMAGE_ID must be non-empty" in err
def test_image_override_duplicate_image_id_exits(
tmp_path, monkeypatch, capsys
):
_redirect_overrides_root(tmp_path, monkeypatch)
captured: dict[str, Any] = {}
with pytest.raises(SystemExit) as excinfo:
_exec_main_block(
captured,
[
"src.phase_z2_pipeline",
"03.mdx",
"--override-image",
"img-abc=10,15,30,25",
"--override-image",
"img-abc=20,25,30,35",
],
monkeypatch,
)
assert excinfo.value.code == 2
err = capsys.readouterr().err
assert "duplicate IMAGE_ID 'img-abc'" in err
def test_image_override_wrong_float_count_exits(
tmp_path, monkeypatch, capsys
):
_redirect_overrides_root(tmp_path, monkeypatch)
captured: dict[str, Any] = {}
with pytest.raises(SystemExit) as excinfo:
_exec_main_block(
captured,
[
"src.phase_z2_pipeline",
"03.mdx",
"--override-image",
"img-abc=10,15,30",
],
monkeypatch,
)
assert excinfo.value.code == 2
err = capsys.readouterr().err
assert "expects 4 floats X,Y,W,H" in err
def test_image_override_too_many_floats_exits(
tmp_path, monkeypatch, capsys
):
_redirect_overrides_root(tmp_path, monkeypatch)
captured: dict[str, Any] = {}
with pytest.raises(SystemExit) as excinfo:
_exec_main_block(
captured,
[
"src.phase_z2_pipeline",
"03.mdx",
"--override-image",
"img-abc=10,15,30,25,99",
],
monkeypatch,
)
assert excinfo.value.code == 2
err = capsys.readouterr().err
assert "expects 4 floats X,Y,W,H" in err
def test_image_override_non_numeric_value_exits(
tmp_path, monkeypatch, capsys
):
_redirect_overrides_root(tmp_path, monkeypatch)
captured: dict[str, Any] = {}
with pytest.raises(SystemExit) as excinfo:
_exec_main_block(
captured,
[
"src.phase_z2_pipeline",
"03.mdx",
"--override-image",
"img-abc=10,abc,30,25",
],
monkeypatch,
)
assert excinfo.value.code == 2
err = capsys.readouterr().err
assert "floats parse fail" in err
# -- isolation guard ------------------------------------------------------
def test_image_override_does_not_leak_into_sibling_axes(tmp_path, monkeypatch):
"""A populated image override must not perturb the other four axes."""
_redirect_overrides_root(tmp_path, monkeypatch)
captured: dict[str, Any] = {}
_exec_main_block(
captured,
[
"src.phase_z2_pipeline",
"03.mdx",
"--override-image",
"img-abc=10,15,30,25",
],
monkeypatch,
)
assert captured["override_image_overrides"] == {
"img-abc": {"x": 10.0, "y": 15.0, "w": 30.0, "h": 25.0},
}
assert captured["override_layout"] is None
assert captured["override_frames"] is None
assert captured["override_zone_geometries"] is None
assert captured["override_section_assignments"] is None