TestDefinitionContainer.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. # Copyright (c) 2019 Ultimaker B.V.
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. import json # To check files for unnecessarily overridden properties.
  4. import os
  5. import pytest #This module contains automated tests.
  6. from typing import Any, Dict
  7. import uuid
  8. from unittest.mock import patch, MagicMock
  9. import UM.Settings.ContainerRegistry #To create empty instance containers.
  10. import UM.Settings.ContainerStack #To set the container registry the container stacks use.
  11. from UM.Settings.DefinitionContainer import DefinitionContainer #To check against the class of DefinitionContainer.
  12. from UM.VersionUpgradeManager import FilesDataUpdateResult
  13. from UM.Resources import Resources
  14. Resources.addSearchPath(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "resources")))
  15. machine_filepaths = sorted(os.listdir(os.path.join(os.path.dirname(__file__), "..", "..", "resources", "definitions")))
  16. machine_filepaths = [os.path.join(os.path.dirname(__file__), "..", "..", "resources", "definitions", filename) for filename in machine_filepaths]
  17. extruder_filepaths = sorted(os.listdir(os.path.join(os.path.dirname(__file__), "..", "..", "resources", "extruders")))
  18. extruder_filepaths = [os.path.join(os.path.dirname(__file__), "..", "..", "resources", "extruders", filename) for filename in extruder_filepaths]
  19. definition_filepaths = machine_filepaths + extruder_filepaths
  20. all_meshes = os.listdir(os.path.join(os.path.dirname(__file__), "..", "..", "resources", "meshes"))
  21. all_images = os.listdir(os.path.join(os.path.dirname(__file__), "..", "..", "resources", "images"))
  22. # Loading definition files needs a functioning ContainerRegistry
  23. cr = UM.Settings.ContainerRegistry.ContainerRegistry(None)
  24. @pytest.fixture
  25. def definition_container():
  26. uid = str(uuid.uuid4())
  27. result = UM.Settings.DefinitionContainer.DefinitionContainer(uid)
  28. assert result.getId() == uid
  29. return result
  30. @pytest.mark.parametrize("file_path", definition_filepaths)
  31. def test_definitionIds(file_path):
  32. """
  33. Test the validity of the definition IDs.
  34. :param file_path: The path of the machine definition to test.
  35. """
  36. definition_id = os.path.basename(file_path).split(".")[0]
  37. assert " " not in definition_id, "Definition located at [%s] contains spaces, this is now allowed!" % file_path # Definition IDs are not allowed to have spaces.
  38. @pytest.mark.parametrize("file_path", definition_filepaths)
  39. def test_noCategory(file_path):
  40. """
  41. Categories for definition files have been deprecated. Test that they are not
  42. present.
  43. :param file_path: The path of the machine definition to test.
  44. """
  45. with open(file_path, encoding = "utf-8") as f:
  46. json = f.read()
  47. metadata = DefinitionContainer.deserializeMetadata(json, "test_container_id")
  48. assert "category" not in metadata[0], "Definition located at [%s] referenced a category, which is no longer allowed" % file_path
  49. @pytest.mark.parametrize("file_path", machine_filepaths)
  50. def test_validateMachineDefinitionContainer(file_path, definition_container):
  51. """Tests all definition containers"""
  52. file_name = os.path.basename(file_path)
  53. if file_name == "fdmprinter.def.json" or file_name == "fdmextruder.def.json":
  54. return # Stop checking, these are root files.
  55. mocked_vum = MagicMock()
  56. mocked_vum.updateFilesData = lambda ct, v, fdl, fnl: FilesDataUpdateResult(ct, v, fdl, fnl)
  57. with patch("UM.VersionUpgradeManager.VersionUpgradeManager.getInstance", MagicMock(return_value = mocked_vum)):
  58. assertIsDefinitionValid(definition_container, file_path)
  59. def assertIsDefinitionValid(definition_container, file_path):
  60. with open(file_path, encoding = "utf-8") as data:
  61. json = data.read()
  62. parser, is_valid = definition_container.readAndValidateSerialized(json)
  63. assert is_valid # The definition has invalid JSON structure.
  64. metadata = DefinitionContainer.deserializeMetadata(json, "whatever")
  65. # If the definition defines a platform file, it should be in /resources/meshes/
  66. if "platform" in metadata[0]:
  67. assert metadata[0]["platform"] in all_meshes, "Definition located at [%s] references a platform that could not be found" % file_path
  68. if "platform_texture" in metadata[0]:
  69. assert metadata[0]["platform_texture"] in all_images, "Definition located at [%s] references a platform_texture that could not be found" % file_path
  70. @pytest.mark.parametrize("file_path", definition_filepaths)
  71. def test_validateOverridingDefaultValue(file_path: str):
  72. """Tests whether setting values are not being hidden by parent containers.
  73. When a definition container defines a "default_value" but inherits from a
  74. definition that defines a "value", the "default_value" is ineffective. This
  75. test fails on those things.
  76. """
  77. with open(file_path, encoding = "utf-8") as f:
  78. doc = json.load(f)
  79. if "inherits" not in doc:
  80. return # We only want to check for documents where the inheritance overrides the children. If there's no inheritance, this can't happen so it's fine.
  81. if "overrides" not in doc:
  82. return # No settings are being overridden. No need to check anything.
  83. parent_settings = getInheritedSettings(doc["inherits"])
  84. faulty_keys = set()
  85. for key, val in doc["overrides"].items():
  86. if key in parent_settings and "value" in parent_settings[key]:
  87. if "default_value" in val:
  88. faulty_keys.add(key)
  89. assert not faulty_keys, "Unnecessary default_values for {faulty_keys} in {file_name}".format(faulty_keys = sorted(faulty_keys), file_name = file_path) # If there is a value in the parent settings, then the default_value is not effective.
  90. def getInheritedSettings(definition_id: str) -> Dict[str, Any]:
  91. """Get all settings and their properties from a definition we're inheriting from.
  92. :param definition_id: The definition we're inheriting from.
  93. :return: A dictionary of settings by key. Each setting is a dictionary of properties.
  94. """
  95. definition_path = os.path.join(os.path.dirname(__file__), "..", "..", "resources", "definitions", definition_id + ".def.json")
  96. with open(definition_path, encoding = "utf-8") as f:
  97. doc = json.load(f)
  98. result = {}
  99. if "inherits" in doc: # Recursive inheritance.
  100. result.update(getInheritedSettings(doc["inherits"]))
  101. if "settings" in doc:
  102. result.update(flattenSettings(doc["settings"]))
  103. if "overrides" in doc:
  104. result = merge_dicts(result, doc["overrides"])
  105. return result
  106. def flattenSettings(settings: Dict[str, Any]) -> Dict[str, Any]:
  107. """Put all settings in the main dictionary rather than in children dicts.
  108. :param settings: Nested settings. The keys are the setting IDs. The values
  109. are dictionaries of properties per setting, including the "children" property.
  110. :return: A dictionary of settings by key. Each setting is a dictionary of properties.
  111. """
  112. result = {}
  113. for entry, contents in settings.items():
  114. if "children" in contents:
  115. result.update(flattenSettings(contents["children"]))
  116. del contents["children"]
  117. result[entry] = contents
  118. return result
  119. def merge_dicts(base: Dict[str, Any], overrides: Dict[str, Any]) -> Dict[str, Any]:
  120. """Make one dictionary override the other. Nested dictionaries override each
  121. other in the same way.
  122. :param base: A dictionary of settings that will get overridden by the other.
  123. :param overrides: A dictionary of settings that will override the other.
  124. :return: Combined setting data.
  125. """
  126. result = {}
  127. result.update(base)
  128. for key, val in overrides.items():
  129. if key not in result:
  130. result[key] = val
  131. continue
  132. if isinstance(result[key], dict) and isinstance(val, dict):
  133. result[key] = merge_dicts(result[key], val)
  134. else:
  135. result[key] = val
  136. return result
  137. @pytest.mark.parametrize("file_path", definition_filepaths)
  138. def test_noId(file_path: str):
  139. """Verifies that definition contains don't have an ID field.
  140. ID fields are legacy. They should not be used any more. This is legacy that
  141. people don't seem to be able to get used to.
  142. """
  143. with open(file_path, encoding = "utf-8") as f:
  144. doc = json.load(f)
  145. assert "id" not in doc, "Definitions should not have an ID field."
  146. @pytest.mark.parametrize("file_path", extruder_filepaths)
  147. def test_extruderMatch(file_path: str):
  148. """
  149. Verifies that extruders say that they work on the same extruder_nr as what is listed in their machine definition.
  150. """
  151. extruder_id = os.path.basename(file_path).split(".")[0]
  152. with open(file_path, encoding = "utf-8") as f:
  153. doc = json.load(f)
  154. if "metadata" not in doc:
  155. return # May not be desirable either, but it's probably unfinished then.
  156. if "machine" not in doc["metadata"] or "position" not in doc["metadata"]:
  157. return # FDMextruder doesn't have this since it's not linked to a particular printer.
  158. machine = doc["metadata"]["machine"]
  159. position = doc["metadata"]["position"]
  160. # Find the machine definition.
  161. for machine_filepath in machine_filepaths:
  162. machine_id = os.path.basename(machine_filepath).split(".")[0]
  163. if machine_id == machine:
  164. break
  165. else:
  166. assert False, "The machine ID {machine} is not found.".format(machine = machine)
  167. with open(machine_filepath, encoding = "utf-8") as f:
  168. machine_doc = json.load(f)
  169. # Make sure that the two match up.
  170. assert "metadata" in machine_doc, "Machine definition missing metadata entry."
  171. assert "machine_extruder_trains" in machine_doc["metadata"], "Machine must define extruder trains."
  172. extruder_trains = machine_doc["metadata"]["machine_extruder_trains"]
  173. assert position in extruder_trains, "There must be a reference to the extruder in the machine definition."
  174. assert extruder_trains[position] == extruder_id, "The extruder referenced in the machine definition must match up."
  175. # Also test if the extruder_nr setting is properly overridden.
  176. if "overrides" not in doc or "extruder_nr" not in doc["overrides"] or "default_value" not in doc["overrides"]["extruder_nr"]:
  177. assert position == "0" # Default to 0 is allowed.
  178. assert doc["overrides"]["extruder_nr"]["default_value"] == int(position)
  179. @pytest.mark.parametrize("file_path", definition_filepaths)
  180. def test_noNewSettings(file_path: str):
  181. """
  182. Tests that a printer definition doesn't define any new settings.
  183. Settings that are not common to all printers can cause Cura to crash, for instance when the setting is saved in a
  184. profile and that profile is then used in a different printer.
  185. :param file_path: A path to a definition file to test.
  186. """
  187. filename = os.path.basename(file_path)
  188. if filename == "fdmprinter.def.json" or filename == "fdmextruder.def.json":
  189. return # FDMPrinter and FDMExtruder, being the basis for all printers and extruders, are allowed to define new settings since they will be available for all printers then.
  190. with open(file_path, encoding = "utf-8") as f:
  191. doc = json.load(f)
  192. assert "settings" not in doc