#!/usr/bin/env bash
set -euo pipefail
TEST_REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
declare -r TEST_REPO_ROOT
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" != IMAGE_JOBS ]; then
printf 'IMAGE_JOBS=3\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" != SPLASH_PAGE ]; then
printf 'SPLASH_PAGE=yes\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"]["image_jobs"] == "3"
assert metadata["settings"]["shuffle"] is False
assert isinstance(metadata["settings"]["splash_page"], bool)
assert "original_basepath" in metadata["settings"]
PY
}
test::assert_no_html_subdir_output() {
local -r dist_dir="$1"; shift
test::assert_path_absent "$dist_dir/html"
if grep -R -n --include='*.html' 'html/' "$dist_dir"; then
echo "FAIL: expected no html/ links in generated HTML" >&2
exit 1
fi
}
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
)
config=$(<"$TEST_TMPDIR/photoalbum.conf")
test::assert_contains \
"TEMPLATE_DIR=$TEST_REPO_ROOT/share/templates/default" \
"$config"
test::teardown
}
test_init_with_hash_in_source_path() {
local config
local output_dir
local repo_dir
test::setup
repo_dir="$TEST_TMPDIR/repo#with-hash"
output_dir="$TEST_TMPDIR/output"
mkdir -p "$repo_dir" "$output_dir"
cp -R \
"$TEST_REPO_ROOT/bin" \
"$TEST_REPO_ROOT/share" \
"$TEST_REPO_ROOT/src" \
"$repo_dir/"
(
cd "$output_dir"
PHOTOALBUM_DEFAULT_RC="$TEST_TMPDIR/missing" \
"$repo_dir/bin/photoalbum" --init >/dev/null
test::assert_file_exists photoalbum.conf
)
config=$(<"$output_dir/photoalbum.conf")
test::assert_contains \
"TEMPLATE_DIR=$repo_dir/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 "$(<"$TEST_TMPDIR/photoalbum.conf")" = 'sentinel'
test::teardown
}
test_just_install_and_deinstall_with_destdir() {
local stage_dir
test::setup
stage_dir="$TEST_TMPDIR/stage"
(
cd "$TEST_REPO_ROOT"
DESTDIR="$stage_dir" PREFIX=/usr just install
)
test::assert_file_exists "$stage_dir/usr/bin/photoalbum"
test::assert_file_exists "$stage_dir/etc/default/photoalbum"
test::assert_file_exists \
"$stage_dir/usr/share/photoalbum/templates/default/view.tmpl"
(
cd "$TEST_REPO_ROOT"
DESTDIR="$stage_dir" PREFIX=/usr just deinstall
)
test::assert_path_absent "$stage_dir/usr/bin/photoalbum"
test::assert_path_absent "$stage_dir/etc/default/photoalbum"
test::assert_path_absent "$stage_dir/usr/share/photoalbum"
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/page-2.html"
test::assert_path_absent "$TEST_TMPDIR/custom-dist/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/page-1.html")
view_html=$(<"$TEST_TMPDIR/cli-dist/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_contains 'href="page-2.html" class="arrow">⇒' \
"$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/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_random_seed_repeats_html_with_shuffle() {
local config_file
local fake_bin
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-one" \
'Seeded shuffle album' 2
(
cd "$TEST_TMPDIR"
PATH="$fake_bin:$PATH" TEST_SORT_LOG="$sort_log" \
"$TEST_PHOTOALBUM" \
--generate \
--shuffle \
--random-seed stable-seed \
--dist "$TEST_TMPDIR/dist-one"
PATH="$fake_bin:$PATH" TEST_SORT_LOG="$sort_log" \
"$TEST_PHOTOALBUM" \
--generate \
--shuffle \
--random-seed stable-seed \
--dist "$TEST_TMPDIR/dist-two"
)
if ! diff -ru \
--exclude=blurs \
--exclude=photoalbum.json \
--exclude=photos \
--exclude=thumbs \
"$TEST_TMPDIR/dist-one" \
"$TEST_TMPDIR/dist-two"; then
echo 'FAIL: seeded generation should produce identical HTML' >&2
exit 1
fi
if ! cmp -s "$TEST_TMPDIR/dist-one/index.html" \
"$TEST_TMPDIR/dist-two/index.html"; then
echo 'FAIL: seeded generation should produce identical top-level HTML' >&2
exit 1
fi
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_default_tar_opts_create_archive() {
local config_file
local fake_bin
local tar_log
local tarball
local tarball_listing
test::setup
fake_bin="$TEST_TMPDIR/bin"
config_file="$TEST_TMPDIR/photoalbum.conf"
tar_log="$TEST_TMPDIR/tar.log"
test::install_fake_imagemagick "$fake_bin"
test::install_tar_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" \
'Default tar opts' 40
printf 'TARBALL_INCLUDE=yes\n' >> "$config_file"
(
cd "$TEST_TMPDIR"
PATH="$fake_bin:$PATH" TEST_TAR_LOG="$tar_log" \
"$TEST_PHOTOALBUM" --generate
)
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::assert_contains $'arg0=-c\narg1=-f' "$(<"$tar_log")"
test::teardown
}
test_generate_custom_tarball_suffix_cleans_previous_archive() {
local config_file
local fake_bin
local first_tarball
local second_tarball
test::setup
fake_bin="$TEST_TMPDIR/bin"
config_file="$TEST_TMPDIR/photoalbum.conf"
test::install_fake_imagemagick "$fake_bin"
cat > "$fake_bin/date" <<'DATE'
#!/usr/bin/env bash
set -euo pipefail
count=0
if [ -f "$TEST_FAKE_DATE_COUNTER" ]; then
count=$(<"$TEST_FAKE_DATE_COUNTER")
fi
count=$(( count + 1 ))
printf '%s\n' "$count" > "$TEST_FAKE_DATE_COUNTER"
if [ "${1:-}" = -u ]; then
printf '2026-06-05T12:00:%02dZ\n' "$count"
else
printf '2026-06-05-1200%02d\n' "$count"
fi
DATE
chmod 0755 "$fake_bin/date"
PATH="$fake_bin:$PATH" \
test::generate_fixture_images "$TEST_TMPDIR/incoming"
test::write_album_config \
"$config_file" "$TEST_TMPDIR/incoming" "$TEST_TMPDIR/dist" \
'Custom tarball suffix cleanup' 40
{
printf 'TARBALL_INCLUDE=yes\n'
printf 'TARBALL_SUFFIX=.tgz\n'
} >> "$config_file"
(
cd "$TEST_TMPDIR"
PATH="$fake_bin:$PATH" \
TEST_FAKE_DATE_COUNTER="$TEST_TMPDIR/date-count" \
"$TEST_PHOTOALBUM" --generate
)
first_tarball=$(find "$TEST_TMPDIR/dist" -maxdepth 1 -name '*.tgz' -print)
(
cd "$TEST_TMPDIR"
PATH="$fake_bin:$PATH" \
TEST_FAKE_DATE_COUNTER="$TEST_TMPDIR/date-count" \
"$TEST_PHOTOALBUM" --generate
)
test::assert_find_count 1 "$TEST_TMPDIR/dist" '*.tgz'
test::assert_path_absent "$first_tarball"
second_tarball=$(find "$TEST_TMPDIR/dist" -maxdepth 1 -name '*.tgz' -print)
test::assert_contains "$TEST_TMPDIR/dist/incoming-" "$second_tarball"
test::teardown
}
test_generate_scalar_multi_tar_opts_create_archive() {
local config_file
local fake_bin
local tar_log
local tarball
local tarball_listing
test::setup
fake_bin="$TEST_TMPDIR/bin"
config_file="$TEST_TMPDIR/photoalbum.conf"
tar_log="$TEST_TMPDIR/tar.log"
test::install_fake_imagemagick "$fake_bin"
test::install_tar_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" \
'Scalar multi tar opts' 40
{
printf 'TARBALL_INCLUDE=yes\n'
printf 'TAR_OPTS=%q\n' '--sort=name --mtime=@0 -c'
} >> "$config_file"
(
cd "$TEST_TMPDIR"
PATH="$fake_bin:$PATH" TEST_TAR_LOG="$tar_log" \
"$TEST_PHOTOALBUM" --generate
)
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::assert_contains \
$'arg0=--sort=name\narg1=--mtime=@0\narg2=-c\narg3=-f' \
"$(<"$tar_log")"
test::teardown
}
test_default_output_reports_routine_progress() {
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"
PATH="$fake_bin:$PATH" \
test::generate_fixture_images "$TEST_TMPDIR/incoming"
test::write_album_config \
"$config_file" "$TEST_TMPDIR/incoming" "$TEST_TMPDIR/dist" \
'Default output album' 40
output=$(
cd "$TEST_TMPDIR"
PATH="$fake_bin:$PATH" "$TEST_PHOTOALBUM" --generate
)
test::assert_contains 'Processing 01-landscape.jpg to' "$output"
test::assert_contains 'Rendering header template into ' "$output"
test::assert_contains 'Rendering view template into ' "$output"
test::assert_contains 'Creating thumb ' "$output"
test::assert_not_contains 'Verbose:' "$output"
test::teardown
}
test_quiet_output_suppresses_routine_progress() {
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"
PATH="$fake_bin:$PATH" \
test::generate_fixture_images "$TEST_TMPDIR/incoming"
test::write_album_config \
"$config_file" "$TEST_TMPDIR/incoming" "$TEST_TMPDIR/dist" \
'Quiet output album' 40
output=$(
cd "$TEST_TMPDIR"
PATH="$fake_bin:$PATH" "$TEST_PHOTOALBUM" --quiet --generate
)
test::assert_not_contains 'Processing ' "$output"
test::assert_not_contains 'Rendering ' "$output"
test::assert_not_contains 'Creating thumb ' "$output"
test::assert_file_exists "$TEST_TMPDIR/dist/photoalbum.json"
test::teardown
}
test_quiet_output_keeps_errors_on_stderr() {
local config_file
local output_file
local stderr_file
local stdout
local stderr
local -i status=0
test::setup
config_file="$TEST_TMPDIR/photoalbum.conf"
output_file="$TEST_TMPDIR/stdout"
stderr_file="$TEST_TMPDIR/stderr"
test::write_album_config \
"$config_file" "$TEST_TMPDIR/missing" "$TEST_TMPDIR/dist" \
'Quiet error album' 40
set +e
(
cd "$TEST_TMPDIR"
"$TEST_PHOTOALBUM" --quiet --generate \
> "$output_file" 2> "$stderr_file"
)
status=$?
set -e
if (( status == 0 )); then
echo 'FAIL: expected quiet generation to fail' >&2
exit 1
fi
stdout=$(<"$output_file")
stderr=$(<"$stderr_file")
test "$stdout" = ''
test::assert_contains \
"ERROR: You have to create $TEST_TMPDIR/missing first" \
"$stderr"
test::teardown
}
test_verbose_output_reports_processing_decisions() {
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"
PATH="$fake_bin:$PATH" \
test::generate_fixture_images "$TEST_TMPDIR/incoming"
test::write_album_config \
"$config_file" "$TEST_TMPDIR/incoming" "$TEST_TMPDIR/dist" \
'Verbose output album' 40
(
cd "$TEST_TMPDIR"
PATH="$fake_bin:$PATH" "$TEST_PHOTOALBUM" --generate >/dev/null
)
output=$(
cd "$TEST_TMPDIR"
PATH="$fake_bin:$PATH" "$TEST_PHOTOALBUM" --verbose --generate
)
test::assert_contains 'Verbose: Selected config file: ./photoalbum.conf' \
"$output"
test::assert_contains "Verbose: Effective incoming directory: $TEST_TMPDIR/incoming" \
"$output"
test::assert_contains "Verbose: Effective output directory: $TEST_TMPDIR/dist" \
"$output"
test::assert_contains \
"Verbose: Effective template directory: $TEST_REPO_ROOT/share/templates/default" \
"$output"
test::assert_contains \
'Verbose: Tarball disabled; no archive will be created' \
"$output"
test::assert_contains \
"Verbose: Skipped existing photo $TEST_TMPDIR/dist/photos/01-landscape.jpg" \
"$output"
test::assert_contains 'Verbose: Skipped existing thumb and blur' "$output"
test::teardown
}
test_generate_image_jobs_limits_parallel_imagemagick() {
local active_file
local config_file
local fake_bin
local lock_file
local log_file
local max_file
local max_seen
test::setup
fake_bin="$TEST_TMPDIR/bin"
config_file="$TEST_TMPDIR/photoalbum.conf"
lock_file="$TEST_TMPDIR/parallel.lock"
active_file="$TEST_TMPDIR/parallel.active"
max_file="$TEST_TMPDIR/parallel.max"
log_file="$TEST_TMPDIR/parallel.log"
test::install_parallel_imagemagick_spy "$fake_bin"
mkdir -p "$TEST_TMPDIR/incoming"
printf 'fake image\n' > "$TEST_TMPDIR/incoming/01.jpg"
printf 'fake image\n' > "$TEST_TMPDIR/incoming/02.jpg"
printf 'fake image\n' > "$TEST_TMPDIR/incoming/03.jpg"
printf 'fake image\n' > "$TEST_TMPDIR/incoming/04.jpg"
test::write_album_config \
"$config_file" "$TEST_TMPDIR/incoming" "$TEST_TMPDIR/dist" \
'Parallel image jobs album' 40
(
cd "$TEST_TMPDIR"
PATH="$fake_bin:$PATH" \
TEST_PARALLEL_MAGICK_LOCK="$lock_file" \
TEST_PARALLEL_MAGICK_ACTIVE="$active_file" \
TEST_PARALLEL_MAGICK_MAX="$max_file" \
TEST_PARALLEL_MAGICK_LOG="$log_file" \
"$TEST_PHOTOALBUM" --image-jobs 2 --generate
)
max_seen=$(<"$max_file")
if (( max_seen < 2 || max_seen > 2 )); then
echo "FAIL: expected max parallel ImageMagick jobs to be 2" >&2
echo "max_seen=$max_seen" >&2
cat "$log_file" >&2
exit 1
fi
test::assert_file_exists "$TEST_TMPDIR/dist/photos/01.jpg"
test::assert_file_exists "$TEST_TMPDIR/dist/thumbs/01.jpg"
test::assert_file_exists "$TEST_TMPDIR/dist/blurs/01.jpg"
test::teardown
}
test_generate_image_jobs_waits_for_any_finished_imagemagick() {
local config_file
local fake_bin
local finish_01_line
local lock_file
local log_file
local log_output
local start_03_line
test::setup
fake_bin="$TEST_TMPDIR/bin"
config_file="$TEST_TMPDIR/photoalbum.conf"
lock_file="$TEST_TMPDIR/wait-n.lock"
log_file="$TEST_TMPDIR/wait-n.log"
test::install_wait_n_imagemagick_spy "$fake_bin"
mkdir -p "$TEST_TMPDIR/incoming"
printf 'fake image\n' > "$TEST_TMPDIR/incoming/01.jpg"
printf 'fake image\n' > "$TEST_TMPDIR/incoming/02.jpg"
printf 'fake image\n' > "$TEST_TMPDIR/incoming/03.jpg"
printf 'fake image\n' > "$TEST_TMPDIR/incoming/04.jpg"
test::write_album_config \
"$config_file" "$TEST_TMPDIR/incoming" "$TEST_TMPDIR/dist" \
'Wait n image jobs album' 40
(
cd "$TEST_TMPDIR"
PATH="$fake_bin:$PATH" \
TEST_WAIT_N_MAGICK_LOCK="$lock_file" \
TEST_WAIT_N_MAGICK_LOG="$log_file" \
"$TEST_PHOTOALBUM" --image-jobs 2 --generate
)
log_output=$(<"$log_file")
test::assert_contains 'start photos/03.jpg' "$log_output"
test::assert_contains 'finish photos/01.jpg' "$log_output"
start_03_line=$(
grep -n '^start photos/03\.jpg$' "$log_file" \
| head -n 1 \
| cut -d: -f1
)
finish_01_line=$(
grep -n '^finish photos/01\.jpg$' "$log_file" \
| head -n 1 \
| cut -d: -f1
)
if (( start_03_line >= finish_01_line )); then
echo 'FAIL: expected photos/03.jpg to start before photos/01.jpg finished' >&2
cat "$log_file" >&2
exit 1
fi
test::teardown
}
test_generate_image_jobs_limits_parallel_identify() {
local active_file
local config_file
local fake_bin
local identify_count
local lock_file
local log_file
local max_file
local max_seen
test::setup
fake_bin="$TEST_TMPDIR/bin"
config_file="$TEST_TMPDIR/photoalbum.conf"
lock_file="$TEST_TMPDIR/identify.lock"
active_file="$TEST_TMPDIR/identify.active"
max_file="$TEST_TMPDIR/identify.max"
log_file="$TEST_TMPDIR/identify.log"
test::install_parallel_identify_spy "$fake_bin"
mkdir -p "$TEST_TMPDIR/incoming"
printf 'fake image\n' > "$TEST_TMPDIR/incoming/01.jpg"
printf 'fake image\n' > "$TEST_TMPDIR/incoming/02.jpg"
printf 'fake image\n' > "$TEST_TMPDIR/incoming/03.jpg"
printf 'fake image\n' > "$TEST_TMPDIR/incoming/04.jpg"
test::write_album_config \
"$config_file" "$TEST_TMPDIR/incoming" "$TEST_TMPDIR/dist" \
'Parallel identify album' 40
(
cd "$TEST_TMPDIR"
PATH="$fake_bin:$PATH" \
TEST_PARALLEL_IDENTIFY_LOCK="$lock_file" \
TEST_PARALLEL_IDENTIFY_ACTIVE="$active_file" \
TEST_PARALLEL_IDENTIFY_MAX="$max_file" \
TEST_PARALLEL_IDENTIFY_LOG="$log_file" \
"$TEST_PHOTOALBUM" --image-jobs 2 --generate
)
max_seen=$(<"$max_file")
if (( max_seen > 2 )); then
echo 'FAIL: expected at most 2 parallel ImageMagick identify jobs' >&2
echo "max_seen=$max_seen" >&2
cat "$log_file" >&2
exit 1
fi
identify_count=$(grep -c '^start ' "$log_file")
if (( identify_count != 4 )); then
echo 'FAIL: expected one identify call per source image' >&2
echo "identify_count=$identify_count" >&2
cat "$log_file" >&2
exit 1
fi
test::teardown
}
test_repeated_output_flags_use_last_value() {
local config_file
local fake_bin
local quiet_last_output
local verbose_last_output
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" \
'Repeated output flags album' 40
verbose_last_output=$(
cd "$TEST_TMPDIR"
PATH="$fake_bin:$PATH" "$TEST_PHOTOALBUM" \
--quiet --verbose --generate
)
quiet_last_output=$(
cd "$TEST_TMPDIR"
PATH="$fake_bin:$PATH" "$TEST_PHOTOALBUM" \
--verbose --quiet --generate
)
test::assert_contains 'Verbose: Selected config file: ./photoalbum.conf' \
"$verbose_last_output"
test::assert_not_contains 'Verbose:' "$quiet_last_output"
test::assert_not_contains 'Processing ' "$quiet_last_output"
test::assert_not_contains 'Rendering ' "$quiet_last_output"
test::teardown
}
test_print_config_reflects_defaults() {
local expected
local output
test::setup
output=$(
cd "$TEST_TMPDIR"
PHOTOALBUM_DEFAULT_TEMPLATE_DIR="$TEST_TMPDIR/missing-installed" \
"$TEST_PHOTOALBUM" \
--print-config \
--config "$TEST_REPO_ROOT/src/photoalbum.default.conf"
)
expected=$(cat <> "$config_file"
output=$(
cd "$TEST_TMPDIR"
PHOTOALBUM_DEFAULT_TEMPLATE_DIR="$TEST_TMPDIR/missing-installed" \
"$TEST_PHOTOALBUM" --print-config
)
test::assert_contains "TEMPLATE_DIR=$template_dir" "$output"
test::teardown
}
test_print_config_resolves_installed_default_template() {
local config_file
local installed_template_dir
local output
test::setup
config_file="$TEST_TMPDIR/photoalbum.conf"
installed_template_dir="$TEST_TMPDIR/usr/share/photoalbum/templates/default"
mkdir -p "$(dirname "$installed_template_dir")"
cp -R "$TEST_REPO_ROOT/share/templates/default" "$installed_template_dir"
test::write_preflight_config \
"$config_file" "$TEST_TMPDIR/incoming" "$TEST_TMPDIR/dist" \
/usr/share/photoalbum/templates/default
output=$(
cd "$TEST_TMPDIR"
PHOTOALBUM_DEFAULT_TEMPLATE_DIR="$installed_template_dir" \
"$TEST_PHOTOALBUM" --print-config
)
test::assert_contains "TEMPLATE_DIR=$installed_template_dir" "$output"
test::teardown
}
test_print_config_resolves_repo_default_template() {
local config_file
local output
test::setup
config_file="$TEST_TMPDIR/photoalbum.conf"
test::write_preflight_config \
"$config_file" "$TEST_TMPDIR/incoming" "$TEST_TMPDIR/dist" \
/usr/share/photoalbum/templates/default
output=$(
cd "$TEST_TMPDIR"
PHOTOALBUM_DEFAULT_TEMPLATE_DIR="$TEST_TMPDIR/missing-installed" \
"$TEST_PHOTOALBUM" --print-config
)
test::assert_contains \
"TEMPLATE_DIR=$TEST_REPO_ROOT/share/templates/default" \
"$output"
test::teardown
}
test_print_config_keeps_cli_template_override() {
local config_file
local installed_template_dir
local output
local template_dir
test::setup
config_file="$TEST_TMPDIR/photoalbum.conf"
installed_template_dir="$TEST_TMPDIR/usr/share/photoalbum/templates/default"
template_dir="$TEST_TMPDIR/cli-template"
mkdir -p "$(dirname "$installed_template_dir")"
cp -R "$TEST_REPO_ROOT/share/templates/default" "$installed_template_dir"
test::write_preflight_config \
"$config_file" "$TEST_TMPDIR/incoming" "$TEST_TMPDIR/dist" \
/usr/share/photoalbum/templates/default
output=$(
cd "$TEST_TMPDIR"
PHOTOALBUM_DEFAULT_TEMPLATE_DIR="$installed_template_dir" \
"$TEST_PHOTOALBUM" --print-config --template "$template_dir"
)
test::assert_contains "TEMPLATE_DIR=$template_dir" "$output"
test::teardown
}
test_print_config_reads_selected_config() {
local config_file
local expected
local output
test::setup
config_file="$TEST_TMPDIR/custom.conf"
test::write_album_config \
"$config_file" "$TEST_TMPDIR/custom-incoming" \
"$TEST_TMPDIR/custom-dist" 'Selected config' 7
printf 'SHUFFLE=yes\n' >> "$config_file"
output=$(
cd "$TEST_TMPDIR"
"$TEST_PHOTOALBUM" --print-config --config "$config_file"
)
expected=$(cat <> "$config_file"
output=$(
cd "$TEST_TMPDIR"
"$TEST_PHOTOALBUM" \
--print-config --no-shuffle --no-splash --no-tarball
)
test::assert_contains 'SHUFFLE=no' "$output"
test::assert_contains 'SPLASH_PAGE=no' "$output"
test::assert_contains 'TARBALL_INCLUDE=no' "$output"
test::teardown
}
test_print_config_normalizes_scalar_and_array_tar_opts() {
local array_config
local array_output
local scalar_config
local scalar_output
test::setup
scalar_config="$TEST_TMPDIR/scalar.conf"
array_config="$TEST_TMPDIR/array.conf"
test::write_album_config \
"$scalar_config" "$TEST_TMPDIR/incoming" "$TEST_TMPDIR/dist" \
'Scalar tar opts' 40
{
printf 'TARBALL_INCLUDE=yes\n'
printf 'TAR_OPTS=%q\n' '--sort=name --mtime=@0 -c'
} >> "$scalar_config"
test::write_album_config \
"$array_config" "$TEST_TMPDIR/incoming" "$TEST_TMPDIR/dist" \
'Array tar opts' 40
printf 'TAR_OPTS=(--sort=name --mtime=@0 -c)\n' >> "$array_config"
scalar_output=$("$TEST_PHOTOALBUM" --print-config --config "$scalar_config")
array_output=$("$TEST_PHOTOALBUM" --print-config --config "$array_config")
test::assert_contains \
'TAR_OPTS=( --sort=name --mtime=@0 -c )' \
"$scalar_output"
test::assert_contains \
'TAR_OPTS=( --sort=name --mtime=@0 -c )' \
"$array_output"
test::teardown
}
test_print_config_quiet_and_verbose_keep_machine_output() {
local config_file
local plain_output
local quiet_output
local verbose_output
test::setup
config_file="$TEST_TMPDIR/photoalbum.conf"
test::write_album_config \
"$config_file" "$TEST_TMPDIR/incoming" "$TEST_TMPDIR/dist" \
'Output mode config' 40
plain_output=$(
cd "$TEST_TMPDIR"
"$TEST_PHOTOALBUM" --print-config
)
quiet_output=$(
cd "$TEST_TMPDIR"
"$TEST_PHOTOALBUM" --quiet --print-config
)
verbose_output=$(
cd "$TEST_TMPDIR"
"$TEST_PHOTOALBUM" --verbose --print-config
)
test "$quiet_output" = "$plain_output"
test "$verbose_output" = "$plain_output"
test::assert_not_contains 'Verbose:' "$verbose_output"
test::teardown
}
test_print_config_validates_basic_values_without_generation_preflight() {
local config_file
local output
test::setup
config_file="$TEST_TMPDIR/photoalbum.conf"
test::write_album_config \
"$config_file" "$TEST_TMPDIR/missing-incoming" \
"$TEST_TMPDIR/missing-parent/dist" 'Printable missing paths' 40
printf 'TEMPLATE_DIR=%q\n' "$TEST_TMPDIR/missing-template" >> "$config_file"
output=$(
cd "$TEST_TMPDIR"
"$TEST_PHOTOALBUM" --print-config
)
test::assert_contains "INCOMING_DIR=$TEST_TMPDIR/missing-incoming" "$output"
test::assert_contains "DIST_DIR=$TEST_TMPDIR/missing-parent/dist" "$output"
test::assert_contains "TEMPLATE_DIR=$TEST_TMPDIR/missing-template" "$output"
printf 'MAXPREVIEWS=not-a-number\n' >> "$config_file"
output=$(
cd "$TEST_TMPDIR"
test::capture_failure_output "$TEST_PHOTOALBUM" --print-config
)
test::assert_contains 'ERROR: MAXPREVIEWS must be a positive integer' \
"$output"
test::assert_path_absent "$TEST_TMPDIR/missing-parent"
test::teardown
}
test_dry_run_reports_cli_overrides_without_writes() {
local config_file
local dist_dir
local fake_bin
local forbidden_log
local output
test::setup
fake_bin="$TEST_TMPDIR/bin"
config_file="$TEST_TMPDIR/custom.conf"
dist_dir="$TEST_TMPDIR/dry-dist"
forbidden_log="$TEST_TMPDIR/forbidden-tools.log"
test::install_fake_imagemagick "$fake_bin"
PATH="$fake_bin:$PATH" \
test::generate_fixture_images "$TEST_TMPDIR/incoming"
test::install_failing_generation_tools "$fake_bin"
{
printf 'TITLE=%q\n' 'Config dry title'
printf 'THUMBHEIGHT=10\n'
printf 'HEIGHT=20\n'
printf 'MAXPREVIEWS=40\n'
printf 'SHUFFLE=no\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=no\n'
} > "$config_file"
output=$(
cd "$TEST_TMPDIR"
PATH="$fake_bin:$PATH" \
TEST_FORBIDDEN_TOOL_LOG="$forbidden_log" \
"$TEST_PHOTOALBUM" \
--dry-run \
--config "$config_file" \
--incoming "$TEST_TMPDIR/incoming" \
--dist "$dist_dir" \
--template "$TEST_REPO_ROOT/share/templates/default" \
--title 'CLI dry title' \
--height 456 \
--thumbheight 45 \
--maxpreviews 2 \
--image-jobs 2 \
--random-seed dry-seed \
--shuffle \
--no-splash \
--tarball
)
test::assert_contains 'Dry run: no files will be written.' "$output"
test::assert_contains "Config source: $config_file" "$output"
test::assert_contains "Incoming directory: $TEST_TMPDIR/incoming" "$output"
test::assert_contains "Output directory: $dist_dir" "$output"
test::assert_contains \
"Template directory: $TEST_REPO_ROOT/share/templates/default" \
"$output"
test::assert_contains 'Title: CLI dry title' "$output"
test::assert_contains 'Height: 456' "$output"
test::assert_contains 'Thumb height: 45' "$output"
test::assert_contains 'Max previews per page: 2' "$output"
test::assert_contains 'Image jobs: 2' "$output"
test::assert_contains 'Random seed: dry-seed' "$output"
test::assert_contains 'Shuffle: yes' "$output"
test::assert_contains 'Splash page: no' "$output"
test::assert_contains 'Image count: 6' "$output"
test::assert_contains 'Tarball setting: yes' "$output"
test::assert_contains 'Tarball name plan: incoming-.tar' \
"$output"
test::assert_contains 'Planned directories:' "$output"
test::assert_contains " $dist_dir/photos" "$output"
test::assert_contains 'Planned generated files:' "$output"
test::assert_contains \
" $dist_dir/index.html (1 album index redirect)" \
"$output"
test::assert_contains " $dist_dir/photoalbum.json" "$output"
test::assert_contains " $dist_dir/photos/* (6 image files)" "$output"
test::assert_contains " $dist_dir/thumbs/* (6 image files)" "$output"
test::assert_contains " $dist_dir/blurs/* (6 image files)" "$output"
test::assert_contains " $dist_dir/page-*.html (3 preview pages)" \
"$output"
test::assert_contains \
" $dist_dir/[page]-[image].html (6 view pages)" \
"$output"
test::assert_contains \
" $dist_dir/[page]-[image]-details.html (6 details pages)" \
"$output"
test::assert_contains \
" $dist_dir/[redirect].html (7 navigation redirects)" \
"$output"
test::assert_not_contains "$dist_dir/html" "$output"
test::assert_contains " $dist_dir/incoming-.tar" "$output"
test::assert_not_contains 'Processing ' "$output"
test::assert_not_contains 'Creating tarball ' "$output"
test::assert_path_absent "$dist_dir"
test::assert_path_absent "$TEST_TMPDIR/config-dist"
test::assert_path_absent "$forbidden_log"
test::assert_no_staging_dirs "$TEST_TMPDIR"
test::teardown
}
test_dry_run_rejects_invalid_config_and_input() {
local config_file
local dist_dir
local incoming_dir
local output
test::setup
config_file="$TEST_TMPDIR/photoalbum.conf"
incoming_dir="$TEST_TMPDIR/incoming"
dist_dir="$TEST_TMPDIR/dist"
mkdir -p "$incoming_dir"
test::write_preflight_config \
"$config_file" "$incoming_dir" "$dist_dir" \
"$TEST_REPO_ROOT/share/templates/default"
printf 'MAXPREVIEWS=not-a-number\n' >> "$config_file"
output=$(
cd "$TEST_TMPDIR"
test::capture_failure_output \
"$TEST_PHOTOALBUM" --dry-run --config "$config_file"
)
test::assert_contains 'ERROR: MAXPREVIEWS must be a positive integer' \
"$output"
test::assert_path_absent "$dist_dir"
test::write_preflight_config \
"$config_file" "$TEST_TMPDIR/missing" "$dist_dir" \
"$TEST_REPO_ROOT/share/templates/default"
output=$(
cd "$TEST_TMPDIR"
test::capture_failure_output \
"$TEST_PHOTOALBUM" --dry-run --config "$config_file"
)
test::assert_contains \
"ERROR: You have to create $TEST_TMPDIR/missing first" \
"$output"
test::assert_path_absent "$dist_dir"
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 'extension-looking basename\n' > "$TEST_TMPDIR/incoming/jpg"
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/jpg"
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/jpg"
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_contains \
'WARNING: Ignoring unsupported incoming file: jpg' \
"$output"
test::assert_not_contains 'Processing notes.txt' "$output"
test::assert_not_contains 'Processing README.md' "$output"
test::assert_not_contains 'Processing jpg' "$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
IMAGE_JOBS
)
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
SPLASH_PAGE
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/details.tmpl must be readable" \
"$output"
test::assert_path_absent "$dist_dir"
test::teardown
}
test_dry_run_rejects_missing_default_template_dir() {
local config_file
local dist_dir
local incoming_dir
local output
local photoalbum_copy
local template_dir
test::setup
config_file="$TEST_TMPDIR/photoalbum.conf"
dist_dir="$TEST_TMPDIR/dist"
incoming_dir="$TEST_TMPDIR/incoming"
photoalbum_copy="$TEST_TMPDIR/photoalbum"
template_dir="$TEST_TMPDIR/missing-default-template"
mkdir -p "$incoming_dir"
cp "$TEST_PHOTOALBUM" "$photoalbum_copy"
test::write_preflight_config \
"$config_file" "$incoming_dir" "$dist_dir" \
/usr/share/photoalbum/templates/default
output=$(
cd "$TEST_TMPDIR"
PHOTOALBUM_DEFAULT_TEMPLATE_DIR="$template_dir" \
test::capture_failure_output \
"$photoalbum_copy" --dry-run --config "$config_file"
)
test::assert_contains \
"ERROR: TEMPLATE_DIR $template_dir must be a readable directory" \
"$output"
test::assert_path_absent "$dist_dir"
test::teardown
}
test_integration_generates_album_outputs_and_cleans() {
local config_file
local details_html
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/page-1.html"
test::assert_file_exists "$TEST_TMPDIR/dist/page-2.html"
test::assert_file_exists "$TEST_TMPDIR/dist/page-3.html"
test::assert_file_exists "$TEST_TMPDIR/dist/1-1.html"
test::assert_file_exists "$TEST_TMPDIR/dist/1-1-details.html"
test::assert_file_exists "$TEST_TMPDIR/dist/3-2.html"
test::assert_file_exists "$TEST_TMPDIR/dist/3-2-details.html"
test::assert_file_exists "$TEST_TMPDIR/dist/index.html"
test::assert_file_exists "$TEST_TMPDIR/dist/photoalbum.json"
test::assert_no_html_subdir_output "$TEST_TMPDIR/dist"
page_html=$(<"$TEST_TMPDIR/dist/page-1.html")
details_html=$(<"$TEST_TMPDIR/dist/1-1-details.html")
top_index_html=$(<"$TEST_TMPDIR/dist/index.html")
test::assert_contains "name='04 filename with spaces.jpg'" \
"$(<"$TEST_TMPDIR/dist/page-2.html")"
test::assert_contains 'Next 2 pictures' "$page_html"
test::assert_contains 'No EXIF details available.' "$details_html"
test::assert_contains '' "$details_html"
test::assert_contains '
' "$details_html"
test::assert_contains '
' \
"$details_html"
test::assert_contains "class='view details-photo" "$details_html"
test::assert_contains 'href="1-1.html">Image view' "$details_html"
test::assert_contains '
Integration album' "$top_index_html"
test::assert_contains 'Enter album' "$top_index_html"
test::assert_contains 'href="page-1.html"' "$top_index_html"
test::assert_contains \
'

"$dist_dir/1-$view.html"
record_rendered_view_page rendered_view_pages rendered_last_views \
1 "$view"
done
: > "$dist_dir/2-1.html"
record_rendered_view_page rendered_view_pages rendered_last_views 2 1
touch -t 202606050000 "$dist_dir"/1-*.html "$dist_dir/2-1.html"
render_view_redirects "$html_dir" rendered_view_pages rendered_last_views
test::assert_file_exists "$dist_dir/1-11.html"
redirect_html=$(<"$dist_dir/1-11.html")
test::assert_contains 'url=2-1.html' "$redirect_html"
test "$(<"$dist_dir/1-10.html")" = ''
test::teardown
}
test_render_view_redirects_wraps_when_last_page_full() {
local dist_dir
local html_dir
local next_redirect_html
local prev_redirect_html
# Passed by name to record_rendered_view_page and render_view_redirects.
# shellcheck disable=SC2034
local -A rendered_last_views=()
# Passed by name to record_rendered_view_page and render_view_redirects.
# shellcheck disable=SC2034
local -a rendered_view_pages=()
local view
test::setup
dist_dir="$TEST_TMPDIR/dist"
html_dir='.'
mkdir -p "$dist_dir"
# shellcheck source=/dev/null
source <(sed '$d' "$TEST_PHOTOALBUM")
export DIST_DIR="$dist_dir"
export TEMPLATE_DIR="$TEST_REPO_ROOT/share/templates/default"
export PHOTOALBUM_OUTPUT_MODE=quiet
export MAXPREVIEWS=2
for view in 1 2; do
: > "$dist_dir/1-$view.html"
: > "$dist_dir/2-$view.html"
: > "$dist_dir/3-$view.html"
record_rendered_view_page rendered_view_pages rendered_last_views \
1 "$view"
record_rendered_view_page rendered_view_pages rendered_last_views \
2 "$view"
record_rendered_view_page rendered_view_pages rendered_last_views \
3 "$view"
done
render_view_redirects "$html_dir" rendered_view_pages rendered_last_views
test::assert_file_exists "$dist_dir/0-2.html"
test::assert_file_exists "$dist_dir/3-3.html"
prev_redirect_html=$(<"$dist_dir/0-2.html")
next_redirect_html=$(<"$dist_dir/3-3.html")
test::assert_contains 'url=3-2.html' "$prev_redirect_html"
test::assert_contains 'url=1-1.html' "$next_redirect_html"
test::assert_path_absent "$dist_dir/4-1.html"
test::teardown
}
test_generate_config_no_splash_keeps_index_redirect() {
local config_file
local fake_bin
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" \
'No splash config album' 40
printf 'SPLASH_PAGE=no\n' >> "$config_file"
(
cd "$TEST_TMPDIR"
PATH="$fake_bin:$PATH" "$TEST_PHOTOALBUM" --generate
)
top_index_html=$(<"$TEST_TMPDIR/dist/index.html")
test::assert_contains 'url=page-1.html' "$top_index_html"
test::assert_not_contains 'Enter album' "$top_index_html"
test::assert_not_contains '