Zig Version
0.16.0
Steps to Reproduce and Observed Behavior
Also reproduces on master (lib/std/Build/Step/Run.zig as of 2026年05月04日 — the relevant code at lines 1635-1655 is unchanged in spirit from 0.16.0).
Run zig build test against any project where the test executable is linked against a native library that emits informational lines to stderr during normal test execution (libdatachannel, OpenSSL with verbose logging, any C++ library that logs through std::cerr, etc.). All tests pass and Build Summary reports success, but stderr looks like this:
[rtc:2] rtc::impl::DtlsTransport::doRecv@597: DTLS recv: Handshake failed: SSL - The connection indicated an EOF
[rtc:2] rtc::impl::DtlsTransport::doRecv@605: DTLS handshake failed
... (many similar lines from the linked library) ...
failed command: ./.zig-cache/o/<hash>/test --cache-dir=./.zig-cache --seed=0x... --listen=-
Build Summary: 17/17 steps succeeded; 331/331 tests passed
test success
+- run test 331 pass (331 total)
+- ...
echo $? after the build returns 0. The "failed command:" line is misleading — the build summary correctly reports success and nothing has actually failed. The only signal something is wrong is the stale line in stderr.
Easiest way to reproduce locally: link libc and pthread, spawn a thread that calls fprintf(stderr, ...) once per test, and run zig build test.
Tracing the cause:
lib/std/Build/Step/Run.zig, in spawnChildAndCollect, sets step.result_failed_command as a placeholder before running the child (line 1635-1636 on master):
// If an error occurs, it's caused by this command:
assert(run.step.result_failed_command == null);
run.step.result_failed_command = try Step.allocPrintCmd(options.gpa, child.cwd, argv);
The non-zig_test path through evalGeneric clears this placeholder on success (line 1426). The zig_test path (line 1638-1655) does not — it returns null directly without freeing or nulling the field.
Then lib/compiler/build_runner.zig line 1383-1390 calls printErrorMessages whenever there is any error/warning content OR result_stderr.len > 0:
if (s.result_error_bundle.errorMessageCount() > 0 or
s.result_error_msgs.items.len > 0 or
s.result_stderr.len > 0)
{
...
printErrorMessages(...) catch {};
}
printErrorMessages then unconditionally prints failed command: ... if failing_step.result_failed_command is non-null (line 1515-1523), regardless of whether the step actually failed.
So benign child stderr is enough to flush a stale placeholder string out as if the command had failed.
Expected Behavior
When zig build test succeeds, no failed command: line should appear on stderr. The Build Summary already reports success; the additional line implies the test step failed when it did not.
To fix, two reasonable repair sites:
-
Clear step.result_failed_command on the zig_test success path in Run.zig (mirrors the existing clear at line 1426 in the foreign-target path), or
-
In build_runner.zig's printErrorMessages, skip the failed command: block when failing_step.state == .success.
Happy to send a PR for whichever the maintainers prefer. (1) is a ~5-line change, (2) is a one-liner.
### Zig Version
0.16.0
### Steps to Reproduce and Observed Behavior
Also reproduces on master (lib/std/Build/Step/Run.zig as of 2026年05月04日 — the relevant code at lines 1635-1655 is unchanged in spirit from 0.16.0).
Run zig build test against any project where the test executable is linked against a native library that emits informational lines to stderr during normal test execution (libdatachannel, OpenSSL with verbose logging, any C++ library that logs through std::cerr, etc.). All tests pass and Build Summary reports success, but stderr looks like this:
```
[rtc:2] rtc::impl::DtlsTransport::doRecv@597: DTLS recv: Handshake failed: SSL - The connection indicated an EOF
[rtc:2] rtc::impl::DtlsTransport::doRecv@605: DTLS handshake failed
... (many similar lines from the linked library) ...
failed command: ./.zig-cache/o/<hash>/test --cache-dir=./.zig-cache --seed=0x... --listen=-
Build Summary: 17/17 steps succeeded; 331/331 tests passed
test success
+- run test 331 pass (331 total)
+- ...
```
echo $? after the build returns 0. The "failed command:" line is misleading — the build summary correctly reports success and nothing has actually failed. The only signal something is wrong is the stale line in stderr.
Easiest way to reproduce locally: link libc and pthread, spawn a thread that calls fprintf(stderr, ...) once per test, and run zig build test.
**Tracing the cause:**
lib/std/Build/Step/Run.zig, in spawnChildAndCollect, sets step.result_failed_command as a placeholder before running the child (line 1635-1636 on master):
// If an error occurs, it's caused by this command:
```
assert(run.step.result_failed_command == null);
run.step.result_failed_command = try Step.allocPrintCmd(options.gpa, child.cwd, argv);
```
The non-zig_test path through evalGeneric clears this placeholder on success (line 1426). The zig_test path (line 1638-1655) does not — it returns null directly without freeing or nulling the field.
Then lib/compiler/build_runner.zig line 1383-1390 calls printErrorMessages whenever there is any error/warning content OR result_stderr.len > 0:
```
if (s.result_error_bundle.errorMessageCount() > 0 or
s.result_error_msgs.items.len > 0 or
s.result_stderr.len > 0)
{
...
printErrorMessages(...) catch {};
}
```
printErrorMessages then unconditionally prints failed command: ... if failing_step.result_failed_command is non-null (line 1515-1523), regardless of whether the step actually failed.
So benign child stderr is enough to flush a stale placeholder string out as if the command had failed.
### Expected Behavior
When zig build test succeeds, no failed command: line should appear on stderr. The Build Summary already reports success; the additional line implies the test step failed when it did not.
To fix, two reasonable repair sites:
1. Clear step.result_failed_command on the zig_test success path in Run.zig (mirrors the existing clear at line 1426 in the foreign-target path), or
2. In build_runner.zig's printErrorMessages, skip the failed command: block when failing_step.state == .success.
Happy to send a PR for whichever the maintainers prefer. (1) is a ~5-line change, (2) is a one-liner.