CAM: Replace complete tool management (PR 21425)
This commit is contained in:
@@ -21,10 +21,20 @@
|
||||
# ***************************************************************************
|
||||
|
||||
import FreeCAD
|
||||
import os
|
||||
import Part
|
||||
import Path
|
||||
import math
|
||||
import pathlib
|
||||
import unittest
|
||||
from Path.Tool.assets import AssetManager, MemoryStore, DummyAssetSerializer
|
||||
from Path.Tool.library.serializers import FCTLSerializer
|
||||
from Path.Tool.toolbit.serializers import FCTBSerializer
|
||||
from Path.Tool.camassets import ensure_assets_initialized
|
||||
from Path.Tool.library import Library
|
||||
from Path.Tool.toolbit import ToolBit
|
||||
from Path.Tool.shape import ToolBitShape
|
||||
from Path.Tool.shape.models.icon import ToolBitShapeSvgIcon, ToolBitShapePngIcon
|
||||
|
||||
from FreeCAD import Vector
|
||||
|
||||
@@ -196,3 +206,43 @@ class PathTestBase(unittest.TestCase):
|
||||
failed_objects = [o.Name for o in objs if "Invalid" in o.State]
|
||||
if len(failed_objects) > 0:
|
||||
self.fail(msg or f"Recompute failed for {failed_objects}")
|
||||
|
||||
|
||||
class PathTestWithAssets(PathTestBase):
|
||||
"""
|
||||
A base class that creates an AssetManager, so tests can easily fetch
|
||||
test data. Examples:
|
||||
|
||||
toolbit = self.assets.get("toolbit://ballend")
|
||||
toolbit = self.assets.get("toolbitshape://chamfer")
|
||||
"""
|
||||
|
||||
__tool_dir = pathlib.Path(os.path.realpath(__file__)).parent.parent / "Tools"
|
||||
|
||||
def setUp(self):
|
||||
# Set up the manager with an in-memory store.
|
||||
self.assets: AssetManager = AssetManager()
|
||||
self.asset_store: MemoryStore = MemoryStore("local")
|
||||
self.assets.register_store(self.asset_store)
|
||||
|
||||
# Register some asset classes.
|
||||
self.assets.register_asset(Library, FCTLSerializer)
|
||||
self.assets.register_asset(ToolBit, FCTBSerializer)
|
||||
self.assets.register_asset(ToolBitShape, DummyAssetSerializer)
|
||||
self.assets.register_asset(ToolBitShapeSvgIcon, DummyAssetSerializer)
|
||||
self.assets.register_asset(ToolBitShapePngIcon, DummyAssetSerializer)
|
||||
|
||||
# Include the built-in assets from src/Mod/CAM/Tools.
|
||||
# These functions only copy if there are no assets, so this
|
||||
# must be done BEFORE adding the additional test assets below.
|
||||
ensure_assets_initialized(self.assets, self.asset_store.name)
|
||||
|
||||
# Additional test assets.
|
||||
for path in pathlib.Path(self.__tool_dir / "Bit").glob("*.fctb"):
|
||||
self.assets.add_file("toolbit", path)
|
||||
for path in pathlib.Path(self.__tool_dir / "Shape").glob("*.fcstd"):
|
||||
self.assets.add_file("toolbitshape", path)
|
||||
|
||||
def tearDown(self):
|
||||
del self.assets
|
||||
del self.asset_store
|
||||
|
||||
@@ -25,7 +25,7 @@ import Part
|
||||
import Path
|
||||
import Path.Base.FeedRate as PathFeedRate
|
||||
import Path.Base.MachineState as PathMachineState
|
||||
import Path.Tool.Bit as PathToolBit
|
||||
from Path.Tool.toolbit import ToolBit
|
||||
import Path.Tool.Controller as PathToolController
|
||||
import PathScripts.PathUtils as PathUtils
|
||||
|
||||
@@ -34,12 +34,13 @@ from CAMTests.PathTestUtils import PathTestBase
|
||||
|
||||
def createTool(name="t1", diameter=1.75):
|
||||
attrs = {
|
||||
"shape": None,
|
||||
"name": name,
|
||||
"name": name or "t1",
|
||||
"shape": "endmill.fcstd",
|
||||
"parameter": {"Diameter": diameter},
|
||||
"attribute": [],
|
||||
"attribute": {},
|
||||
}
|
||||
return PathToolBit.Factory.CreateFromAttrs(attrs, name)
|
||||
toolbit = ToolBit.from_dict(attrs)
|
||||
return toolbit.attach_to_doc(doc=FreeCAD.ActiveDocument)
|
||||
|
||||
|
||||
class TestPathHelpers(PathTestBase):
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
|
||||
import Path
|
||||
import Path.Op.Deburr as PathDeburr
|
||||
import Path.Tool.Bit as PathToolBit
|
||||
import CAMTests.PathTestUtils as PathTestUtils
|
||||
|
||||
Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule())
|
||||
|
||||
@@ -51,11 +51,19 @@ class TestPathPreferences(PathTestUtils.PathTestBase):
|
||||
def test10(self):
|
||||
"""Default paths for tools are resolved correctly"""
|
||||
|
||||
self.assertTrue(Path.Preferences.pathDefaultToolsPath().endswith("/CAM/Tools/"))
|
||||
self.assertTrue(Path.Preferences.pathDefaultToolsPath("Bit").endswith("/CAM/Tools/Bit"))
|
||||
self.assertTrue(
|
||||
Path.Preferences.pathDefaultToolsPath("Library").endswith("/CAM/Tools/Library")
|
||||
self.assertEqual(
|
||||
Path.Preferences.getDefaultAssetPath().parts[-2:],
|
||||
("CAM", "Tools"),
|
||||
str(Path.Preferences.getDefaultAssetPath()),
|
||||
)
|
||||
self.assertTrue(
|
||||
Path.Preferences.pathDefaultToolsPath("Template").endswith("/CAM/Tools/Template")
|
||||
self.assertEqual(
|
||||
Path.Preferences.getBuiltinToolPath().parts[-2:],
|
||||
("CAM", "Tools"),
|
||||
str(Path.Preferences.getBuiltinToolPath()),
|
||||
)
|
||||
self.assertEqual(
|
||||
Path.Preferences.getBuiltinShapePath().parts[-3:],
|
||||
("CAM", "Tools", "Shape"),
|
||||
str(Path.Preferences.getBuiltinShapePath()),
|
||||
)
|
||||
self.assertEqual(Path.Preferences.getToolBitPath().name, "Bit")
|
||||
|
||||
41
src/Mod/CAM/CAMTests/TestPathToolAsset.py
Normal file
41
src/Mod/CAM/CAMTests/TestPathToolAsset.py
Normal file
@@ -0,0 +1,41 @@
|
||||
import unittest
|
||||
from typing import Any, List, Mapping
|
||||
from Path.Tool.assets import Asset, AssetUri
|
||||
|
||||
|
||||
class TestAsset(Asset):
|
||||
asset_type: str = "test_asset"
|
||||
|
||||
@classmethod
|
||||
def dependencies(cls, data: bytes) -> List[AssetUri]:
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, data: bytes, id: str, dependencies: Mapping[AssetUri, Any]) -> Any:
|
||||
return "dummy_object"
|
||||
|
||||
def to_bytes(self) -> bytes:
|
||||
return b"dummy_serialized_data"
|
||||
|
||||
def get_id(self) -> str:
|
||||
# Dummy implementation for testing purposes
|
||||
return "dummy_id"
|
||||
|
||||
|
||||
class TestPathToolAsset(unittest.TestCase):
|
||||
def test_asset_cannot_be_instantiated(self):
|
||||
with self.assertRaises(TypeError):
|
||||
Asset() # type: ignore
|
||||
|
||||
def test_asset_can_be_instantiated_and_has_members(self):
|
||||
asset = TestAsset()
|
||||
self.assertIsInstance(asset, Asset)
|
||||
self.assertEqual(asset.asset_type, "test_asset")
|
||||
self.assertEqual(asset.to_bytes(), b"dummy_serialized_data")
|
||||
self.assertEqual(TestAsset.dependencies(b"some_data"), [])
|
||||
self.assertEqual(TestAsset.from_bytes(b"some_data", "some_id", {}), "dummy_object")
|
||||
self.assertEqual(asset.get_id(), "dummy_id")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
342
src/Mod/CAM/CAMTests/TestPathToolAssetCache.py
Normal file
342
src/Mod/CAM/CAMTests/TestPathToolAssetCache.py
Normal file
@@ -0,0 +1,342 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import unittest
|
||||
import asyncio
|
||||
import hashlib
|
||||
from typing import Any, Type, Optional, List, Mapping
|
||||
from Path.Tool.assets.cache import AssetCache, CacheKey
|
||||
from Path.Tool.assets import (
|
||||
AssetManager,
|
||||
Asset,
|
||||
AssetUri,
|
||||
AssetSerializer,
|
||||
DummyAssetSerializer,
|
||||
MemoryStore,
|
||||
)
|
||||
|
||||
|
||||
class MockAsset(Asset):
|
||||
asset_type: str = "mock_asset"
|
||||
_build_counter = 0
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
asset_id: str,
|
||||
raw_data: bytes,
|
||||
dependencies: Optional[Mapping[AssetUri, Any]] = None,
|
||||
):
|
||||
super().__init__() # Initialize Asset ABC
|
||||
self._asset_id = asset_id # Store id internally
|
||||
self.raw_data_content = raw_data
|
||||
self.resolved_dependencies = dependencies or {}
|
||||
MockAsset._build_counter += 1
|
||||
self.build_id = MockAsset._build_counter
|
||||
|
||||
def get_id(self) -> str: # Implement abstract method
|
||||
return self._asset_id
|
||||
|
||||
# get_uri() is inherited from Asset and uses self.asset_type and self.get_id()
|
||||
|
||||
@classmethod
|
||||
def extract_dependencies(cls, data: bytes, serializer: AssetSerializer) -> List[AssetUri]:
|
||||
"""Extracts URIs of dependencies from serialized data."""
|
||||
# This mock implementation handles the simple "dep:" format
|
||||
data_str = data.decode()
|
||||
if data_str.startswith("dep:"):
|
||||
try:
|
||||
# Get content after the first "dep:"
|
||||
dep_content = data_str.split(":", 1)[1]
|
||||
except IndexError:
|
||||
# This case should ideally not be reached if startswith("dep:") is true
|
||||
# and there's content after "dep:", but good for robustness.
|
||||
return []
|
||||
|
||||
dep_uri_strings = dep_content.split(",")
|
||||
uris = []
|
||||
for uri_string in dep_uri_strings:
|
||||
uri_string = uri_string.strip() # Remove leading/trailing whitespace
|
||||
if not uri_string:
|
||||
continue
|
||||
try:
|
||||
uris.append(AssetUri(uri_string))
|
||||
except ValueError:
|
||||
# This print will now show the full problematic uri_string
|
||||
print(f"Warning: Could not parse mock dependency URI: '{uri_string}'")
|
||||
return uris
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
def from_bytes(
|
||||
cls: Type["MockAsset"],
|
||||
data: bytes,
|
||||
id: str,
|
||||
dependencies: Optional[Mapping[AssetUri, Any]],
|
||||
serializer: AssetSerializer,
|
||||
) -> "MockAsset":
|
||||
return cls(asset_id=id, raw_data=data, dependencies=dependencies)
|
||||
|
||||
def to_bytes(self, serializer: AssetSerializer) -> bytes: # Implement abstract method
|
||||
return self.raw_data_content
|
||||
|
||||
|
||||
class MockAssetB(Asset): # New mock asset class for type 'mock_asset_b'
|
||||
asset_type: str = "mock_asset_b"
|
||||
_build_counter = 0 # Separate counter if needed, or share MockAsset's
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
asset_id: str,
|
||||
raw_data: bytes,
|
||||
dependencies: Optional[Mapping[AssetUri, Any]] = None,
|
||||
):
|
||||
super().__init__()
|
||||
self._asset_id = asset_id
|
||||
self.raw_data_content = raw_data
|
||||
self.resolved_dependencies = dependencies or {}
|
||||
MockAssetB._build_counter += 1
|
||||
self.build_id = MockAssetB._build_counter
|
||||
|
||||
def get_id(self) -> str:
|
||||
return self._asset_id
|
||||
|
||||
@classmethod
|
||||
def extract_dependencies(cls, data: bytes, serializer: AssetSerializer) -> List[AssetUri]:
|
||||
# Keep simple, or adapt if MockAssetB has different dep logic
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
def from_bytes(
|
||||
cls: Type["MockAssetB"],
|
||||
data: bytes,
|
||||
id: str,
|
||||
dependencies: Optional[Mapping[AssetUri, Any]],
|
||||
serializer: AssetSerializer,
|
||||
) -> "MockAssetB":
|
||||
return cls(asset_id=id, raw_data=data, dependencies=dependencies)
|
||||
|
||||
def to_bytes(self, serializer: AssetSerializer) -> bytes:
|
||||
return self.raw_data_content
|
||||
|
||||
|
||||
def _get_raw_data_hash(raw_data: bytes) -> int:
|
||||
return int(hashlib.sha256(raw_data).hexdigest(), 16)
|
||||
|
||||
|
||||
class TestPathToolAssetCache(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.cache = AssetCache(max_size_bytes=1000)
|
||||
|
||||
def test_put_and_get_simple(self):
|
||||
key = CacheKey("store1", "mock_asset://id1", _get_raw_data_hash(b"data1"), ("dep1",))
|
||||
asset_obj = MockAsset(asset_id="id1", raw_data=b"data1")
|
||||
self.cache.put(key, asset_obj, len(b"data1"), {"dep_uri_str"})
|
||||
|
||||
retrieved = self.cache.get(key)
|
||||
self.assertIsNotNone(retrieved)
|
||||
# Assuming retrieved is MockAsset, it will have get_id()
|
||||
self.assertEqual(retrieved.get_id(), "id1")
|
||||
|
||||
def test_get_miss(self):
|
||||
key = CacheKey("store1", "mock_asset://id1", _get_raw_data_hash(b"data1"), tuple())
|
||||
self.assertIsNone(self.cache.get(key))
|
||||
|
||||
def test_lru_eviction(self):
|
||||
asset_data_size = 300
|
||||
asset1_data = b"a" * asset_data_size
|
||||
asset2_data = b"b" * asset_data_size
|
||||
asset3_data = b"c" * asset_data_size
|
||||
asset4_data = b"d" * asset_data_size
|
||||
|
||||
key1 = CacheKey("s", "mock_asset://id1", _get_raw_data_hash(asset1_data), tuple())
|
||||
key2 = CacheKey("s", "mock_asset://id2", _get_raw_data_hash(asset2_data), tuple())
|
||||
key3 = CacheKey("s", "mock_asset://id3", _get_raw_data_hash(asset3_data), tuple())
|
||||
key4 = CacheKey("s", "mock_asset://id4", _get_raw_data_hash(asset4_data), tuple())
|
||||
|
||||
self.cache.put(key1, MockAsset("id1", asset1_data), asset_data_size, set())
|
||||
self.cache.put(key2, MockAsset("id2", asset2_data), asset_data_size, set())
|
||||
self.cache.put(key3, MockAsset("id3", asset3_data), asset_data_size, set())
|
||||
|
||||
self.assertEqual(self.cache.current_size_bytes, 3 * asset_data_size)
|
||||
self.assertIsNotNone(self.cache.get(key1)) # Access key1 to make it MRU
|
||||
|
||||
# Adding key4 should evict key2 (oldest after key1 accessed)
|
||||
self.cache.put(key4, MockAsset("id4", asset4_data), asset_data_size, set())
|
||||
self.assertEqual(self.cache.current_size_bytes, 3 * asset_data_size)
|
||||
self.assertIsNotNone(self.cache.get(key1))
|
||||
self.assertIsNone(self.cache.get(key2)) # Evicted
|
||||
self.assertIsNotNone(self.cache.get(key3))
|
||||
self.assertIsNotNone(self.cache.get(key4))
|
||||
|
||||
def test_invalidate_direct(self):
|
||||
key = CacheKey("s", "mock_asset://id1", _get_raw_data_hash(b"data"), tuple())
|
||||
self.cache.put(key, MockAsset("id1", b"data"), 4, set())
|
||||
retrieved = self.cache.get(key) # Ensure it's there
|
||||
self.assertIsNotNone(retrieved)
|
||||
|
||||
self.cache.invalidate_for_uri("mock_asset://id1")
|
||||
self.assertIsNone(self.cache.get(key))
|
||||
self.assertEqual(self.cache.current_size_bytes, 0)
|
||||
|
||||
def test_invalidate_recursive(self):
|
||||
data_a = b"data_a_dep:mock_asset_b://idB" # A depends on B
|
||||
data_b = b"data_b"
|
||||
data_c = b"data_c_dep:mock_asset_a://idA" # C depends on A
|
||||
|
||||
uri_a_str = "mock_asset_a://idA"
|
||||
uri_b_str = "mock_asset_b://idB"
|
||||
uri_c_str = "mock_asset_c://idC"
|
||||
|
||||
key_b = CacheKey("s", uri_b_str, _get_raw_data_hash(data_b), tuple())
|
||||
key_a = CacheKey("s", uri_a_str, _get_raw_data_hash(data_a), (uri_b_str,))
|
||||
key_c = CacheKey("s", uri_c_str, _get_raw_data_hash(data_c), (uri_a_str,))
|
||||
|
||||
self.cache.put(key_b, MockAsset("idB", data_b), len(data_b), set())
|
||||
self.cache.put(key_a, MockAsset("idA", data_a), len(data_a), {uri_b_str})
|
||||
self.cache.put(key_c, MockAsset("idC", data_c), len(data_c), {uri_a_str})
|
||||
|
||||
self.assertIsNotNone(self.cache.get(key_a))
|
||||
self.assertIsNotNone(self.cache.get(key_b))
|
||||
self.assertIsNotNone(self.cache.get(key_c))
|
||||
|
||||
self.cache.invalidate_for_uri(uri_b_str) # Invalidate B
|
||||
|
||||
self.assertIsNone(self.cache.get(key_a), "Asset A should be invalidated")
|
||||
self.assertIsNone(self.cache.get(key_b), "Asset B should be invalidated")
|
||||
self.assertIsNone(self.cache.get(key_c), "Asset C should be invalidated")
|
||||
self.assertEqual(self.cache.current_size_bytes, 0)
|
||||
|
||||
def test_clear_cache(self):
|
||||
key = CacheKey("s", "mock_asset://id1", _get_raw_data_hash(b"data"), tuple())
|
||||
self.cache.put(key, MockAsset("id1", b"data"), 4, set())
|
||||
self.assertNotEqual(self.cache.current_size_bytes, 0)
|
||||
|
||||
self.cache.clear()
|
||||
self.assertIsNone(self.cache.get(key))
|
||||
self.assertEqual(self.cache.current_size_bytes, 0)
|
||||
self.assertEqual(len(self.cache._cache_dependencies_map), 0)
|
||||
self.assertEqual(len(self.cache._cache_dependents_map), 0)
|
||||
|
||||
|
||||
class TestPathToolAssetCacheIntegration(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(self.loop)
|
||||
|
||||
self.manager = AssetManager(cache_max_size_bytes=10 * 1024) # 10KB cache
|
||||
self.store_name = "test_store"
|
||||
self.store = MemoryStore(name=self.store_name)
|
||||
self.manager.register_store(self.store, cacheable=True)
|
||||
self.manager.register_asset(MockAsset, DummyAssetSerializer)
|
||||
self.manager.register_asset(MockAssetB, DummyAssetSerializer) # Register the new mock type
|
||||
MockAsset._build_counter = 0
|
||||
MockAssetB._build_counter = 0
|
||||
|
||||
def tearDown(self):
|
||||
self.loop.close()
|
||||
|
||||
def _run_async(self, coro):
|
||||
return self.loop.run_until_complete(coro)
|
||||
|
||||
def test_get_caches_asset(self):
|
||||
uri_str = "mock_asset://asset1"
|
||||
raw_data = b"asset1_data"
|
||||
self._run_async(self.store.create("mock_asset", "asset1", raw_data))
|
||||
|
||||
# First get - should build and cache
|
||||
asset1 = self.manager.get(uri_str, store=self.store_name)
|
||||
self.assertIsInstance(asset1, MockAsset)
|
||||
self.assertEqual(asset1.get_id(), "asset1")
|
||||
self.assertEqual(MockAsset._build_counter, 1) # Built once
|
||||
|
||||
# Second get - should hit cache
|
||||
asset2 = self.manager.get(uri_str, store=self.store_name)
|
||||
self.assertIsInstance(asset2, MockAsset)
|
||||
self.assertEqual(asset2.get_id(), "asset1")
|
||||
self.assertEqual(MockAsset._build_counter, 1) # Still 1, not rebuilt
|
||||
self.assertIs(asset1, asset2) # Should be the same instance from cache
|
||||
|
||||
def test_get_respects_depth_in_cache_key(self):
|
||||
uri_str = "mock_asset://asset_depth"
|
||||
# A depends on B (mock_asset_b://dep_b)
|
||||
raw_data_a = b"dep:mock_asset_b://dep_b"
|
||||
raw_data_b = b"dep_b_data"
|
||||
|
||||
self._run_async(self.store.create("mock_asset", "asset_depth", raw_data_a))
|
||||
self._run_async(self.store.create("mock_asset_b", "dep_b", raw_data_b))
|
||||
|
||||
# Get with depth=0 (shallow)
|
||||
asset_shallow = self.manager.get(uri_str, store=self.store_name, depth=0)
|
||||
self.assertIsInstance(asset_shallow, MockAsset)
|
||||
self.assertEqual(len(asset_shallow.resolved_dependencies), 0)
|
||||
self.assertEqual(MockAsset._build_counter, 1) # asset_depth built
|
||||
|
||||
# Get with depth=None (full)
|
||||
asset_full = self.manager.get(uri_str, store=self.store_name, depth=None)
|
||||
self.assertIsInstance(asset_full, MockAsset)
|
||||
self.assertEqual(len(asset_full.resolved_dependencies), 1)
|
||||
# asset_depth (MockAsset) built twice (once shallow, once full)
|
||||
self.assertEqual(MockAsset._build_counter, 2)
|
||||
# dep_b (MockAssetB) built once as a dependency of the full asset_depth
|
||||
self.assertEqual(MockAssetB._build_counter, 1)
|
||||
|
||||
# Get shallow again - should hit shallow cache
|
||||
asset_shallow_2 = self.manager.get(uri_str, store=self.store_name, depth=0)
|
||||
self.assertIs(asset_shallow, asset_shallow_2)
|
||||
self.assertEqual(MockAsset._build_counter, 2) # No new MockAsset builds
|
||||
self.assertEqual(MockAssetB._build_counter, 1) # No new MockAssetB builds
|
||||
|
||||
# Get full again - should hit full cache
|
||||
asset_full_2 = self.manager.get(uri_str, store=self.store_name, depth=None)
|
||||
self.assertIs(asset_full, asset_full_2)
|
||||
self.assertEqual(MockAsset._build_counter, 2) # No new MockAsset builds
|
||||
self.assertEqual(MockAssetB._build_counter, 1) # No new MockAssetB builds
|
||||
|
||||
def test_update_invalidates_cache(self):
|
||||
uri_str = "mock_asset://asset_upd"
|
||||
raw_data_v1 = b"version1"
|
||||
raw_data_v2 = b"version2"
|
||||
asset_uri = AssetUri(uri_str) # Use real AssetUri
|
||||
|
||||
self._run_async(self.store.create(asset_uri.asset_type, asset_uri.asset_id, raw_data_v1))
|
||||
|
||||
asset_v1 = self.manager.get(asset_uri, store=self.store_name)
|
||||
self.assertEqual(asset_v1.raw_data_content, raw_data_v1)
|
||||
self.assertEqual(MockAsset._build_counter, 1)
|
||||
|
||||
# Update the asset in the store (MemoryStore creates new version)
|
||||
# For this test, let's simulate an update by re-adding with add_raw
|
||||
# which should trigger invalidation.
|
||||
# Note: MemoryStore's update creates a new version, so get() would get latest.
|
||||
# To test invalidation of the *exact* cached object, we'd need to ensure
|
||||
# the cache key changes (e.g. raw_data_hash).
|
||||
# Let's use add_raw which calls invalidate.
|
||||
self.manager.add_raw(
|
||||
asset_uri.asset_type, asset_uri.asset_id, raw_data_v2, store=self.store_name
|
||||
)
|
||||
|
||||
# Get again - should rebuild because v1 was invalidated
|
||||
# And MemoryStore's get() for a URI without version gets latest.
|
||||
# The add_raw invalidates based on URI (type+id), so all versions of it.
|
||||
asset_v2 = self.manager.get(asset_uri, store=self.store_name)
|
||||
self.assertEqual(asset_v2.raw_data_content, raw_data_v2)
|
||||
self.assertEqual(MockAsset._build_counter, 2) # Rebuilt
|
||||
|
||||
def test_delete_invalidates_cache(self):
|
||||
uri_str = "mock_asset://asset_del"
|
||||
raw_data = b"delete_me"
|
||||
asset_uri = AssetUri(uri_str)
|
||||
self._run_async(self.store.create(asset_uri.asset_type, asset_uri.asset_id, raw_data))
|
||||
|
||||
asset1 = self.manager.get(asset_uri, store=self.store_name)
|
||||
self.assertIsNotNone(asset1)
|
||||
self.assertEqual(MockAsset._build_counter, 1)
|
||||
|
||||
self.manager.delete(asset_uri, store=self.store_name)
|
||||
|
||||
with self.assertRaises(FileNotFoundError):
|
||||
self.manager.get(asset_uri, store=self.store_name)
|
||||
# Check build counter didn't increase due to trying to get deleted asset
|
||||
self.assertEqual(MockAsset._build_counter, 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
468
src/Mod/CAM/CAMTests/TestPathToolAssetManager.py
Normal file
468
src/Mod/CAM/CAMTests/TestPathToolAssetManager.py
Normal file
@@ -0,0 +1,468 @@
|
||||
import unittest
|
||||
import asyncio
|
||||
from unittest.mock import Mock
|
||||
import pathlib
|
||||
import tempfile
|
||||
from typing import Any, Mapping, List
|
||||
from Path.Tool.assets import (
|
||||
AssetManager,
|
||||
FileStore,
|
||||
Asset,
|
||||
AssetUri,
|
||||
MemoryStore,
|
||||
AssetSerializer,
|
||||
DummyAssetSerializer,
|
||||
)
|
||||
|
||||
|
||||
# Mock Asset class for testing
|
||||
class MockAsset(Asset):
|
||||
asset_type: str = "mock_asset"
|
||||
|
||||
def __init__(self, data: Any = None, id: str = "mock_id"):
|
||||
self._data = data
|
||||
self._id = id
|
||||
|
||||
@classmethod
|
||||
def extract_dependencies(cls, data: bytes, serializer: AssetSerializer) -> List[AssetUri]:
|
||||
# Mock implementation doesn't use data or format for dependencies
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
def from_bytes(
|
||||
cls,
|
||||
data: bytes,
|
||||
id: str,
|
||||
dependencies: Mapping[AssetUri, Asset] | None,
|
||||
serializer: AssetSerializer,
|
||||
) -> "MockAsset":
|
||||
# Create instance with provided id
|
||||
return cls(data, id)
|
||||
|
||||
def to_bytes(self, serializer: AssetSerializer) -> bytes:
|
||||
return self._data
|
||||
|
||||
def get_id(self) -> str:
|
||||
return self._id
|
||||
|
||||
|
||||
class TestPathToolAssetManager(unittest.TestCase):
|
||||
def test_register_store(self):
|
||||
manager = AssetManager()
|
||||
mock_store_local = Mock()
|
||||
mock_store_local.name = "local"
|
||||
mock_store_remote = Mock()
|
||||
mock_store_remote.name = "remote"
|
||||
|
||||
manager.register_store(mock_store_local)
|
||||
self.assertEqual(manager.stores["local"], mock_store_local)
|
||||
|
||||
manager.register_store(mock_store_remote)
|
||||
self.assertEqual(manager.stores["remote"], mock_store_remote)
|
||||
|
||||
# Test overwriting
|
||||
mock_store_local_new = Mock()
|
||||
mock_store_local_new.name = "local"
|
||||
manager.register_store(mock_store_local_new)
|
||||
self.assertEqual(manager.stores["local"], mock_store_local_new)
|
||||
|
||||
def test_register_asset(self):
|
||||
manager = AssetManager()
|
||||
# Register the actual MockAsset class
|
||||
manager.register_asset(MockAsset, DummyAssetSerializer)
|
||||
self.assertEqual(manager._asset_classes[MockAsset.asset_type], MockAsset)
|
||||
|
||||
# Test registering a different actual Asset class
|
||||
class AnotherMockAsset(Asset):
|
||||
asset_type: str = "another_mock_asset"
|
||||
|
||||
@classmethod
|
||||
def dependencies(cls, data: bytes) -> List[AssetUri]:
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
def from_bytes(
|
||||
cls,
|
||||
data: bytes,
|
||||
id: str,
|
||||
dependencies: Mapping[AssetUri, Asset] | None,
|
||||
serializer: AssetSerializer,
|
||||
) -> "AnotherMockAsset":
|
||||
return cls()
|
||||
|
||||
def to_bytes(self, serializer: AssetSerializer) -> bytes:
|
||||
return b""
|
||||
|
||||
def get_id(self) -> str:
|
||||
return "another_mock_id"
|
||||
|
||||
manager.register_asset(AnotherMockAsset, DummyAssetSerializer)
|
||||
self.assertEqual(manager._asset_classes[AnotherMockAsset.asset_type], AnotherMockAsset)
|
||||
|
||||
# Test overwriting
|
||||
manager.register_asset(
|
||||
MockAsset, DummyAssetSerializer
|
||||
) # Registering again should overwrite
|
||||
self.assertEqual(manager._asset_classes[MockAsset.asset_type], MockAsset)
|
||||
|
||||
# Test registering non-Asset class
|
||||
with self.assertRaises(TypeError):
|
||||
|
||||
class NotAnAsset(Asset): # Inherit from Asset
|
||||
pass
|
||||
|
||||
manager.register_asset(NotAnAsset, DummyAssetSerializer)
|
||||
|
||||
def test_get(self):
|
||||
# Setup AssetManager with a real LocalStore and the MockAsset class
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
base_dir = pathlib.Path(tmpdir)
|
||||
local_store = FileStore("local", base_dir)
|
||||
manager = AssetManager()
|
||||
manager.register_store(local_store)
|
||||
|
||||
# Register the MockAsset class
|
||||
manager.register_asset(MockAsset, DummyAssetSerializer)
|
||||
|
||||
# Create a test asset file via AssetManager
|
||||
test_data = b"test asset data"
|
||||
test_uri = manager.add_raw(
|
||||
asset_type=MockAsset.asset_type,
|
||||
asset_id="dummy_id_get",
|
||||
data=test_data,
|
||||
store="local",
|
||||
)
|
||||
|
||||
# Call AssetManager.get
|
||||
retrieved_object = manager.get(test_uri)
|
||||
|
||||
# Assert the retrieved object is an instance of MockAsset
|
||||
self.assertIsInstance(retrieved_object, MockAsset)
|
||||
# Assert the data was passed to from_bytes
|
||||
self.assertEqual(retrieved_object._data, test_data)
|
||||
|
||||
# Test error handling for non-existent URI
|
||||
non_existent_uri = AssetUri.build(MockAsset.asset_type, "non_existent", "1")
|
||||
with self.assertRaises(FileNotFoundError):
|
||||
manager.get(non_existent_uri)
|
||||
|
||||
# Test error handling for no asset class registered
|
||||
non_registered_uri = AssetUri.build("non_existent_type", "dummy_id", "1")
|
||||
# Need to create a dummy file for the store to find
|
||||
dummy_data = b"dummy"
|
||||
manager.add_raw(
|
||||
asset_type="non_existent_type", asset_id="dummy_id", data=dummy_data, store="local"
|
||||
)
|
||||
|
||||
with self.assertRaises(ValueError) as cm:
|
||||
manager.get(non_registered_uri)
|
||||
self.assertIn("No asset class registered for asset type:", str(cm.exception))
|
||||
|
||||
def test_delete(self):
|
||||
# Setup AssetManager with a real LocalStore
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
base_dir = pathlib.Path(tmpdir)
|
||||
local_store = FileStore("local", base_dir)
|
||||
manager = AssetManager()
|
||||
manager.register_store(local_store)
|
||||
|
||||
# Create a test asset file
|
||||
test_data = b"test asset data to delete"
|
||||
test_uri = manager.add_raw(
|
||||
asset_type="temp_asset", asset_id="dummy_id_delete", data=test_data, store="local"
|
||||
)
|
||||
test_path = base_dir / "temp_asset" / str(test_uri.asset_id) / str(test_uri.version)
|
||||
self.assertTrue(test_path.exists())
|
||||
|
||||
# Call AssetManager.delete
|
||||
manager.delete(test_uri)
|
||||
|
||||
# Verify file deletion
|
||||
self.assertFalse(test_path.exists())
|
||||
|
||||
# Test error handling for non-existent URI (should not raise error
|
||||
# as LocalStore.delete handles this)
|
||||
non_existent_uri = AssetUri.build(
|
||||
"temp_asset", "non_existent", "1" # Keep original for logging
|
||||
)
|
||||
manager.delete(non_existent_uri) # Should not raise
|
||||
|
||||
def test_create(self):
|
||||
# Setup AssetManager with LocalStore and MockAsset class
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
base_dir = pathlib.Path(tmpdir)
|
||||
local_store = FileStore("local", base_dir)
|
||||
manager = AssetManager()
|
||||
manager.register_store(local_store)
|
||||
|
||||
# Register the MockAsset class
|
||||
manager.register_asset(MockAsset, DummyAssetSerializer)
|
||||
|
||||
# Create a MockAsset instance with a specific id
|
||||
test_obj = MockAsset(b"object data", id="mocked_asset_id")
|
||||
|
||||
# Call manager.add to create
|
||||
created_uri = manager.add(test_obj, store="local")
|
||||
|
||||
# Assert returned URI is as expected
|
||||
expected_uri = AssetUri.build(MockAsset.asset_type, "mocked_asset_id", "1")
|
||||
self.assertEqual(created_uri, expected_uri)
|
||||
|
||||
# Verify the asset was created
|
||||
retrieved_data = asyncio.run(local_store.get(created_uri))
|
||||
self.assertEqual(retrieved_data, test_obj.to_bytes(DummyAssetSerializer))
|
||||
|
||||
# Test error handling (store not found)
|
||||
with self.assertRaises(ValueError) as cm:
|
||||
manager.add(test_obj, store="non_existent_store")
|
||||
self.assertIn("No store registered for name:", str(cm.exception))
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
local_store = MemoryStore("local")
|
||||
manager = AssetManager()
|
||||
manager.register_store(local_store)
|
||||
|
||||
# Register the MockAsset class
|
||||
manager.register_asset(MockAsset, DummyAssetSerializer)
|
||||
|
||||
# First, create an asset
|
||||
initial_data = b"initial data"
|
||||
asset_id = "some_asset_id"
|
||||
test_uri = manager.add_raw(MockAsset.asset_type, asset_id, initial_data, "local")
|
||||
self.assertEqual(test_uri.version, "1")
|
||||
|
||||
# Create a MockAsset instance with the same id for update
|
||||
updated_data = b"updated object data"
|
||||
test_obj = MockAsset(updated_data, id=asset_id)
|
||||
|
||||
# Call manager.add to update
|
||||
updated_uri = manager.add(test_obj, store="local")
|
||||
|
||||
# Assert returned URI matches the original except for version
|
||||
self.assertEqual(updated_uri.asset_type, test_uri.asset_type)
|
||||
self.assertEqual(updated_uri.asset_id, test_uri.asset_id)
|
||||
self.assertEqual(updated_uri.version, "2")
|
||||
|
||||
# Verify the asset was updated
|
||||
obj = manager.get(updated_uri, store="local")
|
||||
self.assertEqual(updated_data, test_obj.to_bytes(DummyAssetSerializer))
|
||||
self.assertEqual(updated_data, obj.to_bytes(DummyAssetSerializer))
|
||||
|
||||
# Test error handling (store not found)
|
||||
with self.assertRaises(ValueError) as cm:
|
||||
manager.add(test_obj, store="non_existent_store")
|
||||
self.assertIn("No store registered for name:", str(cm.exception))
|
||||
|
||||
def test_create_raw(self):
|
||||
# Setup AssetManager with a real MemoryStore
|
||||
memory_store = MemoryStore("memory_raw")
|
||||
manager = AssetManager()
|
||||
manager.register_store(memory_store)
|
||||
|
||||
asset_type = "raw_test_type"
|
||||
asset_id = "raw_test_id"
|
||||
data = b"raw test data"
|
||||
|
||||
# Expected URI with version 1 (assuming MemoryStore uses integer versions)
|
||||
expected_uri = AssetUri.build(asset_type, asset_id, "1")
|
||||
|
||||
# Call manager.add_raw
|
||||
created_uri = manager.add_raw(
|
||||
asset_type=asset_type, asset_id=asset_id, data=data, store="memory_raw"
|
||||
)
|
||||
|
||||
# Assert returned URI is correct (check asset_type and asset_id)
|
||||
self.assertEqual(created_uri.asset_type, asset_type)
|
||||
self.assertEqual(created_uri.asset_id, asset_id)
|
||||
self.assertEqual(created_uri, expected_uri)
|
||||
|
||||
# Verify data was stored using the actual created_uri
|
||||
# Await the async get method using asyncio.run
|
||||
retrieved_data = asyncio.run(memory_store.get(created_uri))
|
||||
self.assertEqual(retrieved_data, data)
|
||||
|
||||
# Test error handling (store not found)
|
||||
with self.assertRaises(ValueError) as cm:
|
||||
manager.add_raw(
|
||||
asset_type=asset_type, asset_id=asset_id, data=data, store="non_existent_store"
|
||||
)
|
||||
self.assertIn("No store registered for name:", str(cm.exception))
|
||||
|
||||
def test_get_raw(self):
|
||||
# Setup AssetManager with a real MemoryStore
|
||||
memory_store = MemoryStore("memory_raw_get")
|
||||
manager = AssetManager()
|
||||
manager.register_store(memory_store)
|
||||
|
||||
test_uri_str = "test_type://test_id/1"
|
||||
test_uri = AssetUri(test_uri_str)
|
||||
expected_data = b"retrieved raw data"
|
||||
|
||||
# Manually put data into the memory store
|
||||
manager.add_raw("test_type", "test_id", expected_data, "memory_raw_get")
|
||||
|
||||
# Call manager.get_raw using the URI returned by add_raw
|
||||
retrieved_data = manager.get_raw(test_uri, store="memory_raw_get")
|
||||
|
||||
# Assert returned data matches store's result
|
||||
self.assertEqual(retrieved_data, expected_data)
|
||||
|
||||
# Test error handling (store not found)
|
||||
non_existent_uri = AssetUri("type://id/1")
|
||||
with self.assertRaises(ValueError) as cm:
|
||||
manager.get_raw(non_existent_uri, store="non_existent_store")
|
||||
self.assertIn("No store registered for name:", str(cm.exception))
|
||||
|
||||
def test_is_empty(self):
|
||||
# Setup AssetManager with a real MemoryStore
|
||||
memory_store = MemoryStore("memory_empty")
|
||||
manager = AssetManager()
|
||||
manager.register_store(memory_store)
|
||||
|
||||
# Test when store is empty
|
||||
self.assertTrue(manager.is_empty(store="memory_empty"))
|
||||
|
||||
# Add an asset and test again
|
||||
manager.add_raw("test_type", "test_id", b"data", "memory_empty")
|
||||
self.assertFalse(manager.is_empty(store="memory_empty"))
|
||||
|
||||
# Test with asset type
|
||||
self.assertTrue(manager.is_empty(store="memory_empty", asset_type="another_type"))
|
||||
self.assertFalse(manager.is_empty(store="memory_empty", asset_type="test_type"))
|
||||
|
||||
# Test error handling (store not found)
|
||||
with self.assertRaises(ValueError) as cm:
|
||||
manager.is_empty(store="non_existent_store")
|
||||
self.assertIn("No store registered for name:", str(cm.exception))
|
||||
|
||||
def test_count_assets(self):
|
||||
# Setup AssetManager with a real MemoryStore
|
||||
memory_store = MemoryStore("memory_count")
|
||||
manager = AssetManager()
|
||||
manager.register_store(memory_store)
|
||||
|
||||
# Test when store is empty
|
||||
self.assertEqual(manager.count_assets(store="memory_count"), 0)
|
||||
self.assertEqual(manager.count_assets(store="memory_count", asset_type="type1"), 0)
|
||||
|
||||
# Add assets and test counts
|
||||
manager.add_raw("type1", "asset1", b"data1", "memory_count")
|
||||
self.assertEqual(manager.count_assets(store="memory_count"), 1)
|
||||
self.assertEqual(manager.count_assets(store="memory_count", asset_type="type1"), 1)
|
||||
self.assertEqual(manager.count_assets(store="memory_count", asset_type="type2"), 0)
|
||||
|
||||
manager.add_raw("type2", "asset2", b"data2", "memory_count")
|
||||
manager.add_raw("type1", "asset3", b"data3", "memory_count")
|
||||
self.assertEqual(manager.count_assets(store="memory_count"), 3)
|
||||
self.assertEqual(manager.count_assets(store="memory_count", asset_type="type1"), 2)
|
||||
self.assertEqual(manager.count_assets(store="memory_count", asset_type="type2"), 1)
|
||||
|
||||
# Test error handling (store not found)
|
||||
with self.assertRaises(ValueError) as cm:
|
||||
manager.count_assets(store="non_existent_store")
|
||||
self.assertIn("No store registered for name:", str(cm.exception))
|
||||
|
||||
def test_get_bulk(self):
|
||||
# Setup AssetManager with a real MemoryStore and MockAsset class
|
||||
memory_store = MemoryStore("memory_bulk")
|
||||
manager = AssetManager()
|
||||
manager.register_store(memory_store)
|
||||
manager.register_asset(MockAsset, DummyAssetSerializer)
|
||||
|
||||
# Create some assets in the memory store
|
||||
data1 = b"data for id1"
|
||||
data2 = b"data for id2"
|
||||
uri1 = manager.add_raw(MockAsset.asset_type, "id1", data1, "memory_bulk")
|
||||
uri2 = manager.add_raw(MockAsset.asset_type, "id2", data2, "memory_bulk")
|
||||
uri3 = AssetUri.build(MockAsset.asset_type, "non_existent", "1")
|
||||
uris = [uri1, uri2, uri3]
|
||||
|
||||
# Call manager.get_bulk
|
||||
retrieved_assets = manager.get_bulk(uris, store="memory_bulk")
|
||||
|
||||
# Assert the correct number of assets were returned
|
||||
self.assertEqual(len(retrieved_assets), 3)
|
||||
|
||||
# Assert the retrieved assets are MockAsset instances with correct data
|
||||
self.assertIsInstance(retrieved_assets[0], MockAsset)
|
||||
self.assertEqual(
|
||||
retrieved_assets[0].to_bytes(DummyAssetSerializer),
|
||||
data1,
|
||||
)
|
||||
|
||||
self.assertIsInstance(retrieved_assets[1], MockAsset)
|
||||
self.assertEqual(
|
||||
retrieved_assets[1].to_bytes(DummyAssetSerializer),
|
||||
data2,
|
||||
)
|
||||
|
||||
# Assert the non-existent asset is None
|
||||
self.assertIsNone(retrieved_assets[2])
|
||||
|
||||
# Test error handling (store not found)
|
||||
with self.assertRaises(ValueError) as cm:
|
||||
manager.get_bulk(uris, store="non_existent_store")
|
||||
self.assertIn("No store registered for name:", str(cm.exception))
|
||||
|
||||
def test_fetch(self):
|
||||
# Setup AssetManager with a real MemoryStore and MockAsset class
|
||||
memory_store = MemoryStore("memory_fetch")
|
||||
manager = AssetManager()
|
||||
manager.register_store(memory_store)
|
||||
manager.register_asset(MockAsset, DummyAssetSerializer)
|
||||
|
||||
# Create some assets in the memory store
|
||||
data1 = b"data for id1"
|
||||
data2 = b"data for id2"
|
||||
manager.add_raw(MockAsset.asset_type, "id1", data1, "memory_fetch")
|
||||
manager.add_raw(MockAsset.asset_type, "id2", data2, "memory_fetch")
|
||||
# Create an asset of a different type
|
||||
manager.add_raw("another_type", "id3", b"data for id3", "memory_fetch")
|
||||
AssetUri.build(MockAsset.asset_type, "non_existent", "1")
|
||||
|
||||
# Call manager.fetch without filters
|
||||
# This should raise ValueError because uri3 has an unregistered type
|
||||
with self.assertRaises(ValueError) as cm:
|
||||
manager.fetch(store="memory_fetch")
|
||||
self.assertIn("No asset class registered for asset type:", str(cm.exception))
|
||||
|
||||
# Now test fetching with a registered asset type filter
|
||||
# Setup a new manager and store to avoid state from previous test
|
||||
memory_store_filtered = MemoryStore("memory_fetch_filtered")
|
||||
manager_filtered = AssetManager()
|
||||
manager_filtered.register_store(memory_store_filtered)
|
||||
manager_filtered.register_asset(MockAsset, DummyAssetSerializer)
|
||||
|
||||
# Create assets again
|
||||
manager_filtered.add_raw(MockAsset.asset_type, "id1", data1, "memory_fetch_filtered")
|
||||
manager_filtered.add_raw(MockAsset.asset_type, "id2", data2, "memory_fetch_filtered")
|
||||
manager_filtered.add_raw("another_type", "id3", b"data for id3", "memory_fetch_filtered")
|
||||
|
||||
retrieved_assets_filtered = manager_filtered.fetch(
|
||||
asset_type=MockAsset.asset_type, store="memory_fetch_filtered"
|
||||
)
|
||||
|
||||
# Assert the correct number of assets were returned
|
||||
self.assertEqual(len(retrieved_assets_filtered), 2)
|
||||
|
||||
# Assert the retrieved assets are MockAsset instances with correct data
|
||||
self.assertIsInstance(retrieved_assets_filtered[0], MockAsset)
|
||||
self.assertEqual(
|
||||
retrieved_assets_filtered[0].to_bytes(DummyAssetSerializer).decode("utf-8"),
|
||||
data1.decode("utf-8"),
|
||||
)
|
||||
|
||||
self.assertIsInstance(retrieved_assets_filtered[1], MockAsset)
|
||||
self.assertEqual(
|
||||
retrieved_assets_filtered[1].to_bytes(DummyAssetSerializer).decode("utf-8"),
|
||||
data2.decode("utf-8"),
|
||||
)
|
||||
|
||||
# Test error handling (store not found)
|
||||
with self.assertRaises(ValueError) as cm:
|
||||
manager.fetch(store="non_existent_store")
|
||||
self.assertIn("No store registered for name:", str(cm.exception))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
389
src/Mod/CAM/CAMTests/TestPathToolAssetStore.py
Normal file
389
src/Mod/CAM/CAMTests/TestPathToolAssetStore.py
Normal file
@@ -0,0 +1,389 @@
|
||||
import unittest
|
||||
import pathlib
|
||||
import asyncio
|
||||
import tempfile
|
||||
from uuid import uuid4
|
||||
from Path.Tool.assets import (
|
||||
AssetUri,
|
||||
AssetStore,
|
||||
MemoryStore,
|
||||
FileStore,
|
||||
)
|
||||
|
||||
|
||||
class BaseTestPathToolAssetStore(unittest.TestCase):
|
||||
"""
|
||||
Base test suite for Path Tool Asset Stores assuming full versioning support.
|
||||
Store-agnostic tests without direct file system access.
|
||||
"""
|
||||
|
||||
store: AssetStore
|
||||
|
||||
def setUp(self):
|
||||
self.tmp_dir = tempfile.TemporaryDirectory()
|
||||
self.tmp_path = pathlib.Path(self.tmp_dir.name)
|
||||
|
||||
def tearDown(self):
|
||||
self.tmp_dir.cleanup()
|
||||
|
||||
def test_name(self):
|
||||
self.assertIsNotNone(self.store)
|
||||
self.assertIsInstance(self.store.name, str)
|
||||
self.assertTrue(len(self.store.name) > 0)
|
||||
|
||||
def test_create_and_get(self):
|
||||
async def async_test():
|
||||
data = b"test data"
|
||||
asset_type = f"type_{uuid4()}"
|
||||
asset_id = f"asset_{uuid4()}"
|
||||
uri = await self.store.create(asset_type, asset_id, data)
|
||||
|
||||
self.assertIsInstance(uri, AssetUri)
|
||||
self.assertEqual(uri.asset_type, asset_type)
|
||||
self.assertEqual(uri.asset_id, asset_id)
|
||||
self.assertIsNotNone(uri.version)
|
||||
|
||||
retrieved_data = await self.store.get(uri)
|
||||
self.assertEqual(retrieved_data, data)
|
||||
|
||||
# Test non-existent URI
|
||||
non_existent_uri = AssetUri.build(
|
||||
asset_type="non_existent", asset_id="missing", version="1"
|
||||
)
|
||||
with self.assertRaises(FileNotFoundError):
|
||||
await self.store.get(non_existent_uri)
|
||||
|
||||
asyncio.run(async_test())
|
||||
|
||||
def test_delete(self):
|
||||
async def async_test():
|
||||
data = b"data to delete"
|
||||
asset_type = "delete_type"
|
||||
asset_id = f"asset_{uuid4()}"
|
||||
uri = await self.store.create(asset_type, asset_id, data)
|
||||
|
||||
await self.store.delete(uri)
|
||||
|
||||
with self.assertRaises(FileNotFoundError):
|
||||
await self.store.get(uri)
|
||||
|
||||
# Deleting non-existent URI should not raise
|
||||
non_existent_uri = AssetUri.build(
|
||||
asset_type="non_existent", asset_id="missing", version="1"
|
||||
)
|
||||
await self.store.delete(non_existent_uri)
|
||||
|
||||
asyncio.run(async_test())
|
||||
|
||||
def test_is_empty(self):
|
||||
async def async_test():
|
||||
self.assertTrue(await self.store.is_empty())
|
||||
self.assertTrue(await self.store.is_empty("type1"))
|
||||
|
||||
uri1 = await self.store.create("type1", f"asset_{uuid4()}", b"data")
|
||||
self.assertFalse(await self.store.is_empty())
|
||||
self.assertFalse(await self.store.is_empty("type1"))
|
||||
self.assertTrue(await self.store.is_empty("type2"))
|
||||
|
||||
uri2 = await self.store.create("type2", f"asset_{uuid4()}", b"data")
|
||||
await self.store.delete(uri1)
|
||||
self.assertFalse(await self.store.is_empty())
|
||||
self.assertTrue(await self.store.is_empty("type1"))
|
||||
self.assertFalse(await self.store.is_empty("type2"))
|
||||
|
||||
await self.store.delete(uri2)
|
||||
self.assertTrue(await self.store.is_empty())
|
||||
|
||||
asyncio.run(async_test())
|
||||
|
||||
def test_count_assets(self):
|
||||
async def async_test():
|
||||
self.assertEqual(await self.store.count_assets(), 0)
|
||||
self.assertEqual(await self.store.count_assets("type1"), 0)
|
||||
|
||||
uri1 = await self.store.create("type1", f"asset1_{uuid4()}", b"data1")
|
||||
self.assertEqual(await self.store.count_assets(), 1)
|
||||
self.assertEqual(await self.store.count_assets("type1"), 1)
|
||||
self.assertEqual(await self.store.count_assets("type2"), 0)
|
||||
|
||||
uri2 = await self.store.create("type2", f"asset2_{uuid4()}", b"data2")
|
||||
uri3 = await self.store.create("type1", f"asset3_{uuid4()}", b"data3")
|
||||
self.assertEqual(await self.store.count_assets(), 3)
|
||||
self.assertEqual(await self.store.count_assets("type1"), 2)
|
||||
self.assertEqual(await self.store.count_assets("type2"), 1)
|
||||
|
||||
await self.store.delete(uri1)
|
||||
self.assertEqual(await self.store.count_assets(), 2)
|
||||
self.assertEqual(await self.store.count_assets("type1"), 1)
|
||||
self.assertEqual(await self.store.count_assets("type2"), 1)
|
||||
|
||||
await self.store.delete(uri2)
|
||||
await self.store.delete(uri3)
|
||||
self.assertEqual(await self.store.count_assets(), 0)
|
||||
self.assertEqual(await self.store.count_assets("type1"), 0)
|
||||
self.assertEqual(await self.store.count_assets("type2"), 0)
|
||||
|
||||
asyncio.run(async_test())
|
||||
|
||||
def test_list_assets(self):
|
||||
async def async_test():
|
||||
asset_typedata = [
|
||||
("type1", f"asset1_{uuid4()}", b"data1"),
|
||||
("type1", f"asset2_{uuid4()}", b"data2"),
|
||||
("type2", f"asset3_{uuid4()}", b"data3"),
|
||||
]
|
||||
|
||||
uris = []
|
||||
for asset_type, asset_id, data in asset_typedata:
|
||||
uri = await self.store.create(asset_type, asset_id, data)
|
||||
uris.append(uri)
|
||||
|
||||
all_assets = await self.store.list_assets()
|
||||
self.assertEqual(len(all_assets), 3)
|
||||
for uri in uris:
|
||||
self.assertTrue(any(u.asset_id == uri.asset_id for u in all_assets))
|
||||
|
||||
type1_assets = await self.store.list_assets(asset_type="type1")
|
||||
self.assertEqual(len(type1_assets), 2)
|
||||
self.assertTrue(any(u.asset_id == uris[0].asset_id for u in type1_assets))
|
||||
self.assertTrue(any(u.asset_id == uris[1].asset_id for u in type1_assets))
|
||||
|
||||
paginated = await self.store.list_assets(limit=2)
|
||||
self.assertEqual(len(paginated), 2)
|
||||
|
||||
asyncio.run(async_test())
|
||||
|
||||
def test_update_versioning(self):
|
||||
async def async_test():
|
||||
initial_data = b"initial data"
|
||||
updated_data = b"updated data"
|
||||
asset_type = f"update_{uuid4()}"
|
||||
asset_id = f"asset_{uuid4()}"
|
||||
|
||||
uri1 = await self.store.create(asset_type, asset_id, initial_data)
|
||||
uri2 = await self.store.update(uri1, updated_data)
|
||||
|
||||
self.assertEqual(uri1.asset_type, uri2.asset_type)
|
||||
self.assertEqual(uri1.asset_id, uri2.asset_id)
|
||||
self.assertEqual(uri1.version, "1")
|
||||
self.assertEqual(uri2.version, "2")
|
||||
self.assertNotEqual(uri1.version, uri2.version)
|
||||
|
||||
self.assertEqual(await self.store.get(uri1), initial_data)
|
||||
self.assertEqual(await self.store.get(uri2), updated_data)
|
||||
|
||||
with self.assertRaises(FileNotFoundError):
|
||||
non_existent_uri = AssetUri.build(
|
||||
asset_type="non_existent", asset_id="missing", version="1"
|
||||
)
|
||||
await self.store.update(non_existent_uri, b"data")
|
||||
|
||||
asyncio.run(async_test())
|
||||
|
||||
def test_list_versions(self):
|
||||
async def async_test():
|
||||
asset_type = f"version_{uuid4()}"
|
||||
asset_id = f"asset_{uuid4()}"
|
||||
data1 = b"version1"
|
||||
data2 = b"version2"
|
||||
|
||||
uri1 = await self.store.create(asset_type, asset_id, data1)
|
||||
uri2 = await self.store.update(uri1, data2)
|
||||
|
||||
versions = await self.store.list_versions(uri1)
|
||||
self.assertEqual(len(versions), 2)
|
||||
version_ids = {v.version for v in versions if v.version}
|
||||
self.assertEqual(version_ids, {uri1.version, uri2.version})
|
||||
|
||||
non_existent_uri = AssetUri.build(
|
||||
asset_type="non_existent", asset_id="missing", version="1"
|
||||
)
|
||||
self.assertEqual(await self.store.list_versions(non_existent_uri), [])
|
||||
|
||||
asyncio.run(async_test())
|
||||
|
||||
def test_create_with_empty_data(self):
|
||||
async def async_test():
|
||||
data = b""
|
||||
asset_type = "shape"
|
||||
asset_id = f"asset_{uuid4()}"
|
||||
uri = await self.store.create(asset_type, asset_id, data)
|
||||
|
||||
self.assertIsInstance(uri, AssetUri)
|
||||
retrieved_data = await self.store.get(uri)
|
||||
self.assertEqual(retrieved_data, data)
|
||||
|
||||
asyncio.run(async_test())
|
||||
|
||||
def test_list_assets_non_existent_type(self):
|
||||
async def async_test():
|
||||
assets = await self.store.list_assets(asset_type=f"non_existent_type_{uuid4()}")
|
||||
self.assertEqual(len(assets), 0)
|
||||
|
||||
asyncio.run(async_test())
|
||||
|
||||
def test_list_assets_pagination_offset_too_high(self):
|
||||
async def async_test():
|
||||
await self.store.create("shape", f"asset1_{uuid4()}", b"data")
|
||||
assets = await self.store.list_assets(offset=100) # Assuming less than 100 assets
|
||||
self.assertEqual(len(assets), 0)
|
||||
|
||||
asyncio.run(async_test())
|
||||
|
||||
def test_list_assets_pagination_limit_zero(self):
|
||||
async def async_test():
|
||||
await self.store.create("shape", f"asset1_{uuid4()}", b"data")
|
||||
assets = await self.store.list_assets(limit=0)
|
||||
self.assertEqual(len(assets), 0)
|
||||
|
||||
asyncio.run(async_test())
|
||||
|
||||
def test_create_delete_recreate(self):
|
||||
async def async_test():
|
||||
asset_type = "shape"
|
||||
asset_id = f"asset_{uuid4()}"
|
||||
data1 = b"first data"
|
||||
data2 = b"second data"
|
||||
|
||||
uri1 = await self.store.create(asset_type, asset_id, data1)
|
||||
self.assertEqual(await self.store.get(uri1), data1)
|
||||
# For versioned stores, this would be version "1"
|
||||
# For stores that don't deeply track versions, it's important what happens on recreate
|
||||
|
||||
await self.store.delete(uri1)
|
||||
with self.assertRaises(FileNotFoundError):
|
||||
await self.store.get(uri1)
|
||||
|
||||
uri2 = await self.store.create(asset_type, asset_id, data2)
|
||||
self.assertEqual(await self.store.get(uri2), data2)
|
||||
|
||||
# Behavior of uri1.version vs uri2.version depends on store implementation
|
||||
# For a fully versioned store that starts fresh:
|
||||
self.assertEqual(
|
||||
uri2.version, "1", "Recreating should yield version 1 for a fresh start"
|
||||
)
|
||||
|
||||
# Ensure only the new asset exists if the store fully removes old versions
|
||||
versions = await self.store.list_versions(
|
||||
AssetUri.build(asset_type=asset_type, asset_id=asset_id)
|
||||
)
|
||||
self.assertEqual(len(versions), 1)
|
||||
self.assertEqual(versions[0].version, "1")
|
||||
|
||||
asyncio.run(async_test())
|
||||
|
||||
def test_get_non_existent_specific_version(self):
|
||||
async def async_test():
|
||||
asset_type = "shape"
|
||||
asset_id = f"asset_{uuid4()}"
|
||||
await self.store.create(asset_type, asset_id, b"data_v1")
|
||||
|
||||
non_existent_version_uri = AssetUri.build(
|
||||
asset_type=asset_type,
|
||||
asset_id=asset_id,
|
||||
version="99", # Assuming version 99 won't exist
|
||||
)
|
||||
with self.assertRaises(FileNotFoundError):
|
||||
await self.store.get(non_existent_version_uri)
|
||||
|
||||
asyncio.run(async_test())
|
||||
|
||||
def test_delete_last_version(self):
|
||||
async def async_test():
|
||||
asset_type = "shape"
|
||||
asset_id = f"asset_{uuid4()}"
|
||||
|
||||
uri_v1 = await self.store.create(asset_type, asset_id, b"v1_data")
|
||||
uri_v2 = await self.store.update(uri_v1, b"v2_data")
|
||||
|
||||
await self.store.delete(uri_v2) # Delete latest version
|
||||
with self.assertRaises(FileNotFoundError):
|
||||
await self.store.get(uri_v2)
|
||||
|
||||
# v1 should still exist
|
||||
self.assertEqual(await self.store.get(uri_v1), b"v1_data")
|
||||
versions_after_v2_delete = await self.store.list_versions(uri_v1)
|
||||
self.assertEqual(len(versions_after_v2_delete), 1)
|
||||
self.assertEqual(versions_after_v2_delete[0].version, "1")
|
||||
|
||||
await self.store.delete(uri_v1) # Delete the now last version (v1)
|
||||
with self.assertRaises(FileNotFoundError):
|
||||
await self.store.get(uri_v1)
|
||||
|
||||
versions_after_all_delete = await self.store.list_versions(uri_v1)
|
||||
self.assertEqual(len(versions_after_all_delete), 0)
|
||||
|
||||
# Asset should not appear in list_assets
|
||||
listed_assets = await self.store.list_assets(asset_type=asset_type)
|
||||
self.assertFalse(any(a.asset_id == asset_id for a in listed_assets))
|
||||
self.assertTrue(await self.store.is_empty(asset_type))
|
||||
|
||||
asyncio.run(async_test())
|
||||
|
||||
def test_get_latest_on_non_existent_asset(self):
|
||||
async def async_test():
|
||||
latest_uri = AssetUri.build(
|
||||
asset_type="shape",
|
||||
asset_id=f"non_existent_id_for_latest_{uuid4()}",
|
||||
version="latest",
|
||||
)
|
||||
with self.assertRaises(
|
||||
FileNotFoundError
|
||||
): # Or custom NoVersionsFoundError if that's how store behaves
|
||||
await self.store.get(latest_uri)
|
||||
|
||||
asyncio.run(async_test())
|
||||
|
||||
|
||||
class TestPathToolFileStore(BaseTestPathToolAssetStore):
|
||||
"""Test suite for FileStore with full versioning support."""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
asset_type_map = {
|
||||
"*": "{asset_type}/{asset_id}/{version}",
|
||||
"special1": "Especial/{asset_id}/{version}",
|
||||
"special2": "my/super/{asset_id}.spcl",
|
||||
}
|
||||
self.store = FileStore("versioned", self.tmp_path, asset_type_map)
|
||||
|
||||
def test_get_latest_version(self):
|
||||
async def async_test():
|
||||
asset_type = f"latest_{uuid4()}"
|
||||
asset_id = f"asset_{uuid4()}"
|
||||
|
||||
uri1 = await self.store.create(asset_type, asset_id, b"v1")
|
||||
await self.store.update(uri1, b"v2")
|
||||
|
||||
latest_uri = AssetUri.build(asset_type=asset_type, asset_id=asset_id, version="latest")
|
||||
self.assertEqual(await self.store.get(latest_uri), b"v2")
|
||||
|
||||
asyncio.run(async_test())
|
||||
|
||||
def test_delete_all_versions(self):
|
||||
async def async_test():
|
||||
asset_type = f"delete_{uuid4()}"
|
||||
asset_id = f"asset_{uuid4()}"
|
||||
|
||||
uri1 = await self.store.create(asset_type, asset_id, b"v1")
|
||||
await self.store.update(uri1, b"v2")
|
||||
|
||||
uri = AssetUri.build(asset_type=asset_type, asset_id=asset_id)
|
||||
await self.store.delete(uri)
|
||||
|
||||
with self.assertRaises(FileNotFoundError):
|
||||
await self.store.get(uri1)
|
||||
|
||||
asyncio.run(async_test())
|
||||
|
||||
|
||||
class TestPathToolMemoryStore(BaseTestPathToolAssetStore):
|
||||
"""Test suite for MemoryStore."""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.store = MemoryStore("memory_test")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
101
src/Mod/CAM/CAMTests/TestPathToolAssetUri.py
Normal file
101
src/Mod/CAM/CAMTests/TestPathToolAssetUri.py
Normal file
@@ -0,0 +1,101 @@
|
||||
import unittest
|
||||
from Path.Tool.assets.uri import AssetUri
|
||||
|
||||
|
||||
class TestPathToolAssetUri(unittest.TestCase):
|
||||
"""
|
||||
Test suite for the AssetUri utility class.
|
||||
"""
|
||||
|
||||
def test_uri_parsing_full(self):
|
||||
uri_string = "remote://asset_id/version?" "param1=value1¶m2=value2"
|
||||
uri = AssetUri(uri_string)
|
||||
self.assertEqual(uri.asset_type, "remote")
|
||||
self.assertEqual(uri.asset_id, "asset_id")
|
||||
self.assertEqual(uri.version, "version")
|
||||
self.assertEqual(uri.params, {"param1": ["value1"], "param2": ["value2"]})
|
||||
self.assertEqual(str(uri), uri_string)
|
||||
self.assertEqual(repr(uri), f"AssetUri('{uri_string}')")
|
||||
|
||||
def test_uri_parsing_local(self):
|
||||
uri_string = "local://id/2?param=value"
|
||||
uri = AssetUri(uri_string)
|
||||
self.assertEqual(uri.asset_type, "local")
|
||||
self.assertEqual(uri.asset_id, "id")
|
||||
self.assertEqual(uri.version, "2")
|
||||
self.assertEqual(uri.params, {"param": ["value"]})
|
||||
self.assertEqual(str(uri), uri_string)
|
||||
self.assertEqual(repr(uri), f"AssetUri('{uri_string}')")
|
||||
|
||||
def test_uri_parsing_no_params(self):
|
||||
uri_string = "file://asset_id/1"
|
||||
uri = AssetUri(uri_string)
|
||||
self.assertEqual(uri.asset_type, "file")
|
||||
self.assertEqual(uri.asset_id, "asset_id")
|
||||
self.assertEqual(uri.version, "1")
|
||||
self.assertEqual(uri.params, {})
|
||||
self.assertEqual(str(uri), uri_string)
|
||||
self.assertEqual(repr(uri), f"AssetUri('{uri_string}')")
|
||||
|
||||
def test_uri_version_missing(self):
|
||||
uri_string = "foo://asset"
|
||||
uri = AssetUri(uri_string)
|
||||
self.assertEqual(uri.asset_type, "foo")
|
||||
self.assertEqual(uri.asset_id, "asset")
|
||||
self.assertIsNone(uri.version)
|
||||
self.assertEqual(uri.params, {})
|
||||
self.assertEqual(str(uri), uri_string)
|
||||
|
||||
def test_uri_parsing_with_version(self):
|
||||
"""
|
||||
Test parsing a URI string with asset_type, asset_id, and version.
|
||||
"""
|
||||
uri_string = "test_type://test_id/1"
|
||||
uri = AssetUri(uri_string)
|
||||
self.assertEqual(uri.asset_type, "test_type")
|
||||
self.assertEqual(uri.asset_id, "test_id")
|
||||
self.assertEqual(uri.version, "1")
|
||||
self.assertEqual(uri.params, {})
|
||||
self.assertEqual(str(uri), uri_string)
|
||||
self.assertEqual(repr(uri), f"AssetUri('{uri_string}')")
|
||||
|
||||
def test_uri_build_full(self):
|
||||
expected_uri_string = "local://asset_id/version?param1=value1"
|
||||
uri = AssetUri.build(
|
||||
asset_type="local", asset_id="asset_id", version="version", params={"param1": "value1"}
|
||||
)
|
||||
self.assertEqual(str(uri), expected_uri_string)
|
||||
self.assertEqual(uri.asset_type, "local")
|
||||
self.assertEqual(uri.asset_id, "asset_id")
|
||||
self.assertEqual(uri.version, "version")
|
||||
self.assertEqual(uri.params, {"param1": ["value1"]}) # parse_qs always returns list
|
||||
|
||||
def test_uri_build_latest_version_no_params(self):
|
||||
expected_uri_string = "remote://id/latest"
|
||||
uri = AssetUri.build(asset_type="remote", asset_id="id", version="latest")
|
||||
self.assertEqual(str(uri), expected_uri_string)
|
||||
self.assertEqual(uri.asset_type, "remote")
|
||||
self.assertEqual(uri.asset_id, "id")
|
||||
self.assertEqual(uri.version, "latest")
|
||||
self.assertEqual(uri.params, {})
|
||||
|
||||
def test_uri_equality(self):
|
||||
uri1 = AssetUri("local://asset/version")
|
||||
uri2 = AssetUri("local://asset/version")
|
||||
uri3 = AssetUri("local://asset/another_version")
|
||||
self.assertEqual(uri1, uri2)
|
||||
self.assertNotEqual(uri1, uri3)
|
||||
self.assertNotEqual(uri1, "not a uri")
|
||||
|
||||
def test_uri_parsing_invalid_path_structure(self):
|
||||
"""
|
||||
Test that parsing a URI string with an invalid path structure
|
||||
(more than one component) raises a ValueError.
|
||||
"""
|
||||
uri_string = "local://foo/bar/1"
|
||||
with self.assertRaisesRegex(ValueError, "Invalid URI path structure:"):
|
||||
AssetUri(uri_string)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -20,134 +20,62 @@
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
import Path.Tool.Bit as PathToolBit
|
||||
import CAMTests.PathTestUtils as PathTestUtils
|
||||
import glob
|
||||
from typing import cast
|
||||
import os
|
||||
|
||||
TestToolDir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "Tools")
|
||||
TestInvalidDir = os.path.join(
|
||||
TestToolDir, "some", "silly", "path", "that", "should", "not", "exist"
|
||||
)
|
||||
|
||||
TestToolBitName = "test-path-tool-bit-bit-00.fctb"
|
||||
TestToolShapeName = "test-path-tool-bit-shape-00.fcstd"
|
||||
TestToolLibraryName = "test-path-tool-bit-library-00.fctl"
|
||||
import uuid
|
||||
import pathlib
|
||||
import FreeCAD
|
||||
from CAMTests.PathTestUtils import PathTestWithAssets
|
||||
from Path.Tool.library import Library
|
||||
from Path.Tool.shape import ToolBitShapeBullnose
|
||||
from Path.Tool.toolbit import ToolBitEndmill, ToolBitBullnose
|
||||
|
||||
|
||||
def testToolShape(path=TestToolDir, name=TestToolShapeName):
|
||||
return os.path.join(path, "Shape", name)
|
||||
TOOL_DIR = pathlib.Path(os.path.realpath(__file__)).parent.parent / "Tools"
|
||||
SHAPE_DIR = TOOL_DIR / "Shape"
|
||||
BIT_DIR = TOOL_DIR / "Bit"
|
||||
|
||||
|
||||
def testToolBit(path=TestToolDir, name=TestToolBitName):
|
||||
return os.path.join(path, "Bit", name)
|
||||
|
||||
|
||||
def testToolLibrary(path=TestToolDir, name=TestToolLibraryName):
|
||||
return os.path.join(path, "Library", name)
|
||||
|
||||
|
||||
def printTree(path, indent):
|
||||
print("{} {}".format(indent, os.path.basename(path)))
|
||||
if os.path.isdir(path):
|
||||
if os.path.basename(path).startswith("__"):
|
||||
print("{} ...".format(indent))
|
||||
else:
|
||||
for foo in sorted(glob.glob(os.path.join(path, "*"))):
|
||||
printTree(foo, "{} ".format(indent))
|
||||
|
||||
|
||||
class TestPathToolBit(PathTestUtils.PathTestBase):
|
||||
def test(self):
|
||||
"""Log test setup directory structure"""
|
||||
# Enable this test if there are errors showing up in the build system with the
|
||||
# paths that work OK locally. It'll print out the directory tree, and if it
|
||||
# doesn't look right you know where to look for it
|
||||
print()
|
||||
print("realpath : {}".format(os.path.realpath(__file__)))
|
||||
print(" Tools : {}".format(TestToolDir))
|
||||
print(" dir : {}".format(os.path.dirname(os.path.realpath(__file__))))
|
||||
printTree(os.path.dirname(os.path.realpath(__file__)), " :")
|
||||
|
||||
def test00(self):
|
||||
"""Find a tool shape from file name"""
|
||||
path = PathToolBit.findToolShape("endmill.fcstd")
|
||||
self.assertIsNot(path, None)
|
||||
self.assertNotEqual(path, "endmill.fcstd")
|
||||
|
||||
def test01(self):
|
||||
"""Not find a relative path shape if not stored in default location"""
|
||||
path = PathToolBit.findToolShape(TestToolShapeName)
|
||||
self.assertIsNone(path)
|
||||
|
||||
def test02(self):
|
||||
"""Find a relative path shape if it's local to a bit path"""
|
||||
path = PathToolBit.findToolShape(TestToolShapeName, testToolBit())
|
||||
self.assertIsNot(path, None)
|
||||
self.assertEqual(path, testToolShape())
|
||||
|
||||
def test03(self):
|
||||
"""Not find a tool shape from an invalid absolute path."""
|
||||
path = PathToolBit.findToolShape(testToolShape(TestInvalidDir))
|
||||
self.assertIsNone(path)
|
||||
|
||||
def test04(self):
|
||||
"""Find a tool shape from a valid absolute path."""
|
||||
path = PathToolBit.findToolShape(testToolShape())
|
||||
self.assertIsNot(path, None)
|
||||
self.assertEqual(path, testToolShape())
|
||||
|
||||
def test10(self):
|
||||
class TestPathToolBit(PathTestWithAssets):
|
||||
def testGetToolBit(self):
|
||||
"""Find a tool bit from file name"""
|
||||
path = PathToolBit.findToolBit("5mm_Endmill.fctb")
|
||||
self.assertIsNot(path, None)
|
||||
self.assertNotEqual(path, "5mm_Endmill.fctb")
|
||||
toolbit = self.assets.get("toolbit://5mm_Endmill")
|
||||
self.assertIsInstance(toolbit, ToolBitEndmill)
|
||||
self.assertEqual(toolbit.id, "5mm_Endmill")
|
||||
|
||||
def test11(self):
|
||||
"""Not find a relative path bit if not stored in default location"""
|
||||
path = PathToolBit.findToolBit(TestToolBitName)
|
||||
self.assertIsNone(path)
|
||||
|
||||
def test12(self):
|
||||
"""Find a relative path bit if it's local to a library path"""
|
||||
path = PathToolBit.findToolBit(TestToolBitName, testToolLibrary())
|
||||
self.assertIsNot(path, None)
|
||||
self.assertEqual(path, testToolBit())
|
||||
|
||||
def test13(self):
|
||||
"""Not find a tool bit from an invalid absolute path."""
|
||||
path = PathToolBit.findToolBit(testToolBit(TestInvalidDir))
|
||||
self.assertIsNone(path)
|
||||
|
||||
def test14(self):
|
||||
"""Find a tool bit from a valid absolute path."""
|
||||
path = PathToolBit.findToolBit(testToolBit())
|
||||
self.assertIsNot(path, None)
|
||||
self.assertEqual(path, testToolBit())
|
||||
|
||||
def test20(self):
|
||||
def testGetLibrary(self):
|
||||
"""Find a tool library from file name"""
|
||||
path = PathToolBit.findToolLibrary("Default.fctl")
|
||||
self.assertIsNot(path, None)
|
||||
self.assertNotEqual(path, "Default.fctl")
|
||||
library = self.assets.get("toolbitlibrary://Default")
|
||||
self.assertIsInstance(library, Library)
|
||||
self.assertEqual(library.id, "Default")
|
||||
|
||||
def test21(self):
|
||||
"""Not find a relative path library if not stored in default location"""
|
||||
path = PathToolBit.findToolLibrary(TestToolLibraryName)
|
||||
self.assertIsNone(path)
|
||||
def testBullnose(self):
|
||||
"""Test ToolBitBullnose basic parameters"""
|
||||
shape = self.assets.get("toolbitshape://bullnose")
|
||||
shape = cast(ToolBitShapeBullnose, shape)
|
||||
|
||||
def test22(self):
|
||||
"""[skipped] Find a relative path library if it's local to <what?>"""
|
||||
# this is not a valid test for libraries because t
|
||||
self.assertTrue(True)
|
||||
bullnose_bit = ToolBitBullnose(shape, id="mybullnose")
|
||||
self.assertEqual(bullnose_bit.get_id(), "mybullnose")
|
||||
|
||||
def test23(self):
|
||||
"""Not find a tool library from an invalid absolute path."""
|
||||
path = PathToolBit.findToolLibrary(testToolLibrary(TestInvalidDir))
|
||||
self.assertIsNone(path)
|
||||
bullnose_bit = ToolBitBullnose(shape)
|
||||
uuid.UUID(bullnose_bit.get_id()) # will raise if not valid UUID
|
||||
|
||||
def test24(self):
|
||||
"""Find a tool library from a valid absolute path."""
|
||||
path = PathToolBit.findToolBit(testToolBit())
|
||||
self.assertIsNot(path, None)
|
||||
self.assertEqual(path, testToolBit())
|
||||
# Parameters should be loaded from the shape file and set on the tool bit's object
|
||||
self.assertEqual(bullnose_bit.obj.Diameter, FreeCAD.Units.Quantity("5.0 mm"))
|
||||
self.assertEqual(bullnose_bit.obj.FlatRadius, FreeCAD.Units.Quantity("1.5 mm"))
|
||||
|
||||
def testToolBitPickle(self):
|
||||
"""Test if ToolBit is picklable"""
|
||||
import pickle
|
||||
|
||||
shape = self.assets.get("toolbitshape://bullnose")
|
||||
shape = cast(ToolBitShapeBullnose, shape)
|
||||
bullnose_bit = ToolBitBullnose(shape, id="mybullnose")
|
||||
try:
|
||||
pickled_bit = pickle.dumps(bullnose_bit)
|
||||
unpickled_bit = pickle.loads(pickled_bit)
|
||||
self.assertIsInstance(unpickled_bit, ToolBitBullnose)
|
||||
self.assertEqual(unpickled_bit.get_id(), "mybullnose")
|
||||
# Add more assertions here to check if other attributes are preserved
|
||||
except Exception as e:
|
||||
self.fail(f"ToolBit is not picklable: {e}")
|
||||
|
||||
151
src/Mod/CAM/CAMTests/TestPathToolBitBrowserWidget.py
Normal file
151
src/Mod/CAM/CAMTests/TestPathToolBitBrowserWidget.py
Normal file
@@ -0,0 +1,151 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2024 FreeCAD Team *
|
||||
# * *
|
||||
# * This program is free software; you can redistribute it and/or modify *
|
||||
# * it under the terms of the GNU Lesser General Public License (LGPL) *
|
||||
# * as published by the Free Software Foundation; either version 2 of *
|
||||
# * the License, or (at your option) any later version. *
|
||||
# * for detail see the LICENSE text file. *
|
||||
# * *
|
||||
# * This program is distributed in the hope that it will be useful, *
|
||||
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
# * GNU Library General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Library General Public *
|
||||
# * License along with this program; if not, write to the FreeCAD *
|
||||
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
|
||||
# * USA *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
"""Unit tests for the ToolBitBrowserWidget."""
|
||||
|
||||
import unittest
|
||||
from unittest.mock import MagicMock
|
||||
from typing import cast
|
||||
from Path.Tool.toolbit.ui.browser import ToolBitBrowserWidget, ToolBitUriRole
|
||||
from Path.Tool.toolbit.ui.tablecell import TwoLineTableCell
|
||||
from Path.Tool.toolbit.models.base import ToolBit
|
||||
from .PathTestUtils import PathTestWithAssets
|
||||
|
||||
|
||||
class TestToolBitBrowserWidget(PathTestWithAssets):
|
||||
"""Tests for ToolBitBrowserWidget using real assets and widgets."""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp() # Call the base class setUp to initialize assets
|
||||
# The browser widget uses the global cam_assets, which is set up
|
||||
# by PathTestWithAssets.
|
||||
self.widget = ToolBitBrowserWidget(self.assets)
|
||||
|
||||
def test_initial_fetch(self):
|
||||
# Verify that the list widget is populated after initialization
|
||||
# The default test assets include some toolbits.
|
||||
self.assertGreater(self.widget._tool_list_widget.count(), 0)
|
||||
|
||||
# Verify apply_filter was called on the list widget with empty string
|
||||
# We can check the search_highlight property on the cell widgets
|
||||
# as apply_filter sets this.
|
||||
for i in range(self.widget._tool_list_widget.count()):
|
||||
item = self.widget._tool_list_widget.item(i)
|
||||
cell = self.widget._tool_list_widget.itemWidget(item)
|
||||
self.assertIsInstance(cell, TwoLineTableCell)
|
||||
self.assertEqual(cell.search_highlight, "")
|
||||
|
||||
def test_search_filtering(self):
|
||||
# Simulate typing in the search box
|
||||
search_term = "Endmill"
|
||||
self.widget._search_edit.setText(search_term)
|
||||
|
||||
# Directly trigger the fetch and filtering logic
|
||||
self.widget._trigger_fetch()
|
||||
|
||||
# Verify that the filter was applied to the list widget
|
||||
# We can check if items are hidden/shown based on the filter term
|
||||
# This requires knowing the content of the test assets.
|
||||
# Assuming '5mm_Endmill' and '10mm_Endmill' contain "Endmill"
|
||||
# and 'BallEndmill_3mm' does not.
|
||||
|
||||
# Re-fetch assets to know their labels/names for verification
|
||||
all_assets = self.assets.fetch(asset_type="toolbit", depth=0)
|
||||
expected_visible_uris = set()
|
||||
for asset in all_assets:
|
||||
tb = cast(ToolBit, asset)
|
||||
is_expected = (
|
||||
search_term.lower() in tb.label.lower() or search_term.lower() in tb.summary.lower()
|
||||
)
|
||||
if is_expected:
|
||||
expected_visible_uris.add(str(tb.get_uri()))
|
||||
|
||||
actual_visible_uris = set()
|
||||
for i in range(self.widget._tool_list_widget.count()):
|
||||
item = self.widget._tool_list_widget.item(i)
|
||||
cell = self.widget._tool_list_widget.itemWidget(item)
|
||||
self.assertIsInstance(cell, TwoLineTableCell)
|
||||
item_uri = item.data(ToolBitUriRole)
|
||||
|
||||
# Verify highlight was called on all cells
|
||||
self.assertEqual(cell.search_highlight, search_term)
|
||||
|
||||
if not item.isHidden():
|
||||
actual_visible_uris.add(item_uri)
|
||||
|
||||
self.assertEqual(actual_visible_uris, expected_visible_uris)
|
||||
|
||||
def test_lazy_loading_on_scroll(self):
|
||||
# This test requires more than self._batch_size toolbits to be effective.
|
||||
# The default test assets might not have enough.
|
||||
# We'll assume there are enough for the test structure.
|
||||
|
||||
initial_count = self.widget._tool_list_widget.count()
|
||||
if initial_count < self.widget._batch_size:
|
||||
self.skipTest("Not enough toolbits for lazy loading test.")
|
||||
|
||||
# Simulate scrolling to the bottom by emitting the signal
|
||||
scrollbar = self.widget._tool_list_widget.verticalScrollBar()
|
||||
# Set the scrollbar value to its maximum to simulate reaching the end
|
||||
scrollbar.valueChanged.emit(scrollbar.maximum())
|
||||
|
||||
# Verify that more items were loaded
|
||||
new_count = self.widget._tool_list_widget.count()
|
||||
self.assertGreater(new_count, initial_count)
|
||||
# Verify that the number of new items is approximately the batch size
|
||||
self.assertAlmostEqual(
|
||||
new_count - initial_count, self.widget._batch_size, delta=5
|
||||
) # Allow small delta
|
||||
|
||||
def test_tool_selected_signal(self):
|
||||
mock_slot = MagicMock()
|
||||
self.widget.toolSelected.connect(mock_slot)
|
||||
|
||||
# Select the first item in the list widget
|
||||
if self.widget._tool_list_widget.count() == 0:
|
||||
self.skipTest("Not enough toolbits for selection test.")
|
||||
first_item = self.widget._tool_list_widget.item(0)
|
||||
self.widget._tool_list_widget.setCurrentItem(first_item)
|
||||
|
||||
# Verify signal was emitted with the correct URI
|
||||
expected_uri = first_item.data(ToolBitUriRole)
|
||||
mock_slot.assert_called_once_with(expected_uri)
|
||||
|
||||
def test_tool_edit_requested_signal(self):
|
||||
mock_slot = MagicMock()
|
||||
self.widget.itemDoubleClicked.connect(mock_slot)
|
||||
|
||||
# Double-click the first item in the list widget
|
||||
if self.widget._tool_list_widget.count() == 0:
|
||||
self.skipTest("Not enough toolbits for double-click test.")
|
||||
|
||||
first_item = self.widget._tool_list_widget.item(0)
|
||||
# Simulate double-click signal emission from the list widget
|
||||
self.widget._tool_list_widget.itemDoubleClicked.emit(first_item)
|
||||
|
||||
# Verify signal was emitted with the correct URI
|
||||
expected_uri = first_item.data(ToolBitUriRole)
|
||||
mock_slot.assert_called_once_with(expected_uri)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
124
src/Mod/CAM/CAMTests/TestPathToolBitEditorWidget.py
Normal file
124
src/Mod/CAM/CAMTests/TestPathToolBitEditorWidget.py
Normal file
@@ -0,0 +1,124 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2024 FreeCAD Team *
|
||||
# * *
|
||||
# * This program is free software; you can redistribute it and/or modify *
|
||||
# * it under the terms of the GNU Lesser General Public License (LGPL) *
|
||||
# * as published by the Free Software Foundation; either version 2 of *
|
||||
# * the License, or (at your option) any later version. *
|
||||
# * for detail see the LICENSE text file. *
|
||||
# * *
|
||||
# * This program is distributed in the hope that it will be useful, *
|
||||
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
# * GNU Library General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Library General Public *
|
||||
# * License along with this program; if not, write to the FreeCAD *
|
||||
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
|
||||
# * USA *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
"""Unit tests for the ToolBitEditorWidget."""
|
||||
|
||||
import unittest
|
||||
from unittest.mock import MagicMock
|
||||
from Path.Tool.toolbit.ui.editor import ToolBitPropertiesWidget
|
||||
from Path.Tool.toolbit.models.base import ToolBit
|
||||
from Path.Tool.shape.ui.shapewidget import ShapeWidget
|
||||
from Path.Tool.ui.property import BasePropertyEditorWidget
|
||||
from .PathTestUtils import PathTestWithAssets
|
||||
|
||||
|
||||
class TestToolBitPropertiesWidget(PathTestWithAssets):
|
||||
"""Tests for ToolBitEditorWidget using real assets and widgets."""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp() # Call the base class setUp to initialize assets
|
||||
self.widget = ToolBitPropertiesWidget()
|
||||
|
||||
def test_load_toolbit(self):
|
||||
# Get a real ToolBit asset
|
||||
toolbit = self.assets.get("toolbit://5mm_Endmill")
|
||||
self.assertIsInstance(toolbit, ToolBit)
|
||||
|
||||
self.widget.load_toolbit(toolbit)
|
||||
|
||||
# Verify label and ID are populated
|
||||
self.assertEqual(self.widget._label_edit.text(), toolbit.obj.Label)
|
||||
self.assertEqual(self.widget._id_label.text(), toolbit.get_id())
|
||||
|
||||
# Verify DocumentObjectEditorWidget is populated
|
||||
self.assertEqual(self.widget._property_editor._obj, toolbit.obj)
|
||||
# Check if properties were passed to the property editor
|
||||
self.assertGreater(len(self.widget._property_editor._properties_to_show), 0)
|
||||
|
||||
# Verify ShapeWidget is created and populated
|
||||
self.assertIsNotNone(self.widget._shape_widget)
|
||||
self.assertIsInstance(self.widget._shape_widget, ShapeWidget)
|
||||
# We can't easily check the internal shape of ShapeWidget without mocks,
|
||||
# but we can verify it was created.
|
||||
|
||||
def test_label_changed_updates_object(self):
|
||||
toolbit = self.assets.get("toolbit://5mm_Endmill")
|
||||
self.widget.load_toolbit(toolbit)
|
||||
|
||||
new_label = "Updated Endmill"
|
||||
self.widget._label_edit.setText(new_label)
|
||||
# Simulate editing finished signal
|
||||
self.widget._label_edit.editingFinished.emit()
|
||||
|
||||
# Verify the toolbit object's label is updated
|
||||
self.assertEqual(toolbit.obj.Label, new_label)
|
||||
|
||||
def test_property_changed_signal_emitted(self):
|
||||
toolbit = self.assets.get("toolbit://5mm_Endmill")
|
||||
self.widget.load_toolbit(toolbit)
|
||||
|
||||
mock_slot = MagicMock()
|
||||
self.widget.toolBitChanged.connect(mock_slot)
|
||||
|
||||
# Simulate a property change in the DocumentObjectEditorWidget
|
||||
# We need to trigger the signal from the real property editor.
|
||||
# This requires accessing a child editor and emitting its signal.
|
||||
# Find a child editor (e.g., the first one)
|
||||
if self.widget._property_editor._property_editors:
|
||||
first_prop_name = list(self.widget._property_editor._property_editors.keys())[0]
|
||||
child_editor = self.widget._property_editor._property_editors[first_prop_name]
|
||||
self.assertIsInstance(child_editor, BasePropertyEditorWidget)
|
||||
|
||||
# Emit the propertyChanged signal from the child editor
|
||||
child_editor.propertyChanged.emit()
|
||||
|
||||
# Verify the ToolBitEditorWidget's signal was emitted
|
||||
mock_slot.assert_called_once()
|
||||
else:
|
||||
self.skipTest("DocumentObjectEditorWidget has no property editors.")
|
||||
|
||||
def test_save_toolbit(self):
|
||||
# The save_toolbit method primarily ensures the label is updated
|
||||
# and potentially calls updateObject on the property editor.
|
||||
# Since property changes are signal-driven, this method is more
|
||||
# for explicit save actions.
|
||||
|
||||
toolbit = self.assets.get("toolbit://5mm_Endmill")
|
||||
self.widget.load_toolbit(toolbit)
|
||||
|
||||
initial_label = toolbit.obj.Label
|
||||
new_label = "Another Label"
|
||||
self.widget._label_edit.setText(new_label)
|
||||
|
||||
# Call save_toolbit
|
||||
self.widget.save_toolbit()
|
||||
|
||||
# Verify the label was updated
|
||||
self.assertEqual(toolbit.obj.Label, new_label)
|
||||
|
||||
# We can't easily verify if updateObject was called on the property
|
||||
# editor without mocks, but we trust the implementation based on
|
||||
# the DocumentObjectEditorWidget's own tests.
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
143
src/Mod/CAM/CAMTests/TestPathToolBitListWidget.py
Normal file
143
src/Mod/CAM/CAMTests/TestPathToolBitListWidget.py
Normal file
@@ -0,0 +1,143 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2024 FreeCAD Team *
|
||||
# * *
|
||||
# * This program is free software; you can redistribute it and/or modify *
|
||||
# * it under the terms of the GNU Lesser General Public License (LGPL) *
|
||||
# * as published by the Free Software Foundation; either version 2 of *
|
||||
# * the License, or (at your option) any later version. *
|
||||
# * for detail see the LICENSE text file. *
|
||||
# * *
|
||||
# * This program is distributed in the hope that it will be useful, *
|
||||
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
# * GNU Library General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Library General Public *
|
||||
# * License along with this program; if not, write to the Free Software *
|
||||
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
|
||||
# * USA *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
"""Unit tests for the ToolBitListWidget."""
|
||||
|
||||
import unittest
|
||||
from Path.Tool.toolbit.ui.toollist import ToolBitListWidget, ToolBitUriRole
|
||||
from Path.Tool.toolbit.ui.tablecell import TwoLineTableCell
|
||||
from .PathTestUtils import PathTestWithAssets # Import the base test class
|
||||
|
||||
|
||||
class TestToolBitListWidget(PathTestWithAssets):
|
||||
"""Tests for ToolBitListWidget using real assets."""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp() # Call the base class setUp to initialize assets
|
||||
self.widget = ToolBitListWidget()
|
||||
|
||||
def test_add_toolbit(self):
|
||||
# Get a real ToolBit asset
|
||||
toolbit = self.assets.get("toolbit://5mm_Endmill")
|
||||
tool_no = 1
|
||||
|
||||
self.widget.add_toolbit(toolbit, str(tool_no))
|
||||
|
||||
self.assertEqual(self.widget.count(), 1)
|
||||
item = self.widget.item(0)
|
||||
self.assertIsNotNone(item)
|
||||
|
||||
cell_widget = self.widget.itemWidget(item)
|
||||
self.assertIsInstance(cell_widget, TwoLineTableCell) # Check against real class
|
||||
|
||||
# Verify cell widget properties are set correctly
|
||||
self.assertEqual(cell_widget.tool_no, str(tool_no))
|
||||
self.assertEqual(cell_widget.upper_text, toolbit.label)
|
||||
# Assuming the 5mm_Endmill asset has a shape named 'Endmill'
|
||||
self.assertEqual(cell_widget.lower_text, "5.00 mm 4-flute endmill, 30.00 mm cutting edge")
|
||||
|
||||
# Verify URI is stored in item data
|
||||
stored_uri = item.data(ToolBitUriRole)
|
||||
self.assertEqual(stored_uri, str(toolbit.get_uri()))
|
||||
|
||||
def test_clear_list(self):
|
||||
# Add some real items first
|
||||
toolbit1 = self.assets.get("toolbit://5mm_Endmill")
|
||||
toolbit2 = self.assets.get("toolbit://slittingsaw")
|
||||
self.widget.add_toolbit(toolbit1, 1)
|
||||
self.widget.add_toolbit(toolbit2, 2)
|
||||
self.assertEqual(self.widget.count(), 2)
|
||||
|
||||
self.widget.clear_list()
|
||||
self.assertEqual(self.widget.count(), 0)
|
||||
|
||||
def test_apply_filter(self):
|
||||
# Add items with distinct text for filtering
|
||||
toolbit1 = self.assets.get("toolbit://5mm_Endmill")
|
||||
toolbit2 = self.assets.get("toolbit://slittingsaw")
|
||||
toolbit3 = self.assets.get("toolbit://probe")
|
||||
|
||||
self.widget.add_toolbit(toolbit1, 1)
|
||||
self.widget.add_toolbit(toolbit2, 2)
|
||||
self.widget.add_toolbit(toolbit3, 3)
|
||||
|
||||
items = [self.widget.item(i) for i in range(self.widget.count())]
|
||||
cells = [self.widget.itemWidget(item) for item in items]
|
||||
|
||||
# Test filter "Endmill"
|
||||
self.widget.apply_filter("Endmill")
|
||||
|
||||
self.assertFalse(items[0].isHidden()) # 5mm Endmill
|
||||
self.assertTrue(items[1].isHidden()) # slittingsaw
|
||||
self.assertTrue(items[2].isHidden()) # probe
|
||||
|
||||
# Verify highlight was called on all cells
|
||||
for cell in cells:
|
||||
self.assertEqual(cell.search_highlight, "Endmill")
|
||||
|
||||
# Test filter "Ballnose"
|
||||
self.widget.apply_filter("Ballnose")
|
||||
|
||||
self.assertTrue(items[0].isHidden()) # 5mm Endmill
|
||||
self.assertTrue(items[1].isHidden()) # slittingsaw
|
||||
self.assertTrue(items[2].isHidden()) # probe
|
||||
|
||||
# Verify highlight was called again
|
||||
for cell in cells:
|
||||
self.assertEqual(cell.search_highlight, "Ballnose")
|
||||
|
||||
# Test filter "3mm"
|
||||
self.widget.apply_filter("3mm")
|
||||
|
||||
self.assertTrue(items[0].isHidden()) # 5mm Endmill
|
||||
self.assertTrue(items[1].isHidden()) # slittingsaw
|
||||
self.assertTrue(items[2].isHidden()) # probe
|
||||
|
||||
# Verify highlight was called again
|
||||
for cell in cells:
|
||||
self.assertEqual(cell.search_highlight, "3mm")
|
||||
|
||||
def test_get_selected_toolbit_uri(self):
|
||||
toolbit1 = self.assets.get("toolbit://5mm_Endmill")
|
||||
toolbit2 = self.assets.get("toolbit://slittingsaw")
|
||||
|
||||
self.widget.add_toolbit(toolbit1, 1)
|
||||
self.widget.add_toolbit(toolbit2, 2)
|
||||
|
||||
# No selection initially
|
||||
self.assertIsNone(self.widget.get_selected_toolbit_uri())
|
||||
|
||||
# Select the first item
|
||||
self.widget.setCurrentItem(self.widget.item(0))
|
||||
self.assertEqual(self.widget.get_selected_toolbit_uri(), str(toolbit1.get_uri()))
|
||||
|
||||
# Select the second item
|
||||
self.widget.setCurrentItem(self.widget.item(1))
|
||||
self.assertEqual(self.widget.get_selected_toolbit_uri(), str(toolbit2.get_uri()))
|
||||
|
||||
# Clear selection (simulate by setting current item to None)
|
||||
self.widget.setCurrentItem(None)
|
||||
self.assertIsNone(self.widget.get_selected_toolbit_uri())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
241
src/Mod/CAM/CAMTests/TestPathToolBitPropertyEditorWidget.py
Normal file
241
src/Mod/CAM/CAMTests/TestPathToolBitPropertyEditorWidget.py
Normal file
@@ -0,0 +1,241 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2024 FreeCAD Team *
|
||||
# * *
|
||||
# * This program is free software; you can redistribute it and/or modify *
|
||||
# * it under the terms of the GNU Lesser General Public License (LGPL) *
|
||||
# * as published by the Free Software Foundation; either version 2 of *
|
||||
# * the License, or (at your option) any later version. *
|
||||
# * for detail see the LICENSE text file. *
|
||||
# * *
|
||||
# * This program is distributed in the hope that it will be useful, *
|
||||
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
# * GNU Library General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Library General Public *
|
||||
# * License along with this program; if not, write to the Free Software *
|
||||
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
|
||||
# * USA *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
"""Unit tests for the Property Editor Widgets."""
|
||||
|
||||
import unittest
|
||||
import FreeCAD
|
||||
from Path.Tool.ui.property import (
|
||||
BasePropertyEditorWidget,
|
||||
QuantityPropertyEditorWidget,
|
||||
BoolPropertyEditorWidget,
|
||||
IntPropertyEditorWidget,
|
||||
EnumPropertyEditorWidget,
|
||||
LabelPropertyEditorWidget,
|
||||
)
|
||||
from Path.Tool.toolbit.docobject import DetachedDocumentObject
|
||||
|
||||
|
||||
class TestPropertyEditorFactory(unittest.TestCase):
|
||||
"""Tests the BasePropertyEditorWidget.for_property factory method."""
|
||||
|
||||
def setUp(self):
|
||||
# Use the real DetachedDocumentObject
|
||||
self.obj = DetachedDocumentObject()
|
||||
# Add properties using the DetachedDocumentObject API with correct signature
|
||||
self.obj.addProperty("App::PropertyLength", "Length", "Base", "Length property")
|
||||
self.obj.Length = FreeCAD.Units.Quantity(10.0) # Set value separately
|
||||
|
||||
self.obj.addProperty("App::PropertyBool", "IsEnabled", "Base", "Boolean property")
|
||||
self.obj.IsEnabled = True # Set value separately
|
||||
|
||||
self.obj.addProperty("App::PropertyInt", "Count", "Base", "Integer property")
|
||||
self.obj.Count = 5 # Set value separately
|
||||
|
||||
self.obj.addProperty("App::PropertyEnumeration", "Mode", "Base", "Enumeration property")
|
||||
# Set enums and initial value separately
|
||||
self.obj.Mode = ["Auto", "Manual"]
|
||||
|
||||
self.obj.addProperty("App::PropertyString", "Comment", "Base", "String property")
|
||||
self.obj.Comment = "Test" # Set value separately
|
||||
|
||||
def test_quantity_creation(self):
|
||||
widget = BasePropertyEditorWidget.for_property(self.obj, "Length")
|
||||
self.assertIsInstance(widget, QuantityPropertyEditorWidget)
|
||||
|
||||
def test_bool_creation(self):
|
||||
widget = BasePropertyEditorWidget.for_property(self.obj, "IsEnabled")
|
||||
self.assertIsInstance(widget, BoolPropertyEditorWidget)
|
||||
|
||||
def test_int_creation(self):
|
||||
widget = BasePropertyEditorWidget.for_property(self.obj, "Count")
|
||||
self.assertIsInstance(widget, IntPropertyEditorWidget)
|
||||
|
||||
def test_enum_creation(self):
|
||||
widget = BasePropertyEditorWidget.for_property(self.obj, "Mode")
|
||||
self.assertIsInstance(widget, EnumPropertyEditorWidget)
|
||||
|
||||
def test_label_creation_for_string(self):
|
||||
widget = BasePropertyEditorWidget.for_property(self.obj, "Comment")
|
||||
self.assertIsInstance(widget, LabelPropertyEditorWidget)
|
||||
|
||||
def test_label_creation_for_invalid_prop(self):
|
||||
widget = BasePropertyEditorWidget.for_property(self.obj, "NonExistent")
|
||||
self.assertIsInstance(widget, LabelPropertyEditorWidget)
|
||||
|
||||
|
||||
class TestQuantityPropertyEditorWidget(unittest.TestCase):
|
||||
"""Tests for QuantityPropertyEditorWidget."""
|
||||
|
||||
def setUp(self):
|
||||
self.obj = DetachedDocumentObject()
|
||||
self.obj.addProperty("App::PropertyLength", "Length", "Base", "Length property")
|
||||
self.obj.Length = FreeCAD.Units.Quantity("10.0 mm")
|
||||
self.widget = QuantityPropertyEditorWidget(self.obj, "Length")
|
||||
# Access the real editor widget
|
||||
self.editor = self.widget._editor_widget
|
||||
self.widget.updateWidget()
|
||||
|
||||
def test_update_property(self):
|
||||
# Check if the real widget's value is updated
|
||||
self.assertEqual(self.editor.property("rawValue"), 10.0)
|
||||
# Check if the real widget's value and unit are updated
|
||||
self.assertEqual(self.editor.property("value").UserString, "10.00 mm")
|
||||
|
||||
# Simulate changing the raw value and check if the object's value updates
|
||||
self.editor.lineEdit().setText("12.0")
|
||||
self.widget.updateProperty()
|
||||
self.assertEqual(self.obj.Length.Value, 12.0)
|
||||
self.assertEqual(self.obj.Length.UserString, "12.00 mm")
|
||||
|
||||
# Try assignment with unit.
|
||||
self.editor.lineEdit().setText("15.5 in")
|
||||
self.widget.updateProperty()
|
||||
updated_value = self.obj.getPropertyByName("Length")
|
||||
self.assertIsInstance(updated_value, FreeCAD.Units.Quantity)
|
||||
self.assertEqual(updated_value, FreeCAD.Units.Quantity("15.5 in"))
|
||||
|
||||
|
||||
class TestBoolPropertyEditorWidget(unittest.TestCase):
|
||||
"""Tests for BoolPropertyEditorWidget."""
|
||||
|
||||
def setUp(self):
|
||||
self.obj = DetachedDocumentObject()
|
||||
self.obj.addProperty("App::PropertyBool", "IsEnabled", "Base", "Boolean property")
|
||||
self.obj.IsEnabled = True
|
||||
self.widget = BoolPropertyEditorWidget(self.obj, "IsEnabled")
|
||||
# Access the real editor widget
|
||||
self.editor = self.widget._editor_widget
|
||||
|
||||
def test_update_widget(self):
|
||||
self.widget.updateWidget()
|
||||
# Check if the real widget's value is updated
|
||||
self.assertEqual(self.editor.currentIndex(), 1) # True is index 1
|
||||
|
||||
self.obj.setPropertyByName("IsEnabled", False)
|
||||
self.widget.updateWidget()
|
||||
self.assertEqual(self.editor.currentIndex(), 0) # False is index 0
|
||||
|
||||
def test_update_property(self):
|
||||
# Simulate user changing value in the combobox
|
||||
self.editor.setCurrentIndex(0) # Select False
|
||||
self.widget._on_index_changed(0)
|
||||
self.assertEqual(self.obj.getPropertyByName("IsEnabled"), False)
|
||||
|
||||
self.editor.setCurrentIndex(1) # Select True
|
||||
self.widget._on_index_changed(1)
|
||||
self.assertEqual(self.obj.getPropertyByName("IsEnabled"), True)
|
||||
|
||||
|
||||
class TestIntPropertyEditorWidget(unittest.TestCase):
|
||||
"""Tests for IntPropertyEditorWidget."""
|
||||
|
||||
def setUp(self):
|
||||
self.obj = DetachedDocumentObject()
|
||||
self.obj.addProperty("App::PropertyInt", "Count", "Base", "Integer property")
|
||||
self.obj.Count = 5
|
||||
self.widget = IntPropertyEditorWidget(self.obj, "Count")
|
||||
# Access the real editor widget
|
||||
self.editor = self.widget._editor_widget
|
||||
|
||||
def test_update_widget(self):
|
||||
self.widget.updateWidget()
|
||||
# Check if the real widget's value is updated
|
||||
self.assertEqual(self.editor.value(), 5)
|
||||
|
||||
self.obj.setPropertyByName("Count", 100)
|
||||
self.widget.updateWidget()
|
||||
self.assertEqual(self.editor.value(), 100)
|
||||
|
||||
def test_update_property(self):
|
||||
# Simulate user changing value in the spinbox
|
||||
self.editor.setValue(42)
|
||||
self.widget.updateProperty()
|
||||
self.assertEqual(self.obj.getPropertyByName("Count"), 42)
|
||||
|
||||
|
||||
class TestEnumPropertyEditorWidget(unittest.TestCase):
|
||||
"""Tests for EnumPropertyEditorWidget."""
|
||||
|
||||
def setUp(self):
|
||||
self.obj = DetachedDocumentObject()
|
||||
self.obj.addProperty("App::PropertyEnumeration", "Mode", "Base", "Enumeration property")
|
||||
self.obj.Mode = ["Auto", "Manual", "Semi"] # Set enums and initial value
|
||||
self.widget = EnumPropertyEditorWidget(self.obj, "Mode")
|
||||
# Access the real editor widget
|
||||
self.editor = self.widget._editor_widget
|
||||
|
||||
def test_populate_enum(self):
|
||||
# Check if the real widget is populated
|
||||
self.assertEqual(self.editor.count(), 3)
|
||||
self.assertEqual(self.editor.itemText(0), "Auto")
|
||||
self.assertEqual(self.editor.itemText(1), "Manual")
|
||||
self.assertEqual(self.editor.itemText(2), "Semi")
|
||||
|
||||
def test_update_widget(self):
|
||||
self.widget.updateWidget()
|
||||
# Check if the real widget's value is updated
|
||||
self.assertEqual(self.editor.currentText(), "Auto")
|
||||
|
||||
self.obj.setPropertyByName("Mode", "Manual")
|
||||
self.widget.updateWidget()
|
||||
self.assertEqual(self.editor.currentText(), "Manual")
|
||||
|
||||
def test_update_property(self):
|
||||
# Simulate user changing value in the combobox
|
||||
self.editor.setCurrentIndex(1) # Select Manual
|
||||
self.widget._on_index_changed(1)
|
||||
self.assertEqual(self.obj.getPropertyByName("Mode"), "Manual")
|
||||
|
||||
self.editor.setCurrentIndex(2) # Select Semi
|
||||
self.widget._on_index_changed(2)
|
||||
self.assertEqual(self.obj.getPropertyByName("Mode"), "Semi")
|
||||
|
||||
|
||||
class TestLabelPropertyEditorWidget(unittest.TestCase):
|
||||
"""Tests for LabelPropertyEditorWidget."""
|
||||
|
||||
def setUp(self):
|
||||
self.obj = DetachedDocumentObject()
|
||||
self.obj.addProperty("App::PropertyString", "Comment", "Base", "String property")
|
||||
self.obj.Comment = "Test Comment"
|
||||
self.widget = LabelPropertyEditorWidget(self.obj, "Comment")
|
||||
# Access the real editor widget
|
||||
self.editor = self.widget._editor_widget
|
||||
|
||||
def test_update_widget(self):
|
||||
self.widget.updateWidget()
|
||||
# Check if the real widget's value is updated
|
||||
self.assertEqual(self.editor.text(), "Test Comment")
|
||||
|
||||
self.obj.setPropertyByName("Comment", "New Comment")
|
||||
self.widget.updateWidget()
|
||||
self.assertEqual(self.editor.text(), "New Comment")
|
||||
|
||||
def test_update_property_is_noop(self):
|
||||
# updateProperty should do nothing for a read-only label
|
||||
self.widget.updateProperty()
|
||||
# No assertions needed, just ensure it doesn't raise errors
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
134
src/Mod/CAM/CAMTests/TestPathToolBitSerializer.py
Normal file
134
src/Mod/CAM/CAMTests/TestPathToolBitSerializer.py
Normal file
@@ -0,0 +1,134 @@
|
||||
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,
|
||||
)
|
||||
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(str(deserialized_bit.get_diameter()), "4.12 mm")
|
||||
self.assertEqual(str(deserialized_bit.get_length()), "15.0 mm")
|
||||
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(data.get("parameter", {}).get("Diameter"), "4.12 mm")
|
||||
self.assertEqual(data.get("parameter", {}).get("Length"), "15.0 mm", data)
|
||||
|
||||
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(str(deserialized_bit.get_diameter()), "4.12 mm")
|
||||
self.assertEqual(str(deserialized_bit.get_length()), "15.0 mm")
|
||||
@@ -21,10 +21,8 @@
|
||||
# ***************************************************************************
|
||||
|
||||
import FreeCAD
|
||||
import Path
|
||||
import Path.Tool.Bit as PathToolBit
|
||||
from Path.Tool.toolbit import ToolBit
|
||||
import Path.Tool.Controller as PathToolController
|
||||
|
||||
from CAMTests.PathTestUtils import PathTestBase
|
||||
|
||||
|
||||
@@ -39,12 +37,14 @@ class TestPathToolController(PathTestBase):
|
||||
|
||||
def createTool(self, name="t1", diameter=1.75):
|
||||
attrs = {
|
||||
"shape": None,
|
||||
"name": name,
|
||||
"name": name or "t1",
|
||||
"shape": "endmill.fcstd",
|
||||
"parameter": {"Diameter": diameter},
|
||||
"attribute": [],
|
||||
"attribute": {},
|
||||
}
|
||||
return PathToolBit.Factory.CreateFromAttrs(attrs, name)
|
||||
print(f"Debug: attrs['attribute'] is {attrs['attribute']}")
|
||||
toolbit = ToolBit.from_dict(attrs)
|
||||
return toolbit.attach_to_doc(doc=FreeCAD.ActiveDocument)
|
||||
|
||||
def test00(self):
|
||||
"""Verify ToolController templateAttrs"""
|
||||
@@ -72,7 +72,7 @@ class TestPathToolController(PathTestBase):
|
||||
self.assertEqual(attrs["hrapid"], "28.0 mm/s")
|
||||
self.assertEqual(attrs["dir"], "Reverse")
|
||||
self.assertEqual(attrs["speed"], 12000)
|
||||
self.assertEqual(attrs["tool"], t.Proxy.templateAttrs(t))
|
||||
self.assertEqual(attrs["tool"], t.Proxy.to_dict())
|
||||
|
||||
return tc
|
||||
|
||||
|
||||
299
src/Mod/CAM/CAMTests/TestPathToolDocumentObjectEditorWidget.py
Normal file
299
src/Mod/CAM/CAMTests/TestPathToolDocumentObjectEditorWidget.py
Normal file
@@ -0,0 +1,299 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2024 FreeCAD Team *
|
||||
# * *
|
||||
# * This program is free software; you can redistribute it and/or modify *
|
||||
# * it under the terms of the GNU Lesser General Public License (LGPL) *
|
||||
# * as published by the Free Software Foundation; either version 2 of *
|
||||
# * the License, or (at your option) any later version. *
|
||||
# * for detail see the LICENSE text file. *
|
||||
# * *
|
||||
# * This program is distributed in the hope that it will be useful, *
|
||||
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
# * GNU Library General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Library General Public *
|
||||
# * License along with this program; if not, write to the Free Software *
|
||||
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
|
||||
# * USA *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
"""Unit tests for the DocumentObjectEditorWidget."""
|
||||
|
||||
import unittest
|
||||
from unittest.mock import MagicMock
|
||||
import FreeCAD
|
||||
from PySide import QtGui
|
||||
from Path.Tool.ui.property import (
|
||||
BasePropertyEditorWidget,
|
||||
QuantityPropertyEditorWidget,
|
||||
BoolPropertyEditorWidget,
|
||||
IntPropertyEditorWidget,
|
||||
EnumPropertyEditorWidget,
|
||||
LabelPropertyEditorWidget,
|
||||
)
|
||||
from Path.Tool.ui.docobject import DocumentObjectEditorWidget, _get_label_text
|
||||
from Path.Tool.toolbit.docobject import DetachedDocumentObject
|
||||
|
||||
|
||||
class TestDocumentObjectEditorWidget(unittest.TestCase):
|
||||
"""Tests for DocumentObjectEditorWidget."""
|
||||
|
||||
def test_populate_form(self):
|
||||
obj = DetachedDocumentObject()
|
||||
obj.addProperty("App::PropertyString", "Prop1", "Group1", "Doc1")
|
||||
obj.Prop1 = "Value1"
|
||||
obj.addProperty("App::PropertyInt", "Prop2", "Group1", "Doc2")
|
||||
obj.Prop2 = 123
|
||||
obj.addProperty("App::PropertyLength", "Prop3", "Group1", "Doc3")
|
||||
obj.Prop3 = FreeCAD.Units.Quantity(5.0, "mm")
|
||||
obj.addProperty("App::PropertyBool", "Prop4", "Group1", "Doc4")
|
||||
obj.Prop4 = False
|
||||
obj.addProperty("App::PropertyEnumeration", "Prop5", "Group1", "Doc5")
|
||||
obj.Prop5 = ["OptionA", "OptionB"]
|
||||
|
||||
properties_to_show = ["Prop1", "Prop2", "Prop3", "Prop4", "Prop5", "NonExistent"]
|
||||
property_suffixes = {"Prop1": "Suffix1", "Prop3": "Len"}
|
||||
|
||||
widget = DocumentObjectEditorWidget(
|
||||
obj=obj, properties_to_show=properties_to_show, property_suffixes=property_suffixes
|
||||
)
|
||||
|
||||
# Verify the layout contains the correct number of rows (excluding non-existent)
|
||||
expected_row_count = len([p for p in properties_to_show if hasattr(obj, p)])
|
||||
self.assertEqual(widget._layout.rowCount(), expected_row_count)
|
||||
|
||||
# Verify labels and widgets are added correctly and are of the expected types
|
||||
prop_names_in_layout = []
|
||||
for i in range(widget._layout.rowCount()):
|
||||
label_item = widget._layout.itemAt(i, QtGui.QFormLayout.LabelRole)
|
||||
field_item = widget._layout.itemAt(i, QtGui.QFormLayout.FieldRole)
|
||||
|
||||
self.assertIsNotNone(label_item)
|
||||
self.assertIsNotNone(field_item)
|
||||
|
||||
label_widget = label_item.widget()
|
||||
field_widget = field_item.widget()
|
||||
|
||||
self.assertIsInstance(label_widget, QtGui.QLabel)
|
||||
self.assertIsInstance(
|
||||
field_widget, BasePropertyEditorWidget
|
||||
) # Check against base class
|
||||
|
||||
# Determine the property name from the label text (reverse of _get_label_text)
|
||||
# This is a bit fragile, but necessary without storing prop_name in the label widget
|
||||
label_text = label_widget.text()
|
||||
prop_name = None
|
||||
for original_prop_name in properties_to_show:
|
||||
expected_label = _get_label_text(original_prop_name)
|
||||
suffix = property_suffixes.get(original_prop_name)
|
||||
if suffix:
|
||||
expected_label = f"{expected_label} ({suffix}):"
|
||||
else:
|
||||
expected_label = f"{expected_label}:"
|
||||
if label_text == expected_label:
|
||||
prop_name = original_prop_name
|
||||
break
|
||||
|
||||
self.assertIsNotNone(
|
||||
prop_name, f"Could not determine property name for label: {label_text}"
|
||||
)
|
||||
prop_names_in_layout.append(prop_name)
|
||||
|
||||
# Verify widget type based on property type
|
||||
if prop_name == "Prop1":
|
||||
self.assertIsInstance(field_widget, LabelPropertyEditorWidget)
|
||||
self.assertEqual(label_widget.text(), "Prop1 (Suffix1):")
|
||||
elif prop_name == "Prop2":
|
||||
self.assertIsInstance(field_widget, IntPropertyEditorWidget)
|
||||
self.assertEqual(label_widget.text(), "Prop2:")
|
||||
elif prop_name == "Prop3":
|
||||
self.assertIsInstance(field_widget, QuantityPropertyEditorWidget)
|
||||
self.assertEqual(label_widget.text(), "Prop3 (Len):")
|
||||
elif prop_name == "Prop4":
|
||||
self.assertIsInstance(field_widget, BoolPropertyEditorWidget)
|
||||
self.assertEqual(label_widget.text(), "Prop4:")
|
||||
elif prop_name == "Prop5":
|
||||
self.assertIsInstance(field_widget, EnumPropertyEditorWidget)
|
||||
self.assertEqual(label_widget.text(), "Prop5:")
|
||||
|
||||
# Verify property editors are stored
|
||||
self.assertEqual(len(widget._property_editors), expected_row_count)
|
||||
for prop_name in prop_names_in_layout:
|
||||
self.assertIn(prop_name, widget._property_editors)
|
||||
self.assertIsInstance(widget._property_editors[prop_name], BasePropertyEditorWidget)
|
||||
|
||||
def test_set_object(self):
|
||||
obj1 = DetachedDocumentObject()
|
||||
obj1.addProperty("App::PropertyString", "PropA", "GroupA", "DocA")
|
||||
obj1.PropA = "ValueA"
|
||||
|
||||
obj2 = DetachedDocumentObject()
|
||||
obj2.addProperty("App::PropertyString", "PropA", "GroupA", "DocA")
|
||||
obj2.PropA = "ValueB"
|
||||
|
||||
properties_to_show = ["PropA"]
|
||||
widget = DocumentObjectEditorWidget(obj=obj1, properties_to_show=properties_to_show)
|
||||
|
||||
# Get the initial editor widget instance
|
||||
initial_editor = widget._property_editors["PropA"]
|
||||
self.assertIsInstance(initial_editor, BasePropertyEditorWidget)
|
||||
|
||||
# Set a new object
|
||||
widget.setObject(obj2)
|
||||
|
||||
# Verify that the editor widget instance is the same
|
||||
self.assertEqual(widget._property_editors["PropA"], initial_editor)
|
||||
# Verify that attachTo was called on the existing editor widget
|
||||
# This requires the real attachTo method to be implemented correctly
|
||||
# and the editor widget to update its internal object reference.
|
||||
# We can't easily assert the internal state change without mocks,
|
||||
# but we can trust the implementation of attachTo in PropertyEditorWidget.
|
||||
# We can verify updateWidget was called.
|
||||
# Note: This test relies on the side effect of attachTo calling updateWidget
|
||||
# in the real implementation.
|
||||
# We can't directly assert method calls without mocks, so we'll rely on
|
||||
# the fact that setting the object and then updating the UI should
|
||||
# reflect the new object's values if attachTo worked.
|
||||
widget.updateUI()
|
||||
# Check if the child widget's display reflects obj2's value
|
||||
# This requires accessing the child widget's internal editor widget
|
||||
# which might be fragile. A better approach is to trust the unit tests
|
||||
# for the individual PropertyEditorWidgets and focus on the
|
||||
# DocumentObjectEditorWidget's logic of calling attachTo and updateUI.
|
||||
|
||||
def test_set_properties_to_show(self):
|
||||
obj = DetachedDocumentObject()
|
||||
obj.addProperty("App::PropertyString", "Prop1", "Group1", "Doc1")
|
||||
obj.Prop1 = "Value1"
|
||||
obj.addProperty("App::PropertyInt", "Prop2", "Group1", "Doc2")
|
||||
obj.Prop2 = 123
|
||||
|
||||
widget = DocumentObjectEditorWidget(obj=obj, properties_to_show=["Prop1"])
|
||||
|
||||
# Store the initial editor instance for Prop1
|
||||
initial_prop1_editor = widget._property_editors["Prop1"]
|
||||
|
||||
# Set new properties to show
|
||||
new_properties_to_show = ["Prop1", "Prop2"]
|
||||
new_suffixes = {"Prop2": "Suffix2"}
|
||||
widget.setPropertiesToShow(new_properties_to_show, new_suffixes)
|
||||
|
||||
# Verify that the form was repopulated with the new properties
|
||||
self.assertEqual(widget._layout.rowCount(), len(new_properties_to_show))
|
||||
|
||||
# Verify property editors are updated and new ones created
|
||||
self.assertEqual(len(widget._property_editors), len(new_properties_to_show))
|
||||
self.assertIn("Prop1", widget._property_editors)
|
||||
self.assertIn("Prop2", widget._property_editors)
|
||||
|
||||
# Verify that the editor for Prop1 is a *new* instance after repopulation
|
||||
self.assertIsNot(widget._property_editors["Prop1"], initial_prop1_editor)
|
||||
self.assertIsInstance(widget._property_editors["Prop2"], IntPropertyEditorWidget)
|
||||
|
||||
# Verify labels including suffixes
|
||||
prop_names_in_layout = []
|
||||
for i in range(widget._layout.rowCount()):
|
||||
label_item = widget._layout.itemAt(i, QtGui.QFormLayout.LabelRole)
|
||||
label_widget = label_item.widget()
|
||||
label_text = label_widget.text()
|
||||
|
||||
prop_name = None
|
||||
for original_prop_name in new_properties_to_show:
|
||||
expected_label = _get_label_text(original_prop_name)
|
||||
suffix = new_suffixes.get(original_prop_name)
|
||||
if suffix:
|
||||
expected_label = f"{expected_label} ({suffix}):"
|
||||
else:
|
||||
expected_label = f"{expected_label}:"
|
||||
if label_text == expected_label:
|
||||
prop_name = original_prop_name
|
||||
break
|
||||
prop_names_in_layout.append(prop_name)
|
||||
|
||||
self.assertIn("Prop1", prop_names_in_layout)
|
||||
self.assertIn("Prop2", prop_names_in_layout)
|
||||
self.assertEqual(
|
||||
widget._layout.itemAt(0, QtGui.QFormLayout.LabelRole).widget().text(), "Prop1:"
|
||||
)
|
||||
self.assertEqual(
|
||||
widget._layout.itemAt(1, QtGui.QFormLayout.LabelRole).widget().text(),
|
||||
"Prop2 (Suffix2):",
|
||||
)
|
||||
|
||||
def test_property_changed_signal(self):
|
||||
obj = DetachedDocumentObject()
|
||||
obj.addProperty("App::PropertyString", "Prop1", "Group1", "Doc1")
|
||||
obj.Prop1 = "Value1"
|
||||
|
||||
widget = DocumentObjectEditorWidget(obj=obj, properties_to_show=["Prop1"])
|
||||
|
||||
# Connect to the widget's propertyChanged signal
|
||||
mock_slot = MagicMock()
|
||||
widget.propertyChanged.connect(mock_slot)
|
||||
|
||||
# Get the real child editor widget
|
||||
child_editor = widget._property_editors["Prop1"]
|
||||
self.assertIsInstance(child_editor, BasePropertyEditorWidget)
|
||||
|
||||
# Emit the signal from the real child editor
|
||||
child_editor.propertyChanged.emit()
|
||||
|
||||
# Verify that the widget's signal was emitted
|
||||
mock_slot.assert_called_once()
|
||||
|
||||
def test_update_ui(self):
|
||||
obj = DetachedDocumentObject()
|
||||
obj.addProperty("App::PropertyString", "Prop1", "Group1", "Doc1")
|
||||
obj.Prop1 = "Value1"
|
||||
obj.addProperty("App::PropertyInt", "Prop2", "Group1", "Doc2")
|
||||
obj.Prop2 = 123
|
||||
|
||||
properties_to_show = ["Prop1", "Prop2"]
|
||||
widget = DocumentObjectEditorWidget(obj=obj, properties_to_show=properties_to_show)
|
||||
|
||||
# Get the real child editor widgets
|
||||
editor1 = widget._property_editors["Prop1"]
|
||||
editor2 = widget._property_editors["Prop2"]
|
||||
|
||||
# Mock their updateWidget methods to check if they are called
|
||||
editor1.updateWidget = MagicMock()
|
||||
editor2.updateWidget = MagicMock()
|
||||
|
||||
# Call updateUI
|
||||
widget.updateUI()
|
||||
|
||||
# Verify that updateWidget was called on all child editors
|
||||
editor1.updateWidget.assert_called_once()
|
||||
editor2.updateWidget.assert_called_once()
|
||||
|
||||
def test_update_object(self):
|
||||
obj = DetachedDocumentObject()
|
||||
obj.addProperty("App::PropertyString", "Prop1", "Group1", "Doc1")
|
||||
obj.Prop1 = "Value1"
|
||||
obj.addProperty("App::PropertyInt", "Prop2", "Group1", "Doc2")
|
||||
obj.Prop2 = 123
|
||||
|
||||
properties_to_show = ["Prop1", "Prop2"]
|
||||
widget = DocumentObjectEditorWidget(obj=obj, properties_to_show=properties_to_show)
|
||||
|
||||
# Get the real child editor widgets
|
||||
editor1 = widget._property_editors["Prop1"]
|
||||
editor2 = widget._property_editors["Prop2"]
|
||||
|
||||
# Mock their updateProperty methods to check if they are called
|
||||
editor1.updateProperty = MagicMock()
|
||||
editor2.updateProperty = MagicMock()
|
||||
|
||||
# Call updateObject
|
||||
widget.updateObject()
|
||||
|
||||
# Verify that updateProperty was called on all child editors
|
||||
editor1.updateProperty.assert_called_once()
|
||||
editor2.updateProperty.assert_called_once()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
381
src/Mod/CAM/CAMTests/TestPathToolLibrary.py
Normal file
381
src/Mod/CAM/CAMTests/TestPathToolLibrary.py
Normal file
@@ -0,0 +1,381 @@
|
||||
import unittest
|
||||
from Path.Tool.library import Library
|
||||
from Path.Tool.toolbit import ToolBitEndmill, ToolBitDrill, ToolBitVBit
|
||||
from Path.Tool.shape import (
|
||||
ToolBitShapeEndmill,
|
||||
ToolBitShapeDrill,
|
||||
ToolBitShapeVBit,
|
||||
)
|
||||
|
||||
|
||||
class TestPathToolLibrary(unittest.TestCase):
|
||||
def test_init(self):
|
||||
library = Library("Test Library")
|
||||
self.assertEqual(library.label, "Test Library")
|
||||
self.assertIsNotNone(library.id)
|
||||
self.assertEqual(len(library._bits), 0)
|
||||
self.assertEqual(len(library._bit_nos), 0)
|
||||
|
||||
def test_str(self):
|
||||
library = Library("My Library", "123-abc")
|
||||
self.assertEqual(str(library), '123-abc "My Library"')
|
||||
|
||||
def test_eq(self):
|
||||
library1 = Library("Lib1", "same-id")
|
||||
library2 = Library("Lib2", "same-id")
|
||||
library3 = Library("Lib3", "different-id")
|
||||
self.assertEqual(library1, library2)
|
||||
self.assertNotEqual(library1, library3)
|
||||
|
||||
def test_iter(self):
|
||||
library = Library("Test Library")
|
||||
# Create ToolBitShape instances with required parameters
|
||||
shape1 = ToolBitShapeEndmill(
|
||||
id="dummy_endmill",
|
||||
CuttingEdgeHeight=10.0,
|
||||
Diameter=10.0,
|
||||
Flutes=2,
|
||||
Length=50.0,
|
||||
ShankDiameter=10.0,
|
||||
)
|
||||
shape2 = ToolBitShapeDrill(
|
||||
id="dummy_drill",
|
||||
Diameter=5.0,
|
||||
Length=40.0,
|
||||
ShankDiameter=5.0,
|
||||
Flutes=2,
|
||||
TipAngle=118.0,
|
||||
)
|
||||
bit1 = ToolBitEndmill(shape1)
|
||||
bit2 = ToolBitDrill(shape2)
|
||||
library.add_bit(bit1)
|
||||
library.add_bit(bit2)
|
||||
_bits_list = list(library)
|
||||
self.assertEqual(len(_bits_list), 2)
|
||||
self.assertIn(bit1, _bits_list)
|
||||
self.assertIn(bit2, _bits_list)
|
||||
|
||||
def test_get_next_bit_no(self):
|
||||
library = Library("Test Library")
|
||||
self.assertEqual(library.get_next_bit_no(), 1)
|
||||
# Using ToolBit instances in _bit_nos with ToolBitShape
|
||||
shape_a = ToolBitShapeEndmill(
|
||||
id="dummy_a",
|
||||
CuttingEdgeHeight=1.0,
|
||||
Diameter=1.0,
|
||||
Flutes=1,
|
||||
Length=10.0,
|
||||
ShankDiameter=1.0,
|
||||
)
|
||||
shape_b = ToolBitShapeDrill(
|
||||
id="dummy_b",
|
||||
Diameter=2.0,
|
||||
Length=20.0,
|
||||
ShankDiameter=2.0,
|
||||
Flutes=2,
|
||||
TipAngle=118.0,
|
||||
)
|
||||
shape_c = ToolBitShapeVBit(
|
||||
id="dummy_c",
|
||||
Diameter=3.0,
|
||||
Angle=90.0,
|
||||
Length=30.0,
|
||||
ShankDiameter=3.0,
|
||||
CuttingEdgeAngle=30.0,
|
||||
CuttingEdgeHeight=10.0,
|
||||
Flutes=2,
|
||||
TipDiameter=1.0,
|
||||
)
|
||||
library._bit_nos = {
|
||||
1: ToolBitEndmill(shape_a),
|
||||
5: ToolBitDrill(shape_b),
|
||||
2: ToolBitVBit(shape_c),
|
||||
}
|
||||
self.assertEqual(library.get_next_bit_no(), 6)
|
||||
library._bit_nos = {}
|
||||
self.assertEqual(library.get_next_bit_no(), 1)
|
||||
|
||||
def test_get_bit_no_from_bit(self):
|
||||
library = Library("Test Library")
|
||||
shape1 = ToolBitShapeEndmill(
|
||||
id="dummy_endmill_1",
|
||||
CuttingEdgeHeight=10.0,
|
||||
Diameter=10.0,
|
||||
Flutes=2,
|
||||
Length=50.0,
|
||||
ShankDiameter=10.0,
|
||||
)
|
||||
shape2 = ToolBitShapeDrill(
|
||||
id="dummy_drill_1",
|
||||
Diameter=5.0,
|
||||
Length=40.0,
|
||||
ShankDiameter=5.0,
|
||||
Flutes=2,
|
||||
TipAngle=118.0,
|
||||
)
|
||||
bit1 = ToolBitEndmill(shape1)
|
||||
bit2 = ToolBitDrill(shape2)
|
||||
library.add_bit(bit1, 1)
|
||||
library.add_bit(bit2, 2)
|
||||
self.assertEqual(library.get_bit_no_from_bit(bit1), 1)
|
||||
self.assertEqual(library.get_bit_no_from_bit(bit2), 2)
|
||||
shape_cutter = ToolBitShapeVBit(
|
||||
id="dummy_cutter_1",
|
||||
Diameter=3.0,
|
||||
Angle=90.0,
|
||||
Length=30.0,
|
||||
ShankDiameter=3.0,
|
||||
CuttingEdgeAngle=30.0,
|
||||
CuttingEdgeHeight=10.0,
|
||||
Flutes=2,
|
||||
TipDiameter=1.0,
|
||||
)
|
||||
self.assertIsNone(library.get_bit_no_from_bit(ToolBitVBit(shape_cutter)))
|
||||
|
||||
def test_assign_new_bit_no(self):
|
||||
library = Library("Test Library")
|
||||
shape1 = ToolBitShapeEndmill(
|
||||
id="dummy_endmill_2",
|
||||
CuttingEdgeHeight=10.0,
|
||||
Diameter=10.0,
|
||||
Flutes=2,
|
||||
Length=50.0,
|
||||
ShankDiameter=10.0,
|
||||
)
|
||||
shape2 = ToolBitShapeDrill(
|
||||
id="dummy_drill_2",
|
||||
Diameter=5.0,
|
||||
Length=40.0,
|
||||
ShankDiameter=5.0,
|
||||
Flutes=2,
|
||||
TipAngle=118.0,
|
||||
)
|
||||
bit1 = ToolBitEndmill(shape1)
|
||||
bit2 = ToolBitDrill(shape2)
|
||||
|
||||
# Assign bit1 without specifying number (should get 1)
|
||||
library.add_bit(bit1)
|
||||
self.assertEqual(len(library._bits), 1)
|
||||
self.assertEqual(len(library._bit_nos), 1)
|
||||
self.assertIn(1, library._bit_nos)
|
||||
self.assertEqual(library._bit_nos[1], bit1)
|
||||
|
||||
# Assign bit2 to number 1 (should reassign bit1)
|
||||
library.add_bit(bit2, 1)
|
||||
self.assertEqual(len(library._bits), 2)
|
||||
self.assertEqual(len(library._bit_nos), 2)
|
||||
self.assertIn(1, library._bit_nos)
|
||||
self.assertEqual(library._bit_nos[1], bit2)
|
||||
# Check if bit1 was reassigned to a new bit number (should be 2)
|
||||
self.assertIn(2, library._bit_nos)
|
||||
self.assertEqual(library._bit_nos[2], bit1)
|
||||
|
||||
# Assign bit2 to number 10
|
||||
library.assign_new_bit_no(bit2, 10)
|
||||
self.assertEqual(len(library._bits), 2)
|
||||
self.assertEqual(len(library._bit_nos), 2)
|
||||
self.assertIn(10, library._bit_nos)
|
||||
self.assertEqual(library._bit_nos[10], bit2)
|
||||
self.assertNotIn(1, library._bit_nos) # bit2 should no longer be at 1
|
||||
self.assertIn(2, library._bit_nos)
|
||||
self.assertEqual(library._bit_nos[2], bit1)
|
||||
|
||||
# Assign bit1 to number 5
|
||||
library.assign_new_bit_no(bit1, 5)
|
||||
self.assertEqual(len(library._bits), 2)
|
||||
self.assertEqual(len(library._bit_nos), 2)
|
||||
self.assertIn(5, library._bit_nos)
|
||||
self.assertEqual(library._bit_nos[5], bit1)
|
||||
self.assertNotIn(2, library._bit_nos) # bit1 should no longer be at 2
|
||||
self.assertIn(10, library._bit_nos)
|
||||
self.assertEqual(library._bit_nos[10], bit2)
|
||||
|
||||
def test_add_bit(self):
|
||||
library = Library("Test Library")
|
||||
shape1 = ToolBitShapeEndmill(
|
||||
id="dummy_endmill_3",
|
||||
CuttingEdgeHeight=10.0,
|
||||
Diameter=10.0,
|
||||
Flutes=2,
|
||||
Length=50.0,
|
||||
ShankDiameter=10.0,
|
||||
)
|
||||
shape2 = ToolBitShapeDrill(
|
||||
id="dummy_drill_3",
|
||||
Diameter=5.0,
|
||||
Length=40.0,
|
||||
ShankDiameter=5.0,
|
||||
Flutes=2,
|
||||
TipAngle=118.0,
|
||||
)
|
||||
bit1 = ToolBitEndmill(shape1)
|
||||
bit2 = ToolBitDrill(shape2)
|
||||
|
||||
library.add_bit(bit1)
|
||||
self.assertEqual(len(library._bits), 1)
|
||||
self.assertIn(bit1, library._bits)
|
||||
self.assertEqual(len(library._bit_nos), 1)
|
||||
self.assertIn(1, library._bit_nos)
|
||||
self.assertEqual(library._bit_nos[1], bit1)
|
||||
|
||||
library.add_bit(bit2, 5)
|
||||
self.assertEqual(len(library._bits), 2)
|
||||
self.assertIn(bit1, library._bits)
|
||||
self.assertIn(bit2, library._bits)
|
||||
self.assertEqual(len(library._bit_nos), 2)
|
||||
self.assertIn(1, library._bit_nos)
|
||||
self.assertEqual(library._bit_nos[1], bit1)
|
||||
self.assertIn(5, library._bit_nos)
|
||||
self.assertEqual(library._bit_nos[5], bit2)
|
||||
|
||||
# Add bit1 again (should not increase bit count in _bits list)
|
||||
library.add_bit(bit1, 10)
|
||||
self.assertEqual(len(library._bits), 2)
|
||||
self.assertIn(bit1, library._bits)
|
||||
self.assertIn(bit2, library._bits)
|
||||
self.assertEqual(len(library._bit_nos), 2) # _bit_nos count remains 2
|
||||
self.assertIn(10, library._bit_nos)
|
||||
self.assertEqual(library._bit_nos[10], bit1)
|
||||
self.assertIn(5, library._bit_nos)
|
||||
self.assertEqual(library._bit_nos[5], bit2)
|
||||
self.assertNotIn(1, library._bit_nos) # bit1 should no longer be at 1
|
||||
|
||||
def test_get_bits(self):
|
||||
library = Library("Test Library")
|
||||
shape1 = ToolBitShapeEndmill(
|
||||
id="dummy_endmill_4",
|
||||
CuttingEdgeHeight=10.0,
|
||||
Diameter=10.0,
|
||||
Flutes=2,
|
||||
Length=50.0,
|
||||
ShankDiameter=10.0,
|
||||
)
|
||||
shape2 = ToolBitShapeDrill(
|
||||
id="dummy_drill_4",
|
||||
Diameter=5.0,
|
||||
Length=40.0,
|
||||
ShankDiameter=5.0,
|
||||
Flutes=2,
|
||||
TipAngle=118.0,
|
||||
)
|
||||
bit1 = ToolBitEndmill(shape1)
|
||||
bit2 = ToolBitDrill(shape2)
|
||||
self.assertEqual(library.get_bits(), [])
|
||||
library.add_bit(bit1)
|
||||
library.add_bit(bit2)
|
||||
_bits_list = library.get_bits()
|
||||
self.assertEqual(len(_bits_list), 2)
|
||||
self.assertIn(bit1, _bits_list)
|
||||
self.assertIn(bit2, _bits_list)
|
||||
|
||||
def test_has_bit(self):
|
||||
library = Library("Test Library")
|
||||
shape1 = ToolBitShapeEndmill(
|
||||
id="dummy_endmill_5",
|
||||
CuttingEdgeHeight=10.0,
|
||||
Diameter=10.0,
|
||||
Flutes=2,
|
||||
Length=50.0,
|
||||
ShankDiameter=10.0,
|
||||
)
|
||||
shape2 = ToolBitShapeDrill(
|
||||
id="dummy_drill_5",
|
||||
Diameter=5.0,
|
||||
Length=40.0,
|
||||
ShankDiameter=5.0,
|
||||
Flutes=2,
|
||||
TipAngle=118.0,
|
||||
)
|
||||
bit1 = ToolBitEndmill(shape1)
|
||||
bit2 = ToolBitDrill(shape2)
|
||||
library.add_bit(bit1)
|
||||
self.assertTrue(library.has_bit(bit1))
|
||||
self.assertFalse(library.has_bit(bit2))
|
||||
# Create a new ToolBit with the same properties but different instance
|
||||
shape1_copy = ToolBitShapeEndmill(
|
||||
id="dummy_endmill_5_copy", # Use a different ID for the copy
|
||||
CuttingEdgeHeight=10.0,
|
||||
Diameter=10.0,
|
||||
Flutes=2,
|
||||
Length=50.0,
|
||||
ShankDiameter=10.0,
|
||||
)
|
||||
bit1_copy = ToolBitEndmill(shape1_copy)
|
||||
self.assertFalse(library.has_bit(bit1_copy))
|
||||
|
||||
def test_remove_bit(self):
|
||||
library = Library("Test Library")
|
||||
shape1 = ToolBitShapeEndmill(
|
||||
id="dummy_endmill_6",
|
||||
CuttingEdgeHeight=10.0,
|
||||
Diameter=10.0,
|
||||
Flutes=2,
|
||||
Length=50.0,
|
||||
ShankDiameter=10.0,
|
||||
)
|
||||
shape2 = ToolBitShapeDrill(
|
||||
id="dummy_drill_6",
|
||||
Diameter=5.0,
|
||||
Length=40.0,
|
||||
ShankDiameter=5.0,
|
||||
Flutes=2,
|
||||
TipAngle=118.0,
|
||||
)
|
||||
shape3 = ToolBitShapeVBit(
|
||||
id="dummy_cutter_6",
|
||||
Diameter=3.0,
|
||||
Angle=90.0,
|
||||
Length=30.0,
|
||||
ShankDiameter=3.0,
|
||||
CuttingEdgeAngle=30.0,
|
||||
CuttingEdgeHeight=10.0,
|
||||
Flutes=2,
|
||||
TipDiameter=1.0,
|
||||
)
|
||||
bit1 = ToolBitEndmill(shape1)
|
||||
bit2 = ToolBitDrill(shape2)
|
||||
bit3 = ToolBitVBit(shape3)
|
||||
|
||||
library.add_bit(bit1, 1)
|
||||
library.add_bit(bit2, 2)
|
||||
library.add_bit(bit3, 3)
|
||||
self.assertEqual(len(library._bits), 3)
|
||||
self.assertEqual(len(library._bit_nos), 3)
|
||||
|
||||
library.remove_bit(bit2)
|
||||
self.assertEqual(len(library._bits), 2)
|
||||
self.assertNotIn(bit2, library._bits)
|
||||
self.assertEqual(len(library._bit_nos), 2)
|
||||
self.assertNotIn(2, library._bit_nos)
|
||||
self.assertNotIn(bit2, library._bit_nos.values())
|
||||
|
||||
library.remove_bit(bit1)
|
||||
self.assertEqual(len(library._bits), 1)
|
||||
self.assertNotIn(bit1, library._bits)
|
||||
self.assertEqual(len(library._bit_nos), 1)
|
||||
self.assertNotIn(1, library._bit_nos)
|
||||
self.assertNotIn(bit1, library._bit_nos.values())
|
||||
|
||||
library.remove_bit(bit3)
|
||||
self.assertEqual(len(library._bits), 0)
|
||||
self.assertNotIn(bit3, library._bits)
|
||||
self.assertEqual(len(library._bit_nos), 0)
|
||||
self.assertNotIn(3, library._bit_nos)
|
||||
self.assertNotIn(bit3, library._bit_nos.values())
|
||||
|
||||
# Removing a non-existent bit should not raise an error
|
||||
shape_nonexistent = ToolBitShapeEndmill(
|
||||
id="dummy_nonexistent_6",
|
||||
CuttingEdgeHeight=99.0,
|
||||
Diameter=99.0,
|
||||
Flutes=1,
|
||||
Length=99.0,
|
||||
ShankDiameter=99.0,
|
||||
)
|
||||
library.remove_bit(ToolBitEndmill(shape_nonexistent))
|
||||
self.assertEqual(len(library._bits), 0)
|
||||
self.assertEqual(len(library._bit_nos), 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
128
src/Mod/CAM/CAMTests/TestPathToolLibrarySerializer.py
Normal file
128
src/Mod/CAM/CAMTests/TestPathToolLibrarySerializer.py
Normal file
@@ -0,0 +1,128 @@
|
||||
import unittest
|
||||
import json
|
||||
import FreeCAD
|
||||
from CAMTests.PathTestUtils import PathTestWithAssets
|
||||
from Path.Tool.library import Library
|
||||
from Path.Tool.toolbit import ToolBitEndmill
|
||||
from Path.Tool.shape import ToolBitShapeEndmill
|
||||
from Path.Tool.library.serializers import CamoticsLibrarySerializer, LinuxCNCSerializer
|
||||
|
||||
|
||||
class TestPathToolLibrarySerializerBase(PathTestWithAssets):
|
||||
"""Base class for Library serializer tests."""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.test_library_id = "test_library"
|
||||
self.test_library_label = "Test Library"
|
||||
self.test_library = Library(self.test_library_label, id=self.test_library_id)
|
||||
|
||||
# Create some dummy tool bits
|
||||
shape1 = ToolBitShapeEndmill("endmill_1")
|
||||
shape1.set_parameter("Diameter", FreeCAD.Units.Quantity("6.0 mm"))
|
||||
shape1.set_parameter("Length", FreeCAD.Units.Quantity("20.0 mm"))
|
||||
tool1 = ToolBitEndmill(shape1, id="tool_1")
|
||||
tool1.label = "Endmill 6mm"
|
||||
|
||||
shape2 = ToolBitShapeEndmill("endmill_2")
|
||||
shape2.set_parameter("Diameter", FreeCAD.Units.Quantity("3.0 mm"))
|
||||
shape2.set_parameter("Length", FreeCAD.Units.Quantity("15.0 mm"))
|
||||
tool2 = ToolBitEndmill(shape2, id="tool_2")
|
||||
tool2.label = "Endmill 3mm"
|
||||
|
||||
self.test_library.add_bit(tool1, 1)
|
||||
self.test_library.add_bit(tool2, 2)
|
||||
|
||||
|
||||
class TestCamoticsLibrarySerializer(TestPathToolLibrarySerializerBase):
|
||||
"""Tests for the CamoticsLibrarySerializer."""
|
||||
|
||||
def test_camotics_serialize(self):
|
||||
serializer = CamoticsLibrarySerializer
|
||||
serialized_data = serializer.serialize(self.test_library)
|
||||
self.assertIsInstance(serialized_data, bytes)
|
||||
|
||||
# Verify the content structure (basic check)
|
||||
data_dict = json.loads(serialized_data.decode("utf-8"))
|
||||
self.assertIn("1", data_dict)
|
||||
self.assertIn("2", data_dict)
|
||||
self.assertEqual(data_dict["1"]["description"], self.test_library._bit_nos[1].label)
|
||||
self.assertEqual(
|
||||
data_dict["2"]["diameter"],
|
||||
self.test_library._bit_nos[2]._tool_bit_shape.get_parameter("Diameter"),
|
||||
)
|
||||
|
||||
def test_camotics_deserialize(self):
|
||||
serializer = CamoticsLibrarySerializer
|
||||
# Create a dummy serialized data matching the expected format
|
||||
dummy_data = {
|
||||
"10": {
|
||||
"units": "metric",
|
||||
"shape": "Ballnose",
|
||||
"length": 25,
|
||||
"diameter": 8,
|
||||
"description": "Ballnose 8mm",
|
||||
},
|
||||
"20": {
|
||||
"units": "metric",
|
||||
"shape": "Cylindrical",
|
||||
"length": 30,
|
||||
"diameter": 10,
|
||||
"description": "Endmill 10mm",
|
||||
},
|
||||
}
|
||||
dummy_bytes = json.dumps(dummy_data, indent=2).encode("utf-8")
|
||||
|
||||
# Deserialize the data
|
||||
deserialized_library = serializer.deserialize(dummy_bytes, "deserialized_lib", {})
|
||||
|
||||
self.assertIsInstance(deserialized_library, Library)
|
||||
self.assertEqual(deserialized_library.get_id(), "deserialized_lib")
|
||||
self.assertEqual(len(deserialized_library._bit_nos), 2)
|
||||
|
||||
tool_10 = deserialized_library._bit_nos.get(10)
|
||||
assert tool_10 is not None, "tool not in the library"
|
||||
self.assertEqual(tool_10.label, "Ballnose 8mm")
|
||||
self.assertEqual(tool_10._tool_bit_shape.name, "Ballend")
|
||||
self.assertEqual(
|
||||
tool_10._tool_bit_shape.get_parameter("Diameter"), FreeCAD.Units.Quantity("8 mm")
|
||||
)
|
||||
self.assertEqual(
|
||||
tool_10._tool_bit_shape.get_parameter("Length"), FreeCAD.Units.Quantity("25 mm")
|
||||
)
|
||||
|
||||
tool_20 = deserialized_library._bit_nos.get(20)
|
||||
assert tool_20 is not None, "tool not in the library"
|
||||
self.assertEqual(tool_20.label, "Endmill 10mm")
|
||||
self.assertEqual(tool_20._tool_bit_shape.name, "Endmill")
|
||||
self.assertEqual(
|
||||
tool_20._tool_bit_shape.get_parameter("Diameter"), FreeCAD.Units.Quantity("10 mm")
|
||||
)
|
||||
self.assertEqual(
|
||||
tool_20._tool_bit_shape.get_parameter("Length"), FreeCAD.Units.Quantity("30 mm")
|
||||
)
|
||||
|
||||
|
||||
class TestLinuxCNCLibrarySerializer(TestPathToolLibrarySerializerBase):
|
||||
"""Tests for the LinuxCNCLibrarySerializer."""
|
||||
|
||||
def test_linuxcnc_serialize(self):
|
||||
serializer = LinuxCNCSerializer
|
||||
serialized_data = serializer.serialize(self.test_library)
|
||||
self.assertIsInstance(serialized_data, bytes)
|
||||
|
||||
# Verify the content format (basic check)
|
||||
lines = serialized_data.decode("ascii", "ignore").strip().split("\n")
|
||||
self.assertEqual(len(lines), 2)
|
||||
self.assertTrue(lines[0].startswith("T1 P D6.0 ;Endmill 6mm"))
|
||||
self.assertTrue(lines[1].startswith("T2 P D3.0 ;Endmill 3mm"))
|
||||
|
||||
def test_linuxcnc_deserialize_not_implemented(self):
|
||||
serializer = LinuxCNCSerializer
|
||||
dummy_data = b"T1 D6.0 ;Endmill 6mm\n"
|
||||
with self.assertRaises(NotImplementedError):
|
||||
serializer.deserialize(dummy_data, "dummy_id", {})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
206
src/Mod/CAM/CAMTests/TestPathToolMachine.py
Normal file
206
src/Mod/CAM/CAMTests/TestPathToolMachine.py
Normal file
@@ -0,0 +1,206 @@
|
||||
import unittest
|
||||
import FreeCAD
|
||||
from Path.Tool.machine.models.machine import Machine
|
||||
|
||||
|
||||
class TestPathToolMachine(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.default_machine = Machine()
|
||||
|
||||
def test_initialization_defaults(self):
|
||||
self.assertEqual(self.default_machine.label, "Machine")
|
||||
self.assertAlmostEqual(self.default_machine.max_power.getValueAs("W").Value, 2000)
|
||||
self.assertAlmostEqual(self.default_machine.get_min_rpm_value(), 3000)
|
||||
self.assertAlmostEqual(self.default_machine.get_max_rpm_value(), 60000)
|
||||
self.assertAlmostEqual(self.default_machine.min_feed.getValueAs("mm/min").Value, 1)
|
||||
self.assertAlmostEqual(self.default_machine.max_feed.getValueAs("mm/min").Value, 2000)
|
||||
expected_peak_torque_rpm = 60000 / 3
|
||||
self.assertAlmostEqual(
|
||||
self.default_machine.get_peak_torque_rpm_value(),
|
||||
expected_peak_torque_rpm,
|
||||
)
|
||||
expected_max_torque_nm = 2000 * 9.5488 / expected_peak_torque_rpm
|
||||
self.assertAlmostEqual(
|
||||
self.default_machine.max_torque.getValueAs("Nm").Value,
|
||||
expected_max_torque_nm,
|
||||
)
|
||||
self.assertIsNotNone(self.default_machine.id)
|
||||
|
||||
def test_initialization_custom_values(self):
|
||||
custom_machine = Machine(
|
||||
label="Custom Machine",
|
||||
max_power=5,
|
||||
min_rpm=1000,
|
||||
max_rpm=20000,
|
||||
max_torque=50,
|
||||
peak_torque_rpm=15000,
|
||||
min_feed=10,
|
||||
max_feed=5000,
|
||||
id="custom-id",
|
||||
)
|
||||
self.assertEqual(custom_machine.label, "Custom Machine")
|
||||
self.assertAlmostEqual(custom_machine.max_power.getValueAs("W").Value, 5000)
|
||||
self.assertAlmostEqual(custom_machine.get_min_rpm_value(), 1000)
|
||||
self.assertAlmostEqual(custom_machine.get_max_rpm_value(), 20000)
|
||||
self.assertAlmostEqual(custom_machine.max_torque.getValueAs("Nm").Value, 50)
|
||||
self.assertAlmostEqual(custom_machine.get_peak_torque_rpm_value(), 15000)
|
||||
self.assertAlmostEqual(custom_machine.min_feed.getValueAs("mm/min").Value, 10)
|
||||
self.assertAlmostEqual(custom_machine.max_feed.getValueAs("mm/min").Value, 5000)
|
||||
self.assertEqual(custom_machine.id, "custom-id")
|
||||
|
||||
def test_initialization_custom_torque_quantity(self):
|
||||
custom_torque_machine = Machine(max_torque=FreeCAD.Units.Quantity(100, "Nm"))
|
||||
self.assertAlmostEqual(custom_torque_machine.max_torque.getValueAs("Nm").Value, 100)
|
||||
|
||||
def test_validate_valid(self):
|
||||
try:
|
||||
self.default_machine.validate()
|
||||
except AttributeError as e:
|
||||
self.fail(f"Validation failed unexpectedly: {e}")
|
||||
|
||||
def test_validate_missing_label(self):
|
||||
self.default_machine.label = ""
|
||||
with self.assertRaisesRegex(AttributeError, "Machine name is required"):
|
||||
self.default_machine.validate()
|
||||
|
||||
def test_validate_peak_torque_rpm_greater_than_max_rpm(self):
|
||||
self.default_machine.set_peak_torque_rpm(70000)
|
||||
with self.assertRaisesRegex(AttributeError, "Peak Torque RPM.*must be less than max RPM"):
|
||||
self.default_machine.validate()
|
||||
|
||||
def test_validate_max_rpm_less_than_min_rpm(self):
|
||||
self.default_machine = Machine()
|
||||
self.default_machine.set_min_rpm(4000) # min_rpm = 4000 RPM
|
||||
self.default_machine.set_peak_torque_rpm(1000) # peak_torque_rpm = 1000 RPM
|
||||
self.default_machine._max_rpm = 2000 / 60.0 # max_rpm = 2000 RPM (33.33 1/s)
|
||||
self.assertLess(
|
||||
self.default_machine.get_max_rpm_value(),
|
||||
self.default_machine.get_min_rpm_value(),
|
||||
)
|
||||
with self.assertRaisesRegex(AttributeError, "Max RPM must be larger than min RPM"):
|
||||
self.default_machine.validate()
|
||||
|
||||
def test_validate_max_feed_less_than_min_feed(self):
|
||||
self.default_machine.set_min_feed(1000)
|
||||
self.default_machine._max_feed = 500
|
||||
with self.assertRaisesRegex(AttributeError, "Max feed must be larger than min feed"):
|
||||
self.default_machine.validate()
|
||||
|
||||
def test_get_torque_at_rpm(self):
|
||||
torque_below_peak = self.default_machine.get_torque_at_rpm(10000)
|
||||
expected_peak_torque_rpm = 60000 / 3
|
||||
expected_max_torque_nm = 2000 * 9.5488 / expected_peak_torque_rpm
|
||||
expected_torque_below_peak = expected_max_torque_nm / expected_peak_torque_rpm * 10000
|
||||
self.assertAlmostEqual(torque_below_peak, expected_torque_below_peak)
|
||||
|
||||
torque_at_peak = self.default_machine.get_torque_at_rpm(
|
||||
self.default_machine.get_peak_torque_rpm_value()
|
||||
)
|
||||
self.assertAlmostEqual(
|
||||
torque_at_peak,
|
||||
self.default_machine.max_torque.getValueAs("Nm").Value,
|
||||
)
|
||||
|
||||
torque_above_peak = self.default_machine.get_torque_at_rpm(50000)
|
||||
expected_torque_above_peak = 2000 * 9.5488 / 50000
|
||||
self.assertAlmostEqual(torque_above_peak, expected_torque_above_peak)
|
||||
|
||||
def test_set_label(self):
|
||||
self.default_machine.label = "New Label"
|
||||
self.assertEqual(self.default_machine.label, "New Label")
|
||||
|
||||
def test_set_max_power(self):
|
||||
self.default_machine = Machine()
|
||||
self.default_machine.set_max_power(5, "hp")
|
||||
self.assertAlmostEqual(
|
||||
self.default_machine.max_power.getValueAs("W").Value,
|
||||
5 * 745.7,
|
||||
places=4,
|
||||
)
|
||||
with self.assertRaisesRegex(AttributeError, "Max power must be positive"):
|
||||
self.default_machine.set_max_power(0)
|
||||
|
||||
def test_set_min_rpm(self):
|
||||
self.default_machine = Machine()
|
||||
self.default_machine.set_min_rpm(5000)
|
||||
self.assertAlmostEqual(self.default_machine.get_min_rpm_value(), 5000)
|
||||
with self.assertRaisesRegex(AttributeError, "Min RPM cannot be negative"):
|
||||
self.default_machine.set_min_rpm(-100)
|
||||
self.default_machine = Machine()
|
||||
self.default_machine.set_min_rpm(70000)
|
||||
self.assertAlmostEqual(self.default_machine.get_min_rpm_value(), 70000)
|
||||
self.assertAlmostEqual(self.default_machine.get_max_rpm_value(), 70001)
|
||||
|
||||
def test_set_max_rpm(self):
|
||||
self.default_machine = Machine()
|
||||
self.default_machine.set_max_rpm(50000)
|
||||
self.assertAlmostEqual(self.default_machine.get_max_rpm_value(), 50000)
|
||||
with self.assertRaisesRegex(AttributeError, "Max RPM must be positive"):
|
||||
self.default_machine.set_max_rpm(0)
|
||||
self.default_machine = Machine()
|
||||
self.default_machine.set_max_rpm(2000)
|
||||
self.assertAlmostEqual(self.default_machine.get_max_rpm_value(), 2000)
|
||||
self.assertAlmostEqual(self.default_machine.get_min_rpm_value(), 1999)
|
||||
self.default_machine = Machine()
|
||||
self.default_machine.set_max_rpm(0.5)
|
||||
self.assertAlmostEqual(self.default_machine.get_max_rpm_value(), 0.5)
|
||||
self.assertAlmostEqual(self.default_machine.get_min_rpm_value(), 0)
|
||||
|
||||
def test_set_min_feed(self):
|
||||
self.default_machine = Machine()
|
||||
self.default_machine.set_min_feed(500, "inch/min")
|
||||
self.assertAlmostEqual(
|
||||
self.default_machine.min_feed.getValueAs("mm/min").Value,
|
||||
500 * 25.4,
|
||||
places=4,
|
||||
)
|
||||
with self.assertRaisesRegex(AttributeError, "Min feed cannot be negative"):
|
||||
self.default_machine.set_min_feed(-10)
|
||||
self.default_machine = Machine()
|
||||
self.default_machine.set_min_feed(3000)
|
||||
self.assertAlmostEqual(self.default_machine.min_feed.getValueAs("mm/min").Value, 3000)
|
||||
self.assertAlmostEqual(self.default_machine.max_feed.getValueAs("mm/min").Value, 3001)
|
||||
|
||||
def test_set_max_feed(self):
|
||||
self.default_machine = Machine()
|
||||
self.default_machine.set_max_feed(3000)
|
||||
self.assertAlmostEqual(self.default_machine.max_feed.getValueAs("mm/min").Value, 3000)
|
||||
with self.assertRaisesRegex(AttributeError, "Max feed must be positive"):
|
||||
self.default_machine.set_max_feed(0)
|
||||
self.default_machine = Machine()
|
||||
self.default_machine.set_min_feed(600)
|
||||
self.default_machine.set_max_feed(500)
|
||||
self.assertAlmostEqual(self.default_machine.max_feed.getValueAs("mm/min").Value, 500)
|
||||
self.assertAlmostEqual(self.default_machine.min_feed.getValueAs("mm/min").Value, 499)
|
||||
self.default_machine = Machine()
|
||||
self.default_machine.set_max_feed(0.5)
|
||||
self.assertAlmostEqual(self.default_machine.max_feed.getValueAs("mm/min").Value, 0.5)
|
||||
self.assertAlmostEqual(self.default_machine.min_feed.getValueAs("mm/min").Value, 0)
|
||||
|
||||
def test_set_peak_torque_rpm(self):
|
||||
self.default_machine = Machine()
|
||||
self.default_machine.set_peak_torque_rpm(40000)
|
||||
self.assertAlmostEqual(self.default_machine.get_peak_torque_rpm_value(), 40000)
|
||||
with self.assertRaisesRegex(AttributeError, "Peak torque RPM cannot be negative"):
|
||||
self.default_machine.set_peak_torque_rpm(-100)
|
||||
|
||||
def test_set_max_torque(self):
|
||||
self.default_machine = Machine()
|
||||
self.default_machine.set_max_torque(200, "in-lbf")
|
||||
self.assertAlmostEqual(
|
||||
self.default_machine.max_torque.getValueAs("Nm").Value,
|
||||
200 * 0.112985,
|
||||
places=4,
|
||||
)
|
||||
with self.assertRaisesRegex(AttributeError, "Max torque must be positive"):
|
||||
self.default_machine.set_max_torque(0)
|
||||
|
||||
def test_dump(self):
|
||||
try:
|
||||
self.default_machine.dump(False)
|
||||
except Exception as e:
|
||||
self.fail(f"dump() method failed unexpectedly: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
391
src/Mod/CAM/CAMTests/TestPathToolShapeClasses.py
Normal file
391
src/Mod/CAM/CAMTests/TestPathToolShapeClasses.py
Normal file
@@ -0,0 +1,391 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Unit tests for the Path.Tool.Shape module and its utilities.
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Mapping, Tuple
|
||||
import FreeCAD
|
||||
from CAMTests.PathTestUtils import PathTestWithAssets
|
||||
from Path.Tool.assets import DummyAssetSerializer
|
||||
from Path.Tool.shape import (
|
||||
ToolBitShape,
|
||||
ToolBitShapeBallend,
|
||||
ToolBitShapeVBit,
|
||||
ToolBitShapeBullnose,
|
||||
ToolBitShapeSlittingSaw,
|
||||
)
|
||||
|
||||
|
||||
# Helper dummy class for testing abstract methods
|
||||
class DummyShape(ToolBitShape):
|
||||
name = "dummy"
|
||||
|
||||
def __init__(self, id, **kwargs):
|
||||
super().__init__(id=id, **kwargs)
|
||||
# Always define defaults in the subclass
|
||||
self._defaults = {
|
||||
"Param1": FreeCAD.Units.Quantity("10 mm"),
|
||||
"Param2": FreeCAD.Units.Quantity("5 deg"),
|
||||
}
|
||||
# Merge defaults into _params, allowing kwargs to override
|
||||
self._params = self._defaults | self._params
|
||||
|
||||
@classmethod
|
||||
def schema(cls) -> Mapping[str, Tuple[str, str]]:
|
||||
return {
|
||||
"Param1": (
|
||||
FreeCAD.Qt.translate("Param1", "Parameter 1"),
|
||||
"App::PropertyLength",
|
||||
),
|
||||
"Param2": (
|
||||
FreeCAD.Qt.translate("Param2", "Parameter 2"),
|
||||
"App::PropertyAngle",
|
||||
),
|
||||
}
|
||||
|
||||
@property
|
||||
def label(self):
|
||||
return "Dummy Shape"
|
||||
|
||||
|
||||
def unit(param):
|
||||
return param.getUserPreferred()[2]
|
||||
|
||||
|
||||
class TestPathToolShapeClasses(PathTestWithAssets):
|
||||
"""Tests for the concrete ToolBitShape subclasses."""
|
||||
|
||||
def _test_shape_common(self, alias):
|
||||
uri = ToolBitShape.resolve_name(alias)
|
||||
shape = self.assets.get(uri)
|
||||
return shape.get_parameters()
|
||||
|
||||
def test_base_init_with_defaults(self):
|
||||
"""Test base class initialization uses default parameters."""
|
||||
# Provide a dummy filepath and id for instantiation
|
||||
shape = DummyShape(id="dummy_shape_1", filepath=Path("/fake/dummy.fcstd"))
|
||||
self.assertEqual(str(shape.get_parameter("Param1")), "10.0 mm")
|
||||
self.assertEqual(str(shape.get_parameter("Param2")), "5.0 deg")
|
||||
|
||||
def test_base_init_with_kwargs(self):
|
||||
"""Test base class initialization overrides defaults with kwargs."""
|
||||
# Provide a dummy filepath for instantiation
|
||||
shape = DummyShape(
|
||||
id="dummy_shape_2",
|
||||
filepath=Path("/fake/dummy.fcstd"),
|
||||
Param1=FreeCAD.Units.Quantity("20 mm"),
|
||||
Param3="Ignored",
|
||||
)
|
||||
self.assertEqual(shape.get_parameter("Param1").Value, 20.0)
|
||||
self.assertEqual(shape.get_parameter("Param1").Value, 20.0)
|
||||
self.assertEqual(
|
||||
str(shape.get_parameter("Param1").Unit),
|
||||
"Unit: mm (1,0,0,0,0,0,0,0) [Length]",
|
||||
)
|
||||
self.assertEqual(shape.get_parameter("Param2").Value, 5.0)
|
||||
self.assertEqual(
|
||||
str(shape.get_parameter("Param2").Unit),
|
||||
"Unit: deg (0,0,0,0,0,0,0,1) [Angle]",
|
||||
) # Should remain default
|
||||
|
||||
def test_base_get_set_parameter(self):
|
||||
"""Test getting and setting individual parameters."""
|
||||
# Provide a dummy filepath for instantiation
|
||||
shape = DummyShape(id="dummy_shape_3", filepath=Path("/fake/dummy.fcstd"))
|
||||
self.assertEqual(shape.get_parameter("Param1").Value, 10.0)
|
||||
self.assertEqual(shape.get_parameter("Param1").Unit, FreeCAD.Units.Unit("mm"))
|
||||
shape.set_parameter("Param1", FreeCAD.Units.Quantity("15 mm"))
|
||||
self.assertEqual(shape.get_parameter("Param1").Value, 15.0)
|
||||
self.assertEqual(shape.get_parameter("Param1").Unit, FreeCAD.Units.Unit("mm"))
|
||||
with self.assertRaisesRegex(KeyError, "Shape 'dummy' has no parameter 'InvalidParam'"):
|
||||
shape.get_parameter("InvalidParam")
|
||||
|
||||
def test_base_get_parameters(self):
|
||||
"""Test getting the full parameter dictionary."""
|
||||
# Provide a dummy filepath for instantiation
|
||||
shape = DummyShape(
|
||||
id="dummy_shape_4",
|
||||
filepath=Path("/fake/dummy.fcstd"),
|
||||
Param1=FreeCAD.Units.Quantity("12 mm"),
|
||||
)
|
||||
# Create mock quantity instances using the configured mock class
|
||||
expected_param1 = FreeCAD.Units.Quantity("12.0 mm")
|
||||
expected_param2 = FreeCAD.Units.Quantity("5.0 deg")
|
||||
|
||||
expected = {"Param1": expected_param1, "Param2": expected_param2}
|
||||
params = shape.get_parameters()
|
||||
self.assertEqual(params["Param1"].Value, expected["Param1"].Value)
|
||||
self.assertEqual(str(params["Param1"].Unit), str(expected["Param1"].Unit))
|
||||
self.assertEqual(params["Param2"].Value, expected["Param2"].Value)
|
||||
self.assertEqual(str(params["Param2"].Unit), str(expected["Param2"].Unit))
|
||||
|
||||
def test_base_name_property(self):
|
||||
"""Test the name property returns the primary alias."""
|
||||
# Provide a dummy filepath for instantiation
|
||||
shape = DummyShape(id="dummy_shape_5", filepath=Path("/fake/dummy.fcstd"))
|
||||
self.assertEqual(shape.name, "dummy")
|
||||
|
||||
def test_base_get_parameter_label(self):
|
||||
"""Test retrieving parameter labels."""
|
||||
# Provide a dummy filepath for instantiation
|
||||
shape = DummyShape(id="dummy_shape_6", filepath=Path("/fake/dummy.fcstd"))
|
||||
self.assertEqual(shape.get_parameter_label("Param1"), "Parameter 1")
|
||||
self.assertEqual(shape.get_parameter_label("Param2"), "Parameter 2")
|
||||
# Test fallback for unknown parameter
|
||||
self.assertEqual(shape.get_parameter_label("UnknownParam"), "UnknownParam")
|
||||
|
||||
def test_base_get_expected_shape_parameters(self):
|
||||
"""Test retrieving the list of expected parameter names."""
|
||||
expected = ["Param1", "Param2"]
|
||||
self.assertCountEqual(DummyShape.get_expected_shape_parameters(), expected)
|
||||
|
||||
def test_base_str_repr(self):
|
||||
"""Test string representation."""
|
||||
# Provide a dummy filepath for instantiation
|
||||
shape = DummyShape(id="dummy_shape_7", filepath=Path("/fake/dummy.fcstd"))
|
||||
# Dynamically construct the expected string using the actual parameter string representations
|
||||
params_str = ", ".join(f"{name}={str(val)}" for name, val in shape.get_parameters().items())
|
||||
expected_str = f"dummy({params_str})"
|
||||
self.assertEqual(str(shape), expected_str)
|
||||
self.assertEqual(repr(shape), expected_str)
|
||||
|
||||
def test_base_resolve_name(self):
|
||||
"""Test resolving shape aliases to canonical names."""
|
||||
self.assertEqual(ToolBitShape.resolve_name("ballend").asset_id, "ballend")
|
||||
self.assertEqual(ToolBitShape.resolve_name("Ballend").asset_id, "ballend")
|
||||
self.assertEqual(ToolBitShape.resolve_name("v-bit").asset_id, "vbit")
|
||||
self.assertEqual(ToolBitShape.resolve_name("VBit").asset_id, "vbit")
|
||||
self.assertEqual(ToolBitShape.resolve_name("torus").asset_id, "bullnose")
|
||||
self.assertEqual(ToolBitShape.resolve_name("bullnose").asset_id, "bullnose")
|
||||
self.assertEqual(ToolBitShape.resolve_name("slitting-saw").asset_id, "slittingsaw")
|
||||
self.assertEqual(ToolBitShape.resolve_name("SlittingSaw").asset_id, "slittingsaw")
|
||||
# Test unknown name - should return the input name
|
||||
self.assertEqual(ToolBitShape.resolve_name("nonexistent").asset_id, "nonexistent")
|
||||
self.assertEqual(ToolBitShape.resolve_name("UnknownShape").asset_id, "UnknownShape")
|
||||
|
||||
def test_concrete_classes_instantiation(self):
|
||||
"""Test that all concrete classes can be instantiated."""
|
||||
# No patching of FreeCAD document operations here.
|
||||
# The test relies on the actual FreeCAD environment.
|
||||
|
||||
shape_uris = self.assets.list_assets(asset_type="toolbitshape")
|
||||
for uri in shape_uris:
|
||||
# Skip the DummyShape asset if it exists
|
||||
if uri.asset_id == "dummy":
|
||||
continue
|
||||
|
||||
with self.subTest(uri=uri):
|
||||
instance = self.assets.get(uri)
|
||||
self.assertIsInstance(instance, ToolBitShape)
|
||||
# Check if default params were set by checking if the
|
||||
# parameters dictionary is not empty.
|
||||
self.assertTrue(instance.get_parameters())
|
||||
|
||||
def test_get_shape_class(self):
|
||||
"""Test the get_shape_class function."""
|
||||
uri = ToolBitShape.resolve_name("ballend")
|
||||
self.assets.get(uri) # Ensure it's loadable
|
||||
|
||||
self.assertEqual(ToolBitShape.get_subclass_by_name("ballend"), ToolBitShapeBallend)
|
||||
self.assertEqual(ToolBitShape.get_subclass_by_name("v-bit"), ToolBitShapeVBit)
|
||||
self.assertEqual(ToolBitShape.get_subclass_by_name("VBit"), ToolBitShapeVBit)
|
||||
self.assertEqual(ToolBitShape.get_subclass_by_name("torus"), ToolBitShapeBullnose)
|
||||
self.assertEqual(ToolBitShape.get_subclass_by_name("slitting-saw"), ToolBitShapeSlittingSaw)
|
||||
self.assertIsNone(ToolBitShape.get_subclass_by_name("nonexistent"))
|
||||
|
||||
# The following tests for default parameters and labels
|
||||
# should also not use mocks for FreeCAD document operations or Units.
|
||||
# They should rely on the actual FreeCAD environment and the
|
||||
# load_file method of the base class.
|
||||
|
||||
def test_toolbitshapeballend_defaults(self):
|
||||
"""Test ToolBitShapeBallend default parameters and labels."""
|
||||
# Provide a dummy filepath for instantiation.
|
||||
# The actual file content is not loaded in this test,
|
||||
# only the default parameters are checked.
|
||||
shape = self._test_shape_common("ballend")
|
||||
self.assertEqual(shape["Diameter"].Value, 5.0)
|
||||
self.assertEqual(unit(shape["Diameter"]), "mm")
|
||||
self.assertEqual(shape["Length"].Value, 50.0)
|
||||
self.assertEqual(unit(shape["Length"]), "mm")
|
||||
# Need an instance to get parameter labels, get it from the asset manager
|
||||
uri = ToolBitShape.resolve_name("ballend")
|
||||
instance = self.assets.get(uri)
|
||||
self.assertEqual(instance.get_parameter_label("Diameter"), "Diameter")
|
||||
self.assertEqual(instance.get_parameter_label("Length"), "Overall tool length")
|
||||
|
||||
def test_toolbitshapedrill_defaults(self):
|
||||
"""Test ToolBitShapeDrill default parameters and labels."""
|
||||
# Provide a dummy filepath for instantiation.
|
||||
shape = self._test_shape_common("drill")
|
||||
self.assertEqual(shape["Diameter"].Value, 3.0)
|
||||
self.assertEqual(unit(shape["Diameter"]), "mm")
|
||||
self.assertEqual(shape["TipAngle"].Value, 119.0)
|
||||
self.assertEqual(unit(shape["TipAngle"]), "°")
|
||||
# Need an instance to get parameter labels, get it from the asset manager
|
||||
uri = ToolBitShape.resolve_name("drill")
|
||||
instance = self.assets.get(uri)
|
||||
self.assertEqual(instance.get_parameter_label("Diameter"), "Diameter")
|
||||
self.assertEqual(instance.get_parameter_label("TipAngle"), "Tip angle")
|
||||
|
||||
def test_toolbitshapechamfer_defaults(self):
|
||||
"""Test ToolBitShapeChamfer default parameters and labels."""
|
||||
# Provide a dummy filepath for instantiation.
|
||||
shape = self._test_shape_common("chamfer")
|
||||
self.assertEqual(shape["Diameter"].Value, 12.0)
|
||||
self.assertEqual(unit(shape["Diameter"]), "mm")
|
||||
self.assertEqual(shape["CuttingEdgeAngle"].Value, 60.0)
|
||||
self.assertEqual(unit(shape["CuttingEdgeAngle"]), "°")
|
||||
# Need an instance to get parameter labels, get it from the asset manager
|
||||
uri = ToolBitShape.resolve_name("chamfer")
|
||||
instance = self.assets.get(uri)
|
||||
self.assertEqual(instance.get_parameter_label("Diameter"), "Diameter")
|
||||
|
||||
def test_toolbitshapedovetail_defaults(self):
|
||||
"""Test ToolBitShapeDovetail default parameters and labels."""
|
||||
# Provide a dummy filepath for instantiation.
|
||||
shape = self._test_shape_common("dovetail")
|
||||
self.assertEqual(shape["Diameter"].Value, 20.0)
|
||||
self.assertEqual(unit(shape["Diameter"]), "mm")
|
||||
self.assertEqual(shape["CuttingEdgeAngle"].Value, 60.0)
|
||||
self.assertEqual(unit(shape["CuttingEdgeAngle"]), "°")
|
||||
# Need an instance to get parameter labels, get it from the asset manager
|
||||
uri = ToolBitShape.resolve_name("dovetail")
|
||||
instance = self.assets.get(uri)
|
||||
self.assertEqual(instance.get_parameter_label("CuttingEdgeAngle"), "Cutting angle")
|
||||
|
||||
def test_toolbitshapeendmill_defaults(self):
|
||||
"""Test ToolBitShapeEndmill default parameters and labels."""
|
||||
# Provide a dummy filepath for instantiation.
|
||||
shape = self._test_shape_common("endmill")
|
||||
self.assertEqual(shape["Diameter"].Value, 5.0)
|
||||
self.assertEqual(unit(shape["Diameter"]), "mm")
|
||||
self.assertEqual(shape["CuttingEdgeHeight"].Value, 30.0)
|
||||
self.assertEqual(unit(shape["CuttingEdgeHeight"]), "mm")
|
||||
# Need an instance to get parameter labels, get it from the asset manager
|
||||
uri = ToolBitShape.resolve_name("endmill")
|
||||
instance = self.assets.get(uri)
|
||||
self.assertEqual(instance.get_parameter_label("CuttingEdgeHeight"), "Cutting edge height")
|
||||
|
||||
def test_toolbitshapeprobe_defaults(self):
|
||||
"""Test ToolBitShapeProbe default parameters and labels."""
|
||||
# Provide a dummy filepath for instantiation.
|
||||
shape = self._test_shape_common("probe")
|
||||
self.assertEqual(shape["Diameter"].Value, 6.0)
|
||||
self.assertEqual(unit(shape["Diameter"]), "mm")
|
||||
self.assertEqual(shape["ShaftDiameter"].Value, 4.0)
|
||||
self.assertEqual(unit(shape["ShaftDiameter"]), "mm")
|
||||
# Need an instance to get parameter labels, get it from the asset manager
|
||||
uri = ToolBitShape.resolve_name("probe")
|
||||
instance = self.assets.get(uri)
|
||||
self.assertEqual(instance.get_parameter_label("Diameter"), "Ball diameter")
|
||||
self.assertEqual(instance.get_parameter_label("ShaftDiameter"), "Shaft diameter")
|
||||
|
||||
def test_toolbitshapereamer_defaults(self):
|
||||
"""Test ToolBitShapeReamer default parameters and labels."""
|
||||
# Provide a dummy filepath for instantiation.
|
||||
shape = self._test_shape_common("reamer")
|
||||
self.assertEqual(shape["Diameter"].Value, 5.0)
|
||||
self.assertEqual(unit(shape["Diameter"]), "mm")
|
||||
self.assertEqual(shape["Length"].Value, 50.0)
|
||||
self.assertEqual(unit(shape["Length"]), "mm")
|
||||
# Need an instance to get parameter labels, get it from the asset manager
|
||||
uri = ToolBitShape.resolve_name("reamer")
|
||||
instance = self.assets.get(uri)
|
||||
self.assertEqual(instance.get_parameter_label("Diameter"), "Diameter")
|
||||
|
||||
def test_toolbitshapeslittingsaw_defaults(self):
|
||||
"""Test ToolBitShapeSlittingSaw default parameters and labels."""
|
||||
# Provide a dummy filepath for instantiation.
|
||||
shape = self._test_shape_common("slittingsaw")
|
||||
self.assertEqual(shape["Diameter"].Value, 100.0)
|
||||
self.assertEqual(unit(shape["Diameter"]), "mm")
|
||||
self.assertEqual(shape["BladeThickness"].Value, 3.0)
|
||||
self.assertEqual(unit(shape["BladeThickness"]), "mm")
|
||||
# Need an instance to get parameter labels, get it from the asset manager
|
||||
uri = ToolBitShape.resolve_name("slittingsaw")
|
||||
instance = self.assets.get(uri)
|
||||
self.assertEqual(instance.get_parameter_label("BladeThickness"), "Blade thickness")
|
||||
|
||||
def test_toolbitshapetap_defaults(self):
|
||||
"""Test ToolBitShapeTap default parameters and labels."""
|
||||
# Provide a dummy filepath for instantiation.
|
||||
shape = self._test_shape_common("tap")
|
||||
self.assertAlmostEqual(shape["Diameter"].Value, 8, 4)
|
||||
self.assertEqual(unit(shape["Diameter"]), "mm")
|
||||
self.assertEqual(shape["TipAngle"].Value, 90.0)
|
||||
self.assertEqual(unit(shape["TipAngle"]), "°")
|
||||
# Need an instance to get parameter labels, get it from the asset manager
|
||||
uri = ToolBitShape.resolve_name("tap")
|
||||
instance = self.assets.get(uri)
|
||||
self.assertEqual(instance.get_parameter_label("TipAngle"), "Tip angle")
|
||||
|
||||
def test_toolbitshapethreadmill_defaults(self):
|
||||
"""Test ToolBitShapeThreadMill default parameters and labels."""
|
||||
# Provide a dummy filepath for instantiation.
|
||||
shape = self._test_shape_common("threadmill")
|
||||
self.assertEqual(shape["Diameter"].Value, 5.0)
|
||||
self.assertEqual(unit(shape["Diameter"]), "mm")
|
||||
self.assertEqual(shape["cuttingAngle"].Value, 60.0)
|
||||
self.assertEqual(unit(shape["cuttingAngle"]), "°")
|
||||
# Need an instance to get parameter labels, get it from the asset manager
|
||||
uri = ToolBitShape.resolve_name("threadmill")
|
||||
instance = self.assets.get(uri)
|
||||
self.assertEqual(instance.get_parameter_label("cuttingAngle"), "Cutting angle")
|
||||
|
||||
def test_toolbitshapebullnose_defaults(self):
|
||||
"""Test ToolBitShapeBullnose default parameters and labels."""
|
||||
# Provide a dummy filepath for instantiation.
|
||||
shape = self._test_shape_common("bullnose")
|
||||
self.assertEqual(shape["Diameter"].Value, 5.0)
|
||||
self.assertEqual(unit(shape["Diameter"]), "mm")
|
||||
self.assertEqual(shape["FlatRadius"].Value, 1.5)
|
||||
self.assertEqual(unit(shape["FlatRadius"]), "mm")
|
||||
# Need an instance to get parameter labels, get it from the asset manager
|
||||
uri = ToolBitShape.resolve_name("bullnose")
|
||||
instance = self.assets.get(uri)
|
||||
self.assertEqual(instance.get_parameter_label("FlatRadius"), "Torus radius")
|
||||
|
||||
def test_toolbitshapevbit_defaults(self):
|
||||
"""Test ToolBitShapeVBit default parameters and labels."""
|
||||
# Provide a dummy filepath for instantiation.
|
||||
shape = self._test_shape_common("vbit")
|
||||
self.assertEqual(shape["Diameter"].Value, 10.0)
|
||||
self.assertEqual(unit(shape["Diameter"]), "mm")
|
||||
self.assertEqual(shape["CuttingEdgeAngle"].Value, 90.0)
|
||||
self.assertEqual(unit(shape["CuttingEdgeAngle"]), "°")
|
||||
self.assertEqual(shape["TipDiameter"].Value, 1.0)
|
||||
self.assertEqual(unit(shape["TipDiameter"]), "mm")
|
||||
# Need an instance to get parameter labels, get it from the asset manager
|
||||
uri = ToolBitShape.resolve_name("vbit")
|
||||
instance = self.assets.get(uri)
|
||||
self.assertEqual(instance.get_parameter_label("CuttingEdgeAngle"), "Cutting edge angle")
|
||||
|
||||
def test_serialize_deserialize(self):
|
||||
"""
|
||||
Tests serialization and deserialization of a ToolBitShape object
|
||||
using the Asset interface methods.
|
||||
"""
|
||||
# Load a shape instance from a fixture file
|
||||
fixture_path = (
|
||||
Path(__file__).parent / "Tools" / "Shape" / "test-path-tool-bit-shape-00.fcstd"
|
||||
)
|
||||
original_shape = ToolBitShape.from_file(fixture_path)
|
||||
|
||||
# Serialize the shape using the to_bytes method
|
||||
serialized_data = original_shape.to_bytes(DummyAssetSerializer)
|
||||
|
||||
# Assert that the serialized data is bytes and not empty
|
||||
self.assertIsInstance(serialized_data, bytes)
|
||||
self.assertTrue(len(serialized_data) > 0)
|
||||
|
||||
# Deserialize the data using the from_bytes classmethod
|
||||
# Provide an empty dependencies mapping for this test
|
||||
deserialized_shape = ToolBitShape.from_bytes(
|
||||
serialized_data, original_shape.get_id(), {}, DummyAssetSerializer
|
||||
)
|
||||
|
||||
# Assert that the deserialized object is a ToolBitShape instance
|
||||
self.assertIsInstance(deserialized_shape, ToolBitShape)
|
||||
# Assert that the deserialized shape has the same parameters as the original
|
||||
self.assertEqual(original_shape.get_parameters(), deserialized_shape.get_parameters())
|
||||
self.assertEqual(original_shape.name, deserialized_shape.name)
|
||||
220
src/Mod/CAM/CAMTests/TestPathToolShapeDoc.py
Normal file
220
src/Mod/CAM/CAMTests/TestPathToolShapeDoc.py
Normal file
@@ -0,0 +1,220 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Unit tests for the Path.Tool.Shape module and its document utilities.
|
||||
import unittest
|
||||
from unittest.mock import patch, MagicMock, call
|
||||
from Path.Tool.shape import doc
|
||||
import os
|
||||
|
||||
|
||||
mock_freecad = MagicMock(Name="FreeCAD_Mock")
|
||||
mock_freecad.Console = MagicMock()
|
||||
mock_freecad.Console.PrintWarning = MagicMock()
|
||||
mock_freecad.Console.PrintError = MagicMock()
|
||||
|
||||
mock_obj = MagicMock(Name="Object_Mock")
|
||||
mock_obj.Label = "MockObjectLabel"
|
||||
mock_obj.Name = "MockObjectName"
|
||||
|
||||
mock_doc = MagicMock(Name="Document_Mock")
|
||||
mock_doc.Objects = [mock_obj]
|
||||
|
||||
|
||||
class TestPathToolShapeDoc(unittest.TestCase):
|
||||
def setUp(self):
|
||||
"""Reset mocks before each test."""
|
||||
# Resetting the top-level mock recursively resets its children
|
||||
# (newDocument, getDocument, openDocument, closeDocument, Console, etc.)
|
||||
# and their call counts, return_values, side_effects.
|
||||
mock_freecad.reset_mock()
|
||||
mock_doc.reset_mock()
|
||||
mock_obj.reset_mock()
|
||||
|
||||
# Re-establish default state/attributes potentially cleared by reset_mock
|
||||
# or needed for tests.
|
||||
mock_doc.Objects = [mock_obj]
|
||||
mock_obj.Label = "MockObjectLabel"
|
||||
mock_obj.Name = "MockObjectName"
|
||||
# Ensure mock_doc also has a Name attribute used in tests/code
|
||||
mock_doc.Name = "Document_Mock" # Used in closeDocument calls
|
||||
|
||||
# Clear attributes potentially added by setattr in previous tests.
|
||||
# reset_mock() doesn't remove attributes added this way.
|
||||
# Focus on attributes known to be added by tests in this file.
|
||||
for attr_name in ["Diameter", "Length", "Height"]:
|
||||
if hasattr(mock_obj, attr_name):
|
||||
try:
|
||||
delattr(mock_obj, attr_name)
|
||||
except AttributeError:
|
||||
pass # Ignore if already gone
|
||||
|
||||
"""Tests for the document utility functions in Path/Tool/Shape/doc.py"""
|
||||
|
||||
def test_doc_find_shape_object_body_priority(self):
|
||||
"""Test find_shape_object prioritizes PartDesign::Body."""
|
||||
body_obj = MagicMock(Name="Body_Mock")
|
||||
body_obj.isDerivedFrom = lambda typeName: typeName == "PartDesign::Body"
|
||||
part_obj = MagicMock(Name="Part_Mock")
|
||||
part_obj.isDerivedFrom = lambda typeName: typeName == "Part::Feature"
|
||||
mock_doc.Objects = [part_obj, body_obj]
|
||||
found = doc.find_shape_object(mock_doc)
|
||||
self.assertEqual(found, body_obj)
|
||||
|
||||
def test_doc_find_shape_object_part_fallback(self):
|
||||
"""Test find_shape_object falls back to Part::Feature."""
|
||||
part_obj = MagicMock(Name="Part_Mock")
|
||||
part_obj.isDerivedFrom = lambda typeName: typeName == "Part::Feature"
|
||||
other_obj = MagicMock(Name="Other_Mock")
|
||||
other_obj.isDerivedFrom = lambda typeName: False
|
||||
mock_doc.Objects = [other_obj, part_obj]
|
||||
found = doc.find_shape_object(mock_doc)
|
||||
self.assertEqual(found, part_obj)
|
||||
|
||||
def test_doc_find_shape_object_first_obj_fallback(self):
|
||||
"""Test find_shape_object falls back to the first object."""
|
||||
other_obj1 = MagicMock(Name="Other1_Mock")
|
||||
other_obj1.isDerivedFrom = lambda typeName: False
|
||||
other_obj2 = MagicMock(Name="Other2_Mock")
|
||||
other_obj2.isDerivedFrom = lambda typeName: False
|
||||
mock_doc.Objects = [other_obj1, other_obj2]
|
||||
found = doc.find_shape_object(mock_doc)
|
||||
self.assertEqual(found, other_obj1)
|
||||
|
||||
def test_doc_find_shape_object_no_objects(self):
|
||||
"""Test find_shape_object returns None if document has no objects."""
|
||||
mock_doc.Objects = []
|
||||
found = doc.find_shape_object(mock_doc)
|
||||
self.assertIsNone(found)
|
||||
|
||||
def test_doc_get_object_properties_found(self):
|
||||
"""Test get_object_properties extracts existing properties."""
|
||||
setattr(mock_obj, "Diameter", "10 mm")
|
||||
setattr(mock_obj, "Length", "50 mm")
|
||||
params = doc.get_object_properties(mock_obj, ["Diameter", "Length"])
|
||||
# Expecting just the values, not tuples
|
||||
self.assertEqual(params, {"Diameter": "10 mm", "Length": "50 mm"})
|
||||
mock_freecad.Console.PrintWarning.assert_not_called()
|
||||
|
||||
@patch("Path.Tool.shape.doc.FreeCAD", new=mock_freecad)
|
||||
def test_doc_get_object_properties_missing(self):
|
||||
"""Test get_object_properties handles missing properties with warning."""
|
||||
# Re-import doc within the patch context to use the mocked FreeCAD
|
||||
import Path.Tool.shape.doc as doc_patched
|
||||
|
||||
setattr(mock_obj, "Diameter", "10 mm")
|
||||
# Explicitly delete Height to ensure hasattr returns False for MagicMock
|
||||
if hasattr(mock_obj, "Height"):
|
||||
delattr(mock_obj, "Height")
|
||||
params = doc_patched.get_object_properties(mock_obj, ["Diameter", "Height"])
|
||||
# Expecting just the values, not tuples
|
||||
self.assertEqual(params, {"Diameter": "10 mm", "Height": None}) # Height is missing
|
||||
expected_calls = [
|
||||
# The 'Could not get type' warning is from base.py's set_parameter,
|
||||
# not get_object_properties. Removing it from expected calls here.
|
||||
call(
|
||||
"Parameter 'Height' not found on object 'MockObjectLabel' "
|
||||
"(MockObjectName). Default value will be used by the shape "
|
||||
"class.\n"
|
||||
)
|
||||
]
|
||||
mock_freecad.Console.PrintWarning.assert_has_calls(expected_calls, any_order=True)
|
||||
|
||||
@patch("FreeCAD.openDocument")
|
||||
@patch("FreeCAD.getDocument")
|
||||
@patch("FreeCAD.closeDocument")
|
||||
def test_ShapeDocFromBytes(self, mock_close_doc, mock_get_doc, mock_open_doc):
|
||||
"""Test ShapeDocFromBytes loads doc from a byte string."""
|
||||
content = b"fake_content"
|
||||
mock_opened_doc = MagicMock(Name="OpenedDoc_Mock")
|
||||
mock_get_doc.return_value = mock_opened_doc
|
||||
|
||||
temp_file_path = None
|
||||
try:
|
||||
with doc.ShapeDocFromBytes(content=content) as temp_doc:
|
||||
# Verify temp file creation and content
|
||||
mock_open_doc.assert_called_once()
|
||||
temp_file_path = mock_open_doc.call_args[0][0]
|
||||
self.assertTrue(os.path.exists(temp_file_path))
|
||||
with open(temp_file_path, "rb") as f:
|
||||
self.assertEqual(f.read(), content)
|
||||
|
||||
self.assertEqual(temp_doc, mock_open_doc.return_value)
|
||||
|
||||
# Verify cleanup after exiting the context
|
||||
mock_close_doc.assert_called_once_with(mock_open_doc.return_value.Name)
|
||||
self.assertFalse(os.path.exists(temp_file_path))
|
||||
|
||||
finally:
|
||||
# Ensure cleanup even if test fails before assertion
|
||||
if temp_file_path and os.path.exists(temp_file_path):
|
||||
os.remove(temp_file_path)
|
||||
|
||||
@patch("FreeCAD.openDocument")
|
||||
@patch("FreeCAD.getDocument")
|
||||
@patch("FreeCAD.closeDocument")
|
||||
def test_ShapeDocFromBytes_open_exception(self, mock_close_doc, mock_get_doc, mock_open_doc):
|
||||
"""Test ShapeDocFromBytes propagates exceptions and cleans up."""
|
||||
content = b"fake_content_exception"
|
||||
load_error = Exception("Fake load error")
|
||||
mock_open_doc.side_effect = load_error
|
||||
|
||||
temp_file_path = None
|
||||
try:
|
||||
with self.assertRaises(Exception) as cm:
|
||||
with doc.ShapeDocFromBytes(content=content):
|
||||
pass
|
||||
|
||||
self.assertEqual(cm.exception, load_error)
|
||||
|
||||
# Verify temp file was created before the exception
|
||||
mock_open_doc.assert_called_once()
|
||||
temp_file_path = mock_open_doc.call_args[0][0]
|
||||
self.assertTrue(os.path.exists(temp_file_path))
|
||||
with open(temp_file_path, "rb") as f:
|
||||
self.assertEqual(f.read(), content)
|
||||
|
||||
mock_get_doc.assert_not_called()
|
||||
# closeDocument is called in __exit__ only if _doc is not None,
|
||||
# which it will be if openDocument failed.
|
||||
mock_close_doc.assert_not_called()
|
||||
|
||||
finally:
|
||||
# Verify cleanup after exiting the context (even with exception)
|
||||
if temp_file_path and os.path.exists(temp_file_path):
|
||||
os.remove(temp_file_path)
|
||||
# Assert removal only if temp_file_path was set
|
||||
if temp_file_path:
|
||||
self.assertFalse(os.path.exists(temp_file_path))
|
||||
|
||||
@patch("FreeCAD.openDocument")
|
||||
@patch("FreeCAD.getDocument")
|
||||
@patch("FreeCAD.closeDocument")
|
||||
def test_ShapeDocFromBytes_exit_cleans_up(self, mock_close_doc, mock_get_doc, mock_open_doc):
|
||||
"""Test ShapeDocFromBytes __exit__ cleans up temp file."""
|
||||
content = b"fake_content_cleanup"
|
||||
mock_opened_doc = MagicMock(Name="OpenedDoc_Cleanup_Mock")
|
||||
mock_get_doc.return_value = mock_opened_doc
|
||||
|
||||
temp_file_path = None
|
||||
try:
|
||||
with doc.ShapeDocFromBytes(content=content):
|
||||
mock_open_doc.assert_called_once()
|
||||
temp_file_path = mock_open_doc.call_args[0][0]
|
||||
self.assertTrue(os.path.exists(temp_file_path))
|
||||
with open(temp_file_path, "rb") as f:
|
||||
self.assertEqual(f.read(), content)
|
||||
# No assertions on the returned doc here, focus is on cleanup
|
||||
pass # Exit the context
|
||||
|
||||
# Verify cleanup after exiting the context
|
||||
mock_close_doc.assert_called_once_with(mock_open_doc.return_value.Name)
|
||||
self.assertFalse(os.path.exists(temp_file_path))
|
||||
|
||||
finally:
|
||||
# Ensure cleanup even if test fails before assertion
|
||||
if temp_file_path and os.path.exists(temp_file_path):
|
||||
os.remove(temp_file_path)
|
||||
|
||||
|
||||
# Test execution
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
261
src/Mod/CAM/CAMTests/TestPathToolShapeIcon.py
Normal file
261
src/Mod/CAM/CAMTests/TestPathToolShapeIcon.py
Normal file
@@ -0,0 +1,261 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import unittest
|
||||
import unittest.mock
|
||||
import pathlib
|
||||
from tempfile import TemporaryDirectory
|
||||
from PySide import QtCore, QtGui
|
||||
from CAMTests.PathTestUtils import PathTestWithAssets
|
||||
from Path.Tool.assets import DummyAssetSerializer
|
||||
from Path.Tool.shape.models.icon import (
|
||||
ToolBitShapeIcon,
|
||||
ToolBitShapeSvgIcon,
|
||||
ToolBitShapePngIcon,
|
||||
)
|
||||
|
||||
|
||||
class TestToolBitShapeIconBase(PathTestWithAssets):
|
||||
"""Base class for ToolBitShapeIcon tests."""
|
||||
|
||||
ICON_CLASS = ToolBitShapeIcon
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
# Ensure a QApplication exists for QPixmap tests
|
||||
self.app = QtGui.QApplication.instance()
|
||||
|
||||
# Create a test shape and a test SVG icon.
|
||||
self.test_shape = self.assets.get("toolbitshape://ballend")
|
||||
self.test_svg = self.test_shape.icon
|
||||
assert self.test_svg is not None
|
||||
self.icon = self.ICON_CLASS("test_icon_base", b"")
|
||||
|
||||
def tearDown(self):
|
||||
self.app = None
|
||||
return super().tearDown()
|
||||
|
||||
def test_create_instance(self):
|
||||
# Test basic instance creation
|
||||
icon_id = "test_icon_123.dat"
|
||||
icon = self.ICON_CLASS(icon_id, b"")
|
||||
self.assertEqual(icon.get_id(), icon_id)
|
||||
self.assertEqual(icon.data, b"")
|
||||
self.assertIsInstance(icon.abbreviations, dict)
|
||||
|
||||
def test_to_bytes(self):
|
||||
# Test serializing to bytes
|
||||
icon_id = "test_to_bytes.bin"
|
||||
icon_data = b"some_binary_data"
|
||||
icon = ToolBitShapeIcon(icon_id, icon_data)
|
||||
self.assertEqual(icon.to_bytes(DummyAssetSerializer), icon_data)
|
||||
|
||||
def test_get_size_in_bytes(self):
|
||||
# Test getting icon data length
|
||||
icon_with_data = ToolBitShapeIcon("with_data.bin", b"abc")
|
||||
self.assertEqual(icon_with_data.get_size_in_bytes(), 3)
|
||||
|
||||
icon_no_data = ToolBitShapeIcon("no_data.dat", b"")
|
||||
self.assertEqual(icon_no_data.get_size_in_bytes(), 0)
|
||||
|
||||
@unittest.mock.patch("Path.Tool.shape.util.create_thumbnail_from_data")
|
||||
def test_from_shape_data_success(self, mock_create_thumbnail):
|
||||
# Test creating instance from shape data - success case
|
||||
shape_id = "test_shape"
|
||||
thumbnail_data = b"png thumbnail data"
|
||||
mock_create_thumbnail.return_value = thumbnail_data
|
||||
icon = ToolBitShapeIcon.from_shape_data(self.test_svg.data, shape_id)
|
||||
|
||||
mock_create_thumbnail.assert_called_once_with(self.test_svg.data)
|
||||
self.assertIsNotNone(icon)
|
||||
self.assertIsInstance(icon, ToolBitShapePngIcon)
|
||||
self.assertEqual(icon.get_id(), shape_id)
|
||||
self.assertEqual(icon.data, thumbnail_data)
|
||||
self.assertEqual(icon.abbreviations, {})
|
||||
|
||||
@unittest.mock.patch("Path.Tool.shape.util.create_thumbnail_from_data")
|
||||
def test_from_shape_data_failure(self, mock_create_thumbnail):
|
||||
# Test creating instance from shape data - failure case
|
||||
shape_id = "test_shape"
|
||||
mock_create_thumbnail.return_value = None
|
||||
icon_failed = ToolBitShapeIcon.from_shape_data(self.test_svg.data, shape_id)
|
||||
|
||||
mock_create_thumbnail.assert_called_once_with(self.test_svg.data)
|
||||
self.assertIsNone(icon_failed)
|
||||
|
||||
def test_get_png(self):
|
||||
if not self.app:
|
||||
self.skipTest("QApplication not available, skipping test_get_png")
|
||||
if type(self) is TestToolBitShapeIconBase:
|
||||
self.skipTest("Skipping test on abstract base class")
|
||||
# Test getting PNG data from the icon
|
||||
icon_size = QtCore.QSize(16, 16)
|
||||
png_data = self.icon.get_png(icon_size)
|
||||
self.assertIsInstance(png_data, bytes)
|
||||
self.assertTrue(len(png_data) > 0)
|
||||
|
||||
def test_get_qpixmap(self):
|
||||
if not self.app:
|
||||
self.skipTest("QApplication not available, skipping test_get_qpixmap")
|
||||
if type(self) is TestToolBitShapeIconBase:
|
||||
self.skipTest("Skipping test on abstract base class")
|
||||
# Test getting QPixmap from the icon
|
||||
icon_size = QtCore.QSize(31, 32)
|
||||
pixmap = self.icon.get_qpixmap(icon_size)
|
||||
self.assertIsInstance(pixmap, QtGui.QPixmap)
|
||||
self.assertFalse(pixmap.isNull())
|
||||
self.assertEqual(pixmap.size().width(), 31)
|
||||
self.assertEqual(pixmap.size().height(), 32)
|
||||
|
||||
|
||||
class TestToolBitShapeSvgIcon(TestToolBitShapeIconBase):
|
||||
"""Tests specifically for ToolBitShapeSvgIcon."""
|
||||
|
||||
ICON_CLASS = ToolBitShapeSvgIcon
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.icon = ToolBitShapeSvgIcon("test_icon_svg", self.test_svg.data)
|
||||
|
||||
def test_from_bytes_svg(self):
|
||||
# Test creating instance from bytes with SVG
|
||||
icon_svg = ToolBitShapeSvgIcon.from_bytes(
|
||||
self.test_svg.data, "test_from_bytes.svg", {}, DummyAssetSerializer
|
||||
)
|
||||
self.assertEqual(icon_svg.get_id(), "test_from_bytes.svg")
|
||||
self.assertEqual(icon_svg.data, self.test_svg.data)
|
||||
self.assertIsInstance(icon_svg.abbreviations, dict)
|
||||
|
||||
def test_round_trip_serialization_svg(self):
|
||||
# Test serialization and deserialization round trip for SVG
|
||||
svg_id = "round_trip_svg.svg"
|
||||
icon_svg = ToolBitShapeSvgIcon(svg_id, self.test_svg.data)
|
||||
serialized_svg = icon_svg.to_bytes(DummyAssetSerializer)
|
||||
deserialized_svg = ToolBitShapeSvgIcon.from_bytes(
|
||||
serialized_svg, svg_id, {}, DummyAssetSerializer
|
||||
)
|
||||
self.assertEqual(deserialized_svg.get_id(), svg_id)
|
||||
self.assertEqual(deserialized_svg.data, self.test_svg.data)
|
||||
# Abbreviations are extracted on access, so we don't check the dict directly
|
||||
self.assertIsInstance(deserialized_svg.abbreviations, dict)
|
||||
|
||||
def test_from_file_svg(self):
|
||||
# We cannot use NamedTemporaryFile on Windows, because there
|
||||
# we may not have permission to read the tempfile while it is
|
||||
# still open.
|
||||
# So we use TemporaryDirectory instead, to ensure cleanup while
|
||||
# still having a the temporary file inside it.
|
||||
with TemporaryDirectory() as thedir:
|
||||
tempfile = pathlib.Path(thedir, "test.svg")
|
||||
tempfile.write_bytes(self.test_svg.data)
|
||||
|
||||
icon_id = "dummy_icon"
|
||||
icon = ToolBitShapeIcon.from_file(tempfile, icon_id)
|
||||
self.assertIsInstance(icon, ToolBitShapeSvgIcon)
|
||||
self.assertEqual(icon.get_id(), icon_id)
|
||||
self.assertEqual(icon.data, self.test_svg.data)
|
||||
self.assertIsInstance(icon.abbreviations, dict)
|
||||
|
||||
def test_abbreviations_cached_property_svg(self):
|
||||
# Test abbreviations property and caching for SVG
|
||||
icon_svg = ToolBitShapeSvgIcon("cached_abbr.svg", self.test_svg.data)
|
||||
|
||||
# Accessing the property should call the static method
|
||||
with unittest.mock.patch.object(
|
||||
ToolBitShapeSvgIcon, "get_abbreviations_from_svg"
|
||||
) as mock_get_abbr:
|
||||
mock_get_abbr.return_value = {"param1": "A1"}
|
||||
abbr1 = icon_svg.abbreviations
|
||||
abbr2 = icon_svg.abbreviations
|
||||
mock_get_abbr.assert_called_once_with(self.test_svg.data)
|
||||
self.assertEqual(abbr1, {"param1": "A1"})
|
||||
self.assertEqual(abbr2, {"param1": "A1"})
|
||||
|
||||
def test_get_abbr_svg(self):
|
||||
# Test getting abbreviations for SVG
|
||||
icon_data = self.test_svg.data
|
||||
icon = ToolBitShapeSvgIcon("abbr_test.svg", icon_data)
|
||||
# Assuming the test_svg data has 'diameter' and 'length' ids
|
||||
self.assertIsNotNone(icon.get_abbr("Diameter"))
|
||||
self.assertIsNotNone(icon.get_abbr("Length"))
|
||||
self.assertIsNone(icon.get_abbr("NonExistent"))
|
||||
|
||||
def test_get_abbreviations_from_svg_static(self):
|
||||
# Test static method get_abbreviations_from_svg
|
||||
svg_content = self.test_svg.data
|
||||
abbr = ToolBitShapeSvgIcon.get_abbreviations_from_svg(svg_content)
|
||||
# Assuming the test_svg data has 'diameter' and 'length' ids
|
||||
self.assertIn("diameter", abbr)
|
||||
self.assertIn("length", abbr)
|
||||
|
||||
# Test with invalid SVG
|
||||
invalid_svg = b"<svg><text id='param1'>A1</text>" # Missing closing tag
|
||||
abbr_invalid = ToolBitShapeSvgIcon.get_abbreviations_from_svg(invalid_svg)
|
||||
self.assertEqual(abbr_invalid, {})
|
||||
|
||||
# Test with no text elements
|
||||
no_text_svg = b'<svg xmlns="http://www.w3.org/2000/svg"><rect/></svg>'
|
||||
abbr_no_text = ToolBitShapeSvgIcon.get_abbreviations_from_svg(no_text_svg)
|
||||
self.assertEqual(abbr_no_text, {})
|
||||
|
||||
|
||||
class TestToolBitShapePngIcon(TestToolBitShapeIconBase):
|
||||
"""Tests specifically for ToolBitShapePngIcon."""
|
||||
|
||||
ICON_CLASS = ToolBitShapePngIcon
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.png_data = b"\x89PNG\r\n\x1a\n" # Basic PNG signature
|
||||
self.icon = ToolBitShapePngIcon("test_icon_png", self.png_data)
|
||||
|
||||
def test_from_bytes_png(self):
|
||||
# Test creating instance from bytes with PNG
|
||||
icon_png = ToolBitShapePngIcon.from_bytes(
|
||||
self.png_data, "test_from_bytes.png", {}, DummyAssetSerializer
|
||||
)
|
||||
self.assertEqual(icon_png.get_id(), "test_from_bytes.png")
|
||||
self.assertEqual(icon_png.data, self.png_data)
|
||||
self.assertEqual(icon_png.abbreviations, {}) # No abbreviations for PNG
|
||||
|
||||
def test_round_trip_serialization_png(self):
|
||||
# Test serialization and deserialization round trip for PNG
|
||||
png_id = "round_trip_png.png"
|
||||
serialized_png = self.icon.to_bytes(DummyAssetSerializer)
|
||||
deserialized_png = ToolBitShapePngIcon.from_bytes(
|
||||
serialized_png, png_id, {}, DummyAssetSerializer
|
||||
)
|
||||
self.assertEqual(deserialized_png.get_id(), png_id)
|
||||
self.assertEqual(deserialized_png.data, self.png_data)
|
||||
self.assertEqual(deserialized_png.abbreviations, {})
|
||||
|
||||
def test_from_file_png(self):
|
||||
png_data = b"\\x89PNG\\r\\n\\x1a\\n"
|
||||
# We cannot use NamedTemporaryFile on Windows, because there
|
||||
# we may not have permission to read the tempfile while it is
|
||||
# still open.
|
||||
# So we use TemporaryDirectory instead, to ensure cleanup while
|
||||
# still having a the temporary file inside it.
|
||||
with TemporaryDirectory() as thedir:
|
||||
tempfile = pathlib.Path(thedir, "test.png")
|
||||
tempfile.write_bytes(png_data)
|
||||
|
||||
icon_id = "dummy_icon"
|
||||
icon = ToolBitShapeIcon.from_file(tempfile, icon_id)
|
||||
self.assertIsInstance(icon, ToolBitShapePngIcon)
|
||||
self.assertEqual(icon.get_id(), icon_id)
|
||||
self.assertEqual(icon.data, png_data)
|
||||
self.assertEqual(icon.abbreviations, {})
|
||||
|
||||
def test_abbreviations_cached_property_png(self):
|
||||
# Test abbreviations property and caching for PNG
|
||||
self.assertEqual(self.icon.abbreviations, {})
|
||||
|
||||
def test_get_abbr_png(self):
|
||||
# Test getting abbreviations for PNG
|
||||
self.assertIsNone(self.icon.get_abbr("Diameter"))
|
||||
|
||||
def test_get_qpixmap(self):
|
||||
self.skipTest("Skipping test, have no test data")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -24,10 +24,8 @@ import FreeCAD
|
||||
import Part
|
||||
import Path.Main.Job as PathJob
|
||||
import Path.Op.Vcarve as PathVcarve
|
||||
import Path.Tool.Bit as PathToolBit
|
||||
import math
|
||||
|
||||
from CAMTests.PathTestUtils import PathTestBase
|
||||
from CAMTests.PathTestUtils import PathTestWithAssets
|
||||
|
||||
|
||||
class VbitTool(object):
|
||||
@@ -43,10 +41,11 @@ Scale45 = 2.414214
|
||||
Scale60 = math.sqrt(3)
|
||||
|
||||
|
||||
class TestPathVcarve(PathTestBase):
|
||||
class TestPathVcarve(PathTestWithAssets):
|
||||
"""Test Vcarve milling basics."""
|
||||
|
||||
def tearDown(self):
|
||||
super().tearDown()
|
||||
if hasattr(self, "doc"):
|
||||
FreeCAD.closeDocument(self.doc.Name)
|
||||
|
||||
@@ -56,8 +55,9 @@ class TestPathVcarve(PathTestBase):
|
||||
rect = Part.makePolygon([(0, 0, 0), (5, 0, 0), (5, 10, 0), (0, 10, 0), (0, 0, 0)])
|
||||
part.Shape = Part.makeFace(rect, "Part::FaceMakerSimple")
|
||||
job = PathJob.Create("Job", [part])
|
||||
tool_file = PathToolBit.findToolBit("60degree_Vbit.fctb")
|
||||
job.Tools.Group[0].Tool = PathToolBit.Factory.CreateFrom(tool_file)
|
||||
toolbit = self.assets.get("toolbit://60degree_Vbit")
|
||||
loaded_tool = toolbit.attach_to_doc(doc=job.Document)
|
||||
job.Tools.Group[0].Tool = loaded_tool
|
||||
|
||||
op = PathVcarve.Create("TestVCarve")
|
||||
op.Base = job.Model.Group[0]
|
||||
|
||||
Reference in New Issue
Block a user