diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..338bc4f --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,35 @@ +name: test + +on: + pull_request: + push: + branches: + - main + +jobs: + tests: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.11", "3.12"] + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt -r requirements-dev.txt + playwright install --with-deps + - name: Run unit and integration tests + run: pytest tests/unit tests/integration + - name: Run e2e tests + run: pytest tests/e2e + - name: Upload coverage + uses: actions/upload-artifact@v4 + with: + name: coverage-${{ matrix.python-version }} + path: coverage.xml diff --git a/TESTING_GUIDE.md b/TESTING_GUIDE.md new file mode 100644 index 0000000..714d542 --- /dev/null +++ b/TESTING_GUIDE.md @@ -0,0 +1,26 @@ +# Testing Guide + +This project uses **pytest** for unit and integration tests and **Playwright** for end-to-end tests. + +## Setup + +1. Install Python dependencies: + ```bash + pip install -r requirements.txt -r requirements-dev.txt + playwright install --with-deps + ``` + +## Running Tests + +- **Unit & Integration**: + ```bash + pytest + ``` + Coverage is enforced at 80% by default. + +- **End-to-End**: + ```bash + pytest tests/e2e + ``` + +Playwright will start the local server automatically during the E2E tests. diff --git a/playwright.config.py b/playwright.config.py new file mode 100644 index 0000000..27b71e7 --- /dev/null +++ b/playwright.config.py @@ -0,0 +1,9 @@ +import os +from playwright.sync_api import Playwright + +BASE_URL = os.getenv('BASE_URL', 'http://localhost:5000') + +# Default configuration for Playwright tests + +def pytest_configure(config): + config.base_url = BASE_URL diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..97c6a56 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,4 @@ +[pytest] +addopts = --cov=src --cov-report=term-missing --cov-report=xml --cov-fail-under=80 +testpaths = tests + diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..7b081c1 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,4 @@ +pytest>=8.0,<9.0 +pytest-cov>=4.1,<5.0 +pytest-playwright>=0.4,<0.5 +playwright>=1.40,<2.0 diff --git a/requirements.txt b/requirements.txt index d39ab37..f470cff 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -numpy==1.24.3 +numpy==1.26.4 plotly==5.14.1 flask==2.3.2 gunicorn==22.0.0 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/e2e/__init__.py b/tests/e2e/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/e2e/test_smoke.py b/tests/e2e/test_smoke.py new file mode 100644 index 0000000..479d66c --- /dev/null +++ b/tests/e2e/test_smoke.py @@ -0,0 +1,21 @@ +import subprocess +import time +import os + +import pytest + +SERVER_PORT = os.getenv('PORT', '5000') + +@pytest.fixture(scope="session", autouse=True) +def server(): + proc = subprocess.Popen(["python", "src/app.py"], env={**os.environ, "FLASK_DEBUG": "false"}) + time.sleep(3) + yield + proc.terminate() + proc.wait() + + +@pytest.mark.asyncio +async def test_homepage(page): + await page.goto(f"http://localhost:{SERVER_PORT}/") + assert await page.text_content("body") is not None diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/test_routes.py b/tests/integration/test_routes.py new file mode 100644 index 0000000..f18656a --- /dev/null +++ b/tests/integration/test_routes.py @@ -0,0 +1,27 @@ +from src.app import app + + +def test_compute_topological_compatibility_route(): + client = app.test_client() + payload = { + 'skb1': { + 'tx': 0.1, + 'ty': 0.1, + 'tz': 0.1, + 'tt': 0.1, + 'orientable': 1, + 'genus': 0 + }, + 'skb2': { + 'tx': -0.1, + 'ty': -0.1, + 'tz': -0.1, + 'tt': -0.1, + 'orientable': 1, + 'genus': 0 + } + } + response = client.post('/compute_topological_compatibility', json=payload) + assert response.status_code == 200 + data = response.get_json() + assert 'compatible' in data diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/test_app.py b/tests/unit/test_app.py new file mode 100644 index 0000000..3cabd9e --- /dev/null +++ b/tests/unit/test_app.py @@ -0,0 +1,34 @@ +import numpy as np +from src import app as app_module + + +def test_generate_twisted_strip_shape(): + twists = [1, 1, 1, 0] + x, y, z, u, v = app_module.generate_twisted_strip(twists, t=0.0, loop_factor=1) + assert x.shape == (50, 50) + assert y.shape == (50, 50) + assert z.shape == (50, 50) + assert u.shape == (50, 50) + assert v.shape == (50, 50) + + +def test_compute_topological_compatibility_returns_dict(): + skb1 = { + 'tx': 0.1, + 'ty': 0.1, + 'tz': 0.1, + 'tt': 0.1, + 'orientable': 1, + 'genus': 0 + } + skb2 = { + 'tx': -0.1, + 'ty': -0.1, + 'tz': -0.1, + 'tt': -0.1, + 'orientable': 1, + 'genus': 0 + } + result = app_module.compute_topological_compatibility(skb1=skb1, skb2=skb2) + assert isinstance(result, dict) + assert 'compatible' in result