fix(packaging): 堵两处判定假绿 + 证据账读数可复跑(M249 r1 P2-1/2/3)

P2-1 判据 2:`Selected: AdapterInfo {` 与字段不在同一行时(pretty-print Debug 漂移)
`device_type` 取不到、旧写法默认 `"?"` 而 `"?" ∉ {Cpu,Other}` ⇒ 判绿。改成取不到即判红。
P2-2 判据 1:`"wgpu" in renderers` 是「曾经出现过」,把 CPU 那次的 stderr >> 追加进来
再配 CPU 那张图会判绿。改成 `set(renderers) == {"wgpu"}`(全部 [shot] 行都声称 wgpu)。
P2-3 doc §2:A6 `--expect-refuse` 那行的「3/3」需要传 PNG 路径才成立,按表复跑是 2/2;
命令列补上 PNG,并单列「不传 PNG = 2/2」一行。

行数:判据脚本 383 → 311(自测脚手架搬到 tests/gpu_shot_selftest.py,124 行;向量
16 → 19 条,红 16 / 绿 3,新增「多行 Debug」「空体」「混入异种 renderer」三条靶子)。
This commit is contained in:
worker-gpushot committed 2026-11-11 13:13:36 +08:00
1 parent d7a9ae15a5
commit 9aefb6a5b0
5 files changed
+191 -114

No files matched your search

+29 -101
View File
@@ -25,15 +25,13 @@
python packaging/check_gpu_shot.py shot.stderr.log shot.png # 正例:判定,0/1 退出
python packaging/check_gpu_shot.py a6.stderr.log --expect-refuse # 负例:判定拒绝是否成立
python packaging/check_gpu_shot.py --self-test # 16 条向量(数据在 tests/)
python packaging/check_gpu_shot.py --self-test # 19 条向量(数据+脚手架都在 tests/)
"""
from __future__ import annotations
import json
import re
import sys
import tempfile
import zlib
from pathlib import Path
@@ -44,7 +42,6 @@ from check_screenshot import ( # noqa: E402
DEFAULT_MIN_UNIQUE,
PngError,
decode_png,
encode_png,
measure,
verdict,
)
@@ -122,20 +119,24 @@ def shot_size(text: str) -> tuple[int, int] | None:
return (int(match.group(1)), int(match.group(2))) if match else None
def _pick(pattern: re.Pattern[str], text: str, default: str = "?") -> str:
def _pick(pattern: re.Pattern[str], text: str, default: str | None = "?") -> str | None:
match = pattern.search(text)
return match.group(1) if match else default
def selected_adapter(text: str) -> dict[str, str] | None:
"""只取 `Selected:` 那一行:`Available adapters: [...]` 里有 12 条 `AdapterInfo`,取错就全错。"""
def selected_adapter(text: str) -> dict[str, str | None] | None:
"""只取 `Selected:` 那一行:`Available adapters: [...]` 里有 12 条 `AdapterInfo`,取错就全错。
`device_type` 取不到时返回 `None`(**不是** `"?"`):一旦 iced 改成 pretty-print Debug,
字段就落不到 `{` 同一行,此时按「取不到 = 判红」处理,不许静默给绿(M249 r1 P2-1)。
"""
match = SELECTED_RE.search(text)
if match is None:
return None
body = match.group("body")
return {
"name": _pick(ADAPTER_NAME_RE, body),
"device_type": _pick(DEVICE_TYPE_RE, body),
"device_type": _pick(DEVICE_TYPE_RE, body, None),
"backend": _pick(ADAPTER_BACKEND_RE, body),
}
@@ -170,9 +171,11 @@ def judge_evidence(log_path: Path, png_path: Path) -> Report:
return report
renderers = list(dict.fromkeys(shot_renderers(text)))
# 「这张图是 wgpu 出的」= 日志里**每一行** `[shot] … renderer=` 都说 wgpu。用 `in` 就退化成
# 「曾经出现过」:把 CPU 那次的 stderr `>>` 追加进来、配 CPU 那张图,会静默判绿(M249 r1 P2-2)。
report.add(
1,
"wgpu" in renderers,
set(renderers) == {"wgpu"},
"实际 renderer=" + "/".join(renderers) if renderers else "日志里没有 `[shot] … renderer=…` 行(没走到出图那一步?)",
)
@@ -180,10 +183,15 @@ def judge_evidence(log_path: Path, png_path: Path) -> Report:
if adapter is None:
report.add(2, False, "日志里没有 `Selected: AdapterInfo { … }`(wgpu 没建成设备,或只打印了 Available adapters)")
else:
device_type = adapter["device_type"]
# `None` = 字段不在 `{` 同一行(多行 Debug 漂移)或字段缺失:按取不到即失败处理,
# 宁可假红也不许「格式一变就静默给绿」。
report.add(
2,
adapter["device_type"] not in DISALLOWED_DEVICE_TYPES,
f'device_type={adapter["device_type"]}({adapter["name"]} / {adapter["backend"]})',
device_type not in (None, "?") and device_type not in DISALLOWED_DEVICE_TYPES,
f'device_type={device_type}({adapter["name"]} / {adapter["backend"]})'
if device_type not in (None, "?")
else "取不到 device_type:`Selected: AdapterInfo {` 与字段不在同一行(多行 Debug),或字段缺失",
)
width, height, stats = decode_shot(png_path, report)
@@ -258,7 +266,10 @@ def main(argv: list[str]) -> int:
if arg == "--expect-refuse":
expect_refuse = True
elif arg == "--self-test":
return self_test()
sys.path.insert(0, str(Path(__file__).resolve().parent / "tests"))
from gpu_shot_selftest import run_selftest # 延迟 import:脚手架反过来要拿本模块的判据函数
return run_selftest(judge_evidence, judge_refuse)
elif arg in ("-h", "--help"):
print(__doc__)
return 0
@@ -285,96 +296,13 @@ def main(argv: list[str]) -> int:
return 1 if report.problems() else 0
# ────────────────────────── 自测:合成日志 + 合成 PNG ──────────────────────────
# ────────────────────────── 自测 ──────────────────────────
#
# 向量在 `packaging/tests/gpu_shot_vectors.json`(数据与代码分开,同 guard_vectors.json 的做法),
# 每条的 `expect_fail` 是**期望的失败判据清单**,逐条对:关掉一条判据、或让某条判据被别的东西
# 掩盖,清单就对不上、本节报红。真图不在仓库里(`DevDocs/` 被 ignore),所以这颗钉子不能指望
# 真实产物——它守的是判据本身,真实产物由报告里的实跑退出码守。
VECTORS_PATH = Path(__file__).resolve().parent / "tests" / "gpu_shot_vectors.json"
def synth_log(
*,
renderer: str | None = "wgpu",
device_type: str | None = "DiscreteGpu",
backend: str | None = "final=wgpu result=started",
size: tuple[int, int] | None = (200, 120),
) -> 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 device_type is not None:
lines.append(
'[gpu] iced_wgpu::window::compositor: Selected: AdapterInfo { name: "AMD Radeon RX 6600 XT",'
' vendor: 4098, device: 29695, device_type: %s, driver: "AMD proprietary driver",'
' driver_info: "22.8.2", backend: Vulkan, }' % device_type
)
if size is not None:
lines.append(f"[shot] {size[0]}x{size[1]} scale=1 -> selftest.png")
if renderer is not None:
lines.append(f'[shot] screen=gallery theme=light renderer={renderer} 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]) -> Report:
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))
return judge_refuse(log_path, png_path) if case.get("mode") == "refuse" else judge_evidence(log_path, png_path)
def self_test() -> int:
"""跑 `tests/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).problems()
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
# 向量数据在 `packaging/tests/gpu_shot_vectors.json`,造样本与逐条比对的脚手架在
# `packaging/tests/gpu_shot_selftest.py`(判据脚本自己守 ≤400 行,脚手架不占这里的行数)。
# 每条的 `expect_fail` 是**期望的失败判据清单**,逐条对:关掉一条判据、或让某条判据被别的
# 东西掩盖,清单就对不上。真图不在仓库里(`DevDocs/` 被 ignore),所以这颗钉子守判据,真实
# 产物由报告里的实跑退出码守。
if __name__ == "__main__":
for stream in (sys.stdout, sys.stderr):