diff --git a/py/BUILD.bazel b/py/BUILD.bazel index 341ba76fc7d59..7e0fd01d84f8a 100644 --- a/py/BUILD.bazel +++ b/py/BUILD.bazel @@ -77,6 +77,7 @@ TEST_DEPS = [ requirement("pytest-instafail"), requirement("pytest-trio"), requirement("pytest-mock"), + requirement("rich"), requirement("zipp"), "@rules_python//python/runfiles", ] diff --git a/py/conftest.py b/py/conftest.py index 2100f062a27c3..07938ab0b17a2 100644 --- a/py/conftest.py +++ b/py/conftest.py @@ -20,10 +20,13 @@ import socketserver import sys import threading +import types from dataclasses import dataclass from pathlib import Path import pytest +import rich.console +import rich.traceback from selenium import webdriver from selenium.common.exceptions import WebDriverException @@ -44,6 +47,64 @@ ) +TRACEBACK_WIDTH = 130 +# don't force colors on RBE since errors get redirected to a log file +force_terminal = "REMOTE_BUILD" not in os.environ +console = rich.console.Console(force_terminal=force_terminal, width=TRACEBACK_WIDTH) + + +def extract_traceback_frames(tb): + """Extract frames from a traceback object.""" + frames = [] + while tb: + if hasattr(tb, "tb_frame") and hasattr(tb, "tb_lineno"): + # Skip frames without source files + if Path(tb.tb_frame.f_code.co_filename).exists(): + frames.append((tb.tb_frame, tb.tb_lineno, getattr(tb, "tb_lasti", 0))) + tb = getattr(tb, "tb_next", None) + return frames + + +def filter_frames(frames): + """Filter out frames from pytest internals.""" + skip_modules = ["pytest", "_pytest", "pluggy"] + filtered = [] + for frame, lineno, lasti in reversed(frames): + mod_name = frame.f_globals.get("__name__", "") + if not any(skip in mod_name for skip in skip_modules): + filtered.append((frame, lineno, lasti)) + return filtered + + +def rebuild_traceback(frames): + """Rebuild a traceback object from frames list.""" + new_tb = None + for frame, lineno, lasti in frames: + new_tb = types.TracebackType(new_tb, frame, lasti, lineno) + return new_tb + + +def pytest_runtest_makereport(item, call): + """Hook to print Rich traceback for test failures.""" + if call.excinfo is None: + return + exc_type = call.excinfo.type + exc_value = call.excinfo.value + exc_tb = call.excinfo.tb + frames = extract_traceback_frames(exc_tb) + filtered_frames = filter_frames(frames) + new_tb = rebuild_traceback(filtered_frames) + tb = rich.traceback.Traceback.from_exception( + exc_type, + exc_value, + new_tb, + show_locals=False, + max_frames=5, + width=TRACEBACK_WIDTH, + ) + console.print("\n", tb) + + def pytest_addoption(parser): parser.addoption( "--driver", diff --git a/py/pyproject.toml b/py/pyproject.toml index 4541939a47f80..64da3bf53e5cd 100644 --- a/py/pyproject.toml +++ b/py/pyproject.toml @@ -92,6 +92,10 @@ console_output_style = "progress" faulthandler_timeout = "60" log_cli = true trio_mode = true +addopts = ["-s", "--tb=no"] +filterwarnings = [ + "ignore::DeprecationWarning", +] markers = [ "xfail_chrome: Tests expected to fail in Chrome", "xfail_edge: Tests expected to fail in Edge",