Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
14 changes: 14 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
17 changes: 5 additions & 12 deletions tests/integration/test_blender_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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"
Expand All @@ -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
Expand Down
11 changes: 11 additions & 0 deletions tests/integration/test_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Comment on lines +218 to +219

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Count class-based Blender tests in the guard

When a Blender test is added as a class method—a test style already used in tests/test_pose_editor.py—the ^def test_ pattern ignores the indented method, so this guard can pass even if the method lacks @requires_blender. Because the module-level pytestmark was removed, that test would run on Blender-free CI and call the helper with no executable instead of being skipped; inspect collected/AST test functions or retain a module-level mark.

Useful? React with 👍 / 👎.

assert tests == marked, f"{path.name}: {tests - marked} test(s) start Blender without @requires_blender"
Loading