#!/usr/bin/env bash
set -euo pipefail
declare -r TEST_REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
declare -r TEST_PHOTOALBUM="${PHOTOALBUM:-$TEST_REPO_ROOT/bin/photoalbum}"
# shellcheck source=tests/helpers.sh
source "$TEST_REPO_ROOT/tests/helpers.sh"
test::write_preflight_config() {
local -r config_file="$1"; shift
local -r incoming_dir="$1"; shift
local -r dist_dir="$1"; shift
local -r template_dir="$1"; shift
local -r omitted_var="${1:-}"
{
if [ "$omitted_var" != TITLE ]; then
printf 'TITLE=%q\n' 'Preflight album'
fi
if [ "$omitted_var" != THUMBHEIGHT ]; then
printf 'THUMBHEIGHT=30\n'
fi
if [ "$omitted_var" != HEIGHT ]; then
printf 'HEIGHT=120\n'
fi
if [ "$omitted_var" != MAXPREVIEWS ]; then
printf 'MAXPREVIEWS=40\n'
fi
if [ "$omitted_var" != INCOMING_DIR ]; then
printf 'INCOMING_DIR=%q\n' "$incoming_dir"
fi
if [ "$omitted_var" != DIST_DIR ]; then
printf 'DIST_DIR=%q\n' "$dist_dir"
fi
if [ "$omitted_var" != TEMPLATE_DIR ]; then
printf 'TEMPLATE_DIR=%q\n' "$template_dir"
fi
if [ "$omitted_var" != SHUFFLE ]; then
printf 'SHUFFLE=no\n'
fi
if [ "$omitted_var" != TARBALL_INCLUDE ]; then
printf 'TARBALL_INCLUDE=no\n'
fi
} > "$config_file"
}
test::assert_generation_metadata() {
local -r metadata_file="$1"; shift
local -r config_source="$1"; shift
local -r incoming_dir="$1"; shift
local -r dist_dir="$1"; shift
local -r template_dir="$1"; shift
local -r tarball_included="$1"; shift
local -r title="$1"; shift
local -r maxpreviews="$1"; shift
python3 - \
"$metadata_file" \
"$config_source" \
"$incoming_dir" \
"$dist_dir" \
"$template_dir" \
"$tarball_included" \
"$title" \
"$maxpreviews" <<'PY'
import datetime
import json
import pathlib
import re
import sys
(
metadata_file,
config_source,
incoming_dir,
dist_dir,
template_dir,
tarball_included,
title,
maxpreviews,
) = sys.argv[1:]
metadata = json.loads(pathlib.Path(metadata_file).read_text())
required = {
"generator",
"generated_at",
"config_source",
"template",
"source",
"generated",
"tarball",
"settings",
}
missing = sorted(required - set(metadata))
assert not missing, f"missing metadata keys: {missing}"
timestamp = metadata["generated_at"]
assert re.fullmatch(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z", timestamp)
datetime.datetime.strptime(timestamp, "%Y-%m-%dT%H:%M:%SZ")
incoming_path = pathlib.Path(incoming_dir)
dist_path = pathlib.Path(dist_dir)
template_path = pathlib.Path(template_dir)
tarball_expected = tarball_included == "yes"
tarball_files = sorted(path.name for path in dist_path.glob("*.tar"))
assert metadata["generator"]["name"] == "photoalbum"
assert re.fullmatch(r"\d+\.\d+\.\d+", metadata["generator"]["version"])
assert metadata["config_source"] == config_source
assert metadata["template"]["directory"] == template_dir
assert metadata["template"]["name"] == template_path.name
assert metadata["source"]["incoming_dir"] == incoming_dir
supported_extensions = {".gif", ".jpeg", ".jpg", ".png", ".webp"}
assert metadata["source"]["image_count"] == sum(
1
for path in incoming_path.iterdir()
if path.is_file() and path.suffix.lower() in supported_extensions
)
assert metadata["generated"]["photo_count"] == sum(
1 for path in (dist_path / "photos").iterdir() if path.is_file()
)
assert metadata["generated"]["thumb_count"] == sum(
1 for path in (dist_path / "thumbs").iterdir() if path.is_file()
)
assert metadata["generated"]["html_count"] == sum(
1 for path in dist_path.rglob("*.html") if path.is_file()
)
assert metadata["tarball"]["included"] is tarball_expected
assert metadata["tarball"]["file"] == (tarball_files[0] if tarball_files else "")
assert metadata["settings"]["title"] == title
assert metadata["settings"]["height"] == "120"
assert metadata["settings"]["thumbheight"] == "30"
assert metadata["settings"]["maxpreviews"] == maxpreviews
assert metadata["settings"]["shuffle"] is False
assert "original_basepath" in metadata["settings"]
PY
}
test_version() {
local output
output=$(test::run_photoalbum --version)
test::assert_contains 'This is Photoalbum Version' "$output"
}
test_init() {
local config
test::setup
(
cd "$TEST_TMPDIR"
PHOTOALBUM_DEFAULT_RC="$TEST_TMPDIR/missing" \
"$TEST_PHOTOALBUM" --init >/dev/null
test::assert_file_exists photoalbum.conf
test::assert_path_absent Makefile
)
config=$(<"$TEST_TMPDIR/photoalbum.conf")
test::assert_contains \
"TEMPLATE_DIR=$TEST_REPO_ROOT/share/templates/default" \
"$config"
test::teardown
}
test_init_existing_config_fails_without_overwrite() {
local output
test::setup
printf 'sentinel\n' > "$TEST_TMPDIR/photoalbum.conf"
output=$(
cd "$TEST_TMPDIR"
test::capture_failure_output "$TEST_PHOTOALBUM" --init
)
test::assert_contains 'Error: photoalbum.conf already exists' "$output"
test "$(cat "$TEST_TMPDIR/photoalbum.conf")" = 'sentinel'
test::teardown
}
test_clean() {
local staging_dir
test::setup
printf 'DIST_DIR=%q/dist\n' "$TEST_TMPDIR" \
> "$TEST_TMPDIR/photoalbum.conf"
mkdir -p "$TEST_TMPDIR/dist"
staging_dir="$TEST_TMPDIR/.photoalbum.dist.staging.manual"
mkdir -p "$staging_dir"
(
cd "$TEST_TMPDIR"
"$TEST_PHOTOALBUM" --clean
test::assert_path_absent "$TEST_TMPDIR/dist"
test::assert_dir_exists "$staging_dir"
)
test::teardown
}
test_clean_with_config() {
local config_file
test::setup
config_file="$TEST_TMPDIR/custom.conf"
printf 'DIST_DIR=%q/custom-dist\n' "$TEST_TMPDIR" > "$config_file"
mkdir -p "$TEST_TMPDIR/custom-dist"
(
cd "$TEST_TMPDIR"
"$TEST_PHOTOALBUM" --clean --config "$config_file"
test::assert_path_absent "$TEST_TMPDIR/custom-dist"
)
test::teardown
}
test_clean_cli_dist_overrides_config() {
local config_file
test::setup
config_file="$TEST_TMPDIR/photoalbum.conf"
printf 'DIST_DIR=%q/config-dist\n' "$TEST_TMPDIR" > "$config_file"
mkdir -p "$TEST_TMPDIR/config-dist" "$TEST_TMPDIR/cli-dist"
(
cd "$TEST_TMPDIR"
"$TEST_PHOTOALBUM" --clean --dist "$TEST_TMPDIR/cli-dist"
test::assert_dir_exists "$TEST_TMPDIR/config-dist"
test::assert_path_absent "$TEST_TMPDIR/cli-dist"
)
test::teardown
}
test_missing_config_fails_without_legacy_fallbacks() {
local default_rc
local home_dir
local generate_output
local clean_output
test::setup
home_dir="$TEST_TMPDIR/home"
default_rc="$TEST_TMPDIR/default-photoalbum"
mkdir -p "$home_dir"
printf 'DIST_DIR=%q/legacy-dist\n' "$TEST_TMPDIR" \
> "$TEST_TMPDIR/photoalbumrc"
printf 'DIST_DIR=%q/home-dist\n' "$TEST_TMPDIR" > "$home_dir/.photoalbumrc"
printf 'DIST_DIR=%q/default-dist\n' "$TEST_TMPDIR" > "$default_rc"
generate_output=$(
cd "$TEST_TMPDIR"
HOME="$home_dir" PHOTOALBUM_DEFAULT_RC="$default_rc" \
test::capture_failure_output "$TEST_PHOTOALBUM" --generate
)
clean_output=$(
cd "$TEST_TMPDIR"
HOME="$home_dir" PHOTOALBUM_DEFAULT_RC="$default_rc" \
test::capture_failure_output "$TEST_PHOTOALBUM" --clean
)
test::assert_contains 'Error: Can not find config file ./photoalbum.conf' \
"$generate_output"
test::assert_contains 'Run photoalbum --init to create ./photoalbum.conf.' \
"$generate_output"
test::assert_contains 'Error: Can not find config file ./photoalbum.conf' \
"$clean_output"
test::assert_contains 'Run photoalbum --init to create ./photoalbum.conf.' \
"$clean_output"
test::assert_path_absent "$TEST_TMPDIR/legacy-dist"
test::assert_path_absent "$TEST_TMPDIR/home-dist"
test::assert_path_absent "$TEST_TMPDIR/default-dist"
test::teardown
}
test_generate_with_config_missing_incoming_fails() {
local config_file
local output
test::setup
config_file="$TEST_TMPDIR/custom.conf"
test::write_album_config \
"$config_file" "$TEST_TMPDIR/missing" "$TEST_TMPDIR/custom-dist" \
'Missing incoming config' 40
output=$(
cd "$TEST_TMPDIR"
test::capture_failure_output \
"$TEST_PHOTOALBUM" --generate --config "$config_file"
)
test::assert_contains \
"ERROR: You have to create $TEST_TMPDIR/missing first" \
"$output"
test::assert_path_absent "$TEST_TMPDIR/custom-dist"
test::teardown
}
test_generate_with_config_succeeds_without_default_config() {
local config_file
local fake_bin
test::setup
fake_bin="$TEST_TMPDIR/bin"
config_file="$TEST_TMPDIR/custom.conf"
test::install_fake_imagemagick "$fake_bin"
test::generate_fixture_images "$TEST_TMPDIR/custom-incoming"
test::write_album_config \
"$config_file" "$TEST_TMPDIR/custom-incoming" \
"$TEST_TMPDIR/custom-dist" 'Custom config album' 3
(
cd "$TEST_TMPDIR"
test::assert_path_absent photoalbum.conf
PATH="$fake_bin:$PATH" "$TEST_PHOTOALBUM" \
--generate --config "$config_file"
)
test::assert_file_exists "$TEST_TMPDIR/custom-dist/photos/01-landscape.jpg"
test::assert_file_exists "$TEST_TMPDIR/custom-dist/html/page-2.html"
test::teardown
}
test_generate_cli_overrides_config_values() {
local config_file
local fake_bin
local page_html
local view_html
test::setup
fake_bin="$TEST_TMPDIR/bin"
config_file="$TEST_TMPDIR/photoalbum.conf"
test::install_fake_imagemagick "$fake_bin"
PATH="$fake_bin:$PATH" \
test::generate_fixture_images "$TEST_TMPDIR/cli-incoming"
{
printf 'TITLE=%q\n' 'Config title'
printf 'THUMBHEIGHT=10\n'
printf 'HEIGHT=20\n'
printf 'MAXPREVIEWS=40\n'
printf 'SHUFFLE=yes\n'
printf 'INCOMING_DIR=%q/config-incoming\n' "$TEST_TMPDIR"
printf 'DIST_DIR=%q/config-dist\n' "$TEST_TMPDIR"
printf 'TEMPLATE_DIR=%q/config-template\n' "$TEST_TMPDIR"
printf 'TARBALL_INCLUDE=yes\n'
} > "$config_file"
(
cd "$TEST_TMPDIR"
PATH="$fake_bin:$PATH" "$TEST_PHOTOALBUM" \
--generate \
--incoming "$TEST_TMPDIR/cli-incoming" \
--dist "$TEST_TMPDIR/cli-dist" \
--template "$TEST_REPO_ROOT/share/templates/default" \
--title 'CLI title' \
--height 456 \
--thumbheight 45 \
--maxpreviews 1 \
--no-shuffle \
--no-tarball
)
page_html=$(<"$TEST_TMPDIR/cli-dist/html/page-1.html")
view_html=$(<"$TEST_TMPDIR/cli-dist/html/1-1.html")
test::assert_file_exists "$TEST_TMPDIR/cli-dist/photos/01-landscape.jpg"
test::assert_file_exists "$TEST_TMPDIR/cli-dist/photos/02-portrait.jpg"
test::assert_file_exists "$TEST_TMPDIR/cli-dist/photos/03-square.jpg"
test::assert_file_exists \
"$TEST_TMPDIR/cli-dist/photos/04 filename with spaces.jpg"
test::assert_file_exists "$TEST_TMPDIR/cli-dist/photos/05-extra.jpg"
test::assert_file_exists "$TEST_TMPDIR/cli-dist/photos/06-extra.jpg"
test::assert_path_absent "$TEST_TMPDIR/config-dist"
test::assert_find_count 0 "$TEST_TMPDIR/cli-dist" '*.tar'
test::assert_contains '
CLI title' "$page_html"
test::assert_contains 'height: 45px;' "$page_html"
test::assert_contains 'max-height: 456px;' "$view_html"
test::assert_contains 'Next 1 pictures' "$page_html"
test::assert_not_contains 'Config title' "$page_html"
test::teardown
}
test_generate_shuffle_override_uses_random_order() {
local config_file
local fake_bin
local sort_log_output
local sort_log
test::setup
fake_bin="$TEST_TMPDIR/bin"
config_file="$TEST_TMPDIR/photoalbum.conf"
sort_log="$TEST_TMPDIR/sort.log"
test::install_fake_imagemagick "$fake_bin"
test::install_sort_spy "$fake_bin"
PATH="$fake_bin:$PATH" \
test::generate_fixture_images "$TEST_TMPDIR/incoming"
test::write_album_config \
"$config_file" "$TEST_TMPDIR/incoming" "$TEST_TMPDIR/dist" \
'Shuffle override' 40
(
cd "$TEST_TMPDIR"
PATH="$fake_bin:$PATH" TEST_SORT_LOG="$sort_log" \
"$TEST_PHOTOALBUM" --generate --shuffle
)
sort_log_output=$(<"$sort_log")
test::assert_contains 'photo-shuffle -R' "$sort_log_output"
test::teardown
}
test_generate_no_shuffle_override_uses_sorted_order() {
local config_file
local fake_bin
local page_html
local sort_log
test::setup
fake_bin="$TEST_TMPDIR/bin"
config_file="$TEST_TMPDIR/photoalbum.conf"
sort_log="$TEST_TMPDIR/sort.log"
test::install_fake_imagemagick "$fake_bin"
test::install_sort_spy "$fake_bin"
PATH="$fake_bin:$PATH" \
test::generate_fixture_images "$TEST_TMPDIR/incoming"
test::write_album_config \
"$config_file" "$TEST_TMPDIR/incoming" "$TEST_TMPDIR/dist" \
'No shuffle override' 40
printf 'SHUFFLE=yes\n' >> "$config_file"
(
cd "$TEST_TMPDIR"
PATH="$fake_bin:$PATH" TEST_SORT_LOG="$sort_log" \
"$TEST_PHOTOALBUM" --generate --no-shuffle
)
page_html=$(<"$TEST_TMPDIR/dist/html/page-1.html")
test::assert_contains_before \
"name='01-landscape.jpg'" \
"name='06-extra.jpg'" \
"$page_html"
test::assert_path_absent "$sort_log"
test::teardown
}
test_generate_cli_tarball_overrides_config() {
local config_file
local fake_bin
local tarball
local tarball_listing
test::setup
fake_bin="$TEST_TMPDIR/bin"
config_file="$TEST_TMPDIR/photoalbum.conf"
test::install_fake_imagemagick "$fake_bin"
PATH="$fake_bin:$PATH" \
test::generate_fixture_images "$TEST_TMPDIR/incoming"
test::write_album_config \
"$config_file" "$TEST_TMPDIR/incoming" "$TEST_TMPDIR/dist" \
'Tarball override' 40
(
cd "$TEST_TMPDIR"
PATH="$fake_bin:$PATH" "$TEST_PHOTOALBUM" --generate --tarball
)
tarball=$(find "$TEST_TMPDIR/dist" -maxdepth 1 -name '*.tar' -print)
test::assert_contains "$TEST_TMPDIR/dist/incoming-" "$tarball"
tarball_listing=$(tar -tf "$tarball")
test::assert_contains 'incoming/01-landscape.jpg' "$tarball_listing"
test::teardown
}
test_generate_cli_no_tarball_overrides_config() {
local config_file
local fake_bin
test::setup
fake_bin="$TEST_TMPDIR/bin"
config_file="$TEST_TMPDIR/photoalbum.conf"
test::install_fake_imagemagick "$fake_bin"
PATH="$fake_bin:$PATH" \
test::generate_fixture_images "$TEST_TMPDIR/incoming"
test::write_album_config \
"$config_file" "$TEST_TMPDIR/incoming" "$TEST_TMPDIR/dist" \
'No tarball override' 40
printf 'TARBALL_INCLUDE=yes\n' >> "$config_file"
(
cd "$TEST_TMPDIR"
PATH="$fake_bin:$PATH" "$TEST_PHOTOALBUM" --generate --no-tarball
)
test::assert_find_count 0 "$TEST_TMPDIR/dist" '*.tar'
test::teardown
}
test_generate_ignores_unsupported_incoming_files_with_warning() {
local config_file
local fake_bin
local output
test::setup
fake_bin="$TEST_TMPDIR/bin"
config_file="$TEST_TMPDIR/photoalbum.conf"
test::install_fake_imagemagick "$fake_bin"
mkdir -p "$TEST_TMPDIR/incoming"
"$TEST_IMAGEMAGICK" -size 160x90 xc:red \
"$TEST_TMPDIR/incoming/01-upper.JPG"
"$TEST_IMAGEMAGICK" -size 160x90 xc:red \
"$TEST_TMPDIR/incoming/02-photo.jpeg"
"$TEST_IMAGEMAGICK" -size 160x90 xc:red \
"$TEST_TMPDIR/incoming/03-photo.png"
"$TEST_IMAGEMAGICK" -size 160x90 xc:red \
"$TEST_TMPDIR/incoming/04-photo.webp"
"$TEST_IMAGEMAGICK" -size 160x90 xc:red \
"$TEST_TMPDIR/incoming/05-photo.gif"
printf 'notes\n' > "$TEST_TMPDIR/incoming/notes.txt"
printf '# album notes\n' > "$TEST_TMPDIR/incoming/README.md"
mkdir -p "$TEST_TMPDIR/dist/photos"
printf 'stale cached unsupported file\n' \
> "$TEST_TMPDIR/dist/photos/notes.txt"
printf 'stale cached unsupported file\n' \
> "$TEST_TMPDIR/dist/photos/README.md"
test::write_album_config \
"$config_file" "$TEST_TMPDIR/incoming" "$TEST_TMPDIR/dist" \
'Extension filter album' 40
output=$(
cd "$TEST_TMPDIR"
PATH="$fake_bin:$PATH" "$TEST_PHOTOALBUM" --generate 2>&1
)
test::assert_file_exists "$TEST_TMPDIR/dist/photos/01-upper.JPG"
test::assert_file_exists "$TEST_TMPDIR/dist/photos/02-photo.jpeg"
test::assert_file_exists "$TEST_TMPDIR/dist/photos/03-photo.png"
test::assert_file_exists "$TEST_TMPDIR/dist/photos/04-photo.webp"
test::assert_file_exists "$TEST_TMPDIR/dist/photos/05-photo.gif"
test::assert_path_absent "$TEST_TMPDIR/dist/photos/notes.txt"
test::assert_path_absent "$TEST_TMPDIR/dist/photos/README.md"
test::assert_contains \
'WARNING: Ignoring unsupported incoming file: README.md' \
"$output"
test::assert_contains \
'WARNING: Ignoring unsupported incoming file: notes.txt' \
"$output"
test::assert_not_contains 'Processing notes.txt' "$output"
test::assert_not_contains 'Processing README.md' "$output"
python3 - "$TEST_TMPDIR/dist/photoalbum.json" <<'PY'
import json
import pathlib
import sys
metadata = json.loads(pathlib.Path(sys.argv[1]).read_text())
assert metadata["source"]["image_count"] == 5
assert metadata["generated"]["photo_count"] == 5
PY
test::teardown
}
test_generate_missing_incoming_fails() {
local output
test::setup
test::write_album_config \
"$TEST_TMPDIR/photoalbum.conf" "$TEST_TMPDIR/missing" \
"$TEST_TMPDIR/dist" 'Missing incoming' 40
output=$(
cd "$TEST_TMPDIR"
test::capture_failure_output "$TEST_PHOTOALBUM" --generate
)
test::assert_contains \
"ERROR: You have to create $TEST_TMPDIR/missing first" \
"$output"
test::assert_path_absent "$TEST_TMPDIR/dist"
test::teardown
}
test_generate_preflight_rejects_missing_required_vars() {
local case_dir
local config_file
local dist_dir
local incoming_dir
local output
local required_var
local -a required_vars=(
TITLE
THUMBHEIGHT
MAXPREVIEWS
INCOMING_DIR
DIST_DIR
TEMPLATE_DIR
)
test::setup
for required_var in "${required_vars[@]}"; do
case_dir="$TEST_TMPDIR/missing-$required_var"
incoming_dir="$case_dir/incoming"
dist_dir="$case_dir/dist"
config_file="$case_dir/photoalbum.conf"
mkdir -p "$incoming_dir"
test::write_preflight_config \
"$config_file" "$incoming_dir" "$dist_dir" \
"$TEST_REPO_ROOT/share/templates/default" "$required_var"
output=$(
cd "$case_dir"
test::capture_failure_output \
"$TEST_PHOTOALBUM" --generate --config "$config_file"
)
test::assert_contains \
"ERROR: $required_var must be set in photoalbum configuration" \
"$output"
test::assert_path_absent "$dist_dir"
done
test::teardown
}
test_generate_preflight_rejects_invalid_numbers() {
local case_dir
local config_file
local dist_dir
local incoming_dir
local numeric_var
local output
local -a numeric_vars=(
HEIGHT
THUMBHEIGHT
MAXPREVIEWS
)
test::setup
for numeric_var in "${numeric_vars[@]}"; do
case_dir="$TEST_TMPDIR/invalid-$numeric_var"
incoming_dir="$case_dir/incoming"
dist_dir="$case_dir/dist"
config_file="$case_dir/photoalbum.conf"
mkdir -p "$incoming_dir"
test::write_preflight_config \
"$config_file" "$incoming_dir" "$dist_dir" \
"$TEST_REPO_ROOT/share/templates/default"
printf '%s=not-a-number\n' "$numeric_var" >> "$config_file"
output=$(
cd "$case_dir"
test::capture_failure_output \
"$TEST_PHOTOALBUM" --generate --config "$config_file"
)
test::assert_contains \
"ERROR: $numeric_var must be a positive integer" \
"$output"
test::assert_path_absent "$dist_dir"
done
test::teardown
}
test_generate_preflight_accepts_empty_height() {
local config_file
local fake_bin
test::setup
fake_bin="$TEST_TMPDIR/bin"
config_file="$TEST_TMPDIR/photoalbum.conf"
test::install_fake_imagemagick "$fake_bin"
PATH="$fake_bin:$PATH" \
test::generate_fixture_images "$TEST_TMPDIR/incoming"
test::write_preflight_config \
"$config_file" "$TEST_TMPDIR/incoming" "$TEST_TMPDIR/dist" \
"$TEST_REPO_ROOT/share/templates/default" HEIGHT
(
cd "$TEST_TMPDIR"
PATH="$fake_bin:$PATH" "$TEST_PHOTOALBUM" --generate
)
test::assert_file_exists "$TEST_TMPDIR/dist/photos/01-landscape.jpg"
test::teardown
}
test_generate_preflight_rejects_invalid_yes_no_values() {
local bool_var
local case_dir
local config_file
local dist_dir
local incoming_dir
local output
local -a bool_vars=(
SHUFFLE
TARBALL_INCLUDE
)
test::setup
for bool_var in "${bool_vars[@]}"; do
case_dir="$TEST_TMPDIR/invalid-$bool_var"
incoming_dir="$case_dir/incoming"
dist_dir="$case_dir/dist"
config_file="$case_dir/photoalbum.conf"
mkdir -p "$incoming_dir"
test::write_preflight_config \
"$config_file" "$incoming_dir" "$dist_dir" \
"$TEST_REPO_ROOT/share/templates/default"
printf '%s=maybe\n' "$bool_var" >> "$config_file"
output=$(
cd "$case_dir"
test::capture_failure_output \
"$TEST_PHOTOALBUM" --generate --config "$config_file"
)
test::assert_contains "ERROR: $bool_var must be yes or no" "$output"
test::assert_path_absent "$dist_dir"
done
test::teardown
}
test_generate_preflight_rejects_unwritable_dist_parent() {
local config_file
local dist_dir
local dist_parent
local incoming_dir
local output
test::setup
config_file="$TEST_TMPDIR/photoalbum.conf"
dist_parent="$TEST_TMPDIR/unwritable"
dist_dir="$dist_parent/dist"
incoming_dir="$TEST_TMPDIR/incoming"
mkdir -p "$incoming_dir" "$dist_parent"
chmod 0555 "$dist_parent"
if [ -w "$dist_parent" ]; then
chmod 0755 "$dist_parent"
test::teardown
return
fi
test::write_preflight_config \
"$config_file" "$incoming_dir" "$dist_dir" \
"$TEST_REPO_ROOT/share/templates/default"
output=$(
cd "$TEST_TMPDIR"
test::capture_failure_output \
"$TEST_PHOTOALBUM" --generate --config "$config_file"
)
chmod 0755 "$dist_parent"
test::assert_contains \
"ERROR: DIST_DIR parent $dist_parent must be writable" \
"$output"
test::assert_path_absent "$dist_dir"
test::teardown
}
test_generate_preflight_accepts_nested_new_dist_dir() {
local config_file
local dist_dir
local fake_bin
local incoming_dir
test::setup
fake_bin="$TEST_TMPDIR/bin"
config_file="$TEST_TMPDIR/photoalbum.conf"
incoming_dir="$TEST_TMPDIR/incoming"
dist_dir="$TEST_TMPDIR/site/albums/out"
mkdir -p "$TEST_TMPDIR/site"
test::install_fake_imagemagick "$fake_bin"
PATH="$fake_bin:$PATH" test::generate_fixture_images "$incoming_dir"
test::write_preflight_config \
"$config_file" "$incoming_dir" "$dist_dir" \
"$TEST_REPO_ROOT/share/templates/default"
(
cd "$TEST_TMPDIR"
PATH="$fake_bin:$PATH" "$TEST_PHOTOALBUM" --generate
)
test::assert_file_exists "$dist_dir/photos/01-landscape.jpg"
test::teardown
}
test_generate_preflight_rejects_missing_templates() {
local config_file
local dist_dir
local incoming_dir
local output
local template_dir
test::setup
config_file="$TEST_TMPDIR/photoalbum.conf"
dist_dir="$TEST_TMPDIR/dist"
incoming_dir="$TEST_TMPDIR/incoming"
template_dir="$TEST_TMPDIR/templates"
mkdir -p "$incoming_dir" "$template_dir"
test::write_preflight_config \
"$config_file" "$incoming_dir" "$dist_dir" "$template_dir"
output=$(
cd "$TEST_TMPDIR"
test::capture_failure_output \
"$TEST_PHOTOALBUM" --generate --config "$config_file"
)
test::assert_contains \
"ERROR: template file $template_dir/footer.tmpl must be readable" \
"$output"
test::assert_path_absent "$dist_dir"
test::teardown
}
test_integration_generates_album_outputs_and_cleans() {
local config_file
local fake_bin
local page_html
local tarball
local tarball_listing
local top_index_html
test::setup
fake_bin="$TEST_TMPDIR/bin"
config_file="$TEST_TMPDIR/photoalbum.conf"
test::install_fake_imagemagick "$fake_bin"
PATH="$fake_bin:$PATH" \
test::generate_fixture_images "$TEST_TMPDIR/incoming"
test::write_album_config \
"$config_file" "$TEST_TMPDIR/incoming" "$TEST_TMPDIR/dist" \
'Integration album' 2
(
cd "$TEST_TMPDIR"
PATH="$fake_bin:$PATH" "$TEST_PHOTOALBUM" --generate
)
test::assert_file_exists "$TEST_TMPDIR/dist/photos/01-landscape.jpg"
test::assert_file_exists "$TEST_TMPDIR/dist/photos/02-portrait.jpg"
test::assert_file_exists "$TEST_TMPDIR/dist/photos/03-square.jpg"
test::assert_file_exists \
"$TEST_TMPDIR/dist/photos/04 filename with spaces.jpg"
test::assert_file_exists "$TEST_TMPDIR/dist/thumbs/01-landscape.jpg"
test::assert_file_exists "$TEST_TMPDIR/dist/blurs/01-landscape.jpg"
test::assert_file_exists "$TEST_TMPDIR/dist/html/page-1.html"
test::assert_file_exists "$TEST_TMPDIR/dist/html/page-2.html"
test::assert_file_exists "$TEST_TMPDIR/dist/html/page-3.html"
test::assert_file_exists "$TEST_TMPDIR/dist/html/1-1.html"
test::assert_file_exists "$TEST_TMPDIR/dist/html/3-2.html"
test::assert_file_exists "$TEST_TMPDIR/dist/html/index.html"
test::assert_file_exists "$TEST_TMPDIR/dist/index.html"
test::assert_file_exists "$TEST_TMPDIR/dist/photoalbum.json"
page_html=$(<"$TEST_TMPDIR/dist/html/page-1.html")
top_index_html=$(<"$TEST_TMPDIR/dist/index.html")
test::assert_contains "name='04 filename with spaces.jpg'" \
"$(<"$TEST_TMPDIR/dist/html/page-2.html")"
test::assert_contains 'Next 2 pictures' "$page_html"
test::assert_contains 'url=./html/index.html' "$top_index_html"
test::assert_find_count 0 "$TEST_TMPDIR/dist" '*.tar'
test::assert_generation_metadata \
"$TEST_TMPDIR/dist/photoalbum.json" \
'./photoalbum.conf' \
"$TEST_TMPDIR/incoming" \
"$TEST_TMPDIR/dist" \
"$TEST_REPO_ROOT/share/templates/default" \
no \
'Integration album' \
2
(
cd "$TEST_TMPDIR"
PATH="$fake_bin:$PATH" "$TEST_PHOTOALBUM" --generate --tarball
)
test::assert_file_exists "$TEST_TMPDIR/dist/photoalbum.json"
tarball=$(find "$TEST_TMPDIR/dist" -maxdepth 1 -name '*.tar' -print)
test::assert_contains "$TEST_TMPDIR/dist/incoming-" "$tarball"
tarball_listing=$(tar -tf "$tarball")
test::assert_contains \
'incoming/04 filename with spaces.jpg' \
"$tarball_listing"
test::assert_generation_metadata \
"$TEST_TMPDIR/dist/photoalbum.json" \
'./photoalbum.conf' \
"$TEST_TMPDIR/incoming" \
"$TEST_TMPDIR/dist" \
"$TEST_REPO_ROOT/share/templates/default" \
yes \
'Integration album' \
2
(
cd "$TEST_TMPDIR"
"$TEST_PHOTOALBUM" --clean
)
test::assert_path_absent "$TEST_TMPDIR/dist"
test::teardown
}
test_generate_replaces_dist_after_success() {
local config_file
local fake_bin
test::setup
fake_bin="$TEST_TMPDIR/bin"
config_file="$TEST_TMPDIR/photoalbum.conf"
test::install_fake_imagemagick "$fake_bin"
PATH="$fake_bin:$PATH" \
test::generate_fixture_images "$TEST_TMPDIR/incoming"
test::write_album_config \
"$config_file" "$TEST_TMPDIR/incoming" "$TEST_TMPDIR/dist" \
'Replace album' 40
mkdir -p "$TEST_TMPDIR/dist/html" "$TEST_TMPDIR/dist/photos"
printf 'stale\n' > "$TEST_TMPDIR/dist/stale-root-file"
printf 'stale\n' > "$TEST_TMPDIR/dist/html/stale.html"
printf 'stale\n' > "$TEST_TMPDIR/dist/photos/stale.jpg"
(
cd "$TEST_TMPDIR"
PATH="$fake_bin:$PATH" "$TEST_PHOTOALBUM" --generate
)
test::assert_file_exists "$TEST_TMPDIR/dist/photos/01-landscape.jpg"
test::assert_file_exists "$TEST_TMPDIR/dist/html/page-1.html"
test::assert_path_absent "$TEST_TMPDIR/dist/stale-root-file"
test::assert_path_absent "$TEST_TMPDIR/dist/html/stale.html"
test::assert_path_absent "$TEST_TMPDIR/dist/photos/stale.jpg"
test::assert_no_staging_dirs "$TEST_TMPDIR"
test::teardown
}
test_generate_imagemagick_failure_preserves_dist() {
local config_file
local fake_bin
local output
test::setup
fake_bin="$TEST_TMPDIR/bin"
config_file="$TEST_TMPDIR/photoalbum.conf"
test::install_failing_imagemagick "$fake_bin"
mkdir -p "$TEST_TMPDIR/incoming" "$TEST_TMPDIR/dist"
printf 'fake image\n' > "$TEST_TMPDIR/incoming/01.jpg"
printf 'old dist\n' > "$TEST_TMPDIR/dist/index.html"
printf 'keep me\n' > "$TEST_TMPDIR/dist/sentinel"
test::write_album_config \
"$config_file" "$TEST_TMPDIR/incoming" "$TEST_TMPDIR/dist" \
'Failing ImageMagick album' 40
output=$(
cd "$TEST_TMPDIR"
PATH="$fake_bin:$PATH" \
test::capture_failure_output "$TEST_PHOTOALBUM" --generate
)
test::assert_contains 'simulated ImageMagick failure' "$output"
test "$(cat "$TEST_TMPDIR/dist/index.html")" = 'old dist'
test "$(cat "$TEST_TMPDIR/dist/sentinel")" = 'keep me'
test::assert_path_absent "$TEST_TMPDIR/dist/photos/01.jpg"
test::assert_path_absent "$TEST_TMPDIR/dist/photoalbum.json"
test::assert_no_staging_dirs "$TEST_TMPDIR"
test::teardown
}
test_generate_template_failure_preserves_dist() {
local config_file
local fake_bin
local output
local template_dir
test::setup
fake_bin="$TEST_TMPDIR/bin"
config_file="$TEST_TMPDIR/photoalbum.conf"
template_dir="$TEST_TMPDIR/templates"
test::install_fake_imagemagick "$fake_bin"
PATH="$fake_bin:$PATH" \
test::generate_fixture_images "$TEST_TMPDIR/incoming"
cp -R "$TEST_REPO_ROOT/share/templates/default" "$template_dir"
printf 'return 42\n' > "$template_dir/preview.tmpl"
mkdir -p "$TEST_TMPDIR/dist"
printf 'old index\n' > "$TEST_TMPDIR/dist/index.html"
test::write_album_config \
"$config_file" "$TEST_TMPDIR/incoming" "$TEST_TMPDIR/dist" \
'Failing template album' 40
printf 'TEMPLATE_DIR=%q\n' "$template_dir" >> "$config_file"
output=$(
cd "$TEST_TMPDIR"
PATH="$fake_bin:$PATH" \
test::capture_failure_output "$TEST_PHOTOALBUM" --generate
)
test::assert_contains 'Generating' "$output"
test "$(cat "$TEST_TMPDIR/dist/index.html")" = 'old index'
test::assert_path_absent "$TEST_TMPDIR/dist/photos/01-landscape.jpg"
test::assert_path_absent "$TEST_TMPDIR/dist/photoalbum.json"
test::assert_no_staging_dirs "$TEST_TMPDIR"
test::teardown
}
test_generate_swap_failure_restores_dist() {
local config_file
local fake_bin
local mv_count_file
local output
test::setup
fake_bin="$TEST_TMPDIR/bin"
config_file="$TEST_TMPDIR/photoalbum.conf"
mv_count_file="$TEST_TMPDIR/mv-count"
test::install_fake_imagemagick "$fake_bin"
test::install_mv_spy "$fake_bin"
PATH="$fake_bin:$PATH" \
test::generate_fixture_images "$TEST_TMPDIR/incoming"
mkdir -p "$TEST_TMPDIR/dist"
printf 'old index\n' > "$TEST_TMPDIR/dist/index.html"
printf 'old sentinel\n' > "$TEST_TMPDIR/dist/sentinel"
test::write_album_config \
"$config_file" "$TEST_TMPDIR/incoming" "$TEST_TMPDIR/dist" \
'Swap failure album' 40
output=$(
cd "$TEST_TMPDIR"
PATH="$fake_bin:$PATH" \
TEST_MV_COUNT_FILE="$mv_count_file" \
TEST_FAIL_MV_ON=2 \
test::capture_failure_output "$TEST_PHOTOALBUM" --generate
)
test::assert_contains 'simulated mv failure' "$output"
test "$(cat "$TEST_TMPDIR/dist/index.html")" = 'old index'
test "$(cat "$TEST_TMPDIR/dist/sentinel")" = 'old sentinel'
test::assert_path_absent "$TEST_TMPDIR/dist/photos/01-landscape.jpg"
test::assert_path_absent "$TEST_TMPDIR/dist/photoalbum.json"
test::assert_no_staging_dirs "$TEST_TMPDIR"
test::teardown
}
test_generate_missing_imagemagick_fails() {
local config_file
local output
local path_bin
test::setup
config_file="$TEST_TMPDIR/photoalbum.conf"
path_bin="$TEST_TMPDIR/path-bin"
test::install_coreutils_without_imagemagick "$path_bin"
mkdir -p "$TEST_TMPDIR/incoming"
printf 'fake image\n' > "$TEST_TMPDIR/incoming/01.jpg"
test::write_album_config \
"$config_file" "$TEST_TMPDIR/incoming" "$TEST_TMPDIR/dist" \
'Missing ImageMagick' 40
output=$(
cd "$TEST_TMPDIR"
PATH="$path_bin" test::capture_failure_output \
"$TEST_PHOTOALBUM" --generate
)
test::assert_contains \
'ERROR: ImageMagick is required; install magick or convert' \
"$output"
test::assert_path_absent "$TEST_TMPDIR/dist"
test::teardown
}
test_generate_escapes_html_values() {
local config_file
local css_photo
local fake_bin
local original_basepath
local original_basepath_html
local page_html
local photo_html
local photo_name
local title
local title_html
local view_html
test::setup
fake_bin="$TEST_TMPDIR/bin"
config_file="$TEST_TMPDIR/photoalbum.conf"
photo_name="kid's_\"&.jpg"
photo_html='kid's_"<tag>&.jpg'
css_photo='kid\000027s_\000022\00003ctag\00003e\000026.jpg'
title="A & \"quoted\" 'ok'"
title_html='A & "quoted" <title> 'ok''
original_basepath="https://example.test/original?album=\"&owner=O'Neil"
original_basepath_html='https://example.test/original?album="<x>&owner=O'Neil'
test::install_fake_imagemagick "$fake_bin"
mkdir -p "$TEST_TMPDIR/incoming"
printf 'fake image\n' > "$TEST_TMPDIR/incoming/$photo_name"
{
printf 'TITLE=%q\n' "$title"
printf 'THUMBHEIGHT=30\n'
printf 'HEIGHT=120\n'
printf 'MAXPREVIEWS=40\n'
printf 'INCOMING_DIR=%q/incoming\n' "$TEST_TMPDIR"
printf 'DIST_DIR=%q/dist\n' "$TEST_TMPDIR"
printf 'TEMPLATE_DIR=%q/share/templates/default\n' "$TEST_REPO_ROOT"
printf 'ORIGINAL_BASEPATH=%q\n' "$original_basepath"
printf 'TARBALL_INCLUDE=yes\n'
printf 'TARBALL_SUFFIX=%q\n' '&"'\''.tar'
} > "$config_file"
(
cd "$TEST_TMPDIR"
PATH="$fake_bin:$PATH" "$TEST_PHOTOALBUM" --generate
)
page_html=$(<"$TEST_TMPDIR/dist/html/page-1.html")
view_html=$(<"$TEST_TMPDIR/dist/html/1-1.html")
test::assert_contains "$title_html" "$page_html"
test::assert_contains \
"background-image: url(\"../blurs/$css_photo\");" \
"$page_html"
test::assert_contains "name='$photo_html'" "$page_html"
test::assert_contains "src='../thumbs/$photo_html'" "$page_html"
test::assert_contains '&"'.tar' "$page_html"
test::assert_contains "href=\"page-1.html#$photo_html\"" "$view_html"
test::assert_contains "href ='../photos/$photo_html'" "$view_html"
test::assert_contains \
"href=\"$original_basepath_html/$photo_html\"" \
"$view_html"
test::assert_not_contains 'A & "quoted" ' "$page_html"
test::assert_not_contains "$photo_name" "$view_html"
test::teardown
}
test_generate_metadata_escapes_json_and_custom_tarball_suffix() {
local config_file
local fake_bin
local tarball
local title
test::setup
fake_bin="$TEST_TMPDIR/bin"
config_file="$TEST_TMPDIR/photoalbum.conf"
title=$'Metadata \e album'
test::install_fake_imagemagick "$fake_bin"
mkdir -p "$TEST_TMPDIR/incoming"
printf 'fake image\n' > "$TEST_TMPDIR/incoming/01.jpg"
{
printf 'TITLE=%q\n' "$title"
printf 'THUMBHEIGHT=30\n'
printf 'HEIGHT=120\n'
printf 'MAXPREVIEWS=40\n'
printf 'INCOMING_DIR=%q/incoming\n' "$TEST_TMPDIR"
printf 'DIST_DIR=%q/dist\n' "$TEST_TMPDIR"
printf 'TEMPLATE_DIR=%q/share/templates/default\n' "$TEST_REPO_ROOT"
printf 'TARBALL_INCLUDE=yes\n'
printf 'TARBALL_SUFFIX=.tgz\n'
} > "$config_file"
(
cd "$TEST_TMPDIR"
PATH="$fake_bin:$PATH" "$TEST_PHOTOALBUM" --generate
)
python3 - "$TEST_TMPDIR/dist/photoalbum.json" "$title" <<'PY'
import json
import pathlib
import sys
metadata_file, title = sys.argv[1:]
metadata = json.loads(pathlib.Path(metadata_file).read_text())
assert metadata["settings"]["title"] == title
assert metadata["tarball"]["included"] is True
assert metadata["tarball"]["file"].endswith(".tgz")
PY
tarball=$(find "$TEST_TMPDIR/dist" -maxdepth 1 -type f -name '*.tgz' -print)
test::assert_contains "$TEST_TMPDIR/dist/incoming-" "$tarball"
test::teardown
}
test_generate_preserves_space_filename_without_reprocessing() {
local config_file
local fake_bin
local first_output
local photo_name
local second_output
test::setup
fake_bin="$TEST_TMPDIR/bin"
config_file="$TEST_TMPDIR/photoalbum.conf"
photo_name='a b.jpg'
test::install_fake_imagemagick "$fake_bin"
mkdir -p "$TEST_TMPDIR/incoming"
printf 'fake image\n' > "$TEST_TMPDIR/incoming/$photo_name"
test::write_album_config \
"$config_file" "$TEST_TMPDIR/incoming" "$TEST_TMPDIR/dist" \
'Space test' 40
first_output=$(
cd "$TEST_TMPDIR"
PATH="$fake_bin:$PATH" "$TEST_PHOTOALBUM" --generate
)
second_output=$(
cd "$TEST_TMPDIR"
PATH="$fake_bin:$PATH" "$TEST_PHOTOALBUM" --generate
)
test::assert_file_exists "$TEST_TMPDIR/dist/photos/$photo_name"
test::assert_path_absent "$TEST_TMPDIR/dist/photos/a_b.jpg"
test::assert_contains "Processing $photo_name to" "$first_output"
test::assert_contains \
"Already exists: $TEST_TMPDIR/dist/photos/$photo_name" \
"$second_output"
test::assert_not_contains "Processing $photo_name to" "$second_output"
test::teardown
}
test_generate_handles_space_and_underscore_names_distinctly() {
local config_file
local fake_bin
local page_html
test::setup
fake_bin="$TEST_TMPDIR/bin"
config_file="$TEST_TMPDIR/photoalbum.conf"
test::install_fake_imagemagick "$fake_bin"
mkdir -p "$TEST_TMPDIR/incoming"
printf 'fake image\n' > "$TEST_TMPDIR/incoming/a b.jpg"
printf 'fake image\n' > "$TEST_TMPDIR/incoming/a_b.jpg"
test::write_album_config \
"$config_file" "$TEST_TMPDIR/incoming" "$TEST_TMPDIR/dist" \
'Collision test' 40
(
cd "$TEST_TMPDIR"
PATH="$fake_bin:$PATH" "$TEST_PHOTOALBUM" --generate
)
page_html=$(<"$TEST_TMPDIR/dist/html/page-1.html")
test::assert_file_exists "$TEST_TMPDIR/dist/photos/a b.jpg"
test::assert_file_exists "$TEST_TMPDIR/dist/photos/a_b.jpg"
test::assert_file_exists "$TEST_TMPDIR/dist/thumbs/a b.jpg"
test::assert_file_exists "$TEST_TMPDIR/dist/thumbs/a_b.jpg"
test::assert_contains "src='../thumbs/a b.jpg'" "$page_html"
test::assert_contains "src='../thumbs/a_b.jpg'" "$page_html"
test::teardown
}
test_positional_commands_fail_without_deprecation() {
local output
local old_command
local -a old_commands=(clean generate version makemake)
for old_command in "${old_commands[@]}"; do
output=$(test::capture_failure_output "$TEST_PHOTOALBUM" "$old_command")
test::assert_contains 'Usage:' "$output"
test::assert_not_contains 'deprecat' "$output"
test::assert_not_contains 'makemake' "$output"
done
}
test_unknown_options_and_conflicting_actions_fail() {
test::assert_failure 'unsupported option is rejected' \
"$TEST_PHOTOALBUM" --unknown
test::assert_failure 'generate/clean conflict is rejected' \
"$TEST_PHOTOALBUM" --generate --clean
test::assert_failure 'generate/init conflict is rejected' \
"$TEST_PHOTOALBUM" --generate --init
test::assert_failure 'clean/version conflict is rejected' \
"$TEST_PHOTOALBUM" --clean --version
}
test_empty_args_fail() {
local output
output=$(test::capture_failure_output "$TEST_PHOTOALBUM")
test::assert_contains 'Usage:' "$output"
}
test_extra_args_fail() {
test::assert_failure 'extra operand is rejected' "$TEST_PHOTOALBUM" --version extra
test::assert_failure \
'--incoming is rejected with --version' \
"$TEST_PHOTOALBUM" --version --incoming /tmp/incoming
test::assert_failure \
'--config is rejected with --init' \
"$TEST_PHOTOALBUM" --init --config custom.conf
}
test_missing_option_values_fail() {
local option
local -a value_options=(
--config
--incoming
--dist
--template
--title
--height
--thumbheight
--maxpreviews
)
for option in "${value_options[@]}"; do
test::assert_failure "$option requires a value" "$TEST_PHOTOALBUM" "$option"
done
}
main() {
trap test::teardown EXIT
test::run_case '--version succeeds' test_version
test::run_case '--init succeeds' test_init
test::run_case \
'--init refuses existing config without overwrite' \
test_init_existing_config_fails_without_overwrite
test::run_case '--clean succeeds' test_clean
test::run_case '--clean --config succeeds' test_clean_with_config
test::run_case '--clean --dist overrides config' \
test_clean_cli_dist_overrides_config
test::run_case \
'missing config ignores legacy fallbacks' \
test_missing_config_fails_without_legacy_fallbacks
test::run_case \
'--generate --config reads selected config on failure path' \
test_generate_with_config_missing_incoming_fails
test::run_case \
'--generate --config succeeds without default config' \
test_generate_with_config_succeeds_without_default_config
test::run_case \
'--generate CLI options override config' \
test_generate_cli_overrides_config_values
test::run_case \
'--generate --shuffle overrides config' \
test_generate_shuffle_override_uses_random_order
test::run_case \
'--generate --no-shuffle overrides config' \
test_generate_no_shuffle_override_uses_sorted_order
test::run_case \
'--generate --tarball overrides config' \
test_generate_cli_tarball_overrides_config
test::run_case \
'--generate --no-tarball overrides config' \
test_generate_cli_no_tarball_overrides_config
test::run_case \
'--generate ignores unsupported incoming files with warning' \
test_generate_ignores_unsupported_incoming_files_with_warning
test::run_case \
'--generate missing incoming fails' \
test_generate_missing_incoming_fails
test::run_case \
'--generate preflight rejects missing required vars' \
test_generate_preflight_rejects_missing_required_vars
test::run_case \
'--generate preflight rejects invalid numbers' \
test_generate_preflight_rejects_invalid_numbers
test::run_case \
'--generate preflight accepts empty HEIGHT' \
test_generate_preflight_accepts_empty_height
test::run_case \
'--generate preflight rejects invalid yes/no values' \
test_generate_preflight_rejects_invalid_yes_no_values
test::run_case \
'--generate preflight rejects unwritable dist parent' \
test_generate_preflight_rejects_unwritable_dist_parent
test::run_case \
'--generate preflight accepts nested new dist dir' \
test_generate_preflight_accepts_nested_new_dist_dir
test::run_case \
'--generate preflight rejects missing templates' \
test_generate_preflight_rejects_missing_templates
test::run_case \
'--generate creates output structure and --clean removes it' \
test_integration_generates_album_outputs_and_cleans
test::run_case \
'--generate replaces final dist after success' \
test_generate_replaces_dist_after_success
test::run_case \
'--generate ImageMagick failure preserves final dist' \
test_generate_imagemagick_failure_preserves_dist
test::run_case \
'--generate template failure preserves final dist' \
test_generate_template_failure_preserves_dist
test::run_case \
'--generate swap failure restores final dist' \
test_generate_swap_failure_restores_dist
test::run_case \
'--generate fails when ImageMagick is missing' \
test_generate_missing_imagemagick_fails
test::run_case \
'--generate escapes generated HTML values' \
test_generate_escapes_html_values
test::run_case \
'--generate metadata escapes JSON and custom tarball suffix' \
test_generate_metadata_escapes_json_and_custom_tarball_suffix
test::run_case \
'--generate preserves filenames with spaces without reprocessing' \
test_generate_preserves_space_filename_without_reprocessing
test::run_case \
'--generate handles spaces and underscores distinctly' \
test_generate_handles_space_and_underscore_names_distinctly
test::run_case \
'positional commands fail without deprecation output' \
test_positional_commands_fail_without_deprecation
test::run_case \
'unknown options and conflicting actions fail' \
test_unknown_options_and_conflicting_actions_fail
test::run_case 'empty args fail' test_empty_args_fail
test::run_case 'extra args fail' test_extra_args_fail
test::run_case 'missing option values fail' test_missing_option_values_fail
}
main "$@"