baldertest 0.1.0b7__py3-none-any.whl → 0.1.0b9__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 CHANGED
@@ -1,4 +1,16 @@
1
1
  # file generated by setuptools_scm
2
2
  # don't change, don't track in version control
3
- __version__ = version = '0.1.0b7'
3
+ TYPE_CHECKING = False
4
+ if TYPE_CHECKING:
5
+ from typing import Tuple, Union
6
+ VERSION_TUPLE = Tuple[Union[int, str], ...]
7
+ else:
8
+ VERSION_TUPLE = object
9
+
10
+ version: str
11
+ __version__: str
12
+ __version_tuple__: VERSION_TUPLE
13
+ version_tuple: VERSION_TUPLE
14
+
15
+ __version__ = version = '0.1.0b9'
4
16
  __version_tuple__ = version_tuple = (0, 1, 0)
_balder/balder_session.py CHANGED
@@ -268,11 +268,6 @@ class BalderSession:
268
268
 
269
269
  self.parsed_args = self.cmd_arg_parser.parse_args(self._alt_cmd_args)
270
270
 
271
- if self.parsed_args.only_with_scenario is not None:
272
- raise NotImplementedError("the parameter `--only-with-scenario` is not supported in this version of balder")
273
- if self.parsed_args.only_with_setup is not None:
274
- raise NotImplementedError("the parameter `--only-with-setup` is not supported in this version of balder")
275
-
276
271
  self.working_dir = self.parsed_args.working_dir
277
272
  self.collect_only = self.parsed_args.collect_only
278
273
  self.resolve_only = self.parsed_args.resolve_only
@@ -285,7 +280,10 @@ class BalderSession:
285
280
  """
286
281
  This method collects all data.
287
282
  """
288
- self.collector.collect(plugin_manager=self.plugin_manager)
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)
289
287
 
290
288
  def solve(self):
291
289
  """
@@ -302,7 +300,8 @@ class BalderSession:
302
300
  .. note::
303
301
  Note that the method creates an :class:`ExecutorTree`, that hasn't to be completely resolved yet.
304
302
  """
305
- self.executor_tree = self.solver.get_executor_tree(plugin_manager=self.plugin_manager)
303
+ self.executor_tree = self.solver.get_executor_tree(plugin_manager=self.plugin_manager,
304
+ add_discarded=self.show_discarded)
306
305
  self.plugin_manager.execute_filter_executor_tree(executor_tree=self.executor_tree)
307
306
 
308
307
  def run(self):
_balder/collector.py CHANGED
@@ -5,6 +5,7 @@ import os
5
5
  import sys
6
6
  import types
7
7
  import logging
8
+ import fnmatch
8
9
  import inspect
9
10
  import pathlib
10
11
  import functools
@@ -677,27 +678,59 @@ class Collector:
677
678
  for cur_setup in self.all_setups:
678
679
  SetupController.get_for(cur_setup).validate_feature_possibility()
679
680
 
680
- def collect(self, plugin_manager: PluginManager):
681
+ def _filter_paths_after_allowed_paths(self, paths: List[pathlib.Path], filter_patterns: List[str]) \
682
+ -> List[pathlib.Path]:
683
+ """
684
+ This method filters the given list of filepaths for the given filter_patterns. It returns a list with all
685
+ remaining paths that are mathing the filter statements in `filter_paths`.
686
+
687
+ Patterns are the same like for `fnmatch <https://docs.python.org/3/library/fnmatch.html#module-fnmatch>`_.
688
+
689
+ :param paths: a list of all filepaths that should be filtered
690
+ :param filter_patterns: a list of relative filter patterns that should be applied on the files of the classes
691
+ :return: returns all classes of items that match any of the given patterns.
692
+ """
693
+ remaining = []
694
+
695
+ for cur_pattern in filter_patterns:
696
+ remaining += [cur_abs_path for cur_abs_path in paths
697
+ if fnmatch.fnmatch(str(cur_abs_path.relative_to(self.working_dir)), cur_pattern)]
698
+ return list(set(remaining))
699
+
700
+
701
+ def collect(self, plugin_manager: PluginManager, scenario_filter_patterns: Union[List[str], None],
702
+ setup_filter_patterns: Union[List[str], None]):
681
703
  """
682
704
  This method manages the entire collection process.
683
705
 
684
706
  :param plugin_manager: contains the reference to the used plugin manager
707
+ :param scenario_filter_patterns: a list with filter patterns for scenarios
708
+ :param setup_filter_patterns: a list with filter patterns for setups
685
709
  """
686
710
  # load all py files
687
711
  self.load_balderglob_py_file()
688
712
  self._all_py_files = self.get_all_py_files()
689
713
  self._all_py_files = plugin_manager.execute_modify_collected_pyfiles(self._all_py_files)
690
714
 
715
+ if scenario_filter_patterns:
716
+ all_scenario_filepaths = self._filter_paths_after_allowed_paths(
717
+ paths=self._all_py_files, filter_patterns=scenario_filter_patterns)
718
+ else:
719
+ all_scenario_filepaths = self._all_py_files
720
+ if setup_filter_patterns:
721
+ all_setup_filepaths = self._filter_paths_after_allowed_paths(
722
+ paths=self._all_py_files, filter_patterns=setup_filter_patterns)
723
+ else:
724
+ all_setup_filepaths = self._all_py_files
725
+
691
726
  # collect all `Connection` classes (has to be first, because scenarios/setups can use them)
692
727
  self.load_all_connection_classes(py_file_paths=self._all_py_files)
693
728
  self._all_connections = self.get_all_connection_classes()
694
729
 
695
730
  # collect all `Scenario` classes
696
- self._all_scenarios = self.get_all_scenario_classes(
697
- py_file_paths=self._all_py_files, filter_abstracts=True)
731
+ self._all_scenarios = self.get_all_scenario_classes(py_file_paths=all_scenario_filepaths, filter_abstracts=True)
698
732
  # collect all `Setup` classes
699
- self._all_setups = self.get_all_setup_classes(
700
- py_file_paths=self._all_py_files, filter_abstracts=True)
733
+ self._all_setups = self.get_all_setup_classes(py_file_paths=all_setup_filepaths, filter_abstracts=True)
701
734
 
702
735
  self._all_scenarios, self._all_setups = plugin_manager.execute_collected_classes(
703
736
  scenarios=self._all_scenarios, setups=self._all_setups)
_balder/connection.py CHANGED
@@ -88,6 +88,17 @@ class Connection:
88
88
  all_hashes += hash(cur_child)
89
89
  return hash(all_hashes)
90
90
 
91
+ def clone_without_based_on_elements(self) -> Connection:
92
+ """
93
+ This method returns a copied version of this element, while all `_based_on_connections` are removed (the copied
94
+ element has an empty list here).
95
+
96
+ :return: a python copied object of this item
97
+ """
98
+ self_copy = copy.copy(self)
99
+ self_copy._based_on_connections = [] # pylint: disable=protected-access
100
+ return self_copy
101
+
91
102
  def clone(self) -> Connection:
92
103
  """
93
104
  This method returns an exact clone of this connection. For this clone every inner connection object will be
@@ -95,18 +106,15 @@ class Connection:
95
106
  same for this object and the clone). The method will make a normal copy for every connection object in the
96
107
  `_based_on_elements` list.
97
108
  """
98
- self_copy = copy.copy(self)
99
- self_copy._based_on_connections = []
109
+ self_copy = self.clone_without_based_on_elements()
100
110
 
101
111
  for cur_based_on in self._based_on_connections:
102
112
  if isinstance(cur_based_on, tuple):
103
- cloned_tuple = ()
104
- for cur_tuple_element in cur_based_on:
105
- cloned_tuple = cloned_tuple + (cur_tuple_element.clone(), )
106
- self_copy._based_on_connections.append(cloned_tuple)
113
+ cloned_tuple = tuple([cur_tuple_element.clone() for cur_tuple_element in cur_based_on])
114
+ self_copy.append_to_based_on(cloned_tuple)
107
115
  elif isinstance(cur_based_on, Connection):
108
116
  cloned_cur_based_on = cur_based_on.clone()
109
- self_copy._based_on_connections.append(cloned_cur_based_on)
117
+ self_copy.append_to_based_on(cloned_cur_based_on)
110
118
  else:
111
119
  raise TypeError('based on element is not from valid type')
112
120
  return self_copy
@@ -132,22 +140,21 @@ class Connection:
132
140
  for cur_tuple in Connection.__cut_tuple_from_only_parent_to_child(elem.based_on_elements[0]):
133
141
  # for this we do not have to use `clone`, because we copy the base object with `copy.copy` and
134
142
  # completely replace the `_based_on_connections` by our own
135
- copied_conn = copy.copy(elem)
136
- copied_conn._based_on_connections = [cur_tuple]
143
+ copied_conn = elem.clone_without_based_on_elements()
144
+ copied_conn.append_to_based_on(cur_tuple)
137
145
  all_pieces.append(copied_conn)
138
146
  return all_pieces
139
147
  # if this element is the last element with a parent -> copy it and remove the parent, return it
140
148
  if len(elem.based_on_elements[0].based_on_elements) == 0:
141
- new_elem = copy.copy(elem)
142
- new_elem._based_on_connections = []
149
+ new_elem = elem.clone_without_based_on_elements()
143
150
  all_pieces.append(new_elem)
144
151
  return all_pieces
145
152
  # otherwise, the current item has grandparents, so call the method recursively for parents and add a copy of
146
153
  # this object as child
147
154
  all_possible_parents = Connection.__cut_conn_from_only_parent_to_child(elem.based_on_elements[0])
148
155
  for cur_parent in all_possible_parents:
149
- copied_conn = copy.copy(elem)
150
- copied_conn._based_on_connections = [cur_parent]
156
+ copied_conn = elem.clone_without_based_on_elements()
157
+ copied_conn.append_to_based_on(cur_parent)
151
158
  all_pieces.append(copied_conn)
152
159
  return all_pieces
153
160
 
@@ -165,15 +172,12 @@ class Connection:
165
172
  Note that this method also returns every possible ordering
166
173
  """
167
174
 
168
- tuple_with_all_possibilities = ()
169
- for cur_tuple_item in elem:
170
- tuple_with_all_possibilities += (Connection.__cut_conn_from_only_parent_to_child(cur_tuple_item), )
175
+ tuple_with_all_possibilities = (
176
+ tuple([Connection.__cut_conn_from_only_parent_to_child(cur_tuple_item) for cur_tuple_item in elem]))
171
177
 
172
178
  cloned_tuple_list = []
173
179
  for cur_tuple in list(itertools.product(*tuple_with_all_possibilities)):
174
- cloned_tuple: Tuple[Connection] = ()
175
- for cur_tuple_item in cur_tuple:
176
- cloned_tuple += (cur_tuple_item.clone(), )
180
+ cloned_tuple = tuple([cur_tuple_item.clone() for cur_tuple_item in cur_tuple])
177
181
  cloned_tuple_list.append(cloned_tuple)
178
182
  return cloned_tuple_list
179
183
 
@@ -215,11 +219,9 @@ class Connection:
215
219
  while len(cur_sub_other_single) > 0:
216
220
  if isinstance(cur_sub_other_single[0], tuple):
217
221
  # found a tuple -> check if length does match
218
- if len(cur_sub_other_single[0]) >= len(cur_tuple_single):
219
- # length does match
220
- if tuple_is_contained_in_other(
221
- cur_tuple_single, contained_in_tuple=cur_sub_other_single[0]):
222
- return True
222
+ if len(cur_sub_other_single[0]) >= len(cur_tuple_single) and tuple_is_contained_in_other(
223
+ cur_tuple_single, contained_in_tuple=cur_sub_other_single[0]):
224
+ return True
223
225
  # otherwise, this complete element is not possible - skip this single!
224
226
  break
225
227
  cur_sub_other_single = cur_sub_other_single[0].based_on_elements
@@ -371,6 +373,75 @@ class Connection:
371
373
  this_instance.append_to_based_on(*new_items)
372
374
  return this_instance
373
375
 
376
+ @classmethod
377
+ def check_equal_connections_are_in(
378
+ cls, cnns_from: List[Connection], are_in_cnns_from: List[Connection], ignore_metadata: bool=False) -> bool:
379
+ """
380
+ This method validates that the elements from the first list are contained (are equal) with one of the elements
381
+ in the second list.
382
+
383
+ .. note::
384
+ This method only checks that every single connections from the first element is contained in the second too.
385
+ It does not check the other direction. If you want to validate this, you need to call this method with both
386
+ possibilities.
387
+
388
+ :param cnns_from: the first list of connections
389
+ :param are_in_cnns_from: the second list of connection
390
+ :param ignore_metadata: True, if the metadata of the single connections should be ignored
391
+ :return: True in case that every connection of the first list is equal with one in the second list, otherwise
392
+ False
393
+ """
394
+ for cur_cnn in cnns_from:
395
+ found_equal = False
396
+ for cur_other_cnn in are_in_cnns_from:
397
+ if cur_cnn.equal_with(cur_other_cnn, ignore_metadata=ignore_metadata):
398
+ found_equal = True
399
+ break
400
+ if not found_equal:
401
+ return False
402
+ return True
403
+
404
+ @classmethod
405
+ def check_equal_connection_tuples_are_in(
406
+ cls, tuples_from: List[Tuple[Connection]], are_in_tuples_from: List[Tuple[Connection]],
407
+ ignore_metadata: bool=False) -> bool:
408
+ """
409
+ This method validates that the connection tuples from the first list are contained (are equal) within the tuple
410
+ elements of the second list.
411
+
412
+ .. note::
413
+ This method only checks that every single tuple from the first element is contained in the second list.
414
+ It does not check the other direction. If you want to validate this, you need to call this method with both
415
+ possibilities.
416
+
417
+ :param tuples_from: the first list of connection-tuples
418
+ :param are_in_tuples_from: the second list of connection-tuples
419
+ :param ignore_metadata: True, if the metadata of the single connections should be ignored
420
+ :return: True in case that every tuple connections of the first list is equal with one tuple in the second,
421
+ otherwise False
422
+ """
423
+ for cur_search_tuple in tuples_from:
424
+ found_match_for_cur_search_tuple = False
425
+ # go through each unmatched other tuple
426
+ for cur_other_tuple in are_in_tuples_from:
427
+ cur_search_tuple_is_completely_in_other_tuple = True
428
+ for cur_search_tuple_elem in cur_search_tuple:
429
+ fount_it = False
430
+ for cur_other_tuple_elem in cur_other_tuple:
431
+ if cur_search_tuple_elem.equal_with(cur_other_tuple_elem, ignore_metadata=ignore_metadata):
432
+ fount_it = True
433
+ break
434
+ if not fount_it:
435
+ cur_search_tuple_is_completely_in_other_tuple = False
436
+ break
437
+ if cur_search_tuple_is_completely_in_other_tuple:
438
+ # here we have no match
439
+ found_match_for_cur_search_tuple = True
440
+ break
441
+ if not found_match_for_cur_search_tuple:
442
+ return False
443
+ return True
444
+
374
445
  # ---------------------------------- PROPERTIES --------------------------------------------------------------------
375
446
 
376
447
  @property
@@ -484,7 +555,7 @@ class Connection:
484
555
 
485
556
  return False
486
557
 
487
- def _get_intersection_with_other_single(self, other_conn: Union[Connection, Tuple[Connection]]) \
558
+ def get_intersection_with_other_single(self, other_conn: Union[Connection, Tuple[Connection]]) \
488
559
  -> List[Connection, Tuple[Connection]]:
489
560
  """
490
561
  A helper method that returns an intersection between the two connections (self and the given one).
@@ -519,27 +590,23 @@ class Connection:
519
590
 
520
591
  intersection = []
521
592
 
593
+ def determine_for(pieces: List[Union[Connection, Tuple[Connection]]], in_other_cnn: Connection):
594
+ for cur_piece in pieces:
595
+ if isinstance(cur_piece, Connection):
596
+ if cur_piece.contained_in(in_other_cnn, ignore_metadata=True):
597
+ intersection.append(cur_piece)
598
+ else:
599
+ # isinstance of tuple
600
+ if Connection.check_if_tuple_contained_in_connection(cur_piece, in_other_cnn):
601
+ intersection.append(cur_piece)
602
+
522
603
  #: check if some sub elements of self connection are contained in `other_conn`
523
604
  self_pieces = self.cut_into_all_possible_subtrees()
524
- for cur_piece in self_pieces:
525
- if isinstance(cur_piece, Connection):
526
- if cur_piece.contained_in(other_conn, ignore_metadata=True):
527
- intersection.append(cur_piece)
528
- else:
529
- # isinstance of tuple
530
- if Connection.check_if_tuple_contained_in_connection(cur_piece, other_conn):
531
- intersection.append(cur_piece)
605
+ determine_for(pieces=self_pieces, in_other_cnn=other_conn)
532
606
 
533
607
  #: check if some sub elements of `other_conn` are contained in self connection
534
608
  other_pieces = other_conn.cut_into_all_possible_subtrees()
535
- for cur_piece in other_pieces:
536
- if isinstance(cur_piece, Connection):
537
- if cur_piece.contained_in(self, ignore_metadata=True):
538
- intersection.append(cur_piece)
539
- else:
540
- # isinstance of tuple
541
- if Connection.check_if_tuple_contained_in_connection(cur_piece, self):
542
- intersection.append(cur_piece)
609
+ determine_for(pieces=other_pieces, in_other_cnn=self)
543
610
 
544
611
  #: filter all duplicated (and contained in each other) connections
545
612
  intersection_without_duplicates = []
@@ -779,19 +846,19 @@ class Connection:
779
846
  method returns this child connection directly without any :class:`Connection` container otherwise the
780
847
  :class:`Connection` container class with all resolved child classes will be returned.
781
848
  """
782
- copied_base = copy.copy(self)
783
- if not copied_base.is_resolved():
784
- copied_base._based_on_connections = []
849
+ if self.is_resolved():
850
+ copied_base = self.clone()
851
+ else:
852
+ copied_base = self.clone_without_based_on_elements()
853
+
785
854
  if self.__class__ == Connection:
786
855
  # the base object is a container Connection - iterate over the items and determine the values for them
787
856
  for cur_item in self.based_on_elements.copy():
788
857
  if isinstance(cur_item, tuple):
789
- new_tuple = ()
790
- for cur_tuple_item in cur_item:
791
- new_tuple += (cur_tuple_item.get_resolved(), )
792
- copied_base._based_on_connections.append(new_tuple)
858
+ new_tuple = tuple([cur_tuple_item.get_resolved() for cur_tuple_item in cur_item])
859
+ copied_base.append_to_based_on(new_tuple)
793
860
  else:
794
- copied_base._based_on_connections.append(cur_item.get_resolved())
861
+ copied_base.append_to_based_on(cur_item.get_resolved())
795
862
  else:
796
863
  for next_higher_parent in self.based_on_elements:
797
864
  if isinstance(next_higher_parent, tuple):
@@ -811,21 +878,20 @@ class Connection:
811
878
  # resolve the opportunities and create multiple possible tuples where all elements are direct
812
879
  # parents
813
880
  for cur_possibility in itertools.product(*direct_ancestors_tuple):
814
- new_child_tuple = ()
815
- for cur_tuple_element in cur_possibility:
816
- new_child_tuple += (cur_tuple_element.get_resolved(), )
817
- copied_base._based_on_connections.append(new_child_tuple)
881
+ new_child_tuple = (
882
+ tuple([cur_tuple_element.get_resolved() for cur_tuple_element in cur_possibility]))
883
+ copied_base.append_to_based_on(new_child_tuple)
818
884
  else:
819
885
  if next_higher_parent.__class__ in self.__class__.get_parents():
820
886
  # is already a direct parent
821
- copied_base._based_on_connections.append(next_higher_parent.get_resolved())
887
+ copied_base.append_to_based_on(next_higher_parent.get_resolved())
822
888
  else:
823
889
  # only add the first level of direct parents - deeper will be added by recursively call of
824
890
  # `get_resolved`
825
891
  for cur_self_direct_parent in self.__class__.get_parents():
826
892
  if next_higher_parent.__class__.is_parent_of(cur_self_direct_parent):
827
893
  new_child = cur_self_direct_parent.based_on(next_higher_parent)
828
- copied_base._based_on_connections.append(new_child.get_resolved())
894
+ copied_base.append_to_based_on(new_child.get_resolved())
829
895
  # if it is a connection container, where only one element exists that is no tuple -> return this directly
830
896
  # instead of the container
831
897
  if copied_base.__class__ == Connection and len(copied_base.based_on_elements) == 1 and not \
@@ -865,8 +931,8 @@ class Connection:
865
931
  return [resolved_obj]
866
932
  for cur_child in resolved_obj.based_on_elements:
867
933
  for cur_single_child in cur_child.get_singles():
868
- copied_base = copy.copy(resolved_obj)
869
- copied_base._based_on_connections = [cur_single_child]
934
+ copied_base = resolved_obj.clone_without_based_on_elements()
935
+ copied_base.append_to_based_on(cur_single_child)
870
936
  all_singles.append(copied_base)
871
937
  # convert all tuple objects in a connection object and set metadata of self object
872
938
  cleaned_singles = []
@@ -979,40 +1045,6 @@ class Connection:
979
1045
  resolved_self = self.get_resolved()
980
1046
  resolved_other = other_conn.get_resolved()
981
1047
 
982
- def check_elems_if(elems_from, are_in_elems_from):
983
- for cur_elem in elems_from:
984
- found_equal = False
985
- for cur_other_elem in are_in_elems_from:
986
- if cur_elem.equal_with(cur_other_elem, ignore_metadata=ignore_metadata):
987
- found_equal = True
988
- break
989
- if not found_equal:
990
- return False
991
- return True
992
-
993
- def check_tuples_if(tuples_from, are_in_tuples_from):
994
- for cur_search_tuple in tuples_from:
995
- found_match_for_cur_search_tuple = False
996
- # go through each unmatched other tuple
997
- for cur_other_tuple in are_in_tuples_from:
998
- cur_search_tuple_is_completely_in_other_tuple = True
999
- for cur_search_tuple_elem in cur_search_tuple:
1000
- fount_it = False
1001
- for cur_other_tuple_elem in cur_other_tuple:
1002
- if cur_search_tuple_elem.equal_with(cur_other_tuple_elem, ignore_metadata=ignore_metadata):
1003
- fount_it = True
1004
- break
1005
- if not fount_it:
1006
- cur_search_tuple_is_completely_in_other_tuple = False
1007
- break
1008
- if cur_search_tuple_is_completely_in_other_tuple:
1009
- # here we have no match
1010
- found_match_for_cur_search_tuple = True
1011
- break
1012
- if not found_match_for_cur_search_tuple:
1013
- return False
1014
- return True
1015
-
1016
1048
  self_based_on_elems = [
1017
1049
  cur_elem for cur_elem in resolved_self.based_on_elements if isinstance(cur_elem, Connection)]
1018
1050
  self_based_on_tuples = [
@@ -1023,13 +1055,20 @@ class Connection:
1023
1055
  cur_elem for cur_elem in resolved_other.based_on_elements if isinstance(cur_elem, tuple)]
1024
1056
 
1025
1057
  # check single connection elements (if they match all in both directions)
1026
- if not check_elems_if(elems_from=self_based_on_elems, are_in_elems_from=other_based_on_elems) or \
1027
- not check_elems_if(elems_from=other_based_on_elems, are_in_elems_from=self_based_on_elems):
1058
+ if (not self.__class__.check_equal_connections_are_in(
1059
+ cnns_from=self_based_on_elems, are_in_cnns_from=other_based_on_elems, ignore_metadata=ignore_metadata)
1060
+ or not self.__class__.check_equal_connections_are_in(
1061
+ cnns_from=other_based_on_elems, are_in_cnns_from=self_based_on_elems,
1062
+ ignore_metadata=ignore_metadata)):
1028
1063
  return False
1029
1064
 
1030
1065
  # check tuple connection elements (if they match all in both directions)
1031
- if not check_tuples_if(tuples_from=self_based_on_tuples, are_in_tuples_from=other_based_on_tuples) or \
1032
- not check_tuples_if(tuples_from=other_based_on_tuples, are_in_tuples_from=self_based_on_tuples):
1066
+ if (not self.__class__.check_equal_connection_tuples_are_in(
1067
+ tuples_from=self_based_on_tuples, are_in_tuples_from=other_based_on_tuples,
1068
+ ignore_metadata=ignore_metadata)
1069
+ or not self.__class__.check_equal_connection_tuples_are_in(
1070
+ tuples_from=other_based_on_tuples, are_in_tuples_from=self_based_on_tuples,
1071
+ ignore_metadata=ignore_metadata)):
1033
1072
  return False
1034
1073
 
1035
1074
  return True
@@ -1208,7 +1247,7 @@ class Connection:
1208
1247
  # determine intersections between all of these single components
1209
1248
  for cur_self_conn in self_conn_singles:
1210
1249
  for cur_other_conn in other_conn_singles:
1211
- for cur_intersection in cur_self_conn._get_intersection_with_other_single(cur_other_conn):
1250
+ for cur_intersection in cur_self_conn.get_intersection_with_other_single(cur_other_conn):
1212
1251
  if isinstance(cur_intersection, tuple):
1213
1252
  cur_intersection = Connection.based_on(cur_intersection)
1214
1253
  if cur_intersection not in intersections:
@@ -1236,7 +1275,8 @@ class Connection:
1236
1275
 
1237
1276
  return intersection_filtered[0].clone()
1238
1277
 
1239
- def append_to_based_on(self, *args: Union[Tuple[Union[Type[Connection], Connection]]]) -> None:
1278
+ def append_to_based_on(
1279
+ self, *args: Union[Connection, Type[Connection], Tuple[Union[Type[Connection], Connection]]]) -> None:
1240
1280
  """
1241
1281
  with this method you can extend the internal based_on list with the transferred elements. Any number of
1242
1282
  :meth:`Connection` objects or tuples with :meth:`Connection` objects can be given to this method
@@ -129,6 +129,39 @@ class FeatureController(Controller):
129
129
  return [Connection()]
130
130
  return intersection
131
131
 
132
+ def _determine_all_theoretically_unordered_method_variations(
133
+ self, of_method_name: str, for_vdevice: Type[VDevice],
134
+ with_connection: Union[Connection, Tuple[Connection]]) -> Dict[Callable, Connection]:
135
+ """
136
+ This method returns all theoretically matching method variations. It returns more than one, if there are
137
+ multiple method variation for the given VDevice in this feature, where the given connection is part of the
138
+ connection described by the method variation.
139
+
140
+ :param of_method_name: the name of the method that should be returned
141
+ :param for_vdevice: the VDevice that is mapped
142
+ :param with_connection: the connection that is used between the device that uses the related feature and the
143
+ VDevice
144
+ :return: a dictionary that holds all available method variation that matches here
145
+ """
146
+ all_possible_method_variations = {}
147
+ for cur_impl_method, cur_method_impl_dict in self.get_method_based_for_vdevice()[of_method_name].items():
148
+ if for_vdevice in cur_method_impl_dict.keys():
149
+ cur_impl_method_cnns = []
150
+ for cur_cnn in cur_method_impl_dict[for_vdevice]:
151
+ cur_impl_method_cnns += cur_cnn.get_singles()
152
+ for cur_single_impl_method_cnn in cur_impl_method_cnns:
153
+ if cur_single_impl_method_cnn.contained_in(with_connection, ignore_metadata=True):
154
+ # this variation is possible
155
+ # ADD IT if it is not available yet
156
+ if cur_impl_method not in all_possible_method_variations.keys():
157
+ all_possible_method_variations[cur_impl_method] = cur_single_impl_method_cnn
158
+ # COMBINE IT if it is already available
159
+ else:
160
+ all_possible_method_variations[cur_impl_method] = Connection.based_on(
161
+ all_possible_method_variations[cur_impl_method], cur_single_impl_method_cnn)
162
+ return all_possible_method_variations
163
+
164
+
132
165
  # ---------------------------------- METHODS -----------------------------------------------------------------------
133
166
 
134
167
  def get_class_based_for_vdevice(self) -> Union[Dict[Type[VDevice], List[Connection]], None]:
@@ -321,24 +354,10 @@ class FeatureController(Controller):
321
354
  raise ValueError(f"can not find the method `{of_method_name}` in method variation data dictionary")
322
355
 
323
356
  # first determine all possible method-variations
324
- all_possible_method_variations = {}
325
- for cur_impl_method, cur_method_impl_dict in self.get_method_based_for_vdevice()[of_method_name].items():
326
- if for_vdevice in cur_method_impl_dict.keys():
327
- cur_impl_method_cnns = []
328
- for cur_cnn in cur_method_impl_dict[for_vdevice]:
329
- cur_impl_method_cnns += cur_cnn.get_singles()
330
- for cur_single_impl_method_cnn in cur_impl_method_cnns:
331
- if cur_single_impl_method_cnn.contained_in(with_connection, ignore_metadata=True):
332
- # this variation is possible
333
- # ADD IT if it is not available yet
334
- if cur_impl_method not in all_possible_method_variations.keys():
335
- all_possible_method_variations[cur_impl_method] = cur_single_impl_method_cnn
336
- # COMBINE IT if it is already available
337
- else:
338
- all_possible_method_variations[cur_impl_method] = Connection.based_on(
339
- all_possible_method_variations[cur_impl_method], cur_single_impl_method_cnn)
357
+ all_possible_method_variations = self._determine_all_theoretically_unordered_method_variations(
358
+ of_method_name=of_method_name, for_vdevice=for_vdevice, with_connection=with_connection)
340
359
 
341
- # if there are more than one possible method variation, try to sort them hierarchical
360
+ # there are no method variations in this feature directly -> check parent classes
342
361
  if len(all_possible_method_variations) == 0:
343
362
  # try to execute this method in parent classes
344
363
  for cur_base in self.related_cls.__bases__:
@@ -355,9 +374,11 @@ class FeatureController(Controller):
355
374
  f"and usable connection `{with_connection.get_tree_str()}´")
356
375
  return None
357
376
 
377
+ # we only have one -> selection is clear
358
378
  if len(all_possible_method_variations) == 1:
359
379
  return list(all_possible_method_variations.keys())[0]
360
380
 
381
+ # if there are more than one possible method variation, try to sort them hierarchical
361
382
  # we have to determine the outer one
362
383
  length_before = None
363
384
  while length_before is None or length_before != len(all_possible_method_variations):
@@ -188,7 +188,7 @@ class NormalScenarioSetupController(Controller, ABC):
188
188
 
189
189
  devices = self.get_all_inner_device_classes()
190
190
  abs_parent_devices = parent_scenario_or_setup_controller.get_all_abs_inner_device_classes()
191
- abs_parent_devices_as_names = [cur_parent.__name__ for cur_parent in abs_parent_devices]
191
+ abs_parent_devices_by_name = {cur_parent.__name__: cur_parent for cur_parent in abs_parent_devices}
192
192
 
193
193
  if len(devices) == 0:
194
194
  # ignore it because cur item has no own device definitions
@@ -197,10 +197,7 @@ class NormalScenarioSetupController(Controller, ABC):
197
197
  # check that a device is newly defined or has the same name as the parent device
198
198
  for cur_item_device in devices:
199
199
  # check if name exists in parent
200
- relevant_parent_according_naming = None
201
- if cur_item_device.__name__ in abs_parent_devices_as_names:
202
- relevant_parent_according_naming = \
203
- abs_parent_devices[abs_parent_devices_as_names.index(cur_item_device.__name__)]
200
+ relevant_parent_according_naming = abs_parent_devices_by_name.get(cur_item_device.__name__, None)
204
201
 
205
202
  # check if device is inherited from a parent
206
203
  relevant_parent_device_according_inheritance = None
@@ -236,11 +233,7 @@ class NormalScenarioSetupController(Controller, ABC):
236
233
 
237
234
  # secure that all parent devices are implemented here too
238
235
  for cur_parent in abs_parent_devices:
239
- found_parent = False
240
- for cur_item_device in devices:
241
- if issubclass(cur_item_device, cur_parent):
242
- found_parent = True
243
- break
236
+ found_parent = len([dev for dev in devices if issubclass(dev, cur_parent)]) > 0
244
237
  if not found_parent:
245
238
  raise DeviceOverwritingError(
246
239
  f"found a device `{cur_parent.__qualname__}` which is part of a parent class, but it is "
@@ -1,13 +1,13 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import time
4
- from typing import List, Dict, Union, Type, TYPE_CHECKING
4
+ from typing import List, Union, Type, TYPE_CHECKING
5
5
 
6
6
  import sys
7
7
  import types
8
8
  import traceback
9
9
  from abc import ABC, abstractmethod
10
- from _balder.testresult import FixturePartResult, ResultState
10
+ from _balder.testresult import FixturePartResult, ResultState, ResultSummary
11
11
  from _balder.previous_executor_mark import PreviousExecutorMark
12
12
  from _balder.testresult import TestcaseResult
13
13
 
@@ -158,8 +158,7 @@ class BasicExecutor(ABC):
158
158
  if cur_child.has_runnable_tests(consider_discarded_too):
159
159
  return True
160
160
  return False
161
- else:
162
- return True
161
+ return True
163
162
 
164
163
  def get_all_base_instances_of_this_branch(
165
164
  self, with_type: Union[Type[Setup], Type[Scenario], Type[types.FunctionType]],
@@ -193,10 +192,12 @@ class BasicExecutor(ABC):
193
192
  return list(set(result))
194
193
 
195
194
  @abstractmethod
196
- def cleanup_empty_executor_branches(self):
195
+ def cleanup_empty_executor_branches(self, consider_discarded=False):
197
196
  """
198
197
  This method searches the whole tree and removes branches where an executor item has no own children. It can
199
198
  remove these branches, because they have no valid matchings.
199
+
200
+ :param consider_discarded: true if this method should consider discarded branches, otherwise False
200
201
  """
201
202
 
202
203
  def filter_tree_for_user_filters(self):
@@ -206,31 +207,19 @@ class BasicExecutor(ABC):
206
207
  for cur_child_executor in self.all_child_executors:
207
208
  cur_child_executor.filter_tree_for_user_filters()
208
209
 
209
- def testsummary(self) -> Dict[ResultState, int]:
210
+ def testsummary(self) -> ResultSummary:
210
211
  """
211
212
  returns a dictionary with all possible :class:`ResultState` as keys and the number of times they have occurred
212
213
  in this branch as values
213
214
  """
214
215
 
215
- summary = {
216
- ResultState.NOT_RUN: 0,
217
- ResultState.FAILURE: 0,
218
- ResultState.ERROR: 0,
219
- ResultState.SUCCESS: 0,
220
- ResultState.SKIP: 0,
221
- ResultState.COVERED_BY: 0
222
- }
216
+ summary = ResultSummary()
223
217
 
224
218
  if isinstance(self.body_result, TestcaseResult):
225
- summary[self.executor_result] = 1
219
+ setattr(summary, self.executor_result.value, 1)
226
220
  else:
227
221
  for cur_child_exec in self.all_child_executors:
228
- cur_child_dict = cur_child_exec.testsummary()
229
- for cur_key in cur_child_dict.keys():
230
- if cur_key in summary:
231
- summary[cur_key] += cur_child_dict[cur_key]
232
- else:
233
- summary[cur_key] = cur_child_dict[cur_key]
222
+ summary += cur_child_exec.testsummary()
234
223
  return summary
235
224
 
236
225
  def execute(self, show_discarded=False):
@@ -1,10 +1,11 @@
1
1
  from __future__ import annotations
2
2
  from typing import Union, List, Type, TYPE_CHECKING
3
3
 
4
+ from dataclasses import fields
4
5
  from _balder.executor.setup_executor import SetupExecutor
5
6
  from _balder.executor.basic_executor import BasicExecutor
6
7
  from _balder.fixture_manager import FixtureManager
7
- from _balder.testresult import ResultState, BranchBodyResult
8
+ from _balder.testresult import ResultState, BranchBodyResult, ResultSummary
8
9
  from _balder.previous_executor_mark import PreviousExecutorMark
9
10
 
10
11
  if TYPE_CHECKING:
@@ -55,7 +56,8 @@ class ExecutorTree(BasicExecutor):
55
56
  # ---------------------------------- PROTECTED METHODS -------------------------------------------------------------
56
57
 
57
58
  def _prepare_execution(self, show_discarded):
58
- pass
59
+ if not show_discarded:
60
+ self.update_inner_feature_reference_in_all_setups()
59
61
 
60
62
  def _body_execution(self, show_discarded):
61
63
  for cur_setup_executor in self.get_setup_executors():
@@ -129,16 +131,26 @@ class ExecutorTree(BasicExecutor):
129
131
  # can not find some
130
132
  return None
131
133
 
132
- def cleanup_empty_executor_branches(self):
134
+ def cleanup_empty_executor_branches(self, consider_discarded=False):
133
135
  to_remove_executor = []
134
136
  for cur_setup_executor in self.get_setup_executors():
135
- cur_setup_executor.cleanup_empty_executor_branches()
137
+ cur_setup_executor.cleanup_empty_executor_branches(consider_discarded=consider_discarded)
136
138
  if len(cur_setup_executor.get_scenario_executors()) == 0:
137
139
  # remove this whole executor because it has no children anymore
138
140
  to_remove_executor.append(cur_setup_executor)
139
141
  for cur_setup_executor in to_remove_executor:
140
142
  self._setup_executors.remove(cur_setup_executor)
141
143
 
144
+ def update_inner_feature_reference_in_all_setups(self):
145
+ """
146
+ This method iterates over all setups and triggers the method that updates the inner feature references.
147
+
148
+ This needs to be done on ExecutorTree level, because this reference should also be accessible within session
149
+ fixtures (in case they have setup scope).
150
+ """
151
+ for cur_setup_executor in self.get_setup_executors():
152
+ cur_setup_executor.update_inner_referenced_feature_instances()
153
+
142
154
  def execute(self, show_discarded=False) -> None:
143
155
  """
144
156
  This method executes this branch of the tree
@@ -163,12 +175,12 @@ class ExecutorTree(BasicExecutor):
163
175
  print_line(end_text)
164
176
  summary = self.testsummary()
165
177
  is_first = True
166
- for cur_key, cur_val in summary.items():
178
+ for cur_field in fields(ResultSummary):
167
179
  if is_first:
168
180
  is_first = False
169
181
  else:
170
182
  print(" | ", end="")
171
- print(f"TOTAL {cur_key.value}: {cur_val}", end="")
183
+ print(f"TOTAL {cur_field.name}: {getattr(summary, cur_field.name)}", end="")
172
184
  print("")
173
185
 
174
186
  def print_tree(self, show_discarded=False) -> None:
@@ -192,7 +204,8 @@ class ExecutorTree(BasicExecutor):
192
204
  for cur_key, cur_val in mapping_printings.items():
193
205
  print(("{} {:<" + str(max_len) + "} = {}").format(start_char, cur_key, cur_val))
194
206
  for cur_testcase_excutor in cur_variation_executor.get_testcase_executors():
195
- print(f"{start_char} -> Testcase<{cur_testcase_excutor.base_testcase_callable.__qualname__}>")
207
+ print(f"{start_char} -> Testcase<"
208
+ f"{cur_testcase_excutor.base_testcase_callable.__qualname__}>")
196
209
  if cur_variation_executor.prev_mark == PreviousExecutorMark.DISCARDED:
197
210
  print(f"{start_char}")
198
211
  print(f"{start_char} DISCARDED BECAUSE "
@@ -6,6 +6,7 @@ from _balder.utils import get_class_that_defines_method
6
6
  from _balder.executor.basic_executor import BasicExecutor
7
7
  from _balder.executor.variation_executor import VariationExecutor
8
8
  from _balder.previous_executor_mark import PreviousExecutorMark
9
+ from _balder.controllers.scenario_controller import ScenarioController
9
10
 
10
11
  if TYPE_CHECKING:
11
12
  from _balder.scenario import Scenario
@@ -64,6 +65,10 @@ class ScenarioExecutor(BasicExecutor):
64
65
  """returns the :class:`Scenario` class that belongs to this executor"""
65
66
  return self._base_scenario_class
66
67
 
68
+ @property
69
+ def base_scenario_controller(self) -> ScenarioController:
70
+ return ScenarioController.get_for(self.base_scenario_class.__class__)
71
+
67
72
  @property
68
73
  def fixture_manager(self) -> FixtureManager:
69
74
  """returns the current active fixture manager that belongs to this scenario executor"""
@@ -116,12 +121,12 @@ class ScenarioExecutor(BasicExecutor):
116
121
  if cur_executor.prev_mark != PreviousExecutorMark.DISCARDED]
117
122
  return self._variation_executors
118
123
 
119
- def cleanup_empty_executor_branches(self):
124
+ def cleanup_empty_executor_branches(self, consider_discarded=False):
120
125
  """
121
126
  This method removes all sub executors that are empty and not relevant anymore.
122
127
  """
123
128
  to_remove_executor = []
124
- for cur_variation_executor in self.get_variation_executors():
129
+ for cur_variation_executor in self.get_variation_executors(return_discarded=consider_discarded):
125
130
  if len(cur_variation_executor.get_testcase_executors()) == 0:
126
131
  # remove this whole executor because it has no children anymore
127
132
  to_remove_executor.append(cur_variation_executor)
@@ -5,6 +5,9 @@ from _balder.testresult import ResultState, BranchBodyResult
5
5
  from _balder.executor.basic_executor import BasicExecutor
6
6
  from _balder.executor.scenario_executor import ScenarioExecutor
7
7
  from _balder.previous_executor_mark import PreviousExecutorMark
8
+ from _balder.controllers.setup_controller import SetupController
9
+ from _balder.controllers.device_controller import DeviceController
10
+ from _balder.controllers.feature_controller import FeatureController
8
11
 
9
12
  if TYPE_CHECKING:
10
13
  from _balder.setup import Setup
@@ -58,6 +61,10 @@ class SetupExecutor(BasicExecutor):
58
61
  """returns the base :class:`Setup` that belongs to this executor"""
59
62
  return self._base_setup_class
60
63
 
64
+ @property
65
+ def base_setup_controller(self) -> SetupController:
66
+ return SetupController.get_for(self.base_setup_class.__class__)
67
+
61
68
  @property
62
69
  def fixture_manager(self) -> FixtureManager:
63
70
  """returns the current active fixture manager for this executor"""
@@ -84,15 +91,43 @@ class SetupExecutor(BasicExecutor):
84
91
 
85
92
  # ---------------------------------- METHODS -----------------------------------------------------------------------
86
93
 
94
+ def update_inner_referenced_feature_instances(self):
95
+ """
96
+ This method ensures that all inner referenced feature instances of all devices that are used in this setup are
97
+ set to the correct feature instance. For every existing device, this method goes trough all assigned features
98
+ and checks for a inner-referenced feature. It makes sure, that every inner-referenced feature variable has the
99
+ final assigned setup feature that belongs to it.
100
+
101
+ # TODO check where we validate that inner references feature exists in setup
102
+ """
103
+ for cur_setup_device in self.base_setup_controller.get_all_abs_inner_device_classes():
104
+ # these features are subclasses of the real defined one (because they are already the replaced ones)
105
+ all_device_features = DeviceController.get_for(cur_setup_device).get_all_instantiated_feature_objects()
106
+ all_instantiated_feature_objs_of_this_dev = [cur_feature for _, cur_feature in all_device_features.items()]
107
+ for _, cur_feature in all_device_features.items():
108
+ cur_feature_controller = FeatureController.get_for(cur_feature.__class__)
109
+ # now check the inner referenced features of this feature and check if that exists in the device
110
+ for cur_ref_feature_name, cur_ref_feature in \
111
+ cur_feature_controller.get_inner_referenced_features().items():
112
+ potential_candidates = [
113
+ candidate_feature for candidate_feature in all_instantiated_feature_objs_of_this_dev
114
+ if isinstance(candidate_feature, cur_ref_feature.__class__)]
115
+ if len(potential_candidates) != 1:
116
+ raise RuntimeError("found none or more than one potential replacing candidates")
117
+ replacing_candidate = potential_candidates[0]
118
+ # because `cur_feature` is only the object instance, the value will be overwritten only for this
119
+ # object
120
+ setattr(cur_feature, cur_ref_feature_name, replacing_candidate)
121
+
87
122
  def get_scenario_executors(self) -> List[ScenarioExecutor]:
88
123
  """returns a list with all scenario executors that belongs to this setup executor"""
89
124
  return self._scenario_executors
90
125
 
91
- def cleanup_empty_executor_branches(self):
126
+ def cleanup_empty_executor_branches(self, consider_discarded=False):
92
127
  to_remove_executor = []
93
128
  for cur_scenario_executor in self.get_scenario_executors():
94
- cur_scenario_executor.cleanup_empty_executor_branches()
95
- if len(cur_scenario_executor.get_variation_executors()) == 0:
129
+ cur_scenario_executor.cleanup_empty_executor_branches(consider_discarded=consider_discarded)
130
+ if len(cur_scenario_executor.get_variation_executors(return_discarded=consider_discarded)) == 0:
96
131
  # remove this whole executor because it has no children anymore
97
132
  to_remove_executor.append(cur_scenario_executor)
98
133
  for cur_scenario_executor in to_remove_executor:
@@ -7,7 +7,6 @@ import traceback
7
7
  from _balder.utils import inspect_method
8
8
  from _balder.testresult import ResultState, TestcaseResult
9
9
  from _balder.executor.basic_executor import BasicExecutor
10
- from _balder.previous_executor_mark import PreviousExecutorMark
11
10
 
12
11
  if TYPE_CHECKING:
13
12
  from _balder.executor.variation_executor import VariationExecutor
@@ -132,7 +131,7 @@ class TestcaseExecutor(BasicExecutor):
132
131
  return True
133
132
  return False
134
133
 
135
- def cleanup_empty_executor_branches(self):
134
+ def cleanup_empty_executor_branches(self, consider_discarded=False):
136
135
  """
137
136
  This method searches the whole tree and removes branches where an executor item has no own children. It can
138
137
  remove these branches, because they have no valid matchings.
@@ -5,7 +5,7 @@ import inspect
5
5
  import logging
6
6
  from _balder.device import Device
7
7
  from _balder.connection import Connection
8
- from _balder.testresult import ResultState, BranchBodyResult
8
+ from _balder.testresult import ResultState, BranchBodyResult, ResultSummary
9
9
  from _balder.executor.basic_executor import BasicExecutor
10
10
  from _balder.executor.testcase_executor import TestcaseExecutor
11
11
  from _balder.previous_executor_mark import PreviousExecutorMark
@@ -134,6 +134,7 @@ class VariationExecutor(BasicExecutor):
134
134
 
135
135
  @property
136
136
  def not_applicable_variation_exc(self) -> Union[NotApplicableVariationException, None]:
137
+ """holds the :class:`NotApplicableVariationException` that describes why this variation in not applicable"""
137
138
  return self._not_applicable_variation_exc
138
139
 
139
140
  # ---------------------------------- PROTECTED METHODS -------------------------------------------------------------
@@ -149,7 +150,6 @@ class VariationExecutor(BasicExecutor):
149
150
  self.determine_abs_variation_connections()
150
151
  self.update_scenario_device_feature_instances()
151
152
  self.update_active_vdevice_device_mappings_in_all_features()
152
- self.update_inner_referenced_feature_instances()
153
153
  self.exchange_unmapped_vdevice_references()
154
154
  self.update_vdevice_referenced_feature_instances()
155
155
  self.set_conn_dependent_methods()
@@ -282,17 +282,10 @@ class VariationExecutor(BasicExecutor):
282
282
 
283
283
  # ---------------------------------- METHODS -----------------------------------------------------------------------
284
284
 
285
- def testsummary(self):
285
+ def testsummary(self) -> ResultSummary:
286
286
  if self.can_be_applied():
287
287
  return super().testsummary()
288
- return {
289
- ResultState.NOT_RUN: 0,
290
- ResultState.FAILURE: 0,
291
- ResultState.ERROR: 0,
292
- ResultState.SUCCESS: 0,
293
- ResultState.SKIP: 0,
294
- ResultState.COVERED_BY: 0
295
- }
288
+ return ResultSummary()
296
289
 
297
290
  def get_testcase_executors(self) -> List[TestcaseExecutor]:
298
291
  """returns all sub testcase executors that belongs to this variation-executor"""
@@ -483,7 +476,7 @@ class VariationExecutor(BasicExecutor):
483
476
  # can not find some
484
477
  return None
485
478
 
486
- def cleanup_empty_executor_branches(self):
479
+ def cleanup_empty_executor_branches(self, consider_discarded=False):
487
480
  """
488
481
  This method searches the whole tree and removes branches where an executor item has no own children. It can
489
482
  remove these branches, because they have no valid matchings.
@@ -569,34 +562,6 @@ class VariationExecutor(BasicExecutor):
569
562
  for cur_feature, cur_original_mapping in cur_feature_mapping_dict.items():
570
563
  cur_feature.active_vdevices = cur_original_mapping
571
564
 
572
- def update_inner_referenced_feature_instances(self):
573
- """
574
- This method ensures that all inner referenced feature instances of the used feature object, will be replaced
575
- with the related feature instances of the device object itself.
576
-
577
- .. note::
578
- Note that this method expects that the true defined scenario features are already replaced with the real
579
- setup features. So the method requires that the method `update_scenario_device_feature_instances()` was
580
- called before.
581
- """
582
- for scenario_device, _ in self._base_device_mapping.items():
583
- # these features are subclasses of the real defined one (because they are already the replaced ones)
584
- all_device_features = DeviceController.get_for(scenario_device).get_all_instantiated_feature_objects()
585
- all_instantiated_feature_objs = [cur_feature for _, cur_feature in all_device_features.items()]
586
- for _, cur_feature in all_device_features.items():
587
- cur_feature_controller = FeatureController.get_for(cur_feature.__class__)
588
- # now check the inner referenced features of this feature and check if that exists in the device
589
- for cur_ref_feature_name, cur_ref_feature in \
590
- cur_feature_controller.get_inner_referenced_features().items():
591
- potential_candidates = [candidate_feature for candidate_feature in all_instantiated_feature_objs
592
- if isinstance(candidate_feature, cur_ref_feature.__class__)]
593
- if len(potential_candidates) != 1:
594
- raise RuntimeError("found none or more than one potential replacing candidates")
595
- replacing_candidate = potential_candidates[0]
596
- # because `cur_feature` is only the object instance, the value will be overwritten only for this
597
- # object
598
- setattr(cur_feature, cur_ref_feature_name, replacing_candidate)
599
-
600
565
  def exchange_unmapped_vdevice_references(self):
601
566
  """
602
567
  This method exchanges all :class:`VDevice` references to an instance of :class:`UnmappedVDevice` if the
@@ -721,10 +686,7 @@ class VariationExecutor(BasicExecutor):
721
686
  if cur_scenario_device not in abs_var_scenario_device_cnns.keys():
722
687
  abs_var_scenario_device_cnns[cur_scenario_device] = {}
723
688
 
724
- if cur_cnn.from_device == cur_scenario_device:
725
- cur_to_device = cur_cnn.to_device
726
- else:
727
- cur_to_device = cur_cnn.from_device
689
+ cur_to_device, _ = cur_cnn.get_conn_partner_of(cur_scenario_device)
728
690
 
729
691
  if cur_to_device not in abs_var_scenario_device_cnns[cur_scenario_device].keys():
730
692
  abs_var_scenario_device_cnns[cur_scenario_device][cur_to_device] = []
@@ -394,6 +394,41 @@ class FixtureManager:
394
394
  if exception:
395
395
  raise exception
396
396
 
397
+ def get_fixture_for_class(
398
+ self,
399
+ execution_level: Union[Type[ExecutorTree], Type[SetupExecutor], Type[ScenarioExecutor],
400
+ Type[VariationExecutor], Type[TestcaseExecutor]],
401
+ setup_or_scenario_class: Union[None, Type[Setup], Type[Scenario]],
402
+ parent_classes: bool = True
403
+ ) -> List[Tuple[MethodLiteralType, Callable]]:
404
+ """
405
+ This method returns all classes of a specific Setup/Scenario class for a specific execution-level.
406
+
407
+ :param execution_level: the execution level the fixture should be returned for
408
+ :param setup_or_scenario_class: the scenario or setup class, the fixtures should be returned for
409
+ :param parent_classes: true if the method should look for fixtures in parent classes too
410
+ :return: list with all fixtures that are matching search criteria
411
+ """
412
+ # current relevant EXECUTION LEVEL - all other levels are not relevant for this call
413
+ cur_execution_level = self.resolve_type_level[execution_level]
414
+ # get all fixtures of the current relevant level
415
+ fixtures_of_exec_level = self.fixtures.get(cur_execution_level, {})
416
+ if setup_or_scenario_class is not None and parent_classes:
417
+ all_fixtures = []
418
+ for cur_parent_class in inspect.getmro(setup_or_scenario_class):
419
+ if issubclass(cur_parent_class, (Scenario, Setup)):
420
+ all_fixtures += self.get_fixture_for_class(execution_level, cur_parent_class, False)
421
+ # go through list and remove all overwritten fixtures
422
+ _added_fixtures = []
423
+ remaining_fixtures = []
424
+ for cur_fixture_tuple in all_fixtures:
425
+ if cur_fixture_tuple[1].__name__ not in _added_fixtures:
426
+ _added_fixtures.append(cur_fixture_tuple[1].__name__)
427
+ remaining_fixtures.append(cur_fixture_tuple)
428
+ return remaining_fixtures
429
+ else:
430
+ return fixtures_of_exec_level.get(setup_or_scenario_class, [])
431
+
397
432
  def get_all_fixtures_for_current_level(
398
433
  self, branch: Union[ExecutorTree, SetupExecutor, ScenarioExecutor, VariationExecutor, TestcaseExecutor]) \
399
434
  -> Dict[Union[Type[ExecutorTree], Type[Scenario], Type[Setup]],
@@ -416,28 +451,24 @@ class FixtureManager:
416
451
  argument (this list is ordered after the call hierarchy)
417
452
  """
418
453
  from _balder.executor.executor_tree import ExecutorTree
419
- # current relevant EXECUTION LEVEL - all other levels are not relevant for this call
420
- cur_execution_level = self.resolve_type_level[branch.__class__]
421
- # get all fixtures of the current relevant level
422
- fixtures_of_exec_level = self.fixtures.get(cur_execution_level, {})
423
454
 
424
455
  all_fixtures = {}
425
456
  # get all relevant fixtures of `balderglob.py` (None is key for balderglob fixtures)
426
- glob_fixtures = fixtures_of_exec_level.get(None, [])
457
+ glob_fixtures = self.get_fixture_for_class(branch.__class__, None)
427
458
  all_fixtures[ExecutorTree] = {}
428
459
  all_fixtures[ExecutorTree][ExecutorTree] = glob_fixtures
429
460
  # get all relevant fixtures with definition scope "setup"
430
461
  all_fixtures[Setup] = {}
431
462
  for cur_setup in branch.get_all_base_instances_of_this_branch(Setup, only_runnable_elements=True):
432
463
  # check if there exists fixtures for the current setup
433
- cur_setup_fixtures = fixtures_of_exec_level.get(cur_setup.__class__, [])
464
+ cur_setup_fixtures = self.get_fixture_for_class(branch.__class__, cur_setup.__class__)
434
465
  if cur_setup_fixtures:
435
466
  all_fixtures[Setup][cur_setup.__class__] = cur_setup_fixtures
436
467
 
437
468
  # get all relevant fixtures with definition scope "scenario"
438
469
  all_fixtures[Scenario] = {}
439
470
  for cur_scenario in branch.get_all_base_instances_of_this_branch(Scenario, only_runnable_elements=True):
440
- cur_scenario_fixtures = fixtures_of_exec_level.get(cur_scenario.__class__, [])
471
+ cur_scenario_fixtures = self.get_fixture_for_class(branch.__class__, cur_scenario.__class__)
441
472
  if cur_scenario_fixtures:
442
473
  all_fixtures[Scenario][cur_scenario.__class__] = cur_scenario_fixtures
443
474
 
_balder/routing_path.py CHANGED
@@ -284,8 +284,6 @@ class RoutingPath:
284
284
 
285
285
  if elem_before is not None:
286
286
  # check if device and nodes of the two elements are the same -> have to be a chain
287
- last_device, last_node_name = self._get_end_device_and_node()
288
-
289
287
  if isinstance(elem, Connection):
290
288
  if (self.end_device, self.end_node_name) not in \
291
289
  [(elem.from_device, elem.from_node_name), (elem.to_device, elem.to_node_name)]:
_balder/solver.py CHANGED
@@ -170,10 +170,14 @@ class Solver:
170
170
  self._mapping = initial_mapping
171
171
  self._resolving_was_executed = True
172
172
 
173
- def get_executor_tree(self, plugin_manager: PluginManager) -> ExecutorTree: # pylint: disable=unused-argument
173
+ # pylint: disable-next=unused-argument
174
+ def get_executor_tree(self, plugin_manager: PluginManager, add_discarded=False) -> ExecutorTree:
174
175
  """
175
176
  This method builds the ExecutorTree from the resolved data and returns it
176
177
 
178
+ :param plugin_manager: the related plugin manager object
179
+ :param add_discarded: True in case discarded elements should be added to the tree, otherwise False
180
+
177
181
  :return: the executor tree is built on the basis of the mapping data
178
182
  """
179
183
 
@@ -215,7 +219,7 @@ class Solver:
215
219
  # now filter all elements that have no child elements
216
220
  # -> these are items that have no valid matching, because no variation can be applied for it (there are no
217
221
  # required :class:`Feature` matching or there exists no possible routing for the variation)
218
- executor_tree.cleanup_empty_executor_branches()
222
+ executor_tree.cleanup_empty_executor_branches(consider_discarded=add_discarded)
219
223
 
220
224
  self._set_data_for_covered_by_in_tree(executor_tree=executor_tree)
221
225
 
_balder/testresult.py CHANGED
@@ -1,4 +1,6 @@
1
1
  from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, fields
2
4
  from typing import TYPE_CHECKING, List, Union
3
5
 
4
6
  from enum import Enum
@@ -12,21 +14,21 @@ class ResultState(Enum):
12
14
  enumeration that describes the possible results of a testcase-/fixture-executor
13
15
  """
14
16
  # this state will be assigned if the executor doesn't run yet
15
- NOT_RUN = 'NOT_RUN'
17
+ NOT_RUN = 'not_run'
16
18
  # this state will be assigned if the test fails (only assignable to :class:`TestcaseExecutor`)
17
- FAILURE = 'FAILURE'
19
+ FAILURE = 'failure'
18
20
  # this state will be assigned if the executor can not be executed because the fixture fails (only possible in
19
21
  # construction part of fixtures, if the error occurs in teardown the next higher executer get this state) - will be
20
22
  # assigned to the executor which has the fixture that fails
21
- ERROR = 'ERROR'
23
+ ERROR = 'error'
22
24
  # this state will be assigned if the executor was executed successfully (session fixture and teardown fixture; for
23
25
  # :class:`TestcaseExecutor` also the testcase itself)
24
- SUCCESS = 'SUCCESS'
26
+ SUCCESS = 'success'
25
27
  # this state will be assigned if the executor was skipped
26
- SKIP = 'SKIP'
28
+ SKIP = 'skip'
27
29
  # this state will be assigned if the executor is covered by another executor and wasn't executed (only assignable
28
30
  # to :class:`ScenarioExecutor` and :class:`TestcaseExecutor`)
29
- COVERED_BY = 'COVERED_BY'
31
+ COVERED_BY = 'covered_by'
30
32
 
31
33
  @staticmethod
32
34
  def priority_order() -> List[ResultState]:
@@ -128,3 +130,33 @@ class TestcaseResult(_SettableResult):
128
130
  #: contains the possible values that can be set for this Result type
129
131
  ALLOWED_STATES = [ResultState.NOT_RUN, ResultState.FAILURE, ResultState.SUCCESS, ResultState.SKIP,
130
132
  ResultState.COVERED_BY]
133
+
134
+ @dataclass
135
+ class ResultSummary:
136
+ """
137
+ object that holds the test results for multiple tests like on branch or global level
138
+ """
139
+ # this state will be assigned if the executor doesn't run yet
140
+ not_run: int = 0
141
+ # this state will be assigned if the test fails (only assignable to :class:`TestcaseExecutor`)
142
+ failure: int = 0
143
+ # this state will be assigned if the executor can not be executed because the fixture fails (only possible in
144
+ # construction part of fixtures, if the error occurs in teardown the next higher executer get this state) - will be
145
+ # assigned to the executor which has the fixture that fails
146
+ error: int = 0
147
+ # this state will be assigned if the executor was executed successfully (session fixture and teardown fixture; for
148
+ # :class:`TestcaseExecutor` also the testcase itself)
149
+ success: int = 0
150
+ # this state will be assigned if the executor was skipped
151
+ skip: int = 0
152
+ # this state will be assigned if the executor is covered by another executor and wasn't executed (only assignable
153
+ # to :class:`ScenarioExecutor` and :class:`TestcaseExecutor`)
154
+ covered_by: int = 0
155
+
156
+ def __add__(self, other) -> ResultSummary:
157
+ new_summary = ResultSummary()
158
+ for cur_field in fields(self.__class__):
159
+ self_val = getattr(self, cur_field.name)
160
+ other_val = getattr(other, cur_field.name)
161
+ setattr(new_summary, cur_field.name, self_val + other_val)
162
+ return new_summary
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: baldertest
3
- Version: 0.1.0b7
3
+ Version: 0.1.0b9
4
4
  Summary: balder: reusable scenario based test framework
5
5
  Home-page: https://docs.balder.dev
6
6
  Author: Max Stahlschmidt and others
@@ -1,10 +1,10 @@
1
1
  _balder/__init__.py,sha256=Qk4wkVInPlXLFV36Yco5K7PDawJoeeWQVakzj6g5pmA,195
2
- _balder/_version.py,sha256=Mlz6U-dFHSBS29dqTLVb3_npm2vadOIIiwZaPdAO_Ag,162
2
+ _balder/_version.py,sha256=xdDxoDc8nbJHb9-aJOnXPcJO9u_wtajm1m932_7HjoI,413
3
3
  _balder/balder_plugin.py,sha256=EQzJP1dwwVDydhMLJtAmTCXOczlDuXBJur05lalmK_k,3136
4
- _balder/balder_session.py,sha256=bgpgvpYqFmtUlPFYEXQ10Mvj0IhLO86Yut__Gmc3l2c,16211
4
+ _balder/balder_session.py,sha256=WPOcarZQhTzMeAQt-hRqI8GtXN5TdMIqzyhgYD2h4rw,16079
5
5
  _balder/balder_settings.py,sha256=U96PVep7dGSaTXrMfeZMYf6oCIcEDPEqrBlFcoX476s,582
6
- _balder/collector.py,sha256=PfGVY34wrz2oauDEu-HY53S7w5HlQoaYwuNHQE0HuRk,40336
7
- _balder/connection.py,sha256=T2g63SbtTRT7d8MvabTSbPQYP9DhIhn2zo8zc54OwoY,69633
6
+ _balder/collector.py,sha256=T_7LO8ZNty8EyOmM-U-uhU9-umdAv1fbuMVNjQWUwj0,42156
7
+ _balder/connection.py,sha256=MNazK97CIEJCA4o1krjKjtilVhOYMWD8TZT0YT5nCJs,71778
8
8
  _balder/decorator_connect.py,sha256=TvyJNIslBAYVQWhsfSeFgXKp_DP7sZF1BmcP6RhIdKo,5988
9
9
  _balder/decorator_covered_by.py,sha256=Y6WMUuyn_uvFkjGfbts8OE5Bsir_7LTRB-jxYGaYk4Y,4069
10
10
  _balder/decorator_fixture.py,sha256=Nvr3H-r8LlPiGSGEst3D1M-KQaBqSA1u1u13qzZrSwk,1138
@@ -15,15 +15,15 @@ _balder/device.py,sha256=5O3tqj_iLKfHb5Zi_viJ76VH82cMOzX58OzRrMRRv0k,833
15
15
  _balder/exceptions.py,sha256=d0YSw8zVax2bFakG_1hcLlAXlyQh5jXnOuIPL1HLfI4,4202
16
16
  _balder/exit_code.py,sha256=P0oFWKfjMo36Frv13ADRcm8eSPN3kE-WmZBE9qZJHdA,513
17
17
  _balder/feature.py,sha256=B3yPc-WZwLt1Q4dO9s2j9g1MBTcMBjn6oWoLnRgrwSs,3845
18
- _balder/fixture_manager.py,sha256=rsepVAp0BRbakcwWtY59NtFDipTLvFVxPqJoCXYrliM,26843
18
+ _balder/fixture_manager.py,sha256=_fJZAaBK8Pej96juvdkigi7rjKJ-RTbfxhkSs78OBvg,28656
19
19
  _balder/node_gateway.py,sha256=64mv7Nx82JVknnQ09UXC-AcdDl6i_OB6NOsq_uBxeYo,4710
20
20
  _balder/plugin_manager.py,sha256=Ev2jnx4NtFHDsZ3C6h0HrJtQisqLO-V34JRM3wzTnFM,6921
21
21
  _balder/previous_executor_mark.py,sha256=gwpGu7d-kwPzQT8CmaPfuEG6fess2Upf5Q-zX6Oi6NY,835
22
- _balder/routing_path.py,sha256=7geuwPqBVnB-wAZ1-CjSLedNClVWb5rS628u8fRbvFw,17247
22
+ _balder/routing_path.py,sha256=6MJkhzBTHow2ESXzKQ2otwRFbPcKhLTYVy-zh7c5HeE,17172
23
23
  _balder/scenario.py,sha256=ATowBUl2HYQBmJHZ-eBpliqjPsWPnZAme9kwIeX3Tak,840
24
24
  _balder/setup.py,sha256=zSgtzNIWTVBjiZ5mn-qfpqIAnP3Im73t3Lqoaw0gWEI,763
25
- _balder/solver.py,sha256=m4BiQxnTG7qsKlSM6tSpVFICocRohUyTH729VcM5QqE,11764
26
- _balder/testresult.py,sha256=6yo3EcuLlpM23vdrYzn5VDcG35qelAQudmyoLpwZoHM,5251
25
+ _balder/solver.py,sha256=qgcCRyLg3nsO2d3H0pukxmm2oe_h6t0pBGyZ-FgQYKc,11998
26
+ _balder/testresult.py,sha256=byJD3F84TNH40k30pjnxeLHXAAE-lqbVlk1JOSBtdNo,6783
27
27
  _balder/unmapped_vdevice.py,sha256=oKr01YVTLViWtZkYz8kx8ccTx-KmwgNrHuQqqD4eLQw,513
28
28
  _balder/utils.py,sha256=2kcX6bZIJW9Z8g-Hv0ue2mdOLBQYq4T2XSZpH2in1MQ,3092
29
29
  _balder/vdevice.py,sha256=fc2xuMnTuN1RyfWh9mqFgLdSO9yGA75eERobTXUQ9JA,215
@@ -33,18 +33,18 @@ _balder/controllers/__init__.py,sha256=UNb6QzMj4TqPI15OSvXyUJlA-NSai0CKkQhV5JIsb
33
33
  _balder/controllers/base_device_controller.py,sha256=g-vY2SqKFUC9yGOvHLfbdmILT3sK2YyaWKSfvTRcC0o,3174
34
34
  _balder/controllers/controller.py,sha256=XGRE5LKWxxftEf-bZODvKxXwULu09iG131wMwRoz4Fk,803
35
35
  _balder/controllers/device_controller.py,sha256=bT8c0_9UmrAmzuZ-dsleSnw2pIppfITDG4ttEZjWlC8,23477
36
- _balder/controllers/feature_controller.py,sha256=Dsjdbcs6OFQZDCJjOstqOghr3S88u-RdJkXukZHHDfU,42427
37
- _balder/controllers/normal_scenario_setup_controller.py,sha256=mJdqMP7xlhtloQgKBAmgWe5a-oyYqmruUKiL-rLOe08,22144
36
+ _balder/controllers/feature_controller.py,sha256=jaIbKbiEOdYRpYWuGdaByQBtlThK-u7OAr_xYhAfnEg,43737
37
+ _balder/controllers/normal_scenario_setup_controller.py,sha256=cF2U-tC3LEyWdZDqZP_mN3Q_7uIzg9jiY_J--Pi5kk8,21877
38
38
  _balder/controllers/scenario_controller.py,sha256=gpN_wxEYcHlh6DVk7pRbPj_x2cmM6iIAskcpLstQo2g,17973
39
39
  _balder/controllers/setup_controller.py,sha256=iNIMFjawYJWaSToUUfpmRK6ssycPyZGNlcvms8c7GKM,7135
40
40
  _balder/controllers/vdevice_controller.py,sha256=6-PidCKgvUteZVJsbGkKX69f3cYYYnolONl5Gja16W8,5777
41
41
  _balder/executor/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
42
- _balder/executor/basic_executor.py,sha256=ujG5HMu7dZMmpFA3EHDpi3mBIW-b_kWtzsJFK1Dy9-I,11480
43
- _balder/executor/executor_tree.py,sha256=cwhdM4rdvf52B4DFT9IdZhWh4TW3Dd4BlyZevFH-Yew,9687
44
- _balder/executor/scenario_executor.py,sha256=stAi1ulXxa60XND5Ot8jXRG_4tGGHKI31EyuNNPiwYE,10807
45
- _balder/executor/setup_executor.py,sha256=z6naHYh4kzkAR3RuRjTymstuJ6lic-iKM7rWx6jNgzM,5747
46
- _balder/executor/testcase_executor.py,sha256=VVQsMu15w3IYhPt4nvWfcQIH_RM1BHyJfUs4amOUxWI,6677
47
- _balder/executor/variation_executor.py,sha256=gSqQCTZkQwKvll_9yWP65cmUYR_egniG4gntraj_JSE,55374
42
+ _balder/executor/basic_executor.py,sha256=PYuQ1TKXKnbnQirCmayjDSyPyBmqJws1HWqkHmGKZtw,11137
43
+ _balder/executor/executor_tree.py,sha256=nEZ2bwWuayPZQrGSucFiDzuFSrYDnUVzqnVCtDLq2Ys,10439
44
+ _balder/executor/scenario_executor.py,sha256=3j5lAFCA4zCxhivjMPFGlWMi_ieZGkdwnZ_JQoAt4Rw,11094
45
+ _balder/executor/setup_executor.py,sha256=FsPTPsXZMoVB7xibEpCcwjC-BpkfGVZmx5lVbuebduw,8326
46
+ _balder/executor/testcase_executor.py,sha256=jNfACO-AuOktIgCVcsavh0rHbUr5MmI-jML-LPMvo0w,6639
47
+ _balder/executor/variation_executor.py,sha256=Ms8dfZPIL5-sYzMg2c1-WANDz8ZBHqNHXgegDobm7Gg,53106
48
48
  _balder/objects/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
49
49
  _balder/objects/connections/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
50
50
  _balder/objects/connections/osi_1_physical.py,sha256=74lKWJd6ETEtvNXH0_dmTbkZlStJ_af218pQkUht0aA,2189
@@ -60,9 +60,9 @@ balder/__init__.py,sha256=BfbdkW_CZCXWGqtdg1gpAjO0AY8b4h-P-flWsIMG-X0,1003
60
60
  balder/connections.py,sha256=H6rf7UsiVY_FeZLngZXCT9WDw9cQqpiDiPbz_0J4yjM,2331
61
61
  balder/devices.py,sha256=zupHtz8yaiEjzR8CrvgZU-RzsDQcZFeN5mObfhtjwSw,173
62
62
  balder/exceptions.py,sha256=z_vlipJIsFwwJy9Ae_oGDJGPTINiAInMNZuCvEy6SUE,1884
63
- baldertest-0.1.0b7.dist-info/LICENSE,sha256=Daz9qTpqbiq-klWb2Q9lYOmn3rJ5oIQnbs62sGcqOZ4,1084
64
- baldertest-0.1.0b7.dist-info/METADATA,sha256=wB5TDyLXJNzyQvWmXqxuCD5VB9Voum3iTtY06jpFjlk,15783
65
- baldertest-0.1.0b7.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
66
- baldertest-0.1.0b7.dist-info/entry_points.txt,sha256=hzqu_nrMKTCi5IJqzS1fhIXWEiL7mTGZ-kgj2lUYlRU,65
67
- baldertest-0.1.0b7.dist-info/top_level.txt,sha256=RUkIBkNLqHMemx2C9aEpoS65dpqb6_jU-oagIPxGQEA,15
68
- baldertest-0.1.0b7.dist-info/RECORD,,
63
+ baldertest-0.1.0b9.dist-info/LICENSE,sha256=Daz9qTpqbiq-klWb2Q9lYOmn3rJ5oIQnbs62sGcqOZ4,1084
64
+ baldertest-0.1.0b9.dist-info/METADATA,sha256=HzdUqwAR2FIIYoAWg6RxXoFOJzUOqpkxcA7q-V3SHbM,15783
65
+ baldertest-0.1.0b9.dist-info/WHEEL,sha256=mguMlWGMX-VHnMpKOjjQidIo1ssRlCFu4a4mBpz1s2M,91
66
+ baldertest-0.1.0b9.dist-info/entry_points.txt,sha256=hzqu_nrMKTCi5IJqzS1fhIXWEiL7mTGZ-kgj2lUYlRU,65
67
+ baldertest-0.1.0b9.dist-info/top_level.txt,sha256=RUkIBkNLqHMemx2C9aEpoS65dpqb6_jU-oagIPxGQEA,15
68
+ baldertest-0.1.0b9.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.40.0)
2
+ Generator: setuptools (70.1.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5