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.
Files changed (36) hide show
  1. _balder/_version.py +1 -1
  2. _balder/balder_session.py +4 -2
  3. _balder/collector.py +115 -7
  4. _balder/console/balder.py +1 -1
  5. _balder/controllers/device_controller.py +1 -1
  6. _balder/controllers/normal_scenario_setup_controller.py +2 -2
  7. _balder/controllers/scenario_controller.py +91 -1
  8. _balder/decorator_fixture.py +4 -7
  9. _balder/decorator_for_vdevice.py +4 -6
  10. _balder/decorator_parametrize.py +31 -0
  11. _balder/decorator_parametrize_by_feature.py +36 -0
  12. _balder/exceptions.py +6 -0
  13. _balder/executor/basic_executable_executor.py +126 -0
  14. _balder/executor/basic_executor.py +1 -78
  15. _balder/executor/executor_tree.py +7 -5
  16. _balder/executor/parametrized_testcase_executor.py +52 -0
  17. _balder/executor/scenario_executor.py +5 -2
  18. _balder/executor/setup_executor.py +5 -2
  19. _balder/executor/testcase_executor.py +42 -9
  20. _balder/executor/unresolved_parametrized_testcase_executor.py +209 -0
  21. _balder/executor/variation_executor.py +26 -8
  22. _balder/fixture_definition_scope.py +19 -0
  23. _balder/fixture_execution_level.py +22 -0
  24. _balder/fixture_manager.py +169 -182
  25. _balder/fixture_metadata.py +26 -0
  26. _balder/parametrization.py +75 -0
  27. _balder/solver.py +51 -31
  28. balder/__init__.py +6 -0
  29. balder/exceptions.py +4 -3
  30. balder/parametrization.py +8 -0
  31. {baldertest-0.1.0b9.dist-info → baldertest-0.1.0b10.dist-info}/METADATA +1 -1
  32. {baldertest-0.1.0b9.dist-info → baldertest-0.1.0b10.dist-info}/RECORD +36 -26
  33. {baldertest-0.1.0b9.dist-info → baldertest-0.1.0b10.dist-info}/WHEEL +1 -1
  34. {baldertest-0.1.0b9.dist-info → baldertest-0.1.0b10.dist-info}/LICENSE +0 -0
  35. {baldertest-0.1.0b9.dist-info → baldertest-0.1.0b10.dist-info}/entry_points.txt +0 -0
  36. {baldertest-0.1.0b9.dist-info → baldertest-0.1.0b10.dist-info}/top_level.txt +0 -0
_balder/_version.py CHANGED
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '0.1.0b9'
15
+ __version__ = version = '0.1.0b10'
16
16
  __version_tuple__ = version_tuple = (0, 1, 0)
_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, scenarios=self.all_collected_scenarios,
293
- connections=self.all_collected_connections, raw_fixtures=self.collector.raw_fixtures)
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
- raw_fixtures = {}
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
- Union[Type[Connection], Connection, Tuple[Union[Type[Connection], Connection]],
46
- List[Union[Type[Connection], Connection, Tuple[Union[Type[Connection], Connection]]]]]]]] = {}
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 Devuce parent classes for `{self.related_cls.__name__}` "
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 Exception(
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.
@@ -3,7 +3,7 @@ from typing import Literal
3
3
 
4
4
  import functools
5
5
  from _balder.collector import Collector
6
- from _balder.fixture_manager import FixtureManager
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 = FixtureManager.EXECUTION_LEVEL_ORDER
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 add the fixture to FixtureManager.raw_fixtures - class determination will be done later by
22
- # :meth:`Collector`
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):
@@ -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, Device],
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) and not isinstance(vdevice, VDevice):
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
- if func not in Collector._possible_method_variations.keys():
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