diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000000..f4a4d0771a --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,12 @@ +image: registry.gitlab.com/ultimaker/cura/cura-build-environment:centos7 + +stages: + - build + +build-and-test: + stage: build + script: + - docker/build.sh + artifacts: + paths: + - build diff --git a/CMakeLists.txt b/CMakeLists.txt index be6c9d938e..ba427a745d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,11 +1,10 @@ -project(cura NONE) -cmake_minimum_required(VERSION 2.8.12) - -set(CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake/ - ${CMAKE_MODULE_PATH}) +project(cura) +cmake_minimum_required(VERSION 3.6) include(GNUInstallDirs) +list(APPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake) + set(URANIUM_DIR "${CMAKE_SOURCE_DIR}/../Uranium" CACHE DIRECTORY "The location of the Uranium repository") set(URANIUM_SCRIPTS_DIR "${URANIUM_DIR}/scripts" CACHE DIRECTORY "The location of the scripts directory of the Uranium repository") @@ -28,6 +27,26 @@ set(CURA_CLOUD_API_VERSION "" CACHE STRING "Alternative Cura cloud API version") configure_file(${CMAKE_SOURCE_DIR}/cura.desktop.in ${CMAKE_BINARY_DIR}/cura.desktop @ONLY) configure_file(cura/CuraVersion.py.in CuraVersion.py @ONLY) + +# FIXME: Remove the code for CMake <3.12 once we have switched over completely. +# FindPython3 is a new module since CMake 3.12. It deprecates FindPythonInterp and FindPythonLibs. The FindPython3 +# module is copied from the CMake repository here so in CMake <3.12 we can still use it. +if(${CMAKE_VERSION} VERSION_LESS 3.12) + # Use FindPythonInterp and FindPythonLibs for CMake <3.12 + find_package(PythonInterp 3 REQUIRED) + + set(Python3_EXECUTABLE ${PYTHON_EXECUTABLE}) + + set(Python3_VERSION ${PYTHON_VERSION_STRING}) + set(Python3_VERSION_MAJOR ${PYTHON_VERSION_MAJOR}) + set(Python3_VERSION_MINOR ${PYTHON_VERSION_MINOR}) + set(Python3_VERSION_PATCH ${PYTHON_VERSION_PATCH}) +else() + # Use FindPython3 for CMake >=3.12 + find_package(Python3 REQUIRED COMPONENTS Interpreter Development) +endif() + + if(NOT ${URANIUM_DIR} STREQUAL "") set(CMAKE_MODULE_PATH "${URANIUM_DIR}/cmake") endif() @@ -40,12 +59,12 @@ if(NOT ${URANIUM_SCRIPTS_DIR} STREQUAL "") CREATE_TRANSLATION_TARGETS() endif() -find_package(PythonInterp 3.5.0 REQUIRED) install(DIRECTORY resources DESTINATION ${CMAKE_INSTALL_DATADIR}/cura) install(DIRECTORY plugins DESTINATION lib${LIB_SUFFIX}/cura) + if(NOT APPLE AND NOT WIN32) install(FILES cura_app.py DESTINATION ${CMAKE_INSTALL_BINDIR} @@ -53,16 +72,16 @@ if(NOT APPLE AND NOT WIN32) RENAME cura) if(EXISTS /etc/debian_version) install(DIRECTORY cura - DESTINATION lib${LIB_SUFFIX}/python${PYTHON_VERSION_MAJOR}/dist-packages + DESTINATION lib${LIB_SUFFIX}/python${Python3_VERSION_MAJOR}/dist-packages FILES_MATCHING PATTERN *.py) install(FILES ${CMAKE_BINARY_DIR}/CuraVersion.py - DESTINATION lib${LIB_SUFFIX}/python${PYTHON_VERSION_MAJOR}/dist-packages/cura) + DESTINATION lib${LIB_SUFFIX}/python${Python3_VERSION_MAJOR}/dist-packages/cura) else() install(DIRECTORY cura - DESTINATION lib${LIB_SUFFIX}/python${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR}/site-packages + DESTINATION lib${LIB_SUFFIX}/python${Python3_VERSION_MAJOR}.${Python3_VERSION_MINOR}/site-packages FILES_MATCHING PATTERN *.py) install(FILES ${CMAKE_BINARY_DIR}/CuraVersion.py - DESTINATION lib${LIB_SUFFIX}/python${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR}/site-packages/cura) + DESTINATION lib${LIB_SUFFIX}/python${Python3_VERSION_MAJOR}.${Python3_VERSION_MINOR}/site-packages/cura) endif() install(FILES ${CMAKE_BINARY_DIR}/cura.desktop DESTINATION ${CMAKE_INSTALL_DATADIR}/applications) @@ -78,8 +97,8 @@ else() DESTINATION ${CMAKE_INSTALL_BINDIR} PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE) install(DIRECTORY cura - DESTINATION lib${LIB_SUFFIX}/python${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR}/site-packages + DESTINATION lib${LIB_SUFFIX}/python${Python3_VERSION_MAJOR}.${Python3_VERSION_MINOR}/site-packages FILES_MATCHING PATTERN *.py) install(FILES ${CMAKE_BINARY_DIR}/CuraVersion.py - DESTINATION lib${LIB_SUFFIX}/python${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR}/site-packages/cura) + DESTINATION lib${LIB_SUFFIX}/python${Python3_VERSION_MAJOR}.${Python3_VERSION_MINOR}/site-packages/cura) endif() diff --git a/cmake/CuraTests.cmake b/cmake/CuraTests.cmake index b6d04de036..c0762e2b91 100644 --- a/cmake/CuraTests.cmake +++ b/cmake/CuraTests.cmake @@ -1,10 +1,21 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -enable_testing() +include(CTest) include(CMakeParseArguments) -find_package(PythonInterp 3.5.0 REQUIRED) +# FIXME: Remove the code for CMake <3.12 once we have switched over completely. +# FindPython3 is a new module since CMake 3.12. It deprecates FindPythonInterp and FindPythonLibs. The FindPython3 +# module is copied from the CMake repository here so in CMake <3.12 we can still use it. +if(${CMAKE_VERSION} VERSION_LESS 3.12) + # Use FindPythonInterp and FindPythonLibs for CMake <3.12 + find_package(PythonInterp 3 REQUIRED) + + set(Python3_EXECUTABLE ${PYTHON_EXECUTABLE}) +else() + # Use FindPython3 for CMake >=3.12 + find_package(Python3 REQUIRED COMPONENTS Interpreter Development) +endif() add_custom_target(test-verbose COMMAND ${CMAKE_CTEST_COMMAND} --verbose) @@ -36,7 +47,7 @@ function(cura_add_test) if (NOT ${test_exists}) add_test( NAME ${_NAME} - COMMAND ${PYTHON_EXECUTABLE} -m pytest --verbose --full-trace --capture=no --no-print-log --junitxml=${CMAKE_BINARY_DIR}/junit-${_NAME}.xml ${_DIRECTORY} + COMMAND ${Python3_EXECUTABLE} -m pytest --verbose --full-trace --capture=no --no-print-log --junitxml=${CMAKE_BINARY_DIR}/junit-${_NAME}.xml ${_DIRECTORY} ) set_tests_properties(${_NAME} PROPERTIES ENVIRONMENT LANG=C) set_tests_properties(${_NAME} PROPERTIES ENVIRONMENT "PYTHONPATH=${_PYTHONPATH}") @@ -59,13 +70,13 @@ endforeach() #Add code style test. add_test( NAME "code-style" - COMMAND ${PYTHON_EXECUTABLE} run_mypy.py + COMMAND ${Python3_EXECUTABLE} run_mypy.py WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} ) #Add test for whether the shortcut alt-keys are unique in every translation. add_test( NAME "shortcut-keys" - COMMAND ${PYTHON_EXECUTABLE} scripts/check_shortcut_keys.py + COMMAND ${Python3_EXECUTABLE} scripts/check_shortcut_keys.py WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} -) \ No newline at end of file +) diff --git a/cura/OAuth2/AuthorizationHelpers.py b/cura/OAuth2/AuthorizationHelpers.py index f4a29962a4..08309fa30e 100644 --- a/cura/OAuth2/AuthorizationHelpers.py +++ b/cura/OAuth2/AuthorizationHelpers.py @@ -50,6 +50,7 @@ class AuthorizationHelpers: # \param refresh_token: # \return An AuthenticationResponse object. def getAccessTokenUsingRefreshToken(self, refresh_token: str) -> "AuthenticationResponse": + Logger.log("d", "Refreshing the access token.") data = { "client_id": self._settings.CLIENT_ID if self._settings.CLIENT_ID is not None else "", "redirect_uri": self._settings.CALLBACK_URL if self._settings.CALLBACK_URL is not None else "", diff --git a/cura/OAuth2/AuthorizationService.py b/cura/OAuth2/AuthorizationService.py index 1f20f2d87f..d721945b79 100644 --- a/cura/OAuth2/AuthorizationService.py +++ b/cura/OAuth2/AuthorizationService.py @@ -68,6 +68,7 @@ class AuthorizationService: self._user_profile = self._parseJWT() except requests.exceptions.ConnectionError: # Unable to get connection, can't login. + Logger.logException("w", "Unable to validate user data with the remote server.") return None if not self._user_profile and self._auth_data: @@ -83,6 +84,7 @@ class AuthorizationService: def _parseJWT(self) -> Optional["UserProfile"]: if not self._auth_data or self._auth_data.access_token is None: # If no auth data exists, we should always log in again. + Logger.log("d", "There was no auth data or access token") return None user_data = self._auth_helpers.parseJWT(self._auth_data.access_token) if user_data: @@ -90,9 +92,11 @@ class AuthorizationService: return user_data # The JWT was expired or invalid and we should request a new one. if self._auth_data.refresh_token is None: + Logger.log("w", "There was no refresh token in the auth data.") return None self._auth_data = self._auth_helpers.getAccessTokenUsingRefreshToken(self._auth_data.refresh_token) if not self._auth_data or self._auth_data.access_token is None: + Logger.log("w", "Unable to use the refresh token to get a new access token.") # The token could not be refreshed using the refresh token. We should login again. return None diff --git a/docker/build.sh b/docker/build.sh new file mode 100755 index 0000000000..88fec8b869 --- /dev/null +++ b/docker/build.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash + +# Abort at the first error. +set -e + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +PROJECT_DIR="$( cd "${SCRIPT_DIR}/.." && pwd )" + +# Make sure that environment variables are set properly +source /opt/rh/devtoolset-7/enable +export PATH="${CURA_BUILD_ENV_PATH}/bin:${PATH}" +export PKG_CONFIG_PATH="${CURA_BUILD_ENV_PATH}/lib/pkgconfig:${PKG_CONFIG_PATH}" + +cd "${PROJECT_DIR}" + +# +# Clone Uranium and set PYTHONPATH first +# + +# Check the branch to use: +# 1. Use the Uranium branch with the branch same if it exists. +# 2. Otherwise, use the default branch name "master" +URANIUM_BRANCH="${CI_COMMIT_REF_NAME:-master}" +output="$(git ls-remote --heads https://github.com/Ultimaker/Uranium.git "${URANIUM_BRANCH}")" +if [ -z "${output}" ]; then + echo "Could not find Uranium banch ${URANIUM_BRANCH}, fallback to use master." + URANIUM_BRANCH="master" +fi + +echo "Using Uranium branch ${URANIUM_BRANCH} ..." +git clone --depth=1 -b "${URANIUM_BRANCH}" https://github.com/Ultimaker/Uranium.git "${PROJECT_DIR}"/Uranium +export PYTHONPATH="${PROJECT_DIR}/Uranium:.:${PYTHONPATH}" + +mkdir build +cd build +cmake3 \ + -DCMAKE_BUILD_TYPE=Debug \ + -DCMAKE_PREFIX_PATH="${CURA_BUILD_ENV_PATH}" \ + -DURANIUM_DIR="${PROJECT_DIR}/Uranium" \ + -DBUILD_TESTS=ON \ + .. +make +ctest3 --verbose --output-on-failure -T Test diff --git a/plugins/GCodeReader/FlavorParser.py b/plugins/GCodeReader/FlavorParser.py index f8618712a1..4c88e5c953 100644 --- a/plugins/GCodeReader/FlavorParser.py +++ b/plugins/GCodeReader/FlavorParser.py @@ -1,31 +1,33 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. +import math +import re +from typing import Dict, List, NamedTuple, Optional, Union + +import numpy + from UM.Backend import Backend from UM.Job import Job from UM.Logger import Logger from UM.Math.Vector import Vector from UM.Message import Message -from cura.Scene.CuraSceneNode import CuraSceneNode from UM.i18n import i18nCatalog -catalog = i18nCatalog("cura") - from cura.CuraApplication import CuraApplication from cura.LayerDataBuilder import LayerDataBuilder from cura.LayerDataDecorator import LayerDataDecorator from cura.LayerPolygon import LayerPolygon +from cura.Scene.CuraSceneNode import CuraSceneNode from cura.Scene.GCodeListDecorator import GCodeListDecorator from cura.Settings.ExtruderManager import ExtruderManager -import numpy -import math -import re -from typing import Dict, List, NamedTuple, Optional, Union +catalog = i18nCatalog("cura") PositionOptional = NamedTuple("Position", [("x", Optional[float]), ("y", Optional[float]), ("z", Optional[float]), ("f", Optional[float]), ("e", Optional[float])]) Position = NamedTuple("Position", [("x", float), ("y", float), ("z", float), ("f", float), ("e", List[float])]) + ## This parser is intended to interpret the common firmware codes among all the # different flavors class FlavorParser: @@ -33,7 +35,7 @@ class FlavorParser: def __init__(self) -> None: CuraApplication.getInstance().hideMessageSignal.connect(self._onHideMessage) self._cancelled = False - self._message = None + self._message = None # type: Optional[Message] self._layer_number = 0 self._extruder_number = 0 self._clearValues() @@ -425,7 +427,8 @@ class FlavorParser: if line.startswith("M"): M = self._getInt(line, "M") - self.processMCode(M, line, current_position, current_path) + if M is not None: + self.processMCode(M, line, current_position, current_path) # "Flush" leftovers. Last layer paths are still stored if len(current_path) > 1: @@ -463,7 +466,7 @@ class FlavorParser: Logger.log("w", "File doesn't contain any valid layers") settings = CuraApplication.getInstance().getGlobalContainerStack() - if not settings.getProperty("machine_center_is_zero", "value"): + if settings is not None and not settings.getProperty("machine_center_is_zero", "value"): machine_width = settings.getProperty("machine_width", "value") machine_depth = settings.getProperty("machine_depth", "value") scene_node.setPosition(Vector(-machine_width / 2, 0, machine_depth / 2)) diff --git a/plugins/PostProcessingPlugin/PostProcessingPlugin.py b/plugins/PostProcessingPlugin/PostProcessingPlugin.py index 78f9cc0516..123733b863 100644 --- a/plugins/PostProcessingPlugin/PostProcessingPlugin.py +++ b/plugins/PostProcessingPlugin/PostProcessingPlugin.py @@ -162,7 +162,7 @@ class PostProcessingPlugin(QObject, Extension): loaded_script = importlib.util.module_from_spec(spec) if spec.loader is None: continue - spec.loader.exec_module(loaded_script) + spec.loader.exec_module(loaded_script) # type: ignore sys.modules[script_name] = loaded_script #TODO: This could be a security risk. Overwrite any module with a user-provided name? loaded_class = getattr(loaded_script, script_name) diff --git a/plugins/UFPReader/UFPReader.py b/plugins/UFPReader/UFPReader.py index 275726b25b..18527e6450 100644 --- a/plugins/UFPReader/UFPReader.py +++ b/plugins/UFPReader/UFPReader.py @@ -38,5 +38,5 @@ class UFPReader(MeshReader): # Open the GCodeReader to parse the data gcode_reader = PluginRegistry.getInstance().getPluginObject("GCodeReader") # type: ignore - gcode_reader.preReadFromStream(gcode_stream) - return gcode_reader.readFromStream(gcode_stream) + gcode_reader.preReadFromStream(gcode_stream) # type: ignore + return gcode_reader.readFromStream(gcode_stream) # type: ignore diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py index fb8e9d9408..43f0a71f6a 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py @@ -93,10 +93,11 @@ class CloudOutputDevice(NetworkedPrinterOutputDevice): # We use the Cura Connect monitor tab to get most functionality right away. if PluginRegistry.getInstance() is not None: - self._monitor_view_qml_path = os.path.join( - PluginRegistry.getInstance().getPluginPath("UM3NetworkPrinting"), - "resources", "qml", "MonitorStage.qml" - ) + plugin_path = PluginRegistry.getInstance().getPluginPath("UM3NetworkPrinting") + if plugin_path is None: + Logger.log("e", "Cloud not find plugin path for plugin UM3NetworkPrnting") + raise RuntimeError("Cloud not find plugin path for plugin UM3NetworkPrnting") + self._monitor_view_qml_path = os.path.join(plugin_path, "resources", "qml", "MonitorStage.qml") # Trigger the printersChanged signal when the private signal is triggered. self.printersChanged.connect(self._clusterPrintersChanged) @@ -158,7 +159,7 @@ class CloudOutputDevice(NetworkedPrinterOutputDevice): # the host name should then be "ultimakersystem-aabbccdd0011" if network_key.startswith(self.clusterData.host_name): return True - + # However, for manually added printers, the local IP address is used in lieu of a proper # network key, so check for that as well if self.clusterData.host_internal_ip is not None and network_key.find(self.clusterData.host_internal_ip): diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py index 19ec34a6bb..498e141b73 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py @@ -32,8 +32,8 @@ class CloudOutputDeviceManager: # The translation catalog for this device. I18N_CATALOG = i18nCatalog("cura") - addedCloudCluster = Signal(CloudOutputDevice) - removedCloudCluster = Signal(CloudOutputDevice) + addedCloudCluster = Signal() + removedCloudCluster = Signal() def __init__(self) -> None: # Persistent dict containing the remote clusters for the authenticated user. diff --git a/plugins/UM3NetworkPrinting/src/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/src/ClusterUM3OutputDevice.py index 2e3b51546b..d2e18fcb75 100644 --- a/plugins/UM3NetworkPrinting/src/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/ClusterUM3OutputDevice.py @@ -66,10 +66,11 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): self._received_print_jobs = False # type: bool if PluginRegistry.getInstance() is not None: - self._monitor_view_qml_path = os.path.join( - PluginRegistry.getInstance().getPluginPath("UM3NetworkPrinting"), - "resources", "qml", "MonitorStage.qml" - ) + plugin_path = PluginRegistry.getInstance().getPluginPath("UM3NetworkPrinting") + if plugin_path is None: + Logger.log("e", "Cloud not find plugin path for plugin UM3NetworkPrnting") + raise RuntimeError("Cloud not find plugin path for plugin UM3NetworkPrnting") + self._monitor_view_qml_path = os.path.join(plugin_path, "resources", "qml", "MonitorStage.qml") # Trigger the printersChanged signal when the private signal is triggered self.printersChanged.connect(self._clusterPrintersChanged) diff --git a/tests/Machines/Models/TestDiscoveredPrintersModel.py b/tests/Machines/Models/TestDiscoveredPrintersModel.py index 4ccc3ba523..28be2536a0 100644 --- a/tests/Machines/Models/TestDiscoveredPrintersModel.py +++ b/tests/Machines/Models/TestDiscoveredPrintersModel.py @@ -1,4 +1,4 @@ -from unittest.mock import MagicMock +from unittest.mock import MagicMock, PropertyMock import pytest @@ -12,6 +12,8 @@ def discovered_printer_model(application) -> DiscoveredPrintersModel: def test_discoveredPrinters(discovered_printer_model): mocked_device = MagicMock() + cluster_size = PropertyMock(return_value = 1) + type(mocked_device).clusterSize = cluster_size mocked_callback = MagicMock() discovered_printer_model.addDiscoveredPrinter("ip", "key", "name", mocked_callback, "machine_type", mocked_device)