baldertest 0.1.0b9__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 +4 -2
- _balder/collector.py +115 -7
- _balder/console/balder.py +1 -1
- _balder/controllers/device_controller.py +1 -1
- _balder/controllers/normal_scenario_setup_controller.py +2 -2
- _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 +1 -78
- _balder/executor/executor_tree.py +7 -5
- _balder/executor/parametrized_testcase_executor.py +52 -0
- _balder/executor/scenario_executor.py +5 -2
- _balder/executor/setup_executor.py +5 -2
- _balder/executor/testcase_executor.py +42 -9
- _balder/executor/unresolved_parametrized_testcase_executor.py +209 -0
- _balder/executor/variation_executor.py +26 -8
- _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/__init__.py +6 -0
- balder/exceptions.py +4 -3
- balder/parametrization.py +8 -0
- {baldertest-0.1.0b9.dist-info → baldertest-0.1.0b10.dist-info}/METADATA +1 -1
- {baldertest-0.1.0b9.dist-info → baldertest-0.1.0b10.dist-info}/RECORD +36 -26
- {baldertest-0.1.0b9.dist-info → baldertest-0.1.0b10.dist-info}/WHEEL +1 -1
- {baldertest-0.1.0b9.dist-info → baldertest-0.1.0b10.dist-info}/LICENSE +0 -0
- {baldertest-0.1.0b9.dist-info → baldertest-0.1.0b10.dist-info}/entry_points.txt +0 -0
- {baldertest-0.1.0b9.dist-info → baldertest-0.1.0b10.dist-info}/top_level.txt +0 -0
_balder/_version.py
CHANGED
_balder/balder_session.py
CHANGED
|
@@ -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, TYPE_CHECKING
|
|
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
|
|
@@ -697,7 +805,6 @@ class Collector:
|
|
|
697
805
|
if fnmatch.fnmatch(str(cur_abs_path.relative_to(self.working_dir)), cur_pattern)]
|
|
698
806
|
return list(set(remaining))
|
|
699
807
|
|
|
700
|
-
|
|
701
808
|
def collect(self, plugin_manager: PluginManager, scenario_filter_patterns: Union[List[str], None],
|
|
702
809
|
setup_filter_patterns: Union[List[str], None]):
|
|
703
810
|
"""
|
|
@@ -739,6 +846,7 @@ class Collector:
|
|
|
739
846
|
self._all_setups = Collector.filter_parent_classes_of(items=self._all_setups)
|
|
740
847
|
|
|
741
848
|
Collector.rework_method_variation_decorators()
|
|
849
|
+
Collector.rework_parametrization_decorators()
|
|
742
850
|
|
|
743
851
|
# do some further stuff after everything was read
|
|
744
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:
|
|
@@ -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
|
|
@@ -278,7 +278,7 @@ class NormalScenarioSetupController(Controller, ABC):
|
|
|
278
278
|
found_it = True
|
|
279
279
|
break
|
|
280
280
|
if not found_it:
|
|
281
|
-
raise
|
|
281
|
+
raise MissingFeaturesOfVDeviceError(
|
|
282
282
|
f"the device `{related_device.__name__}` which is mapped to the VDevice "
|
|
283
283
|
f"`{active_vdevice.__name__}` doesn't have an implementation for the feature "
|
|
284
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
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
import traceback
|
|
9
|
+
from abc import ABC, abstractmethod
|
|
10
|
+
|
|
11
|
+
from _balder.executor.basic_executor import BasicExecutor
|
|
12
|
+
from _balder.testresult import ResultState, TestcaseResult
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from _balder.fixture_execution_level import FixtureExecutionLevel
|
|
16
|
+
from _balder.fixture_manager import FixtureManager
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class BasicExecutableExecutor(BasicExecutor, ABC):
|
|
20
|
+
"""
|
|
21
|
+
The BasicExecutor class is an abstract class that represents the parent class of all executors. Together with other
|
|
22
|
+
executor classes, an executor forms a tree structure in which individual tests, which later on are executed, are
|
|
23
|
+
assigned to individual scenarios
|
|
24
|
+
"""
|
|
25
|
+
fixture_execution_level: FixtureExecutionLevel = None
|
|
26
|
+
|
|
27
|
+
def __init__(self):
|
|
28
|
+
super().__init__()
|
|
29
|
+
|
|
30
|
+
# holds the execution time of this branch (with branch fixtures)
|
|
31
|
+
self.execution_time_sec = None
|
|
32
|
+
|
|
33
|
+
# ---------------------------------- STATIC METHODS ----------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
# ---------------------------------- CLASS METHODS ----------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
# ---------------------------------- PROPERTIES --------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
@abstractmethod
|
|
41
|
+
def fixture_manager(self) -> FixtureManager:
|
|
42
|
+
"""returns the active fixture manager instance"""
|
|
43
|
+
|
|
44
|
+
# ---------------------------------- PROTECTED METHODS -------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
@abstractmethod
|
|
47
|
+
def _prepare_execution(self, show_discarded):
|
|
48
|
+
"""
|
|
49
|
+
This method runs before the branch will be executed and before the fixture construction code of this branch
|
|
50
|
+
runs.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
@abstractmethod
|
|
54
|
+
def _body_execution(self, show_discarded):
|
|
55
|
+
"""
|
|
56
|
+
This method runs between the fixture construction and teardown code. It should trigger the execution of the
|
|
57
|
+
child branches.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
@abstractmethod
|
|
61
|
+
def _cleanup_execution(self, show_discarded):
|
|
62
|
+
"""
|
|
63
|
+
This method runs after the branch was executed (also after the fixture teardown code ran)
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
# ---------------------------------- METHODS -----------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
def set_result_for_whole_branch(self, value: ResultState):
|
|
69
|
+
"""
|
|
70
|
+
This method sets the executor result for all sub executors.
|
|
71
|
+
|
|
72
|
+
:param value: the new value that should be set for this branch
|
|
73
|
+
"""
|
|
74
|
+
if value not in (ResultState.SKIP, ResultState.COVERED_BY, ResultState.NOT_RUN):
|
|
75
|
+
raise ValueError("can not set a state that is not NOT_RUN, SKIP or COVERED_BY for a whole branch")
|
|
76
|
+
for cur_child_executor in self.all_child_executors:
|
|
77
|
+
if isinstance(cur_child_executor.body_result, TestcaseResult):
|
|
78
|
+
cur_child_executor.body_result.set_result(result=value, exception=None)
|
|
79
|
+
|
|
80
|
+
@abstractmethod
|
|
81
|
+
def cleanup_empty_executor_branches(self, consider_discarded=False):
|
|
82
|
+
"""
|
|
83
|
+
This method searches the whole tree and removes branches where an executor item has no own children. It can
|
|
84
|
+
remove these branches, because they have no valid matchings.
|
|
85
|
+
|
|
86
|
+
:param consider_discarded: true if this method should consider discarded branches, otherwise False
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
def filter_tree_for_user_filters(self):
|
|
90
|
+
"""
|
|
91
|
+
This method calls all user defined filters that are to be applied to the executor tree.
|
|
92
|
+
"""
|
|
93
|
+
for cur_child_executor in self.all_child_executors:
|
|
94
|
+
cur_child_executor.filter_tree_for_user_filters()
|
|
95
|
+
|
|
96
|
+
def execute(self, show_discarded=False):
|
|
97
|
+
"""
|
|
98
|
+
Executes the whole branch
|
|
99
|
+
"""
|
|
100
|
+
start_time = time.perf_counter()
|
|
101
|
+
self._prepare_execution(show_discarded=show_discarded)
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
try:
|
|
105
|
+
if self.has_runnable_tests():
|
|
106
|
+
self.fixture_manager.enter(self)
|
|
107
|
+
self.construct_result.set_result(ResultState.SUCCESS)
|
|
108
|
+
|
|
109
|
+
self._body_execution(show_discarded=show_discarded)
|
|
110
|
+
except Exception as exc: # pylint: disable=broad-exception-caught
|
|
111
|
+
# this has to be a construction fixture error
|
|
112
|
+
traceback.print_exception(*sys.exc_info())
|
|
113
|
+
self.construct_result.set_result(ResultState.ERROR, exc)
|
|
114
|
+
finally:
|
|
115
|
+
if self.has_runnable_tests():
|
|
116
|
+
if self.fixture_manager.is_allowed_to_leave(self):
|
|
117
|
+
self.fixture_manager.leave(self)
|
|
118
|
+
self.teardown_result.set_result(ResultState.SUCCESS)
|
|
119
|
+
except Exception as exc: # pylint: disable=broad-exception-caught
|
|
120
|
+
# this has to be a teardown fixture error
|
|
121
|
+
traceback.print_exception(*sys.exc_info())
|
|
122
|
+
self.teardown_result.set_result(ResultState.ERROR, exc)
|
|
123
|
+
|
|
124
|
+
self._cleanup_execution(show_discarded=show_discarded)
|
|
125
|
+
|
|
126
|
+
self.execution_time_sec = time.perf_counter() - start_time
|