blob: 5060f9117b05fa067c41a72ca6be70fed88101bd [file]
# Copyright 2026 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Exception utilities for Mobly tests."""
import logging
from typing import NoReturn
_LOGGER = logging.getLogger(__name__)
def unroll_and_raise(exc: BaseException) -> NoReturn:
"""Logs the full ancestry tree and raises the earliest root cause found via context."""
seen = set()
log_entries = []
def _traverse(
e: BaseException, level: int = 0, is_main_path: bool = True
) -> BaseException:
if id(e) in seen:
return e
seen.add(id(e))
log_entries.append((e, level, is_main_path))
ctx = getattr(e, "__context__", None)
cause = getattr(e, "__cause__", None)
# Strategy: Prioritize context as the chronological 'main' ancestor.
main_ancestor = ctx or cause
# If both exist, follow the cause branch as a 'side' traversal.
side_ancestor = cause if (ctx and cause) else None
if side_ancestor:
_traverse(side_ancestor, level + 1, is_main_path=False)
if main_ancestor:
return _traverse(
main_ancestor, level + 1, is_main_path=is_main_path
)
return e
root = _traverse(exc, level=0, is_main_path=True)
if len(log_entries) > 1:
_LOGGER.warning(
"Multiple exceptions occurred during execution and teardown:"
)
for i, (e, level, is_main) in enumerate(log_entries):
prefix = " " * level
tag = "[Primary]" if is_main else "[Explanation]"
_LOGGER.warning(
f"{prefix}{tag} Exception {len(log_entries)-i}: {type(e).__name__}: {e}"
)
raise root