|
20 | 20 | import socketserver |
21 | 21 | import sys |
22 | 22 | import threading |
| 23 | +import types |
23 | 24 | from dataclasses import dataclass |
24 | 25 | from pathlib import Path |
25 | 26 |
|
26 | 27 | import pytest |
| 28 | +import rich.console |
| 29 | +import rich.traceback |
27 | 30 |
|
28 | 31 | from selenium import webdriver |
29 | 32 | from selenium.common.exceptions import WebDriverException |
|
44 | 47 | ) |
45 | 48 |
|
46 | 49 |
|
| 50 | +TRACEBACK_WIDTH = 130 |
| 51 | +# don't force colors on RBE since errors get redirected to a log file |
| 52 | +force_terminal = "REMOTE_BUILD" not in os.environ |
| 53 | +console = rich.console.Console(force_terminal=force_terminal, width=TRACEBACK_WIDTH) |
| 54 | + |
| 55 | + |
| 56 | +def extract_traceback_frames(tb): |
| 57 | + """Extract frames from a traceback object.""" |
| 58 | + frames = [] |
| 59 | + while tb: |
| 60 | + if hasattr(tb, "tb_frame") and hasattr(tb, "tb_lineno"): |
| 61 | + # Skip frames without source files |
| 62 | + if Path(tb.tb_frame.f_code.co_filename).exists(): |
| 63 | + frames.append((tb.tb_frame, tb.tb_lineno, getattr(tb, "tb_lasti", 0))) |
| 64 | + tb = getattr(tb, "tb_next", None) |
| 65 | + return frames |
| 66 | + |
| 67 | + |
| 68 | +def filter_frames(frames): |
| 69 | + """Filter out frames from pytest internals.""" |
| 70 | + skip_modules = ["pytest", "_pytest", "pluggy"] |
| 71 | + filtered = [] |
| 72 | + for frame, lineno, lasti in reversed(frames): |
| 73 | + mod_name = frame.f_globals.get("__name__", "") |
| 74 | + if not any(skip in mod_name for skip in skip_modules): |
| 75 | + filtered.append((frame, lineno, lasti)) |
| 76 | + return filtered |
| 77 | + |
| 78 | + |
| 79 | +def rebuild_traceback(frames): |
| 80 | + """Rebuild a traceback object from frames list.""" |
| 81 | + new_tb = None |
| 82 | + for frame, lineno, lasti in frames: |
| 83 | + new_tb = types.TracebackType(new_tb, frame, lasti, lineno) |
| 84 | + return new_tb |
| 85 | + |
| 86 | + |
| 87 | +def pytest_runtest_makereport(item, call): |
| 88 | + """Hook to print Rich traceback for test failures.""" |
| 89 | + if call.excinfo is None: |
| 90 | + return |
| 91 | + exc_type = call.excinfo.type |
| 92 | + exc_value = call.excinfo.value |
| 93 | + exc_tb = call.excinfo.tb |
| 94 | + frames = extract_traceback_frames(exc_tb) |
| 95 | + filtered_frames = filter_frames(frames) |
| 96 | + new_tb = rebuild_traceback(filtered_frames) |
| 97 | + tb = rich.traceback.Traceback.from_exception( |
| 98 | + exc_type, |
| 99 | + exc_value, |
| 100 | + new_tb, |
| 101 | + show_locals=False, |
| 102 | + max_frames=5, |
| 103 | + width=TRACEBACK_WIDTH, |
| 104 | + ) |
| 105 | + console.print("\n", tb) |
| 106 | + |
| 107 | + |
47 | 108 | def pytest_addoption(parser): |
48 | 109 | parser.addoption( |
49 | 110 | "--driver", |
|
0 commit comments