baldertest 0.1.0__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/__init__.py +12 -0
- _balder/_version.py +34 -0
- _balder/balder_plugin.py +73 -0
- _balder/balder_session.py +341 -0
- _balder/balder_settings.py +15 -0
- _balder/cnnrelations/__init__.py +7 -0
- _balder/cnnrelations/and_connection_relation.py +176 -0
- _balder/cnnrelations/base_connection_relation.py +270 -0
- _balder/cnnrelations/or_connection_relation.py +65 -0
- _balder/collector.py +874 -0
- _balder/connection.py +863 -0
- _balder/connection_metadata.py +255 -0
- _balder/console/__init__.py +0 -0
- _balder/console/balder.py +58 -0
- _balder/controllers/__init__.py +12 -0
- _balder/controllers/base_device_controller.py +72 -0
- _balder/controllers/controller.py +29 -0
- _balder/controllers/device_controller.py +446 -0
- _balder/controllers/feature_controller.py +715 -0
- _balder/controllers/normal_scenario_setup_controller.py +402 -0
- _balder/controllers/scenario_controller.py +524 -0
- _balder/controllers/setup_controller.py +134 -0
- _balder/controllers/vdevice_controller.py +95 -0
- _balder/decorator_connect.py +104 -0
- _balder/decorator_covered_by.py +74 -0
- _balder/decorator_fixture.py +29 -0
- _balder/decorator_for_vdevice.py +118 -0
- _balder/decorator_gateway.py +34 -0
- _balder/decorator_insert_into_tree.py +52 -0
- _balder/decorator_parametrize.py +31 -0
- _balder/decorator_parametrize_by_feature.py +36 -0
- _balder/device.py +18 -0
- _balder/exceptions.py +182 -0
- _balder/executor/__init__.py +0 -0
- _balder/executor/basic_executable_executor.py +133 -0
- _balder/executor/basic_executor.py +205 -0
- _balder/executor/executor_tree.py +217 -0
- _balder/executor/parametrized_testcase_executor.py +52 -0
- _balder/executor/scenario_executor.py +169 -0
- _balder/executor/setup_executor.py +163 -0
- _balder/executor/testcase_executor.py +203 -0
- _balder/executor/unresolved_parametrized_testcase_executor.py +184 -0
- _balder/executor/variation_executor.py +882 -0
- _balder/exit_code.py +19 -0
- _balder/feature.py +74 -0
- _balder/feature_replacement_mapping.py +107 -0
- _balder/feature_vdevice_mapping.py +88 -0
- _balder/fixture_definition_scope.py +19 -0
- _balder/fixture_execution_level.py +22 -0
- _balder/fixture_manager.py +483 -0
- _balder/fixture_metadata.py +26 -0
- _balder/node_gateway.py +103 -0
- _balder/objects/__init__.py +0 -0
- _balder/objects/connections/__init__.py +0 -0
- _balder/objects/connections/osi_1_physical.py +116 -0
- _balder/objects/connections/osi_2_datalink.py +35 -0
- _balder/objects/connections/osi_3_network.py +47 -0
- _balder/objects/connections/osi_4_transport.py +40 -0
- _balder/objects/connections/osi_5_session.py +13 -0
- _balder/objects/connections/osi_6_presentation.py +13 -0
- _balder/objects/connections/osi_7_application.py +83 -0
- _balder/objects/devices/__init__.py +0 -0
- _balder/objects/devices/this_device.py +12 -0
- _balder/parametrization.py +75 -0
- _balder/plugin_manager.py +138 -0
- _balder/previous_executor_mark.py +23 -0
- _balder/routing_path.py +335 -0
- _balder/scenario.py +20 -0
- _balder/setup.py +18 -0
- _balder/solver.py +246 -0
- _balder/testresult.py +163 -0
- _balder/unmapped_vdevice.py +13 -0
- _balder/utils/__init__.py +0 -0
- _balder/utils/functions.py +103 -0
- _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
- _balder/vdevice.py +9 -0
- balder/__init__.py +56 -0
- balder/connections.py +43 -0
- balder/devices.py +9 -0
- balder/exceptions.py +44 -0
- balder/parametrization.py +8 -0
- baldertest-0.1.0.dist-info/METADATA +356 -0
- baldertest-0.1.0.dist-info/RECORD +89 -0
- baldertest-0.1.0.dist-info/WHEEL +5 -0
- baldertest-0.1.0.dist-info/entry_points.txt +2 -0
- baldertest-0.1.0.dist-info/licenses/LICENSE +21 -0
- baldertest-0.1.0.dist-info/top_level.txt +2 -0
_balder/__init__.py
ADDED
_balder/_version.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# file generated by setuptools-scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"__version__",
|
|
6
|
+
"__version_tuple__",
|
|
7
|
+
"version",
|
|
8
|
+
"version_tuple",
|
|
9
|
+
"__commit_id__",
|
|
10
|
+
"commit_id",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
TYPE_CHECKING = False
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from typing import Tuple
|
|
16
|
+
from typing import Union
|
|
17
|
+
|
|
18
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
+
COMMIT_ID = Union[str, None]
|
|
20
|
+
else:
|
|
21
|
+
VERSION_TUPLE = object
|
|
22
|
+
COMMIT_ID = object
|
|
23
|
+
|
|
24
|
+
version: str
|
|
25
|
+
__version__: str
|
|
26
|
+
__version_tuple__: VERSION_TUPLE
|
|
27
|
+
version_tuple: VERSION_TUPLE
|
|
28
|
+
commit_id: COMMIT_ID
|
|
29
|
+
__commit_id__: COMMIT_ID
|
|
30
|
+
|
|
31
|
+
__version__ = version = '0.1.0'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 1, 0)
|
|
33
|
+
|
|
34
|
+
__commit_id__ = commit_id = None
|
_balder/balder_plugin.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import TYPE_CHECKING, Type, Tuple, List, Union
|
|
3
|
+
|
|
4
|
+
import pathlib
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from _balder.executor.executor_tree import ExecutorTree
|
|
8
|
+
from _balder.balder_session import BalderSession
|
|
9
|
+
from _balder.setup import Setup
|
|
10
|
+
from _balder.scenario import Scenario
|
|
11
|
+
import argparse
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BalderPlugin:
|
|
15
|
+
"""
|
|
16
|
+
This is the balder plugin class. You can create your own plugin, by creating a subclass of it. With that you are
|
|
17
|
+
able to overwrite the methods you want to use in your plugin.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, session: BalderSession):
|
|
21
|
+
self.balder_session = session
|
|
22
|
+
|
|
23
|
+
def addoption(self, argument_parser: argparse.ArgumentParser):
|
|
24
|
+
"""
|
|
25
|
+
The callback will be executed while the `ArgumentParser` is being created.
|
|
26
|
+
|
|
27
|
+
:param argument_parser: the argument parser object
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def modify_collected_pyfiles(self, pyfiles: List[pathlib.Path]) -> List[pathlib.Path]:
|
|
31
|
+
"""
|
|
32
|
+
This callback will be executed after the :class:`Collector` has collected all python files that are inside the
|
|
33
|
+
current working directory.
|
|
34
|
+
|
|
35
|
+
.. note::
|
|
36
|
+
Note that these files are not filtered yet. The list will contain every existing python file.
|
|
37
|
+
|
|
38
|
+
:param pyfiles: a list with all python filepaths
|
|
39
|
+
|
|
40
|
+
:return: the new list of all filepaths
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def modify_collected_classes(self, scenarios: List[Type[Scenario]], setups: List[Type[Setup]]) \
|
|
44
|
+
-> Tuple[List[Type[Scenario]], List[Type[Setup]]]:
|
|
45
|
+
"""
|
|
46
|
+
This callback will be executed after the :class:`Collector` has collected the :class:`Scenario` classes and the
|
|
47
|
+
:class:`Setup` classes.
|
|
48
|
+
|
|
49
|
+
:param scenarios: all collected :class:`Scenario` classes that are currently in the collected list
|
|
50
|
+
|
|
51
|
+
:param setups: all collected :class:`Setup` classes that are currently in the collected list
|
|
52
|
+
|
|
53
|
+
:return: a tuple of lists, where the first list is the new list with all :class:`Scenario` classes, the second
|
|
54
|
+
element is a list with all :class:`Setup` classes
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def filter_executor_tree(self, executor_tree: ExecutorTree) -> None:
|
|
58
|
+
"""
|
|
59
|
+
This callback will be executed before the ExecutorTree runs. It contains the current representation
|
|
60
|
+
of the :class:`ExecutorTree`, that will be executed in the next step. With this callback it is possible to
|
|
61
|
+
manipulate the :class:`ExecutorTree`. You have not to return something, the given ``executor_tree`` is a
|
|
62
|
+
reference.
|
|
63
|
+
|
|
64
|
+
:param executor_tree: the reference to the main :class:`ExecutorTree` object balder uses for this session
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def session_finished(self, executor_tree: Union[ExecutorTree, None]):
|
|
68
|
+
"""
|
|
69
|
+
This callback will be executed at the end of every session. The callback will run in a `collect-only` and
|
|
70
|
+
`resolve-only` session too. Note, that the `executor_tree` argument is None in a `collect-only` session.
|
|
71
|
+
|
|
72
|
+
:param executor_tree: the reference to the main :class:`ExecutorTree` object that balder uses for this session
|
|
73
|
+
"""
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Union, List, Tuple, Dict, Type, TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import inspect
|
|
7
|
+
import pathlib
|
|
8
|
+
import argparse
|
|
9
|
+
import balder
|
|
10
|
+
from _balder.balder_plugin import BalderPlugin
|
|
11
|
+
from _balder.plugin_manager import PluginManager
|
|
12
|
+
from _balder.executor.executor_tree import ExecutorTree
|
|
13
|
+
from _balder.collector import Collector
|
|
14
|
+
from _balder.solver import Solver
|
|
15
|
+
from _balder.exceptions import DuplicateBalderSettingError
|
|
16
|
+
from _balder.balder_settings import BalderSettings
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from _balder.setup import Setup
|
|
20
|
+
from _balder.device import Device
|
|
21
|
+
from _balder.scenario import Scenario
|
|
22
|
+
from _balder.connection import Connection
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class BalderSession:
|
|
26
|
+
"""
|
|
27
|
+
This is the main balder executable object. It contains all information about the current session and executes the
|
|
28
|
+
different steps.
|
|
29
|
+
|
|
30
|
+
This object contains all command line arguments that can be used, while calling `balder ..`. All settings that
|
|
31
|
+
are given in `balderglob.py` will be imported in `self.baldersetting`
|
|
32
|
+
"""
|
|
33
|
+
# this is the default value (will be overwritten in this constructor if necessary)
|
|
34
|
+
baldersettings: BalderSettings = BalderSettings()
|
|
35
|
+
|
|
36
|
+
def __init__(self, cmd_args: Union[List[str], None] = None, working_dir: Union[pathlib.Path, None] = None):
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
:param cmd_args: optional the command line list of strings that should be parsed instead of parsing the real
|
|
40
|
+
command line arguments (used for testing)
|
|
41
|
+
|
|
42
|
+
:param working_dir: the working directory that should be used instead of the given value in `cmd_arg_str` or
|
|
43
|
+
the current directory (determined by `os.getcwd()`)
|
|
44
|
+
"""
|
|
45
|
+
#: contains the alternative command line arguments as a string list (has to be given, if the object should
|
|
46
|
+
#: not use the console params of this call)
|
|
47
|
+
self._alt_cmd_args = cmd_args
|
|
48
|
+
#: contains a reference to the ArgumentParser that parses the command line string
|
|
49
|
+
self.cmd_arg_parser = None
|
|
50
|
+
#: contains the parsed object
|
|
51
|
+
self.parsed_args: argparse.Namespace
|
|
52
|
+
|
|
53
|
+
##
|
|
54
|
+
# all general settings that can be modified by command line arguments
|
|
55
|
+
##
|
|
56
|
+
|
|
57
|
+
#: the working directory for this balder session (default: current directory from `os.getcwd()`)
|
|
58
|
+
self.working_dir: Union[pathlib.Path, None] = pathlib.Path(os.getcwd())
|
|
59
|
+
#: specifies that the tests should only be collected but not be resolved and executed
|
|
60
|
+
self.collect_only: Union[bool, None] = None
|
|
61
|
+
#: specifies that the tests should only be collected and resolved but not executed
|
|
62
|
+
self.resolve_only: Union[bool, None] = None
|
|
63
|
+
#: specifies that all discarded variations should be printed (with information why they were discarded)
|
|
64
|
+
self.show_discarded: Union[bool, None] = None
|
|
65
|
+
#: contains a number of :class:`Setup` class strings that should only be considered for the execution
|
|
66
|
+
self.only_with_setup: Union[List[str], None] = None
|
|
67
|
+
#: contains a number of :class:`Scenario` class strings that should only be considered for the execution
|
|
68
|
+
self.only_with_scenario: Union[List[str], None] = None
|
|
69
|
+
#: if this is true, the test run should include duplicated tests that are declared as covered_by another test
|
|
70
|
+
#: method
|
|
71
|
+
self.force_covered_by_duplicates: Union[bool, None] = None
|
|
72
|
+
|
|
73
|
+
self.preparse_args()
|
|
74
|
+
|
|
75
|
+
#: overwrite working directory if necessary
|
|
76
|
+
if working_dir:
|
|
77
|
+
self.working_dir = working_dir
|
|
78
|
+
|
|
79
|
+
# add the current working variable to sys.path
|
|
80
|
+
sys.path.insert(0, str(self.working_dir.absolute()))
|
|
81
|
+
|
|
82
|
+
##
|
|
83
|
+
# instantiate and initialize all components to completely load the plugins (plugins could access the command
|
|
84
|
+
# line argument parser)
|
|
85
|
+
##
|
|
86
|
+
#: contains the reference to the used PluginManager
|
|
87
|
+
self.plugin_manager = PluginManager()
|
|
88
|
+
#: contains the reference to the used :class:`Collector` class
|
|
89
|
+
self.collector = Collector(self.working_dir)
|
|
90
|
+
|
|
91
|
+
# determine the balder settings
|
|
92
|
+
BalderSession.baldersettings = self.get_baldersettings_from_balderglob()
|
|
93
|
+
BalderSession.baldersettings = BalderSession.baldersettings if BalderSession.baldersettings is not None else \
|
|
94
|
+
BalderSettings()
|
|
95
|
+
|
|
96
|
+
if BalderSession.baldersettings.force_covered_by_duplicates:
|
|
97
|
+
# overwrite console argument only if the value in BalderSettings is true (because cmd line can only
|
|
98
|
+
# overwrite the value False)
|
|
99
|
+
self.force_covered_by_duplicates = True
|
|
100
|
+
|
|
101
|
+
for cur_plugin_cls in self.get_balderplugins_from_balderglob():
|
|
102
|
+
self.plugin_manager.register(cur_plugin_cls, self)
|
|
103
|
+
|
|
104
|
+
##
|
|
105
|
+
# instantiate all sub objects that are relevant for this test session
|
|
106
|
+
##
|
|
107
|
+
self.parse_args()
|
|
108
|
+
|
|
109
|
+
#: overwrite working directory if necessary
|
|
110
|
+
if working_dir:
|
|
111
|
+
self.working_dir = working_dir
|
|
112
|
+
|
|
113
|
+
#: contains the reference to the used :class:`Solver´ class (or none, if there was no solving executed till
|
|
114
|
+
#: now)
|
|
115
|
+
self.solver: Union[Solver, None] = None
|
|
116
|
+
#: contains the reference to the used :class:`ExecutorTree` class (or none, if there was no solving executed
|
|
117
|
+
#: till now)
|
|
118
|
+
self.executor_tree: Union[ExecutorTree, None] = None
|
|
119
|
+
|
|
120
|
+
# ---------------------------------- STATIC METHODS ----------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
@staticmethod
|
|
123
|
+
def get_current_active_global_conntree_name():
|
|
124
|
+
"""
|
|
125
|
+
This method returns the current active global connection tree name, depending on the current active setting in
|
|
126
|
+
:class:`BalderSettings`, that is active for the current run.
|
|
127
|
+
"""
|
|
128
|
+
if BalderSession.baldersettings is None:
|
|
129
|
+
raise ValueError("no baldersettings loaded yet")
|
|
130
|
+
|
|
131
|
+
return BalderSession.baldersettings.used_global_connection_tree
|
|
132
|
+
|
|
133
|
+
# ---------------------------------- CLASS METHODS -----------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
# ---------------------------------- PROPERTIES --------------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def all_collected_pyfiles(self) -> List[pathlib.Path]:
|
|
139
|
+
"""returns all collected pyfiles"""
|
|
140
|
+
try:
|
|
141
|
+
return self.collector.all_pyfiles
|
|
142
|
+
except AttributeError as exc:
|
|
143
|
+
raise RuntimeError("this property is only available after the collecting process was executed") from exc
|
|
144
|
+
|
|
145
|
+
@property
|
|
146
|
+
def all_collected_setups(self) -> List[Type[Setup]]:
|
|
147
|
+
"""returns all collected :class:`Setup` classes"""
|
|
148
|
+
try:
|
|
149
|
+
return self.collector.all_setups
|
|
150
|
+
except AttributeError as exc:
|
|
151
|
+
raise RuntimeError("this property is only available after the collecting process was executed") from exc
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
def all_collected_scenarios(self) -> List[Type[Scenario]]:
|
|
155
|
+
"""returns all collected :class:`Scenario` classes"""
|
|
156
|
+
try:
|
|
157
|
+
return self.collector.all_scenarios
|
|
158
|
+
except AttributeError as exc:
|
|
159
|
+
raise RuntimeError("this property is only available after the collecting process was executed") from exc
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
def all_collected_connections(self) -> List[Type[Connection]]:
|
|
163
|
+
"""returns all collected :class:`Connection` classes"""
|
|
164
|
+
try:
|
|
165
|
+
return self.collector.all_connections
|
|
166
|
+
except AttributeError as exc:
|
|
167
|
+
raise RuntimeError("this property is only available after the collecting process was executed") from exc
|
|
168
|
+
|
|
169
|
+
@property
|
|
170
|
+
def all_resolved_mappings(self) -> List[Tuple[Type[Setup], Type[Scenario], Dict[Type[Device], Type[Device]]]]:
|
|
171
|
+
"""returns all resolved mappings for the :class:`Device` mappings between :class:`Scenario` and
|
|
172
|
+
:class:`Setup`"""
|
|
173
|
+
try:
|
|
174
|
+
return self.solver.all_mappings
|
|
175
|
+
except AttributeError as exc:
|
|
176
|
+
raise RuntimeError("this property is only available after the resolving process was executed") from exc
|
|
177
|
+
|
|
178
|
+
# ---------------------------------- PROTECTED METHODS -------------------------------------------------------------
|
|
179
|
+
|
|
180
|
+
# ---------------------------------- METHODS -----------------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
def get_baldersettings_from_balderglob(self) -> Union[BalderSettings, None]:
|
|
183
|
+
"""
|
|
184
|
+
Helper method that checks if there is a valid :class:`BalderSettings` class in the given module
|
|
185
|
+
"""
|
|
186
|
+
module = self.collector.load_balderglob_py_file()
|
|
187
|
+
class_members = inspect.getmembers(module, inspect.isclass)
|
|
188
|
+
all_classes = []
|
|
189
|
+
for _, cur_class in class_members:
|
|
190
|
+
if issubclass(cur_class, BalderSettings):
|
|
191
|
+
all_classes.append(cur_class)
|
|
192
|
+
if len(all_classes) == 0:
|
|
193
|
+
return None
|
|
194
|
+
if len(all_classes) > 1:
|
|
195
|
+
raise DuplicateBalderSettingError(f"found more than one object that could be a BalderSettings object - "
|
|
196
|
+
f"found {','.join([cur_class.__name__ for cur_class in all_classes])}")
|
|
197
|
+
return all_classes[0]()
|
|
198
|
+
|
|
199
|
+
def get_balderplugins_from_balderglob(self) -> List[Type[BalderPlugin]]:
|
|
200
|
+
"""
|
|
201
|
+
Helper method that loads all valid :class:`BalderPlugin` classes and returns them
|
|
202
|
+
"""
|
|
203
|
+
module = self.collector.load_balderglob_py_file()
|
|
204
|
+
class_members = inspect.getmembers(module, inspect.isclass)
|
|
205
|
+
all_classes = []
|
|
206
|
+
for _, cur_class in class_members:
|
|
207
|
+
if issubclass(cur_class, BalderPlugin):
|
|
208
|
+
all_classes.append(cur_class)
|
|
209
|
+
return all_classes
|
|
210
|
+
|
|
211
|
+
def preparse_args(self):
|
|
212
|
+
"""
|
|
213
|
+
This method pre-parses the console arguments and checks if one or more of the most important (for example
|
|
214
|
+
`--working-dir`) attributes are given as CLI argument. This will be automatically pre-set. With that balder
|
|
215
|
+
secures that it can read the correct `balderglob.py` file, to initialize the plugins correctly.
|
|
216
|
+
"""
|
|
217
|
+
argv_working_dir_key = "--working-dir"
|
|
218
|
+
if argv_working_dir_key in sys.argv:
|
|
219
|
+
found_idx = sys.argv.index(argv_working_dir_key)
|
|
220
|
+
if found_idx != -1:
|
|
221
|
+
if len(sys.argv) <= found_idx + 1:
|
|
222
|
+
raise AttributeError(f"no path given for `{argv_working_dir_key}`")
|
|
223
|
+
|
|
224
|
+
self.working_dir = pathlib.Path(sys.argv[found_idx + 1]).absolute()
|
|
225
|
+
if not self.working_dir.is_dir():
|
|
226
|
+
raise NotADirectoryError(
|
|
227
|
+
f'can not parse the given working directory `{self.working_dir}` correctly or the given '
|
|
228
|
+
f'path is no directory..')
|
|
229
|
+
|
|
230
|
+
def parse_args(self):
|
|
231
|
+
"""
|
|
232
|
+
This method can be used to parse the `arg` object and fill all data from that object into the properties of this
|
|
233
|
+
:class:`BalderSession` object.
|
|
234
|
+
"""
|
|
235
|
+
self.cmd_arg_parser = argparse.ArgumentParser(
|
|
236
|
+
description='Balder is a simple scenario-based test system that allows you to run your tests on various '
|
|
237
|
+
'devices without rewriting them')
|
|
238
|
+
|
|
239
|
+
self.cmd_arg_parser.add_argument(
|
|
240
|
+
'--working-dir', nargs="?", default=os.getcwd(),
|
|
241
|
+
help="a explicit working directory on which the testsystem is to be executed with")
|
|
242
|
+
|
|
243
|
+
self.cmd_arg_parser.add_argument(
|
|
244
|
+
'--collect-only', action='store_true',
|
|
245
|
+
help="specifies that the tests are only collected but not resolved and executed")
|
|
246
|
+
|
|
247
|
+
self.cmd_arg_parser.add_argument(
|
|
248
|
+
'--resolve-only', action='store_true',
|
|
249
|
+
help="specifies that the tests are only collected and resolved but not executed")
|
|
250
|
+
|
|
251
|
+
self.cmd_arg_parser.add_argument(
|
|
252
|
+
'--show-discarded', action='store_true',
|
|
253
|
+
help="specifies that all discarded variations should be printed (with information why they were discarded)")
|
|
254
|
+
|
|
255
|
+
self.cmd_arg_parser.add_argument(
|
|
256
|
+
'--only-with-setup', nargs="*",
|
|
257
|
+
help="defines a number of Setup classes which should only be considered for the execution")
|
|
258
|
+
|
|
259
|
+
self.cmd_arg_parser.add_argument(
|
|
260
|
+
'--only-with-scenario', nargs="*",
|
|
261
|
+
help="defines a number of Scenario classes which should only be considered for the execution")
|
|
262
|
+
self.cmd_arg_parser.add_argument(
|
|
263
|
+
'--force-covered-by-duplicates', action='store_true',
|
|
264
|
+
help="specifies that the test run should include duplicated tests that are declared as covered_by another "
|
|
265
|
+
"test method (also true if it was already set in baldersetting object)")
|
|
266
|
+
|
|
267
|
+
self.plugin_manager.execute_addoption(self.cmd_arg_parser)
|
|
268
|
+
|
|
269
|
+
self.parsed_args = self.cmd_arg_parser.parse_args(self._alt_cmd_args)
|
|
270
|
+
|
|
271
|
+
self.working_dir = self.parsed_args.working_dir
|
|
272
|
+
self.collect_only = self.parsed_args.collect_only
|
|
273
|
+
self.resolve_only = self.parsed_args.resolve_only
|
|
274
|
+
self.show_discarded = self.parsed_args.show_discarded
|
|
275
|
+
self.only_with_setup = self.parsed_args.only_with_setup
|
|
276
|
+
self.only_with_scenario = self.parsed_args.only_with_scenario
|
|
277
|
+
self.force_covered_by_duplicates = self.parsed_args.force_covered_by_duplicates
|
|
278
|
+
|
|
279
|
+
def collect(self):
|
|
280
|
+
"""
|
|
281
|
+
This method collects all data.
|
|
282
|
+
"""
|
|
283
|
+
self.collector.collect(
|
|
284
|
+
plugin_manager=self.plugin_manager,
|
|
285
|
+
scenario_filter_patterns=self.only_with_scenario,
|
|
286
|
+
setup_filter_patterns=self.only_with_setup)
|
|
287
|
+
|
|
288
|
+
def solve(self):
|
|
289
|
+
"""
|
|
290
|
+
This method resolves all classes and executes different checks, that can be done before the test session starts.
|
|
291
|
+
"""
|
|
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())
|
|
296
|
+
self.solver.resolve(plugin_manager=self.plugin_manager)
|
|
297
|
+
|
|
298
|
+
def create_executor_tree(self):
|
|
299
|
+
"""
|
|
300
|
+
This method creates the executor tree object.
|
|
301
|
+
|
|
302
|
+
.. note::
|
|
303
|
+
Note that the method creates an :class:`ExecutorTree`, that hasn't to be completely resolved yet.
|
|
304
|
+
"""
|
|
305
|
+
self.executor_tree = self.solver.get_executor_tree(plugin_manager=self.plugin_manager,
|
|
306
|
+
add_discarded=self.show_discarded)
|
|
307
|
+
self.plugin_manager.execute_filter_executor_tree(executor_tree=self.executor_tree)
|
|
308
|
+
|
|
309
|
+
def run(self):
|
|
310
|
+
"""
|
|
311
|
+
This method executes the whole session
|
|
312
|
+
"""
|
|
313
|
+
line_length = 120
|
|
314
|
+
|
|
315
|
+
self.collect()
|
|
316
|
+
|
|
317
|
+
def print_rect_row(text):
|
|
318
|
+
line = "| " + text
|
|
319
|
+
line = line + " " * (line_length - len(line) - 1) + "|"
|
|
320
|
+
print(line)
|
|
321
|
+
|
|
322
|
+
print("+" + "-" * (line_length - 2) + "+")
|
|
323
|
+
print_rect_row("BALDER Testsystem")
|
|
324
|
+
sys_version = sys.version.replace('\n', '')
|
|
325
|
+
print_rect_row(f" python version {sys_version} | balder version {balder.__version__}")
|
|
326
|
+
print("+" + "-" * (line_length - 2) + "+")
|
|
327
|
+
print(f"Collect {len(self.all_collected_setups)} Setups and {len(self.all_collected_scenarios)} Scenarios")
|
|
328
|
+
if not self.collect_only:
|
|
329
|
+
self.solve()
|
|
330
|
+
self.create_executor_tree()
|
|
331
|
+
count_valid = len(self.executor_tree.get_all_variation_executors())
|
|
332
|
+
count_discarded = len(self.executor_tree.get_all_variation_executors(return_discarded=True)) - count_valid
|
|
333
|
+
addon_text = f" ({count_discarded} discarded)" if self.show_discarded else ""
|
|
334
|
+
print(f" resolve them to {count_valid} valid variations{addon_text}")
|
|
335
|
+
print("")
|
|
336
|
+
if not self.resolve_only:
|
|
337
|
+
self.executor_tree.execute(show_discarded=self.show_discarded)
|
|
338
|
+
else:
|
|
339
|
+
self.executor_tree.print_tree(show_discarded=self.show_discarded)
|
|
340
|
+
|
|
341
|
+
self.plugin_manager.execute_session_finished(self.executor_tree)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class BalderSettings:
|
|
5
|
+
"""
|
|
6
|
+
This class can be overwritten to manipulate the default settings for the balder test system. You can overwrite these
|
|
7
|
+
settings by defining a subclass in your `balderglob.py`.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
#: specifies that the test run should include duplicated tests that are declared as ``@covered_by`` another test
|
|
11
|
+
#: method
|
|
12
|
+
force_covered_by_duplicates = False
|
|
13
|
+
|
|
14
|
+
#: specifies the connection tree identifier that should be used as global identifier ("" is the default one)
|
|
15
|
+
used_global_connection_tree = ""
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import TYPE_CHECKING, Type
|
|
3
|
+
|
|
4
|
+
import itertools
|
|
5
|
+
|
|
6
|
+
from .base_connection_relation import BaseConnectionRelation, BaseConnectionRelationT
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from ..connection import List, Union, Connection
|
|
10
|
+
from .or_connection_relation import OrConnectionRelation
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AndConnectionRelation(BaseConnectionRelation):
|
|
14
|
+
"""
|
|
15
|
+
describes an AND relation between connections
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def get_tree_str(self) -> str:
|
|
19
|
+
based_on_strings = [cur_elem.get_tree_str() for cur_elem in self._connections]
|
|
20
|
+
return f"({' | '.join(based_on_strings)})"
|
|
21
|
+
|
|
22
|
+
def get_possibilities_for_direct_parent_cnn(self, for_cnn_class: Type[Connection]) -> list[AndConnectionRelation]:
|
|
23
|
+
"""
|
|
24
|
+
Helper method that returns a list of possible :class:`AndConnectionRelation` elements that hold the next parent
|
|
25
|
+
in the connection-tree that can be there instead of the origin connection.
|
|
26
|
+
The method returns a list, because there can exist different possibilities for this AND connection.
|
|
27
|
+
"""
|
|
28
|
+
direct_ancestors_relations = ()
|
|
29
|
+
for cur_and_elem in self.connections:
|
|
30
|
+
# `cur_and_elem` needs to be a connection, because we are using simplified which has only
|
|
31
|
+
# `OR[AND[Cnn, ...], Cnn, ..]`
|
|
32
|
+
if cur_and_elem.__class__ in for_cnn_class.get_parents():
|
|
33
|
+
# element already is a direct ancestor
|
|
34
|
+
direct_ancestors_relations += (cur_and_elem,)
|
|
35
|
+
else:
|
|
36
|
+
all_pos_possibilities = []
|
|
37
|
+
# add all possible direct parents to the possibilities list
|
|
38
|
+
for cur_direct_parent in for_cnn_class.get_parents():
|
|
39
|
+
if cur_direct_parent.is_parent_of(cur_and_elem.__class__):
|
|
40
|
+
all_pos_possibilities.append(cur_direct_parent.based_on(cur_and_elem))
|
|
41
|
+
direct_ancestors_relations += (all_pos_possibilities,)
|
|
42
|
+
# resolve the opportunities and create multiple possible AND relations where all elements are
|
|
43
|
+
# direct parents
|
|
44
|
+
return [
|
|
45
|
+
AndConnectionRelation(*cur_possibility).get_resolved()
|
|
46
|
+
for cur_possibility in itertools.product(*direct_ancestors_relations)
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
def get_simplified_relation(self) -> OrConnectionRelation:
|
|
50
|
+
from ..connection import Connection # pylint: disable=import-outside-toplevel
|
|
51
|
+
from .or_connection_relation import OrConnectionRelation # pylint: disable=import-outside-toplevel
|
|
52
|
+
|
|
53
|
+
# create template with all elements that are definitely contained in every new AND relation (only
|
|
54
|
+
# ``Connection`` objects here)
|
|
55
|
+
and_template = AndConnectionRelation()
|
|
56
|
+
# add all OR relations that needs to be further resolved to that list
|
|
57
|
+
self_simplified_or_relations = []
|
|
58
|
+
|
|
59
|
+
# first: go through all inner elements and convert all relations in simplified relations
|
|
60
|
+
for cur_elem in self.connections:
|
|
61
|
+
if isinstance(cur_elem, Connection):
|
|
62
|
+
# that is fine - add it to the template
|
|
63
|
+
and_template.append(cur_elem)
|
|
64
|
+
elif isinstance(cur_elem, (AndConnectionRelation, OrConnectionRelation)):
|
|
65
|
+
# simplify this AND/OR relation and add the items of it
|
|
66
|
+
# (the simplified version is always an OR relation!)
|
|
67
|
+
self_simplified_or_relations.append(cur_elem.get_simplified_relation().connections)
|
|
68
|
+
else:
|
|
69
|
+
raise TypeError(f'unexpected type for inner element `{cur_elem}`')
|
|
70
|
+
# now our `self_simplified_or_relations` can only consist of the following nesting: `List[OR[Conn, AND[Conn]]]`
|
|
71
|
+
# we need to resolve this constellation into `OR[Conn, AND[Conn]]` now
|
|
72
|
+
|
|
73
|
+
# now generate all possibilities out of the OR relations and add them to the AND template
|
|
74
|
+
all_new_ands = []
|
|
75
|
+
for cur_variation_tuple in itertools.product(*self_simplified_or_relations):
|
|
76
|
+
for cur_item in cur_variation_tuple:
|
|
77
|
+
if isinstance(cur_item, Connection):
|
|
78
|
+
# just add the single connection item to the AND template
|
|
79
|
+
new_full_and_relation = and_template.clone()
|
|
80
|
+
new_full_and_relation.append(cur_item)
|
|
81
|
+
all_new_ands.append(new_full_and_relation)
|
|
82
|
+
elif isinstance(cur_item, AndConnectionRelation):
|
|
83
|
+
new_full_and_relation = and_template.clone()
|
|
84
|
+
new_full_and_relation.extend(cur_item.connections)
|
|
85
|
+
all_new_ands.append(new_full_and_relation)
|
|
86
|
+
else:
|
|
87
|
+
#: note: inner element can not be an OR here!
|
|
88
|
+
raise TypeError(f'detect illegal type `{cur_item.__class__}` for inner element')
|
|
89
|
+
if not self_simplified_or_relations:
|
|
90
|
+
all_new_ands.append(and_template)
|
|
91
|
+
return OrConnectionRelation(*all_new_ands)
|
|
92
|
+
|
|
93
|
+
def is_single(self) -> bool:
|
|
94
|
+
|
|
95
|
+
if len(self._connections) == 0:
|
|
96
|
+
return True
|
|
97
|
+
|
|
98
|
+
return min(cnn.is_single() for cnn in self._connections)
|
|
99
|
+
|
|
100
|
+
def get_singles(self) -> List[Connection]:
|
|
101
|
+
from ..connection import Connection # pylint: disable=import-outside-toplevel
|
|
102
|
+
|
|
103
|
+
singles_and_relations = ()
|
|
104
|
+
for cur_elem in self._connections:
|
|
105
|
+
# get all singles of this AND relation element
|
|
106
|
+
singles_and_relations += (cur_elem.get_singles(),)
|
|
107
|
+
# now get the variations and add them to our results
|
|
108
|
+
return [
|
|
109
|
+
Connection.based_on(AndConnectionRelation(*cur_tuple))
|
|
110
|
+
for cur_tuple in itertools.product(*singles_and_relations)
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
def cut_into_all_possible_subtree_branches(self) -> List[AndConnectionRelation]:
|
|
114
|
+
if not self.is_single():
|
|
115
|
+
raise ValueError('can not execute method, because relation is not single')
|
|
116
|
+
|
|
117
|
+
tuple_with_all_possibilities = (
|
|
118
|
+
tuple(cur_tuple_item.cut_into_all_possible_subtree_branches() for cur_tuple_item in self._connections))
|
|
119
|
+
|
|
120
|
+
cloned_tuple_list = []
|
|
121
|
+
for cur_tuple in list(itertools.product(*tuple_with_all_possibilities)):
|
|
122
|
+
cloned_tuple = AndConnectionRelation(*[cur_tuple_item.clone() for cur_tuple_item in cur_tuple])
|
|
123
|
+
cloned_tuple_list.append(cloned_tuple)
|
|
124
|
+
return cloned_tuple_list
|
|
125
|
+
|
|
126
|
+
def contained_in(self, other_conn: Union[Connection, BaseConnectionRelationT], ignore_metadata=False) -> bool:
|
|
127
|
+
# This method checks if the AND relation is contained in the `other_conn`. To ensure that an AND relation is
|
|
128
|
+
# contained in a connection tree, there has to be another AND relation into the `other_conn`, that has the same
|
|
129
|
+
# length or is bigger. In addition, there has to exist an order combination where every element of the this AND
|
|
130
|
+
# relation is contained in the found AND relation of the `other_cnn`. In this case it doesn't matter where the
|
|
131
|
+
# AND relation is in `other_elem` (will be converted to single, and AND relation will be searched in all
|
|
132
|
+
# BASED_ON elements). If the AND relation of `other_conn` has fewer items than this AND relation, it will be
|
|
133
|
+
# ignored. The method only search for a valid existing item in the `other_conn` AND relation for every item of
|
|
134
|
+
# this AND relation.
|
|
135
|
+
from ..connection import Connection # pylint: disable=import-outside-toplevel
|
|
136
|
+
|
|
137
|
+
if not self.is_resolved():
|
|
138
|
+
raise ValueError('can not execute method, because connection relation is not resolved')
|
|
139
|
+
if not other_conn.is_resolved():
|
|
140
|
+
raise ValueError('can not execute method, because other connection relation is not resolved')
|
|
141
|
+
|
|
142
|
+
if isinstance(other_conn, BaseConnectionRelation):
|
|
143
|
+
other_conn = Connection.based_on(other_conn)
|
|
144
|
+
|
|
145
|
+
self_singles = self.get_singles()
|
|
146
|
+
other_singles = other_conn.get_singles()
|
|
147
|
+
|
|
148
|
+
for cur_self_single, cur_other_single in itertools.product(self_singles, other_singles):
|
|
149
|
+
# check if we can find an AND relation in the other object -> go the single connection upwards and
|
|
150
|
+
# search for a `AndConnectionRelation`
|
|
151
|
+
|
|
152
|
+
# self is a container connection -> use raw inner AND list
|
|
153
|
+
cur_self_single_and_relation = cur_self_single.based_on_elements.connections[0]
|
|
154
|
+
|
|
155
|
+
cur_sub_other_single = cur_other_single
|
|
156
|
+
while cur_sub_other_single is not None:
|
|
157
|
+
if isinstance(cur_sub_other_single, AndConnectionRelation):
|
|
158
|
+
# found an AND relation -> check if length does match
|
|
159
|
+
if len(cur_sub_other_single) < len(cur_self_single_and_relation):
|
|
160
|
+
# this complete element is not possible - skip this single!
|
|
161
|
+
break
|
|
162
|
+
# length is okay, no check if every single element is contained in one of this tuple
|
|
163
|
+
for cur_inner_self_elem in cur_self_single_and_relation.connections:
|
|
164
|
+
|
|
165
|
+
if not cur_inner_self_elem.contained_in(cur_sub_other_single,
|
|
166
|
+
ignore_metadata=ignore_metadata):
|
|
167
|
+
# at least one element is not contained in other AND relation - this complete element
|
|
168
|
+
# is not possible - skip this single!
|
|
169
|
+
break
|
|
170
|
+
# all items are contained in the current other AND relation -> match
|
|
171
|
+
return True
|
|
172
|
+
# go further up, if this element is no AND relation
|
|
173
|
+
cur_sub_other_single = cur_sub_other_single.based_on_elements[0] \
|
|
174
|
+
if cur_sub_other_single.based_on_elements else None
|
|
175
|
+
|
|
176
|
+
return False
|