Bazel: Taming Bulky bazel test Output

2025/09/20

BUILDbazel🍞

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

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

  1. Open the CI log viewer. Search for bazel coverage (or bazel test). Jump to the last occurrence.
  2. A few lines after the command, you should see the test summary with counts and status.
  3. 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...

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:

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.