186 lines
6.5 KiB
Python
186 lines
6.5 KiB
Python
"""Tests for revision metadata and manifest generation."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from orisys_sensor_design.cli import resolve_blender_executable
|
|
from orisys_sensor_design.manifest import (
|
|
EXPORT_JOB_SCHEMA,
|
|
RevisionPaths,
|
|
build_export_job,
|
|
build_manifest,
|
|
default_manifest_path,
|
|
export_object_names,
|
|
export_output_dir,
|
|
export_revision_dir,
|
|
repo_root,
|
|
sha256_directory,
|
|
validate_revision_metadata,
|
|
write_export_job,
|
|
write_manifest,
|
|
)
|
|
|
|
|
|
@pytest.fixture()
|
|
def revision_dir() -> Path:
|
|
root = repo_root()
|
|
return root / "sensors" / "finger" / "r01"
|
|
|
|
|
|
def test_repo_root_finds_pyproject(revision_dir: Path) -> None:
|
|
root = repo_root(revision_dir)
|
|
assert (root / "pyproject.toml").is_file()
|
|
|
|
|
|
def test_validate_metadata_reports_missing_blend(tmp_path: Path) -> None:
|
|
revision = tmp_path / "r99"
|
|
(revision / "profiles").mkdir(parents=True)
|
|
(revision / "design.yaml").write_text(
|
|
"sensor: finger\nrevision: r99\nmeters_per_source_unit: 0.001\n"
|
|
"source_blend: source/assembly.blend\n",
|
|
encoding="utf-8",
|
|
)
|
|
for name in ("compute.yaml", "studio.yaml"):
|
|
(revision / "profiles" / name).write_text(f"profile: {name.split('.')[0]}\n", encoding="utf-8")
|
|
paths = RevisionPaths.from_revision_dir(revision)
|
|
errors = validate_revision_metadata(paths)
|
|
assert any("Source blend not found" in message for message in errors)
|
|
|
|
|
|
def test_export_object_names_from_design(revision_dir: Path) -> None:
|
|
names = export_object_names(RevisionPaths.from_revision_dir(revision_dir))
|
|
assert names == {
|
|
"sensing_surface": "sensing_surface",
|
|
"cam_sensor": "cam_sensor",
|
|
"origin_sensor": "origin_sensor",
|
|
}
|
|
|
|
|
|
def test_build_export_job_round_trips_json(revision_dir: Path, tmp_path: Path) -> None:
|
|
paths = RevisionPaths.from_revision_dir(revision_dir)
|
|
output_dir = tmp_path / "compute"
|
|
job = build_export_job(paths, output_dir, "compute")
|
|
assert job["schema"] == EXPORT_JOB_SCHEMA
|
|
assert job["profile"] == "compute"
|
|
assert job["objects"] == {
|
|
"sensing_surface": "sensing_surface",
|
|
"cam_sensor": "cam_sensor",
|
|
"origin_sensor": "origin_sensor",
|
|
}
|
|
assert Path(job["output"]) == output_dir.resolve()
|
|
|
|
job_path = write_export_job(job, tmp_path)
|
|
assert job_path.name == "_export_job.json"
|
|
assert job_path.parent == tmp_path
|
|
loaded = json.loads(job_path.read_text(encoding="utf-8"))
|
|
assert loaded["schema"] == EXPORT_JOB_SCHEMA
|
|
assert loaded["objects"]["cam_sensor"] == "cam_sensor"
|
|
assert loaded["objects"]["sensing_surface"] == "sensing_surface"
|
|
assert loaded["objects"]["origin_sensor"] == "origin_sensor"
|
|
|
|
|
|
def test_export_output_dir_matches_profile(revision_dir: Path) -> None:
|
|
root = repo_root(revision_dir)
|
|
paths = RevisionPaths.from_revision_dir(revision_dir)
|
|
out = export_output_dir(root, paths, "compute")
|
|
assert out == (root / "build/export/finger/r01/compute").resolve()
|
|
assert export_revision_dir(root, paths, "compute") == (
|
|
root / "build/export/finger/r01"
|
|
).resolve()
|
|
assert default_manifest_path(root, paths, "compute") == (
|
|
root / "build/export/finger/r01/manifest.json"
|
|
).resolve()
|
|
|
|
|
|
def test_bundle_hash_ignores_generated_metadata(tmp_path: Path) -> None:
|
|
payload = tmp_path / "compute"
|
|
payload.mkdir()
|
|
(payload / "mesh.npz").write_bytes(b"mesh")
|
|
digest = sha256_directory(payload)
|
|
(payload / "_export_job.json").write_text("{}", encoding="utf-8")
|
|
(payload / "manifest.json").write_text("{}", encoding="utf-8")
|
|
assert sha256_directory(payload) == digest
|
|
|
|
|
|
def test_manifest_writes_without_export(revision_dir: Path, tmp_path: Path) -> None:
|
|
manifest = build_manifest(
|
|
root=repo_root(revision_dir),
|
|
revision_dir=revision_dir,
|
|
profile="compute",
|
|
)
|
|
out = tmp_path / "manifest.json"
|
|
write_manifest(out, manifest)
|
|
loaded = json.loads(out.read_text(encoding="utf-8"))
|
|
assert loaded["sensor"] == "finger"
|
|
assert loaded["revision"] == "r01"
|
|
assert loaded["profile"] == "compute"
|
|
assert loaded["expected_artifacts"] == [
|
|
"mesh.npz",
|
|
"housing.npz",
|
|
"camera_intrinsics.npz",
|
|
"world_to_cam.npy",
|
|
]
|
|
assert loaded["source_blend"].endswith("geometry.blend")
|
|
|
|
|
|
def test_resolve_blender_respects_env(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
|
fake = tmp_path / "blender.exe"
|
|
fake.write_text("", encoding="utf-8")
|
|
monkeypatch.setenv("ORISYS_BLENDER", str(fake))
|
|
monkeypatch.delenv("BLENDER_EXE", raising=False)
|
|
assert resolve_blender_executable() == fake.resolve()
|
|
|
|
|
|
def test_resolve_blender_from_program_files(
|
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
) -> None:
|
|
monkeypatch.delenv("ORISYS_BLENDER", raising=False)
|
|
monkeypatch.delenv("BLENDER_EXE", raising=False)
|
|
monkeypatch.setattr("orisys_sensor_design.cli.shutil.which", lambda _name: None)
|
|
|
|
pf = tmp_path / "ProgramFiles"
|
|
v42 = pf / "Blender Foundation" / "Blender 4.2"
|
|
v50 = pf / "Blender Foundation" / "Blender 5.0"
|
|
v42.mkdir(parents=True)
|
|
v50.mkdir(parents=True)
|
|
exe42 = v42 / "blender.exe"
|
|
exe50 = v50 / "blender.exe"
|
|
exe42.write_text("", encoding="utf-8")
|
|
exe50.write_text("", encoding="utf-8")
|
|
|
|
monkeypatch.setenv("ProgramFiles", str(pf))
|
|
monkeypatch.setenv("ProgramFiles(x86)", str(tmp_path / "unused-x86"))
|
|
|
|
# No lock preference → newest folder name wins (5.0).
|
|
assert resolve_blender_executable(root=None) == exe50.resolve()
|
|
|
|
|
|
def test_resolve_blender_prefers_lock_lts(
|
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
) -> None:
|
|
monkeypatch.delenv("ORISYS_BLENDER", raising=False)
|
|
monkeypatch.delenv("BLENDER_EXE", raising=False)
|
|
monkeypatch.setattr("orisys_sensor_design.cli.shutil.which", lambda _name: None)
|
|
|
|
pf = tmp_path / "ProgramFiles"
|
|
v42 = pf / "Blender Foundation" / "Blender 4.2"
|
|
v50 = pf / "Blender Foundation" / "Blender 5.0"
|
|
v42.mkdir(parents=True)
|
|
v50.mkdir(parents=True)
|
|
(v42 / "blender.exe").write_text("", encoding="utf-8")
|
|
(v50 / "blender.exe").write_text("", encoding="utf-8")
|
|
monkeypatch.setenv("ProgramFiles", str(pf))
|
|
monkeypatch.setenv("ProgramFiles(x86)", str(tmp_path / "unused-x86"))
|
|
|
|
root = tmp_path / "repo"
|
|
root.mkdir()
|
|
(root / "blender.lock.json").write_text(
|
|
json.dumps({"blender_version": "4.2.0", "blender_lts": "4.2"}),
|
|
encoding="utf-8",
|
|
)
|
|
assert resolve_blender_executable(root=root) == (v42 / "blender.exe").resolve()
|