#!/usr/bin/env atf-sh
# SPDX-License-Identifier: GPL-2.0-only

. $(atf_get_srcdir)/test_env.sh
init_tests \
	active_lock_skips_builder \
	simultaneous_builders_only_build_once \
	termination_releases_lock \
	killed_builder_keeps_lock_until_children_exit \
	config_failure_releases_lock \
	architecture_detection_failure \
	stale_pid_does_not_block_builder \
	git_branch_checkout_failure \
	git_pull_failure \
	git_revision_detection_failures \
	git_release_checkout_failure \
	git_release_revision_failure \
	git_empty_revision_failure \
	release_build_records_tag_commit \
	noop_when_uploaded_marker_exists \
	build_progress_and_package_upload_notification \
	configured_paths_override_buildrepo_paths \
	relative_paths_stay_consistent \
	default_paths_passed_to_buildrepo \
	edge_rc_tag_does_not_create_release \
	force_release_on_edge_uses_edge_name_and_profile \
	stable_branch_release_uses_versioned_name \
	release_dependency_failure_retries \
	release_image_failure_retries \
	release_upload_failure_retries \
	release_retry_without_upload_marker \
	release_waits_for_package_upload \
	invalid_pending_release_stops_builder \
	release_cleanup_failure_retries \
	release_package_failure_retries \
	release_termination_retries \
	network_disabled_keeps_pending_release \
	buildrepo_success_with_fail_output \
	buildrepo_failure_without_output \
	buildrepo_progress_then_failure \
	buildrepo_failure_reports_failed_and_preserves_log

setup_builder_env() {
	TEST_ROOT="$PWD/testroot"
	TEST_BIN_DIR="$TEST_ROOT/bin"
	TEST_STATE_DIR="$TEST_ROOT/state"
	TEST_APORTS_DIR="$TEST_ROOT/aports checkout"
	TEST_PACKAGES_DIR="$TEST_ROOT/package output"
	TEST_LOG_DIR="$TEST_ROOT/logs"
	TEST_HOME_DIR="$TEST_ROOT/home"
	APORTS_BUILD_CONF="$TEST_ROOT/aports build.conf"
	APORTS_BUILD_LOCKFILE="$TEST_ROOT/aports build.pid"
	TMPDIR="$TEST_ROOT/tmp files"

	export TEST_ROOT TEST_BIN_DIR TEST_STATE_DIR TEST_APORTS_DIR TEST_PACKAGES_DIR
	export TEST_LOG_DIR TEST_HOME_DIR APORTS_BUILD_CONF APORTS_BUILD_LOCKFILE TMPDIR
	export HOME="$TEST_HOME_DIR"
	export MOCK_STATE_DIR="$TEST_STATE_DIR"
	export PATH="$TEST_BIN_DIR:$(atf_get_srcdir)/..:$PATH"

	mkdir -p "$TEST_BIN_DIR" "$TEST_STATE_DIR" "$TEST_APORTS_DIR/scripts" \
		"$TEST_PACKAGES_DIR" "$TEST_LOG_DIR" "$TEST_HOME_DIR" "$TMPDIR"

	create_mock_commands
	create_fake_mkimage
	write_config "${1:-master}"
	set_git_state "${2:-v1.0-0-gold}" "${3:-v1.0}" "${4:-v1.0-1-gnew}" "${5:-v1.0}" "${1:-master}"
}

create_mock_commands() {
	cat >"$TEST_BIN_DIR/abuild" <<-'EOF'
		#!/bin/sh
		case "$1" in
		-A)
			if [ -f "$MOCK_STATE_DIR/abuild.arch" ]; then
				cat "$MOCK_STATE_DIR/abuild.arch"
			else
				echo x86_64
			fi
			[ ! -f "$MOCK_STATE_DIR/abuild.rc" ] || exit "$(cat "$MOCK_STATE_DIR/abuild.rc")"
			;;
		*) exit 0 ;;
		esac
	EOF
	chmod +x "$TEST_BIN_DIR/abuild"

	cat >"$TEST_BIN_DIR/abuild-apk" <<-'EOF'
		#!/bin/sh
		printf '%s\n' "$*" >> "$MOCK_STATE_DIR/abuild-apk.log"
		case "$*" in
		'add --virtual .alpine-release-deps '*)
			: > "$MOCK_STATE_DIR/release-deps-installed"
			[ ! -f "$MOCK_STATE_DIR/release-add.rc" ] || exit "$(cat "$MOCK_STATE_DIR/release-add.rc")"
			;;
		'del .alpine-release-deps')
			[ ! -f "$MOCK_STATE_DIR/release-del.rc" ] || exit "$(cat "$MOCK_STATE_DIR/release-del.rc")"
			rm -f "$MOCK_STATE_DIR/release-deps-installed"
			;;
		esac
		exit 0
	EOF
	chmod +x "$TEST_BIN_DIR/abuild-apk"

	cat >"$TEST_BIN_DIR/hostname" <<-'EOF'
		#!/bin/sh
		echo builder1
	EOF
	chmod +x "$TEST_BIN_DIR/hostname"

	cat >"$TEST_BIN_DIR/mosquitto_pub" <<-'EOF'
		#!/bin/sh
		topic=
		msg=
		retain=0
		while [ $# -gt 0 ]; do
			case "$1" in
			-t) topic=$2; shift 2 ;;
			-m) msg=$2; shift 2 ;;
			-r) retain=1; shift ;;
			-q|-h) shift 2 ;;
			*) shift ;;
			esac
		done
		printf '%s|%s|%s\n' "$topic" "$msg" "$retain" >> "$MOCK_STATE_DIR/mqtt.log"
		exit 0
	EOF
	chmod +x "$TEST_BIN_DIR/mosquitto_pub"

	cat >"$TEST_BIN_DIR/buildrepo" <<-'EOF'
		#!/bin/sh
		printf '%s\n' "$*" >> "$MOCK_STATE_DIR/buildrepo.args"
		printf '%s\n' "$@" > "$MOCK_STATE_DIR/buildrepo.argv"
		if [ -f "$MOCK_STATE_DIR/check-paths" ]; then
			while [ $# -gt 0 ]; do
				case "$1" in
				-a) aportsdir=$2; shift 2;;
				-d) repodest=$2; shift 2;;
				-p) shift;;
				*) repo=$1; shift;;
				esac
			done
			[ "$aportsdir" = "$TEST_APORTS_DIR" ] || exit 91
			[ "$repodest" = "$TEST_PACKAGES_DIR" ] || exit 92
			[ "$PWD" = "$aportsdir" ] || exit 93
			mkdir -p "$repodest/$repo/x86_64"
			: > "$repodest/$repo/x86_64/pkgfoo-1.0-r0.apk"
		fi
		if [ -f "$MOCK_STATE_DIR/buildrepo.pause" ]; then
			: > "$MOCK_STATE_DIR/buildrepo.started"
			sleep 1
		fi
		if [ -f "$MOCK_STATE_DIR/buildrepo.stdout" ]; then
			cat "$MOCK_STATE_DIR/buildrepo.stdout"
		fi
		if [ -f "$MOCK_STATE_DIR/buildrepo.wait_for_progress" ]; then
			for i in $(seq 1 100); do
				if grep -F 'build/builder1|1/2 3/4 main/pkgfoo 1.0-r0|1' "$MOCK_STATE_DIR/mqtt.log" >/dev/null 2>&1; then
					: > "$MOCK_STATE_DIR/buildrepo.progress_seen"
					break
				fi
				sleep 0.05
			done
			[ -f "$MOCK_STATE_DIR/buildrepo.progress_seen" ] || exit 88
		fi
		if [ -f "$MOCK_STATE_DIR/buildrepo.rc" ]; then
			rc=$(cat "$MOCK_STATE_DIR/buildrepo.rc")
		else
			rc=0
		fi
		exit "$rc"
	EOF
	chmod +x "$TEST_BIN_DIR/buildrepo"

	cat >"$TEST_BIN_DIR/git" <<-'EOF'
		#!/bin/sh
		stage_file="$MOCK_STATE_DIR/git-stage"
		stage=$(cat "$stage_file" 2>/dev/null || echo old)
		printf '%s\n' "$*" >> "$MOCK_STATE_DIR/git.log"
		operation=$1
		case "$1" in
		describe)
			[ "$2" = "--abbrev=0" ] && operation=tag
			operation="$operation-$stage"
			;;
		checkout)
			if [ "$2" = "$(cat "$MOCK_STATE_DIR/git-branch")" ]; then
				operation=checkout-branch
			else
				operation=checkout-tag
			fi
			;;
		esac
		if [ -f "$MOCK_STATE_DIR/git.$operation.rc" ]; then
			exit "$(cat "$MOCK_STATE_DIR/git.$operation.rc")"
		fi
		case "$1" in
		describe)
			if [ "$2" = "--abbrev=0" ]; then
				if [ "$stage" = "old" ]; then
					cat "$MOCK_STATE_DIR/git-old-tag"
				else
					cat "$MOCK_STATE_DIR/git-new-tag"
				fi
			elif [ "$stage" = "release" ]; then
				cat "$MOCK_STATE_DIR/git-new-tag"
			else
				if [ "$stage" = "old" ]; then
					cat "$MOCK_STATE_DIR/git-old-desc"
				else
					cat "$MOCK_STATE_DIR/git-new-desc"
				fi
			fi
			;;
		checkout)
			if [ "$2" != "$(cat "$MOCK_STATE_DIR/git-branch")" ]; then
				echo release > "$stage_file"
			fi
			;;
		pull)
			echo new > "$stage_file"
			;;
		rev-parse)
			cat "$MOCK_STATE_DIR/git-$stage-commit"
			;;
		*)
			printf 'unexpected git %s\n' "$*" >> "$MOCK_STATE_DIR/git.log"
			;;
		esac
		exit 0
	EOF
	chmod +x "$TEST_BIN_DIR/git"

	cat >"$TEST_BIN_DIR/rsync" <<-'EOF'
		#!/bin/sh
		kind=packages
		for arg in "$@"; do
			case "$arg" in
			*releases/*) kind=release ;;
			esac
		done
		printf '%s:%s\n' "$kind" "$*" >> "$MOCK_STATE_DIR/rsync.log"
		if [ "$kind" = packages ] && [ -f "$MOCK_STATE_DIR/check-paths" ]; then
			[ "$PWD" = "$TEST_PACKAGES_DIR" ] || exit 94
			[ -f main/x86_64/pkgfoo-1.0-r0.apk ] || exit 95
		fi
		if [ -f "$MOCK_STATE_DIR/rsync.$kind.fail-once" ]; then
			rm -f "$MOCK_STATE_DIR/rsync.$kind.fail-once"
			exit 42
		fi
		if [ -f "$MOCK_STATE_DIR/rsync.$kind.stdout" ]; then
			cat "$MOCK_STATE_DIR/rsync.$kind.stdout"
		fi
		if [ -f "$MOCK_STATE_DIR/rsync.$kind.rc" ]; then
			rc=$(cat "$MOCK_STATE_DIR/rsync.$kind.rc")
		else
			rc=0
		fi
		exit "$rc"
	EOF
	chmod +x "$TEST_BIN_DIR/rsync"
}

create_fake_mkimage() {
	cat >"$TEST_APORTS_DIR/scripts/mkimage.sh" <<-'EOF'
		#!/bin/sh
		printf '%s\n' "$*" >> "$MOCK_STATE_DIR/mkimage.log"
		printf '%s\n' "$@" > "$MOCK_STATE_DIR/mkimage.argv"
		cp "$TEST_LOG_DIR/builder1.log" "$MOCK_STATE_DIR/mkimage.build.log"
		if [ -f "$MOCK_STATE_DIR/mkimage.pause" ]; then
			: > "$MOCK_STATE_DIR/mkimage.started"
			sleep 1
		fi
		outdir=
		while [ $# -gt 0 ]; do
			case "$1" in
			--outdir) outdir=$2; shift 2 ;;
			*) shift ;;
			esac
		done
		if [ -n "$outdir" ]; then
			mkdir -p "$outdir"
			: > "$outdir/release-artifact.iso"
		fi
		if [ -f "$MOCK_STATE_DIR/mkimage.rc" ]; then
			rc=$(cat "$MOCK_STATE_DIR/mkimage.rc")
		else
			rc=0
		fi
		exit "$rc"
	EOF
	chmod +x "$TEST_APORTS_DIR/scripts/mkimage.sh"
}

write_config() {
	cat >"$APORTS_BUILD_CONF" <<-EOF
		git_branch=$1
		APORTS="$TEST_APORTS_DIR"
		REPOSDIR="$TEST_PACKAGES_DIR"
		REPOS="main"
		logdir=$TEST_LOG_DIR
		buildrepo="buildrepo -p"
	EOF
}

set_git_state() {
	printf '%s\n' "$1" > "$TEST_STATE_DIR/git-old-desc"
	printf '%s\n' "$2" > "$TEST_STATE_DIR/git-old-tag"
	printf '%s\n' "$3" > "$TEST_STATE_DIR/git-new-desc"
	printf '%s\n' "$4" > "$TEST_STATE_DIR/git-new-tag"
	printf '%s\n' "$5" > "$TEST_STATE_DIR/git-branch"
	printf 'old\n' > "$TEST_STATE_DIR/git-stage"
	printf '%040d\n' 1 > "$TEST_STATE_DIR/git-old-commit"
	printf '%040d\n' 2 > "$TEST_STATE_DIR/git-new-commit"
	printf '%040d\n' 3 > "$TEST_STATE_DIR/git-release-commit"
}

run_builder_ok() {
	atf_check -s exit:0 -o ignore -e ignore \
		env PATH="$PATH" HOME="$HOME" TMPDIR="$TMPDIR" \
			MOCK_STATE_DIR="$MOCK_STATE_DIR" \
			APORTS_BUILD_CONF="$APORTS_BUILD_CONF" \
			APORTS_BUILD_LOCKFILE="$APORTS_BUILD_LOCKFILE" \
			sh "$(atf_get_srcdir)/../aports-build" "$@"
}

run_builder_fail() {
	atf_check -s exit:1 -o ignore -e ignore \
		env PATH="$PATH" HOME="$HOME" TMPDIR="$TMPDIR" \
			MOCK_STATE_DIR="$MOCK_STATE_DIR" \
			APORTS_BUILD_CONF="$APORTS_BUILD_CONF" \
			APORTS_BUILD_LOCKFILE="$APORTS_BUILD_LOCKFILE" \
			sh "$(atf_get_srcdir)/../aports-build" "$@"
}

assert_contains() {
	local file="$1"
	local needle="$2"
	grep -F -- "$needle" "$file" >/dev/null 2>&1 || \
		atf_fail "missing '$needle' in $file"
}

assert_not_exists() {
	[ ! -e "$1" ] || atf_fail "$1 should not exist"
}

architecture_detection_failure_body() {
	for result in empty failed failed-with-output; do
		setup_builder_env
		: > "$TEST_STATE_DIR/abuild.arch"
		if [ "$result" != empty ]; then
			printf '42\n' > "$TEST_STATE_DIR/abuild.rc"
		fi
		if [ "$result" = failed-with-output ]; then
			printf 'x86_64\n' > "$TEST_STATE_DIR/abuild.arch"
		fi
		atf_check -s exit:1 -o empty \
			-e inline:'aports-build: could not determine architecture\n' \
			sh "$(atf_get_srcdir)/../aports-build"
		atf_check -s exit:0 -o match:'^[0-9].*: could not determine arch$' \
			cat "$TMPDIR/aports-build.log"
		assert_not_exists "$TEST_STATE_DIR/git.log"
		assert_not_exists "$TEST_STATE_DIR/buildrepo.args"
		assert_not_exists "$TEST_STATE_DIR/rsync.log"
		assert_not_exists "$TEST_STATE_DIR/mqtt.log"
		assert_not_exists "$APORTS_BUILD_LOCKFILE"
		atf_check -s exit:0 flock -n "$APORTS_BUILD_LOCKFILE.lock" true
		rm -f "$TEST_STATE_DIR/abuild.rc" "$TMPDIR/aports-build.log"
	done
}

active_lock_skips_builder_body() {
	setup_builder_env
	exec 9>"$APORTS_BUILD_LOCKFILE.lock"
	atf_check -s exit:0 flock -n 9
	printf '%s\n' "$$" > "$APORTS_BUILD_LOCKFILE"
	# A live lock must prevent even loading the configured builder environment.
	printf 'exit 42\n' > "$APORTS_BUILD_CONF"

	run_builder_ok

	atf_check -s exit:0 -o inline:"$$\n" cat "$APORTS_BUILD_LOCKFILE"
	assert_not_exists "$TEST_STATE_DIR/buildrepo.args"
	assert_not_exists "$TEST_STATE_DIR/mqtt.log"
}

simultaneous_builders_only_build_once_body() {
	setup_builder_env
	: > "$TEST_STATE_DIR/buildrepo.pause"

	sh "$(atf_get_srcdir)/../aports-build" >"$TEST_STATE_DIR/first.out" 2>&1 &
	first_pid=$!
	sh "$(atf_get_srcdir)/../aports-build" >"$TEST_STATE_DIR/second.out" 2>&1 &
	second_pid=$!
	wait "$first_pid" || atf_fail "first builder failed"
	wait "$second_pid" || atf_fail "second builder failed"

	atf_check -s exit:0 -o inline:"1\n" sh -c 'wc -l < "$1"' sh "$TEST_STATE_DIR/buildrepo.args"
	assert_not_exists "$APORTS_BUILD_LOCKFILE"
	atf_check -s exit:0 flock -n "$APORTS_BUILD_LOCKFILE.lock" true
}

wait_for_build_start() {
	for i in $(seq 1 100); do
		[ -f "$TEST_STATE_DIR/buildrepo.started" ] && break
		sleep 0.05
	done
	if [ ! -f "$TEST_STATE_DIR/buildrepo.started" ]; then
		kill -TERM "$builder_pid" 2>/dev/null
		wait "$builder_pid"
		atf_fail "builder did not start"
	fi
}

termination_releases_lock_body() {
	setup_builder_env
	: > "$TEST_STATE_DIR/buildrepo.pause"
	sh "$(atf_get_srcdir)/../aports-build" >"$TEST_STATE_DIR/builder.out" 2>&1 &
	builder_pid=$!
	wait_for_build_start

	kill -TERM "$builder_pid"
	wait "$builder_pid"
	rc=$?
	[ "$rc" -eq 143 ] || atf_fail "expected TERM exit 143, got $rc"

	assert_not_exists "$APORTS_BUILD_LOCKFILE"
	atf_check -s exit:0 flock -n "$APORTS_BUILD_LOCKFILE.lock" true
	atf_check -s exit:1 -o empty grep -F 'build/builder1|idle|1' "$TEST_STATE_DIR/mqtt.log"
}

killed_builder_keeps_lock_until_children_exit_body() {
	setup_builder_env
	: > "$TEST_STATE_DIR/buildrepo.pause"
	sh "$(atf_get_srcdir)/../aports-build" >"$TEST_STATE_DIR/builder.out" 2>&1 &
	builder_pid=$!
	wait_for_build_start

	kill -KILL "$builder_pid"
	wait "$builder_pid"
	# The still-running build command must exclude a replacement orchestrator.
	run_builder_ok
	atf_check -s exit:0 -o inline:"1\n" sh -c 'wc -l < "$1"' sh "$TEST_STATE_DIR/buildrepo.args"

	for i in $(seq 1 100); do
		flock -n "$APORTS_BUILD_LOCKFILE.lock" true && break
		sleep 0.05
	done
	atf_check -s exit:0 flock -n "$APORTS_BUILD_LOCKFILE.lock" true
	run_builder_ok
	atf_check -s exit:0 -o inline:"2\n" sh -c 'wc -l < "$1"' sh "$TEST_STATE_DIR/buildrepo.args"
	assert_not_exists "$APORTS_BUILD_LOCKFILE"
}

config_failure_releases_lock_body() {
	setup_builder_env
	printf 'exit 42\n' > "$APORTS_BUILD_CONF"
	atf_check -s exit:42 -o ignore -e ignore sh "$(atf_get_srcdir)/../aports-build"

	assert_not_exists "$APORTS_BUILD_LOCKFILE"
	atf_check -s exit:0 flock -n "$APORTS_BUILD_LOCKFILE.lock" true
	write_config master
	run_builder_ok
}

stale_pid_does_not_block_builder_body() {
	setup_builder_env master v1.0-1-gsame v1.0 v1.0-1-gsame v1.0
	# Even a reused, live PID is not a lock when nobody holds the flock file.
	printf '%s\n' "$$" > "$APORTS_BUILD_LOCKFILE"
	: > "$TMPDIR/uploaded"
	run_builder_ok

	assert_contains "$TEST_STATE_DIR/mqtt.log" "build/builder1|idle|1"
	assert_not_exists "$APORTS_BUILD_LOCKFILE"
}

noop_when_uploaded_marker_exists_body() {
	setup_builder_env master v1.0-1-gsame v1.0 v1.0-1-gsame v1.0
	: > "$TMPDIR/uploaded"

	run_builder_ok

	assert_not_exists "$TEST_STATE_DIR/buildrepo.args"
	assert_contains "$TEST_STATE_DIR/mqtt.log" "build/builder1|pulling git|1"
	assert_contains "$TEST_STATE_DIR/mqtt.log" "build/builder1|idle|1"
	[ -f "$TMPDIR/uploaded" ] || atf_fail "existing upload marker was removed"
	assert_not_exists "$APORTS_BUILD_LOCKFILE"
}

assert_git_failure() {
	assert_not_exists "$TEST_STATE_DIR/buildrepo.args"
	assert_not_exists "$TEST_STATE_DIR/rsync.log"
	assert_not_exists "$TEST_STATE_DIR/mkimage.log"
	assert_not_exists "$TEST_STATE_DIR/abuild-apk.log"
	assert_not_exists "$TMPDIR/uploaded"
	atf_check -s exit:0 -o inline:'previous run\n' cat "$TEST_LOG_DIR/builder1.log"
	assert_not_exists "$APORTS_BUILD_LOCKFILE"
	atf_check -s exit:0 flock -n "$APORTS_BUILD_LOCKFILE.lock" true
}

run_git_failure() {
	local operation="$1"
	printf 'previous run\n' > "$TEST_LOG_DIR/builder1.log"
	printf '%s\n' "${2:-42}" > "$TEST_STATE_DIR/git.$operation.rc"
	run_builder_fail
	assert_git_failure
	rm -f "$TEST_STATE_DIR/git.$operation.rc"
}

git_branch_checkout_failure_body() {
	setup_builder_env
	run_git_failure checkout-branch
	atf_check -s exit:1 -o empty grep -x pull "$TEST_STATE_DIR/git.log"
}

git_pull_failure_body() {
	setup_builder_env
	run_git_failure pull
	assert_contains "$TEST_STATE_DIR/git.log" 'checkout master'
}

git_revision_detection_failures_body() {
	for operation in describe-old tag-old describe-new tag-new rev-parse; do
		setup_builder_env
		run_git_failure "$operation"
	done
}

git_release_checkout_failure_body() {
	setup_builder_env master v1.0-0-gold v1.0 v1.1-1-gnew v1.1
	run_git_failure checkout-tag
	assert_contains "$TEST_STATE_DIR/git.log" 'checkout v1.1'
}

git_release_revision_failure_body() {
	setup_builder_env master v1.0-0-gold v1.0 v1.1-1-gnew v1.1
	run_git_failure describe-release
}

git_empty_revision_failure_body() {
	for operation in describe-old tag-old describe-new tag-new rev-parse; do
		setup_builder_env
		# Success without a revision must not let an empty value drive the build.
		run_git_failure "$operation" 0
	done
}

release_build_records_tag_commit_body() {
	setup_builder_env master v1.0-0-gold v1.0 v1.1-1-gnew v1.1
	run_builder_ok
	atf_check -s exit:0 -o inline:"source commit: $(cat "$TEST_STATE_DIR/git-release-commit")\n" \
		grep '^source commit:' "$TEST_STATE_DIR/mkimage.build.log"
	assert_contains "$TEST_STATE_DIR/mqtt.log" 'build/builder1|building v1.1|1'
}

build_progress_and_package_upload_notification_body() {
	setup_builder_env master v1.0-0-gold v1.0 v1.0-1-gnew v1.0
	printf '1/2 3/4 main/pkgfoo 1.0-r0\n' > "$TEST_STATE_DIR/buildrepo.stdout"
	printf '>f+++++++++ pkgfoo-1.0-r0.apk\n' > "$TEST_STATE_DIR/rsync.packages.stdout"
	: > "$TEST_STATE_DIR/buildrepo.wait_for_progress"

	run_builder_ok

	atf_check -s exit:0 -o inline:"-p\n-a\n$TEST_APORTS_DIR\n-d\n$TEST_PACKAGES_DIR\nmain\n" \
		cat "$TEST_STATE_DIR/buildrepo.argv"
	assert_contains "$TEST_STATE_DIR/mqtt.log" "build/builder1|1/2 3/4 main/pkgfoo 1.0-r0|1"
	assert_contains "$TEST_STATE_DIR/mqtt.log" "build/builder1|uploading packages to main|1"
	assert_contains "$TEST_STATE_DIR/mqtt.log" "rsync/dl-master.alpinelinux.org/edge/x86_64|edge/main/x86_64|0"
	assert_contains "$TMPDIR/upload-main" ">f+++++++++ pkgfoo-1.0-r0.apk"
	[ -f "$TMPDIR/uploaded" ] || atf_fail "missing isolated upload marker"
	assert_not_exists "$APORTS_BUILD_LOCKFILE"
	atf_check -s exit:0 -o inline:"source commit: $(cat "$TEST_STATE_DIR/git-new-commit")\n" \
		grep '^source commit:' "$TEST_LOG_DIR/builder1.log"
	[ -f "$TEST_STATE_DIR/buildrepo.progress_seen" ] || atf_fail "progress was not published during the build"
}

configured_paths_override_buildrepo_paths_body() {
	setup_builder_env
	printf 'buildrepo="buildrepo -p -a /unused/aports -d /unused/packages"\n' >> "$APORTS_BUILD_CONF"
	: > "$TEST_STATE_DIR/check-paths"
	run_builder_ok
	atf_check -s exit:0 -o inline:"-p\n-a\n/unused/aports\n-d\n/unused/packages\n-a\n$TEST_APORTS_DIR\n-d\n$TEST_PACKAGES_DIR\nmain\n" \
		cat "$TEST_STATE_DIR/buildrepo.argv"
	[ -f "$TEST_PACKAGES_DIR/main/x86_64/pkgfoo-1.0-r0.apk" ] || atf_fail 'package was not built in the upload tree'
	assert_contains "$TEST_STATE_DIR/mqtt.log" 'build/builder1|idle|1'
}

relative_paths_stay_consistent_body() {
	setup_builder_env
	printf 'APORTS="testroot/aports checkout"\nREPOSDIR="testroot/package output"\n' >> "$APORTS_BUILD_CONF"
	: > "$TEST_STATE_DIR/check-paths"
	run_builder_ok
	atf_check -s exit:0 -o inline:"-p\n-a\n$TEST_APORTS_DIR\n-d\n$TEST_PACKAGES_DIR\nmain\n" \
		cat "$TEST_STATE_DIR/buildrepo.argv"
	[ -f "$TEST_PACKAGES_DIR/main/x86_64/pkgfoo-1.0-r0.apk" ] || atf_fail 'relative output path changed with the working directory'
}

default_paths_passed_to_buildrepo_body() {
	setup_builder_env
	TEST_APORTS_DIR="$TEST_HOME_DIR/aports"
	TEST_PACKAGES_DIR="$TEST_HOME_DIR/packages"
	mkdir -p "$TEST_APORTS_DIR" "$TEST_PACKAGES_DIR"
	write_config master
	printf 'unset APORTS REPOSDIR\n' >> "$APORTS_BUILD_CONF"
	: > "$TEST_STATE_DIR/check-paths"
	run_builder_ok
	atf_check -s exit:0 -o inline:"-p\n-a\n$TEST_APORTS_DIR\n-d\n$TEST_PACKAGES_DIR\nmain\n" \
		cat "$TEST_STATE_DIR/buildrepo.argv"
}

edge_rc_tag_does_not_create_release_body() {
	setup_builder_env master v1.0-0-gold v1.0 v1.1_rc1-0-gnew v1.1_rc1

	run_builder_ok

	assert_not_exists "$TEST_STATE_DIR/mkimage.log"
}

force_release_on_edge_uses_edge_name_and_profile_body() {
	setup_builder_env master v1.0 v1.0 v1.0 v1.0
	printf 'sent release\n' > "$TEST_STATE_DIR/rsync.release.stdout"

	run_builder_ok -F

	assert_contains "$TEST_STATE_DIR/mkimage.log" "--profile minirootfs netboot"
	atf_check -s exit:0 -o inline:"--repository\n$TEST_PACKAGES_DIR/main\n--yaml\n--tag\n1.0\n--outdir\n$TEST_PACKAGES_DIR/releases/x86_64\n--profile\nminirootfs netboot\n" \
		cat "$TEST_STATE_DIR/mkimage.argv"
	assert_contains "$TEST_STATE_DIR/rsync.log" "release:--archive --update --verbose --mkpath $TEST_PACKAGES_DIR/releases/x86_64/release-artifact.iso dl-master.alpinelinux.org:alpine/edge/releases/x86_64/"
	assert_contains "$TEST_STATE_DIR/mqtt.log" "rsync/dl-master.alpinelinux.org/edge/x86_64|edge/releases/x86_64|0"
}

stable_branch_release_uses_versioned_name_body() {
	setup_builder_env 3.22-stable v3.22.0 v3.22.0 v3.22.0 v3.22.0
	printf 'sent release\n' > "$TEST_STATE_DIR/rsync.release.stdout"

	run_builder_ok -F

	assert_contains "$TEST_STATE_DIR/rsync.log" "dl-master.alpinelinux.org:alpine/v3.22/releases/x86_64/"
}

setup_release_retry() {
	setup_builder_env master v1.0 v1.0 v1.1 v1.1
	cp "$TEST_STATE_DIR/git-release-commit" "$TEST_STATE_DIR/git-new-commit"
	RELEASE_STATE="$TEST_PACKAGES_DIR/releases/x86_64/.pending-edge"
}

assert_release_failure() {
	assert_contains "$TEST_STATE_DIR/mqtt.log" 'build/builder1|failed|1'
	atf_check -s exit:1 -o empty grep -F 'build/builder1|idle|1' "$TEST_STATE_DIR/mqtt.log"
	atf_check -s exit:1 -o empty grep -F 'rsync/dl-master.alpinelinux.org/edge/x86_64|edge/releases/x86_64|0' "$TEST_STATE_DIR/mqtt.log"
	atf_check -s exit:0 -o inline:"v1.1\n$(cat "$TEST_STATE_DIR/git-release-commit")\nrelease\n" cat "$RELEASE_STATE"
	[ -f "$TMPDIR/uploaded" ] || atf_fail 'completed package uploads were not recorded'
	assert_not_exists "$APORTS_BUILD_LOCKFILE"
	atf_check -s exit:0 flock -n "$APORTS_BUILD_LOCKFILE.lock" true
}

run_release_failure_retry() {
	local failure="$1"
	setup_release_retry
	printf '42\n' > "$TEST_STATE_DIR/$failure.rc"
	run_builder_fail
	assert_release_failure
	assert_contains "$TEST_STATE_DIR/abuild-apk.log" 'del .alpine-release-deps'
	[ "$failure" = release-del ] || assert_not_exists "$TEST_STATE_DIR/release-deps-installed"
	case "$failure" in
	release-add) assert_not_exists "$TEST_STATE_DIR/mkimage.log";;
	*) assert_contains "$TEST_STATE_DIR/mkimage.log" '--tag 1.1';;
	esac
	if [ "$failure" = release-add ] || [ "$failure" = mkimage ]; then
		atf_check -s exit:1 -o empty grep '^release:' "$TEST_STATE_DIR/rsync.log"
	fi
	rm -f "$TEST_STATE_DIR/$failure.rc"
	if [ "$2" = without-marker ]; then
		rm -f "$TMPDIR/uploaded"
	fi
	run_builder_ok
	assert_not_exists "$RELEASE_STATE"
	assert_not_exists "$TEST_STATE_DIR/release-deps-installed"
	assert_contains "$TEST_STATE_DIR/mqtt.log" 'rsync/dl-master.alpinelinux.org/edge/x86_64|edge/releases/x86_64|0'
	assert_contains "$TEST_STATE_DIR/mqtt.log" 'build/builder1|idle|1'
	assert_contains "$TEST_STATE_DIR/git.log" "checkout $(cat "$TEST_STATE_DIR/git-release-commit")"
	atf_check -s exit:0 -o inline:'1\n' sh -c 'wc -l < "$1"' sh "$TEST_STATE_DIR/buildrepo.args"
	atf_check -s exit:0 -o inline:'1\n' grep -c '^packages:' "$TEST_STATE_DIR/rsync.log"
}

release_dependency_failure_retries_body() { run_release_failure_retry release-add; }
release_image_failure_retries_body() { run_release_failure_retry mkimage; }
release_upload_failure_retries_body() { run_release_failure_retry rsync.release; }
release_cleanup_failure_retries_body() { run_release_failure_retry release-del; }
release_retry_without_upload_marker_body() { run_release_failure_retry rsync.release without-marker; }

release_waits_for_package_upload_body() {
	setup_release_retry
	: > "$TEST_STATE_DIR/rsync.packages.fail-once"
	run_builder_ok
	assert_not_exists "$RELEASE_STATE"
	atf_check -s exit:0 -o inline:'2\n' grep -c '^packages:' "$TEST_STATE_DIR/rsync.log"
	atf_check -s exit:0 -o inline:'1\n' grep -c '^release:' "$TEST_STATE_DIR/rsync.log"
	atf_check -s exit:0 -o inline:'1\n' sh -c 'wc -l < "$1"' sh "$TEST_STATE_DIR/mkimage.log"
}

invalid_pending_release_stops_builder_body() {
	setup_release_retry
	mkdir -p "${RELEASE_STATE%/*}"
	printf 'v1.1\nnot-a-commit\nrelease\n' > "$RELEASE_STATE"
	run_builder_fail
	assert_not_exists "$TEST_STATE_DIR/buildrepo.args"
	assert_not_exists "$TEST_STATE_DIR/git.log"
	assert_not_exists "$TEST_STATE_DIR/rsync.log"
	assert_not_exists "$APORTS_BUILD_LOCKFILE"
	atf_check -s exit:0 flock -n "$APORTS_BUILD_LOCKFILE.lock" true
}

release_package_failure_retries_body() {
	setup_release_retry
	printf '42\n' > "$TEST_STATE_DIR/buildrepo.rc"
	run_builder_fail
	assert_contains "$RELEASE_STATE" 'build'
	assert_not_exists "$TEST_STATE_DIR/mkimage.log"
	rm -f "$TEST_STATE_DIR/buildrepo.rc"
	run_builder_ok
	assert_not_exists "$RELEASE_STATE"
	assert_contains "$TEST_STATE_DIR/mqtt.log" 'rsync/dl-master.alpinelinux.org/edge/x86_64|edge/releases/x86_64|0'
	atf_check -s exit:0 -o inline:'2\n' sh -c 'wc -l < "$1"' sh "$TEST_STATE_DIR/buildrepo.args"
}

network_disabled_keeps_pending_release_body() {
	setup_release_retry
	printf '42\n' > "$TEST_STATE_DIR/mkimage.rc"
	run_builder_fail
	rm -f "$TEST_STATE_DIR/mkimage.rc"
	cp "$RELEASE_STATE" "$TEST_STATE_DIR/pending-before"
	run_builder_ok -n
	atf_check -s exit:0 cmp "$TEST_STATE_DIR/pending-before" "$RELEASE_STATE"
	atf_check -s exit:1 -o empty grep '^release:' "$TEST_STATE_DIR/rsync.log"
}

release_termination_retries_body() {
	setup_release_retry
	: > "$TEST_STATE_DIR/mkimage.pause"
	sh "$(atf_get_srcdir)/../aports-build" >"$TEST_STATE_DIR/builder.out" 2>&1 &
	builder_pid=$!
	for i in $(seq 1 100); do
		[ -f "$TEST_STATE_DIR/mkimage.started" ] && break
		sleep 0.05
	done
	if [ ! -f "$TEST_STATE_DIR/mkimage.started" ]; then
		kill -TERM "$builder_pid" 2>/dev/null
		wait "$builder_pid"
		atf_fail 'release did not start'
	fi
	kill -TERM "$builder_pid"
	wait "$builder_pid"
	rc=$?
	[ "$rc" -eq 143 ] || atf_fail "expected TERM exit 143, got $rc"
	assert_not_exists "$APORTS_BUILD_LOCKFILE"
	assert_not_exists "$TEST_STATE_DIR/release-deps-installed"
	[ -f "$RELEASE_STATE" ] || atf_fail 'pending release was lost on termination'
	atf_check -s exit:1 -o empty grep -F 'edge/releases/x86_64|0' "$TEST_STATE_DIR/mqtt.log"
	rm -f "$TEST_STATE_DIR/mkimage.pause"
	run_builder_ok
	assert_not_exists "$RELEASE_STATE"
	atf_check -s exit:0 -o inline:'1\n' sh -c 'wc -l < "$1"' sh "$TEST_STATE_DIR/buildrepo.args"
}

assert_build_failure() {
	assert_contains "$TEST_STATE_DIR/mqtt.log" "build/builder1|failed|1"
	atf_check -s exit:1 -o empty grep -F 'build/builder1|idle|1' "$TEST_STATE_DIR/mqtt.log"
	assert_not_exists "$TEST_STATE_DIR/rsync.log"
	assert_not_exists "$TMPDIR/uploaded"
	atf_check -s exit:0 cmp "$TEST_LOG_DIR/builder1.log" "$TEST_LOG_DIR/builder1.v1.0-1-gnew.log"
	assert_not_exists "$APORTS_BUILD_LOCKFILE"
	atf_check -s exit:0 flock -n "$APORTS_BUILD_LOCKFILE.lock" true
	atf_check -s exit:0 -o empty find "$TMPDIR" -name 'aports-build.*'
}

buildrepo_success_with_fail_output_body() {
	setup_builder_env
	printf 'FAIL\n  literal \\output\nlast line' > "$TEST_STATE_DIR/buildrepo.stdout"
	run_builder_ok

	assert_contains "$TEST_LOG_DIR/builder1.log" 'FAIL'
	assert_contains "$TEST_LOG_DIR/builder1.log" '  literal \output'
	assert_contains "$TEST_LOG_DIR/builder1.log" 'last line'
	assert_contains "$TEST_STATE_DIR/mqtt.log" "build/builder1|idle|1"
	atf_check -s exit:1 -o empty grep -F 'build/builder1|failed|1' "$TEST_STATE_DIR/mqtt.log"
	atf_check -s exit:0 -o empty find "$TMPDIR" -name 'aports-build.*'
}

buildrepo_failure_without_output_body() {
	setup_builder_env
	printf '42\n' > "$TEST_STATE_DIR/buildrepo.rc"
	run_builder_fail
	assert_build_failure
}

buildrepo_progress_then_failure_body() {
	setup_builder_env
	printf '1/2 3/4 main/pkgfoo 1.0-r0\nfailure details' > "$TEST_STATE_DIR/buildrepo.stdout"
	printf '42\n' > "$TEST_STATE_DIR/buildrepo.rc"
	: > "$TEST_STATE_DIR/buildrepo.wait_for_progress"
	run_builder_fail

	assert_build_failure
	assert_contains "$TEST_LOG_DIR/builder1.v1.0-1-gnew.log" 'failure details'
	[ -f "$TEST_STATE_DIR/buildrepo.progress_seen" ] || atf_fail "progress was not published during the build"
}

buildrepo_failure_reports_failed_and_preserves_log_body() {
	setup_builder_env master v1.0-0-gold v1.0 v1.0-1-gnew v1.0
	printf 'broken output\n' > "$TEST_STATE_DIR/buildrepo.stdout"
	printf '1\n' > "$TEST_STATE_DIR/buildrepo.rc"

	run_builder_fail

	assert_build_failure
	assert_contains "$TEST_LOG_DIR/builder1.v1.0-1-gnew.log" 'broken output'
}
