#!/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