|
17 | 17 | from . import base, disk, shell |
18 | 18 |
|
19 | 19 | __author__ = 'Linuxfabrik GmbH, Zurich/Switzerland' |
20 | | -__version__ = '2026041302' |
| 20 | +__version__ = '2026041303' |
21 | 21 |
|
22 | 22 |
|
23 | 23 | def run(test_instance, plugin, testcase): |
@@ -161,6 +161,71 @@ def _method(self): |
161 | 161 | setattr(test_class, method_name, _make(testcase)) |
162 | 162 |
|
163 | 163 |
|
| 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 | + |
164 | 229 | @contextlib.contextmanager |
165 | 230 | def run_container( |
166 | 231 | image, |
|
0 commit comments