TestDefinitionContainer.py 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  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. ## Tests all definition containers
  38. @pytest.mark.parametrize("file_path", machine_filepaths)
  39. def test_validateMachineDefinitionContainer(file_path, definition_container):
  40. file_name = os.path.basename(file_path)
  41. if file_name == "fdmprinter.def.json" or file_name == "fdmextruder.def.json":
  42. return # Stop checking, these are root files.
  43. from UM.VersionUpgradeManager import FilesDataUpdateResult
  44. mocked_vum = MagicMock()
  45. mocked_vum.updateFilesData = lambda ct, v, fdl, fnl: FilesDataUpdateResult(ct, v, fdl, fnl)
  46. with patch("UM.VersionUpgradeManager.VersionUpgradeManager.getInstance", MagicMock(return_value = mocked_vum)):
  47. assertIsDefinitionValid(definition_container, file_path)
  48. def assertIsDefinitionValid(definition_container, file_path):
  49. with open(file_path, encoding = "utf-8") as data:
  50. json = data.read()
  51. parser, is_valid = definition_container.readAndValidateSerialized(json)
  52. assert is_valid #The definition has invalid JSON structure.
  53. metadata = DefinitionContainer.deserializeMetadata(json, "whatever")
  54. # If the definition defines a platform file, it should be in /resources/meshes/
  55. if "platform" in metadata[0]:
  56. assert metadata[0]["platform"] in all_meshes
  57. if "platform_texture" in metadata[0]:
  58. assert metadata[0]["platform_texture"] in all_images
  59. ## Tests whether setting values are not being hidden by parent containers.
  60. #
  61. # When a definition container defines a "default_value" but inherits from a
  62. # definition that defines a "value", the "default_value" is ineffective. This
  63. # test fails on those things.
  64. @pytest.mark.parametrize("file_path", definition_filepaths)
  65. def test_validateOverridingDefaultValue(file_path: str):
  66. with open(file_path, encoding = "utf-8") as f:
  67. doc = json.load(f)
  68. if "inherits" not in doc:
  69. 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.
  70. if "overrides" not in doc:
  71. return # No settings are being overridden. No need to check anything.
  72. parent_settings = getInheritedSettings(doc["inherits"])
  73. faulty_keys = set()
  74. for key, val in doc["overrides"].items():
  75. if key in parent_settings and "value" in parent_settings[key]:
  76. if "default_value" in val:
  77. faulty_keys.add(key)
  78. 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.
  79. ## Get all settings and their properties from a definition we're inheriting
  80. # from.
  81. # \param definition_id The definition we're inheriting from.
  82. # \return A dictionary of settings by key. Each setting is a dictionary of
  83. # properties.
  84. def getInheritedSettings(definition_id: str) -> Dict[str, Any]:
  85. definition_path = os.path.join(os.path.dirname(__file__), "..", "..", "resources", "definitions", definition_id + ".def.json")
  86. with open(definition_path, encoding = "utf-8") as f:
  87. doc = json.load(f)
  88. result = {}
  89. if "inherits" in doc: # Recursive inheritance.
  90. result.update(getInheritedSettings(doc["inherits"]))
  91. if "settings" in doc:
  92. result.update(flattenSettings(doc["settings"]))
  93. if "overrides" in doc:
  94. result = merge_dicts(result, doc["overrides"])
  95. return result
  96. ## Put all settings in the main dictionary rather than in children dicts.
  97. # \param settings Nested settings. The keys are the setting IDs. The values
  98. # are dictionaries of properties per setting, including the "children"
  99. # property.
  100. # \return A dictionary of settings by key. Each setting is a dictionary of
  101. # properties.
  102. def flattenSettings(settings: Dict[str, Any]) -> Dict[str, Any]:
  103. result = {}
  104. for entry, contents in settings.items():
  105. if "children" in contents:
  106. result.update(flattenSettings(contents["children"]))
  107. del contents["children"]
  108. result[entry] = contents
  109. return result
  110. ## Make one dictionary override the other. Nested dictionaries override each
  111. # other in the same way.
  112. # \param base A dictionary of settings that will get overridden by the other.
  113. # \param overrides A dictionary of settings that will override the other.
  114. # \return Combined setting data.
  115. def merge_dicts(base: Dict[str, Any], overrides: Dict[str, Any]) -> Dict[str, Any]:
  116. result = {}
  117. result.update(base)
  118. for key, val in overrides.items():
  119. if key not in result:
  120. result[key] = val
  121. continue
  122. if isinstance(result[key], dict) and isinstance(val, dict):
  123. result[key] = merge_dicts(result[key], val)
  124. else:
  125. result[key] = val
  126. return result
  127. ## Verifies that definition contains don't have an ID field.
  128. #
  129. # ID fields are legacy. They should not be used any more. This is legacy that
  130. # people don't seem to be able to get used to.
  131. @pytest.mark.parametrize("file_path", definition_filepaths)
  132. def test_noId(file_path: str):
  133. with open(file_path, encoding = "utf-8") as f:
  134. doc = json.load(f)
  135. assert "id" not in doc, "Definitions should not have an ID field."
  136. ## Verifies that extruders say that they work on the same extruder_nr as what
  137. # is listed in their machine definition.
  138. @pytest.mark.parametrize("file_path", extruder_filepaths)
  139. def test_extruderMatch(file_path: str):
  140. extruder_id = os.path.basename(file_path).split(".")[0]
  141. with open(file_path, encoding = "utf-8") as f:
  142. doc = json.load(f)
  143. if "metadata" not in doc:
  144. return # May not be desirable either, but it's probably unfinished then.
  145. if "machine" not in doc["metadata"] or "position" not in doc["metadata"]:
  146. return # FDMextruder doesn't have this since it's not linked to a particular printer.
  147. machine = doc["metadata"]["machine"]
  148. position = doc["metadata"]["position"]
  149. # Find the machine definition.
  150. for machine_filepath in machine_filepaths:
  151. machine_id = os.path.basename(machine_filepath).split(".")[0]
  152. if machine_id == machine:
  153. break
  154. else:
  155. assert False, "The machine ID {machine} is not found.".format(machine = machine)
  156. with open(machine_filepath, encoding = "utf-8") as f:
  157. machine_doc = json.load(f)
  158. # Make sure that the two match up.
  159. assert "metadata" in machine_doc, "Machine definition missing metadata entry."
  160. assert "machine_extruder_trains" in machine_doc["metadata"], "Machine must define extruder trains."
  161. extruder_trains = machine_doc["metadata"]["machine_extruder_trains"]
  162. assert position in extruder_trains, "There must be a reference to the extruder in the machine definition."
  163. assert extruder_trains[position] == extruder_id, "The extruder referenced in the machine definition must match up."
  164. # Also test if the extruder_nr setting is properly overridden.
  165. if "overrides" not in doc or "extruder_nr" not in doc["overrides"] or "default_value" not in doc["overrides"]["extruder_nr"]:
  166. assert position == "0" # Default to 0 is allowed.
  167. assert doc["overrides"]["extruder_nr"]["default_value"] == int(position)