Merge pull request #2862 from tom-dalton-fanduel/issue-2836-fixture-collection-bug

Issue 2836 fixture collection bug
This commit is contained in:
Ronny Pfannschmidt 2017-10-24 16:32:50 +02:00 committed by GitHub
commit 5631a86296
11 changed files with 111 additions and 17 deletions

View File

@ -164,6 +164,7 @@ Stephan Obermann
Tareq Alayan Tareq Alayan
Ted Xiao Ted Xiao
Thomas Grainger Thomas Grainger
Tom Dalton
Tom Viner Tom Viner
Trevor Bekolay Trevor Bekolay
Tyler Goodlet Tyler Goodlet

View File

@ -1,13 +1,14 @@
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
import sys
from py._code.code import FormattedExcinfo
import py
import warnings
import inspect import inspect
import sys
import warnings
import py
from py._code.code import FormattedExcinfo
import _pytest import _pytest
from _pytest import nodes
from _pytest._code.code import TerminalRepr from _pytest._code.code import TerminalRepr
from _pytest.compat import ( from _pytest.compat import (
NOTSET, exc_clear, _format_args, NOTSET, exc_clear, _format_args,
@ -15,9 +16,10 @@ from _pytest.compat import (
is_generator, isclass, getimfunc, is_generator, isclass, getimfunc,
getlocation, getfuncargnames, getlocation, getfuncargnames,
safe_getattr, safe_getattr,
FuncargnamesCompatAttr,
) )
from _pytest.outcomes import fail, TEST_OUTCOME from _pytest.outcomes import fail, TEST_OUTCOME
from _pytest.compat import FuncargnamesCompatAttr
if sys.version_info[:2] == (2, 6): if sys.version_info[:2] == (2, 6):
from ordereddict import OrderedDict from ordereddict import OrderedDict
@ -981,8 +983,8 @@ class FixtureManager:
# by their test id) # by their test id)
if p.basename.startswith("conftest.py"): if p.basename.startswith("conftest.py"):
nodeid = p.dirpath().relto(self.config.rootdir) nodeid = p.dirpath().relto(self.config.rootdir)
if p.sep != "/": if p.sep != nodes.SEP:
nodeid = nodeid.replace(p.sep, "/") nodeid = nodeid.replace(p.sep, nodes.SEP)
self.parsefactories(plugin, nodeid) self.parsefactories(plugin, nodeid)
def _getautousenames(self, nodeid): def _getautousenames(self, nodeid):
@ -1132,5 +1134,5 @@ class FixtureManager:
def _matchfactories(self, fixturedefs, nodeid): def _matchfactories(self, fixturedefs, nodeid):
for fixturedef in fixturedefs: for fixturedef in fixturedefs:
if nodeid.startswith(fixturedef.baseid): if nodes.ischildnode(fixturedef.baseid, nodeid):
yield fixturedef yield fixturedef

View File

@ -17,6 +17,7 @@ import re
import sys import sys
import time import time
import pytest import pytest
from _pytest import nodes
from _pytest.config import filename_arg from _pytest.config import filename_arg
# Python 2.X and 3.X compatibility # Python 2.X and 3.X compatibility
@ -252,7 +253,7 @@ def mangle_test_address(address):
except ValueError: except ValueError:
pass pass
# convert file path to dotted path # convert file path to dotted path
names[0] = names[0].replace("/", '.') names[0] = names[0].replace(nodes.SEP, '.')
names[0] = _py_ext_re.sub("", names[0]) names[0] = _py_ext_re.sub("", names[0])
# put any params back # put any params back
names[-1] += possible_open_bracket + params names[-1] += possible_open_bracket + params

View File

@ -6,6 +6,7 @@ import os
import sys import sys
import _pytest import _pytest
from _pytest import nodes
import _pytest._code import _pytest._code
import py import py
try: try:
@ -14,8 +15,8 @@ except ImportError:
from UserDict import DictMixin as MappingMixin from UserDict import DictMixin as MappingMixin
from _pytest.config import directory_arg, UsageError, hookimpl from _pytest.config import directory_arg, UsageError, hookimpl
from _pytest.runner import collect_one_node
from _pytest.outcomes import exit from _pytest.outcomes import exit
from _pytest.runner import collect_one_node
tracebackcutdir = py.path.local(_pytest.__file__).dirpath() tracebackcutdir = py.path.local(_pytest.__file__).dirpath()
@ -516,14 +517,14 @@ class FSCollector(Collector):
rel = fspath.relto(parent.fspath) rel = fspath.relto(parent.fspath)
if rel: if rel:
name = rel name = rel
name = name.replace(os.sep, "/") name = name.replace(os.sep, nodes.SEP)
super(FSCollector, self).__init__(name, parent, config, session) super(FSCollector, self).__init__(name, parent, config, session)
self.fspath = fspath self.fspath = fspath
def _makeid(self): def _makeid(self):
relpath = self.fspath.relto(self.config.rootdir) relpath = self.fspath.relto(self.config.rootdir)
if os.sep != "/": if os.sep != nodes.SEP:
relpath = relpath.replace(os.sep, "/") relpath = relpath.replace(os.sep, nodes.SEP)
return relpath return relpath

37
_pytest/nodes.py Normal file
View File

@ -0,0 +1,37 @@
SEP = "/"
def _splitnode(nodeid):
"""Split a nodeid into constituent 'parts'.
Node IDs are strings, and can be things like:
''
'testing/code'
'testing/code/test_excinfo.py'
'testing/code/test_excinfo.py::TestFormattedExcinfo::()'
Return values are lists e.g.
[]
['testing', 'code']
['testing', 'code', 'test_excinfo.py']
['testing', 'code', 'test_excinfo.py', 'TestFormattedExcinfo', '()']
"""
if nodeid == '':
# If there is no root node at all, return an empty list so the caller's logic can remain sane
return []
parts = nodeid.split(SEP)
# Replace single last element 'test_foo.py::Bar::()' with multiple elements 'test_foo.py', 'Bar', '()'
parts[-1:] = parts[-1].split("::")
return parts
def ischildnode(baseid, nodeid):
"""Return True if the nodeid is a child node of the baseid.
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

View File

@ -217,7 +217,8 @@ class ApproxScalar(ApproxBase):
absolute tolerance or a relative tolerance, depending on what the user absolute tolerance or a relative tolerance, depending on what the user
specified or which would be larger. specified or which would be larger.
""" """
def set_default(x, default): return x if x is not None else default def set_default(x, default):
return x if x is not None else default
# Figure out what the absolute tolerance should be. ``self.abs`` is # Figure out what the absolute tolerance should be. ``self.abs`` is
# either None or a value specified by the user. # either None or a value specified by the user.

View File

@ -13,6 +13,7 @@ import sys
import time import time
import platform import platform
from _pytest import nodes
import _pytest._pluggy as pluggy import _pytest._pluggy as pluggy
@ -452,7 +453,7 @@ class TerminalReporter:
if fspath: if fspath:
res = mkrel(nodeid).replace("::()", "") # parens-normalization res = mkrel(nodeid).replace("::()", "") # parens-normalization
if nodeid.split("::")[0] != fspath.replace("\\", "/"): if nodeid.split("::")[0] != fspath.replace("\\", nodes.SEP):
res += " <- " + self.startdir.bestrelpath(fspath) res += " <- " + self.startdir.bestrelpath(fspath)
else: else:
res = "[location]" res = "[location]"

1
changelog/2836.bug Normal file
View File

@ -0,0 +1 @@
Match fixture paths against actual path segments in order to avoid matching folders which share a prefix.

View File

@ -2,6 +2,7 @@ from __future__ import absolute_import, division, print_function
import pytest import pytest
import py import py
import _pytest._code
from _pytest.main import Session, EXIT_NOTESTSCOLLECTED, _in_venv from _pytest.main import Session, EXIT_NOTESTSCOLLECTED, _in_venv
@ -830,3 +831,28 @@ def test_continue_on_collection_errors_maxfail(testdir):
"*Interrupted: stopping after 3 failures*", "*Interrupted: stopping after 3 failures*",
"*1 failed, 2 error*", "*1 failed, 2 error*",
]) ])
def test_fixture_scope_sibling_conftests(testdir):
"""Regression test case for https://github.com/pytest-dev/pytest/issues/2836"""
foo_path = testdir.mkpydir("foo")
foo_path.join("conftest.py").write(_pytest._code.Source("""
import pytest
@pytest.fixture
def fix():
return 1
"""))
foo_path.join("test_foo.py").write("def test_foo(fix): assert fix == 1")
# Tests in `food/` should not see the conftest fixture from `foo/`
food_path = testdir.mkpydir("food")
food_path.join("test_food.py").write("def test_food(fix): assert fix == 1")
res = testdir.runpytest()
assert res.ret == 1
res.stdout.fnmatch_lines([
"*ERROR at setup of test_food*",
"E*fixture 'fix' not found",
"*1 passed, 1 error*",
])

18
testing/test_nodes.py Normal file
View File

@ -0,0 +1,18 @@
import pytest
from _pytest import nodes
@pytest.mark.parametrize("baseid, nodeid, expected", (
('', '', True),
('', 'foo', True),
('', 'foo/bar', True),
('', 'foo/bar::TestBaz::()', True),
('foo', 'food', False),
('foo/bar::TestBaz::()', 'foo/bar', False),
('foo/bar::TestBaz::()', 'foo/bar::TestBop::()', False),
('foo/bar', 'foo/bar::TestBop::()', True),
))
def test_ischildnode(baseid, nodeid, expected):
result = nodes.ischildnode(baseid, nodeid)
assert result is expected

View File

@ -213,3 +213,8 @@ filterwarnings =
[flake8] [flake8]
max-line-length = 120 max-line-length = 120
exclude = _pytest/vendored_packages/pluggy.py exclude = _pytest/vendored_packages/pluggy.py
ignore=
# do not use bare except'
E722
# ambiguous variable name 'l'
E741