|
- """Build the portable report from completed test evidence, without credentials."""
- import html
- import json
- import re
- import zipfile
- from pathlib import Path
-
- import markdown
- from markdown.extensions.toc import slugify_unicode
-
- ROOT = Path(__file__).resolve().parents[1]
- REPORT = ROOT / 'reports/review-20260905'
-
-
- def main():
- live = json.loads((REPORT / 'live/results.json').read_text(encoding='utf-8'))['stats']
- front = json.loads((REPORT / 'frontend-e2e/results.json').read_text(encoding='utf-8'))['stats']
- cleanup = json.loads((REPORT / 'cleanup.json').read_text(encoding='utf-8'))
- if any(value['unexpected'] or value['skipped'] or value['flaky'] for value in (live, front)):
- raise RuntimeError('Final browser evidence is not fully passing')
- if not all(cleanup['originalRowsUnchanged'].values()) or cleanup['remainingActiveTestUsers']:
- raise RuntimeError('Cleanup evidence is not passing')
- source = (REPORT / 'review-report.md').read_text(encoding='utf-8')
- source = source.replace('状态:最终浏览器复核中', '状态:审查与验证完成,4 项功能差异和 1 项素材选择待确认')
- summary = f'''| 验证 | 最终结果 | 证据与范围 |
- |---|---|---|
- | 后端全量测试 | 402/402 通过,0 失败、错误、跳过 | Gateway 1、Auth 86、Tran 315;含新增迟到事件回归 |
- | 后端构建 | 通过 | Java 17 / Maven;三个服务打包完成 |
- | 前端 Node 回归 | 25/25 通过 | 原 24 项加视频资产、候选与抓拍合同 |
- | 前端构建 | 通过 | vue-tsc、Vite、旧模型 iframe 校验 |
- | 真实服务 E2E(Edge) | {live['expected']}/4 通过,0 失败/跳过/重试 | {live['duration']/1000:.1f} 秒;开始时间 {live['startTime']}(UTC) |
- | 浏览器模拟接口回归 | {front['expected']}/15 通过,0 失败/跳过/重试 | {front['duration']/1000:.1f} 秒;编辑保存、状态组合、军工绿/暗色主题 |
- | 数据库迁移 | 通过 | 033、035;首次、重跑和指定中断点恢复;开发库检查值均为 0 |
- | 测试脚本类型检查、Git 空白检查 | 通过 | 两份专用 Playwright 测试与配置;API/Web/E2E 仓库 |
-
- 最终通过结果来自各自一次完整运行,未启用自动重试。补入三次完整场景加载与截图后,真实虚拟用例曾超过原 120 秒测试总预算,断言和评定已完成但截图收尾超时;保留 [该轮记录](diagnostics/live-resource-wait-timeout.json),随后将该用例预算调到 240 秒,并在其他浏览器回归结束后重新完整运行。单次资源就绪仍有 60 秒上限。
-
- 实装视频新增检查还发现本机 Playwright 自带 Chromium 不支持 H.264(`canPlayType` 返回空字符串);[失败轮次](diagnostics/live-chromium-h264-unsupported.json)与[Edge 播放探针](diagnostics/video-probe-msedge.json)保留。最终真实 E2E 使用已安装 Microsoft Edge,模拟接口回归继续使用 Chromium。另独立修复了实际素材与候选配置不匹配的 WEB-06;最终用例验证默认视频可播、片长覆盖末尾候选,并在结尾得到 12 条候选。测试开发阶段的权限夹具、请求拦截范围与 UI 选择器错误已修正,不计为产品缺陷。'''
- source = source.replace('<!-- FINAL_TEST_RESULTS -->', summary)
- cleared = ', '.join(f"{table.removeprefix('ut_teaching_')} {count}" for table, count in cleanup['deleted'].items() if count)
- source = source.replace('<!-- CLEANUP_RESULTS -->', f"最终清理时间:`{cleanup['time']}`。删除本轮临时任务及关联记录({cleared}),测试账号已停用/软删除,会话已撤销,临时管理员密码文件已移除。四张原始教学表的既有行逐行对照均未变化,剩余活动测试账号为 0。审计与软删除记录按系统机制保留。见 [最终清理记录](cleanup.json);较早轮次见 `diagnostics/cleanup-*.json`。")
- titles = {
- '01-login-three-roles': '真实服务:登录仅展示教员、学员、管理员',
- '02-virtual-formal-task-center': '真实服务:正式虚拟任务中心',
- '03-virtual-1920': '真实服务:1920 宽度任务中心',
- '04-teacher-content': '真实服务:教员训练编排列表已加载',
- '05-virtual-started-no-exam-overlay': '真实服务:开始训练、模型就绪,无错误考核遮罩',
- '06-virtual-all-steps-completed': '真实服务:8/8 步骤完成,真实模型已加载',
- '07-virtual-reviewed': '真实服务:提交评定后刷新',
- '08-physical-api-task-list': '真实服务:实装任务清单',
- '09-physical-offline-replay-boundary': '真实页面:离线视频就绪,并标明不推进正式进度、不计分',
- '10-physical-twelve-candidates': '真实页面:匹配视频到达末尾,12 条离线候选可达、等待复核',
- '01-saved-step-move': '模拟接口:移动已保存步骤后保持正确顺序',
- '01-saved-step-delete': '模拟接口:删除步骤后末步进度 100%',
- '01-saved-step-duplicate': '模拟接口:复制步骤后重新编号',
- '02-multichannel-virtual-save': '模拟接口:虚拟渠道配置保存与重开',
- '03-multichannel-confrontation-save': '模拟接口:对抗渠道配置保存与重开',
- '05-failed-before-retry': '模拟接口:失败步骤出现重试入口',
- '06-failed-after-retry': '模拟接口:重试后尝试 2 次、失败 1 次',
- '07-no-retry-SUBMITTED': '模拟接口:已提交不出现重试按钮',
- '07-no-retry-TERMINATED': '模拟接口:已终止不出现重试按钮',
- '08-visual-safety-military': '模拟接口:军工绿主题安全区,对比度 12.00',
- '08-visual-safety-dark': '模拟接口:暗色主题安全区,对比度 15.30',
- }
- gallery = []
- count = 0
- for folder, caption in [('live', '真实服务与页面'), ('frontend-e2e', '模拟接口浏览器回归')]:
- gallery.append(f'<h3>{caption}</h3><div class="gallery">')
- for picture in sorted((REPORT / folder / 'screenshots').glob('*.png')):
- if picture.name.startswith('failure-'):
- continue
- relative = picture.relative_to(REPORT).as_posix()
- title = titles.get(picture.stem, '模拟接口:关闭遮罩状态矩阵 ' + picture.stem.replace('04-overlay-', ''))
- gallery.append(f'<figure><a href="{relative}"><img loading="lazy" src="{relative}" alt="{html.escape(title)}"></a><figcaption>{html.escape(title)}</figcaption></figure>')
- count += 1
- gallery.append('</div>')
- source = source.replace('<!-- SCREENSHOT_GALLERY -->', f'共 {count} 张最终 E2E 截图。\n\n' + '\n'.join(gallery))
- (REPORT / 'review-report.md').write_text(source, encoding='utf-8')
- style = '''*{box-sizing:border-box}body{margin:0;background:#edf2f7;color:#203248;font:16px/1.75 "Microsoft YaHei","Segoe UI",sans-serif}header{background:#0c2b47;color:white;padding:22px max(24px,calc((100vw - 1160px)/2))}header a{color:#c1e6ff;margin-right:24px}main{max-width:1200px;margin:24px auto 60px;padding:36px;background:white;border-radius:12px;box-shadow:0 6px 24px #1028400c}h1{font-size:30px;line-height:1.4;margin-top:0}h2{font-size:22px;margin:36px 0 14px;border-bottom:2px solid #dbe8f2;padding-bottom:8px}h3{font-size:18px}p{margin:13px 0}a{color:#126fba;text-decoration:none}a:hover{text-decoration:underline}table{border-collapse:collapse;width:100%;margin:18px 0;font-size:14px;table-layout:auto}th,td{padding:11px 13px;border:1px solid #dce5ed;text-align:left;vertical-align:top;overflow-wrap:anywhere}th{background:#edf5fc}tr:nth-child(even){background:#f9fbfd}code{font:13px/1.5 Consolas,monospace;background:#edf2f7;padding:2px 5px;border-radius:4px;overflow-wrap:anywhere}pre{padding:18px;overflow:auto;background:#edf2f7;border-radius:8px}pre code{padding:0}img{max-width:100%;height:auto}details{padding:12px;background:#f7f9fc;margin:12px 0}.gallery{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:20px}figure{margin:0;border:1px solid #dce5ed;border-radius:8px;overflow:hidden;background:#f8fafc}figure img{display:block;width:100%;height:300px;object-fit:contain;background:#e8eef3}figcaption{padding:12px;font-size:14px;color:#37526b}footer{color:#657c91;font-size:13px;margin-top:36px}li{margin:8px 0}@media(max-width:760px){main{margin:0;padding:20px;border-radius:0}.gallery{grid-template-columns:1fr}table{display:block;overflow:auto}h1{font-size:25px}header{padding:16px}}@media print{body{background:white}main{margin:0;box-shadow:none;max-width:none}header{display:none}figure{break-inside:avoid}h2{break-after:avoid}}'''
- for item in REPORT.glob('*.md'):
- content = item.read_text(encoding='utf-8')
- content = re.sub(r'\(([^()]+)\.md(#[^()]*)?\)', lambda m: '(' + m[1] + '.html' + (m[2] or '') + ')' if (REPORT / (m[1] + '.md')).exists() else m[0], content)
- content = content.replace('<details>', '<details markdown="1">')
- body = markdown.markdown(content, extensions=['markdown.extensions.tables', 'markdown.extensions.fenced_code', 'markdown.extensions.md_in_html', 'markdown.extensions.toc'], extension_configs={'markdown.extensions.toc': {'slugify': slugify_unicode}})
- document = f'<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>{html.escape(item.stem)} · Code Review</title><style>{style}</style></head><body><header><a href="index.html">审查总报告</a><a href="live/html/index.html">真实 E2E</a><a href="frontend-e2e/html/index.html">模拟接口回归</a></header><main>{body}<footer>2026-09-05 · 开发环境审查 · 图片可点击查看原尺寸</footer></main></body></html>'
- item.with_suffix('.html').write_text(document, encoding='utf-8')
- if item.name == 'review-report.md':
- (REPORT / 'index.html').write_text(document, encoding='utf-8')
- archive = REPORT.parent / 'review-20260905.zip'
- with zipfile.ZipFile(archive, 'w', zipfile.ZIP_DEFLATED, compresslevel=6) as zipped:
- for item in REPORT.rglob('*'):
- if not item.is_file() or item.name.startswith('failure-') or 'test-results' in item.parts or 'results' in item.parts:
- continue
- zipped.write(item, item.relative_to(REPORT.parent).as_posix())
- print(json.dumps({'screenshots': count, 'live': live, 'frontend': front, 'archiveBytes': archive.stat().st_size}, ensure_ascii=False))
-
-
- if __name__ == '__main__':
- main()
|