Skip to content

Commit 145f66c

Browse files
committed
Write more tests for long polling functionality
1 parent 3586007 commit 145f66c

3 files changed

Lines changed: 878 additions & 0 deletions

File tree

tests/api_resources/devboxes/test_executions.py

Lines changed: 335 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@
44

55
import os
66
from typing import Any, cast
7+
from unittest.mock import Mock, patch
78

89
import pytest
910

1011
from tests.utils import assert_matches_type
1112
from runloop_api_client import Runloop, AsyncRunloop
1213
from runloop_api_client.types import DevboxExecutionDetailView, DevboxAsyncExecutionDetailView
14+
from runloop_api_client._exceptions import APIStatusError
15+
from runloop_api_client.lib.polling import PollingConfig, PollingTimeout
1316

1417
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
1518

@@ -224,6 +227,172 @@ def test_path_params_kill(self, client: Runloop) -> None:
224227
devbox_id="devbox_id",
225228
)
226229

230+
# Polling method tests
231+
@parametrize
232+
def test_method_await_completed_success(self, client: Runloop) -> None:
233+
"""Test await_completed with successful polling to completed state"""
234+
235+
# Mock the wait_for_status calls - first returns running, then completed
236+
mock_execution_running = DevboxAsyncExecutionDetailView(
237+
devbox_id="devbox_id",
238+
execution_id="execution_id",
239+
status="running",
240+
stdout="Starting...",
241+
stderr="",
242+
)
243+
244+
mock_execution_completed = DevboxAsyncExecutionDetailView(
245+
devbox_id="devbox_id",
246+
execution_id="execution_id",
247+
status="completed",
248+
stdout="Starting...\nFinished!",
249+
stderr="",
250+
)
251+
252+
with patch.object(client.devboxes.executions, '_post') as mock_post:
253+
mock_post.side_effect = [mock_execution_running, mock_execution_completed]
254+
255+
result = client.devboxes.executions.await_completed("execution_id", "devbox_id")
256+
257+
assert result.execution_id == "execution_id"
258+
assert result.status == "completed"
259+
assert mock_post.call_count == 2
260+
261+
@parametrize
262+
def test_method_await_completed_immediate_success(self, client: Runloop) -> None:
263+
"""Test await_completed when execution is already completed"""
264+
265+
mock_execution_completed = DevboxAsyncExecutionDetailView(
266+
devbox_id="devbox_id",
267+
execution_id="execution_id",
268+
status="completed",
269+
stdout="Already finished!",
270+
stderr="",
271+
)
272+
273+
with patch.object(client.devboxes.executions, '_post') as mock_post:
274+
mock_post.return_value = mock_execution_completed
275+
276+
result = client.devboxes.executions.await_completed("execution_id", "devbox_id")
277+
278+
assert result.execution_id == "execution_id"
279+
assert result.status == "completed"
280+
assert mock_post.call_count == 1
281+
282+
@parametrize
283+
def test_method_await_completed_timeout_handling(self, client: Runloop) -> None:
284+
"""Test await_completed handles 408 timeouts correctly"""
285+
286+
# Create a mock 408 response
287+
mock_response = Mock()
288+
mock_response.status_code = 408
289+
mock_408_error = APIStatusError("Request timeout", response=mock_response, body=None)
290+
291+
mock_execution_completed = DevboxAsyncExecutionDetailView(
292+
devbox_id="devbox_id",
293+
execution_id="execution_id",
294+
status="completed",
295+
stdout="Finished after timeout!",
296+
stderr="",
297+
)
298+
299+
with patch.object(client.devboxes.executions, '_post') as mock_post:
300+
# First call raises 408, second call succeeds
301+
mock_post.side_effect = [mock_408_error, mock_execution_completed]
302+
303+
result = client.devboxes.executions.await_completed("execution_id", "devbox_id")
304+
305+
assert result.execution_id == "execution_id"
306+
assert result.status == "completed"
307+
assert mock_post.call_count == 2
308+
309+
@parametrize
310+
def test_method_await_completed_other_error(self, client: Runloop) -> None:
311+
"""Test await_completed re-raises non-408 errors"""
312+
313+
# Create a mock 500 response
314+
mock_response = Mock()
315+
mock_response.status_code = 500
316+
mock_500_error = APIStatusError("Internal server error", response=mock_response, body=None)
317+
318+
with patch.object(client.devboxes.executions, '_post') as mock_post:
319+
mock_post.side_effect = mock_500_error
320+
321+
with pytest.raises(APIStatusError, match="Internal server error"):
322+
client.devboxes.executions.await_completed("execution_id", "devbox_id")
323+
324+
@parametrize
325+
def test_method_await_completed_with_config(self, client: Runloop) -> None:
326+
"""Test await_completed with custom polling configuration"""
327+
328+
mock_execution_completed = DevboxAsyncExecutionDetailView(
329+
devbox_id="devbox_id",
330+
execution_id="execution_id",
331+
status="completed",
332+
stdout="Finished with config!",
333+
stderr="",
334+
)
335+
336+
config = PollingConfig(interval_seconds=0.1, max_attempts=10)
337+
338+
with patch.object(client.devboxes.executions, '_post') as mock_post:
339+
mock_post.return_value = mock_execution_completed
340+
341+
result = client.devboxes.executions.await_completed("execution_id", "devbox_id", config=config)
342+
343+
assert result.execution_id == "execution_id"
344+
assert result.status == "completed"
345+
346+
@parametrize
347+
def test_method_await_completed_polling_timeout(self, client: Runloop) -> None:
348+
"""Test await_completed raises PollingTimeout when max attempts exceeded"""
349+
350+
mock_execution_running = DevboxAsyncExecutionDetailView(
351+
devbox_id="devbox_id",
352+
execution_id="execution_id",
353+
status="running",
354+
stdout="Still running...",
355+
stderr="",
356+
)
357+
358+
config = PollingConfig(interval_seconds=0.01, max_attempts=2)
359+
360+
with patch.object(client.devboxes.executions, '_post') as mock_post:
361+
mock_post.return_value = mock_execution_running
362+
363+
with pytest.raises(PollingTimeout):
364+
client.devboxes.executions.await_completed("execution_id", "devbox_id", config=config)
365+
366+
@parametrize
367+
def test_method_await_completed_various_statuses(self, client: Runloop) -> None:
368+
"""Test await_completed correctly handles different execution statuses"""
369+
370+
# Test with queued status first
371+
mock_execution_queued = DevboxAsyncExecutionDetailView(
372+
devbox_id="devbox_id",
373+
execution_id="execution_id",
374+
status="queued",
375+
stdout="",
376+
stderr="",
377+
)
378+
379+
mock_execution_completed = DevboxAsyncExecutionDetailView(
380+
devbox_id="devbox_id",
381+
execution_id="execution_id",
382+
status="completed",
383+
stdout="Done!",
384+
stderr="",
385+
)
386+
387+
with patch.object(client.devboxes.executions, '_post') as mock_post:
388+
mock_post.side_effect = [mock_execution_queued, mock_execution_completed]
389+
390+
result = client.devboxes.executions.await_completed("execution_id", "devbox_id")
391+
392+
assert result.execution_id == "execution_id"
393+
assert result.status == "completed"
394+
assert mock_post.call_count == 2
395+
227396

228397
class TestAsyncExecutions:
229398
parametrize = pytest.mark.parametrize(
@@ -436,3 +605,169 @@ async def test_path_params_kill(self, async_client: AsyncRunloop) -> None:
436605
execution_id="",
437606
devbox_id="devbox_id",
438607
)
608+
609+
# Async polling method tests
610+
@parametrize
611+
async def test_method_await_completed_success(self, async_client: AsyncRunloop) -> None:
612+
"""Test await_completed with successful polling to completed state"""
613+
614+
# Mock the wait_for_status calls - first returns running, then completed
615+
mock_execution_running = DevboxAsyncExecutionDetailView(
616+
devbox_id="devbox_id",
617+
execution_id="execution_id",
618+
status="running",
619+
stdout="Starting...",
620+
stderr="",
621+
)
622+
623+
mock_execution_completed = DevboxAsyncExecutionDetailView(
624+
devbox_id="devbox_id",
625+
execution_id="execution_id",
626+
status="completed",
627+
stdout="Starting...\nFinished!",
628+
stderr="",
629+
)
630+
631+
with patch.object(async_client.devboxes.executions, '_post') as mock_post:
632+
mock_post.side_effect = [mock_execution_running, mock_execution_completed]
633+
634+
result = await async_client.devboxes.executions.await_completed("execution_id", devbox_id="devbox_id")
635+
636+
assert result.execution_id == "execution_id"
637+
assert result.status == "completed"
638+
assert mock_post.call_count == 2
639+
640+
@parametrize
641+
async def test_method_await_completed_immediate_success(self, async_client: AsyncRunloop) -> None:
642+
"""Test await_completed when execution is already completed"""
643+
644+
mock_execution_completed = DevboxAsyncExecutionDetailView(
645+
devbox_id="devbox_id",
646+
execution_id="execution_id",
647+
status="completed",
648+
stdout="Already finished!",
649+
stderr="",
650+
)
651+
652+
with patch.object(async_client.devboxes.executions, '_post') as mock_post:
653+
mock_post.return_value = mock_execution_completed
654+
655+
result = await async_client.devboxes.executions.await_completed("execution_id", devbox_id="devbox_id")
656+
657+
assert result.execution_id == "execution_id"
658+
assert result.status == "completed"
659+
assert mock_post.call_count == 1
660+
661+
@parametrize
662+
async def test_method_await_completed_timeout_handling(self, async_client: AsyncRunloop) -> None:
663+
"""Test await_completed handles 408 timeouts correctly"""
664+
665+
# Create a mock 408 response
666+
mock_response = Mock()
667+
mock_response.status_code = 408
668+
mock_408_error = APIStatusError("Request timeout", response=mock_response, body=None)
669+
670+
mock_execution_completed = DevboxAsyncExecutionDetailView(
671+
devbox_id="devbox_id",
672+
execution_id="execution_id",
673+
status="completed",
674+
stdout="Finished after timeout!",
675+
stderr="",
676+
)
677+
678+
with patch.object(async_client.devboxes.executions, '_post') as mock_post:
679+
# First call raises 408, second call succeeds
680+
mock_post.side_effect = [mock_408_error, mock_execution_completed]
681+
682+
result = await async_client.devboxes.executions.await_completed("execution_id", devbox_id="devbox_id")
683+
684+
assert result.execution_id == "execution_id"
685+
assert result.status == "completed"
686+
assert mock_post.call_count == 2
687+
688+
@parametrize
689+
async def test_method_await_completed_other_error(self, async_client: AsyncRunloop) -> None:
690+
"""Test await_completed re-raises non-408 errors"""
691+
692+
# Create a mock 500 response
693+
mock_response = Mock()
694+
mock_response.status_code = 500
695+
mock_500_error = APIStatusError("Internal server error", response=mock_response, body=None)
696+
697+
with patch.object(async_client.devboxes.executions, '_post') as mock_post:
698+
mock_post.side_effect = mock_500_error
699+
700+
with pytest.raises(APIStatusError, match="Internal server error"):
701+
await async_client.devboxes.executions.await_completed("execution_id", devbox_id="devbox_id")
702+
703+
@parametrize
704+
async def test_method_await_completed_with_config(self, async_client: AsyncRunloop) -> None:
705+
"""Test await_completed with custom polling configuration"""
706+
707+
mock_execution_completed = DevboxAsyncExecutionDetailView(
708+
devbox_id="devbox_id",
709+
execution_id="execution_id",
710+
status="completed",
711+
stdout="Finished with config!",
712+
stderr="",
713+
)
714+
715+
config = PollingConfig(interval_seconds=0.1, max_attempts=10)
716+
717+
with patch.object(async_client.devboxes.executions, '_post') as mock_post:
718+
mock_post.return_value = mock_execution_completed
719+
720+
result = await async_client.devboxes.executions.await_completed("execution_id", devbox_id="devbox_id", polling_config=config)
721+
722+
assert result.execution_id == "execution_id"
723+
assert result.status == "completed"
724+
725+
@parametrize
726+
async def test_method_await_completed_polling_timeout(self, async_client: AsyncRunloop) -> None:
727+
"""Test await_completed raises PollingTimeout when max attempts exceeded"""
728+
729+
mock_execution_running = DevboxAsyncExecutionDetailView(
730+
devbox_id="devbox_id",
731+
execution_id="execution_id",
732+
status="running",
733+
stdout="Still running...",
734+
stderr="",
735+
)
736+
737+
config = PollingConfig(interval_seconds=0.01, max_attempts=2)
738+
739+
with patch.object(async_client.devboxes.executions, '_post') as mock_post:
740+
mock_post.return_value = mock_execution_running
741+
742+
with pytest.raises(PollingTimeout):
743+
await async_client.devboxes.executions.await_completed("execution_id", devbox_id="devbox_id", polling_config=config)
744+
745+
@parametrize
746+
async def test_method_await_completed_various_statuses(self, async_client: AsyncRunloop) -> None:
747+
"""Test await_completed correctly handles different execution statuses"""
748+
749+
# Test with queued status first
750+
mock_execution_queued = DevboxAsyncExecutionDetailView(
751+
devbox_id="devbox_id",
752+
execution_id="execution_id",
753+
status="queued",
754+
stdout="",
755+
stderr="",
756+
)
757+
758+
mock_execution_completed = DevboxAsyncExecutionDetailView(
759+
devbox_id="devbox_id",
760+
execution_id="execution_id",
761+
status="completed",
762+
stdout="Done!",
763+
stderr="",
764+
)
765+
766+
with patch.object(async_client.devboxes.executions, '_post') as mock_post:
767+
mock_post.side_effect = [mock_execution_queued, mock_execution_completed]
768+
769+
result = await async_client.devboxes.executions.await_completed("execution_id", devbox_id="devbox_id")
770+
771+
assert result.execution_id == "execution_id"
772+
assert result.status == "completed"
773+
assert mock_post.call_count == 2

0 commit comments

Comments
 (0)