Files
chongming-mqtt/test/test_internal.py
T

133 lines
4.3 KiB
Python

"""
InternalMQTTClient 单元测试
覆盖:URI 凭据解码、请求-响应关联(回显 request_id / {rid} 模板 / 兜底)、
超时、并发乱序、订阅去重。
"""
import asyncio
import json
from chongming_mqtt.internal_mqtt import InternalMQTTClient
from fakes import attach, run
def test_uri_decoded():
"""URI 中 percent-encoded 凭据应被解码"""
c = InternalMQTTClient("mqtt://u:p%40w@host:1883")
assert c._username == "u"
assert c._password == "p@w"
assert c._clean_uri == "mqtt://host:1883"
def test_request_not_running_returns_none():
"""未连接时 request 返回 None"""
c = InternalMQTTClient("mqtt://127.0.0.1:1883")
assert run(c.request("/req", "/resp", {})) is None
def test_request_echo_rid():
"""对方回显 request_id → 精确匹配响应"""
client = InternalMQTTClient("mqtt://127.0.0.1:1883")
async def respond(topic, payload):
rid = json.loads(payload)["request_id"]
await client._handle("/resp", json.dumps({"request_id": rid, "ok": True}).encode())
attach(client, on_publish=respond)
r = run(client.request("/req", "/resp", {"a": 1}, timeout=3))
assert r is not None
assert r.get("ok") is True
assert "request_id" in r
def test_request_rid_template():
"""response_topic 含 {rid} → 订阅唯一主题并按主题匹配"""
client = InternalMQTTClient("mqtt://127.0.0.1:1883")
async def respond(topic, payload):
rid = json.loads(payload)["request_id"]
await client._handle(f"/resp/{rid}", json.dumps({"request_id": rid, "tpl": True}).encode())
fake = attach(client, on_publish=respond)
r = run(client.request("/req", "/resp/{rid}", {}, timeout=3))
assert r is not None
assert r.get("tpl") is True
# {rid} 模板:订阅的是唯一主题(含实际 rid),而非原模板
assert any("/resp/" in t for t, _ in fake.subscribed)
def test_request_timeout_returns_none():
"""对方不回复 → 超时返回 None"""
class NoReply:
async def publish(self, topic, payload, qos):
pass
async def subscribe(self, topics):
pass
client = InternalMQTTClient("mqtt://127.0.0.1:1883")
client._client = NoReply()
client._running = True
assert run(client.request("/req", "/resp", {}, timeout=0.2)) is None
def test_request_fifo_fallback():
"""对方不回显 request_id(固定主题)→ 兜底 FIFO 取最早挂起请求"""
client = InternalMQTTClient("mqtt://127.0.0.1:1883")
async def respond(topic, payload):
await client._handle("/resp", json.dumps({"v": "first"}).encode())
attach(client, on_publish=respond)
r = run(client.request("/req", "/resp", {}, timeout=3))
assert r == {"v": "first"}
def test_request_concurrent_out_of_order():
"""并发请求 + 响应乱序 → 每个请求按 request_id 拿回自己的响应"""
client = InternalMQTTClient("mqtt://127.0.0.1:1883")
attach(client)
async def do_req(tag):
rid = f"r-{tag}"
fut = asyncio.get_running_loop().create_future()
client._pending[rid] = fut
if tag == "x":
await asyncio.sleep(0.05)
# 模拟响应乱序:先回 y,再回 x
await client._handle("/resp", json.dumps({"request_id": "r-y", "v": "y"}).encode())
await client._handle("/resp", json.dumps({"request_id": "r-x", "v": "x"}).encode())
return await asyncio.wait_for(fut, 2), rid
async def main():
t1 = asyncio.create_task(do_req("x"))
t2 = asyncio.create_task(do_req("y"))
return await asyncio.gather(t1, t2)
results = run(main())
by_rid = {rid: res for res, rid in results}
assert by_rid["r-x"]["v"] == "x"
assert by_rid["r-y"]["v"] == "y"
def test_subscribe_once_dedup():
"""同一响应主题重复订阅应去重"""
class SpySub:
def __init__(self):
self.topics = []
async def publish(self, topic, payload, qos):
pass
async def subscribe(self, topics):
self.topics.extend(t for t, _ in topics)
spy = SpySub()
client = InternalMQTTClient("mqtt://127.0.0.1:1883")
client._client = spy
client._running = True
run(client._subscribe_once("/resp"))
run(client._subscribe_once("/resp"))
assert spy.topics == ["/resp"]