mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-09 03:18:44 +00:00
fix(e2e): run case init() inside try so partial-init failures tear down
run_case called case.init() outside the try/finally that runs teardown(), so a case that registers cleanups progressively (create team, then user, then key) and then fails partway through init() would leak the already-created entities on the long-lived shared proxy. Move init() inside the try so teardown always runs. Add a regression test that registers a cleanup then raises mid-init and asserts the resource is still released.
This commit is contained in:
parent
0c57b54d77
commit
87d1f5b41c
2 changed files with 54 additions and 4 deletions
|
|
@ -22,7 +22,8 @@ class E2ECase(Protocol):
|
|||
"""A stateful e2e check run against a long-lived proxy.
|
||||
|
||||
init() acquires resources, run() exercises behaviour and asserts, teardown()
|
||||
releases everything init() created. teardown() must run even if run() raises.
|
||||
releases everything init() created. teardown() must run even if init() fails
|
||||
partway or run() raises.
|
||||
"""
|
||||
|
||||
def init(self) -> None: ...
|
||||
|
|
@ -35,11 +36,14 @@ class E2ECase(Protocol):
|
|||
def run_case(case: E2ECase) -> None:
|
||||
"""Drive a case through its lifecycle: init -> run -> teardown.
|
||||
|
||||
teardown always runs, even if run() raises (or skips), so resources the case
|
||||
created on the long-lived proxy are released.
|
||||
teardown always runs - even when init() fails partway or run() raises (or
|
||||
skips) - so resources the case already registered on the long-lived proxy are
|
||||
released. init() is inside the try because cases register cleanups
|
||||
progressively (e.g. create team, then user, then key), and a failure after
|
||||
the first creation must still release what came before.
|
||||
"""
|
||||
case.init()
|
||||
try:
|
||||
case.init()
|
||||
case.run()
|
||||
finally:
|
||||
case.teardown()
|
||||
|
|
|
|||
46
tests/e2e/test_lifecycle.py
Normal file
46
tests/e2e/test_lifecycle.py
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
"""Unit coverage for the lifecycle harness (lifecycle.run_case).
|
||||
|
||||
Cases register cleanups progressively during init() (create team, then user, then
|
||||
key), so a failure partway through init() must still release whatever was already
|
||||
created on the long-lived shared proxy. This guards that contract.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Callable, List
|
||||
|
||||
import pytest
|
||||
|
||||
from lifecycle import run_case
|
||||
|
||||
|
||||
@dataclass
|
||||
class _PartialInitCase:
|
||||
"""init() registers a cleanup, then raises before finishing - mirroring a real
|
||||
case that creates a resource, registers its delete, then fails on the next
|
||||
step."""
|
||||
|
||||
released: List[str] = field(default_factory=list)
|
||||
_undo: List[Callable[[], None]] = field(default_factory=list)
|
||||
|
||||
def init(self) -> None:
|
||||
self._undo.append(lambda: self.released.append("first"))
|
||||
raise RuntimeError("init failed after registering the first resource")
|
||||
|
||||
def run(self) -> None:
|
||||
raise AssertionError("run() must not execute when init() failed")
|
||||
|
||||
def teardown(self) -> None:
|
||||
for undo in reversed(self._undo):
|
||||
undo()
|
||||
|
||||
|
||||
def test_run_case_releases_resources_when_init_fails_partway() -> None:
|
||||
case = _PartialInitCase()
|
||||
|
||||
with pytest.raises(RuntimeError, match="init failed"):
|
||||
run_case(case)
|
||||
|
||||
assert case.released == ["first"], (
|
||||
"a resource registered before init() failed must still be released, or it "
|
||||
"leaks on the long-lived shared proxy"
|
||||
)
|
||||
Loading…
Add table
Reference in a new issue