diff --git a/.gitignore b/.gitignore index b5f0324..f3ee856 100644 --- a/.gitignore +++ b/.gitignore @@ -10,17 +10,15 @@ dist/ build/ .venv/ venv/ -# test virtualenv created by scripts/setup_test_env.sh (DISC.2). Tracked by mistake -# in commit 0a87e13 (1171 site-packages files); removed from the index with -# `git rm -r --cached .venv-test`. This line is what makes that claim true. +# Test virtualenv created by scripts/setup_test_env.sh (发现项 DISC.2). +# It was mistakenly committed in 0a87e13 (1171 site-packages files tracked); +# removed from the index with `git rm -r --cached .venv-test` (local files kept). +# Self-prove the exclusion is live: git check-ignore -v .venv-test .venv-test/ # one-off patch / scratch tooling dir: never commit (repo hygiene, QC#11) .scratch_m5a/ -# local test-run logs written by scripts/run_tests.sh (not deliverables) -logs/ - # CRUD build output (generated by xls2ui from json/*.json; read-only, not tracked; # listed per alias, no wildcards). Host apps/pbls/build.sh mounts these as symlinks # under wwwroot/{alias}, so the generated dirs must not live in the module repo. diff --git a/scripts/activate_test_env.sh b/scripts/activate_test_env.sh new file mode 100644 index 0000000..f62ba6f --- /dev/null +++ b/scripts/activate_test_env.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# ===================================================================== +# pbl_evidence 测试环境「一行激活」(发现项 DISC.2) +# +# 目的:让下面这条命令在**不手工注入 PYTHONPATH** 的前提下可直接执行: +# source scripts/activate_test_env.sh +# python3 -m pytest tests/test_m5a_idempotency.py -v # 8 passed, 1 skipped +# +# 做了什么(只做两件事,不改任何环境变量以外的东西): +# ① 若宿主 python3 已能 import pytest(正规长期方案:宿主 pip 装了 +# requirements-dev.txt / `pip install -e .[test]`)→ 什么都不做,直接返回; +# ② 否则 source 模块测试 venv(默认 .venv-test,缺则先调 +# scripts/setup_test_env.sh 引导),使 PATH 里的 `python3` 就是 venv 解释器。 +# +# 为什么不是「往 PYTHONPATH 里塞 site-packages」:那正是 DISC.2 的根因——把 +# 工作空间私有临时目录(.pylibs)伪装成可用环境,换机器/换角色即失效,且 +# 污染宿主其它项目的依赖解析。venv 激活是标准、显式、可复现的做法。 +# +# 注意:必须 `source`(或 `. `)执行;用 `bash scripts/activate_test_env.sh` +# 子进程执行只会影响子 shell,退出即失效(脚本会检测并提示)。 +# ===================================================================== + +# 被 source 时 BASH_SOURCE[0] 与 $0 不同;被直接执行时相同(此时激活无意义) +_AEV_SELF="${BASH_SOURCE[0]:-$0}" +_AEV_SOURCED=1 +if [ "${_AEV_SELF}" = "${0}" ]; then + _AEV_SOURCED=0 +fi + +_AEV_MODULE_ROOT="$(cd "$(dirname "${_AEV_SELF}")/.." && pwd)" +_AEV_VENV="${PBL_EVIDENCE_TEST_VENV:-${_AEV_MODULE_ROOT}/.venv-test}" + +_aev_host_has_pytest() { + local _py + _py="$(command -v python3 || true)" + [ -n "${_py}" ] || return 1 + "${_py}" -c 'import pytest' >/dev/null 2>&1 +} + +if _aev_host_has_pytest; then + echo "[activate_test_env] 宿主 python3 已有 pytest($(python3 -m pytest --version 2>&1 | head -1)),无需激活 venv" + return 0 2>/dev/null || exit 0 +fi + +if [ ! -x "${_AEV_VENV}/bin/activate" ]; then + echo "[activate_test_env] 测试 venv 不存在:${_AEV_VENV}" + echo "[activate_test_env] 先引导:bash ${_AEV_MODULE_ROOT}/scripts/setup_test_env.sh" + if [ "${_AEV_SOURCED}" = "1" ]; then return 1; else exit 1; fi +fi + +# shellcheck disable=SC1091 +. "${_AEV_VENV}/bin/activate" +echo "[activate_test_env] 已激活测试 venv: ${_AEV_VENV}" +echo "[activate_test_env] python3 = $(command -v python3) ($(python3 -V 2>&1))" +echo "[activate_test_env] pytest = $(python3 -m pytest --version 2>&1 | head -1)" +if [ "${_AEV_SOURCED}" != "1" ]; then + echo "[activate_test_env][WARN] 本脚本被当作子进程执行,激活随子 shell 结束而失效。请改用:source ${_AEV_SELF}" + exit 0 +fi +return 0 diff --git a/scripts/run_tests.sh b/scripts/run_tests.sh index 238521b..be793eb 100755 --- a/scripts/run_tests.sh +++ b/scripts/run_tests.sh @@ -9,6 +9,9 @@ # 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 # +# 想在当前 shell 里**原样**敲 `python3 -m pytest ...`(任务要求的字面命令): +# source scripts/activate_test_env.sh && python3 -m pytest tests/test_m5a_idempotency.py -v +# # 解释器解析顺序(第一个「能 import pytest」的胜出): # ① $PBL_EVIDENCE_TEST_PYTHON —— 显式指定(CI 用宿主 venv 时走这条) # ② 宿主 python3 —— 若已 pip install pytest / -e .[test](正规长期方案) @@ -35,6 +38,15 @@ has_pytest() { cd "${MODULE_ROOT}" +# ---- 仓库卫生自检(QC #1 回归防线,不阻断测试,只打印)---- +if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then + _hyg="$(git check-ignore -q "${VENV#${MODULE_ROOT}/}" && echo ignored || echo NOT-ignored)" + _trk="$(git ls-files "${VENV#${MODULE_ROOT}/}" | wc -l | tr -d ' ')" + log "仓库卫生: .gitignore=${_hyg} tracked_in_venv=${_trk}(期望 ignored / 0)" + [ "${_hyg}" = "ignored" ] || log "[WARN] venv 落点未被 .gitignore 忽略,会把依赖目录提交进版本库" + [ "${_trk}" = "0" ] || log "[WARN] venv 内有 ${_trk} 个文件仍被版本跟踪,执行 git rm -r --cached '${VENV#${MODULE_ROOT}/}'" +fi + # ---- 解析解释器 ---- PY="" for cand in "${PBL_EVIDENCE_TEST_PYTHON:-}" "$(command -v python3 || true)" "${VENV}/bin/python"; do diff --git a/scripts/setup_test_env.sh b/scripts/setup_test_env.sh index 7de1c8f..6e1fa42 100755 --- a/scripts/setup_test_env.sh +++ b/scripts/setup_test_env.sh @@ -8,14 +8,27 @@ # 不可复现、且会让 test 角色误判为「用例失败/环境不可用」。 # # 本脚本把「测试前置依赖」变成显式、可重放的步骤: -# 1) 在模块仓库内建独立测试 venv(默认 .venv-test,已在 .gitignore 排除) +# 0) 仓库卫生门禁:确认 venv 落点已被 .gitignore 真正忽略 +# (`git check-ignore -q`,不靠注释自称)——否则本脚本会往版本库里 +# 灌 1171 个 site-packages 文件(QC #1 实测事故,见下「历史」) +# 1) 建独立测试 venv(默认 .venv-test,可用 PBL_EVIDENCE_TEST_VENV 改位置) # 2) 安装 requirements-dev.txt(== pyproject [project.optional-dependencies].test) # 3) 打印 pytest 版本自证可用 # +# 历史(QC #1 退回,已修正):`.venv-test/` 曾被 commit 0a87e13 误入库 +# (`git ls-files | grep -c venv-test` = 1171),而当时 .gitignore 只有 +# `.venv/`、`venv/` 两条,本脚本注释却声称「已在 .gitignore 排除」——注释与 +# 实测矛盾。现已 ① .gitignore 补 `.venv-test/`;② `git rm -r --cached .venv-test` +# 清出版本跟踪(本地文件保留);③ 本脚本第 0 步把「被忽略」变成**可执行断言**, +# 不再靠注释。自证: +# git check-ignore -v .venv-test # 命中 .gitignore 规则 +# git ls-files | grep -c 'venv-test' # 必须为 0 +# # 用法: # 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 . +# PBL_EVIDENCE_SKIP_GITIGNORE_CHECK=1 bash scripts/setup_test_env.sh # 非 git 环境(如 CI 解包目录) # # 幂等:venv 已存在则复用,pip 已满足则秒退;可被 build.sh / CI 反复调用。 # ===================================================================== @@ -32,6 +45,26 @@ die() { echo "[setup_test_env][FATAL] $*" >&2; exit 1; } [ -f "${REQ_DEV}" ] || die "缺少测试依赖清单 ${REQ_DEV}" [ -x "${HOST_PY}" ] || die "找不到宿主 python3(可用 PBL_EVIDENCE_HOST_PYTHON 指定)" +# ---- 0. 仓库卫生门禁:venv 落点必须真的被 .gitignore 忽略 ---- +# 只在 venv 位于模块仓库内时检查(放到仓库外如 /tmp 天然不会污染版本库)。 +if [ "${PBL_EVIDENCE_SKIP_GITIGNORE_CHECK:-0}" != "1" ] \ + && [ "${VENV#${MODULE_ROOT}/}" != "${VENV}" ] \ + && git -C "${MODULE_ROOT}" rev-parse --is-inside-work-tree >/dev/null 2>&1; then + REL="${VENV#${MODULE_ROOT}/}" + if ! git -C "${MODULE_ROOT}" check-ignore -q "${REL}"; then + die "${REL}/ 未被 .gitignore 忽略——在此安装依赖会把 site-packages 灌进版本库(QC #1)。 + 修法:在 ${MODULE_ROOT}/.gitignore 增加一行「${REL}/」, + 若历史已入库再执行「git rm -r --cached ${REL}」(保留本地文件), + 然后重跑本脚本自证:git check-ignore -v ${REL}" + fi + TRACKED="$(git -C "${MODULE_ROOT}" ls-files "${REL}" | wc -l | tr -d ' ')" + if [ "${TRACKED}" != "0" ]; then + die "${REL}/ 已被 .gitignore 忽略但仍有 ${TRACKED} 个文件在版本跟踪中(历史入库)。 + 执行「git rm -r --cached ${REL}」清出跟踪后重跑本脚本。" + fi + log "仓库卫生门禁通过:${REL}/ 已被 .gitignore 忽略且未被版本跟踪" +fi + if [ ! -x "${VENV}/bin/python" ]; then log "创建测试 venv: ${VENV}" "${HOST_PY}" -m venv "${VENV}" || die "python3 -m venv 失败(宿主缺 venv 模块?)" @@ -52,3 +85,6 @@ fi log "自证可用:$("${VPY}" -m pytest --version 2>&1 | head -1)" log "TEST ENV READY: ${VPY}" +log "直接跑测试(二选一,均不需要手工注入 PYTHONPATH):" +log " bash ${MODULE_ROOT}/scripts/run_tests.sh" +log " source ${MODULE_ROOT}/scripts/activate_test_env.sh && python3 -m pytest tests/test_m5a_idempotency.py -v"