Files
dpb/backend/scripts/test_bre_wave12_tc.py
T
34047007@qq.com b95053c52c init: 初始化 dpb 桃育种系统代码库
前后端 + 后端 FastAPI 全量源码、部署脚本与文档。
2026-08-06 00:17:49 +08:00

190 lines
11 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.
"""波次1-2 修复行为聚焦验证(TestClient,无需起服务)。
运行:
cd d:\dpb\dpb\backend
C:\ai\miniconda3\envs\dpb\python.exe -X utf8 scripts/test_bre_wave12_tc.py
覆盖:
1) 1.1 模糊 LIKE 搜索(site_name 部分匹配 / personnel.name / trait_name
2) 2.1 create 前置校验: 引用不存在的父记录 -> 明确 409(非 DB 原文)
3) 2.5 跨基地迁移: 空 plot 换基地成功; 换到不存在基地 -> 409; (site_id,plot_code) 重码 -> 409
4) 2.2 delete 下游校验: 删有 plot 的 site -> 409; 删有 tree 的 plot -> 409
5) 2.3 唯一约束: 同 site 下重复 plot_code -> 409
"""
import os
os.environ["ENVIRONMENT"] = "dev"
os.environ["PYTHONUTF8"] = "1"
import sys
sys.path.insert(0, r"d:\dpb\dpb\backend")
import main
from fastapi.testclient import TestClient
create_app = main.create_app
TOKEN = None
def login(client):
global TOKEN
d = {"username": "super", "password": "123456", "grant_type": "password", "login_type": "PC端"}
r = client.post("/api/v1/system/auth/login", data=d)
b = r.json()
if r.status_code == 200 and b.get("code") == 0:
TOKEN = b["data"]["access_token"]; return
key = client.get("/api/v1/system/auth/captcha/get").json()["data"]["key"]
client.post("/api/v1/system/auth/captcha/slider/complete", json={"captcha_key": key})
d["captcha_key"] = key
r = client.post("/api/v1/system/auth/login", data=d)
b = r.json()
assert r.status_code == 200 and b.get("code") == 0, f"LOGIN FAIL {r.status_code} {b}"
TOKEN = b["data"]["access_token"]
def auth():
return {"Authorization": f"Bearer {TOKEN}"}
def call(client, method, url, **kw):
r = client.request(method, url, headers=auth(), **kw)
try:
body = r.json()
except Exception:
body = {"_raw": r.text[:300]}
return r.status_code, body
def main_():
with TestClient(create_app()) as client:
login(client)
RUN = __import__("datetime").datetime.now().strftime("%H%M%S")
passed = 0
def expect(name, cond, detail=""):
nonlocal passed
ok = bool(cond)
print(f"[{'OK ' if ok else 'FAIL'}] {name}" + (f" ({detail})" if detail and not ok else ""))
if not ok:
sys.exit(1)
passed += 1
print("\n=== 1) 模糊 LIKE 搜索 ===")
kw = f"模糊{RUN}"
st = call(client, "POST", "/api/v1/bre/site/create", json={"site_name": kw + "A"})
expect("create site A", st[0] == 200, st[1])
st2 = call(client, "POST", "/api/v1/bre/site/create", json={"site_name": "普通" + RUN})
expect("create site B", st2[0] == 200, st2[1])
r = call(client, "GET", "/api/v1/bre/site/list", params={"page": 1, "page_size": 20, "site_name": kw})
expect("site_name LIKE '%kw%' 返回匹配", r[0] == 200 and any(kw in s.get("site_name", "") for s in r[1]["data"]["items"]), r[1])
r2 = call(client, "GET", "/api/v1/bre/site/list", params={"page": 1, "page_size": 20, "site_name": "不存在的词_xyz_不存在"})
items = r2[1]["data"]["items"] if r2[0] == 200 else []
expect("模糊词无结果返回空", r2[0] == 200 and items == [], r2[1])
per = call(client, "POST", "/api/v1/bre/personnel/create", json={"name": f"张{RUN}"})
expect("create personnel", per[0] == 200, per[1])
r3 = call(client, "GET", "/api/v1/bre/personnel/list", params={"page": 1, "page_size": 20, "name": RUN})
expect("personnel.name 模糊匹配", r3[0] == 200 and len(r3[1]["data"]["items"]) == 1, r3[1])
print("\n=== 2) 2.1 前置校验: 缺失父记录 -> 明确 409 ===")
r = call(client, "POST", "/api/v1/bre/plot/create", json={"site_id": 99999999, "plot_code": f"X{RUN}"})
expect("plot.create 不存在 site_id -> 409", r[0] == 409 and ("基地" in r[1].get("msg", "") or "不存在" in r[1].get("msg", "")), r[1])
r = call(client, "POST", "/api/v1/bre/tree/create", json={"combination_id": 99999999, "tree_no": f"T{RUN}", "status": "alive"})
expect("tree.create 不存在 combination_id -> 409", r[0] == 409 and "组合" in r[1].get("msg", ""), r[1])
r = call(client, "POST", "/api/v1/bre/selection_rule/create", json={"target_id": 99999999, "rule_name": f"R{RUN}", "conditions_json": "{}"})
expect("selection_rule.create 不存在 target_id -> 409", r[0] == 409 and "目标" in r[1].get("msg", ""), r[1])
r = call(client, "POST", "/api/v1/bre/trial_study/create", json={"trial_id": 99999999, "study_name": f"S{RUN}"})
expect("trial_study.create 不存在 trial_id -> 409", r[0] == 409 and "试验" in r[1].get("msg", ""), r[1])
r = call(client, "POST", "/api/v1/bre/pedigree/create", json={"combination_id": 99999999, "child_code": f"C{RUN}"})
expect("pedigree.create 不存在 combination_id -> 409", r[0] == 409, r[1])
print("\n=== 3) 2.5 跨基地迁移 + 2.3 唯一约束 ===")
siteA = call(client, "POST", "/api/v1/bre/site/create", json={"site_name": f"迁移A{RUN}"})[1]["data"]
siteB = call(client, "POST", "/api/v1/bre/site/create", json={"site_name": f"迁移B{RUN}"})[1]["data"]
plot = call(client, "POST", "/api/v1/bre/plot/create", json={"site_id": siteA["id"], "plot_code": f"P{RUN}"})
expect("plot.create", plot[0] == 200, plot[1])
plot_id = plot[1]["data"]["id"]
r = call(client, "POST", "/api/v1/bre/plot/create", json={"site_id": siteA["id"], "plot_code": f"P{RUN}"})
expect("同基地重码 plot_code -> 409", r[0] == 409, r[1])
r = call(client, "PUT", f"/api/v1/bre/plot/update/{plot_id}", json={"site_id": 88888888, "plot_code": f"P{RUN}"})
expect("plot 换到不存在基地 -> 409", r[0] == 409, r[1])
r = call(client, "PUT", f"/api/v1/bre/plot/update/{plot_id}", json={"site_id": siteB["id"], "plot_code": f"P{RUN}"})
expect("空 plot 换基地成功", r[0] == 200, r[1])
print("\n=== 4) 2.2 delete 下游校验 ===")
plot2 = call(client, "POST", "/api/v1/bre/plot/create", json={"site_id": siteA["id"], "plot_code": f"Q{RUN}"})
expect("plot2.create", plot2[0] == 200, plot2[1])
r = call(client, "DELETE", "/api/v1/bre/site/delete", json=[siteA["id"]])
expect("删有 plot 的 site -> 409", r[0] == 409, r[1])
r = call(client, "DELETE", "/api/v1/bre/plot/delete", json=[plot2[1]["data"]["id"]])
expect("删空 plot 成功", r[0] == 200, r[1])
r = call(client, "DELETE", "/api/v1/bre/site/delete", json=[siteA["id"]])
expect("清空后删 siteA 成功", r[0] == 200, r[1])
print("\n=== 5) 1.3 导入结构化结果 ImportResultSchema ===")
from openpyxl import Workbook
import io
headers5 = ["所属基地", "地块编号/名称", "行向", "行数", "每行株数", "株行距/网格说明", "地块面积(亩)"]
wb = Workbook(); ws = wb.active; ws.append(headers5)
ws.append([siteB["site_name"], f"IMPT{RUN}1", "东西行", 4, 5, "1.2m", 1.5]) # 合法行
ws.append(["不存在的基地XYZ", f"IMPT{RUN}2", "东西行", 2, 2, "", 0.5]) # 基地不存在
ws.append([siteB["site_name"], f"IMPT{RUN}3", "随便方向", 2, 2, "", 0.5]) # 行向非法
buf = io.BytesIO(); wb.save(buf); buf.seek(0)
imp = call(client, "POST", "/api/v1/bre/plot/import", files={
"file": ("import.xlsx", buf.getvalue(),
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")})
expect("import 返回 200", imp[0] == 200, imp[1])
d = imp[1].get("data", {})
expect("valid_count=1", d.get("valid_count") == 1, d)
expect("invalid_count=2", d.get("invalid_count") == 2, d)
expect("message_list 含坏行原因", len(d.get("message_list", [])) == 2 and
all("不存在" in m for m in d["message_list"]) and
any("基地名称" in m for m in d["message_list"]) and any("row_orientation" in m for m in d["message_list"]),
d)
# 重复导入走 update_support 更新(不报"已存在"
imp2 = call(client, "POST", "/api/v1/bre/plot/import", files={
"file": ("import.xlsx", buf.getvalue(),
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")})
expect("重复导入 valid_count=1update_support 更新)", imp2[0] == 200 and imp2[1].get("data", {}).get("valid_count") == 1, imp2[1])
print("\n=== 6) 1.2 导出带筛选参数 ===")
exp_r = client.post("/api/v1/bre/plot/export", headers=auth(),
params={"site_id": siteB["id"], "plot_code": f"IMPT{RUN}"})
expect("plot export(site_id + LIKE) 200/xlsx", exp_r.status_code == 200 and
"spreadsheetml" in exp_r.headers.get("content-type", ""), f"HTTP {exp_r.status_code}")
from openpyxl import load_workbook
wb6 = load_workbook(io.BytesIO(exp_r.content), read_only=True, data_only=True)
ws6 = wb6.active
rows6 = [[c.value for c in row] for row in ws6.iter_rows()]
wb6.close()
hdr6 = rows6[0] if rows6 else []
body6 = [r for r in rows6[1:] if any(v is not None for v in r)]
site_col = hdr6.index("所属基地") if "所属基地" in hdr6 else -1
expect("导出列含所属基地", site_col >= 0, hdr6)
expect("导出仅含匹配行(1条)", len(body6) == 1 and body6[0][site_col] == siteB["site_name"], body6)
exp_all = client.post("/api/v1/bre/plot/export", headers=auth(),
params={"site_id": siteB["id"]})
expect("导出全量 siteB 行数=2", exp_all.status_code == 200, str(exp_all.status_code))
wb7 = load_workbook(io.BytesIO(exp_all.content), read_only=True, data_only=True)
rows7 = [[c.value for c in row] for row in wb7.active.iter_rows()]
wb7.close()
body7 = [r for r in rows7[1:] if any(v is not None for v in r)]
expect("siteB 全部 2 行导出", len(body7) == 2, body7)
print("\n=== 清理 ===")
call(client, "DELETE", "/api/v1/bre/plot/delete", json=[plot_id])
for pc in (f"IMPT{RUN}1", f"IMPT{RUN}2", f"IMPT{RUN}3"):
rl = call(client, "GET", "/api/v1/bre/plot/list", params={"page": 1, "page_size": 50, "plot_code": pc})
ids = [x["id"] for x in rl[1]["data"]["items"]] if rl[0] == 200 else []
if ids:
call(client, "DELETE", "/api/v1/bre/plot/delete", json=ids)
call(client, "DELETE", "/api/v1/bre/site/delete", json=[siteB["id"]])
call(client, "DELETE", "/api/v1/bre/site/delete", json=[st[1]["data"]["id"]])
call(client, "DELETE", "/api/v1/bre/site/delete", json=[st2[1]["data"]["id"]])
call(client, "DELETE", "/api/v1/bre/personnel/delete", json=[per[1]["data"]["id"]])
print(f"\nALL {passed} CHECKS PASS \u2705")
if __name__ == "__main__":
main_()