chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,281 @@
|
||||
import datetime
|
||||
from io import BytesIO, StringIO
|
||||
|
||||
import pytest
|
||||
|
||||
from e2b import Volume
|
||||
from e2b.exceptions import NotFoundException, VolumeException
|
||||
|
||||
|
||||
class TestWriteFileAndReadFile:
|
||||
def test_write_and_read_text_file(self, volume: Volume):
|
||||
path = "/test.txt"
|
||||
content = "Hello, World!"
|
||||
|
||||
volume.write_file(path, content)
|
||||
read_content = volume.read_file(path, format="text")
|
||||
|
||||
assert read_content == content
|
||||
|
||||
def test_write_and_read_bytes(self, volume: Volume):
|
||||
path = "/test-bytes.txt"
|
||||
content = "Test bytes content"
|
||||
content_bytes = content.encode("utf-8")
|
||||
|
||||
volume.write_file(path, content_bytes)
|
||||
read_bytes = volume.read_file(path, format="bytes")
|
||||
|
||||
assert read_bytes == content_bytes
|
||||
|
||||
def test_write_and_read_binary_data(self, volume: Volume):
|
||||
path = "/binary.bin"
|
||||
binary_data = bytes([0, 1, 2, 3, 4])
|
||||
|
||||
volume.write_file(path, binary_data)
|
||||
read_bytes = volume.read_file(path, format="bytes")
|
||||
|
||||
assert read_bytes == binary_data
|
||||
|
||||
def test_write_and_read_stream(self, volume: Volume):
|
||||
path = "/test-stream.txt"
|
||||
content = "Test stream content"
|
||||
stream = BytesIO(content.encode("utf-8"))
|
||||
|
||||
volume.write_file(path, stream)
|
||||
read_stream = volume.read_file(path, format="stream")
|
||||
read_content = b"".join(read_stream).decode("utf-8")
|
||||
|
||||
assert read_content == content
|
||||
|
||||
def test_write_and_read_text_stream(self, volume: Volume):
|
||||
path = "/test-text-stream.txt"
|
||||
content = "Test text stream content"
|
||||
stream = StringIO(content)
|
||||
|
||||
volume.write_file(path, stream)
|
||||
read_content = volume.read_file(path, format="text")
|
||||
|
||||
assert read_content == content
|
||||
|
||||
def test_write_and_read_empty_file(self, volume: Volume):
|
||||
path = "/empty.txt"
|
||||
content = ""
|
||||
|
||||
volume.write_file(path, content)
|
||||
read_content = volume.read_file(path, format="text")
|
||||
|
||||
assert read_content == content
|
||||
|
||||
def test_overwrite_with_force(self, volume: Volume):
|
||||
path = "/overwrite.txt"
|
||||
initial_content = "Initial content"
|
||||
new_content = "New content"
|
||||
|
||||
volume.write_file(path, initial_content)
|
||||
volume.write_file(path, new_content, force=True)
|
||||
read_content = volume.read_file(path, format="text")
|
||||
|
||||
assert read_content == new_content
|
||||
|
||||
def test_write_existing_file_without_force_raises(self, volume: Volume):
|
||||
path = "/no-overwrite.txt"
|
||||
initial_content = "Initial content"
|
||||
new_content = "New content"
|
||||
|
||||
volume.write_file(path, initial_content)
|
||||
with pytest.raises(VolumeException):
|
||||
volume.write_file(path, new_content, force=False)
|
||||
|
||||
def test_write_file_with_metadata(self, volume: Volume):
|
||||
path = "/metadata.txt"
|
||||
content = "File with metadata"
|
||||
|
||||
volume.write_file(path, content, uid=1000, gid=1000, mode=0o644)
|
||||
|
||||
entry_info = 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
|
||||
|
||||
def test_write_file_in_nested_directory(self, volume: Volume):
|
||||
dir_path = "/nested/deep/path"
|
||||
file_path = f"{dir_path}/file.txt"
|
||||
content = "Nested file content"
|
||||
|
||||
volume.make_dir(dir_path, force=True)
|
||||
volume.write_file(file_path, content)
|
||||
read_content = volume.read_file(file_path, format="text")
|
||||
|
||||
assert read_content == content
|
||||
|
||||
|
||||
class TestGetInfo:
|
||||
def test_get_info_for_file(self, volume: Volume):
|
||||
path = "/info-file.txt"
|
||||
content = "File for info test"
|
||||
|
||||
volume.write_file(path, content)
|
||||
info = 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)
|
||||
|
||||
def test_get_info_for_directory(self, volume: Volume):
|
||||
path = "/info-dir"
|
||||
|
||||
volume.make_dir(path)
|
||||
info = volume.get_info(path)
|
||||
|
||||
assert info.name == "info-dir"
|
||||
assert info.type.value == "directory"
|
||||
assert info.path == path
|
||||
|
||||
def test_exists_returns_false_for_nonexistent(self, volume: Volume):
|
||||
assert volume.exists("/non-existent.txt") is False
|
||||
|
||||
|
||||
class TestUpdateMetadata:
|
||||
def test_update_file_metadata(self, volume: Volume):
|
||||
path = "/metadata-update.txt"
|
||||
volume.write_file(path, "Content")
|
||||
|
||||
updated = 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
|
||||
|
||||
def test_update_metadata_nonexistent_raises(self, volume: Volume):
|
||||
with pytest.raises(NotFoundException):
|
||||
volume.update_metadata("/non-existent.txt", mode=0o644)
|
||||
|
||||
|
||||
class TestMakeDir:
|
||||
def test_create_directory(self, volume: Volume):
|
||||
path = "/test-dir"
|
||||
|
||||
volume.make_dir(path)
|
||||
info = volume.get_info(path)
|
||||
|
||||
assert info.type.value == "directory"
|
||||
assert info.path == path
|
||||
|
||||
def test_create_nested_directories_with_force(self, volume: Volume):
|
||||
path = "/nested/deep/directory"
|
||||
|
||||
volume.make_dir(path, force=True)
|
||||
info = volume.get_info(path)
|
||||
|
||||
assert info.type.value == "directory"
|
||||
|
||||
def test_create_existing_directory_without_force_raises(self, volume: Volume):
|
||||
path = "/existing-dir"
|
||||
|
||||
volume.make_dir(path)
|
||||
with pytest.raises(VolumeException):
|
||||
volume.make_dir(path, force=False)
|
||||
|
||||
def test_create_directory_with_metadata(self, volume: Volume):
|
||||
path = "/dir-with-metadata"
|
||||
|
||||
volume.make_dir(path, uid=1000, gid=1000, mode=0o755)
|
||||
|
||||
info = volume.get_info(path)
|
||||
assert info.type.value == "directory"
|
||||
assert info.uid == 1000
|
||||
assert info.gid == 1000
|
||||
assert info.mode & 0o777 == 0o755
|
||||
|
||||
|
||||
class TestList:
|
||||
def test_list_directory_contents(self, volume: Volume):
|
||||
volume.write_file("/file1.txt", "Content 1")
|
||||
volume.write_file("/file2.txt", "Content 2")
|
||||
volume.make_dir("/dir1")
|
||||
|
||||
entries = 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
|
||||
|
||||
def test_list_nested_directory(self, volume: Volume):
|
||||
volume.make_dir("/nested", force=True)
|
||||
volume.write_file("/nested/file.txt", "Content")
|
||||
|
||||
entries = 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")
|
||||
def test_list_with_depth(self, volume: Volume):
|
||||
volume.make_dir("/deep/nested/structure", force=True)
|
||||
volume.write_file("/deep/nested/structure/file.txt", "Content")
|
||||
|
||||
entries = volume.list("/deep", depth=2)
|
||||
|
||||
assert len(entries) > 0
|
||||
|
||||
def test_list_nonexistent_raises(self, volume: Volume):
|
||||
with pytest.raises(NotFoundException):
|
||||
volume.list("/non-existent")
|
||||
|
||||
|
||||
class TestRemove:
|
||||
def test_remove_file(self, volume: Volume):
|
||||
path = "/to-remove.txt"
|
||||
volume.write_file(path, "Content")
|
||||
|
||||
volume.remove(path)
|
||||
|
||||
assert volume.exists(path) is False
|
||||
|
||||
def test_remove_directory(self, volume: Volume):
|
||||
path = "/to-remove-dir"
|
||||
volume.make_dir(path)
|
||||
|
||||
volume.remove(path)
|
||||
|
||||
assert volume.exists(path) is False
|
||||
|
||||
def test_remove_directory_recursively(self, volume: Volume):
|
||||
dir_path = "/recursive-dir"
|
||||
volume.make_dir(f"{dir_path}/nested", force=True)
|
||||
volume.write_file(f"{dir_path}/nested/file.txt", "Content")
|
||||
|
||||
volume.remove(dir_path)
|
||||
|
||||
assert volume.exists(dir_path) is False
|
||||
|
||||
def test_remove_nonexistent_raises(self, volume: Volume):
|
||||
with pytest.raises(NotFoundException):
|
||||
volume.remove("/non-existent.txt")
|
||||
|
||||
|
||||
class TestFileOperationsLifecycle:
|
||||
def test_directory_with_multiple_files(self, volume: Volume):
|
||||
dir_path = "/multi-file-dir"
|
||||
volume.make_dir(dir_path)
|
||||
|
||||
files = ["file1.txt", "file2.txt", "file3.txt"]
|
||||
for file_name in files:
|
||||
volume.write_file(f"{dir_path}/{file_name}", f"Content of {file_name}")
|
||||
|
||||
entries = volume.list(dir_path)
|
||||
assert len(entries) >= len(files)
|
||||
|
||||
for file_name in files:
|
||||
content = volume.read_file(f"{dir_path}/{file_name}", format="text")
|
||||
assert content == f"Content of {file_name}"
|
||||
|
||||
volume.remove(dir_path)
|
||||
assert volume.exists(dir_path) is False
|
||||
@@ -0,0 +1,175 @@
|
||||
from http import HTTPStatus
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
|
||||
from e2b import Volume
|
||||
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()
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
def mock_get_volumes(*, client):
|
||||
return Response(
|
||||
status_code=HTTPStatus(200),
|
||||
content=b"",
|
||||
headers={},
|
||||
parsed=list(_volumes.values()),
|
||||
)
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
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, "sync_detailed", mock_post_volumes)
|
||||
monkeypatch.setattr(get_volumes_mod, "sync_detailed", mock_get_volumes)
|
||||
monkeypatch.setattr(get_volume_mod, "sync_detailed", mock_get_volume)
|
||||
monkeypatch.setattr(delete_volume_mod, "sync_detailed", mock_delete_volume)
|
||||
|
||||
|
||||
def test_create_volume():
|
||||
vol = Volume.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
|
||||
|
||||
|
||||
def test_get_volume_info():
|
||||
created = Volume.create("info-volume")
|
||||
info = Volume.get_info(created.volume_id)
|
||||
|
||||
assert info.volume_id == created.volume_id
|
||||
assert info.name == "info-volume"
|
||||
assert info.token is not None
|
||||
|
||||
|
||||
def test_list_volumes():
|
||||
Volume.create("vol-a")
|
||||
Volume.create("vol-b")
|
||||
|
||||
volumes = Volume.list()
|
||||
|
||||
assert len(volumes) == 2
|
||||
names = sorted([v.name for v in volumes])
|
||||
assert names == ["vol-a", "vol-b"]
|
||||
|
||||
|
||||
def test_list_volumes_empty():
|
||||
volumes = Volume.list()
|
||||
assert len(volumes) == 0
|
||||
|
||||
|
||||
def test_destroy_volume():
|
||||
vol = Volume.create("to-delete")
|
||||
result = Volume.destroy(vol.volume_id)
|
||||
|
||||
assert result is True
|
||||
|
||||
volumes = Volume.list()
|
||||
assert len(volumes) == 0
|
||||
|
||||
|
||||
def test_destroy_nonexistent_volume():
|
||||
result = Volume.destroy("non-existent-id")
|
||||
assert result is False
|
||||
|
||||
|
||||
def test_get_info_nonexistent_volume():
|
||||
with pytest.raises(NotFoundException):
|
||||
Volume.get_info("non-existent-id")
|
||||
|
||||
|
||||
def test_create_volume_keeps_proxy_for_content_calls():
|
||||
vol = Volume.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"
|
||||
|
||||
|
||||
def test_volume_per_call_proxy_overrides_instance():
|
||||
vol = Volume.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"
|
||||
|
||||
|
||||
def test_volume_full_lifecycle():
|
||||
# Create
|
||||
vol = Volume.create("lifecycle-vol")
|
||||
assert vol.name == "lifecycle-vol"
|
||||
|
||||
# Get info
|
||||
info = Volume.get_info(vol.volume_id)
|
||||
assert info.name == "lifecycle-vol"
|
||||
|
||||
# List
|
||||
volumes = Volume.list()
|
||||
assert len(volumes) == 1
|
||||
assert volumes[0].volume_id == vol.volume_id
|
||||
|
||||
# Destroy
|
||||
destroyed = Volume.destroy(vol.volume_id)
|
||||
assert destroyed is True
|
||||
|
||||
# List again
|
||||
volumes = Volume.list()
|
||||
assert len(volumes) == 0
|
||||
@@ -0,0 +1,68 @@
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from e2b import Volume
|
||||
from e2b.exceptions import NotFoundException
|
||||
import e2b.volume.volume_sync as volume_sync_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) -> Volume:
|
||||
real_get_api_client = volume_sync_mod.get_volume_api_client
|
||||
|
||||
def mock_get_api_client(config, **kwargs):
|
||||
client = real_get_api_client(config, **kwargs)
|
||||
client.set_httpx_client(
|
||||
httpx.Client(
|
||||
base_url=config.api_url,
|
||||
transport=httpx.MockTransport(_handler),
|
||||
)
|
||||
)
|
||||
return client
|
||||
|
||||
monkeypatch.setattr(volume_sync_mod, "get_volume_api_client", mock_get_api_client)
|
||||
return Volume(volume_id="vol-1", name="test-volume", token="vol-token")
|
||||
|
||||
|
||||
def test_read_file_stream_yields_content(volume: Volume):
|
||||
stream = volume.read_file("hello.txt", format="stream")
|
||||
assert b"".join(stream) == b"hello world"
|
||||
|
||||
|
||||
def test_read_file_stream_raises_for_missing_path(volume: Volume):
|
||||
with pytest.raises(NotFoundException):
|
||||
for _ in volume.read_file("missing.txt", format="stream"):
|
||||
pass
|
||||
|
||||
|
||||
def test_read_file_stream_of_empty_file(volume: Volume):
|
||||
stream = volume.read_file("empty.txt", format="stream")
|
||||
assert b"".join(stream) == b""
|
||||
|
||||
|
||||
def test_read_file_text_and_bytes(volume: Volume):
|
||||
assert volume.read_file("hello.txt") == "hello world"
|
||||
assert volume.read_file("hello.txt", format="bytes") == b"hello world"
|
||||
|
||||
|
||||
def test_read_file_text_and_bytes_of_empty_file(volume: Volume):
|
||||
assert volume.read_file("empty.txt") == ""
|
||||
assert volume.read_file("empty.txt", format="bytes") == b""
|
||||
|
||||
|
||||
def test_read_file_missing_path_raises(volume: Volume):
|
||||
with pytest.raises(NotFoundException):
|
||||
volume.read_file("missing.txt")
|
||||
Reference in New Issue
Block a user