Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# Authors: Sylvain MARIE <sylvain.marie@se.com>
2# + All contributors to <https://github.com/smarie/python-pytest-steps>
3#
4# License: 3-clause BSD, <https://github.com/smarie/python-pytest-steps/blob/master/LICENSE>
5from copy import copy
7# WARNING do not import pytest-harvest here: it should remain optional
8from .steps import _get_step_param_names_or_default
9from .steps_common import remove_param_from_pytest_node_str_id
11try: # python 3.5+
12 from typing import Union, Iterable, Any
13except ImportError:
14 pass
17def handle_steps_in_results_dct(results_dct,
18 is_flat=False, # type: bool
19 raise_if_one_test_without_step_id=False, # type: bool
20 no_step_id='-', # type: str
21 step_param_names=None, # type: Union[str, Iterable[str]]
22 keep_orig_id=True,
23 no_steps_policy='raise' # type str
24 ):
25 """
26 Improves the synthesis dictionary so that
27 - the keys are replaced with a tuple (new_test_id, step_id) where new_test_id is a step-independent test id
28 - the 'step_id' parameter is removed from the contents
30 `is_flat` should be set to `True` if the dictionary has been flattened by `pytest-harvest`.
32 The step id is identified by looking at the pytest parameters, and finding one with a name included in the
33 `step_param_names` list (`None` uses the default names). If no step id is found on an entry, it is replaced with
34 the value of `no_step_id` except if `raise_if_one_test_without_step_id=True` - in which case an error is raised.
36 If all step ids are missing, for all entries in the dictionary, `no_steps_policy` determines what happens: it can
37 either skip the whole function and return a copy of the input ('skip', or behave as usual ('ignore'), or raise an
38 error ('raise').
40 If `keep_orig_id` is set to True (default), the original id is added to each entry.
42 :param results_dct: a synthesis dictionary created by `pytest-harvest`.
43 :param is_flat: to declare that synth_dct was flatten or not (if it was generated using `get_session_synthesis_dct`
44 with `flatten=True` or `False`).
45 :param raise_if_one_test_without_step_id: if this is set to `True` and at least one step id can not be found in the
46 tests, an error will be raised. By default this is set to `False`: in that case, when the step id is not found
47 it is replaced with value of the `no_step_id` parameter.
48 :param no_step_id: the identifier to use when the step id is not found (if `raise_if_no_step_id` is `False`)
49 :param step_param_names: a singleton or iterable containing the names of the test step parameters used in the
50 tests. By default the list is `[GENERATOR_MODE_STEP_ARGNAME, TEST_STEP_ARGNAME_DEFAULT]` to cover both
51 generator-mode and legacy manual mode.
52 :param keep_orig_id: if True (default) the original test id will appear in the dict entries under 'pytest_id'
53 :param no_steps_policy: if `'ignore` the returned dictionary keys will be tuples (test id, step id) in all
54 cases, even if no step is present. If 'skip' and no step is present, the method will return a copy of the input
55 and will not modify anything. If 'raise' (default) and no step is present, an error is raised.
56 :return: a dictionary where the keys are tuples of (new_test_id, step_id), and the values are copies of the initial
57 dictionarie's ones, except that the step id parameter is not present anymore
58 """
60 # validate parameters
61 step_param_names = _get_step_param_names_or_default(step_param_names)
62 if not isinstance(no_steps_policy, str):
63 # python 2 compatibility: unicode literals
64 no_steps_policy = str(no_steps_policy)
65 if no_steps_policy not in {'ignore', 'raise', 'skip'}:
66 raise ValueError("`no_steps_policy` should be one of {'ignore', 'raise', 'skip'}")
68 # edge case of empty dict
69 if len(results_dct) == 0:
70 return copy(results_dct)
72 # create an object of the same container type
73 res_dct = type(results_dct)()
75 # fill it
76 one_step_id_was_present = False
77 for test_id, test_info in results_dct.items():
78 # copy the first level (no deepcopy because we do not want to perform copies of entries in the dict)
79 new_info = copy(test_info)
80 if not is_flat:
81 # non-flattened: all parameters should be in a nested dict entry
82 if "params" in new_info:
83 where_params_dct = copy(new_info["params"])
84 elif "pytest_params" in new_info:
85 where_params_dct = copy(new_info["pytest_params"])
86 else:
87 raise KeyError("Could not find information related to parameters in provided dict. Maybe it was "
88 "created with flatten=True? In this case please set flatten=True here too")
90 # if there is a 'fixtures' entry, replace it with a copy ?
91 # not needed a priori
92 # if 'fixtures' in new_info:
93 # new_info['fixtures'] = copy(new_info['fixtures'])
94 else:
95 # flattened: all parameters should be in dedicated entries at the root level
96 where_params_dct = new_info
98 step_name_params = set(step_param_names).intersection(set(where_params_dct.keys()))
99 if len(step_name_params) == 1:
100 # remember that there was at least one
101 one_step_id_was_present = True
103 # use the key to retrieve step id value and remove it from where it was
104 step_id_key = step_name_params.pop()
105 step_id = where_params_dct.pop(step_id_key)
107 elif len(step_name_params) == 0:
108 if raise_if_one_test_without_step_id:
109 raise ValueError("The synthesis dictionary provided does not seem to contain step name parameters for "
110 "test node '%s'" % test_id)
111 else:
112 # use the default id for "no step"
113 step_id = no_step_id
114 else:
115 raise ValueError("The synthesis dictionary provided contains several step name parameters for test node "
116 "'%s': %s" % (test_id, step_name_params))
118 # finally create the new id by replacing in the existing id (whatever its position in the parameters order)
119 new_id = remove_step_from_test_id(test_id, step_id)
121 # remember the old id
122 if keep_orig_id:
123 new_info['pytest_id'] = new_id
124 # move it to the beginning of the dict
125 new_info.move_to_end('pytest_id', last=False)
127 # store the element
128 res_dct[(new_id, step_id)] = new_info
130 if not one_step_id_was_present:
131 if no_steps_policy == 'skip':
132 # do not return the modified one, and return the initial dictionary (a copy)
133 return copy(results_dct)
134 elif no_steps_policy == 'raise':
135 raise ValueError("No step ids can be found in provided dictionary. You can ignore this error by switching "
136 "to `no_steps_policy`='ignore'")
137 else:
138 # 'ignore": same
139 return res_dct
141 return res_dct
144handle_steps_in_synthesis_dct = handle_steps_in_results_dct
145"""deprecated alias - to remove"""
148def remove_step_from_test_id(test_id, step_id):
149 """
150 Returns a new test id where the step parameter is not present anymore.
152 :param step_id:
153 :param test_id:
154 :return:
155 """
156 # from math import isnan
157 # if isnan(step_id):
158 # return test_id
159 return remove_param_from_pytest_node_str_id(test_id, step_id)
162def get_all_pytest_param_names_except_step_id(session,
163 filter=None, # type: Any
164 filter_incomplete=False, # type: bool
165 step_param_names=None # type: Union[str, Iterable[str]]
166 ):
167 """
168 Like pytest-harvest's `get_all_pytest_param_names` this function returns the list of all unique parameter names
169 used in all items in the provided session, with given filter. However "step id" parameters are removed
170 automatically.
172 An optional `filter` can be provided, that can be a singleton or iterable of pytest objects (typically test
173 functions) and/or module names.
175 If this method is called before the end of the pytest session, some nodes might be incomplete, i.e. they will not
176 have data for the three stages (setup/call/teardown). By default these nodes are filtered out but you can set
177 `filter_incomplete=False` to make them appear. They will have a special 'pending' synthesis status.
179 :param session: a pytest session object.
180 :param filter: a singleton or iterable of pytest objects on which to filter the returned dict on (the returned
181 items will only by pytest nodes for which the pytest object is one of the ones provided). One can also use
182 modules or the special `THIS MODULE` item.
183 :param filter_incomplete: a boolean indicating if incomplete nodes (without the three stages setup/call/teardown)
184 should appear in the results (False) or not (True). Note: by default incomplete nodes DO APPEAR (this is
185 different from get_session_synthesis_dct behaviour)
186 :param step_param_names: a singleton or iterable containing the names of the test step parameters used in the
187 tests. By default the list is `[GENERATOR_MODE_STEP_ARGNAME, TEST_STEP_ARGNAME_DEFAULT]` to cover both
188 generator-mode and legacy manual mode.
189 :return:
190 """
191 try:
192 from pytest_harvest import get_all_pytest_param_names
194 # test_step_param_names
195 step_param_names = _get_step_param_names_or_default(step_param_names)
197 return [p for p in get_all_pytest_param_names(session, filter=filter, filter_incomplete=filter_incomplete)
198 if p not in step_param_names]
200 except ImportError:
201 raise ImportError("pytest-harvest>=1.0.0 is required to use "
202 "`get_all_pytest_param_names_except_step_id`")