#!/bin/sh

# This script contains CI tests for FreeBSD, testing
#
# - cargo build & cargo test
# - configure and start sccache-dist and scheduler
# - test distributed compile
# - test that the cache is used
#
# It creates a temporary test pool backed by a
# file (using mdconfig) and does a full configuration
# of pot.
#
# After running it copies the sccache log file into
# the repo's root directory. It also does a full
# cleanup (removal of all temporary files, test pool
# etc.) after each run. This can be prevented by
# setting FREEBSD_CI_NOCLEAN in the environment:
#
#     FREEBSD_CI_NOCLEAN=1 scripts/freebsd-ci-test.sh
#
# When running in a loop, time and bandwidth can be
# saved by placing FreeBSD distribution files in
# $HOME/.potcache
#
#     mkdir $HOME/.potcache
#     fetch -o $HOME/.potcache/15.0-RELEASE_base.txz \
#     https://ftp.freebsd.org/pub/FreeBSD/releases/amd64/15.0-RELEASE/base.txz
#
# This script can be run from a github action. When run locally, make
# sure to install the required packages:
#
#     pkg install -y ca-root-nss curl gmake gtar pot sudo
#

# shellcheck disable=SC3040
set -eo pipefail

init()
{
	base=$(realpath "$(dirname "$0")"/..)
	OS_VERSION="$(freebsd-version | awk -F- '{print $1}')"
	PUB_INTF="$(netstat -4rn | grep default | awk '{ print $4}')"
	TEST_TMPDIR=$(mktemp -d "/tmp/sccache_freebsd.XXXXXXX") || exit 1
	chmod g+r "$TEST_TMPDIR"
	export XDG_CONFIG_HOME="$TEST_TMPDIR/.config"
	mkdir -p "$XDG_CONFIG_HOME"
	export SCCACHE_DIR="$TEST_TMPDIR/.cache"
	killall sccache 2>/dev/null || true
	killall sccache-dist 2>/dev/null || true
	export RUST_LOG_STYLE=never
}

output_env_info()
{
	echo "## user"
	whoami
	echo "## environment"
	env | sort
	echo "## network"
	ifconfig
	echo "## tooling info"
	cargo -V
	rustc -V
	curl --version
	# See https://github.com/bsdpot/pot/pull/253
	pot version || true
	gtar --version
	echo "## installed packages"
	pkg info
}

build_and_test_project()
{
	echo "#### building sccache (cargo)"
	cd "$base"
	FAULT=0
	export RUSTFLAGS="-C debuginfo=0"
	cargo build --features "dist-client,dist-server" || FAULT=1
	echo "#### testing sccache (cargo)"
	cargo test --features "dist-client,dist-server" -- \
	  --test-threads 1 || FAULT=1
	unset RUSTFLAGS
	if [ "$FAULT" -eq 0 ]; then
		# save build time by avoiding "cargo install"
		cp -a target/debug/sccache target/debug/sccache-dist \
		  "$HOME/.cargo/bin/."
	fi
	if [ $FAULT -ne 0 ]; then return 1; fi
}

prepare_and_run_sccache_dist()
{
	echo "#### preparing sccache-dist"
	SECRET_KEY="$(sccache-dist auth generate-jwt-hs256-key)"
	CLIENT_AUTH_KEY="$(sccache-dist auth generate-jwt-hs256-key)"
	# create scheduler.conf
	cat >"$TEST_TMPDIR"/scheduler.conf <<-EOF
	public_addr = "127.0.0.1:10600"
	[client_auth]
	type = "token"
	token = "$CLIENT_AUTH_KEY"
	[server_auth]
	type = "jwt_hs256"
	secret_key = "$SECRET_KEY"
	EOF
	SERVER_TOKEN="$(sccache-dist auth generate-jwt-hs256-server-token \
	  --config="$TEST_TMPDIR"/scheduler.conf \
	  --server="127.0.0.1:10501")"

	# Create server.conf
	cat >"$TEST_TMPDIR"/server.conf <<-EOF
	cache_dir = "$TEST_TMPDIR/toolchains"
	public_addr = "127.0.0.1:10501"
	scheduler_url = "http://127.0.0.1:10600"
	[builder]
	type = "pot"
	pot_fs_root = "$TEST_TMPDIR/pot"
	[scheduler_auth]
	type = "jwt_token"
	token = "$SERVER_TOKEN"
	EOF

	# create sccache client config
	TC="$(rustup toolchain list | grep default | awk '{ print $1 }')"
	RUSTC_PATH="$HOME/.rustup/toolchains/$TC/bin/rustc"
	mkdir -p "$XDG_CONFIG_HOME/sccache"
	cat >"$XDG_CONFIG_HOME/sccache/config" <<-EOF
	[dist]
	scheduler_url = "http://127.0.0.1:10600"
	toolchain_cache_size = 5368709120
	cache_dir = "$HOME/.cache/sccache-dist-client"
	[dist.auth]
	type = "token"
	token = "$CLIENT_AUTH_KEY"
	[[dist.toolchains]]
	type = "path_override"
	compiler_executable = "/usr/bin/cc"
	archive = "$TEST_TMPDIR/empty.tar.gz"
	archive_compiler_executable = "/usr/bin/cc"
	[[dist.toolchains]]
	type = "path_override"
	compiler_executable = "$RUSTC_PATH"
	archive = "$TEST_TMPDIR/rust-toolchain.tgz"
	archive_compiler_executable = "$RUSTC_PATH"
	EOF

	echo "Creating toolchain tarballs"
	gtar cvf - --files-from /dev/null | \
	  gzip -n >"$TEST_TMPDIR/empty.tar.gz"
	gtar cf - --sort=name --mtime='2022-06-28 17:35Z' "$HOME/.rustup"  | \
	  gzip -n >"$TEST_TMPDIR/rust-toolchain.tgz"

	echo "Starting scheduler"
	sccache-dist scheduler --config "$TEST_TMPDIR"/scheduler.conf
}

prepare_zpool()
{
	echo "#### preparing zpool"
	sudo dd if=/dev/zero of="$TEST_TMPDIR/zfs1" bs=1 count=1 seek=3G
	MDUNIT=$(sudo mdconfig -a -n -t vnode -S 4096 -f "$TEST_TMPDIR/zfs1")
	zdev="/dev/md$MDUNIT"
	sudo zpool create -f potpool "$zdev"
}

prepare_pot()
{
	echo "#### preparing pot"
	sudo sysrc -f /usr/local/etc/pot/pot.conf POT_ZFS_ROOT=potpool/pot
	sudo sysrc -f /usr/local/etc/pot/pot.conf POT_EXTIF="$PUB_INTF"
	sudo sysrc -f /usr/local/etc/pot/pot.conf POT_TMP="$TEST_TMPDIR"
	sudo sysrc -f /usr/local/etc/pot/pot.conf \
	  POT_FS_ROOT="$TEST_TMPDIR/pot"
	sudo sysrc -f /usr/local/etc/pot/pot.conf POT_GROUP=wheel
	sudo pot init -f ""
	sudo pot version
	sudo cp "$HOME"/.potcache/*.txz /var/cache/pot 2>/dev/null || true
	sudo pot create -p sccache-template -N alias -i "lo0|127.0.0.2" \
	  -t single -b "$OS_VERSION"
	sudo pot set-cmd -p sccache-template -c /usr/bin/true
	sudo pot set-attr -p sccache-template -A no-rc-script -V YES
	sudo pot snapshot -p sccache-template
}

start_build_server()
{
	echo "#### starting build-server (as root)"
	SCCACHE_DIST_LOG=debug RUST_LOG=info sudo \
	  "$HOME"/.cargo/bin/sccache-dist server \
	  --config "$TEST_TMPDIR"/server.conf &
}

wait_for_build_server()
{
	echo "#### waiting for build server to become available"
	count=0
	while [ "$(sockstat -q4l -p 10501 | wc -l | xargs)" -eq "0" ]; do
		count=$(( count + 1 ))
		if [ $count -gt 60 ]; then
			2>&1 echo "Build server did not become available"
			return 1
		fi
		sleep 5
	done
}

create_build_test_project()
{
	echo "#### create and build test project"
	cd "$TEST_TMPDIR"
	cargo init buildtest
	cd buildtest
	echo 'chrono = "0.4"' >>Cargo.toml
}

start_sccache_server()
{
	echo "#### starting sccache-server"
	killall sccache 2>/dev/null || true
	SCCACHE_ERROR_LOG="$TEST_TMPDIR"/sccache_log.txt SCCACHE_LOG=info \
	  RUST_LOG=info sccache --start-server
	sleep 10
}

test_sccache_dist_01()
{
	echo "#### running scache_dist test 01"
	cd "$TEST_TMPDIR/buildtest"
	RUSTC_WRAPPER=sccache cargo build
	STATS="$(sccache -s)"
	echo "Statistics of first buildtest"
	echo "$STATS"
	CACHE_HITS="$(echo "$STATS" | \
	  grep "Cache hits" | grep -v Rust | \awk '{ print $3 }')"
	FAILED_DIST="$(echo "$STATS" | \
	  grep "Failed distributed compilations" | awk '{ print $4 }')"
	SUCCEEDED_DIST="$(echo "$STATS" | \
	  (grep -F "127.0.0.1:10501" || echo 0 0) | awk '{ print $2 }')"

	if [ "$CACHE_HITS" -ne 0 ]; then
		2>&1 echo "Unexpected cache hits"
		return 1
	fi
	# We sometimes get "connection closed before message completed"
	# on the first remote build (which will make sccache fall-back
	# to building locally). Until this has been resolved, accept
	# one failed remote build.
	if [ "$FAILED_DIST" -gt 1 ]; then
		2>&1 echo "More than one distributed compilations failed"
		cat "$TEST_TMPDIR"/sccache_log.txt
		return 1
	fi
	if [ "$SUCCEEDED_DIST" -eq 0 ]; then
		2>&1 echo "No distributed compilations succeeded"
		return 1
	fi
}

test_sccache_dist_02()
{
	echo "#### running scache_dist test 02"
	cd "$TEST_TMPDIR/buildtest"
	sccache -z
	cargo clean
	RUSTC_WRAPPER=sccache cargo build
	STATS="$(sccache -s)"
	echo "Statistics of second buildtest"
	echo "$STATS"
	CACHE_HITS="$(echo "$STATS" | \
	  grep "Cache hits" | grep -v Rust | \awk '{ print $3 }')"
	FAILED_DIST="$(echo "$STATS" | \
	  grep "Failed distributed compilations" | awk '{ print $4 }')"
	SUCCEEDED_DIST="$(echo "$STATS" | \
	  (grep -F "127.0.0.1:10501" || echo 0 0) | awk '{ print $2 }')"

	if [ "$CACHE_HITS" -eq 0 ]; then
		2>&1 echo "No cache hits when there should be some"
		return 1
	fi
	# We sometimes get "connection closed before message completed"
	# on the first remote build (which will make sccache fall-back
	# to building locally). Until this has been resolved, accept
	# one failed remote build.
	if [ "$FAILED_DIST" -gt 1 ]; then
		2>&1 echo "More than one distributed compilations failed"
		return 1
	fi
	if [ "$SUCCEEDED_DIST" -ne 0 ]; then
		2>&1 echo "Unexpected distributed compilations happened"
		return 1
	fi
}

cleanup()
{
	echo "#### cleaning up"
	set +e
	sccache --stop-server
	killall sccache
	killall sccache-dist && sleep 3
	sudo killall sccache-dist && sleep 3
	sudo killall -9 sccache-dist
	killall sccache
	cp "$TEST_TMPDIR/sccache_log.txt" "$base/sccache_log_$(date +%s).txt"
	if [ -z "$FREEBSD_CI_NOCLEAN" ]; then
		for name in $(pot ls -q); do
			sudo pot stop -p "$name"
		done
		sudo pot de-init
		sudo zpool destroy -f potpool
		if [ -n "$MDUNIT" ]; then
			sudo mdconfig -d -u "$MDUNIT"
		fi
		sudo rm -rf "$TEST_TMPDIR"
	fi
	set -e
}

install_signal_handler()
{
	trap 'remove_signal_handler; cleanup; exit' EXIT INT HUP
}

remove_signal_handler()
{
	trap - EXIT INT HUP
}

main()
{
	install_signal_handler
	init
	output_env_info
	build_and_test_project
	prepare_and_run_sccache_dist
	prepare_zpool
	prepare_pot
	start_build_server
	wait_for_build_server
	create_build_test_project
	start_sccache_server
	test_sccache_dist_01
	test_sccache_dist_02
	remove_signal_handler
	cleanup
}

# run main function
main
