chore: import upstream snapshot with attribution
This commit is contained in:
@@ -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 Template
|
||||
from e2b.template.types import InstructionType
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_from_dockerfile():
|
||||
dockerfile = """FROM node:24
|
||||
WORKDIR /app
|
||||
COPY package.json .
|
||||
RUN npm install
|
||||
ENTRYPOINT ["sleep", "20"]"""
|
||||
|
||||
template = Template().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()
|
||||
def test_from_dockerfile_with_default_user_and_workdir():
|
||||
dockerfile = "FROM node:24"
|
||||
|
||||
template = Template().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()
|
||||
def test_from_dockerfile_with_custom_user_and_workdir():
|
||||
dockerfile = "FROM node:24\nUSER mish\nWORKDIR /home/mish"
|
||||
|
||||
template = Template().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()
|
||||
def test_from_dockerfile_with_multi_source_copy():
|
||||
dockerfile = """FROM node:24
|
||||
COPY file1.txt file2.txt file3.txt /dest/"""
|
||||
|
||||
template = Template().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()
|
||||
def test_from_dockerfile_with_multi_source_copy_chown():
|
||||
dockerfile = """FROM node:24
|
||||
COPY --chown=myuser:mygroup pkg.json pkg-lock.json /app/"""
|
||||
|
||||
template = Template().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()
|
||||
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 = Template().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 Template
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_make_symlink(build):
|
||||
template = (
|
||||
Template()
|
||||
.from_image("ubuntu:22.04")
|
||||
.skip_cache()
|
||||
.make_symlink(".bashrc", ".bashrc.local")
|
||||
.run_cmd('test "$(readlink .bashrc.local)" = ".bashrc"')
|
||||
)
|
||||
|
||||
build(template)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_make_symlink_force(build):
|
||||
template = (
|
||||
Template()
|
||||
.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"')
|
||||
)
|
||||
|
||||
build(template)
|
||||
@@ -0,0 +1,40 @@
|
||||
import pytest
|
||||
|
||||
from e2b import Template
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_run_command(build):
|
||||
template = Template().from_image("ubuntu:22.04").skip_cache().run_cmd("ls -l")
|
||||
|
||||
build(template)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_run_command_as_different_user(build):
|
||||
template = (
|
||||
Template()
|
||||
.from_image("ubuntu:22.04")
|
||||
.skip_cache()
|
||||
.run_cmd('test "$(whoami)" = "root"', user="root")
|
||||
)
|
||||
|
||||
build(template)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_run_command_as_user_that_does_not_exist(build):
|
||||
template = (
|
||||
Template()
|
||||
.from_image("ubuntu:22.04")
|
||||
.skip_cache()
|
||||
.run_cmd("whoami", user="root123")
|
||||
)
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
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 Template
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_to_dockerfile():
|
||||
template = (
|
||||
Template()
|
||||
.from_ubuntu_image("24.04")
|
||||
.copy("README.md", "/app/README.md")
|
||||
.run_cmd('echo "Hello, World!"')
|
||||
)
|
||||
|
||||
dockerfile = Template.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()
|
||||
def test_to_dockerfile_with_options():
|
||||
template = (
|
||||
Template()
|
||||
.from_ubuntu_image("24.04")
|
||||
.copy("README.md", "/app/README.md", user="root")
|
||||
.run_cmd('echo "Hello, World!"', user="root")
|
||||
)
|
||||
|
||||
dockerfile = Template.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()
|
||||
def test_to_dockerfile_with_env_instructions():
|
||||
template = (
|
||||
Template()
|
||||
.from_ubuntu_image("24.04")
|
||||
.set_envs({"NODE_ENV": "production", "PORT": "8080"})
|
||||
.set_envs({"DEBUG": "false"})
|
||||
)
|
||||
|
||||
dockerfile = Template.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 Template, wait_for_timeout
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
@pytest.mark.timeout(10)
|
||||
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 = (
|
||||
Template()
|
||||
.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 = Template.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 = Template.get_build_status(build_info)
|
||||
assert status.status.value == "building"
|
||||
@@ -0,0 +1,92 @@
|
||||
import tempfile
|
||||
|
||||
import pytest
|
||||
import os
|
||||
import shutil
|
||||
|
||||
from e2b import Template, wait_for_timeout, default_build_logger
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def setup_test_folder():
|
||||
test_dir = tempfile.mkdtemp(prefix="python_sync_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()
|
||||
def test_build_template(build, setup_test_folder):
|
||||
template = (
|
||||
Template(file_context_path=setup_test_folder)
|
||||
# using base image to avoid re-building ubuntu:22.04 image
|
||||
.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))
|
||||
)
|
||||
|
||||
build(template, skip_cache=True, on_build_logs=default_build_logger())
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_build_template_from_base_template(build):
|
||||
template = Template().from_template("base")
|
||||
build(template, skip_cache=True, on_build_logs=default_build_logger())
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_build_template_with_symlinks(build, setup_test_folder):
|
||||
template = (
|
||||
Template(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")
|
||||
)
|
||||
|
||||
build(template)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_build_template_with_resolve_symlinks(build, setup_test_folder):
|
||||
template = (
|
||||
Template(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")
|
||||
)
|
||||
|
||||
build(template)
|
||||
@@ -0,0 +1,20 @@
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
|
||||
from e2b import Template
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_check_base_template_name_exists():
|
||||
"""Test that the base template name exists."""
|
||||
exists = Template.exists("base")
|
||||
assert exists is True
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_check_non_existing_name():
|
||||
"""Test that a non-existing name returns False."""
|
||||
non_existing_name = f"nonexistent-{uuid.uuid4()}"
|
||||
exists = Template.exists(non_existing_name)
|
||||
assert exists is False
|
||||
@@ -0,0 +1,422 @@
|
||||
import traceback
|
||||
from types import SimpleNamespace
|
||||
from typing import List, Optional
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
import linecache
|
||||
|
||||
from e2b import Template, CopyItem, wait_for_timeout
|
||||
from e2b.template.types import TemplateBuildStatus
|
||||
import e2b.template_sync.main as template_sync_main
|
||||
import e2b.template_sync.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):
|
||||
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 [])
|
||||
|
||||
def mock_trigger_build(client, template_id: str, build_id: str, template):
|
||||
return None
|
||||
|
||||
def mock_get_file_upload_link(
|
||||
client, template_id: str, files_hash: str, stack_trace=None
|
||||
):
|
||||
return SimpleNamespace(present=True, url=None)
|
||||
|
||||
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_sync_main, "request_build", mock_request_build)
|
||||
monkeypatch.setattr(template_sync_main, "trigger_build", mock_trigger_build)
|
||||
monkeypatch.setattr(
|
||||
template_sync_main, "get_file_upload_link", mock_get_file_upload_link
|
||||
)
|
||||
monkeypatch.setattr(build_api_mod, "get_build_status", mock_get_build_status)
|
||||
|
||||
|
||||
def _expect_to_throw_and_check_trace(func, expected_method: str):
|
||||
try:
|
||||
func()
|
||||
assert False, "Expected Template.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()
|
||||
def test_traces_on_from_image(build):
|
||||
template = Template()
|
||||
template = template.from_image("e2b.dev/this-image-does-not-exist")
|
||||
_expect_to_throw_and_check_trace(
|
||||
lambda: build(template, name="from_image", skip_cache=True), "from_image"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_traces_on_from_template(build):
|
||||
template = Template().from_template("this-template-does-not-exist")
|
||||
_expect_to_throw_and_check_trace(
|
||||
lambda: build(template, name="from_template", skip_cache=True), "from_template"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_traces_on_from_dockerfile(build):
|
||||
template = Template()
|
||||
template = template.from_dockerfile("FROM ubuntu:22.04\nRUN nonexistent")
|
||||
_expect_to_throw_and_check_trace(
|
||||
lambda: build(template, name="from_dockerfile", skip_cache=True),
|
||||
"from_dockerfile",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_traces_on_from_image_registry(build):
|
||||
template = Template()
|
||||
template = template.from_image(
|
||||
"registry.example.com/nonexistent:latest",
|
||||
username="test",
|
||||
password="test",
|
||||
)
|
||||
_expect_to_throw_and_check_trace(
|
||||
lambda: build(template, name="from_image_registry", skip_cache=True),
|
||||
"from_image",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_traces_on_from_image_credentials():
|
||||
_expect_to_throw_and_check_trace(
|
||||
lambda: Template().from_image("ubuntu:22.04", username="user"),
|
||||
"from_image",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_traces_on_from_aws_registry(build):
|
||||
template = Template()
|
||||
template = template.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",
|
||||
)
|
||||
_expect_to_throw_and_check_trace(
|
||||
lambda: build(template, name="from_aws_registry"), "from_aws_registry"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_traces_on_from_gcp_registry(build):
|
||||
template = Template()
|
||||
template = template.from_gcp_registry(
|
||||
"gcr.io/nonexistent-project/nonexistent:latest",
|
||||
service_account_json={
|
||||
"type": "service_account",
|
||||
},
|
||||
)
|
||||
_expect_to_throw_and_check_trace(
|
||||
lambda: build(template, name="from_gcp_registry"), "from_gcp_registry"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_traces_on_copy(build):
|
||||
template = Template()
|
||||
template = template.from_base_image()
|
||||
template = template.skip_cache().copy(non_existent_path, non_existent_path)
|
||||
_expect_to_throw_and_check_trace(lambda: build(template, name="copy"), "copy")
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_traces_on_copyItems(build):
|
||||
template = Template()
|
||||
template = template.from_base_image()
|
||||
template = template.skip_cache().copy_items(
|
||||
[CopyItem(src=non_existent_path, dest=non_existent_path)]
|
||||
)
|
||||
_expect_to_throw_and_check_trace(
|
||||
lambda: build(template, name="copy_items"), "copy_items"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_traces_on_second_source_of_multi_source_copy(build):
|
||||
template = Template()
|
||||
template = template.from_base_image()
|
||||
template = template.copy(["test_stacktrace.py", "test_tags.py"], ".")
|
||||
_expect_to_throw_and_check_trace(
|
||||
lambda: build(template, name="multi_source_copy_second_source"), "copy"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_traces_on_step_after_multi_source_copy(build):
|
||||
template = Template()
|
||||
template = template.from_base_image()
|
||||
template = template.copy(["test_stacktrace.py", "test_tags.py"], ".")
|
||||
template = template.run_cmd(f"cat {non_existent_path}")
|
||||
_expect_to_throw_and_check_trace(
|
||||
lambda: build(template, name="multi_source_copy_next_step"), "run_cmd"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_traces_on_second_item_of_copy_items(build):
|
||||
template = Template()
|
||||
template = template.from_base_image()
|
||||
template = template.copy_items(
|
||||
[
|
||||
CopyItem(src="test_stacktrace.py", dest="."),
|
||||
CopyItem(src="test_tags.py", dest="."),
|
||||
]
|
||||
)
|
||||
_expect_to_throw_and_check_trace(
|
||||
lambda: build(template, name="copy_items_second_item"), "copy_items"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_traces_on_step_after_copy_items(build):
|
||||
template = Template()
|
||||
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}")
|
||||
_expect_to_throw_and_check_trace(
|
||||
lambda: build(template, name="copy_items_next_step"), "run_cmd"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_traces_on_copy_absolute_path():
|
||||
_expect_to_throw_and_check_trace(
|
||||
lambda: Template().from_base_image().copy("/absolute/path", "/absolute/path"),
|
||||
"copy",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_traces_on_copyItems_absolute_path():
|
||||
_expect_to_throw_and_check_trace(
|
||||
lambda: (
|
||||
Template()
|
||||
.from_base_image()
|
||||
.copy_items([CopyItem(src="/absolute/path", dest="/absolute/path")])
|
||||
),
|
||||
"copy_items",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_traces_on_remove(build):
|
||||
template = Template()
|
||||
template = template.from_base_image()
|
||||
template = template.skip_cache().remove(non_existent_path)
|
||||
_expect_to_throw_and_check_trace(lambda: build(template, name="remove"), "remove")
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_traces_on_rename(build):
|
||||
template = Template()
|
||||
template = template.from_base_image()
|
||||
template = template.skip_cache().rename(non_existent_path, "/tmp/dest.txt")
|
||||
_expect_to_throw_and_check_trace(lambda: build(template, name="rename"), "rename")
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_traces_on_make_dir(build):
|
||||
template = Template()
|
||||
template = template.from_base_image()
|
||||
template = template.set_user("root").skip_cache().make_dir("/root/.bashrc")
|
||||
_expect_to_throw_and_check_trace(
|
||||
lambda: build(template, name="make_dir"), "make_dir"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_traces_on_make_symlink(build):
|
||||
template = Template()
|
||||
template = template.from_base_image()
|
||||
template = template.skip_cache().make_symlink(".bashrc", ".bashrc")
|
||||
_expect_to_throw_and_check_trace(
|
||||
lambda: build(template, name="make_symlink"), "make_symlink"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_traces_on_run_cmd(build):
|
||||
template = Template()
|
||||
template = template.from_base_image()
|
||||
template = template.skip_cache().run_cmd(f"cat {non_existent_path}")
|
||||
_expect_to_throw_and_check_trace(lambda: build(template, name="run_cmd"), "run_cmd")
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_traces_on_set_workdir(build):
|
||||
template = Template()
|
||||
template = template.from_base_image()
|
||||
template = template.set_user("root").skip_cache().set_workdir("/root/.bashrc")
|
||||
_expect_to_throw_and_check_trace(
|
||||
lambda: build(template, name="set_workdir"), "set_workdir"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_traces_on_set_user(build):
|
||||
template = Template()
|
||||
template = template.from_base_image()
|
||||
template = template.skip_cache().set_user("; exit 1")
|
||||
_expect_to_throw_and_check_trace(
|
||||
lambda: build(template, name="set_user"), "set_user"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_traces_on_pip_install(build):
|
||||
template = Template()
|
||||
template = template.from_base_image()
|
||||
template = template.skip_cache().pip_install("nonexistent-package")
|
||||
_expect_to_throw_and_check_trace(
|
||||
lambda: build(template, name="pip_install"), "pip_install"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_traces_on_npm_install(build):
|
||||
template = Template()
|
||||
template = template.from_base_image()
|
||||
template = template.skip_cache().npm_install("nonexistent-package")
|
||||
_expect_to_throw_and_check_trace(
|
||||
lambda: build(template, name="npm_install"), "npm_install"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_traces_on_apt_install(build):
|
||||
template = Template()
|
||||
template = template.from_base_image()
|
||||
template = template.skip_cache().apt_install("nonexistent-package")
|
||||
_expect_to_throw_and_check_trace(
|
||||
lambda: build(template, name="apt_install"), "apt_install"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_traces_on_git_clone(build):
|
||||
template = Template()
|
||||
template = template.from_base_image()
|
||||
template = template.skip_cache().git_clone("https://github.com/repo.git")
|
||||
_expect_to_throw_and_check_trace(
|
||||
lambda: build(template, name="git_clone"), "git_clone"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_traces_on_set_start_cmd(build):
|
||||
template = Template()
|
||||
template = template.from_base_image()
|
||||
template = template.set_start_cmd(
|
||||
f"./{non_existent_path}", wait_for_timeout(10_000)
|
||||
)
|
||||
_expect_to_throw_and_check_trace(
|
||||
lambda: build(template, name="set_start_cmd"), "set_start_cmd"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_traces_on_add_mcp_server():
|
||||
# needs mcp-gateway as base template, without it no mcp servers can be added
|
||||
_expect_to_throw_and_check_trace(
|
||||
lambda: Template().from_base_image().skip_cache().add_mcp_server("exa"),
|
||||
"add_mcp_server",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_traces_on_dev_container_prebuild(build):
|
||||
template = Template()
|
||||
template = template.from_template("devcontainer")
|
||||
template = template.skip_cache().beta_dev_container_prebuild(non_existent_path)
|
||||
_expect_to_throw_and_check_trace(
|
||||
lambda: build(template, name="beta_dev_container_prebuild"),
|
||||
"beta_dev_container_prebuild",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_traces_on_set_dev_container_start(build):
|
||||
template = Template()
|
||||
template = template.from_template("devcontainer")
|
||||
template = template.beta_set_dev_container_start(non_existent_path)
|
||||
_expect_to_throw_and_check_trace(
|
||||
lambda: build(template, name="beta_set_dev_container_start"),
|
||||
"beta_set_dev_container_start",
|
||||
)
|
||||
@@ -0,0 +1,228 @@
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
|
||||
from e2b import TemplateTag, TemplateTagInfo, Template
|
||||
from e2b.exceptions import TemplateException
|
||||
import e2b.template_sync.main as template_sync_main
|
||||
|
||||
|
||||
class TestAssignTags:
|
||||
"""Tests for Template.assign_tags method."""
|
||||
|
||||
def test_assign_single_tag(self, monkeypatch):
|
||||
"""Test assigning a single tag to a template."""
|
||||
mock_assign_tags = Mock(
|
||||
return_value=TemplateTagInfo(
|
||||
build_id="00000000-0000-0000-0000-000000000000",
|
||||
tags=["production"],
|
||||
)
|
||||
)
|
||||
|
||||
monkeypatch.setattr(
|
||||
template_sync_main, "get_api_client", lambda *args, **kwargs: None
|
||||
)
|
||||
monkeypatch.setattr(template_sync_main, "assign_tags", mock_assign_tags)
|
||||
|
||||
result = Template.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"]
|
||||
|
||||
def test_assign_multiple_tags(self, monkeypatch):
|
||||
"""Test assigning multiple tags to a template."""
|
||||
mock_assign_tags = Mock(
|
||||
return_value=TemplateTagInfo(
|
||||
build_id="00000000-0000-0000-0000-000000000000",
|
||||
tags=["production", "stable"],
|
||||
)
|
||||
)
|
||||
|
||||
monkeypatch.setattr(
|
||||
template_sync_main, "get_api_client", lambda *args, **kwargs: None
|
||||
)
|
||||
monkeypatch.setattr(template_sync_main, "assign_tags", mock_assign_tags)
|
||||
|
||||
result = Template.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 Template.remove_tags method."""
|
||||
|
||||
def test_remove_single_tag(self, monkeypatch):
|
||||
"""Test deleting a single tag from a template."""
|
||||
mock_remove_tags = Mock(return_value=None)
|
||||
|
||||
monkeypatch.setattr(
|
||||
template_sync_main, "get_api_client", lambda *args, **kwargs: None
|
||||
)
|
||||
monkeypatch.setattr(template_sync_main, "remove_tags", mock_remove_tags)
|
||||
|
||||
Template.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"]
|
||||
|
||||
def test_remove_multiple_tags(self, monkeypatch):
|
||||
"""Test deleting multiple tags from a template."""
|
||||
mock_remove_tags = Mock(return_value=None)
|
||||
|
||||
monkeypatch.setattr(
|
||||
template_sync_main, "get_api_client", lambda *args, **kwargs: None
|
||||
)
|
||||
monkeypatch.setattr(template_sync_main, "remove_tags", mock_remove_tags)
|
||||
|
||||
Template.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"]
|
||||
|
||||
def test_remove_tags_error(self, monkeypatch):
|
||||
"""Test that remove_tags raises an error for nonexistent template."""
|
||||
mock_remove_tags = Mock(side_effect=TemplateException("Template not found"))
|
||||
|
||||
monkeypatch.setattr(
|
||||
template_sync_main, "get_api_client", lambda *args, **kwargs: None
|
||||
)
|
||||
monkeypatch.setattr(template_sync_main, "remove_tags", mock_remove_tags)
|
||||
|
||||
with pytest.raises(TemplateException):
|
||||
Template.remove_tags("nonexistent", ["tag"])
|
||||
|
||||
|
||||
class TestGetTags:
|
||||
"""Tests for Template.get_tags method."""
|
||||
|
||||
def test_get_tags(self, monkeypatch):
|
||||
"""Test getting tags for a template."""
|
||||
mock_get_template_tags = Mock(
|
||||
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_sync_main, "get_api_client", lambda *args, **kwargs: None
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
template_sync_main, "get_template_tags", mock_get_template_tags
|
||||
)
|
||||
|
||||
result = Template.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()
|
||||
|
||||
def test_get_tags_error(self, monkeypatch):
|
||||
"""Test that get_tags raises an error for nonexistent template."""
|
||||
mock_get_template_tags = Mock(
|
||||
side_effect=TemplateException("Template not found")
|
||||
)
|
||||
|
||||
monkeypatch.setattr(
|
||||
template_sync_main, "get_api_client", lambda *args, **kwargs: None
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
template_sync_main, "get_template_tags", mock_get_template_tags
|
||||
)
|
||||
|
||||
with pytest.raises(TemplateException):
|
||||
Template.get_tags("nonexistent")
|
||||
|
||||
|
||||
# Integration tests
|
||||
class TestTagsIntegration:
|
||||
"""Integration tests for Template tags functionality."""
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_build_template_with_tags_assign_and_delete(self, 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 = build(template, name=initial_tag)
|
||||
|
||||
assert build_info.build_id
|
||||
assert build_info.template_id
|
||||
|
||||
# Assign additional tags
|
||||
tag_info = Template.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()
|
||||
def test_assign_single_tag_to_existing_template(self, 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()
|
||||
build(template, name=initial_tag)
|
||||
|
||||
# Assign single tag (not array)
|
||||
tag_info = Template.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()
|
||||
def test_rejects_invalid_tag_format_missing_alias(self, 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()
|
||||
build(template, name=initial_tag)
|
||||
|
||||
# Tag without alias (starts with colon) should be rejected
|
||||
with pytest.raises(Exception):
|
||||
Template.assign_tags(initial_tag, ":invalid-tag")
|
||||
|
||||
@pytest.mark.skip_debug()
|
||||
def test_rejects_invalid_tag_format_missing_tag(self, 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()
|
||||
build(template, name=initial_tag)
|
||||
|
||||
# Tag without tag portion (ends with colon) should be rejected
|
||||
with pytest.raises(Exception):
|
||||
Template.assign_tags(initial_tag, f"{template_name}:")
|
||||
@@ -0,0 +1,193 @@
|
||||
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_sync.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.
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
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_httpx_client(self):
|
||||
raise AssertionError("signed uploads should not use the API client")
|
||||
|
||||
client = UploadClient(base_url="http://test", token="test")
|
||||
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"]
|
||||
|
||||
|
||||
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.Client
|
||||
|
||||
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_sync.build_api.httpx.Client", side_effect=spy_client
|
||||
):
|
||||
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"]
|
||||
|
||||
|
||||
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 = _capture_upload_timeout(tmp_path)
|
||||
assert timeout == httpx.Timeout(FILE_UPLOAD_TIMEOUT_SECONDS)
|
||||
|
||||
|
||||
def test_upload_file_honors_explicit_request_timeout(tmp_path):
|
||||
# An explicitly set request_timeout overrides the 1-hour upload default.
|
||||
timeout = _capture_upload_timeout(tmp_path, request_timeout=5.0)
|
||||
assert timeout == httpx.Timeout(5.0)
|
||||
|
||||
|
||||
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_sync.build_api.tar_file_stream",
|
||||
side_effect=failing_close_stream,
|
||||
):
|
||||
# Must not raise despite close() failing after a 200 response.
|
||||
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
|
||||
Reference in New Issue
Block a user