forked from heavy-duty/ceremony
Scoped to the fixtures the new rule actually binds: a fragment whose predicate complaint is already its name, a smuggled heading, a dangling heading or the 300-character bound is left alone, so the diagnosis it tests is still the one it draws. The section-predicate fixtures are untouched (D4). The computed-length fixtures keep their measured lengths: the cite is seven characters, so an entry that must measure exactly 300 builds 293 of the run and lets the cite carry the rest. 33.md's cite lands on the last continuation line, which is the wrapped-citation case. Refs #262
437 lines
13 KiB
Bash
437 lines
13 KiB
Bash
#!/usr/bin/env bash
|
|
# Contract tests for bin/changelog-assemble (issue #114). Constructed
|
|
# fixture trees, no git repos — the same discipline as
|
|
# test/changelog-armed.test.sh. set -u, not -e: failing commands are
|
|
# behavior for the harness to inspect.
|
|
set -u
|
|
|
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
# shellcheck source=test/harness.sh
|
|
. "$ROOT/test/harness.sh"
|
|
# shellcheck source=lib/changelog.sh
|
|
. "$ROOT/lib/changelog.sh"
|
|
|
|
TOOL="$ROOT/bin/changelog-assemble"
|
|
SECTION="$ROOT/bin/changelog-section"
|
|
|
|
TMP="$(mktemp -d)"
|
|
trap 'rm -rf "$TMP"' EXIT
|
|
|
|
# tree <name> — a fixture tree with changelog.d/ and its README marker;
|
|
# the changelog body arrives on stdin.
|
|
tree() {
|
|
mkdir -p "$TMP/$1/changelog.d"
|
|
printf 'Machine-assembled; see heavy-duty/ceremony#112.\n' >"$TMP/$1/changelog.d/README.md"
|
|
cat >"$TMP/$1/CHANGELOG.md"
|
|
}
|
|
|
|
# frag <tree> <name> — a fragment; body on stdin.
|
|
frag() {
|
|
cat >"$TMP/$1/changelog.d/$2"
|
|
}
|
|
|
|
# The tool reads the consumer's tree at its working directory, so every
|
|
# case runs from inside a constructed fixture tree.
|
|
in_tree() {
|
|
local dir="$1"
|
|
shift
|
|
(cd "$TMP/$dir" && "$TOOL" "$@")
|
|
}
|
|
|
|
assert_file() {
|
|
local file="$1" expected="$2" actual
|
|
actual="$(cat "$file")"
|
|
[ "$actual" = "$expected" ] || {
|
|
printf 'wanted:\n%s\ngot:\n%s\n' "$expected" "$actual"
|
|
return 1
|
|
}
|
|
}
|
|
|
|
BASE_CHANGELOG=$'# Changelog\n\nPreamble prose belongs to no section.\n\n## 0.1.0 — 2026-07-01\n\n- The shipped entry.'
|
|
|
|
# --- flat write: exact bytes, exact deletions --------------------------------
|
|
|
|
tree flat-one <<EOF
|
|
$BASE_CHANGELOG
|
|
EOF
|
|
frag flat-one 12.md <<'EOF'
|
|
- Twelve landed (#12).
|
|
EOF
|
|
check "flat: one fragment assembles and stamps" 0 "consumed 1 fragment" \
|
|
in_tree flat-one 0.2.0 2026-07-24
|
|
check "flat: preamble and shipped section stay byte-identical around the insert" 0 "" \
|
|
assert_file "$TMP/flat-one/CHANGELOG.md" \
|
|
$'# Changelog\n\nPreamble prose belongs to no section.\n\n## 0.2.0 — 2026-07-24\n\n- Twelve landed (#12).\n\n## 0.1.0 — 2026-07-01\n\n- The shipped entry.'
|
|
check "flat: the consumed fragment is deleted" 1 "" \
|
|
test -e "$TMP/flat-one/changelog.d/12.md"
|
|
check "flat: README.md survives consumption" 0 "" \
|
|
test -e "$TMP/flat-one/changelog.d/README.md"
|
|
|
|
# --- publication order: numeric, newest first, cross-repo beside local -------
|
|
|
|
tree flat-many <<EOF
|
|
$BASE_CHANGELOG
|
|
EOF
|
|
frag flat-many 2.md <<'EOF'
|
|
- Two (#2).
|
|
EOF
|
|
frag flat-many 9.md <<'EOF'
|
|
- Nine (#9).
|
|
EOF
|
|
frag flat-many 10.md <<'EOF'
|
|
- Ten (#10).
|
|
EOF
|
|
frag flat-many ceremony-14.md <<'EOF'
|
|
- Fourteen crossed over — naïve reflows would mangle this café's
|
|
continuation line, so it must survive verbatim (#14).
|
|
EOF
|
|
|
|
assert_check() {
|
|
local dir="$1" expected="$2" actual
|
|
actual="$(in_tree "$dir" 0.2.0 2026-07-24 --check)"
|
|
[ "$actual" = "$expected" ] || {
|
|
printf 'wanted:\n%s\ngot:\n%s\n' "$expected" "$actual"
|
|
return 1
|
|
}
|
|
}
|
|
check "flat: numeric-descending order (10.md before 9.md), cross-repo name beside local" 0 "" \
|
|
assert_check flat-many $'- Fourteen crossed over — naïve reflows would mangle this café'"'"$'s\n continuation line, so it must survive verbatim (#14).\n- Ten (#10).\n- Nine (#9).\n- Two (#2).'
|
|
|
|
# --- grouped write: canonical order, unnamed group appended ------------------
|
|
|
|
tree grouped <<'EOF'
|
|
# Changelog
|
|
|
|
Preamble prose belongs to no section.
|
|
|
|
## 0.1.0 — 2026-07-01
|
|
|
|
### Fixed
|
|
|
|
- The shipped entry.
|
|
EOF
|
|
frag grouped 21.md <<'EOF'
|
|
### Fixed
|
|
|
|
- Fixed twenty-one (#21).
|
|
EOF
|
|
frag grouped 20.md <<'EOF'
|
|
### Added
|
|
|
|
- Added twenty (#20).
|
|
- Added twenty, second bullet (#20).
|
|
|
|
### Docs
|
|
|
|
- Docs twenty (#20).
|
|
EOF
|
|
frag grouped 19.md <<'EOF'
|
|
### Security
|
|
|
|
- Security nineteen (#19).
|
|
|
|
### Added
|
|
|
|
- Added nineteen (#19).
|
|
EOF
|
|
GROUPED_BODY=$'### Added\n\n- Added twenty (#20).\n- Added twenty, second bullet (#20).\n- Added nineteen (#19).\n\n### Fixed\n\n- Fixed twenty-one (#21).\n\n### Security\n\n- Security nineteen (#19).\n\n### Docs\n\n- Docs twenty (#20).'
|
|
check "grouped: --check shows canonical order, multi-bullet group, unnamed group last" 0 "" \
|
|
assert_check grouped "$GROUPED_BODY"
|
|
check "grouped: write mode assembles the same section" 0 "consumed 3 fragment" \
|
|
in_tree grouped 0.2.0 2026-07-24
|
|
check "grouped: the written file is exact" 0 "" \
|
|
assert_file "$TMP/grouped/CHANGELOG.md" \
|
|
$'# Changelog\n\nPreamble prose belongs to no section.\n\n## 0.2.0 — 2026-07-24\n\n'"$GROUPED_BODY"$'\n\n## 0.1.0 — 2026-07-01\n\n### Fixed\n\n- The shipped entry.'
|
|
|
|
# --- the declared anchor: the first grouped release over a flat history ------
|
|
# The flip ceremony (#182): a 'grouped' sentinel admits grouped fragments
|
|
# over a flat newest published section, the sentinel is never a stray file,
|
|
# and it survives the consumption (D5) — the next -dev tree still declares
|
|
# its shape.
|
|
|
|
tree flip <<EOF
|
|
$BASE_CHANGELOG
|
|
EOF
|
|
printf 'grouped\n' >"$TMP/flip/changelog.d/shape"
|
|
frag flip 40.md <<'EOF'
|
|
### Added
|
|
|
|
- Forty landed (#40).
|
|
EOF
|
|
check "sentinel: the flip release assembles grouped over a flat published section" 0 \
|
|
"consumed 1 fragment" in_tree flip 0.2.0 2026-07-24
|
|
check "sentinel: the written flip section is exact" 0 "" \
|
|
assert_file "$TMP/flip/CHANGELOG.md" \
|
|
$'# Changelog\n\nPreamble prose belongs to no section.\n\n## 0.2.0 — 2026-07-24\n\n### Added\n\n- Forty landed (#40).\n\n## 0.1.0 — 2026-07-01\n\n- The shipped entry.'
|
|
check "sentinel: changelog.d/shape survives consumption" 0 "" \
|
|
test -e "$TMP/flip/changelog.d/shape"
|
|
|
|
tree flip-flat-frag <<EOF
|
|
$BASE_CHANGELOG
|
|
EOF
|
|
printf 'grouped\n' >"$TMP/flip-flat-frag/changelog.d/shape"
|
|
frag flip-flat-frag 41.md <<'EOF'
|
|
- Flat forty-one (#41).
|
|
EOF
|
|
check "sentinel: a flat fragment under 'grouped' refuses, sentinel named" 1 \
|
|
"changelog.d/shape' declares grouped" in_tree flip-flat-frag 0.2.0 2026-07-24
|
|
|
|
tree flip-malformed <<EOF
|
|
$BASE_CHANGELOG
|
|
EOF
|
|
printf 'Grouped\n' >"$TMP/flip-malformed/changelog.d/shape"
|
|
frag flip-malformed 42.md <<'EOF'
|
|
### Added
|
|
|
|
- Forty-two (#42).
|
|
EOF
|
|
check "sentinel: a malformed sentinel refuses, file named" 1 \
|
|
"changelog.d/shape' declares neither shape" in_tree flip-malformed 0.2.0 2026-07-24
|
|
|
|
# --- a changelog holding only its preamble -----------------------------------
|
|
|
|
tree preamble-only <<'EOF'
|
|
# Changelog
|
|
|
|
Only preamble so far.
|
|
EOF
|
|
frag preamble-only 1.md <<'EOF'
|
|
- The first entry ever (#1).
|
|
EOF
|
|
check "a changelog with no section yet gets the section after the preamble" 0 "" \
|
|
in_tree preamble-only 0.1.0 2026-07-24
|
|
check "preamble-only write is exact" 0 "" \
|
|
assert_file "$TMP/preamble-only/CHANGELOG.md" \
|
|
$'# Changelog\n\nOnly preamble so far.\n\n## 0.1.0 — 2026-07-24\n\n- The first entry ever (#1).'
|
|
|
|
# --- --check is provably read-only -------------------------------------------
|
|
|
|
tree check-readonly <<EOF
|
|
$BASE_CHANGELOG
|
|
EOF
|
|
frag check-readonly 5.md <<'EOF'
|
|
- Five (#5).
|
|
EOF
|
|
cp -R "$TMP/check-readonly" "$TMP/check-readonly.before"
|
|
check "--check prints the assembled body" 0 "Five (#5)." \
|
|
in_tree check-readonly 0.2.0 2026-07-24 --check
|
|
check "--check is read-only: the tree is byte-identical before and after" 0 "" \
|
|
diff -r "$TMP/check-readonly.before" "$TMP/check-readonly"
|
|
|
|
# --- the date defaults to today (UTC) ----------------------------------------
|
|
|
|
check "date defaults without an argument" 0 "consumed 1 fragment" \
|
|
in_tree check-readonly 0.2.0
|
|
check "the defaulted stamp is a UTC date" 0 "" \
|
|
grep -qE '^## 0\.2\.0 — [0-9]{4}-[0-9]{2}-[0-9]{2}$' "$TMP/check-readonly/CHANGELOG.md"
|
|
|
|
# --- --changelog and --dir override the defaults -----------------------------
|
|
|
|
mkdir -p "$TMP/flagged/frags"
|
|
printf '# Changelog\n\n## 0.1.0 — 2026-07-01\n\n- Shipped.\n' >"$TMP/flagged/NOTES.md"
|
|
printf -- '- Flagged entry (#2).\n' >"$TMP/flagged/frags/2.md"
|
|
check "--changelog and --dir override the defaults" 0 "" \
|
|
"$TOOL" 0.2.0 2026-07-24 --changelog "$TMP/flagged/NOTES.md" --dir "$TMP/flagged/frags"
|
|
check "the flag-driven write landed in the named changelog" 0 "" \
|
|
grep -qF -- "- Flagged entry (#2)." "$TMP/flagged/NOTES.md"
|
|
|
|
# --- refusals: each names the file responsible -------------------------------
|
|
|
|
tree empty-frags <<EOF
|
|
$BASE_CHANGELOG
|
|
EOF
|
|
rm "$TMP/empty-frags/changelog.d/README.md"
|
|
check "an empty directory refuses — a release publishes prose" 1 "zero fragments" \
|
|
in_tree empty-frags 0.2.0
|
|
check "the empty-directory refusal names the directory" 1 "changelog.d" \
|
|
in_tree empty-frags 0.2.0
|
|
|
|
tree only-readme <<EOF
|
|
$BASE_CHANGELOG
|
|
EOF
|
|
check "a README-only directory refuses with zero fragments" 1 "zero fragments" \
|
|
in_tree only-readme 0.2.0
|
|
|
|
tree no-bullet <<EOF
|
|
$BASE_CHANGELOG
|
|
EOF
|
|
frag no-bullet 3.md <<'EOF'
|
|
Prose without a bullet is a heading in spirit.
|
|
EOF
|
|
check "a fragment with no bullet refuses, file named" 1 \
|
|
"fragment 'changelog.d/3.md' has no entries" \
|
|
in_tree no-bullet 0.2.0
|
|
|
|
tree dangling <<EOF
|
|
$BASE_CHANGELOG
|
|
EOF
|
|
frag dangling 4.md <<'EOF'
|
|
### Added
|
|
|
|
### Fixed
|
|
|
|
- Fixed entry.
|
|
EOF
|
|
check "a dangling grouped heading refuses, file and heading named" 1 \
|
|
"fragment 'changelog.d/4.md' has an empty heading: '### Added'" \
|
|
in_tree dangling 0.2.0
|
|
|
|
tree smuggled <<EOF
|
|
$BASE_CHANGELOG
|
|
EOF
|
|
frag smuggled 6.md <<'EOF'
|
|
## 0.2.0 — 2026-07-24
|
|
|
|
- An entry under a smuggled heading.
|
|
EOF
|
|
check "a fragment carrying a '## ' line refuses, file named" 1 \
|
|
"fragment 'changelog.d/6.md' carries a '## ' heading" \
|
|
in_tree smuggled 0.2.0
|
|
|
|
tree stray-txt <<EOF
|
|
$BASE_CHANGELOG
|
|
EOF
|
|
frag stray-txt 7.md <<'EOF'
|
|
- Seven (#7).
|
|
EOF
|
|
frag stray-txt notes.txt <<'EOF'
|
|
A stray scratchpad.
|
|
EOF
|
|
check "a stray notes.txt refuses, file named" 1 "notes.txt" \
|
|
in_tree stray-txt 0.2.0
|
|
|
|
tree stray-markdown <<EOF
|
|
$BASE_CHANGELOG
|
|
EOF
|
|
frag stray-markdown 12.markdown <<'EOF'
|
|
- Wrong extension.
|
|
EOF
|
|
check "12.markdown refuses on the name pattern" 1 "12.markdown" \
|
|
in_tree stray-markdown 0.2.0
|
|
|
|
tree stray-case <<EOF
|
|
$BASE_CHANGELOG
|
|
EOF
|
|
frag stray-case Fix-12.md <<'EOF'
|
|
- Uppercase prefix.
|
|
EOF
|
|
check "Fix-12.md refuses on the name pattern" 1 "Fix-12.md" \
|
|
in_tree stray-case 0.2.0
|
|
|
|
tree mixed <<EOF
|
|
$BASE_CHANGELOG
|
|
EOF
|
|
frag mixed 5.md <<'EOF'
|
|
- Flat five (#5).
|
|
EOF
|
|
frag mixed 6.md <<'EOF'
|
|
### Added
|
|
|
|
- Grouped six (#6).
|
|
EOF
|
|
check "grouped + flat mixed refuses, both files named" 1 "6.md" \
|
|
in_tree mixed 0.2.0
|
|
check "the mixed refusal names the flat side too" 1 "5.md" \
|
|
in_tree mixed 0.2.0
|
|
|
|
tree grouped-over-flat <<EOF
|
|
$BASE_CHANGELOG
|
|
EOF
|
|
frag grouped-over-flat 6.md <<'EOF'
|
|
### Added
|
|
|
|
- Grouped six (#6).
|
|
EOF
|
|
check "an all-grouped set over a flat published section refuses before assembly" 1 \
|
|
"fragment 'changelog.d/6.md' is grouped but newest published section '0.1.0'" \
|
|
in_tree grouped-over-flat 0.2.0
|
|
|
|
tree already <<'EOF'
|
|
# Changelog
|
|
|
|
## 0.2.0 — 2026-07-20
|
|
|
|
- Already shipped.
|
|
EOF
|
|
frag already 4.md <<'EOF'
|
|
- A late fragment (#4).
|
|
EOF
|
|
check "an already-present section refuses — the ceremony was already run" 1 \
|
|
"already has a section for '0.2.0'" \
|
|
in_tree already 0.2.0
|
|
|
|
# Whole-version matching, as everywhere in this family: an rc section never
|
|
# blocks the bare version.
|
|
tree rc-present <<'EOF'
|
|
# Changelog
|
|
|
|
## 0.2.0-rc1 — 2026-07-15
|
|
|
|
- The candidate's entry.
|
|
EOF
|
|
frag rc-present 8.md <<'EOF'
|
|
- The real release entry (#8).
|
|
EOF
|
|
check "an rc section does not block assembling the bare version" 0 "" \
|
|
in_tree rc-present 0.2.0 2026-07-24
|
|
|
|
mkdir -p "$TMP/no-changelog/changelog.d"
|
|
printf -- '- Entry (#2).\n' >"$TMP/no-changelog/changelog.d/2.md"
|
|
check "a missing changelog refuses" 1 "no such file" \
|
|
in_tree no-changelog 0.2.0
|
|
|
|
# --- usage errors exit 2 -----------------------------------------------------
|
|
|
|
check "no arguments is a usage error" 2 "usage:" in_tree flat-one
|
|
check "an unknown flag is a usage error" 2 "usage:" in_tree flat-one 0.2.0 --frobnicate
|
|
check "a third positional is a usage error" 2 "usage:" in_tree flat-one 0.2.0 2026-07-24 extra
|
|
check "--dir without a value is a usage error" 2 "usage:" in_tree flat-one 0.2.0 --dir
|
|
|
|
# --- round trip: the publisher and the assembler agree by test ---------------
|
|
|
|
tree round-trip <<'EOF'
|
|
# Changelog
|
|
|
|
Preamble prose belongs to no section.
|
|
|
|
## 0.1.0 — 2026-07-01
|
|
|
|
### Fixed
|
|
|
|
- The shipped entry.
|
|
EOF
|
|
frag round-trip 30.md <<'EOF'
|
|
### Added
|
|
|
|
- Thirty — wraps onto a
|
|
continuation line with a naïve café (#30).
|
|
EOF
|
|
frag round-trip 29.md <<'EOF'
|
|
### Fixed
|
|
|
|
- Fixed twenty-nine (#29).
|
|
EOF
|
|
CHECKED="$(in_tree round-trip 0.2.0 2026-07-24 --check)"
|
|
check "round trip: write mode succeeds after --check" 0 "" \
|
|
in_tree round-trip 0.2.0 2026-07-24
|
|
|
|
assert_round_trip() {
|
|
local published
|
|
published="$( (cd "$TMP/round-trip" && "$SECTION" 0.2.0) )"
|
|
[ "$published" = "$CHECKED" ] || {
|
|
printf -- '--check said:\n%s\nthe publisher said:\n%s\n' "$CHECKED" "$published"
|
|
return 1
|
|
}
|
|
}
|
|
check "round trip: bin/changelog-section returns exactly the body --check printed" 0 "" \
|
|
assert_round_trip
|
|
check "round trip: the assembled section reports no problem" 0 "" \
|
|
changelog_section_problem "$TMP/round-trip/CHANGELOG.md" 0.2.0
|
|
|
|
# --- idempotence: the ceremony cannot run twice ------------------------------
|
|
|
|
check "a second write over the consumed directory refuses with zero fragments" 1 \
|
|
"zero fragments" \
|
|
in_tree round-trip 0.2.0 2026-07-24
|
|
|
|
summary
|