blob: fc7edc8dcb999c228a4a38fddc0a40beabc626eb [file]
#!/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 <report> 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 <package> 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