check / check (push) Successful in 31s
A line an earlier canonical commit held counted as the repo's own, so the script stopped on the outdated lines a re-vendor exists to replace. It now clones prompts and also treats every line removed from the file in its history as canonical. The .golangci.yml stop message no longer tells anyone to delete a line. The tests use a local git repository in place of prompts and gain one scenario each for .golangci.yml and .gitignore. Model: opus-5-5
568 lines
16 KiB
Bash
Executable File
568 lines
16 KiB
Bash
Executable File
#!/bin/sh
|
|
# test/update-policy.sh: test script/update-policy on throwaway repos.
|
|
# Each scenario gets one temp directory holding a repo, its local bare
|
|
# origin, a local git repository of the canonical files in place of
|
|
# prompts, and a stub curl that serves them, so nothing touches the
|
|
# network.
|
|
set -eu
|
|
|
|
UPDATE_POLICY="$(cd "$(dirname "$0")/.." && pwd -P)/script/update-policy"
|
|
|
|
dir=""
|
|
trap 'rm -rf "$dir"' EXIT
|
|
|
|
fail() {
|
|
echo "FAIL: $scenario: $*" >&2
|
|
cat "$dir/output" >&2
|
|
exit 1
|
|
}
|
|
|
|
write_stub_curl() {
|
|
mkdir "$dir/bin"
|
|
cat > "$dir/bin/curl" << 'EOF'
|
|
#!/bin/sh
|
|
previous=""
|
|
fail_flag=""
|
|
for argument in "$@"; do
|
|
[ "$previous" = "--output" ] && output="$argument"
|
|
[ "$argument" = "--fail" ] && fail_flag=yes
|
|
previous="$argument"
|
|
done
|
|
url="$previous"
|
|
base="https://git.eeqj.de/sneak/prompts/raw/commit/$HASH/"
|
|
path="${url#"$base"}"
|
|
source="$(dirname "$0")/../canonical/$path"
|
|
if [ "$path" = "$url" ] || [ ! -f "$source" ]; then
|
|
[ -n "$fail_flag" ] && exit 22
|
|
echo "404 page not found" > "$output"
|
|
exit 0
|
|
fi
|
|
cp "$source" "$output"
|
|
EOF
|
|
chmod +x "$dir/bin/curl"
|
|
}
|
|
|
|
write_canonical_files() {
|
|
mkdir -p "$dir/canonical/prompts"
|
|
cat > "$dir/canonical/.gitignore" << 'EOF'
|
|
*.log
|
|
*.tmp
|
|
|
|
# Own entries go below this comment.
|
|
EOF
|
|
cat > "$dir/canonical/.dockerignore" << 'EOF'
|
|
**/*.log
|
|
**/*.tmp
|
|
|
|
# Own entries go below this comment.
|
|
EOF
|
|
cat > "$dir/canonical/.editorconfig" << 'EOF'
|
|
root = true
|
|
[*]
|
|
indent_size = 4
|
|
|
|
# Own sections go below this comment.
|
|
EOF
|
|
cat > "$dir/canonical/.golangci.yml" << 'EOF'
|
|
linters:
|
|
settings:
|
|
depguard:
|
|
rules:
|
|
test-support:
|
|
deny:
|
|
- pkg: net/http/httptest
|
|
desc: canonical
|
|
- pkg: net/http/cgi
|
|
desc: added canonically
|
|
other:
|
|
deny:
|
|
- pkg: example.com/other
|
|
EOF
|
|
echo '{ "tabWidth": 4 }' > "$dir/canonical/.prettierrc"
|
|
echo "new policy" > "$dir/canonical/prompts/REPO_POLICIES.md"
|
|
echo "new api policy" > "$dir/canonical/prompts/API_POLICIES.md"
|
|
}
|
|
|
|
# commit_canonical: commit the canonical files as they stand. The stub
|
|
# curl serves only the newest commit; every other commit gets a 404.
|
|
commit_canonical() {
|
|
git -C "$dir/canonical" add .gitignore .dockerignore .editorconfig \
|
|
.golangci.yml .prettierrc prompts
|
|
git -C "$dir/canonical" commit -q -m "Canonical"
|
|
HASH="$(git -C "$dir/canonical" rev-parse HEAD)"
|
|
}
|
|
|
|
# write_outdated_files: this repo's files, one canonical version behind,
|
|
# with the repo's own entries in the places the script keeps them. The
|
|
# comment above the closing comment in .gitignore is replaced, not kept.
|
|
write_outdated_files() {
|
|
cat > "$dir/repo/.gitignore" << 'EOF'
|
|
*.log
|
|
|
|
# An outdated comment.
|
|
# Own entries go below this comment.
|
|
/myapp
|
|
EOF
|
|
cat > "$dir/repo/.dockerignore" << 'EOF'
|
|
**/*.log
|
|
|
|
# Own entries go below this comment.
|
|
/myapp
|
|
EOF
|
|
cat > "$dir/repo/.editorconfig" << 'EOF'
|
|
root = true
|
|
[*]
|
|
|
|
# Own sections go below this comment.
|
|
[*.py]
|
|
indent_size = 2
|
|
EOF
|
|
cat > "$dir/repo/.golangci.yml" << 'EOF'
|
|
linters:
|
|
settings:
|
|
depguard:
|
|
rules:
|
|
test-support:
|
|
deny:
|
|
- pkg: net/http/httptest
|
|
desc: canonical
|
|
# Our own test helpers.
|
|
- pkg: example.com/app/internal/apptest
|
|
desc: own entry
|
|
other:
|
|
deny:
|
|
- pkg: example.com/other
|
|
EOF
|
|
echo '{ "tabWidth": 2 }' > "$dir/repo/.prettierrc"
|
|
echo "old policy" > "$dir/repo/REPO_POLICIES.md"
|
|
}
|
|
|
|
# start_scenario <name>: make a new temp directory holding a repo with
|
|
# the outdated files, not yet committed, and an empty origin.
|
|
start_scenario() {
|
|
scenario="$1"
|
|
rm -rf "$dir"
|
|
dir="$(mktemp -d)"
|
|
: > "$dir/output"
|
|
write_stub_curl
|
|
write_canonical_files
|
|
|
|
# git clone reads no repo's own config, so the setting that points
|
|
# git at the local canonical repository has to be in this file.
|
|
export GIT_CONFIG_GLOBAL="$dir/gitconfig"
|
|
git config --global user.name "Test"
|
|
git config --global user.email "test@example.com"
|
|
git config --global "url.$dir/canonical.insteadOf" \
|
|
"https://git.eeqj.de/sneak/prompts.git"
|
|
|
|
git init -q -b main "$dir/canonical"
|
|
export HASH
|
|
commit_canonical
|
|
|
|
git init -q --bare -b main "$dir/origin.git"
|
|
git init -q -b main "$dir/repo"
|
|
git -C "$dir/repo" remote add origin "$dir/origin.git"
|
|
|
|
mkdir "$dir/repo/script"
|
|
cp "$UPDATE_POLICY" "$dir/repo/script/update-policy"
|
|
printf '#!/bin/sh\necho example\n' > "$dir/repo/script/projectname"
|
|
chmod +x "$dir/repo/script/projectname"
|
|
write_outdated_files
|
|
}
|
|
|
|
# commit_and_push_fixture <paths...>: commit those paths, remove the
|
|
# rest of the outdated files, and record the commit as $before.
|
|
commit_and_push_fixture() {
|
|
(
|
|
cd "$dir/repo"
|
|
git add script "$@"
|
|
git commit -q -m "Fixture"
|
|
git push -q origin HEAD
|
|
git clean -q -f -x
|
|
)
|
|
before="$(git -C "$dir/repo" rev-parse HEAD)"
|
|
}
|
|
|
|
run_update_policy() {
|
|
status=0
|
|
(cd "$dir/repo" && PATH="$dir/bin:$PATH" script/update-policy "$@") \
|
|
> "$dir/output" 2>&1 || status=$?
|
|
}
|
|
|
|
reject_pushes() {
|
|
printf '#!/bin/sh\nexit 1\n' > "$dir/origin.git/hooks/pre-receive"
|
|
chmod +x "$dir/origin.git/hooks/pre-receive"
|
|
}
|
|
|
|
accept_pushes() {
|
|
rm "$dir/origin.git/hooks/pre-receive"
|
|
}
|
|
|
|
head_of_repo() {
|
|
git -C "$dir/repo" rev-parse HEAD
|
|
}
|
|
|
|
head_of_origin() {
|
|
git -C "$dir/origin.git" rev-parse refs/heads/main
|
|
}
|
|
|
|
expect_success() {
|
|
[ "$status" -eq 0 ] || fail "exited $status"
|
|
}
|
|
|
|
expect_failure() {
|
|
[ "$status" -ne 0 ] || fail "exited 0"
|
|
}
|
|
|
|
expect_output() {
|
|
grep -q -F -e "$1" "$dir/output" || fail "output lacks: $1"
|
|
}
|
|
|
|
expect_output_without() {
|
|
if grep -q -F -e "$1" "$dir/output"; then
|
|
fail "output has: $1"
|
|
fi
|
|
}
|
|
|
|
# expect_file <path>: the file must equal standard input.
|
|
expect_file() {
|
|
cat > "$dir/expected"
|
|
diff "$dir/expected" "$dir/repo/$1" >&2 || fail "$1 is wrong"
|
|
}
|
|
|
|
expect_absent() {
|
|
[ ! -e "$dir/repo/$1" ] || fail "$1 exists"
|
|
}
|
|
|
|
expect_nothing_written() {
|
|
[ -z "$(git -C "$dir/repo" status --porcelain)" ] ||
|
|
fail "the working tree changed"
|
|
[ "$(head_of_repo)" = "$before" ] || fail "the repo has a new commit"
|
|
[ "$(head_of_origin)" = "$before" ] || fail "origin has a new commit"
|
|
}
|
|
|
|
test_own_entries_kept() {
|
|
start_scenario "own entries kept in all four files"
|
|
commit_and_push_fixture .gitignore .dockerignore .editorconfig \
|
|
.golangci.yml .prettierrc REPO_POLICIES.md
|
|
run_update_policy "$HASH"
|
|
expect_success
|
|
|
|
expect_file .gitignore << 'EOF'
|
|
*.log
|
|
*.tmp
|
|
|
|
# Own entries go below this comment.
|
|
/myapp
|
|
EOF
|
|
expect_file .dockerignore << 'EOF'
|
|
**/*.log
|
|
**/*.tmp
|
|
|
|
# Own entries go below this comment.
|
|
/myapp
|
|
EOF
|
|
expect_file .editorconfig << 'EOF'
|
|
root = true
|
|
[*]
|
|
indent_size = 4
|
|
|
|
# Own sections go below this comment.
|
|
[*.py]
|
|
indent_size = 2
|
|
EOF
|
|
expect_file .golangci.yml << 'EOF'
|
|
linters:
|
|
settings:
|
|
depguard:
|
|
rules:
|
|
test-support:
|
|
deny:
|
|
- pkg: net/http/httptest
|
|
desc: canonical
|
|
- pkg: net/http/cgi
|
|
desc: added canonically
|
|
# Our own test helpers.
|
|
- pkg: example.com/app/internal/apptest
|
|
desc: own entry
|
|
other:
|
|
deny:
|
|
- pkg: example.com/other
|
|
EOF
|
|
expect_file .prettierrc << 'EOF'
|
|
{ "tabWidth": 4 }
|
|
EOF
|
|
expect_file REPO_POLICIES.md << 'EOF'
|
|
new policy
|
|
EOF
|
|
|
|
[ "$(head_of_origin)" = "$(head_of_repo)" ] ||
|
|
fail "the commit was not pushed"
|
|
[ "$(head_of_repo)" != "$before" ] || fail "no commit was made"
|
|
git -C "$dir/repo" log -1 --format=%s | grep -q -F -e "$HASH" ||
|
|
fail "the commit message does not name the hash"
|
|
}
|
|
|
|
test_files_not_carried_are_skipped() {
|
|
start_scenario "files the repo does not carry skipped"
|
|
mkdir "$dir/repo/docs"
|
|
mv "$dir/repo/REPO_POLICIES.md" "$dir/repo/docs/REPO_POLICIES.md"
|
|
commit_and_push_fixture docs/REPO_POLICIES.md
|
|
run_update_policy "$HASH"
|
|
expect_success
|
|
|
|
expect_file docs/REPO_POLICIES.md << 'EOF'
|
|
new policy
|
|
EOF
|
|
expect_absent REPO_POLICIES.md
|
|
expect_absent API_POLICIES.md
|
|
expect_absent docs/API_POLICIES.md
|
|
expect_absent .gitignore
|
|
expect_absent .dockerignore
|
|
expect_absent .editorconfig
|
|
expect_absent .golangci.yml
|
|
expect_absent .prettierrc
|
|
[ "$(head_of_origin)" = "$(head_of_repo)" ] ||
|
|
fail "the commit was not pushed"
|
|
}
|
|
|
|
test_nothing_changed() {
|
|
start_scenario "nothing changed"
|
|
cp "$dir/canonical/.prettierrc" "$dir/repo/.prettierrc"
|
|
commit_and_push_fixture .prettierrc
|
|
run_update_policy "$HASH"
|
|
expect_success
|
|
expect_output "nothing to commit"
|
|
expect_nothing_written
|
|
}
|
|
|
|
test_bad_commit_hash() {
|
|
for hash in not-a-hash 0000000000000000000000000000000000000000; do
|
|
start_scenario "bad commit hash $hash stops with nothing written"
|
|
commit_and_push_fixture .gitignore .prettierrc REPO_POLICIES.md
|
|
run_update_policy "$hash"
|
|
expect_failure
|
|
expect_nothing_written
|
|
done
|
|
# The last hash has the right form, so it is the one that reaches curl.
|
|
expect_output "cannot fetch"
|
|
}
|
|
|
|
test_push_failure() {
|
|
start_scenario "push failure exits non-zero"
|
|
commit_and_push_fixture .prettierrc
|
|
reject_pushes
|
|
run_update_policy "$HASH"
|
|
expect_failure
|
|
expect_output "push failed"
|
|
[ "$(head_of_origin)" = "$before" ] || fail "origin has a new commit"
|
|
}
|
|
|
|
# Each of these files, with the repo's own entries above the closing
|
|
# comment. The comment added with them must not be named.
|
|
test_own_entry_above_closing_comment() {
|
|
for file in .gitignore .dockerignore .editorconfig; do
|
|
start_scenario "own entries above the closing comment of $file stop"
|
|
{
|
|
printf '/myapp\n/.tool/\n\n# A comment of our own.\n'
|
|
cat "$dir/repo/$file"
|
|
} > "$dir/merged"
|
|
mv "$dir/merged" "$dir/repo/$file"
|
|
commit_and_push_fixture .gitignore .dockerignore .editorconfig \
|
|
.prettierrc REPO_POLICIES.md
|
|
run_update_policy "$HASH"
|
|
expect_failure
|
|
expect_output "$file has entries"
|
|
expect_output "/myapp"
|
|
expect_output "/.tool/"
|
|
expect_output_without "A comment of our own."
|
|
expect_nothing_written
|
|
done
|
|
}
|
|
|
|
test_retry_after_failed_push() {
|
|
start_scenario "a retry after a failed push delivers the commit"
|
|
commit_and_push_fixture .prettierrc
|
|
reject_pushes
|
|
run_update_policy "$HASH"
|
|
expect_failure
|
|
committed="$(head_of_repo)"
|
|
[ "$committed" != "$before" ] || fail "no commit was made"
|
|
[ "$(head_of_origin)" = "$before" ] || fail "origin has a new commit"
|
|
|
|
accept_pushes
|
|
run_update_policy "$HASH"
|
|
expect_success
|
|
[ "$(head_of_repo)" = "$committed" ] || fail "the retry made a new commit"
|
|
[ "$(head_of_origin)" = "$committed" ] ||
|
|
fail "the retry did not deliver the commit"
|
|
}
|
|
|
|
test_uncommitted_change_stops() {
|
|
start_scenario "a carried file with uncommitted changes stops"
|
|
commit_and_push_fixture .prettierrc REPO_POLICIES.md
|
|
echo "uncommitted" >> "$dir/repo/REPO_POLICIES.md"
|
|
run_update_policy "$HASH"
|
|
expect_failure
|
|
expect_output "REPO_POLICIES.md has uncommitted changes"
|
|
expect_file REPO_POLICIES.md << 'EOF'
|
|
old policy
|
|
uncommitted
|
|
EOF
|
|
[ "$(head_of_repo)" = "$before" ] || fail "the repo has a new commit"
|
|
[ "$(head_of_origin)" = "$before" ] || fail "origin has a new commit"
|
|
}
|
|
|
|
test_gitignore_lacks_canonical_last_line() {
|
|
start_scenario ".gitignore lacking the canonical last line stops"
|
|
printf '*.log\n/myapp\n' > "$dir/repo/.gitignore"
|
|
commit_and_push_fixture .gitignore .prettierrc
|
|
run_update_policy "$HASH"
|
|
expect_failure
|
|
expect_output ".gitignore lacks this line"
|
|
expect_nothing_written
|
|
}
|
|
|
|
test_fetched_golangci_without_deny_list() {
|
|
start_scenario "a fetched .golangci.yml with no deny list stops"
|
|
cat > "$dir/canonical/.golangci.yml" << 'EOF'
|
|
linters:
|
|
settings:
|
|
depguard:
|
|
rules:
|
|
test-support:
|
|
files:
|
|
- "**/*.go"
|
|
other:
|
|
deny:
|
|
- pkg: example.com/other
|
|
EOF
|
|
commit_and_push_fixture .golangci.yml .prettierrc
|
|
run_update_policy "$HASH"
|
|
expect_failure
|
|
expect_output "no deny list in its test-support rule"
|
|
expect_nothing_written
|
|
}
|
|
|
|
test_own_golangci_setting_stops() {
|
|
start_scenario "a .golangci.yml setting the canonical file lacks stops"
|
|
cat >> "$dir/repo/.golangci.yml" << 'EOF'
|
|
# A comment of our own.
|
|
forbidigo:
|
|
forbid:
|
|
- pattern: model.Operator
|
|
EOF
|
|
commit_and_push_fixture .golangci.yml .prettierrc
|
|
run_update_policy "$HASH"
|
|
expect_failure
|
|
expect_output ".golangci.yml has lines"
|
|
expect_output "forbidigo:"
|
|
expect_output "- pattern: model.Operator"
|
|
expect_output_without "A comment of our own."
|
|
expect_nothing_written
|
|
}
|
|
|
|
test_source_repo_stops() {
|
|
start_scenario "the repo the policy files come from stops"
|
|
printf '#!/bin/sh\necho prompts\n' > "$dir/repo/script/projectname"
|
|
commit_and_push_fixture .prettierrc REPO_POLICIES.md
|
|
run_update_policy "$HASH"
|
|
expect_failure
|
|
expect_output "this repo is the source of the policy files"
|
|
expect_nothing_written
|
|
}
|
|
|
|
test_staged_file_stays_out_of_commit() {
|
|
start_scenario "a file staged before the script starts stays out of its commit"
|
|
commit_and_push_fixture .prettierrc
|
|
echo "notes" > "$dir/repo/notes.txt"
|
|
git -C "$dir/repo" add notes.txt
|
|
run_update_policy "$HASH"
|
|
expect_success
|
|
[ "$(git -C "$dir/repo" show --name-only --format= HEAD)" = ".prettierrc" ] ||
|
|
fail "the commit holds more than the policy file"
|
|
[ "$(git -C "$dir/repo" diff --cached --name-only)" = "notes.txt" ] ||
|
|
fail "notes.txt is no longer staged"
|
|
}
|
|
|
|
test_short_commit_hash_refused() {
|
|
start_scenario "a hexadecimal hash shorter than 40 characters is refused"
|
|
commit_and_push_fixture .prettierrc
|
|
run_update_policy "${HASH%?}"
|
|
expect_failure
|
|
expect_output "not a full commit hash"
|
|
expect_nothing_written
|
|
}
|
|
|
|
# git asks the local canonical repository for the head of main, and the
|
|
# stub curl serves only that commit.
|
|
test_no_hash_uses_head_of_main() {
|
|
start_scenario "with no hash the script uses the head of main"
|
|
commit_and_push_fixture .prettierrc
|
|
run_update_policy
|
|
expect_success
|
|
expect_file .prettierrc << 'EOF'
|
|
{ "tabWidth": 4 }
|
|
EOF
|
|
git -C "$dir/repo" log -1 --format=%s | grep -q -F -e "$HASH" ||
|
|
fail "the commit message does not name the head of main"
|
|
[ "$(head_of_origin)" = "$(head_of_repo)" ] ||
|
|
fail "the commit was not pushed"
|
|
}
|
|
|
|
# expect_former_canonical_line_replaced <file> <line>: an earlier
|
|
# canonical commit held the line and the newest does not. This repo's
|
|
# file still has it, and the script replaces it and pushes.
|
|
expect_former_canonical_line_replaced() {
|
|
cp "$dir/canonical/$1" "$dir/newest"
|
|
{ echo "$2"; cat "$dir/newest"; } > "$dir/canonical/$1"
|
|
commit_canonical
|
|
cp "$dir/newest" "$dir/canonical/$1"
|
|
commit_canonical
|
|
{ echo "$2"; cat "$dir/repo/$1"; } > "$dir/merged"
|
|
mv "$dir/merged" "$dir/repo/$1"
|
|
commit_and_push_fixture "$1" .prettierrc
|
|
|
|
run_update_policy "$HASH"
|
|
expect_success
|
|
if grep -q -F -x -e "$2" "$dir/repo/$1"; then
|
|
fail "$1 still has: $2"
|
|
fi
|
|
[ "$(head_of_repo)" != "$before" ] || fail "no commit was made"
|
|
[ "$(head_of_origin)" = "$(head_of_repo)" ] ||
|
|
fail "the commit was not pushed"
|
|
}
|
|
|
|
test_former_canonical_golangci_line_replaced() {
|
|
start_scenario "a .golangci.yml line an earlier canonical commit held is replaced"
|
|
expect_former_canonical_line_replaced .golangci.yml "version: 1"
|
|
}
|
|
|
|
test_former_canonical_gitignore_line_replaced() {
|
|
start_scenario "a .gitignore line above the closing comment that an earlier canonical commit held is replaced"
|
|
expect_former_canonical_line_replaced .gitignore "*.old"
|
|
}
|
|
|
|
main() {
|
|
test_own_entries_kept
|
|
test_files_not_carried_are_skipped
|
|
test_nothing_changed
|
|
test_bad_commit_hash
|
|
test_push_failure
|
|
test_own_entry_above_closing_comment
|
|
test_retry_after_failed_push
|
|
test_uncommitted_change_stops
|
|
test_gitignore_lacks_canonical_last_line
|
|
test_fetched_golangci_without_deny_list
|
|
test_own_golangci_setting_stops
|
|
test_source_repo_stops
|
|
test_staged_file_stays_out_of_commit
|
|
test_short_commit_hash_refused
|
|
test_no_hash_uses_head_of_main
|
|
test_former_canonical_golangci_line_replaced
|
|
test_former_canonical_gitignore_line_replaced
|
|
echo "update-policy tests passed"
|
|
}
|
|
|
|
main "$@"
|