supervision/tests/cv2/test_integration.py

115 lines
3.6 KiB
Python

"""Integration checks for the OpenCV compatibility boundary."""
from __future__ import annotations
import ast
import os
import subprocess
import sys
from pathlib import Path
import numpy as np
from supervision._cv2._image import _add_weighted
PROJECT_ROOT = Path(__file__).resolve().parents[2]
SOURCE_ROOT = PROJECT_ROOT / "src" / "supervision"
TEST_ROOT = PROJECT_ROOT / "tests"
def _direct_cv2_imports(root: Path, excluded: set[Path] | None = None) -> list[str]:
"""Return direct cv2 import locations under a source or test tree."""
excluded = excluded or set()
imports: list[str] = []
for path in sorted(root.rglob("*.py")):
if path in excluded:
continue
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
for node in ast.walk(tree):
is_direct_import = isinstance(node, ast.Import) and any(
alias.name == "cv2" for alias in node.names
)
is_direct_from_import = (
isinstance(node, ast.ImportFrom) and node.module == "cv2"
)
if is_direct_import or is_direct_from_import:
relative_path = path.relative_to(PROJECT_ROOT).as_posix()
imports.append(f"{relative_path}:{node.lineno}")
return imports
def _blocked_cv2_environment(tmp_path: Path) -> dict[str, str]:
"""Create a subprocess environment that rejects every cv2 import."""
blocker = tmp_path / "sitecustomize.py"
blocker.write_text(
"""import sys
class BlockCv2:
def find_spec(self, fullname, path=None, target=None):
if fullname == "cv2":
raise ModuleNotFoundError("blocked for integration test")
return None
sys.meta_path.insert(0, BlockCv2())
""",
encoding="utf-8",
)
environment = os.environ.copy()
python_path = [str(tmp_path), str(PROJECT_ROOT / "src")]
if existing_path := environment.get("PYTHONPATH"):
python_path.append(existing_path)
environment["PYTHONPATH"] = os.pathsep.join(python_path)
return environment
def test_production_imports_cv2_only_through_facade() -> None:
"""Keep native OpenCV imports inside the private facade module."""
imports = _direct_cv2_imports(SOURCE_ROOT)
assert all(
location.startswith("src/supervision/_cv2/__init__.py:") for location in imports
)
def test_ordinary_tests_use_facade_instead_of_native_cv2() -> None:
"""Keep ordinary fixtures and regression tests runnable without OpenCV."""
reference_root = TEST_ROOT / "cv2"
imports = _direct_cv2_imports(TEST_ROOT, excluded=set(reference_root.rglob("*.py")))
assert imports == []
def test_fallback_add_weighted_accepts_opencv_keyword_names() -> None:
"""Accept OpenCV's public `src1` and `src2` parameter names."""
source = np.array([[0, 100], [200, 255]], dtype=np.uint8)
other = np.full_like(source, 50)
actual = _add_weighted(src1=source, alpha=0.5, src2=other, beta=0.5, gamma=10)
expected = _add_weighted(source, 0.5, other, 0.5, 10)
np.testing.assert_array_equal(actual, expected)
def test_ordinary_suite_passes_when_cv2_is_blocked(tmp_path: Path) -> None:
"""Run all non-reference tests in a process where cv2 cannot be imported."""
completed = subprocess.run(
[
sys.executable,
"-m",
"pytest",
"tests",
"--ignore=tests/cv2",
"-q",
"--disable-warnings",
],
cwd=PROJECT_ROOT,
env=_blocked_cv2_environment(tmp_path),
capture_output=True,
text=True,
timeout=180,
)
assert completed.returncode == 0, completed.stdout + completed.stderr