109 lines
3.8 KiB
Python
109 lines
3.8 KiB
Python
"""Shared pytest configuration and fixtures for the NeuroPose test suite."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import os
|
||
from collections.abc import Iterator
|
||
from pathlib import Path
|
||
from typing import Any
|
||
|
||
import cv2
|
||
import numpy as np
|
||
import pytest
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Environment isolation
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
@pytest.fixture(autouse=True)
|
||
def _isolate_environment(
|
||
monkeypatch: pytest.MonkeyPatch,
|
||
tmp_path_factory: pytest.TempPathFactory,
|
||
) -> Iterator[None]:
|
||
"""Isolate every test from the developer's real home directory.
|
||
|
||
Points ``$HOME`` and ``$XDG_DATA_HOME`` at per-test temp directories so
|
||
that any code path that uses the default ``Settings()`` (which reaches
|
||
into ``~/.local/share/neuropose``) cannot accidentally write to the real
|
||
machine. Also clears any ``NEUROPOSE_*`` environment variables that may
|
||
be set in the developer's shell, so test behaviour does not depend on
|
||
who is running the test suite.
|
||
"""
|
||
isolated = tmp_path_factory.mktemp("neuropose_env_isolation")
|
||
monkeypatch.setenv("HOME", str(isolated))
|
||
monkeypatch.setenv("XDG_DATA_HOME", str(isolated / "xdg"))
|
||
for key in list(os.environ):
|
||
if key.startswith("NEUROPOSE_"):
|
||
monkeypatch.delenv(key, raising=False)
|
||
yield
|
||
|
||
|
||
@pytest.fixture
|
||
def xdg_home() -> Path:
|
||
"""Return the isolated ``$XDG_DATA_HOME`` set up by ``_isolate_environment``."""
|
||
return Path(os.environ["XDG_DATA_HOME"])
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Synthetic video + fake MeTRAbs model
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
@pytest.fixture
|
||
def synthetic_video(tmp_path: Path) -> Path:
|
||
"""Generate a tiny synthetic video at test time.
|
||
|
||
The fixture writes a 5-frame, 32×32 MJPG-encoded ``.avi`` file. MJPG is
|
||
chosen over ``mp4v`` because it ships with ``opencv-python-headless`` on
|
||
every platform we target, whereas ``mp4v`` occasionally requires an
|
||
ffmpeg binary that may not be present on minimal CI runners.
|
||
"""
|
||
path = tmp_path / "synthetic.avi"
|
||
fourcc = cv2.VideoWriter_fourcc(*"MJPG")
|
||
writer = cv2.VideoWriter(str(path), fourcc, 30.0, (32, 32))
|
||
assert writer.isOpened(), "cv2.VideoWriter failed to open; MJPG codec missing?"
|
||
for i in range(5):
|
||
# Distinct brightness per frame so a downstream check could verify
|
||
# the test is actually reading frame-by-frame.
|
||
frame = np.full((32, 32, 3), i * 40, dtype=np.uint8)
|
||
writer.write(frame)
|
||
writer.release()
|
||
assert path.exists() and path.stat().st_size > 0, "Synthetic video is empty."
|
||
return path
|
||
|
||
|
||
class _FakeMetrabsModel:
|
||
"""Minimal stand-in for the MeTRAbs model used in unit tests.
|
||
|
||
Returns deterministic pose data (one person, two joints) per call so
|
||
tests can assert on shapes without importing TensorFlow or downloading
|
||
the real model. The returned arrays are plain numpy so the estimator's
|
||
``_to_nested_list`` helper exercises its non-``numpy()`` branch.
|
||
"""
|
||
|
||
def __init__(self) -> None:
|
||
self.call_count = 0
|
||
|
||
def detect_poses(
|
||
self,
|
||
image: Any,
|
||
*,
|
||
default_fov_degrees: float,
|
||
skeleton: str,
|
||
) -> dict[str, np.ndarray]:
|
||
del image, default_fov_degrees, skeleton # signature-compatible with MeTRAbs
|
||
self.call_count += 1
|
||
return {
|
||
"boxes": np.array([[0.0, 0.0, 32.0, 32.0, 0.95]]),
|
||
"poses3d": np.array([[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]]),
|
||
"poses2d": np.array([[[10.0, 20.0], [30.0, 40.0]]]),
|
||
}
|
||
|
||
|
||
@pytest.fixture
|
||
def fake_metrabs_model() -> _FakeMetrabsModel:
|
||
"""Return a fresh fake MeTRAbs model instance for a single test."""
|
||
return _FakeMetrabsModel()
|