From b35ee686a91b0ddce4f6506d7a399e18b08cccf5 Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Sun, 26 Apr 2026 18:50:38 +0300 Subject: [PATCH] test+docs: Granian RSGI e2e POST + CI note (issue #12) - Add subprocess e2e for POST + JSON body via real granian --interface rsgi - Document granian e2e in development.md; comment in ci.yml Closes #12 --- .github/workflows/ci.yml | 1 + docs/development.md | 6 +++- tests/test_granian_e2e.py | 68 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 615295a..278a819 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -68,6 +68,7 @@ jobs: exit 1 fi uv pip install --force-reinstall "${wheels[0]}" + # Full suite includes tests/test_granian_e2e.py (real Granian subprocess, --interface rsgi). - name: Pytest (isolated dir so repo tree does not shadow the installed package) run: | cd "$(mktemp -d)" diff --git a/docs/development.md b/docs/development.md index 6f3a2f7..715440b 100644 --- a/docs/development.md +++ b/docs/development.md @@ -40,12 +40,16 @@ The suite uses **pytest** and is configured in `pyproject.toml` with `testpaths pip install "oxyroute[dev]" # in your dev env, from source after maturin develop ``` +## Granian RSGI (end-to-end) + +`tests/test_granian_e2e.py` starts a real **Granian** subprocess with `--interface rsgi`, sends HTTP requests with **httpx**, then stops the server. It is part of the normal **pytest** run when `granian` is installed (`oxyroute[dev]` includes it). The same file runs in **CI** on every matrix combination (Linux, macOS, Windows), so the native RSGI path is exercised against a real server, not only the ASGI in-process transport. + ## Continuous integration The workflow at `.github/workflows/ci.yml` (job name: **ci**): - Matrix across **OS** (Ubuntu, macOS, Windows) and **Python** 3.10 through 3.14 (see `.github/workflows/ci.yml`) -- Installs `maturin`, `pytest`, `httpx`, `oxyjwt`, builds a release wheel, installs it, and runs the tests as above +- Installs dev dependencies (including **granian**), builds a release wheel, installs it, and runs the full pytest suite as above ## See also diff --git a/tests/test_granian_e2e.py b/tests/test_granian_e2e.py index 4495a68..78e42ca 100644 --- a/tests/test_granian_e2e.py +++ b/tests/test_granian_e2e.py @@ -93,3 +93,71 @@ def root() -> str: proc.wait(timeout=10) except subprocess.TimeoutExpired: proc.kill() + + +def test_granian_rsgi_post_json_body() -> None: + d = tempfile.mkdtemp() + p = os.path.join(d, "e2e_rsgi_post.py") + with open(p, "w", encoding="utf-8") as f: + f.write( + textwrap.dedent( + """\ + from oxyroute import App + app = App() + + @app.post("/echo") + def echo(json: dict) -> str: + return f"n:{json.get('n', 0)}" + """ + ) + ) + port = _free_port() + env = os.environ.copy() + env["PYTHONPATH"] = d + cmd: list[str] = [ + sys.executable, + "-m", + "granian", + "e2e_rsgi_post:app", + "--host", + "127.0.0.1", + "--port", + str(port), + "--interface", + "rsgi", + "--workers", + "1", + ] + proc = subprocess.Popen( + cmd, + env=env, + cwd=d, + stdout=subprocess.DEVNULL, + stderr=subprocess.PIPE, + text=True, + ) + try: + deadline = time.time() + 20.0 + last_err: str | None = None + while time.time() < deadline: + time.sleep(0.05) + try: + with httpx.Client(timeout=1.0) as c: + r = c.post(f"http://127.0.0.1:{port}/echo", json={"n": 7}) + if r.status_code == 200 and r.text == "n:7": + return + except (httpx.HTTPError, OSError) as e: + last_err = str(e) + if proc.poll() is not None: + err = proc.stderr.read() if proc.stderr else "" + raise AssertionError( + f"granian exited early: code={proc.returncode} stderr={err!r} last={last_err!r}" + ) + err = proc.stderr.read() if proc.stderr else "" + raise AssertionError(f"server did not become ready. last={last_err!r} stderr={err!r}") + finally: + proc.terminate() + try: + proc.wait(timeout=10) + except subprocess.TimeoutExpired: + proc.kill()