Skip to content

Commit 006a901

Browse files
committed
Properly handle exceptions in multiprocessing tasks
Fix pytest-dev#1984
1 parent 45b21fa commit 006a901

File tree

4 files changed

+96
-5
lines changed

4 files changed

+96
-5
lines changed

CHANGELOG.rst

+10-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* Import errors when collecting test modules now display the full traceback (`#1976`_).
77
Thanks `@cwitty`_ for the report and `@nicoddemus`_ for the PR.
88

9-
* Fix confusing command-line help message for custom options with two or more `metavar` properties (`#2004`_).
9+
* Fix confusing command-line help message for custom options with two or more ``metavar`` properties (`#2004`_).
1010
Thanks `@okulynyak`_ and `@davehunt`_ for the report and `@nicoddemus`_ for the PR.
1111

1212
* When loading plugins, import errors which contain non-ascii messages are now properly handled in Python 2 (`#1998`_).
@@ -23,16 +23,25 @@
2323
* Fix teardown error message in generated xUnit XML.
2424
Thanks `@gdyuldin`_ or the PR.
2525

26+
* Properly handle exceptions in ``multiprocessing`` tasks (`#1984`_).
27+
Thanks `@adborden`_ for the report and `@nicoddemus`_ for the PR.
28+
29+
*
30+
31+
*
32+
2633
*
2734

2835

36+
.. _@adborden: https://github.com/adborden
2937
.. _@cwitty: https://github.com/cwitty
3038
.. _@okulynyak: https://github.com/okulynyak
3139
.. _@matclab: https://github.com/matclab
3240
.. _@gdyuldin: https://github.com/gdyuldin
3341

3442
.. _#442: https://github.com/pytest-dev/pytest/issues/442
3543
.. _#1976: https://github.com/pytest-dev/pytest/issues/1976
44+
.. _#1984: https://github.com/pytest-dev/pytest/issues/1984
3645
.. _#1998: https://github.com/pytest-dev/pytest/issues/1998
3746
.. _#2004: https://github.com/pytest-dev/pytest/issues/2004
3847
.. _#2005: https://github.com/pytest-dev/pytest/issues/2005

_pytest/_code/code.py

+11-4
Original file line numberDiff line numberDiff line change
@@ -623,16 +623,23 @@ def repr_excinfo(self, excinfo):
623623
e = excinfo.value
624624
descr = None
625625
while e is not None:
626-
reprtraceback = self.repr_traceback(excinfo)
627-
reprcrash = excinfo._getreprcrash()
626+
if excinfo:
627+
reprtraceback = self.repr_traceback(excinfo)
628+
reprcrash = excinfo._getreprcrash()
629+
else:
630+
# fallback to native repr if the exception doesn't have a traceback:
631+
# ExceptionInfo objects require a full traceback to work
632+
reprtraceback = ReprTracebackNative(py.std.traceback.format_exception(type(e), e, None))
633+
reprcrash = None
634+
628635
repr_chain += [(reprtraceback, reprcrash, descr)]
629636
if e.__cause__ is not None:
630637
e = e.__cause__
631-
excinfo = ExceptionInfo((type(e), e, e.__traceback__))
638+
excinfo = ExceptionInfo((type(e), e, e.__traceback__)) if e.__traceback__ else None
632639
descr = 'The above exception was the direct cause of the following exception:'
633640
elif e.__context__ is not None:
634641
e = e.__context__
635-
excinfo = ExceptionInfo((type(e), e, e.__traceback__))
642+
excinfo = ExceptionInfo((type(e), e, e.__traceback__)) if e.__traceback__ else None
636643
descr = 'During handling of the above exception, another exception occurred:'
637644
else:
638645
e = None

testing/code/test_excinfo.py

+44
Original file line numberDiff line numberDiff line change
@@ -1050,6 +1050,50 @@ def h():
10501050
assert line.endswith('mod.py')
10511051
assert tw.lines[47] == ":15: AttributeError"
10521052

1053+
@pytest.mark.skipif("sys.version_info[0] < 3")
1054+
@pytest.mark.parametrize('reason, description', [
1055+
('cause', 'The above exception was the direct cause of the following exception:'),
1056+
('context', 'During handling of the above exception, another exception occurred:'),
1057+
])
1058+
def test_exc_chain_repr_without_traceback(self, importasmod, reason, description):
1059+
"""
1060+
Handle representation of exception chains where one of the exceptions doesn't have a
1061+
real traceback, such as those raised in a subprocess submitted by the multiprocessing
1062+
module (#1984).
1063+
"""
1064+
from _pytest.pytester import LineMatcher
1065+
exc_handling_code = ' from e' if reason == 'cause' else ''
1066+
mod = importasmod("""
1067+
def f():
1068+
try:
1069+
g()
1070+
except Exception as e:
1071+
raise RuntimeError('runtime problem'){exc_handling_code}
1072+
def g():
1073+
raise ValueError('invalid value')
1074+
""".format(exc_handling_code=exc_handling_code))
1075+
1076+
with pytest.raises(RuntimeError) as excinfo:
1077+
mod.f()
1078+
1079+
# emulate the issue described in #1984
1080+
attr = '__%s__' % reason
1081+
getattr(excinfo.value, attr).__traceback__ = None
1082+
1083+
r = excinfo.getrepr()
1084+
tw = py.io.TerminalWriter(stringio=True)
1085+
tw.hasmarkup = False
1086+
r.toterminal(tw)
1087+
1088+
matcher = LineMatcher(tw.stringio.getvalue().splitlines())
1089+
matcher.fnmatch_lines([
1090+
"ValueError: invalid value",
1091+
description,
1092+
"* except Exception as e:",
1093+
"> * raise RuntimeError('runtime problem')" + exc_handling_code,
1094+
"E *RuntimeError: runtime problem",
1095+
])
1096+
10531097

10541098
@pytest.mark.parametrize("style", ["short", "long"])
10551099
@pytest.mark.parametrize("encoding", [None, "utf8", "utf16"])

testing/test_assertion.py

+31
Original file line numberDiff line numberDiff line change
@@ -749,6 +749,37 @@ def test_onefails():
749749
"*test_traceback_failure.py:4: AssertionError"
750750
])
751751

752+
753+
@pytest.mark.skipif(sys.version_info[:2] <= (3, 3), reason='Python 3.4+ shows chained exceptions on multiprocess')
754+
def test_exception_handling_no_traceback(testdir):
755+
"""
756+
Handle chain exceptions in tasks submitted by the multiprocess module (#1984).
757+
"""
758+
p1 = testdir.makepyfile("""
759+
from multiprocessing import Pool
760+
761+
def process_task(n):
762+
assert n == 10
763+
764+
def multitask_job():
765+
tasks = [1]
766+
with Pool(processes=1) as pool:
767+
pool.map(process_task, tasks)
768+
769+
def test_multitask_job():
770+
multitask_job()
771+
""")
772+
result = testdir.runpytest(p1, "--tb=long")
773+
result.stdout.fnmatch_lines([
774+
"====* FAILURES *====",
775+
"*multiprocessing.pool.RemoteTraceback:*",
776+
"Traceback (most recent call last):",
777+
"*assert n == 10",
778+
"The above exception was the direct cause of the following exception:",
779+
"> * multitask_job()",
780+
])
781+
782+
752783
@pytest.mark.skipif("'__pypy__' in sys.builtin_module_names or sys.platform.startswith('java')" )
753784
def test_warn_missing(testdir):
754785
testdir.makepyfile("")

0 commit comments

Comments
 (0)