Skip to content

Commit 153a436

Browse files
committed
[8.2.x] fixtures: fix catastrophic performance problem in reorder_items
Manual minimal backport from commit e89d23b. Fix #12355. In the issue, it was reported that the `reorder_items` has quadratic (or worse...) behavior with certain simple parametrizations. After some debugging I found that the problem happens because the "Fix items_by_argkey order" loop keeps adding the same item to the deque, and it reaches epic sizes which causes the slowdown. I don't claim to understand how the `reorder_items` algorithm works, but if as far as I understand, if an item already exists in the deque, the correct thing to do is to move it to the front. Since a deque doesn't have such an (efficient) operation, this switches to `OrderedDict` which can efficiently append from both sides, deduplicate and move to front.
1 parent b41d5a5 commit 153a436

File tree

3 files changed

+31
-8
lines changed

3 files changed

+31
-8
lines changed

changelog/12355.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix possible catastrophic performance slowdown on a certain parametrization pattern involving many higher-scoped parameters.

src/_pytest/fixtures.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from typing import MutableMapping
2424
from typing import NoReturn
2525
from typing import Optional
26+
from typing import OrderedDict
2627
from typing import overload
2728
from typing import Sequence
2829
from typing import Set
@@ -75,8 +76,6 @@
7576

7677

7778
if TYPE_CHECKING:
78-
from typing import Deque
79-
8079
from _pytest.main import Session
8180
from _pytest.python import CallSpec2
8281
from _pytest.python import Function
@@ -207,16 +206,18 @@ def get_parametrized_fixture_keys(
207206

208207
def reorder_items(items: Sequence[nodes.Item]) -> List[nodes.Item]:
209208
argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[FixtureArgKey, None]]] = {}
210-
items_by_argkey: Dict[Scope, Dict[FixtureArgKey, Deque[nodes.Item]]] = {}
209+
items_by_argkey: Dict[
210+
Scope, Dict[FixtureArgKey, OrderedDict[nodes.Item, None]]
211+
] = {}
211212
for scope in HIGH_SCOPES:
212213
scoped_argkeys_cache = argkeys_cache[scope] = {}
213-
scoped_items_by_argkey = items_by_argkey[scope] = defaultdict(deque)
214+
scoped_items_by_argkey = items_by_argkey[scope] = defaultdict(OrderedDict)
214215
for item in items:
215216
keys = dict.fromkeys(get_parametrized_fixture_keys(item, scope), None)
216217
if keys:
217218
scoped_argkeys_cache[item] = keys
218219
for key in keys:
219-
scoped_items_by_argkey[key].append(item)
220+
scoped_items_by_argkey[key][item] = None
220221
items_dict = dict.fromkeys(items, None)
221222
return list(
222223
reorder_items_atscope(items_dict, argkeys_cache, items_by_argkey, Scope.Session)
@@ -226,17 +227,19 @@ def reorder_items(items: Sequence[nodes.Item]) -> List[nodes.Item]:
226227
def fix_cache_order(
227228
item: nodes.Item,
228229
argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[FixtureArgKey, None]]],
229-
items_by_argkey: Dict[Scope, Dict[FixtureArgKey, "Deque[nodes.Item]"]],
230+
items_by_argkey: Dict[Scope, Dict[FixtureArgKey, OrderedDict[nodes.Item, None]]],
230231
) -> None:
231232
for scope in HIGH_SCOPES:
233+
scoped_items_by_argkey = items_by_argkey[scope]
232234
for key in argkeys_cache[scope].get(item, []):
233-
items_by_argkey[scope][key].appendleft(item)
235+
scoped_items_by_argkey[key][item] = None
236+
scoped_items_by_argkey[key].move_to_end(item, last=False)
234237

235238

236239
def reorder_items_atscope(
237240
items: Dict[nodes.Item, None],
238241
argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[FixtureArgKey, None]]],
239-
items_by_argkey: Dict[Scope, Dict[FixtureArgKey, "Deque[nodes.Item]"]],
242+
items_by_argkey: Dict[Scope, Dict[FixtureArgKey, OrderedDict[nodes.Item, None]]],
240243
scope: Scope,
241244
) -> Dict[nodes.Item, None]:
242245
if scope is Scope.Function or len(items) < 3:

testing/python/fixtures.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2219,6 +2219,25 @@ def test_check():
22192219
reprec = pytester.inline_run("-s")
22202220
reprec.assertoutcome(passed=2)
22212221

2222+
def test_reordering_catastrophic_performance(self, pytester: Pytester) -> None:
2223+
"""Check that a certain high-scope parametrization pattern doesn't cause
2224+
a catasrophic slowdown.
2225+
2226+
Regression test for #12355.
2227+
"""
2228+
pytester.makepyfile("""
2229+
import pytest
2230+
2231+
params = tuple("abcdefghijklmnopqrstuvwxyz")
2232+
@pytest.mark.parametrize(params, [range(len(params))] * 3, scope="module")
2233+
def test_parametrize(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z):
2234+
pass
2235+
""")
2236+
2237+
result = pytester.runpytest()
2238+
2239+
result.assert_outcomes(passed=3)
2240+
22222241

22232242
class TestFixtureMarker:
22242243
def test_parametrize(self, pytester: Pytester) -> None:

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy