Skip to content
Open
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
6 changes: 4 additions & 2 deletions .github/workflows/python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ on:

jobs:
build:
timeout-minutes: 20
runs-on: ${{ format('{0}-latest', matrix.os) }}
strategy:
fail-fast: false
Expand All @@ -26,16 +27,17 @@ jobs:
python-version: ${{ format('{0}.{1}', matrix.python-major-version, matrix.python-minor-version) }}

steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up Python ${{ env.python-version }}
uses: actions/setup-python@v2
uses: actions/setup-python@v5
with:
python-version: ${{ env.python-version }}
- name: Install uv and Nox
run: |
python -m pip install --upgrade pip
python -m pip install uv nox
- name: Run tests using Nox
timeout-minutes: 15
# Nox creates the per-version venv and uses `uv sync --frozen` (single
# uv.lock) to install the project and dependencies into it.
run: |
Expand Down
17 changes: 5 additions & 12 deletions noxfile.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,26 @@
import os
import sys
import nox

# Select the Python versions to test against (these must be installed on the system)
python_versions = ['3.10', '3.11', '3.12']

# Configure radCAD for tests
if sys.platform.startswith('win'):
# Use the multiprocessing backend on Windows to avoid recursion depth errors
# TODO Remove this once the recursion depth issue is resolved
# (literal name of Backend.MULTIPROCESSING; avoids importing radcad here)
os.environ['RADCAD_BACKEND'] = 'MULTIPROCESSING'


def install_dependencies(session):
'''Install the project and its dependencies into the session venv with uv'''
venv = os.path.abspath(session.virtualenv.location)
session.run(
'uv', 'sync', '--frozen',
'--python', session.python,
'--extra', 'compat',
'--extra', 'extension-backend-ray',
env={'UV_PROJECT_ENVIRONMENT': session.virtualenv.location},
env={'UV_PROJECT_ENVIRONMENT': venv, 'VIRTUAL_ENV': venv},
external=True,
)

@nox.session(python=python_versions)
def tests(session):
install_dependencies(session)
session.run(
'pytest',
'python', '-m', 'pytest',
# Currently failing on Windows due to recursion depth limit
# '-n', 'auto', # Run tests in parallel using pytest-xdist
'tests',
Expand All @@ -37,7 +30,7 @@ def tests(session):
def benchmarks(session):
install_dependencies(session)
session.run(
'pytest',
'python', '-m', 'pytest',
'benchmarks/benchmark_radcad.py',
f'--benchmark-save=py{session.python}',
)
Expand Down
11 changes: 11 additions & 0 deletions tests/test_engine.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
from radcad import Model, Simulation, Experiment
from radcad.engine import Engine, Backend
from tests.test_cases import basic

def test_radcad_backend_env_sets_default(monkeypatch):
monkeypatch.setenv('RADCAD_BACKEND', 'SINGLE_PROCESS')
assert Engine().backend == Backend.SINGLE_PROCESS


def test_radcad_backend_env_overrides_explicit_backend(monkeypatch):
monkeypatch.setenv('RADCAD_BACKEND', 'SINGLE_PROCESS')
assert Engine(backend=Backend.PATHOS).backend == Backend.SINGLE_PROCESS


def test_run():
states = basic.states
state_update_blocks = basic.state_update_blocks
Expand Down
82 changes: 74 additions & 8 deletions tests/test_examples.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,85 @@
import pytest
import os
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path

import pytest


NOTEBOOK_TIMEOUT_SECONDS = 180


def _tail(text, limit=4000):
if isinstance(text, bytes):
text = text.decode(errors="replace")
return text[-limit:] if text else ""


def check_notebook(notebook):
result = os.popen(f"jupyter nbconvert --to script --execute --stdout {notebook} | ipython").read()
return "1" in result
notebook = Path(notebook)

with tempfile.TemporaryDirectory() as temp_dir:
temp_dir = Path(temp_dir)
work_dir = temp_dir / notebook.parent.name
shutil.copytree(notebook.parent, work_dir)
output_dir = temp_dir / "executed"
output_dir.mkdir()
notebook = work_dir / notebook.name

env = os.environ.copy()
# Force notebooks to single-process execution. Windows multiprocessing
# spawn cannot import notebook-defined functions, and PATHOS/dill has
# historically hit recursion/stack limits with notebook globals on CI.
env["RADCAD_BACKEND"] = "SINGLE_PROCESS"
command = [
sys.executable,
"-m",
"jupyter",
"nbconvert",
"--to",
"notebook",
"--execute",
f"--ExecutePreprocessor.timeout={NOTEBOOK_TIMEOUT_SECONDS}",
"--ExecutePreprocessor.kernel_name=python3",
"--output-dir",
output_dir,
str(notebook),
]
try:
result = subprocess.run(
command,
capture_output=True,
text=True,
timeout=NOTEBOOK_TIMEOUT_SECONDS,
env=env,
)
except subprocess.TimeoutExpired as exc:
pytest.fail(
f"Notebook timed out after {NOTEBOOK_TIMEOUT_SECONDS}s: {notebook}\n"
f"stdout:\n{_tail(exc.stdout)}\n"
f"stderr:\n{_tail(exc.stderr)}"
)

assert result.returncode == 0, (
f"Notebook failed: {notebook}\n"
f"stdout:\n{_tail(result.stdout)}\n"
f"stderr:\n{_tail(result.stderr)}"
)


def test_game_of_life():
assert check_notebook("examples/game_of_life/game-of-life.ipynb")
check_notebook("examples/game_of_life/game-of-life.ipynb")


def test_predator_prey_sd():
assert check_notebook("examples/predator_prey_sd/predator-prey-sd.ipynb")
check_notebook("examples/predator_prey_sd/predator-prey-sd.ipynb")


def test_predator_prey_abm():
assert check_notebook("examples/predator_prey_abm/predator-prey-abm.ipynb")
check_notebook("examples/predator_prey_abm/predator-prey-abm.ipynb")

def test_harmonic_oscillator():
assert check_notebook("examples/harmonic_oscillator/harmonic_oscillator.ipynb")

def test_harmonic_oscillator():
check_notebook("examples/harmonic_oscillator/harmonic_oscillator.ipynb")
Loading