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
_balder/routing_path.py
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import List, Union, Dict, Type, Tuple, Iterable, TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
import copy
|
|
5
|
+
from _balder.connection import Connection
|
|
6
|
+
from _balder.node_gateway import NodeGateway
|
|
7
|
+
from _balder.controllers import DeviceController
|
|
8
|
+
from _balder.exceptions import RoutingBrokenChainError
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from _balder.device import Device
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class RoutingPath:
|
|
15
|
+
"""
|
|
16
|
+
This object describes a possible routing path between two devices (which may be possible via several individual
|
|
17
|
+
connections). If there are several possible paths between two devices, there are correspondingly many of these
|
|
18
|
+
RoutingPath objects.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, *routing_elems: Union[Connection, NodeGateway], start_device: Type[Device],
|
|
22
|
+
start_node_name: str):
|
|
23
|
+
"""
|
|
24
|
+
:param routing_elems: all elements that are parts of this route
|
|
25
|
+
|
|
26
|
+
:param start_device: the device this route starts with (if the first element is a gateway this is optional)
|
|
27
|
+
|
|
28
|
+
:param start_node_name: the node this route starts from (always required!)
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
self._routing_elems = []
|
|
32
|
+
|
|
33
|
+
self._start_device = start_device
|
|
34
|
+
self._start_node_name = start_node_name
|
|
35
|
+
|
|
36
|
+
if not isinstance(routing_elems[0], Connection):
|
|
37
|
+
raise TypeError("the first element is no `Connection` object - every route has to start and end with a"
|
|
38
|
+
"`Connection` object")
|
|
39
|
+
if not isinstance(routing_elems[-1], Connection):
|
|
40
|
+
raise TypeError("the last element is no `Connection` object - every route has to start and end with a"
|
|
41
|
+
"`Connection` object")
|
|
42
|
+
|
|
43
|
+
first_elem = routing_elems[0]
|
|
44
|
+
if isinstance(first_elem, Connection):
|
|
45
|
+
if self._start_device not in (first_elem.from_device, first_elem.to_device):
|
|
46
|
+
raise ValueError(f"the given `start_device={self._start_device.__name__}` does not match with one of "
|
|
47
|
+
f"the available devices of the first routing element")
|
|
48
|
+
if self._start_node_name not in (first_elem.from_node_name, first_elem.to_node_name):
|
|
49
|
+
raise ValueError(f"the given `start_node_name={self._start_node_name}` does not match with one of the "
|
|
50
|
+
f"available node names of the first routing element")
|
|
51
|
+
else:
|
|
52
|
+
raise TypeError(f"the first given element is not an instance of `{Connection.__name__}` or "
|
|
53
|
+
f"`{NodeGateway.__name__}` (is `{type(first_elem)}`)")
|
|
54
|
+
|
|
55
|
+
for cur_elem in routing_elems:
|
|
56
|
+
self.append_element(cur_elem)
|
|
57
|
+
|
|
58
|
+
# ---------------------------------- STATIC METHODS ----------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
@staticmethod
|
|
61
|
+
def __get_abs_setup_dev_cnns_for(setup_devices: Iterable[Type[Device]]) -> List[Connection]:
|
|
62
|
+
"""
|
|
63
|
+
Determines all absolute device connections for a given list of setup devices.
|
|
64
|
+
"""
|
|
65
|
+
setup_devices_cnns = []
|
|
66
|
+
for cur_setup_device in setup_devices:
|
|
67
|
+
for cur_cnn_list in DeviceController.get_for(cur_setup_device).get_all_absolute_connections().values():
|
|
68
|
+
setup_devices_cnns.extend(cur_cnn_list)
|
|
69
|
+
# remove duplicates
|
|
70
|
+
return list(set(setup_devices_cnns))
|
|
71
|
+
|
|
72
|
+
@staticmethod
|
|
73
|
+
def route_through(
|
|
74
|
+
scenario_connection: Connection,
|
|
75
|
+
device_mapping: Dict[Type[Device], Type[Device]],
|
|
76
|
+
alternative_setup_device_cnns: Union[List[Connection], None] = None
|
|
77
|
+
) -> List[RoutingPath]:
|
|
78
|
+
"""
|
|
79
|
+
This static method tries to route the given ``scenario_connection`` with the device_mapping. It returns a list
|
|
80
|
+
of all matched routings between the mapped devices, where the routing is valid to support the requested
|
|
81
|
+
`scenario_connection`.
|
|
82
|
+
|
|
83
|
+
:param scenario_connection: the scenario-device connection object
|
|
84
|
+
|
|
85
|
+
:param device_mapping: the used device mapping for the given `scenario_connection`
|
|
86
|
+
|
|
87
|
+
:param alternative_setup_device_cnns: the alternative used connections between all relevant setup devices (if
|
|
88
|
+
this is none, the router uses the setup-device connections from method
|
|
89
|
+
`get_all_absolute_connections()`, otherwise it uses this dictionary here)
|
|
90
|
+
"""
|
|
91
|
+
setup_devices_cnns = alternative_setup_device_cnns
|
|
92
|
+
if alternative_setup_device_cnns is None:
|
|
93
|
+
setup_devices_cnns = RoutingPath.__get_abs_setup_dev_cnns_for(device_mapping.values())
|
|
94
|
+
|
|
95
|
+
from_setup_device = device_mapping[scenario_connection.from_device]
|
|
96
|
+
to_setup_device = device_mapping[scenario_connection.to_device]
|
|
97
|
+
|
|
98
|
+
# contains a list with all routes that start and end correctly
|
|
99
|
+
all_completed_routes = []
|
|
100
|
+
# contains a list with all possible routes
|
|
101
|
+
all_possible_routes = []
|
|
102
|
+
|
|
103
|
+
# add all connection objects that are mentioned in `from_setup_device`
|
|
104
|
+
for cur_from_setup_node_conn in setup_devices_cnns:
|
|
105
|
+
# only if there is a connection outgoing from `from_setup_device`
|
|
106
|
+
if cur_from_setup_node_conn.has_connection_from_to(start_device=from_setup_device):
|
|
107
|
+
all_possible_routes.append(
|
|
108
|
+
RoutingPath(
|
|
109
|
+
cur_from_setup_node_conn,
|
|
110
|
+
start_device=from_setup_device,
|
|
111
|
+
start_node_name=(cur_from_setup_node_conn.from_node_name
|
|
112
|
+
if cur_from_setup_node_conn.from_device == from_setup_device
|
|
113
|
+
else cur_from_setup_node_conn.to_node_name)
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
# now go through every possibility and add them - filter all Routes that ``has_loop() == True`` or
|
|
117
|
+
# are completed
|
|
118
|
+
while len(all_possible_routes) > 0:
|
|
119
|
+
|
|
120
|
+
# remove all routings that have a loop
|
|
121
|
+
all_possible_routes = [route for route in all_possible_routes.copy() if not route.has_loop()]
|
|
122
|
+
|
|
123
|
+
# remove all not working routing because they have the wrong connection type, by checking that one part
|
|
124
|
+
# connection matches the requirements of the given `scenario_connection`
|
|
125
|
+
all_possible_routes = [
|
|
126
|
+
r for r in all_possible_routes
|
|
127
|
+
if scenario_connection.contained_in(r.get_virtual_connection(), ignore_metadata=True)
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
# move all completely routed connections to `all_completed_routes`
|
|
131
|
+
for cur_routing in all_possible_routes.copy():
|
|
132
|
+
if cur_routing.end_device == to_setup_device:
|
|
133
|
+
# note that all routes already have a virtual connection that matches the requirements of given
|
|
134
|
+
# `scenario_connection`
|
|
135
|
+
all_completed_routes.append(cur_routing)
|
|
136
|
+
all_possible_routes.remove(cur_routing)
|
|
137
|
+
|
|
138
|
+
new_possible_routes = []
|
|
139
|
+
for cur_routing in all_possible_routes.copy():
|
|
140
|
+
# add all existing connections
|
|
141
|
+
all_next_conns = [cur_cnn for cur_cnn in setup_devices_cnns
|
|
142
|
+
if ((cur_cnn.from_device == cur_routing.end_device and
|
|
143
|
+
cur_cnn.from_node_name == cur_routing.end_node_name)
|
|
144
|
+
or (cur_cnn.to_device == cur_routing.end_device and
|
|
145
|
+
cur_cnn.to_node_name == cur_routing.end_node_name))]
|
|
146
|
+
for cur_next_conn in all_next_conns:
|
|
147
|
+
if cur_next_conn == cur_routing.elements[-1]:
|
|
148
|
+
# is the same connection as the last of routing -> SKIP
|
|
149
|
+
continue
|
|
150
|
+
|
|
151
|
+
if cur_next_conn.has_connection_from_to(start_device=cur_routing.end_device):
|
|
152
|
+
# the connection allows the direction the routing needs - only then add it
|
|
153
|
+
copied_routing = cur_routing.copy()
|
|
154
|
+
copied_routing.append_element(cur_next_conn)
|
|
155
|
+
new_possible_routes.append(copied_routing)
|
|
156
|
+
# add all possible gateways
|
|
157
|
+
# for cur_next_gateway in cur_routing.end_device._get_all_gateways():
|
|
158
|
+
# if cur_next_gateway == cur_routing.elements[-1]:
|
|
159
|
+
# # is the same gateway as the last one of the routing -> SKIP
|
|
160
|
+
# continue
|
|
161
|
+
# elif cur_next_gateway.has_connection_from_to(start_node=cur_routing.end_node_name):
|
|
162
|
+
# # the gateway allows the direction the routing needs - only then add it
|
|
163
|
+
# copied_routing = cur_routing.copy()
|
|
164
|
+
# copied_routing.append_element(cur_next_gateway)
|
|
165
|
+
# all_possible_routes.append(copied_routing)
|
|
166
|
+
all_possible_routes = new_possible_routes
|
|
167
|
+
|
|
168
|
+
return all_completed_routes
|
|
169
|
+
|
|
170
|
+
# ---------------------------------- CLASS METHODS ----------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
# ---------------------------------- PROPERTIES --------------------------------------------------------------------
|
|
173
|
+
|
|
174
|
+
@property
|
|
175
|
+
def elements(self) -> List[Union[Connection, NodeGateway]]:
|
|
176
|
+
"""returns all elements that belongs to this routing path"""
|
|
177
|
+
return self._routing_elems
|
|
178
|
+
|
|
179
|
+
@property
|
|
180
|
+
def start_device(self) -> Type[Device]:
|
|
181
|
+
"""returns the device the route starts from"""
|
|
182
|
+
return self._start_device
|
|
183
|
+
|
|
184
|
+
@property
|
|
185
|
+
def start_node_name(self) -> str:
|
|
186
|
+
"""returns the node of the `start_device` this route starts from"""
|
|
187
|
+
return self._start_node_name
|
|
188
|
+
|
|
189
|
+
@property
|
|
190
|
+
def end_device(self) -> Type[Device]:
|
|
191
|
+
"""returns the device the route ends"""
|
|
192
|
+
return self._get_end_device_and_node()[0]
|
|
193
|
+
|
|
194
|
+
@property
|
|
195
|
+
def end_node_name(self) -> str:
|
|
196
|
+
"""returns the node of the `end_device` this route ends"""
|
|
197
|
+
return self._get_end_device_and_node()[1]
|
|
198
|
+
|
|
199
|
+
# ---------------------------------- PROTECTED METHODS -------------------------------------------------------------
|
|
200
|
+
|
|
201
|
+
def _get_end_device_and_node(self, till_idx: int = None) -> Tuple[Type[Device], str]:
|
|
202
|
+
"""
|
|
203
|
+
helper method that determines the end_device and end_node_name
|
|
204
|
+
|
|
205
|
+
:param till_idx: the index of the latest element that should be considered
|
|
206
|
+
|
|
207
|
+
:return: a tuple with the latest device and node
|
|
208
|
+
"""
|
|
209
|
+
cur_device = self._start_device
|
|
210
|
+
cur_node_name = self._start_node_name
|
|
211
|
+
|
|
212
|
+
elements = self._routing_elems
|
|
213
|
+
if till_idx is not None:
|
|
214
|
+
elements = self._routing_elems[:till_idx+1]
|
|
215
|
+
|
|
216
|
+
for cur_route_elem in elements:
|
|
217
|
+
if isinstance(cur_route_elem, NodeGateway):
|
|
218
|
+
if cur_node_name == cur_route_elem.from_node_name:
|
|
219
|
+
cur_node_name = cur_route_elem.to_node_name
|
|
220
|
+
elif cur_node_name == cur_route_elem.to_node_name:
|
|
221
|
+
cur_node_name = cur_route_elem.from_node_name
|
|
222
|
+
else:
|
|
223
|
+
raise RoutingBrokenChainError(
|
|
224
|
+
f"can not chain the routing element `{cur_route_elem.__name__}` with the device "
|
|
225
|
+
f"`{cur_device.__name__}` and node `{cur_node_name}` of element before")
|
|
226
|
+
else:
|
|
227
|
+
if cur_node_name == cur_route_elem.from_node_name and cur_device == cur_route_elem.from_device:
|
|
228
|
+
cur_node_name = cur_route_elem.to_node_name
|
|
229
|
+
cur_device = cur_route_elem.to_device
|
|
230
|
+
elif cur_node_name == cur_route_elem.to_node_name and cur_device == cur_route_elem.to_device:
|
|
231
|
+
cur_node_name = cur_route_elem.from_node_name
|
|
232
|
+
cur_device = cur_route_elem.from_device
|
|
233
|
+
else:
|
|
234
|
+
raise RoutingBrokenChainError(
|
|
235
|
+
f"can not chain the routing element `{cur_route_elem.__name__}` with the device "
|
|
236
|
+
f"`{cur_device.__name__}` and node `{cur_node_name}` of element before")
|
|
237
|
+
return cur_device, cur_node_name
|
|
238
|
+
|
|
239
|
+
# ---------------------------------- METHODS -----------------------------------------------------------------------
|
|
240
|
+
|
|
241
|
+
def has_loop(self) -> bool:
|
|
242
|
+
"""
|
|
243
|
+
This method returns True if it detects an internal loop. An internal loop is given, if one :class:`Device`/node
|
|
244
|
+
pair is mentioned twice (or more) in internal `routing_elements`.
|
|
245
|
+
"""
|
|
246
|
+
all_contact_points = [(self.start_device, self.start_node_name)]
|
|
247
|
+
|
|
248
|
+
for idx in range(len(self._routing_elems)):
|
|
249
|
+
next_device, next_node = self._get_end_device_and_node(idx)
|
|
250
|
+
# now check if one point is mentioned twice
|
|
251
|
+
if (next_device, next_node) in all_contact_points:
|
|
252
|
+
return True
|
|
253
|
+
all_contact_points.append((next_device, next_node))
|
|
254
|
+
|
|
255
|
+
return False
|
|
256
|
+
|
|
257
|
+
def is_bidirectional(self):
|
|
258
|
+
"""
|
|
259
|
+
This method checks whether the route is completely bidirectional. If only one connection is not bidirectional,
|
|
260
|
+
it will return False.
|
|
261
|
+
"""
|
|
262
|
+
for cur_elem in self._routing_elems:
|
|
263
|
+
if isinstance(cur_elem, Connection):
|
|
264
|
+
if not cur_elem.is_bidirectional():
|
|
265
|
+
return False
|
|
266
|
+
else:
|
|
267
|
+
if not cur_elem.is_bidirectional():
|
|
268
|
+
return False
|
|
269
|
+
return True
|
|
270
|
+
|
|
271
|
+
def copy(self) -> RoutingPath:
|
|
272
|
+
"""
|
|
273
|
+
This method creates a copy of this routing.
|
|
274
|
+
"""
|
|
275
|
+
copied_elem = copy.copy(self)
|
|
276
|
+
# also copy list reference
|
|
277
|
+
copied_elem._routing_elems = self._routing_elems.copy() # pylint: disable=protected-access
|
|
278
|
+
return copied_elem
|
|
279
|
+
|
|
280
|
+
def append_element(self, elem: Union[Connection, NodeGateway]) -> None:
|
|
281
|
+
"""
|
|
282
|
+
This method appends an element to the route.
|
|
283
|
+
"""
|
|
284
|
+
elem_before = None if len(self._routing_elems) == 0 else self._routing_elems[-1]
|
|
285
|
+
|
|
286
|
+
if not isinstance(elem, Connection) and not isinstance(elem, NodeGateway):
|
|
287
|
+
raise TypeError(f"the given attribute passed at position {len(self._routing_elems)} is not of the type "
|
|
288
|
+
f"`Connection` or `NodeGateway`")
|
|
289
|
+
|
|
290
|
+
if elem_before is not None:
|
|
291
|
+
# check if device and nodes of the two elements are the same -> have to be a chain
|
|
292
|
+
if isinstance(elem, Connection):
|
|
293
|
+
if (self.end_device, self.end_node_name) not in \
|
|
294
|
+
[(elem.from_device, elem.from_node_name), (elem.to_device, elem.to_node_name)]:
|
|
295
|
+
raise RoutingBrokenChainError(
|
|
296
|
+
f"can not append connection, because neither the from-device/node "
|
|
297
|
+
f"(device: `{elem.from_device.__name__}` | node: `{elem.from_node_name}`) nor the "
|
|
298
|
+
f"to-device/node (device: `{elem.to_device.__name__}` | node: `{elem.to_node_name}`) of the "
|
|
299
|
+
f"connection match with the latest end-device/node (device: `{self.end_device.__name__}` | "
|
|
300
|
+
f"node: `{self.end_node_name}`) of this route")
|
|
301
|
+
else:
|
|
302
|
+
# is a gateway class
|
|
303
|
+
if self.end_device != elem.device:
|
|
304
|
+
raise RoutingBrokenChainError(
|
|
305
|
+
f"can not append gateway, because the device of the gateway (`{elem.device}`) doesn't "
|
|
306
|
+
f"match with the latest end-device (`{self.end_device}`) of this route")
|
|
307
|
+
if self.end_node_name not in [elem.from_node_name, elem.to_node_name]:
|
|
308
|
+
raise RoutingBrokenChainError(
|
|
309
|
+
f"can not append gateway, because neither the from-node "
|
|
310
|
+
f"(`{elem.from_node_name}`) nor the to-node (`{elem.to_node_name}`) of the gateway match with "
|
|
311
|
+
f"the latest end-node (`{self.end_node_name}`) of this route")
|
|
312
|
+
|
|
313
|
+
self._routing_elems.append(elem)
|
|
314
|
+
|
|
315
|
+
def get_virtual_connection(self) -> Connection:
|
|
316
|
+
"""
|
|
317
|
+
This method returns a virtual connection object that describes the connection type this routing
|
|
318
|
+
supports for all of its elements.
|
|
319
|
+
"""
|
|
320
|
+
virtual_connection = self.elements[0].clone()
|
|
321
|
+
virtual_connection.set_metadata_for_all_subitems(None)
|
|
322
|
+
for cur_element in self.elements[1:]:
|
|
323
|
+
if isinstance(cur_element, Connection):
|
|
324
|
+
cur_element_clone = cur_element.clone()
|
|
325
|
+
cur_element_clone.set_metadata_for_all_subitems(None)
|
|
326
|
+
virtual_connection = cur_element_clone.intersection_with(virtual_connection)
|
|
327
|
+
else:
|
|
328
|
+
# is a gateway class
|
|
329
|
+
# todo
|
|
330
|
+
pass
|
|
331
|
+
# set metadata based on this routing
|
|
332
|
+
virtual_connection.metadata.set_from(from_device=self.start_device, from_device_node_name=self.start_node_name)
|
|
333
|
+
virtual_connection.metadata.set_to(to_device=self.end_device, to_device_node_name=self.end_node_name)
|
|
334
|
+
|
|
335
|
+
return virtual_connection
|
_balder/scenario.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from _balder.utils.inner_device_managing_metaclass import InnerDeviceManagingMetaclass
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Scenario(metaclass=InnerDeviceManagingMetaclass):
|
|
6
|
+
"""
|
|
7
|
+
This is the basic scenario class. It represents an abstract class that should be the base class for all scenarios.
|
|
8
|
+
"""
|
|
9
|
+
SKIP = []
|
|
10
|
+
IGNORE = []
|
|
11
|
+
|
|
12
|
+
# ---------------------------------- STATIC METHODS ----------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
# ---------------------------------- CLASS METHODS ----------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
# ---------------------------------- PROPERTIES --------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
# ---------------------------------- PROTECTED METHODS -------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
# ---------------------------------- METHODS -----------------------------------------------------------------------
|
_balder/setup.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from _balder.utils.inner_device_managing_metaclass import InnerDeviceManagingMetaclass
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Setup(metaclass=InnerDeviceManagingMetaclass):
|
|
6
|
+
"""
|
|
7
|
+
This is the abstract basic setup class. It has to be the base class for all setups.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
# ---------------------------------- STATIC METHODS ----------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
# ---------------------------------- CLASS METHODS ----------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
# ---------------------------------- PROPERTIES --------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
# ---------------------------------- PROTECTED METHODS -------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
# ---------------------------------- METHODS -----------------------------------------------------------------------
|
_balder/solver.py
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import List, Dict, Tuple, Type, Union, Callable, TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
import itertools
|
|
5
|
+
from _balder.fixture_manager import FixtureManager
|
|
6
|
+
from _balder.executor.executor_tree import ExecutorTree
|
|
7
|
+
from _balder.executor.setup_executor import SetupExecutor
|
|
8
|
+
from _balder.executor.scenario_executor import ScenarioExecutor
|
|
9
|
+
from _balder.executor.testcase_executor import TestcaseExecutor
|
|
10
|
+
from _balder.executor.variation_executor import VariationExecutor
|
|
11
|
+
from _balder.executor.parametrized_testcase_executor import ParametrizedTestcaseExecutor
|
|
12
|
+
from _balder.executor.unresolved_parametrized_testcase_executor import UnresolvedParametrizedTestcaseExecutor
|
|
13
|
+
from _balder.previous_executor_mark import PreviousExecutorMark
|
|
14
|
+
from _balder.controllers import ScenarioController, SetupController
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from _balder.setup import Setup
|
|
18
|
+
from _balder.device import Device
|
|
19
|
+
from _balder.scenario import Scenario
|
|
20
|
+
from _balder.connection import Connection
|
|
21
|
+
from _balder.plugin_manager import PluginManager
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Solver:
|
|
25
|
+
"""
|
|
26
|
+
The solver class is the class to map corresponding :class:`Scenario` with :class:`Setup` classes. This class
|
|
27
|
+
determines all possibilities in which a scenario can be mapped to a setup constellation.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, setups: List[Type[Setup]], scenarios: List[Type[Scenario]], connections: List[Type[Connection]],
|
|
31
|
+
fixture_manager: Union[FixtureManager, None]):
|
|
32
|
+
#: contains all available setup classes
|
|
33
|
+
self._all_existing_setups = setups
|
|
34
|
+
#: contains all available scenario classes
|
|
35
|
+
self._all_existing_scenarios = scenarios
|
|
36
|
+
#: contains all available connection classes
|
|
37
|
+
self._all_existing_connections = connections
|
|
38
|
+
#: contains all mappings between :meth:`Scenario`'s and :meth:`Setup`'s
|
|
39
|
+
#: will contain all possibilities and will then be reduced to the really applicable set using the `filter_*()`
|
|
40
|
+
#: methods
|
|
41
|
+
self._mapping: List[Tuple[Type[Setup], Type[Scenario], Dict[Type[Device], Type[Device]]]] = []
|
|
42
|
+
self._resolving_was_executed = False
|
|
43
|
+
|
|
44
|
+
self._fixture_manager = fixture_manager
|
|
45
|
+
|
|
46
|
+
# ---------------------------------- STATIC METHODS ----------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
# ---------------------------------- CLASS METHODS -----------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
def _set_data_for_covered_by_in_tree(self, executor_tree: ExecutorTree):
|
|
51
|
+
"""
|
|
52
|
+
This method updates the executor tree and sets all metadata for the `@covered_by()` decorator.
|
|
53
|
+
|
|
54
|
+
:param executor_tree: the executor tree object, that should be updated
|
|
55
|
+
"""
|
|
56
|
+
# now determine all covered_by items
|
|
57
|
+
all_scenarios = executor_tree.get_all_scenario_executors()
|
|
58
|
+
all_testcases = executor_tree.get_all_testcase_executors()
|
|
59
|
+
all_available_testcase_functions = [
|
|
60
|
+
cur_testcase_executor.base_testcase_callable for cur_testcase_executor in all_testcases]
|
|
61
|
+
all_available_scenario_classes = [
|
|
62
|
+
cur_scenario_executor.base_scenario_class for cur_scenario_executor in all_scenarios]
|
|
63
|
+
|
|
64
|
+
covered_by_mapping_of_interest = {}
|
|
65
|
+
for cur_scenario in all_scenarios:
|
|
66
|
+
covered_by_item = cur_scenario.get_covered_by_element()
|
|
67
|
+
if covered_by_item is not None:
|
|
68
|
+
covered_by_mapping_of_interest[cur_scenario] = covered_by_item
|
|
69
|
+
|
|
70
|
+
for cur_testcase in all_testcases:
|
|
71
|
+
covered_by_item = cur_testcase.get_covered_by_element()
|
|
72
|
+
if covered_by_item is not None:
|
|
73
|
+
covered_by_mapping_of_interest[cur_testcase] = covered_by_item
|
|
74
|
+
|
|
75
|
+
# now go throw all covered_by items and check if the destination is contained in the tree -> set prev_mark
|
|
76
|
+
for cur_elem, covered_from_items in covered_by_mapping_of_interest.items():
|
|
77
|
+
all_matched_covered_from_executors = []
|
|
78
|
+
for cur_covered_from_item in covered_from_items:
|
|
79
|
+
if cur_covered_from_item in all_available_scenario_classes:
|
|
80
|
+
all_matched_covered_from_executors.append(
|
|
81
|
+
all_scenarios[all_available_scenario_classes.index(cur_covered_from_item)])
|
|
82
|
+
|
|
83
|
+
elif cur_covered_from_item in all_available_testcase_functions:
|
|
84
|
+
all_matched_covered_from_executors.append(
|
|
85
|
+
all_testcases[all_available_testcase_functions.index(cur_covered_from_item)])
|
|
86
|
+
if len(all_matched_covered_from_executors) > 0:
|
|
87
|
+
cur_elem.prev_mark = PreviousExecutorMark.COVERED_BY
|
|
88
|
+
cur_elem.covered_by_executors = all_matched_covered_from_executors
|
|
89
|
+
|
|
90
|
+
# ---------------------------------- PROPERTIES --------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def all_mappings(self) -> List[Tuple[Type[Setup], Type[Scenario], Dict[Type[Device], Type[Device]]]]:
|
|
94
|
+
"""
|
|
95
|
+
returns all still active mappings between :meth:`Scenario`'s and :meth:`Setup`'s.
|
|
96
|
+
"""
|
|
97
|
+
if not self._mapping or self._resolving_was_executed is False:
|
|
98
|
+
raise AttributeError("please call the `resolve()` method before omitting this value")
|
|
99
|
+
return self._mapping
|
|
100
|
+
|
|
101
|
+
# ---------------------------------- PROTECTED METHODS -------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
def _get_all_unfiltered_mappings(self) -> List[Tuple[Type[Setup], Type[Scenario]]]:
|
|
104
|
+
"""
|
|
105
|
+
This method searches for all possible hits for the internal lists `_all_existing_setups` and
|
|
106
|
+
`_all_existing_scenarios`. It returns all combinations of :meth:`Setup`'s and :meth:`Scenario`'s that could
|
|
107
|
+
exist.
|
|
108
|
+
|
|
109
|
+
:return: a list with tuple pairs that defines all possible pairs of :meth:`Setup` classes and :meth:`Scenario`
|
|
110
|
+
classes
|
|
111
|
+
"""
|
|
112
|
+
matching_list = []
|
|
113
|
+
for cur_setup in self._all_existing_setups:
|
|
114
|
+
for cur_scenario in self._all_existing_scenarios:
|
|
115
|
+
matching_list.append((cur_setup, cur_scenario))
|
|
116
|
+
return matching_list
|
|
117
|
+
|
|
118
|
+
# ---------------------------------- METHODS -----------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
def get_initial_mapping(self) -> List[Tuple[Type[Setup], Type[Scenario], Dict[Type[Device], Type[Device]]]]:
|
|
121
|
+
"""
|
|
122
|
+
This method creates the initial amount of data for `self._mapping`. Only those elements are returned where the
|
|
123
|
+
:meth:`Setup` class has more or the same amount of :meth:`Device`'s than the :meth:`Scenario` class.
|
|
124
|
+
The method does not reduce the set with other criteria yet. This will be done later with filter methods.
|
|
125
|
+
"""
|
|
126
|
+
setup_scenario_matches = self._get_all_unfiltered_mappings()
|
|
127
|
+
|
|
128
|
+
mapping = []
|
|
129
|
+
|
|
130
|
+
for cur_setup, cur_scenario in setup_scenario_matches:
|
|
131
|
+
|
|
132
|
+
setup_devices = SetupController.get_for(cur_setup).get_all_abs_inner_device_classes()
|
|
133
|
+
scenario_devices = ScenarioController.get_for(cur_scenario).get_all_abs_inner_device_classes()
|
|
134
|
+
if len(scenario_devices) <= len(setup_devices):
|
|
135
|
+
# only if there are more or as many devices in the setup as in the scenario
|
|
136
|
+
|
|
137
|
+
# go through every possible constellation
|
|
138
|
+
for cur_setup_devices in itertools.permutations(setup_devices, len(scenario_devices)):
|
|
139
|
+
# get device mapping for this constellation
|
|
140
|
+
device_mapping = {scenario_devices[idx]: cur_setup_devices[idx] for idx in range(
|
|
141
|
+
0, len(scenario_devices))}
|
|
142
|
+
mapping.append((cur_setup, cur_scenario, device_mapping))
|
|
143
|
+
return mapping
|
|
144
|
+
|
|
145
|
+
def resolve(self, plugin_manager: PluginManager) -> None: # pylint: disable=unused-argument
|
|
146
|
+
"""
|
|
147
|
+
This method carries out the entire resolve process and saves the end result in the object property
|
|
148
|
+
`self._mapping`.
|
|
149
|
+
"""
|
|
150
|
+
# reset mapping list
|
|
151
|
+
self._mapping = []
|
|
152
|
+
initial_mapping = self.get_initial_mapping()
|
|
153
|
+
self._mapping = initial_mapping
|
|
154
|
+
self._resolving_was_executed = True
|
|
155
|
+
|
|
156
|
+
def get_static_parametrized_testcase_executor_for(
|
|
157
|
+
self,
|
|
158
|
+
variation_executor: VariationExecutor,
|
|
159
|
+
testcase: Callable
|
|
160
|
+
) -> List[UnresolvedParametrizedTestcaseExecutor | ParametrizedTestcaseExecutor]:
|
|
161
|
+
"""
|
|
162
|
+
returns a list of all testcase executors, with already resolved static parametrization -
|
|
163
|
+
:class:`UnresolvedParametrizedTest` will be returned for unresolved dynamic parametrization
|
|
164
|
+
|
|
165
|
+
:param variation_executor: the current variation executor
|
|
166
|
+
:param testcase: the current testcase
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
scenario_controller = variation_executor.parent_executor.base_scenario_controller
|
|
170
|
+
if scenario_controller.get_parametrization_for(testcase) is None:
|
|
171
|
+
raise ValueError(f'can not determine parametrization for test `{testcase.__qualname__}` because no '
|
|
172
|
+
f'parametrization exist')
|
|
173
|
+
static_parametrization = scenario_controller.get_parametrization_for(testcase, static=True, dynamic=False)
|
|
174
|
+
executor_typ = UnresolvedParametrizedTestcaseExecutor \
|
|
175
|
+
if scenario_controller.get_parametrization_for(testcase, static=False, dynamic=True) \
|
|
176
|
+
else ParametrizedTestcaseExecutor
|
|
177
|
+
|
|
178
|
+
if not static_parametrization:
|
|
179
|
+
return [executor_typ(testcase, parent=variation_executor)]
|
|
180
|
+
|
|
181
|
+
# generate product
|
|
182
|
+
result = []
|
|
183
|
+
for cur_product in itertools.product(*static_parametrization.values()):
|
|
184
|
+
cur_product_parametrization = dict(zip(static_parametrization.keys(), cur_product))
|
|
185
|
+
result.append(executor_typ(testcase, variation_executor, cur_product_parametrization))
|
|
186
|
+
return result
|
|
187
|
+
|
|
188
|
+
# pylint: disable-next=unused-argument
|
|
189
|
+
def get_executor_tree(self, plugin_manager: PluginManager, add_discarded=False) -> ExecutorTree:
|
|
190
|
+
"""
|
|
191
|
+
This method builds the ExecutorTree from the resolved data and returns it
|
|
192
|
+
|
|
193
|
+
:param plugin_manager: the related plugin manager object
|
|
194
|
+
:param add_discarded: True in case discarded elements should be added to the tree, otherwise False
|
|
195
|
+
|
|
196
|
+
:return: the executor tree is built on the basis of the mapping data
|
|
197
|
+
"""
|
|
198
|
+
|
|
199
|
+
executor_tree = ExecutorTree(self._fixture_manager)
|
|
200
|
+
|
|
201
|
+
# create all setup executor
|
|
202
|
+
|
|
203
|
+
for cur_setup, cur_scenario, cur_device_mapping in self._mapping:
|
|
204
|
+
setup_executor = executor_tree.get_executor_for_setup(setup=cur_setup)
|
|
205
|
+
if setup_executor is None:
|
|
206
|
+
# setup is not available -> create new SetupExecutor
|
|
207
|
+
setup_executor = SetupExecutor(cur_setup, parent=executor_tree)
|
|
208
|
+
executor_tree.add_setup_executor(setup_executor)
|
|
209
|
+
|
|
210
|
+
scenario_executor = setup_executor.get_executor_for_scenario(scenario=cur_scenario)
|
|
211
|
+
if scenario_executor is None:
|
|
212
|
+
# scenario is not available -> create new ScenarioExecutor
|
|
213
|
+
scenario_executor = ScenarioExecutor(cur_scenario, parent=setup_executor)
|
|
214
|
+
setup_executor.add_scenario_executor(scenario_executor)
|
|
215
|
+
|
|
216
|
+
variation_executor = scenario_executor.get_executor_for_device_mapping(device_mapping=cur_device_mapping)
|
|
217
|
+
if variation_executor is None:
|
|
218
|
+
# variation is not available -> create new VariationExecutor
|
|
219
|
+
variation_executor = VariationExecutor(device_mapping=cur_device_mapping, parent=scenario_executor)
|
|
220
|
+
variation_executor.verify_applicability()
|
|
221
|
+
|
|
222
|
+
if not variation_executor.can_be_applied():
|
|
223
|
+
continue
|
|
224
|
+
|
|
225
|
+
scenario_executor.add_variation_executor(variation_executor)
|
|
226
|
+
|
|
227
|
+
for cur_testcase in scenario_executor.base_scenario_controller.get_all_test_methods():
|
|
228
|
+
# we have a parametrization for this test case
|
|
229
|
+
if scenario_executor.base_scenario_controller.get_parametrization_for(cur_testcase):
|
|
230
|
+
testcase_executors = self.get_static_parametrized_testcase_executor_for(
|
|
231
|
+
variation_executor, cur_testcase
|
|
232
|
+
)
|
|
233
|
+
for cur_testcase_executor in testcase_executors:
|
|
234
|
+
variation_executor.add_testcase_executor(cur_testcase_executor)
|
|
235
|
+
else:
|
|
236
|
+
testcase_executor = TestcaseExecutor(cur_testcase, parent=variation_executor)
|
|
237
|
+
variation_executor.add_testcase_executor(testcase_executor)
|
|
238
|
+
|
|
239
|
+
# now filter all elements that have no child elements
|
|
240
|
+
# -> these are items that have no valid matching, because no variation can be applied for it (there are no
|
|
241
|
+
# required :class:`Feature` matching or there exists no possible routing for the variation)
|
|
242
|
+
executor_tree.cleanup_empty_executor_branches(consider_discarded=add_discarded)
|
|
243
|
+
|
|
244
|
+
self._set_data_for_covered_by_in_tree(executor_tree=executor_tree)
|
|
245
|
+
|
|
246
|
+
return executor_tree
|