Skip to content

Commit 4fe1e21

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 eb64569 commit 4fe1e21

1 file changed

Lines changed: 73 additions & 4 deletions

File tree

conftest.py

Lines changed: 73 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import logging
99
import os
1010
import tempfile
11+
from collections import defaultdict
1112

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

46-
from typing import Any, Dict, Generator, Iterable
47+
from typing import Any, Dict, Generator, Iterable, List, Optional
4748

4849
# Do we cache VMs?
4950
try:
@@ -166,10 +167,33 @@ def pytest_generate_tests(metafunc: pytest.Metafunc) -> None:
166167
image_format = ["vhd"] # Not giving image-format will default to doing tests on vhd
167168
metafunc.parametrize("image_format", image_format, scope="session")
168169

169-
def pytest_collection_modifyitems(items: list[pytest.Item], config: pytest.Config) -> None:
170-
# Automatically mark tests based on fixtures they require.
171-
# Check pytest.ini or pytest --markers for marker descriptions.
172170

171+
# Used to group tests together whenever possible and limit parametrized fixture
172+
# "context switching" (needless teardown and setup of SRs, for example)
173+
SCHEDULING_AXES: List[str] = [
174+
"image_format",
175+
]
176+
177+
def get_axis(item: pytest.Item) -> Optional[str]:
178+
callspec = getattr(item, "callspec", None)
179+
if callspec is None:
180+
return None
181+
value = None
182+
# To keep the ordering simple, the current assumption is that only one axis can be defined for a given test.
183+
# For reference, at the time of writing this, the only axis is image_format (qcow2 vs vhd),
184+
# attributed to any test requiring the image_format parametrized fixture.
185+
for axis in SCHEDULING_AXES:
186+
if axis in callspec.params:
187+
assert value is None, "we support at most one parametrized axis per test at the moment"
188+
value = callspec.getparam(axis)
189+
return value
190+
191+
def pytest_collection_modifyitems(items: list[pytest.Item], config: pytest.Config) -> None:
192+
"""
193+
- Automatically mark tests based on fixtures they require.
194+
Check pytest.ini or pytest --markers for marker descriptions.
195+
- Regroup tests by axis (image_format) and by package (leaf directory)
196+
"""
173197
markable_fixtures = [
174198
'uefi_vm',
175199
'unix_vm',
@@ -180,6 +204,9 @@ def pytest_collection_modifyitems(items: list[pytest.Item], config: pytest.Confi
180204
'unused_4k_disks',
181205
]
182206

207+
# -------------
208+
# Apply markers
209+
# -------------
183210
for item in items:
184211
fixturenames = getattr(item, 'fixturenames', ())
185212
for fixturename in markable_fixtures:
@@ -193,6 +220,48 @@ def pytest_collection_modifyitems(items: list[pytest.Item], config: pytest.Confi
193220
# multi_vms implies small_vm
194221
item.add_marker('small_vm')
195222

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

0 commit comments

Comments
 (0)