|
| 1 | +# ✅ 完整解决方案:自动测试发现 |
| 2 | + |
| 3 | +## 🎯 问题和解决方案 |
| 4 | + |
| 5 | +### 问题 |
| 6 | +Pytest 对测试发现有严格的命名要求: |
| 7 | +- 文件名必须是 `test_*.py` 或 `*_test.py` |
| 8 | +- 函数名必须以 `test_` 开头 |
| 9 | + |
| 10 | +用户希望使用 `@evaluation_test` 装饰器后,无论如何命名都能被发现。 |
| 11 | + |
| 12 | +### 解决方案 |
| 13 | + |
| 14 | +#### ✅ 函数名:完全自动处理(无需任何操作) |
| 15 | +使用 `@evaluation_test` 装饰的函数会自动注册正确的测试名称。 |
| 16 | + |
| 17 | +```python |
| 18 | +# 任何函数名都可以! |
| 19 | +@evaluation_test(...) |
| 20 | +async def my_custom_eval(row: EvaluationRow) -> EvaluationRow: |
| 21 | + # 自动注册为 test_my_custom_eval |
| 22 | + ... |
| 23 | +``` |
| 24 | + |
| 25 | +#### ✅ 文件名:三种方式 |
| 26 | + |
| 27 | +**方式 1:明确指定文件(最简单)** |
| 28 | +```bash |
| 29 | +pytest path/to/any_filename.py |
| 30 | +``` |
| 31 | + |
| 32 | +**方式 2:使用标准命名** |
| 33 | +```bash |
| 34 | +# 文件名: test_*.py |
| 35 | +pytest # 自动发现 |
| 36 | +``` |
| 37 | + |
| 38 | +**方式 3:使用 --ep-discover-all 标志** |
| 39 | +```bash |
| 40 | +pytest --ep-discover-all # 发现所有 .py 文件中的测试 |
| 41 | +``` |
| 42 | + |
| 43 | +## 📋 实现的代码修改 |
| 44 | + |
| 45 | +### 1. 函数名自动注册 |
| 46 | + |
| 47 | +**文件**: `eval_protocol/pytest/evaluation_test.py` |
| 48 | + |
| 49 | +```python |
| 50 | +# 在 decorator 返回之前自动注册 |
| 51 | +original_name = test_func.__name__ |
| 52 | +if not original_name.startswith('test_'): |
| 53 | + import sys |
| 54 | + frame = sys._getframe(1) |
| 55 | + caller_globals = frame.f_globals |
| 56 | + test_name = f'test_{original_name}' |
| 57 | + if test_name not in caller_globals: |
| 58 | + caller_globals[test_name] = dual_mode_wrapper |
| 59 | +``` |
| 60 | + |
| 61 | +**工作原理**: |
| 62 | +- 使用 `sys._getframe(1)` 获取调用者的全局命名空间 |
| 63 | +- 在命名空间中注册 `test_{function_name}` 别名 |
| 64 | +- Pytest 扫描模块时发现这个别名 |
| 65 | + |
| 66 | +### 2. Wrapper 名称修正 |
| 67 | + |
| 68 | +**文件**: `eval_protocol/pytest/parameterize.py` |
| 69 | + |
| 70 | +```python |
| 71 | +# 确保 wrapper 的 __name__ 以 test_ 开头 |
| 72 | +original_name = test_func.__name__ |
| 73 | +if not original_name.startswith('test_'): |
| 74 | + wrapper.__name__ = f'test_{original_name}' |
| 75 | +``` |
| 76 | + |
| 77 | +**文件**: `eval_protocol/pytest/dual_mode_wrapper.py` |
| 78 | + |
| 79 | +```python |
| 80 | +# 确保 dual_mode_wrapper 的 __name__ 以 test_ 开头 |
| 81 | +original_name = test_func.__name__ |
| 82 | +if not original_name.startswith('test_'): |
| 83 | + dual_mode_wrapper.__name__ = f'test_{original_name}' |
| 84 | +``` |
| 85 | + |
| 86 | +### 3. 文件名配置选项 |
| 87 | + |
| 88 | +**文件**: `eval_protocol/pytest/plugin.py` |
| 89 | + |
| 90 | +```python |
| 91 | +def pytest_addoption(parser) -> None: |
| 92 | + group = parser.getgroup("eval-protocol") |
| 93 | + group.addoption( |
| 94 | + "--ep-discover-all", |
| 95 | + action="store_true", |
| 96 | + default=False, |
| 97 | + help=( |
| 98 | + "Discover @evaluation_test in all Python files, " |
| 99 | + "not just test_*.py files." |
| 100 | + ), |
| 101 | + ) |
| 102 | + |
| 103 | +def pytest_configure(config) -> None: |
| 104 | + # 启用发现所有 .py 文件 |
| 105 | + if config.getoption("--ep-discover-all", default=False): |
| 106 | + config.option.python_files = ["*.py"] |
| 107 | +``` |
| 108 | + |
| 109 | +## 🧪 验证和测试 |
| 110 | + |
| 111 | +### 测试文件 |
| 112 | +- `tests/test_auto_discovery_simple.py` - 验证函数名自动注册 |
| 113 | +- `examples/auto_discovery_example.py` - 标准命名示例 |
| 114 | +- `examples/my_evaluation.py` - 非标准命名示例 |
| 115 | + |
| 116 | +### 验证结果 |
| 117 | + |
| 118 | +```bash |
| 119 | +# 1. 非标准文件名 + 非标准函数名 |
| 120 | +$ pytest examples/my_evaluation.py --collect-only -v |
| 121 | +collected 1 item |
| 122 | + <Coroutine test_custom_evaluation[rows(len=1)]> ✅ |
| 123 | + |
| 124 | +# 2. 运行测试 |
| 125 | +$ pytest examples/my_evaluation.py -v |
| 126 | +============================== 1 passed in 0.08s =============================== ✅ |
| 127 | + |
| 128 | +# 3. 标准命名 |
| 129 | +$ pytest examples/auto_discovery_example.py --collect-only -v |
| 130 | +collected 3 items |
| 131 | + <Coroutine test_math_evaluation[rows(len=1)]> ✅ |
| 132 | + <Coroutine test_greeting_evaluation[rows(len=1)]> ✅ |
| 133 | + <Coroutine test_coding_task_evaluation[rows(len=1)]> ✅ |
| 134 | +``` |
| 135 | + |
| 136 | +## 📚 使用示例 |
| 137 | + |
| 138 | +### 示例 1:完全自由的命名 |
| 139 | + |
| 140 | +```python |
| 141 | +# 文件: evals/math.py |
| 142 | +from eval_protocol.pytest import evaluation_test |
| 143 | +from eval_protocol.models import EvaluationRow, EvaluateResult |
| 144 | + |
| 145 | +@evaluation_test( |
| 146 | + input_rows=[[EvaluationRow(messages=[{"role": "user", "content": "2+2"}])]] |
| 147 | +) |
| 148 | +async def evaluate_addition(row: EvaluationRow) -> EvaluationRow: |
| 149 | + row.evaluation_result = EvaluateResult(score=1.0) |
| 150 | + return row |
| 151 | +``` |
| 152 | + |
| 153 | +运行: |
| 154 | +```bash |
| 155 | +pytest evals/math.py -v |
| 156 | +``` |
| 157 | + |
| 158 | +### 示例 2:使用标准命名 |
| 159 | + |
| 160 | +```python |
| 161 | +# 文件: tests/test_math_eval.py |
| 162 | +@evaluation_test(...) |
| 163 | +async def test_addition(row: EvaluationRow) -> EvaluationRow: |
| 164 | + ... |
| 165 | +``` |
| 166 | + |
| 167 | +运行: |
| 168 | +```bash |
| 169 | +pytest tests/ # 自动发现所有 test_*.py |
| 170 | +``` |
| 171 | + |
| 172 | +### 示例 3:混合使用 |
| 173 | + |
| 174 | +```python |
| 175 | +# 文件: test_my_evals.py(标准文件名) |
| 176 | +@evaluation_test(...) |
| 177 | +async def math_accuracy_check(row: EvaluationRow) -> EvaluationRow: |
| 178 | + # 函数名不标准也没问题 |
| 179 | + ... |
| 180 | +``` |
| 181 | + |
| 182 | +运行: |
| 183 | +```bash |
| 184 | +pytest # 自动发现 |
| 185 | +``` |
| 186 | + |
| 187 | +## 🎁 特性总结 |
| 188 | + |
| 189 | +| 特性 | 状态 | 说明 | |
| 190 | +|------|------|------| |
| 191 | +| 函数名自由 | ✅ | 任何函数名都能被发现 | |
| 192 | +| 文件名灵活 | ✅ | 支持明确指定或使用标志 | |
| 193 | +| 零配置 | ✅ | 函数名完全自动处理 | |
| 194 | +| 向后兼容 | ✅ | 不影响现有代码 | |
| 195 | +| 无警告 | ✅ | 静默自动处理 | |
| 196 | + |
| 197 | +## 📖 文档 |
| 198 | + |
| 199 | +- `development/auto_test_discovery.md` - 技术实现细节 |
| 200 | +- `development/file_and_function_naming.md` - 文件名和函数名处理指南 |
| 201 | +- `development/FINAL_SUMMARY.md` - 功能总结 |
| 202 | +- `development/COMPLETE_SOLUTION.md` - 本文档 |
| 203 | + |
| 204 | +## 🚀 推荐用法 |
| 205 | + |
| 206 | +### 最简单:明确指定文件 |
| 207 | +```bash |
| 208 | +pytest path/to/your_file.py |
| 209 | +``` |
| 210 | +- ✅ 任何文件名都可以 |
| 211 | +- ✅ 任何函数名都可以 |
| 212 | +- ✅ 无需额外配置 |
| 213 | + |
| 214 | +### 最传统:使用标准命名 |
| 215 | +```bash |
| 216 | +# 文件: test_*.py |
| 217 | +# 函数: test_* 或任意名称 |
| 218 | +pytest |
| 219 | +``` |
| 220 | +- ✅ 自动发现 |
| 221 | +- ✅ 团队熟悉的方式 |
| 222 | + |
| 223 | +### 最灵活:使用 --ep-discover-all |
| 224 | +```bash |
| 225 | +pytest --ep-discover-all |
| 226 | +``` |
| 227 | +- ✅ 发现所有文件中的测试 |
| 228 | +- ✅ 适合大量非标准命名文件 |
| 229 | + |
| 230 | +## ✨ 总结 |
| 231 | + |
| 232 | +现在使用 `@evaluation_test` 装饰器: |
| 233 | + |
| 234 | +1. **函数名**:完全自由,自动处理 ✅ |
| 235 | +2. **文件名**: |
| 236 | + - 明确指定:`pytest your_file.py` ✅ |
| 237 | + - 标准命名:`test_*.py` 自动发现 ✅ |
| 238 | + - 或使用:`pytest --ep-discover-all` ✅ |
| 239 | + |
| 240 | +**用户只需要使用 `@evaluation_test`,其他都自动完成!** 🎉 |
| 241 | + |
0 commit comments