diff --git a/generator/converter.py b/generator/converter.py index 61a294a2b804233f3859880327c3426f07033ac6..3503d9d3678b22bd56f7fc2bbc787bf32fd6bba3 100644 --- a/generator/converter.py +++ b/generator/converter.py @@ -1,11 +1,35 @@ +################################################################################ +# Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) +# +# This program and the accompanying materials are made available under the +# terms of the Eclipse Public License 2.0 which is available at +# http://www.eclipse.org/legal/epl-2.0. +# +# SPDX-License-Identifier: EPL-2.0 +################################################################################ + from dataclasses import dataclass -from typing import List +from typing import Dict, List +import functools + +def init_with_lowercase_keys(cls): + wrapped_init = cls.__init__ + + @functools.wraps(wrapped_init) + def wrapper(self, *args, **kwargs): + lowercase_kwargs = {key.lower(): value for key, value in kwargs.items()} + wrapped_init(self, *args, **lowercase_kwargs) + + cls.__init__ = wrapper + return cls +@init_with_lowercase_keys @dataclass(frozen=True) class Dependency: - name: str - type: str - include: str + name: str + type: str + include: str + class DependencyException(Exception): def __init__(self, dependency_name): @@ -13,23 +37,46 @@ class DependencyException(Exception): message = f"The referenced dependency '{dependency_name}' does not exist." super().__init__(message) + class Converter: - TYPE_MAPPING = { - "runtime": True, - "initialization": False} - - def __init__(self, dependencies: List[Dependency], name: str, values={}): - self.name = name - self.properties = sorted(values.get("Properties", [name])) - self.keep = values.get("Keep", []) - self.runtime = Converter.TYPE_MAPPING[values.get("Type", "initialization").lower()] - self.dependencies = Converter._parse_dependencies(dependencies, values) - - @staticmethod - def _parse_dependencies(dependencies, values): - collected_deps = set() - for d in values.get("Dependencies", []): - if d not in dependencies: - raise DependencyException(d) - collected_deps.add(dependencies[d]) - return sorted(list(collected_deps)) + TYPE_MAPPING = { + "runtime": True, + "initialization": False} + + def __init__(self, dependencies: Dict[str, Dependency], name: str, values={}): + self.name = name + self.properties = sorted(values.get("Properties", [name])) + self.keep = values.get("Keep", []) + self.runtime = Converter.TYPE_MAPPING[values.get( + "Type", "initialization").lower()] + self.dependencies = Converter._parse_dependencies(dependencies, values) + + @staticmethod + def _parse_dependencies(dependencies, values): + collected_deps = set() + for d in values.get("Dependencies", []): + if d not in dependencies: + raise DependencyException(d) + collected_deps.add(dependencies[d]) + return sorted(list(collected_deps)) + + +class ConverterRegistry: + def __init__(self, dependencies: List[Dependency], converter): + self.general = self._parse_definition( + dependencies, converter.get("General", {})) + self.node_specific = self._parse_definitions( + dependencies, converter.get("NodeSpecific", {})) + + def _parse_definitions(self, dependencies, definitions): + return {node_name: self._parse_definition(dependencies, definition) + for node_name, definition in definitions.items()} + + def _parse_definition(self, dependencies, definition): + return [Converter(dependencies, conv_name, conf_config) + for conv_name, conf_config in definition.items()] + + def __getitem__(self, node_name): + converters = list(self.general) + converters.extend(self.node_specific.get(node_name, [])) + return converters diff --git a/generator/open_scenario_tree.py b/generator/open_scenario_tree.py index 6e90e654bb0182f41266372d6af66f032139a3c4..28583a0b6fc9eee329e6e0e914d6cdafda3d8d82 100644 --- a/generator/open_scenario_tree.py +++ b/generator/open_scenario_tree.py @@ -1,5 +1,5 @@ ################################################################################ -# Copyright (c) 2021-2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) +# Copyright (c) 2021-2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) # # This program and the accompanying materials are made available under the # terms of the Eclipse Public License 2.0 which is available at diff --git a/generator/property_view.py b/generator/property_view.py index ae50f06b433060fa8ff5d2cbe5f192b34507b300..5000fa0626b8ca0aa8a87bba4db1efbf4d0555a6 100644 --- a/generator/property_view.py +++ b/generator/property_view.py @@ -1,3 +1,13 @@ +################################################################################ +# Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) +# +# This program and the accompanying materials are made available under the +# terms of the Eclipse Public License 2.0 which is available at +# http://www.eclipse.org/legal/epl-2.0. +# +# SPDX-License-Identifier: EPL-2.0 +################################################################################ + from typing import List from open_scenario_tree import OscProperty from converter import Converter diff --git a/generator/tests/test_converter.py b/generator/tests/test_converter.py index efda1450247f87693841378a83f57173d1028ce2..7f50853e30db09dc845a080dac7cdc96ad6ffeea 100644 --- a/generator/tests/test_converter.py +++ b/generator/tests/test_converter.py @@ -1,5 +1,5 @@ ################################################################################ -# Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) +# Copyright (c) 2023-2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) # # This program and the accompanying materials are made available under the # terms of the Eclipse Public License 2.0 which is available at @@ -12,13 +12,16 @@ import pytest from typing import List from yaml import load, FullLoader -from converter import Converter, Dependency, DependencyException +from converter import ConverterRegistry, Converter, Dependency, DependencyException -TEST_CONVERTER = """ +TEST_METAINFO = """ Converter: General: Position: + Description: > + Position are always converted when the scenario is initialized. + The converter needs the environment. Type: initialization Dependencies: - Environment @@ -26,18 +29,57 @@ Converter: NodeSpecific: LongitudinalDistanceAction: LongitudinalDistance: + Description: > + LongitudinalDistance needs distance and timegap referenced w.r.t an entity. Properties: - entityRef - - distance: consume - - timegap: consume + - distance + - timegap Keep: - entityRef Type: runtime Dependencies: - Environment -""" -TEST_DEPS = """ + SomeActionWithTwoConverters: + FirstConverter: + Description: > + FirstConverter needs a and b. + Properties: + - a + - b + + SecondConverter: + Description: > + SecondConverter needs c and d. + Properties: + - c + - d + + AssignControllerAction: + Controller: + Properties: + - controller + - catalogReference + + AssignRouteAction: + Route: + Properties: + - route + - catalogReference + + EnvironmentAction: + Environment: + Properties: + - environment + - catalogReference + + FollowTrajectoryAction: + Trajectory: + Properties: + - trajectory + - catalogReference + Dependencies: Environment: Name: environment @@ -45,51 +87,46 @@ Dependencies: Include: '<MantleAPI/Execution/i_environment.h>' """ +@pytest.fixture +def dependencies(): + data = load(TEST_METAINFO, FullLoader) + return {d: Dependency(**value) for d, value in data.get("Dependencies", {}).items()} @pytest.fixture -def node_specific(): - data = load(TEST_CONVERTER, FullLoader) - return data.get("Converter", {}).get("NodeSpecific", {}) +def converter_registry(dependencies): + data = load(TEST_METAINFO, FullLoader) + return ConverterRegistry(dependencies, data.get("Converter", {})) +def test__converter_registry__given_full_definition__aggregates_general_and_node_specific_converters(converter_registry): + # collects the general converters + assert(len(converter_registry["I don't care"]) == 1) -@pytest.fixture -def general(): - data = load(TEST_CONVERTER, FullLoader) - return data.get("Converter", {}).get("General", {}) + # collects the general converters + the converter for the specific node + assert(len(converter_registry["LongitudinalDistanceAction"]) == 2) + # collects the general converters + the converter for the specific node + assert(len(converter_registry["SomeActionWithTwoConverters"]) == 3) -@pytest.fixture -def deps(): - data = load(TEST_DEPS, FullLoader) - return data.get("Dependencies", {}) - - -def test__converter__given_full_info__parses_everything(node_specific): - class FakeEnvironment: pass - fake_deps = {"Environment": FakeEnvironment} - node_under_test = next(iter(node_specific.values())) - converter = Converter(fake_deps, *next(iter(node_under_test.items()))) - assert converter.name == "LongitudinalDistance" - assert set(converter.properties) == {"entityRef", "distance", "timegap"} - assert converter.keep == ["entityRef"] - assert converter.runtime == True - assert converter.dependencies == [FakeEnvironment] - - -def test__converter__given_partial_info__parses_everything(general): - class FakeEnvironment: pass - fake_deps = {"Environment": FakeEnvironment} - converter = Converter(fake_deps, *next(iter(general.items()))) +def test__converter_registry__given_full_definition__parses_general_converters(converter_registry, dependencies): + converter = converter_registry["I don't care"][0] # 0 belongs to general converters assert converter.name == "Position" assert converter.properties == ["Position"] assert converter.keep == [] assert converter.runtime == False - assert converter.dependencies == [FakeEnvironment] + assert converter.dependencies == [dependencies["Environment"]] +def test__converter_registry__given_full_definition__parses_everything(converter_registry, dependencies): + converters = converter_registry["LongitudinalDistanceAction"][1] # 0 belongs to general converters + assert converters.name == "LongitudinalDistance" + assert set(converters.properties) == {"entityRef", "distance", "timegap"} + assert converters.keep == ["entityRef"] + assert converters.runtime == True + assert converters.dependencies == [dependencies["Environment"]] -def test__converter__given_no_info__parses_everything(): + +def test__converter__given_no_info__creates_defaults(): fake_deps = {"Environment": None} - converter = Converter(fake_deps,"SomeValue") + converter = Converter(fake_deps, "SomeValue") assert converter.name == "SomeValue" assert converter.properties == ["SomeValue"] assert converter.keep == [] @@ -103,9 +140,26 @@ def test__converter__given_unknown_dependency__raises_error(): assert "UnknownDependency" in str(excinfo.value) -def test__dependency__creation(deps): - dep = Dependency(**{key.lower(): value for key, value in deps['Environment'].items()}) - +def test__dependency__given_uppercase_keys__creates_dependencies(): + dependency_definition = {"NAME": "test_name", "TYPE": "test_type", "INCLUDE": "test_include"} + dep = Dependency(**dependency_definition) + assert dep.include == 'test_include' + assert dep.name == 'test_name' + assert dep.type == 'test_type' + +def test__dependency__given_capitalized_keys__creates_dependencies(): + dependency_definition = {"Name": "test_name", "Type": "test_type", "Include": "test_include"} + dep = Dependency(**dependency_definition) + assert dep.include == 'test_include' + assert dep.name == 'test_name' + assert dep.type == 'test_type' + +def test__dependency__given_lowercase_keys__creates_dependencies(): + dependency_definition = {"name": "test_name", "type": "test_type", "include": "test_include"} + dep = Dependency(**dependency_definition) + assert dep.include == 'test_include' + assert dep.name == 'test_name' + assert dep.type == 'test_type' def test_converter__given_properies__sorts_alphabetically(): fake_deps = {"Environment": None} diff --git a/generator/tests/test_nodeview.py b/generator/tests/test_nodeview.py index 589710a19bc31d47ec66f5083b6d7437a92fd570..ba538b8d811cfa7114d5b85c92347960d571fd26 100644 --- a/generator/tests/test_nodeview.py +++ b/generator/tests/test_nodeview.py @@ -1,4 +1,13 @@ -from dataclasses import dataclass +################################################################################ +# Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) +# +# This program and the accompanying materials are made available under the +# terms of the Eclipse Public License 2.0 which is available at +# http://www.eclipse.org/legal/epl-2.0. +# +# SPDX-License-Identifier: EPL-2.0 +################################################################################ + from open_scenario_tree import OscNode, parse_properties from converter import Converter, Dependency from typing import List @@ -27,6 +36,9 @@ class NodeView: def _generate_properties_view(self): properties = [] unconverted_properties = set(self.node.properties) + + ## apply converters to properties of the node OR which are flagged as general + for converter in self.converter: if set(converter.properties).issubset({prop.name for prop in self.node.properties}): consumed_properties = [prop for prop in self.node.properties if prop.name in converter.properties] @@ -216,3 +228,17 @@ def test__given_node_with_optional_property_and_runtime__generates_ternary_opera assert node_view.properties == [ "[=](){ return ConvertScenarioCustomPropName(nodeUnderTest_->IsSetPropName1() ? std::make_optional(nodeUnderTest_->GetPropName1()) : std::nullopt); }"] + + +### Use Case MR190 +def test__given_node_with_environment_and_catalog_reference__consumes_catalog_reference(): + osc_node = OscNode("EnvironmentAction", parse_properties({ + "environment": { "type.name": "Environment" }, + "catalogReference": { "type.name": "CatalogReference" } + })) + converter = Converter([], "environment", {'Properties': ['environment', 'catalogReference']}) + node_view = NodeView(osc_node, [converter]) + + assert node_view.properties == [ + "ConvertScenarioEnvironment(environmentAction_->GetEnvironment(), environmentAction_->GetCatalogReference())" + ] diff --git a/generator/tests/test_property_view.py b/generator/tests/test_property_view.py index 4b2dc223f7332baca58ed2ff0140d0eda709e382..5ea8a593b03e9f4bcb1b959cdc2dbd0553321da9 100644 --- a/generator/tests/test_property_view.py +++ b/generator/tests/test_property_view.py @@ -1,3 +1,13 @@ +################################################################################ +# Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) +# +# This program and the accompanying materials are made available under the +# terms of the Eclipse Public License 2.0 which is available at +# http://www.eclipse.org/legal/epl-2.0. +# +# SPDX-License-Identifier: EPL-2.0 +################################################################################ + import pytest from property_view import PropertyView diff --git a/generator/tests/test_treebuilder.py b/generator/tests/test_treebuilder.py index 63da203c9a5e71bee4c8734719aed52030895e7e..580d7fb3e2413807fbee5b79b2b27276175343aa 100644 --- a/generator/tests/test_treebuilder.py +++ b/generator/tests/test_treebuilder.py @@ -1,4 +1,14 @@ -from open_scenario_tree import OpenScenarioTree, OscNode, Entity +################################################################################ +# Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) +# +# This program and the accompanying materials are made available under the +# terms of the Eclipse Public License 2.0 which is available at +# http://www.eclipse.org/legal/epl-2.0. +# +# SPDX-License-Identifier: EPL-2.0 +################################################################################ + +from open_scenario_tree import OpenScenarioTree from generate import get_from_json import pytest @@ -18,13 +28,15 @@ class NodeView(): def uml_model(): return get_from_json() + def test__given_choice__generates_data_what_else(uml_model): osc_tree = OpenScenarioTree(uml_model) node_under_test = NodeView(osc_tree["Action"]) assert node_under_test.is_choice assert node_under_test.filename == "Action" - assert node_under_test.items == ["PrivateAction", "GlobalAction", "UserDefinedAction"] + assert node_under_test.items == [ + "PrivateAction", "GlobalAction", "UserDefinedAction"] def test__given_list__generates_data_what_else(uml_model): @@ -35,6 +47,7 @@ def test__given_list__generates_data_what_else(uml_model): assert node_under_test.filename == "GlobalActions" assert node_under_test.items == ["GlobalAction"] + def test__given_list__generates_data_what_else(uml_model): osc_tree = OpenScenarioTree(uml_model) node_under_test = NodeView(osc_tree["GlobalActions"]) @@ -52,6 +65,7 @@ def test__given_leaf__generates_leaf(): assert node_under_test.filename == "VariableCondition" assert node_under_test.items == ["VariableCondition"] + def test__given_leaf__generates_leaf(): osc_tree = OpenScenarioTree(uml_model) node_under_test = NodeView(osc_tree["TrafficStopAction"])