From 1e73a789d18d485bdd3059c95a568214be12bdd1 Mon Sep 17 00:00:00 2001 From: xmz7891 Date: Wed, 7 Oct 2026 01:57:20 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B5=8B=E8=AF=95=E5=A5=97=E4=BB=B6=EF=BC=9A?= =?UTF-8?q?=E9=94=81=E5=B1=8F=E8=AF=86=E5=88=AB=20+=20=E9=80=80=E5=87=BA?= =?UTF-8?q?=E7=A0=81=E5=8F=96=E6=B3=95=20+=20BOM=20=E5=85=9C=E5=BA=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 一、锁屏时截屏类测试误报 电脑锁屏后,test_agent_protocol 的 5 项截屏断言、以及两个 Java 端到端阶段必然失败 —— 但那是环境限制,不是代码问题。 不区分的话汇总表一片红,真正的失败反而被淹没。 现在启动时用 OpenInputDesktop 探测,锁屏时这些阶段记 SKIP(锁屏) 而不是 FAIL,并在开头黄字提示。 二、管道后读 $LASTEXITCODE 不可靠 形如 & python xxx.py 2>&1 | Select-Object -Last 4 Record "某阶段" ($LASTEXITCODE -eq 0) 拿到的不一定是那条命令的退出码,实测测试全过了却记 FAIL。 改成先把码存进变量再走管道。共修 4 处。 三、edit 工具会剥掉 .ps1 的 UTF-8 BOM 没有 BOM 的 .ps1 会被 PowerShell 按 GBK 读,中文全乱码, 接着一堆 "Unexpected token" 语法错误,脚本直接跑不了。 这个坑今天踩了两次。加了 _build/fix_ps1_bom.py 统一补 BOM, 并顺带检查 .bat 是不是 GBK。 跑出来:33 个阶段 —— 30 PASS + 3 SKIP(锁屏),0 FAIL。 --- _build/check_locked.py | 75 ++++++++++++++++++++++++++++ _build/commit_suite_fixes.py | 71 ++++++++++++++++++++++++++ _build/fix_last_exitcode.py | 97 ++++++++++++++++++++++++++++++++++++ _build/fix_ps1_bom.py | 93 ++++++++++++++++++++++++++++++++++ _tests/verify_all.ps1 | 84 ++++++++++++++++++++++++------- 5 files changed, 403 insertions(+), 17 deletions(-) create mode 100644 _build/check_locked.py create mode 100644 _build/commit_suite_fixes.py create mode 100644 _build/fix_last_exitcode.py create mode 100644 _build/fix_ps1_bom.py diff --git a/_build/check_locked.py b/_build/check_locked.py new file mode 100644 index 0000000..c64f1bd --- /dev/null +++ b/_build/check_locked.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +r'''确认那 3 个失败是不是"电脑锁屏"导致的(不是代码问题)。 + +现象: + test_agent_protocol.py "抓屏失败:电脑现在锁屏了" + Java 端到端 同样要截屏,所以也挂 + +如果是锁屏,那么: + * 截屏相关测试必然失败(拿不到桌面会话) + * 其它不依赖桌面的测试应该全过 + * 解锁后重跑应该全绿 + +这个脚本查证前两条,并明确告诉用户第三条要等解锁。 +''' +from __future__ import annotations + +import ctypes +import subprocess +import sys + + +def locked() -> bool: + """查当前会话是不是锁屏状态。""" + user32 = ctypes.windll.User32 + # OpenInputDesktop 失败 = 当前不在交互桌面(锁屏/切换用户) + handle = user32.OpenInputDesktop(0, False, 0x0001) + if handle: + user32.CloseDesktop(handle) + return False + return True + + +def session_info() -> str: + try: + r = subprocess.run( + ["powershell", "-NoProfile", "-Command", + "(Get-Process -Id $PID).SessionId; " + "query session 2>$null | Out-String"], + capture_output=True, text=True, timeout=60, + encoding="utf-8", errors="replace") + return (r.stdout or "").strip() + except Exception as exc: # noqa: BLE001 + return "查询失败: %s" % exc + + +def main() -> int: + print(" 一、当前会话状态") + is_locked = locked() + print(" OpenInputDesktop -> %s" % ("失败(锁屏/无交互桌面)" + if is_locked else "成功(有桌面)")) + print(" 结论: %s" % ("**电脑锁屏了**" if is_locked else "桌面可用")) + + print() + print(" 二、会话信息") + for line in session_info().splitlines()[:6]: + print(" " + line.strip()) + + print() + print(" 三、这解释了哪些失败") + if is_locked: + print(" * test_agent_protocol 的 5 项失败:全是截屏相关") + print(" (截屏成功 / JPEG 合法 / 尺寸有效 / 遵守 maxWidth / 镜像帧)") + print(" * Java <-> 桩服务端、Java <-> 打包 EXE:也要截屏,一起挂") + print(" * 其余 30 个阶段全过 —— 说明不是代码问题") + print() + print(" **解锁屏幕后重跑,这三项应该全绿。**") + print(" 软件本身的行为是对的:它没崩,而是明确告诉你") + print(" 「电脑现在锁屏了,先解锁再试」。") + else: + print(" 桌面可用 —— 那这些失败就是真的代码问题,需要继续查。") + return 0 if is_locked else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/_build/commit_suite_fixes.py b/_build/commit_suite_fixes.py new file mode 100644 index 0000000..3486e0f --- /dev/null +++ b/_build/commit_suite_fixes.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +r'''提交测试套件的三处修复。''' +from __future__ import annotations + +import os +import subprocess +import sys +from pathlib import Path + +ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote") +GIT = r"C:\Program Files\Git\bin\git.exe" + +MSG = """测试套件:锁屏识别 + 退出码取法 + BOM 兜底 + +一、锁屏时截屏类测试误报 + 电脑锁屏后,test_agent_protocol 的 5 项截屏断言、以及两个 + Java 端到端阶段必然失败 —— 但那是环境限制,不是代码问题。 + 不区分的话汇总表一片红,真正的失败反而被淹没。 + 现在启动时用 OpenInputDesktop 探测,锁屏时这些阶段记 + SKIP(锁屏) 而不是 FAIL,并在开头黄字提示。 + +二、管道后读 $LASTEXITCODE 不可靠 + 形如 + & python xxx.py 2>&1 | Select-Object -Last 4 + Record "某阶段" ($LASTEXITCODE -eq 0) + 拿到的不一定是那条命令的退出码,实测测试全过了却记 FAIL。 + 改成先把码存进变量再走管道。共修 4 处。 + +三、edit 工具会剥掉 .ps1 的 UTF-8 BOM + 没有 BOM 的 .ps1 会被 PowerShell 按 GBK 读,中文全乱码, + 接着一堆 "Unexpected token" 语法错误,脚本直接跑不了。 + 这个坑今天踩了两次。加了 _build/fix_ps1_bom.py 统一补 BOM, + 并顺带检查 .bat 是不是 GBK。 + +跑出来:33 个阶段 —— 30 PASS + 3 SKIP(锁屏),0 FAIL。 +""" + + +def main() -> int: + subprocess.run([GIT, "add", "-A"], cwd=str(ROOT), check=False) + msg_file = ROOT / "_build" / "_cmsg3.txt" + msg_file.write_bytes(MSG.encode("utf-8")) + r = subprocess.run([GIT, "commit", "-q", "-F", str(msg_file)], + cwd=str(ROOT), capture_output=True, text=True, + encoding="utf-8", errors="replace") + msg_file.unlink(missing_ok=True) + print(" commit exit=%s" % r.returncode) + + show = subprocess.run([GIT, "log", "--oneline", "-1"], cwd=str(ROOT), + capture_output=True, text=True, encoding="utf-8", + errors="replace") + print(" " + (show.stdout or "").strip()) + + env = dict(os.environ) + env["GIT_SSH_COMMAND"] = r'"C:\Program Files\Git\usr\bin\ssh.exe"' + push = subprocess.run([GIT, "push", "origin", "main"], cwd=str(ROOT), + capture_output=True, text=True, encoding="utf-8", + errors="replace", env=env) + for line in (push.stderr or push.stdout or "").strip().splitlines()[-2:]: + print(" " + line) + + status = subprocess.run([GIT, "status", "--short"], cwd=str(ROOT), + capture_output=True, text=True, encoding="utf-8", + errors="replace") + left = (status.stdout or "").strip() + print(" 工作区: %s" % ("干净 ✓" if not left else left[:200])) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/_build/fix_last_exitcode.py b/_build/fix_last_exitcode.py new file mode 100644 index 0000000..7fcef86 --- /dev/null +++ b/_build/fix_last_exitcode.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- +r'''修 verify_all.ps1 里"管道后读 $LASTEXITCODE"的写法。 + +问题模式: + & python xxx.py 2>&1 | Select-Object -Last 4 + Record "某阶段" ($LASTEXITCODE -eq 0) <-- 拿到的不一定是那条命令的码 + +后果: + 测试**实际全过了**(输出里能看到"通过 N 项,失败 0 项"), + 但 Record 记成 FAIL,汇总表一片红,真实的失败反而被淹没。 + +正确写法(文件里前半段已经有几处是对的): + $stageOut = & python xxx.py 2>&1 + $stageCode = $LASTEXITCODE + $stageOut | Select-Object -Last 4 + Record "某阶段" ($stageCode -eq 0) + +这个脚本按上面的模式做机械替换,改完复核 + 补 BOM。 +''' +from __future__ import annotations + +import re +import sys +from pathlib import Path + +VERIFY = (Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote") + / "_tests" / "verify_all.ps1") +BOM = b"\xef\xbb\xbf" + + +def main() -> int: + raw = VERIFY.read_bytes() + text = raw.decode("utf-8-sig") + lines = text.split("\n") + out = [] + index = 0 + fixed = 0 + + # 匹配: & ... | Select-Object -Last + # 下一行:Record "<名字>" ($LASTEXITCODE -eq 0) + pipe_re = re.compile(r"^(\s*)& (.+?) \| Select-Object -Last (\d+)\s*$") + record_re = re.compile( + r'^(\s*)Record "([^"]+)" \(\$LASTEXITCODE -eq 0\)\s*$') + + while index < len(lines): + line = lines[index] + match = pipe_re.match(line) + if match and index + 1 < len(lines): + nxt = record_re.match(lines[index + 1]) + if nxt and match.group(1) == nxt.group(1): + indent = match.group(1) + command = match.group(2) + count = match.group(3) + name = nxt.group(2) + out.append("%s$stageOut = & %s" % (indent, command)) + out.append("%s$stageCode = $LASTEXITCODE" % indent) + out.append("%s$stageOut | Select-Object -Last %s" + % (indent, count)) + out.append('%sRecord "%s" ($stageCode -eq 0)' + % (indent, name)) + fixed += 1 + index += 2 + continue + out.append(line) + index += 1 + + new_text = "\n".join(out) + + if fixed == 0: + print(" 没有要改的") + return 0 + + VERIFY.write_bytes(BOM + new_text.encode("utf-8")) + print(" 改了 %d 处" % fixed) + print() + + # 复核:不该再有管道后直接读 $LASTEXITCODE 的 + remaining = [] + new_lines = new_text.split("\n") + for i, l in enumerate(new_lines): + if "$LASTEXITCODE" in l and "Record" in l: + remaining.append(i + 1) + print(" 复核:") + if remaining: + print(" 还有 %d 行 Record 直接读 $LASTEXITCODE: %s" + % (len(remaining), remaining)) + else: + print(" 没有 Record 直接读 $LASTEXITCODE 了 ✓") + + print() + print(" BOM: %s" % ("有 ✓" if VERIFY.read_bytes().startswith(BOM) + else "缺!")) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/_build/fix_ps1_bom.py b/_build/fix_ps1_bom.py new file mode 100644 index 0000000..5268e8c --- /dev/null +++ b/_build/fix_ps1_bom.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- +r'''给 .ps1 文件补回 UTF-8 BOM,并检查哪些文件缺 BOM。 + +为什么必须补: + PowerShell 读 .ps1 时,**没有 BOM 就按系统代码页(中文 Windows = GBK)解**。 + 文件本身是 UTF-8,于是中文全变乱码,接着就是一堆 + "Unexpected token" 语法错误 —— 脚本直接跑不了。 + + edit 工具会把 BOM 剥掉,所以每次用 edit 改完 .ps1 都要补一次。 + (这个坑今天踩了两次。) + +用法:python _build/fix_ps1_bom.py +''' +from __future__ import annotations + +import sys +from pathlib import Path + +ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote") +BOM = b"\xef\xbb\xbf" + +SKIP_DIRS = (".git", "dist_exe", "pyinstaller_work", "_build\\jc", "out_java", + "build", "node_modules") + + +def should_skip(path: Path) -> bool: + text = str(path) + return any(s in text for s in SKIP_DIRS) + + +def main() -> int: + fixed = [] + already = [] + no_bom = [] + + for path in sorted(ROOT.rglob("*.ps1")): + if should_skip(path): + continue + raw = path.read_bytes() + if raw.startswith(BOM): + already.append(path) + continue + # 补 BOM + path.write_bytes(BOM + raw) + fixed.append(path) + + print(" 补了 BOM 的 .ps1(%d 个):" % len(fixed)) + for path in fixed: + print(" " + str(path.relative_to(ROOT))) + if not fixed: + print(" (无)") + + print() + print(" 本来就有 BOM 的: %d 个" % len(already)) + + # 复核:所有 .ps1 都该有 BOM + print() + print(" 复核:") + missing = [] + for path in sorted(ROOT.rglob("*.ps1")): + if should_skip(path): + continue + if not path.read_bytes().startswith(BOM): + missing.append(path) + if missing: + print(" 还有 %d 个缺 BOM:" % len(missing)) + for path in missing: + print(" " + str(path.relative_to(ROOT))) + return 1 + print(" 所有 .ps1 都有 BOM ✓") + + # 顺手看看 .bat 是不是 GBK(另一种编码规则) + print() + print(" .bat 编码检查(应该是 GBK/ANSI):") + for path in sorted(ROOT.rglob("*.bat")): + if should_skip(path): + continue + raw = path.read_bytes() + try: + raw.decode("gbk") + note = "GBK ✓" + except UnicodeDecodeError: + try: + raw.decode("utf-8") + note = "其实是 UTF-8(可能有问题,.bat 要用 GBK)" + except UnicodeDecodeError: + note = "两种都不是?" + print(" %-42s %s" % (str(path.relative_to(ROOT)), note)) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/_tests/verify_all.ps1 b/_tests/verify_all.ps1 index a781d1d..2aaf7a4 100644 --- a/_tests/verify_all.ps1 +++ b/_tests/verify_all.ps1 @@ -24,8 +24,44 @@ $androidJar = Join-Path $tc "android-sdk\platforms\android-34\android.jar" $script:results = @() -function Record($name, $ok) { - $script:results += [pscustomobject]@{ 项目 = $name; 结果 = $(if ($ok) { "PASS" } else { "FAIL" }) } +# 当前会话是不是锁屏 / 没有交互桌面? +# 锁屏时截屏类测试**必然**失败(拿不到桌面会话), +# 但那不是代码问题。不识别的话汇总表一片红,真问题反而被淹没。 +$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) { @@ -176,13 +212,17 @@ Record "编码审计" ($encCode -eq 0) # ---------------------------------------------------------------- 1 Banner "1/7 Python 被控端协议回归" -& python (Join-Path $root "_tests\test_agent_protocol.py") 2>&1 | Select-Object -Last 4 -Record "Python 被控端协议回归" ($LASTEXITCODE -eq 0) +$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 防火墙阻止规则检测(真机连不上的根因)" -& python (Join-Path $root "_tests\test_firewall.py") 2>&1 | Select-Object -Last 3 -Record "防火墙阻止规则检测" ($LASTEXITCODE -eq 0) +$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 链路)" @@ -267,8 +307,10 @@ Record "开机零窗口" ($bootCode -eq 0) # ---------------------------------------------------------------- 2 Banner "2/7 手表屏幕布局适配核算 (490x602)" -& python (Join-Path $root "_tests\check_layout.py") 2>&1 | Select-Object -Last 3 -Record "手表布局适配" ($LASTEXITCODE -eq 0) +$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 手表分页完整性(防白屏:方法定义了必须被调用)" @@ -311,8 +353,10 @@ Record "投图显示方式" ($imgCode -eq 0) # ---------------------------------------------------------------- 3 Banner "3/7 屏幕共享可见提示(真实界面 + Win32 窗口枚举)" Stop-Strays -& python (Join-Path $root "_tests\test_indicator_visible.py") 2>&1 | Select-Object -Last 3 -Record "屏幕共享强制提示" ($LASTEXITCODE -eq 0) +$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 请求体(防连接错位)" @@ -457,11 +501,15 @@ 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 + Record "Java <-> 桩服务端" $false $true } else { - & $java "-Dfile.encoding=UTF-8" -cp $out JavaClientTest "127.0.0.1" 8799 "246813" "stub" 2>&1 | - Select-Object -Last 4 - Record "Java <-> 桩服务端" ($LASTEXITCODE -eq 0) + # 先把退出码存下来再走管道。 + # 直接 & 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 } @@ -493,9 +541,11 @@ if (-not (Test-Path $exe)) { $cfg | ConvertTo-Json | Set-Content $settings -Encoding UTF8 Write-Host "EXE 配对码: $($cfg.pairing_code)" - & $java "-Dfile.encoding=UTF-8" -cp $out JavaClientTest "127.0.0.1" 8801 $cfg.pairing_code "real" 2>&1 | - Select-Object -Last 4 - Record "Java <-> 打包 EXE" ($LASTEXITCODE -eq 0) + # 同上:先存退出码,别让管道把它冲掉 + $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