diff --git a/overlay/usr/local/sbin/tkldev-setup b/overlay/usr/local/sbin/tkldev-setup index d824c06..92bcd15 100755 --- a/overlay/usr/local/sbin/tkldev-setup +++ b/overlay/usr/local/sbin/tkldev-setup @@ -13,14 +13,16 @@ $(basename "$0"): [-h|--help] [APP] [... APP] Tool to initialize and maintain TKLDev. Relevant git repos are downloaded/updated (see below), bootstrap is downloaded -if needed and the latest 'fab' package will be installed when relevant. +if needed (or built locally if not available for download) and the latest +'fab' package will be installed when relevant. Options: APP [APP ...] Appliance name/s - defaults if unset: 'core' -f|--force Attempt checkout of default branch (see below) if alternate branch checked out; dirty repos still skipped -n|--full-clone Full clone (rather than default shallow clone)* - -s|--strict Fail rather than skip non-fatal steps + -s|--strict Fail rather than skip non-fatal steps (including a missing + bootstrap download, rather than building it locally) -t|--transition Transition build - no fail on missing bootstrap download** -h|--help Show this help and exit @@ -33,7 +35,7 @@ branches will be skipped. Default git repos, branches and local paths are: remote repo branch local path ------------------------------------------------------ turnkeylinux/buildtasks .x $BT_PATH -turnkeylinux/tklbam-profiles master /turnkey/tklbam-profiles +turnkeylinux/tklbam-profiles master $TKLBAM_PATH turnkeylinux/cdroots .x $FAB_PATH/cdroots (amd64 only) turnkeylinux/common .x $FAB_PATH/common turnkeylinux-apps/APP master $FAB_PATH/products/APP @@ -43,6 +45,7 @@ Env vars: FAB_PATH Fab PATH (default: $FAB_PATH) FAB_ARCH Fab architecture (default: $FAB_ARCH) BT_PATH Buildtasks PATH (default: $BT_PATH) + TKLBAM_PATH tklbam-profiles PATH (default: $TKLBAM_PATH) RELEASE Release to target - e.g. debian/bookworm (default: $RELEASE) ARCH Arch to target (default: $ARCH) BRANCH Preferred series ref (default: $BRANCH) @@ -62,50 +65,6 @@ EOF exit $exit_code } -# check for BT_DEBUG as well as bt scripts call tkldev-setup -if [[ -n "$DEBUG" ]] || [[ -n "$BT_DEBUG" ]]; then - set -x -fi -[[ $(id -u) -eq 0 ]] || fatal "Root user required." - -GIT_DEPTH=${GIT_DEPTH:-1} - -distro="$(lsb_release -si)" -if [[ "${distro,,}" == "turnkey" ]] || [[ "${distro,,}" == "debian" ]]; then - distro="debian" -fi -tkl_ver=$(turnkey-version -t) -unset FORCE STRICT TRANSITION - -export RELEASE="${RELEASE:-${distro}/$(lsb_release -s -c)}" -CODENAME="$(basename "$RELEASE")" -export CODENAME -export FAB_ARCH="${FAB_ARCH:-$(dpkg --print-architecture)}" -export BRANCH="${tkl_ver::2}.x" -IMAGES="http://mirror.turnkeylinux.org/turnkeylinux/images" -FAB_PATH="${FAB_PATH:-/turnkey/fab}" -BOOTSTRAP_NAME="bootstrap-${CODENAME}-${FAB_ARCH}" -BOOTSTRAP_PATH="$FAB_PATH/bootstraps/${CODENAME}-${FAB_ARCH}" -BT_PATH="${BT_PATH:-/turnkey/buildtasks}" -BT_VERIFY="$BT_PATH/bin/signature-verify" - -read -ra BT_REFS <<< "${BT_REFS:-$BRANCH ${BRANCH}-dev master}" -read -ra TKLBAM_REFS <<< "${TKLBAM_REFS:-$BRANCH ${BRANCH}-dev master}" -read -ra COMMON_REFS <<< "${COMMON_REFS:-$BRANCH ${BRANCH}-dev master}" -read -ra APP_REFS <<< "${APP_REFS:-$BRANCH ${BRANCH}-dev master}" -read -ra CDROOTS_REFS <<< "${CDROOTS_REFS:-$BRANCH ${BRANCH}-dev master}" - -GIT_REMOTE_URL="${GIT_REMOTE_URL:-https://github.com/turnkeylinux}" -GIT_REMOTE_NAME="${GIT_REMOTE_NAME:-origin}" - -# remove trailing '/' if needed -GIT_REMOTE_URL="${GIT_REMOTE_URL%/}" -# Append '-apps' to git remote URL for apps if default org is 'turnkeylinux' -APP_GIT_REMOTE_URL="$GIT_REMOTE_URL" -if [[ "$GIT_REMOTE_URL" =~ [:/]turnkeylinux ]]; then - APP_GIT_REMOTE_URL="$GIT_REMOTE_URL-apps" -fi - tkl_remote() { local dir=$1 local git_remote @@ -244,6 +203,105 @@ clone_or_update() { fi } +build_bootstrap() { + local src + src="$(dirname "$FAB_PATH")/bootstrap" + clone_or_update bootstrap "$src" master + info "Building $BOOTSTRAP_NAME locally (from $src)" + make -C "$src" clean + make -C "$src" install \ + FAB_PATH="$FAB_PATH" RELEASE="$RELEASE" FAB_ARCH="$FAB_ARCH" + if [[ ! -f "$BOOTSTRAP_PATH/bin/bash" ]]; then + fatal "Local build of $BOOTSTRAP_NAME did not create $BOOTSTRAP_PATH" + fi +} + +setup_bootstrap() { + if [[ -d "$BOOTSTRAP_PATH" ]]; then + if [[ ! -f "$BOOTSTRAP_PATH/bin/bash" ]]; then + fatal "$BOOTSTRAP_PATH exists, but does not appear to be a bootstrap" + fi + info "$BOOTSTRAP_PATH exists, skipping download." + else + info "Downloading $BOOTSTRAP_NAME" + mkdir -p "$(dirname "$BOOTSTRAP_PATH")" + cd "$(dirname "$BOOTSTRAP_PATH")" + bootstrap_tar="$BOOTSTRAP_NAME.tar.gz" + if wget -nc -nv "$IMAGES/bootstrap/$bootstrap_tar" \ + && wget -nc -nv "$IMAGES/bootstrap/$bootstrap_tar.hash"; then + if [[ -f "$BT_VERIFY" ]]; then + bootstrap_tar_path="$(dirname "$BOOTSTRAP_PATH")/$bootstrap_tar" + BT_DEBUG=$DEBUG $BT_VERIFY \ + --force-gpg "$bootstrap_tar_path" "$bootstrap_tar_path.hash" + else + fatal "$BT_VERIFY script not found, unable to verify downloaded files." + fi + info "Unpacking $BOOTSTRAP_NAME" + mkdir "$BOOTSTRAP_PATH" + tar -zxf "$BOOTSTRAP_NAME.tar.gz" -C "$BOOTSTRAP_PATH" + elif [[ -n $TRANSITION ]]; then + warning "Failed download: $bootstrap_tar - skipping due to transition" + elif [[ -n $STRICT ]]; then + fatal "Failed download: $bootstrap_tar" + else + warning "Failed download: $bootstrap_tar - building it locally instead" + rm -f "$bootstrap_tar" "$bootstrap_tar.hash" + build_bootstrap + fi + fi +} + +# When sourced (as the tests do) stop here: the functions above are defined +# and nothing below runs. +if [[ "${BASH_SOURCE[0]}" != "$0" ]]; then + return 0 +fi + +# check for BT_DEBUG as well as bt scripts call tkldev-setup +if [[ -n "$DEBUG" ]] || [[ -n "$BT_DEBUG" ]]; then + set -x +fi +[[ $(id -u) -eq 0 ]] || fatal "Root user required." + +GIT_DEPTH=${GIT_DEPTH:-1} + +distro="$(lsb_release -si)" +if [[ "${distro,,}" == "turnkey" ]] || [[ "${distro,,}" == "debian" ]]; then + distro="debian" +fi +tkl_ver=$(turnkey-version -t) +unset FORCE STRICT TRANSITION + +export RELEASE="${RELEASE:-${distro}/$(lsb_release -s -c)}" +CODENAME="$(basename "$RELEASE")" +export CODENAME +export FAB_ARCH="${FAB_ARCH:-$(dpkg --print-architecture)}" +export BRANCH="${tkl_ver::2}.x" +IMAGES="http://mirror.turnkeylinux.org/turnkeylinux/images" +FAB_PATH="${FAB_PATH:-/turnkey/fab}" +BOOTSTRAP_NAME="bootstrap-${CODENAME}-${FAB_ARCH}" +BOOTSTRAP_PATH="$FAB_PATH/bootstraps/${CODENAME}-${FAB_ARCH}" +BT_PATH="${BT_PATH:-/turnkey/buildtasks}" +TKLBAM_PATH="${TKLBAM_PATH:-/turnkey/tklbam-profiles}" +BT_VERIFY="$BT_PATH/bin/signature-verify" + +read -ra BT_REFS <<< "${BT_REFS:-$BRANCH ${BRANCH}-dev master}" +read -ra TKLBAM_REFS <<< "${TKLBAM_REFS:-$BRANCH ${BRANCH}-dev master}" +read -ra COMMON_REFS <<< "${COMMON_REFS:-$BRANCH ${BRANCH}-dev master}" +read -ra APP_REFS <<< "${APP_REFS:-$BRANCH ${BRANCH}-dev master}" +read -ra CDROOTS_REFS <<< "${CDROOTS_REFS:-$BRANCH ${BRANCH}-dev master}" + +GIT_REMOTE_URL="${GIT_REMOTE_URL:-https://github.com/turnkeylinux}" +GIT_REMOTE_NAME="${GIT_REMOTE_NAME:-origin}" + +# remove trailing '/' if needed +GIT_REMOTE_URL="${GIT_REMOTE_URL%/}" +# Append '-apps' to git remote URL for apps if default org is 'turnkeylinux' +APP_GIT_REMOTE_URL="$GIT_REMOTE_URL" +if [[ "$GIT_REMOTE_URL" =~ [:/]turnkeylinux ]]; then + APP_GIT_REMOTE_URL="$GIT_REMOTE_URL-apps" +fi + APPS=() while [[ "$#" -gt 0 ]]; do case $1 in @@ -272,7 +330,7 @@ done [[ -n "${APPS[*]}" ]] || APPS=(core) clone_or_update buildtasks "$BT_PATH" "${BT_REFS[@]}" -clone_or_update tklbam-profiles /turnkey/tklbam-profiles "${TKLBAM_REFS[@]}" +clone_or_update tklbam-profiles "$TKLBAM_PATH" "${TKLBAM_REFS[@]}" clone_or_update common "$FAB_PATH/common" "${COMMON_REFS[@]}" for app in "${APPS[@]}"; do clone_or_update _APP_ "$app" "$FAB_PATH/products/$app" "${APP_REFS[@]}" @@ -297,34 +355,5 @@ apt-get update -qq info "Installing newer fab version (if available)" DEBIAN_FRONTEND=noninteractive apt-get install -y fab -if [[ -d "$BOOTSTRAP_PATH" ]]; then - if [[ ! -f "$BOOTSTRAP_PATH/bin/bash" ]]; then - fatal "$BOOTSTRAP_PATH exists, but does not appear to be a bootstrap" - fi - info "$BOOTSTRAP_PATH exists, skipping download." -else - if [[ -n $TRANSITION ]]; then - fail() { warning "Failed download: $* - skipping due to transition"; } - else - fail() { fatal "Failed download: $*"; } - fi - info "Downloading $BOOTSTRAP_NAME" - mkdir -p "$(dirname "$BOOTSTRAP_PATH")" - cd "$(dirname "$BOOTSTRAP_PATH")" - bootstrap_tar="$BOOTSTRAP_NAME.tar.gz" - wget -nc -nv "$IMAGES/bootstrap/$bootstrap_tar" \ - || fail "$bootstrap_tar" - wget -nc -nv "$IMAGES/bootstrap/$bootstrap_tar.hash" \ - || fail "$bootstrap_tar.hash" - if [[ -f "$BT_VERIFY" ]]; then - bootstrap_tar_path=$(dirname "$BOOTSTRAP_PATH/$bootstrap_tar") - BT_DEBUG=$DEBUG $BT_VERIFY \ - --force-gpg "$bootstrap_tar_path" "$bootstrap_tar_path.hash" - else - fatal "$BT_VERIFY script not found, unable to verify downloaded files." - fi - info "Unpacking $BOOTSTRAP_NAME" - mkdir "$BOOTSTRAP_PATH" - tar -zxf "$BOOTSTRAP_NAME.tar.gz" -C "$BOOTSTRAP_PATH" -fi +setup_bootstrap info "$(basename "$0") complete." diff --git a/tests/coverage.sh b/tests/coverage.sh new file mode 100755 index 0000000..a6a49d6 --- /dev/null +++ b/tests/coverage.sh @@ -0,0 +1,37 @@ +#!/bin/bash +# Line coverage of overlay/usr/local/sbin/tkldev-setup under the bats suite +# in this directory, measured with kcov. Exits 1 when the covered share of +# the script is below the threshold (default 95), 2 when a tool is missing. +# +# tests/coverage.sh [THRESHOLD] (or COVERAGE_THRESHOLD in the environment) +# +# COVERAGE_DIR keeps the kcov report (default: a temporary directory). +# Needs the Debian packages kcov and bats. +set -euo pipefail + +here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +target="$(cd "$here/.." && pwd)/overlay/usr/local/sbin/tkldev-setup" +threshold="${1:-${COVERAGE_THRESHOLD:-95}}" + +for tool in kcov bats; do + if ! command -v "$tool" >/dev/null; then + echo "$tool not found (apt-get install $tool)" >&2 + exit 2 + fi +done + +report="${COVERAGE_DIR:-$(mktemp -d)}" +kcov --include-path="$target" "$report" bats "$here" + +# kcov names the sub directory after the command; with --include-path the +# report holds one file, so its first entry is ours +json="$(ls -t "$report"/*/coverage.json | grep -v /kcov-merged/ | head -1)" +percent="$(grep -o '"percent_covered": "[0-9.]*"' "$json" | head -1 | grep -o '[0-9.]*')" +covered="$(grep -o '"covered_lines": "[0-9]*"' "$json" | head -1 | grep -o '[0-9]*')" +total="$(grep -o '"total_lines": "[0-9]*"' "$json" | head -1 | grep -o '[0-9]*')" + +echo "tkldev-setup: $percent percent ($covered of $total lines) covered, threshold $threshold" +if ! awk -v p="$percent" -v t="$threshold" 'BEGIN { exit !(p + 0 >= t + 0) }'; then + echo "coverage below threshold (report: $report)" >&2 + exit 1 +fi diff --git a/tests/stubs/apt-get b/tests/stubs/apt-get new file mode 100755 index 0000000..f8f83d5 --- /dev/null +++ b/tests/stubs/apt-get @@ -0,0 +1,3 @@ +#!/bin/bash +# Stand-in for apt-get: records the call only. +echo "apt-get $*" >> "${STUB_LOG:?}" diff --git a/tests/stubs/dpkg b/tests/stubs/dpkg new file mode 100755 index 0000000..882f31c --- /dev/null +++ b/tests/stubs/dpkg @@ -0,0 +1,4 @@ +#!/bin/bash +# Stand-in for dpkg --print-architecture: STUB_ARCH sets the answer. +echo "dpkg $*" >> "${STUB_LOG:?}" +echo "${STUB_ARCH:-amd64}" diff --git a/tests/stubs/git b/tests/stubs/git new file mode 100755 index 0000000..f82f1f8 --- /dev/null +++ b/tests/stubs/git @@ -0,0 +1,41 @@ +#!/bin/bash +# Stand-in for git used by the tests: records the call in STUB_LOG and +# answers from STUB_* variables instead of touching any repository. +# STUB_REFS refs that "exist" on the remote (ls-remote) +# STUB_GIT_STATUS output of "git status -s" (empty = clean) +# STUB_GIT_BRANCH output of "git branch --show-current" +# STUB_GIT_FAIL subcommands that fail: fetch pull clone checkout, or +# checkout: to fail one branch only +echo "git $*" >> "${STUB_LOG:?}" +if [[ $1 == -C ]]; then + shift 2 +fi +sub=$1 +shift +fails=" ${STUB_GIT_FAIL:-} " +case $sub in + ls-remote) + ref="${*: -1}" + [[ " ${STUB_REFS:-} " == *" $ref "* ]] + ;; + remote) + printf '%s\t%s (fetch)\n' origin https://github.com/turnkeylinux/repo.git + printf '%s\t%s (push)\n' origin https://github.com/turnkeylinux/repo.git + ;; + status) + [[ -z ${STUB_GIT_STATUS:-} ]] || echo "$STUB_GIT_STATUS" + ;; + branch) + echo "${STUB_GIT_BRANCH:-19.x}" + ;; + checkout) + [[ $fails != *" checkout "* && $fails != *" checkout:$1 "* ]] + ;; + clone) + [[ $fails != *" clone "* ]] || exit 128 + mkdir -p "${*: -1}/.git" + ;; + fetch|pull) + [[ $fails != *" $sub "* ]] + ;; +esac diff --git a/tests/stubs/id b/tests/stubs/id new file mode 100755 index 0000000..a41d456 --- /dev/null +++ b/tests/stubs/id @@ -0,0 +1,4 @@ +#!/bin/bash +# Stand-in for id -u: STUB_UID sets the answer (default root). +echo "id $*" >> "${STUB_LOG:?}" +echo "${STUB_UID:-0}" diff --git a/tests/stubs/lsb_release b/tests/stubs/lsb_release new file mode 100755 index 0000000..0bdca71 --- /dev/null +++ b/tests/stubs/lsb_release @@ -0,0 +1,7 @@ +#!/bin/bash +# Stand-in for lsb_release: STUB_DISTRO and STUB_CODENAME set the answers. +echo "lsb_release $*" >> "${STUB_LOG:?}" +case " $* " in + *" -si "*) echo "${STUB_DISTRO:-TurnKey}" ;; + *" -c "*) echo "${STUB_CODENAME:-trixie}" ;; +esac diff --git a/tests/stubs/make b/tests/stubs/make new file mode 100755 index 0000000..a7f1cf5 --- /dev/null +++ b/tests/stubs/make @@ -0,0 +1,19 @@ +#!/bin/bash +# Stand-in for make: records the call and, on "install", creates the +# bootstrap where the FAB_PATH/RELEASE/FAB_ARCH arguments point, unless +# STUB_MAKE_NO_BOOTSTRAP is set. +echo "make $*" >> "${STUB_LOG:?}" +fab_path= release= arch= install= +for arg in "$@"; do + case $arg in + FAB_PATH=*) fab_path="${arg#*=}" ;; + RELEASE=*) release="${arg#*=}" ;; + FAB_ARCH=*) arch="${arg#*=}" ;; + install) install=y ;; + esac +done +if [[ -n $install && -z ${STUB_MAKE_NO_BOOTSTRAP:-} ]]; then + dst="$fab_path/bootstraps/$(basename "$release")-$arch" + mkdir -p "$dst/bin" + echo '#!/bin/sh' > "$dst/bin/bash" +fi diff --git a/tests/stubs/signature-verify b/tests/stubs/signature-verify new file mode 100755 index 0000000..9cbc96f --- /dev/null +++ b/tests/stubs/signature-verify @@ -0,0 +1,5 @@ +#!/bin/bash +# Stand-in for buildtasks/bin/signature-verify: STUB_VERIFY_RC is its exit +# code. The test installs it under BT_PATH/bin. +echo "signature-verify $*" >> "${STUB_LOG:?}" +exit "${STUB_VERIFY_RC:-0}" diff --git a/tests/stubs/turnkey-version b/tests/stubs/turnkey-version new file mode 100755 index 0000000..3837a5d --- /dev/null +++ b/tests/stubs/turnkey-version @@ -0,0 +1,4 @@ +#!/bin/bash +# Stand-in for turnkey-version: STUB_TKL_VERSION sets the answer. +echo "turnkey-version $*" >> "${STUB_LOG:?}" +echo "${STUB_TKL_VERSION:-19.0}" diff --git a/tests/stubs/wget b/tests/stubs/wget new file mode 100755 index 0000000..62e5b71 --- /dev/null +++ b/tests/stubs/wget @@ -0,0 +1,19 @@ +#!/bin/bash +# Stand-in for wget: records the call and creates the requested file in the +# current directory (a real tarball with bin/bash for *.tar.gz). +# STUB_WGET_FAIL "all" fails every download, "hash" only the *.hash one +echo "wget $*" >> "${STUB_LOG:?}" +url="${*: -1}" +name="$(basename "$url")" +fails=" ${STUB_WGET_FAIL:-} " +[[ $fails != *" all "* ]] || exit 8 +if [[ $name == *.hash ]]; then + [[ $fails != *" hash "* ]] || exit 8 + echo "stub hash for $name" > "$name" + exit 0 +fi +work="$(mktemp -d)" +mkdir -p "$work/bin" +echo '#!/bin/sh' > "$work/bin/bash" +tar -C "$work" -zcf "$name" bin +rm -rf "$work" diff --git a/tests/tkldev-setup.bats b/tests/tkldev-setup.bats new file mode 100644 index 0000000..70395d2 --- /dev/null +++ b/tests/tkldev-setup.bats @@ -0,0 +1,588 @@ +#!/usr/bin/env bats +# Tests for overlay/usr/local/sbin/tkldev-setup. +# +# Every external command the script runs (git, wget, apt-get, make, +# lsb_release, turnkey-version, dpkg, id, signature-verify) is replaced by a +# stub from tests/stubs that records its arguments in STUB_LOG, and every +# path the script writes is under a scratch directory. Nothing here touches +# the live system or the network. +# +# Two kinds of tests: "run_setup" executes the whole script (option parsing +# and the main body), "call" sources it inside a fresh "bash -e" and runs one +# function, as the script itself would. + +setup() { + TESTS_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")" && pwd)" + SCRIPT="$TESTS_DIR/../overlay/usr/local/sbin/tkldev-setup" + SCRATCH="$BATS_TEST_TMPDIR" + + export PATH="$TESTS_DIR/stubs:$PATH" + export STUB_LOG="$SCRATCH/calls.log" + : > "$STUB_LOG" + export STUB_REFS="19.x master" + export STUB_GIT_BRANCH="19.x" + unset STUB_GIT_STATUS STUB_GIT_FAIL STUB_WGET_FAIL STUB_MAKE_NO_BOOTSTRAP + unset STUB_DISTRO STUB_CODENAME STUB_TKL_VERSION STUB_ARCH STUB_UID + unset STUB_VERIFY_RC + + export FAB_PATH="$SCRATCH/fab" + export BT_PATH="$SCRATCH/buildtasks" + export TKLBAM_PATH="$SCRATCH/tklbam-profiles" + unset RELEASE FAB_ARCH BRANCH GIT_DEPTH GIT_REMOTE_URL GIT_REMOTE_NAME + unset BT_REFS TKLBAM_REFS COMMON_REFS APP_REFS CDROOTS_REFS + unset DEBUG BT_DEBUG STRICT FORCE TRANSITION + + # buildtasks checked out, with the example config and the verify script + make_git_dir "$BT_PATH" + mkdir -p "$BT_PATH/config.example" "$BT_PATH/bin" + echo 'BT_GPGKEY=DEADBEEF' > "$BT_PATH/config.example/common.cfg" + cp "$TESTS_DIR/stubs/signature-verify" "$BT_PATH/bin/signature-verify" + + # variables the functions read when called on their own + export RELEASE_UNDER_TEST="debian/trixie" + export BOOTSTRAP_NAME="bootstrap-trixie-amd64" + export BOOTSTRAP_PATH="$FAB_PATH/bootstraps/trixie-amd64" + export BT_VERIFY="$BT_PATH/bin/signature-verify" + export IMAGES="http://mirror.turnkeylinux.org/turnkeylinux/images" + export GIT_REMOTE_URL="https://github.com/turnkeylinux" + export APP_GIT_REMOTE_URL="https://github.com/turnkeylinux-apps" + export GIT_DEPTH=1 +} + +make_git_dir() { + mkdir -p "$1/.git" +} + +make_bootstrap() { + mkdir -p "$1/bin" + echo '#!/bin/sh' > "$1/bin/bash" +} + +# run the whole script +run_setup() { + run "$SCRIPT" "$@" +} + +# run one function of the script in a fresh bash -e, as the script does +call() { + run bash -e -c 'source "$1"; shift; "$@"' tkldev-setup-test "$SCRIPT" "$@" +} + +logged() { + grep -qF -- "$1" "$STUB_LOG" +} + +not_logged() { + ! grep -qF -- "$1" "$STUB_LOG" +} + +output_has() { + [[ "$output" == *"$1"* ]] +} + +# --- options ---------------------------------------------------------------- + +@test "-h prints the usage and exits 0" { + run_setup -h + [ "$status" -eq 0 ] + output_has "Tool to initialize and maintain TKLDev" + output_has "FAB_PATH Fab PATH (default: $FAB_PATH)" + output_has "TKLBAM_PATH tklbam-profiles PATH (default: $TKLBAM_PATH)" + not_logged "git clone" +} + +@test "--help is the same as -h" { + run_setup --help + [ "$status" -eq 0 ] + output_has "Options:" + not_logged "git clone" +} + +@test "without arguments core is the only appliance" { + run_setup + [ "$status" -eq 0 ] + logged "git clone --branch 19.x --depth 1 https://github.com/turnkeylinux-apps/core.git $FAB_PATH/products/core" + logged "https://github.com/turnkeylinux/tklbam-profiles.git $TKLBAM_PATH" + logged "https://github.com/turnkeylinux/common.git $FAB_PATH/common" + logged "https://github.com/turnkeylinux/cdroots.git $FAB_PATH/cdroots" + [ "$(grep -c 'products/' "$STUB_LOG")" -eq 1 ] +} + +@test "every argument is processed as an appliance" { + run_setup foo bar + [ "$status" -eq 0 ] + logged "turnkeylinux-apps/foo.git $FAB_PATH/products/foo" + logged "turnkeylinux-apps/bar.git $FAB_PATH/products/bar" + not_logged "products/core" +} + +@test "an unknown option is taken as an appliance name" { + run_setup --bogus + [ "$status" -eq 0 ] + logged "turnkeylinux-apps/--bogus.git $FAB_PATH/products/--bogus" +} + +@test "clones are shallow by default" { + run_setup + [ "$status" -eq 0 ] + logged "git clone --branch 19.x --depth 1 https://github.com/turnkeylinux/common.git" +} + +@test "-n clones without --depth" { + run_setup -n + [ "$status" -eq 0 ] + logged "git clone --branch 19.x https://github.com/turnkeylinux/common.git" + not_logged "--depth" +} + +@test "--full-clone is the same as -n" { + run_setup --full-clone + [ "$status" -eq 0 ] + not_logged "--depth" +} + +@test "a dirty repo is skipped with a warning by default" { + export STUB_GIT_STATUS=" M bin/signature-verify" + run_setup + [ "$status" -eq 0 ] + output_has "WARNING [tkldev-setup]: $BT_PATH has uncommitted files, skipping." +} + +@test "-s fails on a dirty repo instead of skipping" { + export STUB_GIT_STATUS=" M bin/signature-verify" + run_setup -s + [ "$status" -eq 1 ] + output_has "FATAL [tkldev-setup]: $BT_PATH has uncommitted files" + not_logged "git clone" +} + +@test "--strict is the same as -s" { + export STUB_GIT_STATUS=" M bin/signature-verify" + run_setup --strict + [ "$status" -eq 1 ] +} + +@test "-f checks out the resolved branch when another one is checked out" { + export STUB_GIT_BRANCH=master + run_setup -f + [ "$status" -eq 0 ] + output_has "Force used - attempting to checkout 19.x" + logged "git -C $BT_PATH checkout 19.x" + logged "git -C $BT_PATH pull origin 19.x" +} + +@test "--force is the same as -f" { + export STUB_GIT_BRANCH=master + run_setup --force + [ "$status" -eq 0 ] + logged "git -C $BT_PATH checkout 19.x" +} + +@test "-t skips a bootstrap that cannot be downloaded" { + export STUB_WGET_FAIL=all + run_setup -t + [ "$status" -eq 0 ] + output_has "Failed download: bootstrap-trixie-amd64.tar.gz - skipping due to transition" + not_logged "make" +} + +@test "--transition is the same as -t" { + export STUB_WGET_FAIL=all + run_setup --transition + [ "$status" -eq 0 ] + not_logged "make" +} + +# --- main body -------------------------------------------------------------- + +@test "fails when not run as root" { + export STUB_UID=1000 + run_setup + [ "$status" -eq 1 ] + output_has "FATAL [tkldev-setup]: Root user required." + not_logged "lsb_release" +} + +@test "DEBUG and BT_DEBUG turn on tracing" { + DEBUG=1 run_setup + [ "$status" -eq 0 ] + BT_DEBUG=1 run_setup + [ "$status" -eq 0 ] +} + +@test "release and architecture come from the system" { + export STUB_WGET_FAIL=all + run_setup + [ "$status" -eq 0 ] + logged "wget -nc -nv $IMAGES/bootstrap/bootstrap-trixie-amd64.tar.gz" + logged "make -C $SCRATCH/bootstrap install FAB_PATH=$FAB_PATH RELEASE=debian/trixie FAB_ARCH=amd64" +} + +@test "a distro other than TurnKey or Debian keeps its own name" { + export STUB_DISTRO=Ubuntu STUB_CODENAME=noble STUB_WGET_FAIL=all + run_setup + [ "$status" -eq 0 ] + logged "RELEASE=Ubuntu/noble FAB_ARCH=amd64" + logged "bootstrap-noble-amd64.tar.gz" +} + +@test "the series branch comes from turnkey-version" { + export STUB_TKL_VERSION=18.2 STUB_REFS="18.x" + run_setup + [ "$status" -eq 0 ] + logged "git clone --branch 18.x --depth 1 https://github.com/turnkeylinux/common.git" +} + +@test "on arm64 cdroots is not cloned" { + export STUB_ARCH=arm64 + run_setup + [ "$status" -eq 0 ] + not_logged "cdroots" + logged "bootstrap-trixie-arm64.tar.gz" +} + +@test "the example config is copied when config is missing" { + run_setup + [ "$status" -eq 0 ] + [ -f "$BT_PATH/config/common.cfg" ] +} + +@test "an existing config is kept" { + mkdir -p "$BT_PATH/config" + echo 'BT_GPGKEY=CAFEBABE' > "$BT_PATH/config/common.cfg" + run_setup + [ "$status" -eq 0 ] + grep -q CAFEBABE "$BT_PATH/config/common.cfg" +} + +@test "fails when config/common.cfg is missing" { + mkdir -p "$BT_PATH/config" + run_setup + [ "$status" -eq 1 ] + output_has "FATAL [tkldev-setup]: $BT_PATH config not found" + not_logged "apt-get" +} + +@test "a remote outside turnkeylinux is used unchanged for appliances" { + export GIT_REMOTE_URL="https://[2001:db8::1]/mirror/" + run_setup + [ "$status" -eq 0 ] + logged "https://[2001:db8::1]/mirror/common.git" + logged "https://[2001:db8::1]/mirror/core.git" + not_logged "mirror//" + not_logged "mirror-apps" +} + +@test "apt is updated and fab installed" { + run_setup + [ "$status" -eq 0 ] + logged "apt-get update -qq" + logged "apt-get install -y fab" + output_has "INFO [tkldev-setup]: tkldev-setup complete." +} + +# --- resolve_ref ------------------------------------------------------------ + +@test "resolve_ref returns the first candidate that exists on the remote" { + export STUB_REFS="19.x-dev master" + call resolve_ref "$GIT_REMOTE_URL/common" 19.x 19.x-dev master + [ "$status" -eq 0 ] + [ "$output" = "19.x-dev" ] + logged "git ls-remote --exit-code --heads $GIT_REMOTE_URL/common 19.x" + logged "git ls-remote --exit-code --heads $GIT_REMOTE_URL/common 19.x-dev" + not_logged "common master" +} + +@test "resolve_ref skips empty candidates and fails when none exists" { + export STUB_REFS="" + call resolve_ref "$GIT_REMOTE_URL/common" "" 19.x + [ "$status" -eq 1 ] + [ -z "$output" ] + not_logged "common $" + logged "common 19.x" +} + +# --- update_repo ------------------------------------------------------------ + +@test "update_repo fetches and pulls a clean repo on the right branch" { + call update_repo "$BT_PATH" 19.x + [ "$status" -eq 0 ] + logged "git -C $BT_PATH remote --verbose" + logged "git -C $BT_PATH fetch origin" + logged "git -C $BT_PATH pull origin 19.x" + not_logged "checkout" +} + +@test "update_repo skips a dirty repo with a warning" { + export STUB_GIT_STATUS="?? new-file" + call update_repo "$BT_PATH" 19.x + [ "$status" -eq 0 ] + output_has "WARNING [tkldev-setup-test]: $BT_PATH has uncommitted files, skipping." + not_logged "fetch" +} + +@test "update_repo fails on a dirty repo with --strict" { + export STUB_GIT_STATUS="?? new-file" STRICT=true + call update_repo "$BT_PATH" 19.x + [ "$status" -eq 1 ] + output_has "FATAL [tkldev-setup-test]: $BT_PATH has uncommitted files" +} + +@test "update_repo skips when the fetch fails" { + export STUB_GIT_FAIL=fetch + call update_repo "$BT_PATH" 19.x + [ "$status" -eq 0 ] + output_has "($BT_PATH) Fetching remote: origin failed, skipping." + not_logged "pull" +} + +@test "update_repo fails when the fetch fails with --strict" { + export STUB_GIT_FAIL=fetch STRICT=true + call update_repo "$BT_PATH" 19.x + [ "$status" -eq 1 ] + output_has "FATAL [tkldev-setup-test]: ($BT_PATH) Fetching remote: origin failed" +} + +@test "update_repo uses the current branch when none is given" { + export STUB_GIT_BRANCH=master + call update_repo "$BT_PATH" "" + [ "$status" -eq 0 ] + logged "git -C $BT_PATH pull origin master" + not_logged "checkout" +} + +@test "update_repo skips a repo on an alternate branch" { + export STUB_GIT_BRANCH=my-feature + call update_repo "$BT_PATH" 19.x + [ "$status" -eq 0 ] + output_has "($BT_PATH) checked out branch is my-feature (not 19.x) so skipping" + not_logged "pull" +} + +@test "update_repo fails on an alternate branch with --strict" { + export STUB_GIT_BRANCH=my-feature STRICT=true + call update_repo "$BT_PATH" 19.x + [ "$status" -eq 1 ] + output_has "FATAL [tkldev-setup-test]: ($BT_PATH) checked out branch is my-feature (not 19.x)" +} + +@test "update_repo checks out the branch with --force" { + export STUB_GIT_BRANCH=my-feature FORCE=y + call update_repo "$BT_PATH" 19.x + [ "$status" -eq 0 ] + logged "git -C $BT_PATH checkout 19.x" + not_logged "checkout 19.x-dev" + logged "git -C $BT_PATH pull origin 19.x" +} + +@test "update_repo with --force falls back to the -dev branch" { + export STUB_GIT_BRANCH=my-feature FORCE=y STUB_GIT_FAIL="checkout:19.x" + call update_repo "$BT_PATH" 19.x + [ "$status" -eq 0 ] + logged "git -C $BT_PATH checkout 19.x-dev" + logged "git -C $BT_PATH pull origin 19.x" +} + +@test "update_repo with --force skips when neither checkout works" { + export STUB_GIT_BRANCH=my-feature FORCE=y STUB_GIT_FAIL=checkout + call update_repo "$BT_PATH" 19.x + [ "$status" -eq 0 ] + output_has "($BT_PATH) Checking out '19.x' failed, skipping." + not_logged "pull" +} + +@test "update_repo moves from the series dev branch to the release branch" { + export STUB_GIT_BRANCH=19.x-dev + call update_repo "$BT_PATH" 19.x + [ "$status" -eq 0 ] + output_has "current checked out branch is 19.x-dev attempting to checkout 19.x" + logged "git -C $BT_PATH checkout 19.x" + logged "git -C $BT_PATH pull origin 19.x" +} + +@test "update_repo skips when leaving the dev branch fails" { + export STUB_GIT_BRANCH=19.x-dev STUB_GIT_FAIL=checkout + call update_repo "$BT_PATH" 19.x + [ "$status" -eq 0 ] + output_has "($BT_PATH) checkout failed - skipping" + not_logged "pull" +} + +@test "update_repo fails when leaving the dev branch fails with --strict" { + export STUB_GIT_BRANCH=19.x-dev STUB_GIT_FAIL=checkout STRICT=true + call update_repo "$BT_PATH" 19.x + [ "$status" -eq 1 ] + output_has "FATAL [tkldev-setup-test]: ($BT_PATH) checkout failed" +} + +@test "update_repo pulls the requested branch from another series without checkout" { + export STUB_GIT_BRANCH=18.x + call update_repo "$BT_PATH" 19.x + [ "$status" -eq 0 ] + not_logged "checkout" + logged "git -C $BT_PATH pull origin 19.x" +} + +@test "update_repo skips when the pull fails" { + export STUB_GIT_FAIL=pull + call update_repo "$BT_PATH" 19.x + [ "$status" -eq 0 ] + output_has "($BT_PATH) Updating '19.x' failed, skipping." +} + +@test "update_repo fails when the pull fails with --strict" { + export STUB_GIT_FAIL=pull STRICT=true + call update_repo "$BT_PATH" 19.x + [ "$status" -eq 1 ] + output_has "FATAL [tkldev-setup-test]: ($BT_PATH) Updating '19.x' failed" +} + +# --- clone_or_update -------------------------------------------------------- + +@test "clone_or_update updates an existing git checkout" { + call clone_or_update buildtasks "$BT_PATH" 19.x master + [ "$status" -eq 0 ] + output_has "INFO [tkldev-setup-test]: Resolved buildtasks to ref: 19.x" + output_has "$BT_PATH exists and is a git repo, attempting update." + logged "git -C $BT_PATH pull origin 19.x" + not_logged "clone" +} + +@test "clone_or_update fails when the destination is not a git repo" { + mkdir -p "$SCRATCH/plain" + call clone_or_update common "$SCRATCH/plain" 19.x + [ "$status" -eq 1 ] + output_has "FATAL [tkldev-setup-test]: $SCRATCH/plain exists, but is not a git repo." +} + +@test "clone_or_update clones a missing destination with the resolved ref and depth" { + call clone_or_update common "$FAB_PATH/common" 19.x master + [ "$status" -eq 0 ] + output_has "Attempting to clone $GIT_REMOTE_URL/common into $FAB_PATH/common." + logged "git clone --branch 19.x --depth 1 $GIT_REMOTE_URL/common.git $FAB_PATH/common" + [ -d "$FAB_PATH/common/.git" ] +} + +@test "clone_or_update clones fully when GIT_DEPTH is full" { + export GIT_DEPTH=full + call clone_or_update common "$FAB_PATH/common" 19.x + [ "$status" -eq 0 ] + logged "git clone --branch 19.x $GIT_REMOTE_URL/common.git $FAB_PATH/common" +} + +@test "clone_or_update falls back to the default branch when no ref resolves" { + export STUB_REFS="" + call clone_or_update common "$FAB_PATH/common" 19.x master + [ "$status" -eq 0 ] + output_has "Could not resolve preferred refs for common; falling back to repo default branch." + logged "git clone --depth 1 $GIT_REMOTE_URL/common.git $FAB_PATH/common" +} + +@test "clone_or_update fails when no ref resolves with --strict" { + export STUB_REFS="" STRICT=true + call clone_or_update common "$FAB_PATH/common" 19.x master + [ "$status" -eq 1 ] + output_has "FATAL [tkldev-setup-test]: Could not resolve preferred refs for common" + not_logged "clone" +} + +@test "clone_or_update uses the apps remote for _APP_" { + call clone_or_update _APP_ core "$FAB_PATH/products/core" 19.x + [ "$status" -eq 0 ] + logged "git clone --branch 19.x --depth 1 $APP_GIT_REMOTE_URL/core.git $FAB_PATH/products/core" +} + +@test "clone_or_update fails when the clone fails" { + export STUB_GIT_FAIL=clone + call clone_or_update common "$FAB_PATH/common" 19.x + [ "$status" -eq 1 ] + output_has "FATAL [tkldev-setup-test]: Cloning $FAB_PATH/common failed." +} + +# --- setup_bootstrap and build_bootstrap ------------------------------------- + +@test "setup_bootstrap skips the download when the bootstrap exists" { + make_bootstrap "$BOOTSTRAP_PATH" + call setup_bootstrap + [ "$status" -eq 0 ] + output_has "$BOOTSTRAP_PATH exists, skipping download." + not_logged "wget" +} + +@test "setup_bootstrap fails when the directory is not a bootstrap" { + mkdir -p "$BOOTSTRAP_PATH" + call setup_bootstrap + [ "$status" -eq 1 ] + output_has "FATAL [tkldev-setup-test]: $BOOTSTRAP_PATH exists, but does not appear to be a bootstrap" +} + +@test "setup_bootstrap downloads, verifies and unpacks the bootstrap" { + call setup_bootstrap + [ "$status" -eq 0 ] + output_has "INFO [tkldev-setup-test]: Downloading bootstrap-trixie-amd64" + logged "wget -nc -nv $IMAGES/bootstrap/bootstrap-trixie-amd64.tar.gz" + logged "wget -nc -nv $IMAGES/bootstrap/bootstrap-trixie-amd64.tar.gz.hash" + logged "signature-verify --force-gpg $FAB_PATH/bootstraps/bootstrap-trixie-amd64.tar.gz $FAB_PATH/bootstraps/bootstrap-trixie-amd64.tar.gz.hash" + output_has "Unpacking bootstrap-trixie-amd64" + [ -f "$BOOTSTRAP_PATH/bin/bash" ] + not_logged "make" +} + +@test "setup_bootstrap fails when the verify script is missing" { + export BT_VERIFY="$SCRATCH/no-such-verify" + call setup_bootstrap + [ "$status" -eq 1 ] + output_has "FATAL [tkldev-setup-test]: $BT_VERIFY script not found, unable to verify downloaded files." + [ ! -d "$BOOTSTRAP_PATH" ] +} + +@test "setup_bootstrap stops when verification fails" { + export STUB_VERIFY_RC=3 + call setup_bootstrap + [ "$status" -eq 3 ] + [[ "$output" != *"Unpacking"* ]] + [ ! -d "$BOOTSTRAP_PATH" ] +} + +@test "setup_bootstrap builds locally when the download fails" { + export STUB_WGET_FAIL=all RELEASE="$RELEASE_UNDER_TEST" FAB_ARCH=amd64 + call setup_bootstrap + [ "$status" -eq 0 ] + output_has "WARNING [tkldev-setup-test]: Failed download: bootstrap-trixie-amd64.tar.gz - building it locally instead" + logged "git clone --branch master --depth 1 $GIT_REMOTE_URL/bootstrap.git $SCRATCH/bootstrap" + output_has "Building bootstrap-trixie-amd64 locally (from $SCRATCH/bootstrap)" + logged "make -C $SCRATCH/bootstrap clean" + logged "make -C $SCRATCH/bootstrap install FAB_PATH=$FAB_PATH RELEASE=debian/trixie FAB_ARCH=amd64" + [ -f "$BOOTSTRAP_PATH/bin/bash" ] +} + +@test "setup_bootstrap removes a partial download before building" { + export STUB_WGET_FAIL=hash RELEASE="$RELEASE_UNDER_TEST" FAB_ARCH=amd64 + call setup_bootstrap + [ "$status" -eq 0 ] + [ ! -e "$FAB_PATH/bootstraps/bootstrap-trixie-amd64.tar.gz" ] + logged "make -C $SCRATCH/bootstrap install" +} + +@test "setup_bootstrap fails on a missing download with --strict" { + export STUB_WGET_FAIL=all STRICT=true + call setup_bootstrap + [ "$status" -eq 1 ] + output_has "FATAL [tkldev-setup-test]: Failed download: bootstrap-trixie-amd64.tar.gz" + not_logged "make" +} + +@test "setup_bootstrap skips a missing download with --transition, even with --strict" { + export STUB_WGET_FAIL=all STRICT=true TRANSITION=true + call setup_bootstrap + [ "$status" -eq 0 ] + output_has "WARNING [tkldev-setup-test]: Failed download: bootstrap-trixie-amd64.tar.gz - skipping due to transition" + not_logged "make" +} + +@test "build_bootstrap fails when the local build produces no bootstrap" { + export STUB_MAKE_NO_BOOTSTRAP=1 RELEASE="$RELEASE_UNDER_TEST" FAB_ARCH=amd64 + call build_bootstrap + [ "$status" -eq 1 ] + output_has "FATAL [tkldev-setup-test]: Local build of bootstrap-trixie-amd64 did not create $BOOTSTRAP_PATH" +}