一、问题(本轮实测)
`draw-ui` 与 `oil-motion` 目录里**各自带一个内嵌 `.git`** ⇒ 上一次提交把它们记成了 **gitlink(子模块指针)**
⇒ 仓库里只存了一个不属于任何远端的 commit id,**别人克隆下来这两份是空的** ✗(`git status` 显示 ` m draw-ui` / ` m oil-motion` = 子模块内容有改动)。
二、处置(可回退)
· 把两处的 `.git` **挪走**(⛔ 不是删除)⇒ `归档/内嵌git-20261008/{draw-ui,oil-motion}.git`;
· `git rm --cached` 掉那两个 gitlink,再 `git add` 两个目录 ⇒ **按正常文件入库**(内容才真的进仓库)。
三、副作用(如实记)
挪走 `.git` 后,这两个技能**不能再原地 `git pull` 取上游更新**(要更新得重新拉一份覆盖);
如需恢复其本地仓库,把 `归档/内嵌git-20261008/` 里的 `.git` 挪回原处即可。
133 lines
6.8 KiB
Python
133 lines
6.8 KiB
Python
#!/usr/bin/env python3
|
||
"""验证已授权浏览器产生的截图与元数据,再运行像素对比;本脚本不启动浏览器。"""
|
||
import argparse
|
||
import json
|
||
from pathlib import Path
|
||
import subprocess
|
||
import sys
|
||
from urllib.parse import urldefrag, urlparse
|
||
from urllib.request import urlopen
|
||
from html.parser import HTMLParser
|
||
from PIL import Image
|
||
from prepare_capture import compare_geometry, write_geometry_markdown
|
||
|
||
|
||
def verify(manifest_path, out_dir, previous=None):
|
||
if out_dir.exists():
|
||
raise ValueError('输出目录已存在;使用新的轮次目录,拒绝覆盖')
|
||
data = json.loads(manifest_path.read_text(encoding='utf-8'))
|
||
base = manifest_path.parent
|
||
expected = data['expected']
|
||
capture = json.loads((base / data['capture_metadata']).read_text(encoding='utf-8'))
|
||
candidate = base / data['candidate']
|
||
reference = base / data['reference']
|
||
failures = []
|
||
def check(condition, message):
|
||
if not condition:
|
||
failures.append(message)
|
||
for key in ('url', 'title', 'viewport', 'dpr'):
|
||
if key not in capture or key not in expected:
|
||
raise ValueError(f'缺少必填字段:{key}')
|
||
check(bool(expected['title']) and capture['title'] == expected['title'], '页面标题不匹配')
|
||
check(bool(expected['url']) and urldefrag(capture['url'])[0] == urldefrag(expected['url'])[0], '页面URL不匹配')
|
||
check(capture['viewport'] == expected['viewport'], '实际视口不匹配')
|
||
check(capture['dpr'] == expected['dpr'] and capture['dpr'] > 0, '实际DPR不匹配')
|
||
check(capture.get('fonts_ready') is True, '字体尚未加载完成')
|
||
check(capture.get('images_ready') is True, '图片尚未加载完成')
|
||
check(capture.get('full_page') == expected['full_page'], '截图范围与约定不一致')
|
||
scale_mode = capture['screenshot_scale']
|
||
check(scale_mode == expected['screenshot_scale'] and scale_mode in ('css', 'device'), '截图像素缩放方式不匹配')
|
||
scale = capture['dpr'] if scale_mode == 'device' else 1
|
||
target_height = capture['document_height'] if capture['full_page'] else capture['viewport']['height']
|
||
with Image.open(candidate) as image:
|
||
check(abs(image.width - capture['viewport']['width']*scale) < 1, '截图宽度与元数据不符')
|
||
check(abs(image.height - target_height*scale) < 1, '截图高度与元数据不符')
|
||
for name, asset in data.get('expected_assets', {}).items():
|
||
actual = capture.get('assets', {}).get(name, {})
|
||
check(bool(asset) and all(actual.get(key) == value for key, value in asset.items()), f'资源版本或尺寸不匹配:{name}')
|
||
if failures:
|
||
raise ValueError(';'.join(failures))
|
||
geometry = None
|
||
if 'measurement_baseline' in data:
|
||
geometry = compare_geometry(data, capture, base, previous)
|
||
elif previous is not None:
|
||
raise ValueError('前后比较需要由测量基线生成的验收配置')
|
||
command = [sys.executable, str(Path(__file__).with_name('compare_mockup.py')),
|
||
'--reference', str(reference), '--candidate', str(candidate), '--out-dir', str(out_dir), '--prefix', 'verify']
|
||
if data.get('allow_height_difference'):
|
||
command.append('--allow-height-difference')
|
||
for clip in data.get('clips', []):
|
||
command.extend(['--clip', clip])
|
||
out_dir.mkdir(parents=True, exist_ok=False)
|
||
result = subprocess.run(command, capture_output=True, text=True)
|
||
if result.returncode:
|
||
raise ValueError(result.stderr.strip())
|
||
actual_sections = {section['name']: section for section in capture.get('sections', [])}
|
||
positions = []
|
||
for name, target_y in data.get('section_positions', {}).items():
|
||
actual = actual_sections.get(name)
|
||
positions.append({'name': name, 'expected_y': target_y, 'actual_y': actual['y'] if actual else None,
|
||
'delta_y': round(actual['y']-target_y, 3) if actual else None})
|
||
report = {'status': '截图身份与渲染前提通过;视觉仍需复核', 'manifest': str(manifest_path.resolve()),
|
||
'capture': capture, 'section_positions': positions,
|
||
'comparison': str(out_dir / 'verify-metrics.json')}
|
||
if geometry is not None:
|
||
report['geometry'] = geometry
|
||
write_geometry_markdown(out_dir, geometry)
|
||
if not geometry['complete']:
|
||
report['status'] = '截图前提通过,但元素采集不完整,需处理报告中的缺项'
|
||
(out_dir / 'verification.json').write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding='utf-8')
|
||
print(json.dumps(report, ensure_ascii=False, indent=2))
|
||
if geometry is not None and not geometry['complete']:
|
||
raise ValueError('元素采集不完整;诊断报告已保存')
|
||
|
||
|
||
def preflight(manifest_path):
|
||
expected = json.loads(manifest_path.read_text(encoding='utf-8'))['expected']
|
||
url = expected['url']
|
||
if urlparse(url).hostname not in ('127.0.0.1', 'localhost', '::1'):
|
||
raise ValueError('启动前检查仅用于本地页面')
|
||
class TitleParser(HTMLParser):
|
||
def __init__(self):
|
||
super().__init__()
|
||
self.inside = False
|
||
self.parts = []
|
||
def handle_starttag(self, tag, attrs):
|
||
if tag == 'title': self.inside = True
|
||
def handle_endtag(self, tag):
|
||
if tag == 'title': self.inside = False
|
||
def handle_data(self, text):
|
||
if self.inside: self.parts.append(text)
|
||
with urlopen(url, timeout=8) as response:
|
||
if urldefrag(response.geturl())[0] != urldefrag(url)[0]:
|
||
raise ValueError('页面重定向到其他URL')
|
||
html = response.read(2_000_000).decode('utf-8')
|
||
parser = TitleParser()
|
||
parser.feed(html)
|
||
actual = ''.join(parser.parts).strip()
|
||
if not expected['title'] or actual != expected['title']:
|
||
raise ValueError('本地HTML标题不匹配,禁止继续浏览器验证')
|
||
print(json.dumps({'status': '本地服务与页面身份检查通过', 'url': url, 'title': actual}, ensure_ascii=False))
|
||
|
||
|
||
def main():
|
||
parser = argparse.ArgumentParser(description=__doc__)
|
||
parser.add_argument('--manifest', type=Path, required=True)
|
||
parser.add_argument('--out-dir', type=Path)
|
||
parser.add_argument('--previous', type=Path, help='同一测量基线下前轮 verification.json,用于报告改善与退步')
|
||
parser.add_argument('--preflight', action='store_true', help='仅核对本地HTML服务与标题;成功后才能导航')
|
||
args = parser.parse_args()
|
||
try:
|
||
if args.preflight:
|
||
preflight(args.manifest)
|
||
elif args.out_dir is None:
|
||
parser.error('截图验收需要 --out-dir')
|
||
else:
|
||
verify(args.manifest, args.out_dir, args.previous)
|
||
except (ValueError, KeyError, OSError, TypeError) as error:
|
||
parser.exit(2, f'验证失败:{error}\n')
|
||
|
||
|
||
if __name__ == '__main__':
|
||
main()
|