Skip to content

Commit 84bc3a8

Browse files
Copilotbashandbone
authored andcommitted
fix: prevent Python 3.12 CI hang by excluding real_providers tests and hardening timeouts (#337)
* fix: prevent Python 3.12 CI hang with timeout coverage for fixtures - Set timeout_func_only=false in pyproject.toml so pytest timeout covers fixture setup/teardown (previously only test body was covered) - Add asyncio.wait_for(timeout=300) in indexed_test_project fixture to catch hangs during model loading or embedding generation - Add timeout-minutes: 20 to CI test job as safety net - Use signal timeout method in CI for all tests (can interrupt native code)" Agent-Logs-Url: https://github.com/knitli/codeweaver/sessions/f35eacec-3469-4488-98b4-b20056964e04 * fix: move asyncio import to top level, revert accidental mise.toml change Agent-Logs-Url: https://github.com/knitli/codeweaver/sessions/f35eacec-3469-4488-98b4-b20056964e04 * fix: exclude real_providers from CI markers, make signal timeout OS-conditional - Add `not real_providers` to CI test markers — these heavy tests with full model loading/indexing should only run in nightly builds, not on every push. This was the actual root cause of the 3.12 hang. - Make `--timeout-method=signal` conditional on $RUNNER_OS to avoid breaking Windows weekly tests (signal not supported on Windows). - Revert accidental mise.toml hk version change." Agent-Logs-Url: https://github.com/knitli/codeweaver/sessions/90b7c085-c5bb-419a-bb95-a2a4c8abb04d --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
1 parent 5df45eb commit 84bc3a8

5 files changed

Lines changed: 42 additions & 15 deletions

File tree

.github/workflows/_reusable-test.yml

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ jobs:
7171
test:
7272
name: Test Python ${{ matrix.python-version }}${{ matrix.experimental && ' (experimental)' || '' }}
7373
runs-on: ${{ inputs.runner-version }}
74+
timeout-minutes: 20
7475
continue-on-error: ${{ matrix.experimental || false }}
7576
outputs:
7677
benchmark-available: ${{ steps.benchmark-compare.outputs.benchmark-available }}
@@ -201,17 +202,19 @@ jobs:
201202
MISE_PYTHON_VERSION: ${{ steps.setup-mise.outputs.MISE_PYTHON_VERSION }}
202203
TEST_MARKERS: ${{ inputs.test-markers }}
203204
run: |
204-
# Real-provider tests: bump timeout to 15min per test and switch
205-
# the timeout method to `signal` so SIGALRM can interrupt
206-
# blocking C code (ONNX runtime inference). The default `thread`
207-
# method in pyproject.toml can't kill stuck native code, so a
208-
# genuinely hung embedding call would run until the 6h GHA
209-
# job limit. `signal` has asyncio interaction caveats but
210-
# they're acceptable for the integration suite.
205+
# On Linux/macOS, use `signal` timeout method so SIGALRM can
206+
# interrupt blocking C code (ONNX runtime, model loading).
207+
# On Windows, `signal` is not supported by pytest-timeout —
208+
# fall back to the default `thread` method from pyproject.toml.
209+
TIMEOUT_ARGS=""
210+
if [[ "$RUNNER_OS" != "Windows" ]]; then
211+
TIMEOUT_ARGS="--timeout-method=signal"
212+
fi
213+
211214
if [[ "${TEST_MARKERS}" == *"real_providers"* ]]; then
212-
mise run test-cov -m "${TEST_MARKERS}" --timeout=900 --timeout-method=signal
215+
mise run test-cov -m "${TEST_MARKERS}" --timeout=900 ${TIMEOUT_ARGS}
213216
else
214-
mise run test-cov -m "${TEST_MARKERS}"
217+
mise run test-cov -m "${TEST_MARKERS}" ${TIMEOUT_ARGS}
215218
fi
216219
- name: Upload coverage to Codecov
217220
if: ${{ inputs.upload-coverage && matrix.python-version == '3.12' && !inputs.benchmark-tests }}

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ jobs:
4949
QDRANT__SERVICE__API_KEY: ${{ secrets.QDRANT__SERVICE__API_KEY }}
5050
VOYAGE_API_KEY: ${{ secrets.VOYAGE_API_KEY }}
5151
with:
52-
test-markers: "not docker and not qdrant and not dev_only and not skip_ci and not network and not external_api and not flaky"
52+
test-markers: "not docker and not qdrant and not dev_only and not skip_ci and not network and not external_api and not flaky and not real_providers"
5353
upload-coverage: true
5454
run-quality-checks: true
5555

pyproject.toml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -673,8 +673,12 @@ testpaths = ["tests"]
673673
asyncio_mode = "auto"
674674
timeout = "600"
675675
timeout_method = "thread"
676-
# Override timeout for specific markers
677-
timeout_func_only = true # Only apply timeout to test functions, not setup/teardown
676+
# Apply timeout to entire test lifecycle (setup + call + teardown) so that
677+
# fixture hangs (e.g. model loading, indexing) are caught instead of running
678+
# until the GHA job limit. Previously `timeout_func_only = true` left
679+
# fixtures uncovered, causing Python 3.12 CI to hang indefinitely in the
680+
# `indexed_test_project` fixture during `index_project()`.
681+
timeout_func_only = false
678682

679683
[tool.ty.environment]
680684
python-version = "3.12"

scripts/gen_api_docs.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,16 @@
5454
"codeweaver.cli.*.*",
5555
"codeweaver.data.*",
5656
"codeweaver.semantic.data.*",
57-
"",
57+
]
58+
59+
FORCE_INCLUDE_PATTERNS: list[str] = [
60+
# If any module matches both EXCLUDE_PATTERNS and FORCE_INCLUDE_PATTERNS,
61+
# it will be included. This is for modules that are private but still
62+
# worth documenting, or that would otherwise be excluded by an overly
63+
# broad pattern.
64+
"codeweaver.server.agent_api.search.__init__",
65+
"codeweaver.providers.embedding.capabilities.base",
66+
"codeweaver.providers.reranking.capabilities.base",
5867
]
5968

6069

@@ -185,7 +194,7 @@ def _generate_module_docs(loader: GriffeLoader, mod_name: str, output_path: Path
185194

186195
def _is_excluded(dotted_name: str) -> bool:
187196
"""Return True if `dotted_name` matches any `EXCLUDE_PATTERNS` glob."""
188-
return any(fnmatch.fnmatchcase(dotted_name, pat) for pat in EXCLUDE_PATTERNS)
197+
return any(fnmatch.fnmatchcase(dotted_name, pat) for pat in EXCLUDE_PATTERNS) and not any(fnmatch.fnmatchcase(dotted_name, pat) for pat in FORCE_INCLUDE_PATTERNS)
189198

190199

191200
def _iter_modules(src_path: Path) -> Iterator[tuple[str, Path]]:

tests/integration/conftest.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from __future__ import annotations
1010

11+
import asyncio
1112
import contextlib
1213
import logging
1314
import os
@@ -1212,7 +1213,17 @@ async def get_test_settings() -> CodeWeaverSettingsType:
12121213
# Ensure it's using the correct project path
12131214
indexer._project_path = project_path
12141215

1215-
await indexer.index_project(force_reindex=True)
1216+
# Use asyncio.wait_for to prevent indefinite hangs during indexing.
1217+
# On Python 3.12, model loading or embedding generation can hang in
1218+
# native code; without this guard the fixture would block forever
1219+
# since pytest timeout_func_only previously excluded fixtures.
1220+
try:
1221+
await asyncio.wait_for(indexer.index_project(force_reindex=True), timeout=300)
1222+
except TimeoutError:
1223+
pytest.fail(
1224+
"indexed_test_project fixture timed out after 300s during index_project(). "
1225+
"This typically indicates model loading or embedding generation hung in native code."
1226+
)
12161227

12171228
yield project_path
12181229

0 commit comments

Comments
 (0)