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

docs(prd): 删除用例生成相关PRD文档,输出AI用例完善的需求文档实现功能。

- 移除根据PRD文档生成测试用例的需求文档
- 删除用例生成计划执行文档
- 清理相关的PRD规范和实施计划内容
上级 e50d943e
# AI完善测试用例工具 - 环境变量配置文件
#
# 说明:复制此文件为 .env 并填入实际的配置值
# .env 文件已加入 .gitignore,不会被提交到代码仓库
# ==================== Anthropic API 配置 ====================
# 获取API密钥:https://console.anthropic.com/
ANTHROPIC_API_KEY=sk-ant-api03-...
# ==================== API配置(可选)====================
# API基础URL(用于代理服务)
# 官方API可留空或设置为:https://api.anthropic.com
# CCSwitch等代理服务需设置对应endpoint
# API_BASE_URL=https://api.anthropic.com
# ==================== AI服务模式配置 ====================
# AI服务模式选择(可选)
# auto: 自动检测(默认)- 优先使用API,无API密钥时尝试CLI
# api: 强制使用Anthropic API(需要设置ANTHROPIC_API_KEY)
# cli: 强制使用Claude CLI(需要安装:npm install -g @anthropic-ai/claude-code)
# AI_SERVICE_MODE=api
# ==================== 日志配置 ====================
# 日志级别:DEBUG, INFO, WARNING, ERROR
LOG_LEVEL=INFO
# ==================== AI配置(可选)====================
# AI模型:claude-sonnet-4-6, claude-opus-4-6
# AI_MODEL=claude-sonnet-4-6
# AI最大token数
# AI_MAX_TOKENS=8192
# AI温度参数(0-1,越低越稳定)
# AI_TEMPERATURE=0.3
# ==================== 覆盖率目标(可选)====================
# 覆盖率目标(0-1)
# COVERAGE_TARGET=0.9
# AI完善测试用例工具 - Git忽略文件
# 环境变量配置(包含API密钥)
.env
# Python缓存
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
# 虚拟环境
venv/
env/
ENV/
# IDE配置
.vscode/
.idea/
*.swp
*.swo
# 日志文件
logs/*.log
# 输出文件
Perfected/*.xlsx
reports/*.docx
# 临时文件
*.tmp
.DS_Store
Thumbs.db
# 测试文件
*.pyc
# AI完善测试用例工具
## 项目简介
本工具通过结合已有测试用例、需求文档、开发需求PRD文档,利用AI能力对测试用例进行查漏补缺,完善测试用例内容,确保测试用例覆盖全面且符合规范要求。
### 主要功能
- **文档读取**:自动读取需求文档(.docx)、开发PRD文档(.md)、测试用例(.xlsx)
- **功能点提取**:使用AI从需求文档中提取完整的功能点列表
- **覆盖率分析**:分析现有测试用例的覆盖情况
- **缺失检测**:检测功能点、场景、边界条件的缺失
- **用例生成**:使用AI自动生成缺失的测试用例
- **去重处理**:智能去重,确保测试用例不重复
- **报告生成**:生成详细的差异性报告
### 覆盖率目标
- **场景覆盖率目标**:90%
---
## 项目结构
```
AuxiliaryTool/AIPerfectedTestCases/
├── src/ # 源代码目录
│ ├── __init__.py
│ ├── config.py # 配置模块
│ ├── document_reader.py # 文档读取模块
│ ├── test_case_analyzer.py # 测试用例分析模块
│ ├── gap_detector.py # 缺失检测模块
│ ├── case_generator.py # 用例生成模块
│ ├── deduplicator.py # 去重模块
│ ├── report_generator.py # 报告生成模块
│ └── main.py # 主入口模块
├── config/ # 配置文件目录
│ ├── 需求文档/
│ │ └── *.docx # 需求文档
│ ├── 开发PRD/
│ │ └── *.md # 开发PRD文档
│ └── 测试用例/
│ └── 新统一平台权限管理测试用例.xlsx
├── Perfected/ # 输出目录
│ └── 新统一平台权限管理测试用例_完善版本.xlsx
├── reports/ # 报告输出目录
│ └── 新统一平台权限管理测试用例_差异性报告.docx
├── logs/ # 日志目录
├── run.py # 入口脚本
├── requirements.txt # 依赖包列表
└── README.md # 说明文档
```
---
## 安装说明
### 环境要求
- Python 3.10+
- Anthropic API密钥
### 安装步骤
1. **进入项目目录**
```bash
cd AuxiliaryTool/AIPerfectedTestCases
```
2. **安装依赖**
```bash
pip install -r requirements.txt
```
3. **设置API密钥**
**方式一:使用 .env 文件(推荐)**
复制 `.env.example``.env`,然后编辑 `.env` 文件:
```bash
# 复制模板文件
cp .env.example .env
# 编辑 .env 文件,填入你的API密钥
# ANTHROPIC_API_KEY=sk-ant-api03-xxxxxxxxxxxxxxxx
```
**方式二:环境变量(临时)**
**Windows:**
```bash
set ANTHROPIC_API_KEY=your-api-key-here
```
**Linux/Mac:**
```bash
export ANTHROPIC_API_KEY=your-api-key-here
```
---
## 使用说明
### 基础用法
```bash
# 使用默认配置运行
python run.py
```
### 命令行参数
| 参数 | 说明 | 示例 |
|------|------|------|
| `--log-level` | 日志级别(DEBUG/INFO/WARNING/ERROR) | `--log-level DEBUG` |
| `--show-info` | 显示项目信息 | `--show-info` |
| `--check-only` | 仅检查环境,不执行处理 | `--check-only` |
| `--help` | 显示帮助信息 | `--help` |
### 使用示例
```bash
# 正常运行
python run.py
# 启用详细日志
python run.py --log-level DEBUG
# 仅检查环境
python run.py --check-only
# 显示项目信息
python run.py --show-info
```
---
## 输入文件说明
### 1. 需求文档(.docx)
- **位置**`config/需求文档/`
- **格式**:Word文档
- **内容**:功能需求描述
### 2. 开发PRD文档(.md)
- **位置**`config/开发PRD/`
- **格式**:Markdown文档
- **内容**:功能实现细节、边界条件
### 3. 测试用例(.xlsx)
- **位置**`config/测试用例/`
- **格式**:Excel表格
- **列定义**
| 列名 | 说明 |
|------|------|
| 序列号 | 序号,用于排序 |
| 功能模块 | 功能模块名称 |
| 用例编号 | 用例编号 |
| 功能描述 | 功能描述 |
| 用例等级 | 用例等级(P0/P1/P2/P3) |
| 功能编号 | 功能编号 |
| 用例名称 | 用例名称 |
| 预置条件 | 测试预置条件 |
| STEP | 步骤描述 |
| JSON | 自动化测试用例(暂不涉及) |
| 预期结果 | 预期结果 |
| 测试结果 | 测试结果 |
| 测试频次 | 测试频次 |
| 日志/截图/照片 | 日志/截图/照片 |
| 备注 | 备注 |
---
## 输出文件说明
### 1. 完善后的测试用例
- **位置**`Perfected/新统一平台权限管理测试用例_完善版本.xlsx`
- **内容**:原始用例 + 新增用例
- **标记**:新增用例有浅蓝色背景
### 2. 差异性报告
- **位置**`reports/新统一平台权限管理测试用例_差异性报告.docx`
- **内容**
- 功能点缺失的测试用例列表
- 场景覆盖不足的测试用例列表
- 边界条件不足的测试用例列表
- 去重处理结果
- 用例数量增长统计
- 覆盖率百分比统计
- 总结和建议
---
## 处理流程
```
1. 读取文档
├─ 读取需求文档(.docx)
├─ 读取PRD文档(.md)
└─ 读取测试用例(.xlsx)
2. 提取功能点
└─ 使用AI从需求文档中提取功能点列表
3. 分析覆盖率
└─ 分析现有测试用例的覆盖情况
4. 检测缺失
├─ 功能点缺失
├─ 场景覆盖不足
└─ 边界条件不足
5. 生成新用例
├─ 为功能点缺失生成用例
├─ 为场景覆盖不足生成用例
└─ 为边界条件不足生成用例
6. 去重处理
└─ 检测并去除重复用例
7. 保存结果
├─ 保存完善后的测试用例
└─ 生成差异性报告
```
---
## 配置说明
配置文件位于 `src/config.py`,可修改以下参数:
### AI配置
```python
AI_MODEL = "claude-sonnet-4-6" # AI模型选择
AI_MAX_TOKENS = 8192 # 最大token数
AI_TEMPERATURE = 0.3 # 温度参数
```
### 覆盖率目标
```python
COVERAGE_TARGET = 0.9 # 90%覆盖率目标
```
### 测试用例配置
```python
CASE_LEVELS = ["P0", "P1", "P2", "P3"] # 用例等级
DEFAULT_CASE_LEVEL = "P1" # 默认等级
DEFAULT_TEST_FREQUENCY = "每版本" # 默认测试频次
```
---
## 注意事项
1. **API密钥安全**:请妥善保管Anthropic API密钥,不要泄露
2. **成本控制**:大量API调用可能产生费用,建议先小规模测试
3. **人工审核**:AI生成的测试用例需要人工审核确认
4. **文件占用**:运行时请确保测试用例文件没有被其他程序打开
5. **网络连接**:需要稳定的网络连接调用AI服务
---
## 常见问题
### Q1: 提示API密钥未设置怎么办?
请设置环境变量:
- Windows: `set ANTHROPIC_API_KEY=your-key-here`
- Linux/Mac: `export ANTHROPIC_API_KEY=your-key-here`
### Q2: 如何获取Anthropic API密钥?
访问 Anthropic 官网:https://www.anthropic.com/
### Q3: 支持哪些AI模型?
目前支持 Claude Sonnet 4.6 和 Claude Opus 4.6,可在 `config.py` 中修改。
### Q4: 生成的测试用例可以直接使用吗?
建议对AI生成的测试用例进行人工审核和修改后使用。
### Q5: 如何调整覆盖率目标?
修改 `src/config.py` 中的 `COVERAGE_TARGET` 参数。
---
## 版本历史
| 版本 | 日期 | 说明 |
|------|------|------|
| v1.0.0 | 2026-03-10 | 初始版本 |
---
## 优化功能回填
- [ ] 支持自定义AI提示词模板
- [ ] 支持增量更新(只处理变更的部分)
- [ ] 支持多轮对话优化用例
- [ ] 支持导出为多种格式
- [ ] 添加用例质量评分
- [ ] 支持批量处理多个项目
- [ ] 支持GUI界面
# 权限数据扩展补充
## 概述
1. **配置项数据地址**:`Docs\Temp\licence.js`
2. **权限控制数据地址**:`Docs\Temp\permission.json`
## 任务
1. ✅ 根据配置项数据补充权限控制数据
2. ✅ 补充权限控制数据的国际化
3. ✅ 补充相关说明文档
### 补充权限控制数据要求
1. 配置项数据和权限控制数据中都包含有`xx_yyyy_list`数据,帮我根据配置项数据并参考权限控制数据原数据补充权限控制数据,每个都包含基础的`view、create、edit、delete`
2. 配置项数据中只要是`xx_yyyy_list`里的数据,都要补充到权限控制数据中
3. 当补充完数据后,需要确认一下是不是每一个都包含在权限控制数据里面了
### 补充权限控制数据的国际化要求
1. 配置项数据中有着一些注释比如`set_room_enable`的注释为"区域管理",这个将代表着`set_room_list`的为"区域管理",子集`office_manage`也有着`办公室管理`的注释
2. 帮我根据注释以及上面的规律,找到国际化文件中的`permissionClass``xx_yyyy_enable`的注释作为`xx_yyyy_list`的国际化添加进行,将子集的注释补充到`permission`中,并实现国际化
3. 当补充完国际化后,需要确认一下国际化中的`permissionClass`是否包含了每一个`xx_yyyy_enable`的注释作为`xx_yyyy_list`的value,`permission`中包含每一个子集的注释
### 相关说明文档补充要求
1. **文档位置**`Docs\Temp\权限配置README.md`
2. 由于权限控制数据是一个json文件,为了说明相关的配置有何功能,所以在文档进行说明,我要你将配置项和相关的文件说明整合到文档中
## 国际化
当涉及到国际化时,遵循`Docs\PRD\_PRD_规范文档_国际化.md`规范
\ No newline at end of file
# 权限管理页面新增开发
## 📋 概述
1. 权限管理页面地址: `src\views\Backend\Admin\PermissionManage\index.vue`
2. 相关API文档地址:`Docs\Api\API_权限组管理接口文档.md`
3. 新增/修改权限组页面地址:`src\views\Backend\Admin\PermissionManage\components\AddEditPage\index.vue`
4. 权限绑定弹窗地址:`src\views\Backend\Admin\PermissionManage\components\BindDialog\index.vue`
5. 自定义权限控制指令v-permission地址:`src\utils\permission.js`
6. 权限组弹窗权限勾选组件:`src\views\Backend\Admin\PermissionManage\components\PermissionConfig\index.vue`
## 🎯 任务
1. ✅ 在权限管理页面遵循`Docs\PRD\_PRD_规范文档_新建页面.md`并标准化初始化页面,在`src\router.js`注册路由
2. ✅ 参考页面`src\views\Backend\Admin\Role\index.vue`的设计在初始化后的权限管理页新增功能
3. ✅ 将权限管理页面的新增/修改权限组弹窗和绑定弹窗分别抽离成组件放置在`src\views\Backend\Admin\PermissionManage\components`中,注意数据的传输,并优化一下样式
4. ✅ 实现权限组添加权限
5. ✅ 在新增/修改权限组弹窗继续实现任务4的要求
6. ✅ 帮我参考API接口文档,实现权限组的增删改查、禁用/启用功能,绑定功能先不实现
7. ✅ 进入权限管理页面时,帮我请求API接口文档的接口`/permissionGroup/getAllPermissions`获取所有配置项,然后将`src\views\Backend\Admin\PermissionManage\components\PermissionConfig\index.vue`的permissionList替换为接口请求到的真实数据
8. ✅ 重构权限绑定弹窗及补充功能实现
9. ✅ 数据结构调整
10. ✅ 用户反馈说新增/修改权限组弹窗和权限绑定弹窗这两个弹窗数据比较多,使用弹窗显示不全,帮我改成页面组件,不需要新建router路由,只需要改为页面级组件,并遵循`Docs\PRD\_PRD_规范文档_新建页面.md`与使用ui-ux-pro-max优化页面样式,要求样式紧凑一点,尽可能显示多的页面数据
11. 帮我参考新增/修改权限组页面样式,实现权限查看功能与页面
### 权限页面基础功能
1. 实现功能时提供了API接口,但先不进行对接,先在前端用虚拟数据实现并预留操作方法等,等我要接口对接时再进行接口对接
2. 支持模糊搜索,支持基础的增删改查
3. el-table的主要显示字段获取参考相关API文档的"分页查询权限组"接口,显示字段groupName、isEnable、createTime,支持多选,支持分页,操作有修改、删除、绑定
### 权限组添加权限要求
1. 实现功能时提供了API接口,但先不进行对接,先在前端用虚拟数据实现并预留操作方法等,等我要接口对接时再进行接口对接
2. 我在`src\constant\permissionList.js`模拟了权限数据,帮我使用el-checkbox进行显示
3. 文本显示逻辑:在国际化文件中新建permission子集,通过key值匹配显示文本:例如view=i18n文件中的`permission.view`
4. 页面显示:要同时显示名称和勾选框,比如
```json
functionType_82: {
view: 0,
create: 0,
update: 0,
delete: 0,
},
```
要显示为"办公室管理:查看 新增 编辑 删除"
### 重构权限绑定弹窗及补充功能实现要求
1. 弹窗页面重构,当前的设计不符合产品的功能要求,具体功能要求:权限组支持同时绑定多个用户、角色、部门,取消权限配置功能
2. 页面样式自由发挥,符合系统主题即可,优先使用UI/UX Pro Max
3. 参考`@src/views/Backend/Account/User/index.vue:930-984 `实现用户数据请求获取
4. 参考`@src/views/Backend/Account/User/index.vue:986-1010 `实现角色数据的请求获取
5. 参考`@src/views/Backend/Account/User/index.vue:263-273 ``@src/views/Backend/Account/User/index.vue:1055-1068 `实现部门数据的请求获取
6. 根据权限api文档的`/permissionGroup/bindRelation`实现权限绑定
## 任务执行要求
- 在执行任务前,判断当前文件根目录下有没有"当前文件+'_计划执行.md'"文件,例如"PRD_实时转录_页面整合.md"与"PRD_实时转录_页面整合_计划执行.md",没有则新建一个
- 在执行任务中,将将要执行的计划补充在"当前文件+'_计划执行.md'"文件中,执行的任务以"当前文件+'_计划执行.md'"为主
- 在执行任务后,将任务执行的结果返回补充到"当前文件+'_计划执行.md'"文件中,需要写明实现了什么,以及有什么优化项或者有什么风险等
## 问题排查要求
- 在问题排查前,判断当前文件根目录下有没有"当前文件+'_分析执行.md'"文件,例如"PRD_实时转录_页面整合.md"与"PRD_实时转录_页面整合_分析执行.md",没有则新建一个
- 在问题排查中,将将要执行的操作补充在"当前文件+'_分析执行.md'"文件中,执行的操作以"当前文件+'_分析执行.md'"为主
- 在问题排查后,将问题修复的结果返回补充到"当前文件+'_分析执行.md'"文件中,需要写明实现了什么,以及有什么优化项或者有什么风险等
## 过程总结返填
- 在执行任务时,先了解相关的页面与方法,先看看有什么不清楚的或者需要我确认的,先找我确认,再进行代码开发
- 在我确认完后,需要把我确认的东西反填到当前文档中,确保信息对齐
## 代码规范
- 严格按照`Docs\PRD\_PRD_规范文档_代码规范.md`
## 文档规范
- 严格按照`Docs\PRD\_PRD_规范文档_文档规范.md`
## 测试验证
- 严格按照`Docs\PRD\_PRD_规范文档_测试规范.md`
\ No newline at end of file
# 需求文档
## 概述
**文件**
- **初始化权限组方法**:com.ubains.meeting.system.service.impl.PermissionGroupServiceImpl.initBasePermissionGroupsForAllCompanies
- **数据库初始化文件**:com/ubains/meeting/common/AdjustmentDb.java
- **原基础角色基本权限组开发相关文件**:
- **原始需求文档**:Docs/PRD/系统管理/权限管理/PRD_权限组管理_添加权限组_给基础角色添加基本权限组.md
- **计划执行文档**:Docs/PRD/系统管理/权限管理/PRD_权限组管理_添加权限组_给基础角色添加基本权限组_计划执行.md
### 背景
原来开发的基础角色和基本权限组有点问题,需要优化调整一下:
1. 基础角色中有个id为2的Admin基础角色,该角色与后续添加的系统管理员冲突了,他们本质应该是同一个
2. id为2的Admin基础角色没有基本权限组
## 任务
- 参考`原基础角色基本权限组开发相关文件`,了解之前的开发逻辑
- `数据库初始化文件`从第615行到第668行,调整基础角色
- id为6的系统管理员角色不要了
- 安全管理员和审计管理员的id往前移1位,即7->6(安全管理员),8->7(审计管理员)
- `初始化权限组方法`调整权限组和角色的绑定
- 系统管理员的权限组绑定id为2的原Admin基础角色
- 绑定安全管理员和审计管理员时设置id往前移1位,即7->6(安全管理员),8->7(审计管理员)
- 先给出计划执行方案文档,将文档创建在`Docs/PRD/+同路径+/同名文件+_计划执行.md`,等审批完文档再由我考虑是否按计划执行文档来执行
- 生成的计划执行方案文档需添加任务:生成ai输出的总结内容到`Docs/Doc/+同路径+/Doc_+同名文件(不包含前缀PRD_)+的总结.md`
## 规范
严格执行以下文档
#### 代码规范
- Docs/PRD/_PRD_规范文档_代码规范.md
#### 文档规范
- Docs/PRD/_PRD_规范文档_文档规范.md
#### 测试规范
- Docs/PRD/_PRD_规范文档_测试规范.md
\ No newline at end of file
## Plan: 权限组管理功能设计
本方案设计一个基于权限组的RBAC权限管理系统。通过`sys_permission_group`权限组表和三个独立关联表(部门、角色、用户),实现权限组的增删改查及绑定关系。删除采用物理删除并级联清理关联数据,权限合并采用并集策略。
### 数据库设计
**1. 权限组表 `sys_permission_group`**
| 字段 | 类型 | 说明 |
|------------------|--------------|------------------------------------------------------|
| `group_id` | bigint | 主键,自增 |
| `group_name` | varchar(100) | 权限组名称 |
| `permissions` | text | 权限配置JSON,如 `{"permission1":{"create":1,"delete":0}}` |
| `company_number` | varchar(64) | 公司编号(多租户) |
| `is_enable` | int | 是否启用(0:禁用,1:启用) |
| `is_base` | int | 是否基础权限组(0:否,1:是) |
| `create_time` | datetime | 创建时间 |
| `update_time` | datetime | 更新时间 |
**2. 权限组-部门关联表 `sys_permission_group_department`**
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | bigint | 主键,自增 |
| `group_id` | bigint | 权限组ID |
| `department_id` | bigint | 部门ID |
| `create_time` | datetime | 创建时间 |
**索引**`idx_group_id(group_id)``idx_department_id(department_id)`
**3. 权限组-角色关联表 `sys_permission_group_role`**
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | bigint | 主键,自增 |
| `group_id` | bigint | 权限组ID |
| `role_id` | bigint | 角色ID |
| `create_time` | datetime | 创建时间 |
**索引**`idx_group_id(group_id)``idx_role_id(role_id)`
**4. 权限组-用户关联表 `sys_permission_group_user`**
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | bigint | 主键,自增 |
| `group_id` | bigint | 权限组ID |
| `user_id` | bigint | 用户ID |
| `create_time` | datetime | 创建时间 |
**索引**`idx_group_id(group_id)``idx_user_id(user_id)`
**3. permission.json 配置文件格式**
```json
{
"permission1": {"create": 0, "delete": 0, "edit": 0},
"permission2": {"create": 0, "delete": 0, "edit": 0, "view": 0}
}
```
### 接口设计
| 接口 | URL | 方法 | 说明 |
|------|-----|------|------|
| 分页查询权限组 | `/permissionGroup/getPage` | POST | 返回权限组分页列表 |
| 获取所有权限配置 | `/permissionGroup/getAllPermissions` | GET | 读取permission.json返回所有可选权限 |
| 新增权限组 | `/permissionGroup/add` | POST | 创建权限组及权限配置 |
| 修改权限组 | `/permissionGroup/update` | PUT | 更新权限组名称和权限配置 |
| 删除权限组 | `/permissionGroup/delete` | POST | 物理删除权限组,级联删除关联记录 |
| 绑定关系 | `/permissionGroup/bindRelation` | POST | 绑定部门/角色/用户(type区分) |
| 解绑关系 | `/permissionGroup/unbindRelation` | POST | 解绑部门/角色/用户 |
| 查询绑定关系 | `/permissionGroup/getRelations` | POST | 查询权限组已绑定的对象列表 |
| 查询用户最终权限 | `/permissionGroup/getUserPermissions` | GET | 返回用户通过所有途径获得的并集权限 |
### 权限合并逻辑
查询用户最终权限时,合并以下三个来源的权限组:
1. **直接绑定**:查询 `sys_permission_group_user` 表中 `user_id=用户ID` 的权限组
2. **部门绑定**:查询 `sys_permission_group_department` 表中 `department_id=用户所属部门ID` 的权限组
3. **角色绑定**:查询 `sys_permission_group_role` 表中 `role_id=用户所属角色ID` 的权限组
合并策略:对所有权限组的`permissions`字段进行JSON并集,同一操作项有任意一个为1则最终为1。
### Steps
1.[ubains-meeting-persistence](ubains-meeting-persistence/src/main) 创建`PermissionGroup``PermissionGroupDepartment``PermissionGroupRole``PermissionGroupUser`四个实体类,Mapper接口继承`BaseMapper`
2.[ubains-meeting-provider](ubains-meeting-provider/src/main) 创建`IPermissionGroupService`接口及实现类,实现CRUD、绑定解绑、权限并集合并逻辑
3.[ubains-meeting-inner-api/resources](ubains-meeting-inner-api/src/main/resources) 创建`permission.json`配置文件
4.[ubains-meeting-inner-api](ubains-meeting-inner-api/src/main) 创建`PermissionGroupController`,实现9个API接口,删除接口使用`@Transactional`保证级联删除的事务一致性
5. 创建`PermissionUtil`工具类封装权限JSON并集合并方法,供Service层和权限校验拦截器调用
6. 在src/main/java/com/ubains/meeting/common/AdjustmentDb.java的adjustmentDb方法创建对应数据库表
7. 输出PermissionGroupController的接口文档到Docs/Doc/权限管理目录下“API_权限组管理接口文档.md”
### SQL参考
```sql
-- 权限组表
CREATE TABLE `sys_permission_group` (
`group_id` bigint NOT NULL AUTO_INCREMENT,
`group_name` varchar(100) NOT NULL COMMENT '权限组名称',
`permissions` text COMMENT '权限配置JSON',
`company_number` varchar(64) DEFAULT NULL COMMENT '公司编号',
`is_enable` int DEFAULT 1 COMMENT '是否启用(0:禁用,1:启用)',
`is_base` int DEFAULT 0 COMMENT '是否基础权限组(0:否,1:是)',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`group_id`),
KEY `idx_company` (`company_number`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='权限组表';
-- 权限组-部门关联表
CREATE TABLE `sys_permission_group_department` (
`id` bigint NOT NULL AUTO_INCREMENT,
`group_id` bigint NOT NULL COMMENT '权限组ID',
`department_id` bigint NOT NULL COMMENT '部门ID',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_group_id` (`group_id`),
KEY `idx_department_id` (`department_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='权限组-部门关联表';
-- 权限组-角色关联表
CREATE TABLE `sys_permission_group_role` (
`id` bigint NOT NULL AUTO_INCREMENT,
`group_id` bigint NOT NULL COMMENT '权限组ID',
`role_id` bigint NOT NULL COMMENT '角色ID',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_group_id` (`group_id`),
KEY `idx_role_id` (`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='权限组-角色关联表';
-- 权限组-用户关联表
CREATE TABLE `sys_permission_group_user` (
`id` bigint NOT NULL AUTO_INCREMENT,
`group_id` bigint NOT NULL COMMENT '权限组ID',
`user_id` bigint NOT NULL COMMENT '用户ID',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_group_id` (`group_id`),
KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='权限组-用户关联表';
```
### 其他
1.把AI的总结内容输出ubains-meeting-parent\Docs\Doc\权限管理目录下的“PRD_权限组管理_增删改查_权限组功能开发设计的总结.md”
2.日志和注释要详细,尤其是日志,详细到每个步骤和返回,采用LogsUtil的logxxxInverted方法
\ No newline at end of file
# 权限组管理_权限结构_调整权限数据结构
## 概述
原开发的设计文档:Docs\PRD\PRD_系统\权限管理\PRD_权限组管理_增删改查_权限组功能开发设计.md
代码:com.ubains.controller.system.PermissionGroupController
## 目标
1.调整读取permission.json权限配置文件(getAllPermissions)和获取用户最终权限方法(getUserPermissions),
原permission.json的数据结构为`{"permission1":{"create":1,"delete":0}}`
调整后的数据结构为`{"class1":{"permission1":{"create":0,"delete":0,"edit":0}},"class2":{"permission1":{"create":0,"delete":0,"edit":0}}}`,原来的基础上增加了一层权限分类。
2.getAllPermissions方法返回的数据要求permission.json的数据顺序一致。
### 其他
1.把AI的总结内容输出ubains-meeting-parent\Docs\Doc\权限管理目录下的“PRD_权限组管理_权限结构_调整权限数据结构的总结.md”
2.日志和注释要详细,尤其是日志,详细到每个步骤和返回,采用LogsUtil的logxxxInverted方法
\ No newline at end of file
# 需求文档
## 概述
给基础角色添加基本权限组。
基础角色:
- `超级管理员` 角色id = 1
- `系统管理员` 角色id = 6
- `安全管理员` 角色id = 7
- `审计管理员` 角色id = 8
相关文件和代码:
- com.ubains.meeting.common.AdjustmentDb.adjustmentDb 数据库迁移方法,系统初始化时执行
- com.ubains.meeting.system.entity.Role 角色实体类
- com.ubains.meeting.system.entity.PermissionGroup 权限组实体类
-
- 权限组设计相关文件:
- Docs/PRD/PRD_系统/权限管理 该目录下的其他文档是原来权限组设计的文档
- ubains-meeting-inner-api/src/main/resources/config/permission.yml 权限文件
### 任务
- 给基础角色添加基本权限组,权限组名称对应角色名称
- **权限组按公司编号隔离**:每个公司都需要创建4个基础权限组,通过`company_number`字段区分
-`PermissionGroupService`中实现初始化逻辑,在`adjustmentDb`方法中调用
- 超级管理员读取`permission.yml`文件获取所有权限,作为权限组的权限,关联角色id为1
- 系统管理员读取`permission-system.yml`文件(需创建)获取所有权限,作为权限组的权限,关联角色id为6
- 安全管理员读取`permission-security.yml`文件(需创建)获取所有权限,作为权限组的权限,关联角色id为7
- 审计管理员读取`permission-audit.yml`文件(需创建)获取所有权限,作为权限组的权限,关联角色id为8
- 基础权限组的`isBase`字段值为1
- **ID说明**`role_id`固定为1/6/7/8,`group_id`和关联表`id`为自增主键,通过`is_base`+`group_name`+`company_number`判断权限组是否存在
- **优化 `PermissionUtil#loadPermissionConfig()` 方法**:新增带参数的重载方法 `loadPermissionConfig(String fileName)`,支持读取指定的权限配置文件(如 `permission-system.yml`
- 开发前先给出计划执行方案文档到在`Docs/PRD/PRD_系统/权限管理/同名文件+_计划执行.md`
- 生成的计划执行方案文档需添加任务:生成ai输出的总结内容到`Docs/Doc/权限管理/Doc_+同名文件(不包含前缀PRD_)+的总结.md`
### 方法总结
- Docs/PRD/PRD模板、规范/_PRD_方法总结_记录文档.md
### 代码规范
- Docs/PRD/PRD模板、规范/_PRD_规范文档_代码规范.md
### 文档规范
- Docs/PRD/PRD模板、规范/_PRD_规范文档_文档规范.md
### 测试规范
- Docs/PRD/PRD模板、规范/_PRD_规范文档_测试规范.md
### 记录文档
- Docs/PRD/PRD模板、规范/_PRD_问题总结_记录文档.md
# 需求文档
## 概述
权限组目前需要查看详情功能,详情除了基本权限组信息外,还需要有已绑定的角色、部门和用户信息。
**权限组的控制层**:com.ubains.controller.system.PermissionGroupController
## 任务
-`权限组的控制层`上添加新的接口,该接口用于查看权限组详情,该详情包含权限组基本信息、已绑定的角色、部门、用户信息
- 角色、部门、用户信息,不需要多余信息,主要显示名称和id即可
- 注意接口返回要结果加密,使用`@EncryptResult`注解
- 先给出计划执行方案文档,将文档创建在`Docs/PRD/系统管理/权限管理/同名文件+_计划执行.md`,等审批完文档再由我考虑是否按计划执行文档来执行
- 生成的计划执行方案文档需添加任务:生成API文档到`Docs/Doc/系统管理/权限管理/Doc_+同名文件(不包含前缀PRD_)+_API.md`
- 生成的计划执行方案文档需添加任务:生成ai输出的总结内容到`Docs/Doc/系统管理/权限管理/Doc_+同名文件(不包含前缀PRD_)+的总结.md`
## 规范
严格执行以下文档
#### 代码规范
- Docs/PRD/_PRD_规范文档_代码规范.md
#### 文档规范
- Docs/PRD/_PRD_规范文档_文档规范.md
#### 测试规范
- Docs/PRD/_PRD_规范文档_测试规范.md
\ No newline at end of file
# AI完善测试用例工具 - 依赖包列表
# Anthropic AI SDK - Claude API
anthropic>=0.18.0
# Excel文件处理
openpyxl>=3.1.0
# Word文档处理
python-docx>=1.1.0
# 数据处理(可选,用于高级分析)
pandas>=2.0.0
# 环境变量管理
python-dotenv>=1.0.0
# 其他依赖
python-dateutil>=2.8.0
# -*- coding: utf-8 -*-
"""
AI完善测试用例工具 - 入口脚本
使用方法:
python run.py # 使用默认配置
python run.py --help # 显示帮助信息
python run.py --log-level DEBUG # 设置日志级别
"""
import sys
import os
import argparse
from pathlib import Path
# 添加项目根目录到Python路径
PROJECT_ROOT = Path(__file__).parent
sys.path.insert(0, str(PROJECT_ROOT))
# 加载环境变量
try:
from dotenv import load_dotenv
env_file = PROJECT_ROOT / ".env"
if env_file.exists():
load_dotenv(env_file)
# 已自动加载 .env 文件
except ImportError:
pass # python-dotenv 未安装,跳过
from src.config import (
REQUIREMENTS_DIR,
PRD_DIR,
TEST_CASE_FILE,
OUTPUT_TEST_CASE,
OUTPUT_REPORT,
API_KEY_ENV,
get_project_info,
AI_MODEL
)
def print_banner() -> None:
"""打印欢迎横幅"""
print("=" * 60)
print(" AI完善测试用例工具 v1.0.0")
print("=" * 60)
print()
def print_project_info() -> None:
"""打印项目信息"""
info = get_project_info()
print("【项目配置】")
print(f" 项目根目录: {info['project_root']}")
print(f" 需求文档目录: {info['requirements_dir']}")
print(f" PRD文档目录: {info['prd_dir']}")
print(f" 测试用例文件: {info['test_case_file']}")
print(f" AI模型: {info['ai_model']}")
print(f" 覆盖率目标: {info['coverage_target'] * 100}%")
print()
def check_environment() -> bool:
"""
检查运行环境
Returns:
环境是否正常
"""
print("【环境检查】")
# 检查API密钥
api_key = os.environ.get(API_KEY_ENV)
if api_key:
masked_key = api_key[:8] + "..." if len(api_key) > 8 else "***"
print(f" ✓ API密钥: {masked_key}")
else:
print(f" ✗ API密钥: 未设置 ({API_KEY_ENV})")
print()
print("请设置API密钥:")
print(f" Windows: set {API_KEY_ENV}=your-key-here")
print(f" Linux/Mac: export {API_KEY_ENV}=your-key-here")
return False
# 检查需求文档目录
if REQUIREMENTS_DIR.exists():
req_files = list(REQUIREMENTS_DIR.glob("*.docx"))
print(f" ✓ 需求文档: {len(req_files)} 份")
else:
print(f" ✗ 需求文档目录不存在: {REQUIREMENTS_DIR}")
return False
# 检查PRD文档目录
if PRD_DIR.exists():
prd_files = list(PRD_DIR.glob("*.md"))
print(f" ✓ PRD文档: {len(prd_files)} 份")
else:
print(f" ✗ PRD文档目录不存在: {PRD_DIR}")
return False
# 检查测试用例文件
if TEST_CASE_FILE.exists():
print(f" ✓ 测试用例文件: {TEST_CASE_FILE.name}")
else:
print(f" ✗ 测试用例文件不存在: {TEST_CASE_FILE}")
return False
print()
return True
def test_api_connection() -> int:
"""
测试API连接
Returns:
退出码(0表示成功,非0表示失败)
"""
print("【测试API连接】")
print()
api_key = os.environ.get(API_KEY_ENV)
if not api_key:
print("✗ 未设置API密钥")
print(f" 请设置环境变量: {API_KEY_ENV}")
print(" 或在 .env 文件中配置")
return 1
# 隐藏密钥显示
masked_key = api_key[:8] + "..." if len(api_key) > 8 else "***"
print(f"API密钥: {masked_key}")
print(f"AI模型: {AI_MODEL}")
print()
try:
import anthropic
client = anthropic.Anthropic(api_key=api_key)
print("发送测试请求...")
response = client.messages.create(
model=AI_MODEL,
max_tokens=50,
messages=[{"role": "user", "content": "请回复:API连接成功"}]
)
result = response.content[0].text
print()
print("✓ API连接正常")
print(f" AI回复: {result}")
return 0
except anthropic.AuthenticationError:
print()
print("✗ API密钥认证失败")
print(" 请检查API密钥是否正确")
return 1
except anthropic.PermissionDeniedError:
print()
print("✗ API权限不足")
print(" 请检查API密钥是否有调用权限")
return 1
except anthropic.RateLimitError:
print()
print("✗ API调用频率超限")
print(" 请稍后再试")
return 1
except anthropic.APIError as e:
print()
print(f"✗ API错误: {e}")
return 1
except Exception as e:
print()
print(f"✗ 连接失败: {e}")
return 1
def parse_arguments() -> argparse.Namespace:
"""
解析命令行参数
Returns:
解析后的参数对象
"""
parser = argparse.ArgumentParser(
description="AI完善测试用例工具 - 通过AI分析需求文档和PRD文档,自动完善测试用例",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
示例用法:
python run.py # 使用默认配置运行
python run.py --log-level DEBUG # 启用详细日志
python run.py --show-info # 显示项目信息
python run.py --mode api # 强制使用API模式
python run.py --mode cli # 强制使用CLI模式
python run.py --api-key sk-ant-... # 指定API密钥
环境变量:
ANTHROPIC_API_KEY # Anthropic API密钥(API模式必需)
AI_SERVICE_MODE # AI服务模式:auto/api/cli
输出文件:
完善后的测试用例: Perfected/新统一平台权限管理测试用例_完善版本.xlsx
差异性报告: reports/新统一平台权限管理测试用例_差异性报告.docx
"""
)
parser.add_argument(
'--log-level',
type=str,
dest='log_level',
default='INFO',
choices=['DEBUG', 'INFO', 'WARNING', 'ERROR'],
help='日志级别(默认为INFO)'
)
parser.add_argument(
'--show-info',
action='store_true',
dest='show_info',
help='显示项目信息后退出'
)
parser.add_argument(
'--check-only',
action='store_true',
dest='check_only',
help='仅检查环境,不执行处理'
)
parser.add_argument(
'--test-api',
action='store_true',
dest='test_api',
help='测试API连接'
)
parser.add_argument(
'--use-cli',
action='store_true',
dest='use_cli',
help='强制使用Claude CLI模式(已弃用,建议使用--mode cli)'
)
parser.add_argument(
'--mode',
type=str,
dest='mode',
default=None,
choices=['auto', 'api', 'cli'],
help='AI服务模式:auto(自动检测)、api(Anthropic API)、cli(Claude CLI)'
)
parser.add_argument(
'--api-key',
type=str,
dest='api_key',
default=None,
help='Anthropic API密钥(覆盖环境变量)'
)
return parser.parse_args()
def run_cli(args: argparse.Namespace) -> int:
"""
运行命令行程序
Args:
args: 命令行参数
Returns:
退出码(0表示成功,非0表示失败)
"""
# 设置AI服务模式(通过环境变量传递)
if args.mode:
os.environ["AI_SERVICE_MODE"] = args.mode
# 设置API密钥(通过环境变量传递)
if args.api_key:
os.environ[API_KEY_ENV] = args.api_key
# 测试API连接
if args.test_api:
return test_api_connection()
# 打印横幅
print_banner()
# 仅显示信息
if args.show_info:
print_project_info()
return 0
# 仅检查环境
if args.check_only:
if check_environment():
print("✓ 环境检查通过")
return 0
else:
print("✗ 环境检查失败")
return 1
# 检查环境
if not check_environment():
return 1
# 打印项目信息
print_project_info()
# 导入主模块并运行
from src.main import main as run_main
print("【开始处理】")
print()
return run_main(use_cli=args.use_cli)
def main() -> int:
"""
主入口函数
Returns:
退出码
"""
args = parse_arguments()
return run_cli(args)
if __name__ == "__main__":
try:
exit_code = main()
sys.exit(exit_code)
except KeyboardInterrupt:
print("\n\n操作已取消")
sys.exit(130)
except Exception as e:
print(f"\n错误: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
# -*- coding: utf-8 -*-
"""
AI完善测试用例工具
本工具通过结合已有测试用例、需求文档、开发需求PRD文档,
利用AI能力对测试用例进行查漏补缺,完善测试用例内容。
"""
__version__ = "1.0.0"
__author__ = "AI Test Case Enhancer"
# -*- coding: utf-8 -*-
"""
AI服务管理器模块
统一管理Anthropic API和Claude CLI两种调用方式
"""
import os
import subprocess
from typing import Optional, List, Dict, Any
from logging import getLogger
from src.config import API_KEY_ENV, API_KEY_PREFIX, AI_MODEL, get_api_base_url
logger = getLogger(__name__)
class AIServiceManager:
"""
AI服务管理器
自动选择并管理AI服务(API或CLI)
"""
def __init__(self, api_key: Optional[str] = None, use_cli: bool = False):
"""
初始化AI服务管理器
Args:
api_key: Anthropic API密钥(可选)
use_cli: 是否强制使用CLI模式
"""
self.mode = self._determine_mode(api_key, use_cli)
self.cli_adapter = None
self.api_client = None
if self.mode == "cli":
self._init_cli_mode()
elif self.mode == "api":
self._init_api_mode(api_key)
logger.info("AI服务管理器初始化完成,模式: %s", self.mode)
def _determine_mode(self, api_key: Optional[str], use_cli: bool) -> str:
"""确定使用哪种模式"""
# 检查CLI是否可用
cli_available = self._check_cli_available()
if use_cli:
if not cli_available:
raise RuntimeError("强制使用CLI模式,但Claude CLI不可用")
return "cli"
if api_key and api_key.strip():
return "api"
# 自动检测
api_key_from_env = os.environ.get(API_KEY_ENV, "")
if api_key_from_env and api_key_from_env.strip():
return "api"
# 回退到CLI模式(如果可用)
if cli_available:
return "cli"
# 都不可用,抛出异常
raise RuntimeError(
"未找到可用的AI服务。请配置以下之一:\n"
f" 1. 设置环境变量 {API_KEY_ENV}\n"
" 2. 安装Claude CLI: npm install -g @anthropic-ai/claude-code"
)
def _check_cli_available(self) -> bool:
"""检查Claude CLI是否可用"""
try:
result = subprocess.run(
["claude", "--version"],
capture_output=True,
timeout=5
)
return result.returncode == 0
except (FileNotFoundError, subprocess.TimeoutExpired):
return False
def _init_cli_mode(self):
"""初始化CLI模式"""
try:
from src.claude_cli_adapter import ClaudeCLIAdapter
self.cli_adapter = ClaudeCLIAdapter()
if not self.cli_adapter.check_available():
raise RuntimeError("Claude CLI不可用")
logger.info("Claude CLI模式已启用")
except ImportError:
raise RuntimeError("无法导入Claude CLI适配器")
def _init_api_mode(self, api_key: str):
"""初始化API模式"""
try:
import anthropic
base_url = get_api_base_url()
kwargs = {"api_key": api_key}
if base_url != "https://api.anthropic.com":
kwargs["base_url"] = base_url
logger.info("使用自定义API endpoint: %s", base_url)
self.api_client = anthropic.Anthropic(**kwargs)
logger.info("Anthropic API模式已启用")
except ImportError:
raise RuntimeError("未安装anthropic库,请运行: pip install anthropic")
def ask_json(self, prompt: str, model: Optional[str] = None) -> Any:
"""
发送提示词并返回JSON结果
Args:
prompt: 提示词
model: 模型名称(可选)
Returns:
解析后的JSON对象
"""
model = model or AI_MODEL
if self.mode == "cli":
# CLI模式:适配器会自动处理JSON解析
return self.cli_adapter.ask_json(prompt, model)
else:
# API模式:需要手动调用API并解析
response = self.api_client.messages.create(
model=model,
max_tokens=8192,
temperature=0.3,
messages=[{"role": "user", "content": prompt}]
)
result_text = response.content[0].text
return self._parse_json(result_text)
def ask_json_array(self, prompt: str, model: Optional[str] = None) -> List[Dict]:
"""
发送提示词并返回JSON数组结果
Args:
prompt: 提示词
model: 模型名称(可选)
Returns:
解析后的JSON数组
"""
model = model or AI_MODEL
if self.mode == "cli":
# CLI模式:适配器会自动处理JSON数组解析
return self.cli_adapter.ask_json_array(prompt, model)
else:
# API模式:需要手动调用API并解析
response = self.api_client.messages.create(
model=model,
max_tokens=8192,
temperature=0.3,
messages=[{"role": "user", "content": prompt}]
)
result_text = response.content[0].text
return self._parse_json_array(result_text)
def _extract_json(self, text: str) -> str:
"""
从文本中提取JSON字符串
支持多种格式:
1. 纯JSON
2. 包含额外文本
3. 代码块格式
4. 多个JSON对象(取第一个)
Args:
text: 包含JSON的文本
Returns:
提取的JSON字符串,空字符串表示未找到
"""
import re
import json
text = text.strip()
# 尝试直接解析
try:
json.loads(text)
return text
except:
pass
# 尝试从代码块中提取
code_block_pattern = r'```(?:json)?\s*\n?([\s\S]*?)\n?```'
matches = re.findall(code_block_pattern, text)
for match in matches:
try:
json.loads(match.strip())
return match.strip()
except:
continue
# 尝试提取花括号内容(对象)- 使用正则找到第一个完整对象
# 使用括号匹配来处理嵌套
brace_count = 0
start_idx = -1
for i, char in enumerate(text):
if char == '{':
if brace_count == 0:
start_idx = i
brace_count += 1
elif char == '}':
brace_count -= 1
if brace_count == 0 and start_idx >= 0:
try:
json_str = text[start_idx:i+1]
json.loads(json_str)
return json_str
except:
break
# 尝试提取方括号内容(数组)
bracket_count = 0
start_idx = -1
for i, char in enumerate(text):
if char == '[':
if bracket_count == 0:
start_idx = i
bracket_count += 1
elif char == ']':
bracket_count -= 1
if bracket_count == 0 and start_idx >= 0:
try:
json_str = text[start_idx:i+1]
json.loads(json_str)
return json_str
except:
break
return ""
def _parse_json(self, text: str) -> Dict:
"""解析JSON文本"""
import json
logger.debug("尝试解析JSON,内容长度: %d", len(text))
# 使用增强的提取方法
json_str = self._extract_json(text)
if json_str:
try:
result = json.loads(json_str)
# 如果返回的是数组,取第一个元素(兼容智谱AI等返回格式)
if isinstance(result, list):
if len(result) > 0:
if isinstance(result[0], dict):
logger.info("JSON解析结果是数组(%d个元素),取第一个元素", len(result))
return result[0]
else:
logger.warning("JSON数组第一个元素不是对象类型: %s", type(result[0]))
else:
logger.warning("JSON数组为空")
raise ValueError("JSON解析结果为空数组或格式不正确")
return result
except json.JSONDecodeError as e:
logger.error("JSON解析失败: %s", e)
logger.debug("提取的JSON字符串: %s", json_str[:500])
raise ValueError(f"无法解析JSON: {text[:200]}")
def _parse_json_array(self, text: str) -> List:
"""解析JSON数组文本"""
import json
logger.debug("尝试解析JSON数组,内容长度: %d", len(text))
# 使用增强的提取方法
json_str = self._extract_json(text)
if json_str:
try:
result = json.loads(json_str)
if isinstance(result, list):
return result
else:
# 如果是对象,尝试提取数组字段
if isinstance(result, dict):
# 检查是否是标准的gap_detector返回格式(包含三个键)
gap_keys = {"功能点缺失", "场景覆盖不足", "边界条件不足"}
if gap_keys.issubset(result.keys()):
# 这是gap_detector的完整返回格式,直接返回整个对象
logger.info("识别到完整的gap检测结果格式")
return result
# 否则返回第一个找到的数组(兼容旧逻辑)
for key, value in result.items():
if isinstance(value, list):
logger.info("从JSON对象中提取数组: %s", key)
return value
return [result]
except json.JSONDecodeError as e:
logger.error("JSON数组解析失败: %s", e)
logger.debug("提取的JSON字符串: %s", json_str[:500])
# 如果解析失败,记录原始文本以便调试
logger.warning("JSON数组解析失败,记录原始内容(前1000字符):")
logger.warning(text[:1000])
raise ValueError(f"无法解析JSON数组: {text[:200]}")
@staticmethod
def create(auto_select: bool = True) -> 'AIServiceManager':
"""
工厂方法:创建AI服务管理器
Args:
auto_select: 是否自动选择模式(默认True)
Returns:
AI服务管理器实例
"""
if auto_select:
return AIServiceManager()
else:
# 使用API模式
api_key = os.environ.get(API_KEY_ENV)
if not api_key or not api_key.strip():
raise ValueError("未配置API密钥")
return AIServiceManager(api_key=api_key)
# -*- coding: utf-8 -*-
"""
用例生成模块
本模块负责使用AI生成缺失的测试用例
"""
from typing import List, Dict, Optional
from logging import getLogger
import json
import re
from datetime import datetime
from src.config import (
AI_MODEL, AI_MAX_TOKENS, AI_TEMPERATURE,
TEST_CASE_COLUMNS, CASE_LEVELS, DEFAULT_CASE_LEVEL,
DEFAULT_TEST_FREQUENCY, TEST_FREQUENCIES
)
logger = getLogger(__name__)
class CaseGenerator:
"""
测试用例生成器
主要功能:
1. 为功能点缺失生成测试用例
2. 为场景覆盖不足生成测试用例
3. 为边界条件不足生成测试用例
支持两种AI服务模式:
- Anthropic API:使用官方API密钥
- Claude CLI:使用本地CLI工具
"""
def __init__(self, api_key: Optional[str] = None, use_cli: bool = False):
"""
初始化生成器
Args:
api_key: Anthropic API密钥(可选,兼容性保留)
use_cli: 是否使用CLI模式
"""
from src.ai_service_manager import AIServiceManager
# 使用AI服务管理器
self.ai_service = AIServiceManager(api_key=api_key, use_cli=use_cli)
self.case_counter = 1 # 用于生成序列号
logger.info("测试用例生成器初始化完成")
def generate_cases(self, gaps: Dict, requirements: List[Dict],
prds: List[Dict], existing_cases: List[Dict]) -> List[Dict]:
"""
生成缺失的测试用例
Args:
gaps: 缺失检测结果
requirements: 需求文档列表
prds: PRD文档列表
existing_cases: 现有测试用例列表
Returns:
新生成的测试用例列表
"""
logger.info("开始生成测试用例...")
new_cases = []
# 获取下一个序列号
next_seq = self._get_next_sequence(existing_cases)
# 为功能点缺失生成用例
function_cases = self._generate_cases_for_functions(
gaps.get("功能点缺失", []),
requirements, prds,
next_seq
)
new_cases.extend(function_cases)
next_seq += len(function_cases)
# 为场景覆盖不足生成用例
scenario_cases = self._generate_cases_for_scenarios(
gaps.get("场景覆盖不足", []),
requirements, prds,
next_seq
)
new_cases.extend(scenario_cases)
next_seq += len(scenario_cases)
# 为边界条件不足生成用例
boundary_cases = self._generate_cases_for_boundaries(
gaps.get("边界条件不足", []),
requirements, prds,
next_seq
)
new_cases.extend(boundary_cases)
logger.info("测试用例生成完成,共 %d 条", len(new_cases))
return new_cases
def _get_next_sequence(self, existing_cases: List[Dict]) -> int:
"""
获取下一个序列号
Args:
existing_cases: 现有测试用例列表
Returns:
下一个序列号
"""
max_seq = 0
for case in existing_cases:
seq = case.get("序列号")
if isinstance(seq, int):
max_seq = max(max_seq, seq)
elif isinstance(seq, str):
try:
max_seq = max(max_seq, int(seq))
except ValueError:
pass
return max_seq + 1
def _generate_cases_for_functions(self, function_gaps: List[Dict],
requirements: List[Dict],
prds: List[Dict],
start_seq: int) -> List[Dict]:
"""
为功能点缺失生成测试用例
Args:
function_gaps: 功能点缺失列表
requirements: 需求文档列表
prds: PRD文档列表
start_seq: 起始序列号
Returns:
生成的测试用例列表
"""
logger.info("为 %d 个功能点缺失生成测试用例...", len(function_gaps))
cases = []
for i, gap in enumerate(function_gaps):
try:
case = self._generate_case_for_function(gap, requirements, prds, start_seq + i)
if case:
cases.append(case)
except Exception as e:
logger.error("生成功能点测试用例失败: %s, 错误: %s",
gap.get("功能描述", ""), e)
logger.info("功能点测试用例生成完成: %d 条", len(cases))
return cases
def _generate_case_for_function(self, gap: Dict, requirements: List[Dict],
prds: List[Dict], seq: int) -> Optional[Dict]:
"""为单个功能点缺失生成测试用例"""
prompt = f"""请为以下缺失的功能点生成完整的测试用例。
【功能信息】
- 功能模块:{gap.get('功能模块', '')}
- 功能编号:{gap.get('功能编号', '')}
- 功能描述:{gap.get('功能描述', '')}
【要求】
1. 生成详细的测试步骤
2. 预期结果要明确具体
3. 考虑正常流程和基本验证
请输出JSON格式的测试用例:
"""
prompt += json.dumps({
col: "" for col in TEST_CASE_COLUMNS
}, ensure_ascii=False)
prompt += f"""
注意:
1. 必须输出合法的JSON格式
2. 用例编号格式:TC-{gap.get('功能模块', 'TEST')[:4].upper()}-{seq:03d}
3. 用例等级:{gap.get('优先级', DEFAULT_CASE_LEVEL)}
4. STEP要包含详细的操作步骤,每步一行
5. JSON字段填写:暂不涉及自动化测试
6. 测试频次:{DEFAULT_TEST_FREQUENCY}
"""
return self._call_ai_for_case(prompt, gap.get("功能描述", ""))
def _generate_cases_for_scenarios(self, scenario_gaps: List[Dict],
requirements: List[Dict],
prds: List[Dict],
start_seq: int) -> List[Dict]:
"""为场景覆盖不足生成测试用例"""
logger.info("为 %d 个场景覆盖不足生成测试用例...", len(scenario_gaps))
cases = []
for i, gap in enumerate(scenario_gaps):
try:
case = self._generate_case_for_scenario(gap, requirements, prds, start_seq + i)
if case:
cases.append(case)
except Exception as e:
logger.error("生成场景测试用例失败: %s, 错误: %s",
gap.get("缺失场景", ""), e)
logger.info("场景测试用例生成完成: %d 条", len(cases))
return cases
def _generate_case_for_scenario(self, gap: Dict, requirements: List[Dict],
prds: List[Dict], seq: int) -> Optional[Dict]:
"""为单个场景覆盖不足生成测试用例"""
prompt = f"""请为以下缺失的场景生成完整的测试用例。
【功能信息】
- 功能描述:{gap.get('功能描述', '')}
- 已有场景:{gap.get('已有场景', '')}
- 缺失场景:{gap.get('缺失场景', '')}
【要求】
1. 专门针对缺失场景设计测试步骤
2. 预期结果要明确描述该场景的预期行为
3. 考虑异常处理和边界情况
请输出JSON格式的测试用例:
"""
prompt += json.dumps({
col: "" for col in TEST_CASE_COLUMNS
}, ensure_ascii=False)
prompt += f"""
注意:
1. 必须输出合法的JSON格式
2. 用例名称:{gap.get('建议用例名称', f'测试{gap.get("功能描述", "")}{gap.get("缺失场景", "")}')}
3. 用例等级:{gap.get('优先级', DEFAULT_CASE_LEVEL)}
4. STEP要详细描述如何触发该场景
5. 预期结果要描述该场景的预期行为
"""
return self._call_ai_for_case(prompt, gap.get("缺失场景", ""))
def _generate_cases_for_boundaries(self, boundary_gaps: List[Dict],
requirements: List[Dict],
prds: List[Dict],
start_seq: int) -> List[Dict]:
"""为边界条件不足生成测试用例"""
logger.info("为 %d 个边界条件不足生成测试用例...", len(boundary_gaps))
cases = []
for i, gap in enumerate(boundary_gaps):
try:
case = self._generate_case_for_boundary(gap, requirements, prds, start_seq + i)
if case:
cases.append(case)
except Exception as e:
logger.error("生成边界条件测试用例失败: %s, 错误: %s",
gap.get("缺失边界", ""), e)
logger.info("边界条件测试用例生成完成: %d 条", len(cases))
return cases
def _generate_case_for_boundary(self, gap: Dict, requirements: List[Dict],
prds: List[Dict], seq: int) -> Optional[Dict]:
"""为单个边界条件不足生成测试用例"""
prompt = f"""请为以下缺失的边界条件生成完整的测试用例。
【功能信息】
- 功能描述:{gap.get('功能描述', '')}
- 已有边界:{gap.get('已有边界', '')}
- 缺失边界:{gap.get('缺失边界', '')}
【要求】
1. 专门针对该边界条件设计测试步骤
2. 预期结果要明确描述边界值的行为
3. 考虑边界值附近的行为
请输出JSON格式的测试用例:
"""
prompt += json.dumps({
col: "" for col in TEST_CASE_COLUMNS
}, ensure_ascii=False)
prompt += f"""
注意:
1. 必须输出合法的JSON格式
2. 用例名称:{gap.get('建议用例名称', f'测试{gap.get("功能描述", "")}{gap.get("缺失边界", "")}')}
3. 用例等级:{gap.get('优先级', DEFAULT_CASE_LEVEL)}
4. STEP要详细描述如何测试该边界条件
5. 预期结果要描述边界值和附近值的预期行为
"""
return self._call_ai_for_case(prompt, gap.get("缺失边界", ""))
def _call_ai_for_case(self, prompt: str, description: str) -> Optional[Dict]:
"""
调用AI生成单个测试用例
Args:
prompt: 提示词
description: 用例描述(用于日志)
Returns:
测试用例字典,失败返回None
"""
try:
# 使用AI服务管理器调用
case = self.ai_service.ask_json(prompt, AI_MODEL)
# 类型检查:确保返回的是字典(兼容智谱AI等返回数组的情况)
if isinstance(case, list):
logger.debug("AI返回了数组,长度: %d", len(case))
if len(case) > 0 and isinstance(case[0], dict):
logger.info("AI返回了数组,取第一个元素作为测试用例")
case = case[0]
else:
logger.error("AI返回的数组格式不符合预期,第一个元素类型: %s",
type(case[0]) if len(case) > 0 else "空数组")
return None
if not isinstance(case, dict):
logger.error("AI返回的不是字典对象: %s", type(case))
logger.debug("返回内容: %s", str(case)[:200])
return None
# 设置默认值
if not case.get("测试频次"):
case["测试频次"] = DEFAULT_TEST_FREQUENCY
if not case.get("JSON"):
case["JSON"] = "暂不涉及"
return case
except Exception as e:
logger.error("生成测试用例失败(%s): %s", description, e)
logger.debug("错误详情", exc_info=True)
return None
def _parse_case_result(self, result: str) -> Optional[Dict]:
"""
解析AI返回的测试用例
Args:
result: AI返回的文本
Returns:
测试用例字典,失败返回None
"""
import re
logger.debug("尝试解析测试用例JSON,内容长度: %d", len(result))
# 尝试直接解析
try:
case = json.loads(result)
return self._validate_case(case)
except json.JSONDecodeError:
pass
# 尝试从代码块中提取
code_block_pattern = r'```(?:json)?\s*\n?([\s\S]*?)\n?```'
matches = re.findall(code_block_pattern, result)
for match in matches:
try:
case = json.loads(match.strip())
return self._validate_case(case)
except:
continue
# 尝试提取花括号内容(对象)- 使用括号匹配处理嵌套
brace_count = 0
start_idx = -1
for i, char in enumerate(result):
if char == '{':
if brace_count == 0:
start_idx = i
brace_count += 1
elif char == '}':
brace_count -= 1
if brace_count == 0 and start_idx >= 0:
try:
json_str = result[start_idx:i+1]
case = json.loads(json_str)
return self._validate_case(case)
except:
break
logger.warning("未找到有效的JSON对象格式")
logger.debug("AI返回内容: %s", result[:500])
return None
def _validate_case(self, case: Dict) -> Optional[Dict]:
"""
验证测试用例格式
Args:
case: 测试用例字典
Returns:
验证通过的用例,失败返回None
"""
# 检查必填字段
required_fields = ["用例名称", "功能描述", "STEP", "预期结果"]
for field in required_fields:
if not case.get(field):
logger.warning("测试用例缺少必填字段: %s", field)
# 设置默认值
if field == "用例名称":
case[field] = "自动生成的测试用例"
elif field == "STEP":
case[field] = "1. 执行操作\n2. 验证结果"
elif field == "预期结果":
case[field] = "操作成功,结果符合预期"
# 确保所有列都存在
for col in TEST_CASE_COLUMNS:
if col not in case:
case[col] = ""
return case
def format_case_number(module: str, seq: int) -> str:
"""
格式化用例编号
Args:
module: 功能模块
seq: 序号
Returns:
用例编号
"""
# 提取模块简称(前4个字符,大写)
module_short = module[:4].upper() if module else "TEST"
# 去除中文字符
module_short = re.sub(r'[^\w]', '', module_short)
if not module_short:
module_short = "TEST"
return f"TC-{module_short}-{seq:03d}"
# -*- coding: utf-8 -*-
"""
Claude CLI适配器模块
通过subprocess调用claude命令行工具
"""
import subprocess
import json
from typing import Optional, Dict, Any, List
from logging import getLogger
logger = getLogger(__name__)
class ClaudeCLIAdapter:
"""
Claude CLI适配器
通过调用本地的claude命令行工具来使用AI能力
"""
def __init__(self, timeout: int = 300):
"""
初始化适配器
Args:
timeout: 超时时间(秒),默认5分钟
"""
self.timeout = timeout
self.available = None
self.version = None
logger.info("Claude CLI适配器初始化完成")
def check_available(self) -> bool:
"""
检查claude命令是否可用
Returns:
True表示可用,False表示不可用
"""
if self.available is not None:
return self.available
try:
result = subprocess.run(
["claude", "--version"],
capture_output=True,
text=True,
timeout=10,
encoding="utf-8"
)
if result.returncode == 0:
self.version = result.stdout.strip()
self.available = True
logger.info("Claude CLI可用,版本: %s", self.version)
return True
else:
self.available = False
logger.warning("Claude CLI命令返回错误: %s", result.stderr)
return False
except FileNotFoundError:
self.available = False
logger.error("未找到claude命令,请确认已安装Claude Code CLI")
logger.error("安装方式: npm install -g @anthropic-ai/claude-code")
return False
except Exception as e:
self.available = False
logger.error("检查claude命令失败: %s", e)
return False
def ask(self, prompt: str, model: Optional[str] = None) -> str:
"""
调用claude ask命令
Args:
prompt: 提示词
model: 模型名称(可选,默认使用claude配置的默认模型)
Returns:
AI回复文本
"""
if not self.check_available():
raise RuntimeError("Claude CLI不可用")
logger.info("调用Claude CLI...")
logger.debug("提示词长度: %d 字符", len(prompt))
# 构建命令
cmd = ["claude", "ask"]
if model:
cmd.extend(["--model", model])
cmd.append(prompt)
try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=self.timeout,
encoding="utf-8"
)
if result.returncode == 0:
response = result.stdout.strip()
logger.info("Claude CLI调用成功,返回 %d 字符", len(response))
return response
else:
error_msg = result.stderr.strip()
logger.error("Claude CLI调用失败: %s", error_msg)
raise RuntimeError(f"Claude CLI error: {error_msg}")
except subprocess.TimeoutExpired:
logger.error("Claude CLI调用超时(%d秒)", self.timeout)
raise TimeoutError(f"Claude CLI调用超时({self.timeout}秒)")
except Exception as e:
logger.error("Claude CLI调用异常: %s", e)
raise
def ask_json(self, prompt: str, model: Optional[str] = None) -> Dict:
"""
调用claude ask命令,并解析JSON返回
Args:
prompt: 提示词
model: 模型名称(可选)
Returns:
解析后的JSON对象
"""
# 确保提示词要求JSON格式
if "JSON" not in prompt and "json" not in prompt.lower():
if prompt.endswith("。"):
prompt = prompt[:-1] + ",请输出JSON格式的结果。"
else:
prompt = prompt + "\n\n请输出JSON格式的结果。"
response = self.ask(prompt, model)
return self._parse_json_response(response)
def ask_json_array(self, prompt: str, model: Optional[str] = None) -> List[Dict]:
"""
调用claude ask命令,并解析JSON数组返回
Args:
prompt: 提示词
model: 模型名称(可选)
Returns:
解析后的JSON数组
"""
# 确保提示词要求JSON数组格式
if "JSON" not in prompt and "json" not in prompt.lower():
if prompt.endswith("。"):
prompt = prompt[:-1] + ",请输出JSON数组格式的结果。"
else:
prompt = prompt + "\n\n请输出JSON数组格式的结果。"
response = self.ask(prompt, model)
return self._parse_json_array_response(response)
def _parse_json_response(self, response: str) -> Dict:
"""
解析JSON响应
Args:
response: AI返回的文本
Returns:
解析后的JSON对象
"""
try:
return json.loads(response)
except json.JSONDecodeError:
# 尝试提取JSON部分
try:
start = response.find("{")
end = response.rfind("}") + 1
if start >= 0 and end > start:
json_str = response[start:end]
return json.loads(json_str)
except:
pass
logger.error("无法解析JSON响应")
logger.debug("响应内容: %s", response[:500])
raise ValueError("响应不是有效的JSON格式")
def _parse_json_array_response(self, response: str) -> List:
"""
解析JSON数组响应
Args:
response: AI返回的文本
Returns:
解析后的JSON数组
"""
try:
result = json.loads(response)
if isinstance(result, list):
return result
else:
# 如果返回的是对象,尝试提取数组字段
if isinstance(result, dict):
for key, value in result.items():
if isinstance(value, list):
logger.info("从JSON对象中提取数组: %s", key)
return value
return [result]
except json.JSONDecodeError:
# 尝试提取JSON部分
try:
start = response.find("[")
end = response.rfind("]") + 1
if start >= 0 and end > start:
json_str = response[start:end]
return json.loads(json_str)
except:
pass
logger.error("无法解析JSON数组响应")
logger.debug("响应内容: %s", response[:500])
raise ValueError("响应不是有效的JSON数组格式")
def test_claude_cli() -> Dict:
"""
测试Claude CLI连接
Returns:
测试结果字典
"""
adapter = ClaudeCLIAdapter()
result = {
"available": False,
"version": None,
"error": None
}
try:
if adapter.check_available():
result["available"] = True
result["version"] = adapter.version
# 测试调用
logger.info("发送测试请求...")
response = adapter.ask("请回复:测试成功", model="claude-sonnet-4-6")
if "测试成功" in response or "test" in response.lower():
result["test_passed"] = True
else:
result["test_passed"] = False
result["response"] = response
else:
result["error"] = "Claude CLI不可用"
except Exception as e:
result["error"] = str(e)
return result
# 便捷函数
def call_claude(prompt: str, model: Optional[str] = None, timeout: int = 300) -> str:
"""
便捷函数:调用Claude CLI
Args:
prompt: 提示词
model: 模型名称(可选)
timeout: 超时时间(秒)
Returns:
AI回复文本
"""
adapter = ClaudeCLIAdapter(timeout=timeout)
return adapter.ask(prompt, model)
def call_claude_json(prompt: str, model: Optional[str] = None, timeout: int = 300) -> Dict:
"""
便捷函数:调用Claude CLI并返回JSON
Args:
prompt: 提示词
model: 模型名称(可选)
timeout: 超时时间(秒)
Returns:
解析后的JSON对象
"""
adapter = ClaudeCLIAdapter(timeout=timeout)
return adapter.ask_json(prompt, model)
# -*- coding: utf-8 -*-
"""
AI完善测试用例工具 - 配置模块
本模块定义项目所需的配置常量和路径
"""
from pathlib import Path
# ==================== 项目路径 ====================
# 项目根目录
PROJECT_ROOT = Path(__file__).parent.parent
# 配置目录
CONFIG_DIR = PROJECT_ROOT / "config"
# 输出目录
PERFECTED_DIR = PROJECT_ROOT / "Perfected"
REPORTS_DIR = PROJECT_ROOT / "reports"
LOGS_DIR = PROJECT_ROOT / "logs"
# ==================== 输入路径 ====================
# 需求文档目录
REQUIREMENTS_DIR = CONFIG_DIR / "需求文档"
# 开发PRD文档目录
PRD_DIR = CONFIG_DIR / "开发PRD"
# 测试用例文件
TEST_CASE_FILE = CONFIG_DIR / "测试用例" / "新统一平台权限管理测试用例.xlsx"
# ==================== 输出路径 ====================
# 输出测试用例文件
OUTPUT_TEST_CASE = PERFECTED_DIR / "新统一平台权限管理测试用例_完善版本.xlsx"
# 输出差异性报告文件
OUTPUT_REPORT = REPORTS_DIR / "新统一平台权限管理测试用例_差异性报告.docx"
# ==================== 测试用例列定义 ====================
# 测试用例Excel表格的列名
TEST_CASE_COLUMNS = [
"序列号", # 序号,用于排序
"功能模块", # 功能模块名称,用于分类
"用例编号", # 用例编号,用于标识用例
"功能描述", # 功能描述,用于描述功能
"用例等级", # 用例等级,用于描述用例的优先级
"功能编号", # 功能编号,用于标识功能
"用例名称", # 用例名称,用于标识用例
"预置条件", # 测试用例的预置条件,用于准备测试环境
"STEP", # 步骤描述
"JSON", # 自动化测试用例,暂不涉及
"预期结果", # 测试用例的预期结果,用于描述测试结果
"测试结果", # 测试用例的测试结果,用于描述测试结果
"测试频次", # 测试用例的测试频次,用于描述测试用例的测试频次
"日志/截图/照片", # 用于记录测试过程中产生的日志、截图、照片
"备注" # 用于记录测试过程中产生的问题,用于问题排查
]
# ==================== AI配置 ====================
# AI模型选择
# 可选值: "claude-sonnet-4-6", "claude-opus-4-6"
AI_MODEL = "claude-sonnet-4-6"
# AI最大token数
AI_MAX_TOKENS = 8192
# AI温度参数(较低温度确保稳定性)
AI_TEMPERATURE = 0.3
# API密钥环境变量名
API_KEY_ENV = "ANTHROPIC_API_KEY"
# API基础URL(可选,用于代理服务)
# 官方API: https://api.anthropic.com
# CCSwitch等代理: 根据服务商提供的地址配置
API_BASE_URL = "https://api.anthropic.com"
# ==================== AI服务模式 ====================
# AI服务模式:auto(自动检测)、api(Anthropic API)、cli(Claude CLI)
AI_SERVICE_MODE = "auto"
# Claude CLI超时时间(秒)
CLI_TIMEOUT = 300
# API密钥格式前缀(用于验证)
API_KEY_PREFIX = "sk-ant-api03-"
# ==================== 覆盖率目标 ====================
# 场景覆盖率目标(90%)
COVERAGE_TARGET = 0.9
# ==================== 日志配置 ====================
# 日志格式
LOG_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
# 日志日期格式
LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
# 日志文件名
LOG_FILE = LOGS_DIR / "ai_test_case_enhancer.log"
# ==================== 用例编号格式 ====================
# 用例编号前缀
CASE_NUMBER_PREFIX = "TC"
# ==================== 用例等级定义 ====================
# 用例等级选项
CASE_LEVELS = ["P0", "P1", "P2", "P3"]
# 默认用例等级
DEFAULT_CASE_LEVEL = "P1"
# ==================== 测试频次定义 ====================
# 测试频次选项
TEST_FREQUENCIES = ["每次", "每日", "每周", "每月", "每版本", "一次性"]
# 默认测试频次
DEFAULT_TEST_FREQUENCY = "每版本"
def ensure_directories() -> None:
"""
确保所有必要的目录存在
"""
PERFECTED_DIR.mkdir(parents=True, exist_ok=True)
REPORTS_DIR.mkdir(parents=True, exist_ok=True)
LOGS_DIR.mkdir(parents=True, exist_ok=True)
def get_project_info() -> dict:
"""
获取项目信息
Returns:
项目信息字典
"""
return {
"project_root": str(PROJECT_ROOT),
"config_dir": str(CONFIG_DIR),
"requirements_dir": str(REQUIREMENTS_DIR),
"prd_dir": str(PRD_DIR),
"test_case_file": str(TEST_CASE_FILE),
"output_test_case": str(OUTPUT_TEST_CASE),
"output_report": str(OUTPUT_REPORT),
"ai_model": AI_MODEL,
"coverage_target": COVERAGE_TARGET
}
def get_ai_service_mode() -> str:
"""
获取AI服务模式
优先级:环境变量 > 配置文件常量
Returns:
服务模式:'auto', 'api', 'cli'
"""
import os
mode = os.environ.get("AI_SERVICE_MODE", AI_SERVICE_MODE).lower()
# 验证模式值
if mode not in ("auto", "api", "cli"):
return AI_SERVICE_MODE
return mode
def get_api_base_url() -> str:
"""
获取API基础URL
优先级:环境变量 > 配置文件常量
Returns:
API基础URL
"""
import os
return os.environ.get("API_BASE_URL", API_BASE_URL)
# -*- coding: utf-8 -*-
"""
去重模块
本模块负责对生成的测试用例进行去重处理
"""
from typing import List, Dict, Set
from logging import getLogger
import hashlib
logger = getLogger(__name__)
class Deduplicator:
"""
测试用例去重器
主要功能:
1. 检测重复的测试用例
2. 保留唯一用例
3. 生成去重报告
"""
def __init__(self):
"""初始化去重器"""
logger.info("测试用例去重器初始化完成")
def deduplicate(self, existing_cases: List[Dict],
new_cases: List[Dict]) -> Dict:
"""
去重处理
Args:
existing_cases: 现有测试用例列表
new_cases: 新生成的测试用例列表
Returns:
去重结果,包含:
- unique_cases: 唯一用例列表
- duplicate_cases: 重复用例列表
- duplicate_count: 重复用例数量
- summary: 去重统计
"""
logger.info("开始去重处理...")
logger.info("现有用例: %d 条,新生成用例: %d 条",
len(existing_cases), len(new_cases))
# 构建现有用例的指纹集合
existing_fingerprints = set()
for case in existing_cases:
fp = self._generate_fingerprint(case)
existing_fingerprints.add(fp)
logger.debug("现有用例指纹数: %d", len(existing_fingerprints))
# 筛选新用例
unique_cases = []
duplicate_cases = []
new_fingerprints = set()
for i, case in enumerate(new_cases):
fp = self._generate_fingerprint(case)
# 检查是否与现有用例重复
if fp in existing_fingerprints:
duplicate_cases.append({
**case,
"_duplicate_type": "existing",
"_fingerprint": fp
})
logger.debug("新用例 %d 与现有用例重复", i + 1)
continue
# 检查是否与新用例列表中的其他用例重复
if fp in new_fingerprints:
duplicate_cases.append({
**case,
"_duplicate_type": "new",
"_fingerprint": fp
})
logger.debug("新用例 %d 在新用例列表中重复", i + 1)
continue
# 唯一用例
unique_cases.append(case)
new_fingerprints.add(fp)
# 生成统计报告
summary = {
"existing_cases": len(existing_cases),
"new_cases": len(new_cases),
"unique_cases": len(unique_cases),
"duplicate_with_existing": len([c for c in duplicate_cases if c.get("_duplicate_type") == "existing"]),
"duplicate_within_new": len([c for c in duplicate_cases if c.get("_duplicate_type") == "new"]),
"total_duplicates": len(duplicate_cases),
"deduplication_rate": len(unique_cases) / len(new_cases) if new_cases else 0
}
logger.info("去重完成:")
logger.info(" 唯一新用例: %d 条", summary["unique_cases"])
logger.info(" 与现有用例重复: %d 条", summary["duplicate_with_existing"])
logger.info(" 新用例内部重复: %d 条", summary["duplicate_within_new"])
logger.info(" 总计重复: %d 条", summary["total_duplicates"])
logger.info(" 去重率: %.1f%%", summary["deduplication_rate"] * 100)
return {
"unique_cases": unique_cases,
"duplicate_cases": duplicate_cases,
"summary": summary
}
def _generate_fingerprint(self, case: Dict) -> str:
"""
生成用例指纹(用于去重)
Args:
case: 测试用例字典
Returns:
用例指纹字符串
"""
# 使用关键字段生成唯一标识
key_parts = [
str(case.get("功能描述", "")).strip(),
str(case.get("用例名称", "")).strip(),
str(case.get("功能编号", "")).strip()
]
# 将关键字段拼接后生成哈希
content = "|".join(key_parts).strip().lower()
# 如果内容为空,返回空字符串
if not content:
return ""
# 生成MD5哈希
return hashlib.md5(content.encode('utf-8')).hexdigest()
def fuzzy_deduplicate(self, cases: List[Dict],
similarity_threshold: float = 0.85) -> Dict:
"""
模糊去重(基于相似度)
Args:
cases: 测试用例列表
similarity_threshold: 相似度阈值
Returns:
模糊去重结果
"""
logger.info("开始模糊去重(相似度阈值: %.2f)...", similarity_threshold)
unique_cases = []
duplicate_cases = []
checked_indices = set()
for i, case1 in enumerate(cases):
if i in checked_indices:
continue
is_duplicate = False
for j, case2 in enumerate(cases):
if i >= j or j in checked_indices:
continue
similarity = self._calculate_similarity(case1, case2)
if similarity >= similarity_threshold:
# case2 与 case1 相似,标记为重复
duplicate_cases.append({
**case2,
"_duplicate_type": "fuzzy",
"_similar_to": i,
"_similarity": similarity
})
checked_indices.add(j)
is_duplicate = True
logger.debug("用例 %d 与用例 %d 相似(%.2f)", i, j, similarity)
if not is_duplicate:
unique_cases.append(case1)
checked_indices.add(i)
logger.info("模糊去重完成:")
logger.info(" 唯一用例: %d 条", len(unique_cases))
logger.info(" 相似重复: %d 条", len(duplicate_cases))
return {
"unique_cases": unique_cases,
"duplicate_cases": duplicate_cases
}
def _calculate_similarity(self, case1: Dict, case2: Dict) -> float:
"""
计算两个测试用例的相似度
Args:
case1: 测试用例1
case2: 测试用例2
Returns:
相似度(0-1之间)
"""
# 简单的文本相似度计算
def text_similarity(text1: str, text2: str) -> float:
"""计算两个文本的相似度"""
if not text1 or not text2:
return 0.0
words1 = set(text1.lower().split())
words2 = set(text2.lower().split())
if not words1 or not words2:
return 0.0
intersection = words1 & words2
union = words1 | words2
return len(intersection) / len(union) if union else 0.0
# 比较多个字段
fields = ["用例名称", "功能描述", "预期结果"]
similarities = []
for field in fields:
text1 = str(case1.get(field, ""))
text2 = str(case2.get(field, ""))
sim = text_similarity(text1, text2)
similarities.append(sim)
# 返回平均相似度
return sum(similarities) / len(similarities) if similarities else 0.0
def find_similar_cases(self, target_case: Dict,
case_list: List[Dict],
top_n: int = 5) -> List[Dict]:
"""
查找相似的测试用例
Args:
target_case: 目标测试用例
case_list: 测试用例列表
top_n: 返回前N个最相似的用例
Returns:
相似用例列表,每项包含用例和相似度
"""
similarities = []
for i, case in enumerate(case_list):
sim = self._calculate_similarity(target_case, case)
similarities.append({
"index": i,
"case": case,
"similarity": sim
})
# 按相似度降序排序
similarities.sort(key=lambda x: x["similarity"], reverse=True)
return similarities[:top_n]
def merge_test_cases(existing_cases: List[Dict],
new_cases: List[Dict]) -> List[Dict]:
"""
合并测试用例(不进行去重)
Args:
existing_cases: 现有测试用例列表
new_cases: 新测试用例列表
Returns:
合并后的测试用例列表
"""
# 更新序列号
max_seq = 0
for case in existing_cases:
seq = case.get("序列号")
if isinstance(seq, int):
max_seq = max(max_seq, seq)
# 为新用例分配序列号
for i, case in enumerate(new_cases):
max_seq += 1
case["序列号"] = max_seq
return existing_cases + new_cases
# -*- coding: utf-8 -*-
"""
文档读取模块
本模块负责读取需求文档(.docx)、开发PRD文档(.md)和测试用例(.xlsx)
"""
from pathlib import Path
from typing import List, Dict, Optional
from logging import getLogger
try:
from docx import Document
except ImportError:
print("警告: 未安装python-docx库,请运行: pip install python-docx")
raise
try:
import openpyxl
except ImportError:
print("警告: 未安装openpyxl库,请运行: pip install openpyxl")
raise
logger = getLogger(__name__)
class DocumentReader:
"""
文档读取器
负责读取三种类型的文档:
1. 需求文档(.docx格式)
2. 开发PRD文档(.md格式)
3. 测试用例(.xlsx格式)
"""
def __init__(self):
"""初始化文档读取器"""
logger.info("文档读取器初始化完成")
def read_requirement_docs(self, requirements_dir: Path) -> List[Dict]:
"""
读取需求文档(.docx)
Args:
requirements_dir: 需求文档目录路径
Returns:
需求文档列表,每项包含:
- filename: 文件名
- path: 文件路径
- content: 文档内容
"""
if not requirements_dir.exists():
logger.warning("需求文档目录不存在: %s", requirements_dir)
return []
docs = []
docx_files = list(requirements_dir.glob("*.docx"))
if not docx_files:
logger.warning("需求文档目录下没有找到.docx文件: %s", requirements_dir)
return []
logger.info("找到 %d 份需求文档", len(docx_files))
for docx_file in docx_files:
try:
logger.info("读取需求文档: %s", docx_file.name)
doc = Document(docx_file)
# 提取所有段落文本
paragraphs = []
for para in doc.paragraphs:
if para.text.strip():
paragraphs.append(para.text.strip())
# 提取表格内容
tables_content = []
for table in doc.tables:
for row in table.rows:
row_text = " | ".join([cell.text.strip() for cell in row.cells])
if row_text.strip():
tables_content.append(row_text)
content = "\n".join(paragraphs)
if tables_content:
content += "\n\n【表格内容】\n" + "\n".join(tables_content)
docs.append({
"filename": docx_file.name,
"path": str(docx_file),
"content": content
})
logger.debug("文档 %s 读取完成,内容长度: %d 字符",
docx_file.name, len(content))
except Exception as e:
logger.error("读取需求文档失败: %s, 错误: %s", docx_file.name, e)
logger.info("需求文档读取完成,共 %d 份", len(docs))
return docs
def read_prd_docs(self, prd_dir: Path) -> List[Dict]:
"""
读取开发PRD文档(.md)
Args:
prd_dir: PRD文档目录路径
Returns:
PRD文档列表,每项包含:
- filename: 文件名
- path: 文件路径
- content: 文档内容
"""
if not prd_dir.exists():
logger.warning("PRD文档目录不存在: %s", prd_dir)
return []
docs = []
md_files = list(prd_dir.glob("*.md"))
if not md_files:
logger.warning("PRD文档目录下没有找到.md文件: %s", prd_dir)
return []
logger.info("找到 %d 份PRD文档", len(md_files))
for md_file in md_files:
try:
logger.info("读取PRD文档: %s", md_file.name)
with open(md_file, "r", encoding="utf-8") as f:
content = f.read()
docs.append({
"filename": md_file.name,
"path": str(md_file),
"content": content
})
logger.debug("文档 %s 读取完成,内容长度: %d 字符",
md_file.name, len(content))
except Exception as e:
logger.error("读取PRD文档失败: %s, 错误: %s", md_file.name, e)
logger.info("PRD文档读取完成,共 %d 份", len(docs))
return docs
def read_test_cases(self, test_case_file: Path) -> List[Dict]:
"""
读取测试用例(.xlsx)
Args:
test_case_file: 测试用例文件路径
Returns:
测试用例列表,每项是一个字典,键为列名
"""
if not test_case_file.exists():
logger.error("测试用例文件不存在: %s", test_case_file)
raise FileNotFoundError(f"测试用例文件不存在: {test_case_file}")
logger.info("读取测试用例: %s", test_case_file.name)
try:
# 使用read_only和数据模式提高读取可靠性
wb = openpyxl.load_workbook(test_case_file, read_only=True, data_only=True)
# 打印工作表信息
logger.info("工作表列表: %s", wb.sheetnames)
# 使用活动工作表
ws = wb.active
logger.info("使用工作表: %s (大小: %d行 x %d列)",
ws.title, ws.max_row, ws.max_column)
# 查找表头行
headers = None
header_row = 1
for row_idx in range(1, min(20, ws.max_row + 1)):
row_values = [cell.value for cell in ws[row_idx]]
# 检查是否包含"用例名称"列
if any(cell.value and "用例名称" in str(cell.value) for cell in ws[row_idx]):
headers = row_values
header_row = row_idx
logger.info("在第%d行找到表头: %s", row_idx, headers)
break
# 如果没找到表头,使用第1行
if not headers:
logger.warning("未找到包含'用例名称'的表头行,使用第1行作为表头")
headers = [cell.value for cell in ws[1]]
header_row = 1
# 读取数据
cases = []
data_start_row = header_row + 1
empty_rows = 0
valid_rows = 0
for row_idx in range(data_start_row, ws.max_row + 1):
row_values = [cell.value for cell in ws[row_idx]]
# 跳过全空行
if not any(row_values):
empty_rows += 1
continue
# 构建用例字典
case = dict(zip(headers, row_values))
case_name = case.get("用例名称")
# 检查是否有有效内容
if case_name and str(case_name).strip():
case["_row_number"] = row_idx
cases.append(case)
valid_rows += 1
elif row_idx <= data_start_row + 10: # 只打印前10行的调试信息
logger.debug("第%d行:用例名称为空,跳过。行数据: %s",
row_idx, {k: v for k, v in case.items() if v})
wb.close()
logger.info("测试用例读取完成:")
logger.info(" 有效用例: %d 条", len(cases))
logger.info(" 空行跳过: %d 行", empty_rows)
# 统计信息
if cases:
modules = set()
for case in cases:
module = case.get("功能模块")
if module:
modules.add(module)
logger.info(" 功能模块: %s", sorted(modules))
return cases
except Exception as e:
logger.error("读取测试用例失败: %s", e)
import traceback
logger.debug(traceback.format_exc())
raise
def read_all_documents(self, requirements_dir: Path,
prd_dir: Path,
test_case_file: Path) -> Dict:
"""
一次性读取所有文档
Args:
requirements_dir: 需求文档目录
prd_dir: PRD文档目录
test_case_file: 测试用例文件
Returns:
包含所有文档的字典
"""
logger.info("=" * 60)
logger.info("开始读取所有文档...")
logger.info("=" * 60)
requirements = self.read_requirement_docs(requirements_dir)
prds = self.read_prd_docs(prd_dir)
test_cases = self.read_test_cases(test_case_file)
result = {
"requirements": requirements,
"prds": prds,
"test_cases": test_cases,
"summary": {
"requirements_count": len(requirements),
"prds_count": len(prds),
"test_cases_count": len(test_cases)
}
}
logger.info("=" * 60)
logger.info("所有文档读取完成:")
logger.info(" 需求文档: %d 份", result["summary"]["requirements_count"])
logger.info(" PRD文档: %d 份", result["summary"]["prds_count"])
logger.info(" 测试用例: %d 条", result["summary"]["test_cases_count"])
logger.info("=" * 60)
return result
def validate_documents(requirements_dir: Path,
prd_dir: Path,
test_case_file: Path) -> Dict[str, str]:
"""
验证文档是否存在
Args:
requirements_dir: 需求文档目录
prd_dir: PRD文档目录
test_case_file: 测试用例文件
Returns:
验证结果字典
"""
result = {
"requirements_dir": str(requirements_dir),
"prd_dir": str(prd_dir),
"test_case_file": str(test_case_file),
"valid": True,
"errors": []
}
# 检查需求文档目录
if not requirements_dir.exists():
result["valid"] = False
result["errors"].append(f"需求文档目录不存在: {requirements_dir}")
# 检查PRD文档目录
if not prd_dir.exists():
result["valid"] = False
result["errors"].append(f"PRD文档目录不存在: {prd_dir}")
# 检查测试用例文件
if not test_case_file.exists():
result["valid"] = False
result["errors"].append(f"测试用例文件不存在: {test_case_file}")
# 检查需求文档
if requirements_dir.exists():
req_files = list(requirements_dir.glob("*.docx"))
if not req_files:
result["errors"].append(f"需求文档目录下没有.docx文件: {requirements_dir}")
# 检查PRD文档
if prd_dir.exists():
prd_files = list(prd_dir.glob("*.md"))
if not prd_files:
result["errors"].append(f"PRD文档目录下没有.md文件: {prd_dir}")
return result
# -*- coding: utf-8 -*-
"""
缺失检测模块
本模块负责使用AI检测测试用例的缺失情况
"""
import json
from typing import List, Dict, Optional
from logging import getLogger
from src.config import AI_MODEL, COVERAGE_TARGET
logger = getLogger(__name__)
class GapDetector:
"""
缺失检测器
主要功能:
1. 检测功能点缺失
2. 检测场景覆盖不足
3. 检测边界条件不足
支持两种AI服务模式:
- Anthropic API:使用官方API密钥
- Claude CLI:使用本地CLI工具
"""
def __init__(self, api_key: Optional[str] = None, use_cli: bool = False):
"""
初始化检测器
Args:
api_key: Anthropic API密钥(可选,兼容性保留)
use_cli: 是否使用CLI模式
"""
from src.ai_service_manager import AIServiceManager
# 使用AI服务管理器
self.ai_service = AIServiceManager(api_key=api_key, use_cli=use_cli)
logger.info("缺失检测器初始化完成")
def detect_gaps(self, function_points: List[Dict],
test_cases: List[Dict],
requirements: List[Dict],
prds: List[Dict]) -> Dict:
"""
检测测试用例缺失情况
Args:
function_points: 功能点列表
test_cases: 测试用例列表
requirements: 需求文档列表
prds: PRD文档列表
Returns:
缺失检测结果,包含:
- 功能点缺失: 缺失的功能点列表
- 场景覆盖不足: 场景覆盖不足的列表
- 边界条件不足: 边界条件不足的列表
"""
logger.info("开始检测测试用例缺失...")
logger.info(" 功能点数: %d", len(function_points))
logger.info(" 测试用例数: %d", len(test_cases))
# 使用AI分析缺失情况
gaps = self._ai_detect_gaps(function_points, test_cases, requirements, prds)
return gaps
return gaps
def _ai_detect_gaps(self, function_points: List[Dict],
test_cases: List[Dict],
requirements: List[Dict],
prds: List[Dict]) -> Dict:
"""
使用AI检测缺失
Args:
function_points: 功能点列表
test_cases: 测试用例列表
requirements: 需求文档列表
prds: PRD文档列表
Returns:
缺失检测结果
"""
prompt = self._build_gap_prompt(function_points, test_cases, requirements, prds)
try:
# 使用AI服务管理器调用
result = self.ai_service.ask_json_array(prompt, AI_MODEL)
logger.debug("AI返回结果类型: %s", type(result))
if isinstance(result, dict):
logger.debug("AI返回的键: %s", list(result.keys()))
for key, value in result.items():
if isinstance(value, list):
logger.debug(" %s: %d 项", key, len(value))
# 处理返回结果
gaps = {"功能点缺失": [], "场景覆盖不足": [], "边界条件不足": []}
if isinstance(result, dict):
# 直接是字典格式,合并结果
for key in gaps:
if key in result and isinstance(result[key], list):
gaps[key] = result[key]
elif isinstance(result, list):
if len(result) > 0:
if isinstance(result[0], dict):
# 列表中包含字典,可能是单个gap对象
first_item = result[0]
for key in gaps:
if key in first_item and isinstance(first_item[key], list):
gaps[key] = first_item[key]
else:
# 列表直接是功能点缺失
gaps["功能点缺失"] = result
logger.info("AI检测完成:")
logger.info(" 功能点缺失: %d 项", len(gaps["功能点缺失"]))
logger.info(" 场景覆盖不足: %d 项", len(gaps["场景覆盖不足"]))
logger.info(" 边界条件不足: %d 项", len(gaps["边界条件不足"]))
return gaps
except Exception as e:
logger.error("AI调用失败: %s", e)
logger.debug("错误详情", exc_info=True)
return {"功能点缺失": [], "场景覆盖不足": [], "边界条件不足": []}
def _build_gap_prompt(self, function_points: List[Dict],
test_cases: List[Dict],
requirements: List[Dict],
prds: List[Dict]) -> str:
"""
构建缺失检测提示词
Args:
function_points: 功能点列表
test_cases: 测试用例列表
requirements: 需求文档列表
prds: PRD文档列表
Returns:
提示词字符串
"""
prompt = """你是一个专业的测试用例分析专家。请分析以下功能点、需求文档、PRD文档和现有测试用例,识别测试用例的缺失情况。
【功能点列表】
"""
# 添加功能点
prompt += json.dumps(function_points, ensure_ascii=False, indent=2)
# 添加现有测试用例(限制数量)
prompt += f"\n\n【现有测试用例】(共{len(test_cases)}条,显示前30条)\n"
for i, case in enumerate(test_cases[:30], 1):
prompt += f"{i}. 用例名称: {case.get('用例名称', '')}\n"
prompt += f" 功能描述: {case.get('功能描述', '')}\n"
prompt += f" 用例等级: {case.get('用例等级', '')}\n\n"
# 添加需求文档摘要
prompt += "\n【需求文档摘要】\n"
for req in requirements:
prompt += f"- {req['filename']}: {req['content'][:500]}...\n"
# 添加PRD文档摘要
prompt += "\n【PRD文档摘要】\n"
for prd in prds:
prompt += f"- {prd['filename']}: {prd['content'][:500]}...\n"
prompt += """
\n请识别以下三类缺失,输出JSON格式:
{
"功能点缺失": [
{
"功能模块": "模块名",
"功能编号": "F-001",
"功能描述": "缺失的功能描述",
"建议用例名称": "测试XXX功能",
"优先级": "P1"
}
],
"场景覆盖不足": [
{
"功能描述": "XXX功能",
"已有场景": "正常场景",
"缺失场景": "异常场景:XXX",
"建议用例名称": "测试XXX异常场景",
"优先级": "P2"
}
],
"边界条件不足": [
{
"功能描述": "XXX功能",
"已有边界": "常规值",
"缺失边界": "边界值:XXX",
"建议用例名称": "测试XXX边界条件",
"优先级": "P2"
}
]
}
注意:
1. 必须输出合法的JSON格式
2. 只输出JSON对象,不要有其他说明文字
3. 优先级分为:P0(最高)、P1、P2、P3(最低)
4. 优先识别P0和P1级别的缺失
5. 考虑以下常见场景:
- 正常场景:基本功能验证
- 异常场景:错误处理、异常输入
- 边界条件:最大值、最小值、空值、null
- 并发场景:多用户同时操作
- 权限场景:不同角色权限验证
- 性能场景:大数据量、长时间操作
"""
return prompt
def _parse_gap_result(self, result: str) -> Dict:
"""
解析AI返回的缺失结果
Args:
result: AI返回的文本
Returns:
缺失结果字典
"""
import re
logger.debug("尝试解析缺失结果JSON,内容长度: %d", len(result))
# 尝试直接解析
try:
return json.loads(result)
except json.JSONDecodeError:
pass
# 尝试从代码块中提取
code_block_pattern = r'```(?:json)?\s*\n?([\s\S]*?)\n?```'
matches = re.findall(code_block_pattern, result)
for match in matches:
try:
gaps = json.loads(match.strip())
logger.debug("成功解析缺失检测结果")
return gaps
except:
continue
# 尝试提取花括号内容(对象)- 使用括号匹配处理嵌套
brace_count = 0
start_idx = -1
for i, char in enumerate(result):
if char == '{':
if brace_count == 0:
start_idx = i
brace_count += 1
elif char == '}':
brace_count -= 1
if brace_count == 0 and start_idx >= 0:
try:
json_str = result[start_idx:i+1]
gaps = json.loads(json_str)
logger.debug("成功解析缺失检测结果")
return gaps
except:
break
logger.warning("未找到有效的JSON对象格式")
logger.debug("AI返回内容: %s", result[:500])
return {"功能点缺失": [], "场景覆盖不足": [], "边界条件不足": []}
def check_coverage_target(self, coverage_rate: float) -> Dict:
"""
检查是否达到覆盖率目标
Args:
coverage_rate: 当前覆盖率
Returns:
检查结果
"""
target_met = coverage_rate >= COVERAGE_TARGET
gap = COVERAGE_TARGET - coverage_rate
result = {
"current_coverage": coverage_rate,
"target_coverage": COVERAGE_TARGET,
"target_met": target_met,
"gap": max(0, gap),
"gap_percent": max(0, gap) * 100
}
if target_met:
logger.info("✅ 覆盖率达标: %.1f%% >= %.1f%%",
coverage_rate * 100, COVERAGE_TARGET * 100)
else:
logger.warning("⚠️ 覆盖率未达标: %.1f%% < %.1f%%, 差距: %.1f%%",
coverage_rate * 100, COVERAGE_TARGET * 100, result["gap_percent"])
return result
def prioritize_gaps(self, gaps: Dict) -> Dict:
"""
对缺失项进行优先级排序
Args:
gaps: 缺失检测结果
Returns:
按优先级排序的缺失项
"""
priority_order = {"P0": 0, "P1": 1, "P2": 2, "P3": 3}
def sort_by_priority(item):
priority = item.get("优先级", "P3")
return priority_order.get(priority, 4)
result = {
"功能点缺失": sorted(gaps.get("功能点缺失", []), key=sort_by_priority),
"场景覆盖不足": sorted(gaps.get("场景覆盖不足", []), key=sort_by_priority),
"边界条件不足": sorted(gaps.get("边界条件不足", []), key=sort_by_priority)
}
# 统计各优先级数量
for category in ["功能点缺失", "场景覆盖不足", "边界条件不足"]:
items = result[category]
logger.info("%s - P0: %d, P1: %d, P2: %d, P3: %d",
category,
sum(1 for i in items if i.get("优先级") == "P0"),
sum(1 for i in items if i.get("优先级") == "P1"),
sum(1 for i in items if i.get("优先级") == "P2"),
sum(1 for i in items if i.get("优先级") == "P3"))
return result
def estimate_additional_cases(gaps: Dict) -> Dict:
"""
估算需要新增的测试用例数量
Args:
gaps: 缺失检测结果
Returns:
估算结果
"""
function_gaps = len(gaps.get("功能点缺失", []))
scenario_gaps = len(gaps.get("场景覆盖不足", []))
boundary_gaps = len(gaps.get("边界条件不足", []))
total = function_gaps + scenario_gaps + boundary_gaps
result = {
"功能点缺失": function_gaps,
"场景覆盖不足": scenario_gaps,
"边界条件不足": boundary_gaps,
"总计": total
}
logger.info("估算需新增测试用例: %d 条", total)
return result
# -*- coding: utf-8 -*-
"""
AI完善测试用例工具 - 主入口模块
本模块是工具的主入口,负责协调整个处理流程
"""
import os
import sys
import logging
from pathlib import Path
# 先加载环境变量(优先使用.env文件)
try:
from dotenv import load_dotenv
# 使用 __file__ 获取当前文件所在目录的父目录(项目根目录)
CURRENT_DIR = Path(__file__).parent
env_file = CURRENT_DIR.parent / ".env"
if env_file.exists():
load_dotenv(env_file)
except ImportError:
pass # python-dotenv 未安装,跳过
from src.config import (
PROJECT_ROOT,
REQUIREMENTS_DIR,
PRD_DIR,
TEST_CASE_FILE,
OUTPUT_TEST_CASE,
OUTPUT_REPORT,
LOG_FILE,
LOG_FORMAT,
LOG_DATE_FORMAT,
API_KEY_ENV,
API_KEY_PREFIX,
ensure_directories,
COVERAGE_TARGET,
get_ai_service_mode
)
from src.document_reader import DocumentReader, validate_documents
from src.test_case_analyzer import TestCaseAnalyzer, analyze_test_case_quality
from src.gap_detector import GapDetector, estimate_additional_cases
from src.case_generator import CaseGenerator
from src.deduplicator import Deduplicator
from src.report_generator import ReportGenerator
# 配置日志
def setup_logging(log_level: str = "INFO") -> None:
"""
配置日志系统
Args:
log_level: 日志级别
"""
ensure_directories()
level = getattr(logging, log_level.upper(), logging.INFO)
logging.basicConfig(
level=level,
format=LOG_FORMAT,
datefmt=LOG_DATE_FORMAT,
handlers=[
logging.StreamHandler(sys.stdout),
logging.FileHandler(LOG_FILE, encoding="utf-8")
]
)
global logger
logger = logging.getLogger(__name__)
logger.info("日志系统初始化完成,级别=%s", log_level)
logger = logging.getLogger(__name__)
def check_api_key() -> str:
"""
检查API密钥
Returns:
API密钥字符串
Raises:
SystemExit: 如果API密钥不存在
"""
api_key = os.environ.get(API_KEY_ENV)
if not api_key:
logger.error("未设置 %s 环境变量", API_KEY_ENV)
logger.error("请设置API密钥:")
logger.error(" Windows: set %s=your-key-here", API_KEY_ENV)
logger.error(" Linux/Mac: export %s=your-key-here", API_KEY_ENV)
sys.exit(1)
# 隐藏API密钥的显示
masked_key = api_key[:8] + "..." if len(api_key) > 8 else "***"
logger.info("API密钥已配置: %s", masked_key)
return api_key
def validate_inputs() -> None:
"""
验证输入文件和目录
Raises:
SystemExit: 如果验证失败
"""
logger.info("验证输入文件和目录...")
result = validate_documents(REQUIREMENTS_DIR, PRD_DIR, TEST_CASE_FILE)
if not result["valid"]:
logger.error("输入验证失败:")
for error in result["errors"]:
logger.error(" - %s", error)
sys.exit(1)
# 显示警告
if result["errors"]:
logger.warning("发现以下问题:")
for error in result["errors"]:
logger.warning(" - %s", error)
logger.info("输入验证通过")
def main(use_cli: bool = False) -> int:
"""
主函数
Args:
use_cli: 是否强制使用CLI模式
Returns:
退出码(0表示成功,非0表示失败)
"""
try:
# 配置日志
setup_logging()
ensure_directories()
logger.info("=" * 60)
logger.info("AI完善测试用例工具 启动")
logger.info("版本: 1.0.0")
logger.info("=" * 60)
# 检查AI服务模式
api_key = None
service_mode = get_ai_service_mode()
# 命令行参数优先级最高
if use_cli:
service_mode = "cli"
# 根据服务模式初始化
if service_mode == "api":
# 强制使用API模式
api_key = os.environ.get(API_KEY_ENV)
if not api_key or not api_key.strip():
logger.error("API模式需要ANTHROPIC_API_KEY环境变量")
logger.error("请设置API密钥:")
logger.error(" Windows: set ANTHROPIC_API_KEY=your-key-here")
logger.error(" Linux/Mac: export ANTHROPIC_API_KEY=your-key-here")
logger.error("或在 .env 文件中配置: ANTHROPIC_API_KEY=your-key-here")
return 1
use_cli = False
masked_key = api_key[:8] + "..." if len(api_key) > 8 else "***"
logger.info("使用Anthropic API模式(强制),密钥: %s", masked_key)
elif service_mode == "cli":
# 强制使用CLI模式
use_cli = True
logger.info("使用Claude CLI模式(强制)")
else:
# auto模式:自动检测
api_key = os.environ.get(API_KEY_ENV)
if api_key and api_key.strip():
logger.info("检测到API密钥,将使用Anthropic API模式")
use_cli = False
else:
logger.info("未检测到有效的API密钥,将尝试使用Claude CLI模式")
logger.info("提示:可以通过设置环境变量 AI_SERVICE_MODE=api 强制使用API模式")
use_cli = True
if use_cli:
logger.info("使用Claude CLI模式")
else:
masked_key = api_key[:8] + "..." if len(api_key) > 8 else "***"
logger.info("使用Anthropic API模式,密钥: %s", masked_key)
# 验证输入
validate_inputs()
# 1. 读取文档
logger.info("")
logger.info("【步骤1】读取文档")
logger.info("-" * 60)
reader = DocumentReader()
docs = reader.read_all_documents(REQUIREMENTS_DIR, PRD_DIR, TEST_CASE_FILE)
requirements = docs["requirements"]
prds = docs["prds"]
test_cases = docs["test_cases"]
# 2. 分析测试用例质量
logger.info("")
logger.info("【步骤2】分析测试用例质量")
logger.info("-" * 60)
quality_report = analyze_test_case_quality(test_cases)
# 3. 提取功能点
logger.info("")
logger.info("【步骤3】提取功能点")
logger.info("-" * 60)
analyzer = TestCaseAnalyzer(api_key=api_key, use_cli=use_cli)
function_points = analyzer.extract_function_points(requirements, prds)
if not function_points:
logger.warning("未提取到功能点,请检查需求文档内容")
# 继续执行,但使用空列表
# 4. 分析覆盖率
logger.info("")
logger.info("【步骤4】分析覆盖率")
logger.info("-" * 60)
coverage = analyzer.analyze_coverage(function_points, test_cases)
# 5. 检测缺失
logger.info("")
logger.info("【步骤5】检测缺失")
logger.info("-" * 60)
detector = GapDetector(api_key=api_key, use_cli=use_cli)
gaps = detector.detect_gaps(function_points, test_cases, requirements, prds)
# 检查覆盖率目标
coverage_check = detector.check_coverage_target(coverage.get("coverage_rate", 0))
# 估算新增用例数量
estimate = estimate_additional_cases(gaps)
# 6. 生成新用例
logger.info("")
logger.info("【步骤6】生成新测试用例")
logger.info("-" * 60)
generator = CaseGenerator(api_key=api_key, use_cli=use_cli)
new_cases = generator.generate_cases(gaps, requirements, prds, test_cases)
# 7. 去重
logger.info("")
logger.info("【步骤7】去重处理")
logger.info("-" * 60)
deduplicator = Deduplicator()
dedup_result = deduplicator.deduplicate(test_cases, new_cases)
# 8. 保存完善后的测试用例
logger.info("")
logger.info("【步骤8】保存完善后的测试用例")
logger.info("-" * 60)
save_enhanced_test_cases(test_cases, dedup_result["unique_cases"], OUTPUT_TEST_CASE)
# 9. 生成差异性报告
logger.info("")
logger.info("【步骤9】生成差异性报告")
logger.info("-" * 60)
reporter = ReportGenerator()
reporter.generate_report(gaps, dedup_result, coverage, OUTPUT_REPORT)
# 10. 输出总结
logger.info("")
logger.info("=" * 60)
logger.info("处理完成!")
logger.info("=" * 60)
logger.info("【输入】")
logger.info(" 需求文档: %d 份", len(requirements))
logger.info(" PRD文档: %d 份", len(prds))
logger.info(" 原始用例: %d 条", len(test_cases))
logger.info("")
logger.info("【输出】")
logger.info(" 完善后的测试用例: %s", OUTPUT_TEST_CASE)
logger.info(" 差异性报告: %s", OUTPUT_REPORT)
logger.info("")
logger.info("【统计】")
logger.info(" 覆盖率: %.1f%% -> %.1f%% (预计)",
coverage.get("coverage_rate", 0) * 100,
min(100, coverage.get("coverage_rate", 0) * 100 + 20))
logger.info(" 新增用例: %d 条(去重后)", dedup_result["summary"]["unique_cases"])
logger.info(" 功能点缺失: %d 项", len(gaps.get("功能点缺失", [])))
logger.info(" 场景缺失: %d 项", len(gaps.get("场景覆盖不足", [])))
logger.info(" 边界缺失: %d 项", len(gaps.get("边界条件不足", [])))
logger.info("=" * 60)
return 0
except KeyboardInterrupt:
logger.info("")
logger.info("用户中断操作")
return 130
except FileNotFoundError as e:
logger.error("文件未找到: %s", e)
return 1
except PermissionError as e:
logger.error("权限错误: %s", e)
logger.error("请检查文件是否被其他程序占用")
return 1
except Exception as e:
logger.error("处理失败: %s", e)
import traceback
logger.debug(traceback.format_exc())
return 1
def save_enhanced_test_cases(existing_cases: list, new_cases: list,
output_path: Path) -> None:
"""
保存完善后的测试用例
Args:
existing_cases: 现有测试用例列表
new_cases: 新增测试用例列表
output_path: 输出文件路径
"""
import openpyxl
from openpyxl.styles import Font, PatternFill
from src.config import TEST_CASE_COLUMNS
logger.info("正在保存完善后的测试用例...")
# 加载原始文件
wb = openpyxl.load_workbook(TEST_CASE_FILE)
ws = wb.active
# 获取下一个序列号
max_seq = 0
for row in ws.iter_rows(min_row=2, max_row=ws.max_row, values_only=True):
seq = row[0] # 假设序列号是第一列
if isinstance(seq, int):
max_seq = max(max_seq, seq)
# 定义新用例的样式(浅蓝色背景)
new_case_fill = PatternFill(start_color="E6F2FF", end_color="E6F2FF", fill_type="solid")
new_case_font = Font(bold=True)
# 追加新用例
for i, case in enumerate(new_cases):
max_seq += 1
row_data = [case.get(col, "") for col in TEST_CASE_COLUMNS]
row_data[0] = max_seq # 更新序列号
ws.append(row_data)
# 为新添加的行设置样式
new_row = ws.max_row
for cell in ws[new_row]:
cell.fill = new_case_fill
cell.font = new_case_font
# 保存文件
output_path.parent.mkdir(parents=True, exist_ok=True)
wb.save(str(output_path))
logger.info("完善后的测试用例已保存: %s", output_path)
logger.info(" 原有用例: %d 条", len(existing_cases))
logger.info(" 新增用例: %d 条", len(new_cases))
logger.info(" 总计: %d 条", len(existing_cases) + len(new_cases))
if __name__ == "__main__":
sys.exit(main())
# -*- coding: utf-8 -*-
"""
报告生成模块
本模块负责生成差异性报告
"""
from pathlib import Path
from typing import List, Dict
from logging import getLogger
from datetime import datetime
try:
from docx import Document
from docx.shared import Pt, RGBColor, Inches
from docx.enum.text import WD_ALIGN_PARAGRAPH
except ImportError:
print("警告: 未安装python-docx库,请运行: pip install python-docx")
raise
logger = getLogger(__name__)
class ReportGenerator:
"""
差异性报告生成器
主要功能:
1. 生成Word格式的差异性报告
2. 包含功能点缺失、场景覆盖不足、边界条件不足
3. 包含去重处理结果和统计信息
"""
def __init__(self):
"""初始化报告生成器"""
logger.info("报告生成器初始化完成")
def generate_report(self, gaps: Dict, dedup_result: Dict,
coverage: Dict, output_path: Path) -> None:
"""
生成差异性报告
Args:
gaps: 缺失检测结果
dedup_result: 去重结果
coverage: 覆盖率分析结果
output_path: 输出文件路径
"""
logger.info("开始生成差异性报告: %s", output_path)
doc = Document()
# 标题
self._add_title(doc)
# 生成时间
self._add_generation_time(doc)
# 1. 功能点缺失
self._add_missing_functions_section(doc, gaps.get("功能点缺失", []))
# 2. 场景覆盖不足
self._add_scenarios_section(doc, gaps.get("场景覆盖不足", []))
# 3. 边界条件不足
self._add_boundaries_section(doc, gaps.get("边界条件不足", []))
# 4. 去重处理结果
self._add_deduplication_section(doc, dedup_result)
# 5. 用例数量增长统计
self._add_statistics_section(doc, coverage, dedup_result)
# 6. 覆盖率百分比统计
self._add_coverage_section(doc, coverage)
# 7. 总结和建议
self._add_summary_section(doc, coverage, dedup_result)
# 确保输出目录存在
output_path.parent.mkdir(parents=True, exist_ok=True)
# 保存文档
doc.save(str(output_path))
logger.info("报告生成完成: %s", output_path)
def _add_title(self, doc: Document) -> None:
"""添加报告标题"""
title = doc.add_heading("测试用例完善差异性报告", 0)
title.alignment = WD_ALIGN_PARAGRAPH.CENTER
def _add_generation_time(self, doc: Document) -> None:
"""添加生成时间"""
p = doc.add_paragraph()
p.alignment = WD_ALIGN_PARAGRAPH.RIGHT
run = p.add_run(f"生成时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
run.font.size = Pt(10)
run.font.color.rgb = RGBColor(128, 128, 128)
doc.add_paragraph() # 空行
def _add_missing_functions_section(self, doc: Document, gaps: List[Dict]) -> None:
"""添加功能点缺失部分"""
doc.add_heading("一、功能点缺失的测试用例", level=1)
if not gaps:
doc.add_paragraph("✅ 无功能点缺失").italic = True
else:
# 统计各优先级数量
p0_count = sum(1 for g in gaps if g.get("优先级") == "P0")
p1_count = sum(1 for g in gaps if g.get("优先级") == "P1")
p2_count = sum(1 for g in gaps if g.get("优先级") == "P2")
p3_count = sum(1 for g in gaps if g.get("优先级") == "P3")
p = doc.add_paragraph()
p.add_run(f"共 {len(gaps)} 项缺失:")
p.add_run(f" P0: {p0_count} 项").font.color.rgb = RGBColor(220, 20, 60)
p.add_run(f",P1: {p1_count} 项").font.color.rgb = RGBColor(255, 140, 0)
p.add_run(f",P2: {p2_count} 项,P3: {p3_count} 项")
doc.add_paragraph()
# 表格
table = doc.add_table(rows=1, cols=4)
table.style = 'Light Grid Accent 1'
# 表头
headers = table.rows[0].cells
headers[0].text = "序号"
headers[1].text = "功能模块"
headers[2].text = "功能描述"
headers[3].text = "优先级"
# 设置表头样式
for cell in headers:
for paragraph in cell.paragraphs:
paragraph.runs[0].font.bold = True
# 数据行
for i, gap in enumerate(gaps, 1):
row = table.add_row().cells
row[0].text = str(i)
row[1].text = gap.get("功能模块", "")
row[2].text = gap.get("功能描述", "")
row[3].text = gap.get("优先级", "P2")
# 根据优先级设置颜色
priority = gap.get("优先级", "P2")
if priority == "P0":
row[3].paragraphs[0].runs[0].font.color.rgb = RGBColor(220, 20, 60)
elif priority == "P1":
row[3].paragraphs[0].runs[0].font.color.rgb = RGBColor(255, 140, 0)
def _add_scenarios_section(self, doc: Document, gaps: List[Dict]) -> None:
"""添加场景覆盖不足部分"""
doc.add_heading("二、场景覆盖不足的测试用例", level=1)
if not gaps:
doc.add_paragraph("✅ 无场景覆盖不足").italic = True
else:
doc.add_paragraph(f"共 {len(gaps)} 项场景覆盖不足:")
doc.add_paragraph()
# 表格
table = doc.add_table(rows=1, cols=4)
table.style = 'Light Grid Accent 1'
# 表头
headers = table.rows[0].cells
headers[0].text = "序号"
headers[1].text = "功能描述"
headers[2].text = "缺失场景"
headers[3].text = "优先级"
for cell in headers:
for paragraph in cell.paragraphs:
paragraph.runs[0].font.bold = True
# 数据行
for i, gap in enumerate(gaps, 1):
row = table.add_row().cells
row[0].text = str(i)
row[1].text = gap.get("功能描述", "")
row[2].text = gap.get("缺失场景", "")
row[3].text = gap.get("优先级", "P2")
def _add_boundaries_section(self, doc: Document, gaps: List[Dict]) -> None:
"""添加边界条件不足部分"""
doc.add_heading("三、边界条件不足的测试用例", level=1)
if not gaps:
doc.add_paragraph("✅ 无边界条件不足").italic = True
else:
doc.add_paragraph(f"共 {len(gaps)} 项边界条件不足:")
doc.add_paragraph()
# 表格
table = doc.add_table(rows=1, cols=4)
table.style = 'Light Grid Accent 1'
# 表头
headers = table.rows[0].cells
headers[0].text = "序号"
headers[1].text = "功能描述"
headers[2].text = "缺失边界"
headers[3].text = "优先级"
for cell in headers:
for paragraph in cell.paragraphs:
paragraph.runs[0].font.bold = True
# 数据行
for i, gap in enumerate(gaps, 1):
row = table.add_row().cells
row[0].text = str(i)
row[1].text = gap.get("功能描述", "")
row[2].text = gap.get("缺失边界", "")
row[3].text = gap.get("优先级", "P2")
def _add_deduplication_section(self, doc: Document, dedup_result: Dict) -> None:
"""添加去重处理结果部分"""
doc.add_heading("四、去重处理结果", level=1)
summary = dedup_result.get("summary", {})
doc.add_paragraph(f"新增测试用例:{summary.get('new_cases', 0)} 条")
doc.add_paragraph(f"去重后保留:{summary.get('unique_cases', 0)} 条")
doc.add_paragraph(f"与现有用例重复:{summary.get('duplicate_with_existing', 0)} 条")
doc.add_paragraph(f"新用例内部重复:{summary.get('duplicate_within_new', 0)} 条")
# 去重率
dedup_rate = summary.get("deduplication_rate", 0) * 100
p = doc.add_paragraph()
p.add_run(f"去重率:{dedup_rate:.1f}%")
p.runs[0].font.bold = True
p.runs[0].font.size = Pt(12)
def _add_statistics_section(self, doc: Document, coverage: Dict,
dedup_result: Dict) -> None:
"""添加用例数量增长统计部分"""
doc.add_heading("五、用例数量增长统计", level=1)
# 表格
table = doc.add_table(rows=4, cols=2)
table.style = 'Light Grid Accent 1'
# 表头
headers = table.rows[0].cells
headers[0].text = "统计项"
headers[1].text = "数量"
for cell in headers:
for paragraph in cell.paragraphs:
paragraph.runs[0].font.bold = True
# 数据行
table.rows[1].cells[0].text = "原有测试用例数"
table.rows[1].cells[1].text = str(coverage.get("covered_functions", 0))
table.rows[2].cells[0].text = "新增测试用例数(去重后)"
table.rows[2].cells[1].text = str(dedup_result.get("summary", {}).get("unique_cases", 0))
total = coverage.get("covered_functions", 0) + dedup_result.get("summary", {}).get("unique_cases", 0)
table.rows[3].cells[0].text = "完善后测试用例总数"
table.rows[3].cells[1].text = str(total)
# 高亮最后一行
for cell in table.rows[3].cells:
for paragraph in cell.paragraphs:
paragraph.runs[0].font.bold = True
paragraph.runs[0].font.color.rgb = RGBColor(0, 100, 0)
def _add_coverage_section(self, doc: Document, coverage: Dict) -> None:
"""添加覆盖率百分比统计部分"""
doc.add_heading("六、覆盖率百分比统计", level=1)
coverage_rate = coverage.get("coverage_rate", 0) * 100
target_coverage = 90
# 当前覆盖率
p = doc.add_paragraph()
p.add_run("当前覆盖率:").bold = True
p.add_run(f"{coverage_rate:.1f}%")
# 目标覆盖率
p = doc.add_paragraph()
p.add_run("目标覆盖率:").bold = True
p.add_run(f"{target_coverage}%")
doc.add_paragraph()
# 状态判断
if coverage_rate >= target_coverage:
p = doc.add_paragraph()
run = p.add_run("✅ 覆盖率达标!")
run.font.bold = True
run.font.size = Pt(14)
run.font.color.rgb = RGBColor(0, 128, 0)
else:
gap = target_coverage - coverage_rate
p = doc.add_paragraph()
run = p.add_run(f"⚠️ 距离目标还差 {gap:.1f}%")
run.font.bold = True
run.font.size = Pt(14)
run.font.color.rgb = RGBColor(255, 140, 0)
def _add_summary_section(self, doc: Document, coverage: Dict,
dedup_result: Dict) -> None:
"""添加总结和建议部分"""
doc.add_heading("七、总结和建议", level=1)
# 总体情况
coverage_rate = coverage.get("coverage_rate", 0) * 100
new_cases = dedup_result.get("summary", {}).get("unique_cases", 0)
doc.add_paragraph("【总体情况】")
doc.add_paragraph(f"• 当前测试用例覆盖率:{coverage_rate:.1f}%")
doc.add_paragraph(f"• 计划新增测试用例:{new_cases} 条")
doc.add_paragraph()
# 建议
doc.add_paragraph("【建议】")
if coverage_rate < 90:
doc.add_paragraph(f"• 当前覆盖率未达到目标(90%),建议优先补充P0和P1级别的测试用例")
if new_cases > 50:
doc.add_paragraph(f"• 新增测试用例较多({new_cases}条),建议分批执行和验证")
doc.add_paragraph("• 建议对AI生成的测试用例进行人工审核,确保用例质量")
doc.add_paragraph("• 建议在执行新增用例后,再次评估覆盖率")
doc.add_paragraph()
# 注意事项
doc.add_paragraph("【注意事项】")
doc.add_paragraph("• 本报告由AI自动生成,仅供参考")
doc.add_paragraph("• 具体的测试用例内容请在完善版本的Excel文件中查看")
def generate_simple_report(gaps: Dict, dedup_result: Dict,
coverage: Dict, output_path: Path) -> None:
"""
生成简化的报告(仅统计信息)
Args:
gaps: 缺失检测结果
dedup_result: 去重结果
coverage: 覆盖率分析结果
output_path: 输出文件路径
"""
doc = Document()
doc.add_heading("测试用例完善统计报告", 0)
# 覆盖率
coverage_rate = coverage.get("coverage_rate", 0) * 100
doc.add_paragraph(f"当前覆盖率:{coverage_rate:.1f}%")
# 缺失统计
doc.add_paragraph(f"功能点缺失:{len(gaps.get('功能点缺失', []))} 项")
doc.add_paragraph(f"场景覆盖不足:{len(gaps.get('场景覆盖不足', []))} 项")
doc.add_paragraph(f"边界条件不足:{len(gaps.get('边界条件不足', []))} 项")
# 新增用例
new_cases = dedup_result.get("summary", {}).get("unique_cases", 0)
doc.add_paragraph(f"新增测试用例(去重后):{new_cases} 条")
output_path.parent.mkdir(parents=True, exist_ok=True)
doc.save(str(output_path))
# -*- coding: utf-8 -*-
"""
测试用例分析模块
本模块负责使用AI分析需求文档和PRD文档,提取功能点列表
"""
from typing import List, Dict, Optional, Any
from logging import getLogger
import json
import re
import os
try:
import anthropic
except ImportError:
anthropic = None
from src.config import (
AI_MODEL, AI_MAX_TOKENS, AI_TEMPERATURE, API_KEY_ENV, API_KEY_PREFIX,
get_api_base_url
)
logger = getLogger(__name__)
class AIClientInterface:
"""AI客户端接口(抽象类)"""
def ask_json(self, prompt: str, model: Optional[str] = None) -> Any:
"""发送提示词并返回JSON结果"""
raise NotImplementedError
class AnthropicAPIClient(AIClientInterface):
"""Anthropic API客户端"""
def __init__(self, api_key: str, base_url: Optional[str] = None):
if anthropic is None:
raise ImportError("未安装anthropic库,请运行: pip install anthropic")
kwargs = {"api_key": api_key}
if base_url:
kwargs["base_url"] = base_url
logger.info("使用自定义API endpoint: %s", base_url)
self.client = anthropic.Anthropic(**kwargs)
def ask_json(self, prompt: str, model: Optional[str] = None) -> Any:
"""调用Anthropic API"""
model = model or AI_MODEL
response = self.client.messages.create(
model=model,
max_tokens=AI_MAX_TOKENS,
temperature=AI_TEMPERATURE,
messages=[{"role": "user", "content": prompt}]
)
return response.content[0].text
class ClaudeCLIClient(AIClientInterface):
"""Claude CLI客户端"""
def __init__(self):
from src.claude_cli_adapter import ClaudeCLIAdapter
self.adapter = ClaudeCLIAdapter()
if not self.adapter.check_available():
raise RuntimeError("Claude CLI不可用")
def ask_json(self, prompt: str, model: Optional[str] = None) -> Any:
"""调用Claude CLI"""
# CLI适配器会自动处理JSON解析
return self.adapter.ask_json(prompt, model)
class TestCaseAnalyzer:
"""
测试用例分析器
主要功能:
1. 使用AI提取功能点列表
2. 分析测试用例覆盖情况
支持两种AI服务模式:
- Anthropic API:使用官方API密钥
- Claude CLI:使用本地CLI工具
"""
def __init__(self, api_key: Optional[str] = None, use_cli: bool = False):
"""
初始化分析器
Args:
api_key: Anthropic API密钥(可选)
use_cli: 是否强制使用CLI模式(可选)
"""
# 获取API base URL
base_url = get_api_base_url()
# 决定使用哪种模式
if use_cli:
logger.info("强制使用Claude CLI模式")
self.ai_client = ClaudeCLIClient()
self.mode = "cli"
elif api_key and api_key.strip():
logger.info("使用Anthropic API模式")
self.ai_client = AnthropicAPIClient(api_key, base_url)
self.mode = "api"
else:
# 尝试自动检测
api_key_from_env = os.environ.get(API_KEY_ENV, "")
if api_key_from_env and api_key_from_env.strip():
logger.info("检测到API密钥,使用Anthropic API模式")
self.ai_client = AnthropicAPIClient(api_key_from_env, base_url)
self.mode = "api"
else:
logger.info("未检测到API密钥,尝试使用Claude CLI模式")
try:
self.ai_client = ClaudeCLIClient()
self.mode = "cli"
except RuntimeError:
raise RuntimeError(
"无法初始化AI服务:\n"
"1. 未设置Anthropic API密钥\n"
"2. Claude CLI工具不可用\n"
"请至少配置其中一种方式"
)
logger.info("测试用例分析器初始化完成,模式: %s", self.mode)
def extract_function_points(self, requirements: List[Dict],
prds: List[Dict]) -> List[Dict]:
"""
使用AI提取功能点列表
Args:
requirements: 需求文档列表
prds: PRD文档列表
Returns:
功能点列表,每项包含:
- 功能模块: 模块名称
- 功能编号: 功能编号
- 功能描述: 功能描述
- 子功能: 子功能列表(可选)
"""
logger.info("开始提取功能点...")
# 检查是否有需求文档
has_requirements = len(requirements) > 0
if has_requirements:
logger.info("使用需求文档和PRD文档提取功能点")
prompt = self._build_extract_prompt(requirements, prds)
else:
logger.warning("没有需求文档,仅使用PRD文档提取功能点")
prompt = self._build_prd_only_prompt(prds)
try:
# 使用AI客户端发送请求
if self.mode == "api":
# API模式:需要解析返回的文本
result_text = self.ai_client.ask_json(prompt, AI_MODEL)
function_points = self._parse_function_points(result_text)
else:
# CLI模式:adapter直接返回解析后的JSON
function_points = self.ai_client.ask_json(prompt, AI_MODEL)
if isinstance(function_points, dict):
function_points = [function_points]
if not isinstance(function_points, list):
function_points = []
logger.info("功能点提取完成,共 %d 个", len(function_points))
return function_points
except Exception as e:
logger.error("提取功能点失败: %s", e)
logger.debug("错误详情", exc_info=True)
return []
def _build_prd_only_prompt(self, prds: List[Dict]) -> str:
"""
构建仅从PRD提取功能点的提示词
Args:
prds: PRD文档列表
Returns:
提示词字符串
"""
prompt = """你是一个专业的测试用例分析专家。请分析以下开发PRD文档,提取完整的功能点列表。
【要求】
1. 按功能模块分类
2. 每个功能点包含:功能模块、功能编号、功能描述
3. 识别所有子功能
4. 输出格式必须是JSON数组
【开发PRD文档】
"""
# 添加PRD文档内容(限制长度)
for prd in prds:
prompt += f"\n--- {prd['filename']} ---\n"
content = prd['content']
# 限制每个文档最多5000字符
if len(content) > 5000:
content = content[:5000] + "\n...(内容已截断)"
prompt += content + "\n"
prompt += """
\n请输出JSON格式的功能点列表,格式如下:
[
{
"功能模块": "模块名称",
"功能编号": "F-001",
"功能描述": "功能描述",
"子功能": ["子功能1", "子功能2"]
}
]
注意:
1. 必须输出合法的JSON格式
2. 功能编号格式:模块简称-序号,如 AUTH-001, USER-002
3. 如果没有子功能,可以省略"子功能"字段或设为空数组
4. 只输出JSON数组,不要有其他说明文字
"""
return prompt
def _build_extract_prompt(self, requirements: List[Dict],
prds: List[Dict]) -> str:
"""
构建功能点提取提示词
Args:
requirements: 需求文档列表
prds: PRD文档列表
Returns:
提示词字符串
"""
prompt = """你是一个专业的测试用例分析专家。请分析以下需求文档和PRD文档,提取完整的功能点列表。
【要求】
1. 按功能模块分类
2. 每个功能点包含:功能模块、功能编号、功能描述
3. 识别所有子功能
4. 输出格式必须是JSON数组
【需求文档】
"""
# 添加需求文档内容(限制长度)
for req in requirements:
prompt += f"\n--- {req['filename']} ---\n"
content = req['content']
# 限制每个文档最多3000字符
if len(content) > 3000:
content = content[:3000] + "\n...(内容已截断)"
prompt += content + "\n"
prompt += "\n【开发PRD文档】\n"
# 添加PRD文档内容(限制长度)
for prd in prds:
prompt += f"\n--- {prd['filename']} ---\n"
content = prd['content']
# 限制每个文档最多3000字符
if len(content) > 3000:
content = content[:3000] + "\n...(内容已截断)"
prompt += content + "\n"
prompt += """
\n请输出JSON格式的功能点列表,格式如下:
[
{
"功能模块": "模块名称",
"功能编号": "F-001",
"功能描述": "功能描述",
"子功能": ["子功能1", "子功能2"]
},
{
"功能模块": "模块名称",
"功能编号": "F-002",
"功能描述": "功能描述",
"子功能": ["子功能1", "子功能2", "子功能3"]
}
]
注意:
1. 必须输出合法的JSON格式
2. 功能编号格式:模块简称-序号,如 AUTH-001, USER-002
3. 如果没有子功能,可以省略"子功能"字段或设为空数组
4. 只输出JSON数组,不要有其他说明文字
"""
return prompt
def _parse_function_points(self, result: str) -> List[Dict]:
"""
解析AI返回的功能点
Args:
result: AI返回的文本
Returns:
功能点列表
"""
import re
logger.debug("尝试解析功能点JSON,内容长度: %d", len(result))
# 尝试直接解析
try:
return json.loads(result)
except json.JSONDecodeError:
pass
# 尝试从代码块中提取
code_block_pattern = r'```(?:json)?\s*\n?([\s\S]*?)\n?```'
matches = re.findall(code_block_pattern, result)
for match in matches:
try:
return json.loads(match.strip())
except:
continue
# 尝试提取方括号内容(数组)- 使用括号匹配处理嵌套
bracket_count = 0
start_idx = -1
for i, char in enumerate(result):
if char == '[':
if bracket_count == 0:
start_idx = i
bracket_count += 1
elif char == ']':
bracket_count -= 1
if bracket_count == 0 and start_idx >= 0:
try:
json_str = result[start_idx:i+1]
function_points = json.loads(json_str)
logger.debug("成功提取并解析JSON,功能点数量: %d", len(function_points))
return function_points
except:
break
logger.warning("未找到有效的JSON数组格式")
logger.debug("AI返回内容: %s", result[:500])
return []
def analyze_coverage(self, function_points: List[Dict],
test_cases: List[Dict]) -> Dict:
"""
分析测试用例覆盖情况
Args:
function_points: 功能点列表
test_cases: 测试用例列表
Returns:
覆盖率分析结果,包含:
- total_functions: 总功能数
- covered_functions: 已覆盖功能数
- coverage_rate: 覆盖率
- uncovered_functions: 未覆盖功能数
- coverage_details: 覆盖详情
"""
logger.info("开始分析测试用例覆盖情况...")
# 构建已覆盖的功能集合
covered_descriptions = set()
for case in test_cases:
desc = case.get("功能描述", "")
if desc and str(desc).strip():
covered_descriptions.add(str(desc).strip())
# 统计覆盖情况
total_functions = len(function_points)
covered_count = 0
uncovered_list = []
for fp in function_points:
fp_desc = fp.get("功能描述", "")
# 简单匹配:检查功能描述是否在测试用例中
is_covered = False
for covered_desc in covered_descriptions:
if fp_desc.lower() in covered_desc.lower() or covered_desc.lower() in fp_desc.lower():
is_covered = True
break
if is_covered:
covered_count += 1
else:
uncovered_list.append(fp)
coverage_rate = covered_count / total_functions if total_functions > 0 else 0
result = {
"total_functions": total_functions,
"covered_functions": covered_count,
"coverage_rate": coverage_rate,
"uncovered_functions": total_functions - covered_count,
"coverage_details": {
"covered": [fp for fp in function_points if fp not in uncovered_list],
"uncovered": uncovered_list
}
}
logger.info("覆盖率分析完成:")
logger.info(" 总功能数: %d", result["total_functions"])
logger.info(" 已覆盖: %d", result["covered_functions"])
logger.info(" 未覆盖: %d", result["uncovered_functions"])
logger.info(" 覆盖率: %.1f%%", result["coverage_rate"] * 100)
return result
def analyze_by_module(self, test_cases: List[Dict]) -> Dict[str, Dict]:
"""
按功能模块分析测试用例
Args:
test_cases: 测试用例列表
Returns:
按模块分组的统计信息
"""
logger.info("开始按功能模块分析...")
module_stats = {}
for case in test_cases:
module = case.get("功能模块", "未分类")
if not module:
module = "未分类"
if module not in module_stats:
module_stats[module] = {
"count": 0,
"cases": []
}
module_stats[module]["count"] += 1
module_stats[module]["cases"].append({
"name": case.get("用例名称", ""),
"level": case.get("用例等级", "")
})
# 打印统计信息
logger.info("功能模块统计:")
for module, stats in sorted(module_stats.items()):
logger.info(" %s: %d 条用例", module, stats["count"])
return module_stats
def analyze_test_case_quality(test_cases: List[Dict]) -> Dict:
"""
分析测试用例质量
Args:
test_cases: 测试用例列表
Returns:
质量分析结果
"""
logger.info("开始分析测试用例质量...")
quality_issues = []
# 检查必填字段
required_fields = ["用例名称", "功能描述", "STEP", "预期结果"]
for i, case in enumerate(test_cases, 1):
for field in required_fields:
value = case.get(field, "")
if not value or not str(value).strip():
quality_issues.append({
"row": case.get("_row_number", i),
"issue": f"必填字段为空: {field}",
"case_name": case.get("用例名称", "")
})
# 检查用例编号格式
for case in test_cases:
case_number = case.get("用例编号", "")
if case_number and not str(case_number).startswith("TC-"):
quality_issues.append({
"row": case.get("_row_number", ""),
"issue": f"用例编号格式不符合规范: {case_number}",
"case_name": case.get("用例名称", "")
})
result = {
"total_cases": len(test_cases),
"issues_count": len(quality_issues),
"issues": quality_issues,
"quality_rate": (len(test_cases) - len(quality_issues)) / len(test_cases) if test_cases else 1.0
}
logger.info("质量分析完成:")
logger.info(" 总用例数: %d", result["total_cases"])
logger.info(" 问题数: %d", result["issues_count"])
logger.info(" 质量率: %.1f%%", result["quality_rate"] * 100)
return result
# 问题描述
## 问题现象
- 运行强制使用API模式时,提示AI服务无法初始化,提示未检测到有效的Anthropic API密钥。
# 日志信息
```ignorelang
2026-03-10 17:49:49 - src.main - INFO - 【步骤3】提取功能点
2026-03-10 17:49:49 - src.main - INFO - ------------------------------------------------------------
2026-03-10 17:49:49 - src.test_case_analyzer - INFO - 未检测到有效API密钥,尝试使用Claude CLI模式
2026-03-10 17:49:49 - src.claude_cli_adapter - INFO - Claude CLI适配器初始化完成
2026-03-10 17:49:49 - src.claude_cli_adapter - ERROR - 未找到claude命令,请确认已安装Claude Code CLI
2026-03-10 17:49:49 - src.claude_cli_adapter - ERROR - 安装方式: npm install -g @anthropic-ai/claude-code
2026-03-10 17:49:49 - src.main - ERROR - 处理失败: 无法初始化AI服务:
1. 未检测到有效的Anthropic API密钥
2. Claude CLI工具不可用
请至少配置其中一种方式
PS E:\GithubData\ubains-module-test\AuxiliaryTool\AIPerfectedTestCases>
```
# AI服务初始化失败_问题处理_计划执行
## 问题分析
### 问题现象
运行 `python run.py --mode api` 时,在步骤3提取功能点时失败,提示"未检测到有效的Anthropic API密钥"。
### 日志信息
```
2026-03-10 17:49:49 - src.test_case_analyzer - INFO - 未检测到有效API密钥,尝试使用Claude CLI模式
2026-03-10 17:49:49 - src.claude_cli_adapter - ERROR - 未找到claude命令
2026-03-10 17:49:49 - src.main - ERROR - 处理失败: 无法初始化AI服务:
1. 未检测到有效的Anthropic API密钥
2. Claude CLI工具不可用
```
### 根本原因
**main.py 已修复前缀验证**,但 `TestCaseAnalyzer``AIServiceManager` 类中仍然存在前缀验证逻辑:
1. **test_case_analyzer.py**(第93行、第100行):
```python
elif api_key and api_key.startswith(API_KEY_PREFIX): # ❌
```
2. **ai_service_manager.py**(第54行、第59行、第205行):
```python
if api_key and api_key.startswith(API_KEY_PREFIX): # ❌
```
**问题流程**
1. main.py 通过了 API 模式检查(传入 `use_cli=False` `api_key`
2. TestCaseAnalyzer 初始化时,检查 `api_key.startswith(API_KEY_PREFIX)`
3. CCSwitch 格式的密钥不是 `sk-ant-api03-` 开头,检查失败
4. 尝试 CLI 模式,失败后抛出异常
## 修复方案
### 方案概述
移除所有类中的 `API_KEY_PREFIX` 前缀验证,改为仅检查密钥是否存在且非空。
### 详细实施步骤
#### 步骤1:修复 test_case_analyzer.py
- [ ] 93行:移除 `api_key.startswith(API_KEY_PREFIX)` 检查
- [ ] 100行:移除 `api_key_from_env.startswith(API_KEY_PREFIX)` 检查
#### 步骤2:修复 ai_service_manager.py
- [ ] 54行:移除前缀检查
- [ ] 59行:移除前缀检查
- [ ] 205行:移除前缀检查
#### 步骤3:清理不再使用的导入
- [ ] main.py 中移除 `API_KEY_PREFIX` 导入(可选,保留也无影响)
## 实施细节
### test_case_analyzer.py 修改内容
**88-96行修改前**
```python
if use_cli:
logger.info("强制使用Claude CLI模式")
self.ai_client = ClaudeCLIClient()
self.mode = "cli"
elif api_key and api_key.startswith(API_KEY_PREFIX):
logger.info("使用Anthropic API模式")
self.ai_client = AnthropicAPIClient(api_key)
self.mode = "api"
```
**修改后**:
```python
if use_cli:
logger.info("强制使用Claude CLI模式")
self.ai_client = ClaudeCLIClient()
self.mode = "cli"
elif api_key and api_key.strip():
logger.info("使用Anthropic API模式")
self.ai_client = AnthropicAPIClient(api_key)
self.mode = "api"
```
**第97-115行修改前**:
```python
else:
api_key_from_env = os.environ.get(API_KEY_ENV, "")
if api_key_from_env.startswith(API_KEY_PREFIX):
logger.info("检测到有效API密钥,使用Anthropic API模式")
self.ai_client = AnthropicAPIClient(api_key_from_env)
self.mode = "api"
else:
logger.info("未检测到有效API密钥,尝试使用Claude CLI模式")
try:
self.ai_client = ClaudeCLIClient()
self.mode = "cli"
except RuntimeError:
raise RuntimeError(...)
```
**修改后**:
```python
else:
api_key_from_env = os.environ.get(API_KEY_ENV, "")
if api_key_from_env and api_key_from_env.strip():
logger.info("检测到API密钥,使用Anthropic API模式")
self.ai_client = AnthropicAPIClient(api_key_from_env)
self.mode = "api"
else:
logger.info("未检测到API密钥,尝试使用Claude CLI模式")
try:
self.ai_client = ClaudeCLIClient()
self.mode = "cli"
except RuntimeError:
raise RuntimeError(
"无法初始化AI服务:\n"
"1. 未设置Anthropic API密钥\n"
"2. Claude CLI工具不可用\n"
"请至少配置其中一种方式"
)
```
### ai_service_manager.py 修改内容
需要将所有 `api_key.startswith(API_KEY_PREFIX)` 改为 `api_key and api_key.strip()`
## 优化功能回填
### 已完成的优化
- [x] 修复 test_case_analyzer.py 前缀验证(第93行、第100行)
- [x] 修复 ai_service_manager.py 前缀验证(第54行、第59行、第205行)
- [x] 更新错误提示信息(不再强调sk-ant-api03-格式)
### 待完成的优化
-
# 问题描述
## 问题现象
- 运行代码时提示,AI调用失败,Extra data: line 51 column 4 (char 1198)
# 日志信息
```ignorelang
2026-03-10 17:59:22 - src.main - INFO - ------------------------------------------------------------
2026-03-10 17:59:22 - src.ai_service_manager - INFO - 使用自定义API endpoint: https://open.bigmodel.cn/api/anthropic
2026-03-10 17:59:22 - src.ai_service_manager - INFO - Anthropic API模式已启用
2026-03-10 17:59:22 - src.ai_service_manager - INFO - AI服务管理器初始化完成,模式: api
2026-03-10 17:59:22 - src.gap_detector - INFO - 缺失检测器初始化完成
2026-03-10 17:59:22 - src.gap_detector - INFO - 开始检测测试用例缺失...
2026-03-10 17:59:39 - httpx - INFO - HTTP Request: POST https://open.bigmodel.cn/api/anthropic/v1/messages "HTTP/1.1 200 OK"
2026-03-10 17:59:39 - src.gap_detector - ERROR - AI调用失败: Extra data: line 51 column 4 (char 1198)
2026-03-10 17:59:39 - src.gap_detector - INFO - 缺失检测完成:
2026-03-10 17:59:39 - src.gap_detector - INFO - 功能点缺失: 0 项
2026-03-10 17:59:39 - src.gap_detector - INFO - 场景覆盖不足: 0 项
2026-03-10 17:59:39 - src.gap_detector - INFO - 边界条件不足: 0 项
2026-03-10 17:59:39 - src.gap_detector - WARNING - ⚠️ 覆盖率未达标: 0.0% < 90.0%, 差距: 90.0%
2026-03-10 17:59:39 - src.gap_detector - INFO - 估算需新增测试用例: 0 条
2026-03-10 17:59:39 - src.main - INFO -
2026-03-10 17:59:39 - src.main - INFO - 【步骤6】生成新测试用例
2026-03-10 17:59:39 - src.main - INFO - ------------------------------------------------------------
```
# AI调用失败Extra data_问题处理_计划执行
## 问题分析
### 问题现象
运行代码时AI调用失败,错误信息:"Extra data: line 51 column 4 (char 1198)"
### 日志信息
```
2026-03-10 17:59:39 - httpx - INFO - HTTP Request: POST https://open.bigmodel.cn/api/anthropic/v1/messages "HTTP/1.1 200 OK"
2026-03-10 17:59:39 - src.gap_detector - ERROR - AI调用失败: Extra data: line 51 column 4 (char 1198)
```
### 根本原因
1. **JSON解析错误**`json.JSONDecodeError` 的 "Extra data" 错误表示:
- 解析器成功解析了一个完整的JSON对象
- 但在该对象后面还有额外的数据
- 例如:`{"a":1}{"b":2}``{"a":1} some text`
2. **智谱AI(GLM)的返回格式可能与官方API不同**
- 可能包含额外的说明文字
- 可能返回多个JSON对象
- 可能使用不同的JSON格式
3. **当前解析逻辑的限制**
```python
# 当前代码直接使用json.loads()
return json.loads(text)
```
如果文本包含额外数据,会直接抛出异常
## 修复方案
### 方案概述
增强JSON解析逻辑,支持以下情况:
1. 提取第一个完整的JSON对象/数组
2. 忽略JSON前后的额外文本
3. 添加详细的调试日志以便排查问题
4. 使用正则表达式更准确地提取JSON
### 详细实施步骤
#### 步骤1:增强 ai_service_manager.py 的JSON解析
- [ ] 修改 `_parse_json()` 方法,使用正则表达式提取JSON
- [ ] 修改 `_parse_json_array()` 方法,增强解析能力
- [ ] 添加调试日志,记录原始响应内容
#### 步骤2:增强其他模块的JSON解析
- [ ] test_case_analyzer.py `_parse_function_points()`
- [ ] gap_detector.py `_parse_gap_result()`
- [ ] case_generator.py `_parse_case()`
- [ ] 统一使用增强的解析逻辑
#### 步骤3:添加通用JSON解析工具函数
- [ ] 创建统一的JSON解析工具函数
- [ ] 支持多种JSON格式
- [ ] 添加详细的错误处理
## 实施细节
### 新增通用JSON解析函数(在各模块中添加)
```python
import re
import json
def extract_json(text: str) -> str:
"""
从文本中提取JSON字符串
支持以下格式:
1. 纯JSON:{"a":1}
2. 包含额外文本:text {"a":1} more text
3. 代码块格式:```json {"a":1} ```
Args:
text: 包含JSON的文本
Returns:
提取的JSON字符串
"""
# 尝试直接解析
try:
json.loads(text.strip())
return text.strip()
except:
pass
# 尝试从代码块中提取
code_block_pattern = r'```(?:json)?\s*\n?([\s\S]*?)\n?```'
matches = re.findall(code_block_pattern, text)
if matches:
for match in matches:
try:
json.loads(match.strip())
return match.strip()
except:
continue
# 尝试提取花括号内容(对象)
brace_pattern = r'\{[\s\S]*\}'
matches = re.findall(brace_pattern, text)
for match in matches:
try:
json.loads(match)
return match
except:
continue
# 尝试提取方括号内容(数组)
bracket_pattern = r'\[[\s\S]*\]'
matches = re.findall(bracket_pattern, text)
for match in matches:
try:
json.loads(match)
return match
except:
continue
return ""
def safe_parse_json(text: str, default=None):
"""
安全地解析JSON
Args:
text: 待解析的文本
default: 解析失败时的默认返回值
Returns:
解析后的Python对象,或默认值
"""
json_str = extract_json(text)
if not json_str:
logger.warning("未能提取到有效的JSON内容")
logger.debug("原始内容: %s", text[:500])
return default
try:
return json.loads(json_str)
except json.JSONDecodeError as e:
logger.error("JSON解析失败: %s", e)
logger.debug("提取的JSON字符串: %s", json_str[:500])
return default
```
### ai_service_manager.py 修改内容
```python
def _parse_json(self, text: str) -> Dict:
"""解析JSON文本"""
import re
logger.debug("尝试解析JSON,内容长度: %d", len(text))
# 使用通用解析函数
result = safe_parse_json(text)
if result is not None:
return result
raise ValueError(f"无法解析JSON: {text[:200]}")
def _parse_json_array(self, text: str) -> List:
"""解析JSON数组文本"""
import re
logger.debug("尝试解析JSON数组,内容长度: %d", len(text))
# 使用通用解析函数
result = safe_parse_json(text, default=[])
if isinstance(result, list):
return result
# 如果结果是对象,尝试提取数组字段
if isinstance(result, dict):
for key, value in result.items():
if isinstance(value, list):
logger.info("从JSON对象中提取数组: %s", key)
return value
return []
```
### 其他模块同步修改
- test_case_analyzer.py
- gap_detector.py
- case_generator.py
各模块使用相同的 `extract_json()` 和 `safe_parse_json()` 函数。
## 优化功能回填
### 已完成的优化
- [x] 更新 ai_service_manager.py 的JSON解析逻辑(添加_extract_json方法)
- [x] 更新 test_case_analyzer.py 的JSON解析逻辑(增强_parse_function_points)
- [x] 更新 gap_detector.py 的JSON解析逻辑(增强_parse_gap_result)
- [x] 更新 case_generator.py 的JSON解析逻辑(增强_parse_case_result)
### 待完成的优化
- 无
### 修复说明
各模块的JSON解析已增强,支持:
1. 从代码块(```json ... ```)中提取JSON
2. 处理包含额外文本的情况
3. 使用括号匹配算法正确处理嵌套的JSON对象/数组
4. 添加详细的调试日志
# 问题描述
## 问题现象
- 运行强制使用API模式时,提示API调用失败,提示错误码为403 Forbidden,错误信息为Request not allowed,导致缺失检测无法完成。
# 日志信息
```ignorelang
2026-03-10 17:52:54 - src.main - INFO - ------------------------------------------------------------
2026-03-10 17:52:54 - src.ai_service_manager - INFO - Anthropic API模式已启用
2026-03-10 17:52:54 - src.ai_service_manager - INFO - AI服务管理器初始化完成,模式: api
2026-03-10 17:52:54 - src.gap_detector - INFO - 缺失检测器初始化完成
2026-03-10 17:52:54 - src.gap_detector - INFO - 开始检测测试用例缺失...
2026-03-10 17:52:55 - httpx - INFO - HTTP Request: POST https://api.anthropic.com/v1/messages "HTTP/1.1 403 Forbidden"
2026-03-10 17:52:55 - src.gap_detector - ERROR - AI调用失败: Error code: 403 - {'error': {'type': 'forbidden', 'message': 'Request not allowed'}}
2026-03-10 17:52:55 - src.gap_detector - INFO - 缺失检测完成:
2026-03-10 17:52:55 - src.gap_detector - INFO - 功能点缺失: 0 项
2026-03-10 17:52:55 - src.gap_detector - INFO - 场景覆盖不足: 0 项
2026-03-10 17:52:55 - src.gap_detector - INFO - 边界条件不足: 0 项
2026-03-10 17:52:55 - src.gap_detector - WARNING - ⚠️ 覆盖率未达标: 0.0% < 90.0%, 差距: 90.0%
2026-03-10 17:52:55 - src.gap_detector - INFO - 估算需新增测试用例: 0 条
2026-03-10 17:52:55 - src.main - INFO -
2026-03-10 17:52:55 - src.main - INFO - 【步骤6】生成新测试用例
2026-03-10 17:52:55 - src.main - INFO - ------------------------------------------------------------
```
# API模式执行提示调用失败_问题处理_计划执行
## 问题分析
### 问题现象
运行 `python run.py --mode api` 时,API调用返回 403 Forbidden 错误,错误信息为 "Request not allowed"。
### 日志信息
```
2026-03-10 17:52:55 - httpx - INFO - HTTP Request: POST https://api.anthropic.com/v1/messages "HTTP/1.1 403 Forbidden"
2026-03-10 17:52:55 - src.gap_detector - ERROR - AI调用失败: Error code: 403 - {'error': {'type': 'forbidden', 'message': 'Request not allowed'}}
```
### 根本原因
1. **用户的API密钥格式**`845694a2ffd64b6c93cc7359d760234d.j3FuzTgfmreM9qNU`
- 这是CCSwitch等代理服务的密钥格式
- 不是官方Anthropic API的密钥格式
2. **代码调用的是官方endpoint**
- 当前代码请求:`https://api.anthropic.com/v1/messages`
- CCSwitch等代理需要使用其自定义endpoint
3. **Anthropic客户端初始化缺少base_url参数**
```python
# 当前代码
self.client = anthropic.Anthropic(api_key=api_key)
# 需要改为
self.client = anthropic.Anthropic(
api_key=api_key,
base_url=custom_endpoint # 支持自定义endpoint
)
```
## 修复方案
### 方案概述
添加自定义API endpoint支持,允许用户通过环境变量配置代理服务的API地址。
### 详细实施步骤
#### 步骤1:修改 config.py
- [ ] 添加 `API_BASE_URL` 配置常量
- [ ] 添加 `get_api_base_url()` 函数,从环境变量读取自定义endpoint
- [ ] 更新 `get_project_info()` 返回endpoint信息
#### 步骤2:修改 test_case_analyzer.py
- [ ] 修改 `AnthropicAPIClient` 初始化,支持自定义base_url
- [ ] 添加endpoint日志输出
#### 步骤3:修改 ai_service_manager.py
- [ ] 修改 `_init_api_mode` 方法,支持自定义base_url
#### 步骤4:更新 .env.example
- [ ] 添加 `API_BASE_URL` 配置说明
## 实施细节
### config.py 修改内容
```python
# ==================== AI配置 ====================
# API基础URL(可选,用于代理服务)
# 官方API: https://api.anthropic.com
# CCSwitch等代理: 根据服务商提供的地址配置
API_BASE_URL = "https://api.anthropic.com"
def get_api_base_url() -> str:
"""
获取API基础URL
优先级:环境变量 > 配置文件常量
Returns:
API基础URL
"""
import os
return os.environ.get("API_BASE_URL", API_BASE_URL)
```
### test_case_analyzer.py 修改内容
```python
class AnthropicAPIClient(AIClientInterface):
"""Anthropic API客户端"""
def __init__(self, api_key: str, base_url: Optional[str] = None):
if anthropic is None:
raise ImportError("未安装anthropic库,请运行: pip install anthropic")
kwargs = {"api_key": api_key}
if base_url:
kwargs["base_url"] = base_url
logger.info("使用自定义API endpoint: %s", base_url)
self.client = anthropic.Anthropic(**kwargs)
```
同时修改TestCaseAnalyzer的初始化:
```python
from src.config import get_api_base_url
class TestCaseAnalyzer:
def __init__(self, api_key: Optional[str] = None, use_cli: bool = False):
# ... 原有逻辑 ...
elif api_key and api_key.strip():
base_url = get_api_base_url()
logger.info("使用Anthropic API模式")
self.ai_client = AnthropicAPIClient(api_key, base_url)
# ...
```
### ai_service_manager.py 修改内容
```python
def _init_api_mode(self, api_key: str):
"""初始化API模式"""
from src.config import get_api_base_url
try:
import anthropic
base_url = get_api_base_url()
kwargs = {"api_key": api_key}
if base_url != "https://api.anthropic.com":
kwargs["base_url"] = base_url
logger.info("使用自定义API endpoint: %s", base_url)
self.api_client = anthropic.Anthropic(**kwargs)
logger.info("Anthropic API模式已启用")
except ImportError:
raise RuntimeError("未安装anthropic库,请运行: pip install anthropic")
```
### .env.example 更新内容
```bash
# ==================== API配置(可选)====================
# API基础URL(用于代理服务)
# 官方API可留空或设置为:https://api.anthropic.com
# CCSwitch等代理服务需设置对应endpoint
API_BASE_URL=https://api.anthropic.com
```
## 使用说明
### 官方API用户
无需额外配置,使用默认endpoint即可。
### CCSwitch等代理用户
在 `.env` 文件中配置:
```bash
ANTHROPIC_API_KEY=your-key-here
API_BASE_URL=https://your-proxy-endpoint.com
```
## 优化功能回填
### 已完成的优化
- [x] 添加API_BASE_URL配置支持(config.py)
- [x] 添加get_api_base_url()函数(从环境变量读取)
- [x] 修改AnthropicAPIClient支持自定义endpoint(test_case_analyzer.py)
- [x] 修改TestCaseAnalyzer传入base_url参数
- [x] 修改AIServiceManager._init_api_mode支持自定义endpoint
- [x] 更新.env.example配置说明
### 待完成的优化
-
# 问题描述
## 问题现象
- 运行代码时报错:unexpected indent (test_case_analyzer.py, line 348)
# 日志信息
```ignorelang
错误: unexpected indent (test_case_analyzer.py, line 348)
Traceback (most recent call last):
File "E:\GithubData\ubains-module-test\AuxiliaryTool\AIPerfectedTestCases\run.py", line 332, in <module>
exit_code = main()
File "E:\GithubData\ubains-module-test\AuxiliaryTool\AIPerfectedTestCases\run.py", line 327, in main
return run_cli(args)
File "E:\GithubData\ubains-module-test\AuxiliaryTool\AIPerfectedTestCases\run.py", line 311, in run_cli
from src.main import main as run_main
File "E:\GithubData\ubains-module-test\AuxiliaryTool\AIPerfectedTestCases\src\main.py", line 41, in <module>
from src.test_case_analyzer import TestCaseAnalyzer, analyze_test_case_quality
File "E:\GithubData\ubains-module-test\AuxiliaryTool\AIPerfectedTestCases\src\test_case_analyzer.py", line 348
return []
IndentationError: unexpected indent
```
# 代码运行报错unexpected indent_问题处理_计划执行
## 问题分析
### 问题现象
运行代码时报错:`unexpected indent (test_case_analyzer.py, line 348)`
### 日志信息
```
IndentationError: unexpected indent
File "E:\...\test_case_analyzer.py", line 348
return []
```
### 根本原因
**Python缩进错误**:在修改 `test_case_analyzer.py``_parse_function_points()` 方法时,第348行的 `return []` 语句被错误地缩进了。
**问题代码**(第346-348行):
```python
logger.warning("未找到有效的JSON数组格式")
logger.debug("AI返回内容: %s", result[:500])
return [] # ❌ 缩进过多
```
**正确代码**应该是:
```python
logger.warning("未找到有效的JSON数组格式")
logger.debug("AI返回内容: %s", result[:500])
return [] # ✅ 与函数内其他代码同级缩进
```
## 修复方案
### 方案概述
修正 `test_case_analyzer.py` 第348行的缩进,使其与函数内其他代码保持同级。
### 实施细节
将第348行从:
```python
return [] # 8个空格缩进(错误)
```
改为:
```python
return [] # 4个空格缩进(正确,与函数内代码同级)
```
实际上,正确的缩进应该是4个空格(一级缩进),因为这是在 `TestCaseAnalyzer` 类的方法内部。
## 优化功能回填
### 已完成的优化
- [x] 修正 test_case_analyzer.py 第348行的缩进错误
### 待完成的优化
-
# 问题描述
## 问题现象
- 在执行代码后提示API权限不足,我提供的API密钥是正确的,因为我可以正常使用claude code CLI工具。
# 日志信息
```ignorelang
PS E:\GithubData\ubains-module-test\AuxiliaryTool\AIPerfectedTestCases> python run.py --test-api
【测试API连接】
API密钥: 845694a2...
AI模型: claude-sonnet-4-6
发送测试请求...
✗ API权限不足
请检查API密钥是否有调用权限
PS E:\GithubData\ubains-module-test\AuxiliaryTool\AIPerfectedTestCases>
```
\ No newline at end of file
# 代码运行提示API权限不足_问题处理_计划执行
## 1. 问题描述
### 问题现象
执行 `python run.py --test-api` 后提示API权限不足,但用户确认API密钥是正确的,可以正常使用claude code CLI工具。
### 错误信息
```
✗ API权限不足
请检查API密钥是否有调用权限
```
### 当前API密钥格式
```
845694a2ffd64b6c93cc7359d760234d.j3FuzTgfmreM9qNU
```
---
## 2. 问题分析
### 2.1 根本原因
**问题核心**:用户混淆了两种不同的密钥:
| 密钥类型 | 格式 | 用途 | 获取方式 |
|---------|------|------|----------|
| **Anthropic API密钥** | `sk-ant-api03-xxxxx` | 通过Python代码调用Claude API | https://console.anthropic.com/ |
| **Claude Code Session Token** | `xxxxx.xxxxx` (用户提供的格式) | 使用claude code CLI工具 | Claude Code CLI自动生成 |
**当前情况**
- 用户提供的是 **Claude Code Session Token**
- 代码需要的是 **Anthropic API密钥**
- 两种密钥**不互通**
### 2.2 为什么会出现这个问题?
1. **Claude Code CLI** 可以使用Session Token直接与Claude交互
2. **Python anthropic包** 需要使用官方API密钥
3. 两者是**不同的认证方式**
### 2.3 验证方法
检查密钥格式:
- ✅ 正确的API密钥:`sk-ant-api03-` 开头
- ❌ 用户提供的:`845694a2ffd64b6c93cc7359d760234d.` 格式(Session Token)
---
## 3. 解决方案
### 方案A:获取正确的Anthropic API密钥(推荐)
#### 步骤1:访问Anthropic控制台
1. 打开浏览器,访问:https://console.anthropic.com/
2. 登录你的Anthropic账户
#### 步骤2:创建API密钥
1. 点击左侧菜单 **"API Keys"**
2. 点击 **"Create API Key"** 按钮
3. 输入密钥名称(如:AI测试用例工具)
4. 点击 **"Create Key"**
5. **复制生成的密钥**(格式:`sk-ant-api03-xxxxx`
#### 步骤3:更新配置
1. 打开 `.env` 文件
2.`ANTHROPIC_API_KEY` 的值替换为新创建的API密钥
3. 保存文件
#### 步骤4:验证
```bash
python run.py --test-api
```
预期输出:
```
✓ API连接正常
AI回复: API连接成功
```
---
### 方案B:使用Claude Code CLI作为后端(备选方案)
如果用户无法获取API密钥,可以考虑直接调用claude code CLI工具。
#### 工作原理
```
Python代码 → subprocess调用 → claude CLI进程 → 发送请求 → 返回结果 → 进程退出
↑ ↓
└──────────────── 自动拉起,用完即退出 ──────────────────────┘
```
**无需保持CLI工具运行**:每次需要AI功能时,代码会自动启动新的claude进程,处理完成后自动退出。
#### 前置条件
1. ✅ 系统已安装 Claude Code CLI 工具
2.`claude` 命令在系统 PATH 中可用
3. ✅ 已登录过一次(Session Token已保存)
#### 实现方式
**新增模块:`src/claude_cli_adapter.py`**
```python
# -*- coding: utf-8 -*-
"""
Claude CLI适配器模块
通过subprocess调用claude命令行工具
"""
import subprocess
import json
from typing import Optional, Dict, Any
from logging import getLogger
logger = getLogger(__name__)
class ClaudeCLIAdapter:
"""Claude CLI适配器"""
def __init__(self):
"""初始化适配器"""
self.timeout = 300 # 5分钟超时
def check_available(self) -> bool:
"""检查claude命令是否可用"""
try:
result = subprocess.run(
["claude", "--version"],
capture_output=True,
text=True,
timeout=10
)
if result.returncode == 0:
logger.info("Claude CLI版本: %s", result.stdout.strip())
return True
return False
except FileNotFoundError:
logger.error("未找到claude命令,请确认已安装Claude Code CLI")
return False
except Exception as e:
logger.error("检查claude命令失败: %s", e)
return False
def ask(self, prompt: str, model: str = "claude-sonnet-4-6") -> str:
"""
调用claude ask命令
Args:
prompt: 提示词
model: 模型名称
Returns:
AI回复文本
"""
logger.info("调用Claude CLI...")
logger.debug("提示词长度: %d 字符", len(prompt))
try:
result = subprocess.run(
["claude", "ask", "--model", model, prompt],
capture_output=True,
text=True,
timeout=self.timeout,
encoding="utf-8"
)
if result.returncode == 0:
response = result.stdout.strip()
logger.info("Claude CLI调用成功,返回 %d 字符", len(response))
return response
else:
logger.error("Claude CLI调用失败: %s", result.stderr)
raise RuntimeError(f"Claude CLI error: {result.stderr}")
except subprocess.TimeoutExpired:
logger.error("Claude CLI调用超时")
raise TimeoutError("Claude CLI调用超时")
except Exception as e:
logger.error("Claude CLI调用异常: %s", e)
raise
def ask_json(self, prompt: str, model: str = "claude-sonnet-4-6") -> Dict:
"""
调用claude ask命令,并解析JSON返回
Args:
prompt: 提示词
model: 模型名称
Returns:
解析后的JSON对象
"""
# 在提示词中要求返回JSON
if "JSON" not in prompt and "json" not in prompt:
prompt = prompt + "\n\n请输出JSON格式的结果。"
response = self.ask(prompt, model)
return self._parse_json_response(response)
def _parse_json_response(self, response: str) -> Dict:
"""解析JSON响应"""
try:
return json.loads(response)
except json.JSONDecodeError:
# 尝试提取JSON部分
start = response.find("{")
end = response.rfind("}") + 1
if start >= 0 and end > start:
json_str = response[start:end]
return json.loads(json_str)
raise ValueError("无法解析JSON响应")
```
**修改主模块以支持两种方式**
**文件:`src/main.py`**
```python
# 检测使用哪种方式
def get_ai_client():
"""获取AI客户端"""
api_key = os.environ.get(API_KEY_ENV)
if api_key and api_key.startswith("sk-ant-api03-"):
# 使用Anthropic API
logger.info("使用Anthropic API模式")
return {
"type": "api",
"client": TestCaseAnalyzer(api_key),
"detector": GapDetector(api_key),
"generator": CaseGenerator(api_key)
}
else:
# 使用Claude CLI
logger.info("使用Claude CLI模式")
from src.claude_cli_adapter import ClaudeCLIAdapter
adapter = ClaudeCLIAdapter()
if not adapter.check_available():
raise RuntimeError("Claude CLI不可用,且未配置有效的API密钥")
return {
"type": "cli",
"adapter": adapter
}
```
#### 优缺点对比
| 优点 | 缺点 |
|------|------|
| ✅ 无需API密钥 | ⚠️ 需要安装Claude Code CLI |
| ✅ 利用现有Session Token | ⚠️ 调用速度较API慢 |
| ✅ 无额外API费用 | ⚠️ 依赖外部工具 |
| ✅ 配置简单 | ⚠️ 错误处理较复杂 |
#### 使用说明
```bash
# 1. 确认claude命令可用
claude --version
# 2. 正常运行(自动检测模式)
python run.py
# 3. 强制使用CLI模式(可选)
python run.py --use-cli
```
text=True
)
return result.stdout
```
#### 优缺点
| 优点 | 缺点 |
|------|------|
| 无需API密钥 | 需要安装claude code CLI |
| 利用现有Session Token | 调用效率较低 |
| 无额外费用 | 依赖外部工具 |
**注意**:此方案需要用户已安装claude code CLI工具。
---
### 方案C:使用其他免费AI API(替代方案)
如果无法获取Anthropic API密钥,可以考虑使用其他支持免费额度的AI服务:
| 服务 | 免费额度 | 说明 |
|------|----------|------|
| Groq | 免费额度较高 | 支持Llama等模型 |
| Hugging Face | 有限免费 | 支持多种开源模型 |
| DeepSeek | 有限免费 | 支持DeepSeek模型 |
---
## 4. 推荐方案
**强烈推荐使用方案A**:获取正确的Anthropic API密钥
### 理由
1. **稳定性**:官方API最稳定可靠
2. **性能**:直接API调用性能最好
3. **功能完整**:支持所有AI功能
4. **成本可控**:可以监控和控制使用量
### 费用说明
- **Claude Sonnet 4.6**:$3 / 1M input tokens
- **Claude Opus 4.6**:$15 / 1M input tokens
- 对于测试用例生成,通常成本很低(几美分级别)
---
## 5. 实施计划
### 5.1 短期方案(立即执行)
| 步骤 | 操作 | 负责人 |
|------|------|--------|
| 1 | 访问Anthropic控制台获取API密钥 | 用户 |
| 2 | 更新.env文件 | 用户 |
| 3 | 运行测试验证 | 用户 |
### 5.2 中期方案(可选)
| 步骤 | 操作 | 说明 |
|------|------|------|
| 1 | 添加多AI服务支持 | 支持Groq、DeepSeek等 |
| 2 | 添加成本预估 | 显示预估费用 |
| 3 | 添加用量监控 | 追踪API调用次数 |
### 5.3 长期方案(考虑中)
| 步骤 | 操作 | 说明 |
|------|------|------|
| 1 | 支持Claude Code CLI调用 | 作为备选方案 |
| 2 | 本地模型支持 | 使用Ollama等运行本地模型 |
---
## 6. API密钥获取指南
### 详细步骤
1. **打开控制台**
```
https://console.anthropic.com/
```
2. **登录账户**
- 使用邮箱登录
- 或使用Google/GitHub登录
3. **创建API密钥**
- 左侧菜单选择 "API Keys"
- 点击 "Create API Key" 按钮
- 输入名称(可选)
- 选择权限(通常默认即可)
- 点击 "Create Key"
4. **复制密钥**
- 密钥格式:`sk-ant-api03-xxxxx`
- ⚠️ **只在创建时显示一次,请妥善保存**
5. **更新配置文件**
```bash
# 编辑 .env 文件
ANTHROPIC_API_KEY=sk-ant-api03-xxxxx
```
### 安全建议
- ✅ 不要将API密钥提交到代码仓库
- ✅ 不要在公开场合分享API密钥
- ✅ 定期轮换API密钥
- ✅ 为不同项目使用不同的API密钥
---
## 7. 常见问题
### Q1: 我没有信用卡怎么办?
**A**: Anthropic需要绑定信用卡才能创建API密钥。如果没有信用卡,可以考虑:
- 使用预付卡/虚拟卡
- 使用方案B(Claude Code CLI)
- 使用方案C(其他免费AI服务)
### Q2: API密钥的费用如何计算?
**A**: 按token使用量计费:
- 输入tokens:发送给AI的文字
- 输出tokens:AI返回的文字
- 大约1美元可以处理数十万个汉字
### Q3: 如何监控API使用量?
**A**: 在Anthropic控制台的 "Usage" 页面可以查看详细的使用记录和费用。
### Q4: 为什么我的Session Token不能用于API调用?
**A**: 两种密钥使用不同的认证协议:
- API密钥:基于HTTP Header认证
- Session Token:基于WebSocket认证(Claude Code专用)
---
## 8. 实施状态
| 项目 | 状态 |
|------|------|
| 问题分析 | ✅ 完成 |
| 解决方案设计 | ✅ 完成 |
| 用户等待获取API密钥 | ⏳ 进行中 |
---
## 9. 后续支持
如果获取API密钥后仍有问题,请提供以下信息:
1. API测试结果
```bash
python run.py --test-api
```
2. 详细日志
```bash
python run.py --test-api --log-level DEBUG
```
3. API密钥格式确认
- 确认密钥以 `sk-ant-api03-` 开头
- 确认密钥完整复制(没有多余空格)
---
## 10. 附录:密钥格式对比
### 正确的API密钥格式
```
sk-ant-api03-038umqPGqEu3K8XjrV8XA7mBKGc1UHrEjBKL3fF
```
- 长度:约60个字符
- 前缀:`sk-ant-api03-`
### Claude Code Session Token格式
```
845694a2ffd64b6c93cc7359d760234d.j3FuzTgfmreM9qNU
```
- 长度:约50个字符
- 包含点号分隔符
**这两种密钥不互通!**
# 问题描述
## 问题现象
- 在执行代码后提示ClaudeCLT不可哦那个,请确认已安装Claude Code CLI。
# 日志信息
```ignorelang
2026-03-10 17:39:00 - src.main - INFO - 【步骤3】提取功能点
2026-03-10 17:39:00 - src.main - INFO - ------------------------------------------------------------
2026-03-10 17:39:00 - src.test_case_analyzer - INFO - 强制使用Claude CLI模式
2026-03-10 17:39:00 - src.claude_cli_adapter - INFO - Claude CLI适配器初始化完成
2026-03-10 17:39:00 - src.claude_cli_adapter - ERROR - 未找到claude命令,请确认已安装Claude Code CLI
2026-03-10 17:39:00 - src.claude_cli_adapter - ERROR - 安装方式: npm install -g @anthropic-ai/claude-code
2026-03-10 17:39:00 - src.main - ERROR - 处理失败: Claude CLI不可用
```
\ No newline at end of file
# 代码运行提示ClaudeCLI不可用_问题处理_计划执行
## 问题分析
### 问题现象
在执行AI完善测试用例工具后,提示"Claude CLI不可用",程序无法继续运行。
### 日志信息
```
2026-03-10 17:39:00 - src.main - INFO - 【步骤3】提取功能点
2026-03-10 17:39:00 - src.test_case_analyzer - INFO - 强制使用Claude CLI模式
2026-03-10 17:39:00 - src.claude_cli_adapter - INFO - Claude CLI适配器初始化完成
2026-03-10 17:39:00 - src.claude_cli_adapter - ERROR - 未找到claude命令,请确认已安装Claude Code CLI
2026-03-10 17:39:00 - src.claude_cli_adapter - ERROR - 安装方式: npm install -g @anthropic-ai/claude-code
2026-03-10 17:39:00 - src.main - ERROR - 处理失败: Claude CLI不可用
```
### 根本原因
1. **工具支持两种AI服务模式**
- API模式:使用Anthropic官方API(需要API密钥)
- CLI模式:使用本地Claude CLI工具(需要安装`@anthropic-ai/claude-code`
2. **当前问题**
- `src/main.py`第190行:`analyzer = TestCaseAnalyzer(api_key=api_key, use_cli=use_cli)`
-`use_cli=True`时,强制使用CLI模式
- 如果用户未安装Claude CLI,程序会报错退出
3. **配置未被正确使用**
- `src/config.py`中定义了`AI_SERVICE_MODE = "auto"`
- 但main.py未使用此配置,而是通过参数传递`use_cli`
## 修复方案
### 方案概述
1. 修改`src/main.py`:使用`AI_SERVICE_MODE`配置决定AI服务模式
2. 修改`src/config.py`:添加从环境变量读取配置的功能
3. 修改`run.py`:添加命令行参数支持
4. 添加`.env.example`文件:提供配置模板
### 详细实施步骤
#### 步骤1:修改config.py
- [ ] 添加从环境变量读取`AI_SERVICE_MODE`的功能
- [ ] 添加获取AI服务模式的函数
#### 步骤2:修改main.py
- [ ] 使用`get_ai_service_mode()`函数获取服务模式
- [ ] 根据`AI_SERVICE_MODE`配置决定使用API还是CLI模式
- [ ] 改进错误提示,引导用户正确配置
#### 步骤3:修改run.py
- [ ] 添加`--mode`命令行参数,支持api/cli/auto选项
- [ ] 添加`--api-key`命令行参数,直接指定API密钥
#### 步骤4:更新.env.example
- [ ] 添加`AI_SERVICE_MODE`配置项说明
## 实施细节
### config.py 修改内容
```python
# 添加函数
def get_ai_service_mode() -> str:
"""
获取AI服务模式
优先级:环境变量 > 配置文件常量
Returns:
服务模式:'auto', 'api', 'cli'
"""
import os
return os.environ.get("AI_SERVICE_MODE", AI_SERVICE_MODE)
```
### main.py 修改内容
```python
# 修改main函数,使用配置
def main(use_cli: bool = False) -> int:
# 检查AI服务模式
api_key = None
service_mode = get_ai_service_mode()
# 命令行参数优先
if use_cli:
service_mode = "cli"
elif os.environ.get(API_KEY_ENV):
api_key = os.environ.get(API_KEY_ENV)
if api_key.startswith(API_KEY_PREFIX):
service_mode = "api"
# 根据模式初始化
if service_mode == "api":
if not api_key or not api_key.startswith(API_KEY_PREFIX):
logger.error("API模式需要有效的ANTHROPIC_API_KEY环境变量")
logger.error("请设置: set ANTHROPIC_API_KEY=sk-ant-api03-...")
return 1
use_cli = False
elif service_mode == "cli":
use_cli = True
# auto模式保持原有逻辑
```
### run.py 修改内容
```python
import argparse
def main():
parser = argparse.ArgumentParser(description="AI完善测试用例工具")
parser.add_argument("--mode", choices=["auto", "api", "cli"],
default="auto", help="AI服务模式")
parser.add_argument("--api-key", help="Anthropic API密钥")
args = parser.parse_args()
# 设置环境变量
if args.mode:
os.environ["AI_SERVICE_MODE"] = args.mode
if args.api_key:
os.environ[API_KEY_ENV] = args.api_key
# 执行主程序
from src.main import main
sys.exit(main())
```
### .env.example 更新内容
```bash
# Anthropic API密钥(可选)
# 获取方式:https://console.anthropic.com/
# ANTHROPIC_API_KEY=sk-ant-api03-...
# AI服务模式(可选)
# auto: 自动检测(默认)
# api: 强制使用Anthropic API
# cli: 强制使用Claude CLI
AI_SERVICE_MODE=api
```
## 优化功能回填
### 已完成的优化
- [x] 添加AI服务模式配置支持(config.py新增get_ai_service_mode函数)
- [x] 添加命令行参数支持(run.py新增--mode和--api-key参数)
- [x] 改进错误提示信息(main.py新增详细的配置引导)
- [x] 更新.env.example配置模板(添加AI_SERVICE_MODE说明)
### 待完成的优化
-
# 问题描述
## 问题现象
- 运行强制使用API模式时,提示API模式需要有效的ANTHROPIC_API_KEY环境变量,导致无法启动。
# 日志信息
```ignorelang
2026-03-10 17:47:13 - src.main - INFO - 日志系统初始化完成,级别=INFO
2026-03-10 17:47:13 - src.main - INFO - ============================================================
2026-03-10 17:47:13 - src.main - INFO - AI完善测试用例工具 启动
2026-03-10 17:47:13 - src.main - INFO - 版本: 1.0.0
2026-03-10 17:47:13 - src.main - INFO - ============================================================
2026-03-10 17:47:13 - src.main - ERROR - API模式需要有效的ANTHROPIC_API_KEY环境变量
2026-03-10 17:47:13 - src.main - ERROR - 请设置API密钥:
2026-03-10 17:47:13 - src.main - ERROR - Windows: set ANTHROPIC_API_KEY=sk-ant-api03-...
2026-03-10 17:47:13 - src.main - ERROR - Linux/Mac: export ANTHROPIC_API_KEY=sk-ant-api03-...
2026-03-10 17:47:13 - src.main - ERROR - 或在 .env 文件中配置: ANTHROPIC_API_KEY=sk-ant-api03-...
2026-03-10 17:47:13 - src.main - ERROR -
2026-03-10 17:47:13 - src.main - ERROR - 如需使用Claude CLI模式,请设置:
2026-03-10 17:47:13 - src.main - ERROR - 环境变量: set AI_SERVICE_MODE=cli
2026-03-10 17:47:13 - src.main - ERROR - 或命令行参数: python run.py --mode cli
```
# 强制代码使用API模式失败_计划执行
## 问题分析
### 问题现象
运行 `python run.py --mode api` 时,提示"API模式需要有效的ANTHROPIC_API_KEY环境变量",程序无法启动。
### 日志信息
```
2026-03-10 17:47:13 - src.main - ERROR - API模式需要有效的ANTHROPIC_API_KEY环境变量
2026-03-10 17:47:13 - src.main - ERROR - 请设置API密钥:
2026-03-10 17:47:13 - src.main - ERROR - Windows: set ANTHROPIC_API_KEY=sk-ant-api03-...
```
### 根本原因
1. **代码验证逻辑过于严格**
- `src/main.py` 中的 API 模式验证逻辑检查密钥是否以 `sk-ant-api03-` 开头
- 用户的密钥格式是 CCSwitch 格式:`845694a2ffd64b6c93cc7359d760234d.j3FuzTgfmreM9qNU`
- 密钥本身是有效的,但不满足前缀验证,被判定为无效
2. **验证代码位置**
```python
if service_mode == "api":
api_key = os.environ.get(API_KEY_ENV)
if not api_key or not api_key.startswith(API_KEY_PREFIX): # ← 问题在这里
logger.error("API模式需要有效的ANTHROPIC_API_KEY环境变量")
return 1
```
3. **前缀常量定义**
```python
# src/config.py
API_KEY_PREFIX = "sk-ant-api03-" # 这个验证太严格了
```
## 修复方案
### 方案概述
移除 API 模式下的密钥前缀验证,改为仅检查密钥是否存在且非空。
### 详细实施步骤
#### 步骤1:修改 main.py 的 API 模式验证逻辑
- [ ] `not api_key.startswith(API_KEY_PREFIX)` 的验证移除
- [ ] 改为仅检查 `api_key` 是否存在且非空
#### 步骤2:调整错误提示
- [ ] 更新错误提示信息,不再强调 `sk-ant-api03-` 格式
## 实施细节
### main.py 修改内容
**修改前**
```python
if service_mode == "api":
api_key = os.environ.get(API_KEY_ENV)
if not api_key or not api_key.startswith(API_KEY_PREFIX):
logger.error("API模式需要有效的ANTHROPIC_API_KEY环境变量")
logger.error("请设置API密钥:")
logger.error(" Windows: set ANTHROPIC_API_KEY=sk-ant-api03-...")
...
return 1
```
**修改后**:
```python
if service_mode == "api":
api_key = os.environ.get(API_KEY_ENV)
if not api_key or not api_key.strip():
logger.error("API模式需要ANTHROPIC_API_KEY环境变量")
logger.error("请设置API密钥:")
logger.error(" Windows: set ANTHROPIC_API_KEY=your-key-here")
logger.error(" Linux/Mac: export ANTHROPIC_API_KEY=your-key-here")
logger.error("或在 .env 文件中配置: ANTHROPIC_API_KEY=your-key-here")
return 1
```
### 说明
- 移除 `api_key.startswith(API_KEY_PREFIX)` 验证
- 保留非空检查:`not api_key or not api_key.strip()`
- 支持任意格式的有效 API 密钥(包括 CCSwitch 格式)
## 优化功能回填
### 已完成的优化
- [x] 移除API密钥前缀验证(main.py第161行)
- [x] 更新错误提示信息(不再强调sk-ant-api03-格式)
- [x] auto模式逻辑同步修改(移除前缀检查)
### 待完成的优化
-
# 问题描述
## 问题现象
- 在执行代码后生成的完善版用例文件,没有新增内容,差异性报告显示没有任何差异,覆盖率未提升。
- 文件路径:[AuxiliaryTool/AIPerfectedTestCases/Perfected/新统一平台权限管理测试用例_完善版本.xlsx][AuxiliaryTool/AIPerfectedTestCases/reports/新统一平台权限管理测试用例_差异性报告.docx]
# 日志信息
```ignorelang
PS E:\GithubData\ubains-module-test\AuxiliaryTool\AIPerfectedTestCases> python run.py
============================================================
AI完善测试用例工具 v1.0.0
============================================================
【环境检查】
✓ API密钥: your-api...
✓ 需求文档: 0 份
✓ PRD文档: 7 份
✓ 测试用例文件: 新统一平台权限管理测试用例.xlsx
【项目配置】
项目根目录: E:\GithubData\ubains-module-test\AuxiliaryTool\AIPerfectedTestCases
需求文档目录: E:\GithubData\ubains-module-test\AuxiliaryTool\AIPerfectedTestCases\config\需求文档
PRD文档目录: E:\GithubData\ubains-module-test\AuxiliaryTool\AIPerfectedTestCases\config\开发PRD
测试用例文件: E:\GithubData\ubains-module-test\AuxiliaryTool\AIPerfectedTestCases\config\测试用例\新统一平台权限管理测试用例.xlsx
AI模型: claude-sonnet-4-6
覆盖率目标: 90.0%
【开始处理】
2026-03-10 17:13:44 - src.main - INFO - 日志系统初始化完成,级别=INFO
2026-03-10 17:13:44 - src.main - INFO - ============================================================
2026-03-10 17:13:44 - src.main - INFO - AI完善测试用例工具 启动
2026-03-10 17:13:44 - src.main - INFO - 版本: 1.0.0
2026-03-10 17:13:44 - src.main - INFO - ============================================================
2026-03-10 17:13:44 - src.main - INFO - API密钥已配置: your-api...
2026-03-10 17:13:44 - src.main - INFO - 验证输入文件和目录...
2026-03-10 17:13:44 - src.main - WARNING - 发现以下问题:
2026-03-10 17:13:44 - src.main - WARNING - - 需求文档目录下没有.docx文件: E:\GithubData\ubains-module-test\AuxiliaryTool\AIPerfectedTestCases\config\需求文档
2026-03-10 17:13:44 - src.main - INFO - 输入验证通过
2026-03-10 17:13:44 - src.main - INFO -
2026-03-10 17:13:44 - src.main - INFO - 【步骤1】读取文档
2026-03-10 17:13:44 - src.main - INFO - ------------------------------------------------------------
2026-03-10 17:13:44 - src.document_reader - INFO - 文档读取器初始化完成
2026-03-10 17:13:44 - src.document_reader - INFO - ============================================================
2026-03-10 17:13:44 - src.document_reader - INFO - 开始读取所有文档...
2026-03-10 17:13:44 - src.document_reader - INFO - ============================================================
2026-03-10 17:13:44 - src.document_reader - WARNING - 需求文档目录下没有找到.docx文件: E:\GithubData\ubains-module-test\AuxiliaryTool\AIPerfectedTestCases\config\需求文档
2026-03-10 17:13:44 - src.document_reader - INFO - 找到 7 份PRD文档
2026-03-10 17:13:44 - src.document_reader - INFO - 读取PRD文档: PRD_权限控制_数据扩展补充.md
2026-03-10 17:13:44 - src.document_reader - INFO - 读取PRD文档: PRD_权限控制_权限管理页面新增开发.md
2026-03-10 17:13:44 - src.document_reader - INFO - 读取PRD文档: PRD_权限组管理_基础权限组_调整基础角色基础权限组.md
2026-03-10 17:13:44 - src.document_reader - INFO - 读取PRD文档: PRD_权限组管理_增删改查_权限组功能开发设计.md
2026-03-10 17:13:44 - src.document_reader - INFO - 读取PRD文档: PRD_权限组管理_权限结构_调整权限数据结构.md
2026-03-10 17:13:44 - src.document_reader - INFO - 读取PRD文档: PRD_权限组管理_添加权限组_给基础角色添加基本权限组.md
2026-03-10 17:13:44 - src.document_reader - INFO - 读取PRD文档: PRD_权限组管理_详情查看_添加权限组详情查看.md
2026-03-10 17:13:44 - src.document_reader - INFO - PRD文档读取完成,共 7 份
2026-03-10 17:13:44 - src.document_reader - INFO - 读取测试用例: 新统一平台权限管理测试用例.xlsx
2026-03-10 17:13:44 - src.document_reader - INFO - 测试用例读取完成,共 0 条
2026-03-10 17:13:44 - src.document_reader - INFO - ============================================================
2026-03-10 17:13:44 - src.document_reader - INFO - 所有文档读取完成:
2026-03-10 17:13:44 - src.document_reader - INFO - 需求文档: 0 份
2026-03-10 17:13:44 - src.document_reader - INFO - PRD文档: 7 份
2026-03-10 17:13:44 - src.document_reader - INFO - 测试用例: 0 条
2026-03-10 17:13:44 - src.document_reader - INFO - ============================================================
2026-03-10 17:13:44 - src.main - INFO -
2026-03-10 17:13:44 - src.main - INFO - 【步骤2】分析测试用例质量
2026-03-10 17:13:44 - src.main - INFO - ------------------------------------------------------------
2026-03-10 17:13:44 - src.test_case_analyzer - INFO - 开始分析测试用例质量...
2026-03-10 17:13:44 - src.test_case_analyzer - INFO - 质量分析完成:
2026-03-10 17:13:44 - src.test_case_analyzer - INFO - 总用例数: 0
2026-03-10 17:13:44 - src.test_case_analyzer - INFO - 问题数: 0
2026-03-10 17:13:44 - src.test_case_analyzer - INFO - 质量率: 100.0%
2026-03-10 17:13:44 - src.main - INFO -
2026-03-10 17:13:44 - src.main - INFO - 【步骤3】提取功能点
2026-03-10 17:13:44 - src.main - INFO - ------------------------------------------------------------
2026-03-10 17:13:44 - src.test_case_analyzer - INFO - 测试用例分析器初始化完成,模型: claude-sonnet-4-6
2026-03-10 17:13:44 - src.test_case_analyzer - INFO - 开始提取功能点...
2026-03-10 17:13:45 - httpx - INFO - HTTP Request: POST https://api.anthropic.com/v1/messages "HTTP/1.1 403 Forbidden"
2026-03-10 17:13:45 - src.test_case_analyzer - ERROR - AI API调用失败: Error code: 403 - {'error': {'type': 'forbidden', 'message': 'Request not allowed'}}
2026-03-10 17:13:45 - src.main - WARNING - 未提取到功能点,请检查需求文档内容
2026-03-10 17:13:45 - src.main - INFO -
2026-03-10 17:13:45 - src.main - INFO - 【步骤4】分析覆盖率
2026-03-10 17:13:45 - src.main - INFO - ------------------------------------------------------------
2026-03-10 17:13:45 - src.test_case_analyzer - INFO - 开始分析测试用例覆盖情况...
2026-03-10 17:13:45 - src.test_case_analyzer - INFO - 覆盖率分析完成:
2026-03-10 17:13:45 - src.test_case_analyzer - INFO - 总功能数: 0
2026-03-10 17:13:45 - src.test_case_analyzer - INFO - 已覆盖: 0
2026-03-10 17:13:45 - src.test_case_analyzer - INFO - 未覆盖: 0
2026-03-10 17:13:45 - src.test_case_analyzer - INFO - 覆盖率: 0.0%
2026-03-10 17:13:45 - src.main - INFO -
2026-03-10 17:13:45 - src.main - INFO - 【步骤5】检测缺失
2026-03-10 17:13:45 - src.main - INFO - ------------------------------------------------------------
2026-03-10 17:13:45 - src.gap_detector - INFO - 缺失检测器初始化完成,模型: claude-sonnet-4-6
2026-03-10 17:13:45 - src.gap_detector - INFO - 开始检测测试用例缺失...
2026-03-10 17:13:46 - httpx - INFO - HTTP Request: POST https://api.anthropic.com/v1/messages "HTTP/1.1 403 Forbidden"
2026-03-10 17:13:46 - src.gap_detector - ERROR - AI API调用失败: Error code: 403 - {'error': {'type': 'forbidden', 'message': 'Request not allowed'}}
2026-03-10 17:13:46 - src.gap_detector - INFO - 缺失检测完成:
2026-03-10 17:13:46 - src.gap_detector - INFO - 功能点缺失: 0 项
2026-03-10 17:13:46 - src.gap_detector - INFO - 场景覆盖不足: 0 项
2026-03-10 17:13:46 - src.gap_detector - INFO - 边界条件不足: 0 项
2026-03-10 17:13:46 - src.gap_detector - WARNING - ⚠️ 覆盖率未达标: 0.0% < 90.0%, 差距: 90.0%
2026-03-10 17:13:46 - src.gap_detector - INFO - 估算需新增测试用例: 0 条
2026-03-10 17:13:46 - src.main - INFO -
2026-03-10 17:13:46 - src.main - INFO - 【步骤6】生成新测试用例
2026-03-10 17:13:46 - src.main - INFO - ------------------------------------------------------------
2026-03-10 17:13:46 - src.case_generator - INFO - 测试用例生成器初始化完成,模型: claude-sonnet-4-6
2026-03-10 17:13:46 - src.case_generator - INFO - 开始生成测试用例...
2026-03-10 17:13:46 - src.case_generator - INFO - 为 0 个功能点缺失生成测试用例...
2026-03-10 17:13:46 - src.case_generator - INFO - 功能点测试用例生成完成: 0 条
2026-03-10 17:13:46 - src.case_generator - INFO - 为 0 个场景覆盖不足生成测试用例...
2026-03-10 17:13:46 - src.case_generator - INFO - 场景测试用例生成完成: 0 条
2026-03-10 17:13:46 - src.case_generator - INFO - 为 0 个边界条件不足生成测试用例...
2026-03-10 17:13:46 - src.case_generator - INFO - 边界条件测试用例生成完成: 0 条
2026-03-10 17:13:46 - src.case_generator - INFO - 测试用例生成完成,共 0 条
2026-03-10 17:13:46 - src.main - INFO -
2026-03-10 17:13:46 - src.main - INFO - 【步骤7】去重处理
2026-03-10 17:13:46 - src.main - INFO - ------------------------------------------------------------
2026-03-10 17:13:46 - src.deduplicator - INFO - 测试用例去重器初始化完成
2026-03-10 17:13:46 - src.deduplicator - INFO - 开始去重处理...
2026-03-10 17:13:46 - src.deduplicator - INFO - 现有用例: 0 条,新生成用例: 0 条
2026-03-10 17:13:46 - src.deduplicator - INFO - 去重完成:
2026-03-10 17:13:46 - src.deduplicator - INFO - 唯一新用例: 0 条
2026-03-10 17:13:46 - src.deduplicator - INFO - 与现有用例重复: 0 条
2026-03-10 17:13:46 - src.deduplicator - INFO - 新用例内部重复: 0 条
2026-03-10 17:13:46 - src.deduplicator - INFO - 总计重复: 0 条
2026-03-10 17:13:46 - src.deduplicator - INFO - 去重率: 0.0%
2026-03-10 17:13:46 - src.main - INFO -
2026-03-10 17:13:46 - src.main - INFO - 【步骤8】保存完善后的测试用例
2026-03-10 17:13:46 - src.main - INFO - ------------------------------------------------------------
2026-03-10 17:13:46 - src.main - INFO - 正在保存完善后的测试用例...
2026-03-10 17:13:46 - src.main - INFO - 完善后的测试用例已保存: E:\GithubData\ubains-module-test\AuxiliaryTool\AIPerfectedTestCases\Perfected\新统一平台权限管理测试用例_完善版本.xlsx
2026-03-10 17:13:46 - src.main - INFO - 原有用例: 0 条
2026-03-10 17:13:46 - src.main - INFO - 新增用例: 0 条
2026-03-10 17:13:46 - src.main - INFO - 总计: 0 条
2026-03-10 17:13:46 - src.main - INFO -
2026-03-10 17:13:46 - src.main - INFO - 【步骤9】生成差异性报告
2026-03-10 17:13:46 - src.main - INFO - ------------------------------------------------------------
2026-03-10 17:13:46 - src.report_generator - INFO - 报告生成器初始化完成
2026-03-10 17:13:46 - src.report_generator - INFO - 开始生成差异性报告: E:\GithubData\ubains-module-test\AuxiliaryTool\AIPerfectedTestCases\reports\新统一平台权限管理测试用例_差异性报告.docx
2026-03-10 17:13:46 - src.report_generator - INFO - 报告生成完成: E:\GithubData\ubains-module-test\AuxiliaryTool\AIPerfectedTestCases\reports\新统一平台权限管理测试用例_差异性报告.docx
2026-03-10 17:13:46 - src.main - INFO -
2026-03-10 17:13:46 - src.main - INFO - ============================================================
2026-03-10 17:13:46 - src.main - INFO - 处理完成!
2026-03-10 17:13:46 - src.main - INFO - ============================================================
2026-03-10 17:13:46 - src.main - INFO - 【输入】
2026-03-10 17:13:46 - src.main - INFO - 需求文档: 0 份
2026-03-10 17:13:46 - src.main - INFO - PRD文档: 7 份
2026-03-10 17:13:46 - src.main - INFO - 原始用例: 0 条
2026-03-10 17:13:46 - src.main - INFO -
2026-03-10 17:13:46 - src.main - INFO - 【输出】
2026-03-10 17:13:46 - src.main - INFO - 完善后的测试用例: E:\GithubData\ubains-module-test\AuxiliaryTool\AIPerfectedTestCases\Perfected\新统一平台权限管理测试用例_完善版本.xlsx
2026-03-10 17:13:46 - src.main - INFO - 差异性报告: E:\GithubData\ubains-module-test\AuxiliaryTool\AIPerfectedTestCases\reports\新统一平台权限管理测试用例_差异性报告.docx
2026-03-10 17:13:46 - src.main - INFO -
2026-03-10 17:13:46 - src.main - INFO - 【统计】
2026-03-10 17:13:46 - src.main - INFO - 覆盖率: 0.0% -> 20.0% (预计)
2026-03-10 17:13:46 - src.main - INFO - 新增用例: 0 条(去重后)
2026-03-10 17:13:46 - src.main - INFO - 功能点缺失: 0 项
2026-03-10 17:13:46 - src.main - INFO - 场景缺失: 0 项
2026-03-10 17:13:46 - src.main - INFO - 边界缺失: 0 项
2026-03-10 17:13:46 - src.main - INFO - ============================================================
```
\ No newline at end of file
# 生成完善用例完全没有新增内容_问题处理_计划执行
## 1. 问题描述
### 问题现象
执行代码后生成的完善版用例文件没有新增内容,差异性报告显示没有任何差异,覆盖率未提升。
### 问题文件
- 完善用例:`AuxiliaryTool/AIPerfectedTestCases/Perfected/新统一平台权限管理测试用例_完善版本.xlsx`
- 差异性报告:`AuxiliaryTool/AIPerfectedTestCases/reports/新统一平台权限管理测试用例_差异性报告.docx`
---
## 2. 问题分析
### 2.1 核心问题分析
根据日志信息,存在以下问题:
| 问题 | 日志证据 | 影响 |
|------|----------|------|
| 需求文档为空 | `需求文档: 0 份` | 无需求文档供AI分析 |
| 测试用例为空 | `测试用例读取完成,共 0 条` | 无法分析现有覆盖 |
| API调用失败 | `HTTP/1.1 403 Forbidden` | 无法使用AI提取功能点 |
| 功能点为空 | `总功能数: 0` | 无法检测缺失和生成用例 |
### 2.2 根本原因
#### 原因1:API密钥问题(403 Forbidden)
```
HTTP Request: POST https://api.anthropic.com/v1/messages "HTTP/1.1 403 Forbidden"
Error code: 403 - {'error': {'type': 'forbidden', 'message': 'Request not allowed'}}
```
**可能原因**
- API密钥无效或过期
- API密钥格式错误
- API密钥没有调用权限
- 账户余额不足
#### 原因2:需求文档目录为空
```
需求文档目录下没有找到.docx文件: ...config\需求文档
```
**影响**:没有需求文档,AI无法提取功能点列表。
#### 原因3:测试用例读取为0条
```
测试用例读取完成,共 0 条
```
**可能原因**
- Excel文件格式不符合预期
- 表头行不在第一行
- 数据行被跳过
---
## 3. 解决方案
### 3.1 API密钥问题修复
#### 方案A:验证并更新API密钥
1. **验证API密钥是否有效**
- 访问 Anthropic 控制台:https://console.anthropic.com/
- 检查API密钥是否有效
- 检查账户余额和使用情况
2. **更新API密钥**
- 更新 `.env` 文件中的 `ANTHROPIC_API_KEY`
- 确保密钥格式正确:`sk-ant-api03-xxxxxxxxx`
3. **测试API连接**
```bash
# 添加测试命令
python run.py --test-api
```
#### 方案B:添加API测试功能
`run.py` 中添加API测试命令:
```python
def test_api_connection(api_key: str) -> bool:
"""测试API连接"""
try:
import anthropic
client = anthropic.Anthropic(api_key=api_key)
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=10,
messages=[{"role": "user", "content": "Hello"}]
)
print("✓ API连接正常")
return True
except Exception as e:
print(f"✗ API连接失败: {e}")
return False
```
### 3.2 需求文档问题修复
#### 方案A:添加需求文档
将需求文档(.docx格式)放入 `config/需求文档/` 目录。
#### 方案B:支持从PRD文档提取功能点
修改代码,使AI可以从PRD文档中提取功能点(无需需求文档)。
```python
def extract_function_points_from_prd(self, prds: List[Dict]) -> List[Dict]:
"""仅从PRD文档提取功能点"""
prompt = """请分析以下PRD文档,提取完整的功能点列表。
...
"""
```
### 3.3 测试用例读取问题修复
#### 方案A:增强Excel读取兼容性
修改 `document_reader.py`,增加错误处理和调试信息:
```python
def read_test_cases(self, test_case_file: Path) -> List[Dict]:
"""读取测试用例(.xlsx)"""
logger.info("读取测试用例: %s", test_case_file.name)
try:
wb = openpyxl.load_workbook(test_case_file)
# 打印所有sheet名称
logger.debug("工作表列表: %s", wb.sheetnames)
# 使用第一个工作表
ws = wb.active
logger.debug("使用工作表: %s", ws.title)
logger.debug("工作表大小: %d 行 x %d 列", ws.max_row, ws.max_column)
# 读取表头
headers = [cell.value for cell in ws[1]]
logger.debug("表头: %s", headers)
cases = []
empty_rows = 0
for row_idx, row in enumerate(ws.iter_rows(min_row=2, values_only=True), start=2):
# 跳过全空行
if not any(row):
empty_rows += 1
continue
case = dict(zip(headers, row))
# 检查是否有用例名称
case_name = case.get("用例名称")
if case_name and str(case_name).strip():
case["_row_number"] = row_idx
cases.append(case)
else:
logger.debug("第%d行:用例名称为空,跳过", row_idx)
logger.info("测试用例读取完成,共 %d 条(跳过 %d 个空行)",
len(cases), empty_rows)
return cases
except Exception as e:
logger.error("读取测试用例失败: %s", e)
raise
```
#### 方案B:支持指定工作表
添加参数支持指定使用哪个工作表:
```python
def read_test_cases(self, test_case_file: Path, sheet_name: str = None) -> List[Dict]:
"""读取测试用例(.xlsx)"""
wb = openpyxl.load_workbook(test_case_file)
if sheet_name:
ws = wb[sheet_name]
else:
ws = wb.active
```
---
## 4. 实施计划
### 4.1 代码修改
- [ ] 添加API测试功能
- [ ] 修改功能点提取逻辑,支持仅从PRD提取
- [ ] 增强Excel读取错误处理和调试信息
- [ ] 添加详细日志便于排查问题
### 4.2 配置检查
- [ ] 验证API密钥是否有效
- [ ] 检查需求文档目录
- [ ] 检查测试用例文件格式
### 4.3 测试验证
- [ ] 测试API连接
- [ ] 测试PRD文档提取功能点
- [ ] 测试测试用例读取
---
## 5. 代码实现
### 5.1 添加API测试功能
**文件:`run.py`**
```python
def test_api_command() -> int:
"""测试API连接"""
print("【测试API连接】")
print()
api_key = os.environ.get(API_KEY_ENV)
if not api_key:
print("✗ 未设置API密钥")
print(f" 请设置环境变量: {API_KEY_ENV}")
print(" 或在 .env 文件中配置")
return 1
# 隐藏密钥显示
masked_key = api_key[:8] + "..." if len(api_key) > 8 else "***"
print(f"API密钥: {masked_key}")
print()
try:
import anthropic
client = anthropic.Anthropic(api_key=api_key)
print("发送测试请求...")
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=50,
messages=[{"role": "user", "content": "请回复:API连接成功"}]
)
result = response.content[0].text
print(f"✓ API连接正常")
print(f" AI回复: {result}")
return 0
except anthropic.AuthenticationError:
print("✗ API密钥认证失败")
print(" 请检查API密钥是否正确")
return 1
except anthropic.PermissionDeniedError:
print("✗ API权限不足")
print(" 请检查API密钥是否有调用权限")
return 1
except anthropic.RateLimitError:
print("✗ API调用频率超限")
print(" 请稍后再试")
return 1
except Exception as e:
print(f"✗ API连接失败: {e}")
return 1
```
添加命令行参数:
```python
parser.add_argument(
'--test-api',
action='store_true',
dest='test_api',
help='测试API连接'
)
```
### 5.2 修改功能点提取逻辑
**文件:`src/test_case_analyzer.py`**
```python
def extract_function_points(self, requirements: List[Dict],
prds: List[Dict]) -> List[Dict]:
"""
使用AI提取功能点列表
优先从需求文档提取,如果没有需求文档则从PRD文档提取
"""
logger.info("开始提取功能点...")
# 检查是否有需求文档
has_requirements = len(requirements) > 0
if has_requirements:
logger.info("使用需求文档和PRD文档提取功能点")
prompt = self._build_extract_prompt(requirements, prds)
else:
logger.warning("没有需求文档,仅使用PRD文档提取功能点")
prompt = self._build_prd_only_prompt(prds)
# 调用AI...
```
添加仅从PRD提取的提示词:
```python
def _build_prd_only_prompt(self, prds: List[Dict]) -> str:
"""构建仅从PRD提取功能点的提示词"""
prompt = """你是一个专业的测试用例分析专家。请分析以下PRD文档,提取完整的功能点列表。
【要求】
1. 按功能模块分类
2. 每个功能点包含:功能模块、功能编号、功能描述
3. 识别所有子功能
4. 输出格式必须是JSON数组
【开发PRD文档】
"""
for prd in prds:
prompt += f"\n--- {prd['filename']} ---\n"
content = prd['content']
if len(content) > 5000:
content = content[:5000] + "\n...(内容已截断)"
prompt += content + "\n"
prompt += """
\n请输出JSON格式的功能点列表。
注意:必须输出合法的JSON格式,只输出JSON数组,不要有其他说明文字。
"""
return prompt
```
### 5.3 增强Excel读取
**文件:`src/document_reader.py`**
修改 `read_test_cases` 方法,增加更多调试信息:
```python
def read_test_cases(self, test_case_file: Path) -> List[Dict]:
"""读取测试用例(.xlsx)"""
logger.info("读取测试用例: %s", test_case_file.name)
if not test_case_file.exists():
logger.error("测试用例文件不存在: %s", test_case_file)
raise FileNotFoundError(f"测试用例文件不存在: {test_case_file}")
try:
wb = openpyxl.load_workbook(test_case_file, read_only=True, data_only=True)
# 打印工作表信息
logger.info("工作表列表: %s", wb.sheetnames)
# 使用活动工作表
ws = wb.active
logger.info("使用工作表: %s (大小: %d行 x %d列)",
ws.title, ws.max_row, ws.max_column)
# 读取表头
headers = None
for row_idx in range(1, min(10, ws.max_row + 1)):
row_data = [cell.value for cell in ws[row_idx]]
if any("用例名称" in str(cell.value) for cell in ws[row_idx]):
headers = row_data
logger.info("在第%d行找到表头: %s", row_idx, headers)
break
if not headers:
logger.warning("未找到包含'用例名称'的表头行,使用第1行作为表头")
headers = [cell.value for cell in ws[1]]
# 读取数据
cases = []
data_start_row = 2 # 默认从第2行开始
for row_idx in range(data_start_row, ws.max_row + 1):
row_data = [cell.value for cell in ws[row_idx]]
# 跳过空行
if not any(row_data):
continue
case = dict(zip(headers, row_data))
# 检查是否有有效内容
case_name = case.get("用例名称")
if case_name and str(case_name).strip():
case["_row_number"] = row_idx
cases.append(case)
elif row_idx < 10: # 只打印前10行的调试信息
logger.debug("第%d行:用例名称为空或不存在,跳过。行数据: %s",
row_idx, {k: v for k, v in case.items() if v})
logger.info("测试用例读取完成,共 %d 条", len(cases))
wb.close()
return cases
except Exception as e:
logger.error("读取测试用例失败: %s", e)
import traceback
logger.debug(traceback.format_exc())
raise
```
---
## 6. 实施状态
| 项目 | 状态 |
|------|------|
| 问题分析 | ✅ 完成 |
| 解决方案设计 | ✅ 完成 |
| 代码实现 | ⏳ 待实施 |
| 测试验证 | ⏳ 待完成 |
---
## 7. 验证清单
- [ ] API密钥有效,连接测试通过
- [ ] 需求文档已添加或支持仅从PRD提取
- [ ] 测试用例能正确读取
- [ ] 功能点能正确提取
- [ ] 缺失检测能正常工作
- [ ] 新用例能正常生成
- [ ] 差异性报告正确生成
---
## 8. 注意事项
1. **API密钥安全**:不要将API密钥提交到代码仓库
2. **文件格式**:确保测试用例Excel文件格式正确
3. **日志级别**:使用 `--log-level DEBUG` 查看详细日志
4. **网络连接**:确保能访问 Anthropic API
# 问题描述
## 问题现象
- 运行代码后存在`ERROR - 生成测试用例失败(权限数据结构调整后的分类层级展示): 'list' object has no attribute 'get'`报错信息。
# 日志信息
```ignorelang
2026-03-10 18:14:32 - src.main - INFO -
2026-03-10 18:14:32 - src.main - INFO - 【步骤6】生成新测试用例
2026-03-10 18:14:32 - src.main - INFO - ------------------------------------------------------------
2026-03-10 18:14:32 - src.ai_service_manager - INFO - 使用自定义API endpoint: https://open.bigmodel.cn/api/anthropic
2026-03-10 18:14:32 - src.ai_service_manager - INFO - Anthropic API模式已启用
2026-03-10 18:14:32 - src.ai_service_manager - INFO - AI服务管理器初始化完成,模式: api
2026-03-10 18:14:32 - src.case_generator - INFO - 测试用例生成器初始化完成
2026-03-10 18:14:32 - src.case_generator - INFO - 开始生成测试用例...
2026-03-10 18:14:32 - src.case_generator - INFO - 为 6 个功能点缺失生成测试用例...
2026-03-10 18:14:36 - httpx - INFO - HTTP Request: POST https://open.bigmodel.cn/api/anthropic/v1/messages "HTTP/1.1 200 OK"
2026-03-10 18:14:39 - httpx - INFO - HTTP Request: POST https://open.bigmodel.cn/api/anthropic/v1/messages "HTTP/1.1 200 OK"
2026-03-10 18:14:42 - httpx - INFO - HTTP Request: POST https://open.bigmodel.cn/api/anthropic/v1/messages "HTTP/1.1 200 OK"
2026-03-10 18:14:49 - httpx - INFO - HTTP Request: POST https://open.bigmodel.cn/api/anthropic/v1/messages "HTTP/1.1 200 OK"
2026-03-10 18:14:54 - httpx - INFO - HTTP Request: POST https://open.bigmodel.cn/api/anthropic/v1/messages "HTTP/1.1 200 OK"
2026-03-10 18:14:57 - httpx - INFO - HTTP Request: POST https://open.bigmodel.cn/api/anthropic/v1/messages "HTTP/1.1 200 OK"
2026-03-10 18:14:57 - src.case_generator - ERROR - 生成测试用例失败(权限数据结构调整后的分类层级展示): 'list' object has no attribute 'get'
2026-03-10 18:14:57 - src.case_generator - INFO - 功能点测试用例生成完成: 5 条
2026-03-10 18:14:57 - src.case_generator - INFO - 为 5 个场景覆盖不足生成测试用例...
2026-03-10 18:15:03 - httpx - INFO - HTTP Request: POST https://open.bigmodel.cn/api/anthropic/v1/messages "HTTP/1.1 200 OK"
2026-03-10 18:15:06 - httpx - INFO - HTTP Request: POST https://open.bigmodel.cn/api/anthropic/v1/messages "HTTP/1.1 200 OK"
2026-03-10 18:15:13 - httpx - INFO - HTTP Request: POST https://open.bigmodel.cn/api/anthropic/v1/messages "HTTP/1.1 200 OK"
2026-03-10 18:15:18 - httpx - INFO - HTTP Request: POST https://open.bigmodel.cn/api/anthropic/v1/messages "HTTP/1.1 200 OK"
2026-03-10 18:15:22 - httpx - INFO - HTTP Request: POST https://open.bigmodel.cn/api/anthropic/v1/messages "HTTP/1.1 200 OK"
2026-03-10 18:15:22 - src.case_generator - INFO - 场景测试用例生成完成: 5 条
2026-03-10 18:15:22 - src.case_generator - INFO - 为 5 个边界条件不足生成测试用例...
2026-03-10 18:15:25 - httpx - INFO - HTTP Request: POST https://open.bigmodel.cn/api/anthropic/v1/messages "HTTP/1.1 200 OK"
2026-03-10 18:15:29 - httpx - INFO - HTTP Request: POST https://open.bigmodel.cn/api/anthropic/v1/messages "HTTP/1.1 200 OK"
2026-03-10 18:15:33 - httpx - INFO - HTTP Request: POST https://open.bigmodel.cn/api/anthropic/v1/messages "HTTP/1.1 200 OK"
2026-03-10 18:15:38 - httpx - INFO - HTTP Request: POST https://open.bigmodel.cn/api/anthropic/v1/messages "HTTP/1.1 200 OK"
2026-03-10 18:15:42 - httpx - INFO - HTTP Request: POST https://open.bigmodel.cn/api/anthropic/v1/messages "HTTP/1.1 200 OK"
2026-03-10 18:15:42 - src.case_generator - INFO - 边界条件测试用例生成完成: 5 条
2026-03-10 18:15:42 - src.case_generator - INFO - 测试用例生成完成,共 15 条
```
# 生成测试用例失败_问题处理_计划执行
## 问题分析
### 问题现象
运行代码时报错:`'list' object has no attribute 'get'`
### 日志信息
```
2026-03-10 18:14:57 - src.case_generator - ERROR - 生成测试用例失败(权限数据结构调整后的分类层级展示): 'list' object has no attribute 'get'
```
### 根本原因
**问题流程**
1. `case_generator.py` 调用 `self.ai_service.ask_json(prompt)` 期望获取单个测试用例的字典对象
2. `ai_service_manager.py``_parse_json()` 方法声明返回 `Dict`,但实际上直接返回 `json.loads()` 的结果
3. 当智谱AI返回的是一个JSON数组(如 `[{用例1}, {用例2}]`)时,`json.loads()` 返回的是 `list` 而不是 `dict`
4. `case_generator.py` 在第313行调用 `case.get("测试频次")` 时,由于 `case``list` 对象,list没有 `.get()` 方法,导致报错
**问题代码**
```python
# ai_service_manager.py - _parse_json
def _parse_json(self, text: str) -> Dict:
...
return json.loads(json_str) # ❌ 如果JSON是数组,返回的是list
```
**智谱AI的返回格式可能与Claude不同**
- Claude通常按提示词要求返回单个对象
- 智谱AI可能返回包含多个对象的数组
## 修复方案
### 方案概述
1. 修改 `_parse_json` 方法,处理返回数组的情况
2.`case_generator.py``_call_ai_for_case` 中增强类型检查和处理
### 实施细节
#### 修改1:ai_service_manager.py - _parse_json 方法
当解析结果是数组时:
- 如果数组只有一个元素且是对象,返回该对象
- 如果数组有多个元素,记录警告并返回第一个对象
- 这样可以兼容不同的AI返回格式
```python
def _parse_json(self, text: str) -> Dict:
"""解析JSON文本"""
import json
logger.debug("尝试解析JSON,内容长度: %d", len(text))
json_str = self._extract_json(text)
if json_str:
try:
result = json.loads(json_str)
# 如果返回的是数组,取第一个元素
if isinstance(result, list):
if len(result) > 0:
if isinstance(result[0], dict):
logger.info("JSON解析结果是数组,取第一个元素")
return result[0]
else:
logger.warning("JSON数组第一个元素不是对象")
else:
logger.warning("JSON数组为空")
raise ValueError("JSON解析结果为空数组")
return result
except json.JSONDecodeError as e:
logger.error("JSON解析失败: %s", e)
logger.debug("提取的JSON字符串: %s", json_str[:500])
raise ValueError(f"无法解析JSON: {text[:200]}")
```
#### 修改2:case_generator.py - _call_ai_for_case 方法
增加类型检查,确保返回的是字典对象:
```python
def _call_ai_for_case(self, prompt: str, description: str) -> Optional[Dict]:
"""调用AI生成单个测试用例"""
try:
case = self.ai_service.ask_json(prompt, AI_MODEL)
# 类型检查:确保返回的是字典
if isinstance(case, list):
if len(case) > 0 and isinstance(case[0], dict):
logger.info("AI返回了数组,取第一个元素")
case = case[0]
else:
logger.error("AI返回的数组格式不符合预期")
return None
if not isinstance(case, dict):
logger.error("AI返回的不是字典对象: %s", type(case))
return None
# 设置默认值
if not case.get("测试频次"):
case["测试频次"] = DEFAULT_TEST_FREQUENCY
if not case.get("JSON"):
case["JSON"] = "暂不涉及"
return case
except Exception as e:
logger.error("生成测试用例失败(%s): %s", description, e)
logger.debug("错误详情", exc_info=True)
return None
```
## 优化功能回填
### 已完成的优化
- [x] 修改 ai_service_manager.py 的 _parse_json 方法(处理数组返回情况)
- [x] 修改 case_generator.py 的 _call_ai_for_case 方法(增加类型检查)
### 待完成的优化
-
### 修复说明
智谱AI等模型可能返回JSON数组而不是单个对象。修改后的代码:
1. `_parse_json` 方法:当解析结果是数组时,自动取第一个元素
2. `_call_ai_for_case` 方法:增加类型检查,兼容数组返回格式
3. 添加详细的调试日志,便于排查问题
# AI服务调用方式补充说明
## 概述
AI完善测试用例工具支持两种AI调用方式,用户可根据实际情况选择:
---
## 方式对比
| 方式 | 需求 | 费用 | 速度 | 稳定性 |
|------|------|------|------|--------|
| **Anthropic API** | API密钥 | 按量计费 | 快 | 高 |
| **Claude Code CLI** | Claude CLI工具 | 免费 | 较慢 | 中 |
---
## 方式A:Anthropic API(推荐)
### 说明
直接通过Python代码调用Anthropic官方API,需要获取API密钥。
### 获取方式
1. 访问 https://console.anthropic.com/
2. 创建API密钥
3. 配置到 `.env` 文件
### 费用
- Claude Sonnet 4.6:$3 / 1M input tokens
- Claude Opus 4.6:$15 / 1M input tokens
- 预估成本:处理100个测试用例约 $0.05-0.15
### 优点
- ✅ 性能最好,响应最快
- ✅ 功能最完整
- ✅ 可监控使用量和费用
---
## 方式B:Claude Code CLI(备选)
### 说明
通过调用本地的 `claude` 命令行工具来使用AI能力,使用现有的Session Token。
### 工作原理
```
程序运行 → 自动调用claude命令 → 获取结果 → 命令退出
↑ ↓
└─────────────── 无需手动操作,自动拉起 ─────────┘
```
### 前置条件
1. 已安装 Claude Code CLI 工具
2. `claude` 命令在系统 PATH 中可用
3. 已登录过一次(Session Token已保存)
### 配置方式
**自动检测**:程序会自动检测API密钥格式,如果不满足要求则自动切换到CLI模式。
**手动指定**(可选):
```bash
python run.py --use-cli
```
### 费用
- ✅ 免费(使用现有Session Token)
### 优点
- ✅ 无需额外获取API密钥
- ✅ 无额外API费用
- ✅ 配置简单
### 缺点
- ⚠️ 需要系统已安装Claude Code CLI
- ⚠️ 调用速度较直接API慢
- ⚠️ 依赖外部工具
---
## 自动切换逻辑
程序启动时会按以下逻辑自动选择:
```
1. 检查环境变量ANTHROPIC_API_KEY
2. 判断密钥格式
├─ 以 "sk-ant-api03-" 开头?
│ └─ 是 → 使用 Anthropic API 模式
└─ 否 → 检查claude命令是否可用
└─ 可用 → 使用 Claude CLI 模式
```
---
## 使用建议
### 推荐使用 Anthropic API 的场景
- 需要处理大量测试用例(>100个)
- 对处理速度有要求
- 需要精确监控使用量
- 已有API使用环境
### 推荐使用 Claude CLI 的场景
- 测试验证阶段
- 用例数量较少(<50个)
- 无额外API预算
- 已习惯使用Claude Code CLI
---
## 切换方式
### 从CLI切换到API
1. 获取 Anthropic API 密钥
2. 更新 `.env` 文件:
```
ANTHROPIC_API_KEY=sk-ant-api03-xxxxx
```
3. 重新运行程序
### 从API切换到CLI
1. 清空或注释 `.env` 中的 API 密钥
2. 确认 `claude` 命令可用
3. 重新运行程序
# AI完善用例需求文档
## 代码路径
- 代码路径:[AuxiliaryTool/AIPerfectedTestCases]
## 功能需求
### 功能目标
**目标:** 通过结合已有测试用例+需求文档+开发需求PRD文档,对当前已有测试用例进行查漏补缺,完善测试用例内容,确保测试用例覆盖全面且符合规范要求。
### 需求描述
#### 文档路径获取
- 文档路径:[AuxiliaryTool/AIPerfectedTestCases/config]
- 需求文档:`config/需求文档/*.docx`
- 开发需求PRD文档:`config/开发PRD/*.md`
- 测试用例:`config/测试用例/新统一平台权限管理测试用例.xlsx`
#### 测试用例格式确认
- 测试用例格式:Excel表格,包含以下列:
- 序列号:序号,用于排序
- 功能模块:功能模块名称,用于分类
- 用例编号:用例编号,用于标识用例
- 功能描述:功能描述,用于描述功能
- 用例等级:用例等级,用于描述用例的优先级
- 功能编号:功能编号,用于标识功能
- 用例名称:用例名称,用于标识用例
- 预置条件:测试用例的预置条件,用于准备测试环境
- STEP:步骤描述
- JSON:自动化测试用例,暂不涉及
- 预期结果:测试用例的预期结果,用于描述测试结果
- 测试结果:测试用例的测试结果,用于描述测试结果
- 测试频次:测试用例的测试频次,用于描述测试用例的测试频次
- 日志/截图/照片:用于记录测试过程中产生的日志、截图、照片,用于测试结果分析。
- 备注:用于记录测试过程中产生的问题,用于问题排查。
#### 功能实现方式
- AI服务:通过AI服务调用,对需求文档和开发需求PRD文档进行解析和理解,提取功能点,并生成测试用例内容。
- API模式:通过兼容Anthropic API进行调用:
- 支持官方Anthropic API
- 支持通过CCSwitch适配的智谱AI(GLM)等服务
- 配置项:ANTHROPIC_API_KEY、API_BASE_URL
- CLT模式:通过本地claude命令行工具调用,使用现有session token
- 安装: npm install -g @anthropic/claude
- Auto模式:自动检测,优先使用API模式,若API模式不可用则使用CLT模式。
- 需求认知:阅读需求文档中功能描述,确定功能需求。
- 获取测试用例:从测试用例文件中获取当前已有的测试用例内容。
- 读取开发需求PRD文档:从开发需求PRD文档中获取功能实现细节和边界条件。
- 目录位置:`AuxiliaryTool/AIPerfectedTestCases/config/开发PRD`,且存在多份PRD文档,需对每份文档进行读取和分析。
- 查漏补缺:通过对比需求文档和开发需求PRD文档,将缺少的测试用例添加到测试用例文件中。
- 功能点缺失:基于读取需求文档和开发需求PRD文档,比较测试用例中缺少的测试点。
- 场景覆盖不足:基于需求文档和开发需求PRD文档,比较测试用例中缺少的场景,覆盖率目标为90%。
- 边界条件不足:基于需求文档和开发需求PRD文档,比较测试用例中缺少的边界条件。
- 去重处理:对添加的测试用例进行去重,确保测试用例不重复。
- 完善测试用例:将需求文档和开发需求PRD文档中的内容,与已有的测试用例进行对比,并添加缺失的测试用例。
- 生成完善后的新用例文件,不直接修改原文件,添加到`AuxiliaryTool/AIPerfectedTestCases/Perfected`目录下,命名为`新统一平台权限管理测试用例_完善版本.xlsx`
- 生成差异性报告,将差异性报告添加到`AuxiliaryTool/AIPerfectedTestCases/reports`目录下,命名为`新统一平台权限管理测试用例_差异性报告.docx`,报告内容包括:
- 功能点缺失的测试用例列表
- 场景覆盖不足的测试用例列表
- 边界条件不足的测试用例列表
- 去重处理结果
- 用例数量增长统计
- 覆盖率百分比统计
## 规范文档
- 代码规范: `Docs/PRD/01规范文档/_PRD_规范文档_代码规范.md`
- 问题总结: `Docs/PRD/01规范文档/_PRD_问题总结_记录文档.md`
- 方法总结: `Docs/PRD/01规范文档/_PRD_方法总结_记录文档.md`
- 文档规范: `Docs/PRD/01规范文档/_PRD_规范文档_文档规范.md`
- 测试规范: `Docs/PRD/01规范文档/_PRD_规范文档_测试规范.md`
---
\ No newline at end of file
# AI完善用例需求文档_计划执行
## 1. 项目概述
### 1.1 项目背景
通过结合已有测试用例、需求文档、开发需求PRD文档,利用AI能力对测试用例进行查漏补缺,完善测试用例内容,确保测试用例覆盖全面且符合规范要求。
### 1.2 项目目标
- 自动读取需求文档和开发PRD文档
- 分析现有测试用例的覆盖情况
- 识别缺失的功能点、场景、边界条件
- 生成完善的测试用例
- 输出差异性报告
### 1.3 覆盖率目标
- 场景覆盖率目标:**90%**
---
## 2. 项目结构
```
AuxiliaryTool/AIPerfectedTestCases/
├── src/ # 源代码目录
│ ├── __init__.py
│ ├── config.py # 配置模块
│ ├── document_reader.py # 文档读取模块
│ ├── test_case_analyzer.py # 测试用例分析模块
│ ├── gap_detector.py # 缺失检测模块
│ ├── case_generator.py # 用例生成模块
│ ├── deduplicator.py # 去重模块
│ ├── report_generator.py # 报告生成模块
│ └── main.py # 主入口模块
├── config/ # 配置文件目录
│ ├── 需求文档/
│ │ └── *.docx # 需求文档
│ ├── 开发PRD/
│ │ └── *.md # 开发PRD文档
│ └── 测试用例/
│ └── 新统一平台权限管理测试用例.xlsx
├── Perfected/ # 输出目录
│ └── 新统一平台权限管理测试用例_完善版本.xlsx
├── reports/ # 报告输出目录
│ └── 新统一平台权限管理测试用例_差异性报告.docx
├── logs/ # 日志目录
├── run.py # 入口脚本
├── requirements.txt # 依赖包列表
└── README.md # 说明文档
```
---
## 3. 技术方案
### 3.1 技术栈
| 技术 | 用途 |
|------|------|
| Python 3.10+ | 开发语言 |
| openpyxl | Excel文件读写 |
| python-docx | Word文档读写 |
| anthropic | Claude API SDK |
| pandas | 数据处理 |
### 3.2 AI服务调用方式
本工具支持多种AI服务调用方式,可灵活配置:
#### API模式(推荐)
通过兼容Anthropic API格式的服务调用:
- 支持官方Anthropic API
- 支持通过CCSwitch适配的智谱AI(GLM)等服务
- 配置项:`ANTHROPIC_API_KEY``API_BASE_URL``AI_MODEL`
```python
import anthropic
# API模式调用
client = anthropic.Anthropic(
api_key="your-api-key",
base_url="https://api.anthropic.com" # 可选,代理服务需配置
)
response = client.messages.create(
model="claude-sonnet-4-6", # 或 glm-4.7
max_tokens=8192,
messages=[...]
)
```
#### CLI模式
通过本地claude命令行工具调用,使用现有Session Token:
- 安装:`npm install -g @anthropic-ai/claude-code`
- 配置:`AI_SERVICE_MODE=cli`
#### Auto模式(默认)
自动检测,优先使用API模式,若API模式不可用则使用CLI模式:
- 配置:`AI_SERVICE_MODE=auto`
### 3.3 核心流程
```
┌─────────────────────────────────────────────────────────────┐
│ 主流程 │
├─────────────────────────────────────────────────────────────┤
│ 1. 读取配置 │
│ 2. 读取需求文档(.docx) │
│ 3. 读取开发PRD文档(.md) │
│ 4. 读取现有测试用例(.xlsx) │
│ 5. 提取功能点列表(AI分析) │
│ 6. 分析现有用例覆盖情况 │
│ 7. 检测缺失:功能点、场景、边界条件 │
│ 8. 生成新用例(AI生成) │
│ 9. 去重处理 │
│ 10. 生成完善后的测试用例文件 │
│ 11. 生成差异性报告 │
└─────────────────────────────────────────────────────────────┘
```
---
## 4. 详细设计
### 4.1 配置模块 (config.py)
```python
# -*- coding: utf-8 -*-
"""
AI完善测试用例工具 - 配置模块
"""
from pathlib import Path
# 项目路径
PROJECT_ROOT = Path(__file__).parent.parent
CONFIG_DIR = PROJECT_ROOT / "config"
PERFECTED_DIR = PROJECT_ROOT / "Perfected"
REPORTS_DIR = PROJECT_ROOT / "reports"
LOGS_DIR = PROJECT_ROOT / "logs"
# 输入路径
REQUIREMENTS_DIR = CONFIG_DIR / "需求文档"
PRD_DIR = CONFIG_DIR / "开发PRD"
TEST_CASE_FILE = CONFIG_DIR / "测试用例" / "新统一平台权限管理测试用例.xlsx"
# 输出路径
OUTPUT_TEST_CASE = PERFECTED_DIR / "新统一平台权限管理测试用例_完善版本.xlsx"
OUTPUT_REPORT = REPORTS_DIR / "新统一平台权限管理测试用例_差异性报告.docx"
# 测试用例列定义
TEST_CASE_COLUMNS = [
"序列号", "功能模块", "用例编号", "功能描述", "用例等级",
"功能编号", "用例名称", "预置条件", "STEP", "JSON",
"预期结果", "测试结果", "测试频次", "日志/截图/照片", "备注"
]
# AI配置
AI_MODEL = "claude-sonnet-4-6" # 或 "claude-opus-4-6"、"glm-4.7"
AI_MAX_TOKENS = 8192
AI_TEMPERATURE = 0.3 # 较低温度确保稳定性
# API密钥环境变量名
API_KEY_ENV = "ANTHROPIC_API_KEY"
# API基础URL(可选,用于代理服务)
API_BASE_URL = "https://api.anthropic.com"
# AI服务模式:auto(自动检测)、api(Anthropic API)、cli(Claude CLI)
AI_SERVICE_MODE = "auto"
# 覆盖率目标
COVERAGE_TARGET = 0.9 # 90%
# 日志配置
LOG_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
```
### 4.2 文档读取模块 (document_reader.py)
```python
# -*- coding: utf-8 -*-
"""
文档读取模块
"""
from pathlib import Path
from typing import List, Dict
from logging import getLogger
import docx
import openpyxl
logger = getLogger(__name__)
class DocumentReader:
"""文档读取器"""
def read_requirement_docs(self, requirements_dir: Path) -> List[Dict]:
"""
读取需求文档(.docx)
Returns:
需求文档列表,每项包含文件名、内容、功能点列表
"""
docs = []
for docx_file in requirements_dir.glob("*.docx"):
logger.info("读取需求文档: %s", docx_file.name)
doc = docx.Document(docx_file)
content = "\n".join([p.text for p in doc.paragraphs if p.text.strip()])
docs.append({
"filename": docx_file.name,
"path": str(docx_file),
"content": content
})
return docs
def read_prd_docs(self, prd_dir: Path) -> List[Dict]:
"""
读取开发PRD文档(.md)
Returns:
PRD文档列表,每项包含文件名、内容
"""
docs = []
for md_file in prd_dir.glob("*.md"):
logger.info("读取PRD文档: %s", md_file.name)
with open(md_file, "r", encoding="utf-8") as f:
content = f.read()
docs.append({
"filename": md_file.name,
"path": str(md_file),
"content": content
})
return docs
def read_test_cases(self, test_case_file: Path) -> List[Dict]:
"""
读取测试用例(.xlsx)
Returns:
测试用例列表
"""
logger.info("读取测试用例: %s", test_case_file.name)
wb = openpyxl.load_workbook(test_case_file)
ws = wb.active
cases = []
headers = [cell.value for cell in ws[1]]
for row in ws.iter_rows(min_row=2, values_only=True):
case = dict(zip(headers, row))
if case.get("用例名称"): # 跳过空行
cases.append(case)
logger.info("读取到 %d 条测试用例", len(cases))
return cases
```
### 4.3 测试用例分析模块 (test_case_analyzer.py)
```python
# -*- coding: utf-8 -*-
"""
测试用例分析模块
"""
from typing import List, Dict, Set
from logging import getLogger
import anthropic
from src.config import AI_MODEL, AI_MAX_TOKENS, AI_TEMPERATURE
logger = getLogger(__name__)
class TestCaseAnalyzer:
"""测试用例分析器"""
def __init__(self, api_key: str):
self.client = anthropic.Anthropic(api_key=api_key)
def extract_function_points(self, requirements: List[Dict],
prds: List[Dict]) -> List[Dict]:
"""
使用AI提取功能点列表
Args:
requirements: 需求文档列表
prds: PRD文档列表
Returns:
功能点列表,每项包含功能模块、功能编号、功能描述
"""
# 构建提示词
prompt = self._build_extract_prompt(requirements, prds)
response = self.client.messages.create(
model=AI_MODEL,
max_tokens=AI_MAX_TOKENS,
temperature=AI_TEMPERATURE,
messages=[
{
"role": "user",
"content": prompt
}
]
)
# 解析AI返回的功能点
result = response.content[0].text
function_points = self._parse_function_points(result)
logger.info("提取到 %d 个功能点", len(function_points))
return function_points
def _build_extract_prompt(self, requirements: List[Dict],
prds: List[Dict]) -> str:
"""构建功能点提取提示词"""
prompt = """请分析以下需求文档和PRD文档,提取完整的功能点列表。
【要求】
1. 按功能模块分类
2. 每个功能点包含:功能模块、功能编号、功能描述
3. 识别所有子功能
4. 输出格式为JSON数组
【需求文档】
"""
for req in requirements:
prompt += f"\n--- {req['filename']} ---\n{req['content'][:2000]}...\n"
prompt += "\n【开发PRD文档】\n"
for prd in prds:
prompt += f"\n--- {prd['filename']} ---\n{prd['content'][:2000]}...\n"
prompt += """
\n请输出JSON格式的功能点列表:
[
{
"功能模块": "模块名称",
"功能编号": "F-001",
"功能描述": "功能描述",
"子功能": ["子功能1", "子功能2"]
}
]
"""
return prompt
def _parse_function_points(self, result: str) -> List[Dict]:
"""解析AI返回的功能点"""
import json
try:
# 提取JSON部分
start = result.find("[")
end = result.rfind("]") + 1
json_str = result[start:end]
return json.loads(json_str)
except Exception as e:
logger.error("解析功能点失败: %s", e)
return []
def analyze_coverage(self, function_points: List[Dict],
test_cases: List[Dict]) -> Dict:
"""
分析测试用例覆盖情况
Returns:
覆盖率分析结果
"""
covered_functions = set()
for case in test_cases:
if case.get("功能描述"):
covered_functions.add(case["功能描述"])
total_functions = len(function_points)
covered_count = len(covered_functions)
coverage_rate = covered_count / total_functions if total_functions > 0 else 0
return {
"total_functions": total_functions,
"covered_functions": covered_count,
"coverage_rate": coverage_rate,
"uncovered_functions": total_functions - covered_count
}
```
### 4.4 缺失检测模块 (gap_detector.py)
```python
# -*- coding: utf-8 -*-
"""
缺失检测模块
"""
from typing import List, Dict
from logging import getLogger
import anthropic
from src.config import AI_MODEL, AI_MAX_TOKENS, AI_TEMPERATURE
logger = getLogger(__name__)
class GapDetector:
"""缺失检测器"""
def __init__(self, api_key: str):
self.client = anthropic.Anthropic(api_key=api_key)
def detect_gaps(self, function_points: List[Dict],
test_cases: List[Dict],
requirements: List[Dict],
prds: List[Dict]) -> Dict:
"""
检测测试用例缺失情况
Returns:
缺失检测结果
"""
logger.info("开始检测测试用例缺失...")
# 使用AI分析缺失情况
gaps = self._ai_detect_gaps(function_points, test_cases, requirements, prds)
return {
"功能点缺失": gaps.get("功能点缺失", []),
"场景覆盖不足": gaps.get("场景覆盖不足", []),
"边界条件不足": gaps.get("边界条件不足", [])
}
def _ai_detect_gaps(self, function_points: List[Dict],
test_cases: List[Dict],
requirements: List[Dict],
prds: List[Dict]) -> Dict:
"""使用AI检测缺失"""
prompt = self._build_gap_prompt(function_points, test_cases, requirements, prds)
response = self.client.messages.create(
model=AI_MODEL,
max_tokens=AI_MAX_TOKENS,
temperature=AI_TEMPERATURE,
messages=[{"role": "user", "content": prompt}]
)
result = response.content[0].text
return self._parse_gap_result(result)
def _build_gap_prompt(self, function_points: List[Dict],
test_cases: List[Dict],
requirements: List[Dict],
prds: List[Dict]) -> str:
"""构建缺失检测提示词"""
prompt = """请分析以下功能点、需求文档、PRD文档和现有测试用例,识别测试用例的缺失情况。
【功能点列表】
"""
prompt += str(function_points)
prompt += f"\n\n【现有测试用例】(共{len(test_cases)}条)\n"
for i, case in enumerate(test_cases[:20]): # 限制数量
prompt += f"{i+1}. {case.get('用例名称', '')} - {case.get('功能描述', '')}\n"
prompt += "\n请识别以下三类缺失,输出JSON格式:"
prompt += """
{
"功能点缺失": [
{
"功能模块": "模块名",
"功能编号": "F-001",
"功能描述": "缺失的功能描述",
"建议用例名称": "测试XXX功能"
}
],
"场景覆盖不足": [
{
"功能描述": "XXX功能",
"缺失场景": "异常场景:XXX",
"建议用例名称": "测试XXX异常场景"
}
],
"边界条件不足": [
{
"功能描述": "XXX功能",
"缺失边界": "边界值:XXX",
"建议用例名称": "测试XXX边界条件"
}
]
}
"""
return prompt
def _parse_gap_result(self, result: str) -> Dict:
"""解析AI返回的缺失结果"""
import json
try:
start = result.find("{")
end = result.rfind("}") + 1
json_str = result[start:end]
return json.loads(json_str)
except Exception as e:
logger.error("解析缺失结果失败: %s", e)
return {"功能点缺失": [], "场景覆盖不足": [], "边界条件不足": []}
```
### 4.5 用例生成模块 (case_generator.py)
```python
# -*- coding: utf-8 -*-
"""
用例生成模块
"""
from typing import List, Dict
from logging import getLogger
import anthropic
from src.config import AI_MODEL, AI_MAX_TOKENS, AI_TEMPERATURE, TEST_CASE_COLUMNS
logger = getLogger(__name__)
class CaseGenerator:
"""测试用例生成器"""
def __init__(self, api_key: str):
self.client = anthropic.Anthropic(api_key=api_key)
def generate_cases(self, gaps: Dict, requirements: List[Dict],
prds: List[Dict]) -> List[Dict]:
"""
生成缺失的测试用例
Returns:
新生成的测试用例列表
"""
logger.info("开始生成测试用例...")
new_cases = []
# 为功能点缺失生成用例
for gap in gaps.get("功能点缺失", []):
case = self._generate_case_for_function(gap, requirements, prds)
if case:
new_cases.append(case)
# 为场景覆盖不足生成用例
for gap in gaps.get("场景覆盖不足", []):
case = self._generate_case_for_scenario(gap, requirements, prds)
if case:
new_cases.append(case)
# 为边界条件不足生成用例
for gap in gaps.get("边界条件不足", []):
case = self._generate_case_for_boundary(gap, requirements, prds)
if case:
new_cases.append(case)
logger.info("生成 %d 条新测试用例", len(new_cases))
return new_cases
def _generate_case_for_function(self, gap: Dict, requirements: List[Dict],
prds: List[Dict]) -> Dict:
"""为功能点缺失生成测试用例"""
prompt = f"""请为以下缺失的功能点生成完整的测试用例。
【功能信息】
- 功能模块:{gap.get('功能模块')}
- 功能编号:{gap.get('功能编号')}
- 功能描述:{gap.get('功能描述')}
请生成测试用例,输出JSON格式(包含以下字段):
{json.dumps({col: "" for col in TEST_CASE_COLUMNS}, ensure_ascii=False)}
要求:
1. 用例编号格式:TC-模块-序号
2. 用例等级:P1/P2/P3
3. STEP要详细,包含操作步骤
4. 预期结果要明确
"""
response = self.client.messages.create(
model=AI_MODEL,
max_tokens=AI_MAX_TOKENS,
temperature=AI_TEMPERATURE,
messages=[{"role": "user", "content": prompt}]
)
result = response.content[0].text
return self._parse_case(result)
def _generate_case_for_scenario(self, gap: Dict, requirements: List[Dict],
prds: List[Dict]) -> Dict:
"""为场景覆盖不足生成测试用例"""
# 类似实现
pass
def _generate_case_for_boundary(self, gap: Dict, requirements: List[Dict],
prds: List[Dict]) -> Dict:
"""为边界条件不足生成测试用例"""
# 类似实现
pass
def _parse_case(self, result: str) -> Dict:
"""解析AI返回的测试用例"""
import json
try:
start = result.find("{")
end = result.rfind("}") + 1
json_str = result[start:end]
return json.loads(json_str)
except Exception as e:
logger.error("解析测试用例失败: %s", e)
return {}
```
### 4.6 去重模块 (deduplicator.py)
```python
# -*- coding: utf-8 -*-
"""
去重模块
"""
from typing import List, Dict
from logging import getLogger
logger = getLogger(__name__)
class Deduplicator:
"""测试用例去重器"""
def deduplicate(self, existing_cases: List[Dict],
new_cases: List[Dict]) -> Dict:
"""
去重处理
Returns:
去重结果,包含唯一用例列表、重复用例列表
"""
logger.info("开始去重处理...")
# 构建现有用例的指纹集合
existing_fingerprints = set()
for case in existing_cases:
fp = self._generate_fingerprint(case)
existing_fingerprints.add(fp)
# 筛选新用例
unique_cases = []
duplicate_cases = []
for case in new_cases:
fp = self._generate_fingerprint(case)
if fp in existing_fingerprints:
duplicate_cases.append(case)
else:
unique_cases.append(case)
existing_fingerprints.add(fp)
logger.info("去重完成:唯一 %d 条,重复 %d 条",
len(unique_cases), len(duplicate_cases))
return {
"unique_cases": unique_cases,
"duplicate_cases": duplicate_cases,
"duplicate_count": len(duplicate_cases)
}
def _generate_fingerprint(self, case: Dict) -> str:
"""生成用例指纹(用于去重)"""
# 使用功能描述+用例名称+预期结果生成唯一标识
key_parts = [
case.get("功能描述", ""),
case.get("用例名称", ""),
case.get("预期结果", "")
]
return "|".join(key_parts).strip().lower()
```
### 4.7 报告生成模块 (report_generator.py)
```python
# -*- coding: utf-8 -*-
"""
报告生成模块
"""
from pathlib import Path
from typing import List, Dict
from logging import getLogger
from datetime import datetime
from docx import Document
from docx.shared import Pt, RGBColor
logger = getLogger(__name__)
class ReportGenerator:
"""差异性报告生成器"""
def generate_report(self, gaps: Dict, dedup_result: Dict,
coverage: Dict, output_path: Path) -> None:
"""
生成差异性报告
"""
logger.info("开始生成差异性报告...")
doc = Document()
# 标题
title = doc.add_heading("测试用例完善差异性报告", 0)
title.alignment = 1 # 居中
# 生成时间
doc.add_paragraph(f"生成时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
# 1. 功能点缺失
self._add_missing_functions_section(doc, gaps.get("功能点缺失", []))
# 2. 场景覆盖不足
self._add_scenarios_section(doc, gaps.get("场景覆盖不足", []))
# 3. 边界条件不足
self._add_boundaries_section(doc, gaps.get("边界条件不足", []))
# 4. 去重处理结果
self._add_deduplication_section(doc, dedup_result)
# 5. 统计信息
self._add_statistics_section(doc, coverage, dedup_result)
# 保存
output_path.parent.mkdir(parents=True, exist_ok=True)
doc.save(str(output_path))
logger.info("报告生成完成: %s", output_path)
def _add_missing_functions_section(self, doc: Document, gaps: List[Dict]) -> None:
"""添加功能点缺失部分"""
doc.add_heading("一、功能点缺失的测试用例", level=1)
if not gaps:
doc.add_paragraph("无功能点缺失")
else:
for i, gap in enumerate(gaps, 1):
p = doc.add_paragraph()
p.add_run(f"{i}. ").bold = True
p.add_run(f"{gap.get('功能描述', '')}({gap.get('功能模块', '')})")
def _add_scenarios_section(self, doc: Document, gaps: List[Dict]) -> None:
"""添加场景覆盖不足部分"""
doc.add_heading("二、场景覆盖不足的测试用例", level=1)
if not gaps:
doc.add_paragraph("无场景覆盖不足")
else:
for i, gap in enumerate(gaps, 1):
p = doc.add_paragraph()
p.add_run(f"{i}. ").bold = True
p.add_run(f"{gap.get('功能描述', '')} - {gap.get('缺失场景', '')}")
def _add_boundaries_section(self, doc: Document, gaps: List[Dict]) -> None:
"""添加边界条件不足部分"""
doc.add_heading("三、边界条件不足的测试用例", level=1)
if not gaps:
doc.add_paragraph("无边界条件不足")
else:
for i, gap in enumerate(gaps, 1):
p = doc.add_paragraph()
p.add_run(f"{i}. ").bold = True
p.add_run(f"{gap.get('功能描述', '')} - {gap.get('缺失边界', '')}")
def _add_deduplication_section(self, doc: Document, dedup_result: Dict) -> None:
"""添加去重处理结果部分"""
doc.add_heading("四、去重处理结果", level=1)
doc.add_paragraph(f"新增用例中,去重后保留 {dedup_result.get('duplicate_count', 0)} 条唯一用例。")
def _add_statistics_section(self, doc: Document, coverage: Dict,
dedup_result: Dict) -> None:
"""添加统计信息部分"""
doc.add_heading("五、用例数量增长统计", level=1)
total_before = coverage.get("covered_functions", 0)
total_after = total_before + dedup_result.get("duplicate_count", 0)
# 表格
table = doc.add_table(rows=4, cols=2)
table.style = 'Light Grid Accent 1'
table.rows[0].cells[0].text = "统计项"
table.rows[0].cells[1].text = "数量"
table.rows[1].cells[0].text = "原有测试用例数"
table.rows[1].cells[1].text = str(total_before)
table.rows[2].cells[0].text = "新增测试用例数(去重后)"
table.rows[2].cells[1].text = str(dedup_result.get("duplicate_count", 0))
table.rows[3].cells[0].text = "完善后测试用例总数"
table.rows[3].cells[1].text = str(total_after)
# 覆盖率
doc.add_heading("六、覆盖率百分比统计", level=1)
coverage_rate = coverage.get("coverage_rate", 0) * 100
doc.add_paragraph(f"当前覆盖率:{coverage_rate:.1f}%")
doc.add_paragraph(f"目标覆盖率:90%")
if coverage_rate >= 90:
doc.add_paragraph("✅ 覆盖率达标!")
else:
doc.add_paragraph(f"⚠️ 距离目标还差 {90 - coverage_rate:.1f}%")
```
### 4.8 主入口模块 (main.py)
```python
# -*- coding: utf-8 -*-
"""
AI完善测试用例工具 - 主入口模块
"""
import os
import sys
import logging
from pathlib import Path
from src.config import *
from src.document_reader import DocumentReader
from src.test_case_analyzer import TestCaseAnalyzer
from src.gap_detector import GapDetector
from src.case_generator import CaseGenerator
from src.deduplicator import Deduplicator
from src.report_generator import ReportGenerator
# 配置日志
logging.basicConfig(
level=logging.INFO,
format=LOG_FORMAT,
datefmt=LOG_DATE_FORMAT,
handlers=[
logging.StreamHandler(sys.stdout),
logging.FileHandler(LOGS_DIR / "ai_test_case_enhancer.log", encoding="utf-8")
]
)
logger = logging.getLogger(__name__)
def main():
"""主函数"""
logger.info("=" * 60)
logger.info("AI完善测试用例工具 启动")
logger.info("=" * 60)
# 检查API密钥
api_key = os.environ.get("ANTHROPIC_API_KEY")
if not api_key:
logger.error("未设置ANTHROPIC_API_KEY环境变量")
logger.error("请设置:export ANTHROPIC_API_KEY=your-key-here")
return 1
# 确保目录存在
PERFECTED_DIR.mkdir(parents=True, exist_ok=True)
REPORTS_DIR.mkdir(parents=True, exist_ok=True)
LOGS_DIR.mkdir(parents=True, exist_ok=True)
try:
# 1. 读取文档
reader = DocumentReader()
requirements = reader.read_requirement_docs(REQUIREMENTS_DIR)
prds = reader.read_prd_docs(PRD_DIR)
test_cases = reader.read_test_cases(TEST_CASE_FILE)
logger.info("读取到需求文档 %d 份", len(requirements))
logger.info("读取到PRD文档 %d 份", len(prds))
logger.info("读取到测试用例 %d 条", len(test_cases))
# 2. 提取功能点
analyzer = TestCaseAnalyzer(api_key)
function_points = analyzer.extract_function_points(requirements, prds)
# 3. 分析覆盖率
coverage = analyzer.analyze_coverage(function_points, test_cases)
logger.info("当前覆盖率:%.1f%%", coverage["coverage_rate"] * 100)
# 4. 检测缺失
detector = GapDetector(api_key)
gaps = detector.detect_gaps(function_points, test_cases, requirements, prds)
logger.info("功能点缺失:%d 项", len(gaps.get("功能点缺失", [])))
logger.info("场景覆盖不足:%d 项", len(gaps.get("场景覆盖不足", [])))
logger.info("边界条件不足:%d 项", len(gaps.get("边界条件不足", [])))
# 5. 生成新用例
generator = CaseGenerator(api_key)
new_cases = generator.generate_cases(gaps, requirements, prds)
# 6. 去重
deduplicator = Deduplicator()
dedup_result = deduplicator.deduplicate(test_cases, new_cases)
# 7. 合并用例并保存
import openpyxl
wb = openpyxl.load_workbook(TEST_CASE_FILE)
ws = wb.active
# 追加新用例
for case in dedup_result["unique_cases"]:
ws.append([case.get(col, "") for col in TEST_CASE_COLUMNS])
wb.save(str(OUTPUT_TEST_CASE))
logger.info("完善后的测试用例已保存: %s", OUTPUT_TEST_CASE)
# 8. 生成报告
reporter = ReportGenerator()
reporter.generate_report(gaps, dedup_result, coverage, OUTPUT_REPORT)
logger.info("=" * 60)
logger.info("处理完成!")
logger.info("完善后的测试用例: %s", OUTPUT_TEST_CASE)
logger.info("差异性报告: %s", OUTPUT_REPORT)
logger.info("=" * 60)
return 0
except Exception as e:
logger.error("处理失败: %s", e)
import traceback
logger.debug(traceback.format_exc())
return 1
if __name__ == "__main__":
sys.exit(main())
```
---
## 5. 实施计划
### 5.1 实施步骤
| 序号 | 任务 | 预计时间 | 状态 |
|------|------|----------|------|
| 1 | 创建项目目录结构 | 0.5h | ⏳ 待开始 |
| 2 | 实现配置模块 (config.py) | 0.5h | ⏳ 待开始 |
| 3 | 实现文档读取模块 (document_reader.py) | 2h | ⏳ 待开始 |
| 4 | 实现测试用例分析模块 (test_case_analyzer.py) | 3h | ⏳ 待开始 |
| 5 | 实现缺失检测模块 (gap_detector.py) | 3h | ⏳ 待开始 |
| 6 | 实现用例生成模块 (case_generator.py) | 4h | ⏳ 待开始 |
| 7 | 实现去重模块 (deduplicator.py) | 1h | ⏳ 待开始 |
| 8 | 实现报告生成模块 (report_generator.py) | 2h | ⏳ 待开始 |
| 9 | 实现主入口模块 (main.py) | 1h | ⏳ 待开始 |
| 10 | 创建入口脚本 (run.py) | 0.5h | ⏳ 待开始 |
| 11 | 编写requirements.txt | 0.5h | ⏳ 待开始 |
| 12 | 编写README.md | 1h | ⏳ 待开始 |
| 13 | 单元测试 | 2h | ⏳ 待开始 |
| 14 | 集成测试 | 2h | ⏳ 待开始 |
| 15 | 文档完善 | 1h | ⏳ 待开始 |
**总计**:约 24 小时
### 5.2 依赖包
```
anthropic>=0.18.0
openpyxl>=3.1.0
python-docx>=1.1.0
pandas>=2.0.0
```
---
## 6. 测试验证
### 6.1 单元测试
- 测试文档读取功能
- 测试功能点提取功能
- 测试缺失检测功能
- 测试用例生成功能
- 测试去重功能
### 6.2 集成测试
- 端到端流程测试
- 使用实际文档测试
### 6.3 验收标准
| 验收项 | 标准 |
|--------|------|
| 功能点提取 | 能正确提取所有功能点 |
| 缺失检测 | 能识别功能点、场景、边界条件缺失 |
| 用例生成 | 生成符合格式的测试用例 |
| 去重 | 能正确识别并去除重复用例 |
| 报告生成 | 生成完整的差异性报告 |
| 覆盖率 | 最终覆盖率达到90% |
---
## 7. 注意事项
1. **API密钥安全**:ANTHROPIC_API_KEY 应通过环境变量设置,不要硬编码在代码中
2. **提示词优化**:实际使用中可能需要根据AI返回结果优化提示词
3. **成本控制**:大量API调用可能产生费用,建议先小规模测试
4. **人工审核**:AI生成的测试用例需要人工审核确认
---
## 8. 后续优化
- [ ] 支持自定义AI提示词模板
- [ ] 支持增量更新(只处理变更的部分)
- [ ] 支持多轮对话优化用例
- [ ] 支持导出为多种格式
- [ ] 添加用例质量评分
- [ ] 支持批量处理多个项目
# _PRD_根据PRD文档生成测试用例.md
> 版本:V1.1
> 更新日期:2026-01-28
> 适用范围:自动化测试用例生成工具
> 实现脚本:`AuxiliaryTool\用例生成\create_test_xlsx.py`
---
## 1. 背景与目标
### 1.1 背景
测试用例编写是测试工作的基础,但手工从 PRD 文档中提取需求并编写测试用例耗时耗力。现有项目 PRD 文档采用 Markdown 格式编写,具备结构化的编号和描述,适合自动化解析。
### 1.2 目标
实现一套自动化测试用例生成工具,具备:
- 自动从 PRD(Markdown)文档中抽取需求条目
- 基于规则引擎生成测试用例记录(JSON + Excel)
- 支持交互式选择多个 PRD 文档
- 生成的用例符合现有测试用例模板格式
---
## 2. 总体范围
### 2.1 纳入范围
1. **PRD 文档解析**
- 支持 Markdown 格式的 PRD 文档
- 自动识别编号结构(如 `1.1、2.3.1`
- 兼容 Markdown 标题语法(`# 标题`
2. **测试用例生成**
- 自动生成测试用例 JSON 文件
- 写入 Excel 测试用例模板
3. **交互式操作**
- 列出可选 PRD 文档
- 支持多选(如 `1,2,5``1 2 5`
- 自定义 Sheet 名称和输出文件名
### 2.2 不在本期范围
- PRD 文档的语法校验
- 测试用例的智能去重
- 复杂的测试数据生成
- 用例间的依赖关系处理
---
## 3. 术语说明
- **需求条目**:PRD 文档中带有编号的功能描述项
- **用例编号前缀**:测试用例 ID 的前缀(默认 `TC`
- **功能模块**:测试用例所属的模块名称
- **Sheet 复用**:多 PRD 选中时,用例写入同一个 Sheet(追加模式)
---
## 4. 功能需求
### 4.1 PRD 目录定位
#### 4.1.1 目录兼容
脚本需兼容以下两种 PRD 目录结构:
- `Docs/开发PRD`(PRD 文档中约定的路径)
- `Docs/PRD`(仓库中实际常见的路径)
#### 4.1.2 自动检测逻辑
- 按优先级检测目录是否存在
- 若均不存在,使用第一个路径并在后续报错提示
#### 4.1.3 相关路径
| 配置项 | 路径 |
| --- | --- |
| PRD 目录 | `Docs/开发PRD``Docs/PRD` |
| 模板文件 | `AuxiliaryTool/用例生成/用例文件/兰州中石化项目测试用例20251203.xlsx` |
| JSON 输出 | `AuxiliaryTool/用例生成/config/` |
| Excel 输出 | `AuxiliaryTool/用例生成/用例文件/` |
---
### 4.2 需求条目抽取规则
#### 4.2.1 编号识别
支持以下两种格式:
1. **点分编号**:正则 `^\s*(\d+(?:\.\d+)*)\s*[、\.\-]\s*(.+?)\s*[::]?\s*$`
- 示例:`1.1 用户登录``2.3.1-异常处理`
2. **Markdown 标题**:正则 `^\s*(#{1,6})\s+(.+?)\s*$`
- 示例:`## 用户登录``### 异常处理`
#### 4.2.2 内容抽取
- 标题后续行作为详情内容(直到下一个标题)
- **Markdown 标题嵌套处理**:如果当前已有标题,遇到新的 Markdown 标题时,该标题行会被作为详情内容追加,而非创建新条目
- 自动过滤:
- 代码块(```...```
- HTML 注释(<!--...-->
- 无效标题(说明、目录、背景、概述)
- 标题过滤条件:标题长度必须 ≥ 2 个字符
#### 4.2.3 清洗规则
- 移除列表符号(`-``*``+`
- 合并多余空格
- 保留文本核心语义
---
### 4.3 测试用例分类与优先级
#### 4.3.1 功能类别判定
| 类别 | 关键词 |
| --- | --- |
| 异常场景 | 异常、失败、错误、告警、报警、超时、断开、暴涨 |
| 安全/鉴权 | 安全、权限、鉴权、加密、脱敏 |
| 运维可观测 | 报告、输出、word、markdown、邮件、钉钉、通知 |
| 功能测试 | 其他(默认) |
#### 4.3.2 用例等级判定
| 等级 | 关键词 |
| --- | --- |
| 高 | 必须、报警、告警、峰值、暴涨、发送、对接 |
| 中 | 待实现、可以、建议、优化 |
| 中 | 其他(默认) |
#### 4.3.3 用例字段生成规则
| 字段 | 生成规则 |
| --- | --- |
| 序号 | 递增(多 PRD 时全局递增,跨 PRD 累加) |
| 用例编号 | `{前缀}-{序号:03d}`,如 `TC-001``TC-002` |
| 用例名称 | 需求标题 |
| 功能描述 | 需求标题 |
| 功能编号 | 抽取的编号(可能为空,适用于 Markdown 标题) |
| 预置条件 | 固定:`1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备` |
| 操作步骤 | 从详情中提取含"检查、监测、记录、输出、发送、查询、进入、生成、判定、调用、执行"的句子;最多提取 6 条,去重后以分号分隔;**在 Excel 中分号会转换为换行显示**;若无匹配内容,使用默认值:`1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果` |
| 预期结果 | 从详情中提取含"需要、应、必须、输出、记录、发送、判定、成功、失败"的句子;最多提取 6 条,去重后以分号分隔;若无匹配内容,使用默认值:`{标题} 按 PRD 约定产出正确结果(请补充具体断言点)` |
| JSON | **Excel 中强制清空(仅 JSON 文件保留)** |
| 测试结果 | 空(待人工填写) |
| 测试结论 | 空(待人工填写) |
| 日志截屏 | 空(待人工填写) |
| 备注 | 空(待人工填写) |
---
### 4.4 交互式选择
#### 4.4.1 PRD 列表展示
- 列出 PRD 目录下所有 `.md` 文件
- 按文件名排序显示
- 格式:`序号. 文件名`
#### 4.4.2 多选输入
- 支持格式:`1,2,5``1 2 5`
- 支持逗号或空格分隔
- 自动去重
- 输入验证:序号必须在范围内
#### 4.4.3 自定义名称
1. **Sheet 名称**
- 单 PRD 默认:`{PRD文件名}_用例`
- 多 PRD 默认:`多PRD_用例`
- 用户可覆盖输入
- **名称安全处理**
- 自动过滤 Excel 不允许的字符:`[ ] : * ? / \` 替换为 `_`
- 限制最大长度:**31 字符**(Excel Sheet 名称限制)
2. **输出文件名**
- 单 PRD 默认:`{PRD文件名}`
- 多 PRD 默认:`MultiPRD`
- 自动添加时间戳:`{名称}_{YYYYMMDD_HHMMSS}.xlsx`
---
### 4.5 Excel 输出
#### 4.5.1 表头顺序
序号、功能模块、功能类别、用例编号、功能描述、用例等级、功能编号、用例名称、预置条件、操作步骤、JSON、预期结果、测试结果、测试结论、日志截屏、备注
#### 4.5.2 Sheet 处理
- **表头查找**:在模板的前 30 行中,查找包含至少 2 个关键字(`序号`、`用例名称`、`操作步骤`、`预期结果`)的行作为表头行;若未找到,默认使用第 3 行
- 若 Sheet 不存在:创建新 Sheet,复制模板表头行和样式(字体、填充、边框、对齐、数字格式)
- 若 Sheet 已存在:追加数据(多 PRD 场景),从当前最大行下一行开始写入
- 冻结首行:`B2`
#### 4.5.3 样式设置
- **自动换行**:预置条件、操作步骤列
- **对齐方式**:顶部对齐
- **列宽**:根据内容自适应,最大 60
#### 4.5.4 JSON 输出
- 每个 PRD 单独生成 JSON 文件
- 文件名:`{PRD文件名}_用例.json`
- 存放目录:`AuxiliaryTool/用例生成/config/`
---
## 5. 配置与可变项
### 5.1 命令行参数
脚本支持通过命令行参数覆盖默认配置:
| 参数 | 默认值 | 说明 |
| --- | --- | --- |
| `--template` | `兰州中石化项目测试用例20251203.xlsx` | 模板 Excel 文件路径 |
| `--module` | `通用模块` | 功能模块字段值(多 PRD 时统一使用该值) |
| `--prefix` | `TC` | 用例编号前缀(多 PRD 时统一使用该值) |
| `--overwrite` | `false` | 是否覆盖保存到模板文件(默认另存为新文件) |
### 5.2 可变配置项
| 配置项 | 默认值 | 说明 |
| --- | --- | --- |
| PRD 目录 | `Docs/开发PRD` 或 `Docs/PRD` | 自动检测,优先使用存在的目录 |
| 模板文件 | `兰州中石化项目测试用例20251203.xlsx` | 位于 `AuxiliaryTool/用例生成/用例文件/` |
| JSON 输出目录 | `config/` | 位于 `AuxiliaryTool/用例生成/` |
| Excel 输出目录 | `用例文件/` | 位于 `AuxiliaryTool/用例生成/` |
---
## 6. 异常处理与容错要求
1. **PRD 目录为空或不存在**:明确报错提示路径
2. **模板文件不存在**:提示文件路径并退出
3. **无效输入**:提示重试,不退出
4. **无可用需求条目**:记录日志,跳过该 PRD
5. **写入失败**:明确提示错误原因
---
## 7. 交付物
1. Python 脚本:
- `AuxiliaryTool/用例生成/create_test_xlsx.py`
2. 配置输出:
- `AuxiliaryTool/用例生成/config/{PRD名}_用例.json`
3. 用例输出:
- `AuxiliaryTool/用例生成/用例文件/{名称}_{时间戳}.xlsx`
---
## 8. 验收标准
1. 能正确解析 PRD 文档中的编号结构
2. 生成的用例分类和优先级符合规则
3. Excel 输出格式与模板一致
4. 支持多 PRD 合并到同一 Sheet
5. 所有操作有明确日志输出
6. 异常情况有友好提示
---
## 9. 使用示例
### 9.1 基本使用(交互式)
```bash
# 执行脚本
python AuxiliaryTool/用例生成/create_test_xlsx.py
# 交互流程
PRD目录:C:\PycharmData\ubains-module-test\Docs\PRD
1. _PRD_预定系统后端服务监测需求文档.md
2. _PRD_其他需求文档.md
支持多选:输入多个序号(如 1,2,5 或 1 2 5)。
请输入PRD序号(可多选):1
请输入Sheet名称(回车使用默认:_PRD_预定系统后端服务监测需求文档_用例):
请输入生成的测试用例文件名称(回车使用默认:_PRD_预定系统后端服务监测需求文档):
# 输出
已生成 JSON:AuxiliaryTool/用例生成/config/_PRD_预定系统后端服务监测需求文档_用例.json(15条)
已生成Excel:AuxiliaryTool/用例生成/用例文件/_PRD_预定系统后端服务监测需求文档_20260128_150000.xlsx
Sheet:_PRD_预定系统后端服务监测需求文档_用例
总用例条数:15
```
### 9.2 使用命令行参数
```bash
# 指定功能模块和用例前缀
python AuxiliaryTool/用例生成/create_test_xlsx.py --module "会议系统" --prefix "MT"
# 使用自定义模板文件
python AuxiliaryTool/用例生成/create_test_xlsx.py --template "自定义模板.xlsx"
# 覆盖模式(直接覆盖模板文件,不另存)
python AuxiliaryTool/用例生成/create_test_xlsx.py --overwrite
# 组合使用
python AuxiliaryTool/用例生成/create_test_xlsx.py --module "会议系统" --prefix "MT" --template "自定义模板.xlsx"
```
### 9.3 多 PRD 合并示例
```bash
# 选择多个 PRD
请输入PRD序号(可多选):1 2 3
请输入Sheet名称(回车使用默认:多PRD_用例):
请输入生成的测试用例文件名称(回车使用默认:MultiPRD):
# 输出:三个 PRD 的用例合并在同一个 Sheet 中,序号全局递增
已生成 JSON:AuxiliaryTool/用例生成/config/_PRD_预定系统后端服务监测需求文档_用例.json(15条)
已生成 JSON:AuxiliaryTool/用例生成/config/_PRD_其他需求文档_用例.json(8条)
已生成 JSON:AuxiliaryTool/用例生成/config/_PRD_第三个文档_用例.json(12条)
已生成Excel:AuxiliaryTool/用例生成/用例文件/MultiPRD_20260128_150000.xlsx
Sheet:多PRD_用例
总用例条数:35
```
---
## 需求规范
代码规范:Docs/PRD/01规范文档/_PRD_规范文档_代码规范.md
问题总结:Docs/PRD/01规范文档/_PRD_问题总结_记录文档.md
方法总结:Docs/PRD/01规范文档/_PRD_方法总结_记录文档.md
文档规范:Docs/PRD/01规范文档/_PRD_规范文档_文档规范.md
测试规范:Docs/PRD/01规范文档/_PRD_规范文档_测试规范.md
# _PRD_根据PRD文档生成测试用例_计划执行.md
> 版本:V1.0
> 创建日期:2026-01-28
> 关联需求:`_PRD_根据PRD文档生成测试用例.md`
> 目标:基于现有脚本实现,输出完整的开发计划与技术规范
---
## 1. 开发概述
### 1.1 脚本基本信息
| 项目 | 内容 |
|------|------|
| 脚本路径 | `AuxiliaryTool/用例生成/create_test_xlsx.py` |
| 编程语言 | Python 3.x |
| 核心依赖 | `openpyxl`(Excel 操作) |
| 入口函数 | `main()` |
### 1.2 功能模块划分
```
create_test_xlsx.py
├── 1) 路径与常量定义
├── 2) PRD 解析与用例生成
│ ├── RequirementItem 数据类
│ ├── normalize_md() - Markdown 预处理
│ ├── extract_requirement_items() - 需求条目抽取
│ ├── classify_category() - 功能类别判定
│ ├── decide_priority() - 用例等级判定
│ ├── build_steps() - 操作步骤生成
│ ├── build_expected() - 预期结果生成
│ └── prd_to_cases() - PRD 转用例
├── 3) Excel 写入
│ ├── find_header_row() - 表头查找
│ ├── safe_sheet_name() - Sheet 名称安全处理
│ └── write_cases_to_sheet() - 写入 Sheet
└── 4) 交互式操作
├── pick_prds_interactively() - PRD 选择
├── ask_sheet_name_interactively() - Sheet 名称输入
└── ask_output_name_interactively() - 输出文件名输入
```
---
## 2. 开发任务清单
### 2.1 阶段一:基础框架(优先级:高)
#### 任务 1.1:路径与常量定义
**目标**:定义所有路径常量和全局配置
**实现要点**
```python
# 路径计算
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
REPO_ROOT = os.path.abspath(os.path.join(BASE_DIR, "..", ".."))
# PRD 目录候选(兼容两种路径)
PRD_DIR_CANDIDATES = [
os.path.join(REPO_ROOT, "Docs", "开发PRD"),
os.path.join(REPO_ROOT, "Docs", "PRD"),
]
def resolve_prd_dir() -> str:
"""按优先级检测 PRD 目录,返回存在的路径"""
for d in PRD_DIR_CANDIDATES:
if os.path.isdir(d):
return d
return PRD_DIR_CANDIDATES[0] # 默认第一个
```
**表头顺序常量**
```python
headers_order = [
"序号", "功能模块", "功能类别", "用例编号", "功能描述", "用例等级",
"功能编号", "用例名称", "预置条件", "操作步骤", "JSON", "预期结果",
"测试结果", "测试结论", "日志截屏", "备注",
]
```
**验收标准**
- 路径计算正确,跨平台兼容
- `resolve_prd_dir()` 能正确检测存在的目录
---
#### 任务 1.2:命令行参数解析
**目标**:支持命令行参数覆盖默认配置
**实现要点**
```python
import argparse
parser = argparse.ArgumentParser(description="根据PRD生成用例JSON,并写入测试用例Excel")
parser.add_argument("--template", default=TEMPLATE_PATH_DEFAULT, help="模板Excel路径")
parser.add_argument("--module", default="通用模块", help="功能模块字段值")
parser.add_argument("--prefix", default="TC", help="用例编号前缀")
parser.add_argument("--overwrite", action="store_true", help="是否覆盖保存")
args = parser.parse_args()
```
**验收标准**
- `--help` 能正确显示帮助信息
- 各参数能正确覆盖默认值
---
### 2.2 阶段二:PRD 解析(优先级:高)
#### 任务 2.1:数据类定义
**目标**:定义需求条目数据结构
**实现要点**
```python
from dataclasses import dataclass
from typing import List
@dataclass
class RequirementItem:
code: str # 编号(如 "1.1")
title: str # 标题
detail_lines: List[str] # 详情内容行列表
```
---
#### 任务 2.2:正则表达式定义
**目标**:定义编号识别的正则表达式
**实现要点**
```python
# 点分编号:1.1、2.3.1 等
RE_SECTION = re.compile(r"^\s*(?P<code>\d+(?:\.\d+)*)\s*[、\.\-]\s*(?P<title>.+?)\s*[::]?\s*$")
# Markdown 标题:# ## ### 等
RE_MD_HEADING = re.compile(r"^\s*(#{1,6})\s+(?P<title>.+?)\s*$")
```
**测试用例**
| 输入 | 匹配结果 | code | title |
|------|----------|------|-------|
| `1.1 用户登录` | ✓ | `1.1` | `用户登录` |
| `2.3.1-异常处理` | ✓ | `2.3.1` | `异常处理` |
| `## 功能描述` | ✓ | - | `功能描述` |
| `### 接口定义` | ✓ | - | `接口定义` |
---
#### 任务 2.3:Markdown 预处理
**目标**:过滤代码块和 HTML 注释
**实现要点**
```python
def normalize_md(text: str) -> str:
# 统一换行符
text = text.replace("\r\n", "\n").replace("\r", "\n")
# 移除代码块
text = re.sub(r"```.*?```", "", text, flags=re.S)
# 移除 HTML 注释
text = re.sub(r"<!--.*?-->", "", text, flags=re.S)
return text
```
**验收标准**
- 代码块被正确移除
- HTML 注释被正确移除
- 换行符统一为 `\n`
---
#### 任务 2.4:需求条目抽取
**目标**:从 Markdown 文本中抽取需求条目
**核心逻辑**
```python
def extract_requirement_items(md_text: str) -> List[RequirementItem]:
lines = md_text.split("\n")
items: List[RequirementItem] = []
current_code = ""
current_title = ""
current_detail: List[str] = []
def flush():
nonlocal current_code, current_title, current_detail
if current_title:
detail = [clean_line(x) for x in current_detail if clean_line(x)]
items.append(RequirementItem(
code=current_code,
title=clean_line(current_title),
detail_lines=detail
))
current_code = ""
current_title = ""
current_detail = []
for raw in lines:
line = raw.rstrip("\n")
# 匹配点分编号
m = RE_SECTION.match(line)
if m:
flush()
current_code = m.group("code").strip()
current_title = m.group("title").strip()
continue
# 匹配 Markdown 标题
mh = RE_MD_HEADING.match(line)
if mh:
if current_title:
# 已有标题,嵌套标题作为详情
current_detail.append(line)
else:
# 新标题
flush()
current_code = ""
current_title = mh.group("title").strip()
continue
# 详情行
if current_title:
current_detail.append(line)
flush()
# 过滤:标题长度 >= 2
items = [it for it in items if it.title and len(it.title) >= 2]
return items
```
**关键点**
1. 遇到点分编号时,`flush()` 当前条目,开始新条目
2. 遇到 Markdown 标题时:
- 如果已有标题 → 作为详情追加(嵌套处理)
- 如果无标题 → `flush()` 并创建新条目
3. 最后必须 `flush()` 一次
4. 过滤标题长度 < 2 的条目
---
#### 任务 2.5:行清洗
**目标**:清洗单行文本
**实现要点**
```python
def clean_line(line: str) -> str:
line = line.strip()
# 移除列表符号
line = re.sub(r"^[\-\*\+]\s+", "", line)
# 合并多余空格
line = re.sub(r"\s+", " ", line)
return line
```
---
### 2.3 阶段三:用例生成规则(优先级:高)
#### 任务 3.1:功能类别判定
**目标**:根据关键词判定功能类别
**实现要点**
```python
def classify_category(title: str, detail_lines: List[str]) -> str:
text = (title + " " + " ".join(detail_lines)).lower()
if any(k in text for k in ["异常", "失败", "错误", "告警", "报警", "超时", "断开", "暴涨"]):
return "异常场景"
if any(k in text for k in ["安全", "权限", "鉴权", "加密", "脱敏"]):
return "安全/鉴权"
if any(k in text for k in ["报告", "输出", "word", "markdown", "邮件", "钉钉", "通知"]):
return "运维可观测"
return "功能测试"
```
**判定顺序**:异常场景 → 安全/鉴权 → 运维可观测 → 功能测试(默认)
---
#### 任务 3.2:用例等级判定
**目标**:根据关键词判定用例等级
**实现要点**
```python
def decide_priority(title: str, detail_lines: List[str]) -> str:
text = title + " " + " ".join(detail_lines)
if any(k in text for k in ["必须", "报警", "告警", "峰值", "暴涨", "发送", "对接"]):
return "高"
if any(k in text for k in ["待实现", "可以", "建议", "优化"]):
return "中"
return "中" # 默认
```
**判定顺序**:高 → 中(待实现) → 中(默认)
---
#### 任务 3.3:操作步骤生成
**目标**:从详情中提取操作步骤
**实现要点**
```python
def build_steps(detail_lines: List[str]) -> str:
keywords = ["检查", "监测", "记录", "输出", "发送", "查询", "进入", "生成", "判定", "调用", "执行"]
candidates: List[str] = []
for ln in detail_lines:
s = clean_line(ln)
if not s:
continue
if any(k in s for k in keywords):
candidates.append(s)
# 去重
uniq: List[str] = []
for c in candidates:
if c not in uniq:
uniq.append(c)
if not uniq:
return "1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果"
uniq = uniq[:6] # 最多 6 条
return "; ".join([f"{i+1}. {x}" for i, x in enumerate(uniq)])
```
**关键点**
- 关键词列表必须完整
- 去重逻辑保证不重复
- 最多 6 条
- 默认值必须准确
---
#### 任务 3.4:预期结果生成
**目标**:从详情中提取预期结果
**实现要点**
```python
def build_expected(title: str, detail_lines: List[str]) -> str:
keywords = ["需要", "应", "必须", "输出", "记录", "发送", "判定", "成功", "失败"]
candidates: List[str] = []
for ln in detail_lines:
s = clean_line(ln)
if not s:
continue
if any(k in s for k in keywords):
candidates.append(s)
# 去重
uniq: List[str] = []
for c in candidates:
if c not in uniq:
uniq.append(c)
if not uniq:
return f"{title} 按 PRD 约定产出正确结果(请补充具体断言点)"
uniq = uniq[:6] # 最多 6 条
return "; ".join(uniq)
```
---
#### 任务 3.5:用例编号生成
**目标**:生成格式化的用例编号
**实现要点**
```python
def make_case_id(prefix: str, idx: int) -> str:
return f"{prefix}-{idx:03d}" # TC-001, TC-002, ...
```
---
#### 任务 3.6:用例记录生成
**目标**:将需求条目转换为用例记录
**实现要点**
```python
def to_case_record(idx: int, module: str, prefix: str, req: RequirementItem) -> Dict[str, str]:
record: Dict[str, str] = {
"序号": idx,
"功能模块": module,
"功能类别": classify_category(req.title, req.detail_lines),
"用例编号": make_case_id(prefix, idx),
"功能描述": req.title,
"用例等级": decide_priority(req.title, req.detail_lines),
"功能编号": req.code or "",
"用例名称": req.title,
"预置条件": "1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备",
"操作步骤": build_steps(req.detail_lines),
"JSON": "",
"预期结果": build_expected(req.title, req.detail_lines),
"测试结果": "",
"测试结论": "",
"日志截屏": "",
"备注": "",
}
# 确保所有字段存在
for k in headers_order:
record.setdefault(k, "")
return record
```
---
#### 任务 3.7:PRD 转用例(主函数)
**目标**:读取 PRD 文件并转换为用例列表
**实现要点**
```python
def prd_to_cases(prd_path: str, module: str, prefix: str, start_idx: int = 1) -> List[Dict[str, str]]:
with open(prd_path, "r", encoding="utf-8") as f:
md = normalize_md(f.read())
items = extract_requirement_items(md)
cases: List[Dict[str, str]] = []
idx = start_idx
for it in items:
# 过滤无效标题
if any(k in it.title for k in ["说明", "目录", "背景", "概述"]):
continue
cases.append(to_case_record(idx=idx, module=module, prefix=prefix, req=it))
idx += 1
return cases
```
**关键点**
- `start_idx` 支持多 PRD 全局递增
- 过滤"说明、目录、背景、概述"等无效标题
---
### 2.4 阶段四:Excel 写入(优先级:高)
#### 任务 4.1:表头查找
**目标**:在模板中查找表头行
**实现要点**
```python
def find_header_row(template_sheet) -> int:
must_keys = {"序号", "用例名称", "操作步骤", "预期结果"}
max_scan = min(30, template_sheet.max_row or 30)
for r in range(1, max_scan + 1):
values = []
for c in range(1, min(40, template_sheet.max_column or 40) + 1):
v = template_sheet.cell(row=r, column=c).value
if v is None:
continue
values.append(str(v).strip())
# 至少匹配 2 个关键字
if len(must_keys.intersection(values)) >= 2:
return r
return 3 # 默认第 3 行
```
**关键点**
- 扫描前 30 行
- 匹配至少 2 个关键字
- 默认返回 3
---
#### 任务 4.2:Sheet 名称安全处理
**目标**:确保 Sheet 名称符合 Excel 规范
**实现要点**
```python
def safe_sheet_name(name: str) -> str:
# Excel 不允许的字符: [ ] : * ? / \
name = re.sub(r"[\[\]\:\*\?\/\\]", "_", name)
return name[:31] # 最大 31 字符
```
**Excel Sheet 名称限制**
- 最大长度:31 字符
- 不允许字符:`[ ] : * ? / \`
---
#### 任务 4.3:写入 Sheet
**目标**:将用例写入 Excel Sheet
**实现要点**:
```python
from copy import copy
from openpyxl.styles import Alignment
def write_cases_to_sheet(wb, template_sheet, sheet_name: str, cases: List[Dict[str, str]]):
# Sheet 存在检查
if sheet_name in wb.sheetnames:
ws = wb[sheet_name]
start_row = (ws.max_row or 1) + 1 # 追加模式
else:
ws = wb.create_sheet(sheet_name)
start_row = 2
# 复制表头
header_row_index = find_header_row(template_sheet)
for col_idx, cell in enumerate(template_sheet[header_row_index], start=1):
new_cell = ws.cell(row=1, column=col_idx, value=cell.value)
if cell.has_style:
new_cell.font = copy(cell.font)
new_cell.fill = copy(cell.fill)
new_cell.border = copy(cell.border)
new_cell.alignment = copy(cell.alignment)
new_cell.number_format = cell.number_format
ws.freeze_panes = "B2" # 冻结首行
# 写入数据
row = start_row
for case in cases:
for col_idx, header in enumerate(headers_order, start=1):
val = case.get(header, "")
# 分号转换行(预置条件、操作步骤)
if header in ("预置条件", "操作步骤") and isinstance(val, str):
val = val.replace("; ", "\n")
# JSON 强制清空
if header == "JSON":
val = ""
ws.cell(row=row, column=col_idx, value=val)
row += 1
# 自动换行(预置条件、操作步骤)
col_idx_pre = headers_order.index("预置条件") + 1
col_idx_steps = headers_order.index("操作步骤") + 1
for r in range(1, row):
ws.cell(row=r, column=col_idx_pre).alignment = Alignment(wrap_text=True, vertical="top")
ws.cell(row=r, column=col_idx_steps).alignment = Alignment(wrap_text=True, vertical="top")
# 列宽自适应
for col in ws.columns:
max_len = 0
col_letter = col[0].column_letter
for c in col:
v = c.value
if v is None:
continue
l = len(str(v))
if l > max_len:
max_len = l
ws.column_dimensions[col_letter].width = min(max_len + 2, 60)
return ws
```
**关键点**:
1. Sheet 复用:存在则追加,不存在则创建
2. 表头样式复制:font、fill、border、alignment、number_format
3. 分号转换行:预置条件、操作步骤
4. JSON 强制清空
5. 自动换行:预置条件、操作步骤列
6. 列宽最大 60
---
### 2.5 阶段五:交互式操作(优先级:中)
#### 任务 5.1:列出 PRD 文件
**目标**:列出 PRD 目录下的所有 .md 文件
**实现要点**:
```python
def list_prd_files(prd_dir: str) -> List[str]:
if not os.path.isdir(prd_dir):
return []
fs = [f for f in os.listdir(prd_dir) if f.lower().endswith(".md")]
fs.sort()
return fs
```
---
#### 任务 5.2:解析多选输入
**目标**:解析用户输入的序号
**实现要点**:
```python
def parse_multi_input(s: str) -> List[str]:
s = (s or "").strip()
if not s:
return []
parts = re.split(r"[,\s]+", s) # 逗号或空格分隔
return [p for p in (x.strip() for x in parts) if p]
```
**支持格式**:`1,2,5` 或 `1 2 5` 或 `1, 2, 5`
---
#### 任务 5.3:交互式选择 PRD
**目标**:引导用户选择 PRD 文件
**实现要点**:
```python
def pick_prds_interactively(prd_dir: str) -> List[str]:
files = list_prd_files(prd_dir)
if not files:
raise RuntimeError(f"PRD目录为空或不存在:{prd_dir}")
print(f"PRD目录:{prd_dir}")
for i, f in enumerate(files, start=1):
print(f"{i:>2}. {f}")
print("支持多选:输入多个序号(如 1,2,5 或 1 2 5)。")
while True:
s = input("请输入PRD序号(可多选):").strip()
parts = parse_multi_input(s)
if not parts or not all(p.isdigit() for p in parts):
print("输入无效,请输入序号(可多选)。")
continue
idxs: List[int] = []
ok = True
for p in parts:
n = int(p)
if not (1 <= n <= len(files)):
ok = False
break
idxs.append(n)
if not ok:
print("序号超出范围,请重试。")
continue
# 去重并构建路径
seen = set()
paths: List[str] = []
for n in idxs:
if n in seen:
continue
seen.add(n)
paths.append(os.path.join(prd_dir, files[n - 1]))
return paths
```
**关键点**:
1. 空目录检测
2. 输入验证(数字、范围)
3. 自动去重
---
#### 任务 5.4:输入 Sheet 名称
**目标**:获取 Sheet 名称并安全处理
**实现要点**:
```python
def ask_sheet_name_interactively(default_name: str) -> str:
s = input(f"请输入Sheet名称(回车使用默认:{default_name}):").strip()
return safe_sheet_name(s or default_name)
```
---
#### 任务 5.5:输入输出文件名
**目标**:获取输出文件名
**实现要点**:
```python
def ask_output_name_interactively(default_name: str) -> str:
s = input(f"请输入生成的测试用例文件名称(回车使用默认:{default_name}):").strip()
return s or default_name
```
---
### 2.6 阶段六:主流程集成(优先级:高)
#### 任务 6.1:主函数
**目标**:整合所有模块,实现完整流程
**实现要点**:
```python
from datetime import datetime
def main():
# 1. 解析命令行参数
parser = argparse.ArgumentParser(description="根据PRD生成用例JSON,并写入测试用例Excel")
parser.add_argument("--template", default=TEMPLATE_PATH_DEFAULT, help="模板Excel路径")
parser.add_argument("--module", default="通用模块", help="功能模块字段值")
parser.add_argument("--prefix", default="TC", help="用例编号前缀")
parser.add_argument("--overwrite", action="store_true", help="是否覆盖保存")
args = parser.parse_args()
# 2. 定位 PRD 目录
prd_dir = resolve_prd_dir()
# 3. 检查模板文件
if not os.path.exists(args.template):
print("找不到模板文件:", args.template)
return
# 4. 交互式选择 PRD
prd_paths = pick_prds_interactively(prd_dir)
# 5. 确定 Sheet 名称
if len(prd_paths) == 1:
default_sheet = f"{os.path.splitext(os.path.basename(prd_paths[0]))[0]}_用例"
else:
default_sheet = "多PRD_用例"
sheet_name = ask_sheet_name_interactively(default_sheet)
# 6. 确定输出文件名
if len(prd_paths) == 1:
default_out_base = os.path.splitext(os.path.basename(prd_paths[0]))[0]
else:
default_out_base = "MultiPRD"
out_base = ask_output_name_interactively(default_out_base)
# 7. 打开模板
wb = load_workbook(args.template)
template_sheet = wb.worksheets[0]
# 8. 多 PRD 合并到同一个 sheet(序号全局递增)
os.makedirs(OUTPUT_JSON_DIR, exist_ok=True)
total_cases = 0
next_idx = 1
for prd_path in prd_paths:
prd_base = os.path.splitext(os.path.basename(prd_path))[0]
# 生成用例
cases = prd_to_cases(prd_path=prd_path, module=args.module, prefix=args.prefix, start_idx=next_idx)
if not cases:
print(f"未从PRD抽取到可生成用例的条目:{prd_path}")
continue
# 输出 JSON
json_path = os.path.join(OUTPUT_JSON_DIR, f"{prd_base}_用例.json")
with open(json_path, "w", encoding="utf-8") as f:
json.dump(cases, f, ensure_ascii=False, indent=4)
print(f"已生成 JSON:{json_path}({len(cases)}条)")
# 写入 Sheet
write_cases_to_sheet(wb, template_sheet, sheet_name, cases)
total_cases += len(cases)
next_idx += len(cases)
# 9. 保存 Excel
if args.overwrite:
out_xlsx = args.template
else:
os.makedirs(OUTPUT_XLSX_DIR, exist_ok=True)
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
out_xlsx = os.path.join(OUTPUT_XLSX_DIR, f"{out_base}_{ts}.xlsx")
wb.save(out_xlsx)
print("已生成Excel:", out_xlsx)
print("Sheet:", sheet_name)
print("总用例条数:", total_cases)
if __name__ == "__main__":
main()
```
---
## 3. 测试验证
### 3.1 单元测试
| 模块 | 测试项 | 预期结果 |
|------|--------|----------|
| `resolve_prd_dir()` | 目录存在检测 | 返回存在的目录路径 |
| `normalize_md()` | 代码块过滤 | ````...```` 被移除 |
| `normalize_md()` | HTML 注释过滤 | `<!--...-->` 被移除 |
| `extract_requirement_items()` | 点分编号解析 | 正确提取 code 和 title |
| `extract_requirement_items()` | Markdown 标题解析 | 正确提取 title |
| `extract_requirement_items()` | 嵌套标题处理 | 嵌套标题作为详情 |
| `classify_category()` | 异常场景 | 包含"异常"返回"异常场景" |
| `classify_category()` | 安全/鉴权 | 包含"安全"返回"安全/鉴权" |
| `decide_priority()` | 高等级 | 包含"必须"返回"高" |
| `build_steps()` | 无匹配内容 | 返回默认值 |
| `build_steps()` | 有匹配内容 | 返回分号分隔的步骤 |
| `build_expected()` | 无匹配内容 | 返回默认值 |
| `safe_sheet_name()` | 非法字符过滤 | `[ ] : * ? / \` 替换为 `_` |
| `safe_sheet_name()` | 长度限制 | 超过 31 字符截断 |
| `find_header_row()` | 表头查找 | 返回正确行号 |
### 3.2 集成测试
| 场景 | 操作 | 预期结果 |
|------|------|----------|
| 单 PRD 生成 | 选择 1 个 PRD | 生成 JSON 和 Excel |
| 多 PRD 合并 | 选择多个 PRD | 合并在同一 Sheet,序号递增 |
| Sheet 复用 | 重复生成到同一 Sheet | 数据追加而非覆盖 |
| 覆盖模式 | 使用 `--overwrite` | 覆盖模板文件 |
| 命令行参数 | 使用 `--module``--prefix` | 参数生效 |
### 3.3 边界测试
| 场景 | 操作 | 预期结果 |
|------|------|----------|
| 空 PRD 目录 | 无 .md 文件 | 报错提示 |
| 模板文件不存在 | 指定不存在的模板 | 报错提示 |
| 无效输入 | 输入非数字序号 | 提示重试 |
| 序号超范围 | 输入超出范围的序号 | 提示重试 |
| 无可用条目 | PRD 无有效需求 | 跳过该 PRD |
---
## 4. 依赖关系
### 4.1 模块依赖图
```
main()
├── argparse (命令行解析)
├── resolve_prd_dir() (目录定位)
├── pick_prds_interactively()
│ └── list_prd_files()
│ └── parse_multi_input()
├── ask_sheet_name_interactively()
│ └── safe_sheet_name()
├── ask_output_name_interactively()
├── load_workbook() (openpyxl)
├── prd_to_cases()
│ ├── normalize_md()
│ ├── extract_requirement_items()
│ │ ├── clean_line()
│ │ └── RequirementItem
│ └── to_case_record()
│ ├── classify_category()
│ ├── decide_priority()
│ ├── build_steps()
│ ├── build_expected()
│ └── make_case_id()
├── write_cases_to_sheet()
│ ├── find_header_row()
│ └── Alignment() (openpyxl)
└── wb.save() (openpyxl)
```
### 4.2 开发顺序建议
1. **第一批**:基础框架
- 路径与常量定义
- 命令行参数解析
- 数据类定义
2. **第二批**:PRD 解析
- 正则表达式定义
- Markdown 预处理
- 需求条目抽取
- 行清洗
3. **第三批**:用例生成
- 功能类别判定
- 用例等级判定
- 操作步骤生成
- 预期结果生成
- 用例编号生成
- 用例记录生成
- PRD 转用例主函数
4. **第四批**:Excel 写入
- 表头查找
- Sheet 名称安全处理
- 写入 Sheet
5. **第五批**:交互式操作
- 列出 PRD 文件
- 解析多选输入
- 交互式选择 PRD
- 输入处理函数
6. **第六批**:主流程集成
- 主函数
---
## 5. 关键常量与配置
### 5.1 正则表达式
| 变量 | 表达式 | 用途 |
|------|--------|------|
| `RE_SECTION` | `^\s*(\d+(?:\.\d+)*)\s*[、\.\-]\s*(.+?)\s*[::]?\s*$` | 匹配点分编号 |
| `RE_MD_HEADING` | `^\s*(#{1,6})\s+(.+?)\s*$` | 匹配 Markdown 标题 |
### 5.2 关键词列表
| 用途 | 关键词 |
|------|--------|
| 异常场景 | `["异常", "失败", "错误", "告警", "报警", "超时", "断开", "暴涨"]` |
| 安全/鉴权 | `["安全", "权限", "鉴权", "加密", "脱敏"]` |
| 运维可观测 | `["报告", "输出", "word", "markdown", "邮件", "钉钉", "通知"]` |
| 高等级 | `["必须", "报警", "告警", "峰值", "暴涨", "发送", "对接"]` |
| 中等级 | `["待实现", "可以", "建议", "优化"]` |
| 操作步骤 | `["检查", "监测", "记录", "输出", "发送", "查询", "进入", "生成", "判定", "调用", "执行"]` |
| 预期结果 | `["需要", "应", "必须", "输出", "记录", "发送", "判定", "成功", "失败"]` |
| 过滤标题 | `["说明", "目录", "背景", "概述"]` |
### 5.3 默认值
| 配置项 | 默认值 |
|--------|--------|
| 模板文件 | `兰州中石化项目测试用例20251203.xlsx` |
| 功能模块 | `通用模块` |
| 用例编号前缀 | `TC` |
| 预置条件 | `1. 环境已部署; 2. 具备执行权限; 3. 必要依赖/配置已准备` |
| 操作步骤默认值 | `1. 按 PRD 描述执行该功能/流程; 2. 采集相关日志/输出; 3. 记录实际结果` |
| 预期结果默认值 | `{标题} 按 PRD 约定产出正确结果(请补充具体断言点)` |
| Sheet 单 PRD 默认 | `{PRD文件名}_用例` |
| Sheet 多 PRD 默认 | `多PRD_用例` |
| 输出文件单 PRD 默认 | `{PRD文件名}` |
| 输出文件多 PRD 默认 | `MultiPRD` |
### 5.4 限制值
| 配置项 | 限制值 |
|--------|--------|
| Sheet 名称最大长度 | 31 字符 |
| 表头扫描最大行数 | 30 行 |
| 表头扫描最大列数 | 40 列 |
| 操作步骤最大条数 | 6 条 |
| 预期结果最大条数 | 6 条 |
| 列宽最大值 | 60 |
| 标题最小长度 | 2 字符 |
---
## 6. 验收清单
### 6.1 功能验收
- [ ] 能正确解析点分编号格式(如 `1.1``2.3.1`
- [ ] 能正确解析 Markdown 标题格式(如 `## 标题`
- [ ] 能处理 Markdown 标题嵌套场景
- [ ] 功能类别判定符合规则
- [ ] 用例等级判定符合规则
- [ ] 操作步骤生成符合规则(分号分隔、最多 6 条)
- [ ] 预期结果生成符合规则(分号分隔、最多 6 条)
- [ ] 支持单 PRD 生成
- [ ] 支持多 PRD 合并(序号全局递增)
- [ ] Sheet 复用功能正常(追加模式)
- [ ] JSON 文件正确生成
- [ ] Excel 文件正确生成
- [ ] 分号在 Excel 中转换为换行
- [ ] JSON 列在 Excel 中强制清空
- [ ] Sheet 名称非法字符被过滤
- [ ] Sheet 名称长度限制为 31 字符
- [ ] 命令行参数生效
- [ ] 覆盖模式正常工作
### 6.2 异常验收
- [ ] PRD 目录为空时正确报错
- [ ] 模板文件不存在时正确报错
- [ ] 无效输入时提示重试
- [ ] 序号超范围时提示重试
- [ ] 无可用需求条目时跳过该 PRD
- [ ] 写入失败时有明确提示
---
## 7. 参考资料
### 7.1 需求文档
- `_PRD_根据PRD文档生成测试用例.md`
### 7.2 现有实现
- `AuxiliaryTool/用例生成/create_test_xlsx.py`
### 7.3 依赖库
- `openpyxl`:Excel 文件操作
- Python 标准库:`os`, `re`, `json`, `argparse`, `datetime`, `dataclasses`, `typing`, `copy`
### 7.4 需求规范
- 代码规范:`Docs/PRD/01规范文档/_PRD_规范文档_代码规范.md`
- 问题总结:`Docs/PRD/01规范文档/_PRD_问题总结_记录文档.md`
- 方法总结:`Docs/PRD/01规范文档/_PRD_方法总结_记录文档.md`
- 文档规范:`Docs/PRD/01规范文档/_PRD_规范文档_文档规范.md`
- 测试规范:`Docs/PRD/01规范文档/_PRD_规范文档_测试规范.md`
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论