🧪 p3: tests for async callback path, native_async marker, sse_native

swadhinbiswas committed 2 months ago
1ad2e27
1 parent 364904a
commit 1ad2e27
3 files changed
+72 -0
▼crates/justapi-py/python/justapi/test_background.py
+33-0
130
130
# 20 requests × 50ms serialized would take >= 1.0s. Interleaved, they
131
131
# should finish well under that (allow generous CI margin).
132
132
assert elapsed < 0.8, f"async handlers serialized: 20×50ms took {elapsed:.2f}s"
133
+
134
+
135
+@pytest.mark.asyncio
136
+async def test_async_callback_path_concurrent_correctness():
137
+ """Stress the callback-driven async resolution: many concurrent async
138
+ handlers (mix of success and exception) must all complete correctly.
139
+ Guards the `_DoneNotifier` add_done_callback path (ADR-086)."""
140
+ import asyncio
141
+ from justapi import JustAPIApp, HTTPException
142
+ from justapi.testing import AsyncTestClient
143
+
144
+ app = JustAPIApp()
145
+
146
+ @app.get("/ok")
147
+ async def ok(req):
148
+ await asyncio.sleep(0.01)
149
+ return {"ok": True}
150
+
151
+ @app.get("/err")
152
+ async def err(req):
153
+ await asyncio.sleep(0.01)
154
+ raise HTTPException(status_code=404, detail="gone")
155
+
156
+ async with AsyncTestClient(app) as client:
157
+ results = await asyncio.gather(
158
+ *(client.get("/ok") for _ in range(30)),
159
+ *(client.get("/err") for _ in range(30)),
160
+ return_exceptions=True,
161
+ )
162
+ oks = [r for r in results if isinstance(r, dict) and r.get("status") == 200]
163
+ errs = [r for r in results if isinstance(r, dict) and r.get("status") == 404]
164
+ assert len(oks) == 30, f"expected 30 success, got {len(oks)}"
165
+ assert len(errs) == 30, f"expected 30 errors, got {len(errs)}"
133
166
▼crates/justapi-py/python/justapi/test_native.py
+22-0
171
171
172
172
resp = JustAPITestClient(app).get("/")
173
173
assert resp["status"] == 200
174
+
175
+
176
+def test_native_async_marker():
177
+ """@native_async marks a handler; it works like any async handler but is
178
+ flagged for the fastest dispatch (parallel on free-threaded builds, ADR-089)."""
179
+ import asyncio
180
+ from justapi import JustAPIApp, native_async, JustAPITestClient
181
+
182
+ app = JustAPIApp()
183
+
184
+ @app.get("/x")
185
+ @native_async
186
+ async def handler(request):
187
+ await asyncio.sleep(0.001)
188
+ return {"ok": True, "path": request.get("path")}
189
+
190
+ r = JustAPITestClient(app).get("/x")
191
+ assert r["status"] == 200
192
+ assert b"ok" in bytes(r["body"])
193
+ # The decorator marks the raw function; the wrapper propagation is
194
+ # verified by the Rust dispatch using the marker (integration-tested).
195
+ assert getattr(handler, "__native_async__", False) is True
174
196
▼crates/justapi-py/python/justapi/test_sse.py
+17-0
23
23
print("Response body:", repr(body))
24
24
for i in range(5):
25
25
assert f"data: {i}\n\n" in body
26
+
27
+
28
+def test_sse_native_rust_stream():
29
+ """Rust-native SSE stream (ADR-088): events generated entirely in Rust,
30
+ no Python handler — must work on the real server AND test client."""
31
+ from justapi import JustAPIApp, JustAPITestClient
32
+
33
+ app = JustAPIApp()
34
+ app.sse_native("/events", count=3, interval_ms=0)
35
+
36
+ r = JustAPITestClient(app).get("/events")
37
+ assert r["status"] == 200
38
+ body = bytes(r["body"]).decode()
39
+ assert 'data: {"n":1}' in body
40
+ assert 'data: {"n":3}' in body
41
+ # content-type is event-stream
42
+ assert r["headers"].get("content-type", "").startswith("text/event-stream")
26
43