Skip to content
Merged
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
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
Expand Down
6 changes: 5 additions & 1 deletion docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
68 changes: 68 additions & 0 deletions tests/test_granian_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Loading