Skip to content

Commit f3d2ed7

Browse files
committed
diag: probe v2 for Windows shutdown crash (real pytest subprocesses, cp313)
1 parent 0387210 commit f3d2ed7

2 files changed

Lines changed: 101 additions & 6 deletions

File tree

.github/workflows/python-wheels.yml

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -93,13 +93,10 @@ jobs:
9393
VCPKG_DEFAULT_TRIPLET: x64-windows-dynamic-release
9494
VCPKG_DEFAULT_BINARY_CACHE: ${{ github.workspace }}/vcpkg-bincache
9595
CMAKE_TOOLCHAIN_FILE: ${{ github.workspace }}/vcpkg/scripts/buildsystems/vcpkg.cmake
96-
CIBW_BUILD: "*-win_amd64"
96+
CIBW_BUILD: "cp313-win_amd64"
9797
CIBW_TEST_REQUIRES: pytest adbc_driver_manager geoarrow-pyarrow geopandas
98-
# Test command exits 0 even on failure so CIBW continues building all wheels;
99-
# failures are recorded in .test_failed and checked after upload.
100-
# We use Python here to be absolutely sure there are no shell escaping issues.
101-
CIBW_TEST_COMMAND: >-
102-
python -c "import subprocess,sys,pathlib; proj=pathlib.Path(r'{project}'); r=subprocess.run([sys.executable,'-m','pytest',str(pathlib.Path(r'{package}')/'tests'),'-vv','-o','faulthandler_timeout=600']); r.returncode and (print(f'Tests failed (exit {r.returncode}), creating .test_failed marker at {proj.absolute()}') or (proj/'.test_failed').touch() or True); sys.exit(0)"
98+
# DIAGNOSTIC: run the shutdown-crash probe v2 (real pytest subprocesses).
99+
CIBW_TEST_COMMAND: "python {project}/ci/scripts/win_teardown_probe_v2.py {package}/tests"
103100

104101
# Save even on failure (hence a standalone save step gated only on a miss):
105102
# binaries built before a later test failure stay cached for the next run.
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
18+
"""Probe v2 for the Windows wheel shutdown crash.
19+
20+
Probe v1 (synthetic ``python -c`` teardowns) never reproduced the 0xC0000409
21+
process-exit crash, so it needs the real pytest suite's accumulated state. This
22+
harness runs the actual test suite as repeated subprocesses and tallies the
23+
crash exits, across scenarios that isolate the two prime suspects:
24+
25+
* faulthandler: the real CI runs ``pytest -o faulthandler_timeout=600``, which
26+
spawns a watchdog thread (dump_traceback_later). Compare the real command vs.
27+
the same suite with faulthandler fully disabled — if the crash only happens
28+
with the watchdog, the fix is to drop the timeout.
29+
* subsystem: raster/GDAL-heavy tests vs. everything else, to localize which
30+
teardown leaves the bad native state (or show it is cumulative/interaction).
31+
32+
Usage: win_teardown_probe_v2.py <tests_dir>
33+
Always exits 0 — the signal is the printed tally.
34+
"""
35+
36+
import subprocess
37+
import sys
38+
39+
FASTFAIL = 3221226505 # 0xC0000409, Windows __fastfail / STATUS_STACK_BUFFER_OVERRUN
40+
41+
TESTS = sys.argv[1]
42+
43+
# Each scenario is (label, extra pytest args, reps). `-q -p no:cacheprovider`
44+
# keeps output small; the real CI adds `-o faulthandler_timeout=600`.
45+
SCENARIOS = [
46+
# Faithful replica of the CI command (faulthandler watchdog thread present).
47+
("full_faulthandler", ["-o", "faulthandler_timeout=600"], 12),
48+
# Same suite, faulthandler plugin fully disabled (no watchdog, no handlers).
49+
("full_no_faulthandler", ["-p", "no:faulthandler"], 12),
50+
# Localize: raster/GDAL-heavy vs. the rest.
51+
("raster_only", ["-k", "raster or rst or Raster or gdal or GDAL or tiff or tif"], 10),
52+
("no_raster", ["-k", "not (raster or rst or Raster or gdal or GDAL or tiff or tif)"], 10),
53+
]
54+
55+
56+
def base_cmd(extra):
57+
return [sys.executable, "-m", "pytest", TESTS, "-q", "-p", "no:cacheprovider"] + extra
58+
59+
60+
def run_scenario(label, extra, reps):
61+
codes = {}
62+
first_crash_tail = None
63+
passed_example = None
64+
for _ in range(reps):
65+
proc = subprocess.run(base_cmd(extra), capture_output=True, text=True)
66+
rc = proc.returncode
67+
codes[rc] = codes.get(rc, 0) + 1
68+
# Capture the pytest summary line once, to confirm the suite actually ran.
69+
if passed_example is None:
70+
for line in reversed(proc.stdout.splitlines()):
71+
if "passed" in line or "no tests ran" in line:
72+
passed_example = line.strip()
73+
break
74+
if rc != 0 and first_crash_tail is None:
75+
first_crash_tail = (proc.stdout[-600:], proc.stderr[-1200:])
76+
crashes = sum(n for rc, n in codes.items() if rc != 0)
77+
print(f"[{label}] {crashes}/{reps} nonzero exits codes={codes}")
78+
if passed_example:
79+
print(f" (suite ran: {passed_example})")
80+
if first_crash_tail:
81+
out, err = first_crash_tail
82+
print(f" first-crash stdout tail:\n{out}")
83+
print(f" first-crash stderr tail:\n{err}")
84+
return label, crashes, reps
85+
86+
87+
def main():
88+
print(f"python: {sys.version}")
89+
print(f"tests dir: {TESTS}\n")
90+
results = [run_scenario(*s) for s in SCENARIOS]
91+
print("\n=== SUMMARY (fastfail=0xC0000409) ===")
92+
for label, crashes, reps in results:
93+
print(f" {label:22s} {crashes}/{reps}")
94+
sys.exit(0)
95+
96+
97+
if __name__ == "__main__":
98+
main()

0 commit comments

Comments
 (0)