Files
ykxmz/_tests/verify_all.ps1
T
xmz7891 4295b482a1 2.0.0:安装时可设中转服务器 + 手表端新增设置页 + 中转默认留空
按用户定的版本规矩,这一轮既有新内容又有重构,所以升第二位:
1.9.0 -> 2.0.0(最后一位清 0)。

== 一、安装时就能设中转服务器(用户要求)==

安装向导那一页现在是两个输入框:配对码 + 中转服务器。
装的时候是"第一次设置",最该一次把地址填好,不然装完还得去
双击 exe 找设置窗口,很容易漏。

只填域名就行(自动补 https://),留空 = 只走局域网。
批量静默部署同样支持:
    ykxmz-setup-2.0.0.exe /VERYSILENT /CODE=246810 /RELAY=relay.你的域名

为此新增了一个 --relay-url 参数:
    --relay       连接一次、打印状态、然后退出(人工排查用)
    --relay-url   只写进配置,不连接、不退出(安装流程用)
用错那个的话,安装包的命令链会在中间就 return,后面的 --silent
根本跑不到 —— 所以必须分开。

== 二、手表端新增「设置」页(用户要求)==

用户原话:"安卓端加一个设置,各种设置就在这里面调"。
以前设置散在首页(只有一个"中转设置"按钮),现在统一收进设置页:
    * 中转服务器(填写 / 一键清空)
    * 记住的配对码(显示 / 清除)
    * 已配对的电脑(看数量 / 清空,带二次确认)
    * 关于(版本号从 APK 读,不会和代码对不上)

== 三、中转地址默认留空 + 一次性迁移(用户报的问题)==

用户原话:"安卓端为什么还是默认填的我的中转站,以后改成手动填"。

代码里的默认值早就是空的了,但用户看到的还是那个地址 ——
因为**老版本 1.5.0 把作者的域名硬编码成了默认值**,第一次运行就
存进了 SharedPreferences。装新 APK 不会清偏好,所以那个值一直留着。
用户其实从来没"选"过这个地址,是程序硬塞的。

所以加了 migrateLegacyRelay():存的值如果正好等于当时内置的域名,
就当成没设过、清掉。只清完全相等的,用户手填的不会动。

== 四、两个测试自身的毛病 ==

* test_lan_optimize 那条"没有把旧公网域名当默认值"太粗 ——
  它查的是源码里任何位置,于是迁移注释里引用那行老代码就把它判红了。
  改成先剥掉注释再查(断言要测的是"有没有把它当默认值")。

* test_image_modes 偶发红:它开真实 Tk 全屏窗口,在全量套件里跑
  偶尔起不来(单独跑 19/19 全过)。加了有界重试。
  偶发红的测试会让"全量通过"失去意义。

== 五、新增防复发断言 ==

_tests/test_relay_default_blank.py(15 项,回归阶段 3a3b):
    * 默认值必须是空串
    * 除了迁移清单,源码里不许有写死的中转域名
    * 迁移必须真的被调用(不是只定义)
    * 迁移必须是 remove,不能换成另一个硬编码值
    * Windows 端的默认也得是空

理由:用户说过"这个中转站我就自己用,不对外" —— 值得用断言钉死。

== 六、顺手修的 ==

* release_all.py 崩在 Gitee 那一步:Gitee 的
  GET /releases/tags/{tag} 在"标签存在、但还没发行版"时返回的是
  **HTTP 200 + JSON null**(不是 404),于是 .get("id") 直接
  AttributeError。发 1.9.0 时踩到,已改成容忍 None。

* installer/build_installer.py 里 license-combined.txt 是从
  docs/ 三份文档重新生成的,所以光改生成物没用(下次构建又变回去)。
  改成在 build_license() 里统一注入当前版本。

* ExE 文件属性里的版本(version_info.txt)从 1.7.1 起就一直没跟上。

全量回归 43 个阶段全部通过。
2026-10-07 23:02:44 +08:00

701 lines
34 KiB
PowerShell
Raw 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.
# 局域网遥控 全量验证脚本
#
# 依次执行:
# 0. 清理上一次测试可能残留的进程(否则发现端口会被抢占,导致假失败)
# 1. Python 被控端协议回归(含屏幕镜像/截屏)
# 2. 手表屏幕布局适配核算(490x602)
# 3. 屏幕共享可见提示(真实界面 + Win32 窗口枚举)
# 4. 小窗口 / 无托盘 / 后台常驻 / 双启动找回窗口
# 5. Java 协议层编译
# 6. Java <-> 桩服务端(含帧流、截屏、自动发现、单播探测)
# 7. Java <-> 打包后的 EXE 端到端
#
# 用法: powershell -ExecutionPolicy Bypass -File _tests\verify_all.ps1
$ErrorActionPreference = "Continue"
$root = Split-Path -Parent $PSScriptRoot
$tc = Join-Path $root "..\starklink\toolchain"
$jdk = Join-Path $tc "jdk17"
$java = Join-Path $jdk "bin\java.exe"
$javac= Join-Path $jdk "bin\javac.exe"
$out = Join-Path $root "_tests\out_java"
$exe = Join-Path $root "dist_exe\ykxmz\ykxmz.exe"
$androidJar = Join-Path $tc "android-sdk\platforms\android-34\android.jar"
$script:results = @()
# 当前会话是不是锁屏 / 没有交互桌面?
# 锁屏时截屏类测试**必然**失败(拿不到桌面会话),
# 但那不是代码问题。不识别的话汇总表一片红,真问题反而被淹没。
$script:desktopAvailable = $true
try {
Add-Type -Namespace Ykxmz -Name Desk -MemberDefinition @'
[System.Runtime.InteropServices.DllImport("user32.dll", SetLastError=true)]
public static extern System.IntPtr OpenInputDesktop(int flags, bool inherit, int access);
[System.Runtime.InteropServices.DllImport("user32.dll")]
public static extern bool CloseDesktop(System.IntPtr h);
public static bool HasDesktop() {
System.IntPtr h = OpenInputDesktop(0, false, 0x0001);
if (h == System.IntPtr.Zero) return false;
CloseDesktop(h);
return true;
}
'@ -ErrorAction Stop
$script:desktopAvailable = [Ykxmz.Desk]::HasDesktop()
} catch {
$script:desktopAvailable = $true # 查不出来就当可用,别把失败藏起来
}
if (-not $script:desktopAvailable) {
Write-Host ""
Write-Host " ⚠ 检测到电脑处于锁屏状态。" -ForegroundColor Yellow
Write-Host " 截屏相关的测试会失败,这是环境限制,不是代码问题。" -ForegroundColor Yellow
Write-Host " 这些阶段会记为 SKIP 而不是 FAIL。解锁后重跑即可验证。" -ForegroundColor Yellow
}
function Record($name, $ok, $needsDesktop = $false) {
# needsDesktop 的阶段在锁屏时记 SKIP —— 避免误报把真问题淹没
if ($needsDesktop -and -not $script:desktopAvailable) {
$script:results += [pscustomobject]@{
项目 = $name; 结果 = "SKIP(锁屏)" }
return
}
$script:results += [pscustomobject]@{
项目 = $name; 结果 = $(if ($ok) { "PASS" } else { "FAIL" }) }
}
function Banner($text) {
Write-Host ""
Write-Host ("=" * 70) -ForegroundColor DarkCyan
Write-Host " $text" -ForegroundColor Cyan
Write-Host ("=" * 70) -ForegroundColor DarkCyan
}
function Show-Fails($lines) {
# 失败的阶段光看最后几行没用,得把 [FAIL] 打出来
$fails = $lines | Select-String -Pattern '\[FAIL\]'
if ($fails) {
$fails | Select-Object -First 8 | ForEach-Object {
Write-Host " $($_.Line.Trim())" -ForegroundColor Red
}
}
$lines | Select-Object -Last 1 | ForEach-Object {
# ⚠ 这里可能是 ErrorRecord(子进程的 stderr 被塞进输出流时),
# 而 ErrorRecord 没有 .Trim() 方法 —— 直接调会抛
# "does not contain a method named 'Trim'",反而把真正的
# 失败信息盖住。所以先转成字符串再 Trim。
Write-Host " $([string]$_.Trim())"
}
}
function Close-TestWindows {
# 测试会弹真实的 Tk 窗口(首次设置、投图、屏幕共享提示…)。
# 正常路径下测试自己会关;但用例失败/超时时进程还挂着,
# 窗口就留在屏幕上不走了。这里统一收尾。
$closed = 0
Get-Process python, pythonw -ErrorAction SilentlyContinue |
Where-Object { $_.Id -ne $PID } |
ForEach-Object {
$line = (Get-CimInstance Win32_Process -Filter "ProcessId=$($_.Id)" -ErrorAction SilentlyContinue).CommandLine
if ($line -and ($line -match '_tests' -or $line -match 'run_agent' `
-or $line -match 'relay\.py')) {
# 先好好关主窗口
try { $_.CloseMainWindow() | Out-Null } catch { }
Start-Sleep -Milliseconds 250
if (-not $_.HasExited) {
Stop-Process -Id $_.Id -Force -ErrorAction SilentlyContinue
}
$closed++
}
}
# 被控端自己(测试起过 ykxmz.exe)
Get-Process ykxmz -ErrorAction SilentlyContinue | ForEach-Object {
try { $_.CloseMainWindow() | Out-Null } catch { }
Start-Sleep -Milliseconds 250
if (-not $_.HasExited) {
Stop-Process -Id $_.Id -Force -ErrorAction SilentlyContinue
}
$closed++
}
# Java 测试进程
Get-Process java -ErrorAction SilentlyContinue | ForEach-Object {
$line = (Get-CimInstance Win32_Process -Filter "ProcessId=$($_.Id)" -ErrorAction SilentlyContinue).CommandLine
if ($line -and ($line -match 'JavaClientTest' -or $line -match 'JavaRelayTest')) {
Stop-Process -Id $_.Id -Force -ErrorAction SilentlyContinue
$closed++
}
}
if ($closed -gt 0) {
Write-Host " 关了 $closed 个测试进程/窗口" -ForegroundColor DarkGray
}
Start-Sleep -Milliseconds 400
}
function Stop-Strays {
Get-Process ykxmz -ErrorAction SilentlyContinue |
ForEach-Object { Stop-Process -Id $_.Id -Force -ErrorAction SilentlyContinue }
# 各测试会临时起 relay.py / stub / java。
# 只清 run_agent/run_stub 是不够的 —— 漏掉的 relay.py 如果没及时退出,
# 端口就占着,下一个测试起不来,报出"假失败"
# (实测:chunked 单跑 9/9、协议单跑 69/69,套件里却 FAIL)。
Get-Process python, pythonw -ErrorAction SilentlyContinue |
Where-Object { $_.Id -ne $PID } |
ForEach-Object {
$line = (Get-CimInstance Win32_Process -Filter "ProcessId=$($_.Id)" -ErrorAction SilentlyContinue).CommandLine
if ($line -and ($line -match 'run_agent' -or $line -match 'run_stub' `
-or $line -match 'relay\.py' -or $line -match '_tests')) {
Stop-Process -Id $_.Id -Force -ErrorAction SilentlyContinue
}
}
# Java 测试进程(JavaClientTest / JavaRelayTest)
Get-Process java -ErrorAction SilentlyContinue |
ForEach-Object {
$line = (Get-CimInstance Win32_Process -Filter "ProcessId=$($_.Id)" -ErrorAction SilentlyContinue).CommandLine
if ($line -and ($line -match 'JavaClientTest' -or $line -match 'JavaRelayTest')) {
Stop-Process -Id $_.Id -Force -ErrorAction SilentlyContinue
}
}
# 等到关键端口真的空出来(最多 6 秒),别让下一个测试撞上
for ($i = 0; $i -lt 12; $i++) {
$busy = $false
foreach ($p in 8765, 8766, 8799, 8130, 8140) {
if (Get-NetTCPConnection -LocalPort $p -State Listen -ErrorAction SilentlyContinue) {
$busy = $true
}
if (Get-NetUDPEndpoint -LocalPort $p -ErrorAction SilentlyContinue) {
$busy = $true
}
}
if (-not $busy) { break }
Start-Sleep -Milliseconds 500
}
Start-Sleep -Milliseconds 300
}
# ---------------------------------------------------------------- 0
Banner "0/7 清理残留进程与端口"
Stop-Strays
$busy = @()
foreach ($p in 8765, 8766, 8799, 8801, 8803, 8805) {
if (Get-NetUDPEndpoint -LocalPort $p -ErrorAction SilentlyContinue) { $busy += "UDP $p" }
if (Get-NetTCPConnection -LocalPort $p -State Listen -ErrorAction SilentlyContinue) { $busy += "TCP $p" }
}
if ($busy.Count -eq 0) {
Write-Host "端口干净" -ForegroundColor Green
} else {
Write-Host ("仍被占用: " + ($busy -join ", ")) -ForegroundColor Yellow
}
# ---------------------------------------------------------------- 0a
# 清掉上一次测试留在 received 里的文件。
# 不清的话会越攒越多,把 file_list 的前 100 个窗口挤满,
# 导致"看不到刚上传的文件"这类假失败(真踩过:攒到 116 个)。
$receivedDir = Join-Path $root "_tests\received"
if (Test-Path $receivedDir) {
$old = (Get-ChildItem $receivedDir -File -ErrorAction SilentlyContinue).Count
if ($old -gt 0) {
Get-ChildItem $receivedDir -File | Remove-Item -Force -ErrorAction SilentlyContinue
Write-Host " 清掉上次测试残留的 $old 个文件" -ForegroundColor DarkGray
}
}
# ---------------------------------------------------------------- 0
# 编码审计放最前面:编码错了后面全白搭
Banner "0/7 编码审计(BOM / 行尾 / 乱码)"
& python (Join-Path $root "_tests\audit_encoding.py") *> (Join-Path $env:TEMP "lr_enc.txt")
$encCode = $LASTEXITCODE
Get-Content (Join-Path $env:TEMP "lr_enc.txt") -Encoding UTF8 -ErrorAction SilentlyContinue |
Select-Object -Last 3
Record "编码审计" ($encCode -eq 0)
# ---------------------------------------------------------------- 1
Banner "1/7 Python 被控端协议回归"
$stageOut = & python (Join-Path $root "_tests\test_agent_protocol.py") 2>&1
$stageCode = $LASTEXITCODE
$stageOut | Select-Object -Last 4
Record "Python 被控端协议回归" ($stageCode -eq 0) $true
# ---------------------------------------------------------------- 1b
Banner "1b/7 防火墙阻止规则检测(真机连不上的根因)"
$stageOut = & python (Join-Path $root "_tests\test_firewall.py") 2>&1
$stageCode = $LASTEXITCODE
$stageOut | Select-Object -Last 3
Record "防火墙阻止规则检测" ($stageCode -eq 0)
# ---------------------------------------------------------------- 1c
Banner "1c/7 连不上?-> 电脑打开网络自检(UDP 链路)"
Stop-Strays
& python (Join-Path $root "_tests\test_selfcheck.py") *> (Join-Path $env:TEMP "lr_sc.txt")
$scCode = $LASTEXITCODE
Get-Content (Join-Path $env:TEMP "lr_sc.txt") -Encoding UTF8 -ErrorAction SilentlyContinue |
Select-Object -Last 2
Record "自检请求链路" ($scCode -eq 0)
# ---------------------------------------------------------------- 1e
Banner "1e/7 新增功能(电量/磁盘/性能/进程/剪贴板历史/安全边界)"
Stop-Strays
& python (Join-Path $root "_tests\test_new_features.py") *> (Join-Path $env:TEMP "lr_feat.txt")
$featCode = $LASTEXITCODE
Get-Content (Join-Path $env:TEMP "lr_feat.txt") -Encoding UTF8 -ErrorAction SilentlyContinue |
Select-Object -Last 2
Record "新增功能" ($featCode -eq 0)
# ---------------------------------------------------------------- 1f
Banner "1f/7 电脑功能注册表(39 个功能 + 安全边界 + 可配置)"
Stop-Strays
& python (Join-Path $root "_tests\test_features.py") *> (Join-Path $env:TEMP "lr_feats.txt")
$featsCode = $LASTEXITCODE
Get-Content (Join-Path $env:TEMP "lr_feats.txt") -Encoding UTF8 -ErrorAction SilentlyContinue |
Select-Object -Last 2
Record "电脑功能注册表" ($featsCode -eq 0)
# ---------------------------------------------------------------- 1g
Banner "1g/7 功能模糊测试(105 个功能 × 空参数 / 垃圾参数 / 越权路径)"
Stop-Strays
& python (Join-Path $root "_tests\fuzz_features.py") *> (Join-Path $env:TEMP "lr_fuzz.txt")
$fuzzCode = $LASTEXITCODE
Get-Content (Join-Path $env:TEMP "lr_fuzz.txt") -Encoding UTF8 -ErrorAction SilentlyContinue |
Select-Object -Last 3
Record "功能模糊测试" ($fuzzCode -eq 0)
# ---------------------------------------------------------------- 1h
Banner "1h/7 端到端(打包后的 EXE:配对 / 功能列表 / 越权 / 防爆破)"
Stop-Strays
& python (Join-Path $root "_tests\test_e2e_features.py") *> (Join-Path $env:TEMP "lr_e2e.txt")
$e2eCode = $LASTEXITCODE
Get-Content (Join-Path $env:TEMP "lr_e2e.txt") -Encoding UTF8 -ErrorAction SilentlyContinue |
Select-Object -Last 3
Record "端到端(打包 EXE)" ($e2eCode -eq 0)
# ---------------------------------------------------------------- 1i
Banner "1i/7 静默模式(0 窗口 / 无终端 / 认证仍生效)"
Stop-Strays
& python (Join-Path $root "_tests\test_silent_mode.py") *> (Join-Path $env:TEMP "lr_silent.txt")
$silentCode = $LASTEXITCODE
Get-Content (Join-Path $env:TEMP "lr_silent.txt") -Encoding UTF8 -ErrorAction SilentlyContinue |
Select-Object -Last 3
Record "静默模式" ($silentCode -eq 0)
# ---------------------------------------------------------------- 1j
Banner "1j/7 配一次→零窗口(令牌重连,全程无窗口)"
Stop-Strays
& python (Join-Path $root "_tests\test_pair_once_flow.py") *> (Join-Path $env:TEMP "lr_flow.txt")
$flowCode = $LASTEXITCODE
Get-Content (Join-Path $env:TEMP "lr_flow.txt") -Encoding UTF8 -ErrorAction SilentlyContinue |
Select-Object -Last 3
Record "配一次→零窗口" ($flowCode -eq 0)
# ---------------------------------------------------------------- 1k
Banner "1k/7 首次安装设置窗口(出现一次,之后不再出现)"
Stop-Strays
& python (Join-Path $root "_tests\test_first_run_setup.py") *> (Join-Path $env:TEMP "lr_setup.txt")
$setupCode2 = $LASTEXITCODE
Get-Content (Join-Path $env:TEMP "lr_setup.txt") -Encoding UTF8 -ErrorAction SilentlyContinue |
Select-Object -Last 3
Record "首次设置窗口" ($setupCode2 -eq 0)
# ---------------------------------------------------------------- 1l
Banner "1l/7 开机零窗口(照注册表里的自启命令真跑一遍)"
Stop-Strays
& python (Join-Path $root "_tests\test_boot_silent.py") *> (Join-Path $env:TEMP "lr_boot.txt")
$bootCode = $LASTEXITCODE
Get-Content (Join-Path $env:TEMP "lr_boot.txt") -Encoding UTF8 -ErrorAction SilentlyContinue |
Select-Object -Last 3
Record "开机零窗口" ($bootCode -eq 0)
# ---------------------------------------------------------------- 2
Banner "2/7 手表屏幕布局适配核算 (490x602)"
$stageOut = & python (Join-Path $root "_tests\check_layout.py") 2>&1
$stageCode = $LASTEXITCODE
$stageOut | Select-Object -Last 3
Record "手表布局适配" ($stageCode -eq 0)
# ---------------------------------------------------------------- 1z
Banner "1z/7 手表分页完整性(防白屏:方法定义了必须被调用)"
& python (Join-Path $root "_tests\test_no_orphan_methods.py") 2>&1 |
Select-Object -Last 6
Record "手表分页完整性" ($LASTEXITCODE -eq 0)
# ---------------------------------------------------------------- 2a2
Banner "2a2/7 APK 内容(诊断已合并进主程序)"
& python (Join-Path $root "_tests\test_apk_contents.py") *> (Join-Path $env:TEMP "lr_apk.txt")
$apkCode = $LASTEXITCODE
Get-Content (Join-Path $env:TEMP "lr_apk.txt") -Encoding UTF8 -ErrorAction SilentlyContinue |
Select-Object -Last 2
Record "APK 内容" ($apkCode -eq 0)
# ---------------------------------------------------------------- 2a
Banner "2a/7 网络调优(MTU 修复)"
# 这是可用率从 48% 提到 100% 的关键,而且容易被后人误改
# (比如"顺手把接口 MTU 也降一点" —— 实测那样会掉到 50%)。
& python (Join-Path $root "_tests\test_net_tune.py") 2>&1 |
Select-Object -Last 4
Record "网络调优(MTU)" ($LASTEXITCODE -eq 0)
# ---------------------------------------------------------------- 2b
Banner "2b/7 投图显示方式(屏幕中间 / 全屏 / 自定义大小)"
Stop-Strays
$imgLog = Join-Path $env:TEMP "lr_imgmode.txt"
& python (Join-Path $root "_tests\test_image_modes.py") *> $imgLog
$imgCode = $LASTEXITCODE
if ($imgCode -eq 0) {
Get-Content $imgLog -Encoding UTF8 -ErrorAction SilentlyContinue |
Select-Object -Last 3
} else {
Write-Host "失败输出:" -ForegroundColor Red
Get-Content $imgLog -Encoding UTF8 -ErrorAction SilentlyContinue |
Where-Object { $_ -match "FAIL|通过|Traceback|Error" }
}
Record "投图显示方式" ($imgCode -eq 0)
# ---------------------------------------------------------------- 3
Banner "3/7 屏幕共享可见提示(真实界面 + Win32 窗口枚举)"
Stop-Strays
$stageOut = & python (Join-Path $root "_tests\test_indicator_visible.py") 2>&1
$stageCode = $LASTEXITCODE
$stageOut | Select-Object -Last 3
Record "屏幕共享强制提示" ($stageCode -eq 0)
# ---------------------------------------------------------------- 2b
Banner "2b/7 中转 chunked 请求体(防连接错位)"
# Cloudflare 转发时可能用 Transfer-Encoding: chunked。
# 中转原来只读 Content-Length,残留的 "0\r\n\r\n" 会被当成下一个请求行,
# 报 "Bad request syntax ('0')",整条连接错位 -> 隧道大面积 502。
$stageOut = & python (Join-Path $root "_tests\test_chunked_body.py") 2>&1
Show-Fails $stageOut
Record "中转 chunked 请求体" ($LASTEXITCODE -eq 0)
# ---------------------------------------------------------------- 3a
Banner "3a/7 局域网直连路径(不经中转)"
# 手表和电脑同一个 WiFi 时走这条:UDP 广播找到电脑 + TCP 直连。
# 这条路径完全不碰中转,所以单独盯一下。
$stageOut = & python (Join-Path $root "_tests\test_lan_path.py") 2>&1
Show-Fails $stageOut
Record "局域网直连路径" ($LASTEXITCODE -eq 0)
# ---------------------------------------------------------------- 3a2
Banner "3a2/7 局域网优化(网段掩码 / 发现重发 / 回切)"
# 盯四件事:
# 1. 网段判断按网卡真实掩码,不再写死 /24(/16 网络下能直连却去走中转)
# 2. 广播发现重发 + 多网段(原来只发一次、只探一个网段)
# 3. 走中转之后能定期回切局域网
# 4. 默认中转地址为空白时,局域网照样能配对、能执行命令
$stageOut = & python (Join-Path $root "_tests\test_lan_optimize.py") 2>&1
Show-Fails $stageOut
Record "局域网优化" ($LASTEXITCODE -eq 0)
# ---------------------------------------------------------------- 3a3
Banner "3a3/7 手表局域网切换逻辑(源码断言)"
$stageOut = & python (Join-Path $root "_tests\test_watch_lan_switch.py") 2>&1
Show-Fails $stageOut
Record "手表局域网切换" ($LASTEXITCODE -eq 0)
# ---------------------------------------------------------------- 3a3b
Banner "3a3b/7 中转默认必须为空(防内置作者服务器)"
$stageOut = & python (Join-Path $root "_tests\test_relay_default_blank.py") 2>&1
Show-Fails $stageOut
Record "中转默认空白" ($LASTEXITCODE -eq 0)
# ---------------------------------------------------------------- 3a4
Banner "3a4/7 编码与换行(.java/.py 必须 UTF-8 无 BOM + LF)"
# 踩过的坑:edit 工具会把行尾写成 CRLF,而仓库约定是 LF。
# 这个检查专门盯住,免得改动悄悄把整个文件的行尾换掉。
$stageOut = & python (Join-Path $root "_tests\check_encoding.py") 2>&1
Show-Fails $stageOut
Record "编码与换行" ($LASTEXITCODE -eq 0)
# ---------------------------------------------------------------- 3a7
Banner "3a7/7 Python 语法(必须用 compile,不能用 ast.parse)"
# 这一轮被同一件事咬了三次:补丁脚本把 import 插到 from __future__
# 前面,整个文件就废了。而更糟的是 ast.parse 查不出来(它只解析,
# 不做编译期检查)—— 前几次「全仓语法检查 0 问题」都是假绿。
$stageOut = & python (Join-Path $root "_tests\check_python_syntax.py") 2>&1
Show-Fails $stageOut
Record "Python 语法检查" ($LASTEXITCODE -eq 0)
# ---------------------------------------------------------------- 3a8
Banner "3a8/7 版本号一致性(防漏改)"
$stageOut = & python (Join-Path $root "_tests\check_version_consistency.py") 2>&1
Show-Fails $stageOut
Record "版本号一致性" ($LASTEXITCODE -eq 0)
# ---------------------------------------------------------------- 3a5
Banner "3a5/7 局域网/中转对比基准(防测试自己骗人)"
# 这个基准一度是红的,而且报告里写着「这是中转链路的产品 bug」——
# 实际是基准自己有两个 bug:两条路用了同一个配对码、
# 以及 attach 用了旧码(打到已经死掉的设备上)。
# 一个已经修好的产品因此背了黑锅,而且症状(连接超时)
# 看起来完全像产品问题,极难自己发现。
$stageOut = & python (Join-Path $root "_tests\test_lan_bench_guard.py") 2>&1
Show-Fails $stageOut
Record "局域网对比基准" ($LASTEXITCODE -eq 0)
# ---------------------------------------------------------------- 3a6
Banner "3a6/7 管理页 JS 语法(防按钮全死)"
# 踩过两次的坑:admin.py 的 ADMIN_JS 是普通三引号字符串,
# 里面写 \n 会被 Python 变成真实换行;而 JS 的行注释 // 不能跨行,
# 一断行整段 <script> 语法错误 -> 页面上所有按钮失效。
# 第二次尤其讽刺:注释为了举例说明「不能写 \n」,自己写了一个 \n,
# 于是注释被断成两行,把整段 JS 弄坏了。
# 原来那个「管理页 JS 合法性」阶段只查 token 在不在,查不出语法错 ——
# 「测过」和「能用」是两件事。
$stageOut = & python (Join-Path $root "_tests\test_admin_js_syntax.py") 2>&1
Show-Fails $stageOut
Record "管理页 JS 语法" ($LASTEXITCODE -eq 0)
# ---------------------------------------------------------------- 3b
Banner "3b/7 Python 静态检查(pyflakes)"
# 这一层能抓到"运行到某行才炸"的问题。
# 踩过的坑:check_code_available() 用了没导入的 DEFAULT_RELAY_URL,
# 结果装到最后一步弹 NameError —— 而回归全绿、编码审计也过。
$stageOut = & python (Join-Path $root "_tests\audit_python_names.py") 2>&1
Show-Fails $stageOut
Record "Python 静态检查" ($LASTEXITCODE -eq 0)
# ---------------------------------------------------------------- 3b3
Banner "3b3/7 安装包脚本语法(Inno 的注释陷阱)"
# 踩过的坑:Inno 的 [Code] 解析器**连注释内容也处理**。
# 注释里写 [区段名] 报"无效的区段标记",写 {常量} 报 "Syntax error",
# 都是直接编译失败。这个检查专门拦这类。
$stageOut = & python (Join-Path $root "_build\check_iss.py") 2>&1
Show-Fails $stageOut
Record "安装包脚本语法" ($LASTEXITCODE -eq 0)
# ---------------------------------------------------------------- 3b4
Banner "3b4/7 卸载逻辑(不能杀自己 / 不能阻塞)"
# 踩过的坑:
# 1. taskkill /IM ykxmz.exe 把正在卸载的进程自己杀了 -> 删自启没执行
# 2. Popen 不重定向 stdio -> capture_output 等管道 EOF -> "卡死"
# 3. input() 在无 stdin 时永久阻塞 -> 控制面板卸载卡死
# 4. keep_dir 缺失 -> 自删和 Inno 卸载器抢目录
$stageOut = & python (Join-Path $root "_build\test_uninstall_logic.py") 2>&1
Show-Fails $stageOut
Record "卸载逻辑" ($LASTEXITCODE -eq 0)
# ---------------------------------------------------------------- 3b2
Banner "3b2/7 管理页网络体检"
# 用户看到"连不上"时最需要知道:隧道几条连接、公网通不通、延迟多少。
# 中转和 cloudflared 同机,能直接读它的指标。
$stageOut = & python (Join-Path $root "_tests\test_admin_netcheck.py") 2>&1
Show-Fails $stageOut
Record "管理页网络体检" ($LASTEXITCODE -eq 0)
# ---------------------------------------------------------------- 3b5
Banner "3b5/7 安装包完整周期(装 -> 验 -> 卸 -> 再验)"
# 这一步真的装一遍、再卸一遍。踩过的四个卡死 bug 全靠它才发现:
# input() 无 stdin 阻塞 / Popen 不重定向 stdio /
# taskkill 把自己杀了 / 关不掉管理员权限的旧实例导致安装回滚。
# 注意:需要管理员权限,且会真的往 Program Files 装东西。
$stageOut = & python (Join-Path $root "_build\test_installer_cycle.py") 2>&1
Show-Fails $stageOut
Record "安装包完整周期" ($LASTEXITCODE -eq 0)
# ---------------------------------------------------------------- 3b6
Banner "3b6/7 网页响应头(Content-Type / HEAD)"
# 踩过的坑:重做管理页时 _send() 没传 content_type,
# 默认成了 application/json —— 浏览器把整页 HTML 当纯文本显示,
# 用户看到的是一屏源码。
# 更糟的是当时没发现:只验了 HTTP 200 和字节数,两个都对。
# **200 不等于页面能看。**
$stageOut = & python (Join-Path $root "_build\test_response_headers.py") 2>&1
Show-Fails $stageOut
Record "网页响应头" ($LASTEXITCODE -eq 0)
# ---------------------------------------------------------------- 3b7
Banner "3b7/7 对外材料清洁度(不许出现管理后台)"
# 用户规矩:对外公开的内容里只能提官网,不能提中转管理后台。
# 文档没人看,所以把规矩变成会红的断言。
$stageOut = & python (Join-Path $root "_build\test_public_clean.py") 2>&1
Show-Fails $stageOut
Record "对外材料清洁度" ($LASTEXITCODE -eq 0)
# ---------------------------------------------------------------- 3c
Banner "3c/7 中转管理端批量删除"
$stageOut = & python (Join-Path $root "_tests\test_admin_batch_delete.py") 2>&1
Show-Fails $stageOut
Record "中转批量删除" ($LASTEXITCODE -eq 0)
# ---------------------------------------------------------------- 3c2
Banner "3c2/7 中转删除设备(含孤儿绑定自愈)"
# 这个测试原来**不在套件里**(僵尸),所以一直是红的没人知道:
# * 它抓的是 get("/"),而管理页早就搬到 /zhongzhuanzhan 了
# -> 后面三条"找不到删除按钮"
# * 它断言"孤儿绑定会挡着配对码",但那个行为已经被**有意修好**了
# (snapshot() 现在会把指向已不存在设备的码当废码扔掉并落盘)
# -> 用户报过的"设备下线了码还占着"问题已经不需要手动清场
# 修好之后 31/31 通过,收进套件。顺带把「孤儿自愈」也纳入回归。
$stageOut = & python (Join-Path $root "_tests\test_admin_delete.py") 2>&1
Show-Fails $stageOut
Record "中转删除设备" ($LASTEXITCODE -eq 0)
# ---------------------------------------------------------------- 3d
Banner "3d/7 管理页 JS 合法性(防幽灵按键)"
# 管理页的 JS 是拼出来的,字符串里混进真换行会让整段脚本报错,
# 按钮点了没反应。这个阶段逐字符扫描验证。
$stageOut = & python (Join-Path $root "_tests\test_admin_js.py") 2>&1
Show-Fails $stageOut
Record "管理页 JS 合法性" ($LASTEXITCODE -eq 0)
# ---------------------------------------------------------------- 3e
Banner "3e/7 管理页设备控制(接入开关 + 改配对码)"
# 两个功能各自最容易写错的地方:
# * 判断"新设备"必须按 deviceId,不能按配对码 ——
# 否则管理员给老设备改过码之后,老设备会被开关挡在外面
# * 改码按钮不能带 data-device ——
# 现有"单台删除"按 button.danger[data-device] 绑,
# 带上就会被当成删除按钮,点改码变删设备
$stageOut = & python (Join-Path $root "_tests\test_admin_device_control.py") 2>&1
Show-Fails $stageOut
Record "管理页设备控制" ($LASTEXITCODE -eq 0)
# ---------------------------------------------------------------- 4b
Banner "4b/7 手表 Activity 骨架(防白屏)"
# onCreate 被删掉过一次:照样编译通过,但打开就是白屏。
# 这一阶段专门断言生命周期骨架完整。
$stageOut = & python (Join-Path $root "_tests\test_watch_skeleton.py") 2>&1
Show-Fails $stageOut
Record "手表 Activity 骨架" ($LASTEXITCODE -eq 0)
# ---------------------------------------------------------------- 4c
Banner "4c/7 编译整个手表 App(含界面代码)"
# 这一步不能省:只编协议层的话,MainActivity.java 里的错误
# 根本检查不到 —— 改界面时删坏过字段声明,协议层照样通过,
# 回归全绿,结果 APK 编不出来。用 android.jar 做 classpath 编全量。
if (-not (Test-Path $androidJar)) {
Write-Host "找不到 android.jar: $androidJar" -ForegroundColor Red
Record "手表 App 整体编译" $false
} else {
$appOut = Join-Path $root "_tests\out_app"
if (Test-Path $appOut) { Remove-Item -Recurse -Force $appOut }
New-Item -ItemType Directory -Force -Path $appOut | Out-Null
$appSrcs = Get-ChildItem (Join-Path $root "android\src") -Recurse -Filter "*.java" |
ForEach-Object { $_.FullName }
$env:JAVA_TOOL_OPTIONS = "-Duser.language=en -Duser.country=US"
& $javac --release 8 -encoding UTF-8 -cp $androidJar -d $appOut $appSrcs 2>&1 |
Select-String -Pattern "error" | Select-Object -First 8 | ForEach-Object { Write-Host " $_" }
$appOk = ($LASTEXITCODE -eq 0)
$env:JAVA_TOOL_OPTIONS = ""
Write-Host $(if ($appOk) { " App 编译通过" } else { " App 编译失败" })
Record "手表 App 整体编译" $appOk
}
# ---------------------------------------------------------------- 5
Banner "5/7 编译 Java 协议层"
if (Test-Path $out) { Remove-Item -Recurse -Force $out }
New-Item -ItemType Directory -Force -Path $out | Out-Null
# Transport / RelayTransport 是跨网络用的:把"怎么传字节"抽出来,
# 这样 RemoteClient 的协议逻辑不用改就能走中转。
& $javac --release 8 -encoding UTF-8 -d $out `
(Join-Path $root "android\src\com\lanremote\watch\Json.java") `
(Join-Path $root "android\src\com\lanremote\watch\B64.java") `
(Join-Path $root "android\src\com\lanremote\watch\DiscoveryCore.java") `
(Join-Path $root "android\src\com\lanremote\watch\Transport.java") `
(Join-Path $root "android\src\com\lanremote\watch\RelayTransport.java") `
(Join-Path $root "android\src\com\lanremote\watch\TlsCompat.java") `
(Join-Path $root "android\src\com\lanremote\watch\RemoteClient.java") `
(Join-Path $root "_tests\java\JavaClientTest.java") 2>&1 | Out-Null
$compiled = ($LASTEXITCODE -eq 0)
Write-Host $(if ($compiled) { "编译通过" } else { "编译失败" })
Record "Java 协议层编译" $compiled
if (-not $compiled) { return }
# ---------------------------------------------------------------- 6
Banner "6/7 Java <-> 桩服务端 跨语言联调"
Stop-Strays
$stubData = Join-Path $root "_tests\_stubdata"
Remove-Item -Recurse -Force $stubData -ErrorAction SilentlyContinue
$codeFile = Join-Path $env:TEMP "lr_verify_code.txt"
Remove-Item -Force $codeFile -ErrorAction SilentlyContinue
$stub = Start-Process -FilePath "python" `
-ArgumentList "_tests\run_stub_server.py", "--port", "8799",
"--code-file", $codeFile, "--data-dir", $stubData `
-WorkingDirectory $root -PassThru -WindowStyle Hidden `
-RedirectStandardOutput (Join-Path $env:TEMP "lr_verify_stub.txt") `
-RedirectStandardError (Join-Path $env:TEMP "lr_verify_stub_err.txt")
$deadline = (Get-Date).AddSeconds(25)
while (-not (Test-Path $codeFile) -and (Get-Date) -lt $deadline) { Start-Sleep -Milliseconds 300 }
if (-not (Test-Path $codeFile)) {
Write-Host "桩服务端未就绪" -ForegroundColor Red
Get-Content (Join-Path $env:TEMP "lr_verify_stub_err.txt") -ErrorAction SilentlyContinue
Stop-Process -Id $stub.Id -Force -ErrorAction SilentlyContinue
Record "Java <-> 桩服务端" $false $true
} else {
# 先把退出码存下来再走管道。
# 直接 & java ... | Select-Object 然后读 $LASTEXITCODE 是不可靠的 ——
# 实测测试明明全过了,Record 却记成 FAIL(管道会干扰 $LASTEXITCODE)。
$javaOut = & $java "-Dfile.encoding=UTF-8" -cp $out JavaClientTest "127.0.0.1" 8799 "246813" "stub" 2>&1
$javaCode = $LASTEXITCODE
$javaOut | Select-Object -Last 4
Record "Java <-> 桩服务端" ($javaCode -eq 0) $true
Stop-Process -Id $stub.Id -Force -ErrorAction SilentlyContinue
}
# ---------------------------------------------------------------- 7
Banner "7/7 Java <-> 打包后的 EXE 端到端联调"
Stop-Strays
if (-not (Test-Path $exe)) {
Write-Host "未找到打包后的 EXE: $exe" -ForegroundColor Yellow
Write-Host "请先运行: powershell -File windows\build_exe.ps1" -ForegroundColor Yellow
Record "Java <-> 打包 EXE" $false
} else {
$appDir = Split-Path -Parent $exe
Remove-Item -Recurse -Force (Join-Path $appDir "data"), (Join-Path $appDir "received") -ErrorAction SilentlyContinue
# --add-device:静默运行 + 开放配对(默认流程会弹设置窗口,测试会卡住)
$proc = Start-Process -FilePath $exe -ArgumentList "--port", "8801", "--add-device", "10" -PassThru
Start-Sleep -Seconds 7
$settings = Join-Path $appDir "data\settings.json"
$deadline = (Get-Date).AddSeconds(25)
while (-not (Test-Path $settings) -and (Get-Date) -lt $deadline) { Start-Sleep -Milliseconds 400 }
if (-not (Test-Path $settings)) {
Write-Host "EXE 未生成配置,启动失败" -ForegroundColor Red
Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue
Record "Java <-> 打包 EXE" $false
} else {
$cfg = Get-Content $settings -Raw | ConvertFrom-Json
$cfg.image_timeout = 4
$cfg | ConvertTo-Json | Set-Content $settings -Encoding UTF8
Write-Host "EXE 配对码: $($cfg.pairing_code)"
# 同上:先存退出码,别让管道把它冲掉
$javaOut = & $java "-Dfile.encoding=UTF-8" -cp $out JavaClientTest "127.0.0.1" 8801 $cfg.pairing_code "real" 2>&1
$javaCode = $LASTEXITCODE
$javaOut | Select-Object -Last 4
Record "Java <-> 打包 EXE" ($javaCode -eq 0) $true
Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue
Start-Sleep -Seconds 1
Write-Host "`nEXE 接收到的文件:"
Get-ChildItem (Join-Path $appDir "received") -ErrorAction SilentlyContinue |
ForEach-Object { Write-Host (" {0,8:N0} B {1}" -f $_.Length, $_.Name) }
}
}
Stop-Strays
# ---------------------------------------------------------------- 汇总
# 收尾:把测试过程中弹出来的窗口关掉(用户要求)
Close-TestWindows
Banner "验证汇总"
$script:results | Format-Table -AutoSize
$failed = ($script:results | Where-Object { $_.结果 -eq "FAIL" }).Count
if ($failed -eq 0) {
Write-Host "全部通过" -ForegroundColor Green
} else {
Write-Host "$failed 项失败" -ForegroundColor Red
}
exit $failed