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:
1 parent
d7a9ae15a5
commit
9aefb6a5b0
5 files changed
+191
-114
No files matched your search
+29
-101
@@ -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):
|
||||
|
||||
Reference in new issue
Block a user