From 0a2d162eb010f77b73524a5ba00b3f84783ffec9 Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Sun, 29 Mar 2026 14:49:48 +0200 Subject: [PATCH] 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 --- horatio/.gitignore | 2 + .../test/planner/planner_test.dart | 2 +- horatio/run.sh | 109 +++++++++++++++++- 3 files changed, 111 insertions(+), 2 deletions(-) create mode 100644 horatio/.gitignore diff --git a/horatio/.gitignore b/horatio/.gitignore new file mode 100644 index 0000000..e1043e5 --- /dev/null +++ b/horatio/.gitignore @@ -0,0 +1,2 @@ +# run.sh step cache +.cache/ diff --git a/horatio/horatio_core/test/planner/planner_test.dart b/horatio/horatio_core/test/planner/planner_test.dart index e0831ac..61afe23 100644 --- a/horatio/horatio_core/test/planner/planner_test.dart +++ b/horatio/horatio_core/test/planner/planner_test.dart @@ -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, ); diff --git a/horatio/run.sh b/horatio/run.sh index dc0e01a..e7c6ed6 100755 --- a/horatio/run.sh +++ b/horatio/run.sh @@ -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 # 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 +# 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