Skip to content

Commit 7902a27

Browse files
committed
feat(lftest): add attach_each() helper for non-TESTS-dict iterables
Sister of attach_tests(). Where attach_tests() works on a TESTS list of dicts that lftest.run() knows how to execute, attach_each() takes an arbitrary iterable plus a caller-supplied action callable. Useful for container-image matrices, file-based fixtures with stateful per-item setup, and any other pattern that does not fit the TESTS dict shape (testcontainers IMAGES list, cpu-usage CONTAINERFILES list, apache-httpd-status SCENARIOS list).
1 parent 9edb72f commit 7902a27

1 file changed

Lines changed: 66 additions & 1 deletion

File tree

lftest.py

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from . import base, disk, shell
1818

1919
__author__ = 'Linuxfabrik GmbH, Zurich/Switzerland'
20-
__version__ = '2026041302'
20+
__version__ = '2026041303'
2121

2222

2323
def run(test_instance, plugin, testcase):
@@ -161,6 +161,71 @@ def _method(self):
161161
setattr(test_class, method_name, _make(testcase))
162162

163163

164+
def attach_each(test_class, items, action, id_func=str):
165+
"""Attach one ``test_<id>`` method per item to a ``unittest.TestCase``
166+
subclass.
167+
168+
Sister of :func:`attach_tests`. Where ``attach_tests`` works on a
169+
TESTS list of dicts that ``run()`` knows how to execute,
170+
``attach_each`` accepts an arbitrary iterable plus a callable that
171+
decides what to do with each item. Useful for container-image
172+
matrices, file-based fixtures with stateful per-item setup, and
173+
any other pattern that doesn't fit the TESTS-dict shape.
174+
175+
Like ``attach_tests``, this materialises one real test method per
176+
item so unittest counts and names them individually instead of
177+
collapsing the whole loop into a single ``test`` method.
178+
179+
### Parameters
180+
- **test_class** (`type`): a ``unittest.TestCase`` subclass.
181+
- **items** (`iterable`): the things to iterate over (image
182+
tuples, fixture paths, scenario dicts, ...).
183+
- **action** (`callable`): a function ``action(self, item)``
184+
that the generated test method calls with the captured item.
185+
``self`` is the ``unittest.TestCase`` instance and may be
186+
used to issue assertions.
187+
- **id_func** (`callable`, optional): a function that turns one
188+
item into a short, human-readable string used as the test
189+
method name. Defaults to ``str``, which is fine for plain
190+
strings; pass ``lambda it: it[1]`` (or similar) for tuples
191+
and dicts.
192+
193+
### Example
194+
>>> IMAGES = [
195+
... ('quay.io/keycloak/keycloak:25.0.6', 'v25'),
196+
... ('quay.io/keycloak/keycloak:26.6', 'v26'),
197+
... ]
198+
>>>
199+
>>> def _check(test, image_pair):
200+
... image, version_tag = image_pair
201+
... with lib.lftest.run_container(image, ...) as container:
202+
... # ... run plugin, assert ...
203+
... pass
204+
>>>
205+
>>> class TestCheck(unittest.TestCase):
206+
... pass
207+
>>>
208+
>>> attach_each(TestCheck, IMAGES, _check, id_func=lambda it: it[1])
209+
"""
210+
seen = set()
211+
for item in items:
212+
raw_id = id_func(item)
213+
method_name = 'test_' + re.sub(r'\W+', '_', str(raw_id)).strip('_')
214+
if method_name in seen:
215+
raise ValueError(
216+
f'attach_each: duplicate id "{raw_id}" '
217+
f'maps to method name "{method_name}"'
218+
)
219+
seen.add(method_name)
220+
221+
def _make(captured_item):
222+
def _method(self):
223+
action(self, captured_item)
224+
return _method
225+
226+
setattr(test_class, method_name, _make(item))
227+
228+
164229
@contextlib.contextmanager
165230
def run_container(
166231
image,

0 commit comments

Comments
 (0)