656 lines
25 KiB
Python
656 lines
25 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import io
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
from types import ModuleType, SimpleNamespace
|
|
|
|
from tests.python.support.paths import add_repo_path, repo_path
|
|
|
|
add_repo_path("skills/cad/scripts")
|
|
|
|
import snapshot.__main__ as snapshot_main
|
|
from snapshot.__main__ import (
|
|
RENDER_HTML_PATH,
|
|
RUNTIME_DIR,
|
|
SnapshotError,
|
|
load_job_from_options,
|
|
parse_snapshot_args,
|
|
resolve_render_job_packet,
|
|
resolve_snapshot_route_file,
|
|
timestamp_output_path,
|
|
)
|
|
|
|
|
|
class _TtyStringIO(io.StringIO):
|
|
def isatty(self) -> bool:
|
|
return True
|
|
|
|
|
|
def _selector_artifact(*occurrence_ids: str) -> SimpleNamespace:
|
|
return SimpleNamespace(
|
|
selector_bundle=SimpleNamespace(
|
|
manifest={
|
|
"tables": {
|
|
"occurrenceColumns": ["id"],
|
|
"shapeColumns": ["id", "occurrenceId"],
|
|
},
|
|
"occurrences": [[occurrence_id] for occurrence_id in occurrence_ids],
|
|
"shapes": [],
|
|
},
|
|
buffers={},
|
|
)
|
|
)
|
|
|
|
|
|
class SnapshotCliTests(unittest.TestCase):
|
|
def test_cli_import_does_not_import_heavy_cad_modules(self) -> None:
|
|
skill_root = repo_path("skills/cad")
|
|
code = (
|
|
"import sys; sys.path.insert(0, 'scripts'); import snapshot.__main__; "
|
|
"print('OCP.OCP' in sys.modules); "
|
|
"print('cadpy.step_scene' in sys.modules)"
|
|
)
|
|
result = subprocess.run(
|
|
[sys.executable, "-c", code],
|
|
cwd=skill_root,
|
|
check=False,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
text=True,
|
|
)
|
|
self.assertEqual("", result.stderr)
|
|
self.assertEqual(0, result.returncode)
|
|
self.assertEqual(["False", "False"], result.stdout.strip().splitlines())
|
|
|
|
def test_shortcut_job_shape_stays_owned_by_python_cli(self) -> None:
|
|
options = parse_snapshot_args(
|
|
[
|
|
"--input",
|
|
"models/simple/cylindrical_cap.step",
|
|
"--output",
|
|
"tmp/cap.png",
|
|
"--display",
|
|
"wireframe",
|
|
"--size-profile",
|
|
"simple",
|
|
]
|
|
)
|
|
|
|
job = load_job_from_options(options, stdin=_TtyStringIO(), cwd=Path.cwd())
|
|
|
|
self.assertEqual(job["input"], "models/simple/cylindrical_cap.step")
|
|
self.assertNotIn("workspaceRoot", job)
|
|
self.assertNotIn("rootDir", job)
|
|
self.assertEqual(job["outputs"][0]["path"], "tmp/cap.png")
|
|
self.assertEqual(job["display"], {"mode": "wireframe"})
|
|
self.assertEqual(job["render"]["sizeProfile"], "simple")
|
|
|
|
def test_shortcut_focus_and_hide_flags_are_mutually_exclusive(self) -> None:
|
|
with self.assertRaisesRegex(SnapshotError, "--focus and --hide cannot be used"):
|
|
parse_snapshot_args(
|
|
[
|
|
"--input",
|
|
"models/assembly.step",
|
|
"--output",
|
|
"tmp/assembly.png",
|
|
"--focus",
|
|
"#o1.2",
|
|
"--hide=#o1.3.1",
|
|
]
|
|
)
|
|
|
|
def test_display_shortcut_accepts_cad_display_modes(self) -> None:
|
|
for raw_mode, expected_display in [
|
|
("edges", {"mode": "solid"}),
|
|
("x-ray", {"mode": "transparent"}),
|
|
("hidden edges visible", {"mode": "hidden_edges"}),
|
|
("hidden-lines-removed", {"mode": "hidden_lines_removed"}),
|
|
("flat", {"mode": "unshaded"}),
|
|
("appearance", {"mode": "rendered"}),
|
|
("wire", {"mode": "wireframe"}),
|
|
]:
|
|
options = parse_snapshot_args(
|
|
[
|
|
"--input",
|
|
"models/simple/cylindrical_cap.step",
|
|
"--output",
|
|
"tmp/cap.png",
|
|
"--display",
|
|
raw_mode,
|
|
]
|
|
)
|
|
job = load_job_from_options(options, stdin=_TtyStringIO(), cwd=Path.cwd())
|
|
self.assertEqual(job["display"], expected_display)
|
|
|
|
def test_display_json_accepts_exploded_settings(self) -> None:
|
|
options = parse_snapshot_args(
|
|
[
|
|
"--input",
|
|
"models/simple/cylindrical_cap.step",
|
|
"--output",
|
|
"tmp/cap.png",
|
|
"--display",
|
|
'{"projection":"perspective","mode":"rendered","exploded":{"enabled":true,"axis":"radial","spacing":1.6}}',
|
|
]
|
|
)
|
|
job = load_job_from_options(options, stdin=_TtyStringIO(), cwd=Path.cwd())
|
|
self.assertEqual(
|
|
job["display"],
|
|
{
|
|
"projection": "perspective",
|
|
"mode": "rendered",
|
|
"exploded": {"enabled": True, "axis": "radial", "spacing": 1.6},
|
|
},
|
|
)
|
|
|
|
def test_edge_settings_belong_to_display_json(self) -> None:
|
|
options = parse_snapshot_args(
|
|
[
|
|
"--input",
|
|
"models/simple/cylindrical_cap.step",
|
|
"--output",
|
|
"tmp/cap.png",
|
|
"--display",
|
|
'{"edges":{"enabled":false,"color":"#123456"}}',
|
|
]
|
|
)
|
|
job = load_job_from_options(options, stdin=_TtyStringIO(), cwd=Path.cwd())
|
|
self.assertEqual(job["display"], {"edges": {"enabled": False, "color": "#123456"}})
|
|
|
|
appearance_options = parse_snapshot_args(
|
|
[
|
|
"--input",
|
|
"models/simple/cylindrical_cap.step",
|
|
"--output",
|
|
"tmp/cap.png",
|
|
"--appearance",
|
|
'{"edges":{"enabled":false}}',
|
|
]
|
|
)
|
|
with self.assertRaisesRegex(SnapshotError, "unsupported keys: edges"):
|
|
load_job_from_options(appearance_options, stdin=_TtyStringIO(), cwd=Path.cwd())
|
|
|
|
def test_display_shortcut_rejects_unknown_modes(self) -> None:
|
|
options = parse_snapshot_args(
|
|
[
|
|
"--input",
|
|
"models/simple/cylindrical_cap.step",
|
|
"--output",
|
|
"tmp/cap.png",
|
|
"--display",
|
|
"mist",
|
|
]
|
|
)
|
|
with self.assertRaisesRegex(SnapshotError, "Unsupported display mode"):
|
|
load_job_from_options(options, stdin=_TtyStringIO(), cwd=Path.cwd())
|
|
|
|
def test_display_shortcut_rejects_exploded_mode_alias(self) -> None:
|
|
options = parse_snapshot_args(
|
|
[
|
|
"--input",
|
|
"models/simple/cylindrical_cap.step",
|
|
"--output",
|
|
"tmp/cap.png",
|
|
"--display",
|
|
"exploded",
|
|
]
|
|
)
|
|
with self.assertRaisesRegex(SnapshotError, "Unsupported display mode"):
|
|
load_job_from_options(options, stdin=_TtyStringIO(), cwd=Path.cwd())
|
|
|
|
def test_shortcut_focus_flags_apply_selection(self) -> None:
|
|
options = parse_snapshot_args(
|
|
[
|
|
"--input",
|
|
"models/assembly.step",
|
|
"--output",
|
|
"tmp/assembly.png",
|
|
"--focus",
|
|
"#o1.2",
|
|
"#o1.3",
|
|
]
|
|
)
|
|
|
|
job = load_job_from_options(options, stdin=_TtyStringIO(), cwd=Path.cwd())
|
|
|
|
self.assertEqual(
|
|
job["selection"],
|
|
{
|
|
"focus": ["#o1.2", "#o1.3"],
|
|
},
|
|
)
|
|
|
|
def test_output_paths_are_timestamped_when_jobs_are_resolved(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary_directory:
|
|
root = Path(temporary_directory).resolve()
|
|
models = root / "models"
|
|
models.mkdir()
|
|
(models / "part.step").write_text("ISO-10303-21;\nEND-ISO-10303-21;\n", encoding="utf-8")
|
|
(models / ".part.step.glb").write_bytes(b"glb")
|
|
|
|
original_timestamp = snapshot_main.snapshot_timestamp
|
|
original_ensure = snapshot_main.ensure_step_topology_artifact
|
|
try:
|
|
snapshot_main.snapshot_timestamp = lambda: "20260527T163012Z"
|
|
snapshot_main.ensure_step_topology_artifact = lambda *args, **kwargs: None
|
|
|
|
packet = resolve_render_job_packet(
|
|
{
|
|
"jobs": [
|
|
{
|
|
"input": "models/part.step",
|
|
"outputs": [
|
|
{"path": "tmp/iso.png", "camera": "iso"},
|
|
{"path": "tmp/front.png", "camera": "front"},
|
|
],
|
|
},
|
|
{
|
|
"input": "models/part.step",
|
|
"mode": "orbit",
|
|
"outputs": [{"path": "tmp/orbit.gif"}],
|
|
},
|
|
]
|
|
},
|
|
cwd=root,
|
|
)
|
|
finally:
|
|
snapshot_main.snapshot_timestamp = original_timestamp
|
|
snapshot_main.ensure_step_topology_artifact = original_ensure
|
|
|
|
output_paths = [
|
|
Path(output["path"]).relative_to(root).as_posix()
|
|
for job in packet["jobs"]
|
|
for output in job["outputs"]
|
|
]
|
|
|
|
self.assertEqual(
|
|
output_paths,
|
|
[
|
|
"tmp/iso_20260527T163012Z.png",
|
|
"tmp/front_20260527T163012Z.png",
|
|
"tmp/orbit_20260527T163012Z.gif",
|
|
],
|
|
)
|
|
|
|
def test_render_job_derives_asset_root_from_input_path(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary_directory:
|
|
root = Path(temporary_directory).resolve()
|
|
models = root / "models"
|
|
models.mkdir()
|
|
(models / "part.step").write_text("ISO-10303-21;\nEND-ISO-10303-21;\n", encoding="utf-8")
|
|
(models / ".part.step.glb").write_bytes(b"glb")
|
|
|
|
original_ensure = snapshot_main.ensure_step_topology_artifact
|
|
try:
|
|
snapshot_main.ensure_step_topology_artifact = lambda *args, **kwargs: None
|
|
packet = resolve_render_job_packet(
|
|
{
|
|
"input": "models/part.step",
|
|
"outputs": [{"path": "tmp/iso.png", "camera": "iso"}],
|
|
},
|
|
cwd=root,
|
|
)
|
|
finally:
|
|
snapshot_main.ensure_step_topology_artifact = original_ensure
|
|
|
|
job = packet["jobs"][0]
|
|
self.assertNotIn("workspaceRoot", job)
|
|
self.assertNotIn("rootDir", job)
|
|
self.assertEqual(job["resolved"]["rootPath"], str(models))
|
|
self.assertEqual(job["resolved"]["inputUrl"], "/__render_asset/part.step")
|
|
self.assertEqual(job["resolved"]["glbUrl"], "/__render_asset/.part.step.glb")
|
|
|
|
def test_render_job_ensures_step_artifact_for_step_input(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary_directory:
|
|
root = Path(temporary_directory).resolve()
|
|
models = root / "models"
|
|
models.mkdir()
|
|
step_path = models / "part.step"
|
|
step_path.write_text("ISO-10303-21;\nEND-ISO-10303-21;\n", encoding="utf-8")
|
|
(models / ".part.step.glb").write_bytes(b"glb")
|
|
calls = []
|
|
|
|
def fake_ensure(target, **kwargs):
|
|
calls.append((target, kwargs))
|
|
return None
|
|
|
|
original_ensure = snapshot_main.ensure_step_topology_artifact
|
|
try:
|
|
snapshot_main.ensure_step_topology_artifact = fake_ensure
|
|
resolve_render_job_packet(
|
|
{
|
|
"input": "models/part.step",
|
|
"outputs": [{"path": "tmp/iso.png", "camera": "iso"}],
|
|
},
|
|
cwd=root,
|
|
)
|
|
finally:
|
|
snapshot_main.ensure_step_topology_artifact = original_ensure
|
|
|
|
self.assertEqual(len(calls), 1)
|
|
target, kwargs = calls[0]
|
|
self.assertEqual(target.step_path, step_path)
|
|
self.assertEqual(target.source_path, step_path)
|
|
self.assertEqual(kwargs["owner"], "cad-snapshot")
|
|
self.assertFalse(kwargs["require_selector"])
|
|
|
|
def test_render_job_rejects_non_step_input_without_artifact_generation(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary_directory:
|
|
root = Path(temporary_directory).resolve()
|
|
models = root / "models"
|
|
models.mkdir()
|
|
(models / "robot.urdf").write_text("<robot name=\"r\" />\n", encoding="utf-8")
|
|
calls = []
|
|
|
|
def fake_ensure(target, **kwargs):
|
|
calls.append((target, kwargs))
|
|
return None
|
|
|
|
original_ensure = snapshot_main.ensure_step_topology_artifact
|
|
try:
|
|
snapshot_main.ensure_step_topology_artifact = fake_ensure
|
|
with self.assertRaisesRegex(
|
|
SnapshotError,
|
|
"Snapshot supports only STEP/STP inputs or same-stem Python generators",
|
|
):
|
|
resolve_render_job_packet(
|
|
{
|
|
"input": "models/robot.urdf",
|
|
"outputs": [{"path": "tmp/iso.png", "camera": "iso"}],
|
|
},
|
|
cwd=root,
|
|
)
|
|
finally:
|
|
snapshot_main.ensure_step_topology_artifact = original_ensure
|
|
|
|
self.assertEqual(calls, [])
|
|
|
|
def test_render_job_requires_selector_topology_for_cad_refs(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary_directory:
|
|
root = Path(temporary_directory).resolve()
|
|
models = root / "models"
|
|
models.mkdir()
|
|
step_path = models / "assembly.step"
|
|
step_path.write_text("ISO-10303-21;\nEND-ISO-10303-21;\n", encoding="utf-8")
|
|
(models / ".assembly.step.glb").write_bytes(b"glb")
|
|
calls = []
|
|
|
|
def fake_ensure(target, **kwargs):
|
|
calls.append((target, kwargs))
|
|
return _selector_artifact("o1", "o1.2")
|
|
|
|
original_ensure = snapshot_main.ensure_step_topology_artifact
|
|
try:
|
|
snapshot_main.ensure_step_topology_artifact = fake_ensure
|
|
resolve_render_job_packet(
|
|
{
|
|
"input": "models/assembly.step",
|
|
"selection": {"focus": ["#o1.2"]},
|
|
"outputs": [{"path": "tmp/iso.png", "camera": "iso"}],
|
|
},
|
|
cwd=root,
|
|
)
|
|
finally:
|
|
snapshot_main.ensure_step_topology_artifact = original_ensure
|
|
|
|
self.assertEqual(len(calls), 1)
|
|
target, kwargs = calls[0]
|
|
self.assertEqual(target.step_path, step_path)
|
|
self.assertTrue(kwargs["require_selector"])
|
|
|
|
def test_render_job_normalizes_focus_selector_refs(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary_directory:
|
|
root = Path(temporary_directory).resolve()
|
|
models = root / "models"
|
|
models.mkdir()
|
|
(models / "assembly.step").write_text("ISO-10303-21;\nEND-ISO-10303-21;\n", encoding="utf-8")
|
|
(models / ".assembly.step.glb").write_bytes(b"glb")
|
|
|
|
original_ensure = snapshot_main.ensure_step_topology_artifact
|
|
try:
|
|
snapshot_main.ensure_step_topology_artifact = lambda *args, **kwargs: _selector_artifact(
|
|
"o1",
|
|
"o1.2",
|
|
"o1.2.1",
|
|
"o1.3",
|
|
)
|
|
packet = resolve_render_job_packet(
|
|
{
|
|
"input": "models/assembly.step",
|
|
"selection": {
|
|
"focus": ["#o1.2", "#o1.3"],
|
|
},
|
|
"outputs": [{"path": "tmp/iso.png", "camera": "iso"}],
|
|
},
|
|
cwd=root,
|
|
)
|
|
finally:
|
|
snapshot_main.ensure_step_topology_artifact = original_ensure
|
|
|
|
selection = packet["jobs"][0]["selection"]
|
|
self.assertEqual(selection["focus"], ["o1.2", "o1.3"])
|
|
|
|
def test_render_job_normalizes_hide_selector_refs(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary_directory:
|
|
root = Path(temporary_directory).resolve()
|
|
models = root / "models"
|
|
models.mkdir()
|
|
(models / "assembly.step").write_text("ISO-10303-21;\nEND-ISO-10303-21;\n", encoding="utf-8")
|
|
(models / ".assembly.step.glb").write_bytes(b"glb")
|
|
|
|
original_ensure = snapshot_main.ensure_step_topology_artifact
|
|
try:
|
|
snapshot_main.ensure_step_topology_artifact = lambda *args, **kwargs: _selector_artifact(
|
|
"o1",
|
|
"o1.2",
|
|
"o1.2.1",
|
|
"o1.3",
|
|
)
|
|
packet = resolve_render_job_packet(
|
|
{
|
|
"input": "models/assembly.step",
|
|
"selection": {"hide": ["#o1.2.1"]},
|
|
"outputs": [{"path": "tmp/iso.png", "camera": "iso"}],
|
|
},
|
|
cwd=root,
|
|
)
|
|
finally:
|
|
snapshot_main.ensure_step_topology_artifact = original_ensure
|
|
|
|
selection = packet["jobs"][0]["selection"]
|
|
self.assertEqual(selection["hide"], ["o1.2.1"])
|
|
|
|
def test_render_job_rejects_face_focus_refs(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary_directory:
|
|
root = Path(temporary_directory).resolve()
|
|
models = root / "models"
|
|
models.mkdir()
|
|
(models / "assembly.step").write_text("ISO-10303-21;\nEND-ISO-10303-21;\n", encoding="utf-8")
|
|
(models / ".assembly.step.glb").write_bytes(b"glb")
|
|
|
|
original_ensure = snapshot_main.ensure_step_topology_artifact
|
|
try:
|
|
snapshot_main.ensure_step_topology_artifact = lambda *args, **kwargs: _selector_artifact("o1", "o1.2")
|
|
with self.assertRaisesRegex(SnapshotError, "part/subassembly occurrence refs"):
|
|
resolve_render_job_packet(
|
|
{
|
|
"input": "models/assembly.step",
|
|
"selection": {"focus": ["#o1.2.f1"]},
|
|
"outputs": [{"path": "tmp/iso.png", "camera": "iso"}],
|
|
},
|
|
cwd=root,
|
|
)
|
|
finally:
|
|
snapshot_main.ensure_step_topology_artifact = original_ensure
|
|
|
|
def test_render_job_rejects_mixed_focus_and_hide_selection(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary_directory:
|
|
root = Path(temporary_directory).resolve()
|
|
models = root / "models"
|
|
models.mkdir()
|
|
(models / "assembly.step").write_text("ISO-10303-21;\nEND-ISO-10303-21;\n", encoding="utf-8")
|
|
(models / ".assembly.step.glb").write_bytes(b"glb")
|
|
|
|
original_ensure = snapshot_main.ensure_step_topology_artifact
|
|
try:
|
|
snapshot_main.ensure_step_topology_artifact = lambda *args, **kwargs: _selector_artifact(
|
|
"o1",
|
|
"o1.2",
|
|
"o1.3",
|
|
)
|
|
with self.assertRaisesRegex(SnapshotError, "selection.focus/refs and selection.hide cannot be used"):
|
|
resolve_render_job_packet(
|
|
{
|
|
"input": "models/assembly.step",
|
|
"selection": {
|
|
"focus": ["#o1.2"],
|
|
"hide": ["#o1.3"],
|
|
},
|
|
"outputs": [{"path": "tmp/iso.png", "camera": "iso"}],
|
|
},
|
|
cwd=root,
|
|
)
|
|
finally:
|
|
snapshot_main.ensure_step_topology_artifact = original_ensure
|
|
|
|
def test_snapshot_root_flags_and_job_fields_are_removed(self) -> None:
|
|
with self.assertRaisesRegex(SnapshotError, "Unknown argument: --workspace-root"):
|
|
parse_snapshot_args(["--workspace-root", "/tmp"])
|
|
with self.assertRaisesRegex(SnapshotError, "Unknown argument: --root-dir"):
|
|
parse_snapshot_args(["--root-dir", "models"])
|
|
with self.assertRaisesRegex(SnapshotError, "no longer accept workspaceRoot or rootDir"):
|
|
resolve_render_job_packet(
|
|
{
|
|
"input": "part.step",
|
|
"workspaceRoot": "/tmp",
|
|
"outputs": [{"path": "tmp/iso.png"}],
|
|
},
|
|
cwd=Path.cwd(),
|
|
)
|
|
|
|
def test_timestamp_output_path_preserves_extension(self) -> None:
|
|
self.assertEqual(
|
|
timestamp_output_path("snapshots/review.png", "20260527T163012Z"),
|
|
"snapshots/review_20260527T163012Z.png",
|
|
)
|
|
|
|
def test_removed_daemon_flags_stay_removed(self) -> None:
|
|
with self.assertRaisesRegex(SnapshotError, "daemon commands have been removed"):
|
|
parse_snapshot_args(["daemon"])
|
|
with self.assertRaisesRegex(SnapshotError, "--socket has been removed"):
|
|
parse_snapshot_args(["--socket", "snapshot.sock"])
|
|
|
|
def test_runtime_routes_are_self_contained(self) -> None:
|
|
self.assertEqual(
|
|
resolve_snapshot_route_file("http://snapshot.local/render.html"),
|
|
RENDER_HTML_PATH,
|
|
)
|
|
self.assertEqual(
|
|
resolve_snapshot_route_file("http://snapshot.local/snapshot-render.js"),
|
|
RUNTIME_DIR / "snapshot-render.js",
|
|
)
|
|
|
|
def test_snapshot_renderer_does_not_force_chromium_single_process(self) -> None:
|
|
captured_launch_options = {}
|
|
|
|
class FakePage:
|
|
async def route(self, *args, **kwargs):
|
|
pass
|
|
|
|
async def goto(self, *args, **kwargs):
|
|
pass
|
|
|
|
async def wait_for_function(self, *args, **kwargs):
|
|
pass
|
|
|
|
class FakeContext:
|
|
async def new_page(self):
|
|
return FakePage()
|
|
|
|
async def close(self):
|
|
pass
|
|
|
|
class FakeBrowser:
|
|
async def new_context(self, *args, **kwargs):
|
|
return FakeContext()
|
|
|
|
async def close(self):
|
|
pass
|
|
|
|
class FakeChromium:
|
|
async def launch(self, **kwargs):
|
|
captured_launch_options.update(kwargs)
|
|
return FakeBrowser()
|
|
|
|
class FakePlaywright:
|
|
def __init__(self) -> None:
|
|
self.chromium = FakeChromium()
|
|
|
|
async def stop(self):
|
|
pass
|
|
|
|
fake_playwright = FakePlaywright()
|
|
|
|
class FakeAsyncPlaywright:
|
|
async def start(self):
|
|
return fake_playwright
|
|
|
|
async_api_module = ModuleType("playwright.async_api")
|
|
async_api_module.async_playwright = FakeAsyncPlaywright
|
|
playwright_module = ModuleType("playwright")
|
|
playwright_module.__path__ = []
|
|
|
|
original_playwright = sys.modules.get("playwright")
|
|
original_async_api = sys.modules.get("playwright.async_api")
|
|
try:
|
|
sys.modules["playwright"] = playwright_module
|
|
sys.modules["playwright.async_api"] = async_api_module
|
|
|
|
async def start_renderer() -> None:
|
|
renderer = snapshot_main.BatchSnapshotRenderer()
|
|
try:
|
|
await renderer.start()
|
|
finally:
|
|
await renderer.close()
|
|
|
|
asyncio.run(start_renderer())
|
|
finally:
|
|
if original_playwright is None:
|
|
sys.modules.pop("playwright", None)
|
|
else:
|
|
sys.modules["playwright"] = original_playwright
|
|
if original_async_api is None:
|
|
sys.modules.pop("playwright.async_api", None)
|
|
else:
|
|
sys.modules["playwright.async_api"] = original_async_api
|
|
|
|
self.assertNotIn("--single-process", captured_launch_options.get("args") or [])
|
|
|
|
def test_snapshot_tool_has_no_sideways_runtime_dependencies(self) -> None:
|
|
snapshot_root = repo_path("skills/cad/scripts/snapshot")
|
|
checked_files = [
|
|
snapshot_root / "__main__.py",
|
|
snapshot_root / "runtime" / "render.html",
|
|
snapshot_root / "runtime" / "snapshot-render.js",
|
|
]
|
|
forbidden = (
|
|
"packages/cadjs",
|
|
"skills/cad-viewer",
|
|
"/node_modules/",
|
|
"\\node_modules\\",
|
|
"CADJS_NODE_MODULES_ROOT",
|
|
)
|
|
for checked_file in checked_files:
|
|
text = checked_file.read_text(encoding="utf-8")
|
|
for token in forbidden:
|
|
self.assertNotIn(token, text, f"{checked_file} should not reference {token}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|