Benchmark records an exercise as solved when the test command exits non-zero with no output
benchmark/benchmark.py:1038-1048 (run_unit_tests, defined at :981), read by its caller at :881-892; solved exercises are counted at :508. Code as of 5dc9490 (current main).
success = result.returncode == 0
res = result.stdout
res = cleanup_test_output(res, testdir)
...
if not success:
print(f"Tests failed: {testdir}")
return res try:
errors = run_unit_tests(original_dname, testdir, history_fname, test_files)
except subprocess.TimeoutExpired:
errors = "Tests timed out!"
timeouts += 1
if errors:
test_outcomes.append(False)
else:
test_outcomes.append(True)
breakrun_unit_tests reports a failing run by returning the runner's output and a passing run by returning None. The return code decides only whether that text is returned; the caller then decides pass or fail from whether the text is non-empty. A test command that exits non-zero with empty output returns "", so the exercise is recorded as solved (test_outcomes.append(True)) straight after the function has printed "Tests failed".
Stderr is merged into stdout (stderr=subprocess.STDOUT, :1030), so this needs a process that dies before it writes anything. With pytest that happens when the process is killed, or exits through os._exit, before collection finishes, for example while importing the solution module: the header has not been flushed yet. A process that dies inside a test, after collection, has already written its header and is scored correctly.
Measured
Method: run_unit_tests and cleanup_test_output were extracted with ast from 5dc9490 and executed in a plain Python process, and the caller's if errors: test was applied to the return value. pytest on PATH was a shim that runs python -m pytest (pytest 9.1.1, Python 3.12.4). The dump helper was replaced with a no-op. benchmark.py itself was not run, and no exercise was run through a model.
| Case | Returned | Recorded | Expected |
|---|---|---|---|
| control: solution wrong, one failing test | runner output | failed | failed |
| control: solution right | None |
passed | passed |
| solution kills its own process with SIGKILL while pytest imports it | "" |
passed | failed |
solution calls os._exit(1) while pytest imports it |
"" |
passed | failed |
| control: process killed inside a test, after collection | runner output | failed | failed |
Not measured: how often a real run ends a test process with no output (for example an out-of-memory kill during import), and the other runners in TEST_COMMANDS (cargo, go, gradlew, npm-test.sh, cpp-test.sh), which were not run.
Consequence
An exercise whose test process dies before printing anything is counted in pass_rate_1 / pass_rate_2 (:508-513) as solved. benchmark/README.md:125-126 defines those figures as the percent of tasks that had all tests passing, and the README invites benchmark results to be submitted for the leaderboard data files (:143-144).
Related, with a different mechanism: #5681 and #5688 (a model-created conftest.py that makes the runner exit 0 while tests fail). That change distrusts an exit code of 0; the if not success: return res path above is not touched by it.
Suggested fix
The same function already computes success = result.returncode == 0 (:1038). Return a non-empty message whenever not success, for example return res or f"Tests failed with exit code {result.returncode}", so the caller's truthiness test can no longer read a failure as a pass.
Source: Aider-AI/aider