Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions src/runloop_api_client/resources/devboxes/devboxes.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
from typing_extensions import Literal

import httpx
from uuid_utils import uuid7

# uuid_utils is not typed
from uuid_utils import uuid7 # type: ignore

from .lsp import (
LspResource,
Expand Down Expand Up @@ -809,7 +811,7 @@ def execute_and_await_completion(
return the result within the initial request's timeout. If the execution is not yet
complete, it switches to using wait_for_command to minimize latency while waiting.
"""
command_id = str(uuid7())
command_id = str(uuid7()) # type: ignore
execution = self.execute(
devbox_id,
command=command,
Expand Down Expand Up @@ -2251,7 +2253,7 @@ async def execute_and_await_completion(
complete, it switches to using wait_for_command to minimize latency while waiting.
"""

command_id = str(uuid7())
command_id = str(uuid7()) # type: ignore
execution = await self.execute(
devbox_id,
command=command,
Expand Down
9 changes: 5 additions & 4 deletions tests/smoketests/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Smoke tests

End-to-end smoke tests run against the real API to validate critical flows (devboxes, snapshots, blueprints, executions/log tailing, scenarios/benchmarks).
End-to-end smoke tests run against the real API to validate critical flows (devboxes, snapshots, blueprints, executions/log tailing, scenarios/benchmarks). Theses smoketests run both the
async and sync clients.

- Local run (requires `RUNLOOP_API_KEY`):

Expand All @@ -13,13 +14,13 @@ export RUNLOOP_API_KEY=... # required
uv pip install -r requirements-dev.lock

# Run all tests
RUN_SMOKETESTS=1 uv run pytest -q -vv tests/smoketests
RUN_SMOKETESTS=1 uv run pytest -q -vv -m smoketest tests/smoketests

# Run a single file
RUN_SMOKETESTS=1 uv run pytest -q -vv tests/smoketests/test_devboxes.py
RUN_SMOKETESTS=1 uv run pytest -q -vv -m smoketest tests/smoketests/test_devboxes.py

# Run a single test by name
RUN_SMOKETESTS=1 uv run pytest -q -k "test_create_and_await_running_timeout" tests/smoketests/test_devboxes.py
RUN_SMOKETESTS=1 uv run pytest -q -k -m smoketest "test_create_and_await_running_timeout" tests/smoketests/test_devboxes.py
```

- GitHub Actions: add repo secret `RUNLOOP_SMOKETEST_DEV_API_KEY` and `RUNLOOP_SMOKETEST_PROD_API_KEY`. The workflow `.github/workflows/smoketests.yml` supports an input `environment` (dev|prod) and runs these tests in CI.
Expand Down
38 changes: 38 additions & 0 deletions tests/smoketests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from __future__ import annotations

from typing import Iterator

import pytest
from _pytest.fixtures import FixtureRequest # pyright: ignore[reportPrivateImportUsage]

from runloop_api_client import Runloop

from .utils import make_client, make_async_client_adapter

"""
This file is used to create a client fixture for the tests.
It makes it possible to run the tests with both sync and async clients.
"""


@pytest.fixture(scope="module", params=["sync", "async"], ids=["sync-client", "async-client"])
def client(request: FixtureRequest) -> Iterator[Runloop]:
if request.param == "sync":
c: Runloop = make_client()
try:
yield c
finally:
try:
# Runloop supports close()
c.close()
except Exception:
pass
else:
c: Runloop = make_async_client_adapter()
try:
yield c
finally:
try:
c.close()
except Exception:
pass
32 changes: 18 additions & 14 deletions tests/smoketests/test_blueprints.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,26 @@
from __future__ import annotations

from typing import Iterator

import pytest

from runloop_api_client import Runloop
from runloop_api_client.lib.polling import PollingConfig

from .utils import make_client, unique_name
from .utils import unique_name

pytestmark = [pytest.mark.smoketest]


client = make_client()
@pytest.fixture(autouse=True, scope="module")
def _cleanup(client: Runloop) -> Iterator[None]: # pyright: ignore[reportUnusedFunction]
yield
global _blueprint_id
if _blueprint_id:
try:
client.blueprints.delete(_blueprint_id)
except Exception:
pass


"""
Expand All @@ -18,17 +31,8 @@
_blueprint_name = unique_name("bp")


def teardown_module() -> None:
global _blueprint_id
if _blueprint_id:
try:
client.blueprints.delete(_blueprint_id)
except Exception:
pass


@pytest.mark.timeout(30)
def test_create_blueprint_and_await_build() -> None:
def test_create_blueprint_and_await_build(client: Runloop) -> None:
global _blueprint_id
created = client.blueprints.create_and_await_build_complete(
name=_blueprint_name,
Expand All @@ -39,7 +43,7 @@ def test_create_blueprint_and_await_build() -> None:


@pytest.mark.timeout(30)
def test_start_devbox_from_base_blueprint_by_id() -> None:
def test_start_devbox_from_base_blueprint_by_id(client: Runloop) -> None:
assert _blueprint_id
devbox = client.devboxes.create_and_await_running(
blueprint_id=_blueprint_id,
Expand All @@ -51,7 +55,7 @@ def test_start_devbox_from_base_blueprint_by_id() -> None:


@pytest.mark.timeout(30)
def test_start_devbox_from_base_blueprint_by_name() -> None:
def test_start_devbox_from_base_blueprint_by_name(client: Runloop) -> None:
devbox = client.devboxes.create_and_await_running(
blueprint_name=_blueprint_name,
polling_config=PollingConfig(max_attempts=120, interval_seconds=5.0, timeout_seconds=20 * 60),
Expand Down
32 changes: 23 additions & 9 deletions tests/smoketests/test_devboxes.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,27 @@
from __future__ import annotations

from typing import Iterator

import pytest

from runloop_api_client import Runloop
from runloop_api_client.lib.polling import PollingConfig, PollingTimeout

from .utils import make_client, unique_name
from .utils import unique_name

pytestmark = [pytest.mark.smoketest]


client = make_client()
@pytest.fixture(autouse=True, scope="module")
def _cleanup(client: Runloop) -> Iterator[None]: # pyright: ignore[reportUnusedFunction]
yield
global _devbox_id
if _devbox_id:
try:
client.devboxes.shutdown(_devbox_id)
except Exception:
pass


"""
Tests are run sequentially and can be dependent on each other.
Expand All @@ -17,14 +31,14 @@


@pytest.mark.timeout(30)
def test_create_devbox() -> None:
def test_create_devbox(client: Runloop) -> None:
created = client.devboxes.create(name=unique_name("smoke-devbox"))
assert created.id
client.devboxes.shutdown(created.id)


@pytest.mark.timeout(30)
def test_await_running_create_and_await_running() -> None:
def test_await_running_create_and_await_running(client: Runloop) -> None:
global _devbox_id
created = client.devboxes.create_and_await_running(
name=unique_name("smoketest-devbox2"),
Expand All @@ -34,27 +48,27 @@ def test_await_running_create_and_await_running() -> None:
_devbox_id = created.id


def test_list_devboxes() -> None:
def test_list_devboxes(client: Runloop) -> None:
page = client.devboxes.list(limit=10)
assert isinstance(page.devboxes, list)
assert len(page.devboxes) > 0


def test_retrieve_devbox() -> None:
def test_retrieve_devbox(client: Runloop) -> None:
assert _devbox_id
view = client.devboxes.retrieve(_devbox_id)
assert view.id == _devbox_id


def test_shutdown_devbox() -> None:
def test_shutdown_devbox(client: Runloop) -> None:
assert _devbox_id
view = client.devboxes.shutdown(_devbox_id)
assert view.id == _devbox_id
assert view.status == "shutdown"


@pytest.mark.timeout(90)
def test_create_and_await_running_long_set_up() -> None:
def test_create_and_await_running_long_set_up(client: Runloop) -> None:
created = client.devboxes.create_and_await_running(
name=unique_name("smoketest-devbox-await-running-long-set-up"),
launch_parameters={"launch_commands": ["sleep 70"]},
Expand All @@ -65,7 +79,7 @@ def test_create_and_await_running_long_set_up() -> None:


@pytest.mark.timeout(30)
def test_create_and_await_running_timeout() -> None:
def test_create_and_await_running_timeout(client: Runloop) -> None:
with pytest.raises(PollingTimeout):
client.devboxes.create_and_await_running(
name=unique_name("smoketest-devbox-await-running-timeout"),
Expand Down
52 changes: 28 additions & 24 deletions tests/smoketests/test_executions.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,26 @@
from __future__ import annotations

from typing import Iterator

import pytest

from runloop_api_client import Runloop
from runloop_api_client.lib.polling import PollingConfig

from .utils import make_client, unique_name
from .utils import unique_name

pytestmark = [pytest.mark.smoketest]


client = make_client()
@pytest.fixture(autouse=True, scope="module")
def _cleanup(client: Runloop) -> Iterator[None]: # pyright: ignore[reportUnusedFunction]
yield
global _devbox_id
if _devbox_id:
try:
client.devboxes.shutdown(_devbox_id)
except Exception:
pass


"""
Expand All @@ -18,17 +31,8 @@
_exec_id = None


@pytest.fixture(scope="session")
def some_function_name():
# setup
yield
# teardown
if _devbox_id:
client.devboxes.shutdown(_devbox_id)


@pytest.mark.timeout(30)
def test_launch_devbox() -> None:
def test_launch_devbox(client: Runloop) -> None:
global _devbox_id
created = client.devboxes.create_and_await_running(
name=unique_name("exec-devbox"),
Expand All @@ -38,7 +42,7 @@ def test_launch_devbox() -> None:


@pytest.mark.timeout(30)
def test_execute_async_and_await_completion() -> None:
def test_execute_async_and_await_completion(client: Runloop) -> None:
assert _devbox_id
global _exec_id
started = client.devboxes.executions.execute_async(_devbox_id, command="echo hello && sleep 1")
Expand All @@ -52,7 +56,7 @@ def test_execute_async_and_await_completion() -> None:


@pytest.mark.timeout(30)
def test_tail_stdout_logs() -> None:
def test_tail_stdout_logs(client: Runloop) -> None:
assert _devbox_id and _exec_id
stream = client.devboxes.executions.stream_stdout_updates(execution_id=_exec_id, devbox_id=_devbox_id)
received = ""
Expand All @@ -64,7 +68,7 @@ def test_tail_stdout_logs() -> None:


@pytest.mark.timeout(30)
def test_execute_and_await_completion() -> None:
def test_execute_and_await_completion(client: Runloop) -> None:
assert _devbox_id
completed = client.devboxes.execute_and_await_completion(
_devbox_id,
Expand All @@ -74,15 +78,15 @@ def test_execute_and_await_completion() -> None:
assert completed.status == "completed"


@pytest.mark.timeout(90)
def test_execute_and_await_completion_long_running() -> None:
assert _devbox_id
completed = client.devboxes.execute_and_await_completion(
_devbox_id,
command="echo hello && sleep 70",
polling_config=PollingConfig(max_attempts=120, interval_seconds=2.0),
)
assert completed.status == "completed"
# @pytest.mark.timeout(90)
# def test_execute_and_await_completion_long_running(client: Runloop) -> None:
# assert _devbox_id
# completed = client.devboxes.execute_and_await_completion(
# _devbox_id,
# command="echo hello && sleep 70",
# polling_config=PollingConfig(max_attempts=120, interval_seconds=2.0),
# )
# assert completed.status == "completed"


# TODO: Uncomment this test when we fix timeouts for polling
Expand Down
Loading