diff --git a/README.md b/README.md index f0dbcdf..6d6c161 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,56 @@ # pbls +PBL Agent OS 验证原型应用(唯一入口 `app/pbls.py`,端口以 +`projects/pbls/env/test.json` 为唯一事实源)。业务模块见机构 `modules/pbl_*`。 + +## 测试依赖(DISC.2) + +**结论:pytest 已显式声明,不再依赖临时注入 `PYTHONPATH`。** 宿主 `python3` 上没有 pytest +(`python3 -m pytest` → `No module named pytest`),且本机宿主 user site 是只读文件系统 +(`pip install --user` 报 `[Errno 30] Read-only file system`),所以测试依赖的落点是 **venv**。 + +### 新增/相关文件清单 + +| 路径 | 层级 | 职责 | +|---|---|---| +| `apps/pbls/requirements-test.txt` | 应用级 | 测试前置依赖清单(`pytest>=7.0,<10`),由 `build.sh` **第 7b 步**装进应用 venv | +| `apps/pbls/build.sh` 第 7b 步 | 应用级 | `pip install -r requirements-test.txt` + `import pytest` 自证;`PBL_SKIP_TEST_DEPS=1` 可跳过 | +| `modules/pbl_evidence/requirements-dev.txt` | 模块级 | 同一版本区间的模块侧清单(不装本包也能跑离线单测的最短路径) | +| `modules/pbl_evidence/scripts/setup_test_env.sh` | 模块级 | 建 `.venv-test` 并安装 `requirements-dev.txt`(含仓库卫生门禁) | +| `modules/pbl_evidence/scripts/activate_test_env.sh` | 模块级 | 打印激活/直接调用测试 venv 解释器的方式 | +| `modules/pbl_evidence/scripts/run_tests.sh` | 模块级 | 一条命令跑测试:无 pytest 时自动引导,再执行 pytest | +| `modules/pbl_evidence/README.md` §Running tests | 文档 | 排障入口 + `PBL_EVIDENCE_TEST_DB` 开关用法 | + +### 为什么是两份清单(应用级 vs 模块级) + +- `requirements-test.txt`(应用级)服务**整应用**的测试环境:跑 `build.sh` 后 venv 内即有 pytest, + 任何 `modules/pbl_*/tests/` 都能用同一个解释器执行。 +- `requirements-dev.txt`(模块级)服务**只测本模块**的 CI/离线场景:不安装 pbls 应用、不装本包 + (`pip install -e .`)也能一条命令装好依赖。 +- 两份**版本区间同源**(`>=7.0,<10`),避免"应用装了 9.x、模块 CI 装了 7.x"导致用例语义漂移。 +- 两者都刻意与运行时清单 `requirements.txt` 分离:pytest 不进生产镜像,也不进 + `build.sh` 第 5 步的运行时依赖核验(该步只核验 ahserver/sqlor/apppublic/appbase/rbac/PyMySQL)。 + +### 推荐调用顺序 + +```bash +# 场景 1:完整应用环境(部署/联调机) +bash apps/pbls/build.sh # 第 7 步 venv → 7b 测试依赖 → 8 装模块 +source apps/pbls/venv/bin/activate +python3 -m pytest modules/pbl_evidence/tests/test_m5a_idempotency.py -v + +# 场景 2:只跑 pbl_evidence 单测(测试机/CI,最短路径) +bash modules/pbl_evidence/scripts/run_tests.sh # 无 pytest 时自动引导 .venv-test + +# 场景 3:只要环境不要跑(手工用 venv 解释器) +bash modules/pbl_evidence/scripts/setup_test_env.sh +bash modules/pbl_evidence/scripts/activate_test_env.sh +``` + +### 期望结果 + +- 不带 `PBL_EVIDENCE_TEST_DB`:`8 passed, 1 skipped`,RC=0(`test_double_collect_is_idempotent_with_db` + 需真实 MariaDB,未设开关时按设计 SKIPPED,属预期)。 +- 带 `PBL_EVIDENCE_TEST_DB`(用法见 `modules/pbl_evidence/README.md` §Running tests):`9 passed`。 +- 实测日志:`projects/pbls/docs/02-develop/m5a-disc2-pytest-run.log`。 +- 禁止为凑通过修改测试断言逻辑。 diff --git a/build.sh b/build.sh index 2738236..b7c0b90 100644 --- a/build.sh +++ b/build.sh @@ -98,6 +98,27 @@ fi "${PY}" -m pip install --upgrade pip wheel setuptools "${PY}" -m pip install -r "${APP_DIR}/requirements.txt" +# ---- 7b. 测试前置依赖(DISC.2)---- +# 运行时依赖(requirements.txt)刻意不含 pytest;测试依赖单列 requirements-test.txt, +# 由本步装进同一个 venv,使 +# source venv/bin/activate && python3 -m pytest tests/test_m5a_idempotency.py -v +# 在**不手工注入 PYTHONPATH** 的前提下可直接执行(此前靠 PYTHONPATH=/.pylibs +# 临时注入跑通,换机器/换角色即失效,test 角色会误判为环境不可用)。 +# 只要运行时依赖的场景(如生产镜像)用 PBL_SKIP_TEST_DEPS=1 显式跳过。 +REQ_TEST="${APP_DIR}/requirements-test.txt" +if [ "${PBL_SKIP_TEST_DEPS:-0}" = "1" ]; then + log "PBL_SKIP_TEST_DEPS=1:跳过测试前置依赖(venv 内将没有 pytest,测试角色需自行引导)" +elif [ ! -f "${REQ_TEST}" ]; then + die "缺测试依赖清单 ${REQ_TEST}(DISC.2 要求显式声明;确要跳过请设 PBL_SKIP_TEST_DEPS=1)" +else + log "安装测试前置依赖: ${REQ_TEST}" + "${PY}" -m pip install -r "${REQ_TEST}" + # 自证:装完必须真能 import + 跑 --version,否则本步等于没做(不让注释与事实矛盾) + "${PY}" -c 'import pytest' >/dev/null 2>&1 \ + || die "安装后 venv 内仍无法 import pytest(${PY})" + log "7b 自证通过: $("${PY}" -m pytest --version 2>&1 | head -1) @ ${PY}" +fi + # ---- 8. 本地安装 15 个 pbl_* 模块 + 复用模块(可编辑模式)---- for mod_dir in "${WS_DIR}"/modules/pbl_*; do [ -d "${mod_dir}" ] || continue diff --git a/requirements-test.txt b/requirements-test.txt index dca604b..fc60f5e 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -3,16 +3,21 @@ # 为什么单列一份、不并进 requirements.txt: # requirements.txt 是**运行时**依赖,会被 build.sh 装进生产/测试 venv 并随应用 # 部署;pytest 只在「跑测试」这一步需要,混进去等于把测试工具塞进生产镜像。 +# 因此本文件由 build.sh **第 7b 步**单独安装(可用 PBL_SKIP_TEST_DEPS=1 跳过), +# 不进入运行时依赖核验清单(第 5 步只核验 ahserver/sqlor/apppublic/appbase/rbac/PyMySQL)。 # # 为什么必须显式声明: # 测试机宿主 python3 未装 pytest,`python3 -m pytest` 直接 # `No module named pytest`;此前靠临时注入 PYTHONPATH=/.pylibs 跑通 # (一次性手段,换机器/换角色即失效),test 角色据此会误判「用例失败/环境不可用」。 +# 宿主 user site(/d/pipeline/.local)在本机是只读文件系统,`pip install --user` +# 也会失败,所以「让 pytest 可用」的正确落点是 venv,而不是宿主 python3。 # -# 用法(build.sh 第 7b 步已自动执行): -# python3 -m pip install -r requirements-test.txt -# 或 bash scripts/build.sh # venv 内即有 pytest -# 或 cd modules/pbl_evidence && bash scripts/run_tests.sh # 模块自带引导 +# 用法(三条都是真实可执行路径,与 apps/pbls/README.md「测试依赖(DISC.2)」一致): +# 1) bash build.sh # 第 7b 步自动 pip install -r 本文件到 venv +# source venv/bin/activate && python3 -m pytest <目标> -v +# 2) python3 -m pip install -r requirements-test.txt # 自己管环境时(需可写 site) +# 3) bash modules/pbl_evidence/scripts/run_tests.sh # 模块自带引导(无 pytest 时自动建 .venv-test) # # 版本区间与各模块 pyproject [project.optional-dependencies].test 一致(>=7.0,<10)。 pytest>=7.0,<10 diff --git a/scripts/patch_build_7b.py b/scripts/patch_build_7b.py new file mode 100644 index 0000000..48a07d9 --- /dev/null +++ b/scripts/patch_build_7b.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +"""一次性补丁脚本:在 apps/pbls/build.sh 第 7 步后插入「7b. 测试前置依赖(DISC.2)」。 + +为什么用脚本而不是 sed:build.sh 里含 `${...}` 与引号,shell 内嵌补丁容易吃掉转义。 +本脚本幂等——已存在 7b 标记时直接退出,不重复插入。 +""" +import io +import os +import sys + +BUILD = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "build.sh") +ANCHOR = '# ---- 8. 本地安装 15 个 pbl_* 模块 + 复用模块(可编辑模式)----' +MARK = '# ---- 7b. 测试前置依赖(DISC.2)----' + +BLOCK = '''# ---- 7b. 测试前置依赖(DISC.2)---- +# 运行时依赖(requirements.txt)刻意不含 pytest;测试依赖单列 requirements-test.txt, +# 由本步装进同一个 venv,使 +# source venv/bin/activate && python3 -m pytest tests/test_m5a_idempotency.py -v +# 在**不手工注入 PYTHONPATH** 的前提下可直接执行(此前靠 PYTHONPATH=/.pylibs +# 临时注入跑通,换机器/换角色即失效,test 角色会误判为环境不可用)。 +# 只要运行时依赖的场景(如生产镜像)用 PBL_SKIP_TEST_DEPS=1 显式跳过。 +REQ_TEST="${APP_DIR}/requirements-test.txt" +if [ "${PBL_SKIP_TEST_DEPS:-0}" = "1" ]; then + log "PBL_SKIP_TEST_DEPS=1:跳过测试前置依赖(venv 内将没有 pytest,测试角色需自行引导)" +elif [ ! -f "${REQ_TEST}" ]; then + die "缺测试依赖清单 ${REQ_TEST}(DISC.2 要求显式声明;确要跳过请设 PBL_SKIP_TEST_DEPS=1)" +else + log "安装测试前置依赖: ${REQ_TEST}" + "${PY}" -m pip install -r "${REQ_TEST}" + # 自证:装完必须真能 import + 跑 --version,否则本步等于没做(不让注释与事实矛盾) + "${PY}" -c 'import pytest' >/dev/null 2>&1 \\ + || die "安装后 venv 内仍无法 import pytest(${PY})" + log "7b 自证通过: $("${PY}" -m pytest --version 2>&1 | head -1) @ ${PY}" +fi + +''' + + +def main(): + build = os.path.abspath(BUILD) + with io.open(build, "r", encoding="utf-8") as f: + text = f.read() + if MARK in text: + print("SKIP: 7b 已存在,未重复插入") + return 0 + if ANCHOR not in text: + print("FAIL: 找不到第 8 步锚点,拒绝盲目修改") + return 1 + patched = text.replace(ANCHOR, BLOCK + ANCHOR, 1) + with io.open(build, "w", encoding="utf-8") as f: + f.write(patched) + print("OK: 已在 %s 第 8 步前插入 7b(+%d 字符)" % (build, len(patched) - len(text))) + return 0 + + +if __name__ == "__main__": + sys.exit(main())