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.
- _balder/_version.py +9 -4
- _balder/cnnrelations/and_connection_relation.py +28 -1
- _balder/cnnrelations/base_connection_relation.py +1 -1
- _balder/collector.py +130 -119
- _balder/connection.py +76 -80
- _balder/controllers/device_controller.py +5 -24
- _balder/controllers/feature_controller.py +2 -2
- _balder/controllers/normal_scenario_setup_controller.py +1 -2
- _balder/controllers/scenario_controller.py +121 -0
- _balder/controllers/vdevice_controller.py +2 -23
- _balder/decorator_connect.py +3 -3
- _balder/decorator_covered_by.py +28 -36
- _balder/executor/basic_executable_executor.py +13 -6
- _balder/executor/basic_executor.py +31 -4
- _balder/executor/executor_tree.py +5 -4
- _balder/executor/parametrized_testcase_executor.py +2 -2
- _balder/executor/scenario_executor.py +12 -71
- _balder/executor/setup_executor.py +4 -3
- _balder/executor/testcase_executor.py +35 -19
- _balder/executor/unresolved_parametrized_testcase_executor.py +32 -57
- _balder/executor/variation_executor.py +21 -48
- _balder/feature.py +2 -1
- _balder/feature_replacement_mapping.py +39 -1
- _balder/fixture_manager.py +1 -1
- _balder/fixture_metadata.py +1 -1
- _balder/routing_path.py +18 -10
- _balder/scenario.py +2 -2
- _balder/setup.py +2 -1
- _balder/testresult.py +4 -3
- _balder/utils/__init__.py +0 -0
- _balder/{utils.py → utils/functions.py} +29 -31
- _balder/utils/inner_device_managing_metaclass.py +14 -0
- _balder/utils/mixin_can_be_covered_by_executor.py +24 -0
- _balder/utils/typings.py +4 -0
- {baldertest-0.1.0b12.dist-info → baldertest-0.1.0b14.dist-info}/METADATA +3 -2
- {baldertest-0.1.0b12.dist-info → baldertest-0.1.0b14.dist-info}/RECORD +40 -36
- {baldertest-0.1.0b12.dist-info → baldertest-0.1.0b14.dist-info}/WHEEL +1 -1
- {baldertest-0.1.0b12.dist-info → baldertest-0.1.0b14.dist-info}/entry_points.txt +0 -0
- {baldertest-0.1.0b12.dist-info → baldertest-0.1.0b14.dist-info/licenses}/LICENSE +0 -0
- {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
|
|
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
|
-
|
|
354
|
-
|
|
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
|
-
|
|
399
|
-
if
|
|
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
|
-
|
|
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
|
|
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
|
"""
|
_balder/decorator_connect.py
CHANGED
|
@@ -69,10 +69,10 @@ def connect(
|
|
|
69
69
|
nonlocal self_node_name
|
|
70
70
|
nonlocal dest_node_name
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
this_outer_ref = decorated_cls.__qualname__.split('.')[:-1]
|
|
73
73
|
if not isinstance(with_device, str):
|
|
74
|
-
|
|
75
|
-
if
|
|
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__}`")
|
_balder/decorator_covered_by.py
CHANGED
|
@@ -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
|
|
20
|
-
issubclass(get_class_that_defines_method(item), Scenario):
|
|
20
|
+
elif isinstance(item, str) and item.startswith("test_"):
|
|
21
21
|
pass
|
|
22
|
-
elif
|
|
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
|
|
26
|
-
"
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
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
|
|
60
|
-
"
|
|
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
|
|
63
|
-
f"
|
|
64
|
-
f"
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
|
78
|
-
f"
|
|
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
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
94
|
-
cur_child_executor.
|
|
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) ->
|
|
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
|
-
|
|
163
|
-
cur_child_executor.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
69
|
+
elif prev_mark == PreviousExecutorMark.SKIP:
|
|
69
70
|
cur_setup_executor.set_result_for_whole_branch(ResultState.SKIP)
|
|
70
|
-
elif
|
|
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
|
|
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
|
-
|
|
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__,
|