baldertest 0.1.0b14__py3-none-any.whl → 0.1.2__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 +16 -3
- _balder/executor/executor_tree.py +28 -14
- _balder/executor/scenario_executor.py +2 -1
- _balder/executor/setup_executor.py +20 -6
- _balder/executor/variation_executor.py +32 -4
- _balder/solver.py +0 -3
- baldertest-0.1.2.dist-info/METADATA +360 -0
- {baldertest-0.1.0b14.dist-info → baldertest-0.1.2.dist-info}/RECORD +12 -12
- {baldertest-0.1.0b14.dist-info → baldertest-0.1.2.dist-info}/WHEEL +1 -1
- {baldertest-0.1.0b14.dist-info → baldertest-0.1.2.dist-info}/licenses/LICENSE +1 -1
- baldertest-0.1.0b14.dist-info/METADATA +0 -389
- {baldertest-0.1.0b14.dist-info → baldertest-0.1.2.dist-info}/entry_points.txt +0 -0
- {baldertest-0.1.0b14.dist-info → baldertest-0.1.2.dist-info}/top_level.txt +0 -0
_balder/_version.py
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
# file generated by setuptools-scm
|
|
2
2
|
# don't change, don't track in version control
|
|
3
3
|
|
|
4
|
-
__all__ = [
|
|
4
|
+
__all__ = [
|
|
5
|
+
"__version__",
|
|
6
|
+
"__version_tuple__",
|
|
7
|
+
"version",
|
|
8
|
+
"version_tuple",
|
|
9
|
+
"__commit_id__",
|
|
10
|
+
"commit_id",
|
|
11
|
+
]
|
|
5
12
|
|
|
6
13
|
TYPE_CHECKING = False
|
|
7
14
|
if TYPE_CHECKING:
|
|
@@ -9,13 +16,19 @@ if TYPE_CHECKING:
|
|
|
9
16
|
from typing import Union
|
|
10
17
|
|
|
11
18
|
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
+
COMMIT_ID = Union[str, None]
|
|
12
20
|
else:
|
|
13
21
|
VERSION_TUPLE = object
|
|
22
|
+
COMMIT_ID = object
|
|
14
23
|
|
|
15
24
|
version: str
|
|
16
25
|
__version__: str
|
|
17
26
|
__version_tuple__: VERSION_TUPLE
|
|
18
27
|
version_tuple: VERSION_TUPLE
|
|
28
|
+
commit_id: COMMIT_ID
|
|
29
|
+
__commit_id__: COMMIT_ID
|
|
19
30
|
|
|
20
|
-
__version__ = version = '0.1.
|
|
21
|
-
__version_tuple__ = version_tuple = (0, 1,
|
|
31
|
+
__version__ = version = '0.1.2'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 1, 2)
|
|
33
|
+
|
|
34
|
+
__commit_id__ = commit_id = None
|
|
@@ -62,9 +62,10 @@ class ExecutorTree(BasicExecutableExecutor):
|
|
|
62
62
|
self.update_inner_feature_reference_in_all_setups()
|
|
63
63
|
|
|
64
64
|
def _body_execution(self, show_discarded):
|
|
65
|
-
for cur_setup_executor in self.get_setup_executors():
|
|
65
|
+
for cur_setup_executor in self.get_setup_executors(return_discarded=show_discarded):
|
|
66
66
|
prev_mark = cur_setup_executor.prev_mark
|
|
67
|
-
if cur_setup_executor.has_runnable_tests(show_discarded)
|
|
67
|
+
if cur_setup_executor.has_runnable_tests(consider_discarded_too=show_discarded) \
|
|
68
|
+
or cur_setup_executor.has_skipped_tests():
|
|
68
69
|
cur_setup_executor.execute(show_discarded=show_discarded)
|
|
69
70
|
elif prev_mark == PreviousExecutorMark.SKIP:
|
|
70
71
|
cur_setup_executor.set_result_for_whole_branch(ResultState.SKIP)
|
|
@@ -78,17 +79,30 @@ class ExecutorTree(BasicExecutableExecutor):
|
|
|
78
79
|
|
|
79
80
|
# ---------------------------------- METHODS -----------------------------------------------------------------------
|
|
80
81
|
|
|
81
|
-
def get_setup_executors(self) -> List[SetupExecutor]:
|
|
82
|
-
"""
|
|
83
|
-
|
|
82
|
+
def get_setup_executors(self, return_discarded=False) -> List[SetupExecutor]:
|
|
83
|
+
"""
|
|
84
|
+
returns all setup executors of this tree
|
|
84
85
|
|
|
85
|
-
|
|
86
|
+
:param return_discarded: True if the method should return discarded variations too
|
|
87
|
+
|
|
88
|
+
:return: a list of relevant :class:`SetupExecutor`
|
|
89
|
+
"""
|
|
90
|
+
if return_discarded:
|
|
91
|
+
return self._setup_executors
|
|
92
|
+
|
|
93
|
+
return [
|
|
94
|
+
cur_setup_executor
|
|
95
|
+
for cur_setup_executor in self._setup_executors
|
|
96
|
+
if len(cur_setup_executor.get_scenario_executors(return_discarded=False)) > 0
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
def get_all_scenario_executors(self, return_discarded=False) -> List[ScenarioExecutor]:
|
|
86
100
|
"""
|
|
87
101
|
returns a list with all scenario executors
|
|
88
102
|
"""
|
|
89
103
|
all_scenario_executor = []
|
|
90
|
-
for cur_setup_executor in self.get_setup_executors():
|
|
91
|
-
all_scenario_executor += cur_setup_executor.get_scenario_executors()
|
|
104
|
+
for cur_setup_executor in self.get_setup_executors(return_discarded=return_discarded):
|
|
105
|
+
all_scenario_executor += cur_setup_executor.get_scenario_executors(return_discarded=return_discarded)
|
|
92
106
|
return all_scenario_executor
|
|
93
107
|
|
|
94
108
|
def get_all_variation_executors(self, return_discarded=False) -> List[VariationExecutor]:
|
|
@@ -96,7 +110,7 @@ class ExecutorTree(BasicExecutableExecutor):
|
|
|
96
110
|
returns a list with all variation executors
|
|
97
111
|
"""
|
|
98
112
|
all_variation_executor = []
|
|
99
|
-
for cur_scenario_executor in self.get_all_scenario_executors():
|
|
113
|
+
for cur_scenario_executor in self.get_all_scenario_executors(return_discarded=return_discarded):
|
|
100
114
|
all_variation_executor += cur_scenario_executor.get_variation_executors(return_discarded=return_discarded)
|
|
101
115
|
return all_variation_executor
|
|
102
116
|
|
|
@@ -136,9 +150,9 @@ class ExecutorTree(BasicExecutableExecutor):
|
|
|
136
150
|
|
|
137
151
|
def cleanup_empty_executor_branches(self, consider_discarded=False):
|
|
138
152
|
to_remove_executor = []
|
|
139
|
-
for cur_setup_executor in self.get_setup_executors():
|
|
153
|
+
for cur_setup_executor in self.get_setup_executors(return_discarded=consider_discarded):
|
|
140
154
|
cur_setup_executor.cleanup_empty_executor_branches(consider_discarded=consider_discarded)
|
|
141
|
-
if len(cur_setup_executor.get_scenario_executors()) == 0:
|
|
155
|
+
if len(cur_setup_executor.get_scenario_executors(return_discarded=consider_discarded)) == 0:
|
|
142
156
|
# remove this whole executor because it has no children anymore
|
|
143
157
|
to_remove_executor.append(cur_setup_executor)
|
|
144
158
|
for cur_setup_executor in to_remove_executor:
|
|
@@ -169,7 +183,7 @@ class ExecutorTree(BasicExecutableExecutor):
|
|
|
169
183
|
print_line(start_text)
|
|
170
184
|
# check if there exists runnable elements
|
|
171
185
|
runnables = [cur_exec.has_runnable_tests(consider_discarded_too=show_discarded)
|
|
172
|
-
for cur_exec in self.get_setup_executors()]
|
|
186
|
+
for cur_exec in self.get_setup_executors(return_discarded=show_discarded)]
|
|
173
187
|
one_or_more_runnable_setups = None if len(runnables) == 0 else max(runnables)
|
|
174
188
|
if one_or_more_runnable_setups:
|
|
175
189
|
super().execute(show_discarded=show_discarded)
|
|
@@ -189,8 +203,8 @@ class ExecutorTree(BasicExecutableExecutor):
|
|
|
189
203
|
def print_tree(self, show_discarded=False) -> None:
|
|
190
204
|
"""this method is an auxiliary method which outputs the entire tree"""
|
|
191
205
|
print("RESOLVING OVERVIEW", end="\n\n")
|
|
192
|
-
for cur_setup_executor in self.get_setup_executors():
|
|
193
|
-
for cur_scenario_executor in cur_setup_executor.get_scenario_executors():
|
|
206
|
+
for cur_setup_executor in self.get_setup_executors(return_discarded=show_discarded):
|
|
207
|
+
for cur_scenario_executor in cur_setup_executor.get_scenario_executors(return_discarded=show_discarded):
|
|
194
208
|
for cur_variation_executor in cur_scenario_executor.get_variation_executors(
|
|
195
209
|
return_discarded=show_discarded):
|
|
196
210
|
applicable = cur_variation_executor.prev_mark != PreviousExecutorMark.DISCARDED
|
|
@@ -99,7 +99,8 @@ class ScenarioExecutor(BasicExecutableExecutor):
|
|
|
99
99
|
def _body_execution(self, show_discarded):
|
|
100
100
|
for cur_variation_executor in self.get_variation_executors(return_discarded=show_discarded):
|
|
101
101
|
prev_mark = cur_variation_executor.prev_mark
|
|
102
|
-
if cur_variation_executor.has_runnable_tests()
|
|
102
|
+
if cur_variation_executor.has_runnable_tests(consider_discarded_too=show_discarded) \
|
|
103
|
+
or cur_variation_executor.has_skipped_tests():
|
|
103
104
|
cur_variation_executor.execute(show_discarded=show_discarded)
|
|
104
105
|
elif prev_mark == PreviousExecutorMark.SKIP:
|
|
105
106
|
cur_variation_executor.set_result_for_whole_branch(ResultState.SKIP)
|
|
@@ -79,9 +79,10 @@ class SetupExecutor(BasicExecutableExecutor):
|
|
|
79
79
|
print(f"SETUP {self.base_setup_class.__class__.__name__}")
|
|
80
80
|
|
|
81
81
|
def _body_execution(self, show_discarded):
|
|
82
|
-
for cur_scenario_executor in self.get_scenario_executors():
|
|
82
|
+
for cur_scenario_executor in self.get_scenario_executors(return_discarded=show_discarded):
|
|
83
83
|
prev_mark = cur_scenario_executor.prev_mark
|
|
84
|
-
if cur_scenario_executor.has_runnable_tests(show_discarded)
|
|
84
|
+
if cur_scenario_executor.has_runnable_tests(consider_discarded_too=show_discarded) \
|
|
85
|
+
or cur_scenario_executor.has_skipped_tests():
|
|
85
86
|
cur_scenario_executor.execute(show_discarded=show_discarded)
|
|
86
87
|
elif prev_mark == PreviousExecutorMark.SKIP:
|
|
87
88
|
cur_scenario_executor.set_result_for_whole_branch(ResultState.SKIP)
|
|
@@ -123,13 +124,26 @@ class SetupExecutor(BasicExecutableExecutor):
|
|
|
123
124
|
# object
|
|
124
125
|
setattr(cur_feature, cur_ref_feature_name, replacing_candidate)
|
|
125
126
|
|
|
126
|
-
def get_scenario_executors(self) -> List[ScenarioExecutor]:
|
|
127
|
-
"""
|
|
128
|
-
|
|
127
|
+
def get_scenario_executors(self, return_discarded=False) -> List[ScenarioExecutor]:
|
|
128
|
+
"""
|
|
129
|
+
returns a list with all scenario executors that belongs to this setup executor
|
|
130
|
+
|
|
131
|
+
:param return_discarded: True if the method should return discarded variations too
|
|
132
|
+
|
|
133
|
+
:return: a list of relevant :class:`ScenarioExecutor`
|
|
134
|
+
"""
|
|
135
|
+
if return_discarded:
|
|
136
|
+
return self._scenario_executors
|
|
137
|
+
|
|
138
|
+
return [
|
|
139
|
+
cur_scenario_executor
|
|
140
|
+
for cur_scenario_executor in self._scenario_executors
|
|
141
|
+
if len(cur_scenario_executor.get_variation_executors(return_discarded=False)) > 0
|
|
142
|
+
]
|
|
129
143
|
|
|
130
144
|
def cleanup_empty_executor_branches(self, consider_discarded=False):
|
|
131
145
|
to_remove_executor = []
|
|
132
|
-
for cur_scenario_executor in self.get_scenario_executors():
|
|
146
|
+
for cur_scenario_executor in self.get_scenario_executors(return_discarded=consider_discarded):
|
|
133
147
|
cur_scenario_executor.cleanup_empty_executor_branches(consider_discarded=consider_discarded)
|
|
134
148
|
if len(cur_scenario_executor.get_variation_executors(return_discarded=consider_discarded)) == 0:
|
|
135
149
|
# remove this whole executor because it has no children anymore
|
|
@@ -191,6 +191,7 @@ class VariationExecutor(BasicExecutableExecutor):
|
|
|
191
191
|
# do nothing if this variation can not be applied (is discarded)
|
|
192
192
|
return
|
|
193
193
|
|
|
194
|
+
self.restore_vdevice_feature_instances()
|
|
194
195
|
self.restore_original_vdevice_references()
|
|
195
196
|
self.revert_active_vdevice_device_mappings_in_all_features()
|
|
196
197
|
self.revert_scenario_device_feature_instances()
|
|
@@ -614,6 +615,8 @@ class VariationExecutor(BasicExecutableExecutor):
|
|
|
614
615
|
def restore_original_vdevice_references(self):
|
|
615
616
|
"""
|
|
616
617
|
This method restores all previously exchanged :class:`VDevice` references to the original ones.
|
|
618
|
+
|
|
619
|
+
This method is the cleanup method for :meth:`VariationExecutor.exchange_unmapped_vdevice_references`.
|
|
617
620
|
"""
|
|
618
621
|
all_devices = NormalScenarioSetupController.get_for(
|
|
619
622
|
self.cur_scenario_class.__class__).get_all_abs_inner_device_classes()
|
|
@@ -633,10 +636,10 @@ class VariationExecutor(BasicExecutableExecutor):
|
|
|
633
636
|
objects, will be replaced with the related feature instances of the mapped device object.
|
|
634
637
|
|
|
635
638
|
.. note::
|
|
636
|
-
Note that this method expects that the
|
|
637
|
-
setup features. In addition to that, the method expects, that the vDevice-Device mapping of
|
|
638
|
-
was set to the resolved setup device!
|
|
639
|
-
`update_scenario_device_feature_instances()`
|
|
639
|
+
Note that this method expects that the defined scenario features instances are already replaced with the
|
|
640
|
+
current active setup features. In addition to that, the method expects, that the vDevice-Device mapping of
|
|
641
|
+
every feature was set to the resolved setup device! This means, the method
|
|
642
|
+
`update_scenario_device_feature_instances()` needs to be called before.
|
|
640
643
|
"""
|
|
641
644
|
for scenario_device, _ in self._base_device_mapping.items():
|
|
642
645
|
# these features are subclasses of the real defined one (because they are already the replaced ones)
|
|
@@ -662,6 +665,31 @@ class VariationExecutor(BasicExecutableExecutor):
|
|
|
662
665
|
# object
|
|
663
666
|
setattr(cur_vdevice, cur_vdevice_attr_name, replacing_candidate)
|
|
664
667
|
|
|
668
|
+
def restore_vdevice_feature_instances(self):
|
|
669
|
+
"""
|
|
670
|
+
This method restores all previously exchanged :class:`VDevice` feature references to the original ones.
|
|
671
|
+
|
|
672
|
+
This method is the cleanup method for :meth:`VariationExecutor.update_vdevice_referenced_feature_instances`.
|
|
673
|
+
|
|
674
|
+
.. note::
|
|
675
|
+
Note that this method expects that the defined scenario features are still replaced with the active
|
|
676
|
+
setup features. In addition to that, the method expects, that the vDevice-Device mapping of every feature
|
|
677
|
+
is still set!
|
|
678
|
+
|
|
679
|
+
"""
|
|
680
|
+
for scenario_device, _ in self._base_device_mapping.items():
|
|
681
|
+
# these features are subclasses of the real defined one (because they are already the replaced ones)
|
|
682
|
+
all_device_features = DeviceController.get_for(scenario_device).get_all_instantiated_feature_objects()
|
|
683
|
+
for _, cur_feature in all_device_features.items():
|
|
684
|
+
# now get the related vDevice class and update its attributes
|
|
685
|
+
cur_vdevice, cur_device = cur_feature.active_vdevice_device_mapping
|
|
686
|
+
if cur_vdevice is not None and cur_device is not None:
|
|
687
|
+
cur_vdevice_controller = VDeviceController.get_for(cur_vdevice)
|
|
688
|
+
original_features_of_cur_vdev = cur_vdevice_controller.get_original_instanced_feature_objects()
|
|
689
|
+
|
|
690
|
+
for cur_vdev_attr_name, cur_vdev_orig_feature in original_features_of_cur_vdev.items():
|
|
691
|
+
setattr(cur_vdevice, cur_vdev_attr_name, cur_vdev_orig_feature)
|
|
692
|
+
|
|
665
693
|
def verify_applicability(self) -> None:
|
|
666
694
|
"""
|
|
667
695
|
This method verifies if this variation is executable. First the method checks if all defined
|
_balder/solver.py
CHANGED
|
@@ -219,9 +219,6 @@ class Solver:
|
|
|
219
219
|
variation_executor = VariationExecutor(device_mapping=cur_device_mapping, parent=scenario_executor)
|
|
220
220
|
variation_executor.verify_applicability()
|
|
221
221
|
|
|
222
|
-
if not variation_executor.can_be_applied():
|
|
223
|
-
continue
|
|
224
|
-
|
|
225
222
|
scenario_executor.add_variation_executor(variation_executor)
|
|
226
223
|
|
|
227
224
|
for cur_testcase in scenario_executor.base_scenario_controller.get_all_test_methods():
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: baldertest
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: balder: reusable scenario based test framework
|
|
5
|
+
Home-page: https://docs.balder.dev
|
|
6
|
+
Author: Max Stahlschmidt and others
|
|
7
|
+
License: MIT
|
|
8
|
+
Project-URL: Source, https://github.com/balder-dev/balder
|
|
9
|
+
Project-URL: Tracker, https://github.com/balder-dev/balder/issues
|
|
10
|
+
Keywords: test,systemtest,reusable,scenario
|
|
11
|
+
Platform: unix
|
|
12
|
+
Platform: linux
|
|
13
|
+
Platform: osx
|
|
14
|
+
Platform: cygwin
|
|
15
|
+
Platform: win32
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: MacOS :: MacOS X
|
|
18
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
19
|
+
Classifier: Operating System :: POSIX
|
|
20
|
+
Classifier: Intended Audience :: Developers
|
|
21
|
+
Classifier: Programming Language :: Python :: 3
|
|
22
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
26
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
27
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
28
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
29
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
30
|
+
Classifier: Topic :: Software Development :: Testing
|
|
31
|
+
Classifier: Topic :: Utilities
|
|
32
|
+
Requires-Python: >=3.9
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
License-File: LICENSE
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
<div align="center">
|
|
39
|
+
<img style="margin: 20px;max-width: 68%" src="https://docs.balder.dev/en/latest/_static/balder_w_boarder.png" alt="Balder logo">
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
Balder is a flexible Python test system that allows you to reuse test code written once for different but similar
|
|
43
|
+
platforms, devices, or applications. It enables you to install ready-to-use test cases and provides various test
|
|
44
|
+
development features that help you test your software or devices much faster.
|
|
45
|
+
|
|
46
|
+
You can use shared test code by installing an [existing BalderHub project](https://hub.balder.dev), or you can create
|
|
47
|
+
your own. This makes test development for your project much faster, since it is oftentimes enough to install a BalderHub
|
|
48
|
+
project and only provide the user-specific code.
|
|
49
|
+
|
|
50
|
+
Be part of the progress and share your tests with others, your company, or the whole world.
|
|
51
|
+
|
|
52
|
+
# Installation
|
|
53
|
+
|
|
54
|
+
You can install the latest release with pip:
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
python -m pip install baldertest
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
# Run Balder
|
|
61
|
+
|
|
62
|
+
After you've installed it, you can run Balder inside a Balder environment with the following command:
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
balder
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
You can also provide a specific path to the balder environment directory by using this console argument:
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
balder --working-dir /path/to/working/dir
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
# How does it work?
|
|
75
|
+
|
|
76
|
+
Balder allows you to reuse previously written test code by dividing it into the components that **are needed** for a
|
|
77
|
+
test (`Scenario`) and the components that **you have** (`Setup`).
|
|
78
|
+
|
|
79
|
+
You can define a test within a method of a `Scenario` class. This is often an abstract layer, where you only describe
|
|
80
|
+
the general business logic without providing any specific implementation details.
|
|
81
|
+
|
|
82
|
+
These specific implementation details are provided in the `Setup` classes. They describe exactly **what you have**. In
|
|
83
|
+
these classes, you provide an implementation for the abstract elements that were defined earlier in the `Scenario`.
|
|
84
|
+
|
|
85
|
+
Balder then automatically searches for matching mappings and runs your tests using them.
|
|
86
|
+
|
|
87
|
+
## Define the `Scenario` class
|
|
88
|
+
|
|
89
|
+
Inside `Scenario` or `Setup` classes, you can describe the environment using inner `Device` classes. For example, let's
|
|
90
|
+
write a test that validates the functionality of a lamp. For that, keep in mind that we want to make this test as
|
|
91
|
+
flexible as possible. It should be able to run with all kind of things that have a lamp:
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
import balder
|
|
95
|
+
from lib.scenario_features import BaseLightFeature
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class ScenarioLight(balder.Scenario):
|
|
99
|
+
|
|
100
|
+
# The device with its features that are required for this test
|
|
101
|
+
class LightSpendingDevice(balder.Device):
|
|
102
|
+
light = BaseLightFeature()
|
|
103
|
+
|
|
104
|
+
def test_check_light(self):
|
|
105
|
+
self.LightSpendingDevice.light.switch_on()
|
|
106
|
+
assert self.LightSpendingDevice.light.light_is_on()
|
|
107
|
+
self.LightSpendingDevice.light.switch_off()
|
|
108
|
+
assert not self.LightSpendingDevice.light.light_is_on()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Here, we have defined that a `LightSpendingDevice` **needs to have** a feature called `BaseLightFeature` so that this
|
|
115
|
+
scenario can be executed.
|
|
116
|
+
|
|
117
|
+
We have also added a test case (named with a `test_*()` prefix) called `test_check_light`, which executes the validation
|
|
118
|
+
of a lamp, by switching it on and off and checking its state.
|
|
119
|
+
|
|
120
|
+
**Note:** The `BaseLightFeature` is an abstract Feature class that defines the abstract methods `switch_on()`,
|
|
121
|
+
`switch_off()`, and `light_is_on()`.
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
## Define the `Setup` class
|
|
125
|
+
|
|
126
|
+
The next step is defining a `Setup` class, which describes what we have. For a `Scenario` to match a `Setup`, the
|
|
127
|
+
features of all scenario devices must be implemented by the mapped setup devices.
|
|
128
|
+
|
|
129
|
+
For example, if we want to test a car that includes a lamp, we could have a setup like the one shown below:
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
import balder
|
|
133
|
+
from lib.setup_features import CarEngineFeature, CarLightFeature
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class SetupGarage(balder.Setup):
|
|
137
|
+
|
|
138
|
+
class Car(balder.Device):
|
|
139
|
+
car_engine = CarEngineFeature()
|
|
140
|
+
car_light = CarLightFeature() # subclass of `lib.scenario_feature.BaseLightFeature`
|
|
141
|
+
...
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
When you run Balder in this environment, it will collect the `ScenarioLight` and the `SetupMyCar` classes and try to
|
|
147
|
+
find mappings between them. Based on the `ScenarioLight`, Balder looks for a device that provides an implementation of
|
|
148
|
+
the single `BaseLightFeature`. To do this, it scans all available setups. Since the `SetupMyCar.Car` device provides an
|
|
149
|
+
implementation through the `CarLightFeature`, this device will match.
|
|
150
|
+
|
|
151
|
+
```shell
|
|
152
|
+
+----------------------------------------------------------------------------------------------------------------------+
|
|
153
|
+
| BALDER Testsystem |
|
|
154
|
+
| python version 3.10.12 (main, Aug 15 2025, 14:32:43) [GCC 11.4.0] | balder version 0.1.0b14 |
|
|
155
|
+
+----------------------------------------------------------------------------------------------------------------------+
|
|
156
|
+
Collect 1 Setups and 1 Scenarios
|
|
157
|
+
resolve them to 1 valid variations
|
|
158
|
+
|
|
159
|
+
================================================== START TESTSESSION ===================================================
|
|
160
|
+
SETUP SetupGarage
|
|
161
|
+
SCENARIO ScenarioLight
|
|
162
|
+
VARIATION ScenarioLight.LightSpendingDevice:SetupGarage.Car
|
|
163
|
+
TEST ScenarioLight.test_check_light [.]
|
|
164
|
+
================================================== FINISH TESTSESSION ==================================================
|
|
165
|
+
TOTAL NOT_RUN: 0 | TOTAL FAILURE: 0 | TOTAL ERROR: 0 | TOTAL SUCCESS: 1 | TOTAL SKIP: 0 | TOTAL COVERED_BY: 0
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Add another Device to the `Setup` class
|
|
169
|
+
|
|
170
|
+
Now the big advantage of Balder comes into play. We can run our test with all devices that can implement the
|
|
171
|
+
`BaseLightFeature`, independent of how this will be implemented in detail. **You do not need to rewrite the test**.
|
|
172
|
+
|
|
173
|
+
So, We have more devices in our garage. So let's add them:
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
import balder
|
|
177
|
+
from lib.setup_features import CarEngineFeature, CarLightFeature, PedalFeature, BicycleLightFeature, GateOpenerFeature
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class SetupGarage(balder.Setup):
|
|
181
|
+
|
|
182
|
+
class Car(balder.Device):
|
|
183
|
+
car_engine = CarEngineFeature()
|
|
184
|
+
car_light = CarLightFeature() # subclass of `lib.scenario_feature.BaseLightFeature`
|
|
185
|
+
...
|
|
186
|
+
|
|
187
|
+
class Bicycle(balder.Device):
|
|
188
|
+
pedals = PedalFeature()
|
|
189
|
+
light = BicycleLightFeature() # another subclass of `lib.scenario_feature.BaseLightFeature`
|
|
190
|
+
|
|
191
|
+
class GarageGate(balder.Device):
|
|
192
|
+
opener = GateOpenerFeature()
|
|
193
|
+
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
If we run Balder now, it will find more mappings because the `Bicycle` device also provides an implementation for the
|
|
197
|
+
`BaseLightFeature` we are looking for.
|
|
198
|
+
|
|
199
|
+
```shell
|
|
200
|
+
+----------------------------------------------------------------------------------------------------------------------+
|
|
201
|
+
| BALDER Testsystem |
|
|
202
|
+
| python version 3.10.12 (main, Aug 15 2025, 14:32:43) [GCC 11.4.0] | balder version 0.1.0b14 |
|
|
203
|
+
+----------------------------------------------------------------------------------------------------------------------+
|
|
204
|
+
Collect 1 Setups and 1 Scenarios
|
|
205
|
+
resolve them to 2 valid variations
|
|
206
|
+
|
|
207
|
+
================================================== START TESTSESSION ===================================================
|
|
208
|
+
SETUP SetupGarage
|
|
209
|
+
SCENARIO ScenarioLight
|
|
210
|
+
VARIATION ScenarioLight.LightSpendingDevice:SetupGarage.Bicycle
|
|
211
|
+
TEST ScenarioLight.test_check_light [.]
|
|
212
|
+
VARIATION ScenarioLight.LightSpendingDevice:SetupGarage.Car
|
|
213
|
+
TEST ScenarioLight.test_check_light [.]
|
|
214
|
+
================================================== FINISH TESTSESSION ==================================================
|
|
215
|
+
TOTAL NOT_RUN: 0 | TOTAL FAILURE: 0 | TOTAL ERROR: 0 | TOTAL SUCCESS: 2 | TOTAL SKIP: 0 | TOTAL COVERED_BY: 0
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Balder handles all of this for you. You only need to describe your environment by defining `Scenario` and `Setup`
|
|
219
|
+
classes, then provide the specific implementations by creating the features. Balder will automatically search for and
|
|
220
|
+
apply the mappings between them.
|
|
221
|
+
|
|
222
|
+
**NOTE:** Balder offers many more elements to design complete device structures, including connections between multiple
|
|
223
|
+
devices.
|
|
224
|
+
|
|
225
|
+
You can learn more about that in the
|
|
226
|
+
[Tutorial Section of the Documentation](https://docs.balder.dev/en/latest/tutorial_guide/index.html).
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
# Example: Use an installable BalderHub package
|
|
230
|
+
|
|
231
|
+
With Balder, you can create custom test environments or install open-source-available test packages, known as
|
|
232
|
+
[BalderHub packages](https://hub.balder.dev). For example, if you want to test the login functionality of a website, simply use the
|
|
233
|
+
ready-to-use scenario `ScenarioSimpleLogin` from the [`balderhub-auth` package](https://hub.balder.dev/projects/auth/en/latest/examples.html),
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
We want to use [Selenium](https://www.selenium.dev/) to control the browser and of course use html elements, so let's install
|
|
237
|
+
`balderhub-selenium` and `balderhub-html` right away.
|
|
238
|
+
|
|
239
|
+
```
|
|
240
|
+
$ pip install balderhub-auth balderhub-selenium balderhub-html
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
So as mentioned, you don't need to define a scenario and a test yourself; you can simply import it:
|
|
244
|
+
|
|
245
|
+
```python
|
|
246
|
+
# file `scenario_balderhub.py`
|
|
247
|
+
|
|
248
|
+
from balderhub.auth.scenarios import ScenarioSimpleLogin
|
|
249
|
+
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
According to the [documentation of this BalderHub project](https://hub.balder.dev/projects/auth/en/latest/examples.html),
|
|
253
|
+
we only need to define the login page by overwriting the ``LoginPage`` feature:
|
|
254
|
+
|
|
255
|
+
```python
|
|
256
|
+
|
|
257
|
+
# file `lib/pages.py`
|
|
258
|
+
|
|
259
|
+
import balderhub.auth.contrib.html.pages
|
|
260
|
+
from balderhub.html.lib.utils import Selector
|
|
261
|
+
from balderhub.url.lib.utils import Url
|
|
262
|
+
import balderhub.html.lib.utils.components as html
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
class LoginPage(balderhub.auth.contrib.html.pages.LoginPage):
|
|
266
|
+
|
|
267
|
+
url = Url('https://example.com')
|
|
268
|
+
|
|
269
|
+
# Overwrite abstract property
|
|
270
|
+
@property
|
|
271
|
+
def input_username(self):
|
|
272
|
+
return html.inputs.HtmlTextInput.by_selector(self.driver, Selector.by_name('user'))
|
|
273
|
+
|
|
274
|
+
# Overwrite abstract property
|
|
275
|
+
@property
|
|
276
|
+
def input_password(self):
|
|
277
|
+
return html.inputs.HtmlPasswordInput.by_selector(self.driver, Selector.by_name('user'))
|
|
278
|
+
|
|
279
|
+
# Overwrite abstract property
|
|
280
|
+
@property
|
|
281
|
+
def btn_login(self):
|
|
282
|
+
return html.HtmlButtonElement.by_selector(self.driver, Selector.by_id('submit-button'))
|
|
283
|
+
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
And use it in our setup:
|
|
287
|
+
|
|
288
|
+
```python
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
# file `setups/setup_office.py`
|
|
292
|
+
|
|
293
|
+
import balder
|
|
294
|
+
import balderhub.auth.lib.scenario_features.role
|
|
295
|
+
from balderhub.selenium.lib.setup_features import SeleniumChromeWebdriverFeature
|
|
296
|
+
|
|
297
|
+
from lib.pages import LoginPage
|
|
298
|
+
|
|
299
|
+
class UserConfig(balderhub.auth.lib.scenario_features.role.UserRoleFeature):
|
|
300
|
+
# provide the credentials for the log in
|
|
301
|
+
username = 'admin'
|
|
302
|
+
password = 'secret'
|
|
303
|
+
|
|
304
|
+
class SetupOffice(balder.Setup):
|
|
305
|
+
|
|
306
|
+
class Server(balder.Device):
|
|
307
|
+
user = UserConfig()
|
|
308
|
+
|
|
309
|
+
class Browser(balder.Device):
|
|
310
|
+
selenium = SeleniumChromeWebdriverFeature()
|
|
311
|
+
page_login = LoginPage()
|
|
312
|
+
|
|
313
|
+
# fixture to prepare selenium - will be executed before the test session runs
|
|
314
|
+
@balder.fixture('session')
|
|
315
|
+
def selenium(self):
|
|
316
|
+
self.Browser.selenium.create()
|
|
317
|
+
yield
|
|
318
|
+
self.Browser.selenium.quit()
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
When you run Balder now, it will execute a complete login test that you didn't write yourself -
|
|
322
|
+
**it was created by the open-source community**.
|
|
323
|
+
|
|
324
|
+
```shell
|
|
325
|
+
+----------------------------------------------------------------------------------------------------------------------+
|
|
326
|
+
| BALDER Testsystem |
|
|
327
|
+
| python version 3.10.12 (main, Aug 15 2025, 14:32:43) [GCC 11.4.0] | balder version 0.1.0b14 |
|
|
328
|
+
+----------------------------------------------------------------------------------------------------------------------+
|
|
329
|
+
Collect 1 Setups and 1 Scenarios
|
|
330
|
+
resolve them to 1 valid variations
|
|
331
|
+
|
|
332
|
+
================================================== START TESTSESSION ===================================================
|
|
333
|
+
SETUP SetupOffice
|
|
334
|
+
SCENARIO ScenarioSimpleLogin
|
|
335
|
+
VARIATION ScenarioSimpleLogin.Client:SetupOffice.Browser | ScenarioSimpleLogin.System:SetupOffice.Server
|
|
336
|
+
TEST ScenarioSimpleLogin.test_login [.]
|
|
337
|
+
================================================== FINISH TESTSESSION ==================================================
|
|
338
|
+
TOTAL NOT_RUN: 0 | TOTAL FAILURE: 0 | TOTAL ERROR: 0 | TOTAL SUCCESS: 1 | TOTAL SKIP: 0 | TOTAL COVERED_BY: 0
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
If you'd like to learn more about it, feel free to dive [into the documentation](https://balder.dev).
|
|
342
|
+
|
|
343
|
+
# Contribution guidelines
|
|
344
|
+
|
|
345
|
+
Any help is appreciated. If you want to contribute to balder, take a look into the
|
|
346
|
+
[contribution guidelines](https://github.com/balder-dev/balder/blob/main/CONTRIBUTING.md).
|
|
347
|
+
|
|
348
|
+
Are you an expert in your field? Do you enjoy the concept of balder? How about creating your own
|
|
349
|
+
BalderHub project? You can contribute to an existing project or create your own. If you are not sure, a project for
|
|
350
|
+
your idea already exists or if you want to discuss your ideas with others, feel free to
|
|
351
|
+
[create an issue in the BalderHub main entry project](https://github.com/balder-dev/hub.balder.dev/issues) or
|
|
352
|
+
[start a new discussion](https://github.com/balder-dev/hub.balder.dev/discussions).
|
|
353
|
+
|
|
354
|
+
# License
|
|
355
|
+
|
|
356
|
+
Balder is free and Open-Source
|
|
357
|
+
|
|
358
|
+
Copyright (c) 2022-2025 Max Stahlschmidt and others
|
|
359
|
+
|
|
360
|
+
Distributed under the terms of the MIT license
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
_balder/__init__.py,sha256=Qk4wkVInPlXLFV36Yco5K7PDawJoeeWQVakzj6g5pmA,195
|
|
2
|
-
_balder/_version.py,sha256=
|
|
2
|
+
_balder/_version.py,sha256=Ok5oAXdWgR9aghaFXTafTeDW6sYO3uVe6d2Nket57R4,704
|
|
3
3
|
_balder/balder_plugin.py,sha256=EQzJP1dwwVDydhMLJtAmTCXOczlDuXBJur05lalmK_k,3136
|
|
4
4
|
_balder/balder_session.py,sha256=ezT86gC_VzPQZOQ4r5qQ75IEm6rXZHiIpEqZDczkRsE,16149
|
|
5
5
|
_balder/balder_settings.py,sha256=U96PVep7dGSaTXrMfeZMYf6oCIcEDPEqrBlFcoX476s,582
|
|
@@ -31,7 +31,7 @@ _balder/previous_executor_mark.py,sha256=gwpGu7d-kwPzQT8CmaPfuEG6fess2Upf5Q-zX6O
|
|
|
31
31
|
_balder/routing_path.py,sha256=icIp09sC51sBsNHZPrWzGOtLqrOT5Se_dj0Db7MwtwY,17093
|
|
32
32
|
_balder/scenario.py,sha256=beHkEqb9pnhrMOt1qfgATSBWVfHahw9RsOCT-uxK6TE,954
|
|
33
33
|
_balder/setup.py,sha256=sIyuqhDSCzxtkWXW48jRrLDvGWkZwgpL6NvEKrFM65E,890
|
|
34
|
-
_balder/solver.py,sha256=
|
|
34
|
+
_balder/solver.py,sha256=eB7pf9224AJAbeF4HlMDHMMy06mmq3C-LiyX4NBlphw,13031
|
|
35
35
|
_balder/testresult.py,sha256=AE_LWCwmR9aR36IHfPARaaqossBahv-P2URCZ8cFAvM,6893
|
|
36
36
|
_balder/unmapped_vdevice.py,sha256=oKr01YVTLViWtZkYz8kx8ccTx-KmwgNrHuQqqD4eLQw,513
|
|
37
37
|
_balder/vdevice.py,sha256=fc2xuMnTuN1RyfWh9mqFgLdSO9yGA75eERobTXUQ9JA,215
|
|
@@ -53,13 +53,13 @@ _balder/controllers/vdevice_controller.py,sha256=rWK3oHzXbNlgXf0xbWyssDevx0qQ7Bm
|
|
|
53
53
|
_balder/executor/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
54
54
|
_balder/executor/basic_executable_executor.py,sha256=RWwc8c4B7qO2BxlUB8dZH9IC64qm_KWh8tGCoBB8v6o,5395
|
|
55
55
|
_balder/executor/basic_executor.py,sha256=6rLSBrTGodlaRVfHV2kpXPjbAqUPEq9-2txCxz_-fXg,9327
|
|
56
|
-
_balder/executor/executor_tree.py,sha256=
|
|
56
|
+
_balder/executor/executor_tree.py,sha256=ZyZYRyeOjvZEW_BRds9Ujfa3TrNxa_ouAGRsLNoSVwk,11457
|
|
57
57
|
_balder/executor/parametrized_testcase_executor.py,sha256=Wk4XitAVOMGU7pSn7AOyvOsuL9CorcqFJ-iRuVtat1M,2149
|
|
58
|
-
_balder/executor/scenario_executor.py,sha256=
|
|
59
|
-
_balder/executor/setup_executor.py,sha256
|
|
58
|
+
_balder/executor/scenario_executor.py,sha256=NMx4hJpY5Xtsd4ugrQTdX4bcczgi_aabtlqHIkMfXl8,8211
|
|
59
|
+
_balder/executor/setup_executor.py,sha256=-nLTH0enq7IbWMlXPZF_sY5Mt78aDesMkFpco6qU8uM,9167
|
|
60
60
|
_balder/executor/testcase_executor.py,sha256=Yrifk_8hsm1KKrE6udd84EyBywmWiUN00MlcezMQ7xA,8216
|
|
61
61
|
_balder/executor/unresolved_parametrized_testcase_executor.py,sha256=Pb-LhEFNspb03pYTqTyFvSWPFMoRCjO8OVjjP2rHFMw,7628
|
|
62
|
-
_balder/executor/variation_executor.py,sha256=
|
|
62
|
+
_balder/executor/variation_executor.py,sha256=wdWOIuXqkxsl-wifuEFWYaH_hLuvaYjvotdVPMM2g8I,53252
|
|
63
63
|
_balder/objects/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
64
64
|
_balder/objects/connections/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
65
65
|
_balder/objects/connections/osi_1_physical.py,sha256=74lKWJd6ETEtvNXH0_dmTbkZlStJ_af218pQkUht0aA,2189
|
|
@@ -81,9 +81,9 @@ balder/connections.py,sha256=H6rf7UsiVY_FeZLngZXCT9WDw9cQqpiDiPbz_0J4yjM,2331
|
|
|
81
81
|
balder/devices.py,sha256=zupHtz8yaiEjzR8CrvgZU-RzsDQcZFeN5mObfhtjwSw,173
|
|
82
82
|
balder/exceptions.py,sha256=iaR4P2L7K3LggYSDnjCGLheZEaGgnMilxDQdoYD5KHQ,1954
|
|
83
83
|
balder/parametrization.py,sha256=R8U67f6DEnXdDc9cGOgS8yFTEAfhglv1v9mnAUAExUg,150
|
|
84
|
-
baldertest-0.1.
|
|
85
|
-
baldertest-0.1.
|
|
86
|
-
baldertest-0.1.
|
|
87
|
-
baldertest-0.1.
|
|
88
|
-
baldertest-0.1.
|
|
89
|
-
baldertest-0.1.
|
|
84
|
+
baldertest-0.1.2.dist-info/licenses/LICENSE,sha256=kNB2-kpqR9c3rEyXsMaQiqb1jiLZ5j6R6TSda63dcJY,1089
|
|
85
|
+
baldertest-0.1.2.dist-info/METADATA,sha256=OI7I2-tt0pTFpfxF95nOODxncv8y0XAXv61JWuJV0Eo,14730
|
|
86
|
+
baldertest-0.1.2.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
87
|
+
baldertest-0.1.2.dist-info/entry_points.txt,sha256=hzqu_nrMKTCi5IJqzS1fhIXWEiL7mTGZ-kgj2lUYlRU,65
|
|
88
|
+
baldertest-0.1.2.dist-info/top_level.txt,sha256=RUkIBkNLqHMemx2C9aEpoS65dpqb6_jU-oagIPxGQEA,15
|
|
89
|
+
baldertest-0.1.2.dist-info/RECORD,,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c) 2022 Max Stahlschmidt and others
|
|
3
|
+
Copyright (c) 2022-2025 Max Stahlschmidt and others
|
|
4
4
|
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
|
@@ -1,389 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: baldertest
|
|
3
|
-
Version: 0.1.0b14
|
|
4
|
-
Summary: balder: reusable scenario based test framework
|
|
5
|
-
Home-page: https://docs.balder.dev
|
|
6
|
-
Author: Max Stahlschmidt and others
|
|
7
|
-
License: MIT
|
|
8
|
-
Project-URL: Source, https://github.com/balder-dev/balder
|
|
9
|
-
Project-URL: Tracker, https://github.com/balder-dev/balder/issues
|
|
10
|
-
Keywords: test,systemtest,reusable,scenario
|
|
11
|
-
Platform: unix
|
|
12
|
-
Platform: linux
|
|
13
|
-
Platform: osx
|
|
14
|
-
Platform: cygwin
|
|
15
|
-
Platform: win32
|
|
16
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
-
Classifier: Operating System :: MacOS :: MacOS X
|
|
18
|
-
Classifier: Operating System :: Microsoft :: Windows
|
|
19
|
-
Classifier: Operating System :: POSIX
|
|
20
|
-
Classifier: Intended Audience :: Developers
|
|
21
|
-
Classifier: Programming Language :: Python :: 3
|
|
22
|
-
Classifier: Programming Language :: Python :: 3 :: Only
|
|
23
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
24
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
25
|
-
Classifier: Topic :: Software Development :: Libraries
|
|
26
|
-
Classifier: Topic :: Software Development :: Testing
|
|
27
|
-
Classifier: Topic :: Utilities
|
|
28
|
-
Requires-Python: >=3.9
|
|
29
|
-
Description-Content-Type: text/markdown
|
|
30
|
-
License-File: LICENSE
|
|
31
|
-
Dynamic: license-file
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
<div align="center">
|
|
35
|
-
<img style="margin: 20px;max-width: 68%" src="https://docs.balder.dev/en/latest/_static/balder_w_boarder.png" alt="Balder logo">
|
|
36
|
-
</div>
|
|
37
|
-
|
|
38
|
-
Balder is a very powerful, universal and flexible python test system that allows you to reuse a once written testcode as
|
|
39
|
-
efficiently as possible for different but similar platforms/devices/applications. Balder's goal is
|
|
40
|
-
being a platform for combining the single steps of defining, developing and documenting the entire test
|
|
41
|
-
process while using test scenarios which can be reused across different projects.
|
|
42
|
-
|
|
43
|
-
You can share your own testcode by creating a new BalderHub project, or you use an
|
|
44
|
-
[existing BalderHub project](https://hub.balder.dev), by simply installing and using it. This makes the test development
|
|
45
|
-
for your project much faster, since it is often times enough to only provide the user-specific code.
|
|
46
|
-
|
|
47
|
-
Be part of the progress and share your tests with others, your company or the whole world.
|
|
48
|
-
|
|
49
|
-
# Installation
|
|
50
|
-
|
|
51
|
-
You can install the latest release with pip:
|
|
52
|
-
|
|
53
|
-
```
|
|
54
|
-
python -m pip install baldertest
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
# Run Balder
|
|
58
|
-
|
|
59
|
-
After you've installed it, you can run Balder inside a Balder environment with the following command:
|
|
60
|
-
|
|
61
|
-
```
|
|
62
|
-
balder
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
You can also provide a specific path to the balder environment directory by using this console argument:
|
|
66
|
-
|
|
67
|
-
```
|
|
68
|
-
balder --working-dir /path/to/working/dir
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
# How does it work?
|
|
72
|
-
|
|
73
|
-
Balder allows you to reuse previously written test code by dividing it into the components that **are needed** for a
|
|
74
|
-
test (`Scenario`) and the components that **we have** (`Setup`).
|
|
75
|
-
|
|
76
|
-
`Scenario` classes define a test. Only describe the most important aspects **you need** for the execution of the
|
|
77
|
-
corresponding test (method of the scenario class) inside `Scenario` classes. Often it is enough to define abstract
|
|
78
|
-
methods in the scenario-level features.
|
|
79
|
-
|
|
80
|
-
In contrast, `Setup` classes describe exactly what **you have**. This is where you define all the devices and their
|
|
81
|
-
features. Balder will then automatically search for mappings and run your test with them.
|
|
82
|
-
|
|
83
|
-
## Define the `Scenario` class
|
|
84
|
-
|
|
85
|
-
Inside `Scenario` or `Setup` classes, inner `Device` classes describe your environment. For example, if you want to test
|
|
86
|
-
the process of sending a message between two devices, you can create a `Scenario` like shown below:
|
|
87
|
-
|
|
88
|
-
```python
|
|
89
|
-
import balder
|
|
90
|
-
from .features import SendMessageFeature, RecvMessageFeature
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
class ScenarioMessaging(balder.Scenario):
|
|
94
|
-
|
|
95
|
-
class Sender(balder.Device):
|
|
96
|
-
send = SendMessageFeature()
|
|
97
|
-
|
|
98
|
-
@balder.connect(Sender, over_connection=balder.Connection())
|
|
99
|
-
class Receiver(balder.Device):
|
|
100
|
-
recv = RecvMessageFeature()
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
You have now defined, that the `Sender` device must be able to send messages (has `SendMessageFeature()`), while
|
|
107
|
-
the `Receiver` device must be able to receive messages (has `RecvMessageFeature()`). Both devices are connected with
|
|
108
|
-
each other. For this we use the general connection `balder.Connection()`, which allows every type of connection.
|
|
109
|
-
|
|
110
|
-
You can implement your test, by adding a new method that starts with `test_*()`:
|
|
111
|
-
|
|
112
|
-
```python
|
|
113
|
-
import balder
|
|
114
|
-
from .features import SendMessageFeature, RecvMessageFeature
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
class ScenarioMessaging(balder.Scenario):
|
|
118
|
-
|
|
119
|
-
class Sender(balder.Device):
|
|
120
|
-
send = SendMessageFeature()
|
|
121
|
-
|
|
122
|
-
@balder.connect(Sender, over_connection=balder.Connection())
|
|
123
|
-
class Receiver(balder.Device):
|
|
124
|
-
recv = RecvMessageFeature()
|
|
125
|
-
|
|
126
|
-
def test_send_msg(self):
|
|
127
|
-
MESSAGE_TXT = "Hello World"
|
|
128
|
-
self.Sender.send.send_msg(MESSAGE_TXT)
|
|
129
|
-
received_msg = self.Receiver.recv.get_last_received_msg()
|
|
130
|
-
assert received_msg == MESSAGE_TXT
|
|
131
|
-
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
## Define the `Setup` class
|
|
135
|
-
|
|
136
|
-
The next step is defining a `Setup` class, which describes what we have. For a `Scenario` to match a `Setup`, all of
|
|
137
|
-
your scenario-devices must be able to map to a sub selection of some setup-devices.
|
|
138
|
-
|
|
139
|
-
For example, if you want to verify if your DUT is able to receive messages from your computer (both are connected over
|
|
140
|
-
USB), just create a `Setup` class with the both devices `Computer` and `Dut` and add an implementation (subclasses) of
|
|
141
|
-
our previously defined feature classes `SendMessageFeature` and `RecvMessageFeature` to them:
|
|
142
|
-
|
|
143
|
-
```python
|
|
144
|
-
import balder
|
|
145
|
-
from balder import connections as cnns
|
|
146
|
-
from .setup_features import SendUsbMessageFeature, RecvUsbMessageFeature
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
class SetupOverUsb(balder.Setup):
|
|
150
|
-
|
|
151
|
-
class Computer(balder.Device):
|
|
152
|
-
# non-abstract subclass of `SendMessageFeature`
|
|
153
|
-
send = SendUsbMessageFeature()
|
|
154
|
-
|
|
155
|
-
@balder.connect(Computer, over_connection=cnns.UsbConnection())
|
|
156
|
-
class Dut(balder.Device):
|
|
157
|
-
# non-abstract subclass of `RecvMessageFeature`
|
|
158
|
-
recv = RecvUsbMessageFeature()
|
|
159
|
-
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
With the features `SendUsbMessageFeature` and `RecvUsbMessageFeature`, both devices hold an implementation of our
|
|
163
|
-
previous scenario-level features `SendMessageFeature` and `RecvMessageFeature`. They are the child classes of our
|
|
164
|
-
scenario-level features and hold the full implementation for sending/receiving data over USB.
|
|
165
|
-
|
|
166
|
-
As soon as you run Balder, Balder will automatically detect that our scenario `ScenarioMessaging` can be mapped to the
|
|
167
|
-
`SetupOverUsb`. This will cause Balder to run the test `test_send_msg()` with the implemented setup-level version of
|
|
168
|
-
the features.
|
|
169
|
-
|
|
170
|
-
The big advantage of Balder is the reusability. If you want to test if the communication also works in the other
|
|
171
|
-
direction, just add the features inverted:
|
|
172
|
-
|
|
173
|
-
```python
|
|
174
|
-
import balder
|
|
175
|
-
from balder import connections as cnns
|
|
176
|
-
from .setup_features import SendUsbMessageFeature, RecvUsbMessageFeature
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
class SetupOverUsb(balder.Setup):
|
|
180
|
-
|
|
181
|
-
class Computer(balder.Device):
|
|
182
|
-
# non-abstract subclass of `SendMessageFeature`
|
|
183
|
-
send = SendUsbMessageFeature()
|
|
184
|
-
# non-abstract subclass of `RecvMessageFeature`
|
|
185
|
-
recv = RecvUsbMessageFeature()
|
|
186
|
-
|
|
187
|
-
@balder.connect(Computer, over_connection=cnns.UsbConnection())
|
|
188
|
-
class Dut(balder.Device):
|
|
189
|
-
# non-abstract subclass of `SendMessageFeature`
|
|
190
|
-
send = SendUsbMessageFeature()
|
|
191
|
-
# non-abstract subclass of `RecvMessageFeature`
|
|
192
|
-
recv = RecvUsbMessageFeature()
|
|
193
|
-
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
Balder will now run the test once with the `Computer` being the sender and once with the `Dut` being the Sender, even
|
|
197
|
-
though you didn't implement anything new.
|
|
198
|
-
|
|
199
|
-
| | `ScenarioMessaging.Sender` | `ScenarioMessaging.Sender` |
|
|
200
|
-
|-------------|----------------------------|----------------------------|
|
|
201
|
-
| VARIATION 1 | `SetupOverUsb.Computer` | `SetupOverUsb.Dut` |
|
|
202
|
-
| VARIATION 2 | `SetupOverUsb.Dut` | `SetupOverUsb.Computer` |
|
|
203
|
-
|
|
204
|
-
Do you have another device, that should be tested too? Just add it to your setup:
|
|
205
|
-
|
|
206
|
-
```python
|
|
207
|
-
import balder
|
|
208
|
-
from balder import connections as cnns
|
|
209
|
-
from .setup_features import SendUsbMessageFeature, RecvUsbMessageFeature
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
class SetupOverUsb(balder.Setup):
|
|
213
|
-
|
|
214
|
-
class Computer(balder.Device):
|
|
215
|
-
# non-abstract subclass of `SendMessageFeature`
|
|
216
|
-
send = SendUsbMessageFeature()
|
|
217
|
-
# non-abstract subclass of `RecvMessageFeature`
|
|
218
|
-
recv = RecvUsbMessageFeature()
|
|
219
|
-
|
|
220
|
-
@balder.connect(Computer, over_connection=cnns.UsbConnection())
|
|
221
|
-
class Dut(balder.Device):
|
|
222
|
-
# non-abstract subclass of `SendMessageFeature`
|
|
223
|
-
send = SendUsbMessageFeature()
|
|
224
|
-
# non-abstract subclass of `RecvMessageFeature`
|
|
225
|
-
recv = RecvUsbMessageFeature()
|
|
226
|
-
|
|
227
|
-
@balder.connect(Computer, over_connection=cnns.UsbConnection())
|
|
228
|
-
class AnotherDut(balder.Device):
|
|
229
|
-
# non-abstract subclass of `RecvMessageFeature`
|
|
230
|
-
recv = RecvUsbMessageFeature()
|
|
231
|
-
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
Now balder will run our scenario once each with the following mappings:
|
|
235
|
-
|
|
236
|
-
| | `ScenarioMessaging.Sender` | `ScenarioMessaging.Sender` |
|
|
237
|
-
|-------------|----------------------------|----------------------------|
|
|
238
|
-
| VARIATION 1 | `SetupOverUsb.Computer` | `SetupOverUsb.Dut` |
|
|
239
|
-
| VARIATION 2 | `SetupOverUsb.Dut` | `SetupOverUsb.Computer` |
|
|
240
|
-
| VARIATION 3 | `SetupOverUsb.Computer` | `SetupOverUsb.AnotherDut` |
|
|
241
|
-
|
|
242
|
-
If you want to test a `Dut` device that does not use USB for communication, you can also add other feature
|
|
243
|
-
implementations of ``SendMessageFeature`` and ``RecvMessageFeature`` in your setup devices. For this we just add a new
|
|
244
|
-
setup:
|
|
245
|
-
|
|
246
|
-
```python
|
|
247
|
-
import balder
|
|
248
|
-
from balder import connections as cnns
|
|
249
|
-
from .setup_features import SendUsbMessageFeature, RecvUsbMessageFeature, SendBluetoothMessageFeature, RecvBluetoothMessageFeature
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
class SetupOverUsb(balder.Setup):
|
|
253
|
-
|
|
254
|
-
...
|
|
255
|
-
|
|
256
|
-
class SetupOverBluetooth(balder.Setup):
|
|
257
|
-
class Computer(balder.Device):
|
|
258
|
-
# non-abstract subclass of `SendMessageFeature`
|
|
259
|
-
send = SendBluetoothMessageFeature()
|
|
260
|
-
|
|
261
|
-
@balder.connect(Computer, over_connection=cnns.BluetoothConnection)
|
|
262
|
-
class Dut(balder.Device):
|
|
263
|
-
# non-abstract subclass of `RecvMessageFeature`
|
|
264
|
-
recv = RecvBluetoothMessageFeature()
|
|
265
|
-
|
|
266
|
-
```
|
|
267
|
-
|
|
268
|
-
If you now execute Balder, it will run the scenario with all possible device constellations of the `SetupOverUsb` and
|
|
269
|
-
the `SetupOverBluetooth`.
|
|
270
|
-
|
|
271
|
-
| | `ScenarioMessaging.Sender` | `ScenarioMessaging.Sender` |
|
|
272
|
-
|-------------|---------------------------------|----------------------------|
|
|
273
|
-
| VARIATION 1 | `SetupOverUsb.Computer` | `SetupOverUsb.Dut` |
|
|
274
|
-
| VARIATION 2 | `SetupOverUsb.Dut` | `SetupOverUsb.Computer` |
|
|
275
|
-
| VARIATION 3 | `SetupOverUsb.Computer` | `SetupOverUsb.AnotherDut` |
|
|
276
|
-
| VARIATION 4 | `SetupOverBluetooth.Computer` | `SetupOverBluetooth.Dut` |
|
|
277
|
-
|
|
278
|
-
NOTE: *You could also add all of these devices in a shared setup and use one common feature for both protocols, but for this you would need to use VDevices. You can read more about this in [the documentation section about VDevices](https://docs.balder.dev/en/latest/basics/vdevices.html)*
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
# Example: Use an installable BalderHub package
|
|
282
|
-
|
|
283
|
-
With Balder you can create custom test environments or install open source available test packages, so called
|
|
284
|
-
[BalderHub packages](https://hub.balder.dev). If you want to test a SNMP client device for example, you can use the
|
|
285
|
-
[package balderhub-snmpagent](https://github.com/balder-dev/balderhub-snmpagent). Just install it with:
|
|
286
|
-
|
|
287
|
-
```
|
|
288
|
-
$ pip install balderhub-snmpagent
|
|
289
|
-
```
|
|
290
|
-
|
|
291
|
-
You only need to provide two things: The configuration of your DUT as subclass of `SnmpSystemConfig` and your
|
|
292
|
-
environment (the `Setup` class):
|
|
293
|
-
|
|
294
|
-
```python
|
|
295
|
-
# file `features.py`
|
|
296
|
-
from balderhub.snmpagent.lib import features
|
|
297
|
-
|
|
298
|
-
class MySnmpSystemConfig(features.SnmpSystemConfig):
|
|
299
|
-
|
|
300
|
-
host = "192.168.178.28"
|
|
301
|
-
sys_descr = "my fancy sysDescr"
|
|
302
|
-
sys_object_id = "1.3.6.1.4.1.1234.2.3.9.1"
|
|
303
|
-
read_community = "public"
|
|
304
|
-
write_community = "public"
|
|
305
|
-
|
|
306
|
-
```
|
|
307
|
-
|
|
308
|
-
```python
|
|
309
|
-
# file `setup_example.py`
|
|
310
|
-
import balder
|
|
311
|
-
from balderhub.snmpagent.lib.connections import SnmpConnection
|
|
312
|
-
from balderhub.snmpagent.lib.features import HasSnmpSystemGroupFeature
|
|
313
|
-
from balderhub.snmpagent.lib.setup_features import SendSnmpGetRequestPysnmpFeature, SendSnmpSetRequestPysnmpFeature
|
|
314
|
-
from . import features as setup_features
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
class SetupPrinter(balder.Setup):
|
|
318
|
-
|
|
319
|
-
class Printer(balder.Device):
|
|
320
|
-
_snmp_sys = HasSnmpSystemGroupFeature()
|
|
321
|
-
config = setup_features.SnmpSystemConfig()
|
|
322
|
-
|
|
323
|
-
@balder.connect(Printer, over_connection=SnmpConnection())
|
|
324
|
-
class HostPc(balder.Device):
|
|
325
|
-
get_request_snmp = SendSnmpGetRequestPysnmpFeature()
|
|
326
|
-
set_request_snmp = SendSnmpSetRequestPysnmpFeature()
|
|
327
|
-
|
|
328
|
-
```
|
|
329
|
-
|
|
330
|
-
Call Balder in your project:
|
|
331
|
-
|
|
332
|
-
```
|
|
333
|
-
$ balder
|
|
334
|
-
```
|
|
335
|
-
|
|
336
|
-
And all existing and matching tests in [balderhub-snmpagent](https://github.com/balder-dev/balderhub-snmpagent) will
|
|
337
|
-
then be executed for you:
|
|
338
|
-
|
|
339
|
-
```
|
|
340
|
-
+----------------------------------------------------------------------------------------------------------------------+
|
|
341
|
-
| BALDER Testsystem |
|
|
342
|
-
| python version 3.10.6 (main, Mar 10 2023, 10:55:28) [GCC 11.3.0] | balder version 0.1.0b6 |
|
|
343
|
-
+----------------------------------------------------------------------------------------------------------------------+
|
|
344
|
-
Collect 1 Setups and 3 Scenarios
|
|
345
|
-
resolve them to 3 mapping candidates
|
|
346
|
-
|
|
347
|
-
================================================== START TESTSESSION ===================================================
|
|
348
|
-
SETUP SetupPrinter
|
|
349
|
-
SCENARIO ScenarioMibSysDescr
|
|
350
|
-
VARIATION ScenarioMibSysDescr.SnmpAgent:SetupPrinter.Printer | ScenarioMibSysDescr.SnmpManager:SetupPrinter.HostPc
|
|
351
|
-
TEST ScenarioMibSysDescr.test_get_sys_descr [.]
|
|
352
|
-
TEST ScenarioMibSysDescr.test_get_sys_descr_ascii_check [.]
|
|
353
|
-
TEST ScenarioMibSysDescr.test_set_sys_descr [.]
|
|
354
|
-
SCENARIO ScenarioMibSysObjectId
|
|
355
|
-
VARIATION ScenarioMibSysObjectId.SnmpAgent:SetupPrinter.Printer | ScenarioMibSysObjectId.SnmpManager:SetupPrinter.HostPc
|
|
356
|
-
TEST ScenarioMibSysObjectId.test_get_sys_object_id [.]
|
|
357
|
-
TEST ScenarioMibSysObjectId.test_set_sys_object_id [.]
|
|
358
|
-
SCENARIO ScenarioMibSysUpTime
|
|
359
|
-
VARIATION ScenarioMibSysUpTime.SnmpAgent:SetupPrinter.Printer | ScenarioMibSysUpTime.SnmpManager:SetupPrinter.HostPc
|
|
360
|
-
TEST ScenarioMibSysUpTime.test_get_sys_up_time [.]
|
|
361
|
-
TEST ScenarioMibSysUpTime.test_get_sys_up_time_changed_check [.]
|
|
362
|
-
TEST ScenarioMibSysUpTime.test_set_sys_up_time [.]
|
|
363
|
-
================================================== FINISH TESTSESSION ==================================================
|
|
364
|
-
TOTAL NOT_RUN: 0 | TOTAL FAILURE: 0 | TOTAL ERROR: 0 | TOTAL SUCCESS: 8 | TOTAL SKIP: 0 | TOTAL COVERED_BY: 0
|
|
365
|
-
```
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
# Contribution guidelines
|
|
369
|
-
|
|
370
|
-
Any help is appreciated. If you want to contribute to balder, take a look into the
|
|
371
|
-
[contribution guidelines](https://github.com/balder-dev/balder/blob/main/CONTRIBUTING.md).
|
|
372
|
-
|
|
373
|
-
Balder is still in its early steps. Unfortunately, this also means that we don't have a broad variety of
|
|
374
|
-
[BalderHub projects](https://hub.balder.dev) at the moment.
|
|
375
|
-
|
|
376
|
-
Are you an expert in your field? Do you enjoy the concept of balder? How about you create your own
|
|
377
|
-
BalderHub project? Take a look into our [Balder GitHub Group](https://github.com/balder-dev) and feel free to share
|
|
378
|
-
your ideas. You can contribute to an existing project or create your own. If you are not sure, a project for your idea
|
|
379
|
-
already exists or if you want to discuss your ideas with others, feel free to
|
|
380
|
-
[create an issue in the BalderHub main entry project](https://github.com/balder-dev/hub.balder.dev/issues) or
|
|
381
|
-
[start a new discussion](https://github.com/balder-dev/hub.balder.dev/discussions).
|
|
382
|
-
|
|
383
|
-
# License
|
|
384
|
-
|
|
385
|
-
Balder is free and Open-Source
|
|
386
|
-
|
|
387
|
-
Copyright (c) 2022 Max Stahlschmidt and others
|
|
388
|
-
|
|
389
|
-
Distributed under the terms of the MIT license
|
|
File without changes
|
|
File without changes
|