Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,6 @@
"defaults"
],
"dependencies": {
"e2b": "^2.6.0"
"e2b": "^2.7.0"
}
}
31 changes: 8 additions & 23 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

77 changes: 53 additions & 24 deletions python/poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions python/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ python = "^3.9"

httpx = ">=0.20.0, <1.0.0"
attrs = ">=21.3.0"
e2b = "^2.6.0"
e2b = "^2.7.0"

[tool.poetry.group.dev.dependencies]
pytest = "^7.4.0"
pytest = "^8.2.0"
python-dotenv = "^1.0.0"
pytest-dotenv = "^0.5.2"
pytest-asyncio = "^0.23.7"
pytest-asyncio = "^0.24.0"
pytest-xdist = "^3.6.1"
pydoc-markdown = "^4.8.2"
matplotlib = "^3.8.0"
Expand Down
59 changes: 46 additions & 13 deletions python/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import pytest
import pytest_asyncio
import os
import asyncio

from logging import warning

Expand All @@ -10,6 +11,19 @@
timeout = 60


# Override the event loop so it never closes during test execution
# This helps with pytest-xdist and prevents "Event loop is closed" errors
@pytest.fixture(scope="session")
def event_loop():
"""Create a session-scoped event loop for all async tests."""
try:
loop = asyncio.get_running_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
yield loop
loop.close()


@pytest.fixture()
def template():
return os.getenv("E2B_TESTS_TEMPLATE") or "code-interpreter-v1"
Expand All @@ -31,20 +45,39 @@ def sandbox(template, debug):
)


@pytest_asyncio.fixture
async def async_sandbox(template, debug):
async_sandbox = await AsyncSandbox.create(template, timeout=timeout, debug=debug)
@pytest.fixture
def async_sandbox_factory(request, template, debug, event_loop):
"""Factory for creating async sandboxes with proper cleanup."""

try:
yield async_sandbox
finally:
try:
await async_sandbox.kill()
except: # noqa: E722
if not debug:
warning(
"Failed to kill sandbox — this is expected if the test runs with local envd."
)
async def factory(template_override=None, **kwargs):
template_name = template_override or template
kwargs.setdefault("timeout", timeout)
kwargs.setdefault("debug", debug)

sandbox = await AsyncSandbox.create(template_name, **kwargs)

def kill():
async def _kill():
try:
await sandbox.kill()
except: # noqa: E722
if not debug:
warning(
"Failed to kill sandbox — this is expected if the test runs with local envd."
)

event_loop.run_until_complete(_kill())

request.addfinalizer(kill)
return sandbox

return factory
Comment on lines +48 to +74

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Run sandbox teardown on the loop that created it

The finalizer registered in async_sandbox_factory always uses the session-scoped event_loop to execute sandbox.kill(). However, the sandbox itself is created on whatever loop is running the fixture/test. With pytest-asyncio 0.24’s default asyncio_mode=auto, each async test runs inside its own asyncio.run loop, so the sandbox is bound to that per-test loop. Executing its teardown on a different loop raises RuntimeError: Task/Future attached to a different loop, which prevents the sandbox from being killed and leaks resources. Store the loop used during creation (e.g., via asyncio.get_running_loop()) and run the cleanup on that loop, or perform the cleanup in an async finalizer instead of switching loops.

Useful? React with 👍 / 👎.



@pytest.fixture
async def async_sandbox(async_sandbox_factory):
"""Default async sandbox fixture."""
return await async_sandbox_factory()


@pytest.fixture
Expand Down