first commit

This commit is contained in:
2026-11-04 09:43:05 -07:00
commit c33736775b
25 changed files with 2056 additions and 0 deletions
+13
View File
@@ -0,0 +1,13 @@
# Every tracked file (not symlink) that starts with a bash or sh shebang (\043 is "#", which
# make would otherwise read as the start of a comment)
SCRIPTS := $(shell git ls-files -s | awk '$$1 != 120000 { print $$4 }' | xargs awk 'FNR == 1 && /^#!.*[\/ ](ba)?sh/ { print FILENAME }')
.PHONY: check lint test
check: lint test
lint:
shellcheck --severity=warning $(SCRIPTS)
test:
bats tests
+91
View File
@@ -0,0 +1,91 @@
# coredesktop-dev
Tools for working on [CoreDesktop](https://gitea.federated.computer/Federated_Computer/coredesktop)
itself. Only contributors need these, so they're a CoreDesktop sibling project
rather than part of CoreDesktop.
## Install
```
sibling add <url of this repo>
```
It asks where your working copy of CoreDesktop is (default
`~/Projects/coredesktop`), and offers to clone it there. CoreDesktop's `update`
keeps this project current from then on. To change the working copy, run
`sibling add` again.
It installs shellcheck, bats, tea (Gitea's CLI), QEMU and xorriso.
## Why: the two copies of CoreDesktop
A developer's machine has two copies of CoreDesktop:
- **Deployed**, in `~/.local/share/coredesktop`: what the machine runs, and
what `update` pulls (fast-forward only). Never edit it: a changed tracked
file stops `update` from pulling.
- **Working**, e.g. `~/Projects/coredesktop`: where you make changes. Nothing
runs from here unless you run it.
These commands let you test the working copy without touching the deployed one.
## Commands
| Command | |
|---------|---|
| `coredesktop-dev doctor` | Check the tools, the working copy, and that the deployed copy can still update |
| `coredesktop-dev check [--fast]` | `make lint` and `make test` in the working copy, plus shellcheck on scripts not committed yet. `--fast`: shellcheck only |
| `coredesktop-dev try [--keep] [cmd...]` | Run the working copy's code as if it were installed, in a throwaway home with its own D-Bus session and no display, e.g. `coredesktop-dev try configure --list`. sudo, yay, pacman and systemctl still change this machine |
| `coredesktop-dev deployed [status\|restore]` | Show the deployed copy's state, or undo local changes to it (after asking) |
| `coredesktop-dev hooks [status\|install\|remove]` | A pre-push hook in the working copy that runs `check` first. `git push --no-verify` skips it |
| `coredesktop-dev smoketest ...` | Test install and update on virtual machines (below) |
Each takes `--help`. In a Flatpak (an editor's built-in terminal, say),
coredesktop-dev runs itself on the host with `flatpak-spawn --host`.
## Smoketest
Runs CoreDesktop's end-to-end scenarios, in `smoketest/` in the working copy,
on throwaway QEMU virtual machines: a fresh install, updates of all kinds,
failing migrations and hooks, low disk space, a month-old machine, and more.
See `smoketest/README.md` in CoreDesktop for what's tested and how to add a
scenario.
```
coredesktop-dev smoketest list # the scenarios
coredesktop-dev smoketest run # the quick ones (tier pr), against HEAD
coredesktop-dev smoketest run --tier all # everything; hours
coredesktop-dev smoketest run 40 43 # just these
coredesktop-dev smoketest run --keep 40 # on failure, leave the VM up to look around
coredesktop-dev smoketest shell installed # SSH into a throwaway VM
```
What it needs: KVM (virtualization turned on in the firmware), about 8 GB of
RAM and 4 CPUs per VM (`SMOKETEST_MEMORY`, `SMOKETEST_CPUS`), and disk space in
`~/.cache/coredesktop-dev/smoketest`: VM disks grow as large as a real install,
about 50 GB, and each run's VM disk is deleted when it's done.
The first run builds two images, which takes a while:
- **base**: Arch's official cloud image, with GNOME added the way archinstall's
desktop profile installs it. The user `tester` (password `tester`) logs in to
GNOME automatically and has passwordless sudo.
- **installed**: base after CoreDesktop's `install.sh`, at the commit your
branch started from. Scenarios on this image first `update` it to the commit
under test. Rebuild it now and then (`smoketest images installed`) so that
update has less to catch up on.
Downloaded Arch packages are kept in the cache folder and shared with every
VM, so they download once. Flatpaks aren't cached.
To watch a VM's screen, set `SMOKETEST_VNC=1` and connect a VNC viewer to
`127.0.0.1:5901`.
## Developing this project
```
make check # shellcheck + bats
```
The tests use fake commands, like CoreDesktop's: nothing touches the machine,
and the smoketest tests use a fake QEMU.
+46
View File
@@ -0,0 +1,46 @@
#!/bin/bash
#
# Purpose : Tools for working on CoreDesktop itself (see README.md)
SELF="$(readlink -f "${BASH_SOURCE[0]}")"
LIB="$(dirname "$SELF")/../lib"
source "$LIB/common.sh"
usage() {
cat << EOF
Usage: coredesktop-dev <command> [--help]
doctor Check this machine is set up to work on CoreDesktop
check Run the linters and tests on the working copy
try Run the working copy's code as if it were installed, in a throwaway home
deployed Show, or restore, the installed copy of CoreDesktop
hooks Install or remove the pre-push hook that runs check
smoketest Test install and update on a virtual machine
EOF
}
command="${1:-}"
shift
case "$command" in
doctor | check | try | deployed | hooks | smoketest) ;;
"" | -h | --help | help)
usage
exit 0
;;
*)
usage >&2
exit 1
;;
esac
# In a Flatpak (an editor's terminal, say), pacman, systemctl, qemu and the
# host's tools aren't there: run on the host instead
if in_flatpak && [[ "${1:-}" != -h && "${1:-}" != --help ]]; then
echo "Running in a Flatpak; running coredesktop-dev $command on the host (flatpak-spawn --host)." >&2
exec flatpak-spawn --host --env=COREDESKTOP_DEV_VIA_FLATPAK=1 "$SELF" "$command" "$@"
fi
source "$LIB/$command.sh"
"cmd_$command" "$@"
Executable
+18
View File
@@ -0,0 +1,18 @@
#!/bin/bash
# Installed by coredesktop-dev; remove with: coredesktop-dev hooks remove
#
# Runs coredesktop-dev check before git push, and stops the push if it fails.
CHECK="$HOME/.local/bin/coredesktop-dev"
if [ ! -x "$CHECK" ]; then
echo "pre-push: coredesktop-dev isn't installed, so the checks didn't run."
exit 0
fi
if ! "$CHECK" check; then
echo
echo "pre-push: the checks failed, so nothing was pushed."
echo "To push anyway: git push --no-verify"
exit 1
fi
+77
View File
@@ -0,0 +1,77 @@
#!/bin/bash
#
# Purpose : Install coredesktop-dev (run by CoreDesktop's `sibling add`). Safe to
# re-run: that's how to change the working copy or repair the install.
set -e
SELF="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
COREDESKTOP="$HOME/.local/share/coredesktop"
if [ ! -f "$COREDESKTOP/utils.sh" ]; then
echo "CoreDesktop isn't installed in $COREDESKTOP; install it first."
exit 1
fi
source "$COREDESKTOP/utils.sh"
source "$SELF/lib/common.sh"
load_config
default="${WORKING_COPY:-$HOME/Projects/coredesktop}"
# Ask until the answer is a CoreDesktop clone, or a folder to clone one into
while true; do
if ! read -rp "Your working copy of CoreDesktop, where you make changes [$default]: " answer; then
echo
die "No answer; run this again in a terminal: bash $SELF/install.sh"
fi
dir="${answer:-$default}"
dir="${dir/#\~/$HOME}"
dir="$(realpath -m "$dir")"
if is_deployed_copy "$dir"; then
echo "$dir is the deployed copy, which update keeps current. Changing it stops"
echo "update from pulling. Choose another folder, e.g. ~/Projects/coredesktop."
continue
fi
if is_coredesktop_clone "$dir"; then
origin="$(git -C "$dir" remote get-url origin 2> /dev/null || true)"
if [[ "$origin" != *coredesktop* ]]; then
echo "Note: $dir's origin ($origin) doesn't look like CoreDesktop's repo."
fi
break
fi
if [ -e "$dir" ] && [ -n "$(ls -A "$dir" 2> /dev/null)" ]; then
echo "$dir isn't a clone of CoreDesktop, and isn't empty. Choose another folder."
continue
fi
url="$(git -C "$COREDESKTOP" remote get-url origin)"
read -rp "Clone CoreDesktop from $url into $dir? [Y/n] " answer || answer=n
if [[ "$answer" != [nN]* ]]; then
git clone "$url" "$dir"
break
fi
done
save_config "$dir"
WORKING_COPY="$dir"
echo "Working copy: $dir (saved in $CONFIG)"
bash "$SELF/update.sh"
source "$SELF/lib/hooks.sh"
if ! hook_is_ours "$(hook_path)"; then
echo
echo "A pre-push hook can run coredesktop-dev check before each git push from"
echo "$dir, and stop the push if it fails."
if confirm "Install it?"; then
"$HOME/.local/bin/coredesktop-dev" hooks install || true
fi
fi
echo
"$HOME/.local/bin/coredesktop-dev" doctor || true
echo
echo "coredesktop-dev installed. Run coredesktop-dev --help to see what it does."
+63
View File
@@ -0,0 +1,63 @@
#!/bin/bash
#
# Purpose : coredesktop-dev check: lint and test the working copy, including
# scripts not committed yet, which CoreDesktop's make lint skips
check_usage() {
cat << EOF
Usage: coredesktop-dev check [--fast]
Runs make lint and make test in the working copy, then shellcheck on the
scripts git doesn't track yet. --fast runs only the shellcheck steps.
EOF
}
# Print each file named on stdin (NUL-separated) that starts with a bash or sh
# shebang, as the Makefile picks them, or is a .sh file
shell_scripts() {
local file
while IFS= read -r -d '' file; do
[ -f "$file" ] && [ ! -L "$file" ] || continue
if [[ "$file" == *.sh ]] || head -n 1 "$file" | grep -qE '^#!.*[/ ](ba)?sh'; then
echo "$file"
fi
done
}
cmd_check() {
local fast=0
case "${1:-}" in
-h | --help) check_usage; return 0 ;;
--fast) fast=1 ;;
"") ;;
*) check_usage >&2; return 1 ;;
esac
require_working_copy
cd "$WORKING_COPY" || return 1
local status=0 tracked untracked lint="ok" bats="skipped (--fast)"
mapfile -t tracked < <(git ls-files -z | shell_scripts)
mapfile -t untracked < <(git ls-files -z --others --exclude-standard | shell_scripts)
echo "== make lint"
make --no-print-directory lint || { lint="FAILED"; status=1; }
if (( ${#untracked[@]} > 0 )); then
echo "== shellcheck on untracked scripts: ${untracked[*]}"
shellcheck --severity=warning "${untracked[@]}" || { lint="FAILED"; status=1; }
fi
if (( fast == 0 )); then
echo "== make test"
if make --no-print-directory test; then
bats="ok ($(bats --count tests 2> /dev/null) tests)"
else
bats="FAILED"
status=1
fi
fi
echo
echo "shellcheck: $lint (${#tracked[@]} files, ${#untracked[@]} untracked) bats: $bats"
return $status
}
+67
View File
@@ -0,0 +1,67 @@
#!/bin/bash
#
# Purpose : Paths, settings and helpers shared by coredesktop-dev's scripts
# The deployed copy: the machine's running CoreDesktop, which update pulls
COREDESKTOP="$HOME/.local/share/coredesktop"
CONFIG_DIR="$HOME/.config/coredesktop-dev"
CONFIG="$CONFIG_DIR/config"
# First line after the shebang of the pre-push hook this project installs, so it
# never overwrites or removes a hook someone else wrote
HOOK_MARKER="# Installed by coredesktop-dev; remove with: coredesktop-dev hooks remove"
die() {
echo "$*" >&2
exit 1
}
confirm() {
local answer
read -rp "$1 [y/N] " answer
[[ "$answer" == [yY]* ]]
}
# Sets WORKING_COPY from the settings file (empty if there is none)
load_config() {
WORKING_COPY=""
# shellcheck source=/dev/null
[ -f "$CONFIG" ] && source "$CONFIG"
return 0
}
save_config() {
mkdir -p "$CONFIG_DIR"
printf 'WORKING_COPY=%q\n' "$1" > "$CONFIG"
}
# True if DIR is the deployed copy, however it's spelled
is_deployed_copy() {
[ -e "$1" ] && [ "$(realpath "$1")" = "$(realpath "$COREDESKTOP" 2> /dev/null)" ]
}
# True if DIR is the top of a git clone of CoreDesktop
is_coredesktop_clone() {
[ -d "$1" ] \
&& [ "$(git -C "$1" rev-parse --show-toplevel 2> /dev/null)" = "$(realpath "$1")" ] \
&& [ -f "$1/update" ] && [ -d "$1/updates.d" ]
}
# Load the settings and stop unless they point at a CoreDesktop clone
require_working_copy() {
load_config
[ -n "$WORKING_COPY" ] \
|| die "No working copy is set. Run $(sibling_dir)/install.sh, or: sibling add <coredesktop-dev url>"
is_coredesktop_clone "$WORKING_COPY" \
|| die "The working copy, $WORKING_COPY, isn't a clone of CoreDesktop (set in $CONFIG)."
}
in_flatpak() {
[ -f /.flatpak-info ] || [ -n "${FLATPAK_ID:-}" ]
}
# This project's folder
sibling_dir() {
cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd
}
+79
View File
@@ -0,0 +1,79 @@
#!/bin/bash
#
# Purpose : coredesktop-dev deployed: show the deployed copy of CoreDesktop
# (~/.local/share/coredesktop), or put back its tracked files so
# update can pull again
deployed_usage() {
cat << EOF
Usage: coredesktop-dev deployed [status|restore]
status Branch, commit, how far behind upstream, and local changes (default)
restore Undo local changes to tracked files, after asking. Untracked files
are listed and left alone.
EOF
}
cmd_deployed() {
case "${1:-status}" in
-h | --help) deployed_usage; return 0 ;;
status | restore) ;;
*) deployed_usage >&2; return 1 ;;
esac
git -C "$COREDESKTOP" rev-parse --git-dir &> /dev/null \
|| die "CoreDesktop isn't installed in $COREDESKTOP."
"deployed_${1:-status}"
}
deployed_status() {
local behind changes
echo "Deployed copy: $COREDESKTOP"
echo "Branch: $(git -C "$COREDESKTOP" rev-parse --abbrev-ref HEAD)"
echo "Commit: $(git -C "$COREDESKTOP" log -1 --format='%cd (%h) %s' --date=format:%Y.%m.%d)"
if GIT_TERMINAL_PROMPT=0 timeout 30 git -C "$COREDESKTOP" fetch --quiet &> /dev/null \
&& behind="$(git -C "$COREDESKTOP" rev-list --count 'HEAD..@{upstream}' 2> /dev/null)"; then
echo "Behind: $behind commits (update pulls them)"
else
echo "Behind: unknown (couldn't reach upstream)"
fi
changes="$(git -C "$COREDESKTOP" status --porcelain --untracked-files=no)"
if [ -z "$changes" ]; then
echo "Changes: none"
else
echo "Changes: these stop update from pulling (coredesktop-dev deployed restore):"
echo "$changes" | sed 's/^/ /'
fi
}
deployed_restore() {
local untracked
if [ -z "$(git -C "$COREDESKTOP" status --porcelain --untracked-files=no)" ]; then
echo "The deployed copy has no local changes."
return 0
fi
git -C "$COREDESKTOP" diff HEAD --stat
untracked="$(git -C "$COREDESKTOP" ls-files --others --exclude-standard)"
if [ -n "$untracked" ]; then
echo
echo "Untracked files, which are left alone:"
echo "$untracked" | sed 's/^/ /'
fi
echo
if ! confirm "Undo the changes above in $COREDESKTOP?"; then
echo "Nothing changed."
return 1
fi
git -C "$COREDESKTOP" checkout HEAD -- . || return 1
if [ -n "$(git -C "$COREDESKTOP" status --porcelain --untracked-files=no)" ]; then
echo "Restored, but some changes remain (new files added to git's index?):"
git -C "$COREDESKTOP" status --short --untracked-files=no
return 1
fi
echo "Restored. update can pull again."
}
+113
View File
@@ -0,0 +1,113 @@
#!/bin/bash
#
# Purpose : coredesktop-dev doctor: check this machine is set up to work on
# CoreDesktop. Read-only.
doctor_usage() {
cat << EOF
Usage: coredesktop-dev doctor
Checks the tools are installed, the working copy is set up, and the deployed
copy (~/.local/share/coredesktop) can still update. Exits 1 if anything needs
fixing.
EOF
}
PROBLEMS=0
ok() {
echo "ok $1"
}
problem() {
echo "PROBLEM $1"
shift
local line
for line in "$@"; do
echo " $line"
done
PROBLEMS=1
}
note() {
echo "note $1"
}
cmd_doctor() {
case "${1:-}" in
-h | --help) doctor_usage; return 0 ;;
"") ;;
*) doctor_usage >&2; return 1 ;;
esac
local tool missing=()
for tool in shellcheck bats tea qemu-system-x86_64 qemu-img xorriso; do
command -v "$tool" &> /dev/null || missing+=("$tool")
done
if (( ${#missing[@]} == 0 )); then
ok "shellcheck, bats, tea, QEMU and xorriso are installed"
else
problem "not installed: ${missing[*]}" "fix: bash $(sibling_dir)/update.sh"
fi
if [ -r /dev/kvm ] && [ -w /dev/kvm ]; then
ok "KVM is available for the smoketest's virtual machines"
else
problem "can't use /dev/kvm, so the smoketest can't run VMs" \
"fix: turn on virtualization (VT-x/AMD-V) in the firmware settings"
fi
load_config
if [ -z "$WORKING_COPY" ]; then
problem "no working copy is set" "fix: bash $(sibling_dir)/install.sh"
elif is_coredesktop_clone "$WORKING_COPY"; then
ok "working copy: $WORKING_COPY"
else
problem "the working copy, $WORKING_COPY, isn't a clone of CoreDesktop" \
"fix: bash $(sibling_dir)/install.sh"
fi
doctor_deployed
if [ -n "${COREDESKTOP_DEV_VIA_FLATPAK:-}" ] || in_flatpak; then
note "you ran this in a Flatpak, which has no pacman, systemctl or host /tmp;
coredesktop-dev runs itself on the host for you (flatpak-spawn --host)"
fi
if [ -n "$WORKING_COPY" ] && hook_is_ours "$(hook_path)"; then
ok "the pre-push hook is installed"
else
note "the pre-push hook isn't installed (coredesktop-dev hooks install)"
fi
return $PROBLEMS
}
doctor_deployed() {
if ! git -C "$COREDESKTOP" rev-parse --git-dir &> /dev/null; then
problem "CoreDesktop isn't installed in $COREDESKTOP"
return
fi
local changes
changes="$(git -C "$COREDESKTOP" status --porcelain --untracked-files=no)"
if [ -z "$changes" ]; then
ok "the deployed copy has no local changes"
else
problem "the deployed copy has local changes, so update can't pull:" \
"${changes//$'\n'/$'\n' }" \
"fix: coredesktop-dev deployed restore"
fi
if ! GIT_TERMINAL_PROMPT=0 timeout 30 git -C "$COREDESKTOP" fetch --quiet &> /dev/null; then
problem "couldn't reach the deployed copy's upstream to check it can update"
elif git -C "$COREDESKTOP" merge-base --is-ancestor HEAD '@{upstream}' 2> /dev/null; then
ok "the deployed copy can fast-forward to its upstream"
else
problem "the deployed copy has commits its upstream doesn't, so update's pull will fail" \
"see: git -C $COREDESKTOP log @{upstream}..HEAD"
fi
}
# hooks.sh has hook_path and hook_is_ours
source "$(dirname "${BASH_SOURCE[0]}")/hooks.sh"
+70
View File
@@ -0,0 +1,70 @@
#!/bin/bash
#
# Purpose : coredesktop-dev hooks: install or remove the working copy's pre-push
# hook, which runs coredesktop-dev check before each push
hooks_usage() {
cat << EOF
Usage: coredesktop-dev hooks [status|install|remove]
status Show whether the pre-push hook is installed (the default)
install Run coredesktop-dev check before each git push from the working copy
remove Remove that hook
git push --no-verify skips the hook for one push. A pre-push hook that
coredesktop-dev didn't install is never replaced or removed.
EOF
}
# Where git looks for the working copy's pre-push hook (core.hooksPath aware)
hook_path() {
local path
path="$(git -C "$WORKING_COPY" rev-parse --git-path hooks/pre-push)" || return 1
[[ "$path" == /* ]] || path="$WORKING_COPY/$path"
echo "$path"
}
hook_is_ours() {
[ -f "$1" ] && grep -qxF "$HOOK_MARKER" "$1"
}
cmd_hooks() {
local hook
case "${1:-status}" in
-h | --help) hooks_usage; return 0 ;;
status | install | remove) ;;
*) hooks_usage >&2; return 1 ;;
esac
require_working_copy
hook="$(hook_path)" || die "Couldn't find the working copy's git hooks folder."
case "${1:-status}" in
status)
if hook_is_ours "$hook"; then
echo "The pre-push hook is installed: $hook"
elif [ -e "$hook" ]; then
echo "$hook is someone else's hook; coredesktop-dev leaves it alone."
else
echo "The pre-push hook isn't installed. To install it: coredesktop-dev hooks install"
fi
;;
install)
if [ -e "$hook" ] && ! hook_is_ours "$hook"; then
die "$hook already exists and coredesktop-dev didn't write it, so it's left alone."
fi
mkdir -p "$(dirname "$hook")"
install -m 755 "$(sibling_dir)/hooks/pre-push" "$hook"
echo "Installed $hook: each git push now runs coredesktop-dev check first."
;;
remove)
if hook_is_ours "$hook"; then
rm "$hook"
echo "Removed $hook."
elif [ -e "$hook" ]; then
die "$hook isn't coredesktop-dev's, so it's left alone."
else
echo "The pre-push hook isn't installed."
fi
;;
esac
}
+353
View File
@@ -0,0 +1,353 @@
#!/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
}
+71
View File
@@ -0,0 +1,71 @@
#!/bin/bash
#
# Purpose : coredesktop-dev try: run the working copy's code as if it were the
# deployed CoreDesktop, in a throwaway home with its own session
try_usage() {
cat << EOF
Usage: coredesktop-dev try [--keep] [command...]
Runs command (or a shell) with a throwaway HOME where
~/.local/share/coredesktop is the working copy, and update, configure, version
etc. in ~/.local/bin are the working copy's. It gets its own XDG_RUNTIME_DIR
and D-Bus session, and no display, so it can't touch your desktop session.
sudo, yay, pacman and systemctl still change this machine.
--keep Keep the throwaway folder afterwards, and print where it is
Example: coredesktop-dev try configure --list
EOF
}
TRY_COMMANDS=(update refresh sibling version configure audit)
cmd_try() {
local keep=0
while [ $# -gt 0 ]; do
case "$1" in
-h | --help) try_usage; return 0 ;;
--keep) keep=1; shift ;;
--) shift; break ;;
*) break ;;
esac
done
require_working_copy
local tmp home runtime cmd status
tmp="$(mktemp -d "${TMPDIR:-/tmp}/coredesktop-dev-try.XXXXXX")" || return 1
home="$tmp/home"
runtime="$tmp/runtime"
mkdir -p "$home/.local/share" "$home/.local/bin" "$home/.local/state" \
"$home/.config" "$home/.cache"
mkdir -m 700 "$runtime"
ln -s "$WORKING_COPY" "$home/.local/share/coredesktop"
for cmd in "${TRY_COMMANDS[@]}"; do
ln -s "$WORKING_COPY/$cmd" "$home/.local/bin/$cmd"
done
echo "coredesktop-dev try: HOME is $home, with the working copy as CoreDesktop." >&2
echo "Careful: sudo, yay, pacman and systemctl still change this machine." >&2
(( $# > 0 )) || set -- bash
env -u DISPLAY -u WAYLAND_DISPLAY -u DBUS_SESSION_BUS_ADDRESS -u SSH_AUTH_SOCK \
HOME="$home" \
XDG_CONFIG_HOME="$home/.config" \
XDG_DATA_HOME="$home/.local/share" \
XDG_STATE_HOME="$home/.local/state" \
XDG_CACHE_HOME="$home/.cache" \
XDG_RUNTIME_DIR="$runtime" \
PATH="$home/.local/bin:$PATH" \
dbus-run-session -- "$@"
status=$?
if (( keep )); then
echo "Kept $tmp" >&2
else
rm -rf "$tmp"
fi
return $status
}
+182
View File
@@ -0,0 +1,182 @@
#!/bin/bash
#
# Purpose : Start, reach and stop the smoketest's QEMU virtual machines. Defines
# what CoreDesktop's smoketest/lib/scenario.sh expects of a driver:
# vm_exec, vm_put, vm_reboot, VM_GIT_SSH and VM_GIT_URL.
SMOKETEST_STATE="${XDG_CACHE_HOME:-$HOME/.cache}/coredesktop-dev/smoketest"
IMAGES="$SMOKETEST_STATE/images"
# Shared with every VM as its /var/cache/pacman/pkg, so packages download once
PKG_CACHE="$SMOKETEST_STATE/pkg"
SSH_KEY="$SMOKETEST_STATE/id_ed25519"
KVM_DEVICE="${KVM_DEVICE:-/dev/kvm}"
VM_USER=tester
VM_MEMORY="${SMOKETEST_MEMORY:-8G}"
VM_CPUS="${SMOKETEST_CPUS:-4}"
# Longest a single command in the VM may run (an install takes an hour or more)
VM_EXEC_TIMEOUT="${SMOKETEST_EXEC_TIMEOUT:-4h}"
# Seconds to wait for the VM to answer on SSH, and for GNOME to start
VM_SSH_TIMEOUT="${SMOKETEST_SSH_TIMEOUT:-900}"
VM_DESKTOP_TIMEOUT="${SMOKETEST_DESKTOP_TIMEOUT:-300}"
SSH_OPTS=(
-i "$SSH_KEY"
-o IdentitiesOnly=yes
-o StrictHostKeyChecking=no
-o UserKnownHostsFile=/dev/null
-o LogLevel=ERROR
-o BatchMode=yes
-o ConnectTimeout=10
-o ServerAliveInterval=15
-o ServerAliveCountMax=8
)
# Commands in the VM run as they would in a GNOME Terminal: with the GNOME
# session's environment, in an interactive bash that has read ~/.bashrc
VM_SESSION_ENV='eval "$(systemctl --user show-environment 2> /dev/null | sed "s/^/export /")"
export TERM="${TERM:-dumb}"'
ensure_ssh_key() {
mkdir -p "$SMOKETEST_STATE"
[ -f "$SSH_KEY" ] || ssh-keygen -q -t ed25519 -N '' -C coredesktop-smoketest -f "$SSH_KEY"
}
free_port() {
local port i
for i in $(seq 50); do
port=$((20000 + RANDOM % 20000))
[ -z "$(ss -Hltn "( sport = :$port )" 2> /dev/null)" ] && { echo "$port"; return 0; }
done
return 1
}
# vm_start DIR DISK [QEMU ARGS...]: boot DISK in the background. DIR gets the
# pid file and the serial console's log.
vm_start() {
VM_DIR="$1"
local disk="$2"
shift 2
local display=(-display none)
[ -n "${SMOKETEST_VNC:-}" ] && display+=(-vnc "127.0.0.1:$SMOKETEST_VNC")
ensure_ssh_key
mkdir -p "$VM_DIR" "$PKG_CACHE"
VM_PORT="$(free_port)" || die "Couldn't find a free port for the VM's SSH."
qemu-system-x86_64 \
-name coredesktop-smoketest \
-machine q35,accel=kvm \
-cpu host \
-smp "$VM_CPUS" \
-m "$VM_MEMORY" \
-drive "file=$disk,if=virtio,format=qcow2,discard=unmap" \
-nic "user,model=virtio-net-pci,hostfwd=tcp:127.0.0.1:$VM_PORT-:22" \
-virtfs "local,path=$PKG_CACHE,mount_tag=pkgcache,security_model=mapped-xattr" \
-vga virtio \
"${display[@]}" \
-serial "file:$VM_DIR/serial.log" \
-pidfile "$VM_DIR/qemu.pid" \
-daemonize \
"$@" || die "QEMU didn't start."
VM_GIT_SSH="ssh$(printf ' %q' "${SSH_OPTS[@]}")"
VM_GIT_URL="ssh://$VM_USER@127.0.0.1:$VM_PORT"
export VM_GIT_SSH VM_GIT_URL
}
vm_pid() {
cat "$VM_DIR/qemu.pid" 2> /dev/null
}
vm_running() {
local pid
pid="$(vm_pid)" && [ -n "$pid" ] && kill -0 "$pid" 2> /dev/null
}
vm_ssh() {
ssh "${SSH_OPTS[@]}" -p "$VM_PORT" "$VM_USER@127.0.0.1" "$@"
}
# The command to SSH in by hand
vm_ssh_command() {
echo "ssh -i $SSH_KEY -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p $VM_PORT $VM_USER@127.0.0.1"
}
wait_for_ssh() {
local deadline=$((SECONDS + VM_SSH_TIMEOUT))
until vm_ssh true < /dev/null 2> /dev/null; do
vm_running || die "The VM stopped. Its console log: $VM_DIR/serial.log"
(( SECONDS < deadline )) || die "The VM didn't answer on SSH. Its console log: $VM_DIR/serial.log"
sleep 5
done
}
# Wait for the automatic GNOME login. Not fatal: the health checks catch a
# desktop that didn't start.
wait_for_desktop() {
local deadline=$((SECONDS + VM_DESKTOP_TIMEOUT))
until vm_ssh "pgrep -u $VM_USER -x gnome-shell" < /dev/null &> /dev/null; do
if (( SECONDS >= deadline )); then
echo "Warning: GNOME didn't start in the VM within ${VM_DESKTOP_TIMEOUT}s."
return 0
fi
sleep 5
done
}
vm_wait_up() {
wait_for_ssh
wait_for_desktop
}
# vm_exec SCRIPT: run SCRIPT with bash in the VM, as tester, in the GNOME
# session's environment. Interactive bash's complaints about having no
# terminal are dropped.
vm_exec() {
timeout "$VM_EXEC_TIMEOUT" ssh "${SSH_OPTS[@]}" -p "$VM_PORT" -T "$VM_USER@127.0.0.1" \
"bash -ic $(printf '%q' "$VM_SESSION_ENV
$1")" < /dev/null \
2> >(grep -v -e '^bash: cannot set terminal process group' -e '^bash: no job control in this shell' >&2)
}
# vm_put LOCAL REMOTE
vm_put() {
scp -q "${SSH_OPTS[@]}" -P "$VM_PORT" "$1" "$VM_USER@127.0.0.1:$2"
}
vm_boot_id() {
vm_ssh 'cat /proc/sys/kernel/random/boot_id' < /dev/null 2> /dev/null
}
vm_reboot() {
local before deadline=$((SECONDS + VM_SSH_TIMEOUT))
echo "== reboot"
before="$(vm_boot_id)"
vm_ssh 'sudo systemctl reboot' < /dev/null &> /dev/null || true
until [ "$(vm_boot_id)" != "$before" ] && [ -n "$(vm_boot_id)" ]; do
vm_running || die "The VM stopped while rebooting. Its console log: $VM_DIR/serial.log"
(( SECONDS < deadline )) || die "The VM didn't come back after rebooting. Its console log: $VM_DIR/serial.log"
sleep 5
done
wait_for_desktop
}
# Shut the VM down cleanly (so its disk can be kept), killing it if it hangs
vm_stop() {
vm_running || return 0
vm_ssh 'sudo systemctl poweroff' < /dev/null &> /dev/null || true
local deadline=$((SECONDS + 120))
while vm_running && (( SECONDS < deadline )); do
sleep 2
done
vm_kill
}
vm_kill() {
local pid
pid="$(vm_pid)" || return 0
[ -n "$pid" ] && kill "$pid" 2> /dev/null
rm -f "$VM_DIR/qemu.pid"
return 0
}
+59
View File
@@ -0,0 +1,59 @@
#!/usr/bin/env bats
load test_helper
setup() {
common_setup
make_working_copy
stub make
# Fails on any file that contains BAD, as if it had a warning
stub shellcheck 'for f in "$@"; do [ -f "$f" ] && grep -q BAD "$f" && exit 1; done; exit 0'
stub bats 'echo 7'
}
@test "check runs make lint and make test, and shellcheck on untracked scripts" {
printf '%s\n' '#!/bin/bash' 'echo fine' > "$WC/new-script"
printf '%s\n' 'echo fine' > "$WC/new-hook.sh"
echo "not a script" > "$WC/notes.txt"
run cdd check
[ "$status" -eq 0 ]
called make --no-print-directory lint
called make --no-print-directory test
grep -q "^shellcheck --severity=warning .*new-script" "$STUB_LOG"
grep -q "^shellcheck --severity=warning .*new-hook.sh" "$STUB_LOG"
[[ "$output" == *"shellcheck: ok ("*" files, 2 untracked) bats: ok (7 tests)"* ]]
run grep -q "^shellcheck .*notes.txt" "$STUB_LOG"
[ "$status" -eq 1 ]
}
@test "check fails on a warning in an untracked script" {
printf '%s\n' '#!/bin/bash' 'BAD' > "$WC/new-script"
run cdd check
[ "$status" -eq 1 ]
[[ "$output" == *"shellcheck: FAILED"* ]]
}
@test "check skips ignored files" {
echo "ignored-script" > "$WC/.gitignore"
printf '%s\n' '#!/bin/bash' 'BAD' > "$WC/ignored-script"
run cdd check
[ "$status" -eq 0 ]
}
@test "check --fast runs only the shellcheck steps" {
run cdd check --fast
[ "$status" -eq 0 ]
called make --no-print-directory lint
uncalled make --no-print-directory test
[[ "$output" == *"bats: skipped (--fast)"* ]]
}
@test "check fails when make test fails" {
stub make '[ "$2" != test ]'
run cdd check
[ "$status" -eq 1 ]
[[ "$output" == *"bats: FAILED"* ]]
}
+56
View File
@@ -0,0 +1,56 @@
#!/usr/bin/env bats
load test_helper
@test "deployed status shows the commit, how far behind, and local changes" {
make_working_copy
git -C "$WC" commit -q --allow-empty -m "new"
git -C "$WC" push -q origin main
echo "# edited" >> "$DEPLOYED/update"
run cdd deployed
[ "$status" -eq 0 ]
[[ "$output" == *"Branch: main"* ]]
[[ "$output" == *"CoreDesktop"* ]]
[[ "$output" == *"Behind: 1 commits"* ]]
[[ "$output" == *"M update"* ]]
}
@test "deployed restore asks first, and changes nothing on no" {
echo "# edited" >> "$DEPLOYED/update"
run cdd deployed restore <<< n
[ "$status" -eq 1 ]
[[ "$output" == *"Nothing changed."* ]]
grep -q "# edited" "$DEPLOYED/update"
}
@test "deployed restore puts back tracked files and leaves untracked ones" {
echo "# edited" >> "$DEPLOYED/update"
rm "$DEPLOYED/utils.sh"
echo "mine" > "$DEPLOYED/untracked-file"
run cdd deployed restore <<< y
[ "$status" -eq 0 ]
[[ "$output" == *"untracked-file"* ]]
[ -f "$DEPLOYED/utils.sh" ]
[ "$(cat "$DEPLOYED/untracked-file")" = mine ]
[ -z "$(git -C "$DEPLOYED" status --porcelain --untracked-files=no)" ]
run grep -q "# edited" "$DEPLOYED/update"
[ "$status" -eq 1 ]
}
@test "deployed restore puts back staged changes too" {
echo "# edited" >> "$DEPLOYED/update"
git -C "$DEPLOYED" add update
run cdd deployed restore <<< y
[ "$status" -eq 0 ]
[ -z "$(git -C "$DEPLOYED" status --porcelain --untracked-files=no)" ]
}
@test "deployed restore with nothing to restore doesn't ask" {
run cdd deployed restore < /dev/null
[ "$status" -eq 0 ]
[[ "$output" == *"no local changes"* ]]
}
+73
View File
@@ -0,0 +1,73 @@
#!/usr/bin/env bats
load test_helper
@test "doctor reports local changes in the deployed copy and how to fix them" {
make_working_copy
echo "# edited" >> "$DEPLOYED/update"
run cdd doctor
[ "$status" -eq 1 ]
[[ "$output" == *"PROBLEM the deployed copy has local changes"* ]]
[[ "$output" == *"M update"* ]]
[[ "$output" == *"coredesktop-dev deployed restore"* ]]
}
@test "doctor is happy with a clean deployed copy that can fast-forward" {
make_working_copy
# Upstream moves on; the deployed copy is behind, which is fine
git -C "$WC" commit -q --allow-empty -m "new"
git -C "$WC" push -q origin main
run cdd doctor
[[ "$output" == *"ok the deployed copy has no local changes"* ]]
[[ "$output" == *"ok the deployed copy can fast-forward to its upstream"* ]]
[[ "$output" == *"ok working copy: $WC"* ]]
}
@test "doctor reports a deployed copy with commits upstream doesn't have" {
make_working_copy
git -C "$DEPLOYED" commit -q --allow-empty -m "local commit"
run cdd doctor
[ "$status" -eq 1 ]
[[ "$output" == *"has commits its upstream doesn't"* ]]
}
@test "doctor reports a missing working copy" {
run cdd doctor
[ "$status" -eq 1 ]
[[ "$output" == *"PROBLEM no working copy is set"* ]]
}
@test "doctor reports when it was run from a Flatpak" {
make_working_copy
COREDESKTOP_DEV_VIA_FLATPAK=1 run cdd doctor
[[ "$output" == *"note you ran this in a Flatpak"* ]]
}
@test "doctor reports the pre-push hook" {
make_working_copy
run cdd doctor
[[ "$output" == *"the pre-push hook isn't installed"* ]]
cdd hooks install
run cdd doctor
[[ "$output" == *"ok the pre-push hook is installed"* ]]
}
@test "in a Flatpak, coredesktop-dev re-runs itself on the host" {
stub flatpak-spawn
FLATPAK_ID=com.vscodium.codium run cdd doctor
[ "$status" -eq 0 ]
called flatpak-spawn --host --env=COREDESKTOP_DEV_VIA_FLATPAK=1 \
"$(readlink -f "$PROJECT/bin/coredesktop-dev")" doctor
}
@test "--help works in a Flatpak without going to the host" {
stub flatpak-spawn
FLATPAK_ID=com.vscodium.codium run cdd doctor --help
[ "$status" -eq 0 ]
[[ "$output" == *"Usage: coredesktop-dev doctor"* ]]
not_called flatpak-spawn
}
+66
View File
@@ -0,0 +1,66 @@
#!/usr/bin/env bats
load test_helper
setup() {
common_setup
make_working_copy
HOOK="$WC/.git/hooks/pre-push"
}
@test "hooks install installs the pre-push hook" {
run cdd hooks install
[ "$status" -eq 0 ]
[ -x "$HOOK" ]
cmp "$PROJECT/hooks/pre-push" "$HOOK"
run cdd hooks
[[ "$output" == *"The pre-push hook is installed"* ]]
}
@test "hooks install won't overwrite someone else's hook" {
printf '%s\n' '#!/bin/bash' 'mine' > "$HOOK"
run cdd hooks install
[ "$status" -eq 1 ]
[[ "$output" == *"didn't write it"* ]]
[ "$(tail -n 1 "$HOOK")" = mine ]
}
@test "hooks remove removes only our hook" {
cdd hooks install
run cdd hooks remove
[ "$status" -eq 0 ]
[ ! -e "$HOOK" ]
printf '%s\n' '#!/bin/bash' 'mine' > "$HOOK"
run cdd hooks remove
[ "$status" -eq 1 ]
[ -e "$HOOK" ]
}
@test "hooks follows core.hooksPath" {
git -C "$WC" config core.hooksPath .githooks
run cdd hooks install
[ "$status" -eq 0 ]
[ -x "$WC/.githooks/pre-push" ]
}
@test "the hook stops a push when check fails, and says how to skip it" {
mkdir -p "$HOME/.local/bin"
printf '%s\n' '#!/bin/bash' 'exit 1' > "$HOME/.local/bin/coredesktop-dev"
chmod +x "$HOME/.local/bin/coredesktop-dev"
run bash "$PROJECT/hooks/pre-push"
[ "$status" -eq 1 ]
[[ "$output" == *"git push --no-verify"* ]]
}
@test "the hook lets a push through when check passes" {
mkdir -p "$HOME/.local/bin"
printf '%s\n' '#!/bin/bash' 'exit 0' > "$HOME/.local/bin/coredesktop-dev"
chmod +x "$HOME/.local/bin/coredesktop-dev"
run bash "$PROJECT/hooks/pre-push"
[ "$status" -eq 0 ]
}
+81
View File
@@ -0,0 +1,81 @@
#!/usr/bin/env bats
load test_helper
config() {
cat "$HOME/.config/coredesktop-dev/config"
}
@test "install refuses the deployed copy, then takes a working copy" {
git clone -q "$UPSTREAM" "$WC"
run bash "$PROJECT/install.sh" <<< "$(printf '%s\n' "$DEPLOYED" "$WC" n)"
[ "$status" -eq 0 ]
[[ "$output" == *"is the deployed copy"* ]]
[ "$(config)" = "WORKING_COPY=$(printf '%q' "$WC")" ]
[ "$(readlink "$HOME/.local/bin/coredesktop-dev")" = "$PROJECT/bin/coredesktop-dev" ]
called install_packages shellcheck bats tea qemu-base libisoburn
}
@test "install refuses the deployed copy however it's spelled" {
git clone -q "$UPSTREAM" "$WC"
run bash "$PROJECT/install.sh" <<< "$(printf '%s\n' '~/.local/share/coredesktop/' "$WC" n)"
[ "$status" -eq 0 ]
[[ "$output" == *"is the deployed copy"* ]]
[ "$(config)" = "WORKING_COPY=$(printf '%q' "$WC")" ]
}
@test "install offers to clone the working copy from the deployed copy's origin" {
run bash "$PROJECT/install.sh" <<< "$(printf '%s\n' "" y n)"
[ "$status" -eq 0 ]
[ "$(git -C "$WC" remote get-url origin)" = "$UPSTREAM" ]
[ "$(config)" = "WORKING_COPY=$(printf '%q' "$WC")" ]
}
@test "install won't use a folder that isn't a clone and isn't empty" {
mkdir -p "$HOME/other"
touch "$HOME/other/file"
git clone -q "$UPSTREAM" "$WC"
run bash "$PROJECT/install.sh" <<< "$(printf '%s\n' "$HOME/other" "$WC" n)"
[ "$status" -eq 0 ]
[[ "$output" == *"isn't a clone of CoreDesktop, and isn't empty"* ]]
[ "$(config)" = "WORKING_COPY=$(printf '%q' "$WC")" ]
}
@test "install is safe to re-run, and Enter keeps the saved working copy" {
# Somewhere other than the default, ~/Projects/coredesktop
other="$HOME/work/coredesktop"
git clone -q "$UPSTREAM" "$other"
run bash "$PROJECT/install.sh" <<< "$(printf '%s\n' "$other" n)"
[ "$status" -eq 0 ]
run bash "$PROJECT/install.sh" <<< "$(printf '%s\n' "" n)"
[ "$status" -eq 0 ]
[ "$(config)" = "WORKING_COPY=$(printf '%q' "$other")" ]
[ ! -e "$WC" ]
}
@test "install stops without an answer instead of looping" {
run bash "$PROJECT/install.sh" < /dev/null
[ "$status" -eq 1 ]
[[ "$output" == *"No answer"* ]]
[ ! -f "$HOME/.config/coredesktop-dev/config" ]
}
@test "install offers the pre-push hook, and installs it on yes" {
git clone -q "$UPSTREAM" "$WC"
run bash "$PROJECT/install.sh" <<< "$(printf '%s\n' "$WC" y)"
[ "$status" -eq 0 ]
grep -qxF "# Installed by coredesktop-dev; remove with: coredesktop-dev hooks remove" \
"$WC/.git/hooks/pre-push"
}
@test "install stops if CoreDesktop isn't installed" {
rm -rf "$DEPLOYED"
run bash "$PROJECT/install.sh" <<< ""
[ "$status" -eq 1 ]
[[ "$output" == *"CoreDesktop isn't installed"* ]]
}
+169
View File
@@ -0,0 +1,169 @@
#!/usr/bin/env bats
load test_helper
# The driver with fake QEMU: "VMs" are sleep processes, and ssh runs its command
# right here. The working copy has a small fake smoketest of its own.
setup() {
common_setup
make_working_copy
export KVM_DEVICE="$BATS_TEST_TMPDIR/kvm"
touch "$KVM_DEVICE"
export SMOKETEST_DESKTOP_TIMEOUT=0 SMOKETEST_SSH_TIMEOUT=5
STATE="$HOME/.cache/coredesktop-dev/smoketest"
mkdir -p "$STATE/images"
touch "$STATE/images/base.qcow2" "$STATE/images/installed.qcow2"
git -C "$WC" rev-parse HEAD > "$STATE/images/installed.ref"
# A VM is a sleep that lets go of bats' output pipes
stub qemu-system-x86_64 'while [ $# -gt 0 ]; do [ "$1" = -pidfile ] && pidfile="$2"; shift; done
/usr/bin/sleep 300 > /dev/null 2>&1 3>&- &
echo $! > "$pidfile"'
stub qemu-img 'touch "${@: -1}"'
stub xorriso
stub curl
stub ss
stub ssh-keygen 'touch "${@: -1}" "${@: -1}.pub"'
stub systemctl
stub ssh 'while [ $# -gt 0 ]; do
case "$1" in -i | -o | -p) shift 2 ;; *@*) shift; break ;; *) shift ;; esac
done
[ $# -gt 0 ] || exit 0
cd "$HOME" && exec bash -c "$*"'
stub scp 'dest="${*: -1}"; cp "${@: -2:1}" "${dest#*:}"'
SC="$WC/smoketest/scenarios"
mkdir -p "$SC" "$WC/smoketest/lib"
cat > "$WC/smoketest/lib/scenario.sh" << 'EOF'
SKIP_STATUS=3
smoketest_prepare() { echo "prepare $IMAGE $SMOKETEST_REF $IMAGE_REF"; }
fail() { echo "FAIL: $*"; exit 1; }
skip() { echo "SKIP: $*"; exit 3; }
EOF
scenario_file 10-passes pr installed 'vm_exec "echo in the vm"; echo "ref=$SMOKETEST_REF"'
scenario_file 20-fails pr base 'false; echo "still going"'
scenario_file 30-variants nightly base 'echo "variant $VARIANT"' 'VARIANTS=(one two)'
scenario_file 40-skips pr base 'skip "not today"'
}
# scenario_file NAME TIER IMAGE BODY [EXTRA]
scenario_file() {
printf '%s\n' "DESCRIPTION=\"$1 does things\"" "TIER=$2" "IMAGE=$3" "${5:-}" \
"scenario() {" "$4" "}" > "$SC/$1.sh"
}
last_run() {
ls -d "$STATE/runs/"* | tail -n 1
}
@test "list shows each scenario's tier, image and description" {
run cdd smoketest list
[ "$status" -eq 0 ]
[[ "$output" == *"10-passes"*"pr"*"installed"*"10-passes does things"* ]]
[[ "$output" == *"30-variants"*"nightly"*"base"*"(2 variants)"* ]]
}
@test "run runs the pr scenarios against HEAD and reports each" {
run cdd smoketest run
[ "$status" -eq 1 ]
[[ "$output" == *"PASS 10-passes"* ]]
[[ "$output" == *"FAIL 20-fails"* ]]
[[ "$output" == *"SKIP 40-skips"* ]]
[[ "$output" != *"30-variants"* ]]
[[ "$output" == *"in the vm"* ]]
[[ "$output" == *"ref=$(git -C "$WC" rev-parse HEAD)"* ]]
[ -f "$(last_run)/summary" ]
}
@test "a scenario stops at its first failing command" {
run cdd smoketest run 20
[ "$status" -eq 1 ]
[[ "$output" != *"still going"* ]]
}
@test "run picks scenarios by name or number" {
run cdd smoketest run 10 40-skips
[ "$status" -eq 0 ]
[[ "$output" == *"PASS 10-passes"* ]]
[[ "$output" == *"SKIP 40-skips"* ]]
[[ "$output" != *"20-fails"* ]]
}
@test "run stops on an unknown scenario before starting any VM" {
run cdd smoketest run 10 nosuch
[ "$status" -eq 1 ]
[[ "$output" == *"No scenario called nosuch"* ]]
not_called qemu-system-x86_64
}
@test "each variant runs in its own VM" {
run cdd smoketest run 30
[ "$status" -eq 0 ]
[[ "$output" == *"PASS 30-variants @ one"* ]]
[[ "$output" == *"PASS 30-variants @ two"* ]]
[ "$(grep -c '^qemu-system-x86_64 ' "$STUB_LOG")" -eq 2 ]
grep -q "variant two" "$(last_run)/30-variants@two/scenario.log"
}
@test "--tier all runs every scenario" {
run cdd smoketest run --tier all
[[ "$output" == *"30-variants @ one"* ]]
[[ "$output" == *"10-passes"* ]]
}
@test "the scenario boots a copy of its image, and gets the installed image's commit" {
run cdd smoketest run 10
[ "$status" -eq 0 ]
grep -q "^qemu-img create -q -f qcow2 -b $STATE/images/installed.qcow2 " "$STUB_LOG"
[[ "$output" == *"prepare installed $(git -C "$WC" rev-parse HEAD) $(cat "$STATE/images/installed.ref")"* ]]
}
@test "the VM is stopped and its disk deleted after each scenario" {
run cdd smoketest run 10 20
for dir in "$(last_run)"/*/; do
[ ! -e "$dir/qemu.pid" ]
[ ! -e "$dir/disk.qcow2" ]
done
}
@test "--keep leaves a failed scenario's VM running, and stops the run" {
run cdd smoketest run --keep 20 10
[ "$status" -eq 1 ]
[[ "$output" == *"The VM is still running"* ]]
[[ "$output" != *"PASS 10-passes"* ]]
pid="$(cat "$(last_run)/20-fails/qemu.pid")"
kill -0 "$pid"
kill "$pid"
}
@test "run --ref tests another commit" {
first="$(git -C "$WC" rev-parse HEAD)"
git -C "$WC" commit -q --allow-empty -m second
run cdd smoketest run --ref "$first" 10
[ "$status" -eq 0 ]
[[ "$output" == *"ref=$first"* ]]
}
@test "run stops without KVM" {
rm "$KVM_DEVICE"
run cdd smoketest run 10
[ "$status" -eq 1 ]
[[ "$output" == *"Can't use $KVM_DEVICE"* ]]
}
@test "smoketest needs a working copy with a smoketest folder" {
rm -rf "$WC/smoketest"
run cdd smoketest list
[ "$status" -eq 1 ]
[[ "$output" == *"has no smoketest/scenarios"* ]]
}
@test "images shows what's built" {
rm "$STATE/images/installed.qcow2"
run cdd smoketest images
[ "$status" -eq 0 ]
[[ "$output" == *"base built"* ]]
[[ "$output" == *"installed not built"* ]]
}
+92
View File
@@ -0,0 +1,92 @@
#!/bin/bash
# Shared setup for the bats tests, after CoreDesktop's: each test gets its own
# HOME and a bin dir at the front of PATH for fake commands. On top of that,
# HOME holds a fake CoreDesktop: an upstream repo, the deployed copy cloned
# from it, and a working copy at ~/Projects/coredesktop.
# Used by the .bats files that load this one
# shellcheck disable=SC2034
PROJECT="$(cd "$BATS_TEST_DIRNAME/.." && pwd)"
common_setup() {
export HOME="$BATS_TEST_TMPDIR/home"
export STUB_BIN="$BATS_TEST_TMPDIR/bin"
export STUB_LOG="$BATS_TEST_TMPDIR/stub.log"
export TMPDIR="$BATS_TEST_TMPDIR/tmp"
mkdir -p "$HOME" "$STUB_BIN" "$TMPDIR"
: > "$STUB_LOG"
export PATH="$STUB_BIN:$PATH"
unset XDG_CONFIG_HOME XDG_CACHE_HOME FLATPAK_ID COREDESKTOP_DEV_VIA_FLATPAK
export GIT_AUTHOR_NAME=Test GIT_AUTHOR_EMAIL=test@localhost
export GIT_COMMITTER_NAME=Test GIT_COMMITTER_EMAIL=test@localhost
export GIT_CONFIG_NOSYSTEM=1
UPSTREAM="$BATS_TEST_TMPDIR/upstream.git"
DEPLOYED="$HOME/.local/share/coredesktop"
WC="$HOME/Projects/coredesktop"
make_coredesktop
}
setup() {
common_setup
}
# A CoreDesktop upstream with one commit, and the deployed copy cloned from it.
# The fake utils.sh logs install_packages instead of installing anything.
make_coredesktop() {
local src="$BATS_TEST_TMPDIR/src"
git init -q --initial-branch=main "$src"
mkdir -p "$src/updates.d"
printf '%s\n' '#!/bin/bash' 'echo update' > "$src/update"
chmod +x "$src/update"
touch "$src/updates.d/.keep"
printf '%s\n' '#!/bin/bash' 'install_packages() { echo "install_packages $*" >> "$STUB_LOG"; }' \
> "$src/utils.sh"
printf 'check:\n\ttrue\n' > "$src/Makefile"
git -C "$src" add -A
git -C "$src" commit -qm "CoreDesktop"
git clone -q --bare "$src" "$UPSTREAM"
rm -rf "$src"
git clone -q "$UPSTREAM" "$DEPLOYED"
}
# Clone the working copy and point the settings at it
make_working_copy() {
git clone -q "$UPSTREAM" "$WC"
mkdir -p "$HOME/.config/coredesktop-dev"
printf 'WORKING_COPY=%q\n' "$WC" > "$HOME/.config/coredesktop-dev/config"
}
# Run the coredesktop-dev command
cdd() {
"$PROJECT/bin/coredesktop-dev" "$@"
}
# stub NAME [BODY]: put a fake NAME on PATH that appends "NAME args..." to
# $STUB_LOG and then runs BODY, a bash snippet that sees the args as "$@"
stub() {
local name="$1" body="${2:-}"
cat > "$STUB_BIN/$name" << EOF
#!/bin/bash
echo "$name \$*" >> "\$STUB_LOG"
$body
EOF
chmod +x "$STUB_BIN/$name"
}
# called NAME ARGS...: true if the stub log has exactly the line "NAME ARGS..."
called() {
grep -qxF "$*" "$STUB_LOG"
}
# not_called NAME: true if the stub NAME never ran
not_called() {
! grep -q "^$1 " "$STUB_LOG"
}
# uncalled NAME ARGS...: the opposite of called. (A bare "! called ..." would
# not fail the test: bats, like set -e, ignores the status of a ! command.)
uncalled() {
! called "$@"
}
+47
View File
@@ -0,0 +1,47 @@
#!/usr/bin/env bats
load test_helper
setup() {
common_setup
make_working_copy
stub dbus-run-session '[ "$1" = -- ] && shift; exec "$@"'
export DISPLAY=:0 WAYLAND_DISPLAY=wayland-0 DBUS_SESSION_BUS_ADDRESS=unix:path=/nowhere SSH_AUTH_SOCK=/nowhere
}
@test "try runs the command in a throwaway home with the working copy as CoreDesktop" {
run cdd try bash -c 'echo "home=$HOME"
echo "runtime=$XDG_RUNTIME_DIR $(stat -c %a "$XDG_RUNTIME_DIR")"
echo "display=${DISPLAY-unset} ${WAYLAND_DISPLAY-unset} ${DBUS_SESSION_BUS_ADDRESS-unset} ${SSH_AUTH_SOCK-unset}"
echo "coredesktop=$(readlink ~/.local/share/coredesktop)"
echo "update=$(readlink "$(command -v update)")"
echo "config=$XDG_CONFIG_HOME"'
[ "$status" -eq 0 ]
[[ "$output" == *"home=$TMPDIR/coredesktop-dev-try."*"/home"* ]]
[[ "$output" == *"runtime=$TMPDIR/coredesktop-dev-try."*"/runtime 700"* ]]
[[ "$output" == *"display=unset unset unset unset"* ]]
[[ "$output" == *"coredesktop=$WC"* ]]
[[ "$output" == *"update=$WC/update"* ]]
[[ "$output" == *"config=$TMPDIR/coredesktop-dev-try."*"/home/.config"* ]]
[[ "$output" == *"still change this machine"* ]]
grep -q "^dbus-run-session -- bash" "$STUB_LOG"
}
@test "try cleans up afterwards, and passes on the command's exit status" {
run cdd try bash -c 'exit 4'
[ "$status" -eq 4 ]
[ -z "$(ls "$TMPDIR")" ]
}
@test "try --keep keeps the throwaway folder" {
run cdd try --keep true
[ "$status" -eq 0 ]
[[ "$output" == *"Kept $TMPDIR/coredesktop-dev-try."* ]]
[ -n "$(ls "$TMPDIR")" ]
}
@test "try doesn't touch the real home" {
run cdd try bash -c 'touch ~/made-by-try'
[ "$status" -eq 0 ]
[ ! -e "$HOME/made-by-try" ]
}
+51
View File
@@ -0,0 +1,51 @@
#!/usr/bin/env bats
load test_helper
HOOK_MARKER="# Installed by coredesktop-dev; remove with: coredesktop-dev hooks remove"
@test "update installs the packages and links the command, without asking anything" {
run bash "$PROJECT/update.sh" < /dev/null
[ "$status" -eq 0 ]
called install_packages shellcheck bats tea qemu-base libisoburn
[ "$(readlink "$HOME/.local/bin/coredesktop-dev")" = "$PROJECT/bin/coredesktop-dev" ]
}
@test "update fixes a link that points somewhere else" {
mkdir -p "$HOME/.local/bin"
ln -s /nowhere "$HOME/.local/bin/coredesktop-dev"
run bash "$PROJECT/update.sh" < /dev/null
[ "$status" -eq 0 ]
[ "$(readlink "$HOME/.local/bin/coredesktop-dev")" = "$PROJECT/bin/coredesktop-dev" ]
}
@test "update leaves an unchanged pre-push hook alone" {
make_working_copy
cp "$PROJECT/hooks/pre-push" "$WC/.git/hooks/pre-push"
touch -d '2020-01-01' "$WC/.git/hooks/pre-push"
run bash "$PROJECT/update.sh" < /dev/null
[ "$status" -eq 0 ]
[ "$(date -r "$WC/.git/hooks/pre-push" +%Y)" = 2020 ]
[[ "$output" != *"Updated the pre-push hook"* ]]
}
@test "update refreshes our pre-push hook when it has changed" {
make_working_copy
printf '%s\n' '#!/bin/bash' "$HOOK_MARKER" 'old version' > "$WC/.git/hooks/pre-push"
run bash "$PROJECT/update.sh" < /dev/null
[ "$status" -eq 0 ]
cmp "$PROJECT/hooks/pre-push" "$WC/.git/hooks/pre-push"
[[ "$output" == *"Updated the pre-push hook"* ]]
}
@test "update doesn't touch someone else's pre-push hook" {
make_working_copy
printf '%s\n' '#!/bin/bash' 'mine' > "$WC/.git/hooks/pre-push"
run bash "$PROJECT/update.sh" < /dev/null
[ "$status" -eq 0 ]
[ "$(tail -n 1 "$WC/.git/hooks/pre-push")" = mine ]
}
+41
View File
@@ -0,0 +1,41 @@
#!/bin/bash
#
# Purpose : Keep coredesktop-dev current (run by CoreDesktop's `update`).
# Never asks anything.
set -e
SELF="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
COREDESKTOP="$HOME/.local/share/coredesktop"
source "$COREDESKTOP/utils.sh"
source "$SELF/lib/common.sh"
PACKAGES=(
shellcheck # make lint
bats # make test
tea # Gitea's CLI, for pull requests
qemu-base # the smoketest's virtual machines
libisoburn # xorriso, for the smoketest VM's cloud-init disk
)
echo "Checking coredesktop-dev's packages..."
install_packages "${PACKAGES[@]}"
# A symlink, so each pull updates the command
mkdir -p "$HOME/.local/bin"
if [ "$(readlink "$HOME/.local/bin/coredesktop-dev")" != "$SELF/bin/coredesktop-dev" ]; then
ln -sfn "$SELF/bin/coredesktop-dev" "$HOME/.local/bin/coredesktop-dev"
echo "Linked ~/.local/bin/coredesktop-dev"
fi
# Refresh the working copy's pre-push hook, if it's ours and has changed
load_config
if [ -n "$WORKING_COPY" ] && is_coredesktop_clone "$WORKING_COPY"; then
source "$SELF/lib/hooks.sh"
hook="$(hook_path)"
if hook_is_ours "$hook" && ! cmp -s "$SELF/hooks/pre-push" "$hook"; then
install -m 755 "$SELF/hooks/pre-push" "$hook"
echo "Updated the pre-push hook in $WORKING_COPY"
fi
fi
+61
View File
@@ -0,0 +1,61 @@
#!/bin/bash
#
# Purpose : Turn Arch's cloud image into the machine CoreDesktop's README starts
# from: Arch with archinstall's desktop profile and GNOME. Run as root
# in the VM while building the smoketest's base image.
#
# Differences from a real archinstall machine, on purpose: the user (tester)
# logs in to GNOME automatically and has passwordless sudo, so the smoketest
# runs without anyone at the keyboard.
set -euo pipefail
USER_NAME=tester
echo "== Growing / to fill the disk"
btrfs filesystem resize max / || echo "Couldn't grow /; continuing."
df -h /
echo "== Sharing the host's package cache"
if ! grep -q '^pkgcache ' /etc/fstab; then
echo 'pkgcache /var/cache/pacman/pkg 9p trans=virtio,version=9p2000.L,msize=524288,nofail 0 0' >> /etc/fstab
fi
systemctl daemon-reload
mountpoint -q /var/cache/pacman/pkg || mount /var/cache/pacman/pkg \
|| echo "No shared package cache; packages download into the VM."
echo "== Installing GNOME"
pacman -Sy --needed --noconfirm archlinux-keyring
# archinstall's desktop profile with GNOME, NetworkManager and PipeWire
pacman -Su --needed --noconfirm \
gnome gnome-tweaks gdm \
networkmanager \
pipewire pipewire-alsa pipewire-pulse pipewire-jack wireplumber \
base-devel git sudo openssh nano vim htop wget smartmontools xdg-utils
echo "== Locale"
sed -i 's/^#en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen
locale-gen
echo LANG=en_US.UTF-8 > /etc/locale.conf
echo "== NetworkManager instead of systemd-networkd"
systemctl disable systemd-networkd.service systemd-networkd.socket systemd-resolved.service || true
# NetworkManager writes its own resolv.conf; the cloud image's points at
# systemd-resolved
rm -f /etc/resolv.conf
systemctl enable NetworkManager.service gdm.service sshd.service
echo "== Automatic login to GNOME as $USER_NAME"
cat > /etc/gdm/custom.conf << EOF
[daemon]
AutomaticLoginEnable=True
AutomaticLogin=$USER_NAME
EOF
echo "== Smoketest folder"
install -d -o "$USER_NAME" -g "$USER_NAME" /srv/smoketest
# cloud-init has done its job (the user, their SSH key and sudo rights)
touch /etc/cloud/cloud-init.disabled
echo "== Done"
+17
View File
@@ -0,0 +1,17 @@
#cloud-config
# The smoketest VM's user. coredesktop-dev puts its SSH key in place of @SSH_KEY@.
users:
- name: tester
shell: /bin/bash
groups: [wheel]
sudo: "ALL=(ALL) NOPASSWD:ALL"
lock_passwd: false
ssh_authorized_keys:
- @SSH_KEY@
# For logging in at the console (SMOKETEST_VNC) when something goes wrong
chpasswd:
expire: false
users:
- name: tester
password: tester
type: text