Skip to content

Commit ae128e4

Browse files
authored
Merge pull request #331 from horw/feat/parametrize_wrapper
feat: parametrize_wrapper
2 parents ff05ae6 + d4f6e17 commit ae128e4

File tree

3 files changed

+497
-0
lines changed

3 files changed

+497
-0
lines changed

docs/usages/markers.rst

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,227 @@ Here are examples of how to use ``skip_if_soc`` with different conditions:
6161
@pytest.mark.parametrize("target", SUPPORTED_TARGETS, indirect=True)
6262
def test_template():
6363
pass
64+
65+
*********************
66+
``idf_parametrize``
67+
*********************
68+
69+
``idf_parametrize`` is a wrapper around ``pytest.mark.parametrize`` that simplifies and extends string-based parameterization for tests. By using ``idf_parametrize``, testing parameters becomes more flexible and easier to maintain.
70+
71+
**Key Features:**
72+
73+
- **Target Expansion**: Automatically expands lists of supported targets, reducing redundancy in test definitions.
74+
- **Markers**: use a marker as one of the parameters. If a marker is used, put it as the last parameter.
75+
76+
Use Cases
77+
=========
78+
79+
Target Extension
80+
----------------
81+
82+
In scenarios where the supported targets are [esp32, esp32c3, esp32s3], ``idf_parametrize`` simplifies the process of creating parameterized tests by automatically expanding the target list.
83+
84+
**Example:**
85+
86+
.. code:: python
87+
88+
@idf_parametrize('target', [
89+
('supported_targets'),
90+
], indirect=True)
91+
@idf_parametrize('config', [
92+
'default',
93+
'psram'
94+
])
95+
def test_st(dut: Dut) -> None:
96+
...
97+
98+
**Equivalent to:**
99+
100+
.. code:: python
101+
102+
@pytest.mark.parametrize('target', [
103+
'esp32',
104+
'esp32c3',
105+
'esp32s3'
106+
], indirect=True)
107+
@pytest.mark.parametrize('config', [
108+
'default',
109+
'psram'
110+
])
111+
def test_st(dut: Dut) -> None:
112+
...
113+
114+
**Resulting Parameters Matrix:**
115+
116+
.. list-table::
117+
:header-rows: 1
118+
119+
- - Target
120+
- Config
121+
- - esp32
122+
- default
123+
- - esp32c3
124+
- default
125+
- - esp32s3
126+
- default
127+
- - esp32
128+
- psram
129+
- - esp32c3
130+
- psram
131+
- - esp32s3
132+
- psram
133+
134+
Markers
135+
-------
136+
137+
Markers can also be combined for added flexibility. It must be placed in the last position. In this case, if some test cases do not have markers, you can skip their definition. Look at the example.
138+
139+
**Example:**
140+
141+
In IDF testing, an environment marker (``marker``) determines which test runner will execute a test. This enables tests to run on various runners, such as:
142+
143+
- **generic**: Tests run on generic runners.
144+
- **sdcard**: Tests require an SD card runner.
145+
- **usb_device**: Tests require a USB device runner.
146+
147+
.. code:: python
148+
149+
@pytest.mark.generic
150+
@idf_parametrize('config', [
151+
'defaults'
152+
], indirect=['config'])
153+
@idf_parametrize('target, markers', [
154+
('esp32', (pytest.mark.usb_device,)),
155+
('esp32c3')
156+
('esp32', (pytest.mark.sdcard,))
157+
], indirect=['target'])
158+
def test_console(dut: Dut, test_on: str) -> None:
159+
...
160+
161+
**Resulting Parameters Matrix:**
162+
163+
.. list-table::
164+
:header-rows: 1
165+
166+
- - Target
167+
- Markers
168+
- - esp32
169+
- generic, usb_device
170+
- - esp32c3
171+
- generic, sdcard
172+
- - esp32
173+
- generic, sdcard
174+
175+
Examples
176+
========
177+
178+
Target with Config
179+
------------------
180+
181+
**Example:**
182+
183+
.. code:: python
184+
185+
@idf_parametrize('target, config', [
186+
('esp32', 'release'),
187+
('esp32c3', 'default'),
188+
('supported_target', 'psram')
189+
], indirect=True)
190+
def test_st(dut: Dut) -> None:
191+
...
192+
193+
**Resulting Parameters Matrix:**
194+
195+
.. list-table::
196+
:header-rows: 1
197+
198+
- - Target
199+
- Config
200+
- - esp32
201+
- release
202+
- - esp32c3
203+
- default
204+
- - esp32
205+
- psram
206+
- - esp32c3
207+
- psram
208+
- - esp32s3
209+
- psram
210+
211+
Supported Target on Runners
212+
---------------------------
213+
214+
**Example:**
215+
216+
.. code:: python
217+
218+
@idf_parametrize('target, markers', [
219+
('esp32', (pytest.mark.generic, )),
220+
('esp32c3', (pytest.mark.sdcard, )),
221+
('supported_target', (pytest.mark.usb_device, ))
222+
], indirect=True)
223+
def test_st(dut: Dut) -> None:
224+
...
225+
226+
**Resulting Parameters Matrix:**
227+
228+
.. list-table::
229+
:header-rows: 1
230+
231+
- - Target
232+
- Markers
233+
- - esp32
234+
- generic
235+
- - esp32c3
236+
- sdcard
237+
- - esp32
238+
- usb_device
239+
- - esp32c3
240+
- usb_device
241+
- - esp32s3
242+
- usb_device
243+
244+
Runner for All Tests
245+
--------------------
246+
247+
**Example:**
248+
249+
.. code:: python
250+
251+
@pytest.mark.generic
252+
@idf_parametrize('target, config', [
253+
('esp32', 'release'),
254+
('esp32c3', 'default'),
255+
('supported_target', 'psram')
256+
], indirect=True)
257+
def test_st(dut: Dut) -> None:
258+
...
259+
260+
**Resulting Parameters Matrix:**
261+
262+
.. list-table::
263+
:header-rows: 1
264+
265+
- - Target
266+
- Config
267+
- Markers
268+
269+
- - esp32
270+
- release
271+
- generic
272+
273+
- - esp32c3
274+
- default
275+
- generic
276+
277+
- - esp32
278+
- psram
279+
- generic
280+
281+
- - esp32c3
282+
- psram
283+
- generic
284+
285+
- - esp32s3
286+
- psram
287+
- generic
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import typing as t
2+
3+
import pytest
4+
from esp_bool_parser import PREVIEW_TARGETS, SUPPORTED_TARGETS
5+
6+
7+
def _expand_target_values(values: t.List[t.List[t.Any]], target_index: int) -> t.List[t.List[t.Any]]:
8+
"""
9+
Expands target-specific values into individual test cases.
10+
"""
11+
expanded_values = []
12+
for value in values:
13+
target = value[target_index]
14+
if target == 'supported_targets':
15+
expanded_values.extend([
16+
value[:target_index] + [target] + value[target_index + 1 :] for target in SUPPORTED_TARGETS
17+
])
18+
elif target == 'preview_targets':
19+
expanded_values.extend([
20+
value[:target_index] + [target] + value[target_index + 1 :] for target in PREVIEW_TARGETS
21+
])
22+
else:
23+
expanded_values.append(value)
24+
return expanded_values
25+
26+
27+
def _process_pytest_value(value: t.Union[t.List[t.Any], t.Any], param_count: int) -> t.Any:
28+
"""
29+
Processes a single parameter value, converting it to pytest.param if needed.
30+
"""
31+
if not isinstance(value, (list, tuple)):
32+
return value
33+
34+
if len(value) > param_count + 1:
35+
raise ValueError(f'Expected at most {param_count + 1} elements (params + marks), got {len(value)}')
36+
37+
params, marks = [], []
38+
if len(value) > param_count:
39+
mark_values = value[-1]
40+
marks.extend(mark_values if isinstance(mark_values, (tuple, list)) else (mark_values,))
41+
42+
params.extend(value[:param_count])
43+
44+
return pytest.param(*params, marks=tuple(marks))
45+
46+
47+
def idf_parametrize(
48+
param_names: str,
49+
values: t.List[t.Union[t.Any, t.Tuple[t.Any, ...]]],
50+
indirect: (t.Union[bool, t.Sequence[str]]) = False,
51+
) -> t.Callable[..., None]:
52+
"""
53+
A decorator to unify pytest.mark.parametrize usage in esp-idf.
54+
55+
Args:
56+
param_names: A comma-separated string of parameter names that will be passed to
57+
the test function.
58+
values: A list of parameter values where each value corresponds to the parameters
59+
defined in param_names.
60+
indirect: A list of arguments names (subset of argnames) or a boolean. If True
61+
the list contains all names from the argnames. Each argvalue corresponding to an
62+
argname in this list will be passed as request.param to its respective argname
63+
fixture function so that it can perform more expensive setups during the setup
64+
phase of a test rather than at collection time.
65+
66+
Returns:
67+
Decorated test function with parametrization applied
68+
"""
69+
param_list = [name.strip() for name in param_names.split(',')]
70+
for param in param_list:
71+
if not param:
72+
raise ValueError(f'One of the provided parameters name is empty: {param_list}')
73+
74+
param_count = len(param_list)
75+
param_list[:] = [_p for _p in param_list if _p not in ('markers',)]
76+
target_index = param_list.index('target') if 'target' in param_list else -1
77+
normalized_values = [[value] if param_count == 1 else list(value) for value in values]
78+
param_count = len(param_list)
79+
80+
if target_index != -1:
81+
normalized_values = _expand_target_values(normalized_values, target_index)
82+
83+
processed_values = [_process_pytest_value(value, param_count) for value in normalized_values]
84+
85+
def decorator(func):
86+
return pytest.mark.parametrize(','.join(param_list), processed_values, indirect=indirect)(func)
87+
88+
return decorator

0 commit comments

Comments
 (0)