Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Build, Test, And Coverage Commands

bazel run @rules_dx//:dx -- build //...
bazel run @rules_dx//:dx -- test //...
bazel run @rules_dx//:dx -- coverage //...

No scope means //.... Pass a label, pattern, file, or dir to narrow it. Use --here for the current dir tree, or --affected for the targets your git changes reach (see Scope Defaults). File scopes resolve to the owning targets. For dx test and dx coverage, a file runs the tests that depend on its owners. A test tagged manual runs only when it owns the file. Args after -- go to Bazel unchanged. dx build, dx test, dx coverage, dx run and dx deploy also pass --@rules_dx//config:validate=false, --@rules_dx//config:clippy_output_diagnostics=true and --@rules_dx//config:rustc_output_diagnostics=true. Another value for one of them, or for the matching @rules_rust//rust/settings flag, fails before Bazel starts with exit 2. dx spells them with the name your MODULE.bazel gives rules_dx, for example --@my_dx//config:validate=false when repo_name = "my_dx".

Ctrl-C, SIGTERM or SIGHUP stops a run. A SIGHUP that was ignored when dx started, as under nohup, stays ignored. Ctrl-C, and a signal sent to dx itself, even from outside a container, reach Bazel or the app once. On macOS in a terminal, a SIGINT or SIGHUP sent to dx alone is not passed on, and dx stops once Bazel or the app ends; send SIGTERM to stop it right away. Outside a terminal, every process dx started gets it too, and any still running after 10 seconds is killed. A stopped run prints dx: cancelled: stopped by signal N, exits 128 plus the signal number, and emits no command_finished event. The shared Bazel server keeps running. If Bazel dies by a signal dx did not get, dx reports a bazel_signalled error and exits 1.

Outside a terminal, if dx is killed with SIGKILL, Linux stops the process dx started. Anything else in that process group keeps running until the next dx run in that worktree stops it, prints dx: stopped leftover process group <id> from dx run <pid>, which is gone, and with --output=json emits a notice event with code orphan_process_group_stopped, after command_started. On macOS the next run stops the whole tree. In a terminal, a SIGKILL to dx alone leaves its children running. Outside a terminal, when the process dx started exits, dx stops anything still running in its process group. A dx that exits on its own never leaves anything for the next run to stop.

dx build And dx test

dx build [--here] [--affected] [--debug|--release] [--report failures=<path>] [scope...] [-- bazel-options...]
dx test [--here] [--affected] [--debug|--release] [--report junit=<path>] [--report failures=<path>] [--report results=<path>] [scope...] [-- bazel-options...]

Builds or tests the scope. --debug uses the dx_debug profile, --release uses dx_release. No flag uses dx_dev. The two flags conflict. --debug changes compilation only. It does not start or attach a debugger. See Supported Surface. Exec tools build at fastbuild; --release builds them at opt. Extra --config values pass through after the dx profile in their original order. Only a conflicting dx profile is rejected. dx adds its own report flags after your Bazel options, so a --config cannot change them. dx test and dx coverage reject --remote_download_minimal, --remote_download_toplevel and any --remote_download_outputs other than all. dx coverage also rejects any --combined_report other than lcov. dx writes its build event file with --build_event_json_file_path_conversion=false, so it rejects --build_event_json_file and any other --build_event_json_file_path_conversion value. A rejected flag fails before Bazel starts with exit 2.

dx forces these flags and treats them as protected: --skip_incompatible_explicit_targets (build, test and coverage), the three --@rules_dx//config: settings for validation and rustc and clippy diagnostics, --remote_download_outputs=all (test and coverage), --combined_report=lcov (coverage), and --build_event_json_file. Another value for one of them, and Bazel startup options after --, exit 2.

Output: --output text|json. Reports: dx test writes --report junit=<path> JUnit reports. Repeat the flag for more files. Use - for stdout. A relative path anchors at the workspace root; an absolute one writes as given. Colliding destinations fail before anything runs, parents must exist, and a failed write keeps the previous file bytes. JUnit case names gain [run=N] or [config=<id>] when a target has several runs or configurations, and [shard=N,attempt=N] for later shards and attempts. A test.xml with invalid UTF-8 still counts; dx replaces the bad bytes and prints a dx: warning: on stderr. dx test --output=json adds one test_outcome event per test result except a plain pass (passed or no_cases with complete evidence), with its Bazel status, run, shard, attempt, cached state, duration, and case counts. A failing result also carries failing_cases (up to 10, each with classname, name, outcome and message), log_path, and log_tail (the last 40 lines, at most 8 KiB of test.log). artifacts and evidence_complete appear only when evidence is missing. final is false on an attempt that a later attempt of the same target, configuration, run and shard replaced, and appears only then. Plain passes are not events; passed_after_retry is. When dx collected any test result, command_finished has tests, counted once per result (per target, configuration, run and shard, so a target with four shards counts four), with passed (every result that passed, including passed_after_retry and no_cases, matching the passed entries in the results report), failed (results whose final attempt has outcome failed or timeout), cached (passed results that came from the cache), and no_cases (passed results whose report had no test cases; part of passed), and skipped (tests with outcome skipped or filtered). Earlier attempts of a retried test still appear as test_outcome events with final: false and are not counted.

A test that Bazel skipped has outcome skipped. A test that a tag or size filter removed from the run has outcome filtered. Neither changes how the run ends. When any test was filtered, dx emits one notice event with code tests_filtered that gives the count and the first labels. In text mode it prints dx: tests_filtered: ... on stderr.

--report results=<path> writes one JSON file with every test result, passed or not. It follows the same path rules as the other reports. With --output=json and no results report, dx writes it to .dx/results.json in the workspace and emits a report event with the path. A failed write of that default file fails the run, like a failed explicit --report results=<path>: dx keeps Bazel's nonzero code or else exits nonzero, results_complete is false, and a report_failed error is emitted. To write the file somewhere else, pass --report results=<path>. Such a run removes the old .dx/results.json first, even when it stops before Bazel starts. Text runs and runs with a results report leave it alone. The file has:

  • record_version: 1, exit_code: Bazel's code.
  • run_id: identifies the run that wrote the file; command_finished carries the same run_id. Concurrent runs share .dx/results.json, so check that run_id matches command_finished.run_id, or give each run its own path with --report results=<path>.
  • complete: false when results are incomplete.
  • results: one entry per test result, with the test_outcome fields, every case as classname, name, outcome and message, and log_path when the result did not pass. A replaced attempt carries final: false.

dx build, dx test and dx coverage report each failure Bazel records when it exits nonzero. --output=text prints one dx: build failure: line per failure on stderr. A repeated failure lists its target and up to twenty others in that line, then xN, the total across targets and configurations. Its rustc diagnostics follow it. Each diagnostic lists its labels, notes and helps indented below it. --output=json adds one build_failure event per failure, before command_finished. The same failure from several targets or configurations is one event with a count and up to 20 other targets. Each event has:

  • record_version: 1. New fields are only ever added.
  • target, configuration, target_kind (test, other or unknown).
  • stage: analysis, tool_acquisition, compilation, linking, test_execution, other or unknown.
  • count and targets: only when the failure repeats.
  • category: analysis_failed, loading_failed, tool_unavailable, tool_missing, compile_failed, link_failed, test_execution_failed, action_failed, target_not_built, skipped, cancelled or aborted.
  • action: mnemonic, primary_output and, when known, exit_code.
  • tool: clippy for Clippy actions and rustc for other Rust actions.
  • message: the first line of Bazel's reason, for failures with no action.
  • diagnostics: status, total and up to 50 items with severity, code, message, path, line, column and an ordered related list. Each related item has role (label, note or help), message, and when known path, line, column, replacement and applicability. Labels read from rendered output have a line but no column. Related items are not counted in total. Link errors have none.

Anything Bazel did not report reads unknown. An analysis failure has no action, so its action fields are unknown. A test target that fails to build emits a build_failure with target_kind test and no test case in the JUnit report. It also gets a test_outcome with outcome failed_to_build, no case counts, and status FAILED_TO_BUILD only when Bazel reports that summary. The run's results are incomplete. Environment variables and command lines are never reported.

Diagnostics come from rustc's JSON or rendered output in the failed action's stderr. status is parsed when diagnostics were read, none when the stderr held none, unavailable when the stderr is unreadable or named only on a remote cache, and unknown for non-Rust actions. A Rust action whose diagnostics include a linking with error is reported as linking.

The failures report: --report failures=<path> on dx build, dx test and dx coverage writes one JSON file with the failures of this run. It follows the same path rules as the other reports and may share a run with junit or lcov. Use - for stdout in text mode. The file is written on success too, with an empty failures list. A failed write is a report_failed error. It fails a run Bazel passed and never changes Bazel's own code.

The file has:

  • record_version: 1, exit_code: Bazel's code.
  • complete: false when the build events end early or cannot be read, with the reason in problem.
  • truncated: true when any stderr was cut at a limit.
  • failures_omitted: failures past the first 200.
  • failures: the build_failure fields above, each with a stderr object.

stderr has status, bytes, truncated, redacted_lines and, for captured output, text. status is:

  • structured for Rust actions whose diagnostics were parsed. Read diagnostics instead.
  • captured or truncated for other actions. Each failure keeps at most 64 KiB and the file at most 1 MiB. Later failures read omitted_limit.
  • remote when the build events still name the stderr only on a remote cache. dx asks Bazel to name local stderr files, so this is rare.
  • absent when Bazel named no stderr, unreadable when the local file is gone.

Only this run's failed actions are read. No directory is scanned and no history is kept. Lines that start a command line ((cd or exec env) are dropped and counted in redacted_lines. Environment variables and command lines are never reported. Positions come only from rustc diagnostics.

bazel run @rules_dx//:dx -- build --report failures=build-failures.json //...
bazel run @rules_dx//:dx -- test --report junit=junit.xml --report failures=failures.json
bazel run @rules_dx//:dx -- test --report results=results.json //...

In an agent session (CLAUDECODE, CODEX_THREAD_ID, CURSOR_AGENT, GEMINI_CLI or AI_AGENT set), dx build, dx test, dx coverage and dx run default to --output=json. They pass --noshow_progress, --noshow_loading_progress, --noshow_timestamps, --curses=no, --ui_event_filters=-info,-debug and --test_summary=none to Bazel before your own Bazel options, so Bazel prints no progress on stderr. Those quiet Bazel flags apply only when the output is JSON. For dx run, args after -- still go only to the app. --output, DX_OUTPUT and the output setting in dx.toml still win. In CI these variables are ignored.

If Bazel fails and its event file is missing or ends early, dx still returns Bazel's code and reports a build_events_incomplete notice. dx test and dx coverage write the reports they can, and the results report has complete: false. If Bazel succeeds and dx test or dx coverage cannot read the event file, dx exits 1 with unreadable_bep or invalid_bep.

Exit codes: 0 success. 2 usage, scope or owner errors. 1 operational failures, including no_tests and query_launch_failed. Bazel failures keep Bazel's code. Results are incomplete when a result has no test.xml, a test.xml is missing, truncated or invalid, Bazel announces a result it never reports, a target has a summary but no result, or the build events end before Bazel's last event. Another test's evidence never makes up for it. Incomplete results fail the run, even when Bazel passes. --output=text prints dx: incomplete_results: <reason> on stderr, --output=json emits an incomplete_results error, the JUnit report gets a dx.infrastructure error case, and results_complete reads false. Failing tests with every result collected keep Bazel's code 3 and read results_complete: true. Other Bazel failures read false. With exit 3 and at least one failed result, --output=json emits a tests_failed error with cause test and Bazel's status. Other Bazel failures emit bazel_failed. dx coverage does the same.

bazel run @rules_dx//:dx -- build //cli/...
bazel run @rules_dx//:dx -- test --here
bazel run @rules_dx//:dx -- test --affected
bazel run @rules_dx//:dx -- test //... -- --jobs=4
bazel run @rules_dx//:dx -- test //cli/process/... -- --test_filter=workflow_argv

--affected builds or tests the changed targets and their reverse dependencies. A Cargo.lock or crate_universe lockfile change selects the users of the changed crates. Other lock files and patches named in MODULE.bazel widen to //...; see scope defaults. dx test --affected and dx coverage --affected run only the affected test targets. With nothing affected they print no affected targets and exit 0. Usage errors exit 2. Affected targets that do not fit the host platform are skipped; see Scope Defaults.

Args after -- reach the test binary through Bazel. dx test and dx coverage accept repeatable --test_arg tokens, in equals and two-token form, with spaces and Unicode kept verbatim. --test_filter stays a Bazel option. When the selected tests are rust_test targets, optionally with their rustfmt_test and rust_clippy_test checks, dx also passes the last --test_filter value to the test binaries as a libtest filter, so only matching cases run. Other commands reject --test_arg; run those cases with bazel directly.

Tests run once by default. Only tests marked flaky in their build definition retry, up to three attempts. Diagnose one test with retries through dx test <label> -- --flaky_test_attempts=3, or through bazel test --config=retry <label>. A test that passes after retry is reported as passed_after_retry with its attempt number.

dx run

dx run [--debug|--release] <label...> [-- args...]

Builds the runnable targets in scope, then launches them in scope order. Explicit labels and patterns run sequentially. File and directory scopes must resolve to exactly one runnable target. Args after -- go to the app. The default profile is development; --release selects release. dx run works with CI=true. dx run --apply is a usage error. The build holds a build slot. The slot frees before the app starts.

Output: --output text|json. dx run has no report format. With --output=json, an app that exits nonzero emits an error with code application_failed, cause application, the app's exit code as status, and correlation run:<label>. A failed build emits an error with code bazel_failed and starts no app.

Exit codes: 0 success, 2 usage or scope errors, 1 no runnable target or a launch failure, or a file or directory scope that matches more than one runnable target (ambiguous). A failed build keeps Bazel's exit code. The app's own code is the exit code, and the first app that exits nonzero stops the later targets.

bazel run @rules_dx//:dx -- run @rules_dx//:dx -- --help
bazel run @rules_dx//:dx -- run --release @rules_dx//:dx -- --help

dx deploy

dx deploy [--apply] [--debug|--release] <label> [-- args...]

Builds one deployable target without publishing it. Pass --apply to run the deployment. Takes exactly one label. The profile is the flag if given, else the target's profile attribute, else the command default. With --apply, dx sets DX_PROFILE for the deploy tool. Args after -- go to the app, and only reach it on an --apply launch.

Output: text only.

Exit codes: 0 success, 2 usage or scope errors, 1 a build or launch failure. The target's own code is the exit code. A passing check names the --apply command that publishes.

dx coverage

dx coverage [--here] [--affected] [--min-coverage <percent>] [--report lcov=<path>] [--report failures=<path>] [--report results=<path>] [scope...] [-- bazel-options...]

Collects LCOV coverage over the scope. --min-coverage takes a whole number from 0 to 100 and fails below that percent. The gate compares one line rate (covered over executable lines, across all files) against it. Without the flag, coverage collects without enforcing. A passing gate prints coverage <percent>% (<covered>/<eligible> lines) meets minimum <n>%. A failing one prints coverage_below_minimum: coverage <percent>% (<covered>/<eligible> lines) below minimum <n>% on stderr, or emits an error event with code coverage_below_minimum with --output=json. A collected LCOV with zero executable lines also fails. C and C++ tests report line coverage through LLVM, with no setup needed.

Output: --output text|json. Reports: --report lcov=<path> writes combined LCOV. Repeat the flag for more files. Use - for stdout. A relative path anchors at the workspace root; an absolute one writes as given. Colliding destinations fail before anything runs, parents must exist, and a failed write keeps the previous file bytes. dx coverage --output=json adds one test_outcome event per test result except a plain pass, as for dx test. A failing result carries failing_cases, log_path and log_tail as in dx test; a missing or invalid test.xml there does not make coverage incomplete. command_finished counts tests as for dx test.

--report failures=<path> and --report results=<path> work here too, as described above.

Coverage sources

dx coverage also lists the production sources each cell owns. The source list comes from the instrumented files of the targets in scope, so --instrumentation_filter decides which files count. Each source gets one disposition:

  • eligible: a production source in a covered language that the LCOV reports with executable lines.
  • support: a test or testonly source, a TypeScript declaration file (.d.ts, .d.mts, .d.cts), a source the LCOV reports with no executable lines, or a C or C++ header the LCOV does not report.
  • excluded: a source the LCOV does not report, with one valid LCOV_EXCL_START / LCOV_EXCL_STOP range over every line that is not blank or a comment.
  • unsupported: a source in a language the gate does not read. These are counted, never dropped.

In text mode dx prints one counts line on stderr. With --output=json the same text is the message of a notice event with code coverage_sources and level info:

dx: coverage sources <cell>: <raw> raw, <excluded> excluded, <eligible> eligible lines; <n> eligible, <n> support, <n> excluded, <n> unsupported files

<cell> is <os>_<cpu>, such as linux_x86_64. A production source missing from the LCOV, or an LCOV path no target owns, prints dx: coverage_sources_incomplete: <message> on stderr, or emits a notice event with code coverage_sources_incomplete and level warning with --output=json. These notices do not change the exit code. --build_tests_only is refused, because it hides library targets from the source list. dx coverage also passes --build_tests_only=false after your options, so a .bazelrc entry or --config that turns it on has no effect.

Exit codes: 0 success. 2 usage, scope or owner errors. 1 operational failures, coverage below minimum, or incomplete coverage. Bazel failures keep Bazel's code. Coverage is incomplete when Bazel reports a coverage.dat artifact that is missing or invalid, when no coverage artifacts are reported, when a target's coverage source list is missing or unreadable, or when the build events are incomplete as for dx test. Incomplete coverage fails the run, blocks --min-coverage, and marks a written LCOV report incomplete. A retried test counts only its final attempt. Tests without coverage outputs are not expected to produce coverage. Empty but valid coverage.dat files are accepted and contribute no lines.

bazel run @rules_dx//:dx -- coverage //...
bazel run @rules_dx//:dx -- coverage --min-coverage 100 //...

Coverage ignores use LCOV_EXCL_LINE or LCOV_EXCL_START / LCOV_EXCL_STOP with a short reason: and issue:. Bare ignores without a reason fail the gate. reason: goes on the marker line or the line before and holds at most 120 characters. issue: must contain a digit.

The comment syntax depends on the language: // for Rust, Go, JS/TS, the JVM languages (including Kotlin scripts, .kts), .NET and C/C++; # for Python and Ruby; % for Erlang (.erl and .hrl). Markers in other sources (.bzl, .pyi, .cu/.cuh, .d.ts) are not read.

Line And Branch Metrics

This section describes //tools/coverage:coverage_bin, not dx coverage.

The coverage gate enforces exact executable-line coverage. Branch outcomes are measured separately and never change the gate. Each file with branch records reports raw, ignored, eligible and taken counts with its language. Branches on lines covered by LCOV_EXCL_LINE or a START/STOP range are counted as ignored and are left out of eligible, taken, missed and the rate. The declared branch summary is still checked against all records. Files without branch records report unavailable instead of a rate. A declared branch summary that does not match its records fails the report as incomplete.