跳到主要内容

验证与数据集组装(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:56run_validation:

  1. 准备基线(pre-gold):跑一次干净仓库的测试。常规模式下整个 repo 只跑一次 pre-gold(valid.py:203main 里,按 repo 缓存到 <repo>.ref 目录);若 profile 设了 min_pregold,则每个实例单独跑(valid.py:74)。
  2. 打 bug patch 跑测试(post-gold):run_patch_in_container(valid.py:102)。
  3. 超时则记 timed_out 直接判废(valid.py:110)。
  4. get_valid_report 出对比报告(valid.py:131)。
  5. 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.pylog_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.jsonharness/gather.py有效实例转成 SWE-bench 风格任务并落到 mirror 仓库:

  1. 门禁(process_instance,gather.py:328-339):必须 FAIL_TO_PASS>0PASS_TO_PASS>0 且没超时(PR-mirror 实例有放宽:pr_exception)。
  2. clone mirror 仓库 → git checkout -b <instance_id> → apply bug patch → commit(gather.py:466-537)。apply 用三种命令逐个试(git apply / --reject / patch --fuzz,常量 GIT_APPLY_CMDSconstants.py:40),容忍轻微漂移。
  3. 删掉 F2P 测试文件并单独 commit(gather.py:539-558):因为这些测试是「评判答案」用的,不能让 agent 在仓库里直接看到。
  4. 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.pyrun_validationmainprint_report
状态对比(闸门核心)swesmith/harness/grading.pyget_valid_reportread_test_outputget_eval_tests_report
在容器里跑 patchswesmith/harness/utils.pyrun_patch_in_containerrun_threadpool
只跑相关测试swesmith/profiles/base.pyget_test_cmdget_test_files_get_cached_test_paths
pytest 日志解析swesmith/profiles/python.pylog_parser
组装成数据集swesmith/harness/gather.pyprocess_instancecheck_if_branch_exists
apply 命令清单swesmith/constants.pyGIT_APPLY_CMDS