Cura/tests/Settings/TestDefinitionContainer.py
Ghostkeeper 174b326f57
Also test validity and correctness of extruder definitions
This makes a few things slightly simpler as well since it now parameterises the tests with the entire path of the definition file so we don't have to reconstruct that in every test.
2019-11-01 14:18:35 +01:00

140 lines
No EOL
6.4 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 os.path
import pytest #This module contains automated tests.
from typing import Any, Dict
import uuid
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"))
@pytest.fixture
def definition_container():
uid = str(uuid.uuid4())
result = UM.Settings.DefinitionContainer.DefinitionContainer(uid)
assert result.getId() == uid
return result
## Tests all definition containers
@pytest.mark.parametrize("file_path", definition_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.
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", machine_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"])
for key, val in doc["overrides"].items():
if "value" in parent_settings[key]:
assert "default_value" not in val, "Unnecessary default_value for {key} in {file_name}".format(key = key, 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", machine_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."