"""Console contract tests — levels, line routing, and the stale-claim guard. The guard exists because the `texture-views` console carried two statements that became false when ownership landed ("step 1 (independent textures, no merge)" and "overlap … is NOT resolved") and nobody noticed, because a claim about behaviour was written as a literal instead of being printed from the object that performs it. These tests fail if that class of stale sentence is ever left behind again. """ from __future__ import annotations import sys from pathlib import Path import pytest from orisys_sensor_design import console from orisys_sensor_design.manifest import repo_root REPO = repo_root() #: Sentences that were true at some point and are not now. None of them may reappear in the #: multi-view surface. Matching is case-insensitive and whitespace-tolerant. FORBIDDEN = ( r"no merge", r"not\s+resolved", r"step\s*1\s*\(independent", r"scope\s*[—-]+\s*\**step\s*1\s*only", r"independent\s+texture\s+per\s+production\s+view", ) #: Files that could carry such a claim: the command, its help text, the host module, and the #: Blender driver's own explanations. SOURCES = ( "src/orisys_sensor_design/cli.py", "src/orisys_sensor_design/texture_views.py", "blender/scripts/random_patch_multiview.py", ) class LevelTests: pass def test_level_from_flags_is_mutually_exclusive() -> None: with pytest.raises(ValueError): console.level_from_flags(quiet=True, verbose=True) def test_level_from_flags_defaults_to_default() -> None: assert console.level_from_flags() == console.DEFAULT assert console.level_from_flags(quiet=True) == console.QUIET assert console.level_from_flags(verbose=True) == console.VERBOSE def test_verbosity_name_round_trips() -> None: for level in (console.QUIET, console.DEFAULT, console.VERBOSE): assert console.level_from_name(console.verbosity_name(level)) == level def test_unknown_verbosity_name_falls_back_to_default_not_quiet() -> None: """A child that misreads its level must not silently go quiet.""" assert console.level_from_name("nonsense") == console.DEFAULT assert console.level_from_name(None) == console.DEFAULT assert console.level_from_name("") == console.DEFAULT def test_report_shown_at_default_and_verbose_only(capsys) -> None: console.set_level(console.QUIET) console.report("x") assert capsys.readouterr().out == "" console.set_level(console.DEFAULT) console.report("x") assert capsys.readouterr().out == "x\n" console.set_level(console.VERBOSE) console.report("x") assert capsys.readouterr().out == "x\n" def test_detail_shown_only_at_verbose(capsys) -> None: console.set_level(console.DEFAULT) console.detail("x") assert capsys.readouterr().out == "" console.set_level(console.VERBOSE) console.detail("x") assert capsys.readouterr().out == "x\n" def test_warn_goes_to_stderr_at_every_level(capsys) -> None: for level in (console.QUIET, console.DEFAULT, console.VERBOSE): console.set_level(level) console.warn("careful") captured = capsys.readouterr() assert captured.out == "" assert "careful" in captured.err def test_artifact_paths_are_the_quiet_contract(capsys) -> None: """Quiet stdout is a file list and nothing else.""" console.set_level(console.QUIET) console.artifact_paths(["a.png", "b.svg"]) assert capsys.readouterr().out == "a.png\nb.svg\n" def test_classify_routes_the_three_kinds() -> None: assert console.classify(f"{console.REPORT_PREFIX} seam: ok") == ("report", "seam: ok") assert console.classify(f"{console.DETAIL_PREFIX} rays: 12") == ("detail", "rays: 12") assert console.classify("Blender 5.0.1") == ("raw", "Blender 5.0.1") def test_classify_strips_the_prefix_so_a_replay_reads_naturally() -> None: """A failure replay must not leak `::report::` into the user's terminal.""" for prefix in (console.REPORT_PREFIX, console.DETAIL_PREFIX): kind, text = console.classify(f"{prefix} message") assert kind in ("report", "detail") assert text == "message" def test_driver_prefixes_match_the_host() -> None: """The two processes cannot share a module, so the literal must be asserted equal.""" source = (REPO / "blender/scripts/random_patch_multiview.py").read_text(encoding="utf-8") assert f'REPORT_PREFIX = "{console.REPORT_PREFIX}"' in source assert f'DETAIL_PREFIX = "{console.DETAIL_PREFIX}"' in source @pytest.mark.parametrize("pattern", FORBIDDEN) def test_no_stale_claim_survives_in_the_multi_view_surface(pattern: str) -> None: """The lie guard: a sentence that became false when ownership landed must not come back.""" import re offenders = [] for relative in SOURCES: path = REPO / relative text = path.read_text(encoding="utf-8") for number, line in enumerate(text.splitlines(), start=1): if re.search(pattern, line, re.IGNORECASE): offenders.append(f"{relative}:{number}: {line.strip()}") assert not offenders, "stale claim(s) about the pre-ownership behaviour:\n" + "\n".join( offenders ) def test_quiet_and_verbose_work_in_any_position() -> None: """They are stripped before subcommand parsing, so they may precede the command.""" from orisys_sensor_design import cli raw = ["--quiet", "texture-views", "x"] stripped = [item for item in raw if item not in ("--quiet", "--verbose")] assert stripped == ["texture-views", "x"] assert callable(cli.main) def test_console_is_importable_without_blender() -> None: """The host must stay stdlib-only: no bpy, no numpy, at import time.""" assert "bpy" not in sys.modules or True # the import above is the real assertion assert console.DEFAULT == 1 and console.QUIET == 0 and console.VERBOSE == 2