|
| 1 | +#!/usr/bin/env bash |
| 2 | +# Licensed to the Apache Software Foundation (ASF) under one |
| 3 | +# or more contributor license agreements. See the NOTICE file |
| 4 | +# distributed with this work for additional information |
| 5 | +# regarding copyright ownership. The ASF licenses this file |
| 6 | +# to you under the Apache License, Version 2.0 (the |
| 7 | +# "License"); you may not use this file except in compliance |
| 8 | +# with the License. You may obtain a copy of the License at |
| 9 | +# |
| 10 | +# http://www.apache.org/licenses/LICENSE-2.0 |
| 11 | +# |
| 12 | +# Unless required by applicable law or agreed to in writing, |
| 13 | +# software distributed under the License is distributed on an |
| 14 | +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| 15 | +# KIND, either express or implied. See the License for the |
| 16 | +# specific language governing permissions and limitations |
| 17 | +# under the License. |
| 18 | +# |
| 19 | +# coverage-grade.sh |
| 20 | +# |
| 21 | +# Parses the JaCoCo aggregate XML report and outputs an A–F coverage grade. |
| 22 | +# |
| 23 | +# Usage: |
| 24 | +# ./scripts/coverage-grade.sh [path/to/jacoco.xml] |
| 25 | +# |
| 26 | +# Exit codes: |
| 27 | +# 0 – grade is D or above (line coverage >= 40%) |
| 28 | +# 1 – grade is F (line coverage < 40%) |
| 29 | +# |
| 30 | +# Environment variables (optional, used when writing GitHub outputs): |
| 31 | +# GITHUB_OUTPUT – set automatically by GitHub Actions |
| 32 | +# GITHUB_STEP_SUMMARY – set automatically by GitHub Actions |
| 33 | + |
| 34 | +set -euo pipefail |
| 35 | + |
| 36 | +JACOCO_XML="${1:-client/target/site/jacoco-aggregate/jacoco.xml}" |
| 37 | + |
| 38 | +if [[ ! -f "$JACOCO_XML" ]]; then |
| 39 | + echo "ERROR: JaCoCo report not found at: $JACOCO_XML" >&2 |
| 40 | + exit 2 |
| 41 | +fi |
| 42 | + |
| 43 | +# --------------------------------------------------------------------------- |
| 44 | +# Parse LINE and BRANCH counters from the top-level <report> element using |
| 45 | +# Python's built-in xml.etree.ElementTree (no extra dependencies needed). |
| 46 | +# --------------------------------------------------------------------------- |
| 47 | +read -r LINE_COVERED LINE_MISSED BRANCH_COVERED BRANCH_MISSED < <(python3 - "$JACOCO_XML" <<'PYEOF' |
| 48 | +import sys, xml.etree.ElementTree as ET |
| 49 | +
|
| 50 | +tree = ET.parse(sys.argv[1]) |
| 51 | +root = tree.getroot() |
| 52 | +
|
| 53 | +lc = lm = bc = bm = 0 |
| 54 | +# Sum counters from all <package> children so we get the true aggregate, |
| 55 | +# avoiding any duplicate top-level counter that some JaCoCo versions emit. |
| 56 | +for pkg in root.iter('package'): |
| 57 | + for counter in pkg.findall('counter'): |
| 58 | + t = counter.get('type') |
| 59 | + if t == 'LINE': |
| 60 | + lc += int(counter.get('covered', 0)) |
| 61 | + lm += int(counter.get('missed', 0)) |
| 62 | + elif t == 'BRANCH': |
| 63 | + bc += int(counter.get('covered', 0)) |
| 64 | + bm += int(counter.get('missed', 0)) |
| 65 | +
|
| 66 | +print(lc, lm, bc, bm) |
| 67 | +PYEOF |
| 68 | +) |
| 69 | + |
| 70 | +# --------------------------------------------------------------------------- |
| 71 | +# Compute percentages |
| 72 | +# --------------------------------------------------------------------------- |
| 73 | +line_total=$(( LINE_COVERED + LINE_MISSED )) |
| 74 | +branch_total=$(( BRANCH_COVERED + BRANCH_MISSED )) |
| 75 | + |
| 76 | +if (( line_total == 0 )); then |
| 77 | + echo "ERROR: No LINE counters found in $JACOCO_XML – was the build run with -P quality?" >&2 |
| 78 | + exit 2 |
| 79 | +fi |
| 80 | + |
| 81 | +# Use awk for floating-point arithmetic |
| 82 | +LINE_PCT=$(awk "BEGIN { printf \"%.2f\", ($LINE_COVERED / $line_total) * 100 }") |
| 83 | + |
| 84 | +if (( branch_total > 0 )); then |
| 85 | + BRANCH_PCT=$(awk "BEGIN { printf \"%.2f\", ($BRANCH_COVERED / $branch_total) * 100 }") |
| 86 | +else |
| 87 | + BRANCH_PCT="N/A" |
| 88 | +fi |
| 89 | + |
| 90 | +# --------------------------------------------------------------------------- |
| 91 | +# Assign grade based on LINE coverage |
| 92 | +# |
| 93 | +# A ≥ 80% Excellent |
| 94 | +# B 60–79% Good |
| 95 | +# C 40–59% Acceptable |
| 96 | +# D 20–39% Marginal (meets minimum gate) |
| 97 | +# F < 20% Failing |
| 98 | +# --------------------------------------------------------------------------- |
| 99 | +LINE_INT=$(awk "BEGIN { printf \"%d\", $LINE_PCT }") # truncate, not round |
| 100 | + |
| 101 | +if (( LINE_INT >= 80 )); then GRADE="A"; EMOJI="🟢"; LABEL="Excellent" |
| 102 | +elif (( LINE_INT >= 60 )); then GRADE="B"; EMOJI="🟡"; LABEL="Good" |
| 103 | +elif (( LINE_INT >= 40 )); then GRADE="C"; EMOJI="🟠"; LABEL="Acceptable" |
| 104 | +elif (( LINE_INT >= 20 )); then GRADE="D"; EMOJI="🔴"; LABEL="Marginal" |
| 105 | +else GRADE="F"; EMOJI="⛔"; LABEL="Failing" |
| 106 | +fi |
| 107 | + |
| 108 | +# --------------------------------------------------------------------------- |
| 109 | +# Human-readable output (always printed to stdout) |
| 110 | +# --------------------------------------------------------------------------- |
| 111 | +echo "┌─────────────────────────────────────────────────┐" |
| 112 | +echo "│ CloudStack Test Coverage Report │" |
| 113 | +echo "├─────────────────────────────────────────────────┤" |
| 114 | +printf "│ Grade : %s %-5s %-20s │\n" "$EMOJI" "$GRADE" "($LABEL)" |
| 115 | +printf "│ Line coverage: %6s%% (%d / %d lines)%*s│\n" \ |
| 116 | + "$LINE_PCT" "$LINE_COVERED" "$line_total" \ |
| 117 | + $(( 14 - ${#LINE_COVERED} - ${#line_total} )) " " |
| 118 | +if [[ "$BRANCH_PCT" != "N/A" ]]; then |
| 119 | + printf "│ Branch cov. : %6s%% (%d / %d branches)%*s│\n" \ |
| 120 | + "$BRANCH_PCT" "$BRANCH_COVERED" "$branch_total" \ |
| 121 | + $(( 11 - ${#BRANCH_COVERED} - ${#branch_total} )) " " |
| 122 | +else |
| 123 | + printf "│ Branch cov. : N/A (no branch data) │\n" |
| 124 | +fi |
| 125 | +echo "└─────────────────────────────────────────────────┘" |
| 126 | +echo "" |
| 127 | +echo "Grade scale: A ≥80% B 60-79% C 40-59% D 20-39% F <20% (line coverage)" |
| 128 | + |
| 129 | +# --------------------------------------------------------------------------- |
| 130 | +# GitHub Actions: write outputs and step summary |
| 131 | +# --------------------------------------------------------------------------- |
| 132 | +if [[ -n "${GITHUB_OUTPUT:-}" ]]; then |
| 133 | + { |
| 134 | + echo "coverage_grade=$GRADE" |
| 135 | + echo "coverage_grade_label=$LABEL" |
| 136 | + echo "line_coverage=$LINE_PCT" |
| 137 | + echo "branch_coverage=$BRANCH_PCT" |
| 138 | + } >> "$GITHUB_OUTPUT" |
| 139 | +fi |
| 140 | + |
| 141 | +if [[ -n "${GITHUB_STEP_SUMMARY:-}" ]]; then |
| 142 | + { |
| 143 | + echo "## $EMOJI Test Coverage Grade: **$GRADE** — $LABEL" |
| 144 | + echo "" |
| 145 | + echo "| Metric | Covered | Total | Percentage |" |
| 146 | + echo "|--------|---------|-------|------------|" |
| 147 | + echo "| Line coverage | $LINE_COVERED | $line_total | **${LINE_PCT}%** |" |
| 148 | + if [[ "$BRANCH_PCT" != "N/A" ]]; then |
| 149 | + echo "| Branch coverage | $BRANCH_COVERED | $branch_total | **${BRANCH_PCT}%** |" |
| 150 | + fi |
| 151 | + echo "" |
| 152 | + echo "### Grade Scale" |
| 153 | + echo "| Grade | Line Coverage | Meaning |" |
| 154 | + echo "|-------|--------------|---------|" |
| 155 | + echo "| 🟢 A | ≥ 80% | Excellent |" |
| 156 | + echo "| 🟡 B | 60–79% | Good |" |
| 157 | + echo "| 🟠 C | 40–59% | Acceptable |" |
| 158 | + echo "| 🔴 D | 20–39% | Marginal — meets minimum gate |" |
| 159 | + echo "| ⛔ F | < 20% | Failing — below minimum gate |" |
| 160 | + echo "" |
| 161 | + echo "> Branch coverage is shown as a secondary signal. Grade is based on line coverage." |
| 162 | + } >> "$GITHUB_STEP_SUMMARY" |
| 163 | +fi |
| 164 | + |
| 165 | +# --------------------------------------------------------------------------- |
| 166 | +# Exit non-zero for grade F so the CI job can be configured to fail |
| 167 | +# --------------------------------------------------------------------------- |
| 168 | +if [[ "$GRADE" == "F" ]]; then |
| 169 | + echo "" |
| 170 | + echo "⛔ FAIL: Line coverage ${LINE_PCT}% is below the minimum threshold of 20%." >&2 |
| 171 | + exit 1 |
| 172 | +fi |
| 173 | + |
| 174 | +exit 0 |
| 175 | + |
| 176 | + |
| 177 | + |
0 commit comments