Skip to content

Commit 8cb9c3b

Browse files
committed
chore(ci): add coverage-all target for per-module SPM coverage gate
The previous make test-coverage target only ran the BocanTests scheme (App folder) and filtered the xccov report to Bocan/Observability targets — none of the SPM modules under Modules/ contributed coverage. This is almost certainly why stale-test regressions slip through between phases. - Scripts/coverage-all.sh iterates every Modules/* package, runs swift test --enable-code-coverage, then parses llvm-cov report for the TOTAL line coverage. Threshold defaults to 0 (report-only) with COVERAGE_THRESHOLD for a global floor and COVERAGE_MIN_<MOD> for per-module floors. - Make target: make coverage-all - Fix two stale tests this gate immediately surfaced: Observability/AppLoggerTests missing 'subsonic' LogCategory Persistence/MigrationTests expecting 19 migrations (now 20) Current floor: Subsonic 2.56%, Scrobble 13.1%, UI 17.6%, Playback 30.0%, Acoustics 37.5%, Persistence 40.1%, Metadata 52.9%, Library 57.1%, AudioEngine 59.0%, Observability 80.9%.
1 parent 82aa01c commit 8cb9c3b

4 files changed

Lines changed: 127 additions & 5 deletions

File tree

Makefile

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.PHONY: help bootstrap bundle-fpcalc embed-deps brew-bundle doctor open generate build tests test test-coverage test-audio-engine test-persistence test-metadata test-library test-acoustics test-ui test-playback test-scrobble uitest lint format format-check install-hooks clean
1+
.PHONY: help bootstrap bundle-fpcalc embed-deps brew-bundle doctor open generate build tests test test-coverage coverage-all test-audio-engine test-persistence test-metadata test-library test-acoustics test-ui test-playback test-scrobble uitest lint format format-check install-hooks clean
22

33
## tests: Run format, lint, full test matrix — one line per stage, errors shown inline
44
tests:
@@ -98,6 +98,15 @@ test-coverage:
9898
| xcbeautify
9999
Scripts/coverage-report.sh build/TestResults.xcresult 80
100100

101+
## coverage-all: Run SPM module tests with coverage and fail if any module is below threshold
102+
## Defaults to report-only (threshold 0). Override with COVERAGE_THRESHOLD=NN for a global
103+
## floor, or COVERAGE_MIN_<MODULE>=NN for a per-module floor (e.g. COVERAGE_MIN_UI=20).
104+
coverage-all:
105+
@echo "=============================="
106+
@echo "= Per-module Coverage Gate"
107+
@echo "=============================="
108+
Scripts/coverage-all.sh $(or $(COVERAGE_THRESHOLD),0)
109+
101110
## test-audio-engine: Run AudioEngine SPM package tests (requires FFmpeg via Homebrew)
102111
test-audio-engine:
103112
@echo "=============================="

Modules/Observability/Tests/ObservabilityTests/AppLoggerTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ struct LogCategoryTests {
6565
let names = Set(LogCategory.allCases.map(\.rawValue))
6666
let expected: Set = [
6767
"app", "audio", "library", "metadata", "persistence",
68-
"ui", "network", "playback", "cast", "scrobble",
68+
"ui", "network", "playback", "cast", "scrobble", "subsonic",
6969
]
7070
#expect(names == expected)
7171
}

Modules/Persistence/Tests/PersistenceTests/MigrationTests.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ struct MigrationTests {
88
func migrationsApplyToEmptyDatabase() async throws {
99
let db = try await Database(location: .inMemory)
1010
let version = try await db.schemaVersion()
11-
#expect(version == 19)
11+
#expect(version == 20)
1212
}
1313

1414
@Test("Integrity check passes after migration")
@@ -63,10 +63,10 @@ struct MigrationTests {
6363
#expect(value == "1")
6464
}
6565

66-
@Test("Migrator reports nineteen migrations")
66+
@Test("Migrator reports twenty migrations")
6767
func migratorReportsAllMigrations() {
6868
let migrator = Migrator.make()
69-
#expect(migrator.migrations.count == 19)
69+
#expect(migrator.migrations.count == 20)
7070
}
7171

7272
@Test("Playlists table has kind and accent_color after M007")

Scripts/coverage-all.sh

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
#!/usr/bin/env bash
2+
# coverage-all.sh — Run `swift test --enable-code-coverage` against every
3+
# SPM module under Modules/ and fail if any module's line coverage falls
4+
# below the threshold.
5+
#
6+
# Usage: Scripts/coverage-all.sh <threshold-percent> [module ...]
7+
# Example: Scripts/coverage-all.sh 70
8+
# Scripts/coverage-all.sh 70 UI Playback
9+
#
10+
# Per-module thresholds can be overridden via env vars of the form
11+
# COVERAGE_MIN_<MODULE>, e.g. COVERAGE_MIN_UI=20 for SwiftUI-heavy targets.
12+
set -euo pipefail
13+
14+
THRESHOLD_DEFAULT="${1:?Usage: $0 <threshold> [module ...]}"
15+
shift || true
16+
17+
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
18+
MODULES_DIR="$REPO_ROOT/Modules"
19+
20+
if [[ "$#" -gt 0 ]]; then
21+
MODULES=("$@")
22+
else
23+
MODULES=()
24+
for d in "$MODULES_DIR"/*/; do
25+
name="$(basename "$d")"
26+
# Skip modules without tests
27+
if [[ -d "$d/Tests" ]] && ls "$d/Tests"/*/ >/dev/null 2>&1; then
28+
MODULES+=("$name")
29+
fi
30+
done
31+
fi
32+
33+
echo "=== coverage-all ==="
34+
echo "Default minimum : ${THRESHOLD_DEFAULT}%"
35+
echo "Modules : ${MODULES[*]}"
36+
echo ""
37+
38+
FAILED=()
39+
SUMMARY=()
40+
41+
for module in "${MODULES[@]}"; do
42+
mod_dir="$MODULES_DIR/$module"
43+
if [[ ! -f "$mod_dir/Package.swift" ]]; then
44+
echo " skip $module (no Package.swift)"
45+
continue
46+
fi
47+
48+
upper="$(echo "$module" | tr '[:lower:]' '[:upper:]')"
49+
threshold_var="COVERAGE_MIN_${upper}"
50+
threshold="${!threshold_var:-$THRESHOLD_DEFAULT}"
51+
52+
echo "------------------------------------------------------------"
53+
echo "[$module] running tests (min ${threshold}%)"
54+
echo "------------------------------------------------------------"
55+
56+
(cd "$mod_dir" && swift test --enable-code-coverage --quiet)
57+
58+
profdata="$mod_dir/.build/debug/codecov/default.profdata"
59+
xctest="$mod_dir/.build/debug/${module}PackageTests.xctest"
60+
if [[ ! -d "$xctest" ]]; then
61+
# Fall back to glob
62+
xctest="$(find "$mod_dir/.build/debug" -maxdepth 1 -type d -name '*PackageTests.xctest' | head -1)"
63+
fi
64+
binary="$xctest/Contents/MacOS/$(basename "$xctest" .xctest)"
65+
66+
if [[ ! -f "$profdata" || ! -x "$binary" ]]; then
67+
echo " ERROR: missing coverage artefacts for $module"
68+
echo " profdata: $profdata"
69+
echo " binary : $binary"
70+
FAILED+=("$module (missing artefacts)")
71+
continue
72+
fi
73+
74+
total_line="$(xcrun llvm-cov report \
75+
"$binary" \
76+
-instr-profile="$profdata" \
77+
-ignore-filename-regex='(\.build|/Tests/|/checkouts/|\.derivedSources)' \
78+
2>/dev/null \
79+
| awk '/^TOTAL/ {print $7}' \
80+
| tr -d '%')"
81+
82+
if [[ -z "$total_line" ]]; then
83+
echo " ERROR: could not parse llvm-cov output for $module"
84+
FAILED+=("$module (parse error)")
85+
continue
86+
fi
87+
88+
# llvm-cov prints percentages with one decimal; compare as integer floor.
89+
percent_int="$(printf '%.0f' "$total_line")"
90+
if [[ "$percent_int" -lt "$threshold" ]]; then
91+
mark="✗ BELOW ${threshold}%"
92+
FAILED+=("$module: ${total_line}% < ${threshold}%")
93+
else
94+
mark=""
95+
fi
96+
line=" $module : ${total_line}% $mark"
97+
echo "$line"
98+
SUMMARY+=("$line")
99+
done
100+
101+
echo ""
102+
echo "=== Summary ==="
103+
for s in "${SUMMARY[@]}"; do echo "$s"; done
104+
105+
if [[ "${#FAILED[@]}" -gt 0 ]]; then
106+
echo ""
107+
echo "Coverage gate failed:"
108+
for f in "${FAILED[@]}"; do echo " - $f"; done
109+
exit 1
110+
fi
111+
112+
echo ""
113+
echo "All modules at or above their threshold."

0 commit comments

Comments
 (0)