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.
Files changed (89) hide show
  1. _balder/__init__.py +12 -0
  2. _balder/_version.py +34 -0
  3. _balder/balder_plugin.py +73 -0
  4. _balder/balder_session.py +341 -0
  5. _balder/balder_settings.py +15 -0
  6. _balder/cnnrelations/__init__.py +7 -0
  7. _balder/cnnrelations/and_connection_relation.py +176 -0
  8. _balder/cnnrelations/base_connection_relation.py +270 -0
  9. _balder/cnnrelations/or_connection_relation.py +65 -0
  10. _balder/collector.py +874 -0
  11. _balder/connection.py +863 -0
  12. _balder/connection_metadata.py +255 -0
  13. _balder/console/__init__.py +0 -0
  14. _balder/console/balder.py +58 -0
  15. _balder/controllers/__init__.py +12 -0
  16. _balder/controllers/base_device_controller.py +72 -0
  17. _balder/controllers/controller.py +29 -0
  18. _balder/controllers/device_controller.py +446 -0
  19. _balder/controllers/feature_controller.py +715 -0
  20. _balder/controllers/normal_scenario_setup_controller.py +402 -0
  21. _balder/controllers/scenario_controller.py +524 -0
  22. _balder/controllers/setup_controller.py +134 -0
  23. _balder/controllers/vdevice_controller.py +95 -0
  24. _balder/decorator_connect.py +104 -0
  25. _balder/decorator_covered_by.py +74 -0
  26. _balder/decorator_fixture.py +29 -0
  27. _balder/decorator_for_vdevice.py +118 -0
  28. _balder/decorator_gateway.py +34 -0
  29. _balder/decorator_insert_into_tree.py +52 -0
  30. _balder/decorator_parametrize.py +31 -0
  31. _balder/decorator_parametrize_by_feature.py +36 -0
  32. _balder/device.py +18 -0
  33. _balder/exceptions.py +182 -0
  34. _balder/executor/__init__.py +0 -0
  35. _balder/executor/basic_executable_executor.py +133 -0
  36. _balder/executor/basic_executor.py +205 -0
  37. _balder/executor/executor_tree.py +217 -0
  38. _balder/executor/parametrized_testcase_executor.py +52 -0
  39. _balder/executor/scenario_executor.py +169 -0
  40. _balder/executor/setup_executor.py +163 -0
  41. _balder/executor/testcase_executor.py +203 -0
  42. _balder/executor/unresolved_parametrized_testcase_executor.py +184 -0
  43. _balder/executor/variation_executor.py +882 -0
  44. _balder/exit_code.py +19 -0
  45. _balder/feature.py +74 -0
  46. _balder/feature_replacement_mapping.py +107 -0
  47. _balder/feature_vdevice_mapping.py +88 -0
  48. _balder/fixture_definition_scope.py +19 -0
  49. _balder/fixture_execution_level.py +22 -0
  50. _balder/fixture_manager.py +483 -0
  51. _balder/fixture_metadata.py +26 -0
  52. _balder/node_gateway.py +103 -0
  53. _balder/objects/__init__.py +0 -0
  54. _balder/objects/connections/__init__.py +0 -0
  55. _balder/objects/connections/osi_1_physical.py +116 -0
  56. _balder/objects/connections/osi_2_datalink.py +35 -0
  57. _balder/objects/connections/osi_3_network.py +47 -0
  58. _balder/objects/connections/osi_4_transport.py +40 -0
  59. _balder/objects/connections/osi_5_session.py +13 -0
  60. _balder/objects/connections/osi_6_presentation.py +13 -0
  61. _balder/objects/connections/osi_7_application.py +83 -0
  62. _balder/objects/devices/__init__.py +0 -0
  63. _balder/objects/devices/this_device.py +12 -0
  64. _balder/parametrization.py +75 -0
  65. _balder/plugin_manager.py +138 -0
  66. _balder/previous_executor_mark.py +23 -0
  67. _balder/routing_path.py +335 -0
  68. _balder/scenario.py +20 -0
  69. _balder/setup.py +18 -0
  70. _balder/solver.py +246 -0
  71. _balder/testresult.py +163 -0
  72. _balder/unmapped_vdevice.py +13 -0
  73. _balder/utils/__init__.py +0 -0
  74. _balder/utils/functions.py +103 -0
  75. _balder/utils/inner_device_managing_metaclass.py +14 -0
  76. _balder/utils/mixin_can_be_covered_by_executor.py +24 -0
  77. _balder/utils/typings.py +4 -0
  78. _balder/vdevice.py +9 -0
  79. balder/__init__.py +56 -0
  80. balder/connections.py +43 -0
  81. balder/devices.py +9 -0
  82. balder/exceptions.py +44 -0
  83. balder/parametrization.py +8 -0
  84. baldertest-0.1.0.dist-info/METADATA +356 -0
  85. baldertest-0.1.0.dist-info/RECORD +89 -0
  86. baldertest-0.1.0.dist-info/WHEEL +5 -0
  87. baldertest-0.1.0.dist-info/entry_points.txt +2 -0
  88. baldertest-0.1.0.dist-info/licenses/LICENSE +21 -0
  89. baldertest-0.1.0.dist-info/top_level.txt +2 -0
_balder/__init__.py ADDED
@@ -0,0 +1,12 @@
1
+
2
+ __all__ = [
3
+ "__version__",
4
+
5
+ "__version_tuple__"
6
+ ]
7
+
8
+ try:
9
+ from ._version import __version__, __version_tuple__
10
+ except ImportError:
11
+ __version__ = ""
12
+ __version_tuple__ = tuple()
_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
@@ -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,7 @@
1
+ from .and_connection_relation import AndConnectionRelation
2
+ from .or_connection_relation import OrConnectionRelation
3
+
4
+ __all__ = [
5
+ 'AndConnectionRelation',
6
+ 'OrConnectionRelation'
7
+ ]
@@ -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