"""The stroke CLI must preserve colour sources and verify all exported formats.""" from __future__ import annotations import argparse import json from pathlib import Path from orisys_sensor_design import cli from orisys_sensor_design.manifest import RevisionPaths, sha256_file REVISION = Path(__file__).resolve().parents[1] / "sensors" / "finger-xynova" / "r01" def _args(job: Path, output_dir: Path) -> argparse.Namespace: return argparse.Namespace( views=False, source_job=job, output_dir=output_dir, camera_width_fraction=0.5, seed=13, ) def _job(tmp_path: Path) -> tuple[Path, Path, Path]: paths = RevisionPaths.from_revision_dir(REVISION) png = tmp_path / "colour.png" svg = tmp_path / "colour.svg" png.write_bytes(b"colour pixels") svg.write_bytes(b"colour geometry") job_path = tmp_path / "_texture_job.json" job_path.write_text( json.dumps( { "schema": "orisys-sensor-design/texture-job-v3", "revision_dir": str(paths.root), "source_blend": str(paths.source_blend), "outputs": [ { "uv_map": "compute", "artifacts": {"png": str(png), "svg": str(svg)}, } ], } ), encoding="utf-8", ) return job_path, png, svg def test_stroke_cli_exports_three_formats_and_manifest(tmp_path, monkeypatch): job, png, svg = _job(tmp_path) output_dir = tmp_path / "strokes" calls = [] def run_blender(script, blend, args): calls.append((script, blend, args)) # exist_ok: the CLI writes _stroke_pattern_params.json into the output directory before # it launches Blender, so the directory already exists. output_dir.mkdir(parents=True, exist_ok=True) for fmt in ("png", "svg", "pdf"): (output_dir / f"stroke_texture_compute.{fmt}").write_bytes(fmt.encode()) return 0 monkeypatch.setattr(cli, "_run_blender_script", run_blender) assert cli.cmd_stroke_texture(REVISION, _args(job, output_dir)) == 0 assert calls[0][0] == "random_stroke_pattern.py" # `--seed` and `--camera-width-fraction` are overrides folded into the params file, so the # contract is now the effective tuning the script reads. params = json.loads( Path(calls[0][2][calls[0][2].index("--params") + 1]).read_text(encoding="utf-8") ) assert params["seed"] == 13 assert params["width_fraction"] == 0.5 manifest = json.loads((output_dir / "stroke_texture_manifest.json").read_text()) assert manifest["schema"] == "orisys-sensor-design/stroke-texture-manifest-v1" assert manifest["source_job"]["sha256"] == sha256_file(job) assert manifest["outputs"][0]["sources"]["png"]["sha256"] == sha256_file(png) assert manifest["outputs"][0]["sources"]["svg"]["sha256"] == sha256_file(svg) assert set(manifest["outputs"][0]["artifacts"]) == {"png", "svg", "pdf"} assert png.read_bytes() == b"colour pixels" assert cli.cmd_stroke_texture(REVISION, _args(job, output_dir)) == 2 assert len(calls) == 1 def test_stroke_cli_refuses_missing_source_svg(tmp_path, monkeypatch): job, _png, svg = _job(tmp_path) svg.unlink() monkeypatch.setattr( cli, "_run_blender_script", lambda *_: (_ for _ in ()).throw(AssertionError("Blender must not run")), ) assert cli.cmd_stroke_texture(REVISION, _args(job, tmp_path / "strokes")) == 3 def test_stroke_cli_rejects_incomplete_source_job(tmp_path, monkeypatch): job, _png, _svg = _job(tmp_path) data = json.loads(job.read_text(encoding="utf-8")) del data["outputs"][0]["artifacts"]["svg"] job.write_text(json.dumps(data), encoding="utf-8") monkeypatch.setattr( cli, "_run_blender_script", lambda *_: (_ for _ in ()).throw(AssertionError("Blender must not run")), ) assert cli.cmd_stroke_texture(REVISION, _args(job, tmp_path / "strokes")) == 2