- Base platform from base.git (ERP base: auth, core, config, message, workflow, plugin) - Created erp-diary module skeleton (lib.rs, dto.rs, error.rs, event.rs, state.rs) - Integrated erp-diary into workspace and erp-server - Added DiaryModule registration in main.rs - Added DiaryState FromRef in state.rs - Diary routes mounted (empty routes, ready for implementation) - Product design spec v1.2 preserved in docs/ - Implementation plan preserved in plans/ Cargo check: OK Cargo test: OK (78+ base tests passing)
513 lines
20 KiB
Python
513 lines
20 KiB
Python
#!/usr/bin/env python3
|
||
"""HMS 患者建档链路端到端 API 测试"""
|
||
import json
|
||
import sys
|
||
import time
|
||
import urllib.request
|
||
import urllib.error
|
||
|
||
BASE_URL = "http://localhost:3000/api/v1"
|
||
TOKEN = None
|
||
RESULTS = []
|
||
|
||
def log(test_id, name, status, detail):
|
||
"""记录测试结果"""
|
||
RESULTS.append({"id": test_id, "name": name, "status": status, "detail": detail})
|
||
icon = "PASS" if status == "PASS" else ("FAIL" if status == "FAIL" else "WARN")
|
||
print(f" [{icon}] {test_id}: {name} -- {detail}")
|
||
|
||
def api_call(method, path, data=None, token=None, expect_status=None):
|
||
"""发送 API 请求"""
|
||
url = f"{BASE_URL}{path}"
|
||
headers = {"Content-Type": "application/json"}
|
||
if token:
|
||
headers["Authorization"] = f"Bearer {token}"
|
||
|
||
body = json.dumps(data).encode("utf-8") if data else None
|
||
req = urllib.request.Request(url, data=body, headers=headers, method=method)
|
||
|
||
try:
|
||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||
status = resp.status
|
||
resp_data = json.loads(resp.read().decode("utf-8"))
|
||
if expect_status and status != expect_status:
|
||
return status, resp_data, f"Expected {expect_status}, got {status}"
|
||
return status, resp_data, None
|
||
except urllib.error.HTTPError as e:
|
||
resp_body = e.read().decode("utf-8", errors="replace")
|
||
try:
|
||
resp_data = json.loads(resp_body)
|
||
except:
|
||
resp_data = {"raw": resp_body}
|
||
if expect_status and e.code == expect_status:
|
||
return e.code, resp_data, None
|
||
return e.code, resp_data, f"HTTP {e.code}: {resp_body[:200]}"
|
||
except Exception as e:
|
||
return 0, None, str(e)
|
||
|
||
# ============================================================
|
||
# Step 0: Login
|
||
# ============================================================
|
||
print("\n" + "="*60)
|
||
print("Step 0: 登录获取 Token")
|
||
print("="*60)
|
||
|
||
status, resp, err = api_call("POST", "/auth/login",
|
||
{"username": "admin", "password": "Admin@2026"}, expect_status=200)
|
||
|
||
if err:
|
||
# 可能限流,等一下重试
|
||
print(f" 首次登录失败: {err}")
|
||
print(" 等待 20 秒后重试...")
|
||
time.sleep(20)
|
||
status, resp, err = api_call("POST", "/auth/login",
|
||
{"username": "admin", "password": "Admin@2026"}, expect_status=200)
|
||
|
||
if err or not resp or not resp.get("success"):
|
||
log("T0", "登录", "FAIL", f"登录失败: {err or resp}")
|
||
sys.exit(1)
|
||
|
||
TOKEN = resp["data"]["access_token"]
|
||
user = resp["data"]["user"]
|
||
log("T0", "登录", "PASS", f"用户: {user['display_name']}, 角色: {[r['name'] for r in user['roles']]}")
|
||
|
||
# ============================================================
|
||
# Test 1.1: Patient List
|
||
# ============================================================
|
||
print("\n" + "="*60)
|
||
print("Test 1.1: 患者列表")
|
||
print("="*60)
|
||
|
||
status, resp, err = api_call("GET", "/health/patients?page=1&page_size=10", token=TOKEN)
|
||
if err:
|
||
log("T1.1", "患者列表", "FAIL", err)
|
||
else:
|
||
total = resp.get("data", {}).get("total", 0)
|
||
items = resp.get("data", {}).get("items", [])
|
||
log("T1.1", "患者列表", "PASS", f"success={resp['success']}, total={total}, items={len(items)}")
|
||
if items:
|
||
p = items[0]
|
||
print(f" 首条: id={p.get('id')}, name={p.get('name')}, gender={p.get('gender')}")
|
||
|
||
# ============================================================
|
||
# Test 1.2: Create Patient (Valid)
|
||
# ============================================================
|
||
print("\n" + "="*60)
|
||
print("Test 1.2: 创建患者 - 完整有效数据")
|
||
print("="*60)
|
||
|
||
import random
|
||
_ts = str(int(time.time() * 1000))[-6:]
|
||
|
||
patient_data = {
|
||
"name": f"API测试患者_{_ts}",
|
||
"gender": "male",
|
||
"birth_date": "1990-05-15",
|
||
"phone": f"138{random.randint(10000000, 99999999)}",
|
||
"blood_type": "A",
|
||
"emergency_contact_name": "紧急联系人",
|
||
"emergency_contact_phone": f"139{random.randint(10000000, 99999999)}"
|
||
}
|
||
|
||
patient_id = None # 预定义,防止后续 NameError
|
||
|
||
status, resp, err = api_call("POST", "/health/patients", patient_data, token=TOKEN)
|
||
if err:
|
||
log("T1.2", "创建患者(有效)", "FAIL", err)
|
||
elif resp and resp.get("success"):
|
||
patient = resp["data"]
|
||
patient_id = patient.get("id")
|
||
log("T1.2", "创建患者(有效)", "PASS",
|
||
f"id={patient_id}, name={patient.get('name')}, gender={patient.get('gender')}, version={patient.get('version')}")
|
||
print(f" birth_date={patient.get('birth_date')}, phone={patient.get('phone')}")
|
||
print(f" blood_type={patient.get('blood_type')}, tenant_id={patient.get('tenant_id')}")
|
||
else:
|
||
log("T1.2", "创建患者(有效)", "FAIL", f"success={resp.get('success')}, error={resp.get('error')}")
|
||
|
||
# ============================================================
|
||
# Test 1.3: Create Patient - Empty Name (should fail)
|
||
# ============================================================
|
||
print("\n" + "="*60)
|
||
print("Test 1.3: 创建患者 - 空名称(应失败)")
|
||
print("="*60)
|
||
|
||
status, resp, err = api_call("POST", "/health/patients",
|
||
{"name": "", "gender": "male", "birth_date": "1990-01-01"}, token=TOKEN)
|
||
|
||
if status == 400 or (resp and not resp.get("success")):
|
||
log("T1.3", "创建患者(空名称)", "PASS",
|
||
f"正确拒绝: status={status}, error={resp.get('error', resp.get('message', 'N/A'))}")
|
||
elif status == 201 or (resp and resp.get("success")):
|
||
log("T1.3", "创建患者(空名称)", "FAIL", "空名称被接受,应该被拒绝")
|
||
else:
|
||
log("T1.3", "创建患者(空名称)", "FAIL", f"status={status}, resp={resp}")
|
||
|
||
# ============================================================
|
||
# Test 1.4: Create Patient - Future Birth Date (should fail)
|
||
# ============================================================
|
||
print("\n" + "="*60)
|
||
print("Test 1.4: 创建患者 - 未来出生日期(应失败)")
|
||
print("="*60)
|
||
|
||
status, resp, err = api_call("POST", "/health/patients",
|
||
{"name": "未来患者", "gender": "male", "birth_date": "2099-01-01"}, token=TOKEN)
|
||
|
||
if status == 400 or (resp and not resp.get("success")):
|
||
log("T1.4", "创建患者(未来日期)", "PASS",
|
||
f"正确拒绝: status={status}, error={resp.get('error', resp.get('message', 'N/A'))}")
|
||
elif status == 201 or (resp and resp.get("success")):
|
||
log("T1.4", "创建患者(未来日期)", "FAIL", "未来出生日期被接受,应该被拒绝")
|
||
else:
|
||
log("T1.4", "创建患者(未来日期)", "FAIL", f"status={status}, resp={resp}")
|
||
|
||
# ============================================================
|
||
# Test 2.1: Patient Detail + PII Check
|
||
# ============================================================
|
||
print("\n" + "="*60)
|
||
print("Test 2.1: 患者详情 + PII 脱敏验证")
|
||
print("="*60)
|
||
|
||
if patient_id:
|
||
status, resp, err = api_call("GET", f"/health/patients/{patient_id}", token=TOKEN)
|
||
if err:
|
||
log("T2.1", "患者详情", "FAIL", err)
|
||
else:
|
||
p = resp.get("data", {})
|
||
log("T2.1", "患者详情", "PASS", f"success={resp['success']}, name={p.get('name')}")
|
||
# PII 检查: phone 是否为明文或脱敏
|
||
phone = p.get("phone", "N/A")
|
||
emergency_phone = p.get("emergency_contact_phone", "N/A")
|
||
print(f" phone={phone}")
|
||
print(f" emergency_contact_phone={emergency_phone}")
|
||
print(f" id_card_number={p.get('id_card_number', 'N/A')}")
|
||
# 检查标准字段
|
||
has_tenant_id = "tenant_id" in p
|
||
has_created_at = "created_at" in p
|
||
has_version = "version" in p
|
||
print(f" tenant_id={'存在' if has_tenant_id else '缺失'}, "
|
||
f"created_at={'存在' if has_created_at else '缺失'}, "
|
||
f"version={'存在' if has_version else '缺失'}")
|
||
if not (has_tenant_id and has_created_at and has_version):
|
||
log("T2.1b", "标准字段检查", "WARN", "部分标准字段缺失")
|
||
else:
|
||
print(f" 标准字段检查: 通过")
|
||
else:
|
||
log("T2.1", "患者详情", "SKIP", "无 patient_id (创建患者失败)")
|
||
|
||
# ============================================================
|
||
# Test 2.2: Patient Detail - Non-existent ID (should 404)
|
||
# ============================================================
|
||
print("\n" + "="*60)
|
||
print("Test 2.2: 患者详情 - 不存在的ID(应404)")
|
||
print("="*60)
|
||
|
||
status, resp, err = api_call("GET", "/health/patients/00000000-0000-0000-0000-000000000000", token=TOKEN)
|
||
if status == 404:
|
||
log("T2.2", "患者详情(不存在)", "PASS", f"正确返回 404")
|
||
elif err:
|
||
log("T2.2", "患者详情(不存在)", "WARN", f"status={status}, err={err}")
|
||
else:
|
||
log("T2.2", "患者详情(不存在)", "FAIL", f"status={status}, 应为 404")
|
||
|
||
# ============================================================
|
||
# Test 3.1: Patient Tags - Create Tag
|
||
# ============================================================
|
||
print("\n" + "="*60)
|
||
print("Test 3.1: 患者标签 - 创建标签")
|
||
print("="*60)
|
||
|
||
tag_id = None
|
||
status, resp, err = api_call("POST", "/health/patient-tags",
|
||
{"name": f"API测试标签_{_ts}", "color": "#FF5500"}, token=TOKEN)
|
||
|
||
if err:
|
||
log("T3.1", "创建标签", "FAIL", err)
|
||
elif resp and resp.get("success"):
|
||
tag = resp["data"]
|
||
tag_id = tag.get("id")
|
||
log("T3.1", "创建标签", "PASS", f"id={tag_id}, name={tag.get('name')}, color={tag.get('color')}")
|
||
else:
|
||
log("T3.1", "创建标签", "FAIL", f"status={status}, error={resp}")
|
||
|
||
# ============================================================
|
||
# Test 3.2: Patient Tags - List
|
||
# ============================================================
|
||
print("\n" + "="*60)
|
||
print("Test 3.2: 患者标签 - 列表")
|
||
print("="*60)
|
||
|
||
status, resp, err = api_call("GET", "/health/patient-tags", token=TOKEN)
|
||
if err:
|
||
log("T3.2", "标签列表", "FAIL", err)
|
||
else:
|
||
raw_data = resp.get("data")
|
||
if isinstance(raw_data, list):
|
||
total = len(raw_data)
|
||
log("T3.2", "标签列表", "PASS", f"success={resp['success']}, count={total}")
|
||
elif isinstance(raw_data, dict):
|
||
items = raw_data.get("items", [])
|
||
total = raw_data.get("total", len(items))
|
||
log("T3.2", "标签列表", "PASS", f"success={resp['success']}, total={total}")
|
||
else:
|
||
log("T3.2", "标签列表", "PASS", f"success={resp['success']}, data_type={type(raw_data)}")
|
||
|
||
# ============================================================
|
||
# Test 3.3: Patient Tags - Assign to Patient
|
||
# ============================================================
|
||
print("\n" + "="*60)
|
||
print("Test 3.3: 患者标签 - 关联标签到患者")
|
||
print("="*60)
|
||
|
||
if patient_id and tag_id:
|
||
# 尝试关联标签 - 可能的 API 路径
|
||
status, resp, err = api_call("POST", f"/health/patients/{patient_id}/tags",
|
||
{"tag_ids": [tag_id]}, token=TOKEN)
|
||
|
||
if err and status == 404:
|
||
# 尝试替代路径
|
||
status2, resp2, err2 = api_call("POST", "/health/patient-tag-relations",
|
||
{"patient_id": patient_id, "tag_id": tag_id}, token=TOKEN)
|
||
if err2:
|
||
log("T3.3", "关联标签", "WARN", f"标签关联路径未确认: {err[:100]}")
|
||
else:
|
||
log("T3.3", "关联标签", "PASS", f"通过 /patient-tag-relations: {resp2}")
|
||
elif err:
|
||
log("T3.3", "关联标签", "WARN", f"status={status}, err={err[:150]}")
|
||
else:
|
||
log("T3.3", "关联标签", "PASS", f"success={resp.get('success')}")
|
||
else:
|
||
log("T3.3", "关联标签", "SKIP", "缺少 patient_id 或 tag_id")
|
||
|
||
# ============================================================
|
||
# Test 4.1: Patient Update - Normal
|
||
# ============================================================
|
||
print("\n" + "="*60)
|
||
print("Test 4.1: 患者更新 - 正常更新")
|
||
print("="*60)
|
||
|
||
updated_version = None
|
||
if patient_id:
|
||
# 先获取当前 version
|
||
status, resp, err = api_call("GET", f"/health/patients/{patient_id}", token=TOKEN)
|
||
if not err and resp.get("success"):
|
||
current_version = resp["data"].get("version")
|
||
print(f" 当前 version: {current_version}")
|
||
|
||
update_data = {
|
||
"name": "API测试患者-已更新",
|
||
"phone": "13800003333",
|
||
"version": current_version
|
||
}
|
||
|
||
status, resp, err = api_call("PUT", f"/health/patients/{patient_id}", update_data, token=TOKEN)
|
||
if err:
|
||
log("T4.1", "患者更新", "FAIL", err)
|
||
elif resp and resp.get("success"):
|
||
updated = resp["data"]
|
||
updated_version = updated.get("version")
|
||
log("T4.1", "患者更新", "PASS",
|
||
f"name={updated.get('name')}, version={current_version}->{updated_version}")
|
||
else:
|
||
log("T4.1", "患者更新", "FAIL", f"success={resp.get('success')}, error={resp}")
|
||
else:
|
||
log("T4.1", "患者更新", "FAIL", f"获取患者失败: {err}")
|
||
else:
|
||
log("T4.1", "患者更新", "SKIP", "无 patient_id")
|
||
|
||
# ============================================================
|
||
# Test 4.2: Patient Update - Optimistic Lock (should fail)
|
||
# ============================================================
|
||
print("\n" + "="*60)
|
||
print("Test 4.2: 患者更新 - 乐观锁冲突(应失败)")
|
||
print("="*60)
|
||
|
||
if patient_id:
|
||
# 使用旧 version 触发乐观锁冲突
|
||
stale_update = {
|
||
"name": "API测试患者-冲突更新",
|
||
"version": 1 # 旧版本号
|
||
}
|
||
|
||
status, resp, err = api_call("PUT", f"/health/patients/{patient_id}", stale_update, token=TOKEN)
|
||
if status == 409 or (resp and not resp.get("success")):
|
||
log("T4.2", "乐观锁冲突", "PASS",
|
||
f"正确拒绝: status={status}, error={resp.get('error', resp.get('message', 'N/A'))}")
|
||
elif resp and resp.get("success"):
|
||
log("T4.2", "乐观锁冲突", "FAIL", "旧版本号更新被接受,乐观锁未生效")
|
||
else:
|
||
log("T4.2", "乐观锁冲突", "WARN", f"status={status}, resp={str(resp)[:200]}")
|
||
else:
|
||
log("T4.2", "乐观锁冲突", "SKIP", "无 patient_id")
|
||
|
||
# ============================================================
|
||
# Test 5.1: Security - No Auth (should 401)
|
||
# ============================================================
|
||
print("\n" + "="*60)
|
||
print("Test 5.1: 安全测试 - 无认证访问(应401)")
|
||
print("="*60)
|
||
|
||
status, resp, err = api_call("GET", "/health/patients")
|
||
if status == 401:
|
||
log("T5.1", "无认证访问", "PASS", "正确返回 401")
|
||
elif err:
|
||
log("T5.1", "无认证访问", "FAIL", f"status={status}, 应为 401")
|
||
else:
|
||
log("T5.1", "无认证访问", "FAIL", f"status={status}, 成功访问但应该被拒绝")
|
||
|
||
# ============================================================
|
||
# Test 5.2: Security - SQL Injection Attempt
|
||
# ============================================================
|
||
print("\n" + "="*60)
|
||
print("Test 5.2: 安全测试 - SQL 注入尝试")
|
||
print("="*60)
|
||
|
||
status, resp, err = api_call("GET", "/health/patients?search=%27%3B%20DROP%20TABLE%20patients%3B%20--", token=TOKEN)
|
||
if status == 500:
|
||
log("T5.2", "SQL注入防护", "FAIL", "服务器返回 500,可能存在注入风险")
|
||
elif resp and resp.get("success"):
|
||
# 正常返回搜索结果 = 注入被参数化查询防住了
|
||
log("T5.2", "SQL注入防护", "PASS", f"注入被参数化查询拦截,正常返回数据")
|
||
else:
|
||
log("T5.2", "SQL注入防护", "PASS", f"status={status}, 注入未导致服务异常")
|
||
|
||
# ============================================================
|
||
# Test 5.3: Security - Invalid Token (should 401)
|
||
# ============================================================
|
||
print("\n" + "="*60)
|
||
print("Test 5.3: 安全测试 - 无效 Token(应401)")
|
||
print("="*60)
|
||
|
||
status, resp, err = api_call("GET", "/health/patients", token="invalid_token_here")
|
||
if status == 401:
|
||
log("T5.3", "无效Token", "PASS", "正确返回 401")
|
||
else:
|
||
log("T5.3", "无效Token", "FAIL", f"status={status}, 应为 401")
|
||
|
||
# ============================================================
|
||
# Test 6.1: Pagination
|
||
# ============================================================
|
||
print("\n" + "="*60)
|
||
print("Test 6.1: 分页查询")
|
||
print("="*60)
|
||
|
||
status, resp, err = api_call("GET", "/health/patients?page=1&page_size=2", token=TOKEN)
|
||
if err:
|
||
log("T6.1", "分页查询", "FAIL", err)
|
||
else:
|
||
total = resp.get("data", {}).get("total", 0)
|
||
items = resp.get("data", {}).get("items", [])
|
||
page = resp.get("data", {}).get("page", "N/A")
|
||
page_size = resp.get("data", {}).get("page_size", resp.get("data", {}).get("per_page", "N/A"))
|
||
log("T6.1", "分页查询", "PASS",
|
||
f"total={total}, page={page}, page_size={page_size}, returned={len(items)}")
|
||
|
||
# ============================================================
|
||
# Test 7.1: Create patient with minimal data
|
||
# ============================================================
|
||
print("\n" + "="*60)
|
||
print("Test 7.1: 创建患者 - 最小必填数据")
|
||
print("="*60)
|
||
|
||
status, resp, err = api_call("POST", "/health/patients",
|
||
{"name": f"最小数据患者_{_ts}", "gender": "female", "birth_date": "2000-01-01"}, token=TOKEN)
|
||
|
||
if err:
|
||
log("T7.1", "创建患者(最小数据)", "FAIL", err)
|
||
elif resp and resp.get("success"):
|
||
p = resp["data"]
|
||
log("T7.1", "创建患者(最小数据)", "PASS",
|
||
f"id={p.get('id')}, name={p.get('name')}, optional fields: phone={p.get('phone')}, blood_type={p.get('blood_type')}")
|
||
else:
|
||
log("T7.1", "创建患者(最小数据)", "FAIL", f"success={resp.get('success')}, error={resp}")
|
||
|
||
# ============================================================
|
||
# Test 7.2: Create patient with whitespace-only name (should fail)
|
||
# ============================================================
|
||
print("\n" + "="*60)
|
||
print("Test 7.2: 创建患者 - 纯空格名称(应失败)")
|
||
print("="*60)
|
||
|
||
status, resp, err = api_call("POST", "/health/patients",
|
||
{"name": " ", "gender": "male", "birth_date": "1990-01-01"}, token=TOKEN)
|
||
|
||
if status == 400 or (resp and not resp.get("success")):
|
||
log("T7.2", "创建患者(空格名称)", "PASS",
|
||
f"正确拒绝: status={status}, error={resp.get('error', resp.get('message', 'N/A'))}")
|
||
else:
|
||
log("T7.2", "创建患者(空格名称)", "FAIL", f"纯空格名称被接受: status={status}")
|
||
|
||
# ============================================================
|
||
# Test 8.1: Invalid gender
|
||
# ============================================================
|
||
print("\n" + "="*60)
|
||
print("Test 8.1: 创建患者 - 无效性别值")
|
||
print("="*60)
|
||
|
||
status, resp, err = api_call("POST", "/health/patients",
|
||
{"name": "无效性别", "gender": "invalid_gender", "birth_date": "1990-01-01"}, token=TOKEN)
|
||
|
||
if status == 400 or (resp and not resp.get("success")):
|
||
log("T8.1", "创建患者(无效性别)", "PASS",
|
||
f"正确拒绝: status={status}, error={resp.get('error', resp.get('message', 'N/A'))}")
|
||
elif resp and resp.get("success"):
|
||
log("T8.1", "创建患者(无效性别)", "WARN", f"无效性别被接受(可能是开放枚举): gender={resp['data'].get('gender')}")
|
||
else:
|
||
log("T8.1", "创建患者(无效性别)", "WARN", f"status={status}")
|
||
|
||
# ============================================================
|
||
# Test 9.1: Invalid birth_date format
|
||
# ============================================================
|
||
print("\n" + "="*60)
|
||
print("Test 9.1: 创建患者 - 无效日期格式")
|
||
print("="*60)
|
||
|
||
status, resp, err = api_call("POST", "/health/patients",
|
||
{"name": "无效日期", "gender": "male", "birth_date": "not-a-date"}, token=TOKEN)
|
||
|
||
if status == 400 or (resp and not resp.get("success")):
|
||
log("T9.1", "创建患者(无效日期格式)", "PASS",
|
||
f"正确拒绝: status={status}, error={resp.get('error', resp.get('message', 'N/A'))}")
|
||
else:
|
||
log("T9.1", "创建患者(无效日期格式)", "FAIL", f"无效日期格式被接受: status={status}")
|
||
|
||
# ============================================================
|
||
# Summary
|
||
# ============================================================
|
||
print("\n" + "="*60)
|
||
print("测试汇总")
|
||
print("="*60)
|
||
|
||
passed = sum(1 for r in RESULTS if r["status"] == "PASS")
|
||
failed = sum(1 for r in RESULTS if r["status"] == "FAIL")
|
||
warned = sum(1 for r in RESULTS if r["status"] == "WARN")
|
||
skipped = sum(1 for r in RESULTS if r["status"] == "SKIP")
|
||
total = len(RESULTS)
|
||
|
||
print(f"\n 总计: {total} 项测试")
|
||
print(f" PASS: {passed}")
|
||
print(f" FAIL: {failed}")
|
||
print(f" WARN: {warned}")
|
||
print(f" SKIP: {skipped}")
|
||
print(f" 通过率: {passed/total*100:.1f}%")
|
||
|
||
if failed > 0:
|
||
print("\n 失败项:")
|
||
for r in RESULTS:
|
||
if r["status"] == "FAIL":
|
||
print(f" [{r['id']}] {r['name']}: {r['detail']}")
|
||
|
||
if warned > 0:
|
||
print("\n 警告项:")
|
||
for r in RESULTS:
|
||
if r["status"] == "WARN":
|
||
print(f" [{r['id']}] {r['name']}: {r['detail']}")
|
||
|
||
print("\n" + "="*60)
|
||
if failed == 0:
|
||
print("结论: 所有测试通过")
|
||
elif failed <= 2:
|
||
print(f"结论: 基本通过,有 {failed} 项失败需要关注")
|
||
else:
|
||
print(f"结论: 有 {failed} 项失败,需要修复")
|
||
print("="*60)
|