Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 11 additions & 20 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,13 @@ jobs:
needs: [lint, changes]
if: needs.changes.outputs.python == 'true' || github.event_name == 'push'
runs-on: ${{ matrix.os }}
env:
# setup-python on macOS upgrades certifi via pip; pip's HTTP cache
# occasionally fails to deserialize across runner image versions and
# surfaces as a noisy "Cache entry deserialization failed" annotation.
# Disabling pip's cache eliminates the warning at zero cost (we use uv
# for actual package installs).
PIP_NO_CACHE_DIR: "1"
strategy:
matrix:
os: [ubuntu-latest]
Expand Down Expand Up @@ -247,26 +254,10 @@ jobs:
args: --release --features extension-module,postgres,redis,native-async,workflows

- name: Run Python test suite
run: |
set +e
uv run python -m pytest tests/python/ -v --junitxml=test-results.xml
PYTEST_EXIT=$?
if [ $PYTEST_EXIT -eq 0 ]; then exit 0; fi
# SIGABRT (134) during interpreter shutdown is a known PyO3 issue;
# check junit XML to confirm all tests actually passed.
if [ $PYTEST_EXIT -eq 134 ] && [ -f test-results.xml ]; then
FAILURES=$(python -c "
import xml.etree.ElementTree as ET
r = ET.parse('test-results.xml').getroot()
print(int(r.get('failures',0)) + int(r.get('errors',0)))
")
if [ "$FAILURES" = "0" ]; then
echo "::warning::Tests passed but process crashed during cleanup (known PyO3 issue)"
exit 0
fi
fi
exit $PYTEST_EXIT
shell: bash
# The pytest_unconfigure hook in tests/python/conftest.py calls
# ``os._exit(0)`` on a clean run to bypass CPython finalization and
# avoid the PyO3 daemon-thread SIGABRT we used to paper over here.
run: uv run python -m pytest tests/python/ -v --junitxml=test-results.xml

ci-status:
name: CI status
Expand Down
33 changes: 33 additions & 0 deletions tests/python/conftest.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Shared fixtures for taskito tests."""

import os
import sys
import threading
from collections.abc import Generator
from pathlib import Path
Expand All @@ -24,3 +26,34 @@ def run_worker(queue: Queue) -> Generator[threading.Thread]:
yield thread
queue._inner.request_shutdown()
thread.join(timeout=5)


_PYTEST_EXIT_STATUS: int = 0


def pytest_sessionfinish(session: pytest.Session, exitstatus: int) -> None:
"""Capture the exit status for ``pytest_unconfigure`` to act on."""
global _PYTEST_EXIT_STATUS
_PYTEST_EXIT_STATUS = int(exitstatus)


def pytest_unconfigure(config: pytest.Config) -> None:
"""Bypass CPython's interpreter finalization on a clean exit.

Many tests leave PyO3-backed daemon threads (heartbeat, async executor,
webhook delivery, distributed-lock extender) running at process end.
During ``Py_Finalize`` those threads may try to (re)acquire the GIL after
it has been torn down, producing ``FATAL: exception not rethrown`` and a
SIGABRT — even though every test passed. ``os._exit`` skips finalization
entirely after the terminal summary and junit XML are already written,
eliminating the spurious crash.

``pytest_unconfigure`` fires after every other hook (terminal summary,
junitxml plugin, etc.), so output and side effects are preserved. We
skip the bypass on failure so pytest's normal traceback machinery still
runs.
"""
if _PYTEST_EXIT_STATUS == 0:
sys.stdout.flush()
sys.stderr.flush()
os._exit(0)
Loading