Skip to content

Commit 901f446

Browse files
committed
Allow to print the stack trace of the last retry error
1 parent 4e7dfc2 commit 901f446

File tree

3 files changed

+55
-17
lines changed

3 files changed

+55
-17
lines changed

framework/errors.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
14+
import traceback
1415
from typing import Any
1516

1617

@@ -56,3 +57,16 @@ def note_blanket_error_info_below(
5657
f"{cls.note_blanket_error(reason)}"
5758
f"# Please inspect the information below:\n{info_below}"
5859
)
60+
61+
62+
def format_error_with_trace(error: Exception) -> str:
63+
"""Returns formatted exception its stack trace."""
64+
return "".join(traceback.TracebackException.from_exception(error).format())
65+
66+
67+
def format_trace_only(error: Exception) -> str:
68+
"""Same as format_error_with_trace, but stack trace only."""
69+
exception_tb = traceback.TracebackException.from_exception(error)
70+
if not exception_tb.stack:
71+
return ""
72+
return "".join(exception_tb.stack.format())

framework/helpers/retryers.py

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@
3030
from tenacity import stop
3131
from tenacity import wait
3232
from tenacity.retry import retry_base
33+
from typing_extensions import Self
34+
35+
import framework.errors
3336

3437
retryers_logger = logging.getLogger(__name__)
3538
# Type aliases
@@ -229,6 +232,8 @@ class RetryError(tenacity.RetryError):
229232

230233
last_attempt: tenacity.Future
231234
note: str = ""
235+
_message: str = ""
236+
_print_last_trace: bool = True
232237

233238
def __init__(
234239
self,
@@ -238,35 +243,55 @@ def __init__(
238243
attempts: int = 0,
239244
check_result: Optional[CheckResultFn] = None,
240245
note: str = "",
246+
print_last_trace: bool = True,
241247
):
242248
last_attempt: tenacity.Future = retry_state.outcome
243249
super().__init__(last_attempt)
250+
self._print_last_trace = print_last_trace
251+
252+
message = f"Retry error"
244253

245-
self.message = f"Retry error"
246254
if retry_state.fn is None:
247255
# Context manager
248-
self.message += f":"
256+
message += f":"
249257
else:
250258
callback_name = tenacity_utils.get_callback_name(retry_state.fn)
251-
self.message += f" calling {callback_name}:"
259+
message += f" calling {callback_name}:"
260+
252261
if timeout:
253-
self.message += f" timeout {timeout} (h:mm:ss) exceeded"
262+
message += f" timeout {timeout} (h:mm:ss) exceeded"
254263
if attempts:
255-
self.message += " or"
264+
message += " or"
265+
256266
if attempts:
257-
self.message += f" {attempts} attempts exhausted"
267+
message += f" {attempts} attempts exhausted"
258268

259-
self.message += "."
269+
message += "."
260270

261271
if last_attempt.failed:
262272
err = last_attempt.exception()
263-
self.message += f" Last exception: {type(err).__name__}: {err}"
273+
message += f" Last Retry Attempt Error: {type(err).__name__}"
264274
elif check_result:
265-
self.message += " Check result callback returned False."
275+
message += " Check result callback returned False."
276+
277+
self._message = message
266278

267279
if note:
268280
self.add_note(note)
269281

282+
@property
283+
def message(self):
284+
message = ""
285+
286+
# TODO(sergiitk): consider if we want to have print-by-default
287+
# and/or ignore-by-default exception lists.
288+
if cause := self.exception() and self._print_last_trace:
289+
cause_trace = framework.errors.format_error_with_trace(cause)
290+
message += f"Last Retry Attempt Traceback:\n{cause_trace}\n\n"
291+
292+
message += self._message
293+
return message
294+
270295
def result(self, *, default=None):
271296
return (
272297
self.last_attempt.result()
@@ -295,6 +320,10 @@ def reason_str(self):
295320
def _exception_str(cls, err: Optional[BaseException]) -> str:
296321
return f"{type(err).__name__}: {err}" if err else "???"
297322

323+
def with_last_trace(self, enabled: bool = True) -> Self:
324+
self._print_last_trace = enabled
325+
return self
326+
298327
# TODO(sergiitk): Remove in py3.11, this will be built-in. See PEP 678.
299328
def add_note(self, note: str):
300329
self.note = note

framework/test_cases/base_testcase.py

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,14 @@
1313
# limitations under the License.
1414
"""Base test case used for xds test suites."""
1515
import inspect
16-
import traceback
1716
from typing import Optional, Union
1817
import unittest
1918

2019
from absl import logging
2120
from absl.testing import absltest
2221

22+
import framework.errors
23+
2324

2425
class BaseTestCase(absltest.TestCase):
2526
# @override
@@ -135,7 +136,7 @@ def _log_framed_test_failure(
135136
) -> None:
136137
trace: str
137138
if isinstance(error, Exception):
138-
trace = cls._format_error_with_trace(error)
139+
trace = framework.errors.format_error_with_trace(error)
139140
else:
140141
trace = error
141142

@@ -154,9 +155,3 @@ def _log_framed_test_failure(
154155
"error": trace,
155156
},
156157
)
157-
158-
@classmethod
159-
def _format_error_with_trace(cls, error: Exception) -> str:
160-
return "".join(
161-
traceback.TracebackException.from_exception(error).format()
162-
)

0 commit comments

Comments
 (0)