Merge branch 'master' of ssh://github.com/Ultimaker/Cura

This commit is contained in:
Ghostkeeper 2019-07-22 11:42:08 +02:00
commit 5a51a036d7
No known key found for this signature in database
GPG Key ID: 86BEF881AE2CF276
37 changed files with 316 additions and 50 deletions

View File

@ -10,3 +10,10 @@ build-and-test:
artifacts:
paths:
- build
build-and-test_merge-requests:
stage: build
script:
- docker/build.sh
only:
- merge_requests

View File

@ -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

View File

@ -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:

View File

@ -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)

View File

@ -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:
@ -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:

View File

@ -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:

View File

@ -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()

View File

@ -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

View File

@ -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")

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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,7 +24,70 @@ 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()
assert manager.getMaterialGroupListByGUID("TEST!")[0].name == "test"
assert manager.getMaterialGroupListByGUID("TEST2!")[0].name == "base_material"
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()
@ -34,11 +97,43 @@ def test_getAvailableMaterials(application):
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"

View File

@ -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)

View File

@ -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