mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-05-26 18:06:00 +00:00
e344be8d94
* feat(tests): add Blockbuster runtime gate for event-loop blocking IO Adds a strict runtime gate that fails CI when sync blocking IO calls run on the asyncio event loop thread through DeerFlow business code. Components: - backend/tests/support/detectors/blocking_io_runtime.py — Blockbuster context scoped to `app.*` and `deerflow.*` so test infrastructure, pytest internals, and third-party libraries stay silent. - backend/tests/blocking_io/conftest.py — pytest_runtest_protocol hookwrapper that wraps every item (setup + call + teardown) with the strict context. Respects `@pytest.mark.allow_blocking_io` opt-out. - backend/tests/blocking_io/test_skills_load.py — regression anchor for the #1917 fix (asyncio.to_thread offload around LocalSkillStorage.load_skills). - backend/tests/blocking_io/test_sqlite_lifespan.py — regression anchor for the #1912 fix (asyncio.to_thread offload around ensure_sqlite_parent_dir). - backend/tests/blocking_io/test_gate_smoke.py — meta-test asserting the gate actually catches unoffloaded blocking IO and that the `@pytest.mark.allow_blocking_io` opt-out works. - backend/Makefile — `make test-blocking-io` target. - .github/workflows/backend-blocking-io-tests.yml — hard-fail PR gate on ubuntu-latest. Windows matrix deferred to follow-up. Dependencies: - blockbuster>=1.5.26,<1.6 added to dev group. Coverage boundary (called out in PR body): the gate only catches blocking IO on code paths the test suite actually exercises. Static AST inventory (separate, informational) is the complementary coverage tool. Three blind spot categories — untested paths, mocked-away paths, env-mismatched paths — are documented in the PR description. Findings surfaced while authoring this PR: - resolve_sqlite_conn_str in runtime/store/_sqlite_utils.py:19 does sync Path.resolve() -> os.path.abspath on the lifespan loop thread, ahead of the #1912 fix. Not addressed here; tracked as follow-up. Tests: 4 passed locally (`make test-blocking-io`). Lint/format: clean (`ruff check` and `ruff format --check`). * fix(tests): scope Blockbuster gate to blocking-io suite * fix(tests): harden Blockbuster runtime gate * test(blocking-io): add project rule extension point * test(blocking-io): address review cleanup
56 lines
1.9 KiB
Python
56 lines
1.9 KiB
Python
"""Smoke test: the strict Blockbuster gate is wired up and actively catching.
|
|
|
|
Independent of any specific production code path, asserts that calling a
|
|
known blocking IO function directly from an `async def` (without an
|
|
`asyncio.to_thread` wrapper) raises `BlockingError`. If this test ever
|
|
stops raising, the gate machinery itself is broken — typical causes are
|
|
`scanned_modules` misconfiguration, accidental removal of the Blockbuster
|
|
dev dependency, or the conftest hookwrapper no longer firing.
|
|
|
|
This is the meta-test that protects every other test in this directory
|
|
from silent regressions (a green gate that no longer catches anything is
|
|
worse than no gate at all).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from blockbuster import BlockingError
|
|
from support.detectors.blocking_io_runtime import detect_blocking_io_strict
|
|
|
|
pytestmark = pytest.mark.asyncio
|
|
|
|
|
|
async def test_gate_catches_unoffloaded_blocking_io_in_deerflow_module(tmp_path: Path) -> None:
|
|
from deerflow.runtime.store._sqlite_utils import ensure_sqlite_parent_dir
|
|
|
|
db_file = tmp_path / "subdir" / "store.db"
|
|
|
|
with pytest.raises(BlockingError):
|
|
ensure_sqlite_parent_dir(str(db_file))
|
|
|
|
|
|
async def test_gate_restores_blockbuster_patches_after_exceptions() -> None:
|
|
original_stat = os.stat
|
|
|
|
with pytest.raises(RuntimeError, match="boom"):
|
|
with detect_blocking_io_strict():
|
|
raise RuntimeError("boom")
|
|
|
|
assert os.stat is original_stat
|
|
|
|
|
|
@pytest.mark.allow_blocking_io
|
|
async def test_allow_blocking_io_marker_opts_out_of_gate(tmp_path: Path) -> None:
|
|
"""Verify the @pytest.mark.allow_blocking_io opt-out actually disables the gate."""
|
|
from deerflow.runtime.store._sqlite_utils import ensure_sqlite_parent_dir
|
|
|
|
db_file = tmp_path / "subdir" / "store.db"
|
|
|
|
ensure_sqlite_parent_dir(str(db_file))
|
|
|
|
assert db_file.parent.exists()
|