Files
Robot_Arm/tests/verify.py
T

173 lines
6.3 KiB
Python
Raw Normal View History

# -*- coding: utf-8 -*-
"""重构期的不变量关卡。
python -m tests.verify # 数据哈希 + IK 数值(秒级,每步都跑)
python -m tests.verify --shots # 再加全套截图比对(约 3 分钟,前端改动后跑)
基线在 tests/baselines/ 下,随仓库走。产物按**文件名**匹配,不写死目录:
mesh 从 web/ 搬到 webapp/data/ 这类移动不该让关卡失效,也不该让我以为它通过了。
任何一项对不上就退出码 1。
"""
import hashlib
import io
import json
import os
import subprocess
import sys
sys.stdout.reconfigure(encoding="utf-8")
HERE = os.path.dirname(os.path.abspath(__file__))
REPO = os.path.dirname(HERE)
BASE = os.path.join(HERE, "baselines")
# 每个产物允许出现的目录,按优先级;找到第一个即取。
CANDIDATES = {
"mesh_arm.json": ["webapp/data", "web"],
"sheets.jsdata": ["webapp/data", "web"],
"travel.json": ["docs", "webapp/data"],
"travel_detail.json": ["docs", "webapp/data"],
"collide.json": ["docs", "webapp/data"],
"screws.json": ["docs"],
}
def md5(path):
h = hashlib.md5()
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(1 << 20), b""):
h.update(chunk)
return h.hexdigest()
def read_list(name):
"""基线文件是 'hash filename';md5sum 的二进制模式会在文件名前加 '*',剥掉。"""
out = {}
for ln in io.open(os.path.join(BASE, name), encoding="utf-8"):
ln = ln.strip()
if ln:
h, f = ln.split()
out[f.lstrip("*")] = h
return out
def locate(fname, dirs):
for d in dirs:
p = os.path.join(REPO, d, fname)
if os.path.exists(p):
return p
return None
def check_data():
base = read_list("data_by_name.md5")
bad, rows = 0, []
for fname, dirs in CANDIDATES.items():
want = base.get(fname)
if want is None:
rows.append(("SKIP", fname, "基线里没有这项"))
continue
p = locate(fname, dirs)
if p is None:
bad += 1
rows.append(("MISS", fname, "找不到文件(试过的目录: %s)" % ", ".join(dirs)))
continue
got = md5(p)
ok = got == want
if not ok:
bad += 1
rows.append(("OK " if ok else "DIFF", fname,
os.path.relpath(p, REPO).replace("\\", "/") + " " + got[:12] +
("" if ok else " 基线 " + want[:12])))
for st, f, msg in rows:
print(" [%s] %-20s %s" % (st, f, msg))
return bad
def check_ik():
r = subprocess.run(["node", os.path.join(HERE, "ik_baseline.mjs"),
"--compare", os.path.join(BASE, "ik_exact.json")],
capture_output=True, text=True, cwd=REPO,
encoding="utf-8", errors="replace")
out = r.stdout or ""
for ln in out.splitlines():
print(" " + ln)
if r.returncode != 0:
print(" node 退出码 %d: %s" % (r.returncode, (r.stderr or "").strip()[:300]))
# 没有 "== exact" 这一行就说明探针根本没跑出比对结果(编码炸了、node 不在、路径错…),
# 绝不能因为"没报差异"就算通过。
if "== exact" not in out:
print(" [MISS] ik_baseline 没有输出比对结果,判 FAIL")
return 1
return 0 if r.returncode == 0 else 1
def check_shots():
"""两层判定,依据是"同一构建连跑两轮"的实测(scratch/_gate/flaky_scan.py):
104 张逐字节可复现 -> md5 必须相同;
3 张线束 x-ray 不可复现(mean<=0.0017、无像素差>8,纯抗锯齿噪声)
-> 与 tests/baselines/flaky/ 里的参考像素比,超阈值才算回归。
把不可复现的也要求 md5 相同,等于给每一步埋假 FAIL,最后只会被人肉忽略。"""
import numpy as np
from PIL import Image
base = read_list("shots_stable.md5")
th = json.load(io.open(os.path.join(BASE, "flaky", "thresholds.json"), encoding="utf-8"))
r = subprocess.run([sys.executable, "-m", "arm.viewer.shot"],
capture_output=True, text=True, cwd=REPO,
encoding="utf-8", errors="replace")
out = r.stdout or ""
if "all shots ok" not in out and "OK " not in out:
print(" [MISS] shot.py 没有正常输出(退出码 %s),判 FAIL" % r.returncode)
return 999
bad = [l for l in out.splitlines() if "FAIL" in l]
if bad:
print(" [MISS] 截图失败 %d 个: %s" % (len(bad), bad[:3]))
fails = len(bad)
for fname, want in sorted(base.items()):
p = os.path.join(REPO, fname.replace("/", os.sep))
if not os.path.exists(p):
print(" [MISS] %s" % fname)
fails += 1
continue
if md5(p) != want:
print(" [DIFF] %-40s" % fname)
fails += 1
print(" 逐字节比对: %d 张稳定视图,%d 张不一致" % (len(base), fails - len(bad)))
tol_fails = 0
for name, ref in sorted(th["views"].items()):
p = os.path.join(REPO, "docs", "shots", name + ".png")
q = os.path.join(BASE, "flaky", name + ".png")
if not (os.path.exists(p) and os.path.exists(q)):
print(" [MISS] 容差比对缺文件: %s" % name)
tol_fails += 1
continue
d = np.abs(np.asarray(Image.open(p).convert("RGB"), dtype=np.int16)
- np.asarray(Image.open(q).convert("RGB"), dtype=np.int16))
mean, mx = float(d.mean()), int(d.max())
over = mean > th["mean_max"] or mx > th["pixel_max"]
tol_fails += 1 if over else 0
print(" [%s] %-34s mean=%.5f (<= %.2f) 最大单像素差=%d (<= %d)"
% ("DIFF" if over else "OK ", name, mean, th["mean_max"], mx, th["pixel_max"]))
print(" 容差比对: %d 张噪声视图,%d 张超阈值" % (len(th["views"]), tol_fails))
return fails + tol_fails
def main():
argv = sys.argv[1:]
fails = 0
print("== 数据产物哈希 ==")
fails += check_data()
print("== IK 数值基线 ==")
fails += check_ik()
if "--shots" in argv:
print("== 截图逐字节比对 ==")
fails += check_shots()
print("RESULT:", "PASS" if fails == 0 else "FAIL(%d 项不一致)" % fails)
return 1 if fails else 0
if __name__ == "__main__":
sys.exit(main())