数据截至 (上游 commit 7a21e0577295)
第 3 章 · 判分内核(日志 → status map → resolved)
这是「分数」真正诞生的地方。输入是上一章产出的测试日志
test_output.txt,输出是这道题resolved: true/false。三步走:切日志 → 解析成状态表 → 和 F2P/P2P 比对。
3.1 第一步:从日志切出测试输出、解析成 status map
get_logs_eval(swebench/harness/grading.py:113)做两件事:
- 先排雷:若日志里出现
APPLY_PATCH_FAIL/RESET_FAILED/TESTS_ERROR/TESTS_TIMEOUT任一标记,直接返回「没测出结果」(:60-73)。 - 切段 + 解析:用第 2 章的两个标记把测试输出段切出来,再交给按仓库选定的解析器:
# 真实源码节选 (grading.py:53, 79-89)
log_parser = MAP_REPO_TO_PARSER[repo]
...
test_content = content.split(START_TEST_OUTPUT)[1].split(END_TEST_OUTPUT)[0]
status_map = log_parser(test_content, test_spec)
if not status_map: # Modal 环境下输出可能跑到标记外
status_map = log_parser(content, test_spec) # 退而解析整段日志
这是它在干嘛:不同测试框架的输出长得完全不同,所以每个仓库映射到一个专门的解析器(MAP_REPO_TO_PARSER,swebench/harness/log_parsers/__init__.py:10)。解析结果是 {测试名: 状态} 的字典(status map)。
3.1.1 解析器举例:pytest vs Django
- pytest(
parse_log_pytest,log_parsers/python.py:21-42):pytest -rA的每行形如PASSED test_x.py::test_foo。解析器逐行看开头是否是某个TestStatus,是则把行[1](测试名)映射到行[0](状态)。 - Django(
parse_log_django,:64+):Django 的 runner 输出是test_foo ... ok这种,解析器改为匹配... ok/... FAIL/... ERROR等后缀。
状态枚举见 TestStatus(constants/__init__.py:11-17):PASSED/FAILED/SKIPPED/ERROR/XFAIL。
3.2 第二步:定义「通过」和「失败」
判分前先统一口径(grading.py:85-109):
# 真实源码
def test_passed(case, sm): # 在 map 里且状态是 PASSED 或 XFAIL
return case in sm and sm[case] in [TestStatus.PASSED.value, TestStatus.XFAIL.value]
def test_failed(case, sm): # 不在 map 里,或状态是 FAILED/ERROR
return case not in sm or sm[case] in [TestStatus.FAILED.value, TestStatus.ERROR.value]
注意两个细节:XFAIL(预期失败)算通过;一个测试根本没出现在日志里算失败——保守判定,防止「测试压根没跑」被误当成功。
3.3 第三步:和 F2P/P2P 比对
get_eval_tests_report(grading.py:179)拿 status map 去逐个核对 gold 给的两个列表:
- 遍历
FAIL_TO_PASS的每个测试 → 用test_passed分进 success / failure(:144-147)。 - 遍历
PASS_TO_PASS的每个测试 → 同样分桶(:150-153)。
比对矩阵(注释在 :110-120):
| gold 类别 | 评测结果 | 含义 |
|---|---|---|
| F2P | 通过 | 成功(问题被解决) |
| F2P | 失败 | 失败(没解决) |
| P2P | 通过 | 成功(没改坏) |
| P2P | 失败 | 失败(改坏了别的) |
然后算两个比率(:194-212):compute_fail_to_pass = F2P 通过数 / F2P 总数;compute_pass_to_pass 同理。空集合按惯例记 1(:199-200)。
3.4 最终判定:resolved 三档
get_resolution_status(grading.py:309-326)给出三档结论:
「怎么读:必须两条线都满分,才算 FULL(真正解决)。」
f2p == 1 且 p2p == 1 ─────────────► RESOLVED_FULL ✓ 算解决
0 < f2p < 1 且 p2p == 1 ────────────► RESOLVED_PARTIAL 部分
其它(含 p2p 没满分 = 改坏了) ────────► RESOLVED_NO ✗
而 get_eval_report(:235)里只有 FULL 才把 resolved 置 True(:289-290)。换句话说:只要弄坏了任何一个 P2P 测试,或没修好全部 F2P 测试,这道题就不算解决。这正是 SWE-bench 难刷分的根本原因——它要的是「真修好且没副作用」。
3.5 报告对象:每道题的四个布尔位
get_eval_report 产出的 report_map[instance_id] 带四个关键标志(:256-261):patch_is_None(补丁是空)、patch_exists、patch_successfully_applied(日志成功解析出结果)、resolved。逐层短路:补丁为空就直接返回,解析不出结果就停在 patch_successfully_applied=False。
3.6 一个边界:只有失败信号的仓库
少数前端仓库(chartjs/Chart.js、processing/p5.js、markedjs/marked,见 FAIL_ONLY_REPOS,constants/__init__.py:72)的测试框架不会逐个报「通过」,只报失败。对它们用 EvalType.FAIL_ONLY(判定分发见 grading.py:231):判定逻辑反过来——只要某测试没被明确标 FAILED 就当通过(check_fail_only,grading.py:221)。
3.7 汇总成 run report
所有题判完,make_run_report(swebench/harness/reporting.py:16)扫每道题的 report.json,分桶成 resolved / unresolved / error / empty_patch / incomplete,打印汇总并落盘成 <model>.<run_id>.json(:127-159)。这份文件里的 resolved_ids 数量除以总数,就是大家常引用的「SWE-bench 解决率」。
3.8 小结
- 判分三步:切日志段 → 按仓库选解析器得 status map → 和 F2P/P2P 比对。
- 缺席的测试算失败;XFAIL 算通过;保守。
resolved只认 FULL:F2P 全过 且 P2P 全过。- 个别前端仓库走「只看失败」的反向判定。