This commit is contained in:
qing committed 2026-07-30 21:57:04 +08:00
1 parent f935448389
commit 3f97dd92ef
26 files changed
+1865 -451

No files matched your search

+145
View File
@@ -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()