first commit

This commit is contained in:
2026-07-22 13:48:46 +08:00
commit c87751c3dc
2820 changed files with 726976 additions and 0 deletions
@@ -0,0 +1,234 @@
from __future__ import annotations
import sys
import types
import unittest
from contextlib import contextmanager
from cadpy.assembly import AssemblyHelper, MateTarget, label_shape, label_text, target
from cadpy.step_export import _collect_assembly_mates
class FakeLocation:
def __init__(self, value):
self.value = value
def __mul__(self, other):
return FakeLocation(("mul", self.value, other.value))
class FakePart:
def __init__(self):
self.joints = {}
self.label = None
class FakeJoint:
def __init__(self, *, label, to_part, joint_location=None, **options):
self.label = label
self.to_part = to_part
self.location = joint_location
self.options = options
self.connections = []
to_part.joints[label] = self
def connect_to(self, other, **options):
self.connections.append((other, options))
class FakeCompound:
def __init__(self, *, label, children):
self.label = label
self.children = tuple(children)
@contextmanager
def fake_build123d():
module = types.SimpleNamespace(
BallJoint=FakeJoint,
Compound=FakeCompound,
CylindricalJoint=FakeJoint,
LinearJoint=FakeJoint,
Location=FakeLocation,
RevoluteJoint=FakeJoint,
RigidJoint=FakeJoint,
)
original = sys.modules.get("build123d")
sys.modules["build123d"] = module
try:
yield module
finally:
if original is None:
sys.modules.pop("build123d", None)
else:
sys.modules["build123d"] = original
class AssemblyHelperTests(unittest.TestCase):
def test_label_text_normalizes_tokens(self) -> None:
self.assertEqual(
"base_plate:left_side",
label_text("base plate", "left:side"),
)
def test_label_shape_sets_native_label_and_color(self) -> None:
shape = types.SimpleNamespace()
color = object()
returned = label_shape(shape, "m3 standoff", "front left", color=color)
self.assertIs(returned, shape)
self.assertEqual("m3_standoff:front_left", shape.label)
self.assertIs(color, shape.color)
def test_helper_connects_fixed_joint_to_moving_joint(self) -> None:
with fake_build123d():
assembly = AssemblyHelper("enclosure")
base = assembly.add(FakePart(), "base")
lid = assembly.add(FakePart(), "lid")
base_frame = assembly.rigid_frame(base, "lid_seat", FakeLocation("base_frame"))
lid_frame = assembly.rigid_frame(lid, "underside", FakeLocation("lid_frame"))
relation = assembly.face_to_face(base_frame, lid_frame)
fixed_joint = base.joints["lid_seat"]
moving_joint = lid.joints["underside"]
self.assertEqual("face_to_face", relation.relation)
self.assertEqual("lid_seat", relation.fixed)
self.assertEqual("underside", relation.moving)
self.assertEqual({"part": "base", "frame": "lid_seat"}, relation.fixed_endpoint)
self.assertEqual({"part": "lid", "frame": "underside"}, relation.moving_endpoint)
self.assertEqual([(moving_joint, {})], fixed_joint.connections)
def test_build_records_mate_endpoint_payloads(self) -> None:
with fake_build123d():
assembly = AssemblyHelper("enclosure")
base = assembly.add(FakePart(), "base")
lid = assembly.add(FakePart(), "lid")
base_frame = assembly.rigid_frame(base, "lid_seat", FakeLocation("base_frame"))
lid_frame = assembly.rigid_frame(lid, "underside", FakeLocation("lid_frame"))
assembly.face_to_face(base_frame, lid_frame, label="lid_mate")
compound = assembly.build()
self.assertEqual(
[
{
"id": "m1",
"label": "m1",
"sourceLabel": "lid_mate",
"type": "face_to_face",
"relation": "face_to_face",
"fixed": "lid_seat",
"moving": "underside",
"parameters": {},
"fixedEndpoint": {"part": "base", "frame": "lid_seat"},
"movingEndpoint": {"part": "lid", "frame": "underside"},
}
],
compound.assembly_mates,
)
def test_export_collection_renumbers_mates_globally(self) -> None:
with fake_build123d():
first = AssemblyHelper("first")
first_base = first.add(FakePart(), "first_base")
first_lid = first.add(FakePart(), "first_lid")
first_base_frame = first.rigid_frame(first_base, "seat", FakeLocation("first_base"))
first_lid_frame = first.rigid_frame(first_lid, "underside", FakeLocation("first_lid"))
first.face_to_face(first_base_frame, first_lid_frame, label="first_mate")
first_compound = first.build()
second = AssemblyHelper("second")
second_base = second.add(FakePart(), "second_base")
second_lid = second.add(FakePart(), "second_lid")
second_base_frame = second.rigid_frame(second_base, "seat", FakeLocation("second_base"))
second_lid_frame = second.rigid_frame(second_lid, "underside", FakeLocation("second_lid"))
second.face_to_face(second_base_frame, second_lid_frame, label="second_mate")
second_compound = second.build()
root = FakeCompound(label="root", children=[first_compound, second_compound])
mates = _collect_assembly_mates(root)
self.assertEqual(["m1", "m2"], [mate["id"] for mate in mates])
self.assertEqual(["m1", "m2"], [mate["label"] for mate in mates])
self.assertEqual(["first_mate", "second_mate"], [mate["sourceLabel"] for mate in mates])
def test_helper_accepts_existing_native_joint_labels(self) -> None:
with fake_build123d():
assembly = AssemblyHelper("hinge")
frame = FakePart()
leaf = FakePart()
fixed_joint = FakeJoint(
label="hinge_axis",
to_part=frame,
joint_location=FakeLocation("frame_axis"),
)
moving_joint = FakeJoint(
label="leaf_axis",
to_part=leaf,
joint_location=FakeLocation("leaf_axis"),
)
relation = assembly.revolute(
(frame, "hinge_axis"),
(leaf, "leaf_axis"),
angle=45,
)
self.assertEqual("revolute", relation.relation)
self.assertEqual({"angle": 45}, relation.parameters)
self.assertEqual([(moving_joint, {"angle": 45})], fixed_joint.connections)
def test_axis_frames_use_native_joint_axis_argument(self) -> None:
with fake_build123d():
assembly = AssemblyHelper("hinge")
frame = FakePart()
frame_target = assembly.revolute_frame(
frame,
"hinge_axis",
"Axis.Z",
angular_range=(-90, 90),
)
self.assertEqual(MateTarget(frame, "hinge_axis"), frame_target)
joint = frame.joints["hinge_axis"]
self.assertIsNone(joint.location)
self.assertEqual({"axis": "Axis.Z", "angular_range": (-90, 90)}, joint.options)
def test_offset_target_creates_temporary_native_joint(self) -> None:
with fake_build123d():
assembly = AssemblyHelper("offset")
base = FakePart()
lid = FakePart()
base_frame = assembly.rigid_frame(base, "seat", FakeLocation("base"))
lid_frame = assembly.rigid_frame(lid, "underside", FakeLocation("lid"))
relation = assembly.face_to_face(base_frame, lid_frame, offset=0.5)
offset_joint = base.joints["seat:offset"]
self.assertIsInstance(offset_joint.location, FakeLocation)
self.assertEqual(("mul", "base", (0.0, 0.0, 0.5)), offset_joint.location.value)
self.assertEqual("seat:offset", relation.fixed)
def test_build_returns_labeled_compound(self) -> None:
with fake_build123d():
assembly = AssemblyHelper("robot arm")
base = assembly.add(FakePart(), "base")
arm = assembly.add(FakePart(), "arm")
compound = assembly.build()
self.assertEqual("robot_arm", compound.label)
self.assertEqual((base, arm), compound.children)
def test_target_tuple_is_available_for_call_sites(self) -> None:
part = object()
self.assertEqual(MateTarget(part, "axis"), target(part, "axis"))
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,460 @@
from __future__ import annotations
import contextlib
import io
import tempfile
import unittest
import warnings
from pathlib import Path
from types import SimpleNamespace
from unittest import mock
from tests.python.support.paths import add_repo_path
add_repo_path("packages/cadpy/src")
from cadpy import generation
from cadpy.assembly_spec import (
IDENTITY_TRANSFORM,
AssemblyInstanceSpec,
AssemblyNodeSpec,
AssemblySpec,
)
from cadpy.catalog import CadSource
from cadpy.metadata import parse_generator_metadata
from cadpy.step_export import _create_bin_xcaf_doc, export_build123d_step_scene
from cadpy.step_scene import LoadedStepScene, _bbox_from_shape, scene_leaf_occurrences, scene_occurrence_shape
def _rounded_color(color: tuple[float, ...]) -> tuple[float, ...]:
return tuple(round(component, 3) for component in color)
def _srgb_to_linear(component: float) -> float:
if component <= 0.04045:
return component / 12.92
return ((component + 0.055) / 1.055) ** 2.4
class CompoundAssemblyGenerationTests(unittest.TestCase):
def test_step_payload_rejects_legacy_output_field(self) -> None:
with self.assertRaisesRegex(TypeError, "unsupported field\\(s\\): step_output"):
generation._normalize_step_payload(
{"shape": object(), "step_output": "legacy.step"},
script_path=Path("part.py"),
)
def test_step_payload_preserves_instance_assembly_mates(self) -> None:
payload = generation._normalize_step_payload(
{
"instances": [
{
"path": "servo.step",
"name": "servo",
}
],
"assembly_mates": [
{
"sourceLabel": "servo_to_bracket",
"relation": "rigid",
"fixed": "servo_mount",
"moving": "bracket_foot",
}
],
},
script_path=Path("assembly.py"),
)
self.assertEqual("servo_to_bracket", payload["assembly_mates"][0]["sourceLabel"])
def test_attach_envelope_assembly_mates_renumbers_for_scene(self) -> None:
scene = LoadedStepScene(step_path=Path("assembly.step"), roots=[], prototype_shapes={})
generation._attach_envelope_assembly_mates(
scene,
{
"assembly_mates": [
{
"sourceLabel": "servo_to_bracket",
"relation": "rigid",
"fixed": "servo_mount",
"moving": "bracket_foot",
}
]
},
script_path=Path("assembly.py"),
)
self.assertEqual(
[
{
"id": "m1",
"label": "m1",
"sourceLabel": "servo_to_bracket",
"relation": "rigid",
"fixed": "servo_mount",
"moving": "bracket_foot",
}
],
scene.assembly_mates,
)
def test_dxf_payload_rejects_legacy_output_field(self) -> None:
with self.assertRaisesRegex(TypeError, "unsupported field\\(s\\): dxf_output"):
generation._normalize_dxf_payload(
{"document": object(), "dxf_output": "legacy.dxf"},
script_path=Path("part.py"),
)
def test_metadata_rejects_legacy_output_fields(self) -> None:
cases = [
("gen_step", "return {'shape': object(), 'step_output': 'legacy.step'}", "step_output"),
("gen_dxf", "return {'document': object(), 'dxf_output': 'legacy.dxf'}", "dxf_output"),
("gen_urdf", "return {'xml': '<robot />', 'urdf_output': 'legacy.urdf'}", "urdf_output"),
("gen_sdf", "return {'xml': '<sdf version=\"1.12\" />', 'sdf_output': 'legacy.sdf'}", "sdf_output"),
]
for function_name, return_line, field_name in cases:
with self.subTest(function_name=function_name), tempfile.TemporaryDirectory(prefix="cadpy-output-field-") as tempdir:
script_path = Path(tempdir) / "part.py"
script_path.write_text(
"\n".join(
[
"def gen_step():",
" return {'shape': object()}",
"",
f"def {function_name}():",
f" {return_line}",
"",
]
),
encoding="utf-8",
)
with self.assertRaisesRegex(ValueError, f"unsupported field\\(s\\): {field_name}"):
parse_generator_metadata(script_path)
def test_run_selected_specs_preserves_action_stdout(self) -> None:
spec = SimpleNamespace(source_ref="part.py")
stdout = io.StringIO()
with contextlib.redirect_stdout(stdout):
generation._run_selected_specs(
[spec],
action=lambda _spec: print("generator summary"),
logger=generation.CliLogger("test", stream=io.StringIO()),
success_message=None,
)
self.assertEqual("generator summary\n", stdout.getvalue())
def test_compound_with_explicit_children_is_discovered_as_assembly(self) -> None:
with tempfile.TemporaryDirectory(prefix="cadpy-compound-") as tempdir:
script_path = Path(tempdir) / "robot_arm.py"
script_path.write_text(
"\n".join(
[
"from build123d import Compound",
"",
"def gen_step():",
" parts = []",
" assembly = Compound(",
" obj=parts,",
" children=parts,",
" label='robot_arm_static_display_pose',",
" )",
" return assembly",
"",
]
),
encoding="utf-8",
)
metadata = parse_generator_metadata(script_path)
self.assertIsNotNone(metadata)
self.assertEqual("assembly", metadata.kind)
def test_compound_with_literal_obj_sequence_is_discovered_as_assembly(self) -> None:
with tempfile.TemporaryDirectory(prefix="cadpy-compound-") as tempdir:
script_path = Path(tempdir) / "compound_arm.py"
script_path.write_text(
"\n".join(
[
"from build123d import Box, Compound",
"",
"def gen_step():",
" left = Box(1, 1, 1)",
" right = Box(1, 1, 1)",
" return Compound(obj=[left, right], label='compound_arm')",
"",
]
),
encoding="utf-8",
)
metadata = parse_generator_metadata(script_path)
self.assertIsNotNone(metadata)
self.assertEqual("assembly", metadata.kind)
def test_childless_compound_obj_sequence_is_runtime_assembly(self) -> None:
import build123d
left = build123d.Box(1, 1, 1)
right = build123d.Box(1, 1, 1)
shape = build123d.Compound(obj=[left, right], label="compound_arm")
self.assertEqual("assembly", generation._shape_payload_entry_kind(shape, fallback="part"))
def test_labeled_childless_compound_does_not_warn_without_color(self) -> None:
import build123d
left = build123d.Box(1, 1, 1)
right = build123d.Box(1, 1, 1)
shape = build123d.Compound(obj=[left, right], label="compound_arm")
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
_create_bin_xcaf_doc(shape)
messages = [str(item.message) for item in caught]
self.assertNotIn("Unknown Compound type, color not set", messages)
def test_colored_child_shapes_survive_compound_assembly_export(self) -> None:
import build123d
with tempfile.TemporaryDirectory(prefix="cadpy-compound-") as tempdir:
left = build123d.Box(1, 1, 1)
left.label = "red_child"
left.color = build123d.Color(1, 0, 0)
right = build123d.Pos(2, 0, 0) * build123d.Box(1, 1, 1)
right.label = "blue_child"
right.color = build123d.Color(0, 0, 1)
shape = build123d.Compound(children=[left, right], label="colored_assembly")
scene = export_build123d_step_scene(
shape,
Path(tempdir) / "colored_assembly.step",
text_to_cad_entry_kind="assembly",
)
colors = {
tuple(round(component, 3) for component in color)
for color in scene.prototype_colors.values()
}
colors.update(
tuple(round(component, 3) for component in node.color)
for root in scene.roots
for node in root.children
if node.color is not None
)
self.assertEqual(1, len(scene.roots))
self.assertEqual(2, len(scene.roots[0].children))
self.assertIn((1.0, 0.0, 0.0, 1.0), colors)
self.assertIn((0.0, 0.0, 1.0, 1.0), colors)
def test_nested_colored_compound_keeps_parent_transform(self) -> None:
import build123d
with tempfile.TemporaryDirectory(prefix="cadpy-compound-") as tempdir:
child = build123d.Box(1, 1, 1)
child.label = "motor_body"
child.color = build123d.Color(0.1, 0.2, 0.3)
expected_color = _rounded_color(child.color)
expected_linear_color = _rounded_color(
(
*(_srgb_to_linear(component) for component in expected_color[:3]),
expected_color[3],
)
)
nested = build123d.Compound(children=[child], label="imported_motor")
placed = build123d.Pos(20, 0, 0) * nested
placed.label = "placed_motor"
root = build123d.Compound(children=[placed], label="arm")
scene = export_build123d_step_scene(
root,
Path(tempdir) / "arm.step",
text_to_cad_entry_kind="assembly",
)
leaves = scene_leaf_occurrences(scene)
self.assertEqual(1, len(leaves))
bbox = _bbox_from_shape(scene_occurrence_shape(scene, leaves[0]))
self.assertGreater(bbox["min"][0], 19.0)
self.assertLess(bbox["max"][0], 21.0)
self.assertIn(
_rounded_color(leaves[0].color),
{expected_color, expected_linear_color},
)
def test_shape_payload_can_export_with_assembly_entry_kind(self) -> None:
import build123d
with tempfile.TemporaryDirectory(prefix="cadpy-compound-") as tempdir:
script_path = Path(tempdir) / "robot_arm.py"
script_path.write_text("def gen_step():\n return None\n", encoding="utf-8")
output_path = script_path.with_suffix(".step")
scene = LoadedStepScene(step_path=output_path.resolve(), roots=[], prototype_shapes={})
left = build123d.Box(1, 1, 1)
right = build123d.Box(1, 1, 1)
shape = build123d.Compound(children=[left, right], label="robot_arm")
with (
mock.patch.object(
generation,
"python_source_hash",
return_value=SimpleNamespace(
source_path="robot_arm.py",
source_hash="hash-123",
),
),
mock.patch.object(generation, "export_build123d_step_scene", return_value=scene) as export_scene,
):
result = generation._write_shape_step_payload(
{"shape": shape},
output_path=output_path,
script_path=script_path,
logger=generation.CliLogger("test"),
entry_kind="assembly",
)
self.assertIs(result, scene)
self.assertEqual("assembly", export_scene.call_args.kwargs["text_to_cad_entry_kind"])
self.assertEqual("assembly", getattr(scene, "text_to_cad_entry_kind", None))
self.assertEqual("shape", getattr(scene, "step_payload_kind", None))
def test_effective_spec_follows_runtime_shape_entry_kind(self) -> None:
step_path = Path("/tmp/compound.step")
scene = LoadedStepScene(step_path=step_path, roots=[], prototype_shapes={})
scene.text_to_cad_entry_kind = "assembly"
spec = generation.EntrySpec(
source_ref="compound.py",
cad_ref="compound",
kind="part",
source_path=Path("/tmp/compound.py"),
display_name="compound",
source="generated",
step_path=step_path,
script_path=Path("/tmp/compound.py"),
)
effective = generation._effective_step_spec_for_scene(spec, scene)
self.assertEqual("assembly", effective.kind)
self.assertEqual("part", spec.kind)
def test_artifact_outputs_use_runtime_shape_entry_kind(self) -> None:
with tempfile.TemporaryDirectory(prefix="cadpy-compound-") as tempdir:
step_path = Path(tempdir) / "compound.step"
script_path = Path(tempdir) / "compound.py"
scene = LoadedStepScene(step_path=step_path.resolve(), roots=[], prototype_shapes={})
scene.text_to_cad_entry_kind = "assembly"
scene.source_kind = "python"
scene.source_path = "compound.py"
scene.source_hash = "source-hash"
spec = generation.EntrySpec(
source_ref="compound.py",
cad_ref="compound",
kind="part",
source_path=script_path,
display_name="compound",
source="generated",
step_path=step_path,
script_path=script_path,
)
selector_bundle = generation.SelectorBundle(manifest={"stats": {}})
with (
mock.patch.object(generation, "_existing_topology_artifact_matches_spec_without_scene", return_value=False),
mock.patch.object(generation, "_existing_topology_artifact_matches_options", return_value=False),
mock.patch.object(generation, "_selector_options_for_part", return_value=generation.SelectorOptions()),
mock.patch.object(generation, "mesh_step_scene"),
mock.patch.object(generation, "scene_export_shape"),
mock.patch.object(generation, "_reset_step_artifact_dir"),
mock.patch.object(generation, "_run_artifact_jobs", return_value={"GLB/topology": selector_bundle}),
):
result = generation._generate_part_outputs(
spec,
entries_by_step_path={step_path.resolve(): spec},
preloaded_scene=scene,
require_step_file=False,
force=True,
)
self.assertEqual("assembly", result.spec.kind)
self.assertIs(result.selector_bundle, selector_bundle)
def test_dependency_expansion_walks_flattened_grouping_node_leaves(self) -> None:
with tempfile.TemporaryDirectory(prefix="cadpy-dependencies-") as tempdir:
root = Path(tempdir)
assembly_path = root / "grouped_assembly.py"
assembly_step = root / "grouped_assembly.step"
leaf_step = root / "nested_part.step"
assembly_path.write_text("def gen_step():\n return None\n", encoding="utf-8")
leaf_step.write_text("ISO-10303-21; END-ISO-10303-21;\n", encoding="utf-8")
identity = tuple(IDENTITY_TRANSFORM)
leaf_instance = AssemblyInstanceSpec(
instance_id="nested_part",
source_path=leaf_step.resolve(),
path="nested_part.step",
name="nested_part",
transform=identity,
)
leaf_node = AssemblyNodeSpec(
instance_id="nested_part",
name="nested_part",
transform=identity,
source_path=leaf_step.resolve(),
path="nested_part.step",
children=(),
)
grouping_node = AssemblyNodeSpec(
instance_id="front_group",
name="front_group",
transform=identity,
source_path=None,
path=None,
children=(leaf_node,),
)
assembly_spec = AssemblySpec(
assembly_path=assembly_path.resolve(),
instances=(leaf_instance,),
children=(grouping_node,),
)
entry = generation.EntrySpec(
source_ref="grouped_assembly.py",
cad_ref="grouped_assembly",
kind="assembly",
source_path=assembly_path.resolve(),
display_name="grouped_assembly",
source="generated",
step_path=assembly_step.resolve(),
script_path=assembly_path.resolve(),
)
leaf_source = CadSource(
source_ref="nested_part.step",
cad_ref="nested_part",
kind="part",
source_path=leaf_step.resolve(),
source="imported",
origin_path=leaf_step.resolve(),
step_path=leaf_step.resolve(),
)
with (
mock.patch.object(generation, "read_assembly_spec", return_value=assembly_spec),
mock.patch.object(generation, "_source_lookup_by_path", return_value={leaf_step.resolve(): leaf_source}),
):
expanded = generation._expand_specs_with_file_dependencies([entry])
self.assertEqual(
[assembly_path.resolve(), leaf_step.resolve()],
[spec.source_path for spec in expanded],
)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,71 @@
from __future__ import annotations
import json
import tempfile
import unittest
from pathlib import Path
from tests.python.support.paths import add_repo_path
add_repo_path("packages/cadpy/src")
from cadpy.generation_status import GenerationOutput, track_generation_run
class GenerationStatusTests(unittest.TestCase):
def test_generation_status_uses_hidden_lock_next_to_output_and_cleans_up(self) -> None:
with tempfile.TemporaryDirectory(prefix="cadpy-generation-status-") as tempdir:
workspace = Path(tempdir) / "workspace"
model_dir = workspace / "models"
skill_dir = Path(tempdir) / "installed-skill"
source_path = model_dir / "part.py"
step_path = model_dir / "part.step"
model_dir.mkdir(parents=True)
source_path.write_text("def gen_step():\n return None\n", encoding="utf-8")
(skill_dir / "scripts").mkdir(parents=True)
(skill_dir / "SKILL.md").write_text("# skill\n", encoding="utf-8")
with track_generation_run(
source_path=source_path,
generator="gen_step",
outputs=[GenerationOutput(step_path, "step")],
repo_root=skill_dir,
):
lock_paths = tuple(model_dir.glob(".part.step.*.generation.lock.json"))
self.assertEqual(1, len(lock_paths))
payload = json.loads(lock_paths[0].read_text(encoding="utf-8"))
self.assertEqual("running", payload["status"])
self.assertEqual("gen_step", payload["generator"])
self.assertEqual("part.py", payload["sourcePath"])
self.assertEqual("part.step", payload["outputs"][0]["path"])
self.assertFalse(tuple(skill_dir.rglob("*.generation.lock.json")))
self.assertFalse(lock_paths[0].exists())
def test_generation_status_writes_one_lock_for_each_output_without_blocking(self) -> None:
with tempfile.TemporaryDirectory(prefix="cadpy-generation-status-") as tempdir:
model_dir = Path(tempdir) / "models"
step_path = model_dir / "part.step"
glb_path = model_dir / ".part.step.glb"
model_dir.mkdir(parents=True)
with track_generation_run(
source_path=None,
generator="gen_step",
outputs=[
GenerationOutput(step_path, "step"),
GenerationOutput(glb_path, "glb"),
],
repo_root=Path(tempdir),
):
step_locks = tuple(model_dir.glob(".part.step.*.generation.lock.json"))
glb_locks = tuple(model_dir.glob("..part.step.glb.*.generation.lock.json"))
self.assertEqual(1, len(step_locks))
self.assertEqual(1, len(glb_locks))
self.assertNotEqual(step_locks[0], glb_locks[0])
self.assertFalse(tuple(model_dir.glob("*.generation.lock.json")))
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,25 @@
from __future__ import annotations
import unittest
from tests.python.support.paths import add_repo_path
add_repo_path("packages/cadpy/src")
from cadpy.glb import _GlbBuilder
class GlbMaterialTests(unittest.TestCase):
def test_materials_record_source_color_hint(self) -> None:
builder = _GlbBuilder()
source_index = builder.add_material((0.1, 0.1, 0.1, 1.0), source_color=True)
fallback_index = builder.add_material((0.72, 0.72, 0.72, 1.0), source_color=False)
materials = builder.json["materials"]
self.assertEqual({"cadSourceColor": True}, materials[source_index]["extras"])
self.assertEqual({"cadSourceColor": False}, materials[fallback_index]["extras"])
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,95 @@
from __future__ import annotations
import json
import tempfile
import unittest
from pathlib import Path
from tests.python.support.paths import add_repo_path
add_repo_path("packages/cadpy_metadata/src")
from cadpy_metadata.generator import (
GenerationOutput,
PythonSourceIdentity,
track_generation_run,
xml_with_text_to_cad_metadata,
)
class MetadataGenerationStatusTests(unittest.TestCase):
def test_generation_status_uses_hidden_lock_next_to_output_and_cleans_up(self) -> None:
with tempfile.TemporaryDirectory(prefix="cadpy-metadata-generation-status-") as tempdir:
workspace = Path(tempdir) / "workspace"
model_dir = workspace / "models"
skill_dir = Path(tempdir) / "installed-skill"
source_path = model_dir / "robot_urdf.py"
urdf_path = model_dir / "robot.urdf"
model_dir.mkdir(parents=True)
source_path.write_text("def gen_urdf():\n return '<robot />'\n", encoding="utf-8")
(skill_dir / "scripts").mkdir(parents=True)
(skill_dir / "SKILL.md").write_text("# skill\n", encoding="utf-8")
with track_generation_run(
source_path=source_path,
generator="gen_urdf",
outputs=[GenerationOutput(urdf_path, "urdf")],
repo_root=skill_dir,
):
lock_paths = tuple(model_dir.glob(".robot.urdf.*.generation.lock.json"))
self.assertEqual(1, len(lock_paths))
payload = json.loads(lock_paths[0].read_text(encoding="utf-8"))
self.assertEqual("running", payload["status"])
self.assertEqual("gen_urdf", payload["generator"])
self.assertEqual("robot_urdf.py", payload["sourcePath"])
self.assertEqual("robot.urdf", payload["outputs"][0]["path"])
self.assertFalse(tuple(skill_dir.rglob("*.generation.lock.json")))
self.assertFalse(lock_paths[0].exists())
def test_generation_status_writes_one_lock_for_each_output(self) -> None:
with tempfile.TemporaryDirectory(prefix="cadpy-metadata-generation-status-") as tempdir:
model_dir = Path(tempdir) / "models"
urdf_path = model_dir / "robot.urdf"
srdf_path = model_dir / "robot.srdf"
model_dir.mkdir(parents=True)
with track_generation_run(
source_path=None,
generator="gen_srdf",
outputs=[
GenerationOutput(urdf_path, "urdf"),
GenerationOutput(srdf_path, "srdf"),
],
repo_root=Path(tempdir),
):
urdf_locks = tuple(model_dir.glob(".robot.urdf.*.generation.lock.json"))
srdf_locks = tuple(model_dir.glob(".robot.srdf.*.generation.lock.json"))
self.assertEqual(1, len(urdf_locks))
self.assertEqual(1, len(srdf_locks))
self.assertNotEqual(urdf_locks[0], srdf_locks[0])
self.assertFalse(tuple(model_dir.glob("*.generation.lock.json")))
def test_xml_metadata_source_path_is_relative_to_output(self) -> None:
with tempfile.TemporaryDirectory(prefix="cadpy-metadata-xml-") as tempdir:
workspace = Path(tempdir)
source_path = workspace / "src" / "robot.py"
output_path = workspace / "out" / "robot.urdf"
identity = PythonSourceIdentity(
source_path="src/robot.py",
source_hash="source-hash",
)
xml = xml_with_text_to_cad_metadata(
"<robot name=\"sample\" />",
identity,
output_path=output_path,
source_path=source_path,
)
self.assertIn("<!-- cadpy:sourcePath=../src/robot.py -->", xml)
if __name__ == "__main__":
unittest.main()