# -*- coding: utf-8 -*- r'''验「后台点一下让电脑自己更新」这条链路。 == 链路 == 管理页点「更新」 -> POST /v1/admin/update_device {deviceId} -> Relay.request_update() 把 {"cmd":"update","action":"start"} 塞进该设备的 to_agent 队列 -> 被控端下一次长轮询拿到它 -> server.py 的 _do_update:查最新版 -> 下载 -> 校验 -> 静默装 -> 重启 == 为什么要专门测 == 这条链路横跨三端(管理页 JS -> 中转 -> 被控端),任何一环写错, 表现都是"我点了更新,电脑没反应" —— 而用户在办公室里没法排查。 而且它**会重启别人的电脑**,所以几个边界必须卡死: * 设备不存在 -> 明确说没有,不能静默成功 * 设备离线 -> 直接拒。命令挂在队列里等它上线才执行, 那时用户早忘了,突然重启很莫名其妙 * 重复点 -> 不重复塞(队列会堆积) * 在线 -> 真的塞进队列,而且内容就是那条更新命令 == 上一版测试自己写错的地方 == 我用了个 FakeDevice,`__init__` 里默认 `_on = True`, 结果"离线设备"那条用例测的其实是**在线**设备 —— 假绿。 现在 FakeDevice 显式接收 online 参数,不再有隐藏默认值。 ''' from __future__ import annotations import json import re import sys import threading from pathlib import Path ROOT = Path(__file__).resolve().parent.parent sys.path.insert(0, str(ROOT / "relay")) FAILS: list = [] def check(name: str, ok: bool, why: str = "") -> None: print(" [%s] %s%s" % ("PASS" if ok else "FAIL", name, (" —— " + why) if why else "")) if not ok: FAILS.append(name) class FakeDevice: """假的被控端。online 必须显式给,不给就报错 —— 第一版给了个 True 的默认值,导致"离线"用例永远测不到离线。""" def __init__(self, online: bool): self.to_agent = bytearray() self._online = bool(online) self.lan_ips: list = [] self.port = 0 def online(self) -> bool: return self._online def push_to_agent(self, data: bytes) -> None: self.to_agent.extend(data) def main() -> int: print("=" * 58) print(" 后台「更新」按钮链路") print("=" * 58) print() import relay as R print("一、Relay.request_update 的边界") r = R.Relay.__new__(R.Relay) r.lock = threading.RLock() r.devices = {} res = r.request_update("不存在的设备") check("设备不存在 -> 拒绝", res.get("ok") is False, "拿到 %r" % res) check(" 错误信息说清楚是没这台", "没有这台设备" in str(res.get("error")), "拿到 %r" % res.get("error")) check("设备 id 为空也当不存在", r.request_update("").get("ok") is False) check(" None 也不炸", r.request_update(None).get("ok") is False) off = FakeDevice(online=False) r.devices["off1"] = off res = r.request_update("off1") check("离线设备 -> 拒绝", res.get("ok") is False, "拿到 %r" % res) check(" 理由提到离线", "离线" in str(res.get("error")), "拿到 %r" % res.get("error")) check(" 离线时**没往队列里塞东西**", len(off.to_agent) == 0, "队列里有 %d 字节 —— 离线时不该堆命令" % len(off.to_agent)) on = FakeDevice(online=True) r.devices["on1"] = on res = r.request_update("on1") check("在线设备 -> 成功", res.get("ok") is True, "拿到 %r" % res) check(" 队列里真的放了东西", len(on.to_agent) > 0) raw = bytes(on.to_agent).decode("utf-8") check(" 是换行结尾的 JSON(协议要求)", raw.endswith("\n"), "尾部是 %r" % raw[-6:]) try: msg = json.loads(raw.strip()) except Exception as exc: # noqa: BLE001 check(" 内容是合法 JSON", False, str(exc)) msg = {} check(" 内容是合法 JSON", bool(msg)) check(" cmd == update", msg.get("cmd") == "update", "拿到 %r" % msg) check(" action == start", msg.get("action") == "start", "拿到 %r" % msg) n_before = len(on.to_agent) res = r.request_update("on1") check("重复点 -> 不重复塞", len(on.to_agent) == n_before, "队列从 %d 涨到 %d" % (n_before, len(on.to_agent))) check(" 但仍然返回 ok(用户看到的是「已经在排队」)", res.get("ok") is True and "排队" in str(res.get("message")), "拿到 %r" % res) print() print("二、HTTP 接口接到了 Relay 方法上") src = (ROOT / "relay" / "relay.py").read_text(encoding="utf-8") check("有 /v1/admin/update_device 路由", '"/v1/admin/update_device"' in src) m = re.search(r'if path == "/v1/admin/update_device":(.{0,300})', src, re.S) body = m.group(1) if m else "" check(" 它调的是 RELAY.request_update", "RELAY.request_update(" in body, "应该走 Relay 的方法,而不是在 HTTP 处理里伸手掏 RELAY.devices") check(" 它把 deviceId 传下去了", 'get("deviceId")' in body) check("有 Relay.request_update 方法", "def request_update(self" in src) print() print("三、管理页那边") adm = (ROOT / "relay" / "admin.py").read_text(encoding="utf-8") check("设备行有「更新」按钮", 'data-update="%s"' in adm) check("JS 里有 data-update 的处理", "querySelectorAll('button[data-update]')" in adm) check("JS 会二次确认(别误点就重启别人电脑)", "confirm(" in adm and "更新到最新版" in adm) check("离线设备的按钮带 disabled", 'disabled title="设备离线' in adm) check("设备行显示了版本", 'data-l="版本"' in adm) check("表头也有版本列", "