Skip to content

Commit f9bbd25

Browse files
authored
chore: Smoke tests for tutorial code (#157)
1 parent 0a592c6 commit f9bbd25

4 files changed

Lines changed: 115 additions & 0 deletions

File tree

.github/workflows/lint-test.yaml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,36 @@ jobs:
182182
include-hidden-files: true
183183
path: .coverage.py${{ matrix.python_version }}.integration.tuner*
184184

185+
test-smoke:
186+
name: Tests - smoke
187+
runs-on: ubuntu-latest
188+
timeout-minutes: 5
189+
strategy:
190+
matrix:
191+
python_version: [3.12, 3.13]
192+
steps:
193+
- name: Checkout
194+
uses: actions/checkout@v4
195+
196+
- name: Install python
197+
uses: actions/setup-python@v5
198+
with:
199+
python-version: ${{matrix.python_version}}
200+
201+
- name: Install uv
202+
uses: astral-sh/setup-uv@v4
203+
with:
204+
enable-cache: true
205+
cache-dependency-glob: "uv.lock"
206+
207+
- name: Install project
208+
run: uv sync --group test
209+
210+
- name: Run smoke tests
211+
run: uv run pytest ./tests/smoke/
212+
env:
213+
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
214+
185215
coverage-report:
186216
name: Report coverage
187217
needs: [test-unit, test-integration, test-integration-tuner] # Depends on tests passing

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,9 @@ plugboard = "plugboard.cli:app"
110110
asyncio_mode = "auto"
111111
asyncio_default_fixture_loop_scope = "session"
112112
asyncio_default_test_loop_scope = "session"
113+
markers = [
114+
"smoke: marks tests as smoke tests (deselect with '-m \"not smoke\"')"
115+
]
113116

114117
[tool.coverage.run]
115118
source = ["plugboard"]

tests/smoke/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Smoke tests package."""

tests/smoke/test_examples_smoke.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
"""Smoke tests for examples/tutorials Python files."""
2+
3+
import os
4+
from pathlib import Path
5+
import subprocess
6+
import sys
7+
from typing import Iterator, Tuple
8+
9+
import pytest
10+
11+
12+
SMOKE_TEST_TIMEOUT = 90
13+
PROJECT_ROOT = Path(__file__).parent.parent.parent
14+
15+
16+
@pytest.fixture(scope="module", autouse=True)
17+
def ray_disable_uv_run() -> Iterator[None]:
18+
"""Disable Ray's `uv run` runtime environment for smoke tests."""
19+
# uv run environment will prevent tests from running outside of the project root
20+
# This is necessary because the smoke tests run in a separate process
21+
os.environ["RAY_ENABLE_UV_RUN_RUNTIME_ENV"] = "0"
22+
yield
23+
os.environ.pop("RAY_ENABLE_UV_RUN_RUNTIME_ENV", None)
24+
25+
26+
def pytest_generate_tests(metafunc: pytest.Metafunc) -> None:
27+
"""Dynamically generate test parameters for each tutorial file."""
28+
if "file_and_dir" in metafunc.fixturenames:
29+
# Get tutorial files
30+
tutorials_dir = PROJECT_ROOT / "examples" / "tutorials"
31+
32+
if not tutorials_dir.exists():
33+
pytest.skip(f"Tutorials directory not found: {tutorials_dir}")
34+
35+
tutorial_files = []
36+
for py_file in tutorials_dir.rglob("*.py"):
37+
working_dir = py_file.parent
38+
tutorial_files.append((py_file, working_dir))
39+
40+
if not tutorial_files:
41+
pytest.skip("No Python files found in examples/tutorials")
42+
43+
# Create test IDs for better test output
44+
test_ids = [str(py_file.relative_to(PROJECT_ROOT)) for py_file, _ in tutorial_files]
45+
46+
metafunc.parametrize("file_and_dir", tutorial_files, ids=test_ids)
47+
48+
49+
@pytest.mark.smoke
50+
def test_tutorial_file_runs(file_and_dir: Tuple[Path, Path]) -> None:
51+
"""Test that a tutorial file runs without errors."""
52+
py_file, working_dir = file_and_dir
53+
54+
try:
55+
process = subprocess.Popen( # noqa: S603
56+
[sys.executable, py_file.name],
57+
cwd=working_dir,
58+
stdout=subprocess.PIPE,
59+
stderr=subprocess.PIPE,
60+
text=True,
61+
)
62+
try:
63+
stdout, stderr = process.communicate(timeout=SMOKE_TEST_TIMEOUT)
64+
except subprocess.TimeoutExpired:
65+
process.kill()
66+
stdout, stderr = process.communicate()
67+
pytest.skip(
68+
f"{py_file.relative_to(PROJECT_ROOT)} timed out after {SMOKE_TEST_TIMEOUT} seconds"
69+
)
70+
71+
if process.returncode != 0:
72+
error_msg = (
73+
f"Tutorial file {py_file.relative_to(PROJECT_ROOT)} "
74+
f"failed to run successfully.\n"
75+
f"Return code: {process.returncode}\n"
76+
f"STDOUT:\n{stdout}\n"
77+
f"STDERR:\n{stderr}"
78+
)
79+
pytest.fail(error_msg)
80+
except Exception as e:
81+
pytest.fail(f"Error running tutorial file {py_file.relative_to(PROJECT_ROOT)}: {e}")

0 commit comments

Comments
 (0)