Files
34047007@qq.com 86126ea16f checkpoint: 架构治理四件套——统一 async / 测试真 PG / Redis 降级可选 / DPB 扶正
- A1 业务层统一 async:gencode 表内省改 run_sync 异步(不再阻塞事件循环)、
  check_db 改 async_engine、dict_util 启动预热异步载入;psycopg 降为
  APScheduler SQLAlchemyJobStore 专用同步孤岛(注释标注)
- A2 测试真 PG 化:conftest 弃 SQLite + mock Redis,改连真实 PG16(dpb_test)+
  Redis(number_gen advisory lock 路径真被测);新增 docker/test-compose.yaml
  测试依赖栈(postgres:16:5433 + redis:7:6380);run_ci 前置探测 + 容器兜底,
  pytest -m pg 10 套正式 tc 全过(真实 PG16 + Redis)
- B1 Redis 启动不强依赖:redis_connect 失败降级启动,缓存类回源 DB、存储类
  (会话/AI 配置/调度)经 require_redis 守卫返回 503;真机 3 阶段降级测试
  PASS=11 FAIL=0(独立 Redis 6390 + 后端 8091,不动共享 dev 服务)
- C 扶正 DPB:后端横幅/日志(dpb.log)/README/pyproject/ai_factory agent 名、
  前端 package.json/署名注释/链接文案/deploy.sh/docker 注释全部去 fastapiadmin;
  grep backend/app + frontend/web/src 零残留
- 验证:全量回归 pass=72 fail=0
2026-08-07 09:06:19 +08:00

188 lines
8.5 KiB
Python
Raw Permalink 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 -*-
"""本机 CI stage:验收门 = 真实 PostgreSQL16 + Redis 就绪 → `pytest -m pg` 全绿。
背景:工程已纳入 gitGitea gonsun/dpb2026-08-06 起);CI stage 落地为本机驱动脚本。
v8 验收标准「CI 跑通 pytest 全绿」= 本脚本 EXIT=0。
step 0 探测真实 PG16backend/env/.env.dev 的 DATABASE_*,可用 TC_CI_DB_* 覆盖)+ Redis
REDIS_*)连通。本机 PG/Redis 未就绪且 docker 可用时,自动
`docker compose -f docker/test-compose.yaml up -d` 拉起测试依赖栈
postgres:16:5433 + redis:7:6380,预建 dpb_test)并注入 TC_CI_* 覆盖后重探。
仍失败给出明确指引并 exit 非 0。
step 1 backend 目录下 `pytest -m pg`dpb python + -X utf8)跑 10 套统计引擎正式 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 的 10 套正式 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")),
}
def _maybe_start_compose() -> bool:
"""本机 PG/Redis 未就绪时,用 docker compose 拉起测试依赖栈(postgres:16 + redis)。
测试栈端口与宿主机常驻服务错开(5433/6380),预建测试库 dpb_test。
返回是否成功拉起(docker 缺失/失败返回 False,交由 step 0 给出指引)。
"""
import shutil
if shutil.which("docker") is None:
return False
compose = os.path.join(ROOT, "docker", "test-compose.yaml")
try:
subprocess.run(
["docker", "compose", "-f", compose, "up", "-d"],
cwd=ROOT, capture_output=True, timeout=120, check=True,
)
return True
except Exception as exc: # noqa: BLE001
print(f" [warn] docker compose 拉起测试依赖失败: {str(exc)[:120]}")
return False
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 stagepytest -m pg10 套统计引擎正式 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[step 0] 本机 PG/Redis 未就绪,尝试 docker compose 拉起测试依赖栈…")
if _maybe_start_compose():
# 测试栈端口与常驻服务错开(5433/6380),预建 dpb_test;注入 TC_CI_* 覆盖
os.environ.update({
"TC_CI_DB_HOST": "127.0.0.1", "TC_CI_DB_PORT": "5433",
"TC_CI_DB_USER": "dpb", "TC_CI_DB_PASSWORD": "dpb",
"TC_CI_DB_NAME": "dpb_test",
"TC_CI_REDIS_HOST": "127.0.0.1", "TC_CI_REDIS_PORT": "6380",
"TC_CI_REDIS_DB": "1",
})
cfg = _env_cfg()
# postgres:16 首次初始化可能需数十秒,轮询等健康
for _ in range(15):
db_ok, redis_ok, db_err, redis_err = asyncio.run(_probe(cfg))
if db_ok and redis_ok:
break
import time
time.sleep(5)
print(f"[step 0] 容器 PG {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(" · 或安装 docker 后由本脚本自动拉起 docker/test-compose.yaml 测试依赖栈")
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())