Merged in issue616 (pull request #252)

fix issue616 - conftest visibility fixes.
This commit is contained in:
Ronny Pfannschmidt 2015-02-28 10:02:58 +01:00
commit 1e6e373913
13 changed files with 328 additions and 81 deletions

View File

@ -1,6 +1,20 @@
2.7.0.dev (compared to 2.6.4)
-----------------------------
- fix issue616: conftest.py files and their contained fixutres are now
properly considered for visibility, independently from the exact
current working directory and test arguments that are used.
Many thanks to Eric Siegerman and his PR235 which contains
systematic tests for conftest visibility and now passes.
This change also introduces the concept of a ``rootdir`` which
is printed as a new pytest header and documented in the pytest
customize web page.
- change reporting of "diverted" tests, i.e. tests that are collected
in one file but actually come from another (e.g. when tests in a test class
come from a base class in a different file). We now show the nodeid
and indicate via a postfix the other file.
- add ability to set command line options by environment variable PYTEST_ADDOPTS.
- added documentation on the new pytest-dev teams on bitbucket and

View File

@ -657,6 +657,12 @@ class Config(object):
sys.stderr.write("INTERNALERROR> %s\n" %line)
sys.stderr.flush()
def cwd_relative_nodeid(self, nodeid):
# nodeid's are relative to the rootpath, compute relative to cwd
if self.invocation_dir != self.rootdir:
fullpath = self.rootdir.join(nodeid)
nodeid = self.invocation_dir.bestrelpath(fullpath)
return nodeid
@classmethod
def fromdictargs(cls, option_dict, args):
@ -691,14 +697,9 @@ class Config(object):
def _initini(self, args):
parsed_args = self._parser.parse_known_args(args)
if parsed_args.inifilename:
iniconfig = py.iniconfig.IniConfig(parsed_args.inifilename)
if 'pytest' in iniconfig.sections:
self.inicfg = iniconfig['pytest']
else:
self.inicfg = {}
else:
self.inicfg = getcfg(args, ["pytest.ini", "tox.ini", "setup.cfg"])
r = determine_setup(parsed_args.inifilename, parsed_args.file_or_dir)
self.rootdir, self.inifile, self.inicfg = r
self.invocation_dir = py.path.local()
self._parser.addini('addopts', 'extra command line options', 'args')
self._parser.addini('minversion', 'minimally required pytest version')
@ -859,8 +860,58 @@ def getcfg(args, inibasenames):
if exists(p):
iniconfig = py.iniconfig.IniConfig(p)
if 'pytest' in iniconfig.sections:
return iniconfig['pytest']
return {}
return base, p, iniconfig['pytest']
elif inibasename == "pytest.ini":
# allowed to be empty
return base, p, {}
return None, None, None
def get_common_ancestor(args):
# args are what we get after early command line parsing (usually
# strings, but can be py.path.local objects as well)
common_ancestor = None
for arg in args:
if str(arg)[0] == "-":
continue
p = py.path.local(arg)
if common_ancestor is None:
common_ancestor = p
else:
if p.relto(common_ancestor) or p == common_ancestor:
continue
elif common_ancestor.relto(p):
common_ancestor = p
else:
shared = p.common(common_ancestor)
if shared is not None:
common_ancestor = shared
if common_ancestor is None:
common_ancestor = py.path.local()
elif not common_ancestor.isdir():
common_ancestor = common_ancestor.dirpath()
return common_ancestor
def determine_setup(inifile, args):
if inifile:
iniconfig = py.iniconfig.IniConfig(inifile)
try:
inicfg = iniconfig["pytest"]
except KeyError:
inicfg = None
rootdir = get_common_ancestor(args)
else:
ancestor = get_common_ancestor(args)
rootdir, inifile, inicfg = getcfg(
[ancestor], ["pytest.ini", "tox.ini", "setup.cfg"])
if rootdir is None:
for rootdir in ancestor.parts(reverse=True):
if rootdir.join("setup.py").exists():
break
else:
rootdir = ancestor
return rootdir, inifile, inicfg or {}
def setns(obj, dic):

View File

@ -457,9 +457,7 @@ class FSCollector(Collector):
self.fspath = fspath
def _makeid(self):
if self == self.session:
return "."
relpath = self.session.fspath.bestrelpath(self.fspath)
relpath = self.fspath.relto(self.config.rootdir)
if os.sep != "/":
relpath = relpath.replace(os.sep, "/")
return relpath
@ -510,7 +508,7 @@ class Session(FSCollector):
__module__ = 'builtins' # for py3
def __init__(self, config):
FSCollector.__init__(self, py.path.local(), parent=None,
FSCollector.__init__(self, config.rootdir, parent=None,
config=config, session=self)
self.config.pluginmanager.register(self, name="session", prepend=True)
self._testsfailed = 0
@ -520,6 +518,9 @@ class Session(FSCollector):
self.startdir = py.path.local()
self._fs2hookproxy = {}
def _makeid(self):
return ""
def pytest_collectstart(self):
if self.shouldstop:
raise self.Interrupted(self.shouldstop)
@ -663,7 +664,7 @@ class Session(FSCollector):
arg = self._tryconvertpyarg(arg)
parts = str(arg).split("::")
relpath = parts[0].replace("/", os.sep)
path = self.fspath.join(relpath, abs=True)
path = self.config.invocation_dir.join(relpath, abs=True)
if not path.check():
if self.config.option.pyargs:
msg = "file or package not found: "

View File

@ -306,9 +306,8 @@ class TmpTestdir:
session = Session(config)
assert '::' not in str(arg)
p = py.path.local(arg)
x = session.fspath.bestrelpath(p)
config.hook.pytest_sessionstart(session=session)
res = session.perform_collect([x], genitems=False)[0]
res = session.perform_collect([str(p)], genitems=False)[0]
config.hook.pytest_sessionfinish(session=session, exitstatus=EXIT_OK)
return res
@ -395,8 +394,7 @@ class TmpTestdir:
def parseconfigure(self, *args):
config = self.parseconfig(*args)
config.do_configure()
self.request.addfinalizer(lambda:
config.do_unconfigure())
self.request.addfinalizer(config.do_unconfigure)
return config
def getitem(self, source, funcname="test_func"):

View File

@ -1653,11 +1653,9 @@ class FixtureManager:
# what fixtures are visible for particular tests (as denoted
# by their test id)
if p.basename.startswith("conftest.py"):
nodeid = self.session.fspath.bestrelpath(p.dirpath())
nodeid = p.dirpath().relto(self.config.rootdir)
if p.sep != "/":
nodeid = nodeid.replace(p.sep, "/")
if nodeid == ".":
nodeid = ""
self.parsefactories(plugin, nodeid)
self._seenplugins.add(plugin)

View File

@ -218,14 +218,14 @@ def show_simple(terminalreporter, lines, stat, format):
failed = terminalreporter.stats.get(stat)
if failed:
for rep in failed:
pos = rep.nodeid
lines.append(format %(pos, ))
pos = terminalreporter.config.cwd_relative_nodeid(rep.nodeid)
lines.append(format %(pos,))
def show_xfailed(terminalreporter, lines):
xfailed = terminalreporter.stats.get("xfailed")
if xfailed:
for rep in xfailed:
pos = rep.nodeid
pos = terminalreporter.config.cwd_relative_nodeid(rep.nodeid)
reason = rep.wasxfail
lines.append("XFAIL %s" % (pos,))
if reason:
@ -235,7 +235,7 @@ def show_xpassed(terminalreporter, lines):
xpassed = terminalreporter.stats.get("xpassed")
if xpassed:
for rep in xpassed:
pos = rep.nodeid
pos = terminalreporter.config.cwd_relative_nodeid(rep.nodeid)
reason = rep.wasxfail
lines.append("XPASS %s %s" %(pos, reason))

View File

@ -95,7 +95,7 @@ class TerminalReporter:
self._numcollected = 0
self.stats = {}
self.startdir = self.curdir = py.path.local()
self.startdir = py.path.local()
if file is None:
file = sys.stdout
self._tw = self.writer = py.io.TerminalWriter(file)
@ -111,12 +111,12 @@ class TerminalReporter:
char = {'xfailed': 'x', 'skipped': 's'}.get(char, char)
return char in self.reportchars
def write_fspath_result(self, fspath, res):
def write_fspath_result(self, nodeid, res):
fspath = self.config.rootdir.join(nodeid.split("::")[0])
if fspath != self.currentfspath:
self.currentfspath = fspath
#fspath = self.startdir.bestrelpath(fspath)
fspath = self.startdir.bestrelpath(fspath)
self._tw.line()
#relpath = self.startdir.bestrelpath(fspath)
self._tw.write(fspath + " ")
self._tw.write(res)
@ -182,12 +182,12 @@ class TerminalReporter:
def pytest_runtest_logstart(self, nodeid, location):
# ensure that the path is printed before the
# 1st test of a module starts running
fspath = nodeid.split("::")[0]
if self.showlongtestinfo:
line = self._locationline(fspath, *location)
line = self._locationline(nodeid, *location)
self.write_ensure_prefix(line, "")
elif self.showfspath:
self.write_fspath_result(fspath, "")
fsid = nodeid.split("::")[0]
self.write_fspath_result(fsid, "")
def pytest_runtest_logreport(self, report):
rep = report
@ -200,7 +200,7 @@ class TerminalReporter:
return
if self.verbosity <= 0:
if not hasattr(rep, 'node') and self.showfspath:
self.write_fspath_result(rep.fspath, letter)
self.write_fspath_result(rep.nodeid, letter)
else:
self._tw.write(letter)
else:
@ -213,7 +213,7 @@ class TerminalReporter:
markup = {'red':True}
elif rep.skipped:
markup = {'yellow':True}
line = self._locationline(str(rep.fspath), *rep.location)
line = self._locationline(rep.nodeid, *rep.location)
if not hasattr(rep, 'node'):
self.write_ensure_prefix(line, word, **markup)
#self._tw.write(word, **markup)
@ -237,7 +237,7 @@ class TerminalReporter:
items = [x for x in report.result if isinstance(x, pytest.Item)]
self._numcollected += len(items)
if self.hasmarkup:
#self.write_fspath_result(report.fspath, 'E')
#self.write_fspath_result(report.nodeid, 'E')
self.report_collect()
def report_collect(self, final=False):
@ -288,6 +288,10 @@ class TerminalReporter:
self.write_line(line)
def pytest_report_header(self, config):
inifile = ""
if config.inifile:
inifile = config.rootdir.bestrelpath(config.inifile)
lines = ["rootdir: %s, inifile: %s" %(config.rootdir, inifile)]
plugininfo = config.pluginmanager._plugin_distinfo
if plugininfo:
l = []
@ -296,7 +300,8 @@ class TerminalReporter:
if name.startswith("pytest-"):
name = name[7:]
l.append(name)
return "plugins: %s" % ", ".join(l)
lines.append("plugins: %s" % ", ".join(l))
return lines
def pytest_collection_finish(self, session):
if self.config.option.collectonly:
@ -378,19 +383,24 @@ class TerminalReporter:
else:
excrepr.reprcrash.toterminal(self._tw)
def _locationline(self, collect_fspath, fspath, lineno, domain):
def _locationline(self, nodeid, fspath, lineno, domain):
def mkrel(nodeid):
line = self.config.cwd_relative_nodeid(nodeid)
if domain and line.endswith(domain):
line = line[:-len(domain)]
l = domain.split("[")
l[0] = l[0].replace('.', '::') # don't replace '.' in params
line += "[".join(l)
return line
# collect_fspath comes from testid which has a "/"-normalized path
if fspath and fspath.replace("\\", "/") != collect_fspath:
fspath = "%s <- %s" % (collect_fspath, fspath)
if fspath:
line = str(fspath)
if domain:
split = str(domain).split('[')
split[0] = split[0].replace('.', '::') # don't replace '.' in params
line += "::" + '['.join(split)
res = mkrel(nodeid).replace("::()", "") # parens-normalization
if nodeid.split("::")[0] != fspath.replace("\\", "/"):
res += " <- " + self.startdir.bestrelpath(fspath)
else:
line = "[location]"
return line + " "
res = "[location]"
return res + " "
def _getfailureheadline(self, rep):
if hasattr(rep, 'location'):

View File

@ -12,37 +12,73 @@ configurations files by using the general help option::
This will display command line and configuration file settings
which were registered by installed plugins.
.. _rootdir:
.. _inifiles:
How test configuration is read from configuration INI-files
-------------------------------------------------------------
initialization: determining rootdir and inifile
-----------------------------------------------
``pytest`` searches for the first matching ini-style configuration file
in the directories of command line argument and the directories above.
It looks for file basenames in this order::
.. versionadded:: 2.7
pytest determines a "rootdir" for each test run which depends on
the command line arguments (specified test files, paths) and on
the existence of inifiles. The determined rootdir and ini-file are
printed as part of the pytest header. The rootdir is used for constructing
"nodeids" during collection and may also be used by plugins to store
project/testrun-specific information.
Here is the algorithm which finds the rootdir from ``args``:
- determine the common ancestor directory for the specified ``args``.
- look for ``pytest.ini``, ``tox.ini`` and ``setup.cfg`` files in the
ancestor directory and upwards. If one is matched, it becomes the
ini-file and its directory becomes the rootdir. An existing
``pytest.ini`` file will always be considered a match whereas
``tox.ini`` and ``setup.cfg`` will only match if they contain
a ``[pytest]`` section.
- if no ini-file was found, look for ``setup.py`` upwards from
the common ancestor directory to determine the ``rootdir``.
- if no ini-file and no ``setup.py`` was found, use the already
determined common ancestor as root directory. This allows to
work with pytest in structures that are not part of a package
and don't have any particular ini-file configuration.
Note that options from multiple ini-files candidates are never merged,
the first one wins (``pytest.ini`` always wins even if it does not
contain a ``[pytest]`` section).
The ``config`` object will subsequently carry these attributes:
- ``config.rootdir``: the determined root directory, guaranteed to exist.
- ``config.inifile``: the determined ini-file, may be ``None``.
The rootdir is used a reference directory for constructing test
addresses ("nodeids") and can be used also by plugins for storing
per-testrun information.
Example::
py.test path/to/testdir path/other/
will determine the common ancestor as ``path`` and then
check for ini-files as follows::
# first look for pytest.ini files
path/pytest.ini
path/setup.cfg # must also contain [pytest] section to match
path/tox.ini # must also contain [pytest] section to match
pytest.ini
tox.ini
setup.cfg
... # all the way down to the root
Searching stops when the first ``[pytest]`` section is found in any of
these files. There is no merging of configuration values from multiple
files. Example::
# now look for setup.py
path/setup.py
setup.py
... # all the way down to the root
py.test path/to/testdir
will look in the following dirs for a config file::
path/to/testdir/pytest.ini
path/to/testdir/tox.ini
path/to/testdir/setup.cfg
path/to/pytest.ini
path/to/tox.ini
path/to/setup.cfg
... # up until root of filesystem
If argument is provided to a ``pytest`` run, the current working directory
is used to start the search.
.. _`how to change command line options defaults`:
.. _`adding default options`:
@ -67,6 +103,8 @@ line options while the environment is in use::
From now on, running ``pytest`` will add the specified options.
Builtin configuration file options
----------------------------------------------

View File

@ -343,7 +343,7 @@ class TestSession:
("pytest_make_collect_report", "collector.fspath == p"),
("pytest_pycollect_makeitem", "name == 'test_func'"),
("pytest_collectreport", "report.nodeid.startswith(p.basename)"),
("pytest_collectreport", "report.nodeid == '.'")
("pytest_collectreport", "report.nodeid == ''")
])
def test_collect_protocol_method(self, testdir):
@ -478,7 +478,7 @@ class Test_getinitialnodes:
config = testdir.parseconfigure(x)
col = testdir.getnode(config, x)
assert isinstance(col, pytest.Module)
assert col.name == 'subdir/x.py'
assert col.name == 'x.py'
assert col.parent.parent is None
for col in col.listchain():
assert col.config is config

View File

@ -1,6 +1,6 @@
import py, pytest
from _pytest.config import getcfg
from _pytest.config import getcfg, get_common_ancestor, determine_setup
class TestParseIni:
def test_getcfg_and_config(self, testdir, tmpdir):
@ -10,7 +10,7 @@ class TestParseIni:
[pytest]
name = value
"""))
cfg = getcfg([sub], ["setup.cfg"])
rootdir, inifile, cfg = getcfg([sub], ["setup.cfg"])
assert cfg['name'] == "value"
config = testdir.parseconfigure(sub)
assert config.inicfg['name'] == 'value'
@ -400,3 +400,55 @@ class TestWarning:
*WT1*test_warn_on_test_item*:5*hello*
*1 warning*
""")
class TestRootdir:
def test_simple_noini(self, tmpdir):
assert get_common_ancestor([tmpdir]) == tmpdir
assert get_common_ancestor([tmpdir.mkdir("a"), tmpdir]) == tmpdir
assert get_common_ancestor([tmpdir, tmpdir.join("a")]) == tmpdir
with tmpdir.as_cwd():
assert get_common_ancestor([]) == tmpdir
@pytest.mark.parametrize("name", "setup.cfg tox.ini pytest.ini".split())
def test_with_ini(self, tmpdir, name):
inifile = tmpdir.join(name)
inifile.write("[pytest]\n")
a = tmpdir.mkdir("a")
b = a.mkdir("b")
for args in ([tmpdir], [a], [b]):
rootdir, inifile, inicfg = determine_setup(None, args)
assert rootdir == tmpdir
assert inifile == inifile
rootdir, inifile, inicfg = determine_setup(None, [b,a])
assert rootdir == tmpdir
assert inifile == inifile
@pytest.mark.parametrize("name", "setup.cfg tox.ini".split())
def test_pytestini_overides_empty_other(self, tmpdir, name):
inifile = tmpdir.ensure("pytest.ini")
a = tmpdir.mkdir("a")
a.ensure(name)
rootdir, inifile, inicfg = determine_setup(None, [a])
assert rootdir == tmpdir
assert inifile == inifile
def test_setuppy_fallback(self, tmpdir):
a = tmpdir.mkdir("a")
a.ensure("setup.cfg")
tmpdir.ensure("setup.py")
rootdir, inifile, inicfg = determine_setup(None, [a])
assert rootdir == tmpdir
assert inifile is None
assert inicfg == {}
def test_nothing(self, tmpdir):
rootdir, inifile, inicfg = determine_setup(None, [tmpdir])
assert rootdir == tmpdir
assert inifile is None
assert inicfg == {}
def test_with_specific_inifile(self, tmpdir):
inifile = tmpdir.ensure("pytest.ini")
rootdir, inifile, inicfg = determine_setup(inifile, [tmpdir])
assert rootdir == tmpdir

View File

@ -1,7 +1,9 @@
from textwrap import dedent
import py, pytest
from _pytest.config import Conftest
@pytest.fixture(scope="module", params=["global", "inpackage"])
def basedir(request):
from _pytest.tmpdir import tmpdir
@ -255,3 +257,89 @@ def test_conftest_found_with_double_dash(testdir):
result.stdout.fnmatch_lines("""
*--hello-world*
""")
class TestConftestVisibility:
def _setup_tree(self, testdir): # for issue616
# example mostly taken from:
# https://mail.python.org/pipermail/pytest-dev/2014-September/002617.html
runner = testdir.mkdir("empty")
package = testdir.mkdir("package")
package.join("conftest.py").write(dedent("""\
import pytest
@pytest.fixture
def fxtr():
return "from-package"
"""))
package.join("test_pkgroot.py").write(dedent("""\
def test_pkgroot(fxtr):
assert fxtr == "from-package"
"""))
swc = package.mkdir("swc")
swc.join("__init__.py").ensure()
swc.join("conftest.py").write(dedent("""\
import pytest
@pytest.fixture
def fxtr():
return "from-swc"
"""))
swc.join("test_with_conftest.py").write(dedent("""\
def test_with_conftest(fxtr):
assert fxtr == "from-swc"
"""))
snc = package.mkdir("snc")
snc.join("__init__.py").ensure()
snc.join("test_no_conftest.py").write(dedent("""\
def test_no_conftest(fxtr):
assert fxtr == "from-package" # No local conftest.py, so should
# use value from parent dir's
"""))
print ("created directory structure:")
for x in testdir.tmpdir.visit():
print (" " + x.relto(testdir.tmpdir))
return {
"runner": runner,
"package": package,
"swc": swc,
"snc": snc}
# N.B.: "swc" stands for "subdir with conftest.py"
# "snc" stands for "subdir no [i.e. without] conftest.py"
@pytest.mark.parametrize("chdir,testarg,expect_ntests_passed", [
("runner", "..", 3),
("package", "..", 3),
("swc", "../..", 3),
("snc", "../..", 3),
("runner", "../package", 3),
("package", ".", 3),
("swc", "..", 3),
("snc", "..", 3),
("runner", "../package/swc", 1),
("package", "./swc", 1),
("swc", ".", 1),
("snc", "../swc", 1),
("runner", "../package/snc", 1),
("package", "./snc", 1),
("swc", "../snc", 1),
("snc", ".", 1),
])
@pytest.mark.issue616
def test_parsefactories_relative_node_ids(
self, testdir, chdir,testarg, expect_ntests_passed):
dirs = self._setup_tree(testdir)
print("pytest run in cwd: %s" %(
dirs[chdir].relto(testdir.tmpdir)))
print("pytestarg : %s" %(testarg))
print("expected pass : %s" %(expect_ntests_passed))
with dirs[chdir].as_cwd():
reprec = testdir.inline_run(testarg, "-q", "--traceconfig")
reprec.assertoutcome(passed=expect_ntests_passed)

View File

@ -1,9 +1,6 @@
from _pytest.doctest import DoctestItem, DoctestModule, DoctestTextfile
import py, pytest
import pdb
class TestDoctests:
def test_collect_testtextfile(self, testdir):

View File

@ -77,11 +77,11 @@ class TestTerminal:
def test_writeline(self, testdir, linecomp):
modcol = testdir.getmodulecol("def test_one(): pass")
rep = TerminalReporter(modcol.config, file=linecomp.stringio)
rep.write_fspath_result(py.path.local("xy.py"), '.')
rep.write_fspath_result(modcol.nodeid, ".")
rep.write_line("hello world")
lines = linecomp.stringio.getvalue().split('\n')
assert not lines[0]
assert lines[1].endswith("xy.py .")
assert lines[1].endswith(modcol.name + " .")
assert lines[2] == "hello world"
def test_show_runtest_logstart(self, testdir, linecomp):
@ -126,7 +126,7 @@ class TestTerminal:
])
result = testdir.runpytest("-v", p2)
result.stdout.fnmatch_lines([
"*test_p2.py <- *test_p1.py::TestMore::test_p1*",
"*test_p2.py::TestMore::test_p1* <- *test_p1.py*PASSED",
])
def test_itemreport_directclasses_not_shown_as_subclasses(self, testdir):