Files
create/src/Mod/CAM/CAMTests/TestPathToolBitSerializer.py
Billy Huddleston 9cab1f8f52 CAM: Make CAM tests and serialization robust to locale-dependent decimal separators
Updated CAM unit tests and LinuxCNC serializer to handle decimal separators consistently, ensuring tests pass regardless of system locale. Assertions now compare FreeCAD.Units.Quantity objects directly or normalize decimal separators in strings. LinuxCNC serializer output is forced to use periods for decimals.

src/Mod/CAM/CAMTests/TestPathToolBitListWidget.py:
- Normalize decimal separators in tool description assertions for locale robustness.

src/Mod/CAM/CAMTests/TestPathToolBitPropertyEditorWidget.py:
- Use FreeCAD.Units.Quantity for direct quantity comparisons in property editor tests.

src/Mod/CAM/CAMTests/TestPathToolBitSerializer.py:
- Compare deserialized and serialized quantities using FreeCAD.Units.Quantity for consistency.

src/Mod/CAM/CAMTests/TestPathToolShapeClasses.py:
- Compare parameter values and units directly instead of relying on string formatting.

src/Mod/CAM/Path/Tool/library/serializers/linuxcnc.py:
- Force period as decimal separator in LinuxCNC serializer output to avoid locale issues.
2025-11-24 18:53:32 +01:00

232 lines
9.3 KiB
Python

# SPDX-License-Identifier: LGPL-2.1-or-later
import yaml
import json
from typing import Type, cast
import FreeCAD
from CAMTests.PathTestUtils import PathTestWithAssets
from Path.Tool.toolbit import ToolBit, ToolBitEndmill
from Path.Tool.toolbit.serializers import (
FCTBSerializer,
CamoticsToolBitSerializer,
YamlToolBitSerializer,
)
from Path.Tool.assets.asset import Asset
from Path.Tool.assets.serializer import AssetSerializer
from Path.Tool.assets.uri import AssetUri
from Path.Tool.shape import ToolBitShapeEndmill
from typing import Mapping
class _BaseToolBitSerializerTestCase(PathTestWithAssets):
"""Base test case for ToolBit Serializers."""
__test__ = False
serializer_class: Type[AssetSerializer]
test_tool_bit: ToolBit
def setUp(self):
"""Create a tool bit for each test."""
super().setUp()
if self.serializer_class is None or not issubclass(self.serializer_class, AssetSerializer):
raise NotImplementedError("Subclasses must define a valid serializer_class")
self.test_tool_bit = cast(ToolBitEndmill, self.assets.get("toolbit://5mm_Endmill"))
self.test_tool_bit.label = "Test Tool"
self.test_tool_bit.set_diameter(FreeCAD.Units.Quantity("4.12 mm"))
self.test_tool_bit.set_length(FreeCAD.Units.Quantity("15.0 mm"))
def test_serialize(self):
"""Test serialization of a toolbit."""
if self.test_tool_bit is None:
raise NotImplementedError("Subclasses must define a test_tool_bit")
serialized_data = self.serializer_class.serialize(self.test_tool_bit)
self.assertIsInstance(serialized_data, bytes)
def test_extract_dependencies(self):
"""Test dependency extraction."""
# This test assumes that the serializers don't have dependencies
# and can be overridden in subclasses if needed.
serialized_data = self.serializer_class.serialize(self.test_tool_bit)
dependencies = self.serializer_class.extract_dependencies(serialized_data)
self.assertIsInstance(dependencies, list)
self.assertEqual(len(dependencies), 0)
class TestCamoticsToolBitSerializer(_BaseToolBitSerializerTestCase):
serializer_class = CamoticsToolBitSerializer
def test_serialize(self):
super().test_serialize()
serialized_data = self.serializer_class.serialize(self.test_tool_bit)
# Camotics specific assertions
expected_substrings = [
b'"units": "metric"',
b'"shape": "Cylindrical"',
b'"length": 15',
b'"diameter": 4.12',
b'"description": "Test Tool"',
]
for substring in expected_substrings:
self.assertIn(substring, serialized_data)
def test_deserialize(self):
# Create a known serialized data string based on the Camotics format
camotics_data = (
b'{"units": "metric", "shape": "Cylindrical", "length": 15, '
b'"diameter": 4.12, "description": "Test Tool"}'
)
deserialized_bit = cast(
ToolBitEndmill,
self.serializer_class.deserialize(camotics_data, id="test_id", dependencies=None),
)
self.assertIsInstance(deserialized_bit, ToolBit)
self.assertEqual(deserialized_bit.label, "Test Tool")
self.assertEqual(
deserialized_bit.get_diameter(), FreeCAD.Units.Quantity(4.12, FreeCAD.Units.Length)
)
self.assertEqual(
deserialized_bit.get_length(), FreeCAD.Units.Quantity(15.0, FreeCAD.Units.Length)
)
self.assertEqual(deserialized_bit.get_shape_name(), "Endmill")
class TestFCTBSerializer(_BaseToolBitSerializerTestCase):
serializer_class = FCTBSerializer
def test_serialize(self):
super().test_serialize()
serialized_data = self.serializer_class.serialize(self.test_tool_bit)
# FCTB specific assertions (JSON format)
data = json.loads(serialized_data.decode("utf-8"))
self.assertEqual(data.get("name"), "Test Tool")
self.assertEqual(data.get("shape"), "endmill.fcstd")
self.assertEqual(
FreeCAD.Units.Quantity(data.get("parameter", {}).get("Diameter")),
FreeCAD.Units.Quantity(4.12, FreeCAD.Units.Length),
)
self.assertEqual(
FreeCAD.Units.Quantity(data.get("parameter", {}).get("Length")),
FreeCAD.Units.Quantity(15.0, FreeCAD.Units.Length),
)
def test_extract_dependencies(self):
"""Test dependency extraction for FCTB."""
fctb_data = (
b'{"name": "Test Tool", "pocket": null, "shape": "endmill", '
b'"parameter": {"Diameter": "4.12 mm", "Length": "15.0 mm"}, "attribute": {}}'
)
dependencies = self.serializer_class.extract_dependencies(fctb_data)
self.assertIsInstance(dependencies, list)
self.assertEqual(len(dependencies), 1)
self.assertEqual(dependencies[0], AssetUri.build("toolbitshape", "endmill"))
def test_deserialize(self):
# Create a known serialized data string based on the FCTB format
fctb_data = (
b'{"name": "Test Tool", "pocket": null, "shape": "endmill", '
b'"parameter": {"Diameter": "4.12 mm", "Length": "15.0 mm"}, "attribute": {}}'
)
# Create a ToolBitShapeEndmill instance for 'endmill'
shape = ToolBitShapeEndmill("endmill")
# Create the dependencies dictionary with the shape instance
dependencies: Mapping[AssetUri, Asset] = {AssetUri.build("toolbitshape", "endmill"): shape}
# Provide dummy id and dependencies for deserialization test
deserialized_bit = cast(
ToolBitEndmill,
self.serializer_class.deserialize(fctb_data, id="test_id", dependencies=dependencies),
)
self.assertIsInstance(deserialized_bit, ToolBit)
self.assertEqual(deserialized_bit.label, "Test Tool")
self.assertEqual(deserialized_bit.get_shape_name(), "Endmill")
self.assertEqual(
deserialized_bit.get_diameter(), FreeCAD.Units.Quantity(4.12, FreeCAD.Units.Length)
)
self.assertEqual(
deserialized_bit.get_length(), FreeCAD.Units.Quantity(15.0, FreeCAD.Units.Length)
)
class TestYamlToolBitSerializer(_BaseToolBitSerializerTestCase):
serializer_class = YamlToolBitSerializer
def test_serialize(self):
super().test_serialize()
serialized_data = self.serializer_class.serialize(self.test_tool_bit)
# YAML specific assertions
data = yaml.safe_load(serialized_data.decode("utf-8"))
self.assertEqual(data.get("id"), "5mm_Endmill")
self.assertEqual(data.get("name"), "Test Tool")
self.assertEqual(data.get("shape"), "endmill.fcstd")
self.assertEqual(data.get("shape-type"), "Endmill")
self.assertEqual(
FreeCAD.Units.Quantity(data.get("parameter", {}).get("Diameter")),
FreeCAD.Units.Quantity(4.12, FreeCAD.Units.Length),
)
self.assertEqual(
FreeCAD.Units.Quantity(data.get("parameter", {}).get("Length")),
FreeCAD.Units.Quantity(15.0, FreeCAD.Units.Length),
)
def test_extract_dependencies(self):
"""Test dependency extraction for YAML."""
yaml_data = (
b"name: Test Tool\n"
b"shape: endmill\n"
b"shape-type: Endmill\n"
b"parameter:\n"
b" Diameter: 4.12 mm\n"
b" Length: 15.0 mm\n"
b"attribute: {}\n"
)
dependencies = self.serializer_class.extract_dependencies(yaml_data)
self.assertIsInstance(dependencies, list)
self.assertEqual(len(dependencies), 1)
self.assertEqual(dependencies[0], AssetUri.build("toolbitshape", "endmill"))
def test_deserialize(self):
# Create a known serialized data string based on the YAML format
yaml_data = (
b"id: TestID\n"
b"name: Test Tool\n"
b"shape: endmill\n"
b"shape-type: Endmill\n"
b"parameter:\n"
b" Diameter: 4.12 mm\n"
b" Length: 15.0 mm\n"
b"attribute: {}\n"
)
# Create a ToolBitShapeEndmill instance for 'endmill'
shape = ToolBitShapeEndmill("endmill")
# Create the dependencies dictionary with the shape instance
dependencies: Mapping[AssetUri, Asset] = {AssetUri.build("toolbitshape", "endmill"): shape}
# Provide dummy id and dependencies for deserialization test
deserialized_bit = cast(
ToolBitEndmill,
self.serializer_class.deserialize(yaml_data, "TestID", dependencies=dependencies),
)
self.assertIsInstance(deserialized_bit, ToolBit)
self.assertEqual(deserialized_bit.id, "TestID")
self.assertEqual(deserialized_bit.label, "Test Tool")
self.assertEqual(deserialized_bit.get_shape_name(), "Endmill")
self.assertEqual(
deserialized_bit.get_diameter(), FreeCAD.Units.Quantity(4.12, FreeCAD.Units.Length)
)
self.assertEqual(
deserialized_bit.get_length(), FreeCAD.Units.Quantity(15.0, FreeCAD.Units.Length)
)
# Test with ID argument.
deserialized_bit = cast(
ToolBitEndmill,
self.serializer_class.deserialize(yaml_data, id="test_id", dependencies=dependencies),
)
self.assertEqual(deserialized_bit.id, "test_id")