baldertest 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- _balder/__init__.py +12 -0
- _balder/_version.py +34 -0
- _balder/balder_plugin.py +73 -0
- _balder/balder_session.py +341 -0
- _balder/balder_settings.py +15 -0
- _balder/cnnrelations/__init__.py +7 -0
- _balder/cnnrelations/and_connection_relation.py +176 -0
- _balder/cnnrelations/base_connection_relation.py +270 -0
- _balder/cnnrelations/or_connection_relation.py +65 -0
- _balder/collector.py +874 -0
- _balder/connection.py +863 -0
- _balder/connection_metadata.py +255 -0
- _balder/console/__init__.py +0 -0
- _balder/console/balder.py +58 -0
- _balder/controllers/__init__.py +12 -0
- _balder/controllers/base_device_controller.py +72 -0
- _balder/controllers/controller.py +29 -0
- _balder/controllers/device_controller.py +446 -0
- _balder/controllers/feature_controller.py +715 -0
- _balder/controllers/normal_scenario_setup_controller.py +402 -0
- _balder/controllers/scenario_controller.py +524 -0
- _balder/controllers/setup_controller.py +134 -0
- _balder/controllers/vdevice_controller.py +95 -0
- _balder/decorator_connect.py +104 -0
- _balder/decorator_covered_by.py +74 -0
- _balder/decorator_fixture.py +29 -0
- _balder/decorator_for_vdevice.py +118 -0
- _balder/decorator_gateway.py +34 -0
- _balder/decorator_insert_into_tree.py +52 -0
- _balder/decorator_parametrize.py +31 -0
- _balder/decorator_parametrize_by_feature.py +36 -0
- _balder/device.py +18 -0
- _balder/exceptions.py +182 -0
- _balder/executor/__init__.py +0 -0
- _balder/executor/basic_executable_executor.py +133 -0
- _balder/executor/basic_executor.py +205 -0
- _balder/executor/executor_tree.py +217 -0
- _balder/executor/parametrized_testcase_executor.py +52 -0
- _balder/executor/scenario_executor.py +169 -0
- _balder/executor/setup_executor.py +163 -0
- _balder/executor/testcase_executor.py +203 -0
- _balder/executor/unresolved_parametrized_testcase_executor.py +184 -0
- _balder/executor/variation_executor.py +882 -0
- _balder/exit_code.py +19 -0
- _balder/feature.py +74 -0
- _balder/feature_replacement_mapping.py +107 -0
- _balder/feature_vdevice_mapping.py +88 -0
- _balder/fixture_definition_scope.py +19 -0
- _balder/fixture_execution_level.py +22 -0
- _balder/fixture_manager.py +483 -0
- _balder/fixture_metadata.py +26 -0
- _balder/node_gateway.py +103 -0
- _balder/objects/__init__.py +0 -0
- _balder/objects/connections/__init__.py +0 -0
- _balder/objects/connections/osi_1_physical.py +116 -0
- _balder/objects/connections/osi_2_datalink.py +35 -0
- _balder/objects/connections/osi_3_network.py +47 -0
- _balder/objects/connections/osi_4_transport.py +40 -0
- _balder/objects/connections/osi_5_session.py +13 -0
- _balder/objects/connections/osi_6_presentation.py +13 -0
- _balder/objects/connections/osi_7_application.py +83 -0
- _balder/objects/devices/__init__.py +0 -0
- _balder/objects/devices/this_device.py +12 -0
- _balder/parametrization.py +75 -0
- _balder/plugin_manager.py +138 -0
- _balder/previous_executor_mark.py +23 -0
- _balder/routing_path.py +335 -0
- _balder/scenario.py +20 -0
- _balder/setup.py +18 -0
- _balder/solver.py +246 -0
- _balder/testresult.py +163 -0
- _balder/unmapped_vdevice.py +13 -0
- _balder/utils/__init__.py +0 -0
- _balder/utils/functions.py +103 -0
- _balder/utils/inner_device_managing_metaclass.py +14 -0
- _balder/utils/mixin_can_be_covered_by_executor.py +24 -0
- _balder/utils/typings.py +4 -0
- _balder/vdevice.py +9 -0
- balder/__init__.py +56 -0
- balder/connections.py +43 -0
- balder/devices.py +9 -0
- balder/exceptions.py +44 -0
- balder/parametrization.py +8 -0
- baldertest-0.1.0.dist-info/METADATA +356 -0
- baldertest-0.1.0.dist-info/RECORD +89 -0
- baldertest-0.1.0.dist-info/WHEEL +5 -0
- baldertest-0.1.0.dist-info/entry_points.txt +2 -0
- baldertest-0.1.0.dist-info/licenses/LICENSE +21 -0
- baldertest-0.1.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Type, Dict, List, Tuple, Union, Callable, Iterable, Any
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
import inspect
|
|
6
|
+
from collections import OrderedDict
|
|
7
|
+
from _balder.cnnrelations import OrConnectionRelation
|
|
8
|
+
from _balder.device import Device
|
|
9
|
+
from _balder.scenario import Scenario
|
|
10
|
+
from _balder.connection import Connection
|
|
11
|
+
from _balder.controllers.feature_controller import FeatureController
|
|
12
|
+
from _balder.controllers.device_controller import DeviceController
|
|
13
|
+
from _balder.controllers.normal_scenario_setup_controller import NormalScenarioSetupController
|
|
14
|
+
from _balder.parametrization import FeatureAccessSelector, Parameter
|
|
15
|
+
from _balder.exceptions import UnclearAssignableFeatureConnectionError, ConnectionIntersectionError, \
|
|
16
|
+
MultiInheritanceError
|
|
17
|
+
from _balder.utils.functions import get_scenario_inheritance_list_of
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__file__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ScenarioController(NormalScenarioSetupController):
|
|
23
|
+
"""
|
|
24
|
+
This is the controller class for :class:`Scenario` items.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
# helper property to disable manual constructor creation
|
|
28
|
+
__priv_instantiate_key = object()
|
|
29
|
+
|
|
30
|
+
#: contains all existing scenarios and its corresponding controller object
|
|
31
|
+
_items: Dict[Type[Scenario], ScenarioController] = {}
|
|
32
|
+
|
|
33
|
+
_parametrization: Dict[Callable, Dict[str, Union[Iterable[Any], FeatureAccessSelector]]] = {}
|
|
34
|
+
|
|
35
|
+
def __init__(self, related_cls, _priv_instantiate_key):
|
|
36
|
+
|
|
37
|
+
# describes if the current controller is for setups or for scenarios (has to be set in child controller)
|
|
38
|
+
self._related_type = Scenario
|
|
39
|
+
|
|
40
|
+
# holds covered-by configuration
|
|
41
|
+
self._covered_by = {}
|
|
42
|
+
|
|
43
|
+
# this helps to make this constructor only possible inside the controller object
|
|
44
|
+
if _priv_instantiate_key != ScenarioController.__priv_instantiate_key:
|
|
45
|
+
raise RuntimeError('it is not allowed to instantiate a controller manually -> use the static method '
|
|
46
|
+
'`ScenarioController.get_for()` for it')
|
|
47
|
+
|
|
48
|
+
if not isinstance(related_cls, type):
|
|
49
|
+
raise TypeError('the attribute `related_cls` has to be a type (no object)')
|
|
50
|
+
if not issubclass(related_cls, Scenario):
|
|
51
|
+
raise TypeError(f'the attribute `related_cls` has to be a sub-type of `{Scenario.__name__}`')
|
|
52
|
+
if related_cls == Scenario:
|
|
53
|
+
raise TypeError(f'the attribute `related_cls` is `{Scenario.__name__}` - controllers for native type are '
|
|
54
|
+
f'forbidden')
|
|
55
|
+
# contains a reference to the related class this controller instance belongs to
|
|
56
|
+
self._related_cls = related_cls
|
|
57
|
+
|
|
58
|
+
# ---------------------------------- STATIC METHODS ----------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
@staticmethod
|
|
61
|
+
def get_for(related_cls: Type[Scenario]) -> ScenarioController:
|
|
62
|
+
"""
|
|
63
|
+
This class returns the current existing controller instance for the given item. If the instance does not exist
|
|
64
|
+
yet, it will automatically create it and saves the instance in an internal dictionary.
|
|
65
|
+
"""
|
|
66
|
+
if ScenarioController._items.get(related_cls) is None:
|
|
67
|
+
item = ScenarioController(related_cls, _priv_instantiate_key=ScenarioController.__priv_instantiate_key)
|
|
68
|
+
ScenarioController._items[related_cls] = item
|
|
69
|
+
|
|
70
|
+
return ScenarioController._items.get(related_cls)
|
|
71
|
+
|
|
72
|
+
# ---------------------------------- CLASS METHODS ----------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
# ---------------------------------- PROPERTIES --------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def related_cls(self) -> Type[Scenario]:
|
|
78
|
+
return self._related_cls
|
|
79
|
+
|
|
80
|
+
# ---------------------------------- PROTECTED METHODS -------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
# ---------------------------------- METHODS -----------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
def register_parametrization(
|
|
85
|
+
self,
|
|
86
|
+
test_method: Callable,
|
|
87
|
+
field_name: str,
|
|
88
|
+
values: Iterable[Any] | FeatureAccessSelector
|
|
89
|
+
) -> None:
|
|
90
|
+
"""
|
|
91
|
+
This method registers a custom parametrization for a test method of this Scenario
|
|
92
|
+
"""
|
|
93
|
+
if test_method not in self.get_all_test_methods():
|
|
94
|
+
raise ValueError(f'got test method `{test_method.__qualname__}` which is no part of the '
|
|
95
|
+
f'scenario `{self.related_cls}`')
|
|
96
|
+
if test_method not in self._parametrization.keys():
|
|
97
|
+
self._parametrization[test_method] = {}
|
|
98
|
+
if field_name in self._parametrization[test_method].keys():
|
|
99
|
+
raise ValueError(f'field name `{field_name}` for test method `{test_method.__qualname__}` already '
|
|
100
|
+
f'registered')
|
|
101
|
+
self._parametrization[test_method][field_name] = values
|
|
102
|
+
|
|
103
|
+
def get_parametrization_for(
|
|
104
|
+
self,
|
|
105
|
+
test_method: Callable,
|
|
106
|
+
static: bool = True,
|
|
107
|
+
dynamic: bool = True,
|
|
108
|
+
) -> OrderedDict[str, Iterable[Any] | FeatureAccessSelector] | None:
|
|
109
|
+
"""
|
|
110
|
+
This method returns the parametrization for a test method of this Scenario. It returns the parameter
|
|
111
|
+
configuration for every parameter in an OrderedDict.
|
|
112
|
+
|
|
113
|
+
:param test_method: the test method of the Scenario
|
|
114
|
+
:param static: if False, all static parameters will not be included into the dict.
|
|
115
|
+
:param dynamic: if False, all dynamic parameters will not be included into the dict.
|
|
116
|
+
"""
|
|
117
|
+
if test_method not in self._parametrization.keys():
|
|
118
|
+
return None
|
|
119
|
+
params = self._parametrization[test_method]
|
|
120
|
+
|
|
121
|
+
# get arguments in defined order
|
|
122
|
+
arguments = [name for name in inspect.getfullargspec(test_method).args if name in params.keys()]
|
|
123
|
+
ordered_dict = OrderedDict()
|
|
124
|
+
for cur_arg in arguments:
|
|
125
|
+
cur_value = params[cur_arg]
|
|
126
|
+
if isinstance(cur_value, FeatureAccessSelector) and dynamic is False:
|
|
127
|
+
continue
|
|
128
|
+
if not isinstance(cur_value, FeatureAccessSelector) and static is False:
|
|
129
|
+
continue
|
|
130
|
+
ordered_dict[cur_arg] = params[cur_arg]
|
|
131
|
+
return ordered_dict
|
|
132
|
+
|
|
133
|
+
def register_covered_by_for(self, meth: Union[str, None], covered_by: Union[Scenario, Callable, None]) -> None:
|
|
134
|
+
"""
|
|
135
|
+
This method registers a covered-by statement for this Scenario. If `meth` is provided, the statement is for the
|
|
136
|
+
specific test method of the scenario, otherwise it is for the whole setup. The item provided in `covered_by`
|
|
137
|
+
describes the test object that covers this scenario (method).
|
|
138
|
+
|
|
139
|
+
:param meth: if provided this attribute describes the test method that should be registered, otherwise the whole
|
|
140
|
+
scenario will be registered
|
|
141
|
+
:param covered_by: describes the test object that covers this scenario (method)
|
|
142
|
+
"""
|
|
143
|
+
if not (meth is None or isinstance(meth, str)):
|
|
144
|
+
raise TypeError('meth needs to be None or a string')
|
|
145
|
+
if meth is not None:
|
|
146
|
+
if not meth.startswith('test_'):
|
|
147
|
+
raise TypeError(
|
|
148
|
+
f"the use of the `@covered_by` decorator is only allowed for `Scenario` objects and test methods "
|
|
149
|
+
f"of `Scenario` objects - the method `{self.related_cls.__name__}.{meth}` does not start with "
|
|
150
|
+
f"`test_` and is not a valid test method")
|
|
151
|
+
if not hasattr(self.related_cls, meth):
|
|
152
|
+
raise ValueError(
|
|
153
|
+
f"the provided test method `{meth}` does not exist in scenario `{self.related_cls.__name__}`"
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
if meth not in self._covered_by.keys():
|
|
157
|
+
self._covered_by[meth] = []
|
|
158
|
+
if covered_by is None:
|
|
159
|
+
# reset it
|
|
160
|
+
# todo what if there are more than one decorator in one class
|
|
161
|
+
del self._covered_by[meth]
|
|
162
|
+
else:
|
|
163
|
+
self._covered_by[meth].append(covered_by)
|
|
164
|
+
|
|
165
|
+
def get_raw_covered_by_dict(self) -> Dict[Union[str, None], List[Union[Scenario, Callable]]]:
|
|
166
|
+
"""
|
|
167
|
+
:return: returns the internal covered-by dictionary
|
|
168
|
+
"""
|
|
169
|
+
return self._covered_by.copy()
|
|
170
|
+
|
|
171
|
+
def get_abs_covered_by_dict(self) -> Dict[Union[str, None], List[Union[Scenario, Callable]]]:
|
|
172
|
+
"""
|
|
173
|
+
This method resolves the covered-by statements over all inheritance levels. It automatically
|
|
174
|
+
cleans up every inheritance of the covered_by decorators for every parent class of this scenario.
|
|
175
|
+
"""
|
|
176
|
+
parent_classes = [p for p in self.related_cls.__bases__ if issubclass(p, Scenario) and p != Scenario]
|
|
177
|
+
if len(parent_classes) > 1:
|
|
178
|
+
raise MultiInheritanceError(
|
|
179
|
+
f'can not resolve classes for `{self.related_cls}` because there are more than one Scenario based '
|
|
180
|
+
f'parent classes'
|
|
181
|
+
)
|
|
182
|
+
# no more parent classes -> raw is absolute
|
|
183
|
+
if len(parent_classes) == 0:
|
|
184
|
+
return self.get_raw_covered_by_dict()
|
|
185
|
+
parent_controller = self.__class__.get_for(parent_classes[0])
|
|
186
|
+
self_raw_covered_by_dict = self.get_raw_covered_by_dict()
|
|
187
|
+
|
|
188
|
+
#: first fill result with data from parent controller
|
|
189
|
+
result = {
|
|
190
|
+
k if k is None else getattr(self.related_cls, k.__name__): v
|
|
191
|
+
for k, v in parent_controller.get_abs_covered_by_dict().items()
|
|
192
|
+
}
|
|
193
|
+
for cur_callable, cur_coveredby in self_raw_covered_by_dict.items():
|
|
194
|
+
if cur_callable in result.keys():
|
|
195
|
+
result[cur_callable].extend(cur_coveredby)
|
|
196
|
+
else:
|
|
197
|
+
result[cur_callable] = cur_coveredby
|
|
198
|
+
return result
|
|
199
|
+
|
|
200
|
+
def check_for_parameter_loop_in_dynamic_parametrization(self, cur_fn: Callable):
|
|
201
|
+
"""
|
|
202
|
+
This method checks for a parameter loop in all dynamic parametrization for a specific test method. If it detects
|
|
203
|
+
a loop an AttributeError is thrown
|
|
204
|
+
"""
|
|
205
|
+
# only dynamic parametrization can have Parameter
|
|
206
|
+
parametrization = self.get_parametrization_for(cur_fn, static=False, dynamic=True)
|
|
207
|
+
|
|
208
|
+
def get_dependent_parameters_of_attribute(attribute: str) -> List[str] | None:
|
|
209
|
+
cur_feature_access_selector = parametrization.get(attribute)
|
|
210
|
+
if cur_feature_access_selector is None:
|
|
211
|
+
return None
|
|
212
|
+
# relevant are parameters only if they are from :class:`Parameter` and contained in the dynamic
|
|
213
|
+
# parametrization
|
|
214
|
+
return [param.name for param in cur_feature_access_selector.parameters.values()
|
|
215
|
+
if isinstance(param, Parameter) and param.name in parametrization.keys()]
|
|
216
|
+
|
|
217
|
+
def recursive_parameter_loop_check(for_attribute, with_attribute: str):
|
|
218
|
+
dependent_attr = get_dependent_parameters_of_attribute(with_attribute)
|
|
219
|
+
if dependent_attr is None:
|
|
220
|
+
# no problematic dependencies because attribute is no dynamic attribute
|
|
221
|
+
return
|
|
222
|
+
if len(dependent_attr) == 0:
|
|
223
|
+
# no problematic dependencies
|
|
224
|
+
return
|
|
225
|
+
|
|
226
|
+
if for_attribute in dependent_attr:
|
|
227
|
+
# loop detected
|
|
228
|
+
raise AttributeError('detect a loop in Parameter() object - can not apply parametrization')
|
|
229
|
+
# go deeper and resolve all dependent
|
|
230
|
+
for cur_dependent_attr in dependent_attr:
|
|
231
|
+
recursive_parameter_loop_check(for_attribute, cur_dependent_attr)
|
|
232
|
+
return
|
|
233
|
+
|
|
234
|
+
for cur_attr in parametrization.keys():
|
|
235
|
+
recursive_parameter_loop_check(cur_attr, cur_attr)
|
|
236
|
+
|
|
237
|
+
def get_next_parent_class(self) -> Union[Type[Scenario], None]:
|
|
238
|
+
"""
|
|
239
|
+
This method returns the next parent class which is a subclass of the :class:`Scenario` itself.
|
|
240
|
+
|
|
241
|
+
:return: returns the next parent class or None if the next parent class is :class:`Scenario`
|
|
242
|
+
itself
|
|
243
|
+
"""
|
|
244
|
+
next_base_class = None
|
|
245
|
+
for cur_base in self.related_cls.__bases__:
|
|
246
|
+
if issubclass(cur_base, Scenario):
|
|
247
|
+
if next_base_class is not None:
|
|
248
|
+
raise MultiInheritanceError(
|
|
249
|
+
f"found more than one Scenario parent classes for `{self.related_cls.__name__}` "
|
|
250
|
+
f"- multi inheritance is not allowed for Scenario/Setup classes")
|
|
251
|
+
next_base_class = cur_base
|
|
252
|
+
if next_base_class == Scenario:
|
|
253
|
+
return None
|
|
254
|
+
return next_base_class
|
|
255
|
+
|
|
256
|
+
def get_all_test_methods(self) -> List[callable]:
|
|
257
|
+
"""
|
|
258
|
+
This method returns all test methods that were defined in the related scenario. A testmethod has to start with
|
|
259
|
+
`test_*`.
|
|
260
|
+
"""
|
|
261
|
+
all_relevant_func = []
|
|
262
|
+
|
|
263
|
+
all_methods = inspect.getmembers(self.related_cls, inspect.isfunction)
|
|
264
|
+
for cur_method_name, cur_function in all_methods:
|
|
265
|
+
if cur_method_name.startswith('test_'):
|
|
266
|
+
all_relevant_func.append(cur_function)
|
|
267
|
+
|
|
268
|
+
return all_relevant_func
|
|
269
|
+
|
|
270
|
+
def get_ignore_test_methods(self) -> List[callable]:
|
|
271
|
+
"""
|
|
272
|
+
This method returns a list of all methods that have the IGNORE marker. It automatically resolves marker that
|
|
273
|
+
were provided on parent class scenarios.
|
|
274
|
+
"""
|
|
275
|
+
result = []
|
|
276
|
+
next_parent_class = get_scenario_inheritance_list_of(self.related_cls)[1]
|
|
277
|
+
if next_parent_class != Scenario:
|
|
278
|
+
next_parent_class_controller = ScenarioController.get_for(next_parent_class)
|
|
279
|
+
next_parent_ignore_meths = next_parent_class_controller.get_ignore_test_methods()
|
|
280
|
+
result.extend(next_parent_ignore_meths)
|
|
281
|
+
for cur_ignore_meth_as_str in self.related_cls.IGNORE:
|
|
282
|
+
cur_ignore_meth = getattr(self.related_cls, cur_ignore_meth_as_str)
|
|
283
|
+
result.append(cur_ignore_meth)
|
|
284
|
+
return list(set(result))
|
|
285
|
+
|
|
286
|
+
def get_skip_test_methods(self) -> List[callable]:
|
|
287
|
+
"""
|
|
288
|
+
This method returns a list of all methods that have the SKIP marker. It automatically resolves marker that were
|
|
289
|
+
provided on parent class scenarios.
|
|
290
|
+
"""
|
|
291
|
+
result = []
|
|
292
|
+
next_parent_class = get_scenario_inheritance_list_of(self.related_cls)[1]
|
|
293
|
+
next_parent_ignore_meths = []
|
|
294
|
+
|
|
295
|
+
if next_parent_class != Scenario:
|
|
296
|
+
next_parent_class_controller = ScenarioController.get_for(next_parent_class)
|
|
297
|
+
next_parent_ignore_meths = next_parent_class_controller.get_ignore_test_methods()
|
|
298
|
+
next_parent_skip_meths = next_parent_class_controller.get_skip_test_methods()
|
|
299
|
+
result.extend(next_parent_skip_meths)
|
|
300
|
+
|
|
301
|
+
for cur_skip_meth_as_str in self.related_cls.SKIP:
|
|
302
|
+
cur_skip_meth = getattr(self.related_cls, cur_skip_meth_as_str)
|
|
303
|
+
if cur_skip_meth in next_parent_ignore_meths:
|
|
304
|
+
raise ValueError(f'found skip method `{cur_skip_meth}` defined in `{self.related_cls}.SKIP`, but was '
|
|
305
|
+
f'already added to IGNORE in parent class')
|
|
306
|
+
result.append(cur_skip_meth)
|
|
307
|
+
|
|
308
|
+
return list(set(result))
|
|
309
|
+
|
|
310
|
+
def get_run_test_methods(self) -> List[callable]:
|
|
311
|
+
"""
|
|
312
|
+
This method returns a list of all methods that should run in this scenario. It automatically resolves
|
|
313
|
+
SKIP/IGNORE marker that were provided on parent class scenarios.
|
|
314
|
+
"""
|
|
315
|
+
result = (set(self.get_all_test_methods())
|
|
316
|
+
- set(self.get_skip_test_methods())
|
|
317
|
+
- set(self.get_ignore_test_methods()))
|
|
318
|
+
return list(result)
|
|
319
|
+
|
|
320
|
+
def validate_feature_clearance_for_parallel_connections(self):
|
|
321
|
+
"""
|
|
322
|
+
This method validates for every active class-based feature (only the ones that have an active VDevice<->Device
|
|
323
|
+
mapping), that there exist a clear scenario-device-connection for this feature. The method throws an
|
|
324
|
+
:class:`UnclearAssignableFeatureConnectionError` if there exists more than one possible device-connection
|
|
325
|
+
for the related devices and the method is not able to determine a clear connection.
|
|
326
|
+
"""
|
|
327
|
+
|
|
328
|
+
for cur_from_device in self.get_all_abs_inner_device_classes():
|
|
329
|
+
# determine all VDevice-Device mappings for this one, by iterating over all instantiated Feature classes
|
|
330
|
+
cur_from_device_instantiated_features = \
|
|
331
|
+
DeviceController.get_for(cur_from_device).get_all_instantiated_feature_objects()
|
|
332
|
+
for _, cur_feature in cur_from_device_instantiated_features.items():
|
|
333
|
+
mapped_vdevice, mapped_device = cur_feature.active_vdevice_device_mapping
|
|
334
|
+
if mapped_device is None:
|
|
335
|
+
# ignore this, because we have no vDevices here
|
|
336
|
+
continue
|
|
337
|
+
|
|
338
|
+
# now check if one or more single of the classbased connection are CONTAINED IN the possible
|
|
339
|
+
# parallel connection (only if there exists more than one parallel)
|
|
340
|
+
feature_cnn = FeatureController.get_for(
|
|
341
|
+
cur_feature.__class__).get_abs_class_based_for_vdevice()[mapped_vdevice]
|
|
342
|
+
|
|
343
|
+
# search node names that is the relevant connection
|
|
344
|
+
relevant_cnns: List[Connection] = []
|
|
345
|
+
mapped_device_abs_cnns = DeviceController.get_for(mapped_device).get_all_absolute_connections()
|
|
346
|
+
for _, all_connections in mapped_device_abs_cnns.items():
|
|
347
|
+
relevant_cnns += [cur_cnn for cur_cnn in all_connections
|
|
348
|
+
if cur_cnn.has_connection_from_to(cur_from_device, end_device=mapped_device)]
|
|
349
|
+
|
|
350
|
+
if len(relevant_cnns) <= 1:
|
|
351
|
+
# ignore if there are not more than one relevant connection
|
|
352
|
+
continue
|
|
353
|
+
|
|
354
|
+
# there are some parallel connections -> check that only one fits with the feature
|
|
355
|
+
matched_relevant_cnns = []
|
|
356
|
+
for cur_relevant_cnn in relevant_cnns:
|
|
357
|
+
cur_relevant_cnn_singles = cur_relevant_cnn.get_singles()
|
|
358
|
+
|
|
359
|
+
for cur_relevant_single in cur_relevant_cnn_singles:
|
|
360
|
+
matches = [True for cur_feature_cnn in feature_cnn.get_singles()
|
|
361
|
+
if cur_feature_cnn.contained_in(cur_relevant_single)]
|
|
362
|
+
if len(matches):
|
|
363
|
+
matched_relevant_cnns.append(True)
|
|
364
|
+
break
|
|
365
|
+
if sum(matched_relevant_cnns) > 1:
|
|
366
|
+
raise UnclearAssignableFeatureConnectionError(
|
|
367
|
+
f"the devices {cur_from_device.__name__} and {mapped_device.__name__} have "
|
|
368
|
+
f"multiple parallel connections - the device `{cur_from_device.__name__}` uses a "
|
|
369
|
+
f"feature `{cur_feature.__class__.__name__}` that matches with the device "
|
|
370
|
+
f"`{mapped_device.__name__}`, but it is not clear which of the parallel connection "
|
|
371
|
+
f"could be used")
|
|
372
|
+
|
|
373
|
+
def get_feature_cleaned_absolute_single_connections(self) -> \
|
|
374
|
+
Tuple[Dict[Type[Device], Dict[str, Dict[Type[Device], Dict[str, List[Connection]]]]], Dict[
|
|
375
|
+
Tuple[Device, Device], List[Connection]]]:
|
|
376
|
+
"""
|
|
377
|
+
This method returns all absolute-single connections between all devices of this scenario, but already cleaned
|
|
378
|
+
based on the cumulated class-based decorators of all the feature devices.
|
|
379
|
+
|
|
380
|
+
.. note::
|
|
381
|
+
Please note, that the reduction candidates connections do not have to be unique.
|
|
382
|
+
|
|
383
|
+
:return: returns a tuple with the cleaned up connections (sorted as dictionary per device) and the reduced
|
|
384
|
+
connections as second element
|
|
385
|
+
"""
|
|
386
|
+
reduction_candidates = {}
|
|
387
|
+
|
|
388
|
+
def add_reduction_candidate(device, other_device, connections: Connection):
|
|
389
|
+
|
|
390
|
+
if (device, other_device) not in reduction_candidates.keys() and \
|
|
391
|
+
(other_device, device) not in reduction_candidates.keys():
|
|
392
|
+
# we have to add it as new list
|
|
393
|
+
reduction_candidates[(device, other_device)] = [connections]
|
|
394
|
+
elif (device, other_device) in reduction_candidates.keys():
|
|
395
|
+
reduction_candidates[(device, other_device)].append(connections)
|
|
396
|
+
elif (other_device, device) in reduction_candidates.keys():
|
|
397
|
+
reduction_candidates[(other_device, device)].append(connections)
|
|
398
|
+
|
|
399
|
+
# start to generate the singles for every connection between the devices of every scenario
|
|
400
|
+
all_abs_single_connections = self.get_absolute_single_connections()
|
|
401
|
+
|
|
402
|
+
def reduce_based_on_feature_cnns_for_devices(
|
|
403
|
+
feature_cnn: Connection, dev1: Type[Device], node_dev1: str, dev2: Type[Device], node_dev2: str):
|
|
404
|
+
# execute further process only if there is exactly one relevant connection
|
|
405
|
+
start_length_before_reduction = \
|
|
406
|
+
len(all_abs_single_connections[dev1][node_dev1][
|
|
407
|
+
dev2][node_dev2])
|
|
408
|
+
for cur_abs_connection in \
|
|
409
|
+
all_abs_single_connections[dev1][node_dev1][
|
|
410
|
+
dev2][node_dev2].copy():
|
|
411
|
+
if not feature_cnn.contained_in(cur_abs_connection, ignore_metadata=True):
|
|
412
|
+
# this abs single connection is not fulfilled by the current feature -> remove it
|
|
413
|
+
all_abs_single_connections[dev1][node_dev1][
|
|
414
|
+
dev2][node_dev2].remove(cur_abs_connection)
|
|
415
|
+
add_reduction_candidate(dev1, dev2, cur_abs_connection)
|
|
416
|
+
if start_length_before_reduction > 0 and \
|
|
417
|
+
len(all_abs_single_connections[dev1][node_dev1][
|
|
418
|
+
dev2][node_dev2]) == 0:
|
|
419
|
+
raise ConnectionIntersectionError(
|
|
420
|
+
f"the `{self.related_cls.__name__}` has a connection from device "
|
|
421
|
+
f"`{dev1.__name__}` to `{dev2.__name__}` - some mapped VDevices of "
|
|
422
|
+
f"their feature classes define mismatched connections")
|
|
423
|
+
|
|
424
|
+
def get_single_cnns_between_device_for_feature(from_device, to_device, relevant_feature_cnn):
|
|
425
|
+
# search node names that is the relevant connection
|
|
426
|
+
relevant_cnns: List[List[Connection]] = []
|
|
427
|
+
for _, cur_node_data in all_abs_single_connections[from_device].items():
|
|
428
|
+
for cur_to_device, cur_to_device_data in cur_node_data.items():
|
|
429
|
+
if cur_to_device == to_device:
|
|
430
|
+
relevant_cnns += [cur_cnns for _, cur_cnns in cur_to_device_data.items()]
|
|
431
|
+
|
|
432
|
+
result_singles = None
|
|
433
|
+
if len(relevant_cnns) > 1:
|
|
434
|
+
# there exists parallel connections - filter only the relevant one
|
|
435
|
+
for cur_single_cnns in relevant_cnns:
|
|
436
|
+
for cur_single_cnn in cur_single_cnns:
|
|
437
|
+
if relevant_feature_cnn.contained_in(cur_single_cnn):
|
|
438
|
+
# this is the relevant connection (all other can not fit, because we have
|
|
439
|
+
# already checked this with method
|
|
440
|
+
# `scenario_controller.validate_feature_clearance_for_parallel_connections()`)
|
|
441
|
+
result_singles = cur_single_cnns
|
|
442
|
+
break
|
|
443
|
+
if result_singles is not None:
|
|
444
|
+
break
|
|
445
|
+
elif len(relevant_cnns) == 1:
|
|
446
|
+
result_singles = relevant_cnns[0]
|
|
447
|
+
if result_singles is None:
|
|
448
|
+
raise ValueError("can not find relevant connection of all parallel connections")
|
|
449
|
+
return result_singles
|
|
450
|
+
|
|
451
|
+
all_devices = self.get_all_abs_inner_device_classes()
|
|
452
|
+
for cur_from_device in all_devices:
|
|
453
|
+
# determine all VDevice-Device mappings for this one, by iterating over all instantiated Feature classes
|
|
454
|
+
cur_from_device_instantiated_features = \
|
|
455
|
+
DeviceController.get_for(cur_from_device).get_all_instantiated_feature_objects()
|
|
456
|
+
for _, cur_feature in cur_from_device_instantiated_features.items():
|
|
457
|
+
mapped_vdevice, mapped_device = cur_feature.active_vdevice_device_mapping
|
|
458
|
+
if mapped_device is None:
|
|
459
|
+
# ignore this, because we have no vDevices here
|
|
460
|
+
continue
|
|
461
|
+
|
|
462
|
+
# now try to reduce the scenario connections according to the requirements of the feature class
|
|
463
|
+
cur_feature_cnn = \
|
|
464
|
+
FeatureController.get_for(
|
|
465
|
+
cur_feature.__class__).get_abs_class_based_for_vdevice()[mapped_vdevice]
|
|
466
|
+
|
|
467
|
+
device_cnn_singles = get_single_cnns_between_device_for_feature(
|
|
468
|
+
from_device=cur_from_device, to_device=mapped_device, relevant_feature_cnn=cur_feature_cnn)
|
|
469
|
+
|
|
470
|
+
reduce_based_on_feature_cnns_for_devices(
|
|
471
|
+
cur_feature_cnn, device_cnn_singles[0].from_device, device_cnn_singles[0].from_node_name,
|
|
472
|
+
device_cnn_singles[0].to_device, device_cnn_singles[0].to_node_name)
|
|
473
|
+
# do the same for the opposite direction (features are always bidirectional)
|
|
474
|
+
reduce_based_on_feature_cnns_for_devices(
|
|
475
|
+
cur_feature_cnn, device_cnn_singles[0].to_device, device_cnn_singles[0].to_node_name,
|
|
476
|
+
device_cnn_singles[0].from_device, device_cnn_singles[0].from_node_name)
|
|
477
|
+
|
|
478
|
+
return all_abs_single_connections, reduction_candidates
|
|
479
|
+
|
|
480
|
+
def determine_absolute_device_connections(self):
|
|
481
|
+
"""
|
|
482
|
+
This method determines the real possible Sub-Connections for every element of the scenarios. For this the method
|
|
483
|
+
will create a possible intersection connection, for the :class:´Connection´ between two devices and
|
|
484
|
+
all :class:`Connection`-Subtrees that are allowed for the mapped vDevices in the used :class:`Feature`
|
|
485
|
+
classes.
|
|
486
|
+
The data will be saved in the :class:`Device` property ``_absolute_connections``. If the method detects an empty
|
|
487
|
+
intersection between two devices that are connected through a VDevice-Device mapping, the method will throw an
|
|
488
|
+
exception.
|
|
489
|
+
"""
|
|
490
|
+
|
|
491
|
+
# start to generate the singles for every connection between the devices of every scenario
|
|
492
|
+
all_abs_single_connections, reduction_candidates = self.get_feature_cleaned_absolute_single_connections()
|
|
493
|
+
|
|
494
|
+
# generate all required warnings
|
|
495
|
+
for cur_warning_tuple in reduction_candidates:
|
|
496
|
+
logger.warning(f"detect some connections between the devices `{cur_warning_tuple[0].__name__}` and "
|
|
497
|
+
f"`{cur_warning_tuple[1].__name__}` of scenario `{self.related_cls.__name__}` that can be "
|
|
498
|
+
f"reduced, because their related features only use a subset of the defined connection")
|
|
499
|
+
|
|
500
|
+
# first cleanup the relevant absolute connections
|
|
501
|
+
for cur_from_device, from_device_data in all_abs_single_connections.items():
|
|
502
|
+
for _, from_node_data in from_device_data.items():
|
|
503
|
+
for cur_to_device, to_device_data in from_node_data.items():
|
|
504
|
+
cur_from_device_controller = DeviceController.get_for(cur_from_device)
|
|
505
|
+
cur_to_device_controller = DeviceController.get_for(cur_to_device)
|
|
506
|
+
|
|
507
|
+
cur_from_device_controller.cleanup_absolute_connections_with(cur_to_device)
|
|
508
|
+
cur_to_device_controller.cleanup_absolute_connections_with(cur_from_device)
|
|
509
|
+
|
|
510
|
+
# replace all absolute connection with the single ones
|
|
511
|
+
for cur_from_device, from_device_data in all_abs_single_connections.items():
|
|
512
|
+
for _, from_node_data in from_device_data.items():
|
|
513
|
+
for cur_to_device, to_device_data in from_node_data.items():
|
|
514
|
+
for _, cur_single_cnns in to_device_data.items():
|
|
515
|
+
|
|
516
|
+
cur_from_device_controller = DeviceController.get_for(cur_from_device)
|
|
517
|
+
cur_to_device_controller = DeviceController.get_for(cur_to_device)
|
|
518
|
+
|
|
519
|
+
new_cnn = Connection.based_on(OrConnectionRelation(*cur_single_cnns))
|
|
520
|
+
new_cnn.set_metadata_for_all_subitems(cur_single_cnns[0].metadata)
|
|
521
|
+
if cur_from_device == cur_single_cnns[0].from_device:
|
|
522
|
+
cur_from_device_controller.add_new_absolute_connection(new_cnn)
|
|
523
|
+
else:
|
|
524
|
+
cur_to_device_controller.add_new_absolute_connection(new_cnn)
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Type, Dict, Union, TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
from _balder.setup import Setup
|
|
6
|
+
from _balder.exceptions import IllegalVDeviceMappingError, MultiInheritanceError
|
|
7
|
+
from _balder.controllers.feature_controller import FeatureController
|
|
8
|
+
from _balder.controllers.device_controller import DeviceController
|
|
9
|
+
from _balder.controllers.normal_scenario_setup_controller import NormalScenarioSetupController
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__file__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SetupController(NormalScenarioSetupController):
|
|
18
|
+
"""
|
|
19
|
+
This is the controller class for :class:`Setup` items.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
# helper property to disable manual constructor creation
|
|
23
|
+
__priv_instantiate_key = object()
|
|
24
|
+
|
|
25
|
+
#: contains all existing setups and its corresponding controller object
|
|
26
|
+
_items: Dict[Type[Setup], SetupController] = {}
|
|
27
|
+
|
|
28
|
+
def __init__(self, related_cls, _priv_instantiate_key):
|
|
29
|
+
|
|
30
|
+
# describes if the current controller is for setups or for scenarios (has to be set in child controller)
|
|
31
|
+
self._related_type = Setup
|
|
32
|
+
|
|
33
|
+
# this helps to make this constructor only possible inside the controller object
|
|
34
|
+
if _priv_instantiate_key != SetupController.__priv_instantiate_key:
|
|
35
|
+
raise RuntimeError('it is not allowed to instantiate a controller manually -> use the static method '
|
|
36
|
+
'`SetupController.get_for()` for it')
|
|
37
|
+
|
|
38
|
+
if not isinstance(related_cls, type):
|
|
39
|
+
raise TypeError('the attribute `related_cls` has to be a type (no object)')
|
|
40
|
+
if not issubclass(related_cls, Setup):
|
|
41
|
+
raise TypeError(f'the attribute `related_cls` has to be a sub-type of `{Setup.__name__}`')
|
|
42
|
+
if related_cls == Setup:
|
|
43
|
+
raise TypeError(f'the attribute `related_cls` is `{Setup.__name__}` - controllers for native type are '
|
|
44
|
+
f'forbidden')
|
|
45
|
+
# contains a reference to the related class this controller instance belongs to
|
|
46
|
+
self._related_cls = related_cls
|
|
47
|
+
|
|
48
|
+
# ---------------------------------- STATIC METHODS ----------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
@staticmethod
|
|
51
|
+
def get_for(related_cls: Type[Setup]) -> SetupController:
|
|
52
|
+
"""
|
|
53
|
+
This class returns the current existing controller instance for the given item. If the instance does not exist
|
|
54
|
+
yet, it will automatically create it and saves the instance in an internal dictionary.
|
|
55
|
+
"""
|
|
56
|
+
if SetupController._items.get(related_cls) is None:
|
|
57
|
+
item = SetupController(related_cls, _priv_instantiate_key=SetupController.__priv_instantiate_key)
|
|
58
|
+
SetupController._items[related_cls] = item
|
|
59
|
+
|
|
60
|
+
return SetupController._items.get(related_cls)
|
|
61
|
+
|
|
62
|
+
# ---------------------------------- CLASS METHODS -----------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
# ---------------------------------- PROPERTIES --------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def related_cls(self) -> Type[Setup]:
|
|
68
|
+
return self._related_cls
|
|
69
|
+
|
|
70
|
+
# ---------------------------------- PROTECTED METHODS -------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
# ---------------------------------- METHODS -----------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
def get_next_parent_class(self) -> Union[Type[Setup], None]:
|
|
75
|
+
"""
|
|
76
|
+
This method returns the next parent class which is a subclass of the :class:`Setup` itself.
|
|
77
|
+
|
|
78
|
+
:return: returns the next parent class or None if the next parent class is :class:`Setup`
|
|
79
|
+
itself
|
|
80
|
+
"""
|
|
81
|
+
next_base_class = None
|
|
82
|
+
for cur_base in self.related_cls.__bases__:
|
|
83
|
+
if issubclass(cur_base, Setup):
|
|
84
|
+
if next_base_class is not None:
|
|
85
|
+
raise MultiInheritanceError(
|
|
86
|
+
f"found more than one Setup parent classes for `{self.related_cls.__name__}` "
|
|
87
|
+
f"- multi inheritance is not allowed for Scenario/Setup classes")
|
|
88
|
+
next_base_class = cur_base
|
|
89
|
+
if next_base_class == Setup:
|
|
90
|
+
return None
|
|
91
|
+
return next_base_class
|
|
92
|
+
|
|
93
|
+
def validate_feature_possibility(self):
|
|
94
|
+
"""
|
|
95
|
+
This method validates that every feature connection (that already has a vDevice<->Device mapping on setup level)
|
|
96
|
+
has a connection that is CONTAINED-IN the connection of the related setup devices.
|
|
97
|
+
"""
|
|
98
|
+
all_devices = self.get_all_abs_inner_device_classes()
|
|
99
|
+
for cur_device in all_devices:
|
|
100
|
+
cur_device_instantiated_features = \
|
|
101
|
+
DeviceController.get_for(cur_device).get_all_instantiated_feature_objects()
|
|
102
|
+
for _, cur_feature in cur_device_instantiated_features.items():
|
|
103
|
+
mapped_vdevice, mapped_device = cur_feature.active_vdevice_device_mapping
|
|
104
|
+
if mapped_device is None:
|
|
105
|
+
# ignore this, because we have no vDevice mapping on setup level
|
|
106
|
+
continue
|
|
107
|
+
|
|
108
|
+
cur_feature_controller = FeatureController.get_for(cur_feature.__class__)
|
|
109
|
+
feature_class_based_for_vdevice = cur_feature_controller.get_abs_class_based_for_vdevice()
|
|
110
|
+
if not feature_class_based_for_vdevice or mapped_vdevice not in feature_class_based_for_vdevice.keys():
|
|
111
|
+
# there exists no class based for vdevice information (at least for the current active vdevice)
|
|
112
|
+
continue
|
|
113
|
+
|
|
114
|
+
# there exists a class based requirement for this vDevice
|
|
115
|
+
class_based_cnn = feature_class_based_for_vdevice[mapped_vdevice]
|
|
116
|
+
# search relevant connection
|
|
117
|
+
cur_device_controller = DeviceController.get_for(cur_device)
|
|
118
|
+
for _, cur_cnn_list in cur_device_controller.get_all_absolute_connections().items():
|
|
119
|
+
for cur_cnn in cur_cnn_list:
|
|
120
|
+
if not cur_cnn.has_connection_from_to(cur_device, end_device=mapped_device):
|
|
121
|
+
# this connection can be ignored, because it is no connection between the current device
|
|
122
|
+
# and the mapped device
|
|
123
|
+
continue
|
|
124
|
+
# check if the class-based feature connection is CONTAINED-IN this
|
|
125
|
+
# absolute-connection
|
|
126
|
+
if not class_based_cnn.contained_in(cur_cnn, ignore_metadata=True):
|
|
127
|
+
|
|
128
|
+
raise IllegalVDeviceMappingError(
|
|
129
|
+
f"the @for_vdevice connection for vDevice `{mapped_vdevice.__name__}` "
|
|
130
|
+
f"of feature `{cur_feature.__class__.__name__}` (used in "
|
|
131
|
+
f"`{cur_device.__qualname__}`) uses a connection that does not fit "
|
|
132
|
+
f"with the connection defined in setup class "
|
|
133
|
+
f"`{cur_device_controller.get_outer_class().__name__}` to related "
|
|
134
|
+
f"device `{mapped_device.__name__}`")
|