diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f4a4d0771a..00e1b69a15 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -10,3 +10,10 @@ build-and-test: artifacts: paths: - build + +build-and-test_merge-requests: + stage: build + script: + - docker/build.sh + only: + - merge_requests diff --git a/cura/BuildVolume.py b/cura/BuildVolume.py index bf38e6e562..e47659a7c1 100755 --- a/cura/BuildVolume.py +++ b/cura/BuildVolume.py @@ -258,7 +258,7 @@ class BuildVolume(SceneNode): node.setOutsideBuildArea(True) continue - if node.collidesWithArea(self.getDisallowedAreas()): + if node.collidesWithAreas(self.getDisallowedAreas()): node.setOutsideBuildArea(True) continue # If the entire node is below the build plate, still mark it as outside. @@ -312,7 +312,7 @@ class BuildVolume(SceneNode): node.setOutsideBuildArea(True) return - if node.collidesWithArea(self.getDisallowedAreas()): + if node.collidesWithAreas(self.getDisallowedAreas()): node.setOutsideBuildArea(True) return diff --git a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py index 86da9bf57f..e23341ba8a 100644 --- a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py +++ b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py @@ -60,8 +60,8 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): self._gcode = [] # type: List[str] self._connection_state_before_timeout = None # type: Optional[ConnectionState] - def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False, - file_handler: Optional[FileHandler] = None, **kwargs: str) -> None: + def requestWrite(self, nodes: List["SceneNode"], file_name: Optional[str] = None, limit_mimetypes: bool = False, + file_handler: Optional["FileHandler"] = None, filter_by_machine: bool = False, **kwargs) -> None: raise NotImplementedError("requestWrite needs to be implemented") def setAuthenticationState(self, authentication_state: AuthState) -> None: diff --git a/cura/PrinterOutput/PrinterOutputDevice.py b/cura/PrinterOutput/PrinterOutputDevice.py index 8e1b220a86..d4a37b3d68 100644 --- a/cura/PrinterOutput/PrinterOutputDevice.py +++ b/cura/PrinterOutput/PrinterOutputDevice.py @@ -144,7 +144,7 @@ class PrinterOutputDevice(QObject, OutputDevice): return None def requestWrite(self, nodes: List["SceneNode"], file_name: Optional[str] = None, limit_mimetypes: bool = False, - file_handler: Optional["FileHandler"] = None, **kwargs: str) -> None: + file_handler: Optional["FileHandler"] = None, filter_by_machine: bool = False, **kwargs) -> None: raise NotImplementedError("requestWrite needs to be implemented") @pyqtProperty(QObject, notify = printersChanged) diff --git a/cura/Scene/CuraSceneNode.py b/cura/Scene/CuraSceneNode.py index 38a988f982..4215c8fa84 100644 --- a/cura/Scene/CuraSceneNode.py +++ b/cura/Scene/CuraSceneNode.py @@ -6,13 +6,13 @@ from typing import cast, Dict, List, Optional from UM.Application import Application from UM.Math.AxisAlignedBox import AxisAlignedBox -from UM.Math.Polygon import Polygon #For typing. +from UM.Math.Polygon import Polygon # For typing. from UM.Scene.SceneNode import SceneNode -from UM.Scene.SceneNodeDecorator import SceneNodeDecorator #To cast the deepcopy of every decorator back to SceneNodeDecorator. +from UM.Scene.SceneNodeDecorator import SceneNodeDecorator # To cast the deepcopy of every decorator back to SceneNodeDecorator. -import cura.CuraApplication #To get the build plate. -from cura.Settings.ExtruderStack import ExtruderStack #For typing. -from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator #For per-object settings. +import cura.CuraApplication # To get the build plate. +from cura.Settings.ExtruderStack import ExtruderStack # For typing. +from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator # For per-object settings. ## Scene nodes that are models are only seen when selecting the corresponding build plate @@ -21,7 +21,7 @@ class CuraSceneNode(SceneNode): def __init__(self, parent: Optional["SceneNode"] = None, visible: bool = True, name: str = "", no_setting_override: bool = False) -> None: super().__init__(parent = parent, visible = visible, name = name) if not no_setting_override: - self.addDecorator(SettingOverrideDecorator()) # now we always have a getActiveExtruderPosition, unless explicitly disabled + self.addDecorator(SettingOverrideDecorator()) # Now we always have a getActiveExtruderPosition, unless explicitly disabled self._outside_buildarea = False def setOutsideBuildArea(self, new_value: bool) -> None: @@ -59,7 +59,7 @@ class CuraSceneNode(SceneNode): if extruder_id is not None: if extruder_id == extruder.getId(): return extruder - else: # If the id is unknown, then return the extruder in the position 0 + else: # If the id is unknown, then return the extruder in the position 0 try: if extruder.getMetaDataEntry("position", default = "0") == "0": # Check if the position is zero return extruder @@ -87,13 +87,13 @@ class CuraSceneNode(SceneNode): ] ## Return if any area collides with the convex hull of this scene node - def collidesWithArea(self, areas: List[Polygon]) -> bool: + def collidesWithAreas(self, areas: List[Polygon]) -> bool: convex_hull = self.callDecoration("getConvexHull") if convex_hull: if not convex_hull.isValid(): return False - # Check for collisions between disallowed areas and the object + # Check for collisions between provided areas and the object for area in areas: overlap = convex_hull.intersectsPolygon(area) if overlap is None: diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py index 8d94f41ea5..fc2cdae563 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py @@ -171,8 +171,8 @@ class CloudOutputDevice(NetworkedPrinterOutputDevice): self.setConnectionText(I18N_CATALOG.i18nc("@info:status", "Connected via Cloud")) ## Called when Cura requests an output device to receive a (G-code) file. - def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False, - file_handler: Optional[FileHandler] = None, **kwargs: str) -> None: + def requestWrite(self, nodes: List["SceneNode"], file_name: Optional[str] = None, limit_mimetypes: bool = False, + file_handler: Optional["FileHandler"] = None, filter_by_machine: bool = False, **kwargs) -> None: # Show an error message if we're already sending a job. if self._progress.visible: diff --git a/plugins/UM3NetworkPrinting/src/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/src/ClusterUM3OutputDevice.py index 5344fe2021..177836bccd 100644 --- a/plugins/UM3NetworkPrinting/src/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/ClusterUM3OutputDevice.py @@ -106,8 +106,8 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): self._active_camera_url = QUrl() # type: QUrl - def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False, - file_handler: Optional[FileHandler] = None, **kwargs: str) -> None: + def requestWrite(self, nodes: List["SceneNode"], file_name: Optional[str] = None, limit_mimetypes: bool = False, + file_handler: Optional["FileHandler"] = None, filter_by_machine: bool = False, **kwargs) -> None: self.writeStarted.emit(self) self.sendMaterialProfiles() diff --git a/plugins/UM3NetworkPrinting/src/LegacyUM3OutputDevice.py b/plugins/UM3NetworkPrinting/src/LegacyUM3OutputDevice.py index 5c1948b977..7d759264e5 100644 --- a/plugins/UM3NetworkPrinting/src/LegacyUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/LegacyUM3OutputDevice.py @@ -178,7 +178,8 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): # NotImplementedError. We can simply ignore these. pass - def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False, file_handler: Optional[FileHandler] = None, **kwargs: str) -> None: + def requestWrite(self, nodes: List["SceneNode"], file_name: Optional[str] = None, limit_mimetypes: bool = False, + file_handler: Optional["FileHandler"] = None, filter_by_machine: bool = False, **kwargs) -> None: if not self.activePrinter: # No active printer. Unable to write return diff --git a/plugins/USBPrinting/USBPrinterOutputDevice.py b/plugins/USBPrinting/USBPrinterOutputDevice.py index 0e2d159ae6..e32e4c8745 100644 --- a/plugins/USBPrinting/USBPrinterOutputDevice.py +++ b/plugins/USBPrinting/USBPrinterOutputDevice.py @@ -24,11 +24,15 @@ from queue import Queue from serial import Serial, SerialException, SerialTimeoutException from threading import Thread, Event from time import time -from typing import Union, Optional, List, cast +from typing import Union, Optional, List, cast, TYPE_CHECKING import re import functools # Used for reduce +if TYPE_CHECKING: + from UM.FileHandler.FileHandler import FileHandler + from UM.Scene.SceneNode import SceneNode + catalog = i18nCatalog("cura") @@ -117,14 +121,16 @@ class USBPrinterOutputDevice(PrinterOutputDevice): # \param filter_by_machine Whether to filter MIME types by machine. This # is ignored. # \param kwargs Keyword arguments. - def requestWrite(self, nodes, file_name: str = None, filter_by_machine = False, file_handler = None, **kwargs): + def requestWrite(self, nodes: List["SceneNode"], file_name: Optional[str] = None, limit_mimetypes: bool = False, + file_handler: Optional["FileHandler"] = None, filter_by_machine: bool = False, **kwargs) -> None: if self._is_printing: message = Message(text = catalog.i18nc("@message", "A print is still in progress. Cura cannot start another print via USB until the previous print has completed."), title = catalog.i18nc("@message", "Print in Progress")) message.show() return # Already printing self.writeStarted.emit(self) # cancel any ongoing preheat timer before starting a print - self._printers[0].getController().stopPreheatTimers() + controller = cast(GenericOutputController, self._printers[0].getController()) + controller.stopPreheatTimers() CuraApplication.getInstance().getController().setActiveStage("MonitorStage") diff --git a/resources/imade3d_jellybox/PETG/jbo_generic_petg_0.4_coarse.inst.cfg b/resources/quality/imade3d_jellybox/PETG/jbo_generic_petg_0.4_coarse.inst.cfg similarity index 100% rename from resources/imade3d_jellybox/PETG/jbo_generic_petg_0.4_coarse.inst.cfg rename to resources/quality/imade3d_jellybox/PETG/jbo_generic_petg_0.4_coarse.inst.cfg diff --git a/resources/imade3d_jellybox/PETG/jbo_generic_petg_0.4_fine.inst.cfg b/resources/quality/imade3d_jellybox/PETG/jbo_generic_petg_0.4_fine.inst.cfg similarity index 100% rename from resources/imade3d_jellybox/PETG/jbo_generic_petg_0.4_fine.inst.cfg rename to resources/quality/imade3d_jellybox/PETG/jbo_generic_petg_0.4_fine.inst.cfg diff --git a/resources/imade3d_jellybox/PETG/jbo_generic_petg_0.4_medium.inst.cfg b/resources/quality/imade3d_jellybox/PETG/jbo_generic_petg_0.4_medium.inst.cfg similarity index 100% rename from resources/imade3d_jellybox/PETG/jbo_generic_petg_0.4_medium.inst.cfg rename to resources/quality/imade3d_jellybox/PETG/jbo_generic_petg_0.4_medium.inst.cfg diff --git a/resources/imade3d_jellybox/PLA/jbo_generic_pla_0.4_coarse.inst.cfg b/resources/quality/imade3d_jellybox/PLA/jbo_generic_pla_0.4_coarse.inst.cfg similarity index 100% rename from resources/imade3d_jellybox/PLA/jbo_generic_pla_0.4_coarse.inst.cfg rename to resources/quality/imade3d_jellybox/PLA/jbo_generic_pla_0.4_coarse.inst.cfg diff --git a/resources/imade3d_jellybox/PLA/jbo_generic_pla_0.4_fine.inst.cfg b/resources/quality/imade3d_jellybox/PLA/jbo_generic_pla_0.4_fine.inst.cfg similarity index 100% rename from resources/imade3d_jellybox/PLA/jbo_generic_pla_0.4_fine.inst.cfg rename to resources/quality/imade3d_jellybox/PLA/jbo_generic_pla_0.4_fine.inst.cfg diff --git a/resources/imade3d_jellybox/PLA/jbo_generic_pla_0.4_medium.inst.cfg b/resources/quality/imade3d_jellybox/PLA/jbo_generic_pla_0.4_medium.inst.cfg similarity index 100% rename from resources/imade3d_jellybox/PLA/jbo_generic_pla_0.4_medium.inst.cfg rename to resources/quality/imade3d_jellybox/PLA/jbo_generic_pla_0.4_medium.inst.cfg diff --git a/resources/imade3d_jellybox/PLA/jbo_generic_pla_0.4_ultrafine.inst.cfg b/resources/quality/imade3d_jellybox/PLA/jbo_generic_pla_0.4_ultrafine.inst.cfg similarity index 100% rename from resources/imade3d_jellybox/PLA/jbo_generic_pla_0.4_ultrafine.inst.cfg rename to resources/quality/imade3d_jellybox/PLA/jbo_generic_pla_0.4_ultrafine.inst.cfg diff --git a/resources/imade3d_jellybox/jbo_global_coarse.inst.cfg b/resources/quality/imade3d_jellybox/imade3d_jellybox_coarse.inst.cfg similarity index 100% rename from resources/imade3d_jellybox/jbo_global_coarse.inst.cfg rename to resources/quality/imade3d_jellybox/imade3d_jellybox_coarse.inst.cfg diff --git a/resources/imade3d_jellybox/jbo_global_fine.inst.cfg b/resources/quality/imade3d_jellybox/imade3d_jellybox_fine.inst.cfg similarity index 100% rename from resources/imade3d_jellybox/jbo_global_fine.inst.cfg rename to resources/quality/imade3d_jellybox/imade3d_jellybox_fine.inst.cfg diff --git a/resources/imade3d_jellybox/jbo_global_normal.inst.cfg b/resources/quality/imade3d_jellybox/imade3d_jellybox_normal.inst.cfg similarity index 100% rename from resources/imade3d_jellybox/jbo_global_normal.inst.cfg rename to resources/quality/imade3d_jellybox/imade3d_jellybox_normal.inst.cfg diff --git a/resources/imade3d_jellybox/jbo_global_ultrafine.inst.cfg b/resources/quality/imade3d_jellybox/imade3d_jellybox_ultrafine.inst.cfg similarity index 100% rename from resources/imade3d_jellybox/jbo_global_ultrafine.inst.cfg rename to resources/quality/imade3d_jellybox/imade3d_jellybox_ultrafine.inst.cfg diff --git a/resources/imade3d_jellybox_2/PETG/jb2_generic_petg_0.4_coarse.inst.cfg b/resources/quality/imade3d_jellybox_2/PETG/jb2_generic_petg_0.4_coarse.inst.cfg similarity index 100% rename from resources/imade3d_jellybox_2/PETG/jb2_generic_petg_0.4_coarse.inst.cfg rename to resources/quality/imade3d_jellybox_2/PETG/jb2_generic_petg_0.4_coarse.inst.cfg diff --git a/resources/imade3d_jellybox_2/PETG/jb2_generic_petg_0.4_fine.inst.cfg b/resources/quality/imade3d_jellybox_2/PETG/jb2_generic_petg_0.4_fine.inst.cfg similarity index 100% rename from resources/imade3d_jellybox_2/PETG/jb2_generic_petg_0.4_fine.inst.cfg rename to resources/quality/imade3d_jellybox_2/PETG/jb2_generic_petg_0.4_fine.inst.cfg diff --git a/resources/imade3d_jellybox_2/PETG/jb2_generic_petg_0.4_medium.inst.cfg b/resources/quality/imade3d_jellybox_2/PETG/jb2_generic_petg_0.4_medium.inst.cfg similarity index 100% rename from resources/imade3d_jellybox_2/PETG/jb2_generic_petg_0.4_medium.inst.cfg rename to resources/quality/imade3d_jellybox_2/PETG/jb2_generic_petg_0.4_medium.inst.cfg diff --git a/resources/imade3d_jellybox_2/PLA/jb2_generic_pla_0.4_coarse.inst.cfg b/resources/quality/imade3d_jellybox_2/PLA/jb2_generic_pla_0.4_coarse.inst.cfg similarity index 100% rename from resources/imade3d_jellybox_2/PLA/jb2_generic_pla_0.4_coarse.inst.cfg rename to resources/quality/imade3d_jellybox_2/PLA/jb2_generic_pla_0.4_coarse.inst.cfg diff --git a/resources/imade3d_jellybox_2/PLA/jb2_generic_pla_0.4_fine.inst.cfg b/resources/quality/imade3d_jellybox_2/PLA/jb2_generic_pla_0.4_fine.inst.cfg similarity index 100% rename from resources/imade3d_jellybox_2/PLA/jb2_generic_pla_0.4_fine.inst.cfg rename to resources/quality/imade3d_jellybox_2/PLA/jb2_generic_pla_0.4_fine.inst.cfg diff --git a/resources/imade3d_jellybox_2/PLA/jb2_generic_pla_0.4_medium.inst.cfg b/resources/quality/imade3d_jellybox_2/PLA/jb2_generic_pla_0.4_medium.inst.cfg similarity index 100% rename from resources/imade3d_jellybox_2/PLA/jb2_generic_pla_0.4_medium.inst.cfg rename to resources/quality/imade3d_jellybox_2/PLA/jb2_generic_pla_0.4_medium.inst.cfg diff --git a/resources/imade3d_jellybox_2/PLA/jb2_generic_pla_0.4_ultrafine.inst.cfg b/resources/quality/imade3d_jellybox_2/PLA/jb2_generic_pla_0.4_ultrafine.inst.cfg similarity index 100% rename from resources/imade3d_jellybox_2/PLA/jb2_generic_pla_0.4_ultrafine.inst.cfg rename to resources/quality/imade3d_jellybox_2/PLA/jb2_generic_pla_0.4_ultrafine.inst.cfg diff --git a/resources/imade3d_jellybox_2/jb2_global_coarse.inst.cfg b/resources/quality/imade3d_jellybox_2/jb2_global_coarse.inst.cfg similarity index 100% rename from resources/imade3d_jellybox_2/jb2_global_coarse.inst.cfg rename to resources/quality/imade3d_jellybox_2/jb2_global_coarse.inst.cfg diff --git a/resources/imade3d_jellybox_2/jb2_global_fine.inst.cfg b/resources/quality/imade3d_jellybox_2/jb2_global_fine.inst.cfg similarity index 100% rename from resources/imade3d_jellybox_2/jb2_global_fine.inst.cfg rename to resources/quality/imade3d_jellybox_2/jb2_global_fine.inst.cfg diff --git a/resources/imade3d_jellybox_2/jb2_global_normal.inst.cfg b/resources/quality/imade3d_jellybox_2/jb2_global_normal.inst.cfg similarity index 100% rename from resources/imade3d_jellybox_2/jb2_global_normal.inst.cfg rename to resources/quality/imade3d_jellybox_2/jb2_global_normal.inst.cfg diff --git a/resources/imade3d_jellybox_2/jb2_global_ultrafine.inst.cfg b/resources/quality/imade3d_jellybox_2/jb2_global_ultrafine.inst.cfg similarity index 100% rename from resources/imade3d_jellybox_2/jb2_global_ultrafine.inst.cfg rename to resources/quality/imade3d_jellybox_2/jb2_global_ultrafine.inst.cfg diff --git a/tests/TestCuraSceneNode.py b/tests/TestCuraSceneNode.py new file mode 100644 index 0000000000..d4c1809c1e --- /dev/null +++ b/tests/TestCuraSceneNode.py @@ -0,0 +1,54 @@ +from UM.Math.Polygon import Polygon +from UM.Scene.SceneNodeDecorator import SceneNodeDecorator +from cura.Scene.CuraSceneNode import CuraSceneNode +import pytest + +from unittest.mock import patch + + +class TestConvexHullDecorator(SceneNodeDecorator): + def __init__(self): + super().__init__() + + def getConvexHull(self): + return Polygon([[5, 5], [-5, 5], [-5, -5], [5, -5]]) + + +class TestInvalidConvexHullDecorator(SceneNodeDecorator): + def __init__(self): + super().__init__() + + def getConvexHull(self): + return Polygon() + + +@pytest.fixture() +def cura_scene_node(): + # Replace the SettingOverrideDecorator with an empty decorator + with patch("cura.Scene.CuraSceneNode.SettingOverrideDecorator", SceneNodeDecorator): + return CuraSceneNode() + + +class TestCollidesWithAreas: + def test_noConvexHull(self, cura_scene_node): + assert not cura_scene_node.collidesWithAreas([Polygon([[10, 10], [-10, 10], [-10, -10], [10, -10]])]) + + def test_convexHullIntersects(self, cura_scene_node): + cura_scene_node.addDecorator(TestConvexHullDecorator()) + assert cura_scene_node.collidesWithAreas([Polygon([[10, 10], [-10, 10], [-10, -10], [10, -10]])]) + + def test_convexHullNoIntersection(self, cura_scene_node): + cura_scene_node.addDecorator(TestConvexHullDecorator()) + + assert not cura_scene_node.collidesWithAreas([Polygon([[60, 60], [40, 60], [40, 40], [60, 40]])]) + + def test_invalidConvexHull(self, cura_scene_node): + cura_scene_node.addDecorator(TestInvalidConvexHullDecorator()) + assert not cura_scene_node.collidesWithAreas([Polygon([[10, 10], [-10, 10], [-10, -10], [10, -10]])]) + + +def test_outsideBuildArea(cura_scene_node): + cura_scene_node.setOutsideBuildArea(True) + assert cura_scene_node.isOutsideBuildArea + + diff --git a/tests/TestExtruderManager.py b/tests/TestExtruderManager.py new file mode 100644 index 0000000000..4ad75989de --- /dev/null +++ b/tests/TestExtruderManager.py @@ -0,0 +1,31 @@ + +from unittest.mock import MagicMock, patch + + +def createMockedExtruder(extruder_id): + extruder = MagicMock() + extruder.getId = MagicMock(return_value = extruder_id) + return extruder + + +def test_getAllExtruderSettings(extruder_manager): + extruder_1 = createMockedExtruder("extruder_1") + extruder_1.getProperty = MagicMock(return_value ="beep") + extruder_2 = createMockedExtruder("extruder_2") + extruder_2.getProperty = MagicMock(return_value="zomg") + extruder_manager.getActiveExtruderStacks = MagicMock(return_value = [extruder_1, extruder_2]) + assert extruder_manager.getAllExtruderSettings("whatever", "value") == ["beep", "zomg"] + + +def test_registerExtruder(extruder_manager): + extruder = createMockedExtruder("beep") + extruder.getMetaDataEntry = MagicMock(return_value = "0") # because the extruder position gets called + + extruder_manager.extrudersChanged = MagicMock() + extruder_manager.registerExtruder(extruder, "zomg") + + assert extruder_manager.extrudersChanged.emit.call_count == 1 + + # Doing it again should not trigger anything + extruder_manager.registerExtruder(extruder, "zomg") + assert extruder_manager.extrudersChanged.emit.call_count == 1 diff --git a/tests/TestMachineManager.py b/tests/TestMachineManager.py index 250f285e1f..1e8cd8fb12 100644 --- a/tests/TestMachineManager.py +++ b/tests/TestMachineManager.py @@ -2,8 +2,6 @@ from unittest.mock import MagicMock, patch import pytest -from UM.Settings.ContainerRegistry import ContainerRegistry -from cura.Settings.ExtruderManager import ExtruderManager from cura.Settings.MachineManager import MachineManager @@ -13,22 +11,6 @@ def global_stack(): stack.getId = MagicMock(return_value ="GlobalStack") return stack -@pytest.fixture() -def container_registry() -> ContainerRegistry: - return MagicMock(name = "ContainerRegistry") - - -@pytest.fixture() -def extruder_manager(application, container_registry) -> ExtruderManager: - if ExtruderManager.getInstance() is not None: - # Reset the data - ExtruderManager._ExtruderManager__instance = None - - with patch("cura.CuraApplication.CuraApplication.getInstance", MagicMock(return_value=application)): - with patch("UM.Settings.ContainerRegistry.ContainerRegistry.getInstance", MagicMock(return_value=container_registry)): - manager = ExtruderManager() - return manager - @pytest.fixture() def machine_manager(application, extruder_manager, container_registry, global_stack) -> MachineManager: diff --git a/tests/TestMaterialManager.py b/tests/TestMaterialManager.py index 92380d40ae..4f339f54a4 100644 --- a/tests/TestMaterialManager.py +++ b/tests/TestMaterialManager.py @@ -4,8 +4,8 @@ from cura.Machines.MaterialManager import MaterialManager mocked_registry = MagicMock() -material_1 = {"id": "test", "GUID":"TEST!", "base_file": "base_material", "definition": "fdmmachine", "approximate_diameter": 3, "brand": "generic"} -material_2 = {"id": "base_material", "GUID": "TEST2!", "base_file": "test", "definition": "fdmmachine", "approximate_diameter": 3} +material_1 = {"id": "test", "GUID":"TEST!", "base_file": "base_material", "definition": "fdmmachine", "approximate_diameter": "3", "brand": "generic", "material": "pla"} +material_2 = {"id": "base_material", "GUID": "TEST2!", "base_file": "test", "definition": "fdmmachine", "approximate_diameter": "3", "material": "pla"} mocked_registry.findContainersMetadata = MagicMock(return_value = [material_1, material_2]) @@ -24,21 +24,116 @@ def test_initialize(application): assert manager.getMaterialGroup("test").name == "test" -def test_getAvailableMaterials(application): +def test_getMaterialGroupListByGUID(application): with patch("UM.Application.Application.getInstance", MagicMock(return_value=application)): manager = MaterialManager(mocked_registry) manager.initialize() - available_materials = manager.getAvailableMaterials(mocked_definition, None, None, 3) + assert manager.getMaterialGroupListByGUID("TEST!")[0].name == "test" + assert manager.getMaterialGroupListByGUID("TEST2!")[0].name == "base_material" - assert "base_material" in available_materials - assert "test" in available_materials + +def test_getRootMaterialIDforDiameter(application): + with patch("UM.Application.Application.getInstance", MagicMock(return_value=application)): + manager = MaterialManager(mocked_registry) + manager.initialize() + + assert manager.getRootMaterialIDForDiameter("base_material", "3") == "base_material" + + +def test_getMaterialNodeByTypeMachineHasNoMaterials(application): + with patch("UM.Application.Application.getInstance", MagicMock(return_value=application)): + manager = MaterialManager(mocked_registry) + manager.initialize() + + mocked_stack = MagicMock() + assert manager.getMaterialNodeByType(mocked_stack, "0", "nozzle", "", "") is None + + +def test_getMaterialNodeByTypeMachineHasMaterialsButNoMaterialFound(application): + with patch("UM.Application.Application.getInstance", MagicMock(return_value=application)): + manager = MaterialManager(mocked_registry) + manager.initialize() + + mocked_stack = MagicMock() + mocked_stack.definition.getMetaDataEntry = MagicMock(return_value = True) # For the "has_materials" metadata + + assert manager.getMaterialNodeByType(mocked_stack, "0", "nozzle", "", "") is None + + +def test_getMaterialNodeByTypeMachineHasMaterialsAndMaterialExists(application): + with patch("UM.Application.Application.getInstance", MagicMock(return_value=application)): + manager = MaterialManager(mocked_registry) + manager.initialize() + mocked_result = MagicMock() + manager.getMaterialNode = MagicMock(return_value = mocked_result) + mocked_stack = MagicMock() + mocked_stack.definition.getMetaDataEntry = MagicMock(return_value = True) # For the "has_materials" metadata + + assert manager.getMaterialNodeByType(mocked_stack, "0", "nozzle", "", "TEST!") is mocked_result + + +def test_getAllMaterialGroups(application): + with patch("UM.Application.Application.getInstance", MagicMock(return_value=application)): + manager = MaterialManager(mocked_registry) + manager.initialize() + + material_groups = manager.getAllMaterialGroups() + + assert "base_material" in material_groups + assert "test" in material_groups + assert material_groups["base_material"].name == "base_material" + assert material_groups["test"].name == "test" + + +class TestAvailableMaterials: + def test_happy(self, application): + with patch("UM.Application.Application.getInstance", MagicMock(return_value=application)): + manager = MaterialManager(mocked_registry) + manager.initialize() + + available_materials = manager.getAvailableMaterials(mocked_definition, None, None, 3) + + assert "base_material" in available_materials + assert "test" in available_materials + + def test_wrongDiameter(self, application): + with patch("UM.Application.Application.getInstance", MagicMock(return_value=application)): + manager = MaterialManager(mocked_registry) + manager.initialize() + + available_materials = manager.getAvailableMaterials(mocked_definition, None, None, 200) + assert available_materials == {} # Nothing found. + + def test_excludedMaterials(self, application): + with patch("UM.Application.Application.getInstance", MagicMock(return_value=application)): + manager = MaterialManager(mocked_registry) + manager.initialize() + with patch.object(mocked_definition, "getMetaDataEntry", MagicMock(return_value = ["test"])): + available_materials = manager.getAvailableMaterials(mocked_definition, None, None, 3) + assert "base_material" in available_materials + assert "test" not in available_materials + + +class Test_getFallbackMaterialIdByMaterialType: + def test_happyFlow(self, application): + with patch("UM.Application.Application.getInstance", MagicMock(return_value=application)): + manager = MaterialManager(mocked_registry) + manager.initialize() + + assert manager.getFallbackMaterialIdByMaterialType("pla") == "test" + + def test_unknownMaterial(self, application): + with patch("UM.Application.Application.getInstance", MagicMock(return_value=application)): + manager = MaterialManager(mocked_registry) + manager.initialize() + assert manager.getFallbackMaterialIdByMaterialType("OMGZOMG") is None def test_getMaterialNode(application): with patch("UM.Application.Application.getInstance", MagicMock(return_value=application)): manager = MaterialManager(mocked_registry) - manager.initialize() + manager._updateMaps() assert manager.getMaterialNode("fdmmachine", None, None, 3, "base_material").getMetaDataEntry("id") == "test" diff --git a/tests/TestQualityManager.py b/tests/TestQualityManager.py index 50318260b2..c701d56da0 100644 --- a/tests/TestQualityManager.py +++ b/tests/TestQualityManager.py @@ -1,11 +1,10 @@ -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch import pytest +from cura.Machines.QualityChangesGroup import QualityChangesGroup from cura.Machines.QualityManager import QualityManager - - mocked_stack = MagicMock() mocked_extruder = MagicMock() @@ -28,6 +27,8 @@ def container_registry(): {"id": "test_material", "definition": "fdmprinter", "quality_type": "normal", "name": "test_name_material", "material": "base_material", "type": "quality"}, {"id": "quality_changes_id", "definition": "fdmprinter", "type": "quality_changes", "quality_type": "amazing!", "name": "herp"}] result.findContainersMetadata = MagicMock(return_value = mocked_metadata) + + result.uniqueName = MagicMock(return_value = "uniqueName") return result @@ -58,3 +59,72 @@ def test_getQualityChangesGroup(quality_mocked_application): manager.initialize() assert "herp" in manager.getQualityChangesGroups(mocked_stack) + + +def test_getDefaultQualityType(quality_mocked_application): + manager = QualityManager(quality_mocked_application) + manager.initialize() + mocked_stack = MagicMock() + mocked_stack.definition.getMetaDataEntry = MagicMock(return_value = "normal") + assert manager.getDefaultQualityType(mocked_stack).quality_type == "normal" + + +def test_createQualityChanges(quality_mocked_application): + manager = QualityManager(quality_mocked_application) + mocked_quality_changes = MagicMock() + manager._createQualityChanges = MagicMock(return_value = mocked_quality_changes) + manager.initialize() + container_manager = MagicMock() + + manager._container_registry.addContainer = MagicMock() # Side effect we want to check. + with patch("cura.Settings.ContainerManager.ContainerManager.getInstance", container_manager): + manager.createQualityChanges("derp") + assert manager._container_registry.addContainer.called_once_with(mocked_quality_changes) + + +def test_renameQualityChangesGroup(quality_mocked_application): + manager = QualityManager(quality_mocked_application) + manager.initialize() + + mocked_container = MagicMock() + + quality_changes_group = QualityChangesGroup("zomg", "beep") + quality_changes_group.getAllNodes = MagicMock(return_value = [mocked_container]) + + # We need to check for "uniqueName" instead of "NEWRANDOMNAMEYAY" because the mocked registry always returns + # "uniqueName" when attempting to generate an unique name. + assert manager.renameQualityChangesGroup(quality_changes_group, "NEWRANDOMNAMEYAY") == "uniqueName" + assert mocked_container.setName.called_once_with("uniqueName") + + +def test_duplicateQualityChangesQualityOnly(quality_mocked_application): + manager = QualityManager(quality_mocked_application) + manager.initialize() + mocked_quality = MagicMock() + quality_group = MagicMock() + quality_group.getAllNodes = MagicMock(return_value = mocked_quality) + mocked_quality_changes = MagicMock() + + # Isolate what we want to test (in this case, we're not interested if createQualityChanges does it's job!) + manager._createQualityChanges = MagicMock(return_value = mocked_quality_changes) + manager._container_registry.addContainer = MagicMock() # Side effect we want to check. + + manager.duplicateQualityChanges("zomg", {"quality_group": quality_group, "quality_changes_group": None}) + assert manager._container_registry.addContainer.called_once_with(mocked_quality_changes) + + +def test_duplicateQualityChanges(quality_mocked_application): + manager = QualityManager(quality_mocked_application) + manager.initialize() + mocked_quality = MagicMock() + quality_group = MagicMock() + quality_group.getAllNodes = MagicMock(return_value = mocked_quality) + quality_changes_group = MagicMock() + mocked_quality_changes = MagicMock() + quality_changes_group.getAllNodes = MagicMock(return_value=[mocked_quality_changes]) + mocked_quality_changes.duplicate = MagicMock(return_value = mocked_quality_changes) + + manager._container_registry.addContainer = MagicMock() # Side effect we want to check. + + manager.duplicateQualityChanges("zomg", {"quality_group": quality_group, "quality_changes_group": quality_changes_group}) + assert manager._container_registry.addContainer.called_once_with(mocked_quality_changes) \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index 7f46c202b3..829253dfd1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,9 +11,12 @@ import Savitar # Dont remove this line import Arcus # No really. Don't. It needs to be there! from UM.Qt.QtApplication import QtApplication # QtApplication import is required, even though it isn't used. # Even though your IDE says these files are not used, don't believe it. It's lying. They need to be there. +from UM.Settings.ContainerRegistry import ContainerRegistry from cura.CuraApplication import CuraApplication +from cura.Settings.ExtruderManager import ExtruderManager from cura.UI.MachineActionManager import MachineActionManager +from unittest.mock import MagicMock, patch # Create a CuraApplication object that will be shared among all tests. It needs to be initialized. # Since we need to use it more that once, we create the application the first time and use its instance afterwards. @@ -22,7 +25,24 @@ def application() -> CuraApplication: app = unittest.mock.MagicMock() return app + +@pytest.fixture() +def container_registry() -> ContainerRegistry: + return MagicMock(name = "ContainerRegistry") + + # Returns a MachineActionManager instance. @pytest.fixture() def machine_action_manager(application) -> MachineActionManager: return MachineActionManager(application) + +@pytest.fixture() +def extruder_manager(application, container_registry) -> ExtruderManager: + if ExtruderManager.getInstance() is not None: + # Reset the data + ExtruderManager._ExtruderManager__instance = None + + with patch("cura.CuraApplication.CuraApplication.getInstance", MagicMock(return_value=application)): + with patch("UM.Settings.ContainerRegistry.ContainerRegistry.getInstance", MagicMock(return_value=container_registry)): + manager = ExtruderManager() + return manager \ No newline at end of file