smoke: discoverable scenarios + selectable runs + parallel dispatch
`scripts/smoke.sh` was a 600-line linear script: no way to see what it covers without reading the whole thing, and no way to run a single scenario when iterating. Every iteration paid the full ~5-10 min suite, which made fast feedback loops painful enough to avoid the suite. Refactor into a registry + per-scenario functions: - Top-of-file SMOKE_SCENARIOS (ordered) + SMOKE_DESCS (one-line desc per scenario) + SMOKE_CLASS (pure / repodir / global) drive both listing and dispatch. The 21 existing scenario blocks become scenario_<name> functions. Bodies are the inline blocks verbatim, modulo the workspace fixture move described below. - New CLI: --list (cheap discovery, no install / no env-vars), --scenario NAME (or NAME,NAME,...), --jobs N (parallel dispatch), -h / --help. - New setup_fixtures runs once after the install/doctor/restart preamble and produces the throwaway git repo at $repodir that 'repodir'-class scenarios consume. Lifted out of scenario_workspace_run so single- scenario invocations (e.g. --scenario workspace_dryrun) get the fixture even when the scenario that historically built it isn't selected. - Wipe ~/.local/state/banger/ssh/known_hosts in the install preamble. `system uninstall --purge` clears /var/lib/banger but the user-side known_hosts persists by design — and smoke creates VMs that reuse guest IPs (172.16.0.2 etc.) with fresh host keys every run, so a leftover entry trips StrictHostKeyChecking and the daemon's wait- for-ssh sees only timeouts. This was the real cause of the "guest ssh did not come up" flakes that surface across smoke iterations. Parallel dispatch: - --jobs N opts into a slot-limited pool: 'pure' scenarios fan out as individual jobs; 'repodir' scenarios fuse into a single serial chain (since they mutate $repodir in registry order); 'global' scenarios run serially after the pool, one at a time. - Cap is min(N, 8) — each parallel slot runs an 8 GiB VM, so RAM is the binding constraint. - Parallel-mode stdout/stderr per scenario buffer to per-scenario logs and emit one PASS/FAIL line on completion; on FAIL the buffer is dumped. Serial mode (--jobs 1, the default) keeps stdout unbuffered exactly as before. - Parallelism is documented as experimental in --help: it surfaces real daemon-side concurrency bugs (image auto-pull manifest race, work-seed-refresh race on the shared work-seed.ext4) that don't appear in serial mode and that need their own fix in the daemon. Serial (--jobs 1) is the reliable path; --jobs N is for fast- iteration dev work where occasional re-runs are acceptable. Exit codes: 0 ok, 1 assertion failed, 2 usage error (unknown scenario, missing SCENARIO=), 77 explicit selection skipped (NAT when sudo iptables is unavailable AND nat is the only selected scenario; soft-skip otherwise). Makefile additions: - `make smoke-list` — cheap discovery, no smoke-build dep, no env vars. - `make smoke-one SCENARIO=name` — single-scenario run, full preamble. MAKECMDGOALS guard catches missing SCENARIO= before any rebuild. - `make smoke JOBS=N` — passes through to the script's --jobs N. - Help text covers all three. Verified: serial full suite passes 21/21 in ~140s on this host; make smoke-one SCENARIO=workspace_restart runs the recently-added regression test alone in ~50s. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c9358ab390
commit
115eec8576
2 changed files with 872 additions and 402 deletions
33
Makefile
33
Makefile
|
|
@ -33,7 +33,16 @@ GO_LDFLAGS := -X banger/internal/buildinfo.Version=$(VERSION) -X banger/internal
|
|||
|
||||
.DEFAULT_GOAL := help
|
||||
|
||||
.PHONY: help build banger bangerd test fmt tidy clean install uninstall lint lint-go lint-shell coverage coverage-html coverage-total coverage-combined coverage-combined-html smoke smoke-build smoke-coverage-html smoke-clean smoke-fresh
|
||||
# `make smoke-one` requires SCENARIO=. Validate before any prerequisite
|
||||
# (notably smoke-build) so a typo'd invocation doesn't pay for a Go
|
||||
# rebuild before learning it's wrong.
|
||||
ifneq (,$(filter smoke-one,$(MAKECMDGOALS)))
|
||||
ifndef SCENARIO
|
||||
$(error smoke-one needs SCENARIO=name (see `make smoke-list` for names))
|
||||
endif
|
||||
endif
|
||||
|
||||
.PHONY: help build banger bangerd test fmt tidy clean install uninstall lint lint-go lint-shell coverage coverage-html coverage-total coverage-combined coverage-combined-html smoke smoke-build smoke-list smoke-one smoke-coverage-html smoke-clean smoke-fresh
|
||||
|
||||
help:
|
||||
@printf '%s\n' \
|
||||
|
|
@ -52,6 +61,9 @@ help:
|
|||
' make tidy Run go mod tidy' \
|
||||
' make clean Remove built Go binaries and coverage artefacts' \
|
||||
' make smoke Build instrumented binaries, run the supported systemd smoke suite, report coverage (needs KVM + sudo)' \
|
||||
' make smoke JOBS=N Same, but dispatch parallel-safe scenarios across N slots (1-8; default 1)' \
|
||||
' make smoke-list Print the list of smoke scenarios with descriptions (no build, no install)' \
|
||||
' make smoke-one SCENARIO=NAME Run a single smoke scenario (still does the install preamble)' \
|
||||
' make smoke-fresh smoke-clean + smoke — purges stale smoke-owned installs before a clean supported-path run' \
|
||||
' make smoke-coverage-html HTML coverage report from the last smoke run' \
|
||||
' make smoke-clean Remove the smoke build tree and purge any stale smoke-owned system install'
|
||||
|
|
@ -170,11 +182,28 @@ smoke: smoke-build
|
|||
BANGER_SMOKE_BIN_DIR="$(abspath $(SMOKE_BIN_DIR))" \
|
||||
BANGER_SMOKE_COVER_DIR="$(abspath $(SMOKE_COVER_DIR))" \
|
||||
BANGER_SMOKE_XDG_DIR="$(abspath $(SMOKE_XDG_DIR))" \
|
||||
bash "$(SMOKE_SCRIPT)"
|
||||
bash "$(SMOKE_SCRIPT)" $(if $(JOBS),--jobs $(JOBS))
|
||||
@echo ''
|
||||
@echo 'Smoke coverage:'
|
||||
@$(GO) tool covdata percent -i="$(SMOKE_COVER_DIR)"
|
||||
|
||||
# smoke-list is intentionally cheap: no smoke-build dep, no env vars.
|
||||
# The script's --list path short-circuits before any side-effect or
|
||||
# env validation, so this works on a fresh checkout.
|
||||
smoke-list:
|
||||
@bash "$(SMOKE_SCRIPT)" --list
|
||||
|
||||
# smoke-one runs one scenario (or a comma-separated list) with the same
|
||||
# install preamble as the full suite. Useful when iterating on a specific
|
||||
# scenario — see `make smoke-list` for names.
|
||||
smoke-one: smoke-build
|
||||
rm -rf "$(SMOKE_COVER_DIR)"
|
||||
mkdir -p "$(SMOKE_COVER_DIR)" "$(SMOKE_XDG_DIR)"
|
||||
BANGER_SMOKE_BIN_DIR="$(abspath $(SMOKE_BIN_DIR))" \
|
||||
BANGER_SMOKE_COVER_DIR="$(abspath $(SMOKE_COVER_DIR))" \
|
||||
BANGER_SMOKE_XDG_DIR="$(abspath $(SMOKE_XDG_DIR))" \
|
||||
bash "$(SMOKE_SCRIPT)" --scenario "$(SCENARIO)"
|
||||
|
||||
smoke-coverage-html: smoke
|
||||
$(GO) tool covdata textfmt -i="$(SMOKE_COVER_DIR)" -o="$(SMOKE_DIR)/cover.out"
|
||||
$(GO) tool cover -html="$(SMOKE_DIR)/cover.out" -o "$(SMOKE_DIR)/cover.html"
|
||||
|
|
|
|||
561
scripts/smoke.sh
561
scripts/smoke.sh
|
|
@ -16,11 +16,26 @@
|
|||
# Scratch files live under $BANGER_SMOKE_XDG_DIR (historic name kept for
|
||||
# make-compat). Service state uses the real supported system paths and is
|
||||
# purged by the smoke cleanup path.
|
||||
#
|
||||
# Usage:
|
||||
# scripts/smoke.sh # full suite, serial
|
||||
# scripts/smoke.sh --list # cheap discovery, no install
|
||||
# scripts/smoke.sh --scenario NAME # single scenario
|
||||
# scripts/smoke.sh --scenario a,b,c # comma list, registry order
|
||||
# scripts/smoke.sh --jobs N # parallel dispatch (default 1)
|
||||
# scripts/smoke.sh -h | --help # this help
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 success
|
||||
# 1 assertion failed
|
||||
# 2 usage error (unknown scenario, bad flag)
|
||||
# 77 scenario explicitly selected but env can't run it (autotools "skip")
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
log() { printf '[smoke] %s\n' "$*" >&2; }
|
||||
die() { printf '[smoke] FAIL: %s\n' "$*" >&2; exit 1; }
|
||||
usage_die() { printf '[smoke] usage: %s\n' "$*" >&2; exit 2; }
|
||||
|
||||
wait_for_ssh() {
|
||||
local vm="$1"
|
||||
|
|
@ -34,6 +49,200 @@ wait_for_ssh() {
|
|||
return 1
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Scenario registry. Order in SMOKE_SCENARIOS is the run order for full
|
||||
# suite mode and the order shown in --list. Class drives parallelism:
|
||||
# pure — independent VMs, parallel-safe
|
||||
# repodir — share $repodir mutations; serial chain in registry order
|
||||
# global — assert host-global state (iptables, vm row counts, ssh-config
|
||||
# on a fake HOME); run serially after everything else
|
||||
# Names are bash function suffixes — `scenario_<name>` must exist.
|
||||
# ---------------------------------------------------------------------
|
||||
SMOKE_SCENARIOS=(
|
||||
bare_run
|
||||
workspace_run
|
||||
exit_code
|
||||
workspace_dryrun
|
||||
include_untracked
|
||||
workspace_export
|
||||
concurrent_run
|
||||
vm_lifecycle
|
||||
vm_set
|
||||
vm_restart
|
||||
vm_kill
|
||||
vm_prune
|
||||
vm_ports
|
||||
workspace_full_copy
|
||||
workspace_basecommit
|
||||
workspace_restart
|
||||
vm_exec
|
||||
ssh_config
|
||||
nat
|
||||
invalid_spec
|
||||
invalid_name
|
||||
)
|
||||
|
||||
declare -A SMOKE_DESCS=(
|
||||
[bare_run]="bare vm run: create + start + ssh + echo + --rm"
|
||||
[workspace_run]="workspace vm run: ship git repo, read file in guest"
|
||||
[exit_code]="exit-code propagation: guest sh -c 'exit 42' returns rc=42"
|
||||
[workspace_dryrun]="workspace dry-run: list tracked files without a VM"
|
||||
[include_untracked]="--include-untracked ships files outside the git index"
|
||||
[workspace_export]="workspace export round-trip: guest edit -> patch marker"
|
||||
[concurrent_run]="two parallel --rm invocations both succeed"
|
||||
[vm_lifecycle]="explicit create / stop / start / ssh / delete"
|
||||
[vm_set]="reconfigure vcpu while stopped; guest sees new count"
|
||||
[vm_restart]="restart verb: boot_id changes"
|
||||
[vm_kill]="vm kill --signal KILL: stopped, no leaked dm device"
|
||||
[vm_prune]="prune -f removes stopped VMs, preserves running ones"
|
||||
[vm_ports]="vm ports: sshd :22 visible via VM DNS name"
|
||||
[workspace_full_copy]="workspace prepare --mode full_copy: alternate transfer path"
|
||||
[workspace_basecommit]="workspace export --base-commit: guest commits captured"
|
||||
[workspace_restart]="workspace prepare -> stop -> start preserves marker"
|
||||
[vm_exec]="vm exec: auto-cd, exit-code, stale-warn, --auto-prepare resync"
|
||||
[ssh_config]="ssh-config --install / --uninstall: idempotent, HOME-isolated"
|
||||
[nat]="--nat installs per-VM MASQUERADE; control VM does not"
|
||||
[invalid_spec]="--vcpu 0 rejected, no VM row leaked"
|
||||
[invalid_name]="bad names (uppercase/space/dot/leading-hyphen) all rejected"
|
||||
)
|
||||
|
||||
declare -A SMOKE_CLASS=(
|
||||
[bare_run]=pure
|
||||
[workspace_run]=repodir
|
||||
[exit_code]=pure
|
||||
[workspace_dryrun]=repodir
|
||||
[include_untracked]=repodir
|
||||
[workspace_export]=repodir
|
||||
[concurrent_run]=pure
|
||||
[vm_lifecycle]=pure
|
||||
[vm_set]=pure
|
||||
[vm_restart]=pure
|
||||
[vm_kill]=pure
|
||||
[vm_prune]=pure
|
||||
[vm_ports]=pure
|
||||
[workspace_full_copy]=repodir
|
||||
[workspace_basecommit]=repodir
|
||||
[workspace_restart]=repodir
|
||||
[vm_exec]=repodir
|
||||
[ssh_config]=pure
|
||||
[nat]=global
|
||||
[invalid_spec]=global
|
||||
[invalid_name]=global
|
||||
)
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
scripts/smoke.sh — banger end-to-end smoke suite
|
||||
|
||||
Usage:
|
||||
scripts/smoke.sh run the full suite (serial)
|
||||
scripts/smoke.sh --list list all scenarios (no install)
|
||||
scripts/smoke.sh --scenario NAME run a single scenario
|
||||
scripts/smoke.sh --scenario a,b,c run a comma-separated list
|
||||
scripts/smoke.sh --jobs N parallel dispatch (default 1)
|
||||
scripts/smoke.sh -h | --help this help
|
||||
|
||||
Notes:
|
||||
--list works on a fresh checkout — no sudo, no KVM, no smoke-build.
|
||||
--jobs N caps at min(N, 8); each parallel slot runs an 8 GiB VM.
|
||||
Scenarios in the 'repodir' class share fixture mutations and run as
|
||||
a serial chain regardless of --jobs.
|
||||
|
||||
Parallelism (--jobs >1) is experimental: it surfaces real concurrency
|
||||
bugs in the daemon's image-pull and work-seed-refresh paths that don't
|
||||
appear in serial mode. Use serial (--jobs 1, the default) for reliable
|
||||
CI-style runs; use --jobs N when you can tolerate a few re-runs to
|
||||
debug something fast.
|
||||
|
||||
Exit codes: 0 ok, 1 fail, 2 usage error, 77 explicit selection skipped.
|
||||
EOF
|
||||
}
|
||||
|
||||
list_scenarios() {
|
||||
local name
|
||||
for name in "${SMOKE_SCENARIOS[@]}"; do
|
||||
printf ' %-22s %s\n' "$name" "${SMOKE_DESCS[$name]}"
|
||||
done
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Argument parsing. Done before env-var checks so --list / --help work
|
||||
# on a fresh checkout, and so a typo in --scenario fails before we
|
||||
# touch sudo / system install.
|
||||
# ---------------------------------------------------------------------
|
||||
SMOKE_LIST=0
|
||||
SMOKE_FILTER=""
|
||||
SMOKE_EXPLICIT=0
|
||||
SMOKE_JOBS=1
|
||||
|
||||
while (( $# > 0 )); do
|
||||
case "$1" in
|
||||
--list)
|
||||
SMOKE_LIST=1; shift ;;
|
||||
--scenario)
|
||||
[[ $# -ge 2 ]] || usage_die "--scenario requires a name (see --list)"
|
||||
SMOKE_FILTER="$2"; SMOKE_EXPLICIT=1; shift 2 ;;
|
||||
--scenario=*)
|
||||
SMOKE_FILTER="${1#--scenario=}"; SMOKE_EXPLICIT=1; shift ;;
|
||||
--jobs)
|
||||
[[ $# -ge 2 ]] || usage_die "--jobs requires N"
|
||||
SMOKE_JOBS="$2"; shift 2 ;;
|
||||
--jobs=*)
|
||||
SMOKE_JOBS="${1#--jobs=}"; shift ;;
|
||||
-h|--help)
|
||||
usage; exit 0 ;;
|
||||
*)
|
||||
usage_die "unknown argument: $1 (try --help)" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if (( SMOKE_LIST )); then
|
||||
list_scenarios
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Validate --jobs.
|
||||
if ! [[ "$SMOKE_JOBS" =~ ^[1-9][0-9]*$ ]]; then
|
||||
usage_die "--jobs must be a positive integer; got '$SMOKE_JOBS'"
|
||||
fi
|
||||
if (( SMOKE_JOBS > 8 )); then
|
||||
log "capping --jobs at 8 (each parallel slot runs an 8 GiB VM)"
|
||||
SMOKE_JOBS=8
|
||||
fi
|
||||
|
||||
# Resolve --scenario filter into SMOKE_SELECTED in registry order.
|
||||
SMOKE_SELECTED=()
|
||||
if [[ -n "$SMOKE_FILTER" ]]; then
|
||||
declare -A _requested=()
|
||||
IFS=',' read -r -a _names <<<"$SMOKE_FILTER"
|
||||
for name in "${_names[@]}"; do
|
||||
name="${name// /}"
|
||||
[[ -n "$name" ]] || continue
|
||||
if [[ -z "${SMOKE_DESCS[$name]+x}" ]]; then
|
||||
printf '[smoke] unknown scenario: %s\n' "$name" >&2
|
||||
printf '[smoke] available scenarios:\n' >&2
|
||||
list_scenarios >&2
|
||||
exit 2
|
||||
fi
|
||||
_requested[$name]=1
|
||||
done
|
||||
for name in "${SMOKE_SCENARIOS[@]}"; do
|
||||
if [[ -n "${_requested[$name]+x}" ]]; then
|
||||
SMOKE_SELECTED+=("$name")
|
||||
fi
|
||||
done
|
||||
unset _requested _names
|
||||
else
|
||||
SMOKE_SELECTED=("${SMOKE_SCENARIOS[@]}")
|
||||
fi
|
||||
|
||||
if (( ${#SMOKE_SELECTED[@]} == 0 )); then
|
||||
usage_die "no scenarios selected"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Env checks. Required for any scenario; not required for --list/--help.
|
||||
# ---------------------------------------------------------------------
|
||||
: "${BANGER_SMOKE_BIN_DIR:?must point at the instrumented binary dir, set by make smoke}"
|
||||
: "${BANGER_SMOKE_COVER_DIR:?must point at the coverage dir, set by make smoke}"
|
||||
: "${BANGER_SMOKE_XDG_DIR:?must point at the smoke scratch root, set by make smoke}"
|
||||
|
|
@ -48,6 +257,7 @@ done
|
|||
|
||||
scratch_root="$BANGER_SMOKE_XDG_DIR"
|
||||
runtime_dir=
|
||||
repodir=
|
||||
smoke_owner="$(id -un)"
|
||||
smoke_marker='/etc/banger/.smoke-owned'
|
||||
service_cover_dir='/var/lib/banger'
|
||||
|
|
@ -115,6 +325,7 @@ cleanup() {
|
|||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
install_preamble() {
|
||||
if sudo test -f /etc/banger/install.toml; then
|
||||
if sudo test -f "$smoke_marker"; then
|
||||
log 'found stale smoke-owned install; purging it first'
|
||||
|
|
@ -124,6 +335,16 @@ if sudo test -f /etc/banger/install.toml; then
|
|||
fi
|
||||
fi
|
||||
|
||||
# Wipe the user-side known_hosts. `system uninstall --purge` clears
|
||||
# /var/lib/banger but the user-state known_hosts at
|
||||
# ~/.local/state/banger/ssh/known_hosts is by-design left alone — it's
|
||||
# the user's data, not the daemon's. Smoke creates VMs that reuse
|
||||
# guest IPs (172.16.0.2 etc.) with fresh host keys every run, so a
|
||||
# leftover entry from a prior run trips StrictHostKeyChecking and
|
||||
# the daemon's wait-for-ssh sees only timeouts. Removing the file
|
||||
# is safe — the daemon recreates it on first connect.
|
||||
rm -f "$HOME/.local/state/banger/ssh/known_hosts" 2>/dev/null || true
|
||||
|
||||
log 'installing smoke-owned services'
|
||||
sudo env \
|
||||
GOCOVERDIR="$BANGER_SMOKE_COVER_DIR" \
|
||||
|
|
@ -133,6 +354,7 @@ sudo env \
|
|||
|| die 'system install failed'
|
||||
sudo touch "$smoke_marker"
|
||||
|
||||
local status_out
|
||||
status_out="$("$BANGER" system status)" || die 'system status failed after install'
|
||||
grep -qE '^active +active' <<<"$status_out" || die "owner daemon not active after install: $status_out"
|
||||
grep -qE '^helper_active +active' <<<"$status_out" || die "root helper not active after install: $status_out"
|
||||
|
|
@ -147,14 +369,15 @@ sudo_banger "$BANGER" system restart >/dev/null || die 'system restart failed'
|
|||
status_out="$("$BANGER" system status)" || die 'system status failed after restart'
|
||||
grep -qE '^active +active' <<<"$status_out" || die "owner daemon not active after restart: $status_out"
|
||||
grep -qE '^helper_active +active' <<<"$status_out" || die "root helper not active after restart: $status_out"
|
||||
}
|
||||
|
||||
# --- bare vm run ------------------------------------------------------
|
||||
log "bare vm run: create + start + ssh + exec 'echo smoke-bare-ok' + --rm"
|
||||
bare_out="$("$BANGER" vm run --rm -- echo smoke-bare-ok)" || die "bare vm run exit $?"
|
||||
grep -q 'smoke-bare-ok' <<<"$bare_out" || die "bare vm run stdout missing marker: $bare_out"
|
||||
|
||||
# --- workspace vm run -------------------------------------------------
|
||||
log 'workspace vm run: preparing a throwaway git repo'
|
||||
# setup_fixtures builds the throwaway git repo at $repodir that every
|
||||
# 'repodir'-class scenario consumes. Pulled out of scenario_workspace_run
|
||||
# so single-scenario invocations (e.g. --scenario workspace_dryrun) get
|
||||
# the fixture even when the scenario that historically created it is
|
||||
# not selected.
|
||||
setup_fixtures() {
|
||||
log 'setup_fixtures: preparing throwaway git repo for repodir-class scenarios'
|
||||
repodir="$runtime_dir/fake-repo"
|
||||
mkdir -p "$repodir"
|
||||
(
|
||||
|
|
@ -167,64 +390,91 @@ mkdir -p "$repodir"
|
|||
git add .
|
||||
git commit -q -m init
|
||||
)
|
||||
}
|
||||
|
||||
log "workspace vm run: create + start + workspace prepare + cat guest file + --rm"
|
||||
# ---------------------------------------------------------------------
|
||||
# Scenario implementations. Each is a function `scenario_<name>` that
|
||||
# logs its description first and then runs assertions. Bodies are the
|
||||
# pre-refactor inline blocks, modulo the workspace_run fixture move.
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
scenario_bare_run() {
|
||||
log "${SMOKE_DESCS[bare_run]}"
|
||||
local bare_out
|
||||
bare_out="$("$BANGER" vm run --rm -- echo smoke-bare-ok)" || die "bare vm run exit $?"
|
||||
grep -q 'smoke-bare-ok' <<<"$bare_out" || die "bare vm run stdout missing marker: $bare_out"
|
||||
}
|
||||
|
||||
scenario_workspace_run() {
|
||||
log "${SMOKE_DESCS[workspace_run]}"
|
||||
local ws_out
|
||||
ws_out="$("$BANGER" vm run --rm "$repodir" -- cat /root/repo/smoke-file.txt)" || die "workspace vm run exit $?"
|
||||
grep -q 'smoke-workspace-marker' <<<"$ws_out" || die "workspace vm run didn't ship smoke-file.txt: $ws_out"
|
||||
}
|
||||
|
||||
# --- command exit-code propagation ------------------------------------
|
||||
log 'exit-code propagation: guest `sh -c "exit 42"` must produce rc=42'
|
||||
scenario_exit_code() {
|
||||
log "${SMOKE_DESCS[exit_code]}"
|
||||
local rc
|
||||
set +e
|
||||
"$BANGER" vm run --rm -- sh -c 'exit 42'
|
||||
rc=$?
|
||||
set -e
|
||||
[[ "$rc" -eq 42 ]] || die "exit-code propagation: got rc=$rc, want 42"
|
||||
}
|
||||
|
||||
# --- workspace dry-run (no VM) ----------------------------------------
|
||||
log 'workspace dry-run: list tracked files without creating a VM'
|
||||
scenario_workspace_dryrun() {
|
||||
log "${SMOKE_DESCS[workspace_dryrun]}"
|
||||
local dry_out
|
||||
dry_out="$("$BANGER" vm run --dry-run "$repodir")" || die "dry-run exit $?"
|
||||
grep -q 'smoke-file.txt' <<<"$dry_out" || die "dry-run didn't list smoke-file.txt: $dry_out"
|
||||
grep -q 'mode: tracked only' <<<"$dry_out" || die "dry-run mode line missing or wrong: $dry_out"
|
||||
}
|
||||
|
||||
# --- workspace --include-untracked -----------------------------------
|
||||
log 'workspace --include-untracked: opt-in ships files outside the git index'
|
||||
scenario_include_untracked() {
|
||||
log "${SMOKE_DESCS[include_untracked]}"
|
||||
echo 'untracked-marker' > "$repodir/smoke-untracked.txt"
|
||||
local inc_out
|
||||
inc_out="$("$BANGER" vm run --rm --include-untracked "$repodir" -- cat /root/repo/smoke-untracked.txt)" || die "include-untracked vm run exit $?"
|
||||
grep -q 'untracked-marker' <<<"$inc_out" || die "--include-untracked didn't ship the untracked file: $inc_out"
|
||||
# Self-cleanup: scenario added an untracked file, scenario removes it.
|
||||
rm -f "$repodir/smoke-untracked.txt"
|
||||
}
|
||||
|
||||
# --- workspace export round-trip --------------------------------------
|
||||
log 'workspace export: create + prepare + guest edit + export + assert marker'
|
||||
scenario_workspace_export() {
|
||||
log "${SMOKE_DESCS[workspace_export]}"
|
||||
"$BANGER" vm create --name smoke-export --image debian-bookworm >/dev/null \
|
||||
|| die "export: vm create exit $?"
|
||||
"$BANGER" vm workspace prepare smoke-export "$repodir" >/dev/null \
|
||||
|| die "export: workspace prepare exit $?"
|
||||
"$BANGER" vm ssh smoke-export -- sh -c 'echo guest-edit > /root/repo/new-guest-file.txt' \
|
||||
|| die "export: guest-side file write exit $?"
|
||||
export_patch="$runtime_dir/smoke-export.diff"
|
||||
local export_patch="$runtime_dir/smoke-export.diff"
|
||||
"$BANGER" vm workspace export smoke-export --output "$export_patch" \
|
||||
|| die "export: workspace export exit $?"
|
||||
[[ -s "$export_patch" ]] || die "export: patch file empty at $export_patch"
|
||||
grep -q 'new-guest-file.txt' "$export_patch" \
|
||||
|| die "export: patch missing new-guest-file.txt marker (head: $(head -c 400 "$export_patch"))"
|
||||
cleanup_export_vm
|
||||
}
|
||||
|
||||
# --- concurrent vm runs -----------------------------------------------
|
||||
log 'concurrent vm runs: two --rm invocations must both succeed'
|
||||
tmpA="$runtime_dir/concurrent-a.out"
|
||||
tmpB="$runtime_dir/concurrent-b.out"
|
||||
scenario_concurrent_run() {
|
||||
log "${SMOKE_DESCS[concurrent_run]}"
|
||||
local tmpA="$runtime_dir/concurrent-a.out"
|
||||
local tmpB="$runtime_dir/concurrent-b.out"
|
||||
"$BANGER" vm run --rm -- echo smoke-concurrent-a > "$tmpA" 2>&1 &
|
||||
pidA=$!
|
||||
local pidA=$!
|
||||
"$BANGER" vm run --rm -- echo smoke-concurrent-b > "$tmpB" 2>&1 &
|
||||
pidB=$!
|
||||
local pidB=$!
|
||||
wait "$pidA" || die "concurrent VM A exited non-zero: $(cat "$tmpA")"
|
||||
wait "$pidB" || die "concurrent VM B exited non-zero: $(cat "$tmpB")"
|
||||
grep -q 'smoke-concurrent-a' "$tmpA" || die "concurrent VM A missing marker: $(cat "$tmpA")"
|
||||
grep -q 'smoke-concurrent-b' "$tmpB" || die "concurrent VM B missing marker: $(cat "$tmpB")"
|
||||
}
|
||||
|
||||
# --- vm lifecycle (create → stop → start → delete) --------------------
|
||||
log 'vm lifecycle: explicit create / stop / start / ssh / delete'
|
||||
lifecycle_name=smoke-lifecycle
|
||||
scenario_vm_lifecycle() {
|
||||
log "${SMOKE_DESCS[vm_lifecycle]}"
|
||||
local lifecycle_name=smoke-lifecycle
|
||||
local show_out ssh_out rc
|
||||
"$BANGER" vm create --name "$lifecycle_name" >/dev/null || die "vm create $lifecycle_name failed"
|
||||
show_out="$("$BANGER" vm show "$lifecycle_name")" || die "vm show after create failed"
|
||||
grep -q '"state": "running"' <<<"$show_out" || die "post-create state not running: $show_out"
|
||||
|
|
@ -251,9 +501,11 @@ set +e
|
|||
rc=$?
|
||||
set -e
|
||||
[[ "$rc" -ne 0 ]] || die "vm show still finds $lifecycle_name after delete"
|
||||
}
|
||||
|
||||
# --- vm set reconfiguration (vcpu change + restart) -------------------
|
||||
log 'vm set: create --vcpu 2 → stop → set --vcpu 4 → restart → nproc=4'
|
||||
scenario_vm_set() {
|
||||
log "${SMOKE_DESCS[vm_set]}"
|
||||
local nproc_before nproc_after rc
|
||||
"$BANGER" vm create --name smoke-set --vcpu 2 >/dev/null || die 'vm set: create failed'
|
||||
wait_for_ssh smoke-set || die 'vm set: initial ssh did not come up'
|
||||
|
||||
|
|
@ -279,9 +531,11 @@ set -e
|
|||
|| die "vm set: post-reconfig nproc got '$nproc_after', want 4 (spec change didn't land)"
|
||||
|
||||
"$BANGER" vm delete smoke-set >/dev/null || die 'vm set: delete failed'
|
||||
}
|
||||
|
||||
# --- vm restart (dedicated verb) --------------------------------------
|
||||
log 'vm restart: boot_id must change across the verb'
|
||||
scenario_vm_restart() {
|
||||
log "${SMOKE_DESCS[vm_restart]}"
|
||||
local boot_before boot_after
|
||||
"$BANGER" vm create --name smoke-restart >/dev/null || die 'vm restart: create failed'
|
||||
wait_for_ssh smoke-restart || die 'vm restart: initial ssh never came up'
|
||||
boot_before="$("$BANGER" vm ssh smoke-restart -- cat /proc/sys/kernel/random/boot_id | tr -d '[:space:]')"
|
||||
|
|
@ -295,9 +549,11 @@ boot_after="$("$BANGER" vm ssh smoke-restart -- cat /proc/sys/kernel/random/boot
|
|||
|| die "vm restart: boot_id unchanged ($boot_before); verb didn't actually reboot the guest"
|
||||
|
||||
"$BANGER" vm delete smoke-restart >/dev/null || die 'vm restart: delete failed'
|
||||
}
|
||||
|
||||
# --- vm kill (--signal KILL, forceful path) ---------------------------
|
||||
log 'vm kill --signal KILL: forceful terminate, state=stopped, no leaked dm device'
|
||||
scenario_vm_kill() {
|
||||
log "${SMOKE_DESCS[vm_kill]}"
|
||||
local dm_name show_out
|
||||
"$BANGER" vm create --name smoke-kill >/dev/null || die 'vm kill: create failed'
|
||||
dm_name="$("$BANGER" vm show smoke-kill 2>/dev/null | awk -F'"' '/"dm_dev"|fc-rootfs-/ {for(i=1;i<=NF;i++) if($i~/^fc-rootfs-/) print $i}' | head -1 || true)"
|
||||
"$BANGER" vm kill --signal KILL smoke-kill >/dev/null || die 'vm kill: verb failed'
|
||||
|
|
@ -309,9 +565,10 @@ if [[ -n "$dm_name" ]]; then
|
|||
fi
|
||||
fi
|
||||
"$BANGER" vm delete smoke-kill >/dev/null || die 'vm kill: delete failed'
|
||||
}
|
||||
|
||||
# --- vm prune (-f) ----------------------------------------------------
|
||||
log 'vm prune -f: removes stopped VMs, preserves running ones'
|
||||
scenario_vm_prune() {
|
||||
log "${SMOKE_DESCS[vm_prune]}"
|
||||
"$BANGER" vm create --name smoke-prune-running >/dev/null || die 'vm prune: create running failed'
|
||||
"$BANGER" vm create --name smoke-prune-stopped >/dev/null || die 'vm prune: create stopped failed'
|
||||
"$BANGER" vm stop smoke-prune-stopped >/dev/null || die 'vm prune: stop the stopped one failed'
|
||||
|
|
@ -324,9 +581,11 @@ if "$BANGER" vm show smoke-prune-stopped >/dev/null 2>&1; then
|
|||
fi
|
||||
|
||||
"$BANGER" vm delete smoke-prune-running >/dev/null || die 'vm prune: cleanup delete failed'
|
||||
}
|
||||
|
||||
# --- vm ports ---------------------------------------------------------
|
||||
log 'vm ports: sshd :22 visible from host, endpoint uses the VM DNS name'
|
||||
scenario_vm_ports() {
|
||||
log "${SMOKE_DESCS[vm_ports]}"
|
||||
local ports_out
|
||||
"$BANGER" vm create --name smoke-ports >/dev/null || die 'vm ports: create failed'
|
||||
wait_for_ssh smoke-ports || die 'vm ports: ssh did not come up'
|
||||
|
||||
|
|
@ -338,9 +597,11 @@ grep -q 'sshd' <<<"$ports_out" \
|
|||
|| die "vm ports: expected process 'sshd' in output; got: $ports_out"
|
||||
|
||||
"$BANGER" vm delete smoke-ports >/dev/null || die 'vm ports: delete failed'
|
||||
}
|
||||
|
||||
# --- workspace prepare --mode full_copy -------------------------------
|
||||
log 'workspace prepare --mode full_copy: alternate transfer path still delivers'
|
||||
scenario_workspace_full_copy() {
|
||||
log "${SMOKE_DESCS[workspace_full_copy]}"
|
||||
local fc_out
|
||||
"$BANGER" vm create --name smoke-fc >/dev/null || die 'workspace fc: create failed'
|
||||
"$BANGER" vm workspace prepare smoke-fc "$repodir" --mode full_copy >/dev/null \
|
||||
|| die 'workspace fc: prepare --mode full_copy failed'
|
||||
|
|
@ -350,27 +611,29 @@ grep -q 'smoke-workspace-marker' <<<"$fc_out" \
|
|||
|| die "workspace fc: marker missing in full_copy workspace: $fc_out"
|
||||
|
||||
"$BANGER" vm delete smoke-fc >/dev/null || die 'workspace fc: delete failed'
|
||||
}
|
||||
|
||||
# --- workspace export --base-commit (committed guest delta) -----------
|
||||
log 'workspace export --base-commit: guest-side commits captured in patch'
|
||||
scenario_workspace_basecommit() {
|
||||
log "${SMOKE_DESCS[workspace_basecommit]}"
|
||||
"$BANGER" vm create --name smoke-basecommit >/dev/null || die 'export base: create failed'
|
||||
"$BANGER" vm workspace prepare smoke-basecommit "$repodir" >/dev/null \
|
||||
|| die 'export base: prepare failed'
|
||||
|
||||
local base_sha
|
||||
base_sha="$("$BANGER" vm ssh smoke-basecommit -- sh -c 'cd /root/repo && git rev-parse HEAD' | tr -d '[:space:]')"
|
||||
[[ "${#base_sha}" -eq 40 ]] || die "export base: bad base sha: $base_sha"
|
||||
|
||||
"$BANGER" vm ssh smoke-basecommit -- sh -c "cd /root/repo && git -c user.email=smoke@smoke -c user.name=smoke checkout -b smoke-branch >/dev/null 2>&1 && echo committed-marker > smoke-committed.txt && git add smoke-committed.txt && git -c user.email=smoke@smoke -c user.name=smoke commit -q -m 'guest side'" \
|
||||
|| die 'export base: guest-side commit failed'
|
||||
|
||||
plain_patch="$runtime_dir/smoke-plain.diff"
|
||||
local plain_patch="$runtime_dir/smoke-plain.diff"
|
||||
"$BANGER" vm workspace export smoke-basecommit --output "$plain_patch" \
|
||||
|| die 'export base: plain export failed'
|
||||
if [[ -f "$plain_patch" ]] && grep -q 'smoke-committed.txt' "$plain_patch"; then
|
||||
die 'export base: plain export unexpectedly captured the guest-side commit'
|
||||
fi
|
||||
|
||||
base_patch="$runtime_dir/smoke-base.diff"
|
||||
local base_patch="$runtime_dir/smoke-base.diff"
|
||||
"$BANGER" vm workspace export smoke-basecommit --base-commit "$base_sha" --output "$base_patch" \
|
||||
|| die 'export base: --base-commit export failed'
|
||||
[[ -s "$base_patch" ]] || die 'export base: patch file empty'
|
||||
|
|
@ -378,15 +641,17 @@ grep -q 'smoke-committed.txt' "$base_patch" \
|
|||
|| die "export base: --base-commit patch missing committed marker (head: $(head -c 400 "$base_patch"))"
|
||||
|
||||
"$BANGER" vm delete smoke-basecommit >/dev/null || die 'export base: delete failed'
|
||||
}
|
||||
|
||||
# --- workspace + stop/start lifecycle ---------------------------------
|
||||
log 'workspace + stop/start: prepare → stop → start must yield a usable rootfs and preserve workspace contents'
|
||||
scenario_workspace_restart() {
|
||||
log "${SMOKE_DESCS[workspace_restart]}"
|
||||
"$BANGER" vm create --name smoke-wsrestart >/dev/null \
|
||||
|| die 'workspace stop/start: create failed'
|
||||
"$BANGER" vm workspace prepare smoke-wsrestart "$repodir" >/dev/null \
|
||||
|| die 'workspace stop/start: prepare failed'
|
||||
|
||||
# Sanity: marker is present before the stop/start cycle.
|
||||
local pre_out
|
||||
pre_out="$("$BANGER" vm ssh smoke-wsrestart -- cat /root/repo/smoke-file.txt)" \
|
||||
|| die 'workspace stop/start: pre-cycle ssh read failed'
|
||||
grep -q 'smoke-workspace-marker' <<<"$pre_out" \
|
||||
|
|
@ -399,6 +664,7 @@ grep -q 'smoke-workspace-marker' <<<"$pre_out" \
|
|||
wait_for_ssh smoke-wsrestart \
|
||||
|| die 'workspace stop/start: ssh did not come up after restart'
|
||||
|
||||
local post_out
|
||||
post_out="$("$BANGER" vm ssh smoke-wsrestart -- cat /root/repo/smoke-file.txt)" \
|
||||
|| die 'workspace stop/start: post-cycle ssh read failed'
|
||||
grep -q 'smoke-workspace-marker' <<<"$post_out" \
|
||||
|
|
@ -406,9 +672,11 @@ grep -q 'smoke-workspace-marker' <<<"$post_out" \
|
|||
|
||||
"$BANGER" vm delete smoke-wsrestart >/dev/null \
|
||||
|| die 'workspace stop/start: delete failed'
|
||||
}
|
||||
|
||||
# --- vm exec (workspace-aware, dirty detection, auto-prepare) ---------
|
||||
log 'vm exec: cd into prepared workspace, exit-code propagation, stale-warn, --auto-prepare resync'
|
||||
scenario_vm_exec() {
|
||||
log "${SMOKE_DESCS[vm_exec]}"
|
||||
local show_out exec_cat exec_pwd rc
|
||||
"$BANGER" vm create --name smoke-exec >/dev/null || die 'vm exec: create failed'
|
||||
"$BANGER" vm workspace prepare smoke-exec "$repodir" >/dev/null \
|
||||
|| die 'vm exec: workspace prepare failed'
|
||||
|
|
@ -446,7 +714,8 @@ set -e
|
|||
git add smoke-exec-new.txt
|
||||
git commit -q -m 'add smoke-exec-new.txt after prepare'
|
||||
)
|
||||
stale_stderr="$runtime_dir/smoke-exec-stale.err"
|
||||
local stale_stderr="$runtime_dir/smoke-exec-stale.err"
|
||||
local ls_rc
|
||||
set +e
|
||||
"$BANGER" vm exec smoke-exec -- ls smoke-exec-new.txt >/dev/null 2>"$stale_stderr"
|
||||
ls_rc=$?
|
||||
|
|
@ -459,6 +728,7 @@ grep -q -- '--auto-prepare' "$stale_stderr" \
|
|||
|| die "vm exec: stale warning didn't mention --auto-prepare hint; got: $(cat "$stale_stderr")"
|
||||
|
||||
# --auto-prepare: re-syncs workspace, then runs the command. New file appears.
|
||||
local auto_out
|
||||
auto_out="$("$BANGER" vm exec smoke-exec --auto-prepare -- cat smoke-exec-new.txt)" \
|
||||
|| die 'vm exec: --auto-prepare run failed'
|
||||
grep -q 'post-prepare-marker' <<<"$auto_out" \
|
||||
|
|
@ -466,14 +736,15 @@ grep -q 'post-prepare-marker' <<<"$auto_out" \
|
|||
|
||||
# After auto-prepare, the warning must NOT reappear on the next exec —
|
||||
# stored HEAD should now match the host.
|
||||
clean_stderr="$runtime_dir/smoke-exec-clean.err"
|
||||
local clean_stderr="$runtime_dir/smoke-exec-clean.err"
|
||||
"$BANGER" vm exec smoke-exec -- true 2>"$clean_stderr" \
|
||||
|| die 'vm exec: post-auto-prepare exec failed'
|
||||
if grep -q 'workspace stale' "$clean_stderr"; then
|
||||
die "vm exec: stale warning persisted after --auto-prepare; got: $(cat "$clean_stderr")"
|
||||
fi
|
||||
|
||||
# Reset repo state so later sections see the original tree.
|
||||
# Self-cleanup: scenario added a host-side commit, scenario rolls it back
|
||||
# so downstream repodir-class scenarios see the original tree.
|
||||
(
|
||||
cd "$repodir"
|
||||
git reset --hard HEAD~1 -q
|
||||
|
|
@ -483,6 +754,7 @@ fi
|
|||
# with a clear "not running" message. Done last so we can delete from
|
||||
# the stopped state without needing a restart.
|
||||
"$BANGER" vm stop smoke-exec >/dev/null || die 'vm exec: stop for not-running test failed'
|
||||
local stopped_err
|
||||
set +e
|
||||
stopped_err="$("$BANGER" vm exec smoke-exec -- true 2>&1)"
|
||||
rc=$?
|
||||
|
|
@ -492,10 +764,11 @@ grep -q 'not running' <<<"$stopped_err" \
|
|||
|| die "vm exec: stopped-VM error missing 'not running' phrase: $stopped_err"
|
||||
|
||||
"$BANGER" vm delete smoke-exec >/dev/null || die 'vm exec: delete failed'
|
||||
}
|
||||
|
||||
# --- ssh-config install / uninstall (HOME-isolated) -------------------
|
||||
log 'ssh-config --install / --uninstall: idempotent, survives round-trip'
|
||||
fake_home="$scratch_root/fake-home"
|
||||
scenario_ssh_config() {
|
||||
log "${SMOKE_DESCS[ssh_config]}"
|
||||
local fake_home="$scratch_root/fake-home"
|
||||
mkdir -p "$fake_home/.ssh"
|
||||
printf 'Host myserver\n HostName example.invalid\n' > "$fake_home/.ssh/config"
|
||||
|
||||
|
|
@ -508,6 +781,7 @@ printf 'Host myserver\n HostName example.invalid\n' > "$fake_home/.ssh/config"
|
|||
|| die 'ssh-config: install clobbered pre-existing content (!!)'
|
||||
|
||||
"$BANGER" ssh-config --install >/dev/null || die 'ssh-config: second install failed'
|
||||
local include_count
|
||||
include_count="$(grep -c '^Include .*banger' "$fake_home/.ssh/config")"
|
||||
[[ "$include_count" == "1" ]] \
|
||||
|| die "ssh-config: install not idempotent (Include appeared $include_count times)"
|
||||
|
|
@ -519,15 +793,28 @@ printf 'Host myserver\n HostName example.invalid\n' > "$fake_home/.ssh/config"
|
|||
grep -q '^Host myserver' "$fake_home/.ssh/config" \
|
||||
|| die 'ssh-config: uninstall nuked user content (!!)'
|
||||
)
|
||||
}
|
||||
|
||||
# --- NAT rule installation (per-VM MASQUERADE) ------------------------
|
||||
log 'NAT: --nat installs a per-VM MASQUERADE rule; no --nat means no rule'
|
||||
scenario_nat() {
|
||||
log "${SMOKE_DESCS[nat]}"
|
||||
if ! sudo -n iptables -t nat -S POSTROUTING >/dev/null 2>&1; then
|
||||
# Env-skip semantics:
|
||||
# - implicit (no --scenario, or mixed --scenario list): soft-skip.
|
||||
# - explicit (only "nat" selected): exit 77 to distinguish from
|
||||
# a real failure for callers that care.
|
||||
if (( SMOKE_EXPLICIT == 1 )) && (( ${#SMOKE_SELECTED[@]} == 1 )) \
|
||||
&& [[ "${SMOKE_SELECTED[0]}" == "nat" ]]; then
|
||||
log 'NAT: passwordless sudo iptables unavailable; explicit selection — exiting 77 (autotools skip)'
|
||||
exit 77
|
||||
fi
|
||||
log 'NAT: skipping — passwordless sudo iptables unavailable'
|
||||
else
|
||||
return 0
|
||||
fi
|
||||
|
||||
"$BANGER" vm create --name smoke-nat --nat >/dev/null || die 'NAT: create --nat failed'
|
||||
"$BANGER" vm create --name smoke-nocnat >/dev/null || die 'NAT: control create failed'
|
||||
|
||||
local nat_ip ctl_ip postrouting rule_count
|
||||
nat_ip="$("$BANGER" vm show smoke-nat 2>/dev/null | awk -F'"' '/"guest_ip"/ {print $4}')"
|
||||
ctl_ip="$("$BANGER" vm show smoke-nocnat 2>/dev/null | awk -F'"' '/"guest_ip"/ {print $4}')"
|
||||
[[ -n "$nat_ip" && -n "$ctl_ip" ]] || die "NAT: couldn't read guest IPs (nat='$nat_ip', ctl='$ctl_ip')"
|
||||
|
|
@ -552,10 +839,11 @@ else
|
|||
if grep -q -- "-s $nat_ip/32.*-j MASQUERADE" <<<"$postrouting"; then
|
||||
die "NAT: delete left a MASQUERADE rule behind for $nat_ip"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# --- invalid spec rejection + no artifact leak ------------------------
|
||||
log 'invalid spec rejection: --vcpu 0 must fail and leave no VM behind'
|
||||
scenario_invalid_spec() {
|
||||
log "${SMOKE_DESCS[invalid_spec]}"
|
||||
local pre_vms post_vms rc
|
||||
pre_vms="$("$BANGER" vm list --all 2>/dev/null | wc -l)"
|
||||
set +e
|
||||
"$BANGER" vm run --rm --vcpu 0 -- echo unused >/dev/null 2>&1
|
||||
|
|
@ -564,9 +852,11 @@ set -e
|
|||
[[ "$rc" -ne 0 ]] || die 'invalid spec: vm run succeeded despite --vcpu 0'
|
||||
post_vms="$("$BANGER" vm list --all 2>/dev/null | wc -l)"
|
||||
[[ "$pre_vms" == "$post_vms" ]] || die "invalid spec leaked a VM row: pre=$pre_vms, post=$post_vms"
|
||||
}
|
||||
|
||||
# --- invalid name rejection ------------------------------------------
|
||||
log 'invalid name rejection: uppercase / space / dot / leading-hyphen must all fail'
|
||||
scenario_invalid_name() {
|
||||
log "${SMOKE_DESCS[invalid_name]}"
|
||||
local pre_vms post_vms rc
|
||||
pre_vms="$("$BANGER" vm list --all 2>/dev/null | wc -l)"
|
||||
for bad in 'MyBox' 'my box' 'box.vm' '-box'; do
|
||||
set +e
|
||||
|
|
@ -578,5 +868,156 @@ done
|
|||
post_vms="$("$BANGER" vm list --all 2>/dev/null | wc -l)"
|
||||
[[ "$pre_vms" == "$post_vms" ]] \
|
||||
|| die "invalid name leaked VM row(s): pre=$pre_vms, post=$post_vms"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Dispatchers.
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
# run_serial calls each named scenario in-process. die() exits the
|
||||
# script with rc=1 on any failure (current behavior). Stdout is
|
||||
# unbuffered — identical to the pre-refactor experience.
|
||||
run_serial() {
|
||||
local name
|
||||
for name in "$@"; do
|
||||
"scenario_$name"
|
||||
done
|
||||
}
|
||||
|
||||
# run_repodir_chain runs the repodir scenarios serially (registry order)
|
||||
# inside a subshell so it can be backgrounded as one virtual job in the
|
||||
# parallel pool. Buffered stdout/stderr go to one logfile.
|
||||
run_repodir_chain() {
|
||||
local logfile="$runtime_dir/parallel-repodir.log"
|
||||
local rc=0
|
||||
(
|
||||
local name
|
||||
for name in "$@"; do
|
||||
"scenario_$name" || exit 1
|
||||
done
|
||||
) >"$logfile" 2>&1 || rc=$?
|
||||
return $rc
|
||||
}
|
||||
|
||||
# run_one_buffered runs a single scenario in a subshell with stdout/stderr
|
||||
# captured to a per-scenario logfile. On failure the buffer is dumped on
|
||||
# the main stderr; on success only the one-line PASS is shown.
|
||||
run_one_buffered() {
|
||||
local name=$1
|
||||
local logfile="$runtime_dir/parallel-$name.log"
|
||||
local rc=0
|
||||
( "scenario_$name" ) >"$logfile" 2>&1 || rc=$?
|
||||
if (( rc == 0 )); then
|
||||
printf '[smoke] %s: PASS\n' "$name" >&2
|
||||
else
|
||||
printf '[smoke] %s: FAIL (rc=%d)\n' "$name" "$rc" >&2
|
||||
sed 's/^/[smoke:'"$name"'] /' "$logfile" >&2
|
||||
fi
|
||||
return $rc
|
||||
}
|
||||
|
||||
# run_parallel splits the selection into pure singletons + a single fused
|
||||
# repodir chain (if any), runs them all in a slot-limited pool, then
|
||||
# runs global scenarios serially in registry order. Reports per-scenario
|
||||
# outcomes; final exit is non-zero iff any sub-job failed.
|
||||
run_parallel() {
|
||||
local jobs=$1; shift
|
||||
local selected=("$@")
|
||||
|
||||
local pure=() repodir_chain=() global=()
|
||||
local name
|
||||
for name in "${selected[@]}"; do
|
||||
case "${SMOKE_CLASS[$name]}" in
|
||||
pure) pure+=("$name") ;;
|
||||
repodir) repodir_chain+=("$name") ;;
|
||||
global) global+=("$name") ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Build the parallel-pool job list. The repodir chain (if any) is one
|
||||
# virtual job — it runs its scenarios serially inside a subshell and
|
||||
# competes with pure scenarios for a slot.
|
||||
local pool=()
|
||||
for name in "${pure[@]}"; do
|
||||
pool+=("pure:$name")
|
||||
done
|
||||
if (( ${#repodir_chain[@]} > 0 )); then
|
||||
pool+=("repodir:$(IFS=' '; echo "${repodir_chain[*]}")")
|
||||
fi
|
||||
|
||||
log "parallel pool: ${#pool[@]} job(s), ${#global[@]} global; jobs=$jobs"
|
||||
|
||||
declare -A pid_kind=()
|
||||
declare -A pid_label=()
|
||||
local active=0
|
||||
local failures=0
|
||||
|
||||
local job kind payload
|
||||
for job in "${pool[@]}"; do
|
||||
kind="${job%%:*}"
|
||||
payload="${job#*:}"
|
||||
while (( active >= jobs )); do
|
||||
if ! wait -n; then
|
||||
failures=$(( failures + 1 ))
|
||||
fi
|
||||
active=$(( active - 1 ))
|
||||
done
|
||||
if [[ "$kind" == "pure" ]]; then
|
||||
run_one_buffered "$payload" &
|
||||
else
|
||||
# repodir chain: payload is a space-separated list of names
|
||||
# shellcheck disable=SC2086
|
||||
( run_repodir_chain $payload ) &
|
||||
local p=$!
|
||||
pid_kind[$p]=repodir
|
||||
pid_label[$p]="$payload"
|
||||
fi
|
||||
active=$(( active + 1 ))
|
||||
done
|
||||
|
||||
# Drain remaining jobs.
|
||||
while (( active > 0 )); do
|
||||
if ! wait -n; then
|
||||
failures=$(( failures + 1 ))
|
||||
fi
|
||||
active=$(( active - 1 ))
|
||||
done
|
||||
|
||||
# Emit a one-line report for the repodir chain if it ran.
|
||||
if (( ${#repodir_chain[@]} > 0 )); then
|
||||
local logfile="$runtime_dir/parallel-repodir.log"
|
||||
if [[ -s "$logfile" ]]; then
|
||||
log "repodir chain log:"
|
||||
sed 's/^/[smoke:repodir] /' "$logfile" >&2
|
||||
fi
|
||||
fi
|
||||
|
||||
if (( failures > 0 )); then
|
||||
log "parallel pool: $failures job(s) failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Global scenarios: serial, in registry order, current behavior.
|
||||
if (( ${#global[@]} > 0 )); then
|
||||
log "global pool: ${#global[@]} scenario(s) (serial)"
|
||||
run_serial "${global[@]}"
|
||||
fi
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Main.
|
||||
# ---------------------------------------------------------------------
|
||||
install_preamble
|
||||
setup_fixtures
|
||||
|
||||
if (( SMOKE_JOBS == 1 )); then
|
||||
run_serial "${SMOKE_SELECTED[@]}"
|
||||
else
|
||||
run_parallel "$SMOKE_JOBS" "${SMOKE_SELECTED[@]}"
|
||||
fi
|
||||
|
||||
if (( ${#SMOKE_SELECTED[@]} == ${#SMOKE_SCENARIOS[@]} )); then
|
||||
log 'all scenarios passed'
|
||||
else
|
||||
log "scenario(s) passed: ${SMOKE_SELECTED[*]}"
|
||||
fi
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue