390 lines
20 KiB
Python
390 lines
20 KiB
Python
# -*- coding: utf-8 -*-
|
||||
|
|
"""孟德尔/亲本一致性检验正式 tc 套件:TestClient 走真实 API + 落库/隔离 golden 断言。
|
|||
|
|
|
|||
|
|
fixture(镜像 e2e_mendelian_persist):P1/P2 全 0/1 → T1、P3/P4 全 0/0 → T2(5 标记)。
|
|||
|
|
T1 全 0/0 → rate=1.0 兼容;T2 全 1/1 → 每标记 alien → rate=0.0 → flag。
|
|||
|
|
经真实 HTTP 端点 POST /api/v1/bre/genotype_call/mendelian/check/run 断言:
|
|||
|
|
[1] 返回 check_id / isolated=1 / cleared=0 + 计数 2/1/0/0
|
|||
|
|
[2] bre_mendelian_check 批次落库(阈值/计数/data_version/input_hash)
|
|||
|
|
[3] 子表 2 行:T1 rate=1.0 flag=false / T2 rate=0.0 flag=true incompatible=5
|
|||
|
|
[4] T2 进入隔离注册表 bre_pedigree_exclusion(source=mendelian / source_check_id)
|
|||
|
|
[5] 阈值 0.0 重跑 → flagged=0 / cleared=1 + 隔离行清除(可逆)
|
|||
|
|
[6] GET 只读预览零回归(无 check_id / 纯计算)
|
|||
|
|
依赖: Redis + PG 正常(TestClient 走真实 lifespan)。运行后自动清理。
|
|||
|
|
"""
|
|||
|
|
import os
|
|||
|
|
os.environ["ENVIRONMENT"] = "dev"
|
|||
|
|
os.environ["PYTHONUTF8"] = "1"
|
|||
|
|
|
|||
|
|
import sys, asyncio # noqa: E402
|
|||
|
|
sys.path.insert(0, r"d:\dpb\dpb\backend")
|
|||
|
|
|
|||
|
|
import main # noqa: E402
|
|||
|
|
from fastapi.testclient import TestClient # noqa: E402
|
|||
|
|
from sqlalchemy import delete, select # noqa: E402
|
|||
|
|
|
|||
|
|
from app.core.database import create_async_engine_and_session # noqa: E402
|
|||
|
|
from app.api.v1.module_system.user.model import UserModel # noqa: E402 (注册 mapper)
|
|||
|
|
from app.api.v1.module_bre.target.model import TargetModel # noqa: E402
|
|||
|
|
from app.api.v1.module_bre.trait.model import TraitModel # noqa: E402
|
|||
|
|
from app.api.v1.module_bre.germplasm.model import BreedingGermplasmModel # noqa: E402
|
|||
|
|
from app.api.v1.module_bre.cross_combination.model import CrossCombinationModel # noqa: E402
|
|||
|
|
from app.api.v1.module_bre.tree.model import TreeModel, TreePedigreeExclusionModel # noqa: E402
|
|||
|
|
from app.api.v1.module_bre.trait_observation.model import TraitObservationModel # noqa: E402
|
|||
|
|
from app.api.v1.module_bre.marker.model import MarkerModel # noqa: E402
|
|||
|
|
from app.api.v1.module_bre.genotype_dataset.model import GenotypingDatasetModel # noqa: E402
|
|||
|
|
from app.api.v1.module_bre.genotype_sample.model import GenotypeSampleModel # noqa: E402
|
|||
|
|
from app.api.v1.module_bre.genotype_call.model import ( # noqa: E402
|
|||
|
|
GenotypeCallModel,
|
|||
|
|
MendelianCheckModel,
|
|||
|
|
MendelianCheckTreeModel,
|
|||
|
|
)
|
|||
|
|
from app.api.v1.module_bre.genotype_call.service import GENOTYPE_DATA_VERSION # noqa: E402
|
|||
|
|
|
|||
|
|
create_app = main.create_app
|
|||
|
|
TOKEN = None
|
|||
|
|
ok, fail = 0, 0
|
|||
|
|
|
|||
|
|
PREFIX = "TC_MEND"
|
|||
|
|
tokens: dict[str, list[int]] = {
|
|||
|
|
"germ": [], "tree": [], "combo": [], "target": [],
|
|||
|
|
"marker": [], "dataset": [], "sample": [], "call": [],
|
|||
|
|
"obs": [],
|
|||
|
|
}
|
|||
|
|
check_ids: list[int] = []
|
|||
|
|
FIX: dict = {}
|
|||
|
|
|
|||
|
|
|
|||
|
|
def check(name, cond, detail=""):
|
|||
|
|
global ok, fail
|
|||
|
|
if cond:
|
|||
|
|
ok += 1
|
|||
|
|
print(f" [ok] {name} {detail}")
|
|||
|
|
else:
|
|||
|
|
fail += 1
|
|||
|
|
print(f" [FAIL] {name} {detail}")
|
|||
|
|
|
|||
|
|
|
|||
|
|
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}"}
|
|||
|
|
|
|||
|
|
|
|||
|
|
async def _build_fixture() -> None:
|
|||
|
|
engine, sf = create_async_engine_and_session()
|
|||
|
|
try:
|
|||
|
|
async with sf() as db:
|
|||
|
|
target = TargetModel(target_name=f"{PREFIX}-T", created_id=1)
|
|||
|
|
db.add(target)
|
|||
|
|
await db.flush()
|
|||
|
|
tokens["target"].append(target.id)
|
|||
|
|
trait = TraitModel(trait_code=f"tM_{PREFIX}", trait_name=f"果径{PREFIX}",
|
|||
|
|
data_type="numeric", unit="mm", is_core="1", direction="desc",
|
|||
|
|
into_ebv="1", default_h2=0.5, created_id=1)
|
|||
|
|
db.add(trait)
|
|||
|
|
await db.flush()
|
|||
|
|
P1 = BreedingGermplasmModel(cultivar_name=f"{PREFIX}-P1", can_be_female=True, created_id=1)
|
|||
|
|
P2 = BreedingGermplasmModel(cultivar_name=f"{PREFIX}-P2", can_be_male=True, created_id=1)
|
|||
|
|
P3 = BreedingGermplasmModel(cultivar_name=f"{PREFIX}-P3", can_be_female=True, created_id=1)
|
|||
|
|
P4 = BreedingGermplasmModel(cultivar_name=f"{PREFIX}-P4", can_be_male=True, created_id=1)
|
|||
|
|
db.add_all([P1, P2, P3, P4])
|
|||
|
|
await db.flush()
|
|||
|
|
tokens["germ"] += [P1.id, P2.id, P3.id, P4.id]
|
|||
|
|
C0 = CrossCombinationModel(combination_code=f"{PREFIX}-C0", bre_target_id=target.id,
|
|||
|
|
female_parent_id=P1.id, male_parent_id=P2.id,
|
|||
|
|
design_type="full_diallel", created_id=1)
|
|||
|
|
C1 = CrossCombinationModel(combination_code=f"{PREFIX}-C1", bre_target_id=target.id,
|
|||
|
|
female_parent_id=P3.id, male_parent_id=P4.id,
|
|||
|
|
design_type="full_diallel", created_id=1)
|
|||
|
|
db.add_all([C0, C1])
|
|||
|
|
await db.flush()
|
|||
|
|
tokens["combo"] += [C0.id, C1.id]
|
|||
|
|
T1 = TreeModel(combination_id=C0.id, tree_no=f"{PREFIX}-T1", status="alive",
|
|||
|
|
stage="evaluation", generation="F1", dam_id=P1.id, sire_id=P2.id, created_id=1)
|
|||
|
|
T2 = TreeModel(combination_id=C1.id, tree_no=f"{PREFIX}-T2", status="alive",
|
|||
|
|
stage="evaluation", generation="F1", dam_id=P3.id, sire_id=P4.id, created_id=1)
|
|||
|
|
db.add_all([T1, T2])
|
|||
|
|
await db.flush()
|
|||
|
|
tokens["tree"] += [T1.id, T2.id]
|
|||
|
|
FIX.update({"T1": T1.id, "T2": T2.id, "P1": P1.id, "P2": P2.id,
|
|||
|
|
"P3": P3.id, "P4": P4.id})
|
|||
|
|
o1 = TraitObservationModel(tree_id=T1.id, combination_id=C0.id, trait_id=trait.id,
|
|||
|
|
value_numeric=25.0, evaluate_year=2025, created_id=1)
|
|||
|
|
o2 = TraitObservationModel(tree_id=T2.id, combination_id=C1.id, trait_id=trait.id,
|
|||
|
|
value_numeric=20.0, evaluate_year=2025, created_id=1)
|
|||
|
|
db.add_all([o1, o2])
|
|||
|
|
await db.flush()
|
|||
|
|
tokens["obs"] += [o1.id, o2.id]
|
|||
|
|
markers = []
|
|||
|
|
for j in range(5):
|
|||
|
|
m = MarkerModel(marker_name=f"{PREFIX}-M{j + 1}", marker_type="SNP",
|
|||
|
|
chromosome=str(j), position=j * 100, created_id=1)
|
|||
|
|
db.add(m)
|
|||
|
|
markers.append(m)
|
|||
|
|
await db.flush()
|
|||
|
|
tokens["marker"] = [m.id for m in markers]
|
|||
|
|
mids = [m.id for m in markers]
|
|||
|
|
ds = GenotypingDatasetModel(dataset_name=f"{PREFIX}-DS", platform="SNP",
|
|||
|
|
purpose="MD", created_id=1)
|
|||
|
|
db.add(ds)
|
|||
|
|
await db.flush()
|
|||
|
|
tokens["dataset"].append(ds.id)
|
|||
|
|
FIX["dataset_id"] = ds.id
|
|||
|
|
|
|||
|
|
def mk_sample(ds_id, no, source_type, source_id):
|
|||
|
|
s = GenotypeSampleModel(sample_name=no, dataset_id=ds_id,
|
|||
|
|
source_type=source_type, source_id=source_id, created_id=1)
|
|||
|
|
db.add(s)
|
|||
|
|
return s
|
|||
|
|
|
|||
|
|
samp = [
|
|||
|
|
mk_sample(ds.id, f"{PREFIX}-P1-S", "germplasm", P1.id),
|
|||
|
|
mk_sample(ds.id, f"{PREFIX}-P2-S", "germplasm", P2.id),
|
|||
|
|
mk_sample(ds.id, f"{PREFIX}-P3-S", "germplasm", P3.id),
|
|||
|
|
mk_sample(ds.id, f"{PREFIX}-P4-S", "germplasm", P4.id),
|
|||
|
|
mk_sample(ds.id, f"{PREFIX}-T1-S", "tree", T1.id),
|
|||
|
|
mk_sample(ds.id, f"{PREFIX}-T2-S", "tree", T2.id),
|
|||
|
|
]
|
|||
|
|
await db.flush()
|
|||
|
|
tokens["sample"] = [s.id for s in samp]
|
|||
|
|
calls = []
|
|||
|
|
gt_map = {"P1": "0/1", "P2": "0/1", "P3": "0/0", "P4": "0/0", "T1": "0/0", "T2": "1/1"}
|
|||
|
|
for name, s in zip(("P1", "P2", "P3", "P4", "T1", "T2"), samp):
|
|||
|
|
for mid in mids:
|
|||
|
|
calls.append(GenotypeCallModel(sample_id=s.id, marker_id=mid,
|
|||
|
|
allele=gt_map[name], created_id=1))
|
|||
|
|
db.add_all(calls)
|
|||
|
|
await db.flush()
|
|||
|
|
tokens["call"] = [c.id for c in calls]
|
|||
|
|
await db.commit()
|
|||
|
|
finally:
|
|||
|
|
await engine.dispose()
|
|||
|
|
|
|||
|
|
|
|||
|
|
async def _verify_and_cleanup() -> None:
|
|||
|
|
engine, sf = create_async_engine_and_session()
|
|||
|
|
try:
|
|||
|
|
async with sf() as db:
|
|||
|
|
r = FIX["run1"]
|
|||
|
|
# ---- [2] 批次落库 ----
|
|||
|
|
crow = await db.get(MendelianCheckModel, r["check_id"])
|
|||
|
|
check("[2] bre_mendelian_check 落库(阈值/计数/data_version/input_hash)",
|
|||
|
|
crow is not None and float(crow.compat_threshold) == 0.98
|
|||
|
|
and crow.trees_checked == 2 and crow.trees_flagged == 1
|
|||
|
|
and crow.data_version == GENOTYPE_DATA_VERSION and crow.input_hash,
|
|||
|
|
f"v={crow.data_version if crow else None}")
|
|||
|
|
# ---- [3] 子表 ----
|
|||
|
|
childs = (await db.execute(select(MendelianCheckTreeModel).where(
|
|||
|
|
MendelianCheckTreeModel.check_id == r["check_id"]))).scalars().all()
|
|||
|
|
child_by_tree = {int(c.tree_id): c for c in childs}
|
|||
|
|
check("[3] 子表 2 行:T1 rate=1.0 flag=false / T2 rate=0.0 flag=true incompatible=5",
|
|||
|
|
len(childs) == 2
|
|||
|
|
and float(child_by_tree[FIX["T1"]].compatibility_rate) == 1.0
|
|||
|
|
and not child_by_tree[FIX["T1"]].flag
|
|||
|
|
and float(child_by_tree[FIX["T2"]].compatibility_rate) == 0.0
|
|||
|
|
and child_by_tree[FIX["T2"]].flag
|
|||
|
|
and child_by_tree[FIX["T2"]].status == "checked"
|
|||
|
|
and child_by_tree[FIX["T2"]].incompatible == 5,
|
|||
|
|
f"T1={child_by_tree[FIX['T1']].compatibility_rate}/{child_by_tree[FIX['T1']].flag} "
|
|||
|
|
f"T2={child_by_tree[FIX['T2']].compatibility_rate}/{child_by_tree[FIX['T2']].flag}")
|
|||
|
|
# ---- [4] 隔离注册表在 main_ 中 run1 后立即验(run2 会清除),此处只验 [5] ----
|
|||
|
|
# ---- [5] 可逆:隔离行清除 ----
|
|||
|
|
if FIX.get("run2"):
|
|||
|
|
excl2 = (await db.execute(select(TreePedigreeExclusionModel).where(
|
|||
|
|
TreePedigreeExclusionModel.is_deleted.is_(False),
|
|||
|
|
TreePedigreeExclusionModel.tree_id == FIX["T2"]))).scalars().all()
|
|||
|
|
check("[5] 阈值 0.0 后 T2 隔离行已清除", len(excl2) == 0, f"n={len(excl2)}")
|
|||
|
|
|
|||
|
|
# ---- cleanup ----
|
|||
|
|
if tokens["tree"]:
|
|||
|
|
await db.execute(delete(TreePedigreeExclusionModel).where(
|
|||
|
|
TreePedigreeExclusionModel.tree_id.in_(tokens["tree"])))
|
|||
|
|
if check_ids:
|
|||
|
|
await db.execute(delete(MendelianCheckTreeModel).where(
|
|||
|
|
MendelianCheckTreeModel.check_id.in_(check_ids)))
|
|||
|
|
await db.execute(delete(MendelianCheckModel).where(
|
|||
|
|
MendelianCheckModel.id.in_(check_ids)))
|
|||
|
|
if tokens["call"]:
|
|||
|
|
await db.execute(delete(GenotypeCallModel).where(
|
|||
|
|
GenotypeCallModel.id.in_(tokens["call"])))
|
|||
|
|
if tokens["sample"]:
|
|||
|
|
await db.execute(delete(GenotypeSampleModel).where(
|
|||
|
|
GenotypeSampleModel.id.in_(tokens["sample"])))
|
|||
|
|
if tokens["marker"]:
|
|||
|
|
await db.execute(delete(MarkerModel).where(MarkerModel.id.in_(tokens["marker"])))
|
|||
|
|
if tokens["dataset"]:
|
|||
|
|
await db.execute(delete(GenotypingDatasetModel).where(
|
|||
|
|
GenotypingDatasetModel.id.in_(tokens["dataset"])))
|
|||
|
|
if tokens["obs"]:
|
|||
|
|
await db.execute(delete(TraitObservationModel).where(
|
|||
|
|
TraitObservationModel.id.in_(tokens["obs"])))
|
|||
|
|
if tokens["tree"]:
|
|||
|
|
await db.execute(delete(TreeModel).where(TreeModel.id.in_(tokens["tree"])))
|
|||
|
|
if tokens["combo"]:
|
|||
|
|
await db.execute(delete(CrossCombinationModel).where(
|
|||
|
|
CrossCombinationModel.id.in_(tokens["combo"])))
|
|||
|
|
if tokens["germ"]:
|
|||
|
|
await db.execute(delete(BreedingGermplasmModel).where(
|
|||
|
|
BreedingGermplasmModel.id.in_(tokens["germ"])))
|
|||
|
|
await db.execute(delete(TraitModel).where(TraitModel.trait_code == f"tM_{PREFIX}"))
|
|||
|
|
if tokens["target"]:
|
|||
|
|
await db.execute(delete(TargetModel).where(TargetModel.id.in_(tokens["target"])))
|
|||
|
|
await db.commit()
|
|||
|
|
print(f"[cleanup] 孟德尔 tc 数据已清(check={len(check_ids)})")
|
|||
|
|
finally:
|
|||
|
|
await engine.dispose()
|
|||
|
|
|
|||
|
|
|
|||
|
|
async def _check_isolated(check_id: int) -> None:
|
|||
|
|
"""run1 落库后立即查隔离注册表(必须在 run2 清除之前验)。"""
|
|||
|
|
engine, sf = create_async_engine_and_session()
|
|||
|
|
try:
|
|||
|
|
async with sf() as db:
|
|||
|
|
excl = (await db.execute(select(TreePedigreeExclusionModel).where(
|
|||
|
|
TreePedigreeExclusionModel.is_deleted.is_(False),
|
|||
|
|
TreePedigreeExclusionModel.tree_id == FIX["T2"]))).scalars().all()
|
|||
|
|
check("[4] T2 进入隔离注册表(source=mendelian / source_check_id)",
|
|||
|
|
len(excl) == 1 and excl[0].source_type == "mendelian"
|
|||
|
|
and excl[0].source_check_id == check_id, f"n={len(excl)}")
|
|||
|
|
finally:
|
|||
|
|
await engine.dispose()
|
|||
|
|
|
|||
|
|
|
|||
|
|
async def _wipe() -> None:
|
|||
|
|
"""启动前清理本前缀残留(防上次进程被杀/建 fixture 中途失败留下的脏数据)。"""
|
|||
|
|
engine, sf = create_async_engine_and_session()
|
|||
|
|
try:
|
|||
|
|
async with sf() as db:
|
|||
|
|
ds_ids = list((await db.execute(select(GenotypingDatasetModel.id).where(
|
|||
|
|
GenotypingDatasetModel.dataset_name.like(f"{PREFIX}-%")))).scalars())
|
|||
|
|
tree_ids = list((await db.execute(select(TreeModel.id).where(
|
|||
|
|
TreeModel.tree_no.like(f"{PREFIX}-%")))).scalars())
|
|||
|
|
if tree_ids:
|
|||
|
|
await db.execute(delete(TreePedigreeExclusionModel).where(
|
|||
|
|
TreePedigreeExclusionModel.tree_id.in_(tree_ids)))
|
|||
|
|
check_ids = list((await db.execute(select(MendelianCheckModel.id).where(
|
|||
|
|
MendelianCheckModel.dataset_id.in_(ds_ids)))).scalars())
|
|||
|
|
if check_ids:
|
|||
|
|
await db.execute(delete(MendelianCheckTreeModel).where(
|
|||
|
|
MendelianCheckTreeModel.check_id.in_(check_ids)))
|
|||
|
|
await db.execute(delete(MendelianCheckModel).where(
|
|||
|
|
MendelianCheckModel.id.in_(check_ids)))
|
|||
|
|
if ds_ids:
|
|||
|
|
sample_ids = list((await db.execute(select(GenotypeSampleModel.id).where(
|
|||
|
|
GenotypeSampleModel.dataset_id.in_(ds_ids)))).scalars())
|
|||
|
|
if sample_ids:
|
|||
|
|
await db.execute(delete(GenotypeCallModel).where(
|
|||
|
|
GenotypeCallModel.sample_id.in_(sample_ids)))
|
|||
|
|
await db.execute(delete(GenotypeSampleModel).where(
|
|||
|
|
GenotypeSampleModel.id.in_(sample_ids)))
|
|||
|
|
await db.execute(delete(GenotypingDatasetModel).where(
|
|||
|
|
GenotypingDatasetModel.id.in_(ds_ids)))
|
|||
|
|
if tree_ids:
|
|||
|
|
obs_ids = list((await db.execute(select(TraitObservationModel.id).where(
|
|||
|
|
TraitObservationModel.tree_id.in_(tree_ids)))).scalars())
|
|||
|
|
if obs_ids:
|
|||
|
|
await db.execute(delete(TraitObservationModel).where(
|
|||
|
|
TraitObservationModel.id.in_(obs_ids)))
|
|||
|
|
await db.execute(delete(TreeModel).where(TreeModel.id.in_(tree_ids)))
|
|||
|
|
marker_ids = list((await db.execute(select(MarkerModel.id).where(
|
|||
|
|
MarkerModel.marker_name.like(f"{PREFIX}-%")))).scalars())
|
|||
|
|
if marker_ids:
|
|||
|
|
await db.execute(delete(MarkerModel).where(MarkerModel.id.in_(marker_ids)))
|
|||
|
|
combo_ids = list((await db.execute(select(CrossCombinationModel.id).where(
|
|||
|
|
CrossCombinationModel.combination_code.like(f"{PREFIX}-%")))).scalars())
|
|||
|
|
if combo_ids:
|
|||
|
|
await db.execute(delete(CrossCombinationModel).where(
|
|||
|
|
CrossCombinationModel.id.in_(combo_ids)))
|
|||
|
|
germ_ids = list((await db.execute(select(BreedingGermplasmModel.id).where(
|
|||
|
|
BreedingGermplasmModel.cultivar_name.like(f"{PREFIX}-%")))).scalars())
|
|||
|
|
if germ_ids:
|
|||
|
|
await db.execute(delete(BreedingGermplasmModel).where(
|
|||
|
|
BreedingGermplasmModel.id.in_(germ_ids)))
|
|||
|
|
await db.execute(delete(TraitModel).where(TraitModel.trait_code == f"tM_{PREFIX}"))
|
|||
|
|
await db.execute(delete(TargetModel).where(
|
|||
|
|
TargetModel.target_name.like(f"{PREFIX}-%")))
|
|||
|
|
await db.commit()
|
|||
|
|
print(f"[preclean] 孟德尔 tc 前缀残留已清(ds={len(ds_ids)} tree={len(tree_ids)})")
|
|||
|
|
finally:
|
|||
|
|
await engine.dispose()
|
|||
|
|
|
|||
|
|
|
|||
|
|
def main_() -> None:
|
|||
|
|
asyncio.run(_wipe())
|
|||
|
|
asyncio.run(_build_fixture())
|
|||
|
|
try:
|
|||
|
|
with TestClient(create_app()) as client:
|
|||
|
|
login(client)
|
|||
|
|
H = auth()
|
|||
|
|
# ---- [1] POST run ----
|
|||
|
|
r = client.post("/api/v1/bre/genotype_call/mendelian/check/run",
|
|||
|
|
json={"dataset_id": FIX["dataset_id"], "compat_threshold": 0.98},
|
|||
|
|
headers=H)
|
|||
|
|
check("[HTTP] mendelian/check/run 200", r.status_code == 200,
|
|||
|
|
f"{r.status_code} {str(r.text)[:150]}")
|
|||
|
|
b = r.json()
|
|||
|
|
data = b.get("data") if b.get("code") == 0 else None
|
|||
|
|
check("[1] 返回 check_id / isolated=1 / cleared=0", data is not None
|
|||
|
|
and isinstance(data["check_id"], int) and data["isolated"] == 1
|
|||
|
|
and data["cleared"] == 0, f"{data}")
|
|||
|
|
if data:
|
|||
|
|
FIX["run1"] = data
|
|||
|
|
check_ids.append(data["check_id"])
|
|||
|
|
check("[1] 汇总计数 checked=2 / flagged=1 / no_parents=0 / missing=0",
|
|||
|
|
data["trees_checked"] == 2 and data["trees_flagged"] == 1
|
|||
|
|
and data["trees_no_parents"] == 0
|
|||
|
|
and data["trees_missing_parent_genotype"] == 0,
|
|||
|
|
f"{data['trees_checked']}/{data['trees_flagged']}")
|
|||
|
|
# ---- [4] 隔离注册表:run1 刚落库即验(run2 阈值 0.0 会清除)----
|
|||
|
|
asyncio.run(_check_isolated(FIX["run1"]["check_id"]))
|
|||
|
|
|
|||
|
|
# ---- [5] 阈值 0.0 重跑(可逆)----
|
|||
|
|
r2 = client.post("/api/v1/bre/genotype_call/mendelian/check/run",
|
|||
|
|
json={"dataset_id": FIX["dataset_id"], "compat_threshold": 0.0},
|
|||
|
|
headers=H)
|
|||
|
|
b2 = r2.json()
|
|||
|
|
d2 = b2.get("data") if r2.status_code == 200 and b2.get("code") == 0 else None
|
|||
|
|
check("[5] 阈值 0.0:flagged=0 / isolated=0 / cleared=1",
|
|||
|
|
d2 is not None and d2["trees_flagged"] == 0
|
|||
|
|
and d2["isolated"] == 0 and d2["cleared"] == 1, f"{d2}")
|
|||
|
|
if d2:
|
|||
|
|
FIX["run2"] = d2
|
|||
|
|
check_ids.append(d2["check_id"])
|
|||
|
|
|
|||
|
|
# ---- [6] GET 只读预览零回归 ----
|
|||
|
|
g = client.get("/api/v1/bre/genotype_call/mendelian/check",
|
|||
|
|
params={"dataset_id": FIX["dataset_id"], "compat_threshold": 0.98},
|
|||
|
|
headers=H)
|
|||
|
|
gb = g.json()
|
|||
|
|
gd = gb.get("data") if g.status_code == 200 and gb.get("code") == 0 else None
|
|||
|
|
check("[6] GET 只读预览原结构(无 check_id / 纯计算)",
|
|||
|
|
gd is not None and "check_id" not in gd
|
|||
|
|
and gd["trees_flagged"] == 1 and len(gd["trees"]) == 2
|
|||
|
|
and gd["summary"]["incompatible_markers"] == 5,
|
|||
|
|
f"flagged={gd and gd['trees_flagged']}")
|
|||
|
|
finally:
|
|||
|
|
asyncio.run(_verify_and_cleanup())
|
|||
|
|
|
|||
|
|
print(f"\n===== 孟德尔 tc 套件:ok={ok} fail={fail} =====")
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
main_()
|
|||
|
|
sys.exit(1 if fail else 0)
|