提交 b227d759 authored 作者: 陈泽健's avatar 陈泽健

fix(execution): 运行中 total_cases 保持计划总数(会话53副作用,单点删除覆盖行恢复计划总数口径,437 测试全绿,5.60 部署并实机验证)

Co-Authored-By: 's avatarClaude <noreply@anthropic.com>
上级 f363abdd
# 问题处理与计划执行 — 执行进度总用例数显示修复
> **文档类型**:问题处理单 + 计划执行
> **所属模块**:执行中心(UI 自动化执行)
> **优先级**:P1(显示口径错误,不丢数据但误导用户)
> **创建日期**:2026-09-08
> **来源**:用户反馈「5.44 定时任务用例只有 60 多个,应该有 300 多条」
---
## 1. 问题描述
用户查看 5.44 定时任务执行时,发现「用例数」只有 60 多个,而定时任务选中模块下实际有 340+ 条 UI 用例。
### 1.1 现场核实(2026-09-08,5.44 实查)
| 核查项 | 结果 |
|--------|------|
| `test_cases` 总数 | 409(= 342 UI + 67 安全) |
| 定时任务「UI自动化回归」选中模块 | 28 个 |
| 按调度逻辑(`status=active` + `case_type=ui`)实际收集 | **342 条** ✅ |
| 当轮执行 `case_results`(exec_94ccad06,09:35 触发) | 252 pending + 71 passed + 19 failed = **342** ✅ |
| 该执行 `total_cases` 字段(运行中) | **89**(= 71 + 19 + 0)❌ |
| 上一轮完整执行 total_cases | 341(263 + 79,≈342,差 1 为单条结果写入失败) |
**结论**:用例数据无任何丢失,342 条全部正常排入执行。「60 多个」是**运行中 `total_cases` 字段被实时覆盖为「已完成数」**导致的显示口径错误。
## 2. 根因分析
### 2.1 直接根因
`backend/app/services/execution_service.py` `_persist_case_result()` 实时回调中(会话 53 MissingGreenlet 修复引入):
```python
execution.passed = passed
execution.failed = failed
execution.skipped = skipped
# 显式覆盖 total_cases(passed+failed+skipped),计算通过率不再依赖
# calculate_pass_rate() 读 self.total_cases 列(commit 后 ORM 实例已过期,
# 读列会触发同步 lazy load → MissingGreenlet)。
execution.total_cases = passed + failed + skipped # ← 问题行
execution.pass_rate = round(passed / (passed + failed + skipped) * 100, 2) ...
```
每个用例完成后把 `total_cases` 覆盖为「当时已完成数」,导致:
| 展示位置 | 现状错误表现 | 正确表现 |
|----------|-------------|---------|
| 执行中心「总用例数」统计卡 | 运行中从 0 涨到 342(跑完才是总数) | 全程恒为 342(计划总数) |
| 执行记录列表「用例数」列 | 运行中显示已完成数(用户误以为用例丢了) | 恒为计划总数 |
| 执行进度条 `progressPercent` | **恒为 100%**(分子 = 分母,进度语义完全失效) | 随执行从 0% 涨到 100% |
### 2.2 为什么当时会加这行
会话 53 修 MySQL `MissingGreenlet`:失败回滚后 ORM 实例过期,`calculate_pass_rate()``self.total_cases` 列触发同步 lazy load → 500。当时的修法是显式赋值 `total_cases` 让通过率不依赖读列。
### 2.3 修复依据(为什么可以安全删除)
- **通过率本就不依赖 `total_cases` 列**`_persist_case_result` 中的 `pass_rate` 已用本地变量 `passed/(passed+failed+skipped)` 计算,覆盖 `total_cases` 对它无意义。
- **计划总数在创建时已落库**`create_execution()``total_cases=len(case_ids)`(571 行),删除覆盖后该值全程保持。
- **过期风险已重新评估**`database.py` 配置 `expire_on_commit=False`,commit 后属性不过期;`MissingGreenlet` 仅发生在「回滚后读列」场景。执行收尾块(927-930 行)读 `execution.start_time/end_time` 的既有行为本次**不动**,风险面无扩大。
- **收尾 `calculate_pass_rate()` 口径自然修正**:正常完成时 `total_cases(342) = passed+failed+skipped`,通过率不变;中途取消时通过率分母为计划总数(语义更合理——pending 的用例计入未通过基数,与执行详情页 pending 展示一致)。
## 3. 修复方案
### 3.1 代码改动(单文件单点)
**文件**`backend/app/services/execution_service.py`
**改动**:删除 `_persist_case_result``execution.total_cases = passed + failed + skipped` 覆盖行,注释改为说明「total_cases 保持创建时的计划总数,运行中不覆盖」。
### 3.2 不改的部分
| 范围 | 理由 |
|------|------|
| `create_execution()` | `total_cases=len(case_ids)` 已是正确口径 |
| 执行收尾块(924-931 行) | 读列行为维持现状,避免扩大改动面 |
| 安全测试路径(1318 行 `total_cases = len(case_results)`) | 安全执行为一次性批量写库,无运行中展示诉求 |
| 前端 | 展示字段就是后端 `total_cases`,后端口径修正后自动恢复 |
| 看门狗 / 报告生成 | 不读取运行中 `total_cases` 做判定 |
## 4. 计划执行步骤
| # | 步骤 | 状态 |
|---|------|------|
| 1 | 输出本文档 | ✅ |
| 2 | 删除 `execution_service.py``total_cases` 覆盖行 + 更新注释 | ✅ |
| 3 | 后端全量 `pytest tests/` 回归(重点:执行相关测试) | ✅ 437 passed |
| 4 | 部署 5.60:MD5 部署前核对 → scp 单文件 → `docker restart` → 健康检查 | ✅(15s healthy,`/health` 200) |
| 5 | **不部署** 5.44 / 5.202(用户明确本次只更新 5.60) | — |
## 5. 验收标准
1. 后端全量测试通过,无新增失败。✅ 437 passed
2. 5.60 部署后容器 healthy、`/health` 200。✅
3. 下一次执行运行中:`total_cases` 恒为计划总数。✅ **实机验证**:exec_a92b33d4(19 用例)运行中查库 `total_cases=19``case_results=3 failed + 16 pending`(修复前此处会显示 3);进度条恢复 0%→100% 语义。
4. 5.44 上现网定时任务不受影响(未部署,待后续统一部署时随会话 53 改动一起上)。
### ⚠️ 部署过程备注
重启时 5.60 有一笔 10:39 手动触发的执行(exec_ff03b772)正在运行,被重启中断;FastAPI shutdown 钩子正常收尾(日志「执行完成 通过=0 失败=2」),未留僵尸 running 记录。10:44 起新执行正常。
## 6. 遗留与关联
- 会话 53 待办「5.44 看板用例断言文案修正」不受本修复影响,仍待处理。
- 本修复与未提交的会话 53 改动同在 `execution_service.py`,git 提交时需一并说明(用户未要求本次提交,遵循「未明确要求不自动 commit」约束)。
---
*本文档由 Claude Code 于 2026-09-08 创建(问题处理与修复执行同步完成)。*
......@@ -2,12 +2,40 @@
> **生成时间**: 2026-09-08
> **当前分支**: `platform-auto-test`
> **最近提交**: `b80abae3` feat(recorder): 真录制器与定位质量提升 Phase 1-5(引擎/路由/E2E/前端)(会话 53 改动未提交,工作区待提交
> **状态**: 🟢 **会话53 完成(2026-09-08):5.44 登录/落库双 P0 攻坚闭环(do_login→_navigate_to_home 主动落首页 + error_message 60000B 截断 + MissingGreenlet 绕过),5 条核心用例 3轮回归从全失败→4/5 通过,仅剩看板断言文案 1 条('.block' 实际文本「新建会议」≠ 预期「今日会议」);同步部署 5.60(7 后端文件 + 前端 dist 95 文件,健康检查全绿,RECORDER_ENABLED 生效)**(历史会话见下章节)
> **最近提交**: 本会话(54)`fix(execution): 运行中 total_cases 保持计划总数`(提交推送后回填 hash
> **状态**: 🟢 **会话54 完成(2026-09-08):执行进度「总用例数」显示修复——运行中 total_cases 被覆盖为已完成数(会话53 MissingGreenlet 修复副作用)导致 5.44 定时任务执行中显示 60 多而非 342、前端进度条恒 100%;单点删除覆盖行恢复计划总数口径,437 测试全绿,已部署 5.60 并实机验证(运行中 total_cases 恒为 19)**(历史会话见下章节)
---
## 📊 当前状态(会话 53,2026-09-08 · 5.44 登录/落库 P0 攻坚闭环 + 5.60 部署)
## 📊 当前状态(会话 54,2026-09-08 · 执行进度总数显示修复 + 5.60 部署)
**背景**:用户反馈「5.44 定时任务用例只有 60 多个,应该有 300 多条」。5.44 实查核实:用例数据无丢失(409 = 342 UI + 67 安全;定时任务 28 模块实际收集 342 条,当轮 case_results 252 pending + 71 passed + 19 failed = 342 全部排入)。「60 多」是**运行中 `total_cases` 字段被实时覆盖为「已完成数」**的显示口径错误——会话 53 MissingGreenlet 修复的副作用。
### ✅ 修复内容
- **根因**`execution_service.py` `_persist_case_result()``execution.total_cases = passed + failed + skipped`,每个用例完成后把总用例数覆盖为已完成数 → 执行中心「总用例数」运行中从 0 涨到 342、进度条 `(passed+failed+skipped)/total_cases` 恒 100%。
- **修复**:单点删除该覆盖行,`total_cases` 保持 `create_execution()` 写入的计划总数 `len(case_ids)`。通过率分子分母均用本地变量,不受影响;`expire_on_commit=False` 已确认,收尾 `calculate_pass_rate()` 读列安全(MissingGreenlet 仅回滚后触发,收尾路径未动)。
- **文档**`Docs/PRD/需求文档/执行中心/_问题处理与计划执行_执行进度总数显示修复.md`(问题核实数据 / 根因链 / 方案依据 / 执行步骤 / 验收标准 / 部署备注)。
- **测试**:后端全量 `pytest tests/` **437 passed**(70s,无回归)。
### ✅ 5.60 部署与实机验证
- 部署前拉取远程文件 diff:与本地仅差本次一处(4行→5行),无其他窗口 WIP;远程备份 `execution_service.py.bak_20260908`
- 上传(LF 行尾)→ 容器 py_compile OK → restart 15s healthy → `/health` 200。
- **实机验证**:新代码下第一笔执行 `exec_a92b33d4`(19 用例)运行中查库 `total_cases=19` 恒定(case_results = 3 failed + 16 pending;修复前此处显示 3)——总用例数不再从 0 往上涨,进度条恢复 0%→100% 语义。
- **验证脚本**`backend/scripts/verify_560_total_cases_fix.py`(传执行 ID 复查;被 .gitignore 忽略未入库)。诊断脚本 `diag_544_sched_case_count.py` 已入库。
### ⚠️ 部署过程备注(如实记录)
- 重启时 5.60 有一笔 10:39 手动触发的执行(exec_ff03b772)正在运行,被重启中断;FastAPI shutdown 钩子正常收尾(日志「执行完成 通过=0 失败=2」),未留僵尸 running 记录,10:44 起新执行正常。
### ⚠️ 待办
- **5.44 / 5.202 部署本修复**(本次按用户要求仅部署 5.60;5.44 运行中显示口径仍旧,跑完终值正确;需与下一次统一部署一起上)
- **5.44 看板用例收尾**(会话 53 遗留):case_6699d6dd 步骤 3 断言修正为「新建会议」→ 5/5 回归确认
- **5.202 部署**会话 53 改动(遗留)
- **5.60 录制器真实环境验收**(会话 52 P2)
- 清理诊断脚本(diag_544_*.py、dump_544_cases.py、dump9_cases.json、tmp_wait_deploy_544.py 等)
---
## 📊 历史状态(会话 53,2026-09-08 · 5.44 登录/落库 P0 攻坚闭环 + 5.60 部署)
**背景**:承接会话 51 遗留 P0(5.44 全量套件 ~100% 失败,`do_login` 后落不到首页,卡 `.nav_right_list.el-dropdown-selfdefine` 等待),本次在 5.44 容器内完成根因定位、代码修复、用例数据修复与回归,并将全部修复同步部署到 5.60。
......@@ -256,4 +284,4 @@
---
*本文档由 Claude Code 于 2026-09-08 更新(会话 53:5.44 登录/落库双 P0 闭环(do_login 后 _navigate_to_home 主动落首页 + error_message 60000B/log-error 20000 截断 + 滚动计数绕过 MissingGreenlet),fix_544_login_cases.py 重写 5 用例+删 4 垃圾用例(409 总数),3 轮回归 4/5(看板 .block 文本实测「新建会议」);5.60 部署 7 后端文件 + dist 95 文件全绿(RECORDER_ENABLED 生效、PLATFORM_BASE_URL 保留)。历史:会话 52 真录制器 Phase 5(`b80abae3`);会话 51 截图瘦身部署 5.44(`228c732a`+`45a247b8`);会话 50 会议巡检 navigate 直达 3/3 通过 `1924d77f`+`d2660651`;会话 49 视讯设备直达升级 11/11 `35a58b2e`;会话 48 看门狗修复 + 缺口补齐 8/8 `720bf764`)。*
*本文档由 Claude Code 于 2026-09-08 更新(会话 54:执行进度「总用例数」显示修复——运行中 total_cases 被覆盖为已完成数的会话 53 副作用,单点删除恢复计划总数口径(437 测试全绿),已部署 5.60 并实机验证运行中 total_cases 恒为 19;问题处理文档入库 Docs/PRD/需求文档/执行中心/。历史:会话 53 5.44 登录/落库双 P0 闭环(do_login 后 _navigate_to_home 主动落首页 + error_message 60000B/log-error 20000 截断 + 滚动计数绕过 MissingGreenlet),fix_544_login_cases.py 重写 5 用例+删 4 垃圾用例(409 总数),3 轮回归 4/5(看板 .block 文本实测「新建会议」);5.60 部署 7 后端文件 + dist 95 文件全绿(RECORDER_ENABLED 生效、PLATFORM_BASE_URL 保留);会话 52 真录制器 Phase 5(`b80abae3`);会话 51 截图瘦身部署 5.44(`228c732a`+`45a247b8`);会话 50 会议巡检 navigate 直达 3/3 通过 `1924d77f`+`d2660651`;会话 49 视讯设备直达升级 11/11 `35a58b2e`;会话 48 看门狗修复 + 缺口补齐 8/8 `720bf764`)。*
......@@ -729,10 +729,11 @@ class ExecutionService:
execution.passed = passed
execution.failed = failed
execution.skipped = skipped
# 显式覆盖 total_cases(passed+failed+skipped),计算通过率不再依赖
# calculate_pass_rate() 读 self.total_cases 列(commit 后 ORM 实例已过期,
# 读列会触发同步 lazy load → MissingGreenlet)。
execution.total_cases = passed + failed + skipped
# total_cases 不在此覆盖:保持 create_execution 写入的计划总数,
# 运行中「总用例数」恒为计划值、进度条 (passed+failed+skipped)/
# total_cases 才有进度语义(曾覆盖为已完成数,导致定时任务执行中
# 显示 60 多而非 342、进度条恒 100%——见执行中心问题处理文档)。
# 通过率分子分母均用本地变量,不依赖 total_cases 列。
execution.pass_rate = round(passed / (passed + failed + skipped) * 100, 2) if (passed + failed + skipped) else 0.0
await self.db.commit() # 立即提交,前端轮询可见
......
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
诊断:5.44 定时任务用例数为何只有 60 多(预期 300+)
检查项:
1. scheduled_tasks 配置(module_ids / case_type / enabled)
2. test_cases 总数 / 按 status 分布 / 按 case_type 分布
3. 各模块下的 active ui 用例数
4. 任务选中模块集合内的 active ui 用例总数(调度实际会收到的数量)
5. 最近几次 scheduled 执行的 total_cases
"""
import sys
sys.stdout.reconfigure(encoding="utf-8")
import json
import paramiko
HOST, USER, PWD = "192.168.5.44", "root", "Ubains@123"
MYSQL = "plat-auto-test-mysql"
DB = "plat_auto_test"
cli = paramiko.SSHClient()
cli.set_missing_host_key_policy(paramiko.AutoAddPolicy())
cli.connect(HOST, username=USER, password=PWD, timeout=20)
def run(cmd, timeout=60):
_, stdout, stderr = cli.exec_command(cmd, timeout=timeout)
return (stdout.read().decode("utf-8", errors="replace").strip(),
stderr.read().decode("utf-8", errors="replace").strip())
def sql(q, label):
out, err = run(
f"docker exec {MYSQL} mysql -uroot -p'Ubains@2025' {DB} -e \"{q}\" 2>/dev/null"
)
print(f"\n----- {label} -----")
print(out if out else f"(空) err: {err[:300]}")
return out
# 1. 定时任务配置
tasks_out = sql(
"SELECT id, name, enabled, case_type, module_ids, schedule_type, last_run_at, next_run_at "
"FROM scheduled_tasks;", "1. 定时任务配置"
)
# 2. 用例总数与分布
sql("SELECT COUNT(*) AS total FROM test_cases;", "2a. 用例总数")
sql("SELECT status, COUNT(*) FROM test_cases GROUP BY status;", "2b. 按 status 分布")
sql("SELECT case_type, COUNT(*) FROM test_cases GROUP BY case_type;", "2c. 按 case_type 分布")
# 3. 各模块 active ui 用例数
sql(
"SELECT m.id, m.name, COUNT(c.id) AS active_ui "
"FROM modules m LEFT JOIN test_cases c "
"ON c.module_id=m.id AND c.status='active' AND c.case_type='ui' "
"GROUP BY m.id, m.name ORDER BY active_ui DESC;", "3. 各模块 active+ui 用例数"
)
# 4. 计算每个任务选中模块内的实际用例数(与调度逻辑一致)
if tasks_out:
lines = tasks_out.splitlines()
header = lines[0].split("\t")
idx = {h: i for i, h in enumerate(header)}
for row in lines[1:]:
cols = row.split("\t")
if len(cols) < len(header):
continue
name = cols[idx["name"]]
raw = cols[idx["module_ids"]]
try:
mod_ids = json.loads(raw.replace("'", '"')) if raw else []
except Exception:
mod_ids = [m.strip("\"'[] ") for m in raw.split(",") if m.strip("\"'[] ")]
id_list = ",".join(f"'{m}'" for m in mod_ids if m)
print(f"\n----- 任务「{name}」module_ids={mod_ids} ({len(mod_ids)} 个模块) -----")
if not id_list:
print("(未选中任何模块)")
continue
sql(
f"SELECT COUNT(*) AS will_run FROM test_cases "
f"WHERE module_id IN ({id_list}) AND status='active' AND case_type='ui';",
f"4. 任务「{name}」调度实际收到的用例数",
)
# 5. 最近 scheduled 执行的规模
sql(
"SELECT id, name, trigger_type, status, total_cases, passed, failed, created_at "
"FROM executions WHERE trigger_type='scheduled' "
"ORDER BY created_at DESC LIMIT 6;", "5. 最近 scheduled 执行"
)
cli.close()
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论