Skip to content

Commit 5631a86

Browse files
Merge pull request #2862 from tom-dalton-fanduel/issue-2836-fixture-collection-bug
Issue 2836 fixture collection bug
2 parents 7b608f9 + f5e72d2 commit 5631a86

File tree

11 files changed

+109
-15
lines changed

11 files changed

+109
-15
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ Stephan Obermann
164164
Tareq Alayan
165165
Ted Xiao
166166
Thomas Grainger
167+
Tom Dalton
167168
Tom Viner
168169
Trevor Bekolay
169170
Tyler Goodlet

_pytest/fixtures.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,25 @@
11
from __future__ import absolute_import, division, print_function
2-
import sys
32

4-
from py._code.code import FormattedExcinfo
3+
import inspect
4+
import sys
5+
import warnings
56

67
import py
7-
import warnings
8+
from py._code.code import FormattedExcinfo
89

9-
import inspect
1010
import _pytest
11+
from _pytest import nodes
1112
from _pytest._code.code import TerminalRepr
1213
from _pytest.compat import (
1314
NOTSET, exc_clear, _format_args,
1415
getfslineno, get_real_func,
1516
is_generator, isclass, getimfunc,
1617
getlocation, getfuncargnames,
1718
safe_getattr,
19+
FuncargnamesCompatAttr,
1820
)
1921
from _pytest.outcomes import fail, TEST_OUTCOME
20-
from _pytest.compat import FuncargnamesCompatAttr
22+
2123

2224
if sys.version_info[:2] == (2, 6):
2325
from ordereddict import OrderedDict
@@ -981,8 +983,8 @@ def pytest_plugin_registered(self, plugin):
981983
# by their test id)
982984
if p.basename.startswith("conftest.py"):
983985
nodeid = p.dirpath().relto(self.config.rootdir)
984-
if p.sep != "/":
985-
nodeid = nodeid.replace(p.sep, "/")
986+
if p.sep != nodes.SEP:
987+
nodeid = nodeid.replace(p.sep, nodes.SEP)
986988
self.parsefactories(plugin, nodeid)
987989

988990
def _getautousenames(self, nodeid):
@@ -1132,5 +1134,5 @@ def getfixturedefs(self, argname, nodeid):
11321134

11331135
def _matchfactories(self, fixturedefs, nodeid):
11341136
for fixturedef in fixturedefs:
1135-
if nodeid.startswith(fixturedef.baseid):
1137+
if nodes.ischildnode(fixturedef.baseid, nodeid):
11361138
yield fixturedef

_pytest/junitxml.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import sys
1818
import time
1919
import pytest
20+
from _pytest import nodes
2021
from _pytest.config import filename_arg
2122

2223
# Python 2.X and 3.X compatibility
@@ -252,7 +253,7 @@ def mangle_test_address(address):
252253
except ValueError:
253254
pass
254255
# convert file path to dotted path
255-
names[0] = names[0].replace("/", '.')
256+
names[0] = names[0].replace(nodes.SEP, '.')
256257
names[0] = _py_ext_re.sub("", names[0])
257258
# put any params back
258259
names[-1] += possible_open_bracket + params

_pytest/main.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import sys
77

88
import _pytest
9+
from _pytest import nodes
910
import _pytest._code
1011
import py
1112
try:
@@ -14,8 +15,8 @@
1415
from UserDict import DictMixin as MappingMixin
1516

1617
from _pytest.config import directory_arg, UsageError, hookimpl
17-
from _pytest.runner import collect_one_node
1818
from _pytest.outcomes import exit
19+
from _pytest.runner import collect_one_node
1920

2021
tracebackcutdir = py.path.local(_pytest.__file__).dirpath()
2122

@@ -516,14 +517,14 @@ def __init__(self, fspath, parent=None, config=None, session=None):
516517
rel = fspath.relto(parent.fspath)
517518
if rel:
518519
name = rel
519-
name = name.replace(os.sep, "/")
520+
name = name.replace(os.sep, nodes.SEP)
520521
super(FSCollector, self).__init__(name, parent, config, session)
521522
self.fspath = fspath
522523

523524
def _makeid(self):
524525
relpath = self.fspath.relto(self.config.rootdir)
525-
if os.sep != "/":
526-
relpath = relpath.replace(os.sep, "/")
526+
if os.sep != nodes.SEP:
527+
relpath = relpath.replace(os.sep, nodes.SEP)
527528
return relpath
528529

529530

_pytest/nodes.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
SEP = "/"
2+
3+
4+
def _splitnode(nodeid):
5+
"""Split a nodeid into constituent 'parts'.
6+
7+
Node IDs are strings, and can be things like:
8+
''
9+
'testing/code'
10+
'testing/code/test_excinfo.py'
11+
'testing/code/test_excinfo.py::TestFormattedExcinfo::()'
12+
13+
Return values are lists e.g.
14+
[]
15+
['testing', 'code']
16+
['testing', 'code', 'test_excinfo.py']
17+
['testing', 'code', 'test_excinfo.py', 'TestFormattedExcinfo', '()']
18+
"""
19+
if nodeid == '':
20+
# If there is no root node at all, return an empty list so the caller's logic can remain sane
21+
return []
22+
parts = nodeid.split(SEP)
23+
# Replace single last element 'test_foo.py::Bar::()' with multiple elements 'test_foo.py', 'Bar', '()'
24+
parts[-1:] = parts[-1].split("::")
25+
return parts
26+
27+
28+
def ischildnode(baseid, nodeid):
29+
"""Return True if the nodeid is a child node of the baseid.
30+
31+
E.g. 'foo/bar::Baz::()' is a child of 'foo', 'foo/bar' and 'foo/bar::Baz', but not of 'foo/blorp'
32+
"""
33+
base_parts = _splitnode(baseid)
34+
node_parts = _splitnode(nodeid)
35+
if len(node_parts) < len(base_parts):
36+
return False
37+
return node_parts[:len(base_parts)] == base_parts

_pytest/python_api.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,8 @@ def tolerance(self):
217217
absolute tolerance or a relative tolerance, depending on what the user
218218
specified or which would be larger.
219219
"""
220-
def set_default(x, default): return x if x is not None else default
220+
def set_default(x, default):
221+
return x if x is not None else default
221222

222223
# Figure out what the absolute tolerance should be. ``self.abs`` is
223224
# either None or a value specified by the user.

_pytest/terminal.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import time
1414
import platform
1515

16+
from _pytest import nodes
1617
import _pytest._pluggy as pluggy
1718

1819

@@ -452,7 +453,7 @@ def mkrel(nodeid):
452453

453454
if fspath:
454455
res = mkrel(nodeid).replace("::()", "") # parens-normalization
455-
if nodeid.split("::")[0] != fspath.replace("\\", "/"):
456+
if nodeid.split("::")[0] != fspath.replace("\\", nodes.SEP):
456457
res += " <- " + self.startdir.bestrelpath(fspath)
457458
else:
458459
res = "[location]"

changelog/2836.bug

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Match fixture paths against actual path segments in order to avoid matching folders which share a prefix.

testing/test_collection.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import pytest
33
import py
44

5+
import _pytest._code
56
from _pytest.main import Session, EXIT_NOTESTSCOLLECTED, _in_venv
67

78

@@ -830,3 +831,28 @@ def test_continue_on_collection_errors_maxfail(testdir):
830831
"*Interrupted: stopping after 3 failures*",
831832
"*1 failed, 2 error*",
832833
])
834+
835+
836+
def test_fixture_scope_sibling_conftests(testdir):
837+
"""Regression test case for https://github.com/pytest-dev/pytest/issues/2836"""
838+
foo_path = testdir.mkpydir("foo")
839+
foo_path.join("conftest.py").write(_pytest._code.Source("""
840+
import pytest
841+
@pytest.fixture
842+
def fix():
843+
return 1
844+
"""))
845+
foo_path.join("test_foo.py").write("def test_foo(fix): assert fix == 1")
846+
847+
# Tests in `food/` should not see the conftest fixture from `foo/`
848+
food_path = testdir.mkpydir("food")
849+
food_path.join("test_food.py").write("def test_food(fix): assert fix == 1")
850+
851+
res = testdir.runpytest()
852+
assert res.ret == 1
853+
854+
res.stdout.fnmatch_lines([
855+
"*ERROR at setup of test_food*",
856+
"E*fixture 'fix' not found",
857+
"*1 passed, 1 error*",
858+
])

testing/test_nodes.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import pytest
2+
3+
from _pytest import nodes
4+
5+
6+
@pytest.mark.parametrize("baseid, nodeid, expected", (
7+
('', '', True),
8+
('', 'foo', True),
9+
('', 'foo/bar', True),
10+
('', 'foo/bar::TestBaz::()', True),
11+
('foo', 'food', False),
12+
('foo/bar::TestBaz::()', 'foo/bar', False),
13+
('foo/bar::TestBaz::()', 'foo/bar::TestBop::()', False),
14+
('foo/bar', 'foo/bar::TestBop::()', True),
15+
))
16+
def test_ischildnode(baseid, nodeid, expected):
17+
result = nodes.ischildnode(baseid, nodeid)
18+
assert result is expected

tox.ini

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,3 +213,8 @@ filterwarnings =
213213
[flake8]
214214
max-line-length = 120
215215
exclude = _pytest/vendored_packages/pluggy.py
216+
ignore=
217+
# do not use bare except'
218+
E722
219+
# ambiguous variable name 'l'
220+
E741

0 commit comments

Comments
 (0)