Cura/tests/Settings/TestDefinitionContainer.py
Ghostkeeper a8ccec709f
Add test for spaces in definition IDs
Contributes to issue CURA-7201.
2020-02-07 13:55:24 +01:00

199 lines
9.3 KiB
Python

# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import json # To check files for unnecessarily overridden properties.
import os
import pytest #This module contains automated tests.
from typing import Any, Dict
import uuid
from unittest.mock import patch, MagicMock
import UM.Settings.ContainerRegistry #To create empty instance containers.
import UM.Settings.ContainerStack #To set the container registry the container stacks use.
from UM.Settings.DefinitionContainer import DefinitionContainer #To check against the class of DefinitionContainer.
from UM.Resources import Resources
Resources.addSearchPath(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "resources")))
machine_filepaths = sorted(os.listdir(os.path.join(os.path.dirname(__file__), "..", "..", "resources", "definitions")))
machine_filepaths = [os.path.join(os.path.dirname(__file__), "..", "..", "resources", "definitions", filename) for filename in machine_filepaths]
extruder_filepaths = sorted(os.listdir(os.path.join(os.path.dirname(__file__), "..", "..", "resources", "extruders")))
extruder_filepaths = [os.path.join(os.path.dirname(__file__), "..", "..", "resources", "extruders", filename) for filename in extruder_filepaths]
definition_filepaths = machine_filepaths + extruder_filepaths
all_meshes = os.listdir(os.path.join(os.path.dirname(__file__), "..", "..", "resources", "meshes"))
all_images = os.listdir(os.path.join(os.path.dirname(__file__), "..", "..", "resources", "images"))
# Loading definition files needs a functioning ContainerRegistry
cr = UM.Settings.ContainerRegistry.ContainerRegistry(None)
@pytest.fixture
def definition_container():
uid = str(uuid.uuid4())
result = UM.Settings.DefinitionContainer.DefinitionContainer(uid)
assert result.getId() == uid
return result
@pytest.mark.parametrize("file_path", definition_filepaths)
def test_definitionIds(file_path):
"""
Test the validity of the definition IDs.
:param file_path: The path of the machine definition to test.
"""
definition_id = os.path.basename(file_path).split(".")[0]
assert " " not in definition_id # Definition IDs are not allowed to have spaces.
## Tests all definition containers
@pytest.mark.parametrize("file_path", machine_filepaths)
def test_validateMachineDefinitionContainer(file_path, definition_container):
file_name = os.path.basename(file_path)
if file_name == "fdmprinter.def.json" or file_name == "fdmextruder.def.json":
return # Stop checking, these are root files.
from UM.VersionUpgradeManager import FilesDataUpdateResult
mocked_vum = MagicMock()
mocked_vum.updateFilesData = lambda ct, v, fdl, fnl: FilesDataUpdateResult(ct, v, fdl, fnl)
with patch("UM.VersionUpgradeManager.VersionUpgradeManager.getInstance", MagicMock(return_value = mocked_vum)):
assertIsDefinitionValid(definition_container, file_path)
def assertIsDefinitionValid(definition_container, file_path):
with open(file_path, encoding = "utf-8") as data:
json = data.read()
parser, is_valid = definition_container.readAndValidateSerialized(json)
assert is_valid #The definition has invalid JSON structure.
metadata = DefinitionContainer.deserializeMetadata(json, "whatever")
# If the definition defines a platform file, it should be in /resources/meshes/
if "platform" in metadata[0]:
assert metadata[0]["platform"] in all_meshes
if "platform_texture" in metadata[0]:
assert metadata[0]["platform_texture"] in all_images
## Tests whether setting values are not being hidden by parent containers.
#
# When a definition container defines a "default_value" but inherits from a
# definition that defines a "value", the "default_value" is ineffective. This
# test fails on those things.
@pytest.mark.parametrize("file_path", definition_filepaths)
def test_validateOverridingDefaultValue(file_path: str):
with open(file_path, encoding = "utf-8") as f:
doc = json.load(f)
if "inherits" not in doc:
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.
if "overrides" not in doc:
return # No settings are being overridden. No need to check anything.
parent_settings = getInheritedSettings(doc["inherits"])
faulty_keys = set()
for key, val in doc["overrides"].items():
if key in parent_settings and "value" in parent_settings[key]:
if "default_value" in val:
faulty_keys.add(key)
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.
## Get all settings and their properties from a definition we're inheriting
# from.
# \param definition_id The definition we're inheriting from.
# \return A dictionary of settings by key. Each setting is a dictionary of
# properties.
def getInheritedSettings(definition_id: str) -> Dict[str, Any]:
definition_path = os.path.join(os.path.dirname(__file__), "..", "..", "resources", "definitions", definition_id + ".def.json")
with open(definition_path, encoding = "utf-8") as f:
doc = json.load(f)
result = {}
if "inherits" in doc: # Recursive inheritance.
result.update(getInheritedSettings(doc["inherits"]))
if "settings" in doc:
result.update(flattenSettings(doc["settings"]))
if "overrides" in doc:
result = merge_dicts(result, doc["overrides"])
return result
## Put all settings in the main dictionary rather than in children dicts.
# \param settings Nested settings. The keys are the setting IDs. The values
# are dictionaries of properties per setting, including the "children"
# property.
# \return A dictionary of settings by key. Each setting is a dictionary of
# properties.
def flattenSettings(settings: Dict[str, Any]) -> Dict[str, Any]:
result = {}
for entry, contents in settings.items():
if "children" in contents:
result.update(flattenSettings(contents["children"]))
del contents["children"]
result[entry] = contents
return result
## Make one dictionary override the other. Nested dictionaries override each
# other in the same way.
# \param base A dictionary of settings that will get overridden by the other.
# \param overrides A dictionary of settings that will override the other.
# \return Combined setting data.
def merge_dicts(base: Dict[str, Any], overrides: Dict[str, Any]) -> Dict[str, Any]:
result = {}
result.update(base)
for key, val in overrides.items():
if key not in result:
result[key] = val
continue
if isinstance(result[key], dict) and isinstance(val, dict):
result[key] = merge_dicts(result[key], val)
else:
result[key] = val
return result
## Verifies that definition contains don't have an ID field.
#
# ID fields are legacy. They should not be used any more. This is legacy that
# people don't seem to be able to get used to.
@pytest.mark.parametrize("file_path", definition_filepaths)
def test_noId(file_path: str):
with open(file_path, encoding = "utf-8") as f:
doc = json.load(f)
assert "id" not in doc, "Definitions should not have an ID field."
## Verifies that extruders say that they work on the same extruder_nr as what
# is listed in their machine definition.
@pytest.mark.parametrize("file_path", extruder_filepaths)
def test_extruderMatch(file_path: str):
extruder_id = os.path.basename(file_path).split(".")[0]
with open(file_path, encoding = "utf-8") as f:
doc = json.load(f)
if "metadata" not in doc:
return # May not be desirable either, but it's probably unfinished then.
if "machine" not in doc["metadata"] or "position" not in doc["metadata"]:
return # FDMextruder doesn't have this since it's not linked to a particular printer.
machine = doc["metadata"]["machine"]
position = doc["metadata"]["position"]
# Find the machine definition.
for machine_filepath in machine_filepaths:
machine_id = os.path.basename(machine_filepath).split(".")[0]
if machine_id == machine:
break
else:
assert False, "The machine ID {machine} is not found.".format(machine = machine)
with open(machine_filepath, encoding = "utf-8") as f:
machine_doc = json.load(f)
# Make sure that the two match up.
assert "metadata" in machine_doc, "Machine definition missing metadata entry."
assert "machine_extruder_trains" in machine_doc["metadata"], "Machine must define extruder trains."
extruder_trains = machine_doc["metadata"]["machine_extruder_trains"]
assert position in extruder_trains, "There must be a reference to the extruder in the machine definition."
assert extruder_trains[position] == extruder_id, "The extruder referenced in the machine definition must match up."
# Also test if the extruder_nr setting is properly overridden.
if "overrides" not in doc or "extruder_nr" not in doc["overrides"] or "default_value" not in doc["overrides"]["extruder_nr"]:
assert position == "0" # Default to 0 is allowed.
assert doc["overrides"]["extruder_nr"]["default_value"] == int(position)