diff --git a/integration/conftest.py b/integration/conftest.py new file mode 100644 index 0000000..05b6c62 --- /dev/null +++ b/integration/conftest.py @@ -0,0 +1,14 @@ +"""Fixtures for the integration suite.""" + +import os + +import pytest + + +@pytest.fixture(scope="session") +def base_url(): + """Base URL of the running artifact, injected by `rbs integration-test`.""" + url = os.environ.get("RBS_BASE_URL") + if not url: + pytest.fail("RBS_BASE_URL is unset; run this suite via `rbs integration-test`") + return url.rstrip("/") diff --git a/integration/test_api.py b/integration/test_api.py new file mode 100644 index 0000000..423a274 --- /dev/null +++ b/integration/test_api.py @@ -0,0 +1,55 @@ +"""Integration tests: the built image answering over HTTP. + +Nothing here imports quantumsolver. Unlike tests/, which drives the app in-process via Flask's +test client, this suite only knows a URL — so it exercises the wheel, gunicorn, and the container +the way Redux does. +""" + +import pytest +import requests + +# Generous: each solve spawns a process that re-imports qiskit, and the app self-limits at 25s. +TIMEOUT = 40 + +# ([f0, f1], expected classification) +DEUTSCH_CASES = [ + ([0, 0], "constant"), + ([0, 1], "balanced"), +] + +# (f_bits, expected hidden string) +BERNSTEIN_VAZIRANI_CASES = [ + ([0, 1, 0, 1, 1, 0, 1, 0], "101"), +] + + +def test_health(base_url): + """GET /health reports ok — the same route rbs polls for readiness.""" + resp = requests.get(f"{base_url}/health", timeout=TIMEOUT) + assert resp.status_code == 200 + assert resp.json()["status"] == "ok" + + +@pytest.mark.parametrize("path", ["/", "/index"]) +def test_landing_routes(base_url, path): + """The landing routes answer.""" + resp = requests.get(f"{base_url}{path}", timeout=TIMEOUT) + assert resp.status_code == 200 + + +@pytest.mark.parametrize("f,expected", DEUTSCH_CASES) +def test_deutsch(base_url, f, expected): + """POST /deutsch-quantum classifies the oracle as constant or balanced.""" + resp = requests.post(f"{base_url}/deutsch-quantum", json=f, timeout=TIMEOUT) + assert resp.status_code == 200 + assert resp.json()["answer"] == expected + + +@pytest.mark.parametrize("f,expected", BERNSTEIN_VAZIRANI_CASES) +def test_bernstein_vazirani(base_url, f, expected): + """POST /bernstein-vazirani-quantum recovers the hidden string.""" + resp = requests.post( + f"{base_url}/bernstein-vazirani-quantum", json=f, timeout=TIMEOUT + ) + assert resp.status_code == 200 + assert resp.json()["answer"] == expected diff --git a/pyproject.toml b/pyproject.toml index 737ec7a..8776b86 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,6 +39,12 @@ select = ["E", "F", "I", "W", "B", "UP"] # black owns line length; don't double-enforce it (it won't wrap comments/strings) ignore = ["E501"] +[tool.pytest.ini_options] +# `rbs unit-test` runs a bare `pytest`, which would otherwise collect from the repo root and +# sweep up integration/ — a suite that needs a running container. Keep the default run to tests/; +# `rbs integration-test` names integration/ explicitly, which overrides this. +testpaths = ["tests"] + [tool.coverage.run] source = ["quantumsolver"] diff --git a/rbs.toml b/rbs.toml index 65dca27..4d79e04 100644 --- a/rbs.toml +++ b/rbs.toml @@ -12,7 +12,10 @@ port = 27100 health-path = "/health" [integration] -command = "uv run pytest tests/integration -v" +# Lives outside tests/ so a bare `pytest` (what `rbs unit-test` runs) never collects it; +# see [tool.pytest.ini_options] in pyproject.toml. No [[integration.services]] needed — +# quantumsolver is a leaf, so the ephemeral stack is just the artifact itself. +command = "uv run pytest integration -v" [push] on-branch = "main"