143 lines
5.0 KiB
Python
143 lines
5.0 KiB
Python
"""Guards for the pytest harness itself.
|
|
|
|
``tests/conftest.py`` was accidentally deleted in commit edc6e42 while four test
|
|
modules still depended on its fixtures, which made the suite unrunnable without
|
|
anything failing loudly in CI. These tests turn that class of regression into an
|
|
explicit failure, and also catch a stale ``orisys`` install silently shadowing
|
|
``src/orisys`` (a real hazard: base/py312 environments have older orisys copies
|
|
in site-packages).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import functools
|
|
import importlib.util
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
CONFTEST_PATH = Path(__file__).resolve().parent / "conftest.py"
|
|
|
|
# Names that must exist in tests/conftest.py.
|
|
REQUIRED_CONFTEST_NAMES = (
|
|
"DEFAULT_VIDEO_CANDIDATES",
|
|
"make_sensor",
|
|
"test_video_path",
|
|
"test_config_name",
|
|
"available_backends",
|
|
"sensor_factory",
|
|
)
|
|
|
|
# Fixtures that must be resolvable through pytest.
|
|
REQUIRED_FIXTURES = (
|
|
"test_video_path",
|
|
"test_config_name",
|
|
"available_backends",
|
|
"sensor_factory",
|
|
)
|
|
|
|
|
|
@functools.lru_cache(maxsize=1)
|
|
def _load_conftest_module():
|
|
"""Load tests/conftest.py by path so a deletion is observable here."""
|
|
spec = importlib.util.spec_from_file_location("_orisys_conftest_probe", CONFTEST_PATH)
|
|
if spec is None or spec.loader is None:
|
|
raise AssertionError(f"Cannot load {CONFTEST_PATH}")
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
def test_conftest_defines_shared_fixtures():
|
|
assert CONFTEST_PATH.is_file(), (
|
|
f"{CONFTEST_PATH} is missing. test_backends.py, test_smoke_api.py, "
|
|
"test_errors_and_cleanup.py and test_longrun_stability.py depend on its "
|
|
"fixtures; it was accidentally deleted in commit edc6e42. Restore it "
|
|
"instead of deleting it."
|
|
)
|
|
|
|
module = _load_conftest_module()
|
|
missing = [
|
|
name for name in REQUIRED_CONFTEST_NAMES if getattr(module, name, None) is None
|
|
]
|
|
assert not missing, (
|
|
f"tests/conftest.py no longer defines: {', '.join(missing)}. "
|
|
"The shared sensor-test fixtures are required by the suite."
|
|
)
|
|
|
|
|
|
def test_default_test_video_is_reachable():
|
|
"""At least one default candidate must exist, or every sensor test skips."""
|
|
module = _load_conftest_module()
|
|
candidates = [REPO_ROOT / Path(rel) for rel in module.DEFAULT_VIDEO_CANDIDATES]
|
|
existing = [path for path in candidates if path.is_file()]
|
|
assert existing, (
|
|
"None of the default test videos exist:\n "
|
|
+ "\n ".join(str(path) for path in candidates)
|
|
+ "\nCommit a sample clip under data/sample/ or set ORISYS_TEST_VIDEO."
|
|
)
|
|
|
|
|
|
def test_fixtures_are_registered_with_pytest(request):
|
|
missing: list[str] = []
|
|
for name in REQUIRED_FIXTURES:
|
|
try:
|
|
request.getfixturevalue(name)
|
|
except pytest.FixtureLookupError:
|
|
missing.append(name)
|
|
assert not missing, (
|
|
f"pytest did not register these fixtures: {', '.join(missing)}. "
|
|
"Check that tests/conftest.py is present and the @pytest.fixture "
|
|
"decorators are intact."
|
|
)
|
|
|
|
|
|
def test_import_precedence_is_correct():
|
|
"""examples/ must beat build/ for `gui`; build/ must beat site-packages.
|
|
|
|
Regression guard: build/gui is a package named `gui`, so if build/ precedes
|
|
examples/ on sys.path then `import gui.viewer2d` resolves to the build
|
|
tooling and tests/test_qt_viewer_backend.py fails at collection.
|
|
"""
|
|
import common
|
|
import gui
|
|
import wheel
|
|
|
|
examples_gui = (REPO_ROOT / "examples" / "gui").resolve()
|
|
build_root = (REPO_ROOT / "build").resolve()
|
|
|
|
assert Path(gui.__file__).resolve().parent == examples_gui, (
|
|
f"`gui` resolved to {gui.__file__} instead of {examples_gui}; "
|
|
"examples/ must precede build/ on sys.path"
|
|
)
|
|
for module in (common, wheel):
|
|
resolved = Path(module.__file__).resolve()
|
|
assert build_root in resolved.parents, (
|
|
f"`{module.__name__}` resolved to {resolved}, outside {build_root}; "
|
|
"build/ must precede site-packages on sys.path"
|
|
)
|
|
|
|
|
|
def test_tests_run_against_checkout_not_stale_install():
|
|
"""Fail loudly when an old orisys in site-packages shadows src/orisys."""
|
|
src_pkg = (REPO_ROOT / "src" / "orisys").resolve()
|
|
if not src_pkg.is_dir():
|
|
pytest.skip("src/orisys is not present in this checkout layout")
|
|
|
|
import orisys
|
|
|
|
module_file = getattr(orisys, "__file__", None)
|
|
assert module_file, (
|
|
"import orisys resolved to a namespace package (__file__ is None) "
|
|
f"instead of {src_pkg}. A stale orisys in site-packages is shadowing "
|
|
"the checkout; run `pip uninstall -y orisys` in this environment."
|
|
)
|
|
|
|
resolved_parent = Path(module_file).resolve().parent
|
|
assert resolved_parent == src_pkg, (
|
|
f"import orisys resolved to {resolved_parent} instead of {src_pkg}. "
|
|
"A stale orisys in site-packages is shadowing the checkout; run "
|
|
"`pip uninstall -y orisys` in this environment."
|
|
)
|