baldertest 0.1.0b8__py3-none-any.whl → 0.1.0b10__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/_version.py +1 -1
- _balder/balder_session.py +5 -3
- _balder/collector.py +117 -8
- _balder/console/balder.py +1 -1
- _balder/controllers/device_controller.py +1 -1
- _balder/controllers/feature_controller.py +38 -17
- _balder/controllers/normal_scenario_setup_controller.py +5 -12
- _balder/controllers/scenario_controller.py +91 -1
- _balder/decorator_fixture.py +4 -7
- _balder/decorator_for_vdevice.py +4 -6
- _balder/decorator_parametrize.py +31 -0
- _balder/decorator_parametrize_by_feature.py +36 -0
- _balder/exceptions.py +6 -0
- _balder/executor/basic_executable_executor.py +126 -0
- _balder/executor/basic_executor.py +6 -95
- _balder/executor/executor_tree.py +22 -8
- _balder/executor/parametrized_testcase_executor.py +52 -0
- _balder/executor/scenario_executor.py +10 -2
- _balder/executor/setup_executor.py +40 -2
- _balder/executor/testcase_executor.py +42 -9
- _balder/executor/unresolved_parametrized_testcase_executor.py +209 -0
- _balder/executor/variation_executor.py +30 -51
- _balder/fixture_definition_scope.py +19 -0
- _balder/fixture_execution_level.py +22 -0
- _balder/fixture_manager.py +169 -182
- _balder/fixture_metadata.py +26 -0
- _balder/parametrization.py +75 -0
- _balder/solver.py +51 -31
- _balder/testresult.py +38 -6
- balder/__init__.py +6 -0
- balder/exceptions.py +4 -3
- balder/parametrization.py +8 -0
- {baldertest-0.1.0b8.dist-info → baldertest-0.1.0b10.dist-info}/METADATA +1 -1
- {baldertest-0.1.0b8.dist-info → baldertest-0.1.0b10.dist-info}/RECORD +38 -28
- {baldertest-0.1.0b8.dist-info → baldertest-0.1.0b10.dist-info}/WHEEL +1 -1
- {baldertest-0.1.0b8.dist-info → baldertest-0.1.0b10.dist-info}/LICENSE +0 -0
- {baldertest-0.1.0b8.dist-info → baldertest-0.1.0b10.dist-info}/entry_points.txt +0 -0
- {baldertest-0.1.0b8.dist-info → baldertest-0.1.0b10.dist-info}/top_level.txt +0 -0
_balder/_version.py
CHANGED
_balder/balder_session.py
CHANGED
|
@@ -21,7 +21,7 @@ if TYPE_CHECKING:
|
|
|
21
21
|
from _balder.scenario import Scenario
|
|
22
22
|
from _balder.connection import Connection
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
|
|
25
25
|
class BalderSession:
|
|
26
26
|
"""
|
|
27
27
|
This is the main balder executable object. It contains all information about the current session and executes the
|
|
@@ -289,8 +289,10 @@ class BalderSession:
|
|
|
289
289
|
"""
|
|
290
290
|
This method resolves all classes and executes different checks, that can be done before the test session starts.
|
|
291
291
|
"""
|
|
292
|
-
self.solver = Solver(setups=self.all_collected_setups,
|
|
293
|
-
|
|
292
|
+
self.solver = Solver(setups=self.all_collected_setups,
|
|
293
|
+
scenarios=self.all_collected_scenarios,
|
|
294
|
+
connections=self.all_collected_connections,
|
|
295
|
+
fixture_manager=self.collector.get_fixture_manager())
|
|
294
296
|
self.solver.resolve(plugin_manager=self.plugin_manager)
|
|
295
297
|
|
|
296
298
|
def create_executor_tree(self):
|
_balder/collector.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
-
from typing import List, Type, Union, Dict, Callable, Tuple,
|
|
2
|
+
from typing import List, Type, Union, Dict, Callable, Tuple, Iterable, Any, TYPE_CHECKING
|
|
3
3
|
|
|
4
4
|
import os
|
|
5
5
|
import sys
|
|
@@ -10,13 +10,16 @@ import inspect
|
|
|
10
10
|
import pathlib
|
|
11
11
|
import functools
|
|
12
12
|
import importlib.util
|
|
13
|
-
from _balder.utils import get_class_that_defines_method
|
|
13
|
+
from _balder.utils import get_class_that_defines_method, inspect_method
|
|
14
14
|
from _balder.setup import Setup
|
|
15
15
|
from _balder.device import Device
|
|
16
16
|
from _balder.feature import Feature
|
|
17
17
|
from _balder.vdevice import VDevice
|
|
18
18
|
from _balder.scenario import Scenario
|
|
19
19
|
from _balder.connection import Connection
|
|
20
|
+
from _balder.parametrization import FeatureAccessSelector, Parameter
|
|
21
|
+
from _balder.fixture_manager import FixtureManager
|
|
22
|
+
from _balder.fixture_execution_level import FixtureExecutionLevel
|
|
20
23
|
from _balder.controllers import ScenarioController, SetupController, DeviceController, VDeviceController, \
|
|
21
24
|
FeatureController, NormalScenarioSetupController
|
|
22
25
|
from _balder.exceptions import DuplicateForVDeviceError, UnknownVDeviceException
|
|
@@ -24,6 +27,7 @@ from _balder.utils import get_scenario_inheritance_list_of
|
|
|
24
27
|
|
|
25
28
|
if TYPE_CHECKING:
|
|
26
29
|
from _balder.plugin_manager import PluginManager
|
|
30
|
+
ConnectionType = Union[Type[Connection], Connection, Tuple[Union[Type[Connection], Connection]]]
|
|
27
31
|
|
|
28
32
|
logger = logging.getLogger(__file__)
|
|
29
33
|
|
|
@@ -34,16 +38,23 @@ class Collector:
|
|
|
34
38
|
but secures that all relevant data is being collected.
|
|
35
39
|
"""
|
|
36
40
|
# metadata object that contains all raw fixtures (classes that were not be resolved yet)
|
|
37
|
-
|
|
41
|
+
_raw_fixtures = {}
|
|
38
42
|
|
|
39
43
|
# this static attribute will be managed by the decorator `@for_vdevice(..)`. It holds all functions/methods that
|
|
40
44
|
# were decorated with `@for_vdevice(..)` (without checking their correctness). The collector will check them later
|
|
41
45
|
# with the method `rework_method_variation_decorators()`
|
|
42
46
|
_possible_method_variations: Dict[
|
|
43
47
|
Callable,
|
|
44
|
-
List[Tuple[Union[Type[VDevice], str],
|
|
45
|
-
|
|
46
|
-
|
|
48
|
+
List[Tuple[Union[Type[VDevice], str], Union[ConnectionType, List[ConnectionType]]]]
|
|
49
|
+
] = {}
|
|
50
|
+
|
|
51
|
+
# this static attribute will be managed by the decorator `@parametrize(..)`. It holds all functions/methods that
|
|
52
|
+
# were decorated with `@parametrize(..)` (without checking their correctness). The collector will check it later
|
|
53
|
+
# with the method `rework_static_parametrization_decorators()`
|
|
54
|
+
_possible_parametrization: Dict[
|
|
55
|
+
Callable,
|
|
56
|
+
Dict[str, Union[Iterable[Any], FeatureAccessSelector]]
|
|
57
|
+
] = {}
|
|
47
58
|
|
|
48
59
|
def __init__(self, working_dir: pathlib.Path):
|
|
49
60
|
self.working_dir = pathlib.Path(working_dir)
|
|
@@ -58,6 +69,54 @@ class Collector:
|
|
|
58
69
|
|
|
59
70
|
self.balderglob_was_loaded = False
|
|
60
71
|
|
|
72
|
+
@staticmethod
|
|
73
|
+
def register_raw_fixture(fixture: Callable, level: str):
|
|
74
|
+
"""
|
|
75
|
+
allows to register a new fixture - used by decorator `@balder.fixture()`
|
|
76
|
+
|
|
77
|
+
:param level: the fixture level
|
|
78
|
+
:param fixture: the fixture callable itself
|
|
79
|
+
"""
|
|
80
|
+
if level not in Collector._raw_fixtures.keys():
|
|
81
|
+
Collector._raw_fixtures[level] = []
|
|
82
|
+
Collector._raw_fixtures[level].append(fixture)
|
|
83
|
+
|
|
84
|
+
@staticmethod
|
|
85
|
+
def register_possible_method_variation(
|
|
86
|
+
meth: Callable,
|
|
87
|
+
vdevice: Union[Type[VDevice], str],
|
|
88
|
+
with_connections: Union[ConnectionType, List[ConnectionType]]):
|
|
89
|
+
"""
|
|
90
|
+
allows to register a new method variation - used by decorator `@balder.for_vdevice()`
|
|
91
|
+
|
|
92
|
+
:param meth: the method that should be registered
|
|
93
|
+
:param vdevice: the vdevice the method is for
|
|
94
|
+
:param with_connections: the connections the method is for
|
|
95
|
+
"""
|
|
96
|
+
if meth not in Collector._possible_method_variations.keys():
|
|
97
|
+
Collector._possible_method_variations[meth] = []
|
|
98
|
+
Collector._possible_method_variations[meth].append((vdevice, with_connections))
|
|
99
|
+
|
|
100
|
+
@staticmethod
|
|
101
|
+
def register_possible_parametrization(
|
|
102
|
+
meth: Callable,
|
|
103
|
+
field_name: str,
|
|
104
|
+
values: Union[Iterable[Any], FeatureAccessSelector]
|
|
105
|
+
):
|
|
106
|
+
"""
|
|
107
|
+
allows to register a possible parametrization - used by decorator `@balder.parametrize()` or
|
|
108
|
+
`@balder.parametrize_by_feature()`
|
|
109
|
+
|
|
110
|
+
:param meth: the method that should be registered
|
|
111
|
+
:param field_name: the name of the method argument, the parametrized value should be added
|
|
112
|
+
:param values: an Iterable of all values that should be parametrized or the FeatureAccessSelector object
|
|
113
|
+
"""
|
|
114
|
+
if meth not in Collector._possible_parametrization.keys():
|
|
115
|
+
Collector._possible_parametrization[meth] = {}
|
|
116
|
+
if field_name in Collector._possible_parametrization[meth].keys():
|
|
117
|
+
raise ValueError(f'field `{field_name}` already registered for method `{meth.__qualname__}`')
|
|
118
|
+
Collector._possible_parametrization[meth][field_name] = values
|
|
119
|
+
|
|
61
120
|
@property
|
|
62
121
|
def all_pyfiles(self) -> List[pathlib.Path]:
|
|
63
122
|
"""returns a list of all python files that were be found by the collector"""
|
|
@@ -96,6 +155,23 @@ class Collector:
|
|
|
96
155
|
raise AttributeError("please call the `collect()` method before omitting this value")
|
|
97
156
|
return self._all_connections
|
|
98
157
|
|
|
158
|
+
def get_fixture_manager(self) -> FixtureManager:
|
|
159
|
+
"""
|
|
160
|
+
Resolves all fixtures and returns the fixture manager for this session
|
|
161
|
+
:return: the fixture manager that is valid for this session
|
|
162
|
+
"""
|
|
163
|
+
resolved_dict = {}
|
|
164
|
+
for cur_level_as_str, cur_module_fixture_dict in self._raw_fixtures.items():
|
|
165
|
+
cur_level = FixtureExecutionLevel(cur_level_as_str)
|
|
166
|
+
resolved_dict[cur_level] = {}
|
|
167
|
+
for cur_fn in cur_module_fixture_dict:
|
|
168
|
+
cls, func_type = inspect_method(cur_fn)
|
|
169
|
+
# mechanism also works for balderglob fixtures (`func_type` is 'function' and `cls` is None)
|
|
170
|
+
if cls not in resolved_dict[cur_level].keys():
|
|
171
|
+
resolved_dict[cur_level][cls] = []
|
|
172
|
+
resolved_dict[cur_level][cls].append((func_type, cur_fn))
|
|
173
|
+
return FixtureManager(resolved_dict)
|
|
174
|
+
|
|
99
175
|
def load_balderglob_py_file(self) -> Union[types.ModuleType, None]:
|
|
100
176
|
"""
|
|
101
177
|
This method loads the global balderglob.py file and returns the module or None if the file does not exist.
|
|
@@ -475,6 +551,38 @@ class Collector:
|
|
|
475
551
|
setattr(owner, name, new_callback)
|
|
476
552
|
owner_feature_controller.set_method_based_for_vdevice(owner_for_vdevice)
|
|
477
553
|
|
|
554
|
+
@staticmethod
|
|
555
|
+
def rework_parametrization_decorators():
|
|
556
|
+
"""
|
|
557
|
+
This method iterates over the static attribute `Collector._possible_static_parametrization` and checks if these
|
|
558
|
+
decorated functions are valid (if they are test methods and part of a :meth:`Scenario` class).
|
|
559
|
+
"""
|
|
560
|
+
|
|
561
|
+
for cur_fn, cur_decorator_data_dict in Collector._possible_parametrization.items():
|
|
562
|
+
owner = get_class_that_defines_method(cur_fn)
|
|
563
|
+
if not issubclass(owner, Scenario):
|
|
564
|
+
raise TypeError(f'the related class of `{cur_fn.__qualname__}` is not a `Scenario` class')
|
|
565
|
+
owner_scenario_controller = ScenarioController.get_for(owner)
|
|
566
|
+
if cur_fn not in owner_scenario_controller.get_all_test_methods():
|
|
567
|
+
raise TypeError(f'the method {cur_fn.__qualname__} is not a test method')
|
|
568
|
+
args_of_cur_fn = inspect.getfullargspec(cur_fn).args
|
|
569
|
+
|
|
570
|
+
for cur_field_name, cur_value_list in cur_decorator_data_dict.items():
|
|
571
|
+
if isinstance(cur_value_list, FeatureAccessSelector):
|
|
572
|
+
# make sure that all parameters exist in test method parametrization
|
|
573
|
+
for cur_value_parameter in cur_value_list.parameters.values():
|
|
574
|
+
if isinstance(cur_value_parameter, Parameter):
|
|
575
|
+
if cur_value_parameter.name not in cur_decorator_data_dict.keys():
|
|
576
|
+
raise AttributeError(f'can not find attribute `{cur_value_parameter.name}` that is '
|
|
577
|
+
f'used in parametrization for attribute `{cur_field_name}` in '
|
|
578
|
+
f'test method `{cur_fn.__qualname__}`')
|
|
579
|
+
if cur_field_name not in args_of_cur_fn:
|
|
580
|
+
raise ValueError(f'the argument `{cur_field_name}` does not exist in test method '
|
|
581
|
+
f'`{cur_fn.__qualname__}`')
|
|
582
|
+
owner_scenario_controller.register_parametrization(cur_fn, cur_field_name, cur_value_list)
|
|
583
|
+
|
|
584
|
+
owner_scenario_controller.check_for_parameter_loop_in_dynamic_parametrization(cur_fn)
|
|
585
|
+
|
|
478
586
|
def get_all_scenario_feature_classes(self) -> List[Type[Feature]]:
|
|
479
587
|
"""
|
|
480
588
|
This method returns a list with all :class:`Feature` classes that are being instantiated in one or more
|
|
@@ -678,7 +786,8 @@ class Collector:
|
|
|
678
786
|
for cur_setup in self.all_setups:
|
|
679
787
|
SetupController.get_for(cur_setup).validate_feature_possibility()
|
|
680
788
|
|
|
681
|
-
def _filter_paths_after_allowed_paths(self, paths: List[pathlib.Path], filter_patterns: List[str])
|
|
789
|
+
def _filter_paths_after_allowed_paths(self, paths: List[pathlib.Path], filter_patterns: List[str]) \
|
|
790
|
+
-> List[pathlib.Path]:
|
|
682
791
|
"""
|
|
683
792
|
This method filters the given list of filepaths for the given filter_patterns. It returns a list with all
|
|
684
793
|
remaining paths that are mathing the filter statements in `filter_paths`.
|
|
@@ -696,7 +805,6 @@ class Collector:
|
|
|
696
805
|
if fnmatch.fnmatch(str(cur_abs_path.relative_to(self.working_dir)), cur_pattern)]
|
|
697
806
|
return list(set(remaining))
|
|
698
807
|
|
|
699
|
-
|
|
700
808
|
def collect(self, plugin_manager: PluginManager, scenario_filter_patterns: Union[List[str], None],
|
|
701
809
|
setup_filter_patterns: Union[List[str], None]):
|
|
702
810
|
"""
|
|
@@ -738,6 +846,7 @@ class Collector:
|
|
|
738
846
|
self._all_setups = Collector.filter_parent_classes_of(items=self._all_setups)
|
|
739
847
|
|
|
740
848
|
Collector.rework_method_variation_decorators()
|
|
849
|
+
Collector.rework_parametrization_decorators()
|
|
741
850
|
|
|
742
851
|
# do some further stuff after everything was read
|
|
743
852
|
self._set_original_vdevice_in_features()
|
_balder/console/balder.py
CHANGED
|
@@ -50,7 +50,7 @@ def _console_balder_debug(cmd_args: Optional[List[str]] = None, working_dir: Uni
|
|
|
50
50
|
cb_balder_exc(exc)
|
|
51
51
|
traceback.print_exception(*sys.exc_info())
|
|
52
52
|
sys.exit(ExitCode.BALDER_USAGE_ERROR.value)
|
|
53
|
-
except Exception as exc:
|
|
53
|
+
except Exception as exc: # pylint: disable=broad-exception-caught
|
|
54
54
|
# a unexpected error occurs
|
|
55
55
|
if cb_unexpected_exc:
|
|
56
56
|
cb_unexpected_exc(exc)
|
|
@@ -122,7 +122,7 @@ class DeviceController(BaseDeviceController, ABC):
|
|
|
122
122
|
if issubclass(cur_base, Device):
|
|
123
123
|
if next_base_class is not None:
|
|
124
124
|
raise MultiInheritanceError(
|
|
125
|
-
f"found more than one
|
|
125
|
+
f"found more than one Device parent classes for `{self.related_cls.__name__}` "
|
|
126
126
|
f"- multi inheritance is not allowed for Device classes")
|
|
127
127
|
next_base_class = cur_base
|
|
128
128
|
if next_base_class == Device:
|
|
@@ -129,6 +129,39 @@ class FeatureController(Controller):
|
|
|
129
129
|
return [Connection()]
|
|
130
130
|
return intersection
|
|
131
131
|
|
|
132
|
+
def _determine_all_theoretically_unordered_method_variations(
|
|
133
|
+
self, of_method_name: str, for_vdevice: Type[VDevice],
|
|
134
|
+
with_connection: Union[Connection, Tuple[Connection]]) -> Dict[Callable, Connection]:
|
|
135
|
+
"""
|
|
136
|
+
This method returns all theoretically matching method variations. It returns more than one, if there are
|
|
137
|
+
multiple method variation for the given VDevice in this feature, where the given connection is part of the
|
|
138
|
+
connection described by the method variation.
|
|
139
|
+
|
|
140
|
+
:param of_method_name: the name of the method that should be returned
|
|
141
|
+
:param for_vdevice: the VDevice that is mapped
|
|
142
|
+
:param with_connection: the connection that is used between the device that uses the related feature and the
|
|
143
|
+
VDevice
|
|
144
|
+
:return: a dictionary that holds all available method variation that matches here
|
|
145
|
+
"""
|
|
146
|
+
all_possible_method_variations = {}
|
|
147
|
+
for cur_impl_method, cur_method_impl_dict in self.get_method_based_for_vdevice()[of_method_name].items():
|
|
148
|
+
if for_vdevice in cur_method_impl_dict.keys():
|
|
149
|
+
cur_impl_method_cnns = []
|
|
150
|
+
for cur_cnn in cur_method_impl_dict[for_vdevice]:
|
|
151
|
+
cur_impl_method_cnns += cur_cnn.get_singles()
|
|
152
|
+
for cur_single_impl_method_cnn in cur_impl_method_cnns:
|
|
153
|
+
if cur_single_impl_method_cnn.contained_in(with_connection, ignore_metadata=True):
|
|
154
|
+
# this variation is possible
|
|
155
|
+
# ADD IT if it is not available yet
|
|
156
|
+
if cur_impl_method not in all_possible_method_variations.keys():
|
|
157
|
+
all_possible_method_variations[cur_impl_method] = cur_single_impl_method_cnn
|
|
158
|
+
# COMBINE IT if it is already available
|
|
159
|
+
else:
|
|
160
|
+
all_possible_method_variations[cur_impl_method] = Connection.based_on(
|
|
161
|
+
all_possible_method_variations[cur_impl_method], cur_single_impl_method_cnn)
|
|
162
|
+
return all_possible_method_variations
|
|
163
|
+
|
|
164
|
+
|
|
132
165
|
# ---------------------------------- METHODS -----------------------------------------------------------------------
|
|
133
166
|
|
|
134
167
|
def get_class_based_for_vdevice(self) -> Union[Dict[Type[VDevice], List[Connection]], None]:
|
|
@@ -321,24 +354,10 @@ class FeatureController(Controller):
|
|
|
321
354
|
raise ValueError(f"can not find the method `{of_method_name}` in method variation data dictionary")
|
|
322
355
|
|
|
323
356
|
# first determine all possible method-variations
|
|
324
|
-
all_possible_method_variations =
|
|
325
|
-
|
|
326
|
-
if for_vdevice in cur_method_impl_dict.keys():
|
|
327
|
-
cur_impl_method_cnns = []
|
|
328
|
-
for cur_cnn in cur_method_impl_dict[for_vdevice]:
|
|
329
|
-
cur_impl_method_cnns += cur_cnn.get_singles()
|
|
330
|
-
for cur_single_impl_method_cnn in cur_impl_method_cnns:
|
|
331
|
-
if cur_single_impl_method_cnn.contained_in(with_connection, ignore_metadata=True):
|
|
332
|
-
# this variation is possible
|
|
333
|
-
# ADD IT if it is not available yet
|
|
334
|
-
if cur_impl_method not in all_possible_method_variations.keys():
|
|
335
|
-
all_possible_method_variations[cur_impl_method] = cur_single_impl_method_cnn
|
|
336
|
-
# COMBINE IT if it is already available
|
|
337
|
-
else:
|
|
338
|
-
all_possible_method_variations[cur_impl_method] = Connection.based_on(
|
|
339
|
-
all_possible_method_variations[cur_impl_method], cur_single_impl_method_cnn)
|
|
357
|
+
all_possible_method_variations = self._determine_all_theoretically_unordered_method_variations(
|
|
358
|
+
of_method_name=of_method_name, for_vdevice=for_vdevice, with_connection=with_connection)
|
|
340
359
|
|
|
341
|
-
#
|
|
360
|
+
# there are no method variations in this feature directly -> check parent classes
|
|
342
361
|
if len(all_possible_method_variations) == 0:
|
|
343
362
|
# try to execute this method in parent classes
|
|
344
363
|
for cur_base in self.related_cls.__bases__:
|
|
@@ -355,9 +374,11 @@ class FeatureController(Controller):
|
|
|
355
374
|
f"and usable connection `{with_connection.get_tree_str()}´")
|
|
356
375
|
return None
|
|
357
376
|
|
|
377
|
+
# we only have one -> selection is clear
|
|
358
378
|
if len(all_possible_method_variations) == 1:
|
|
359
379
|
return list(all_possible_method_variations.keys())[0]
|
|
360
380
|
|
|
381
|
+
# if there are more than one possible method variation, try to sort them hierarchical
|
|
361
382
|
# we have to determine the outer one
|
|
362
383
|
length_before = None
|
|
363
384
|
while length_before is None or length_before != len(all_possible_method_variations):
|
|
@@ -10,7 +10,7 @@ from _balder.scenario import Scenario
|
|
|
10
10
|
from _balder.controllers.controller import Controller
|
|
11
11
|
from _balder.controllers.device_controller import DeviceController
|
|
12
12
|
from _balder.controllers.vdevice_controller import VDeviceController
|
|
13
|
-
from _balder.exceptions import MultiInheritanceError, DeviceOverwritingError
|
|
13
|
+
from _balder.exceptions import MultiInheritanceError, DeviceOverwritingError, MissingFeaturesOfVDeviceError
|
|
14
14
|
|
|
15
15
|
if TYPE_CHECKING:
|
|
16
16
|
from _balder.connection import Connection
|
|
@@ -188,7 +188,7 @@ class NormalScenarioSetupController(Controller, ABC):
|
|
|
188
188
|
|
|
189
189
|
devices = self.get_all_inner_device_classes()
|
|
190
190
|
abs_parent_devices = parent_scenario_or_setup_controller.get_all_abs_inner_device_classes()
|
|
191
|
-
|
|
191
|
+
abs_parent_devices_by_name = {cur_parent.__name__: cur_parent for cur_parent in abs_parent_devices}
|
|
192
192
|
|
|
193
193
|
if len(devices) == 0:
|
|
194
194
|
# ignore it because cur item has no own device definitions
|
|
@@ -197,10 +197,7 @@ class NormalScenarioSetupController(Controller, ABC):
|
|
|
197
197
|
# check that a device is newly defined or has the same name as the parent device
|
|
198
198
|
for cur_item_device in devices:
|
|
199
199
|
# check if name exists in parent
|
|
200
|
-
relevant_parent_according_naming = None
|
|
201
|
-
if cur_item_device.__name__ in abs_parent_devices_as_names:
|
|
202
|
-
relevant_parent_according_naming = \
|
|
203
|
-
abs_parent_devices[abs_parent_devices_as_names.index(cur_item_device.__name__)]
|
|
200
|
+
relevant_parent_according_naming = abs_parent_devices_by_name.get(cur_item_device.__name__, None)
|
|
204
201
|
|
|
205
202
|
# check if device is inherited from a parent
|
|
206
203
|
relevant_parent_device_according_inheritance = None
|
|
@@ -236,11 +233,7 @@ class NormalScenarioSetupController(Controller, ABC):
|
|
|
236
233
|
|
|
237
234
|
# secure that all parent devices are implemented here too
|
|
238
235
|
for cur_parent in abs_parent_devices:
|
|
239
|
-
found_parent =
|
|
240
|
-
for cur_item_device in devices:
|
|
241
|
-
if issubclass(cur_item_device, cur_parent):
|
|
242
|
-
found_parent = True
|
|
243
|
-
break
|
|
236
|
+
found_parent = len([dev for dev in devices if issubclass(dev, cur_parent)]) > 0
|
|
244
237
|
if not found_parent:
|
|
245
238
|
raise DeviceOverwritingError(
|
|
246
239
|
f"found a device `{cur_parent.__qualname__}` which is part of a parent class, but it is "
|
|
@@ -285,7 +278,7 @@ class NormalScenarioSetupController(Controller, ABC):
|
|
|
285
278
|
found_it = True
|
|
286
279
|
break
|
|
287
280
|
if not found_it:
|
|
288
|
-
raise
|
|
281
|
+
raise MissingFeaturesOfVDeviceError(
|
|
289
282
|
f"the device `{related_device.__name__}` which is mapped to the VDevice "
|
|
290
283
|
f"`{active_vdevice.__name__}` doesn't have an implementation for the feature "
|
|
291
284
|
f"`{cur_vdevice_feature.__class__.__name__}` required by the VDevice class "
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
-
from typing import Type, Dict, List, Tuple, Union
|
|
2
|
+
from typing import Type, Dict, List, Tuple, Union, Callable, Iterable, Any
|
|
3
3
|
|
|
4
4
|
import logging
|
|
5
5
|
import inspect
|
|
6
|
+
from collections import OrderedDict
|
|
6
7
|
from _balder.device import Device
|
|
7
8
|
from _balder.scenario import Scenario
|
|
8
9
|
from _balder.connection import Connection
|
|
9
10
|
from _balder.controllers.feature_controller import FeatureController
|
|
10
11
|
from _balder.controllers.device_controller import DeviceController
|
|
11
12
|
from _balder.controllers.normal_scenario_setup_controller import NormalScenarioSetupController
|
|
13
|
+
from _balder.parametrization import FeatureAccessSelector, Parameter
|
|
12
14
|
from _balder.exceptions import UnclearAssignableFeatureConnectionError, ConnectionIntersectionError, \
|
|
13
15
|
MultiInheritanceError
|
|
14
16
|
|
|
@@ -26,6 +28,8 @@ class ScenarioController(NormalScenarioSetupController):
|
|
|
26
28
|
#: contains all existing scenarios and its corresponding controller object
|
|
27
29
|
_items: Dict[Type[Scenario], ScenarioController] = {}
|
|
28
30
|
|
|
31
|
+
_parametrization: Dict[Callable, Dict[str, Union[Iterable[Any], FeatureAccessSelector]]] = {}
|
|
32
|
+
|
|
29
33
|
def __init__(self, related_cls, _priv_instantiate_key):
|
|
30
34
|
|
|
31
35
|
# describes if the current controller is for setups or for scenarios (has to be set in child controller)
|
|
@@ -72,6 +76,92 @@ class ScenarioController(NormalScenarioSetupController):
|
|
|
72
76
|
|
|
73
77
|
# ---------------------------------- METHODS -----------------------------------------------------------------------
|
|
74
78
|
|
|
79
|
+
def register_parametrization(
|
|
80
|
+
self,
|
|
81
|
+
test_method: Callable,
|
|
82
|
+
field_name: str,
|
|
83
|
+
values: Iterable[Any] | FeatureAccessSelector
|
|
84
|
+
) -> None:
|
|
85
|
+
"""
|
|
86
|
+
This method registers a custom parametrization for a test method of this Scenario
|
|
87
|
+
"""
|
|
88
|
+
if test_method not in self.get_all_test_methods():
|
|
89
|
+
raise ValueError(f'got test method `{test_method.__qualname__}` which is no part of the '
|
|
90
|
+
f'scenario `{self.related_cls}`')
|
|
91
|
+
if test_method not in self._parametrization.keys():
|
|
92
|
+
self._parametrization[test_method] = {}
|
|
93
|
+
if field_name in self._parametrization[test_method].keys():
|
|
94
|
+
raise ValueError(f'field name `{field_name}` for test method `{test_method.__qualname__}` already '
|
|
95
|
+
f'registered')
|
|
96
|
+
self._parametrization[test_method][field_name] = values
|
|
97
|
+
|
|
98
|
+
def get_parametrization_for(
|
|
99
|
+
self,
|
|
100
|
+
test_method: Callable,
|
|
101
|
+
static: bool = True,
|
|
102
|
+
dynamic: bool = True,
|
|
103
|
+
) -> OrderedDict[str, Iterable[Any] | FeatureAccessSelector] | None:
|
|
104
|
+
"""
|
|
105
|
+
This method returns the parametrization for a test method of this Scenario. It returns the parameter
|
|
106
|
+
configuration for every parameter in an OrderedDict.
|
|
107
|
+
|
|
108
|
+
:param test_method: the test method of the Scenario
|
|
109
|
+
:param static: if False, all static parameters will not be included into the dict.
|
|
110
|
+
:param dynamic: if False, all dynamic parameters will not be included into the dict.
|
|
111
|
+
"""
|
|
112
|
+
if test_method not in self._parametrization.keys():
|
|
113
|
+
return None
|
|
114
|
+
params = self._parametrization[test_method]
|
|
115
|
+
|
|
116
|
+
# get arguments in defined order
|
|
117
|
+
arguments = [name for name in inspect.getfullargspec(test_method).args if name in params.keys()]
|
|
118
|
+
ordered_dict = OrderedDict()
|
|
119
|
+
for cur_arg in arguments:
|
|
120
|
+
cur_value = params[cur_arg]
|
|
121
|
+
if isinstance(cur_value, FeatureAccessSelector) and dynamic is False:
|
|
122
|
+
continue
|
|
123
|
+
if not isinstance(cur_value, FeatureAccessSelector) and static is False:
|
|
124
|
+
continue
|
|
125
|
+
ordered_dict[cur_arg] = params[cur_arg]
|
|
126
|
+
return ordered_dict
|
|
127
|
+
|
|
128
|
+
def check_for_parameter_loop_in_dynamic_parametrization(self, cur_fn: Callable):
|
|
129
|
+
"""
|
|
130
|
+
This method checks for a parameter loop in all dynamic parametrization for a specific test method. If it detects
|
|
131
|
+
a loop an AttributeError is thrown
|
|
132
|
+
"""
|
|
133
|
+
# only dynamic parametrization can have Parameter
|
|
134
|
+
parametrization = self.get_parametrization_for(cur_fn, static=False, dynamic=True)
|
|
135
|
+
|
|
136
|
+
def get_dependent_parameters_of_attribute(attribute: str) -> List[str] | None:
|
|
137
|
+
cur_feature_access_selector = parametrization.get(attribute)
|
|
138
|
+
if cur_feature_access_selector is None:
|
|
139
|
+
return None
|
|
140
|
+
# relevant are parameters only if they are from :class:`Parameter` and contained in the dynamic
|
|
141
|
+
# parametrization
|
|
142
|
+
return [param.name for param in cur_feature_access_selector.parameters.values()
|
|
143
|
+
if isinstance(param, Parameter) and param.name in parametrization.keys()]
|
|
144
|
+
|
|
145
|
+
def recursive_parameter_loop_check(for_attribute, with_attribute: str):
|
|
146
|
+
dependent_attr = get_dependent_parameters_of_attribute(with_attribute)
|
|
147
|
+
if dependent_attr is None:
|
|
148
|
+
# no problematic dependencies because attribute is no dynamic attribute
|
|
149
|
+
return
|
|
150
|
+
if len(dependent_attr) == 0:
|
|
151
|
+
# no problematic dependencies
|
|
152
|
+
return
|
|
153
|
+
|
|
154
|
+
if for_attribute in dependent_attr:
|
|
155
|
+
# loop detected
|
|
156
|
+
raise AttributeError('detect a loop in Parameter() object - can not apply parametrization')
|
|
157
|
+
# go deeper and resolve all dependent
|
|
158
|
+
for cur_dependent_attr in dependent_attr:
|
|
159
|
+
recursive_parameter_loop_check(for_attribute, cur_dependent_attr)
|
|
160
|
+
return
|
|
161
|
+
|
|
162
|
+
for cur_attr in parametrization.keys():
|
|
163
|
+
recursive_parameter_loop_check(cur_attr, cur_attr)
|
|
164
|
+
|
|
75
165
|
def get_next_parent_class(self) -> Union[Type[Scenario], None]:
|
|
76
166
|
"""
|
|
77
167
|
This method returns the next parent class which is a subclass of the :class:`Scenario` itself.
|
_balder/decorator_fixture.py
CHANGED
|
@@ -3,7 +3,7 @@ from typing import Literal
|
|
|
3
3
|
|
|
4
4
|
import functools
|
|
5
5
|
from _balder.collector import Collector
|
|
6
|
-
from _balder.
|
|
6
|
+
from _balder.fixture_execution_level import FixtureExecutionLevel
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
def fixture(level: Literal['session', 'setup', 'scenario', 'variation', 'testcase']):
|
|
@@ -12,17 +12,14 @@ def fixture(level: Literal['session', 'setup', 'scenario', 'variation', 'testcas
|
|
|
12
12
|
|
|
13
13
|
:param level: the execution level the fixture should have
|
|
14
14
|
"""
|
|
15
|
-
allowed_levels =
|
|
15
|
+
allowed_levels = [level.value for level in FixtureExecutionLevel]
|
|
16
16
|
|
|
17
17
|
if level not in allowed_levels:
|
|
18
18
|
raise ValueError(f"the value of `level` must be a `str` with one of the values `{'`, `'.join(allowed_levels)}`")
|
|
19
19
|
|
|
20
20
|
def decorator_fixture(func):
|
|
21
|
-
# always
|
|
22
|
-
|
|
23
|
-
if level not in Collector.raw_fixtures.keys():
|
|
24
|
-
Collector.raw_fixtures[level] = []
|
|
25
|
-
Collector.raw_fixtures[level].append(func)
|
|
21
|
+
# always register the raw fixture in Collector - class determination will be done later by :meth:`Collector`
|
|
22
|
+
Collector.register_raw_fixture(func, level)
|
|
26
23
|
|
|
27
24
|
@functools.wraps(func)
|
|
28
25
|
def wrapper_fixture(*args, **kwargs):
|
_balder/decorator_for_vdevice.py
CHANGED
|
@@ -4,7 +4,6 @@ from typing import List, Union, Type, Tuple
|
|
|
4
4
|
import inspect
|
|
5
5
|
from _balder.collector import Collector
|
|
6
6
|
from _balder.feature import Feature
|
|
7
|
-
from _balder.device import Device
|
|
8
7
|
from _balder.vdevice import VDevice
|
|
9
8
|
from _balder.connection import Connection
|
|
10
9
|
from _balder.controllers import FeatureController
|
|
@@ -12,7 +11,7 @@ from _balder.exceptions import DuplicateForVDeviceError, UnknownVDeviceException
|
|
|
12
11
|
|
|
13
12
|
|
|
14
13
|
def for_vdevice(
|
|
15
|
-
vdevice: Union[str,
|
|
14
|
+
vdevice: Union[str, Type[VDevice]],
|
|
16
15
|
with_connections: Union[
|
|
17
16
|
Type[Connection], Connection, Tuple[Union[Type[Connection], Connection]],
|
|
18
17
|
List[Union[Type[Connection], Connection, Tuple[Union[Type[Connection], Connection]]]]] = Connection(),
|
|
@@ -58,9 +57,10 @@ def for_vdevice(
|
|
|
58
57
|
f"a element of it")
|
|
59
58
|
|
|
60
59
|
idx += 1
|
|
60
|
+
|
|
61
61
|
# note: if `args` is an empty list - no special sub-connection-tree bindings
|
|
62
62
|
|
|
63
|
-
if not isinstance(vdevice, str)
|
|
63
|
+
if not (isinstance(vdevice, str) or (isinstance(vdevice, type) and issubclass(vdevice, VDevice))):
|
|
64
64
|
raise ValueError('the given element for `vdevice` has to be a `str` or has to be a subclass of'
|
|
65
65
|
'`VDevice`')
|
|
66
66
|
|
|
@@ -74,9 +74,7 @@ def for_vdevice(
|
|
|
74
74
|
self.func = func
|
|
75
75
|
|
|
76
76
|
# we detect a decorated non-class object -> save it and check it later in collector
|
|
77
|
-
|
|
78
|
-
Collector._possible_method_variations[func] = []
|
|
79
|
-
Collector._possible_method_variations[func].append((vdevice, with_connections))
|
|
77
|
+
Collector.register_possible_method_variation(func, vdevice, with_connections)
|
|
80
78
|
|
|
81
79
|
def __new__(cls, *args, **kwargs): # pylint: disable=unused-argument
|
|
82
80
|
nonlocal vdevice
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Iterable, Any
|
|
3
|
+
|
|
4
|
+
import inspect
|
|
5
|
+
from _balder.collector import Collector
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def parametrize(
|
|
9
|
+
field_name: str,
|
|
10
|
+
values: Iterable[Any],
|
|
11
|
+
):
|
|
12
|
+
"""
|
|
13
|
+
Allows to parametrize a test function. This decorator will be used to statically parametrize a test function.
|
|
14
|
+
|
|
15
|
+
:param field_name: the field name of the test function
|
|
16
|
+
|
|
17
|
+
:param values: an iterable of values, that should be used to parametrize the test function
|
|
18
|
+
"""
|
|
19
|
+
if not isinstance(field_name, str):
|
|
20
|
+
raise ValueError('the given field name must be a string')
|
|
21
|
+
|
|
22
|
+
def decorator(func):
|
|
23
|
+
nonlocal field_name
|
|
24
|
+
nonlocal values
|
|
25
|
+
|
|
26
|
+
if not inspect.isfunction(func):
|
|
27
|
+
raise TypeError('the decorated object needs to be a test method')
|
|
28
|
+
|
|
29
|
+
Collector.register_possible_parametrization(func, field_name, values)
|
|
30
|
+
return func
|
|
31
|
+
return decorator
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Dict, Tuple, Type
|
|
3
|
+
|
|
4
|
+
import inspect
|
|
5
|
+
from _balder.collector import Collector
|
|
6
|
+
from _balder.device import Device
|
|
7
|
+
from _balder.parametrization import FeatureAccessSelector, Parameter, Value
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def parametrize_by_feature(
|
|
11
|
+
field_name: str,
|
|
12
|
+
feature_accessor: Tuple[Type[Device], str, str],
|
|
13
|
+
parameter: Dict[str, FeatureAccessSelector | Parameter | Value] = None
|
|
14
|
+
):
|
|
15
|
+
"""
|
|
16
|
+
Allows to parametrize a test function. This decorator will be used to dynamically parametrize a test function, by
|
|
17
|
+
the value a setup feature returns before entering the test.
|
|
18
|
+
|
|
19
|
+
:param field_name: the field name of the test function
|
|
20
|
+
:param feature_accessor: a tuple that provides information for accessing the feature
|
|
21
|
+
:param parameter: the parameter to parametrize the feature method (if necessary)
|
|
22
|
+
"""
|
|
23
|
+
if not isinstance(field_name, str):
|
|
24
|
+
raise ValueError('the given field name must be a string')
|
|
25
|
+
if parameter is None:
|
|
26
|
+
parameter = {}
|
|
27
|
+
|
|
28
|
+
feature_accessor = FeatureAccessSelector(*feature_accessor, parameters=parameter)
|
|
29
|
+
|
|
30
|
+
def decorator(func):
|
|
31
|
+
if not inspect.isfunction(func):
|
|
32
|
+
raise TypeError('the decorated object needs to be a test method')
|
|
33
|
+
|
|
34
|
+
Collector.register_possible_parametrization(func, field_name, feature_accessor)
|
|
35
|
+
return func
|
|
36
|
+
return decorator
|
_balder/exceptions.py
CHANGED
|
@@ -158,6 +158,12 @@ class IllegalVDeviceMappingError(BalderException):
|
|
|
158
158
|
"""
|
|
159
159
|
|
|
160
160
|
|
|
161
|
+
class MissingFeaturesOfVDeviceError(BalderException):
|
|
162
|
+
"""
|
|
163
|
+
is thrown if the related device does not implement all features specified in its mapped VDevice.
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
|
|
161
167
|
class NotApplicableVariationException(BalderException):
|
|
162
168
|
"""
|
|
163
169
|
is thrown internally after the current variation is not applicable
|