From 2f0cb147fb50645962a4a29b06104c5a0e5d81ed Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 2 Jul 2026 23:41:24 +0000 Subject: [PATCH] =?UTF-8?q?fix(test):=20mutmut=20=E3=81=AE=20clean=20test?= =?UTF-8?q?=20=E5=A4=B1=E6=95=97=E3=82=92=E4=BF=AE=E6=AD=A3=EF=BC=88?= =?UTF-8?q?=E3=82=B0=E3=83=AD=E3=83=BC=E3=83=90=E3=83=AB=20event=20loop=20?= =?UTF-8?q?=E6=B1=9A=E6=9F=93=E3=81=AE=E9=99=A4=E5=8E=BB=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mutmut 3.x は同一プロセスで pytest スイートを複数回実行する(stats 収集 → clean test 検証 → ミュータント実行)。tests/security/test_ssrf_github.py の _run() だけがグローバル asyncio event loop を set/get していたため、 test_llm_clients.py の asyncio.run() が残したグローバル loop の壊れ状態が 2 周目に持ち越され、get_event_loop() が "RuntimeError: There is no current event loop" を送出。これで `make mutation-backend` が clean test 失敗(Failed to run clean test)で exit(1) していた。 _run() を他 11 個の _run と同じ「ローカル loop を作って閉じるだけ」の分離 パターンに統一し、反復実行に対して冪等にする。再発防止として .claude/rules/backend/test.md のアンチパターンに追記。 - プロダクトコード(app/)は無変更。テストの意図(不正 username を HTTP 発行前に弾く)も不変。 - 検証: フルスイートを同一プロセスで 2 回実行し、修正前=2 回目 1 failed → 修正後=両方 667 passed を確認。ruff clean。 Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01SsRavDhjAWMQby1g6gHVyR --- .claude/rules/backend/test.md | 1 + backend/tests/security/test_ssrf_github.py | 12 +++++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.claude/rules/backend/test.md b/.claude/rules/backend/test.md index 44b48d31..585e1665 100644 --- a/.claude/rules/backend/test.md +++ b/.claude/rules/backend/test.md @@ -50,3 +50,4 @@ pytest は標準 SQLite + `Base.metadata.create_all` で動くため、**本番 - `try / except Exception: pass` をテストコード内で使う(失敗を隠す) - `time.sleep` での同期待ち(フレーキーになる。`AsyncMock` / `monkeypatch` を使う) - 過剰モック: SQLAlchemy セッション全体をモックする等。実 DB セッションを使うこと +- **テストで `asyncio.set_event_loop` / `get_event_loop`(グローバル event loop)を触る**: mutmut 3.x は同一プロセスでスイートを複数回実行するため、グローバル loop 状態が実行間に漏れて clean test が `RuntimeError: There is no current event loop` で落ちる(`make mutation-backend` が `Failed to run clean test` で停止した実績)。async は `loop = asyncio.new_event_loop(); try: loop.run_until_complete(...); finally: loop.close()` の分離パターンで実行する(グローバル loop を設定・復元しない) diff --git a/backend/tests/security/test_ssrf_github.py b/backend/tests/security/test_ssrf_github.py index 750b928b..c6347343 100644 --- a/backend/tests/security/test_ssrf_github.py +++ b/backend/tests/security/test_ssrf_github.py @@ -32,16 +32,18 @@ def _run(coro): - """既存テストの event loop 前提を壊さず非同期関数を実行する。""" - original = asyncio.get_event_loop_policy().get_event_loop() + """async 関数を専用 event loop で同期実行する(グローバル loop を触らない)。 + + mutmut 3.x は同一プロセスで pytest スイートを複数回実行するため、 + ``asyncio.set_event_loop`` / ``get_event_loop`` でグローバル loop 状態に + 触れると実行間に汚染が漏れて clean test が落ちる。ローカル loop を作って + 閉じるだけの分離パターンにして反復実行に対して冪等にする。 + """ loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) try: return loop.run_until_complete(coro) finally: loop.close() - # 一時 loop を閉じたら元の loop へ戻す(新規 loop を作って放置すると leak する)。 - asyncio.set_event_loop(original) @pytest.mark.parametrize("bad", _MALICIOUS_OWNERS)