From d0bdda61993384ebe87cc66f44f1ae7dfc4a70d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Lo=CC=81pez=20Man=CC=83as?= Date: Mon, 21 Sep 2026 09:19:59 +0200 Subject: [PATCH] feat(ci): publish an instrumented coverage badge Closes #241. The coverage history added in #1000 already records a TOTAL row per suite on every merge to main, so the badge only needs to expose it. A new 'badge' subcommand emits shields.io endpoint JSON next to COVERAGE.md on the coverage-history branch, and README.md points an endpoint badge at it. The badge reports the instrumentation suite, not unit. This library is a Compose wrapper around a map, so JVM unit tests structurally cannot cover much of it: unit line coverage is 0.42% against 20.87% instrumented. The label says 'coverage (instrumented)' so the number is not mistaken for a whole-project figure. push_coverage_history.sh needed two fixes to carry the new file: it hard-coded history.csv and COVERAGE.md in both its change detection and its git add, and it checked for changes with 'git diff' before staging, which cannot see an untracked file and would have skipped the badge's first commit entirely. --- .github/scripts/coverage_history.py | 74 ++++++++++++++++++++++++ .github/scripts/push_coverage_history.sh | 18 ++++-- .github/workflows/coverage-history.yml | 6 +- README.md | 1 + 4 files changed, 94 insertions(+), 5 deletions(-) diff --git a/.github/scripts/coverage_history.py b/.github/scripts/coverage_history.py index a0461071..33aeb235 100755 --- a/.github/scripts/coverage_history.py +++ b/.github/scripts/coverage_history.py @@ -32,6 +32,7 @@ render regenerate the human-readable COVERAGE.md from that CSV compare render a markdown coverage diff against the last recorded entry, for posting on a pull request + badge emit shields.io endpoint JSON for the latest recorded total Standard library only, so it runs on any CI image without extra setup. """ @@ -563,6 +564,55 @@ def delta(current: float, module: str, field: str) -> str: print(body) +# shields.io endpoint schema: https://shields.io/badges/endpoint-badge +BADGE_SCHEMA_VERSION = 1 + +# Floor percentages, highest first. Anything under the last floor is red. +BADGE_COLORS = ( + (90.0, "brightgreen"), + (75.0, "green"), + (60.0, "yellowgreen"), + (40.0, "yellow"), + (20.0, "orange"), +) + + +def badge_color(percentage: float) -> str: + for floor, color in BADGE_COLORS: + if percentage >= floor: + return color + return "red" + + +def cmd_badge(args: argparse.Namespace) -> None: + """Write shields.io endpoint JSON for one suite's latest TOTAL row. + + Read by an endpoint badge in README.md. The file is published on the + coverage history branch rather than main, so refreshing the badge costs + contributors nothing (see coverage-history.yml for why that matters). + + A suite with no recorded entry yet renders as "unknown" rather than + failing, so the badge degrades quietly instead of breaking the README. + """ + total = latest_entry(read_history(args.csv), args.suite).get("TOTAL") + if total is None: + payload = {"message": "unknown", "color": "lightgrey"} + else: + percentage = float(total[args.metric]) + payload = { + "message": f"{percentage:.0f}%", + "color": badge_color(percentage), + } + + payload = {"schemaVersion": BADGE_SCHEMA_VERSION, "label": args.label, **payload} + text = json.dumps(payload, indent=2) + "\n" + if args.out: + with open(args.out, "w", encoding="utf-8") as handle: + handle.write(text) + else: + sys.stdout.write(text) + + def add_common(parser: argparse.ArgumentParser) -> None: parser.add_argument( "--suite", @@ -630,6 +680,30 @@ def main() -> None: ) compare.set_defaults(func=cmd_compare) + badge = subparsers.add_parser( + "badge", help="emit shields.io endpoint JSON for the latest total" + ) + badge.add_argument( + "--suite", + choices=sorted(SUITES), + default="instrumentation", + help="which suite to report (default: instrumentation)", + ) + badge.add_argument("--csv", default=DEFAULT_CSV) + badge.add_argument("--out", default="", help="write here instead of stdout") + badge.add_argument( + "--label", + default="coverage (instrumented)", + help="text on the left half of the badge", + ) + badge.add_argument( + "--metric", + default="line_pct", + choices=("line_pct", "branch_pct", "instruction_pct"), + help="which percentage to display (default: line_pct)", + ) + badge.set_defaults(func=cmd_badge) + args = parser.parse_args() args.func(args) diff --git a/.github/scripts/push_coverage_history.sh b/.github/scripts/push_coverage_history.sh index 0d6baa7e..40795851 100755 --- a/.github/scripts/push_coverage_history.sh +++ b/.github/scripts/push_coverage_history.sh @@ -35,17 +35,25 @@ DATA_DIR="${DATA_DIR:-coverage-data}" DATA_BRANCH="${DATA_BRANCH:-coverage-history}" SCRIPT="${SCRIPT:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/coverage_history.py}" +# Every generated file published on the data branch. COVERAGE.md and the badge +# are both derived from history.csv, so they are regenerated, not merged. +FILES=(history.csv COVERAGE.md coverage-badge.json) + cd "$DATA_DIR" git config user.name 'googlemaps-bot' git config user.email 'googlemaps-bot@google.com' -if git diff --quiet -- history.csv COVERAGE.md; then +# Stage first and compare the index: a newly generated file is untracked, and +# "git diff" does not see untracked files, so checking before staging would +# skip the commit the first time a new output appears. +git add -- "${FILES[@]}" + +if git diff --cached --quiet -- "${FILES[@]}"; then echo "Coverage history unchanged; nothing to commit." exit 0 fi -git add history.csv COVERAGE.md git commit -m "$MESSAGE" for attempt in 1 2 3; do @@ -65,13 +73,15 @@ for attempt in 1 2 3; do python3 "$SCRIPT" merge --csv history.csv --ours "$OURS" python3 "$SCRIPT" render --csv history.csv --out COVERAGE.md + python3 "$SCRIPT" badge --csv history.csv --out coverage-badge.json + + git add -- "${FILES[@]}" - if git diff --quiet -- history.csv COVERAGE.md; then + if git diff --cached --quiet -- "${FILES[@]}"; then echo "Our rows are already on $DATA_BRANCH; nothing left to push." exit 0 fi - git add history.csv COVERAGE.md git commit -m "$MESSAGE" done diff --git a/.github/workflows/coverage-history.yml b/.github/workflows/coverage-history.yml index f8a036c6..c0647ac6 100644 --- a/.github/workflows/coverage-history.yml +++ b/.github/workflows/coverage-history.yml @@ -35,7 +35,7 @@ concurrency: cancel-in-progress: false env: - # Branch holding history.csv and COVERAGE.md. + # Branch holding history.csv, COVERAGE.md and coverage-badge.json. DATA_BRANCH: coverage-history # How long to wait for the pull request's emulator run to finish before # giving up and recording unit coverage only. instrumentation-test.yml takes @@ -111,6 +111,8 @@ jobs: --commit "$COMMIT_SHA" --pr "$PR_NUMBER" --subject "$SUBJECT" python3 .github/scripts/coverage_history.py render \ --csv coverage-data/history.csv --out coverage-data/COVERAGE.md + python3 .github/scripts/coverage_history.py badge \ + --csv coverage-data/history.csv --out coverage-data/coverage-badge.json - name: Push unit coverage run: | @@ -189,6 +191,8 @@ jobs: --commit "$COMMIT_SHA" --pr "$PR_NUMBER" --subject "$SUBJECT" python3 .github/scripts/coverage_history.py render \ --csv coverage-data/history.csv --out coverage-data/COVERAGE.md + python3 .github/scripts/coverage_history.py badge \ + --csv coverage-data/history.csv --out coverage-data/coverage-badge.json - name: Push instrumentation coverage if: steps.emulator.outputs.run_id != '' diff --git a/README.md b/README.md index aebcb08b..8b1d6a66 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ ![Release](https://github.com/googlemaps/android-maps-compose/workflows/Release/badge.svg) ![Stable](https://img.shields.io/badge/stability-stable-green) [![Tests/Build](https://github.com/googlemaps/android-maps-compose/actions/workflows/test.yml/badge.svg)](https://github.com/googlemaps/android-maps-compose/actions/workflows/test.yml) +[![Instrumented coverage](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fgooglemaps%2Fandroid-maps-compose%2Fcoverage-history%2Fcoverage-badge.json)](https://github.com/googlemaps/android-maps-compose/blob/coverage-history/COVERAGE.md) ![Contributors](https://img.shields.io/github/contributors/googlemaps/android-maps-compose?color=green) [![License](https://img.shields.io/github/license/googlemaps/android-maps-compose?color=blue)][license]