Skip to content

Commit 9bbeb54

Browse files
authored
Merge pull request #544 from xcp-ng/stormi/ordering_constraints
Test execution ordering: regroup by axis/package to reduce fixture churn
2 parents 48bdf0e + 4fe1e21 commit 9bbeb54

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)