Skip to content

Commit 2305d32

Browse files
authored
Merge pull request #1628 from omarkohl/exit_on_collection_error
Exit pytest on collection error (without executing tests)
2 parents 54872e9 + ede7478 commit 2305d32

8 files changed

+152
-10
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ Michael Birtwell
7272
Michael Droettboom
7373
Mike Lundy
7474
Nicolas Delaby
75+
Oleg Pidsadnyi
7576
Omar Kohl
7677
Pieter Mulder
7778
Piotr Banaszkiewicz

CHANGELOG.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@
7474
message to raise when no exception occurred.
7575
Thanks `@palaviv`_ for the complete PR (`#1616`_).
7676

77+
* Fix `#1421`_: Exit tests if a collection error occurs and add
78+
``--continue-on-collection-errors`` option to restore previous behaviour.
79+
Thanks `@olegpidsadnyi`_ and `@omarkohl`_ for the complete PR (`#1628`_).
80+
7781
.. _@milliams: https://github.com/milliams
7882
.. _@csaftoiu: https://github.com/csaftoiu
7983
.. _@novas0x2a: https://github.com/novas0x2a
@@ -83,7 +87,9 @@
8387
.. _@palaviv: https://github.com/palaviv
8488
.. _@omarkohl: https://github.com/omarkohl
8589
.. _@mikofski: https://github.com/mikofski
90+
.. _@olegpidsadnyi: https://github.com/olegpidsadnyi
8691

92+
.. _#1421: https://github.com/pytest-dev/pytest/issues/1421
8793
.. _#1426: https://github.com/pytest-dev/pytest/issues/1426
8894
.. _#1428: https://github.com/pytest-dev/pytest/pull/1428
8995
.. _#1444: https://github.com/pytest-dev/pytest/pull/1444
@@ -98,6 +104,7 @@
98104
.. _#372: https://github.com/pytest-dev/pytest/issues/372
99105
.. _#1544: https://github.com/pytest-dev/pytest/issues/1544
100106
.. _#1616: https://github.com/pytest-dev/pytest/pull/1616
107+
.. _#1628: https://github.com/pytest-dev/pytest/pull/1628
101108

102109

103110
**Bug Fixes**

_pytest/main.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ def pytest_addoption(parser):
4848
help="run pytest in strict mode, warnings become errors.")
4949
group._addoption("-c", metavar="file", type=str, dest="inifilename",
5050
help="load configuration from `file` instead of trying to locate one of the implicit configuration files.")
51+
group._addoption("--continue-on-collection-errors", action="store_true",
52+
default=False, dest="continue_on_collection_errors",
53+
help="Force test execution even if collection errors occur.")
5154

5255
group = parser.getgroup("collect", "collection")
5356
group.addoption('--collectonly', '--collect-only', action="store_true",
@@ -133,6 +136,11 @@ def pytest_collection(session):
133136
return session.perform_collect()
134137

135138
def pytest_runtestloop(session):
139+
if (session.testsfailed and
140+
not session.config.option.continue_on_collection_errors):
141+
raise session.Interrupted(
142+
"%d errors during collection" % session.testsfailed)
143+
136144
if session.config.option.collectonly:
137145
return True
138146

testing/acceptance_test.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ def test_this():
120120
"ImportError while importing test module*",
121121
"'No module named *does_not_work*",
122122
])
123-
assert result.ret == 1
123+
assert result.ret == 2
124124

125125
def test_not_collectable_arguments(self, testdir):
126126
p1 = testdir.makepyfile("")
@@ -665,11 +665,13 @@ def test_with_failing_collection(self, testdir):
665665
testdir.makepyfile(self.source)
666666
testdir.makepyfile(test_collecterror="""xyz""")
667667
result = testdir.runpytest("--durations=2", "-k test_1")
668-
assert result.ret != 0
668+
assert result.ret == 2
669669
result.stdout.fnmatch_lines([
670-
"*durations*",
671-
"*call*test_1*",
670+
"*Interrupted: 1 errors during collection*",
672671
])
672+
# Collection errors abort test execution, therefore no duration is
673+
# output
674+
assert "duration" not in result.stdout.str()
673675

674676
def test_with_not(self, testdir):
675677
testdir.makepyfile(self.source)

testing/test_collection.py

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -642,3 +642,114 @@ def test___repr__():
642642
""")
643643
reprec = testdir.inline_run("-k repr")
644644
reprec.assertoutcome(passed=1, failed=0)
645+
646+
647+
COLLECTION_ERROR_PY_FILES = dict(
648+
test_01_failure="""
649+
def test_1():
650+
assert False
651+
""",
652+
test_02_import_error="""
653+
import asdfasdfasdf
654+
def test_2():
655+
assert True
656+
""",
657+
test_03_import_error="""
658+
import asdfasdfasdf
659+
def test_3():
660+
assert True
661+
""",
662+
test_04_success="""
663+
def test_4():
664+
assert True
665+
""",
666+
)
667+
668+
def test_exit_on_collection_error(testdir):
669+
"""Verify that all collection errors are collected and no tests executed"""
670+
testdir.makepyfile(**COLLECTION_ERROR_PY_FILES)
671+
672+
res = testdir.runpytest()
673+
assert res.ret == 2
674+
675+
res.stdout.fnmatch_lines([
676+
"collected 2 items / 2 errors",
677+
"*ERROR collecting test_02_import_error.py*",
678+
"*No module named *asdfa*",
679+
"*ERROR collecting test_03_import_error.py*",
680+
"*No module named *asdfa*",
681+
])
682+
683+
684+
def test_exit_on_collection_with_maxfail_smaller_than_n_errors(testdir):
685+
"""
686+
Verify collection is aborted once maxfail errors are encountered ignoring
687+
further modules which would cause more collection errors.
688+
"""
689+
testdir.makepyfile(**COLLECTION_ERROR_PY_FILES)
690+
691+
res = testdir.runpytest("--maxfail=1")
692+
assert res.ret == 2
693+
694+
res.stdout.fnmatch_lines([
695+
"*ERROR collecting test_02_import_error.py*",
696+
"*No module named *asdfa*",
697+
"*Interrupted: stopping after 1 failures*",
698+
])
699+
700+
assert 'test_03' not in res.stdout.str()
701+
702+
703+
def test_exit_on_collection_with_maxfail_bigger_than_n_errors(testdir):
704+
"""
705+
Verify the test run aborts due to collection errors even if maxfail count of
706+
errors was not reached.
707+
"""
708+
testdir.makepyfile(**COLLECTION_ERROR_PY_FILES)
709+
710+
res = testdir.runpytest("--maxfail=4")
711+
assert res.ret == 2
712+
713+
res.stdout.fnmatch_lines([
714+
"collected 2 items / 2 errors",
715+
"*ERROR collecting test_02_import_error.py*",
716+
"*No module named *asdfa*",
717+
"*ERROR collecting test_03_import_error.py*",
718+
"*No module named *asdfa*",
719+
])
720+
721+
722+
def test_continue_on_collection_errors(testdir):
723+
"""
724+
Verify tests are executed even when collection errors occur when the
725+
--continue-on-collection-errors flag is set
726+
"""
727+
testdir.makepyfile(**COLLECTION_ERROR_PY_FILES)
728+
729+
res = testdir.runpytest("--continue-on-collection-errors")
730+
assert res.ret == 1
731+
732+
res.stdout.fnmatch_lines([
733+
"collected 2 items / 2 errors",
734+
"*1 failed, 1 passed, 2 error*",
735+
])
736+
737+
738+
def test_continue_on_collection_errors_maxfail(testdir):
739+
"""
740+
Verify tests are executed even when collection errors occur and that maxfail
741+
is honoured (including the collection error count).
742+
4 tests: 2 collection errors + 1 failure + 1 success
743+
test_4 is never executed because the test run is with --maxfail=3 which
744+
means it is interrupted after the 2 collection errors + 1 failure.
745+
"""
746+
testdir.makepyfile(**COLLECTION_ERROR_PY_FILES)
747+
748+
res = testdir.runpytest("--continue-on-collection-errors", "--maxfail=3")
749+
assert res.ret == 2
750+
751+
res.stdout.fnmatch_lines([
752+
"collected 2 items / 2 errors",
753+
"*Interrupted: stopping after 3 failures*",
754+
"*1 failed, 2 error*",
755+
])

testing/test_doctest.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -199,8 +199,20 @@ def test(self):
199199
"*1 failed*",
200200
])
201201

202+
def test_doctest_unex_importerror_only_txt(self, testdir):
203+
testdir.maketxtfile("""
204+
>>> import asdalsdkjaslkdjasd
205+
>>>
206+
""")
207+
result = testdir.runpytest()
208+
# doctest is never executed because of error during hello.py collection
209+
result.stdout.fnmatch_lines([
210+
"*>>> import asdals*",
211+
"*UNEXPECTED*ImportError*",
212+
"ImportError: No module named *asdal*",
213+
])
202214

203-
def test_doctest_unex_importerror(self, testdir):
215+
def test_doctest_unex_importerror_with_module(self, testdir):
204216
testdir.tmpdir.join("hello.py").write(_pytest._code.Source("""
205217
import asdalsdkjaslkdjasd
206218
"""))
@@ -209,10 +221,11 @@ def test_doctest_unex_importerror(self, testdir):
209221
>>>
210222
""")
211223
result = testdir.runpytest("--doctest-modules")
224+
# doctest is never executed because of error during hello.py collection
212225
result.stdout.fnmatch_lines([
213-
"*>>> import hello",
214-
"*UNEXPECTED*ImportError*",
215-
"*import asdals*",
226+
"*ERROR collecting hello.py*",
227+
"*ImportError: No module named *asdals*",
228+
"*Interrupted: 1 errors during collection*",
216229
])
217230

218231
def test_doctestmodule(self, testdir):

testing/test_resultlog.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,6 @@ def test_func():
231231
pass
232232
""")
233233
result = testdir.runpytest("--resultlog=log")
234-
assert result.ret == 1
234+
assert result.ret == 2
235235

236236

testing/test_terminal.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ def test_method(self):
273273
def test_collectonly_error(self, testdir):
274274
p = testdir.makepyfile("import Errlkjqweqwe")
275275
result = testdir.runpytest("--collect-only", p)
276-
assert result.ret == 1
276+
assert result.ret == 2
277277
result.stdout.fnmatch_lines(_pytest._code.Source("""
278278
*ERROR*
279279
*ImportError*

0 commit comments

Comments
 (0)