- 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
175 lines
6.8 KiB
Python
175 lines
6.8 KiB
Python
# -*- 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 路径)。
|
||
- 标记 pg(pyproject 已注册),`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}"
|