diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py
index d58def4545..c6fb02753f 100644
--- a/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py
+++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py
@@ -52,7 +52,7 @@ class CloudApiClient:
def getClusters(self, on_finished: Callable[[List[CloudClusterResponse]], Any]) -> None:
url = "{}/clusters".format(self.CLUSTER_API_ROOT)
reply = self._manager.get(self._createEmptyRequest(url))
- self._addCallbacks(reply, on_finished, CloudClusterResponse)
+ self._addCallback(reply, on_finished, CloudClusterResponse)
## Retrieves the status of the given cluster.
# \param cluster_id: The ID of the cluster.
@@ -60,7 +60,7 @@ class CloudApiClient:
def getClusterStatus(self, cluster_id: str, on_finished: Callable[[CloudClusterStatus], Any]) -> None:
url = "{}/clusters/{}/status".format(self.CLUSTER_API_ROOT, cluster_id)
reply = self._manager.get(self._createEmptyRequest(url))
- self._addCallbacks(reply, on_finished, CloudClusterStatus)
+ self._addCallback(reply, on_finished, CloudClusterStatus)
## Requests the cloud to register the upload of a print job mesh.
# \param request: The request object.
@@ -70,7 +70,7 @@ class CloudApiClient:
url = "{}/jobs/upload".format(self.CURA_API_ROOT)
body = json.dumps({"data": request.toDict()})
reply = self._manager.put(self._createEmptyRequest(url), body.encode())
- self._addCallbacks(reply, on_finished, CloudPrintJobResponse)
+ self._addCallback(reply, on_finished, CloudPrintJobResponse)
## Requests the cloud to register the upload of a print job mesh.
# \param upload_response: The object received after requesting an upload with `self.requestUpload`.
@@ -90,7 +90,7 @@ class CloudApiClient:
def requestPrint(self, cluster_id: str, job_id: str, on_finished: Callable[[CloudPrintResponse], Any]) -> None:
url = "{}/clusters/{}/print/{}".format(self.CLUSTER_API_ROOT, cluster_id, job_id)
reply = self._manager.post(self._createEmptyRequest(url), b"")
- self._addCallbacks(reply, on_finished, CloudPrintResponse)
+ self._addCallback(reply, on_finished, CloudPrintResponse)
## We override _createEmptyRequest in order to add the user credentials.
# \param url: The URL to request
@@ -147,7 +147,7 @@ class CloudApiClient:
# \param on_finished: The callback in case the response is successful.
# \param model: The type of the model to convert the response to. It may either be a single record or a list.
# \return: A function that can be passed to the
- def _addCallbacks(self,
+ def _addCallback(self,
reply: QNetworkReply,
on_finished: Union[Callable[[Model], Any], Callable[[List[Model]], Any]],
model: Type[Model],
diff --git a/plugins/UM3NetworkPrinting/tests/Cloud/TestCloudApiClient.py b/plugins/UM3NetworkPrinting/tests/Cloud/TestCloudApiClient.py
index 49d2b1b34b..0c0c8cffdf 100644
--- a/plugins/UM3NetworkPrinting/tests/Cloud/TestCloudApiClient.py
+++ b/plugins/UM3NetworkPrinting/tests/Cloud/TestCloudApiClient.py
@@ -6,7 +6,6 @@ from typing import List
from unittest import TestCase
from unittest.mock import patch, MagicMock
-from cura.CuraApplication import CuraApplication
from cura.CuraConstants import CuraCloudAPIRoot
from src.Cloud.CloudApiClient import CloudApiClient
from src.Cloud.Models.CloudClusterResponse import CloudClusterResponse
@@ -29,7 +28,6 @@ class TestCloudApiClient(TestCase):
self.account = MagicMock()
self.account.isLoggedIn.return_value = True
- self.app = CuraApplication.getInstance()
self.network = NetworkManagerMock()
with patch("src.Cloud.CloudApiClient.QNetworkAccessManager", return_value = self.network):
self.api = CloudApiClient(self.account, self._errorHandler)
diff --git a/plugins/UM3NetworkPrinting/tests/Cloud/TestCloudOutputDevice.py b/plugins/UM3NetworkPrinting/tests/Cloud/TestCloudOutputDevice.py
index 90a1b2fa96..34e04689c2 100644
--- a/plugins/UM3NetworkPrinting/tests/Cloud/TestCloudOutputDevice.py
+++ b/plugins/UM3NetworkPrinting/tests/Cloud/TestCloudOutputDevice.py
@@ -30,9 +30,13 @@ class TestCloudOutputDevice(TestCase):
def setUp(self):
super().setUp()
- self.app = CuraApplication.getInstance()
- self.backend = MagicMock(backendStateChange = Signal())
- self.app.setBackend(self.backend)
+ self.app = MagicMock()
+
+ self.patches = [patch("UM.Qt.QtApplication.QtApplication.getInstance", return_value=self.app),
+ patch("UM.Application.Application.getInstance", return_value=self.app)]
+ for patched_method in self.patches:
+ patched_method.start()
+
self.cluster = CloudClusterResponse(self.CLUSTER_ID, self.HOST_GUID, self.HOST_NAME, is_online=True,
status="active")
@@ -41,6 +45,7 @@ class TestCloudOutputDevice(TestCase):
self.onError = MagicMock()
with patch("src.Cloud.CloudApiClient.QNetworkAccessManager", return_value = self.network):
self._api = CloudApiClient(self.account, self.onError)
+
self.device = CloudOutputDevice(self._api, self.cluster)
self.cluster_status = parseFixture("getClusterStatusResponse")
self.network.prepareReply("GET", self.STATUS_URL, 200, readFixture("getClusterStatusResponse"))
@@ -48,6 +53,8 @@ class TestCloudOutputDevice(TestCase):
def tearDown(self):
super().tearDown()
self.network.flushReplies()
+ for patched_method in self.patches:
+ patched_method.stop()
def test_status(self):
self.device._update()
@@ -105,9 +112,8 @@ class TestCloudOutputDevice(TestCase):
self.network.flushReplies()
self.assertEqual([], self.device.printers)
- @patch("cura.CuraApplication.CuraApplication.getGlobalContainerStack")
- def test_print_to_cloud(self, global_container_stack_mock):
- active_machine_mock = global_container_stack_mock.return_value
+ def test_print_to_cloud(self):
+ active_machine_mock = self.app.getGlobalContainerStack.return_value
active_machine_mock.getMetaDataEntry.side_effect = {"file_formats": "application/gzip"}.get
request_upload_response = parseFixture("putJobUploadResponse")
diff --git a/plugins/UM3NetworkPrinting/tests/Cloud/TestCloudOutputDeviceManager.py b/plugins/UM3NetworkPrinting/tests/Cloud/TestCloudOutputDeviceManager.py
index 01b1b18ff1..96137a3edb 100644
--- a/plugins/UM3NetworkPrinting/tests/Cloud/TestCloudOutputDeviceManager.py
+++ b/plugins/UM3NetworkPrinting/tests/Cloud/TestCloudOutputDeviceManager.py
@@ -1,14 +1,14 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from unittest import TestCase
-from unittest.mock import patch
+from unittest.mock import patch, MagicMock
-from cura.CuraApplication import CuraApplication
+from UM.OutputDevice.OutputDeviceManager import OutputDeviceManager
from cura.CuraConstants import CuraCloudAPIRoot
from src.Cloud.CloudOutputDevice import CloudOutputDevice
from src.Cloud.CloudOutputDeviceManager import CloudOutputDeviceManager
from tests.Cloud.Fixtures import parseFixture, readFixture
-from .NetworkManagerMock import NetworkManagerMock
+from .NetworkManagerMock import NetworkManagerMock, FakeSignal
class TestCloudOutputDeviceManager(TestCase):
@@ -18,9 +18,19 @@ class TestCloudOutputDeviceManager(TestCase):
def setUp(self):
super().setUp()
- self.app = CuraApplication.getInstance()
+ self.app = MagicMock()
+ self.device_manager = OutputDeviceManager()
+ self.app.getOutputDeviceManager.return_value = self.device_manager
+
+ self.patches = [patch("UM.Qt.QtApplication.QtApplication.getInstance", return_value=self.app),
+ patch("UM.Application.Application.getInstance", return_value=self.app)]
+ for patched_method in self.patches:
+ patched_method.start()
+
self.network = NetworkManagerMock()
- with patch("src.Cloud.CloudApiClient.QNetworkAccessManager", return_value = self.network):
+ self.timer = MagicMock(timeout = FakeSignal())
+ with patch("src.Cloud.CloudApiClient.QNetworkAccessManager", return_value = self.network), \
+ patch("src.Cloud.CloudOutputDeviceManager.QTimer", return_value = self.timer):
self.manager = CloudOutputDeviceManager()
self.clusters_response = parseFixture("getClusters")
self.network.prepareReply("GET", self.URL, 200, readFixture("getClusters"))
@@ -28,7 +38,11 @@ class TestCloudOutputDeviceManager(TestCase):
def tearDown(self):
try:
self._beforeTearDown()
+
+ self.network.flushReplies()
self.manager.stop()
+ for patched_method in self.patches:
+ patched_method.stop()
finally:
super().tearDown()
@@ -38,8 +52,7 @@ class TestCloudOutputDeviceManager(TestCase):
# let the network send replies
self.network.flushReplies()
# get the created devices
- device_manager = self.app.getOutputDeviceManager()
- devices = device_manager.getOutputDevices()
+ devices = self.device_manager.getOutputDevices()
# get the server data
clusters = self.clusters_response.get("data", [])
self.assertEqual([CloudOutputDevice] * len(clusters), [type(d) for d in devices])
@@ -48,13 +61,12 @@ class TestCloudOutputDeviceManager(TestCase):
key=lambda device_dict: device_dict["host_version"]))
for device in clusters:
- device_manager.getOutputDevice(device["cluster_id"]).close()
- device_manager.removeOutputDevice(device["cluster_id"])
+ self.device_manager.getOutputDevice(device["cluster_id"]).close()
+ self.device_manager.removeOutputDevice(device["cluster_id"])
## Runs the initial request to retrieve the clusters.
def _loadData(self):
self.manager.start()
- self.manager._onLoginStateChanged(is_logged_in = True)
self.network.flushReplies()
def test_device_is_created(self):
@@ -79,22 +91,20 @@ class TestCloudOutputDeviceManager(TestCase):
self.manager._update_timer.timeout.emit()
- @patch("cura.CuraApplication.CuraApplication.getGlobalContainerStack")
- def test_device_connects_by_cluster_id(self, global_container_stack_mock):
- active_machine_mock = global_container_stack_mock.return_value
+ def test_device_connects_by_cluster_id(self):
+ active_machine_mock = self.app.getGlobalContainerStack.return_value
cluster1, cluster2 = self.clusters_response["data"]
cluster_id = cluster1["cluster_id"]
active_machine_mock.getMetaDataEntry.side_effect = {"um_cloud_cluster_id": cluster_id}.get
self._loadData()
- self.assertTrue(self.app.getOutputDeviceManager().getOutputDevice(cluster1["cluster_id"]).isConnected())
- self.assertFalse(self.app.getOutputDeviceManager().getOutputDevice(cluster2["cluster_id"]).isConnected())
+ self.assertTrue(self.device_manager.getOutputDevice(cluster1["cluster_id"]).isConnected())
+ self.assertFalse(self.device_manager.getOutputDevice(cluster2["cluster_id"]).isConnected())
self.assertEquals([], active_machine_mock.setMetaDataEntry.mock_calls)
- @patch("cura.CuraApplication.CuraApplication.getGlobalContainerStack")
- def test_device_connects_by_network_key(self, global_container_stack_mock):
- active_machine_mock = global_container_stack_mock.return_value
+ def test_device_connects_by_network_key(self):
+ active_machine_mock = self.app.getGlobalContainerStack.return_value
cluster1, cluster2 = self.clusters_response["data"]
network_key = cluster2["host_name"] + ".ultimaker.local"
@@ -102,8 +112,9 @@ class TestCloudOutputDeviceManager(TestCase):
self._loadData()
- self.assertFalse(self.app.getOutputDeviceManager().getOutputDevice(cluster1["cluster_id"]).isConnected())
- self.assertTrue(self.app.getOutputDeviceManager().getOutputDevice(cluster2["cluster_id"]).isConnected())
+ self.assertEqual([False, True],
+ [self.device_manager.getOutputDevice(cluster["cluster_id"]).isConnected()
+ for cluster in (cluster1, cluster2)])
active_machine_mock.setMetaDataEntry.assert_called_with("um_cloud_cluster_id", cluster2["cluster_id"])
diff --git a/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py b/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py
new file mode 100644
index 0000000000..b669eb192a
--- /dev/null
+++ b/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py
@@ -0,0 +1,190 @@
+# Copyright (c) 2018 Ultimaker B.V.
+# Copyright (c) 2018 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+import io
+import json
+from unittest import TestCase, mock
+from unittest.mock import patch, call
+
+from PyQt5.QtCore import QByteArray
+
+from UM.MimeTypeDatabase import MimeType
+from UM.Application import Application
+from plugins.UM3NetworkPrinting.src.SendMaterialJob import SendMaterialJob
+
+
+@patch("builtins.open", lambda _, __: io.StringIO(""))
+@patch("UM.MimeTypeDatabase.MimeTypeDatabase.getMimeTypeForFile",
+ lambda _: MimeType(name = "application/x-ultimaker-material-profile", comment = "Ultimaker Material Profile",
+ suffixes = ["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):
+ _LOCAL_MATERIAL_WHITE = {"type": "material", "status": "unknown", "id": "generic_pla_white",
+ "base_file": "generic_pla_white", "setting_version": "5", "name": "White PLA",
+ "brand": "Generic", "material": "PLA", "color_name": "White",
+ "GUID": "badb0ee7-87c8-4f3f-9398-938587b67dce", "version": "1", "color_code": "#ffffff",
+ "description": "Test PLA White", "adhesion_info": "Use glue.", "approximate_diameter": "3",
+ "properties": {"density": "1.00", "diameter": "2.85", "weight": "750"},
+ "definition": "fdmprinter", "compatible": True}
+
+ _LOCAL_MATERIAL_BLACK = {"type": "material", "status": "unknown", "id": "generic_pla_black",
+ "base_file": "generic_pla_black", "setting_version": "5", "name": "Yellow CPE",
+ "brand": "Ultimaker", "material": "CPE", "color_name": "Black",
+ "GUID": "5fbb362a-41f9-4818-bb43-15ea6df34aa4", "version": "1", "color_code": "#000000",
+ "description": "Test PLA Black", "adhesion_info": "Use glue.", "approximate_diameter": "3",
+ "properties": {"density": "1.01", "diameter": "2.85", "weight": "750"},
+ "definition": "fdmprinter", "compatible": True}
+
+ _REMOTE_MATERIAL_WHITE = {
+ "guid": "badb0ee7-87c8-4f3f-9398-938587b67dce",
+ "material": "PLA",
+ "brand": "Generic",
+ "version": 1,
+ "color": "White",
+ "density": 1.00
+ }
+
+ _REMOTE_MATERIAL_BLACK = {
+ "guid": "5fbb362a-41f9-4818-bb43-15ea6df34aa4",
+ "material": "PLA",
+ "brand": "Generic",
+ "version": 2,
+ "color": "Black",
+ "density": 1.00
+ }
+
+ def test_run(self, device_mock, reply_mock):
+ job = SendMaterialJob(device_mock)
+ job.run()
+
+ # We expect the materials endpoint to be called when the job runs.
+ device_mock.get.assert_called_with("materials/", on_finished = job._onGetRemoteMaterials)
+
+ def test__onGetRemoteMaterials_withFailedRequest(self, reply_mock, device_mock):
+ reply_mock.attribute.return_value = 404
+ job = SendMaterialJob(device_mock)
+ job._onGetRemoteMaterials(reply_mock)
+
+ # We expect the device not to be called for any follow up.
+ self.assertEqual(0, device_mock.createFormPart.call_count)
+
+ def test__onGetRemoteMaterials_withWrongEncoding(self, reply_mock, device_mock):
+ 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)
+
+ # Given that the parsing fails we do no expect the device to be called for any follow up.
+ self.assertEqual(0, device_mock.createFormPart.call_count)
+
+ def test__onGetRemoteMaterials_withBadJsonAnswer(self, reply_mock, device_mock):
+ reply_mock.attribute.return_value = 200
+ reply_mock.readAll.return_value = QByteArray(b"Six sick hicks nick six slick bricks with picks and sticks.")
+ job = SendMaterialJob(device_mock)
+ job._onGetRemoteMaterials(reply_mock)
+
+ # Given that the parsing fails we do no expect the device to be called for any follow up.
+ self.assertEqual(0, device_mock.createFormPart.call_count)
+
+ def test__onGetRemoteMaterials_withMissingGuidInRemoteMaterial(self, reply_mock, device_mock):
+ reply_mock.attribute.return_value = 200
+ remote_material_without_guid = self._REMOTE_MATERIAL_WHITE.copy()
+ del remote_material_without_guid["guid"]
+ reply_mock.readAll.return_value = QByteArray(json.dumps([remote_material_without_guid]).encode("ascii"))
+ job = SendMaterialJob(device_mock)
+ job._onGetRemoteMaterials(reply_mock)
+
+ # Given that parsing fails we do not expect the device to be called for any follow up.
+ self.assertEqual(0, device_mock.createFormPart.call_count)
+
+ @patch("cura.Settings.CuraContainerRegistry")
+ @patch("UM.Application")
+ def test__onGetRemoteMaterials_withInvalidVersionInLocalMaterial(self, application_mock, container_registry_mock,
+ reply_mock, device_mock):
+ reply_mock.attribute.return_value = 200
+ reply_mock.readAll.return_value = QByteArray(json.dumps([self._REMOTE_MATERIAL_WHITE]).encode("ascii"))
+
+ localMaterialWhiteWithInvalidVersion = self._LOCAL_MATERIAL_WHITE.copy()
+ localMaterialWhiteWithInvalidVersion["version"] = "one"
+ container_registry_mock.findContainersMetadata.return_value = [localMaterialWhiteWithInvalidVersion]
+
+ application_mock.getContainerRegistry.return_value = container_registry_mock
+
+ with mock.patch.object(Application, "getInstance", new = lambda: application_mock):
+ job = SendMaterialJob(device_mock)
+ job._onGetRemoteMaterials(reply_mock)
+
+ self.assertEqual(0, device_mock.createFormPart.call_count)
+
+ @patch("cura.Settings.CuraContainerRegistry")
+ @patch("UM.Application")
+ def test__onGetRemoteMaterials_withNoUpdate(self, application_mock, container_registry_mock, reply_mock,
+ device_mock):
+ application_mock.getContainerRegistry.return_value = container_registry_mock
+
+ device_mock.createFormPart.return_value = "_xXx_"
+
+ container_registry_mock.findContainersMetadata.return_value = [self._LOCAL_MATERIAL_WHITE]
+
+ reply_mock.attribute.return_value = 200
+ reply_mock.readAll.return_value = QByteArray(json.dumps([self._REMOTE_MATERIAL_WHITE]).encode("ascii"))
+
+ with mock.patch.object(Application, "getInstance", new = lambda: application_mock):
+ job = SendMaterialJob(device_mock)
+ job._onGetRemoteMaterials(reply_mock)
+
+ self.assertEqual(0, device_mock.createFormPart.call_count)
+ self.assertEqual(0, device_mock.postFormWithParts.call_count)
+
+ @patch("cura.Settings.CuraContainerRegistry")
+ @patch("UM.Application")
+ def test__onGetRemoteMaterials_withUpdatedMaterial(self, application_mock, container_registry_mock, reply_mock,
+ device_mock):
+ application_mock.getContainerRegistry.return_value = container_registry_mock
+
+ device_mock.createFormPart.return_value = "_xXx_"
+
+ localMaterialWhiteWithHigherVersion = self._LOCAL_MATERIAL_WHITE.copy()
+ localMaterialWhiteWithHigherVersion["version"] = "2"
+ container_registry_mock.findContainersMetadata.return_value = [localMaterialWhiteWithHigherVersion]
+
+ reply_mock.attribute.return_value = 200
+ reply_mock.readAll.return_value = QByteArray(json.dumps([self._REMOTE_MATERIAL_WHITE]).encode("ascii"))
+
+ with mock.patch.object(Application, "getInstance", new = lambda: application_mock):
+ job = SendMaterialJob(device_mock)
+ job._onGetRemoteMaterials(reply_mock)
+
+ self.assertEqual(1, device_mock.createFormPart.call_count)
+ self.assertEqual(1, device_mock.postFormWithParts.call_count)
+ self.assertEquals(
+ [call.createFormPart("name=\"file\"; filename=\"generic_pla_white.xml.fdm_material\"", ""),
+ call.postFormWithParts(target = "materials/", parts = ["_xXx_"], on_finished = job.sendingFinished)],
+ device_mock.method_calls)
+
+ @patch("cura.Settings.CuraContainerRegistry")
+ @patch("UM.Application")
+ def test__onGetRemoteMaterials_withNewMaterial(self, application_mock, container_registry_mock, reply_mock,
+ device_mock):
+ application_mock.getContainerRegistry.return_value = container_registry_mock
+
+ device_mock.createFormPart.return_value = "_xXx_"
+
+ container_registry_mock.findContainersMetadata.return_value = [self._LOCAL_MATERIAL_WHITE,
+ self._LOCAL_MATERIAL_BLACK]
+
+ reply_mock.attribute.return_value = 200
+ reply_mock.readAll.return_value = QByteArray(json.dumps([self._REMOTE_MATERIAL_BLACK]).encode("ascii"))
+
+ with mock.patch.object(Application, "getInstance", new = lambda: application_mock):
+ job = SendMaterialJob(device_mock)
+ job._onGetRemoteMaterials(reply_mock)
+
+ self.assertEqual(1, device_mock.createFormPart.call_count)
+ self.assertEqual(1, device_mock.postFormWithParts.call_count)
+ self.assertEquals(
+ [call.createFormPart("name=\"file\"; filename=\"generic_pla_white.xml.fdm_material\"", ""),
+ call.postFormWithParts(target = "materials/", parts = ["_xXx_"], on_finished = job.sendingFinished)],
+ device_mock.method_calls)
diff --git a/plugins/UM3NetworkPrinting/tests/conftest.py b/plugins/UM3NetworkPrinting/tests/conftest.py
deleted file mode 100644
index ce49bd3cb7..0000000000
--- a/plugins/UM3NetworkPrinting/tests/conftest.py
+++ /dev/null
@@ -1,40 +0,0 @@
-# Copyright (c) 2018 Ultimaker B.V.
-# Cura is released under the terms of the LGPLv3 or higher.
-
-import pytest
-from UM.Signal import Signal
-
-from cura.CuraApplication import CuraApplication
-from cura.Machines.MaterialManager import MaterialManager
-
-
-# This mock application must extend from Application and not QtApplication otherwise some QObjects are created and
-# a segfault is raised.
-class FixtureApplication(CuraApplication):
- def __init__(self):
- super().__init__()
- super().initialize()
- Signal._signalQueue = self
-
- self.getPreferences().addPreference("cura/favorite_materials", "")
-
- self._material_manager = MaterialManager(self._container_registry, parent = self)
- self._material_manager.initialize()
-
- def functionEvent(self, event):
- event.call()
-
- def parseCommandLine(self):
- pass
-
- def processEvents(self):
- pass
-
-
-@pytest.fixture(autouse=True)
-def application():
- # Since we need to use it more that once, we create the application the first time and use its instance the second
- application = FixtureApplication.getInstance()
- if application is None:
- application = FixtureApplication()
- return application