diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7e70688..c07f08e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,5 +51,8 @@ jobs: - name: Outfit lab tests working-directory: games/ultima-online/outfit-lab run: python -m unittest -q + # Blender matrix: neither job installs Blender and SPRITEMOTION_BLENDER is unset, so the tests marked + # `requires_blender` (tests/integration/test_blender_loop.py) report as skipped here. They run on a + # machine with Blender 4.2 or newer: local gates, or a runner that sets SPRITEMOTION_BLENDER. # Hosted runners do not prove proprietary-model renders or in-game fit. # Those require local acceptance evidence in the PR. diff --git a/tests/conftest.py b/tests/conftest.py index 646cbb4..d47238f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -19,6 +19,20 @@ _spec.loader.exec_module(sys.modules["spritemotion"]) +def blender_exe() -> str | None: + """The Blender executable (SPRITEMOTION_BLENDER first, see spritemotion.blender), or None when there is none.""" + from spritemotion.blender import find_blender + try: + exe = find_blender() + except RuntimeError: + return None + return exe if Path(exe).exists() else None + + +# Every test that starts Blender carries this mark; the CI jobs that have no Blender report them as skipped. +requires_blender = pytest.mark.skipif(blender_exe() is None, reason="Blender not found (set SPRITEMOTION_BLENDER)") + + def load_module(path: Path, name: str): """Import a script that lives outside the package (game adapters, examples, migrations).""" spec = importlib.util.spec_from_file_location(name, path) diff --git a/tests/integration/test_blender_loop.py b/tests/integration/test_blender_loop.py index b873d77..6ffb6ea 100644 --- a/tests/integration/test_blender_loop.py +++ b/tests/integration/test_blender_loop.py @@ -10,23 +10,13 @@ import pytest -from spritemotion.blender import find_blender from spritemotion.jsonio import read_json from spritemotion.pipeline.cli import main as cli -from conftest import REPO, SAMPLE +from conftest import REPO, SAMPLE, blender_exe, requires_blender -def locate_blender() -> str | None: - try: - exe = find_blender() - except RuntimeError: - return None - return exe if Path(exe).exists() else None - - -BLENDER = locate_blender() -pytestmark = pytest.mark.skipif(BLENDER is None, reason="Blender not found (set SPRITEMOTION_BLENDER)") +BLENDER = blender_exe() def blender(*args, blend: Path | None = None) -> str: @@ -36,11 +26,13 @@ def blender(*args, blend: Path | None = None) -> str: return done.stdout +@requires_blender def test_blender_smoke(tmp_path): out = blender(REPO / "tools" / "blender" / "tests" / "smoke_test.py", "--", "--out", tmp_path) assert "6/6 Blender checks passed" in out, out[-2000:] +@requires_blender def test_sample_loop(tmp_path): mapping = SAMPLE / "rig" / "sample-mapping.json" blend, rig, fit = tmp_path / "sample.blend", tmp_path / "rig.json", tmp_path / "fit.json" @@ -63,6 +55,7 @@ def test_sample_loop(tmp_path): assert min(summary["by_direction"].values()) > 0.85 +@requires_blender def test_rekeying_keeps_earlier_passes(tmp_path): """A second pass saved over the same scene keeps the first as a file version and as an action.""" from spritemotion.pipeline import versions diff --git a/tests/integration/test_repository.py b/tests/integration/test_repository.py index 41ef104..131fd57 100644 --- a/tests/integration/test_repository.py +++ b/tests/integration/test_repository.py @@ -207,3 +207,14 @@ def test_code_has_no_hardcoded_windows_font_or_blender_paths(): if HARDCODED_TOOL_PATH.search(line): offenders.append(f"{rel}:{number}: {line.strip()[:100]}") assert offenders == [] + + +def test_every_test_that_starts_blender_is_marked_requires_blender(): + """A machine without Blender must skip those tests with a reason, not fail them (see the CI note in ci.yml).""" + for path in sorted((REPO / "tests").rglob("test_*.py")): + text = path.read_text(encoding="utf-8").replace("\r\n", "\n") + if path.name == Path(__file__).name or "--factory-startup" not in text: + continue + tests = len(re.findall(r"^def test_", text, re.MULTILINE)) + marked = len(re.findall(r"^@requires_blender\ndef test_", text, re.MULTILINE)) + assert tests == marked, f"{path.name}: {tests - marked} test(s) start Blender without @requires_blender"