baldertest 0.1.0b12__py3-none-any.whl → 0.1.0b14__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 (40) hide show
  1. _balder/_version.py +9 -4
  2. _balder/cnnrelations/and_connection_relation.py +28 -1
  3. _balder/cnnrelations/base_connection_relation.py +1 -1
  4. _balder/collector.py +130 -119
  5. _balder/connection.py +76 -80
  6. _balder/controllers/device_controller.py +5 -24
  7. _balder/controllers/feature_controller.py +2 -2
  8. _balder/controllers/normal_scenario_setup_controller.py +1 -2
  9. _balder/controllers/scenario_controller.py +121 -0
  10. _balder/controllers/vdevice_controller.py +2 -23
  11. _balder/decorator_connect.py +3 -3
  12. _balder/decorator_covered_by.py +28 -36
  13. _balder/executor/basic_executable_executor.py +13 -6
  14. _balder/executor/basic_executor.py +31 -4
  15. _balder/executor/executor_tree.py +5 -4
  16. _balder/executor/parametrized_testcase_executor.py +2 -2
  17. _balder/executor/scenario_executor.py +12 -71
  18. _balder/executor/setup_executor.py +4 -3
  19. _balder/executor/testcase_executor.py +35 -19
  20. _balder/executor/unresolved_parametrized_testcase_executor.py +32 -57
  21. _balder/executor/variation_executor.py +21 -48
  22. _balder/feature.py +2 -1
  23. _balder/feature_replacement_mapping.py +39 -1
  24. _balder/fixture_manager.py +1 -1
  25. _balder/fixture_metadata.py +1 -1
  26. _balder/routing_path.py +18 -10
  27. _balder/scenario.py +2 -2
  28. _balder/setup.py +2 -1
  29. _balder/testresult.py +4 -3
  30. _balder/utils/__init__.py +0 -0
  31. _balder/{utils.py → utils/functions.py} +29 -31
  32. _balder/utils/inner_device_managing_metaclass.py +14 -0
  33. _balder/utils/mixin_can_be_covered_by_executor.py +24 -0
  34. _balder/utils/typings.py +4 -0
  35. {baldertest-0.1.0b12.dist-info → baldertest-0.1.0b14.dist-info}/METADATA +3 -2
  36. {baldertest-0.1.0b12.dist-info → baldertest-0.1.0b14.dist-info}/RECORD +40 -36
  37. {baldertest-0.1.0b12.dist-info → baldertest-0.1.0b14.dist-info}/WHEEL +1 -1
  38. {baldertest-0.1.0b12.dist-info → baldertest-0.1.0b14.dist-info}/entry_points.txt +0 -0
  39. {baldertest-0.1.0b12.dist-info → baldertest-0.1.0b14.dist-info/licenses}/LICENSE +0 -0
  40. {baldertest-0.1.0b12.dist-info → baldertest-0.1.0b14.dist-info}/top_level.txt +0 -0
@@ -3,7 +3,6 @@ from __future__ import annotations
3
3
  import copy
4
4
  from typing import Dict, List, Type, Union, TYPE_CHECKING
5
5
 
6
- import sys
7
6
  import logging
8
7
  import inspect
9
8
  from abc import ABC
@@ -13,7 +12,7 @@ from _balder.vdevice import VDevice
13
12
  from _balder.scenario import Scenario
14
13
  from _balder.controllers.base_device_controller import BaseDeviceController
15
14
  from _balder.controllers.feature_controller import FeatureController
16
- from _balder.exceptions import DeviceScopeError, DeviceResolvingException, InnerFeatureResolvingError, \
15
+ from _balder.exceptions import DeviceResolvingException, InnerFeatureResolvingError, \
17
16
  FeatureOverwritingError, MultiInheritanceError
18
17
  if TYPE_CHECKING:
19
18
  from _balder.connection import Connection
@@ -108,6 +107,7 @@ class DeviceController(BaseDeviceController, ABC):
108
107
  # ---------------------------------- PROTECTED METHODS -------------------------------------------------------------
109
108
 
110
109
  def __get_outer_class_controller(self) -> Union[ScenarioController, SetupController]:
110
+ # pylint: disable-next=import-outside-toplevel
111
111
  from _balder.controllers.normal_scenario_setup_controller import NormalScenarioSetupController
112
112
 
113
113
  outer_class = self.get_outer_class()
@@ -307,25 +307,7 @@ class DeviceController(BaseDeviceController, ABC):
307
307
  This method delivers the outer class of the related device. This has to be a :class:`Setup` or a
308
308
  :class:`Scenario`.
309
309
  """
310
-
311
- if self.related_cls.__qualname__.count('.') == 0:
312
- raise DeviceScopeError("the current device class is no inner class")
313
-
314
- if self.related_cls.__qualname__.count('.') > 1:
315
- raise DeviceScopeError("the current device class is no direct inner class (deeper than one)")
316
-
317
- outer_class_name, _ = self.related_cls.__qualname__.split('.')
318
-
319
- outer_class = [cur_class for cur_name, cur_class in inspect.getmembers(
320
- inspect.getmodule(self.related_cls)) if cur_name == outer_class_name][0]
321
-
322
- all_inner_classes = [cur_inner_class for _, cur_inner_class in inspect.getmembers(outer_class, inspect.isclass)]
323
- if self.related_cls in all_inner_classes:
324
- if not issubclass(outer_class, Setup) and not issubclass(outer_class, Scenario):
325
- raise TypeError(
326
- f"the outer class is of the type `{outer_class.__name__}` - this is not allowed")
327
- return outer_class
328
- raise RuntimeError(f"can not find the outer class of this given device `{self.related_cls.__qualname__}`")
310
+ return getattr(self.related_cls, '_outer_balder_class', None)
329
311
 
330
312
  def resolve_connection_device_strings(self):
331
313
  """
@@ -350,9 +332,8 @@ class DeviceController(BaseDeviceController, ABC):
350
332
  return
351
333
 
352
334
  # get outer class of `from_device`
353
- outer_cls_name_from_device = cur_conn.from_device.__qualname__.rpartition('.')[0]
354
- mod = sys.modules[cur_conn.from_device.__module__]
355
- parent_cls_from_device = getattr(mod, outer_cls_name_from_device)
335
+ from_device_controller = DeviceController.get_for(cur_conn.from_device)
336
+ parent_cls_from_device = from_device_controller.get_outer_class()
356
337
 
357
338
  all_inner_classes_of_outer = dict(inspect.getmembers(parent_cls_from_device, inspect.isclass))
358
339
  if cur_conn.to_device in all_inner_classes_of_outer.keys():
@@ -395,8 +395,8 @@ class FeatureController(Controller):
395
395
  if not issubclass(cur_class, VDevice):
396
396
  # filter all classes and make sure that only the child classes of :class:`VDevice` remain
397
397
  continue
398
- outer_class_name, _ = cur_class.__qualname__.split('.')[-2:]
399
- if outer_class_name != self.related_cls.__name__:
398
+
399
+ if VDeviceController.get_for(cur_class).get_outer_class() != self.related_cls:
400
400
  # filter all classes that do not match the setup name in __qualname__
401
401
  continue
402
402
  # otherwise, add this candidate
@@ -72,8 +72,7 @@ class NormalScenarioSetupController(Controller, ABC):
72
72
  if not issubclass(cur_class, Device):
73
73
  # filter all classes and make sure that only the child classes of :meth:`Device` remain
74
74
  continue
75
- outer_class_name, _ = cur_class.__qualname__.split('.')
76
- if outer_class_name != self.related_cls.__name__:
75
+ if DeviceController.get_for(cur_class).get_outer_class() != self.related_cls:
77
76
  # filter all classes that do not match the setup name in __qualname__
78
77
  continue
79
78
  # otherwise, add this candidate
@@ -14,6 +14,7 @@ from _balder.controllers.normal_scenario_setup_controller import NormalScenarioS
14
14
  from _balder.parametrization import FeatureAccessSelector, Parameter
15
15
  from _balder.exceptions import UnclearAssignableFeatureConnectionError, ConnectionIntersectionError, \
16
16
  MultiInheritanceError
17
+ from _balder.utils.functions import get_scenario_inheritance_list_of
17
18
 
18
19
  logger = logging.getLogger(__file__)
19
20
 
@@ -36,6 +37,9 @@ class ScenarioController(NormalScenarioSetupController):
36
37
  # describes if the current controller is for setups or for scenarios (has to be set in child controller)
37
38
  self._related_type = Scenario
38
39
 
40
+ # holds covered-by configuration
41
+ self._covered_by = {}
42
+
39
43
  # this helps to make this constructor only possible inside the controller object
40
44
  if _priv_instantiate_key != ScenarioController.__priv_instantiate_key:
41
45
  raise RuntimeError('it is not allowed to instantiate a controller manually -> use the static method '
@@ -126,6 +130,73 @@ class ScenarioController(NormalScenarioSetupController):
126
130
  ordered_dict[cur_arg] = params[cur_arg]
127
131
  return ordered_dict
128
132
 
133
+ def register_covered_by_for(self, meth: Union[str, None], covered_by: Union[Scenario, Callable, None]) -> None:
134
+ """
135
+ This method registers a covered-by statement for this Scenario. If `meth` is provided, the statement is for the
136
+ specific test method of the scenario, otherwise it is for the whole setup. The item provided in `covered_by`
137
+ describes the test object that covers this scenario (method).
138
+
139
+ :param meth: if provided this attribute describes the test method that should be registered, otherwise the whole
140
+ scenario will be registered
141
+ :param covered_by: describes the test object that covers this scenario (method)
142
+ """
143
+ if not (meth is None or isinstance(meth, str)):
144
+ raise TypeError('meth needs to be None or a string')
145
+ if meth is not None:
146
+ if not meth.startswith('test_'):
147
+ raise TypeError(
148
+ f"the use of the `@covered_by` decorator is only allowed for `Scenario` objects and test methods "
149
+ f"of `Scenario` objects - the method `{self.related_cls.__name__}.{meth}` does not start with "
150
+ f"`test_` and is not a valid test method")
151
+ if not hasattr(self.related_cls, meth):
152
+ raise ValueError(
153
+ f"the provided test method `{meth}` does not exist in scenario `{self.related_cls.__name__}`"
154
+ )
155
+
156
+ if meth not in self._covered_by.keys():
157
+ self._covered_by[meth] = []
158
+ if covered_by is None:
159
+ # reset it
160
+ # todo what if there are more than one decorator in one class
161
+ del self._covered_by[meth]
162
+ else:
163
+ self._covered_by[meth].append(covered_by)
164
+
165
+ def get_raw_covered_by_dict(self) -> Dict[Union[str, None], List[Union[Scenario, Callable]]]:
166
+ """
167
+ :return: returns the internal covered-by dictionary
168
+ """
169
+ return self._covered_by.copy()
170
+
171
+ def get_abs_covered_by_dict(self) -> Dict[Union[str, None], List[Union[Scenario, Callable]]]:
172
+ """
173
+ This method resolves the covered-by statements over all inheritance levels. It automatically
174
+ cleans up every inheritance of the covered_by decorators for every parent class of this scenario.
175
+ """
176
+ parent_classes = [p for p in self.related_cls.__bases__ if issubclass(p, Scenario) and p != Scenario]
177
+ if len(parent_classes) > 1:
178
+ raise MultiInheritanceError(
179
+ f'can not resolve classes for `{self.related_cls}` because there are more than one Scenario based '
180
+ f'parent classes'
181
+ )
182
+ # no more parent classes -> raw is absolute
183
+ if len(parent_classes) == 0:
184
+ return self.get_raw_covered_by_dict()
185
+ parent_controller = self.__class__.get_for(parent_classes[0])
186
+ self_raw_covered_by_dict = self.get_raw_covered_by_dict()
187
+
188
+ #: first fill result with data from parent controller
189
+ result = {
190
+ k if k is None else getattr(self.related_cls, k.__name__): v
191
+ for k, v in parent_controller.get_abs_covered_by_dict().items()
192
+ }
193
+ for cur_callable, cur_coveredby in self_raw_covered_by_dict.items():
194
+ if cur_callable in result.keys():
195
+ result[cur_callable].extend(cur_coveredby)
196
+ else:
197
+ result[cur_callable] = cur_coveredby
198
+ return result
199
+
129
200
  def check_for_parameter_loop_in_dynamic_parametrization(self, cur_fn: Callable):
130
201
  """
131
202
  This method checks for a parameter loop in all dynamic parametrization for a specific test method. If it detects
@@ -196,6 +267,56 @@ class ScenarioController(NormalScenarioSetupController):
196
267
 
197
268
  return all_relevant_func
198
269
 
270
+ def get_ignore_test_methods(self) -> List[callable]:
271
+ """
272
+ This method returns a list of all methods that have the IGNORE marker. It automatically resolves marker that
273
+ were provided on parent class scenarios.
274
+ """
275
+ result = []
276
+ next_parent_class = get_scenario_inheritance_list_of(self.related_cls)[1]
277
+ if next_parent_class != Scenario:
278
+ next_parent_class_controller = ScenarioController.get_for(next_parent_class)
279
+ next_parent_ignore_meths = next_parent_class_controller.get_ignore_test_methods()
280
+ result.extend(next_parent_ignore_meths)
281
+ for cur_ignore_meth_as_str in self.related_cls.IGNORE:
282
+ cur_ignore_meth = getattr(self.related_cls, cur_ignore_meth_as_str)
283
+ result.append(cur_ignore_meth)
284
+ return list(set(result))
285
+
286
+ def get_skip_test_methods(self) -> List[callable]:
287
+ """
288
+ This method returns a list of all methods that have the SKIP marker. It automatically resolves marker that were
289
+ provided on parent class scenarios.
290
+ """
291
+ result = []
292
+ next_parent_class = get_scenario_inheritance_list_of(self.related_cls)[1]
293
+ next_parent_ignore_meths = []
294
+
295
+ if next_parent_class != Scenario:
296
+ next_parent_class_controller = ScenarioController.get_for(next_parent_class)
297
+ next_parent_ignore_meths = next_parent_class_controller.get_ignore_test_methods()
298
+ next_parent_skip_meths = next_parent_class_controller.get_skip_test_methods()
299
+ result.extend(next_parent_skip_meths)
300
+
301
+ for cur_skip_meth_as_str in self.related_cls.SKIP:
302
+ cur_skip_meth = getattr(self.related_cls, cur_skip_meth_as_str)
303
+ if cur_skip_meth in next_parent_ignore_meths:
304
+ raise ValueError(f'found skip method `{cur_skip_meth}` defined in `{self.related_cls}.SKIP`, but was '
305
+ f'already added to IGNORE in parent class')
306
+ result.append(cur_skip_meth)
307
+
308
+ return list(set(result))
309
+
310
+ def get_run_test_methods(self) -> List[callable]:
311
+ """
312
+ This method returns a list of all methods that should run in this scenario. It automatically resolves
313
+ SKIP/IGNORE marker that were provided on parent class scenarios.
314
+ """
315
+ result = (set(self.get_all_test_methods())
316
+ - set(self.get_skip_test_methods())
317
+ - set(self.get_ignore_test_methods()))
318
+ return list(result)
319
+
199
320
  def validate_feature_clearance_for_parallel_connections(self):
200
321
  """
201
322
  This method validates for every active class-based feature (only the ones that have an active VDevice<->Device
@@ -2,11 +2,10 @@ from __future__ import annotations
2
2
  from typing import Type, Dict, Union
3
3
 
4
4
  import logging
5
- import inspect
6
5
  from _balder.vdevice import VDevice
7
6
  from _balder.feature import Feature
8
7
  from _balder.controllers.base_device_controller import BaseDeviceController
9
- from _balder.exceptions import DeviceScopeError, VDeviceResolvingError
8
+ from _balder.exceptions import VDeviceResolvingError
10
9
 
11
10
  logger = logging.getLogger(__file__)
12
11
 
@@ -69,27 +68,7 @@ class VDeviceController(BaseDeviceController):
69
68
  """
70
69
  This method delivers the outer class of this device. In Balder, this has to be a :class:`Feature`.
71
70
  """
72
-
73
- if self.related_cls.__qualname__.count('.') == 0:
74
- raise DeviceScopeError("the current device class is no inner class")
75
- all_splits = self.related_cls.__qualname__.split('.')
76
-
77
- outer_class = [cur_class for cur_name, cur_class in inspect.getmembers(
78
- inspect.getmodule(self.related_cls), inspect.isclass) if cur_name == all_splits[0]][0]
79
- # only get the next outer class (not higher)
80
- for cur_class_name in all_splits[1:-1]:
81
- outer_class = getattr(outer_class, cur_class_name)
82
- # instantiate it to get the real type (not the decorator class of `@for_vdevice`)
83
- # we can instantiate it, because we do nothing in it
84
- outer_class = outer_class().__class__
85
-
86
- all_inner_classes = [cur_inner_class for _, cur_inner_class in inspect.getmembers(outer_class, inspect.isclass)]
87
- if self.related_cls in all_inner_classes:
88
- if not issubclass(outer_class, Feature):
89
- raise TypeError(f"the found outer class is of type `{outer_class.__name__}` - this is a type which is "
90
- f"not allowed (outer class has to be a `Feature` class)")
91
- return outer_class
92
- raise RuntimeError(f"can not find the outer class of this vDevice `{self.related_cls.__qualname__}`")
71
+ return getattr(self.related_cls, '_outer_balder_class', None)
93
72
 
94
73
  def get_next_parent_vdevice(self) -> Union[Type[VDevice], None]:
95
74
  """
@@ -69,10 +69,10 @@ def connect(
69
69
  nonlocal self_node_name
70
70
  nonlocal dest_node_name
71
71
 
72
- this_outer_class_name, _ = decorated_cls.__qualname__.split('.')
72
+ this_outer_ref = decorated_cls.__qualname__.split('.')[:-1]
73
73
  if not isinstance(with_device, str):
74
- other_outer_class_name, _ = with_device.__qualname__.split('.')
75
- if this_outer_class_name != other_outer_class_name:
74
+ other_outer_ref = with_device.__qualname__.split('.')[:-1]
75
+ if this_outer_ref != other_outer_ref:
76
76
  raise ValueError(
77
77
  f"the given device is not mentioned in this setup/scenario - please create a new "
78
78
  f"direct inner device class, it can be inherited from `{with_device.__qualname__}`")
@@ -2,11 +2,12 @@ from __future__ import annotations
2
2
  from typing import Type, Union
3
3
 
4
4
  import inspect
5
+
6
+ from _balder.controllers import ScenarioController
5
7
  from _balder.scenario import Scenario
6
- from _balder.utils import get_class_that_defines_method
7
8
 
8
9
 
9
- def covered_by(item: Union[Type[Scenario], callable, None]):
10
+ def covered_by(item: Union[Type[Scenario], str, callable, None]):
10
11
  """
11
12
  This decorator defines that there exists another Scenario class or test method item that has a similar
12
13
  implementation like the decorated :class:`Scenario` class or the decorated test method.
@@ -16,14 +17,15 @@ def covered_by(item: Union[Type[Scenario], callable, None]):
16
17
 
17
18
  if item is None:
18
19
  pass
19
- elif callable(item) and inspect.isfunction(item) and item.__name__.startswith("test_") and \
20
- issubclass(get_class_that_defines_method(item), Scenario):
20
+ elif isinstance(item, str) and item.startswith("test_"):
21
21
  pass
22
- elif isinstance(item, type) and issubclass(item, Scenario):
22
+ elif callable(item) and inspect.isfunction(item) and item.__name__.startswith("test_"):
23
23
  pass
24
+ elif isinstance(item, type) and issubclass(item, Scenario):
25
+ raise NotImplementedError('The covered-by other scenario classes is not supported yet')
24
26
  else:
25
- raise TypeError("the given element for `item` must be a `Scenario` (or a subclass thereof) or a test method of "
26
- "a scenario class (has to start with `test_`)")
27
+ raise TypeError("the given element for `item` must be a test method of a scenario class (has to start with "
28
+ "`test_`)")
27
29
 
28
30
  class CoveredByDecorator:
29
31
  """decorator class for `@covered_by` decorator"""
@@ -36,46 +38,36 @@ def covered_by(item: Union[Type[Scenario], callable, None]):
36
38
  raise TypeError(f"The decorator `@covered_by` may only be used for `Scenario` objects or for test "
37
39
  f"methods of one `Scenario` object. This is not possible for the applied class "
38
40
  f"`{func.__name__}`.")
39
- if not hasattr(func, '_covered_by'):
40
- func._covered_by = {}
41
- if func not in func._covered_by.keys():
42
- func._covered_by[func] = []
43
- if item is None:
44
- # reset it
45
- func._covered_by[func] = []
46
- elif item not in func._covered_by[func]:
47
- func._covered_by[func].append(item)
48
- elif inspect.isfunction(func):
41
+ raise NotImplementedError('The covered-by decoration of other scenario classes is not supported yet')
42
+ # scenario_controller = ScenarioController.get_for(func)
43
+ # register for the whole class
44
+ # scenario_controller.register_covered_by_for(meth=None, covered_by=item)
45
+ if inspect.isfunction(func):
49
46
  # work will done in `__set_name__`
50
47
  pass
51
48
  else:
52
49
  raise TypeError(f"The use of the `@covered_by` decorator is not allowed for the `{str(func)}` element. "
53
- f"You should only use this decorator for `Scenario` elements or test method elements "
54
- f"of a `Scenario` object")
50
+ f"You should only use this decorator for test method elements of a `Scenario` object")
55
51
 
56
52
  def __set_name__(self, owner, name):
57
53
  if issubclass(owner, Scenario):
58
54
  if not inspect.isfunction(self.func):
59
- raise TypeError("the use of the `@covered_by` decorator is only allowed for `Scenario` objects and "
60
- "test methods of `Scenario` objects")
55
+ raise TypeError("the use of the `@covered_by` decorator is only allowed for test methods of "
56
+ "`Scenario` objects")
61
57
  if not name.startswith('test_'):
62
- raise TypeError(f"the use of the `@covered_by` decorator is only allowed for `Scenario` objects "
63
- f"and test methods of `Scenario` objects - the method `{owner.__name__}.{name}` "
64
- f"does not start with `test_` and is not a valid test method")
65
-
66
- if not hasattr(owner, '_covered_by'):
67
- owner._covered_by = {}
68
- if self.func not in owner._covered_by.keys():
69
- owner._covered_by[self.func] = []
70
- if item is None:
71
- # reset it
72
- owner._covered_by[self.func] = []
73
- elif item not in owner._covered_by[self.func]:
74
- owner._covered_by[self.func].append(item)
58
+ raise TypeError(f"the use of the `@covered_by` decorator is only allowed for test methods of "
59
+ f"`Scenario` objects - the method `{owner.__name__}.{name}` does not start with "
60
+ f"`test_` and is not a valid test method")
61
+ # if item is a string - resolve method
62
+ cleared_item = item
63
+ if isinstance(item, str):
64
+ cleared_item = getattr(owner, item)
65
+ scenario_controller = ScenarioController.get_for(owner)
66
+ scenario_controller.register_covered_by_for(meth=name, covered_by=cleared_item)
75
67
  else:
76
68
  raise TypeError(f"The use of the `@covered_by` decorator is not allowed for methods of a "
77
- f"`{owner.__name__}`. You should only use this decorator for `Scenario` objects or "
78
- f"valid test methods of a `Scenario` object")
69
+ f"`{owner.__name__}`. You should only use this decorator for valid test methods of a "
70
+ f"`Scenario` object")
79
71
 
80
72
  setattr(owner, name, self.func)
81
73
 
@@ -9,7 +9,7 @@ import traceback
9
9
  from abc import ABC, abstractmethod
10
10
 
11
11
  from _balder.executor.basic_executor import BasicExecutor
12
- from _balder.testresult import ResultState, TestcaseResult
12
+ from _balder.testresult import ResultState
13
13
 
14
14
  if TYPE_CHECKING:
15
15
  from _balder.fixture_execution_level import FixtureExecutionLevel
@@ -73,9 +73,11 @@ class BasicExecutableExecutor(BasicExecutor, ABC):
73
73
  """
74
74
  if value not in (ResultState.SKIP, ResultState.COVERED_BY, ResultState.NOT_RUN):
75
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)
76
+ if self.all_child_executors is None:
77
+ self.body_result.set_result(result=value, exception=None)
78
+ else:
79
+ for cur_child_executor in self.all_child_executors:
80
+ cur_child_executor.set_result_for_whole_branch(value)
79
81
 
80
82
  @abstractmethod
81
83
  def cleanup_empty_executor_branches(self, consider_discarded=False):
@@ -90,8 +92,9 @@ class BasicExecutableExecutor(BasicExecutor, ABC):
90
92
  """
91
93
  This method calls all user defined filters that are to be applied to the executor tree.
92
94
  """
93
- for cur_child_executor in self.all_child_executors:
94
- cur_child_executor.filter_tree_for_user_filters()
95
+ if self.all_child_executors:
96
+ for cur_child_executor in self.all_child_executors:
97
+ cur_child_executor.filter_tree_for_user_filters()
95
98
 
96
99
  def execute(self, show_discarded=False):
97
100
  """
@@ -105,6 +108,8 @@ class BasicExecutableExecutor(BasicExecutor, ABC):
105
108
  if self.has_runnable_tests():
106
109
  self.fixture_manager.enter(self)
107
110
  self.construct_result.set_result(ResultState.SUCCESS)
111
+ else:
112
+ self.construct_result.set_result(ResultState.NOT_RUN)
108
113
 
109
114
  self._body_execution(show_discarded=show_discarded)
110
115
  except Exception as exc: # pylint: disable=broad-exception-caught
@@ -116,6 +121,8 @@ class BasicExecutableExecutor(BasicExecutor, ABC):
116
121
  if self.fixture_manager.is_allowed_to_leave(self):
117
122
  self.fixture_manager.leave(self)
118
123
  self.teardown_result.set_result(ResultState.SUCCESS)
124
+ else:
125
+ self.teardown_result.set_result(ResultState.NOT_RUN)
119
126
  except Exception as exc: # pylint: disable=broad-exception-caught
120
127
  # this has to be a teardown fixture error
121
128
  traceback.print_exception(*sys.exc_info())
@@ -38,7 +38,7 @@ class BasicExecutor(ABC):
38
38
 
39
39
  @property
40
40
  @abstractmethod
41
- def all_child_executors(self) -> List[BasicExecutor]:
41
+ def all_child_executors(self) -> list[BasicExecutor] | None:
42
42
  """
43
43
  returns all child executors of this object or None if no child executors can exist (this element is a leaf)
44
44
  """
@@ -115,6 +115,32 @@ class BasicExecutor(ABC):
115
115
  return False
116
116
  return True
117
117
 
118
+ def has_skipped_tests(self) -> bool:
119
+ """
120
+ This method returns true if this executor element has at least one test that is marked to be skipped. The method
121
+ returns true if minimum one of its children has `prev_mark=SKIP`.
122
+ """
123
+ if self.all_child_executors is not None:
124
+ # the executor has child executors -> check them
125
+ for cur_child in self.all_child_executors:
126
+ if cur_child.has_skipped_tests():
127
+ return True
128
+ return False
129
+ return False
130
+
131
+ def has_covered_by_tests(self) -> bool:
132
+ """
133
+ This method returns true if this executor element has at least one test that is marked as covered-by. The method
134
+ returns true if minimum one of its children has `prev_mark=COVERED_BY`.
135
+ """
136
+ if self.all_child_executors is not None:
137
+ # the executor has child executors -> check them
138
+ for cur_child in self.all_child_executors:
139
+ if cur_child.has_covered_by_tests():
140
+ return True
141
+ return False
142
+ return False
143
+
118
144
  def get_all_base_instances_of_this_branch(
119
145
  self, with_type: Union[Type[Setup], Type[Scenario], Type[types.FunctionType]],
120
146
  only_runnable_elements: bool = True) -> List[Union[Setup, Scenario, object]]:
@@ -159,8 +185,9 @@ class BasicExecutor(ABC):
159
185
  """
160
186
  This method calls all user defined filters that are to be applied to the executor tree.
161
187
  """
162
- for cur_child_executor in self.all_child_executors:
163
- cur_child_executor.filter_tree_for_user_filters()
188
+ if self.all_child_executors:
189
+ for cur_child_executor in self.all_child_executors:
190
+ cur_child_executor.filter_tree_for_user_filters()
164
191
 
165
192
  def testsummary(self) -> ResultSummary:
166
193
  """
@@ -172,7 +199,7 @@ class BasicExecutor(ABC):
172
199
 
173
200
  if isinstance(self.body_result, TestcaseResult):
174
201
  setattr(summary, self.executor_result.value, 1)
175
- else:
202
+ elif self.all_child_executors:
176
203
  for cur_child_exec in self.all_child_executors:
177
204
  summary += cur_child_exec.testsummary()
178
205
  return summary
@@ -38,7 +38,7 @@ class ExecutorTree(BasicExecutableExecutor):
38
38
  # ---------------------------------- PROPERTIES --------------------------------------------------------------------
39
39
 
40
40
  @property
41
- def all_child_executors(self) -> List[BasicExecutableExecutor]:
41
+ def all_child_executors(self) -> List[BasicExecutableExecutor] | None:
42
42
  return self._setup_executors
43
43
 
44
44
  @property
@@ -63,11 +63,12 @@ class ExecutorTree(BasicExecutableExecutor):
63
63
 
64
64
  def _body_execution(self, show_discarded):
65
65
  for cur_setup_executor in self.get_setup_executors():
66
- if cur_setup_executor.has_runnable_tests(consider_discarded_too=show_discarded):
66
+ prev_mark = cur_setup_executor.prev_mark
67
+ if cur_setup_executor.has_runnable_tests(show_discarded) or cur_setup_executor.has_skipped_tests():
67
68
  cur_setup_executor.execute(show_discarded=show_discarded)
68
- elif cur_setup_executor.prev_mark == PreviousExecutorMark.SKIP:
69
+ elif prev_mark == PreviousExecutorMark.SKIP:
69
70
  cur_setup_executor.set_result_for_whole_branch(ResultState.SKIP)
70
- elif cur_setup_executor.prev_mark == PreviousExecutorMark.COVERED_BY:
71
+ elif prev_mark == PreviousExecutorMark.COVERED_BY:
71
72
  cur_setup_executor.set_result_for_whole_branch(ResultState.COVERED_BY)
72
73
  else:
73
74
  cur_setup_executor.set_result_for_whole_branch(ResultState.NOT_RUN)
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
  from typing import Any, TYPE_CHECKING
3
3
 
4
4
  from collections import OrderedDict
5
- from _balder.utils import inspect_method
5
+ from _balder.utils.functions import get_method_type
6
6
  from .testcase_executor import TestcaseExecutor
7
7
 
8
8
  if TYPE_CHECKING:
@@ -39,7 +39,7 @@ class ParametrizedTestcaseExecutor(TestcaseExecutor):
39
39
  return super().full_test_name_str
40
40
 
41
41
  def get_all_test_method_args(self):
42
- _, func_type = inspect_method(self.base_testcase_callable)
42
+ func_type = get_method_type(self.base_testcase_obj.__class__, self.base_testcase_callable)
43
43
  all_kwargs = self.fixture_manager.get_all_attribute_values(
44
44
  self,
45
45
  self.base_testcase_obj.__class__,