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.
Files changed (38) hide show
  1. _balder/_version.py +1 -1
  2. _balder/balder_session.py +5 -3
  3. _balder/collector.py +117 -8
  4. _balder/console/balder.py +1 -1
  5. _balder/controllers/device_controller.py +1 -1
  6. _balder/controllers/feature_controller.py +38 -17
  7. _balder/controllers/normal_scenario_setup_controller.py +5 -12
  8. _balder/controllers/scenario_controller.py +91 -1
  9. _balder/decorator_fixture.py +4 -7
  10. _balder/decorator_for_vdevice.py +4 -6
  11. _balder/decorator_parametrize.py +31 -0
  12. _balder/decorator_parametrize_by_feature.py +36 -0
  13. _balder/exceptions.py +6 -0
  14. _balder/executor/basic_executable_executor.py +126 -0
  15. _balder/executor/basic_executor.py +6 -95
  16. _balder/executor/executor_tree.py +22 -8
  17. _balder/executor/parametrized_testcase_executor.py +52 -0
  18. _balder/executor/scenario_executor.py +10 -2
  19. _balder/executor/setup_executor.py +40 -2
  20. _balder/executor/testcase_executor.py +42 -9
  21. _balder/executor/unresolved_parametrized_testcase_executor.py +209 -0
  22. _balder/executor/variation_executor.py +30 -51
  23. _balder/fixture_definition_scope.py +19 -0
  24. _balder/fixture_execution_level.py +22 -0
  25. _balder/fixture_manager.py +169 -182
  26. _balder/fixture_metadata.py +26 -0
  27. _balder/parametrization.py +75 -0
  28. _balder/solver.py +51 -31
  29. _balder/testresult.py +38 -6
  30. balder/__init__.py +6 -0
  31. balder/exceptions.py +4 -3
  32. balder/parametrization.py +8 -0
  33. {baldertest-0.1.0b8.dist-info → baldertest-0.1.0b10.dist-info}/METADATA +1 -1
  34. {baldertest-0.1.0b8.dist-info → baldertest-0.1.0b10.dist-info}/RECORD +38 -28
  35. {baldertest-0.1.0b8.dist-info → baldertest-0.1.0b10.dist-info}/WHEEL +1 -1
  36. {baldertest-0.1.0b8.dist-info → baldertest-0.1.0b10.dist-info}/LICENSE +0 -0
  37. {baldertest-0.1.0b8.dist-info → baldertest-0.1.0b10.dist-info}/entry_points.txt +0 -0
  38. {baldertest-0.1.0b8.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.0b8'
15
+ __version__ = version = '0.1.0b10'
16
16
  __version_tuple__ = version_tuple = (0, 1, 0)
_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
- # pylint: disable-next=too-many-instance-attributes
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, 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, Any
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
@@ -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]) -> List[pathlib.Path]:
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 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:
@@ -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
- for cur_impl_method, cur_method_impl_dict in self.get_method_based_for_vdevice()[of_method_name].items():
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
- # if there are more than one possible method variation, try to sort them hierarchical
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
- abs_parent_devices_as_names = [cur_parent.__name__ for cur_parent in abs_parent_devices]
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 = False
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 Exception(
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.
@@ -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