Exit pytest on collection error (without executing tests)
Add --continue-on-collection-errors option to restore the previous behaviour: Execute tests (that were successfully collected) even when collection errors happen. Some tests had to be modified e.g. because the return code changed to 2 (EXIT_INTERRUPTED) instead of 1 (EXIT_TESTSFAILED) because an Interrupted exception is raised on collection error. Implemented via pair programming with: Oleg Pidsadnyi <oleg.pidsadnyi@gmail.com> closes #1421
This commit is contained in:
parent
54872e94b4
commit
ede7478dcc
1
AUTHORS
1
AUTHORS
|
@ -72,6 +72,7 @@ Michael Birtwell
|
||||||
Michael Droettboom
|
Michael Droettboom
|
||||||
Mike Lundy
|
Mike Lundy
|
||||||
Nicolas Delaby
|
Nicolas Delaby
|
||||||
|
Oleg Pidsadnyi
|
||||||
Omar Kohl
|
Omar Kohl
|
||||||
Pieter Mulder
|
Pieter Mulder
|
||||||
Piotr Banaszkiewicz
|
Piotr Banaszkiewicz
|
||||||
|
|
|
@ -74,6 +74,10 @@
|
||||||
message to raise when no exception occurred.
|
message to raise when no exception occurred.
|
||||||
Thanks `@palaviv`_ for the complete PR (`#1616`_).
|
Thanks `@palaviv`_ for the complete PR (`#1616`_).
|
||||||
|
|
||||||
|
* Fix `#1421`_: Exit tests if a collection error occurs and add
|
||||||
|
``--continue-on-collection-errors`` option to restore previous behaviour.
|
||||||
|
Thanks `@olegpidsadnyi`_ and `@omarkohl`_ for the complete PR (`#1628`_).
|
||||||
|
|
||||||
.. _@milliams: https://github.com/milliams
|
.. _@milliams: https://github.com/milliams
|
||||||
.. _@csaftoiu: https://github.com/csaftoiu
|
.. _@csaftoiu: https://github.com/csaftoiu
|
||||||
.. _@novas0x2a: https://github.com/novas0x2a
|
.. _@novas0x2a: https://github.com/novas0x2a
|
||||||
|
@ -83,7 +87,9 @@
|
||||||
.. _@palaviv: https://github.com/palaviv
|
.. _@palaviv: https://github.com/palaviv
|
||||||
.. _@omarkohl: https://github.com/omarkohl
|
.. _@omarkohl: https://github.com/omarkohl
|
||||||
.. _@mikofski: https://github.com/mikofski
|
.. _@mikofski: https://github.com/mikofski
|
||||||
|
.. _@olegpidsadnyi: https://github.com/olegpidsadnyi
|
||||||
|
|
||||||
|
.. _#1421: https://github.com/pytest-dev/pytest/issues/1421
|
||||||
.. _#1426: https://github.com/pytest-dev/pytest/issues/1426
|
.. _#1426: https://github.com/pytest-dev/pytest/issues/1426
|
||||||
.. _#1428: https://github.com/pytest-dev/pytest/pull/1428
|
.. _#1428: https://github.com/pytest-dev/pytest/pull/1428
|
||||||
.. _#1444: https://github.com/pytest-dev/pytest/pull/1444
|
.. _#1444: https://github.com/pytest-dev/pytest/pull/1444
|
||||||
|
@ -98,6 +104,7 @@
|
||||||
.. _#372: https://github.com/pytest-dev/pytest/issues/372
|
.. _#372: https://github.com/pytest-dev/pytest/issues/372
|
||||||
.. _#1544: https://github.com/pytest-dev/pytest/issues/1544
|
.. _#1544: https://github.com/pytest-dev/pytest/issues/1544
|
||||||
.. _#1616: https://github.com/pytest-dev/pytest/pull/1616
|
.. _#1616: https://github.com/pytest-dev/pytest/pull/1616
|
||||||
|
.. _#1628: https://github.com/pytest-dev/pytest/pull/1628
|
||||||
|
|
||||||
|
|
||||||
**Bug Fixes**
|
**Bug Fixes**
|
||||||
|
|
|
@ -48,6 +48,9 @@ def pytest_addoption(parser):
|
||||||
help="run pytest in strict mode, warnings become errors.")
|
help="run pytest in strict mode, warnings become errors.")
|
||||||
group._addoption("-c", metavar="file", type=str, dest="inifilename",
|
group._addoption("-c", metavar="file", type=str, dest="inifilename",
|
||||||
help="load configuration from `file` instead of trying to locate one of the implicit configuration files.")
|
help="load configuration from `file` instead of trying to locate one of the implicit configuration files.")
|
||||||
|
group._addoption("--continue-on-collection-errors", action="store_true",
|
||||||
|
default=False, dest="continue_on_collection_errors",
|
||||||
|
help="Force test execution even if collection errors occur.")
|
||||||
|
|
||||||
group = parser.getgroup("collect", "collection")
|
group = parser.getgroup("collect", "collection")
|
||||||
group.addoption('--collectonly', '--collect-only', action="store_true",
|
group.addoption('--collectonly', '--collect-only', action="store_true",
|
||||||
|
@ -133,6 +136,11 @@ def pytest_collection(session):
|
||||||
return session.perform_collect()
|
return session.perform_collect()
|
||||||
|
|
||||||
def pytest_runtestloop(session):
|
def pytest_runtestloop(session):
|
||||||
|
if (session.testsfailed and
|
||||||
|
not session.config.option.continue_on_collection_errors):
|
||||||
|
raise session.Interrupted(
|
||||||
|
"%d errors during collection" % session.testsfailed)
|
||||||
|
|
||||||
if session.config.option.collectonly:
|
if session.config.option.collectonly:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
|
@ -120,7 +120,7 @@ class TestGeneralUsage:
|
||||||
"ImportError while importing test module*",
|
"ImportError while importing test module*",
|
||||||
"'No module named *does_not_work*",
|
"'No module named *does_not_work*",
|
||||||
])
|
])
|
||||||
assert result.ret == 1
|
assert result.ret == 2
|
||||||
|
|
||||||
def test_not_collectable_arguments(self, testdir):
|
def test_not_collectable_arguments(self, testdir):
|
||||||
p1 = testdir.makepyfile("")
|
p1 = testdir.makepyfile("")
|
||||||
|
@ -665,11 +665,13 @@ class TestDurations:
|
||||||
testdir.makepyfile(self.source)
|
testdir.makepyfile(self.source)
|
||||||
testdir.makepyfile(test_collecterror="""xyz""")
|
testdir.makepyfile(test_collecterror="""xyz""")
|
||||||
result = testdir.runpytest("--durations=2", "-k test_1")
|
result = testdir.runpytest("--durations=2", "-k test_1")
|
||||||
assert result.ret != 0
|
assert result.ret == 2
|
||||||
result.stdout.fnmatch_lines([
|
result.stdout.fnmatch_lines([
|
||||||
"*durations*",
|
"*Interrupted: 1 errors during collection*",
|
||||||
"*call*test_1*",
|
|
||||||
])
|
])
|
||||||
|
# Collection errors abort test execution, therefore no duration is
|
||||||
|
# output
|
||||||
|
assert "duration" not in result.stdout.str()
|
||||||
|
|
||||||
def test_with_not(self, testdir):
|
def test_with_not(self, testdir):
|
||||||
testdir.makepyfile(self.source)
|
testdir.makepyfile(self.source)
|
||||||
|
|
|
@ -642,3 +642,114 @@ class TestNodekeywords:
|
||||||
""")
|
""")
|
||||||
reprec = testdir.inline_run("-k repr")
|
reprec = testdir.inline_run("-k repr")
|
||||||
reprec.assertoutcome(passed=1, failed=0)
|
reprec.assertoutcome(passed=1, failed=0)
|
||||||
|
|
||||||
|
|
||||||
|
COLLECTION_ERROR_PY_FILES = dict(
|
||||||
|
test_01_failure="""
|
||||||
|
def test_1():
|
||||||
|
assert False
|
||||||
|
""",
|
||||||
|
test_02_import_error="""
|
||||||
|
import asdfasdfasdf
|
||||||
|
def test_2():
|
||||||
|
assert True
|
||||||
|
""",
|
||||||
|
test_03_import_error="""
|
||||||
|
import asdfasdfasdf
|
||||||
|
def test_3():
|
||||||
|
assert True
|
||||||
|
""",
|
||||||
|
test_04_success="""
|
||||||
|
def test_4():
|
||||||
|
assert True
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_exit_on_collection_error(testdir):
|
||||||
|
"""Verify that all collection errors are collected and no tests executed"""
|
||||||
|
testdir.makepyfile(**COLLECTION_ERROR_PY_FILES)
|
||||||
|
|
||||||
|
res = testdir.runpytest()
|
||||||
|
assert res.ret == 2
|
||||||
|
|
||||||
|
res.stdout.fnmatch_lines([
|
||||||
|
"collected 2 items / 2 errors",
|
||||||
|
"*ERROR collecting test_02_import_error.py*",
|
||||||
|
"*No module named *asdfa*",
|
||||||
|
"*ERROR collecting test_03_import_error.py*",
|
||||||
|
"*No module named *asdfa*",
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
def test_exit_on_collection_with_maxfail_smaller_than_n_errors(testdir):
|
||||||
|
"""
|
||||||
|
Verify collection is aborted once maxfail errors are encountered ignoring
|
||||||
|
further modules which would cause more collection errors.
|
||||||
|
"""
|
||||||
|
testdir.makepyfile(**COLLECTION_ERROR_PY_FILES)
|
||||||
|
|
||||||
|
res = testdir.runpytest("--maxfail=1")
|
||||||
|
assert res.ret == 2
|
||||||
|
|
||||||
|
res.stdout.fnmatch_lines([
|
||||||
|
"*ERROR collecting test_02_import_error.py*",
|
||||||
|
"*No module named *asdfa*",
|
||||||
|
"*Interrupted: stopping after 1 failures*",
|
||||||
|
])
|
||||||
|
|
||||||
|
assert 'test_03' not in res.stdout.str()
|
||||||
|
|
||||||
|
|
||||||
|
def test_exit_on_collection_with_maxfail_bigger_than_n_errors(testdir):
|
||||||
|
"""
|
||||||
|
Verify the test run aborts due to collection errors even if maxfail count of
|
||||||
|
errors was not reached.
|
||||||
|
"""
|
||||||
|
testdir.makepyfile(**COLLECTION_ERROR_PY_FILES)
|
||||||
|
|
||||||
|
res = testdir.runpytest("--maxfail=4")
|
||||||
|
assert res.ret == 2
|
||||||
|
|
||||||
|
res.stdout.fnmatch_lines([
|
||||||
|
"collected 2 items / 2 errors",
|
||||||
|
"*ERROR collecting test_02_import_error.py*",
|
||||||
|
"*No module named *asdfa*",
|
||||||
|
"*ERROR collecting test_03_import_error.py*",
|
||||||
|
"*No module named *asdfa*",
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
def test_continue_on_collection_errors(testdir):
|
||||||
|
"""
|
||||||
|
Verify tests are executed even when collection errors occur when the
|
||||||
|
--continue-on-collection-errors flag is set
|
||||||
|
"""
|
||||||
|
testdir.makepyfile(**COLLECTION_ERROR_PY_FILES)
|
||||||
|
|
||||||
|
res = testdir.runpytest("--continue-on-collection-errors")
|
||||||
|
assert res.ret == 1
|
||||||
|
|
||||||
|
res.stdout.fnmatch_lines([
|
||||||
|
"collected 2 items / 2 errors",
|
||||||
|
"*1 failed, 1 passed, 2 error*",
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
def test_continue_on_collection_errors_maxfail(testdir):
|
||||||
|
"""
|
||||||
|
Verify tests are executed even when collection errors occur and that maxfail
|
||||||
|
is honoured (including the collection error count).
|
||||||
|
4 tests: 2 collection errors + 1 failure + 1 success
|
||||||
|
test_4 is never executed because the test run is with --maxfail=3 which
|
||||||
|
means it is interrupted after the 2 collection errors + 1 failure.
|
||||||
|
"""
|
||||||
|
testdir.makepyfile(**COLLECTION_ERROR_PY_FILES)
|
||||||
|
|
||||||
|
res = testdir.runpytest("--continue-on-collection-errors", "--maxfail=3")
|
||||||
|
assert res.ret == 2
|
||||||
|
|
||||||
|
res.stdout.fnmatch_lines([
|
||||||
|
"collected 2 items / 2 errors",
|
||||||
|
"*Interrupted: stopping after 3 failures*",
|
||||||
|
"*1 failed, 2 error*",
|
||||||
|
])
|
||||||
|
|
|
@ -199,8 +199,20 @@ class TestDoctests:
|
||||||
"*1 failed*",
|
"*1 failed*",
|
||||||
])
|
])
|
||||||
|
|
||||||
|
def test_doctest_unex_importerror_only_txt(self, testdir):
|
||||||
|
testdir.maketxtfile("""
|
||||||
|
>>> import asdalsdkjaslkdjasd
|
||||||
|
>>>
|
||||||
|
""")
|
||||||
|
result = testdir.runpytest()
|
||||||
|
# doctest is never executed because of error during hello.py collection
|
||||||
|
result.stdout.fnmatch_lines([
|
||||||
|
"*>>> import asdals*",
|
||||||
|
"*UNEXPECTED*ImportError*",
|
||||||
|
"ImportError: No module named *asdal*",
|
||||||
|
])
|
||||||
|
|
||||||
def test_doctest_unex_importerror(self, testdir):
|
def test_doctest_unex_importerror_with_module(self, testdir):
|
||||||
testdir.tmpdir.join("hello.py").write(_pytest._code.Source("""
|
testdir.tmpdir.join("hello.py").write(_pytest._code.Source("""
|
||||||
import asdalsdkjaslkdjasd
|
import asdalsdkjaslkdjasd
|
||||||
"""))
|
"""))
|
||||||
|
@ -209,10 +221,11 @@ class TestDoctests:
|
||||||
>>>
|
>>>
|
||||||
""")
|
""")
|
||||||
result = testdir.runpytest("--doctest-modules")
|
result = testdir.runpytest("--doctest-modules")
|
||||||
|
# doctest is never executed because of error during hello.py collection
|
||||||
result.stdout.fnmatch_lines([
|
result.stdout.fnmatch_lines([
|
||||||
"*>>> import hello",
|
"*ERROR collecting hello.py*",
|
||||||
"*UNEXPECTED*ImportError*",
|
"*ImportError: No module named *asdals*",
|
||||||
"*import asdals*",
|
"*Interrupted: 1 errors during collection*",
|
||||||
])
|
])
|
||||||
|
|
||||||
def test_doctestmodule(self, testdir):
|
def test_doctestmodule(self, testdir):
|
||||||
|
|
|
@ -231,6 +231,6 @@ def test_failure_issue380(testdir):
|
||||||
pass
|
pass
|
||||||
""")
|
""")
|
||||||
result = testdir.runpytest("--resultlog=log")
|
result = testdir.runpytest("--resultlog=log")
|
||||||
assert result.ret == 1
|
assert result.ret == 2
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -273,7 +273,7 @@ class TestCollectonly:
|
||||||
def test_collectonly_error(self, testdir):
|
def test_collectonly_error(self, testdir):
|
||||||
p = testdir.makepyfile("import Errlkjqweqwe")
|
p = testdir.makepyfile("import Errlkjqweqwe")
|
||||||
result = testdir.runpytest("--collect-only", p)
|
result = testdir.runpytest("--collect-only", p)
|
||||||
assert result.ret == 1
|
assert result.ret == 2
|
||||||
result.stdout.fnmatch_lines(_pytest._code.Source("""
|
result.stdout.fnmatch_lines(_pytest._code.Source("""
|
||||||
*ERROR*
|
*ERROR*
|
||||||
*ImportError*
|
*ImportError*
|
||||||
|
|
Loading…
Reference in New Issue