Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
241 changes: 241 additions & 0 deletions development/COMPLETE_SOLUTION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
# ✅ 完整解决方案:自动测试发现

## 🎯 问题和解决方案

### 问题
Pytest 对测试发现有严格的命名要求:
- 文件名必须是 `test_*.py` 或 `*_test.py`
- 函数名必须以 `test_` 开头

用户希望使用 `@evaluation_test` 装饰器后,无论如何命名都能被发现。

### 解决方案

#### ✅ 函数名:完全自动处理(无需任何操作)
使用 `@evaluation_test` 装饰的函数会自动注册正确的测试名称。

```python
# 任何函数名都可以!
@evaluation_test(...)
async def my_custom_eval(row: EvaluationRow) -> EvaluationRow:
# 自动注册为 test_my_custom_eval
...
```

#### ✅ 文件名:三种方式

**方式 1:明确指定文件(最简单)**
```bash
pytest path/to/any_filename.py
```

**方式 2:使用标准命名**
```bash
# 文件名: test_*.py
pytest # 自动发现
```

**方式 3:使用 --ep-discover-all 标志**
```bash
pytest --ep-discover-all # 发现所有 .py 文件中的测试
```

## 📋 实现的代码修改

### 1. 函数名自动注册

**文件**: `eval_protocol/pytest/evaluation_test.py`

```python
# 在 decorator 返回之前自动注册
original_name = test_func.__name__
if not original_name.startswith('test_'):
import sys
frame = sys._getframe(1)
caller_globals = frame.f_globals
test_name = f'test_{original_name}'
if test_name not in caller_globals:
caller_globals[test_name] = dual_mode_wrapper
```

**工作原理**:
- 使用 `sys._getframe(1)` 获取调用者的全局命名空间
- 在命名空间中注册 `test_{function_name}` 别名
- Pytest 扫描模块时发现这个别名

### 2. Wrapper 名称修正

**文件**: `eval_protocol/pytest/parameterize.py`

```python
# 确保 wrapper 的 __name__ 以 test_ 开头
original_name = test_func.__name__
if not original_name.startswith('test_'):
wrapper.__name__ = f'test_{original_name}'
```

**文件**: `eval_protocol/pytest/dual_mode_wrapper.py`

```python
# 确保 dual_mode_wrapper 的 __name__ 以 test_ 开头
original_name = test_func.__name__
if not original_name.startswith('test_'):
dual_mode_wrapper.__name__ = f'test_{original_name}'
```

### 3. 文件名配置选项

**文件**: `eval_protocol/pytest/plugin.py`

```python
def pytest_addoption(parser) -> None:
group = parser.getgroup("eval-protocol")
group.addoption(
"--ep-discover-all",
action="store_true",
default=False,
help=(
"Discover @evaluation_test in all Python files, "
"not just test_*.py files."
),
)

def pytest_configure(config) -> None:
# 启用发现所有 .py 文件
if config.getoption("--ep-discover-all", default=False):
config.option.python_files = ["*.py"]
```

## 🧪 验证和测试

### 测试文件
- `tests/test_auto_discovery_simple.py` - 验证函数名自动注册
- `examples/auto_discovery_example.py` - 标准命名示例
- `examples/my_evaluation.py` - 非标准命名示例

### 验证结果

```bash
# 1. 非标准文件名 + 非标准函数名
$ pytest examples/my_evaluation.py --collect-only -v
collected 1 item
<Coroutine test_custom_evaluation[rows(len=1)]> ✅

# 2. 运行测试
$ pytest examples/my_evaluation.py -v
============================== 1 passed in 0.08s =============================== ✅

# 3. 标准命名
$ pytest examples/auto_discovery_example.py --collect-only -v
collected 3 items
<Coroutine test_math_evaluation[rows(len=1)]> ✅
<Coroutine test_greeting_evaluation[rows(len=1)]> ✅
<Coroutine test_coding_task_evaluation[rows(len=1)]> ✅
```

## 📚 使用示例

### 示例 1:完全自由的命名

```python
# 文件: evals/math.py
from eval_protocol.pytest import evaluation_test
from eval_protocol.models import EvaluationRow, EvaluateResult

@evaluation_test(
input_rows=[[EvaluationRow(messages=[{"role": "user", "content": "2+2"}])]]
)
async def evaluate_addition(row: EvaluationRow) -> EvaluationRow:
row.evaluation_result = EvaluateResult(score=1.0)
return row
```

运行:
```bash
pytest evals/math.py -v
```

### 示例 2:使用标准命名

```python
# 文件: tests/test_math_eval.py
@evaluation_test(...)
async def test_addition(row: EvaluationRow) -> EvaluationRow:
...
```

运行:
```bash
pytest tests/ # 自动发现所有 test_*.py
```

### 示例 3:混合使用

```python
# 文件: test_my_evals.py(标准文件名)
@evaluation_test(...)
async def math_accuracy_check(row: EvaluationRow) -> EvaluationRow:
# 函数名不标准也没问题
...
```

运行:
```bash
pytest # 自动发现
```

## 🎁 特性总结

| 特性 | 状态 | 说明 |
|------|------|------|
| 函数名自由 | ✅ | 任何函数名都能被发现 |
| 文件名灵活 | ✅ | 支持明确指定或使用标志 |
| 零配置 | ✅ | 函数名完全自动处理 |
| 向后兼容 | ✅ | 不影响现有代码 |
| 无警告 | ✅ | 静默自动处理 |

## 📖 文档

- `development/auto_test_discovery.md` - 技术实现细节
- `development/file_and_function_naming.md` - 文件名和函数名处理指南
- `development/FINAL_SUMMARY.md` - 功能总结
- `development/COMPLETE_SOLUTION.md` - 本文档

## 🚀 推荐用法

### 最简单:明确指定文件
```bash
pytest path/to/your_file.py
```
- ✅ 任何文件名都可以
- ✅ 任何函数名都可以
- ✅ 无需额外配置

### 最传统:使用标准命名
```bash
# 文件: test_*.py
# 函数: test_* 或任意名称
pytest
```
- ✅ 自动发现
- ✅ 团队熟悉的方式

### 最灵活:使用 --ep-discover-all
```bash
pytest --ep-discover-all
```
- ✅ 发现所有文件中的测试
- ✅ 适合大量非标准命名文件

## ✨ 总结

现在使用 `@evaluation_test` 装饰器:

1. **函数名**:完全自由,自动处理 ✅
2. **文件名**:
- 明确指定:`pytest your_file.py` ✅
- 标准命名:`test_*.py` 自动发现 ✅
- 或使用:`pytest --ep-discover-all` ✅

**用户只需要使用 `@evaluation_test`,其他都自动完成!** 🎉

111 changes: 111 additions & 0 deletions development/FINAL_SUMMARY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# ✅ 完成:自动测试发现功能

## 目标

确保所有使用 `@evaluation_test` 装饰的函数都能被 pytest 自动发现,无论函数名是否符合 pytest 命名规范。

## 实现方案

### 核心机制:自动注册

当函数名不以 `test_` 开头时,decorator 会:
1. 自动在调用者的全局命名空间中注册一个以 `test_` 开头的别名
2. Pytest 扫描模块时会发现这个别名
3. 用户无需修改任何代码或命名

### 代码修改

#### 1. `eval_protocol/pytest/evaluation_test.py`
- ✅ 移除了警告功能
- ✅ 添加了自动注册逻辑(使用 `sys._getframe` 访问调用者的全局命名空间)

#### 2. `eval_protocol/pytest/parameterize.py`
- ✅ 确保 wrapper 的 `__name__` 属性以 `test_` 开头

#### 3. `eval_protocol/pytest/dual_mode_wrapper.py`
- ✅ 确保 dual_mode_wrapper 的 `__name__` 属性以 `test_` 开头

## 使用示例

```python
from eval_protocol.pytest import evaluation_test
from eval_protocol.models import EvaluationRow, EvaluateResult

# ✅ 不需要以 test_ 开头 - 会自动注册为 test_my_evaluation
@evaluation_test(
input_rows=[[EvaluationRow(messages=[{"role": "user", "content": "Hello"}])]]
)
async def my_evaluation(row: EvaluationRow) -> EvaluationRow:
row.evaluation_result = EvaluateResult(score=1.0)
return row

# ✅ 已经符合命名规范 - 正常工作
@evaluation_test(
input_rows=[[EvaluationRow(messages=[{"role": "user", "content": "Hello"}])]]
)
async def test_my_evaluation(row: EvaluationRow) -> EvaluationRow:
row.evaluation_result = EvaluateResult(score=1.0)
return row
```

## 验证结果

```bash
$ pytest --collect-only tests/test_auto_discovery_simple.py -v
collected 2 items

<Module test_auto_discovery_simple.py>
<Coroutine test_my_custom_eval[rows(len=1)]> # 自动注册!
<Coroutine test_proper_eval[rows(len=1)]>

$ pytest tests/test_auto_discovery_simple.py -v
============================== 2 passed in 0.15s ==============================
```

## 特点

### ✅ 优点
1. **零配置**:无需任何额外配置
2. **无需警告**:静默自动处理,不打扰用户
3. **完全兼容**:不影响已有代码
4. **简单直接**:用户只需使用 `@evaluation_test`,其他都自动处理
5. **可靠**:经过测试验证

### 🎯 工作原理
- Pytest 通过扫描模块的全局命名空间来发现测试
- 我们在装饰时自动在命名空间中注册正确命名的别名
- 用户原始函数名保持不变,可以继续使用

## 测试覆盖

- ✅ `tests/test_auto_discovery_simple.py` - 验证自动发现功能
- 测试不以 `test_` 开头的函数能被发现
- 测试以 `test_` 开头的函数正常工作
- 所有测试通过

## 文档

- `development/auto_test_discovery.md` - 详细技术文档
- `development/FINAL_SUMMARY.md` - 本文档

## 总结

现在,用户只需要:

```python
@evaluation_test(...)
async def any_function_name(row: EvaluationRow) -> EvaluationRow:
# 无论函数名是什么,都能被 pytest 发现!
...
```

**就这么简单!** 🎉

不需要:
- ❌ 记住命名规范
- ❌ 收到警告信息
- ❌ 手动配置 pytest
- ❌ 修改现有代码

只要使用 `@evaluation_test`,就能保证测试被发现!✨

Loading
Loading