Skip to content

Commit d73e689

Browse files
committed
fix issue616 - conftest visibility fixes. This is achieved by
refactoring how nodeid's are constructed. They now are always relative to the "common rootdir" of a test run which is determined by finding a common ancestor of all testrun arguments. --HG-- branch : issue616
1 parent aa757f7 commit d73e689

File tree

13 files changed

+327
-79
lines changed

13 files changed

+327
-79
lines changed

CHANGELOG

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,20 @@
11
2.7.0.dev (compared to 2.6.4)
22
-----------------------------
33

4+
- fix issue616: conftest.py files and their contained fixutres are now
5+
properly considered for visibility, independently from the exact
6+
current working directory and test arguments that are used.
7+
Many thanks to Eric Siegerman and his PR235 which contains
8+
systematic tests for conftest visibility and now passes.
9+
This change also introduces the concept of a ``rootdir`` which
10+
is printed as a new pytest header and documented in the pytest
11+
customize web page.
12+
13+
- change reporting of "diverted" tests, i.e. tests that are collected
14+
in one file but actually come from another (e.g. when tests in a test class
15+
come from a base class in a different file). We now show the nodeid
16+
and indicate via a postfix the other file.
17+
418
- add ability to set command line options by environment variable PYTEST_ADDOPTS.
519

620
- fix issue655: work around different ways that cause python2/3

_pytest/config.py

Lines changed: 61 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -657,6 +657,12 @@ def notify_exception(self, excinfo, option=None):
657657
sys.stderr.write("INTERNALERROR> %s\n" %line)
658658
sys.stderr.flush()
659659

660+
def cwd_relative_nodeid(self, nodeid):
661+
# nodeid's are relative to the rootpath, compute relative to cwd
662+
if self.invocation_dir != self.rootdir:
663+
fullpath = self.rootdir.join(nodeid)
664+
nodeid = self.invocation_dir.bestrelpath(fullpath)
665+
return nodeid
660666

661667
@classmethod
662668
def fromdictargs(cls, option_dict, args):
@@ -691,14 +697,9 @@ def pytest_load_initial_conftests(self, early_config):
691697

692698
def _initini(self, args):
693699
parsed_args = self._parser.parse_known_args(args)
694-
if parsed_args.inifilename:
695-
iniconfig = py.iniconfig.IniConfig(parsed_args.inifilename)
696-
if 'pytest' in iniconfig.sections:
697-
self.inicfg = iniconfig['pytest']
698-
else:
699-
self.inicfg = {}
700-
else:
701-
self.inicfg = getcfg(args, ["pytest.ini", "tox.ini", "setup.cfg"])
700+
r = determine_setup(parsed_args.inifilename, parsed_args.file_or_dir)
701+
self.rootdir, self.inifile, self.inicfg = r
702+
self.invocation_dir = py.path.local()
702703
self._parser.addini('addopts', 'extra command line options', 'args')
703704
self._parser.addini('minversion', 'minimally required pytest version')
704705

@@ -859,8 +860,58 @@ def getcfg(args, inibasenames):
859860
if exists(p):
860861
iniconfig = py.iniconfig.IniConfig(p)
861862
if 'pytest' in iniconfig.sections:
862-
return iniconfig['pytest']
863-
return {}
863+
return base, p, iniconfig['pytest']
864+
elif inibasename == "pytest.ini":
865+
# allowed to be empty
866+
return base, p, {}
867+
return None, None, None
868+
869+
870+
def get_common_ancestor(args):
871+
# args are what we get after early command line parsing (usually
872+
# strings, but can be py.path.local objects as well)
873+
common_ancestor = None
874+
for arg in args:
875+
if str(arg)[0] == "-":
876+
continue
877+
p = py.path.local(arg)
878+
if common_ancestor is None:
879+
common_ancestor = p
880+
else:
881+
if p.relto(common_ancestor) or p == common_ancestor:
882+
continue
883+
elif common_ancestor.relto(p):
884+
common_ancestor = p
885+
else:
886+
shared = p.common(common_ancestor)
887+
if shared is not None:
888+
common_ancestor = shared
889+
if common_ancestor is None:
890+
common_ancestor = py.path.local()
891+
elif not common_ancestor.isdir():
892+
common_ancestor = common_ancestor.dirpath()
893+
return common_ancestor
894+
895+
896+
def determine_setup(inifile, args):
897+
if inifile:
898+
iniconfig = py.iniconfig.IniConfig(inifile)
899+
try:
900+
inicfg = iniconfig["pytest"]
901+
except KeyError:
902+
inicfg = None
903+
rootdir = get_common_ancestor(args)
904+
else:
905+
ancestor = get_common_ancestor(args)
906+
rootdir, inifile, inicfg = getcfg(
907+
[ancestor], ["pytest.ini", "tox.ini", "setup.cfg"])
908+
if rootdir is None:
909+
for rootdir in ancestor.parts(reverse=True):
910+
if rootdir.join("setup.py").exists():
911+
break
912+
else:
913+
rootdir = ancestor
914+
return rootdir, inifile, inicfg or {}
864915

865916

866917
def setns(obj, dic):

_pytest/main.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -457,9 +457,7 @@ def __init__(self, fspath, parent=None, config=None, session=None):
457457
self.fspath = fspath
458458

459459
def _makeid(self):
460-
if self == self.session:
461-
return "."
462-
relpath = self.session.fspath.bestrelpath(self.fspath)
460+
relpath = self.fspath.relto(self.config.rootdir)
463461
if os.sep != "/":
464462
relpath = relpath.replace(os.sep, "/")
465463
return relpath
@@ -510,7 +508,7 @@ class Interrupted(KeyboardInterrupt):
510508
__module__ = 'builtins' # for py3
511509

512510
def __init__(self, config):
513-
FSCollector.__init__(self, py.path.local(), parent=None,
511+
FSCollector.__init__(self, config.rootdir, parent=None,
514512
config=config, session=self)
515513
self.config.pluginmanager.register(self, name="session", prepend=True)
516514
self._testsfailed = 0
@@ -520,6 +518,9 @@ def __init__(self, config):
520518
self.startdir = py.path.local()
521519
self._fs2hookproxy = {}
522520

521+
def _makeid(self):
522+
return ""
523+
523524
def pytest_collectstart(self):
524525
if self.shouldstop:
525526
raise self.Interrupted(self.shouldstop)
@@ -663,7 +664,7 @@ def _parsearg(self, arg):
663664
arg = self._tryconvertpyarg(arg)
664665
parts = str(arg).split("::")
665666
relpath = parts[0].replace("/", os.sep)
666-
path = self.fspath.join(relpath, abs=True)
667+
path = self.config.invocation_dir.join(relpath, abs=True)
667668
if not path.check():
668669
if self.config.option.pyargs:
669670
msg = "file or package not found: "

_pytest/pytester.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -306,9 +306,8 @@ def getnode(self, config, arg):
306306
session = Session(config)
307307
assert '::' not in str(arg)
308308
p = py.path.local(arg)
309-
x = session.fspath.bestrelpath(p)
310309
config.hook.pytest_sessionstart(session=session)
311-
res = session.perform_collect([x], genitems=False)[0]
310+
res = session.perform_collect([str(p)], genitems=False)[0]
312311
config.hook.pytest_sessionfinish(session=session, exitstatus=EXIT_OK)
313312
return res
314313

@@ -395,8 +394,7 @@ def ensure_unconfigure():
395394
def parseconfigure(self, *args):
396395
config = self.parseconfig(*args)
397396
config.do_configure()
398-
self.request.addfinalizer(lambda:
399-
config.do_unconfigure())
397+
self.request.addfinalizer(config.do_unconfigure)
400398
return config
401399

402400
def getitem(self, source, funcname="test_func"):

_pytest/python.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1653,11 +1653,9 @@ def pytest_plugin_registered(self, plugin):
16531653
# what fixtures are visible for particular tests (as denoted
16541654
# by their test id)
16551655
if p.basename.startswith("conftest.py"):
1656-
nodeid = self.session.fspath.bestrelpath(p.dirpath())
1656+
nodeid = p.dirpath().relto(self.config.rootdir)
16571657
if p.sep != "/":
16581658
nodeid = nodeid.replace(p.sep, "/")
1659-
if nodeid == ".":
1660-
nodeid = ""
16611659
self.parsefactories(plugin, nodeid)
16621660
self._seenplugins.add(plugin)
16631661

_pytest/skipping.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -218,14 +218,14 @@ def show_simple(terminalreporter, lines, stat, format):
218218
failed = terminalreporter.stats.get(stat)
219219
if failed:
220220
for rep in failed:
221-
pos = rep.nodeid
222-
lines.append(format %(pos, ))
221+
pos = terminalreporter.config.cwd_relative_nodeid(rep.nodeid)
222+
lines.append(format %(pos,))
223223

224224
def show_xfailed(terminalreporter, lines):
225225
xfailed = terminalreporter.stats.get("xfailed")
226226
if xfailed:
227227
for rep in xfailed:
228-
pos = rep.nodeid
228+
pos = terminalreporter.config.cwd_relative_nodeid(rep.nodeid)
229229
reason = rep.wasxfail
230230
lines.append("XFAIL %s" % (pos,))
231231
if reason:
@@ -235,7 +235,7 @@ def show_xpassed(terminalreporter, lines):
235235
xpassed = terminalreporter.stats.get("xpassed")
236236
if xpassed:
237237
for rep in xpassed:
238-
pos = rep.nodeid
238+
pos = terminalreporter.config.cwd_relative_nodeid(rep.nodeid)
239239
reason = rep.wasxfail
240240
lines.append("XPASS %s %s" %(pos, reason))
241241

_pytest/terminal.py

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ def __init__(self, config, file=None):
9595
self._numcollected = 0
9696

9797
self.stats = {}
98-
self.startdir = self.curdir = py.path.local()
98+
self.startdir = py.path.local()
9999
if file is None:
100100
file = sys.stdout
101101
self._tw = self.writer = py.io.TerminalWriter(file)
@@ -111,12 +111,12 @@ def hasopt(self, char):
111111
char = {'xfailed': 'x', 'skipped': 's'}.get(char, char)
112112
return char in self.reportchars
113113

114-
def write_fspath_result(self, fspath, res):
114+
def write_fspath_result(self, nodeid, res):
115+
fspath = self.config.rootdir.join(nodeid.split("::")[0])
115116
if fspath != self.currentfspath:
116117
self.currentfspath = fspath
117-
#fspath = self.startdir.bestrelpath(fspath)
118+
fspath = self.startdir.bestrelpath(fspath)
118119
self._tw.line()
119-
#relpath = self.startdir.bestrelpath(fspath)
120120
self._tw.write(fspath + " ")
121121
self._tw.write(res)
122122

@@ -182,12 +182,12 @@ def pytest_deselected(self, items):
182182
def pytest_runtest_logstart(self, nodeid, location):
183183
# ensure that the path is printed before the
184184
# 1st test of a module starts running
185-
fspath = nodeid.split("::")[0]
186185
if self.showlongtestinfo:
187-
line = self._locationline(fspath, *location)
186+
line = self._locationline(nodeid, *location)
188187
self.write_ensure_prefix(line, "")
189188
elif self.showfspath:
190-
self.write_fspath_result(fspath, "")
189+
fsid = nodeid.split("::")[0]
190+
self.write_fspath_result(fsid, "")
191191

192192
def pytest_runtest_logreport(self, report):
193193
rep = report
@@ -200,7 +200,7 @@ def pytest_runtest_logreport(self, report):
200200
return
201201
if self.verbosity <= 0:
202202
if not hasattr(rep, 'node') and self.showfspath:
203-
self.write_fspath_result(rep.fspath, letter)
203+
self.write_fspath_result(rep.nodeid, letter)
204204
else:
205205
self._tw.write(letter)
206206
else:
@@ -213,7 +213,7 @@ def pytest_runtest_logreport(self, report):
213213
markup = {'red':True}
214214
elif rep.skipped:
215215
markup = {'yellow':True}
216-
line = self._locationline(str(rep.fspath), *rep.location)
216+
line = self._locationline(rep.nodeid, *rep.location)
217217
if not hasattr(rep, 'node'):
218218
self.write_ensure_prefix(line, word, **markup)
219219
#self._tw.write(word, **markup)
@@ -237,7 +237,7 @@ def pytest_collectreport(self, report):
237237
items = [x for x in report.result if isinstance(x, pytest.Item)]
238238
self._numcollected += len(items)
239239
if self.hasmarkup:
240-
#self.write_fspath_result(report.fspath, 'E')
240+
#self.write_fspath_result(report.nodeid, 'E')
241241
self.report_collect()
242242

243243
def report_collect(self, final=False):
@@ -288,6 +288,10 @@ def pytest_sessionstart(self, session):
288288
self.write_line(line)
289289

290290
def pytest_report_header(self, config):
291+
inifile = ""
292+
if config.inifile:
293+
inifile = config.rootdir.bestrelpath(config.inifile)
294+
lines = ["rootdir: %s, inifile: %s" %(config.rootdir, inifile)]
291295
plugininfo = config.pluginmanager._plugin_distinfo
292296
if plugininfo:
293297
l = []
@@ -296,7 +300,8 @@ def pytest_report_header(self, config):
296300
if name.startswith("pytest-"):
297301
name = name[7:]
298302
l.append(name)
299-
return "plugins: %s" % ", ".join(l)
303+
lines.append("plugins: %s" % ", ".join(l))
304+
return lines
300305

301306
def pytest_collection_finish(self, session):
302307
if self.config.option.collectonly:
@@ -378,19 +383,24 @@ def _report_keyboardinterrupt(self):
378383
else:
379384
excrepr.reprcrash.toterminal(self._tw)
380385

381-
def _locationline(self, collect_fspath, fspath, lineno, domain):
386+
def _locationline(self, nodeid, fspath, lineno, domain):
387+
def mkrel(nodeid):
388+
line = self.config.cwd_relative_nodeid(nodeid)
389+
if domain and line.endswith(domain):
390+
line = line[:-len(domain)]
391+
l = domain.split("[")
392+
l[0] = l[0].replace('.', '::') # don't replace '.' in params
393+
line += "[".join(l)
394+
return line
382395
# collect_fspath comes from testid which has a "/"-normalized path
383-
if fspath and fspath.replace("\\", "/") != collect_fspath:
384-
fspath = "%s <- %s" % (collect_fspath, fspath)
396+
385397
if fspath:
386-
line = str(fspath)
387-
if domain:
388-
split = str(domain).split('[')
389-
split[0] = split[0].replace('.', '::') # don't replace '.' in params
390-
line += "::" + '['.join(split)
398+
res = mkrel(nodeid).replace("::()", "") # parens-normalization
399+
if nodeid.split("::")[0] != fspath.replace("\\", "/"):
400+
res += " <- " + self.startdir.bestrelpath(fspath)
391401
else:
392-
line = "[location]"
393-
return line + " "
402+
res = "[location]"
403+
return res + " "
394404

395405
def _getfailureheadline(self, rep):
396406
if hasattr(rep, 'location'):

0 commit comments

Comments
 (0)