chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:47:58 +08:00
commit b16403ea71
789 changed files with 115226 additions and 0 deletions
@@ -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