Skip to content

Commit b74cc13

Browse files
committed
unit test refactoring
1 parent 48c13e1 commit b74cc13

29 files changed

+2093
-1855
lines changed

.coverage

0 Bytes
Binary file not shown.

src/runloop_api_client/sdk/_sync.py

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from __future__ import annotations
22

3-
from typing import Any, Dict, Literal, Mapping, Iterable, Optional
3+
from typing import Dict, Literal, Mapping, Iterable, Optional
44
from pathlib import Path
55

66
import httpx
@@ -13,6 +13,7 @@
1313
from .blueprint import Blueprint
1414
from ..lib.polling import PollingConfig
1515
from .storage_object import StorageObject
16+
from ..types.blueprint_create_params import Service
1617
from ..types.shared_params.launch_parameters import LaunchParameters
1718
from ..types.shared_params.code_mount_parameters import CodeMountParameters
1819

@@ -263,27 +264,35 @@ def create(
263264
self,
264265
*,
265266
name: str,
266-
base_blueprint_id: Optional[str] | NotGiven = NOT_GIVEN,
267-
code_mounts: Optional[Iterable[CodeMountParameters]] | NotGiven = NOT_GIVEN,
268-
dockerfile: Optional[str] | NotGiven = NOT_GIVEN,
269-
file_mounts: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN,
270-
launch_parameters: Optional[LaunchParameters] | NotGiven = NOT_GIVEN,
271-
services: Optional[Iterable[Any]] | NotGiven = NOT_GIVEN,
272-
system_setup_commands: Optional[SequenceNotStr[str]] | NotGiven = NOT_GIVEN,
267+
base_blueprint_id: Optional[str] | Omit = omit,
268+
base_blueprint_name: Optional[str] | Omit = omit,
269+
build_args: Optional[Dict[str, str]] | Omit = omit,
270+
code_mounts: Optional[Iterable[CodeMountParameters]] | Omit = omit,
271+
dockerfile: Optional[str] | Omit = omit,
272+
file_mounts: Optional[Dict[str, str]] | Omit = omit,
273+
launch_parameters: Optional[LaunchParameters] | Omit = omit,
274+
metadata: Optional[Dict[str, str]] | Omit = omit,
275+
secrets: Optional[Dict[str, str]] | Omit = omit,
276+
services: Optional[Iterable[Service]] | Omit = omit,
277+
system_setup_commands: Optional[SequenceNotStr[str]] | Omit = omit,
273278
polling_config: PollingConfig | None = None,
274279
extra_headers: Headers | None = None,
275280
extra_query: Query | None = None,
276281
extra_body: Body | None = None,
277-
timeout: float | Timeout | None | NotGiven = not_given,
282+
timeout: float | Timeout | None | NotGiven = NOT_GIVEN,
278283
idempotency_key: str | None = None,
279284
) -> Blueprint:
280285
blueprint = self._client.blueprints.create_and_await_build_complete(
281286
name=name,
282287
base_blueprint_id=base_blueprint_id,
288+
base_blueprint_name=base_blueprint_name,
289+
build_args=build_args,
283290
code_mounts=code_mounts,
284291
dockerfile=dockerfile,
285292
file_mounts=file_mounts,
286293
launch_parameters=launch_parameters,
294+
metadata=metadata,
295+
secrets=secrets,
287296
services=services,
288297
system_setup_commands=system_setup_commands,
289298
polling_config=polling_config,

tests/sdk/async_devbox/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Tests for async Devbox functionality."""

tests/sdk/async_devbox/conftest.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"""Shared fixtures and utilities for async Devbox tests.
2+
3+
This module contains fixtures and helpers specific to async devbox testing
4+
that are shared across multiple test modules in this directory.
5+
"""
6+
# Currently minimal - add shared helpers if patterns emerge
Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
"""Tests for core AsyncDevbox functionality.
2+
3+
Tests the primary AsyncDevbox class including initialization, async CRUD
4+
operations, snapshot creation, blueprint launching, and async execution methods.
5+
"""
6+
7+
from __future__ import annotations
8+
9+
from types import SimpleNamespace
10+
from unittest.mock import AsyncMock
11+
12+
import pytest
13+
14+
from tests.sdk.conftest import MockDevboxView
15+
from runloop_api_client.sdk import AsyncDevbox
16+
from runloop_api_client._types import NotGiven
17+
from runloop_api_client.lib.polling import PollingConfig
18+
from runloop_api_client.sdk.async_devbox import (
19+
_AsyncFileInterface,
20+
_AsyncCommandInterface,
21+
_AsyncNetworkInterface,
22+
)
23+
24+
25+
class TestAsyncDevbox:
26+
"""Tests for AsyncDevbox class."""
27+
28+
def test_init(self, mock_async_client: AsyncMock) -> None:
29+
"""Test AsyncDevbox initialization."""
30+
devbox = AsyncDevbox(mock_async_client, "dev_123")
31+
assert devbox.id == "dev_123"
32+
33+
def test_repr(self, mock_async_client: AsyncMock) -> None:
34+
"""Test AsyncDevbox string representation."""
35+
devbox = AsyncDevbox(mock_async_client, "dev_123")
36+
assert repr(devbox) == "<AsyncDevbox id='dev_123'>"
37+
38+
@pytest.mark.asyncio
39+
async def test_context_manager_enter_exit(self, mock_async_client: AsyncMock, devbox_view: MockDevboxView) -> None:
40+
"""Test context manager behavior with successful shutdown."""
41+
mock_async_client.devboxes.shutdown = AsyncMock(return_value=devbox_view)
42+
43+
async with AsyncDevbox(mock_async_client, "dev_123") as devbox:
44+
assert devbox.id == "dev_123"
45+
46+
call_kwargs = mock_async_client.devboxes.shutdown.call_args[1]
47+
assert isinstance(call_kwargs["timeout"], NotGiven)
48+
49+
@pytest.mark.asyncio
50+
async def test_context_manager_exception_handling(self, mock_async_client: AsyncMock) -> None:
51+
"""Test context manager handles exceptions during shutdown."""
52+
mock_async_client.devboxes.shutdown = AsyncMock(side_effect=RuntimeError("Shutdown failed"))
53+
54+
with pytest.raises(ValueError, match="Test error"):
55+
async with AsyncDevbox(mock_async_client, "dev_123"):
56+
raise ValueError("Test error")
57+
58+
# Shutdown should be called even when body raises exception
59+
mock_async_client.devboxes.shutdown.assert_called_once()
60+
61+
@pytest.mark.asyncio
62+
async def test_get_info(self, mock_async_client: AsyncMock, devbox_view: MockDevboxView) -> None:
63+
"""Test get_info method."""
64+
mock_async_client.devboxes.retrieve = AsyncMock(return_value=devbox_view)
65+
66+
devbox = AsyncDevbox(mock_async_client, "dev_123")
67+
result = await devbox.get_info(
68+
extra_headers={"X-Custom": "value"},
69+
extra_query={"param": "value"},
70+
extra_body={"key": "value"},
71+
timeout=30.0,
72+
)
73+
74+
assert result == devbox_view
75+
mock_async_client.devboxes.retrieve.assert_called_once_with(
76+
"dev_123",
77+
extra_headers={"X-Custom": "value"},
78+
extra_query={"param": "value"},
79+
extra_body={"key": "value"},
80+
timeout=30.0,
81+
)
82+
83+
@pytest.mark.asyncio
84+
async def test_await_running(self, mock_async_client: AsyncMock, devbox_view: MockDevboxView) -> None:
85+
"""Test await_running method."""
86+
mock_async_client.devboxes.await_running = AsyncMock(return_value=devbox_view)
87+
polling_config = PollingConfig(timeout_seconds=60.0)
88+
89+
devbox = AsyncDevbox(mock_async_client, "dev_123")
90+
result = await devbox.await_running(polling_config=polling_config)
91+
92+
assert result == devbox_view
93+
mock_async_client.devboxes.await_running.assert_called_once_with(
94+
"dev_123",
95+
polling_config=polling_config,
96+
)
97+
98+
@pytest.mark.asyncio
99+
async def test_await_suspended(self, mock_async_client: AsyncMock, devbox_view: MockDevboxView) -> None:
100+
"""Test await_suspended method."""
101+
mock_async_client.devboxes.await_suspended = AsyncMock(return_value=devbox_view)
102+
polling_config = PollingConfig(timeout_seconds=60.0)
103+
104+
devbox = AsyncDevbox(mock_async_client, "dev_123")
105+
result = await devbox.await_suspended(polling_config=polling_config)
106+
107+
assert result == devbox_view
108+
mock_async_client.devboxes.await_suspended.assert_called_once_with(
109+
"dev_123",
110+
polling_config=polling_config,
111+
)
112+
113+
@pytest.mark.asyncio
114+
async def test_shutdown(self, mock_async_client: AsyncMock, devbox_view: MockDevboxView) -> None:
115+
"""Test shutdown method."""
116+
mock_async_client.devboxes.shutdown = AsyncMock(return_value=devbox_view)
117+
118+
devbox = AsyncDevbox(mock_async_client, "dev_123")
119+
result = await devbox.shutdown(
120+
extra_headers={"X-Custom": "value"},
121+
extra_query={"param": "value"},
122+
extra_body={"key": "value"},
123+
timeout=30.0,
124+
idempotency_key="key-123",
125+
)
126+
127+
assert result == devbox_view
128+
mock_async_client.devboxes.shutdown.assert_called_once_with(
129+
"dev_123",
130+
extra_headers={"X-Custom": "value"},
131+
extra_query={"param": "value"},
132+
extra_body={"key": "value"},
133+
timeout=30.0,
134+
idempotency_key="key-123",
135+
)
136+
137+
@pytest.mark.asyncio
138+
async def test_suspend(self, mock_async_client: AsyncMock, devbox_view: MockDevboxView) -> None:
139+
"""Test suspend method."""
140+
mock_async_client.devboxes.suspend = AsyncMock(return_value=None)
141+
mock_async_client.devboxes.await_suspended = AsyncMock(return_value=devbox_view)
142+
polling_config = PollingConfig(timeout_seconds=60.0)
143+
144+
devbox = AsyncDevbox(mock_async_client, "dev_123")
145+
result = await devbox.suspend(
146+
polling_config=polling_config,
147+
extra_headers={"X-Custom": "value"},
148+
extra_query={"param": "value"},
149+
extra_body={"key": "value"},
150+
timeout=30.0,
151+
idempotency_key="key-123",
152+
)
153+
154+
assert result == devbox_view
155+
mock_async_client.devboxes.suspend.assert_called_once_with(
156+
"dev_123",
157+
extra_headers={"X-Custom": "value"},
158+
extra_query={"param": "value"},
159+
extra_body={"key": "value"},
160+
timeout=30.0,
161+
idempotency_key="key-123",
162+
)
163+
mock_async_client.devboxes.await_suspended.assert_called_once_with(
164+
"dev_123",
165+
polling_config=polling_config,
166+
)
167+
168+
@pytest.mark.asyncio
169+
async def test_resume(self, mock_async_client: AsyncMock, devbox_view: MockDevboxView) -> None:
170+
"""Test resume method."""
171+
mock_async_client.devboxes.resume = AsyncMock(return_value=None)
172+
mock_async_client.devboxes.await_running = AsyncMock(return_value=devbox_view)
173+
polling_config = PollingConfig(timeout_seconds=60.0)
174+
175+
devbox = AsyncDevbox(mock_async_client, "dev_123")
176+
result = await devbox.resume(
177+
polling_config=polling_config,
178+
extra_headers={"X-Custom": "value"},
179+
extra_query={"param": "value"},
180+
extra_body={"key": "value"},
181+
timeout=30.0,
182+
idempotency_key="key-123",
183+
)
184+
185+
assert result == devbox_view
186+
mock_async_client.devboxes.resume.assert_called_once_with(
187+
"dev_123",
188+
extra_headers={"X-Custom": "value"},
189+
extra_query={"param": "value"},
190+
extra_body={"key": "value"},
191+
timeout=30.0,
192+
idempotency_key="key-123",
193+
)
194+
mock_async_client.devboxes.await_running.assert_called_once_with(
195+
"dev_123",
196+
polling_config=polling_config,
197+
)
198+
199+
@pytest.mark.asyncio
200+
async def test_keep_alive(self, mock_async_client: AsyncMock) -> None:
201+
"""Test keep_alive method."""
202+
mock_async_client.devboxes.keep_alive = AsyncMock(return_value=object())
203+
204+
devbox = AsyncDevbox(mock_async_client, "dev_123")
205+
result = await devbox.keep_alive(
206+
extra_headers={"X-Custom": "value"},
207+
extra_query={"param": "value"},
208+
extra_body={"key": "value"},
209+
timeout=30.0,
210+
idempotency_key="key-123",
211+
)
212+
213+
assert result is not None # Verify return value is propagated
214+
mock_async_client.devboxes.keep_alive.assert_called_once_with(
215+
"dev_123",
216+
extra_headers={"X-Custom": "value"},
217+
extra_query={"param": "value"},
218+
extra_body={"key": "value"},
219+
timeout=30.0,
220+
idempotency_key="key-123",
221+
)
222+
223+
@pytest.mark.asyncio
224+
async def test_snapshot_disk(self, mock_async_client: AsyncMock) -> None:
225+
"""Test snapshot_disk waits for completion."""
226+
snapshot_data = SimpleNamespace(id="snap_123")
227+
snapshot_status = SimpleNamespace(status="completed")
228+
229+
mock_async_client.devboxes.snapshot_disk_async = AsyncMock(return_value=snapshot_data)
230+
mock_async_client.devboxes.disk_snapshots.await_completed = AsyncMock(return_value=snapshot_status)
231+
232+
devbox = AsyncDevbox(mock_async_client, "dev_123")
233+
polling_config = PollingConfig(timeout_seconds=60.0)
234+
snapshot = await devbox.snapshot_disk(
235+
name="test-snapshot",
236+
metadata={"key": "value"},
237+
polling_config=polling_config,
238+
extra_headers={"X-Custom": "value"},
239+
)
240+
241+
assert snapshot.id == "snap_123"
242+
mock_async_client.devboxes.snapshot_disk_async.assert_called_once()
243+
mock_async_client.devboxes.disk_snapshots.await_completed.assert_called_once()
244+
245+
@pytest.mark.asyncio
246+
async def test_snapshot_disk_async(self, mock_async_client: AsyncMock) -> None:
247+
"""Test snapshot_disk_async returns immediately."""
248+
snapshot_data = SimpleNamespace(id="snap_123")
249+
mock_async_client.devboxes.snapshot_disk_async = AsyncMock(return_value=snapshot_data)
250+
251+
devbox = AsyncDevbox(mock_async_client, "dev_123")
252+
snapshot = await devbox.snapshot_disk_async(
253+
name="test-snapshot",
254+
metadata={"key": "value"},
255+
extra_headers={"X-Custom": "value"},
256+
)
257+
258+
assert snapshot.id == "snap_123"
259+
mock_async_client.devboxes.snapshot_disk_async.assert_called_once()
260+
261+
@pytest.mark.asyncio
262+
async def test_close(self, mock_async_client: AsyncMock, devbox_view: MockDevboxView) -> None:
263+
"""Test close method calls shutdown."""
264+
mock_async_client.devboxes.shutdown = AsyncMock(return_value=devbox_view)
265+
266+
devbox = AsyncDevbox(mock_async_client, "dev_123")
267+
await devbox.close()
268+
269+
mock_async_client.devboxes.shutdown.assert_called_once()
270+
271+
def test_cmd_property(self, mock_async_client: AsyncMock) -> None:
272+
"""Test cmd property returns AsyncCommandInterface."""
273+
devbox = AsyncDevbox(mock_async_client, "dev_123")
274+
cmd = devbox.cmd
275+
assert isinstance(cmd, _AsyncCommandInterface)
276+
assert cmd._devbox is devbox
277+
278+
def test_file_property(self, mock_async_client: AsyncMock) -> None:
279+
"""Test file property returns AsyncFileInterface."""
280+
devbox = AsyncDevbox(mock_async_client, "dev_123")
281+
file_interface = devbox.file
282+
assert isinstance(file_interface, _AsyncFileInterface)
283+
assert file_interface._devbox is devbox
284+
285+
def test_net_property(self, mock_async_client: AsyncMock) -> None:
286+
"""Test net property returns AsyncNetworkInterface."""
287+
devbox = AsyncDevbox(mock_async_client, "dev_123")
288+
net = devbox.net
289+
assert isinstance(net, _AsyncNetworkInterface)
290+
assert net._devbox is devbox
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
"""Tests for AsyncDevbox error handling.
2+
3+
Tests async error scenarios including network errors.
4+
"""
5+
6+
from __future__ import annotations
7+
8+
from unittest.mock import AsyncMock
9+
10+
import httpx
11+
import pytest
12+
13+
from runloop_api_client.sdk import AsyncDevbox
14+
15+
16+
class TestAsyncDevboxErrorHandling:
17+
"""Tests for AsyncDevbox error handling scenarios."""
18+
19+
@pytest.mark.asyncio
20+
async def test_async_network_error(self, mock_async_client: AsyncMock) -> None:
21+
"""Test handling of network errors in async."""
22+
mock_async_client.devboxes.retrieve = AsyncMock(side_effect=httpx.NetworkError("Connection failed"))
23+
24+
devbox = AsyncDevbox(mock_async_client, "dev_123")
25+
with pytest.raises(httpx.NetworkError):
26+
await devbox.get_info()

0 commit comments

Comments
 (0)