Skip to content

Commit 9568250

Browse files
committed
Test execution ordering: regroup by axis/package to reduce fixture churn
pytest's default ordering could interleave image_format=vhd/qcow2 tests in a way that repeatedly destroys and recreates SRs. It could also choose to cross package boundaries as an attempt to optimize test runs, but we'd prefer to see tests logically grouped, for easier log analysis and fixture management (which is perfectible and which we'll have to improve anyway). We start with pytest's computed ordering, then regroup tests by parametrized axis (currently only image_format) and by leaf python package, in order to keep related tests together and reduce needless costly context switching. Axis-less tests intentionally share the same ordering level as the first parametrized axis so they stay near their original position instead of being grouped at the beginning of the run. Interfering with pytest's ordering is not something to do lightly, so it's possible that we may discover issues caused by this. But meanwhile, this has shown a notable reduction (10%) of the number of setup/teardown operations when both vhd and qcow2 image formats are set, and test order closer to what we'd intuitively expect. Signed-off-by: Samuel Verschelde <stormi-xcp@ylix.fr>
1 parent 826fd23 commit 9568250

1 file changed

Lines changed: 71 additions & 3 deletions

File tree

conftest.py

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import logging
88
import os
99
import tempfile
10+
from collections import defaultdict
1011

1112
import git
1213
from cryptography.hazmat.primitives.serialization import SSHCertPrivateKeyTypes
@@ -42,7 +43,7 @@
4243
# need to import them in the global conftest.py so that they are recognized as fixtures.
4344
from pkgfixtures import formatted_and_mounted_ext4_disk, sr_disk_wiped
4445

45-
from typing import Any, Dict, Generator, Iterable
46+
from typing import Any, Dict, Generator, Iterable, List, Optional
4647

4748
# Do we cache VMs?
4849
try:
@@ -153,9 +154,32 @@ def pytest_generate_tests(metafunc: pytest.Metafunc) -> None:
153154
image_format = ["vhd"] # Not giving image-format will default to doing tests on vhd
154155
metafunc.parametrize("image_format", image_format, scope="session")
155156

157+
158+
# Used to group tests together whenever possible and limit parametrized fixture
159+
# "context switching" (needless teardown and setup of SRs, for example)
160+
SCHEDULING_AXES: List[str] = [
161+
"image_format",
162+
]
163+
156164
def pytest_collection_modifyitems(items: list[pytest.Item], config: pytest.Config) -> None:
157-
# Automatically mark tests based on fixtures they require.
158-
# Check pytest.ini or pytest --markers for marker descriptions.
165+
"""
166+
- Automatically mark tests based on fixtures they require.
167+
Check pytest.ini or pytest --markers for marker descriptions.
168+
- Regroup tests by axis (image_format) and by package (leaf directory)
169+
"""
170+
def get_axis(item: pytest.Item) -> Optional[str]:
171+
callspec = getattr(item, "callspec", None)
172+
if callspec is None:
173+
return None
174+
value = None
175+
# To keep the ordering simple, the current assumption is that only one axis can be defined for a given test.
176+
# For reference, at the time of writing this, the only axis is image_format (qcow2 vs vhd),
177+
# attributed to any test requiring the image_format parametrized fixture.
178+
for axis in SCHEDULING_AXES:
179+
if axis in callspec.params:
180+
assert value is None, "we support at most one parametrized axis per test at the moment"
181+
value = callspec.getparam(axis)
182+
return value
159183

160184
markable_fixtures = [
161185
'uefi_vm',
@@ -167,6 +191,9 @@ def pytest_collection_modifyitems(items: list[pytest.Item], config: pytest.Confi
167191
'unused_4k_disks',
168192
]
169193

194+
# -------------
195+
# Apply markers
196+
# -------------
170197
for item in items:
171198
fixturenames = getattr(item, 'fixturenames', ())
172199
for fixturename in markable_fixtures:
@@ -180,6 +207,47 @@ def pytest_collection_modifyitems(items: list[pytest.Item], config: pytest.Confi
180207
# multi_vms implies small_vm
181208
item.add_marker('small_vm')
182209

210+
# -----------------------------------------------------
211+
# Build execution matrix: axis -> leaf package -> items
212+
# -----------------------------------------------------
213+
axis_ordering: Dict[Optional[str], int] = defaultdict(int)
214+
# "None" gets the same order value as the first real axis, on purpose,
215+
# so that we may better retain initial test order.
216+
# For example, if we start with this test order:
217+
# 1. test_A: no axis
218+
# 2. test_B: image_format axis, giving us both test_B[vhd] and testB[qcow2]
219+
# 3. test_C: no axis
220+
# 4. test_D: image_format axis, giving us both test_D[vhd] and testD[qcow2]
221+
# We don't want all axis-less tests grouped at the beginning:
222+
# test_A -> test_C -> test_B[vhd ] -> test_D[vhd] -> test_B[qcow2] -> test_D[qcow2]
223+
# Instead, we want to keep the intial order as much as possible:
224+
# test_A -> test_B[vhd ] -> test_C -> test_D[vhd] -> test_B[qcow2] -> test_D[qcow2]
225+
# Here only the two QCOW2 tests get pushed to the back in order to limit context switching from VHD to QCOW2.
226+
axis_ordering[None] = 1
227+
228+
for item in items:
229+
axis = get_axis(item)
230+
if axis not in axis_ordering:
231+
axis_ordering[axis] = len(axis_ordering) # 1 (same as None's order), then 2, etc.
232+
233+
grouped: Dict[int, Dict[Optional[pytest.Package], list[pytest.Item]]] = defaultdict(lambda: defaultdict(list))
234+
235+
# List the items in the order that pytest initially determined, and add extra grouping criteria.
236+
for item in items:
237+
axis_order = axis_ordering[get_axis(item)]
238+
package = item.getparent(pytest.Package)
239+
grouped[axis_order][package].append(item)
240+
241+
# Flatten back to a list of items
242+
new_items: List[pytest.Item] = [
243+
item
244+
for axis_order in sorted(grouped) # apply axis_ordering here
245+
for package in grouped[axis_order]
246+
for item in grouped[axis_order][package]
247+
]
248+
items[:] = new_items
249+
250+
183251
# BEGIN make test results visible from fixtures
184252
# from https://docs.pytest.org/en/latest/example/simple.html#making-test-result-information-available-in-fixtures
185253

0 commit comments

Comments
 (0)