Commit 1477dd02 authored by Kirill Smelkov's avatar Kirill Smelkov

golang: Fix defer exception chaining in the presence of subchains

A deferred function can raise exception and this exception itself can
have .__context__ - consider for example if B1 is raised and its chain
is B1->B2->B3. Before calling that deferred function, we save
then-current exception A1 into GoFrame.exc_ctx and link-to .exc_ctx after
the call. We were previously trying to link-to .exc_ctx from raised
exception itself - i.e. B1 in the above example which is not correct:
B1 was raised while B2 was being raised ... etc and it was B3 who was
raised after A1. The consequence was that A1 was list, since B1 already
had non-empty .__context__

-> Fix it by linking-to A1 from B3, not from B1. In other words by
linking-to .exc_ctx chain from tail of exception chain of raised
exception.

We can be sure that updated test is correct because it passes under
Python3 where exception chaining is implemented natively.

Fixes: bb9a94c3 (golang: Teach defer to chain exceptions (PEP 3134) even on Python2)
parent 06cac90b
...@@ -131,8 +131,29 @@ class _GoFrame: ...@@ -131,8 +131,29 @@ class _GoFrame:
# py2: simulate exception chaining (PEP 3134) # py2: simulate exception chaining (PEP 3134)
if six.PY2: if six.PY2:
if exc_val is not None: if exc_val is not None:
if getattr(exc_val, '__context__', None) is None: # exc_val is current outer exception raised by e.g. earlier
exc_val.__context__ = __goframe__.exc_ctx # defers; it can be itself chained.
# .exc_ctx is current inner exception we saved before calling
# code that raised exc_val. For example:
#
# _GoFrame.__exit__:
# saves .exc_ctx # .exc_ctx = A1
# with __goframe__:
# call other defer from .deferv
# __exit__(exc_val): # exc_val = B3 (-> linked to B2 -> B1)
#
# the order in which exceptions were raised is: A1 B1 B2 B3
# thus A1 is the context of B1, or in other words, .exc_ctx
# should be linked to from tail of exc_val exception chain.
exc_tail = exc_val
while 1:
_ = getattr(exc_tail, '__context__', None)
if _ is None:
break
exc_tail = _
exc_tail.__context__ = __goframe__.exc_ctx
# make sure .__cause__ and .__suppress_context__ are always present
if not hasattr(exc_val, '__cause__'): if not hasattr(exc_val, '__cause__'):
exc_val.__cause__ = None exc_val.__cause__ = None
if not hasattr(exc_val, '__suppress_context__'): if not hasattr(exc_val, '__suppress_context__'):
......
...@@ -1246,11 +1246,22 @@ def test_defer_excchain(): ...@@ -1246,11 +1246,22 @@ def test_defer_excchain():
@func @func
def d2(): # NOTE regular raise inside @func def d2(): # NOTE regular raise inside @func
1/0 # which initially sets .__context__ to None 1/0 # which initially sets .__context__ to None
@func
def d3(): def d3():
raise RuntimeError("d3: bbb") # d33->d32->d31 subchain that has to be correctly glued with neighbours as:
# "d4: bbb" -> d33->d32->d31 -> 1/0
def d31(): raise RuntimeError("d31")
def d32(): raise RuntimeError("d32")
def d33(): raise RuntimeError("d33")
defer(d33)
defer(d32)
defer(d31)
def d4():
raise RuntimeError("d4: bbb")
@func @func
def _(): def _():
defer(d4)
defer(d3) defer(d3)
defer(d2) defer(d2)
defer(d1) defer(d1)
...@@ -1259,17 +1270,44 @@ def test_defer_excchain(): ...@@ -1259,17 +1270,44 @@ def test_defer_excchain():
with raises(RuntimeError) as exci: with raises(RuntimeError) as exci:
_() _()
e3 = exci.value e4 = exci.value
assert type(e3) is RuntimeError assert type(e4) is RuntimeError
assert e3.args == ("d3: bbb",) assert e4.args == ("d4: bbb",)
assert e3.__cause__ is None assert e4.__cause__ is None
assert e3.__context__ is not None assert e4.__context__ is not None
if six.PY3: # .__traceback__ of top-level exception if six.PY3: # .__traceback__ of top-level exception
assert e3.__traceback__ is not None assert e4.__traceback__ is not None
tb3 = Traceback(e3.__traceback__) tb4 = Traceback(e4.__traceback__)
assert tb3[-1].name == "d3" assert tb4[-1].name == "d4"
e2 = e3.__context__ e33 = e4.__context__
assert type(e33) is RuntimeError
assert e33.args == ("d33",)
assert e33.__cause__ is None
assert e33.__context__ is not None
assert e33.__traceback__ is not None
tb33 = Traceback(e33.__traceback__)
assert tb33[-1].name == "d33"
e32 = e33.__context__
assert type(e32) is RuntimeError
assert e32.args == ("d32",)
assert e32.__cause__ is None
assert e32.__context__ is not None
assert e32.__traceback__ is not None
tb32 = Traceback(e32.__traceback__)
assert tb32[-1].name == "d32"
e31 = e32.__context__
assert type(e31) is RuntimeError
assert e31.args == ("d31",)
assert e31.__cause__ is None
assert e31.__context__ is not None
assert e31.__traceback__ is not None
tb31 = Traceback(e31.__traceback__)
assert tb31[-1].name == "d31"
e2 = e31.__context__
assert type(e2) is ZeroDivisionError assert type(e2) is ZeroDivisionError
#assert e2.args == ("division by zero",) # text is different in between py23 #assert e2.args == ("division by zero",) # text is different in between py23
assert e2.__cause__ is None assert e2.__cause__ is None
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment