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.
Files changed (89) hide show
  1. _balder/__init__.py +12 -0
  2. _balder/_version.py +34 -0
  3. _balder/balder_plugin.py +73 -0
  4. _balder/balder_session.py +341 -0
  5. _balder/balder_settings.py +15 -0
  6. _balder/cnnrelations/__init__.py +7 -0
  7. _balder/cnnrelations/and_connection_relation.py +176 -0
  8. _balder/cnnrelations/base_connection_relation.py +270 -0
  9. _balder/cnnrelations/or_connection_relation.py +65 -0
  10. _balder/collector.py +874 -0
  11. _balder/connection.py +863 -0
  12. _balder/connection_metadata.py +255 -0
  13. _balder/console/__init__.py +0 -0
  14. _balder/console/balder.py +58 -0
  15. _balder/controllers/__init__.py +12 -0
  16. _balder/controllers/base_device_controller.py +72 -0
  17. _balder/controllers/controller.py +29 -0
  18. _balder/controllers/device_controller.py +446 -0
  19. _balder/controllers/feature_controller.py +715 -0
  20. _balder/controllers/normal_scenario_setup_controller.py +402 -0
  21. _balder/controllers/scenario_controller.py +524 -0
  22. _balder/controllers/setup_controller.py +134 -0
  23. _balder/controllers/vdevice_controller.py +95 -0
  24. _balder/decorator_connect.py +104 -0
  25. _balder/decorator_covered_by.py +74 -0
  26. _balder/decorator_fixture.py +29 -0
  27. _balder/decorator_for_vdevice.py +118 -0
  28. _balder/decorator_gateway.py +34 -0
  29. _balder/decorator_insert_into_tree.py +52 -0
  30. _balder/decorator_parametrize.py +31 -0
  31. _balder/decorator_parametrize_by_feature.py +36 -0
  32. _balder/device.py +18 -0
  33. _balder/exceptions.py +182 -0
  34. _balder/executor/__init__.py +0 -0
  35. _balder/executor/basic_executable_executor.py +133 -0
  36. _balder/executor/basic_executor.py +205 -0
  37. _balder/executor/executor_tree.py +217 -0
  38. _balder/executor/parametrized_testcase_executor.py +52 -0
  39. _balder/executor/scenario_executor.py +169 -0
  40. _balder/executor/setup_executor.py +163 -0
  41. _balder/executor/testcase_executor.py +203 -0
  42. _balder/executor/unresolved_parametrized_testcase_executor.py +184 -0
  43. _balder/executor/variation_executor.py +882 -0
  44. _balder/exit_code.py +19 -0
  45. _balder/feature.py +74 -0
  46. _balder/feature_replacement_mapping.py +107 -0
  47. _balder/feature_vdevice_mapping.py +88 -0
  48. _balder/fixture_definition_scope.py +19 -0
  49. _balder/fixture_execution_level.py +22 -0
  50. _balder/fixture_manager.py +483 -0
  51. _balder/fixture_metadata.py +26 -0
  52. _balder/node_gateway.py +103 -0
  53. _balder/objects/__init__.py +0 -0
  54. _balder/objects/connections/__init__.py +0 -0
  55. _balder/objects/connections/osi_1_physical.py +116 -0
  56. _balder/objects/connections/osi_2_datalink.py +35 -0
  57. _balder/objects/connections/osi_3_network.py +47 -0
  58. _balder/objects/connections/osi_4_transport.py +40 -0
  59. _balder/objects/connections/osi_5_session.py +13 -0
  60. _balder/objects/connections/osi_6_presentation.py +13 -0
  61. _balder/objects/connections/osi_7_application.py +83 -0
  62. _balder/objects/devices/__init__.py +0 -0
  63. _balder/objects/devices/this_device.py +12 -0
  64. _balder/parametrization.py +75 -0
  65. _balder/plugin_manager.py +138 -0
  66. _balder/previous_executor_mark.py +23 -0
  67. _balder/routing_path.py +335 -0
  68. _balder/scenario.py +20 -0
  69. _balder/setup.py +18 -0
  70. _balder/solver.py +246 -0
  71. _balder/testresult.py +163 -0
  72. _balder/unmapped_vdevice.py +13 -0
  73. _balder/utils/__init__.py +0 -0
  74. _balder/utils/functions.py +103 -0
  75. _balder/utils/inner_device_managing_metaclass.py +14 -0
  76. _balder/utils/mixin_can_be_covered_by_executor.py +24 -0
  77. _balder/utils/typings.py +4 -0
  78. _balder/vdevice.py +9 -0
  79. balder/__init__.py +56 -0
  80. balder/connections.py +43 -0
  81. balder/devices.py +9 -0
  82. balder/exceptions.py +44 -0
  83. balder/parametrization.py +8 -0
  84. baldertest-0.1.0.dist-info/METADATA +356 -0
  85. baldertest-0.1.0.dist-info/RECORD +89 -0
  86. baldertest-0.1.0.dist-info/WHEEL +5 -0
  87. baldertest-0.1.0.dist-info/entry_points.txt +2 -0
  88. baldertest-0.1.0.dist-info/licenses/LICENSE +21 -0
  89. 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__}`")