first commit
This commit is contained in:
@@ -0,0 +1,933 @@
|
||||
import contextlib
|
||||
import io
|
||||
import shutil
|
||||
import unittest
|
||||
from array import array
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
from tests.python.support.paths import add_repo_path
|
||||
|
||||
add_repo_path("skills/cad/scripts/inspect")
|
||||
|
||||
from inspect_refs import cli as inspect_cli
|
||||
from inspect_refs import inspect as refs_inspect
|
||||
from cadpy import cad_ref_syntax as refs_syntax
|
||||
from cadpy import assembly_spec
|
||||
from cadpy import generation as cad_generation
|
||||
from cadpy import step_targets
|
||||
from cadpy.glb_topology import STEP_TOPOLOGY_SCHEMA_VERSION
|
||||
from cadpy.render import part_glb_path
|
||||
from cadpy.selector_types import SelectorBundle, SelectorProfile
|
||||
from cadpy.source_hash import python_source_hash
|
||||
from tests.python.support.cad_test_roots import IsolatedCadRoots
|
||||
|
||||
|
||||
def _refs_manifest(cad_ref: str) -> dict[str, object]:
|
||||
return {
|
||||
"schemaVersion": STEP_TOPOLOGY_SCHEMA_VERSION,
|
||||
"profile": "refs",
|
||||
"cadPath": cad_ref,
|
||||
"stepPath": f"{cad_ref}.step",
|
||||
"stepHash": "step-hash-123",
|
||||
"bbox": {"min": [0.0, 0.0, 0.0], "max": [10.0, 10.0, 10.0]},
|
||||
"stats": {
|
||||
"occurrenceCount": 2,
|
||||
"leafOccurrenceCount": 1,
|
||||
"shapeCount": 1,
|
||||
"faceCount": 2,
|
||||
"edgeCount": 2,
|
||||
"vertexCount": 1,
|
||||
},
|
||||
"tables": {
|
||||
"occurrenceColumns": [
|
||||
"id",
|
||||
"path",
|
||||
"name",
|
||||
"sourceName",
|
||||
"parentId",
|
||||
"transform",
|
||||
"bbox",
|
||||
"shapeStart",
|
||||
"shapeCount",
|
||||
"faceStart",
|
||||
"faceCount",
|
||||
"edgeStart",
|
||||
"edgeCount",
|
||||
"vertexStart",
|
||||
"vertexCount",
|
||||
],
|
||||
"shapeColumns": [
|
||||
"id",
|
||||
"occurrenceId",
|
||||
"ordinal",
|
||||
"kind",
|
||||
"bbox",
|
||||
"center",
|
||||
"area",
|
||||
"volume",
|
||||
"faceStart",
|
||||
"faceCount",
|
||||
"edgeStart",
|
||||
"edgeCount",
|
||||
"vertexStart",
|
||||
"vertexCount",
|
||||
],
|
||||
"faceColumns": [
|
||||
"id",
|
||||
"occurrenceId",
|
||||
"shapeId",
|
||||
"ordinal",
|
||||
"surfaceType",
|
||||
"area",
|
||||
"center",
|
||||
"normal",
|
||||
"bbox",
|
||||
"edgeStart",
|
||||
"edgeCount",
|
||||
"relevance",
|
||||
"flags",
|
||||
"params",
|
||||
"triangleStart",
|
||||
"triangleCount",
|
||||
],
|
||||
"edgeColumns": [
|
||||
"id",
|
||||
"occurrenceId",
|
||||
"shapeId",
|
||||
"ordinal",
|
||||
"curveType",
|
||||
"length",
|
||||
"center",
|
||||
"bbox",
|
||||
"faceStart",
|
||||
"faceCount",
|
||||
"vertexStart",
|
||||
"vertexCount",
|
||||
"relevance",
|
||||
"flags",
|
||||
"params",
|
||||
"segmentStart",
|
||||
"segmentCount",
|
||||
],
|
||||
"vertexColumns": [
|
||||
"id",
|
||||
"occurrenceId",
|
||||
"shapeId",
|
||||
"ordinal",
|
||||
"center",
|
||||
"bbox",
|
||||
"edgeStart",
|
||||
"edgeCount",
|
||||
"relevance",
|
||||
"flags",
|
||||
],
|
||||
},
|
||||
"occurrences": [
|
||||
[
|
||||
"o1",
|
||||
"1",
|
||||
"Root",
|
||||
"Root",
|
||||
None,
|
||||
[1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1],
|
||||
{"min": [0.0, 0.0, 0.0], "max": [10.0, 10.0, 10.0]},
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
1,
|
||||
],
|
||||
[
|
||||
"o1.2",
|
||||
"1.2",
|
||||
"Bracket",
|
||||
"Bracket",
|
||||
"o1",
|
||||
[1, 0, 0, 5, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1],
|
||||
{"min": [5.0, 0.0, 0.0], "max": [10.0, 10.0, 10.0]},
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
1,
|
||||
],
|
||||
],
|
||||
"shapes": [
|
||||
[
|
||||
"o1.2.s1",
|
||||
"o1.2",
|
||||
1,
|
||||
"solid",
|
||||
{"min": [5.0, 0.0, 0.0], "max": [10.0, 10.0, 10.0]},
|
||||
[7.5, 5.0, 5.0],
|
||||
100.0,
|
||||
250.0,
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
1,
|
||||
]
|
||||
],
|
||||
"faces": [
|
||||
[
|
||||
"o1.2.f1",
|
||||
"o1.2",
|
||||
"o1.2.s1",
|
||||
1,
|
||||
"plane",
|
||||
20.0,
|
||||
[6.0, 1.0, 0.0],
|
||||
[0.0, 0.0, 1.0],
|
||||
{"min": [5.0, 0.0, 0.0], "max": [7.0, 2.0, 0.0]},
|
||||
0,
|
||||
2,
|
||||
80,
|
||||
0,
|
||||
{"origin": [5.0, 0.0, 0.0], "axis": [0.0, 0.0, 1.0]},
|
||||
0,
|
||||
0,
|
||||
],
|
||||
[
|
||||
"o1.2.f2",
|
||||
"o1.2",
|
||||
"o1.2.s1",
|
||||
2,
|
||||
"cylinder",
|
||||
12.0,
|
||||
[7.0, 2.0, 1.0],
|
||||
[1.0, 0.0, 0.0],
|
||||
{"min": [6.0, 1.0, 0.0], "max": [8.0, 3.0, 2.0]},
|
||||
1,
|
||||
0,
|
||||
60,
|
||||
0,
|
||||
{"center": [7.0, 2.0, 1.0], "axis": [1.0, 0.0, 0.0], "radius": 1.0},
|
||||
0,
|
||||
0,
|
||||
],
|
||||
],
|
||||
"edges": [
|
||||
[
|
||||
"o1.2.e1",
|
||||
"o1.2",
|
||||
"o1.2.s1",
|
||||
1,
|
||||
"line",
|
||||
4.0,
|
||||
[6.0, 1.0, 0.0],
|
||||
{"min": [5.0, 0.0, 0.0], "max": [7.0, 2.0, 0.0]},
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
1,
|
||||
90,
|
||||
0,
|
||||
{"origin": [5.0, 0.0, 0.0], "direction": [1.0, 0.0, 0.0]},
|
||||
0,
|
||||
0,
|
||||
],
|
||||
[
|
||||
"o1.2.e2",
|
||||
"o1.2",
|
||||
"o1.2.s1",
|
||||
2,
|
||||
"line",
|
||||
3.0,
|
||||
[5.5, 0.5, 0.0],
|
||||
{"min": [5.0, 0.0, 0.0], "max": [6.0, 1.0, 0.0]},
|
||||
2,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
75,
|
||||
0,
|
||||
{"origin": [5.0, 0.0, 0.0], "direction": [0.0, 1.0, 0.0]},
|
||||
0,
|
||||
0,
|
||||
],
|
||||
],
|
||||
"vertices": [
|
||||
[
|
||||
"o1.2.v1",
|
||||
"o1.2",
|
||||
"o1.2.s1",
|
||||
1,
|
||||
[5.0, 0.0, 0.0],
|
||||
{"min": [5.0, 0.0, 0.0], "max": [5.0, 0.0, 0.0]},
|
||||
0,
|
||||
2,
|
||||
95,
|
||||
0,
|
||||
]
|
||||
],
|
||||
"assemblyMates": [
|
||||
{
|
||||
"id": "m1",
|
||||
"label": "m1",
|
||||
"sourceLabel": "face_to_face:block_pocket_floor_offset:bottom_center",
|
||||
"type": "face_to_face",
|
||||
"fixed": "block_pocket_floor:offset",
|
||||
"moving": "bottom_center",
|
||||
"parameters": {"offset": 0.2},
|
||||
"fixedEndpoint": {
|
||||
"position": [6.0, 1.0, 0.0],
|
||||
"axes": {
|
||||
"z": [0.0, 0.0, 1.0],
|
||||
},
|
||||
},
|
||||
"movingEndpoint": {
|
||||
"position": [7.0, 2.0, 1.0],
|
||||
"axes": {
|
||||
"z": [0.0, 0.0, 1.0],
|
||||
},
|
||||
},
|
||||
}
|
||||
],
|
||||
"relations": {
|
||||
"faceEdgeRows": [0, 1, 0],
|
||||
"edgeFaceRows": [0, 1, 0],
|
||||
"edgeVertexRows": [0, 0],
|
||||
"vertexEdgeRows": [0, 1],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _summary_manifest(cad_ref: str) -> dict[str, object]:
|
||||
return {
|
||||
"schemaVersion": STEP_TOPOLOGY_SCHEMA_VERSION,
|
||||
"profile": "summary",
|
||||
"cadPath": cad_ref,
|
||||
"stepPath": f"{cad_ref}.step",
|
||||
"stepHash": "step-hash-123",
|
||||
"bbox": {"min": [0.0, 0.0, 0.0], "max": [10.0, 10.0, 10.0]},
|
||||
"stats": {
|
||||
"occurrenceCount": 1,
|
||||
"leafOccurrenceCount": 1,
|
||||
"shapeCount": 1,
|
||||
"faceCount": 2,
|
||||
"edgeCount": 2,
|
||||
"vertexCount": 1,
|
||||
},
|
||||
"tables": {
|
||||
"occurrenceColumns": [
|
||||
"id",
|
||||
"path",
|
||||
"name",
|
||||
"sourceName",
|
||||
"parentId",
|
||||
"transform",
|
||||
"bbox",
|
||||
"shapeStart",
|
||||
"shapeCount",
|
||||
"faceStart",
|
||||
"faceCount",
|
||||
"edgeStart",
|
||||
"edgeCount",
|
||||
"vertexStart",
|
||||
"vertexCount",
|
||||
],
|
||||
"shapeColumns": [],
|
||||
"faceColumns": [],
|
||||
"edgeColumns": [],
|
||||
"vertexColumns": [],
|
||||
},
|
||||
"occurrences": [
|
||||
[
|
||||
"o1",
|
||||
"1",
|
||||
"Part",
|
||||
"Part",
|
||||
None,
|
||||
[1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1],
|
||||
{"min": [0.0, 0.0, 0.0], "max": [10.0, 10.0, 10.0]},
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
1,
|
||||
]
|
||||
],
|
||||
"shapes": [],
|
||||
"faces": [],
|
||||
"edges": [],
|
||||
"vertices": [],
|
||||
}
|
||||
|
||||
|
||||
class InspectRefsSyntaxTests(unittest.TestCase):
|
||||
def test_normalize_selector_list_inherits_occurrence_prefix(self) -> None:
|
||||
selectors = refs_syntax.normalize_selector_list("o1.2.f12,f13,e7,v2,s3")
|
||||
|
||||
self.assertEqual(
|
||||
["o1.2.f12", "o1.2.f13", "o1.2.e7", "o1.2.v2", "o1.2.s3"],
|
||||
selectors,
|
||||
)
|
||||
|
||||
class InspectRefsTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self._isolated_roots = IsolatedCadRoots(self, prefix="refs-inspect-")
|
||||
tempdir = self._isolated_roots.temporary_cad_directory(prefix="tmp-refs-inspect-")
|
||||
self._tempdir = tempdir
|
||||
self.temp_root = Path(tempdir.name)
|
||||
self.relative_dir = self.temp_root.relative_to(assembly_spec.CAD_ROOT).as_posix()
|
||||
self.lookup_ref = f"{self.relative_dir}/sample"
|
||||
self.cad_ref = self.lookup_ref
|
||||
self.step_path = self.temp_root / "sample.step"
|
||||
self.step_path.write_text("ISO-10303-21; END-ISO-10303-21;\n")
|
||||
self.addCleanup(self._tempdir.cleanup)
|
||||
self.addCleanup(lambda: shutil.rmtree(self.temp_root, ignore_errors=True))
|
||||
|
||||
def _touch_glb(self, step_path: Path | None = None) -> Path:
|
||||
glb_path = part_glb_path(step_path or self.step_path)
|
||||
glb_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
glb_path.write_bytes(b"glb")
|
||||
return glb_path
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _mock_glb_topology(
|
||||
self,
|
||||
manifest: dict[str, object],
|
||||
*,
|
||||
step_path: Path | None = None,
|
||||
buffers: dict[str, array] | None = None,
|
||||
include_selector: bool = True,
|
||||
include_index: bool = True,
|
||||
current_hash: str | None = None,
|
||||
strip_selector_keys: tuple[str, ...] = (),
|
||||
):
|
||||
resolved_step_path = step_path or self.step_path
|
||||
edge_rendering = {
|
||||
"visibilityClasses": ["feature", "tangent", "seam", "degenerate"],
|
||||
"generatedVisibilityClasses": ["feature"],
|
||||
"visibilityClassCounts": {"feature": 1},
|
||||
"generatedVisibilityClassCounts": {"feature": 1},
|
||||
}
|
||||
mesh = {
|
||||
"linearDeflection": 0.006,
|
||||
"angularDeflection": 0.2,
|
||||
"relative": True,
|
||||
"resolution": {
|
||||
"profile": "extra-fine",
|
||||
"hints": {
|
||||
"effectiveComplexityScore": 2,
|
||||
"curvaturePressureScore": 2,
|
||||
"leafOccurrenceCount": 1,
|
||||
"occurrenceFaceCount": 2,
|
||||
"occurrenceEdgeCount": 2,
|
||||
},
|
||||
},
|
||||
}
|
||||
topology_manifest = {"schemaVersion": STEP_TOPOLOGY_SCHEMA_VERSION, **manifest}
|
||||
source_kind = str(topology_manifest.get("sourceKind") or "step").strip().lower()
|
||||
source_path = resolved_step_path.with_suffix(".py") if source_kind == "python" else resolved_step_path
|
||||
topology_manifest.setdefault("sourceKind", source_kind)
|
||||
topology_manifest.setdefault("sourcePath", self._manifest_path(source_path))
|
||||
topology_manifest.setdefault("stepPath", self._manifest_path(resolved_step_path))
|
||||
topology_manifest.setdefault("edgeRendering", edge_rendering)
|
||||
topology_manifest.setdefault("mesh", mesh)
|
||||
edge_manifest = {
|
||||
"schemaVersion": STEP_TOPOLOGY_SCHEMA_VERSION,
|
||||
"profile": "surface-edges",
|
||||
"edgeRendering": edge_rendering,
|
||||
"primitiveAttributes": {
|
||||
"barycentric": "_CAD_EDGE_BARYCENTRIC",
|
||||
"class": "_CAD_EDGE_CLASS",
|
||||
},
|
||||
"buffers": {"views": {"surfaceHalfEdges": {}}},
|
||||
}
|
||||
if source_kind == "python":
|
||||
edge_manifest["sourceKind"] = "python"
|
||||
edge_manifest["sourcePath"] = topology_manifest.get("sourcePath")
|
||||
edge_manifest["sourceHash"] = topology_manifest.get("sourceHash")
|
||||
if topology_manifest.get("stepHash"):
|
||||
edge_manifest["stepHash"] = topology_manifest.get("stepHash")
|
||||
else:
|
||||
edge_manifest["sourceKind"] = "step"
|
||||
edge_manifest["sourcePath"] = topology_manifest.get("sourcePath")
|
||||
edge_manifest["stepHash"] = topology_manifest.get("stepHash")
|
||||
self._touch_glb(resolved_step_path)
|
||||
stack = contextlib.ExitStack()
|
||||
with stack:
|
||||
stack.enter_context(mock.patch.object(step_targets, "find_step_path", return_value=resolved_step_path))
|
||||
expected_step_hash = str(topology_manifest.get("stepHash") or "") if current_hash is None else current_hash
|
||||
stack.enter_context(mock.patch.object(step_targets, "step_file_hash", return_value=expected_step_hash))
|
||||
stack.enter_context(mock.patch.object(cad_generation, "step_file_hash", return_value=expected_step_hash))
|
||||
stack.enter_context(
|
||||
mock.patch.object(
|
||||
step_targets,
|
||||
"read_step_topology_manifest_from_glb",
|
||||
return_value=topology_manifest if include_index else None,
|
||||
)
|
||||
)
|
||||
stack.enter_context(
|
||||
mock.patch.object(
|
||||
step_targets,
|
||||
"read_step_display_edge_manifest_from_glb",
|
||||
return_value=edge_manifest,
|
||||
)
|
||||
)
|
||||
stack.enter_context(mock.patch.object(step_targets, "glb_primitives_have_surface_edge_attributes", return_value=True))
|
||||
stack.enter_context(mock.patch.object(step_targets, "glb_surface_edge_class_has_nonzero_values", return_value=True))
|
||||
selector_topology_manifest = {
|
||||
key: value for key, value in topology_manifest.items() if key not in strip_selector_keys
|
||||
}
|
||||
stack.enter_context(
|
||||
mock.patch.object(
|
||||
step_targets,
|
||||
"read_step_topology_bundle_from_glb",
|
||||
return_value=(
|
||||
SelectorBundle(manifest=selector_topology_manifest, buffers=buffers or {})
|
||||
if include_selector
|
||||
else None
|
||||
),
|
||||
)
|
||||
)
|
||||
yield
|
||||
|
||||
def _manifest_path(self, path: Path) -> str:
|
||||
resolved = path.resolve()
|
||||
try:
|
||||
return resolved.relative_to(assembly_spec.REPO_ROOT).as_posix()
|
||||
except ValueError:
|
||||
return resolved.as_posix()
|
||||
|
||||
def test_whole_entry_summary_uses_glb_index(self) -> None:
|
||||
with self._mock_glb_topology(_summary_manifest(self.cad_ref), include_selector=False):
|
||||
result = refs_inspect.inspect_cad_refs(self.cad_ref)
|
||||
|
||||
self.assertTrue(result["ok"])
|
||||
token = result["tokens"][0]
|
||||
self.assertEqual(1, token["summary"]["occurrenceCount"])
|
||||
self.assertEqual(2, token["summary"]["faceCount"])
|
||||
self.assertEqual([], token["selections"])
|
||||
|
||||
def test_facts_kind_falls_back_to_index_manifest_for_assembly(self) -> None:
|
||||
manifest = {**_refs_manifest(self.cad_ref), "entryKind": "assembly"}
|
||||
|
||||
with self._mock_glb_topology(manifest, strip_selector_keys=("entryKind", "assembly")):
|
||||
result = refs_inspect.inspect_cad_refs(self.cad_ref, facts=True)
|
||||
|
||||
self.assertTrue(result["ok"])
|
||||
token = result["tokens"][0]
|
||||
self.assertEqual("assembly", token["summary"]["kind"])
|
||||
self.assertEqual("assembly", token["entryFacts"]["kind"])
|
||||
|
||||
def test_python_backed_glb_only_entry_inspects_without_step_file(self) -> None:
|
||||
self.step_path.unlink()
|
||||
script_path = self.step_path.with_suffix(".py")
|
||||
script_path.write_text("def gen_step():\n return object()\n", encoding="utf-8")
|
||||
source_identity = python_source_hash(script_path)
|
||||
manifest = {
|
||||
**_summary_manifest(self.cad_ref),
|
||||
"sourceKind": "python",
|
||||
"sourceHash": source_identity.source_hash,
|
||||
}
|
||||
|
||||
with self._mock_glb_topology(manifest, include_selector=False):
|
||||
result = refs_inspect.inspect_cad_refs(self.cad_ref)
|
||||
|
||||
self.assertTrue(result["ok"])
|
||||
token = result["tokens"][0]
|
||||
self.assertEqual(refs_inspect._relative_to_repo(self.step_path), token["stepPath"])
|
||||
self.assertEqual(1, token["summary"]["occurrenceCount"])
|
||||
|
||||
def test_context_provider_can_supply_in_memory_entry_context(self) -> None:
|
||||
requested_profiles = []
|
||||
|
||||
def provider(cad_path, profile):
|
||||
requested_profiles.append(profile)
|
||||
if cad_path != self.cad_ref:
|
||||
return None
|
||||
manifest = _summary_manifest(cad_path)
|
||||
return refs_inspect.EntryContext(
|
||||
cad_path=cad_path,
|
||||
kind="part",
|
||||
source_path=self.step_path,
|
||||
step_path=self.step_path,
|
||||
manifest=manifest,
|
||||
selector_index=refs_inspect.lookup.build_selector_index(manifest),
|
||||
)
|
||||
|
||||
result = refs_inspect.inspect_cad_refs(self.cad_ref, context_provider=provider)
|
||||
|
||||
self.assertTrue(result["ok"])
|
||||
self.assertEqual([SelectorProfile.SUMMARY], requested_profiles)
|
||||
self.assertEqual(2, result["tokens"][0]["summary"]["faceCount"])
|
||||
|
||||
def test_face_lookup_resolves_single_occurrence_alias_and_detail(self) -> None:
|
||||
with self._mock_glb_topology(_refs_manifest(self.cad_ref)):
|
||||
result = refs_inspect.inspect_cad_refs(self.cad_ref, "#o1.2.f1", detail=True)
|
||||
|
||||
self.assertTrue(result["ok"])
|
||||
selection = result["tokens"][0]["selections"][0]
|
||||
self.assertEqual("face", selection["selectorType"])
|
||||
self.assertEqual("o1.2.f1", selection["normalizedSelector"])
|
||||
self.assertEqual("plane area=20.0", selection["summary"])
|
||||
self.assertEqual(["e1", "e2"], selection["detail"]["adjacentEdgeSelectors"])
|
||||
|
||||
def test_assembly_mate_lookup_resolves_numbered_ref(self) -> None:
|
||||
with self._mock_glb_topology(_refs_manifest(self.cad_ref)):
|
||||
result = refs_inspect.inspect_cad_refs(self.cad_ref, "#m1", detail=True, positioning=True)
|
||||
|
||||
self.assertTrue(result["ok"])
|
||||
selection = result["tokens"][0]["selections"][0]
|
||||
self.assertEqual("mate", selection["selectorType"])
|
||||
self.assertEqual("m1", selection["normalizedSelector"])
|
||||
self.assertEqual("#m1", selection["copyText"])
|
||||
self.assertEqual("Mate face_to_face:block_pocket_floor_offset:bottom_center", selection["label"])
|
||||
self.assertEqual("face_to_face block_pocket_floor:offset -> bottom_center", selection["summary"])
|
||||
self.assertEqual("face_to_face:block_pocket_floor_offset:bottom_center", selection["detail"]["sourceLabel"])
|
||||
self.assertEqual({"offset": 0.2}, selection["detail"]["parameters"])
|
||||
self.assertEqual("mate", selection["positioning"]["selectorType"])
|
||||
self.assertEqual([6.0, 1.0, 0.0], selection["positioning"]["fixedEndpoint"]["position"])
|
||||
|
||||
def test_vertex_lookup_resolves_corner_detail(self) -> None:
|
||||
with self._mock_glb_topology(_refs_manifest(self.cad_ref)):
|
||||
result = refs_inspect.inspect_cad_refs(self.cad_ref, "#o1.2.v1", detail=True)
|
||||
|
||||
self.assertTrue(result["ok"])
|
||||
selection = result["tokens"][0]["selections"][0]
|
||||
self.assertEqual("vertex", selection["selectorType"])
|
||||
self.assertEqual("o1.2.v1", selection["normalizedSelector"])
|
||||
self.assertEqual("corner edges=2", selection["summary"])
|
||||
self.assertEqual(["e1", "e2"], selection["detail"]["adjacentEdgeSelectors"])
|
||||
self.assertEqual(["f1", "f2"], selection["detail"]["adjacentFaceSelectors"])
|
||||
|
||||
def test_single_occurrence_alias_is_compacted_in_copy_text(self) -> None:
|
||||
with self._mock_glb_topology(_summary_manifest(self.cad_ref)):
|
||||
result = refs_inspect.inspect_cad_refs(self.cad_ref, "#f2", detail=True)
|
||||
|
||||
self.assertFalse(result["ok"])
|
||||
|
||||
with self._mock_glb_topology(
|
||||
{
|
||||
**_refs_manifest(self.cad_ref),
|
||||
"stats": {
|
||||
"occurrenceCount": 1,
|
||||
"leafOccurrenceCount": 1,
|
||||
"shapeCount": 1,
|
||||
"faceCount": 2,
|
||||
"edgeCount": 2,
|
||||
"vertexCount": 1,
|
||||
},
|
||||
"occurrences": [_refs_manifest(self.cad_ref)["occurrences"][1]],
|
||||
},
|
||||
):
|
||||
result = refs_inspect.inspect_cad_refs(self.cad_ref, "#v1", detail=True)
|
||||
|
||||
self.assertTrue(result["ok"])
|
||||
selection = result["tokens"][0]["selections"][0]
|
||||
self.assertEqual("v1", selection["displaySelector"])
|
||||
self.assertEqual("#v1", selection["copyText"])
|
||||
|
||||
def test_old_part_selector_syntax_is_rejected(self) -> None:
|
||||
with self._mock_glb_topology(_refs_manifest(self.cad_ref)):
|
||||
result = refs_inspect.inspect_cad_refs(self.cad_ref, "#p:legacy.f1")
|
||||
|
||||
self.assertFalse(result["ok"])
|
||||
self.assertEqual("selector", result["errors"][0]["kind"])
|
||||
|
||||
def test_topology_flag_returns_full_selector_lists(self) -> None:
|
||||
with self._mock_glb_topology(_refs_manifest(self.cad_ref)):
|
||||
result = refs_inspect.inspect_cad_refs(self.cad_ref, include_topology=True)
|
||||
|
||||
self.assertTrue(result["ok"])
|
||||
topology = result["tokens"][0]["topology"]
|
||||
self.assertIn("f1", topology["faces"])
|
||||
self.assertIn("e1", topology["edges"])
|
||||
self.assertIn("v1", topology["vertices"])
|
||||
|
||||
def test_detail_uses_glb_buffer_backed_relation_rows(self) -> None:
|
||||
manifest = {
|
||||
**_refs_manifest(self.cad_ref),
|
||||
"relations": {
|
||||
"faceEdgeRowsView": "faceEdgeRows",
|
||||
"edgeFaceRowsView": "edgeFaceRows",
|
||||
"edgeVertexRowsView": "edgeVertexRows",
|
||||
"vertexEdgeRowsView": "vertexEdgeRows",
|
||||
},
|
||||
}
|
||||
buffers = {
|
||||
"faceEdgeRows": array("I", [0, 1, 0]),
|
||||
"edgeFaceRows": array("I", [0, 1, 0]),
|
||||
"edgeVertexRows": array("I", [0, 0]),
|
||||
"vertexEdgeRows": array("I", [0, 1]),
|
||||
}
|
||||
|
||||
with self._mock_glb_topology(manifest, buffers=buffers):
|
||||
result = refs_inspect.inspect_cad_refs(self.cad_ref, "#o1.2.f1", detail=True)
|
||||
|
||||
self.assertTrue(result["ok"])
|
||||
selection = result["tokens"][0]["selections"][0]
|
||||
self.assertEqual(["e1", "e2"], selection["detail"]["adjacentEdgeSelectors"])
|
||||
|
||||
def test_missing_glb_topology_is_an_inspect_error(self) -> None:
|
||||
result = refs_inspect.inspect_cad_refs(self.cad_ref)
|
||||
|
||||
self.assertFalse(result["ok"])
|
||||
error = result["errors"][0]
|
||||
self.assertEqual("glb_regeneration_failed", error["code"])
|
||||
self.assertIn("\nRegenerate STEP artifacts with the following command using the CAD skill:", error["message"])
|
||||
self.assertNotIn("scripts.step", error["message"])
|
||||
self.assertIn("regenerateCommand", error)
|
||||
self.assertEqual("python scripts/step", error["regenerateCommand"])
|
||||
|
||||
def test_missing_selector_topology_is_an_inspect_error(self) -> None:
|
||||
with self._mock_glb_topology(_refs_manifest(self.cad_ref), include_selector=False):
|
||||
result = refs_inspect.inspect_cad_refs(self.cad_ref, "#f1")
|
||||
|
||||
self.assertFalse(result["ok"])
|
||||
error = result["errors"][0]
|
||||
self.assertEqual("glb_regeneration_failed", error["code"])
|
||||
self.assertIn("\nRegenerate STEP artifacts with the following command using the CAD skill:", error["message"])
|
||||
|
||||
def test_missing_step_topology_is_an_inspect_error(self) -> None:
|
||||
with self._mock_glb_topology(_summary_manifest(self.cad_ref), include_index=False):
|
||||
result = refs_inspect.inspect_cad_refs(self.cad_ref)
|
||||
|
||||
self.assertFalse(result["ok"])
|
||||
error = result["errors"][0]
|
||||
self.assertEqual("glb_regeneration_failed", error["code"])
|
||||
self.assertIn("\nRegenerate STEP artifacts with the following command using the CAD skill:", error["message"])
|
||||
|
||||
def test_unsupported_step_topology_is_an_inspect_error(self) -> None:
|
||||
self._touch_glb()
|
||||
manifest = {**_summary_manifest(self.cad_ref), "schemaVersion": STEP_TOPOLOGY_SCHEMA_VERSION + 1}
|
||||
|
||||
with mock.patch.object(step_targets, "find_step_path", return_value=self.step_path), mock.patch.object(
|
||||
step_targets,
|
||||
"read_step_topology_manifest_from_glb",
|
||||
return_value=manifest,
|
||||
):
|
||||
result = refs_inspect.inspect_cad_refs(self.cad_ref)
|
||||
|
||||
self.assertFalse(result["ok"])
|
||||
error = result["errors"][0]
|
||||
self.assertEqual("glb_regeneration_failed", error["code"])
|
||||
self.assertIn("\nRegenerate STEP artifacts with the following command using the CAD skill:", error["message"])
|
||||
|
||||
def test_stale_glb_topology_is_an_inspect_error(self) -> None:
|
||||
with self._mock_glb_topology(_refs_manifest(self.cad_ref), current_hash="new-step-hash"):
|
||||
result = refs_inspect.inspect_cad_refs(self.cad_ref)
|
||||
|
||||
self.assertFalse(result["ok"])
|
||||
error = result["errors"][0]
|
||||
self.assertEqual("glb_regeneration_failed", error["code"])
|
||||
self.assertIn("\nRegenerate STEP artifacts with the following command using the CAD skill:", error["message"])
|
||||
|
||||
def test_legacy_cad_ref_mismatch_is_accepted_when_hash_matches(self) -> None:
|
||||
with self._mock_glb_topology({**_refs_manifest("other/ref"), "stepHash": "step-hash-123"}):
|
||||
result = refs_inspect.inspect_cad_refs(self.cad_ref)
|
||||
|
||||
self.assertTrue(result["ok"])
|
||||
self.assertEqual(self.cad_ref, result["tokens"][0]["cadPath"])
|
||||
|
||||
def test_non_leaf_occurrence_detail_reports_children(self) -> None:
|
||||
with self._mock_glb_topology(_refs_manifest(self.cad_ref)):
|
||||
result = refs_inspect.inspect_cad_refs(self.cad_ref, "#o1", detail=True)
|
||||
|
||||
self.assertTrue(result["ok"])
|
||||
selection = result["tokens"][0]["selections"][0]
|
||||
self.assertEqual("occurrence", selection["selectorType"])
|
||||
self.assertEqual("o1", selection["normalizedSelector"])
|
||||
self.assertEqual(1, selection["detail"]["childCount"])
|
||||
self.assertEqual(["o1.2"], selection["detail"]["descendantOccurrenceIds"])
|
||||
|
||||
def test_assembly_topology_lookup_resolves_from_generated_step(self) -> None:
|
||||
assembly_cad_ref = f"{self.relative_dir}/sample-assembly"
|
||||
assembly_path = self.temp_root / "sample-assembly.py"
|
||||
assembly_step_path = self.temp_root / "sample-assembly.step"
|
||||
assembly_path.write_text(
|
||||
"def gen_step():\n"
|
||||
" return {'instances': []}\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
assembly_step_path.write_text("ISO-10303-21; END-ISO-10303-21;\n", encoding="utf-8")
|
||||
source_identity = python_source_hash(assembly_path)
|
||||
|
||||
with mock.patch.object(
|
||||
step_targets,
|
||||
"resolve_cad_source_path",
|
||||
return_value=("assembly", assembly_path),
|
||||
), self._mock_glb_topology(
|
||||
{
|
||||
**_refs_manifest(assembly_cad_ref),
|
||||
"sourceKind": "python",
|
||||
"sourceHash": source_identity.source_hash,
|
||||
"stepHash": cad_generation.step_file_hash(assembly_step_path),
|
||||
},
|
||||
step_path=assembly_step_path,
|
||||
):
|
||||
result = refs_inspect.inspect_cad_refs(assembly_cad_ref, "#o1.2.f1", detail=True)
|
||||
|
||||
self.assertTrue(result["ok"])
|
||||
selection = result["tokens"][0]["selections"][0]
|
||||
self.assertEqual("assembly", result["tokens"][0]["summary"]["kind"])
|
||||
self.assertEqual("face", selection["selectorType"])
|
||||
self.assertEqual("o1.2.f1", selection["normalizedSelector"])
|
||||
|
||||
def test_positioning_flag_returns_plane_facts(self) -> None:
|
||||
with self._mock_glb_topology(_refs_manifest(self.cad_ref)):
|
||||
result = refs_inspect.inspect_cad_refs(
|
||||
self.cad_ref,
|
||||
"#o1.2.f1",
|
||||
positioning=True,
|
||||
)
|
||||
|
||||
self.assertTrue(result["ok"])
|
||||
positioning = result["tokens"][0]["selections"][0]["positioning"]
|
||||
self.assertEqual("plane", positioning["kind"])
|
||||
self.assertEqual("z", positioning["axis"])
|
||||
self.assertEqual(0.0, positioning["coordinate"])
|
||||
self.assertEqual([0.0, 0.0, 1.0], positioning["normal"])
|
||||
|
||||
def test_planes_flag_returns_entry_planes(self) -> None:
|
||||
with self._mock_glb_topology(_refs_manifest(self.cad_ref)):
|
||||
result = refs_inspect.inspect_cad_refs(
|
||||
self.cad_ref,
|
||||
planes=True,
|
||||
plane_coordinate_tolerance=0.01,
|
||||
plane_min_area_ratio=0.0,
|
||||
plane_limit=1,
|
||||
)
|
||||
|
||||
self.assertTrue(result["ok"])
|
||||
planes = result["tokens"][0]["planes"]
|
||||
self.assertEqual(1, len(planes))
|
||||
self.assertEqual("z", planes[0]["axis"])
|
||||
|
||||
def test_refs_text_format_includes_entry_reports(self) -> None:
|
||||
result = {
|
||||
"ok": True,
|
||||
"tokens": [
|
||||
{
|
||||
"cadPath": self.cad_ref,
|
||||
"summary": {"faceCount": 2, "edgeCount": 2},
|
||||
"entryFacts": {
|
||||
"size": [10.0, 10.0, 10.0],
|
||||
"center": [5.0, 5.0, 5.0],
|
||||
"extentAxis": "x",
|
||||
"diag": 17.320508,
|
||||
"kind": "part",
|
||||
},
|
||||
"planes": [
|
||||
{
|
||||
"axis": "z",
|
||||
"coordinate": 0.0,
|
||||
"normalSign": 1,
|
||||
"faceCount": 1,
|
||||
"totalArea": 100.0,
|
||||
}
|
||||
],
|
||||
"selections": [],
|
||||
}
|
||||
],
|
||||
"errors": [],
|
||||
}
|
||||
|
||||
text = inspect_cli._format_refs_text(result, quiet=False, verbose=False)
|
||||
|
||||
self.assertIn("facts: size=[10, 10, 10]", text)
|
||||
self.assertIn("planes: 1 major groups", text)
|
||||
self.assertIn("z=0", text)
|
||||
|
||||
def test_diff_planes_returns_entry_planes(self) -> None:
|
||||
with self._mock_glb_topology(_refs_manifest(self.cad_ref)):
|
||||
result = refs_inspect.diff_entry_targets(
|
||||
self.cad_ref,
|
||||
self.cad_ref,
|
||||
planes=True,
|
||||
plane_limit=1,
|
||||
)
|
||||
|
||||
self.assertTrue(result["ok"])
|
||||
self.assertEqual(1, len(result["diff"]["leftMajorPlanes"]))
|
||||
self.assertEqual(1, len(result["diff"]["rightMajorPlanes"]))
|
||||
|
||||
def test_cli_parses_current_agentic_commands(self) -> None:
|
||||
parser = inspect_cli.build_parser()
|
||||
|
||||
refs_args = parser.parse_args(["refs", "entry.step", "#f1", "--detail", "--facts"])
|
||||
self.assertEqual("refs", refs_args.command)
|
||||
self.assertTrue(refs_args.detail)
|
||||
self.assertTrue(refs_args.facts)
|
||||
|
||||
diff_args = parser.parse_args(
|
||||
["diff", "left", "right", "--planes", "--plane-coordinate-tolerance", "0.02", "--plane-limit", "3"]
|
||||
)
|
||||
self.assertTrue(diff_args.planes)
|
||||
self.assertEqual(0.02, diff_args.plane_coordinate_tolerance)
|
||||
self.assertEqual(3, diff_args.plane_limit)
|
||||
|
||||
with contextlib.redirect_stderr(io.StringIO()), self.assertRaises(SystemExit) as render_exit:
|
||||
parser.parse_args(["render", "list", "part.step", "--format", "text"])
|
||||
self.assertEqual(2, render_exit.exception.code)
|
||||
|
||||
worker_args = parser.parse_args(["worker"])
|
||||
self.assertEqual("worker", worker_args.command)
|
||||
|
||||
top_level_verbose_args = parser.parse_args(["--verbose", "refs", "entry.step", "#f1"])
|
||||
self.assertTrue(top_level_verbose_args.verbose)
|
||||
|
||||
with contextlib.redirect_stderr(io.StringIO()), self.assertRaises(SystemExit) as verbose_render_exit:
|
||||
parser.parse_args(["--verbose", "render", "view", "part.step", "--output", "part.png"])
|
||||
self.assertEqual(2, verbose_render_exit.exception.code)
|
||||
|
||||
def test_worker_response_wraps_inspect_result(self) -> None:
|
||||
response = inspect_cli._worker_response('{"id":"missing-input","argv":["refs"]}')
|
||||
|
||||
self.assertFalse(response["ok"])
|
||||
self.assertEqual("missing-input", response["id"])
|
||||
self.assertEqual(2, response["exitCode"])
|
||||
self.assertIn("No STEP/CAD entry target provided", response["result"]["errors"][0]["message"])
|
||||
|
||||
def test_frame_command_returns_occurrence_axes(self) -> None:
|
||||
with self._mock_glb_topology(_refs_manifest(self.cad_ref)):
|
||||
result = refs_inspect.inspect_target_frame(self.cad_ref, "#o1.2")
|
||||
|
||||
self.assertTrue(result["ok"])
|
||||
self.assertEqual([5.0, 0.0, 0.0], result["frame"]["translation"])
|
||||
self.assertEqual([1.0, 0.0, 0.0], result["frame"]["localAxes"]["x"])
|
||||
|
||||
def test_measure_targets_returns_signed_axis_distance(self) -> None:
|
||||
with self._mock_glb_topology(_refs_manifest(self.cad_ref)):
|
||||
result = refs_inspect.measure_targets(
|
||||
self.cad_ref,
|
||||
"#o1.2.f1",
|
||||
"#o1.2.f2",
|
||||
axis="x",
|
||||
)
|
||||
|
||||
self.assertTrue(result["ok"])
|
||||
self.assertEqual("x", result["axis"])
|
||||
self.assertEqual(5.0, result["from"]["coordinate"])
|
||||
self.assertEqual(7.0, result["to"]["coordinate"])
|
||||
self.assertEqual(2.0, result["measurement"]["signedDistance"])
|
||||
|
||||
def test_align_targets_returns_flush_translation_delta(self) -> None:
|
||||
with self._mock_glb_topology(_refs_manifest(self.cad_ref)):
|
||||
result = refs_inspect.align_targets(
|
||||
self.cad_ref,
|
||||
"#o1.2.f1",
|
||||
"#o1.2.f2",
|
||||
axis="x",
|
||||
)
|
||||
|
||||
self.assertTrue(result["ok"])
|
||||
self.assertEqual([2.0, 0.0, 0.0], result["alignment"]["translationVector"])
|
||||
self.assertEqual(2.0, result["alignment"]["transformTranslationDelta"]["3"])
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user