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,483 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import itertools
|
|
4
|
+
from typing import List, Tuple, Generator, Dict, Union, Type, Callable, Iterable, TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
import inspect
|
|
7
|
+
from graphlib import TopologicalSorter
|
|
8
|
+
from _balder.executor.testcase_executor import TestcaseExecutor
|
|
9
|
+
from _balder.scenario import Scenario
|
|
10
|
+
from _balder.setup import Setup
|
|
11
|
+
from _balder.fixture_definition_scope import FixtureDefinitionScope
|
|
12
|
+
from _balder.fixture_execution_level import FixtureExecutionLevel
|
|
13
|
+
from _balder.fixture_metadata import FixtureMetadata
|
|
14
|
+
from _balder.executor.basic_executor import BasicExecutor
|
|
15
|
+
from _balder.executor.setup_executor import SetupExecutor
|
|
16
|
+
from _balder.executor.scenario_executor import ScenarioExecutor
|
|
17
|
+
from _balder.executor.variation_executor import VariationExecutor
|
|
18
|
+
from _balder.exceptions import LostInExecutorTreeException, FixtureReferenceError, UnclearSetupScopedFixtureReference, \
|
|
19
|
+
UnclearUniqueClassReference
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from _balder.utils.functions import MethodLiteralType
|
|
23
|
+
from _balder.executor.executor_tree import ExecutorTree
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class FixtureManager:
|
|
27
|
+
"""
|
|
28
|
+
This class is the global fixture manager. It provides various methods to manage fixtures in the balder system.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
fixtures: Dict[FixtureExecutionLevel,
|
|
34
|
+
Dict[Union[None, Type[Scenario], Type[Setup]], List[Tuple[MethodLiteralType, Callable]]]]):
|
|
35
|
+
|
|
36
|
+
# The first key is the fixture level, the second key is the namespace in which the fixture is defined. As value
|
|
37
|
+
# a list with tuples is returned. The first element is the type of the method/function and the second is the
|
|
38
|
+
# callable itself.
|
|
39
|
+
self.fixtures: Dict[FixtureExecutionLevel,
|
|
40
|
+
Dict[Union[None, Type[Scenario], Type[Setup]], List[Tuple[MethodLiteralType, Callable]]]] \
|
|
41
|
+
= fixtures
|
|
42
|
+
|
|
43
|
+
# contains all active fixtures with their namespace, their func_type, their callable, the generator object and
|
|
44
|
+
# the result according to the fixture's construction code (will be cleaned after it leaves a level)
|
|
45
|
+
self.current_tree_fixtures: Dict[FixtureExecutionLevel, List[FixtureMetadata]] = {}
|
|
46
|
+
|
|
47
|
+
# ---------------------------------- STATIC METHODS ----------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
# ---------------------------------- CLASS METHODS ----------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
# ---------------------------------- PROPERTIES --------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def all_already_run_fixtures(self) -> List[Callable]:
|
|
55
|
+
"""
|
|
56
|
+
returns a list of all fixtures that have already been run
|
|
57
|
+
"""
|
|
58
|
+
complete_list_in_order = []
|
|
59
|
+
for cur_level in FixtureExecutionLevel:
|
|
60
|
+
if cur_level in self.current_tree_fixtures.keys():
|
|
61
|
+
complete_list_in_order += [
|
|
62
|
+
cur_fixture_metadata.callable for cur_fixture_metadata in self.current_tree_fixtures[cur_level]]
|
|
63
|
+
return complete_list_in_order
|
|
64
|
+
|
|
65
|
+
# ---------------------------------- PROTECTED METHODS -------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
def _validate_for_unclear_setup_scoped_fixture_reference(
|
|
68
|
+
self, fixture_callable_namespace: Union[None, Type[Scenario], Type[Setup]],
|
|
69
|
+
fixture_callable: Callable, arguments: List[str], cur_execution_level: FixtureExecutionLevel):
|
|
70
|
+
"""
|
|
71
|
+
This helper method validates a given fixture reference for the unclear setup scoped fixture reference.
|
|
72
|
+
|
|
73
|
+
The problem:
|
|
74
|
+
A scenario scoped fixture (defined in a scenario) with the execution level SESSION references a fixture (over
|
|
75
|
+
an argument) with a name that is also used for one or more setup scoped fixtures with execution level SESSION.
|
|
76
|
+
|
|
77
|
+
For this constellation it is unclear which referenced setup scoped fixture is the correct one.
|
|
78
|
+
|
|
79
|
+
.. note::
|
|
80
|
+
Note that the error will also be thrown if only one setup fixture with the referenced name exists. (This
|
|
81
|
+
would work, but it becomes difficult to understand it. Balder prohibits it.
|
|
82
|
+
|
|
83
|
+
.. note::
|
|
84
|
+
The error will be thrown if a scenario scoped fixture with the execution level SESSION references a fixture
|
|
85
|
+
name that exists in one or more setup scoped fixtures with the execution level SESSION.
|
|
86
|
+
|
|
87
|
+
:param fixture_callable_namespace: the namespace of the current fixture or None if it is a `balderglob.py` file
|
|
88
|
+
|
|
89
|
+
:param fixture_callable: the callable with the arguments
|
|
90
|
+
|
|
91
|
+
:param arguments: the execution level of the fixture
|
|
92
|
+
|
|
93
|
+
:param cur_execution_level: the current execution level that is currently active
|
|
94
|
+
"""
|
|
95
|
+
# this method is only interested in fixtures with execution level SESSION!
|
|
96
|
+
|
|
97
|
+
if (fixture_callable_namespace is not None and isinstance(fixture_callable_namespace, Scenario)
|
|
98
|
+
and cur_execution_level == FixtureExecutionLevel.SESSION):
|
|
99
|
+
all_setup_scoped_fixtures = []
|
|
100
|
+
|
|
101
|
+
for cur_namespace_type, fixtures in self.fixtures.get(FixtureExecutionLevel.SESSION, {}).items():
|
|
102
|
+
if cur_namespace_type is not None and issubclass(cur_namespace_type, Setup):
|
|
103
|
+
all_setup_scoped_fixtures += [cur_fixt.__name__ for _, cur_fixt in fixtures]
|
|
104
|
+
for cur_arg in arguments:
|
|
105
|
+
if cur_arg in all_setup_scoped_fixtures:
|
|
106
|
+
raise UnclearSetupScopedFixtureReference(
|
|
107
|
+
f"your fixture `{fixture_callable.__name__}` has the definition scope SCENARIO and tries to "
|
|
108
|
+
f"access a fixture name `{cur_arg}` - the environment has one or more SESSION "
|
|
109
|
+
f"fixtures with the definition scope that have the same name - please rename them!")
|
|
110
|
+
|
|
111
|
+
def _sort_fixture_list_of_same_definition_scope(
|
|
112
|
+
self, fixture_namespace_dict: Dict[Union[None, Type[Scenario], Type[Setup]], List[object]],
|
|
113
|
+
outer_scope_fixtures: List[object]) \
|
|
114
|
+
-> List[Tuple[Union[None, Type[Scenario], Type[Setup]], str, Callable]]:
|
|
115
|
+
"""
|
|
116
|
+
This is a helper method that allows to sort the fixtures given in `fixture_namespace_dict`, depending on their
|
|
117
|
+
arguments.
|
|
118
|
+
It also checks that every fixture that is mentioned in the arguments is available and an item of
|
|
119
|
+
``fixture_namespace_dict`` or ``outer_scope_fixtures``. If the method detects a loop between some fixtures, the
|
|
120
|
+
method throws an exception.
|
|
121
|
+
|
|
122
|
+
:param fixture_namespace_dict: the unordered dictionary, where the definition scope namespace object is the key
|
|
123
|
+
and a list with the fixture tuple is the value
|
|
124
|
+
|
|
125
|
+
:param outer_scope_fixtures: the list of fixtures that have already been ordered as elements before the given
|
|
126
|
+
one in ``fixture_list`` (for example the item from another **definition-scope** or
|
|
127
|
+
from another **execution-level**)
|
|
128
|
+
|
|
129
|
+
:return: the list of tuples where the first element is the definition scope object, the second element is the
|
|
130
|
+
func_type of the fixture and the third is the fixture callable itself
|
|
131
|
+
"""
|
|
132
|
+
if len(fixture_namespace_dict) == 0:
|
|
133
|
+
return []
|
|
134
|
+
|
|
135
|
+
# returns the func_type for the given fixture
|
|
136
|
+
fixture_func_types = {}
|
|
137
|
+
for _, cur_fixture_list in fixture_namespace_dict.items():
|
|
138
|
+
for cur_func_type, cur_fixture in cur_fixture_list:
|
|
139
|
+
fixture_func_types[cur_fixture] = cur_func_type
|
|
140
|
+
|
|
141
|
+
# - if we want to access a fixture (over name) from another fixture we have to be careful!
|
|
142
|
+
# -> if we try to access a fixture with a fixture which is not part of the same namespace scope -> ERROR
|
|
143
|
+
# -> if we try to access a fixture with a fixture which is part of the same namespace scope -> this fixture
|
|
144
|
+
# has to run before the other (and return the value to the referencing one)
|
|
145
|
+
|
|
146
|
+
# now resolve dependencies, by returning them as single items and add them to the TopologicalSorter
|
|
147
|
+
sorter = TopologicalSorter()
|
|
148
|
+
|
|
149
|
+
for cur_namespace_type, cur_fixture_list in fixture_namespace_dict.items():
|
|
150
|
+
for _, cur_fixture in cur_fixture_list:
|
|
151
|
+
# add the item to the sorter, because it should always be executed (also if it is not referenced from
|
|
152
|
+
# another one)
|
|
153
|
+
sorter.add((cur_namespace_type, cur_fixture))
|
|
154
|
+
# determine all function/method arguments that has to be resolved for this fixture
|
|
155
|
+
cur_fixture_args = inspect.getfullargspec(cur_fixture).args
|
|
156
|
+
if fixture_func_types[cur_fixture] in ["instancemethod", "classmethod"]:
|
|
157
|
+
# this is a class method (remove `cls`) or an instance method (remove `self`)
|
|
158
|
+
cur_fixture_args = cur_fixture_args[1:]
|
|
159
|
+
# now try to find a possible fixture for every argument
|
|
160
|
+
for cur_arg in cur_fixture_args:
|
|
161
|
+
# search the fixture in current namespace
|
|
162
|
+
cur_fixture_list_as_names = [search_fixture.__name__ for _, search_fixture in cur_fixture_list]
|
|
163
|
+
found = False
|
|
164
|
+
# found referenced fixture in current namespace and definition scope
|
|
165
|
+
if cur_arg in cur_fixture_list_as_names:
|
|
166
|
+
_, cur_arg_fixture = cur_fixture_list[cur_fixture_list_as_names.index(cur_arg)]
|
|
167
|
+
# only add fixture if the referenced one is not the same as the current fixture
|
|
168
|
+
if cur_fixture != cur_arg_fixture:
|
|
169
|
+
sorter.add((cur_namespace_type, cur_fixture), (cur_namespace_type, cur_arg_fixture))
|
|
170
|
+
found = True
|
|
171
|
+
# did not find referenced fixture in current namespace and definition scope -> search for it in
|
|
172
|
+
# outer scopes
|
|
173
|
+
if not found:
|
|
174
|
+
# search it in outer fixture list to secure that they exist
|
|
175
|
+
if cur_arg not in \
|
|
176
|
+
[cur_outer_fixture.__name__ for cur_outer_fixture in outer_scope_fixtures]:
|
|
177
|
+
raise FixtureReferenceError(
|
|
178
|
+
f"can not find a previous fixture with the name `{cur_arg}`")
|
|
179
|
+
|
|
180
|
+
return [(cur_definition_scope, fixture_func_types[cur_func], cur_func)
|
|
181
|
+
for cur_definition_scope, cur_func in sorter.static_order()]
|
|
182
|
+
|
|
183
|
+
def _determine_setup_and_scenario_type(
|
|
184
|
+
self,
|
|
185
|
+
from_branch: Union[ExecutorTree, SetupExecutor, ScenarioExecutor, VariationExecutor, TestcaseExecutor],
|
|
186
|
+
callable_func_namespace: Union[None, Type[Scenario], Type[Setup]]) \
|
|
187
|
+
-> Tuple[Union[None, Type[Setup]], Union[None, Type[Scenario]]]:
|
|
188
|
+
"""
|
|
189
|
+
determines the setup and scenario type for a specific fixture based on the branch the system is in
|
|
190
|
+
|
|
191
|
+
:param from_branch: the branch for which the types should be determined
|
|
192
|
+
:param callable_func_namespace: the namespace of the current fixture or `None` if it is defined in balderglob
|
|
193
|
+
file
|
|
194
|
+
"""
|
|
195
|
+
# determine namespaces (in the correct order)
|
|
196
|
+
setup_type = None
|
|
197
|
+
scenario_type = None
|
|
198
|
+
if isinstance(from_branch, SetupExecutor):
|
|
199
|
+
setup_type = from_branch.base_setup_class.__class__
|
|
200
|
+
# normally the scenario is None - only if the current namespace is a scenario we can use it
|
|
201
|
+
if callable_func_namespace is not None and issubclass(callable_func_namespace, Scenario):
|
|
202
|
+
scenario_type = callable_func_namespace
|
|
203
|
+
else:
|
|
204
|
+
scenario_type = None
|
|
205
|
+
elif isinstance(from_branch, ScenarioExecutor):
|
|
206
|
+
setup_type = from_branch.parent_executor.base_setup_class.__class__
|
|
207
|
+
scenario_type = from_branch.base_scenario_class.__class__
|
|
208
|
+
elif isinstance(from_branch, VariationExecutor):
|
|
209
|
+
setup_type = from_branch.cur_setup_class.__class__
|
|
210
|
+
scenario_type = from_branch.cur_scenario_class.__class__
|
|
211
|
+
elif isinstance(from_branch, TestcaseExecutor):
|
|
212
|
+
setup_type = from_branch.parent_executor.cur_setup_class.__class__
|
|
213
|
+
scenario_type = from_branch.parent_executor.cur_scenario_class.__class__
|
|
214
|
+
return setup_type, scenario_type
|
|
215
|
+
|
|
216
|
+
# ---------------------------------- METHODS -----------------------------------------------------------------------
|
|
217
|
+
|
|
218
|
+
def is_allowed_to_enter(
|
|
219
|
+
self, branch: Union[BasicExecutor, ExecutorTree, SetupExecutor, ScenarioExecutor, VariationExecutor,
|
|
220
|
+
TestcaseExecutor]) -> bool:
|
|
221
|
+
"""
|
|
222
|
+
This method return true if the given branch can be entered, otherwise false
|
|
223
|
+
"""
|
|
224
|
+
return branch.fixture_execution_level not in self.current_tree_fixtures.keys()
|
|
225
|
+
|
|
226
|
+
def is_allowed_to_leave(
|
|
227
|
+
self, branch: Union[BasicExecutor, ExecutorTree, SetupExecutor, ScenarioExecutor, VariationExecutor,
|
|
228
|
+
TestcaseExecutor]) \
|
|
229
|
+
-> bool:
|
|
230
|
+
"""
|
|
231
|
+
This method returns true if the given branch can be left now (there exist entries from earlier run enter()
|
|
232
|
+
for this branch), otherwise false
|
|
233
|
+
"""
|
|
234
|
+
return branch.fixture_execution_level in self.current_tree_fixtures.keys()
|
|
235
|
+
|
|
236
|
+
def enter(self, branch: Union[BasicExecutor, ExecutorTree, SetupExecutor, ScenarioExecutor, VariationExecutor,
|
|
237
|
+
TestcaseExecutor]):
|
|
238
|
+
"""
|
|
239
|
+
With this method you enter a branch for the fixture manager in order to execute the fixtures contained in it
|
|
240
|
+
|
|
241
|
+
:param branch: specifies the element of the ExecutorTree that should be entered (note that the current position
|
|
242
|
+
is very important here)
|
|
243
|
+
|
|
244
|
+
:raise BalderFixtureException: is thrown if an error occurs while executing a user fixture
|
|
245
|
+
"""
|
|
246
|
+
|
|
247
|
+
if not self.is_allowed_to_enter(branch):
|
|
248
|
+
raise LostInExecutorTreeException(
|
|
249
|
+
"the current branch that should be entered is not allowed, because other branches weren't left yet")
|
|
250
|
+
|
|
251
|
+
def empty():
|
|
252
|
+
yield None
|
|
253
|
+
# now iterate over all fixtures that should be executed in this enter() call
|
|
254
|
+
# -> collect them with all different DEFINITION-SCOPES
|
|
255
|
+
for cur_definition_scope in FixtureDefinitionScope:
|
|
256
|
+
cur_fixture_list = self.get_all_fixtures_for_current_level(branch=branch).get(cur_definition_scope)
|
|
257
|
+
for cur_scope_namespace_type, cur_fixture_func_type, cur_fixture in cur_fixture_list:
|
|
258
|
+
try:
|
|
259
|
+
if cur_fixture_func_type in ["function", "staticmethod"]:
|
|
260
|
+
# fixture is a function or a staticmethod - no first special attribute
|
|
261
|
+
kwargs = self.get_all_attribute_values(branch, cur_scope_namespace_type, cur_fixture,
|
|
262
|
+
cur_fixture_func_type)
|
|
263
|
+
cur_generator = cur_fixture(**kwargs)
|
|
264
|
+
elif cur_fixture_func_type == "classmethod":
|
|
265
|
+
kwargs = self.get_all_attribute_values(branch, cur_scope_namespace_type, cur_fixture,
|
|
266
|
+
cur_fixture_func_type)
|
|
267
|
+
cur_generator = cur_fixture(cur_scope_namespace_type, **kwargs)
|
|
268
|
+
elif cur_fixture_func_type == "instancemethod":
|
|
269
|
+
self_reference = branch.get_all_base_instances_of_this_branch(
|
|
270
|
+
with_type=cur_scope_namespace_type, only_runnable_elements=True)
|
|
271
|
+
if len(self_reference) != 1:
|
|
272
|
+
raise UnclearUniqueClassReference(
|
|
273
|
+
f"can not find exactly one reference of the class "
|
|
274
|
+
f"`{cur_scope_namespace_type.__name__}` in current tree branch")
|
|
275
|
+
kwargs = self.get_all_attribute_values(branch, cur_scope_namespace_type, cur_fixture,
|
|
276
|
+
cur_fixture_func_type)
|
|
277
|
+
cur_generator = cur_fixture(self_reference[0], **kwargs)
|
|
278
|
+
else:
|
|
279
|
+
raise ValueError(f"found illegal value for func_type `{cur_fixture_func_type}` for fixture "
|
|
280
|
+
f"`{cur_fixture.__name__}`")
|
|
281
|
+
if isinstance(cur_generator, Generator):
|
|
282
|
+
cur_retvalue = next(cur_generator)
|
|
283
|
+
else:
|
|
284
|
+
cur_retvalue = cur_generator
|
|
285
|
+
cur_generator = empty()
|
|
286
|
+
next(cur_generator)
|
|
287
|
+
# add the executed fixtures to global reference
|
|
288
|
+
if branch.fixture_execution_level not in self.current_tree_fixtures.keys():
|
|
289
|
+
self.current_tree_fixtures[branch.fixture_execution_level] = []
|
|
290
|
+
self.current_tree_fixtures[branch.fixture_execution_level].append(
|
|
291
|
+
FixtureMetadata(namespace=cur_scope_namespace_type, function_type=cur_fixture_func_type,
|
|
292
|
+
callable=cur_fixture, generator=cur_generator, retval=cur_retvalue))
|
|
293
|
+
except StopIteration:
|
|
294
|
+
pass
|
|
295
|
+
# every other exception that is thrown, will be recognized and rethrown
|
|
296
|
+
|
|
297
|
+
def leave(self, branch: Union[BasicExecutor, ExecutorTree, SetupExecutor, ScenarioExecutor, VariationExecutor,
|
|
298
|
+
TestcaseExecutor]):
|
|
299
|
+
"""
|
|
300
|
+
With this method you leave a previously entered branch and execute the teardown code of the fixtures. Note that
|
|
301
|
+
you can only leave the branch that you entered before!
|
|
302
|
+
|
|
303
|
+
:param branch: specifies the element of the ExecutorTree that should be left (note that the current position
|
|
304
|
+
is very important here)
|
|
305
|
+
"""
|
|
306
|
+
if branch.fixture_execution_level not in self.current_tree_fixtures.keys():
|
|
307
|
+
raise LostInExecutorTreeException("can not leave the current branch, because it was not entered before")
|
|
308
|
+
|
|
309
|
+
current_tree_fixtures_reversed = self.current_tree_fixtures[branch.fixture_execution_level]
|
|
310
|
+
current_tree_fixtures_reversed.reverse()
|
|
311
|
+
exception = None
|
|
312
|
+
for cur_fixture_metadata in current_tree_fixtures_reversed:
|
|
313
|
+
try:
|
|
314
|
+
next(cur_fixture_metadata.generator)
|
|
315
|
+
except StopIteration:
|
|
316
|
+
pass
|
|
317
|
+
except Exception as exc: # pylint: disable=broad-exception-caught
|
|
318
|
+
if not exception:
|
|
319
|
+
# only save the first exception
|
|
320
|
+
exception = exc
|
|
321
|
+
|
|
322
|
+
# reset the left location
|
|
323
|
+
del self.current_tree_fixtures[branch.fixture_execution_level]
|
|
324
|
+
|
|
325
|
+
if exception:
|
|
326
|
+
raise exception
|
|
327
|
+
|
|
328
|
+
def get_all_attribute_values(
|
|
329
|
+
self, branch: Union[ExecutorTree, SetupExecutor, ScenarioExecutor, VariationExecutor, TestcaseExecutor],
|
|
330
|
+
callable_func_namespace: Union[None, Type[Scenario], Type[Setup]], callable_func: Callable,
|
|
331
|
+
func_type: str, ignore_attributes: Iterable[str] = None) -> Dict[str, object]:
|
|
332
|
+
"""
|
|
333
|
+
This method tries to fill the unresolved function/method arguments of the given fixture callable. For this it
|
|
334
|
+
searches the return values of all already executed fixtures and supplies the argument values of the
|
|
335
|
+
given ``fixture`` callable in a dictionary.
|
|
336
|
+
|
|
337
|
+
It automatically manages `self` / `cls` references for func_type `instancemethod` or `classmethod`. It
|
|
338
|
+
only returns the fixture references and ignores `self` / `cls`
|
|
339
|
+
|
|
340
|
+
First the method tries to find the arguments in all fixtures that are in the same namespace. If it does not find
|
|
341
|
+
any references, it will look in the next higher scope. It only uses fixture that has run before!
|
|
342
|
+
|
|
343
|
+
:param branch: the current active branch
|
|
344
|
+
:param callable_func_namespace: the namespace of the current fixture or `None` if it is defined in balderglob
|
|
345
|
+
file
|
|
346
|
+
:param callable_func: the callable with the arguments
|
|
347
|
+
:param func_type: returns the func_type of the fixture - depending on this value the first argument will be
|
|
348
|
+
ignored, because it has to be `cls` for `classmethod` and `self` for `instancemethod`
|
|
349
|
+
:param ignore_attributes: holds a list of attributes in the test method that should be ignored
|
|
350
|
+
:return: the method returns a dictionary with the attribute name as key and the return value as value
|
|
351
|
+
|
|
352
|
+
"""
|
|
353
|
+
arguments = inspect.getfullargspec(callable_func).args
|
|
354
|
+
result_dict = {}
|
|
355
|
+
|
|
356
|
+
if func_type in ["classmethod", "instancemethod"]:
|
|
357
|
+
arguments = arguments[1:]
|
|
358
|
+
if ignore_attributes is None:
|
|
359
|
+
ignore_attributes = []
|
|
360
|
+
|
|
361
|
+
self._validate_for_unclear_setup_scoped_fixture_reference(
|
|
362
|
+
callable_func_namespace, callable_func, arguments, cur_execution_level=branch.fixture_execution_level)
|
|
363
|
+
all_possible_namespaces = [None]
|
|
364
|
+
setup_type, scenario_type = self._determine_setup_and_scenario_type(
|
|
365
|
+
from_branch=branch, callable_func_namespace=callable_func_namespace)
|
|
366
|
+
|
|
367
|
+
# add to possible namespaces only if the namespace of the current fixture allows this
|
|
368
|
+
if callable_func_namespace is not None:
|
|
369
|
+
if (issubclass(callable_func_namespace, Setup) or issubclass(callable_func_namespace, Scenario)) \
|
|
370
|
+
and setup_type is not None:
|
|
371
|
+
all_possible_namespaces.append(setup_type)
|
|
372
|
+
if issubclass(callable_func_namespace, Scenario) and scenario_type is not None:
|
|
373
|
+
all_possible_namespaces.append(scenario_type)
|
|
374
|
+
|
|
375
|
+
for cur_arg in arguments:
|
|
376
|
+
if cur_arg in ignore_attributes:
|
|
377
|
+
continue
|
|
378
|
+
# go to the most specific fixture, because more specific ones overwrite the more global ones
|
|
379
|
+
for cur_possible_namespace, cur_level in itertools.product(all_possible_namespaces, FixtureExecutionLevel):
|
|
380
|
+
if cur_level not in self.current_tree_fixtures.keys():
|
|
381
|
+
continue
|
|
382
|
+
# filter only these fixtures that have the same namespace
|
|
383
|
+
for cur_fixture_metadata in self.current_tree_fixtures[cur_level]:
|
|
384
|
+
if (cur_fixture_metadata.namespace == cur_possible_namespace
|
|
385
|
+
and cur_fixture_metadata.callable.__name__ == cur_arg):
|
|
386
|
+
result_dict[cur_arg] = cur_fixture_metadata.retval
|
|
387
|
+
if cur_arg not in result_dict.keys():
|
|
388
|
+
raise FixtureReferenceError(
|
|
389
|
+
f"the argument `{cur_arg}` in fixture `{callable_func.__qualname__}` could not be resolved")
|
|
390
|
+
return result_dict
|
|
391
|
+
|
|
392
|
+
def get_fixture_for_class(self, execution_level: FixtureExecutionLevel,
|
|
393
|
+
setup_or_scenario_class: Union[None, Type[Setup], Type[Scenario]],
|
|
394
|
+
parent_classes: bool = True) -> List[Tuple[MethodLiteralType, Callable]]:
|
|
395
|
+
"""
|
|
396
|
+
This method returns all classes of a specific Setup/Scenario class for a specific execution-level.
|
|
397
|
+
|
|
398
|
+
:param execution_level: the execution level the fixture should be returned for
|
|
399
|
+
:param setup_or_scenario_class: the scenario or setup class, the fixtures should be returned for
|
|
400
|
+
:param parent_classes: true if the method should look for fixtures in parent classes too
|
|
401
|
+
:return: list with all fixtures that are matching search criteria
|
|
402
|
+
"""
|
|
403
|
+
# get all fixtures of the current relevant level (only `execution_level` is relevant - all other levels are
|
|
404
|
+
# not relevant for this call)
|
|
405
|
+
fixtures_of_exec_level = self.fixtures.get(execution_level, {})
|
|
406
|
+
if setup_or_scenario_class is not None and parent_classes:
|
|
407
|
+
all_fixtures = []
|
|
408
|
+
for cur_parent_class in inspect.getmro(setup_or_scenario_class):
|
|
409
|
+
if issubclass(cur_parent_class, (Scenario, Setup)):
|
|
410
|
+
all_fixtures += self.get_fixture_for_class(execution_level, cur_parent_class, False)
|
|
411
|
+
# go through list and remove all overwritten fixtures
|
|
412
|
+
_added_fixtures = []
|
|
413
|
+
remaining_fixtures = []
|
|
414
|
+
for cur_fixture_tuple in all_fixtures:
|
|
415
|
+
if cur_fixture_tuple[1].__name__ not in _added_fixtures:
|
|
416
|
+
_added_fixtures.append(cur_fixture_tuple[1].__name__)
|
|
417
|
+
remaining_fixtures.append(cur_fixture_tuple)
|
|
418
|
+
return remaining_fixtures
|
|
419
|
+
return fixtures_of_exec_level.get(setup_or_scenario_class, [])
|
|
420
|
+
|
|
421
|
+
def get_all_fixtures_for_current_level(
|
|
422
|
+
self, branch: Union[ExecutorTree, SetupExecutor, ScenarioExecutor, VariationExecutor, TestcaseExecutor]) \
|
|
423
|
+
-> Dict[FixtureDefinitionScope, List[Tuple[Union[None, Type[Scenario], Type[Setup]], str, object]]]:
|
|
424
|
+
"""
|
|
425
|
+
This method delivers all fixtures which should be executed for the given branch of the executor tree.
|
|
426
|
+
|
|
427
|
+
The method collects the following fixtures from all three possible DEFINITION-SCOPES:
|
|
428
|
+
1. collect them from balderglob.py (see ``FixtureManager._glob_fixtures``)
|
|
429
|
+
2. collect them from current used Setup OR if no specific one is set (because we are in a higher branch):
|
|
430
|
+
collect them from all that are part of the current branch
|
|
431
|
+
3. collect them from current used Scenario OR if no specific one is set (because we are in a higher branch):
|
|
432
|
+
collect them from all that are part of the current branch
|
|
433
|
+
|
|
434
|
+
:param branch: the branch which is currently active
|
|
435
|
+
|
|
436
|
+
:return: a dictionary where the definition object (DEFINITION SCOPE) is the key and a list is the
|
|
437
|
+
value - this value list has tuple elements as children, where the namespace class (:class:`Scenario`
|
|
438
|
+
or :class:`Setup`) as first argument, the fixture func_type as second and the fixture callable as third
|
|
439
|
+
argument (this list is ordered after the call hierarchy)
|
|
440
|
+
"""
|
|
441
|
+
all_fixtures = {}
|
|
442
|
+
# get all relevant fixtures of `balderglob.py` (None is key for balderglob fixtures)
|
|
443
|
+
glob_fixtures = self.get_fixture_for_class(branch.fixture_execution_level, None)
|
|
444
|
+
all_fixtures[FixtureDefinitionScope.GLOB] = {}
|
|
445
|
+
all_fixtures[FixtureDefinitionScope.GLOB][None] = glob_fixtures
|
|
446
|
+
# get all relevant fixtures with definition scope "setup"
|
|
447
|
+
all_fixtures[FixtureDefinitionScope.SETUP] = {}
|
|
448
|
+
for cur_setup in branch.get_all_base_instances_of_this_branch(Setup, only_runnable_elements=True):
|
|
449
|
+
# check if there exists fixtures for the current setup
|
|
450
|
+
cur_setup_fixtures = self.get_fixture_for_class(branch.fixture_execution_level, cur_setup.__class__)
|
|
451
|
+
if cur_setup_fixtures:
|
|
452
|
+
all_fixtures[FixtureDefinitionScope.SETUP][cur_setup.__class__] = cur_setup_fixtures
|
|
453
|
+
|
|
454
|
+
# get all relevant fixtures with definition scope "scenario"
|
|
455
|
+
all_fixtures[FixtureDefinitionScope.SCENARIO] = {}
|
|
456
|
+
for cur_scenario in branch.get_all_base_instances_of_this_branch(Scenario, only_runnable_elements=True):
|
|
457
|
+
cur_scenario_fixtures = self.get_fixture_for_class(branch.fixture_execution_level, cur_scenario.__class__)
|
|
458
|
+
if cur_scenario_fixtures:
|
|
459
|
+
all_fixtures[FixtureDefinitionScope.SCENARIO][cur_scenario.__class__] = cur_scenario_fixtures
|
|
460
|
+
|
|
461
|
+
ordered_fixtures = {}
|
|
462
|
+
# Now the basic order is: [All of ExecutorTree] -> [All of Setup] -> [All of Scenario]
|
|
463
|
+
# but the order within these DEFINITION SCOPES has to be determined now!
|
|
464
|
+
outer_scope_fixtures = self.all_already_run_fixtures
|
|
465
|
+
ordered_fixtures[FixtureDefinitionScope.GLOB] = self._sort_fixture_list_of_same_definition_scope(
|
|
466
|
+
fixture_namespace_dict=all_fixtures[FixtureDefinitionScope.GLOB], outer_scope_fixtures=outer_scope_fixtures)
|
|
467
|
+
|
|
468
|
+
outer_scope_fixtures = \
|
|
469
|
+
self.all_already_run_fixtures + \
|
|
470
|
+
[cur_fixture for _, _, cur_fixture in ordered_fixtures[FixtureDefinitionScope.GLOB]]
|
|
471
|
+
ordered_fixtures[FixtureDefinitionScope.SETUP] = self._sort_fixture_list_of_same_definition_scope(
|
|
472
|
+
fixture_namespace_dict=all_fixtures[FixtureDefinitionScope.SETUP],
|
|
473
|
+
outer_scope_fixtures=outer_scope_fixtures)
|
|
474
|
+
|
|
475
|
+
outer_scope_fixtures = \
|
|
476
|
+
self.all_already_run_fixtures + \
|
|
477
|
+
[cur_fixture for _, _, cur_fixture in ordered_fixtures[FixtureDefinitionScope.GLOB]] + \
|
|
478
|
+
[cur_fixture for _, _, cur_fixture in ordered_fixtures[FixtureDefinitionScope.SETUP]]
|
|
479
|
+
ordered_fixtures[FixtureDefinitionScope.SCENARIO] = self._sort_fixture_list_of_same_definition_scope(
|
|
480
|
+
fixture_namespace_dict=all_fixtures[FixtureDefinitionScope.SCENARIO],
|
|
481
|
+
outer_scope_fixtures=outer_scope_fixtures)
|
|
482
|
+
|
|
483
|
+
return ordered_fixtures
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Union, Type, Callable, Generator, TYPE_CHECKING
|
|
3
|
+
import dataclasses
|
|
4
|
+
|
|
5
|
+
from .utils.typings import MethodLiteralType
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from _balder.scenario import Scenario
|
|
9
|
+
from _balder.setup import Setup
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclasses.dataclass
|
|
13
|
+
class FixtureMetadata:
|
|
14
|
+
"""
|
|
15
|
+
describes meta information for a fixture
|
|
16
|
+
"""
|
|
17
|
+
#: the namespace where it is defined (None if it is in a balderglob file)
|
|
18
|
+
namespace: Union[None, Type[Scenario], Type[Setup]]
|
|
19
|
+
#: the type of the fixture function
|
|
20
|
+
function_type: MethodLiteralType
|
|
21
|
+
#: the fixture callable itself
|
|
22
|
+
callable: Callable
|
|
23
|
+
#: the generator object (if the fixture is not a generator, it holds an empty generator)
|
|
24
|
+
generator: Generator
|
|
25
|
+
#: result according to the fixture's construction code (will be cleaned after it leaves a level)
|
|
26
|
+
retval: object
|
_balder/node_gateway.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from _balder.exceptions import NodeNotExistsError
|
|
4
|
+
from _balder.controllers import DeviceController
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class NodeGateway:
|
|
8
|
+
"""
|
|
9
|
+
This is a class that describes a gateway between two nodes of a device. A gateway enables the interaction of
|
|
10
|
+
different connection nodes of a device.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, device, from_node: str, to_node: str, bidirectional: bool):
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
:param device: the device this gateway belongs to
|
|
17
|
+
|
|
18
|
+
:param from_node: the node name where the gateway starts
|
|
19
|
+
|
|
20
|
+
:param to_node: the node name where the gateway ends
|
|
21
|
+
|
|
22
|
+
:param bidirectional: if this value is true, the gateway can "translate" in both directions
|
|
23
|
+
"""
|
|
24
|
+
#: the device this gateway belongs to
|
|
25
|
+
self.device = device
|
|
26
|
+
|
|
27
|
+
#: the node name of the device the gateway starts
|
|
28
|
+
self.from_node_name = from_node
|
|
29
|
+
|
|
30
|
+
#: the node name of the device the gateway ends
|
|
31
|
+
self.to_node_name = to_node
|
|
32
|
+
|
|
33
|
+
#: if this value is true, the gateway can "translate" in both directions
|
|
34
|
+
self._bidirectional = bidirectional
|
|
35
|
+
|
|
36
|
+
# ---------------------------------- STATIC METHODS ----------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
# ---------------------------------- CLASS METHODS ----------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
# ---------------------------------- PROPERTIES --------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
# ---------------------------------- PROTECTED METHODS -------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
# ---------------------------------- METHODS -----------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
def validate_given_node_names(self):
|
|
47
|
+
"""
|
|
48
|
+
This method validates whether the given node names actually exist in the transferred device. Otherwise, this
|
|
49
|
+
method throws an exception.
|
|
50
|
+
"""
|
|
51
|
+
device_controller = DeviceController.get_for(self.device)
|
|
52
|
+
if self.from_node_name not in device_controller.get_all_connections().keys():
|
|
53
|
+
raise NodeNotExistsError(f"the from_node `{self.from_node_name}` mentioned by the `@gateway` decorator "
|
|
54
|
+
f"does not exist as a node in device `{self.__class__.__name__}`")
|
|
55
|
+
if self.to_node_name not in device_controller.get_all_connections().keys():
|
|
56
|
+
raise NodeNotExistsError(f"the to_node `{self.to_node_name}` mentioned by the `@gateway` decorator does "
|
|
57
|
+
f"not exist as a node in device `{self.__class__.__name__}`")
|
|
58
|
+
|
|
59
|
+
def get_conn_partner_of(self, node) -> str:
|
|
60
|
+
"""
|
|
61
|
+
This method returns the partner node of this gateway - it always returns the other not given side
|
|
62
|
+
"""
|
|
63
|
+
if node not in (self.from_node_name, self.to_node_name):
|
|
64
|
+
raise ValueError(f"the given node `{node}` is no component of this connection")
|
|
65
|
+
|
|
66
|
+
return self.to_node_name if node == self.from_node_name else self.from_node_name
|
|
67
|
+
|
|
68
|
+
def has_connection_from_to(self, start_node, end_node=None) -> bool:
|
|
69
|
+
"""
|
|
70
|
+
This method checks if there is a connection from ``start_node`` to ``end_node``. This will always return true
|
|
71
|
+
if the gateway supports bidirectional communication or the ``start_node`` and ``end_node`` given in this method
|
|
72
|
+
are also the ``start_node`` and ``end_node`` mentioned in this connection object.
|
|
73
|
+
|
|
74
|
+
.. note::
|
|
75
|
+
Of course the given node names have to be the nodes mentioned internally!
|
|
76
|
+
|
|
77
|
+
:param start_node: the node the connection should start
|
|
78
|
+
|
|
79
|
+
:param end_node: the node the connection should end (optional, will be autofilled automatically)
|
|
80
|
+
|
|
81
|
+
:return: returns true if the given direction is possible
|
|
82
|
+
"""
|
|
83
|
+
if start_node not in (self.from_node_name, self.to_node_name):
|
|
84
|
+
raise ValueError(
|
|
85
|
+
f"the given start_node `{start_node.__qualname__}` is no component of this connection")
|
|
86
|
+
if end_node is not None:
|
|
87
|
+
if end_node not in (self.from_node_name, self.to_node_name):
|
|
88
|
+
raise ValueError(
|
|
89
|
+
f"the given end_node `{end_node.__qualname__}` is no component of this connection")
|
|
90
|
+
else:
|
|
91
|
+
end_node = self.from_node_name if start_node == self.to_node_name else self.to_node_name
|
|
92
|
+
|
|
93
|
+
if self.is_bidirectional():
|
|
94
|
+
return True
|
|
95
|
+
|
|
96
|
+
if start_node == self.from_node_name and end_node == self.to_node_name:
|
|
97
|
+
return True
|
|
98
|
+
|
|
99
|
+
return False
|
|
100
|
+
|
|
101
|
+
def is_bidirectional(self) -> bool:
|
|
102
|
+
"""returns true if the gateway works in both directional"""
|
|
103
|
+
return self._bidirectional
|
|
File without changes
|
|
File without changes
|