Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 
 
 

100 строки
10 KiB

  1. """Build the portable report from completed test evidence, without credentials."""
  2. import html
  3. import json
  4. import re
  5. import zipfile
  6. from pathlib import Path
  7. import markdown
  8. from markdown.extensions.toc import slugify_unicode
  9. ROOT = Path(__file__).resolve().parents[1]
  10. REPORT = ROOT / 'reports/review-20260905'
  11. def main():
  12. live = json.loads((REPORT / 'live/results.json').read_text(encoding='utf-8'))['stats']
  13. front = json.loads((REPORT / 'frontend-e2e/results.json').read_text(encoding='utf-8'))['stats']
  14. cleanup = json.loads((REPORT / 'cleanup.json').read_text(encoding='utf-8'))
  15. if any(value['unexpected'] or value['skipped'] or value['flaky'] for value in (live, front)):
  16. raise RuntimeError('Final browser evidence is not fully passing')
  17. if not all(cleanup['originalRowsUnchanged'].values()) or cleanup['remainingActiveTestUsers']:
  18. raise RuntimeError('Cleanup evidence is not passing')
  19. source = (REPORT / 'review-report.md').read_text(encoding='utf-8')
  20. source = source.replace('状态:最终浏览器复核中', '状态:审查与验证完成,4 项功能差异和 1 项素材选择待确认')
  21. summary = f'''| 验证 | 最终结果 | 证据与范围 |
  22. |---|---|---|
  23. | 后端全量测试 | 402/402 通过,0 失败、错误、跳过 | Gateway 1、Auth 86、Tran 315;含新增迟到事件回归 |
  24. | 后端构建 | 通过 | Java 17 / Maven;三个服务打包完成 |
  25. | 前端 Node 回归 | 25/25 通过 | 原 24 项加视频资产、候选与抓拍合同 |
  26. | 前端构建 | 通过 | vue-tsc、Vite、旧模型 iframe 校验 |
  27. | 真实服务 E2E(Edge) | {live['expected']}/4 通过,0 失败/跳过/重试 | {live['duration']/1000:.1f} 秒;开始时间 {live['startTime']}(UTC) |
  28. | 浏览器模拟接口回归 | {front['expected']}/15 通过,0 失败/跳过/重试 | {front['duration']/1000:.1f} 秒;编辑保存、状态组合、军工绿/暗色主题 |
  29. | 数据库迁移 | 通过 | 033、035;首次、重跑和指定中断点恢复;开发库检查值均为 0 |
  30. | 测试脚本类型检查、Git 空白检查 | 通过 | 两份专用 Playwright 测试与配置;API/Web/E2E 仓库 |
  31. 最终通过结果来自各自一次完整运行,未启用自动重试。补入三次完整场景加载与截图后,真实虚拟用例曾超过原 120 秒测试总预算,断言和评定已完成但截图收尾超时;保留 [该轮记录](diagnostics/live-resource-wait-timeout.json),随后将该用例预算调到 240 秒,并在其他浏览器回归结束后重新完整运行。单次资源就绪仍有 60 秒上限。
  32. 实装视频新增检查还发现本机 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 选择器错误已修正,不计为产品缺陷。'''
  33. source = source.replace('<!-- FINAL_TEST_RESULTS -->', summary)
  34. cleared = ', '.join(f"{table.removeprefix('ut_teaching_')} {count}" for table, count in cleanup['deleted'].items() if count)
  35. source = source.replace('<!-- CLEANUP_RESULTS -->', f"最终清理时间:`{cleanup['time']}`。删除本轮临时任务及关联记录({cleared}),测试账号已停用/软删除,会话已撤销,临时管理员密码文件已移除。四张原始教学表的既有行逐行对照均未变化,剩余活动测试账号为 0。审计与软删除记录按系统机制保留。见 [最终清理记录](cleanup.json);较早轮次见 `diagnostics/cleanup-*.json`。")
  36. titles = {
  37. '01-login-three-roles': '真实服务:登录仅展示教员、学员、管理员',
  38. '02-virtual-formal-task-center': '真实服务:正式虚拟任务中心',
  39. '03-virtual-1920': '真实服务:1920 宽度任务中心',
  40. '04-teacher-content': '真实服务:教员训练编排列表已加载',
  41. '05-virtual-started-no-exam-overlay': '真实服务:开始训练、模型就绪,无错误考核遮罩',
  42. '06-virtual-all-steps-completed': '真实服务:8/8 步骤完成,真实模型已加载',
  43. '07-virtual-reviewed': '真实服务:提交评定后刷新',
  44. '08-physical-api-task-list': '真实服务:实装任务清单',
  45. '09-physical-offline-replay-boundary': '真实页面:离线视频就绪,并标明不推进正式进度、不计分',
  46. '10-physical-twelve-candidates': '真实页面:匹配视频到达末尾,12 条离线候选可达、等待复核',
  47. '01-saved-step-move': '模拟接口:移动已保存步骤后保持正确顺序',
  48. '01-saved-step-delete': '模拟接口:删除步骤后末步进度 100%',
  49. '01-saved-step-duplicate': '模拟接口:复制步骤后重新编号',
  50. '02-multichannel-virtual-save': '模拟接口:虚拟渠道配置保存与重开',
  51. '03-multichannel-confrontation-save': '模拟接口:对抗渠道配置保存与重开',
  52. '05-failed-before-retry': '模拟接口:失败步骤出现重试入口',
  53. '06-failed-after-retry': '模拟接口:重试后尝试 2 次、失败 1 次',
  54. '07-no-retry-SUBMITTED': '模拟接口:已提交不出现重试按钮',
  55. '07-no-retry-TERMINATED': '模拟接口:已终止不出现重试按钮',
  56. '08-visual-safety-military': '模拟接口:军工绿主题安全区,对比度 12.00',
  57. '08-visual-safety-dark': '模拟接口:暗色主题安全区,对比度 15.30',
  58. }
  59. gallery = []
  60. count = 0
  61. for folder, caption in [('live', '真实服务与页面'), ('frontend-e2e', '模拟接口浏览器回归')]:
  62. gallery.append(f'<h3>{caption}</h3><div class="gallery">')
  63. for picture in sorted((REPORT / folder / 'screenshots').glob('*.png')):
  64. if picture.name.startswith('failure-'):
  65. continue
  66. relative = picture.relative_to(REPORT).as_posix()
  67. title = titles.get(picture.stem, '模拟接口:关闭遮罩状态矩阵 ' + picture.stem.replace('04-overlay-', ''))
  68. gallery.append(f'<figure><a href="{relative}"><img loading="lazy" src="{relative}" alt="{html.escape(title)}"></a><figcaption>{html.escape(title)}</figcaption></figure>')
  69. count += 1
  70. gallery.append('</div>')
  71. source = source.replace('<!-- SCREENSHOT_GALLERY -->', f'共 {count} 张最终 E2E 截图。\n\n' + '\n'.join(gallery))
  72. (REPORT / 'review-report.md').write_text(source, encoding='utf-8')
  73. 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}}'''
  74. for item in REPORT.glob('*.md'):
  75. content = item.read_text(encoding='utf-8')
  76. content = re.sub(r'\(([^()]+)\.md(#[^()]*)?\)', lambda m: '(' + m[1] + '.html' + (m[2] or '') + ')' if (REPORT / (m[1] + '.md')).exists() else m[0], content)
  77. content = content.replace('<details>', '<details markdown="1">')
  78. 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}})
  79. 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>'
  80. item.with_suffix('.html').write_text(document, encoding='utf-8')
  81. if item.name == 'review-report.md':
  82. (REPORT / 'index.html').write_text(document, encoding='utf-8')
  83. archive = REPORT.parent / 'review-20260905.zip'
  84. with zipfile.ZipFile(archive, 'w', zipfile.ZIP_DEFLATED, compresslevel=6) as zipped:
  85. for item in REPORT.rglob('*'):
  86. if not item.is_file() or item.name.startswith('failure-') or 'test-results' in item.parts or 'results' in item.parts:
  87. continue
  88. zipped.write(item, item.relative_to(REPORT.parent).as_posix())
  89. print(json.dumps({'screenshots': count, 'live': live, 'frontend': front, 'archiveBytes': archive.stat().st_size}, ensure_ascii=False))
  90. if __name__ == '__main__':
  91. main()