Merge branch 'tests-for-um3networkplugin' into cloud-output-device

This commit is contained in:
ChrisTerBeke 2018-11-23 14:08:55 +01:00
commit 8ee39c0489
3 changed files with 98 additions and 97 deletions

View file

@ -2,32 +2,32 @@
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from collections import namedtuple from collections import namedtuple
ClusterMaterial = namedtuple('ClusterMaterial', [ ClusterMaterial = namedtuple("ClusterMaterial", [
'guid', "guid", # Type: str
'material', "material", # Type: str
'brand', "brand", # Type: str
'version', "version", # Type: int
'color', "color", # Type: str
'density' "density" # Type: str
]) ])
LocalMaterial = namedtuple('LocalMaterial', [ LocalMaterial = namedtuple("LocalMaterial", [
'GUID', "GUID", # Type: str
'id', "id", # Type: str
'type', "type", # Type: str
'status', "status", # Type: str
'base_file', "base_file", # Type: str
'setting_version', "setting_version", # Type: int
'version', "version", # Type: int
'name', "name", # Type: str
'brand', "brand", # Type: str
'material', "material", # Type: str
'color_name', "color_name", # Type: str
'color_code', "color_code", # Type: str
'description', "description", # Type: str
'adhesion_info', "adhesion_info", # Type: str
'approximate_diameter', "approximate_diameter", # Type: str
'properties', "properties", # Type: str
'definition', "definition", # Type: str
'compatible' "compatible" # Type: str
]) ])

View file

@ -2,24 +2,24 @@
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
import json import json
import os import os
import re
import urllib.parse import urllib.parse
from typing import Dict, TYPE_CHECKING, Set from typing import Dict, TYPE_CHECKING, Set
from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest
from UM.Application import Application
from UM.Job import Job from UM.Job import Job
from UM.Logger import Logger from UM.Logger import Logger
from UM.MimeTypeDatabase import MimeTypeDatabase from UM.MimeTypeDatabase import MimeTypeDatabase
from UM.Resources import Resources from UM.Resources import Resources
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
# Absolute imports don't work in plugins # Absolute imports don't work in plugins
from .Models import ClusterMaterial, LocalMaterial from .Models import ClusterMaterial, LocalMaterial
if TYPE_CHECKING: if TYPE_CHECKING:
from .ClusterUM3OutputDevice import ClusterUM3OutputDevice from .ClusterUM3OutputDevice import ClusterUM3OutputDevice
## Asynchronous job to send material profiles to the printer. ## Asynchronous job to send material profiles to the printer.
# #
# This way it won't freeze up the interface while sending those materials. # This way it won't freeze up the interface while sending those materials.
@ -28,7 +28,6 @@ class SendMaterialJob(Job):
def __init__(self, device: "ClusterUM3OutputDevice") -> None: def __init__(self, device: "ClusterUM3OutputDevice") -> None:
super().__init__() super().__init__()
self.device = device # type: ClusterUM3OutputDevice self.device = device # type: ClusterUM3OutputDevice
self._application = CuraApplication.getInstance() # type: CuraApplication
## Send the request to the printer and register a callback ## Send the request to the printer and register a callback
def run(self) -> None: def run(self) -> None:
@ -45,13 +44,9 @@ class SendMaterialJob(Job):
return return
# Collect materials from the printer's reply and send the missing ones if needed. # Collect materials from the printer's reply and send the missing ones if needed.
try:
remote_materials_by_guid = self._parseReply(reply) remote_materials_by_guid = self._parseReply(reply)
if remote_materials_by_guid:
self._sendMissingMaterials(remote_materials_by_guid) self._sendMissingMaterials(remote_materials_by_guid)
except json.JSONDecodeError:
Logger.logException("w", "Error parsing materials from printer")
except TypeError:
Logger.logException("w", "Error parsing materials from printer")
## Determine which materials should be updated and send them to the printer. ## Determine which materials should be updated and send them to the printer.
# #
@ -86,7 +81,7 @@ class SendMaterialJob(Job):
return { return {
material.id material.id
for guid, material in local_materials.items() for guid, material in local_materials.items()
if guid not in remote_materials or int(material.version) > remote_materials[guid].version if guid not in remote_materials or material.version > remote_materials[guid].version
} }
## Send the materials to the printer. ## Send the materials to the printer.
@ -154,12 +149,18 @@ class SendMaterialJob(Job):
# Parses the reply to a "/materials" request to the printer # Parses the reply to a "/materials" request to the printer
# #
# \return a dictionary of ClusterMaterial objects by GUID # \return a dictionary of ClusterMaterial objects by GUID
# \throw json.JSONDecodeError Raised when the reply does not contain a valid json string
# \throw KeyError Raised when on of the materials does not include a valid guid # \throw KeyError Raised when on of the materials does not include a valid guid
@classmethod @classmethod
def _parseReply(cls, reply: QNetworkReply) -> Dict[str, ClusterMaterial]: def _parseReply(cls, reply: QNetworkReply) -> Dict[str, ClusterMaterial]:
try:
remote_materials = json.loads(reply.readAll().data().decode("utf-8")) remote_materials = json.loads(reply.readAll().data().decode("utf-8"))
return {material["guid"]: ClusterMaterial(**material) for material in remote_materials} return {material["guid"]: ClusterMaterial(**material) for material in remote_materials}
except UnicodeDecodeError:
Logger.log("e", "Request material storage on printer: I didn't understand the printer's answer.")
except json.JSONDecodeError:
Logger.log("e", "Request material storage on printer: I didn't understand the printer's answer.")
except TypeError:
Logger.log("e", "Request material storage on printer: Printer's answer was missing GUIDs.")
## Retrieves a list of local materials ## Retrieves a list of local materials
# #
@ -168,22 +169,24 @@ class SendMaterialJob(Job):
# \return a dictionary of LocalMaterial objects by GUID # \return a dictionary of LocalMaterial objects by GUID
def _getLocalMaterials(self) -> Dict[str, LocalMaterial]: def _getLocalMaterials(self) -> Dict[str, LocalMaterial]:
result = {} # type: Dict[str, LocalMaterial] result = {} # type: Dict[str, LocalMaterial]
container_registry = self._application.getContainerRegistry() container_registry = Application.getInstance().getContainerRegistry()
material_containers = container_registry.findContainersMetadata(type = "material") material_containers = container_registry.findContainersMetadata(type = "material")
# Find the latest version of all material containers in the registry. # Find the latest version of all material containers in the registry.
for material in material_containers: for material in material_containers:
try: try:
material = LocalMaterial(**material)
# material version must be an int # material version must be an int
if not re.match("\d+", material.version): material["version"] = int(material["version"])
Logger.logException("w", "Local material {} has invalid version '{}'."
.format(material["id"], material.version))
continue
if material.GUID not in result or material.version > result.get(material.GUID).version: # Create a new local material
result[material.GUID] = material local_material = LocalMaterial(**material)
if local_material.GUID not in result or \
local_material.version > result.get(local_material.GUID).version:
result[local_material.GUID] = local_material
except KeyError:
Logger.logException("w", "Local material {} has missing values.".format(material["id"]))
except ValueError: except ValueError:
Logger.logException("w", "Local material {} has invalid values.".format(material["id"])) Logger.logException("w", "Local material {} has invalid values.".format(material["id"]))

View file

@ -8,7 +8,7 @@ from unittest.mock import patch, call
from PyQt5.QtCore import QByteArray from PyQt5.QtCore import QByteArray
from UM.MimeTypeDatabase import MimeType from UM.MimeTypeDatabase import MimeType
from cura.CuraApplication import CuraApplication from UM.Application import Application
from plugins.UM3NetworkPrinting.src.SendMaterialJob import SendMaterialJob from plugins.UM3NetworkPrinting.src.SendMaterialJob import SendMaterialJob
@ -17,9 +17,11 @@ from plugins.UM3NetworkPrinting.src.SendMaterialJob import SendMaterialJob
lambda _: MimeType(name = "application/x-ultimaker-material-profile", comment = "Ultimaker Material Profile", lambda _: MimeType(name = "application/x-ultimaker-material-profile", comment = "Ultimaker Material Profile",
suffixes = ["xml.fdm_material"])) suffixes = ["xml.fdm_material"]))
@patch("UM.Resources.Resources.getAllResourcesOfType", lambda _: ["/materials/generic_pla_white.xml.fdm_material"]) @patch("UM.Resources.Resources.getAllResourcesOfType", lambda _: ["/materials/generic_pla_white.xml.fdm_material"])
@patch("plugins.UM3NetworkPrinting.src.ClusterUM3OutputDevice")
@patch("PyQt5.QtNetwork.QNetworkReply")
class TestSendMaterialJob(TestCase): class TestSendMaterialJob(TestCase):
_LOCAL_MATERIAL_WHITE = {"type": "material", "status": "unknown", "id": "generic_pla_white", _LOCAL_MATERIAL_WHITE = {"type": "material", "status": "unknown", "id": "generic_pla_white",
"base_file": "generic_pla_white", "setting_version": 5, "name": "White PLA", "base_file": "generic_pla_white", "setting_version": "5", "name": "White PLA",
"brand": "Generic", "material": "PLA", "color_name": "White", "brand": "Generic", "material": "PLA", "color_name": "White",
"GUID": "badb0ee7-87c8-4f3f-9398-938587b67dce", "version": "1", "color_code": "#ffffff", "GUID": "badb0ee7-87c8-4f3f-9398-938587b67dce", "version": "1", "color_code": "#ffffff",
"description": "Test PLA White", "adhesion_info": "Use glue.", "approximate_diameter": "3", "description": "Test PLA White", "adhesion_info": "Use glue.", "approximate_diameter": "3",
@ -27,7 +29,7 @@ class TestSendMaterialJob(TestCase):
"definition": "fdmprinter", "compatible": True} "definition": "fdmprinter", "compatible": True}
_LOCAL_MATERIAL_BLACK = {"type": "material", "status": "unknown", "id": "generic_pla_black", _LOCAL_MATERIAL_BLACK = {"type": "material", "status": "unknown", "id": "generic_pla_black",
"base_file": "generic_pla_black", "setting_version": 5, "name": "Yellow CPE", "base_file": "generic_pla_black", "setting_version": "5", "name": "Yellow CPE",
"brand": "Ultimaker", "material": "CPE", "color_name": "Black", "brand": "Ultimaker", "material": "CPE", "color_name": "Black",
"GUID": "5fbb362a-41f9-4818-bb43-15ea6df34aa4", "version": "1", "color_code": "#000000", "GUID": "5fbb362a-41f9-4818-bb43-15ea6df34aa4", "version": "1", "color_code": "#000000",
"description": "Test PLA Black", "adhesion_info": "Use glue.", "approximate_diameter": "3", "description": "Test PLA Black", "adhesion_info": "Use glue.", "approximate_diameter": "3",
@ -52,16 +54,13 @@ class TestSendMaterialJob(TestCase):
"density": 1.00 "density": 1.00
} }
@patch("plugins.UM3NetworkPrinting.src.ClusterUM3OutputDevice") def test_run(self, device_mock, reply_mock):
def test_run(self, device_mock):
job = SendMaterialJob(device_mock) job = SendMaterialJob(device_mock)
job.run() job.run()
# We expect the materials endpoint to be called when the job runs. # We expect the materials endpoint to be called when the job runs.
device_mock.get.assert_called_with("materials/", on_finished = job._onGetRemoteMaterials) device_mock.get.assert_called_with("materials/", on_finished = job._onGetRemoteMaterials)
@patch("plugins.UM3NetworkPrinting.src.ClusterUM3OutputDevice")
@patch("PyQt5.QtNetwork.QNetworkReply")
def test__onGetRemoteMaterials_withFailedRequest(self, reply_mock, device_mock): def test__onGetRemoteMaterials_withFailedRequest(self, reply_mock, device_mock):
reply_mock.attribute.return_value = 404 reply_mock.attribute.return_value = 404
job = SendMaterialJob(device_mock) job = SendMaterialJob(device_mock)
@ -71,8 +70,17 @@ class TestSendMaterialJob(TestCase):
self.assertEqual([call.attribute(0), call.errorString()], reply_mock.method_calls) self.assertEqual([call.attribute(0), call.errorString()], reply_mock.method_calls)
self.assertEqual(0, device_mock.createFormPart.call_count) self.assertEqual(0, device_mock.createFormPart.call_count)
@patch("plugins.UM3NetworkPrinting.src.ClusterUM3OutputDevice") def test__onGetRemoteMaterials_withWrongEncoding(self, reply_mock, device_mock):
@patch("PyQt5.QtNetwork.QNetworkReply") reply_mock.attribute.return_value = 200
reply_mock.readAll.return_value = QByteArray(json.dumps([self._REMOTE_MATERIAL_WHITE]).encode("cp500"))
job = SendMaterialJob(device_mock)
job._onGetRemoteMaterials(reply_mock)
# We expect the reply to be called once to try to get the printers from the list (readAll()).
# Given that the parsing fails we do no expect the device to be called for any follow up.
self.assertEqual([call.attribute(0), call.readAll()], reply_mock.method_calls)
self.assertEqual(0, device_mock.createFormPart.call_count)
def test__onGetRemoteMaterials_withBadJsonAnswer(self, reply_mock, device_mock): def test__onGetRemoteMaterials_withBadJsonAnswer(self, reply_mock, device_mock):
reply_mock.attribute.return_value = 200 reply_mock.attribute.return_value = 200
reply_mock.readAll.return_value = QByteArray(b"Six sick hicks nick six slick bricks with picks and sticks.") reply_mock.readAll.return_value = QByteArray(b"Six sick hicks nick six slick bricks with picks and sticks.")
@ -84,8 +92,6 @@ class TestSendMaterialJob(TestCase):
self.assertEqual([call.attribute(0), call.readAll()], reply_mock.method_calls) self.assertEqual([call.attribute(0), call.readAll()], reply_mock.method_calls)
self.assertEqual(0, device_mock.createFormPart.call_count) self.assertEqual(0, device_mock.createFormPart.call_count)
@patch("plugins.UM3NetworkPrinting.src.ClusterUM3OutputDevice")
@patch("PyQt5.QtNetwork.QNetworkReply")
def test__onGetRemoteMaterials_withMissingGuidInRemoteMaterial(self, reply_mock, device_mock): def test__onGetRemoteMaterials_withMissingGuidInRemoteMaterial(self, reply_mock, device_mock):
reply_mock.attribute.return_value = 200 reply_mock.attribute.return_value = 200
remote_material_without_guid = self._REMOTE_MATERIAL_WHITE.copy() remote_material_without_guid = self._REMOTE_MATERIAL_WHITE.copy()
@ -100,11 +106,9 @@ class TestSendMaterialJob(TestCase):
self.assertEqual(0, device_mock.createFormPart.call_count) self.assertEqual(0, device_mock.createFormPart.call_count)
@patch("cura.Settings.CuraContainerRegistry") @patch("cura.Settings.CuraContainerRegistry")
@patch("cura.CuraApplication") @patch("UM.Application")
@patch("plugins.UM3NetworkPrinting.src.ClusterUM3OutputDevice") def test__onGetRemoteMaterials_withInvalidVersionInLocalMaterial(self, application_mock, container_registry_mock,
@patch("PyQt5.QtNetwork.QNetworkReply") reply_mock, device_mock):
def test__onGetRemoteMaterials_withInvalidVersionInLocalMaterial(self, reply_mock, device_mock, application_mock,
container_registry_mock):
reply_mock.attribute.return_value = 200 reply_mock.attribute.return_value = 200
reply_mock.readAll.return_value = QByteArray(json.dumps([self._REMOTE_MATERIAL_WHITE]).encode("ascii")) reply_mock.readAll.return_value = QByteArray(json.dumps([self._REMOTE_MATERIAL_WHITE]).encode("ascii"))
@ -114,7 +118,7 @@ class TestSendMaterialJob(TestCase):
application_mock.getContainerRegistry.return_value = container_registry_mock application_mock.getContainerRegistry.return_value = container_registry_mock
with mock.patch.object(CuraApplication, "getInstance", new = lambda: application_mock): with mock.patch.object(Application, "getInstance", new = lambda: application_mock):
job = SendMaterialJob(device_mock) job = SendMaterialJob(device_mock)
job._onGetRemoteMaterials(reply_mock) job._onGetRemoteMaterials(reply_mock)
@ -124,11 +128,9 @@ class TestSendMaterialJob(TestCase):
self.assertEqual(0, device_mock.createFormPart.call_count) self.assertEqual(0, device_mock.createFormPart.call_count)
@patch("cura.Settings.CuraContainerRegistry") @patch("cura.Settings.CuraContainerRegistry")
@patch("cura.CuraApplication") @patch("UM.Application")
@patch("plugins.UM3NetworkPrinting.src.ClusterUM3OutputDevice") def test__onGetRemoteMaterials_withNoUpdate(self, application_mock, container_registry_mock, reply_mock,
@patch("PyQt5.QtNetwork.QNetworkReply") device_mock):
def test__onGetRemoteMaterials_withNoUpdate(self, reply_mock, device_mock, application_mock,
container_registry_mock):
application_mock.getContainerRegistry.return_value = container_registry_mock application_mock.getContainerRegistry.return_value = container_registry_mock
device_mock.createFormPart.return_value = "_xXx_" device_mock.createFormPart.return_value = "_xXx_"
@ -138,7 +140,7 @@ class TestSendMaterialJob(TestCase):
reply_mock.attribute.return_value = 200 reply_mock.attribute.return_value = 200
reply_mock.readAll.return_value = QByteArray(json.dumps([self._REMOTE_MATERIAL_WHITE]).encode("ascii")) reply_mock.readAll.return_value = QByteArray(json.dumps([self._REMOTE_MATERIAL_WHITE]).encode("ascii"))
with mock.patch.object(CuraApplication, "getInstance", new = lambda: application_mock): with mock.patch.object(Application, "getInstance", new = lambda: application_mock):
job = SendMaterialJob(device_mock) job = SendMaterialJob(device_mock)
job._onGetRemoteMaterials(reply_mock) job._onGetRemoteMaterials(reply_mock)
@ -149,11 +151,9 @@ class TestSendMaterialJob(TestCase):
self.assertEqual(0, device_mock.postFormWithParts.call_count) self.assertEqual(0, device_mock.postFormWithParts.call_count)
@patch("cura.Settings.CuraContainerRegistry") @patch("cura.Settings.CuraContainerRegistry")
@patch("cura.CuraApplication") @patch("UM.Application")
@patch("plugins.UM3NetworkPrinting.src.ClusterUM3OutputDevice") def test__onGetRemoteMaterials_withUpdatedMaterial(self, application_mock, container_registry_mock, reply_mock,
@patch("PyQt5.QtNetwork.QNetworkReply") device_mock):
def test__onGetRemoteMaterials_withUpdatedMaterial(self, reply_mock, device_mock, application_mock,
container_registry_mock):
application_mock.getContainerRegistry.return_value = container_registry_mock application_mock.getContainerRegistry.return_value = container_registry_mock
device_mock.createFormPart.return_value = "_xXx_" device_mock.createFormPart.return_value = "_xXx_"
@ -165,7 +165,7 @@ class TestSendMaterialJob(TestCase):
reply_mock.attribute.return_value = 200 reply_mock.attribute.return_value = 200
reply_mock.readAll.return_value = QByteArray(json.dumps([self._REMOTE_MATERIAL_WHITE]).encode("ascii")) reply_mock.readAll.return_value = QByteArray(json.dumps([self._REMOTE_MATERIAL_WHITE]).encode("ascii"))
with mock.patch.object(CuraApplication, "getInstance", new = lambda: application_mock): with mock.patch.object(Application, "getInstance", new = lambda: application_mock):
job = SendMaterialJob(device_mock) job = SendMaterialJob(device_mock)
job._onGetRemoteMaterials(reply_mock) job._onGetRemoteMaterials(reply_mock)
@ -180,11 +180,9 @@ class TestSendMaterialJob(TestCase):
device_mock.method_calls) device_mock.method_calls)
@patch("cura.Settings.CuraContainerRegistry") @patch("cura.Settings.CuraContainerRegistry")
@patch("cura.CuraApplication") @patch("UM.Application")
@patch("plugins.UM3NetworkPrinting.src.ClusterUM3OutputDevice") def test__onGetRemoteMaterials_withNewMaterial(self, application_mock, container_registry_mock, reply_mock,
@patch("PyQt5.QtNetwork.QNetworkReply") device_mock):
def test__onGetRemoteMaterials_withNewMaterial(self, reply_mock, device_mock, application_mock,
container_registry_mock):
application_mock.getContainerRegistry.return_value = container_registry_mock application_mock.getContainerRegistry.return_value = container_registry_mock
device_mock.createFormPart.return_value = "_xXx_" device_mock.createFormPart.return_value = "_xXx_"
@ -195,7 +193,7 @@ class TestSendMaterialJob(TestCase):
reply_mock.attribute.return_value = 200 reply_mock.attribute.return_value = 200
reply_mock.readAll.return_value = QByteArray(json.dumps([self._REMOTE_MATERIAL_BLACK]).encode("ascii")) reply_mock.readAll.return_value = QByteArray(json.dumps([self._REMOTE_MATERIAL_BLACK]).encode("ascii"))
with mock.patch.object(CuraApplication, "getInstance", new = lambda: application_mock): with mock.patch.object(Application, "getInstance", new = lambda: application_mock):
job = SendMaterialJob(device_mock) job = SendMaterialJob(device_mock)
job._onGetRemoteMaterials(reply_mock) job._onGetRemoteMaterials(reply_mock)