compliance/run_api_tests.py
gongwenxin 156dcdfaf9 step2
2025-05-19 17:09:09 +08:00

387 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

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.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
API测试工具
此工具使用DDMS测试编排器从YAPI或Swagger定义执行API测试。
支持使用规则库进行高级验证。
"""
import os
import sys
import json
import logging
import argparse
from pathlib import Path
from ddms_compliance_suite.test_orchestrator import APITestOrchestrator, TestSummary
from ddms_compliance_suite.models.rule_models import (
PerformanceRule, SecurityRule, RESTfulDesignRule, ErrorHandlingRule,
RuleCategory, TargetType, RuleLifecycle, RuleScope, SeverityLevel
)
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
def parse_args():
"""解析命令行参数"""
parser = argparse.ArgumentParser(description='DDMS API测试工具')
# 基本参数
parser.add_argument('--base-url', required=True, help='API基础URL')
parser.add_argument('--verbose', '-v', action='store_true', help='启用详细日志')
parser.add_argument('--output', '-o', help='输出文件路径')
parser.add_argument('--format', choices=['json', 'html'], default='json', help='输出格式')
# API定义参数
api_group = parser.add_argument_group('API定义源')
api_group.add_argument('--yapi', help='YAPI定义文件路径')
api_group.add_argument('--swagger', help='Swagger定义文件路径')
# 过滤参数
filter_group = parser.add_argument_group('过滤选项')
filter_group.add_argument('--categories', help='YAPI分类逗号分隔')
filter_group.add_argument('--tags', help='Swagger标签逗号分隔')
filter_group.add_argument('--list-categories', action='store_true', help='列出YAPI分类')
filter_group.add_argument('--list-tags', action='store_true', help='列出Swagger标签')
# 规则库参数
rule_group = parser.add_argument_group('规则库选项')
rule_group.add_argument('--rules-path', default='./rules', help='规则库路径')
# rule_group.add_argument('--disable-rules', action='store_true', help='禁用规则验证',default=False)
rule_group.add_argument('--disable-rules', action='store_true', help='禁用规则验证',default=True)
rule_group.add_argument('--list-rules', action='store_true', help='列出可用规则',default=False)
rule_group.add_argument('--create-basic-rules', action='store_true',
help='创建基本规则集性能、安全、RESTful设计、错误处理',default=False)
# 新增:自定义测试用例参数组
custom_tc_group = parser.add_argument_group('自定义测试用例选项')
custom_tc_group.add_argument('--custom-test-cases-dir',
default=None, # 或者 './custom_testcases' 如果想设为默认
help='存放自定义APITestCase Python文件的目录路径。如果未提供则不加载自定义测试。')
return parser.parse_args()
def list_yapi_categories(yapi_file: str):
"""列出YAPI分类"""
from ddms_compliance_suite.input_parser.parser import InputParser
logger.info(f"从YAPI文件解析分类: {yapi_file}")
parser = InputParser()
parsed_yapi = parser.parse_yapi_spec(yapi_file)
if not parsed_yapi:
logger.error(f"解析YAPI文件失败: {yapi_file}")
return
print("\nYAPI分类:")
for i, category in enumerate(parsed_yapi.categories, 1):
print(f"{i}. {category.get('name', '未命名')} - {category.get('desc', '无描述')}")
def list_swagger_tags(swagger_file: str):
"""列出Swagger标签"""
from ddms_compliance_suite.input_parser.parser import InputParser
logger.info(f"从Swagger文件解析标签: {swagger_file}")
parser = InputParser()
parsed_swagger = parser.parse_swagger_spec(swagger_file)
if not parsed_swagger:
logger.error(f"解析Swagger文件失败: {swagger_file}")
return
print("\nSwagger标签:")
for i, tag in enumerate(parsed_swagger.tags, 1):
print(f"{i}. {tag.get('name', '未命名')} - {tag.get('description', '无描述')}")
def create_basic_rules(orchestrator):
"""创建基本规则集"""
logger.info("创建基本规则集...")
# 1. 性能规则 - 响应时间不超过500毫秒
performance_rule = PerformanceRule(
id="response-time-max-500ms",
name="响应时间不超过500毫秒",
description="验证API响应时间不超过500毫秒",
category=RuleCategory.PERFORMANCE,
severity=SeverityLevel.WARNING,
target_type=TargetType.API_RESPONSE,
lifecycle=RuleLifecycle.RESPONSE_VALIDATION,
scope=RuleScope.RESPONSE_TIME,
threshold=500.0,
metric="response_time",
unit="ms"
)
# 2. 安全规则 - HTTPS必须使用
security_rule = SecurityRule(
id="https-only-rule",
name="HTTPS强制使用规则",
description="验证API请求是否使用了HTTPS协议",
category=RuleCategory.SECURITY,
severity=SeverityLevel.ERROR,
target_type=TargetType.API_REQUEST,
lifecycle=RuleLifecycle.REQUEST_PREPARATION,
scope=RuleScope.SECURITY,
check_type="transport_security",
expected_value="https"
)
# 3. RESTful设计规则 - URL路径格式
restful_rule = RESTfulDesignRule(
id="restful-url-pattern",
name="RESTful URL设计规则",
description="验证API URL是否符合RESTful设计规范",
category=RuleCategory.API_DESIGN,
severity=SeverityLevel.WARNING,
target_type=TargetType.API_REQUEST,
lifecycle=RuleLifecycle.REQUEST_PREPARATION,
scope=RuleScope.REQUEST_URL,
design_aspect="URL设计",
pattern=r"^/api/v\d+/[a-z0-9-]+(/[a-z0-9-]+)*$"
)
# 4. 错误处理规则 - 错误响应格式
error_rule = ErrorHandlingRule(
id="standard-error-response",
name="标准错误响应格式规则",
description="验证API错误响应是否符合标准格式",
category=RuleCategory.ERROR_HANDLING,
severity=SeverityLevel.WARNING,
target_type=TargetType.API_RESPONSE,
lifecycle=RuleLifecycle.RESPONSE_VALIDATION,
scope=RuleScope.RESPONSE_BODY,
error_code="*",
expected_status=400
)
# 保存规则到规则库
orchestrator.rule_repo.save_rule(performance_rule)
orchestrator.rule_repo.save_rule(security_rule)
orchestrator.rule_repo.save_rule(restful_rule)
orchestrator.rule_repo.save_rule(error_rule)
logger.info("已创建基本规则集")
def list_rules(rule_repo_path: str):
"""列出可用规则"""
from ddms_compliance_suite.models.config_models import RuleRepositoryConfig, RuleStorageConfig
from ddms_compliance_suite.rule_repository.repository import RuleRepository
# 初始化规则库
rule_config = RuleRepositoryConfig(
storage=RuleStorageConfig(path=rule_repo_path)
)
repo = RuleRepository(rule_config)
# 查询所有规则
rules = repo.query_rules()
if not rules:
print("未找到规则。使用 --create-basic-rules 创建基本规则集。")
return
print(f"\n找到 {len(rules)} 条规则:")
# 按类别分组
rules_by_category = {}
for rule in rules:
category = str(rule.category)
if category not in rules_by_category:
rules_by_category[category] = []
rules_by_category[category].append(rule)
# 打印规则
for category, category_rules in rules_by_category.items():
print(f"\n{category}:")
for rule in category_rules:
print(f" - {rule.id}: {rule.name} (严重性: {rule.severity}, 版本: {rule.version})")
print(f" {rule.description}")
print(f" 生命周期: {rule.lifecycle}, 作用域: {rule.scope}")
def save_results(summary: TestSummary, output_file: str, format_type: str):
"""保存测试结果"""
if format_type == 'json':
with open(output_file, 'w', encoding='utf-8') as f:
f.write(summary.to_json(pretty=True))
logger.info(f"测试结果已保存为JSON: {output_file}")
elif format_type == 'html':
# 创建简单的HTML报告
html_content = f"""
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>API测试报告</title>
<style>
body {{ font-family: Arial, sans-serif; margin: 20px; }}
.summary {{ background-color: #f5f5f5; padding: 15px; border-radius: 5px; }}
.pass {{ color: green; }}
.fail {{ color: red; }}
.error {{ color: orange; }}
.skip {{ color: gray; }}
table {{ border-collapse: collapse; width: 100%; }}
th, td {{ border: 1px solid #ddd; padding: 8px; text-align: left; }}
th {{ background-color: #f2f2f2; }}
tr:nth-child(even) {{ background-color: #f9f9f9; }}
</style>
</head>
<body>
<h1>API测试报告</h1>
<div class="summary">
<h2>测试结果摘要</h2>
<p>总测试数: {summary.total}</p>
<p class="pass">通过: {summary.passed}</p>
<p class="fail">失败: {summary.failed}</p>
<p class="error">错误: {summary.error}</p>
<p class="skip">跳过: {summary.skipped}</p>
<p>成功率: {summary.success_rate:.2f}%</p>
<p>总耗时: {summary.duration:.2f}秒</p>
<p>开始时间: {summary.start_time.isoformat()}</p>
<p>结束时间: {summary.end_time.isoformat() if summary.end_time else 'N/A'}</p>
</div>
<h2>详细测试结果</h2>
<table>
<tr>
<th>端点</th>
<th>状态</th>
<th>消息</th>
<th>响应码</th>
<th>耗时(秒)</th>
</tr>
"""
for result in summary.results:
status_class = "pass" if result.status == "通过" else "fail" if result.status == "失败" else "error" if result.status == "错误" else "skip"
response_code = result.api_response.status_code if result.api_response else "N/A"
html_content += f"""
<tr>
<td>{result.endpoint_id}</td>
<td class="{status_class}">{result.status}</td>
<td>{result.message}</td>
<td>{response_code}</td>
<td>{result.elapsed_time:.4f}</td>
</tr>
"""
html_content += """
</table>
</body>
</html>
"""
with open(output_file, 'w', encoding='utf-8') as f:
f.write(html_content)
logger.info(f"测试结果已保存为HTML: {output_file}")
def main():
"""主函数"""
args = parse_args()
if args.verbose:
logging.getLogger('ddms_compliance_suite').setLevel(logging.DEBUG)
logger.setLevel(logging.DEBUG)
logger.debug("已启用详细日志模式")
# 列出规则
if args.list_rules:
list_rules(args.rules_path)
return 0
# 检查是否提供了API定义源
if not args.yapi and not args.swagger:
logger.error("请提供API定义源--yapi 或 --swagger")
return 1
# 列出分类/标签
if args.list_categories and args.yapi:
list_yapi_categories(args.yapi)
return 0
if args.list_tags and args.swagger:
list_swagger_tags(args.swagger)
return 0
# 解析分类/标签过滤器
categories = args.categories.split(',') if args.categories else None
tags = args.tags.split(',') if args.tags else None
# 实例化测试编排器
# 将 custom_test_cases_dir 参数传递给 APITestOrchestrator 的构造函数
orchestrator = APITestOrchestrator(
base_url=args.base_url,
rule_repo_path=args.rules_path,
custom_test_cases_dir=args.custom_test_cases_dir # 新增参数
)
# 创建基本规则集
if args.create_basic_rules:
create_basic_rules(orchestrator)
# 如果禁用规则,替换规则执行器的方法
if args.disable_rules:
logger.info("规则验证已禁用")
# 替换规则执行器的方法为空实现
orchestrator.rule_executor.execute_rules_for_lifecycle = lambda *args, **kwargs: []
orchestrator.rule_executor.execute_rules_for_target = lambda *args, **kwargs: []
# 运行测试
summary = None
if args.yapi:
logger.info(f"从YAPI文件运行测试: {args.yapi}")
summary = orchestrator.run_tests_from_yapi(
yapi_file_path=args.yapi,
categories=categories,
custom_test_cases_dir=args.custom_test_cases_dir # 也传递给具体执行方法,以支持运行时覆盖
)
elif args.swagger:
logger.info(f"从Swagger文件运行测试: {args.swagger}")
summary = orchestrator.run_tests_from_swagger(
swagger_file_path=args.swagger,
tags=tags,
custom_test_cases_dir=args.custom_test_cases_dir # 也传递给具体执行方法
)
else:
logger.error("必须提供YAPI或Swagger文件路径")
if not summary:
logger.error("测试执行失败")
return 1
# 打印结果摘要
summary.print_summary_to_console()
# 保存结果
if args.output:
save_results(summary, args.output, args.format)
# 根据测试结果设置退出码
# 直接访问 TestSummary 的属性
has_endpoint_errors = summary.endpoints_error > 0
has_endpoint_failures = summary.endpoints_failed > 0
# 或者,也可以关注测试用例级别的失败/错误
# has_test_case_errors = summary.test_cases_error > 0
# has_test_case_failures = summary.test_cases_failed > 0
if has_endpoint_errors or has_endpoint_failures:
return 1 # 表示测试运行中存在失败或错误
else:
return 0 # 所有端点测试通过或部分成功(无错误或关键失败)
if __name__ == "__main__":
sys.exit(main())
# python run_api_tests.py --base-url http://127.0.0.1:4523/m1/6389742-6086420-default --swagger assets/doc/井筒API示例swagger.json --custom-test-cases-dir ./custom_testcases \
# --verbose \
# --output test_report.json