Merge pull request #7931 from bluetech/xunit-quadratic-2

fixtures: fix quadratic behavior in the number of autouse fixtures
This commit is contained in:
Ran Benita 2020-10-25 01:24:38 +03:00 committed by GitHub
commit 65e6e39b76
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 63 additions and 69 deletions

View File

@ -0,0 +1 @@
Fixed quadratic behavior and improved performance of collection of items using autouse fixtures and xunit fixtures.

View File

@ -1412,9 +1412,10 @@ class FixtureManager:
self.config: Config = session.config self.config: Config = session.config
self._arg2fixturedefs: Dict[str, List[FixtureDef[Any]]] = {} self._arg2fixturedefs: Dict[str, List[FixtureDef[Any]]] = {}
self._holderobjseen: Set[object] = set() self._holderobjseen: Set[object] = set()
self._nodeid_and_autousenames: List[Tuple[str, List[str]]] = [ # A mapping from a nodeid to a list of autouse fixtures it defines.
("", self.config.getini("usefixtures")) self._nodeid_autousenames: Dict[str, List[str]] = {
] "": self.config.getini("usefixtures"),
}
session.config.pluginmanager.register(self, "funcmanage") session.config.pluginmanager.register(self, "funcmanage")
def _get_direct_parametrize_args(self, node: nodes.Node) -> List[str]: def _get_direct_parametrize_args(self, node: nodes.Node) -> List[str]:
@ -1476,18 +1477,12 @@ class FixtureManager:
self.parsefactories(plugin, nodeid) self.parsefactories(plugin, nodeid)
def _getautousenames(self, nodeid: str) -> List[str]: def _getautousenames(self, nodeid: str) -> Iterator[str]:
"""Return a list of fixture names to be used.""" """Return the names of autouse fixtures applicable to nodeid."""
autousenames: List[str] = [] for parentnodeid in nodes.iterparentnodeids(nodeid):
for baseid, basenames in self._nodeid_and_autousenames: basenames = self._nodeid_autousenames.get(parentnodeid)
if nodeid.startswith(baseid): if basenames:
if baseid: yield from basenames
i = len(baseid)
nextchar = nodeid[i : i + 1]
if nextchar and nextchar not in ":/":
continue
autousenames.extend(basenames)
return autousenames
def getfixtureclosure( def getfixtureclosure(
self, self,
@ -1503,7 +1498,7 @@ class FixtureManager:
# (discovering matching fixtures for a given name/node is expensive). # (discovering matching fixtures for a given name/node is expensive).
parentid = parentnode.nodeid parentid = parentnode.nodeid
fixturenames_closure = self._getautousenames(parentid) fixturenames_closure = list(self._getautousenames(parentid))
def merge(otherlist: Iterable[str]) -> None: def merge(otherlist: Iterable[str]) -> None:
for arg in otherlist: for arg in otherlist:
@ -1648,7 +1643,7 @@ class FixtureManager:
autousenames.append(name) autousenames.append(name)
if autousenames: if autousenames:
self._nodeid_and_autousenames.append((nodeid or "", autousenames)) self._nodeid_autousenames.setdefault(nodeid or "", []).extend(autousenames)
def getfixturedefs( def getfixturedefs(
self, argname: str, nodeid: str self, argname: str, nodeid: str
@ -1668,6 +1663,7 @@ class FixtureManager:
def _matchfactories( def _matchfactories(
self, fixturedefs: Iterable[FixtureDef[Any]], nodeid: str self, fixturedefs: Iterable[FixtureDef[Any]], nodeid: str
) -> Iterator[FixtureDef[Any]]: ) -> Iterator[FixtureDef[Any]]:
parentnodeids = set(nodes.iterparentnodeids(nodeid))
for fixturedef in fixturedefs: for fixturedef in fixturedefs:
if nodes.ischildnode(fixturedef.baseid, nodeid): if fixturedef.baseid in parentnodeids:
yield fixturedef yield fixturedef

View File

@ -1,6 +1,5 @@
import os import os
import warnings import warnings
from functools import lru_cache
from pathlib import Path from pathlib import Path
from typing import Callable from typing import Callable
from typing import Iterable from typing import Iterable
@ -44,46 +43,39 @@ SEP = "/"
tracebackcutdir = py.path.local(_pytest.__file__).dirpath() tracebackcutdir = py.path.local(_pytest.__file__).dirpath()
@lru_cache(maxsize=None) def iterparentnodeids(nodeid: str) -> Iterator[str]:
def _splitnode(nodeid: str) -> Tuple[str, ...]: """Return the parent node IDs of a given node ID, inclusive.
"""Split a nodeid into constituent 'parts'.
Node IDs are strings, and can be things like: For the node ID
''
'testing/code'
'testing/code/test_excinfo.py'
'testing/code/test_excinfo.py::TestFormattedExcinfo'
Return values are lists e.g. "testing/code/test_excinfo.py::TestFormattedExcinfo::test_repr_source"
[]
['testing', 'code'] the result would be
['testing', 'code', 'test_excinfo.py']
['testing', 'code', 'test_excinfo.py', 'TestFormattedExcinfo'] ""
"testing"
"testing/code"
"testing/code/test_excinfo.py"
"testing/code/test_excinfo.py::TestFormattedExcinfo"
"testing/code/test_excinfo.py::TestFormattedExcinfo::test_repr_source"
Note that :: parts are only considered at the last / component.
""" """
if nodeid == "": pos = 0
# If there is no root node at all, return an empty list so the caller's sep = SEP
# logic can remain sane. yield ""
return () while True:
parts = nodeid.split(SEP) at = nodeid.find(sep, pos)
# Replace single last element 'test_foo.py::Bar' with multiple elements if at == -1 and sep == SEP:
# 'test_foo.py', 'Bar'. sep = "::"
parts[-1:] = parts[-1].split("::") elif at == -1:
# Convert parts into a tuple to avoid possible errors with caching of a if nodeid:
# mutable type. yield nodeid
return tuple(parts) break
else:
if at:
def ischildnode(baseid: str, nodeid: str) -> bool: yield nodeid[:at]
"""Return True if the nodeid is a child node of the baseid. pos = at + len(sep)
E.g. 'foo/bar::Baz' is a child of 'foo', 'foo/bar' and 'foo/bar::Baz',
but not of 'foo/blorp'.
"""
base_parts = _splitnode(baseid)
node_parts = _splitnode(nodeid)
if len(node_parts) < len(base_parts):
return False
return node_parts[: len(base_parts)] == base_parts
_NodeType = TypeVar("_NodeType", bound="Node") _NodeType = TypeVar("_NodeType", bound="Node")

View File

@ -1710,7 +1710,7 @@ class TestAutouseDiscovery:
""" """
from _pytest.pytester import get_public_names from _pytest.pytester import get_public_names
def test_check_setup(item, fm): def test_check_setup(item, fm):
autousenames = fm._getautousenames(item.nodeid) autousenames = list(fm._getautousenames(item.nodeid))
assert len(get_public_names(autousenames)) == 2 assert len(get_public_names(autousenames)) == 2
assert "perfunction2" in autousenames assert "perfunction2" in autousenames
assert "perfunction" in autousenames assert "perfunction" in autousenames

View File

@ -1,3 +1,5 @@
from typing import List
import py import py
import pytest import pytest
@ -6,21 +8,24 @@ from _pytest.pytester import Testdir
@pytest.mark.parametrize( @pytest.mark.parametrize(
"baseid, nodeid, expected", ("nodeid", "expected"),
( (
("", "", True), ("", [""]),
("", "foo", True), ("a", ["", "a"]),
("", "foo/bar", True), ("aa/b", ["", "aa", "aa/b"]),
("", "foo/bar::TestBaz", True), ("a/b/c", ["", "a", "a/b", "a/b/c"]),
("foo", "food", False), ("a/bbb/c::D", ["", "a", "a/bbb", "a/bbb/c", "a/bbb/c::D"]),
("foo/bar::TestBaz", "foo/bar", False), ("a/b/c::D::eee", ["", "a", "a/b", "a/b/c", "a/b/c::D", "a/b/c::D::eee"]),
("foo/bar::TestBaz", "foo/bar::TestBop", False), # :: considered only at the last component.
("foo/bar", "foo/bar::TestBop", True), ("::xx", ["", "::xx"]),
("a/b/c::D/d::e", ["", "a", "a/b", "a/b/c::D", "a/b/c::D/d", "a/b/c::D/d::e"]),
# : alone is not a separator.
("a/b::D:e:f::g", ["", "a", "a/b", "a/b::D:e:f", "a/b::D:e:f::g"]),
), ),
) )
def test_ischildnode(baseid: str, nodeid: str, expected: bool) -> None: def test_iterparentnodeids(nodeid: str, expected: List[str]) -> None:
result = nodes.ischildnode(baseid, nodeid) result = list(nodes.iterparentnodeids(nodeid))
assert result is expected assert result == expected
def test_node_from_parent_disallowed_arguments() -> None: def test_node_from_parent_disallowed_arguments() -> None: