coverage-check.sh 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
  1. #!/usr/bin/env bash
  2. # Run pytest with coverage and exit non-zero when coverage is below a threshold.
  3. #
  4. # Usage: coverage-check.sh [--threshold N] [--cov TARGET] [pytest-args...]
  5. # Input: a pytest project in the current directory; OR CM_COVERAGE_OVERRIDE=PCT
  6. # to judge a given percentage offline (test seam — skips pytest entirely)
  7. # Output: one verdict line on stdout, e.g. "coverage-check<TAB>pass<TAB>90<TAB>80"
  8. # Stderr: status banners and the full pytest run (progress, coverage report, errors)
  9. # Exit: 0 pass, 1 below threshold, 2 usage, 5 pytest missing
  10. #
  11. # Examples:
  12. # coverage-check.sh
  13. # coverage-check.sh --threshold 90 --cov mypkg
  14. # CM_COVERAGE_OVERRIDE=72 coverage-check.sh --threshold 80 # offline test mode
  15. set -uo pipefail
  16. THRESHOLD=80
  17. COV_TARGET="src"
  18. PYTEST_ARGS=()
  19. OVERRIDE="${CM_COVERAGE_OVERRIDE:-}"
  20. usage() {
  21. cat <<'EOF'
  22. Usage: coverage-check.sh [--threshold N] [--cov TARGET] [pytest-args...]
  23. Run pytest with coverage; exit non-zero when coverage is below --threshold.
  24. The verdict is one line on stdout; pytest output and status banners go to stderr.
  25. Options:
  26. --threshold N minimum coverage percent (default 80)
  27. --cov TARGET package to measure (default src)
  28. Offline test seam:
  29. CM_COVERAGE_OVERRIDE=PCT skip pytest, judge this percentage instead.
  30. Exit codes: 0 pass, 1 below threshold, 2 usage, 5 pytest missing.
  31. Examples:
  32. coverage-check.sh
  33. coverage-check.sh --threshold 90 --cov mypkg
  34. CM_COVERAGE_OVERRIDE=72 coverage-check.sh --threshold 80
  35. EOF
  36. }
  37. while [[ $# -gt 0 ]]; do
  38. case "$1" in
  39. -h|--help) usage; exit 0 ;;
  40. --threshold)
  41. [[ $# -ge 2 ]] || { echo "coverage-check.sh: --threshold needs a value" >&2; exit 2; }
  42. THRESHOLD="$2"; shift 2 ;;
  43. --threshold=*) THRESHOLD="${1#--threshold=}"; shift ;;
  44. --cov)
  45. [[ $# -ge 2 ]] || { echo "coverage-check.sh: --cov needs a value" >&2; exit 2; }
  46. COV_TARGET="$2"; shift 2 ;;
  47. --cov=*) COV_TARGET="${1#--cov=}"; shift ;;
  48. --) shift; while [[ $# -gt 0 ]]; do PYTEST_ARGS+=("$1"); shift; done ;;
  49. -*) echo "coverage-check.sh: unknown option: $1" >&2; usage >&2; exit 2 ;;
  50. *) PYTEST_ARGS+=("$1"); shift ;;
  51. esac
  52. done
  53. # threshold must be a number
  54. if ! [[ "$THRESHOLD" =~ ^[0-9]+([.][0-9]+)?$ ]]; then
  55. echo "coverage-check.sh: --threshold must be a number, got '$THRESHOLD'" >&2
  56. exit 2
  57. fi
  58. # ge PCT THRESHOLD -> returns 0 if PCT >= THRESHOLD (float-safe via awk)
  59. ge() { awk -v a="$1" -v b="$2" 'BEGIN { exit !(a+0 >= b+0) }'; }
  60. # Offline test seam: judge a supplied percentage without running pytest. This is
  61. # what lets the threshold logic be exercised offline, deterministically, with no
  62. # slow suite and no network — the same pattern check-ytdlp-version.sh uses.
  63. if [[ -n "$OVERRIDE" ]]; then
  64. if ! [[ "$OVERRIDE" =~ ^[0-9]+([.][0-9]+)?$ ]]; then
  65. echo "coverage-check.sh: CM_COVERAGE_OVERRIDE must be a number, got '$OVERRIDE'" >&2
  66. exit 2
  67. fi
  68. if ge "$OVERRIDE" "$THRESHOLD"; then
  69. printf 'coverage-check\tpass\t%s\t%s\n' "$OVERRIDE" "$THRESHOLD"; exit 0
  70. else
  71. printf 'coverage-check\tfail\t%s\t%s\n' "$OVERRIDE" "$THRESHOLD"; exit 1
  72. fi
  73. fi
  74. # Live path: run pytest with coverage. All of its output (progress + coverage
  75. # report) is data for a human, not for this script's caller, so it goes to
  76. # stderr; the one-line verdict on stdout is the machine-readable product.
  77. command -v pytest >/dev/null 2>&1 || {
  78. echo "coverage-check.sh: pytest not found (pip install pytest pytest-cov)" >&2
  79. exit 5
  80. }
  81. printf '=== Running tests with coverage (threshold %s%%) ===\n' "$THRESHOLD" >&2
  82. pytest \
  83. --cov="$COV_TARGET" \
  84. --cov-report=term-missing \
  85. --cov-report=html \
  86. --cov-fail-under="$THRESHOLD" \
  87. "${PYTEST_ARGS[@]}" >&2
  88. rc=$?
  89. case "$rc" in
  90. 0) printf 'coverage-check\tpass\t-\t%s\n' "$THRESHOLD"; exit 0 ;;
  91. 2) printf 'coverage-check\tfail\t-\t%s\n' "$THRESHOLD"; exit 1 ;; # pytest-cov below-threshold signal
  92. *) printf 'coverage-check.sh: pytest exited %s\n' "$rc" >&2; exit "$rc" ;;
  93. esac