#!/usr/bin/env bash # Show how a code change alters the service parser output for every template and the reference fixture. # # Usage: # ./scripts/parser-snapshot-diff save # 1. before you change code: save the current output # ./scripts/parser-snapshot-diff baseline # 2. after the change: compare with the saved output # ./scripts/parser-snapshot-diff [git-ref] # or compare with a git ref (default: main) # ./scripts/parser-snapshot-diff baseline -- --filter='"template appwrite"' # extra Pest arguments # # Each run parses everything with tests/Feature/ServiceParserReferenceTest.php (about 90 seconds). # A git ref is exported with git archive to a temporary directory and parsed with the current test, # fixtures and templates, so the diff shows only the effect of the code change. # The terminal shows one line per changed case; the full diff is written to storage/framework/testing/. set -euo pipefail repo="$(git -C "$(dirname "$0")/.." rev-parse --show-toplevel)" mode="${1:-main}" shift || true [[ "${1:-}" == "--" ]] && shift pest_args=("$@") testing="$repo/storage/framework/testing" # gitignored, same file system as the repository saved="$testing/parser-snapshots-baseline" report="$testing/parser-snapshot-diff.diff" work="$(mktemp -d "$testing/parser-snapshot-diff.XXXXXX")" trap 'rm -rf "$work"' EXIT # Run the golden test in with UPDATE_PARSER_SNAPSHOTS=1 and copy its expected files to . parse_into() { local dir="$1" target="$2" label="$3" expected="$1/tests/Fixtures/parser/expected" backup="$work/expected-backup" status=0 echo "==> Parsing on $label (about 90 seconds)" # The run rewrites the expected files: put the previous ones back afterwards, so the repository is unchanged. rm -rf "$backup" cp -r "$expected" "$backup" rm -rf "$expected/templates" (cd "$dir" && UPDATE_PARSER_SNAPSHOTS=1 php -d memory_limit=2G artisan test --compact \ tests/Feature/ServiceParserReferenceTest.php "${pest_args[@]}" >"$work/run.log" 2>&1) || status=$? rm -rf "$target" cp -r "$expected" "$target" rm -rf "$expected" mv "$backup" "$expected" if [[ $status -ne 0 ]]; then echo "Test run on $label failed:" >&2 tail -30 "$work/run.log" >&2 exit 1 fi } case "$mode" in save) parse_into "$repo" "$saved" "working tree" echo "Saved baseline: $saved" echo "Change the code, then run: ./scripts/parser-snapshot-diff baseline" exit 0 ;; baseline) [[ -d "$saved" ]] || { echo "No saved baseline. Run ./scripts/parser-snapshot-diff save first." >&2; exit 1; } base_label="saved baseline" cp -r "$saved" "$work/base-expected" ;; *) base_label="$mode" base="$work/base" mkdir -p "$base" echo "==> Exporting $mode ($(git -C "$repo" rev-parse --short "$mode"))" git -C "$repo" archive "$mode" | tar -x -C "$base" # Hard links: fast and no extra disk space. A symlink would make Composer load the classes of $repo. cp -al "$repo/vendor" "$base/vendor" [[ -f "$repo/.env" ]] && cp "$repo/.env" "$base/.env" rm -rf "$base/templates/compose" "$base/tests/Fixtures/parser" cp -r "$repo/templates/compose" "$base/templates/compose" mkdir -p "$base/tests/Fixtures/parser" cp "$repo"/tests/Fixtures/parser/*.yaml "$base/tests/Fixtures/parser/" cp "$repo/tests/Feature/ServiceParserReferenceTest.php" "$base/tests/Feature/" parse_into "$base" "$work/base-expected" "$mode" ;; esac parse_into "$repo" "$work/current-expected" "working tree" echo if diff -ru "$work/base-expected" "$work/current-expected" | sed -E "s#$work/(base|current)-expected/##g" >"$report"; then echo "No differences from $base_label: the parser output of every case is the same." exit 0 fi # Line 2 is the first step (1_deploy): an error there means the case did not parse at all on that side. first_step_error() { sed -n 2p "$1" | grep '^ error:' | cut -c10-130 || true; } summary() { local file="$1" base_file="$work/base-expected/$1" current_file="$work/current-expected/$1" base_error current_error [[ -f "$base_file" ]] || { echo " new case $file"; return; } [[ -f "$current_file" ]] || { echo " removed case $file"; return; } base_error="$(first_step_error "$base_file")" current_error="$(first_step_error "$current_file")" if [[ -n "$base_error" && -z "$current_error" ]]; then echo " base error $file: $base_error" elif [[ -n "$current_error" && -z "$base_error" ]]; then echo " CURRENT ERROR $file: $current_error" else echo " changed $file (+$(diff "$base_file" "$current_file" | grep -c '^>' || true) -$(diff "$base_file" "$current_file" | grep -c '^<' || true))" fi } changed_cases=() while IFS= read -r file; do cmp -s "$work/base-expected/$file" "$work/current-expected/$file" || changed_cases+=("$file") done < <( (cd "$work/base-expected" && find . -name '*.yaml'; cd "$work/current-expected" && find . -name '*.yaml') | sed 's|^\./||' | sort -u) lines="$(for file in "${changed_cases[@]}"; do summary "$file"; done)" echo "${#changed_cases[@]} case(s) differ from $base_label:" echo "$lines" echo echo "Full diff: $report" if grep -q '^ base error' <<<"$lines"; then echo "Note: 'base error' cases did not parse on $base_label, so there is no base output to compare with." fi exit 1