Files
Unlucky-Launcher/packaging/tests/gpu_shot_selftest.py
T
AskaEth a5d1dec495
CI / fmt (rustfmt --check) (push) Waiting to run
CI / clippy (ubuntu-latest) (push) Waiting to run
CI / clippy (windows-latest) (push) Waiting to run
CI / test (ubuntu-latest) (push) Waiting to run
CI / test (windows-latest) (push) Waiting to run
CI / build release (ubuntu-latest) (push) Waiting to run
CI / build release (windows-latest) (push) Waiting to run
CI / runtime smoke (ubuntu x64, xvfb) (push) Waiting to run
CI / runtime smoke (macos arm64, probe) (push) Skipped
CI / update-e2e (windows x64) (push) Waiting to run
GPU evidence / GPU 判据自测(不需 GPU;跳过 ≠ 已验证) (push) Waiting to run
GPU evidence / GPU 证据 — 探测可用产物与硬件适配器 (push) Waiting to run
Guards / core 零 UI 守门 (push) Waiting to run
Guards / presenter 零 UI 守门 (push) Waiting to run
Guards / design 零第三方依赖守门 (push) Waiting to run
Guards / unsafe 落点守门(集合必须恰好等于期望) (push) Waiting to run
Guards / 文件行数 ≤400 守门 (push) Waiting to run
Guards / 依赖许可与来源(cargo-deny) (push) Waiting to run
Release guard / 发布标记语义守门(push tag → 稳定版) (push) Waiting to run
Signing status(播报,不是门禁) / 判据自测(三行/退出码/变异重放/接线) (push) Waiting to run
Signing status(播报,不是门禁) / 签名就绪度(播报) (push) Waiting to run
Release / 选槽位(默认四个:Windows 两档 + Linux 两档) (push) Waiting to run
release: v0.2.4 里程碑快照(孤儿提交,无父;树 = dev 68362e5)
本批内容见 CHANGELOG.md 的 0.2.4 段。按 D41 用孤儿快照推 main,保留「公开史只有里程碑」的模型。
2026-11-11 21:41:38 +08:00

125 lines
6.1 KiB
Python
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.
#!/usr/bin/env python3
"""`packaging/check_gpu_shot.py --self-test` 的执行器(M249):合成日志 + 合成 PNG + 向量比对。
为什么单独一份而不是塞进判据脚本:那一份要守 ≤400 行硬线,而这里全是「按真实行格式手拼日志、
现造 PNG、逐条对 `expect_fail`」的**测试脚手架**,与判据逻辑无关。数据另外放
`gpu_shot_vectors.json`(同 `guard_vectors.json` 的做法),被测的判据函数由调用方**以参数传入**
——本文件不反向 import `check_gpu_shot`,免得循环导入。
一颗诚实的钉子:本文件的样本是**按今天的日志格式**手拼的,所以它守**判据**,不守**格式漂移**。
产品真换了 Debug 形状时,靠的是判据里「取不到 device_type 就算失败」这类保守口径(见 doc §3)。
"""
from __future__ import annotations
import json
import sys
import tempfile
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) # `check_screenshot` 在 packaging/
from check_screenshot import encode_png # noqa: E402
#: 向量数据。`log` 是 `synth_log` 的关键字覆盖(缺省键取函数默认值,`null` = 该行不出现)。
VECTORS_PATH = Path(__file__).resolve().parent / "gpu_shot_vectors.json"
SELFTEST_SIZE = (200, 120)
def synth_log(
*,
renderer: str | None = "wgpu",
device_type: str | None = "DiscreteGpu",
backend: str | None = "final=wgpu result=started",
size: tuple[int, int] | None = None,
no_size_line: bool = False,
multiline_selected: bool = False,
empty_selected: bool = False,
extra_shot_renderer: str | None = None,
) -> str:
"""按 M246 真实日志的**行格式**拼一份最小日志(传 `None` = 该行故意不出现)。"""
lines = [
"[probe] renderer = wgpu reason=forced-gpu 兜底=tiny-skia(失败会先打印具名错误)",
# 真实日志里永远有这一行提示文本,且它含 `Selected: AdapterInfo` 字样 —— 留着它,
# 「只有提示、没有真 Selected 行」的向量才是在测真东西。
"[probe] 取证:出现 `[gpu] iced_wgpu: …Selected: AdapterInfo…` ⇒ GPU 真生效;出现 `[gpu] wgpu 初始化失败` ⇒ 已降级 CPU",
]
if empty_selected: # `Selected: AdapterInfo {` 之后再没有内容(字段全丢)
lines.append("[gpu] iced_wgpu::window::compositor: Selected: AdapterInfo {")
elif device_type is not None:
fields = [
' name: "AMD Radeon RX 6600 XT",',
f" device_type: {device_type},",
' backend: Vulkan,',
]
if multiline_selected: # pretty-print Debug:字段不在 `{` 那一行(P2-1 的形状)
lines.append("[gpu] iced_wgpu::window::compositor: Selected: AdapterInfo {")
lines.extend(fields)
lines.append("}")
else:
lines.append("[gpu] iced_wgpu::window::compositor: Selected: AdapterInfo {" + "".join(fields) + " }")
if not no_size_line:
dims = size or SELFTEST_SIZE
lines.append(f"[shot] {dims[0]}x{dims[1]} scale=1 -> selftest.png")
for value in (renderer, extra_shot_renderer):
if value is not None:
lines.append(f'[shot] screen=gallery theme=light renderer={value} route=Detail("survival")')
if backend is not None:
lines.append(f"[backend] {backend} elapsed=1000ms evidence=selftest")
return "\n".join(lines) + "\n"
def synth_png(kind: str, size: tuple[int, int]) -> bytes:
"""`content` = 有内容的合成帧(渐变 + 8px 棋盘格);`flat` = 全白。"""
width, height = size
def content_row(y: int) -> bytes:
return b"".join(
bytes((x * 255 // max(width - 1, 1), y * 255 // max(height - 1, 1), 40))
if ((x // 8) + (y // 8)) % 2 == 0
else b"\x14\x14\x14"
for x in range(width)
)
if kind == "flat":
return encode_png(width, height, lambda _y: b"\xff" * (width * 3))
return encode_png(width, height, content_row)
def run_vector(root: Path, index: int, case: dict, default_size: tuple[int, int], judges: tuple) -> list[str]:
"""跑一条向量,返回它实际红的判据清单。`judges` = (judge_evidence, judge_refuse)。"""
judge_evidence, judge_refuse = judges
overrides = {key: (tuple(value) if isinstance(value, list) else value) for key, value in case.get("log", {}).items()}
log_path = root / f"case{index:02d}.log"
log_path.write_text(synth_log(**{"size": default_size, **overrides}), encoding="utf-8")
png_path = root / f"case{index:02d}.png"
png_size = tuple(case.get("png_size", default_size))
if case.get("png") == "truncated":
png_path.write_bytes(synth_png("content", png_size)[:200])
elif case.get("png") is not None:
png_path.write_bytes(synth_png(case["png"], png_size))
report = judge_refuse(log_path, png_path) if case.get("mode") == "refuse" else judge_evidence(log_path, png_path)
return report.problems()
def run_selftest(judge_evidence, judge_refuse) -> int:
"""跑 `gpu_shot_vectors.json` 的全部向量,逐条对 `expect_fail`(多一条少一条都算失败)。"""
data = json.loads(VECTORS_PATH.read_text(encoding="utf-8"))
default_size = tuple(data["size"])
failures = 0
with tempfile.TemporaryDirectory(prefix="ull-gpu-shot-") as tmp:
root = Path(tmp)
for index, case in enumerate(data["cases"], start=1):
got = run_vector(root, index, case, default_size, (judge_evidence, judge_refuse))
want = case["expect_fail"]
if got != want:
print(f"::error::自测失败:{case['label']} 期望失败 {want},实际 {got}", file=sys.stderr)
failures += 1
else:
print(f"OK: {case['label']} → {'红 ' + ' / '.join(got) if got else '绿'}")
red = sum(1 for case in data["cases"] if case["expect_fail"])
if failures:
return 1
print(f"OK: GPU 截图证据判定自测通过({len(data['cases'])} 条向量:红 {red} / 绿 {len(data['cases']) - red})")
return 0