chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
import pytest
|
||||
|
||||
from e2b import AsyncSandbox
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_get_info(async_sandbox: AsyncSandbox):
|
||||
info = await AsyncSandbox.get_info(async_sandbox.sandbox_id)
|
||||
assert info.sandbox_id == async_sandbox.sandbox_id
|
||||
@@ -0,0 +1,21 @@
|
||||
import pytest
|
||||
|
||||
from e2b import AsyncSandbox, SandboxQuery, SandboxState
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_kill_existing_sandbox(async_sandbox: AsyncSandbox, sandbox_test_id: str):
|
||||
assert await AsyncSandbox.kill(async_sandbox.sandbox_id)
|
||||
|
||||
paginator = AsyncSandbox.list(
|
||||
query=SandboxQuery(
|
||||
state=[SandboxState.RUNNING], metadata={"sandbox_test_id": sandbox_test_id}
|
||||
)
|
||||
)
|
||||
sandboxes = await paginator.next_items()
|
||||
assert async_sandbox.sandbox_id not in [s.sandbox_id for s in sandboxes]
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_kill_non_existing_sandbox():
|
||||
assert not await AsyncSandbox.kill("nonexistingsandbox")
|
||||
@@ -0,0 +1,190 @@
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
|
||||
from e2b import AsyncSandbox, SandboxQuery, SandboxState
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_list_sandboxes(async_sandbox: AsyncSandbox, sandbox_test_id: str):
|
||||
paginator = AsyncSandbox.list(
|
||||
query=SandboxQuery(metadata={"sandbox_test_id": sandbox_test_id})
|
||||
)
|
||||
sandboxes = await paginator.next_items()
|
||||
assert len(sandboxes) >= 1
|
||||
assert async_sandbox.sandbox_id in [sbx.sandbox_id for sbx in sandboxes]
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_list_sandboxes_with_filter(sandbox_test_id: str, async_sandbox_factory):
|
||||
unique_id = str(uuid.uuid4())
|
||||
extra_sbx = await async_sandbox_factory(metadata={"unique_id": unique_id})
|
||||
|
||||
paginator = AsyncSandbox.list(query=SandboxQuery(metadata={"unique_id": unique_id}))
|
||||
sandboxes = await paginator.next_items()
|
||||
assert len(sandboxes) == 1
|
||||
assert sandboxes[0].sandbox_id == extra_sbx.sandbox_id
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_list_running_sandboxes(
|
||||
async_sandbox: AsyncSandbox, sandbox_test_id: str
|
||||
):
|
||||
paginator = AsyncSandbox.list(
|
||||
query=SandboxQuery(
|
||||
metadata={"sandbox_test_id": sandbox_test_id}, state=[SandboxState.RUNNING]
|
||||
)
|
||||
)
|
||||
sandboxes = await paginator.next_items()
|
||||
assert len(sandboxes) >= 1
|
||||
|
||||
# Verify our running sandbox is in the list
|
||||
assert any(
|
||||
s.sandbox_id == async_sandbox.sandbox_id and s.state == SandboxState.RUNNING
|
||||
for s in sandboxes
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_list_paused_sandboxes(async_sandbox: AsyncSandbox, sandbox_test_id: str):
|
||||
await async_sandbox.beta_pause()
|
||||
|
||||
paginator = AsyncSandbox.list(
|
||||
query=SandboxQuery(
|
||||
metadata={"sandbox_test_id": sandbox_test_id}, state=[SandboxState.PAUSED]
|
||||
)
|
||||
)
|
||||
sandboxes = await paginator.next_items()
|
||||
assert len(sandboxes) >= 1
|
||||
|
||||
# Verify our paused sandbox is in the list
|
||||
assert any(
|
||||
s.sandbox_id == async_sandbox.sandbox_id and s.state == SandboxState.PAUSED
|
||||
for s in sandboxes
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_paginate_running_sandboxes(sandbox_test_id: str, async_sandbox_factory):
|
||||
sbx1 = await async_sandbox_factory()
|
||||
sbx2 = await async_sandbox_factory()
|
||||
|
||||
# Test pagination with limit
|
||||
paginator = AsyncSandbox.list(
|
||||
query=SandboxQuery(
|
||||
metadata={"sandbox_test_id": sandbox_test_id},
|
||||
state=[SandboxState.RUNNING],
|
||||
),
|
||||
limit=1,
|
||||
)
|
||||
sandboxes = await paginator.next_items()
|
||||
|
||||
# Check first page
|
||||
assert len(sandboxes) == 1
|
||||
assert sandboxes[0].state == SandboxState.RUNNING
|
||||
assert paginator.has_next is True
|
||||
assert paginator.next_token is not None
|
||||
assert sandboxes[0].sandbox_id == sbx2.sandbox_id
|
||||
|
||||
# Get second page
|
||||
sandboxes2 = await paginator.next_items()
|
||||
|
||||
# Check second page
|
||||
assert len(sandboxes2) == 1
|
||||
assert sandboxes2[0].state == SandboxState.RUNNING
|
||||
assert paginator.has_next is False
|
||||
assert paginator.next_token is None
|
||||
assert sandboxes2[0].sandbox_id == sbx1.sandbox_id
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_paginate_paused_sandboxes(
|
||||
async_sandbox: AsyncSandbox, sandbox_test_id: str, async_sandbox_factory
|
||||
):
|
||||
await async_sandbox.beta_pause()
|
||||
|
||||
# create another paused sandbox
|
||||
extra_sbx = await async_sandbox_factory(
|
||||
metadata={"sandbox_test_id": sandbox_test_id}
|
||||
)
|
||||
await extra_sbx.beta_pause()
|
||||
|
||||
# Test pagination with limit
|
||||
paginator = AsyncSandbox.list(
|
||||
query=SandboxQuery(
|
||||
state=[SandboxState.PAUSED],
|
||||
metadata={"sandbox_test_id": sandbox_test_id},
|
||||
),
|
||||
limit=1,
|
||||
)
|
||||
sandboxes = await paginator.next_items()
|
||||
|
||||
# Check first page
|
||||
assert len(sandboxes) == 1
|
||||
assert sandboxes[0].state == SandboxState.PAUSED
|
||||
assert paginator.has_next is True
|
||||
assert paginator.next_token is not None
|
||||
assert sandboxes[0].sandbox_id == extra_sbx.sandbox_id
|
||||
|
||||
# Get second page
|
||||
sandboxes2 = await paginator.next_items()
|
||||
|
||||
# Check second page
|
||||
assert len(sandboxes2) == 1
|
||||
assert sandboxes2[0].state == SandboxState.PAUSED
|
||||
assert paginator.has_next is False
|
||||
assert paginator.next_token is None
|
||||
assert sandboxes2[0].sandbox_id == async_sandbox.sandbox_id
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_paginate_running_and_paused_sandboxes(
|
||||
async_sandbox: AsyncSandbox, sandbox_test_id: str, async_sandbox_factory
|
||||
):
|
||||
# Create extra paused sandbox
|
||||
extra_sbx = await async_sandbox_factory(
|
||||
metadata={"sandbox_test_id": sandbox_test_id}
|
||||
)
|
||||
await extra_sbx.beta_pause()
|
||||
|
||||
# Test pagination with limit
|
||||
paginator = AsyncSandbox.list(
|
||||
query=SandboxQuery(
|
||||
metadata={"sandbox_test_id": sandbox_test_id},
|
||||
state=[SandboxState.RUNNING, SandboxState.PAUSED],
|
||||
),
|
||||
limit=1,
|
||||
)
|
||||
sandboxes = await paginator.next_items()
|
||||
|
||||
# Check first page
|
||||
assert len(sandboxes) == 1
|
||||
assert sandboxes[0].state == SandboxState.PAUSED
|
||||
assert paginator.has_next is True
|
||||
assert paginator.next_token is not None
|
||||
assert sandboxes[0].sandbox_id == extra_sbx.sandbox_id
|
||||
|
||||
# Get second page
|
||||
sandboxes2 = await paginator.next_items()
|
||||
|
||||
# Check second page
|
||||
assert len(sandboxes2) == 1
|
||||
assert sandboxes2[0].state == SandboxState.RUNNING
|
||||
assert paginator.has_next is False
|
||||
assert paginator.next_token is None
|
||||
assert sandboxes2[0].sandbox_id == async_sandbox.sandbox_id
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_paginate_iterator(async_sandbox: AsyncSandbox, sandbox_test_id: str):
|
||||
paginator = AsyncSandbox.list(
|
||||
query=SandboxQuery(metadata={"sandbox_test_id": sandbox_test_id})
|
||||
)
|
||||
sandboxes_list = []
|
||||
|
||||
while paginator.has_next:
|
||||
sandboxes = await paginator.next_items()
|
||||
sandboxes_list.extend(sandboxes)
|
||||
|
||||
assert len(sandboxes_list) > 0
|
||||
assert async_sandbox.sandbox_id in [sbx.sandbox_id for sbx in sandboxes_list]
|
||||
@@ -0,0 +1,19 @@
|
||||
import pytest
|
||||
from e2b import AsyncSandbox
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_pause_sandbox(async_sandbox: AsyncSandbox):
|
||||
await AsyncSandbox.pause(async_sandbox.sandbox_id)
|
||||
assert not await async_sandbox.is_running()
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_resume_sandbox(async_sandbox: AsyncSandbox):
|
||||
# pause
|
||||
await AsyncSandbox.pause(async_sandbox.sandbox_id)
|
||||
assert not await async_sandbox.is_running()
|
||||
|
||||
# resume
|
||||
await AsyncSandbox.connect(async_sandbox.sandbox_id)
|
||||
assert await async_sandbox.is_running()
|
||||
@@ -0,0 +1,18 @@
|
||||
import pytest
|
||||
|
||||
from e2b import NotFoundException, AsyncSandbox
|
||||
|
||||
|
||||
async def test_connect_to_process(async_sandbox: AsyncSandbox):
|
||||
cmd = await async_sandbox.commands.run("sleep 10", background=True)
|
||||
pid = cmd.pid
|
||||
|
||||
process_info = await async_sandbox.commands.connect(pid)
|
||||
assert process_info.pid == pid
|
||||
|
||||
|
||||
async def test_connect_to_non_existing_process(async_sandbox: AsyncSandbox):
|
||||
non_existing_pid = 999999
|
||||
|
||||
with pytest.raises(NotFoundException):
|
||||
await async_sandbox.commands.connect(non_existing_pid)
|
||||
@@ -0,0 +1,19 @@
|
||||
import pytest
|
||||
|
||||
from e2b import AsyncSandbox, CommandExitException
|
||||
|
||||
|
||||
async def test_kill_process(async_sandbox: AsyncSandbox):
|
||||
cmd = await async_sandbox.commands.run("sleep 10", background=True)
|
||||
pid = cmd.pid
|
||||
|
||||
await async_sandbox.commands.kill(pid)
|
||||
|
||||
with pytest.raises(CommandExitException):
|
||||
await async_sandbox.commands.run(f"kill -0 {pid}")
|
||||
|
||||
|
||||
async def test_kill_non_existing_process(async_sandbox: AsyncSandbox):
|
||||
non_existing_pid = 999999
|
||||
|
||||
assert not await async_sandbox.commands.kill(non_existing_pid)
|
||||
@@ -0,0 +1,13 @@
|
||||
from e2b import AsyncSandbox
|
||||
|
||||
|
||||
async def test_kill_process(async_sandbox: AsyncSandbox):
|
||||
c1 = await async_sandbox.commands.run("sleep 10", background=True)
|
||||
c2 = await async_sandbox.commands.run("sleep 10", background=True)
|
||||
|
||||
processes = await async_sandbox.commands.list()
|
||||
|
||||
assert len(processes) >= 2
|
||||
pids = [p.pid for p in processes]
|
||||
assert c1.pid in pids
|
||||
assert c2.pid in pids
|
||||
@@ -0,0 +1,35 @@
|
||||
import pytest
|
||||
|
||||
from e2b import AsyncSandbox
|
||||
|
||||
|
||||
async def test_command_envs(async_sandbox: AsyncSandbox):
|
||||
cmd = await async_sandbox.commands.run("echo $FOO", envs={"FOO": "bar"})
|
||||
assert cmd.stdout.strip() == "bar"
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_sandbox_envs(async_sandbox_factory):
|
||||
sbx = await async_sandbox_factory(envs={"FOO": "bar"})
|
||||
|
||||
cmd = await sbx.commands.run("echo $FOO")
|
||||
assert cmd.stdout.strip() == "bar"
|
||||
|
||||
|
||||
async def test_bash_command_scoped_env_vars(async_sandbox: AsyncSandbox):
|
||||
cmd = await async_sandbox.commands.run("echo $FOO", envs={"FOO": "bar"})
|
||||
assert cmd.exit_code == 0
|
||||
assert cmd.stdout.strip() == "bar"
|
||||
|
||||
# test that it is secure and not accessible to subsequent commands
|
||||
cmd2 = await async_sandbox.commands.run('sudo echo "$FOO"')
|
||||
assert cmd2.exit_code == 0
|
||||
assert cmd2.stdout.strip() == ""
|
||||
|
||||
|
||||
async def test_python_command_scoped_env_vars(async_sandbox: AsyncSandbox):
|
||||
cmd = await async_sandbox.commands.run(
|
||||
"python3 -c \"import os; print(os.environ['FOO'])\"", envs={"FOO": "bar"}
|
||||
)
|
||||
assert cmd.exit_code == 0
|
||||
assert cmd.stdout.strip() == "bar"
|
||||
@@ -0,0 +1,53 @@
|
||||
import pytest
|
||||
|
||||
from e2b import AsyncSandbox, TimeoutException
|
||||
|
||||
|
||||
async def test_run(async_sandbox: AsyncSandbox):
|
||||
text = "Hello, World!"
|
||||
|
||||
cmd = await async_sandbox.commands.run(f'echo "{text}"')
|
||||
|
||||
assert cmd.exit_code == 0
|
||||
assert cmd.stdout == f"{text}\n"
|
||||
|
||||
|
||||
async def test_run_with_special_characters(async_sandbox: AsyncSandbox):
|
||||
text = "!@#$%^&*()_+"
|
||||
|
||||
cmd = await async_sandbox.commands.run(f'echo "{text}"')
|
||||
|
||||
assert cmd.exit_code == 0
|
||||
|
||||
|
||||
# assert cmd.stdout == f"{text}\n"
|
||||
|
||||
|
||||
async def test_run_with_broken_utf8(async_sandbox: AsyncSandbox):
|
||||
# Create a string with 8191 'a' characters followed by the problematic byte 0xe2
|
||||
long_str = "a" * 8191 + "\\xe2"
|
||||
result = await async_sandbox.commands.run(f'printf "{long_str}"')
|
||||
assert result.exit_code == 0
|
||||
|
||||
# The broken UTF-8 bytes should be replaced with the Unicode replacement character
|
||||
assert result.stdout == ("a" * 8191 + "\ufffd")
|
||||
|
||||
|
||||
async def test_run_with_multiline_string(async_sandbox: AsyncSandbox):
|
||||
text = "Hello,\nWorld!"
|
||||
|
||||
cmd = await async_sandbox.commands.run(f'echo "{text}"')
|
||||
|
||||
assert cmd.exit_code == 0
|
||||
assert cmd.stdout == f"{text}\n"
|
||||
|
||||
|
||||
async def test_run_with_timeout(async_sandbox: AsyncSandbox):
|
||||
cmd = await async_sandbox.commands.run('echo "Hello, World!"', timeout=10)
|
||||
|
||||
assert cmd.exit_code == 0
|
||||
|
||||
|
||||
async def test_run_with_too_short_timeout(async_sandbox: AsyncSandbox):
|
||||
with pytest.raises(TimeoutException):
|
||||
await async_sandbox.commands.run("sleep 10", timeout=2)
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
import pytest
|
||||
|
||||
from e2b import AsyncSandbox, TimeoutException
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_kill_sandbox_while_command_is_running(async_sandbox: AsyncSandbox):
|
||||
cmd = await async_sandbox.commands.run("sleep 60", background=True)
|
||||
|
||||
await async_sandbox.kill()
|
||||
|
||||
with pytest.raises(TimeoutException) as exc_info:
|
||||
await cmd.wait()
|
||||
|
||||
# The health check confirms the sandbox is gone, so the error states it outright
|
||||
assert "sandbox was killed or reached its end of life" in str(exc_info.value)
|
||||
@@ -0,0 +1,94 @@
|
||||
import asyncio
|
||||
|
||||
from e2b import AsyncSandbox
|
||||
|
||||
|
||||
async def test_send_stdin_to_process(async_sandbox: AsyncSandbox):
|
||||
ev = asyncio.Event()
|
||||
|
||||
def handle_event(stdout: str):
|
||||
ev.set()
|
||||
|
||||
cmd = await async_sandbox.commands.run(
|
||||
"cat", background=True, on_stdout=handle_event, stdin=True
|
||||
)
|
||||
await async_sandbox.commands.send_stdin(cmd.pid, "Hello, World!")
|
||||
|
||||
await ev.wait()
|
||||
|
||||
assert cmd.stdout == "Hello, World!"
|
||||
|
||||
|
||||
async def test_send_bytes_stdin_to_process(async_sandbox: AsyncSandbox):
|
||||
ev = asyncio.Event()
|
||||
|
||||
def handle_event(stdout: str):
|
||||
ev.set()
|
||||
|
||||
cmd = await async_sandbox.commands.run(
|
||||
"cat", background=True, on_stdout=handle_event, stdin=True
|
||||
)
|
||||
await async_sandbox.commands.send_stdin(cmd.pid, b"Hello, World!")
|
||||
|
||||
await ev.wait()
|
||||
|
||||
assert cmd.stdout == "Hello, World!"
|
||||
|
||||
|
||||
async def test_send_stdin_via_command_handle(async_sandbox: AsyncSandbox):
|
||||
ev = asyncio.Event()
|
||||
|
||||
def handle_event(stdout: str):
|
||||
ev.set()
|
||||
|
||||
cmd = await async_sandbox.commands.run(
|
||||
"cat", background=True, on_stdout=handle_event, stdin=True
|
||||
)
|
||||
await cmd.send_stdin("Hello, World!")
|
||||
|
||||
await ev.wait()
|
||||
|
||||
assert cmd.stdout == "Hello, World!"
|
||||
|
||||
|
||||
async def test_close_stdin_via_command_handle(async_sandbox: AsyncSandbox):
|
||||
cmd = await async_sandbox.commands.run("cat", background=True, stdin=True)
|
||||
await cmd.send_stdin("Hello, World!")
|
||||
await cmd.close_stdin()
|
||||
|
||||
# `cat` exits once stdin is closed (EOF).
|
||||
result = await cmd.wait()
|
||||
assert result.exit_code == 0
|
||||
assert result.stdout == "Hello, World!"
|
||||
|
||||
|
||||
async def test_send_special_characters_to_process(async_sandbox: AsyncSandbox):
|
||||
ev = asyncio.Event()
|
||||
|
||||
def handle_event(stdout: str):
|
||||
ev.set()
|
||||
|
||||
cmd = await async_sandbox.commands.run(
|
||||
"cat", background=True, on_stdout=handle_event, stdin=True
|
||||
)
|
||||
await async_sandbox.commands.send_stdin(cmd.pid, "!@#$%^&*()_+")
|
||||
|
||||
await ev.wait()
|
||||
|
||||
assert cmd.stdout == "!@#$%^&*()_+"
|
||||
|
||||
|
||||
async def test_send_multiline_string_to_process(async_sandbox: AsyncSandbox):
|
||||
ev = asyncio.Event()
|
||||
|
||||
def handle_event(stdout: str):
|
||||
ev.set()
|
||||
|
||||
cmd = await async_sandbox.commands.run(
|
||||
"cat", background=True, on_stdout=handle_event, stdin=True
|
||||
)
|
||||
await async_sandbox.commands.send_stdin(cmd.pid, "Hello,\nWorld!")
|
||||
|
||||
await ev.wait()
|
||||
|
||||
assert cmd.stdout == "Hello,\nWorld!"
|
||||
@@ -0,0 +1,62 @@
|
||||
from e2b import AsyncSandbox
|
||||
from e2b.sandbox.filesystem.filesystem import WriteEntry
|
||||
|
||||
|
||||
async def test_write_and_read_with_gzip(async_sandbox: AsyncSandbox, debug):
|
||||
filename = "test_gzip_write.txt"
|
||||
content = "This is a test file with gzip encoding."
|
||||
|
||||
info = await async_sandbox.files.write(filename, content, gzip=True)
|
||||
assert info.path == f"/home/user/{filename}"
|
||||
|
||||
read_content = await async_sandbox.files.read(filename, gzip=True)
|
||||
assert read_content == content
|
||||
|
||||
if debug:
|
||||
await async_sandbox.files.remove(filename)
|
||||
|
||||
|
||||
async def test_write_gzip_read_plain(async_sandbox: AsyncSandbox, debug):
|
||||
filename = "test_gzip_write_plain_read.txt"
|
||||
content = "Written with gzip, read without."
|
||||
|
||||
await async_sandbox.files.write(filename, content, gzip=True)
|
||||
|
||||
read_content = await async_sandbox.files.read(filename)
|
||||
assert read_content == content
|
||||
|
||||
if debug:
|
||||
await async_sandbox.files.remove(filename)
|
||||
|
||||
|
||||
async def test_write_files_with_gzip(async_sandbox: AsyncSandbox, debug):
|
||||
files = [
|
||||
WriteEntry(path="gzip_multi_1.txt", data="File 1 content"),
|
||||
WriteEntry(path="gzip_multi_2.txt", data="File 2 content"),
|
||||
WriteEntry(path="gzip_multi_3.txt", data="File 3 content"),
|
||||
]
|
||||
|
||||
infos = await async_sandbox.files.write_files(files, gzip=True)
|
||||
assert len(infos) == len(files)
|
||||
|
||||
for file in files:
|
||||
read_content = await async_sandbox.files.read(file["path"])
|
||||
assert read_content == file["data"]
|
||||
|
||||
if debug:
|
||||
for file in files:
|
||||
await async_sandbox.files.remove(file["path"])
|
||||
|
||||
|
||||
async def test_read_bytes_with_gzip(async_sandbox: AsyncSandbox, debug):
|
||||
filename = "test_gzip_bytes.txt"
|
||||
content = "Binary content with gzip."
|
||||
|
||||
await async_sandbox.files.write(filename, content)
|
||||
|
||||
read_bytes = await async_sandbox.files.read(filename, format="bytes", gzip=True)
|
||||
assert isinstance(read_bytes, bytearray)
|
||||
assert read_bytes.decode("utf-8") == content
|
||||
|
||||
if debug:
|
||||
await async_sandbox.files.remove(filename)
|
||||
@@ -0,0 +1,8 @@
|
||||
from e2b import AsyncSandbox
|
||||
|
||||
|
||||
async def test_exists(async_sandbox: AsyncSandbox):
|
||||
filename = "test_exists.txt"
|
||||
|
||||
await async_sandbox.files.write(filename, "test")
|
||||
assert await async_sandbox.files.exists(filename)
|
||||
@@ -0,0 +1,248 @@
|
||||
import uuid
|
||||
from typing import Any
|
||||
|
||||
from e2b import AsyncSandbox, FileType
|
||||
|
||||
|
||||
async def test_list_directory(async_sandbox: AsyncSandbox):
|
||||
home_dir_name = "/home/user"
|
||||
parent_dir_name = f"test_directory_{uuid.uuid4()}"
|
||||
|
||||
await async_sandbox.files.make_dir(parent_dir_name)
|
||||
await async_sandbox.files.make_dir(f"{parent_dir_name}/subdir1")
|
||||
await async_sandbox.files.make_dir(f"{parent_dir_name}/subdir2")
|
||||
await async_sandbox.files.make_dir(f"{parent_dir_name}/subdir1/subdir1_1")
|
||||
await async_sandbox.files.make_dir(f"{parent_dir_name}/subdir1/subdir1_2")
|
||||
await async_sandbox.files.make_dir(f"{parent_dir_name}/subdir2/subdir2_1")
|
||||
await async_sandbox.files.make_dir(f"{parent_dir_name}/subdir2/subdir2_2")
|
||||
await async_sandbox.files.write(f"{parent_dir_name}/file1.txt", "Hello, world!")
|
||||
|
||||
test_cases: list[dict[str, Any]] = [
|
||||
{
|
||||
"name": "default depth (1)",
|
||||
"depth": None,
|
||||
"expected_len": 3,
|
||||
"expected_file_names": [
|
||||
"file1.txt",
|
||||
"subdir1",
|
||||
"subdir2",
|
||||
],
|
||||
"expected_file_types": [
|
||||
FileType.FILE,
|
||||
FileType.DIR,
|
||||
FileType.DIR,
|
||||
],
|
||||
"expected_file_paths": [
|
||||
f"{home_dir_name}/{parent_dir_name}/file1.txt",
|
||||
f"{home_dir_name}/{parent_dir_name}/subdir1",
|
||||
f"{home_dir_name}/{parent_dir_name}/subdir2",
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "explicit depth 1",
|
||||
"depth": 1,
|
||||
"expected_len": 3,
|
||||
"expected_file_names": [
|
||||
"file1.txt",
|
||||
"subdir1",
|
||||
"subdir2",
|
||||
],
|
||||
"expected_file_types": [
|
||||
FileType.FILE,
|
||||
FileType.DIR,
|
||||
FileType.DIR,
|
||||
],
|
||||
"expected_file_paths": [
|
||||
f"{home_dir_name}/{parent_dir_name}/file1.txt",
|
||||
f"{home_dir_name}/{parent_dir_name}/subdir1",
|
||||
f"{home_dir_name}/{parent_dir_name}/subdir2",
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "explicit depth 2",
|
||||
"depth": 2,
|
||||
"expected_len": 7,
|
||||
"expected_file_types": [
|
||||
FileType.FILE,
|
||||
FileType.DIR,
|
||||
FileType.DIR,
|
||||
FileType.DIR,
|
||||
FileType.DIR,
|
||||
FileType.DIR,
|
||||
FileType.DIR,
|
||||
],
|
||||
"expected_file_names": [
|
||||
"file1.txt",
|
||||
"subdir1",
|
||||
"subdir1_1",
|
||||
"subdir1_2",
|
||||
"subdir2",
|
||||
"subdir2_1",
|
||||
"subdir2_2",
|
||||
],
|
||||
"expected_file_paths": [
|
||||
f"{home_dir_name}/{parent_dir_name}/file1.txt",
|
||||
f"{home_dir_name}/{parent_dir_name}/subdir1",
|
||||
f"{home_dir_name}/{parent_dir_name}/subdir1/subdir1_1",
|
||||
f"{home_dir_name}/{parent_dir_name}/subdir1/subdir1_2",
|
||||
f"{home_dir_name}/{parent_dir_name}/subdir2",
|
||||
f"{home_dir_name}/{parent_dir_name}/subdir2/subdir2_1",
|
||||
f"{home_dir_name}/{parent_dir_name}/subdir2/subdir2_2",
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "explicit depth 3 (should be the same as depth 2)",
|
||||
"depth": 3,
|
||||
"expected_len": 7,
|
||||
"expected_file_names": [
|
||||
"file1.txt",
|
||||
"subdir1",
|
||||
"subdir1_1",
|
||||
"subdir1_2",
|
||||
"subdir2",
|
||||
"subdir2_1",
|
||||
"subdir2_2",
|
||||
],
|
||||
"expected_file_types": [
|
||||
FileType.FILE,
|
||||
FileType.DIR,
|
||||
FileType.DIR,
|
||||
FileType.DIR,
|
||||
FileType.DIR,
|
||||
FileType.DIR,
|
||||
FileType.DIR,
|
||||
],
|
||||
"expected_file_paths": [
|
||||
f"{home_dir_name}/{parent_dir_name}/file1.txt",
|
||||
f"{home_dir_name}/{parent_dir_name}/subdir1",
|
||||
f"{home_dir_name}/{parent_dir_name}/subdir1/subdir1_1",
|
||||
f"{home_dir_name}/{parent_dir_name}/subdir1/subdir1_2",
|
||||
f"{home_dir_name}/{parent_dir_name}/subdir2",
|
||||
f"{home_dir_name}/{parent_dir_name}/subdir2/subdir2_1",
|
||||
f"{home_dir_name}/{parent_dir_name}/subdir2/subdir2_2",
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
for test_case in test_cases:
|
||||
files = await async_sandbox.files.list(
|
||||
parent_dir_name,
|
||||
depth=test_case["depth"] if test_case["depth"] is not None else None,
|
||||
)
|
||||
|
||||
assert len(files) == test_case["expected_len"]
|
||||
|
||||
for i in range(len(test_case["expected_file_names"])):
|
||||
assert files[i].name == test_case["expected_file_names"][i]
|
||||
assert files[i].path == test_case["expected_file_paths"][i]
|
||||
assert files[i].type == test_case["expected_file_types"][i]
|
||||
|
||||
await async_sandbox.files.remove(parent_dir_name)
|
||||
|
||||
|
||||
async def test_list_directory_error_cases(async_sandbox: AsyncSandbox):
|
||||
parent_dir_name = f"test_directory_{uuid.uuid4()}"
|
||||
await async_sandbox.files.make_dir(parent_dir_name)
|
||||
|
||||
expected_error_message = "depth should be at least 1"
|
||||
try:
|
||||
await async_sandbox.files.list(parent_dir_name, depth=-1)
|
||||
assert False, "Expected error but none was thrown"
|
||||
except Exception as err:
|
||||
assert expected_error_message in str(err), (
|
||||
f'expected error message to include "{expected_error_message}"'
|
||||
)
|
||||
|
||||
await async_sandbox.files.remove(parent_dir_name)
|
||||
|
||||
|
||||
async def test_file_entry_details(async_sandbox: AsyncSandbox):
|
||||
test_dir = "test-file-entry"
|
||||
file_path = f"{test_dir}/test.txt"
|
||||
content = "Hello, World!"
|
||||
|
||||
await async_sandbox.files.make_dir(test_dir)
|
||||
await async_sandbox.files.write(file_path, content)
|
||||
|
||||
files = await async_sandbox.files.list(test_dir, depth=1)
|
||||
assert len(files) == 1
|
||||
|
||||
file_entry = files[0]
|
||||
assert file_entry.name == "test.txt"
|
||||
assert file_entry.path == f"/home/user/{file_path}"
|
||||
assert file_entry.type == FileType.FILE
|
||||
assert file_entry.mode == 0o644
|
||||
assert file_entry.permissions == "-rw-r--r--"
|
||||
assert file_entry.owner == "user"
|
||||
assert file_entry.group == "user"
|
||||
assert file_entry.size == len(content)
|
||||
assert file_entry.modified_time is not None
|
||||
assert file_entry.symlink_target is None
|
||||
|
||||
await async_sandbox.files.remove(test_dir)
|
||||
|
||||
|
||||
async def test_directory_entry_details(async_sandbox: AsyncSandbox):
|
||||
test_dir = "test-entry-info"
|
||||
sub_dir = f"{test_dir}/subdir"
|
||||
|
||||
await async_sandbox.files.make_dir(test_dir)
|
||||
await async_sandbox.files.make_dir(sub_dir)
|
||||
|
||||
files = await async_sandbox.files.list(test_dir, depth=1)
|
||||
assert len(files) == 1
|
||||
|
||||
dir_entry = files[0]
|
||||
assert dir_entry.name == "subdir"
|
||||
assert dir_entry.path == f"/home/user/{sub_dir}"
|
||||
assert dir_entry.type == FileType.DIR
|
||||
assert dir_entry.mode == 0o755
|
||||
assert dir_entry.permissions == "drwxr-xr-x"
|
||||
assert dir_entry.owner == "user"
|
||||
assert dir_entry.group == "user"
|
||||
assert dir_entry.modified_time is not None
|
||||
assert dir_entry.symlink_target is None
|
||||
|
||||
await async_sandbox.files.remove(test_dir)
|
||||
|
||||
|
||||
async def test_mixed_entries(async_sandbox: AsyncSandbox):
|
||||
test_dir = "test-mixed-entries"
|
||||
sub_dir = f"{test_dir}/subdir"
|
||||
file_path = f"{test_dir}/test.txt"
|
||||
content = "Hello, World!"
|
||||
|
||||
await async_sandbox.files.make_dir(test_dir)
|
||||
await async_sandbox.files.make_dir(sub_dir)
|
||||
await async_sandbox.files.write(file_path, content)
|
||||
|
||||
files = await async_sandbox.files.list(test_dir, depth=1)
|
||||
assert len(files) == 2
|
||||
|
||||
# Create a dictionary of entries by name for easier verification
|
||||
entries = {entry.name: entry for entry in files}
|
||||
|
||||
# Verify directory entry
|
||||
dir_entry = entries.get("subdir")
|
||||
assert dir_entry is not None
|
||||
assert dir_entry.path == f"/home/user/{sub_dir}"
|
||||
assert dir_entry.type == FileType.DIR
|
||||
assert dir_entry.mode == 0o755
|
||||
assert dir_entry.permissions == "drwxr-xr-x"
|
||||
assert dir_entry.owner == "user"
|
||||
assert dir_entry.group == "user"
|
||||
assert dir_entry.modified_time is not None
|
||||
|
||||
# Verify file entry
|
||||
file_entry = entries.get("test.txt")
|
||||
assert file_entry is not None
|
||||
assert file_entry.path == f"/home/user/{file_path}"
|
||||
assert file_entry.type == FileType.FILE
|
||||
assert file_entry.mode == 0o644
|
||||
assert file_entry.permissions == "-rw-r--r--"
|
||||
assert file_entry.owner == "user"
|
||||
assert file_entry.group == "user"
|
||||
assert file_entry.size == len(content)
|
||||
assert file_entry.modified_time is not None
|
||||
|
||||
await async_sandbox.files.remove(test_dir)
|
||||
@@ -0,0 +1,79 @@
|
||||
import pytest
|
||||
from e2b.exceptions import FileNotFoundException
|
||||
from e2b import AsyncSandbox, FileType
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_info_of_file(async_sandbox: AsyncSandbox):
|
||||
filename = "test_file.txt"
|
||||
|
||||
await async_sandbox.files.write(filename, "test")
|
||||
info = await async_sandbox.files.get_info(filename)
|
||||
current_path = await async_sandbox.commands.run("pwd")
|
||||
|
||||
assert info.name == filename
|
||||
assert info.type == FileType.FILE
|
||||
assert info.path == f"{current_path.stdout.strip()}/{filename}"
|
||||
assert info.size == 4
|
||||
assert info.mode == 0o644
|
||||
assert info.permissions == "-rw-r--r--"
|
||||
assert info.owner == "user"
|
||||
assert info.group == "user"
|
||||
assert info.modified_time is not None
|
||||
assert info.modified_time.tzinfo is not None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_info_of_nonexistent_file(async_sandbox: AsyncSandbox):
|
||||
filename = "test_does_not_exist.txt"
|
||||
|
||||
with pytest.raises(FileNotFoundException):
|
||||
await async_sandbox.files.get_info(filename)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_info_of_directory(async_sandbox: AsyncSandbox):
|
||||
dirname = "test_dir"
|
||||
|
||||
await async_sandbox.files.make_dir(dirname)
|
||||
info = await async_sandbox.files.get_info(dirname)
|
||||
current_path = await async_sandbox.commands.run("pwd")
|
||||
|
||||
assert info.name == dirname
|
||||
assert info.type == FileType.DIR
|
||||
assert info.path == f"{current_path.stdout.strip()}/{dirname}"
|
||||
assert info.size > 0
|
||||
assert info.mode == 0o755
|
||||
assert info.permissions == "drwxr-xr-x"
|
||||
assert info.owner == "user"
|
||||
assert info.group == "user"
|
||||
assert info.modified_time is not None
|
||||
assert info.modified_time.tzinfo is not None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_info_of_nonexistent_directory(async_sandbox: AsyncSandbox):
|
||||
dirname = "test_does_not_exist_dir"
|
||||
|
||||
with pytest.raises(FileNotFoundException):
|
||||
await async_sandbox.files.get_info(dirname)
|
||||
|
||||
|
||||
async def test_file_symlink(async_sandbox: AsyncSandbox):
|
||||
test_dir = "test-simlink-entry"
|
||||
file_name = "test.txt"
|
||||
content = "Hello, World!"
|
||||
|
||||
await async_sandbox.files.make_dir(test_dir)
|
||||
await async_sandbox.files.write(f"{test_dir}/{file_name}", content)
|
||||
|
||||
symlink_name = "symlink_to_test.txt"
|
||||
await async_sandbox.commands.run(f"ln -s {file_name} {symlink_name}", cwd=test_dir)
|
||||
|
||||
file = await async_sandbox.files.get_info(f"{test_dir}/{symlink_name}")
|
||||
|
||||
pwd = await async_sandbox.commands.run("pwd")
|
||||
assert file.type == FileType.FILE
|
||||
assert file.symlink_target == f"{pwd.stdout.strip()}/{test_dir}/{file_name}"
|
||||
|
||||
await async_sandbox.files.remove(test_dir)
|
||||
@@ -0,0 +1,29 @@
|
||||
import uuid
|
||||
|
||||
from e2b import AsyncSandbox
|
||||
|
||||
|
||||
async def test_make_directory(async_sandbox: AsyncSandbox):
|
||||
dir_name = f"test_directory_{uuid.uuid4()}"
|
||||
|
||||
await async_sandbox.files.make_dir(dir_name)
|
||||
exists = await async_sandbox.files.exists(dir_name)
|
||||
assert exists
|
||||
|
||||
|
||||
async def test_make_directory_already_exists(async_sandbox: AsyncSandbox):
|
||||
dir_name = f"test_directory_{uuid.uuid4()}"
|
||||
|
||||
created = await async_sandbox.files.make_dir(dir_name)
|
||||
assert created
|
||||
|
||||
created = await async_sandbox.files.make_dir(dir_name)
|
||||
assert not created
|
||||
|
||||
|
||||
async def test_make_nested_directory(async_sandbox: AsyncSandbox):
|
||||
nested_dir_name = f"test_directory_{uuid.uuid4()}/nested_directory"
|
||||
|
||||
await async_sandbox.files.make_dir(nested_dir_name)
|
||||
exists = await async_sandbox.files.exists(nested_dir_name)
|
||||
assert exists
|
||||
@@ -0,0 +1,172 @@
|
||||
import pytest
|
||||
|
||||
from e2b import AsyncSandbox
|
||||
from e2b.exceptions import InvalidArgumentException
|
||||
from e2b.sandbox.filesystem.filesystem import WriteEntry
|
||||
|
||||
|
||||
async def test_write_file_with_metadata(async_sandbox: AsyncSandbox, debug):
|
||||
filename = "test_metadata.txt"
|
||||
content = "This is a test file with metadata."
|
||||
metadata = {"author": "mish", "purpose": "upload"}
|
||||
|
||||
info = await async_sandbox.files.write(filename, content, metadata=metadata)
|
||||
assert info.metadata == metadata
|
||||
|
||||
# Metadata is persisted and surfaced on subsequent reads.
|
||||
stat = await async_sandbox.files.get_info(filename)
|
||||
assert stat.metadata == metadata
|
||||
|
||||
if debug:
|
||||
await async_sandbox.files.remove(filename)
|
||||
|
||||
|
||||
async def test_write_file_with_metadata_octet_stream(
|
||||
async_sandbox: AsyncSandbox, debug
|
||||
):
|
||||
filename = "test_metadata_octet.txt"
|
||||
content = "This is a test file with metadata."
|
||||
metadata = {"author": "mish", "purpose": "upload"}
|
||||
|
||||
info = await async_sandbox.files.write(
|
||||
filename, content, metadata=metadata, use_octet_stream=True
|
||||
)
|
||||
assert info.metadata == metadata
|
||||
|
||||
stat = await async_sandbox.files.get_info(filename)
|
||||
assert stat.metadata == metadata
|
||||
|
||||
if debug:
|
||||
await async_sandbox.files.remove(filename)
|
||||
|
||||
|
||||
async def test_write_file_without_metadata(async_sandbox: AsyncSandbox, debug):
|
||||
filename = "test_no_metadata.txt"
|
||||
|
||||
info = await async_sandbox.files.write(filename, "no metadata here")
|
||||
assert info.metadata is None
|
||||
|
||||
stat = await async_sandbox.files.get_info(filename)
|
||||
assert stat.metadata is None
|
||||
|
||||
if debug:
|
||||
await async_sandbox.files.remove(filename)
|
||||
|
||||
|
||||
async def test_write_files_applies_metadata_to_every_file(
|
||||
async_sandbox: AsyncSandbox, debug
|
||||
):
|
||||
# The same metadata is applied to every file in the upload.
|
||||
metadata = {"source": "test-suite"}
|
||||
files = [
|
||||
WriteEntry(path="metadata_multi_1.txt", data="File 1"),
|
||||
WriteEntry(path="metadata_multi_2.txt", data="File 2"),
|
||||
]
|
||||
|
||||
infos = await async_sandbox.files.write_files(files, metadata=metadata)
|
||||
assert len(infos) == len(files)
|
||||
|
||||
for info in infos:
|
||||
assert info.metadata == metadata
|
||||
stat = await async_sandbox.files.get_info(info.path)
|
||||
assert stat.metadata == metadata
|
||||
|
||||
if debug:
|
||||
for file in files:
|
||||
await async_sandbox.files.remove(file["path"])
|
||||
|
||||
|
||||
async def test_metadata_surfaced_when_listing(async_sandbox: AsyncSandbox, debug):
|
||||
dirname = "metadata_list_dir"
|
||||
filename = "listed.txt"
|
||||
metadata = {"tag": "listed"}
|
||||
|
||||
await async_sandbox.files.make_dir(dirname)
|
||||
await async_sandbox.files.write(
|
||||
f"{dirname}/{filename}", "content", metadata=metadata
|
||||
)
|
||||
|
||||
entries = await async_sandbox.files.list(dirname)
|
||||
entry = next((e for e in entries if e.name == filename), None)
|
||||
assert entry is not None
|
||||
assert entry.metadata == metadata
|
||||
|
||||
if debug:
|
||||
await async_sandbox.files.remove(dirname)
|
||||
|
||||
|
||||
async def test_metadata_surfaced_after_rename(async_sandbox: AsyncSandbox, debug):
|
||||
old_path = "metadata_rename_old.txt"
|
||||
new_path = "metadata_rename_new.txt"
|
||||
metadata = {"stage": "renamed"}
|
||||
|
||||
await async_sandbox.files.write(old_path, "content", metadata=metadata)
|
||||
info = await async_sandbox.files.rename(old_path, new_path)
|
||||
assert info.metadata == metadata
|
||||
|
||||
if debug:
|
||||
await async_sandbox.files.remove(new_path)
|
||||
|
||||
|
||||
async def test_overwriting_clears_stale_metadata(async_sandbox: AsyncSandbox, debug):
|
||||
filename = "metadata_overwrite.txt"
|
||||
|
||||
await async_sandbox.files.write(filename, "first", metadata={"author": "mish"})
|
||||
|
||||
# Overwriting without metadata removes the previously stored metadata.
|
||||
info = await async_sandbox.files.write(filename, "second")
|
||||
assert info.metadata is None
|
||||
|
||||
stat = await async_sandbox.files.get_info(filename)
|
||||
assert stat.metadata is None
|
||||
|
||||
if debug:
|
||||
await async_sandbox.files.remove(filename)
|
||||
|
||||
|
||||
async def test_metadata_set_via_xattrs_surfaced_in_get_info(
|
||||
async_sandbox: AsyncSandbox, debug
|
||||
):
|
||||
filename = "metadata_xattr.txt"
|
||||
await async_sandbox.files.write(filename, "content")
|
||||
|
||||
cmd = await async_sandbox.commands.run(f"realpath {filename}")
|
||||
file_path = cmd.stdout.strip()
|
||||
|
||||
# Set an xattr directly in the `user.e2b.` namespace (out-of-band, not via
|
||||
# the SDK upload); it should surface as metadata (with the namespace prefix
|
||||
# stripped) when reading the file info.
|
||||
await async_sandbox.commands.run(
|
||||
f"python3 -c \"import os; os.setxattr('{file_path}', 'user.e2b.author', b'mish')\""
|
||||
)
|
||||
|
||||
info = await async_sandbox.files.get_info(filename)
|
||||
assert info.metadata == {"author": "mish"}
|
||||
|
||||
if debug:
|
||||
await async_sandbox.files.remove(filename)
|
||||
|
||||
|
||||
async def test_write_rejects_invalid_metadata(async_sandbox: AsyncSandbox):
|
||||
filename = "invalid_metadata.txt"
|
||||
|
||||
# Key with a space is not a valid HTTP header token.
|
||||
with pytest.raises(InvalidArgumentException):
|
||||
await async_sandbox.files.write(filename, "x", metadata={"bad key": "value"})
|
||||
|
||||
# Empty key.
|
||||
with pytest.raises(InvalidArgumentException):
|
||||
await async_sandbox.files.write(filename, "x", metadata={"": "value"})
|
||||
|
||||
# Value with a non-printable / non-ASCII character.
|
||||
with pytest.raises(InvalidArgumentException):
|
||||
await async_sandbox.files.write(filename, "x", metadata={"good": "bad\nvalue"})
|
||||
|
||||
# Trailing newline (Python's `$` would accept it; `\Z` must not).
|
||||
with pytest.raises(InvalidArgumentException):
|
||||
await async_sandbox.files.write(filename, "x", metadata={"good": "value\n"})
|
||||
with pytest.raises(InvalidArgumentException):
|
||||
await async_sandbox.files.write(filename, "x", metadata={"key\n": "value"})
|
||||
|
||||
# The file must not have been created by a rejected write.
|
||||
assert not await async_sandbox.files.exists(filename)
|
||||
@@ -0,0 +1,95 @@
|
||||
import pytest
|
||||
|
||||
from e2b import FileNotFoundException, NotFoundException, AsyncSandbox
|
||||
|
||||
|
||||
async def test_read_file(async_sandbox: AsyncSandbox):
|
||||
filename = "test_read.txt"
|
||||
content = "Hello, world!"
|
||||
|
||||
await async_sandbox.files.write(filename, content)
|
||||
read_content = await async_sandbox.files.read(filename)
|
||||
assert read_content == content
|
||||
|
||||
|
||||
async def test_read_non_existing_file(async_sandbox: AsyncSandbox):
|
||||
filename = "non_existing_file.txt"
|
||||
|
||||
with pytest.raises(FileNotFoundException):
|
||||
await async_sandbox.files.read(filename)
|
||||
|
||||
|
||||
async def test_read_non_existing_file_catches_with_deprecated_not_found_exception(
|
||||
async_sandbox: AsyncSandbox,
|
||||
):
|
||||
filename = "non_existing_file.txt"
|
||||
|
||||
with pytest.raises(NotFoundException):
|
||||
await async_sandbox.files.read(filename)
|
||||
|
||||
|
||||
async def test_read_empty_file(async_sandbox: AsyncSandbox):
|
||||
filename = "empty_file.txt"
|
||||
content = ""
|
||||
|
||||
await async_sandbox.commands.run(f"touch {filename}")
|
||||
read_content = await async_sandbox.files.read(filename)
|
||||
assert read_content == content
|
||||
|
||||
|
||||
async def test_read_file_as_stream(async_sandbox: AsyncSandbox):
|
||||
filename = "test_read_stream.txt"
|
||||
content = "Streamed read content. " * 10_000
|
||||
|
||||
await async_sandbox.files.write(filename, content)
|
||||
stream = await async_sandbox.files.read(filename, format="stream")
|
||||
chunks = []
|
||||
async for chunk in stream:
|
||||
chunks.append(chunk)
|
||||
read_content = b"".join(chunks).decode("utf-8")
|
||||
assert read_content == content
|
||||
|
||||
|
||||
async def test_read_file_as_stream_with_gzip(async_sandbox: AsyncSandbox):
|
||||
filename = "test_read_stream_gzip.txt"
|
||||
content = "Streamed gzipped read content. " * 10_000
|
||||
|
||||
await async_sandbox.files.write(filename, content)
|
||||
stream = await async_sandbox.files.read(filename, format="stream", gzip=True)
|
||||
chunks = []
|
||||
async for chunk in stream:
|
||||
chunks.append(chunk)
|
||||
read_content = b"".join(chunks).decode("utf-8")
|
||||
assert read_content == content
|
||||
|
||||
|
||||
async def test_read_non_existing_file_as_stream(async_sandbox: AsyncSandbox):
|
||||
filename = "non_existing_file.txt"
|
||||
|
||||
with pytest.raises(FileNotFoundException):
|
||||
await async_sandbox.files.read(filename, format="stream")
|
||||
|
||||
|
||||
async def test_read_file_as_stream_context_manager(async_sandbox: AsyncSandbox):
|
||||
filename = "test_read_stream_ctx.txt"
|
||||
content = "Streamed read content. " * 10_000
|
||||
|
||||
await async_sandbox.files.write(filename, content)
|
||||
chunks = []
|
||||
async with await async_sandbox.files.read(filename, format="stream") as stream:
|
||||
async for chunk in stream:
|
||||
chunks.append(chunk)
|
||||
read_content = b"".join(chunks).decode("utf-8")
|
||||
assert read_content == content
|
||||
|
||||
|
||||
async def test_read_file_as_stream_partial_then_close(async_sandbox: AsyncSandbox):
|
||||
filename = "test_read_stream_partial.txt"
|
||||
content = "Streamed read content. " * 10_000
|
||||
|
||||
await async_sandbox.files.write(filename, content)
|
||||
# Reading only the first chunk and closing must not raise or leak.
|
||||
stream = await async_sandbox.files.read(filename, format="stream")
|
||||
first = await stream.__anext__()
|
||||
assert len(first) > 0
|
||||
await stream.aclose()
|
||||
@@ -0,0 +1,18 @@
|
||||
from e2b import AsyncSandbox
|
||||
|
||||
|
||||
async def test_remove_file(async_sandbox: AsyncSandbox):
|
||||
filename = "test_remove.txt"
|
||||
content = "This file will be removed."
|
||||
|
||||
await async_sandbox.files.write(filename, content)
|
||||
|
||||
await async_sandbox.files.remove(filename)
|
||||
|
||||
exists = await async_sandbox.files.exists(filename)
|
||||
assert not exists
|
||||
|
||||
|
||||
async def test_remove_non_existing_file(async_sandbox: AsyncSandbox):
|
||||
filename = "non_existing_file.txt"
|
||||
await async_sandbox.files.remove(filename)
|
||||
@@ -0,0 +1,29 @@
|
||||
import pytest
|
||||
|
||||
from e2b import FileNotFoundException, AsyncSandbox
|
||||
|
||||
|
||||
async def test_rename_file(async_sandbox: AsyncSandbox):
|
||||
old_filename = "test_rename_old.txt"
|
||||
new_filename = "test_rename_new.txt"
|
||||
content = "This file will be renamed."
|
||||
|
||||
await async_sandbox.files.write(old_filename, content)
|
||||
info = await async_sandbox.files.rename(old_filename, new_filename)
|
||||
assert info.path == f"/home/user/{new_filename}"
|
||||
|
||||
exists_old = await async_sandbox.files.exists(old_filename)
|
||||
exists_new = await async_sandbox.files.exists(new_filename)
|
||||
assert not exists_old
|
||||
assert exists_new
|
||||
|
||||
read_content = await async_sandbox.files.read(new_filename)
|
||||
assert read_content == content
|
||||
|
||||
|
||||
async def test_rename_non_existing_file(async_sandbox: AsyncSandbox):
|
||||
old_filename = "non_existing_file.txt"
|
||||
new_filename = "new_non_existing_file.txt"
|
||||
|
||||
with pytest.raises(FileNotFoundException):
|
||||
await async_sandbox.files.rename(old_filename, new_filename)
|
||||
@@ -0,0 +1,60 @@
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
import json
|
||||
import pytest
|
||||
|
||||
from e2b.sandbox_async.main import AsyncSandbox
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_download_url_with_signing(async_sandbox: AsyncSandbox):
|
||||
file_path = "test_download_url_with_signing.txt"
|
||||
file_content = "This file will be watched."
|
||||
|
||||
await async_sandbox.files.write(file_path, file_content)
|
||||
signed_url = async_sandbox.download_url(file_path, "user")
|
||||
|
||||
with urllib.request.urlopen(signed_url) as resp:
|
||||
assert resp.status == 200
|
||||
body_bytes = resp.read()
|
||||
body_text = body_bytes.decode()
|
||||
assert body_text == file_content
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_download_url_with_signing_and_expiration(async_sandbox: AsyncSandbox):
|
||||
file_path = "test_download_url_with_signing.txt"
|
||||
file_content = "This file will be watched."
|
||||
|
||||
await async_sandbox.files.write(file_path, file_content)
|
||||
signed_url = async_sandbox.download_url(file_path, "user", 120)
|
||||
|
||||
with urllib.request.urlopen(signed_url) as resp:
|
||||
assert resp.status == 200
|
||||
body_bytes = resp.read()
|
||||
body_text = body_bytes.decode()
|
||||
assert body_text == file_content
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_download_url_with_expired_signing(async_sandbox: AsyncSandbox):
|
||||
file_path = "test_download_url_with_signing.txt"
|
||||
file_content = "This file will be watched."
|
||||
|
||||
await async_sandbox.files.write(file_path, file_content)
|
||||
|
||||
signed_url = async_sandbox.download_url(
|
||||
file_path, "user", use_signature_expiration=-120
|
||||
)
|
||||
|
||||
with pytest.raises(urllib.error.HTTPError) as exc_info:
|
||||
urllib.request.urlopen(signed_url)
|
||||
|
||||
err = exc_info.value
|
||||
assert err.code == 401, f"Unexpected status {err.code}"
|
||||
|
||||
error_json_str = err.read().decode() # bytes ➜ str
|
||||
error_payload = json.loads(error_json_str) # str ➜ dict
|
||||
|
||||
expected_payload = {"code": 401, "message": "signature is already expired"}
|
||||
assert error_payload == expected_payload
|
||||
@@ -0,0 +1,185 @@
|
||||
import pytest
|
||||
|
||||
from asyncio import Event
|
||||
|
||||
from e2b import (
|
||||
FileNotFoundException,
|
||||
AsyncSandbox,
|
||||
FilesystemEvent,
|
||||
FilesystemEventType,
|
||||
FileType,
|
||||
SandboxException,
|
||||
)
|
||||
|
||||
|
||||
async def test_watch_directory_changes_with_entry_info(async_sandbox: AsyncSandbox):
|
||||
dirname = "test_watch_dir_entry"
|
||||
filename = "test_watch.txt"
|
||||
content = "This file will be watched."
|
||||
new_content = "This file has been modified."
|
||||
|
||||
await async_sandbox.files.make_dir(dirname)
|
||||
await async_sandbox.files.write(f"{dirname}/{filename}", content)
|
||||
|
||||
event_triggered = Event()
|
||||
received: list[FilesystemEvent] = []
|
||||
|
||||
def handle_event(e: FilesystemEvent):
|
||||
if e.type == FilesystemEventType.WRITE and e.name == filename:
|
||||
received.append(e)
|
||||
event_triggered.set()
|
||||
|
||||
handle = await async_sandbox.files.watch_dir(
|
||||
dirname, on_event=handle_event, include_entry=True
|
||||
)
|
||||
|
||||
await async_sandbox.files.write(f"{dirname}/{filename}", new_content)
|
||||
|
||||
await event_triggered.wait()
|
||||
|
||||
write_event = received[0]
|
||||
# The entry is populated best-effort for events where the path still exists.
|
||||
assert write_event.entry is not None
|
||||
assert write_event.entry.name == filename
|
||||
assert write_event.entry.path == f"/home/user/{dirname}/{filename}"
|
||||
assert write_event.entry.type == FileType.FILE
|
||||
|
||||
await handle.stop()
|
||||
|
||||
|
||||
async def test_watch_directory_changes_with_network_mounts_allowed(
|
||||
async_sandbox: AsyncSandbox,
|
||||
):
|
||||
dirname = "test_watch_dir_network_mounts"
|
||||
filename = "test_watch.txt"
|
||||
content = "This file will be watched."
|
||||
new_content = "This file has been modified."
|
||||
|
||||
await async_sandbox.files.make_dir(dirname)
|
||||
await async_sandbox.files.write(f"{dirname}/{filename}", content)
|
||||
|
||||
event_triggered = Event()
|
||||
|
||||
def handle_event(e: FilesystemEvent):
|
||||
if e.type == FilesystemEventType.WRITE and e.name == filename:
|
||||
event_triggered.set()
|
||||
|
||||
# The flag only lifts the network-mount restriction — watching a regular
|
||||
# directory must work the same with it enabled.
|
||||
handle = await async_sandbox.files.watch_dir(
|
||||
dirname, on_event=handle_event, allow_network_mounts=True
|
||||
)
|
||||
|
||||
await async_sandbox.files.write(f"{dirname}/{filename}", new_content)
|
||||
|
||||
await event_triggered.wait()
|
||||
|
||||
await handle.stop()
|
||||
|
||||
|
||||
async def test_watch_directory_changes(async_sandbox: AsyncSandbox):
|
||||
dirname = "test_watch_dir"
|
||||
filename = "test_watch.txt"
|
||||
content = "This file will be watched."
|
||||
new_content = "This file has been modified."
|
||||
|
||||
await async_sandbox.files.make_dir(dirname)
|
||||
await async_sandbox.files.write(f"{dirname}/{filename}", content)
|
||||
|
||||
event_triggered = Event()
|
||||
|
||||
def handle_event(e: FilesystemEvent):
|
||||
if e.type == FilesystemEventType.WRITE and e.name == filename:
|
||||
event_triggered.set()
|
||||
|
||||
handle = await async_sandbox.files.watch_dir(dirname, on_event=handle_event)
|
||||
|
||||
await async_sandbox.files.write(f"{dirname}/{filename}", new_content)
|
||||
|
||||
await event_triggered.wait()
|
||||
|
||||
await handle.stop()
|
||||
|
||||
|
||||
async def test_watch_recursive_directory_changes(async_sandbox: AsyncSandbox):
|
||||
dirname = "test_recursive_watch_dir"
|
||||
nested_dirname = "test_nested_watch_dir"
|
||||
filename = "test_watch.txt"
|
||||
content = "This file will be watched."
|
||||
|
||||
await async_sandbox.files.remove(dirname)
|
||||
await async_sandbox.files.make_dir(f"{dirname}/{nested_dirname}")
|
||||
|
||||
event_triggered = Event()
|
||||
|
||||
expected_filename = f"{nested_dirname}/{filename}"
|
||||
|
||||
def handle_event(e: FilesystemEvent):
|
||||
if e.type == FilesystemEventType.WRITE and e.name == expected_filename:
|
||||
event_triggered.set()
|
||||
|
||||
handle = await async_sandbox.files.watch_dir(
|
||||
dirname, on_event=handle_event, recursive=True
|
||||
)
|
||||
|
||||
await async_sandbox.files.write(f"{dirname}/{nested_dirname}/{filename}", content)
|
||||
|
||||
await event_triggered.wait()
|
||||
|
||||
await handle.stop()
|
||||
|
||||
|
||||
async def test_watch_recursive_directory_after_nested_folder_addition(
|
||||
async_sandbox: AsyncSandbox,
|
||||
):
|
||||
dirname = "test_recursive_watch_dir_add"
|
||||
nested_dirname = "test_nested_watch_dir"
|
||||
filename = "test_watch.txt"
|
||||
content = "This file will be watched."
|
||||
|
||||
await async_sandbox.files.remove(dirname)
|
||||
await async_sandbox.files.make_dir(dirname)
|
||||
|
||||
event_triggered_file = Event()
|
||||
event_triggered_folder = Event()
|
||||
|
||||
expected_filename = f"{nested_dirname}/{filename}"
|
||||
|
||||
def handle_event(e: FilesystemEvent):
|
||||
if e.type == FilesystemEventType.WRITE and e.name == expected_filename:
|
||||
event_triggered_file.set()
|
||||
return
|
||||
if e.type == FilesystemEventType.CREATE and e.name == nested_dirname:
|
||||
event_triggered_folder.set()
|
||||
|
||||
handle = await async_sandbox.files.watch_dir(
|
||||
dirname, on_event=handle_event, recursive=True
|
||||
)
|
||||
|
||||
await async_sandbox.files.make_dir(f"{dirname}/{nested_dirname}")
|
||||
await event_triggered_folder.wait()
|
||||
|
||||
await async_sandbox.files.write(f"{dirname}/{nested_dirname}/{filename}", content)
|
||||
await event_triggered_file.wait()
|
||||
|
||||
await handle.stop()
|
||||
|
||||
|
||||
async def test_watch_non_existing_directory(async_sandbox: AsyncSandbox):
|
||||
dirname = "non_existing_watch_dir"
|
||||
|
||||
with pytest.raises(FileNotFoundException):
|
||||
await async_sandbox.files.watch_dir(dirname, on_event=lambda e: None)
|
||||
|
||||
|
||||
async def test_watch_file(async_sandbox: AsyncSandbox):
|
||||
filename = "test_watch.txt"
|
||||
await async_sandbox.files.write(filename, "This file will be watched.")
|
||||
|
||||
with pytest.raises(SandboxException):
|
||||
await async_sandbox.files.watch_dir(filename, on_event=lambda e: None)
|
||||
|
||||
|
||||
async def test_watch_file_with_secured_envd(async_sandbox):
|
||||
await async_sandbox.files.watch_dir("/home/user/", on_event=lambda e: None)
|
||||
await async_sandbox.files.write("test_watch.txt", "This file will be watched.")
|
||||
@@ -0,0 +1,220 @@
|
||||
import io
|
||||
import uuid
|
||||
|
||||
from e2b import AsyncSandbox
|
||||
from e2b.sandbox.filesystem.filesystem import FileType, WriteEntry
|
||||
from e2b.sandbox_async.filesystem.filesystem import WriteInfo
|
||||
|
||||
|
||||
async def test_write_text_file(async_sandbox: AsyncSandbox, debug):
|
||||
filename = "test_write.txt"
|
||||
content = "This is a test file."
|
||||
|
||||
info = await async_sandbox.files.write(filename, content)
|
||||
assert info.path == f"/home/user/{filename}"
|
||||
assert info.type == FileType.FILE
|
||||
|
||||
exists = await async_sandbox.files.exists(filename)
|
||||
assert exists
|
||||
|
||||
read_content = await async_sandbox.files.read(filename)
|
||||
assert read_content == content
|
||||
|
||||
if debug:
|
||||
await async_sandbox.files.remove(filename)
|
||||
|
||||
|
||||
async def test_write_binary_file(async_sandbox: AsyncSandbox, debug):
|
||||
filename = "test_write.bin"
|
||||
text = "This is a test binary file."
|
||||
# equivalent to `open("path/to/local/file", "rb")`
|
||||
content = io.BytesIO(text.encode("utf-8"))
|
||||
|
||||
info = await async_sandbox.files.write(filename, content)
|
||||
assert info.path == f"/home/user/{filename}"
|
||||
|
||||
exists = await async_sandbox.files.exists(filename)
|
||||
assert exists
|
||||
|
||||
read_content = await async_sandbox.files.read(filename)
|
||||
assert read_content == text
|
||||
|
||||
if debug:
|
||||
await async_sandbox.files.remove(filename)
|
||||
|
||||
|
||||
async def test_write_multiple_files(async_sandbox: AsyncSandbox, debug):
|
||||
num_test_files = 10
|
||||
|
||||
# Attempt to write with empty files array
|
||||
empty_info = await async_sandbox.files.write_files([])
|
||||
assert isinstance(empty_info, list)
|
||||
assert len(empty_info) == 0
|
||||
|
||||
# Attempt to write with one file in array
|
||||
one_file_path = "one_test_file.txt"
|
||||
info = await async_sandbox.files.write_files(
|
||||
[WriteEntry(path=one_file_path, data="This is a test file.")]
|
||||
)
|
||||
|
||||
assert isinstance(info, list)
|
||||
assert len(info) == 1
|
||||
info = info[0]
|
||||
assert isinstance(info, WriteInfo)
|
||||
assert info.path == "/home/user/one_test_file.txt"
|
||||
exists = await async_sandbox.files.exists(info.path)
|
||||
assert exists
|
||||
|
||||
read_content = await async_sandbox.files.read(info.path)
|
||||
assert read_content == "This is a test file."
|
||||
|
||||
# Attempt to write with multiple files in array
|
||||
files = []
|
||||
for i in range(num_test_files):
|
||||
path = f"test_write_{i}.txt"
|
||||
content = f"This is a test file {i}."
|
||||
files.append(WriteEntry(path=path, data=content))
|
||||
|
||||
infos = await async_sandbox.files.write_files(files)
|
||||
assert isinstance(infos, list)
|
||||
assert len(infos) == len(files)
|
||||
for i, info in enumerate(infos):
|
||||
assert isinstance(info, WriteInfo)
|
||||
assert info.path == f"/home/user/test_write_{i}.txt"
|
||||
assert info.type == FileType.FILE
|
||||
exists = await async_sandbox.files.exists(path)
|
||||
assert exists
|
||||
|
||||
read_content = await async_sandbox.files.read(info.path)
|
||||
assert read_content == files[i]["data"]
|
||||
|
||||
if debug:
|
||||
await async_sandbox.files.remove(one_file_path)
|
||||
for i in range(num_test_files):
|
||||
await async_sandbox.files.remove(f"test_write_{i}.txt")
|
||||
|
||||
|
||||
async def test_overwrite_file(async_sandbox: AsyncSandbox, debug):
|
||||
filename = "test_overwrite.txt"
|
||||
initial_content = "Initial content."
|
||||
new_content = "New content."
|
||||
|
||||
await async_sandbox.files.write(filename, initial_content)
|
||||
await async_sandbox.files.write(filename, new_content)
|
||||
read_content = await async_sandbox.files.read(filename)
|
||||
assert read_content == new_content
|
||||
|
||||
if debug:
|
||||
await async_sandbox.files.remove(filename)
|
||||
|
||||
|
||||
async def test_write_to_non_existing_directory(async_sandbox: AsyncSandbox, debug):
|
||||
filename = f"non_existing_dir_{uuid.uuid4()}/test_write.txt"
|
||||
content = "This should succeed too."
|
||||
|
||||
await async_sandbox.files.write(filename, content)
|
||||
exists = await async_sandbox.files.exists(filename)
|
||||
assert exists
|
||||
|
||||
read_content = await async_sandbox.files.read(filename)
|
||||
assert read_content == content
|
||||
|
||||
if debug:
|
||||
await async_sandbox.files.remove(filename)
|
||||
|
||||
|
||||
async def test_write_with_secured_envd(async_sandbox_factory):
|
||||
filename = f"non_existing_dir_{uuid.uuid4()}/test_write.txt"
|
||||
content = "This should succeed too."
|
||||
|
||||
sbx = await async_sandbox_factory(timeout=30, secure=True)
|
||||
|
||||
assert await sbx.is_running()
|
||||
assert sbx._envd_version is not None
|
||||
assert sbx._envd_access_token is not None
|
||||
|
||||
await sbx.files.write(filename, content)
|
||||
|
||||
exists = await sbx.files.exists(filename)
|
||||
assert exists
|
||||
|
||||
read_content = await sbx.files.read(filename)
|
||||
assert read_content == content
|
||||
|
||||
|
||||
async def test_write_files_with_different_data_types(
|
||||
async_sandbox: AsyncSandbox, debug
|
||||
):
|
||||
text_data = "Text string data"
|
||||
bytes_data = b"Bytes data"
|
||||
bytes_io_data = io.BytesIO(b"BytesIO data")
|
||||
string_io_data = io.StringIO("StringIO data")
|
||||
|
||||
files = [
|
||||
WriteEntry(path="writefiles_text.txt", data=text_data),
|
||||
WriteEntry(path="writefiles_bytes.bin", data=bytes_data),
|
||||
WriteEntry(path="writefiles_bytesio.bin", data=bytes_io_data),
|
||||
WriteEntry(path="writefiles_stringio.txt", data=string_io_data),
|
||||
]
|
||||
|
||||
infos = await async_sandbox.files.write_files(files)
|
||||
|
||||
assert len(infos) == 4
|
||||
|
||||
text_content = await async_sandbox.files.read("writefiles_text.txt")
|
||||
assert text_content == text_data
|
||||
|
||||
bytes_content = await async_sandbox.files.read("writefiles_bytes.bin")
|
||||
assert bytes_content == "Bytes data"
|
||||
|
||||
bytes_io_content = await async_sandbox.files.read("writefiles_bytesio.bin")
|
||||
assert bytes_io_content == "BytesIO data"
|
||||
|
||||
string_io_content = await async_sandbox.files.read("writefiles_stringio.txt")
|
||||
assert string_io_content == "StringIO data"
|
||||
|
||||
if debug:
|
||||
for file in files:
|
||||
await async_sandbox.files.remove(file["path"])
|
||||
|
||||
|
||||
async def test_write_io_with_octet_stream(async_sandbox: AsyncSandbox, debug):
|
||||
filename = "test_write_octet_io.bin"
|
||||
text = "Streamed octet-stream upload. " * 10_000
|
||||
content = io.BytesIO(text.encode("utf-8"))
|
||||
|
||||
info = await async_sandbox.files.write(filename, content, use_octet_stream=True)
|
||||
assert info.path == f"/home/user/{filename}"
|
||||
|
||||
read_content = await async_sandbox.files.read(filename)
|
||||
assert read_content == text
|
||||
|
||||
if debug:
|
||||
await async_sandbox.files.remove(filename)
|
||||
|
||||
|
||||
async def test_write_text_io_with_octet_stream(async_sandbox: AsyncSandbox, debug):
|
||||
filename = "test_write_octet_text_io.txt"
|
||||
text = "Streamed text octet-stream upload."
|
||||
|
||||
await async_sandbox.files.write(filename, io.StringIO(text), use_octet_stream=True)
|
||||
|
||||
read_content = await async_sandbox.files.read(filename)
|
||||
assert read_content == text
|
||||
|
||||
if debug:
|
||||
await async_sandbox.files.remove(filename)
|
||||
|
||||
|
||||
async def test_write_io_with_octet_stream_and_gzip(async_sandbox: AsyncSandbox, debug):
|
||||
filename = "test_write_octet_io_gzip.bin"
|
||||
text = "Streamed gzipped octet-stream upload. " * 10_000
|
||||
content = io.BytesIO(text.encode("utf-8"))
|
||||
|
||||
await async_sandbox.files.write(filename, content, use_octet_stream=True, gzip=True)
|
||||
|
||||
read_content = await async_sandbox.files.read(filename)
|
||||
assert read_content == text
|
||||
|
||||
if debug:
|
||||
await async_sandbox.files.remove(filename)
|
||||
@@ -0,0 +1,41 @@
|
||||
import asyncio
|
||||
|
||||
from e2b import AsyncSandbox
|
||||
from e2b.sandbox.commands.command_handle import PtySize
|
||||
|
||||
|
||||
async def test_connect_to_pty(async_sandbox: AsyncSandbox):
|
||||
output1 = []
|
||||
output2 = []
|
||||
|
||||
def append_data(data: list, x: bytes):
|
||||
data.append(x.decode("utf-8"))
|
||||
|
||||
# First, create a terminal and disconnect the on_data handler
|
||||
terminal = await async_sandbox.pty.create(
|
||||
PtySize(80, 24),
|
||||
on_data=lambda x: append_data(output1, x),
|
||||
envs={"FOO": "bar"},
|
||||
)
|
||||
|
||||
await async_sandbox.pty.send_stdin(terminal.pid, b"echo $FOO\n")
|
||||
|
||||
# Give time for the command output in the first connection
|
||||
await asyncio.sleep(0.3)
|
||||
|
||||
await terminal.disconnect()
|
||||
|
||||
# Now connect again, with a new on_data handler
|
||||
reconnect_handle = await async_sandbox.pty.connect(
|
||||
terminal.pid, on_data=lambda x: append_data(output2, x)
|
||||
)
|
||||
|
||||
await async_sandbox.pty.send_stdin(terminal.pid, b"echo $FOO\nexit\n")
|
||||
|
||||
await reconnect_handle.wait()
|
||||
|
||||
assert terminal.pid == reconnect_handle.pid
|
||||
assert reconnect_handle.exit_code == 0
|
||||
|
||||
assert "bar" in "".join(output1)
|
||||
assert "bar" in "".join(output2)
|
||||
@@ -0,0 +1,21 @@
|
||||
from e2b import AsyncSandbox
|
||||
from e2b.sandbox.commands.command_handle import PtySize
|
||||
|
||||
|
||||
async def test_pty_create(async_sandbox: AsyncSandbox):
|
||||
output = []
|
||||
|
||||
def append_data(data: list, x: bytes):
|
||||
data.append(x.decode("utf-8"))
|
||||
|
||||
terminal = await async_sandbox.pty.create(
|
||||
PtySize(80, 24), on_data=lambda x: append_data(output, x), envs={"ABC": "123"}
|
||||
)
|
||||
|
||||
await async_sandbox.pty.send_stdin(terminal.pid, b"echo $ABC\n")
|
||||
await async_sandbox.pty.send_stdin(terminal.pid, b"exit\n")
|
||||
|
||||
await terminal.wait()
|
||||
assert terminal.exit_code == 0
|
||||
|
||||
assert "123" in "".join(output)
|
||||
@@ -0,0 +1,20 @@
|
||||
import pytest
|
||||
|
||||
from e2b import AsyncSandbox, CommandExitException
|
||||
from e2b.sandbox.commands.command_handle import PtySize
|
||||
|
||||
|
||||
async def test_kill_pty(async_sandbox: AsyncSandbox):
|
||||
terminal = await async_sandbox.pty.create(PtySize(80, 24), on_data=lambda _: None)
|
||||
|
||||
assert await async_sandbox.pty.kill(terminal.pid)
|
||||
|
||||
# The PTY process should no longer be running.
|
||||
with pytest.raises(CommandExitException):
|
||||
await async_sandbox.commands.run(f"kill -0 {terminal.pid}")
|
||||
|
||||
|
||||
async def test_kill_non_existing_pty(async_sandbox: AsyncSandbox):
|
||||
non_existing_pid = 999999
|
||||
|
||||
assert not await async_sandbox.pty.kill(non_existing_pid)
|
||||
@@ -0,0 +1,34 @@
|
||||
from e2b import AsyncSandbox
|
||||
from e2b.sandbox.commands.command_handle import PtySize
|
||||
|
||||
|
||||
async def test_resize(async_sandbox: AsyncSandbox):
|
||||
output = []
|
||||
|
||||
def append_data(data: list, x: bytes):
|
||||
data.append(x.decode("utf-8"))
|
||||
|
||||
terminal = await async_sandbox.pty.create(
|
||||
PtySize(cols=80, rows=24), on_data=lambda x: append_data(output, x)
|
||||
)
|
||||
|
||||
await async_sandbox.pty.send_stdin(terminal.pid, b"tput cols\n")
|
||||
await async_sandbox.pty.send_stdin(terminal.pid, b"exit\n")
|
||||
await terminal.wait()
|
||||
assert terminal.exit_code == 0
|
||||
|
||||
assert "80" in "".join(output)
|
||||
|
||||
output = []
|
||||
|
||||
terminal = await async_sandbox.pty.create(
|
||||
PtySize(cols=80, rows=24), on_data=lambda x: append_data(output, x)
|
||||
)
|
||||
|
||||
await async_sandbox.pty.resize(terminal.pid, PtySize(cols=100, rows=24))
|
||||
await async_sandbox.pty.send_stdin(terminal.pid, b"tput cols\n")
|
||||
await async_sandbox.pty.send_stdin(terminal.pid, b"exit\n")
|
||||
|
||||
await terminal.wait()
|
||||
assert terminal.exit_code == 0
|
||||
assert "100" in "".join(output)
|
||||
@@ -0,0 +1,11 @@
|
||||
from e2b import AsyncSandbox
|
||||
from e2b.sandbox.commands.command_handle import PtySize
|
||||
|
||||
|
||||
async def test_send_input(async_sandbox: AsyncSandbox):
|
||||
terminal = await async_sandbox.pty.create(
|
||||
PtySize(cols=80, rows=24), on_data=lambda x: print(x)
|
||||
)
|
||||
await async_sandbox.pty.send_stdin(terminal.pid, b"exit\n")
|
||||
await terminal.wait()
|
||||
assert terminal.exit_code == 0
|
||||
@@ -0,0 +1,128 @@
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
from packaging.version import Version
|
||||
|
||||
from e2b import AsyncSandbox
|
||||
from e2b.api import SandboxCreateResponse
|
||||
from e2b.connection_config import ConnectionConfig
|
||||
import e2b.sandbox_async.main as sandbox_async_main
|
||||
|
||||
BASE_DOMAIN = "base.e2b.dev"
|
||||
BASE_REQUEST_TIMEOUT = 11
|
||||
BASE_DEBUG = False
|
||||
BASE_HEADERS = {"X-Test": "base"}
|
||||
|
||||
|
||||
def create_sandbox(monkeypatch, api_key: str) -> AsyncSandbox:
|
||||
dummy_transport = SimpleNamespace(pool=object())
|
||||
|
||||
monkeypatch.setattr(
|
||||
sandbox_async_main, "get_transport", lambda *_args, **_kwargs: dummy_transport
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
sandbox_async_main.httpx, "AsyncClient", lambda *args, **kwargs: object()
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
sandbox_async_main, "Filesystem", lambda *args, **kwargs: object()
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
sandbox_async_main, "Commands", lambda *args, **kwargs: object()
|
||||
)
|
||||
monkeypatch.setattr(sandbox_async_main, "Pty", lambda *args, **kwargs: object())
|
||||
monkeypatch.setattr(sandbox_async_main, "Git", lambda *args, **kwargs: object())
|
||||
|
||||
return AsyncSandbox(
|
||||
sandbox_id="sbx-test",
|
||||
sandbox_domain="sandbox.e2b.dev",
|
||||
envd_version=Version("0.2.4"),
|
||||
envd_access_token="tok",
|
||||
traffic_access_token="tok",
|
||||
connection_config=ConnectionConfig(
|
||||
api_key=api_key,
|
||||
domain=BASE_DOMAIN,
|
||||
request_timeout=BASE_REQUEST_TIMEOUT,
|
||||
debug=BASE_DEBUG,
|
||||
api_headers=BASE_HEADERS,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_pause_passes_connection_config_without_overrides(
|
||||
monkeypatch, test_api_key
|
||||
):
|
||||
mock_pause = AsyncMock(return_value="sbx-test")
|
||||
monkeypatch.setattr(sandbox_async_main.SandboxApi, "_cls_pause", mock_pause)
|
||||
|
||||
sandbox = create_sandbox(monkeypatch, test_api_key)
|
||||
await sandbox.pause()
|
||||
|
||||
mock_pause.assert_awaited_once()
|
||||
assert mock_pause.call_args.kwargs["sandbox_id"] == "sbx-test"
|
||||
assert mock_pause.call_args.kwargs["api_key"] == test_api_key
|
||||
assert mock_pause.call_args.kwargs["domain"] == BASE_DOMAIN
|
||||
assert mock_pause.call_args.kwargs["request_timeout"] == BASE_REQUEST_TIMEOUT
|
||||
assert mock_pause.call_args.kwargs["debug"] == BASE_DEBUG
|
||||
assert mock_pause.call_args.kwargs["headers"]["X-Test"] == BASE_HEADERS["X-Test"]
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_pause_applies_overrides(monkeypatch, test_api_key):
|
||||
mock_pause = AsyncMock(return_value="sbx-test")
|
||||
monkeypatch.setattr(sandbox_async_main.SandboxApi, "_cls_pause", mock_pause)
|
||||
|
||||
sandbox = create_sandbox(monkeypatch, test_api_key)
|
||||
await sandbox.pause(
|
||||
domain="override.e2b.dev",
|
||||
request_timeout=20,
|
||||
api_headers={"X-Extra": "1"},
|
||||
)
|
||||
|
||||
mock_pause.assert_awaited_once()
|
||||
assert mock_pause.call_args.kwargs["sandbox_id"] == "sbx-test"
|
||||
assert mock_pause.call_args.kwargs["api_key"] == test_api_key
|
||||
assert mock_pause.call_args.kwargs["domain"] == "override.e2b.dev"
|
||||
assert mock_pause.call_args.kwargs["request_timeout"] == 20
|
||||
assert mock_pause.call_args.kwargs["debug"] == BASE_DEBUG
|
||||
assert mock_pause.call_args.kwargs["headers"]["X-Test"] == BASE_HEADERS["X-Test"]
|
||||
assert mock_pause.call_args.kwargs["headers"]["X-Extra"] == "1"
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_connect_sets_stable_host_routing_headers(monkeypatch, test_api_key):
|
||||
mock_connect = AsyncMock(
|
||||
return_value=SandboxCreateResponse(
|
||||
sandbox_id="sbx-test",
|
||||
sandbox_domain="e2b.app",
|
||||
envd_version="0.4.0",
|
||||
envd_access_token="tok",
|
||||
traffic_access_token="traffic",
|
||||
)
|
||||
)
|
||||
monkeypatch.setattr(sandbox_async_main.SandboxApi, "_cls_connect", mock_connect)
|
||||
|
||||
monkeypatch.setattr(ConnectionConfig, "_integration", "testing/version")
|
||||
config = ConnectionConfig(
|
||||
api_key=test_api_key,
|
||||
headers=BASE_HEADERS,
|
||||
)
|
||||
sandbox = await AsyncSandbox.connect(
|
||||
"sbx-test",
|
||||
**config.get_api_params(),
|
||||
)
|
||||
|
||||
assert sandbox.envd_api_url == "https://sandbox.e2b.app"
|
||||
assert "X-Test" not in sandbox.connection_config.sandbox_headers
|
||||
assert sandbox.connection_config.sandbox_headers["User-Agent"].startswith(
|
||||
"e2b-python-sdk/"
|
||||
)
|
||||
assert sandbox.connection_config.sandbox_headers["User-Agent"].endswith(
|
||||
" testing/version"
|
||||
)
|
||||
assert sandbox.connection_config.sandbox_headers["E2b-Sandbox-Id"] == "sbx-test"
|
||||
assert sandbox.connection_config.sandbox_headers["E2b-Sandbox-Port"] == str(
|
||||
ConnectionConfig.envd_port
|
||||
)
|
||||
assert sandbox.connection_config.sandbox_headers["X-Access-Token"] == "tok"
|
||||
@@ -0,0 +1,145 @@
|
||||
import uuid
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
|
||||
from e2b import AsyncSandbox
|
||||
from e2b.api.client.api.sandboxes import post_sandboxes_sandbox_id_connect
|
||||
from e2b.api.client.models import Sandbox as SandboxModel
|
||||
import e2b.sandbox_async.main as sandbox_async_main
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_connect(async_sandbox_factory):
|
||||
sbx = await async_sandbox_factory(timeout=10)
|
||||
|
||||
assert await sbx.is_running()
|
||||
|
||||
sbx_connection = await AsyncSandbox.connect(sbx.sandbox_id)
|
||||
assert await sbx_connection.is_running()
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_connect_with_secure(async_sandbox_factory):
|
||||
dir_name = f"test_directory_{uuid.uuid4()}"
|
||||
|
||||
sbx = await async_sandbox_factory(timeout=10, secure=True)
|
||||
assert await sbx.is_running()
|
||||
|
||||
sbx_connection = await AsyncSandbox.connect(sbx.sandbox_id)
|
||||
|
||||
await sbx_connection.files.make_dir(dir_name)
|
||||
files = await sbx_connection.files.list(dir_name)
|
||||
assert len(files) == 0
|
||||
assert await sbx_connection.is_running()
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_connect_to_paused_sandbox_resumes(async_sandbox):
|
||||
await async_sandbox.pause()
|
||||
assert not await async_sandbox.is_running()
|
||||
|
||||
resumed = await AsyncSandbox.connect(async_sandbox.sandbox_id)
|
||||
assert await resumed.is_running()
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_resume_does_not_shorten_timeout_on_running_sandbox(
|
||||
async_sandbox_factory,
|
||||
):
|
||||
# Create sandbox with a 300 second timeout
|
||||
sbx = await async_sandbox_factory(timeout=300)
|
||||
assert await sbx.is_running()
|
||||
|
||||
# Get initial info to check end_at
|
||||
info_before = await AsyncSandbox.get_info(sbx.sandbox_id)
|
||||
|
||||
# Connect with a shorter timeout (10 seconds)
|
||||
await AsyncSandbox.connect(sbx.sandbox_id, timeout=10)
|
||||
|
||||
# Get info after connection
|
||||
info_after = await AsyncSandbox.get_info(sbx.sandbox_id)
|
||||
|
||||
# The end_at time should not have been shortened. It should be the same
|
||||
assert info_after.end_at == info_before.end_at, (
|
||||
f"Timeout was changed: before={info_before.end_at}, after={info_after.end_at}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_connect_extends_timeout_on_running_sandbox(async_sandbox):
|
||||
# Create sandbox with a short timeout
|
||||
assert await async_sandbox.is_running()
|
||||
|
||||
# Get initial info to check end_at
|
||||
info_before = await async_sandbox.get_info()
|
||||
|
||||
# Connect with a longer timeout
|
||||
await AsyncSandbox.connect(async_sandbox.sandbox_id, timeout=600)
|
||||
|
||||
# Get info after connection
|
||||
info_after = await AsyncSandbox.get_info(async_sandbox.sandbox_id)
|
||||
|
||||
# The end_at time should have been extended
|
||||
assert info_after.end_at > info_before.end_at, (
|
||||
f"Timeout was not extended: before={info_before.end_at}, after={info_after.end_at}"
|
||||
)
|
||||
|
||||
|
||||
async def test_connect_in_debug_mode_does_not_call_api(monkeypatch, test_api_key):
|
||||
mock_connect = AsyncMock()
|
||||
monkeypatch.setattr(sandbox_async_main.SandboxApi, "_cls_connect", mock_connect)
|
||||
|
||||
sbx = await AsyncSandbox.connect("sbx-debug", debug=True, api_key=test_api_key)
|
||||
|
||||
mock_connect.assert_not_called()
|
||||
assert sbx.sandbox_id == "sbx-debug"
|
||||
assert sbx._envd_access_token is None
|
||||
assert sbx.traffic_access_token is None
|
||||
|
||||
|
||||
async def test_connect_in_env_debug_mode_does_not_call_api(monkeypatch, test_api_key):
|
||||
monkeypatch.setenv("E2B_DEBUG", "true")
|
||||
mock_connect = AsyncMock()
|
||||
monkeypatch.setattr(sandbox_async_main.SandboxApi, "_cls_connect", mock_connect)
|
||||
|
||||
sbx = await AsyncSandbox.connect("sbx-debug", api_key=test_api_key)
|
||||
|
||||
mock_connect.assert_not_called()
|
||||
assert sbx.sandbox_id == "sbx-debug"
|
||||
|
||||
|
||||
async def test_instance_connect_in_debug_mode_does_not_call_api(
|
||||
monkeypatch, test_api_key
|
||||
):
|
||||
mock_connect = AsyncMock()
|
||||
monkeypatch.setattr(sandbox_async_main.SandboxApi, "_cls_connect", mock_connect)
|
||||
|
||||
sbx = await AsyncSandbox.connect("sbx-debug", debug=True, api_key=test_api_key)
|
||||
|
||||
assert await sbx.connect() is sbx
|
||||
mock_connect.assert_not_called()
|
||||
|
||||
|
||||
async def test_connect_normalizes_unset_tokens(monkeypatch, test_api_key):
|
||||
# Tokens and domain are absent in the API response for non-secure sandboxes
|
||||
model = SandboxModel(
|
||||
client_id="client-id",
|
||||
envd_version="0.2.4",
|
||||
sandbox_id="sbx-test",
|
||||
template_id="template-id",
|
||||
)
|
||||
mock_request = AsyncMock(
|
||||
return_value=SimpleNamespace(status_code=200, parsed=model)
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
post_sandboxes_sandbox_id_connect, "asyncio_detailed", mock_request
|
||||
)
|
||||
|
||||
sbx = await AsyncSandbox.connect("sbx-test", debug=False, api_key=test_api_key)
|
||||
|
||||
mock_request.assert_called_once()
|
||||
assert sbx._envd_access_token is None
|
||||
assert sbx.traffic_access_token is None
|
||||
assert "signature" not in sbx.download_url("test.txt")
|
||||
@@ -0,0 +1,194 @@
|
||||
import asyncio
|
||||
from typing import Any, cast
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from e2b import AsyncSandbox, SandboxQuery, SandboxState
|
||||
from e2b.api.client.models import (
|
||||
NewSandbox,
|
||||
SandboxAutoResumeConfig,
|
||||
)
|
||||
from e2b.exceptions import InvalidArgumentException
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_start(async_sandbox):
|
||||
assert await async_sandbox.is_running()
|
||||
assert async_sandbox._envd_version is not None
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_metadata(async_sandbox_factory):
|
||||
sbx = await async_sandbox_factory(timeout=5, metadata={"test-key": "test-value"})
|
||||
|
||||
paginator = AsyncSandbox.list(
|
||||
query=SandboxQuery(metadata={"test-key": "test-value"})
|
||||
)
|
||||
sandboxes = await paginator.next_items()
|
||||
|
||||
for sbx_info in sandboxes:
|
||||
if sbx.sandbox_id == sbx_info.sandbox_id:
|
||||
assert sbx_info.metadata is not None
|
||||
assert sbx_info.metadata["test-key"] == "test-value"
|
||||
break
|
||||
else:
|
||||
assert False, "Sandbox not found"
|
||||
|
||||
|
||||
def test_create_payload_serializes_auto_resume_enabled():
|
||||
body = NewSandbox(
|
||||
template_id="template-id",
|
||||
auto_pause=True,
|
||||
auto_resume=SandboxAutoResumeConfig(enabled=True),
|
||||
)
|
||||
|
||||
assert body.to_dict()["autoPause"] is True
|
||||
assert body.to_dict()["autoResume"] == {"enabled": True}
|
||||
|
||||
|
||||
def test_create_payload_deserializes_auto_resume_enabled():
|
||||
body = NewSandbox.from_dict(
|
||||
{
|
||||
"templateID": "template-id",
|
||||
"autoPause": False,
|
||||
"autoResume": {"enabled": False},
|
||||
}
|
||||
)
|
||||
|
||||
assert isinstance(body.auto_resume, SandboxAutoResumeConfig)
|
||||
assert body.auto_resume.to_dict() == {"enabled": False}
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_filesystem_only_auto_pause_rejects_auto_resume():
|
||||
# A filesystem-only auto-pause snapshot can only be resumed explicitly, so
|
||||
# combining keep_memory=False with auto_resume is rejected client-side.
|
||||
with pytest.raises(InvalidArgumentException):
|
||||
await AsyncSandbox.create(
|
||||
timeout=3,
|
||||
lifecycle={
|
||||
"on_timeout": {"action": "pause", "keep_memory": False},
|
||||
"auto_resume": True,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_keep_memory_not_allowed_with_kill():
|
||||
# The discriminated union forbids keep_memory on action="kill" at type-check
|
||||
# time; the runtime guard rejects it for callers that bypass the type
|
||||
# (cast(Any, ...) feeds the deliberately type-invalid input).
|
||||
with pytest.raises(InvalidArgumentException):
|
||||
await AsyncSandbox.create(
|
||||
timeout=3,
|
||||
lifecycle=cast(
|
||||
Any, {"on_timeout": {"action": "kill", "keep_memory": False}}
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_invalid_on_timeout_type_does_not_crash(async_sandbox_factory):
|
||||
# An untyped/invalid on_timeout (e.g. None) must not crash create; it falls
|
||||
# back to kill semantics like a missing on_timeout (the sandbox just starts).
|
||||
sbx = await async_sandbox_factory(
|
||||
timeout=10, lifecycle=cast(Any, {"on_timeout": None})
|
||||
)
|
||||
assert await sbx.is_running()
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_keep_memory_none_defaults_to_full_memory(async_sandbox_factory):
|
||||
# An explicit None keep_memory must default to full memory (not filesystem-only):
|
||||
# the timeout auto-pause then resumes the SAME sandbox in place (memory restore),
|
||||
# so the boot id is unchanged. A changed boot id would mean None was wrongly
|
||||
# treated as filesystem-only (cold boot).
|
||||
sbx = await async_sandbox_factory(
|
||||
timeout=60,
|
||||
lifecycle={"on_timeout": {"action": "pause", "keep_memory": None}},
|
||||
)
|
||||
boot_before = (await sbx.files.read("/proc/sys/kernel/random/boot_id")).strip()
|
||||
|
||||
await sbx.set_timeout(0) # force the timeout auto-pause now
|
||||
for _ in range(150):
|
||||
if not await sbx.is_running():
|
||||
break
|
||||
await asyncio.sleep(0.2)
|
||||
assert not await sbx.is_running()
|
||||
|
||||
resumed = await sbx.connect()
|
||||
assert resumed.sandbox_id == sbx.sandbox_id # same sandbox
|
||||
boot_after = (await resumed.files.read("/proc/sys/kernel/random/boot_id")).strip()
|
||||
assert boot_after == boot_before # memory restore in place, not a cold boot
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_auto_pause_filesystem_only_reboots(async_sandbox_factory):
|
||||
# keep_memory=False makes the timeout auto-pause filesystem-only, so resuming
|
||||
# cold-boots the sandbox from disk.
|
||||
sandbox = await async_sandbox_factory(
|
||||
timeout=3,
|
||||
lifecycle={"on_timeout": {"action": "pause", "keep_memory": False}},
|
||||
)
|
||||
|
||||
marker = "auto-pause-fs-only"
|
||||
await sandbox.files.write("/home/user/auto-pause-marker.txt", marker)
|
||||
boot_before = (await sandbox.files.read("/proc/sys/kernel/random/boot_id")).strip()
|
||||
|
||||
await asyncio.sleep(5)
|
||||
|
||||
assert (await sandbox.get_info()).state == SandboxState.PAUSED
|
||||
|
||||
# A filesystem-only snapshot cannot auto-resume on traffic; connect resumes
|
||||
# it by cold-booting.
|
||||
resumed = await sandbox.connect()
|
||||
|
||||
persisted = (await resumed.files.read("/home/user/auto-pause-marker.txt")).strip()
|
||||
assert persisted == marker
|
||||
|
||||
boot_after = (await resumed.files.read("/proc/sys/kernel/random/boot_id")).strip()
|
||||
assert boot_after != boot_before
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_auto_pause_without_auto_resume_requires_connect(async_sandbox_factory):
|
||||
sandbox = await async_sandbox_factory(
|
||||
timeout=3,
|
||||
lifecycle={"on_timeout": "pause", "auto_resume": False},
|
||||
)
|
||||
|
||||
await asyncio.sleep(5)
|
||||
|
||||
assert (await sandbox.get_info()).state == SandboxState.PAUSED
|
||||
assert not await sandbox.is_running()
|
||||
|
||||
await sandbox.connect()
|
||||
|
||||
assert (await sandbox.get_info()).state == SandboxState.RUNNING
|
||||
assert await sandbox.is_running()
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_auto_resume_wakes_on_http_request(async_sandbox_factory):
|
||||
sandbox = await async_sandbox_factory(
|
||||
timeout=3,
|
||||
lifecycle={"on_timeout": "pause", "auto_resume": True},
|
||||
)
|
||||
|
||||
cmd = await sandbox.commands.run("python3 -m http.server 8000", background=True)
|
||||
try:
|
||||
await asyncio.sleep(5)
|
||||
|
||||
url = f"https://{sandbox.get_host(8000)}"
|
||||
async with httpx.AsyncClient(timeout=15.0) as client:
|
||||
res = await client.get(url)
|
||||
|
||||
assert res.status_code == 200
|
||||
assert (await sandbox.get_info()).state == SandboxState.RUNNING
|
||||
assert await sandbox.is_running()
|
||||
finally:
|
||||
try:
|
||||
await cmd.kill()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,30 @@
|
||||
import asyncio
|
||||
|
||||
import httpx
|
||||
|
||||
from e2b import AsyncSandbox
|
||||
|
||||
|
||||
async def test_ping_server(async_sandbox: AsyncSandbox, debug, helpers):
|
||||
cmd = await async_sandbox.commands.run(
|
||||
"python -m http.server 8000",
|
||||
background=True,
|
||||
)
|
||||
|
||||
disable = helpers.catch_cmd_exit_error_in_background(cmd)
|
||||
|
||||
try:
|
||||
host = async_sandbox.get_host(8000)
|
||||
|
||||
status_code = None
|
||||
async with httpx.AsyncClient() as client:
|
||||
for _ in range(20):
|
||||
res = await client.get(f"{'http' if debug else 'https'}://{host}")
|
||||
status_code = res.status_code
|
||||
if res.status_code == 200:
|
||||
break
|
||||
await asyncio.sleep(0.5)
|
||||
assert status_code == 200
|
||||
disable()
|
||||
finally:
|
||||
await cmd.kill()
|
||||
@@ -0,0 +1,42 @@
|
||||
import pytest
|
||||
|
||||
from e2b.sandbox.commands.command_handle import CommandExitException
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_internet_access_enabled(async_sandbox_factory):
|
||||
"""Test that sandbox with internet access enabled can reach external websites."""
|
||||
sbx = await async_sandbox_factory(allow_internet_access=True)
|
||||
|
||||
# Test internet connectivity by making a curl request to a reliable external site
|
||||
result = await sbx.commands.run(
|
||||
"curl -s -o /dev/null -w '%{http_code}' https://connectivitycheck.gstatic.com/generate_204"
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
assert result.stdout.strip() == "204"
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_internet_access_disabled(async_sandbox_factory):
|
||||
"""Test that sandbox with internet access disabled cannot reach external websites."""
|
||||
sbx = await async_sandbox_factory(allow_internet_access=False)
|
||||
|
||||
# Test that internet connectivity is blocked by making a curl request
|
||||
with pytest.raises(CommandExitException) as exc_info:
|
||||
await sbx.commands.run(
|
||||
"curl --connect-timeout 3 --max-time 5 -Is https://connectivitycheck.gstatic.com/generate_204"
|
||||
)
|
||||
# The command should fail or timeout when internet access is disabled
|
||||
assert exc_info.value.exit_code != 0
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_internet_access_default(async_sandbox):
|
||||
"""Test that sandbox with default settings (no explicit allow_internet_access) has internet access."""
|
||||
# Test internet connectivity by making a curl request to a reliable external site
|
||||
|
||||
result = await async_sandbox.commands.run(
|
||||
"curl -s -o /dev/null -w '%{http_code}' https://connectivitycheck.gstatic.com/generate_204"
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
assert result.stdout.strip() == "204"
|
||||
@@ -0,0 +1,16 @@
|
||||
import pytest
|
||||
|
||||
from e2b import AsyncSandbox, SandboxQuery, SandboxState
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_kill(async_sandbox: AsyncSandbox, sandbox_test_id: str):
|
||||
await async_sandbox.kill()
|
||||
|
||||
paginator = AsyncSandbox.list(
|
||||
query=SandboxQuery(
|
||||
state=[SandboxState.RUNNING], metadata={"sandbox_test_id": sandbox_test_id}
|
||||
)
|
||||
)
|
||||
sandboxes = await paginator.next_items()
|
||||
assert async_sandbox.sandbox_id not in [s.sandbox_id for s in sandboxes]
|
||||
@@ -0,0 +1,62 @@
|
||||
import asyncio
|
||||
import datetime
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
@pytest.mark.timeout(60)
|
||||
async def test_sbx_metrics(async_sandbox_factory):
|
||||
sbx = await async_sandbox_factory(timeout=60)
|
||||
|
||||
# Wait for the sandbox to have some metrics
|
||||
metrics = []
|
||||
for _ in range(60):
|
||||
metrics = await sbx.get_metrics()
|
||||
if len(metrics) > 0:
|
||||
break
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
assert len(metrics) > 0
|
||||
|
||||
metric = metrics[0]
|
||||
assert metric.cpu_count is not None
|
||||
assert metric.cpu_used_pct is not None
|
||||
assert metric.mem_used is not None
|
||||
assert metric.mem_total is not None
|
||||
assert metric.disk_used is not None
|
||||
assert metric.disk_total is not None
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
@pytest.mark.timeout(60)
|
||||
async def test_sbx_metrics_time_range(async_sandbox_factory):
|
||||
start_time = datetime.datetime.now(datetime.timezone.utc)
|
||||
sbx = await async_sandbox_factory(timeout=60)
|
||||
|
||||
# Wait for the sandbox to have some metrics within the test's time window
|
||||
metrics = []
|
||||
end_time = start_time
|
||||
for _ in range(60):
|
||||
end_time = datetime.datetime.now(datetime.timezone.utc)
|
||||
metrics = await sbx.get_metrics(start=start_time, end=end_time)
|
||||
if len(metrics) > 0:
|
||||
break
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
assert len(metrics) > 0
|
||||
|
||||
# All returned metrics must fall within the requested time range
|
||||
# (10s slack - metric timestamps are aligned to collection buckets,
|
||||
# currently 5s, and the query params are second-precision)
|
||||
slack = 10
|
||||
for metric in metrics:
|
||||
assert metric.timestamp.timestamp() >= start_time.timestamp() - slack
|
||||
assert metric.timestamp.timestamp() <= end_time.timestamp() + slack
|
||||
|
||||
# A time range from before the sandbox existed must return no metrics
|
||||
metrics = await sbx.get_metrics(
|
||||
start=start_time - datetime.timedelta(hours=1),
|
||||
end=start_time - datetime.timedelta(minutes=30),
|
||||
)
|
||||
assert len(metrics) == 0
|
||||
@@ -0,0 +1,318 @@
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from e2b import SandboxNetworkOpts
|
||||
from e2b.sandbox.commands.command_handle import CommandExitException
|
||||
|
||||
|
||||
async def wait_for_status(
|
||||
client: httpx.AsyncClient,
|
||||
url: str,
|
||||
status_code: int,
|
||||
headers: dict[str, str] | None = None,
|
||||
timeout: float = 15,
|
||||
) -> httpx.Response:
|
||||
deadline = asyncio.get_running_loop().time() + timeout
|
||||
response: httpx.Response | None = None
|
||||
|
||||
while asyncio.get_running_loop().time() < deadline:
|
||||
response = await client.get(url, headers=headers, follow_redirects=True)
|
||||
if response.status_code == status_code:
|
||||
return response
|
||||
await asyncio.sleep(1)
|
||||
|
||||
assert response is not None
|
||||
return response
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_allow_specific_ip_with_deny_all(async_sandbox_factory):
|
||||
"""Test that sandbox with denyOut all and allowOut creates a whitelist."""
|
||||
async_sandbox = await async_sandbox_factory(
|
||||
network=SandboxNetworkOpts(
|
||||
deny_out=lambda ctx: [ctx.all_traffic], allow_out=["1.1.1.1"]
|
||||
)
|
||||
)
|
||||
|
||||
# Test that allowed IP works
|
||||
result = await async_sandbox.commands.run(
|
||||
"curl -s -o /dev/null -w '%{http_code}' https://1.1.1.1"
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
assert result.stdout.strip() == "301"
|
||||
|
||||
# Test that other IPs are denied
|
||||
with pytest.raises(CommandExitException) as exc_info:
|
||||
await async_sandbox.commands.run(
|
||||
"curl --connect-timeout 3 --max-time 5 -Is https://8.8.8.8"
|
||||
)
|
||||
assert exc_info.value.exit_code != 0
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_deny_specific_ip(async_sandbox_factory):
|
||||
"""Test that sandbox with denyOut denies specified IP addresses."""
|
||||
async_sandbox = await async_sandbox_factory(
|
||||
network=SandboxNetworkOpts(deny_out=["8.8.8.8"])
|
||||
)
|
||||
|
||||
# Test that denied IP fails
|
||||
with pytest.raises(CommandExitException) as exc_info:
|
||||
await async_sandbox.commands.run(
|
||||
"curl --connect-timeout 3 --max-time 5 -Is https://8.8.8.8"
|
||||
)
|
||||
assert exc_info.value.exit_code != 0
|
||||
|
||||
# Test that other IPs work
|
||||
result = await async_sandbox.commands.run(
|
||||
"curl -s -o /dev/null -w '%{http_code}' https://1.1.1.1"
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
assert result.stdout.strip() == "301"
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_deny_all_traffic(async_sandbox_factory):
|
||||
"""Test that sandbox can deny all traffic using the all_traffic selector."""
|
||||
async_sandbox = await async_sandbox_factory(
|
||||
network=SandboxNetworkOpts(deny_out=lambda ctx: [ctx.all_traffic]), timeout=30
|
||||
)
|
||||
|
||||
# Test that all traffic is denied
|
||||
with pytest.raises(CommandExitException) as exc_info:
|
||||
await async_sandbox.commands.run(
|
||||
"curl --connect-timeout 3 --max-time 5 -Is https://1.1.1.1"
|
||||
)
|
||||
assert exc_info.value.exit_code != 0
|
||||
|
||||
with pytest.raises(CommandExitException) as exc_info:
|
||||
await async_sandbox.commands.run(
|
||||
"curl --connect-timeout 3 --max-time 5 -Is https://8.8.8.8"
|
||||
)
|
||||
assert exc_info.value.exit_code != 0
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_allow_takes_precedence_over_deny(async_sandbox_factory):
|
||||
"""Test that allowOut takes precedence over denyOut."""
|
||||
async_sandbox = await async_sandbox_factory(
|
||||
network=SandboxNetworkOpts(
|
||||
deny_out=lambda ctx: [ctx.all_traffic], allow_out=["1.1.1.1", "8.8.8.8"]
|
||||
)
|
||||
)
|
||||
|
||||
# Test that 1.1.1.1 works (explicitly allowed)
|
||||
result1 = await async_sandbox.commands.run(
|
||||
"curl -s -o /dev/null -w '%{http_code}' https://1.1.1.1"
|
||||
)
|
||||
assert result1.exit_code == 0
|
||||
assert result1.stdout.strip() == "301"
|
||||
|
||||
# Test that 8.8.8.8 also works (explicitly allowed, takes precedence over deny_out)
|
||||
result2 = await async_sandbox.commands.run(
|
||||
"curl -s -o /dev/null -w '%{http_code}' https://8.8.8.8"
|
||||
)
|
||||
assert result2.exit_code == 0
|
||||
assert result2.stdout.strip() == "302"
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_allow_public_traffic_false(async_sandbox_factory):
|
||||
"""Test that sandbox with allow_public_traffic=False requires traffic access token."""
|
||||
async_sandbox = await async_sandbox_factory(
|
||||
secure=True, network=SandboxNetworkOpts(allow_public_traffic=False)
|
||||
)
|
||||
|
||||
# Verify the sandbox was created successfully and has a traffic access token
|
||||
assert async_sandbox.traffic_access_token is not None
|
||||
|
||||
# Start a simple HTTP server in the sandbox
|
||||
port = 8080
|
||||
await async_sandbox.commands.run(
|
||||
f"python3 -m http.server {port}", background=True, timeout=0
|
||||
)
|
||||
|
||||
# Wait for server to start
|
||||
await asyncio.sleep(3)
|
||||
|
||||
# Get the public URL for the sandbox
|
||||
sandbox_url = f"https://{async_sandbox.get_host(port)}"
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
# Test 1: Request without traffic access token should fail with 403
|
||||
response = await client.get(sandbox_url, follow_redirects=True)
|
||||
assert response.status_code == 403
|
||||
|
||||
# Test 2: Request with valid traffic access token should succeed
|
||||
headers = {"e2b-traffic-access-token": async_sandbox.traffic_access_token}
|
||||
response = await wait_for_status(client, sandbox_url, 200, headers=headers)
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_allow_public_traffic_true(async_sandbox_factory):
|
||||
"""Test that sandbox with allow_public_traffic=True works without token."""
|
||||
async_sandbox = await async_sandbox_factory(
|
||||
network=SandboxNetworkOpts(allow_public_traffic=True)
|
||||
)
|
||||
|
||||
# Start a simple HTTP server in the sandbox
|
||||
port = 8080
|
||||
await async_sandbox.commands.run(
|
||||
f"python3 -m http.server {port}", background=True, timeout=0
|
||||
)
|
||||
|
||||
# Wait for server to start
|
||||
await asyncio.sleep(3)
|
||||
|
||||
# Get the public URL for the sandbox
|
||||
sandbox_url = f"https://{async_sandbox.get_host(port)}"
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
# Request without traffic access token should succeed (public access enabled)
|
||||
response = await wait_for_status(client, sandbox_url, 200)
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_firewall_transform_injects_headers(async_sandbox_factory):
|
||||
"""Test that a firewall rule with a transform injects headers into outbound requests."""
|
||||
injected_header = "X-E2B-Test-Token"
|
||||
injected_value = "e2b-transform-value-123"
|
||||
|
||||
network: SandboxNetworkOpts = {
|
||||
"rules": {
|
||||
"httpbin.e2b.team": [
|
||||
{"transform": {"headers": {injected_header: injected_value}}},
|
||||
],
|
||||
},
|
||||
}
|
||||
async_sandbox = await async_sandbox_factory(network=network)
|
||||
|
||||
result = await async_sandbox.commands.run(
|
||||
"curl -sS --max-time 10 https://httpbin.e2b.team/headers"
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
|
||||
parsed = json.loads(result.stdout)
|
||||
reflected = parsed["headers"].get(injected_header)
|
||||
assert reflected == injected_value, (
|
||||
f"expected httpbin to reflect {injected_header}={injected_value}, "
|
||||
f"got headers: {parsed['headers']}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_update_network_applies_restrictions(async_sandbox_factory):
|
||||
"""update_network can add egress restrictions to a running sandbox."""
|
||||
async_sandbox = await async_sandbox_factory()
|
||||
|
||||
# Baseline: 8.8.8.8 reachable.
|
||||
before = await async_sandbox.commands.run(
|
||||
"curl -s -o /dev/null -w '%{http_code}' https://8.8.8.8"
|
||||
)
|
||||
assert before.exit_code == 0
|
||||
|
||||
await async_sandbox.update_network({"deny_out": ["8.8.8.8"]})
|
||||
|
||||
# 8.8.8.8 is now denied.
|
||||
with pytest.raises(CommandExitException) as exc_info:
|
||||
await async_sandbox.commands.run(
|
||||
"curl --connect-timeout 3 --max-time 5 -Is https://8.8.8.8"
|
||||
)
|
||||
assert exc_info.value.exit_code != 0
|
||||
|
||||
# Other destinations stay reachable.
|
||||
result = await async_sandbox.commands.run(
|
||||
"curl -s -o /dev/null -w '%{http_code}' https://1.1.1.1"
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_update_network_clears_existing_rules(async_sandbox_factory):
|
||||
"""update_network replaces all egress rules; omitted fields are cleared."""
|
||||
async_sandbox = await async_sandbox_factory(
|
||||
network=SandboxNetworkOpts(
|
||||
deny_out=lambda ctx: [ctx.all_traffic],
|
||||
allow_out=["1.1.1.1"],
|
||||
)
|
||||
)
|
||||
|
||||
# Baseline from create-time config: 8.8.8.8 denied.
|
||||
with pytest.raises(CommandExitException):
|
||||
await async_sandbox.commands.run(
|
||||
"curl --connect-timeout 3 --max-time 5 -Is https://8.8.8.8"
|
||||
)
|
||||
|
||||
# Empty update clears allow_out / deny_out entirely.
|
||||
await async_sandbox.update_network({})
|
||||
|
||||
r1 = await async_sandbox.commands.run(
|
||||
"curl -s -o /dev/null -w '%{http_code}' https://1.1.1.1"
|
||||
)
|
||||
assert r1.exit_code == 0
|
||||
|
||||
r2 = await async_sandbox.commands.run(
|
||||
"curl -s -o /dev/null -w '%{http_code}' https://8.8.8.8"
|
||||
)
|
||||
assert r2.exit_code == 0
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_mask_request_host(async_sandbox_factory):
|
||||
"""Test that mask_request_host modifies the Host header correctly."""
|
||||
async_sandbox = await async_sandbox_factory(
|
||||
network=SandboxNetworkOpts(mask_request_host="custom-host.example.com:${PORT}"),
|
||||
timeout=60,
|
||||
)
|
||||
|
||||
import asyncio
|
||||
|
||||
import httpx
|
||||
|
||||
port = 8080
|
||||
output_file = "/tmp/headers.txt"
|
||||
|
||||
# Start a Python HTTP server that captures request headers and writes them to a file
|
||||
await async_sandbox.commands.run(
|
||||
f"""python3 -c "
|
||||
import http.server, json
|
||||
class H(http.server.BaseHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
with open('{output_file}', 'w') as f:
|
||||
for k, v in self.headers.items():
|
||||
f.write(k + ': ' + v + chr(10))
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
def log_message(self, *a): pass
|
||||
http.server.HTTPServer(('', {port}), H).handle_request()
|
||||
" """,
|
||||
background=True,
|
||||
)
|
||||
|
||||
await asyncio.sleep(2)
|
||||
|
||||
# Get the public URL for the sandbox
|
||||
sandbox_url = f"https://{async_sandbox.get_host(port)}"
|
||||
|
||||
# Make a request from OUTSIDE the sandbox through the proxy
|
||||
# The Host header should be modified according to mask_request_host
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
await client.get(sandbox_url, timeout=5.0)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
await asyncio.sleep(1)
|
||||
|
||||
# Read the captured headers from inside the sandbox
|
||||
result = await async_sandbox.commands.run(f"cat {output_file}")
|
||||
|
||||
# Verify the Host header was modified according to mask_request_host
|
||||
assert "Host:" in result.stdout
|
||||
assert "custom-host.example.com" in result.stdout
|
||||
assert str(port) in result.stdout
|
||||
@@ -0,0 +1,26 @@
|
||||
import pytest
|
||||
|
||||
from e2b import AsyncSandbox
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_start_secured(async_sandbox_factory):
|
||||
sbx = await async_sandbox_factory(timeout=5, secure=True)
|
||||
|
||||
assert await sbx.is_running()
|
||||
assert sbx._envd_version is not None
|
||||
assert sbx._envd_access_token is not None
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_connect_to_secured(async_sandbox_factory):
|
||||
sbx = await async_sandbox_factory(timeout=100, secure=True)
|
||||
|
||||
assert await sbx.is_running()
|
||||
assert sbx._envd_version is not None
|
||||
assert sbx._envd_access_token is not None
|
||||
|
||||
sbx_connection = await AsyncSandbox.connect(sbx.sandbox_id)
|
||||
assert await sbx_connection.is_running()
|
||||
assert sbx_connection._envd_version is not None
|
||||
assert sbx_connection._envd_access_token is not None
|
||||
@@ -0,0 +1,15 @@
|
||||
import pytest
|
||||
from e2b import AsyncSandbox
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_snapshot(async_sandbox: AsyncSandbox):
|
||||
assert await async_sandbox.is_running()
|
||||
|
||||
await async_sandbox.pause()
|
||||
assert not await async_sandbox.is_running()
|
||||
|
||||
resumed_sandbox = await async_sandbox.connect()
|
||||
assert await async_sandbox.is_running()
|
||||
assert await resumed_sandbox.is_running()
|
||||
assert resumed_sandbox.sandbox_id == async_sandbox.sandbox_id
|
||||
@@ -0,0 +1,164 @@
|
||||
import pytest
|
||||
from e2b import AsyncSandbox
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_create_snapshot(async_sandbox: AsyncSandbox):
|
||||
snapshot = await async_sandbox.create_snapshot()
|
||||
|
||||
assert snapshot.snapshot_id
|
||||
assert len(snapshot.snapshot_id) > 0
|
||||
|
||||
await AsyncSandbox.delete_snapshot(snapshot.snapshot_id)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_create_sandbox_from_snapshot(async_sandbox: AsyncSandbox):
|
||||
test_content = "content from original sandbox"
|
||||
await async_sandbox.files.write("/home/user/test.txt", test_content)
|
||||
|
||||
snapshot = await async_sandbox.create_snapshot()
|
||||
|
||||
try:
|
||||
new_sandbox = await AsyncSandbox.create(snapshot.snapshot_id)
|
||||
|
||||
try:
|
||||
content = await new_sandbox.files.read("/home/user/test.txt")
|
||||
assert content == test_content
|
||||
finally:
|
||||
await new_sandbox.kill()
|
||||
finally:
|
||||
await AsyncSandbox.delete_snapshot(snapshot.snapshot_id)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_create_multiple_sandboxes_from_snapshot(async_sandbox: AsyncSandbox):
|
||||
test_content = "shared snapshot content"
|
||||
await async_sandbox.files.write("/home/user/shared.txt", test_content)
|
||||
|
||||
snapshot = await async_sandbox.create_snapshot()
|
||||
|
||||
try:
|
||||
sandbox1 = await AsyncSandbox.create(snapshot.snapshot_id)
|
||||
sandbox2 = await AsyncSandbox.create(snapshot.snapshot_id)
|
||||
|
||||
try:
|
||||
content1 = await sandbox1.files.read("/home/user/shared.txt")
|
||||
content2 = await sandbox2.files.read("/home/user/shared.txt")
|
||||
|
||||
assert content1 == test_content
|
||||
assert content2 == test_content
|
||||
|
||||
await sandbox1.files.write("/home/user/shared.txt", "modified in sandbox1")
|
||||
|
||||
modified_content = await sandbox1.files.read("/home/user/shared.txt")
|
||||
unchanged_content = await sandbox2.files.read("/home/user/shared.txt")
|
||||
|
||||
assert modified_content == "modified in sandbox1"
|
||||
assert unchanged_content == test_content
|
||||
finally:
|
||||
await sandbox1.kill()
|
||||
await sandbox2.kill()
|
||||
finally:
|
||||
await AsyncSandbox.delete_snapshot(snapshot.snapshot_id)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_list_snapshots(async_sandbox: AsyncSandbox):
|
||||
snapshot = await async_sandbox.create_snapshot()
|
||||
|
||||
try:
|
||||
paginator = AsyncSandbox.list_snapshots()
|
||||
assert paginator.has_next
|
||||
|
||||
snapshots = await paginator.next_items()
|
||||
assert isinstance(snapshots, list)
|
||||
|
||||
found = any(s.snapshot_id == snapshot.snapshot_id for s in snapshots)
|
||||
assert found
|
||||
finally:
|
||||
await AsyncSandbox.delete_snapshot(snapshot.snapshot_id)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_list_snapshots_for_sandbox(async_sandbox: AsyncSandbox):
|
||||
snapshot = await async_sandbox.create_snapshot()
|
||||
|
||||
try:
|
||||
paginator = AsyncSandbox.list_snapshots(
|
||||
sandbox_id=async_sandbox.sandbox_id,
|
||||
)
|
||||
snapshots = await paginator.next_items()
|
||||
|
||||
found = any(s.snapshot_id == snapshot.snapshot_id for s in snapshots)
|
||||
assert found
|
||||
finally:
|
||||
await AsyncSandbox.delete_snapshot(snapshot.snapshot_id)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_create_named_snapshot(async_sandbox: AsyncSandbox, sandbox_test_id: str):
|
||||
snapshot_name = f"snap-{sandbox_test_id}"
|
||||
|
||||
snapshot = await async_sandbox.create_snapshot(name=snapshot_name)
|
||||
|
||||
try:
|
||||
assert snapshot.snapshot_id
|
||||
assert isinstance(snapshot.names, list)
|
||||
assert len(snapshot.names) > 0
|
||||
assert any(snapshot_name in n for n in snapshot.names)
|
||||
finally:
|
||||
await AsyncSandbox.delete_snapshot(snapshot.snapshot_id)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_delete_snapshot(async_sandbox: AsyncSandbox):
|
||||
snapshot = await async_sandbox.create_snapshot()
|
||||
|
||||
deleted = await AsyncSandbox.delete_snapshot(snapshot.snapshot_id)
|
||||
assert deleted is True
|
||||
|
||||
deleted_again = await AsyncSandbox.delete_snapshot(snapshot.snapshot_id)
|
||||
assert deleted_again is False
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_snapshot_preserves_filesystem(async_sandbox: AsyncSandbox):
|
||||
app_dir = "/home/user/app"
|
||||
config_path = f"{app_dir}/config.json"
|
||||
config_content = '{"env": "test"}'
|
||||
data_path = f"{app_dir}/data.txt"
|
||||
data_content = "important data"
|
||||
|
||||
await async_sandbox.files.make_dir(app_dir)
|
||||
await async_sandbox.files.write(config_path, config_content)
|
||||
await async_sandbox.files.write(data_path, data_content)
|
||||
|
||||
snapshot = await async_sandbox.create_snapshot()
|
||||
|
||||
try:
|
||||
new_sandbox = await AsyncSandbox.create(snapshot.snapshot_id)
|
||||
|
||||
try:
|
||||
dir_exists = await new_sandbox.files.exists(app_dir)
|
||||
assert dir_exists
|
||||
|
||||
config = await new_sandbox.files.read(config_path)
|
||||
data = await new_sandbox.files.read(data_path)
|
||||
|
||||
assert config == config_content
|
||||
assert data == data_content
|
||||
finally:
|
||||
await new_sandbox.kill()
|
||||
finally:
|
||||
await AsyncSandbox.delete_snapshot(snapshot.snapshot_id)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_create_snapshot_class_method(async_sandbox: AsyncSandbox):
|
||||
snapshot = await AsyncSandbox.create_snapshot(async_sandbox.sandbox_id)
|
||||
|
||||
assert snapshot.snapshot_id
|
||||
assert len(snapshot.snapshot_id) > 0
|
||||
|
||||
await AsyncSandbox.delete_snapshot(snapshot.snapshot_id)
|
||||
@@ -0,0 +1,31 @@
|
||||
import pytest
|
||||
from e2b import AsyncSandbox
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_pause_filesystem_only(async_sandbox: AsyncSandbox):
|
||||
# A marker on the persisted rootfs and the kernel boot id before pausing.
|
||||
await async_sandbox.files.write("/home/user/fs-only-marker.txt", "persisted")
|
||||
boot_before = (
|
||||
await async_sandbox.files.read("/proc/sys/kernel/random/boot_id")
|
||||
).strip()
|
||||
|
||||
# Filesystem-only pause: only the rootfs is persisted, no memory snapshot.
|
||||
assert await async_sandbox.pause(keep_memory=False)
|
||||
assert not await async_sandbox.is_running()
|
||||
|
||||
# Resuming a filesystem-only snapshot cold-boots (reboots) from the rootfs.
|
||||
resumed = await async_sandbox.connect()
|
||||
assert await resumed.is_running()
|
||||
assert resumed.sandbox_id == async_sandbox.sandbox_id
|
||||
|
||||
# connect() returns the same handle, and its credentials stay valid across
|
||||
# the resume (the backend re-binds the same envd access token on the cold
|
||||
# boot). The rootfs survives the reboot...
|
||||
marker = (await resumed.files.read("/home/user/fs-only-marker.txt")).strip()
|
||||
assert marker == "persisted"
|
||||
|
||||
# ...while a fresh kernel boot id proves the guest cold-booted rather than
|
||||
# being restored from a memory snapshot.
|
||||
boot_after = (await resumed.files.read("/proc/sys/kernel/random/boot_id")).strip()
|
||||
assert boot_after != boot_before
|
||||
@@ -0,0 +1,30 @@
|
||||
import pytest
|
||||
from datetime import datetime
|
||||
|
||||
from time import sleep
|
||||
|
||||
from e2b import AsyncSandbox
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_shorten_timeout(async_sandbox: AsyncSandbox):
|
||||
await async_sandbox.set_timeout(5)
|
||||
sleep(6)
|
||||
|
||||
is_running = await async_sandbox.is_running()
|
||||
assert is_running is False
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_shorten_then_lengthen_timeout(async_sandbox: AsyncSandbox):
|
||||
await async_sandbox.set_timeout(5)
|
||||
sleep(1)
|
||||
await async_sandbox.set_timeout(10)
|
||||
sleep(6)
|
||||
await async_sandbox.is_running()
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_get_timeout(async_sandbox: AsyncSandbox):
|
||||
info = await async_sandbox.get_info()
|
||||
assert isinstance(info.end_at, datetime)
|
||||
@@ -0,0 +1,11 @@
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
||||
def pytest_collection_modifyitems(items):
|
||||
for item in items:
|
||||
if str(item.fspath).startswith(_DIR):
|
||||
item.add_marker(pytest.mark.timeout(180))
|
||||
@@ -0,0 +1,133 @@
|
||||
import pytest
|
||||
|
||||
from e2b import AsyncTemplate
|
||||
from e2b.template.types import InstructionType
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_from_dockerfile():
|
||||
dockerfile = """FROM node:24
|
||||
WORKDIR /app
|
||||
COPY package.json .
|
||||
RUN npm install
|
||||
ENTRYPOINT ["sleep", "20"]"""
|
||||
|
||||
template = AsyncTemplate().from_dockerfile(dockerfile)
|
||||
|
||||
# base image
|
||||
assert template._template._base_image == "node:24"
|
||||
|
||||
instructions = template._template._instructions
|
||||
|
||||
# Docker defaults
|
||||
assert instructions[1]["type"] == InstructionType.WORKDIR
|
||||
assert instructions[1]["args"][0] == "/"
|
||||
|
||||
# Instructions from Dockerfile
|
||||
assert instructions[2]["type"] == InstructionType.WORKDIR
|
||||
assert instructions[2]["args"][0] == "/app"
|
||||
|
||||
assert instructions[3]["type"] == InstructionType.COPY
|
||||
assert instructions[3]["args"][0] == "package.json"
|
||||
assert instructions[3]["args"][1] == "."
|
||||
|
||||
assert instructions[4]["type"] == InstructionType.RUN
|
||||
assert instructions[4]["args"][0] == "npm install"
|
||||
|
||||
# E2B defaults appended
|
||||
assert instructions[5]["type"] == InstructionType.USER
|
||||
assert instructions[5]["args"][0] == "user"
|
||||
|
||||
# Start command
|
||||
assert template._template._start_cmd == "sleep 20"
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_from_dockerfile_with_default_user_and_workdir():
|
||||
dockerfile = "FROM node:24"
|
||||
|
||||
template = AsyncTemplate().from_dockerfile(dockerfile)
|
||||
|
||||
assert template._template._instructions[-2]["type"] == InstructionType.USER
|
||||
assert template._template._instructions[-2]["args"][0] == "user"
|
||||
assert template._template._instructions[-1]["type"] == InstructionType.WORKDIR
|
||||
assert template._template._instructions[-1]["args"][0] == "/home/user"
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_from_dockerfile_with_custom_user_and_workdir():
|
||||
dockerfile = "FROM node:24\nUSER mish\nWORKDIR /home/mish"
|
||||
|
||||
template = AsyncTemplate().from_dockerfile(dockerfile)
|
||||
|
||||
assert template._template._instructions[-2]["type"] == InstructionType.USER
|
||||
assert template._template._instructions[-2]["args"][0] == "mish"
|
||||
assert template._template._instructions[-1]["type"] == InstructionType.WORKDIR
|
||||
assert template._template._instructions[-1]["args"][0] == "/home/mish"
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_from_dockerfile_with_multi_source_copy():
|
||||
dockerfile = """FROM node:24
|
||||
COPY file1.txt file2.txt file3.txt /dest/"""
|
||||
|
||||
template = AsyncTemplate().from_dockerfile(dockerfile)
|
||||
|
||||
instructions = template._template._instructions
|
||||
|
||||
copy_instructions = [i for i in instructions if i["type"] == InstructionType.COPY]
|
||||
|
||||
assert len(copy_instructions) == 3
|
||||
assert copy_instructions[0]["args"][0] == "file1.txt"
|
||||
assert copy_instructions[0]["args"][1] == "/dest/"
|
||||
assert copy_instructions[1]["args"][0] == "file2.txt"
|
||||
assert copy_instructions[1]["args"][1] == "/dest/"
|
||||
assert copy_instructions[2]["args"][0] == "file3.txt"
|
||||
assert copy_instructions[2]["args"][1] == "/dest/"
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_from_dockerfile_with_multi_source_copy_chown():
|
||||
dockerfile = """FROM node:24
|
||||
COPY --chown=myuser:mygroup pkg.json pkg-lock.json /app/"""
|
||||
|
||||
template = AsyncTemplate().from_dockerfile(dockerfile)
|
||||
|
||||
instructions = template._template._instructions
|
||||
|
||||
copy_instructions = [i for i in instructions if i["type"] == InstructionType.COPY]
|
||||
|
||||
assert len(copy_instructions) == 2
|
||||
assert copy_instructions[0]["args"][0] == "pkg.json"
|
||||
assert copy_instructions[0]["args"][1] == "/app/"
|
||||
assert copy_instructions[0]["args"][2] == "myuser:mygroup"
|
||||
assert copy_instructions[1]["args"][0] == "pkg-lock.json"
|
||||
assert copy_instructions[1]["args"][1] == "/app/"
|
||||
assert copy_instructions[1]["args"][2] == "myuser:mygroup"
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_from_dockerfile_with_copy_chown():
|
||||
dockerfile = """FROM node:24
|
||||
COPY --chown=myuser:mygroup app.js /app/
|
||||
COPY --chown=anotheruser config.json /config/"""
|
||||
|
||||
template = AsyncTemplate().from_dockerfile(dockerfile)
|
||||
|
||||
instructions = template._template._instructions
|
||||
|
||||
# First COPY instruction (after initial USER root and WORKDIR /)
|
||||
copy_instruction1 = instructions[2]
|
||||
assert copy_instruction1["type"] == InstructionType.COPY
|
||||
assert copy_instruction1["args"][0] == "app.js"
|
||||
assert copy_instruction1["args"][1] == "/app/"
|
||||
assert copy_instruction1["args"][2] == "myuser:mygroup" # user from --chown
|
||||
|
||||
# Second COPY instruction
|
||||
copy_instruction2 = instructions[3]
|
||||
assert copy_instruction2["type"] == InstructionType.COPY
|
||||
assert copy_instruction2["args"][0] == "config.json"
|
||||
assert copy_instruction2["args"][1] == "/config/"
|
||||
assert (
|
||||
copy_instruction2["args"][2] == "anotheruser"
|
||||
) # user from --chown (without group)
|
||||
@@ -0,0 +1,32 @@
|
||||
import pytest
|
||||
|
||||
from e2b import AsyncTemplate
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_make_symlink(async_build):
|
||||
template = (
|
||||
AsyncTemplate()
|
||||
.from_image("ubuntu:22.04")
|
||||
.skip_cache()
|
||||
.make_symlink(".bashrc", ".bashrc.local")
|
||||
.run_cmd('test "$(readlink .bashrc.local)" = ".bashrc"')
|
||||
)
|
||||
|
||||
await async_build(template)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_make_symlink_force(async_build):
|
||||
template = (
|
||||
AsyncTemplate()
|
||||
.from_image("ubuntu:22.04")
|
||||
.make_symlink(".bashrc", ".bashrc.local")
|
||||
.skip_cache()
|
||||
.make_symlink(
|
||||
".bashrc", ".bashrc.local", force=True
|
||||
) # Overwrite existing symlink
|
||||
.run_cmd('test "$(readlink .bashrc.local)" = ".bashrc"')
|
||||
)
|
||||
|
||||
await async_build(template)
|
||||
@@ -0,0 +1,40 @@
|
||||
import pytest
|
||||
|
||||
from e2b import AsyncTemplate
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_run_command(async_build):
|
||||
template = AsyncTemplate().from_image("ubuntu:22.04").skip_cache().run_cmd("ls -l")
|
||||
|
||||
await async_build(template)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_run_command_as_different_user(async_build):
|
||||
template = (
|
||||
AsyncTemplate()
|
||||
.from_image("ubuntu:22.04")
|
||||
.skip_cache()
|
||||
.run_cmd('test "$(whoami)" = "root"', user="root")
|
||||
)
|
||||
|
||||
await async_build(template)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_run_command_as_user_that_does_not_exist(async_build):
|
||||
template = (
|
||||
AsyncTemplate()
|
||||
.from_image("ubuntu:22.04")
|
||||
.skip_cache()
|
||||
.run_cmd("whoami", user="root123")
|
||||
)
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
await async_build(template)
|
||||
|
||||
assert (
|
||||
"failed to run command 'whoami': command failed: unauthenticated: invalid username: 'root123'"
|
||||
in str(exc_info.value)
|
||||
)
|
||||
@@ -0,0 +1,57 @@
|
||||
import pytest
|
||||
|
||||
from e2b import AsyncTemplate
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_to_dockerfile():
|
||||
template = (
|
||||
AsyncTemplate()
|
||||
.from_ubuntu_image("24.04")
|
||||
.copy("README.md", "/app/README.md")
|
||||
.run_cmd('echo "Hello, World!"')
|
||||
)
|
||||
|
||||
dockerfile = AsyncTemplate.to_dockerfile(template)
|
||||
|
||||
expected_dockerfile = """FROM ubuntu:24.04
|
||||
COPY README.md /app/README.md
|
||||
RUN echo "Hello, World!"
|
||||
"""
|
||||
assert dockerfile == expected_dockerfile
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_to_dockerfile_with_options():
|
||||
template = (
|
||||
AsyncTemplate()
|
||||
.from_ubuntu_image("24.04")
|
||||
.copy("README.md", "/app/README.md", user="root")
|
||||
.run_cmd('echo "Hello, World!"', user="root")
|
||||
)
|
||||
|
||||
dockerfile = AsyncTemplate.to_dockerfile(template)
|
||||
|
||||
expected_dockerfile = """FROM ubuntu:24.04
|
||||
COPY README.md /app/README.md
|
||||
RUN echo "Hello, World!"
|
||||
"""
|
||||
assert dockerfile == expected_dockerfile
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_to_dockerfile_with_env_instructions():
|
||||
template = (
|
||||
AsyncTemplate()
|
||||
.from_ubuntu_image("24.04")
|
||||
.set_envs({"NODE_ENV": "production", "PORT": "8080"})
|
||||
.set_envs({"DEBUG": "false"})
|
||||
)
|
||||
|
||||
dockerfile = AsyncTemplate.to_dockerfile(template)
|
||||
|
||||
expected_dockerfile = """FROM ubuntu:24.04
|
||||
ENV NODE_ENV=production PORT=8080
|
||||
ENV DEBUG=false
|
||||
"""
|
||||
assert dockerfile == expected_dockerfile
|
||||
@@ -0,0 +1,34 @@
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
|
||||
from e2b import AsyncTemplate, wait_for_timeout
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
@pytest.mark.timeout(10)
|
||||
async def test_build_in_background_should_start_build_and_return_info():
|
||||
"""Test that build_in_background returns immediately without waiting for build to complete."""
|
||||
template = (
|
||||
AsyncTemplate()
|
||||
.from_image("ubuntu:22.04")
|
||||
.skip_cache()
|
||||
.run_cmd("sleep 5") # Add a delay to ensure build takes time
|
||||
.set_start_cmd('echo "Hello"', wait_for_timeout(10_000))
|
||||
)
|
||||
|
||||
name = f"e2b-test:v1-{uuid.uuid4()}"
|
||||
|
||||
build_info = await AsyncTemplate.build_in_background(
|
||||
template,
|
||||
name,
|
||||
cpu_count=1,
|
||||
memory_mb=1024,
|
||||
)
|
||||
|
||||
# Should return quickly (within a few seconds), not wait for the full build
|
||||
assert build_info is not None
|
||||
|
||||
# Verify the build is actually running
|
||||
status = await AsyncTemplate.get_build_status(build_info)
|
||||
assert status.status.value == "building"
|
||||
@@ -0,0 +1,102 @@
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
|
||||
import pytest
|
||||
|
||||
from e2b import AsyncTemplate, default_build_logger, wait_for_timeout
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def setup_test_folder():
|
||||
test_dir = tempfile.mkdtemp(prefix="python_async_test_")
|
||||
folder_path = os.path.join(test_dir, "folder")
|
||||
|
||||
os.makedirs(folder_path, exist_ok=True)
|
||||
with open(os.path.join(folder_path, "test.txt"), "w") as f:
|
||||
f.write("This is a test file.")
|
||||
|
||||
# Create relative symlink
|
||||
symlink_path = os.path.join(folder_path, "symlink.txt")
|
||||
if os.path.exists(symlink_path):
|
||||
os.remove(symlink_path)
|
||||
os.symlink("test.txt", symlink_path)
|
||||
|
||||
# Create absolute symlink
|
||||
symlink2_path = os.path.join(folder_path, "symlink2.txt")
|
||||
if os.path.exists(symlink2_path):
|
||||
os.remove(symlink2_path)
|
||||
os.symlink(os.path.join(folder_path, "test.txt"), symlink2_path)
|
||||
|
||||
# Create a symlink to a file that does not exist
|
||||
symlink3_path = os.path.join(folder_path, "symlink3.txt")
|
||||
if os.path.exists(symlink3_path):
|
||||
os.remove(symlink3_path)
|
||||
os.symlink("12345test.txt", symlink3_path)
|
||||
|
||||
yield test_dir
|
||||
|
||||
# Cleanup
|
||||
shutil.rmtree(test_dir, ignore_errors=True)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_build_template(async_build, setup_test_folder):
|
||||
template = (
|
||||
AsyncTemplate(file_context_path=setup_test_folder)
|
||||
.from_base_image()
|
||||
.copy("folder/*", "folder", force_upload=True)
|
||||
.run_cmd("cat folder/test.txt")
|
||||
.set_workdir("/app")
|
||||
.set_start_cmd("echo 'Hello, world!'", wait_for_timeout(10_000))
|
||||
)
|
||||
|
||||
await async_build(template, skip_cache=True, on_build_logs=default_build_logger())
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_build_template_from_base_template(async_build):
|
||||
template = AsyncTemplate().from_template("base")
|
||||
await async_build(template, skip_cache=True, on_build_logs=default_build_logger())
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_build_template_with_symlinks(async_build, setup_test_folder):
|
||||
template = (
|
||||
AsyncTemplate(file_context_path=setup_test_folder)
|
||||
.from_image("ubuntu:22.04")
|
||||
.skip_cache()
|
||||
.copy("folder/*", "folder", force_upload=True)
|
||||
.run_cmd("cat folder/symlink.txt")
|
||||
)
|
||||
|
||||
await async_build(template)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_build_template_with_resolve_symlinks(async_build, setup_test_folder):
|
||||
template = (
|
||||
AsyncTemplate(file_context_path=setup_test_folder)
|
||||
.from_image("ubuntu:22.04")
|
||||
.skip_cache()
|
||||
.copy(
|
||||
"folder/symlink.txt",
|
||||
"folder/symlink.txt",
|
||||
force_upload=True,
|
||||
resolve_symlinks=True,
|
||||
)
|
||||
.run_cmd("cat folder/symlink.txt")
|
||||
)
|
||||
|
||||
await async_build(template)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_build_template_with_skip_cache(async_build, setup_test_folder):
|
||||
template = (
|
||||
AsyncTemplate(file_context_path=setup_test_folder)
|
||||
.skip_cache()
|
||||
.from_image("ubuntu:22.04")
|
||||
)
|
||||
|
||||
await async_build(template)
|
||||
@@ -0,0 +1,20 @@
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
|
||||
from e2b import AsyncTemplate
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_check_base_template_name_exists():
|
||||
"""Test that the base template name exists."""
|
||||
exists = await AsyncTemplate.exists("base")
|
||||
assert exists is True
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_check_non_existing_name():
|
||||
"""Test that a non-existing name returns False."""
|
||||
non_existing_name = f"nonexistent-{uuid.uuid4()}"
|
||||
exists = await AsyncTemplate.exists(non_existing_name)
|
||||
assert exists is False
|
||||
@@ -0,0 +1,426 @@
|
||||
import traceback
|
||||
from types import SimpleNamespace
|
||||
from typing import List, Optional
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
import linecache
|
||||
|
||||
from e2b import AsyncTemplate, CopyItem, wait_for_timeout
|
||||
from e2b.template.types import TemplateBuildStatus
|
||||
import e2b.template_async.main as template_async_main
|
||||
import e2b.template_async.build_api as build_api_mod
|
||||
|
||||
non_existent_path = "nonexistent/path"
|
||||
|
||||
# map template alias -> failed step index
|
||||
failure_map: dict[str, Optional[int]] = {
|
||||
"from_image": 0,
|
||||
"from_template": 0,
|
||||
"from_dockerfile": 0,
|
||||
"from_image_registry": 0,
|
||||
"from_aws_registry": 0,
|
||||
"from_gcp_registry": 0,
|
||||
"copy": None,
|
||||
"copy_items": None,
|
||||
# multi-source copy produces two COPY instructions (steps 1 and 2),
|
||||
# the run_cmd after it is step 3
|
||||
"multi_source_copy_second_source": 2,
|
||||
"multi_source_copy_next_step": 3,
|
||||
"copy_items_second_item": 2,
|
||||
"copy_items_next_step": 3,
|
||||
"remove": 1,
|
||||
"rename": 1,
|
||||
"make_dir": 1,
|
||||
"make_symlink": 1,
|
||||
"run_cmd": 1,
|
||||
"set_workdir": 1,
|
||||
"set_user": 1,
|
||||
"pip_install": 1,
|
||||
"npm_install": 1,
|
||||
"apt_install": 1,
|
||||
"git_clone": 1,
|
||||
"set_start_cmd": 1,
|
||||
"add_mcp_server": None,
|
||||
"beta_dev_container_prebuild": 1,
|
||||
"beta_set_dev_container_start": 1,
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_template_build(monkeypatch):
|
||||
async def mock_request_build(
|
||||
client, name: str, tags: Optional[List[str]], cpu_count: int, memory_mb: int
|
||||
):
|
||||
return SimpleNamespace(template_id=name, build_id=str(uuid4()), tags=tags or [])
|
||||
|
||||
async def mock_trigger_build(client, template_id: str, build_id: str, template):
|
||||
return None
|
||||
|
||||
async def mock_get_file_upload_link(
|
||||
client, template_id: str, files_hash: str, stack_trace=None
|
||||
):
|
||||
return SimpleNamespace(present=True, url=None)
|
||||
|
||||
async def mock_get_build_status(
|
||||
client, template_id: str, build_id: str, logs_offset: int
|
||||
):
|
||||
step = failure_map[template_id]
|
||||
reason = SimpleNamespace(
|
||||
message="Mocked API build error",
|
||||
log_entries=[],
|
||||
step=str(step) if step is not None else None,
|
||||
)
|
||||
return SimpleNamespace(
|
||||
status=TemplateBuildStatus.ERROR,
|
||||
log_entries=[],
|
||||
reason=reason,
|
||||
)
|
||||
|
||||
monkeypatch.setattr(template_async_main, "request_build", mock_request_build)
|
||||
monkeypatch.setattr(template_async_main, "trigger_build", mock_trigger_build)
|
||||
monkeypatch.setattr(
|
||||
template_async_main, "get_file_upload_link", mock_get_file_upload_link
|
||||
)
|
||||
monkeypatch.setattr(build_api_mod, "get_build_status", mock_get_build_status)
|
||||
|
||||
|
||||
async def _expect_to_throw_and_check_trace(func, expected_method: str):
|
||||
try:
|
||||
await func()
|
||||
assert False, "Expected AsyncTemplate.build to raise an exception"
|
||||
except Exception as e: # noqa: BLE001 - we want to assert on the traceback regardless of type
|
||||
tb = e.__traceback__
|
||||
saw_this_file = False
|
||||
saw_expected_method = False
|
||||
while tb is not None:
|
||||
traceback_file = tb.tb_frame.f_code.co_filename
|
||||
if traceback_file == __file__:
|
||||
saw_this_file = True
|
||||
caller_line = linecache.getline(traceback_file, tb.tb_lineno)
|
||||
if caller_line and f".{expected_method}(" in caller_line:
|
||||
saw_expected_method = True
|
||||
break
|
||||
tb = tb.tb_next
|
||||
assert saw_this_file, traceback.format_exc()
|
||||
assert saw_expected_method, traceback.format_exc()
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_traces_on_from_image(async_build):
|
||||
template = AsyncTemplate().from_image("e2b.dev/this-image-does-not-exist")
|
||||
await _expect_to_throw_and_check_trace(
|
||||
lambda: async_build(template, name="from_image", skip_cache=True), "from_image"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_traces_on_from_template(async_build):
|
||||
template = AsyncTemplate().from_template("this-template-does-not-exist")
|
||||
await _expect_to_throw_and_check_trace(
|
||||
lambda: async_build(template, name="from_template", skip_cache=True),
|
||||
"from_template",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_traces_on_from_dockerfile(async_build):
|
||||
template = AsyncTemplate().from_dockerfile("FROM ubuntu:22.04\nRUN nonexistent")
|
||||
await _expect_to_throw_and_check_trace(
|
||||
lambda: async_build(template, name="from_dockerfile", skip_cache=True),
|
||||
"from_dockerfile",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_traces_on_from_image_registry(async_build):
|
||||
template = AsyncTemplate().from_image(
|
||||
"registry.example.com/nonexistent:latest",
|
||||
username="test",
|
||||
password="test",
|
||||
)
|
||||
await _expect_to_throw_and_check_trace(
|
||||
lambda: async_build(template, name="from_image_registry", skip_cache=True),
|
||||
"from_image",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_traces_on_from_image_credentials():
|
||||
await _expect_to_throw_and_check_trace(
|
||||
lambda: AsyncTemplate().from_image("ubuntu:22.04", username="user"),
|
||||
"from_image",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_traces_on_from_aws_registry(async_build):
|
||||
template = AsyncTemplate().from_aws_registry(
|
||||
"123456789.dkr.ecr.us-east-1.amazonaws.com/nonexistent:latest",
|
||||
access_key_id="test",
|
||||
secret_access_key="test",
|
||||
region="us-east-1",
|
||||
)
|
||||
await _expect_to_throw_and_check_trace(
|
||||
lambda: async_build(template, name="from_aws_registry"), "from_aws_registry"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_traces_on_from_gcp_registry(async_build):
|
||||
template = AsyncTemplate().from_gcp_registry(
|
||||
"gcr.io/nonexistent-project/nonexistent:latest",
|
||||
service_account_json={
|
||||
"type": "service_account",
|
||||
},
|
||||
)
|
||||
await _expect_to_throw_and_check_trace(
|
||||
lambda: async_build(template, name="from_gcp_registry"), "from_gcp_registry"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_traces_on_copy(async_build):
|
||||
template = AsyncTemplate()
|
||||
template = template.from_base_image()
|
||||
template = template.skip_cache().copy(non_existent_path, non_existent_path)
|
||||
await _expect_to_throw_and_check_trace(
|
||||
lambda: async_build(template, name="copy"), "copy"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_traces_on_copyItems(async_build):
|
||||
template = AsyncTemplate()
|
||||
template = template.from_base_image()
|
||||
template = template.skip_cache().copy_items(
|
||||
[CopyItem(src=non_existent_path, dest=non_existent_path)]
|
||||
)
|
||||
await _expect_to_throw_and_check_trace(
|
||||
lambda: async_build(template, name="copy_items"), "copy_items"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_traces_on_second_source_of_multi_source_copy(async_build):
|
||||
template = AsyncTemplate()
|
||||
template = template.from_base_image()
|
||||
template = template.copy(["test_stacktrace.py", "test_tags.py"], ".")
|
||||
await _expect_to_throw_and_check_trace(
|
||||
lambda: async_build(template, name="multi_source_copy_second_source"), "copy"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_traces_on_step_after_multi_source_copy(async_build):
|
||||
template = AsyncTemplate()
|
||||
template = template.from_base_image()
|
||||
template = template.copy(["test_stacktrace.py", "test_tags.py"], ".")
|
||||
template = template.run_cmd(f"cat {non_existent_path}")
|
||||
await _expect_to_throw_and_check_trace(
|
||||
lambda: async_build(template, name="multi_source_copy_next_step"), "run_cmd"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_traces_on_second_item_of_copy_items(async_build):
|
||||
template = AsyncTemplate()
|
||||
template = template.from_base_image()
|
||||
template = template.copy_items(
|
||||
[
|
||||
CopyItem(src="test_stacktrace.py", dest="."),
|
||||
CopyItem(src="test_tags.py", dest="."),
|
||||
]
|
||||
)
|
||||
await _expect_to_throw_and_check_trace(
|
||||
lambda: async_build(template, name="copy_items_second_item"), "copy_items"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_traces_on_step_after_copy_items(async_build):
|
||||
template = AsyncTemplate()
|
||||
template = template.from_base_image()
|
||||
template = template.copy_items(
|
||||
[
|
||||
CopyItem(src="test_stacktrace.py", dest="."),
|
||||
CopyItem(src="test_tags.py", dest="."),
|
||||
]
|
||||
)
|
||||
template = template.run_cmd(f"cat {non_existent_path}")
|
||||
await _expect_to_throw_and_check_trace(
|
||||
lambda: async_build(template, name="copy_items_next_step"), "run_cmd"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_traces_on_copy_absolute_path():
|
||||
await _expect_to_throw_and_check_trace(
|
||||
lambda: AsyncTemplate()
|
||||
.from_base_image()
|
||||
.copy("/absolute/path", "/absolute/path"),
|
||||
"copy",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_traces_on_copyItems_absolute_path():
|
||||
await _expect_to_throw_and_check_trace(
|
||||
lambda: AsyncTemplate()
|
||||
.from_base_image()
|
||||
.copy_items([CopyItem(src="/absolute/path", dest="/absolute/path")]),
|
||||
"copy_items",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_traces_on_remove(async_build):
|
||||
template = AsyncTemplate()
|
||||
template = template.from_base_image()
|
||||
template = template.skip_cache().remove(non_existent_path)
|
||||
await _expect_to_throw_and_check_trace(
|
||||
lambda: async_build(template, name="remove"), "remove"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_traces_on_rename(async_build):
|
||||
template = AsyncTemplate()
|
||||
template = template.from_base_image()
|
||||
template = template.skip_cache().rename(non_existent_path, "/tmp/dest.txt")
|
||||
await _expect_to_throw_and_check_trace(
|
||||
lambda: async_build(template, name="rename"), "rename"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_traces_on_make_dir(async_build):
|
||||
template = AsyncTemplate()
|
||||
template = template.from_base_image()
|
||||
template = template.set_user("root").skip_cache().make_dir("/root/.bashrc")
|
||||
await _expect_to_throw_and_check_trace(
|
||||
lambda: async_build(template, name="make_dir"), "make_dir"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_traces_on_make_symlink(async_build):
|
||||
template = AsyncTemplate()
|
||||
template = template.from_base_image()
|
||||
template = template.skip_cache().make_symlink(".bashrc", ".bashrc")
|
||||
await _expect_to_throw_and_check_trace(
|
||||
lambda: async_build(template, name="make_symlink"), "make_symlink"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_traces_on_run_cmd(async_build):
|
||||
template = AsyncTemplate()
|
||||
template = template.from_base_image()
|
||||
template = template.skip_cache().run_cmd(f"cat {non_existent_path}")
|
||||
await _expect_to_throw_and_check_trace(
|
||||
lambda: async_build(template, name="run_cmd"), "run_cmd"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_traces_on_set_workdir(async_build):
|
||||
template = AsyncTemplate()
|
||||
template = template.from_base_image()
|
||||
template = template.set_user("root").skip_cache().set_workdir("/root/.bashrc")
|
||||
await _expect_to_throw_and_check_trace(
|
||||
lambda: async_build(template, name="set_workdir"), "set_workdir"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_traces_on_set_user(async_build):
|
||||
template = AsyncTemplate()
|
||||
template = template.from_base_image()
|
||||
template = template.skip_cache().set_user("; exit 1")
|
||||
await _expect_to_throw_and_check_trace(
|
||||
lambda: async_build(template, name="set_user"), "set_user"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_traces_on_pip_install(async_build):
|
||||
template = AsyncTemplate()
|
||||
template = template.from_base_image()
|
||||
template = template.skip_cache().pip_install("nonexistent-package")
|
||||
await _expect_to_throw_and_check_trace(
|
||||
lambda: async_build(template, name="pip_install"), "pip_install"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_traces_on_npm_install(async_build):
|
||||
template = AsyncTemplate()
|
||||
template = template.from_base_image()
|
||||
template = template.skip_cache().npm_install("nonexistent-package")
|
||||
await _expect_to_throw_and_check_trace(
|
||||
lambda: async_build(template, name="npm_install"), "npm_install"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_traces_on_apt_install(async_build):
|
||||
template = AsyncTemplate()
|
||||
template = template.from_base_image()
|
||||
template = template.skip_cache().apt_install("nonexistent-package")
|
||||
await _expect_to_throw_and_check_trace(
|
||||
lambda: async_build(template, name="apt_install"), "apt_install"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_traces_on_git_clone(async_build):
|
||||
template = AsyncTemplate()
|
||||
template = template.from_base_image()
|
||||
template = template.skip_cache().git_clone("https://github.com/repo.git")
|
||||
await _expect_to_throw_and_check_trace(
|
||||
lambda: async_build(template, name="git_clone"), "git_clone"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_traces_on_start_cmd(async_build):
|
||||
template = AsyncTemplate()
|
||||
template = template.from_base_image()
|
||||
template = template.set_start_cmd(
|
||||
f"./{non_existent_path}", wait_for_timeout(10_000)
|
||||
)
|
||||
await _expect_to_throw_and_check_trace(
|
||||
lambda: async_build(template, name="set_start_cmd"), "set_start_cmd"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_traces_on_add_mcp_server():
|
||||
# needs mcp-gateway as base template, without it no mcp servers can be added
|
||||
await _expect_to_throw_and_check_trace(
|
||||
lambda: AsyncTemplate().from_base_image().skip_cache().add_mcp_server("exa"),
|
||||
"add_mcp_server",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_traces_on_dev_container_prebuild(async_build):
|
||||
template = AsyncTemplate()
|
||||
template = template.from_template("devcontainer")
|
||||
template = template.skip_cache().beta_dev_container_prebuild(non_existent_path)
|
||||
await _expect_to_throw_and_check_trace(
|
||||
lambda: async_build(template, name="beta_dev_container_prebuild"),
|
||||
"beta_dev_container_prebuild",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_traces_on_set_dev_container_start(async_build):
|
||||
template = AsyncTemplate()
|
||||
template = template.from_template("devcontainer")
|
||||
template = template.beta_set_dev_container_start(non_existent_path)
|
||||
await _expect_to_throw_and_check_trace(
|
||||
lambda: async_build(template, name="beta_set_dev_container_start"),
|
||||
"beta_set_dev_container_start",
|
||||
)
|
||||
@@ -0,0 +1,241 @@
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
|
||||
from e2b import AsyncTemplate, TemplateTag, TemplateTagInfo, Template
|
||||
from e2b.exceptions import TemplateException
|
||||
import e2b.template_async.main as template_async_main
|
||||
|
||||
|
||||
class TestAssignTags:
|
||||
"""Tests for AsyncTemplate.assign_tags method."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_assign_single_tag(self, monkeypatch):
|
||||
"""Test assigning a single tag to a template."""
|
||||
mock_assign_tags = AsyncMock(
|
||||
return_value=TemplateTagInfo(
|
||||
build_id="00000000-0000-0000-0000-000000000000",
|
||||
tags=["production"],
|
||||
)
|
||||
)
|
||||
|
||||
monkeypatch.setattr(
|
||||
template_async_main, "get_api_client", lambda *args, **kwargs: None
|
||||
)
|
||||
monkeypatch.setattr(template_async_main, "assign_tags", mock_assign_tags)
|
||||
|
||||
result = await AsyncTemplate.assign_tags("my-template:v1.0", "production")
|
||||
|
||||
assert result.build_id == "00000000-0000-0000-0000-000000000000"
|
||||
assert "production" in result.tags
|
||||
mock_assign_tags.assert_called_once()
|
||||
_, target, tags = mock_assign_tags.call_args[0]
|
||||
assert target == "my-template:v1.0"
|
||||
assert tags == ["production"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_assign_multiple_tags(self, monkeypatch):
|
||||
"""Test assigning multiple tags to a template."""
|
||||
mock_assign_tags = AsyncMock(
|
||||
return_value=TemplateTagInfo(
|
||||
build_id="00000000-0000-0000-0000-000000000000",
|
||||
tags=["production", "stable"],
|
||||
)
|
||||
)
|
||||
|
||||
monkeypatch.setattr(
|
||||
template_async_main, "get_api_client", lambda *args, **kwargs: None
|
||||
)
|
||||
monkeypatch.setattr(template_async_main, "assign_tags", mock_assign_tags)
|
||||
|
||||
result = await AsyncTemplate.assign_tags(
|
||||
"my-template:v1.0", ["production", "stable"]
|
||||
)
|
||||
|
||||
assert result.build_id == "00000000-0000-0000-0000-000000000000"
|
||||
assert "production" in result.tags
|
||||
assert "stable" in result.tags
|
||||
mock_assign_tags.assert_called_once()
|
||||
_, _, tags = mock_assign_tags.call_args[0]
|
||||
assert tags == ["production", "stable"]
|
||||
|
||||
|
||||
class TestRemoveTags:
|
||||
"""Tests for AsyncTemplate.remove_tags method."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_remove_single_tag(self, monkeypatch):
|
||||
"""Test deleting a single tag from a template."""
|
||||
mock_remove_tags = AsyncMock(return_value=None)
|
||||
|
||||
monkeypatch.setattr(
|
||||
template_async_main, "get_api_client", lambda *args, **kwargs: None
|
||||
)
|
||||
monkeypatch.setattr(template_async_main, "remove_tags", mock_remove_tags)
|
||||
|
||||
await AsyncTemplate.remove_tags("my-template", "production")
|
||||
|
||||
mock_remove_tags.assert_called_once()
|
||||
_, name, tags = mock_remove_tags.call_args[0]
|
||||
assert name == "my-template"
|
||||
assert tags == ["production"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_remove_multiple_tags(self, monkeypatch):
|
||||
"""Test deleting multiple tags from a template."""
|
||||
mock_remove_tags = AsyncMock(return_value=None)
|
||||
|
||||
monkeypatch.setattr(
|
||||
template_async_main, "get_api_client", lambda *args, **kwargs: None
|
||||
)
|
||||
monkeypatch.setattr(template_async_main, "remove_tags", mock_remove_tags)
|
||||
|
||||
await AsyncTemplate.remove_tags("my-template", ["production", "staging"])
|
||||
|
||||
mock_remove_tags.assert_called_once()
|
||||
_, name, tags = mock_remove_tags.call_args[0]
|
||||
assert name == "my-template"
|
||||
assert tags == ["production", "staging"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_remove_tags_error(self, monkeypatch):
|
||||
"""Test that remove_tags raises an error for nonexistent template."""
|
||||
mock_remove_tags = AsyncMock(
|
||||
side_effect=TemplateException("Template not found")
|
||||
)
|
||||
|
||||
monkeypatch.setattr(
|
||||
template_async_main, "get_api_client", lambda *args, **kwargs: None
|
||||
)
|
||||
monkeypatch.setattr(template_async_main, "remove_tags", mock_remove_tags)
|
||||
|
||||
with pytest.raises(TemplateException):
|
||||
await AsyncTemplate.remove_tags("nonexistent", ["tag"])
|
||||
|
||||
|
||||
class TestGetTags:
|
||||
"""Tests for AsyncTemplate.get_tags method."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_tags(self, monkeypatch):
|
||||
"""Test getting tags for a template."""
|
||||
mock_get_template_tags = AsyncMock(
|
||||
return_value=[
|
||||
TemplateTag(
|
||||
tag="v1.0",
|
||||
build_id="00000000-0000-0000-0000-000000000000",
|
||||
created_at=datetime(2024, 1, 15, 10, 30, 0, tzinfo=timezone.utc),
|
||||
),
|
||||
TemplateTag(
|
||||
tag="latest",
|
||||
build_id="11111111-1111-1111-1111-111111111111",
|
||||
created_at=datetime(2024, 1, 16, 12, 0, 0, tzinfo=timezone.utc),
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
monkeypatch.setattr(
|
||||
template_async_main, "get_api_client", lambda *args, **kwargs: None
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
template_async_main, "get_template_tags", mock_get_template_tags
|
||||
)
|
||||
|
||||
result = await AsyncTemplate.get_tags("my-template")
|
||||
|
||||
assert len(result) == 2
|
||||
assert result[0].tag == "v1.0"
|
||||
assert result[0].build_id == "00000000-0000-0000-0000-000000000000"
|
||||
assert isinstance(result[0].created_at, datetime)
|
||||
assert result[1].tag == "latest"
|
||||
mock_get_template_tags.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_tags_error(self, monkeypatch):
|
||||
"""Test that get_tags raises an error for nonexistent template."""
|
||||
mock_get_template_tags = AsyncMock(
|
||||
side_effect=TemplateException("Template not found")
|
||||
)
|
||||
|
||||
monkeypatch.setattr(
|
||||
template_async_main, "get_api_client", lambda *args, **kwargs: None
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
template_async_main, "get_template_tags", mock_get_template_tags
|
||||
)
|
||||
|
||||
with pytest.raises(TemplateException):
|
||||
await AsyncTemplate.get_tags("nonexistent")
|
||||
|
||||
|
||||
# Integration tests
|
||||
class TestTagsIntegration:
|
||||
"""Integration tests for AsyncTemplate tags functionality."""
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_build_template_with_tags_assign_and_delete(self, async_build):
|
||||
"""Test building a template with tags, assigning new tags, and deleting."""
|
||||
template_name = "e2b-tags-test"
|
||||
initial_tag = f"{template_name}:v1-{uuid.uuid4().hex}"
|
||||
|
||||
# Build a template with initial tag
|
||||
template = Template().from_base_image()
|
||||
build_info = await async_build(template, name=initial_tag)
|
||||
|
||||
assert build_info.build_id
|
||||
assert build_info.template_id
|
||||
|
||||
# Assign additional tags
|
||||
tag_info = await AsyncTemplate.assign_tags(
|
||||
initial_tag, ["production", "latest"]
|
||||
)
|
||||
|
||||
assert tag_info.build_id
|
||||
# API returns just the tag portion, not the full alias:tag
|
||||
assert "production" in tag_info.tags
|
||||
assert "latest" in tag_info.tags
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_assign_single_tag_to_existing_template(self, async_build):
|
||||
"""Test assigning a single tag (not array) to an existing template."""
|
||||
template_name = "e2b-tags-test"
|
||||
initial_tag = f"{template_name}:v1-{uuid.uuid4().hex}"
|
||||
|
||||
template = Template().from_base_image()
|
||||
await async_build(template, name=initial_tag)
|
||||
|
||||
# Assign single tag (not array)
|
||||
tag_info = await AsyncTemplate.assign_tags(initial_tag, "stable")
|
||||
|
||||
assert tag_info.build_id
|
||||
# API returns just the tag portion, not the full alias:tag
|
||||
assert "stable" in tag_info.tags
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_rejects_invalid_tag_format_missing_alias(self, async_build):
|
||||
"""Test that tag without alias (starts with colon) is rejected."""
|
||||
template_name = "e2b-tags-test"
|
||||
initial_tag = f"{template_name}:v1-{uuid.uuid4().hex}"
|
||||
|
||||
template = Template().from_base_image()
|
||||
await async_build(template, name=initial_tag)
|
||||
|
||||
# Tag without alias (starts with colon) should be rejected
|
||||
with pytest.raises(Exception):
|
||||
await AsyncTemplate.assign_tags(initial_tag, ":invalid-tag")
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
async def test_rejects_invalid_tag_format_missing_tag(self, async_build):
|
||||
"""Test that tag without tag portion (ends with colon) is rejected."""
|
||||
template_name = "e2b-tags-test"
|
||||
initial_tag = f"{template_name}:v1-{uuid.uuid4().hex}"
|
||||
|
||||
template = Template().from_base_image()
|
||||
await async_build(template, name=initial_tag)
|
||||
|
||||
# Tag without tag portion (ends with colon) should be rejected
|
||||
with pytest.raises(Exception):
|
||||
await AsyncTemplate.assign_tags(initial_tag, f"{template_name}:")
|
||||
@@ -0,0 +1,197 @@
|
||||
import threading
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
from unittest import mock
|
||||
|
||||
import httpx
|
||||
|
||||
from e2b.api.client.client import AuthenticatedClient
|
||||
from e2b.template import utils as template_utils
|
||||
from e2b.template.consts import FILE_UPLOAD_TIMEOUT_SECONDS
|
||||
from e2b.template_async.build_api import upload_file
|
||||
|
||||
|
||||
# Regression test for e2b-dev/e2b#1243 — upload_file must set Content-Length
|
||||
# and must not fall back to Transfer-Encoding: chunked. S3 presigned PUT URLs
|
||||
# reject chunked encoding with 501 NotImplemented. The archive is streamed
|
||||
# from a temporary file on disk with a known Content-Length instead of being
|
||||
# buffered in memory; this test guards that contract.
|
||||
#
|
||||
# The mock server runs in a daemon thread and doesn't need to be async — the
|
||||
# httpx.AsyncClient connects to it via asyncio sockets without blocking the
|
||||
# event loop.
|
||||
|
||||
|
||||
def _make_server():
|
||||
state = {"headers": None, "body_length": 0}
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
def do_PUT(self):
|
||||
state["headers"] = dict(self.headers)
|
||||
length = int(self.headers.get("Content-Length", 0))
|
||||
body = self.rfile.read(length) if length else b""
|
||||
state["body_length"] = len(body)
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
|
||||
def log_message(self, *args, **kwargs):
|
||||
return
|
||||
|
||||
server = HTTPServer(("127.0.0.1", 0), Handler)
|
||||
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
return server, thread, state
|
||||
|
||||
|
||||
async def test_upload_file_sets_content_length_and_no_chunked_encoding(tmp_path):
|
||||
(tmp_path / "hello.txt").write_text("hello world")
|
||||
|
||||
server, thread, state = _make_server()
|
||||
host, port = server.server_address
|
||||
url = f"http://{host}:{port}/upload"
|
||||
|
||||
try:
|
||||
|
||||
class UploadClient(AuthenticatedClient):
|
||||
def get_async_httpx_client(self):
|
||||
raise AssertionError("signed uploads should not use the API client")
|
||||
|
||||
client = UploadClient(base_url="http://test", token="test")
|
||||
await upload_file(
|
||||
api_client=client,
|
||||
file_name="*.txt",
|
||||
context_path=str(tmp_path),
|
||||
url=url,
|
||||
ignore_patterns=[],
|
||||
resolve_symlinks=False,
|
||||
gzip=True,
|
||||
stack_trace=None,
|
||||
)
|
||||
finally:
|
||||
server.shutdown()
|
||||
server.server_close()
|
||||
thread.join(timeout=5)
|
||||
|
||||
assert state["headers"] is not None
|
||||
content_length = state["headers"].get("Content-Length")
|
||||
assert content_length is not None
|
||||
assert int(content_length) > 0
|
||||
assert int(content_length) == state["body_length"]
|
||||
|
||||
transfer_encoding = state["headers"].get("Transfer-Encoding")
|
||||
if transfer_encoding is not None:
|
||||
assert "chunked" not in transfer_encoding.lower()
|
||||
assert "Authorization" not in state["headers"]
|
||||
|
||||
|
||||
async def _capture_upload_timeout(tmp_path, request_timeout=None):
|
||||
"""Run upload_file against a local server, capturing the httpx timeout."""
|
||||
(tmp_path / "hello.txt").write_text("hello world")
|
||||
|
||||
server, thread, state = _make_server()
|
||||
host, port = server.server_address
|
||||
url = f"http://{host}:{port}/upload"
|
||||
|
||||
captured = {}
|
||||
real_client = httpx.AsyncClient
|
||||
|
||||
def spy_client(*args, **kwargs):
|
||||
captured["timeout"] = kwargs.get("timeout")
|
||||
return real_client(*args, **kwargs)
|
||||
|
||||
try:
|
||||
client = AuthenticatedClient(base_url="http://test", token="test")
|
||||
kwargs = {}
|
||||
if request_timeout is not None:
|
||||
kwargs["request_timeout"] = request_timeout
|
||||
with mock.patch(
|
||||
"e2b.template_async.build_api.httpx.AsyncClient", side_effect=spy_client
|
||||
):
|
||||
await upload_file(
|
||||
api_client=client,
|
||||
file_name="*.txt",
|
||||
context_path=str(tmp_path),
|
||||
url=url,
|
||||
ignore_patterns=[],
|
||||
resolve_symlinks=False,
|
||||
gzip=True,
|
||||
stack_trace=None,
|
||||
**kwargs,
|
||||
)
|
||||
finally:
|
||||
server.shutdown()
|
||||
server.server_close()
|
||||
thread.join(timeout=5)
|
||||
|
||||
return captured["timeout"]
|
||||
|
||||
|
||||
async def test_upload_file_defaults_to_one_hour_timeout(tmp_path):
|
||||
# Uploads of large archives need far longer than the 60s general API
|
||||
# timeout, so the default upload timeout is 1 hour (matches the JS SDK).
|
||||
timeout = await _capture_upload_timeout(tmp_path)
|
||||
assert timeout == httpx.Timeout(FILE_UPLOAD_TIMEOUT_SECONDS)
|
||||
|
||||
|
||||
async def test_upload_file_honors_explicit_request_timeout(tmp_path):
|
||||
# An explicitly set request_timeout overrides the 1-hour upload default.
|
||||
timeout = await _capture_upload_timeout(tmp_path, request_timeout=5.0)
|
||||
assert timeout == httpx.Timeout(5.0)
|
||||
|
||||
|
||||
async def test_upload_file_ignores_post_upload_close_failure(tmp_path):
|
||||
# Regression test: once S3 has accepted the archive, closing the spooled
|
||||
# temp file in the `finally` block can raise. That failure must not be
|
||||
# wrapped as a FileUploadException — the upload already succeeded.
|
||||
(tmp_path / "hello.txt").write_text("hello world")
|
||||
|
||||
server, thread, state = _make_server()
|
||||
host, port = server.server_address
|
||||
url = f"http://{host}:{port}/upload"
|
||||
|
||||
real_tar_file_stream = template_utils.tar_file_stream
|
||||
|
||||
class _FailingCloseFile:
|
||||
# Proxies a real spooled temp file but raises on close(). The
|
||||
# underlying file object is a C type whose `close` attribute can't
|
||||
# be reassigned, so we wrap it instead.
|
||||
def __init__(self, inner):
|
||||
self._inner = inner
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._inner, name)
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._inner)
|
||||
|
||||
def close(self):
|
||||
# Run the real close so we don't leak the temp file, then
|
||||
# simulate a close failure surfacing from the `finally`.
|
||||
self._inner.close()
|
||||
raise OSError("close failed")
|
||||
|
||||
def failing_close_stream(*args, **kwargs):
|
||||
return _FailingCloseFile(real_tar_file_stream(*args, **kwargs))
|
||||
|
||||
try:
|
||||
client = AuthenticatedClient(base_url="http://test", token="test")
|
||||
with mock.patch(
|
||||
"e2b.template_async.build_api.tar_file_stream",
|
||||
side_effect=failing_close_stream,
|
||||
):
|
||||
# Must not raise despite close() failing after a 200 response.
|
||||
await upload_file(
|
||||
api_client=client,
|
||||
file_name="*.txt",
|
||||
context_path=str(tmp_path),
|
||||
url=url,
|
||||
ignore_patterns=[],
|
||||
resolve_symlinks=False,
|
||||
gzip=True,
|
||||
stack_trace=None,
|
||||
)
|
||||
finally:
|
||||
server.shutdown()
|
||||
server.server_close()
|
||||
thread.join(timeout=5)
|
||||
|
||||
assert state["headers"] is not None
|
||||
@@ -0,0 +1,298 @@
|
||||
import datetime
|
||||
from io import BytesIO, StringIO
|
||||
|
||||
import pytest
|
||||
|
||||
from e2b import AsyncVolume
|
||||
from e2b.exceptions import NotFoundException, VolumeException
|
||||
|
||||
|
||||
class TestWriteFileAndReadFile:
|
||||
async def test_write_and_read_text_file(self, async_volume: AsyncVolume):
|
||||
path = "/test.txt"
|
||||
content = "Hello, World!"
|
||||
|
||||
await async_volume.write_file(path, content)
|
||||
read_content = await async_volume.read_file(path, format="text")
|
||||
|
||||
assert read_content == content
|
||||
|
||||
async def test_write_and_read_bytes(self, async_volume: AsyncVolume):
|
||||
path = "/test-bytes.txt"
|
||||
content = "Test bytes content"
|
||||
content_bytes = content.encode("utf-8")
|
||||
|
||||
await async_volume.write_file(path, content_bytes)
|
||||
read_bytes = await async_volume.read_file(path, format="bytes")
|
||||
|
||||
assert read_bytes == content_bytes
|
||||
|
||||
async def test_write_and_read_binary_data(self, async_volume: AsyncVolume):
|
||||
path = "/binary.bin"
|
||||
binary_data = bytes([0, 1, 2, 3, 4])
|
||||
|
||||
await async_volume.write_file(path, binary_data)
|
||||
read_bytes = await async_volume.read_file(path, format="bytes")
|
||||
|
||||
assert read_bytes == binary_data
|
||||
|
||||
async def test_write_and_read_stream(self, async_volume: AsyncVolume):
|
||||
path = "/test-stream.txt"
|
||||
content = "Test stream content"
|
||||
stream = BytesIO(content.encode("utf-8"))
|
||||
|
||||
await async_volume.write_file(path, stream)
|
||||
read_stream = await async_volume.read_file(path, format="stream")
|
||||
chunks = []
|
||||
async for chunk in read_stream:
|
||||
chunks.append(chunk)
|
||||
read_content = b"".join(chunks).decode("utf-8")
|
||||
|
||||
assert read_content == content
|
||||
|
||||
async def test_write_and_read_text_stream(self, async_volume: AsyncVolume):
|
||||
path = "/test-text-stream.txt"
|
||||
content = "Test text stream content"
|
||||
stream = StringIO(content)
|
||||
|
||||
await async_volume.write_file(path, stream)
|
||||
read_content = await async_volume.read_file(path, format="text")
|
||||
|
||||
assert read_content == content
|
||||
|
||||
async def test_write_and_read_empty_file(self, async_volume: AsyncVolume):
|
||||
path = "/empty.txt"
|
||||
content = ""
|
||||
|
||||
await async_volume.write_file(path, content)
|
||||
read_content = await async_volume.read_file(path, format="text")
|
||||
|
||||
assert read_content == content
|
||||
|
||||
async def test_overwrite_with_force(self, async_volume: AsyncVolume):
|
||||
path = "/overwrite.txt"
|
||||
initial_content = "Initial content"
|
||||
new_content = "New content"
|
||||
|
||||
await async_volume.write_file(path, initial_content)
|
||||
await async_volume.write_file(path, new_content, force=True)
|
||||
read_content = await async_volume.read_file(path, format="text")
|
||||
|
||||
assert read_content == new_content
|
||||
|
||||
async def test_write_existing_file_without_force_raises(
|
||||
self, async_volume: AsyncVolume
|
||||
):
|
||||
path = "/no-overwrite.txt"
|
||||
initial_content = "Initial content"
|
||||
new_content = "New content"
|
||||
|
||||
await async_volume.write_file(path, initial_content)
|
||||
with pytest.raises(VolumeException):
|
||||
await async_volume.write_file(path, new_content, force=False)
|
||||
|
||||
async def test_write_file_with_metadata(self, async_volume: AsyncVolume):
|
||||
path = "/metadata.txt"
|
||||
content = "File with metadata"
|
||||
|
||||
await async_volume.write_file(path, content, uid=1000, gid=1000, mode=0o644)
|
||||
|
||||
entry_info = await async_volume.get_info(path)
|
||||
assert entry_info.type.value == "file"
|
||||
assert entry_info.uid == 1000
|
||||
assert entry_info.gid == 1000
|
||||
assert entry_info.mode == 0o644
|
||||
|
||||
async def test_write_file_in_nested_directory(self, async_volume: AsyncVolume):
|
||||
dir_path = "/nested/deep/path"
|
||||
file_path = f"{dir_path}/file.txt"
|
||||
content = "Nested file content"
|
||||
|
||||
await async_volume.make_dir(dir_path, force=True)
|
||||
await async_volume.write_file(file_path, content)
|
||||
read_content = await async_volume.read_file(file_path, format="text")
|
||||
|
||||
assert read_content == content
|
||||
|
||||
|
||||
class TestGetInfo:
|
||||
async def test_get_info_for_file(self, async_volume: AsyncVolume):
|
||||
path = "/info-file.txt"
|
||||
content = "File for info test"
|
||||
|
||||
await async_volume.write_file(path, content)
|
||||
info = await async_volume.get_info(path)
|
||||
|
||||
assert info.name == "info-file.txt"
|
||||
assert info.type.value == "file"
|
||||
assert info.path == path
|
||||
assert isinstance(info.mtime, datetime.datetime)
|
||||
assert isinstance(info.ctime, datetime.datetime)
|
||||
|
||||
async def test_get_info_for_directory(self, async_volume: AsyncVolume):
|
||||
path = "/info-dir"
|
||||
|
||||
await async_volume.make_dir(path)
|
||||
info = await async_volume.get_info(path)
|
||||
|
||||
assert info.name == "info-dir"
|
||||
assert info.type.value == "directory"
|
||||
assert info.path == path
|
||||
|
||||
async def test_exists_returns_false_for_nonexistent(
|
||||
self, async_volume: AsyncVolume
|
||||
):
|
||||
assert await async_volume.exists("/non-existent.txt") is False
|
||||
|
||||
|
||||
class TestUpdateMetadata:
|
||||
async def test_update_file_metadata(self, async_volume: AsyncVolume):
|
||||
path = "/metadata-update.txt"
|
||||
await async_volume.write_file(path, "Content")
|
||||
|
||||
updated = await async_volume.update_metadata(
|
||||
path, uid=1001, gid=1001, mode=0o755
|
||||
)
|
||||
|
||||
assert updated.path == path
|
||||
assert updated.type.value == "file"
|
||||
assert updated.uid == 1001
|
||||
assert updated.gid == 1001
|
||||
assert updated.mode == 0o755
|
||||
|
||||
async def test_update_metadata_nonexistent_raises(self, async_volume: AsyncVolume):
|
||||
with pytest.raises(NotFoundException):
|
||||
await async_volume.update_metadata("/non-existent.txt", mode=0o644)
|
||||
|
||||
|
||||
class TestMakeDir:
|
||||
async def test_create_directory(self, async_volume: AsyncVolume):
|
||||
path = "/test-dir"
|
||||
|
||||
await async_volume.make_dir(path)
|
||||
info = await async_volume.get_info(path)
|
||||
|
||||
assert info.type.value == "directory"
|
||||
assert info.path == path
|
||||
|
||||
async def test_create_nested_directories_with_force(
|
||||
self, async_volume: AsyncVolume
|
||||
):
|
||||
path = "/nested/deep/directory"
|
||||
|
||||
await async_volume.make_dir(path, force=True)
|
||||
info = await async_volume.get_info(path)
|
||||
|
||||
assert info.type.value == "directory"
|
||||
|
||||
async def test_create_existing_directory_without_force_raises(
|
||||
self, async_volume: AsyncVolume
|
||||
):
|
||||
path = "/existing-dir"
|
||||
|
||||
await async_volume.make_dir(path)
|
||||
with pytest.raises(VolumeException):
|
||||
await async_volume.make_dir(path, force=False)
|
||||
|
||||
async def test_create_directory_with_metadata(self, async_volume: AsyncVolume):
|
||||
path = "/dir-with-metadata"
|
||||
|
||||
await async_volume.make_dir(path, uid=1000, gid=1000, mode=0o755)
|
||||
|
||||
info = await async_volume.get_info(path)
|
||||
assert info.type.value == "directory"
|
||||
assert info.uid == 1000
|
||||
assert info.gid == 1000
|
||||
assert info.mode & 0o777 == 0o755
|
||||
|
||||
|
||||
class TestList:
|
||||
async def test_list_directory_contents(self, async_volume: AsyncVolume):
|
||||
await async_volume.write_file("/file1.txt", "Content 1")
|
||||
await async_volume.write_file("/file2.txt", "Content 2")
|
||||
await async_volume.make_dir("/dir1")
|
||||
|
||||
entries = await async_volume.list("/")
|
||||
|
||||
assert len(entries) >= 3
|
||||
file_names = sorted([e.name for e in entries])
|
||||
assert "file1.txt" in file_names
|
||||
assert "file2.txt" in file_names
|
||||
assert "dir1" in file_names
|
||||
|
||||
async def test_list_nested_directory(self, async_volume: AsyncVolume):
|
||||
await async_volume.make_dir("/nested", force=True)
|
||||
await async_volume.write_file("/nested/file.txt", "Content")
|
||||
|
||||
entries = await async_volume.list("/nested")
|
||||
|
||||
assert len(entries) >= 1
|
||||
assert any(e.name == "file.txt" for e in entries)
|
||||
|
||||
@pytest.mark.skip(reason="depth option not yet supported")
|
||||
async def test_list_with_depth(self, async_volume: AsyncVolume):
|
||||
await async_volume.make_dir("/deep/nested/structure", force=True)
|
||||
await async_volume.write_file("/deep/nested/structure/file.txt", "Content")
|
||||
|
||||
entries = await async_volume.list("/deep", depth=2)
|
||||
|
||||
assert len(entries) > 0
|
||||
|
||||
async def test_list_nonexistent_raises(self, async_volume: AsyncVolume):
|
||||
with pytest.raises(NotFoundException):
|
||||
await async_volume.list("/non-existent")
|
||||
|
||||
|
||||
class TestRemove:
|
||||
async def test_remove_file(self, async_volume: AsyncVolume):
|
||||
path = "/to-remove.txt"
|
||||
await async_volume.write_file(path, "Content")
|
||||
|
||||
await async_volume.remove(path)
|
||||
|
||||
assert await async_volume.exists(path) is False
|
||||
|
||||
async def test_remove_directory(self, async_volume: AsyncVolume):
|
||||
path = "/to-remove-dir"
|
||||
await async_volume.make_dir(path)
|
||||
|
||||
await async_volume.remove(path)
|
||||
|
||||
assert await async_volume.exists(path) is False
|
||||
|
||||
async def test_remove_directory_recursively(self, async_volume: AsyncVolume):
|
||||
dir_path = "/recursive-dir"
|
||||
await async_volume.make_dir(f"{dir_path}/nested", force=True)
|
||||
await async_volume.write_file(f"{dir_path}/nested/file.txt", "Content")
|
||||
|
||||
await async_volume.remove(dir_path)
|
||||
|
||||
assert await async_volume.exists(dir_path) is False
|
||||
|
||||
async def test_remove_nonexistent_raises(self, async_volume: AsyncVolume):
|
||||
with pytest.raises(NotFoundException):
|
||||
await async_volume.remove("/non-existent.txt")
|
||||
|
||||
|
||||
class TestFileOperationsLifecycle:
|
||||
async def test_directory_with_multiple_files(self, async_volume: AsyncVolume):
|
||||
dir_path = "/multi-file-dir"
|
||||
await async_volume.make_dir(dir_path)
|
||||
|
||||
files = ["file1.txt", "file2.txt", "file3.txt"]
|
||||
for file_name in files:
|
||||
await async_volume.write_file(
|
||||
f"{dir_path}/{file_name}", f"Content of {file_name}"
|
||||
)
|
||||
|
||||
entries = await async_volume.list(dir_path)
|
||||
assert len(entries) >= len(files)
|
||||
|
||||
for file_name in files:
|
||||
content = await async_volume.read_file(
|
||||
f"{dir_path}/{file_name}", format="text"
|
||||
)
|
||||
assert content == f"Content of {file_name}"
|
||||
|
||||
await async_volume.remove(dir_path)
|
||||
assert await async_volume.exists(dir_path) is False
|
||||
@@ -0,0 +1,177 @@
|
||||
from http import HTTPStatus
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
|
||||
from e2b import AsyncVolume
|
||||
from e2b.exceptions import NotFoundException
|
||||
from e2b.api.client.models.volume_and_token import VolumeAndToken
|
||||
from e2b.api.client.types import Response
|
||||
import e2b.api.client.api.volumes.post_volumes as post_volumes_mod
|
||||
import e2b.api.client.api.volumes.get_volumes as get_volumes_mod
|
||||
import e2b.api.client.api.volumes.get_volumes_volume_id as get_volume_mod
|
||||
import e2b.api.client.api.volumes.delete_volumes_volume_id as delete_volume_mod
|
||||
|
||||
# In-memory store for mock volumes
|
||||
_volumes: dict[str, VolumeAndToken] = {}
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_volume_api(monkeypatch, test_api_key):
|
||||
monkeypatch.setenv("E2B_API_KEY", test_api_key)
|
||||
_volumes.clear()
|
||||
|
||||
async def mock_post_volumes(*, client, body):
|
||||
vol_id = str(uuid4())
|
||||
token = f"vol-token-{uuid4()}"
|
||||
vol = VolumeAndToken(volume_id=vol_id, name=body.name, token=token)
|
||||
_volumes[vol_id] = vol
|
||||
return Response(
|
||||
status_code=HTTPStatus(201),
|
||||
content=b"",
|
||||
headers={},
|
||||
parsed=vol,
|
||||
)
|
||||
|
||||
async def mock_get_volumes(*, client):
|
||||
return Response(
|
||||
status_code=HTTPStatus(200),
|
||||
content=b"",
|
||||
headers={},
|
||||
parsed=list(_volumes.values()),
|
||||
)
|
||||
|
||||
async def mock_get_volume(volume_id, *, client):
|
||||
vol = _volumes.get(volume_id)
|
||||
if vol is None:
|
||||
return Response(
|
||||
status_code=HTTPStatus(404),
|
||||
content=b"",
|
||||
headers={},
|
||||
parsed=None,
|
||||
)
|
||||
return Response(
|
||||
status_code=HTTPStatus(200),
|
||||
content=b"",
|
||||
headers={},
|
||||
parsed=vol,
|
||||
)
|
||||
|
||||
async def mock_delete_volume(volume_id, *, client):
|
||||
if volume_id not in _volumes:
|
||||
return Response(
|
||||
status_code=HTTPStatus(404),
|
||||
content=b"",
|
||||
headers={},
|
||||
parsed=None,
|
||||
)
|
||||
del _volumes[volume_id]
|
||||
return Response(
|
||||
status_code=HTTPStatus(204),
|
||||
content=b"",
|
||||
headers={},
|
||||
parsed=None,
|
||||
)
|
||||
|
||||
monkeypatch.setattr(post_volumes_mod, "asyncio_detailed", mock_post_volumes)
|
||||
monkeypatch.setattr(get_volumes_mod, "asyncio_detailed", mock_get_volumes)
|
||||
monkeypatch.setattr(get_volume_mod, "asyncio_detailed", mock_get_volume)
|
||||
monkeypatch.setattr(delete_volume_mod, "asyncio_detailed", mock_delete_volume)
|
||||
|
||||
|
||||
async def test_create_volume():
|
||||
vol = await AsyncVolume.create("test-volume")
|
||||
|
||||
assert vol is not None
|
||||
assert vol.volume_id is not None
|
||||
assert vol.name == "test-volume"
|
||||
assert vol.token is not None
|
||||
|
||||
|
||||
async def test_get_volume_info():
|
||||
created = await AsyncVolume.create("info-volume")
|
||||
info = await AsyncVolume.get_info(created.volume_id)
|
||||
|
||||
assert info.volume_id == created.volume_id
|
||||
assert info.name == "info-volume"
|
||||
assert info.token is not None
|
||||
|
||||
|
||||
async def test_list_volumes():
|
||||
await AsyncVolume.create("vol-a")
|
||||
await AsyncVolume.create("vol-b")
|
||||
|
||||
volumes = await AsyncVolume.list()
|
||||
|
||||
assert len(volumes) == 2
|
||||
names = sorted([v.name for v in volumes])
|
||||
assert names == ["vol-a", "vol-b"]
|
||||
|
||||
|
||||
async def test_list_volumes_empty():
|
||||
volumes = await AsyncVolume.list()
|
||||
assert len(volumes) == 0
|
||||
|
||||
|
||||
async def test_destroy_volume():
|
||||
vol = await AsyncVolume.create("to-delete")
|
||||
result = await AsyncVolume.destroy(vol.volume_id)
|
||||
|
||||
assert result is True
|
||||
|
||||
volumes = await AsyncVolume.list()
|
||||
assert len(volumes) == 0
|
||||
|
||||
|
||||
async def test_destroy_nonexistent_volume():
|
||||
result = await AsyncVolume.destroy("non-existent-id")
|
||||
assert result is False
|
||||
|
||||
|
||||
async def test_get_info_nonexistent_volume():
|
||||
with pytest.raises(NotFoundException):
|
||||
await AsyncVolume.get_info("non-existent-id")
|
||||
|
||||
|
||||
async def test_create_volume_keeps_proxy_for_content_calls():
|
||||
vol = await AsyncVolume.create(
|
||||
"proxy-volume", proxy="http://user:pass@127.0.0.1:8080"
|
||||
)
|
||||
|
||||
# The proxy is stored on the instance...
|
||||
assert vol._proxy == "http://user:pass@127.0.0.1:8080"
|
||||
|
||||
# ...and instance methods (which build a VolumeConnectionConfig with no
|
||||
# per-call proxy) pick it up rather than falling back to no proxy.
|
||||
config = vol._get_volume_config()
|
||||
assert config.proxy == "http://user:pass@127.0.0.1:8080"
|
||||
|
||||
|
||||
async def test_volume_per_call_proxy_overrides_instance():
|
||||
vol = await AsyncVolume.create("proxy-volume", proxy="http://127.0.0.1:8080")
|
||||
|
||||
config = vol._get_volume_config(proxy="http://127.0.0.1:9090")
|
||||
assert config.proxy == "http://127.0.0.1:9090"
|
||||
|
||||
|
||||
async def test_volume_full_lifecycle():
|
||||
# Create
|
||||
vol = await AsyncVolume.create("lifecycle-vol")
|
||||
assert vol.name == "lifecycle-vol"
|
||||
|
||||
# Get info
|
||||
info = await AsyncVolume.get_info(vol.volume_id)
|
||||
assert info.name == "lifecycle-vol"
|
||||
|
||||
# List
|
||||
volumes = await AsyncVolume.list()
|
||||
assert len(volumes) == 1
|
||||
assert volumes[0].volume_id == vol.volume_id
|
||||
|
||||
# Destroy
|
||||
destroyed = await AsyncVolume.destroy(vol.volume_id)
|
||||
assert destroyed is True
|
||||
|
||||
# List again
|
||||
volumes = await AsyncVolume.list()
|
||||
assert len(volumes) == 0
|
||||
@@ -0,0 +1,70 @@
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from e2b import AsyncVolume
|
||||
from e2b.exceptions import NotFoundException
|
||||
import e2b.volume.volume_async as volume_async_mod
|
||||
|
||||
_files = {
|
||||
"hello.txt": b"hello world",
|
||||
"empty.txt": b"",
|
||||
}
|
||||
|
||||
|
||||
def _handler(request: httpx.Request) -> httpx.Response:
|
||||
path = request.url.params.get("path")
|
||||
content = _files.get(path)
|
||||
if content is None:
|
||||
return httpx.Response(404)
|
||||
return httpx.Response(200, content=content)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def volume(monkeypatch) -> AsyncVolume:
|
||||
real_get_api_client = volume_async_mod.get_volume_api_client
|
||||
|
||||
def mock_get_api_client(config, **kwargs):
|
||||
client = real_get_api_client(config, **kwargs)
|
||||
client.set_async_httpx_client(
|
||||
httpx.AsyncClient(
|
||||
base_url=config.api_url,
|
||||
transport=httpx.MockTransport(_handler),
|
||||
)
|
||||
)
|
||||
return client
|
||||
|
||||
monkeypatch.setattr(volume_async_mod, "get_volume_api_client", mock_get_api_client)
|
||||
return AsyncVolume(volume_id="vol-1", name="test-volume", token="vol-token")
|
||||
|
||||
|
||||
async def test_read_file_stream_yields_content(volume: AsyncVolume):
|
||||
stream = await volume.read_file("hello.txt", format="stream")
|
||||
chunks = [chunk async for chunk in stream]
|
||||
assert b"".join(chunks) == b"hello world"
|
||||
|
||||
|
||||
async def test_read_file_stream_raises_for_missing_path(volume: AsyncVolume):
|
||||
with pytest.raises(NotFoundException):
|
||||
async for _ in await volume.read_file("missing.txt", format="stream"):
|
||||
pass
|
||||
|
||||
|
||||
async def test_read_file_stream_of_empty_file(volume: AsyncVolume):
|
||||
stream = await volume.read_file("empty.txt", format="stream")
|
||||
chunks = [chunk async for chunk in stream]
|
||||
assert b"".join(chunks) == b""
|
||||
|
||||
|
||||
async def test_read_file_text_and_bytes(volume: AsyncVolume):
|
||||
assert await volume.read_file("hello.txt") == "hello world"
|
||||
assert await volume.read_file("hello.txt", format="bytes") == b"hello world"
|
||||
|
||||
|
||||
async def test_read_file_text_and_bytes_of_empty_file(volume: AsyncVolume):
|
||||
assert await volume.read_file("empty.txt") == ""
|
||||
assert await volume.read_file("empty.txt", format="bytes") == b""
|
||||
|
||||
|
||||
async def test_read_file_missing_path_raises(volume: AsyncVolume):
|
||||
with pytest.raises(NotFoundException):
|
||||
await volume.read_file("missing.txt")
|
||||
@@ -0,0 +1,45 @@
|
||||
from e2b.sandbox.commands.command_handle import CommandExitException
|
||||
|
||||
import pytest
|
||||
|
||||
from e2b import Sandbox
|
||||
|
||||
|
||||
class Desktop(Sandbox):
|
||||
default_template = "desktop"
|
||||
|
||||
@staticmethod
|
||||
def _wrap_pyautogui_code(code: str):
|
||||
return f"""
|
||||
import pyautogui
|
||||
import os
|
||||
import Xlib.display
|
||||
|
||||
display = Xlib.display.Display(os.environ["DISPLAY"])
|
||||
pyautogui._pyautogui_x11._display = display
|
||||
|
||||
{code}
|
||||
exit(0)
|
||||
"""
|
||||
|
||||
def pyautogui(self, pyautogui_code: str):
|
||||
code_path = "/home/user/code-4f3a0850-1a83-47b2-8402-67b039a084ae.py"
|
||||
print(code_path)
|
||||
|
||||
code = self._wrap_pyautogui_code(pyautogui_code)
|
||||
|
||||
self.files.write(code_path, code)
|
||||
|
||||
self.commands.run(f"python {code_path}")
|
||||
|
||||
|
||||
@pytest.mark.skip
|
||||
def test_envelope_decode():
|
||||
with Desktop(timeout=30) as desktop:
|
||||
for _ in range(10):
|
||||
with pytest.raises(CommandExitException):
|
||||
desktop.pyautogui(
|
||||
"""
|
||||
pyautogui.write("Hello, ")
|
||||
"""
|
||||
)
|
||||
@@ -0,0 +1,280 @@
|
||||
import asyncio
|
||||
import os
|
||||
import random
|
||||
import string
|
||||
from typing import Callable, Dict, Optional
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from e2b import (
|
||||
AsyncCommandHandle,
|
||||
AsyncSandbox,
|
||||
AsyncTemplate,
|
||||
AsyncVolume,
|
||||
CommandExitException,
|
||||
CommandHandle,
|
||||
LogEntry,
|
||||
Sandbox,
|
||||
Template,
|
||||
TemplateClass,
|
||||
Volume,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_api_key() -> str:
|
||||
"""Placeholder API key with a valid format for tests that don't hit the API."""
|
||||
return "e2b_" + "0" * 40
|
||||
|
||||
|
||||
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
|
||||
def pytest_runtest_makereport(item, call):
|
||||
outcome = yield
|
||||
rep = outcome.get_result()
|
||||
if rep.when == "call":
|
||||
item._test_failed = rep.failed
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def sandbox_test_id():
|
||||
return f"test_{_generate_random_string()}"
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def template():
|
||||
return "base"
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def sandbox_factory(request, template, sandbox_test_id):
|
||||
def factory(*, template_name: str = template, **kwargs):
|
||||
metadata = kwargs.setdefault("metadata", dict())
|
||||
metadata.setdefault("sandbox_test_id", sandbox_test_id)
|
||||
|
||||
sandbox = Sandbox.create(template_name, **kwargs)
|
||||
|
||||
def finalizer():
|
||||
if getattr(request.node, "_test_failed", False):
|
||||
print(f"\n[TEST FAILED] Sandbox ID: {sandbox.sandbox_id}")
|
||||
sandbox.kill()
|
||||
|
||||
request.addfinalizer(finalizer)
|
||||
|
||||
return sandbox
|
||||
|
||||
return factory
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def sandbox(sandbox_factory):
|
||||
return sandbox_factory()
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def async_sandbox_factory(request, template, sandbox_test_id):
|
||||
sandboxes: list = []
|
||||
|
||||
async def factory(*, template_name: str = template, **kwargs):
|
||||
metadata = kwargs.setdefault("metadata", dict())
|
||||
metadata.setdefault("sandbox_test_id", sandbox_test_id)
|
||||
|
||||
sandbox = await AsyncSandbox.create(template_name, **kwargs)
|
||||
sandboxes.append(sandbox)
|
||||
return sandbox
|
||||
|
||||
yield factory
|
||||
|
||||
if getattr(request.node, "_test_failed", False):
|
||||
for sandbox in sandboxes:
|
||||
print(f"\n[TEST FAILED] Sandbox ID: {sandbox.sandbox_id}")
|
||||
|
||||
results = await asyncio.gather(
|
||||
*(sandbox.kill() for sandbox in sandboxes), return_exceptions=True
|
||||
)
|
||||
for sandbox, result in zip(sandboxes, results):
|
||||
if isinstance(result, BaseException):
|
||||
print(f"\n[TEARDOWN FAILED] Sandbox ID: {sandbox.sandbox_id}: {result!r}")
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def async_sandbox(async_sandbox_factory):
|
||||
return await async_sandbox_factory()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def build():
|
||||
def _build(
|
||||
template: TemplateClass,
|
||||
name: Optional[str] = None,
|
||||
skip_cache: bool = False,
|
||||
on_build_logs: Optional[Callable[[LogEntry], None]] = None,
|
||||
):
|
||||
build_name = name or f"e2b-test-{_generate_random_string()}"
|
||||
build_info: Dict[str, Optional[str]] = {"template_id": None, "build_id": None}
|
||||
|
||||
def capture_logs(log: LogEntry):
|
||||
import re
|
||||
|
||||
if "Template created with ID:" in log.message:
|
||||
match = re.search(
|
||||
r"Template created with ID: ([^,]+), Build ID: (.+)", log.message
|
||||
)
|
||||
if match:
|
||||
build_info["template_id"] = match.group(1)
|
||||
build_info["build_id"] = match.group(2)
|
||||
if on_build_logs:
|
||||
on_build_logs(log)
|
||||
|
||||
try:
|
||||
return Template.build(
|
||||
template,
|
||||
build_name,
|
||||
cpu_count=1,
|
||||
memory_mb=1024,
|
||||
skip_cache=skip_cache,
|
||||
on_build_logs=capture_logs,
|
||||
)
|
||||
except Exception as e:
|
||||
print(
|
||||
f"\n[BUILD FAILED] name={build_name}, "
|
||||
f"template_id={build_info['template_id']}, "
|
||||
f"build_id={build_info['build_id']}, error={e}"
|
||||
)
|
||||
raise
|
||||
|
||||
return _build
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
def async_build():
|
||||
async def _async_build(
|
||||
template: TemplateClass,
|
||||
name: Optional[str] = None,
|
||||
skip_cache: bool = False,
|
||||
on_build_logs: Optional[Callable[[LogEntry], None]] = None,
|
||||
):
|
||||
build_name = name or f"e2b-test-{_generate_random_string()}"
|
||||
build_info: Dict[str, Optional[str]] = {"template_id": None, "build_id": None}
|
||||
|
||||
def capture_logs(log: LogEntry):
|
||||
import re
|
||||
|
||||
if "Template created with ID:" in log.message:
|
||||
match = re.search(
|
||||
r"Template created with ID: ([^,]+), Build ID: (.+)", log.message
|
||||
)
|
||||
if match:
|
||||
build_info["template_id"] = match.group(1)
|
||||
build_info["build_id"] = match.group(2)
|
||||
if on_build_logs:
|
||||
on_build_logs(log)
|
||||
|
||||
try:
|
||||
return await AsyncTemplate.build(
|
||||
template,
|
||||
build_name,
|
||||
cpu_count=1,
|
||||
memory_mb=1024,
|
||||
skip_cache=skip_cache,
|
||||
on_build_logs=capture_logs,
|
||||
)
|
||||
except Exception as e:
|
||||
print(
|
||||
f"\n[BUILD FAILED] name={build_name}, "
|
||||
f"template_id={build_info['template_id']}, "
|
||||
f"build_id={build_info['build_id']}, error={e}"
|
||||
)
|
||||
raise
|
||||
|
||||
return _async_build
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def debug():
|
||||
return os.getenv("E2B_DEBUG") is not None
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def skip_by_debug(request, debug):
|
||||
if request.node.get_closest_marker("skip_debug"):
|
||||
if debug:
|
||||
pytest.skip("skipped because E2B_DEBUG is set")
|
||||
|
||||
|
||||
class Helpers:
|
||||
@staticmethod
|
||||
def catch_cmd_exit_error_in_background(cmd: AsyncCommandHandle):
|
||||
disabled = False
|
||||
|
||||
async def wait_for_exit():
|
||||
try:
|
||||
await cmd.wait()
|
||||
except CommandExitException as e:
|
||||
if not disabled:
|
||||
assert False, (
|
||||
f"command failed with exit code {e.exit_code}: {e.stderr}"
|
||||
)
|
||||
|
||||
asyncio.create_task(wait_for_exit())
|
||||
|
||||
def disable():
|
||||
nonlocal disabled
|
||||
disabled = True
|
||||
|
||||
return disable
|
||||
|
||||
@staticmethod
|
||||
def check_cmd_exit_error(cmd: CommandHandle):
|
||||
try:
|
||||
cmd.wait()
|
||||
except CommandExitException as e:
|
||||
assert False, f"command failed with exit code {e.exit_code}: {e.stderr}"
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def helpers():
|
||||
return Helpers
|
||||
|
||||
|
||||
def _generate_random_string(length: int = 8) -> str:
|
||||
return "".join(random.choices(string.ascii_lowercase + string.digits, k=length))
|
||||
|
||||
|
||||
def _skip_unless_volume_tests_enabled():
|
||||
if os.getenv("ENABLE_VOLUME_TESTS") is None:
|
||||
pytest.skip("skipped because ENABLE_VOLUME_TESTS is not set")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def volume(request):
|
||||
_skip_unless_volume_tests_enabled()
|
||||
vol = Volume.create(f"test-vol-{_generate_random_string()}")
|
||||
|
||||
def finalizer():
|
||||
if getattr(request.node, "_test_failed", False):
|
||||
print(f"\n[TEST FAILED] Volume ID: {vol.volume_id}")
|
||||
try:
|
||||
Volume.destroy(vol.volume_id)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
request.addfinalizer(finalizer)
|
||||
return vol
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def async_volume(request):
|
||||
_skip_unless_volume_tests_enabled()
|
||||
vol = await AsyncVolume.create(f"test-vol-{_generate_random_string()}")
|
||||
try:
|
||||
yield vol
|
||||
finally:
|
||||
if getattr(request.node, "_test_failed", False):
|
||||
print(f"\n[TEST FAILED] Volume ID: {vol.volume_id}")
|
||||
try:
|
||||
await AsyncVolume.destroy(vol.volume_id)
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,171 @@
|
||||
import asyncio
|
||||
|
||||
import pytest
|
||||
|
||||
from e2b_connect.client import (
|
||||
EnvelopeFlags,
|
||||
ServerStreamParser,
|
||||
_retry,
|
||||
encode_envelope,
|
||||
)
|
||||
|
||||
|
||||
class GoodError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class BadError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def test_sync_retry_after_expected_exception():
|
||||
total = 0
|
||||
|
||||
@_retry(GoodError, 1)
|
||||
def f():
|
||||
nonlocal total
|
||||
total += 1
|
||||
raise GoodError()
|
||||
|
||||
with pytest.raises(GoodError):
|
||||
f()
|
||||
|
||||
assert total == 2
|
||||
|
||||
|
||||
def test_sync_do_not_retry_on_unexpected_exception():
|
||||
total = 0
|
||||
|
||||
@_retry(GoodError, 1)
|
||||
def f():
|
||||
nonlocal total
|
||||
total += 1
|
||||
raise BadError()
|
||||
|
||||
with pytest.raises(BadError):
|
||||
f()
|
||||
|
||||
assert total == 1
|
||||
|
||||
|
||||
def test_sync_do_not_throw_when_retry_works():
|
||||
total = 0
|
||||
|
||||
@_retry(GoodError, 1)
|
||||
def f():
|
||||
nonlocal total
|
||||
total += 1
|
||||
|
||||
if total < 2:
|
||||
raise GoodError()
|
||||
|
||||
return True
|
||||
|
||||
result = f()
|
||||
assert result is True
|
||||
assert total == 2
|
||||
|
||||
|
||||
async def test_async_retry_after_expected_exception():
|
||||
total = 0
|
||||
|
||||
@_retry(GoodError, 1)
|
||||
async def f():
|
||||
nonlocal total
|
||||
total += 1
|
||||
raise GoodError()
|
||||
|
||||
with pytest.raises(GoodError):
|
||||
await f()
|
||||
|
||||
assert total == 2
|
||||
|
||||
|
||||
async def test_async_do_not_retry_on_unexpected_exception():
|
||||
total = 0
|
||||
|
||||
@_retry(GoodError, 1)
|
||||
async def f():
|
||||
nonlocal total
|
||||
total += 1
|
||||
raise BadError()
|
||||
|
||||
with pytest.raises(BadError):
|
||||
await f()
|
||||
|
||||
assert total == 1
|
||||
|
||||
|
||||
async def test_async_do_not_throw_when_retry_works():
|
||||
total = 0
|
||||
|
||||
@_retry(GoodError, 1)
|
||||
async def f():
|
||||
nonlocal total
|
||||
total += 1
|
||||
|
||||
if total < 2:
|
||||
raise GoodError()
|
||||
|
||||
return True
|
||||
|
||||
result = await f()
|
||||
assert result is True
|
||||
assert total == 2
|
||||
|
||||
|
||||
async def test_async_with_multiple_await_calls():
|
||||
total = 0
|
||||
|
||||
async def a():
|
||||
await asyncio.sleep(0.001)
|
||||
|
||||
@_retry(GoodError, 1)
|
||||
async def f():
|
||||
nonlocal total
|
||||
total += 1
|
||||
|
||||
await a()
|
||||
|
||||
if total < 2:
|
||||
raise GoodError()
|
||||
|
||||
await a()
|
||||
|
||||
return True
|
||||
|
||||
result = await f()
|
||||
assert result is True
|
||||
assert total == 2
|
||||
|
||||
|
||||
def make_parser():
|
||||
return ServerStreamParser(
|
||||
decode=lambda data, msg_type: data,
|
||||
response_type=None,
|
||||
)
|
||||
|
||||
|
||||
def test_parser_yields_messages_from_single_chunk():
|
||||
parser = make_parser()
|
||||
envelope = encode_envelope(flags=EnvelopeFlags(0), data=b"abc")
|
||||
|
||||
assert list(parser.parse(envelope)) == [b"abc"]
|
||||
|
||||
|
||||
def test_parser_handles_payload_split_after_header():
|
||||
parser = make_parser()
|
||||
envelope = encode_envelope(flags=EnvelopeFlags(0), data=b"abc")
|
||||
|
||||
# The header plus one payload byte arrive first; the remaining payload
|
||||
# (shorter than a header) arrives in a later chunk.
|
||||
assert list(parser.parse(envelope[:6])) == []
|
||||
assert list(parser.parse(envelope[6:])) == [b"abc"]
|
||||
|
||||
|
||||
def test_parser_handles_end_stream_payload_shorter_than_header():
|
||||
parser = make_parser()
|
||||
envelope = encode_envelope(flags=EnvelopeFlags.end_stream, data=b"{}")
|
||||
|
||||
assert list(parser.parse(envelope[:5])) == []
|
||||
assert list(parser.parse(envelope[5:])) == []
|
||||
@@ -0,0 +1,74 @@
|
||||
import random
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
|
||||
BASE_DIR = "/tmp/test-git"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def git_sandbox(sandbox_factory):
|
||||
return sandbox_factory(timeout=10)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def git_author():
|
||||
return "Sandbox Bot", "sandbox@example.com"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def git_credentials():
|
||||
return "git", "token", "example.com", "https"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def git_base_dir(git_sandbox):
|
||||
base_dir = f"{BASE_DIR}/{uuid4().hex}"
|
||||
git_sandbox.commands.run(f'rm -rf "{base_dir}" && mkdir -p "{base_dir}"')
|
||||
yield base_dir
|
||||
git_sandbox.commands.run(f'rm -rf "{base_dir}"')
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def git_repo(git_sandbox, git_base_dir, git_author):
|
||||
repo_path = f"{git_base_dir}/repo"
|
||||
git_sandbox.git.init(repo_path, initial_branch="main")
|
||||
author_name, author_email = git_author
|
||||
git_sandbox.git.configure_user(author_name, author_email)
|
||||
return repo_path
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def git_repo_with_commit(git_sandbox, git_repo, git_author):
|
||||
author_name, author_email = git_author
|
||||
git_sandbox.files.write(f"{git_repo}/README.md", "hello\n")
|
||||
git_sandbox.git.add(git_repo, all=True)
|
||||
git_sandbox.git.commit(
|
||||
git_repo,
|
||||
message="Initial commit",
|
||||
author_name=author_name,
|
||||
author_email=author_email,
|
||||
)
|
||||
return git_repo
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def git_daemon(git_sandbox, git_base_dir):
|
||||
remote_path = f"{git_base_dir}/remote.git"
|
||||
git_sandbox.commands.run(f'git init --bare --initial-branch=main "{remote_path}"')
|
||||
port = 9418 + random.randint(0, 1000)
|
||||
cmd = (
|
||||
f'git daemon --reuseaddr --base-path="{git_base_dir}" --export-all '
|
||||
f"--enable=receive-pack --informative-errors --listen=127.0.0.1 --port={port}"
|
||||
)
|
||||
handle = git_sandbox.commands.run(cmd, background=True)
|
||||
git_sandbox.commands.run("sleep 1")
|
||||
try:
|
||||
yield {
|
||||
"remote_path": remote_path,
|
||||
"remote_url": f"git://127.0.0.1:{port}/remote.git",
|
||||
"port": port,
|
||||
"base_dir": git_base_dir,
|
||||
}
|
||||
finally:
|
||||
handle.kill()
|
||||
@@ -0,0 +1,16 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_add_stages_files(git_sandbox, git_repo):
|
||||
git_sandbox.files.write(f"{git_repo}/README.md", "hello\n")
|
||||
|
||||
git_sandbox.git.add(git_repo, all=True)
|
||||
|
||||
status = git_sandbox.git.status(git_repo)
|
||||
entry = next(
|
||||
(item for item in status.file_status if item.name == "README.md"), None
|
||||
)
|
||||
assert entry is not None
|
||||
assert entry.status == "added"
|
||||
assert entry.staged is True
|
||||
@@ -0,0 +1,31 @@
|
||||
import pytest
|
||||
|
||||
from e2b.exceptions import InvalidArgumentException
|
||||
from e2b.sandbox._git import (
|
||||
build_remote_add_args,
|
||||
build_remote_get_command,
|
||||
build_reset_args,
|
||||
)
|
||||
|
||||
|
||||
def test_build_reset_args_rejects_invalid_mode():
|
||||
with pytest.raises(InvalidArgumentException) as exc:
|
||||
build_reset_args("bogus", None, None)
|
||||
# Order must match the JS SDK exactly.
|
||||
assert "Reset mode must be one of soft, mixed, hard, merge, keep." in str(exc.value)
|
||||
|
||||
|
||||
def test_build_reset_args_accepts_valid_mode():
|
||||
assert build_reset_args("hard", "HEAD", None) == ["reset", "--hard", "HEAD"]
|
||||
|
||||
|
||||
def test_build_remote_add_args_requires_name_and_url():
|
||||
with pytest.raises(InvalidArgumentException):
|
||||
build_remote_add_args("", "https://example.com", False)
|
||||
with pytest.raises(InvalidArgumentException):
|
||||
build_remote_add_args("origin", "", False)
|
||||
|
||||
|
||||
def test_build_remote_get_command_requires_name():
|
||||
with pytest.raises(InvalidArgumentException):
|
||||
build_remote_get_command("/repo", "")
|
||||
@@ -0,0 +1,52 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_branches_lists_current_and_feature(git_sandbox, git_repo_with_commit):
|
||||
repo_path = git_repo_with_commit
|
||||
|
||||
git_sandbox.commands.run(f'git -C "{repo_path}" branch feature')
|
||||
|
||||
branches = git_sandbox.git.branches(repo_path)
|
||||
assert branches.current_branch == "main"
|
||||
assert "main" in branches.branches
|
||||
assert "feature" in branches.branches
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_checkout_branch_switches_branch(git_sandbox, git_repo_with_commit):
|
||||
repo_path = git_repo_with_commit
|
||||
|
||||
git_sandbox.commands.run(f'git -C "{repo_path}" branch feature')
|
||||
git_sandbox.git.checkout_branch(repo_path, "feature")
|
||||
|
||||
head = git_sandbox.commands.run(
|
||||
f'git -C "{repo_path}" rev-parse --abbrev-ref HEAD'
|
||||
).stdout.strip()
|
||||
assert head == "feature"
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_create_branch_creates_branch(git_sandbox, git_repo_with_commit):
|
||||
repo_path = git_repo_with_commit
|
||||
|
||||
git_sandbox.git.create_branch(repo_path, "feature")
|
||||
|
||||
branches = git_sandbox.git.branches(repo_path)
|
||||
assert "feature" in branches.branches
|
||||
assert branches.current_branch == "feature"
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_delete_branch_removes_branch(git_sandbox, git_repo_with_commit):
|
||||
repo_path = git_repo_with_commit
|
||||
|
||||
git_sandbox.commands.run(f'git -C "{repo_path}" branch feature')
|
||||
git_sandbox.git.delete_branch(repo_path, "feature")
|
||||
|
||||
branch = git_sandbox.commands.run(
|
||||
f'git -C "{repo_path}" branch --list feature'
|
||||
).stdout.strip()
|
||||
branches = git_sandbox.git.branches(repo_path)
|
||||
assert branch == ""
|
||||
assert "feature" not in branches.branches
|
||||
@@ -0,0 +1,22 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_clone_fetches_repo(
|
||||
git_sandbox, git_repo_with_commit, git_daemon, git_base_dir
|
||||
):
|
||||
repo_path = git_repo_with_commit
|
||||
remote_url = git_daemon["remote_url"]
|
||||
clone_path = f"{git_base_dir}/clone"
|
||||
|
||||
git_sandbox.git.remote_add(repo_path, "origin", remote_url)
|
||||
git_sandbox.git.push(
|
||||
repo_path,
|
||||
remote="origin",
|
||||
branch="main",
|
||||
set_upstream=True,
|
||||
)
|
||||
|
||||
git_sandbox.git.clone(remote_url, clone_path)
|
||||
contents = git_sandbox.files.read(f"{clone_path}/README.md")
|
||||
assert "hello" in contents
|
||||
@@ -0,0 +1,46 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_commit_creates_commit(git_sandbox, git_repo, git_author):
|
||||
git_sandbox.set_timeout(20)
|
||||
|
||||
author_name, author_email = git_author
|
||||
|
||||
git_sandbox.files.write(f"{git_repo}/README.md", "hello\n")
|
||||
git_sandbox.git.add(git_repo, all=True)
|
||||
git_sandbox.git.commit(
|
||||
git_repo,
|
||||
message="Initial commit",
|
||||
author_name=author_name,
|
||||
author_email=author_email,
|
||||
)
|
||||
|
||||
message = git_sandbox.commands.run(
|
||||
f'git -C "{git_repo}" log -1 --pretty=%B'
|
||||
).stdout.strip()
|
||||
assert message == "Initial commit"
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_commit_uses_config_for_missing_author(git_sandbox, git_repo, git_author):
|
||||
_, expected_email = git_author
|
||||
|
||||
git_sandbox.files.write(f"{git_repo}/README.md", "hello\n")
|
||||
git_sandbox.git.add(git_repo, all=True)
|
||||
override_name = "Override Bot"
|
||||
git_sandbox.git.commit(
|
||||
git_repo,
|
||||
message="Partial author commit",
|
||||
author_name=override_name,
|
||||
)
|
||||
|
||||
author_name = git_sandbox.commands.run(
|
||||
f'git -C "{git_repo}" log -1 --pretty=%an'
|
||||
).stdout.strip()
|
||||
logged_email = git_sandbox.commands.run(
|
||||
f'git -C "{git_repo}" log -1 --pretty=%ae'
|
||||
).stdout.strip()
|
||||
|
||||
assert author_name == override_name
|
||||
assert logged_email == expected_email
|
||||
@@ -0,0 +1,52 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_get_config_reads_local_config(git_sandbox, git_repo):
|
||||
git_sandbox.commands.run(f'git -C "{git_repo}" config --local pull.rebase true')
|
||||
|
||||
value = git_sandbox.git.get_config("pull.rebase", scope="local", path=git_repo)
|
||||
command_value = git_sandbox.commands.run(
|
||||
f'git -C "{git_repo}" config --local --get pull.rebase'
|
||||
).stdout.strip()
|
||||
assert value == "true"
|
||||
assert command_value == "true"
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_set_config_updates_local_config(git_sandbox, git_repo):
|
||||
git_sandbox.git.set_config(
|
||||
"pull.rebase",
|
||||
"true",
|
||||
scope="local",
|
||||
path=git_repo,
|
||||
)
|
||||
|
||||
value = git_sandbox.commands.run(
|
||||
f'git -C "{git_repo}" config --local --get pull.rebase'
|
||||
).stdout.strip()
|
||||
configured_value = git_sandbox.git.get_config(
|
||||
"pull.rebase", scope="local", path=git_repo
|
||||
)
|
||||
assert value == "true"
|
||||
assert configured_value == "true"
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_configure_user_sets_global_config(git_sandbox, git_author):
|
||||
author_name, author_email = git_author
|
||||
|
||||
git_sandbox.git.configure_user(author_name, author_email)
|
||||
|
||||
name = git_sandbox.commands.run(
|
||||
"git config --global --get user.name"
|
||||
).stdout.strip()
|
||||
email = git_sandbox.commands.run(
|
||||
"git config --global --get user.email"
|
||||
).stdout.strip()
|
||||
configured_name = git_sandbox.git.get_config("user.name", scope="global")
|
||||
configured_email = git_sandbox.git.get_config("user.email", scope="global")
|
||||
assert name == author_name
|
||||
assert email == author_email
|
||||
assert configured_name == author_name
|
||||
assert configured_email == author_email
|
||||
@@ -0,0 +1,20 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_dangerously_authenticate_sets_helper(git_sandbox, git_credentials):
|
||||
username, password, host, protocol = git_credentials
|
||||
|
||||
git_sandbox.git.dangerously_authenticate(
|
||||
username,
|
||||
password,
|
||||
host=host,
|
||||
protocol=protocol,
|
||||
)
|
||||
|
||||
helper = git_sandbox.commands.run(
|
||||
"git config --global --get credential.helper"
|
||||
).stdout.strip()
|
||||
configured_helper = git_sandbox.git.get_config("credential.helper", scope="global")
|
||||
assert helper == "store"
|
||||
assert configured_helper == "store"
|
||||
@@ -0,0 +1,14 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_init_creates_repo(git_sandbox, git_base_dir):
|
||||
repo_path = f"{git_base_dir}/repo"
|
||||
|
||||
git_sandbox.git.init(repo_path, initial_branch="main")
|
||||
|
||||
assert git_sandbox.files.exists(f"{repo_path}/.git")
|
||||
head = git_sandbox.commands.run(
|
||||
f'git -C "{repo_path}" symbolic-ref --short HEAD'
|
||||
).stdout.strip()
|
||||
assert head == "main"
|
||||
@@ -0,0 +1,33 @@
|
||||
import inspect
|
||||
|
||||
from e2b.sandbox_async.git import Git as AsyncGit
|
||||
from e2b.sandbox_sync.git import Git as SyncGit
|
||||
|
||||
|
||||
def _public_methods(cls):
|
||||
return {
|
||||
name: getattr(cls, name)
|
||||
for name in sorted(dir(cls))
|
||||
if not name.startswith("_") and callable(getattr(cls, name))
|
||||
}
|
||||
|
||||
|
||||
def test_identical_method_signatures():
|
||||
sync = _public_methods(SyncGit)
|
||||
async_ = _public_methods(AsyncGit)
|
||||
|
||||
assert set(sync) == set(async_), (
|
||||
f"missing from async: {set(sync) - set(async_)}, "
|
||||
f"missing from sync: {set(async_) - set(sync)}"
|
||||
)
|
||||
|
||||
for name in sync:
|
||||
assert inspect.signature(sync[name]) == inspect.signature(async_[name]), (
|
||||
f"{name}: sync{inspect.signature(sync[name])} "
|
||||
f"!= async{inspect.signature(async_[name])}"
|
||||
)
|
||||
|
||||
|
||||
def test_async_methods_are_coroutines():
|
||||
for name, method in _public_methods(AsyncGit).items():
|
||||
assert inspect.iscoroutinefunction(method), f"AsyncGit.{name} is not async"
|
||||
@@ -0,0 +1,44 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_remote_get_returns_none_for_missing_remote(git_sandbox, git_repo):
|
||||
repo_path = git_repo
|
||||
missing_url = git_sandbox.git.remote_get(repo_path, "origin")
|
||||
assert missing_url is None
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_remote_add_adds_remote(git_sandbox, git_repo, git_daemon):
|
||||
repo_path = git_repo
|
||||
remote_url = git_daemon["remote_url"]
|
||||
|
||||
git_sandbox.git.remote_add(repo_path, "origin", remote_url)
|
||||
current_url = git_sandbox.git.remote_get(repo_path, "origin")
|
||||
assert current_url == remote_url
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_remote_add_overwrites_existing_remote(git_sandbox, git_repo, git_daemon):
|
||||
repo_path = git_repo
|
||||
remote_url = git_daemon["remote_url"]
|
||||
|
||||
git_sandbox.git.remote_add(repo_path, "origin", remote_url)
|
||||
current_url = git_sandbox.commands.run(
|
||||
f'git -C "{repo_path}" remote get-url origin'
|
||||
).stdout.strip()
|
||||
current_remote = git_sandbox.git.remote_get(repo_path, "origin")
|
||||
assert current_url == remote_url
|
||||
assert current_remote == remote_url
|
||||
|
||||
second_path = f"{git_daemon['base_dir']}/remote-2.git"
|
||||
git_sandbox.commands.run(f'git init --bare --initial-branch=main "{second_path}"')
|
||||
second_url = f"git://127.0.0.1:{git_daemon['port']}/remote-2.git"
|
||||
git_sandbox.git.remote_add(repo_path, "origin", second_url, overwrite=True)
|
||||
|
||||
updated_url = git_sandbox.commands.run(
|
||||
f'git -C "{repo_path}" remote get-url origin'
|
||||
).stdout.strip()
|
||||
updated_remote = git_sandbox.git.remote_get(repo_path, "origin")
|
||||
assert updated_url == second_url
|
||||
assert updated_remote == second_url
|
||||
@@ -0,0 +1,17 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_reset_hard_discards_changes(git_sandbox, git_repo_with_commit):
|
||||
git_sandbox.files.write(f"{git_repo_with_commit}/README.md", "changed\n")
|
||||
|
||||
status = git_sandbox.git.status(git_repo_with_commit)
|
||||
assert status.is_clean is False
|
||||
|
||||
git_sandbox.git.reset(git_repo_with_commit, mode="hard", target="HEAD")
|
||||
|
||||
status_after = git_sandbox.git.status(git_repo_with_commit)
|
||||
assert status_after.is_clean is True
|
||||
|
||||
contents = git_sandbox.files.read(f"{git_repo_with_commit}/README.md")
|
||||
assert contents == "hello\n"
|
||||
@@ -0,0 +1,37 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_restore_unstages_changes(git_sandbox, git_repo_with_commit):
|
||||
git_sandbox.files.write(f"{git_repo_with_commit}/README.md", "changed\n")
|
||||
git_sandbox.git.add(git_repo_with_commit, files=["README.md"])
|
||||
|
||||
status = git_sandbox.git.status(git_repo_with_commit)
|
||||
assert status.has_staged is True
|
||||
|
||||
git_sandbox.git.restore(
|
||||
git_repo_with_commit,
|
||||
paths=["README.md"],
|
||||
staged=True,
|
||||
worktree=False,
|
||||
)
|
||||
|
||||
status_after = git_sandbox.git.status(git_repo_with_commit)
|
||||
assert status_after.has_staged is False
|
||||
assert status_after.has_changes is True
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_restore_worktree_discards_changes(git_sandbox, git_repo_with_commit):
|
||||
git_sandbox.files.write(f"{git_repo_with_commit}/README.md", "changed\n")
|
||||
|
||||
status = git_sandbox.git.status(git_repo_with_commit)
|
||||
assert status.is_clean is False
|
||||
|
||||
git_sandbox.git.restore(git_repo_with_commit, paths=["README.md"])
|
||||
|
||||
status_after = git_sandbox.git.status(git_repo_with_commit)
|
||||
assert status_after.is_clean is True
|
||||
|
||||
contents = git_sandbox.files.read(f"{git_repo_with_commit}/README.md")
|
||||
assert contents == "hello\n"
|
||||
@@ -0,0 +1,88 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_status_reports_untracked_file(git_sandbox, git_repo):
|
||||
git_sandbox.files.write(f"{git_repo}/README.md", "hello\n")
|
||||
|
||||
status = git_sandbox.git.status(git_repo)
|
||||
entry = next(
|
||||
(item for item in status.file_status if item.name == "README.md"), None
|
||||
)
|
||||
|
||||
assert entry is not None
|
||||
assert entry.status == "untracked"
|
||||
assert status.is_clean is False
|
||||
assert status.has_changes is True
|
||||
assert status.has_untracked is True
|
||||
assert status.has_staged is False
|
||||
assert status.has_conflicts is False
|
||||
assert status.total_count == 1
|
||||
assert status.staged_count == 0
|
||||
assert status.unstaged_count == 1
|
||||
assert status.untracked_count == 1
|
||||
assert status.conflict_count == 0
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_status_reports_added_modified_deleted_renamed(
|
||||
git_sandbox, git_repo, git_author
|
||||
):
|
||||
author_name, author_email = git_author
|
||||
|
||||
git_sandbox.files.write(f"{git_repo}/README.md", "hello\n")
|
||||
git_sandbox.files.write(f"{git_repo}/DELETE.md", "delete me\n")
|
||||
git_sandbox.files.write(f"{git_repo}/RENAME.md", "rename me\n")
|
||||
git_sandbox.git.add(git_repo, all=True)
|
||||
git_sandbox.git.commit(
|
||||
git_repo,
|
||||
message="Initial commit",
|
||||
author_name=author_name,
|
||||
author_email=author_email,
|
||||
)
|
||||
|
||||
git_sandbox.files.write(f"{git_repo}/README.md", "hello again\n")
|
||||
git_sandbox.files.write(f"{git_repo}/NEW.md", "new file\n")
|
||||
git_sandbox.git.add(git_repo, files=["NEW.md"])
|
||||
git_sandbox.commands.run(f'git -C "{git_repo}" rm DELETE.md')
|
||||
git_sandbox.commands.run(f'git -C "{git_repo}" mv RENAME.md RENAMED.md')
|
||||
|
||||
status = git_sandbox.git.status(git_repo)
|
||||
modified = next(
|
||||
(item for item in status.file_status if item.name == "README.md"), None
|
||||
)
|
||||
added = next((item for item in status.file_status if item.name == "NEW.md"), None)
|
||||
deleted = next(
|
||||
(item for item in status.file_status if item.name == "DELETE.md"), None
|
||||
)
|
||||
renamed = next(
|
||||
(item for item in status.file_status if item.name == "RENAMED.md"),
|
||||
None,
|
||||
)
|
||||
|
||||
assert modified is not None
|
||||
assert modified.status == "modified"
|
||||
assert modified.staged is False
|
||||
|
||||
assert added is not None
|
||||
assert added.status == "added"
|
||||
assert added.staged is True
|
||||
|
||||
assert deleted is not None
|
||||
assert deleted.status == "deleted"
|
||||
assert deleted.staged is True
|
||||
|
||||
assert renamed is not None
|
||||
assert renamed.status == "renamed"
|
||||
assert renamed.staged is True
|
||||
assert renamed.renamed_from == "RENAME.md"
|
||||
|
||||
assert status.has_changes is True
|
||||
assert status.has_staged is True
|
||||
assert status.has_untracked is False
|
||||
assert status.has_conflicts is False
|
||||
assert status.total_count == 4
|
||||
assert status.staged_count == 3
|
||||
assert status.unstaged_count == 1
|
||||
assert status.untracked_count == 0
|
||||
assert status.conflict_count == 0
|
||||
@@ -0,0 +1,81 @@
|
||||
import pytest
|
||||
|
||||
from e2b.exceptions import GitUpstreamException
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_push_updates_remote(git_sandbox, git_repo_with_commit, git_daemon):
|
||||
repo_path = git_repo_with_commit
|
||||
remote_url = git_daemon["remote_url"]
|
||||
|
||||
git_sandbox.git.remote_add(repo_path, "origin", remote_url)
|
||||
git_sandbox.git.push(
|
||||
repo_path,
|
||||
remote="origin",
|
||||
branch="main",
|
||||
set_upstream=True,
|
||||
)
|
||||
|
||||
message = git_sandbox.commands.run(
|
||||
f'git --git-dir="{git_daemon["remote_path"]}" log -1 --pretty=%B'
|
||||
).stdout.strip()
|
||||
assert message == "Initial commit"
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_push_without_upstream_warns(git_sandbox, git_repo_with_commit, git_daemon):
|
||||
repo_path = git_repo_with_commit
|
||||
remote_url = git_daemon["remote_url"]
|
||||
|
||||
git_sandbox.git.remote_add(repo_path, "origin", remote_url)
|
||||
|
||||
with pytest.raises(GitUpstreamException) as exc:
|
||||
git_sandbox.git.push(repo_path, set_upstream=False)
|
||||
|
||||
assert "no upstream branch is configured" in str(exc.value).lower()
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_pull_updates_clone(
|
||||
git_sandbox, git_repo_with_commit, git_daemon, git_base_dir, git_author
|
||||
):
|
||||
repo_path = git_repo_with_commit
|
||||
remote_url = git_daemon["remote_url"]
|
||||
clone_path = f"{git_base_dir}/clone"
|
||||
|
||||
git_sandbox.git.remote_add(repo_path, "origin", remote_url)
|
||||
git_sandbox.git.push(
|
||||
repo_path,
|
||||
remote="origin",
|
||||
branch="main",
|
||||
set_upstream=True,
|
||||
)
|
||||
git_sandbox.git.clone(remote_url, clone_path)
|
||||
|
||||
git_sandbox.files.write(f"{repo_path}/README.md", "hello\nmore\n")
|
||||
author_name, author_email = git_author
|
||||
git_sandbox.git.add(repo_path, all=True)
|
||||
git_sandbox.git.commit(
|
||||
repo_path,
|
||||
message="Update README",
|
||||
author_name=author_name,
|
||||
author_email=author_email,
|
||||
)
|
||||
git_sandbox.git.push(repo_path)
|
||||
|
||||
git_sandbox.git.pull(clone_path)
|
||||
contents = git_sandbox.files.read(f"{clone_path}/README.md")
|
||||
assert "more" in contents
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_pull_without_upstream_warns(git_sandbox, git_repo_with_commit, git_daemon):
|
||||
repo_path = git_repo_with_commit
|
||||
remote_url = git_daemon["remote_url"]
|
||||
|
||||
git_sandbox.git.remote_add(repo_path, "origin", remote_url)
|
||||
|
||||
with pytest.raises(GitUpstreamException) as exc:
|
||||
git_sandbox.git.pull(repo_path)
|
||||
|
||||
assert "no upstream branch is configured" in str(exc.value).lower()
|
||||
@@ -0,0 +1,48 @@
|
||||
from e2b.template.readycmd import (
|
||||
wait_for_file,
|
||||
wait_for_port,
|
||||
wait_for_process,
|
||||
wait_for_timeout,
|
||||
wait_for_url,
|
||||
)
|
||||
|
||||
|
||||
def test_wait_for_port_matches_the_exact_listening_port():
|
||||
cmd = wait_for_port(80).get_cmd()
|
||||
assert cmd == '[ -n "$(ss -Htuln sport = :80)" ]'
|
||||
|
||||
|
||||
def test_wait_for_url_quotes_the_url():
|
||||
cmd = wait_for_url("http://localhost:3000/health?ready=1&x=y").get_cmd()
|
||||
assert cmd == (
|
||||
'curl -s -o /dev/null -w "%{http_code}" '
|
||||
"'http://localhost:3000/health?ready=1&x=y' | grep -q \"200\""
|
||||
)
|
||||
|
||||
|
||||
def test_wait_for_url_keeps_simple_urls_unquoted():
|
||||
cmd = wait_for_url("http://localhost:3000/health").get_cmd()
|
||||
assert cmd == (
|
||||
'curl -s -o /dev/null -w "%{http_code}" '
|
||||
'http://localhost:3000/health | grep -q "200"'
|
||||
)
|
||||
|
||||
|
||||
def test_wait_for_process_quotes_the_process_name():
|
||||
cmd = wait_for_process("my daemon").get_cmd()
|
||||
assert cmd == "pgrep 'my daemon' > /dev/null"
|
||||
|
||||
|
||||
def test_wait_for_file_quotes_the_filename():
|
||||
cmd = wait_for_file("/tmp/ready file").get_cmd()
|
||||
assert cmd == "[ -f '/tmp/ready file' ]"
|
||||
|
||||
|
||||
def test_wait_for_file_keeps_simple_paths_unquoted():
|
||||
cmd = wait_for_file("/tmp/ready").get_cmd()
|
||||
assert cmd == "[ -f /tmp/ready ]"
|
||||
|
||||
|
||||
def test_wait_for_timeout_converts_milliseconds_to_seconds():
|
||||
assert wait_for_timeout(5000).get_cmd() == "sleep 5"
|
||||
assert wait_for_timeout(100).get_cmd() == "sleep 1"
|
||||
@@ -0,0 +1,334 @@
|
||||
import os
|
||||
import tempfile
|
||||
import pytest
|
||||
from e2b.template.utils import get_all_files_in_path
|
||||
|
||||
|
||||
class TestGetAllFilesInPath:
|
||||
@pytest.fixture
|
||||
def test_dir(self):
|
||||
"""Create a temporary directory for testing."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
yield tmpdir
|
||||
|
||||
def test_should_return_files_matching_simple_pattern(self, test_dir):
|
||||
"""Test that function returns files matching a simple pattern."""
|
||||
# Create test files
|
||||
with open(os.path.join(test_dir, "file1.txt"), "w") as f:
|
||||
f.write("content1")
|
||||
with open(os.path.join(test_dir, "file2.txt"), "w") as f:
|
||||
f.write("content2")
|
||||
with open(os.path.join(test_dir, "file3.js"), "w") as f:
|
||||
f.write("content3")
|
||||
|
||||
files = get_all_files_in_path("*.txt", test_dir, [])
|
||||
|
||||
assert len(files) == 2
|
||||
assert any("file1.txt" in f for f in files)
|
||||
assert any("file2.txt" in f for f in files)
|
||||
assert not any("file3.js" in f for f in files)
|
||||
|
||||
def test_should_handle_directory_patterns_recursively(self, test_dir):
|
||||
"""Test that function handles directory patterns recursively."""
|
||||
# Create nested directory structure
|
||||
os.makedirs(os.path.join(test_dir, "src", "components"), exist_ok=True)
|
||||
os.makedirs(os.path.join(test_dir, "src", "utils"), exist_ok=True)
|
||||
|
||||
with open(os.path.join(test_dir, "src", "index.ts"), "w") as f:
|
||||
f.write("index content")
|
||||
with open(os.path.join(test_dir, "src", "components", "Button.tsx"), "w") as f:
|
||||
f.write("button content")
|
||||
with open(os.path.join(test_dir, "src", "utils", "helper.ts"), "w") as f:
|
||||
f.write("helper content")
|
||||
with open(os.path.join(test_dir, "README.md"), "w") as f:
|
||||
f.write("readme content")
|
||||
|
||||
files = get_all_files_in_path("src", test_dir, [])
|
||||
|
||||
assert len(files) == 6 # 3 files + 3 directories (src, components, utils)
|
||||
assert any("index.ts" in f for f in files)
|
||||
assert any("Button.tsx" in f for f in files)
|
||||
assert any("helper.ts" in f for f in files)
|
||||
assert not any("README.md" in f for f in files)
|
||||
|
||||
def test_should_respect_ignore_patterns(self, test_dir):
|
||||
"""Test that function respects ignore patterns."""
|
||||
# Create test files
|
||||
with open(os.path.join(test_dir, "file1.txt"), "w") as f:
|
||||
f.write("content1")
|
||||
with open(os.path.join(test_dir, "file2.txt"), "w") as f:
|
||||
f.write("content2")
|
||||
with open(os.path.join(test_dir, "temp.txt"), "w") as f:
|
||||
f.write("temp content")
|
||||
with open(os.path.join(test_dir, "backup.txt"), "w") as f:
|
||||
f.write("backup content")
|
||||
|
||||
files = get_all_files_in_path("*.txt", test_dir, ["temp*", "backup*"])
|
||||
|
||||
assert len(files) == 2
|
||||
assert any("file1.txt" in f for f in files)
|
||||
assert any("file2.txt" in f for f in files)
|
||||
assert not any("temp.txt" in f for f in files)
|
||||
assert not any("backup.txt" in f for f in files)
|
||||
|
||||
def test_should_handle_complex_ignore_patterns(self, test_dir):
|
||||
"""Test that function handles complex ignore patterns."""
|
||||
# Create nested structure with various file types
|
||||
os.makedirs(os.path.join(test_dir, "src", "components"), exist_ok=True)
|
||||
os.makedirs(os.path.join(test_dir, "src", "utils"), exist_ok=True)
|
||||
os.makedirs(os.path.join(test_dir, "tests"), exist_ok=True)
|
||||
|
||||
with open(os.path.join(test_dir, "src", "index.ts"), "w") as f:
|
||||
f.write("index content")
|
||||
with open(os.path.join(test_dir, "src", "components", "Button.tsx"), "w") as f:
|
||||
f.write("button content")
|
||||
with open(os.path.join(test_dir, "src", "utils", "helper.ts"), "w") as f:
|
||||
f.write("helper content")
|
||||
with open(os.path.join(test_dir, "tests", "test.spec.ts"), "w") as f:
|
||||
f.write("test content")
|
||||
with open(
|
||||
os.path.join(test_dir, "src", "components", "Button.test.tsx"), "w"
|
||||
) as f:
|
||||
f.write("test content")
|
||||
with open(os.path.join(test_dir, "src", "utils", "helper.spec.ts"), "w") as f:
|
||||
f.write("spec content")
|
||||
|
||||
files = get_all_files_in_path("src", test_dir, ["**/*.test.*", "**/*.spec.*"])
|
||||
|
||||
assert len(files) == 6 # 3 files + 3 directories (src, components, utils)
|
||||
assert any("index.ts" in f for f in files)
|
||||
assert any("Button.tsx" in f for f in files)
|
||||
assert any("helper.ts" in f for f in files)
|
||||
assert not any("Button.test.tsx" in f for f in files)
|
||||
assert not any("helper.spec.ts" in f for f in files)
|
||||
|
||||
def test_should_handle_empty_directories(self, test_dir):
|
||||
"""Test that function handles empty directories."""
|
||||
os.makedirs(os.path.join(test_dir, "empty"), exist_ok=True)
|
||||
with open(os.path.join(test_dir, "file.txt"), "w") as f:
|
||||
f.write("content")
|
||||
|
||||
files = get_all_files_in_path("empty", test_dir, [])
|
||||
|
||||
assert len(files) == 1
|
||||
|
||||
def test_should_handle_mixed_files_and_directories(self, test_dir):
|
||||
"""Test that function handles mixed files and directories."""
|
||||
# Create a mix of files and directories
|
||||
with open(os.path.join(test_dir, "file1.txt"), "w") as f:
|
||||
f.write("content1")
|
||||
os.makedirs(os.path.join(test_dir, "dir1"), exist_ok=True)
|
||||
with open(os.path.join(test_dir, "dir1", "file2.txt"), "w") as f:
|
||||
f.write("content2")
|
||||
with open(os.path.join(test_dir, "file3.txt"), "w") as f:
|
||||
f.write("content3")
|
||||
|
||||
files = get_all_files_in_path("*", test_dir, [])
|
||||
|
||||
assert len(files) == 4
|
||||
assert any("file1.txt" in f for f in files)
|
||||
assert any("file2.txt" in f for f in files)
|
||||
assert any("file3.txt" in f for f in files)
|
||||
|
||||
def test_should_handle_glob_patterns_with_subdirectories(self, test_dir):
|
||||
"""Test that function handles glob patterns with subdirectories."""
|
||||
# Create nested structure
|
||||
os.makedirs(os.path.join(test_dir, "src", "components"), exist_ok=True)
|
||||
os.makedirs(os.path.join(test_dir, "src", "utils"), exist_ok=True)
|
||||
|
||||
with open(os.path.join(test_dir, "src", "index.ts"), "w") as f:
|
||||
f.write("index content")
|
||||
with open(os.path.join(test_dir, "src", "components", "Button.tsx"), "w") as f:
|
||||
f.write("button content")
|
||||
with open(os.path.join(test_dir, "src", "utils", "helper.ts"), "w") as f:
|
||||
f.write("helper content")
|
||||
with open(os.path.join(test_dir, "src", "components", "Button.css"), "w") as f:
|
||||
f.write("css content")
|
||||
|
||||
files = get_all_files_in_path("src/**/*", test_dir, [])
|
||||
|
||||
assert len(files) == 6
|
||||
assert any("index.ts" in f for f in files)
|
||||
assert any("Button.tsx" in f for f in files)
|
||||
assert any("helper.ts" in f for f in files)
|
||||
assert any("Button.css" in f for f in files)
|
||||
|
||||
def test_should_handle_specific_file_extensions(self, test_dir):
|
||||
"""Test that function handles specific file extensions."""
|
||||
with open(os.path.join(test_dir, "file1.ts"), "w") as f:
|
||||
f.write("ts content")
|
||||
with open(os.path.join(test_dir, "file2.js"), "w") as f:
|
||||
f.write("js content")
|
||||
with open(os.path.join(test_dir, "file3.tsx"), "w") as f:
|
||||
f.write("tsx content")
|
||||
with open(os.path.join(test_dir, "file4.css"), "w") as f:
|
||||
f.write("css content")
|
||||
|
||||
files = get_all_files_in_path("*.ts", test_dir, [])
|
||||
|
||||
assert len(files) == 1
|
||||
assert any("file1.ts" in f for f in files)
|
||||
|
||||
def test_should_return_sorted_files(self, test_dir):
|
||||
"""Test that function returns sorted files."""
|
||||
with open(os.path.join(test_dir, "zebra.txt"), "w") as f:
|
||||
f.write("z content")
|
||||
with open(os.path.join(test_dir, "apple.txt"), "w") as f:
|
||||
f.write("a content")
|
||||
with open(os.path.join(test_dir, "banana.txt"), "w") as f:
|
||||
f.write("b content")
|
||||
|
||||
files = get_all_files_in_path("*.txt", test_dir, [])
|
||||
|
||||
assert len(files) == 3
|
||||
assert "apple.txt" in files[0]
|
||||
assert "banana.txt" in files[1]
|
||||
assert "zebra.txt" in files[2]
|
||||
|
||||
def test_should_handle_no_matching_files(self, test_dir):
|
||||
"""Test that function handles no matching files."""
|
||||
with open(os.path.join(test_dir, "file.txt"), "w") as f:
|
||||
f.write("content")
|
||||
|
||||
files = get_all_files_in_path("*.js", test_dir, [])
|
||||
|
||||
assert len(files) == 0
|
||||
|
||||
def test_should_handle_complex_ignore_patterns_with_directories(self, test_dir):
|
||||
"""Test that function handles complex ignore patterns with directories."""
|
||||
# Create a complex structure
|
||||
os.makedirs(os.path.join(test_dir, "src", "components"), exist_ok=True)
|
||||
os.makedirs(os.path.join(test_dir, "src", "utils"), exist_ok=True)
|
||||
os.makedirs(os.path.join(test_dir, "src", "tests"), exist_ok=True)
|
||||
os.makedirs(os.path.join(test_dir, "dist"), exist_ok=True)
|
||||
|
||||
with open(os.path.join(test_dir, "src", "index.ts"), "w") as f:
|
||||
f.write("index content")
|
||||
with open(os.path.join(test_dir, "src", "components", "Button.tsx"), "w") as f:
|
||||
f.write("button content")
|
||||
with open(os.path.join(test_dir, "src", "utils", "helper.ts"), "w") as f:
|
||||
f.write("helper content")
|
||||
with open(os.path.join(test_dir, "src", "tests", "test.spec.ts"), "w") as f:
|
||||
f.write("test content")
|
||||
with open(os.path.join(test_dir, "dist", "bundle.js"), "w") as f:
|
||||
f.write("bundle content")
|
||||
with open(os.path.join(test_dir, "README.md"), "w") as f:
|
||||
f.write("readme content")
|
||||
|
||||
files = get_all_files_in_path("src", test_dir, ["**/tests/**", "**/*.spec.*"])
|
||||
|
||||
assert len(files) == 6 # 3 files + 3 directories (src, components, utils)
|
||||
assert any("index.ts" in f for f in files)
|
||||
assert any("Button.tsx" in f for f in files)
|
||||
assert any("helper.ts" in f for f in files)
|
||||
assert not any("test.spec.ts" in f for f in files)
|
||||
|
||||
def test_should_include_dotfiles(self, test_dir):
|
||||
"""Test that function includes files starting with a dot."""
|
||||
with open(os.path.join(test_dir, "file.txt"), "w") as f:
|
||||
f.write("content")
|
||||
with open(os.path.join(test_dir, ".env"), "w") as f:
|
||||
f.write("SECRET=123")
|
||||
with open(os.path.join(test_dir, ".gitignore"), "w") as f:
|
||||
f.write("node_modules")
|
||||
|
||||
files = get_all_files_in_path("*", test_dir, [])
|
||||
|
||||
assert len(files) == 3
|
||||
assert any(".env" in f for f in files)
|
||||
assert any(".gitignore" in f for f in files)
|
||||
assert any("file.txt" in f for f in files)
|
||||
|
||||
def test_should_include_dotfiles_in_subdirectories(self, test_dir):
|
||||
"""Test that function includes dotfiles inside subdirectories."""
|
||||
os.makedirs(os.path.join(test_dir, "src"), exist_ok=True)
|
||||
with open(os.path.join(test_dir, "src", "index.ts"), "w") as f:
|
||||
f.write("content")
|
||||
with open(os.path.join(test_dir, "src", ".env.local"), "w") as f:
|
||||
f.write("SECRET=123")
|
||||
|
||||
files = get_all_files_in_path("src", test_dir, [])
|
||||
|
||||
assert any("index.ts" in f for f in files)
|
||||
assert any(".env.local" in f for f in files)
|
||||
|
||||
def test_should_include_dotdirectories_and_their_contents(self, test_dir):
|
||||
"""Test that function includes dot-prefixed directories and their contents."""
|
||||
os.makedirs(os.path.join(test_dir, ".hidden"), exist_ok=True)
|
||||
with open(os.path.join(test_dir, ".hidden", "config.json"), "w") as f:
|
||||
f.write("{}")
|
||||
with open(os.path.join(test_dir, "visible.txt"), "w") as f:
|
||||
f.write("content")
|
||||
|
||||
files = get_all_files_in_path("*", test_dir, [])
|
||||
|
||||
assert any(".hidden" in f for f in files)
|
||||
assert any("config.json" in f for f in files)
|
||||
assert any("visible.txt" in f for f in files)
|
||||
|
||||
def test_should_respect_ignore_patterns_for_dotfiles(self, test_dir):
|
||||
"""Test that dotfiles can be excluded via ignore patterns."""
|
||||
with open(os.path.join(test_dir, ".env"), "w") as f:
|
||||
f.write("SECRET=123")
|
||||
with open(os.path.join(test_dir, ".gitignore"), "w") as f:
|
||||
f.write("node_modules")
|
||||
with open(os.path.join(test_dir, "file.txt"), "w") as f:
|
||||
f.write("content")
|
||||
|
||||
files = get_all_files_in_path("*", test_dir, [".env"])
|
||||
|
||||
assert len(files) == 2
|
||||
assert not any(f.endswith(".env") for f in files)
|
||||
assert any(".gitignore" in f for f in files)
|
||||
assert any("file.txt" in f for f in files)
|
||||
|
||||
def test_should_handle_symlinks(self, test_dir):
|
||||
"""Test that function handles symbolic links."""
|
||||
# Create a file and a symlink to it
|
||||
with open(os.path.join(test_dir, "original.txt"), "w") as f:
|
||||
f.write("original content")
|
||||
|
||||
# Create symlink (only on Unix-like systems)
|
||||
if hasattr(os, "symlink"):
|
||||
os.symlink("original.txt", os.path.join(test_dir, "link.txt"))
|
||||
|
||||
files = get_all_files_in_path("*.txt", test_dir, [])
|
||||
|
||||
assert len(files) == 2
|
||||
assert any("original.txt" in f for f in files)
|
||||
assert any("link.txt" in f for f in files)
|
||||
|
||||
def test_should_handle_nested_ignore_patterns(self, test_dir):
|
||||
"""Test that function handles nested ignore patterns."""
|
||||
# Create nested structure
|
||||
os.makedirs(os.path.join(test_dir, "src", "components", "ui"), exist_ok=True)
|
||||
os.makedirs(os.path.join(test_dir, "src", "components", "forms"), exist_ok=True)
|
||||
os.makedirs(os.path.join(test_dir, "src", "utils"), exist_ok=True)
|
||||
|
||||
with open(os.path.join(test_dir, "src", "index.ts"), "w") as f:
|
||||
f.write("index content")
|
||||
with open(
|
||||
os.path.join(test_dir, "src", "components", "ui", "Button.tsx"), "w"
|
||||
) as f:
|
||||
f.write("button content")
|
||||
with open(
|
||||
os.path.join(test_dir, "src", "components", "forms", "Input.tsx"), "w"
|
||||
) as f:
|
||||
f.write("input content")
|
||||
with open(os.path.join(test_dir, "src", "utils", "helper.ts"), "w") as f:
|
||||
f.write("helper content")
|
||||
with open(
|
||||
os.path.join(test_dir, "src", "components", "ui", "Button.test.tsx"), "w"
|
||||
) as f:
|
||||
f.write("test content")
|
||||
|
||||
files = get_all_files_in_path("src", test_dir, ["**/ui/**"])
|
||||
|
||||
assert (
|
||||
len(files) == 7
|
||||
) # 3 files + 4 directories (src, components, forms, utils)
|
||||
assert any("index.ts" in f for f in files)
|
||||
assert any("Input.tsx" in f for f in files)
|
||||
assert any("helper.ts" in f for f in files)
|
||||
assert not any("Button.tsx" in f for f in files)
|
||||
assert not any("Button.test.tsx" in f for f in files)
|
||||
@@ -0,0 +1,6 @@
|
||||
import os
|
||||
from e2b.template.utils import get_caller_directory
|
||||
|
||||
|
||||
def test_get_caller_directory():
|
||||
assert get_caller_directory(1) == os.path.dirname(__file__)
|
||||
@@ -0,0 +1,45 @@
|
||||
import pytest
|
||||
|
||||
from e2b.template.utils import normalize_build_arguments
|
||||
from e2b.exceptions import TemplateException
|
||||
|
||||
|
||||
def test_handles_string_name():
|
||||
result = normalize_build_arguments(name="my-template:v1.0")
|
||||
assert result == "my-template:v1.0"
|
||||
|
||||
|
||||
def test_handles_name_without_tag():
|
||||
result = normalize_build_arguments(name="my-template")
|
||||
assert result == "my-template"
|
||||
|
||||
|
||||
def test_handles_legacy_alias():
|
||||
result = normalize_build_arguments(alias="my-template")
|
||||
assert result == "my-template"
|
||||
|
||||
|
||||
def test_name_takes_precedence_over_alias():
|
||||
# When both are provided, name should be used
|
||||
result = normalize_build_arguments(name="from-name", alias="from-alias")
|
||||
assert result == "from-name"
|
||||
|
||||
|
||||
def test_throws_for_empty_name():
|
||||
with pytest.raises(TemplateException, match="Name must be provided"):
|
||||
normalize_build_arguments(name="")
|
||||
|
||||
|
||||
def test_throws_for_empty_alias():
|
||||
with pytest.raises(TemplateException, match="Name must be provided"):
|
||||
normalize_build_arguments(alias="")
|
||||
|
||||
|
||||
def test_throws_for_missing_name_and_alias():
|
||||
with pytest.raises(TemplateException, match="Name must be provided"):
|
||||
normalize_build_arguments()
|
||||
|
||||
|
||||
def test_throws_for_none_values():
|
||||
with pytest.raises(TemplateException, match="Name must be provided"):
|
||||
normalize_build_arguments(name=None, alias=None)
|
||||
@@ -0,0 +1,29 @@
|
||||
from e2b.template.utils import strip_ansi_escape_codes
|
||||
|
||||
|
||||
def test_strips_basic_sgr_color():
|
||||
assert strip_ansi_escape_codes("\x1b[31mred\x1b[0m") == "red"
|
||||
|
||||
|
||||
def test_strips_semicolon_separated_params():
|
||||
assert strip_ansi_escape_codes("\x1b[1;31;42mhi\x1b[0m") == "hi"
|
||||
|
||||
|
||||
def test_strips_semicolon_256_color():
|
||||
assert strip_ansi_escape_codes("\x1b[38;5;82mX\x1b[0m") == "X"
|
||||
|
||||
|
||||
def test_strips_colon_256_color():
|
||||
assert strip_ansi_escape_codes("\x1b[38:5:82mX\x1b[0m") == "X"
|
||||
|
||||
|
||||
def test_strips_colon_truecolor():
|
||||
assert strip_ansi_escape_codes("\x1b[38:2::255:0:0mRED\x1b[0m") == "RED"
|
||||
|
||||
|
||||
def test_strips_colon_curly_underline():
|
||||
assert strip_ansi_escape_codes("\x1b[4:3mX\x1b[0m") == "X"
|
||||
|
||||
|
||||
def test_leaves_plain_text_unchanged():
|
||||
assert strip_ansi_escape_codes("no escape codes here") == "no escape codes here"
|
||||
@@ -0,0 +1,172 @@
|
||||
import os
|
||||
import tempfile
|
||||
import tarfile
|
||||
import pytest
|
||||
from typing import IO
|
||||
from e2b.template.utils import tar_file_stream
|
||||
|
||||
|
||||
class TestTarFileStream:
|
||||
@pytest.fixture
|
||||
def test_dir(self):
|
||||
"""Create a temporary directory for testing."""
|
||||
tmpdir = tempfile.TemporaryDirectory()
|
||||
yield tmpdir.name
|
||||
tmpdir.cleanup()
|
||||
|
||||
def _extract_tar_contents(self, tar_buffer: IO[bytes]) -> dict:
|
||||
"""Extract tar contents into a dictionary mapping paths to file contents."""
|
||||
tar_buffer.seek(0)
|
||||
contents = {}
|
||||
# "r:*" auto-detects compressed vs. uncompressed archives
|
||||
with tarfile.open(fileobj=tar_buffer, mode="r:*") as tar:
|
||||
for member in tar.getmembers():
|
||||
if member.isfile():
|
||||
file_obj = tar.extractfile(member)
|
||||
if file_obj:
|
||||
contents[member.name] = file_obj.read()
|
||||
elif member.isdir():
|
||||
contents[member.name] = None # Mark as directory
|
||||
return contents
|
||||
|
||||
def test_should_create_tar_with_simple_files(self, test_dir):
|
||||
"""Test that function creates tar with simple files."""
|
||||
# Create test files
|
||||
file1_path = os.path.join(test_dir, "file1.txt")
|
||||
file2_path = os.path.join(test_dir, "file2.txt")
|
||||
|
||||
with open(file1_path, "w") as f:
|
||||
f.write("content1")
|
||||
with open(file2_path, "w") as f:
|
||||
f.write("content2")
|
||||
|
||||
tar_buffer = tar_file_stream("*.txt", test_dir, [], False, True)
|
||||
contents = self._extract_tar_contents(tar_buffer)
|
||||
|
||||
assert len(contents) == 2
|
||||
assert "file1.txt" in contents
|
||||
assert "file2.txt" in contents
|
||||
assert contents["file1.txt"] == b"content1"
|
||||
assert contents["file2.txt"] == b"content2"
|
||||
|
||||
def test_should_create_uncompressed_tar_when_gzip_disabled(self, test_dir):
|
||||
"""Test that function creates an uncompressed tar when gzip=False."""
|
||||
file1_path = os.path.join(test_dir, "file1.txt")
|
||||
file2_path = os.path.join(test_dir, "file2.txt")
|
||||
|
||||
with open(file1_path, "w") as f:
|
||||
f.write("content1")
|
||||
with open(file2_path, "w") as f:
|
||||
f.write("content2")
|
||||
|
||||
tar_buffer = tar_file_stream("*.txt", test_dir, [], False, False)
|
||||
|
||||
# gzip streams start with the magic bytes 0x1f 0x8b — a plain tar must not
|
||||
tar_buffer.seek(0)
|
||||
assert tar_buffer.read(2) != b"\x1f\x8b"
|
||||
|
||||
# The archive must still be readable and contain the original files
|
||||
contents = self._extract_tar_contents(tar_buffer)
|
||||
assert contents["file1.txt"] == b"content1"
|
||||
assert contents["file2.txt"] == b"content2"
|
||||
|
||||
def test_should_respect_ignore_patterns(self, test_dir):
|
||||
"""Test that function respects ignore patterns."""
|
||||
# Create test files
|
||||
with open(os.path.join(test_dir, "file1.txt"), "w") as f:
|
||||
f.write("content1")
|
||||
with open(os.path.join(test_dir, "file2.txt"), "w") as f:
|
||||
f.write("content2")
|
||||
with open(os.path.join(test_dir, "temp.txt"), "w") as f:
|
||||
f.write("temp content")
|
||||
with open(os.path.join(test_dir, "backup.txt"), "w") as f:
|
||||
f.write("backup content")
|
||||
|
||||
tar_buffer = tar_file_stream(
|
||||
"*.txt", test_dir, ["temp*", "backup*"], False, True
|
||||
)
|
||||
contents = self._extract_tar_contents(tar_buffer)
|
||||
|
||||
assert len(contents) == 2
|
||||
assert "file1.txt" in contents
|
||||
assert "file2.txt" in contents
|
||||
assert contents["file1.txt"] == b"content1"
|
||||
assert contents["file2.txt"] == b"content2"
|
||||
assert "temp.txt" not in contents
|
||||
assert "backup.txt" not in contents
|
||||
|
||||
def test_should_handle_nested_files(self, test_dir):
|
||||
"""Test that function handles nested directory structures."""
|
||||
# Create nested directory structure
|
||||
nested_dir = os.path.join(test_dir, "src", "components")
|
||||
os.makedirs(nested_dir, exist_ok=True)
|
||||
|
||||
with open(os.path.join(test_dir, "src", "index.ts"), "w") as f:
|
||||
f.write("index content")
|
||||
with open(os.path.join(nested_dir, "Button.tsx"), "w") as f:
|
||||
f.write("button content")
|
||||
|
||||
tar_buffer = tar_file_stream("src", test_dir, [], False, True)
|
||||
contents = self._extract_tar_contents(tar_buffer)
|
||||
|
||||
# Should include the directory and files
|
||||
assert "src/index.ts" in contents
|
||||
assert "src/components/Button.tsx" in contents
|
||||
|
||||
def test_should_resolve_symlinks_when_enabled(self, test_dir):
|
||||
"""Test that function resolves symlinks when resolve_symlinks=True."""
|
||||
if not hasattr(os, "symlink"):
|
||||
pytest.skip("Symlinks not supported on this platform")
|
||||
|
||||
# Create original file
|
||||
original_path = os.path.join(test_dir, "original.txt")
|
||||
with open(original_path, "w") as f:
|
||||
f.write("original content")
|
||||
|
||||
# Create symlink
|
||||
symlink_path = os.path.join(test_dir, "link.txt")
|
||||
os.symlink("original.txt", symlink_path)
|
||||
|
||||
# Test with resolve_symlinks=True
|
||||
tar_buffer = tar_file_stream("*.txt", test_dir, [], True, True)
|
||||
contents = self._extract_tar_contents(tar_buffer)
|
||||
|
||||
# Both files should be in tar
|
||||
assert "original.txt" in contents
|
||||
assert "link.txt" in contents
|
||||
# Symlink should be resolved (contain actual content, not link)
|
||||
assert contents["original.txt"] == b"original content"
|
||||
assert contents["link.txt"] == b"original content"
|
||||
|
||||
def test_should_preserve_symlinks_when_disabled(self, test_dir):
|
||||
"""Test that function preserves symlinks when resolve_symlinks=False."""
|
||||
if not hasattr(os, "symlink"):
|
||||
pytest.skip("Symlinks not supported on this platform")
|
||||
|
||||
# Create original file
|
||||
original_path = os.path.join(test_dir, "original.txt")
|
||||
with open(original_path, "w") as f:
|
||||
f.write("original content")
|
||||
|
||||
# Create symlink
|
||||
symlink_path = os.path.join(test_dir, "link.txt")
|
||||
os.symlink("original.txt", symlink_path)
|
||||
|
||||
# Test with resolve_symlinks=False
|
||||
tar_buffer = tar_file_stream("*.txt", test_dir, [], False, True)
|
||||
tar_buffer.seek(0)
|
||||
|
||||
with tarfile.open(fileobj=tar_buffer, mode="r:gz") as tar:
|
||||
members = {m.name: m for m in tar.getmembers()}
|
||||
|
||||
# Both files should be in tar
|
||||
assert "original.txt" in members
|
||||
assert "link.txt" in members
|
||||
|
||||
# Original should be a regular file
|
||||
assert members["original.txt"].isfile()
|
||||
assert not members["original.txt"].issym()
|
||||
|
||||
# Link should be a symlink
|
||||
assert members["link.txt"].issym()
|
||||
assert members["link.txt"].linkname == "original.txt"
|
||||
@@ -0,0 +1,106 @@
|
||||
import sys
|
||||
import pytest
|
||||
|
||||
from e2b.template.utils import validate_relative_path
|
||||
from e2b.exceptions import TemplateException
|
||||
|
||||
is_windows = sys.platform == "win32"
|
||||
|
||||
|
||||
class TestValidateRelativePathValid:
|
||||
"""Test cases for valid paths."""
|
||||
|
||||
def test_accepts_simple_relative_path(self):
|
||||
validate_relative_path("foo", None)
|
||||
|
||||
def test_accepts_nested_relative_path(self):
|
||||
validate_relative_path("foo/bar", None)
|
||||
|
||||
def test_accepts_path_with_dot_prefix(self):
|
||||
validate_relative_path("./foo", None)
|
||||
|
||||
def test_accepts_nested_path_with_dot_prefix(self):
|
||||
validate_relative_path("./foo/bar", None)
|
||||
|
||||
def test_accepts_internal_parent_ref_within_context(self):
|
||||
validate_relative_path("foo/../bar", None)
|
||||
|
||||
def test_accepts_current_directory(self):
|
||||
validate_relative_path(".", None)
|
||||
|
||||
def test_accepts_glob_patterns(self):
|
||||
validate_relative_path("*.txt", None)
|
||||
validate_relative_path("**/*.ts", None)
|
||||
validate_relative_path("src/**/*", None)
|
||||
|
||||
def test_accepts_filenames_starting_with_double_dots(self):
|
||||
validate_relative_path("..myconfig", None)
|
||||
validate_relative_path("..cache", None)
|
||||
validate_relative_path("...something", None)
|
||||
validate_relative_path("foo/..myconfig", None)
|
||||
|
||||
|
||||
class TestValidateRelativePathInvalidAbsolute:
|
||||
"""Test cases for invalid absolute paths."""
|
||||
|
||||
def test_rejects_unix_absolute_path(self):
|
||||
with pytest.raises(TemplateException) as excinfo:
|
||||
validate_relative_path("/absolute/path", None)
|
||||
assert "absolute paths are not allowed" in str(excinfo.value)
|
||||
|
||||
def test_rejects_root_path(self):
|
||||
with pytest.raises(TemplateException):
|
||||
validate_relative_path("/", None)
|
||||
|
||||
@pytest.mark.skipif(not is_windows, reason="Windows path test only runs on Windows")
|
||||
def test_rejects_windows_drive_letter_path(self):
|
||||
with pytest.raises(TemplateException) as excinfo:
|
||||
validate_relative_path("C:\\Windows\\System32", None)
|
||||
assert "absolute paths are not allowed" in str(excinfo.value)
|
||||
|
||||
|
||||
class TestValidateRelativePathInvalidEscape:
|
||||
"""Test cases for paths that escape the context directory."""
|
||||
|
||||
def test_rejects_simple_parent_directory_escape(self):
|
||||
with pytest.raises(TemplateException) as excinfo:
|
||||
validate_relative_path("../foo", None)
|
||||
assert "path escapes the context directory" in str(excinfo.value)
|
||||
|
||||
def test_rejects_double_parent_directory_escape(self):
|
||||
with pytest.raises(TemplateException):
|
||||
validate_relative_path("../../foo", None)
|
||||
|
||||
def test_rejects_nested_parent_refs_escape(self):
|
||||
with pytest.raises(TemplateException):
|
||||
validate_relative_path("foo/../../bar", None)
|
||||
|
||||
def test_rejects_dot_prefix_escape(self):
|
||||
with pytest.raises(TemplateException):
|
||||
validate_relative_path("./foo/../../../bar", None)
|
||||
|
||||
def test_rejects_just_parent_directory(self):
|
||||
with pytest.raises(TemplateException):
|
||||
validate_relative_path("..", None)
|
||||
|
||||
def test_rejects_current_directory_followed_by_parent(self):
|
||||
with pytest.raises(TemplateException):
|
||||
validate_relative_path("./..", None)
|
||||
|
||||
def test_rejects_deeply_nested_escape(self):
|
||||
with pytest.raises(TemplateException):
|
||||
validate_relative_path("a/b/c/../../../../escape", None)
|
||||
|
||||
|
||||
class TestValidateRelativePathErrorMessages:
|
||||
"""Test cases for error message content."""
|
||||
|
||||
def test_absolute_path_error_includes_path(self):
|
||||
with pytest.raises(TemplateException) as excinfo:
|
||||
validate_relative_path("/etc/passwd", None)
|
||||
assert "/etc/passwd" in str(excinfo.value)
|
||||
|
||||
def test_escape_path_error_includes_path(self):
|
||||
with pytest.raises(TemplateException) as excinfo:
|
||||
validate_relative_path("../secret", None)
|
||||
assert "../secret" in str(excinfo.value)
|
||||
@@ -0,0 +1,9 @@
|
||||
import pytest
|
||||
|
||||
from e2b import Sandbox
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_get_info(sandbox: Sandbox):
|
||||
info = Sandbox.get_info(sandbox.sandbox_id)
|
||||
assert info.sandbox_id == sandbox.sandbox_id
|
||||
@@ -0,0 +1,21 @@
|
||||
import pytest
|
||||
|
||||
from e2b import Sandbox, SandboxQuery, SandboxState
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_kill_existing_sandbox(sandbox: Sandbox, sandbox_test_id: str):
|
||||
assert Sandbox.kill(sandbox.sandbox_id)
|
||||
|
||||
paginator = Sandbox.list(
|
||||
query=SandboxQuery(
|
||||
state=[SandboxState.RUNNING], metadata={"sandbox_test_id": sandbox_test_id}
|
||||
)
|
||||
)
|
||||
sandboxes = paginator.next_items()
|
||||
assert sandbox.sandbox_id not in [s.sandbox_id for s in sandboxes]
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_kill_non_existing_sandbox():
|
||||
assert not Sandbox.kill("nonexistingsandbox")
|
||||
@@ -0,0 +1,189 @@
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
|
||||
from e2b import Sandbox, SandboxQuery, SandboxState
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_list_sandboxes(sandbox: Sandbox, sandbox_test_id: str):
|
||||
paginator = Sandbox.list(
|
||||
query=SandboxQuery(metadata={"sandbox_test_id": sandbox_test_id})
|
||||
)
|
||||
sandboxes = paginator.next_items()
|
||||
assert len(sandboxes) >= 1
|
||||
assert sandbox.sandbox_id in [sbx.sandbox_id for sbx in sandboxes]
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_list_sandboxes_with_filter(sandbox_factory, sandbox_test_id: str):
|
||||
unique_id = str(uuid.uuid4())
|
||||
extra_sbx = sandbox_factory(metadata={"unique_id": unique_id})
|
||||
|
||||
paginator = Sandbox.list(query=SandboxQuery(metadata={"unique_id": unique_id}))
|
||||
sandboxes = paginator.next_items()
|
||||
assert len(sandboxes) == 1
|
||||
assert sandboxes[0].sandbox_id == extra_sbx.sandbox_id
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_list_running_sandboxes(sandbox: Sandbox, sandbox_test_id: str):
|
||||
paginator = Sandbox.list(
|
||||
query=SandboxQuery(
|
||||
metadata={"sandbox_test_id": sandbox_test_id}, state=[SandboxState.RUNNING]
|
||||
)
|
||||
)
|
||||
sandboxes = paginator.next_items()
|
||||
assert len(sandboxes) >= 1
|
||||
|
||||
# Verify our running sandbox is in the list
|
||||
assert any(
|
||||
s.sandbox_id == sandbox.sandbox_id and s.state == SandboxState.RUNNING
|
||||
for s in sandboxes
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_list_paused_sandboxes(sandbox: Sandbox, sandbox_test_id: str):
|
||||
sandbox.beta_pause()
|
||||
|
||||
paginator = Sandbox.list(
|
||||
query=SandboxQuery(
|
||||
metadata={"sandbox_test_id": sandbox_test_id}, state=[SandboxState.PAUSED]
|
||||
)
|
||||
)
|
||||
sandboxes = paginator.next_items()
|
||||
assert len(sandboxes) >= 1
|
||||
|
||||
# Verify our paused sandbox is in the list
|
||||
assert any(
|
||||
s.sandbox_id == sandbox.sandbox_id and s.state == SandboxState.PAUSED
|
||||
for s in sandboxes
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_paginate_running_sandboxes(
|
||||
sandbox: Sandbox, sandbox_factory, sandbox_test_id: str
|
||||
):
|
||||
# Create two sandboxes
|
||||
extra_sbx = sandbox_factory(metadata={"sandbox_test_id": sandbox_test_id})
|
||||
|
||||
# Test pagination with limit
|
||||
paginator = Sandbox.list(
|
||||
query=SandboxQuery(
|
||||
metadata={"sandbox_test_id": sandbox_test_id},
|
||||
state=[SandboxState.RUNNING],
|
||||
),
|
||||
limit=1,
|
||||
)
|
||||
|
||||
sandboxes = paginator.next_items()
|
||||
|
||||
# Check first page
|
||||
assert len(sandboxes) == 1
|
||||
assert sandboxes[0].state == SandboxState.RUNNING
|
||||
assert paginator.has_next is True
|
||||
assert paginator.next_token is not None
|
||||
assert sandboxes[0].sandbox_id == extra_sbx.sandbox_id
|
||||
|
||||
# Get second page
|
||||
sandboxes = paginator.next_items()
|
||||
|
||||
# Check second page
|
||||
assert len(sandboxes) == 1
|
||||
assert sandboxes[0].state == SandboxState.RUNNING
|
||||
assert paginator.has_next is False
|
||||
assert paginator.next_token is None
|
||||
assert sandboxes[0].sandbox_id == sandbox.sandbox_id
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_paginate_paused_sandboxes(
|
||||
sandbox: Sandbox, sandbox_factory, sandbox_test_id: str
|
||||
):
|
||||
sandbox.beta_pause()
|
||||
|
||||
# create another paused sandbox
|
||||
extra_sbx = sandbox_factory(metadata={"sandbox_test_id": sandbox_test_id})
|
||||
extra_sbx.beta_pause()
|
||||
|
||||
# Test pagination with limit
|
||||
paginator = Sandbox.list(
|
||||
query=SandboxQuery(
|
||||
state=[SandboxState.PAUSED],
|
||||
metadata={"sandbox_test_id": sandbox_test_id},
|
||||
),
|
||||
limit=1,
|
||||
)
|
||||
|
||||
sandboxes = paginator.next_items()
|
||||
|
||||
# Check first page
|
||||
assert len(sandboxes) == 1
|
||||
assert sandboxes[0].state == SandboxState.PAUSED
|
||||
assert paginator.has_next is True
|
||||
assert paginator.next_token is not None
|
||||
assert sandboxes[0].sandbox_id == extra_sbx.sandbox_id
|
||||
|
||||
# Get second page
|
||||
sandboxes = paginator.next_items()
|
||||
|
||||
# Check second page
|
||||
assert len(sandboxes) == 1
|
||||
assert sandboxes[0].state == SandboxState.PAUSED
|
||||
assert paginator.has_next is False
|
||||
assert paginator.next_token is None
|
||||
assert sandboxes[0].sandbox_id == sandbox.sandbox_id
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_paginate_running_and_paused_sandboxes(
|
||||
sandbox: Sandbox, sandbox_factory, sandbox_test_id: str
|
||||
):
|
||||
# Create extra paused sandbox
|
||||
extra_sbx = sandbox_factory(metadata={"sandbox_test_id": sandbox_test_id})
|
||||
extra_sbx.beta_pause()
|
||||
|
||||
# Test pagination with limit
|
||||
paginator = Sandbox.list(
|
||||
query=SandboxQuery(
|
||||
metadata={"sandbox_test_id": sandbox_test_id},
|
||||
state=[SandboxState.RUNNING, SandboxState.PAUSED],
|
||||
),
|
||||
limit=1,
|
||||
)
|
||||
|
||||
sandboxes = paginator.next_items()
|
||||
|
||||
# Check first page
|
||||
assert len(sandboxes) == 1
|
||||
assert sandboxes[0].state == SandboxState.PAUSED
|
||||
assert paginator.has_next is True
|
||||
assert paginator.next_token is not None
|
||||
assert sandboxes[0].sandbox_id == extra_sbx.sandbox_id
|
||||
|
||||
# Get second page
|
||||
sandboxes = paginator.next_items()
|
||||
|
||||
# Check second page
|
||||
assert len(sandboxes) == 1
|
||||
assert sandboxes[0].state == SandboxState.RUNNING
|
||||
assert paginator.has_next is False
|
||||
assert paginator.next_token is None
|
||||
assert sandboxes[0].sandbox_id == sandbox.sandbox_id
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_paginate_iterator(sandbox: Sandbox, sandbox_test_id: str):
|
||||
paginator = Sandbox.list(
|
||||
query=SandboxQuery(metadata={"sandbox_test_id": sandbox_test_id})
|
||||
)
|
||||
sandboxes_list = []
|
||||
|
||||
while paginator.has_next:
|
||||
sandboxes = paginator.next_items()
|
||||
sandboxes_list.extend(sandboxes)
|
||||
|
||||
assert len(sandboxes_list) > 0
|
||||
assert sandbox.sandbox_id in [sbx.sandbox_id for sbx in sandboxes_list]
|
||||
@@ -0,0 +1,19 @@
|
||||
import pytest
|
||||
from e2b import Sandbox
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_pause_sandbox(sandbox: Sandbox):
|
||||
Sandbox.pause(sandbox.sandbox_id)
|
||||
assert not sandbox.is_running()
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_resume_sandbox(sandbox: Sandbox):
|
||||
# pause
|
||||
Sandbox.pause(sandbox.sandbox_id)
|
||||
assert not sandbox.is_running()
|
||||
|
||||
# resume
|
||||
Sandbox.connect(sandbox.sandbox_id)
|
||||
assert sandbox.is_running()
|
||||
@@ -0,0 +1,18 @@
|
||||
import pytest
|
||||
|
||||
from e2b import NotFoundException
|
||||
|
||||
|
||||
def test_connect_to_process(sandbox):
|
||||
cmd = sandbox.commands.run("sleep 10", background=True)
|
||||
pid = cmd.pid
|
||||
|
||||
process_info = sandbox.commands.connect(pid)
|
||||
assert process_info.pid == pid
|
||||
|
||||
|
||||
def test_connect_to_non_existing_process(sandbox):
|
||||
non_existing_pid = 999999
|
||||
|
||||
with pytest.raises(NotFoundException):
|
||||
sandbox.commands.connect(non_existing_pid)
|
||||
@@ -0,0 +1,19 @@
|
||||
import pytest
|
||||
|
||||
from e2b import Sandbox, CommandExitException
|
||||
|
||||
|
||||
def test_kill_process(sandbox: Sandbox):
|
||||
cmd = sandbox.commands.run("sleep 10", background=True)
|
||||
pid = cmd.pid
|
||||
|
||||
sandbox.commands.kill(pid)
|
||||
|
||||
with pytest.raises(CommandExitException):
|
||||
sandbox.commands.run(f"kill -0 {pid}")
|
||||
|
||||
|
||||
def test_kill_non_existing_process(sandbox):
|
||||
non_existing_pid = 999999
|
||||
|
||||
assert not sandbox.commands.kill(non_existing_pid)
|
||||
@@ -0,0 +1,13 @@
|
||||
from e2b import Sandbox
|
||||
|
||||
|
||||
def test_kill_process(sandbox: Sandbox):
|
||||
c1 = sandbox.commands.run("sleep 10", background=True)
|
||||
c2 = sandbox.commands.run("sleep 10", background=True)
|
||||
|
||||
processes = sandbox.commands.list()
|
||||
|
||||
assert len(processes) >= 2
|
||||
pids = [p.pid for p in processes]
|
||||
assert c1.pid in pids
|
||||
assert c2.pid in pids
|
||||
@@ -0,0 +1,35 @@
|
||||
import pytest
|
||||
|
||||
from e2b import Sandbox
|
||||
|
||||
|
||||
def test_command_envs(sandbox: Sandbox):
|
||||
cmd = sandbox.commands.run("echo $FOO", envs={"FOO": "bar"})
|
||||
assert cmd.stdout.strip() == "bar"
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_sandbox_envs(sandbox_factory):
|
||||
sbx = sandbox_factory(envs={"FOO": "bar"})
|
||||
|
||||
cmd = sbx.commands.run("echo $FOO")
|
||||
assert cmd.stdout.strip() == "bar"
|
||||
|
||||
|
||||
def test_bash_command_scoped_env_vars(sandbox: Sandbox):
|
||||
cmd = sandbox.commands.run("echo $FOO", envs={"FOO": "bar"})
|
||||
assert cmd.exit_code == 0
|
||||
assert cmd.stdout.strip() == "bar"
|
||||
|
||||
# test that it is secure and not accessible to subsequent commands
|
||||
cmd2 = sandbox.commands.run('sudo echo "$FOO"')
|
||||
assert cmd2.exit_code == 0
|
||||
assert cmd2.stdout.strip() == ""
|
||||
|
||||
|
||||
def test_python_command_scoped_env_vars(sandbox: Sandbox):
|
||||
cmd = sandbox.commands.run(
|
||||
"python3 -c \"import os; print(os.environ['FOO'])\"", envs={"FOO": "bar"}
|
||||
)
|
||||
assert cmd.exit_code == 0
|
||||
assert cmd.stdout.strip() == "bar"
|
||||
@@ -0,0 +1,58 @@
|
||||
import pytest
|
||||
|
||||
from e2b import Sandbox, TimeoutException
|
||||
|
||||
|
||||
def test_run(sandbox: Sandbox):
|
||||
text = "Hello, World!"
|
||||
|
||||
cmd = sandbox.commands.run(f'echo "{text}"')
|
||||
|
||||
assert cmd.exit_code == 0
|
||||
assert cmd.stdout == f"{text}\n"
|
||||
|
||||
|
||||
def test_run_with_special_characters(sandbox: Sandbox):
|
||||
text = "!@#$%^&*()_+"
|
||||
|
||||
cmd = sandbox.commands.run(f'echo "{text}"')
|
||||
|
||||
assert cmd.exit_code == 0
|
||||
assert cmd.stdout == f"{text}\n"
|
||||
|
||||
|
||||
def test_run_with_broken_utf8(sandbox: Sandbox):
|
||||
# Create a string with 8191 'a' characters followed by the problematic byte 0xe2
|
||||
long_str = "a" * 8191 + "\\xe2"
|
||||
result = sandbox.commands.run(f'printf "{long_str}"')
|
||||
assert result.exit_code == 0
|
||||
|
||||
# The broken UTF-8 bytes should be replaced with the Unicode replacement character
|
||||
assert result.stdout == ("a" * 8191 + "\ufffd")
|
||||
|
||||
|
||||
def test_run_with_multiline_string(sandbox):
|
||||
text = "Hello,\nWorld!"
|
||||
|
||||
cmd = sandbox.commands.run(f'echo "{text}"')
|
||||
|
||||
assert cmd.exit_code == 0
|
||||
assert cmd.stdout == f"{text}\n"
|
||||
|
||||
|
||||
def test_run_with_timeout(sandbox):
|
||||
cmd = sandbox.commands.run('echo "Hello, World!"', timeout=10)
|
||||
|
||||
assert cmd.exit_code == 0
|
||||
|
||||
|
||||
def test_run_with_too_short_timeout(sandbox):
|
||||
with pytest.raises(TimeoutException):
|
||||
sandbox.commands.run("sleep 10", timeout=2)
|
||||
|
||||
|
||||
def test_run_with_too_short_timeout_iterating(sandbox):
|
||||
cmd = sandbox.commands.run("sleep 10", timeout=2, background=True)
|
||||
with pytest.raises(TimeoutException):
|
||||
for _ in cmd:
|
||||
pass
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
import pytest
|
||||
|
||||
from e2b import Sandbox, TimeoutException
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_kill_sandbox_while_command_is_running(sandbox: Sandbox):
|
||||
cmd = sandbox.commands.run("sleep 60", background=True)
|
||||
|
||||
sandbox.kill()
|
||||
|
||||
with pytest.raises(TimeoutException) as exc_info:
|
||||
cmd.wait()
|
||||
|
||||
# The health check confirms the sandbox is gone, so the error states it outright
|
||||
assert "sandbox was killed or reached its end of life" in str(exc_info.value)
|
||||
@@ -0,0 +1,57 @@
|
||||
from e2b import Sandbox
|
||||
|
||||
|
||||
def test_send_stdin_to_process(sandbox: Sandbox):
|
||||
cmd = sandbox.commands.run("cat", background=True, stdin=True)
|
||||
sandbox.commands.send_stdin(cmd.pid, "Hello, World!")
|
||||
|
||||
for stdout, _, _ in cmd:
|
||||
assert stdout == "Hello, World!"
|
||||
break
|
||||
|
||||
|
||||
def test_send_bytes_stdin_to_process(sandbox: Sandbox):
|
||||
cmd = sandbox.commands.run("cat", background=True, stdin=True)
|
||||
sandbox.commands.send_stdin(cmd.pid, b"Hello, World!")
|
||||
|
||||
for stdout, _, _ in cmd:
|
||||
assert stdout == "Hello, World!"
|
||||
break
|
||||
|
||||
|
||||
def test_send_stdin_via_command_handle(sandbox: Sandbox):
|
||||
cmd = sandbox.commands.run("cat", background=True, stdin=True)
|
||||
cmd.send_stdin("Hello, World!")
|
||||
|
||||
for stdout, _, _ in cmd:
|
||||
assert stdout == "Hello, World!"
|
||||
break
|
||||
|
||||
|
||||
def test_close_stdin_via_command_handle(sandbox: Sandbox):
|
||||
cmd = sandbox.commands.run("cat", background=True, stdin=True)
|
||||
cmd.send_stdin("Hello, World!")
|
||||
cmd.close_stdin()
|
||||
|
||||
# `cat` exits once stdin is closed (EOF).
|
||||
result = cmd.wait()
|
||||
assert result.exit_code == 0
|
||||
assert result.stdout == "Hello, World!"
|
||||
|
||||
|
||||
def test_send_special_characters_to_process(sandbox: Sandbox):
|
||||
cmd = sandbox.commands.run("cat", background=True, stdin=True)
|
||||
sandbox.commands.send_stdin(cmd.pid, "!@#$%^&*()_+")
|
||||
|
||||
for stdout, _, _ in cmd:
|
||||
assert stdout == "!@#$%^&*()_+"
|
||||
break
|
||||
|
||||
|
||||
def test_send_multiline_string_to_process(sandbox: Sandbox):
|
||||
cmd = sandbox.commands.run("cat", background=True, stdin=True)
|
||||
sandbox.commands.send_stdin(cmd.pid, "Hello,\nWorld!")
|
||||
|
||||
for stdout, _, _ in cmd:
|
||||
assert stdout == "Hello,\nWorld!"
|
||||
break
|
||||
@@ -0,0 +1,61 @@
|
||||
from e2b.sandbox.filesystem.filesystem import WriteEntry
|
||||
|
||||
|
||||
def test_write_and_read_with_gzip(sandbox, debug):
|
||||
filename = "test_gzip_write.txt"
|
||||
content = "This is a test file with gzip encoding."
|
||||
|
||||
info = sandbox.files.write(filename, content, gzip=True)
|
||||
assert info.path == f"/home/user/{filename}"
|
||||
|
||||
read_content = sandbox.files.read(filename, gzip=True)
|
||||
assert read_content == content
|
||||
|
||||
if debug:
|
||||
sandbox.files.remove(filename)
|
||||
|
||||
|
||||
def test_write_gzip_read_plain(sandbox, debug):
|
||||
filename = "test_gzip_write_plain_read.txt"
|
||||
content = "Written with gzip, read without."
|
||||
|
||||
sandbox.files.write(filename, content, gzip=True)
|
||||
|
||||
read_content = sandbox.files.read(filename)
|
||||
assert read_content == content
|
||||
|
||||
if debug:
|
||||
sandbox.files.remove(filename)
|
||||
|
||||
|
||||
def test_write_files_with_gzip(sandbox, debug):
|
||||
files = [
|
||||
WriteEntry(path="gzip_multi_1.txt", data="File 1 content"),
|
||||
WriteEntry(path="gzip_multi_2.txt", data="File 2 content"),
|
||||
WriteEntry(path="gzip_multi_3.txt", data="File 3 content"),
|
||||
]
|
||||
|
||||
infos = sandbox.files.write_files(files, gzip=True)
|
||||
assert len(infos) == len(files)
|
||||
|
||||
for i, file in enumerate(files):
|
||||
read_content = sandbox.files.read(file["path"])
|
||||
assert read_content == file["data"]
|
||||
|
||||
if debug:
|
||||
for file in files:
|
||||
sandbox.files.remove(file["path"])
|
||||
|
||||
|
||||
def test_read_bytes_with_gzip(sandbox, debug):
|
||||
filename = "test_gzip_bytes.txt"
|
||||
content = "Binary content with gzip."
|
||||
|
||||
sandbox.files.write(filename, content)
|
||||
|
||||
read_bytes = sandbox.files.read(filename, format="bytes", gzip=True)
|
||||
assert isinstance(read_bytes, bytearray)
|
||||
assert read_bytes.decode("utf-8") == content
|
||||
|
||||
if debug:
|
||||
sandbox.files.remove(filename)
|
||||
@@ -0,0 +1,8 @@
|
||||
from e2b import Sandbox
|
||||
|
||||
|
||||
def test_exists(sandbox: Sandbox):
|
||||
filename = "test_exists.txt"
|
||||
|
||||
sandbox.files.write(filename, "test")
|
||||
assert sandbox.files.exists(filename)
|
||||
@@ -0,0 +1,247 @@
|
||||
import uuid
|
||||
from typing import Any
|
||||
|
||||
from e2b import Sandbox, FileType
|
||||
|
||||
|
||||
def test_list_directory(sandbox: Sandbox):
|
||||
home_dir_name = "/home/user"
|
||||
parent_dir_name = f"test_directory_{uuid.uuid4()}"
|
||||
|
||||
sandbox.files.make_dir(parent_dir_name)
|
||||
sandbox.files.make_dir(f"{parent_dir_name}/subdir1")
|
||||
sandbox.files.make_dir(f"{parent_dir_name}/subdir2")
|
||||
sandbox.files.make_dir(f"{parent_dir_name}/subdir1/subdir1_1")
|
||||
sandbox.files.make_dir(f"{parent_dir_name}/subdir1/subdir1_2")
|
||||
sandbox.files.make_dir(f"{parent_dir_name}/subdir2/subdir2_1")
|
||||
sandbox.files.make_dir(f"{parent_dir_name}/subdir2/subdir2_2")
|
||||
sandbox.files.write(f"{parent_dir_name}/file1.txt", "Hello, world!")
|
||||
|
||||
test_cases: list[dict[str, Any]] = [
|
||||
{
|
||||
"name": "default depth (1)",
|
||||
"depth": None,
|
||||
"expected_len": 3,
|
||||
"expected_file_names": [
|
||||
"file1.txt",
|
||||
"subdir1",
|
||||
"subdir2",
|
||||
],
|
||||
"expected_file_types": [
|
||||
FileType.FILE,
|
||||
FileType.DIR,
|
||||
FileType.DIR,
|
||||
],
|
||||
"expected_file_paths": [
|
||||
f"{home_dir_name}/{parent_dir_name}/file1.txt",
|
||||
f"{home_dir_name}/{parent_dir_name}/subdir1",
|
||||
f"{home_dir_name}/{parent_dir_name}/subdir2",
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "explicit depth 1",
|
||||
"depth": 1,
|
||||
"expected_len": 3,
|
||||
"expected_file_names": [
|
||||
"file1.txt",
|
||||
"subdir1",
|
||||
"subdir2",
|
||||
],
|
||||
"expected_file_types": [
|
||||
FileType.FILE,
|
||||
FileType.DIR,
|
||||
FileType.DIR,
|
||||
],
|
||||
"expected_file_paths": [
|
||||
f"{home_dir_name}/{parent_dir_name}/file1.txt",
|
||||
f"{home_dir_name}/{parent_dir_name}/subdir1",
|
||||
f"{home_dir_name}/{parent_dir_name}/subdir2",
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "explicit depth 2",
|
||||
"depth": 2,
|
||||
"expected_len": 7,
|
||||
"expected_file_types": [
|
||||
FileType.FILE,
|
||||
FileType.DIR,
|
||||
FileType.DIR,
|
||||
FileType.DIR,
|
||||
FileType.DIR,
|
||||
FileType.DIR,
|
||||
FileType.DIR,
|
||||
],
|
||||
"expected_file_names": [
|
||||
"file1.txt",
|
||||
"subdir1",
|
||||
"subdir1_1",
|
||||
"subdir1_2",
|
||||
"subdir2",
|
||||
"subdir2_1",
|
||||
"subdir2_2",
|
||||
],
|
||||
"expected_file_paths": [
|
||||
f"{home_dir_name}/{parent_dir_name}/file1.txt",
|
||||
f"{home_dir_name}/{parent_dir_name}/subdir1",
|
||||
f"{home_dir_name}/{parent_dir_name}/subdir1/subdir1_1",
|
||||
f"{home_dir_name}/{parent_dir_name}/subdir1/subdir1_2",
|
||||
f"{home_dir_name}/{parent_dir_name}/subdir2",
|
||||
f"{home_dir_name}/{parent_dir_name}/subdir2/subdir2_1",
|
||||
f"{home_dir_name}/{parent_dir_name}/subdir2/subdir2_2",
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "explicit depth 3 (should be the same as depth 2)",
|
||||
"depth": 3,
|
||||
"expected_len": 7,
|
||||
"expected_file_names": [
|
||||
"file1.txt",
|
||||
"subdir1",
|
||||
"subdir1_1",
|
||||
"subdir1_2",
|
||||
"subdir2",
|
||||
"subdir2_1",
|
||||
"subdir2_2",
|
||||
],
|
||||
"expected_file_types": [
|
||||
FileType.FILE,
|
||||
FileType.DIR,
|
||||
FileType.DIR,
|
||||
FileType.DIR,
|
||||
FileType.DIR,
|
||||
FileType.DIR,
|
||||
FileType.DIR,
|
||||
],
|
||||
"expected_file_paths": [
|
||||
f"{home_dir_name}/{parent_dir_name}/file1.txt",
|
||||
f"{home_dir_name}/{parent_dir_name}/subdir1",
|
||||
f"{home_dir_name}/{parent_dir_name}/subdir1/subdir1_1",
|
||||
f"{home_dir_name}/{parent_dir_name}/subdir1/subdir1_2",
|
||||
f"{home_dir_name}/{parent_dir_name}/subdir2",
|
||||
f"{home_dir_name}/{parent_dir_name}/subdir2/subdir2_1",
|
||||
f"{home_dir_name}/{parent_dir_name}/subdir2/subdir2_2",
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
for test_case in test_cases:
|
||||
files = sandbox.files.list(
|
||||
parent_dir_name,
|
||||
depth=test_case["depth"] if test_case["depth"] is not None else None,
|
||||
)
|
||||
|
||||
assert len(files) == test_case["expected_len"]
|
||||
|
||||
for i in range(len(test_case["expected_file_names"])):
|
||||
assert files[i].name == test_case["expected_file_names"][i]
|
||||
assert files[i].path == test_case["expected_file_paths"][i]
|
||||
assert files[i].type == test_case["expected_file_types"][i]
|
||||
|
||||
sandbox.files.remove(parent_dir_name)
|
||||
|
||||
|
||||
def test_list_directory_error_cases(sandbox: Sandbox):
|
||||
parent_dir_name = f"test_directory_{uuid.uuid4()}"
|
||||
sandbox.files.make_dir(parent_dir_name)
|
||||
|
||||
expected_error_message = "depth should be at least 1"
|
||||
try:
|
||||
sandbox.files.list(parent_dir_name, depth=-1)
|
||||
assert False, "Expected error but none was thrown"
|
||||
except Exception as err:
|
||||
assert expected_error_message in str(err), (
|
||||
f'expected error message to include "{expected_error_message}"'
|
||||
)
|
||||
|
||||
sandbox.files.remove(parent_dir_name)
|
||||
|
||||
|
||||
def test_file_entry_details(sandbox: Sandbox):
|
||||
test_dir = "test-file-entry"
|
||||
file_path = f"{test_dir}/test.txt"
|
||||
content = "Hello, World!"
|
||||
|
||||
sandbox.files.make_dir(test_dir)
|
||||
sandbox.files.write(file_path, content)
|
||||
|
||||
files = sandbox.files.list(test_dir, depth=1)
|
||||
assert len(files) == 1
|
||||
|
||||
file_entry = files[0]
|
||||
assert file_entry.name == "test.txt"
|
||||
assert file_entry.path == f"/home/user/{file_path}"
|
||||
assert file_entry.type == FileType.FILE
|
||||
assert file_entry.mode == 0o644
|
||||
assert file_entry.permissions == "-rw-r--r--"
|
||||
assert file_entry.owner == "user"
|
||||
assert file_entry.group == "user"
|
||||
assert file_entry.size == len(content)
|
||||
assert file_entry.modified_time is not None
|
||||
assert file_entry.symlink_target is None
|
||||
|
||||
sandbox.files.remove(test_dir)
|
||||
|
||||
|
||||
def test_directory_entry_details(sandbox: Sandbox):
|
||||
test_dir = "test-entry-info"
|
||||
sub_dir = f"{test_dir}/subdir"
|
||||
|
||||
sandbox.files.make_dir(test_dir)
|
||||
sandbox.files.make_dir(sub_dir)
|
||||
|
||||
files = sandbox.files.list(test_dir, depth=1)
|
||||
assert len(files) == 1
|
||||
|
||||
dir_entry = files[0]
|
||||
assert dir_entry.name == "subdir"
|
||||
assert dir_entry.path == f"/home/user/{sub_dir}"
|
||||
assert dir_entry.type == FileType.DIR
|
||||
assert dir_entry.mode == 0o755
|
||||
assert dir_entry.permissions == "drwxr-xr-x"
|
||||
assert dir_entry.owner == "user"
|
||||
assert dir_entry.group == "user"
|
||||
assert dir_entry.modified_time is not None
|
||||
|
||||
sandbox.files.remove(test_dir)
|
||||
|
||||
|
||||
def test_mixed_entries(sandbox: Sandbox):
|
||||
test_dir = "test-mixed-entries"
|
||||
sub_dir = f"{test_dir}/subdir"
|
||||
file_path = f"{test_dir}/test.txt"
|
||||
content = "Hello, World!"
|
||||
|
||||
sandbox.files.make_dir(test_dir)
|
||||
sandbox.files.make_dir(sub_dir)
|
||||
sandbox.files.write(file_path, content)
|
||||
|
||||
files = sandbox.files.list(test_dir, depth=1)
|
||||
assert len(files) == 2
|
||||
|
||||
# Create a dictionary of entries by name for easier verification
|
||||
entries = {entry.name: entry for entry in files}
|
||||
|
||||
# Verify directory entry
|
||||
dir_entry = entries.get("subdir")
|
||||
assert dir_entry is not None
|
||||
assert dir_entry.path == f"/home/user/{sub_dir}"
|
||||
assert dir_entry.type == FileType.DIR
|
||||
assert dir_entry.mode == 0o755
|
||||
assert dir_entry.permissions == "drwxr-xr-x"
|
||||
assert dir_entry.owner == "user"
|
||||
assert dir_entry.group == "user"
|
||||
assert dir_entry.modified_time is not None
|
||||
|
||||
# Verify file entry
|
||||
file_entry = entries.get("test.txt")
|
||||
assert file_entry is not None
|
||||
assert file_entry.path == f"/home/user/{file_path}"
|
||||
assert file_entry.type == FileType.FILE
|
||||
assert file_entry.mode == 0o644
|
||||
assert file_entry.permissions == "-rw-r--r--"
|
||||
assert file_entry.owner == "user"
|
||||
assert file_entry.group == "user"
|
||||
assert file_entry.size == len(content)
|
||||
assert file_entry.modified_time is not None
|
||||
|
||||
sandbox.files.remove(test_dir)
|
||||
@@ -0,0 +1,75 @@
|
||||
import pytest
|
||||
from e2b.exceptions import FileNotFoundException
|
||||
from e2b import Sandbox, FileType
|
||||
|
||||
|
||||
def test_get_info_of_file(sandbox: Sandbox):
|
||||
filename = "test_file.txt"
|
||||
|
||||
sandbox.files.write(filename, "test")
|
||||
info = sandbox.files.get_info(filename)
|
||||
current_path = sandbox.commands.run("pwd")
|
||||
|
||||
assert info.name == filename
|
||||
assert info.type == FileType.FILE
|
||||
assert info.path == f"{current_path.stdout.strip()}/{filename}"
|
||||
assert info.size == 4
|
||||
assert info.mode == 0o644
|
||||
assert info.permissions == "-rw-r--r--"
|
||||
assert info.owner == "user"
|
||||
assert info.group == "user"
|
||||
assert info.modified_time is not None
|
||||
assert info.modified_time.tzinfo is not None
|
||||
|
||||
|
||||
def test_get_info_of_nonexistent_file(sandbox: Sandbox):
|
||||
filename = "test_does_not_exist.txt"
|
||||
|
||||
with pytest.raises(FileNotFoundException):
|
||||
sandbox.files.get_info(filename)
|
||||
|
||||
|
||||
def test_get_info_of_directory(sandbox: Sandbox):
|
||||
dirname = "test_dir"
|
||||
|
||||
sandbox.files.make_dir(dirname)
|
||||
info = sandbox.files.get_info(dirname)
|
||||
current_path = sandbox.commands.run("pwd")
|
||||
|
||||
assert info.name == dirname
|
||||
assert info.type == FileType.DIR
|
||||
assert info.path == f"{current_path.stdout.strip()}/{dirname}"
|
||||
assert info.size > 0
|
||||
assert info.mode == 0o755
|
||||
assert info.permissions == "drwxr-xr-x"
|
||||
assert info.owner == "user"
|
||||
assert info.group == "user"
|
||||
assert info.modified_time is not None
|
||||
assert info.modified_time.tzinfo is not None
|
||||
|
||||
|
||||
def test_get_info_of_nonexistent_directory(sandbox: Sandbox):
|
||||
dirname = "test_does_not_exist_dir"
|
||||
|
||||
with pytest.raises(FileNotFoundException):
|
||||
sandbox.files.get_info(dirname)
|
||||
|
||||
|
||||
def test_file_symlink(sandbox: Sandbox):
|
||||
test_dir = "test-simlink-entry"
|
||||
file_name = "test.txt"
|
||||
content = "Hello, World!"
|
||||
|
||||
sandbox.files.make_dir(test_dir)
|
||||
sandbox.files.write(f"{test_dir}/{file_name}", content)
|
||||
|
||||
symlink_name = "symlink_to_test.txt"
|
||||
sandbox.commands.run(f"ln -s {file_name} {symlink_name}", cwd=test_dir)
|
||||
|
||||
file = sandbox.files.get_info(f"{test_dir}/{symlink_name}")
|
||||
|
||||
pwd = sandbox.commands.run("pwd")
|
||||
assert file.type == FileType.FILE
|
||||
assert file.symlink_target == f"{pwd.stdout.strip()}/{test_dir}/{file_name}"
|
||||
|
||||
sandbox.files.remove(test_dir)
|
||||
@@ -0,0 +1,29 @@
|
||||
import uuid
|
||||
|
||||
from e2b import Sandbox
|
||||
|
||||
|
||||
def test_make_directory(sandbox: Sandbox):
|
||||
dir_name = f"test_directory_{uuid.uuid4()}"
|
||||
|
||||
sandbox.files.make_dir(dir_name)
|
||||
exists = sandbox.files.exists(dir_name)
|
||||
assert exists
|
||||
|
||||
|
||||
async def test_make_directory_already_exists(sandbox: Sandbox):
|
||||
dir_name = f"test_directory_{uuid.uuid4()}"
|
||||
|
||||
created = sandbox.files.make_dir(dir_name)
|
||||
assert created
|
||||
|
||||
created = sandbox.files.make_dir(dir_name)
|
||||
assert not created
|
||||
|
||||
|
||||
def test_make_nested_directory(sandbox: Sandbox):
|
||||
nested_dir_name = f"test_directory_{uuid.uuid4()}/nested_directory"
|
||||
|
||||
sandbox.files.make_dir(nested_dir_name)
|
||||
exists = sandbox.files.exists(nested_dir_name)
|
||||
assert exists
|
||||
@@ -0,0 +1,163 @@
|
||||
import pytest
|
||||
|
||||
from e2b.exceptions import InvalidArgumentException
|
||||
|
||||
|
||||
def test_write_file_with_metadata(sandbox, debug):
|
||||
filename = "test_metadata.txt"
|
||||
content = "This is a test file with metadata."
|
||||
metadata = {"author": "mish", "purpose": "upload"}
|
||||
|
||||
info = sandbox.files.write(filename, content, metadata=metadata)
|
||||
assert info.metadata == metadata
|
||||
|
||||
# Metadata is persisted and surfaced on subsequent reads.
|
||||
stat = sandbox.files.get_info(filename)
|
||||
assert stat.metadata == metadata
|
||||
|
||||
if debug:
|
||||
sandbox.files.remove(filename)
|
||||
|
||||
|
||||
def test_write_file_with_metadata_octet_stream(sandbox, debug):
|
||||
filename = "test_metadata_octet.txt"
|
||||
content = "This is a test file with metadata."
|
||||
metadata = {"author": "mish", "purpose": "upload"}
|
||||
|
||||
info = sandbox.files.write(
|
||||
filename, content, metadata=metadata, use_octet_stream=True
|
||||
)
|
||||
assert info.metadata == metadata
|
||||
|
||||
stat = sandbox.files.get_info(filename)
|
||||
assert stat.metadata == metadata
|
||||
|
||||
if debug:
|
||||
sandbox.files.remove(filename)
|
||||
|
||||
|
||||
def test_write_file_without_metadata(sandbox, debug):
|
||||
filename = "test_no_metadata.txt"
|
||||
|
||||
info = sandbox.files.write(filename, "no metadata here")
|
||||
assert info.metadata is None
|
||||
|
||||
stat = sandbox.files.get_info(filename)
|
||||
assert stat.metadata is None
|
||||
|
||||
if debug:
|
||||
sandbox.files.remove(filename)
|
||||
|
||||
|
||||
def test_write_files_applies_metadata_to_every_file(sandbox, debug):
|
||||
from e2b.sandbox.filesystem.filesystem import WriteEntry
|
||||
|
||||
# The same metadata is applied to every file in the upload.
|
||||
metadata = {"source": "test-suite"}
|
||||
files = [
|
||||
WriteEntry(path="metadata_multi_1.txt", data="File 1"),
|
||||
WriteEntry(path="metadata_multi_2.txt", data="File 2"),
|
||||
]
|
||||
|
||||
infos = sandbox.files.write_files(files, metadata=metadata)
|
||||
assert len(infos) == len(files)
|
||||
|
||||
for info in infos:
|
||||
assert info.metadata == metadata
|
||||
stat = sandbox.files.get_info(info.path)
|
||||
assert stat.metadata == metadata
|
||||
|
||||
if debug:
|
||||
for file in files:
|
||||
sandbox.files.remove(file["path"])
|
||||
|
||||
|
||||
def test_metadata_surfaced_when_listing(sandbox, debug):
|
||||
dirname = "metadata_list_dir"
|
||||
filename = "listed.txt"
|
||||
metadata = {"tag": "listed"}
|
||||
|
||||
sandbox.files.make_dir(dirname)
|
||||
sandbox.files.write(f"{dirname}/{filename}", "content", metadata=metadata)
|
||||
|
||||
entries = sandbox.files.list(dirname)
|
||||
entry = next((e for e in entries if e.name == filename), None)
|
||||
assert entry is not None
|
||||
assert entry.metadata == metadata
|
||||
|
||||
if debug:
|
||||
sandbox.files.remove(dirname)
|
||||
|
||||
|
||||
def test_metadata_surfaced_after_rename(sandbox, debug):
|
||||
old_path = "metadata_rename_old.txt"
|
||||
new_path = "metadata_rename_new.txt"
|
||||
metadata = {"stage": "renamed"}
|
||||
|
||||
sandbox.files.write(old_path, "content", metadata=metadata)
|
||||
info = sandbox.files.rename(old_path, new_path)
|
||||
assert info.metadata == metadata
|
||||
|
||||
if debug:
|
||||
sandbox.files.remove(new_path)
|
||||
|
||||
|
||||
def test_overwriting_clears_stale_metadata(sandbox, debug):
|
||||
filename = "metadata_overwrite.txt"
|
||||
|
||||
sandbox.files.write(filename, "first", metadata={"author": "mish"})
|
||||
|
||||
# Overwriting without metadata removes the previously stored metadata.
|
||||
info = sandbox.files.write(filename, "second")
|
||||
assert info.metadata is None
|
||||
|
||||
stat = sandbox.files.get_info(filename)
|
||||
assert stat.metadata is None
|
||||
|
||||
if debug:
|
||||
sandbox.files.remove(filename)
|
||||
|
||||
|
||||
def test_metadata_set_via_xattrs_surfaced_in_get_info(sandbox, debug):
|
||||
filename = "metadata_xattr.txt"
|
||||
sandbox.files.write(filename, "content")
|
||||
|
||||
file_path = sandbox.commands.run(f"realpath {filename}").stdout.strip()
|
||||
|
||||
# Set an xattr directly in the `user.e2b.` namespace (out-of-band, not via
|
||||
# the SDK upload); it should surface as metadata (with the namespace prefix
|
||||
# stripped) when reading the file info.
|
||||
sandbox.commands.run(
|
||||
f"python3 -c \"import os; os.setxattr('{file_path}', 'user.e2b.author', b'mish')\""
|
||||
)
|
||||
|
||||
info = sandbox.files.get_info(filename)
|
||||
assert info.metadata == {"author": "mish"}
|
||||
|
||||
if debug:
|
||||
sandbox.files.remove(filename)
|
||||
|
||||
|
||||
def test_write_rejects_invalid_metadata(sandbox):
|
||||
filename = "invalid_metadata.txt"
|
||||
|
||||
# Key with a space is not a valid HTTP header token.
|
||||
with pytest.raises(InvalidArgumentException):
|
||||
sandbox.files.write(filename, "x", metadata={"bad key": "value"})
|
||||
|
||||
# Empty key.
|
||||
with pytest.raises(InvalidArgumentException):
|
||||
sandbox.files.write(filename, "x", metadata={"": "value"})
|
||||
|
||||
# Value with a non-printable / non-ASCII character.
|
||||
with pytest.raises(InvalidArgumentException):
|
||||
sandbox.files.write(filename, "x", metadata={"good": "bad\nvalue"})
|
||||
|
||||
# Trailing newline (Python's `$` would accept it; `\Z` must not).
|
||||
with pytest.raises(InvalidArgumentException):
|
||||
sandbox.files.write(filename, "x", metadata={"good": "value\n"})
|
||||
with pytest.raises(InvalidArgumentException):
|
||||
sandbox.files.write(filename, "x", metadata={"key\n": "value"})
|
||||
|
||||
# The file must not have been created by a rejected write.
|
||||
assert not sandbox.files.exists(filename)
|
||||
@@ -0,0 +1,83 @@
|
||||
import pytest
|
||||
from e2b import FileNotFoundException, NotFoundException
|
||||
|
||||
|
||||
def test_read_file(sandbox):
|
||||
filename = "test_read.txt"
|
||||
content = "Hello, world!"
|
||||
|
||||
sandbox.files.write(filename, content)
|
||||
read_content = sandbox.files.read(filename)
|
||||
assert read_content == content
|
||||
|
||||
|
||||
def test_read_non_existing_file(sandbox):
|
||||
filename = "non_existing_file.txt"
|
||||
|
||||
with pytest.raises(FileNotFoundException):
|
||||
sandbox.files.read(filename)
|
||||
|
||||
|
||||
def test_read_non_existing_file_catches_with_deprecated_not_found_exception(sandbox):
|
||||
filename = "non_existing_file.txt"
|
||||
|
||||
with pytest.raises(NotFoundException):
|
||||
sandbox.files.read(filename)
|
||||
|
||||
|
||||
def test_read_empty_file(sandbox):
|
||||
filename = "empty_file.txt"
|
||||
content = ""
|
||||
|
||||
sandbox.commands.run(f"touch {filename}")
|
||||
read_content = sandbox.files.read(filename)
|
||||
assert read_content == content
|
||||
|
||||
|
||||
def test_read_file_as_stream(sandbox):
|
||||
filename = "test_read_stream.txt"
|
||||
content = "Streamed read content. " * 10_000
|
||||
|
||||
sandbox.files.write(filename, content)
|
||||
stream = sandbox.files.read(filename, format="stream")
|
||||
read_content = b"".join(stream).decode("utf-8")
|
||||
assert read_content == content
|
||||
|
||||
|
||||
def test_read_file_as_stream_with_gzip(sandbox):
|
||||
filename = "test_read_stream_gzip.txt"
|
||||
content = "Streamed gzipped read content. " * 10_000
|
||||
|
||||
sandbox.files.write(filename, content)
|
||||
stream = sandbox.files.read(filename, format="stream", gzip=True)
|
||||
read_content = b"".join(stream).decode("utf-8")
|
||||
assert read_content == content
|
||||
|
||||
|
||||
def test_read_non_existing_file_as_stream(sandbox):
|
||||
filename = "non_existing_file.txt"
|
||||
|
||||
with pytest.raises(FileNotFoundException):
|
||||
sandbox.files.read(filename, format="stream")
|
||||
|
||||
|
||||
def test_read_file_as_stream_context_manager(sandbox):
|
||||
filename = "test_read_stream_ctx.txt"
|
||||
content = "Streamed read content. " * 10_000
|
||||
|
||||
sandbox.files.write(filename, content)
|
||||
with sandbox.files.read(filename, format="stream") as stream:
|
||||
read_content = b"".join(stream).decode("utf-8")
|
||||
assert read_content == content
|
||||
|
||||
|
||||
def test_read_file_as_stream_partial_then_close(sandbox):
|
||||
filename = "test_read_stream_partial.txt"
|
||||
content = "Streamed read content. " * 10_000
|
||||
|
||||
sandbox.files.write(filename, content)
|
||||
# Reading only the first chunk and closing must not raise or leak.
|
||||
stream = sandbox.files.read(filename, format="stream")
|
||||
first = next(iter(stream))
|
||||
assert len(first) > 0
|
||||
stream.close()
|
||||
@@ -0,0 +1,18 @@
|
||||
from e2b import Sandbox
|
||||
|
||||
|
||||
def test_remove_file(sandbox: Sandbox):
|
||||
filename = "test_remove.txt"
|
||||
content = "This file will be removed."
|
||||
|
||||
sandbox.files.write(filename, content)
|
||||
|
||||
sandbox.files.remove(filename)
|
||||
|
||||
exists = sandbox.files.exists(filename)
|
||||
assert not exists
|
||||
|
||||
|
||||
def test_remove_non_existing_file(sandbox):
|
||||
filename = "non_existing_file.txt"
|
||||
sandbox.files.remove(filename)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user