feat(horatio): add step caching to run.sh with -f force flag

Each pipeline step computes a sha256 over its relevant source files and
skips re-execution when the hash matches the cached value. A .cache/
directory under horatio/ stores the per-step hashes.

Cache boundaries:
- *_get: pubspec.yaml
- core_format/analyze/test: all *.dart in horatio_core/
- app_analyze: all *.dart + analysis_options.yaml in horatio_app/
- app_test/dead_code: all *.dart in both packages

Use -f or --force to bypass the cache and re-run everything.

Also fixes:
- shellcheck SC2155 in run.sh and dead_code.sh
- codespell typo (thats -> that's) in planner_test.dart
This commit is contained in:
Krzysztof kuhy Rudnicki 2026-03-29 14:49:48 +02:00
parent da9727a21d
commit 09d8088865
3 changed files with 111 additions and 2 deletions

2
horatio/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
# run.sh step cache
.cache/

View File

@ -58,7 +58,7 @@ void main() {
expect(
comparator.grade(
'To be or not to be that is the question',
"To be or not to be that's the question",
"To be or not to be that's the question",
),
LineMatchGrade.minor,
);

View File

@ -13,6 +13,7 @@
# ./run.sh analyze # Run analysis only
# ./run.sh run # Build and launch the desktop app
# ./run.sh web # Run as Flutter web app (for inspection/testing)
# ./run.sh -f <cmd> # Force-run, ignoring the step cache
# ============================================================================
set -euo pipefail
@ -21,6 +22,48 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_DIR
readonly CORE_DIR="$SCRIPT_DIR/horatio_core"
readonly APP_DIR="$SCRIPT_DIR/horatio_app"
readonly CACHE_DIR="$SCRIPT_DIR/.cache"
FORCE=false
# -- Caching helpers ---------------------------------------------------------
# Compute a sha256 over the contents of every file matching a find expression.
# Usage: files_hash <dir> <find-args...>
# Example: files_hash "$CORE_DIR" -name '*.dart'
files_hash() {
local dir="$1"; shift
find "$dir" "$@" -type f -print0 \
| sort -z \
| xargs -0 sha256sum \
| sha256sum \
| awk '{ print $1 }'
}
# Check whether a step can be skipped. Returns 0 (skip) when the cached hash
# matches the current hash, 1 (run) otherwise. Always returns 1 when FORCE.
step_cached() {
local step="$1"
local current_hash="$2"
if $FORCE; then
return 1
fi
local cache_file="$CACHE_DIR/$step"
if [[ -f "$cache_file" ]] && [[ "$(cat "$cache_file")" == "$current_hash" ]]; then
return 0
fi
return 1
}
# Record a successful step so subsequent runs can skip it.
cache_step() {
local step="$1"
local hash="$2"
mkdir -p "$CACHE_DIR"
printf '%s' "$hash" > "$CACHE_DIR/$step"
}
# -- Helpers -----------------------------------------------------------------
@ -131,49 +174,98 @@ ensure_flutter() {
# -- Core package tasks ------------------------------------------------------
core_get() {
local h
h=$(files_hash "$CORE_DIR" -name 'pubspec.yaml')
if step_cached core_get "$h"; then
echo " [cached] core_get — skipping"
return
fi
heading "Upgrading core dependencies"
cd "$CORE_DIR"
dart pub upgrade --major-versions
cache_step core_get "$h"
}
core_analyze() {
local h
h=$(files_hash "$CORE_DIR" -name '*.dart' -o -name 'analysis_options.yaml')
if step_cached core_analyze "$h"; then
echo " [cached] core_analyze — skipping"
return
fi
heading "Analyzing horatio_core"
cd "$CORE_DIR"
dart analyze --fatal-infos
cache_step core_analyze "$h"
}
core_test() {
local h
h=$(files_hash "$CORE_DIR" -name '*.dart')
if step_cached core_test "$h"; then
echo " [cached] core_test — skipping"
return
fi
heading "Testing horatio_core (with coverage)"
cd "$CORE_DIR"
dart run coverage:test_with_coverage
check_coverage "$CORE_DIR/coverage/lcov.info" "horatio_core" 100
cache_step core_test "$h"
}
core_format() {
local h
h=$(files_hash "$CORE_DIR" -name '*.dart')
if step_cached core_format "$h"; then
echo " [cached] core_format — skipping"
return
fi
heading "Formatting horatio_core"
cd "$CORE_DIR"
dart format --set-exit-if-changed .
cache_step core_format "$h"
}
# -- App tasks ---------------------------------------------------------------
app_get() {
local h
h=$(files_hash "$APP_DIR" -name 'pubspec.yaml')
if step_cached app_get "$h"; then
echo " [cached] app_get — skipping"
return
fi
heading "Upgrading app dependencies"
cd "$APP_DIR"
flutter pub upgrade --major-versions
cache_step app_get "$h"
}
app_analyze() {
local h
h=$(files_hash "$APP_DIR" -name '*.dart' -o -name 'analysis_options.yaml')
if step_cached app_analyze "$h"; then
echo " [cached] app_analyze — skipping"
return
fi
heading "Analyzing horatio_app"
cd "$APP_DIR"
flutter analyze --fatal-infos
cache_step app_analyze "$h"
}
app_test() {
local h
h=$(cat <(files_hash "$CORE_DIR" -name '*.dart') <(files_hash "$APP_DIR" -name '*.dart') | sha256sum | awk '{ print $1 }')
if step_cached app_test "$h"; then
echo " [cached] app_test — skipping"
return
fi
heading "Testing horatio_app (with coverage)"
cd "$APP_DIR"
flutter test --coverage
check_coverage "$APP_DIR/coverage/lcov.info" "horatio_app" 100
cache_step app_test "$h"
}
app_build() {
@ -197,8 +289,15 @@ app_web() {
# -- Pipelines ---------------------------------------------------------------
do_dead_code() {
local h
h=$(cat <(files_hash "$CORE_DIR" -name '*.dart') <(files_hash "$APP_DIR" -name '*.dart') | sha256sum | awk '{ print $1 }')
if step_cached dead_code "$h"; then
echo " [cached] dead_code — skipping"
return
fi
heading "Dead code detection & auto-removal"
bash "$SCRIPT_DIR/dead_code.sh"
cache_step dead_code "$h"
}
do_analyze() {
@ -252,6 +351,14 @@ do_web() {
# -- Main --------------------------------------------------------------------
main() {
# Parse flags.
while [[ "${1:-}" == -* ]]; do
case "$1" in
-f|--force) FORCE=true; shift ;;
*) echo "Unknown flag: $1"; exit 1 ;;
esac
done
local cmd="${1:-full}"
case "$cmd" in
@ -262,7 +369,7 @@ main() {
run) do_run ;;
web) do_web ;;
*)
echo "Usage: $0 {analyze|test|dead-code|full|run|web}"
echo "Usage: $0 [-f|--force] {analyze|test|dead-code|full|run|web}"
exit 1
;;
esac