障害を注入してリソース漏れを見つける
目標
正常・二重終了・失敗の経路と、アプリのlifespanの開始・後始末を観測します。
なぜ重要なのか
リクエストが成功したときは接続を閉じるコードがありましたが、例外が発生すると開いた接続が残りました。成功レスポンスだけを確認するテストでは、このリークは表に出ませんでした。ライフサイクルのテストは、戻り値よりも、リソースがいつ開き、いつ閉じたかに集中する必要があります。
ステップ
/root/work/test-lifespan-cleanup-lab/test_service.pyで、提供されたservice.pyの次の公開契約をテストしてください: new_resource()は{open:False, events:[]}の新しい辞書で、呼び出し同士でeventsを共有しません。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。
最初に1回だけ準備してください。既存のファイルは上書きしません。
mkdir -p /root/work/test-lifespan-cleanup-lab
test -e /root/work/test-lifespan-cleanup-lab/service.py || cp /opt/fixtures/ten_labs/test-lifespan-cleanup-lab/service.py /root/work/test-lifespan-cleanup-lab/service.py
test -e /root/work/test-lifespan-cleanup-lab/test_service.py || cp /opt/fixtures/ten_labs/test-lifespan-cleanup-lab/test_service.py /root/work/test-lifespan-cleanup-lab/test_service.py
cd /root/work/test-lifespan-cleanup-lab
-
/root/work/test-lifespan-cleanup-lab/test_service.pyで、提供されたservice.pyの次の公開契約をテストしてください: start(resource)は、すでに開いていればValueError、そうでなければopen=Trueに変えてeventsに'open'を追加します。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。 -
/root/work/test-lifespan-cleanup-lab/test_service.pyで、提供されたservice.pyの次の公開契約をテストしてください: stop(resource)は、開いているときだけopen=Falseに変え、'close'をeventsに追加します。すでに閉じていればそのままにします。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。 -
/root/work/test-lifespan-cleanup-lab/test_service.pyで、提供されたservice.pyの次の公開契約をテストしてください: read(resource)は、閉じていればRuntimeError、開いていれば{ready:True}を返します。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。 -
/root/work/test-lifespan-cleanup-lab/test_service.pyで、提供されたservice.pyの次の公開契約をテストしてください: scope(resource)はcontextmanagerです。入るときにstartし、ブロックの中にはresourceをyieldし、ブロックの成功・失敗のどちらでもstopで閉じます。ブロックの例外は伝播させます。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。 -
/root/work/test-lifespan-cleanup-lab/test_service.pyで、提供されたservice.pyの次の公開契約をテストしてください: lifespan_for(resource)は、asynccontextmanagerの関数lifespan(app)を返します。scope(resource)の中でapp.state.resourceを設定してyieldします。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。 -
/root/work/test-lifespan-cleanup-lab/test_service.pyで、提供されたservice.pyの次の公開契約をテストしてください: create_app(resource)は、lifespan_forを使います。GET /readyは、app.state.resourceをreadした結果を返します。contextの終了時にリソースを閉じる必要があります。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。 -
/root/work/test-lifespan-cleanup-lab/test_service.pyで、提供されたservice.pyの次の公開契約をテストしてください: exercise(resource, fail=False)は、with TestClient(create_app(resource))の中でGET /readyを呼び出します。fail=Trueならその中でRuntimeErrorを送出し、そうでなければレスポンスのJSONを返します。どちらの場合もリソースが閉じている必要があります。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。
参考
- インターネットやパッケージのインストールなしで、既存のlab-dev環境で行います。
- 各ステップは、45秒の採点バジェットの範囲内で実行されます。実際のsleepやネットワーク呼び出しを追加しないでください。
- 提出したテストは、別の一時フォルダーで、正常な実装と欠陥のある実装に対して実行されます。正常な実装では、実際に実行したテストがすべて成功し、欠陥のある実装では、テスト本文が失敗する必要があります。収集エラー、実行0件、すべてスキップ、強制終了は成功ではありません。pytestの基本機能と提供されたライブラリだけを使ってください。
- FastAPI公式ドキュメント・pytest公式ドキュメント・Python sqlite3
- 制限: 学習用のリソース辞書は、実際のDB接続プールの代わりとなる観測装置です。本番では、部分的な初期化の失敗、接続プールの並行性、キャンセル処理と終了のタイムアウトも設計する必要があります。イベントの文字列をレポートに書くのではなく、学習者のコードが実行して変えたオブジェクトの状態を検査します。提供された実装は読んでもかまいませんが、採点は別のコピーを使用します。ソースの文言の検査やファイルの修正で欠陥を回避せず、公開インターフェースの実行結果を検査してください。
リソースの状態を独立させる(テスト)
/root/work/test-lifespan-cleanup-lab/test_service.pyで、提供されたservice.pyの次の公開契約をテストしてください: new_resource()は{open:False, events:[]}の新しい辞書で、呼び出し同士でeventsを共有しません。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。
最初に1回だけ準備してください。既存のファイルは上書きしません。
mkdir -p /root/work/test-lifespan-cleanup-lab
test -e /root/work/test-lifespan-cleanup-lab/service.py || cp /opt/fixtures/ten_labs/test-lifespan-cleanup-lab/service.py /root/work/test-lifespan-cleanup-lab/service.py
test -e /root/work/test-lifespan-cleanup-lab/test_service.py || cp /opt/fixtures/ten_labs/test-lifespan-cleanup-lab/test_service.py /root/work/test-lifespan-cleanup-lab/test_service.py
cd /root/work/test-lifespan-cleanup-lab
変更可能なリストを、グローバルやデフォルト引数で共有しません。実装ファイルは修正しません。pytest.raisesで期待する例外を確認し、正常な結果には具体的な期待値をassertしてください。
保存後、bash /opt/lab/checks/test-lifespan-cleanup-lab/01-contract.shで確認してください。
二重の開始を拒否する(テスト)
/root/work/test-lifespan-cleanup-lab/test_service.pyで、提供されたservice.pyの次の公開契約をテストしてください: start(resource)は、すでに開いていればValueError、そうでなければopen=Trueに変えてeventsに'open'を追加します。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。
2回開始してリソースを1つ見失う動作を拒否します。実装ファイルは修正しません。pytest.raisesで期待する例外を確認し、正常な結果には具体的な期待値をassertしてください。
保存後、bash /opt/lab/checks/test-lifespan-cleanup-lab/02-contract.shで確認してください。
終了を冪等にする(テスト)
/root/work/test-lifespan-cleanup-lab/test_service.pyで、提供されたservice.pyの次の公開契約をテストしてください: stop(resource)は、開いているときだけopen=Falseに変え、'close'をeventsに追加します。すでに閉じていればそのままにします。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。
複数の後始末の経路が重なっても、重複したcloseイベントが出てはいけません。実装ファイルは修正しません。pytest.raisesで期待する例外を確認し、正常な結果には具体的な期待値をassertしてください。
保存後、bash /opt/lab/checks/test-lifespan-cleanup-lab/03-contract.shで確認してください。
閉じたリソースの使用を防ぐ(テスト)
/root/work/test-lifespan-cleanup-lab/test_service.pyで、提供されたservice.pyの次の公開契約をテストしてください: read(resource)は、閉じていればRuntimeError、開いていれば{ready:True}を返します。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。
準備ができた状態と、オブジェクトが存在することは別です。オブジェクトがあっても、閉じていることがあります。実装ファイルは修正しません。pytest.raisesで期待する例外を確認し、正常な結果には具体的な期待値をassertしてください。
保存後、bash /opt/lab/checks/test-lifespan-cleanup-lab/04-contract.shで確認してください。
例外の経路にfinallyを置く(テスト)
/root/work/test-lifespan-cleanup-lab/test_service.pyで、提供されたservice.pyの次の公開契約をテストしてください: scope(resource)はcontextmanagerです。入るときにstartし、ブロックの中にはresourceをyieldし、ブロックの成功・失敗のどちらでもstopで閉じます。ブロックの例外は伝播させます。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。
yieldの後にだけcloseを書くと、例外が発生した場合にその行へ到達できません。実装ファイルは修正しません。pytest.raisesで期待する例外を確認し、正常な結果には具体的な期待値をassertしてください。
保存後、bash /opt/lab/checks/test-lifespan-cleanup-lab/05-contract.shで確認してください。
アプリのライフサイクルとリソースをつなぐ(テスト)
/root/work/test-lifespan-cleanup-lab/test_service.pyで、提供されたservice.pyの次の公開契約をテストしてください: lifespan_for(resource)は、asynccontextmanagerの関数lifespan(app)を返します。scope(resource)の中でapp.state.resourceを設定してyieldします。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。
lifespan関数自体を呼び出すのではなく、FastAPIのコンストラクターに渡します。実装ファイルは修正しません。pytest.raisesで期待する例外を確認し、正常な結果には具体的な期待値をassertしてください。
保存後、bash /opt/lab/checks/test-lifespan-cleanup-lab/06-contract.shで確認してください。
準備状態を実際のリクエストで読み取る(テスト)
/root/work/test-lifespan-cleanup-lab/test_service.pyで、提供されたservice.pyの次の公開契約をテストしてください: create_app(resource)は、lifespan_forを使います。GET /readyは、app.state.resourceをreadした結果を返します。contextの終了時にリソースを閉じる必要があります。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。
with TestClientを使って初めて、lifespanの開始と終了の両方が実行されます。実装ファイルは修正しません。pytest.raisesで期待する例外を確認し、正常な結果には具体的な期待値をassertしてください。
保存後、bash /opt/lab/checks/test-lifespan-cleanup-lab/07-contract.shで確認してください。
リクエストの後の失敗も後始末する(テスト)
/root/work/test-lifespan-cleanup-lab/test_service.pyで、提供されたservice.pyの次の公開契約をテストしてください: exercise(resource, fail=False)は、with TestClient(create_app(resource))の中でGET /readyを呼び出します。fail=Trueならその中でRuntimeErrorを送出し、そうでなければレスポンスのJSONを返します。どちらの場合もリソースが閉じている必要があります。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。
正常な経路と例外の経路を、同じ後始末の構造にまとめれば、抜けている終了経路を減らせます。実装ファイルは修正しません。pytest.raisesで期待する例外を確認し、正常な結果には具体的な期待値をassertしてください。
保存後、bash /opt/lab/checks/test-lifespan-cleanup-lab/08-contract.shで確認してください。