mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-11-10 19:07:16 +01:00
Split the Flutter coverage gate into per-tree shard hooks plus a merge hook
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (flutter, flutter-analyze, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (mypy, mypy, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (pylint, pylint, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (rest, , mypy,pylint,flutter-analyze) (push) Waiting to run
Tests / test (push) Waiting to run
Tests / lock-popup-safety (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (0) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (1) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (2) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (3) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (4) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (5) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (6) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (7) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (8) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (9) (push) Waiting to run
Tests / flutter-coverage (push) Blocked by required conditions
Tests / web-coverage (push) Waiting to run
release-apk / release (APK) (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (flutter, flutter-analyze, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (mypy, mypy, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (pylint, pylint, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (rest, , mypy,pylint,flutter-analyze) (push) Waiting to run
Tests / test (push) Waiting to run
Tests / lock-popup-safety (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (0) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (1) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (2) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (3) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (4) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (5) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (6) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (7) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (8) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (9) (push) Waiting to run
Tests / flutter-coverage (push) Blocked by required conditions
Tests / web-coverage (push) Waiting to run
release-apk / release (APK) (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
The whole-suite check (~96 s capped) outlived the 60 s action cap. Manual-stage ids flutter-test-coverage-shard-0..3 each record their bucket's lcov under the HEAD tree hash; flutter-test-coverage-merge fails unless all four exist for the same tree, then applies the unchanged 100% bar and lib-file guard to the union. CI (--shard/--merged) is unchanged. Claude-Session: https://claude.ai/code/session_01WzxdWYmRsPBNf6gi2LVjFA Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6060ebeb4e
commit
8b0b881ec1
@ -272,12 +272,46 @@ repos:
|
||||
language: system
|
||||
files: '^stronglift_replacement/workout_app/.*\.(dart|yaml)$'
|
||||
pass_filenames: false
|
||||
- id: flutter-test-coverage
|
||||
name: flutter test --coverage 100% (workout app)
|
||||
entry: bash scripts/check_flutter_coverage.sh
|
||||
- id: flutter-test-coverage-shard-0
|
||||
name: flutter test bucket 0/4 (workout app, local gate)
|
||||
entry: bash scripts/check_flutter_coverage.sh --gate-shard 0
|
||||
language: system
|
||||
files: '^stronglift_replacement/workout_app/.*\.dart$'
|
||||
pass_filenames: false
|
||||
always_run: true
|
||||
require_serial: true
|
||||
stages: [manual]
|
||||
- id: flutter-test-coverage-shard-1
|
||||
name: flutter test bucket 1/4 (workout app, local gate)
|
||||
entry: bash scripts/check_flutter_coverage.sh --gate-shard 1
|
||||
language: system
|
||||
pass_filenames: false
|
||||
always_run: true
|
||||
require_serial: true
|
||||
stages: [manual]
|
||||
- id: flutter-test-coverage-shard-2
|
||||
name: flutter test bucket 2/4 (workout app, local gate)
|
||||
entry: bash scripts/check_flutter_coverage.sh --gate-shard 2
|
||||
language: system
|
||||
pass_filenames: false
|
||||
always_run: true
|
||||
require_serial: true
|
||||
stages: [manual]
|
||||
- id: flutter-test-coverage-shard-3
|
||||
name: flutter test bucket 3/4 (workout app, local gate)
|
||||
entry: bash scripts/check_flutter_coverage.sh --gate-shard 3
|
||||
language: system
|
||||
pass_filenames: false
|
||||
always_run: true
|
||||
require_serial: true
|
||||
stages: [manual]
|
||||
# Fails unless every shard above passed on THIS tree, then applies the
|
||||
# unchanged 100% bar and every-lib-file guard to their union.
|
||||
- id: flutter-test-coverage-merge
|
||||
name: flutter coverage 100% (union of the 4 buckets)
|
||||
entry: bash scripts/check_flutter_coverage.sh --gate-merge
|
||||
language: system
|
||||
pass_filenames: false
|
||||
always_run: true
|
||||
require_serial: true
|
||||
stages: [manual]
|
||||
|
||||
|
||||
@ -15,22 +15,43 @@
|
||||
# union N shards' lcov files
|
||||
# into coverage/lcov.info,
|
||||
# then both checks
|
||||
# scripts/check_flutter_coverage.sh --gate-shard I
|
||||
# local-gate form of --shard:
|
||||
# bucket I of GATE_SHARDS on
|
||||
# a CLEAN tree, lcov kept for
|
||||
# that tree hash
|
||||
# scripts/check_flutter_coverage.sh --gate-merge the checks on the union of
|
||||
# ALL GATE_SHARDS kept lcovs
|
||||
# of the current tree
|
||||
#
|
||||
# Why shards: the suite took ~150 s on one 4-core runner, almost all of it
|
||||
# compiling 77 test files. `flutter test --total-shards` cannot help -- it
|
||||
# splits the test CASES inside every file, so each shard still compiles all
|
||||
# 77 -- hence buckets of whole files, one per CI runner, and a fan-in job that
|
||||
# runs the same two checks on their union.
|
||||
#
|
||||
# The --gate-* forms exist because one whole-suite run (~96 s under the 4 GiB
|
||||
# cap) outlives the 60 s per-command limit. They are the manual-stage hooks
|
||||
# flutter-test-coverage-shard-0..N-1 and flutter-test-coverage-merge, and stay
|
||||
# fail-closed: a shard refuses a tree that is not exactly HEAD, files its lcov
|
||||
# under that tree's hash, and the merge refuses unless every shard of the SAME
|
||||
# tree is present -- so the gate stamp needs all shards green on one tree.
|
||||
set -euo pipefail
|
||||
|
||||
APP_DIR="stronglift_replacement/workout_app"
|
||||
LCOV_INFO="$APP_DIR/coverage/lcov.info"
|
||||
# Number of local-gate buckets; must equal the flutter-test-coverage-shard-*
|
||||
# hooks in .pre-commit-config.yaml (the merge counts them, see below).
|
||||
GATE_SHARDS=4
|
||||
|
||||
die() {
|
||||
echo "ERROR: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# shellcheck source=flutter_coverage_lib.sh
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/flutter_coverage_lib.sh"
|
||||
|
||||
# Runs `flutter test --coverage` in the app, on the given test files (none =
|
||||
# the whole suite).
|
||||
run_flutter_tests() {
|
||||
@ -50,72 +71,6 @@ run_flutter_tests() {
|
||||
flutter test --coverage "${concurrency[@]}" "$@")
|
||||
}
|
||||
|
||||
# Prints bucket INDEX of TOTAL: every TOTAL-th test file, in a fixed sort
|
||||
# order, so the N buckets partition the suite exactly.
|
||||
shard_files() {
|
||||
local index=$1 total=$2 i=0 file
|
||||
while IFS= read -r file; do
|
||||
if (( i % total == index )); then
|
||||
printf '%s\n' "$file"
|
||||
fi
|
||||
i=$(( i + 1 ))
|
||||
done < <(cd "$APP_DIR" && find test -name '*_test.dart' | LC_ALL=C sort)
|
||||
}
|
||||
|
||||
# Writes to OUT the union of the given lcov files: per source file, every
|
||||
# line any shard instrumented, hit if any shard hit it -- which is what one
|
||||
# unsharded run reports, since flutter merges its test isolates the same way.
|
||||
# LF/LH are recomputed from that union rather than summed, because a lib file
|
||||
# loaded by two shards would otherwise count its lines twice.
|
||||
merge_lcov() {
|
||||
local out=$1 file line sf="" rest ln hits key lf lh
|
||||
shift
|
||||
local -A hit=() lines_of=() seen=()
|
||||
local -a order=()
|
||||
for file in "$@"; do
|
||||
while IFS= read -r line; do
|
||||
case $line in
|
||||
SF:*)
|
||||
sf=${line#SF:}
|
||||
if [[ -z ${seen[$sf]:-} ]]; then
|
||||
seen[$sf]=1
|
||||
order+=("$sf")
|
||||
fi
|
||||
;;
|
||||
DA:*)
|
||||
rest=${line#DA:}
|
||||
ln=${rest%%,*}
|
||||
rest=${rest#*,}
|
||||
hits=${rest%%,*}
|
||||
key="$sf|$ln"
|
||||
if [[ -z ${hit[$key]:-} ]]; then
|
||||
hit[$key]=$hits
|
||||
lines_of[$sf]+="$ln "
|
||||
else
|
||||
hit[$key]=$(( hit[$key] + hits ))
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done < "$file"
|
||||
done
|
||||
mkdir -p "$(dirname "$out")"
|
||||
for sf in "${order[@]}"; do
|
||||
echo "SF:$sf"
|
||||
lf=0
|
||||
lh=0
|
||||
for ln in ${lines_of[$sf]:-}; do
|
||||
echo "DA:$ln,${hit[$sf|$ln]}"
|
||||
lf=$(( lf + 1 ))
|
||||
if (( hit[$sf|$ln] > 0 )); then
|
||||
lh=$(( lh + 1 ))
|
||||
fi
|
||||
done
|
||||
echo "LF:$lf"
|
||||
echo "LH:$lh"
|
||||
echo "end_of_record"
|
||||
done > "$out"
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
"")
|
||||
run_flutter_tests
|
||||
@ -131,6 +86,45 @@ case "${1:-}" in
|
||||
run_flutter_tests "${files[@]}"
|
||||
exit 0
|
||||
;;
|
||||
--gate-shard)
|
||||
[[ ${2:-} =~ ^[0-9]+$ ]] || die "--gate-shard wants a shard index"
|
||||
index=$2
|
||||
(( index < GATE_SHARDS )) || die "shard index $index is not below $GATE_SHARDS"
|
||||
require_hooks_match
|
||||
require_clean_tree
|
||||
tree=$(git rev-parse 'HEAD^{tree}')
|
||||
dir=$(gate_dir)
|
||||
# Older trees can never be stamped again; this bounds the cache.
|
||||
find "$dir" -mindepth 1 -maxdepth 1 -type d ! -name "$tree" -exec rm -rf {} + 2>/dev/null || true
|
||||
mkdir -p "$dir/$tree"
|
||||
rm -f "$dir/$tree/shard-$index.lcov"
|
||||
mapfile -t files < <(shard_files "$index" "$GATE_SHARDS")
|
||||
(( ${#files[@]} > 0 )) || die "shard $index/$GATE_SHARDS has no test files"
|
||||
echo "Gate shard $index/$GATE_SHARDS: ${#files[@]} test files, tree ${tree:0:12}"
|
||||
rm -f "$LCOV_INFO"
|
||||
run_flutter_tests "${files[@]}"
|
||||
[[ -s "$LCOV_INFO" ]] || die "flutter produced no $LCOV_INFO"
|
||||
# The tests must have left the tree untouched, or the lcov is not "of" it.
|
||||
require_clean_tree
|
||||
[[ $(git rev-parse 'HEAD^{tree}') == "$tree" ]] || die "HEAD moved while the shard ran"
|
||||
cp "$LCOV_INFO" "$dir/$tree/shard-$index.lcov.tmp"
|
||||
mv -f "$dir/$tree/shard-$index.lcov.tmp" "$dir/$tree/shard-$index.lcov"
|
||||
echo "Recorded shard $index for tree ${tree:0:12}."
|
||||
exit 0
|
||||
;;
|
||||
--gate-merge)
|
||||
require_hooks_match
|
||||
require_clean_tree
|
||||
tree=$(git rev-parse 'HEAD^{tree}')
|
||||
dir=$(gate_dir)/$tree
|
||||
shard_lcovs=()
|
||||
for (( i = 0; i < GATE_SHARDS; i++ )); do
|
||||
[[ -s "$dir/shard-$i.lcov" ]] \
|
||||
|| die "no lcov for shard $i of tree ${tree:0:12} -- run hook flutter-test-coverage-shard-$i first."
|
||||
shard_lcovs+=("$dir/shard-$i.lcov")
|
||||
done
|
||||
merge_lcov "$LCOV_INFO" "${shard_lcovs[@]}"
|
||||
;;
|
||||
--merged)
|
||||
[[ ${2:-} =~ ^[1-9][0-9]*$ ]] || die "--merged wants a shard count first"
|
||||
expected=$2
|
||||
|
||||
94
scripts/flutter_coverage_lib.sh
Normal file
94
scripts/flutter_coverage_lib.sh
Normal file
@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env bash
|
||||
# Helpers for check_flutter_coverage.sh (sourced, never run): test bucketing,
|
||||
# lcov union, and the clean-tree guards of the local-gate shard hooks. Split out
|
||||
# to keep both files under the 250-line cap. Expects APP_DIR, GATE_SHARDS and
|
||||
# die() from the caller.
|
||||
|
||||
# Prints bucket INDEX of TOTAL: every TOTAL-th test file, in a fixed sort
|
||||
# order, so the N buckets partition the suite exactly.
|
||||
shard_files() {
|
||||
local index=$1 total=$2 i=0 file
|
||||
while IFS= read -r file; do
|
||||
if (( i % total == index )); then
|
||||
printf '%s\n' "$file"
|
||||
fi
|
||||
i=$(( i + 1 ))
|
||||
done < <(cd "$APP_DIR" && git ls-files -- 'test/*_test.dart' | LC_ALL=C sort)
|
||||
}
|
||||
|
||||
# Gate helpers -----------------------------------------------------------------
|
||||
|
||||
gate_dir() {
|
||||
printf '%s/coverage-shards\n' "$(git rev-parse --git-dir)"
|
||||
}
|
||||
|
||||
# Refuses unless the working tree is exactly HEAD: the stamp records HEAD's
|
||||
# tree, so the tests must have seen nothing else. Untracked files are ignored
|
||||
# on purpose (as gate_stamp.sh does); shard_files only lists tracked tests.
|
||||
require_clean_tree() {
|
||||
[[ -z "$(git status --porcelain --untracked-files=no)" ]] \
|
||||
|| die "working tree differs from HEAD -- commit first, then run the gate."
|
||||
}
|
||||
|
||||
# Ensures the hook list in .pre-commit-config.yaml has GATE_SHARDS shards, so
|
||||
# raising/lowering the constant alone cannot silently drop a bucket.
|
||||
require_hooks_match() {
|
||||
local n
|
||||
n=$(grep -c '^ - id: flutter-test-coverage-shard-' .pre-commit-config.yaml || true)
|
||||
(( n == GATE_SHARDS )) \
|
||||
|| die ".pre-commit-config.yaml declares $n flutter-test-coverage-shard-* hooks, GATE_SHARDS is $GATE_SHARDS"
|
||||
}
|
||||
|
||||
# Writes to OUT the union of the given lcov files: per source file, every
|
||||
# line any shard instrumented, hit if any shard hit it -- which is what one
|
||||
# unsharded run reports, since flutter merges its test isolates the same way.
|
||||
# LF/LH are recomputed from that union rather than summed, because a lib file
|
||||
# loaded by two shards would otherwise count its lines twice.
|
||||
merge_lcov() {
|
||||
local out=$1 file line sf="" rest ln hits key lf lh
|
||||
shift
|
||||
local -A hit=() lines_of=() seen=()
|
||||
local -a order=()
|
||||
for file in "$@"; do
|
||||
while IFS= read -r line; do
|
||||
case $line in
|
||||
SF:*)
|
||||
sf=${line#SF:}
|
||||
if [[ -z ${seen[$sf]:-} ]]; then
|
||||
seen[$sf]=1
|
||||
order+=("$sf")
|
||||
fi
|
||||
;;
|
||||
DA:*)
|
||||
rest=${line#DA:}
|
||||
ln=${rest%%,*}
|
||||
rest=${rest#*,}
|
||||
hits=${rest%%,*}
|
||||
key="$sf|$ln"
|
||||
if [[ -z ${hit[$key]:-} ]]; then
|
||||
hit[$key]=$hits
|
||||
lines_of[$sf]+="$ln "
|
||||
else
|
||||
hit[$key]=$(( hit[$key] + hits ))
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done < "$file"
|
||||
done
|
||||
mkdir -p "$(dirname "$out")"
|
||||
for sf in "${order[@]}"; do
|
||||
echo "SF:$sf"
|
||||
lf=0
|
||||
lh=0
|
||||
for ln in ${lines_of[$sf]:-}; do
|
||||
echo "DA:$ln,${hit[$sf|$ln]}"
|
||||
lf=$(( lf + 1 ))
|
||||
if (( hit[$sf|$ln] > 0 )); then
|
||||
lh=$(( lh + 1 ))
|
||||
fi
|
||||
done
|
||||
echo "LF:$lf"
|
||||
echo "LH:$lh"
|
||||
echo "end_of_record"
|
||||
done > "$out"
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user