update test and build scripts

This commit is contained in:
ykcheungab
2026-03-09 13:33:18 +08:00
parent 36118ab9cd
commit fa76d66162
19 changed files with 472 additions and 234 deletions
+116
View File
@@ -0,0 +1,116 @@
import os
import sys
from pathlib import Path
import pytest
REPO_ROOT = Path(__file__).resolve().parents[1]
SRC_ROOT = REPO_ROOT / "src"
if str(SRC_ROOT) not in sys.path:
sys.path.insert(0, str(SRC_ROOT))
from orisys import Sensor
from orisys import configs as orisys_configs
from orisys.backend_availability import check_cupy_available, check_opencl_available
def _resolve_test_video() -> Path:
env_video = os.getenv("ORISYS_TEST_VIDEO")
if env_video:
raw = Path(env_video)
candidates = [
raw, # as provided (absolute or cwd-relative)
REPO_ROOT / raw, # repo-relative
]
# Common typo fallback: "test/..." -> "tests/..."
if env_video.startswith("test/") or env_video.startswith("test\\"):
fixed = Path("tests", *Path(env_video).parts[1:])
candidates.append(REPO_ROOT / fixed)
for candidate in candidates:
if candidate.exists():
return candidate
return raw
# Prefer tests/ fixture clip; fallback kept by caller exists check.
return REPO_ROOT / "tests" / "idle_1min.mp4"
@pytest.fixture(scope="session")
def test_video_path() -> Path:
path = _resolve_test_video()
if not path.exists():
pytest.skip(
"Test video not found. Set ORISYS_TEST_VIDEO to a valid video path."
)
return path
@pytest.fixture(scope="session")
def test_config_name() -> str:
requested = os.getenv("ORISYS_TEST_CONFIG")
def _is_usable_config(name: str) -> bool:
cfg = orisys_configs.load_config(name)
return "box_size" in cfg.get("stitcher", {})
if requested:
if not _is_usable_config(requested):
raise RuntimeError(
f"ORISYS_TEST_CONFIG='{requested}' is missing required key "
"'stitcher.box_size'. Use a config like 'ddjx01' or provide a full path."
)
return requested
# Auto-pick a stable config for tests (avoid prototype variants missing keys).
for name in orisys_configs.list_config():
try:
if _is_usable_config(name):
return name
except Exception:
continue
raise RuntimeError(
"No usable config found for tests (missing 'stitcher.box_size'). "
"Set ORISYS_TEST_CONFIG to a valid config JSON/path."
)
@pytest.fixture(scope="session")
def available_backends() -> list[str]:
backends = ["cpu"]
if check_cupy_available():
backends.append("cupy")
if check_opencl_available():
backends.append("opencl")
return backends
def make_sensor(
video_path: Path,
config_name: str = "prototype",
backend: str = "cpu",
verbose: bool = False,
) -> Sensor:
use_cuda = backend in ("cupy", "opencl")
return Sensor(
vid_src=str(video_path),
cuda=use_cuda,
isstitch=True,
verbose=verbose,
config_name=config_name,
backend=backend,
)
@pytest.fixture
def sensor_factory(test_video_path, test_config_name):
def _factory(backend: str = "cpu", verbose: bool = False):
return make_sensor(
test_video_path,
config_name=test_config_name,
backend=backend,
verbose=verbose,
)
return _factory
+6
View File
@@ -0,0 +1,6 @@
# set
set ORISYS_TEST_VIDEO=test\idle_1min.mp4
set ORISYS_TEST_CONFIG=ddjx01
# run command
pytest -q -rs
+66
View File
@@ -0,0 +1,66 @@
import os
import numpy as np
def _collect_vnormal_sequence(sensor, frames: int) -> np.ndarray:
seq = []
for _ in range(frames):
img = sensor.get_img()
assert img is not None
sensor.compute_deformation(check_motion=False)
(vnormal,) = sensor.read_info(sensor.info.VNORMAL)
seq.append(vnormal.copy())
return np.asarray(seq)
def test_available_backends_smoke(sensor_factory, available_backends):
for backend in available_backends:
sensor = sensor_factory(backend=backend)
try:
img = sensor.get_img()
assert img is not None
sensor.compute_deformation(check_motion=False)
flow, vnormal, vshear = sensor.read_info(
sensor.info.VRAW,
sensor.info.VNORMAL,
sensor.info.VSHEAR,
)
assert flow.shape == vnormal.shape == vshear.shape
assert flow.shape[-1] == 2
assert np.isfinite(flow).all()
assert np.isfinite(vnormal).all()
assert np.isfinite(vshear).all()
finally:
sensor.disconnect()
def test_backend_parity_against_cpu(sensor_factory, available_backends):
if len(available_backends) <= 1:
return
compare_frames = int(os.getenv("ORISYS_PARITY_FRAMES", "30"))
warmup = int(os.getenv("ORISYS_PARITY_WARMUP", "5"))
tol = float(os.getenv("ORISYS_PARITY_REL_TOL", "2.0"))
cpu_sensor = sensor_factory(backend="cpu")
try:
cpu_seq = _collect_vnormal_sequence(cpu_sensor, compare_frames)
finally:
cpu_sensor.disconnect()
cpu_ref = cpu_seq[warmup:].mean(axis=0)
cpu_scale = float(np.mean(np.abs(cpu_ref))) + 1e-6
for backend in available_backends:
if backend == "cpu":
continue
sensor = sensor_factory(backend=backend)
try:
seq = _collect_vnormal_sequence(sensor, compare_frames)
finally:
sensor.disconnect()
cand_ref = seq[warmup:].mean(axis=0)
rel = float(np.mean(np.abs(cand_ref - cpu_ref)) / cpu_scale)
assert rel < tol, f"{backend} parity drift too high: relative_error={rel:.4f}, tol={tol}"
+32
View File
@@ -0,0 +1,32 @@
from pathlib import Path
import pytest
from orisys import Sensor
def test_compute_deformation_requires_get_img(sensor_factory):
sensor = sensor_factory(backend="cpu")
try:
with pytest.raises(Exception):
sensor.compute_deformation(check_motion=False)
finally:
sensor.disconnect()
def test_invalid_video_source_raises():
bad_path = Path("this_video_file_should_not_exist_123456.mp4")
with pytest.raises(Exception):
sensor = Sensor(vid_src=str(bad_path), verbose=False, backend="cpu")
sensor.disconnect()
def test_repeated_connect_disconnect(sensor_factory):
for _ in range(3):
sensor = sensor_factory(backend="cpu")
try:
img = sensor.get_img()
assert img is not None
sensor.compute_deformation(check_motion=False)
finally:
sensor.disconnect()
+41
View File
@@ -0,0 +1,41 @@
import os
import numpy as np
import pytest
@pytest.mark.slow
def test_vnormal_longrun_no_outlier_jumps(sensor_factory, available_backends):
preferred = os.getenv("ORISYS_LONGRUN_BACKEND", "cupy")
backend = preferred if preferred in available_backends else "cpu"
frames = int(os.getenv("ORISYS_LONGRUN_FRAMES", "1200"))
ratio_limit = float(os.getenv("ORISYS_LONGRUN_MAX_P99_P95_RATIO", "6.0"))
sensor = sensor_factory(backend=backend)
deltas = []
prev = None
try:
for _ in range(frames):
img = sensor.get_img()
assert img is not None
sensor.compute_deformation(check_motion=False)
(vnormal,) = sensor.read_info(sensor.info.VNORMAL)
assert np.isfinite(vnormal).all()
if prev is not None:
deltas.append(float(np.mean(np.abs(vnormal - prev))))
prev = vnormal.copy()
finally:
sensor.disconnect()
if len(deltas) < 30:
pytest.skip("Not enough frames to evaluate stability.")
p95 = float(np.percentile(deltas, 95))
p99 = float(np.percentile(deltas, 99))
ratio = p99 / (p95 + 1e-9)
assert ratio < ratio_limit, (
f"Long-run jump risk detected on {backend}: p99/p95={ratio:.3f} "
f"(limit={ratio_limit})."
)
+47
View File
@@ -0,0 +1,47 @@
import numpy as np
import orisys
def test_import_version():
assert isinstance(orisys.__version__, str)
assert len(orisys.__version__) > 0
def test_sensor_smoke_pipeline(sensor_factory):
sensor = sensor_factory(backend="cpu")
try:
img = sensor.get_img()
assert img is not None
assert isinstance(img, np.ndarray)
assert img.ndim in (2, 3)
result = sensor.compute_deformation(check_motion=False, threshold=6)
assert result is None
flow, vnormal, vshear = sensor.read_info(
sensor.info.VRAW,
sensor.info.VNORMAL,
sensor.info.VSHEAR,
)
assert flow.ndim == 3 and flow.shape[-1] == 2
assert vnormal.shape == flow.shape
assert vshear.shape == flow.shape
assert np.isfinite(flow).all()
assert np.isfinite(vnormal).all()
assert np.isfinite(vshear).all()
fnormal, fsx, fsy = sensor.read_info(
sensor.info.FNORMAL,
sensor.info.FSHEARX,
sensor.info.FSHEARY,
)
assert np.isfinite([fnormal, fsx, fsy]).all()
contact = sensor.compute_contact()
assert isinstance(contact, dict)
assert "centroid_found" in contact
assert "divergence_map" in contact
assert isinstance(contact["divergence_map"], np.ndarray)
finally:
sensor.disconnect()