|
| 1 | +#!/usr/bin/env bash |
| 2 | +set -euo pipefail |
| 3 | + |
| 4 | +# Compare Maven Surefire test execution totals between two revisions. |
| 5 | +# |
| 6 | +# Usage: |
| 7 | +# scripts/compare-test-totals.sh [BASE_REF] [HEAD_REF] |
| 8 | +# |
| 9 | +# Defaults: |
| 10 | +# BASE_REF: origin/main (or main if origin/main not found) |
| 11 | +# HEAD_REF: HEAD (current checkout) |
| 12 | +# |
| 13 | +# Notes: |
| 14 | +# - Uses git worktree to create a clean checkout for BASE_REF under ./.worktrees/base |
| 15 | +# - Runs tests with project-recommended options: |
| 16 | +# MAVEN_OPTS: -Xms3g -Xmx6g -Djdk.xml.xpathExprGrpLimit=500 -Djdk.xml.xpathExprOpLimit=500 |
| 17 | +# Maven args: -B -Dmaven.javadoc.skip=true -PskipBundlePlugin,minimal-fix-latest test |
| 18 | +# - Sums tests, failures, errors, skipped across all modules from Surefire XML reports |
| 19 | +# - Prints a summary table and exits non-zero if totals differ |
| 20 | +# - Requires Java 8 on PATH (will warn if not JDK 8) |
| 21 | + |
| 22 | +BASE_REF=${1:-} |
| 23 | +HEAD_REF=${2:-HEAD} |
| 24 | + |
| 25 | +if [[ -z "${BASE_REF}" ]]; then |
| 26 | + if git show-ref --verify --quiet refs/remotes/origin/main; then |
| 27 | + BASE_REF="origin/main" |
| 28 | + else |
| 29 | + BASE_REF="main" |
| 30 | + fi |
| 31 | +fi |
| 32 | + |
| 33 | +# Ensure we're at repo root (script lives in scripts/) |
| 34 | +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) |
| 35 | +REPO_ROOT=$(cd -- "${SCRIPT_DIR}/.." && pwd) |
| 36 | +cd "${REPO_ROOT}" |
| 37 | + |
| 38 | +# Check Java version (warn only) |
| 39 | +if command -v java >/dev/null 2>&1; then |
| 40 | + JAVA_VER=$(java -version 2>&1 | head -n1 | sed -E 's/.*version "([^"]+)".*/\1/') |
| 41 | + case "${JAVA_VER}" in |
| 42 | + 1.8.*|8*) : ;; # ok |
| 43 | + *) echo "[warn] Java version is ${JAVA_VER}. The project requests JDK 8 for this check." >&2 ;; |
| 44 | + esac |
| 45 | +else |
| 46 | + echo "[warn] java not found on PATH; proceeding but Maven will likely fail." >&2 |
| 47 | +fi |
| 48 | + |
| 49 | +# Maven options per project guidelines |
| 50 | +export MAVEN_OPTS="-Xms3g -Xmx6g -Djdk.xml.xpathExprGrpLimit=500 -Djdk.xml.xpathExprOpLimit=500" |
| 51 | +MVN_CMD=("./mvnw" -B -Dmaven.javadoc.skip=true -PskipBundlePlugin,minimal-fix-latest test) |
| 52 | + |
| 53 | +WORKTREES_DIR="${REPO_ROOT}/.worktrees" |
| 54 | +BASE_DIR="${WORKTREES_DIR}/base" |
| 55 | + |
| 56 | +cleanup() { |
| 57 | + set +e |
| 58 | + if [[ -d "${BASE_DIR}" ]]; then |
| 59 | + git worktree remove --force "${BASE_DIR}" >/dev/null 2>&1 || true |
| 60 | + rm -rf "${BASE_DIR}" >/dev/null 2>&1 || true |
| 61 | + fi |
| 62 | +} |
| 63 | +trap cleanup EXIT |
| 64 | + |
| 65 | +mkdir -p "${WORKTREES_DIR}" |
| 66 | + |
| 67 | +# Add worktree for base |
| 68 | +if [[ -d "${BASE_DIR}" ]]; then |
| 69 | + git worktree remove --force "${BASE_DIR}" >/dev/null 2>&1 || true |
| 70 | + rm -rf "${BASE_DIR}" |
| 71 | +fi |
| 72 | + |
| 73 | +echo "[info] Preparing base worktree at ${BASE_DIR} for ${BASE_REF}" |
| 74 | +git fetch --all --prune --tags >/dev/null 2>&1 || true |
| 75 | +# Use --detach to avoid creating a new branch |
| 76 | +git worktree add --detach "${BASE_DIR}" "${BASE_REF}" >/dev/null |
| 77 | + |
| 78 | +# Function to run tests and collect totals |
| 79 | +run_and_collect() { |
| 80 | + local dir="$1" |
| 81 | + local label="$2" |
| 82 | + echo "[info] Running tests for ${label} in ${dir}" |
| 83 | + (cd "${dir}" && "${MVN_CMD[@]}" >/dev/null) |
| 84 | + |
| 85 | + # Find all Surefire XML suite reports and sum attributes |
| 86 | + # We prefer files named TEST-*.xml (per-class), but also include top-level *-suite.xml if present. |
| 87 | + local xml_files |
| 88 | + # shellcheck disable=SC2207 |
| 89 | + xml_files=($(find "${dir}" -type f \( -path "*/target/surefire-reports/TEST-*.xml" -o -path "*/target/surefire-reports/*-suite.xml" -o -path "*/target/surefire-reports/*.xml" \) -print)) |
| 90 | + |
| 91 | + if [[ ${#xml_files[@]} -eq 0 ]]; then |
| 92 | + echo "tests=0 failures=0 errors=0 skipped=0"; return 0 |
| 93 | + fi |
| 94 | + |
| 95 | + awk 'BEGIN{tests=0;fail=0;err=0;skip=0} |
| 96 | + /<testsuite /{ |
| 97 | + if(match($0, /tests="([0-9]+)"/, a)) tests+=a[1]; |
| 98 | + if(match($0, /failures="([0-9]+)"/, a)) fail+=a[1]; |
| 99 | + if(match($0, /errors="([0-9]+)"/, a)) err+=a[1]; |
| 100 | + if(match($0, /skipped="([0-9]+)"/, a)) skip+=a[1]; |
| 101 | + } |
| 102 | + END{printf("tests=%d failures=%d errors=%d skipped=%d\n", tests, fail, err, skip)}' "${xml_files[@]}" |
| 103 | +} |
| 104 | + |
| 105 | +# Base |
| 106 | +BASE_SUMMARY=$(run_and_collect "${BASE_DIR}" "BASE:${BASE_REF}") |
| 107 | +BASE_TESTS=$(echo "${BASE_SUMMARY}" | awk '{for(i=1;i<=NF;i++){split($i,a,"="); if(a[1]=="tests") print a[2]}}') |
| 108 | +BASE_FAIL=$(echo "${BASE_SUMMARY}" | awk '{for(i=1;i<=NF;i++){split($i,a,"="); if(a[1]=="failures") print a[2]}}') |
| 109 | +BASE_ERR=$(echo "${BASE_SUMMARY}" | awk '{for(i=1;i<=NF;i++){split($i,a,"="); if(a[1]=="errors") print a[2]}}') |
| 110 | +BASE_SKIP=$(echo "${BASE_SUMMARY}" | awk '{for(i=1;i<=NF;i++){split($i,a,"="); if(a[1]=="skipped") print a[2]}}') |
| 111 | + |
| 112 | +# Head (current checkout or provided ref via another worktree when not HEAD) |
| 113 | +HEAD_DIR="${REPO_ROOT}" |
| 114 | +if [[ "${HEAD_REF}" != "HEAD" ]]; then |
| 115 | + # create a transient worktree for explicit HEAD_REF so current tree remains untouched |
| 116 | + HEAD_DIR="${WORKTREES_DIR}/head" |
| 117 | + git worktree add --detach "${HEAD_DIR}" "${HEAD_REF}" >/dev/null |
| 118 | +fi |
| 119 | + |
| 120 | +HEAD_SUMMARY=$(run_and_collect "${HEAD_DIR}" "HEAD:${HEAD_REF}") |
| 121 | +HEAD_TESTS=$(echo "${HEAD_SUMMARY}" | awk '{for(i=1;i<=NF;i++){split($i,a,"="); if(a[1]=="tests") print a[2]}}') |
| 122 | +HEAD_FAIL=$(echo "${HEAD_SUMMARY}" | awk '{for(i=1;i<=NF;i++){split($i,a,"="); if(a[1]=="failures") print a[2]}}') |
| 123 | +HEAD_ERR=$(echo "${HEAD_SUMMARY}" | awk '{for(i=1;i<=NF;i++){split($i,a,"="); if(a[1]=="errors") print a[2]}}') |
| 124 | +HEAD_SKIP=$(echo "${HEAD_SUMMARY}" | awk '{for(i=1;i<=NF;i++){split($i,a,"="); if(a[1]=="skipped") print a[2]}}') |
| 125 | + |
| 126 | +# If we created a HEAD worktree, ensure it gets cleaned as well on exit |
| 127 | +if [[ -d "${WORKTREES_DIR}/head" ]]; then |
| 128 | + trap 'git worktree remove --force "${WORKTREES_DIR}/head" >/dev/null 2>&1 || true; cleanup' EXIT |
| 129 | +fi |
| 130 | + |
| 131 | +# Print summary |
| 132 | +printf "\nTest totals (summed across modules)\n" |
| 133 | +printf "%-12s %10s %10s %10s %10s\n" "Ref" "Tests" "Failures" "Errors" "Skipped" |
| 134 | +printf "%-12s %10d %10d %10d %10d\n" "BASE" "${BASE_TESTS}" "${BASE_FAIL}" "${BASE_ERR}" "${BASE_SKIP}" |
| 135 | +printf "%-12s %10d %10d %10d %10d\n" "HEAD" "${HEAD_TESTS}" "${HEAD_FAIL}" "${HEAD_ERR}" "${HEAD_SKIP}" |
| 136 | + |
| 137 | +DT=$(( HEAD_TESTS - BASE_TESTS )) |
| 138 | +DF=$(( HEAD_FAIL - BASE_FAIL )) |
| 139 | +DE=$(( HEAD_ERR - BASE_ERR )) |
| 140 | +DS=$(( HEAD_SKIP - BASE_SKIP )) |
| 141 | + |
| 142 | +printf "%-12s %10s %10s %10s %10s\n" "Δ(HEAD-BASE)" "${DT}" "${DF}" "${DE}" "${DS}" |
| 143 | + |
| 144 | +# Exit non-zero if overall test count differs |
| 145 | +if [[ ${DT} -ne 0 ]]; then |
| 146 | + echo "\n[fail] Test count changed by ${DT} (HEAD=${HEAD_TESTS}, BASE=${BASE_TESTS})." >&2 |
| 147 | + exit 1 |
| 148 | +fi |
| 149 | + |
| 150 | +# Also flag if failures/errors increased |
| 151 | +if [[ ${DF} -gt 0 || ${DE} -gt 0 ]]; then |
| 152 | + echo "\n[warn] Failures/errors increased (Δfailures=${DF}, Δerrors=${DE})." >&2 |
| 153 | +fi |
| 154 | + |
| 155 | +echo "\n[ok] Test totals match between ${BASE_REF} and ${HEAD_REF}." |
0 commit comments