Skip to content

Commit 32cb484

Browse files
committed
Add initial smoketests
1 parent 49d9d2f commit 32cb484

File tree

15 files changed

+472
-5
lines changed

15 files changed

+472
-5
lines changed

.github/workflows/ci.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,4 +95,6 @@ jobs:
9595
run: ./scripts/bootstrap
9696

9797
- name: Run tests
98-
run: ./scripts/test
98+
env:
99+
RUN_SMOKETESTS: '0'
100+
run: ./scripts/test --ignore=tests/smoketests

.github/workflows/smoketests.yml

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
name: Smoketests
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
environment:
7+
description: "Target environment"
8+
type: choice
9+
default: dev
10+
options:
11+
- dev
12+
- prod
13+
14+
jobs:
15+
smoke:
16+
runs-on: ubuntu-latest
17+
timeout-minutes: 120
18+
defaults:
19+
run:
20+
working-directory: api-client-python
21+
steps:
22+
- name: Checkout
23+
uses: actions/checkout@v4
24+
25+
- name: Setup uv
26+
uses: astral-sh/setup-uv@v4
27+
with:
28+
python-version: "3.11"
29+
30+
- name: Install dependencies
31+
run: |
32+
uv pip install -r requirements-dev.lock
33+
34+
- name: Configure environment
35+
env:
36+
DEV_KEY: ${{ secrets.RUNLOOP_SMOKETEST_DEV_API_KEY }}
37+
PROD_KEY: ${{ secrets.RUNLOOP_SMOKETEST_PROD_API_KEY }}
38+
run: |
39+
if [ "${{ github.event.inputs.environment }}" = "prod" ]; then
40+
echo "RUNLOOP_API_KEY=${PROD_KEY}" >> $GITHUB_ENV
41+
echo "RUNLOOP_BASE_URL=https://api.runloop.ai" >> $GITHUB_ENV
42+
else
43+
echo "RUNLOOP_API_KEY=${DEV_KEY}" >> $GITHUB_ENV
44+
echo "RUNLOOP_BASE_URL=https://api.runloop.pro" >> $GITHUB_ENV
45+
fi
46+
echo "DEBUG=false" >> $GITHUB_ENV
47+
echo "RUN_SMOKETESTS=1" >> $GITHUB_ENV
48+
echo "PYTHONPATH=${{ github.workspace }}/api-client-python/src" >> $GITHUB_ENV
49+
50+
- name: Run smoke tests (pytest via uv)
51+
env:
52+
# Force sequential to avoid overloading remote resources
53+
PYTEST_ADDOPTS: "-n 1 -m smoketest"
54+
run: |
55+
uv run pytest -q -vv tests/smoketests

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,5 @@ dist
1313
.envrc
1414
codegen.log
1515
Brewfile.lock.json
16+
17+
.DS_Store

pyproject.toml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ dev-dependencies = [
5151
"respx",
5252
"pytest",
5353
"pytest-asyncio",
54+
"pytest-timeout",
5455
"ruff",
5556
"time-machine",
5657
"nox",
@@ -134,15 +135,17 @@ replacement = '[\1](https://github.com/runloopai/api-client-python/tree/main/\g<
134135

135136
[tool.pytest.ini_options]
136137
testpaths = ["tests"]
137-
addopts = "--tb=short -n auto"
138+
addopts = "--tb=short -n auto --dist=loadfile -m 'not smoketest'"
138139
xfail_strict = true
139140
asyncio_mode = "auto"
140141
asyncio_default_fixture_loop_scope = "session"
141142
filterwarnings = [
142143
"error"
143144
]
144145
markers = [
145-
"skip_if_strict: skip test if using strict validation (for prism mock server issues)"
146+
"skip_if_strict: skip test if using strict validation (for prism mock server issues)",
147+
"timeout: per-test timeout provided by pytest-timeout",
148+
"smoketest: end-to-end smoke tests against real API",
146149
]
147150

148151
[tool.pyright]

requirements-dev.lock

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ pyright==1.1.399
9898
pytest==8.3.3
9999
# via pytest-asyncio
100100
# via pytest-xdist
101+
pytest-timeout==2.3.1
102+
# via runloop-api-client (dev)
101103
pytest-asyncio==0.24.0
102104
pytest-xdist==3.7.0
103105
python-dateutil==2.8.2

scripts/test

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,15 @@ fi
5555
export DEFER_PYDANTIC_BUILD=false
5656

5757
echo "==> Running tests"
58-
rye run pytest "$@"
58+
59+
# By default, exclude smoketests unless explicitly enabled.
60+
# This ensures PR CI doesn't run E2E smoketests unless RUN_SMOKETESTS=1.
61+
PYTEST_ARGS=()
62+
if [ "${RUN_SMOKETESTS}" != "1" ]; then
63+
PYTEST_ARGS+=( -m "not smoketest" )
64+
fi
65+
66+
rye run pytest "${PYTEST_ARGS[@]}" "$@"
5967

6068
echo "==> Running Pydantic v1 tests"
61-
rye run nox -s test-pydantic-v1 -- "$@"
69+
rye run nox -s test-pydantic-v1 -- "${PYTEST_ARGS[@]}" "$@"

tests/conftest.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,16 @@ def pytest_collection_modifyitems(items: list[pytest.Function]) -> None:
7171
)
7272
item.add_marker(pytest.mark.skip(reason=reason))
7373

74+
# Finally, deselect smoketests by default unless explicitly enabled.
75+
# This ensures PR CI will not run smoketests unintentionally.
76+
if os.getenv("RUN_SMOKETESTS") != "1":
77+
kept: list[pytest.Function] = []
78+
for item in items:
79+
if item.get_closest_marker("smoketest") is None:
80+
kept.append(item)
81+
if len(kept) != len(items):
82+
items[:] = kept
83+
7484

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

tests/smoketests/README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Smoke tests
2+
3+
End-to-end smoke tests run against the real API to validate critical flows (devboxes, snapshots, blueprints, executions/log tailing, scenarios/benchmarks).
4+
5+
- Local run (requires `RUNLOOP_API_KEY`):
6+
7+
```bash
8+
export RUNLOOP_API_KEY=... # required
9+
# optionally override API base
10+
# export RUNLOOP_BASE_URL=https://api.runloop.ai
11+
12+
# Install deps and run via uv
13+
uv pip install -r requirements-dev.lock
14+
15+
# Run all tests
16+
RUN_SMOKETESTS=1 uv run pytest -q -vv tests/smoketests
17+
18+
# Run a single file
19+
RUN_SMOKETESTS=1 uv run pytest -q -vv tests/smoketests/test_devboxes.py
20+
21+
# Run a single test by name
22+
RUN_SMOKETESTS=1 uv run pytest -q -k "test_create_and_await_running_timeout" tests/smoketests/test_devboxes.py
23+
```
24+
25+
- 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.
26+
27+

tests/smoketests/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"""Test package for smoketests.
2+
3+
Ensures relative imports like `from .utils import ...` work under pytest.
4+
"""
5+
6+
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import pytest
2+
3+
from runloop_api_client.lib.polling import PollingConfig
4+
5+
from .utils import make_client, unique_name
6+
7+
pytestmark = [pytest.mark.smoketest]
8+
9+
10+
client = make_client()
11+
12+
13+
"""
14+
Tests are run sequentially and can be dependent on each other.
15+
This is to avoid overloading resources and save efficiency.
16+
"""
17+
_blueprint_id = None
18+
_blueprint_name = unique_name("bp")
19+
20+
21+
def teardown_module() -> None:
22+
global _blueprint_id
23+
if _blueprint_id:
24+
try:
25+
client.blueprints.delete(_blueprint_id)
26+
except Exception:
27+
pass
28+
29+
30+
@pytest.mark.timeout(30)
31+
def test_create_blueprint_and_await_build() -> None:
32+
global _blueprint_id
33+
created = client.blueprints.create_and_await_build_complete(
34+
name=_blueprint_name,
35+
polling_config=PollingConfig(max_attempts=180, interval_seconds=5.0, timeout_seconds=30 * 60),
36+
)
37+
assert created.status == "build_complete"
38+
_blueprint_id = created.id
39+
40+
41+
@pytest.mark.timeout(30)
42+
def test_start_devbox_from_base_blueprint_by_id() -> None:
43+
assert _blueprint_id
44+
devbox = client.devboxes.create_and_await_running(
45+
blueprint_id=_blueprint_id,
46+
polling_config=PollingConfig(max_attempts=120, interval_seconds=5.0, timeout_seconds=20 * 60),
47+
)
48+
assert devbox.blueprint_id == _blueprint_id
49+
assert devbox.status == "running"
50+
client.devboxes.shutdown(devbox.id)
51+
52+
53+
@pytest.mark.timeout(30)
54+
def test_start_devbox_from_base_blueprint_by_name() -> None:
55+
devbox = client.devboxes.create_and_await_running(
56+
blueprint_name=_blueprint_name,
57+
polling_config=PollingConfig(max_attempts=120, interval_seconds=5.0, timeout_seconds=20 * 60),
58+
)
59+
assert devbox.blueprint_id
60+
assert devbox.status == "running"
61+
client.devboxes.shutdown(devbox.id)

0 commit comments

Comments
 (0)