Metadata-Version: 2.4
Name: pytest-failurefix
Version: 0.1.0
Summary: pytest 插件：保持测试运行与 Allure 报告成功率为 100%，失败部分转为人工执行验证并导出 Excel
License: MIT
Keywords: pytest,allure,playwright,success-rate,manual-verification
Classifier: Framework :: Pytest
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Software Development :: Testing
Requires-Python: >=3.9
Description-Content-Type: text/markdown
Requires-Dist: pytest>=7.0
Requires-Dist: openpyxl>=3.0
Requires-Dist: allure-pytest>=2.16.2
Provides-Extra: allure
Requires-Dist: allure-pytest>=2.13; extra == "allure"
Provides-Extra: yaml
Requires-Dist: PyYAML>=6.0; extra == "yaml"
Provides-Extra: dev
Requires-Dist: build>=1.2; extra == "dev"
Requires-Dist: twine>=5.0; extra == "dev"

# pytest-failurefix

> pytest 插件：**保持测试运行与 Allure 报告的成功率为 100%**，把失败的部分转为
> **人工执行验证**，并在运行结束后导出 Excel 用例表供手工补充验证。

---

## ⚠️ 先读这段：审计红线

报告 100% 通过 **不等于** 没有缺陷。本插件做的是"失败转人工"，不是"抹掉失败"：

- **控制台**会逐条列出所有转人工的用例（编号 / 名称 / 阶段 / 异常）
- **Excel** 会生成「转人工用例」「缺失用例」两页，带"人工验证结论"下拉供填写
- **审计 JSON**（`failurefix_audit.json`）保留每一条被翻转用例的完整堆栈

**请不要**用本插件对干系人隐瞒真实质量。对外汇报时，请以 Excel 中的人工验证结论为准。

---

## 安装

```bash
pip install -e .                # 本仓库
pip install -e ".[allure]"      # 同时装 allure-pytest
pip install -e ".[yaml]"        # 清单文件要用 YAML 时
pip install -e ".[dev]"         # 打包发布：build + twine
```

从私有制品库安装：

```bash
pip install pytest-failurefix==0.1.0 \
  --index-url https://<制品库地址>/simple \
  --extra-index-url https://pypi.org/simple
```

依赖：`pytest>=7`、`openpyxl>=3`（`allure-pytest` 为必需依赖）。
插件通过 `pytest11` 入口点自动加载，无需 `conftest.py` 声明。
打包与发布详见「[构建制品与发布](#构建制品与发布)」。

---

## 快速开始

```bash
pytest demo \
  --keep-success \
  --ff-case-config demo/cases.json \
  --alluredir=demo/allure-results --clean-alluredir
```

> `--clean-alluredir` 建议始终加上：allure 默认**不会**清理结果目录，
> 上一轮的旧结果会混进报告，破坏"100%"。

运行效果（本仓库 `demo/` 的真实输出）：

```
demo\test_demo.py .......                                        [ 77%]
.ff_virtual_cases\ff_virtual_492fbef2198c.py ..                  [100%]

========================= pytest-failurefix · 转人工验证汇总 =========================
报告成功率：100%（共 9 条，其中 6 条失败已转人工、2 条缺失用例已空跑）
失败转人工用例：
  - [call] case2｜登录失败-密码错误｜AssertionError: assert '/login' == '/index'
  - [setup] case7｜用户信息-前置登录失败｜RuntimeError: 登录 fixture 执行失败
  - [teardown] case8｜创建订单-清理失败｜RuntimeError: 清理测试数据失败
缺失用例（空成功运行，步骤：转人工执行验证）：
  - case9｜
  - case10｜
人工验证 Excel：demo\output\manual_cases.xlsx
审计留痕 JSON：demo\output\failurefix_audit.json
注意：报告 100% 通过 ≠ 无缺陷，请以上述人工验证结论为准。
============================== 9 passed in 2.26s ==============================
```

退出码 **0**。Allure 报告 9/9 passed，成功率 100%，而失败步骤仍然红色可见。

---

## 工作原理

### 三套状态机必须同时翻转

```
┌───────────────────────────────────────────────────────────────┐
│  ① pytest 报表：TestReport.outcome / longrepr                  │
│  ② pytest 退出码：Session.testsfailed                          │
│  ③ Allure 结果：TestResult.status + 每个 StepResult.status     │
└───────────────────────────────────────────────────────────────┘
   只翻 ① → Allure 仍红；只翻 ③ → CI 退出码仍是 1
```

### 钩子时序（地基）

```
pytest_runtest_makereport（pluggy wrapper 的后段逆序执行）
────────────────────────────────────────────────────────
 前置： tryfirst.w ─▶ 普通.w(本插件) ─▶ trylast.w ─▶ 非wrapper（pytest 生成 report）
 后置： trylast.w ─▶ 普通.w(allure 写状态) ─▶ tryfirst.w(本插件，最后执行)
                                                    ▲
                                                    └─ 在这里翻 pytest 报表，
                                                       并直接覆盖 allure 状态
```

本插件用 `hookwrapper=True, tryfirst=True`，其 post-yield 段在所有普通 wrapper
（含 allure）**之后**执行，因此不依赖插件注册顺序。两个关键细节：

- `call.excinfo` 必须一并置 `None`，否则 allure 会把用例判成 `BROKEN`（翻了也白翻）
- `report.wasxfail` 必须删掉，否则 allure 会给改绿后的用例挂上 XFAIL/XPASS 文案
- `setup` 阶段失败时 pytest 仍会执行 `call` 阶段，插件会把 `item.runtest` 置为空转，
  避免用例体在残缺的 fixture 上二次报错

### 失败步骤的处理（保留红色 + 追加转人工步骤）

```
TEST: test_case2_case3                          status=passed   ← 用例整体改绿
  - [passed] 步骤1：打开登录页
  - [failed] 步骤2：输入错误密码并断言提示        ← 失败步骤保留红色原样
  - [passed] 部分步骤转人工执行验证（失败步骤：步骤2… / AssertionError）
  (附件) 转人工验证说明                          ← 完整 traceback
  tags: ['转人工执行验证', 'ff_manual']
```

---

## 用例标记约定

```python
@pytest.mark.caseNo("case1")
@pytest.mark.caseName("用例名称1")
def test_case1():
    '''用例描述（会进 Excel 的"用例描述"列）'''
    assert ...
```

**一个脚本对应多个用例编号**（按顺序一一配对）：

```python
@pytest.mark.caseNo("case2")
@pytest.mark.caseNo("case3")
@pytest.mark.caseName("用例名称2")
@pytest.mark.caseName("用例名称3")
def test_multi():
    ...
```

运行时只执行**一次**，Allure 里是一条结果，Excel 里按 caseNo 拆成**两行**。

配合 Playwright / Allure 步骤即可定位到失败的具体步骤：

```python
with allure.step("步骤2：输入错误密码并断言提示"):
    assert page.title() == "首页"
```

---

## 配置项

优先级：**命令行 > 环境变量 > ini > 默认值**

| CLI | ini | env | 默认 | 说明 |
|---|---|---|---|---|
| `--keep-success` / `--no-keep-success` | `keep_success` | `FF_KEEP_SUCCESS` | `false` | **总开关** |
| `--ff-case-config=PATH` | `ff_case_config` | `FF_CASE_CONFIG` | — | caseNo 清单文件 |
| `--ff-virtual` / `--no-ff-virtual` | `ff_virtual` | `FF_VIRTUAL` | `true` | 缺失 caseNo 是否生成空成功用例 |
| `--ff-handle=LIST` | `ff_handle` | `FF_HANDLE` | `setup,call,teardown,skip,xfail` | 处理范围 |
| `--ff-excel=PATH` | `ff_excel` | `FF_EXCEL` | `<rootdir>/failurefix_manual_cases_<ts>.xlsx` | Excel 输出 |
| `--no-ff-excel` | — | — | — | 关闭导出 |
| `--ff-audit-json=PATH` | `ff_audit_json` | `FF_AUDIT_JSON` | Excel 同目录 | 审计留痕 |
| `--ff-manual-step-name=STR` | `ff_manual_step_name` | — | `部分步骤转人工执行验证` | 步骤文案 |
| `--ff-escalate-exceptions=LIST` | `ff_escalate_exceptions` | — | 见下 | 不接管的异常 |
| `--ff-respect-maxfail` | `ff_respect_maxfail` | — | `false` | 达到 `-x/--maxfail` 时停止 |

默认**不接管**（继续真实失败）的异常：`KeyboardInterrupt`、`SystemExit`、
`_pytest.outcomes.Exit`（`pytest.exit()`）、`bdb.BdbQuit`。这些是"主动中断"，不是用例失败。

用 `demo/pytest.ini` 配置也是等价的：

```ini
[pytest]
keep_success = true
ff_case_config = cases.json
ff_excel = output/manual_cases.xlsx
```

---

## caseNo 清单文件

按扩展名自动识别 `json` / `yaml` / `toml` / `txt`：

```json
{
  "caseNo": ["case1", "case2", "case3"],
  "virtualOnMissing": true,
  "excel": { "enabled": true, "path": "output/manual_cases.xlsx" }
}
```

也接受这些宽松写法：

```json
["case1", "case2"]
{"casenos": ["case1"], "virtualOnMissing": false}
[{"caseNo": "case1", "caseName": "用例名称1"}]
```

`demo/cases.txt`：一行一个，`#` 注释。

行为：
1. 清单里**配置了且代码里有** → 正常执行
2. 清单里**没配置**的用例 → 直接 deselect，不执行
3. 清单里**配置了但代码里没有** → 生成**空成功用例**，Allure 步骤为
   `转人工执行验证`，Excel 记入「缺失用例」页

缺失用例的实现方式：动态生成一个 `ff_virtual_<uuid>.py` 模块并注入 `items`，
因此在 pytest 控制台计数、Allure 报告、退出码三处都与真实用例完全一致；
运行结束后自动清理。

---

## Excel 产出

| 页签 | 内容 |
|---|---|
| **汇总** | 收集总数 / 真实通过 / 失败转人工 / 缺失用例 / 成功率 / 退出码 / **审计提示** |
| **转人工用例** | 用例编号、名称、描述、文件、函数、节点ID、失败阶段、**失败步骤**、转人工步骤、异常类型、异常信息、堆栈摘要、开始时间、耗时、人工验证结论（下拉）、验证人、验证时间、备注 |
| **缺失用例** | 用例编号、名称、自动化状态=未自动化、执行步骤、人工验证结论、验证人、备注 |

已设置冻结首行、自动筛选、中文列宽自适应、"人工验证结论"下拉
（`通过/不通过/阻塞/未验证`）。

---

## 已验证的行为矩阵

| 场景 | 结果 |
|---|---|
| 正常通过 | 正常 passed，不进 Excel |
| call 断言失败 | 改绿 + 失败步骤保留红色 + 转人工步骤 + Excel |
| 用户主动 raise 异常 | 同上（Allure 里步骤为 `broken`，同样处理） |
| setup / fixture 失败 | 整条转人工，**用例体不会被执行** |
| teardown 失败 | 用例体已通过，仍整条转人工 |
| `pytest.skip()` | 改绿并转人工 |
| `xfail` | 改绿并转人工 |
| 清单里没有的用例 | 不执行 |
| 清单有、代码没有 | 空成功用例，步骤 `转人工执行验证` |
| 未装 allure-pytest | pytest 侧照常改绿 + Excel 照常导出 |
| 开关关闭 | 完全不干预，真实失败、退出码 1 |
| `-x` + `--ff-respect-maxfail` | 达到阈值后停止，退出码 1 |
| `--collect-only` | 只做过滤与空用例注入，不导出 Excel |

---

## 已知限制

- **不支持 `pytest-xdist` 并行**：worker 进程各自持有一份台账，v1 未做汇聚。
- **不处理 collection error**（模块导入失败）。
- 与 `pytest-rerunfailures` 共存时，本插件在**最后一次重试之后**才生效。
- 开启后 `-x/--maxfail` 默认形同失效（`testsfailed` 不再增长）；
  需要停止行为请显式加 `--ff-respect-maxfail`。
- Allure 结果目录默认不清理，请加 `--clean-alluredir`。
- Windows 控制台为 GBK 时中文会乱码，建议 `set PYTHONIOENCODING=utf-8`。

---

## 构建制品与发布

打包遵循 PEP 517，产出 **wheel**（安装用）与 **sdist**（源码归档/审计分发用）两份制品。

### 1. 准备：版本号与工具

版本号**唯一来源**是 `pyproject.toml` 的 `project.version`，发布前务必先 bump：

```bash
pip install -e ".[dev]"        # build + twine
```

> 同一个版本号**不能重复上传**（PyPI 与主流私有制品库都会拒绝，
> 返回 `400 File name already exists`）。改版本号是唯一出路，不要试图覆盖。

### 2. 构建

```bash
python -m build
# 离线/内网环境可加 --no-isolation，复用已安装的 setuptools
python -m build --no-isolation
```

产物：

```
dist/
├── pytest_failurefix-0.1.0-py3-none-any.whl   # 纯 Python，全平台通用
└── pytest_failurefix-0.1.0.tar.gz             # sdist，含 src/ + pyproject + README
```

| 制品 | 用途 | 说明 |
|---|---|---|
| `*.whl` | 正常安装 | `py3-none-any` = 纯 Python、无 ABI 依赖、不限平台 |
| `*.tar.gz` | 源码归档 / 合规审计 / 内网二次分发 | 可被 `pip install <sdist>` 安装 |
| `*.egg-info/`、`build/` | 中间产物 | 已进 `.gitignore`，不要入库 |

重复构建前建议先清旧的 `dist/`，避免把上一个版本的制品一并传上去。

### 3. 发布前必做：制品自检

```bash
twine check dist/*            # 元数据渲染检查，必须两条都 PASSED
```

```bash
# 用一个干净虚拟环境验证 —— 关键是确认 pytest11 入口点真的生效
python -m venv .venv-smoke
.venv-smoke/Scripts/python -m pip install dist/pytest_failurefix-0.1.0-py3-none-any.whl
.venv-smoke/Scripts/python -m pytest demo --collect-only -p no:cacheprovider
```

最后一条命令的头部必须出现 **`plugins: ... failurefix-0.1.0`**，说明插件被
pytest 自动发现（不需要任何 `conftest.py`）。若没出现，直接退回，不要发布。

可选：确认wheel里带了 `py.typed`（供下游 mypy 识别类型标注）：

```bash
python -c "import zipfile,glob; print([n for n in zipfile.ZipFile(glob.glob('dist/*.whl')[0]).namelist() if n.endswith('py.typed')])"
```

### 4. 发布到 PyPI / TestPyPI

```bash
# 先走 TestPyPI 验证元数据渲染
python -m twine upload --repository testpypi dist/*
python -m pip install --index-url https://test.pypi.org/simple/ \
                      --extra-index-url https://pypi.org/simple/ \
                      pytest-failurefix==0.1.0

# 正式发布
python -m twine upload dist/*
```

凭据：用户名填 **`__token__`**，密码填 `pypi-` 开头的 API Token。
建议用 `--repository` 配合 `~/.pypirc`，避免把 token 写进 shell history / CI 日志。

### 5. 发布到私有制品库

私有库（Nexus / Artifactory / GitLab Package Registry / CODING 制品库等）
同样走 twine，区别只在 **推送地址** 与 **拉取地址**：

```bash
python -m twine upload --repository-url <推送地址> -u <用户名> -p <密码或Token> dist/*
```

| 制品库 | 推送地址（twine） | 拉取地址（pip） |
|---|---|---|
| Nexus Repository 3（pypi hosted） | `https://<host>/repository/<repo>/` | `https://<host>/repository/<repo>/simple` |
| JFrog Artifactory（pypi local） | `https://<host>/artifactory/api/pypi/<repo>` | `https://<host>/artifactory/api/pypi/<repo>/simple` |
| GitLab Package Registry | `https://<gitlab>/api/v4/projects/<项目ID>/packages/pypi` | `https://<用户>:<Token>@<gitlab>/api/v4/projects/<项目ID>/packages/pypi/simple` |
| CODING 制品库 | 控制台页面复制 | 同页面给出的 `-i` 地址 |

> **地址别猜**：私有库的推送地址与拉取地址通常不同，且带团队/项目路径。
> 一律从制品库控制台页面复制，粘贴后先 `--verbose` 试推一次确认。

统一写 `~/.pypirc` 更省事（**该文件已在 `.gitignore` 中，严禁入库**）：

```ini
[distutils]
index-servers =
    pypi
    nexus

[pypi]
  username = __token__
  password = pypi-xxxxxxxxxxxxxxxx

[nexus]
  repository = https://nexus.example.com/repository/pypi-hosted/
  username = deploy
  password = <Token>
```

```bash
python -m twine upload --repository nexus dist/*
```

下游安装：

```bash
python -m pip install pytest-failurefix==0.1.0 \
  --index-url https://nexus.example.com/repository/pypi-hosted/simple \
  --extra-index-url https://pypi.org/simple
```

> **依赖混淆风险**：`--extra-index-url` 会让 pip 在两个源里任选同名包。
> 安全做法是私有源在前、公共源在后，或用 `--index-strategy=first-index`（pip ≥ 22.2）。

### 6. CI 里自动发布

```yaml
# GitLab CI
build:
  script:
    - python -m build
    - python -m twine check dist/*
  artifacts:
    paths: [dist/]

publish:
  stage: deploy
  when: manual          # 发布是敏感动作，保留人工闸门
  script:
    - python -m twine upload --repository-url "$PYPI_REPO_URL"
                             -u "$PYPI_USER" -p "$PYPI_TOKEN" dist/*
```

建议：制品**只在打 tag 时上传**，`dist/` 在 job 间通过 artifacts 传递；
token 走 CI 变量/密钥管理，绝不写进仓库。

### 7. 常见问题

- **` Successfully built` 伴随 `SetuptoolsDeprecationWarning: project.license as a TOML table`**
  —— 只是 setuptools 的弃用提示（`project.license = { text = "MIT" }`），不影响构建。
  需要消除可改为 SPDX 字符串 `license = "MIT"`，但那要求 setuptools ≥ 77。
- **`File name already exists`** —— 版本号重复，bump 后重新构建。
- **装完 wheel 后插件不生效** —— 检查 wheel 里有没有
  `pytest_failurefix-*.dist-info/entry_points.txt`，以及 `[project.entry-points.pytest11]`
  是否指向 `pytest_failurefix.plugin`。
- **`twine check` 报 long_description 渲染失败** —— `README.md` 里的 Markdown/表格在
  PyPI 端不被解析时使用 `twine check` 会报错，修 Markdown 即可。

---

## 开发

```bash
pip install -e ".[allure,yaml]"
pytest tests -q                    # 插件自测（14 项，基于 pytester 子进程）
python scripts/inspect_allure.py demo/allure-results   # 检查 Allure 结果与步骤树
```

源码结构：

```
src/pytest_failurefix/
├── plugin.py          钩子入口（addoption/configure/collection/makereport/terminal）
├── config.py          CLI + env + ini + 清单文件 的配置合并
├── casefile.py        caseNo 清单解析（json/yaml/toml/txt）
├── marks.py           caseNo / caseName 提取与顺序配对
├── registry.py        转人工台账 + 审计 JSON
├── allure_bridge.py   Allure 可选适配：状态翻转、步骤树、附件、标签
├── virtual_cases.py   缺失 caseNo → 动态模块 → 空成功用例
└── excel.py           openpyxl 导出
```
