Skip to content

Commit 962243c

Browse files
committed
ci: trace imports and swallowed exceptions into the crash log
The stack from the last round places the fault inside an extension module's initialisation, two exec_module frames below cuda.core._context, but does not name it. PYTHONVERBOSE was meant to: it announces every import, so its last line would be the module. It could not deliver -- its output goes through the interpreter's C-level stdio, which buffers when redirected to a file, and the child's tail stopped at `import 'colorama'` several hundred lines before the crash. The crash log does not have that problem. It is line buffered, and both of its lines survived every run so far, so the same question goes through that file instead. A meta path finder records each name and returns None, leaving the real finders to do the work; because the write lands before the module begins executing, a fault during an extension's init leaves that module as the last IMPORT line. sys.unraisablehook goes to the same file. A Cython `cdef ... noexcept` function cannot propagate an exception, so one raised inside it is printed and cleared and whatever it was computing keeps its default -- in _resource_handles.pyx, a NULL driver function pointer that faults only when something later calls through it. Recording unraisables in sequence with the IMPORT lines puts a swallowed error next to the module that swallowed it. The previous hook still runs, so stderr is unchanged. Both are off unless PYTEST_CRASHLOG is set, and tracing starts before pytest.main because the fault happens before pytest_configure would run.
1 parent 9138df0 commit 962243c

2 files changed

Lines changed: 64 additions & 0 deletions

File tree

‎ci/tools/pytest_crashlog.py‎

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,63 @@ def note(message):
6464
_log.write(f"{message}\n")
6565

6666

67+
class _ImportNoter:
68+
"""A meta path finder that records a name and then steps aside.
69+
70+
Returning None means "I cannot supply this module", so the real finders
71+
run right after and behaviour is unchanged. What it buys is a line in
72+
the log for every import the interpreter begins, in the order it begins
73+
them -- and because the log is line buffered, the innermost name is on
74+
disk before the module it names starts executing. A crash inside an
75+
extension module's init therefore leaves that module as the last line.
76+
77+
This exists because PYTHONVERBOSE could not answer the same question:
78+
its output goes through the interpreter's C-level stdio, which buffers
79+
when redirected to a file, so a fault takes the last several hundred
80+
lines with it.
81+
"""
82+
83+
@staticmethod
84+
def find_spec(fullname, path=None, target=None): # noqa: ARG004 - MetaPathFinder signature
85+
note(f"IMPORT {fullname}")
86+
return None
87+
88+
89+
def _note_unraisable(unraisable):
90+
"""Record what would otherwise scroll past as `Exception ignored in:`.
91+
92+
A Cython `cdef ... noexcept` function cannot propagate an exception, so
93+
one raised inside it is printed and cleared, and whatever it was
94+
computing is left at its default -- a NULL function pointer, in the case
95+
of cuda.core's driver-pointer initialisers, which faults only later when
96+
something calls through it. Routing unraisables into this log puts them
97+
in sequence with the IMPORT lines, so a swallowed error and the module
98+
that swallowed it appear together.
99+
"""
100+
exc_type = getattr(unraisable.exc_type, "__name__", unraisable.exc_type)
101+
note(f"UNRAISABLE {exc_type}: {unraisable.exc_value!r} in {unraisable.object!r}")
102+
if _prev_unraisablehook is not None:
103+
_prev_unraisablehook(unraisable)
104+
105+
106+
_prev_unraisablehook = None
107+
_tracing = False
108+
109+
110+
def enable_tracing():
111+
"""Start recording imports and unraisable exceptions. Idempotent."""
112+
global _prev_unraisablehook, _tracing
113+
if _tracing or _log is None:
114+
return
115+
_tracing = True
116+
# First in the list, so the name is recorded before any real finder can
117+
# start loading it.
118+
sys.meta_path.insert(0, _ImportNoter)
119+
_prev_unraisablehook = sys.unraisablehook
120+
sys.unraisablehook = _note_unraisable
121+
note("tracing enabled")
122+
123+
67124
def pytest_configure():
68125
# Covers direct `-p pytest_crashlog` use, where nothing called open_log().
69126
# By the time this runs the initial conftests have already been imported,

‎ci/tools/run_pytest_with_stack.py‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,13 @@ def main():
125125
# would otherwise leave no file at all, indistinguishable from a crash
126126
# before pytest ever started.
127127
pytest_crashlog.open_log()
128+
# The nightly's log has so far ended at "entering pytest.main", which
129+
# says the fault is somewhere in the initial conftest import but not
130+
# which module. Tracing imports through the same line-buffered file
131+
# answers that: the last IMPORT line names the module that was being
132+
# loaded. Enabled here so it also covers pytest's own startup, since
133+
# the fault happens before pytest_configure would run.
134+
pytest_crashlog.enable_tracing()
128135
pytest_crashlog.note("entering pytest.main")
129136
# Hand pytest the imported module rather than "-p pytest_crashlog":
130137
# naming it would make pytest import it a second time and warn that

0 commit comments

Comments
 (0)