lib.sh
#!/bin/bash
## Shared helpers for the patchette tests. Sourced, not executed.
## Provides a hermetic, fully-pinned git environment (fixed identities and a
## tick clock belong HERE, not in the tool: tests author commits, patchette
## never does), the test corpus builder, and PASS/FAIL reporting helpers.
set -euo pipefail
export GIT_CONFIG_GLOBAL=/dev/null
export GIT_CONFIG_SYSTEM=/dev/null
export GIT_CONFIG_NOSYSTEM=1
export GIT_ATTR_NOSYSTEM=1
export LC_ALL=C.UTF-8
export TZ=UTC
export GIT_AUTHOR_NAME="Ada Downstream"
export GIT_AUTHOR_EMAIL="ada@downstream.example"
export GIT_COMMITTER_NAME="Ada Downstream"
export GIT_COMMITTER_EMAIL="ada@downstream.example"
PATCHETTE=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/bin/patchette
RESULT=0
## EXPECT is the single assertion primitive for tests.
##
## A failed expectation prints a FAIL line, marks the script failed, and lets
## the test continue; setup failures, by contrast, abort via set -e.
## (Hence EXPECT, not ASSERT: an ASSERT would abort.)
##
## EXPECT success <desc> -- <cmd>...
## EXPECT refusal <desc> <message-fragment> -- <cmd>...
## EXPECT equal <desc> -- <got> <want>
##
## The `--` is required: it marks where EXPECT's own arguments end,
## and checking its position catches a missing or extra argument at the call site.
## House layout puts the mode and description on one line, ending in `-- \`,
## and the subject on the next, indented:
##
## EXPECT refusal "a duplicate series entry is an error" "duplicate series entry" -- \
## "$PATCHETTE" verify --subject="$subject" --patches="$patches"
##
## `refusal` passes only when the command exits nonzero AND its output contains
## the fragment: an exit code alone could come from any guard (or any crash),
## and each test should stay tied to its specific refusal.
## Commands run with output captured, shown only on failure.
## They run on the right side of `||`, which disables set -e within them:
## fine for external commands, but a shell function passed in won't stop at its first failure.
## Misuse (unknown mode, misplaced `--`, wrong arity) is a harness bug and exits the script.
##
## - @param $1 mode: success, refusal, or equal
## - @param $2 check description
## - @stdout one PASS or FAIL line
## - @state RESULT -- set to 1 on failure
EXPECT() {
local mode=${1:-} desc=${2:-} want='' out rc=0
shift 2 || _harness_bug "EXPECT: needs at least <mode> <desc>"
if [[ $mode == refusal ]]; then
[[ $# -ge 1 ]] || _harness_bug "EXPECT refusal '$desc': missing message fragment"
want=$1
shift
fi
[[ ${1:-} == -- ]] || _harness_bug "EXPECT $mode '$desc': expected -- where its own arguments end, got '${1:-}'"
shift
case $mode in
success)
[[ $# -ge 1 ]] || _harness_bug "EXPECT success '$desc': no command after --"
out=$("$@" 2>&1) || rc=$?
if [[ $rc -eq 0 ]]; then
_pass "$desc"
else
_fail "$desc: exit $rc; output: $out"
fi
;;
refusal)
[[ $# -ge 1 ]] || _harness_bug "EXPECT refusal '$desc': no command after --"
out=$("$@" 2>&1) || rc=$?
if [[ $rc -eq 0 ]]; then
_fail "$desc: expected a refusal, but the command succeeded"
elif [[ $out != *"$want"* ]]; then
_fail "$desc: refused, but not with the expected message '$want'; got: $out"
else
_pass "$desc"
fi
;;
equal)
[[ $# -eq 2 ]] || _harness_bug "EXPECT equal '$desc': needs exactly <got> <want> after --"
if [[ $1 == "$2" ]]; then
_pass "$desc"
else
_fail "$desc: got '$1', want '$2'"
fi
;;
*)
_harness_bug "EXPECT: unknown mode '$mode'"
;;
esac
}
_pass() {
echo "PASS $*"
}
_fail() {
echo "FAIL $*"
RESULT=1
}
_harness_bug() {
>&2 echo "harness bug: $*"
exit 2
}
_TICK=1700000000
## Advance the fixed clock by one minute, for the next commit.
##
## - @state GIT_AUTHOR_DATE, GIT_COMMITTER_DATE -- set to the new clock value.
tick() {
_TICK=$((_TICK + 60))
export GIT_AUTHOR_DATE="@${_TICK} +0000"
export GIT_COMMITTER_DATE="@${_TICK} +0000"
}
## Build the test corpus: an upstream repo, and a dev clone carrying two
## to-be-tracked branches. Every commit is hostile on purpose.
##
## `topic/fancy` exercises: linear commits, a subject-slug collision, a rename,
## a merge (with an in-range side branch), an extra commit header in the
## jj `change-id` style, an empty commit, a body with a blank line, and a
## message without a final newline.
## `docs-polish` exists to make the layout's branch mapping visible.
## `odd-bytes` carries content that git config or attributes could perturb.
##
## - @param $1 output directory; created. Gets `upstream/`, `dev/`, and a
## sourceable `FACTS` file recording the base SHA and branch tips.
make_corpus() (
local out=$1 base first raw new
mkdir -p "$out"
out=$(cd "$out" && pwd)
# --- upstream ---
git init -q -b main "$out/upstream"
cd "$out/upstream"
{
printf 'The frobnicator.\n'
for i in 1 2 3 4 5 6 7 8; do printf 'gear %s turns smoothly\n' "$i"; done
} >frobnicator.txt
mkdir widgets
printf 'widget: mk1\nstate: dusty\n' >widgets/widget.txt
printf '# Machinery\n\nUpstream docs.\n' >README.md
git add -A
tick
git commit -q -m "Initial machinery"
printf '\nMore upstream docs.\n' >>README.md
git add -A
tick
git commit -q -m "Document the machinery"
base=$(git rev-parse HEAD)
# --- dev clone, branch topic/fancy ---
git clone -q "$out/upstream" "$out/dev"
cd "$out/dev"
git switch -qc topic/fancy
# plain linear change
sed -i 's/gear 3 turns smoothly/gear 3 turns BETTER/' frobnicator.txt
git add -A
tick
git commit -q -m "Fix the frobnicator"
first=$(git rev-parse HEAD)
# same subject again: forces the slug collision rule to show itself
sed -i 's/gear 5 turns smoothly/gear 5 turns BETTER/' frobnicator.txt
git add -A
tick
git commit -q -m "Fix the frobnicator"
# rename plus edit
mkdir gizmos
git mv widgets/widget.txt gizmos/widget.txt
sed -i 's/state: dusty/state: polished/' gizmos/widget.txt
git add -A
tick
git commit -q -m "Move widget into place"
# a side branch forked in-range, then merged: exercises parents-as-slugs
git switch -qc sprocket-work "$first"
printf '# Sprockets\n\nThey spin.\n' >SPROCKETS.md
git add -A
tick
git commit -q -m "Add sprocket docs"
git switch -q topic/fancy
tick
git merge -q --no-ff -m "Merge sprocket work" sprocket-work
git branch -qD sprocket-work
# extra commit header, jj change-id style; handcrafted via raw object rewrite.
# The body (with its blank line) exercises message framing in the exports.
sed -i 's/gear 7 turns smoothly/gear 7 hums quietly/' frobnicator.txt
git add -A
tick
git commit -q -m "Teach gizmo to hum" -m $'The hum is load-bearing.\nDo not ask why.'
raw=$(mktemp)
git cat-file commit HEAD >"$raw"
new=$(sed '/^committer /a change-id kpqxwvuzyntsmrlokqjihvgzfyexwdcu' "$raw" |
git hash-object -t commit -w --stdin)
rm "$raw"
git update-ref refs/heads/topic/fancy "$new"
# empty commit
tick
git commit -q --allow-empty -m "Note to self"
# message without a final newline: porcelain never makes one, but the object
# model allows it, and total fidelity means total
sed -i 's/gear 8 turns smoothly/gear 8 clicks twice/' frobnicator.txt
git add -A
tick
git commit -q -m "Scribble scribble"
new=$(git cat-file commit HEAD | head -c -1 | git hash-object -t commit -w --stdin)
git update-ref refs/heads/topic/fancy "$new"
# --- second branch, small on purpose ---
git switch -qc docs-polish main
printf '\nDownstream notes: be kind to the machinery.\n' >>README.md
git add -A
tick
git commit -q -m "Polish the readme"
# --- third branch: content that git config or attributes could bite on ---
# Trailing whitespace, CRLF, binary, a non-ASCII path with a space, a
# symlink, a mode change, and C code whose edit makes nearby hunks with
# function-name hints and blank context lines. The second commit is the
# one that matters: creations have no context to perturb.
git switch -qc odd-bytes main
mkdir odd
printf 'clean line\ntrailing \nclean again\n' >odd/trailing.txt
printf 'one\r\ntwo\r\n' >odd/crlf.txt
printf 'bin\0ary\0\1\2' >odd/blob.bin
printf 'na\xc3\xafve\n' >"odd/naïve file.txt"
printf '#!/bin/sh\necho hi\n' >odd/run.sh
ln -s trailing.txt odd/link
{
printf '#include <stdio.h>\n\nint gear(int n)\n{\n\tint x = n;\n\n\tx = x * 2;\n\treturn x;\n}\n'
for i in 1 2 3 4 5 6 7 8; do printf '/* filler %s */\n' "$i"; done
printf 'int hum(void)\n{\n\tint y = 1;\n\n\ty = y + 1;\n\n\treturn y;\n}\n'
} >odd/tool.c
git add -A
tick
git commit -q -m "Add odd bytes"
sed -i 's/x = x \* 2;/x = x * 3;/; s/y = y + 1;/y = y + 2;/' odd/tool.c
printf 'more trailing \t\n' >>odd/trailing.txt
printf 'three\r\n' >>odd/crlf.txt
printf '\3\0' >>odd/blob.bin
printf 'still na\xc3\xafve\n' >>"odd/naïve file.txt"
chmod +x odd/run.sh
git add -A
tick
git commit -q -m "Stir odd bytes"
{
printf 'base=%s\n' "$base"
printf 'tip_topic_fancy=%s\n' "$(git rev-parse topic/fancy)"
printf 'tip_docs_polish=%s\n' "$(git rev-parse docs-polish)"
printf 'tip_odd_bytes=%s\n' "$(git rev-parse odd-bytes)"
} >"$out/FACTS"
)
tests/lib.sh · 8.9 KiB · 271bda8865