feat(#79): IMP-51 image_overrides axis (u1~u11 backend stamp+CLI+CSS inject + frontend drag/resize+persistence + tests)
This commit is contained in:
348
tests/test_phase_z2_cli_overrides.py
Normal file
348
tests/test_phase_z2_cli_overrides.py
Normal file
@@ -0,0 +1,348 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user