Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ Run the same local gate that backs PR `CI Gate` before opening a PR:
bash scripts/ci/run-fast-checks.sh
```

The gate checks CI policy drift, shell syntax, secret patterns, privacy fixtures, Swift tests, debug and release builds, and CLI help output.
The gate checks CI policy drift, shell syntax, secret patterns, privacy fixtures, Swift tests, source coverage with CRAP-style low-coverage indicators, debug and release builds, and CLI help output.

## Roadmap

Expand Down
1 change: 1 addition & 0 deletions scripts/ci/check-ci-policy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ if [[ -f "$fast_script" ]]; then
require_line "$fast_script" "bash scripts/check-detect-secrets.sh --all-files"
require_line "$fast_script" "bash scripts/check-privacy-fixtures.sh"
require_line "$fast_script" "swift test"
require_line "$fast_script" "bash scripts/ci/test-coverage-indicators.sh"
require_line "$fast_script" "swift build"
require_line "$fast_script" "swift build -c release"
require_line "$fast_script" "swift run icloud-cli --help"
Expand Down
21 changes: 1 addition & 20 deletions scripts/ci/check-coverage.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail

minimum_line_coverage="${COVERAGE_MIN_LINES:-68}"
module_cache="${CLANG_MODULE_CACHE_PATH:-$PWD/.build/module-cache}"
mkdir -p "$module_cache"
export CLANG_MODULE_CACHE_PATH="$module_cache"
Expand All @@ -22,22 +21,4 @@ else
coverage_json="$(swift test --show-codecov-path)"
fi

source_line_coverage="$(
jq -r --arg prefix "$PWD/Sources/ICloudCLICore/" '
[.data[0].files[]
| select(.filename | startswith($prefix))
| .summary.lines]
| {count: (map(.count) | add), covered: (map(.covered) | add)}
| (.covered * 100 / .count)
' "$coverage_json"
)"

printf 'Source line coverage: %.2f%% (minimum %.2f%%)\n' "$source_line_coverage" "$minimum_line_coverage"

awk -v actual="$source_line_coverage" -v minimum="$minimum_line_coverage" '
BEGIN {
if (actual + 0 < minimum + 0) {
exit 1
}
}
'
bash scripts/ci/report-coverage-indicators.sh "$coverage_json" "$PWD/Sources/ICloudCLICore/"
97 changes: 97 additions & 0 deletions scripts/ci/report-coverage-indicators.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
#!/usr/bin/env bash
set -euo pipefail

coverage_json="${1:?usage: report-coverage-indicators.sh COVERAGE_JSON [SOURCE_PREFIX]}"
source_prefix="${2:-$PWD/Sources/ICloudCLICore/}"
minimum_line_coverage="${COVERAGE_MIN_LINES:-68}"
crap_indicator_limit="${CRAP_INDICATOR_LIMIT:-5}"
crap_max_coverage="${CRAP_MAX_COVERAGE:-80}"
crap_min_file_lines="${CRAP_MIN_FILE_LINES:-10}"
crap_min_function_lines="${CRAP_MIN_FUNCTION_LINES:-8}"

source_line_coverage="$(
jq -r --arg prefix "$source_prefix" '
[.data[0].files[]
| select(.filename | startswith($prefix))
| .summary.lines]
| {count: (map(.count // 0) | add // 0), covered: (map(.covered // 0) | add // 0)}
| if .count == 0 then 0 else (.covered * 100 / .count) end
' "$coverage_json"
)"

printf 'Source line coverage: %.2f%% (minimum %.2f%%)\n' "$source_line_coverage" "$minimum_line_coverage"

printf 'CRAP indicators (coverage-driven; llvm-cov JSON does not include cyclomatic complexity):\n'
crap_indicators="$(
jq -r \
--arg prefix "$source_prefix" \
--argjson limit "$crap_indicator_limit" \
--argjson max_coverage "$crap_max_coverage" \
--argjson min_file_lines "$crap_min_file_lines" \
--argjson min_function_lines "$crap_min_function_lines" '
def pct($covered; $count):
if ($count | tonumber) == 0 then 0 else (($covered | tonumber) * 100 / ($count | tonumber)) end;
def line_summary:
.summary.lines as $lines
| {
count: ($lines.count // 0),
covered: ($lines.covered // 0),
pct: pct(($lines.covered // 0); ($lines.count // 0))
};

(
[.data[0].files[]
| select(.filename | startswith($prefix))
| line_summary as $lines
| select($lines.count >= $min_file_lines and $lines.pct < $max_coverage)
| {
kind: "file",
name: (.filename | ltrimstr($prefix)),
pct: $lines.pct,
covered: $lines.covered,
count: $lines.count
}]
+
[.data[0].files[]
| select(.filename | startswith($prefix))
| .filename as $filename
| (.functions // [])[]
| line_summary as $lines
| select($lines.count >= $min_function_lines and $lines.pct < $max_coverage)
| {
kind: "function",
name: (($filename | ltrimstr($prefix)) + "::" + (.name // "<anonymous>")),
pct: $lines.pct,
covered: $lines.covered,
count: $lines.count
}]
)
| sort_by(.pct, (.count * -1), .name)
| .[:$limit]
| .[]
| [.kind, .name, .pct, .covered, .count]
| @tsv
' "$coverage_json" \
| awk -F '\t' '
$1 == "file" {
printf "- %s: %.2f%% line coverage (%s/%s)\n", $2, $3, $4, $5
}
$1 == "function" {
printf "- %s: %.2f%% function line coverage (%s/%s)\n", $2, $3, $4, $5
}
'
)"

if [[ -z "$crap_indicators" ]]; then
echo "- none below ${crap_max_coverage}% coverage with current size thresholds"
else
echo "$crap_indicators"
fi

awk -v actual="$source_line_coverage" -v minimum="$minimum_line_coverage" '
BEGIN {
if (actual + 0 < minimum + 0) {
exit 1
}
}
'
1 change: 1 addition & 0 deletions scripts/ci/run-fast-checks.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ bash scripts/ci/check-shell-syntax.sh
bash scripts/check-detect-secrets.sh --all-files
bash scripts/check-privacy-fixtures.sh
swift test
bash scripts/ci/test-coverage-indicators.sh
bash scripts/ci/check-coverage.sh
bash scripts/ci/run-mutation-smoke.sh
swift build
Expand Down
72 changes: 72 additions & 0 deletions scripts/ci/test-coverage-indicators.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#!/usr/bin/env bash
set -euo pipefail

tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT

fixture="$tmpdir/coverage.json"
source_prefix="$tmpdir/Sources/ICloudCLICore/"
mkdir -p "$source_prefix"

cat >"$fixture" <<JSON
{
"data": [
{
"files": [
{
"filename": "${source_prefix}Healthy.swift",
"summary": {
"lines": { "count": 100, "covered": 95 }
},
"functions": [
{
"name": "healthy()",
"summary": {
"lines": { "count": 24, "covered": 24 }
}
}
]
},
{
"filename": "${source_prefix}Risky.swift",
"summary": {
"lines": { "count": 40, "covered": 10 }
},
"functions": [
{
"name": "riskyBranch()",
"summary": {
"lines": { "count": 20, "covered": 5 }
}
},
{
"name": "untouchedBranch()",
"summary": {
"lines": { "count": 12, "covered": 0 }
}
}
]
}
]
}
]
}
JSON

output="$(
COVERAGE_MIN_LINES=70 CRAP_INDICATOR_LIMIT=4 \
bash scripts/ci/report-coverage-indicators.sh "$fixture" "$source_prefix"
)"

grep -F "Source line coverage: 75.00% (minimum 70.00%)" <<<"$output" >/dev/null
grep -F "CRAP indicators" <<<"$output" >/dev/null
grep -F "Risky.swift: 25.00% line coverage (10/40)" <<<"$output" >/dev/null
grep -F "Risky.swift::untouchedBranch(): 0.00% function line coverage (0/12)" <<<"$output" >/dev/null

if COVERAGE_MIN_LINES=80 bash scripts/ci/report-coverage-indicators.sh "$fixture" "$source_prefix" >/tmp/icloud-cli-coverage-threshold.log 2>&1; then
echo "Expected coverage threshold failure" >&2
cat /tmp/icloud-cli-coverage-threshold.log >&2
exit 1
fi

echo "Coverage indicator tests passed"
Loading