TestDefinitionContainer.py 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  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.Resources import Resources
  13. Resources.addSearchPath(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "resources")))
  14. machine_filepaths = sorted(os.listdir(os.path.join(os.path.dirname(__file__), "..", "..", "resources", "definitions")))
  15. machine_filepaths = [os.path.join(os.path.dirname(__file__), "..", "..", "resources", "definitions", filename) for filename in machine_filepaths]
  16. extruder_filepaths = sorted(os.listdir(os.path.join(os.path.dirname(__file__), "..", "..", "resources", "extruders")))
  17. extruder_filepaths = [os.path.join(os.path.dirname(__file__), "..", "..", "resources", "extruders", filename) for filename in extruder_filepaths]
  18. definition_filepaths = machine_filepaths + extruder_filepaths
  19. all_meshes = os.listdir(os.path.join(os.path.dirname(__file__), "..", "..", "resources", "meshes"))
  20. all_images = os.listdir(os.path.join(os.path.dirname(__file__), "..", "..", "resources", "images"))
  21. # Loading definition files needs a functioning ContainerRegistry
  22. cr = UM.Settings.ContainerRegistry.ContainerRegistry(None)
  23. @pytest.fixture
  24. def definition_container():
  25. uid = str(uuid.uuid4())
  26. result = UM.Settings.DefinitionContainer.DefinitionContainer(uid)
  27. assert result.getId() == uid
  28. return result
  29. @pytest.mark.parametrize("file_path", definition_filepaths)
  30. def test_definitionIds(file_path):
  31. """
  32. Test the validity of the definition IDs.
  33. :param file_path: The path of the machine definition to test.
  34. """
  35. definition_id = os.path.basename(file_path).split(".")[0]
  36. assert " " not in definition_id # Definition IDs are not allowed to have spaces.
  37. @pytest.mark.parametrize("file_path", definition_filepaths)
  38. def test_noCategory(file_path):
  39. """
  40. Categories for definition files have been deprecated. Test that they are not
  41. present.
  42. :param file_path: The path of the machine definition to test.
  43. """
  44. with open(file_path, encoding = "utf-8") as f:
  45. json = f.read()
  46. metadata = DefinitionContainer.deserializeMetadata(json, "test_container_id")
  47. assert "category" not in metadata[0]
  48. @pytest.mark.parametrize("file_path", machine_filepaths)
  49. def test_validateMachineDefinitionContainer(file_path, definition_container):
  50. """Tests all definition containers"""
  51. file_name = os.path.basename(file_path)
  52. if file_name == "fdmprinter.def.json" or file_name == "fdmextruder.def.json":
  53. return # Stop checking, these are root files.
  54. from UM.VersionUpgradeManager import FilesDataUpdateResult
  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
  68. if "platform_texture" in metadata[0]:
  69. assert metadata[0]["platform_texture"] in all_images
  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. """Verifies that extruders say that they work on the same extruder_nr as what is listed in their machine definition."""
  149. extruder_id = os.path.basename(file_path).split(".")[0]
  150. with open(file_path, encoding = "utf-8") as f:
  151. doc = json.load(f)
  152. if "metadata" not in doc:
  153. return # May not be desirable either, but it's probably unfinished then.
  154. if "machine" not in doc["metadata"] or "position" not in doc["metadata"]:
  155. return # FDMextruder doesn't have this since it's not linked to a particular printer.
  156. machine = doc["metadata"]["machine"]
  157. position = doc["metadata"]["position"]
  158. # Find the machine definition.
  159. for machine_filepath in machine_filepaths:
  160. machine_id = os.path.basename(machine_filepath).split(".")[0]
  161. if machine_id == machine:
  162. break
  163. else:
  164. assert False, "The machine ID {machine} is not found.".format(machine = machine)
  165. with open(machine_filepath, encoding = "utf-8") as f:
  166. machine_doc = json.load(f)
  167. # Make sure that the two match up.
  168. assert "metadata" in machine_doc, "Machine definition missing metadata entry."
  169. assert "machine_extruder_trains" in machine_doc["metadata"], "Machine must define extruder trains."
  170. extruder_trains = machine_doc["metadata"]["machine_extruder_trains"]
  171. assert position in extruder_trains, "There must be a reference to the extruder in the machine definition."
  172. assert extruder_trains[position] == extruder_id, "The extruder referenced in the machine definition must match up."
  173. # Also test if the extruder_nr setting is properly overridden.
  174. if "overrides" not in doc or "extruder_nr" not in doc["overrides"] or "default_value" not in doc["overrides"]["extruder_nr"]:
  175. assert position == "0" # Default to 0 is allowed.
  176. assert doc["overrides"]["extruder_nr"]["default_value"] == int(position)