mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-09 22:31:41 +00:00
`pytest.raises(Exception)` with no `match=` passes on any error that broad. A TypeError from a refactor, a botched fixture, an import that moved: all of them read as the rejection the test claims to police, so the test goes green for the wrong reason and stays green after the behaviour it guards is gone. PT011 closes that gap for the 317 sites B017 could not reach, because B017 only fires on a single-statement body with no `as e` binding. Each pattern here is the message the code actually raised, recorded by running the sites under a plugin that logged the concrete type and text per call site, so the assertions describe observed behaviour rather than a guess. Where a site raises more than one message across its parametrize cases, the pattern is an alternation of what was seen; where the exception carries an empty `str()` and puts the text on `.message`, the site keeps a narrow `noqa` with the reason. PT014 removes four parametrize cases that were listed twice. The duplicate re-runs an assertion that already passed, and it usually marks a case someone meant to vary and forgot to edit.
318 lines
9.9 KiB
Python
318 lines
9.9 KiB
Python
"""
|
|
Tests for the e2b code execution sandbox primitive.
|
|
|
|
Unit tests inject a fake async HTTP client (dependency injection, no
|
|
monkeypatching) and assert request shapes and result mapping. Real-network
|
|
integration tests live in tests/integration/sandbox/test_e2b_sandbox.py.
|
|
"""
|
|
|
|
import json
|
|
|
|
import httpx
|
|
import pytest
|
|
|
|
import litellm
|
|
from litellm.llms.base_llm.sandbox.transformation import ContainerHandle
|
|
from litellm.llms.e2b.sandbox.transformation import (
|
|
MAX_OUTPUT_BYTES,
|
|
E2BSandboxConfig,
|
|
)
|
|
|
|
|
|
class FakeResponse:
|
|
def __init__(self, *, json_data=None, lines=None, status_code=200):
|
|
self._json = json_data
|
|
self._lines = lines or []
|
|
self.status_code = status_code
|
|
|
|
def json(self):
|
|
return self._json
|
|
|
|
async def aiter_lines(self):
|
|
for line in self._lines:
|
|
yield line
|
|
|
|
|
|
class FakeHTTPClient:
|
|
"""Records outbound requests and returns canned responses keyed by URL."""
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
create_json=None,
|
|
execute_lines=None,
|
|
delete_status=204,
|
|
execute_raises=None,
|
|
):
|
|
self.create_json = create_json or {
|
|
"sandboxID": "sbx_123",
|
|
"domain": "e2b.app",
|
|
"envdAccessToken": "tok_abc",
|
|
}
|
|
self.execute_lines = execute_lines or []
|
|
self.delete_status = delete_status
|
|
self.execute_raises = execute_raises
|
|
self.calls = []
|
|
|
|
async def post(self, url, headers=None, json=None, stream=False, **kwargs):
|
|
self.calls.append(("POST", url, headers, json))
|
|
if url.endswith("/sandboxes"):
|
|
return FakeResponse(json_data=self.create_json)
|
|
if url.endswith("/execute"):
|
|
if self.execute_raises is not None:
|
|
raise self.execute_raises
|
|
return FakeResponse(lines=self.execute_lines)
|
|
raise AssertionError(f"unexpected POST {url}")
|
|
|
|
async def delete(self, url, headers=None, **kwargs):
|
|
self.calls.append(("DELETE", url, headers, None))
|
|
if not (200 <= self.delete_status < 300):
|
|
raise httpx.HTTPStatusError(
|
|
f"status {self.delete_status}",
|
|
request=httpx.Request("DELETE", url),
|
|
response=httpx.Response(self.delete_status),
|
|
)
|
|
return FakeResponse(status_code=self.delete_status)
|
|
|
|
|
|
# ---------- pure parser ----------
|
|
|
|
|
|
def test_parse_lines_stdout_and_count():
|
|
lines = [
|
|
json.dumps({"type": "stdout", "text": "6\n", "timestamp": 1}),
|
|
json.dumps({"type": "number_of_executions", "execution_count": 1}),
|
|
]
|
|
result = E2BSandboxConfig._parse_lines(lines)
|
|
assert result.stdout == "6\n"
|
|
assert result.execution_count == 1
|
|
assert result.error is None
|
|
|
|
|
|
def test_parse_lines_error_surfaces_name_and_traceback():
|
|
lines = [
|
|
json.dumps(
|
|
{
|
|
"type": "error",
|
|
"name": "ZeroDivisionError",
|
|
"value": "division by zero",
|
|
"traceback": "Traceback (most recent call last): ...",
|
|
}
|
|
)
|
|
]
|
|
result = E2BSandboxConfig._parse_lines(lines)
|
|
assert result.error["name"] == "ZeroDivisionError"
|
|
assert "Traceback" in result.error["traceback"]
|
|
|
|
|
|
def test_parse_lines_result_carries_png():
|
|
lines = [
|
|
json.dumps({"type": "result", "png": "BASE64DATA", "is_main_result": True})
|
|
]
|
|
result = E2BSandboxConfig._parse_lines(lines)
|
|
assert result.results and result.results[0]["png"] == "BASE64DATA"
|
|
assert "type" not in result.results[0]
|
|
|
|
|
|
# ---------- request shapes ----------
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_template_flows_into_create_request_as_templateID():
|
|
client = FakeHTTPClient()
|
|
cfg = E2BSandboxConfig()
|
|
handle = await cfg.acreate_sandbox(
|
|
template="my-custom-template", api_key="e2b_key", client=client
|
|
)
|
|
|
|
method, url, headers, body = client.calls[0]
|
|
assert method == "POST"
|
|
assert url.endswith("/sandboxes")
|
|
assert body["templateID"] == "my-custom-template" # not "template"
|
|
assert body["secure"] is True
|
|
assert headers["X-API-Key"] == "e2b_key"
|
|
assert handle.id == "sbx_123"
|
|
assert handle._hidden_params["envd_access_token"] == "tok_abc"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_defaults_template_when_omitted():
|
|
client = FakeHTTPClient()
|
|
await E2BSandboxConfig().acreate_sandbox(api_key="e2b_key", client=client)
|
|
_, _, _, body = client.calls[0]
|
|
assert body["templateID"] == "code-interpreter-v1"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_run_code_targets_jupyter_host_with_access_token():
|
|
client = FakeHTTPClient(
|
|
execute_lines=[json.dumps({"type": "stdout", "text": "42\n", "timestamp": 1})]
|
|
)
|
|
handle = ContainerHandle(id="sbx_xyz", provider="e2b", domain="e2b.app")
|
|
handle._hidden_params = {"envd_access_token": "tok_run"}
|
|
|
|
result = await E2BSandboxConfig().arun_code(
|
|
container=handle, code="print(6*7)", client=client
|
|
)
|
|
|
|
method, url, headers, body = client.calls[0]
|
|
assert url == "https://49999-sbx_xyz.e2b.app/execute"
|
|
assert headers["X-Access-Token"] == "tok_run"
|
|
assert body["code"] == "print(6*7)"
|
|
assert result.stdout.strip() == "42"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_issues_delete_to_sandbox_id():
|
|
client = FakeHTTPClient(delete_status=204)
|
|
handle = ContainerHandle(id="sbx_del", provider="e2b", domain="e2b.app")
|
|
handle._hidden_params = {"api_key": "e2b_key"}
|
|
|
|
ok = await E2BSandboxConfig().adelete_sandbox(container=handle, client=client)
|
|
|
|
method, url, headers, _ = client.calls[0]
|
|
assert method == "DELETE"
|
|
assert url.endswith("/sandboxes/sbx_del")
|
|
assert ok is True
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_returns_false_on_404():
|
|
client = FakeHTTPClient(delete_status=404)
|
|
handle = ContainerHandle(id="sbx_gone", provider="e2b", domain="e2b.app")
|
|
handle._hidden_params = {"api_key": "e2b_key"}
|
|
ok = await E2BSandboxConfig().adelete_sandbox(container=handle, client=client)
|
|
assert ok is False
|
|
|
|
|
|
# ---------- ephemeral teardown ----------
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_code_interpreter_tool_deletes_even_when_run_raises():
|
|
client = FakeHTTPClient(execute_raises=RuntimeError("boom"))
|
|
|
|
with pytest.raises(RuntimeError, match="boom"):
|
|
await litellm.acode_interpreter_tool(
|
|
provider="e2b", code="1/0", api_key="e2b_key", client=client
|
|
)
|
|
|
|
methods = [c[0] for c in client.calls]
|
|
urls = [c[1] for c in client.calls]
|
|
assert methods == ["POST", "POST", "DELETE"] # create, run(raises), delete
|
|
assert urls[0].endswith("/sandboxes")
|
|
assert urls[1].endswith("/execute")
|
|
assert urls[2].endswith("/sandboxes/sbx_123")
|
|
|
|
|
|
# ---------- correctness guards ----------
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_reraises_non_404_http_error():
|
|
client = FakeHTTPClient(delete_status=500)
|
|
handle = ContainerHandle(id="sbx_err", provider="e2b", domain="e2b.app")
|
|
handle._hidden_params = {"api_key": "e2b_key"}
|
|
with pytest.raises(httpx.HTTPStatusError):
|
|
await E2BSandboxConfig().adelete_sandbox(container=handle, client=client)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_preserves_explicit_zero_timeout():
|
|
client = FakeHTTPClient()
|
|
await E2BSandboxConfig().acreate_sandbox(
|
|
timeout=0, api_key="e2b_key", client=client
|
|
)
|
|
_, _, _, body = client.calls[0]
|
|
assert body["timeout"] == 0
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_run_code_rejects_bare_id_without_access_token():
|
|
client = FakeHTTPClient()
|
|
with pytest.raises(ValueError, match="access token"):
|
|
await E2BSandboxConfig().arun_code(
|
|
container="sbx_no_token", code="print(1)", client=client
|
|
)
|
|
assert client.calls == [] # never reached the network
|
|
|
|
|
|
def test_parse_lines_skips_non_json_lines():
|
|
lines = [
|
|
"not-json-heartbeat",
|
|
json.dumps({"type": "stdout", "text": "ok\n"}),
|
|
"",
|
|
"{partial",
|
|
]
|
|
result = E2BSandboxConfig._parse_lines(lines)
|
|
assert result.stdout == "ok\n"
|
|
assert result.error is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_run_code_aborts_on_output_over_cap():
|
|
big_line = "x" * (MAX_OUTPUT_BYTES + 1)
|
|
client = FakeHTTPClient(execute_lines=[big_line])
|
|
handle = ContainerHandle(id="sbx_big", provider="e2b", domain="e2b.app")
|
|
handle._hidden_params = {"envd_access_token": "tok"}
|
|
with pytest.raises(ValueError, match="exceeded"):
|
|
await E2BSandboxConfig().arun_code(
|
|
container=handle, code="print('x'*999)", client=client
|
|
)
|
|
|
|
|
|
# ---------- public entrypoints ----------
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_public_lifecycle_create_run_delete():
|
|
client = FakeHTTPClient(
|
|
execute_lines=[json.dumps({"type": "stdout", "text": "42\n"})]
|
|
)
|
|
container = await litellm.acreate_sandbox(
|
|
provider="e2b", api_key="e2b_key", client=client
|
|
)
|
|
assert container.id == "sbx_123"
|
|
|
|
result = await litellm.arun_code(
|
|
provider="e2b",
|
|
container=container,
|
|
api_key="e2b_key",
|
|
code="print(6*7)",
|
|
client=client,
|
|
)
|
|
assert result.stdout.strip() == "42"
|
|
|
|
assert (
|
|
await litellm.adelete_sandbox(
|
|
provider="e2b", container=container, api_key="e2b_key", client=client
|
|
)
|
|
is True
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_unsupported_provider_raises():
|
|
with pytest.raises(ValueError, match="not-a-provider' is not a valid SandboxProviders"):
|
|
await litellm.acreate_sandbox(provider="not-a-provider")
|
|
|
|
|
|
# ---------- api_base override ----------
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_uses_api_base_override():
|
|
client = FakeHTTPClient()
|
|
await E2BSandboxConfig().acreate_sandbox(
|
|
api_base="http://my-sandbox:8080", api_key="k", client=client
|
|
)
|
|
_, url, _, _ = client.calls[0]
|
|
assert url == "http://my-sandbox:8080/sandboxes"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_defaults_to_e2b_api_base():
|
|
client = FakeHTTPClient()
|
|
await E2BSandboxConfig().acreate_sandbox(api_key="k", client=client)
|
|
_, url, _, _ = client.calls[0]
|
|
assert url == "https://api.e2b.app/sandboxes"
|