219 lines
10 KiB
Python
219 lines
10 KiB
Python
import os
|
||
import sys
|
||
import json
|
||
import logging
|
||
import argparse
|
||
import traceback # 用于更详细的错误日志
|
||
from pathlib import Path
|
||
from flask import Flask, request, jsonify, send_from_directory
|
||
from flask_cors import CORS # 用于处理跨域请求
|
||
|
||
# 将ddms_compliance_suite的父目录添加到sys.path
|
||
# 假设flask_app.py与ddms_compliance_suite目录在同一级别,或者ddms_compliance_suite在其PYTHONPATH中
|
||
# 如果 ddms_compliance_suite 是一个已安装的包,则不需要这个
|
||
# current_dir = os.path.dirname(os.path.abspath(__file__))
|
||
# project_root = os.path.dirname(current_dir) # 假设项目根目录是上一级
|
||
# sys.path.insert(0, project_root)
|
||
# 或者更具体地添加包含ddms_compliance_suite的目录
|
||
# sys.path.insert(0, os.path.join(project_root, 'ddms_compliance_suite'))
|
||
|
||
from ddms_compliance_suite.test_orchestrator import APITestOrchestrator, TestSummary
|
||
from ddms_compliance_suite.input_parser.parser import InputParser, ParsedYAPISpec, ParsedSwaggerSpec
|
||
# 从 run_api_tests.py 导入辅助函数 (如果它们被重构为可导入的)
|
||
# 为了简单起见,我们可能会直接在 flask_app.py 中重新实现一些逻辑或直接调用Orchestrator
|
||
|
||
app = Flask(__name__, static_folder='static', static_url_path='')
|
||
CORS(app) # 允许所有来源的跨域请求,生产环境中应配置更严格的规则
|
||
|
||
# 配置日志
|
||
logging.basicConfig(
|
||
level=logging.INFO,
|
||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||
)
|
||
logger = logging.getLogger(__name__)
|
||
|
||
# --- 辅助函数 ---
|
||
def get_orchestrator_from_config(config: dict) -> APITestOrchestrator:
|
||
"""根据配置字典实例化APITestOrchestrator"""
|
||
return APITestOrchestrator(
|
||
base_url=config.get('base_url', ''),
|
||
custom_test_cases_dir=config.get('custom_test_cases_dir'),
|
||
llm_api_key=config.get('llm_api_key'),
|
||
llm_base_url=config.get('llm_base_url'),
|
||
llm_model_name=config.get('llm_model_name'),
|
||
use_llm_for_request_body=config.get('use_llm_for_request_body', False),
|
||
use_llm_for_path_params=config.get('use_llm_for_path_params', False),
|
||
use_llm_for_query_params=config.get('use_llm_for_query_params', False),
|
||
use_llm_for_headers=config.get('use_llm_for_headers', False),
|
||
output_dir=config.get('output_dir') # 虽然Orchestrator内部可能不直接用它保存,但可以传入
|
||
)
|
||
|
||
# --- API 端点 ---
|
||
@app.route('/')
|
||
def serve_index():
|
||
return send_from_directory(app.static_folder, 'index.html')
|
||
|
||
@app.route('/run-tests', methods=['POST'])
|
||
def run_tests_endpoint():
|
||
try:
|
||
config_data = request.json
|
||
if not config_data:
|
||
return jsonify({"error": "Request body must be JSON"}), 400
|
||
|
||
logger.info(f"接收到测试运行请求: {config_data}")
|
||
|
||
# 校验必需参数
|
||
if not config_data.get('base_url'):
|
||
return jsonify({"error": "'base_url' is required"}), 400
|
||
if not config_data.get('yapi_file_path') and not config_data.get('swagger_file_path'):
|
||
return jsonify({"error": "Either 'yapi_file_path' or 'swagger_file_path' is required"}), 400
|
||
|
||
orchestrator = get_orchestrator_from_config(config_data)
|
||
summary = TestSummary() # 为本次运行创建新的摘要
|
||
|
||
parsed_spec = None
|
||
api_spec_type = ""
|
||
|
||
if config_data.get('yapi_file_path'):
|
||
api_spec_type = "YAPI"
|
||
yapi_path = config_data['yapi_file_path']
|
||
if not os.path.isabs(yapi_path):
|
||
yapi_path = os.path.join(os.getcwd(), yapi_path) # 假设相对路径相对于服务器工作目录
|
||
if not os.path.exists(yapi_path):
|
||
return jsonify({"error": f"YAPI file not found: {yapi_path}"}), 400
|
||
logger.info(f"解析YAPI文件: {yapi_path}")
|
||
parsed_spec = orchestrator.parser.parse_yapi_spec(yapi_path)
|
||
if not parsed_spec:
|
||
logger.error(f"解析YAPI文件失败: {yapi_path}")
|
||
return jsonify({"error": f"Failed to parse YAPI file: {yapi_path}"}), 500
|
||
|
||
elif config_data.get('swagger_file_path'):
|
||
api_spec_type = "Swagger/OpenAPI"
|
||
swagger_path = config_data['swagger_file_path']
|
||
if not os.path.isabs(swagger_path):
|
||
swagger_path = os.path.join(os.getcwd(), swagger_path) # 假设相对路径
|
||
if not os.path.exists(swagger_path):
|
||
return jsonify({"error": f"Swagger file not found: {swagger_path}"}), 400
|
||
logger.info(f"解析Swagger/OpenAPI文件: {swagger_path}")
|
||
parsed_spec = orchestrator.parser.parse_swagger_spec(swagger_path)
|
||
if not parsed_spec:
|
||
logger.error(f"解析Swagger文件失败: {swagger_path}")
|
||
return jsonify({"error": f"Failed to parse Swagger file: {swagger_path}"}), 500
|
||
|
||
# 执行测试用例
|
||
logger.info(f"开始从已解析的 {api_spec_type} 规范执行测试用例...")
|
||
summary = orchestrator._execute_tests_from_parsed_spec(
|
||
parsed_spec=parsed_spec,
|
||
summary=summary,
|
||
categories=config_data.get('categories'), # 逗号分隔的字符串,需要转为列表
|
||
tags=config_data.get('tags'), # 同上
|
||
custom_test_cases_dir=config_data.get('custom_test_cases_dir')
|
||
)
|
||
logger.info("测试用例执行完成。")
|
||
|
||
# 执行场景测试 (如果指定了目录)
|
||
scenarios_dir = config_data.get('scenarios_dir')
|
||
if scenarios_dir and parsed_spec:
|
||
if not os.path.isabs(scenarios_dir):
|
||
scenarios_dir = os.path.join(os.getcwd(), scenarios_dir)
|
||
logger.info(f"开始执行API场景测试,目录: {scenarios_dir}")
|
||
orchestrator.run_scenarios_from_spec(
|
||
scenarios_dir=scenarios_dir,
|
||
parsed_spec=parsed_spec,
|
||
summary=summary
|
||
)
|
||
logger.info("API场景测试执行完毕。")
|
||
|
||
summary.finalize_summary() # 最终确定摘要,计算总时长等
|
||
# summary.print_summary_to_console() # 后端服务通常不直接打印到控制台
|
||
|
||
# 可以在这里决定如何保存报告,例如保存到 output_dir (如果提供)
|
||
output_dir_path_str = config_data.get('output_dir')
|
||
main_report_file_path_str = ""
|
||
api_calls_output_path_str = ""
|
||
api_calls_filename = "api_call_details.md"
|
||
|
||
if output_dir_path_str:
|
||
output_path = Path(output_dir_path_str)
|
||
output_path.mkdir(parents=True, exist_ok=True)
|
||
main_report_file_path = output_path / f"summary_report.json" # 默认保存为json
|
||
main_report_file_path_str = str(main_report_file_path)
|
||
with open(main_report_file_path, 'w', encoding='utf-8') as f:
|
||
f.write(summary.to_json(pretty=True))
|
||
logger.info(f"主测试报告已保存到: {main_report_file_path}")
|
||
|
||
# 保存API调用详情
|
||
api_calls_output_path_str = str(output_path)
|
||
# (需要从 run_api_tests.py 移植 save_api_call_details_to_file 或类似功能)
|
||
# 暂时跳过保存 api_call_details 文件,因为 orchestrator.get_api_call_details() 需要被调用
|
||
# 并且保存逻辑也需要移植。
|
||
# save_api_call_details_to_file(orchestrator.get_api_call_details(), api_calls_output_path_str, api_calls_filename)
|
||
|
||
return jsonify({
|
||
"message": "测试执行完成。",
|
||
"summary": summary.to_dict(),
|
||
"report_file": main_report_file_path_str, # 报告文件路径(如果保存了)
|
||
# "api_calls_file": api_calls_output_path_str + "/" + api_calls_filename # API调用详情文件路径
|
||
}), 200
|
||
|
||
except Exception as e:
|
||
logger.error(f"执行测试时发生错误: {e}\n{traceback.format_exc()}")
|
||
return jsonify({"error": f"执行测试时发生内部错误: {str(e)}"}), 500
|
||
|
||
@app.route('/list-yapi-categories', methods=['POST'])
|
||
def list_yapi_categories_endpoint():
|
||
try:
|
||
data = request.json
|
||
yapi_file = data.get('yapi_file_path')
|
||
if not yapi_file:
|
||
return jsonify({"error": "'yapi_file_path' is required"}), 400
|
||
|
||
if not os.path.isabs(yapi_file):
|
||
yapi_file = os.path.join(os.getcwd(), yapi_file)
|
||
if not os.path.exists(yapi_file):
|
||
return jsonify({"error": f"YAPI file not found: {yapi_file}"}), 400
|
||
|
||
parser = InputParser()
|
||
parsed_yapi = parser.parse_yapi_spec(yapi_file)
|
||
if not parsed_yapi or not parsed_yapi.categories:
|
||
return jsonify({"error": "Failed to parse YAPI categories or no categories found"}), 500
|
||
|
||
categories_list = [
|
||
{"name": cat.get('name', '未命名'), "description": cat.get('desc', '无描述')}
|
||
for cat in parsed_yapi.categories
|
||
]
|
||
return jsonify(categories_list), 200
|
||
except Exception as e:
|
||
logger.error(f"列出YAPI分类时出错: {e}\n{traceback.format_exc()}")
|
||
return jsonify({"error": f"处理YAPI分类列表时出错: {str(e)}"}), 500
|
||
|
||
@app.route('/list-swagger-tags', methods=['POST'])
|
||
def list_swagger_tags_endpoint():
|
||
try:
|
||
data = request.json
|
||
swagger_file = data.get('swagger_file_path')
|
||
if not swagger_file:
|
||
return jsonify({"error": "'swagger_file_path' is required"}), 400
|
||
|
||
if not os.path.isabs(swagger_file):
|
||
swagger_file = os.path.join(os.getcwd(), swagger_file)
|
||
if not os.path.exists(swagger_file):
|
||
return jsonify({"error": f"Swagger file not found: {swagger_file}"}), 400
|
||
|
||
parser = InputParser()
|
||
parsed_swagger = parser.parse_swagger_spec(swagger_file)
|
||
if not parsed_swagger or not parsed_swagger.tags:
|
||
return jsonify({"error": "Failed to parse Swagger tags or no tags found"}), 500
|
||
|
||
tags_list = [
|
||
{"name": tag.get('name', '未命名'), "description": tag.get('description', '无描述')}
|
||
for tag in parsed_swagger.tags
|
||
]
|
||
return jsonify(tags_list), 200
|
||
except Exception as e:
|
||
logger.error(f"列出Swagger标签时出错: {e}\n{traceback.format_exc()}")
|
||
return jsonify({"error": f"处理Swagger标签列表时出错: {str(e)}"}), 500
|
||
|
||
if __name__ == '__main__':
|
||
# 注意:在生产环境中,应使用Gunicorn或uWSGI等WSGI服务器运行Flask应用
|
||
app.run(debug=True, host='0.0.0.0', port=5050) |