Skip to content

Commit ca04975

Browse files
committed
add tests
1 parent 70fbe38 commit ca04975

File tree

6 files changed

+376
-10
lines changed

6 files changed

+376
-10
lines changed

src/runloop_api_client/sdk/async_.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -368,8 +368,8 @@ async def upload_from_file(
368368
async def upload_from_dir(
369369
self,
370370
dir_path: str | Path,
371-
name: str | None = None,
372371
*,
372+
name: str | None = None,
373373
metadata: Optional[Dict[str, str]] = None,
374374
ttl: timedelta | None = None,
375375
**options: Unpack[LongRequestOptions],
@@ -395,17 +395,17 @@ async def upload_from_dir(
395395
name = name or f"{path.name}.tar.gz"
396396
ttl_ms = int(ttl.total_seconds()) * 1000 if ttl else None
397397

398-
def synchronous_io() -> io.BytesIO:
399-
tar_buffer = io.BytesIO()
400-
with tarfile.open(fileobj=tar_buffer, mode="w:gz") as tar:
401-
tar.add(path, arcname=".", recursive=True)
402-
tar_buffer.seek(0)
403-
return tar_buffer
398+
def synchronous_io() -> bytes:
399+
with io.BytesIO() as tar_buffer:
400+
with tarfile.open(fileobj=tar_buffer, mode="w:gz") as tar:
401+
tar.add(path, arcname=".", recursive=True)
402+
tar_buffer.seek(0)
403+
return tar_buffer.read()
404404

405-
tar_buffer = await asyncio.to_thread(synchronous_io)
405+
tar_bytes = await asyncio.to_thread(synchronous_io)
406406

407407
obj = await self.create(name=name, content_type="tgz", metadata=metadata, ttl_ms=ttl_ms, **options)
408-
await obj.upload_content(tar_buffer)
408+
await obj.upload_content(tar_bytes)
409409
await obj.complete()
410410
return obj
411411

src/runloop_api_client/sdk/sync.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -367,8 +367,8 @@ def upload_from_file(
367367
def upload_from_dir(
368368
self,
369369
dir_path: str | Path,
370-
name: str | None = None,
371370
*,
371+
name: str | None = None,
372372
metadata: Optional[Dict[str, str]] = None,
373373
ttl: timedelta | None = None,
374374
**options: Unpack[LongRequestOptions],

tests/sdk/test_async_clients.py

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
from __future__ import annotations
44

5+
import io
6+
import tarfile
57
from types import SimpleNamespace
68
from pathlib import Path
79
from unittest.mock import AsyncMock
@@ -327,6 +329,161 @@ async def test_upload_from_file_missing_path(self, mock_async_client: AsyncMock,
327329
with pytest.raises(OSError, match="Failed to read file"):
328330
await client.upload_from_file(missing_file)
329331

332+
@pytest.mark.asyncio
333+
async def test_upload_from_dir(
334+
self, mock_async_client: AsyncMock, object_view: MockObjectView, tmp_path: Path
335+
) -> None:
336+
"""Test upload_from_dir method."""
337+
mock_async_client.objects.create = AsyncMock(return_value=object_view)
338+
mock_async_client.objects.complete = AsyncMock(return_value=object_view)
339+
340+
# Create a temporary directory with some files
341+
test_dir = tmp_path / "test_directory"
342+
test_dir.mkdir()
343+
(test_dir / "file1.txt").write_text("content1")
344+
(test_dir / "file2.txt").write_text("content2")
345+
subdir = test_dir / "subdir"
346+
subdir.mkdir()
347+
(subdir / "file3.txt").write_text("content3")
348+
349+
http_client = AsyncMock()
350+
mock_response = create_mock_httpx_response()
351+
http_client.put = AsyncMock(return_value=mock_response)
352+
mock_async_client._client = http_client
353+
354+
client = AsyncStorageObjectOps(mock_async_client)
355+
obj = await client.upload_from_dir(test_dir, name="archive.tar.gz", metadata={"key": "value"})
356+
357+
assert isinstance(obj, AsyncStorageObject)
358+
assert obj.id == "obj_123"
359+
mock_async_client.objects.create.assert_awaited_once_with(
360+
name="archive.tar.gz",
361+
content_type="tgz",
362+
metadata={"key": "value"},
363+
ttl_ms=None,
364+
)
365+
# Verify that put was called with tarball content
366+
http_client.put.assert_awaited_once()
367+
call_args = http_client.put.call_args
368+
assert call_args[0][0] == object_view.upload_url
369+
370+
# Verify it's a valid gzipped tarball
371+
uploaded_content = call_args[1]["content"]
372+
with tarfile.open(fileobj=io.BytesIO(uploaded_content), mode="r:gz") as tar:
373+
members = tar.getmembers()
374+
member_names = [m.name for m in members]
375+
# Should contain our test files (may include directory entries)
376+
assert any("file1.txt" in name for name in member_names)
377+
assert any("file2.txt" in name for name in member_names)
378+
assert any("file3.txt" in name for name in member_names)
379+
380+
mock_async_client.objects.complete.assert_awaited_once()
381+
382+
@pytest.mark.asyncio
383+
async def test_upload_from_dir_default_name(
384+
self, mock_async_client: AsyncMock, object_view: MockObjectView, tmp_path: Path
385+
) -> None:
386+
"""Test upload_from_dir uses directory name by default."""
387+
mock_async_client.objects.create = AsyncMock(return_value=object_view)
388+
mock_async_client.objects.complete = AsyncMock(return_value=object_view)
389+
390+
test_dir = tmp_path / "my_folder"
391+
test_dir.mkdir()
392+
(test_dir / "file.txt").write_text("content")
393+
394+
http_client = AsyncMock()
395+
mock_response = create_mock_httpx_response()
396+
http_client.put = AsyncMock(return_value=mock_response)
397+
mock_async_client._client = http_client
398+
399+
client = AsyncStorageObjectOps(mock_async_client)
400+
obj = await client.upload_from_dir(test_dir)
401+
402+
assert isinstance(obj, AsyncStorageObject)
403+
# Name should be directory name + .tar.gz
404+
mock_async_client.objects.create.assert_awaited_once()
405+
call_args = mock_async_client.objects.create.call_args
406+
assert call_args[1]["name"] == "my_folder.tar.gz"
407+
assert call_args[1]["content_type"] == "tgz"
408+
409+
@pytest.mark.asyncio
410+
async def test_upload_from_dir_with_ttl(
411+
self, mock_async_client: AsyncMock, object_view: MockObjectView, tmp_path: Path
412+
) -> None:
413+
"""Test upload_from_dir with TTL."""
414+
from datetime import timedelta
415+
416+
mock_async_client.objects.create = AsyncMock(return_value=object_view)
417+
mock_async_client.objects.complete = AsyncMock(return_value=object_view)
418+
419+
test_dir = tmp_path / "temp_dir"
420+
test_dir.mkdir()
421+
(test_dir / "file.txt").write_text("temporary content")
422+
423+
http_client = AsyncMock()
424+
mock_response = create_mock_httpx_response()
425+
http_client.put = AsyncMock(return_value=mock_response)
426+
mock_async_client._client = http_client
427+
428+
client = AsyncStorageObjectOps(mock_async_client)
429+
obj = await client.upload_from_dir(test_dir, ttl=timedelta(hours=2))
430+
431+
assert isinstance(obj, AsyncStorageObject)
432+
mock_async_client.objects.create.assert_awaited_once()
433+
call_args = mock_async_client.objects.create.call_args
434+
# 2 hours = 7200 seconds = 7200000 milliseconds
435+
assert call_args[1]["ttl_ms"] == 7200000
436+
437+
@pytest.mark.asyncio
438+
async def test_upload_from_dir_empty_directory(
439+
self, mock_async_client: AsyncMock, object_view: MockObjectView, tmp_path: Path
440+
) -> None:
441+
"""Test upload_from_dir with empty directory."""
442+
mock_async_client.objects.create = AsyncMock(return_value=object_view)
443+
mock_async_client.objects.complete = AsyncMock(return_value=object_view)
444+
445+
test_dir = tmp_path / "empty_dir"
446+
test_dir.mkdir()
447+
448+
http_client = AsyncMock()
449+
mock_response = create_mock_httpx_response()
450+
http_client.put = AsyncMock(return_value=mock_response)
451+
mock_async_client._client = http_client
452+
453+
client = AsyncStorageObjectOps(mock_async_client)
454+
obj = await client.upload_from_dir(test_dir)
455+
456+
assert isinstance(obj, AsyncStorageObject)
457+
assert obj.id == "obj_123"
458+
mock_async_client.objects.create.assert_awaited_once()
459+
http_client.put.assert_awaited_once()
460+
mock_async_client.objects.complete.assert_awaited_once()
461+
462+
@pytest.mark.asyncio
463+
async def test_upload_from_dir_with_string_path(
464+
self, mock_async_client: AsyncMock, object_view: MockObjectView, tmp_path: Path
465+
) -> None:
466+
"""Test upload_from_dir with string path instead of Path object."""
467+
mock_async_client.objects.create = AsyncMock(return_value=object_view)
468+
mock_async_client.objects.complete = AsyncMock(return_value=object_view)
469+
470+
test_dir = tmp_path / "string_path_dir"
471+
test_dir.mkdir()
472+
(test_dir / "file.txt").write_text("content")
473+
474+
http_client = AsyncMock()
475+
mock_response = create_mock_httpx_response()
476+
http_client.put = AsyncMock(return_value=mock_response)
477+
mock_async_client._client = http_client
478+
479+
client = AsyncStorageObjectOps(mock_async_client)
480+
# Pass string path instead of Path object
481+
obj = await client.upload_from_dir(str(test_dir))
482+
483+
assert isinstance(obj, AsyncStorageObject)
484+
assert obj.id == "obj_123"
485+
mock_async_client.objects.create.assert_awaited_once()
486+
330487

331488
class TestAsyncRunloopSDK:
332489
"""Tests for AsyncRunloopSDK class."""

tests/sdk/test_clients.py

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,137 @@ def test_upload_from_file_missing_path(self, mock_client: Mock, tmp_path: Path)
306306
with pytest.raises(OSError, match="Failed to read file"):
307307
client.upload_from_file(missing_file)
308308

309+
def test_upload_from_dir(self, mock_client: Mock, object_view: MockObjectView, tmp_path: Path) -> None:
310+
"""Test upload_from_dir method."""
311+
mock_client.objects.create.return_value = object_view
312+
313+
# Create a temporary directory with some files
314+
test_dir = tmp_path / "test_directory"
315+
test_dir.mkdir()
316+
(test_dir / "file1.txt").write_text("content1")
317+
(test_dir / "file2.txt").write_text("content2")
318+
subdir = test_dir / "subdir"
319+
subdir.mkdir()
320+
(subdir / "file3.txt").write_text("content3")
321+
322+
http_client = Mock()
323+
mock_response = create_mock_httpx_response()
324+
http_client.put.return_value = mock_response
325+
mock_client._client = http_client
326+
327+
client = StorageObjectOps(mock_client)
328+
obj = client.upload_from_dir(test_dir, name="archive.tar.gz", metadata={"key": "value"})
329+
330+
assert isinstance(obj, StorageObject)
331+
assert obj.id == "obj_123"
332+
mock_client.objects.create.assert_called_once_with(
333+
name="archive.tar.gz",
334+
content_type="tgz",
335+
metadata={"key": "value"},
336+
ttl_ms=None,
337+
)
338+
# Verify that put was called with tarball content
339+
http_client.put.assert_called_once()
340+
call_args = http_client.put.call_args
341+
assert call_args[0][0] == object_view.upload_url
342+
# Verify it's a BytesIO object
343+
uploaded_content = call_args[1]["content"]
344+
assert hasattr(uploaded_content, "read")
345+
mock_client.objects.complete.assert_called_once()
346+
347+
def test_upload_from_dir_default_name(self, mock_client: Mock, object_view: MockObjectView, tmp_path: Path) -> None:
348+
"""Test upload_from_dir uses directory name by default."""
349+
mock_client.objects.create.return_value = object_view
350+
351+
test_dir = tmp_path / "my_folder"
352+
test_dir.mkdir()
353+
(test_dir / "file.txt").write_text("content")
354+
355+
http_client = Mock()
356+
mock_response = create_mock_httpx_response()
357+
http_client.put.return_value = mock_response
358+
mock_client._client = http_client
359+
360+
client = StorageObjectOps(mock_client)
361+
obj = client.upload_from_dir(test_dir)
362+
363+
assert isinstance(obj, StorageObject)
364+
# Name should be directory name + .tar.gz
365+
mock_client.objects.create.assert_called_once()
366+
call_args = mock_client.objects.create.call_args
367+
assert call_args[1]["name"] == "my_folder.tar.gz"
368+
assert call_args[1]["content_type"] == "tgz"
369+
370+
def test_upload_from_dir_with_ttl(self, mock_client: Mock, object_view: MockObjectView, tmp_path: Path) -> None:
371+
"""Test upload_from_dir with TTL."""
372+
from datetime import timedelta
373+
374+
mock_client.objects.create.return_value = object_view
375+
376+
test_dir = tmp_path / "temp_dir"
377+
test_dir.mkdir()
378+
(test_dir / "file.txt").write_text("temporary content")
379+
380+
http_client = Mock()
381+
mock_response = create_mock_httpx_response()
382+
http_client.put.return_value = mock_response
383+
mock_client._client = http_client
384+
385+
client = StorageObjectOps(mock_client)
386+
obj = client.upload_from_dir(test_dir, ttl=timedelta(hours=2))
387+
388+
assert isinstance(obj, StorageObject)
389+
mock_client.objects.create.assert_called_once()
390+
call_args = mock_client.objects.create.call_args
391+
# 2 hours = 7200 seconds = 7200000 milliseconds
392+
assert call_args[1]["ttl_ms"] == 7200000
393+
394+
def test_upload_from_dir_empty_directory(
395+
self, mock_client: Mock, object_view: MockObjectView, tmp_path: Path
396+
) -> None:
397+
"""Test upload_from_dir with empty directory."""
398+
mock_client.objects.create.return_value = object_view
399+
400+
test_dir = tmp_path / "empty_dir"
401+
test_dir.mkdir()
402+
403+
http_client = Mock()
404+
mock_response = create_mock_httpx_response()
405+
http_client.put.return_value = mock_response
406+
mock_client._client = http_client
407+
408+
client = StorageObjectOps(mock_client)
409+
obj = client.upload_from_dir(test_dir)
410+
411+
assert isinstance(obj, StorageObject)
412+
assert obj.id == "obj_123"
413+
mock_client.objects.create.assert_called_once()
414+
http_client.put.assert_called_once()
415+
mock_client.objects.complete.assert_called_once()
416+
417+
def test_upload_from_dir_with_string_path(
418+
self, mock_client: Mock, object_view: MockObjectView, tmp_path: Path
419+
) -> None:
420+
"""Test upload_from_dir with string path instead of Path object."""
421+
mock_client.objects.create.return_value = object_view
422+
423+
test_dir = tmp_path / "string_path_dir"
424+
test_dir.mkdir()
425+
(test_dir / "file.txt").write_text("content")
426+
427+
http_client = Mock()
428+
mock_response = create_mock_httpx_response()
429+
http_client.put.return_value = mock_response
430+
mock_client._client = http_client
431+
432+
client = StorageObjectOps(mock_client)
433+
# Pass string path instead of Path object
434+
obj = client.upload_from_dir(str(test_dir))
435+
436+
assert isinstance(obj, StorageObject)
437+
assert obj.id == "obj_123"
438+
mock_client.objects.create.assert_called_once()
439+
309440

310441
class TestRunloopSDK:
311442
"""Tests for RunloopSDK class."""

0 commit comments

Comments
 (0)