验证与数据集组装(harness)
上一章造出了一大堆候选 bug——但谁都不知道哪些真的有用。本章讲怎么用「跑测试」把它们筛真,并把幸存者拼成数据集。这是整个项目的质量闸门。
1. 它要解决的小问题
造 bug 是「乱拳」:改了运算符,可能根本没改变行为;删了个分支,可能没有测试覆盖它。一个 bug 有没有用,唯一标准是:它是否让原本通过的测试变红了?
这就是 SWE-bench 体系的核心信号 FAIL_TO_PASS——「打了修复 patch 后,从失败变成通过」的测试。在造 bug 语境里反过来理解:bug 让这些测试 fail,正确修复才能让它们 pass。同时要有 PASS_TO_PASS(无关测试始终通过),保证这是个局部 bug 而非把整个仓库炸了。
2. 验证主线:pre-gold vs post-gold
核心是一次「对照实验」。怎么读:左边是基线(干净仓库),右边是打了 bug 的仓库,比较同名测试的状态变化。
① 干净仓库跑测试 ──▶ pre-gold 日志 (基线:谁通过谁失败)
② 打上 bug patch 跑 ──▶ post-gold 日志 (改后:谁通过谁失败)
│
③ 对比同名测试的状态
▼
PASS → FAIL = 这个 bug 弄挂的测试 (记为 FAIL_TO_PASS)
PASS → PASS = 不受影响的测试 (记为 PASS_TO_PASS)
命名上有个坑:验证阶段的报告键直接叫
FAIL_TO_PASS/PASS_TO_PASS,但语义算的是「基线 vs 加 bug」。在gather.py入库时,会把PASS_TO_FAIL翻成FAIL_TO_PASS以贴合 SWE-bench 命名约定(harness/gather.py:346注释明说 "Flip PASS_TO_FAIL to FAIL_TO_PASS")。读代码时以注释为准。
单个实例的验证流程在 harness/valid.py:56 的 run_validation:
- 准备基线(pre-gold):跑一次干净仓库的测试。常规模式下整个 repo 只跑一次 pre-gold(
valid.py:203的main里,按 repo 缓存到<repo>.ref目录);若 profile 设了min_pregold,则每个实例单独跑(valid.py:74)。 - 打 bug patch 跑测试(post-gold):
run_patch_in_container(valid.py:102)。 - 超时则记
timed_out直接判废(valid.py:110)。 - 调
get_valid_report出对比报告(valid.py:131)。 - 按
FAIL_TO_PASS是否非空,返回0_f2p(没用)或1+_f2p(有效)。
3. 核心机制:状态对比 get_valid_report
这是闸门的心脏(harness/grading.py:40)。它把两边日志各自解析成「测试名 → 状态」的字典(log_parser,每个 profile 自带),然后逐个测试归类:
# 示意,非源码 —— 对比 pre/post 两个状态字典
for case in postgold_sm:
if case not in pregold_sm: # 改后才出现的测试,跳过(无法对照)
continue
pre, post = pregold_sm[case], postgold_sm[case]
if pre == PASS and post == PASS: report[PASS_TO_PASS].append(case)
elif pre == FAIL and post == PASS: report[FAIL_TO_PASS].append(case)
elif pre == FAIL and post == FAIL: report[FAIL_TO_FAIL].append(case)
elif pre == PASS and post == FAIL: report[PASS_TO_FAIL].append(case) # ← bug 真正弄挂的
真实实现 harness/grading.py:68-90。注意它只看两边都出现的测试(case not in pregold_sm: continue),避免拿「新冒出来的测试」做不公平比较。
日志怎么变成状态字典? 靠 profile 的 log_parser。Python 版用正则扫 pytest 输出(profiles/python.py 的 log_parser:对每行匹配 ^(\S+)\s+<状态>,把 PASSED/FAILED/... 映成 TestStatus)。测试输出还被 >>>>> Start/End Test Output 标记包裹,read_test_output(grading.py:21)负责截取这段、并识别 APPLY_PATCH_FAIL/超时等失败前缀。
4. 性能优化:min_testing(只跑相关测试)
跑全套测试套件很慢。若 profile 设了 min_testing,验证时只跑和被改文件相关的测试。RepoProfile.get_test_cmd(profiles/base.py:564)是这套启发式的所在:
- 优先用实例自带的
test_patch(PR-mirror 来的)直接定位测试文件(base.py:592)。 - 否则按被改文件名猜测试文件名:
foo.py→ 找test_foo.py/foo_test.py等四种常见命名(base.py:616-622)。 - 再退一步:找同名测试目录、或
test_<父目录名>之类(base.py:626-643)。
这是「精度 vs 速度」的工程取舍:少跑测试更快,但要靠命名约定猜对相关测试。
5. 组装数据集:gather 把幸存者推成分支
验证产出每个实例的 report.json。harness/gather.py 把有效实例转成 SWE-bench 风格任务并落 到 mirror 仓库:
- 门禁(
process_instance,gather.py:328-339):必须FAIL_TO_PASS>0且PASS_TO_PASS>0且没超时(PR-mirror 实例有放宽:pr_exception)。 - clone mirror 仓库 →
git checkout -b <instance_id>→ apply bug patch → commit(gather.py:466-537)。apply 用三种命令逐个试(git apply/--reject/patch --fuzz,常量GIT_APPLY_CMDS在constants.py:40),容忍轻微漂移。 - 删掉 F2P 测试文件并单独 commit(
gather.py:539-558):因为这些测试是「评判答案」用的,不能让 agent 在仓库里直接看到。 git push origin <instance_id>:每个 bug = mirror 仓库上的一个分支。最终任务列表写到logs/task_insts/<run_id>.json。
「一个 bug 一个分支」这个设计很关键:加载数据集时,RepoProfile.get_container 就是 git checkout <instance_id> 把容器切到对应分支(profiles/base.py:493)。
6. 边界与坑
- flaky 测试会污染对比:如果某测试本身时好时坏,可能被误判成 F2P。项目靠「整 repo 单次 pre-gold 基线 + 状态精确匹配」缓解,但不能根除。
- apply 失败的 bug 直接丢弃(
gather.py:482),所以造 bug 时补丁格式要规整(这也是generate_patch_fast严格生成 git 兼容 diff 的原因)。 - 验证强依赖 Docker,且项目明说只支持 Ubuntu、不支持 Windows/macOS(README)。
7. 本章代码地图
| 主题 | 文件 | 关键符号 |
|---|---|---|
| 单实例验证流程 | swesmith/harness/valid.py | run_validation、main、print_report |
| 状态对比(闸门核心) | swesmith/harness/grading.py | get_valid_report、read_test_output、get_eval_tests_report |
| 在容器里跑 patch | swesmith/harness/utils.py | run_patch_in_container、run_threadpool |
| 只跑相关测试 | swesmith/profiles/base.py | get_test_cmd、get_test_files、_get_cached_test_paths |
| pytest 日志解析 | swesmith/profiles/python.py | log_parser |
| 组装成数据集 | swesmith/harness/gather.py | process_instance、check_if_branch_exists |
| apply 命令清单 | swesmith/constants.py | GIT_APPLY_CMDS |