Files
ykxmz/_tests/test_no_orphan_methods.py
xmz7891 2061f9525f 2.3.0:手表界面精简 + 播放器 + 鼠标重做
手表端
- 功能页从 10 个杂项精简到 7 个(屏幕/鼠标/媒体/文件/电脑/语音/电源)
- 删掉:游戏封锁、待办、掷骰子、抽奖、壁纸、显示器开关、定时关机、
  剪贴板历史、性能/磁盘/电量、文件夹浏览、教室限制等
- 改成显式列按钮,不再把电脑端所有功能组无脑铺到表盘
- 远程鼠标控制重做:滑动区 + 左键/滚轮/右键三按钮
  滚轮点按=中键、上下滑=滚动
- 新增视频/音乐播放器(手表本地播放):进度拖动、左右滑快进退10秒、
  点画面播放暂停、上一/下一、播完自动下一首

电脑端
- 新增 kill_all(结束所有进程),白名单保护系统关键进程和自身
- 新增 media_list / media_pull(播放器数据源)
  安全边界:只允许媒体目录,用 relative_to 判断,防同前缀兄弟目录绕过

中转站
- known_devices 从 ID 集合改成「设备ID -> 配对码」映射
- 修复:管理员改码后关机超过 6 小时,改码会被超时清理冲掉
- 老格式 state 文件平滑升级
2026-10-18 11:39:14 +08:00

207 lines
9.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
'''防回归:抓"定义了但从没被调用"的方法。
为什么值得单独一个测试:
1.7.0 交付后用户装上发现**一片白屏**。根因是
private void initTabs() { ... } // 定义在第 937 行
而 onCreate 里只调了 buildControlPanel(),**从头到尾没人调 initTabs()**。
结果 tabDefs 为 null -> selectTab() 直接 return -> 内容区空白。
这种错误编译能过、别的测试也能过,只有真机跑起来才看得见,
所以必须静态查出来。
判定方法:
统计方法名在文件里出现的次数(定义那一处也算)。
只出现 1 次 = 只有定义没调用。
分两档:
* **硬性失败**:关键路径上的方法(initTabs / 三个面板 / selectTab)。
这些漏了就是白屏或功能全无,必须拦住。
* **仅报告**:其它可疑方法。有些确实是历史遗留的死代码
(下面的 KNOWN_DEAD 记录了已确认的),有些是接口回调
(由框架调用,本文件里当然只有定义)。
报告出来供清理,但不拦构建 —— 免得噪音太大没人看。
'''
from __future__ import annotations
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
MAIN = (ROOT / "android" / "src" / "com" / "lanremote" / "watch"
/ "MainActivity.java")
# 框架/接口回调:由 Android 或 RemoteClient 调用,本文件里只有定义是正常的
WHITELIST = {
"onCreate", "onDestroy", "onResume", "onPause", "onStart", "onStop",
"onRestart", "onNewIntent", "onSaveInstanceState", "onRestoreInstanceState",
"onBackPressed", "onKeyDown", "onKeyUp", "onWindowFocusChanged",
"onRequestPermissionsResult", "onActivityResult", "onOptionsItemSelected",
"onCreateOptionsMenu", "onPrepareOptionsMenu", "onConfigurationChanged",
"onTouchEvent", "onClick", "onLongClick", "onTouch", "onAnimationUpdate",
"onAnimationStart", "onAnimationEnd", "onAnimationCancel", "onAnimationRepeat",
"onPreDraw", "onGlobalLayout", "onScrollChanged", "onItemClick",
"onItemSelected", "onNothingSelected", "onFocusChange", "onEditorAction",
"run", "call", "get", "set", "equals", "hashCode", "toString",
"uncaughtException", "afterTextChanged", "beforeTextChanged",
"onTextChanged", "onProgressChanged", "onStartTrackingTouch",
"onStopTrackingTouch", "onDateSet", "onTimeSet", "onDismiss",
"onCancel", "onShow", "onKey", "onCreateDialog", "onPrepareDialog",
"onReceive", "onServiceConnected", "onServiceDisconnected",
"onBind", "onCreateView", "onViewCreated", "onDestroyView",
"onAttach", "onDetach", "onActivityCreated", "onOptionsMenuClosed",
"onContextItemSelected", "onCreateContextMenu", "handleMessage",
"onChildViewAdded", "onChildViewRemoved", "onGlobalFocusChanged",
"onLayoutChange", "onScrollStateChanged", "onScrolled",
"onPageStarted", "onPageFinished", "shouldOverrideUrlLoading",
"onReceivedError", "onDownloadStart", "onChildDraw",
# RemoteClient.Listener 的实现
"onDisconnected", "onEvent", "onFrame", "onProgress", "onLog",
}
# 已确认的历史遗留死代码:早期版本的实现,后来换成了别的写法,
# 函数留着但没人调。不影响功能,列出来是为了有朝一日清掉。
KNOWN_DEAD = {
"buildFeatureTab": "旧的分页构建方式,已被 renderGroups() 取代",
"pickFolder": "已被 pickFile() 取代",
"pickClipHistory": "功能已移到服务端注册表",
"confirmWallpaper": "功能已移到服务端注册表",
"confirmMonitor": "功能已移到服务端注册表",
"sendMedia": "已被 media() 取代",
"promptRun": "已被 runFeature() 取代",
"showInfo": "已被 feature_run 的 info 取代",
"findSavedToken": "旧的重连逻辑,已被 RelayTransport 取代",
"glassBackground": "旧的美化辅助函数,样式改成 glass() 后没人用了",
"sp": "旧的字号辅助,已被 dp() 取代",
}
# 关键路径:这些漏了就是白屏 / 功能全无,必须硬性拦住
CRITICAL = ("buildConnectPanel", "buildControlPanel", "initTabs",
"buildMirrorPanel", "buildLanPanel")
passed = 0
failed = 0
def check(name, ok, detail=""):
global passed, failed
if ok:
passed += 1
print(" [PASS] " + name)
else:
failed += 1
print(" [FAIL] " + name + " " + str(detail))
def defined_methods(text: str) -> list[str]:
"""找出主程序里定义的方法名。
⚠ **带 `@Override` 的跳过**。
为什么:这些是**父类/接口回调**,由框架调用,源码里当然只出现一次 ——
而这个扫描的判据正是"名字只出现 1 次 = 没人调"。
踩到的实例:手表端的 `GestureDetector.SimpleOnGestureListener`
里有 onSingleTapConfirmed / onDoubleTap / onScroll,
它们由 Android 的触摸框架回调,源码里各只出现一次。
不跳过的话扫描会把它们报成"未知的未调用方法"(假阳性)——
而这种假阳性最坏的结果是**有人为了让它变绿,把回调删了**,
于是手势就失灵了。
同类还会遇到的:View.OnClickListener.onClick、
DialogInterface.OnClickListener.onClick、Runnable.run 等。
"""
pattern = re.compile(
r"(?:private|public|protected|static|final|\s)+"
r"[\w<>\[\],.\s]+\s+(\w+)\s*\([^)]*\)\s*(?:throws [\w.,\s]+)?\{")
names = []
for match in pattern.finditer(text):
name = match.group(1)
if name in ("if", "for", "while", "switch", "catch", "synchronized",
"try", "return", "new"):
continue
# 往上看几行,如果有 @Override 就跳过
head = text[max(0, match.start() - 200):match.start()]
tail_lines = head.splitlines()[-3:]
if any("@Override" in ln for ln in tail_lines):
continue
names.append(name)
return names
def main() -> int:
check("MainActivity.java 在", MAIN.is_file())
if not MAIN.is_file():
return 1
text = MAIN.read_text(encoding="utf-8")
print()
print("一、关键路径必须被调用(漏了就是白屏)")
# initTabs 专项:白屏的根因就是它
init_def = len(re.findall(r"private void initTabs\s*\(", text))
init_use = len(re.findall(r"\binitTabs\s*\(\s*\)", text))
check("initTabs() 有定义", init_def >= 1, f"定义 {init_def} 处")
check("initTabs() 被调用(否则连上后一片白屏)",
init_use >= 2, f"出现 {init_use} 次(1 次是定义,>1 才算被调用)")
check("initTabs() 紧跟 buildControlPanel()",
re.search(r"buildControlPanel\(\);\s*(?://[^\n]*\n\s*)*initTabs\(\);",
text) is not None,
"必须在 buildControlPanel() 之后立刻调用")
for method in CRITICAL:
n = len(re.findall(r"\b" + method + r"\s*\(", text))
check(f"{method}() 被调用", n > 1, f"出现 {n} 次")
print()
print("二、分页定义完整性")
tab_names = re.findall(r'new TabDef\("([^"]+)"', text)
# 2.3.0:用户要求「杂七杂八功能都删掉」,分页从 10 个精简到 7 个。
# 断言改成 7,并且把期望的名字也列出来 —— 这样以后误删/误加
# 都能立刻看出来,比只数个数更有用。
EXPECT_TABS = ["屏幕", "鼠标", "媒体", "文件", "电脑", "语音", "电源"]
check(f"定义了 {len(EXPECT_TABS)} 个分页", len(tab_names) == len(EXPECT_TABS),
f"实际 {len(tab_names)} 个:{' '.join(tab_names)}")
check("分页名字和 2.3.0 设计一致", tab_names == EXPECT_TABS,
f"实际 {' '.join(tab_names)},期望 {' '.join(EXPECT_TABS)}")
print(f" 分页: {' '.join(tab_names)}")
check("selectTab 有定义", "private void selectTab(" in text)
check("selectTab 被调用", len(re.findall(r"\bselectTab\s*\(", text)) > 1)
print()
print("三、未调用方法扫描")
names = defined_methods(text)
orphans = []
for name in sorted(set(names)):
if name in WHITELIST or name in KNOWN_DEAD:
continue
n = len(re.findall(r"\b" + re.escape(name) + r"\s*\(", text))
if n <= 1:
orphans.append((name, n))
check("没有**未知**的未调用方法", not orphans,
"\n ".join(f"{n}({c} 次)" for n, c in orphans)
if orphans else "")
if orphans:
print()
print(" 未知的未调用方法(可能是漏接的事件处理,也可能是新的死代码):")
for name, cnt in orphans:
print(f" {name} (出现 {cnt} 次)")
if KNOWN_DEAD:
print()
print(f" 已知死代码 {len(KNOWN_DEAD)} 个(不影响功能,待清理):")
for name, why in sorted(KNOWN_DEAD.items()):
still = len(re.findall(r"\b" + re.escape(name) + r"\s*\(", text))
mark = "" if still else " [已删除]"
print(f" {name}: {why}{mark}")
print()
print("=" * 58)
print(f"通过 {passed} 项,失败 {failed} 项")
return 0 if failed == 0 else 1
if __name__ == "__main__":
sys.exit(main())