From 016cf576db340ade4e920e8362f72e8e4b645ee1 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 10 Aug 2026 12:46:23 +0000 Subject: [PATCH 1/4] Add Rust LCOV merger with per-target coverage minimums MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace Bazel's built-in LCOV merger with a dependency-free Rust binary (//tools/coverage:lcov_merger, wired via --coverage_output_generator in .bazelrc) that merges each test's raw per-runner LCOV tracefiles into its coverage.dat and, when the target declares one, enforces a minimum line coverage percentage. Bazel only invokes the merger during 'bazel coverage', so plain 'bazel test' runs are never affected. Targets opt in through their env attribute via //tools/coverage:defs.bzl (coverage_enforced_test macro / coverage_minimum_env helper), which works with any test rule exposing the standard env attribute (go_test, rust_test, kt_jvm_test, py_test, ...). Applied so far: - //tools/go/sample:sample_test — min 90% over tools/go/ (mirrors the CI gate) - //tools/coverage:lcov_merger_test — min 90% over its own sources, enforced by the merger itself (currently at 98%) The merger mirrors the built-in CoverageOutputGenerator's contract: --coverage_dir/--output_file/--filter_sources/--source_file_manifest, full-match filter regexes (small built-in subset, no third-party crates), manifest restriction to instrumented sources, and recomputed summary counters. Unknown flags warn instead of failing so newer Bazels degrade gracefully. Below-minimum targets exit with code 33 and a per-file breakdown in the test log; the merged coverage.dat is still written and --combined_report=lcov works unchanged. rules_rust is a dev_dependency (consumers of bazel-diff as a module do not inherit it), pinned to Rust 1.90.0, edition 2021. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_016KqwUSCW2t6fmvnRFyw9vX --- .bazelrc | 8 + .github/workflows/ci.yaml | 2 +- MODULE.bazel | 18 ++ Makefile | 4 +- tools/coverage/BUILD | 38 +++ tools/coverage/README.md | 117 +++++++++ tools/coverage/defs.bzl | 88 +++++++ tools/coverage/src/args.rs | 110 +++++++++ tools/coverage/src/enforce.rs | 260 ++++++++++++++++++++ tools/coverage/src/lcov.rs | 324 +++++++++++++++++++++++++ tools/coverage/src/lib.rs | 434 ++++++++++++++++++++++++++++++++++ tools/coverage/src/main.rs | 8 + tools/coverage/src/pattern.rs | 232 ++++++++++++++++++ tools/go/sample/BUILD | 11 +- tools/readme_template.md | 30 ++- 15 files changed, 1678 insertions(+), 6 deletions(-) create mode 100644 tools/coverage/BUILD create mode 100644 tools/coverage/README.md create mode 100644 tools/coverage/defs.bzl create mode 100644 tools/coverage/src/args.rs create mode 100644 tools/coverage/src/enforce.rs create mode 100644 tools/coverage/src/lcov.rs create mode 100644 tools/coverage/src/lib.rs create mode 100644 tools/coverage/src/main.rs create mode 100644 tools/coverage/src/pattern.rs diff --git a/.bazelrc b/.bazelrc index e2044b73..d61c3c87 100644 --- a/.bazelrc +++ b/.bazelrc @@ -22,3 +22,11 @@ build:windows --host_cxxopt=/std:c++17 # Avoid cache thrashing, but allow integration tests to find "bazel" on the PATH. common --incompatible_strict_action_env common --test_env=PATH + +# Coverage runs use the Rust LCOV merger from //tools/coverage instead of +# Bazel's built-in one. Besides merging each test's raw tracefiles into its +# coverage.dat, it enforces the per-target line-coverage minimums declared +# via //tools/coverage:defs.bzl (targets opt in through their `env` attr). +# Scoped to the `coverage` command, and Bazel only invokes an LCOV merger in +# coverage mode anyway, so plain `bazel test` runs are never affected. +coverage --coverage_output_generator=//tools/coverage:lcov_merger diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 2204ce38..6b52356a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -62,7 +62,7 @@ jobs: if [ -n "${BAZEL_OUTPUT_USER_ROOT:-}" ]; then BAZEL_STARTUP=(--output_user_root="${BAZEL_OUTPUT_USER_ROOT}") fi - ~/go/bin/bazelisk "${BAZEL_STARTUP[@]}" coverage --combined_report=lcov //cli/... //tools:coverage_check_test //tools/go/... --enable_bzlmod=true --enable_workspace=false + ~/go/bin/bazelisk "${BAZEL_STARTUP[@]}" coverage --combined_report=lcov //cli/... //tools:coverage_check_test //tools/coverage/... //tools/go/... --enable_bzlmod=true --enable_workspace=false - name: Upload coverage report uses: actions/upload-artifact@v4 if: always() diff --git a/MODULE.bazel b/MODULE.bazel index 4b883c6f..35311d74 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -44,6 +44,24 @@ bazel_dep(name = "gazelle", version = "0.45.0", dev_dependency = True) go_sdk = use_extension("@rules_go//go:extensions.bzl", "go_sdk", dev_dependency = True) go_sdk.download(version = "1.23.1") +# Rust support is internal to building/testing bazel-diff: it builds the LCOV +# merger (//tools/coverage) that `bazel coverage` runs use to merge per-test +# tracefiles and enforce per-target coverage minimums. Marked dev_dependency +# so consumers of bazel-diff as a module don't inherit rules_rust via MVS. +bazel_dep(name = "rules_rust", version = "0.73.0", dev_dependency = True) + +rust = use_extension("@rules_rust//rust:extensions.bzl", "rust", dev_dependency = True) +rust.toolchain( + edition = "2021", + versions = ["1.90.0"], +) +use_repo(rust, "rust_toolchains") + +register_toolchains( + "@rust_toolchains//:all", + dev_dependency = True, +) + maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven") maven.install( name = "bazel_diff_maven", diff --git a/Makefile b/Makefile index 03f3be4b..7199a40d 100644 --- a/Makefile +++ b/Makefile @@ -24,7 +24,7 @@ generate-readme: .PHONY: coverage coverage: - bazel coverage --combined_report=lcov //cli/... //tools:coverage_check_test //tools/go/... + bazel coverage --combined_report=lcov //cli/... //tools:coverage_check_test //tools/coverage/... //tools/go/... bazel run //tools:coverage-check -- bazel-out/_coverage/_coverage_report.dat bazel run //tools:coverage-check -- --include tools/go/ --threshold 90 bazel-out/_coverage/_coverage_report.dat @@ -39,6 +39,6 @@ coverage-test: .PHONY: coverage-html coverage-html: - bazel coverage --combined_report=lcov //cli/... //tools:coverage_check_test //tools/go/... + bazel coverage --combined_report=lcov //cli/... //tools:coverage_check_test //tools/coverage/... //tools/go/... bazel run //tools:coverage-check -- bazel-out/_coverage/_coverage_report.dat --html coverage-html @echo "Open coverage-html/index.html in a browser to inspect." diff --git a/tools/coverage/BUILD b/tools/coverage/BUILD new file mode 100644 index 00000000..46f332d1 --- /dev/null +++ b/tools/coverage/BUILD @@ -0,0 +1,38 @@ +load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_library", "rust_test") +load("//tools/coverage:defs.bzl", "coverage_enforced_test") + +# The LCOV merger Bazel invokes inside every test action during +# `bazel coverage` runs (wired up via --coverage_output_generator in +# .bazelrc). Besides merging, it enforces the per-target line-coverage +# minimums declared through //tools/coverage:defs.bzl. See README.md. +rust_library( + name = "lcov_merger_lib", + srcs = [ + "src/args.rs", + "src/enforce.rs", + "src/lcov.rs", + "src/lib.rs", + "src/pattern.rs", + ], + crate_name = "lcov_merger", + crate_root = "src/lib.rs", + edition = "2021", +) + +rust_binary( + name = "lcov_merger", + srcs = ["src/main.rs"], + edition = "2021", + visibility = ["//visibility:public"], + deps = [":lcov_merger_lib"], +) + +# The merger's own tests carry a coverage minimum, enforced — pleasingly — +# by the merger itself when this package runs under `bazel coverage`. +coverage_enforced_test( + coverage_include = ["tools/coverage/src/"], + crate = ":lcov_merger_lib", + min_line_coverage = 90, + name = "lcov_merger_test", + rule = rust_test, +) diff --git a/tools/coverage/README.md b/tools/coverage/README.md new file mode 100644 index 00000000..06173ba9 --- /dev/null +++ b/tools/coverage/README.md @@ -0,0 +1,117 @@ +# Per-target coverage minimums (`//tools/coverage`) + +This package contains a Rust LCOV merger that replaces Bazel's built-in +`@bazel_tools//tools/test:lcov_merger` for this repository, plus Starlark +helpers to declare a **per-target line-coverage minimum** on any test target +that produces coverage. + +## How it plugs into Bazel + +`.bazelrc` contains: + +``` +coverage --coverage_output_generator=//tools/coverage:lcov_merger +``` + +In coverage mode Bazel wraps every test in `collect_coverage.sh`, which +finishes by invoking the configured LCOV merger to combine the raw +per-runner tracefiles (Jacoco emits LCOV for JVM targets, rules_go converts +Go cover profiles to LCOV, rules_rust's llvm-cov toolchain exports LCOV) +into the `coverage.dat` that Bazel publishes for the test. Two properties +fall out of that placement: + +1. **Coverage runs only.** Bazel never invokes an LCOV merger for plain + `bazel test`, so enforcement cannot slow down or fail ordinary test runs. +2. **Per-target `env` is visible.** The merger runs inside the test action, + so it sees the target's `env` attribute — that is how a target declares + its minimum, without any global configuration or custom test rules. + +## Declaring a minimum + +Wrap any test rule that has the standard `env` attribute (`go_test`, +`rust_test`, `kt_jvm_test`, `java_test`, `py_test`, ...): + +```starlark +load("//tools/coverage:defs.bzl", "coverage_enforced_test") +load("@rules_go//go:def.bzl", "go_test") + +coverage_enforced_test( + rule = go_test, + name = "sample_test", + srcs = ["sample_test.go"], + embed = [":sample"], + min_line_coverage = 90, + coverage_include = ["tools/go/"], +) +``` + +or splice the env vars into an existing target with `coverage_minimum_env`: + +```starlark +load("//tools/coverage:defs.bzl", "coverage_minimum_env") + +kt_jvm_test( + name = "DurationConverterTest", + ... + env = coverage_minimum_env( + 85, + coverage_include = ["cli/src/main/kotlin/com/bazel_diff/cli/converter/"], + ), +) +``` + +- `min_line_coverage` — minimum overall line coverage (percent, 0–100) of + the target's merged report. `bazel coverage` fails the target below it, + with a per-file breakdown in the test log; `bazel test` is unaffected. +- `coverage_include` — optional path prefixes scoping which source files + count. Essential for JVM targets: Jacoco instruments the whole library + on the test's classpath, so an unscoped percentage would dilute a focused + unit test's coverage with every other file in the library. Scope each + target to the code it is responsible for covering. +- `coverage_exclude` — optional path prefixes to drop (e.g. generated code). + +Under the hood these become `LCOV_MERGER_MIN_LINE_COVERAGE`, +`LCOV_MERGER_COVERAGE_INCLUDE` and `LCOV_MERGER_COVERAGE_EXCLUDE` in the +target's `env`; only the merger reads them. + +## What a failure looks like + +``` +lcov_merger: line-coverage minimum declared for this target (min 90.00%): + + COV% LINES (hit/total) FILE + 66.67% 8 / 12 tools/go/sample/sample.go + +Target line coverage: 66.67% (8 / 12 lines) +Required minimum: 90.00% +FAIL: line coverage 66.67% is below the required minimum 90.00%. +``` + +The test action exits with code 33 (distinct from ordinary failures), the +merged `coverage.dat` is still written, and the combined report +(`--combined_report=lcov`) is built as usual. + +## Relationship to the repo-wide gate + +`tools/coverage_check.py` still enforces the repo-wide ≥90% floor over the +*combined* report in CI. The per-target minimums here are complementary: +they run per test target, inside the coverage run itself, and catch a +regression in the exact target that introduced it. + +## The merger itself + +`src/` is a dependency-free Rust crate: + +- `args.rs` — the flag contract with `collect_coverage.sh` + (`--coverage_dir`, `--output_file`, `--filter_sources`, + `--source_file_manifest`; unknown flags warn instead of failing). +- `lcov.rs` — LCOV parse/merge/emit. Line and function hits are summed + across tracefiles; `-` branch entries survive only while no run evaluated + the branch; `LF`/`LH`/`FNF`/`FNH`/`BRF`/`BRH` are recomputed. +- `pattern.rs` — the small full-match regex subset `--filter_sources` + patterns need (literals, `.`, classes, `*`/`+`/`?`, escapes). + Unsupported patterns are warned about and skipped. +- `enforce.rs` — reads the env vars above and renders the report. + +`bazel test //tools/coverage:lcov_merger_test` runs its unit tests; under +`bazel coverage` the crate enforces a 90% minimum on itself, via itself. diff --git a/tools/coverage/defs.bzl b/tools/coverage/defs.bzl new file mode 100644 index 00000000..e50f9c6e --- /dev/null +++ b/tools/coverage/defs.bzl @@ -0,0 +1,88 @@ +"""Per-target line-coverage enforcement for any Bazel test target. + +How it works: `.bazelrc` points `--coverage_output_generator` at +`//tools/coverage:lcov_merger`, a Rust drop-in replacement for Bazel's +built-in LCOV merger. Bazel runs that merger inside every test action — +but only under `bazel coverage`, never `bazel test` — to merge the raw +per-runner LCOV tracefiles into the test's `coverage.dat`. Because the +merger runs inside the test action, it sees the target's `env` attribute, +which is the channel these helpers use to declare a minimum: + + load("//tools/coverage:defs.bzl", "coverage_enforced_test") + load("@rules_go//go:def.bzl", "go_test") + + coverage_enforced_test( + rule = go_test, + name = "sample_test", + srcs = ["sample_test.go"], + embed = [":sample"], + min_line_coverage = 90, + coverage_include = ["tools/go/"], + ) + +Any rule with the standard `env` attribute works (`go_test`, `rust_test`, +`kt_jvm_test`, `java_test`, `py_test`, ...). A target whose merged report +falls below its minimum fails the coverage run with the merger's per-file +breakdown in the test log; plain `bazel test` runs are untouched. +""" + +MIN_LINE_COVERAGE_ENV = "LCOV_MERGER_MIN_LINE_COVERAGE" +COVERAGE_INCLUDE_ENV = "LCOV_MERGER_COVERAGE_INCLUDE" +COVERAGE_EXCLUDE_ENV = "LCOV_MERGER_COVERAGE_EXCLUDE" + +def coverage_minimum_env(min_line_coverage, coverage_include = [], coverage_exclude = []): + """Returns the `env` entries declaring a line-coverage minimum. + + Use this directly when you cannot (or prefer not to) route a target + through `coverage_enforced_test`, e.g. to splice into an existing + `env` dict: + + kt_jvm_test( + name = "FooTest", + ... + env = coverage_minimum_env(85, ["cli/src/main/kotlin/foo/"]), + ) + + Args: + min_line_coverage: minimum overall line-coverage percentage (0-100) + for the target's merged LCOV report during `bazel coverage`. + coverage_include: optional path prefixes; when non-empty, only source + files starting with one of them count toward the minimum. Use this + to scope the check to the code the target is responsible for. + coverage_exclude: optional path prefixes removed from the computation + (applied after includes), e.g. generated code. + + Returns: + A dict suitable for (merging into) a test rule's `env` attribute. + """ + if min_line_coverage < 0 or min_line_coverage > 100: + fail("min_line_coverage must be within 0-100, got %s" % min_line_coverage) + env = {MIN_LINE_COVERAGE_ENV: str(min_line_coverage)} + if coverage_include: + env[COVERAGE_INCLUDE_ENV] = ",".join(coverage_include) + if coverage_exclude: + env[COVERAGE_EXCLUDE_ENV] = ",".join(coverage_exclude) + return env + +def coverage_enforced_test( + rule, + name, + min_line_coverage, + coverage_include = [], + coverage_exclude = [], + **kwargs): + """Declares `rule(name = name, ...)` with a coverage minimum attached. + + Args: + rule: any test rule with the standard `env` attribute + (`go_test`, `rust_test`, `kt_jvm_test`, `py_test`, ...). + name: forwarded to the rule. + min_line_coverage: see `coverage_minimum_env`. + coverage_include: see `coverage_minimum_env`. + coverage_exclude: see `coverage_minimum_env`. + **kwargs: every other attribute, forwarded untouched (an existing + `env` is preserved and augmented). + """ + env = dict(kwargs.pop("env", {})) + env.update(coverage_minimum_env(min_line_coverage, coverage_include, coverage_exclude)) + rule(name = name, env = env, **kwargs) diff --git a/tools/coverage/src/args.rs b/tools/coverage/src/args.rs new file mode 100644 index 00000000..4f1d7835 --- /dev/null +++ b/tools/coverage/src/args.rs @@ -0,0 +1,110 @@ +//! Command-line contract with Bazel's `collect_coverage.sh`. +//! +//! In coverage mode Bazel wraps every test in `collect_coverage.sh`, which +//! finishes by invoking the configured LCOV merger roughly as: +//! +//! ```text +//! $LCOV_MERGER --coverage_dir=$COVERAGE_DIR \ +//! --output_file=$COVERAGE_OUTPUT_FILE \ +//! --filter_sources=/usr/bin/.+ ... \ +//! --source_file_manifest=$COVERAGE_MANIFEST +//! ``` +//! +//! Unknown flags are collected as warnings instead of errors so that a newer +//! Bazel adding a flag degrades gracefully rather than failing every +//! coverage run. + +#[derive(Debug, Default, PartialEq)] +pub struct Args { + pub coverage_dir: Option, + pub output_file: Option, + pub filter_sources: Vec, + pub source_file_manifest: Option, + /// Unrecognised flags, reported as warnings. + pub unknown: Vec, +} + +/// Parse argv (without the program name). Both `--flag=value` and +/// `--flag value` spellings are accepted. +pub fn parse(argv: &[String]) -> Result { + let mut args = Args::default(); + let mut iter = argv.iter().peekable(); + while let Some(arg) = iter.next() { + let Some(flag) = arg.strip_prefix("--") else { + return Err(format!("unexpected positional argument '{arg}'")); + }; + let (name, value) = match flag.split_once('=') { + Some((n, v)) => (n, v.to_string()), + None => match iter.next() { + Some(v) => (flag, v.clone()), + None => return Err(format!("flag '--{flag}' is missing a value")), + }, + }; + match name { + "coverage_dir" => args.coverage_dir = Some(value), + "output_file" => args.output_file = Some(value), + "filter_sources" => args.filter_sources.push(value), + "source_file_manifest" => args.source_file_manifest = Some(value), + _ => args.unknown.push(format!("--{name}={value}")), + } + } + if args.coverage_dir.is_none() { + return Err("--coverage_dir is required".to_string()); + } + if args.output_file.is_none() { + return Err("--output_file is required".to_string()); + } + Ok(args) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn to_vec(argv: &[&str]) -> Vec { + argv.iter().map(|s| s.to_string()).collect() + } + + #[test] + fn parses_the_collect_coverage_invocation() { + let args = parse(&to_vec(&[ + "--coverage_dir=/tmp/cov", + "--output_file=/tmp/out.dat", + "--filter_sources=/usr/bin/.+", + "--filter_sources=/usr/lib/.+", + "--source_file_manifest=/tmp/manifest.txt", + ])) + .unwrap(); + assert_eq!(args.coverage_dir.as_deref(), Some("/tmp/cov")); + assert_eq!(args.output_file.as_deref(), Some("/tmp/out.dat")); + assert_eq!(args.filter_sources, vec!["/usr/bin/.+", "/usr/lib/.+"]); + assert_eq!(args.source_file_manifest.as_deref(), Some("/tmp/manifest.txt")); + assert!(args.unknown.is_empty()); + } + + #[test] + fn accepts_space_separated_values() { + let args = parse(&to_vec(&["--coverage_dir", "d", "--output_file", "o"])).unwrap(); + assert_eq!(args.coverage_dir.as_deref(), Some("d")); + assert_eq!(args.output_file.as_deref(), Some("o")); + } + + #[test] + fn unknown_flags_are_warnings_not_errors() { + let args = parse(&to_vec(&[ + "--coverage_dir=d", + "--output_file=o", + "--legacy_branch_coverage=true", + ])) + .unwrap(); + assert_eq!(args.unknown, vec!["--legacy_branch_coverage=true"]); + } + + #[test] + fn missing_required_flags_and_positionals_are_errors() { + assert!(parse(&to_vec(&["--output_file=o"])).is_err()); + assert!(parse(&to_vec(&["--coverage_dir=d"])).is_err()); + assert!(parse(&to_vec(&["stray"])).is_err()); + assert!(parse(&to_vec(&["--coverage_dir"])).is_err()); + } +} diff --git a/tools/coverage/src/enforce.rs b/tools/coverage/src/enforce.rs new file mode 100644 index 00000000..44d131ee --- /dev/null +++ b/tools/coverage/src/enforce.rs @@ -0,0 +1,260 @@ +//! Per-target line-coverage enforcement. +//! +//! The merger runs *inside* the test action (Bazel only spawns it for +//! `bazel coverage`, never for `bazel test`), so it inherits the test +//! target's `env` attribute. That is the channel targets use to opt in: +//! +//! - `LCOV_MERGER_MIN_LINE_COVERAGE`: minimum overall line coverage +//! percentage (0-100) for the target's merged report. Presence of this +//! variable is what activates enforcement. +//! - `LCOV_MERGER_COVERAGE_INCLUDE`: optional comma-separated path prefixes; +//! when set, only `SF:` paths starting with one of them count. +//! - `LCOV_MERGER_COVERAGE_EXCLUDE`: optional comma-separated path prefixes +//! removed from the computation (applied after includes). +//! +//! `//tools/coverage:defs.bzl` provides a macro that injects these for any +//! test rule; nothing here is rule- or language-specific. + +use crate::lcov::Report; +use std::collections::BTreeMap; +use std::fmt::Write as _; + +pub const MIN_LINE_COVERAGE_ENV: &str = "LCOV_MERGER_MIN_LINE_COVERAGE"; +pub const COVERAGE_INCLUDE_ENV: &str = "LCOV_MERGER_COVERAGE_INCLUDE"; +pub const COVERAGE_EXCLUDE_ENV: &str = "LCOV_MERGER_COVERAGE_EXCLUDE"; + +/// Enforcement configuration, parsed from the test action's environment. +#[derive(Debug, Clone, PartialEq)] +pub struct Config { + pub min_line_coverage: f64, + pub include: Vec, + pub exclude: Vec, +} + +/// Read enforcement config from an environment map. Returns: +/// - `Ok(None)` when enforcement is not requested (no min set), +/// - `Ok(Some(config))` when it is, +/// - `Err` when the variables are present but unusable — that is a bug in +/// the target's BUILD file and must fail the coverage action loudly. +pub fn config_from_env(env: &BTreeMap) -> Result, String> { + let Some(raw) = env.get(MIN_LINE_COVERAGE_ENV) else { + return Ok(None); + }; + let min: f64 = raw.trim().parse().map_err(|_| { + format!("{MIN_LINE_COVERAGE_ENV}='{raw}' is not a number (expected 0-100)") + })?; + if !(0.0..=100.0).contains(&min) { + return Err(format!( + "{MIN_LINE_COVERAGE_ENV}={min} is outside the valid range 0-100" + )); + } + let split = |key: &str| -> Vec { + env.get(key) + .map(|v| { + v.split(',') + .map(str::trim) + .filter(|p| !p.is_empty()) + .map(str::to_string) + .collect() + }) + .unwrap_or_default() + }; + Ok(Some(Config { + min_line_coverage: min, + include: split(COVERAGE_INCLUDE_ENV), + exclude: split(COVERAGE_EXCLUDE_ENV), + })) +} + +/// The outcome of an enforcement check. `report` is always populated so the +/// per-file breakdown lands in the test log for passing runs too. +#[derive(Debug, PartialEq)] +pub struct Outcome { + pub passed: bool, + pub report: String, +} + +/// Check the merged report against `config`. +pub fn check(report: &Report, config: &Config) -> Outcome { + let in_scope = |path: &str| -> bool { + (config.include.is_empty() || config.include.iter().any(|p| path.starts_with(p))) + && !config.exclude.iter().any(|p| path.starts_with(p)) + }; + + let mut rows: Vec<(f64, u64, u64, &str)> = Vec::new(); + let mut total_found = 0u64; + let mut total_hit = 0u64; + for (path, cov) in report { + if !in_scope(path) { + continue; + } + let found = cov.lines_found(); + // Skip files with no instrumented lines: LCOV has nothing measurable + // for them, and counting them as 0% would punish files the language's + // instrumentation simply did not visit. + if found == 0 { + continue; + } + let hit = cov.lines_hit(); + total_found += found; + total_hit += hit; + rows.push((hit as f64 / found as f64 * 100.0, hit, found, path)); + } + + let mut text = String::new(); + let _ = writeln!(text, "{:>8} {:>17} FILE", "COV%", "LINES (hit/total)"); + rows.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap().then(a.3.cmp(&b.3))); + for (pct, hit, found, path) in &rows { + let _ = writeln!(text, "{pct:>7.2}% {hit:>7} / {found:<7} {path}"); + } + + if total_found == 0 { + let _ = writeln!( + text, + "\nNo instrumented lines matched the coverage scope (include={:?}, exclude={:?}).", + config.include, config.exclude + ); + let _ = writeln!( + text, + "A minimum of {:.2}% was requested, so this fails: either the scope is wrong or \ + coverage instrumentation did not run for this target.", + config.min_line_coverage + ); + return Outcome { + passed: false, + report: text, + }; + } + + let overall = total_hit as f64 / total_found as f64 * 100.0; + let _ = writeln!( + text, + "\nTarget line coverage: {overall:.2}% ({total_hit} / {total_found} lines)" + ); + let _ = writeln!( + text, + "Required minimum: {:.2}%", + config.min_line_coverage + ); + // Epsilon so a value that only misses the threshold by floating-point + // noise (e.g. 89.999999999) still passes a 90 minimum. + let passed = overall + 1e-9 >= config.min_line_coverage; + let _ = writeln!( + text, + "{}", + if passed { + "PASS: coverage minimum satisfied.".to_string() + } else { + format!( + "FAIL: line coverage {overall:.2}% is below the required minimum {:.2}%.", + config.min_line_coverage + ) + } + ); + Outcome { + passed, + report: text, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::lcov::parse; + + fn env(pairs: &[(&str, &str)]) -> BTreeMap { + pairs + .iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect() + } + + #[test] + fn no_env_means_no_enforcement() { + assert_eq!(config_from_env(&BTreeMap::new()), Ok(None)); + // Include/exclude alone do not activate enforcement. + assert_eq!( + config_from_env(&env(&[(COVERAGE_INCLUDE_ENV, "src/")])), + Ok(None) + ); + } + + #[test] + fn parses_full_config() { + let cfg = config_from_env(&env(&[ + (MIN_LINE_COVERAGE_ENV, "87.5"), + (COVERAGE_INCLUDE_ENV, "src/, lib/ ,"), + (COVERAGE_EXCLUDE_ENV, "src/generated/"), + ])) + .unwrap() + .unwrap(); + assert_eq!(cfg.min_line_coverage, 87.5); + assert_eq!(cfg.include, vec!["src/", "lib/"]); + assert_eq!(cfg.exclude, vec!["src/generated/"]); + } + + #[test] + fn rejects_malformed_minimums() { + assert!(config_from_env(&env(&[(MIN_LINE_COVERAGE_ENV, "ninety")])).is_err()); + assert!(config_from_env(&env(&[(MIN_LINE_COVERAGE_ENV, "101")])).is_err()); + assert!(config_from_env(&env(&[(MIN_LINE_COVERAGE_ENV, "-1")])).is_err()); + } + + fn sample_report() -> Report { + let (report, _) = parse(concat!( + "SF:src/a.rs\nDA:1,1\nDA:2,1\nDA:3,0\nDA:4,1\nend_of_record\n", + "SF:src/b.rs\nDA:1,0\nDA:2,0\nend_of_record\n", + "SF:tests/t.rs\nDA:1,1\nend_of_record\n", + "SF:src/empty.rs\nend_of_record\n", + )); + report + } + + fn cfg(min: f64, include: &[&str], exclude: &[&str]) -> Config { + Config { + min_line_coverage: min, + include: include.iter().map(|s| s.to_string()).collect(), + exclude: exclude.iter().map(|s| s.to_string()).collect(), + } + } + + #[test] + fn passes_at_or_above_the_minimum() { + // src/ scope: a.rs 3/4 + b.rs 0/2 = 3/6 lines = 50%. + let outcome = check(&sample_report(), &cfg(49.5, &["src/"], &[])); + assert!(outcome.passed, "{}", outcome.report); + assert!(outcome.report.contains("Target line coverage: 50.00%")); + assert!(outcome.report.contains("PASS")); + // Threshold exactly at the measured value passes (epsilon). + assert!(check(&sample_report(), &cfg(50.0, &["src/"], &[])).passed); + } + + #[test] + fn fails_below_the_minimum_with_per_file_breakdown() { + let outcome = check(&sample_report(), &cfg(90.0, &["src/"], &[])); + assert!(!outcome.passed); + assert!(outcome.report.contains("FAIL: line coverage 50.00%")); + // Worst file sorts first; the zero-line file is skipped entirely. + assert!(outcome.report.contains("src/b.rs")); + assert!(!outcome.report.contains("src/empty.rs")); + assert!(!outcome.report.contains("tests/t.rs")); + } + + #[test] + fn exclude_prefixes_narrow_the_scope() { + // Excluding the all-miss file lifts coverage to 3/4 = 75%. + let outcome = check(&sample_report(), &cfg(75.0, &["src/"], &["src/b.rs"])); + assert!(outcome.passed, "{}", outcome.report); + // No include: everything counts (4/7 = 57.14%). + let outcome = check(&sample_report(), &cfg(57.0, &[], &[])); + assert!(outcome.passed, "{}", outcome.report); + assert!(outcome.report.contains("tests/t.rs")); + } + + #[test] + fn empty_scope_fails_loudly() { + let outcome = check(&sample_report(), &cfg(50.0, &["nonexistent/"], &[])); + assert!(!outcome.passed); + assert!(outcome.report.contains("No instrumented lines matched")); + } +} diff --git a/tools/coverage/src/lcov.rs b/tools/coverage/src/lcov.rs new file mode 100644 index 00000000..0412e1c7 --- /dev/null +++ b/tools/coverage/src/lcov.rs @@ -0,0 +1,324 @@ +//! LCOV tracefile parsing, merging, and emission. +//! +//! Bazel's per-test coverage post-processor receives a directory of raw +//! per-runner LCOV tracefiles (Jacoco for JVM targets, rules_go's native LCOV +//! conversion for Go, llvm-cov's LCOV export for Rust) and must merge them +//! into the single tracefile Bazel publishes as the test's `coverage.dat`. +//! +//! Merging is keyed by `SF:` path. Line (`DA`) and function (`FNDA`) hit +//! counts are summed; branch (`BRDA`) "taken" counts are summed except that +//! `-` (branch never evaluated) only survives when no tracefile evaluated the +//! branch. `LF`/`LH`/`FNF`/`FNH`/`BRF`/`BRH` totals are recomputed from the +//! merged data rather than trusted from the inputs. + +use std::collections::BTreeMap; +use std::fmt::Write as _; + +/// Branch execution state for one `BRDA` record: `None` means the branch was +/// never evaluated (`-` in LCOV), `Some(n)` means it was taken `n` times. +pub type BranchTaken = Option; + +/// Coverage data for a single source file (one `SF:` record). +#[derive(Debug, Default, Clone, PartialEq, Eq)] +pub struct FileCoverage { + /// Function name -> starting line (`FN`). First definition wins. + pub function_lines: BTreeMap, + /// Function name -> execution count (`FNDA`). + pub function_hits: BTreeMap, + /// Line number -> execution count (`DA`). + pub line_hits: BTreeMap, + /// (line, block id, branch id) -> taken count (`BRDA`). + pub branches: BTreeMap<(u64, String, String), BranchTaken>, +} + +impl FileCoverage { + pub fn lines_found(&self) -> u64 { + self.line_hits.len() as u64 + } + + pub fn lines_hit(&self) -> u64 { + self.line_hits.values().filter(|&&hits| hits > 0).count() as u64 + } + + /// Fold `other` into `self`, summing hit counts. + pub fn merge(&mut self, other: &FileCoverage) { + for (name, line) in &other.function_lines { + self.function_lines + .entry(name.clone()) + .or_insert(*line); + } + for (name, hits) in &other.function_hits { + *self.function_hits.entry(name.clone()).or_insert(0) += hits; + } + for (line, hits) in &other.line_hits { + *self.line_hits.entry(*line).or_insert(0) += hits; + } + for (key, taken) in &other.branches { + let entry = self.branches.entry(key.clone()).or_insert(None); + *entry = match (*entry, *taken) { + (None, None) => None, + (a, b) => Some(a.unwrap_or(0) + b.unwrap_or(0)), + }; + } + } +} + +/// A full report: source file path -> coverage, ordered by path for +/// deterministic output. +pub type Report = BTreeMap; + +/// Parse one LCOV tracefile. Returns the parsed report plus human-readable +/// warnings for lines that could not be understood (the record is skipped, +/// never fatal: one malformed runner output must not fail the whole merge). +pub fn parse(text: &str) -> (Report, Vec) { + let mut report = Report::new(); + let mut warnings = Vec::new(); + let mut current: Option<(String, FileCoverage)> = None; + + for (idx, raw) in text.lines().enumerate() { + let line = raw.trim_end_matches('\r'); + if line.is_empty() { + continue; + } + if let Some(path) = line.strip_prefix("SF:") { + if let Some((prev_path, prev)) = current.take() { + // Tolerate a missing end_of_record before the next SF. + merge_into(&mut report, prev_path, prev); + } + current = Some((path.to_string(), FileCoverage::default())); + continue; + } + if line == "end_of_record" { + if let Some((path, cov)) = current.take() { + merge_into(&mut report, path, cov); + } + continue; + } + let Some((_, cov)) = current.as_mut() else { + // TN: records (and stray junk) outside an SF block are ignored. + continue; + }; + if let Some(rest) = line.strip_prefix("DA:") { + match parse_da(rest) { + Some((line_no, hits)) => { + *cov.line_hits.entry(line_no).or_insert(0) += hits; + } + None => warnings.push(format!("line {}: unparseable DA record '{line}'", idx + 1)), + } + } else if let Some(rest) = line.strip_prefix("FN:") { + match rest.split_once(',') { + // Function names may themselves contain commas (e.g. C++ + // templates), so only the first comma delimits. + Some((line_no, name)) => match line_no.parse::() { + Ok(n) => { + cov.function_lines.entry(name.to_string()).or_insert(n); + } + Err(_) => warnings + .push(format!("line {}: unparseable FN record '{line}'", idx + 1)), + }, + None => warnings.push(format!("line {}: unparseable FN record '{line}'", idx + 1)), + } + } else if let Some(rest) = line.strip_prefix("FNDA:") { + match rest.split_once(',') { + Some((hits, name)) => match hits.parse::() { + Ok(h) => *cov.function_hits.entry(name.to_string()).or_insert(0) += h, + Err(_) => warnings + .push(format!("line {}: unparseable FNDA record '{line}'", idx + 1)), + }, + None => { + warnings.push(format!("line {}: unparseable FNDA record '{line}'", idx + 1)) + } + } + } else if let Some(rest) = line.strip_prefix("BRDA:") { + match parse_brda(rest) { + Some((key, taken)) => { + let entry = cov.branches.entry(key).or_insert(None); + *entry = match (*entry, taken) { + (None, None) => None, + (a, b) => Some(a.unwrap_or(0) + b.unwrap_or(0)), + }; + } + None => { + warnings.push(format!("line {}: unparseable BRDA record '{line}'", idx + 1)) + } + } + } + // LF/LH/FNF/FNH/BRF/BRH are recomputed on output; TN and anything + // else (e.g. lcov 2.x extensions) is intentionally ignored. + } + if let Some((path, cov)) = current.take() { + merge_into(&mut report, path, cov); + } + (report, warnings) +} + +fn merge_into(report: &mut Report, path: String, cov: FileCoverage) { + report.entry(path).or_default().merge(&cov); +} + +/// `DA:,[,]` +fn parse_da(rest: &str) -> Option<(u64, u64)> { + let mut parts = rest.splitn(3, ','); + let line_no = parts.next()?.parse().ok()?; + let hits = parts.next()?.parse().ok()?; + Some((line_no, hits)) +} + +/// `BRDA:,,,` where taken is `-` or a count. +fn parse_brda(rest: &str) -> Option<((u64, String, String), BranchTaken)> { + let mut parts = rest.splitn(4, ','); + let line_no = parts.next()?.parse().ok()?; + let block = parts.next()?.to_string(); + let branch = parts.next()?.to_string(); + let taken = match parts.next()? { + "-" => None, + n => Some(n.parse().ok()?), + }; + Some(((line_no, block, branch), taken)) +} + +/// Merge `incoming` into `merged`. +pub fn merge_reports(merged: &mut Report, incoming: Report) { + for (path, cov) in incoming { + merge_into(merged, path, cov); + } +} + +/// Serialise a report back to LCOV text, mirroring the record order of +/// Bazel's built-in CoverageOutputGenerator (SF, FN, FNDA, FNF, FNH, BRDA, +/// BRF, BRH, DA, LH, LF) so downstream consumers see no format change. +pub fn emit(report: &Report) -> String { + let mut out = String::new(); + for (path, cov) in report { + let _ = writeln!(out, "SF:{path}"); + let mut functions: Vec<(&String, &u64)> = cov.function_lines.iter().collect(); + functions.sort_by_key(|(name, line)| (**line, (*name).clone())); + for (name, line) in &functions { + let _ = writeln!(out, "FN:{line},{name}"); + } + for (name, _) in &functions { + let hits = cov.function_hits.get(*name).copied().unwrap_or(0); + let _ = writeln!(out, "FNDA:{hits},{name}"); + } + if !cov.function_lines.is_empty() { + let _ = writeln!(out, "FNF:{}", cov.function_lines.len()); + let hit = cov + .function_lines + .keys() + .filter(|name| cov.function_hits.get(*name).copied().unwrap_or(0) > 0) + .count(); + let _ = writeln!(out, "FNH:{hit}"); + } + if !cov.branches.is_empty() { + for ((line, block, branch), taken) in &cov.branches { + let taken = match taken { + None => "-".to_string(), + Some(n) => n.to_string(), + }; + let _ = writeln!(out, "BRDA:{line},{block},{branch},{taken}"); + } + let _ = writeln!(out, "BRF:{}", cov.branches.len()); + let hit = cov.branches.values().filter(|t| t.unwrap_or(0) > 0).count(); + let _ = writeln!(out, "BRH:{hit}"); + } + for (line, hits) in &cov.line_hits { + let _ = writeln!(out, "DA:{line},{hits}"); + } + let _ = writeln!(out, "LH:{}", cov.lines_hit()); + let _ = writeln!(out, "LF:{}", cov.lines_found()); + let _ = writeln!(out, "end_of_record"); + } + out +} + +#[cfg(test)] +mod tests { + use super::*; + + const SIMPLE: &str = "TN:\nSF:pkg/a.go\nFN:3,Foo\nFNDA:2,Foo\nDA:3,2\nDA:4,0\nBRDA:3,0,0,1\nBRDA:3,0,1,-\nLH:1\nLF:2\nend_of_record\n"; + + #[test] + fn parses_a_simple_tracefile() { + let (report, warnings) = parse(SIMPLE); + assert!(warnings.is_empty(), "{warnings:?}"); + let cov = &report["pkg/a.go"]; + assert_eq!(cov.line_hits[&3], 2); + assert_eq!(cov.line_hits[&4], 0); + assert_eq!(cov.function_lines["Foo"], 3); + assert_eq!(cov.function_hits["Foo"], 2); + assert_eq!(cov.branches[&(3, "0".into(), "0".into())], Some(1)); + assert_eq!(cov.branches[&(3, "0".into(), "1".into())], None); + assert_eq!(cov.lines_found(), 2); + assert_eq!(cov.lines_hit(), 1); + } + + #[test] + fn merges_hit_counts_across_tracefiles() { + let (a, _) = parse("SF:x.rs\nDA:1,1\nDA:2,0\nFNDA:1,f\nFN:1,f\nBRDA:1,0,0,-\nend_of_record\n"); + let (b, _) = parse("SF:x.rs\nDA:1,3\nDA:5,1\nFNDA:4,f\nFN:1,f\nBRDA:1,0,0,2\nend_of_record\n"); + let mut merged = Report::new(); + merge_reports(&mut merged, a); + merge_reports(&mut merged, b); + let cov = &merged["x.rs"]; + assert_eq!(cov.line_hits[&1], 4); + assert_eq!(cov.line_hits[&2], 0); + assert_eq!(cov.line_hits[&5], 1); + assert_eq!(cov.function_hits["f"], 5); + // '-' + 2 = 2: the branch was evaluated by the second run. + assert_eq!(cov.branches[&(1, "0".into(), "0".into())], Some(2)); + assert_eq!(cov.lines_found(), 3); + assert_eq!(cov.lines_hit(), 2); + } + + #[test] + fn dash_branches_stay_dash_only_when_never_evaluated() { + let (a, _) = parse("SF:x.rs\nBRDA:1,0,0,-\nend_of_record\n"); + let (b, _) = parse("SF:x.rs\nBRDA:1,0,0,-\nend_of_record\n"); + let mut merged = Report::new(); + merge_reports(&mut merged, a); + merge_reports(&mut merged, b); + assert_eq!(merged["x.rs"].branches[&(1, "0".into(), "0".into())], None); + } + + #[test] + fn tolerates_missing_end_of_record_and_crlf() { + let (report, _) = parse("SF:a.kt\r\nDA:1,1\r\nSF:b.kt\r\nDA:2,0\r\n"); + assert_eq!(report.len(), 2); + assert_eq!(report["a.kt"].line_hits[&1], 1); + assert_eq!(report["b.kt"].line_hits[&2], 0); + } + + #[test] + fn malformed_records_warn_but_do_not_fail() { + let (report, warnings) = + parse("SF:a.kt\nDA:notanumber,1\nFN:x\nFN:y,f\nFNDA:z,f\nBRDA:1,0\nDA:7,1\nend_of_record\n"); + assert_eq!(report["a.kt"].line_hits, BTreeMap::from([(7, 1)])); + assert_eq!(warnings.len(), 5); + } + + #[test] + fn function_names_may_contain_commas() { + let (report, warnings) = parse("SF:a.cc\nFN:1,f\nFNDA:2,f\nend_of_record\n"); + assert!(warnings.is_empty()); + assert_eq!(report["a.cc"].function_lines["f"], 1); + assert_eq!(report["a.cc"].function_hits["f"], 2); + } + + #[test] + fn emit_recomputes_summary_counters_and_round_trips() { + let (report, _) = parse(SIMPLE); + let text = emit(&report); + assert_eq!( + text, + "SF:pkg/a.go\nFN:3,Foo\nFNDA:2,Foo\nFNF:1\nFNH:1\nBRDA:3,0,0,1\nBRDA:3,0,1,-\nBRF:2\nBRH:1\nDA:3,2\nDA:4,0\nLH:1\nLF:2\nend_of_record\n" + ); + let (reparsed, _) = parse(&text); + assert_eq!(reparsed, report); + } + + #[test] + fn emit_skips_function_and_branch_blocks_when_absent() { + let (report, _) = parse("SF:a.py\nDA:1,0\nend_of_record\n"); + assert_eq!(emit(&report), "SF:a.py\nDA:1,0\nLH:0\nLF:1\nend_of_record\n"); + } +} diff --git a/tools/coverage/src/lib.rs b/tools/coverage/src/lib.rs new file mode 100644 index 00000000..a5930ba7 --- /dev/null +++ b/tools/coverage/src/lib.rs @@ -0,0 +1,434 @@ +//! bazel-diff's LCOV merger: a drop-in replacement for Bazel's built-in +//! `@bazel_tools//tools/test:lcov_merger` (wired up via +//! `coverage --coverage_output_generator=//tools/coverage:lcov_merger` in +//! `.bazelrc`) that can additionally enforce a per-target line-coverage +//! minimum. +//! +//! Bazel only invokes this binary during `bazel coverage` — plain +//! `bazel test` runs never see it — so enforcement engages exclusively in +//! coverage runs. See `README.md` in this package for the user-facing story. + +pub mod args; +pub mod enforce; +pub mod lcov; +pub mod pattern; + +use std::collections::BTreeMap; +use std::fs; +use std::io::Write; +use std::path::{Path, PathBuf}; + +/// Exit code for a target that produced coverage below its declared minimum. +/// Distinct from `1` (operational failure) so the two are distinguishable in +/// test logs. +pub const EXIT_BELOW_MINIMUM: i32 = 33; + +/// Entry point shared by `main` and the integration tests. Returns the +/// process exit code; all diagnostics go to `log` (stderr in production, +/// captured buffers in tests). +pub fn run(argv: &[String], env: &BTreeMap, log: &mut dyn Write) -> i32 { + let args = match args::parse(argv) { + Ok(args) => args, + Err(msg) => { + let _ = writeln!(log, "lcov_merger: error: {msg}"); + return 1; + } + }; + for unknown in &args.unknown { + let _ = writeln!(log, "lcov_merger: warning: ignoring unknown flag {unknown}"); + } + + let coverage_dir = args.coverage_dir.as_deref().unwrap(); + let output_file = args.output_file.as_deref().unwrap(); + + // 1. Merge every LCOV tracefile found under the coverage directory. + let mut report = lcov::Report::new(); + for path in find_tracefiles(Path::new(coverage_dir), log) { + let text = match fs::read(&path) { + Ok(bytes) => String::from_utf8_lossy(&bytes).into_owned(), + Err(err) => { + let _ = writeln!( + log, + "lcov_merger: warning: skipping unreadable tracefile {}: {err}", + path.display() + ); + continue; + } + }; + let (parsed, warnings) = lcov::parse(&text); + for warning in warnings { + let _ = writeln!(log, "lcov_merger: warning: {}: {warning}", path.display()); + } + lcov::merge_reports(&mut report, parsed); + } + + // 2. Drop sources matching the --filter_sources patterns (system paths + // like /usr/bin/.+ in Bazel's default invocation). + for source in &args.filter_sources { + match pattern::Pattern::compile(source) { + Ok(pattern) => report.retain(|path, _| !pattern.matches(path)), + Err(msg) => { + let _ = writeln!( + log, + "lcov_merger: warning: ignoring --filter_sources pattern: {msg}" + ); + } + } + } + + // 3. Restrict to the sources Bazel declared as instrumented, mirroring + // the built-in merger: manifest entries that are not instrumentation + // metadata (.gcno/.em) name the files the target owns. This is what + // keeps test sources themselves out of the merged report. + if let Some(manifest_path) = args.source_file_manifest.as_deref() { + match fs::read_to_string(manifest_path) { + Ok(manifest) => { + let sources: std::collections::BTreeSet<&str> = manifest + .lines() + .map(str::trim) + .filter(|l| !l.is_empty() && !l.ends_with(".gcno") && !l.ends_with(".em")) + .collect(); + if !sources.is_empty() { + let before = report.len(); + report.retain(|path, _| sources.contains(path.as_str())); + if report.is_empty() && before > 0 { + let _ = writeln!( + log, + "lcov_merger: warning: the source file manifest {manifest_path} \ + matched none of the {before} covered file(s); the merged report \ + is empty. Tracefile paths and manifest paths likely disagree." + ); + } + } + } + Err(err) => { + let _ = writeln!( + log, + "lcov_merger: warning: cannot read source file manifest {manifest_path}: {err}" + ); + } + } + } + + // 4. Publish the merged tracefile — always, even when enforcement fails + // below, so the report is available to debug the failure. + if let Err(err) = fs::write(output_file, lcov::emit(&report)) { + let _ = writeln!( + log, + "lcov_merger: error: cannot write merged report to {output_file}: {err}" + ); + return 1; + } + + // 5. Enforce the target's coverage minimum, if it declared one. + match enforce::config_from_env(env) { + Ok(None) => 0, + Ok(Some(config)) => { + let outcome = enforce::check(&report, &config); + let _ = writeln!( + log, + "lcov_merger: line-coverage minimum declared for this target \ + (min {:.2}%):\n\n{}", + config.min_line_coverage, outcome.report + ); + if outcome.passed { + 0 + } else { + EXIT_BELOW_MINIMUM + } + } + Err(msg) => { + let _ = writeln!(log, "lcov_merger: error: {msg}"); + 1 + } + } +} + +/// Recursively collect LCOV tracefiles (`*.dat` / `*.info`) under `dir`, +/// sorted for deterministic merge order. Other files (e.g. `.profraw` +/// leftovers or gcov intermediates) are ignored: every language rule this +/// repository uses converts to LCOV before the merger runs. +fn find_tracefiles(dir: &Path, log: &mut dyn Write) -> Vec { + let mut found = Vec::new(); + let mut stack = vec![dir.to_path_buf()]; + while let Some(current) = stack.pop() { + let entries = match fs::read_dir(¤t) { + Ok(entries) => entries, + Err(err) => { + let _ = writeln!( + log, + "lcov_merger: warning: cannot list {}: {err}", + current.display() + ); + continue; + } + }; + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + stack.push(path); + } else { + let name = path.file_name().and_then(|n| n.to_str()).unwrap_or(""); + if name.ends_with(".dat") || name.ends_with(".info") { + found.push(path); + } + } + } + } + found.sort(); + found +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::atomic::{AtomicU32, Ordering}; + + /// A unique scratch directory per test, under TEST_TMPDIR when Bazel + /// provides it. + fn scratch_dir(label: &str) -> PathBuf { + static COUNTER: AtomicU32 = AtomicU32::new(0); + let base = std::env::var("TEST_TMPDIR") + .map(PathBuf::from) + .unwrap_or_else(|_| std::env::temp_dir()); + let dir = base.join(format!( + "lcov_merger_test_{}_{label}_{}", + std::process::id(), + COUNTER.fetch_add(1, Ordering::Relaxed) + )); + fs::create_dir_all(&dir).unwrap(); + dir + } + + struct Invocation { + code: i32, + log: String, + merged: String, + } + + /// Drive `run()` against a synthetic COVERAGE_DIR the way + /// collect_coverage.sh would. + fn invoke( + label: &str, + tracefiles: &[(&str, &str)], + manifest: Option<&str>, + env: &[(&str, &str)], + extra_args: &[&str], + ) -> Invocation { + let dir = scratch_dir(label); + let coverage_dir = dir.join("coverage"); + fs::create_dir_all(coverage_dir.join("nested")).unwrap(); + for (name, content) in tracefiles { + fs::write(coverage_dir.join(name), content).unwrap(); + } + let output_file = dir.join("coverage.dat"); + let mut argv = vec![ + format!("--coverage_dir={}", coverage_dir.display()), + format!("--output_file={}", output_file.display()), + ]; + if let Some(manifest_content) = manifest { + let manifest_path = dir.join("manifest.txt"); + fs::write(&manifest_path, manifest_content).unwrap(); + argv.push(format!("--source_file_manifest={}", manifest_path.display())); + } + argv.extend(extra_args.iter().map(|s| s.to_string())); + let env: BTreeMap = env + .iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect(); + let mut log = Vec::new(); + let code = run(&argv, &env, &mut log); + Invocation { + code, + log: String::from_utf8(log).unwrap(), + merged: fs::read_to_string(&output_file).unwrap_or_default(), + } + } + + #[test] + fn merges_multiple_runner_tracefiles() { + let result = invoke( + "merge", + &[ + ("a.dat", "SF:pkg/lib.go\nDA:1,1\nDA:2,0\nend_of_record\n"), + ("nested/b.dat", "SF:pkg/lib.go\nDA:2,3\nend_of_record\n"), + ("ignored.profraw", "not lcov"), + ], + None, + &[], + &[], + ); + assert_eq!(result.code, 0, "{}", result.log); + assert_eq!( + result.merged, + "SF:pkg/lib.go\nDA:1,1\nDA:2,3\nLH:2\nLF:2\nend_of_record\n" + ); + } + + #[test] + fn filters_system_sources_and_ignores_bad_patterns() { + let result = invoke( + "filters", + &[( + "a.dat", + "SF:/usr/bin/tool\nDA:1,1\nend_of_record\nSF:pkg/lib.go\nDA:1,1\nend_of_record\n", + )], + None, + &[], + &["--filter_sources=/usr/bin/.+", "--filter_sources=a|b"], + ); + assert_eq!(result.code, 0); + assert!(!result.merged.contains("/usr/bin/tool")); + assert!(result.merged.contains("SF:pkg/lib.go")); + assert!(result.log.contains("ignoring --filter_sources pattern")); + } + + #[test] + fn manifest_restricts_to_instrumented_sources() { + let result = invoke( + "manifest", + &[( + "a.dat", + "SF:pkg/lib.go\nDA:1,1\nend_of_record\nSF:pkg/lib_test.go\nDA:1,1\nend_of_record\n", + )], + Some("pkg/lib.go\npkg/meta.gcno\npkg/other.em\n"), + &[], + &[], + ); + assert_eq!(result.code, 0); + assert!(result.merged.contains("SF:pkg/lib.go")); + assert!(!result.merged.contains("lib_test.go")); + } + + #[test] + fn metadata_only_manifest_does_not_restrict() { + let result = invoke( + "manifest_meta", + &[("a.dat", "SF:pkg/lib.cc\nDA:1,1\nend_of_record\n")], + Some("pkg/meta.gcno\n"), + &[], + &[], + ); + assert_eq!(result.code, 0); + assert!(result.merged.contains("SF:pkg/lib.cc")); + } + + #[test] + fn mismatched_manifest_warns_about_empty_report() { + let result = invoke( + "manifest_mismatch", + &[("a.dat", "SF:pkg/lib.go\nDA:1,1\nend_of_record\n")], + Some("other/file.go\n"), + &[], + &[], + ); + assert_eq!(result.code, 0); + assert_eq!(result.merged, ""); + assert!(result.log.contains("matched none")); + } + + #[test] + fn enforcement_passes_and_reports_in_the_log() { + let result = invoke( + "enforce_pass", + &[("a.dat", "SF:pkg/lib.go\nDA:1,1\nDA:2,1\nDA:3,0\nend_of_record\n")], + None, + &[(enforce::MIN_LINE_COVERAGE_ENV, "60")], + &[], + ); + assert_eq!(result.code, 0, "{}", result.log); + assert!(result.log.contains("Target line coverage: 66.67%")); + assert!(result.log.contains("PASS")); + } + + #[test] + fn enforcement_fails_with_distinct_exit_code() { + let result = invoke( + "enforce_fail", + &[("a.dat", "SF:pkg/lib.go\nDA:1,1\nDA:2,0\nDA:3,0\nend_of_record\n")], + None, + &[ + (enforce::MIN_LINE_COVERAGE_ENV, "90"), + (enforce::COVERAGE_INCLUDE_ENV, "pkg/"), + ], + &[], + ); + assert_eq!(result.code, EXIT_BELOW_MINIMUM); + assert!(result.log.contains("FAIL: line coverage 33.33%")); + // The merged report is still written for debugging. + assert!(result.merged.contains("SF:pkg/lib.go")); + } + + #[test] + fn enforcement_with_no_coverage_data_fails() { + let result = invoke( + "enforce_empty", + &[], + None, + &[(enforce::MIN_LINE_COVERAGE_ENV, "1")], + &[], + ); + assert_eq!(result.code, EXIT_BELOW_MINIMUM); + assert!(result.log.contains("No instrumented lines matched")); + } + + #[test] + fn bad_enforcement_config_is_a_hard_error() { + let result = invoke( + "enforce_bad", + &[("a.dat", "SF:x\nDA:1,1\nend_of_record\n")], + None, + &[(enforce::MIN_LINE_COVERAGE_ENV, "lots")], + &[], + ); + assert_eq!(result.code, 1); + assert!(result.log.contains("is not a number")); + } + + #[test] + fn no_tracefiles_still_writes_an_empty_report() { + let result = invoke("no_data", &[], None, &[], &[]); + assert_eq!(result.code, 0); + assert_eq!(result.merged, ""); + } + + #[test] + fn unknown_flags_warn_and_bad_argv_errors() { + let result = invoke("unknown_flag", &[], None, &[], &["--future_flag=1"]); + assert_eq!(result.code, 0); + assert!(result.log.contains("ignoring unknown flag --future_flag=1")); + + let mut log = Vec::new(); + let code = run(&["positional".to_string()], &BTreeMap::new(), &mut log); + assert_eq!(code, 1); + assert!(String::from_utf8(log).unwrap().contains("error")); + } + + #[test] + fn unwritable_output_is_a_hard_error() { + let dir = scratch_dir("unwritable"); + let argv = vec![ + format!("--coverage_dir={}", dir.display()), + format!("--output_file={}/no_such_dir/out.dat", dir.display()), + ]; + let mut log = Vec::new(); + let code = run(&argv, &BTreeMap::new(), &mut log); + assert_eq!(code, 1); + assert!(String::from_utf8(log).unwrap().contains("cannot write")); + } + + #[test] + fn missing_coverage_dir_warns_but_produces_empty_report() { + let dir = scratch_dir("missing_dir"); + let out = dir.join("out.dat"); + let argv = vec![ + format!("--coverage_dir={}/does_not_exist", dir.display()), + format!("--output_file={}", out.display()), + ]; + let mut log = Vec::new(); + let code = run(&argv, &BTreeMap::new(), &mut log); + assert_eq!(code, 0); + assert!(String::from_utf8(log).unwrap().contains("cannot list")); + assert_eq!(fs::read_to_string(out).unwrap(), ""); + } +} diff --git a/tools/coverage/src/main.rs b/tools/coverage/src/main.rs new file mode 100644 index 00000000..df51c015 --- /dev/null +++ b/tools/coverage/src/main.rs @@ -0,0 +1,8 @@ +use std::collections::BTreeMap; + +fn main() { + let argv: Vec = std::env::args().skip(1).collect(); + let env: BTreeMap = std::env::vars().collect(); + let code = lcov_merger::run(&argv, &env, &mut std::io::stderr()); + std::process::exit(code); +} diff --git a/tools/coverage/src/pattern.rs b/tools/coverage/src/pattern.rs new file mode 100644 index 00000000..56f06fb4 --- /dev/null +++ b/tools/coverage/src/pattern.rs @@ -0,0 +1,232 @@ +//! A minimal regex subset for Bazel's `--filter_sources` patterns. +//! +//! Bazel's `collect_coverage.sh` invokes the LCOV merger with a fixed set of +//! filter patterns (`/usr/bin/.+`, `/usr/lib/.+`, `/usr/include.+`, +//! `/Applications/.+`), and users may add their own via custom rules. The +//! built-in CoverageOutputGenerator treats these as full-match Java regexes. +//! This module implements the small subset those patterns actually need — +//! literals, `.`, character classes, `*`/`+`/`?` quantifiers, and `\` +//! escapes — with full-string anchoring, avoiding a third-party regex crate. +//! +//! Patterns using syntax outside the subset (alternation, groups, `{n,m}` +//! repetition, anchors) fail to compile; the caller warns and skips the +//! filter rather than aborting the coverage action. + +#[derive(Debug, Clone, PartialEq)] +enum Elem { + /// A literal character. + Char(char), + /// `.` — any single character. + Any, + /// `[...]` — a character class; `(negated, ranges)`. + Class(bool, Vec<(char, char)>), +} + +#[derive(Debug, Clone, Copy, PartialEq)] +enum Quant { + One, + ZeroOrOne, + ZeroOrMore, + OneOrMore, +} + +/// A compiled pattern: a sequence of (element, quantifier) pairs matched +/// against the full input string. +#[derive(Debug, Clone, PartialEq)] +pub struct Pattern { + terms: Vec<(Elem, Quant)>, +} + +impl Pattern { + /// Compile `source`, or return a description of the unsupported syntax. + pub fn compile(source: &str) -> Result { + let mut terms: Vec<(Elem, Quant)> = Vec::new(); + let mut chars = source.chars().peekable(); + while let Some(c) = chars.next() { + let elem = match c { + '.' => Elem::Any, + '\\' => match chars.next() { + Some(escaped @ ('.' | '\\' | '*' | '+' | '?' | '[' | ']' | '/' | '-')) => { + Elem::Char(escaped) + } + Some(other) => { + return Err(format!("unsupported escape '\\{other}' in '{source}'")) + } + None => return Err(format!("dangling '\\' in '{source}'")), + }, + '[' => { + let negated = chars.peek() == Some(&'^'); + if negated { + chars.next(); + } + let mut ranges = Vec::new(); + loop { + match chars.next() { + None => return Err(format!("unterminated '[' in '{source}'")), + Some(']') if !ranges.is_empty() => break, + Some(lo) => { + if chars.peek() == Some(&'-') { + chars.next(); + match chars.next() { + Some(hi) if hi != ']' => ranges.push((lo, hi)), + _ => { + return Err(format!( + "unterminated range in class in '{source}'" + )) + } + } + } else { + ranges.push((lo, lo)); + } + } + } + } + Elem::Class(negated, ranges) + } + '*' | '+' | '?' => { + return Err(format!("quantifier '{c}' with nothing to repeat in '{source}'")) + } + '(' | ')' | '|' | '{' | '}' | '^' | '$' => { + return Err(format!("unsupported regex syntax '{c}' in '{source}'")) + } + other => Elem::Char(other), + }; + let quant = match chars.peek() { + Some('*') => { + chars.next(); + Quant::ZeroOrMore + } + Some('+') => { + chars.next(); + Quant::OneOrMore + } + Some('?') => { + chars.next(); + Quant::ZeroOrOne + } + _ => Quant::One, + }; + terms.push((elem, quant)); + } + Ok(Pattern { terms }) + } + + /// Full-string match (like Java's `Matcher.matches`). + pub fn matches(&self, input: &str) -> bool { + let chars: Vec = input.chars().collect(); + matches_here(&self.terms, &chars, 0) + } +} + +fn elem_matches(elem: &Elem, c: char) -> bool { + match elem { + Elem::Char(lit) => *lit == c, + Elem::Any => true, + Elem::Class(negated, ranges) => { + let inside = ranges.iter().any(|(lo, hi)| *lo <= c && c <= *hi); + inside != *negated + } + } +} + +/// Backtracking matcher: does `terms` consume exactly `input[pos..]`? +fn matches_here(terms: &[(Elem, Quant)], input: &[char], pos: usize) -> bool { + let Some(((elem, quant), rest)) = terms.split_first() else { + return pos == input.len(); + }; + match quant { + Quant::One => { + pos < input.len() + && elem_matches(elem, input[pos]) + && matches_here(rest, input, pos + 1) + } + Quant::ZeroOrOne => { + (pos < input.len() + && elem_matches(elem, input[pos]) + && matches_here(rest, input, pos + 1)) + || matches_here(rest, input, pos) + } + Quant::ZeroOrMore | Quant::OneOrMore => { + // Greedily consume the longest run, then backtrack one character + // at a time until the rest of the pattern matches. + let mut end = pos; + while end < input.len() && elem_matches(elem, input[end]) { + end += 1; + } + let min_end = if *quant == Quant::OneOrMore { pos + 1 } else { pos }; + loop { + if end < min_end { + return false; + } + if matches_here(rest, input, end) { + return true; + } + if end == 0 { + return false; + } + end -= 1; + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn matches(pattern: &str, input: &str) -> bool { + Pattern::compile(pattern).unwrap().matches(input) + } + + #[test] + fn bazels_default_filters_behave_like_full_match_regexes() { + assert!(matches("/usr/bin/.+", "/usr/bin/gcov")); + assert!(!matches("/usr/bin/.+", "/usr/bin/")); + assert!(!matches("/usr/bin/.+", "tools/usr/bin/x")); + assert!(matches("/usr/include.+", "/usr/include/stdio.h")); + assert!(matches("/Applications/.+", "/Applications/Xcode.app/x.h")); + assert!(!matches("/Applications/.+", "cli/src/main/kotlin/Main.kt")); + } + + #[test] + fn literals_dots_and_quantifiers() { + assert!(matches("a.c", "abc")); + assert!(!matches("a.c", "abbc")); + assert!(matches("ab*c", "ac")); + assert!(matches("ab*c", "abbbc")); + assert!(matches("ab?c", "ac")); + assert!(matches("ab?c", "abc")); + assert!(!matches("ab?c", "abbc")); + assert!(matches(".*third_party.*", "some/third_party/lib.cc")); + assert!(matches("a\\.c", "a.c")); + assert!(!matches("a\\.c", "abc")); + } + + #[test] + fn character_classes() { + assert!(matches("[a-c]+x", "abcx")); + assert!(!matches("[a-c]+x", "adx")); + assert!(matches("[^/]+\\.go", "sample.go")); + assert!(!matches("[^/]+\\.go", "dir/sample.go")); + // A ']' in first position inside a class is a literal, as in POSIX. + assert!(matches("[]]", "]")); + } + + #[test] + fn backtracking_terminates_and_matches_greedily() { + assert!(matches(".+.go", "sample.go")); + assert!(matches(".*.*x", "aaax")); + assert!(!matches(".+x", "")); + assert!(!matches(".+", "")); + assert!(matches(".*", "")); + } + + #[test] + fn unsupported_syntax_is_rejected_not_misparsed() { + for bad in [ + "a|b", "(ab)+", "a{2}", "^a", "a$", "\\d+", "a\\", "[abc", "+a", "[a-]x", + ] { + assert!(Pattern::compile(bad).is_err(), "expected error for {bad}"); + } + } +} diff --git a/tools/go/sample/BUILD b/tools/go/sample/BUILD index 41d33f30..014ffe9d 100644 --- a/tools/go/sample/BUILD +++ b/tools/go/sample/BUILD @@ -1,4 +1,5 @@ load("@rules_go//go:def.bzl", "go_library", "go_test") +load("//tools/coverage:defs.bzl", "coverage_enforced_test") go_library( name = "sample", @@ -7,8 +8,14 @@ go_library( visibility = ["//visibility:public"], ) -go_test( +# `bazel coverage` fails this target if line coverage of tools/go/ drops +# below 90% — the same floor the repo-wide CI gate applies to Go — enforced +# per-target by //tools/coverage:lcov_merger. +coverage_enforced_test( + coverage_include = ["tools/go/"], + embed = [":sample"], + min_line_coverage = 90, name = "sample_test", + rule = go_test, srcs = ["sample_test.go"], - embed = [":sample"], ) diff --git a/tools/readme_template.md b/tools/readme_template.md index b32ab9b6..2ac3f5b3 100644 --- a/tools/readme_template.md +++ b/tools/readme_template.md @@ -456,7 +456,7 @@ make coverage ``` This invokes -`bazel coverage --combined_report=lcov //cli/... //tools:coverage_check_test //tools/go/...` +`bazel coverage --combined_report=lcov //cli/... //tools:coverage_check_test //tools/coverage/... //tools/go/...` and then runs `//tools:coverage-check` twice against the resulting LCOV report — once for the Kotlin main sources and once scoped to `tools/go/` (`--include tools/go/`). The check is a Python `py_binary` ([`tools/coverage_check.py`](tools/coverage_check.py)) that prints a @@ -469,6 +469,34 @@ If you've already produced a coverage report and just want to re-check the thres The enforcement logic itself is tested under `//tools:coverage_check_test` — run it directly with `make coverage-test` (or `bazel test //tools:coverage_check_test`). +### Per-target coverage minimums + +In addition to the repo-wide gate above, individual test targets declare their own +line-coverage minimums, enforced *during* the coverage run itself by a Rust LCOV +merger ([`tools/coverage/`](tools/coverage/)) that replaces Bazel's built-in one +(`coverage --coverage_output_generator=//tools/coverage:lcov_merger` in `.bazelrc`). +Bazel only invokes the merger for `bazel coverage`, so plain `bazel test` runs are +unaffected. A target opts in through its `env` attribute via +`//tools/coverage:defs.bzl`: + +```starlark +load("//tools/coverage:defs.bzl", "coverage_enforced_test") + +coverage_enforced_test( + rule = go_test, # any test rule with the standard `env` attribute + name = "sample_test", + min_line_coverage = 90, + coverage_include = ["tools/go/"], + ... +) +``` + +Go (`//tools/go/sample:sample_test`), Rust (`//tools/coverage:lcov_merger_test`) +and the Kotlin/JVM tests under `//cli` all carry such minimums. When a target's +merged report falls below its minimum, the coverage run fails that target and the +test log contains a per-file breakdown. See +[`tools/coverage/README.md`](tools/coverage/README.md) for details. + For an interactive HTML report (annotated source with covered/uncovered lines highlighted), use `make coverage-html`. This requires the `lcov` package (`brew install lcov` on macOS, `apt-get install lcov` on Debian/Ubuntu) and writes From d03e4bb124d6fbffe9f4281f3721c74a14a165d9 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 10 Aug 2026 12:59:50 +0000 Subject: [PATCH 2/4] Enforce per-target coverage minimums on all Kotlin, Go, and Rust tests Apply the lcov_merger enforcement (previous commit) across every test target in the repository that produces coverage: - cli/BUILD: all 40 kt_jvm_test targets declare a minimum via coverage_minimum_env, scoped to the source file(s) each test is responsible for (Jacoco instruments all of :cli-lib for every test, so unscoped percentages would be meaningless). Floors were set from measured per-target coverage minus ~10 points of headroom, rounded down to 5 and capped at 90, to absorb platform/Bazel-version variance while still catching real regressions. E2ETest carries a conservative 30% smoke floor over the whole main tree instead of a measured one. - rules_kotlin ignores --coverage_output_generator: kt_jvm_test hardcodes its _lcov_merger attribute to Bazel's built-in merger instead of the coverage fragment's output_generator configuration field, which would silently bypass enforcement for Kotlin targets. A single_version_override patch (tools/coverage/rules_kotlin_lcov_merger.patch) makes it read the configuration field, like rules_go/rules_rust/rules_java already do. Verified: an unreachable floor fails 'bazel coverage' with the merger's per-file report in the test log, and plain 'bazel test' is unaffected. - gazelle direct dep bumped 0.45.0 -> 0.47.0 to match the version MVS now resolves (rules_rust requires the newer one), silencing the check_direct_dependencies warning. - CI/Makefile coverage universes include //tools/coverage/...; README documents the per-target system. All 40 Kotlin targets (39 measured + E2E), the Go sample test, and the Rust merger's own test pass 'bazel coverage' with enforcement active. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_016KqwUSCW2t6fmvnRFyw9vX --- MODULE.bazel | 15 +- MODULE.bazel.lock | 992 +++++++++--------- README.md | 30 +- cli/BUILD | 174 +++ tools/coverage/BUILD | 2 +- tools/coverage/README.md | 8 + tools/coverage/rules_kotlin_lcov_merger.patch | 19 + tools/go/sample/BUILD | 4 +- 8 files changed, 740 insertions(+), 504 deletions(-) create mode 100644 tools/coverage/rules_kotlin_lcov_merger.patch diff --git a/MODULE.bazel b/MODULE.bazel index 35311d74..939657e3 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -13,6 +13,19 @@ bazel_dep(name = "rules_proto", version = "7.1.0") bazel_dep(name = "rules_java", version = "9.7.0") bazel_dep(name = "rules_kotlin", version = "2.4.0") bazel_dep(name = "rules_license", version = "1.0.0") + +# kt_jvm_test hardcodes its _lcov_merger to Bazel's built-in merger instead of +# reading the coverage fragment's output_generator configuration field, so it +# ignores --coverage_output_generator (see tools/coverage/README.md — that flag +# is how per-target coverage minimums are enforced). Patch it to use the +# configuration field, like rules_go/rules_rust/rules_java do. Root-module-only: +# overrides are ignored when bazel-diff is consumed as a dependency. +single_version_override( + module_name = "rules_kotlin", + patch_strip = 1, + patches = ["//tools/coverage:rules_kotlin_lcov_merger.patch"], +) + bazel_dep(name = "rules_jvm_external", version = "6.10") # Add protobuf and grpc for Bazel 9 compatibility @@ -39,7 +52,7 @@ bazel_dep(name = "rules_python", version = "1.8.4", dev_dependency = True) # whose line coverage is gated at >=90% in CI). Marked dev_dependency so consumers # of bazel-diff as a module don't inherit rules_go/gazelle via MVS. bazel_dep(name = "rules_go", version = "0.60.0", dev_dependency = True) -bazel_dep(name = "gazelle", version = "0.45.0", dev_dependency = True) +bazel_dep(name = "gazelle", version = "0.47.0", dev_dependency = True) go_sdk = use_extension("@rules_go//go:extensions.bzl", "go_sdk", dev_dependency = True) go_sdk.download(version = "1.23.1") diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index af414fcb..7dfb73f8 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -1,504 +1,498 @@ { "lockFileVersion": 24, "registryFileHashes": { - "https://bcr.bazel.build/bazel_registry.json": "8a28e4aff06ee60aed2a8c281907fb8bcbf3b753c91fb5a5c57da3215d5b3497", - "https://bcr.bazel.build/modules/abseil-cpp/20210324.2/MODULE.bazel": "7cd0312e064fde87c8d1cd79ba06c876bd23630c83466e9500321be55c96ace2", - "https://bcr.bazel.build/modules/abseil-cpp/20211102.0/MODULE.bazel": "70390338f7a5106231d20620712f7cccb659cd0e9d073d1991c038eb9fc57589", - "https://bcr.bazel.build/modules/abseil-cpp/20220623.1/MODULE.bazel": "73ae41b6818d423a11fd79d95aedef1258f304448193d4db4ff90e5e7a0f076c", - "https://bcr.bazel.build/modules/abseil-cpp/20230125.1/MODULE.bazel": "89047429cb0207707b2dface14ba7f8df85273d484c2572755be4bab7ce9c3a0", - "https://bcr.bazel.build/modules/abseil-cpp/20230802.0.bcr.1/MODULE.bazel": "1c8cec495288dccd14fdae6e3f95f772c1c91857047a098fad772034264cc8cb", - "https://bcr.bazel.build/modules/abseil-cpp/20230802.0/MODULE.bazel": "d253ae36a8bd9ee3c5955384096ccb6baf16a1b1e93e858370da0a3b94f77c16", - "https://bcr.bazel.build/modules/abseil-cpp/20230802.1/MODULE.bazel": "fa92e2eb41a04df73cdabeec37107316f7e5272650f81d6cc096418fe647b915", - "https://bcr.bazel.build/modules/abseil-cpp/20240116.0/MODULE.bazel": "98dc378d64c12a4e4741ad3362f87fb737ee6a0886b2d90c3cdbb4d93ea3e0bf", - "https://bcr.bazel.build/modules/abseil-cpp/20240116.1/MODULE.bazel": "37bcdb4440fbb61df6a1c296ae01b327f19e9bb521f9b8e26ec854b6f97309ed", - "https://bcr.bazel.build/modules/abseil-cpp/20240116.2/MODULE.bazel": "73939767a4686cd9a520d16af5ab440071ed75cec1a876bf2fcfaf1f71987a16", - "https://bcr.bazel.build/modules/abseil-cpp/20240722.0/MODULE.bazel": "88668a07647adbdc14cb3a7cd116fb23c9dda37a90a1681590b6c9d8339a5b84", - "https://bcr.bazel.build/modules/abseil-cpp/20250127.0/MODULE.bazel": "d1086e248cda6576862b4b3fe9ad76a214e08c189af5b42557a6e1888812c5d5", - "https://bcr.bazel.build/modules/abseil-cpp/20250127.1/MODULE.bazel": "c4a89e7ceb9bf1e25cf84a9f830ff6b817b72874088bf5141b314726e46a57c1", - "https://bcr.bazel.build/modules/abseil-cpp/20250512.0/MODULE.bazel": "c4d02dd22cd87458516655a45512060246ee2a4732f1fbe948a5bd9eb614e626", - "https://bcr.bazel.build/modules/abseil-cpp/20250512.1/MODULE.bazel": "d209fdb6f36ffaf61c509fcc81b19e81b411a999a934a032e10cd009a0226215", - "https://bcr.bazel.build/modules/abseil-cpp/20250814.0/MODULE.bazel": "c43c16ca2c432566cdb78913964497259903ebe8fb7d9b57b38e9f1425b427b8", - "https://bcr.bazel.build/modules/abseil-cpp/20250814.1/MODULE.bazel": "51f2312901470cdab0dbdf3b88c40cd21c62a7ed58a3de45b365ddc5b11bcab2", - "https://bcr.bazel.build/modules/abseil-cpp/20250814.1/source.json": "cea3901d7e299da7320700abbaafe57a65d039f10d0d7ea601c4a66938ea4b0c", - "https://bcr.bazel.build/modules/abseil-py/2.1.0/MODULE.bazel": "5ebe5bf853769c65707e5c28f216798f7a4b1042015e6a36e6d03094d94bec8a", - "https://bcr.bazel.build/modules/abseil-py/2.1.0/source.json": "0e8fc4f088ce07099c1cd6594c20c7ddbb48b4b3c0849b7d94ba94be88ff042b", - "https://bcr.bazel.build/modules/ape/1.0.1/MODULE.bazel": "37411cfd13bfc28cd264674d660a3ecb3b5b35b9dbe4c0b2be098683641b3fee", - "https://bcr.bazel.build/modules/ape/1.0.1/source.json": "96bc5909d1e3ccc4203272815ef874dbfd99651e240c05049f12193d16c1110b", - "https://bcr.bazel.build/modules/apple_support/1.11.1/MODULE.bazel": "1843d7cd8a58369a444fc6000e7304425fba600ff641592161d9f15b179fb896", - "https://bcr.bazel.build/modules/apple_support/1.13.0/MODULE.bazel": "7c8cdea7e031b7f9f67f0b497adf6d2c6a2675e9304ca93a9af6ed84eef5a524", - "https://bcr.bazel.build/modules/apple_support/1.15.1/MODULE.bazel": "a0556fefca0b1bb2de8567b8827518f94db6a6e7e7d632b4c48dc5f865bc7c85", - "https://bcr.bazel.build/modules/apple_support/1.17.1/MODULE.bazel": "655c922ab1209978a94ef6ca7d9d43e940cd97d9c172fb55f94d91ac53f8610b", - "https://bcr.bazel.build/modules/apple_support/1.21.0/MODULE.bazel": "ac1824ed5edf17dee2fdd4927ada30c9f8c3b520be1b5fd02a5da15bc10bff3e", - "https://bcr.bazel.build/modules/apple_support/1.21.1/MODULE.bazel": "5809fa3efab15d1f3c3c635af6974044bac8a4919c62238cce06acee8a8c11f1", - "https://bcr.bazel.build/modules/apple_support/1.23.1/MODULE.bazel": "53763fed456a968cf919b3240427cf3a9d5481ec5466abc9d5dc51bc70087442", - "https://bcr.bazel.build/modules/apple_support/1.24.1/MODULE.bazel": "f46e8ddad60aef170ee92b2f3d00ef66c147ceafea68b6877cb45bd91737f5f8", - "https://bcr.bazel.build/modules/apple_support/1.24.2/MODULE.bazel": "0e62471818affb9f0b26f128831d5c40b074d32e6dda5a0d3852847215a41ca4", - "https://bcr.bazel.build/modules/apple_support/1.24.2/source.json": "2c22c9827093250406c5568da6c54e6fdf0ef06238def3d99c71b12feb057a8d", - "https://bcr.bazel.build/modules/aspect_bazel_lib/1.31.2/MODULE.bazel": "7bee702b4862612f29333590f4b658a5832d433d6f8e4395f090e8f4e85d442f", - "https://bcr.bazel.build/modules/aspect_bazel_lib/1.38.0/MODULE.bazel": "6307fec451ba9962c1c969eb516ebfe1e46528f7fa92e1c9ac8646bef4cdaa3f", - "https://bcr.bazel.build/modules/aspect_bazel_lib/1.40.3/MODULE.bazel": "668e6bcb4d957fc0e284316dba546b705c8d43c857f87119619ee83c4555b859", - "https://bcr.bazel.build/modules/aspect_bazel_lib/1.42.2/MODULE.bazel": "2e0d8ab25c57a14f56ace1c8e881b69050417ff91b2fb7718dc00d201f3c3478", - "https://bcr.bazel.build/modules/aspect_bazel_lib/2.0.0/MODULE.bazel": "e118477db5c49419a88d78ebc7a2c2cea9d49600fe0f490c1903324a2c16ecd9", - "https://bcr.bazel.build/modules/aspect_bazel_lib/2.14.0/MODULE.bazel": "2b31ffcc9bdc8295b2167e07a757dbbc9ac8906e7028e5170a3708cecaac119f", - "https://bcr.bazel.build/modules/aspect_bazel_lib/2.19.3/MODULE.bazel": "253d739ba126f62a5767d832765b12b59e9f8d2bc88cc1572f4a73e46eb298ca", - "https://bcr.bazel.build/modules/aspect_bazel_lib/2.22.0/MODULE.bazel": "7fe0191f047d4fe4a4a46c1107e2350cbb58a8fc2e10913aa4322d3190dec0bf", - "https://bcr.bazel.build/modules/aspect_bazel_lib/2.22.5/MODULE.bazel": "004ba890363d05372a97248c37205ae64b6fa31047629cd2c0895a9d0c7779e8", - "https://bcr.bazel.build/modules/aspect_bazel_lib/2.22.5/source.json": "ac2c3213df8f985785f1d0aeb7f0f73d5324e6e67d593d9b9470fb74a25d4a9b", - "https://bcr.bazel.build/modules/aspect_bazel_lib/2.8.1/MODULE.bazel": "812d2dd42f65dca362152101fbec418029cc8fd34cbad1a2fde905383d705838", - "https://bcr.bazel.build/modules/aspect_rules_js/1.33.1/MODULE.bazel": "db3e7f16e471cf6827059d03af7c21859e7a0d2bc65429a3a11f005d46fc501b", - "https://bcr.bazel.build/modules/aspect_rules_js/1.39.0/MODULE.bazel": "aece421d479e3c31dc3e5f6d49a12acc2700457c03c556650ec7a0ff23fc0d95", - "https://bcr.bazel.build/modules/aspect_rules_js/1.40.0/MODULE.bazel": "01a1014e95e6816b68ecee2584ae929c7d6a1b72e4333ab1ff2d2c6c30babdf1", - "https://bcr.bazel.build/modules/aspect_rules_js/1.40.0/source.json": "b6fd491369e9ef888fdef64b839023a2360caaea8eb370d2cfbfdd2a96721311", - "https://bcr.bazel.build/modules/aspect_rules_lint/0.12.0/MODULE.bazel": "e767c5dbfeb254ec03275a7701b5cfde2c4d2873676804bc7cb27ddff3728fed", - "https://bcr.bazel.build/modules/aspect_rules_lint/2.1.0/MODULE.bazel": "857975c4ada95993c5afa8299f01964a2c182c4120443e2f7355586459bcb651", - "https://bcr.bazel.build/modules/aspect_rules_lint/2.1.0/source.json": "0c4046832f1c83dbe7f1671081c38a8892aae5e1d47590e543b604d1a6f86a57", - "https://bcr.bazel.build/modules/aspect_tools_telemetry/0.2.8/MODULE.bazel": "aa975a83e72bcaac62ee61ab12b788ea324a1d05c4aab28aadb202f647881679", - "https://bcr.bazel.build/modules/aspect_tools_telemetry/0.2.8/source.json": "786cbc49377fb6bf4859aec5b1c61f8fc26b08e9fdb929e2dde2e1e2a406bd24", - "https://bcr.bazel.build/modules/bazel_features/0.1.0/MODULE.bazel": "47011d645b0f949f42ee67f2e8775188a9cf4a0a1528aa2fa4952f2fd00906fd", - "https://bcr.bazel.build/modules/bazel_features/1.0.0/MODULE.bazel": "d7f022dc887efb96e1ee51cec7b2e48d41e36ff59a6e4f216c40e4029e1585bf", - "https://bcr.bazel.build/modules/bazel_features/1.1.0/MODULE.bazel": "cfd42ff3b815a5f39554d97182657f8c4b9719568eb7fded2b9135f084bf760b", - "https://bcr.bazel.build/modules/bazel_features/1.1.1/MODULE.bazel": "27b8c79ef57efe08efccbd9dd6ef70d61b4798320b8d3c134fd571f78963dbcd", - "https://bcr.bazel.build/modules/bazel_features/1.10.0/MODULE.bazel": "f75e8807570484a99be90abcd52b5e1f390362c258bcb73106f4544957a48101", - "https://bcr.bazel.build/modules/bazel_features/1.11.0/MODULE.bazel": "f9382337dd5a474c3b7d334c2f83e50b6eaedc284253334cf823044a26de03e8", - "https://bcr.bazel.build/modules/bazel_features/1.13.0/MODULE.bazel": "c14c33c7c3c730612bdbe14ebbb5e61936b6f11322ea95a6e91cd1ba962f94df", - "https://bcr.bazel.build/modules/bazel_features/1.15.0/MODULE.bazel": "d38ff6e517149dc509406aca0db3ad1efdd890a85e049585b7234d04238e2a4d", - "https://bcr.bazel.build/modules/bazel_features/1.17.0/MODULE.bazel": "039de32d21b816b47bd42c778e0454217e9c9caac4a3cf8e15c7231ee3ddee4d", - "https://bcr.bazel.build/modules/bazel_features/1.18.0/MODULE.bazel": "1be0ae2557ab3a72a57aeb31b29be347bcdc5d2b1eb1e70f39e3851a7e97041a", - "https://bcr.bazel.build/modules/bazel_features/1.19.0/MODULE.bazel": "59adcdf28230d220f0067b1f435b8537dd033bfff8db21335ef9217919c7fb58", - "https://bcr.bazel.build/modules/bazel_features/1.21.0/MODULE.bazel": "675642261665d8eea09989aa3b8afb5c37627f1be178382c320d1b46afba5e3b", - "https://bcr.bazel.build/modules/bazel_features/1.23.0/MODULE.bazel": "fd1ac84bc4e97a5a0816b7fd7d4d4f6d837b0047cf4cbd81652d616af3a6591a", - "https://bcr.bazel.build/modules/bazel_features/1.27.0/MODULE.bazel": "621eeee06c4458a9121d1f104efb80f39d34deff4984e778359c60eaf1a8cb65", - "https://bcr.bazel.build/modules/bazel_features/1.28.0/MODULE.bazel": "4b4200e6cbf8fa335b2c3f43e1d6ef3e240319c33d43d60cc0fbd4b87ece299d", - "https://bcr.bazel.build/modules/bazel_features/1.3.0/MODULE.bazel": "cdcafe83ec318cda34e02948e81d790aab8df7a929cec6f6969f13a489ccecd9", - "https://bcr.bazel.build/modules/bazel_features/1.30.0/MODULE.bazel": "a14b62d05969a293b80257e72e597c2da7f717e1e69fa8b339703ed6731bec87", - "https://bcr.bazel.build/modules/bazel_features/1.32.0/MODULE.bazel": "095d67022a58cb20f7e20e1aefecfa65257a222c18a938e2914fd257b5f1ccdc", - "https://bcr.bazel.build/modules/bazel_features/1.33.0/MODULE.bazel": "8b8dc9d2a4c88609409c3191165bccec0e4cb044cd7a72ccbe826583303459f6", - "https://bcr.bazel.build/modules/bazel_features/1.36.0/MODULE.bazel": "596cb62090b039caf1cad1d52a8bc35cf188ca9a4e279a828005e7ee49a1bec3", - "https://bcr.bazel.build/modules/bazel_features/1.39.0/MODULE.bazel": "28739425c1fc283c91931619749c832b555e60bcd1010b40d8441ce0a5cf726d", - "https://bcr.bazel.build/modules/bazel_features/1.39.0/source.json": "f63cbeb4c602098484d57001e5a07d31cb02bbccde9b5e2c9bf0b29d05283e93", - "https://bcr.bazel.build/modules/bazel_features/1.4.1/MODULE.bazel": "e45b6bb2350aff3e442ae1111c555e27eac1d915e77775f6fdc4b351b758b5d7", - "https://bcr.bazel.build/modules/bazel_features/1.9.0/MODULE.bazel": "885151d58d90d8d9c811eb75e3288c11f850e1d6b481a8c9f766adee4712358b", - "https://bcr.bazel.build/modules/bazel_features/1.9.1/MODULE.bazel": "8f679097876a9b609ad1f60249c49d68bfab783dd9be012faf9d82547b14815a", - "https://bcr.bazel.build/modules/bazel_jar_jar/0.1.7/MODULE.bazel": "d2736a1dbfd8f72befc532823b5112f2597a28064ce4aac280c65bee7d8830a0", - "https://bcr.bazel.build/modules/bazel_lib/3.0.0-beta.1/MODULE.bazel": "407729e232f611c3270005b016b437005daa7b1505826798ea584169a476e878", - "https://bcr.bazel.build/modules/bazel_lib/3.0.0-rc.0/MODULE.bazel": "d6e00979a98ac14ada5e31c8794708b41434d461e7e7ca39b59b765e6d233b18", - "https://bcr.bazel.build/modules/bazel_lib/3.0.0/MODULE.bazel": "22b70b80ac89ad3f3772526cd9feee2fa412c2b01933fea7ed13238a448d370d", - "https://bcr.bazel.build/modules/bazel_lib/3.1.0/MODULE.bazel": "6809765c14e3c766a9b9286c7b0ec56ed87a73326e48fe01749f0c0fdcfe3287", - "https://bcr.bazel.build/modules/bazel_lib/3.1.0/source.json": "aaf7c2dc816219f4cb356c9d65f2555fb7f9543e537199f74a921f7877d23dfb", - "https://bcr.bazel.build/modules/bazel_skylib/1.0.3/MODULE.bazel": "bcb0fd896384802d1ad283b4e4eb4d718eebd8cb820b0a2c3a347fb971afd9d8", - "https://bcr.bazel.build/modules/bazel_skylib/1.1.1/MODULE.bazel": "1add3e7d93ff2e6998f9e118022c84d163917d912f5afafb3058e3d2f1545b5e", - "https://bcr.bazel.build/modules/bazel_skylib/1.2.0/MODULE.bazel": "44fe84260e454ed94ad326352a698422dbe372b21a1ac9f3eab76eb531223686", - "https://bcr.bazel.build/modules/bazel_skylib/1.2.1/MODULE.bazel": "f35baf9da0efe45fa3da1696ae906eea3d615ad41e2e3def4aeb4e8bc0ef9a7a", - "https://bcr.bazel.build/modules/bazel_skylib/1.3.0/MODULE.bazel": "20228b92868bf5cfc41bda7afc8a8ba2a543201851de39d990ec957b513579c5", - "https://bcr.bazel.build/modules/bazel_skylib/1.4.1/MODULE.bazel": "a0dcb779424be33100dcae821e9e27e4f2901d9dfd5333efe5ac6a8d7ab75e1d", - "https://bcr.bazel.build/modules/bazel_skylib/1.4.2/MODULE.bazel": "3bd40978e7a1fac911d5989e6b09d8f64921865a45822d8b09e815eaa726a651", - "https://bcr.bazel.build/modules/bazel_skylib/1.5.0/MODULE.bazel": "32880f5e2945ce6a03d1fbd588e9198c0a959bb42297b2cfaf1685b7bc32e138", - "https://bcr.bazel.build/modules/bazel_skylib/1.6.1/MODULE.bazel": "8fdee2dbaace6c252131c00e1de4b165dc65af02ea278476187765e1a617b917", - "https://bcr.bazel.build/modules/bazel_skylib/1.7.0/MODULE.bazel": "0db596f4563de7938de764cc8deeabec291f55e8ec15299718b93c4423e9796d", - "https://bcr.bazel.build/modules/bazel_skylib/1.7.1/MODULE.bazel": "3120d80c5861aa616222ec015332e5f8d3171e062e3e804a2a0253e1be26e59b", - "https://bcr.bazel.build/modules/bazel_skylib/1.8.1/MODULE.bazel": "88ade7293becda963e0e3ea33e7d54d3425127e0a326e0d17da085a5f1f03ff6", - "https://bcr.bazel.build/modules/bazel_skylib/1.8.2/MODULE.bazel": "69ad6927098316848b34a9142bcc975e018ba27f08c4ff403f50c1b6e646ca67", - "https://bcr.bazel.build/modules/bazel_skylib/1.9.0/MODULE.bazel": "72997b29dfd95c3fa0d0c48322d05590418edef451f8db8db5509c57875fb4b7", - "https://bcr.bazel.build/modules/bazel_skylib/1.9.0/source.json": "7ad77c1e8c1b84222d9b3f3cae016a76639435744c19330b0b37c0a3c9da7dc0", - "https://bcr.bazel.build/modules/bazel_worker_api/0.0.1/MODULE.bazel": "02a13b77321773b2042e70ee5e4c5e099c8ddee4cf2da9cd420442c36938d4bd", - "https://bcr.bazel.build/modules/bazel_worker_api/0.0.4/MODULE.bazel": "460aa12d01231a80cce03c548287b433b321d205b0028ae596728c35e5ee442e", - "https://bcr.bazel.build/modules/bazel_worker_api/0.0.8/MODULE.bazel": "396c1ef53835aafe3d42ce6619080531ee770648303731f16cfaa33fa056bf0c", - "https://bcr.bazel.build/modules/bazel_worker_api/0.0.8/source.json": "abaf8ac9d2ab2f47bda9af4c0c080ff7907378888e1f4bc62a0539dd13ba61e8", - "https://bcr.bazel.build/modules/bazel_worker_java/0.0.4/MODULE.bazel": "82494a01018bb7ef06d4a17ec4cd7a758721f10eb8b6c820a818e70d669500db", - "https://bcr.bazel.build/modules/bazel_worker_java/0.0.8/MODULE.bazel": "e76479eae70bd4e8f5f4c2dfc5d03ab971cfb18750246c7b3f3454c5c2ee6629", - "https://bcr.bazel.build/modules/bazel_worker_java/0.0.8/source.json": "9395c4679444bc47bf7e51a710366a4480aa371c6f6bed01868e2fabcf11acec", - "https://bcr.bazel.build/modules/boringssl/0.0.0-20211025-d4f1ab9/MODULE.bazel": "6ee6353f8b1a701fe2178e1d925034294971350b6d3ac37e67e5a7d463267834", - "https://bcr.bazel.build/modules/boringssl/0.0.0-20230215-5c22014/MODULE.bazel": "4b03dc0d04375fa0271174badcd202ed249870c8e895b26664fd7298abea7282", - "https://bcr.bazel.build/modules/boringssl/0.0.0-20240530-2db0eb3/MODULE.bazel": "d0405b762c5e87cd445b7015f2b8da5400ef9a8dbca0bfefa6c1cea79d528a97", - "https://bcr.bazel.build/modules/boringssl/0.20240913.0/MODULE.bazel": "fcaa7503a5213290831a91ed1eb538551cf11ac0bc3a6ad92d0fef92c5bd25fb", - "https://bcr.bazel.build/modules/boringssl/0.20241024.0/MODULE.bazel": "b540cff73d948cb79cb0bc108d7cef391d2098a25adabfda5043e4ef548dbc87", - "https://bcr.bazel.build/modules/boringssl/0.20241024.0/source.json": "d843092e682b84188c043ac742965d7f96e04c846c7e338187e03238674909a9", - "https://bcr.bazel.build/modules/buildifier_prebuilt/6.4.0/MODULE.bazel": "37389c6b5a40c59410b4226d3bb54b08637f393d66e2fa57925c6fcf68e64bf4", - "https://bcr.bazel.build/modules/buildifier_prebuilt/8.5.1.2/MODULE.bazel": "9a6e0a2e87d1e3da679e157da5192ea351d5739ca1ff51831c2b736d5b6034de", - "https://bcr.bazel.build/modules/buildifier_prebuilt/8.5.1.2/source.json": "33e11b3bf11e39cb762480a7e6ea1d24d044636135cdd8b8e74b07ebcd3b8d8b", - "https://bcr.bazel.build/modules/buildozer/7.1.2/MODULE.bazel": "2e8dd40ede9c454042645fd8d8d0cd1527966aa5c919de86661e62953cd73d84", - "https://bcr.bazel.build/modules/buildozer/7.1.2/source.json": "c9028a501d2db85793a6996205c8de120944f50a0d570438fcae0457a5f9d1f8", - "https://bcr.bazel.build/modules/c-ares/1.15.0/MODULE.bazel": "ba0a78360fdc83f02f437a9e7df0532ad1fbaa59b722f6e715c11effebaa0166", - "https://bcr.bazel.build/modules/c-ares/1.19.1/MODULE.bazel": "73bca21720772370ff91cc8e88bbbaf14897720c6473e87c1ddc0f848284c313", - "https://bcr.bazel.build/modules/c-ares/1.34.5.bcr.1/MODULE.bazel": "f4632f68dbc075342966477d9c94a8a4a299d91e155980b042e1cd9f5a7ebcf5", - "https://bcr.bazel.build/modules/c-ares/1.34.5.bcr.1/source.json": "55fae1e004176f6cb36efcc77de9894f799d985f482b8f82fec989d556745a84", - "https://bcr.bazel.build/modules/cel-spec/0.15.0/MODULE.bazel": "e1eed53d233acbdcf024b4b0bc1528116d92c29713251b5154078ab1348cb600", - "https://bcr.bazel.build/modules/cel-spec/0.24.0/MODULE.bazel": "e310c7aff8490ed689ccafd32729b77a660b9547f5a5ba9b20e967011c324b36", - "https://bcr.bazel.build/modules/cel-spec/0.24.0/source.json": "522d08bc22524e07863276dd0f038f446a83166e91281dcfc07d5b8433c8d89e", - "https://bcr.bazel.build/modules/civetweb/1.16/MODULE.bazel": "46a38f9daeb57392e3827fce7d40926be0c802bd23cdd6bfd3a96c804de42fae", - "https://bcr.bazel.build/modules/civetweb/1.16/source.json": "ba8b9585adb8355cb51b999d57172fd05e7a762c56b8d4bac6db42c99de3beb7", - "https://bcr.bazel.build/modules/curl/8.4.0/MODULE.bazel": "0bc250aa1cb69590049383df7a9537c809591fcf876c620f5f097c58fdc9bc10", - "https://bcr.bazel.build/modules/curl/8.7.1/MODULE.bazel": "088221c35a2939c555e6e47cb31a81c15f8b59f4daa8009b1e9271a502d33485", - "https://bcr.bazel.build/modules/curl/8.8.0/MODULE.bazel": "7da3b3e79b0b4ee8f8c95d640bc6ad7b430ce66ef6e9c9d2bc29b3b5ef85f6fe", - "https://bcr.bazel.build/modules/curl/8.8.0/source.json": "d7d138b6878cf38891692fee0649ace35357fd549b425614d571786f054374d4", - "https://bcr.bazel.build/modules/cython/3.0.11-1/MODULE.bazel": "868b3f5c956c3657420d2302004c6bb92606bfa47e314bab7f2ba0630c7c966c", - "https://bcr.bazel.build/modules/cython/3.0.11-1/source.json": "da318be900b8ca9c3d1018839d3bebc5a8e1645620d0848fa2c696d4ecf7c296", - "https://bcr.bazel.build/modules/download_utils/1.0.1/MODULE.bazel": "f1d0afade59e37de978506d6bbf08d7fe5f94964e86944aaf58efcead827b41b", - "https://bcr.bazel.build/modules/download_utils/1.0.1/source.json": "05ddc5a3b1f7d8f3e5e0fd1617479e1cf72d63d59ab2b1f0463557a14fc6be0a", - "https://bcr.bazel.build/modules/envoy_api/0.0.0-20241214-918efc9/MODULE.bazel": "24e05f6f52f37be63a795192848555a2c8c855e7814dbc1ed419fb04a7005464", - "https://bcr.bazel.build/modules/envoy_api/0.0.0-20250128-4de3c74/MODULE.bazel": "1fe72489212c530086e3ffb0e018b2bfef4663200ca03571570f9f006bef1d75", - "https://bcr.bazel.build/modules/envoy_api/0.0.0-20251105-4a2b9a3/MODULE.bazel": "b66e87a0e0c2207f07e35c321388eb1feb036344565977444b52912c53a84466", - "https://bcr.bazel.build/modules/envoy_api/0.0.0-20251105-4a2b9a3/source.json": "c4780edf780977f2ab7d00a189432c5b0b2fa08c6e4e2e09d2950499364a687d", - "https://bcr.bazel.build/modules/gawk/5.3.2.bcr.1/MODULE.bazel": "cdf8cbe5ee750db04b78878c9633cc76e80dcf4416cbe982ac3a9222f80713c8", - "https://bcr.bazel.build/modules/gawk/5.3.2.bcr.1/source.json": "fa7b512dfcb5eafd90ce3959cf42a2a6fe96144ebbb4b3b3928054895f2afac2", - "https://bcr.bazel.build/modules/gazelle/0.27.0/MODULE.bazel": "3446abd608295de6d90b4a8a118ed64a9ce11dcb3dda2dc3290a22056bd20996", - "https://bcr.bazel.build/modules/gazelle/0.30.0/MODULE.bazel": "f888a1effe338491f35f0e0e85003b47bb9d8295ccba73c37e07702d8d31c65b", - "https://bcr.bazel.build/modules/gazelle/0.32.0/MODULE.bazel": "b499f58a5d0d3537f3cf5b76d8ada18242f64ec474d8391247438bf04f58c7b8", - "https://bcr.bazel.build/modules/gazelle/0.33.0/MODULE.bazel": "a13a0f279b462b784fb8dd52a4074526c4a2afe70e114c7d09066097a46b3350", - "https://bcr.bazel.build/modules/gazelle/0.34.0/MODULE.bazel": "abdd8ce4d70978933209db92e436deb3a8b737859e9354fb5fd11fb5c2004c8a", - "https://bcr.bazel.build/modules/gazelle/0.36.0/MODULE.bazel": "e375d5d6e9a6ca59b0cb38b0540bc9a05b6aa926d322f2de268ad267a2ee74c0", - "https://bcr.bazel.build/modules/gazelle/0.37.0/MODULE.bazel": "d1327ba0907d0275ed5103bfbbb13518f6c04955b402213319d0d6c0ce9839d4", - "https://bcr.bazel.build/modules/gazelle/0.39.1/MODULE.bazel": "1fa3fefad240e535066fd0e6950dfccd627d36dc699ee0034645e51dbde3980f", - "https://bcr.bazel.build/modules/gazelle/0.40.0/MODULE.bazel": "42ba5378ebe845fca43989a53186ab436d956db498acde790685fe0e8f9c6146", - "https://bcr.bazel.build/modules/gazelle/0.45.0/MODULE.bazel": "ecd19ebe9f8e024e1ccffb6d997cc893a974bcc581f1ae08f386bdd448b10687", - "https://bcr.bazel.build/modules/gazelle/0.46.0/MODULE.bazel": "3dec215dacf2427df87b524a2c99da387882a18d753f0b1b38675992bd0a99c6", - "https://bcr.bazel.build/modules/gazelle/0.47.0/MODULE.bazel": "b61bb007c4efad134aa30ee7f4a8e2a39b22aa5685f005edaa022fbd1de43ebc", - "https://bcr.bazel.build/modules/gazelle/0.47.0/source.json": "aeb2e5df14b7fb298625d75d08b9c65bdb0b56014c5eb89da9e5dd0572280ae6", - "https://bcr.bazel.build/modules/google_benchmark/1.8.2/MODULE.bazel": "a70cf1bba851000ba93b58ae2f6d76490a9feb74192e57ab8e8ff13c34ec50cb", - "https://bcr.bazel.build/modules/google_benchmark/1.8.4/MODULE.bazel": "c6d54a11dcf64ee63545f42561eda3fd94c1b5f5ebe1357011de63ae33739d5e", - "https://bcr.bazel.build/modules/google_benchmark/1.8.5/MODULE.bazel": "9ba9b31b984022828a950e3300410977eda2e35df35584c6b0b2d0c2e52766b7", - "https://bcr.bazel.build/modules/google_benchmark/1.9.4/MODULE.bazel": "3bab7c17c10580f87b647478a72a05621f88abc275afb97b578c828f56e59d45", - "https://bcr.bazel.build/modules/google_benchmark/1.9.4/source.json": "8e0036f76a5c2aa9c16ca0da57d8065cff69edeed58f1f85584c588c0ef723a5", - "https://bcr.bazel.build/modules/googleapis-cc/1.0.0/MODULE.bazel": "cf01757e7590c56140a4b81638ff2b3e7074769e6271720bbf738fcda25b6fc2", - "https://bcr.bazel.build/modules/googleapis-cc/1.0.0/source.json": "ab0e3a2ee9968a8848f59872fbbfa3e1f768597d71d2229e6caa319d357967c7", - "https://bcr.bazel.build/modules/googleapis-go/1.0.0/MODULE.bazel": "0a207a4c49da28c5cc1f7b3aeb23c2f7828c85c14aa8d9db0e30357a8d2250ed", - "https://bcr.bazel.build/modules/googleapis-go/1.0.0/source.json": "ef189be4e7853e1ebc6123fe20b71822bf9896bd1f8eed8f68505c4585f72a48", - "https://bcr.bazel.build/modules/googleapis-java/1.0.0/MODULE.bazel": "d633989337d069b5a95e6101777319681d7a4af4677e36801f11839d6512095c", - "https://bcr.bazel.build/modules/googleapis-java/1.0.0/source.json": "ee59e2de37e4b531172870ac0296afa38f1ea004105ee21b2793c31a9d0ddccd", - "https://bcr.bazel.build/modules/googleapis-rules-registry/1.0.0/MODULE.bazel": "97c6a4d413b373d4cc97065da3de1b2166e22cbbb5f4cc9f05760bfa83619e24", - "https://bcr.bazel.build/modules/googleapis-rules-registry/1.0.0/source.json": "cf611c836a60e98e2e2ab2de8004f119e9f06878dcf4ea2d95a437b1b7a89fe9", - "https://bcr.bazel.build/modules/googleapis/0.0.0-20240326-1c8d509c5/MODULE.bazel": "a4b7e46393c1cdcc5a00e6f85524467c48c565256b22b5fae20f84ab4a999a68", - "https://bcr.bazel.build/modules/googleapis/0.0.0-20240819-fe8ba054a/MODULE.bazel": "117b7c7be7327ed5d6c482274533f2dbd78631313f607094d4625c28203cacdf", - "https://bcr.bazel.build/modules/googleapis/0.0.0-20241220-5e258e33.bcr.1/MODULE.bazel": "ee6c30f82ecd476e61f019fb1151aaab380ea419958ff274ef2f0efca7969f5c", - "https://bcr.bazel.build/modules/googleapis/0.0.0-20251003-2193a2bf/MODULE.bazel": "cc9e5ed294ed9ebf42cdbbdddd2df29048519e3797004df1e3f369f31ff4f2d4", - "https://bcr.bazel.build/modules/googleapis/0.0.0-20251003-2193a2bf/source.json": "21558a194c519e27262cca9cf031bb166a666a8b7fb89993b700c828f8dc0857", - "https://bcr.bazel.build/modules/googletest/1.11.0/MODULE.bazel": "3a83f095183f66345ca86aa13c58b59f9f94a2f81999c093d4eeaa2d262d12f4", - "https://bcr.bazel.build/modules/googletest/1.14.0.bcr.1/MODULE.bazel": "22c31a561553727960057361aa33bf20fb2e98584bc4fec007906e27053f80c6", - "https://bcr.bazel.build/modules/googletest/1.14.0/MODULE.bazel": "cfbcbf3e6eac06ef9d85900f64424708cc08687d1b527f0ef65aa7517af8118f", - "https://bcr.bazel.build/modules/googletest/1.15.2/MODULE.bazel": "6de1edc1d26cafb0ea1a6ab3f4d4192d91a312fd2d360b63adaa213cd00b2108", - "https://bcr.bazel.build/modules/googletest/1.16.0/MODULE.bazel": "a175623c69e94fca4ca7acbc12031e637b0c489318cd4805606981d4d7adb34a", - "https://bcr.bazel.build/modules/googletest/1.17.0/MODULE.bazel": "dbec758171594a705933a29fcf69293d2468c49ec1f2ebca65c36f504d72df46", - "https://bcr.bazel.build/modules/googletest/1.17.0/source.json": "38e4454b25fc30f15439c0378e57909ab1fd0a443158aa35aec685da727cd713", - "https://bcr.bazel.build/modules/grpc-java/1.78.0/MODULE.bazel": "48f790fbb95625245295df1283e0dba344a4e30b4a9a9cefbabfa92bd84f3691", - "https://bcr.bazel.build/modules/grpc-proto/0.0.0-20240627-ec30f58/MODULE.bazel": "88de79051e668a04726e9ea94a481ec6f1692086735fd6f488ab908b3b909238", - "https://bcr.bazel.build/modules/grpc/1.41.0/MODULE.bazel": "5bcbfc2b274dabea628f0649dc50c90cf36543b1cfc31624832538644ad1aae8", - "https://bcr.bazel.build/modules/grpc/1.56.3.bcr.1/MODULE.bazel": "cd5b1eb276b806ec5ab85032921f24acc51735a69ace781be586880af20ab33f", - "https://bcr.bazel.build/modules/grpc/1.62.1/MODULE.bazel": "2998211594b8a79a6b459c4e797cfa19f0fb8b3be3149760ec7b8c99abfd426f", - "https://bcr.bazel.build/modules/grpc/1.63.1.bcr.1/MODULE.bazel": "d7b9fef03bd175e6825237b521b18a3c29f1ac15f8aa52c8a1a0f3bd8f33d54b", - "https://bcr.bazel.build/modules/grpc/1.66.0.bcr.2/MODULE.bazel": "0fa2b0fd028ce354febf0fe90f1ed8fecfbfc33118cddd95ac0418cc283333a0", - "https://bcr.bazel.build/modules/grpc/1.66.0.bcr.3/MODULE.bazel": "f6047e89faf488f5e3e65cb2594c6f5e86992abec7487163ff6b623526e543b0", - "https://bcr.bazel.build/modules/grpc/1.69.0/MODULE.bazel": "4e26e05c9e1ef291ccbc96aad8e457b1b8abedbc141623831629da2f8168eef6", - "https://bcr.bazel.build/modules/grpc/1.71.0/MODULE.bazel": "7fcab2c05530373f1a442c362b17740dd0c75b6a2a975eec8f5bf4c70a37928a", - "https://bcr.bazel.build/modules/grpc/1.74.1/MODULE.bazel": "09523be10ba2bfd999683671d0f8f22fb5b20ec77ad89b05ef58ff19a1b65c82", - "https://bcr.bazel.build/modules/grpc/1.76.0/MODULE.bazel": "7373bd407fcb183b8bf379fa35dfe29feebdc6d8ec1bc40004cec6c7b2544be0", - "https://bcr.bazel.build/modules/grpc/1.76.0/source.json": "5c1deef5b02cb7dd52df9cb628d956e02db3ec40bd165a465bc10b7bf47aa16a", - "https://bcr.bazel.build/modules/jq.bzl/0.1.0/MODULE.bazel": "2ce69b1af49952cd4121a9c3055faa679e748ce774c7f1fda9657f936cae902f", - "https://bcr.bazel.build/modules/jq.bzl/0.4.0/MODULE.bazel": "a7b39b37589f2b0dad53fd6c1ccaabbdb290330caa920d7ef3e6aad068cd4ab2", - "https://bcr.bazel.build/modules/jq.bzl/0.4.0/source.json": "52ec7530c4618e03f634b30ff719814a68d7d39c235938b7aa2abbfe1eb1c52c", - "https://bcr.bazel.build/modules/jsoncpp/1.9.5/MODULE.bazel": "31271aedc59e815656f5736f282bb7509a97c7ecb43e927ac1a37966e0578075", - "https://bcr.bazel.build/modules/jsoncpp/1.9.6/MODULE.bazel": "2f8d20d3b7d54143213c4dfc3d98225c42de7d666011528dc8fe91591e2e17b0", - "https://bcr.bazel.build/modules/jsoncpp/1.9.6/source.json": "a04756d367a2126c3541682864ecec52f92cdee80a35735a3cb249ce015ca000", - "https://bcr.bazel.build/modules/libpfm/4.11.0.bcr.1/MODULE.bazel": "e5362dadc90aab6724c83a2cc1e67cbed9c89a05d97fb1f90053c8deb1e445c8", - "https://bcr.bazel.build/modules/libpfm/4.11.0.bcr.1/source.json": "0646414d9037f8aad148781dd760bec90b0b25ac12fda5e03f8aadbd6b9c61e6", - "https://bcr.bazel.build/modules/libpfm/4.11.0/MODULE.bazel": "45061ff025b301940f1e30d2c16bea596c25b176c8b6b3087e92615adbd52902", - "https://bcr.bazel.build/modules/mbedtls/3.6.0/MODULE.bazel": "8e380e4698107c5f8766264d4df92e36766248447858db28187151d884995a09", - "https://bcr.bazel.build/modules/mbedtls/3.6.0/source.json": "1dbe7eb5258050afcc3806b9d43050f71c6f539ce0175535c670df606790b30c", - "https://bcr.bazel.build/modules/nlohmann_json/3.11.3/MODULE.bazel": "87023db2f55fc3a9949c7b08dc711fae4d4be339a80a99d04453c4bb3998eefc", - "https://bcr.bazel.build/modules/nlohmann_json/3.11.3/source.json": "296c63a90c6813e53b3812d24245711981fc7e563d98fe15625f55181494488a", - "https://bcr.bazel.build/modules/nlohmann_json/3.6.1/MODULE.bazel": "6f7b417dcc794d9add9e556673ad25cb3ba835224290f4f848f8e2db1e1fca74", - "https://bcr.bazel.build/modules/opencensus-cpp/0.0.0-20230502-50eb5de.bcr.2/MODULE.bazel": "cc18734138dd18c912c6ce2a59186db28f85d8058c99c9f21b46ca3e0aba0ebe", - "https://bcr.bazel.build/modules/opencensus-cpp/0.0.0-20230502-50eb5de.bcr.2/source.json": "7c135f9d42bb3b045669c3c6ab3bb3c208e00b46aca4422eea64c29811a5b240", - "https://bcr.bazel.build/modules/opencensus-cpp/0.0.0-20230502-50eb5de/MODULE.bazel": "02201d2921dadb4ec90c4980eca4b2a02904eddcf6fa02f3da7594fb7b0d821c", - "https://bcr.bazel.build/modules/opencensus-proto/0.4.1.bcr.2/MODULE.bazel": "789706a714855f92c5c8cfcf1ef32bbb64dcd3b7c9066756ad7986ec59709d29", - "https://bcr.bazel.build/modules/opencensus-proto/0.4.1.bcr.2/source.json": "aadf3f53e08b72376506b7c4ea3d167010c9efb160d7d6e1e304ed646bac1b36", - "https://bcr.bazel.build/modules/opencensus-proto/0.4.1/MODULE.bazel": "4a2e8b4d0b544002502474d611a5a183aa282251e14f6a01afe841c0c1b10372", - "https://bcr.bazel.build/modules/openssl/3.3.1.bcr.1/MODULE.bazel": "49c0c07e8fb87b480bccb842cfee1b32617f11dac590f732573c69058699a3d1", - "https://bcr.bazel.build/modules/openssl/3.3.1.bcr.1/source.json": "0c0872e048bbea052a9c541fb47019481a19201ba5555a71d762ad591bf94e1f", - "https://bcr.bazel.build/modules/opentelemetry-cpp/1.14.2/MODULE.bazel": "089a5613c2a159c7dfde098dabfc61e966889c7d6a81a98422a84c51535ed17d", - "https://bcr.bazel.build/modules/opentelemetry-cpp/1.16.0/MODULE.bazel": "b7379a140f538cea3f749179a2d481ed81942cc6f7b05a6113723eb34ac3b3e7", - "https://bcr.bazel.build/modules/opentelemetry-cpp/1.19.0/MODULE.bazel": "3455326c08b28415648a3d60d8e3c811847ebdbe64474f75b25878f25585aea1", - "https://bcr.bazel.build/modules/opentelemetry-cpp/1.19.0/source.json": "4e48137e4c3ecb99401ff99876df8fa330598d7da051869bec643446e8a8ff95", - "https://bcr.bazel.build/modules/opentelemetry-proto/1.1.0/MODULE.bazel": "a49f406e99bf05ab43ed4f5b3322fbd33adfd484b6546948929d1316299b68bf", - "https://bcr.bazel.build/modules/opentelemetry-proto/1.3.1/MODULE.bazel": "0141a50e989576ee064c11ce8dd5ec89993525bd9f9a09c5618e4dacc8df9352", - "https://bcr.bazel.build/modules/opentelemetry-proto/1.4.0.bcr.1/MODULE.bazel": "5ceaf25e11170d22eded4c8032728b4a3f273765fccda32f9e94f463755c4167", - "https://bcr.bazel.build/modules/opentelemetry-proto/1.5.0/MODULE.bazel": "7543d91a53b98e7b5b37c5a0865b93bff12c1ee022b1e322cd236b968894b030", - "https://bcr.bazel.build/modules/opentelemetry-proto/1.5.0/source.json": "046b721ce203e88cdaad44d7dd17a86b7200eab9388b663b234e72e13ff7b143", - "https://bcr.bazel.build/modules/opentracing-cpp/1.6.0/MODULE.bazel": "b3925269f63561b8b880ae7cf62ccf81f6ece55b62cd791eda9925147ae116ec", - "https://bcr.bazel.build/modules/opentracing-cpp/1.6.0/source.json": "da1cb1add160f5e5074b7272e9db6fd8f1b3336c15032cd0a653af9d2f484aed", - "https://bcr.bazel.build/modules/package_metadata/0.0.2/MODULE.bazel": "fb8d25550742674d63d7b250063d4580ca530499f045d70748b1b142081ebb92", - "https://bcr.bazel.build/modules/package_metadata/0.0.3/MODULE.bazel": "77890552ecea9e284b5424c9de827a58099348763a4359e975c359a83d4faa83", - "https://bcr.bazel.build/modules/package_metadata/0.0.5/MODULE.bazel": "ef4f9439e3270fdd6b9fd4dbc3d2f29d13888e44c529a1b243f7a31dfbc2e8e4", - "https://bcr.bazel.build/modules/package_metadata/0.0.7/MODULE.bazel": "7adb03933fc8401f495800cf4eafcff0edc6da0ff55c7db223ef69d19f689486", - "https://bcr.bazel.build/modules/package_metadata/0.0.7/source.json": "50639625e937b56115012674c797cca7a05a96b4878c87d803c13dc2b31de8a0", - "https://bcr.bazel.build/modules/platforms/0.0.10/MODULE.bazel": "8cb8efaf200bdeb2150d93e162c40f388529a25852b332cec879373771e48ed5", - "https://bcr.bazel.build/modules/platforms/0.0.11/MODULE.bazel": "0daefc49732e227caa8bfa834d65dc52e8cc18a2faf80df25e8caea151a9413f", - "https://bcr.bazel.build/modules/platforms/0.0.4/MODULE.bazel": "9b328e31ee156f53f3c416a64f8491f7eb731742655a47c9eec4703a71644aee", - "https://bcr.bazel.build/modules/platforms/0.0.5/MODULE.bazel": "5733b54ea419d5eaf7997054bb55f6a1d0b5ff8aedf0176fef9eea44f3acda37", - "https://bcr.bazel.build/modules/platforms/0.0.6/MODULE.bazel": "ad6eeef431dc52aefd2d77ed20a4b353f8ebf0f4ecdd26a807d2da5aa8cd0615", - "https://bcr.bazel.build/modules/platforms/0.0.7/MODULE.bazel": "72fd4a0ede9ee5c021f6a8dd92b503e089f46c227ba2813ff183b71616034814", - "https://bcr.bazel.build/modules/platforms/0.0.8/MODULE.bazel": "9f142c03e348f6d263719f5074b21ef3adf0b139ee4c5133e2aa35664da9eb2d", - "https://bcr.bazel.build/modules/platforms/0.0.9/MODULE.bazel": "4a87a60c927b56ddd67db50c89acaa62f4ce2a1d2149ccb63ffd871d5ce29ebc", - "https://bcr.bazel.build/modules/platforms/1.0.0/MODULE.bazel": "f05feb42b48f1b3c225e4ccf351f367be0371411a803198ec34a389fb22aa580", - "https://bcr.bazel.build/modules/platforms/1.0.0/source.json": "f4ff1fd412e0246fd38c82328eb209130ead81d62dcd5a9e40910f867f733d96", - "https://bcr.bazel.build/modules/prometheus-cpp/1.2.4/MODULE.bazel": "0fbe5dcff66311947a3f6b86ebc6a6d9328e31a28413ca864debc4a043f371e5", - "https://bcr.bazel.build/modules/prometheus-cpp/1.3.0.bcr.1/MODULE.bazel": "116ad46e97c1d2aeb020fe2899a342a7e703574ce7c0faf7e4810f938c974a9a", - "https://bcr.bazel.build/modules/prometheus-cpp/1.3.0.bcr.1/source.json": "e813cce2d450708cfcb26e309c5172583a7440776edf354e83e6788c768e5cca", - "https://bcr.bazel.build/modules/prometheus-cpp/1.3.0/MODULE.bazel": "ce82e086bbc0b60267e970f6a54b2ca6d0f22d3eb6633e00e2cc2899c700f3d8", - "https://bcr.bazel.build/modules/protobuf/21.7/MODULE.bazel": "a5a29bb89544f9b97edce05642fac225a808b5b7be74038ea3640fae2f8e66a7", - "https://bcr.bazel.build/modules/protobuf/23.1/MODULE.bazel": "88b393b3eb4101d18129e5db51847cd40a5517a53e81216144a8c32dfeeca52a", - "https://bcr.bazel.build/modules/protobuf/24.4/MODULE.bazel": "7bc7ce5f2abf36b3b7b7c8218d3acdebb9426aeb35c2257c96445756f970eb12", - "https://bcr.bazel.build/modules/protobuf/25.6/MODULE.bazel": "fc0ae073b47c7ede88b825ff79e64f1c058967c7a87a86cdf4abecd9e0516625", - "https://bcr.bazel.build/modules/protobuf/26.0.bcr.1/MODULE.bazel": "8f04d38c2da40a3715ff6bdce4d32c5981e6432557571482d43a62c31a24c2cf", - "https://bcr.bazel.build/modules/protobuf/26.0.bcr.2/MODULE.bazel": "62e0b84ca727bdeb55a6fe1ef180e6b191bbe548a58305ea1426c158067be534", - "https://bcr.bazel.build/modules/protobuf/26.0/MODULE.bazel": "8402da964092af40097f4a205eec2a33fd4a7748dc43632b7d1629bfd9a2b856", - "https://bcr.bazel.build/modules/protobuf/27.0-rc2/MODULE.bazel": "b2b0dbafd57b6bec0ca9b251da02e628c357dab53a097570aa7d79d020f107cf", - "https://bcr.bazel.build/modules/protobuf/27.0/MODULE.bazel": "7873b60be88844a0a1d8f80b9d5d20cfbd8495a689b8763e76c6372998d3f64c", - "https://bcr.bazel.build/modules/protobuf/27.1/MODULE.bazel": "703a7b614728bb06647f965264967a8ef1c39e09e8f167b3ca0bb1fd80449c0d", - "https://bcr.bazel.build/modules/protobuf/27.2/MODULE.bazel": "32450b50673882e4c8c3d10a83f3bc82161b213ed2f80d17e38bece8f165c295", - "https://bcr.bazel.build/modules/protobuf/29.0-rc2.bcr.1/MODULE.bazel": "52f4126f63a2f0bbf36b99c2a87648f08467a4eaf92ba726bc7d6a500bbf770c", - "https://bcr.bazel.build/modules/protobuf/29.0-rc2/MODULE.bazel": "6241d35983510143049943fc0d57937937122baf1b287862f9dc8590fc4c37df", - "https://bcr.bazel.build/modules/protobuf/29.0-rc3/MODULE.bazel": "33c2dfa286578573afc55a7acaea3cada4122b9631007c594bf0729f41c8de92", - "https://bcr.bazel.build/modules/protobuf/29.0/MODULE.bazel": "319dc8bf4c679ff87e71b1ccfb5a6e90a6dbc4693501d471f48662ac46d04e4e", - "https://bcr.bazel.build/modules/protobuf/29.1/MODULE.bazel": "557c3457560ff49e122ed76c0bc3397a64af9574691cb8201b4e46d4ab2ecb95", - "https://bcr.bazel.build/modules/protobuf/3.19.0/MODULE.bazel": "6b5fbb433f760a99a22b18b6850ed5784ef0e9928a72668b66e4d7ccd47db9b0", - "https://bcr.bazel.build/modules/protobuf/3.19.2/MODULE.bazel": "532ffe5f2186b69fdde039efe6df13ba726ff338c6bc82275ad433013fa10573", - "https://bcr.bazel.build/modules/protobuf/3.19.6/MODULE.bazel": "9233edc5e1f2ee276a60de3eaa47ac4132302ef9643238f23128fea53ea12858", - "https://bcr.bazel.build/modules/protobuf/30.0/MODULE.bazel": "0e736de5d52ad7824113f47e65256a26ee74b689ba859c5447a0663e5a075409", - "https://bcr.bazel.build/modules/protobuf/31.1/MODULE.bazel": "379a389bb330b7b8c1cdf331cc90bf3e13de5614799b3b52cdb7c6f389f6b38e", - "https://bcr.bazel.build/modules/protobuf/32.1/MODULE.bazel": "89cd2866a9cb07fee9ff74c41ceace11554f32e0d849de4e23ac55515cfada4d", - "https://bcr.bazel.build/modules/protobuf/33.0/MODULE.bazel": "c5270efb4aad37a2f893536076518793f409ea7df07a06df995d848d1690f21c", - "https://bcr.bazel.build/modules/protobuf/33.4/MODULE.bazel": "114775b816b38b6d0ca620450d6b02550c60ceedfdc8d9a229833b34a223dc42", - "https://bcr.bazel.build/modules/protobuf/33.4/source.json": "555f8686b4c7d6b5ba731fbea13bf656b4bfd9a7ff629c1d9d3f6e1d6155de79", - "https://bcr.bazel.build/modules/protoc-gen-validate/1.0.4.bcr.2/MODULE.bazel": "c4bd2c850211ff5b7dadf9d2d0496c1c922fdedc303c775b01dfd3b3efc907ed", - "https://bcr.bazel.build/modules/protoc-gen-validate/1.0.4/MODULE.bazel": "b8913c154b16177990f6126d2d2477d187f9ddc568e95ee3e2d50fc65d2c494a", - "https://bcr.bazel.build/modules/protoc-gen-validate/1.2.1.bcr.1/MODULE.bazel": "4bf09676b62fa587ae07e073420a76ec8766dcce7545e5f8c68cfa8e484b5120", - "https://bcr.bazel.build/modules/protoc-gen-validate/1.2.1.bcr.2/MODULE.bazel": "3bd4b14a8e7c78dbef973280deabaa139db1fe350aa92da03730a31f59082068", - "https://bcr.bazel.build/modules/protoc-gen-validate/1.2.1.bcr.2/source.json": "14c28a5527fcd699f5efbf83a046666efabed3384364bd48428de89dfdc8110e", - "https://bcr.bazel.build/modules/protoc-gen-validate/1.2.1/MODULE.bazel": "52b51f50533ec4fbd5d613cd093773f979ac2e035d954e02ca11de383f502505", - "https://bcr.bazel.build/modules/pybind11_bazel/2.11.1/MODULE.bazel": "88af1c246226d87e65be78ed49ecd1e6f5e98648558c14ce99176da041dc378e", - "https://bcr.bazel.build/modules/pybind11_bazel/2.12.0/MODULE.bazel": "e6f4c20442eaa7c90d7190d8dc539d0ab422f95c65a57cc59562170c58ae3d34", - "https://bcr.bazel.build/modules/pybind11_bazel/3.0.0/MODULE.bazel": "a2bfa6020ed603a00d944161c63173c7f109774e99bee0c2cd8dbf24159f8134", - "https://bcr.bazel.build/modules/pybind11_bazel/3.0.0/source.json": "d8f5104d4c21d272bf327ebe44366fb0b4c036cdaa1f5cceb21a408ca4ef2ef8", - "https://bcr.bazel.build/modules/rapidjson/1.1.0.bcr.20241007/MODULE.bazel": "82fbcb2e42f9e0040e76ccc74c06c3e46dfd33c64ca359293f8b84df0e6dff4c", - "https://bcr.bazel.build/modules/rapidjson/1.1.0.bcr.20241007/source.json": "5c42389ad0e21fc06b95ad7c0b730008271624a2fa3292e0eab5f30e15adeee3", - "https://bcr.bazel.build/modules/re2/2021-09-01/MODULE.bazel": "bcb6b96f3b071e6fe2d8bed9cc8ada137a105f9d2c5912e91d27528b3d123833", - "https://bcr.bazel.build/modules/re2/2023-09-01/MODULE.bazel": "cb3d511531b16cfc78a225a9e2136007a48cf8a677e4264baeab57fe78a80206", - "https://bcr.bazel.build/modules/re2/2024-05-01/MODULE.bazel": "55a3f059538f381107824e7d00df5df6d061ba1fb80e874e4909c0f0549e8f3e", - "https://bcr.bazel.build/modules/re2/2024-07-02.bcr.1/MODULE.bazel": "b4963dda9b31080be1905ef085ecd7dd6cd47c05c79b9cdf83ade83ab2ab271a", - "https://bcr.bazel.build/modules/re2/2024-07-02/MODULE.bazel": "0eadc4395959969297cbcf31a249ff457f2f1d456228c67719480205aa306daa", - "https://bcr.bazel.build/modules/re2/2025-11-05.bcr.1/MODULE.bazel": "3d9d4995833fc0334fc5c88b56a05288dd25d651544cd7b2233bbd6357bbeba0", - "https://bcr.bazel.build/modules/re2/2025-11-05.bcr.1/source.json": "7df1394aabda1c9bc188a302f5d54b1c657924edd04ebc57d2be29dbd7efd141", - "https://bcr.bazel.build/modules/rules_android/0.1.1/MODULE.bazel": "48809ab0091b07ad0182defb787c4c5328bd3a278938415c00a7b69b50c4d3a8", - "https://bcr.bazel.build/modules/rules_android/0.6.6/MODULE.bazel": "b0fb569752aab65ab1a9db0a8f6cfaf5aa1754965e17e95dcf0e4d88e192a68d", - "https://bcr.bazel.build/modules/rules_android/0.7.1/MODULE.bazel": "a806fc382a774252f228a40e3b11b9fcc6276f8778c7fb33e9f72937c6258363", - "https://bcr.bazel.build/modules/rules_android/0.7.1/source.json": "151440aed3f0f73a00d4ed5cec5d31f63a6fef9b95d8fab1eb1810150fa525f2", - "https://bcr.bazel.build/modules/rules_apple/3.13.0/MODULE.bazel": "b4559a2c6281ca3165275bb36c1f0ac74666632adc5bdb680e366de7ce845f43", - "https://bcr.bazel.build/modules/rules_apple/3.16.0/MODULE.bazel": "0d1caf0b8375942ce98ea944be754a18874041e4e0459401d925577624d3a54a", - "https://bcr.bazel.build/modules/rules_apple/3.5.1/MODULE.bazel": "3d1bbf65ad3692003d36d8a29eff54d4e5c1c5f4bfb60f79e28646a924d9101c", - "https://bcr.bazel.build/modules/rules_apple/4.1.0/MODULE.bazel": "76e10fd4a48038d3fc7c5dc6e63b7063bbf5304a2e3bd42edda6ec660eebea68", - "https://bcr.bazel.build/modules/rules_apple/4.1.0/source.json": "8ee81e1708756f81b343a5eb2b2f0b953f1d25c4ab3d4a68dc02754872e80715", - "https://bcr.bazel.build/modules/rules_buf/0.1.1/MODULE.bazel": "6189aec18a4f7caff599ad41b851ab7645d4f1e114aa6431acf9b0666eb92162", - "https://bcr.bazel.build/modules/rules_buf/0.5.2/MODULE.bazel": "5f2492d284ab9bedf2668178303abf5f3cd7d8cdf85d768951008e88456e9c6a", - "https://bcr.bazel.build/modules/rules_buf/0.5.2/source.json": "41876d4834c0832de4b393de6e55dfd1cb3b25d3109e4ba90eb7fb57c560e0d9", - "https://bcr.bazel.build/modules/rules_cc/0.0.1/MODULE.bazel": "cb2aa0747f84c6c3a78dad4e2049c154f08ab9d166b1273835a8174940365647", - "https://bcr.bazel.build/modules/rules_cc/0.0.10/MODULE.bazel": "ec1705118f7eaedd6e118508d3d26deba2a4e76476ada7e0e3965211be012002", - "https://bcr.bazel.build/modules/rules_cc/0.0.13/MODULE.bazel": "0e8529ed7b323dad0775ff924d2ae5af7640b23553dfcd4d34344c7e7a867191", - "https://bcr.bazel.build/modules/rules_cc/0.0.14/MODULE.bazel": "5e343a3aac88b8d7af3b1b6d2093b55c347b8eefc2e7d1442f7a02dc8fea48ac", - "https://bcr.bazel.build/modules/rules_cc/0.0.15/MODULE.bazel": "6704c35f7b4a72502ee81f61bf88706b54f06b3cbe5558ac17e2e14666cd5dcc", - "https://bcr.bazel.build/modules/rules_cc/0.0.16/MODULE.bazel": "7661303b8fc1b4d7f532e54e9d6565771fea666fbdf839e0a86affcd02defe87", - "https://bcr.bazel.build/modules/rules_cc/0.0.17/MODULE.bazel": "2ae1d8f4238ec67d7185d8861cb0a2cdf4bc608697c331b95bf990e69b62e64a", - "https://bcr.bazel.build/modules/rules_cc/0.0.2/MODULE.bazel": "6915987c90970493ab97393024c156ea8fb9f3bea953b2f3ec05c34f19b5695c", - "https://bcr.bazel.build/modules/rules_cc/0.0.5/MODULE.bazel": "be41f87587998fe8890cd82ea4e848ed8eb799e053c224f78f3ff7fe1a1d9b74", - "https://bcr.bazel.build/modules/rules_cc/0.0.6/MODULE.bazel": "abf360251023dfe3efcef65ab9d56beefa8394d4176dd29529750e1c57eaa33f", - "https://bcr.bazel.build/modules/rules_cc/0.0.8/MODULE.bazel": "964c85c82cfeb6f3855e6a07054fdb159aced38e99a5eecf7bce9d53990afa3e", - "https://bcr.bazel.build/modules/rules_cc/0.0.9/MODULE.bazel": "836e76439f354b89afe6a911a7adf59a6b2518fafb174483ad78a2a2fde7b1c5", - "https://bcr.bazel.build/modules/rules_cc/0.1.1/MODULE.bazel": "2f0222a6f229f0bf44cd711dc13c858dad98c62d52bd51d8fc3a764a83125513", - "https://bcr.bazel.build/modules/rules_cc/0.1.2/MODULE.bazel": "557ddc3a96858ec0d465a87c0a931054d7dcfd6583af2c7ed3baf494407fd8d0", - "https://bcr.bazel.build/modules/rules_cc/0.1.5/MODULE.bazel": "88dfc9361e8b5ae1008ac38f7cdfd45ad738e4fa676a3ad67d19204f045a1fd8", - "https://bcr.bazel.build/modules/rules_cc/0.2.0/MODULE.bazel": "b5c17f90458caae90d2ccd114c81970062946f49f355610ed89bebf954f5783c", - "https://bcr.bazel.build/modules/rules_cc/0.2.13/MODULE.bazel": "eecdd666eda6be16a8d9dc15e44b5c75133405e820f620a234acc4b1fdc5aa37", - "https://bcr.bazel.build/modules/rules_cc/0.2.14/MODULE.bazel": "353c99ed148887ee89c54a17d4100ae7e7e436593d104b668476019023b58df8", - "https://bcr.bazel.build/modules/rules_cc/0.2.16/MODULE.bazel": "9242fa89f950c6ef7702801ab53922e99c69b02310c39fb6e62b2bd30df2a1d4", - "https://bcr.bazel.build/modules/rules_cc/0.2.17/MODULE.bazel": "1849602c86cb60da8613d2de887f9566a6d354a6df6d7009f9d04a14402f9a84", - "https://bcr.bazel.build/modules/rules_cc/0.2.17/source.json": "3832f45d145354049137c0090df04629d9c2b5493dc5c2bf46f1834040133a07", - "https://bcr.bazel.build/modules/rules_cc/0.2.4/MODULE.bazel": "1ff1223dfd24f3ecf8f028446d4a27608aa43c3f41e346d22838a4223980b8cc", - "https://bcr.bazel.build/modules/rules_cc/0.2.8/MODULE.bazel": "f1df20f0bf22c28192a794f29b501ee2018fa37a3862a1a2132ae2940a23a642", - "https://bcr.bazel.build/modules/rules_cc/0.2.9/MODULE.bazel": "34263f1dca62ea664265438cef714d7db124c03e1ed55ebb4f1dc860164308d1", - "https://bcr.bazel.build/modules/rules_diff/1.0.0/MODULE.bazel": "1739509d8db9a6cd7d3584822340d3dfe1f9f27e62462fbca60aa061d88741b2", - "https://bcr.bazel.build/modules/rules_diff/1.0.0/source.json": "fc3824aed007b4db160ffb994036c6e558550857b6634a8e9ccee3e74c659312", - "https://bcr.bazel.build/modules/rules_foreign_cc/0.10.1/MODULE.bazel": "b9527010e5fef060af92b6724edb3691970a5b1f76f74b21d39f7d433641be60", - "https://bcr.bazel.build/modules/rules_foreign_cc/0.15.1/MODULE.bazel": "c2c60d26c79fda484acb95cdbec46e89d6b28b4845cb277160ce1e0c8622bb88", - "https://bcr.bazel.build/modules/rules_foreign_cc/0.15.1/source.json": "a161811a63ba8a859086da3b7ff3ad04f2e9c255d7727b41087103fc0eb22f55", - "https://bcr.bazel.build/modules/rules_foreign_cc/0.9.0/MODULE.bazel": "c9e8c682bf75b0e7c704166d79b599f93b72cfca5ad7477df596947891feeef6", - "https://bcr.bazel.build/modules/rules_fuzzing/0.5.2/MODULE.bazel": "40c97d1144356f52905566c55811f13b299453a14ac7769dfba2ac38192337a8", - "https://bcr.bazel.build/modules/rules_go/0.33.0/MODULE.bazel": "a2b11b64cd24bf94f57454f53288a5dacfe6cb86453eee7761b7637728c1910c", - "https://bcr.bazel.build/modules/rules_go/0.38.1/MODULE.bazel": "fb8e73dd3b6fc4ff9d260ceacd830114891d49904f5bda1c16bc147bcc254f71", - "https://bcr.bazel.build/modules/rules_go/0.39.1/MODULE.bazel": "d34fb2a249403a5f4339c754f1e63dc9e5ad70b47c5e97faee1441fc6636cd61", - "https://bcr.bazel.build/modules/rules_go/0.41.0/MODULE.bazel": "55861d8e8bb0e62cbd2896f60ff303f62ffcb0eddb74ecb0e5c0cbe36fc292c8", - "https://bcr.bazel.build/modules/rules_go/0.42.0/MODULE.bazel": "8cfa875b9aa8c6fce2b2e5925e73c1388173ea3c32a0db4d2b4804b453c14270", - "https://bcr.bazel.build/modules/rules_go/0.45.1/MODULE.bazel": "6d7884f0edf890024eba8ab31a621faa98714df0ec9d512389519f0edff0281a", - "https://bcr.bazel.build/modules/rules_go/0.46.0/MODULE.bazel": "3477df8bdcc49e698b9d25f734c4f3a9f5931ff34ee48a2c662be168f5f2d3fd", - "https://bcr.bazel.build/modules/rules_go/0.48.0/MODULE.bazel": "d00ebcae0908ee3f5e6d53f68677a303d6d59a77beef879598700049c3980a03", - "https://bcr.bazel.build/modules/rules_go/0.50.1/MODULE.bazel": "b91a308dc5782bb0a8021ad4330c81fea5bda77f96b9e4c117b9b9c8f6665ee0", - "https://bcr.bazel.build/modules/rules_go/0.51.0-rc2/MODULE.bazel": "edfc3a9cea7bedb0eaaff37b0d7817c1a4bf72b3c615580b0ffcee6c52690fd4", - "https://bcr.bazel.build/modules/rules_go/0.53.0/MODULE.bazel": "a4ed760d3ac0dbc0d7b967631a9a3fd9100d28f7d9fcf214b4df87d4bfff5f9a", - "https://bcr.bazel.build/modules/rules_go/0.58.3/MODULE.bazel": "5582119a4a39558d8d1b1634bcae46043d4f43a31415e861c3551b2860040b5e", - "https://bcr.bazel.build/modules/rules_go/0.59.0/MODULE.bazel": "b7e43e7414a3139a7547d1b4909b29085fbe5182b6c58cbe1ed4c6272815aeae", - "https://bcr.bazel.build/modules/rules_go/0.60.0/MODULE.bazel": "4a57ff2ffc2a3570e3c5646575c5a4b07287e91bcdac5d1f72383d51502b48cb", - "https://bcr.bazel.build/modules/rules_go/0.60.0/source.json": "1e21368c5e0c3013a110bd79a8fcff8ca46b5bcb2b561713a7273cbfcff7c464", - "https://bcr.bazel.build/modules/rules_java/4.0.0/MODULE.bazel": "5a78a7ae82cd1a33cef56dc578c7d2a46ed0dca12643ee45edbb8417899e6f74", - "https://bcr.bazel.build/modules/rules_java/5.1.0/MODULE.bazel": "324b6478b0343a3ce7a9add8586ad75d24076d6d43d2f622990b9c1cfd8a1b15", - "https://bcr.bazel.build/modules/rules_java/5.3.5/MODULE.bazel": "a4ec4f2db570171e3e5eb753276ee4b389bae16b96207e9d3230895c99644b86", - "https://bcr.bazel.build/modules/rules_java/5.5.0/MODULE.bazel": "486ad1aa15cdc881af632b4b1448b0136c76025a1fe1ad1b65c5899376b83a50", - "https://bcr.bazel.build/modules/rules_java/6.0.0/MODULE.bazel": "8a43b7df601a7ec1af61d79345c17b31ea1fedc6711fd4abfd013ea612978e39", - "https://bcr.bazel.build/modules/rules_java/6.3.0/MODULE.bazel": "a97c7678c19f236a956ad260d59c86e10a463badb7eb2eda787490f4c969b963", - "https://bcr.bazel.build/modules/rules_java/6.3.1/MODULE.bazel": "5a3471c8b84d53d58d5f6e316313680d7dd2c70afac696dbe14b761b0b5c6a06", - "https://bcr.bazel.build/modules/rules_java/6.4.0/MODULE.bazel": "e986a9fe25aeaa84ac17ca093ef13a4637f6107375f64667a15999f77db6c8f6", - "https://bcr.bazel.build/modules/rules_java/6.5.2/MODULE.bazel": "1d440d262d0e08453fa0c4d8f699ba81609ed0e9a9a0f02cd10b3e7942e61e31", - "https://bcr.bazel.build/modules/rules_java/7.0.6/MODULE.bazel": "6ddb07d9857a1a3accc9f6d005f20c969c4659c7710e6269a51db3527e0ea969", - "https://bcr.bazel.build/modules/rules_java/7.1.0/MODULE.bazel": "30d9135a2b6561c761bd67bd4990da591e6bdc128790ce3e7afd6a3558b2fb64", - "https://bcr.bazel.build/modules/rules_java/7.10.0/MODULE.bazel": "530c3beb3067e870561739f1144329a21c851ff771cd752a49e06e3dc9c2e71a", - "https://bcr.bazel.build/modules/rules_java/7.12.2/MODULE.bazel": "579c505165ee757a4280ef83cda0150eea193eed3bef50b1004ba88b99da6de6", - "https://bcr.bazel.build/modules/rules_java/7.2.0/MODULE.bazel": "06c0334c9be61e6cef2c8c84a7800cef502063269a5af25ceb100b192453d4ab", - "https://bcr.bazel.build/modules/rules_java/7.3.2/MODULE.bazel": "50dece891cfdf1741ea230d001aa9c14398062f2b7c066470accace78e412bc2", - "https://bcr.bazel.build/modules/rules_java/7.4.0/MODULE.bazel": "a592852f8a3dd539e82ee6542013bf2cadfc4c6946be8941e189d224500a8934", - "https://bcr.bazel.build/modules/rules_java/7.6.1/MODULE.bazel": "2f14b7e8a1aa2f67ae92bc69d1ec0fa8d9f827c4e17ff5e5f02e91caa3b2d0fe", - "https://bcr.bazel.build/modules/rules_java/7.6.5/MODULE.bazel": "481164be5e02e4cab6e77a36927683263be56b7e36fef918b458d7a8a1ebadb1", - "https://bcr.bazel.build/modules/rules_java/8.13.0/MODULE.bazel": "0444ebf737d144cf2bb2ccb368e7f1cce735264285f2a3711785827c1686625e", - "https://bcr.bazel.build/modules/rules_java/8.14.0/MODULE.bazel": "717717ed40cc69994596a45aec6ea78135ea434b8402fb91b009b9151dd65615", - "https://bcr.bazel.build/modules/rules_java/8.16.1/MODULE.bazel": "0f20b1cecaa8e52f60a8f071e59a20b4e3b9a67f6c56c802ea256f6face692d3", - "https://bcr.bazel.build/modules/rules_java/8.3.2/MODULE.bazel": "7336d5511ad5af0b8615fdc7477535a2e4e723a357b6713af439fe8cf0195017", - "https://bcr.bazel.build/modules/rules_java/8.5.1/MODULE.bazel": "d8a9e38cc5228881f7055a6079f6f7821a073df3744d441978e7a43e20226939", - "https://bcr.bazel.build/modules/rules_java/8.6.0/MODULE.bazel": "9c064c434606d75a086f15ade5edb514308cccd1544c2b2a89bbac4310e41c71", - "https://bcr.bazel.build/modules/rules_java/8.6.1/MODULE.bazel": "f4808e2ab5b0197f094cabce9f4b006a27766beb6a9975931da07099560ca9c2", - "https://bcr.bazel.build/modules/rules_java/8.9.0/MODULE.bazel": "e17c876cb53dcd817b7b7f0d2985b710610169729e8c371b2221cacdcd3dce4a", - "https://bcr.bazel.build/modules/rules_java/9.3.0/MODULE.bazel": "f657c72d65ac449caae9abf2e68e66c0d36f9416848c4c4903d0b3234229e7f2", - "https://bcr.bazel.build/modules/rules_java/9.7.0/MODULE.bazel": "3ce6bd55fdd4fcb3323197736b7d976e0eedcc0eea78ca6186d20d314ba12e12", - "https://bcr.bazel.build/modules/rules_java/9.7.0/source.json": "23b356565156e0fbc71e5dad7cf8e1b951466b7ae3fd731cb1d2c95ca82b1d70", - "https://bcr.bazel.build/modules/rules_jvm_external/4.4.2/MODULE.bazel": "a56b85e418c83eb1839819f0b515c431010160383306d13ec21959ac412d2fe7", - "https://bcr.bazel.build/modules/rules_jvm_external/5.1/MODULE.bazel": "33f6f999e03183f7d088c9be518a63467dfd0be94a11d0055fe2d210f89aa909", - "https://bcr.bazel.build/modules/rules_jvm_external/5.2/MODULE.bazel": "d9351ba35217ad0de03816ef3ed63f89d411349353077348a45348b096615036", - "https://bcr.bazel.build/modules/rules_jvm_external/5.3/MODULE.bazel": "bf93870767689637164657731849fb887ad086739bd5d360d90007a581d5527d", - "https://bcr.bazel.build/modules/rules_jvm_external/6.0/MODULE.bazel": "37c93a5a78d32e895d52f86a8d0416176e915daabd029ccb5594db422e87c495", - "https://bcr.bazel.build/modules/rules_jvm_external/6.1/MODULE.bazel": "75b5fec090dbd46cf9b7d8ea08cf84a0472d92ba3585b476f44c326eda8059c4", - "https://bcr.bazel.build/modules/rules_jvm_external/6.10/MODULE.bazel": "33e636ca6bc9ee0fa090a38aa33c631ded2d8cf6fead4124181d1b35dc474f7c", - "https://bcr.bazel.build/modules/rules_jvm_external/6.10/source.json": "c191249787625db72616a3fb3cc2786ab57355a2e3b615402b8b3b66b0f995b7", - "https://bcr.bazel.build/modules/rules_jvm_external/6.2/MODULE.bazel": "36a6e52487a855f33cb960724eb56547fa87e2c98a0474c3acad94339d7f8e99", - "https://bcr.bazel.build/modules/rules_jvm_external/6.3/MODULE.bazel": "c998e060b85f71e00de5ec552019347c8bca255062c990ac02d051bb80a38df0", - "https://bcr.bazel.build/modules/rules_jvm_external/6.6/MODULE.bazel": "153042249c7060536dc95b6bb9f9bb8063b8a0b0cb7acdb381bddbc2374aed55", - "https://bcr.bazel.build/modules/rules_jvm_external/6.7/MODULE.bazel": "e717beabc4d091ecb2c803c2d341b88590e9116b8bf7947915eeb33aab4f96dd", - "https://bcr.bazel.build/modules/rules_jvm_external/6.9/MODULE.bazel": "07c5db05527db7744a54fcffd653e1550d40e0540207a7f7e6d0a4de5bef8274", - "https://bcr.bazel.build/modules/rules_kotlin/1.9.0/MODULE.bazel": "ef85697305025e5a61f395d4eaede272a5393cee479ace6686dba707de804d59", - "https://bcr.bazel.build/modules/rules_kotlin/1.9.5/MODULE.bazel": "043a16a572f610558ec2030db3ff0c9938574e7dd9f58bded1bb07c0192ef025", - "https://bcr.bazel.build/modules/rules_kotlin/1.9.6/MODULE.bazel": "d269a01a18ee74d0335450b10f62c9ed81f2321d7958a2934e44272fe82dcef3", - "https://bcr.bazel.build/modules/rules_kotlin/2.1.3/MODULE.bazel": "ce7def6d576aa8d3a9c6d10e13b4d157296229674371f67dbf788dae0afae3d5", - "https://bcr.bazel.build/modules/rules_kotlin/2.4.0/MODULE.bazel": "38dac18bb76c0a47ff60dfcd95c666985cbc46374f28ea4eeb868bdbc58c5bec", - "https://bcr.bazel.build/modules/rules_kotlin/2.4.0/source.json": "07b6a307448817c071c4ba90dcb03f801e008959f8dfd8b152a241cc0ee01a23", - "https://bcr.bazel.build/modules/rules_license/0.0.3/MODULE.bazel": "627e9ab0247f7d1e05736b59dbb1b6871373de5ad31c3011880b4133cafd4bd0", - "https://bcr.bazel.build/modules/rules_license/0.0.7/MODULE.bazel": "088fbeb0b6a419005b89cf93fe62d9517c0a2b8bb56af3244af65ecfe37e7d5d", - "https://bcr.bazel.build/modules/rules_license/0.0.8/MODULE.bazel": "5669c6fe49b5134dbf534db681ad3d67a2d49cfc197e4a95f1ca2fd7f3aebe96", - "https://bcr.bazel.build/modules/rules_license/1.0.0/MODULE.bazel": "a7fda60eefdf3d8c827262ba499957e4df06f659330bbe6cdbdb975b768bb65c", - "https://bcr.bazel.build/modules/rules_license/1.0.0/source.json": "a52c89e54cc311196e478f8382df91c15f7a2bfdf4c6cd0e2675cc2ff0b56efb", - "https://bcr.bazel.build/modules/rules_multirun/0.9.0/MODULE.bazel": "32d628ef586b5b23f67e55886b7bc38913ea4160420d66ae90521dda2ff37df0", - "https://bcr.bazel.build/modules/rules_multirun/0.9.0/source.json": "e882ba77962fa6c5fe68619e5c7d0374ec9a219fb8d03c42eadaf6d0243771bd", - "https://bcr.bazel.build/modules/rules_multitool/0.11.0/MODULE.bazel": "8d9dda78d2398e136300d3ef4fbcc89ede7c32c158d8c016fa7d032df41c4aaf", - "https://bcr.bazel.build/modules/rules_multitool/0.11.0/source.json": "0b86574a1eaff37c33aafaff095ea16d6ac846beb94ffc74c4fcf626f8f80681", - "https://bcr.bazel.build/modules/rules_nodejs/5.8.2/MODULE.bazel": "6bc03c8f37f69401b888023bf511cb6ee4781433b0cb56236b2e55a21e3a026a", - "https://bcr.bazel.build/modules/rules_nodejs/6.5.2/MODULE.bazel": "7f9ea68a0ce6d82905ce9f74e76ab8a8b4531ed4c747018c9d76424ad0b3370d", - "https://bcr.bazel.build/modules/rules_nodejs/6.7.3/MODULE.bazel": "c22a48b2a0dbf05a9dc5f83837bbc24c226c1f6e618de3c3a610044c9f336056", - "https://bcr.bazel.build/modules/rules_nodejs/6.7.3/source.json": "a3f966f4415a8a6545e560ee5449eac95cc633f96429d08e87c87775c72f5e09", - "https://bcr.bazel.build/modules/rules_perl/0.2.4/MODULE.bazel": "5f5af7be4bf5fb88d91af7469518f0fd2161718aefc606188f7cd51f436ca938", - "https://bcr.bazel.build/modules/rules_perl/0.2.4/source.json": "574317d6b3c7e4843fe611b76f15e62a1889949f5570702e1ee4ad335ea3c339", - "https://bcr.bazel.build/modules/rules_pkg/0.7.0/MODULE.bazel": "df99f03fc7934a4737122518bb87e667e62d780b610910f0447665a7e2be62dc", - "https://bcr.bazel.build/modules/rules_pkg/1.0.1/MODULE.bazel": "5b1df97dbc29623bccdf2b0dcd0f5cb08e2f2c9050aab1092fd39a41e82686ff", - "https://bcr.bazel.build/modules/rules_pkg/1.0.1/source.json": "bd82e5d7b9ce2d31e380dd9f50c111d678c3bdaca190cb76b0e1c71b05e1ba8a", - "https://bcr.bazel.build/modules/rules_proto/4.0.0/MODULE.bazel": "a7a7b6ce9bee418c1a760b3d84f83a299ad6952f9903c67f19e4edd964894e06", - "https://bcr.bazel.build/modules/rules_proto/5.3.0-21.7/MODULE.bazel": "e8dff86b0971688790ae75528fe1813f71809b5afd57facb44dad9e8eca631b7", - "https://bcr.bazel.build/modules/rules_proto/6.0.0-rc1/MODULE.bazel": "1e5b502e2e1a9e825eef74476a5a1ee524a92297085015a052510b09a1a09483", - "https://bcr.bazel.build/modules/rules_proto/6.0.0/MODULE.bazel": "b531d7f09f58dce456cd61b4579ce8c86b38544da75184eadaf0a7cb7966453f", - "https://bcr.bazel.build/modules/rules_proto/6.0.2/MODULE.bazel": "ce916b775a62b90b61888052a416ccdda405212b6aaeb39522f7dc53431a5e73", - "https://bcr.bazel.build/modules/rules_proto/7.0.2/MODULE.bazel": "bf81793bd6d2ad89a37a40693e56c61b0ee30f7a7fdbaf3eabbf5f39de47dea2", - "https://bcr.bazel.build/modules/rules_proto/7.1.0/MODULE.bazel": "002d62d9108f75bb807cd56245d45648f38275cb3a99dcd45dfb864c5d74cb96", - "https://bcr.bazel.build/modules/rules_proto/7.1.0/source.json": "39f89066c12c24097854e8f57ab8558929f9c8d474d34b2c00ac04630ad8940e", - "https://bcr.bazel.build/modules/rules_python/0.10.2/MODULE.bazel": "cc82bc96f2997baa545ab3ce73f196d040ffb8756fd2d66125a530031cd90e5f", - "https://bcr.bazel.build/modules/rules_python/0.20.0/MODULE.bazel": "bfe14d17f20e3fe900b9588f526f52c967a6f281e47a1d6b988679bd15082286", - "https://bcr.bazel.build/modules/rules_python/0.22.0/MODULE.bazel": "b8057bafa11a9e0f4b08fc3b7cd7bee0dcbccea209ac6fc9a3ff051cd03e19e9", - "https://bcr.bazel.build/modules/rules_python/0.22.1/MODULE.bazel": "26114f0c0b5e93018c0c066d6673f1a2c3737c7e90af95eff30cfee38d0bbac7", - "https://bcr.bazel.build/modules/rules_python/0.23.1/MODULE.bazel": "49ffccf0511cb8414de28321f5fcf2a31312b47c40cc21577144b7447f2bf300", - "https://bcr.bazel.build/modules/rules_python/0.25.0/MODULE.bazel": "72f1506841c920a1afec76975b35312410eea3aa7b63267436bfb1dd91d2d382", - "https://bcr.bazel.build/modules/rules_python/0.26.0/MODULE.bazel": "42cb98cd15954e83b96b540dcc6d5a618eb061f056147ac4ea46e687a066a7c7", - "https://bcr.bazel.build/modules/rules_python/0.27.1/MODULE.bazel": "65dc875cc1a06c30d5bbdba7ab021fd9e551a6579e408a3943a61303e2228a53", - "https://bcr.bazel.build/modules/rules_python/0.28.0/MODULE.bazel": "cba2573d870babc976664a912539b320cbaa7114cd3e8f053c720171cde331ed", - "https://bcr.bazel.build/modules/rules_python/0.29.0/MODULE.bazel": "2ac8cd70524b4b9ec49a0b8284c79e4cd86199296f82f6e0d5da3f783d660c82", - "https://bcr.bazel.build/modules/rules_python/0.31.0/MODULE.bazel": "93a43dc47ee570e6ec9f5779b2e64c1476a6ce921c48cc9a1678a91dd5f8fd58", - "https://bcr.bazel.build/modules/rules_python/0.32.2/MODULE.bazel": "01052470fc30b49de91fb8483d26bea6f664500cfad0b078d4605b03e3a83ed4", - "https://bcr.bazel.build/modules/rules_python/0.33.2/MODULE.bazel": "3e036c4ad8d804a4dad897d333d8dce200d943df4827cb849840055be8d2e937", - "https://bcr.bazel.build/modules/rules_python/0.35.0/MODULE.bazel": "c3657951764cdcdb5a7370d5e885fad5e8c1583320aad18d46f9f110d2c22755", - "https://bcr.bazel.build/modules/rules_python/0.37.1/MODULE.bazel": "3faeb2d9fa0a81f8980643ee33f212308f4d93eea4b9ce6f36d0b742e71e9500", - "https://bcr.bazel.build/modules/rules_python/0.37.2/MODULE.bazel": "b5ffde91410745750b6c13be1c5dc4555ef5bc50562af4a89fd77807fdde626a", - "https://bcr.bazel.build/modules/rules_python/0.4.0/MODULE.bazel": "9208ee05fd48bf09ac60ed269791cf17fb343db56c8226a720fbb1cdf467166c", - "https://bcr.bazel.build/modules/rules_python/0.40.0/MODULE.bazel": "9d1a3cd88ed7d8e39583d9ffe56ae8a244f67783ae89b60caafc9f5cf318ada7", - "https://bcr.bazel.build/modules/rules_python/1.0.0/MODULE.bazel": "898a3d999c22caa585eb062b600f88654bf92efb204fa346fb55f6f8edffca43", - "https://bcr.bazel.build/modules/rules_python/1.2.0/MODULE.bazel": "5aeeb48b2a6c19d668b48adf2b8a2b209a6310c230db0ce77450f148a89846e4", - "https://bcr.bazel.build/modules/rules_python/1.3.0/MODULE.bazel": "8361d57eafb67c09b75bf4bbe6be360e1b8f4f18118ab48037f2bd50aa2ccb13", - "https://bcr.bazel.build/modules/rules_python/1.4.1/MODULE.bazel": "8991ad45bdc25018301d6b7e1d3626afc3c8af8aaf4bc04f23d0b99c938b73a6", - "https://bcr.bazel.build/modules/rules_python/1.6.0/MODULE.bazel": "7e04ad8f8d5bea40451cf80b1bd8262552aa73f841415d20db96b7241bd027d8", - "https://bcr.bazel.build/modules/rules_python/1.6.3/MODULE.bazel": "a7b80c42cb3de5ee2a5fa1abc119684593704fcd2fec83165ebe615dec76574f", - "https://bcr.bazel.build/modules/rules_python/1.8.4/MODULE.bazel": "33e3971e66161a3e955f7a0d411a8d1f291c4ce4c561851512466f3c77ff8ece", - "https://bcr.bazel.build/modules/rules_python/1.8.4/source.json": "9fbc0e57bae52cddcc3831d668bce87a47e0c655104a85098d4459dd9a3b0a10", - "https://bcr.bazel.build/modules/rules_robolectric/4.14.1.2/MODULE.bazel": "d44fec647d0aeb67b9f3b980cf68ba634976f3ae7ccd6c07d790b59b87a4f251", - "https://bcr.bazel.build/modules/rules_robolectric/4.14.1.2/source.json": "37c10335f2361c337c5c1f34ed36d2da70534c23088062b33a8bdaab68aa9dea", - "https://bcr.bazel.build/modules/rules_rust/0.51.0/MODULE.bazel": "2b6d1617ac8503bfdcc0e4520c20539d4bba3a691100bee01afe193ceb0310f9", - "https://bcr.bazel.build/modules/rules_rust/0.67.0/MODULE.bazel": "87c3816c4321352dcfd9e9e26b58e84efc5b21351ae3ef8fb5d0d57bde7237f5", - "https://bcr.bazel.build/modules/rules_rust/0.67.0/source.json": "a8ef4d3be30eb98e060cad9e5875a55b603195487f76e01b619b51a1df4641cc", - "https://bcr.bazel.build/modules/rules_shell/0.1.2/MODULE.bazel": "66e4ca3ce084b04af0b9ff05ff14cab4e5df7503973818bb91cbc6cda08d32fc", - "https://bcr.bazel.build/modules/rules_shell/0.2.0/MODULE.bazel": "fda8a652ab3c7d8fee214de05e7a9916d8b28082234e8d2c0094505c5268ed3c", - "https://bcr.bazel.build/modules/rules_shell/0.3.0/MODULE.bazel": "de4402cd12f4cc8fda2354fce179fdb068c0b9ca1ec2d2b17b3e21b24c1a937b", - "https://bcr.bazel.build/modules/rules_shell/0.4.1/MODULE.bazel": "00e501db01bbf4e3e1dd1595959092c2fadf2087b2852d3f553b5370f5633592", - "https://bcr.bazel.build/modules/rules_shell/0.5.0/MODULE.bazel": "8c8447370594d45539f66858b602b0bb2cb2d3401a4ebb9ad25830c59c0f366d", - "https://bcr.bazel.build/modules/rules_shell/0.6.1/MODULE.bazel": "72e76b0eea4e81611ef5452aa82b3da34caca0c8b7b5c0c9584338aa93bae26b", - "https://bcr.bazel.build/modules/rules_shell/0.6.1/source.json": "20ec05cd5e592055e214b2da8ccb283c7f2a421ea0dc2acbf1aa792e11c03d0c", - "https://bcr.bazel.build/modules/rules_swift/1.16.0/MODULE.bazel": "4a09f199545a60d09895e8281362b1ff3bb08bbde69c6fc87aff5b92fcc916ca", - "https://bcr.bazel.build/modules/rules_swift/1.18.0/MODULE.bazel": "a6aba73625d0dc64c7b4a1e831549b6e375fbddb9d2dde9d80c9de6ec45b24c9", - "https://bcr.bazel.build/modules/rules_swift/2.1.1/MODULE.bazel": "494900a80f944fc7aa61500c2073d9729dff0b764f0e89b824eb746959bc1046", - "https://bcr.bazel.build/modules/rules_swift/2.4.0/MODULE.bazel": "1639617eb1ede28d774d967a738b4a68b0accb40650beadb57c21846beab5efd", - "https://bcr.bazel.build/modules/rules_swift/3.1.2/MODULE.bazel": "72c8f5cf9d26427cee6c76c8e3853eb46ce6b0412a081b2b6db6e8ad56267400", - "https://bcr.bazel.build/modules/rules_swift/3.1.2/source.json": "e85761f3098a6faf40b8187695e3de6d97944e98abd0d8ce579cb2daf6319a66", - "https://bcr.bazel.build/modules/stardoc/0.5.0/MODULE.bazel": "f9f1f46ba8d9c3362648eea571c6f9100680efc44913618811b58cc9c02cd678", - "https://bcr.bazel.build/modules/stardoc/0.5.1/MODULE.bazel": "1a05d92974d0c122f5ccf09291442580317cdd859f07a8655f1db9a60374f9f8", - "https://bcr.bazel.build/modules/stardoc/0.5.3/MODULE.bazel": "c7f6948dae6999bf0db32c1858ae345f112cacf98f174c7a8bb707e41b974f1c", - "https://bcr.bazel.build/modules/stardoc/0.5.4/MODULE.bazel": "6569966df04610b8520957cb8e97cf2e9faac2c0309657c537ab51c16c18a2a4", - "https://bcr.bazel.build/modules/stardoc/0.5.6/MODULE.bazel": "c43dabc564990eeab55e25ed61c07a1aadafe9ece96a4efabb3f8bf9063b71ef", - "https://bcr.bazel.build/modules/stardoc/0.6.2/MODULE.bazel": "7060193196395f5dd668eda046ccbeacebfd98efc77fed418dbe2b82ffaa39fd", - "https://bcr.bazel.build/modules/stardoc/0.7.0/MODULE.bazel": "05e3d6d30c099b6770e97da986c53bd31844d7f13d41412480ea265ac9e8079c", - "https://bcr.bazel.build/modules/stardoc/0.7.1/MODULE.bazel": "3548faea4ee5dda5580f9af150e79d0f6aea934fc60c1cc50f4efdd9420759e7", - "https://bcr.bazel.build/modules/stardoc/0.7.2/MODULE.bazel": "fc152419aa2ea0f51c29583fab1e8c99ddefd5b3778421845606ee628629e0e5", - "https://bcr.bazel.build/modules/stardoc/0.7.2/source.json": "58b029e5e901d6802967754adf0a9056747e8176f017cfe3607c0851f4d42216", - "https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.1/MODULE.bazel": "5e463fbfba7b1701d957555ed45097d7f984211330106ccd1352c6e0af0dcf91", - "https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.2/MODULE.bazel": "75aab2373a4bbe2a1260b9bf2a1ebbdbf872d3bd36f80bff058dccd82e89422f", - "https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.2/source.json": "5fba48bbe0ba48761f9e9f75f92876cafb5d07c0ce059cc7a8027416de94a05b", - "https://bcr.bazel.build/modules/tar.bzl/0.2.1/MODULE.bazel": "52d1c00a80a8cc67acbd01649e83d8dd6a9dc426a6c0b754a04fe8c219c76468", - "https://bcr.bazel.build/modules/tar.bzl/0.5.1/MODULE.bazel": "7c2eb3dcfc53b0f3d6f9acdfd911ca803eaf92aadf54f8ca6e4c1f3aee288351", - "https://bcr.bazel.build/modules/tar.bzl/0.5.1/source.json": "deed3094f7cc779ed1d37a68403847b0e38d9dd9d931e03cb90825f3368b515f", - "https://bcr.bazel.build/modules/toolchain_utils/1.0.2/MODULE.bazel": "9b8be503a4fcfd3b8b952525bff0869177a5234d5c35dc3e566b9f5ca2f755a1", - "https://bcr.bazel.build/modules/toolchain_utils/1.0.2/source.json": "88769ec576dddacafd8cca4631812cf8eead89f10a29d9405d9f7a553de6bf87", - "https://bcr.bazel.build/modules/upb/0.0.0-20211020-160625a/MODULE.bazel": "6cced416be2dc5b9c05efd5b997049ba795e5e4e6fafbe1624f4587767638928", - "https://bcr.bazel.build/modules/upb/0.0.0-20220923-a547704/MODULE.bazel": "7298990c00040a0e2f121f6c32544bab27d4452f80d9ce51349b1a28f3005c43", - "https://bcr.bazel.build/modules/upb/0.0.0-20230516-61a97ef/MODULE.bazel": "c0df5e35ad55e264160417fd0875932ee3c9dda63d9fccace35ac62f45e1b6f9", - "https://bcr.bazel.build/modules/upb/0.0.0-20230907-e7430e6/MODULE.bazel": "3a7dedadf70346e678dc059dbe44d05cbf3ab17f1ce43a1c7a42edc7cbf93fd9", - "https://bcr.bazel.build/modules/xds/0.0.0-20240423-555b57e/MODULE.bazel": "cea509976a77e34131411684ef05a1d6ad194dd71a8d5816643bc5b0af16dc0f", - "https://bcr.bazel.build/modules/xds/0.0.0-20240423-555b57e/source.json": "7227e1fcad55f3f3cab1a08691ecd753cb29cc6380a47bc650851be9f9ad6d20", - "https://bcr.bazel.build/modules/yq.bzl/0.1.1/MODULE.bazel": "9039681f9bcb8958ee2c87ffc74bdafba9f4369096a2b5634b88abc0eaefa072", - "https://bcr.bazel.build/modules/yq.bzl/0.1.1/source.json": "2d2bad780a9f2b9195a4a370314d2c17ae95eaa745cefc2e12fbc49759b15aa3", - "https://bcr.bazel.build/modules/zlib/1.2.11/MODULE.bazel": "07b389abc85fdbca459b69e2ec656ae5622873af3f845e1c9d80fe179f3effa0", - "https://bcr.bazel.build/modules/zlib/1.2.12/MODULE.bazel": "3b1a8834ada2a883674be8cbd36ede1b6ec481477ada359cd2d3ddc562340b27", - "https://bcr.bazel.build/modules/zlib/1.2.13/MODULE.bazel": "aa6deb1b83c18ffecd940c4119aff9567cd0a671d7bba756741cb2ef043a29d5", - "https://bcr.bazel.build/modules/zlib/1.3.1.bcr.1/MODULE.bazel": "6a9fe6e3fc865715a7be9823ce694ceb01e364c35f7a846bf0d2b34762bc066b", - "https://bcr.bazel.build/modules/zlib/1.3.1.bcr.3/MODULE.bazel": "af322bc08976524477c79d1e45e241b6efbeb918c497e8840b8ab116802dda79", - "https://bcr.bazel.build/modules/zlib/1.3.1.bcr.5/MODULE.bazel": "eec517b5bbe5492629466e11dae908d043364302283de25581e3eb944326c4ca", - "https://bcr.bazel.build/modules/zlib/1.3.1.bcr.5/source.json": "22bc55c47af97246cfc093d0acf683a7869377de362b5d1c552c2c2e16b7a806", - "https://bcr.bazel.build/modules/zlib/1.3.1/MODULE.bazel": "751c9940dcfe869f5f7274e1295422a34623555916eb98c174c1e945594bf198", - "https://bcr.bazel.build/modules/zlib/1.3/MODULE.bazel": "6a9c02f19a24dcedb05572b2381446e27c272cd383aed11d41d99da9e3167a72" + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/bazel_registry.json": "8a28e4aff06ee60aed2a8c281907fb8bcbf3b753c91fb5a5c57da3215d5b3497", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/abseil-cpp/20210324.2/MODULE.bazel": "7cd0312e064fde87c8d1cd79ba06c876bd23630c83466e9500321be55c96ace2", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/abseil-cpp/20211102.0/MODULE.bazel": "70390338f7a5106231d20620712f7cccb659cd0e9d073d1991c038eb9fc57589", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/abseil-cpp/20220623.1/MODULE.bazel": "73ae41b6818d423a11fd79d95aedef1258f304448193d4db4ff90e5e7a0f076c", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/abseil-cpp/20230125.1/MODULE.bazel": "89047429cb0207707b2dface14ba7f8df85273d484c2572755be4bab7ce9c3a0", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/abseil-cpp/20230802.0.bcr.1/MODULE.bazel": "1c8cec495288dccd14fdae6e3f95f772c1c91857047a098fad772034264cc8cb", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/abseil-cpp/20230802.0/MODULE.bazel": "d253ae36a8bd9ee3c5955384096ccb6baf16a1b1e93e858370da0a3b94f77c16", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/abseil-cpp/20230802.1/MODULE.bazel": "fa92e2eb41a04df73cdabeec37107316f7e5272650f81d6cc096418fe647b915", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/abseil-cpp/20240116.0/MODULE.bazel": "98dc378d64c12a4e4741ad3362f87fb737ee6a0886b2d90c3cdbb4d93ea3e0bf", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/abseil-cpp/20240116.1/MODULE.bazel": "37bcdb4440fbb61df6a1c296ae01b327f19e9bb521f9b8e26ec854b6f97309ed", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/abseil-cpp/20240116.2/MODULE.bazel": "73939767a4686cd9a520d16af5ab440071ed75cec1a876bf2fcfaf1f71987a16", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/abseil-cpp/20240722.0/MODULE.bazel": "88668a07647adbdc14cb3a7cd116fb23c9dda37a90a1681590b6c9d8339a5b84", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/abseil-cpp/20250127.0/MODULE.bazel": "d1086e248cda6576862b4b3fe9ad76a214e08c189af5b42557a6e1888812c5d5", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/abseil-cpp/20250127.1/MODULE.bazel": "c4a89e7ceb9bf1e25cf84a9f830ff6b817b72874088bf5141b314726e46a57c1", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/abseil-cpp/20250512.0/MODULE.bazel": "c4d02dd22cd87458516655a45512060246ee2a4732f1fbe948a5bd9eb614e626", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/abseil-cpp/20250512.1/MODULE.bazel": "d209fdb6f36ffaf61c509fcc81b19e81b411a999a934a032e10cd009a0226215", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/abseil-cpp/20250814.0/MODULE.bazel": "c43c16ca2c432566cdb78913964497259903ebe8fb7d9b57b38e9f1425b427b8", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/abseil-cpp/20250814.1/MODULE.bazel": "51f2312901470cdab0dbdf3b88c40cd21c62a7ed58a3de45b365ddc5b11bcab2", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/abseil-cpp/20250814.1/source.json": "cea3901d7e299da7320700abbaafe57a65d039f10d0d7ea601c4a66938ea4b0c", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/abseil-py/2.1.0/MODULE.bazel": "5ebe5bf853769c65707e5c28f216798f7a4b1042015e6a36e6d03094d94bec8a", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/abseil-py/2.1.0/source.json": "0e8fc4f088ce07099c1cd6594c20c7ddbb48b4b3c0849b7d94ba94be88ff042b", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/apple_support/1.11.1/MODULE.bazel": "1843d7cd8a58369a444fc6000e7304425fba600ff641592161d9f15b179fb896", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/apple_support/1.13.0/MODULE.bazel": "7c8cdea7e031b7f9f67f0b497adf6d2c6a2675e9304ca93a9af6ed84eef5a524", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/apple_support/1.15.1/MODULE.bazel": "a0556fefca0b1bb2de8567b8827518f94db6a6e7e7d632b4c48dc5f865bc7c85", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/apple_support/1.17.1/MODULE.bazel": "655c922ab1209978a94ef6ca7d9d43e940cd97d9c172fb55f94d91ac53f8610b", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/apple_support/1.21.0/MODULE.bazel": "ac1824ed5edf17dee2fdd4927ada30c9f8c3b520be1b5fd02a5da15bc10bff3e", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/apple_support/1.21.1/MODULE.bazel": "5809fa3efab15d1f3c3c635af6974044bac8a4919c62238cce06acee8a8c11f1", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/apple_support/1.23.1/MODULE.bazel": "53763fed456a968cf919b3240427cf3a9d5481ec5466abc9d5dc51bc70087442", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/apple_support/1.24.1/MODULE.bazel": "f46e8ddad60aef170ee92b2f3d00ef66c147ceafea68b6877cb45bd91737f5f8", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/apple_support/1.24.2/MODULE.bazel": "0e62471818affb9f0b26f128831d5c40b074d32e6dda5a0d3852847215a41ca4", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/apple_support/1.24.2/source.json": "2c22c9827093250406c5568da6c54e6fdf0ef06238def3d99c71b12feb057a8d", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/aspect_bazel_lib/1.31.2/MODULE.bazel": "7bee702b4862612f29333590f4b658a5832d433d6f8e4395f090e8f4e85d442f", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/aspect_bazel_lib/1.38.0/MODULE.bazel": "6307fec451ba9962c1c969eb516ebfe1e46528f7fa92e1c9ac8646bef4cdaa3f", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/aspect_bazel_lib/1.40.3/MODULE.bazel": "668e6bcb4d957fc0e284316dba546b705c8d43c857f87119619ee83c4555b859", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/aspect_bazel_lib/1.42.2/MODULE.bazel": "2e0d8ab25c57a14f56ace1c8e881b69050417ff91b2fb7718dc00d201f3c3478", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/aspect_bazel_lib/2.0.0/MODULE.bazel": "e118477db5c49419a88d78ebc7a2c2cea9d49600fe0f490c1903324a2c16ecd9", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/aspect_bazel_lib/2.14.0/MODULE.bazel": "2b31ffcc9bdc8295b2167e07a757dbbc9ac8906e7028e5170a3708cecaac119f", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/aspect_bazel_lib/2.19.3/MODULE.bazel": "253d739ba126f62a5767d832765b12b59e9f8d2bc88cc1572f4a73e46eb298ca", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/aspect_bazel_lib/2.22.0/MODULE.bazel": "7fe0191f047d4fe4a4a46c1107e2350cbb58a8fc2e10913aa4322d3190dec0bf", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/aspect_bazel_lib/2.22.5/MODULE.bazel": "004ba890363d05372a97248c37205ae64b6fa31047629cd2c0895a9d0c7779e8", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/aspect_bazel_lib/2.22.5/source.json": "ac2c3213df8f985785f1d0aeb7f0f73d5324e6e67d593d9b9470fb74a25d4a9b", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/aspect_bazel_lib/2.8.1/MODULE.bazel": "812d2dd42f65dca362152101fbec418029cc8fd34cbad1a2fde905383d705838", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/aspect_rules_js/1.33.1/MODULE.bazel": "db3e7f16e471cf6827059d03af7c21859e7a0d2bc65429a3a11f005d46fc501b", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/aspect_rules_js/1.39.0/MODULE.bazel": "aece421d479e3c31dc3e5f6d49a12acc2700457c03c556650ec7a0ff23fc0d95", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/aspect_rules_js/1.40.0/MODULE.bazel": "01a1014e95e6816b68ecee2584ae929c7d6a1b72e4333ab1ff2d2c6c30babdf1", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/aspect_rules_js/1.40.0/source.json": "b6fd491369e9ef888fdef64b839023a2360caaea8eb370d2cfbfdd2a96721311", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/aspect_rules_lint/0.12.0/MODULE.bazel": "e767c5dbfeb254ec03275a7701b5cfde2c4d2873676804bc7cb27ddff3728fed", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/aspect_rules_lint/2.1.0/MODULE.bazel": "857975c4ada95993c5afa8299f01964a2c182c4120443e2f7355586459bcb651", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/aspect_rules_lint/2.1.0/source.json": "0c4046832f1c83dbe7f1671081c38a8892aae5e1d47590e543b604d1a6f86a57", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/aspect_tools_telemetry/0.2.8/MODULE.bazel": "aa975a83e72bcaac62ee61ab12b788ea324a1d05c4aab28aadb202f647881679", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/aspect_tools_telemetry/0.2.8/source.json": "786cbc49377fb6bf4859aec5b1c61f8fc26b08e9fdb929e2dde2e1e2a406bd24", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/bazel_features/0.1.0/MODULE.bazel": "47011d645b0f949f42ee67f2e8775188a9cf4a0a1528aa2fa4952f2fd00906fd", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/bazel_features/1.0.0/MODULE.bazel": "d7f022dc887efb96e1ee51cec7b2e48d41e36ff59a6e4f216c40e4029e1585bf", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/bazel_features/1.1.0/MODULE.bazel": "cfd42ff3b815a5f39554d97182657f8c4b9719568eb7fded2b9135f084bf760b", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/bazel_features/1.1.1/MODULE.bazel": "27b8c79ef57efe08efccbd9dd6ef70d61b4798320b8d3c134fd571f78963dbcd", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/bazel_features/1.10.0/MODULE.bazel": "f75e8807570484a99be90abcd52b5e1f390362c258bcb73106f4544957a48101", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/bazel_features/1.11.0/MODULE.bazel": "f9382337dd5a474c3b7d334c2f83e50b6eaedc284253334cf823044a26de03e8", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/bazel_features/1.13.0/MODULE.bazel": "c14c33c7c3c730612bdbe14ebbb5e61936b6f11322ea95a6e91cd1ba962f94df", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/bazel_features/1.15.0/MODULE.bazel": "d38ff6e517149dc509406aca0db3ad1efdd890a85e049585b7234d04238e2a4d", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/bazel_features/1.17.0/MODULE.bazel": "039de32d21b816b47bd42c778e0454217e9c9caac4a3cf8e15c7231ee3ddee4d", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/bazel_features/1.18.0/MODULE.bazel": "1be0ae2557ab3a72a57aeb31b29be347bcdc5d2b1eb1e70f39e3851a7e97041a", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/bazel_features/1.19.0/MODULE.bazel": "59adcdf28230d220f0067b1f435b8537dd033bfff8db21335ef9217919c7fb58", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/bazel_features/1.21.0/MODULE.bazel": "675642261665d8eea09989aa3b8afb5c37627f1be178382c320d1b46afba5e3b", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/bazel_features/1.23.0/MODULE.bazel": "fd1ac84bc4e97a5a0816b7fd7d4d4f6d837b0047cf4cbd81652d616af3a6591a", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/bazel_features/1.27.0/MODULE.bazel": "621eeee06c4458a9121d1f104efb80f39d34deff4984e778359c60eaf1a8cb65", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/bazel_features/1.28.0/MODULE.bazel": "4b4200e6cbf8fa335b2c3f43e1d6ef3e240319c33d43d60cc0fbd4b87ece299d", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/bazel_features/1.3.0/MODULE.bazel": "cdcafe83ec318cda34e02948e81d790aab8df7a929cec6f6969f13a489ccecd9", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/bazel_features/1.30.0/MODULE.bazel": "a14b62d05969a293b80257e72e597c2da7f717e1e69fa8b339703ed6731bec87", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/bazel_features/1.32.0/MODULE.bazel": "095d67022a58cb20f7e20e1aefecfa65257a222c18a938e2914fd257b5f1ccdc", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/bazel_features/1.33.0/MODULE.bazel": "8b8dc9d2a4c88609409c3191165bccec0e4cb044cd7a72ccbe826583303459f6", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/bazel_features/1.36.0/MODULE.bazel": "596cb62090b039caf1cad1d52a8bc35cf188ca9a4e279a828005e7ee49a1bec3", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/bazel_features/1.39.0/MODULE.bazel": "28739425c1fc283c91931619749c832b555e60bcd1010b40d8441ce0a5cf726d", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/bazel_features/1.4.1/MODULE.bazel": "e45b6bb2350aff3e442ae1111c555e27eac1d915e77775f6fdc4b351b758b5d7", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/bazel_features/1.50.0/MODULE.bazel": "2083ef9c7a469f520890483ccf8e0189d6e71e2117e7752e15e6554433d5ae3e", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/bazel_features/1.50.0/source.json": "e0ee3debde2789ff56e4452e612d126925ba9ab64d4bde79c67f099d2902df9b", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/bazel_features/1.9.0/MODULE.bazel": "885151d58d90d8d9c811eb75e3288c11f850e1d6b481a8c9f766adee4712358b", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/bazel_features/1.9.1/MODULE.bazel": "8f679097876a9b609ad1f60249c49d68bfab783dd9be012faf9d82547b14815a", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/bazel_jar_jar/0.1.7/MODULE.bazel": "d2736a1dbfd8f72befc532823b5112f2597a28064ce4aac280c65bee7d8830a0", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/bazel_lib/3.0.0-beta.1/MODULE.bazel": "407729e232f611c3270005b016b437005daa7b1505826798ea584169a476e878", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/bazel_lib/3.0.0-rc.0/MODULE.bazel": "d6e00979a98ac14ada5e31c8794708b41434d461e7e7ca39b59b765e6d233b18", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/bazel_lib/3.0.0/MODULE.bazel": "22b70b80ac89ad3f3772526cd9feee2fa412c2b01933fea7ed13238a448d370d", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/bazel_lib/3.1.0/MODULE.bazel": "6809765c14e3c766a9b9286c7b0ec56ed87a73326e48fe01749f0c0fdcfe3287", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/bazel_lib/3.1.0/source.json": "aaf7c2dc816219f4cb356c9d65f2555fb7f9543e537199f74a921f7877d23dfb", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/bazel_skylib/1.0.3/MODULE.bazel": "bcb0fd896384802d1ad283b4e4eb4d718eebd8cb820b0a2c3a347fb971afd9d8", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/bazel_skylib/1.1.1/MODULE.bazel": "1add3e7d93ff2e6998f9e118022c84d163917d912f5afafb3058e3d2f1545b5e", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/bazel_skylib/1.2.0/MODULE.bazel": "44fe84260e454ed94ad326352a698422dbe372b21a1ac9f3eab76eb531223686", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/bazel_skylib/1.2.1/MODULE.bazel": "f35baf9da0efe45fa3da1696ae906eea3d615ad41e2e3def4aeb4e8bc0ef9a7a", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/bazel_skylib/1.3.0/MODULE.bazel": "20228b92868bf5cfc41bda7afc8a8ba2a543201851de39d990ec957b513579c5", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/bazel_skylib/1.4.1/MODULE.bazel": "a0dcb779424be33100dcae821e9e27e4f2901d9dfd5333efe5ac6a8d7ab75e1d", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/bazel_skylib/1.4.2/MODULE.bazel": "3bd40978e7a1fac911d5989e6b09d8f64921865a45822d8b09e815eaa726a651", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/bazel_skylib/1.5.0/MODULE.bazel": "32880f5e2945ce6a03d1fbd588e9198c0a959bb42297b2cfaf1685b7bc32e138", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/bazel_skylib/1.6.1/MODULE.bazel": "8fdee2dbaace6c252131c00e1de4b165dc65af02ea278476187765e1a617b917", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/bazel_skylib/1.7.0/MODULE.bazel": "0db596f4563de7938de764cc8deeabec291f55e8ec15299718b93c4423e9796d", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/bazel_skylib/1.7.1/MODULE.bazel": "3120d80c5861aa616222ec015332e5f8d3171e062e3e804a2a0253e1be26e59b", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/bazel_skylib/1.8.1/MODULE.bazel": "88ade7293becda963e0e3ea33e7d54d3425127e0a326e0d17da085a5f1f03ff6", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/bazel_skylib/1.8.2/MODULE.bazel": "69ad6927098316848b34a9142bcc975e018ba27f08c4ff403f50c1b6e646ca67", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/bazel_skylib/1.9.0/MODULE.bazel": "72997b29dfd95c3fa0d0c48322d05590418edef451f8db8db5509c57875fb4b7", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/bazel_skylib/1.9.0/source.json": "7ad77c1e8c1b84222d9b3f3cae016a76639435744c19330b0b37c0a3c9da7dc0", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/bazel_worker_api/0.0.1/MODULE.bazel": "02a13b77321773b2042e70ee5e4c5e099c8ddee4cf2da9cd420442c36938d4bd", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/bazel_worker_api/0.0.4/MODULE.bazel": "460aa12d01231a80cce03c548287b433b321d205b0028ae596728c35e5ee442e", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/bazel_worker_api/0.0.8/MODULE.bazel": "396c1ef53835aafe3d42ce6619080531ee770648303731f16cfaa33fa056bf0c", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/bazel_worker_api/0.0.8/source.json": "abaf8ac9d2ab2f47bda9af4c0c080ff7907378888e1f4bc62a0539dd13ba61e8", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/bazel_worker_java/0.0.4/MODULE.bazel": "82494a01018bb7ef06d4a17ec4cd7a758721f10eb8b6c820a818e70d669500db", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/bazel_worker_java/0.0.8/MODULE.bazel": "e76479eae70bd4e8f5f4c2dfc5d03ab971cfb18750246c7b3f3454c5c2ee6629", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/bazel_worker_java/0.0.8/source.json": "9395c4679444bc47bf7e51a710366a4480aa371c6f6bed01868e2fabcf11acec", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/boringssl/0.0.0-20211025-d4f1ab9/MODULE.bazel": "6ee6353f8b1a701fe2178e1d925034294971350b6d3ac37e67e5a7d463267834", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/boringssl/0.0.0-20230215-5c22014/MODULE.bazel": "4b03dc0d04375fa0271174badcd202ed249870c8e895b26664fd7298abea7282", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/boringssl/0.0.0-20240530-2db0eb3/MODULE.bazel": "d0405b762c5e87cd445b7015f2b8da5400ef9a8dbca0bfefa6c1cea79d528a97", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/boringssl/0.20240913.0/MODULE.bazel": "fcaa7503a5213290831a91ed1eb538551cf11ac0bc3a6ad92d0fef92c5bd25fb", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/boringssl/0.20241024.0/MODULE.bazel": "b540cff73d948cb79cb0bc108d7cef391d2098a25adabfda5043e4ef548dbc87", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/boringssl/0.20241024.0/source.json": "d843092e682b84188c043ac742965d7f96e04c846c7e338187e03238674909a9", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/buildifier_prebuilt/6.4.0/MODULE.bazel": "37389c6b5a40c59410b4226d3bb54b08637f393d66e2fa57925c6fcf68e64bf4", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/buildifier_prebuilt/8.5.1.2/MODULE.bazel": "9a6e0a2e87d1e3da679e157da5192ea351d5739ca1ff51831c2b736d5b6034de", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/buildifier_prebuilt/8.5.1.2/source.json": "33e11b3bf11e39cb762480a7e6ea1d24d044636135cdd8b8e74b07ebcd3b8d8b", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/buildozer/7.1.2/MODULE.bazel": "2e8dd40ede9c454042645fd8d8d0cd1527966aa5c919de86661e62953cd73d84", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/buildozer/7.1.2/source.json": "c9028a501d2db85793a6996205c8de120944f50a0d570438fcae0457a5f9d1f8", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/c-ares/1.15.0/MODULE.bazel": "ba0a78360fdc83f02f437a9e7df0532ad1fbaa59b722f6e715c11effebaa0166", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/c-ares/1.19.1/MODULE.bazel": "73bca21720772370ff91cc8e88bbbaf14897720c6473e87c1ddc0f848284c313", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/c-ares/1.34.5.bcr.1/MODULE.bazel": "f4632f68dbc075342966477d9c94a8a4a299d91e155980b042e1cd9f5a7ebcf5", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/c-ares/1.34.5.bcr.1/source.json": "55fae1e004176f6cb36efcc77de9894f799d985f482b8f82fec989d556745a84", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/cel-spec/0.15.0/MODULE.bazel": "e1eed53d233acbdcf024b4b0bc1528116d92c29713251b5154078ab1348cb600", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/cel-spec/0.24.0/MODULE.bazel": "e310c7aff8490ed689ccafd32729b77a660b9547f5a5ba9b20e967011c324b36", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/cel-spec/0.24.0/source.json": "522d08bc22524e07863276dd0f038f446a83166e91281dcfc07d5b8433c8d89e", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/civetweb/1.16/MODULE.bazel": "46a38f9daeb57392e3827fce7d40926be0c802bd23cdd6bfd3a96c804de42fae", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/civetweb/1.16/source.json": "ba8b9585adb8355cb51b999d57172fd05e7a762c56b8d4bac6db42c99de3beb7", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/curl/8.4.0/MODULE.bazel": "0bc250aa1cb69590049383df7a9537c809591fcf876c620f5f097c58fdc9bc10", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/curl/8.7.1/MODULE.bazel": "088221c35a2939c555e6e47cb31a81c15f8b59f4daa8009b1e9271a502d33485", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/curl/8.8.0/MODULE.bazel": "7da3b3e79b0b4ee8f8c95d640bc6ad7b430ce66ef6e9c9d2bc29b3b5ef85f6fe", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/curl/8.8.0/source.json": "d7d138b6878cf38891692fee0649ace35357fd549b425614d571786f054374d4", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/cython/3.0.11-1/MODULE.bazel": "868b3f5c956c3657420d2302004c6bb92606bfa47e314bab7f2ba0630c7c966c", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/cython/3.0.11-1/source.json": "da318be900b8ca9c3d1018839d3bebc5a8e1645620d0848fa2c696d4ecf7c296", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/envoy_api/0.0.0-20241214-918efc9/MODULE.bazel": "24e05f6f52f37be63a795192848555a2c8c855e7814dbc1ed419fb04a7005464", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/envoy_api/0.0.0-20250128-4de3c74/MODULE.bazel": "1fe72489212c530086e3ffb0e018b2bfef4663200ca03571570f9f006bef1d75", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/envoy_api/0.0.0-20251105-4a2b9a3/MODULE.bazel": "b66e87a0e0c2207f07e35c321388eb1feb036344565977444b52912c53a84466", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/envoy_api/0.0.0-20251105-4a2b9a3/source.json": "c4780edf780977f2ab7d00a189432c5b0b2fa08c6e4e2e09d2950499364a687d", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/gawk/5.3.2.bcr.1/MODULE.bazel": "cdf8cbe5ee750db04b78878c9633cc76e80dcf4416cbe982ac3a9222f80713c8", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/gawk/5.3.2.bcr.1/source.json": "fa7b512dfcb5eafd90ce3959cf42a2a6fe96144ebbb4b3b3928054895f2afac2", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/gazelle/0.27.0/MODULE.bazel": "3446abd608295de6d90b4a8a118ed64a9ce11dcb3dda2dc3290a22056bd20996", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/gazelle/0.30.0/MODULE.bazel": "f888a1effe338491f35f0e0e85003b47bb9d8295ccba73c37e07702d8d31c65b", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/gazelle/0.32.0/MODULE.bazel": "b499f58a5d0d3537f3cf5b76d8ada18242f64ec474d8391247438bf04f58c7b8", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/gazelle/0.33.0/MODULE.bazel": "a13a0f279b462b784fb8dd52a4074526c4a2afe70e114c7d09066097a46b3350", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/gazelle/0.34.0/MODULE.bazel": "abdd8ce4d70978933209db92e436deb3a8b737859e9354fb5fd11fb5c2004c8a", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/gazelle/0.36.0/MODULE.bazel": "e375d5d6e9a6ca59b0cb38b0540bc9a05b6aa926d322f2de268ad267a2ee74c0", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/gazelle/0.37.0/MODULE.bazel": "d1327ba0907d0275ed5103bfbbb13518f6c04955b402213319d0d6c0ce9839d4", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/gazelle/0.39.1/MODULE.bazel": "1fa3fefad240e535066fd0e6950dfccd627d36dc699ee0034645e51dbde3980f", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/gazelle/0.40.0/MODULE.bazel": "42ba5378ebe845fca43989a53186ab436d956db498acde790685fe0e8f9c6146", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/gazelle/0.46.0/MODULE.bazel": "3dec215dacf2427df87b524a2c99da387882a18d753f0b1b38675992bd0a99c6", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/gazelle/0.47.0/MODULE.bazel": "b61bb007c4efad134aa30ee7f4a8e2a39b22aa5685f005edaa022fbd1de43ebc", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/gazelle/0.47.0/source.json": "aeb2e5df14b7fb298625d75d08b9c65bdb0b56014c5eb89da9e5dd0572280ae6", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/google_benchmark/1.8.2/MODULE.bazel": "a70cf1bba851000ba93b58ae2f6d76490a9feb74192e57ab8e8ff13c34ec50cb", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/google_benchmark/1.8.4/MODULE.bazel": "c6d54a11dcf64ee63545f42561eda3fd94c1b5f5ebe1357011de63ae33739d5e", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/google_benchmark/1.8.5/MODULE.bazel": "9ba9b31b984022828a950e3300410977eda2e35df35584c6b0b2d0c2e52766b7", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/google_benchmark/1.9.4/MODULE.bazel": "3bab7c17c10580f87b647478a72a05621f88abc275afb97b578c828f56e59d45", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/google_benchmark/1.9.4/source.json": "8e0036f76a5c2aa9c16ca0da57d8065cff69edeed58f1f85584c588c0ef723a5", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/googleapis-cc/1.0.0/MODULE.bazel": "cf01757e7590c56140a4b81638ff2b3e7074769e6271720bbf738fcda25b6fc2", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/googleapis-cc/1.0.0/source.json": "ab0e3a2ee9968a8848f59872fbbfa3e1f768597d71d2229e6caa319d357967c7", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/googleapis-go/1.0.0/MODULE.bazel": "0a207a4c49da28c5cc1f7b3aeb23c2f7828c85c14aa8d9db0e30357a8d2250ed", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/googleapis-go/1.0.0/source.json": "ef189be4e7853e1ebc6123fe20b71822bf9896bd1f8eed8f68505c4585f72a48", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/googleapis-java/1.0.0/MODULE.bazel": "d633989337d069b5a95e6101777319681d7a4af4677e36801f11839d6512095c", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/googleapis-java/1.0.0/source.json": "ee59e2de37e4b531172870ac0296afa38f1ea004105ee21b2793c31a9d0ddccd", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/googleapis-rules-registry/1.0.0/MODULE.bazel": "97c6a4d413b373d4cc97065da3de1b2166e22cbbb5f4cc9f05760bfa83619e24", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/googleapis-rules-registry/1.0.0/source.json": "cf611c836a60e98e2e2ab2de8004f119e9f06878dcf4ea2d95a437b1b7a89fe9", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/googleapis/0.0.0-20240326-1c8d509c5/MODULE.bazel": "a4b7e46393c1cdcc5a00e6f85524467c48c565256b22b5fae20f84ab4a999a68", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/googleapis/0.0.0-20240819-fe8ba054a/MODULE.bazel": "117b7c7be7327ed5d6c482274533f2dbd78631313f607094d4625c28203cacdf", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/googleapis/0.0.0-20241220-5e258e33.bcr.1/MODULE.bazel": "ee6c30f82ecd476e61f019fb1151aaab380ea419958ff274ef2f0efca7969f5c", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/googleapis/0.0.0-20251003-2193a2bf/MODULE.bazel": "cc9e5ed294ed9ebf42cdbbdddd2df29048519e3797004df1e3f369f31ff4f2d4", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/googleapis/0.0.0-20251003-2193a2bf/source.json": "21558a194c519e27262cca9cf031bb166a666a8b7fb89993b700c828f8dc0857", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/googletest/1.11.0/MODULE.bazel": "3a83f095183f66345ca86aa13c58b59f9f94a2f81999c093d4eeaa2d262d12f4", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/googletest/1.14.0.bcr.1/MODULE.bazel": "22c31a561553727960057361aa33bf20fb2e98584bc4fec007906e27053f80c6", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/googletest/1.14.0/MODULE.bazel": "cfbcbf3e6eac06ef9d85900f64424708cc08687d1b527f0ef65aa7517af8118f", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/googletest/1.15.2/MODULE.bazel": "6de1edc1d26cafb0ea1a6ab3f4d4192d91a312fd2d360b63adaa213cd00b2108", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/googletest/1.16.0/MODULE.bazel": "a175623c69e94fca4ca7acbc12031e637b0c489318cd4805606981d4d7adb34a", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/googletest/1.17.0/MODULE.bazel": "dbec758171594a705933a29fcf69293d2468c49ec1f2ebca65c36f504d72df46", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/googletest/1.17.0/source.json": "38e4454b25fc30f15439c0378e57909ab1fd0a443158aa35aec685da727cd713", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/grpc-java/1.78.0/MODULE.bazel": "48f790fbb95625245295df1283e0dba344a4e30b4a9a9cefbabfa92bd84f3691", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/grpc-proto/0.0.0-20240627-ec30f58/MODULE.bazel": "88de79051e668a04726e9ea94a481ec6f1692086735fd6f488ab908b3b909238", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/grpc/1.41.0/MODULE.bazel": "5bcbfc2b274dabea628f0649dc50c90cf36543b1cfc31624832538644ad1aae8", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/grpc/1.56.3.bcr.1/MODULE.bazel": "cd5b1eb276b806ec5ab85032921f24acc51735a69ace781be586880af20ab33f", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/grpc/1.62.1/MODULE.bazel": "2998211594b8a79a6b459c4e797cfa19f0fb8b3be3149760ec7b8c99abfd426f", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/grpc/1.63.1.bcr.1/MODULE.bazel": "d7b9fef03bd175e6825237b521b18a3c29f1ac15f8aa52c8a1a0f3bd8f33d54b", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/grpc/1.66.0.bcr.2/MODULE.bazel": "0fa2b0fd028ce354febf0fe90f1ed8fecfbfc33118cddd95ac0418cc283333a0", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/grpc/1.66.0.bcr.3/MODULE.bazel": "f6047e89faf488f5e3e65cb2594c6f5e86992abec7487163ff6b623526e543b0", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/grpc/1.69.0/MODULE.bazel": "4e26e05c9e1ef291ccbc96aad8e457b1b8abedbc141623831629da2f8168eef6", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/grpc/1.71.0/MODULE.bazel": "7fcab2c05530373f1a442c362b17740dd0c75b6a2a975eec8f5bf4c70a37928a", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/grpc/1.74.1/MODULE.bazel": "09523be10ba2bfd999683671d0f8f22fb5b20ec77ad89b05ef58ff19a1b65c82", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/grpc/1.76.0/MODULE.bazel": "7373bd407fcb183b8bf379fa35dfe29feebdc6d8ec1bc40004cec6c7b2544be0", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/grpc/1.76.0/source.json": "5c1deef5b02cb7dd52df9cb628d956e02db3ec40bd165a465bc10b7bf47aa16a", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/jq.bzl/0.1.0/MODULE.bazel": "2ce69b1af49952cd4121a9c3055faa679e748ce774c7f1fda9657f936cae902f", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/jq.bzl/0.4.0/MODULE.bazel": "a7b39b37589f2b0dad53fd6c1ccaabbdb290330caa920d7ef3e6aad068cd4ab2", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/jq.bzl/0.4.0/source.json": "52ec7530c4618e03f634b30ff719814a68d7d39c235938b7aa2abbfe1eb1c52c", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/jsoncpp/1.9.5/MODULE.bazel": "31271aedc59e815656f5736f282bb7509a97c7ecb43e927ac1a37966e0578075", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/jsoncpp/1.9.6/MODULE.bazel": "2f8d20d3b7d54143213c4dfc3d98225c42de7d666011528dc8fe91591e2e17b0", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/jsoncpp/1.9.6/source.json": "a04756d367a2126c3541682864ecec52f92cdee80a35735a3cb249ce015ca000", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/libpfm/4.11.0.bcr.1/MODULE.bazel": "e5362dadc90aab6724c83a2cc1e67cbed9c89a05d97fb1f90053c8deb1e445c8", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/libpfm/4.11.0.bcr.1/source.json": "0646414d9037f8aad148781dd760bec90b0b25ac12fda5e03f8aadbd6b9c61e6", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/libpfm/4.11.0/MODULE.bazel": "45061ff025b301940f1e30d2c16bea596c25b176c8b6b3087e92615adbd52902", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/mbedtls/3.6.0/MODULE.bazel": "8e380e4698107c5f8766264d4df92e36766248447858db28187151d884995a09", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/mbedtls/3.6.0/source.json": "1dbe7eb5258050afcc3806b9d43050f71c6f539ce0175535c670df606790b30c", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/nlohmann_json/3.11.3/MODULE.bazel": "87023db2f55fc3a9949c7b08dc711fae4d4be339a80a99d04453c4bb3998eefc", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/nlohmann_json/3.11.3/source.json": "296c63a90c6813e53b3812d24245711981fc7e563d98fe15625f55181494488a", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/nlohmann_json/3.6.1/MODULE.bazel": "6f7b417dcc794d9add9e556673ad25cb3ba835224290f4f848f8e2db1e1fca74", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/opencensus-cpp/0.0.0-20230502-50eb5de.bcr.2/MODULE.bazel": "cc18734138dd18c912c6ce2a59186db28f85d8058c99c9f21b46ca3e0aba0ebe", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/opencensus-cpp/0.0.0-20230502-50eb5de.bcr.2/source.json": "7c135f9d42bb3b045669c3c6ab3bb3c208e00b46aca4422eea64c29811a5b240", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/opencensus-cpp/0.0.0-20230502-50eb5de/MODULE.bazel": "02201d2921dadb4ec90c4980eca4b2a02904eddcf6fa02f3da7594fb7b0d821c", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/opencensus-proto/0.4.1.bcr.2/MODULE.bazel": "789706a714855f92c5c8cfcf1ef32bbb64dcd3b7c9066756ad7986ec59709d29", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/opencensus-proto/0.4.1.bcr.2/source.json": "aadf3f53e08b72376506b7c4ea3d167010c9efb160d7d6e1e304ed646bac1b36", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/opencensus-proto/0.4.1/MODULE.bazel": "4a2e8b4d0b544002502474d611a5a183aa282251e14f6a01afe841c0c1b10372", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/openssl/3.3.1.bcr.1/MODULE.bazel": "49c0c07e8fb87b480bccb842cfee1b32617f11dac590f732573c69058699a3d1", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/openssl/3.3.1.bcr.1/source.json": "0c0872e048bbea052a9c541fb47019481a19201ba5555a71d762ad591bf94e1f", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/opentelemetry-cpp/1.14.2/MODULE.bazel": "089a5613c2a159c7dfde098dabfc61e966889c7d6a81a98422a84c51535ed17d", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/opentelemetry-cpp/1.16.0/MODULE.bazel": "b7379a140f538cea3f749179a2d481ed81942cc6f7b05a6113723eb34ac3b3e7", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/opentelemetry-cpp/1.19.0/MODULE.bazel": "3455326c08b28415648a3d60d8e3c811847ebdbe64474f75b25878f25585aea1", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/opentelemetry-cpp/1.19.0/source.json": "4e48137e4c3ecb99401ff99876df8fa330598d7da051869bec643446e8a8ff95", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/opentelemetry-proto/1.1.0/MODULE.bazel": "a49f406e99bf05ab43ed4f5b3322fbd33adfd484b6546948929d1316299b68bf", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/opentelemetry-proto/1.3.1/MODULE.bazel": "0141a50e989576ee064c11ce8dd5ec89993525bd9f9a09c5618e4dacc8df9352", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/opentelemetry-proto/1.4.0.bcr.1/MODULE.bazel": "5ceaf25e11170d22eded4c8032728b4a3f273765fccda32f9e94f463755c4167", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/opentelemetry-proto/1.5.0/MODULE.bazel": "7543d91a53b98e7b5b37c5a0865b93bff12c1ee022b1e322cd236b968894b030", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/opentelemetry-proto/1.5.0/source.json": "046b721ce203e88cdaad44d7dd17a86b7200eab9388b663b234e72e13ff7b143", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/opentracing-cpp/1.6.0/MODULE.bazel": "b3925269f63561b8b880ae7cf62ccf81f6ece55b62cd791eda9925147ae116ec", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/opentracing-cpp/1.6.0/source.json": "da1cb1add160f5e5074b7272e9db6fd8f1b3336c15032cd0a653af9d2f484aed", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/package_metadata/0.0.2/MODULE.bazel": "fb8d25550742674d63d7b250063d4580ca530499f045d70748b1b142081ebb92", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/package_metadata/0.0.3/MODULE.bazel": "77890552ecea9e284b5424c9de827a58099348763a4359e975c359a83d4faa83", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/package_metadata/0.0.5/MODULE.bazel": "ef4f9439e3270fdd6b9fd4dbc3d2f29d13888e44c529a1b243f7a31dfbc2e8e4", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/package_metadata/0.0.7/MODULE.bazel": "7adb03933fc8401f495800cf4eafcff0edc6da0ff55c7db223ef69d19f689486", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/package_metadata/0.0.7/source.json": "50639625e937b56115012674c797cca7a05a96b4878c87d803c13dc2b31de8a0", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/platforms/0.0.10/MODULE.bazel": "8cb8efaf200bdeb2150d93e162c40f388529a25852b332cec879373771e48ed5", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/platforms/0.0.11/MODULE.bazel": "0daefc49732e227caa8bfa834d65dc52e8cc18a2faf80df25e8caea151a9413f", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/platforms/0.0.4/MODULE.bazel": "9b328e31ee156f53f3c416a64f8491f7eb731742655a47c9eec4703a71644aee", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/platforms/0.0.5/MODULE.bazel": "5733b54ea419d5eaf7997054bb55f6a1d0b5ff8aedf0176fef9eea44f3acda37", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/platforms/0.0.6/MODULE.bazel": "ad6eeef431dc52aefd2d77ed20a4b353f8ebf0f4ecdd26a807d2da5aa8cd0615", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/platforms/0.0.7/MODULE.bazel": "72fd4a0ede9ee5c021f6a8dd92b503e089f46c227ba2813ff183b71616034814", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/platforms/0.0.8/MODULE.bazel": "9f142c03e348f6d263719f5074b21ef3adf0b139ee4c5133e2aa35664da9eb2d", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/platforms/0.0.9/MODULE.bazel": "4a87a60c927b56ddd67db50c89acaa62f4ce2a1d2149ccb63ffd871d5ce29ebc", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/platforms/1.0.0/MODULE.bazel": "f05feb42b48f1b3c225e4ccf351f367be0371411a803198ec34a389fb22aa580", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/platforms/1.1.0/MODULE.bazel": "1c0c09f5bdcf4b3f924720d2478a3711cb39f4977019ca5988685e5b7e18b3d2", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/platforms/1.1.0/source.json": "fcf351c47596c939140ab0d333dfdd08ed1ea6ce33c2fe70c12493a301cf1344", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/prometheus-cpp/1.2.4/MODULE.bazel": "0fbe5dcff66311947a3f6b86ebc6a6d9328e31a28413ca864debc4a043f371e5", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/prometheus-cpp/1.3.0.bcr.1/MODULE.bazel": "116ad46e97c1d2aeb020fe2899a342a7e703574ce7c0faf7e4810f938c974a9a", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/prometheus-cpp/1.3.0.bcr.1/source.json": "e813cce2d450708cfcb26e309c5172583a7440776edf354e83e6788c768e5cca", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/prometheus-cpp/1.3.0/MODULE.bazel": "ce82e086bbc0b60267e970f6a54b2ca6d0f22d3eb6633e00e2cc2899c700f3d8", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/protobuf/21.7/MODULE.bazel": "a5a29bb89544f9b97edce05642fac225a808b5b7be74038ea3640fae2f8e66a7", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/protobuf/23.1/MODULE.bazel": "88b393b3eb4101d18129e5db51847cd40a5517a53e81216144a8c32dfeeca52a", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/protobuf/24.4/MODULE.bazel": "7bc7ce5f2abf36b3b7b7c8218d3acdebb9426aeb35c2257c96445756f970eb12", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/protobuf/25.6/MODULE.bazel": "fc0ae073b47c7ede88b825ff79e64f1c058967c7a87a86cdf4abecd9e0516625", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/protobuf/26.0.bcr.1/MODULE.bazel": "8f04d38c2da40a3715ff6bdce4d32c5981e6432557571482d43a62c31a24c2cf", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/protobuf/26.0.bcr.2/MODULE.bazel": "62e0b84ca727bdeb55a6fe1ef180e6b191bbe548a58305ea1426c158067be534", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/protobuf/26.0/MODULE.bazel": "8402da964092af40097f4a205eec2a33fd4a7748dc43632b7d1629bfd9a2b856", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/protobuf/27.0-rc2/MODULE.bazel": "b2b0dbafd57b6bec0ca9b251da02e628c357dab53a097570aa7d79d020f107cf", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/protobuf/27.0/MODULE.bazel": "7873b60be88844a0a1d8f80b9d5d20cfbd8495a689b8763e76c6372998d3f64c", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/protobuf/27.1/MODULE.bazel": "703a7b614728bb06647f965264967a8ef1c39e09e8f167b3ca0bb1fd80449c0d", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/protobuf/27.2/MODULE.bazel": "32450b50673882e4c8c3d10a83f3bc82161b213ed2f80d17e38bece8f165c295", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/protobuf/29.0-rc2.bcr.1/MODULE.bazel": "52f4126f63a2f0bbf36b99c2a87648f08467a4eaf92ba726bc7d6a500bbf770c", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/protobuf/29.0-rc2/MODULE.bazel": "6241d35983510143049943fc0d57937937122baf1b287862f9dc8590fc4c37df", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/protobuf/29.0-rc3/MODULE.bazel": "33c2dfa286578573afc55a7acaea3cada4122b9631007c594bf0729f41c8de92", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/protobuf/29.0/MODULE.bazel": "319dc8bf4c679ff87e71b1ccfb5a6e90a6dbc4693501d471f48662ac46d04e4e", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/protobuf/29.1/MODULE.bazel": "557c3457560ff49e122ed76c0bc3397a64af9574691cb8201b4e46d4ab2ecb95", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/protobuf/3.19.0/MODULE.bazel": "6b5fbb433f760a99a22b18b6850ed5784ef0e9928a72668b66e4d7ccd47db9b0", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/protobuf/3.19.2/MODULE.bazel": "532ffe5f2186b69fdde039efe6df13ba726ff338c6bc82275ad433013fa10573", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/protobuf/3.19.6/MODULE.bazel": "9233edc5e1f2ee276a60de3eaa47ac4132302ef9643238f23128fea53ea12858", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/protobuf/30.0/MODULE.bazel": "0e736de5d52ad7824113f47e65256a26ee74b689ba859c5447a0663e5a075409", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/protobuf/31.1/MODULE.bazel": "379a389bb330b7b8c1cdf331cc90bf3e13de5614799b3b52cdb7c6f389f6b38e", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/protobuf/32.1/MODULE.bazel": "89cd2866a9cb07fee9ff74c41ceace11554f32e0d849de4e23ac55515cfada4d", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/protobuf/33.0/MODULE.bazel": "c5270efb4aad37a2f893536076518793f409ea7df07a06df995d848d1690f21c", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/protobuf/33.4/MODULE.bazel": "114775b816b38b6d0ca620450d6b02550c60ceedfdc8d9a229833b34a223dc42", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/protobuf/33.4/source.json": "555f8686b4c7d6b5ba731fbea13bf656b4bfd9a7ff629c1d9d3f6e1d6155de79", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/protoc-gen-validate/1.0.4.bcr.2/MODULE.bazel": "c4bd2c850211ff5b7dadf9d2d0496c1c922fdedc303c775b01dfd3b3efc907ed", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/protoc-gen-validate/1.0.4/MODULE.bazel": "b8913c154b16177990f6126d2d2477d187f9ddc568e95ee3e2d50fc65d2c494a", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/protoc-gen-validate/1.2.1.bcr.1/MODULE.bazel": "4bf09676b62fa587ae07e073420a76ec8766dcce7545e5f8c68cfa8e484b5120", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/protoc-gen-validate/1.2.1.bcr.2/MODULE.bazel": "3bd4b14a8e7c78dbef973280deabaa139db1fe350aa92da03730a31f59082068", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/protoc-gen-validate/1.2.1.bcr.2/source.json": "14c28a5527fcd699f5efbf83a046666efabed3384364bd48428de89dfdc8110e", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/protoc-gen-validate/1.2.1/MODULE.bazel": "52b51f50533ec4fbd5d613cd093773f979ac2e035d954e02ca11de383f502505", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/pybind11_bazel/2.11.1/MODULE.bazel": "88af1c246226d87e65be78ed49ecd1e6f5e98648558c14ce99176da041dc378e", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/pybind11_bazel/2.12.0/MODULE.bazel": "e6f4c20442eaa7c90d7190d8dc539d0ab422f95c65a57cc59562170c58ae3d34", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/pybind11_bazel/3.0.0/MODULE.bazel": "a2bfa6020ed603a00d944161c63173c7f109774e99bee0c2cd8dbf24159f8134", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/pybind11_bazel/3.0.0/source.json": "d8f5104d4c21d272bf327ebe44366fb0b4c036cdaa1f5cceb21a408ca4ef2ef8", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rapidjson/1.1.0.bcr.20241007/MODULE.bazel": "82fbcb2e42f9e0040e76ccc74c06c3e46dfd33c64ca359293f8b84df0e6dff4c", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rapidjson/1.1.0.bcr.20241007/source.json": "5c42389ad0e21fc06b95ad7c0b730008271624a2fa3292e0eab5f30e15adeee3", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/re2/2021-09-01/MODULE.bazel": "bcb6b96f3b071e6fe2d8bed9cc8ada137a105f9d2c5912e91d27528b3d123833", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/re2/2023-09-01/MODULE.bazel": "cb3d511531b16cfc78a225a9e2136007a48cf8a677e4264baeab57fe78a80206", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/re2/2024-05-01/MODULE.bazel": "55a3f059538f381107824e7d00df5df6d061ba1fb80e874e4909c0f0549e8f3e", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/re2/2024-07-02.bcr.1/MODULE.bazel": "b4963dda9b31080be1905ef085ecd7dd6cd47c05c79b9cdf83ade83ab2ab271a", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/re2/2024-07-02/MODULE.bazel": "0eadc4395959969297cbcf31a249ff457f2f1d456228c67719480205aa306daa", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/re2/2025-11-05.bcr.1/MODULE.bazel": "3d9d4995833fc0334fc5c88b56a05288dd25d651544cd7b2233bbd6357bbeba0", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/re2/2025-11-05.bcr.1/source.json": "7df1394aabda1c9bc188a302f5d54b1c657924edd04ebc57d2be29dbd7efd141", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_android/0.1.1/MODULE.bazel": "48809ab0091b07ad0182defb787c4c5328bd3a278938415c00a7b69b50c4d3a8", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_android/0.6.6/MODULE.bazel": "b0fb569752aab65ab1a9db0a8f6cfaf5aa1754965e17e95dcf0e4d88e192a68d", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_android/0.7.1/MODULE.bazel": "a806fc382a774252f228a40e3b11b9fcc6276f8778c7fb33e9f72937c6258363", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_android/0.7.1/source.json": "151440aed3f0f73a00d4ed5cec5d31f63a6fef9b95d8fab1eb1810150fa525f2", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_apple/3.13.0/MODULE.bazel": "b4559a2c6281ca3165275bb36c1f0ac74666632adc5bdb680e366de7ce845f43", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_apple/3.16.0/MODULE.bazel": "0d1caf0b8375942ce98ea944be754a18874041e4e0459401d925577624d3a54a", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_apple/3.5.1/MODULE.bazel": "3d1bbf65ad3692003d36d8a29eff54d4e5c1c5f4bfb60f79e28646a924d9101c", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_apple/4.1.0/MODULE.bazel": "76e10fd4a48038d3fc7c5dc6e63b7063bbf5304a2e3bd42edda6ec660eebea68", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_apple/4.1.0/source.json": "8ee81e1708756f81b343a5eb2b2f0b953f1d25c4ab3d4a68dc02754872e80715", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_buf/0.1.1/MODULE.bazel": "6189aec18a4f7caff599ad41b851ab7645d4f1e114aa6431acf9b0666eb92162", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_buf/0.5.2/MODULE.bazel": "5f2492d284ab9bedf2668178303abf5f3cd7d8cdf85d768951008e88456e9c6a", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_buf/0.5.2/source.json": "41876d4834c0832de4b393de6e55dfd1cb3b25d3109e4ba90eb7fb57c560e0d9", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_cc/0.0.1/MODULE.bazel": "cb2aa0747f84c6c3a78dad4e2049c154f08ab9d166b1273835a8174940365647", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_cc/0.0.10/MODULE.bazel": "ec1705118f7eaedd6e118508d3d26deba2a4e76476ada7e0e3965211be012002", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_cc/0.0.13/MODULE.bazel": "0e8529ed7b323dad0775ff924d2ae5af7640b23553dfcd4d34344c7e7a867191", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_cc/0.0.14/MODULE.bazel": "5e343a3aac88b8d7af3b1b6d2093b55c347b8eefc2e7d1442f7a02dc8fea48ac", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_cc/0.0.15/MODULE.bazel": "6704c35f7b4a72502ee81f61bf88706b54f06b3cbe5558ac17e2e14666cd5dcc", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_cc/0.0.16/MODULE.bazel": "7661303b8fc1b4d7f532e54e9d6565771fea666fbdf839e0a86affcd02defe87", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_cc/0.0.17/MODULE.bazel": "2ae1d8f4238ec67d7185d8861cb0a2cdf4bc608697c331b95bf990e69b62e64a", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_cc/0.0.2/MODULE.bazel": "6915987c90970493ab97393024c156ea8fb9f3bea953b2f3ec05c34f19b5695c", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_cc/0.0.5/MODULE.bazel": "be41f87587998fe8890cd82ea4e848ed8eb799e053c224f78f3ff7fe1a1d9b74", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_cc/0.0.6/MODULE.bazel": "abf360251023dfe3efcef65ab9d56beefa8394d4176dd29529750e1c57eaa33f", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_cc/0.0.8/MODULE.bazel": "964c85c82cfeb6f3855e6a07054fdb159aced38e99a5eecf7bce9d53990afa3e", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_cc/0.0.9/MODULE.bazel": "836e76439f354b89afe6a911a7adf59a6b2518fafb174483ad78a2a2fde7b1c5", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_cc/0.1.1/MODULE.bazel": "2f0222a6f229f0bf44cd711dc13c858dad98c62d52bd51d8fc3a764a83125513", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_cc/0.1.2/MODULE.bazel": "557ddc3a96858ec0d465a87c0a931054d7dcfd6583af2c7ed3baf494407fd8d0", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_cc/0.1.5/MODULE.bazel": "88dfc9361e8b5ae1008ac38f7cdfd45ad738e4fa676a3ad67d19204f045a1fd8", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_cc/0.2.0/MODULE.bazel": "b5c17f90458caae90d2ccd114c81970062946f49f355610ed89bebf954f5783c", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_cc/0.2.13/MODULE.bazel": "eecdd666eda6be16a8d9dc15e44b5c75133405e820f620a234acc4b1fdc5aa37", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_cc/0.2.14/MODULE.bazel": "353c99ed148887ee89c54a17d4100ae7e7e436593d104b668476019023b58df8", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_cc/0.2.16/MODULE.bazel": "9242fa89f950c6ef7702801ab53922e99c69b02310c39fb6e62b2bd30df2a1d4", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_cc/0.2.17/MODULE.bazel": "1849602c86cb60da8613d2de887f9566a6d354a6df6d7009f9d04a14402f9a84", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_cc/0.2.17/source.json": "3832f45d145354049137c0090df04629d9c2b5493dc5c2bf46f1834040133a07", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_cc/0.2.4/MODULE.bazel": "1ff1223dfd24f3ecf8f028446d4a27608aa43c3f41e346d22838a4223980b8cc", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_cc/0.2.8/MODULE.bazel": "f1df20f0bf22c28192a794f29b501ee2018fa37a3862a1a2132ae2940a23a642", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_cc/0.2.9/MODULE.bazel": "34263f1dca62ea664265438cef714d7db124c03e1ed55ebb4f1dc860164308d1", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_foreign_cc/0.10.1/MODULE.bazel": "b9527010e5fef060af92b6724edb3691970a5b1f76f74b21d39f7d433641be60", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_foreign_cc/0.15.1/MODULE.bazel": "c2c60d26c79fda484acb95cdbec46e89d6b28b4845cb277160ce1e0c8622bb88", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_foreign_cc/0.15.1/source.json": "a161811a63ba8a859086da3b7ff3ad04f2e9c255d7727b41087103fc0eb22f55", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_foreign_cc/0.9.0/MODULE.bazel": "c9e8c682bf75b0e7c704166d79b599f93b72cfca5ad7477df596947891feeef6", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_fuzzing/0.5.2/MODULE.bazel": "40c97d1144356f52905566c55811f13b299453a14ac7769dfba2ac38192337a8", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_go/0.33.0/MODULE.bazel": "a2b11b64cd24bf94f57454f53288a5dacfe6cb86453eee7761b7637728c1910c", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_go/0.38.1/MODULE.bazel": "fb8e73dd3b6fc4ff9d260ceacd830114891d49904f5bda1c16bc147bcc254f71", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_go/0.39.1/MODULE.bazel": "d34fb2a249403a5f4339c754f1e63dc9e5ad70b47c5e97faee1441fc6636cd61", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_go/0.41.0/MODULE.bazel": "55861d8e8bb0e62cbd2896f60ff303f62ffcb0eddb74ecb0e5c0cbe36fc292c8", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_go/0.42.0/MODULE.bazel": "8cfa875b9aa8c6fce2b2e5925e73c1388173ea3c32a0db4d2b4804b453c14270", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_go/0.45.1/MODULE.bazel": "6d7884f0edf890024eba8ab31a621faa98714df0ec9d512389519f0edff0281a", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_go/0.46.0/MODULE.bazel": "3477df8bdcc49e698b9d25f734c4f3a9f5931ff34ee48a2c662be168f5f2d3fd", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_go/0.48.0/MODULE.bazel": "d00ebcae0908ee3f5e6d53f68677a303d6d59a77beef879598700049c3980a03", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_go/0.50.1/MODULE.bazel": "b91a308dc5782bb0a8021ad4330c81fea5bda77f96b9e4c117b9b9c8f6665ee0", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_go/0.51.0-rc2/MODULE.bazel": "edfc3a9cea7bedb0eaaff37b0d7817c1a4bf72b3c615580b0ffcee6c52690fd4", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_go/0.53.0/MODULE.bazel": "a4ed760d3ac0dbc0d7b967631a9a3fd9100d28f7d9fcf214b4df87d4bfff5f9a", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_go/0.58.3/MODULE.bazel": "5582119a4a39558d8d1b1634bcae46043d4f43a31415e861c3551b2860040b5e", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_go/0.59.0/MODULE.bazel": "b7e43e7414a3139a7547d1b4909b29085fbe5182b6c58cbe1ed4c6272815aeae", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_go/0.60.0/MODULE.bazel": "4a57ff2ffc2a3570e3c5646575c5a4b07287e91bcdac5d1f72383d51502b48cb", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_go/0.60.0/source.json": "1e21368c5e0c3013a110bd79a8fcff8ca46b5bcb2b561713a7273cbfcff7c464", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_java/4.0.0/MODULE.bazel": "5a78a7ae82cd1a33cef56dc578c7d2a46ed0dca12643ee45edbb8417899e6f74", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_java/5.1.0/MODULE.bazel": "324b6478b0343a3ce7a9add8586ad75d24076d6d43d2f622990b9c1cfd8a1b15", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_java/5.3.5/MODULE.bazel": "a4ec4f2db570171e3e5eb753276ee4b389bae16b96207e9d3230895c99644b86", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_java/5.5.0/MODULE.bazel": "486ad1aa15cdc881af632b4b1448b0136c76025a1fe1ad1b65c5899376b83a50", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_java/6.0.0/MODULE.bazel": "8a43b7df601a7ec1af61d79345c17b31ea1fedc6711fd4abfd013ea612978e39", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_java/6.3.0/MODULE.bazel": "a97c7678c19f236a956ad260d59c86e10a463badb7eb2eda787490f4c969b963", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_java/6.3.1/MODULE.bazel": "5a3471c8b84d53d58d5f6e316313680d7dd2c70afac696dbe14b761b0b5c6a06", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_java/6.4.0/MODULE.bazel": "e986a9fe25aeaa84ac17ca093ef13a4637f6107375f64667a15999f77db6c8f6", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_java/6.5.2/MODULE.bazel": "1d440d262d0e08453fa0c4d8f699ba81609ed0e9a9a0f02cd10b3e7942e61e31", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_java/7.0.6/MODULE.bazel": "6ddb07d9857a1a3accc9f6d005f20c969c4659c7710e6269a51db3527e0ea969", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_java/7.1.0/MODULE.bazel": "30d9135a2b6561c761bd67bd4990da591e6bdc128790ce3e7afd6a3558b2fb64", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_java/7.10.0/MODULE.bazel": "530c3beb3067e870561739f1144329a21c851ff771cd752a49e06e3dc9c2e71a", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_java/7.12.2/MODULE.bazel": "579c505165ee757a4280ef83cda0150eea193eed3bef50b1004ba88b99da6de6", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_java/7.2.0/MODULE.bazel": "06c0334c9be61e6cef2c8c84a7800cef502063269a5af25ceb100b192453d4ab", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_java/7.3.2/MODULE.bazel": "50dece891cfdf1741ea230d001aa9c14398062f2b7c066470accace78e412bc2", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_java/7.4.0/MODULE.bazel": "a592852f8a3dd539e82ee6542013bf2cadfc4c6946be8941e189d224500a8934", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_java/7.6.1/MODULE.bazel": "2f14b7e8a1aa2f67ae92bc69d1ec0fa8d9f827c4e17ff5e5f02e91caa3b2d0fe", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_java/7.6.5/MODULE.bazel": "481164be5e02e4cab6e77a36927683263be56b7e36fef918b458d7a8a1ebadb1", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_java/8.13.0/MODULE.bazel": "0444ebf737d144cf2bb2ccb368e7f1cce735264285f2a3711785827c1686625e", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_java/8.14.0/MODULE.bazel": "717717ed40cc69994596a45aec6ea78135ea434b8402fb91b009b9151dd65615", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_java/8.16.1/MODULE.bazel": "0f20b1cecaa8e52f60a8f071e59a20b4e3b9a67f6c56c802ea256f6face692d3", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_java/8.3.2/MODULE.bazel": "7336d5511ad5af0b8615fdc7477535a2e4e723a357b6713af439fe8cf0195017", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_java/8.5.1/MODULE.bazel": "d8a9e38cc5228881f7055a6079f6f7821a073df3744d441978e7a43e20226939", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_java/8.6.0/MODULE.bazel": "9c064c434606d75a086f15ade5edb514308cccd1544c2b2a89bbac4310e41c71", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_java/8.6.1/MODULE.bazel": "f4808e2ab5b0197f094cabce9f4b006a27766beb6a9975931da07099560ca9c2", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_java/8.9.0/MODULE.bazel": "e17c876cb53dcd817b7b7f0d2985b710610169729e8c371b2221cacdcd3dce4a", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_java/9.3.0/MODULE.bazel": "f657c72d65ac449caae9abf2e68e66c0d36f9416848c4c4903d0b3234229e7f2", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_java/9.7.0/MODULE.bazel": "3ce6bd55fdd4fcb3323197736b7d976e0eedcc0eea78ca6186d20d314ba12e12", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_java/9.7.0/source.json": "23b356565156e0fbc71e5dad7cf8e1b951466b7ae3fd731cb1d2c95ca82b1d70", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_jvm_external/4.4.2/MODULE.bazel": "a56b85e418c83eb1839819f0b515c431010160383306d13ec21959ac412d2fe7", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_jvm_external/5.1/MODULE.bazel": "33f6f999e03183f7d088c9be518a63467dfd0be94a11d0055fe2d210f89aa909", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_jvm_external/5.2/MODULE.bazel": "d9351ba35217ad0de03816ef3ed63f89d411349353077348a45348b096615036", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_jvm_external/5.3/MODULE.bazel": "bf93870767689637164657731849fb887ad086739bd5d360d90007a581d5527d", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_jvm_external/6.0/MODULE.bazel": "37c93a5a78d32e895d52f86a8d0416176e915daabd029ccb5594db422e87c495", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_jvm_external/6.1/MODULE.bazel": "75b5fec090dbd46cf9b7d8ea08cf84a0472d92ba3585b476f44c326eda8059c4", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_jvm_external/6.10/MODULE.bazel": "33e636ca6bc9ee0fa090a38aa33c631ded2d8cf6fead4124181d1b35dc474f7c", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_jvm_external/6.10/source.json": "c191249787625db72616a3fb3cc2786ab57355a2e3b615402b8b3b66b0f995b7", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_jvm_external/6.2/MODULE.bazel": "36a6e52487a855f33cb960724eb56547fa87e2c98a0474c3acad94339d7f8e99", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_jvm_external/6.3/MODULE.bazel": "c998e060b85f71e00de5ec552019347c8bca255062c990ac02d051bb80a38df0", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_jvm_external/6.6/MODULE.bazel": "153042249c7060536dc95b6bb9f9bb8063b8a0b0cb7acdb381bddbc2374aed55", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_jvm_external/6.7/MODULE.bazel": "e717beabc4d091ecb2c803c2d341b88590e9116b8bf7947915eeb33aab4f96dd", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_jvm_external/6.9/MODULE.bazel": "07c5db05527db7744a54fcffd653e1550d40e0540207a7f7e6d0a4de5bef8274", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_kotlin/1.9.0/MODULE.bazel": "ef85697305025e5a61f395d4eaede272a5393cee479ace6686dba707de804d59", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_kotlin/1.9.5/MODULE.bazel": "043a16a572f610558ec2030db3ff0c9938574e7dd9f58bded1bb07c0192ef025", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_kotlin/1.9.6/MODULE.bazel": "d269a01a18ee74d0335450b10f62c9ed81f2321d7958a2934e44272fe82dcef3", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_kotlin/2.1.3/MODULE.bazel": "ce7def6d576aa8d3a9c6d10e13b4d157296229674371f67dbf788dae0afae3d5", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_kotlin/2.4.0/MODULE.bazel": "38dac18bb76c0a47ff60dfcd95c666985cbc46374f28ea4eeb868bdbc58c5bec", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_kotlin/2.4.0/source.json": "07b6a307448817c071c4ba90dcb03f801e008959f8dfd8b152a241cc0ee01a23", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_license/0.0.3/MODULE.bazel": "627e9ab0247f7d1e05736b59dbb1b6871373de5ad31c3011880b4133cafd4bd0", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_license/0.0.7/MODULE.bazel": "088fbeb0b6a419005b89cf93fe62d9517c0a2b8bb56af3244af65ecfe37e7d5d", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_license/0.0.8/MODULE.bazel": "5669c6fe49b5134dbf534db681ad3d67a2d49cfc197e4a95f1ca2fd7f3aebe96", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_license/1.0.0/MODULE.bazel": "a7fda60eefdf3d8c827262ba499957e4df06f659330bbe6cdbdb975b768bb65c", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_license/1.0.0/source.json": "a52c89e54cc311196e478f8382df91c15f7a2bfdf4c6cd0e2675cc2ff0b56efb", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_multirun/0.9.0/MODULE.bazel": "32d628ef586b5b23f67e55886b7bc38913ea4160420d66ae90521dda2ff37df0", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_multirun/0.9.0/source.json": "e882ba77962fa6c5fe68619e5c7d0374ec9a219fb8d03c42eadaf6d0243771bd", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_multitool/0.11.0/MODULE.bazel": "8d9dda78d2398e136300d3ef4fbcc89ede7c32c158d8c016fa7d032df41c4aaf", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_multitool/0.11.0/source.json": "0b86574a1eaff37c33aafaff095ea16d6ac846beb94ffc74c4fcf626f8f80681", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_nodejs/5.8.2/MODULE.bazel": "6bc03c8f37f69401b888023bf511cb6ee4781433b0cb56236b2e55a21e3a026a", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_nodejs/6.5.2/MODULE.bazel": "7f9ea68a0ce6d82905ce9f74e76ab8a8b4531ed4c747018c9d76424ad0b3370d", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_nodejs/6.7.3/MODULE.bazel": "c22a48b2a0dbf05a9dc5f83837bbc24c226c1f6e618de3c3a610044c9f336056", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_nodejs/6.7.3/source.json": "a3f966f4415a8a6545e560ee5449eac95cc633f96429d08e87c87775c72f5e09", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_perl/0.2.4/MODULE.bazel": "5f5af7be4bf5fb88d91af7469518f0fd2161718aefc606188f7cd51f436ca938", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_perl/0.2.4/source.json": "574317d6b3c7e4843fe611b76f15e62a1889949f5570702e1ee4ad335ea3c339", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_pkg/0.7.0/MODULE.bazel": "df99f03fc7934a4737122518bb87e667e62d780b610910f0447665a7e2be62dc", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_pkg/1.0.1/MODULE.bazel": "5b1df97dbc29623bccdf2b0dcd0f5cb08e2f2c9050aab1092fd39a41e82686ff", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_pkg/1.0.1/source.json": "bd82e5d7b9ce2d31e380dd9f50c111d678c3bdaca190cb76b0e1c71b05e1ba8a", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_proto/4.0.0/MODULE.bazel": "a7a7b6ce9bee418c1a760b3d84f83a299ad6952f9903c67f19e4edd964894e06", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_proto/5.3.0-21.7/MODULE.bazel": "e8dff86b0971688790ae75528fe1813f71809b5afd57facb44dad9e8eca631b7", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_proto/6.0.0-rc1/MODULE.bazel": "1e5b502e2e1a9e825eef74476a5a1ee524a92297085015a052510b09a1a09483", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_proto/6.0.0/MODULE.bazel": "b531d7f09f58dce456cd61b4579ce8c86b38544da75184eadaf0a7cb7966453f", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_proto/6.0.2/MODULE.bazel": "ce916b775a62b90b61888052a416ccdda405212b6aaeb39522f7dc53431a5e73", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_proto/7.0.2/MODULE.bazel": "bf81793bd6d2ad89a37a40693e56c61b0ee30f7a7fdbaf3eabbf5f39de47dea2", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_proto/7.1.0/MODULE.bazel": "002d62d9108f75bb807cd56245d45648f38275cb3a99dcd45dfb864c5d74cb96", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_proto/7.1.0/source.json": "39f89066c12c24097854e8f57ab8558929f9c8d474d34b2c00ac04630ad8940e", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_python/0.10.2/MODULE.bazel": "cc82bc96f2997baa545ab3ce73f196d040ffb8756fd2d66125a530031cd90e5f", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_python/0.20.0/MODULE.bazel": "bfe14d17f20e3fe900b9588f526f52c967a6f281e47a1d6b988679bd15082286", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_python/0.22.0/MODULE.bazel": "b8057bafa11a9e0f4b08fc3b7cd7bee0dcbccea209ac6fc9a3ff051cd03e19e9", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_python/0.22.1/MODULE.bazel": "26114f0c0b5e93018c0c066d6673f1a2c3737c7e90af95eff30cfee38d0bbac7", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_python/0.23.1/MODULE.bazel": "49ffccf0511cb8414de28321f5fcf2a31312b47c40cc21577144b7447f2bf300", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_python/0.25.0/MODULE.bazel": "72f1506841c920a1afec76975b35312410eea3aa7b63267436bfb1dd91d2d382", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_python/0.26.0/MODULE.bazel": "42cb98cd15954e83b96b540dcc6d5a618eb061f056147ac4ea46e687a066a7c7", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_python/0.27.1/MODULE.bazel": "65dc875cc1a06c30d5bbdba7ab021fd9e551a6579e408a3943a61303e2228a53", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_python/0.28.0/MODULE.bazel": "cba2573d870babc976664a912539b320cbaa7114cd3e8f053c720171cde331ed", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_python/0.29.0/MODULE.bazel": "2ac8cd70524b4b9ec49a0b8284c79e4cd86199296f82f6e0d5da3f783d660c82", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_python/0.31.0/MODULE.bazel": "93a43dc47ee570e6ec9f5779b2e64c1476a6ce921c48cc9a1678a91dd5f8fd58", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_python/0.32.2/MODULE.bazel": "01052470fc30b49de91fb8483d26bea6f664500cfad0b078d4605b03e3a83ed4", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_python/0.33.2/MODULE.bazel": "3e036c4ad8d804a4dad897d333d8dce200d943df4827cb849840055be8d2e937", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_python/0.35.0/MODULE.bazel": "c3657951764cdcdb5a7370d5e885fad5e8c1583320aad18d46f9f110d2c22755", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_python/0.37.1/MODULE.bazel": "3faeb2d9fa0a81f8980643ee33f212308f4d93eea4b9ce6f36d0b742e71e9500", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_python/0.37.2/MODULE.bazel": "b5ffde91410745750b6c13be1c5dc4555ef5bc50562af4a89fd77807fdde626a", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_python/0.4.0/MODULE.bazel": "9208ee05fd48bf09ac60ed269791cf17fb343db56c8226a720fbb1cdf467166c", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_python/0.40.0/MODULE.bazel": "9d1a3cd88ed7d8e39583d9ffe56ae8a244f67783ae89b60caafc9f5cf318ada7", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_python/1.0.0/MODULE.bazel": "898a3d999c22caa585eb062b600f88654bf92efb204fa346fb55f6f8edffca43", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_python/1.2.0/MODULE.bazel": "5aeeb48b2a6c19d668b48adf2b8a2b209a6310c230db0ce77450f148a89846e4", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_python/1.3.0/MODULE.bazel": "8361d57eafb67c09b75bf4bbe6be360e1b8f4f18118ab48037f2bd50aa2ccb13", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_python/1.4.1/MODULE.bazel": "8991ad45bdc25018301d6b7e1d3626afc3c8af8aaf4bc04f23d0b99c938b73a6", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_python/1.6.0/MODULE.bazel": "7e04ad8f8d5bea40451cf80b1bd8262552aa73f841415d20db96b7241bd027d8", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_python/1.6.3/MODULE.bazel": "a7b80c42cb3de5ee2a5fa1abc119684593704fcd2fec83165ebe615dec76574f", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_python/1.8.4/MODULE.bazel": "33e3971e66161a3e955f7a0d411a8d1f291c4ce4c561851512466f3c77ff8ece", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_python/1.8.4/source.json": "9fbc0e57bae52cddcc3831d668bce87a47e0c655104a85098d4459dd9a3b0a10", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_robolectric/4.14.1.2/MODULE.bazel": "d44fec647d0aeb67b9f3b980cf68ba634976f3ae7ccd6c07d790b59b87a4f251", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_robolectric/4.14.1.2/source.json": "37c10335f2361c337c5c1f34ed36d2da70534c23088062b33a8bdaab68aa9dea", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_rust/0.51.0/MODULE.bazel": "2b6d1617ac8503bfdcc0e4520c20539d4bba3a691100bee01afe193ceb0310f9", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_rust/0.67.0/MODULE.bazel": "87c3816c4321352dcfd9e9e26b58e84efc5b21351ae3ef8fb5d0d57bde7237f5", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_rust/0.73.0/MODULE.bazel": "25e3b077128612754c4add1b4c90d20a6be06566b623dee6e32038d0e8f93062", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_rust/0.73.0/source.json": "8eeb3d9ba7c57916b63887a651e8f84c2f68b7243af9e712d728c2a0b7882255", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_shell/0.1.2/MODULE.bazel": "66e4ca3ce084b04af0b9ff05ff14cab4e5df7503973818bb91cbc6cda08d32fc", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_shell/0.2.0/MODULE.bazel": "fda8a652ab3c7d8fee214de05e7a9916d8b28082234e8d2c0094505c5268ed3c", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_shell/0.3.0/MODULE.bazel": "de4402cd12f4cc8fda2354fce179fdb068c0b9ca1ec2d2b17b3e21b24c1a937b", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_shell/0.4.1/MODULE.bazel": "00e501db01bbf4e3e1dd1595959092c2fadf2087b2852d3f553b5370f5633592", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_shell/0.5.0/MODULE.bazel": "8c8447370594d45539f66858b602b0bb2cb2d3401a4ebb9ad25830c59c0f366d", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_shell/0.6.1/MODULE.bazel": "72e76b0eea4e81611ef5452aa82b3da34caca0c8b7b5c0c9584338aa93bae26b", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_shell/0.6.1/source.json": "20ec05cd5e592055e214b2da8ccb283c7f2a421ea0dc2acbf1aa792e11c03d0c", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_swift/1.16.0/MODULE.bazel": "4a09f199545a60d09895e8281362b1ff3bb08bbde69c6fc87aff5b92fcc916ca", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_swift/1.18.0/MODULE.bazel": "a6aba73625d0dc64c7b4a1e831549b6e375fbddb9d2dde9d80c9de6ec45b24c9", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_swift/2.1.1/MODULE.bazel": "494900a80f944fc7aa61500c2073d9729dff0b764f0e89b824eb746959bc1046", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_swift/2.4.0/MODULE.bazel": "1639617eb1ede28d774d967a738b4a68b0accb40650beadb57c21846beab5efd", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_swift/3.1.2/MODULE.bazel": "72c8f5cf9d26427cee6c76c8e3853eb46ce6b0412a081b2b6db6e8ad56267400", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/rules_swift/3.1.2/source.json": "e85761f3098a6faf40b8187695e3de6d97944e98abd0d8ce579cb2daf6319a66", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/stardoc/0.5.0/MODULE.bazel": "f9f1f46ba8d9c3362648eea571c6f9100680efc44913618811b58cc9c02cd678", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/stardoc/0.5.1/MODULE.bazel": "1a05d92974d0c122f5ccf09291442580317cdd859f07a8655f1db9a60374f9f8", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/stardoc/0.5.3/MODULE.bazel": "c7f6948dae6999bf0db32c1858ae345f112cacf98f174c7a8bb707e41b974f1c", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/stardoc/0.5.4/MODULE.bazel": "6569966df04610b8520957cb8e97cf2e9faac2c0309657c537ab51c16c18a2a4", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/stardoc/0.5.6/MODULE.bazel": "c43dabc564990eeab55e25ed61c07a1aadafe9ece96a4efabb3f8bf9063b71ef", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/stardoc/0.6.2/MODULE.bazel": "7060193196395f5dd668eda046ccbeacebfd98efc77fed418dbe2b82ffaa39fd", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/stardoc/0.7.0/MODULE.bazel": "05e3d6d30c099b6770e97da986c53bd31844d7f13d41412480ea265ac9e8079c", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/stardoc/0.7.1/MODULE.bazel": "3548faea4ee5dda5580f9af150e79d0f6aea934fc60c1cc50f4efdd9420759e7", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/stardoc/0.7.2/MODULE.bazel": "fc152419aa2ea0f51c29583fab1e8c99ddefd5b3778421845606ee628629e0e5", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/stardoc/0.7.2/source.json": "58b029e5e901d6802967754adf0a9056747e8176f017cfe3607c0851f4d42216", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/swift_argument_parser/1.3.1.1/MODULE.bazel": "5e463fbfba7b1701d957555ed45097d7f984211330106ccd1352c6e0af0dcf91", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/swift_argument_parser/1.3.1.2/MODULE.bazel": "75aab2373a4bbe2a1260b9bf2a1ebbdbf872d3bd36f80bff058dccd82e89422f", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/swift_argument_parser/1.3.1.2/source.json": "5fba48bbe0ba48761f9e9f75f92876cafb5d07c0ce059cc7a8027416de94a05b", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/tar.bzl/0.2.1/MODULE.bazel": "52d1c00a80a8cc67acbd01649e83d8dd6a9dc426a6c0b754a04fe8c219c76468", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/tar.bzl/0.5.1/MODULE.bazel": "7c2eb3dcfc53b0f3d6f9acdfd911ca803eaf92aadf54f8ca6e4c1f3aee288351", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/tar.bzl/0.5.1/source.json": "deed3094f7cc779ed1d37a68403847b0e38d9dd9d931e03cb90825f3368b515f", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/upb/0.0.0-20211020-160625a/MODULE.bazel": "6cced416be2dc5b9c05efd5b997049ba795e5e4e6fafbe1624f4587767638928", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/upb/0.0.0-20220923-a547704/MODULE.bazel": "7298990c00040a0e2f121f6c32544bab27d4452f80d9ce51349b1a28f3005c43", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/upb/0.0.0-20230516-61a97ef/MODULE.bazel": "c0df5e35ad55e264160417fd0875932ee3c9dda63d9fccace35ac62f45e1b6f9", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/upb/0.0.0-20230907-e7430e6/MODULE.bazel": "3a7dedadf70346e678dc059dbe44d05cbf3ab17f1ce43a1c7a42edc7cbf93fd9", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/xds/0.0.0-20240423-555b57e/MODULE.bazel": "cea509976a77e34131411684ef05a1d6ad194dd71a8d5816643bc5b0af16dc0f", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/xds/0.0.0-20240423-555b57e/source.json": "7227e1fcad55f3f3cab1a08691ecd753cb29cc6380a47bc650851be9f9ad6d20", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/yq.bzl/0.1.1/MODULE.bazel": "9039681f9bcb8958ee2c87ffc74bdafba9f4369096a2b5634b88abc0eaefa072", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/yq.bzl/0.1.1/source.json": "2d2bad780a9f2b9195a4a370314d2c17ae95eaa745cefc2e12fbc49759b15aa3", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/zlib/1.2.11/MODULE.bazel": "07b389abc85fdbca459b69e2ec656ae5622873af3f845e1c9d80fe179f3effa0", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/zlib/1.2.12/MODULE.bazel": "3b1a8834ada2a883674be8cbd36ede1b6ec481477ada359cd2d3ddc562340b27", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/zlib/1.2.13/MODULE.bazel": "aa6deb1b83c18ffecd940c4119aff9567cd0a671d7bba756741cb2ef043a29d5", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/zlib/1.3.1.bcr.1/MODULE.bazel": "6a9fe6e3fc865715a7be9823ce694ceb01e364c35f7a846bf0d2b34762bc066b", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/zlib/1.3.1.bcr.3/MODULE.bazel": "af322bc08976524477c79d1e45e241b6efbeb918c497e8840b8ab116802dda79", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/zlib/1.3.1.bcr.5/MODULE.bazel": "eec517b5bbe5492629466e11dae908d043364302283de25581e3eb944326c4ca", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/zlib/1.3.1.bcr.5/source.json": "22bc55c47af97246cfc093d0acf683a7869377de362b5d1c552c2c2e16b7a806", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/zlib/1.3.1/MODULE.bazel": "751c9940dcfe869f5f7274e1295422a34623555916eb98c174c1e945594bf198", + "https://raw.githubusercontent.com/bazelbuild/bazel-central-registry/main/modules/zlib/1.3/MODULE.bazel": "6a9c02f19a24dcedb05572b2381446e27c272cd383aed11d41d99da9e3167a72" }, "selectedYankedVersions": {}, "moduleExtensions": { @@ -835,7 +829,7 @@ }, "@@rules_multitool+//multitool:extension.bzl%multitool": { "general": { - "bzlTransitiveDigest": "rZ7UYa3R4hSTXeQ0h2U9Pb2jcDZIGdfoDP/ltWyT3rw=", + "bzlTransitiveDigest": "IMQwskK+re+OhchtA5adncoLzyqeI1JZX+SguN59J44=", "usagesDigest": "fstLhGUWwSODzCGlnJTYvDr08acGB/scZuVFr6P2Zq0=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, diff --git a/README.md b/README.md index 27a07812..628b4a00 100644 --- a/README.md +++ b/README.md @@ -912,7 +912,7 @@ make coverage ``` This invokes -`bazel coverage --combined_report=lcov //cli/... //tools:coverage_check_test //tools/go/...` +`bazel coverage --combined_report=lcov //cli/... //tools:coverage_check_test //tools/coverage/... //tools/go/...` and then runs `//tools:coverage-check` twice against the resulting LCOV report — once for the Kotlin main sources and once scoped to `tools/go/` (`--include tools/go/`). The check is a Python `py_binary` ([`tools/coverage_check.py`](tools/coverage_check.py)) that prints a @@ -925,6 +925,34 @@ If you've already produced a coverage report and just want to re-check the thres The enforcement logic itself is tested under `//tools:coverage_check_test` — run it directly with `make coverage-test` (or `bazel test //tools:coverage_check_test`). +### Per-target coverage minimums + +In addition to the repo-wide gate above, individual test targets declare their own +line-coverage minimums, enforced *during* the coverage run itself by a Rust LCOV +merger ([`tools/coverage/`](tools/coverage/)) that replaces Bazel's built-in one +(`coverage --coverage_output_generator=//tools/coverage:lcov_merger` in `.bazelrc`). +Bazel only invokes the merger for `bazel coverage`, so plain `bazel test` runs are +unaffected. A target opts in through its `env` attribute via +`//tools/coverage:defs.bzl`: + +```starlark +load("//tools/coverage:defs.bzl", "coverage_enforced_test") + +coverage_enforced_test( + rule = go_test, # any test rule with the standard `env` attribute + name = "sample_test", + min_line_coverage = 90, + coverage_include = ["tools/go/"], + ... +) +``` + +Go (`//tools/go/sample:sample_test`), Rust (`//tools/coverage:lcov_merger_test`) +and the Kotlin/JVM tests under `//cli` all carry such minimums. When a target's +merged report falls below its minimum, the coverage run fails that target and the +test log contains a per-file breakdown. See +[`tools/coverage/README.md`](tools/coverage/README.md) for details. + For an interactive HTML report (annotated source with covered/uncovered lines highlighted), use `make coverage-html`. This requires the `lcov` package (`brew install lcov` on macOS, `apt-get install lcov` on Debian/Ubuntu) and writes diff --git a/cli/BUILD b/cli/BUILD index fe4dab0b..e3381810 100644 --- a/cli/BUILD +++ b/cli/BUILD @@ -1,5 +1,6 @@ load("@rules_java//java:defs.bzl", "java_binary") load("@rules_kotlin//kotlin:jvm.bzl", "kt_jvm_library", "kt_jvm_test") +load("//tools/coverage:defs.bzl", "coverage_minimum_env") config_setting( name = "enable_debug", @@ -51,8 +52,20 @@ kt_jvm_library( ], ) +# Every kt_jvm_test declares a per-target line-coverage minimum, enforced by +# //tools/coverage:lcov_merger during `bazel coverage` only (plain `bazel test` +# is unaffected). Jacoco instruments the whole of :cli-lib for every test, so +# each check is scoped (coverage_include) to the source file(s) the test is +# responsible for covering. Floors are set from measured coverage minus ~10 +# points of headroom (rounded down to 5, capped at 90) to absorb +# platform/Bazel-version variance while still catching real regressions — +# ratchet them upward as coverage improves. kt_jvm_test( name = "BuildGraphHasherTest", + env = coverage_minimum_env( + 70, + ["cli/src/main/kotlin/com/bazel_diff/hash/BuildGraphHasher.kt"], + ), jvm_flags = [ "-Dnet.bytebuddy.experimental=true", ], @@ -62,12 +75,20 @@ kt_jvm_test( kt_jvm_test( name = "TargetHashTest", + env = coverage_minimum_env( + 90, + ["cli/src/main/kotlin/com/bazel_diff/hash/TargetHash.kt"], + ), test_class = "com.bazel_diff.hash.TargetHashTest", runtime_deps = [":cli-test-lib"], ) kt_jvm_test( name = "RuleHasherAlwaysAffectedTagsTest", + env = coverage_minimum_env( + 20, + ["cli/src/main/kotlin/com/bazel_diff/hash/RuleHasher.kt"], + ), test_class = "com.bazel_diff.hash.RuleHasherAlwaysAffectedTagsTest", runtime_deps = [":cli-test-lib"], ) @@ -77,144 +98,240 @@ kt_jvm_test( data = [ ":src/test/kotlin/com/bazel_diff/hash/fixture/foo.ts", ], + env = coverage_minimum_env( + 70, + ["cli/src/main/kotlin/com/bazel_diff/hash/SourceFileHasher.kt"], + ), test_class = "com.bazel_diff.hash.SourceFileHasherTest", runtime_deps = [":cli-test-lib"], ) kt_jvm_test( name = "CalculateImpactedTargetsInteractorTest", + env = coverage_minimum_env( + 75, + ["cli/src/main/kotlin/com/bazel_diff/interactor/CalculateImpactedTargetsInteractor.kt"], + ), test_class = "com.bazel_diff.interactor.CalculateImpactedTargetsInteractorTest", runtime_deps = [":cli-test-lib"], ) kt_jvm_test( name = "CalculateImpactedTargetsInteractorIssue335Test", + env = coverage_minimum_env( + 25, + ["cli/src/main/kotlin/com/bazel_diff/interactor/CalculateImpactedTargetsInteractor.kt"], + ), test_class = "com.bazel_diff.interactor.CalculateImpactedTargetsInteractorIssue335Test", runtime_deps = [":cli-test-lib"], ) kt_jvm_test( name = "CalculateImpactedTargetsInteractorModuleQueryTest", + env = coverage_minimum_env( + 50, + ["cli/src/main/kotlin/com/bazel_diff/interactor/CalculateImpactedTargetsInteractor.kt"], + ), test_class = "com.bazel_diff.interactor.CalculateImpactedTargetsInteractorModuleQueryTest", runtime_deps = [":cli-test-lib"], ) kt_jvm_test( name = "NormalisingPathConverterTest", + env = coverage_minimum_env( + 90, + ["cli/src/main/kotlin/com/bazel_diff/cli/converter/NormalisingPathConverter.kt"], + ), test_class = "com.bazel_diff.cli.converter.NormalisingPathConverterTest", runtime_deps = [":cli-test-lib"], ) kt_jvm_test( name = "OptionsConverterTest", + env = coverage_minimum_env( + 90, + ["cli/src/main/kotlin/com/bazel_diff/cli/converter/OptionsConverter.kt"], + ), test_class = "com.bazel_diff.cli.converter.OptionsConverterTest", runtime_deps = [":cli-test-lib"], ) kt_jvm_test( name = "DurationConverterTest", + env = coverage_minimum_env( + 80, + ["cli/src/main/kotlin/com/bazel_diff/cli/converter/DurationConverter.kt"], + ), test_class = "com.bazel_diff.cli.converter.DurationConverterTest", runtime_deps = [":cli-test-lib"], ) kt_jvm_test( name = "ByteSizeConverterTest", + env = coverage_minimum_env( + 90, + ["cli/src/main/kotlin/com/bazel_diff/cli/converter/ByteSizeConverter.kt"], + ), test_class = "com.bazel_diff.cli.converter.ByteSizeConverterTest", runtime_deps = [":cli-test-lib"], ) kt_jvm_test( name = "DeserialiseHashesInteractorTest", + env = coverage_minimum_env( + 75, + ["cli/src/main/kotlin/com/bazel_diff/interactor/DeserialiseHashesInteractor.kt"], + ), test_class = "com.bazel_diff.interactor.DeserialiseHashesInteractorTest", runtime_deps = [":cli-test-lib"], ) kt_jvm_test( name = "FingerprintInteractorTest", + env = coverage_minimum_env( + 90, + ["cli/src/main/kotlin/com/bazel_diff/interactor/FingerprintInteractor.kt"], + ), test_class = "com.bazel_diff.interactor.FingerprintInteractorTest", runtime_deps = [":cli-test-lib"], ) kt_jvm_test( name = "FingerprintGathererTest", + env = coverage_minimum_env( + 80, + ["cli/src/main/kotlin/com/bazel_diff/cli/FingerprintGatherer.kt"], + ), test_class = "com.bazel_diff.cli.FingerprintGathererTest", runtime_deps = [":cli-test-lib"], ) kt_jvm_test( name = "FingerprintCommandTest", + env = coverage_minimum_env( + 75, + ["cli/src/main/kotlin/com/bazel_diff/cli/FingerprintCommand.kt"], + ), test_class = "com.bazel_diff.cli.FingerprintCommandTest", runtime_deps = [":cli-test-lib"], ) kt_jvm_test( name = "WarmupCommandTest", + env = coverage_minimum_env( + 65, + ["cli/src/main/kotlin/com/bazel_diff/cli/WarmupCommand.kt"], + ), test_class = "com.bazel_diff.cli.WarmupCommandTest", runtime_deps = [":cli-test-lib"], ) kt_jvm_test( name = "BazelRuleTest", + env = coverage_minimum_env( + 65, + ["cli/src/main/kotlin/com/bazel_diff/bazel/BazelRule.kt"], + ), test_class = "com.bazel_diff.bazel.BazelRuleTest", runtime_deps = [":cli-test-lib"], ) kt_jvm_test( name = "BazelClientTest", + env = coverage_minimum_env( + 80, + ["cli/src/main/kotlin/com/bazel_diff/bazel/BazelClient.kt"], + ), test_class = "com.bazel_diff.bazel.BazelClientTest", runtime_deps = [":cli-test-lib"], ) kt_jvm_test( name = "BazelTargetTest", + env = coverage_minimum_env( + 85, + ["cli/src/main/kotlin/com/bazel_diff/bazel/BazelTarget.kt"], + ), test_class = "com.bazel_diff.bazel.BazelTargetTest", runtime_deps = [":cli-test-lib"], ) kt_jvm_test( name = "BazelTargetTypeTest", + env = coverage_minimum_env( + 90, + ["cli/src/main/kotlin/com/bazel_diff/bazel/BazelTargetType.kt"], + ), test_class = "com.bazel_diff.bazel.BazelTargetTypeTest", runtime_deps = [":cli-test-lib"], ) kt_jvm_test( name = "BazelDiffTest", + env = coverage_minimum_env( + 90, + ["cli/src/main/kotlin/com/bazel_diff/cli/BazelDiff.kt"], + ), test_class = "com.bazel_diff.cli.BazelDiffTest", runtime_deps = [":cli-test-lib"], ) kt_jvm_test( name = "VersionProviderTest", + env = coverage_minimum_env( + 55, + ["cli/src/main/kotlin/com/bazel_diff/cli/VersionProvider.kt"], + ), test_class = "com.bazel_diff.cli.VersionProviderTest", runtime_deps = [":cli-test-lib"], ) kt_jvm_test( name = "StderrLoggerTest", + env = coverage_minimum_env( + 90, + ["cli/src/main/kotlin/com/bazel_diff/log/StderrLogger.kt"], + ), test_class = "com.bazel_diff.log.StderrLoggerTest", runtime_deps = [":cli-test-lib"], ) kt_jvm_test( name = "BazelModServiceTest", + env = coverage_minimum_env( + 20, + ["cli/src/main/kotlin/com/bazel_diff/bazel/BazelModService.kt"], + ), test_class = "com.bazel_diff.bazel.BazelModServiceTest", runtime_deps = [":cli-test-lib"], ) kt_jvm_test( name = "ModuleGraphParserTest", + env = coverage_minimum_env( + 65, + ["cli/src/main/kotlin/com/bazel_diff/bazel/ModuleGraphParser.kt"], + ), test_class = "com.bazel_diff.bazel.ModuleGraphParserTest", runtime_deps = [":cli-test-lib"], ) kt_jvm_test( name = "StderrPollutionRegressionTest", + env = coverage_minimum_env( + 15, + ["cli/src/main/kotlin/com/bazel_diff/bazel/ModuleGraphParser.kt"], + ), test_class = "com.bazel_diff.bazel.StderrPollutionRegressionTest", runtime_deps = [":cli-test-lib"], ) kt_jvm_test( name = "ProcessStdinHangRegressionTest", + env = coverage_minimum_env( + 60, + ["cli/src/main/kotlin/com/bazel_diff/process/"], + ), test_class = "com.bazel_diff.process.ProcessStdinHangRegressionTest", runtime_deps = [":cli-test-lib"], ) @@ -225,6 +342,15 @@ kt_jvm_test( # right at the "long" ceiling, so slow-runner jitter was timing it out. timeout = "eternal", data = [":workspaces"], + # Unlike the unit tests above (floors set from measurement), this is a + # conservative smoke floor over the whole main tree: the E2E suite drives + # the real binary through the full hashing/impacted-targets pipeline, so + # well under half of all main-source lines executing means the suite is + # broken. Measure and ratchet once a per-target baseline exists in CI. + env = coverage_minimum_env( + 30, + ["cli/src/main/kotlin/com/bazel_diff/"], + ), test_class = "com.bazel_diff.e2e.E2ETest", runtime_deps = [":cli-test-lib"], ) @@ -235,6 +361,10 @@ kt_jvm_test( ":src/test/kotlin/com/bazel_diff/io/fixture/correct.json", ":src/test/kotlin/com/bazel_diff/io/fixture/wrong.json", ], + env = coverage_minimum_env( + 90, + ["cli/src/main/kotlin/com/bazel_diff/io/ContentHashProvider.kt"], + ), test_class = "com.bazel_diff.io.ContentHashProviderTest", runtime_deps = [ ":cli-test-lib", @@ -243,18 +373,30 @@ kt_jvm_test( kt_jvm_test( name = "ServeCommandTest", + env = coverage_minimum_env( + 60, + ["cli/src/main/kotlin/com/bazel_diff/cli/ServeCommand.kt"], + ), test_class = "com.bazel_diff.cli.ServeCommandTest", runtime_deps = [":cli-test-lib"], ) kt_jvm_test( name = "LocalDiskHashCacheStorageTest", + env = coverage_minimum_env( + 75, + ["cli/src/main/kotlin/com/bazel_diff/server/HashCacheStorage.kt"], + ), test_class = "com.bazel_diff.server.LocalDiskHashCacheStorageTest", runtime_deps = [":cli-test-lib"], ) kt_jvm_test( name = "S3HashCacheStorageTest", + env = coverage_minimum_env( + 80, + ["cli/src/main/kotlin/com/bazel_diff/server/S3HashCacheStorage.kt"], + ), jvm_flags = [ "-Dnet.bytebuddy.experimental=true", ], @@ -264,30 +406,50 @@ kt_jvm_test( kt_jvm_test( name = "TieredHashCacheStorageTest", + env = coverage_minimum_env( + 90, + ["cli/src/main/kotlin/com/bazel_diff/server/TieredHashCacheStorage.kt"], + ), test_class = "com.bazel_diff.server.TieredHashCacheStorageTest", runtime_deps = [":cli-test-lib"], ) kt_jvm_test( name = "CachePrunerTest", + env = coverage_minimum_env( + 80, + ["cli/src/main/kotlin/com/bazel_diff/server/CachePruner.kt"], + ), test_class = "com.bazel_diff.server.CachePrunerTest", runtime_deps = [":cli-test-lib"], ) kt_jvm_test( name = "MetricsServiceTest", + env = coverage_minimum_env( + 80, + ["cli/src/main/kotlin/com/bazel_diff/server/MetricsService.kt"], + ), test_class = "com.bazel_diff.server.MetricsServiceTest", runtime_deps = [":cli-test-lib"], ) kt_jvm_test( name = "GitClientTest", + env = coverage_minimum_env( + 75, + ["cli/src/main/kotlin/com/bazel_diff/server/GitClient.kt"], + ), test_class = "com.bazel_diff.server.GitClientTest", runtime_deps = [":cli-test-lib"], ) kt_jvm_test( name = "HashServiceTest", + env = coverage_minimum_env( + 75, + ["cli/src/main/kotlin/com/bazel_diff/server/HashService.kt"], + ), jvm_flags = [ "-Dnet.bytebuddy.experimental=true", ], @@ -297,6 +459,10 @@ kt_jvm_test( kt_jvm_test( name = "ImpactedTargetsServiceTest", + env = coverage_minimum_env( + 85, + ["cli/src/main/kotlin/com/bazel_diff/server/ImpactedTargetsService.kt"], + ), jvm_flags = [ "-Dnet.bytebuddy.experimental=true", ], @@ -306,12 +472,20 @@ kt_jvm_test( kt_jvm_test( name = "BazelDiffServerTest", + env = coverage_minimum_env( + 85, + ["cli/src/main/kotlin/com/bazel_diff/server/BazelDiffServer.kt"], + ), test_class = "com.bazel_diff.server.BazelDiffServerTest", runtime_deps = [":cli-test-lib"], ) kt_jvm_test( name = "QueryProfilerTest", + env = coverage_minimum_env( + 85, + ["cli/src/main/kotlin/com/bazel_diff/server/QueryProfiler.kt"], + ), test_class = "com.bazel_diff.server.QueryProfilerTest", runtime_deps = [":cli-test-lib"], ) diff --git a/tools/coverage/BUILD b/tools/coverage/BUILD index 46f332d1..6b7566b3 100644 --- a/tools/coverage/BUILD +++ b/tools/coverage/BUILD @@ -30,9 +30,9 @@ rust_binary( # The merger's own tests carry a coverage minimum, enforced — pleasingly — # by the merger itself when this package runs under `bazel coverage`. coverage_enforced_test( + name = "lcov_merger_test", coverage_include = ["tools/coverage/src/"], crate = ":lcov_merger_lib", min_line_coverage = 90, - name = "lcov_merger_test", rule = rust_test, ) diff --git a/tools/coverage/README.md b/tools/coverage/README.md index 06173ba9..2154689f 100644 --- a/tools/coverage/README.md +++ b/tools/coverage/README.md @@ -26,6 +26,14 @@ fall out of that placement: so it sees the target's `env` attribute — that is how a target declares its minimum, without any global configuration or custom test rules. +One caveat: rules_kotlin hardcodes `kt_jvm_test`'s `_lcov_merger` attribute +to Bazel's built-in merger instead of reading the configuration field that +`--coverage_output_generator` sets, which would silently bypass enforcement +for Kotlin targets. `MODULE.bazel` carries a `single_version_override` patch +([`rules_kotlin_lcov_merger.patch`](rules_kotlin_lcov_merger.patch)) that +makes it use the configuration field, like rules_go/rules_rust/rules_java +already do. + ## Declaring a minimum Wrap any test rule that has the standard `env` attribute (`go_test`, diff --git a/tools/coverage/rules_kotlin_lcov_merger.patch b/tools/coverage/rules_kotlin_lcov_merger.patch new file mode 100644 index 00000000..95d98dbe --- /dev/null +++ b/tools/coverage/rules_kotlin_lcov_merger.patch @@ -0,0 +1,19 @@ +Make kt_jvm_test respect --coverage_output_generator. + +rules_kotlin hardcodes the test rule's _lcov_merger attribute to Bazel's +built-in CoverageOutputGenerator instead of reading the coverage fragment's +output_generator configuration field (the mechanism every other major rule +set uses, and what the --coverage_output_generator flag sets). That pins +kt_jvm_test coverage post-processing to the default merger and silently +ignores the custom LCOV merger this repository configures in .bazelrc, which +is what enforces per-target coverage minimums (//tools/coverage). + +--- a/kotlin/internal/jvm/jvm.bzl ++++ b/kotlin/internal/jvm/jvm.bzl +@@ -430,5 +430,5 @@ + "_lcov_merger": attr.label( +- default = Label("@bazel_tools//tools/test/CoverageOutputGenerator/java/com/google/devtools/coverageoutputgenerator:Main"), ++ default = configuration_field(fragment = "coverage", name = "output_generator"), + ), + }), + executable = True, diff --git a/tools/go/sample/BUILD b/tools/go/sample/BUILD index 014ffe9d..2f901ac7 100644 --- a/tools/go/sample/BUILD +++ b/tools/go/sample/BUILD @@ -12,10 +12,10 @@ go_library( # below 90% — the same floor the repo-wide CI gate applies to Go — enforced # per-target by //tools/coverage:lcov_merger. coverage_enforced_test( + name = "sample_test", + srcs = ["sample_test.go"], coverage_include = ["tools/go/"], embed = [":sample"], min_line_coverage = 90, - name = "sample_test", rule = go_test, - srcs = ["sample_test.go"], ) From 1ff21204ca5ffd447747c1cf6f0ce873405608ae Mon Sep 17 00:00:00 2001 From: Maxwell Elliott Date: Mon, 10 Aug 2026 12:20:31 -0400 Subject: [PATCH 3/4] Raise per-target coverage minimums to a 90% default Make 90% the Starlark default, drop enforcement on E2E/partial tests, and expand primary-owner unit tests (including RuleHasherTest) so every remaining coverage_minimum_env target clears the floor. Co-authored-by: Cursor --- README.md | 10 +- cli/BUILD | 156 ++---- .../kotlin/com/bazel_diff/cli/ServeCommand.kt | 36 +- .../com/bazel_diff/cli/VersionProvider.kt | 5 +- .../com/bazel_diff/cli/WarmupCommand.kt | 10 +- .../com/bazel_diff/hash/BuildGraphHasher.kt | 2 +- .../bazel_diff/bazel/BazelModServiceTest.kt | 242 +++++++-- .../com/bazel_diff/bazel/BazelRuleTest.kt | 163 ++++++ .../bazel_diff/bazel/ModuleGraphParserTest.kt | 223 +++++++++ .../bazel_diff/cli/FingerprintCommandTest.kt | 44 ++ .../com/bazel_diff/cli/ServeCommandTest.kt | 185 +++++++ .../com/bazel_diff/cli/VersionProviderTest.kt | 29 ++ .../com/bazel_diff/cli/WarmupCommandTest.kt | 61 +++ .../bazel_diff/hash/BuildGraphHasherTest.kt | 95 +++- .../bazel_diff/hash/FakeSourceFileHasher.kt | 5 + .../com/bazel_diff/hash/RuleHasherTest.kt | 463 ++++++++++++++++++ .../bazel_diff/hash/SourceFileHasherTest.kt | 100 ++++ .../CalculateImpactedTargetsInteractorTest.kt | 349 +++++++++++++ .../DeserialiseHashesInteractorTest.kt | 56 +++ .../com/bazel_diff/server/GitClientTest.kt | 57 +++ .../com/bazel_diff/server/HashServiceTest.kt | 101 ++++ .../server/LocalDiskHashCacheStorageTest.kt | 101 ++++ tools/coverage/BUILD | 1 - tools/coverage/README.md | 7 +- tools/coverage/defs.bzl | 23 +- tools/go/sample/BUILD | 5 +- tools/readme_template.md | 10 +- 27 files changed, 2359 insertions(+), 180 deletions(-) create mode 100644 cli/src/test/kotlin/com/bazel_diff/hash/RuleHasherTest.kt diff --git a/README.md b/README.md index 628b4a00..2f109af5 100644 --- a/README.md +++ b/README.md @@ -941,16 +941,16 @@ load("//tools/coverage:defs.bzl", "coverage_enforced_test") coverage_enforced_test( rule = go_test, # any test rule with the standard `env` attribute name = "sample_test", - min_line_coverage = 90, coverage_include = ["tools/go/"], ... ) ``` -Go (`//tools/go/sample:sample_test`), Rust (`//tools/coverage:lcov_merger_test`) -and the Kotlin/JVM tests under `//cli` all carry such minimums. When a target's -merged report falls below its minimum, the coverage run fails that target and the -test log contains a per-file breakdown. See +The default minimum is 90%. Go (`//tools/go/sample:sample_test`), Rust +(`//tools/coverage:lcov_merger_test`) and the primary-owner Kotlin/JVM tests +under `//cli` all carry such minimums. When a target's merged report falls +below its minimum, the coverage run fails that target and the test log +contains a per-file breakdown. See [`tools/coverage/README.md`](tools/coverage/README.md) for details. For an interactive HTML report (annotated source with covered/uncovered lines diff --git a/cli/BUILD b/cli/BUILD index e3381810..cfabc388 100644 --- a/cli/BUILD +++ b/cli/BUILD @@ -52,19 +52,17 @@ kt_jvm_library( ], ) -# Every kt_jvm_test declares a per-target line-coverage minimum, enforced by -# //tools/coverage:lcov_merger during `bazel coverage` only (plain `bazel test` -# is unaffected). Jacoco instruments the whole of :cli-lib for every test, so -# each check is scoped (coverage_include) to the source file(s) the test is -# responsible for covering. Floors are set from measured coverage minus ~10 -# points of headroom (rounded down to 5, capped at 90) to absorb -# platform/Bazel-version variance while still catching real regressions — -# ratchet them upward as coverage improves. +# Every primary-owner kt_jvm_test declares a per-target line-coverage minimum +# (default 90%), enforced by //tools/coverage:lcov_merger during +# `bazel coverage` only (plain `bazel test` is unaffected). Jacoco instruments +# the whole of :cli-lib for every test, so each check is scoped +# (coverage_include) to the source file(s) the test is responsible for. +# Partial/regression tests and E2ETest omit enforcement; the repo-wide +# coverage gate still covers the combined report. kt_jvm_test( name = "BuildGraphHasherTest", env = coverage_minimum_env( - 70, - ["cli/src/main/kotlin/com/bazel_diff/hash/BuildGraphHasher.kt"], + coverage_include = ["cli/src/main/kotlin/com/bazel_diff/hash/BuildGraphHasher.kt"], ), jvm_flags = [ "-Dnet.bytebuddy.experimental=true", @@ -76,19 +74,23 @@ kt_jvm_test( kt_jvm_test( name = "TargetHashTest", env = coverage_minimum_env( - 90, - ["cli/src/main/kotlin/com/bazel_diff/hash/TargetHash.kt"], + coverage_include = ["cli/src/main/kotlin/com/bazel_diff/hash/TargetHash.kt"], ), test_class = "com.bazel_diff.hash.TargetHashTest", runtime_deps = [":cli-test-lib"], ) kt_jvm_test( - name = "RuleHasherAlwaysAffectedTagsTest", + name = "RuleHasherTest", env = coverage_minimum_env( - 20, - ["cli/src/main/kotlin/com/bazel_diff/hash/RuleHasher.kt"], + coverage_include = ["cli/src/main/kotlin/com/bazel_diff/hash/RuleHasher.kt"], ), + test_class = "com.bazel_diff.hash.RuleHasherTest", + runtime_deps = [":cli-test-lib"], +) + +kt_jvm_test( + name = "RuleHasherAlwaysAffectedTagsTest", test_class = "com.bazel_diff.hash.RuleHasherAlwaysAffectedTagsTest", runtime_deps = [":cli-test-lib"], ) @@ -99,8 +101,7 @@ kt_jvm_test( ":src/test/kotlin/com/bazel_diff/hash/fixture/foo.ts", ], env = coverage_minimum_env( - 70, - ["cli/src/main/kotlin/com/bazel_diff/hash/SourceFileHasher.kt"], + coverage_include = ["cli/src/main/kotlin/com/bazel_diff/hash/SourceFileHasher.kt"], ), test_class = "com.bazel_diff.hash.SourceFileHasherTest", runtime_deps = [":cli-test-lib"], @@ -109,8 +110,7 @@ kt_jvm_test( kt_jvm_test( name = "CalculateImpactedTargetsInteractorTest", env = coverage_minimum_env( - 75, - ["cli/src/main/kotlin/com/bazel_diff/interactor/CalculateImpactedTargetsInteractor.kt"], + coverage_include = ["cli/src/main/kotlin/com/bazel_diff/interactor/CalculateImpactedTargetsInteractor.kt"], ), test_class = "com.bazel_diff.interactor.CalculateImpactedTargetsInteractorTest", runtime_deps = [":cli-test-lib"], @@ -118,20 +118,12 @@ kt_jvm_test( kt_jvm_test( name = "CalculateImpactedTargetsInteractorIssue335Test", - env = coverage_minimum_env( - 25, - ["cli/src/main/kotlin/com/bazel_diff/interactor/CalculateImpactedTargetsInteractor.kt"], - ), test_class = "com.bazel_diff.interactor.CalculateImpactedTargetsInteractorIssue335Test", runtime_deps = [":cli-test-lib"], ) kt_jvm_test( name = "CalculateImpactedTargetsInteractorModuleQueryTest", - env = coverage_minimum_env( - 50, - ["cli/src/main/kotlin/com/bazel_diff/interactor/CalculateImpactedTargetsInteractor.kt"], - ), test_class = "com.bazel_diff.interactor.CalculateImpactedTargetsInteractorModuleQueryTest", runtime_deps = [":cli-test-lib"], ) @@ -139,8 +131,7 @@ kt_jvm_test( kt_jvm_test( name = "NormalisingPathConverterTest", env = coverage_minimum_env( - 90, - ["cli/src/main/kotlin/com/bazel_diff/cli/converter/NormalisingPathConverter.kt"], + coverage_include = ["cli/src/main/kotlin/com/bazel_diff/cli/converter/NormalisingPathConverter.kt"], ), test_class = "com.bazel_diff.cli.converter.NormalisingPathConverterTest", runtime_deps = [":cli-test-lib"], @@ -149,8 +140,7 @@ kt_jvm_test( kt_jvm_test( name = "OptionsConverterTest", env = coverage_minimum_env( - 90, - ["cli/src/main/kotlin/com/bazel_diff/cli/converter/OptionsConverter.kt"], + coverage_include = ["cli/src/main/kotlin/com/bazel_diff/cli/converter/OptionsConverter.kt"], ), test_class = "com.bazel_diff.cli.converter.OptionsConverterTest", runtime_deps = [":cli-test-lib"], @@ -159,8 +149,7 @@ kt_jvm_test( kt_jvm_test( name = "DurationConverterTest", env = coverage_minimum_env( - 80, - ["cli/src/main/kotlin/com/bazel_diff/cli/converter/DurationConverter.kt"], + coverage_include = ["cli/src/main/kotlin/com/bazel_diff/cli/converter/DurationConverter.kt"], ), test_class = "com.bazel_diff.cli.converter.DurationConverterTest", runtime_deps = [":cli-test-lib"], @@ -169,8 +158,7 @@ kt_jvm_test( kt_jvm_test( name = "ByteSizeConverterTest", env = coverage_minimum_env( - 90, - ["cli/src/main/kotlin/com/bazel_diff/cli/converter/ByteSizeConverter.kt"], + coverage_include = ["cli/src/main/kotlin/com/bazel_diff/cli/converter/ByteSizeConverter.kt"], ), test_class = "com.bazel_diff.cli.converter.ByteSizeConverterTest", runtime_deps = [":cli-test-lib"], @@ -179,8 +167,7 @@ kt_jvm_test( kt_jvm_test( name = "DeserialiseHashesInteractorTest", env = coverage_minimum_env( - 75, - ["cli/src/main/kotlin/com/bazel_diff/interactor/DeserialiseHashesInteractor.kt"], + coverage_include = ["cli/src/main/kotlin/com/bazel_diff/interactor/DeserialiseHashesInteractor.kt"], ), test_class = "com.bazel_diff.interactor.DeserialiseHashesInteractorTest", runtime_deps = [":cli-test-lib"], @@ -189,8 +176,7 @@ kt_jvm_test( kt_jvm_test( name = "FingerprintInteractorTest", env = coverage_minimum_env( - 90, - ["cli/src/main/kotlin/com/bazel_diff/interactor/FingerprintInteractor.kt"], + coverage_include = ["cli/src/main/kotlin/com/bazel_diff/interactor/FingerprintInteractor.kt"], ), test_class = "com.bazel_diff.interactor.FingerprintInteractorTest", runtime_deps = [":cli-test-lib"], @@ -199,8 +185,7 @@ kt_jvm_test( kt_jvm_test( name = "FingerprintGathererTest", env = coverage_minimum_env( - 80, - ["cli/src/main/kotlin/com/bazel_diff/cli/FingerprintGatherer.kt"], + coverage_include = ["cli/src/main/kotlin/com/bazel_diff/cli/FingerprintGatherer.kt"], ), test_class = "com.bazel_diff.cli.FingerprintGathererTest", runtime_deps = [":cli-test-lib"], @@ -209,8 +194,7 @@ kt_jvm_test( kt_jvm_test( name = "FingerprintCommandTest", env = coverage_minimum_env( - 75, - ["cli/src/main/kotlin/com/bazel_diff/cli/FingerprintCommand.kt"], + coverage_include = ["cli/src/main/kotlin/com/bazel_diff/cli/FingerprintCommand.kt"], ), test_class = "com.bazel_diff.cli.FingerprintCommandTest", runtime_deps = [":cli-test-lib"], @@ -219,8 +203,7 @@ kt_jvm_test( kt_jvm_test( name = "WarmupCommandTest", env = coverage_minimum_env( - 65, - ["cli/src/main/kotlin/com/bazel_diff/cli/WarmupCommand.kt"], + coverage_include = ["cli/src/main/kotlin/com/bazel_diff/cli/WarmupCommand.kt"], ), test_class = "com.bazel_diff.cli.WarmupCommandTest", runtime_deps = [":cli-test-lib"], @@ -229,8 +212,7 @@ kt_jvm_test( kt_jvm_test( name = "BazelRuleTest", env = coverage_minimum_env( - 65, - ["cli/src/main/kotlin/com/bazel_diff/bazel/BazelRule.kt"], + coverage_include = ["cli/src/main/kotlin/com/bazel_diff/bazel/BazelRule.kt"], ), test_class = "com.bazel_diff.bazel.BazelRuleTest", runtime_deps = [":cli-test-lib"], @@ -239,8 +221,7 @@ kt_jvm_test( kt_jvm_test( name = "BazelClientTest", env = coverage_minimum_env( - 80, - ["cli/src/main/kotlin/com/bazel_diff/bazel/BazelClient.kt"], + coverage_include = ["cli/src/main/kotlin/com/bazel_diff/bazel/BazelClient.kt"], ), test_class = "com.bazel_diff.bazel.BazelClientTest", runtime_deps = [":cli-test-lib"], @@ -249,8 +230,7 @@ kt_jvm_test( kt_jvm_test( name = "BazelTargetTest", env = coverage_minimum_env( - 85, - ["cli/src/main/kotlin/com/bazel_diff/bazel/BazelTarget.kt"], + coverage_include = ["cli/src/main/kotlin/com/bazel_diff/bazel/BazelTarget.kt"], ), test_class = "com.bazel_diff.bazel.BazelTargetTest", runtime_deps = [":cli-test-lib"], @@ -259,8 +239,7 @@ kt_jvm_test( kt_jvm_test( name = "BazelTargetTypeTest", env = coverage_minimum_env( - 90, - ["cli/src/main/kotlin/com/bazel_diff/bazel/BazelTargetType.kt"], + coverage_include = ["cli/src/main/kotlin/com/bazel_diff/bazel/BazelTargetType.kt"], ), test_class = "com.bazel_diff.bazel.BazelTargetTypeTest", runtime_deps = [":cli-test-lib"], @@ -269,8 +248,7 @@ kt_jvm_test( kt_jvm_test( name = "BazelDiffTest", env = coverage_minimum_env( - 90, - ["cli/src/main/kotlin/com/bazel_diff/cli/BazelDiff.kt"], + coverage_include = ["cli/src/main/kotlin/com/bazel_diff/cli/BazelDiff.kt"], ), test_class = "com.bazel_diff.cli.BazelDiffTest", runtime_deps = [":cli-test-lib"], @@ -279,8 +257,7 @@ kt_jvm_test( kt_jvm_test( name = "VersionProviderTest", env = coverage_minimum_env( - 55, - ["cli/src/main/kotlin/com/bazel_diff/cli/VersionProvider.kt"], + coverage_include = ["cli/src/main/kotlin/com/bazel_diff/cli/VersionProvider.kt"], ), test_class = "com.bazel_diff.cli.VersionProviderTest", runtime_deps = [":cli-test-lib"], @@ -289,8 +266,7 @@ kt_jvm_test( kt_jvm_test( name = "StderrLoggerTest", env = coverage_minimum_env( - 90, - ["cli/src/main/kotlin/com/bazel_diff/log/StderrLogger.kt"], + coverage_include = ["cli/src/main/kotlin/com/bazel_diff/log/StderrLogger.kt"], ), test_class = "com.bazel_diff.log.StderrLoggerTest", runtime_deps = [":cli-test-lib"], @@ -299,8 +275,7 @@ kt_jvm_test( kt_jvm_test( name = "BazelModServiceTest", env = coverage_minimum_env( - 20, - ["cli/src/main/kotlin/com/bazel_diff/bazel/BazelModService.kt"], + coverage_include = ["cli/src/main/kotlin/com/bazel_diff/bazel/BazelModService.kt"], ), test_class = "com.bazel_diff.bazel.BazelModServiceTest", runtime_deps = [":cli-test-lib"], @@ -309,8 +284,7 @@ kt_jvm_test( kt_jvm_test( name = "ModuleGraphParserTest", env = coverage_minimum_env( - 65, - ["cli/src/main/kotlin/com/bazel_diff/bazel/ModuleGraphParser.kt"], + coverage_include = ["cli/src/main/kotlin/com/bazel_diff/bazel/ModuleGraphParser.kt"], ), test_class = "com.bazel_diff.bazel.ModuleGraphParserTest", runtime_deps = [":cli-test-lib"], @@ -318,20 +292,12 @@ kt_jvm_test( kt_jvm_test( name = "StderrPollutionRegressionTest", - env = coverage_minimum_env( - 15, - ["cli/src/main/kotlin/com/bazel_diff/bazel/ModuleGraphParser.kt"], - ), test_class = "com.bazel_diff.bazel.StderrPollutionRegressionTest", runtime_deps = [":cli-test-lib"], ) kt_jvm_test( name = "ProcessStdinHangRegressionTest", - env = coverage_minimum_env( - 60, - ["cli/src/main/kotlin/com/bazel_diff/process/"], - ), test_class = "com.bazel_diff.process.ProcessStdinHangRegressionTest", runtime_deps = [":cli-test-lib"], ) @@ -342,15 +308,9 @@ kt_jvm_test( # right at the "long" ceiling, so slow-runner jitter was timing it out. timeout = "eternal", data = [":workspaces"], - # Unlike the unit tests above (floors set from measurement), this is a - # conservative smoke floor over the whole main tree: the E2E suite drives - # the real binary through the full hashing/impacted-targets pipeline, so - # well under half of all main-source lines executing means the suite is - # broken. Measure and ratchet once a per-target baseline exists in CI. - env = coverage_minimum_env( - 30, - ["cli/src/main/kotlin/com/bazel_diff/"], - ), + # No per-target coverage minimum: E2E exercises the real binary across the + # whole main tree, but the repo-wide coverage gate (and primary-owner unit + # tests) own the 90% floor. Enforcement here would duplicate that gate. test_class = "com.bazel_diff.e2e.E2ETest", runtime_deps = [":cli-test-lib"], ) @@ -362,8 +322,7 @@ kt_jvm_test( ":src/test/kotlin/com/bazel_diff/io/fixture/wrong.json", ], env = coverage_minimum_env( - 90, - ["cli/src/main/kotlin/com/bazel_diff/io/ContentHashProvider.kt"], + coverage_include = ["cli/src/main/kotlin/com/bazel_diff/io/ContentHashProvider.kt"], ), test_class = "com.bazel_diff.io.ContentHashProviderTest", runtime_deps = [ @@ -374,8 +333,7 @@ kt_jvm_test( kt_jvm_test( name = "ServeCommandTest", env = coverage_minimum_env( - 60, - ["cli/src/main/kotlin/com/bazel_diff/cli/ServeCommand.kt"], + coverage_include = ["cli/src/main/kotlin/com/bazel_diff/cli/ServeCommand.kt"], ), test_class = "com.bazel_diff.cli.ServeCommandTest", runtime_deps = [":cli-test-lib"], @@ -384,8 +342,7 @@ kt_jvm_test( kt_jvm_test( name = "LocalDiskHashCacheStorageTest", env = coverage_minimum_env( - 75, - ["cli/src/main/kotlin/com/bazel_diff/server/HashCacheStorage.kt"], + coverage_include = ["cli/src/main/kotlin/com/bazel_diff/server/HashCacheStorage.kt"], ), test_class = "com.bazel_diff.server.LocalDiskHashCacheStorageTest", runtime_deps = [":cli-test-lib"], @@ -394,8 +351,7 @@ kt_jvm_test( kt_jvm_test( name = "S3HashCacheStorageTest", env = coverage_minimum_env( - 80, - ["cli/src/main/kotlin/com/bazel_diff/server/S3HashCacheStorage.kt"], + coverage_include = ["cli/src/main/kotlin/com/bazel_diff/server/S3HashCacheStorage.kt"], ), jvm_flags = [ "-Dnet.bytebuddy.experimental=true", @@ -407,8 +363,7 @@ kt_jvm_test( kt_jvm_test( name = "TieredHashCacheStorageTest", env = coverage_minimum_env( - 90, - ["cli/src/main/kotlin/com/bazel_diff/server/TieredHashCacheStorage.kt"], + coverage_include = ["cli/src/main/kotlin/com/bazel_diff/server/TieredHashCacheStorage.kt"], ), test_class = "com.bazel_diff.server.TieredHashCacheStorageTest", runtime_deps = [":cli-test-lib"], @@ -417,8 +372,7 @@ kt_jvm_test( kt_jvm_test( name = "CachePrunerTest", env = coverage_minimum_env( - 80, - ["cli/src/main/kotlin/com/bazel_diff/server/CachePruner.kt"], + coverage_include = ["cli/src/main/kotlin/com/bazel_diff/server/CachePruner.kt"], ), test_class = "com.bazel_diff.server.CachePrunerTest", runtime_deps = [":cli-test-lib"], @@ -427,8 +381,7 @@ kt_jvm_test( kt_jvm_test( name = "MetricsServiceTest", env = coverage_minimum_env( - 80, - ["cli/src/main/kotlin/com/bazel_diff/server/MetricsService.kt"], + coverage_include = ["cli/src/main/kotlin/com/bazel_diff/server/MetricsService.kt"], ), test_class = "com.bazel_diff.server.MetricsServiceTest", runtime_deps = [":cli-test-lib"], @@ -437,8 +390,7 @@ kt_jvm_test( kt_jvm_test( name = "GitClientTest", env = coverage_minimum_env( - 75, - ["cli/src/main/kotlin/com/bazel_diff/server/GitClient.kt"], + coverage_include = ["cli/src/main/kotlin/com/bazel_diff/server/GitClient.kt"], ), test_class = "com.bazel_diff.server.GitClientTest", runtime_deps = [":cli-test-lib"], @@ -447,8 +399,7 @@ kt_jvm_test( kt_jvm_test( name = "HashServiceTest", env = coverage_minimum_env( - 75, - ["cli/src/main/kotlin/com/bazel_diff/server/HashService.kt"], + coverage_include = ["cli/src/main/kotlin/com/bazel_diff/server/HashService.kt"], ), jvm_flags = [ "-Dnet.bytebuddy.experimental=true", @@ -460,8 +411,7 @@ kt_jvm_test( kt_jvm_test( name = "ImpactedTargetsServiceTest", env = coverage_minimum_env( - 85, - ["cli/src/main/kotlin/com/bazel_diff/server/ImpactedTargetsService.kt"], + coverage_include = ["cli/src/main/kotlin/com/bazel_diff/server/ImpactedTargetsService.kt"], ), jvm_flags = [ "-Dnet.bytebuddy.experimental=true", @@ -473,8 +423,7 @@ kt_jvm_test( kt_jvm_test( name = "BazelDiffServerTest", env = coverage_minimum_env( - 85, - ["cli/src/main/kotlin/com/bazel_diff/server/BazelDiffServer.kt"], + coverage_include = ["cli/src/main/kotlin/com/bazel_diff/server/BazelDiffServer.kt"], ), test_class = "com.bazel_diff.server.BazelDiffServerTest", runtime_deps = [":cli-test-lib"], @@ -483,8 +432,7 @@ kt_jvm_test( kt_jvm_test( name = "QueryProfilerTest", env = coverage_minimum_env( - 85, - ["cli/src/main/kotlin/com/bazel_diff/server/QueryProfiler.kt"], + coverage_include = ["cli/src/main/kotlin/com/bazel_diff/server/QueryProfiler.kt"], ), test_class = "com.bazel_diff.server.QueryProfilerTest", runtime_deps = [":cli-test-lib"], diff --git a/cli/src/main/kotlin/com/bazel_diff/cli/ServeCommand.kt b/cli/src/main/kotlin/com/bazel_diff/cli/ServeCommand.kt index 08401a07..596aeca3 100644 --- a/cli/src/main/kotlin/com/bazel_diff/cli/ServeCommand.kt +++ b/cli/src/main/kotlin/com/bazel_diff/cli/ServeCommand.kt @@ -55,7 +55,7 @@ import picocli.CommandLine "Runs bazel-diff as a long-running HTTP query service that returns the impacted targets " + "between two git revisions, caching generated hashes per commit SHA."], versionProvider = VersionProvider::class) -class ServeCommand : Callable { +open class ServeCommand : Callable { @CommandLine.ParentCommand private lateinit var parent: BazelDiff @CommandLine.Option( @@ -365,9 +365,10 @@ class ServeCommand : Callable { /** * Builds the [GitClient]. Git fetch/checkout operations shell out to the `git` binary at - * [gitPath], so a `git` binary must be available on the host. + * [gitPath], so a `git` binary must be available on the host. Overridable in tests so [call] can + * avoid shelling out during the initial-fetch handshake. */ - fun createGitClient(): GitClient = ProcessGitClient(workspacePath, gitPath) + open fun createGitClient(): GitClient = ProcessGitClient(workspacePath, gitPath) /** * Wires the services, starts the HTTP server, and performs the initial git fetch + readiness @@ -493,18 +494,27 @@ class ServeCommand : Callable { seedFilepaths?.readLines()?.filter { it.isNotBlank() }?.map { File(it).toPath() }?.toSet() ?: emptySet() - /** Blocks until a JVM shutdown signal (or thread interrupt), stopping the server cleanly. */ - private fun awaitShutdown(server: BazelDiffServer) { + /** + * Blocks until a JVM shutdown signal (or thread interrupt), stopping the server cleanly. + * + * [registerShutdownHook] and [await] default to the real JVM shutdown-hook + latch wait. Tests + * override this method (see [call]) or pass hooks so both the hook path and the interrupt path + * can be covered without hanging on a long-lived server. + */ + open fun awaitShutdown( + server: BazelDiffServer, + registerShutdownHook: (Thread) -> Unit = Runtime.getRuntime()::addShutdownHook, + await: (CountDownLatch) -> Unit = { it.await() }, + ) { val latch = CountDownLatch(1) - Runtime.getRuntime() - .addShutdownHook( - Thread { - cachePruner?.stop() - server.stop(1) - latch.countDown() - }) + registerShutdownHook( + Thread { + cachePruner?.stop() + server.stop(1) + latch.countDown() + }) try { - latch.await() + await(latch) } catch (e: InterruptedException) { // Treated as a shutdown signal: stop serving and restore the interrupt flag. cachePruner?.stop() diff --git a/cli/src/main/kotlin/com/bazel_diff/cli/VersionProvider.kt b/cli/src/main/kotlin/com/bazel_diff/cli/VersionProvider.kt index bf0220b2..d5222e06 100644 --- a/cli/src/main/kotlin/com/bazel_diff/cli/VersionProvider.kt +++ b/cli/src/main/kotlin/com/bazel_diff/cli/VersionProvider.kt @@ -4,9 +4,10 @@ import java.io.BufferedReader import java.io.InputStreamReader import picocli.CommandLine.IVersionProvider -class VersionProvider : IVersionProvider { +class VersionProvider( + private val classLoader: ClassLoader = VersionProvider::class.java.classLoader +) : IVersionProvider { override fun getVersion(): Array { - val classLoader = this::class.java.classLoader val inputStream = classLoader.getResourceAsStream("cli/version") ?: classLoader.getResourceAsStream("version") diff --git a/cli/src/main/kotlin/com/bazel_diff/cli/WarmupCommand.kt b/cli/src/main/kotlin/com/bazel_diff/cli/WarmupCommand.kt index e800ea8f..8ba4b994 100644 --- a/cli/src/main/kotlin/com/bazel_diff/cli/WarmupCommand.kt +++ b/cli/src/main/kotlin/com/bazel_diff/cli/WarmupCommand.kt @@ -28,7 +28,7 @@ import picocli.CommandLine "revision, writes base hashes + fingerprint to known paths, and exits 0 only once " + "the Bazel server is warm (the host's 'safe to snapshot' signal)."], versionProvider = VersionProvider::class) -class WarmupCommand : GenerateHashesCommand() { +open class WarmupCommand : GenerateHashesCommand() { @CommandLine.Option( names = ["--base-hashes"], @@ -47,7 +47,7 @@ class WarmupCommand : GenerateHashesCommand() { outputPath = baseHashesPath baseHashesPath.parentFile?.mkdirs() - val genResult = super.call() + val genResult = runGenerateHashes() if (genResult != CommandLine.ExitCode.OK) { // Do not write the fingerprint or signal "safe to snapshot" on a failed warmup. return genResult @@ -57,6 +57,12 @@ class WarmupCommand : GenerateHashesCommand() { return CommandLine.ExitCode.OK } + /** + * Invokes [GenerateHashesCommand.call]. Overridable in tests so [call] can be exercised without a + * real `bazel query`. + */ + open fun runGenerateHashes(): Int = super.call() + /** * Computes the fingerprint over the current flag set + workspace and writes it to * [fingerprintOutputPath]. Split out of [call] so it is unit-testable without the bazel-backed diff --git a/cli/src/main/kotlin/com/bazel_diff/hash/BuildGraphHasher.kt b/cli/src/main/kotlin/com/bazel_diff/hash/BuildGraphHasher.kt index 3b0182f8..b221dfbf 100644 --- a/cli/src/main/kotlin/com/bazel_diff/hash/BuildGraphHasher.kt +++ b/cli/src/main/kotlin/com/bazel_diff/hash/BuildGraphHasher.kt @@ -275,7 +275,7 @@ class BuildGraphHasher(private val bazelClient: BazelClient) : KoinComponent { * `//pkg:a` -> `//pkg`, `//:logo` -> `//`, `@@repo//pkg:a` -> `@@repo//pkg`. Labels without a `:` * (not expected from `bazel query` output) are returned unchanged. */ -internal fun labelToPackage(label: String): String { +fun labelToPackage(label: String): String { val colon = label.lastIndexOf(':') return if (colon >= 0) label.substring(0, colon) else label } diff --git a/cli/src/test/kotlin/com/bazel_diff/bazel/BazelModServiceTest.kt b/cli/src/test/kotlin/com/bazel_diff/bazel/BazelModServiceTest.kt index a5f08d58..8e3b6f16 100644 --- a/cli/src/test/kotlin/com/bazel_diff/bazel/BazelModServiceTest.kt +++ b/cli/src/test/kotlin/com/bazel_diff/bazel/BazelModServiceTest.kt @@ -1,11 +1,15 @@ package com.bazel_diff.bazel import assertk.assertThat +import assertk.assertions.contains +import assertk.assertions.isEqualTo import assertk.assertions.isFalse +import assertk.assertions.isNull +import assertk.assertions.isTrue import com.bazel_diff.SilentLogger import com.bazel_diff.log.Logger import java.io.File -import java.nio.file.Paths +import kotlinx.coroutines.runBlocking import org.junit.Rule import org.junit.Test import org.junit.rules.TemporaryFolder @@ -19,9 +23,22 @@ class BazelModServiceTest : KoinTest { @get:Rule val temp: TemporaryFolder = TemporaryFolder() - @Test - fun isBzlmodEnabled_returnsFalse_whenWorkspaceHasNoModuleBazel() { - val workspaceDir = temp.newFolder() + private fun fakeBazel( + name: String = "bazel", + body: String, + ): File = + File(temp.root, name).apply { + writeText("#!/bin/sh\n$body\n") + setExecutable(true) + } + + private fun withService( + workspaceDir: File, + bazel: File, + startupOptions: List = listOf("--host_jvm_args=-Xmx1g"), + noBazelrc: Boolean = true, + block: suspend (BazelModService) -> Unit, + ) { startKoin { modules( module { @@ -29,44 +46,211 @@ class BazelModServiceTest : KoinTest { single { BazelModService( workingDirectory = workspaceDir.toPath(), - bazelPath = Paths.get("bazel"), - startupOptions = listOf("--enable_bzlmod"), - noBazelrc = true, + bazelPath = bazel.toPath(), + startupOptions = startupOptions, + noBazelrc = noBazelrc, ) } }) } try { - val service = get() - assertThat(service.isBzlmodEnabled).isFalse() + runBlocking { block(get()) } } finally { stopKoin() } } - @Test - fun isBzlmodEnabled_returnsConsistentValue_whenWorkspaceHasModuleBazel() { + private fun workspaceWithModule(): File { val workspaceDir = temp.newFolder() File(workspaceDir, "MODULE.bazel").writeText("module(name = \"test\")\n") - startKoin { - modules( - module { - single { SilentLogger } - single { - BazelModService( - workingDirectory = workspaceDir.toPath(), - bazelPath = Paths.get("bazel"), - startupOptions = listOf("--enable_bzlmod"), - noBazelrc = true, - ) - } - }) + return workspaceDir + } + + @Test + fun isBzlmodEnabled_returnsFalse_whenWorkspaceHasNoModuleBazel() { + val workspaceDir = temp.newFolder() + // Fake bazel that would succeed — without MODULE.bazel the production path still runs + // `bazel mod graph`; use a failing binary so this stays environment-independent. + val bazel = fakeBazel(body = "exit 1") + withService(workspaceDir, bazel) { service -> + assertThat(service.isBzlmodEnabled).isFalse() } - try { - val service = get() - service.isBzlmodEnabled - } finally { - stopKoin() + } + + @Test + fun isBzlmodEnabled_returnsTrue_whenModuleBazelPresentAndBazelSucceeds() { + val workspaceDir = workspaceWithModule() + val bazel = fakeBazel(body = "echo 'root'\nexit 0") + withService(workspaceDir, bazel) { service -> + assertThat(service.isBzlmodEnabled).isTrue() + } + } + + @Test + fun isBzlmodEnabled_returnsFalse_whenBazelExitsNonZero_evenWithModuleBazel() { + val workspaceDir = workspaceWithModule() + val bazel = fakeBazel(body = "echo 'ERROR: bzlmod disabled' >&2\nexit 2") + withService(workspaceDir, bazel) { service -> + assertThat(service.isBzlmodEnabled).isFalse() + } + } + + @Test + fun getModuleGraph_returnsTrimmedOutput_onSuccess() { + val workspaceDir = workspaceWithModule() + val bazel = + fakeBazel( + body = + """ + echo ' root' + echo ' |-- foo@1.0' + echo '' + exit 0 + """ + .trimIndent()) + withService(workspaceDir, bazel) { service -> + val graph = service.getModuleGraph() + assertThat(graph).isEqualTo("root\n |-- foo@1.0") + } + } + + @Test + fun getModuleGraph_returnsNull_whenBzlmodDisabled() { + val workspaceDir = workspaceWithModule() + val bazel = fakeBazel(body = "exit 1") + withService(workspaceDir, bazel) { service -> + assertThat(service.getModuleGraph()).isNull() + } + } + + @Test + fun getModuleGraph_returnsNull_onFailureAfterEnabled() { + val workspaceDir = workspaceWithModule() + val countFile = File(temp.root, "mod-graph-count") + // Escape $ so Kotlin does not treat shell $n as string templates. + val bazel = + fakeBazel( + body = + """ + n=`cat '${countFile.absolutePath}' 2>/dev/null || echo 0` + n=`expr "${'$'}n" + 1` + echo "${'$'}n" > '${countFile.absolutePath}' + if [ "${'$'}n" -eq 1 ]; then + echo 'root' + exit 0 + fi + exit 1 + """ + .trimIndent()) + withService(workspaceDir, bazel) { service -> + assertThat(service.isBzlmodEnabled).isTrue() + assertThat(service.getModuleGraph()).isNull() + } + } + + @Test + fun getModuleGraphJson_returnsTrimmedOutput_onSuccess() { + val workspaceDir = workspaceWithModule() + val bazel = + fakeBazel( + body = + """ + case " ${'$'}* " in + *" --output=json "*) + printf ' {"key":""} \n' + ;; + *) + echo 'root' + ;; + esac + exit 0 + """ + .trimIndent()) + withService(workspaceDir, bazel) { service -> + val json = service.getModuleGraphJson() + assertThat(json).isEqualTo("{\"key\":\"\"}") + } + } + + @Test + fun getModuleGraphJson_returnsNull_whenBzlmodDisabled() { + val workspaceDir = workspaceWithModule() + val bazel = fakeBazel(body = "exit 1") + withService(workspaceDir, bazel) { service -> + assertThat(service.getModuleGraphJson()).isNull() + } + } + + @Test + fun getModuleGraphJson_returnsNull_onFailureAfterEnabled() { + val workspaceDir = workspaceWithModule() + val bazel = + fakeBazel( + body = + """ + case " ${'$'}* " in + *" --output=json "*) + exit 1 + ;; + *) + echo 'root' + exit 0 + ;; + esac + """ + .trimIndent()) + withService(workspaceDir, bazel) { service -> + assertThat(service.isBzlmodEnabled).isTrue() + assertThat(service.getModuleGraphJson()).isNull() + } + } + + @Test + fun checkBzlmodEnabled_addsNoBazelrcAndStartupOptions() { + val workspaceDir = workspaceWithModule() + val argsFile = File(temp.root, "bazel-args.txt") + val bazel = + fakeBazel( + body = + """ + echo "${'$'}@" > '${argsFile.absolutePath}' + echo 'root' + exit 0 + """ + .trimIndent()) + withService( + workspaceDir, + bazel, + startupOptions = listOf("--host_jvm_args=-Xmx512m", "--batch"), + noBazelrc = true, + ) { service -> + assertThat(service.isBzlmodEnabled).isTrue() + val args = argsFile.readText() + assertThat(args).contains("--bazelrc=/dev/null") + assertThat(args).contains("--host_jvm_args=-Xmx512m") + assertThat(args).contains("--batch") + assertThat(args).contains("mod") + assertThat(args).contains("graph") + } + } + + @Test + fun getModuleGraph_omitsNoBazelrc_whenDisabled() { + val workspaceDir = workspaceWithModule() + val argsFile = File(temp.root, "bazel-args-nobazelrc-off.txt") + val bazel = + fakeBazel( + body = + """ + echo "${'$'}@" > '${argsFile.absolutePath}' + echo 'root' + exit 0 + """ + .trimIndent()) + withService(workspaceDir, bazel, noBazelrc = false) { service -> + service.getModuleGraph() + val args = argsFile.readText() + assertThat(args.contains("--bazelrc=/dev/null")).isEqualTo(false) } } } diff --git a/cli/src/test/kotlin/com/bazel_diff/bazel/BazelRuleTest.kt b/cli/src/test/kotlin/com/bazel_diff/bazel/BazelRuleTest.kt index 1d5e431b..54724c52 100644 --- a/cli/src/test/kotlin/com/bazel_diff/bazel/BazelRuleTest.kt +++ b/cli/src/test/kotlin/com/bazel_diff/bazel/BazelRuleTest.kt @@ -324,6 +324,169 @@ class BazelRuleTest { assertThat(forward).isEqualTo(reversed) } + // External @repo//... inputs that are not in fineGrainedHashExternalRepos collapse to a + // synthetic //external: label so dependents pick up WORKSPACE/bzlmod repo hash changes. + @Test + fun testExternalRepoInputCollapsesToSyntheticExternalTarget() { + val rule = + Rule.newBuilder() + .setRuleClass("java_library") + .setName("//pkg:lib") + .addRuleInput("@com_google_guava//guava:guava") + .addRuleInput("//pkg:local") + .build() + + val inputs = + BazelRule(rule).ruleInputList(useCquery = false, fineGrainedHashExternalRepos = emptySet()) + + assertThat(inputs) + .isEqualTo( + listOf("//external:com_google_guava", "//pkg:local", "@com_google_guava//guava:guava")) + } + + // Canonical @@repo labels strip all leading @ chars when collapsing. + @Test + fun testCanonicalExternalRepoInputCollapsesWithoutAtSigns() { + val rule = + Rule.newBuilder() + .setRuleClass("java_library") + .setName("//pkg:lib") + .addRuleInput("@@rules_jvm_external//:defs") + .build() + + val inputs = + BazelRule(rule).ruleInputList(useCquery = false, fineGrainedHashExternalRepos = emptySet()) + + assertThat(inputs) + .isEqualTo(listOf("//external:rules_jvm_external", "@@rules_jvm_external//:defs")) + } + + // Repos listed in fineGrainedHashExternalRepos keep their real labels; no //external:* synthetic. + @Test + fun testFineGrainedHashExternalReposPreservesExternalLabel() { + val rule = + Rule.newBuilder() + .setRuleClass("java_library") + .setName("//pkg:lib") + .addRuleInput("@inner_repo//:lib") + .addRuleInput("@other_repo//:lib") + .build() + + val inputs = + BazelRule(rule) + .ruleInputList( + useCquery = false, fineGrainedHashExternalRepos = setOf("@inner_repo")) + + assertThat(inputs) + .isEqualTo( + listOf("//external:other_repo", "@inner_repo//:lib", "@other_repo//:lib")) + } + + // Main-repo spellings must never be collapsed to //external:*. + @Test + fun testMainRepoInputsAreNotCollapsed() { + val rule = + Rule.newBuilder() + .setRuleClass("java_library") + .setName("//pkg:lib") + .addRuleInput("//pkg:dep") + .addRuleInput("@//pkg:at_main") + .addRuleInput("@@//pkg:canonical_main") + .build() + + val inputs = + BazelRule(rule).ruleInputList(useCquery = false, fineGrainedHashExternalRepos = emptySet()) + + assertThat(inputs) + .isEqualTo(listOf("//pkg:dep", "@//pkg:at_main", "@@//pkg:canonical_main")) + } + + // Under cquery, synthetic //external:* inputs still come from rule_input (not configured_rule_input). + @Test + fun testCqueryRuleInputListIncludesExternalSyntheticFromRuleInputs() { + val rule = + Rule.newBuilder() + .setRuleClass("genrule") + .setName("//:gen") + .addRuleInput("@ext//:lib") + .addConfiguredRuleInput( + Build.ConfiguredRuleInput.newBuilder() + .setLabel("//:dep") + .setConfigurationChecksum("cfg-A") + .build()) + .build() + + val inputs = + BazelRule(rule).ruleInputList(useCquery = true, fineGrainedHashExternalRepos = emptySet()) + + assertThat(inputs).isEqualTo(listOf("//:dep|cfg-A", "//external:ext")) + } + + // Labels that start with @ but lack a // package separator are left unchanged. + @Test + fun testExternalInputWithoutPackageSeparatorIsUnchanged() { + val rule = + Rule.newBuilder() + .setRuleClass("java_library") + .setName("//pkg:lib") + .addRuleInput("@nopackage") + .build() + + val inputs = + BazelRule(rule).ruleInputList(useCquery = false, fineGrainedHashExternalRepos = emptySet()) + + assertThat(inputs).isEqualTo(listOf("@nopackage")) + } + + @Test + fun testInstantiationStackReturnsProtoFrames() { + val rule = + Rule.newBuilder() + .setRuleClass("java_library") + .setName("//pkg:lib") + .addInstantiationStack("macros.bzl:10:2: my_macro") + .addInstantiationStack("BUILD:5:1: ") + .build() + + assertThat(BazelRule(rule).instantiationStack) + .isEqualTo(listOf("macros.bzl:10:2: my_macro", "BUILD:5:1: ")) + } + + @Test + fun testInstantiationStackEmptyByDefault() { + val rule = Rule.newBuilder().setRuleClass("java_library").setName("//pkg:lib").build() + + assertThat(BazelRule(rule).instantiationStack).isEqualTo(emptyList()) + } + + // Custom ignoredAttrs are layered on top of DEFAULT_IGNORED_ATTRS (generator_location). + @Test + fun testDigestIgnoresCustomIgnoredAttrs() { + fun ruleWith(tags: String) = + Rule.newBuilder() + .setRuleClass("java_library") + .setName("lib") + .addAttribute( + Attribute.newBuilder() + .setType(Attribute.Discriminator.STRING) + .setName("tags") + .setStringValue(tags) + .build()) + .addAttribute( + Attribute.newBuilder() + .setType(Attribute.Discriminator.STRING) + .setName("generator_location") + .setStringValue("BUILD:1:1") + .build()) + .build() + + val a = BazelRule(ruleWith("keep")) + val b = BazelRule(ruleWith("drop")) + + assertThat(a.digest(setOf("tags"))).isEqualTo(b.digest(setOf("tags"))) + assertThat(a.digest(emptySet())).isNotEqualTo(b.digest(emptySet())) + } + private fun configuredGenrule(depLabel: String, configurationChecksum: String): Rule { return Rule.newBuilder() .setRuleClass("genrule") diff --git a/cli/src/test/kotlin/com/bazel_diff/bazel/ModuleGraphParserTest.kt b/cli/src/test/kotlin/com/bazel_diff/bazel/ModuleGraphParserTest.kt index 34f8559a..5205630d 100644 --- a/cli/src/test/kotlin/com/bazel_diff/bazel/ModuleGraphParserTest.kt +++ b/cli/src/test/kotlin/com/bazel_diff/bazel/ModuleGraphParserTest.kt @@ -365,6 +365,164 @@ class ModuleGraphParserTest { assertThat(first).isEqualTo(second) } + // --------------------------------------------------------------------------------------- + // parseModuleGraphDepEdges + // --------------------------------------------------------------------------------------- + + @Test + fun parseModuleGraphDepEdges_happyPath_mapsModuleNamesToDeps() { + val json = + """ + { + "key": "", + "name": "my-project", + "version": "1.0.0", + "apparentName": "my-project", + "dependencies": [ + { + "key": "abseil-cpp@20240116.2", + "name": "abseil-cpp", + "version": "20240116.2", + "apparentName": "com_google_absl", + "dependencies": [ + { + "key": "googletest@1.14.0", + "name": "googletest", + "version": "1.14.0", + "apparentName": "com_google_googletest", + "dependencies": [] + } + ] + }, + { + "key": "protobuf@21.7", + "name": "protobuf", + "version": "21.7", + "apparentName": "com_google_protobuf", + "dependencies": [] + } + ] + } + """ + .trimIndent() + + val edges = parser.parseModuleGraphDepEdges(json) + + assertThat(edges["my-project"]!!).containsExactlyInAnyOrder("abseil-cpp", "protobuf") + assertThat(edges["abseil-cpp"]!!).containsExactlyInAnyOrder("googletest") + // Leaf modules with empty dependencies still appear with an empty list. + assertThat(edges["googletest"]!!).isEmpty() + assertThat(edges["protobuf"]!!).isEmpty() + } + + @Test + fun parseModuleGraphDepEdges_unexpandedStubs_stillContributeEdges() { + // Same issue #197 shape: middle_repo's dependency on inner_repo is an unexpanded stub. + // parseModuleGraphDepEdges still records the edge and recurses (unlike walkEdges). + val json = + """ + { + "key": "", + "name": "wrapped_external_repo_test", + "version": "0.0.0", + "apparentName": "wrapped_external_repo_test", + "dependencies": [ + { + "key": "inner_repo@_", + "name": "inner_repo", + "version": "0.0.0", + "apparentName": "inner_repo", + "dependencies": [] + }, + { + "key": "middle_repo@_", + "name": "middle_repo", + "version": "0.0.0", + "apparentName": "middle_repo", + "dependencies": [ + { + "key": "inner_repo@_", + "name": "inner_repo", + "version": "0.0.0", + "apparentName": "inner_repo", + "unexpanded": true + } + ] + } + ] + } + """ + .trimIndent() + + val edges = parser.parseModuleGraphDepEdges(json) + + assertThat(edges["wrapped_external_repo_test"]!!) + .containsExactlyInAnyOrder("inner_repo", "middle_repo") + assertThat(edges["middle_repo"]!!).containsExactlyInAnyOrder("inner_repo") + } + + @Test + fun parseModuleGraphDepEdges_withStderrPrefix_extractsEdges() { + val cleanJson = + """ + { + "key": "", + "name": "ws", + "version": "", + "apparentName": "ws", + "dependencies": [ + {"key": "a@1", "name": "a", "version": "1", "apparentName": "a", "dependencies": []} + ] + } + """ + .trimIndent() + val polluted = "INFO: Invocation ID: abc\nLoading: 0 packages loaded\n$cleanJson" + + assertThat(parser.parseModuleGraphDepEdges(polluted)).isEqualTo(parser.parseModuleGraphDepEdges(cleanJson)) + assertThat(parser.parseModuleGraphDepEdges(polluted)["ws"]!!).containsExactlyInAnyOrder("a") + } + + @Test + fun parseModuleGraphDepEdges_withInvalidJson_returnsEmptyMap() { + assertThat(parser.parseModuleGraphDepEdges("{ invalid json")).isEmpty() + } + + @Test + fun parseModuleGraphDepEdges_withMissingBrace_returnsEmptyMap() { + assertThat(parser.parseModuleGraphDepEdges("not json at all")).isEmpty() + } + + @Test + fun parseModuleGraphDepEdges_skipsNonObjectDepsAndMissingNames() { + val json = + """ + { + "key": "", + "name": "root", + "version": "1", + "apparentName": "root", + "dependencies": [ + "string-dep", + {"key": "no-name@1", "version": "1", "apparentName": "x"}, + {"key": "ok@1", "name": "ok", "version": "1", "apparentName": "ok", "dependencies": []} + ] + } + """ + .trimIndent() + + val edges = parser.parseModuleGraphDepEdges(json) + + assertThat(edges["root"]!!).containsExactlyInAnyOrder("ok") + } + + @Test + fun parseModuleGraphDepEdges_moduleWithoutNameOrDeps_returnsEmpty() { + // Missing `name` short-circuits extractDepEdges; no edges recorded. + assertThat(parser.parseModuleGraphDepEdges("""{"key":"","dependencies":[]}""")).isEmpty() + // Present name but missing dependencies array also short-circuits. + assertThat(parser.parseModuleGraphDepEdges("""{"key":"","name":"root"}""")).isEmpty() + } + // --------------------------------------------------------------------------------------- // parseModuleGraphEdges / findTransitiveDependents (issue #197 expansion) // --------------------------------------------------------------------------------------- @@ -372,6 +530,71 @@ class ModuleGraphParserTest { // `@inner_repo` is the user-listed fine-grained repo and `@middle_repo` wraps it, the // expansion has to follow `bazel mod graph` backwards to add `@middle_repo` automatically. + @Test + fun parseModuleGraphEdges_withInvalidJson_returnsEmpty() { + val graph = parser.parseModuleGraphEdges("{ invalid json") + assertThat(graph.edges).isEmpty() + assertThat(graph.rootApparentNames).isEmpty() + } + + @Test + fun parseModuleGraphEdges_withMissingBrace_returnsEmpty() { + val graph = parser.parseModuleGraphEdges("stderr only, no json object") + assertThat(graph.edges).isEmpty() + assertThat(graph.rootApparentNames).isEmpty() + } + + @Test + fun parseModuleGraphEdges_withStderrPrefix_extractsEdges() { + val cleanJson = + """ + { + "key": "", + "name": "ws", + "version": "", + "apparentName": "ws", + "dependencies": [ + {"key": "a@1", "name": "a", "version": "1", "apparentName": "a", "dependencies": []} + ] + } + """ + .trimIndent() + val polluted = "WARNING: something\n$cleanJson" + + val graph = parser.parseModuleGraphEdges(polluted) + assertThat(graph.rootApparentNames).containsExactlyInAnyOrder("ws") + assertThat(graph.edges["ws"]!!).containsExactlyInAnyOrder("a") + } + + @Test + fun parseModuleGraphEdges_skipsNonObjectDepsAndMissingApparentNames() { + val json = + """ + { + "key": "", + "name": "root", + "version": "1", + "apparentName": "root", + "dependencies": [ + 42, + {"key": "no-app@1", "name": "no-app", "version": "1"}, + {"key": "ok@1", "name": "ok", "version": "1", "apparentName": "ok", "dependencies": []} + ] + } + """ + .trimIndent() + + val graph = parser.parseModuleGraphEdges(json) + assertThat(graph.edges["root"]!!).containsExactlyInAnyOrder("ok") + } + + @Test + fun parseModuleGraphEdges_missingApparentName_returnsEmpty() { + val graph = parser.parseModuleGraphEdges("""{"key":"","name":"root","dependencies":[]}""") + assertThat(graph.edges).isEmpty() + assertThat(graph.rootApparentNames).isEmpty() + } + @Test fun parseModuleGraphEdges_realIssue197Shape_extractsEdgesAndRoot() { // Mirrors `bazel mod graph --output=json` for the `wrapped_external_repo` fixture: diff --git a/cli/src/test/kotlin/com/bazel_diff/cli/FingerprintCommandTest.kt b/cli/src/test/kotlin/com/bazel_diff/cli/FingerprintCommandTest.kt index 8d376d20..72d54c13 100644 --- a/cli/src/test/kotlin/com/bazel_diff/cli/FingerprintCommandTest.kt +++ b/cli/src/test/kotlin/com/bazel_diff/cli/FingerprintCommandTest.kt @@ -3,7 +3,9 @@ package com.bazel_diff.cli import assertk.assertThat import assertk.assertions.contains import assertk.assertions.isEqualTo +import java.io.ByteArrayOutputStream import java.io.File +import java.io.PrintStream import org.junit.Rule import org.junit.Test import org.junit.rules.TemporaryFolder @@ -63,4 +65,46 @@ class FingerprintCommandTest { assertThat(cmd.call()).isEqualTo(CommandLine.ExitCode.OK) assertThat(File("-").exists()).isEqualTo(false) } + + @Test + fun nullOutputPathWritesFingerprintJsonToStdout() { + val ws = temp.newFolder("ws4") + val cmd = command(ws).apply { outputPath = null } + val captured = ByteArrayOutputStream() + val previous = System.out + System.setOut(PrintStream(captured)) + try { + assertThat(cmd.call()).isEqualTo(CommandLine.ExitCode.OK) + } finally { + System.setOut(previous) + } + val json = captured.toString() + assertThat(json).contains("\"fingerprint\"") + assertThat(json).contains("\"flags\"") + } + + @Test + fun flagsInOutputAreSortedByKey() { + val ws = temp.newFolder("ws5") + val out = File(temp.root, "sorted.json") + command(ws) + .apply { + outputPath = out + keepGoing = true + useCquery = true + includeTargetType = true + excludeExternalTargets = true + bazelCommandOptions = listOf("--foo") + bazelStartupOptions = listOf("--bar") + } + .call() + val json = out.readText() + // canonicalizeFlags + toSortedMap should emit keys in lexicographic order + val flagsIdx = json.indexOf("\"flags\"") + val keepGoingIdx = json.indexOf("\"keepGoing\"", flagsIdx) + val useCqueryIdx = json.indexOf("\"useCquery\"", flagsIdx) + assert(keepGoingIdx in 1 until useCqueryIdx) { + "expected keepGoing before useCquery in sorted flags block" + } + } } diff --git a/cli/src/test/kotlin/com/bazel_diff/cli/ServeCommandTest.kt b/cli/src/test/kotlin/com/bazel_diff/cli/ServeCommandTest.kt index 8671413d..eb51badb 100644 --- a/cli/src/test/kotlin/com/bazel_diff/cli/ServeCommandTest.kt +++ b/cli/src/test/kotlin/com/bazel_diff/cli/ServeCommandTest.kt @@ -2,12 +2,14 @@ package com.bazel_diff.cli import assertk.assertThat import assertk.assertions.hasLength +import assertk.assertions.hasSize import assertk.assertions.isEqualTo import assertk.assertions.isFalse import assertk.assertions.isInstanceOf import assertk.assertions.isNotEqualTo import assertk.assertions.isNotNull import assertk.assertions.isNull +import assertk.assertions.isTrue import com.bazel_diff.SilentLogger import com.bazel_diff.log.Logger import com.bazel_diff.server.GitClient @@ -19,9 +21,14 @@ import com.bazel_diff.server.ServerMetrics import com.bazel_diff.server.TieredHashCacheStorage import com.google.gson.Gson import com.google.gson.GsonBuilder +import java.io.File import java.net.HttpURLConnection import java.net.URL import java.time.Duration +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicReference import org.junit.After import org.junit.Rule import org.junit.Test @@ -29,6 +36,7 @@ import org.junit.rules.TemporaryFolder import org.koin.dsl.module import org.koin.test.KoinTest import org.koin.test.KoinTestRule +import picocli.CommandLine class ServeCommandTest : KoinTest { @get:Rule @@ -444,4 +452,181 @@ class ServeCommandTest : KoinTest { conn.disconnect() } } + + @Test + fun configFingerprintChangesWithExternalReposFile() { + val base = ServeCommand().computeConfigFingerprint() + val reposFile = temp.newFile().apply { writeText("@maven\n") } + val withFile = + ServeCommand() + .apply { fineGrainedHashExternalReposFile = reposFile } + .computeConfigFingerprint() + assertThat(withFile).isNotEqualTo(base) + } + + @Test + fun buildAndStartServerStartsCachePrunerWhenLimitsConfigured() { + // Exercises the cachePruner = buildCachePruner(...)?.also { it.start() } path with a real + // prunable backend so awaitShutdown can later stop a non-null pruner. + val cmd = command(noFetch = true).apply { cacheMaxEntries = 10 } + val server = + cmd + .buildAndStartServer(FakeGitClient(), LocalDiskHashCacheStorage(cmd.cacheDir)) + .also { startedServers += it } + assertThat(healthCode(server)).isEqualTo(200) + } + + @Test + fun warmUpCacheRunsBeforeReadinessIsFlipped() { + // ready must stay false for the whole warmUpCache call so a load balancer cannot route to an + // instance mid-warmup. + val sawReadyDuringWarmup = AtomicBoolean(true) + val ready = AtomicBoolean(false) + val provider = + object : com.bazel_diff.server.HashProvider { + override fun getHashes( + sha: String, + modifiedFilepaths: Set, + profiler: com.bazel_diff.server.QueryProfiler? + ): com.bazel_diff.interactor.HashFileData { + sawReadyDuringWarmup.set(ready.get()) + return com.bazel_diff.interactor.HashFileData(emptyMap(), null) + } + + override fun withWorkspaceAt(sha: String, block: () -> T): T = block() + } + val cmd = command().apply { warmupRevisions = linkedSetOf("main") } + val server = + com.bazel_diff.server + .BazelDiffServer(0, NoopImpactedTargets) { ready.get() } + .also { startedServers += it } + server.start() + + cmd.performInitialFetch(FakeGitClient(), provider, ready, server) + + assertThat(sawReadyDuringWarmup.get()).isFalse() + assertThat(ready.get()).isTrue() + } + + @Test + fun awaitShutdownHookStopsServerAndPruner() { + val cmd = command(noFetch = true).apply { cacheMaxEntries = 5 } + val server = + cmd + .buildAndStartServer(FakeGitClient(), LocalDiskHashCacheStorage(cmd.cacheDir)) + .also { startedServers += it } + + val hooks = mutableListOf() + cmd.awaitShutdown( + server, + registerShutdownHook = { hooks.add(it) }, + await = { latch -> + assertThat(hooks).hasSize(1) + hooks[0].run() + assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue() + }) + } + + @Test + fun awaitShutdownStopsServerOnInterrupt() { + val cmd = command(noFetch = true).apply { cacheMaxEntries = 5 } + val server = + cmd + .buildAndStartServer(FakeGitClient(), LocalDiskHashCacheStorage(cmd.cacheDir)) + .also { startedServers += it } + + cmd.awaitShutdown( + server, + registerShutdownHook = {}, + await = { throw InterruptedException("test shutdown") }) + + assertThat(Thread.interrupted()).isTrue() // clears the flag restored by awaitShutdown + } + + /** + * Stub bazel binary for [ServeCommand.call]: [hasherModule] eagerly runs `bazel info + * output_base`, so a real binary is not required as long as this script prints a path. + */ + private fun fakeBazelBinary(): File = + File(temp.root, "fake-bazel").apply { + writeText("#!/bin/sh\necho '${temp.root.absolutePath}/fake-output-base'\n") + setExecutable(true) + } + + /** + * Overrides [ServeCommand.awaitShutdown] so [ServeCommand.call] can be exercised end-to-end + * without blocking the test on a JVM shutdown signal. + */ + private inner class ServeCommandUnderTest : ServeCommand() { + val startedServer = AtomicReference(null) + val fakeGit = FakeGitClient() + + override fun createGitClient(): GitClient = fakeGit + + override fun awaitShutdown( + server: com.bazel_diff.server.BazelDiffServer, + registerShutdownHook: (Thread) -> Unit, + await: (CountDownLatch) -> Unit, + ) { + startedServer.set(server) + startedServers += server + // Mirror production cleanup without hanging: stop the pruner via the real hook body, then + // return so call() can unwind stopKoin(). + cachePrunerStopViaHook(server) + } + + private fun cachePrunerStopViaHook(server: com.bazel_diff.server.BazelDiffServer) { + // Invoke the production awaitShutdown hook path with an immediate await so call()'s + // surrounding try/finally is what we are really covering here; the dedicated awaitShutdown* + // tests cover both branches in isolation. + super.awaitShutdown( + server, + registerShutdownHook = { hook -> hook.run() }, + await = { latch -> latch.await(5, TimeUnit.SECONDS) }, + ) + } + } + + @Test + fun callStartsServerAndReturnsOkWithoutHanging() { + val ws = temp.newFolder("ws-call") + val cache = temp.newFolder("cache-call") + val bazel = fakeBazelBinary() + val underTest = ServeCommandUnderTest() + + val exit = + CommandLine( + BazelDiff(), + object : CommandLine.IFactory { + override fun create(cls: Class): K { + @Suppress("UNCHECKED_CAST") + if (ServeCommand::class.java.isAssignableFrom(cls)) return underTest as K + return CommandLine.defaultFactory().create(cls) + } + }) + .execute( + "serve", + "--workspacePath", + ws.absolutePath, + "--cacheDir", + cache.absolutePath, + "--bazelPath", + bazel.absolutePath, + "--port", + "0", + "--no-initial-fetch", + "--cacheMaxEntries", + "3", + "--requestTimeout", + "30", + "--keep_going", + "--cqueryExpression", + "deps(//...)", + "--excludeExternalTargets", + ) + + assertThat(exit).isEqualTo(CommandLine.ExitCode.OK) + assertThat(underTest.fakeGit.fetched).isFalse() + assertThat(underTest.startedServer.get()).isNotNull() + } } diff --git a/cli/src/test/kotlin/com/bazel_diff/cli/VersionProviderTest.kt b/cli/src/test/kotlin/com/bazel_diff/cli/VersionProviderTest.kt index 4eaa7ba8..ff4e78ee 100644 --- a/cli/src/test/kotlin/com/bazel_diff/cli/VersionProviderTest.kt +++ b/cli/src/test/kotlin/com/bazel_diff/cli/VersionProviderTest.kt @@ -2,7 +2,11 @@ package com.bazel_diff.cli import assertk.assertThat import assertk.assertions.hasSize +import assertk.assertions.isEqualTo import assertk.assertions.isNotEmpty +import java.io.ByteArrayInputStream +import java.io.InputStream +import org.junit.Assert.assertThrows import org.junit.Test class VersionProviderTest { @@ -16,4 +20,29 @@ class VersionProviderTest { assertThat(versions).hasSize(1) assertThat(versions[0]).isNotEmpty() } + + @Test + fun missingResourceThrows() { + val emptyLoader = + object : ClassLoader() { + override fun getResourceAsStream(name: String): InputStream? = null + } + assertThrows(IllegalArgumentException::class.java) { + VersionProvider(emptyLoader).getVersion() + } + } + + @Test + fun fallsBackToVersionResourceWhenCliVersionAbsent() { + val loader = + object : ClassLoader() { + override fun getResourceAsStream(name: String): InputStream? = + when (name) { + "cli/version" -> null + "version" -> ByteArrayInputStream("1.2.3-fallback\n".toByteArray()) + else -> null + } + } + assertThat(VersionProvider(loader).getVersion().toList()).isEqualTo(listOf("1.2.3-fallback")) + } } diff --git a/cli/src/test/kotlin/com/bazel_diff/cli/WarmupCommandTest.kt b/cli/src/test/kotlin/com/bazel_diff/cli/WarmupCommandTest.kt index fbcbd107..2185f659 100644 --- a/cli/src/test/kotlin/com/bazel_diff/cli/WarmupCommandTest.kt +++ b/cli/src/test/kotlin/com/bazel_diff/cli/WarmupCommandTest.kt @@ -3,10 +3,12 @@ package com.bazel_diff.cli import assertk.assertThat import assertk.assertions.contains import assertk.assertions.isEqualTo +import assertk.assertions.isFalse import java.io.File import org.junit.Rule import org.junit.Test import org.junit.rules.TemporaryFolder +import picocli.CommandLine class WarmupCommandTest { @get:Rule val temp: TemporaryFolder = TemporaryFolder() @@ -17,6 +19,32 @@ class WarmupCommandTest { setExecutable(true) } + /** + * Stubs [WarmupCommand.runGenerateHashes] so [WarmupCommand.call] can be covered without invoking + * a real `bazel query`. + */ + private class WarmupCommandUnderTest : WarmupCommand() { + var generateHashesResult: Int = CommandLine.ExitCode.OK + var generateHashesCalled = false + + override fun runGenerateHashes(): Int { + generateHashesCalled = true + return generateHashesResult + } + } + + private fun underTest( + ws: File, + baseHashes: File, + fingerprint: File, + ): WarmupCommandUnderTest = + WarmupCommandUnderTest().apply { + workspacePath = ws.toPath() + bazelPath = fakeBazel("Build label: 8.5.1").toPath() + baseHashesPath = baseHashes + fingerprintOutputPath = fingerprint + } + @Test fun writeFingerprintEmitsJsonReflectingFlags() { val ws = temp.newFolder("ws") @@ -73,4 +101,37 @@ class WarmupCommandTest { val fpB = b.readLines().first { it.contains("\"fingerprint\"") } assert(fpA != fpB) { "fingerprint must change when --useCquery changes" } } + + @Test + fun callWritesFingerprintAfterSuccessfulGenerateHashes() { + val ws = temp.newFolder("ws-call-ok") + File(ws, ".bazelrc").writeText("common --x") + val snap = temp.newFolder("snap-ok") + val baseHashes = File(snap, "nested/base_hashes.json") + val fingerprint = File(snap, "nested/fingerprint.json") + val cmd = underTest(ws, baseHashes, fingerprint) + + assertThat(cmd.call()).isEqualTo(CommandLine.ExitCode.OK) + assertThat(cmd.generateHashesCalled).isEqualTo(true) + assertThat(cmd.outputPath).isEqualTo(baseHashes) + assertThat(baseHashes.parentFile.exists()).isEqualTo(true) + assertThat(fingerprint.exists()).isEqualTo(true) + assertThat(fingerprint.readText()).contains("\"fingerprint\"") + } + + @Test + fun callSkipsFingerprintWhenGenerateHashesFails() { + val ws = temp.newFolder("ws-call-fail") + val snap = temp.newFolder("snap-fail") + val baseHashes = File(snap, "base_hashes.json") + val fingerprint = File(snap, "fingerprint.json") + val cmd = + underTest(ws, baseHashes, fingerprint).apply { + generateHashesResult = CommandLine.ExitCode.SOFTWARE + } + + assertThat(cmd.call()).isEqualTo(CommandLine.ExitCode.SOFTWARE) + assertThat(cmd.generateHashesCalled).isEqualTo(true) + assertThat(fingerprint.exists()).isFalse() + } } diff --git a/cli/src/test/kotlin/com/bazel_diff/hash/BuildGraphHasherTest.kt b/cli/src/test/kotlin/com/bazel_diff/hash/BuildGraphHasherTest.kt index 654f372e..16c69016 100644 --- a/cli/src/test/kotlin/com/bazel_diff/hash/BuildGraphHasherTest.kt +++ b/cli/src/test/kotlin/com/bazel_diff/hash/BuildGraphHasherTest.kt @@ -389,6 +389,93 @@ class BuildGraphHasherTest : KoinTest { assertThat(fakeSourceFileHasher.softDigestCalls.get()).isEqualTo(1) } + @Test + fun labelToPackageExtractsPackagePortion() { + assertThat(labelToPackage("//pkg:a")).isEqualTo("//pkg") + assertThat(labelToPackage("//:logo")).isEqualTo("//") + assertThat(labelToPackage("@@repo//pkg:a")).isEqualTo("@@repo//pkg") + assertThat(labelToPackage("//pkg")).isEqualTo("//pkg") + } + + @Test + fun generatedFileWithoutGeneratorThrows() = runBlocking { + declareMock() + val orphan = createGeneratedTarget("gen0", "missing_generator") + whenever(bazelClientMock.queryAllTargets()).thenReturn(listOf(orphan)) + + assertFailure { hasher.hashAllBazelTargetsAndSourcefiles() } + .all { + isInstanceOf(RuntimeException::class) + message().matchesPredicate { + it != null && it.contains("Not possible to traverse the build graph") + } + } + } + + @Test + fun hasherPhaseTimingsArePopulated() = runBlocking { + declareMock() + whenever(bazelClientMock.queryAllTargets()).thenReturn(defaultTargets) + + val timings = HasherPhaseTimings() + hasher.hashAllBazelTargetsAndSourcefiles(timings = timings) + + assertThat(timings.bazelQueryMillis).isGreaterThanOrEqualTo(0) + assertThat(timings.sourceHashMillis).isGreaterThanOrEqualTo(0) + assertThat(timings.targetHashMillis).isGreaterThanOrEqualTo(0) + } + + @Test + fun instantiationStacksDisablePackageBzlSeeds() = runBlocking { + declareMock() + val buildSrc = + createSrcTarget( + name = "//pkg:BUILD.bazel", + digest = "build", + subincludes = listOf("//pkg:macro.bzl")) + val ruleWithoutStack = + createRuleTarget(name = "//pkg:lib", inputs = emptyList(), digest = "digest") + whenever(bazelClientMock.queryAllTargets()).thenReturn(listOf(buildSrc, ruleWithoutStack)) + + fakeSourceFileHasher.softDigestCalls.set(0) + hasher.hashAllBazelTargetsAndSourcefiles() + val callsWithoutStacks = fakeSourceFileHasher.softDigestCalls.get() + assertThat(callsWithoutStacks).isGreaterThan(0) + + val ruleWithStack = + createRuleTarget( + name = "//pkg:lib", + inputs = emptyList(), + digest = "digest", + instantiationStack = listOf("pkg/macro.bzl:1:1: macro")) + whenever(bazelClientMock.queryAllTargets()).thenReturn(listOf(buildSrc, ruleWithStack)) + + fakeSourceFileHasher.softDigestCalls.set(0) + hasher.hashAllBazelTargetsAndSourcefiles() + // Package seeds are skipped; only RuleHasher's per-macro softDigest runs (once). + assertThat(fakeSourceFileHasher.softDigestCalls.get()).isEqualTo(1) + } + + @Test + fun externalSubincludesAreSkippedInPackageBzlSeeds() = runBlocking { + declareMock() + val buildSrc = + createSrcTarget( + name = "//pkg:BUILD.bazel", + digest = "build", + subincludes = listOf("@external_repo//:defs.bzl", "//pkg:local.bzl")) + val rule = createRuleTarget(name = "//pkg:lib", inputs = emptyList(), digest = "digest") + whenever(bazelClientMock.queryAllTargets()).thenReturn(listOf(buildSrc, rule)) + + fakeSourceFileHasher.softDigestCalls.set(0) + val hash = hasher.hashAllBazelTargetsAndSourcefiles() + + // softDigest is still invoked for both labels, but the external one returns null and is + // dropped from the seed — hashing still succeeds and produces a stable package entry. + assertThat(fakeSourceFileHasher.softDigestCalls.get()).isEqualTo(2) + assertThat(hash.containsKey("//pkg:lib")).isEqualTo(true) + } + private fun createRuleTarget( name: String, inputs: List, @@ -414,13 +501,17 @@ class BuildGraphHasherTest : KoinTest { return target } - private fun createSrcTarget(name: String, digest: String): BazelTarget { + private fun createSrcTarget( + name: String, + digest: String, + subincludes: List = emptyList() + ): BazelTarget { fakeSourceFileHasher.add(name, digest.toByteArray()) val target = mock() whenever(target.name).thenReturn(name) whenever(target.sourceFileName).thenReturn(name) - whenever(target.subincludeList).thenReturn(listOf()) + whenever(target.subincludeList).thenReturn(subincludes) return target } } diff --git a/cli/src/test/kotlin/com/bazel_diff/hash/FakeSourceFileHasher.kt b/cli/src/test/kotlin/com/bazel_diff/hash/FakeSourceFileHasher.kt index 09838e9b..fa3f951b 100644 --- a/cli/src/test/kotlin/com/bazel_diff/hash/FakeSourceFileHasher.kt +++ b/cli/src/test/kotlin/com/bazel_diff/hash/FakeSourceFileHasher.kt @@ -22,6 +22,11 @@ class FakeSourceFileHasher : SourceFileHasher { modifiedFilepaths: Set ): ByteArray? { softDigestCalls.incrementAndGet() + // Mirror SourceFileHasherImpl: external-repo labels never contribute a soft digest. + if (sourceFileTarget.name.startsWith("@") && !sourceFileTarget.name.startsWith("@//") && + !sourceFileTarget.name.startsWith("@@//")) { + return null + } return softDigestValue } diff --git a/cli/src/test/kotlin/com/bazel_diff/hash/RuleHasherTest.kt b/cli/src/test/kotlin/com/bazel_diff/hash/RuleHasherTest.kt new file mode 100644 index 00000000..35989fea --- /dev/null +++ b/cli/src/test/kotlin/com/bazel_diff/hash/RuleHasherTest.kt @@ -0,0 +1,463 @@ +package com.bazel_diff.hash + +import assertk.assertFailure +import assertk.assertThat +import assertk.assertions.contains +import assertk.assertions.isEqualTo +import assertk.assertions.isInstanceOf +import assertk.assertions.isNotEqualTo +import assertk.assertions.isNotNull +import assertk.assertions.isNull +import com.bazel_diff.bazel.BazelRule +import com.bazel_diff.extensions.toHexString +import com.bazel_diff.log.Logger +import com.bazel_diff.testModule +import com.google.devtools.build.lib.query2.proto.proto2api.Build +import com.google.devtools.build.lib.query2.proto.proto2api.Build.Attribute +import java.util.concurrent.ConcurrentHashMap +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.koin.dsl.module +import org.koin.test.KoinTest +import org.koin.test.KoinTestRule +import org.koin.test.mock.MockProviderRule +import org.koin.test.mock.declareMock +import org.mockito.Mockito +import org.mockito.junit.MockitoJUnit +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.atLeastOnce +import org.mockito.kotlin.verify + +/** + * Unit coverage for [RuleHasher.digest] paths not already pinned by + * [RuleHasherAlwaysAffectedTagsTest]. + */ +class RuleHasherTest : KoinTest { + @get:Rule val mockitoRule = MockitoJUnit.rule() + + @get:Rule val mockProvider = MockProviderRule.create { clazz -> Mockito.mock(clazz.java) } + + private val fake = FakeSourceFileHasher() + + @get:Rule + val koinTestRule = + KoinTestRule.create { + modules( + testModule(), + module { single { fake } }, + ) + } + + @Before + fun setUp() { + fake.softDigestValue = "fake-soft-digest".toByteArray() + fake.softDigestCalls.set(0) + fake.fakeDigests.clear() + } + + private fun rule( + name: String, + ruleClass: String = "sh_test", + ruleInputs: List = emptyList(), + configuredRuleInputs: List> = emptyList(), + visibility: List = emptyList(), + instantiationStack: List = emptyList(), + tags: List = emptyList(), + ): BazelRule { + val builder = Build.Rule.newBuilder().setName(name).setRuleClass(ruleClass) + ruleInputs.forEach { builder.addRuleInput(it) } + configuredRuleInputs.forEach { (label, checksum) -> + builder.addConfiguredRuleInput( + Build.ConfiguredRuleInput.newBuilder() + .setLabel(label) + .setConfigurationChecksum(checksum) + .build()) + } + if (visibility.isNotEmpty()) { + val attr = + Attribute.newBuilder() + .setName("visibility") + .setType(Attribute.Discriminator.STRING_LIST) + visibility.forEach { attr.addStringListValue(it) } + builder.addAttribute(attr.build()) + } + if (tags.isNotEmpty()) { + val attr = + Attribute.newBuilder().setName("tags").setType(Attribute.Discriminator.STRING_LIST) + tags.forEach { attr.addStringListValue(it) } + builder.addAttribute(attr.build()) + } + instantiationStack.forEach { builder.addInstantiationStack(it) } + return BazelRule(builder.build()) + } + + private fun packageGroup(name: String, packages: List = listOf("//...")): BazelRule { + val attr = + Attribute.newBuilder() + .setName("packages") + .setType(Attribute.Discriminator.STRING_LIST) + packages.forEach { attr.addStringListValue(it) } + return BazelRule( + Build.Rule.newBuilder() + .setName(name) + .setRuleClass("package_group") + .addAttribute(attr.build()) + .build()) + } + + private fun hasher( + useCquery: Boolean = false, + trackDepLabels: Boolean = true, + alwaysAffectedTags: Set = emptySet(), + alwaysAffectedSeed: ByteArray = ByteArray(0), + ): RuleHasher = + RuleHasher( + useCquery = useCquery, + trackDepLabels = trackDepLabels, + fineGrainedHashExternalRepos = emptySet(), + alwaysAffectedTags = alwaysAffectedTags, + alwaysAffectedSeed = alwaysAffectedSeed) + + private fun digest( + hasher: RuleHasher = hasher(), + rule: BazelRule, + allRulesMap: Map = mapOf(rule.name to rule), + ruleHashes: ConcurrentHashMap = ConcurrentHashMap(), + sourceDigests: ConcurrentHashMap = ConcurrentHashMap(), + seedHash: ByteArray? = ByteArray(0), + packageBzlSeeds: Map = emptyMap(), + depPath: LinkedHashSet? = null, + ignoredAttrs: Set = emptySet(), + hashInvocationContext: HashInvocationContext = HashInvocationContext(), + ): TargetDigest = + hasher.digest( + rule, + allRulesMap, + ruleHashes, + sourceDigests, + seedHash, + packageBzlSeeds, + depPath, + ignoredAttrs, + emptySet(), + hashInvocationContext) + + @Test + fun circularDependencySelfLoopThrows() { + val a = rule("//pkg:a") + assertFailure { + digest(rule = a, depPath = linkedSetOf("//pkg:a")) + } + .isInstanceOf(RuleHasher.CircularDependencyException::class) + .transform { it.message!! } + .contains("//pkg:a -> //pkg:a") + } + + @Test + fun circularDependencyMultiHopThrows() { + val a = rule("//pkg:a", ruleInputs = listOf("//pkg:b")) + val b = rule("//pkg:b", ruleInputs = listOf("//pkg:a")) + val rules = mapOf(a.name to a, b.name to b) + + assertFailure { digest(rule = a, allRulesMap = rules) } + .isInstanceOf(RuleHasher.CircularDependencyException::class) + .transform { it.message!! } + .contains("//pkg:a -> //pkg:b -> //pkg:a") + } + + @Test + fun memoizationReturnsCachedDigest() { + val leaf = rule("//pkg:leaf") + val ruleHashes = ConcurrentHashMap() + val first = digest(rule = leaf, ruleHashes = ruleHashes) + assertThat(first.overallDigest.toHexString()) + .isEqualTo(ruleHashes[leaf.name]!!.overallDigest.toHexString()) + + val cached = + TargetDigest("cached-overall".toByteArray(), "cached-direct".toByteArray(), emptyList()) + ruleHashes[leaf.name] = cached + + val second = digest(rule = leaf, ruleHashes = ruleHashes) + assertThat(second.overallDigest.toHexString()).isEqualTo(cached.overallDigest.toHexString()) + assertThat(second.directDigest.toHexString()).isEqualTo(cached.directDigest.toHexString()) + assertThat(second.overallDigest.toHexString()) + .isNotEqualTo(first.overallDigest.toHexString()) + } + + @Test + fun emptyInstantiationStackFallsBackToPackageBzlSeeds() { + val leaf = rule("//pkg:leaf") + val withSeed = + digest( + rule = leaf, + packageBzlSeeds = mapOf("//pkg" to "package-seed-v1".toByteArray())) + val withOtherSeed = + digest( + rule = leaf, + packageBzlSeeds = mapOf("//pkg" to "package-seed-v2".toByteArray())) + val withoutSeed = digest(rule = leaf, packageBzlSeeds = emptyMap()) + + assertThat(withSeed.overallDigest.toHexString()) + .isNotEqualTo(withOtherSeed.overallDigest.toHexString()) + assertThat(withSeed.overallDigest.toHexString()) + .isNotEqualTo(withoutSeed.overallDigest.toHexString()) + } + + @Test + fun nonEmptyInstantiationStackUsesRuleBzlSeedAndFiltersExternalPaths() { + // Main-repo .bzl/.scl frames contribute; external/@/../ frames are ignored. + val withMainRepo = + rule( + "//pkg:lib", + instantiationStack = + listOf( + "tools/macro.bzl:1:1: my_macro", + "defs/helper.scl:2:3: helper", + "external/foo.bzl:1:1: ext", + "@repo//bar.bzl:1:1: remote", + "../outside.bzl:1:1: parent", + "BUILD:1:1: ", + )) + val externalOnly = + rule( + "//pkg:lib", + instantiationStack = + listOf( + "external/foo.bzl:1:1: ext", + "@repo//bar.bzl:1:1: remote", + "../outside.bzl:1:1: parent", + )) + + fake.softDigestValue = "bzl-v1".toByteArray() + val mainV1 = digest(rule = withMainRepo) + fake.softDigestValue = "bzl-v2".toByteArray() + val mainV2 = digest(rule = withMainRepo) + fake.softDigestValue = "bzl-v1".toByteArray() + val externalV1 = digest(rule = externalOnly) + fake.softDigestValue = "bzl-v2".toByteArray() + val externalV2 = digest(rule = externalOnly) + + // Soft-digest of main-repo .bzl/.scl files is mixed in. + assertThat(mainV1.overallDigest.toHexString()).isNotEqualTo(mainV2.overallDigest.toHexString()) + // Filtered-out frames produce a stable empty seed independent of softDigest. + assertThat(externalV1.overallDigest.toHexString()) + .isEqualTo(externalV2.overallDigest.toHexString()) + // Non-null rule seed means packageBzlSeeds are NOT used as a fallback. + val withPackageSeed = + digest( + rule = externalOnly, + packageBzlSeeds = mapOf("//pkg" to "should-not-apply".toByteArray())) + assertThat(withPackageSeed.overallDigest.toHexString()) + .isEqualTo(externalV1.overallDigest.toHexString()) + } + + @Test + fun sourceDigestsUsedWhenInputIsNotARule() { + val consumer = rule("//pkg:consumer", ruleInputs = listOf("//pkg:src.txt")) + val sources = ConcurrentHashMap() + sources["//pkg:src.txt"] = "src-digest-v1".toByteArray() + + val v1 = digest(rule = consumer, sourceDigests = sources) + sources["//pkg:src.txt"] = "src-digest-v2".toByteArray() + val v2 = digest(rule = consumer, sourceDigests = sources) + + assertThat(v1.overallDigest.toHexString()).isNotEqualTo(v2.overallDigest.toHexString()) + } + + @Test + fun recursiveRuleDependencyChangesTransitiveDigest() { + val depV1 = rule("//pkg:dep") + val depV2 = + BazelRule( + Build.Rule.newBuilder() + .setName("//pkg:dep") + .setRuleClass("sh_test") + .addAttribute( + Attribute.newBuilder() + .setName("cmd") + .setType(Attribute.Discriminator.STRING) + .setStringValue("changed") + .build()) + .build()) + val consumer = rule("//pkg:consumer", ruleInputs = listOf("//pkg:dep")) + + val hashV1 = + digest(rule = consumer, allRulesMap = mapOf(consumer.name to consumer, depV1.name to depV1)) + val hashV2 = + digest(rule = consumer, allRulesMap = mapOf(consumer.name to consumer, depV2.name to depV2)) + + assertThat(hashV1.overallDigest.toHexString()) + .isNotEqualTo(hashV2.overallDigest.toHexString()) + // Direct digest excludes transitive dep content. + assertThat(hashV1.directDigest.toHexString()).isEqualTo(hashV2.directDigest.toHexString()) + } + + @Test + fun heuristicSoftDigestLogsInfoAndCachesSourceDigest() { + val logger = declareMock() + val consumer = rule("//pkg:consumer", ruleInputs = listOf("//pkg:unknown.src")) + val sourceDigests = ConcurrentHashMap() + fake.softDigestValue = "heuristic-bytes".toByteArray() + + val result = digest(rule = consumer, sourceDigests = sourceDigests) + + assertThat(result).isNotNull() + assertThat(sourceDigests["//pkg:unknown.src"]!!.toHexString()) + .isEqualTo("heuristic-bytes".toByteArray().toHexString()) + assertThat(fake.softDigestCalls.get()).isEqualTo(1) + + val infoCaptor = argumentCaptor<() -> String>() + verify(logger, atLeastOnce()).i(infoCaptor.capture()) + assertThat(infoCaptor.allValues.any { it().contains("//pkg:unknown.src") }).isEqualTo(true) + } + + @Test + fun nullSoftDigestLogsWarning() { + val logger = declareMock() + val consumer = rule("//pkg:consumer", ruleInputs = listOf("//pkg:missing.src")) + fake.softDigestValue = null + + digest(rule = consumer) + + val warnCaptor = argumentCaptor<() -> String>() + verify(logger, atLeastOnce()).w(warnCaptor.capture()) + assertThat(warnCaptor.allValues.any { it().contains("//pkg:missing.src") }).isEqualTo(true) + } + + @Test + fun packageGroupVisibilityEdgesAreFollowed() { + val group = + packageGroup("//pkg:consumers", packages = listOf("//allowed")) + val gated = + rule("//pkg:gated", visibility = listOf("//pkg:consumers", "//visibility:public")) + val rules = mapOf(gated.name to gated, group.name to group) + + val hashWithGroup = digest(rule = gated, allRulesMap = rules) + val hashWithoutGroup = digest(rule = gated, allRulesMap = mapOf(gated.name to gated)) + assertThat(hashWithGroup.overallDigest.toHexString()) + .isNotEqualTo(hashWithoutGroup.overallDigest.toHexString()) + + // Changing the package_group attribute changes the gated rule's transitive digest. + val groupChanged = + packageGroup("//pkg:consumers", packages = listOf("//other")) + val hashChanged = + digest(rule = gated, allRulesMap = mapOf(gated.name to gated, groupChanged.name to groupChanged)) + assertThat(hashChanged.overallDigest.toHexString()) + .isNotEqualTo(hashWithGroup.overallDigest.toHexString()) + } + + @Test + fun packageGroupVisibilitySkippedWhenVisibilityIgnored() { + val group = packageGroup("//pkg:consumers") + val gated = rule("//pkg:gated", visibility = listOf("//pkg:consumers")) + val rules = mapOf(gated.name to gated, group.name to group) + + val followed = digest(rule = gated, allRulesMap = rules, ignoredAttrs = emptySet()) + val ignored = + digest(rule = gated, allRulesMap = rules, ignoredAttrs = setOf("visibility")) + // Ignoring visibility both drops the attribute from rule.digest and skips the edge walk. + assertThat(followed.overallDigest.toHexString()) + .isNotEqualTo(ignored.overallDigest.toHexString()) + + val ignoredWithoutGroup = + digest( + rule = gated, + allRulesMap = mapOf(gated.name to gated), + ignoredAttrs = setOf("visibility")) + assertThat(ignored.overallDigest.toHexString()) + .isEqualTo(ignoredWithoutGroup.overallDigest.toHexString()) + } + + @Test + fun packageGroupVisibilitySkipsMissingAndNonPackageGroupLabels() { + val notAGroup = rule("//pkg:not_a_group") + val gated = + rule( + "//pkg:gated", + visibility = listOf("//pkg:missing_group", "//pkg:not_a_group", "//pkg:gated")) + // Self label filtered by name==rule.name; missing continues; non-package_group continues. + val result = + digest( + rule = gated, + allRulesMap = mapOf(gated.name to gated, notAGroup.name to notAGroup)) + assertThat(result.deps).isEqualTo(emptyList()) + } + + @Test + fun useCqueryMixesConfiguredRuleInputEncoding() { + val dep = rule("//pkg:dep") + val consumerA = + rule( + "//pkg:consumer", + configuredRuleInputs = listOf("//pkg:dep" to "cfg-A")) + val consumerB = + rule( + "//pkg:consumer", + configuredRuleInputs = listOf("//pkg:dep" to "cfg-B")) + val hasher = hasher(useCquery = true) + + val hashA = + digest( + hasher = hasher, + rule = consumerA, + allRulesMap = mapOf(consumerA.name to consumerA, dep.name to dep)) + val hashB = + digest( + hasher = hasher, + rule = consumerB, + allRulesMap = mapOf(consumerB.name to consumerB, dep.name to dep)) + + assertThat(hashA.overallDigest.toHexString()).isNotEqualTo(hashB.overallDigest.toHexString()) + // Bare label is tracked in deps (checksum stripped). + assertThat(hashA.deps).isEqualTo(listOf("//pkg:dep")) + } + + @Test + fun trackDepLabelsPopulatesDeps() { + val dep = rule("//pkg:dep") + val consumer = rule("//pkg:consumer", ruleInputs = listOf("//pkg:dep")) + val rules = mapOf(consumer.name to consumer, dep.name to dep) + + val tracked = + digest(hasher = hasher(trackDepLabels = true), rule = consumer, allRulesMap = rules) + val untracked = + digest(hasher = hasher(trackDepLabels = false), rule = consumer, allRulesMap = rules) + + assertThat(tracked.deps).isEqualTo(listOf("//pkg:dep")) + assertThat(untracked.deps).isNull() + } + + @Test + fun selfReferenceInRuleInputsSkippedSafely() { + val self = rule("//pkg:self", ruleInputs = listOf("//pkg:self")) + fake.softDigestValue = null + + // Does not recurse into itself (would cycle); falls through to heuristic and completes. + val result = digest(rule = self, allRulesMap = mapOf(self.name to self)) + assertThat(result.overallDigest).isNotNull() + assertThat(result.deps).isEqualTo(emptyList()) + } + + @Test + fun alwaysAffectedSeedIsMixedIntoDirectDigest() { + val tagged = rule("//pkg:lint", tags = listOf("external")) + val a = + digest( + hasher = + hasher( + alwaysAffectedTags = setOf("external"), + alwaysAffectedSeed = "seed-A".toByteArray()), + rule = tagged) + val b = + digest( + hasher = + hasher( + alwaysAffectedTags = setOf("external"), + alwaysAffectedSeed = "seed-B".toByteArray()), + rule = tagged) + + assertThat(a.overallDigest.toHexString()).isNotEqualTo(b.overallDigest.toHexString()) + assertThat(a.directDigest.toHexString()).isNotEqualTo(b.directDigest.toHexString()) + } +} diff --git a/cli/src/test/kotlin/com/bazel_diff/hash/SourceFileHasherTest.kt b/cli/src/test/kotlin/com/bazel_diff/hash/SourceFileHasherTest.kt index c80a8113..c17c16ca 100644 --- a/cli/src/test/kotlin/com/bazel_diff/hash/SourceFileHasherTest.kt +++ b/cli/src/test/kotlin/com/bazel_diff/hash/SourceFileHasherTest.kt @@ -341,4 +341,104 @@ internal class SourceFileHasherTest : KoinTest { Files.deleteIfExists(testDir.resolve("path")) Files.deleteIfExists(testDir) } + + @Test + fun testHashAtSlashMainRepoLabel() = runBlocking { + val hasher = SourceFileHasherImpl(repoAbsolutePath, null, externalRepoResolver) + val target = "@//cli/src/test/kotlin/com/bazel_diff/hash/fixture:foo.ts" + val bazelSourceFileTarget = BazelSourceFileTarget(target, seed) + val actual = hasher.digest(bazelSourceFileTarget).toHexString() + val expected = + sha256 { + safePutBytes(fixtureFileContent) + putBytes(byteArrayOf(0x01)) + putBytes(byteArrayOf(0x00)) + safePutBytes(seed) + safePutBytes(target.toByteArray()) + } + .toHexString() + assertThat(actual).isEqualTo(expected) + } + + @Test + fun testHashDoubleAtSlashMainRepoLabel() = runBlocking { + val hasher = SourceFileHasherImpl(repoAbsolutePath, null, externalRepoResolver) + val target = "@@//cli/src/test/kotlin/com/bazel_diff/hash/fixture:foo.ts" + val bazelSourceFileTarget = BazelSourceFileTarget(target, seed) + val actual = hasher.digest(bazelSourceFileTarget).toHexString() + val expected = + sha256 { + safePutBytes(fixtureFileContent) + putBytes(byteArrayOf(0x01)) + putBytes(byteArrayOf(0x00)) + safePutBytes(seed) + safePutBytes(target.toByteArray()) + } + .toHexString() + assertThat(actual).isEqualTo(expected) + } + + @Test + fun testHashInvalidExternalLabelReturnsEmptyDigest() = runBlocking { + val hasher = SourceFileHasherImpl(repoAbsolutePath, null, externalRepoResolver) + val target = "@not_a_valid_label" + val actual = hasher.digest(BazelSourceFileTarget(target, seed)).toHexString() + assertThat(actual).isEqualTo(sha256 {}.toHexString()) + } + + @Test + fun testHashNonFineGrainedExternalRepoIsSkipped() = runBlocking { + // Repo exists on disk but is not in fineGrainedHashExternalRepos — digest must stay empty + // (seed/name are never mixed in after the early return). + val externalRepoFilePath = outputBasePath.resolve("external/other_repo/path/to/file.txt") + Files.createDirectories(externalRepoFilePath.parent) + externalRepoFilePath.toFile().writeText("ignored") + val hasher = SourceFileHasherImpl(repoAbsolutePath, null, externalRepoResolver, emptySet()) + val target = "@other_repo//path/to:file.txt" + val actual = hasher.digest(BazelSourceFileTarget(target, seed)).toHexString() + assertThat(actual).isEqualTo(sha256 {}.toHexString()) + } + + @Test + fun testSoftDigestNullForNonMainRepoLabel() = runBlocking { + val hasher = SourceFileHasherImpl(repoAbsolutePath, null, externalRepoResolver, setOf("ext")) + assertThat(hasher.softDigest(BazelSourceFileTarget("@ext//:file.txt", seed))).isNull() + assertThat(hasher.softDigest(BazelSourceFileTarget("not-a-label", seed))).isNull() + } + + @Test + fun testSoftDigestNullForDirectory() = runBlocking { + val testDir = Files.createTempDirectory("soft_digest_dir") + val dirPath = testDir.resolve("path/to/dir") + Files.createDirectories(dirPath) + val hasher = SourceFileHasherImpl(testDir, null, externalRepoResolver) + assertThat(hasher.softDigest(BazelSourceFileTarget("//path/to:dir", seed))).isNull() + } + + @Test + fun testKoinInjectedConstructor() = runBlocking { + // Covers the secondary constructor that resolves workingDirectory / ContentHashProvider / + // ExternalRepoResolver from Koin (lines unused by the explicit-arg constructor tests). + val hasher = SourceFileHasherImpl(setOf("external_repo")) + val target = "//cli/src/test/kotlin/com/bazel_diff/hash/fixture:foo.ts" + // working-directory from testModule is "working-directory", so the fixture is missing — + // still exercises the inject path and produces a stable missing-file digest. + val actual = hasher.digest(BazelSourceFileTarget(target, seed)).toHexString() + val expected = + sha256 { + putBytes(byteArrayOf(0x00)) + safePutBytes(seed) + safePutBytes(target.toByteArray()) + } + .toHexString() + assertThat(actual).isEqualTo(expected) + } + + @Test + fun testHashUnrecognizedLabelFormReturnsEmptyDigest() = runBlocking { + val hasher = SourceFileHasherImpl(repoAbsolutePath, null, externalRepoResolver) + val target = "not-a-bazel-label" + val actual = hasher.digest(BazelSourceFileTarget(target, seed)).toHexString() + assertThat(actual).isEqualTo(sha256 {}.toHexString()) + } } diff --git a/cli/src/test/kotlin/com/bazel_diff/interactor/CalculateImpactedTargetsInteractorTest.kt b/cli/src/test/kotlin/com/bazel_diff/interactor/CalculateImpactedTargetsInteractorTest.kt index ee9d573a..d15de826 100644 --- a/cli/src/test/kotlin/com/bazel_diff/interactor/CalculateImpactedTargetsInteractorTest.kt +++ b/cli/src/test/kotlin/com/bazel_diff/interactor/CalculateImpactedTargetsInteractorTest.kt @@ -1,5 +1,7 @@ package com.bazel_diff.interactor +import assertk.all +import assertk.assertFailure import assertk.assertThat import assertk.assertions.* import com.bazel_diff.bazel.BazelQueryService @@ -7,6 +9,7 @@ import com.bazel_diff.bazel.BazelTarget import com.bazel_diff.hash.TargetHash import com.bazel_diff.testModule import java.io.StringWriter +import java.util.concurrent.ConcurrentHashMap import org.junit.Rule import org.junit.Test import org.koin.core.context.loadKoinModules @@ -1001,4 +1004,350 @@ class CalculateImpactedTargetsInteractorTest : KoinTest { assertThat(output).doesNotContain("//:unchanged_a") assertThat(output).doesNotContain("//:unchanged_b") } + + @Test + fun calculateDistanceThrowsForUnimpactedLabel() { + val interactor = CalculateImpactedTargetsInteractor() + val impactedTargets = ConcurrentHashMap() + val impactedLabels = + mapOf("//:impacted" to CalculateImpactedTargetsInteractor.ImpactType.DIRECT) + + assertFailure { + interactor.calculateDistance( + "//:not-impacted", emptyMap(), impactedTargets, impactedLabels) + } + .all { + isInstanceOf(IllegalArgumentException::class) + message().matchesPredicate { + it != null && it.contains("//:not-impacted was not impacted") + } + } + } + + @Test + fun calculateDistanceReturnsCachedMetricsWithoutRecalculating() { + val interactor = CalculateImpactedTargetsInteractor() + val cached = TargetDistanceMetrics(7, 3) + val impactedTargets = ConcurrentHashMap(mapOf("//:cached" to cached)) + val impactedLabels = + mapOf("//:cached" to CalculateImpactedTargetsInteractor.ImpactType.INDIRECT) + + val result = + interactor.calculateDistance("//:cached", emptyMap(), impactedTargets, impactedLabels) + + assertThat(result).isEqualTo(cached) + } + + @Test + fun testExecuteSortsUnknownTypesAfterKnownKinds() { + // kindRank: SourceFile=0, GeneratedFile=1, Rule=2, unknown non-empty=3, null/empty=4 + val startHashes = + mapOf( + "//pkg:zzz_unknown" to TargetHash("Aspect", "u", "u"), + "//pkg:aaa_empty" to TargetHash("", "e", "e"), + "//pkg:rule" to TargetHash("Rule", "r", "r"), + "//pkg:src" to TargetHash("SourceFile", "s", "s"), + ) + val endHashes = startHashes.mapValues { (_, v) -> v.copy(hash = v.hash + "-changed") } + + val outputWriter = StringWriter() + CalculateImpactedTargetsInteractor() + .execute( + from = startHashes, + to = endHashes, + outputWriter = outputWriter, + targetTypes = null, + ) + + val lines = outputWriter.toString().trimEnd('\n').split("\n") + assertThat(lines) + .containsExactly( + "//pkg:src", + "//pkg:rule", + "//pkg:zzz_unknown", + "//pkg:aaa_empty", + ) + } + + @Test + fun testOneSidedModuleGraphFallsBackToHashDiff() { + // detectChangedModules returns empty when either side is null, even if the other is present. + val startHashes = + mapOf( + "//:target1" to TargetHash("", "hash1", "hash1"), + "//:target2" to TargetHash("", "hash2", "hash2")) + val endHashes = + mapOf( + "//:target1" to TargetHash("", "hash1-changed", "hash1-changed"), + "//:target2" to TargetHash("", "hash2", "hash2")) + val moduleGraph = + """ + { + "key": "root", + "name": "root", + "version": "", + "apparentName": "root", + "dependencies": [ + {"key": "abseil-cpp@20240116.2", "name": "abseil-cpp", "version": "20240116.2", "apparentName": "abseil-cpp"} + ] + } + """ + .trimIndent() + + val outputWriter = StringWriter() + CalculateImpactedTargetsInteractor() + .execute( + from = startHashes, + to = endHashes, + outputWriter = outputWriter, + targetTypes = null, + fromModuleGraphJson = null, + toModuleGraphJson = moduleGraph) + + assertThat(outputWriter.toString().trim().split("\n")).containsExactly("//:target1") + } + + @Test + fun testRemovedModuleResolvesFromFromGraph() { + // A module present only in the from-graph (removed) still resolves via fromGraph fallback. + val startHashes = + mapOf( + "//:target1" to TargetHash("", "hash1", "hash1"), + "@@gone~1.0//:lib" to TargetHash("", "ext", "ext")) + val endHashes = + mapOf("//:target1" to TargetHash("", "hash1", "hash1")) + + val fromModuleGraph = + """ + { + "key": "root", "name": "root", "version": "", "apparentName": "root", + "dependencies": [ + {"key": "gone@1.0", "name": "gone", "version": "1.0", "apparentName": "gone"} + ] + } + """ + .trimIndent() + val toModuleGraph = + """ + { + "key": "root", "name": "root", "version": "", "apparentName": "root", + "dependencies": [] + } + """ + .trimIndent() + + val outputWriter = StringWriter() + // No query service — falls back to allTargets.keys when modules change. + CalculateImpactedTargetsInteractor() + .execute( + from = startHashes, + to = endHashes, + outputWriter = outputWriter, + targetTypes = null, + fromModuleGraphJson = fromModuleGraph, + toModuleGraphJson = toModuleGraph) + + val output = outputWriter.toString().trim().split("\n").filter { it.isNotEmpty() }.toSet() + assertThat(output).contains("//:target1") + } + + @Test + fun testPackageDistanceWithCachedIndirectPredecessor() { + // Exercise the cache hit path inside calculateDistance while computing package distance + // across a diamond where two paths share an intermediate node. + val (depEdges, startHashes) = + createTargetHashes( + "//A:1 <- //A:2 <- //B:4", + "//A:1 <- //B:3 <- //B:4", + ) + val endHashes = startHashes.toMutableMap() + makeDirectlyChanged(endHashes, "//A:1") + makeIndirectlyChanged(endHashes, "//A:2", "//B:3", "//B:4") + + val impacted = + CalculateImpactedTargetsInteractor().computeAllDistances(startHashes, endHashes, depEdges) + + assertThat(impacted["//B:4"]).isEqualTo(TargetDistanceMetrics(2, 1)) + } + + @Test + fun changedModuleWithNoMatchingReposFallsBackToHashDiff() { + // Query service is bound, but the changed module name matches no @@ canonical repo in + // allTargets — hits skippedNoMatch / empty moduleRepos and falls back to hash-diff. + val fakeQueryService: BazelQueryService = mock { + onBlocking { query(any(), any()) } doAnswer { emptyList() } + } + loadKoinModules(module { single { fakeQueryService } }) + + val from = + mapOf( + "//:unchanged" to TargetHash("Rule", "h", "h"), + "//:changed" to TargetHash("Rule", "old", "old"), + "@@other_mod~1.0//:lib" to TargetHash("Rule", "e", "e"), + ) + val to = + mapOf( + "//:unchanged" to TargetHash("Rule", "h", "h"), + "//:changed" to TargetHash("Rule", "new", "new"), + "@@other_mod~1.0//:lib" to TargetHash("Rule", "e", "e"), + ) + val fromGraph = + """ + { + "key": "root", "name": "root", "version": "", "apparentName": "root", + "dependencies": [ + {"key": "unmaterialised@1.0", "name": "unmaterialised", "version": "1.0", "apparentName": "unmaterialised"} + ] + } + """ + .trimIndent() + val toGraph = + """ + { + "key": "root", "name": "root", "version": "", "apparentName": "root", + "dependencies": [ + {"key": "unmaterialised@2.0", "name": "unmaterialised", "version": "2.0", "apparentName": "unmaterialised"} + ] + } + """ + .trimIndent() + + val outputWriter = StringWriter() + CalculateImpactedTargetsInteractor() + .execute( + from = from, + to = to, + outputWriter = outputWriter, + targetTypes = null, + fromModuleGraphJson = fromGraph, + toModuleGraphJson = toGraph, + ) + + assertThat(outputWriter.toString().trim().split("\n")).containsExactly("//:changed") + } + + @Test + fun moduleQueryFailureFallsBackToBuildableWorkspaceTargets() { + val fakeQueryService: BazelQueryService = mock { + onBlocking { query(any(), any()) } doAnswer + { + throw RuntimeException("simulated query failure") + } + } + loadKoinModules(module { single { fakeQueryService } }) + + val hashes = + mapOf( + "//app:app" to TargetHash("Rule", "a", "a"), + "//lib:util" to TargetHash("Rule", "b", "b"), + "@@abseil-cpp~20240116.2//:strings" to TargetHash("Rule", "c", "c"), + "//external:abseil-cpp" to TargetHash("Rule", "e", "e"), + ) + val fromGraph = + """ + { + "key": "root", "name": "root", "version": "", "apparentName": "root", + "dependencies": [ + {"key": "abseil-cpp@20240116.2", "name": "abseil-cpp", "version": "20240116.2", "apparentName": "abseil-cpp"} + ] + } + """ + .trimIndent() + val toGraph = + """ + { + "key": "root", "name": "root", "version": "", "apparentName": "root", + "dependencies": [ + {"key": "abseil-cpp@20240722.0", "name": "abseil-cpp", "version": "20240722.0", "apparentName": "abseil-cpp"} + ] + } + """ + .trimIndent() + + // Use the newer version in `to` so the canonical repo filter can still match the + // @@abseil-cpp~... label shape via the '+'/'~' base-repo predicate on the from side's + // materialised repo name prefix "abseil-cpp". + val toHashes = + mapOf( + "//app:app" to TargetHash("Rule", "a", "a"), + "//lib:util" to TargetHash("Rule", "b", "b"), + "@@abseil-cpp~20240722.0//:strings" to TargetHash("Rule", "c2", "c2"), + "//external:abseil-cpp" to TargetHash("Rule", "e", "e"), + ) + + val outputWriter = StringWriter() + CalculateImpactedTargetsInteractor() + .execute( + from = hashes, + to = toHashes, + outputWriter = outputWriter, + targetTypes = null, + fromModuleGraphJson = fromGraph, + toModuleGraphJson = toGraph, + ) + + val impacted = outputWriter.toString().trim().split("\n").filter { it.isNotEmpty() }.toSet() + // Catch fallback keeps buildable workspace targets; hash-diff also adds the version-bumped + // external label. + assertThat(impacted).contains("//app:app") + assertThat(impacted).contains("//lib:util") + } + + @Test + fun moduleQueryFailureOnBzlmodOnlyFallsBackToAllTargets() { + val fakeQueryService: BazelQueryService = mock { + onBlocking { query(any(), any()) } doAnswer + { + throw RuntimeException("simulated query failure") + } + } + loadKoinModules(module { single { fakeQueryService } }) + + val hashes = + mapOf( + "@@abseil-cpp~20240116.2//:strings" to TargetHash("Rule", "a", "a"), + "@@abseil-cpp~20240116.2//:base" to TargetHash("Rule", "b", "b"), + ) + val toHashes = + mapOf( + "@@abseil-cpp~20240722.0//:strings" to TargetHash("Rule", "a2", "a2"), + "@@abseil-cpp~20240722.0//:base" to TargetHash("Rule", "b2", "b2"), + ) + val fromGraph = + """ + { + "key": "root", "name": "root", "version": "", "apparentName": "root", + "dependencies": [ + {"key": "abseil-cpp@20240116.2", "name": "abseil-cpp", "version": "20240116.2", "apparentName": "abseil-cpp"} + ] + } + """ + .trimIndent() + val toGraph = + """ + { + "key": "root", "name": "root", "version": "", "apparentName": "root", + "dependencies": [ + {"key": "abseil-cpp@20240722.0", "name": "abseil-cpp", "version": "20240722.0", "apparentName": "abseil-cpp"} + ] + } + """ + .trimIndent() + + val outputWriter = StringWriter() + CalculateImpactedTargetsInteractor() + .execute( + from = hashes, + to = toHashes, + outputWriter = outputWriter, + targetTypes = null, + fromModuleGraphJson = fromGraph, + toModuleGraphJson = toGraph, + ) + + val impacted = outputWriter.toString().trim().split("\n").filter { it.isNotEmpty() }.toSet() + assertThat(impacted) + .containsExactlyInAnyOrder( + "@@abseil-cpp~20240722.0//:strings", "@@abseil-cpp~20240722.0//:base") + } } diff --git a/cli/src/test/kotlin/com/bazel_diff/interactor/DeserialiseHashesInteractorTest.kt b/cli/src/test/kotlin/com/bazel_diff/interactor/DeserialiseHashesInteractorTest.kt index 75319b88..9651f141 100644 --- a/cli/src/test/kotlin/com/bazel_diff/interactor/DeserialiseHashesInteractorTest.kt +++ b/cli/src/test/kotlin/com/bazel_diff/interactor/DeserialiseHashesInteractorTest.kt @@ -2,6 +2,7 @@ package com.bazel_diff.interactor import assertk.assertThat import assertk.assertions.isEqualTo +import assertk.assertions.isNull import com.bazel_diff.hash.TargetHash import com.bazel_diff.testModule import org.junit.Rule @@ -75,4 +76,59 @@ class DeserialiseHashesInteractorTest : KoinTest { assertThat(data.depEdges).isEqualTo(emptyMap()) } + + @Test + fun executeSimpleDeserialisesFlatStringMap() { + val file = temp.newFile().apply { writeText("""{"a/path":"abc","b/path":"def"}""") } + + assertThat(interactor.executeSimple(file)).isEqualTo(mapOf("a/path" to "abc", "b/path" to "def")) + } + + @Test + fun deserializeDepsReadsAdjacencyList() { + val file = + temp.newFile().apply { + writeText("""{"//a:lib":["//b:lib"], "//b:lib":[]}""") + } + + assertThat(interactor.deserializeDeps(file)) + .isEqualTo(mapOf("//a:lib" to listOf("//b:lib"), "//b:lib" to emptyList())) + } + + @Test + fun executeTargetHashWithMetadataReadsFileWithMetadata() { + val file = + temp.newFile().apply { + writeText( + """{ + | "hashes": {"//a:lib":"Rule#h1~d1"}, + | "metadata": { + | "moduleGraphJson": "{\"nodes\":[]}", + | "depEdges": {"//a:lib":[]} + | } + |}""" + .trimMargin()) + } + + val data = interactor.executeTargetHashWithMetadata(file) + + assertThat(data.hashes).isEqualTo(mapOf("//a:lib" to TargetHash("Rule", "h1", "d1"))) + assertThat(data.moduleGraphJson).isEqualTo("""{"nodes":[]}""") + assertThat(data.depEdges).isEqualTo(mapOf("//a:lib" to emptyList())) + } + + @Test + fun executeTargetHashWithMetadataFromStringLegacyFlatFormat() { + val json = """{"//a:lib":"Rule#h1~d1", "//b:lib":"SourceFile#h2~d2"}""" + + val data = interactor.executeTargetHashWithMetadataFromString(json) + + assertThat(data.hashes) + .isEqualTo( + mapOf( + "//a:lib" to TargetHash("Rule", "h1", "d1"), + "//b:lib" to TargetHash("SourceFile", "h2", "d2"))) + assertThat(data.moduleGraphJson).isNull() + assertThat(data.depEdges).isEqualTo(emptyMap()) + } } diff --git a/cli/src/test/kotlin/com/bazel_diff/server/GitClientTest.kt b/cli/src/test/kotlin/com/bazel_diff/server/GitClientTest.kt index e9df1ff2..9372ed35 100644 --- a/cli/src/test/kotlin/com/bazel_diff/server/GitClientTest.kt +++ b/cli/src/test/kotlin/com/bazel_diff/server/GitClientTest.kt @@ -183,4 +183,61 @@ class GitClientTest : KoinTest { // No remotes to ask -> nothing to fetch from -> false, and no throw. assertThat(ProcessGitClient(temp.root.toPath()).fetchRevision("HEAD")).isFalse() } + + @Test + fun fetchRevisionTriesSubsequentRemotesAfterFirstFails() { + // Two remotes: the first cannot supply the object, the second can. fetchRevision must walk + // remotes and return true once any remote succeeds. + val goodOrigin = File(temp.root, "good-origin").apply { mkdirs() } + runGit(goodOrigin, "init", "-q") + runGit(goodOrigin, "config", "user.email", "test@example.com") + runGit(goodOrigin, "config", "user.name", "test") + runGit(goodOrigin, "config", "uploadpack.allowReachableSHA1InWant", "true") + File(goodOrigin, "file.txt").writeText("one") + runGit(goodOrigin, "add", ".") + runGit(goodOrigin, "commit", "-q", "-m", "first") + val sha = runGit(goodOrigin, "rev-parse", "HEAD") + + val emptyOrigin = File(temp.root, "empty-origin").apply { mkdirs() } + runGit(emptyOrigin, "init", "-q") + runGit(emptyOrigin, "config", "user.email", "test@example.com") + runGit(emptyOrigin, "config", "user.name", "test") + // A distinct empty commit so fetch by SHA from this remote fails. + File(emptyOrigin, "other.txt").writeText("other") + runGit(emptyOrigin, "add", ".") + runGit(emptyOrigin, "commit", "-q", "-m", "unrelated") + + val workspace = File(temp.root, "multi-remote-ws") + runGit(temp.root, "clone", "-q", "file://${emptyOrigin.absolutePath}", workspace.absolutePath) + // Rename the clone's origin and add the good remote second so the first fetch attempt fails. + runGit(workspace, "remote", "rename", "origin", "bad") + runGit(workspace, "remote", "add", "good", "file://${goodOrigin.absolutePath}") + + val client = ProcessGitClient(workspace.toPath()) + assertThrows(MissingRevisionException::class.java) { client.resolveSha(sha) } + assertThat(client.fetchRevision(sha)).isTrue() + assertThat(client.resolveSha(sha)).isEqualTo(sha) + } + + @Test + fun fetchRevisionReturnsFalseWhenListingRemotesFails() { + initRepoWithTwoCommits() + // A git wrapper that fails `git remote` exercises remoteNames()' empty-on-failure path. + val wrapper = + File(temp.root, "git-fail-remote").apply { + writeText( + """ + #!/bin/sh + if [ "${'$'}1" = "remote" ]; then + echo "remote listing disabled" >&2 + exit 1 + fi + exec git "${'$'}@" + """ + .trimIndent()) + setExecutable(true) + } + assertThat(ProcessGitClient(temp.root.toPath(), wrapper.absolutePath).fetchRevision("HEAD")) + .isFalse() + } } diff --git a/cli/src/test/kotlin/com/bazel_diff/server/HashServiceTest.kt b/cli/src/test/kotlin/com/bazel_diff/server/HashServiceTest.kt index 60e55889..f612853b 100644 --- a/cli/src/test/kotlin/com/bazel_diff/server/HashServiceTest.kt +++ b/cli/src/test/kotlin/com/bazel_diff/server/HashServiceTest.kt @@ -6,6 +6,7 @@ import assertk.assertions.doesNotContain import assertk.assertions.hasSize import assertk.assertions.isEqualTo import assertk.assertions.isNotEqualTo +import assertk.assertions.isNotNull import assertk.assertions.isNull import assertk.assertions.startsWith import com.bazel_diff.SilentLogger @@ -14,7 +15,11 @@ import com.bazel_diff.hash.BuildGraphHasher import com.bazel_diff.hash.TargetHash import com.bazel_diff.log.Logger import com.google.gson.GsonBuilder +import java.nio.charset.StandardCharsets import java.nio.file.Path +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicInteger import kotlinx.coroutines.runBlocking import org.junit.Rule import org.junit.Test @@ -29,6 +34,7 @@ import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.whenever +import kotlin.concurrent.thread class HashServiceTest : KoinTest { @get:Rule val mockitoRule = MockitoJUnit.rule() @@ -238,4 +244,99 @@ class HashServiceTest : KoinTest { assertThat(generated.depEdges).isEqualTo(emptyMap()) assertThat(String(storage.entries.values.single())).doesNotContain("depEdges") } + + @Test + fun serializeLegacyFlatFormatWhenNoMetadata() { + whenever(buildGraphHasher.hashAllBazelTargetsAndSourcefiles(any(), any(), any(), anyOrNull())) + .thenReturn(sampleHashes) + runBlocking { whenever(bazelModService.getModuleGraphJson()).thenReturn(null) } + val storage = InMemoryStorage() + + newService(RecordingGitClient(), storage).getHashes("sha1") + + val json = String(storage.entries.values.single(), StandardCharsets.UTF_8) + // Flat shape: label -> hash string, no wrapping "hashes"/"metadata" object. + assertThat(json).doesNotContain("\"hashes\"") + assertThat(json).doesNotContain("\"metadata\"") + assertThat(json).contains("\"//:a\"") + assertThat(json).contains("Rule#h~d") + } + + @Test + fun serializeWithMetadataFields() { + whenever(buildGraphHasher.hashAllBazelTargetsAndSourcefiles(any(), any(), any(), anyOrNull())) + .thenReturn(sampleHashesWithDeps) + runBlocking { whenever(bazelModService.getModuleGraphJson()).thenReturn("""{"graph":1}""") } + val storage = InMemoryStorage() + + newService(RecordingGitClient(), storage, trackDeps = true).getHashes("sha1") + + val json = String(storage.entries.values.single(), StandardCharsets.UTF_8) + assertThat(json).contains("\"hashes\"") + assertThat(json).contains("\"metadata\"") + assertThat(json).contains("\"moduleGraphJson\"") + assertThat(json).contains("\"depEdges\"") + assertThat(json).contains("graph") + } + + @Test + fun deserializeLegacyFlatCacheEntry() { + val storage = InMemoryStorage() + storage.entries["sha1.fp"] = + """{"//:a":"Rule#h~d"}""".toByteArray(StandardCharsets.UTF_8) + + val data = newService(RecordingGitClient(), storage).getHashes("sha1") + + assertThat(data.hashes).isEqualTo(sampleHashes) + assertThat(data.moduleGraphJson).isNull() + assertThat(data.depEdges).isEqualTo(emptyMap()) + // No generation on a pure cache hit. + verify(buildGraphHasher, times(0)) + .hashAllBazelTargetsAndSourcefiles(any(), any(), any(), anyOrNull()) + } + + @Test + fun lockWaitCacheHitAfterConcurrentGeneration() { + val hasherStarted = CountDownLatch(1) + val allowFinish = CountDownLatch(1) + val hasherCalls = AtomicInteger(0) + whenever(buildGraphHasher.hashAllBazelTargetsAndSourcefiles(any(), any(), any(), anyOrNull())) + .thenAnswer { + hasherCalls.incrementAndGet() + hasherStarted.countDown() + allowFinish.await(5, TimeUnit.SECONDS) + sampleHashes + } + runBlocking { whenever(bazelModService.getModuleGraphJson()).thenReturn(null) } + + val service = newService(RecordingGitClient(), InMemoryStorage()) + val missDone = CountDownLatch(1) + val hitProfiler = QueryProfiler() + + val generator = + thread { + service.getHashes("sha1") + missDone.countDown() + } + assertThat(hasherStarted.await(5, TimeUnit.SECONDS)).isEqualTo(true) + + val waiter = + thread { + service.getHashes("sha1", profiler = hitProfiler) + } + // Give the waiter time to miss the initial cache check and block on generationLock. + Thread.sleep(100) + allowFinish.countDown() + generator.join(5_000) + waiter.join(5_000) + assertThat(missDone.await(1, TimeUnit.SECONDS)).isEqualTo(true) + + assertThat(hasherCalls.get()).isEqualTo(1) + val hit = hitProfiler.queryProfile().hashRetrievals.single() + assertThat(hit.cacheHit).isEqualTo(true) + assertThat(hit.lockWaitMillis).isNotNull() + assertThat(hit.lockWaitMillis!! >= 0).isEqualTo(true) + assertThat(hit.cacheReadMillis).isNotNull() + assertThat(hit.generation).isNull() + } } diff --git a/cli/src/test/kotlin/com/bazel_diff/server/LocalDiskHashCacheStorageTest.kt b/cli/src/test/kotlin/com/bazel_diff/server/LocalDiskHashCacheStorageTest.kt index 224af2e0..800c7445 100644 --- a/cli/src/test/kotlin/com/bazel_diff/server/LocalDiskHashCacheStorageTest.kt +++ b/cli/src/test/kotlin/com/bazel_diff/server/LocalDiskHashCacheStorageTest.kt @@ -188,4 +188,105 @@ class LocalDiskHashCacheStorageTest { assertThat(result.evicted).isEqualTo(1) assertThat(Files.exists(sibling)).isTrue() } + + @Test + fun defaultContainsDelegatesToGet() { + // LocalDiskHashCacheStorage overrides contains; exercise the interface default on a bare impl. + val backing = mutableMapOf() + val storage = + object : HashCacheStorage { + override fun get(key: String): ByteArray? = backing[key] + override fun put(key: String, data: ByteArray) { + backing[key] = data + } + } + assertThat(storage.contains("k")).isFalse() + storage.put("k", bytes("v")) + assertThat(storage.contains("k")).isTrue() + } + + @Test + fun getReturnsNullAfterEntryFileDeleted() { + val storage = storage() + storage.put("gone", bytes("x")) + Files.delete(temp.root.toPath().resolve("gone.json")) + assertThat(storage.get("gone")).isNull() + assertThat(storage.contains("gone")).isFalse() + } + + @Test + fun statsAndPruneAreEmptyWhenDirectoryRemoved() { + val dir = temp.newFolder("cache-dir").toPath() + val storage = LocalDiskHashCacheStorage(dir) + storage.put("k", bytes("v")) + // Wipe the directory after construction: stats/prune must degrade gracefully. + dir.toFile().deleteRecursively() + + assertThat(storage.stats()).isEqualTo(CacheStorageStats(0, 0)) + val result = storage.prune(CachePruneLimits(maxEntries = 1)) + assertThat(result).isEqualTo(CachePruneResult(0, 0, 0)) + } + + @Test + fun pruneWithMaxEntriesZeroEvictsEverything() { + val storage = storage() + storage.put("a", bytes("a")) + storage.put("b", bytes("b")) + + val result = storage.prune(CachePruneLimits(maxEntries = 0)) + + assertThat(result.evicted).isEqualTo(2) + assertThat(storage.contains("a")).isFalse() + assertThat(storage.contains("b")).isFalse() + } + + @Test + fun pruneContinuesWhenAnEntryCannotBeDeleted() { + // Best-effort prune: an undeletable entry (uchg on macOS / immutable) must not abort the pass. + val storage = storage() + storage.put("locked", bytes("x")) + storage.put("free", bytes("y")) + setAgeMinutes("locked", 120) + setAgeMinutes("free", 120) + val locked = temp.root.toPath().resolve("locked.json") + val chflags = ProcessBuilder("chflags", "uchg", locked.toString()).start().waitFor() + if (chflags != 0) { + // Environments without chflags: still exercise maxBytes=0 eviction of deletable entries. + storage.prune(CachePruneLimits(maxEntries = 0)) + return + } + try { + val result = storage.prune(CachePruneLimits(maxAge = Duration.ofHours(1))) + // "free" deleted; "locked" may remain if delete threw (caught) or failed. + assertThat(result.scanned).isEqualTo(2) + assertThat(storage.contains("free")).isFalse() + } finally { + ProcessBuilder("chflags", "nouchg", locked.toString()).start().waitFor() + Files.deleteIfExists(locked) + } + } + + @Test + fun getStillReturnsDataWhenTouchFails() { + val storage = storage() + storage.put("locked", bytes("payload")) + val path = temp.root.toPath().resolve("locked.json") + val chflags = ProcessBuilder("chflags", "uchg", path.toString()).start().waitFor() + if (chflags != 0) return + try { + // setLastModifiedTime fails on uchg; touchQuietly must swallow it so the read still succeeds. + assertThat(String(storage.get("locked")!!, StandardCharsets.UTF_8)).isEqualTo("payload") + } finally { + ProcessBuilder("chflags", "nouchg", path.toString()).start().waitFor() + Files.deleteIfExists(path) + } + } + + @Test + fun interfaceTypedCallsReachStatsAndPrune() { + val measurable: MeasurableHashCacheStorage = storage() + assertThat(measurable.stats()).isEqualTo(CacheStorageStats(0, 0)) + val prunable: PrunableHashCacheStorage = storage() + assertThat(prunable.prune(CachePruneLimits(maxBytes = 0))).isEqualTo(CachePruneResult(0, 0, 0)) + } } diff --git a/tools/coverage/BUILD b/tools/coverage/BUILD index 6b7566b3..835e55a2 100644 --- a/tools/coverage/BUILD +++ b/tools/coverage/BUILD @@ -33,6 +33,5 @@ coverage_enforced_test( name = "lcov_merger_test", coverage_include = ["tools/coverage/src/"], crate = ":lcov_merger_lib", - min_line_coverage = 90, rule = rust_test, ) diff --git a/tools/coverage/README.md b/tools/coverage/README.md index 2154689f..cf5850d9 100644 --- a/tools/coverage/README.md +++ b/tools/coverage/README.md @@ -48,7 +48,6 @@ coverage_enforced_test( name = "sample_test", srcs = ["sample_test.go"], embed = [":sample"], - min_line_coverage = 90, coverage_include = ["tools/go/"], ) ``` @@ -62,15 +61,15 @@ kt_jvm_test( name = "DurationConverterTest", ... env = coverage_minimum_env( - 85, coverage_include = ["cli/src/main/kotlin/com/bazel_diff/cli/converter/"], ), ) ``` - `min_line_coverage` — minimum overall line coverage (percent, 0–100) of - the target's merged report. `bazel coverage` fails the target below it, - with a per-file breakdown in the test log; `bazel test` is unaffected. + the target's merged report. Defaults to **90**. `bazel coverage` fails + the target below it, with a per-file breakdown in the test log; + `bazel test` is unaffected. - `coverage_include` — optional path prefixes scoping which source files count. Essential for JVM targets: Jacoco instruments the whole library on the test's classpath, so an unscoped percentage would dilute a focused diff --git a/tools/coverage/defs.bzl b/tools/coverage/defs.bzl index e50f9c6e..3e261b11 100644 --- a/tools/coverage/defs.bzl +++ b/tools/coverage/defs.bzl @@ -16,21 +16,23 @@ which is the channel these helpers use to declare a minimum: name = "sample_test", srcs = ["sample_test.go"], embed = [":sample"], - min_line_coverage = 90, coverage_include = ["tools/go/"], ) -Any rule with the standard `env` attribute works (`go_test`, `rust_test`, -`kt_jvm_test`, `java_test`, `py_test`, ...). A target whose merged report -falls below its minimum fails the coverage run with the merger's per-file -breakdown in the test log; plain `bazel test` runs are untouched. +The default minimum is 90%. Any rule with the standard `env` attribute +works (`go_test`, `rust_test`, `kt_jvm_test`, `java_test`, `py_test`, +...). A target whose merged report falls below its minimum fails the +coverage run with the merger's per-file breakdown in the test log; plain +`bazel test` runs are untouched. """ MIN_LINE_COVERAGE_ENV = "LCOV_MERGER_MIN_LINE_COVERAGE" COVERAGE_INCLUDE_ENV = "LCOV_MERGER_COVERAGE_INCLUDE" COVERAGE_EXCLUDE_ENV = "LCOV_MERGER_COVERAGE_EXCLUDE" -def coverage_minimum_env(min_line_coverage, coverage_include = [], coverage_exclude = []): +DEFAULT_MIN_LINE_COVERAGE = 90 + +def coverage_minimum_env(min_line_coverage = DEFAULT_MIN_LINE_COVERAGE, coverage_include = [], coverage_exclude = []): """Returns the `env` entries declaring a line-coverage minimum. Use this directly when you cannot (or prefer not to) route a target @@ -40,12 +42,15 @@ def coverage_minimum_env(min_line_coverage, coverage_include = [], coverage_excl kt_jvm_test( name = "FooTest", ... - env = coverage_minimum_env(85, ["cli/src/main/kotlin/foo/"]), + env = coverage_minimum_env( + coverage_include = ["cli/src/main/kotlin/foo/"], + ), ) Args: min_line_coverage: minimum overall line-coverage percentage (0-100) for the target's merged LCOV report during `bazel coverage`. + Defaults to 90. coverage_include: optional path prefixes; when non-empty, only source files starting with one of them count toward the minimum. Use this to scope the check to the code the target is responsible for. @@ -67,7 +72,7 @@ def coverage_minimum_env(min_line_coverage, coverage_include = [], coverage_excl def coverage_enforced_test( rule, name, - min_line_coverage, + min_line_coverage = DEFAULT_MIN_LINE_COVERAGE, coverage_include = [], coverage_exclude = [], **kwargs): @@ -77,7 +82,7 @@ def coverage_enforced_test( rule: any test rule with the standard `env` attribute (`go_test`, `rust_test`, `kt_jvm_test`, `py_test`, ...). name: forwarded to the rule. - min_line_coverage: see `coverage_minimum_env`. + min_line_coverage: see `coverage_minimum_env` (defaults to 90). coverage_include: see `coverage_minimum_env`. coverage_exclude: see `coverage_minimum_env`. **kwargs: every other attribute, forwarded untouched (an existing diff --git a/tools/go/sample/BUILD b/tools/go/sample/BUILD index 2f901ac7..3d7c1914 100644 --- a/tools/go/sample/BUILD +++ b/tools/go/sample/BUILD @@ -9,13 +9,12 @@ go_library( ) # `bazel coverage` fails this target if line coverage of tools/go/ drops -# below 90% — the same floor the repo-wide CI gate applies to Go — enforced -# per-target by //tools/coverage:lcov_merger. +# below the default 90% floor — the same floor the repo-wide CI gate +# applies to Go — enforced per-target by //tools/coverage:lcov_merger. coverage_enforced_test( name = "sample_test", srcs = ["sample_test.go"], coverage_include = ["tools/go/"], embed = [":sample"], - min_line_coverage = 90, rule = go_test, ) diff --git a/tools/readme_template.md b/tools/readme_template.md index 2ac3f5b3..359fd7e6 100644 --- a/tools/readme_template.md +++ b/tools/readme_template.md @@ -485,16 +485,16 @@ load("//tools/coverage:defs.bzl", "coverage_enforced_test") coverage_enforced_test( rule = go_test, # any test rule with the standard `env` attribute name = "sample_test", - min_line_coverage = 90, coverage_include = ["tools/go/"], ... ) ``` -Go (`//tools/go/sample:sample_test`), Rust (`//tools/coverage:lcov_merger_test`) -and the Kotlin/JVM tests under `//cli` all carry such minimums. When a target's -merged report falls below its minimum, the coverage run fails that target and the -test log contains a per-file breakdown. See +The default minimum is 90%. Go (`//tools/go/sample:sample_test`), Rust +(`//tools/coverage:lcov_merger_test`) and the primary-owner Kotlin/JVM tests +under `//cli` all carry such minimums. When a target's merged report falls +below its minimum, the coverage run fails that target and the test log +contains a per-file breakdown. See [`tools/coverage/README.md`](tools/coverage/README.md) for details. For an interactive HTML report (annotated source with covered/uncovered lines From a2f59c958aed85aae411376709e953137788b78c Mon Sep 17 00:00:00 2001 From: Maxwell Elliott Date: Mon, 10 Aug 2026 13:20:42 -0400 Subject: [PATCH 4/4] Fix LocalDiskHashCacheStorageTest on Linux CI chflags is macOS-only and ProcessBuilder.start throws when the binary is missing, so the prior skip path never ran on Ubuntu. Use POSIX directory permissions and a VisibleForTesting touch hook instead. Co-authored-by: Cursor --- .../com/bazel_diff/server/HashCacheStorage.kt | 7 +++ .../server/LocalDiskHashCacheStorageTest.kt | 50 ++++++++----------- 2 files changed, 28 insertions(+), 29 deletions(-) diff --git a/cli/src/main/kotlin/com/bazel_diff/server/HashCacheStorage.kt b/cli/src/main/kotlin/com/bazel_diff/server/HashCacheStorage.kt index ffbd38a4..a6138574 100644 --- a/cli/src/main/kotlin/com/bazel_diff/server/HashCacheStorage.kt +++ b/cli/src/main/kotlin/com/bazel_diff/server/HashCacheStorage.kt @@ -216,4 +216,11 @@ class LocalDiskHashCacheStorage(private val directory: Path) : // The mtime bump is only LRU bookkeeping; failing it must never fail the read. } } + + /** + * Test-only: exercise [touchQuietly]'s IOException swallow without OS-specific immutable flags + * (e.g. macOS `chflags uchg`, which is unavailable on Linux CI). + */ + @com.google.common.annotations.VisibleForTesting + fun touchQuietlyForTest(path: Path) = touchQuietly(path) } diff --git a/cli/src/test/kotlin/com/bazel_diff/server/LocalDiskHashCacheStorageTest.kt b/cli/src/test/kotlin/com/bazel_diff/server/LocalDiskHashCacheStorageTest.kt index 800c7445..0fd1564d 100644 --- a/cli/src/test/kotlin/com/bazel_diff/server/LocalDiskHashCacheStorageTest.kt +++ b/cli/src/test/kotlin/com/bazel_diff/server/LocalDiskHashCacheStorageTest.kt @@ -242,44 +242,36 @@ class LocalDiskHashCacheStorageTest { @Test fun pruneContinuesWhenAnEntryCannotBeDeleted() { - // Best-effort prune: an undeletable entry (uchg on macOS / immutable) must not abort the pass. - val storage = storage() - storage.put("locked", bytes("x")) - storage.put("free", bytes("y")) - setAgeMinutes("locked", 120) - setAgeMinutes("free", 120) - val locked = temp.root.toPath().resolve("locked.json") - val chflags = ProcessBuilder("chflags", "uchg", locked.toString()).start().waitFor() - if (chflags != 0) { - // Environments without chflags: still exercise maxBytes=0 eviction of deletable entries. - storage.prune(CachePruneLimits(maxEntries = 0)) - return - } + // Best-effort prune: an undeletable entry must not abort the pass. Make the cache directory + // non-writable so Deletes fail with AccessDeniedException (works on Linux and macOS; unlike + // macOS-only `chflags uchg`, which is missing on Ubuntu CI). + val dir = temp.newFolder("prune-locked").toPath() + val storage = LocalDiskHashCacheStorage(dir) + storage.put("a", bytes("a")) + storage.put("b", bytes("b")) + val originalPerms = Files.getPosixFilePermissions(dir) + Files.setPosixFilePermissions( + dir, java.nio.file.attribute.PosixFilePermissions.fromString("r-xr-xr-x")) try { - val result = storage.prune(CachePruneLimits(maxAge = Duration.ofHours(1))) - // "free" deleted; "locked" may remain if delete threw (caught) or failed. + val result = storage.prune(CachePruneLimits(maxEntries = 0)) assertThat(result.scanned).isEqualTo(2) - assertThat(storage.contains("free")).isFalse() + // Neither entry could be removed; prune still returns without throwing. + assertThat(result.evicted).isEqualTo(0) + assertThat(Files.isRegularFile(dir.resolve("a.json"))).isTrue() + assertThat(Files.isRegularFile(dir.resolve("b.json"))).isTrue() } finally { - ProcessBuilder("chflags", "nouchg", locked.toString()).start().waitFor() - Files.deleteIfExists(locked) + Files.setPosixFilePermissions(dir, originalPerms) } } @Test fun getStillReturnsDataWhenTouchFails() { + // touchQuietly must swallow IOException so a failed mtime bump never fails the read. Call it + // directly with a missing path (setLastModifiedTime throws) — same catch as an immutable file. val storage = storage() - storage.put("locked", bytes("payload")) - val path = temp.root.toPath().resolve("locked.json") - val chflags = ProcessBuilder("chflags", "uchg", path.toString()).start().waitFor() - if (chflags != 0) return - try { - // setLastModifiedTime fails on uchg; touchQuietly must swallow it so the read still succeeds. - assertThat(String(storage.get("locked")!!, StandardCharsets.UTF_8)).isEqualTo("payload") - } finally { - ProcessBuilder("chflags", "nouchg", path.toString()).start().waitFor() - Files.deleteIfExists(path) - } + storage.put("ok", bytes("payload")) + storage.touchQuietlyForTest(temp.root.toPath().resolve("does-not-exist.json")) + assertThat(String(storage.get("ok")!!, StandardCharsets.UTF_8)).isEqualTo("payload") } @Test