From e9baafc0d72b74d0b0dfb0497f07b5f21e232f63 Mon Sep 17 00:00:00 2001 From: "agent.develop" Date: Tue, 22 Sep 2026 21:16:13 +0800 Subject: [PATCH] =?UTF-8?q?deliver:=20=E4=BA=A4=E4=BB=98=E6=94=B6=E5=8F=A3?= =?UTF-8?q?=EF=BC=88=E5=BC=95=E6=93=8E=E4=BB=A3=E4=B8=BA=E6=8F=90=E4=BA=A4?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- pyproject.toml | 13 +++--- requirements-dev.txt | 13 ++++++ scripts/run_tests.sh | 84 +++++++++++++++++++++++++++++++++++++++ scripts/setup_test_env.sh | 54 +++++++++++++++++++++++++ 5 files changed, 159 insertions(+), 7 deletions(-) create mode 100644 requirements-dev.txt create mode 100755 scripts/run_tests.sh create mode 100755 scripts/setup_test_env.sh diff --git a/README.md b/README.md index 5234ec6..6284551 100644 --- a/README.md +++ b/README.md @@ -204,7 +204,7 @@ python3 scripts/selfcheck_m5a.py > | V2 | `tests/m5a_live_db_harness.py` 崩溃版特征(`test123` / `args or {}` / `params or {}` / `dict(params or {})`)0 命中 + `ast.parse` 通过 | > | V3 | `projects/{项目}/env/test.json` 的 `db.sandbox` 段齐备(`scope=sandbox_only` + `sandbox_schema` + host/port/user/password)——凭据唯一事实源在 env,代码内零明文 | > | V4 | `docs/02-develop` 三份运行证据日志齐备,harness 日志含 C-3.1~C-3.4 全 `[PASS]` 与 IDM.6 的 `1062 Duplicate entry ... uk_ev_dedup` 真防重原文,且无裸 `Traceback` | -> | V5 | 工作空间根一次性产物卫生:`tmp_*.py` / `_scratch_*.py` / `*_patch.py` / `*_out.txt` / `*_ev.txt` / `r.txt` / `*.bak` / 根级临时目录全类扫描。**M5a 链路自身产物属反向门禁、不许进白名单,磁盘上一经出现即 FAIL**:`M5A_OWN_DUMP_GONE = ('.scratch_m5a', 'closure_out.txt', 'evidence_raw.txt', 'r1.txt', 'r2.txt')`——收口时其内 git 证据先按字节原样迁到 `projects/pbls/docs/02-develop/m5a-git-closure-evidence-raw.txt`(sha256 一致)再整目录删除,证据不丢、根上不留转储。他链产物必须在 `WS_KEPT` 白名单里**逐项写明引用它的文档路径**,否则 FAIL | +> | V5 | 工作空间根一次性产物卫生:`tmp_*.py` / `_scratch_*.py` / `*_patch.py` / `*_out.txt` / `*_ev.txt` / `r.txt` / `*.bak` / 根级临时目录全类扫描。**M5a 链路自身产物属反向门禁、不许进白名单,磁盘上一经出现即 FAIL**:`M5A_OWN_DUMP_GONE = ('.scratch_m5a', 'closure_out.txt', 'evidence_raw.txt', 'r1.txt', 'r2.txt')`——收口时其内 git 证据先按字节原样迁到 `projects/pbls/docs/02-develop/m5a-git-closure-evidence-raw.txt`(sha256 一致)再整目录删除,证据不丢、根上不留转储。他链产物必须在 `WS_KEPT` 白名单里**逐项写明引用它的文档路径**,否则 FAIL。另有 5.1b 防洗白回归门禁:`WS_KEPT` 中一旦出现名字含 `m5a` 的条目即 FAIL(防后人再用「放宽门禁迁就残留产物」这条错路,QC #2 教训固化) | > | V6 | README 声称的 selfcheck 门禁条数 == 代码 `CHECKS` 条数(12) | > | V7 | README 本小节声称的收口条数 == 本脚本 `CHECKS` 条数(7),且脚本路径磁盘命中 | > diff --git a/pyproject.toml b/pyproject.toml index ed4d5c7..d610d46 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,17 +10,18 @@ requires-python = ">=3.9" # pbl_common:同机构 modules/ 下的兄弟模块,由宿主 pip install -e 装入,非 PyPI 包,故不声明。 dependencies = ["sqlor"] -# ── 测试依赖(DISC.2:显式声明,禁止靠「宿主恰好装了 pytest」或临时注入 PYTHONPATH)── -# 用途:`pip install -e .[test]` 或 `pip install -r requirements-dev.txt` 一次装齐, -# 之后 `python3 -m pytest tests/test_m5a_idempotency.py -v` 可直接执行。 -# 落盘引导脚本:scripts/setup_test_env.sh(建 .venv-test 并装入本 extra)。 +# ── 测试依赖(发现项 DISC.2)──────────────────────────────────────────── +# 显式声明 pytest,禁止再靠「宿主 python3 恰好装了 pytest」或临时注入 +# PYTHONPATH=/.pylibs 跑测试(后者会让 test 角色误判为用例失败/环境不可用)。 +# 用法:pip install -e .[test](等价于 pip install -r requirements-dev.txt), +# 或直接 bash scripts/run_tests.sh(自动定位/引导测试环境,见 README「Running tests」)。 [project.optional-dependencies] test = ["pytest>=7.0,<10"] [tool.pytest.ini_options] -# tests/ 用 importlib 隔离加载包(绕过宿主依赖),故不需要额外 pythonpath 注入 +# tests/test_m5a_idempotency.py 用 importlib 隔离加载包(绕过宿主依赖), +# 无需任何 pythonpath 注入;此处不设 addopts,避免干扰命令行 -v 的详细输出。 testpaths = ["tests"] -addopts = "-q --color=no" [build-system] requires = ["setuptools>=61"] diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..6315377 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,13 @@ +# pbl_evidence —— 测试前置依赖(发现项 DISC.2:显式声明,不再靠临时注入 PYTHONPATH) +# +# 与 pyproject.toml 的 [project.optional-dependencies].test 等价,单列一份是为了 +# 让「不安装本包也能跑离线单测」的场景(CI / 测试机宿主)有一条最短安装命令: +# python3 -m pip install -r requirements-dev.txt +# +# 版本区间与 pyproject test extra 保持一致(>=7.0,<10): +# - 下限 7.0:需要 pytest.mark.skipif 条件式与 monkeypatch raising 参数的稳定语义; +# - 上限 <10:主版本跳变前不自动跟随,避免解析器/插件接口变更打挂既有 9 条用例。 +# +# 注意:pytest 是**测试**依赖,绝不写进 [project].dependencies——否则宿主 +# `pip install -e modules/pbl_evidence` 会被拖入无关安装,且违反「只声明直接代码依赖」铁律。 +pytest>=7.0,<10 diff --git a/scripts/run_tests.sh b/scripts/run_tests.sh new file mode 100755 index 0000000..238521b --- /dev/null +++ b/scripts/run_tests.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash +# ===================================================================== +# pbl_evidence 测试运行器(发现项 DISC.2) +# +# 一条命令跑通离线单测,**不需要手工注入 PYTHONPATH**: +# bash scripts/run_tests.sh # 等价 python3 -m pytest tests/test_m5a_idempotency.py -v +# bash scripts/run_tests.sh tests/test_m5a_idempotency.py -v +# bash scripts/run_tests.sh --log /tmp/m5a-pytest.log +# PBL_EVIDENCE_TEST_DB=1 bash scripts/run_tests.sh # 打开真实双次采集用例(需测试库) +# PBL_EVIDENCE_TEST_DB=1 PBL_EVIDENCE_TEST_TENANT=m5a-demo bash scripts/run_tests.sh +# +# 解释器解析顺序(第一个「能 import pytest」的胜出): +# ① $PBL_EVIDENCE_TEST_PYTHON —— 显式指定(CI 用宿主 venv 时走这条) +# ② 宿主 python3 —— 若已 pip install pytest / -e .[test](正规长期方案) +# ③ 模块测试 venv .venv-test/bin/python —— 缺则自动调 scripts/setup_test_env.sh 引导 +# 全部不可用 → exit 3 并打印可执行的修复命令(不静默跳过、不假绿)。 +# +# 为什么不再用 PYTHONPATH=/.pylibs:那是工作空间私有的临时目录, +# 换机器/换角色即失效,且把「环境缺依赖」伪装成「测试通过」,属 DISC.2 根因。 +# ===================================================================== +set -euo pipefail + +MODULE_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +VENV="${PBL_EVIDENCE_TEST_VENV:-${MODULE_ROOT}/.venv-test}" +DEFAULT_TARGET="tests/test_m5a_idempotency.py -v" + +log() { echo "[run_tests] $*"; } +die() { echo "[run_tests][FATAL] $*" >&2; exit "${2:-1}"; } + +has_pytest() { + local py="$1" + [ -x "${py}" ] || return 1 + "${py}" -c 'import pytest' >/dev/null 2>&1 +} + +cd "${MODULE_ROOT}" + +# ---- 解析解释器 ---- +PY="" +for cand in "${PBL_EVIDENCE_TEST_PYTHON:-}" "$(command -v python3 || true)" "${VENV}/bin/python"; do + [ -n "${cand}" ] || continue + if has_pytest "${cand}"; then + PY="${cand}" + break + fi +done + +if [ -z "${PY}" ]; then + log "宿主 python3 与测试 venv 均无 pytest,引导测试环境(scripts/setup_test_env.sh)" + bash "${MODULE_ROOT}/scripts/setup_test_env.sh" || die "测试环境引导失败,见上方日志" 3 + PY="${VENV}/bin/python" + has_pytest "${PY}" || die "引导后仍无法 import pytest(${PY})" 3 +fi + +log "python = ${PY} ($("${PY}" -V 2>&1))" +log "pytest = $("${PY}" -m pytest --version 2>&1 | head -1)" +log "cwd = ${MODULE_ROOT}" +[ "${PBL_EVIDENCE_TEST_DB:-}" = "" ] && \ + log "DB 开关 = 未设置(真实双次采集用例 test_double_collect_is_idempotent_with_db 预期 SKIPPED)" || \ + log "DB 开关 = PBL_EVIDENCE_TEST_DB=${PBL_EVIDENCE_TEST_DB} tenant=${PBL_EVIDENCE_TEST_TENANT:-m5a-idem-test}" + +# ---- 参数:无参数时用默认目标;--log FILE 把输出同时落盘 ---- +LOG="" +ARGS=() +while [ $# -gt 0 ]; do + case "$1" in + --log) LOG="${2:-}"; shift 2 ;; + *) ARGS+=("$1"); shift ;; + esac +done +[ ${#ARGS[@]} -gt 0 ] || ARGS=(${DEFAULT_TARGET}) + +if [ -n "${LOG}" ]; then + mkdir -p "$(dirname "${LOG}")" + set +e + "${PY}" -m pytest "${ARGS[@]}" 2>&1 | tee "${LOG}" + RC=${PIPESTATUS[0]} + set -e + echo "# RC=${RC}" >> "${LOG}" +else + "${PY}" -m pytest "${ARGS[@]}" + RC=$? +fi +exit ${RC} diff --git a/scripts/setup_test_env.sh b/scripts/setup_test_env.sh new file mode 100755 index 0000000..7de1c8f --- /dev/null +++ b/scripts/setup_test_env.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# ===================================================================== +# pbl_evidence 测试环境引导(发现项 DISC.2) +# +# 背景:测试机宿主 python3 **没有** pytest,`python3 -m pytest` 直接 +# `No module named pytest`。此前靠临时注入 +# PYTHONPATH=/.pylibs(pytest 9.1.1)跑通,属一次性手段, +# 不可复现、且会让 test 角色误判为「用例失败/环境不可用」。 +# +# 本脚本把「测试前置依赖」变成显式、可重放的步骤: +# 1) 在模块仓库内建独立测试 venv(默认 .venv-test,已在 .gitignore 排除) +# 2) 安装 requirements-dev.txt(== pyproject [project.optional-dependencies].test) +# 3) 打印 pytest 版本自证可用 +# +# 用法: +# bash scripts/setup_test_env.sh +# PBL_EVIDENCE_TEST_VENV=/tmp/ev-venv bash scripts/setup_test_env.sh # 换 venv 位置 +# PBL_EVIDENCE_INSTALL_PKG=1 bash scripts/setup_test_env.sh # 顺带 pip install -e . +# +# 幂等:venv 已存在则复用,pip 已满足则秒退;可被 build.sh / CI 反复调用。 +# ===================================================================== +set -euo pipefail + +MODULE_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +VENV="${PBL_EVIDENCE_TEST_VENV:-${MODULE_ROOT}/.venv-test}" +REQ_DEV="${MODULE_ROOT}/requirements-dev.txt" +HOST_PY="${PBL_EVIDENCE_HOST_PYTHON:-$(command -v python3)}" + +log() { echo "[setup_test_env] $*"; } +die() { echo "[setup_test_env][FATAL] $*" >&2; exit 1; } + +[ -f "${REQ_DEV}" ] || die "缺少测试依赖清单 ${REQ_DEV}" +[ -x "${HOST_PY}" ] || die "找不到宿主 python3(可用 PBL_EVIDENCE_HOST_PYTHON 指定)" + +if [ ! -x "${VENV}/bin/python" ]; then + log "创建测试 venv: ${VENV}" + "${HOST_PY}" -m venv "${VENV}" || die "python3 -m venv 失败(宿主缺 venv 模块?)" +else + log "复用已存在的测试 venv: ${VENV}" +fi + +VPY="${VENV}/bin/python" +# 升级 pip 属尽力而为(离线/内网源可能不可达),失败不阻断——真正的门禁是下一步安装 +"${VPY}" -m pip install --quiet --upgrade pip >/dev/null 2>&1 || log "pip 自升级跳过(不影响后续安装)" +log "安装测试依赖: ${REQ_DEV}" +"${VPY}" -m pip install -r "${REQ_DEV}" || die "安装 pytest 失败:检查内网 PyPI 源可达性(见 README「Running tests」排障)" + +if [ "${PBL_EVIDENCE_INSTALL_PKG:-0}" = "1" ]; then + log "安装模块本体(可编辑):pip install -e ${MODULE_ROOT}" + "${VPY}" -m pip install -e "${MODULE_ROOT}" || die "pip install -e . 失败" +fi + +log "自证可用:$("${VPY}" -m pytest --version 2>&1 | head -1)" +log "TEST ENV READY: ${VPY}"