88import logging
99import os
1010import tempfile
11+ from collections import defaultdict
1112
1213import git
1314from cryptography .hazmat .primitives .serialization import SSHCertPrivateKeyTypes
4344# need to import them in the global conftest.py so that they are recognized as fixtures.
4445from 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?
4950try :
@@ -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