From 391e72a2a4579b9051b4870301a7e47768ef0191 Mon Sep 17 00:00:00 2001 From: Maxwell Elliott Date: Mon, 22 Jun 2026 11:40:56 -0400 Subject: [PATCH] ci: enforce >=90% line coverage on go_test targets Adds a separate Go coverage gate so Go production code under tools/go/ is held to the same >=90% line-coverage bar as the Kotlin cli/src/main sources, gated independently so thin coverage in one language can't hide behind the other. - MODULE.bazel: add rules_go/gazelle + a hermetic Go SDK (dev_dependency, so consumers don't inherit them via MVS), matching the nested test-resource workspace versions. - tools/go/sample: a tiny go_library + go_test that exercises the gate end-to-end (and keeps it green until real Go code lands). - ci.yaml: include //tools/go/... in the coverage run and add a step that runs //tools:coverage-check with --include tools/go/ --threshold 90. - Makefile + README/template: document and mirror the Go gate locally. The existing language-agnostic LCOV checker (tools/coverage_check.py) is reused unchanged; rules_go emits workspace-relative SF: paths (tools/go/.../*.go) that the --include prefix scopes cleanly. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/ci.yaml | 11 ++- MODULE.bazel | 9 ++ MODULE.bazel.lock | 173 ++++++++++++++++++++++++++++++++- Makefile | 6 +- README.md | 18 ++-- tools/go/sample/BUILD | 14 +++ tools/go/sample/sample.go | 17 ++++ tools/go/sample/sample_test.go | 18 ++++ tools/readme_template.md | 16 +-- 9 files changed, 264 insertions(+), 18 deletions(-) create mode 100644 tools/go/sample/BUILD create mode 100644 tools/go/sample/sample.go create mode 100644 tools/go/sample/sample_test.go diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 482a2fc..bb4c0c2 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -38,7 +38,7 @@ jobs: - name: Run bazel-diff tests env: USE_BAZEL_VERSION: ${{ matrix.bazel }} - run: ~/go/bin/bazelisk coverage --combined_report=lcov //cli/... //tools:coverage_check_test --enable_bzlmod=true --enable_workspace=false + run: ~/go/bin/bazelisk coverage --combined_report=lcov //cli/... //tools:coverage_check_test //tools/go/... --enable_bzlmod=true --enable_workspace=false - name: Upload coverage report uses: actions/upload-artifact@v4 if: always() @@ -55,6 +55,15 @@ jobs: USE_BAZEL_VERSION: ${{ matrix.bazel }} COVERAGE_THRESHOLD: '90' run: ~/go/bin/bazelisk run //tools:coverage-check -- --badge-json coverage.json bazel-out/_coverage/_coverage_report.dat + - name: Enforce Go coverage threshold (>= 90% line coverage under tools/go/) + env: + # Must match the `Run bazel-diff tests` step above; see the note on the + # Kotlin enforcement step for why USE_BAZEL_VERSION has to be pinned here. + USE_BAZEL_VERSION: ${{ matrix.bazel }} + # Gate Go independently of the Kotlin overall so thin Go coverage can't hide + # behind well-covered Kotlin (and vice versa). --include scopes the report to + # Go production source only (tools/go/, which excludes tools/coverage_check.py). + run: ~/go/bin/bazelisk run //tools:coverage-check -- --include tools/go/ --threshold 90 bazel-out/_coverage/_coverage_report.dat - name: Upload test logs uses: actions/upload-artifact@v4 if: always() diff --git a/MODULE.bazel b/MODULE.bazel index 59d0d41..2bf8c5a 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -35,6 +35,15 @@ bazel_dep(name = "rules_nodejs", version = "6.7.3", dev_dependency = True) bazel_dep(name = "rules_foreign_cc", version = "0.15.1", dev_dependency = True) bazel_dep(name = "rules_python", version = "1.8.4", dev_dependency = True) +# Go support is internal to building/testing bazel-diff (tooling under tools/go, +# 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) + +go_sdk = use_extension("@rules_go//go:extensions.bzl", "go_sdk", dev_dependency = True) +go_sdk.download(version = "1.23.1") + maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven") maven.install( name = "bazel_diff_maven", diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 0dc5a5b..e8825aa 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -73,6 +73,7 @@ "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", @@ -151,6 +152,7 @@ "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", @@ -341,7 +343,8 @@ "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.59.0/source.json": "1df17bb7865cfc029492c30163cee891d0dd8658ea0d5bfdf252c4b6db5c1ef6", + "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", @@ -499,7 +502,7 @@ "//:extensions.bzl%non_module_repositories": { "general": { "bzlTransitiveDigest": "ks6ZQP7BhZgybSu5miBVjOep566JGWK7Lf4pDkWwq4M=", - "usagesDigest": "tAMe7ZJqgv+el/7ycs+hTvnKAW/1xtbdxLysMNxee70=", + "usagesDigest": "vGee9Vu2aMkgyZ0bTWOxC+3c0owTKOmuM2qo+JkDDTQ=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, "envVariables": {}, @@ -1997,6 +2000,172 @@ "5fcd0671a49cecf39b41021621ee1b6e7aa1370f37122b72e80d4fd4185833b6" ] }, + "1.23.1": { + "aix_ppc64": [ + "go1.23.1.aix-ppc64.tar.gz", + "f17f2791717c15728ec63213a014e244c35f9c8846fb29f5a1b63d0c0556f756" + ], + "darwin_amd64": [ + "go1.23.1.darwin-amd64.tar.gz", + "488d9e4ca3e3ed513ee4edd91bef3a2360c65fa6d6be59cf79640bf840130a58" + ], + "darwin_arm64": [ + "go1.23.1.darwin-arm64.tar.gz", + "e223795ca340e285a760a6446ce57a74500b30e57469a4109961d36184d3c05a" + ], + "dragonfly_amd64": [ + "go1.23.1.dragonfly-amd64.tar.gz", + "6af626176923a6ae6c5de6dc1c864f38365793c0e4ecd0d6eab847bdc23953e5" + ], + "freebsd_386": [ + "go1.23.1.freebsd-386.tar.gz", + "cc957c1a019702e6cdc2e257202d42799011ebc1968b6c3bcd6b1965952607d5" + ], + "freebsd_amd64": [ + "go1.23.1.freebsd-amd64.tar.gz", + "a7d57781c50bb80886a8f04066791956d45aa3eea0f83070c5268b6223afb2ff" + ], + "freebsd_arm": [ + "go1.23.1.freebsd-arm.tar.gz", + "c7b09f3fef456048e596db9bea746eb66796aeb82885622b0388feee18f36a3e" + ], + "freebsd_arm64": [ + "go1.23.1.freebsd-arm64.tar.gz", + "b05cd6a77995a0c8439d88df124811c725fb78b942d0b6dd1643529d7ba62f1f" + ], + "freebsd_riscv64": [ + "go1.23.1.freebsd-riscv64.tar.gz", + "56236ae70be1613f2915943b94f53c96be5bffc0719314078facd778a89bc57e" + ], + "illumos_amd64": [ + "go1.23.1.illumos-amd64.tar.gz", + "8644c52df4e831202114fd67c9fcaf1f7233ad27bf945ac53fa7217cf1a0349f" + ], + "linux_386": [ + "go1.23.1.linux-386.tar.gz", + "cdee2f4e2efa001f7ee75c90f2efc310b63346cfbba7b549987e9139527c6b17" + ], + "linux_amd64": [ + "go1.23.1.linux-amd64.tar.gz", + "49bbb517cfa9eee677e1e7897f7cf9cfdbcf49e05f61984a2789136de359f9bd" + ], + "linux_arm64": [ + "go1.23.1.linux-arm64.tar.gz", + "faec7f7f8ae53fda0f3d408f52182d942cc89ef5b7d3d9f23ff117437d4b2d2f" + ], + "linux_armv6l": [ + "go1.23.1.linux-armv6l.tar.gz", + "6c7832c7dcd8fb6d4eb308f672a725393403c74ee7be1aeccd8a443015df99de" + ], + "linux_loong64": [ + "go1.23.1.linux-loong64.tar.gz", + "649ce3856ddc808c00b14a46232eab0bf95e7911cdf497010b17d76656f5ca4e" + ], + "linux_mips": [ + "go1.23.1.linux-mips.tar.gz", + "201911048f234e5a0c51ec94b1a11d4e47062fee4398b1d2faa6c820dc026724" + ], + "linux_mips64": [ + "go1.23.1.linux-mips64.tar.gz", + "2bce3743df463915e45d2612f9476ffb03d0b3750b1cb3879347de08715b5fc6" + ], + "linux_mips64le": [ + "go1.23.1.linux-mips64le.tar.gz", + "54e301f266e33431b0703136e0bbd4cf02461b1ecedd37b7cbd90cb862a98e5f" + ], + "linux_mipsle": [ + "go1.23.1.linux-mipsle.tar.gz", + "8efd495e93d17408c0803595cdc3bf13cb28e0f957aeabd9cc18245fb8e64019" + ], + "linux_ppc64": [ + "go1.23.1.linux-ppc64.tar.gz", + "52bd68689095831ad9af7160844c23b28bb8d0acd268de7e300ff5f0662b7a07" + ], + "linux_ppc64le": [ + "go1.23.1.linux-ppc64le.tar.gz", + "042888cae54b5fbfd9dd1e3b6bc4a5134879777fe6497fc4c62ec394b5ecf2da" + ], + "linux_riscv64": [ + "go1.23.1.linux-riscv64.tar.gz", + "1a4a609f0391bea202d9095453cbfaf7368fa88a04c206bf9dd715a738664dc3" + ], + "linux_s390x": [ + "go1.23.1.linux-s390x.tar.gz", + "47dc49ad45c45e192efa0df7dc7bc5403f5f2d15b5d0dc74ef3018154b616f4d" + ], + "netbsd_386": [ + "go1.23.1.netbsd-386.tar.gz", + "fbfbd5efa6a5d581ea7f5e65015f927db0e52135cab057e43d39d5482da54b61" + ], + "netbsd_amd64": [ + "go1.23.1.netbsd-amd64.tar.gz", + "e96e1cc5cf36113ee6099d1a7306b22cd9c3f975a36bdff954c59f104f22b853" + ], + "netbsd_arm": [ + "go1.23.1.netbsd-arm.tar.gz", + "c394dfc06bfc276a591209a37e09cd39089ec9a9cc3db30b94814ce2e39eb1d4" + ], + "netbsd_arm64": [ + "go1.23.1.netbsd-arm64.tar.gz", + "b3b35d64f32821a68b3e2994032dbefb81978f2ec3f218c7a770623b82d36b8e" + ], + "openbsd_386": [ + "go1.23.1.openbsd-386.tar.gz", + "3c775c4c16c182e33c2c4ac090d9a247a93b3fb18a3df01d87d490f29599faff" + ], + "openbsd_amd64": [ + "go1.23.1.openbsd-amd64.tar.gz", + "5edbe53b47c57b32707fd7154536fbe9eaa79053fea01650c93b54cdba13fc0f" + ], + "openbsd_arm": [ + "go1.23.1.openbsd-arm.tar.gz", + "c30903dd8fa98b8aca8e9db0962ce9f55502aed93e0ef41e5ae148aaa0088de1" + ], + "openbsd_arm64": [ + "go1.23.1.openbsd-arm64.tar.gz", + "12da183489e58f9c6b357bc1b626f85ed7d4220cab31a49d6a49e6ac6a718b67" + ], + "openbsd_ppc64": [ + "go1.23.1.openbsd-ppc64.tar.gz", + "9cc9aad37696a4a10c31dcec9e35a308de0b369dad354d54cf07406ac6fa7c6f" + ], + "openbsd_riscv64": [ + "go1.23.1.openbsd-riscv64.tar.gz", + "e1d740dda062ce5a276a0c3ed7d8b6353238bc8ff405f63e2e3480bfd26a5ec5" + ], + "plan9_386": [ + "go1.23.1.plan9-386.tar.gz", + "da2a37f9987f01f096859230aa13ecc4ad2e7884465bce91004bc78c64435d65" + ], + "plan9_amd64": [ + "go1.23.1.plan9-amd64.tar.gz", + "fd8fff8b0697d55c4a4d02a8dc998192b80a9dc2a057647373d6ff607cad29de" + ], + "plan9_arm": [ + "go1.23.1.plan9-arm.tar.gz", + "52efbc5804c1c86ba7868aa8ebbc31cc8c2a27b62a60fd57944970d48fc67525" + ], + "solaris_amd64": [ + "go1.23.1.solaris-amd64.tar.gz", + "f54205f21e2143f2ada1bf1c00ddf64590f5139d5c3fb77cc06175f0d8cc7567" + ], + "windows_386": [ + "go1.23.1.windows-386.zip", + "ab866f47d7be56e6b1c67f1d529bf4c23331a339fb0785f435a0552d352cb257" + ], + "windows_amd64": [ + "go1.23.1.windows-amd64.zip", + "32dedf277c86610e380e1765593edb66876f00223df71690bd6be68ee17675c0" + ], + "windows_arm": [ + "go1.23.1.windows-arm.zip", + "1a57615a09f13534f88e9f2d7efd5743535d1a5719b19e520eef965a634f8efb" + ], + "windows_arm64": [ + "go1.23.1.windows-arm64.zip", + "64ad0954d2c33f556fb1018d62de091254aa6e3a94f1c8a8b16af0d3701d194e" + ] + }, "1.24.0": { "aix_ppc64": [ "go1.24.0.aix-ppc64.tar.gz", diff --git a/Makefile b/Makefile index 12a3c7c..03f3be4 100644 --- a/Makefile +++ b/Makefile @@ -24,12 +24,14 @@ generate-readme: .PHONY: coverage coverage: - bazel coverage --combined_report=lcov //cli/... //tools:coverage_check_test + bazel coverage --combined_report=lcov //cli/... //tools:coverage_check_test //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 .PHONY: coverage-check coverage-check: 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 .PHONY: coverage-test coverage-test: @@ -37,6 +39,6 @@ coverage-test: .PHONY: coverage-html coverage-html: - bazel coverage --combined_report=lcov //cli/... //tools:coverage_check_test + bazel coverage --combined_report=lcov //cli/... //tools:coverage_check_test //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/README.md b/README.md index 821a81c..550a69b 100644 --- a/README.md +++ b/README.md @@ -462,8 +462,8 @@ bazel run @bazel-diff//cli:bazel-diff -- bazel-diff -h Daniel P. Purkhus
Daniel P. Purkhus
Alex Eagle
Alex Eagle
Anton Malinskiy
Anton Malinskiy
- rdark
rdark
github-actions[bot]
github-actions[bot]
+ rdark
rdark
Ted Kaplan
Ted Kaplan
@@ -536,18 +536,22 @@ bazel test //... ## Code coverage -CI enforces a minimum **90% line coverage** on production sources (`cli/src/main/...`). To -run the same check locally: +CI enforces a minimum **90% line coverage** on production sources. Kotlin +(`cli/src/main/...`) and Go (`tools/go/...`) are gated **independently** at 90% each, so +thin coverage in one language can't hide behind well-covered code in the other. To run the +same checks locally: ```terminal make coverage ``` -This invokes `bazel coverage --combined_report=lcov //cli/... //tools:coverage_check_test` -and then runs `//tools:coverage-check` against the resulting LCOV report. The check is a -Python `py_binary` ([`tools/coverage_check.py`](tools/coverage_check.py)) that prints a +This invokes +`bazel coverage --combined_report=lcov //cli/... //tools:coverage_check_test //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 per-file table sorted by coverage (worst first), the overall percentage, and exits -non-zero if main-source coverage is below the threshold. +non-zero if the scoped coverage is below the threshold. If you've already produced a coverage report and just want to re-check the threshold, `make coverage-check` runs only the binary against `bazel-out/_coverage/_coverage_report.dat`. diff --git a/tools/go/sample/BUILD b/tools/go/sample/BUILD new file mode 100644 index 0000000..41d33f3 --- /dev/null +++ b/tools/go/sample/BUILD @@ -0,0 +1,14 @@ +load("@rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "sample", + srcs = ["sample.go"], + importpath = "github.com/Tinder/bazel-diff/tools/go/sample", + visibility = ["//visibility:public"], +) + +go_test( + name = "sample_test", + srcs = ["sample_test.go"], + embed = [":sample"], +) diff --git a/tools/go/sample/sample.go b/tools/go/sample/sample.go new file mode 100644 index 0000000..f3e98d3 --- /dev/null +++ b/tools/go/sample/sample.go @@ -0,0 +1,17 @@ +// Package sample is a placeholder that exercises the Go coverage gate +// end-to-end. Replace it with real tooling code -- the >=90% line-coverage +// requirement enforced in CI applies to everything under tools/go/. +package sample + +// Add returns the sum of a and b. +func Add(a, b int) int { + return a + b +} + +// Max returns the larger of a and b. +func Max(a, b int) int { + if a > b { + return a + } + return b +} diff --git a/tools/go/sample/sample_test.go b/tools/go/sample/sample_test.go new file mode 100644 index 0000000..7440f98 --- /dev/null +++ b/tools/go/sample/sample_test.go @@ -0,0 +1,18 @@ +package sample + +import "testing" + +func TestAdd(t *testing.T) { + if got := Add(2, 3); got != 5 { + t.Fatalf("Add(2, 3) = %d, want 5", got) + } +} + +func TestMax(t *testing.T) { + if got := Max(2, 3); got != 3 { + t.Fatalf("Max(2, 3) = %d, want 3", got) + } + if got := Max(5, 1); got != 5 { + t.Fatalf("Max(5, 1) = %d, want 5", got) + } +} diff --git a/tools/readme_template.md b/tools/readme_template.md index 23e906c..a40d654 100644 --- a/tools/readme_template.md +++ b/tools/readme_template.md @@ -274,18 +274,22 @@ bazel test //... ## Code coverage -CI enforces a minimum **90% line coverage** on production sources (`cli/src/main/...`). To -run the same check locally: +CI enforces a minimum **90% line coverage** on production sources. Kotlin +(`cli/src/main/...`) and Go (`tools/go/...`) are gated **independently** at 90% each, so +thin coverage in one language can't hide behind well-covered code in the other. To run the +same checks locally: ```terminal make coverage ``` -This invokes `bazel coverage --combined_report=lcov //cli/... //tools:coverage_check_test` -and then runs `//tools:coverage-check` against the resulting LCOV report. The check is a -Python `py_binary` ([`tools/coverage_check.py`](tools/coverage_check.py)) that prints a +This invokes +`bazel coverage --combined_report=lcov //cli/... //tools:coverage_check_test //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 per-file table sorted by coverage (worst first), the overall percentage, and exits -non-zero if main-source coverage is below the threshold. +non-zero if the scoped coverage is below the threshold. If you've already produced a coverage report and just want to re-check the threshold, `make coverage-check` runs only the binary against `bazel-out/_coverage/_coverage_report.dat`.