356 lines
12 KiB
Bash
356 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"
|
|
# Expanded now: $dir is local, and gone by the time the script exits
|
|
# shellcheck disable=SC2064
|
|
trap "vm_kill; rm -rf $(printf '%q' "$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
|
|
}
|