Files
dpb/backend/tests/test_stat_analysis_tc.py
T
34047007@qq.com 0b50e0adfa checkpoint: SQLite 清理——业务/测试层统一 PostgreSQL(删 aiosqlite/fakeredis)
- setting: DATABASE_TYPE 收窄为 postgres,DB_URI/ASYNC_DB_URI 去 sqlite 分支
- database: create_async_engine_and_session 去 sqlite 分支(同步 psycopg 引擎保留给 jobstore)
- number_gen: 去 DATABASE_TYPE 非 postgres 早退(advisory lock 恒定走 PG)
- chat/crud: 删 SqliteDb 分支(agno 无硬依赖)
- pyproject/requirements/uv.lock: 删 aiosqlite、fakeredis;conftest 口径注释同步
- test_stat_analysis_tc: docstring 口径 SQLite→PG
2026-08-07 21:47:42 +08:00

175 lines
6.8 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.
# -*- coding: utf-8 -*-
"""统计引擎正式 tc 套件(ssGBLUP/OCS/MABC/孟德尔 + 补齐 5 套)的 pytest 包装层。
每套 tc = 一个独立脚本(backend/scripts/test_bre_analysis_*_tc.py),走真实 API
TestClient + 真实 PostgreSQL 16 + Redis),脚本自带 fixture 清理并 sys.exit(0/1)。
本包装层以 subprocess 顺序调用这些脚本,断言 rc==0 —— 与 run_regression.py 的执行契约一致。
关键点:
- 全部基于真实 PG16 + Redis,仅当探测到 PG/Redis 可达才跑,否则 skipif 跳过。
- 不 import 任何 app 模块(测试库生命周期由 conftest 管理,与统计引擎 tc 独立);subprocess env 显式构建:
清空可能残留的 DATABASE_*/REDIS_*/POOL_*/CAPTCHA_ENABLE 等连接变量,
置 ENVIRONMENT=dev / DATABASE_TYPE=postgres / PYTHONPATH=backend / PYTHONUTF8=1
再并入从 backend/env/.env.dev 解析的真实连接值(可被 TC_CI_* 覆盖,用于验证 skipif 路径)。
- 标记 pgpyproject 已注册),`pytest -m pg` 只跑本组;run_ci.py 的验收门即此命令。
运行:cd backend && C:/ai/miniconda3/envs/dpb/python.exe -X utf8 -m pytest -m pg -v
"""
import asyncio
import os
import re
import subprocess
import sys
from pathlib import Path
import pytest
BACKEND = Path(__file__).resolve().parents[1]
SCRIPTS = BACKEND / "scripts"
ENV_FILE = BACKEND / "env" / ".env.dev"
PY = os.environ.get("DPB_PYTHON", sys.executable)
# (套件名, 脚本文件名) —— 10 套:4 套既有 + 5 套补齐 + O2–O4 方法学广度
TC_SCRIPTS = [
("ssgblup", "test_bre_analysis_ssgblup_tc.py"),
("ocs", "test_bre_analysis_ocs_tc.py"),
("mabc", "test_bre_analysis_mabc_tc.py"),
("mendelian", "test_bre_analysis_mendelian_tc.py"),
("genetic_corr", "test_bre_analysis_genetic_corr_tc.py"),
("type_b_heredity", "test_bre_analysis_type_b_tc.py"),
("inbreeding_depression", "test_bre_analysis_inbreeding_tc.py"),
("trial_design", "test_bre_analysis_trial_design_tc.py"),
("stability", "test_bre_analysis_stability_tc.py"),
("o234", "test_bre_analysis_o234_tc.py"),
]
_TC_OVERRIDES = {
"db_host": "TC_CI_DB_HOST", "db_port": "TC_CI_DB_PORT", "db_user": "TC_CI_DB_USER",
"db_password": "TC_CI_DB_PASSWORD", "db_name": "TC_CI_DB_NAME",
"redis_host": "TC_CI_REDIS_HOST", "redis_port": "TC_CI_REDIS_PORT",
"redis_db": "TC_CI_REDIS_DB",
}
def _env_cfg() -> dict:
"""解析 backend/env/.env.dev(简单 key=value),返回真实连接配置,可被 TC_CI_* 覆盖。"""
cfg = {}
if ENV_FILE.exists():
for line in ENV_FILE.read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
k, v = line.split("=", 1)
cfg[k.strip()] = v.strip().strip("'\"")
for key, envvar in _TC_OVERRIDES.items():
if envvar in os.environ and os.environ[envvar] != "":
cfg[key] = os.environ[envvar]
return {
"db_host": cfg.get("DATABASE_HOST", "localhost"),
"db_port": int(cfg.get("DATABASE_PORT", "5432")),
"db_user": cfg.get("DATABASE_USER", "dpb"),
"db_password": cfg.get("DATABASE_PASSWORD", ""),
"db_name": cfg.get("DATABASE_NAME", "dpb"),
"redis_host": cfg.get("REDIS_HOST", "localhost"),
"redis_port": int(cfg.get("REDIS_PORT", "6379")),
"redis_db": int(cfg.get("REDIS_DB_NAME", "1")),
}
async def _probe() -> tuple[bool, bool, str]:
import asyncpg
import redis.asyncio as aioredis
cfg = _env_cfg()
db_ok, redis_ok = False, False
db_err, redis_err = "", ""
try:
conn = await asyncpg.connect(
host=cfg["db_host"], port=cfg["db_port"], user=cfg["db_user"],
password=cfg["db_password"] or None, database=cfg["db_name"],
timeout=5)
await conn.close()
db_ok = True
except Exception as exc: # noqa: BLE001
db_err = str(exc)[:100]
try:
# protocol=2 必须与 app.core.database.redis_connect 一致:本机 WSL redis-server 版本较老,
# 不支持 redis-py 7.x 默认 RESP3,需走 RESP2。
r = aioredis.Redis(host=cfg["redis_host"], port=cfg["redis_port"],
db=cfg["redis_db"], protocol=2, socket_connect_timeout=5)
await r.ping()
await r.aclose()
redis_ok = True
except Exception as exc: # noqa: BLE001
redis_err = str(exc)[:100]
detail = (f"PG={'ok' if db_ok else db_err} Redis={'ok' if redis_ok else redis_err}")
return db_ok, redis_ok, detail
_PG: tuple | None = None
def pg_ready() -> tuple[bool, bool, str]:
global _PG
if _PG is None:
_PG = asyncio.run(_probe())
return _PG
def _sub_env() -> dict:
"""显式构建子进程环境:清空可能残留的连接变量,置真实 PG/Redis 连接。"""
env = dict(os.environ)
for k in ("DATABASE_TYPE", "DATABASE_NAME", "POOL_SIZE", "MAX_OVERFLOW",
"CAPTCHA_ENABLE", "ENVIRONMENT", "PYTHONPATH", "PYTHONIOENCODING"):
env.pop(k, None)
cfg = _env_cfg()
env["ENVIRONMENT"] = "dev"
env["DATABASE_TYPE"] = "postgres"
env["PYTHONPATH"] = str(BACKEND)
env["PYTHONUTF8"] = "1"
env["PYTHONIOENCODING"] = "utf-8"
env["DATABASE_HOST"] = cfg["db_host"]
env["DATABASE_PORT"] = str(cfg["db_port"])
env["DATABASE_USER"] = cfg["db_user"]
env["DATABASE_PASSWORD"] = cfg["db_password"]
env["DATABASE_NAME"] = cfg["db_name"]
env["REDIS_HOST"] = cfg["redis_host"]
env["REDIS_PORT"] = str(cfg["redis_port"])
env["REDIS_DB_NAME"] = str(cfg["redis_db"])
return env
def _run_tc(name: str, script: Path) -> str:
"""顺序串跑单个 tc 脚本;返回 '' 表示通过,否则返回失败摘要(尾部日志)。"""
try:
proc = subprocess.run(
[PY, "-X", "utf8", str(script)],
cwd=str(SCRIPTS), env=_sub_env(), capture_output=True, text=True,
encoding="utf-8", errors="replace", timeout=1800)
except subprocess.TimeoutExpired:
return f"[{name}] 超时(>30min"
out = proc.stdout or ""
summary = ""
for line in out.splitlines():
if "===== " in line and "tc 套件" in line:
summary = line.strip()
break
if proc.returncode == 0:
print(f" -> {name}: EXIT=0 {summary}")
return ""
tail = "\n".join(out.splitlines()[-25:])
return f"[{name}] EXIT={proc.returncode} {summary}\n{tail}"
@pytest.mark.pg
@pytest.mark.parametrize("name,script", TC_SCRIPTS)
def test_analysis_tc(name: str, script: str):
db_ok, redis_ok, detail = pg_ready()
if not (db_ok and redis_ok):
pytest.skip(f"需要真实 PostgreSQL16 + Redis{detail}")
script_path = SCRIPTS / script
assert script_path.exists(), f"脚本缺失: {script_path}"
err = _run_tc(name, script_path)
assert err == "", f"\n{err}"