Files
Coredesktop-Dev/lib/smoketest.sh
T
2026-11-04 09:43:05 -07:00

354 lines
12 KiB
Bash

#!/bin/bash
#
# Purpose : coredesktop-dev smoketest: run CoreDesktop's smoketest scenarios
# (smoketest/ in the working copy) on QEMU virtual machines
source "$(dirname "${BASH_SOURCE[0]}")/vm.sh"
CLOUD_IMAGE_URL="${SMOKETEST_CLOUD_IMAGE_URL:-https://geo.mirror.pkgbuild.com/images/latest/Arch-Linux-x86_64-cloudimg.qcow2}"
DISK_SIZE=120G
RUNS="$SMOKETEST_STATE/runs"
smoketest_usage() {
cat << EOF
Usage: coredesktop-dev smoketest <command>
list Show the scenarios
run [options] [scenario...]
Run scenarios against the working copy's HEAD
--tier pr|all pr (the default): the quick ones; all: everything,
including a fresh install and upgrades from older
commits, which take hours
--ref REF Test REF instead of HEAD
--keep After a failure, leave its VM running to look
around in, and stop
images Show the VM images
images base (Re)build the base image: Arch + GNOME
images installed [REF] (Re)build the installed image: base + install.sh at
REF (default: where HEAD branched from upstream)
shell [base|installed] Boot a throwaway copy of an image and SSH in
clean [--all] Delete old runs' logs (--all: images and the
package cache too)
Scenarios are named by file (40-failing-migration) or number (40). Each run
keeps its logs in $RUNS.
Set SMOKETEST_VNC=1 to watch the VM's screen at VNC display 127.0.0.1:1.
EOF
}
cmd_smoketest() {
local sub="${1:-}"
shift || true
case "$sub" in
-h | --help | "") smoketest_usage; [ -n "$sub" ]; return ;;
list | run | images | shell | clean) ;;
*) smoketest_usage >&2; return 1 ;;
esac
require_working_copy
SCENARIOS="$WORKING_COPY/smoketest/scenarios"
[ -d "$SCENARIOS" ] || die "$WORKING_COPY has no smoketest/scenarios; is it an older CoreDesktop?"
"smoketest_$sub" "$@"
}
require_vm_tools() {
local tool
for tool in qemu-system-x86_64 qemu-img xorriso curl ssh scp ssh-keygen; do
command -v "$tool" &> /dev/null \
|| die "$tool isn't installed. Install coredesktop-dev's packages: bash $(sibling_dir)/update.sh"
done
[ -r "$KVM_DEVICE" ] && [ -w "$KVM_DEVICE" ] \
|| die "Can't use $KVM_DEVICE. Turn on virtualization (VT-x/AMD-V) in the firmware settings."
}
# scenario_info FILE: print IMAGE, TIER, DESCRIPTION and the VARIANTS (one per
# line after the first three)
scenario_info() {
(
source "$WORKING_COPY/smoketest/lib/scenario.sh"
source "$1"
printf '%s\n' "$IMAGE" "$TIER" "$DESCRIPTION"
(( ${#VARIANTS[@]} == 0 )) || printf '%s\n' "${VARIANTS[@]}"
)
}
smoketest_list() {
local file info
for file in "$SCENARIOS"/*.sh; do
mapfile -t info < <(scenario_info "$file")
printf '%-28s %-8s %-10s %s' "$(basename "$file" .sh)" "${info[1]}" "${info[0]}" "${info[2]}"
(( ${#info[@]} > 3 )) && printf ' (%d variants)' $(( ${#info[@]} - 3 ))
echo
done
}
# Set SELECTED to the scenario files to run: those named, or those in the tier
select_scenarios() {
local tier="$1" name file found info
shift
SELECTED=()
if (( $# > 0 )); then
for name in "$@"; do
found=""
for file in "$SCENARIOS"/*.sh; do
case "$(basename "$file" .sh)" in
"$name" | "$name"-*) found="$file"; break ;;
esac
done
[ -n "$found" ] || die "No scenario called $name (see: coredesktop-dev smoketest list)"
SELECTED+=("$found")
done
return
fi
for file in "$SCENARIOS"/*.sh; do
mapfile -t info < <(scenario_info "$file")
[[ "$tier" == all || "${info[1]}" == "$tier" ]] && SELECTED+=("$file")
done
return 0
}
smoketest_run() {
local tier=pr ref=HEAD keep=0 names=()
while (( $# > 0 )); do
case "$1" in
--tier) tier="${2:-}"; shift 2 || die "--tier needs pr or all" ;;
--ref) ref="${2:-}"; shift 2 || die "--ref needs a commit" ;;
--keep) keep=1; shift ;;
-*) die "Unknown option: $1" ;;
*) names+=("$1"); shift ;;
esac
done
[[ "$tier" == pr || "$tier" == all ]] || die "--tier is pr or all, not $tier"
require_vm_tools
local sha
sha="$(git -C "$WORKING_COPY" rev-parse --verify --quiet "$ref^{commit}")" || die "No such commit: $ref"
if [ "$ref" = HEAD ] && [ -n "$(git -C "$WORKING_COPY" status --porcelain --untracked-files=no)" ]; then
echo "Note: the working copy has uncommitted changes. Only HEAD is installed and"
echo "updated in the VM; the scenarios themselves are read from the working copy."
fi
select_scenarios "$tier" "${names[@]}"
local files=("${SELECTED[@]}")
(( ${#files[@]} > 0 )) || die "No scenarios to run."
# Build the images the chosen scenarios need, if they aren't there yet
local file info need_installed=0
for file in "${files[@]}"; do
mapfile -t info < <(scenario_info "$file")
[ "${info[0]}" = installed ] && need_installed=1
done
[ -f "$IMAGES/base.qcow2" ] || build_base_image
if (( need_installed )) && [ ! -f "$IMAGES/installed.qcow2" ]; then
build_installed_image "$(default_installed_ref)"
fi
RUN="$RUNS/$(date +%Y%m%d-%H%M%S)"
mkdir -p "$RUN/shared"
echo "Testing $(git -C "$WORKING_COPY" log -1 --format='%h %s' "$sha"); logs in $RUN"
local results=() failed=0 variants variant status
for file in "${files[@]}"; do
mapfile -t info < <(scenario_info "$file")
variants=("${info[@]:3}")
(( ${#variants[@]} > 0 )) || variants=("")
for variant in "${variants[@]}"; do
run_scenario "$file" "$variant" "$sha" "$keep"
status=$?
local label
label="$(basename "$file" .sh)${variant:+ @ $variant}"
case $status in
0) results+=("PASS $label") ;;
3) results+=("SKIP $label") ;;
*) results+=("FAIL $label"); failed=1 ;;
esac
if (( failed && keep )); then
break 2
fi
done
done
echo
printf '%s\n' "${results[@]}" | tee "$RUN/summary"
echo "Logs: $RUN"
return $failed
}
# run_scenario FILE VARIANT SHA KEEP: boot a throwaway copy of the scenario's
# image and run it. Returns 0 for pass, 3 for skip, anything else for fail.
run_scenario() {
local file="$1" variant="$2" sha="$3" keep="$4"
local name dir
name="$(basename "$file" .sh)${variant:+@$variant}"
dir="$RUN/$name"
mkdir -p "$dir"
echo
echo "=== $name"
(
export SMOKETEST_RESULTS="$dir" SMOKETEST_SHARED="$RUN/shared" SMOKETEST_REF="$sha" VARIANT="$variant"
IMAGE_REF="$(cat "$IMAGES/installed.ref" 2> /dev/null)"
export IMAGE_REF
source "$WORKING_COPY/smoketest/lib/scenario.sh"
source "$file"
qemu-img create -q -f qcow2 -b "$IMAGES/$IMAGE.qcow2" -F qcow2 "$dir/disk.qcow2" \
|| die "Couldn't make the VM's disk."
vm_start "$dir" "$dir/disk.qcow2"
trap 'finish_scenario $? '"$keep" EXIT
vm_wait_up
smoketest_prepare
set -e
scenario
) 2>&1 | tee "$dir/scenario.log"
return "${PIPESTATUS[0]}"
}
# Runs as the scenario's subshell exits: stop the VM and delete its disk,
# unless it failed and --keep was given
finish_scenario() {
local status="$1" keep="$2"
if (( status != 0 && status != SKIP_STATUS && keep )); then
echo
echo "The VM is still running. To look around: $(vm_ssh_command)"
echo "To stop it: kill $(vm_pid) Its disk: $VM_DIR/disk.qcow2"
return
fi
vm_kill
rm -f "$VM_DIR/disk.qcow2"
}
# Where the installed image is built by default: the commit HEAD branched
# from upstream, so updating it to HEAD exercises the branch's changes
default_installed_ref() {
git -C "$WORKING_COPY" merge-base HEAD '@{upstream}' 2> /dev/null \
|| git -C "$WORKING_COPY" merge-base HEAD origin/main 2> /dev/null \
|| git -C "$WORKING_COPY" rev-parse HEAD
}
download_cloud_image() {
local image="$IMAGES/cloudimg.qcow2" expected actual
[ -f "$image" ] && return 0
mkdir -p "$IMAGES"
echo "Downloading Arch's cloud image..."
curl -fL --progress-bar -o "$image.part" "$CLOUD_IMAGE_URL" || die "Couldn't download $CLOUD_IMAGE_URL"
expected="$(curl -fsSL "$CLOUD_IMAGE_URL.SHA256" | awk '{print $1}')" \
|| die "Couldn't download $CLOUD_IMAGE_URL.SHA256"
actual="$(sha256sum "$image.part" | awk '{print $1}')"
[ -n "$expected" ] && [ "$expected" = "$actual" ] \
|| die "The cloud image's checksum doesn't match; try again."
mv "$image.part" "$image"
}
build_base_image() {
require_vm_tools
download_cloud_image
local build="$SMOKETEST_STATE/build-base"
rm -rf "$build"
mkdir -p "$build/seed"
echo "Building the base image (Arch + GNOME); log: $build/build.log"
cp --reflink=auto "$IMAGES/cloudimg.qcow2" "$build/disk.qcow2"
qemu-img resize -q "$build/disk.qcow2" "$DISK_SIZE"
ensure_ssh_key
sed "s|@SSH_KEY@|$(cat "$SSH_KEY.pub")|" "$(sibling_dir)/vm/user-data" > "$build/seed/user-data"
printf 'instance-id: coredesktop-smoketest\nlocal-hostname: coredesktop-smoketest\n' \
> "$build/seed/meta-data"
xorriso -as mkisofs -quiet -output "$build/seed.iso" -volid cidata -joliet -rock \
"$build/seed/user-data" "$build/seed/meta-data" || die "Couldn't make the cloud-init disk."
(
vm_start "$build" "$build/disk.qcow2" -cdrom "$build/seed.iso"
trap vm_kill EXIT
wait_for_ssh
vm_ssh 'cloud-init status --wait' < /dev/null > /dev/null || true
vm_put "$(sibling_dir)/vm/provision.sh" /tmp/provision.sh || die "Couldn't copy provision.sh to the VM."
vm_exec 'sudo bash /tmp/provision.sh' || die "Provisioning failed; see $build/build.log"
vm_stop
) 2>&1 | tee "$build/build.log"
(( PIPESTATUS[0] == 0 )) || die "Building the base image failed; see $build/build.log"
mv "$build/disk.qcow2" "$IMAGES/base.qcow2"
# Built on the old base image
rm -f "$IMAGES/installed.qcow2" "$IMAGES/installed.ref"
rm -rf "$build"
echo "Built $IMAGES/base.qcow2"
}
# build_installed_image REF: the base image after install.sh at REF
build_installed_image() {
local ref sha build="$SMOKETEST_STATE/build-installed"
ref="$1"
sha="$(git -C "$WORKING_COPY" rev-parse --verify --quiet "$ref^{commit}")" || die "No such commit: $ref"
require_vm_tools
[ -f "$IMAGES/base.qcow2" ] || build_base_image
rm -rf "$build"
mkdir -p "$build"
echo "Building the installed image at $(git -C "$WORKING_COPY" log -1 --format='%h %s' "$sha");" \
"this takes an hour or more. Log: $build/build.log"
qemu-img create -q -f qcow2 -b "$IMAGES/base.qcow2" -F qcow2 "$build/disk.qcow2"
(
export SMOKETEST_RESULTS="$build" SMOKETEST_SHARED="$build" SMOKETEST_REF="$sha"
IMAGE=base
source "$WORKING_COPY/smoketest/lib/scenario.sh"
vm_start "$build" "$build/disk.qcow2"
trap vm_kill EXIT
vm_wait_up
smoketest_install_image "$sha"
# The README says to reboot after installing
vm_reboot
vm_stop
) 2>&1 | tee "$build/build.log"
(( PIPESTATUS[0] == 0 )) || die "Building the installed image failed; logs in $build"
mv "$build/disk.qcow2" "$IMAGES/installed.qcow2"
echo "$sha" > "$IMAGES/installed.ref"
echo "Built $IMAGES/installed.qcow2"
}
smoketest_images() {
case "${1:-}" in
"")
local image
for image in base installed; do
if [ -f "$IMAGES/$image.qcow2" ]; then
printf '%-10s built %s' "$image" "$(date -r "$IMAGES/$image.qcow2" '+%Y-%m-%d %H:%M')"
[ "$image" = installed ] \
&& printf ', at %s' "$(git -C "$WORKING_COPY" log -1 --format='%h %s' "$(cat "$IMAGES/installed.ref")" 2> /dev/null)"
echo
else
printf '%-10s not built\n' "$image"
fi
done
;;
base) build_base_image ;;
installed) build_installed_image "${2:-$(default_installed_ref)}" ;;
*) smoketest_usage >&2; return 1 ;;
esac
}
smoketest_shell() {
local image="${1:-installed}" dir
[[ "$image" == base || "$image" == installed ]] || die "The image is base or installed, not $image."
require_vm_tools
[ -f "$IMAGES/$image.qcow2" ] || die "The $image image isn't built: coredesktop-dev smoketest images $image"
dir="$SMOKETEST_STATE/shell"
rm -rf "$dir"
mkdir -p "$dir"
qemu-img create -q -f qcow2 -b "$IMAGES/$image.qcow2" -F qcow2 "$dir/disk.qcow2"
vm_start "$dir" "$dir/disk.qcow2"
trap 'vm_kill; rm -rf "$dir"' EXIT
echo "Starting a throwaway copy of the $image image..."
vm_wait_up
vm_ssh -t
}
smoketest_clean() {
case "${1:-}" in
"") rm -rf "$RUNS" && echo "Deleted $RUNS" ;;
--all) rm -rf "$SMOKETEST_STATE" && echo "Deleted $SMOKETEST_STATE" ;;
*) smoketest_usage >&2; return 1 ;;
esac
}