update
This commit is contained in:
1 parent
f935448389
commit
3f97dd92ef
26 files changed
+1865
-451
No files matched your search
@@ -0,0 +1,145 @@
|
||||
"""RpcClient / RpcServer(JSON RPC 约定层)单元测试。
|
||||
|
||||
纯标准库,直接运行:
|
||||
|
||||
cd pymsg
|
||||
python -m unittest discover -s tests -v
|
||||
"""
|
||||
|
||||
import json
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
import unittest
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from pymsg import RpcClient, RpcError, RpcServer, send
|
||||
|
||||
|
||||
def _free_port() -> int:
|
||||
with socket.socket() as s:
|
||||
s.bind(("127.0.0.1", 0))
|
||||
return s.getsockname()[1]
|
||||
|
||||
|
||||
class _RpcFixture:
|
||||
"""后台线程运行 RpcServer;routes: {name: fn}。"""
|
||||
|
||||
def __init__(self, routes, workers=0, json_default=None):
|
||||
self.port = _free_port()
|
||||
self.server = RpcServer("127.0.0.1", self.port, workers=workers,
|
||||
json_default=json_default)
|
||||
for name, fn in routes.items():
|
||||
self.server.add_route(name, fn)
|
||||
self.thread = threading.Thread(target=self.server.start, daemon=True)
|
||||
|
||||
def __enter__(self):
|
||||
self.thread.start()
|
||||
deadline = time.time() + 5
|
||||
while time.time() < deadline:
|
||||
try:
|
||||
with socket.create_connection(("127.0.0.1", self.port), timeout=0.2):
|
||||
return self
|
||||
except OSError:
|
||||
time.sleep(0.02)
|
||||
raise RuntimeError("server did not start in time")
|
||||
|
||||
def __exit__(self, *exc):
|
||||
self.server.stop()
|
||||
self.thread.join(timeout=5)
|
||||
return False
|
||||
|
||||
|
||||
class TestRpc(unittest.TestCase):
|
||||
|
||||
def test_call_returns_data(self):
|
||||
with _RpcFixture({"echo": lambda req: {"got": req["msg"]}}) as fx:
|
||||
rpc = RpcClient("127.0.0.1", fx.port, "t")
|
||||
self.assertEqual(rpc.call("echo", {"msg": "hi"}), {"got": "hi"})
|
||||
|
||||
def test_route_field_is_injected(self):
|
||||
seen = {}
|
||||
|
||||
def handler(req):
|
||||
seen.update(req)
|
||||
return None
|
||||
|
||||
with _RpcFixture({"r": handler}) as fx:
|
||||
RpcClient("127.0.0.1", fx.port).call("r", {"a": 1})
|
||||
# _route 已被服务端 pop,handler 只看到业务字段
|
||||
self.assertEqual(seen, {"a": 1})
|
||||
|
||||
def test_business_error_raises_rpcerror(self):
|
||||
def handler(req):
|
||||
raise ValueError("bad input")
|
||||
|
||||
with _RpcFixture({"x": handler}) as fx:
|
||||
with self.assertRaises(RpcError) as cm:
|
||||
RpcClient("127.0.0.1", fx.port).call("x")
|
||||
self.assertIn("bad input", str(cm.exception))
|
||||
|
||||
def test_unknown_route_raises_rpcerror(self):
|
||||
with _RpcFixture({}) as fx:
|
||||
with self.assertRaises(RpcError) as cm:
|
||||
RpcClient("127.0.0.1", fx.port).call("nope")
|
||||
self.assertIn("未知路由", str(cm.exception))
|
||||
|
||||
def test_malformed_json_gets_error_response(self):
|
||||
"""服务端收到非法 JSON:返回 success=false 且不崩溃,后续请求正常。"""
|
||||
with _RpcFixture({"ok": lambda req: "fine"}) as fx:
|
||||
with socket.create_connection(("127.0.0.1", fx.port), timeout=5) as s:
|
||||
send(s, "not-json{{{")
|
||||
from pymsg import recv
|
||||
resp = json.loads(recv(s, timeout=5))
|
||||
self.assertFalse(resp["success"])
|
||||
self.assertIn("JSON 解析失败", resp["error"])
|
||||
# 服务仍然存活
|
||||
self.assertEqual(RpcClient("127.0.0.1", fx.port).call("ok"), "fine")
|
||||
|
||||
def test_data_none_when_absent(self):
|
||||
with _RpcFixture({"n": lambda req: None}) as fx:
|
||||
self.assertIsNone(RpcClient("127.0.0.1", fx.port).call("n"))
|
||||
|
||||
def test_json_default_serialization(self):
|
||||
def default(o):
|
||||
if isinstance(o, datetime):
|
||||
return o.isoformat()
|
||||
return str(o)
|
||||
|
||||
now = datetime(2026, 7, 30, 12, 0, tzinfo=timezone.utc)
|
||||
with _RpcFixture({"t": lambda req: {"ts": now}}, json_default=default) as fx:
|
||||
data = RpcClient("127.0.0.1", fx.port).call("t")
|
||||
self.assertEqual(data["ts"], now.isoformat())
|
||||
|
||||
def test_concurrent_rpc_with_workers(self):
|
||||
def handler(req):
|
||||
time.sleep(0.3)
|
||||
return req["i"] * 2
|
||||
|
||||
with _RpcFixture({"dbl": handler}, workers=4) as fx:
|
||||
results = [None] * 6
|
||||
|
||||
def w(i):
|
||||
results[i] = RpcClient("127.0.0.1", fx.port).call("dbl", {"i": i})
|
||||
|
||||
t0 = time.monotonic()
|
||||
ts = [threading.Thread(target=w, args=(i,)) for i in range(6)]
|
||||
for t in ts:
|
||||
t.start()
|
||||
for t in ts:
|
||||
t.join(10)
|
||||
elapsed = time.monotonic() - t0
|
||||
|
||||
self.assertEqual(results, [i * 2 for i in range(6)])
|
||||
self.assertLess(elapsed, 1.2, f"workers=4 应并发,实际 {elapsed:.2f}s")
|
||||
|
||||
def test_per_call_timeout_override(self):
|
||||
with _RpcFixture({"slow": lambda req: time.sleep(2)}) as fx:
|
||||
rpc = RpcClient("127.0.0.1", fx.port, timeout=10.0)
|
||||
from pymsg import TimeoutError
|
||||
with self.assertRaises(TimeoutError):
|
||||
rpc.call("slow", timeout=0.3)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user