Files
test_item_gen/app.py
2026-02-04 14:38:52 +08:00

674 lines
27 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# @line_count 500
"""测试专家系统 - Streamlit Web应用"""
import streamlit as st
import json
import os
from pathlib import Path
from typing import List, Dict, Any, Optional
import yaml
# 导入自定义模块
from modules.json_parser import JSONParser
from modules.api_client import APIClient
from modules.prompt_manager import PromptManager
from modules.test_generator import TestGenerator
from modules.output_formatter import OutputFormatter
from process_doc_file import convert_docx_to_json
# 页面配置
st.set_page_config(
page_title="测试专家系统",
page_icon="🧪",
layout="wide",
initial_sidebar_state="expanded"
)
def init_session_state():
"""初始化会话状态"""
if 'api_client' not in st.session_state:
st.session_state.api_client = None
if 'prompt_manager' not in st.session_state:
st.session_state.prompt_manager = None
if 'json_parser' not in st.session_state:
st.session_state.json_parser = None
if 'test_items' not in st.session_state:
st.session_state.test_items = []
if 'test_cases' not in st.session_state:
st.session_state.test_cases = []
if 'function_points' not in st.session_state:
st.session_state.function_points = []
if 'document_info' not in st.session_state:
st.session_state.document_info = {}
if 'standard_manager' not in st.session_state:
st.session_state.standard_manager = None
# 用户自定义Prompt支持
if 'custom_prompts' not in st.session_state:
st.session_state.custom_prompts = {} # {func_point_index: custom_prompt}
if 'custom_prompt_template' not in st.session_state:
st.session_state.custom_prompt_template = None # 批量模板
if 'use_custom_prompt' not in st.session_state:
st.session_state.use_custom_prompt = False # 是否启用自定义prompt
def load_config():
"""加载配置"""
try:
if st.session_state.api_client is None:
st.session_state.api_client = APIClient()
if st.session_state.prompt_manager is None:
st.session_state.prompt_manager = PromptManager()
except Exception as e:
st.error(f"配置加载失败: {str(e)}")
def show_home_page():
"""显示首页"""
st.title("🧪 测试专家系统")
st.markdown("---")
st.markdown("""
### 欢迎使用测试专家系统
本系统基于大模型AI技术能够自动从软件使用说明文档中提取功能点并生成全面的测试项和测试用例。
#### 主要功能
1. **文档解析**: 自动解析JSON格式的软件使用说明书提取功能点和操作步骤
2. **智能生成**: 基于大模型API为每个功能点生成专业的测试项和测试用例
3. **多格式输出**: 支持JSON、Markdown、Excel多种格式导出
4. **Prompt优化**: 支持自定义和优化测试生成Prompt模板
#### 使用流程
1. **配置API**: 在"配置"页面设置大模型API密钥和选择模型
2. **上传文档**: 在"生成测试"页面上传JSON格式的文档
3. **生成测试**: 选择功能点,点击生成按钮
4. **下载结果**: 预览生成结果,下载所需格式的文件
#### 支持的API提供商
- DeepSeek (默认)
- 通义千问
- OpenAI/ChatGPT
---
**开始使用**: 请先前往"配置"页面设置API密钥
""")
def show_config_page():
"""显示配置页面"""
st.title("⚙️ 系统配置")
st.markdown("---")
load_config()
# API配置
st.header("📡 API配置")
api_client = st.session_state.api_client
providers = api_client.config.get('providers', {})
current_provider = api_client.current_provider
# 选择API提供商
provider_options = list(providers.keys())
selected_provider = st.selectbox(
"选择API提供商",
provider_options,
index=provider_options.index(current_provider) if current_provider in provider_options else 0
)
if selected_provider != current_provider:
api_client.set_provider(selected_provider)
st.success(f"已切换到: {selected_provider}")
# 配置API密钥
provider_config = providers[selected_provider]
st.subheader(f"{selected_provider.upper()} 配置")
api_key = st.text_input(
"API密钥",
value=provider_config.get('api_key', '') or os.getenv(f'{selected_provider.upper()}_API_KEY', ''),
type="password",
help="输入API密钥或通过环境变量设置"
)
if st.button("保存API密钥"):
try:
api_client.update_api_key(selected_provider, api_key)
st.success("API密钥已保存")
except Exception as e:
st.error(f"保存失败: {str(e)}")
# 显示其他配置信息
with st.expander("查看配置详情"):
st.json(provider_config)
# 提示信息
st.info("💡 提示Prompt和测试规范配置将在上传文档后进行设置")
def show_generate_page():
"""显示生成页面"""
st.title("🚀 生成测试")
st.markdown("---")
load_config()
# 上传文档(支持 JSON 和 Word
st.header("📄 上传文档")
uploaded_file = st.file_uploader(
"选择文档文件(支持 JSON 或 Word",
type=['json', 'docx'],
help="可以直接上传Word使用说明书.docx系统会自动转换为JSON也可以上传已转换好的JSON文件"
)
if uploaded_file is not None:
try:
# 保存上传的文件
upload_dir = Path("uploads")
upload_dir.mkdir(exist_ok=True)
file_path = upload_dir / uploaded_file.name
with open(file_path, 'wb') as f:
f.write(uploaded_file.getbuffer())
# 如果是Word文档先转换为JSON
if file_path.suffix.lower() == ".docx":
json_path = upload_dir / f"{file_path.stem}.json"
convert_docx_to_json(str(file_path), str(json_path))
parser_target_path = json_path
else:
# 直接使用上传的JSON文件
parser_target_path = file_path
# 解析JSON
parser = JSONParser(str(parser_target_path))
st.session_state.json_parser = parser
# 只有在上传新文档时才清空自定义prompts检查路径是否变化
current_doc_path = str(parser_target_path)
previous_doc_path = st.session_state.get('last_uploaded_doc_path', '')
if current_doc_path != previous_doc_path:
# 新文档清空之前的自定义prompts
st.session_state.custom_prompts.clear()
st.session_state.custom_prompt_template = None
st.session_state.use_custom_prompt = False
st.session_state.last_uploaded_doc_path = current_doc_path
# 获取文档信息
doc_info = parser.get_document_info()
st.session_state.document_info = doc_info
# 显示文档信息
st.success("文档上传成功!")
col1, col2, col3 = st.columns(3)
with col1:
st.metric("文档标题", doc_info.get('title', 'N/A'))
with col2:
st.metric("章节数量", doc_info.get('section_count', 0))
with col3:
st.metric("版本", doc_info.get('version', 'N/A'))
# 提取功能点
function_points = parser.extract_function_points()
st.session_state.function_points = function_points
# 显示功能点
if function_points:
# ========== 新增Prompt/测试规范配置区域 ==========
st.markdown("---")
st.header("📝 Prompt/测试规范配置")
# 1. 需求类型统计
requirement_types = {}
for fp in function_points:
req_type = fp.get('requirement_type', '未知')
requirement_types[req_type] = requirement_types.get(req_type, 0) + 1
if requirement_types:
st.subheader("需求类型统计")
cols = st.columns(len(requirement_types))
for idx, (req_type, count) in enumerate(requirement_types.items()):
with cols[idx]:
st.metric(req_type, count)
# 2. Prompt策略选择
use_standards = st.checkbox(
"使用测试规范(推荐)",
value=True,
help="启用后,系统会根据需求类型自动选择适用的测试规范,生成符合行业标准的测试用例"
)
# 3. 测试规范选择详情(如果启用)
if use_standards:
try:
from modules.test_standard_manager import TestStandardManager
# 初始化测试规范管理器
if st.session_state.standard_manager is None:
st.session_state.standard_manager = TestStandardManager(
api_client=st.session_state.api_client
)
standard_manager = st.session_state.standard_manager
# 显示测试规范选择详情
with st.expander("📋 查看测试规范选择详情"):
for fp in function_points:
requirement = _convert_to_requirement(fp)
standards = standard_manager.get_applicable_standards(
requirement,
use_ai=False # 先不使用AI避免额外调用
)
req_type = requirement.get('requirement_type', '未知')
req_id = requirement.get('requirement_id', fp.get('function_name', ''))
st.markdown(f"**{req_id}** ({req_type})")
if standards:
st.write(f"选择的测试规范: {', '.join(standards)}")
else:
st.write("⚠️ 未找到适用的测试规范")
st.markdown("---")
# 4. Prompt预览与编辑
preview_prompt = st.checkbox("预览规范化Prompt")
if preview_prompt and function_points:
sample_fp = function_points[0]
requirement = _convert_to_requirement(sample_fp)
standards = standard_manager.get_applicable_standards(requirement, use_ai=False)
if standards:
# 生成默认prompt
default_prompt = standard_manager.build_prompt(requirement, standards)
# 获取当前索引(第一个功能点)
sample_idx = 0
# 如果没有自定义prompt使用默认值
if sample_idx not in st.session_state.custom_prompts:
st.session_state.custom_prompts[sample_idx] = default_prompt
# 可编辑的Prompt文本框
edited_prompt = st.text_area(
"Prompt预览可编辑以第一个功能点为例",
value=st.session_state.custom_prompts[sample_idx],
height=400,
key="prompt_editor_preview",
help="您可以直接修改Prompt内容修改后的内容将在生成测试时使用"
)
# 更新session state
st.session_state.custom_prompts[sample_idx] = edited_prompt
# 操作按钮
col1, col2, col3 = st.columns(3)
with col1:
if st.button("🔄 恢复默认", key="restore_default_prompt"):
st.session_state.custom_prompts[sample_idx] = default_prompt
st.session_state.use_custom_prompt = False
st.success("已恢复为默认Prompt")
st.rerun()
with col2:
if st.button("📋 应用到所有功能点", key="apply_to_all"):
# 将当前编辑的prompt应用到所有功能点
for idx in range(len(function_points)):
st.session_state.custom_prompts[idx] = edited_prompt
st.session_state.use_custom_prompt = True
st.success(f"✅ 已应用到 {len(function_points)} 个功能点")
with col3:
if st.button("🗑️ 清空所有自定义", key="clear_all_custom"):
st.session_state.custom_prompts.clear()
st.session_state.use_custom_prompt = False
st.success("已清空所有自定义Prompt")
st.rerun()
# 显示状态信息
if st.session_state.custom_prompts:
custom_count = len(st.session_state.custom_prompts)
st.info(f" 当前有 {custom_count} 个功能点使用自定义Prompt")
else:
st.warning("无法生成Prompt预览未找到适用的测试规范")
except Exception as e:
st.warning(f"测试规范功能加载失败: {str(e)}将使用传统Prompt模式")
use_standards = False
# 5. 更新PromptManager配置
if st.session_state.prompt_manager.use_standards != use_standards:
st.session_state.prompt_manager.use_standards = use_standards
if use_standards and st.session_state.standard_manager:
st.session_state.prompt_manager.standard_manager = st.session_state.standard_manager
# ========== 原有功能点选择部分 ==========
st.markdown("---")
st.header("📋 功能点列表")
# 选择功能点
selected_indices = st.multiselect(
"选择要生成测试的功能点(留空则选择全部)",
options=list(range(len(function_points))),
format_func=lambda x: f"{function_points[x]['module_name']} - {function_points[x]['function_name']}"
)
if not selected_indices:
selected_function_points = function_points
else:
selected_function_points = [function_points[i] for i in selected_indices]
# 显示功能点详情
with st.expander("查看功能点详情"):
for idx, fp in enumerate(selected_function_points):
st.markdown(f"### {idx + 1}. {fp['function_name']}")
st.markdown(f"**模块**: {fp['module_name']}")
st.markdown(f"**描述**: {fp.get('description', 'N/A')}")
if fp.get('operation_steps'):
st.markdown("**操作步骤**:")
for step in fp['operation_steps']:
st.markdown(f"- {step}")
st.markdown("---")
# 生成选项
st.markdown("---")
st.header("⚙️ 生成选项")
generation_mode = st.radio(
"生成模式",
['batch', 'separate'],
format_func=lambda x: {
'batch': '批量生成(一次性生成测试项和测试用例,更快)',
'separate': '分步生成(先生成测试项,再生成测试用例,更灵活)'
}[x]
)
# 开始生成
if st.button("🚀 开始生成测试", type="primary", use_container_width=True):
if not st.session_state.api_client.get_provider_config().get('api_key'):
st.error("请先在配置页面设置API密钥")
else:
generate_tests(selected_function_points, generation_mode)
else:
st.warning("未能从文档中提取到功能点,请检查文档格式。")
except Exception as e:
st.error(f"处理文件失败: {str(e)}")
st.exception(e)
else:
st.info("👆 请上传JSON格式的文档文件")
def generate_tests(function_points: List[Dict[str, Any]], mode: str):
"""生成测试项和测试用例"""
api_client = st.session_state.api_client
prompt_manager = st.session_state.prompt_manager
parser = st.session_state.json_parser
if parser is None:
st.error("请先上传文档")
return
# 先清空之前的自定义prompts
prompt_manager.clear_custom_prompts()
# 注入用户自定义的prompts到PromptManager
all_function_points = st.session_state.function_points
if st.session_state.custom_prompts:
st.info(f"📝 开始注入 {len(st.session_state.custom_prompts)} 个自定义Prompt")
for idx, fp in enumerate(all_function_points):
if idx in st.session_state.custom_prompts:
# 获取功能点ID与TestGenerator._convert_to_requirement保持一致
func_id = fp.get('requirement_id', fp.get('function_name', ''))
custom_prompt = st.session_state.custom_prompts[idx]
# 调试信息
st.write(f" - 索引 {idx}: {fp.get('function_name', 'N/A')}")
st.write(f" - requirement_id: `{fp.get('requirement_id', 'N/A')}`")
st.write(f" - 映射到的ID: `{func_id}`")
st.write(f" - Prompt长度: {len(custom_prompt)} 字符")
prompt_manager.set_custom_prompt(func_id, custom_prompt)
st.success(f"✅ 已加载 {len(st.session_state.custom_prompts)} 个自定义Prompt到缓存")
# 显示缓存内容
with st.expander("🔍 查看Prompt缓存详情"):
st.write("当前PromptManager缓存的自定义Prompt:")
for key, value in prompt_manager.custom_prompts_cache.items():
st.write(f"- **{key}**: {len(value)} 字符")
# 创建生成器
generator = TestGenerator(
str(parser.json_path),
api_client=api_client,
prompt_manager=prompt_manager
)
progress_bar = st.progress(0)
status_text = st.empty()
try:
if mode == 'batch':
# 批量生成
status_text.text("开始批量生成...")
result = generator.generate_batch(
function_points=function_points,
progress_callback=lambda current, total, msg: (
progress_bar.progress(current / total),
status_text.text(f"进度: {current}/{total} - {msg}")
)
)
st.session_state.test_items = result['test_items']
st.session_state.test_cases = result['test_cases']
else:
# 分步生成
status_text.text("第一步:生成测试项...")
test_items = generator.generate_test_items(
function_points=function_points,
progress_callback=lambda current, total, msg: (
progress_bar.progress(current / (total * 2)),
status_text.text(f"生成测试项: {current}/{total} - {msg}")
)
)
st.session_state.test_items = test_items
status_text.text("第二步:生成测试用例...")
test_cases = generator.generate_test_cases(
progress_callback=lambda current, total, msg: (
progress_bar.progress(0.5 + current / (total * 2)),
status_text.text(f"生成测试用例: {current}/{total} - {msg}")
)
)
st.session_state.test_cases = test_cases
progress_bar.progress(1.0)
status_text.text("生成完成!")
st.success(f"✅ 成功生成 {len(st.session_state.test_items)} 个测试项和 {len(st.session_state.test_cases)} 个测试用例")
# 跳转到结果页面
st.session_state.current_page = 'results'
st.rerun()
except Exception as e:
st.error(f"生成失败: {str(e)}")
st.exception(e)
progress_bar.empty()
status_text.empty()
def _convert_to_requirement(func_point: Dict[str, Any]) -> Dict[str, Any]:
"""
将功能点转换为需求格式(用于测试规范选择)
Args:
func_point: 功能点字典
Returns:
需求字典
"""
requirement = {
'requirement_id': func_point.get('requirement_id', func_point.get('function_name', '')),
'requirement_type': func_point.get('requirement_type', '功能需求'),
'description': func_point.get('description', func_point.get('function_name', '')),
'module_name': func_point.get('module_name', '')
}
# 如果有接口信息,添加进去
if func_point.get('interface_info'):
requirement['interface_info'] = func_point['interface_info']
return requirement
def show_results_page():
"""显示结果页面"""
st.title("📊 生成结果")
st.markdown("---")
test_items = st.session_state.test_items
test_cases = st.session_state.test_cases
document_info = st.session_state.document_info
if not test_items and not test_cases:
st.warning('还没有生成测试项和测试用例,请先前往"生成测试"页面')
return
# 统计信息
col1, col2, col3, col4 = st.columns(4)
with col1:
st.metric("测试项数量", len(test_items))
with col2:
st.metric("测试用例数量", len(test_cases))
with col3:
modules = set(item.get('module_name', '') for item in test_items)
st.metric("涉及模块", len(modules))
with col4:
priorities = {}
for case in test_cases:
p = case.get('priority', '')
priorities[p] = priorities.get(p, 0) + 1
high_priority = priorities.get('', 0)
st.metric("高优先级用例", high_priority)
st.markdown("---")
# 预览
st.header("📋 预览")
preview_format = st.selectbox("选择预览格式", ['JSON', 'Markdown'])
if preview_format == 'JSON':
formatter = OutputFormatter(test_items, test_cases, document_info)
json_str = formatter.to_json()
st.json(json.loads(json_str))
else:
formatter = OutputFormatter(test_items, test_cases, document_info)
markdown_str = formatter.to_markdown()
st.markdown(markdown_str)
st.markdown("---")
# 下载
st.header("💾 下载结果")
col1, col2, col3 = st.columns(3)
formatter = OutputFormatter(test_items, test_cases, document_info)
with col1:
if st.button("下载JSON格式", use_container_width=True):
output_path = "outputs/test_results.json"
Path("outputs").mkdir(exist_ok=True)
formatter.to_json(output_path)
with open(output_path, 'rb') as f:
st.download_button(
"⬇️ 下载JSON",
f.read(),
"test_results.json",
"application/json",
key="download_json"
)
with col2:
if st.button("下载Markdown格式", use_container_width=True):
output_path = "outputs/test_results.md"
Path("outputs").mkdir(exist_ok=True)
formatter.to_markdown(output_path)
with open(output_path, 'rb') as f:
st.download_button(
"⬇️ 下载Markdown",
f.read(),
"test_results.md",
"text/markdown",
key="download_md"
)
with col3:
if st.button("下载Excel格式", use_container_width=True):
try:
output_path = "outputs/test_results.xlsx"
Path("outputs").mkdir(exist_ok=True)
formatter.to_excel(output_path)
with open(output_path, 'rb') as f:
st.download_button(
"⬇️ 下载Excel",
f.read(),
"test_results.xlsx",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
key="download_excel"
)
except ImportError:
st.error("需要安装openpyxl: pip install openpyxl")
def main():
"""主函数"""
init_session_state()
# 侧边栏导航
with st.sidebar:
st.title("🧪 测试专家系统")
st.markdown("---")
page = st.radio(
"导航",
['home', 'config', 'generate', 'results'],
format_func=lambda x: {
'home': '🏠 首页',
'config': '⚙️ 配置',
'generate': '🚀 生成测试',
'results': '📊 结果'
}[x]
)
st.markdown("---")
st.markdown("### 使用提示")
st.info("""
1. 首次使用请先配置API密钥
2. 上传JSON格式的文档
3. 选择功能点并生成测试
4. 下载生成的结果
""")
# 路由到对应页面
if page == 'home':
show_home_page()
elif page == 'config':
show_config_page()
elif page == 'generate':
show_generate_page()
elif page == 'results':
show_results_page()
if __name__ == "__main__":
main()