200 lines
9.8 KiB
Python
200 lines
9.8 KiB
Python
"""波次6 桃育种领域需求对齐 聚焦验证(TestClient,无需起服务)。
|
|
|
|
运行:
|
|
cd d:\dpb\dpb\backend
|
|
C:\ai\miniconda3\envs\dpb\python.exe -X utf8 scripts/test_bre_wave6_tc.py
|
|
|
|
覆盖:
|
|
6.1 编号体系: combination_code 留空自动生成(YY+3位); tree_no 留空自动生成(组合-序号)
|
|
6.2 字典补灌: 规划附录 40 枚举全部落库(含 width 窄/中/宽)
|
|
6.3 v_trait_value 视图: 与 bre_trait_observation 聚合结果一致,可被统计查询消费
|
|
6.4 数据质量门禁: 数值观测越出 trait.valid_min/max -> 409; 区间内放行
|
|
6.5 状态机: tree.status / selection_result.is_selected 只能向前流转, 回退 409
|
|
"""
|
|
import os
|
|
os.environ["ENVIRONMENT"] = "dev"
|
|
os.environ["PYTHONUTF8"] = "1"
|
|
|
|
import re
|
|
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
|
|
|
|
|
|
# 规划附录 40 枚举: {dict_type: 期望最少值数}
|
|
APPENDIX_40 = {
|
|
"weather": 5, "level_3": 3, "level_4": 4, "level_5": 5, "depth": 3, "width": 3,
|
|
"brightness": 3, "conc_level": 3, "quality": 3, "pollination": 4, "seed_treatment": 3,
|
|
"identify_status": 6, "growth_vigor": 3, "growth_type": 3, "flower_type": 2,
|
|
"pollen_amount": 4, "fruit_load": 3, "maturity_uniform": 3, "fruit_shape": 5,
|
|
"fruit_apex": 5, "fruit_base_type": 3, "symmetry": 3, "peel_base_color": 5,
|
|
"blush_area": 5, "blush_pattern": 3, "peel_removal": 3, "flesh_color": 7,
|
|
"firmness": 5, "flavor": 7, "pit_adherence": 3, "pit_size": 3, "pit_shape": 5,
|
|
"eval_result": 3, "propagation": 3, "retrial_result": 2, "audit_type": 2,
|
|
"audit_status": 5, "annual_fee": 3, "blush_uniformity": 3, "peel_color_depth": 3,
|
|
}
|
|
|
|
|
|
def check_dict_appendix():
|
|
from sqlalchemy import create_engine, text
|
|
from app.config.setting import settings
|
|
eng = create_engine(settings.DB_URI)
|
|
with eng.connect() as c:
|
|
rows = c.execute(text(
|
|
"SELECT dict_type, count(*) FROM sys_dict_data "
|
|
"WHERE is_deleted = false GROUP BY dict_type"
|
|
)).all()
|
|
counts = dict(rows)
|
|
missing = [dt for dt, n in APPENDIX_40.items() if counts.get(dt, 0) < n]
|
|
return counts, missing
|
|
|
|
|
|
def check_view_consistency():
|
|
"""v_trait_value 聚合结果与 bre_trait_observation 表一致。"""
|
|
from sqlalchemy import create_engine, text
|
|
from app.config.setting import settings
|
|
eng = create_engine(settings.DB_URI)
|
|
q = ("SELECT trait_id, count(*), sum(value_numeric::float) "
|
|
"FROM {src} WHERE is_deleted = false AND value_numeric IS NOT NULL "
|
|
"GROUP BY trait_id ORDER BY trait_id")
|
|
with eng.connect() as c:
|
|
tbl = {r[0]: (r[1], r[2]) for r in c.execute(text(q.format(src="bre_trait_observation")))}
|
|
vw = {r[0]: (r[1], r[2]) for r in c.execute(text(q.format(src="v_trait_value")))}
|
|
return tbl == vw
|
|
|
|
|
|
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=== 6.2 字典: 规划附录 40 枚举完整性 ===")
|
|
counts, missing = check_dict_appendix()
|
|
expect("40 枚举全部落库(值数达标)", not missing, f"缺失/不足: {missing}")
|
|
|
|
print("\n=== 6.3 v_trait_value 视图与源表一致 ===")
|
|
expect("视图聚合 == bre_trait_observation 聚合", check_view_consistency())
|
|
|
|
print("\n=== 6.1 编号自动生成 ===")
|
|
tgt = call(client, "POST", "/api/v1/bre/target/create", json={"target_name": f"波次6目标{RUN}"})
|
|
expect("target.create", tgt[0] == 200, tgt[1])
|
|
tgt_id = tgt[1]["data"]["id"]
|
|
comb = call(client, "POST", "/api/v1/bre/cross_combination/create",
|
|
json={"cross_year": 2026, "bre_target_id": tgt_id, "remark": f"波次6{RUN}"})
|
|
expect("组合留空编号 create -> 200", comb[0] == 200, comb[1])
|
|
code = comb[1]["data"]["combination_code"]
|
|
expect(f"自动生成编号格式 YY+3位 ({code})", isinstance(code, str) and re.fullmatch(r"\d{5}", code), code)
|
|
comb_id = comb[1]["data"]["id"]
|
|
|
|
t1 = call(client, "POST", "/api/v1/bre/tree/create",
|
|
json={"combination_id": comb_id, "status": "alive", "remark": f"单株1{RUN}"})
|
|
expect("单株留空编号 create -> 200", t1[0] == 200, t1[1])
|
|
tn1 = t1[1]["data"]["tree_no"]
|
|
expect(f"自动生成单株编号 = 组合-01 ({tn1})", tn1 == f"{code}-01", tn1)
|
|
t2 = call(client, "POST", "/api/v1/bre/tree/create",
|
|
json={"combination_id": comb_id, "status": "alive", "remark": f"单株2{RUN}"})
|
|
expect("第二株自动编号 = 组合-02", t2[0] == 200 and t2[1]["data"]["tree_no"] == f"{code}-02", t2[1])
|
|
tree1_id = t1[1]["data"]["id"]
|
|
tree2_id = t2[1]["data"]["id"]
|
|
|
|
print("\n=== 6.4 数值门禁 valid_min/max ===")
|
|
trait = call(client, "POST", "/api/v1/bre/trait/create",
|
|
json={"trait_code": f"W6_{RUN}", "trait_name": f"波次6性状{RUN}",
|
|
"data_type": "numeric", "unit": "cm", "valid_min": 0, "valid_max": 10})
|
|
expect("trait.create(valid 0-10)", trait[0] == 200, trait[1])
|
|
trait_id = trait[1]["data"]["id"]
|
|
obs_in = call(client, "POST", "/api/v1/bre/trait_observation/create",
|
|
json={"combination_id": comb_id, "tree_id": tree1_id, "trait_id": trait_id,
|
|
"value_numeric": 5, "evaluate_year": 2026})
|
|
expect("区间内数值 5 放行", obs_in[0] == 200, obs_in[1])
|
|
obs_high = call(client, "POST", "/api/v1/bre/trait_observation/create",
|
|
json={"combination_id": comb_id, "tree_id": tree2_id, "trait_id": trait_id,
|
|
"value_numeric": 15, "evaluate_year": 2026})
|
|
expect("数值 15 > valid_max=10 -> 409", obs_high[0] == 409, obs_high[1])
|
|
obs_low = call(client, "POST", "/api/v1/bre/trait_observation/create",
|
|
json={"combination_id": comb_id, "tree_id": tree2_id, "trait_id": trait_id,
|
|
"value_numeric": -1, "evaluate_year": 2026})
|
|
expect("数值 -1 < valid_min=0 -> 409", obs_low[0] == 409, obs_low[1])
|
|
obs_in_id = obs_in[1]["data"]["id"]
|
|
upd = call(client, "PUT", f"/api/v1/bre/trait_observation/update/{obs_in_id}",
|
|
json={"value_numeric": 99})
|
|
expect("更新越界 -> 409", upd[0] == 409, upd[1])
|
|
upd2 = call(client, "PUT", f"/api/v1/bre/trait_observation/update/{obs_in_id}",
|
|
json={"value_numeric": 8})
|
|
expect("更新区间内 -> 200", upd2[0] == 200, upd2[1])
|
|
|
|
print("\n=== 6.5 状态机 单株状态 ===")
|
|
st_fwd = call(client, "PUT", f"/api/v1/bre/tree/update/{tree1_id}", json={"status": "selected"})
|
|
expect("存活->入选 放行", st_fwd[0] == 200, st_fwd[1])
|
|
st_bwd = call(client, "PUT", f"/api/v1/bre/tree/update/{tree1_id}", json={"status": "alive"})
|
|
expect("入选->存活 回退 409", st_bwd[0] == 409, st_bwd[1])
|
|
st_end = call(client, "PUT", f"/api/v1/bre/tree/update/{tree1_id}", json={"status": "eliminated"})
|
|
expect("入选->淘汰(向前跳转) 放行", st_end[0] == 200, st_end[1])
|
|
|
|
print("\n=== 6.5 状态机 选育结论 ===")
|
|
sr = call(client, "POST", "/api/v1/bre/selection_result/create",
|
|
json={"combination_id": comb_id, "tree_id": tree1_id, "is_selected": "selected",
|
|
"selection_year": 2026})
|
|
expect("选育结论=入选 create", sr[0] == 200, sr[1])
|
|
sr_id = sr[1]["data"]["id"]
|
|
sr_fwd = call(client, "PUT", f"/api/v1/bre/selection_result/update/{sr_id}", json={"is_selected": "primary"})
|
|
expect("入选->初选 放行", sr_fwd[0] == 200, sr_fwd[1])
|
|
sr_bwd = call(client, "PUT", f"/api/v1/bre/selection_result/update/{sr_id}", json={"is_selected": "unknown"})
|
|
expect("初选->未知 回退 409", sr_bwd[0] == 409, sr_bwd[1])
|
|
|
|
print("\n=== 清理 ===")
|
|
call(client, "DELETE", "/api/v1/bre/trait_observation/delete", json=[obs_in_id])
|
|
call(client, "DELETE", "/api/v1/bre/selection_result/delete", json=[sr_id])
|
|
call(client, "DELETE", "/api/v1/bre/tree/delete", json=[tree1_id, tree2_id])
|
|
call(client, "DELETE", "/api/v1/bre/cross_combination/delete", json=[comb_id])
|
|
call(client, "DELETE", "/api/v1/bre/trait/delete", json=[trait_id])
|
|
call(client, "DELETE", "/api/v1/bre/target/delete", json=[tgt_id])
|
|
print(f"\nALL {passed} CHECKS PASS \u2705")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main_()
|