Merge branch 'master' into fix-report-outcome-for-xpass

This commit is contained in:
Raphael Pierzina 2016-08-17 22:15:29 +01:00
commit bb3d6d87b6
11 changed files with 147 additions and 48 deletions

View File

@ -24,6 +24,7 @@ Carl Friedrich Bolz
Charles Cloud Charles Cloud
Charnjit SiNGH (CCSJ) Charnjit SiNGH (CCSJ)
Chris Lamb Chris Lamb
Christian Boelsen
Christian Theunert Christian Theunert
Christian Tismer Christian Tismer
Christopher Gilling Christopher Gilling

View File

@ -3,9 +3,10 @@
**Bug Fixes** **Bug Fixes**
* Add an 'E' to the first line of error messages from FixtureLookupErrorRepr. * Improve error message with fixture lookup errors: add an 'E' to the first
Fixes `#717`_. Thanks `@blueyed`_ for reporting, `@eolo999`_ for the PR line and '>' to the rest. Fixes `#717`_. Thanks `@blueyed`_ for reporting and
and `@tomviner`_ for his guidance during EuroPython2016 sprint. a PR, `@eolo999`_ for the initial PR and `@tomviner`_ for his guidance during
EuroPython2016 sprint.
* Text documents without any doctests no longer appear as "skipped". * Text documents without any doctests no longer appear as "skipped".
Thanks `@graingert`_ for reporting and providing a full PR (`#1580`_). Thanks `@graingert`_ for reporting and providing a full PR (`#1580`_).
@ -35,6 +36,10 @@
deprecated but still present. Thanks to `@RedBeardCode`_ and `@tomviner`_ deprecated but still present. Thanks to `@RedBeardCode`_ and `@tomviner`_
for PR (`#1626`_). for PR (`#1626`_).
* Refined logic for determining the ``rootdir``, considering only valid
paths which fixes a number of issues: `#1594`_, `#1435`_ and `#1471`_.
Thanks to `@blueyed`_ and `@davehunt`_ for the PR.
* Always include full assertion explanation. The previous behaviour was hiding * Always include full assertion explanation. The previous behaviour was hiding
sub-expressions that happened to be False, assuming this was redundant information. sub-expressions that happened to be False, assuming this was redundant information.
Thanks `@bagerard`_ for reporting (`#1503`_). Thanks to `@davehunt`_ and Thanks `@bagerard`_ for reporting (`#1503`_). Thanks to `@davehunt`_ and
@ -61,23 +66,33 @@
* Fixed scope overriding inside metafunc.parametrize (`#634`_). * Fixed scope overriding inside metafunc.parametrize (`#634`_).
Thanks to `@Stranger6667`_ for the PR. Thanks to `@Stranger6667`_ for the PR.
* * Fixed the total tests tally in junit xml output (`#1798`_).
Thanks to `@cryporchild`_ for the PR.
* ``pytest_terminal_summary`` hook now receives the ``exitstatus``
of the test session as argument. Thanks `@blueyed`_ for the PR (`#1809`_).
* *
* * Fixed off-by-one error with lines from ``request.node.warn``.
Thanks to `@blueyed`_ for the PR.
* *
.. _#1210: https://github.com/pytest-dev/pytest/issues/1210 .. _#1210: https://github.com/pytest-dev/pytest/issues/1210
.. _#1435: https://github.com/pytest-dev/pytest/issues/1435
.. _#1471: https://github.com/pytest-dev/pytest/issues/1471
.. _#1479: https://github.com/pytest-dev/pytest/issues/1479 .. _#1479: https://github.com/pytest-dev/pytest/issues/1479
.. _#1503: https://github.com/pytest-dev/pytest/issues/1503 .. _#1503: https://github.com/pytest-dev/pytest/issues/1503
.. _#1553: https://github.com/pytest-dev/pytest/issues/1553 .. _#1553: https://github.com/pytest-dev/pytest/issues/1553
.. _#1579: https://github.com/pytest-dev/pytest/issues/1579 .. _#1579: https://github.com/pytest-dev/pytest/issues/1579
.. _#1580: https://github.com/pytest-dev/pytest/pull/1580 .. _#1580: https://github.com/pytest-dev/pytest/pull/1580
.. _#1594: https://github.com/pytest-dev/pytest/issues/1594
.. _#1597: https://github.com/pytest-dev/pytest/pull/1597 .. _#1597: https://github.com/pytest-dev/pytest/pull/1597
.. _#1605: https://github.com/pytest-dev/pytest/issues/1605 .. _#1605: https://github.com/pytest-dev/pytest/issues/1605
.. _#1626: https://github.com/pytest-dev/pytest/pull/1626 .. _#1626: https://github.com/pytest-dev/pytest/pull/1626
.. _#1798: https://github.com/pytest-dev/pytest/pull/1798
.. _#1809: https://github.com/pytest-dev/pytest/pull/1809
.. _#460: https://github.com/pytest-dev/pytest/pull/460 .. _#460: https://github.com/pytest-dev/pytest/pull/460
.. _#634: https://github.com/pytest-dev/pytest/issues/634 .. _#634: https://github.com/pytest-dev/pytest/issues/634
.. _#717: https://github.com/pytest-dev/pytest/issues/717 .. _#717: https://github.com/pytest-dev/pytest/issues/717
@ -86,6 +101,7 @@
.. _@bagerard: https://github.com/bagerard .. _@bagerard: https://github.com/bagerard
.. _@BeyondEvil: https://github.com/BeyondEvil .. _@BeyondEvil: https://github.com/BeyondEvil
.. _@blueyed: https://github.com/blueyed .. _@blueyed: https://github.com/blueyed
.. _@cryporchild: https://github.com/cryporchild
.. _@davehunt: https://github.com/davehunt .. _@davehunt: https://github.com/davehunt
.. _@DRMacIver: https://github.com/DRMacIver .. _@DRMacIver: https://github.com/DRMacIver
.. _@eolo999: https://github.com/eolo999 .. _@eolo999: https://github.com/eolo999

View File

@ -1095,6 +1095,8 @@ def get_common_ancestor(args):
if str(arg)[0] == "-": if str(arg)[0] == "-":
continue continue
p = py.path.local(arg) p = py.path.local(arg)
if not p.exists():
continue
if common_ancestor is None: if common_ancestor is None:
common_ancestor = p common_ancestor = p
else: else:
@ -1108,21 +1110,28 @@ def get_common_ancestor(args):
common_ancestor = shared common_ancestor = shared
if common_ancestor is None: if common_ancestor is None:
common_ancestor = py.path.local() common_ancestor = py.path.local()
elif not common_ancestor.isdir(): elif common_ancestor.isfile():
common_ancestor = common_ancestor.dirpath() common_ancestor = common_ancestor.dirpath()
return common_ancestor return common_ancestor
def get_dirs_from_args(args):
return [d for d in (py.path.local(x) for x in args
if not str(x).startswith("-"))
if d.exists()]
def determine_setup(inifile, args): def determine_setup(inifile, args):
dirs = get_dirs_from_args(args)
if inifile: if inifile:
iniconfig = py.iniconfig.IniConfig(inifile) iniconfig = py.iniconfig.IniConfig(inifile)
try: try:
inicfg = iniconfig["pytest"] inicfg = iniconfig["pytest"]
except KeyError: except KeyError:
inicfg = None inicfg = None
rootdir = get_common_ancestor(args) rootdir = get_common_ancestor(dirs)
else: else:
ancestor = get_common_ancestor(args) ancestor = get_common_ancestor(dirs)
rootdir, inifile, inicfg = getcfg( rootdir, inifile, inicfg = getcfg(
[ancestor], ["pytest.ini", "tox.ini", "setup.cfg"]) [ancestor], ["pytest.ini", "tox.ini", "setup.cfg"])
if rootdir is None: if rootdir is None:
@ -1130,7 +1139,13 @@ def determine_setup(inifile, args):
if rootdir.join("setup.py").exists(): if rootdir.join("setup.py").exists():
break break
else: else:
rootdir = ancestor rootdir, inifile, inicfg = getcfg(
dirs, ["pytest.ini", "tox.ini", "setup.cfg"])
if rootdir is None:
rootdir = get_common_ancestor([py.path.local(), ancestor])
is_fs_root = os.path.splitdrive(str(rootdir))[1] == os.sep
if is_fs_root:
rootdir = ancestor
return rootdir, inifile, inicfg or {} return rootdir, inifile, inicfg or {}

View File

@ -369,7 +369,7 @@ class LogXML(object):
suite_stop_time = time.time() suite_stop_time = time.time()
suite_time_delta = suite_stop_time - self.suite_start_time suite_time_delta = suite_stop_time - self.suite_start_time
numtests = self.stats['passed'] + self.stats['failure'] + self.stats['skipped'] numtests = self.stats['passed'] + self.stats['failure'] + self.stats['skipped'] + self.stats['error']
logfile.write('<?xml version="1.0" encoding="utf-8"?>') logfile.write('<?xml version="1.0" encoding="utf-8"?>')
logfile.write(Junit.testsuite( logfile.write(Junit.testsuite(

View File

@ -267,7 +267,7 @@ class Node(object):
if fslocation is None: if fslocation is None:
fslocation = getattr(self, "fspath", None) fslocation = getattr(self, "fspath", None)
else: else:
fslocation = "%s:%s" % fslocation[:2] fslocation = "%s:%s" % (fslocation[0], fslocation[1] + 1)
self.ihook.pytest_logwarning.call_historic(kwargs=dict( self.ihook.pytest_logwarning.call_historic(kwargs=dict(
code=code, message=message, code=code, message=message,

View File

@ -9,7 +9,7 @@ import warnings
import py import py
import pytest import pytest
from _pytest._code.code import TerminalRepr from _pytest._code.code import FormattedExcinfo, TerminalRepr
from _pytest.mark import MarkDecorator, MarkerError from _pytest.mark import MarkDecorator, MarkerError
try: try:
@ -1836,6 +1836,7 @@ class FixtureLookupError(LookupError):
return FixtureLookupErrorRepr(fspath, lineno, tblines, msg, self.argname) return FixtureLookupErrorRepr(fspath, lineno, tblines, msg, self.argname)
class FixtureLookupErrorRepr(TerminalRepr): class FixtureLookupErrorRepr(TerminalRepr):
def __init__(self, filename, firstlineno, tblines, errorstring, argname): def __init__(self, filename, firstlineno, tblines, errorstring, argname):
self.tblines = tblines self.tblines = tblines
@ -1845,19 +1846,20 @@ class FixtureLookupErrorRepr(TerminalRepr):
self.argname = argname self.argname = argname
def toterminal(self, tw): def toterminal(self, tw):
#tw.line("FixtureLookupError: %s" %(self.argname), red=True) # tw.line("FixtureLookupError: %s" %(self.argname), red=True)
for tbline in self.tblines: for tbline in self.tblines:
tw.line(tbline.rstrip()) tw.line(tbline.rstrip())
lines = self.errorstring.split("\n") lines = self.errorstring.split("\n")
for line in lines: if lines:
if line == lines[0]: tw.line('{0} {1}'.format(FormattedExcinfo.fail_marker,
prefix = 'E ' lines[0].strip()), red=True)
else: for line in lines[1:]:
prefix = ' ' tw.line('{0} {1}'.format(FormattedExcinfo.flow_marker,
tw.line(prefix + line.strip(), red=True) line.strip()), red=True)
tw.line() tw.line()
tw.line("%s:%d" % (self.filename, self.firstlineno+1)) tw.line("%s:%d" % (self.filename, self.firstlineno+1))
class FixtureManager: class FixtureManager:
""" """
pytest fixtures definitions and information is stored and managed pytest fixtures definitions and information is stored and managed

View File

@ -29,25 +29,29 @@ project/testrun-specific information.
Here is the algorithm which finds the rootdir from ``args``: Here is the algorithm which finds the rootdir from ``args``:
- determine the common ancestor directory for the specified ``args``. - determine the common ancestor directory for the specified ``args`` that are
recognised as paths that exist in the file system. If no such paths are
found, the common ancestor directory is set to the current working directory.
- look for ``pytest.ini``, ``tox.ini`` and ``setup.cfg`` files in the - look for ``pytest.ini``, ``tox.ini`` and ``setup.cfg`` files in the ancestor
ancestor directory and upwards. If one is matched, it becomes the directory and upwards. If one is matched, it becomes the ini-file and its
ini-file and its directory becomes the rootdir. An existing directory becomes the rootdir.
``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 - if no ini-file was found, look for ``setup.py`` upwards from the common
the common ancestor directory to determine the ``rootdir``. ancestor directory to determine the ``rootdir``.
- if no ini-file and no ``setup.py`` was found, use the already - if no ``setup.py`` was found, look for ``pytest.ini``, ``tox.ini`` and
determined common ancestor as root directory. This allows to ``setup.cfg`` in each of the specified ``args`` and upwards. If one is
work with pytest in structures that are not part of a package matched, it becomes the ini-file and its directory becomes the rootdir.
and don't have any particular ini-file configuration.
Note that options from multiple ini-files candidates are never merged, - if no ini-file was found, use the already determined common ancestor as root
the first one wins (``pytest.ini`` always wins even if it does not 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 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. 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). contain a ``[pytest]`` section).
The ``config`` object will subsequently carry these attributes: The ``config`` object will subsequently carry these attributes:

View File

@ -395,10 +395,11 @@ class TestFillFixtures:
""") """)
result = testdir.runpytest() result = testdir.runpytest()
result.stdout.fnmatch_lines([ result.stdout.fnmatch_lines([
"*ERROR*test_lookup_error*", "*ERROR at setup of test_lookup_error*",
"*def test_lookup_error(unknown):*", " def test_lookup_error(unknown):*",
"*fixture*unknown*not found*", "E fixture 'unknown' not found",
"*available fixtures*", "> available fixtures:*",
"> use 'py*test --fixtures *' for help on them.",
"*1 error*", "*1 error*",
]) ])
assert "INTERNAL" not in result.stdout.str() assert "INTERNAL" not in result.stdout.str()

View File

@ -490,7 +490,8 @@ class TestSession:
class Test_getinitialnodes: class Test_getinitialnodes:
def test_global_file(self, testdir, tmpdir): def test_global_file(self, testdir, tmpdir):
x = tmpdir.ensure("x.py") x = tmpdir.ensure("x.py")
config = testdir.parseconfigure(x) with tmpdir.as_cwd():
config = testdir.parseconfigure(x)
col = testdir.getnode(config, x) col = testdir.getnode(config, x)
assert isinstance(col, pytest.Module) assert isinstance(col, pytest.Module)
assert col.name == 'x.py' assert col.name == 'x.py'
@ -504,7 +505,8 @@ class Test_getinitialnodes:
subdir = tmpdir.join("subdir") subdir = tmpdir.join("subdir")
x = subdir.ensure("x.py") x = subdir.ensure("x.py")
subdir.ensure("__init__.py") subdir.ensure("__init__.py")
config = testdir.parseconfigure(x) with subdir.as_cwd():
config = testdir.parseconfigure(x)
col = testdir.getnode(config, x) col = testdir.getnode(config, x)
assert isinstance(col, pytest.Module) assert isinstance(col, pytest.Module)
assert col.name == 'x.py' assert col.name == 'x.py'

View File

@ -468,7 +468,8 @@ def test_consider_args_after_options_for_rootdir_and_inifile(testdir, args):
args[i] = d1 args[i] = d1
elif arg == 'dir2': elif arg == 'dir2':
args[i] = d2 args[i] = d2
result = testdir.runpytest(*args) with root.as_cwd():
result = testdir.runpytest(*args)
result.stdout.fnmatch_lines(['*rootdir: *myroot, inifile: ']) result.stdout.fnmatch_lines(['*rootdir: *myroot, inifile: '])
@ -524,13 +525,14 @@ class TestWarning:
reprec = testdir.inline_run() reprec = testdir.inline_run()
reprec.assertoutcome(passed=1) reprec.assertoutcome(passed=1)
def test_warn_on_test_item_from_request(self, testdir): def test_warn_on_test_item_from_request(self, testdir, request):
testdir.makepyfile(""" testdir.makepyfile("""
import pytest import pytest
@pytest.fixture @pytest.fixture
def fix(request): def fix(request):
request.node.warn("T1", "hello") request.node.warn("T1", "hello")
def test_hello(fix): def test_hello(fix):
pass pass
""") """)
@ -541,16 +543,20 @@ class TestWarning:
result = testdir.runpytest("-rw") result = testdir.runpytest("-rw")
result.stdout.fnmatch_lines(""" result.stdout.fnmatch_lines("""
===*pytest-warning summary*=== ===*pytest-warning summary*===
*WT1*test_warn_on_test_item*:5*hello* *WT1*test_warn_on_test_item*:7 hello*
""") """)
class TestRootdir: class TestRootdir:
def test_simple_noini(self, tmpdir): def test_simple_noini(self, tmpdir):
assert get_common_ancestor([tmpdir]) == tmpdir assert get_common_ancestor([tmpdir]) == tmpdir
assert get_common_ancestor([tmpdir.mkdir("a"), tmpdir]) == tmpdir a = tmpdir.mkdir("a")
assert get_common_ancestor([tmpdir, tmpdir.join("a")]) == tmpdir assert get_common_ancestor([a, tmpdir]) == tmpdir
assert get_common_ancestor([tmpdir, a]) == tmpdir
with tmpdir.as_cwd(): with tmpdir.as_cwd():
assert get_common_ancestor([]) == tmpdir assert get_common_ancestor([]) == tmpdir
no_path = tmpdir.join('does-not-exist')
assert get_common_ancestor([no_path]) == tmpdir
assert get_common_ancestor([no_path.join('a')]) == tmpdir
@pytest.mark.parametrize("name", "setup.cfg tox.ini pytest.ini".split()) @pytest.mark.parametrize("name", "setup.cfg tox.ini pytest.ini".split())
def test_with_ini(self, tmpdir, name): def test_with_ini(self, tmpdir, name):
@ -595,3 +601,34 @@ class TestRootdir:
inifile = tmpdir.ensure("pytest.ini") inifile = tmpdir.ensure("pytest.ini")
rootdir, inifile, inicfg = determine_setup(inifile, [tmpdir]) rootdir, inifile, inicfg = determine_setup(inifile, [tmpdir])
assert rootdir == tmpdir assert rootdir == tmpdir
def test_with_arg_outside_cwd_without_inifile(self, tmpdir):
a = tmpdir.mkdir("a")
b = tmpdir.mkdir("b")
rootdir, inifile, inicfg = determine_setup(None, [a, b])
assert rootdir == tmpdir
assert inifile is None
def test_with_arg_outside_cwd_with_inifile(self, tmpdir):
a = tmpdir.mkdir("a")
b = tmpdir.mkdir("b")
inifile = a.ensure("pytest.ini")
rootdir, parsed_inifile, inicfg = determine_setup(None, [a, b])
assert rootdir == a
assert inifile == parsed_inifile
@pytest.mark.parametrize('dirs', ([], ['does-not-exist'],
['a/does-not-exist']))
def test_with_non_dir_arg(self, dirs, tmpdir):
with tmpdir.ensure(dir=True).as_cwd():
rootdir, inifile, inicfg = determine_setup(None, dirs)
assert rootdir == tmpdir
assert inifile is None
def test_with_existing_file_in_subdir(self, tmpdir):
a = tmpdir.mkdir("a")
a.ensure("exist")
with tmpdir.as_cwd():
rootdir, inifile, inicfg = determine_setup(None, ['a/exist'])
assert rootdir == tmpdir
assert inifile is None

View File

@ -102,6 +102,27 @@ class TestPython:
node = dom.find_first_by_tag("testsuite") node = dom.find_first_by_tag("testsuite")
node.assert_attr(name="pytest", errors=0, failures=1, skips=2, tests=5) node.assert_attr(name="pytest", errors=0, failures=1, skips=2, tests=5)
def test_summing_simple_with_errors(self, testdir):
testdir.makepyfile("""
import pytest
@pytest.fixture
def fixture():
raise Exception()
def test_pass():
pass
def test_fail():
assert 0
def test_error(fixture):
pass
@pytest.mark.xfail
def test_xpass():
assert 1
""")
result, dom = runandparse(testdir)
assert result.ret
node = dom.find_first_by_tag("testsuite")
node.assert_attr(name="pytest", errors=1, failures=1, skips=1, tests=4)
def test_timing_function(self, testdir): def test_timing_function(self, testdir):
testdir.makepyfile(""" testdir.makepyfile("""
import time, pytest import time, pytest
@ -128,7 +149,7 @@ class TestPython:
result, dom = runandparse(testdir) result, dom = runandparse(testdir)
assert result.ret assert result.ret
node = dom.find_first_by_tag("testsuite") node = dom.find_first_by_tag("testsuite")
node.assert_attr(errors=1, tests=0) node.assert_attr(errors=1, tests=1)
tnode = node.find_first_by_tag("testcase") tnode = node.find_first_by_tag("testcase")
tnode.assert_attr( tnode.assert_attr(
file="test_setup_error.py", file="test_setup_error.py",
@ -195,7 +216,7 @@ class TestPython:
result, dom = runandparse(testdir) result, dom = runandparse(testdir)
assert result.ret assert result.ret
node = dom.find_first_by_tag("testsuite") node = dom.find_first_by_tag("testsuite")
node.assert_attr(errors=1, tests=0) node.assert_attr(errors=1, tests=1)
tnode = node.find_first_by_tag("testcase") tnode = node.find_first_by_tag("testcase")
tnode.assert_attr(classname="pytest", name="internal") tnode.assert_attr(classname="pytest", name="internal")
fnode = tnode.find_first_by_tag("error") fnode = tnode.find_first_by_tag("error")
@ -358,7 +379,7 @@ class TestPython:
result, dom = runandparse(testdir) result, dom = runandparse(testdir)
assert result.ret assert result.ret
node = dom.find_first_by_tag("testsuite") node = dom.find_first_by_tag("testsuite")
node.assert_attr(errors=1, tests=0) node.assert_attr(errors=1, tests=1)
tnode = node.find_first_by_tag("testcase") tnode = node.find_first_by_tag("testcase")
tnode.assert_attr( tnode.assert_attr(
file="test_collect_error.py", file="test_collect_error.py",