init: 初始化 dpb 桃育种系统代码库
前后端 + 后端 FastAPI 全量源码、部署脚本与文档。
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""本机 CI stage:验收门 = 真实 PostgreSQL16 + Redis 就绪 → `pytest -m pg` 全绿。
|
||||
|
||||
背景:全工程无 git 仓库(root/backend/frontend 均无 .git),GitHub Actions 是云端服务,
|
||||
本机用不上;CI stage 落地为本机驱动脚本。v8 验收标准「CI 跑通 pytest 全绿」= 本脚本 EXIT=0。
|
||||
|
||||
step 0 探测本机真实 PG16(backend/env/.env.dev 的 DATABASE_*,可用 TC_CI_DB_* 覆盖)+ Redis
|
||||
(REDIS_*)连通;任一失败给出明确指引并 exit 非 0。
|
||||
step 1 backend 目录下 `pytest -m pg`(dpb python + -X utf8)跑 9 套统计引擎正式 tc 套件
|
||||
(backend/tests/test_stat_analysis_tc.py 包装层,subprocess 调真实脚本)。
|
||||
step 2 汇总 EXIT=0 → 「CI 全绿」;日志写 Temp/claude/ci_logs/。
|
||||
|
||||
说明:
|
||||
- 全程真实 PG16 + Redis(本工程无 SQLite);PG/Redis 缺失时 step 0 拦截,不会误绿。
|
||||
- 与 run_regression.py 并存:run_regression = 手动全量回归(e2e 探针 + tc 套件);
|
||||
run_ci = 验收门(只跑 pytest -m pg 的 9 套正式 tc)。
|
||||
- 解释器默认 sys.executable(须用 dpb env python 运行);可用 DPB_PYTHON 覆盖。
|
||||
"""
|
||||
import asyncio
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
|
||||
ROOT = os.path.dirname(os.path.abspath(__file__))
|
||||
BACKEND = os.path.join(ROOT, "backend")
|
||||
ENV_FILE = os.path.join(BACKEND, "env", ".env.dev")
|
||||
LOG_DIR = os.path.join(ROOT, "Temp", "claude", "ci_logs")
|
||||
PY = os.environ.get("DPB_PYTHON", sys.executable)
|
||||
|
||||
|
||||
def _env_cfg() -> dict:
|
||||
cfg = {}
|
||||
if os.path.isfile(ENV_FILE):
|
||||
for line in open(ENV_FILE, encoding="utf-8").read().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 {
|
||||
"DATABASE_HOST": "TC_CI_DB_HOST", "DATABASE_PORT": "TC_CI_DB_PORT",
|
||||
"DATABASE_USER": "TC_CI_DB_USER", "DATABASE_PASSWORD": "TC_CI_DB_PASSWORD",
|
||||
"DATABASE_NAME": "TC_CI_DB_NAME", "REDIS_HOST": "TC_CI_REDIS_HOST",
|
||||
"REDIS_PORT": "TC_CI_REDIS_PORT", "REDIS_DB_NAME": "TC_CI_REDIS_DB",
|
||||
}.items():
|
||||
if envvar in os.environ and os.environ[envvar] != "":
|
||||
cfg[key] = os.environ[envvar]
|
||||
return {
|
||||
"DATABASE_HOST": cfg.get("DATABASE_HOST", "localhost"),
|
||||
"DATABASE_PORT": int(cfg.get("DATABASE_PORT", "5432")),
|
||||
"DATABASE_USER": cfg.get("DATABASE_USER", "dpb"),
|
||||
"DATABASE_PASSWORD": cfg.get("DATABASE_PASSWORD", ""),
|
||||
"DATABASE_NAME": cfg.get("DATABASE_NAME", "dpb"),
|
||||
"REDIS_HOST": cfg.get("REDIS_HOST", "localhost"),
|
||||
"REDIS_PORT": int(cfg.get("REDIS_PORT", "6379")),
|
||||
"REDIS_DB_NAME": int(cfg.get("REDIS_DB_NAME", "1")),
|
||||
}
|
||||
|
||||
|
||||
async def _probe(cfg: dict) -> tuple[bool, bool, str, str]:
|
||||
import asyncpg
|
||||
import redis.asyncio as aioredis
|
||||
|
||||
db_ok = redis_ok = False
|
||||
db_err = redis_err = ""
|
||||
try:
|
||||
conn = await asyncpg.connect(
|
||||
host=cfg["DATABASE_HOST"], port=cfg["DATABASE_PORT"], user=cfg["DATABASE_USER"],
|
||||
password=cfg["DATABASE_PASSWORD"] or None, database=cfg["DATABASE_NAME"], timeout=5)
|
||||
await conn.close()
|
||||
db_ok = True
|
||||
except Exception as exc: # noqa: BLE001
|
||||
db_err = str(exc)[:120]
|
||||
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_NAME"], 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)[:120]
|
||||
return db_ok, redis_ok, db_err, redis_err
|
||||
|
||||
|
||||
def main() -> int:
|
||||
print("=" * 60)
|
||||
print("本机 CI stage:pytest -m pg(10 套统计引擎正式 tc,真实 PG16 + Redis)")
|
||||
print("=" * 60)
|
||||
|
||||
# ---- step 0: 探测基础设施 ----
|
||||
cfg = _env_cfg()
|
||||
db_ok, redis_ok, db_err, redis_err = asyncio.run(_probe(cfg))
|
||||
print(f"[step 0] 探测 PostgreSQL16 "
|
||||
f"{cfg['DATABASE_HOST']}:{cfg['DATABASE_PORT']}/{cfg['DATABASE_NAME']} → "
|
||||
f"{'OK' if db_ok else 'FAIL ' + db_err}")
|
||||
print(f"[step 0] 探测 Redis {cfg['REDIS_HOST']}:{cfg['REDIS_PORT']}/{cfg['REDIS_DB_NAME']} → "
|
||||
f"{'OK' if redis_ok else 'FAIL ' + redis_err}")
|
||||
if not (db_ok and redis_ok):
|
||||
print("\n❌ 基础设施未就绪,CI 无法放行。请检查:")
|
||||
print(" · 本机 PostgreSQL 16 服务是否启动(服务名 postgresql-x64-16,端口 5432)")
|
||||
print(" · Redis 是否启动(redis-server,端口 6379;工程 WSL 内运行则确认 WSL 已起)")
|
||||
print(f" · 连接配置 backend/env/.env.dev 的 DATABASE_*/REDIS_* 是否与本地一致")
|
||||
print(" · 可用 TC_CI_DB_HOST / TC_CI_DB_PORT 等环境变量临时覆盖探测地址")
|
||||
return 1
|
||||
|
||||
# ---- step 1: pytest -m pg ----
|
||||
os.makedirs(LOG_DIR, exist_ok=True)
|
||||
log_path = os.path.join(LOG_DIR, "ci_pytest_mg.log")
|
||||
env = dict(os.environ)
|
||||
env.setdefault("ENVIRONMENT", "dev")
|
||||
env["PYTHONPATH"] = BACKEND
|
||||
env["PYTHONIOENCODING"] = "utf-8"
|
||||
cmd = [PY, "-X", "utf8", "-m", "pytest", "-m", "pg", "-v"]
|
||||
print(f"[step 1] {' '.join(cmd)} (cwd={BACKEND})")
|
||||
print(f" 日志 → {log_path}")
|
||||
with open(log_path, "w", encoding="utf-8") as log:
|
||||
rc = subprocess.call(cmd, cwd=BACKEND, env=env, stdout=log, stderr=subprocess.STDOUT)
|
||||
|
||||
# ---- step 2: 汇总 ----
|
||||
with open(log_path, encoding="utf-8") as log:
|
||||
tail = log.readlines()[-25:]
|
||||
for line in tail:
|
||||
print(" " + line.rstrip())
|
||||
print("=" * 60)
|
||||
if rc == 0:
|
||||
print("✅ CI 全绿:pytest -m pg 10 套统计引擎正式 tc 全部通过")
|
||||
return 0
|
||||
print(f"❌ CI 失败:pytest -m pg EXIT={rc}(完整输出见 {log_path})")
|
||||
return rc
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user