#!/usr/bin/env bash # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # # coverage-grade.sh # # Parses the JaCoCo aggregate XML report and outputs an A–F coverage grade. # # Usage: # ./scripts/coverage-grade.sh [path/to/jacoco.xml] # # Exit codes: # 0 – grade is D or above (line coverage >= 20%) # 1 – grade is F (line coverage < 20%) # # Environment variables (optional, used when writing GitHub outputs): # GITHUB_OUTPUT – set automatically by GitHub Actions # GITHUB_STEP_SUMMARY – set automatically by GitHub Actions set -euo pipefail JACOCO_XML="${1:-client/target/site/jacoco-aggregate/jacoco.xml}" if [[ ! -f "$JACOCO_XML" ]]; then echo "ERROR: JaCoCo report not found at: $JACOCO_XML" >&2 exit 2 fi # --------------------------------------------------------------------------- # Parse LINE and BRANCH counters from the top-level element using # Python's built-in xml.etree.ElementTree (no extra dependencies needed). # --------------------------------------------------------------------------- read -r LINE_COVERED LINE_MISSED BRANCH_COVERED BRANCH_MISSED < <(python3 - "$JACOCO_XML" <<'PYEOF' import sys, xml.etree.ElementTree as ET tree = ET.parse(sys.argv[1]) root = tree.getroot() lc = lm = bc = bm = 0 # Sum counters from all children so we get the true aggregate, # avoiding any duplicate top-level counter that some JaCoCo versions emit. for pkg in root.iter('package'): for counter in pkg.findall('counter'): t = counter.get('type') if t == 'LINE': lc += int(counter.get('covered', 0)) lm += int(counter.get('missed', 0)) elif t == 'BRANCH': bc += int(counter.get('covered', 0)) bm += int(counter.get('missed', 0)) print(lc, lm, bc, bm) PYEOF ) # --------------------------------------------------------------------------- # Compute percentages # --------------------------------------------------------------------------- line_total=$(( LINE_COVERED + LINE_MISSED )) branch_total=$(( BRANCH_COVERED + BRANCH_MISSED )) if (( line_total == 0 )); then echo "ERROR: No LINE counters found in $JACOCO_XML – was the build run with -P quality?" >&2 exit 2 fi # Use awk for floating-point arithmetic LINE_PCT=$(awk "BEGIN { printf \"%.2f\", ($LINE_COVERED / $line_total) * 100 }") if (( branch_total > 0 )); then BRANCH_PCT=$(awk "BEGIN { printf \"%.2f\", ($BRANCH_COVERED / $branch_total) * 100 }") else BRANCH_PCT="N/A" fi # --------------------------------------------------------------------------- # Assign grade based on LINE coverage # # A ≥ 80% Excellent # B 60–79% Good # C 40–59% Acceptable # D 20–39% Marginal (meets minimum gate) # F < 20% Failing # --------------------------------------------------------------------------- LINE_INT=$(awk "BEGIN { printf \"%d\", $LINE_PCT }") # truncate, not round if (( LINE_INT >= 80 )); then GRADE="A"; EMOJI="🟢"; LABEL="Excellent" elif (( LINE_INT >= 60 )); then GRADE="B"; EMOJI="🟡"; LABEL="Good" elif (( LINE_INT >= 40 )); then GRADE="C"; EMOJI="🟠"; LABEL="Acceptable" elif (( LINE_INT >= 20 )); then GRADE="D"; EMOJI="🔴"; LABEL="Marginal" else GRADE="F"; EMOJI="⛔"; LABEL="Failing" fi # --------------------------------------------------------------------------- # Human-readable output (always printed to stdout) # --------------------------------------------------------------------------- echo "┌─────────────────────────────────────────────────┐" echo "│ CloudStack Test Coverage Report │" echo "├─────────────────────────────────────────────────┤" printf "│ Grade : %s %-5s %-20s │\n" "$EMOJI" "$GRADE" "($LABEL)" printf "│ Line coverage: %6s%% (%d / %d lines)%*s│\n" \ "$LINE_PCT" "$LINE_COVERED" "$line_total" \ $(( 14 - ${#LINE_COVERED} - ${#line_total} )) " " if [[ "$BRANCH_PCT" != "N/A" ]]; then printf "│ Branch cov. : %6s%% (%d / %d branches)%*s│\n" \ "$BRANCH_PCT" "$BRANCH_COVERED" "$branch_total" \ $(( 11 - ${#BRANCH_COVERED} - ${#branch_total} )) " " else printf "│ Branch cov. : N/A (no branch data) │\n" fi echo "└─────────────────────────────────────────────────┘" echo "" echo "Grade scale: A ≥80% B 60-79% C 40-59% D 20-39% F <20% (line coverage)" # --------------------------------------------------------------------------- # GitHub Actions: write outputs and step summary # --------------------------------------------------------------------------- if [[ -n "${GITHUB_OUTPUT:-}" ]]; then { echo "coverage_grade=$GRADE" echo "coverage_grade_label=$LABEL" echo "line_coverage=$LINE_PCT" echo "branch_coverage=$BRANCH_PCT" } >> "$GITHUB_OUTPUT" fi if [[ -n "${GITHUB_STEP_SUMMARY:-}" ]]; then { echo "## $EMOJI Test Coverage Grade: **$GRADE** — $LABEL" echo "" echo "| Metric | Covered | Total | Percentage |" echo "|--------|---------|-------|------------|" echo "| Line coverage | $LINE_COVERED | $line_total | **${LINE_PCT}%** |" if [[ "$BRANCH_PCT" != "N/A" ]]; then echo "| Branch coverage | $BRANCH_COVERED | $branch_total | **${BRANCH_PCT}%** |" fi echo "" echo "### Grade Scale" echo "| Grade | Line Coverage | Meaning |" echo "|-------|--------------|---------|" echo "| 🟢 A | ≥ 80% | Excellent - this code sleeps well at night 😴 |" echo "| 🟡 B | 60-79% | Good - almost there, don't stop now 😉 |" echo "| 🟠 C | 40-59% | Acceptable - your code is wearing a seatbelt, but no airbags 😬 |" echo "| 🔴 D | 20-39% | Marginal - boldly shipping where no test has gone before 🖖 |" echo "| ⛔ F | < 20% | tests? what tests? 🔥 |" echo "" echo "> Branch coverage is shown as a secondary signal. Grade is based on line coverage." } >> "$GITHUB_STEP_SUMMARY" fi # --------------------------------------------------------------------------- # Exit non-zero for grade F so the CI job can be configured to fail # --------------------------------------------------------------------------- if [[ "$GRADE" == "F" ]]; then echo "" echo "⛔ FAIL: Line coverage ${LINE_PCT}% is below the minimum threshold of 20%." >&2 exit 1 fi exit 0