A bazel test run on a large monorepo dumps thousands of lines. Somewhere in there is the one test that failed and the one line that explains why. Finding it shouldn’t require a PhD in log archaeology, but sometimes it feels that way.
Here’s how to cut through the noise.
Control the verbosity
Two flags determine what Bazel prints after tests run. Getting these right eliminates most of the scrolling.
--test_output
Controls whether Bazel prints test process output (stdout/stderr from the test binary):
| Value | Behavior |
|---|---|
errors (default) |
Logs for failed tests only |
summary |
No log output at all |
all |
Logs for every test – usually too much |
--test_summary
Controls the pass/fail summary at the end:
| Value | Behavior |
|---|---|
short (default) |
PASS or FAIL per target |
terse |
Like short but hides passing tests |
detailed |
Individual test cases within each target |
testcase |
Aggregate pass/fail counts per target |
For daily work, --test_output=errors --test_summary=terse is the sweet spot. You see only what broke and nothing else.
Reading the output structure
When a test fails with --test_output=errors, Bazel prints something like this:
(06:45:06) [5,553 / 5,574] ... GoLink ...
INFO: From Testing //business/recuit/recruit_provider:recruit_provider_test:
==================== Test output for //business/recuit/recruit_provider:recruit_provider_test:
... (output from the Go test binary) ...
=== RUN Test_checkRecruitProviderConfig
...
--- FAIL: Test_checkRecruitProviderConfig (0.04s)
... stack trace ...
FAIL
coverage: 2.3% of statements
--
Coverage runner: Not collecting coverage for failed test.
... path to the test binary that failed ...
================================================================================
Landmarks to orient by
==================== Test output for <target>:marks the start of a test target’s output. Everything between this and the closing================is what the test binary wrote.=== RUN <case>starts an individual test case.--- PASS/FAIL: <case>ends it.- Logger output (lines like
Error 2025-09-19 ...) can appear both during the test case and after--- FAIL. When you see duplicates, the one nearest after--- FAIL:is usually the most relevant context – but not always. Some cases have no recap after the FAIL line.
Triaging on CI web UIs
CI log viewers add their own layer of pain: truncation, slow rendering, no regex search. Here’s a workflow that minimizes suffering.
Find the summary
- Open the CI log viewer. Search for
bazel coverage(orbazel test). Jump to the last occurrence. - A few lines after the command, you should see the test summary with counts and status.
- If the summary is missing – the build failed before tests ran, or the UI truncated the tail – search for the last progress line instead:
- With failures:
[5,564 / 5,582] 36 / 53 tests, 1 failed; GoLink... - Without failures:
[5,564 / 5,582] 36 / 53 tests; GoLink...
- With failures:
Investigate failures
Search for --- FAIL: <case> and read downward. Then search RUN <case> (three spaces) to jump to the start of that case. Now you have the full picture: what ran, what the error was, and the stack trace.
When the web viewer isn’t enough
Download the log as a file. Name it ci_<commit_short_sha>.log – you can extract the SHA from the log itself (look for CI_EVENT_CHANGE_TARGET_SHA). With the file local, you can use real tools: grep, ripgrep, your editor. And you can checkout that exact commit to reproduce the failure.
Common gotchas
Missing test summaries
Three usual suspects:
- The CI viewer hit its size cap and truncated the output.
- The build or link step failed before tests ran, so there’s no test summary to print.
- Remote cache or download issues killed the command early.
Fix: configure your CI to upload bazel-testlogs/**/test.log as artifacts regardless of exit code. Even when the console output is gone, the test logs survive.
Stack traces outside FAIL markers
Go’s testing package prints --- FAIL: as soon as it determines the result. Then it dumps the logs – panics, t.Log output, etc. With goroutines, output gets interleaved. A worker goroutine might print after the main test goroutine has already been marked failed.
The result: the panic block appears “after” or “outside” the FAIL envelope. It’s not a Bazel bug. It’s Go’s test runner printing status and output in separate passes.
When parsing logs, treat --- FAIL: as your anchor and collect the next panic block that follows, even if it looks like it belongs to a different section.