first commit
This commit is contained in:
commit
1354db6c39
90 files changed
+13123
No files matched your search
Executable
+381
@@ -0,0 +1,381 @@
|
||||
#!/usr/bin/env bash
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
# Copyright (C) 2026 Bryan Joshua Pedini
|
||||
|
||||
# tests/docker/cleanup.sh - restore the Docker image set recorded by snapshot.sh.
|
||||
#
|
||||
# Algorithm, in this order:
|
||||
# a. read the "before" maps from the snapshot; compute the "after" maps the same way
|
||||
# b. re-tag stolen names: every pre-existing repo:tag that now points at another
|
||||
# image (or vanished) is pointed back with `docker tag <id_before> <repo:tag>`
|
||||
# (e.g. a pre-existing archlinux:latest whose tag was taken by a newer pull)
|
||||
# c. untag new names: every repo:tag not in the snapshot -> `docker rmi --no-prune`
|
||||
# (only untags when the image has other names; deletes it on its last name)
|
||||
# d. delete new ids: every image id not in the snapshot -> `docker rmi -f --no-prune`
|
||||
# e. print a summary; exit 1 if a pre-existing repo:tag is missing or points elsewhere
|
||||
#
|
||||
# Safety rules:
|
||||
# - containers are never stopped or removed; an image used by ANY container
|
||||
# (running or stopped) is left in place and reported as left over;
|
||||
# - an image id that is in the snapshot is never deleted: --no-prune keeps
|
||||
# untagged parents, and the last name of a pre-existing image is never removed;
|
||||
# - idempotent: running it again, or when nothing changed, is a no-op.
|
||||
#
|
||||
# usage: cleanup.sh [--dry-run]
|
||||
# exit: 0 ok, 1 a pre-existing repo:tag is still missing/wrong, 2 usage/environment error
|
||||
set -euo pipefail
|
||||
|
||||
prog=${0##*/}
|
||||
here=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
|
||||
tags_file=$here/images-before.tsv
|
||||
ids_file=$here/image-ids-before.txt
|
||||
|
||||
usage() {
|
||||
cat <<USAGE
|
||||
usage: $prog [--dry-run]
|
||||
|
||||
Restore the Docker image set to the snapshot in
|
||||
$tags_file
|
||||
$ids_file
|
||||
(re-tag stolen pre-existing tags, untag new tags, delete new image ids).
|
||||
|
||||
-n, --dry-run print the docker commands instead of running them; the plan is
|
||||
simulated in memory and the summary shows the predicted end state
|
||||
-h, --help this help
|
||||
USAGE
|
||||
}
|
||||
|
||||
dry_run=0
|
||||
for arg in "$@"; do
|
||||
case $arg in
|
||||
-n | --dry-run) dry_run=1 ;;
|
||||
-h | --help) usage; exit 0 ;;
|
||||
*) printf '%s: unknown argument: %s\n' "$prog" "$arg" >&2; usage >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
warn() { printf '%s: WARNING: %s\n' "$prog" "$*" >&2; }
|
||||
die() { printf '%s: error: %s\n' "$prog" "$*" >&2; exit 2; }
|
||||
short() { local h=${1#sha256:}; printf '%s' "${h:0:12}"; }
|
||||
|
||||
[[ -f $tags_file && -f $ids_file ]] ||
|
||||
die "no snapshot (${tags_file##*/}, ${ids_file##*/} in $here); run tests/docker/snapshot.sh first"
|
||||
command -v docker >/dev/null 2>&1 || die "docker CLI not found"
|
||||
docker info >/dev/null 2>&1 || die "cannot talk to the Docker daemon"
|
||||
|
||||
# ---- state -----------------------------------------------------------------
|
||||
declare -A before_tag=() # repo:tag -> id (snapshot)
|
||||
declare -A before_id=() # id -> 1 (snapshot: every id, incl. untagged)
|
||||
declare -A tag_id=() # repo:tag -> id (current; simulated under --dry-run)
|
||||
declare -A tag_count=() # id -> number of repo:tag names on it
|
||||
declare -A id_present=() # id -> 1
|
||||
declare -A used_running=() # id -> "name (status), ..." running/paused/restarting containers
|
||||
declare -A used_stopped=() # id -> "name (status), ..." created/exited/dead containers
|
||||
declare -A id_names=() # id -> "repo:tag ..." as found at start (for messages only)
|
||||
declare -a done_retag=() done_untag=() done_remove=() skipped=() failed=()
|
||||
|
||||
# keys_sorted NAME: keys of associative array NAME, one per line, C-sorted.
|
||||
keys_sorted() {
|
||||
local -n _a=$1
|
||||
((${#_a[@]})) || return 0
|
||||
printf '%s\n' "${!_a[@]}" | LC_ALL=C sort
|
||||
}
|
||||
|
||||
# users_of ID: "running container(s): ...; stopped container(s): ..." or empty.
|
||||
users_of() {
|
||||
local s=
|
||||
[[ -z ${used_running[$1]:-} ]] || s="running container(s): ${used_running[$1]}"
|
||||
[[ -z ${used_stopped[$1]:-} ]] || s+="${s:+; }stopped container(s): ${used_stopped[$1]}"
|
||||
printf '%s' "$s"
|
||||
}
|
||||
|
||||
# run CMD...: print "+ CMD"; unless --dry-run, run it, show its output indented
|
||||
# and leave a one-line error in $run_err on failure.
|
||||
run_err=
|
||||
run() {
|
||||
local out rc=0
|
||||
local -a lines
|
||||
run_err=
|
||||
printf ' + %s\n' "$*"
|
||||
((!dry_run)) || return 0
|
||||
out=$("$@" 2>&1) || rc=$?
|
||||
if [[ -n $out ]]; then
|
||||
mapfile -t lines <<<"$out"
|
||||
printf ' %s\n' "${lines[@]}"
|
||||
fi
|
||||
((rc == 0)) || run_err=${out//$'\n'/ }
|
||||
return "$rc"
|
||||
}
|
||||
|
||||
skip() {
|
||||
printf ' - skip: %s\n' "$*"
|
||||
skipped+=("$*")
|
||||
}
|
||||
|
||||
read_before() {
|
||||
local rt id
|
||||
while IFS=$'\t' read -r rt id || [[ -n $rt ]]; do
|
||||
[[ -n $rt && $rt != \#* ]] || continue
|
||||
if [[ ! $id =~ ^sha256:[0-9a-f]{64}$ ]]; then
|
||||
warn "ignoring malformed line in ${tags_file##*/}: $rt $id"
|
||||
continue
|
||||
fi
|
||||
before_id[$id]=1
|
||||
[[ $rt == *'<none>'* ]] || before_tag[$rt]=$id
|
||||
done <"$tags_file"
|
||||
while read -r id || [[ -n $id ]]; do
|
||||
[[ -n $id && $id != \#* ]] || continue
|
||||
before_id[$id]=1
|
||||
done <"$ids_file"
|
||||
}
|
||||
|
||||
# read_after: (re)load the current image/tag/container state from the daemon.
|
||||
read_after() {
|
||||
local out rt id status name
|
||||
local -a cids=()
|
||||
tag_id=() tag_count=() id_present=() used_running=() used_stopped=()
|
||||
|
||||
out=$(docker images --no-trunc --format '{{.Repository}}:{{.Tag}}\t{{.ID}}') || die "docker images failed"
|
||||
while IFS=$'\t' read -r rt id; do
|
||||
[[ -n $rt && $rt != *'<none>'* ]] || continue
|
||||
tag_id[$rt]=$id
|
||||
tag_count[$id]=$((${tag_count[$id]:-0} + 1))
|
||||
done <<<"$out"
|
||||
|
||||
out=$(docker images -a -q --no-trunc) || die "docker images -a failed"
|
||||
while read -r id; do
|
||||
[[ -z $id ]] || id_present[$id]=1
|
||||
done <<<"$out"
|
||||
|
||||
# Containers can vanish between ps and inspect (other --rm runs finishing),
|
||||
# so inspect errors for individual containers are ignored.
|
||||
out=$(docker ps -a -q --no-trunc) || die "docker ps failed"
|
||||
[[ -z $out ]] || mapfile -t cids <<<"$out"
|
||||
((${#cids[@]})) || return 0
|
||||
out=$(docker inspect --format '{{.Image}}{{"\t"}}{{.State.Status}}{{"\t"}}{{.Name}}' "${cids[@]}" 2>/dev/null) || true
|
||||
while IFS=$'\t' read -r id status name; do
|
||||
[[ -n $id ]] || continue
|
||||
name="${name#/} ($status)"
|
||||
case $status in
|
||||
running | paused | restarting) used_running[$id]+="${used_running[$id]:+, }$name" ;;
|
||||
*) used_stopped[$id]+="${used_stopped[$id]:+, }$name" ;;
|
||||
esac
|
||||
done <<<"$out"
|
||||
}
|
||||
|
||||
# Model updates, applied after every successful (or, under --dry-run, planned)
|
||||
# command so later steps see the resulting state.
|
||||
model_tag() { # ID REF: REF now points at ID
|
||||
local id=$1 rt=$2 old=${tag_id[$2]:-}
|
||||
[[ -z $old ]] || tag_count[$old]=$((${tag_count[$old]:-1} - 1))
|
||||
tag_id[$rt]=$id
|
||||
tag_count[$id]=$((${tag_count[$id]:-0} + 1))
|
||||
id_present[$id]=1
|
||||
}
|
||||
model_untag() { # REF: REF is gone; its image too if it was the last name and unused
|
||||
local rt=$1 id=${tag_id[$1]} n
|
||||
unset 'tag_id[$rt]'
|
||||
n=$((${tag_count[$id]:-1} - 1))
|
||||
tag_count[$id]=$n
|
||||
if ((n <= 0)) && [[ -z ${before_id[$id]:-} && -z $(users_of "$id") ]]; then
|
||||
unset 'id_present[$id]'
|
||||
fi
|
||||
}
|
||||
model_remove() { # ID: image and all its names are gone
|
||||
local id=$1 rt
|
||||
unset 'id_present[$id]'
|
||||
tag_count[$id]=0
|
||||
for rt in "${!tag_id[@]}"; do
|
||||
[[ ${tag_id[$rt]} != "$id" ]] || unset 'tag_id[$rt]'
|
||||
done
|
||||
}
|
||||
|
||||
# ---- b. re-tag pre-existing names that were taken or vanished ----------------
|
||||
step_retag() {
|
||||
local rt id cur from n=0
|
||||
local -a keys
|
||||
mapfile -t keys < <(keys_sorted before_tag)
|
||||
for rt in "${keys[@]}"; do
|
||||
id=${before_tag[$rt]}
|
||||
cur=${tag_id[$rt]:-}
|
||||
[[ $cur != "$id" ]] || continue
|
||||
n=$((n + 1))
|
||||
if [[ -z ${id_present[$id]:-} ]] && ! docker image inspect "$id" >/dev/null 2>&1; then
|
||||
warn "!!! pre-existing image $(short "$id") for $rt NO LONGER EXISTS: cannot restore $rt (re-pull it by digest or re-snapshot)"
|
||||
continue
|
||||
fi
|
||||
if run docker tag "$id" "$rt"; then
|
||||
from="<missing>"
|
||||
[[ -z $cur ]] || from=$(short "$cur")
|
||||
done_retag+=("$rt: $from -> $(short "$id")")
|
||||
model_tag "$id" "$rt"
|
||||
else
|
||||
failed+=("docker tag $(short "$id") $rt: $run_err")
|
||||
fi
|
||||
done
|
||||
((n)) || echo " (nothing to do)"
|
||||
}
|
||||
|
||||
# ---- c. untag names that did not exist before -------------------------------
|
||||
step_untag() {
|
||||
local rt id users n=0
|
||||
local -a keys
|
||||
mapfile -t keys < <(keys_sorted tag_id)
|
||||
for rt in "${keys[@]}"; do
|
||||
[[ -z ${before_tag[$rt]:-} ]] || continue
|
||||
n=$((n + 1))
|
||||
id=${tag_id[$rt]}
|
||||
if ((${tag_count[$id]:-0} <= 1)); then
|
||||
# Last name on this image: `docker rmi` would delete the image as well.
|
||||
if [[ -n ${before_id[$id]:-} ]]; then
|
||||
skip "tag $rt: only name of pre-existing image $(short "$id"), kept so that image is not deleted"
|
||||
continue
|
||||
fi
|
||||
users=$(users_of "$id")
|
||||
if [[ -n $users ]]; then
|
||||
skip "tag $rt ($(short "$id")): image in use by $users"
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
if run docker rmi --no-prune "$rt"; then
|
||||
model_untag "$rt"
|
||||
if [[ -n ${id_present[$id]:-} ]]; then
|
||||
done_untag+=("$rt ($(short "$id"))")
|
||||
else
|
||||
done_untag+=("$rt ($(short "$id"), last name: image deleted)")
|
||||
fi
|
||||
else
|
||||
failed+=("docker rmi $rt: $run_err")
|
||||
fi
|
||||
done
|
||||
((n)) || echo " (nothing to do)"
|
||||
}
|
||||
|
||||
# ---- d. delete image ids that did not exist before ---------------------------
|
||||
step_remove() {
|
||||
local id users label pass nskip=0
|
||||
local -a keys todo=() retry=()
|
||||
local -A err=()
|
||||
mapfile -t keys < <(keys_sorted id_present)
|
||||
for id in "${keys[@]}"; do
|
||||
[[ -z ${before_id[$id]:-} ]] || continue
|
||||
users=$(users_of "$id")
|
||||
if [[ -n $users ]]; then
|
||||
skip "image $(short "$id")${id_names[$id]:+ (${id_names[$id]})}: in use by $users"
|
||||
nskip=$((nskip + 1))
|
||||
continue
|
||||
fi
|
||||
todo+=("$id")
|
||||
done
|
||||
if ((${#todo[@]} == 0)); then
|
||||
((nskip)) || echo " (nothing to do)"
|
||||
return 0
|
||||
fi
|
||||
# With --no-prune a parent image can only go after its children: retry
|
||||
# failures for as long as each pass makes progress.
|
||||
for ((pass = 1; ${#todo[@]} > 0; pass++)); do
|
||||
retry=()
|
||||
for id in "${todo[@]}"; do
|
||||
label="$(short "$id")${id_names[$id]:+ ${id_names[$id]}}"
|
||||
if ((!dry_run)) && ! docker image inspect "$id" >/dev/null 2>&1; then
|
||||
done_remove+=("$label (already gone)")
|
||||
continue
|
||||
fi
|
||||
if run docker rmi -f --no-prune "$id"; then
|
||||
done_remove+=("$label")
|
||||
model_remove "$id"
|
||||
else
|
||||
retry+=("$id")
|
||||
err[$id]=$run_err
|
||||
fi
|
||||
done
|
||||
((${#retry[@]} < ${#todo[@]})) || break
|
||||
todo=("${retry[@]}")
|
||||
done
|
||||
for id in "${retry[@]}"; do
|
||||
failed+=("docker rmi -f $(short "$id"): ${err[$id]}")
|
||||
done
|
||||
}
|
||||
|
||||
# section TITLE ITEM...: print a summary section with its item count.
|
||||
section() {
|
||||
local title=$1
|
||||
shift
|
||||
printf '%-44s %d\n' "$title" "$#"
|
||||
(($# == 0)) || printf ' %s\n' "$@"
|
||||
}
|
||||
|
||||
# ---- main -------------------------------------------------------------------
|
||||
read_before
|
||||
read_after
|
||||
while IFS= read -r rt; do
|
||||
[[ -z $rt ]] || id_names[${tag_id[$rt]}]+="${id_names[${tag_id[$rt]}]:+ }$rt"
|
||||
done < <(keys_sorted tag_id)
|
||||
|
||||
mode="cleanup"
|
||||
((!dry_run)) || mode="cleanup DRY RUN (no docker command is executed)"
|
||||
printf '== %s ==\n' "$mode"
|
||||
printf 'snapshot: %d tags, %d ids (taken %s)\n' "${#before_tag[@]}" "${#before_id[@]}" \
|
||||
"$(date -r "$tags_file" '+%Y-%m-%d %H:%M:%S %Z')"
|
||||
printf 'now: %d tags, %d ids, %d image(s) used by containers\n' "${#tag_id[@]}" "${#id_present[@]}" \
|
||||
"$(printf '%s\n' "${!used_running[@]}" "${!used_stopped[@]}" | sort -u | grep -c . || true)"
|
||||
|
||||
echo "-- b. re-tag pre-existing tags that were taken or vanished"
|
||||
step_retag
|
||||
((dry_run)) || read_after
|
||||
echo "-- c. untag tags that are new since the snapshot"
|
||||
step_untag
|
||||
((dry_run)) || read_after
|
||||
echo "-- d. delete image ids that are new since the snapshot"
|
||||
step_remove
|
||||
((dry_run)) || read_after
|
||||
|
||||
# ---- e. summary ---------------------------------------------------------------
|
||||
declare -a bad=() left=() gone=()
|
||||
while IFS= read -r rt; do
|
||||
[[ -n $rt ]] || continue
|
||||
id=${before_tag[$rt]}
|
||||
cur=${tag_id[$rt]:-}
|
||||
[[ $cur != "$id" ]] || continue
|
||||
if [[ -z ${id_present[$id]:-} ]]; then
|
||||
bad+=("$rt: snapshot image $(short "$id") no longer exists")
|
||||
elif [[ -z $cur ]]; then
|
||||
bad+=("$rt: tag missing (should be $(short "$id"))")
|
||||
else
|
||||
bad+=("$rt: points at $(short "$cur"), should be $(short "$id")")
|
||||
fi
|
||||
done < <(keys_sorted before_tag)
|
||||
while IFS= read -r rt; do
|
||||
[[ -n $rt && -z ${before_tag[$rt]:-} ]] || continue
|
||||
users=$(users_of "${tag_id[$rt]}")
|
||||
left+=("tag $rt -> $(short "${tag_id[$rt]}")${users:+ [in use by $users]}")
|
||||
done < <(keys_sorted tag_id)
|
||||
while IFS= read -r id; do
|
||||
[[ -n $id && -z ${before_id[$id]:-} ]] || continue
|
||||
users=$(users_of "$id")
|
||||
left+=("image $(short "$id")${id_names[$id]:+ ${id_names[$id]}}${users:+ [in use by $users]}")
|
||||
done < <(keys_sorted id_present)
|
||||
while IFS= read -r id; do
|
||||
[[ -n $id && -z ${id_present[$id]:-} ]] || continue
|
||||
gone+=("$(short "$id")")
|
||||
done < <(keys_sorted before_id)
|
||||
|
||||
echo
|
||||
if ((dry_run)); then
|
||||
echo "== summary (DRY RUN: nothing was changed; this is the predicted end state) =="
|
||||
else
|
||||
echo "== summary =="
|
||||
fi
|
||||
section "re-tagged (pre-existing tag restored):" "${done_retag[@]}"
|
||||
section "untagged (new tag removed):" "${done_untag[@]}"
|
||||
section "removed (new image deleted):" "${done_remove[@]}"
|
||||
section "skipped:" "${skipped[@]}"
|
||||
section "failed:" "${failed[@]}"
|
||||
section "left over (new since snapshot, still here):" "${left[@]}"
|
||||
section "pre-existing untagged ids gone (info only):" "${gone[@]}"
|
||||
section "PRE-EXISTING TAGS MISSING OR WRONG:" "${bad[@]}"
|
||||
if ((${#bad[@]})); then
|
||||
echo "result: FAILED - ${#bad[@]} pre-existing repo:tag(s) not restored"
|
||||
exit 1
|
||||
fi
|
||||
echo "result: OK - all ${#before_tag[@]} pre-existing repo:tags point at their snapshot ids"
|
||||
@@ -0,0 +1,26 @@
|
||||
# E2E distro matrix for `make test-e2e`: one "<label> <image>" per line.
|
||||
# Blank lines and lines starting with # are ignored.
|
||||
# <label> is passed to the container as SU_DISTRO and names the log file
|
||||
# tests/docker/logs/<label>.log; run a single one with `make test-e2e E2E_ONLY=<label>`.
|
||||
#
|
||||
# Rocky Linux (see discovery/rpm.md, section 0.2):
|
||||
# - the Docker Hub *library* image `rockylinux:9` is a stale Rocky 9.3 build from
|
||||
# 2023-11-28 (96 pending upgrades on day one), so the upstream-maintained
|
||||
# `rockylinux/rockylinux:9` (Rocky 9.8, same image as quay.io/rockylinux/rockylinux:9)
|
||||
# is used instead;
|
||||
# - `rockylinux:10` does not exist in the library at all; `rockylinux/rockylinux:10`
|
||||
# is the official Rocky 10 image (same image id as quay.io/rockylinux/rockylinux:10).
|
||||
#
|
||||
# label image
|
||||
debian-12 debian:12
|
||||
debian-13 debian:13
|
||||
ubuntu-22.04 ubuntu:22.04
|
||||
ubuntu-24.04 ubuntu:24.04
|
||||
ubuntu-latest ubuntu:latest
|
||||
archlinux archlinux:latest
|
||||
almalinux-9 almalinux:9
|
||||
almalinux-10 almalinux:10
|
||||
rockylinux-9 rockylinux/rockylinux:9
|
||||
rockylinux-10 rockylinux/rockylinux:10
|
||||
opensuse-tumbleweed opensuse/tumbleweed:latest
|
||||
opensuse-leap opensuse/leap:latest
|
||||
Executable
+45
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env bash
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
# Copyright (C) 2026 Bryan Joshua Pedini
|
||||
|
||||
# tests/docker/run-e2e.sh - run the E2E suite in ONE distro container.
|
||||
#
|
||||
# usage: run-e2e.sh <label> <image>
|
||||
# e.g. run-e2e.sh debian-12 debian:12
|
||||
#
|
||||
# The repository is mounted read-only at /work and tests/e2e/entrypoint.sh is run
|
||||
# with bash inside <image>. SU_DISTRO=<label> tells the suite which distro it is
|
||||
# on. Output is shown and also written to tests/docker/logs/<label>.log.
|
||||
# Exit status is the container's (i.e. the suite's) exit status.
|
||||
set -euo pipefail
|
||||
|
||||
prog=${0##*/}
|
||||
if (($# != 2)); then
|
||||
printf 'usage: %s <label> <image>\n' "$prog" >&2
|
||||
exit 1
|
||||
fi
|
||||
label=$1 image=$2
|
||||
if [[ ! $label =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]]; then
|
||||
printf '%s: invalid label %q (allowed: letters, digits, . _ -)\n' "$prog" "$label" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." # repository root
|
||||
if [[ ! -f tests/e2e/entrypoint.sh ]]; then
|
||||
printf '%s: tests/e2e/entrypoint.sh not found in %s\n' "$prog" "$PWD" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_dir=tests/docker/logs
|
||||
log=$log_dir/$label.log
|
||||
mkdir -p -- "$log_dir"
|
||||
|
||||
printf '== e2e %s (%s) %s ==\n' "$label" "$image" "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" | tee -- "$log"
|
||||
status=0
|
||||
docker run --rm -i \
|
||||
-v "$PWD":/work:ro -w /work \
|
||||
-e LC_ALL=C -e DEBIAN_FRONTEND=noninteractive -e SU_DISTRO="$label" \
|
||||
"$image" bash tests/e2e/entrypoint.sh 2>&1 | tee -a -- "$log" || status=${PIPESTATUS[0]}
|
||||
if ((status == 0)); then result=PASS; else result=FAIL; fi
|
||||
printf '== e2e %s: %s (exit %d), log: %s ==\n' "$label" "$result" "$status" "$log" | tee -a -- "$log"
|
||||
exit "$status"
|
||||
Executable
+94
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env bash
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
# Copyright (C) 2026 Bryan Joshua Pedini
|
||||
|
||||
# tests/docker/snapshot.sh - record the host's Docker image set BEFORE tests run,
|
||||
# so tests/docker/cleanup.sh can restore it afterwards.
|
||||
#
|
||||
# Writes, next to this script:
|
||||
# images-before.tsv repo:tag<TAB>image-id (docker images --no-trunc, sorted)
|
||||
# image-ids-before.txt every image id, incl. untagged (docker images -a -q --no-trunc, sort -u)
|
||||
#
|
||||
# An existing snapshot is NEVER overwritten without --force: running `make test`
|
||||
# twice must not "launder" images pulled by the first run into the baseline.
|
||||
# Without --force an existing snapshot is kept and the script exits 0, so
|
||||
# `make docker-snapshot` / `make test` can call it every time.
|
||||
#
|
||||
# usage: snapshot.sh [--force]
|
||||
set -euo pipefail
|
||||
|
||||
prog=${0##*/}
|
||||
here=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
|
||||
tags_file=$here/images-before.tsv
|
||||
ids_file=$here/image-ids-before.txt
|
||||
|
||||
usage() {
|
||||
cat <<USAGE
|
||||
usage: $prog [--force]
|
||||
|
||||
Record the current Docker image set to
|
||||
$tags_file
|
||||
$ids_file
|
||||
|
||||
An existing snapshot is kept unless --force is given. Only use --force when
|
||||
the images currently on the host are exactly the ones you want to keep: every
|
||||
image present at snapshot time is treated as pre-existing by cleanup.sh.
|
||||
USAGE
|
||||
}
|
||||
|
||||
die() {
|
||||
printf '%s: error: %s\n' "$prog" "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
force=0
|
||||
for arg in "$@"; do
|
||||
case $arg in
|
||||
--force) force=1 ;;
|
||||
-h | --help) usage; exit 0 ;;
|
||||
*) printf '%s: unknown argument: %s\n' "$prog" "$arg" >&2; usage >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -e $tags_file && -e $ids_file ]]; then
|
||||
if ((!force)); then
|
||||
printf '%s: snapshot already exists (taken %s), NOT overwriting it.\n' \
|
||||
"$prog" "$(date -r "$tags_file" '+%Y-%m-%d %H:%M:%S %Z')" >&2
|
||||
printf '%s: to re-baseline on purpose: %s --force\n' "$prog" "$0" >&2
|
||||
exit 0
|
||||
fi
|
||||
elif [[ -e $tags_file || -e $ids_file ]]; then
|
||||
((force)) || die "incomplete snapshot (only one of ${tags_file##*/}, ${ids_file##*/} exists); inspect it, then re-run with --force"
|
||||
fi
|
||||
|
||||
docker info >/dev/null 2>&1 || die "cannot talk to the Docker daemon (is it running, are you in the docker group?)"
|
||||
|
||||
tmp_tags=$(mktemp "$tags_file.XXXXXX.tmp")
|
||||
tmp_ids=$(mktemp "$ids_file.XXXXXX.tmp")
|
||||
trap 'rm -f -- "$tmp_tags" "$tmp_ids"' EXIT
|
||||
|
||||
# Exactly the commands of the original manual snapshot (plain `sort`, so the
|
||||
# order follows the caller's locale, byte-identical to running them by hand).
|
||||
# Consumers must not depend on the order: cleanup.sh reads both files into maps.
|
||||
docker images --no-trunc --format '{{.Repository}}:{{.Tag}}\t{{.ID}}' | sort >"$tmp_tags"
|
||||
docker images -a -q --no-trunc | sort -u >"$tmp_ids"
|
||||
|
||||
if [[ -e $tags_file && -e $ids_file ]]; then
|
||||
# --force over an existing snapshot: show what the baseline gains, so an
|
||||
# accidental re-baseline after a test run is at least visible.
|
||||
gained_tags=$(LC_ALL=C comm -13 <(LC_ALL=C sort "$tags_file") <(LC_ALL=C sort "$tmp_tags"))
|
||||
gained_ids=$(LC_ALL=C comm -13 <(LC_ALL=C sort -u "$ids_file") <(LC_ALL=C sort -u "$tmp_ids"))
|
||||
if [[ -n $gained_tags || -n $gained_ids ]]; then
|
||||
printf '%s: WARNING: the new baseline adopts images that were not in the old one:\n' "$prog" >&2
|
||||
if [[ -n $gained_tags ]]; then mapfile -t lines <<<"$gained_tags"; printf ' tag %s\n' "${lines[@]}" >&2; fi
|
||||
if [[ -n $gained_ids ]]; then mapfile -t lines <<<"$gained_ids"; printf ' id %s\n' "${lines[@]}" >&2; fi
|
||||
fi
|
||||
fi
|
||||
|
||||
chmod 0644 "$tmp_tags" "$tmp_ids"
|
||||
mv -f -- "$tmp_tags" "$tags_file"
|
||||
mv -f -- "$tmp_ids" "$ids_file"
|
||||
trap - EXIT
|
||||
|
||||
printf '%s: snapshot written: %d tags -> %s, %d ids -> %s\n' "$prog" \
|
||||
"$(wc -l <"$tags_file")" "${tags_file#"$PWD"/}" "$(wc -l <"$ids_file")" "${ids_file#"$PWD"/}"
|
||||
Executable
+57
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env bash
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
# Copyright (C) 2026 Bryan Joshua Pedini
|
||||
|
||||
# tests/e2e/entrypoint.sh - runs INSIDE one distro container (started by tests/docker/run-e2e.sh,
|
||||
# as root, repository mounted read-only at /work, SU_DISTRO=<label>):
|
||||
# 1. installs the applet as /usr/local/bin/skeptical-updater
|
||||
# 2. builds the deterministic fixture for the package-manager family (su-dummy-{a,b,c}
|
||||
# 1.0 installed, 2.0 offered by a local repo; the distro's own repos are disabled;
|
||||
# su-dummy-orphan: an unrelated, automatically installed package nothing needs)
|
||||
# 3. runs tests/e2e/*.bats with the vendored bats-core against the REAL package manager
|
||||
# Exit status: the bats status (or 1 if the setup failed).
|
||||
set -euo pipefail
|
||||
export LC_ALL=C DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." # repository root (read-only)
|
||||
log() { printf '== entrypoint: %s\n' "$*"; }
|
||||
die() {
|
||||
printf '== entrypoint: ERROR: %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
[[ $(id -u) -eq 0 ]] || die "must run as root"
|
||||
t0=$SECONDS
|
||||
|
||||
install -m 0755 skeptical-updater /usr/local/bin/skeptical-updater
|
||||
|
||||
if command -v pacman >/dev/null 2>&1; then
|
||||
family=pacman
|
||||
# FIXTURE_SYNC=1: a system whose sync database was synced once (--no-refresh lists from it)
|
||||
FIXTURE_ISOLATE=1 FIXTURE_ORPHAN=1 FIXTURE_SYNC=1 bash tests/e2e/fixtures/pacman.sh
|
||||
elif command -v apt-get >/dev/null 2>&1; then
|
||||
family=apt
|
||||
SU_FIXTURE_OFFLINE=1 SU_FIXTURE_ORPHAN=1 SU_FIXTURE_B_AUTO=1 bash tests/e2e/fixtures/apt.sh
|
||||
elif command -v zypper >/dev/null 2>&1 || command -v dnf >/dev/null 2>&1 || command -v yum >/dev/null 2>&1; then
|
||||
family=rpm
|
||||
SU_FIXTURE_ISOLATE=1 SU_FIXTURE_ORPHAN=1 SU_FIXTURE_B_AUTO=1 bash tests/e2e/fixtures/rpm.sh
|
||||
else
|
||||
die "no supported package manager in this image"
|
||||
fi
|
||||
export SU_FAMILY=$family
|
||||
# shellcheck source=/dev/null
|
||||
log "${SU_DISTRO:-?}: family=$family, fixture ready after $((SECONDS - t0))s ($(. /etc/os-release && printf '%s' "$PRETTY_NAME"), bash $BASH_VERSION)"
|
||||
|
||||
TMPDIR=$(mktemp -d /tmp/su-e2e.XXXXXX)
|
||||
export TMPDIR
|
||||
|
||||
# bats needs find(1) to expand a directory argument; openSUSE base images may lack it
|
||||
if command -v find >/dev/null 2>&1; then
|
||||
targets=(tests/e2e)
|
||||
else
|
||||
targets=(tests/e2e/*.bats)
|
||||
fi
|
||||
status=0
|
||||
bash tests/vendor/bats-core/bin/bats --tap "${targets[@]}" || status=$?
|
||||
log "${SU_DISTRO:-?}: bats exited $status after $((SECONDS - t0))s"
|
||||
exit "$status"
|
||||
Executable
+251
@@ -0,0 +1,251 @@
|
||||
#!/usr/bin/env bash
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
# Copyright (C) 2026 Bryan Joshua Pedini
|
||||
|
||||
# apt.sh - APT e2e fixture for skeptical-updater (Debian/Ubuntu containers).
|
||||
# Copy of discovery/apt/fixture.sh; the only changes are the SU_FIXTURE_STRICT_DEP knob
|
||||
# (su-dummy-a 2.0 depends on plain su-dummy-b by default, like the pacman and rpm
|
||||
# fixtures, so excluding su-dummy-b does not keep su-dummy-a back), SU_FIXTURE_ORPHAN and
|
||||
# SU_FIXTURE_B_AUTO.
|
||||
#
|
||||
# Builds a local *flat* APT repository at /srv/su-repo containing dummy packages
|
||||
# and installs the old (1.0) versions, so that after `apt-get update` the package
|
||||
# manager reports deterministic upgrades:
|
||||
#
|
||||
# su-dummy-a 1.0 -> 2.0 (Depends: su-dummy-b, ships a conffile;
|
||||
# SU_FIXTURE_STRICT_DEP=1: 2.0 Depends: su-dummy-b (>= 2.0))
|
||||
# su-dummy-b 1.0 -> 2.0 (dependency of su-dummy-a)
|
||||
# su-dummy-c 1.0 -> 2.0 (standalone)
|
||||
#
|
||||
# Optional extras (SU_FIXTURE_EXTRA=1) for "kept back" / removal / virtual-package
|
||||
# scenarios:
|
||||
# su-dummy-d 1.0 -> 2.0 2.0 adds a NEW dependency (su-dummy-newdep, not installed)
|
||||
# => plain `upgrade` keeps it back, `--with-new-pkgs upgrade`
|
||||
# and `dist-upgrade` install it
|
||||
# su-dummy-e 1.0 -> 2.0 2.0 Conflicts: su-dummy-f (installed, no 2.0 exists)
|
||||
# => only `dist-upgrade` can do it (removes su-dummy-f) - except with apt 3.x
|
||||
# solver3 (ubuntu:latest = 26.04) which refuses to remove the manually
|
||||
# installed su-dummy-f and keeps su-dummy-e back even for dist-upgrade
|
||||
# su-dummy-f 1.0 installed, never upgradable
|
||||
# su-dummy-w 1.0 -> 2.0 Depends: su-virtual | su-dummy-alt (virtual + alternative)
|
||||
# su-dummy-v 1.0 Provides: su-virtual
|
||||
# (+ su-dummy-newdep 1.0 and su-dummy-alt 1.0: only in the repo, never pre-installed)
|
||||
#
|
||||
# Environment knobs (all optional):
|
||||
# SU_REPO_DIR=/srv/su-repo where the repo is created (wiped first; must match /srv/su-*, /tmp/su-*, /var/tmp/su-*)
|
||||
# SU_OLD_DIR=/srv/su-old where the 1.0 .debs are kept (NOT in the repo)
|
||||
# SU_FIXTURE_INDEXER=shell shell | scanpackages | ftparchive
|
||||
# shell = no extra tools needed (dpkg-deb -f + sha256sum)
|
||||
# scanpackages = dpkg-scanpackages (installs dpkg-dev if missing)
|
||||
# ftparchive = apt-ftparchive (installs apt-utils if missing)
|
||||
# SU_FIXTURE_OFFLINE=1 disable the distro's real sources (hermetic: no network at all)
|
||||
# SU_FIXTURE_EXTRA=1 add the extra scenario packages listed above
|
||||
# SU_FIXTURE_BAD=1 add su-dummy-bad: 1.0 installed, 2.0 has a postinst that fails
|
||||
# (apt-get upgrade then exits 100) - for error-path tests
|
||||
# SU_FIXTURE_STRICT_DEP=1 su-dummy-a 2.0 depends on "su-dummy-b (>= 2.0)" (as in discovery/apt):
|
||||
# holding su-dummy-b then also keeps su-dummy-a back
|
||||
# SU_FIXTURE_ORPHAN=1 also install su-dummy-orphan 1.0 marked automatically installed with
|
||||
# nothing depending on it (a pre-existing autoremove candidate)
|
||||
# SU_FIXTURE_B_AUTO=1 mark su-dummy-b automatically installed (a dependency of su-dummy-a,
|
||||
# like the pacman fixture's --asdeps)
|
||||
#
|
||||
# Must run as root inside a throwaway container, e.g.:
|
||||
# docker run --rm -i -v "$PWD/discovery/apt:/d:ro" debian:12 bash /d/fixture.sh
|
||||
set -euo pipefail
|
||||
export LC_ALL=C DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
REPO_DIR=${SU_REPO_DIR:-/srv/su-repo}
|
||||
OLD_DIR=${SU_OLD_DIR:-/srv/su-old}
|
||||
INDEXER=${SU_FIXTURE_INDEXER:-shell}
|
||||
OFFLINE=${SU_FIXTURE_OFFLINE:-0}
|
||||
EXTRA=${SU_FIXTURE_EXTRA:-0}
|
||||
BAD=${SU_FIXTURE_BAD:-0}
|
||||
STRICT_DEP=${SU_FIXTURE_STRICT_DEP:-0}
|
||||
ORPHAN=${SU_FIXTURE_ORPHAN:-0}
|
||||
B_AUTO=${SU_FIXTURE_B_AUTO:-0}
|
||||
SOURCES_FILE=/etc/apt/sources.list.d/su-repo.list
|
||||
T0=$(date +%s)
|
||||
|
||||
log() { printf '[fixture] %s\n' "$*"; }
|
||||
die() { printf '[fixture] ERROR: %s\n' "$*" >&2; exit 1; }
|
||||
|
||||
[ "$(id -u)" -eq 0 ] || die "must run as root"
|
||||
command -v apt-get >/dev/null || die "apt-get not found (not a Debian/Ubuntu image?)"
|
||||
command -v dpkg-deb >/dev/null || die "dpkg-deb not found"
|
||||
case $INDEXER in shell|scanpackages|ftparchive) ;; *) die "bad SU_FIXTURE_INDEXER=$INDEXER" ;; esac
|
||||
# the directories are wiped with rm -rf: only accept obviously fixture-owned locations
|
||||
for d in "$REPO_DIR" "$OLD_DIR"; do
|
||||
case $d in /srv/su-*|/tmp/su-*|/var/tmp/su-*) ;; *) die "refusing to wipe '$d' (must match /srv/su-*, /tmp/su-* or /var/tmp/su-*)" ;; esac
|
||||
done
|
||||
|
||||
# ---------------------------------------------------------------- build debs
|
||||
# mkdeb NAME VERSION OUTDIR [extra control line]...
|
||||
# If $MKDEB_POSTINST is set it becomes the body of the package's postinst script.
|
||||
mkdeb() {
|
||||
local name=$1 ver=$2 out=$3 root line
|
||||
shift 3
|
||||
root=$(mktemp -d)
|
||||
mkdir -p "$root/DEBIAN" "$root/usr/share/su-dummy" "$root/etc/su-dummy"
|
||||
{
|
||||
printf 'Package: %s\nVersion: %s\nSection: misc\nPriority: optional\n' "$name" "$ver"
|
||||
printf 'Architecture: all\nMaintainer: Skeptical Updater Fixture <su-fixture@example.invalid>\n'
|
||||
for line in "$@"; do printf '%s\n' "$line"; done
|
||||
printf 'Description: dummy package %s for skeptical-updater tests\n' "$name"
|
||||
printf ' This package only exists to exercise the package manager.\n'
|
||||
printf ' .\n'
|
||||
printf ' It ships one data file and one conffile (version %s).\n' "$ver"
|
||||
} > "$root/DEBIAN/control"
|
||||
echo "$ver" > "$root/usr/share/su-dummy/$name"
|
||||
echo "version=$ver" > "$root/etc/su-dummy/$name.conf"
|
||||
echo "/etc/su-dummy/$name.conf" > "$root/DEBIAN/conffiles"
|
||||
if [ -n "${MKDEB_POSTINST:-}" ]; then
|
||||
printf '#!/bin/sh\n%s\n' "$MKDEB_POSTINST" > "$root/DEBIAN/postinst"
|
||||
chmod 755 "$root/DEBIAN/postinst"
|
||||
fi
|
||||
mkdir -p "$out"
|
||||
# -Zgzip: readable by every dpkg in the supported images; --root-owner-group: files owned by root
|
||||
dpkg-deb --root-owner-group -Zgzip --build "$root" "$out/${name}_${ver}_all.deb" >/dev/null
|
||||
rm -rf "$root"
|
||||
}
|
||||
|
||||
rm -rf "$REPO_DIR" "$OLD_DIR"
|
||||
mkdir -p "$REPO_DIR" "$OLD_DIR"
|
||||
|
||||
# old versions (installed straight away with dpkg, NOT published in the repo)
|
||||
mkdeb su-dummy-a 1.0 "$OLD_DIR" 'Depends: su-dummy-b'
|
||||
mkdeb su-dummy-b 1.0 "$OLD_DIR"
|
||||
mkdeb su-dummy-c 1.0 "$OLD_DIR"
|
||||
# new versions (published in the repo)
|
||||
if [ "$STRICT_DEP" = 1 ]; then
|
||||
mkdeb su-dummy-a 2.0 "$REPO_DIR" 'Depends: su-dummy-b (>= 2.0)'
|
||||
else
|
||||
mkdeb su-dummy-a 2.0 "$REPO_DIR" 'Depends: su-dummy-b'
|
||||
fi
|
||||
mkdeb su-dummy-b 2.0 "$REPO_DIR"
|
||||
mkdeb su-dummy-c 2.0 "$REPO_DIR"
|
||||
|
||||
if [ "$ORPHAN" = 1 ]; then
|
||||
mkdeb su-dummy-orphan 1.0 "$OLD_DIR" # installed, never upgradable
|
||||
fi
|
||||
|
||||
if [ "$EXTRA" = 1 ]; then
|
||||
mkdeb su-dummy-d 1.0 "$OLD_DIR"
|
||||
mkdeb su-dummy-d 2.0 "$REPO_DIR" 'Depends: su-dummy-newdep'
|
||||
mkdeb su-dummy-newdep 1.0 "$REPO_DIR" # only in the repo, never pre-installed
|
||||
mkdeb su-dummy-e 1.0 "$OLD_DIR"
|
||||
mkdeb su-dummy-e 2.0 "$REPO_DIR" 'Conflicts: su-dummy-f'
|
||||
mkdeb su-dummy-f 1.0 "$OLD_DIR"
|
||||
mkdeb su-dummy-v 1.0 "$OLD_DIR" 'Provides: su-virtual'
|
||||
mkdeb su-dummy-alt 1.0 "$REPO_DIR" # alternative target, not installed
|
||||
mkdeb su-dummy-w 1.0 "$OLD_DIR" 'Depends: su-virtual | su-dummy-alt'
|
||||
mkdeb su-dummy-w 2.0 "$REPO_DIR" 'Depends: su-virtual | su-dummy-alt'
|
||||
fi
|
||||
|
||||
if [ "$BAD" = 1 ]; then
|
||||
mkdeb su-dummy-bad 1.0 "$OLD_DIR"
|
||||
# shellcheck disable=SC2016 # the postinst body is expanded by the package's /bin/sh, not here
|
||||
MKDEB_POSTINST='[ "$1" = configure ] && { echo "su-dummy-bad: postinst failing on purpose" >&2; exit 1; }; exit 0' \
|
||||
mkdeb su-dummy-bad 2.0 "$REPO_DIR"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------- real sources
|
||||
if [ "$OFFLINE" = 1 ]; then
|
||||
mkdir -p /srv/su-disabled-sources
|
||||
for f in /etc/apt/sources.list /etc/apt/sources.list.d/*.sources /etc/apt/sources.list.d/*.list; do
|
||||
[ -e "$f" ] || continue
|
||||
case $f in "$SOURCES_FILE") continue ;; esac
|
||||
mv "$f" "/srv/su-disabled-sources/$(basename "$f")"
|
||||
done
|
||||
log "OFFLINE: real sources moved to /srv/su-disabled-sources"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------- indexing
|
||||
install_tool() { # install_tool PACKAGE : needs the real mirrors
|
||||
[ "$OFFLINE" = 1 ] && die "SU_FIXTURE_INDEXER=$INDEXER needs network to install $1; use shell with OFFLINE"
|
||||
log "installing $1 (needs network)"
|
||||
apt-get update -qq >/dev/null
|
||||
apt-get install -y -qq --no-install-recommends "$1" >/dev/null
|
||||
}
|
||||
|
||||
index_shell() {
|
||||
local f
|
||||
: > Packages
|
||||
for f in ./*.deb; do
|
||||
f=${f#./}
|
||||
dpkg-deb -f "$f" >> Packages
|
||||
printf 'Filename: ./%s\nSize: %s\nMD5sum: %s\nSHA256: %s\n\n' \
|
||||
"$f" "$(stat -c %s "$f")" "$(md5sum < "$f" | cut -d' ' -f1)" "$(sha256sum < "$f" | cut -d' ' -f1)" >> Packages
|
||||
done
|
||||
{
|
||||
printf 'Origin: su-fixture\nLabel: su-fixture\nSuite: su-repo\nCodename: su-repo\n'
|
||||
printf 'Date: %s\n' "$(date -u '+%a, %d %b %Y %H:%M:%S UTC')"
|
||||
printf 'SHA256:\n %s %s Packages\n' "$(sha256sum < Packages | cut -d' ' -f1)" "$(stat -c %s Packages)"
|
||||
} > Release
|
||||
}
|
||||
|
||||
index_scanpackages() {
|
||||
command -v dpkg-scanpackages >/dev/null || install_tool dpkg-dev
|
||||
dpkg-scanpackages --multiversion . /dev/null > Packages 2>/dev/null
|
||||
# dpkg-scanpackages does not write a Release file; make a minimal one
|
||||
{
|
||||
printf 'Origin: su-fixture\nLabel: su-fixture\nSuite: su-repo\nCodename: su-repo\n'
|
||||
printf 'Date: %s\n' "$(date -u '+%a, %d %b %Y %H:%M:%S UTC')"
|
||||
printf 'SHA256:\n %s %s Packages\n' "$(sha256sum < Packages | cut -d' ' -f1)" "$(stat -c %s Packages)"
|
||||
} > Release
|
||||
}
|
||||
|
||||
index_ftparchive() {
|
||||
command -v apt-ftparchive >/dev/null || install_tool apt-utils
|
||||
apt-ftparchive packages . > Packages
|
||||
apt-ftparchive \
|
||||
-o APT::FTPArchive::Release::Origin=su-fixture \
|
||||
-o APT::FTPArchive::Release::Label=su-fixture \
|
||||
-o APT::FTPArchive::Release::Suite=su-repo \
|
||||
-o APT::FTPArchive::Release::Codename=su-repo \
|
||||
release . > Release
|
||||
}
|
||||
|
||||
( cd "$REPO_DIR" && "index_$INDEXER" )
|
||||
log "repo $REPO_DIR indexed with '$INDEXER': $(grep -c '^Package:' "$REPO_DIR/Packages") packages"
|
||||
|
||||
echo "deb [trusted=yes] file:$REPO_DIR ./" > "$SOURCES_FILE"
|
||||
|
||||
# ---------------------------------------------------------------- install old versions
|
||||
# --force-confnew + </dev/null: never block on a conffile prompt if the fixture is re-run in a dirty container
|
||||
dpkg --force-confnew -i "$OLD_DIR"/*.deb >/dev/null </dev/null
|
||||
if [ "$ORPHAN" = 1 ]; then
|
||||
apt-mark auto su-dummy-orphan >/dev/null
|
||||
fi
|
||||
if [ "$B_AUTO" = 1 ]; then
|
||||
apt-mark auto su-dummy-b >/dev/null
|
||||
fi
|
||||
log "installed (old): $(dpkg-query -W -f '${db:Status-Abbrev} ${Package}=${Version}\n' 'su-dummy-*' 2>/dev/null | awk '$1 == "ii" {printf "%s ", $2}')"
|
||||
|
||||
# ---------------------------------------------------------------- refresh + verify
|
||||
# drop stale lists of our own repo first: apt compares mtimes of file: indexes with 1s resolution,
|
||||
# so a repo regenerated within the same second would otherwise be seen as "unchanged"
|
||||
rm -f /var/lib/apt/lists/"$(printf '%s' "$REPO_DIR" | tr / _)"_*
|
||||
apt-get update -qq >/dev/null
|
||||
|
||||
fail=0
|
||||
upg=$(apt list --upgradable 2>/dev/null | awk -F/ '/^su-dummy-/{print $1}' | sort | tr '\n' ' ')
|
||||
for p in su-dummy-a su-dummy-b su-dummy-c; do
|
||||
case " $upg" in *" $p "*) ;; *) log "MISSING from upgradable: $p"; fail=1 ;; esac
|
||||
done
|
||||
sim=$(apt-get -s --with-new-pkgs upgrade 2>/dev/null | awk '/^Inst su-dummy-(a|b|c) /{n++} END{print n+0}')
|
||||
[ "$sim" -ge 3 ] || { log "simulated upgrade only covers $sim of 3 core packages"; fail=1; }
|
||||
[ "$fail" -eq 0 ] || die "fixture verification failed"
|
||||
|
||||
log "upgradable dummy packages: $upg"
|
||||
if [ "$ORPHAN" = 1 ]; then
|
||||
apt-get -s autoremove 2>/dev/null | grep -q '^Remv su-dummy-orphan ' || die "su-dummy-orphan is not an autoremove candidate"
|
||||
log "pre-existing orphan (autoremove candidate): su-dummy-orphan"
|
||||
fi
|
||||
real=$(apt list --upgradable 2>/dev/null | awk -F/ 'NR>1 && $1 !~ /^su-dummy-/{print $1}' | tr '\n' ' ')
|
||||
if [ -n "$real" ]; then
|
||||
log "NOTE: real-world upgradable packages present in this base image: $real"
|
||||
else
|
||||
log "NOTE: no real-world upgradable packages in this base image right now"
|
||||
fi
|
||||
# shellcheck source=/dev/null
|
||||
log "OK in $(( $(date +%s) - T0 ))s (image: $(. /etc/os-release; echo "$PRETTY_NAME"))"
|
||||
Executable
+222
@@ -0,0 +1,222 @@
|
||||
#!/usr/bin/env bash
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
# Copyright (C) 2026 Bryan Joshua Pedini
|
||||
|
||||
# Deterministic pacman fixture for skeptical-updater E2E tests.
|
||||
# (tests/e2e/fixtures/pacman.sh: copy of discovery/pacman/fixture.sh plus the FIXTURE_ORPHAN
|
||||
# knob; tests/e2e/entrypoint.sh runs it with FIXTURE_ISOLATE=1 FIXTURE_ORPHAN=1.)
|
||||
#
|
||||
# Creates three dummy packages installed at 1.0 and a local pacman repo
|
||||
# ([su-dummy], file:///srv/su-repo) that offers them at 2.0:
|
||||
#
|
||||
# su-dummy-a depends=(su-dummy-b) installed explicitly
|
||||
# su-dummy-b installed as a dependency (--asdeps)
|
||||
# su-dummy-c installed explicitly
|
||||
#
|
||||
# It also pre-wires /etc/pacman.d/ignorepkg (empty) and the matching
|
||||
# "Include" line inside the [options] section of /etc/pacman.conf, i.e. the
|
||||
# same layout that pman expects (IgnorePkg = <pkg> lines).
|
||||
#
|
||||
# Run as root inside an Arch container:
|
||||
# docker run --rm -i -v "$PWD/discovery/pacman:/d:ro" archlinux:latest bash /d/fixture.sh
|
||||
#
|
||||
# Environment (all optional):
|
||||
# FIXTURE_BUILDER tar (default) | makepkg
|
||||
# tar : hand-built .pkg.tar.zst (.PKGINFO + bsdtar). No network, no
|
||||
# fakeroot, no build user. ~0.1 s.
|
||||
# makepkg : real makepkg run as the unprivileged user "su-builder" via
|
||||
# runuser. Needs fakeroot (installed with `pacman -Sy fakeroot`
|
||||
# if missing, so it needs network). Slower.
|
||||
# FIXTURE_ISOLATE 0 (default) | 1
|
||||
# 1 comments out every repo section except [su-dummy], so `pacman -Qu`
|
||||
# reports EXACTLY the three dummy packages (no real upgrades, no network
|
||||
# needed after the fixture is built).
|
||||
# FIXTURE_STRICT_DEP 0 (default) | 1
|
||||
# 1 makes su-dummy-a 2.0 depend on "su-dummy-b>=2.0" (instead of plain
|
||||
# "su-dummy-b"), so excluding su-dummy-b from an upgrade (--ignore /
|
||||
# IgnorePkg) while upgrading su-dummy-a fails with unsatisfied deps.
|
||||
# FIXTURE_ORPHAN 0 (default) | 1 -> also install su-dummy-orphan 1.0 --asdeps with nothing
|
||||
# requiring it (a pre-existing orphan, `pacman -Qdtq` lists it)
|
||||
# FIXTURE_REPO repo directory (default /srv/su-repo)
|
||||
# FIXTURE_SYNC 0 (default) | 1 -> run `pacman -Sy` at the end
|
||||
#
|
||||
# The fixture deliberately does NOT sync by default: the tool under test is
|
||||
# expected to perform the refresh itself (pacman -Sy), after which
|
||||
# `pacman -Qu` lists su-dummy-{a,b,c} (1.0-1 -> 2.0-1).
|
||||
|
||||
export LC_ALL=C
|
||||
|
||||
FIXTURE_BUILDER=${FIXTURE_BUILDER:-tar}
|
||||
FIXTURE_ISOLATE=${FIXTURE_ISOLATE:-0}
|
||||
FIXTURE_STRICT_DEP=${FIXTURE_STRICT_DEP:-0}
|
||||
FIXTURE_REPO=${FIXTURE_REPO:-/srv/su-repo}
|
||||
FIXTURE_SYNC=${FIXTURE_SYNC:-0}
|
||||
FIXTURE_ORPHAN=${FIXTURE_ORPHAN:-0}
|
||||
FIXTURE_REPO_NAME=su-dummy
|
||||
FIXTURE_EPOCH=1700000000 # fixed build date => reproducible tarballs
|
||||
|
||||
fx_log() { printf 'fixture: %s\n' "$*" >&2; }
|
||||
fx_die() { fx_log "ERROR: $*"; exit 1; }
|
||||
|
||||
# pacman wrapper: stdout dropped, the harmless "database file for X does not
|
||||
# exist" warnings (fresh image, no sync yet) filtered, real errors kept, exit
|
||||
# status preserved.
|
||||
fx_pacman() {
|
||||
local rc=0
|
||||
pacman "$@" 2>&1 >/dev/null | { grep -v "^warning: database file for" >&2 || true; } || rc=$?
|
||||
return $rc
|
||||
}
|
||||
|
||||
# fx_build_tar <outdir> <name> <pkgver> <pkgdesc> [depend...]
|
||||
# Hand-built package: only a .PKGINFO and the payload are required by pacman -U
|
||||
# and repo-add (no .MTREE / .BUILDINFO needed; `pacman -Qk` is happy too).
|
||||
fx_build_tar() {
|
||||
local out=$1 name=$2 ver=$3 desc=$4; shift 4
|
||||
local w payload
|
||||
w=$(mktemp -d)
|
||||
payload="usr/share/su-dummy/$name"
|
||||
mkdir -p "$w/$payload"
|
||||
printf '%s\n' "$ver" >"$w/$payload/VERSION"
|
||||
{
|
||||
printf 'pkgname = %s\n' "$name"
|
||||
printf 'pkgbase = %s\n' "$name"
|
||||
printf 'pkgver = %s-1\n' "$ver"
|
||||
printf 'pkgdesc = %s\n' "$desc"
|
||||
printf 'url = https://example.invalid/su-dummy\n'
|
||||
printf 'builddate = %s\n' "$FIXTURE_EPOCH"
|
||||
printf 'packager = su-fixture <su-fixture@example.invalid>\n'
|
||||
printf 'size = %s\n' "$(wc -c <"$w/$payload/VERSION")"
|
||||
printf 'arch = any\n'
|
||||
printf 'license = custom\n'
|
||||
local d
|
||||
for d in "$@"; do printf 'depend = %s\n' "$d"; done
|
||||
} >"$w/.PKGINFO"
|
||||
(cd "$w" && bsdtar --zstd --uid 0 --gid 0 --uname root --gname root \
|
||||
-cf "$out/$name-$ver-1-any.pkg.tar.zst" .PKGINFO usr)
|
||||
rm -rf "$w"
|
||||
}
|
||||
|
||||
# fx_build_makepkg <outdir> <name> <pkgver> <pkgdesc> [depend...]
|
||||
# Real makepkg. makepkg refuses to run as root, so run it as an unprivileged
|
||||
# user with `runuser -u` (no sudo needed; `su -s /bin/bash -c` also works).
|
||||
fx_build_makepkg() {
|
||||
local out=$1 name=$2 ver=$3 desc=$4; shift 4
|
||||
local w depends="" d
|
||||
for d in "$@"; do depends+="'$d' "; done
|
||||
w=/home/su-builder/build/$name-$ver
|
||||
mkdir -p "$w"
|
||||
cat >"$w/PKGBUILD" <<EOF
|
||||
pkgname=$name
|
||||
pkgver=$ver
|
||||
pkgrel=1
|
||||
pkgdesc='$desc'
|
||||
arch=('any')
|
||||
url='https://example.invalid/su-dummy'
|
||||
license=('custom')
|
||||
options=('!debug' '!strip') # arch=any payload: no debugedit/binutils needed
|
||||
depends=($depends)
|
||||
package() {
|
||||
install -Dm644 /dev/stdin "\$pkgdir/usr/share/su-dummy/\$pkgname/VERSION" <<<"\$pkgver"
|
||||
}
|
||||
EOF
|
||||
chown -R su-builder: "$w"
|
||||
runuser -u su-builder -- env \
|
||||
PKGDEST="$out" SOURCE_DATE_EPOCH="$FIXTURE_EPOCH" \
|
||||
PACKAGER='su-fixture <su-fixture@example.invalid>' \
|
||||
bash -c "cd '$w' && makepkg --force --nodeps --noconfirm --nocolor >/dev/null 2>&1" \
|
||||
|| { runuser -u su-builder -- bash -c "cd '$w' && makepkg --force --nodeps --noconfirm --nocolor" >&2; fx_die "makepkg failed for $name"; }
|
||||
}
|
||||
|
||||
fx_prepare_makepkg() {
|
||||
if ! command -v fakeroot >/dev/null; then
|
||||
fx_log "makepkg builder: installing fakeroot (needs network)"
|
||||
fx_pacman -Sy --noconfirm --needed fakeroot
|
||||
fi
|
||||
id su-builder >/dev/null 2>&1 || useradd --create-home su-builder
|
||||
}
|
||||
|
||||
fx_build() { # fx_build <outdir> <name> <ver> <desc> [dep...]
|
||||
case $FIXTURE_BUILDER in
|
||||
tar) fx_build_tar "$@" ;;
|
||||
makepkg) fx_build_makepkg "$@" ;;
|
||||
*) fx_die "unknown FIXTURE_BUILDER=$FIXTURE_BUILDER" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
fx_main() {
|
||||
set -euo pipefail
|
||||
[ "$(id -u)" -eq 0 ] || fx_die "must run as root"
|
||||
local old new
|
||||
old=$(mktemp -d); new=$FIXTURE_REPO
|
||||
rm -rf "$FIXTURE_REPO"; mkdir -p "$FIXTURE_REPO"
|
||||
|
||||
chmod 755 "$old"
|
||||
if [ "$FIXTURE_BUILDER" = makepkg ]; then
|
||||
fx_prepare_makepkg
|
||||
chown su-builder: "$old" "$new" # PKGDEST dirs must be writable by the build user
|
||||
fi
|
||||
|
||||
# 1.0 packages (installed) and 2.0 packages (offered by the repo)
|
||||
fx_build "$old" su-dummy-a 1.0 'Skeptical-updater dummy package A' su-dummy-b
|
||||
fx_build "$old" su-dummy-b 1.0 'Skeptical-updater dummy package B (dependency of A)'
|
||||
fx_build "$old" su-dummy-c 1.0 'Skeptical-updater dummy package C (independent)'
|
||||
local a2_dep=su-dummy-b
|
||||
[ "$FIXTURE_STRICT_DEP" = 1 ] && a2_dep='su-dummy-b>=2.0'
|
||||
fx_build "$new" su-dummy-a 2.0 'Skeptical-updater dummy package A' "$a2_dep"
|
||||
fx_build "$new" su-dummy-b 2.0 'Skeptical-updater dummy package B (dependency of A)'
|
||||
fx_build "$new" su-dummy-c 2.0 'Skeptical-updater dummy package C (independent)'
|
||||
|
||||
# Local repository database (repo-add ships with pacman)
|
||||
(cd "$FIXTURE_REPO" && repo-add --quiet "$FIXTURE_REPO_NAME.db.tar.gz" ./*.pkg.tar.zst)
|
||||
chmod -R a+rX "$FIXTURE_REPO"; chmod a+rx /srv 2>/dev/null || true
|
||||
|
||||
# pacman.conf: register the repo (appended at the end => lowest priority)
|
||||
if ! grep -q "^\[$FIXTURE_REPO_NAME\]" /etc/pacman.conf; then
|
||||
{
|
||||
printf '\n[%s]\n' "$FIXTURE_REPO_NAME"
|
||||
printf 'SigLevel = Optional TrustAll\n'
|
||||
printf 'Server = file://%s\n' "$FIXTURE_REPO"
|
||||
} >>/etc/pacman.conf
|
||||
fi
|
||||
|
||||
# pman-style ignore file, included from the [options] section
|
||||
# (inserted right after the FIRST "[options]" header; appending at EOF would
|
||||
# land in whatever section happens to be last).
|
||||
: >/etc/pacman.d/ignorepkg
|
||||
if ! grep -q '^Include *= */etc/pacman.d/ignorepkg' /etc/pacman.conf; then
|
||||
sed -i '0,/^\[options\]/s||[options]\nInclude = /etc/pacman.d/ignorepkg|' /etc/pacman.conf
|
||||
fi
|
||||
|
||||
if [ "$FIXTURE_ISOLATE" = 1 ]; then
|
||||
# comment out all repos except [su-dummy]; keep [options] intact
|
||||
awk -v keep="$FIXTURE_REPO_NAME" '
|
||||
/^\[.*\][[:space:]]*$/ { sec=$0; sub(/^\[/,"",sec); sub(/\].*$/,"",sec); active = (sec=="options" || sec==keep) }
|
||||
{ if (sec != "" && !active && $0 !~ /^[[:space:]]*#/ && $0 !~ /^[[:space:]]*$/) print "#" $0; else print }
|
||||
' /etc/pacman.conf >/etc/pacman.conf.new && mv /etc/pacman.conf.new /etc/pacman.conf
|
||||
fi
|
||||
|
||||
# Install the 1.0 packages: b as a dependency, a and c explicit.
|
||||
fx_pacman -U --noconfirm "$old"/su-dummy-a-1.0-1-any.pkg.tar.zst \
|
||||
"$old"/su-dummy-b-1.0-1-any.pkg.tar.zst \
|
||||
"$old"/su-dummy-c-1.0-1-any.pkg.tar.zst
|
||||
fx_pacman -D --asdeps su-dummy-b
|
||||
if [ "$FIXTURE_ORPHAN" = 1 ]; then
|
||||
fx_build "$old" su-dummy-orphan 1.0 'Skeptical-updater dummy package ORPHAN (nothing requires it)'
|
||||
fx_pacman -U --noconfirm --asdeps "$old"/su-dummy-orphan-1.0-1-any.pkg.tar.zst
|
||||
pacman -Qdtq | grep -qx su-dummy-orphan || fx_die "su-dummy-orphan is not an orphan"
|
||||
fx_log "pre-existing orphan: su-dummy-orphan"
|
||||
fi
|
||||
rm -rf "$old"
|
||||
|
||||
if [ "$FIXTURE_SYNC" = 1 ]; then fx_pacman -Sy; fi
|
||||
|
||||
fx_log "builder=$FIXTURE_BUILDER isolate=$FIXTURE_ISOLATE repo=$FIXTURE_REPO"
|
||||
fx_log "installed: $(pacman -Q su-dummy-a su-dummy-b su-dummy-c 2>/dev/null | tr '\n' ';')"
|
||||
fx_log "repo list: $(pacman-conf --repo-list | tr '\n' ' ')"
|
||||
}
|
||||
|
||||
# Run when executed; do nothing when sourced (so tests can reuse fx_build_tar etc.
|
||||
# without inheriting `set -e`).
|
||||
if [ "${BASH_SOURCE[0]}" = "$0" ]; then
|
||||
fx_main "$@"
|
||||
fi
|
||||
Executable
+217
@@ -0,0 +1,217 @@
|
||||
#!/usr/bin/env bash
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
# Copyright (C) 2026 Bryan Joshua Pedini
|
||||
|
||||
# rpm.sh - deterministic E2E fixture for RPM distros (dnf: Alma/Rocky/..., zypper: openSUSE)
|
||||
# Copy of discovery/rpm/fixture.sh; the only changes are the SU_FIXTURE_ISOLATE,
|
||||
# SU_FIXTURE_ORPHAN and SU_FIXTURE_B_AUTO knobs.
|
||||
#
|
||||
# Builds three dummy packages with rpmbuild, installs version 1.0 of each, publishes
|
||||
# version 2.0 in a local file:// repo and registers that repo, so that the package
|
||||
# manager reports exactly these upgrades:
|
||||
#
|
||||
# su-dummy-a 1.0-1 -> 2.0-1 (noarch, Requires: su-dummy-b)
|
||||
# su-dummy-b 1.0-1 -> 2.0-1 (noarch)
|
||||
# su-dummy-c 1.0-1 -> 2.0-1 (ARCH-SPECIFIC, so the ".x86_64" suffix path is exercised)
|
||||
#
|
||||
# Optional (SU_FIXTURE_EXTRAS=1):
|
||||
# - installs su-dummy-d 1.0 (NOT upgradable): Requires su-dummy-a, /bin/sh (file dep),
|
||||
# "su-dummy-b >= 1.0" (versioned), "(su-dummy-c if su-dummy-b)" (rich dep) and rpmlib(...)
|
||||
# -> for reverse-dep / dependency-closure tests;
|
||||
# - publishes su-dummy-r 1.0 (Requires su-dummy-s) and su-dummy-s 1.0 in the repo, NOT installed
|
||||
# -> for install/remove/--clean-deps tests.
|
||||
#
|
||||
# Optional (SU_FIXTURE_ISOLATE=1): after the build tools are installed, move the distro's
|
||||
# own repository (and, on zypper, service) definitions to /srv/su-disabled-repos, so that
|
||||
# only the su-dummy repo is left: the upgradable list is exactly su-dummy-{a,b,c} and later
|
||||
# refreshes/upgrades do not depend on the state of the real mirrors.
|
||||
#
|
||||
# Optional (SU_FIXTURE_ORPHAN=1): su-dummy-orphan 1.0 is installed as a dependency (of
|
||||
# su-dummy-orphan-parent, installed through the package manager and then removed with rpm -e),
|
||||
# so it is a pre-existing unneeded package (dnf repoquery --unneeded / zypper packages --unneeded).
|
||||
#
|
||||
# Optional (SU_FIXTURE_B_AUTO=1): su-dummy-b counts as installed as a dependency (dnf: `dnf mark
|
||||
# remove`, zypper: /var/lib/zypp/AutoInstalled), like the pacman fixture's --asdeps.
|
||||
#
|
||||
# Re-runnable: re-running resets everything to the "1.0 installed, 2.0 available" state.
|
||||
#
|
||||
# Usage (inside the container, as root): bash fixture.sh
|
||||
# docker run --rm -i -v <project>/discovery/rpm:/d:ro <image> bash /d/fixture.sh
|
||||
set -euo pipefail
|
||||
export LC_ALL=C
|
||||
|
||||
T0=$SECONDS
|
||||
log() { printf '[fixture] %s\n' "$*" >&2; }
|
||||
die() { printf '[fixture] ERROR: %s\n' "$*" >&2; exit 1; }
|
||||
[ "$(id -u)" -eq 0 ] || die "must run as root"
|
||||
|
||||
# ---- 1. backend detection ---------------------------------------------------
|
||||
BACKEND=
|
||||
if command -v zypper >/dev/null 2>&1; then BACKEND=zypper
|
||||
elif command -v dnf >/dev/null 2>&1; then BACKEND=dnf
|
||||
elif command -v yum >/dev/null 2>&1; then BACKEND=yum
|
||||
else die "neither zypper nor dnf/yum found"; fi
|
||||
log "backend=$BACKEND"
|
||||
|
||||
TOP=/var/tmp/su-fixture # rpmbuild topdir
|
||||
REPO=/var/lib/su-dummy-repo # file:// repo (only 2.0 packages)
|
||||
SPEC_DIR=$TOP/SPECS
|
||||
|
||||
# ---- 2. build tools ---------------------------------------------------------
|
||||
t=$SECONDS
|
||||
case $BACKEND in
|
||||
zypper) zypper --non-interactive --quiet install --no-recommends rpm-build createrepo_c >/dev/null ;;
|
||||
dnf|yum) "$BACKEND" -y -q install --setopt=install_weak_deps=False rpm-build createrepo_c >/dev/null ;;
|
||||
esac
|
||||
command -v rpmbuild >/dev/null || die "rpmbuild missing after install"
|
||||
command -v createrepo_c >/dev/null || die "createrepo_c missing after install"
|
||||
log "build tools installed in $((SECONDS - t))s"
|
||||
|
||||
# ---- 2b. optional isolation: only the su-dummy repo stays configured ----------
|
||||
if [ "${SU_FIXTURE_ISOLATE:-0}" = 1 ]; then
|
||||
mkdir -p /srv/su-disabled-repos
|
||||
case $BACKEND in
|
||||
zypper) defs=(/etc/zypp/repos.d/*.repo /etc/zypp/services.d/*.service) ;;
|
||||
dnf|yum) defs=(/etc/yum.repos.d/*.repo) ;;
|
||||
esac
|
||||
for f in "${defs[@]}"; do
|
||||
[ -e "$f" ] || continue
|
||||
case ${f##*/} in su-dummy.repo) continue ;; esac
|
||||
mv "$f" "/srv/su-disabled-repos/${f##*/}"
|
||||
done
|
||||
log "ISOLATE: real repository definitions moved to /srv/su-disabled-repos"
|
||||
fi
|
||||
|
||||
# ---- 3. build RPMs ----------------------------------------------------------
|
||||
rm -rf "$TOP" "$REPO"
|
||||
mkdir -p "$TOP"/{BUILD,BUILDROOT,RPMS,SRPMS,SOURCES,SPECS} "$REPO"
|
||||
|
||||
# mkspec <name> <version> <arch|noarch> <extra spec header lines...>
|
||||
mkspec() {
|
||||
local name=$1 ver=$2 arch=$3; shift 3
|
||||
local f=$SPEC_DIR/$name-$ver.spec
|
||||
{
|
||||
echo "Name: $name"
|
||||
echo "Version: $ver"
|
||||
echo "Release: 1"
|
||||
echo "Summary: skeptical-updater dummy $name (v$ver)"
|
||||
echo "License: MIT"
|
||||
[ "$arch" = noarch ] && echo "BuildArch: noarch"
|
||||
local l; for l in "$@"; do echo "$l"; done
|
||||
echo "%global debug_package %{nil}"
|
||||
echo "%description"
|
||||
echo "Dummy package $name version $ver for skeptical-updater tests."
|
||||
echo ""
|
||||
echo "Second paragraph of $name $ver (blank line above on purpose)."
|
||||
echo "%install"
|
||||
echo "mkdir -p %{buildroot}/usr/share/su-dummy"
|
||||
echo "echo '$name $ver' > %{buildroot}/usr/share/su-dummy/$name"
|
||||
echo "%files"
|
||||
echo "/usr/share/su-dummy/$name"
|
||||
} > "$f"
|
||||
printf '%s\n' "$f"
|
||||
}
|
||||
|
||||
build() { rpmbuild -bb --quiet --define "_topdir $TOP" "$1" >/dev/null 2>"$TOP/rpmbuild.err" \
|
||||
|| { cat "$TOP/rpmbuild.err" >&2; die "rpmbuild failed for $1"; }; }
|
||||
|
||||
t=$SECONDS
|
||||
for v in 1.0 2.0; do
|
||||
build "$(mkspec su-dummy-a $v noarch 'Requires: su-dummy-b')"
|
||||
build "$(mkspec su-dummy-b $v noarch)"
|
||||
build "$(mkspec su-dummy-c $v arch)"
|
||||
done
|
||||
if [ "${SU_FIXTURE_EXTRAS:-0}" = 1 ]; then
|
||||
build "$(mkspec su-dummy-s 1.0 noarch)" # published in the repo, NOT installed
|
||||
build "$(mkspec su-dummy-r 1.0 noarch 'Requires: su-dummy-s')" # (used to test remove/--clean-deps)
|
||||
build "$(mkspec su-dummy-d 1.0 noarch \
|
||||
'Requires: su-dummy-a' \
|
||||
'Requires: /bin/sh' \
|
||||
'Requires: su-dummy-b >= 1.0' \
|
||||
'Requires: (su-dummy-c if su-dummy-b)' \
|
||||
'Requires: rpmlib(CompressedFileNames)')"
|
||||
fi
|
||||
if [ "${SU_FIXTURE_ORPHAN:-0}" = 1 ]; then
|
||||
build "$(mkspec su-dummy-orphan 1.0 noarch)"
|
||||
build "$(mkspec su-dummy-orphan-parent 1.0 noarch 'Requires: su-dummy-orphan')"
|
||||
fi
|
||||
log "rpmbuild done in $((SECONDS - t))s"
|
||||
|
||||
# ---- 4. install 1.0 (rpm directly: distro-agnostic, resets to 1.0 on re-run) ----
|
||||
shopt -s nullglob
|
||||
v1=( "$TOP"/RPMS/*/su-dummy-[abc]-1.0-1.*.rpm )
|
||||
[ "${#v1[@]}" -eq 3 ] || die "expected 3 built 1.0 rpms, got ${#v1[@]}"
|
||||
rpm -Uvh --oldpackage --replacepkgs "${v1[@]}" >/dev/null
|
||||
if [ "${SU_FIXTURE_EXTRAS:-0}" = 1 ]; then
|
||||
rpm -Uvh --oldpackage --replacepkgs "$TOP"/RPMS/noarch/su-dummy-d-1.0-1.*.rpm >/dev/null
|
||||
fi
|
||||
|
||||
# ---- 5. publish 2.0 as a local repo -------------------------------------------
|
||||
v2=( "$TOP"/RPMS/*/su-dummy-[abc]-2.0-1.*.rpm )
|
||||
[ "${#v2[@]}" -eq 3 ] || die "expected 3 built 2.0 rpms, got ${#v2[@]}"
|
||||
cp "${v2[@]}" "$REPO"/
|
||||
if [ "${SU_FIXTURE_EXTRAS:-0}" = 1 ]; then cp "$TOP"/RPMS/noarch/su-dummy-[rs]-1.0-1.*.rpm "$REPO"/; fi
|
||||
if [ "${SU_FIXTURE_ORPHAN:-0}" = 1 ]; then cp "$TOP"/RPMS/noarch/su-dummy-orphan*-1.0-1.*.rpm "$REPO"/; fi
|
||||
createrepo_c --quiet "$REPO" >/dev/null
|
||||
|
||||
# ---- 6. register repo ---------------------------------------------------------
|
||||
case $BACKEND in
|
||||
zypper)
|
||||
zypper --non-interactive --quiet removerepo su-dummy >/dev/null 2>&1 || true
|
||||
zypper --non-interactive --quiet addrepo --no-gpgcheck --no-check --refresh "file://$REPO" su-dummy >/dev/null
|
||||
t=$SECONDS
|
||||
zypper --non-interactive --quiet refresh su-dummy >/dev/null
|
||||
;;
|
||||
dnf|yum)
|
||||
cat > /etc/yum.repos.d/su-dummy.repo <<REPOEOF
|
||||
[su-dummy]
|
||||
name=skeptical-updater dummy packages
|
||||
baseurl=file://$REPO
|
||||
enabled=1
|
||||
gpgcheck=0
|
||||
repo_gpgcheck=0
|
||||
metadata_expire=0
|
||||
REPOEOF
|
||||
t=$SECONDS
|
||||
"$BACKEND" -q makecache --repo su-dummy >/dev/null
|
||||
;;
|
||||
esac
|
||||
log "repo refresh in $((SECONDS - t))s"
|
||||
|
||||
# ---- 6b. optional pre-existing orphan: installed as a dependency, its parent removed ----------
|
||||
if [ "${SU_FIXTURE_ORPHAN:-0}" = 1 ]; then
|
||||
case $BACKEND in
|
||||
zypper) zypper --non-interactive --quiet install su-dummy-orphan-parent >/dev/null ;;
|
||||
dnf|yum) "$BACKEND" -y -q install su-dummy-orphan-parent >/dev/null 2>&1 ;;
|
||||
esac
|
||||
rpm -e su-dummy-orphan-parent
|
||||
case $BACKEND in
|
||||
zypper) unneeded=$(zypper --non-interactive --quiet packages --unneeded 2>/dev/null || true) ;;
|
||||
dnf|yum) unneeded=$("$BACKEND" -q repoquery --unneeded 2>/dev/null || true) ;;
|
||||
esac
|
||||
rpm -q su-dummy-orphan >/dev/null || die "su-dummy-orphan is not installed"
|
||||
printf '%s\n' "$unneeded" | grep -q su-dummy-orphan || die "su-dummy-orphan is not an unneeded package"
|
||||
log "pre-existing orphan (unneeded): su-dummy-orphan"
|
||||
fi
|
||||
|
||||
# ---- 6c. optional: su-dummy-b installed as a dependency (of su-dummy-a) ----------------------
|
||||
if [ "${SU_FIXTURE_B_AUTO:-0}" = 1 ]; then
|
||||
case $BACKEND in
|
||||
zypper) grep -qx su-dummy-b /var/lib/zypp/AutoInstalled 2>/dev/null || echo su-dummy-b >>/var/lib/zypp/AutoInstalled ;;
|
||||
dnf|yum) "$BACKEND" -q mark remove su-dummy-b >/dev/null ;;
|
||||
esac
|
||||
log "su-dummy-b marked as installed as a dependency"
|
||||
fi
|
||||
|
||||
# ---- 7. verify: the three must be listed as upgradable --------------------------
|
||||
want="su-dummy-a su-dummy-b su-dummy-c"
|
||||
case $BACKEND in
|
||||
zypper) listing=$(zypper --non-interactive --quiet list-updates 2>&1 || true) ;;
|
||||
dnf|yum) listing=$("$BACKEND" -q check-update 2>&1 || true) ;;
|
||||
esac
|
||||
bad=0
|
||||
for p in $want; do
|
||||
printf '%s\n' "$listing" | grep -Eq "(^|[| ])$p([. ]|$)" || { log "NOT listed as upgradable: $p"; bad=1; }
|
||||
done
|
||||
if [ "$bad" -ne 0 ]; then printf '%s\n' "$listing" >&2; die "fixture verification failed"; fi
|
||||
log "OK: su-dummy-{a,b,c} 1.0 installed and 2.0 offered by repo 'su-dummy' (total ${SECONDS}s, since start $((SECONDS - T0))s)"
|
||||
@@ -0,0 +1,226 @@
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
# Copyright (C) 2026 Bryan Joshua Pedini
|
||||
|
||||
# tests/e2e/helpers.bash - shared helpers for the E2E suite (sourced by tests/e2e/*.bats).
|
||||
# Runs as root inside a distro container prepared by tests/e2e/entrypoint.sh.
|
||||
# shellcheck shell=bash
|
||||
|
||||
load '../vendor/bats-support/load'
|
||||
load '../vendor/bats-assert/load'
|
||||
|
||||
STATE_DIR=/var/lib/skeptical-updater
|
||||
STATE_FILE=$STATE_DIR/trusted.tsv
|
||||
FAMILY=${SU_FAMILY:?SU_FAMILY not set (run through tests/e2e/entrypoint.sh)}
|
||||
case $FAMILY in
|
||||
apt | pacman) BE=$FAMILY ;;
|
||||
rpm) if command -v zypper >/dev/null 2>&1; then BE=zypper; else BE=dnf; fi ;;
|
||||
*) BE=unknown ;;
|
||||
esac
|
||||
export STATE_DIR STATE_FILE FAMILY BE
|
||||
unset SKEPTICAL_UPDATER_STATE_DIR NO_COLOR
|
||||
|
||||
# shellcheck source=/dev/null
|
||||
pretty_name() { (. /etc/os-release && printf '%s' "$PRETTY_NAME"); }
|
||||
|
||||
# pkg_ver NAME -> upstream version of the installed package (1.0 / 2.0) or "absent"
|
||||
pkg_ver() {
|
||||
local v=''
|
||||
case $BE in
|
||||
apt)
|
||||
v=$(dpkg-query -W -f '${db:Status-Abbrev} ${Version}' "$1" 2>/dev/null) || v=''
|
||||
# ${db:Status-Abbrev}: 1st char = selection (i, h = hold, ...), 2nd = state (i = installed)
|
||||
if [[ ${v:1:1} == i ]]; then v=${v##* }; else v=''; fi
|
||||
;;
|
||||
pacman)
|
||||
v=$(pacman -Q "$1" 2>/dev/null) || v=''
|
||||
v=${v#* }
|
||||
v=${v%-*}
|
||||
;;
|
||||
dnf | zypper) v=$(rpm -q --qf '%{VERSION}' "$1" 2>/dev/null) || v='' ;;
|
||||
esac
|
||||
printf '%s' "${v:-absent}"
|
||||
}
|
||||
|
||||
versions() { printf 'a=%s b=%s c=%s' "$(pkg_ver su-dummy-a)" "$(pkg_ver su-dummy-b)" "$(pkg_ver su-dummy-c)"; }
|
||||
|
||||
# holds_snapshot: the complete native hold/lock/ignore/exclude state, as text
|
||||
holds_snapshot() {
|
||||
case $BE in
|
||||
apt) apt-mark showhold | sort ;;
|
||||
pacman)
|
||||
echo "--- ignorepkg"
|
||||
cat /etc/pacman.d/ignorepkg
|
||||
echo "--- pacman-conf IgnorePkg"
|
||||
pacman-conf IgnorePkg
|
||||
;;
|
||||
dnf) cat /etc/dnf/dnf.conf ;;
|
||||
zypper) if [[ -e /etc/zypp/locks ]]; then cat /etc/zypp/locks; else echo "(no /etc/zypp/locks)"; fi ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# hold_c: a pre-existing, admin-made hold/lock/IgnorePkg/exclude on su-dummy-c
|
||||
hold_c() {
|
||||
case $BE in
|
||||
apt) apt-mark hold su-dummy-c >/dev/null ;;
|
||||
pacman) echo 'IgnorePkg = su-dummy-c' >/etc/pacman.d/ignorepkg ;;
|
||||
dnf) sed -i '/^\[main\]/a excludepkgs=su-dummy-c' /etc/dnf/dnf.conf ;;
|
||||
zypper) zypper --non-interactive --quiet addlock su-dummy-c >/dev/null ;;
|
||||
esac
|
||||
}
|
||||
|
||||
e2e_setup_file() {
|
||||
# remember the pristine configuration once per container
|
||||
case $BE in
|
||||
pacman)
|
||||
# 1.0 packages for e2e_reset (re-running the fixture would rebuild the repo with new
|
||||
# bytes in the same second, leaving the already synced database stale)
|
||||
if [[ ! -d /srv/su-old-pacman ]]; then
|
||||
mkdir -p /srv/su-old-pacman
|
||||
(
|
||||
# shellcheck source=fixtures/pacman.sh
|
||||
. "$BATS_TEST_DIRNAME/fixtures/pacman.sh"
|
||||
fx_build_tar /srv/su-old-pacman su-dummy-a 1.0 'Skeptical-updater dummy package A' su-dummy-b
|
||||
fx_build_tar /srv/su-old-pacman su-dummy-b 1.0 'Skeptical-updater dummy package B (dependency of A)'
|
||||
fx_build_tar /srv/su-old-pacman su-dummy-c 1.0 'Skeptical-updater dummy package C (independent)'
|
||||
)
|
||||
fi
|
||||
;;
|
||||
dnf) [[ -e /srv/su-orig-dnf.conf ]] || cp -p /etc/dnf/dnf.conf /srv/su-orig-dnf.conf ;;
|
||||
zypper)
|
||||
rm -f /etc/zypp/zypp.conf.d/50-su-e2e.conf /etc/zypp/locks-su-e2e
|
||||
if [[ ! -e /srv/su-orig-locks.state ]]; then
|
||||
if [[ -e /etc/zypp/locks ]]; then
|
||||
cp -p /etc/zypp/locks /srv/su-orig-locks
|
||||
echo present >/srv/su-orig-locks.state
|
||||
else
|
||||
echo absent >/srv/su-orig-locks.state
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# e2e_reset: back to "su-dummy-{a,b,c} 1.0 installed, 2.0 available, no holds, no statefile"
|
||||
e2e_reset() {
|
||||
rm -rf -- "$STATE_DIR"
|
||||
case $BE in
|
||||
apt)
|
||||
local -a held=()
|
||||
mapfile -t held < <(apt-mark showhold)
|
||||
((${#held[@]} == 0)) || apt-mark unhold "${held[@]}" >/dev/null
|
||||
dpkg --force-confnew -i /srv/su-old/*.deb </dev/null >/dev/null 2>&1
|
||||
;;
|
||||
pacman)
|
||||
: >/etc/pacman.d/ignorepkg
|
||||
pacman -U --noconfirm /srv/su-old-pacman/*.pkg.tar.zst >/dev/null 2>&1
|
||||
pacman -D --asexplicit su-dummy-a su-dummy-c >/dev/null
|
||||
pacman -D --asdeps su-dummy-b >/dev/null
|
||||
;;
|
||||
dnf | zypper)
|
||||
if [[ $BE == dnf ]]; then
|
||||
cp -p /srv/su-orig-dnf.conf /etc/dnf/dnf.conf
|
||||
else
|
||||
rm -f /etc/zypp/zypp.conf.d/50-su-e2e.conf /etc/zypp/locks-su-e2e
|
||||
fi
|
||||
if [[ $BE == dnf ]]; then
|
||||
:
|
||||
elif [[ $(cat /srv/su-orig-locks.state) == present ]]; then
|
||||
cp -p /srv/su-orig-locks /etc/zypp/locks
|
||||
else
|
||||
rm -f /etc/zypp/locks
|
||||
fi
|
||||
rpm -Uvh --oldpackage --replacepkgs /var/tmp/su-fixture/RPMS/*/su-dummy-[abc]-1.0-1.*.rpm >/dev/null 2>&1
|
||||
;;
|
||||
esac
|
||||
# su-dummy-b is a dependency of su-dummy-a (installed as such), on every backend
|
||||
case $BE in
|
||||
apt) apt-mark auto su-dummy-orphan su-dummy-b >/dev/null ;;
|
||||
dnf) dnf -q mark remove su-dummy-b >/dev/null 2>&1 ;;
|
||||
zypper) grep -qx su-dummy-b /var/lib/zypp/AutoInstalled 2>/dev/null || echo su-dummy-b >>/var/lib/zypp/AutoInstalled ;;
|
||||
esac
|
||||
[[ $(versions) == "a=1.0 b=1.0 c=1.0" && $(pkg_ver su-dummy-orphan) == 1.0 ]] || {
|
||||
echo "e2e_reset: unexpected state: $(versions) orphan=$(pkg_ver su-dummy-orphan)" >&2
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
# the native upgrade command line printed by --dry-run (without exclusions)
|
||||
native_upgrade_regex() {
|
||||
case $BE in
|
||||
apt) printf '%s' '^\+ apt-get -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold -o DPkg::Lock::Timeout=60 --with-new-pkgs upgrade$' ;;
|
||||
pacman) printf '%s' '^\+ pacman -Su --noconfirm$' ;; # after the reviewed databases were installed
|
||||
dnf) printf '%s' '^\+ (dnf|yum) -y --setopt=skip_if_unavailable=0 --setopt=\\\*\.metadata_expire=-1 upgrade$' ;;
|
||||
zypper) printf '%s' '^\+ zypper --non-interactive --no-refresh (update|dist-upgrade)$' ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# rpm_publish NAME VERSION: build NAME-VERSION-1 (arch-specific for su-dummy-c, like the fixture)
|
||||
# with the fixture's rpmbuild tree and add it to the su-dummy repository
|
||||
rpm_publish() {
|
||||
local name=$1 ver=$2 top=/var/tmp/su-fixture repo=/var/lib/su-dummy-repo spec
|
||||
spec=$top/SPECS/$name-$ver.spec
|
||||
{
|
||||
echo "Name: $name"
|
||||
echo "Version: $ver"
|
||||
echo "Release: 1"
|
||||
echo "Summary: skeptical-updater dummy $name (v$ver)"
|
||||
echo "License: MIT"
|
||||
[[ $name == su-dummy-c ]] || echo "BuildArch: noarch"
|
||||
echo "%global debug_package %{nil}"
|
||||
echo "%description"
|
||||
echo "Dummy package $name version $ver."
|
||||
echo "%install"
|
||||
echo "mkdir -p %{buildroot}/usr/share/su-dummy"
|
||||
echo "echo '$name $ver' > %{buildroot}/usr/share/su-dummy/$name"
|
||||
echo "%files"
|
||||
echo "/usr/share/su-dummy/$name"
|
||||
} >"$spec"
|
||||
rpmbuild -bb --quiet --define "_topdir $top" "$spec" >/dev/null 2>&1
|
||||
cp "$top"/RPMS/*/"$name-$ver"-1.*.rpm "$repo"/
|
||||
createrepo_c --quiet --update "$repo" >/dev/null
|
||||
}
|
||||
rpm_unpublish() {
|
||||
rm -f /var/lib/su-dummy-repo/"$1-$2"-1.*.rpm
|
||||
createrepo_c --quiet --update /var/lib/su-dummy-repo >/dev/null
|
||||
}
|
||||
|
||||
# pacman_sync_snapshot: the real sync database directory, with full timestamps
|
||||
pacman_sync_snapshot() { ls -la --time-style=full-iso /var/lib/pacman/sync 2>&1; }
|
||||
|
||||
# pacman_publish NAME VERSION / pacman_unpublish: add a package to the fixture's [su-dummy] repo,
|
||||
# and rebuild that repo with only the fixture's 2.0 packages again
|
||||
pacman_publish() {
|
||||
(
|
||||
# shellcheck source=fixtures/pacman.sh
|
||||
. "$BATS_TEST_DIRNAME/fixtures/pacman.sh"
|
||||
fx_build_tar /srv/su-repo "$1" "$2" "Skeptical-updater dummy package $1 $2"
|
||||
)
|
||||
(cd /srv/su-repo && repo-add --quiet su-dummy.db.tar.gz "./$1-$2-1-any.pkg.tar.zst")
|
||||
}
|
||||
pacman_unpublish() {
|
||||
(cd /srv/su-repo && rm -f su-dummy.db* su-dummy.files* &&
|
||||
for f in ./*.pkg.tar.zst; do case $f in ./su-dummy-[abc]-2.0-1-any.pkg.tar.zst) ;; *) rm -f "$f" ;; esac; done &&
|
||||
repo-add --quiet su-dummy.db.tar.gz ./su-dummy-a-2.0-1-any.pkg.tar.zst ./su-dummy-b-2.0-1-any.pkg.tar.zst ./su-dummy-c-2.0-1-any.pkg.tar.zst)
|
||||
}
|
||||
|
||||
# session_start ARGS...: run skeptical-updater ARGS in the background with a FIFO as stdin and wait
|
||||
# for its prompt; sets SESSION_PID and SESSION_FD (write the input to it; fd 3 belongs to bats)
|
||||
session_start() {
|
||||
mkfifo "$BATS_TEST_TMPDIR/in"
|
||||
skeptical-updater "$@" <"$BATS_TEST_TMPDIR/in" >"$BATS_TEST_TMPDIR/out" 2>&1 &
|
||||
SESSION_PID=$!
|
||||
exec {SESSION_FD}>"$BATS_TEST_TMPDIR/in"
|
||||
for _ in $(seq 1 600); do
|
||||
grep -q '^<enter> upgrade' "$BATS_TEST_TMPDIR/out" && return 0
|
||||
sleep 0.1
|
||||
done
|
||||
return 1
|
||||
}
|
||||
# session_finish INPUT: send INPUT, close stdin, wait; sets SESSION_RC (read by the .bats files)
|
||||
# shellcheck disable=SC2034
|
||||
session_finish() {
|
||||
printf '%s\n' "$1" >&"$SESSION_FD"
|
||||
exec {SESSION_FD}>&-
|
||||
SESSION_RC=0
|
||||
wait "$SESSION_PID" || SESSION_RC=$?
|
||||
}
|
||||
@@ -0,0 +1,381 @@
|
||||
#!/usr/bin/env bats
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
# Copyright (C) 2026 Bryan Joshua Pedini
|
||||
|
||||
# E2E: skeptical-updater against the REAL package manager of the container
|
||||
# (apt, pacman, dnf or zypper), on the su-dummy-{a,b,c} fixture: 1.0 installed,
|
||||
# 2.0 available, su-dummy-a depends on su-dummy-b. Every test starts from that
|
||||
# state (e2e_reset) with no statefile and no holds/locks.
|
||||
|
||||
load helpers
|
||||
|
||||
setup_file() { e2e_setup_file; }
|
||||
setup() { e2e_reset; }
|
||||
|
||||
# shellcheck source=/dev/null
|
||||
zypper_verb() { case $(. /etc/os-release && printf '%s' "$ID") in opensuse-tumbleweed | opensuse-slowroll) echo dist-upgrade ;; *) echo update ;; esac; }
|
||||
|
||||
@test "banner shows the detected backend and the distro, refresh succeeds" {
|
||||
run skeptical-updater list
|
||||
assert_success
|
||||
assert_line --index 0 "Backend: $BE ($(pretty_name))"
|
||||
assert_line --index 1 "Refreshing package lists... done."
|
||||
}
|
||||
|
||||
@test "list shows exactly the three dummies 1.0 -> 2.0" {
|
||||
run skeptical-updater list --no-refresh
|
||||
assert_success
|
||||
assert_line "3 upgradable, 0 trusted (hidden, use --all to show), 3 to review:"
|
||||
assert_line --regexp '^ \[1\] su-dummy-a +1\.0(-1)? +-> +2\.0(-1)? +[^ ]+$'
|
||||
assert_line --regexp '^ \[2\] su-dummy-b +1\.0(-1)? +-> +2\.0(-1)? +[^ ]+$'
|
||||
assert_line --regexp '^ \[3\] su-dummy-c +1\.0(-1)? +-> +2\.0(-1)? +[^ ]+$'
|
||||
refute_line --regexp '^.\[4\]'
|
||||
}
|
||||
|
||||
@test "N? prints the description; su-dummy-b is required by su-dummy-a" {
|
||||
run skeptical-updater --no-refresh <<<$'1?\n2?\nq'
|
||||
assert_success
|
||||
assert_line --regexp '^su-dummy-a: .*([Dd]ummy|skeptical-updater).*$'
|
||||
assert_line --regexp '^su-dummy-b: .*([Dd]ummy|skeptical-updater).*$'
|
||||
# [1] su-dummy-a has no installed reverse dependency, [2] su-dummy-b is needed by su-dummy-a
|
||||
assert_line ' required by (installed): nothing'
|
||||
assert_line ' required by (installed): su-dummy-a'
|
||||
assert_line 'Quit: nothing was upgraded.'
|
||||
}
|
||||
|
||||
@test "N?? prints the native full info" {
|
||||
run skeptical-updater --no-refresh <<<$'1??\nq'
|
||||
assert_success
|
||||
assert_line --regexp '^(Package: su-dummy-a|Name +: su-dummy-a)$'
|
||||
}
|
||||
|
||||
@test "N+ writes the user row and the dep row; list hides both and counts them" {
|
||||
run skeptical-updater --no-refresh <<<$'1+\nq'
|
||||
assert_success
|
||||
assert_line 'trusted su-dummy-a (+1 dependency: su-dummy-b)'
|
||||
[[ -f $STATE_FILE ]]
|
||||
[[ $(stat -c %a "$STATE_DIR") == 755 ]]
|
||||
run cat "$STATE_FILE"
|
||||
assert_line --index 0 '# skeptical-updater trusted packages. Columns: backend package reason added(UTC ISO-8601)'
|
||||
assert_line --index 1 --regexp "^$BE"$'\t'"su-dummy-a"$'\t'"user"$'\t''[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9:]{8}Z$'
|
||||
assert_line --index 2 --regexp "^$BE"$'\t'"su-dummy-b"$'\t'"dep:su-dummy-a"$'\t''[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9:]{8}Z$'
|
||||
run skeptical-updater list --no-refresh
|
||||
assert_success
|
||||
assert_line "3 upgradable, 2 trusted (hidden, use --all to show), 1 to review:"
|
||||
refute_output --partial 'su-dummy-a'
|
||||
refute_output --partial 'su-dummy-b'
|
||||
assert_line --regexp '^ \[1\] su-dummy-c '
|
||||
run skeptical-updater list --no-refresh --all
|
||||
assert_success
|
||||
assert_line "3 upgradable, 2 trusted (marked with =), 1 to review:"
|
||||
assert_line --regexp '^=\[1\] su-dummy-a '
|
||||
assert_line --regexp '^=\[2\] su-dummy-b '
|
||||
assert_line --regexp '^ \[3\] su-dummy-c '
|
||||
}
|
||||
|
||||
@test "trust / trusted / refresh / untrust from the command line" {
|
||||
run skeptical-updater trust su-dummy-a
|
||||
assert_success
|
||||
assert_output 'trusted su-dummy-a (+1 dependency: su-dummy-b)'
|
||||
run skeptical-updater trusted
|
||||
assert_success
|
||||
assert_line --partial "$BE"$'\t'"su-dummy-a"$'\t'"user"$'\t'
|
||||
assert_line --partial "$BE"$'\t'"su-dummy-b"$'\t'"dep:su-dummy-a"$'\t'
|
||||
# refresh recomputes the closure: a removed dep row comes back
|
||||
sed -i '/dep:su-dummy-a/d' "$STATE_FILE"
|
||||
run skeptical-updater refresh
|
||||
assert_success
|
||||
assert_output 'refreshed the dependency closures of 1 trusted root: 1 added, 0 removed'
|
||||
run grep -c $'\tdep:su-dummy-a\t' "$STATE_FILE"
|
||||
assert_output 1
|
||||
run skeptical-updater untrust su-dummy-a
|
||||
assert_success
|
||||
assert_output 'untrusted su-dummy-a (2 rows removed)'
|
||||
run skeptical-updater trusted
|
||||
assert_success
|
||||
assert_output '# skeptical-updater trusted packages. Columns: backend package reason added(UTC ISO-8601)'
|
||||
}
|
||||
|
||||
@test "excluding su-dummy-b (2- then enter) upgrades the others" {
|
||||
before=$(holds_snapshot)
|
||||
run skeptical-updater <<<$'2-\n'
|
||||
assert_success
|
||||
assert_line 'excluded from this run: su-dummy-b'
|
||||
assert_line 'Excluded from this run: su-dummy-b'
|
||||
assert_line 'Upgrade finished.'
|
||||
[[ $(versions) == "a=2.0 b=1.0 c=2.0" ]] || fail "versions after the run: $(versions)"
|
||||
[[ $(holds_snapshot) == "$before" ]] || fail "hold/lock state changed: $(holds_snapshot)"
|
||||
run skeptical-updater list --no-refresh
|
||||
assert_line --regexp '^ \[1\] su-dummy-b +1\.0'
|
||||
}
|
||||
|
||||
@test "a pre-existing hold/lock/IgnorePkg on su-dummy-c survives a run that excludes su-dummy-b" {
|
||||
hold_c
|
||||
before=$(holds_snapshot)
|
||||
[[ -n $before ]]
|
||||
run skeptical-updater list --no-refresh
|
||||
assert_line --regexp '^ \[2\] su-dummy-b '
|
||||
run skeptical-updater <<<$'2-\n'
|
||||
assert_success
|
||||
assert_line 'Excluded from this run: su-dummy-b'
|
||||
[[ $(versions) == "a=2.0 b=1.0 c=1.0" ]] || fail "versions after the run: $(versions)"
|
||||
after=$(holds_snapshot)
|
||||
[[ $after == "$before" ]] || fail $'hold/lock state changed.\nbefore:\n'"$before"$'\nafter:\n'"$after"
|
||||
if [[ $BE == apt ]]; then [[ ! -e $STATE_DIR/holds.added ]]; fi
|
||||
}
|
||||
|
||||
@test "trusting a package re-includes its excluded dependency" {
|
||||
run skeptical-updater <<<$'2-\n1+\n'
|
||||
assert_success
|
||||
assert_line ' (su-dummy-b is trusted now: no longer excluded from this run)'
|
||||
assert_line 'Excluded from this run: none'
|
||||
[[ $(versions) == "a=2.0 b=2.0 c=2.0" ]] || fail "versions after the run: $(versions)"
|
||||
}
|
||||
|
||||
@test "enter upgrades everything" {
|
||||
run skeptical-updater <<<''
|
||||
assert_success
|
||||
assert_line 'Excluded from this run: none'
|
||||
[[ $(versions) == "a=2.0 b=2.0 c=2.0" ]] || fail "versions after the run: $(versions)"
|
||||
run skeptical-updater list --no-refresh
|
||||
assert_success
|
||||
assert_output --partial 'Nothing to upgrade.'
|
||||
}
|
||||
|
||||
@test "Nx purges su-dummy-c after a y confirmation" {
|
||||
run skeptical-updater --no-refresh <<<$'3x\ny\nl\nq'
|
||||
assert_success
|
||||
assert_line 'Purge su-dummy-c now? [y/N] y'
|
||||
assert_line 'purged: su-dummy-c'
|
||||
assert_line '2 upgradable, 0 trusted (hidden, use --all to show), 2 to review:'
|
||||
[[ $(versions) == "a=1.0 b=1.0 c=absent" ]] || fail "versions after the run: $(versions)"
|
||||
[[ $(pkg_ver su-dummy-orphan) == 1.0 ]] || fail "the unrelated orphan was removed"
|
||||
}
|
||||
|
||||
@test "Nx on su-dummy-b is refused: su-dummy-a depends on it; both stay, the row stays listed" {
|
||||
run skeptical-updater --no-refresh <<<$'2x\nl\nq'
|
||||
assert_success
|
||||
assert_line 'not purged: su-dummy-b is required by: su-dummy-a - purge the dependents first'
|
||||
refute_output --partial '[y/N]'
|
||||
[[ $(grep -c '^ \[2\] su-dummy-b ' <<<"$output") -eq 2 ]] || fail "row 2 left the list"
|
||||
[[ $(versions) == "a=1.0 b=1.0 c=1.0" ]] || fail "versions after the run: $(versions)"
|
||||
}
|
||||
|
||||
@test "Nx on su-dummy-a removes su-dummy-b with it (its dependency, nothing else needs it); the orphan survives" {
|
||||
run skeptical-updater --no-refresh <<<$'1x\ny\nl\nq'
|
||||
assert_success
|
||||
assert_line 'Purge su-dummy-a now? This also removes: su-dummy-b [y/N] y'
|
||||
assert_line 'purged: su-dummy-a su-dummy-b'
|
||||
assert_line '1 upgradable, 0 trusted (hidden, use --all to show), 1 to review:'
|
||||
[[ $(versions) == "a=absent b=absent c=1.0" ]] || fail "versions after the run: $(versions)"
|
||||
[[ $(pkg_ver su-dummy-orphan) == 1.0 ]] || fail "the unrelated orphan was removed"
|
||||
}
|
||||
|
||||
@test "purging every package ends the session cleanly" {
|
||||
run skeptical-updater --no-refresh <<<$'1x\ny\n3x\ny'
|
||||
assert_success
|
||||
assert_line 'Nothing left to upgrade.'
|
||||
[[ $(versions) == "a=absent b=absent c=absent" ]] || fail "versions after the run: $(versions)"
|
||||
[[ $(pkg_ver su-dummy-orphan) == 1.0 ]] || fail "the unrelated orphan was removed"
|
||||
}
|
||||
|
||||
@test "Nx answered with anything but y keeps the package" {
|
||||
run skeptical-updater --no-refresh <<<$'3x\nn\nq'
|
||||
assert_success
|
||||
assert_line 'not purged: su-dummy-c'
|
||||
[[ $(versions) == "a=1.0 b=1.0 c=1.0" ]]
|
||||
}
|
||||
|
||||
@test "q leaves everything untouched" {
|
||||
before=$(holds_snapshot)
|
||||
run skeptical-updater <<<$'2-\n1-\nq'
|
||||
assert_success
|
||||
assert_line 'Quit: nothing was upgraded.'
|
||||
[[ $(versions) == "a=1.0 b=1.0 c=1.0" ]]
|
||||
[[ $(holds_snapshot) == "$before" ]]
|
||||
[[ ! -e $STATE_FILE ]]
|
||||
}
|
||||
|
||||
@test "EOF aborts with exit 2 and changes nothing" {
|
||||
before=$(holds_snapshot)
|
||||
run skeptical-updater <<<'2-'
|
||||
assert_failure 2
|
||||
assert_output --partial 'end of input: aborted, nothing was upgraded'
|
||||
[[ $(versions) == "a=1.0 b=1.0 c=1.0" ]]
|
||||
[[ $(holds_snapshot) == "$before" ]]
|
||||
[[ ! -e $STATE_FILE ]]
|
||||
}
|
||||
|
||||
@test "--dry-run: enter prints the native upgrade command and changes nothing" {
|
||||
before=$(holds_snapshot)
|
||||
run skeptical-updater --dry-run <<<''
|
||||
assert_success
|
||||
assert_line --regexp "$(native_upgrade_regex)"
|
||||
assert_line 'Dry run: nothing was changed.'
|
||||
# (3+: trusting su-dummy-a would hide su-dummy-b and so re-include it)
|
||||
run skeptical-updater --dry-run <<<$'2-\n3+\n'
|
||||
assert_success
|
||||
case $BE in
|
||||
apt)
|
||||
assert_line '+ apt-mark hold su-dummy-b'
|
||||
assert_line '+ apt-mark unhold su-dummy-b'
|
||||
;;
|
||||
pacman)
|
||||
assert_line --regexp '^\+ cp -p -- /tmp/skeptical-updater-db\.[0-9]+\.[A-Za-z0-9]+/sync/su-dummy\.db /var/lib/pacman/sync/$'
|
||||
assert_line '+ pacman -Su --noconfirm --ignore su-dummy-b'
|
||||
;;
|
||||
dnf) assert_line --regexp '^\+ (dnf|yum) -y --setopt=skip_if_unavailable=0 --setopt=\\\*\.metadata_expire=-1 upgrade --exclude=su-dummy-b$' ;;
|
||||
zypper)
|
||||
assert_line --regexp '^\+ env ZYPP_CONF=.*/zypp\.conf zypper --non-interactive --quiet addlock su-dummy-b$'
|
||||
assert_line --regexp "^\+ env ZYPP_CONF=.*/zypp\.conf zypper --non-interactive --no-refresh $(zypper_verb)\$"
|
||||
;;
|
||||
esac
|
||||
assert_line --partial "+ $STATE_FILE add: $BE su-dummy-c user"
|
||||
[[ $(versions) == "a=1.0 b=1.0 c=1.0" ]]
|
||||
[[ $(holds_snapshot) == "$before" ]]
|
||||
[[ ! -e $STATE_DIR ]]
|
||||
}
|
||||
|
||||
@test "read-only commands work unprivileged" {
|
||||
if command -v setpriv >/dev/null 2>&1; then
|
||||
asuser() { setpriv --reuid=65534 --regid=65534 --clear-groups -- env TMPDIR=/tmp "$@"; }
|
||||
elif command -v python3 >/dev/null 2>&1; then # almalinux:10 has neither setpriv nor runuser/su
|
||||
asuser() { python3 -c 'import os, sys; os.setgroups([]); os.setgid(65534); os.setuid(65534); os.execvp(sys.argv[1], sys.argv[1:])' env TMPDIR=/tmp "$@"; }
|
||||
else
|
||||
skip "neither setpriv nor python3 available to drop privileges"
|
||||
fi
|
||||
skeptical-updater trust su-dummy-c >/dev/null
|
||||
run asuser skeptical-updater trusted
|
||||
assert_success
|
||||
assert_line --partial "$BE"$'\t'"su-dummy-c"$'\t'"user"
|
||||
run asuser skeptical-updater info su-dummy-b
|
||||
assert_success
|
||||
assert_line ' required by (installed): su-dummy-a'
|
||||
run asuser skeptical-updater --dry-run --no-refresh <<<''
|
||||
assert_success
|
||||
assert_line --regexp "$(native_upgrade_regex)"
|
||||
run asuser skeptical-updater upgrade-me-please
|
||||
assert_failure 1
|
||||
# a real upgrade without root and without sudo is refused
|
||||
if ! command -v sudo >/dev/null 2>&1; then
|
||||
run asuser skeptical-updater <<<''
|
||||
assert_failure 1
|
||||
assert_output --partial 'upgrading needs root and sudo is not available'
|
||||
fi
|
||||
[[ $(versions) == "a=1.0 b=1.0 c=1.0" ]]
|
||||
# a state directory an unprivileged user cannot enter is an error, not "no trusted packages"
|
||||
chmod 0000 /var/lib/skeptical-updater
|
||||
run asuser skeptical-updater trusted
|
||||
chmod 0755 /var/lib/skeptical-updater
|
||||
assert_failure 1
|
||||
assert_output --partial 'cannot access the state directory /var/lib/skeptical-updater (permission denied)'
|
||||
}
|
||||
|
||||
@test "pacman: list, q, end of input and --dry-run never touch the real sync database" {
|
||||
[[ $BE == pacman ]] || skip "pacman only"
|
||||
before=$(pacman_sync_snapshot)
|
||||
run skeptical-updater list
|
||||
assert_success
|
||||
run skeptical-updater <<<'q'
|
||||
assert_success
|
||||
run skeptical-updater <<<'2-'
|
||||
assert_failure 2
|
||||
run skeptical-updater --dry-run <<<''
|
||||
assert_success
|
||||
assert_line 'Checking the reviewed transaction (pacman -Sup)... ok.'
|
||||
assert_line '+ pacman -Su --noconfirm'
|
||||
after=$(pacman_sync_snapshot)
|
||||
[[ $after == "$before" ]] || fail $'the real sync database changed.\nbefore:\n'"$before"$'\nafter:\n'"$after"
|
||||
run ls /tmp
|
||||
refute_output --partial 'skeptical-updater-db.'
|
||||
}
|
||||
|
||||
@test "apt: temporary holds wait for a busy dpkg lock instead of failing" {
|
||||
[[ $BE == apt ]] || skip "apt only"
|
||||
command -v perl >/dev/null 2>&1 || skip "perl not available"
|
||||
# hold dpkg's frontend lock for 4 s (fcntl, as dpkg does), starting now
|
||||
perl -e 'use Fcntl; open(my $f, ">>", "/var/lib/dpkg/lock-frontend") or die; my $l = pack("s s l l i", F_WRLCK, 0, 0, 0, 0); fcntl($f, F_SETLKW, $l) or die; sleep 4;' &
|
||||
locker=$!
|
||||
sleep 0.5
|
||||
run skeptical-updater --no-refresh <<<$'2-\n'
|
||||
wait "$locker" || true
|
||||
assert_success
|
||||
assert_line 'Excluded from this run: su-dummy-b'
|
||||
[[ $(versions) == "a=2.0 b=1.0 c=2.0" ]] || fail "versions after the run: $(versions)"
|
||||
[[ -z $(apt-mark showhold) ]]
|
||||
}
|
||||
|
||||
@test "zypper: a lock whose locksfile.path comes from a zypp.conf.d drop-in is honoured" {
|
||||
[[ $BE == zypper ]] || skip "zypper only"
|
||||
mkdir -p /etc/zypp/zypp.conf.d
|
||||
printf '[main]\nlocksfile.path = /etc/zypp/locks-su-e2e\n' >/etc/zypp/zypp.conf.d/50-su-e2e.conf
|
||||
zypper --non-interactive --quiet addlock su-dummy-c >/dev/null
|
||||
grep -q 'solvable_name: su-dummy-c' /etc/zypp/locks-su-e2e
|
||||
before=$(cat /etc/zypp/locks-su-e2e)
|
||||
run skeptical-updater <<<$'2-\n'
|
||||
assert_success
|
||||
assert_line 'Excluded from this run: su-dummy-b'
|
||||
[[ $(versions) == "a=2.0 b=1.0 c=1.0" ]] || fail "versions after the run: $(versions)"
|
||||
[[ $(cat /etc/zypp/locks-su-e2e) == "$before" ]]
|
||||
}
|
||||
|
||||
@test "zypper: the reviewed metadata is applied (no refresh between review and upgrade)" {
|
||||
[[ $BE == zypper ]] || skip "zypper only (libdnf reads file:// repositories live; apt never refreshes on upgrade)"
|
||||
mkfifo "$BATS_TEST_TMPDIR/in"
|
||||
skeptical-updater <"$BATS_TEST_TMPDIR/in" >"$BATS_TEST_TMPDIR/out" 2>&1 &
|
||||
pid=$!
|
||||
exec {w}>"$BATS_TEST_TMPDIR/in" # (fd 3 belongs to bats)
|
||||
for _ in $(seq 1 600); do
|
||||
grep -q '^<enter> upgrade' "$BATS_TEST_TMPDIR/out" && break
|
||||
sleep 0.1
|
||||
done
|
||||
# a newer su-dummy-c appears in the repository after the review
|
||||
rpm_publish su-dummy-c 3.0
|
||||
echo >&"$w"
|
||||
exec {w}>&-
|
||||
rc=0
|
||||
wait "$pid" || rc=$?
|
||||
rpm_unpublish su-dummy-c 3.0
|
||||
[[ $rc -eq 0 ]] || fail "exit $rc: $(cat "$BATS_TEST_TMPDIR/out")"
|
||||
[[ $(versions) == "a=2.0 b=2.0 c=2.0" ]] || fail "versions after the run: $(versions)"
|
||||
}
|
||||
|
||||
@test "pacman: package databases synced elsewhere during the review: refused (exit 2), nothing changed" {
|
||||
[[ $BE == pacman ]] || skip "pacman only"
|
||||
session_start
|
||||
sleep 1
|
||||
pacman_publish su-dummy-c 3.0
|
||||
pacman -Sy >/dev/null 2>&1 # another terminal: the system's database is now newer than the reviewed one
|
||||
synced=$(sha256sum /var/lib/pacman/sync/su-dummy.db | cut -d' ' -f1)
|
||||
session_finish ''
|
||||
pacman_unpublish
|
||||
pacman -Sy >/dev/null 2>&1
|
||||
[[ $SESSION_RC -eq 2 ]] || fail "exit $SESSION_RC: $(cat "$BATS_TEST_TMPDIR/out")"
|
||||
grep -q 'package databases changed since the review (su-dummy); run skeptical-updater again' "$BATS_TEST_TMPDIR/out"
|
||||
[[ $(versions) == "a=1.0 b=1.0 c=1.0" ]] || fail "versions after the run: $(versions)"
|
||||
[[ ! -e /var/lib/pacman/db.lck ]]
|
||||
[[ -n $synced ]]
|
||||
}
|
||||
|
||||
@test "pacman: <enter> installs exactly the reviewed metadata (the real sync DB becomes the reviewed one)" {
|
||||
[[ $BE == pacman ]] || skip "pacman only"
|
||||
# the system's database is older than what the review will see (su-dummy-c 3.0) ...
|
||||
# (1 s apart from the previous repo rebuild: pacman compares database mtimes in seconds)
|
||||
sleep 1
|
||||
pacman_publish su-dummy-c 3.0
|
||||
session_start
|
||||
reviewed=$(sha256sum /tmp/skeptical-updater-db."$SESSION_PID".*/sync/su-dummy.db | cut -d' ' -f1)
|
||||
# ... and su-dummy-c 4.0 appears in the repository after the review
|
||||
sleep 1
|
||||
pacman_publish su-dummy-c 4.0
|
||||
session_finish ''
|
||||
pacman_unpublish
|
||||
[[ $SESSION_RC -eq 0 ]] || fail "exit $SESSION_RC: $(cat "$BATS_TEST_TMPDIR/out")"
|
||||
grep -qE '^\+ cp -p -- /tmp/skeptical-updater-db\.[0-9]+\.[A-Za-z0-9]+/sync/su-dummy\.db /var/lib/pacman/sync/$' "$BATS_TEST_TMPDIR/out"
|
||||
[[ -n $reviewed ]]
|
||||
[[ $(sha256sum /var/lib/pacman/sync/su-dummy.db | cut -d' ' -f1) == "$reviewed" ]] || fail "the system's su-dummy.db is not the reviewed one"
|
||||
[[ $(versions) == "a=2.0 b=2.0 c=3.0" ]] || fail "versions after the run: $(versions)"
|
||||
[[ ! -e /var/lib/pacman/db.lck ]]
|
||||
run ls /tmp
|
||||
refute_output --partial 'skeptical-updater-db.'
|
||||
}
|
||||
@@ -0,0 +1,787 @@
|
||||
#!/usr/bin/env bats
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
# Copyright (C) 2026 Bryan Joshua Pedini
|
||||
|
||||
# The exact native commands issued per backend: refresh, upgrade + per-run exclusion
|
||||
# (apt hold journal, pacman --ignore, dnf --exclude, zypper private locks), purge plans.
|
||||
|
||||
# each @test runs in its own subshell: per-test exports are intended
|
||||
# shellcheck disable=SC2030,SC2031
|
||||
|
||||
load test_helper
|
||||
|
||||
setup() { common_setup; }
|
||||
|
||||
APT_UPGRADE='apt-get -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold -o DPkg::Lock::Timeout=60 --with-new-pkgs upgrade'
|
||||
DNF_UPGRADE='dnf -y --setopt=skip_if_unavailable=0 --setopt=*.metadata_expire=-1 upgrade'
|
||||
|
||||
# ---- apt ----------------------------------------------------------------------------------------
|
||||
|
||||
@test "apt: refresh is apt-get update -qq -o APT::Update::Error-Mode=any" {
|
||||
run skup list
|
||||
assert_success
|
||||
assert_line 'Refreshing package lists... done.'
|
||||
log_has 'apt-get update -qq -o APT::Update::Error-Mode=any'
|
||||
}
|
||||
|
||||
@test "apt: a failed refresh warns and goes on with the cached lists" {
|
||||
FAKE_APT_UPDATE_RC=100 run skup list
|
||||
assert_success
|
||||
assert_line 'Refreshing package lists... FAILED.'
|
||||
assert_output --partial "E: Failed to fetch http://deb.debian.org/debian/dists/bookworm/InRelease"
|
||||
assert_output --partial 'warning: metadata refresh failed (apt-get update -qq -o APT::Update::Error-Mode=any exited 100); continuing with the cached package lists'
|
||||
assert_line '3 upgradable, 0 trusted (hidden, use --all to show), 3 to review:'
|
||||
}
|
||||
|
||||
@test "apt: the upgrade command line" {
|
||||
run skup --no-refresh <<<''
|
||||
assert_success
|
||||
log_has "$APT_UPGRADE"
|
||||
assert_line "+ $APT_UPGRADE"
|
||||
}
|
||||
|
||||
@test "apt: exclusions are temporary holds; a package the admin held is never held nor released by us" {
|
||||
echo su-dummy-c >"$FAKE_DIR/holds"
|
||||
FAKE_APT_SIM=held run skup --no-refresh <<<$'2-\n3-\n'
|
||||
assert_success
|
||||
assert_line 'Excluded from this run: su-dummy-b su-dummy-c'
|
||||
log_has 'apt-mark showhold'
|
||||
log_has 'apt-mark hold su-dummy-b'
|
||||
log_has 'holds during upgrade: su-dummy-b su-dummy-c '
|
||||
log_has 'apt-mark unhold su-dummy-b'
|
||||
log_lacks '^apt-mark (un)?hold .*su-dummy-c'
|
||||
run cat "$FAKE_DIR/holds"
|
||||
assert_output 'su-dummy-c'
|
||||
[[ ! -e $STATE/holds.added ]]
|
||||
# order: hold, upgrade, unhold
|
||||
run grep -nE '^apt-mark (un)?hold|^apt-get -y' "$FAKE_LOG"
|
||||
assert_line --index 0 --partial 'apt-mark hold su-dummy-b'
|
||||
assert_line --index 1 --partial "$APT_UPGRADE"
|
||||
assert_line --index 2 --partial 'apt-mark unhold su-dummy-b'
|
||||
}
|
||||
|
||||
@test "apt: the holds are written to the journal before apt-mark hold runs" {
|
||||
# a hold that fails half-way: the journal still lets the restore release it
|
||||
FAKE_APT_MARK_HOLD_RC=100 run skup --no-refresh <<<$'1-\n2-\n'
|
||||
assert_failure 1
|
||||
assert_output --partial 'could not place the temporary holds'
|
||||
log_has 'apt-mark unhold su-dummy-a su-dummy-b'
|
||||
run cat "$FAKE_DIR/holds"
|
||||
assert_output ''
|
||||
log_lacks '^apt-get -y'
|
||||
}
|
||||
|
||||
@test "apt: holds are released even when the upgrade fails" {
|
||||
FAKE_APT_UPGRADE_RC=100 run skup --no-refresh <<<$'2-\n'
|
||||
assert_failure 100
|
||||
log_has 'apt-mark unhold su-dummy-b'
|
||||
run cat "$FAKE_DIR/holds"
|
||||
assert_output ''
|
||||
}
|
||||
|
||||
@test "apt: holds are released when the run is killed (SIGTERM) during the upgrade" {
|
||||
FAKE_APT_UPGRADE_KILL=TERM run skup --no-refresh <<<$'2-\n'
|
||||
assert_failure 143
|
||||
assert_output --partial 'interrupted'
|
||||
log_has 'apt-mark unhold su-dummy-b'
|
||||
run cat "$FAKE_DIR/holds"
|
||||
assert_output ''
|
||||
[[ ! -e $STATE/holds.added ]]
|
||||
}
|
||||
|
||||
@test "apt: holds are released on SIGQUIT and SIGHUP too" {
|
||||
FAKE_APT_UPGRADE_KILL=QUIT run skup --no-refresh <<<$'2-\n'
|
||||
assert_failure 131
|
||||
log_has 'apt-mark unhold su-dummy-b'
|
||||
: >"$FAKE_LOG"
|
||||
FAKE_APT_UPGRADE_KILL=HUP run skup --no-refresh <<<$'2-\n'
|
||||
assert_failure 129
|
||||
log_has 'apt-mark unhold su-dummy-b'
|
||||
run cat "$FAKE_DIR/holds"
|
||||
assert_output ''
|
||||
}
|
||||
|
||||
@test "apt: a journal left by a crashed run is replayed at the start of the next root run (even a list)" {
|
||||
printf '%s\n' su-dummy-b su-dummy-c >"$FAKE_DIR/holds" # b leaked by the crash, c is the admin's
|
||||
mkdir -p "$STATE"
|
||||
printf '%s\n' su-dummy-b su-dummy-e >"$STATE/holds.added" # e: leaked, but the admin released it since
|
||||
run skup list --no-refresh
|
||||
assert_success
|
||||
assert_line 'Released temporary holds left over by an interrupted run: su-dummy-b'
|
||||
log_has 'apt-mark unhold su-dummy-b'
|
||||
log_lacks 'unhold .*su-dummy-(c|e)'
|
||||
run cat "$FAKE_DIR/holds"
|
||||
assert_output 'su-dummy-c'
|
||||
[[ ! -e $STATE/holds.added ]]
|
||||
# the replay happens before the listing
|
||||
run grep -nE '^apt-mark unhold|^apt-get -s' "$FAKE_LOG"
|
||||
assert_line --index 0 --partial 'apt-mark unhold su-dummy-b'
|
||||
}
|
||||
|
||||
@test "apt: a journal whose holds are all gone is just removed" {
|
||||
mkdir -p "$STATE"
|
||||
echo su-dummy-e >"$STATE/holds.added"
|
||||
run skup list --no-refresh
|
||||
assert_success
|
||||
assert_line 'Removed a stale hold journal of an interrupted run (nothing was still held).'
|
||||
log_lacks 'unhold'
|
||||
[[ ! -e $STATE/holds.added ]]
|
||||
}
|
||||
|
||||
@test "apt: no replay as non-root or under --dry-run" {
|
||||
mkdir -p "$STATE"
|
||||
echo su-dummy-b >"$STATE/holds.added"
|
||||
echo su-dummy-b >"$FAKE_DIR/holds"
|
||||
run skup --dry-run list --no-refresh
|
||||
assert_success
|
||||
SKEPTICAL_UPDATER_EUID=1000 run skup list --no-refresh
|
||||
assert_success
|
||||
log_lacks 'unhold'
|
||||
[[ -e $STATE/holds.added ]]
|
||||
}
|
||||
|
||||
@test "apt: the replay leaves the journal alone while another run holds its lock" {
|
||||
mkdir -p "$STATE"
|
||||
echo su-dummy-b >"$STATE/holds.added"
|
||||
echo su-dummy-b >"$FAKE_DIR/holds"
|
||||
exec 9>>"$STATE/holds.lock"
|
||||
flock 9
|
||||
run skup list --no-refresh
|
||||
exec 9>&-
|
||||
assert_success
|
||||
assert_output --partial "another skeptical-updater run is using $STATE/holds.added; leaving its temporary holds alone."
|
||||
log_lacks 'unhold'
|
||||
[[ -e $STATE/holds.added ]]
|
||||
}
|
||||
|
||||
@test "apt: apt-mark retries while dpkg's frontend lock is busy (it ignores DPkg::Lock::Timeout)" {
|
||||
FAKE_APT_MARK_LOCKED=2 run skup --no-refresh <<<$'2-\n'
|
||||
assert_success
|
||||
[[ $(grep -c '^apt-mark hold su-dummy-b' "$FAKE_LOG") -eq 3 ]]
|
||||
log_has 'apt-mark unhold su-dummy-b'
|
||||
run cat "$FAKE_DIR/holds"
|
||||
assert_output ''
|
||||
}
|
||||
|
||||
@test "apt: when the restore fails, the holds lock stays held for the exit handler's retry" {
|
||||
FAKE_APT_MARK_UNHOLD_RC=100 run skup --no-refresh <<<$'2-\n'
|
||||
assert_failure 1
|
||||
assert_output --partial 'could not release the temporary holds (su-dummy-b); they are recorded in'
|
||||
[[ $(grep -c '^holds.lock during unhold: ' "$FAKE_LOG") -eq 2 ]]
|
||||
log_lacks '^holds.lock during unhold: free'
|
||||
[[ -e $STATE/holds.added ]]
|
||||
}
|
||||
|
||||
@test "apt: the dpkg 1.22+ wording of the busy lock is retried as well" {
|
||||
FAKE_APT_MARK_LOCKED=1 FAKE_APT_MARK_LOCK_MSG='dpkg: error: dpkg frontend lock was locked by /usr/bin/perl process with pid 3297' \
|
||||
run skup --no-refresh <<<$'2-\n'
|
||||
assert_success
|
||||
[[ $(grep -c '^apt-mark hold su-dummy-b' "$FAKE_LOG") -eq 2 ]]
|
||||
}
|
||||
|
||||
@test "apt: an apt-mark failure whose message merely contains 'lock' (libblockdev) is not retried" {
|
||||
SECONDS=0
|
||||
FAKE_APT_MARK_HOLD_RC=100 FAKE_APT_MARK_FAIL_MSG='E: Unable to locate package libblockdev-crypto3' run skup --no-refresh <<<$'2-\n'
|
||||
assert_failure 1
|
||||
[[ $(grep -c '^apt-mark hold' "$FAKE_LOG") -eq 1 ]]
|
||||
((SECONDS < 20))
|
||||
}
|
||||
|
||||
@test "apt: Nx on a package others depend on is refused (apt-cache rdepends gate), nothing simulated or removed" {
|
||||
run skup --no-refresh <<<$'2x\nl\nq'
|
||||
assert_success
|
||||
assert_line 'not purged: su-dummy-b is required by: su-dummy-a - purge the dependents first'
|
||||
refute_output --partial '[y/N]'
|
||||
log_lacks 'purge'
|
||||
assert_line ' [2] su-dummy-b 1.0 -> 2.0 su-fixture:su-repo'
|
||||
}
|
||||
|
||||
@test "apt: purge plan = the simulated purge minus pre-existing orphans; the target's own orphaned dependencies go too" {
|
||||
FAKE_APT_AUTO_ORPHANS=su-dummy-a:su-dummy-b run skup --no-refresh <<<$'1x\ny\nq'
|
||||
assert_success
|
||||
log_has 'apt-get -s autoremove'
|
||||
log_has 'apt-get -s purge --auto-remove su-dummy-a'
|
||||
log_has 'apt-get -s purge su-dummy-a su-dummy-b'
|
||||
assert_output --partial 'Purge su-dummy-a now? This also removes: su-dummy-b [y/N]'
|
||||
run grep -E '^apt-get -y' "$FAKE_LOG"
|
||||
assert_output 'apt-get -y purge su-dummy-a su-dummy-b'
|
||||
refute_output --partial 'su-dummy-orphan'
|
||||
log_lacks 'autoremove --purge|-y autoremove'
|
||||
}
|
||||
|
||||
@test "apt: belt and braces - a simulated purge that also removes a non-dependency is refused" {
|
||||
FAKE_APT_SIM_EXTRA=su-dummy-c:su-dummy-w run skup --no-refresh <<<$'3x\nq'
|
||||
assert_success
|
||||
assert_output --partial 'cannot purge su-dummy-c: apt would also remove su-dummy-w, which su-dummy-c does not depend on (dependents of it?); nothing was removed'
|
||||
refute_output --partial '[y/N]'
|
||||
log_lacks '^apt-get -y'
|
||||
}
|
||||
|
||||
# ---- pacman -------------------------------------------------------------------------------------
|
||||
|
||||
REAL_SYNC_RE='/pacman-db/sync'
|
||||
|
||||
@test "pacman: the refresh syncs a throwaway --dbpath (pid in its name); the real sync database is never touched" {
|
||||
use_fakes pacman
|
||||
echo "old" >"$FAKE_PACMAN_DBPATH/sync/su-dummy.db"
|
||||
run skup list
|
||||
assert_success
|
||||
run grep -E '^-Sy ' "$FAKE_DIR/pacman-dbpath"
|
||||
assert_output --regexp '^-Sy /tmp/skeptical-updater-db\.[0-9]+\.[A-Za-z0-9]+$'
|
||||
db=${output#-Sy }
|
||||
run grep -E '^-(Qu|Sl|Sup) ' "$FAKE_DIR/pacman-dbpath"
|
||||
refute_output --partial ' real'
|
||||
assert_output --partial "-Qu $db"
|
||||
[[ ! -e $db ]]
|
||||
log_has "pacman -Sy --dbpath $db --logfile /dev/null"
|
||||
[[ $(cat "$FAKE_PACMAN_DBPATH/sync/su-dummy.db") == old ]]
|
||||
}
|
||||
|
||||
@test "pacman: the throwaway database is seeded with the system's databases AND their signatures" {
|
||||
use_fakes pacman
|
||||
echo old >"$FAKE_PACMAN_DBPATH/sync/su-dummy.db"
|
||||
echo old-sig >"$FAKE_PACMAN_DBPATH/sync/su-dummy.db.sig"
|
||||
run skup list
|
||||
assert_success
|
||||
log_has 'seeded: su-dummy.db su-dummy.db.sig'
|
||||
}
|
||||
|
||||
@test "pacman: q, EOF and --dry-run never touch the real sync database" {
|
||||
use_fakes pacman
|
||||
echo old >"$FAKE_PACMAN_DBPATH/sync/su-dummy.db"
|
||||
run skup <<<'q'
|
||||
assert_success
|
||||
run skup <<<'1?'
|
||||
assert_failure 2
|
||||
run skup --dry-run <<<''
|
||||
assert_success
|
||||
assert_line 'Checking the reviewed transaction (pacman -Sup)... ok.'
|
||||
assert_line --regexp "^\+ cp -p -- /tmp/skeptical-updater-db\.[0-9]+\.[A-Za-z0-9]+/sync/su-dummy\.db .*$REAL_SYNC_RE/\$"
|
||||
assert_line '+ pacman -Su --noconfirm'
|
||||
[[ $(cat "$FAKE_PACMAN_DBPATH/sync/su-dummy.db") == old ]]
|
||||
log_lacks '^pacman -Su '
|
||||
run grep -E ' real$' "$FAKE_DIR/pacman-dbpath"
|
||||
refute_output --regexp '^-S(y|u|yu) '
|
||||
run ls /tmp
|
||||
refute_output --partial 'skeptical-updater-db.'
|
||||
}
|
||||
|
||||
@test "pacman: <enter> checks the reviewed transaction, installs the reviewed .db and .db.sig, then runs -Su" {
|
||||
use_fakes pacman
|
||||
echo old >"$FAKE_PACMAN_DBPATH/sync/su-dummy.db"
|
||||
FAKE_PACMAN_SIG=1 run skup <<<$'1-\n3-\n'
|
||||
assert_success
|
||||
# the order: sync the throwaway db, pre-check against it, -Su on the installed copy
|
||||
run grep -nE '^pacman (-Sy|-Sup|-Su|--dbpath)|^real sync' "$FAKE_LOG"
|
||||
assert_output --regexp 'pacman -Sy --dbpath /tmp/skeptical-updater-db\.'
|
||||
assert_output --regexp 'pacman -Sup --noconfirm --ignore su-dummy-a,su-dummy-c --dbpath /tmp/skeptical-updater-db\.[^ ]+ --print-format %n %v'
|
||||
log_has 'pacman -Su --noconfirm --ignore su-dummy-a,su-dummy-c'
|
||||
log_has 'real sync at -Su: reviewed 1 / signature 1'
|
||||
log_lacks '^pacman -Syu'
|
||||
[[ $(cat "$FAKE_PACMAN_DBPATH/sync/su-dummy.db") == "reviewed 1" ]]
|
||||
[[ $(cat "$FAKE_PACMAN_DBPATH/sync/su-dummy.db.sig") == "signature 1" ]]
|
||||
run ls -A "$FAKE_PACMAN_DBPATH/sync"
|
||||
assert_output "$(printf '%s\n' su-dummy.db su-dummy.db.sig)"
|
||||
}
|
||||
|
||||
@test "pacman: a failing pre-check (ignored-dependency trap) changes nothing and exits with its status" {
|
||||
use_fakes pacman
|
||||
echo old >"$FAKE_PACMAN_DBPATH/sync/su-dummy.db"
|
||||
FAKE_PACMAN_SUP_RC=1 run skup <<<$'2-\n'
|
||||
assert_failure 1
|
||||
assert_line 'Checking the reviewed transaction (pacman -Sup)... FAILED.'
|
||||
assert_output --partial ":: unable to satisfy dependency 'su-dummy-b>=2.0' required by su-dummy-a"
|
||||
assert_output --partial 'the reviewed upgrade cannot be done (pacman -Sup exited 1): nothing was changed'
|
||||
assert_output --partial 'hint: pacman aborts the whole transaction under --noconfirm'
|
||||
[[ $(cat "$FAKE_PACMAN_DBPATH/sync/su-dummy.db") == old ]]
|
||||
log_lacks '^pacman -Su '
|
||||
log_lacks '^real sync'
|
||||
}
|
||||
|
||||
@test "pacman: a failing -Su after the databases were installed warns how to finish" {
|
||||
use_fakes pacman
|
||||
FAKE_PACMAN_SU_RC=1 run skup <<<''
|
||||
assert_failure 1
|
||||
assert_output --partial "the system's pacman sync database now holds the reviewed metadata but 'pacman -Su' did not complete: it is newer than the installed packages. Finish the upgrade with 'pacman -Su' (or run skeptical-updater again)"
|
||||
[[ $(grep -c "did not complete" <<<"$output") -eq 1 ]]
|
||||
}
|
||||
|
||||
@test "pacman: an existing db.lck (another pacman) changes nothing and is left alone" {
|
||||
use_fakes pacman
|
||||
echo old >"$FAKE_PACMAN_DBPATH/sync/su-dummy.db"
|
||||
: >"$FAKE_PACMAN_DBPATH/db.lck"
|
||||
run skup <<<''
|
||||
assert_failure 1
|
||||
assert_output --partial "the pacman database is locked by another process ($FAKE_PACMAN_DBPATH/db.lck); nothing was changed"
|
||||
[[ $(cat "$FAKE_PACMAN_DBPATH/sync/su-dummy.db") == old ]]
|
||||
[[ -e $FAKE_PACMAN_DBPATH/db.lck ]]
|
||||
log_lacks '^pacman -Su '
|
||||
}
|
||||
|
||||
@test "pacman: db.lck is ours during the install and gone before -Su runs" {
|
||||
use_fakes pacman
|
||||
echo old >"$FAKE_PACMAN_DBPATH/sync/su-dummy.db"
|
||||
touch -d '2 hours ago' "$FAKE_PACMAN_DBPATH/sync/su-dummy.db"
|
||||
run skup <<<''
|
||||
assert_success
|
||||
log_has 'db.lck at -Su: no'
|
||||
[[ ! -e $FAKE_PACMAN_DBPATH/db.lck ]]
|
||||
}
|
||||
|
||||
@test "pacman: a system database that changed since the review and is newer: refused (exit 2), nothing changed" {
|
||||
use_fakes pacman
|
||||
echo "synced elsewhere" >"$FAKE_PACMAN_DBPATH/sync/su-dummy.db"
|
||||
touch -d '+1 hour' "$FAKE_PACMAN_DBPATH/sync/su-dummy.db"
|
||||
run skup <<<''
|
||||
assert_failure 2
|
||||
assert_output --partial 'package databases changed since the review (su-dummy); run skeptical-updater again'
|
||||
[[ $(cat "$FAKE_PACMAN_DBPATH/sync/su-dummy.db") == "synced elsewhere" ]]
|
||||
[[ ! -e $FAKE_PACMAN_DBPATH/db.lck ]]
|
||||
log_lacks '^pacman -Su '
|
||||
}
|
||||
|
||||
@test "pacman: an identical system database is not copied again" {
|
||||
use_fakes pacman
|
||||
echo "reviewed 1" >"$FAKE_PACMAN_DBPATH/sync/su-dummy.db"
|
||||
touch -d '2 hours ago' "$FAKE_PACMAN_DBPATH/sync/su-dummy.db"
|
||||
before=$(stat -c %Y "$FAKE_PACMAN_DBPATH/sync/su-dummy.db")
|
||||
run skup <<<''
|
||||
assert_success
|
||||
assert_line "The system's package databases are the reviewed ones already."
|
||||
refute_line --regexp '^\+ cp '
|
||||
[[ $(stat -c %Y "$FAKE_PACMAN_DBPATH/sync/su-dummy.db") == "$before" ]]
|
||||
log_has 'pacman -Su --noconfirm'
|
||||
}
|
||||
|
||||
@test "pacman: the install is all-or-nothing: a failed copy renames nothing" {
|
||||
use_fakes pacman
|
||||
echo old >"$FAKE_PACMAN_DBPATH/sync/su-dummy.db"
|
||||
rm -f "$SYSBIN/cp"
|
||||
FAKE_PACMAN_SIG=1 run skup <<<''
|
||||
assert_failure 1
|
||||
assert_output --partial 'cannot copy the reviewed package databases into'
|
||||
[[ $(cat "$FAKE_PACMAN_DBPATH/sync/su-dummy.db") == old ]]
|
||||
[[ ! -e $FAKE_PACMAN_DBPATH/sync/su-dummy.db.sig ]]
|
||||
run ls -A "$FAKE_PACMAN_DBPATH/sync"
|
||||
assert_output 'su-dummy.db'
|
||||
[[ ! -e $FAKE_PACMAN_DBPATH/db.lck ]]
|
||||
log_lacks '^pacman -Su '
|
||||
}
|
||||
|
||||
@test "pacman: Ctrl-C during -Su after the install: the exit handler says how to finish" {
|
||||
use_fakes pacman
|
||||
FAKE_PACMAN_SU_KILL=INT run skup <<<''
|
||||
assert_failure 130
|
||||
assert_output --partial "the system's pacman sync database now holds the reviewed metadata but 'pacman -Su' did not complete"
|
||||
assert_output --partial "Finish the upgrade with 'pacman -Su'"
|
||||
}
|
||||
|
||||
@test "pacman: errors pacman prints about the throwaway database are shown in the listing" {
|
||||
use_fakes pacman
|
||||
FAKE_PACMAN_STDERR="error: could not open database su-dummy: invalid or corrupted database (PGP signature)" run skup list
|
||||
assert_success
|
||||
assert_output --partial 'warning: pacman: error: could not open database su-dummy: invalid or corrupted database (PGP signature)'
|
||||
FAKE_PACMAN_STDERR="warning: database file for 'core' does not exist (use '-Sy' to download)" run skup list
|
||||
refute_output --partial 'warning: pacman:'
|
||||
}
|
||||
|
||||
@test "pacman: --no-refresh (or a failed refresh) upgrades with plain -Su" {
|
||||
use_fakes pacman
|
||||
run skup --no-refresh <<<''
|
||||
assert_success
|
||||
log_has 'pacman -Su --noconfirm'
|
||||
log_lacks '^pacman -Sup --noconfirm'
|
||||
: >"$FAKE_LOG"
|
||||
FAKE_PACMAN_SY_RC=1 run skup <<<''
|
||||
assert_success
|
||||
assert_line 'Refreshing package lists... FAILED.'
|
||||
log_has 'pacman -Su --noconfirm'
|
||||
log_lacks '^pacman -Sup --noconfirm'
|
||||
}
|
||||
|
||||
@test "pacman: a throwaway database that cannot be set up is reported; no refresh, plain -Su" {
|
||||
use_fakes pacman
|
||||
rm -f "$SYSBIN/ln"
|
||||
run skup <<<''
|
||||
assert_success
|
||||
assert_output --partial 'cannot set up the temporary pacman database /tmp/skeptical-updater-db.'
|
||||
assert_output --partial '(chmod/ln/mkdir failed): not refreshing'
|
||||
log_lacks '^pacman -Sy '
|
||||
log_has 'pacman -Su --noconfirm'
|
||||
}
|
||||
|
||||
@test "pacman: throwaway databases of dead runs are removed at the start of a root run, a live run's stays" {
|
||||
use_fakes pacman
|
||||
sleep 0 &
|
||||
dead=$!
|
||||
wait "$dead"
|
||||
mkdir -p "/tmp/skeptical-updater-db.$dead.Stale1/sync" "/tmp/skeptical-updater-db.$$.Live01"
|
||||
ln -s "$FAKE_PACMAN_DBPATH/local" "/tmp/skeptical-updater-db.$dead.Stale1/local"
|
||||
run skup list --no-refresh
|
||||
assert_success
|
||||
assert_line "Removed the temporary pacman database of an interrupted run (/tmp/skeptical-updater-db.$dead.Stale1)."
|
||||
[[ ! -e /tmp/skeptical-updater-db.$dead.Stale1 ]]
|
||||
[[ -d /tmp/skeptical-updater-db.$$.Live01 ]]
|
||||
[[ -d $FAKE_PACMAN_DBPATH/local ]] # the symlink target is untouched
|
||||
rm -rf "/tmp/skeptical-updater-db.$$.Live01"
|
||||
SKEPTICAL_UPDATER_EUID=1000 run skup list --no-refresh
|
||||
refute_output --partial 'Removed the temporary pacman database'
|
||||
}
|
||||
|
||||
@test "pacman: Nx on a package others depend on is refused (Required By), nothing removed" {
|
||||
use_fakes pacman
|
||||
run skup --no-refresh <<<$'2x\nq'
|
||||
assert_success
|
||||
assert_line 'not purged: su-dummy-b is required by: su-dummy-a - purge the dependents first'
|
||||
refute_output --partial '[y/N]'
|
||||
log_lacks '^pacman -R'
|
||||
}
|
||||
|
||||
@test "pacman: purge plan from -Rs --print (no cascade), purge with -Rns" {
|
||||
use_fakes pacman
|
||||
run skup --no-refresh <<<$'1x\ny\nq'
|
||||
assert_success
|
||||
log_has 'pacman -Rs --print --print-format %n -- su-dummy-a'
|
||||
assert_output --partial 'Purge su-dummy-a now? This also removes: su-dummy-b [y/N]'
|
||||
log_has 'pacman -Rns --noconfirm -- su-dummy-a su-dummy-b'
|
||||
log_lacks 'Rsc|Rnsc'
|
||||
}
|
||||
|
||||
@test "pacman: when pacman itself refuses (breaks dependency) its message is shown and nothing is asked" {
|
||||
use_fakes pacman
|
||||
FAKE_PACMAN_RS_REFUSE=1 run skup --no-refresh <<<$'3x\nq'
|
||||
assert_success
|
||||
assert_output --partial ":: removing su-dummy-c breaks dependency 'su-dummy-c' required by su-dummy-a"
|
||||
assert_output --partial 'cannot purge su-dummy-c: pacman refuses (see above); nothing was removed'
|
||||
refute_output --partial '[y/N]'
|
||||
log_lacks '^pacman -Rns'
|
||||
}
|
||||
|
||||
@test "pacman: belt and braces - a plan with a non-dependency is refused" {
|
||||
use_fakes pacman
|
||||
FAKE_PACMAN_RS_EXTRA=su-dummy-c:expat run skup --no-refresh <<<$'3x\nq'
|
||||
assert_success
|
||||
assert_output --partial 'cannot purge su-dummy-c: pacman would also remove expat, which su-dummy-c does not depend on'
|
||||
log_lacks '^pacman -Rns'
|
||||
}
|
||||
@test "pacman: a failed upgrade with exclusions explains the --noconfirm dependency trap" {
|
||||
use_fakes pacman
|
||||
FAKE_PACMAN_SU_RC=1 run skup --no-refresh <<<$'2-\n'
|
||||
assert_failure 1
|
||||
assert_output --partial 'hint: pacman aborts the whole transaction under --noconfirm when an ignored package is needed at a newer version'
|
||||
}
|
||||
|
||||
@test "pacman: IgnorePkg rows ([ignored]) are not added to --ignore (pacman applies them itself)" {
|
||||
use_fakes pacman
|
||||
FAKE_PACMAN_QU=real run skup --no-refresh <<<''
|
||||
assert_success
|
||||
log_has 'pacman -Su --noconfirm'
|
||||
}
|
||||
|
||||
# ---- dnf ----------------------------------------------------------------------------------------
|
||||
|
||||
@test "dnf: refresh is dnf -q --refresh --setopt=skip_if_unavailable=0 makecache" {
|
||||
use_fakes dnf rpm
|
||||
run skup list
|
||||
assert_success
|
||||
log_has 'dnf -q --refresh --setopt=skip_if_unavailable=0 makecache'
|
||||
}
|
||||
|
||||
@test "dnf: a failed refresh warns and goes on" {
|
||||
use_fakes dnf rpm
|
||||
FAKE_DNF_MAKECACHE_RC=1 run skup list
|
||||
assert_success
|
||||
assert_line 'Refreshing package lists... FAILED.'
|
||||
assert_output --partial 'warning: metadata refresh failed'
|
||||
}
|
||||
|
||||
@test "dnf: exclusions are --exclude=a,b; the reviewed metadata is frozen (*.metadata_expire=-1)" {
|
||||
use_fakes dnf rpm
|
||||
run skup --no-refresh <<<$'3-\n4-\n'
|
||||
assert_success
|
||||
assert_line 'Excluded from this run: su-dummy-a su-dummy-b'
|
||||
log_has "$DNF_UPGRADE --exclude=su-dummy-a,su-dummy-b"
|
||||
assert_line "+ dnf -y --setopt=skip_if_unavailable=0 --setopt=\\*.metadata_expire=-1 upgrade --exclude=su-dummy-a,su-dummy-b"
|
||||
}
|
||||
|
||||
@test "dnf: no exclusion => no --exclude" {
|
||||
use_fakes dnf rpm
|
||||
run skup --no-refresh <<<''
|
||||
assert_success
|
||||
log_has "$DNF_UPGRADE"
|
||||
}
|
||||
|
||||
@test "dnf: Nx on a package others depend on is refused (rpm reverse dependencies), nothing removed" {
|
||||
use_fakes dnf rpm
|
||||
export FAKE_RPM_NO_D=1
|
||||
run skup --no-refresh <<<$'4x\nq'
|
||||
assert_success
|
||||
assert_line 'not purged: su-dummy-b is required by: su-dummy-a - purge the dependents first'
|
||||
refute_output --partial '[y/N]'
|
||||
log_lacks 'remove'
|
||||
}
|
||||
|
||||
@test "dnf: purge plan from dnf --assumeno remove (dnf4 table, unused dependencies), then dnf -y remove" {
|
||||
use_fakes dnf rpm
|
||||
export FAKE_RPM_NO_D=1
|
||||
FAKE_DNF_UNUSED=su-dummy-a:su-dummy-b run skup --no-refresh <<<$'3x\ny\nl\nq'
|
||||
assert_success
|
||||
log_has 'dnf --assumeno remove su-dummy-a'
|
||||
assert_output --partial 'Purge su-dummy-a now? This also removes: su-dummy-b [y/N]'
|
||||
log_has 'dnf -y remove su-dummy-a su-dummy-b'
|
||||
[[ $(grep -c '\[4\] su-dummy-b' <<<"$output") -eq 1 ]]
|
||||
[[ $(grep -c '\[3\] su-dummy-a' <<<"$output") -eq 1 ]]
|
||||
}
|
||||
|
||||
@test "dnf: belt and braces - a removal table with a non-dependency is refused" {
|
||||
use_fakes dnf rpm
|
||||
export FAKE_RPM_NO_D=1
|
||||
FAKE_DNF_DEPENDENT=su-dummy-c:bash run skup --no-refresh <<<$'5x\nq'
|
||||
assert_success
|
||||
assert_output --partial 'cannot purge su-dummy-c: dnf would also remove bash, which su-dummy-c does not depend on'
|
||||
log_lacks '^dnf -y remove'
|
||||
}
|
||||
@test "dnf5: the purge plan table is parsed as well" {
|
||||
use_fakes dnf rpm
|
||||
export FAKE_RPM_NO_D=1
|
||||
FAKE_DNF_MAJOR=5 FAKE_DNF_UNUSED=su-dummy-a:su-dummy-b run skup --no-refresh <<<$'3x\ny\nq'
|
||||
assert_success
|
||||
assert_output --partial 'Purge su-dummy-a now? This also removes: su-dummy-b [y/N]'
|
||||
log_has 'dnf -y remove su-dummy-a su-dummy-b'
|
||||
}
|
||||
@test "dnf: a refused removal (protected package) asks nothing and removes nothing" {
|
||||
use_fakes dnf rpm
|
||||
export FAKE_RPM_NO_D=1
|
||||
FAKE_DNF_REMOVE_REFUSE=1 run skup --no-refresh <<<$'5x\nq'
|
||||
assert_success
|
||||
assert_output --partial 'Error: Problem: The operation would result in removing the following protected packages: su-dummy-c'
|
||||
assert_output --partial 'cannot purge su-dummy-c: dnf refuses (see above); nothing was removed'
|
||||
refute_output --partial '[y/N]'
|
||||
log_lacks '^dnf -y remove'
|
||||
}
|
||||
|
||||
@test "dnf: a failed upgrade with exclusions explains the dnf4 excluded-dependency rule" {
|
||||
use_fakes dnf rpm
|
||||
FAKE_DNF_UPGRADE_RC=1 run skup --no-refresh <<<$'4-\n'
|
||||
assert_failure 1
|
||||
assert_output --partial "hint: dnf4 refuses to upgrade a package whose required newer dependency is excluded (exit 1)"
|
||||
}
|
||||
|
||||
@test "yum fallback: the same commands through yum" {
|
||||
use_fakes yum rpm
|
||||
run skup <<<$'4-\n'
|
||||
assert_success
|
||||
log_has 'yum -q --refresh --setopt=skip_if_unavailable=0 makecache'
|
||||
log_has 'yum -y --setopt=skip_if_unavailable=0 --setopt=*.metadata_expire=-1 upgrade --exclude=su-dummy-b'
|
||||
}
|
||||
|
||||
# ---- zypper -------------------------------------------------------------------------------------
|
||||
|
||||
zypp_conf() { # a "real" zypp.conf + locks file with one user lock, in the test dir
|
||||
mkdir -p "$BATS_TEST_TMPDIR/etc"
|
||||
printf '[main]\n# locksfile.path = /etc/zypp/locks\nlocksfile.path = %s\n' "$BATS_TEST_TMPDIR/etc/locks" >"$BATS_TEST_TMPDIR/etc/zypp.conf"
|
||||
printf '\ntype: package\nmatch_type: glob\ncase_sensitive: on\nsolvable_name: su-dummy-c\ncomment: user lock\n\n' >"$BATS_TEST_TMPDIR/etc/locks"
|
||||
export ZYPP_CONF=$BATS_TEST_TMPDIR/etc/zypp.conf
|
||||
}
|
||||
|
||||
@test "zypper: refresh is zypper --non-interactive --quiet refresh" {
|
||||
use_fakes zypper rpm
|
||||
run skup list
|
||||
assert_success
|
||||
log_has 'zypper --non-interactive --quiet refresh'
|
||||
}
|
||||
|
||||
@test "zypper: a failed refresh (exit 4) warns and goes on" {
|
||||
use_fakes zypper rpm
|
||||
FAKE_ZYPPER_REFRESH_RC=4 run skup list
|
||||
assert_success
|
||||
assert_output --partial 'Could not refresh the repositories because of errors.'
|
||||
assert_output --partial 'exited 4); continuing with the cached package lists'
|
||||
}
|
||||
|
||||
@test "zypper: Leap (ID=opensuse-leap) upgrades with update --no-refresh" {
|
||||
use_fakes zypper rpm
|
||||
export SKEPTICAL_UPDATER_OS_RELEASE=$FAKE_DATA/os-release-leap
|
||||
run skup --no-refresh <<<''
|
||||
assert_success
|
||||
log_has 'zypper --non-interactive --no-refresh update'
|
||||
}
|
||||
|
||||
@test "zypper: Tumbleweed (ID=opensuse-tumbleweed) upgrades with dist-upgrade --no-refresh" {
|
||||
use_fakes zypper rpm
|
||||
export SKEPTICAL_UPDATER_OS_RELEASE=$FAKE_DATA/os-release-tumbleweed
|
||||
run skup --no-refresh <<<''
|
||||
assert_success
|
||||
log_has 'zypper --non-interactive --no-refresh dist-upgrade'
|
||||
}
|
||||
|
||||
@test "zypper: exclusions are locks in a private ZYPP_CONF seeded with the real locks; the real lock file is untouched" {
|
||||
use_fakes zypper rpm
|
||||
zypp_conf
|
||||
export SKEPTICAL_UPDATER_OS_RELEASE=$FAKE_DATA/os-release-leap
|
||||
before=$(cksum <"$BATS_TEST_TMPDIR/etc/locks")
|
||||
run skup --no-refresh <<<$'2-\n'
|
||||
assert_success
|
||||
run grep -E '^zypper ZYPP_CONF=.*/skeptical-updater\.' "$FAKE_LOG"
|
||||
assert_line --index 0 --regexp "^zypper ZYPP_CONF=$BATS_TEST_TMPDIR/skeptical-updater\.[A-Za-z0-9]+/zypp\.conf --non-interactive --quiet addlock su-dummy-b\$"
|
||||
assert_line --index 1 --regexp "^zypper ZYPP_CONF=$BATS_TEST_TMPDIR/skeptical-updater\.[A-Za-z0-9]+/zypp\.conf --non-interactive --no-refresh update\$"
|
||||
# the private lock file seen by the upgrade: the user's lock + ours
|
||||
run sed -n '/^--- locks seen by update:/,/^--- end/p' "$FAKE_LOG"
|
||||
assert_line 'solvable_name: su-dummy-c'
|
||||
assert_line 'comment: user lock'
|
||||
assert_line 'solvable_name: su-dummy-b'
|
||||
# real locks unchanged, private directory and its marker removed
|
||||
[[ $(cksum <"$BATS_TEST_TMPDIR/etc/locks") == "$before" ]]
|
||||
run grep -c 'solvable_name' "$BATS_TEST_TMPDIR/etc/locks"
|
||||
assert_output 1
|
||||
run ls "$BATS_TEST_TMPDIR"
|
||||
refute_output --partial 'skeptical-updater.'
|
||||
[[ ! -e $STATE/zypper-private ]]
|
||||
}
|
||||
|
||||
@test "zypper: commented locksfile.path lines are ignored; the private one (last [main]) is used" {
|
||||
use_fakes zypper rpm
|
||||
zypp_conf
|
||||
run skup --no-refresh <<<$'1-\n'
|
||||
assert_success
|
||||
run sed -n '/^--- locks seen by/,/^--- end/p' "$FAKE_LOG"
|
||||
assert_line 'comment: user lock'
|
||||
assert_line 'solvable_name: su-dummy-a'
|
||||
}
|
||||
|
||||
@test "zypper: zypp.conf.d drop-ins are part of the private config; a drop-in locksfile.path is honoured" {
|
||||
use_fakes zypper rpm
|
||||
r=$BATS_TEST_TMPDIR/root
|
||||
mkdir -p "$r/usr/etc/zypp/zypp.conf.d" "$r/etc/zypp/zypp.conf.d"
|
||||
printf '[main]\n# vendor file\n' >"$r/usr/etc/zypp/zypp.conf"
|
||||
printf '[main]\nlocksfile.path = /nonexistent/vendor-dropin-locks\n' >"$r/usr/etc/zypp/zypp.conf.d/10-vendor.conf"
|
||||
printf '[main]\nsolver.onlyRequires = true\nlocksfile.path = %s\n' "$r/admin-locks" >"$r/etc/zypp/zypp.conf.d/50-admin.conf"
|
||||
printf '\ntype: package\nmatch_type: glob\ncase_sensitive: on\nsolvable_name: su-dummy-c\ncomment: drop-in lock\n\n' >"$r/admin-locks"
|
||||
export SKEPTICAL_UPDATER_ZYPP_ROOT=$r
|
||||
run skup --no-refresh <<<$'1-\n'
|
||||
assert_success
|
||||
run sed -n '/^--- locks seen by/,/^--- end/p' "$FAKE_LOG"
|
||||
assert_line 'comment: drop-in lock'
|
||||
assert_line 'solvable_name: su-dummy-a'
|
||||
[[ $(grep -c solvable_name "$r/admin-locks") -eq 1 ]]
|
||||
}
|
||||
|
||||
@test "zypper: with a drop-in, the dry run names every file of the private configuration" {
|
||||
use_fakes zypper rpm
|
||||
r=$BATS_TEST_TMPDIR/root
|
||||
mkdir -p "$r/etc/zypp/zypp.conf.d"
|
||||
printf '[main]\n' >"$r/etc/zypp/zypp.conf"
|
||||
printf '[main]\nlocksfile.path = %s\n' "$r/l" >"$r/etc/zypp/zypp.conf.d/50-admin.conf"
|
||||
SKEPTICAL_UPDATER_ZYPP_ROOT=$r run skup --dry-run --no-refresh <<<$'1-\n'
|
||||
assert_success
|
||||
assert_line --regexp "^\+ # private .*/zypp\.conf: $r/etc/zypp/zypp\.conf \+ $r/etc/zypp/zypp\.conf\.d/50-admin\.conf with locksfile\.path = .*/locks \(a copy of $r/l\)\$"
|
||||
}
|
||||
|
||||
@test "zypper: no exclusion => plain zypper --non-interactive --no-refresh VERB, no private config" {
|
||||
use_fakes zypper rpm
|
||||
export SKEPTICAL_UPDATER_OS_RELEASE=$FAKE_DATA/os-release-leap
|
||||
run skup --no-refresh <<<''
|
||||
assert_success
|
||||
log_lacks 'ZYPP_CONF='
|
||||
log_lacks 'addlock'
|
||||
}
|
||||
|
||||
@test "zypper: Nx on a package others depend on is refused (rpm reverse dependencies), nothing removed" {
|
||||
use_fakes zypper rpm
|
||||
export FAKE_RPM_NO_D=1
|
||||
run skup --no-refresh <<<$'2x\nq'
|
||||
assert_success
|
||||
assert_line 'not purged: su-dummy-b is required by: su-dummy-a - purge the dependents first'
|
||||
refute_output --partial '[y/N]'
|
||||
log_lacks 'remove'
|
||||
}
|
||||
|
||||
@test "zypper: purge plan from remove --dry-run --xmlout (--clean-deps), then remove --clean-deps" {
|
||||
use_fakes zypper rpm
|
||||
export FAKE_RPM_NO_D=1
|
||||
FAKE_ZYPPER_CLEAN=su-dummy-a:su-dummy-b run skup --no-refresh <<<$'1x\ny\nq'
|
||||
assert_success
|
||||
log_has 'zypper --non-interactive --no-refresh --xmlout remove --dry-run --clean-deps su-dummy-a'
|
||||
assert_output --partial 'Purge su-dummy-a now? This also removes: su-dummy-b [y/N]'
|
||||
log_has 'zypper --non-interactive --no-refresh remove --clean-deps su-dummy-a su-dummy-b'
|
||||
}
|
||||
|
||||
@test "zypper: belt and braces - a removal plan with a non-dependency is refused" {
|
||||
use_fakes zypper rpm
|
||||
export FAKE_RPM_NO_D=1
|
||||
FAKE_ZYPPER_EXTRA=su-dummy-c:bash run skup --no-refresh <<<$'3x\nq'
|
||||
assert_success
|
||||
assert_output --partial 'cannot purge su-dummy-c: zypper would also remove bash, which su-dummy-c does not depend on'
|
||||
log_lacks 'remove --clean-deps su-dummy-c'
|
||||
}
|
||||
@test "zypper: a package zypper does not know (exit 104) is not purged, nothing is asked" {
|
||||
use_fakes zypper rpm
|
||||
FAKE_ZYPPER_LU=real run skup --no-refresh <<<$'1x\nq'
|
||||
assert_success
|
||||
assert_output --partial "zypper: no installed package provides 'openSUSE-build-key' (exit 104)"
|
||||
assert_output --partial 'cannot purge openSUSE-build-key: zypper refuses (see above); nothing was removed'
|
||||
refute_output --partial '[y/N]'
|
||||
}
|
||||
|
||||
@test "zypper: exit 4 from the upgrade is passed through, with a hint" {
|
||||
use_fakes zypper rpm
|
||||
export SKEPTICAL_UPDATER_OS_RELEASE=$FAKE_DATA/os-release-tumbleweed
|
||||
FAKE_ZYPPER_UPGRADE_RC=4 run skup --no-refresh <<<''
|
||||
assert_failure 4
|
||||
assert_output --partial "hint: zypper hit a dependency/vendor problem that --non-interactive cancels (exit 4); run 'zypper dist-upgrade' by hand"
|
||||
}
|
||||
|
||||
@test "zypper: an unreadable zypp.conf with exclusions is an error, nothing is upgraded" {
|
||||
use_fakes zypper rpm
|
||||
export ZYPP_CONF=$BATS_TEST_TMPDIR/missing.conf
|
||||
run skup --no-refresh <<<$'1-\n'
|
||||
assert_failure 1
|
||||
assert_output --partial 'cannot read the zypp configuration'
|
||||
log_lacks ' (update|dist-upgrade)$'
|
||||
}
|
||||
|
||||
@test "zypper: a private configuration left by an interrupted run is removed at the next start" {
|
||||
use_fakes zypper rpm
|
||||
mkdir -p "$STATE" "$BATS_TEST_TMPDIR/skeptical-updater.Leftov"
|
||||
: >"$BATS_TEST_TMPDIR/skeptical-updater.Leftov/zypp.conf"
|
||||
echo "$BATS_TEST_TMPDIR/skeptical-updater.Leftov" >"$STATE/zypper-private"
|
||||
run skup list --no-refresh
|
||||
assert_success
|
||||
assert_line "Removed the private zypper configuration of an interrupted run ($BATS_TEST_TMPDIR/skeptical-updater.Leftov)."
|
||||
[[ ! -e $BATS_TEST_TMPDIR/skeptical-updater.Leftov ]]
|
||||
[[ ! -e $STATE/zypper-private ]]
|
||||
}
|
||||
|
||||
@test "zypper: the replay leaves a running upgrade's private configuration alone" {
|
||||
use_fakes zypper rpm
|
||||
mkdir -p "$STATE" "$BATS_TEST_TMPDIR/skeptical-updater.Running"
|
||||
echo "$BATS_TEST_TMPDIR/skeptical-updater.Running" >"$STATE/zypper-private"
|
||||
exec 9>>"$STATE/zypper.lock"
|
||||
flock 9
|
||||
run skup info su-dummy-a
|
||||
exec 9>&-
|
||||
assert_success
|
||||
assert_line 'Note: another skeptical-updater run is using its private zypper configuration; leaving it alone.'
|
||||
[[ -d $BATS_TEST_TMPDIR/skeptical-updater.Running ]]
|
||||
[[ -e $STATE/zypper-private ]]
|
||||
}
|
||||
|
||||
@test "zypper: the upgrade holds zypper.lock while its private configuration exists" {
|
||||
use_fakes zypper rpm
|
||||
zypp_conf
|
||||
run skup --no-refresh <<<$'1-\n'
|
||||
assert_success
|
||||
log_has 'zypper.lock during update: held'
|
||||
flock -n "$STATE/zypper.lock" true
|
||||
}
|
||||
|
||||
@test "zypper: the private configuration is removed when the upgrade is killed" {
|
||||
use_fakes zypper rpm
|
||||
zypp_conf
|
||||
# an upgrade that is killed (SIGTERM to the applet) in the middle
|
||||
FAKE_ZYPPER_UPGRADE_KILL=TERM run skup --no-refresh <<<$'1-\n'
|
||||
assert_failure 143
|
||||
run ls "$BATS_TEST_TMPDIR"
|
||||
refute_output --partial 'skeptical-updater.'
|
||||
[[ ! -e $STATE/zypper-private ]]
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
#!/usr/bin/env bats
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
# Copyright (C) 2026 Bryan Joshua Pedini
|
||||
|
||||
# CLI parsing and exit codes.
|
||||
|
||||
load test_helper
|
||||
|
||||
setup() { common_setup; }
|
||||
|
||||
@test "--help prints the usage and exits 0" {
|
||||
run skup --help
|
||||
assert_success
|
||||
assert_line 'usage: skeptical-updater [options] [command]'
|
||||
assert_line --partial 'refresh recompute dependency closures for every '"'user'"' root'
|
||||
assert_line --partial '--dry-run print native commands instead of running mutating ones'
|
||||
assert_line --partial 'exit status: 0 ok, 1 usage/backend error, 2 aborted'
|
||||
[[ -z $(cat "$FAKE_LOG") ]]
|
||||
}
|
||||
|
||||
@test "-h is --help, even after a command" {
|
||||
run skup list -h
|
||||
assert_success
|
||||
assert_line 'usage: skeptical-updater [options] [command]'
|
||||
}
|
||||
|
||||
@test "--version and -V print the version" {
|
||||
run skup --version
|
||||
assert_success
|
||||
assert_output 'skeptical-updater 0.1.0'
|
||||
run skup -V
|
||||
assert_output 'skeptical-updater 0.1.0'
|
||||
}
|
||||
|
||||
@test "an unknown option is a usage error (exit 1)" {
|
||||
run skup --frobnicate
|
||||
assert_failure 1
|
||||
assert_line 'skeptical-updater: error: unknown option: --frobnicate'
|
||||
assert_line "Try 'skeptical-updater --help'."
|
||||
}
|
||||
|
||||
@test "an unknown command is a usage error (exit 1)" {
|
||||
run skup upgrade
|
||||
assert_failure 1
|
||||
assert_line 'skeptical-updater: error: unknown command: upgrade'
|
||||
}
|
||||
|
||||
@test "list, trusted and refresh take no arguments" {
|
||||
for c in list trusted refresh; do
|
||||
run skup "$c" extra
|
||||
assert_failure 1
|
||||
assert_output --partial "'$c' takes no arguments"
|
||||
done
|
||||
}
|
||||
|
||||
@test "the interactive mode takes no arguments either" {
|
||||
run skup '' foo
|
||||
assert_failure 1
|
||||
}
|
||||
|
||||
@test "trust and untrust need at least one package" {
|
||||
run skup trust
|
||||
assert_failure 1
|
||||
assert_output --partial "'trust' needs at least one package name"
|
||||
run skup untrust
|
||||
assert_failure 1
|
||||
assert_output --partial "'untrust' needs at least one package name"
|
||||
}
|
||||
|
||||
@test "info and show need exactly one package" {
|
||||
run skup info
|
||||
assert_failure 1
|
||||
assert_output --partial "'info' needs exactly one package name"
|
||||
run skup show a b
|
||||
assert_failure 1
|
||||
assert_output --partial "'show' needs exactly one package name"
|
||||
}
|
||||
|
||||
@test "invalid package names are rejected before anything runs" {
|
||||
# shellcheck disable=SC2016 # literal $(id) on purpose
|
||||
for bad in '-rf' 'a b' 'foo;rm' '../x' 'x/y' '$(id)'; do
|
||||
run skup trust -- "$bad"
|
||||
assert_failure 1
|
||||
assert_output --partial "invalid package name: '$bad'"
|
||||
done
|
||||
[[ -z $(cat "$FAKE_LOG") ]]
|
||||
[[ ! -e $STATE ]]
|
||||
}
|
||||
|
||||
@test "valid package names with + . : ~ @ _ are accepted" {
|
||||
run skup --backend=apt trust 'libstdc++6' 'libc6:i386' 'g++-12' 'a.b~c' 'x_y@z'
|
||||
assert_success
|
||||
run cut -f2 "$STATE_FILE"
|
||||
assert_output "$(printf '%s\n' "$HEADER" a.b~c g++-12 libc6:i386 libstdc++6 x_y@z)"
|
||||
}
|
||||
|
||||
@test "--backend must name a known backend" {
|
||||
run skup --backend=portage list
|
||||
assert_failure 1
|
||||
assert_output --partial "unknown backend 'portage' (expected apt, dnf, pacman or zypper)"
|
||||
}
|
||||
|
||||
@test "--backend and --state-dir without a value are usage errors" {
|
||||
run skup list --backend
|
||||
assert_failure 1
|
||||
assert_output --partial '--backend needs a value'
|
||||
run skup list --state-dir
|
||||
assert_failure 1
|
||||
assert_output --partial '--state-dir needs a value'
|
||||
run skup list --state-dir=
|
||||
assert_failure 1
|
||||
assert_output --partial 'empty state directory'
|
||||
}
|
||||
|
||||
@test "options may follow the command; --backend NAME as two words" {
|
||||
run skup list --backend apt --no-refresh
|
||||
assert_success
|
||||
assert_line --index 0 'Backend: apt (Debian GNU/Linux 12 (bookworm))'
|
||||
log_lacks '^apt-get update'
|
||||
}
|
||||
|
||||
@test "--state-dir overrides SKEPTICAL_UPDATER_STATE_DIR" {
|
||||
run skup --state-dir="$BATS_TEST_TMPDIR/other" trust su-dummy-c
|
||||
assert_success
|
||||
[[ -f $BATS_TEST_TMPDIR/other/trusted.tsv ]]
|
||||
[[ ! -e $STATE ]]
|
||||
}
|
||||
|
||||
@test "-- ends option parsing" {
|
||||
run skup info -- --all
|
||||
assert_failure 1
|
||||
assert_output --partial "invalid package name: '--all'"
|
||||
}
|
||||
|
||||
@test "no ANSI colours when stdout is not a terminal" {
|
||||
run skup list --no-refresh
|
||||
assert_success
|
||||
refute_output --partial $'\e['
|
||||
}
|
||||
|
||||
@test "--no-color and NO_COLOR are accepted" {
|
||||
run skup --no-color list --no-refresh
|
||||
assert_success
|
||||
NO_COLOR=1 run skup list --no-refresh
|
||||
assert_success
|
||||
refute_output --partial $'\e['
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
su-dummy-orphan
|
||||
@@ -0,0 +1,13 @@
|
||||
ii su-dummy-a su-dummy-b
|
||||
ii su-dummy-b
|
||||
ii su-dummy-c
|
||||
ii su-dummy-w su-virtual | su-dummy-alt
|
||||
ii su-dummy-v su-virtual
|
||||
rc su-dummy-f su-dummy-c
|
||||
ii base-files awk
|
||||
ii mawk libc6 (>= 2.29) awk
|
||||
ii libc6 libgcc-s1
|
||||
ii libgcc-s1 gcc-12-base (= 12.2.0-14), libc6 (>= 2.35)
|
||||
ii gcc-12-base
|
||||
ii su-dummy-orphan
|
||||
ii su-dummy-c-data
|
||||
|
@@ -0,0 +1,25 @@
|
||||
perl-base:
|
||||
Installed: 5.36.0-7+deb12u3
|
||||
Candidate: 5.36.0-7+deb12u4
|
||||
Version table:
|
||||
5.36.0-7+deb12u4 500
|
||||
500 http://deb.debian.org/debian-security bookworm-security/main amd64 Packages
|
||||
*** 5.36.0-7+deb12u3 500
|
||||
500 http://deb.debian.org/debian bookworm/main amd64 Packages
|
||||
100 /var/lib/dpkg/status
|
||||
su-dummy-c:
|
||||
Installed: 1.0
|
||||
Candidate: 2.0
|
||||
Version table:
|
||||
2.0 500
|
||||
500 file:/srv/su-repo ./ Packages
|
||||
*** 1.0 100
|
||||
100 /var/lib/dpkg/status
|
||||
su-dummy-e:
|
||||
Installed: 1.0
|
||||
Candidate: 2.0
|
||||
Version table:
|
||||
2.0 500
|
||||
500 file:/srv/su-repo ./ Packages
|
||||
*** 1.0 100
|
||||
100 /var/lib/dpkg/status
|
||||
@@ -0,0 +1 @@
|
||||
su-dummy-b: su-dummy-a
|
||||
@@ -0,0 +1,3 @@
|
||||
gpgv
|
||||
Reverse Depends:
|
||||
|apt
|
||||
@@ -0,0 +1,4 @@
|
||||
su-dummy-b
|
||||
Reverse Depends:
|
||||
su-dummy-a
|
||||
su-dummy-a
|
||||
@@ -0,0 +1,5 @@
|
||||
Package: gpgv
|
||||
Version: 2.2.40-1.1
|
||||
Architecture: amd64
|
||||
Description: GNU privacy guard - signature verification tool
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
Package: su-dummy-a
|
||||
Version: 2.0
|
||||
Section: misc
|
||||
Priority: optional
|
||||
Architecture: all
|
||||
Maintainer: Skeptical Updater Fixture <su-fixture@example.invalid>
|
||||
Depends: su-dummy-b (>= 2.0)
|
||||
Description: dummy package su-dummy-a for skeptical-updater tests
|
||||
This package only exists to exercise the package manager.
|
||||
.
|
||||
It ships one data file and one conffile (version 2.0).
|
||||
Description-md5: 36da7dcdaf4ec207be2937d1f5dfa408
|
||||
Filename: ./su-dummy-a_2.0_all.deb
|
||||
Size: 840
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
Package: su-dummy-b
|
||||
Version: 2.0
|
||||
Section: misc
|
||||
Priority: optional
|
||||
Architecture: all
|
||||
Maintainer: Skeptical Updater Fixture <su-fixture@example.invalid>
|
||||
Description: dummy package su-dummy-b for skeptical-updater tests
|
||||
This package only exists to exercise the package manager.
|
||||
.
|
||||
It ships one data file and one conffile (version 2.0).
|
||||
Description-md5: 36da7dcdaf4ec207be2937d1f5dfa408
|
||||
Filename: ./su-dummy-b_2.0_all.deb
|
||||
Size: 840
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
Package: su-dummy-c
|
||||
Version: 2.0
|
||||
Section: misc
|
||||
Priority: optional
|
||||
Architecture: all
|
||||
Maintainer: Skeptical Updater Fixture <su-fixture@example.invalid>
|
||||
Description: dummy package su-dummy-c for skeptical-updater tests
|
||||
This package only exists to exercise the package manager.
|
||||
.
|
||||
It ships one data file and one conffile (version 2.0).
|
||||
Description-md5: 36da7dcdaf4ec207be2937d1f5dfa408
|
||||
Filename: ./su-dummy-c_2.0_all.deb
|
||||
Size: 840
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
Reading package lists...
|
||||
Building dependency tree...
|
||||
Reading state information...
|
||||
Calculating upgrade...
|
||||
The following packages will be upgraded:
|
||||
su-dummy-a su-dummy-b su-dummy-c
|
||||
3 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.
|
||||
Inst su-dummy-b [1.0] (2.0 su-fixture:su-repo [all])
|
||||
Inst su-dummy-a [1.0] (2.0 su-fixture:su-repo [all])
|
||||
Inst su-dummy-c [1.0] (2.0 su-fixture:su-repo [all])
|
||||
Conf su-dummy-b (2.0 su-fixture:su-repo [all])
|
||||
Conf su-dummy-a (2.0 su-fixture:su-repo [all])
|
||||
Conf su-dummy-c (2.0 su-fixture:su-repo [all])
|
||||
@@ -0,0 +1,13 @@
|
||||
Reading package lists...
|
||||
Building dependency tree...
|
||||
Reading state information...
|
||||
Calculating upgrade...
|
||||
The following packages have been kept back:
|
||||
su-dummy-c
|
||||
The following packages will be upgraded:
|
||||
su-dummy-a su-dummy-b
|
||||
2 upgraded, 0 newly installed, 0 to remove and 1 not upgraded.
|
||||
Inst su-dummy-b [1.0] (2.0 su-fixture:su-repo [all])
|
||||
Inst su-dummy-a [1.0] (2.0 su-fixture:su-repo [all])
|
||||
Conf su-dummy-b (2.0 su-fixture:su-repo [all])
|
||||
Conf su-dummy-a (2.0 su-fixture:su-repo [all])
|
||||
@@ -0,0 +1,5 @@
|
||||
Reading package lists...
|
||||
Building dependency tree...
|
||||
Reading state information...
|
||||
Calculating upgrade...
|
||||
0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.
|
||||
@@ -0,0 +1,26 @@
|
||||
NOTE: This is only a simulation!
|
||||
apt-get needs root privileges for real execution.
|
||||
Keep also in mind that locking is deactivated,
|
||||
so don't depend on the relevance to the real current situation!
|
||||
Reading package lists...
|
||||
Building dependency tree...
|
||||
Reading state information...
|
||||
Calculating upgrade...
|
||||
The following NEW packages will be installed:
|
||||
su-dummy-newdep
|
||||
The following packages have been kept back:
|
||||
perl-base su-dummy-c su-dummy-e
|
||||
The following packages will be upgraded:
|
||||
libaudit-common libaudit1 libc6:i386 libssl3t64 su-dummy-m su-dummy-slow
|
||||
6 upgraded, 1 newly installed, 0 to remove and 3 not upgraded.
|
||||
Inst libaudit-common [1:3.1.2-2.1build1.1] (1:3.1.2-2.1ubuntu0.1 Ubuntu:24.04/noble-updates [all])
|
||||
Conf libaudit-common (1:3.1.2-2.1ubuntu0.1 Ubuntu:24.04/noble-updates [all])
|
||||
Inst libaudit1 [1:3.1.2-2.1build1.1] (1:3.1.2-2.1ubuntu0.1 Ubuntu:24.04/noble-updates [amd64])
|
||||
Inst libssl3t64 [3.0.13-0ubuntu3.15] (3.0.13-0ubuntu3.16 Ubuntu:24.04/noble-updates, Ubuntu:24.04/noble-security [amd64])
|
||||
Inst su-dummy-newdep (1.0 su-fixture:su-repo [all])
|
||||
Inst su-dummy-slow [0.9] (1.0 [all])
|
||||
Inst su-dummy-m [1.0] (2.0 su-fixture:su-repo [amd64]) [su-dummy-m:amd64 on su-dummy-m:i386] [su-dummy-m:i386 ]
|
||||
Inst libc6:i386 [2.36-9] (2.36-9+deb12u10 Debian-Security:12/oldstable-security, Debian:12.12/oldstable [i386]) []
|
||||
Remv su-dummy-f [1.0]
|
||||
Conf libaudit1 (1:3.1.2-2.1ubuntu0.1 Ubuntu:24.04/noble-updates [amd64])
|
||||
Conf libssl3t64 (3.0.13-0ubuntu3.16 Ubuntu:24.04/noble-updates, Ubuntu:24.04/noble-security [amd64])
|
||||
@@ -0,0 +1,10 @@
|
||||
|
||||
gnutls.x86_64 3.8.10-9.el9_8 baseos
|
||||
kernel-core.x86_64 5.14.0-612.1.el9 baseos
|
||||
su-dummy-a.noarch 2.0-1 su-dummy
|
||||
su-dummy-b.noarch 2.0-1 su-dummy
|
||||
su-dummy-c.x86_64 2.0-1 su-dummy
|
||||
vim-minimal.x86_64 2:8.2.2637-26.el9_8.23 baseos
|
||||
Obsoleting Packages
|
||||
su-dummy-new.noarch 1.0-1 su-dummy
|
||||
su-dummy-old.noarch 1.0-1 @System
|
||||
@@ -0,0 +1,6 @@
|
||||
Upgrades
|
||||
ca-certificates.noarch 2026.2.90_v9.0.317-1.fc44 updates
|
||||
curl.x86_64 8.18.0-12.fc44 updates
|
||||
su-dummy-a.noarch 2.0-1 su-dummy
|
||||
su-dummy-b.noarch 2.0-1 su-dummy
|
||||
su-dummy-c.x86_64 2.0-1 su-dummy
|
||||
@@ -0,0 +1,4 @@
|
||||
|
||||
Obsoleting Packages
|
||||
su-dummy-new.noarch 1.0-1 su-dummy
|
||||
su-dummy-old.noarch 1.0-1 @System
|
||||
@@ -0,0 +1,28 @@
|
||||
Installed Packages
|
||||
Name : su-dummy-a
|
||||
Version : 1.0
|
||||
Release : 1
|
||||
Architecture : noarch
|
||||
Size : 15
|
||||
Source : su-dummy-a-1.0-1.src.rpm
|
||||
Repository : @System
|
||||
Summary : skeptical-updater dummy su-dummy-a (v1.0)
|
||||
License : MIT
|
||||
Description : Dummy package su-dummy-a version 1.0 for skeptical-updater tests.
|
||||
:
|
||||
: Second paragraph of su-dummy-a 1.0 (blank line above on purpose).
|
||||
|
||||
Available Upgrades
|
||||
Name : su-dummy-a
|
||||
Version : 2.0
|
||||
Release : 1
|
||||
Architecture : noarch
|
||||
Size : 6.3 k
|
||||
Source : su-dummy-a-2.0-1.src.rpm
|
||||
Repository : su-dummy
|
||||
Summary : skeptical-updater dummy su-dummy-a (v2.0)
|
||||
License : MIT
|
||||
Description : Dummy package su-dummy-a version 2.0 for skeptical-updater tests.
|
||||
:
|
||||
: Second paragraph of su-dummy-a 2.0 (blank line above on purpose).
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
NAME="AlmaLinux"
|
||||
VERSION="9.8 (Olive Jaguar)"
|
||||
ID="almalinux"
|
||||
ID_LIKE="rhel centos fedora"
|
||||
PRETTY_NAME="AlmaLinux 9.8 (Olive Jaguar)"
|
||||
@@ -0,0 +1,4 @@
|
||||
NAME="Arch Linux"
|
||||
PRETTY_NAME="Arch Linux"
|
||||
ID=arch
|
||||
BUILD_ID=rolling
|
||||
@@ -0,0 +1,4 @@
|
||||
PRETTY_NAME="Debian GNU/Linux 12 (bookworm)"
|
||||
NAME="Debian GNU/Linux"
|
||||
VERSION_ID="12"
|
||||
ID=debian
|
||||
@@ -0,0 +1,6 @@
|
||||
NAME="openSUSE Leap"
|
||||
VERSION="16.0"
|
||||
ID="opensuse-leap"
|
||||
ID_LIKE="suse opensuse"
|
||||
VERSION_ID="16.0"
|
||||
PRETTY_NAME="openSUSE Leap 16.0"
|
||||
@@ -0,0 +1,5 @@
|
||||
NAME="openSUSE Tumbleweed"
|
||||
ID="opensuse-tumbleweed"
|
||||
ID_LIKE="opensuse suse"
|
||||
VERSION_ID="20261007"
|
||||
PRETTY_NAME="openSUSE Tumbleweed"
|
||||
@@ -0,0 +1,266 @@
|
||||
Name : su-dummy-a
|
||||
Version : 1.0-1
|
||||
Description : Skeptical-updater dummy package A
|
||||
Architecture : any
|
||||
URL : https://example.invalid/su-dummy
|
||||
Licenses : custom
|
||||
Groups : None
|
||||
Provides : None
|
||||
Depends On : su-dummy-b
|
||||
Optional Deps : None
|
||||
Required By : None
|
||||
Optional For : None
|
||||
Conflicts With : None
|
||||
Replaces : None
|
||||
Installed Size : 4.00 B
|
||||
Packager : su-fixture <su-fixture@example.invalid>
|
||||
Build Date : Tue Nov 14 22:13:20 2023
|
||||
Install Date : Fri Oct 9 08:00:00 2026
|
||||
Install Reason : Explicitly installed
|
||||
Install Script : No
|
||||
Validated By : None
|
||||
|
||||
Name : su-dummy-b
|
||||
Version : 1.0-1
|
||||
Description : Skeptical-updater dummy package B (dependency of A)
|
||||
Architecture : any
|
||||
URL : https://example.invalid/su-dummy
|
||||
Licenses : custom
|
||||
Groups : None
|
||||
Provides : None
|
||||
Depends On : None
|
||||
Optional Deps : None
|
||||
Required By : su-dummy-a
|
||||
Optional For : None
|
||||
Conflicts With : None
|
||||
Replaces : None
|
||||
Installed Size : 4.00 B
|
||||
Packager : su-fixture <su-fixture@example.invalid>
|
||||
Build Date : Tue Nov 14 22:13:20 2023
|
||||
Install Date : Fri Oct 9 08:00:00 2026
|
||||
Install Reason : Installed as a dependency for another package
|
||||
Install Script : No
|
||||
Validated By : None
|
||||
|
||||
Name : su-dummy-c
|
||||
Version : 1.0-1
|
||||
Description : Skeptical-updater dummy package C (independent)
|
||||
Architecture : any
|
||||
URL : https://example.invalid/su-dummy
|
||||
Licenses : custom
|
||||
Groups : None
|
||||
Provides : None
|
||||
Depends On : None
|
||||
Optional Deps : None
|
||||
Required By : None
|
||||
Optional For : None
|
||||
Conflicts With : None
|
||||
Replaces : None
|
||||
Installed Size : 4.00 B
|
||||
Packager : su-fixture <su-fixture@example.invalid>
|
||||
Build Date : Tue Nov 14 22:13:20 2023
|
||||
Install Date : Fri Oct 9 08:00:00 2026
|
||||
Install Reason : Explicitly installed
|
||||
Install Script : No
|
||||
Validated By : None
|
||||
|
||||
Name : bash
|
||||
Version : 5.3.20-1
|
||||
Description : The GNU Bourne Again shell
|
||||
Architecture : any
|
||||
URL : https://example.invalid/su-dummy
|
||||
Licenses : custom
|
||||
Groups : None
|
||||
Provides : sh
|
||||
Depends On : readline libreadline.so=8-64 glibc ncurses libncursesw.so=6-64
|
||||
Optional Deps : None
|
||||
Required By : None
|
||||
Optional For : None
|
||||
Conflicts With : None
|
||||
Replaces : None
|
||||
Installed Size : 4.00 B
|
||||
Packager : su-fixture <su-fixture@example.invalid>
|
||||
Build Date : Tue Nov 14 22:13:20 2023
|
||||
Install Date : Fri Oct 9 08:00:00 2026
|
||||
Install Reason : Explicitly installed
|
||||
Install Script : No
|
||||
Validated By : None
|
||||
|
||||
Name : readline
|
||||
Version : 8.3.001-1
|
||||
Description : GNU readline library
|
||||
Architecture : any
|
||||
URL : https://example.invalid/su-dummy
|
||||
Licenses : custom
|
||||
Groups : None
|
||||
Provides : libhistory.so=8-64 libreadline.so=8-64
|
||||
Depends On : glibc ncurses libncursesw.so=6-64
|
||||
Optional Deps : None
|
||||
Required By : bash
|
||||
Optional For : None
|
||||
Conflicts With : None
|
||||
Replaces : None
|
||||
Installed Size : 4.00 B
|
||||
Packager : su-fixture <su-fixture@example.invalid>
|
||||
Build Date : Tue Nov 14 22:13:20 2023
|
||||
Install Date : Fri Oct 9 08:00:00 2026
|
||||
Install Reason : Explicitly installed
|
||||
Install Script : No
|
||||
Validated By : None
|
||||
|
||||
Name : ncurses
|
||||
Version : 6.5-4
|
||||
Description : System V Release 4.0 curses emulation library
|
||||
Architecture : any
|
||||
URL : https://example.invalid/su-dummy
|
||||
Licenses : custom
|
||||
Groups : None
|
||||
Provides : libncurses++w.so=6-64 libncursesw.so=6-64
|
||||
Depends On : glibc gcc-libs
|
||||
Optional Deps : None
|
||||
Required By : bash readline
|
||||
Optional For : None
|
||||
Conflicts With : None
|
||||
Replaces : None
|
||||
Installed Size : 4.00 B
|
||||
Packager : su-fixture <su-fixture@example.invalid>
|
||||
Build Date : Tue Nov 14 22:13:20 2023
|
||||
Install Date : Fri Oct 9 08:00:00 2026
|
||||
Install Reason : Explicitly installed
|
||||
Install Script : No
|
||||
Validated By : None
|
||||
|
||||
Name : gcc-libs
|
||||
Version : 15.2.1-1
|
||||
Description : Runtime libraries shipped by GCC
|
||||
Architecture : any
|
||||
URL : https://example.invalid/su-dummy
|
||||
Licenses : custom
|
||||
Groups : None
|
||||
Provides : None
|
||||
Depends On : glibc>=2.27
|
||||
Optional Deps : None
|
||||
Required By : ncurses
|
||||
Optional For : None
|
||||
Conflicts With : None
|
||||
Replaces : None
|
||||
Installed Size : 4.00 B
|
||||
Packager : su-fixture <su-fixture@example.invalid>
|
||||
Build Date : Tue Nov 14 22:13:20 2023
|
||||
Install Date : Fri Oct 9 08:00:00 2026
|
||||
Install Reason : Explicitly installed
|
||||
Install Script : No
|
||||
Validated By : None
|
||||
|
||||
Name : glibc
|
||||
Version : 2.42-2
|
||||
Description : GNU C Library
|
||||
Architecture : x86_64
|
||||
URL : https://www.gnu.org/software/libc
|
||||
Licenses : GPL-2.0-or-later LGPL-2.1-or-later
|
||||
Groups : None
|
||||
Provides : None
|
||||
Depends On : linux-api-headers>=4.10 tzdata filesystem
|
||||
Optional Deps : gd: for memusagestat
|
||||
perl: for mtrace
|
||||
Required By : bash gcc-libs ncurses
|
||||
readline
|
||||
Optional For : None
|
||||
Conflicts With : None
|
||||
Replaces : None
|
||||
Installed Size : 48.71 MiB
|
||||
Packager : Frederik Schwan <freswa@archlinux.org>
|
||||
Build Date : Sun Sep 7 10:00:00 2026
|
||||
Install Date : Fri Oct 9 08:00:00 2026
|
||||
Install Reason : Installed as a dependency for another package
|
||||
Install Script : Yes
|
||||
Validated By : Signature
|
||||
|
||||
Name : linux-api-headers
|
||||
Version : 6.16-1
|
||||
Description : Kernel headers sanitized for use in userspace
|
||||
Architecture : any
|
||||
URL : https://example.invalid/su-dummy
|
||||
Licenses : custom
|
||||
Groups : None
|
||||
Provides : None
|
||||
Depends On : None
|
||||
Optional Deps : None
|
||||
Required By : glibc
|
||||
Optional For : None
|
||||
Conflicts With : None
|
||||
Replaces : None
|
||||
Installed Size : 4.00 B
|
||||
Packager : su-fixture <su-fixture@example.invalid>
|
||||
Build Date : Tue Nov 14 22:13:20 2023
|
||||
Install Date : Fri Oct 9 08:00:00 2026
|
||||
Install Reason : Explicitly installed
|
||||
Install Script : No
|
||||
Validated By : None
|
||||
|
||||
Name : tzdata
|
||||
Version : 2026a-1
|
||||
Description : Sources for time zone and daylight saving time data
|
||||
Architecture : any
|
||||
URL : https://example.invalid/su-dummy
|
||||
Licenses : custom
|
||||
Groups : None
|
||||
Provides : None
|
||||
Depends On : None
|
||||
Optional Deps : None
|
||||
Required By : glibc
|
||||
Optional For : None
|
||||
Conflicts With : None
|
||||
Replaces : None
|
||||
Installed Size : 4.00 B
|
||||
Packager : su-fixture <su-fixture@example.invalid>
|
||||
Build Date : Tue Nov 14 22:13:20 2023
|
||||
Install Date : Fri Oct 9 08:00:00 2026
|
||||
Install Reason : Explicitly installed
|
||||
Install Script : No
|
||||
Validated By : None
|
||||
|
||||
Name : filesystem
|
||||
Version : 2025.10.12-1
|
||||
Description : Base Arch Linux files
|
||||
Architecture : any
|
||||
URL : https://example.invalid/su-dummy
|
||||
Licenses : custom
|
||||
Groups : None
|
||||
Provides : None
|
||||
Depends On : iana-etc
|
||||
Optional Deps : None
|
||||
Required By : glibc
|
||||
Optional For : None
|
||||
Conflicts With : None
|
||||
Replaces : None
|
||||
Installed Size : 4.00 B
|
||||
Packager : su-fixture <su-fixture@example.invalid>
|
||||
Build Date : Tue Nov 14 22:13:20 2023
|
||||
Install Date : Fri Oct 9 08:00:00 2026
|
||||
Install Reason : Explicitly installed
|
||||
Install Script : No
|
||||
Validated By : None
|
||||
|
||||
Name : iana-etc
|
||||
Version : 20260920-1
|
||||
Description : /etc/protocols and /etc/services provided by IANA
|
||||
Architecture : any
|
||||
URL : https://example.invalid/su-dummy
|
||||
Licenses : custom
|
||||
Groups : None
|
||||
Provides : None
|
||||
Depends On : None
|
||||
Optional Deps : None
|
||||
Required By : filesystem
|
||||
Optional For : None
|
||||
Conflicts With : None
|
||||
Replaces : None
|
||||
Installed Size : 4.00 B
|
||||
Packager : su-fixture <su-fixture@example.invalid>
|
||||
Build Date : Tue Nov 14 22:13:20 2023
|
||||
Install Date : Fri Oct 9 08:00:00 2026
|
||||
Install Reason : Explicitly installed
|
||||
Install Script : No
|
||||
Validated By : None
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
su-dummy-a 1.0-1 -> 2.0-1
|
||||
su-dummy-b 1.0-1 -> 2.0-1
|
||||
su-dummy-c 1.0-1 -> 2.0-1
|
||||
@@ -0,0 +1,6 @@
|
||||
ca-certificates-mozilla 3.130-1 -> 3.131-1
|
||||
expat 2.8.5-1 -> 2.9.0-1 [ignored]
|
||||
glib2 2.88.3-1 -> 2.90.1-1 [ignored]
|
||||
su-dummy-a 1.0-1 -> 2.0-1
|
||||
su-dummy-b 1.0-1 -> 2.0-1
|
||||
su-dummy-c 1.0-1 -> 2.0-1
|
||||
@@ -0,0 +1,95 @@
|
||||
Repository : su-dummy
|
||||
Name : su-dummy-a
|
||||
Version : 2.0-1
|
||||
Description : Skeptical-updater dummy package A
|
||||
Architecture : any
|
||||
URL : https://example.invalid/su-dummy
|
||||
Licenses : custom
|
||||
Groups : None
|
||||
Provides : None
|
||||
Depends On : su-dummy-b
|
||||
Optional Deps : None
|
||||
Conflicts With : None
|
||||
Replaces : None
|
||||
Download Size : 380.00 B
|
||||
Installed Size : 4.00 B
|
||||
Packager : su-fixture <su-fixture@example.invalid>
|
||||
Build Date : Tue Nov 14 22:13:20 2023
|
||||
Validated By : None
|
||||
|
||||
Repository : su-dummy
|
||||
Name : su-dummy-b
|
||||
Version : 2.0-1
|
||||
Description : Skeptical-updater dummy package B (dependency of A)
|
||||
Architecture : any
|
||||
URL : https://example.invalid/su-dummy
|
||||
Licenses : custom
|
||||
Groups : None
|
||||
Provides : None
|
||||
Depends On : None
|
||||
Optional Deps : None
|
||||
Conflicts With : None
|
||||
Replaces : None
|
||||
Download Size : 380.00 B
|
||||
Installed Size : 4.00 B
|
||||
Packager : su-fixture <su-fixture@example.invalid>
|
||||
Build Date : Tue Nov 14 22:13:20 2023
|
||||
Validated By : None
|
||||
|
||||
Repository : su-dummy
|
||||
Name : su-dummy-c
|
||||
Version : 2.0-1
|
||||
Description : Skeptical-updater dummy package C (independent)
|
||||
Architecture : any
|
||||
URL : https://example.invalid/su-dummy
|
||||
Licenses : custom
|
||||
Groups : None
|
||||
Provides : None
|
||||
Depends On : None
|
||||
Optional Deps : None
|
||||
Conflicts With : None
|
||||
Replaces : None
|
||||
Download Size : 380.00 B
|
||||
Installed Size : 4.00 B
|
||||
Packager : su-fixture <su-fixture@example.invalid>
|
||||
Build Date : Tue Nov 14 22:13:20 2023
|
||||
Validated By : None
|
||||
|
||||
Repository : su-dummy
|
||||
Name : su-dummy-new
|
||||
Version : 1.0-1
|
||||
Description : Skeptical-updater dummy package NEW
|
||||
Architecture : any
|
||||
URL : https://example.invalid/su-dummy
|
||||
Licenses : custom
|
||||
Groups : None
|
||||
Provides : None
|
||||
Depends On : None
|
||||
Optional Deps : None
|
||||
Conflicts With : None
|
||||
Replaces : None
|
||||
Download Size : 380.00 B
|
||||
Installed Size : 4.00 B
|
||||
Packager : su-fixture <su-fixture@example.invalid>
|
||||
Build Date : Tue Nov 14 22:13:20 2023
|
||||
Validated By : None
|
||||
|
||||
Repository : su-dummy2
|
||||
Name : su-dummy-b
|
||||
Version : 3.0-1
|
||||
Description : Skeptical-updater dummy package B (dependency of A)
|
||||
Architecture : any
|
||||
URL : https://example.invalid/su-dummy
|
||||
Licenses : custom
|
||||
Groups : None
|
||||
Provides : None
|
||||
Depends On : None
|
||||
Optional Deps : None
|
||||
Conflicts With : None
|
||||
Replaces : None
|
||||
Download Size : 380.00 B
|
||||
Installed Size : 4.00 B
|
||||
Packager : su-fixture <su-fixture@example.invalid>
|
||||
Build Date : Tue Nov 14 22:13:20 2023
|
||||
Validated By : None
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
core bash 5.3.20-1 [installed]
|
||||
core ca-certificates-mozilla 3.131-1 [installed: 3.130-1]
|
||||
core expat 2.9.0-1 [installed: 2.8.5-1]
|
||||
core glib2 2.90.1-1 [installed: 2.88.3-1]
|
||||
extra pacman-contrib 1.13.1-1
|
||||
su-dummy su-dummy-a 2.0-1 [installed: 1.0-1]
|
||||
su-dummy su-dummy-b 2.0-1 [installed: 1.0-1]
|
||||
su-dummy su-dummy-c 2.0-1 [installed: 1.0-1]
|
||||
su-dummy2 su-dummy-b 3.0-1
|
||||
@@ -0,0 +1,61 @@
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
# Copyright (C) 2026 Bryan Joshua Pedini
|
||||
|
||||
# tests/unit/data/rpm/db.bash - the fake rpm database (sourced by fakes/rpm/rpm).
|
||||
# Shaped after discovery/rpm.md sections 4-5 (EL9): the fixture packages incl. the
|
||||
# SU_FIXTURE_EXTRAS su-dummy-d (file dep, versioned dep, rich dep, rpmlib()), plus a small
|
||||
# glibc/bash base (REQUIRENAME values carry no version, like rpm's; rich deps are whole) whose glibc carries the "(glibc-gconv-extra ... if redhat-rpm-config)" rich dep.
|
||||
# shellcheck shell=bash disable=SC2034
|
||||
declare -A ARCH=(
|
||||
[su-dummy-a]=noarch [su-dummy-b]=noarch [su-dummy-c]=x86_64 [su-dummy-d]=noarch
|
||||
[bash]=x86_64 [glibc]=x86_64 [glibc-gconv-extra]=x86_64 [glibc-common]=x86_64
|
||||
[basesystem]=noarch [setup]=noarch [filesystem]=x86_64 [ncurses-libs]=x86_64
|
||||
[redhat-rpm-config]=noarch [rpm]=x86_64 [gnutls]=x86_64 [vim-minimal]=x86_64
|
||||
[kernel-core]=x86_64 [ca-certificates]=noarch [curl]=x86_64
|
||||
)
|
||||
# installed versions ("epoch:version-release" or "version-release"); kernel-core is installonly (two versions)
|
||||
declare -A EVR=(
|
||||
[su-dummy-a]=1.0-1 [su-dummy-b]=1.0-1 [su-dummy-c]=1.0-1 [su-dummy-d]=1.0-1
|
||||
[bash]=5.1.8-9.el9 [glibc]=2.34-275.el9_8 [glibc-gconv-extra]=2.34-275.el9_8 [glibc-common]=2.34-275.el9_8
|
||||
[basesystem]=11-13.el9 [setup]=2.13.7-10.el9 [filesystem]=3.16-5.el9 [ncurses-libs]=6.2-12.20210508.el9
|
||||
[redhat-rpm-config]=210-1.el9 [rpm]=4.16.1.3-40.el9 [gnutls]=3.8.10-4.el9_8 [vim-minimal]=2:8.2.2637-26.el9_8.21
|
||||
[kernel-core]='5.14.0-570.1.el9 5.14.0-611.5.el9' [ca-certificates]=2025.2.80_v9.0.305-91.el9 [curl]=8.18.0-1.fc44
|
||||
)
|
||||
declare -A SUMMARY=(
|
||||
[su-dummy-a]='skeptical-updater dummy su-dummy-a (v1.0)' [su-dummy-b]='skeptical-updater dummy su-dummy-b (v1.0)'
|
||||
[su-dummy-c]='skeptical-updater dummy su-dummy-c (v1.0)' [su-dummy-d]='skeptical-updater dummy su-dummy-d (v1.0)'
|
||||
[bash]='The GNU Bourne Again shell' [glibc]='The GNU libc libraries'
|
||||
)
|
||||
declare -A REQUIRES=(
|
||||
[su-dummy-a]=$'su-dummy-b\nrpmlib(CompressedFileNames)\nrpmlib(FileDigests)'
|
||||
[su-dummy-b]=$'rpmlib(CompressedFileNames)\nrpmlib(FileDigests)'
|
||||
[su-dummy-c]=$'rpmlib(CompressedFileNames)'
|
||||
[su-dummy-d]=$'su-dummy-a\n/bin/sh\nsu-dummy-b\n(su-dummy-c if su-dummy-b)\nrpmlib(CompressedFileNames)'
|
||||
[bash]=$'/usr/bin/sh\nfilesystem\nlibc.so.6()(64bit)\nlibc.so.6(GLIBC_2.34)(64bit)\nlibtinfo.so.6()(64bit)\nrtld(GNU_HASH)'
|
||||
[glibc]=$'(glibc-gconv-extra(x86-64) = 2.34-275.el9_8 if redhat-rpm-config)\nbasesystem\nglibc-common'
|
||||
[glibc-gconv-extra]=$'glibc(x86-64)'
|
||||
[glibc-common]=$'/usr/bin/sh\nglibc'
|
||||
[basesystem]=$'filesystem\nsetup'
|
||||
[setup]=$'system-release'
|
||||
[filesystem]=$'/bin/sh\nsetup'
|
||||
[ncurses-libs]=$'libc.so.6()(64bit)\nrtld(GNU_HASH)'
|
||||
[redhat-rpm-config]=$'rpm\n/usr/bin/sh'
|
||||
[rpm]=$'/usr/bin/sh\nlibc.so.6()(64bit)'
|
||||
)
|
||||
declare -A PROVIDES=(
|
||||
[su-dummy-a]=su-dummy-a [su-dummy-b]=su-dummy-b [su-dummy-c]=$'su-dummy-c\nsu-dummy-c(x86-64)' [su-dummy-d]=su-dummy-d
|
||||
[bash]=$'bash\nbash(x86-64)\nconfig(bash)'
|
||||
[glibc]=$'glibc\nglibc(x86-64)\nlibc.so.6()(64bit)\nlibc.so.6(GLIBC_2.34)(64bit)\nrtld(GNU_HASH)'
|
||||
[glibc-gconv-extra]=$'glibc-gconv-extra\nglibc-gconv-extra(x86-64)'
|
||||
[glibc-common]=glibc-common [basesystem]=basesystem [setup]=setup [filesystem]=filesystem
|
||||
[ncurses-libs]=$'ncurses-libs\nlibtinfo.so.6()(64bit)'
|
||||
[redhat-rpm-config]=redhat-rpm-config [rpm]=rpm
|
||||
)
|
||||
# FAKE_RPM_NO_D=1: su-dummy-d (which requires every dummy) is not installed
|
||||
declare -A FILES=(
|
||||
[su-dummy-a]=/usr/share/su-dummy/su-dummy-a [su-dummy-b]=/usr/share/su-dummy/su-dummy-b
|
||||
[su-dummy-c]=/usr/share/su-dummy/su-dummy-c [su-dummy-d]=/usr/share/su-dummy/su-dummy-d
|
||||
[bash]=$'/bin/sh\n/usr/bin/bash\n/usr/bin/sh'
|
||||
[glibc]=/usr/lib64/libc.so.6
|
||||
)
|
||||
if [[ -n ${FAKE_RPM_NO_D:-} ]]; then unset 'EVR[su-dummy-d]'; fi
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version='1.0'?>
|
||||
<stream>
|
||||
<message type="info">Loading repository data...</message>
|
||||
<message type="info">Computing distribution upgrade...</message>
|
||||
<install-summary download-size="19047" space-usage-diff="0" packages-to-change="4" need-restart="0" need-reboot="0">
|
||||
</install-summary>
|
||||
<prompt id="1" type="0">Continue?</prompt>
|
||||
</stream>
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version='1.0'?>
|
||||
<stream>
|
||||
<message type="info">Loading repository data...</message>
|
||||
<message type="info">Computing distribution upgrade...</message>
|
||||
<install-summary download-size="19047" space-usage-diff="0" packages-to-change="4" need-restart="0" need-reboot="0">
|
||||
<to-upgrade>
|
||||
<solvable type="package" name="su-dummy-a" edition="2.0-1" arch="noarch" repository="su-dummy" edition-old="1.0-1" arch-old="noarch" summary="s su-dummy-a"><description>d
|
||||
|
||||
more</description></solvable><solvable type="package" name="su-dummy-b" edition="2.0-1" arch="noarch" repository="su-dummy" edition-old="1.0-1" arch-old="noarch" summary="s su-dummy-b"><description>d
|
||||
|
||||
more</description></solvable><solvable type="package" name="su-dummy-c" edition="2.0-1" arch="noarch" repository="su-dummy" edition-old="1.0-1" arch-old="noarch" summary="s su-dummy-c"><description>d
|
||||
|
||||
more</description></solvable><solvable type="product" name="openSUSE-release" edition="20261008-1" arch="noarch" repository="su-dummy" edition-old="20261007-1" arch-old="noarch" summary="s openSUSE-release"><description>d
|
||||
|
||||
more</description></solvable></to-upgrade>
|
||||
</install-summary>
|
||||
<prompt id="1" type="0">Continue?</prompt>
|
||||
</stream>
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version='1.0'?>
|
||||
<stream>
|
||||
<message type="info">Loading repository data...</message>
|
||||
<message type="info">Computing distribution upgrade...</message>
|
||||
<install-summary download-size="19047" space-usage-diff="0" packages-to-change="4" need-restart="0" need-reboot="0">
|
||||
<to-remove>
|
||||
<solvable type="package" name="libcurl-mini4" edition="8.16.0-1" arch="noarch" repository="@System" summary="s libcurl-mini4"><description>d
|
||||
|
||||
more</description></solvable></to-remove>
|
||||
<to-install>
|
||||
</to-install>
|
||||
</install-summary>
|
||||
<prompt id="1" type="0">Continue?</prompt>
|
||||
</stream>
|
||||
@@ -0,0 +1,20 @@
|
||||
<?xml version='1.0'?>
|
||||
<stream>
|
||||
<message type="info">Loading repository data...</message>
|
||||
<message type="info">Computing distribution upgrade...</message>
|
||||
<install-summary download-size="19047" space-usage-diff="0" packages-to-change="4" need-restart="0" need-reboot="0">
|
||||
<to-upgrade>
|
||||
<solvable type="package" name="su-dummy-a" edition="2.0-1" arch="noarch" repository="su-dummy" edition-old="1.0-1" arch-old="noarch" summary="s su-dummy-a"><description>d
|
||||
|
||||
more</description></solvable></to-upgrade>
|
||||
<to-install>
|
||||
<solvable type="package" name="su-dummy-newdep" edition="1.0-1" arch="noarch" repository="su-dummy" summary="s su-dummy-newdep"><description>d
|
||||
|
||||
more</description></solvable></to-install>
|
||||
<to-remove>
|
||||
<solvable type="package" name="libcurl-mini4" edition="8.16.0-1" arch="noarch" repository="@System" summary="s libcurl-mini4"><description>d
|
||||
|
||||
more</description></solvable></to-remove>
|
||||
</install-summary>
|
||||
<prompt id="1" type="0">Continue?</prompt>
|
||||
</stream>
|
||||
@@ -0,0 +1,18 @@
|
||||
|
||||
Information for package su-dummy-a:
|
||||
-----------------------------------
|
||||
Repository : su-dummy
|
||||
Name : su-dummy-a
|
||||
Version : 2.0-1
|
||||
Arch : noarch
|
||||
Vendor :
|
||||
Installed Size : 15 B
|
||||
Installed : Yes
|
||||
Status : out-of-date (version 1.0-1 installed)
|
||||
Source package : su-dummy-a-2.0-1.src
|
||||
Summary : skeptical-updater dummy su-dummy-a (v2.0)
|
||||
Description :
|
||||
Dummy package su-dummy-a version 2.0 for skeptical-updater tests.
|
||||
|
||||
Second paragraph of su-dummy-a 2.0 (blank line above on purpose).
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<?xml version='1.0'?>
|
||||
<stream>
|
||||
<message type="info">Loading repository data...</message>
|
||||
<message type="info">Reading installed packages...</message>
|
||||
<update-status version="0.6">
|
||||
<update-list>
|
||||
<update kind="package" name="su-dummy-a" edition="2.0-1" arch="noarch" edition-old="1.0-1"><summary>skeptical-updater dummy su-dummy-a (v2.0)</summary><description>Dummy package su-dummy-a version 2.0 for skeptical-updater tests.
|
||||
|
||||
Second paragraph of su-dummy-a 2.0 (blank line above on purpose).</description><license/><source url="file:/var/lib/su-dummy-repo" alias="su-dummy"/></update><update kind="package" name="su-dummy-b" edition="2.0-1" arch="noarch" edition-old="1.0-1"><summary>skeptical-updater dummy su-dummy-b (v2.0)</summary><description>Dummy package su-dummy-b version 2.0 for skeptical-updater tests.
|
||||
|
||||
Second paragraph of su-dummy-b 2.0 (blank line above on purpose).</description><license/><source url="file:/var/lib/su-dummy-repo" alias="su-dummy"/></update><update kind="package" name="su-dummy-c" edition="2.0-1" arch="x86_64" edition-old="1.0-1"><summary>skeptical-updater dummy su-dummy-c (v2.0)</summary><description>Dummy package su-dummy-c version 2.0 for skeptical-updater tests.
|
||||
|
||||
Second paragraph of su-dummy-c 2.0 (blank line above on purpose).</description><license/><source url="file:/var/lib/su-dummy-repo" alias="su-dummy"/></update></update-list>
|
||||
</update-status>
|
||||
</stream>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version='1.0'?>
|
||||
<stream>
|
||||
<message type="info">Loading repository data...</message>
|
||||
<message type="info">Reading installed packages...</message>
|
||||
<update-status version="0.6">
|
||||
<update-list>
|
||||
</update-list>
|
||||
</update-status>
|
||||
</stream>
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version='1.0'?>
|
||||
<stream>
|
||||
<message type="info">Refreshing service 'openSUSE'.</message>
|
||||
<message type="info">Loading repository data...</message>
|
||||
<message type="info">Reading installed packages...</message>
|
||||
<update-status version="0.6">
|
||||
<update-list>
|
||||
<update kind="package" name="openSUSE-build-key" edition="1.0-69.1" arch="x86_64" edition-old="1.0-68.1"><summary>The public gpg keys for rpm package signature verification</summary><description>This package contains the gpg keys that are used to sign the
|
||||
openSUSE distribution.</description><license/><source url="http://download.opensuse.org/update/tumbleweed/" alias="repo-update"/></update><update kind="package" name="su-dummy-a" edition="2.0-1" arch="noarch" edition-old="1.0-1"><summary>skeptical-updater dummy su-dummy-a (v2.0)</summary><description>Dummy package su-dummy-a version 2.0 for skeptical-updater tests.
|
||||
|
||||
Second paragraph of su-dummy-a 2.0 (blank line above on purpose).</description><license/><source url="file:/var/lib/su-dummy-repo" alias="su-dummy"/></update><update kind="package" name="su-dummy-b" edition="2.0-1" arch="noarch" edition-old="1.0-1"><summary>skeptical-updater dummy su-dummy-b (v2.0)</summary><description>Dummy package su-dummy-b version 2.0 for skeptical-updater tests.
|
||||
|
||||
Second paragraph of su-dummy-b 2.0 (blank line above on purpose).</description><license/><source url="file:/var/lib/su-dummy-repo" alias="openSUSE:repo-oss & co"/></update><update kind="package" name="su-dummy-c" edition="2.0-1" arch="x86_64" edition-old="1.0-1"><summary>skeptical-updater dummy su-dummy-c (v2.0)</summary><description>Dummy package su-dummy-c version 2.0 for skeptical-updater tests.
|
||||
|
||||
Second paragraph of su-dummy-c 2.0 (blank line above on purpose).</description><license/><source url="file:/var/lib/su-dummy-repo" alias="su-dummy"/></update></update-list>
|
||||
</update-status>
|
||||
</stream>
|
||||
@@ -0,0 +1,135 @@
|
||||
#!/usr/bin/env bats
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
# Copyright (C) 2026 Bryan Joshua Pedini
|
||||
|
||||
# Backend detection (pacman > apt > dnf (else yum) > zypper), --backend override, labels, banner.
|
||||
|
||||
# each @test runs in its own subshell: per-test exports are intended
|
||||
# shellcheck disable=SC2030,SC2031
|
||||
|
||||
load test_helper
|
||||
|
||||
setup() { common_setup; }
|
||||
|
||||
@test "pacman wins over apt, dnf and zypper" {
|
||||
use_fakes pacman apt dnf zypper rpm
|
||||
export SKEPTICAL_UPDATER_OS_RELEASE=$FAKE_DATA/os-release-arch
|
||||
run skup list --no-refresh
|
||||
assert_success
|
||||
assert_line --index 0 'Backend: pacman (Arch Linux)'
|
||||
log_has 'pacman -Qu'
|
||||
log_lacks '^(apt-get|dnf|zypper) '
|
||||
}
|
||||
|
||||
@test "apt wins over dnf and zypper" {
|
||||
use_fakes zypper dnf apt rpm
|
||||
run skup list --no-refresh
|
||||
assert_success
|
||||
assert_line --index 0 'Backend: apt (Debian GNU/Linux 12 (bookworm))'
|
||||
log_has 'apt-get -s --with-new-pkgs upgrade'
|
||||
}
|
||||
|
||||
@test "dnf wins over zypper" {
|
||||
use_fakes zypper dnf rpm
|
||||
export SKEPTICAL_UPDATER_OS_RELEASE=$FAKE_DATA/os-release-alma9
|
||||
run skup list --no-refresh
|
||||
assert_success
|
||||
assert_line --index 0 'Backend: dnf (AlmaLinux 9.8 (Olive Jaguar))'
|
||||
log_has 'dnf -q check-update --setopt=skip_if_unavailable=0'
|
||||
}
|
||||
|
||||
@test "yum is used when dnf is missing" {
|
||||
use_fakes zypper yum rpm
|
||||
run skup list --no-refresh
|
||||
assert_success
|
||||
assert_line --index 0 'Backend: yum (Debian GNU/Linux 12 (bookworm))'
|
||||
log_has 'yum -q check-update --setopt=skip_if_unavailable=0'
|
||||
}
|
||||
|
||||
@test "zypper comes last" {
|
||||
use_fakes zypper rpm
|
||||
export SKEPTICAL_UPDATER_OS_RELEASE=$FAKE_DATA/os-release-tumbleweed
|
||||
run skup list --no-refresh
|
||||
assert_success
|
||||
assert_line --index 0 'Backend: zypper (openSUSE Tumbleweed)'
|
||||
log_has 'zypper --non-interactive --no-refresh --xmlout dist-upgrade --dry-run'
|
||||
}
|
||||
|
||||
@test "pman in PATH: label pman, the commands stay pacman" {
|
||||
use_fakes pman pacman
|
||||
export SKEPTICAL_UPDATER_OS_RELEASE=$FAKE_DATA/os-release-arch
|
||||
run skup list
|
||||
assert_success
|
||||
assert_line --index 0 'Backend: pman (Arch Linux)'
|
||||
log_matches '^pacman -Sy --dbpath /tmp/skeptical-updater-db\.[0-9]+\.[A-Za-z0-9]+ --logfile /dev/null$'
|
||||
log_matches '^pacman --dbpath /tmp/skeptical-updater-db\.[0-9]+\.[A-Za-z0-9]+ -Qu$'
|
||||
}
|
||||
|
||||
@test "--backend overrides the detection" {
|
||||
use_fakes pacman apt
|
||||
run skup --backend=apt list --no-refresh
|
||||
assert_success
|
||||
assert_line --index 0 'Backend: apt (Debian GNU/Linux 12 (bookworm))'
|
||||
log_lacks '^pacman '
|
||||
}
|
||||
|
||||
@test "--backend=pman and --backend=yum are aliases of pacman and dnf" {
|
||||
use_fakes pacman apt dnf rpm
|
||||
run skup --backend=pman list --no-refresh
|
||||
assert_success
|
||||
assert_line --index 0 --partial 'Backend: pacman'
|
||||
run skup --backend=yum list --no-refresh
|
||||
assert_success
|
||||
assert_line --index 0 --partial 'Backend: dnf'
|
||||
}
|
||||
|
||||
@test "the statefile backend column is the backend, not the label" {
|
||||
use_fakes pman pacman
|
||||
run skup trust su-dummy-c
|
||||
assert_success
|
||||
run cat "$STATE_FILE"
|
||||
assert_line --regexp "^pacman${T}su-dummy-c${T}user${T}${DATE_RE}\$"
|
||||
}
|
||||
|
||||
@test "no supported package manager: exit 1" {
|
||||
use_fakes
|
||||
run skup list --no-refresh
|
||||
assert_failure 1
|
||||
assert_output --partial 'no supported package manager found in PATH (pacman, apt-get, dnf/yum, zypper)'
|
||||
}
|
||||
|
||||
@test "a forced backend whose tool is missing: exit 1" {
|
||||
use_fakes apt
|
||||
run skup --backend=zypper list --no-refresh
|
||||
assert_failure 1
|
||||
assert_output --partial 'zypper backend: zypper not found in PATH'
|
||||
run skup --backend=pacman list --no-refresh
|
||||
assert_failure 1
|
||||
assert_output --partial 'pacman backend: pacman not found in PATH'
|
||||
run skup --backend=dnf list --no-refresh
|
||||
assert_failure 1
|
||||
assert_output --partial 'dnf backend: neither dnf nor yum found in PATH'
|
||||
}
|
||||
|
||||
@test "the banner uses PRETTY_NAME, quoted or not" {
|
||||
printf "NAME=x\nPRETTY_NAME='Single Quoted 1.0'\n" >"$BATS_TEST_TMPDIR/osr"
|
||||
SKEPTICAL_UPDATER_OS_RELEASE=$BATS_TEST_TMPDIR/osr run skup list --no-refresh
|
||||
assert_line --index 0 'Backend: apt (Single Quoted 1.0)'
|
||||
printf 'PRETTY_NAME=Plain\n' >"$BATS_TEST_TMPDIR/osr"
|
||||
SKEPTICAL_UPDATER_OS_RELEASE=$BATS_TEST_TMPDIR/osr run skup list --no-refresh
|
||||
assert_line --index 0 'Backend: apt (Plain)'
|
||||
}
|
||||
|
||||
@test "the banner without os-release shows just the backend" {
|
||||
SKEPTICAL_UPDATER_OS_RELEASE=$BATS_TEST_TMPDIR/missing run skup list --no-refresh
|
||||
assert_success
|
||||
assert_line --index 0 'Backend: apt'
|
||||
}
|
||||
|
||||
@test "trusted needs no package manager at all" {
|
||||
use_fakes
|
||||
write_state "$HEADER" "apt${T}x${T}user${T}2026-10-09T09:40:12Z"
|
||||
run skup trusted
|
||||
assert_success
|
||||
assert_line "apt${T}x${T}user${T}2026-10-09T09:40:12Z"
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
#!/usr/bin/env bats
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
# Copyright (C) 2026 Bryan Joshua Pedini
|
||||
|
||||
# --dry-run: mutating commands (upgrade, purge, hold/unhold, lock files, statefile writes)
|
||||
# are printed as "+ <cmd>" instead of executed; read-only queries and (as root) the
|
||||
# metadata refresh still run.
|
||||
|
||||
load test_helper
|
||||
|
||||
setup() { common_setup; }
|
||||
|
||||
APT_UPGRADE='apt-get -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold -o DPkg::Lock::Timeout=60 --with-new-pkgs upgrade'
|
||||
|
||||
@test "apt: refresh runs, hold/upgrade/unhold are printed, nothing mutating is executed" {
|
||||
run skup --dry-run <<<$'2-\n'
|
||||
assert_success
|
||||
assert_line 'Refreshing package lists... done.'
|
||||
log_has 'apt-get update -qq -o APT::Update::Error-Mode=any'
|
||||
assert_line '+ apt-mark hold su-dummy-b'
|
||||
assert_line "+ $APT_UPGRADE"
|
||||
assert_line '+ apt-mark unhold su-dummy-b'
|
||||
assert_line 'Dry run: nothing was changed.'
|
||||
log_lacks '^apt-get -y'
|
||||
log_lacks '^apt-mark (un)?hold'
|
||||
run cat "$FAKE_DIR/holds"
|
||||
assert_output ''
|
||||
[[ ! -e $STATE ]]
|
||||
}
|
||||
|
||||
@test "apt: a package the admin already held is not printed as held" {
|
||||
echo su-dummy-c >"$FAKE_DIR/holds"
|
||||
FAKE_APT_SIM=held run skup --dry-run --no-refresh <<<$'3-\n'
|
||||
assert_success
|
||||
refute_line --partial 'apt-mark hold'
|
||||
refute_line --partial 'apt-mark unhold'
|
||||
assert_line "+ $APT_UPGRADE"
|
||||
}
|
||||
|
||||
@test "N+ prints the statefile changes and writes nothing; the rows still leave the list" {
|
||||
run skup --dry-run --no-refresh <<<$'1+\nl\nq'
|
||||
assert_success
|
||||
assert_line "+ mkdir -p -m 0755 $STATE"
|
||||
assert_line "+ $STATE_FILE add: apt su-dummy-a user"
|
||||
assert_line "+ $STATE_FILE add: apt su-dummy-b dep:su-dummy-a"
|
||||
assert_line '3 upgradable, 2 trusted (hidden, use --all to show), 1 to review:'
|
||||
[[ ! -e $STATE ]]
|
||||
}
|
||||
|
||||
@test "Nx prints the purge commands and drops the row" {
|
||||
run skup --dry-run --no-refresh <<<$'3x\ny\nl\nq'
|
||||
assert_success
|
||||
assert_line '+ apt-get -y purge su-dummy-c'
|
||||
refute_output --partial 'autoremove'
|
||||
assert_line '2 upgradable, 0 trusted (hidden, use --all to show), 2 to review:'
|
||||
log_lacks '^apt-get -y'
|
||||
}
|
||||
|
||||
@test "trust, untrust and refresh print their statefile changes and write nothing" {
|
||||
write_state "$HEADER" "apt${T}su-dummy-c${T}user${T}2026-01-01T00:00:00Z"
|
||||
before=$(cat "$STATE_FILE")
|
||||
run skup --dry-run trust su-dummy-a
|
||||
assert_success
|
||||
assert_line 'trusted su-dummy-a (+1 dependency: su-dummy-b)'
|
||||
assert_line "+ $STATE_FILE add: apt su-dummy-a user"
|
||||
assert_line "+ $STATE_FILE add: apt su-dummy-b dep:su-dummy-a"
|
||||
run skup --dry-run untrust su-dummy-c
|
||||
assert_success
|
||||
assert_line "+ $STATE_FILE remove: apt su-dummy-c user"
|
||||
write_state "$HEADER" "apt${T}su-dummy-w${T}user${T}2026-01-01T00:00:00Z"
|
||||
before=$(cat "$STATE_FILE")
|
||||
run skup --dry-run refresh
|
||||
assert_success
|
||||
assert_line "+ $STATE_FILE add: apt su-dummy-v dep:su-dummy-w"
|
||||
[[ $(cat "$STATE_FILE") == "$before" ]]
|
||||
}
|
||||
|
||||
@test "pacman: -Su --ignore and -Rns are printed" {
|
||||
use_fakes pacman
|
||||
run skup --dry-run --no-refresh <<<$'3x\ny\n2-\n'
|
||||
assert_success
|
||||
assert_line '+ pacman -Rns --noconfirm -- su-dummy-c'
|
||||
assert_line '+ pacman -Su --noconfirm --ignore su-dummy-b'
|
||||
log_lacks '^pacman -(Su|Syu|Rns) '
|
||||
}
|
||||
|
||||
@test "dnf: upgrade --exclude and remove are printed" {
|
||||
use_fakes dnf rpm
|
||||
export FAKE_RPM_NO_D=1
|
||||
run skup --dry-run --no-refresh <<<$'5x\ny\n4-\n'
|
||||
assert_success
|
||||
assert_line '+ dnf -y remove su-dummy-c'
|
||||
assert_line '+ dnf -y --setopt=skip_if_unavailable=0 --setopt=\*.metadata_expire=-1 upgrade --exclude=su-dummy-b'
|
||||
log_lacks '^dnf -y'
|
||||
}
|
||||
|
||||
@test "zypper: the private-lock commands are printed, no lock file is written" {
|
||||
use_fakes zypper rpm
|
||||
export SKEPTICAL_UPDATER_OS_RELEASE=$FAKE_DATA/os-release-leap
|
||||
mkdir -p "$BATS_TEST_TMPDIR/etc"
|
||||
printf '[main]\nlocksfile.path = %s\n' "$BATS_TEST_TMPDIR/etc/locks" >"$BATS_TEST_TMPDIR/etc/zypp.conf"
|
||||
export ZYPP_CONF=$BATS_TEST_TMPDIR/etc/zypp.conf
|
||||
run skup --dry-run --no-refresh <<<$'2-\n'
|
||||
assert_success
|
||||
assert_line "+ # private $BATS_TEST_TMPDIR/skeptical-updater.XXXXXX/zypp.conf: $BATS_TEST_TMPDIR/etc/zypp.conf with locksfile.path = $BATS_TEST_TMPDIR/skeptical-updater.XXXXXX/locks (a copy of $BATS_TEST_TMPDIR/etc/locks)"
|
||||
assert_line "+ env ZYPP_CONF=$BATS_TEST_TMPDIR/skeptical-updater.XXXXXX/zypp.conf zypper --non-interactive --quiet addlock su-dummy-b"
|
||||
assert_line "+ env ZYPP_CONF=$BATS_TEST_TMPDIR/skeptical-updater.XXXXXX/zypp.conf zypper --non-interactive --no-refresh update"
|
||||
log_lacks 'addlock| update$'
|
||||
[[ ! -e $BATS_TEST_TMPDIR/etc/locks ]]
|
||||
run ls "$BATS_TEST_TMPDIR"
|
||||
refute_output --partial 'skeptical-updater.'
|
||||
}
|
||||
|
||||
@test "as root the refresh really runs; as non-root it is printed" {
|
||||
run skup --dry-run list
|
||||
log_has 'apt-get update -qq -o APT::Update::Error-Mode=any'
|
||||
: >"$FAKE_LOG"
|
||||
SKEPTICAL_UPDATER_EUID=1000 run skup --dry-run list
|
||||
assert_success
|
||||
assert_line '+ apt-get update -qq -o APT::Update::Error-Mode=any'
|
||||
log_lacks '^apt-get update'
|
||||
}
|
||||
Executable
+42
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env bash
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
# Copyright (C) 2026 Bryan Joshua Pedini
|
||||
|
||||
# fake apt-cache (unit tests): show / rdepends / policy from data/apt/
|
||||
# shellcheck source=../fakelib.bash
|
||||
. "${BASH_SOURCE[0]%/*}/../fakelib.bash"
|
||||
fake_log "$@"
|
||||
pkgs=()
|
||||
for a in "$@"; do case $a in -*) ;; *) pkgs+=("$a") ;; esac; done
|
||||
verb=${pkgs[0]}
|
||||
pkgs=("${pkgs[@]:1}")
|
||||
case $verb in
|
||||
show)
|
||||
f=$FAKE_DATA/apt/show/${pkgs[0]}
|
||||
if [[ ! -f $f ]] || fake_purged "${pkgs[0]}"; then
|
||||
echo "E: No packages found" >&2
|
||||
exit 100
|
||||
fi
|
||||
cat "$f"
|
||||
;;
|
||||
rdepends)
|
||||
f=$FAKE_DATA/apt/rdepends/${pkgs[0]}
|
||||
if [[ -f $f ]]; then cat "$f"; else printf '%s\nReverse Depends:\n' "${pkgs[0]}"; fi
|
||||
;;
|
||||
policy)
|
||||
# print the stanzas of the requested packages
|
||||
want=" ${pkgs[*]} "
|
||||
show=0
|
||||
while IFS= read -r line; do
|
||||
if [[ $line =~ ^([^ ].*):$ ]]; then
|
||||
if [[ $want == *" ${BASH_REMATCH[1]} "* ]]; then show=1; else show=0; fi
|
||||
fi
|
||||
((show)) && printf '%s\n' "$line"
|
||||
done <"$FAKE_DATA/apt/policy.txt"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "fake apt-cache: unhandled: $*" >&2
|
||||
exit 99
|
||||
;;
|
||||
esac
|
||||
Executable
+128
@@ -0,0 +1,128 @@
|
||||
#!/usr/bin/env bash
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
# Copyright (C) 2026 Bryan Joshua Pedini
|
||||
|
||||
# fake apt-get (unit tests). Scenario: FAKE_APT_SIM=fixture|held|real|none|purge (data/apt/sim-*.txt).
|
||||
# Purge simulation: installed = the "ii" rows of data/apt/dpkg-status.tsv; reverse dependencies from
|
||||
# data/apt/purge-rdeps.txt; pre-existing orphans from data/apt/autoremovable.txt (added by
|
||||
# --auto-remove, which is system-wide like the real one); FAKE_APT_AUTO_ORPHANS="pkg:dep ..." are
|
||||
# the auto-installed dependencies a purge of pkg orphans; FAKE_APT_SIM_EXTRA="pkg:other ..." makes a
|
||||
# simulated purge of pkg also remove other (a dependent apt-cache rdepends did not report).
|
||||
# shellcheck source=../fakelib.bash
|
||||
. "${BASH_SOURCE[0]%/*}/../fakelib.bash"
|
||||
fake_log "$@"
|
||||
args=" $* "
|
||||
D=$FAKE_DATA/apt
|
||||
|
||||
installed() { grep -q "^.i *"$'\t'"$1"$'\t' "$D/dpkg-status.tsv" && ! fake_purged "$1"; }
|
||||
orphans() { while IFS= read -r o; do installed "$o" && printf '%s\n' "$o"; done <"$D/autoremovable.txt"; }
|
||||
|
||||
# purge_set [--auto-remove] NAME...: the packages a purge removes, sorted
|
||||
purge_set() {
|
||||
local auto=0 n r line
|
||||
if [[ $1 == --auto-remove ]]; then
|
||||
auto=1
|
||||
shift
|
||||
fi
|
||||
{
|
||||
for n in "$@"; do
|
||||
installed "$n" || continue
|
||||
echo "$n"
|
||||
while IFS= read -r line; do
|
||||
[[ $line == "$n: "* ]] || continue
|
||||
for r in ${line#*: }; do installed "$r" && echo "$r"; done
|
||||
done <"$D/purge-rdeps.txt"
|
||||
if ((auto)); then
|
||||
for r in ${FAKE_APT_AUTO_ORPHANS:-}; do [[ ${r%%:*} == "$n" ]] && installed "${r#*:}" && echo "${r#*:}"; done
|
||||
fi
|
||||
for r in ${FAKE_APT_SIM_EXTRA:-}; do [[ ${r%%:*} == "$n" ]] && installed "${r#*:}" && echo "${r#*:}"; done
|
||||
done
|
||||
if ((auto)); then orphans; fi
|
||||
} | sort -u
|
||||
}
|
||||
|
||||
case $args in
|
||||
*' update '*)
|
||||
rc=$(fake_rc FAKE_APT_UPDATE_RC 0)
|
||||
if ((rc)); then
|
||||
echo "E: Failed to fetch http://deb.debian.org/debian/dists/bookworm/InRelease Temporary failure resolving 'deb.debian.org'" >&2
|
||||
echo "E: Some index files failed to download. They have been ignored, or old ones used instead." >&2
|
||||
fi
|
||||
exit "$rc"
|
||||
;;
|
||||
*' -s '*autoremove*)
|
||||
while IFS= read -r o; do [[ -n $o ]] && echo "Remv $o [1.0]"; done < <(orphans)
|
||||
exit 0
|
||||
;;
|
||||
*' -s '*purge*)
|
||||
names=()
|
||||
auto=()
|
||||
for a in "$@"; do
|
||||
case $a in --auto-remove) auto=(--auto-remove) ;; -* | purge) ;; *) names+=("$a") ;; esac
|
||||
done
|
||||
rc=$(fake_rc FAKE_APT_SIM_PURGE_RC 0)
|
||||
if ((rc)); then
|
||||
echo "E: Unable to correct problems, you have held broken packages." >&2
|
||||
exit "$rc"
|
||||
fi
|
||||
for n in "${names[@]}"; do
|
||||
if ! grep -q $'\t'"$n"$'\t' "$D/dpkg-status.tsv" && [[ $n != su-dummy-newdep ]]; then
|
||||
echo "E: Unable to locate package $n" >&2
|
||||
exit 100
|
||||
fi
|
||||
done
|
||||
while IFS= read -r p; do [[ -n $p ]] && echo "Purg $p [1.0]"; done < <(purge_set "${auto[@]}" "${names[@]}")
|
||||
exit 0
|
||||
;;
|
||||
*' -s '*' upgrade '*)
|
||||
f=$D/sim-${FAKE_APT_SIM:-fixture}.txt
|
||||
while IFS= read -r line; do
|
||||
name=
|
||||
case $line in Inst\ * | Conf\ *) name=${line#* }; name=${name%% *} ;; esac
|
||||
if [[ -n $name ]] && fake_purged "$name"; then continue; fi
|
||||
if [[ $line == ' '* && -f $FAKE_DIR/purged ]]; then
|
||||
while IFS= read -r p; do line=${line//$p/}; done <"$FAKE_DIR/purged"
|
||||
fi
|
||||
printf '%s\n' "$line"
|
||||
done <"$f"
|
||||
exit 0
|
||||
;;
|
||||
*' upgrade '*)
|
||||
echo "holds during upgrade: $(sort "$FAKE_DIR/holds" 2>/dev/null | tr '\n' ' ')" >>"$FAKE_LOG"
|
||||
# which of our lock files does the package manager have open (it must have none)?
|
||||
if [[ -d /proc/$$/fd ]]; then
|
||||
n=0
|
||||
for fd in "/proc/$$/fd"/*; do
|
||||
[[ $(readlink "$fd" 2>/dev/null) == *.lock ]] && n=$((n + 1))
|
||||
done
|
||||
echo "lock fds in the upgrade: $n" >>"$FAKE_LOG"
|
||||
fi
|
||||
if [[ -n ${FAKE_APT_UPGRADE_KILL:-} ]]; then
|
||||
kill -s "$FAKE_APT_UPGRADE_KILL" "$PPID"
|
||||
sleep 0.2
|
||||
fi
|
||||
if [[ -n ${FAKE_KILL_READER:-} ]]; then
|
||||
# close the applet's stdout: kill the process reading it, then keep writing
|
||||
pid=$(cat "$FAKE_DIR/reader.pid")
|
||||
kill -9 "$pid"
|
||||
while kill -0 "$pid" 2>/dev/null; do sleep 0.05; done
|
||||
fi
|
||||
echo "Reading package lists..." 2>/dev/null
|
||||
echo "0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded." 2>/dev/null
|
||||
exit "$(fake_rc FAKE_APT_UPGRADE_RC 0)"
|
||||
;;
|
||||
*' purge '*)
|
||||
rc=$(fake_rc FAKE_APT_PURGE_RC 0)
|
||||
if ((rc == 0)); then
|
||||
for a in "$@"; do case $a in -* | purge) ;; *) fake_mark_purged "$a" ;; esac; done
|
||||
# FAKE_APT_PURGE_ALSO: packages that disappear although the plan did not name them
|
||||
read -r -a also <<<"${FAKE_APT_PURGE_ALSO:-}"
|
||||
((${#also[@]} == 0)) || fake_mark_purged "${also[@]}"
|
||||
else
|
||||
echo "E: Held packages were changed and -y was used without --allow-change-held-packages." >&2
|
||||
fi
|
||||
exit "$rc"
|
||||
;;
|
||||
esac
|
||||
echo "fake apt-get: unhandled: $*" >&2
|
||||
exit 99
|
||||
Executable
+67
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env bash
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
# Copyright (C) 2026 Bryan Joshua Pedini
|
||||
|
||||
# fake apt-mark (unit tests): hold state in $FAKE_DIR/holds (one name per line).
|
||||
# FAKE_APT_MARK_LOCKED=N: the first N hold/unhold calls fail like dpkg's busy frontend lock.
|
||||
# FAKE_APT_MARK_UNHOLD_SIGNAL=SIG: send SIG to the applet before an unhold (a second Ctrl-C).
|
||||
# FAKE_APT_MARK_UNHOLD_RC=N: unhold fails (nothing released). FAKE_APT_MARK_FAIL_MSG: the message a
|
||||
# failing hold prints. Every unhold logs whether $STATE_DIR/holds.lock is held by someone.
|
||||
# shellcheck source=../fakelib.bash
|
||||
. "${BASH_SOURCE[0]%/*}/../fakelib.bash"
|
||||
fake_log "$@"
|
||||
h=$FAKE_DIR/holds
|
||||
touch "$h"
|
||||
verb=$1
|
||||
shift
|
||||
case $verb in hold | unhold)
|
||||
if [[ -n ${FAKE_APT_MARK_LOCKED:-} ]]; then
|
||||
n=$(cat "$FAKE_DIR/locked-calls" 2>/dev/null || echo 0)
|
||||
if ((n < FAKE_APT_MARK_LOCKED)); then
|
||||
echo $((n + 1)) >"$FAKE_DIR/locked-calls"
|
||||
echo "${FAKE_APT_MARK_LOCK_MSG:-dpkg: error: dpkg frontend lock was locked by another process with pid 4242}"
|
||||
echo "E: Sub-process dpkg --set-selections returned an error code (2)"
|
||||
echo "E: Executing dpkg failed. Are you root?"
|
||||
exit 100
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
case $verb in
|
||||
showhold) sort "$h" ;;
|
||||
hold)
|
||||
for p in "$@"; do
|
||||
if grep -qxF -- "$p" "$h"; then echo "$p was already set on hold."; else
|
||||
echo "$p" >>"$h"
|
||||
echo "$p set on hold."
|
||||
fi
|
||||
done
|
||||
[[ -z ${FAKE_APT_MARK_FAIL_MSG:-} ]] || echo "$FAKE_APT_MARK_FAIL_MSG" >&2
|
||||
exit "$(fake_rc FAKE_APT_MARK_HOLD_RC 0)"
|
||||
;;
|
||||
unhold)
|
||||
if [[ -n ${SKEPTICAL_UPDATER_STATE_DIR:-} && -e $SKEPTICAL_UPDATER_STATE_DIR/holds.lock ]] && command -v flock >/dev/null; then
|
||||
if flock -n "$SKEPTICAL_UPDATER_STATE_DIR/holds.lock" true; then l=free; else l=held; fi
|
||||
echo "holds.lock during unhold: $l" >>"$FAKE_LOG"
|
||||
fi
|
||||
if [[ -n ${FAKE_APT_MARK_UNHOLD_RC:-} ]]; then
|
||||
echo "E: Unable to correct problems" >&2
|
||||
exit "$FAKE_APT_MARK_UNHOLD_RC"
|
||||
fi
|
||||
if [[ -n ${FAKE_APT_MARK_UNHOLD_SIGNAL:-} ]]; then
|
||||
kill -s "$FAKE_APT_MARK_UNHOLD_SIGNAL" "$(cat "$FAKE_DIR/applet.pid" 2>/dev/null || echo "$PPID")" 2>/dev/null
|
||||
sleep 0.2
|
||||
fi
|
||||
for p in "$@"; do
|
||||
if grep -qxF -- "$p" "$h"; then
|
||||
grep -vxF -- "$p" "$h" >"$h.new" || true
|
||||
mv "$h.new" "$h"
|
||||
echo "Canceled hold on $p."
|
||||
else echo "$p was already not on hold."; fi
|
||||
done
|
||||
;;
|
||||
*)
|
||||
echo "fake apt-mark: unhandled: $verb $*" >&2
|
||||
exit 99
|
||||
;;
|
||||
esac
|
||||
Executable
+19
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
# Copyright (C) 2026 Bryan Joshua Pedini
|
||||
|
||||
# fake dpkg-query (unit tests): the dpkg database dump used for dependency closures
|
||||
# shellcheck source=../fakelib.bash
|
||||
. "${BASH_SOURCE[0]%/*}/../fakelib.bash"
|
||||
fake_log "$@"
|
||||
# FAKE_STATE_APPEND / FAKE_STATE_SED: simulate another process writing the statefile meanwhile
|
||||
st=${SKEPTICAL_UPDATER_STATE_DIR:-}/trusted.tsv
|
||||
if [[ -n ${FAKE_STATE_APPEND:-} ]]; then printf '%s\n' "$FAKE_STATE_APPEND" >>"$st"; fi
|
||||
if [[ -n ${FAKE_STATE_SED:-} ]]; then sed -i "$FAKE_STATE_SED" "$st"; fi
|
||||
case " $* " in
|
||||
*' -W '*) cat "$FAKE_DATA/apt/dpkg-status.tsv" ;;
|
||||
*)
|
||||
echo "fake dpkg-query: unhandled: $*" >&2
|
||||
exit 99
|
||||
;;
|
||||
esac
|
||||
Executable
+134
@@ -0,0 +1,134 @@
|
||||
#!/usr/bin/env bash
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
# Copyright (C) 2026 Bryan Joshua Pedini
|
||||
|
||||
# fake dnf (unit tests). FAKE_DNF_MAJOR=4|5 picks the output dialect; FAKE_DNF_CHECK_RC=0|1 simulates
|
||||
# "no updates" / an error; FAKE_DNF_CHECK=obsoletes replays a check-update with only an
|
||||
# "Obsoleting Packages" section; FAKE_DNF_REMOVE_REFUSE=1 makes `--assumeno remove` fail;
|
||||
# removal tables: see the --assumeno remove branch.
|
||||
# shellcheck source=../fakelib.bash
|
||||
. "${BASH_SOURCE[0]%/*}/../fakelib.bash"
|
||||
# shellcheck source=../../data/rpm/db.bash
|
||||
. "$FAKE_DATA/rpm/db.bash"
|
||||
fake_log "$@"
|
||||
D=$FAKE_DATA/dnf
|
||||
major=${FAKE_DNF_MAJOR:-4}
|
||||
pos=()
|
||||
for a in "$@"; do case $a in -*) ;; *) pos+=("$a") ;; esac; done
|
||||
installed() { [[ -n ${EVR[$1]:-} ]] && ! fake_purged "$1"; }
|
||||
case " $* " in
|
||||
*' --version '*) if ((major == 5)); then echo "dnf5 version 5.4.3.0"; else echo "4.14.0"; fi ;;
|
||||
*' makecache '*)
|
||||
rc=$(fake_rc FAKE_DNF_MAKECACHE_RC 0)
|
||||
((rc == 0)) || echo "Error: Failed to download metadata for repo 'su-dummy': Cannot download repomd.xml" >&2
|
||||
exit "$rc"
|
||||
;;
|
||||
*' check-update '*)
|
||||
rc=$(fake_rc FAKE_DNF_CHECK_RC 100)
|
||||
f=$D/check-update-dnf$major.txt
|
||||
[[ ${FAKE_DNF_CHECK:-} != obsoletes ]] || f=$D/check-update-obsoletes.txt
|
||||
case $rc in
|
||||
100)
|
||||
while IFS= read -r line; do
|
||||
read -r na _ <<<"$line"
|
||||
[[ -n $na ]] && fake_purged "${na%.*}" && continue
|
||||
printf '%s\n' "$line"
|
||||
done <"$f"
|
||||
;;
|
||||
1) echo "Error: Failed to download metadata for repo 'baseos'" >&2 ;;
|
||||
esac
|
||||
exit "$rc"
|
||||
;;
|
||||
*' repoquery '*)
|
||||
p=${pos[-1]}
|
||||
case " $* " in
|
||||
*' --upgrades '*) case $p in su-dummy-[abc]) printf 'skeptical-updater dummy %s (v2.0)\n' "$p"; ((major == 5)) || echo ;; esac ;;
|
||||
*) case $p in su-dummy-new) printf 'skeptical-updater dummy su-dummy-new (v1.0)\n' ;; esac ;;
|
||||
esac
|
||||
exit 0
|
||||
;;
|
||||
*' info '*)
|
||||
p=${pos[-1]}
|
||||
if [[ -f $D/info-$p.txt ]]; then cat "$D/info-$p.txt"; else
|
||||
echo "Error: No matching Packages to list" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
*' --assumeno '*remove*)
|
||||
# the removal table: the target, its dependents (su-dummy-a for su-dummy-b; FAKE_DNF_DEPENDENT=
|
||||
# "pkg:other"), its unused dependencies (FAKE_DNF_UNUSED="pkg:dep"); FAKE_DNF_WRAP wraps those rows
|
||||
p=${pos[-1]}
|
||||
if [[ -n ${FAKE_DNF_REMOVE_REFUSE:-} ]]; then
|
||||
echo "Error: Problem: The operation would result in removing the following protected packages: $p"
|
||||
exit 1
|
||||
fi
|
||||
if ! installed "$p"; then
|
||||
echo "No match for argument: $p"
|
||||
echo "No packages marked for removal."
|
||||
echo "Dependencies resolved."
|
||||
echo "Nothing to do."
|
||||
echo "Complete!"
|
||||
exit 0
|
||||
fi
|
||||
dependent=() unused=()
|
||||
[[ $p != su-dummy-b ]] || ! installed su-dummy-a || dependent+=(su-dummy-a)
|
||||
for r in ${FAKE_DNF_DEPENDENT:-}; do [[ ${r%%:*} == "$p" ]] && dependent+=("${r#*:}"); done
|
||||
for r in ${FAKE_DNF_UNUSED:-}; do [[ ${r%%:*} == "$p" ]] && unused+=("${r#*:}"); done
|
||||
row() { # row NAME
|
||||
if ((major == 5)); then
|
||||
if [[ -n ${FAKE_DNF_WRAP:-} && $1 != "$p" ]]; then
|
||||
echo " $1"
|
||||
echo " noarch 0:1.0-1 <unknown> 15.0 B"
|
||||
else
|
||||
echo " $1 noarch 0:1.0-1 <unknown> 15.0 B"
|
||||
fi
|
||||
elif [[ -n ${FAKE_DNF_WRAP:-} && $1 != "$p" ]]; then
|
||||
printf ' %s\n' "$1"
|
||||
printf ' noarch 1.0-1 @System 15\n'
|
||||
else
|
||||
printf ' %-20s noarch 1.0-1 @System 15\n' "$1"
|
||||
fi
|
||||
}
|
||||
if ((major == 5)); then
|
||||
echo "Package Arch Version Repository Size"
|
||||
else
|
||||
echo "Dependencies resolved."
|
||||
echo "================================================================================"
|
||||
echo " Package Architecture Version Repository Size"
|
||||
echo "================================================================================"
|
||||
fi
|
||||
echo "Removing:"
|
||||
row "$p"
|
||||
if ((${#dependent[@]})); then
|
||||
echo "Removing dependent packages:"
|
||||
for r in "${dependent[@]}"; do row "$r"; done
|
||||
fi
|
||||
if ((${#unused[@]})); then
|
||||
echo "Removing unused dependencies:"
|
||||
for r in "${unused[@]}"; do row "$r"; done
|
||||
fi
|
||||
echo
|
||||
n=$((1 + ${#dependent[@]} + ${#unused[@]}))
|
||||
if ((major == 5)); then
|
||||
echo "Transaction Summary:"
|
||||
echo " Removing: $n packages"
|
||||
echo "Operation aborted by the user."
|
||||
else
|
||||
echo "Transaction Summary"
|
||||
echo "================================================================================"
|
||||
echo "Remove $n Packages"
|
||||
echo
|
||||
echo "Operation aborted."
|
||||
fi
|
||||
exit 1
|
||||
;;
|
||||
*' upgrade '*) exit "$(fake_rc FAKE_DNF_UPGRADE_RC 0)" ;;
|
||||
*' remove '*)
|
||||
fake_mark_purged "${pos[@]:1}"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "fake dnf: unhandled: $*" >&2
|
||||
exit 99
|
||||
;;
|
||||
esac
|
||||
@@ -0,0 +1,23 @@
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
# Copyright (C) 2026 Bryan Joshua Pedini
|
||||
|
||||
# tests/unit/fakes/fakelib.bash - shared by the fake package-manager commands.
|
||||
# Every fake logs its invocation ("<name> <args...>") to $FAKE_LOG and keeps its mutable
|
||||
# state (holds, purged packages, ...) under $FAKE_DIR. Replayed outputs live in
|
||||
# tests/unit/data/<backend>/ and are copied from the samples recorded in discovery/*.md.
|
||||
# shellcheck shell=bash
|
||||
|
||||
FAKE_NAME=${FAKE_AS:-${0##*/}}
|
||||
: "${FAKE_LOG:?FAKE_LOG not set}" "${FAKE_DIR:?FAKE_DIR not set}" "${FAKE_DATA:?FAKE_DATA not set}"
|
||||
|
||||
fake_log() { printf '%s %s\n' "$FAKE_NAME" "$*" >>"$FAKE_LOG"; }
|
||||
|
||||
# fake_rc VAR DEFAULT: exit code override from the environment (e.g. FAKE_APT_UPGRADE_RC=100)
|
||||
fake_rc() {
|
||||
local v=${!1:-}
|
||||
printf '%s' "${v:-$2}"
|
||||
}
|
||||
|
||||
# fake_purged NAME: was NAME removed by an earlier fake purge in this test?
|
||||
fake_purged() { [[ -f $FAKE_DIR/purged ]] && grep -qxF -- "$1" "$FAKE_DIR/purged"; }
|
||||
fake_mark_purged() { printf '%s\n' "$@" >>"$FAKE_DIR/purged"; }
|
||||
Executable
+153
@@ -0,0 +1,153 @@
|
||||
#!/usr/bin/env bash
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
# Copyright (C) 2026 Bryan Joshua Pedini
|
||||
|
||||
# fake pacman (unit tests). The "real" database is $FAKE_PACMAN_DBPATH (see pacman-conf);
|
||||
# -Sy --dbpath DIR logs what DIR/sync was seeded with and writes DIR/sync/su-dummy.db
|
||||
# ("reviewed <n>"; plus a .sig with FAKE_PACMAN_SIG=1); -Su logs the real su-dummy.db it applies.
|
||||
# FAKE_PACMAN_SUP_RC=1 makes the -Sup pre-check fail like the ignored-dependency trap;
|
||||
# FAKE_PACMAN_SU_KILL=SIG: -Su signals the applet (Ctrl-C during the upgrade);
|
||||
# FAKE_PACMAN_STDERR=TEXT: -Sl and -Qu also print TEXT on stderr (an unreadable database).
|
||||
# -Rs --print: FAKE_PACMAN_RS_REFUSE=1 makes pacman refuse ("breaks dependency");
|
||||
# FAKE_PACMAN_RS_EXTRA="pkg:other" adds other to the plan of pkg.
|
||||
# FAKE_PACMAN_QU=fixture|real|none selects the -Qu sample,
|
||||
# FAKE_PACMAN_SL=empty makes -Sl print nothing, FAKE_PACMAN_SUP_EXTRA="a b" adds packages that
|
||||
# only -Sup (the real transaction) shows. Every -Q*/-S[il] call records its COLUMNS value, every
|
||||
# call its --dbpath (or "real") in $FAKE_DIR/pacman-dbpath.
|
||||
# shellcheck source=../fakelib.bash
|
||||
. "${BASH_SOURCE[0]%/*}/../fakelib.bash"
|
||||
fake_log "$@"
|
||||
D=$FAKE_DATA/pacman
|
||||
|
||||
# blocks FILE NAME...: print the blank-line separated blocks whose Name is one of NAME
|
||||
# (all blocks when no NAME); exit 1 (after printing the others) if a NAME is missing
|
||||
blocks() {
|
||||
local f=$1 line block='' name='' rc=0 n
|
||||
shift
|
||||
local -A want=() found=()
|
||||
for n in "$@"; do want[$n]=1; done
|
||||
while IFS= read -r line || [[ -n $block ]]; do
|
||||
if [[ -z $line ]]; then
|
||||
if [[ -n $block ]] && { (($# == 0)) || [[ -n ${want[$name]:-} ]]; } && ! fake_purged "$name"; then
|
||||
printf '%s\n' "$block"
|
||||
found[$name]=1
|
||||
fi
|
||||
block='' name=''
|
||||
continue
|
||||
fi
|
||||
[[ $line =~ ^Name\ +:\ (.*)$ ]] && name=${BASH_REMATCH[1]}
|
||||
block+=$line$'\n'
|
||||
done <"$f"
|
||||
for n in "$@"; do
|
||||
if [[ -z ${found[$n]:-} ]]; then
|
||||
echo "error: package '$n' was not found" >&2
|
||||
rc=1
|
||||
fi
|
||||
done
|
||||
return "$rc"
|
||||
}
|
||||
|
||||
op='' dbpath=real print=0
|
||||
targets=()
|
||||
while (($#)); do
|
||||
case $1 in
|
||||
--dbpath)
|
||||
dbpath=$2
|
||||
shift
|
||||
;;
|
||||
--logfile | --print-format | --ignore) shift ;;
|
||||
--print) print=1 ;;
|
||||
--*) ;;
|
||||
-[A-Z]*) [[ -n $op ]] || op=$1 ;;
|
||||
-*) ;;
|
||||
*) targets+=("$1") ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
echo "$op $dbpath" >>"$FAKE_DIR/pacman-dbpath"
|
||||
case $op in -Q* | -Si | -Sl) echo "$op ${COLUMNS-unset}" >>"$FAKE_DIR/pacman-columns" ;; esac
|
||||
|
||||
qu() {
|
||||
local f=$D/Qu-${FAKE_PACMAN_QU:-fixture}.txt line
|
||||
[[ -f $f ]] || return 0
|
||||
while IFS= read -r line; do
|
||||
fake_purged "${line%% *}" || printf '%s\n' "$line"
|
||||
done <"$f"
|
||||
}
|
||||
|
||||
case $op in
|
||||
-Sy)
|
||||
if [[ $dbpath != real ]]; then
|
||||
echo "seeded: $(cd "$dbpath/sync" && echo *)" >>"$FAKE_LOG"
|
||||
rc=$(fake_rc FAKE_PACMAN_SY_RC 0)
|
||||
((rc)) && exit "$rc"
|
||||
n=$(($(cat "$FAKE_DIR/sy-count" 2>/dev/null || echo 0) + 1))
|
||||
echo "$n" >"$FAKE_DIR/sy-count"
|
||||
echo "reviewed $n" >"$dbpath/sync/su-dummy.db"
|
||||
[[ -z ${FAKE_PACMAN_SIG:-} ]] || echo "signature $n" >"$dbpath/sync/su-dummy.db.sig"
|
||||
fi
|
||||
exit "$(fake_rc FAKE_PACMAN_SY_RC 0)"
|
||||
;;
|
||||
-Qu)
|
||||
[[ -z ${FAKE_PACMAN_STDERR:-} ]] || echo "$FAKE_PACMAN_STDERR" >&2
|
||||
out=$(qu)
|
||||
[[ -n $out ]] || exit 1
|
||||
printf '%s\n' "$out"
|
||||
;;
|
||||
-Sup)
|
||||
if [[ -n ${FAKE_PACMAN_SUP_RC:-} ]]; then
|
||||
echo ":: unable to satisfy dependency 'su-dummy-b>=2.0' required by su-dummy-a"
|
||||
echo "error: failed to prepare transaction (could not satisfy dependencies)" >&2
|
||||
exit "$FAKE_PACMAN_SUP_RC"
|
||||
fi
|
||||
qu | while read -r n _ _ _ flag; do [[ $flag == '[ignored]' ]] || echo "$n"; done
|
||||
for n in ${FAKE_PACMAN_SUP_EXTRA:-}; do echo "$n"; done
|
||||
;;
|
||||
-Sl)
|
||||
[[ -z ${FAKE_PACMAN_STDERR:-} ]] || echo "$FAKE_PACMAN_STDERR" >&2
|
||||
[[ ${FAKE_PACMAN_SL:-} == empty ]] || cat "$D/Sl.txt"
|
||||
;;
|
||||
-Qi) blocks "$D/Qi.txt" "${targets[@]}" ;;
|
||||
-Si)
|
||||
t=()
|
||||
for a in "${targets[@]}"; do t+=("${a#*/}"); done
|
||||
blocks "$D/Si.txt" "${t[@]}"
|
||||
;;
|
||||
-Su | -Syu)
|
||||
echo "real sync at $op: $(cat "$FAKE_PACMAN_DBPATH/sync/su-dummy.db" 2>/dev/null || echo none) / $(cat "$FAKE_PACMAN_DBPATH/sync/su-dummy.db.sig" 2>/dev/null || echo no-sig)" >>"$FAKE_LOG"
|
||||
if [[ -e $FAKE_PACMAN_DBPATH/db.lck ]]; then echo "db.lck at $op: yes" >>"$FAKE_LOG"; else echo "db.lck at $op: no" >>"$FAKE_LOG"; fi
|
||||
if [[ -n ${FAKE_PACMAN_SU_KILL:-} ]]; then
|
||||
kill -s "$FAKE_PACMAN_SU_KILL" "$PPID"
|
||||
sleep 0.2
|
||||
fi
|
||||
exit "$(fake_rc FAKE_PACMAN_SU_RC 0)"
|
||||
;;
|
||||
-Rs)
|
||||
((print)) || exit 98
|
||||
tg=${targets[0]}
|
||||
if [[ -n ${FAKE_PACMAN_RS_REFUSE:-} || $tg == su-dummy-b ]]; then
|
||||
echo "checking dependencies..."
|
||||
echo ":: removing $tg breaks dependency '$tg' required by su-dummy-a"
|
||||
echo "error: failed to prepare transaction (could not satisfy dependencies)" >&2
|
||||
exit 1
|
||||
fi
|
||||
case $tg in
|
||||
su-dummy-a) printf '%s\n' su-dummy-a su-dummy-b ;; # b was installed as a dependency of a
|
||||
su-dummy-c | glib2 | expat | ca-certificates-mozilla) echo "$tg" ;;
|
||||
*)
|
||||
echo "error: target not found: $tg" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
for r in ${FAKE_PACMAN_RS_EXTRA:-}; do [[ ${r%%:*} == "$tg" ]] && echo "${r#*:}"; done
|
||||
;;
|
||||
-Rns)
|
||||
rc=$(fake_rc FAKE_PACMAN_RNS_RC 0)
|
||||
if ((rc)); then echo "error: failed to prepare transaction (could not satisfy dependencies)" >&2; else fake_mark_purged "${targets[@]}"; fi
|
||||
exit "$rc"
|
||||
;;
|
||||
*)
|
||||
echo "fake pacman: unhandled: $op $*" >&2
|
||||
exit 99
|
||||
;;
|
||||
esac
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
# Copyright (C) 2026 Bryan Joshua Pedini
|
||||
|
||||
# fake pacman-conf (unit tests): the "real" pacman database lives in $FAKE_PACMAN_DBPATH
|
||||
case ${1:-} in
|
||||
DBPath) printf '%s/\n' "${FAKE_PACMAN_DBPATH:?}" ;;
|
||||
*) exit 1 ;;
|
||||
esac
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
# Copyright (C) 2026 Bryan Joshua Pedini
|
||||
|
||||
# fake pman (unit tests): only its presence matters (label "pman")
|
||||
exit 0
|
||||
Executable
+81
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env bash
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
# Copyright (C) 2026 Bryan Joshua Pedini
|
||||
|
||||
# fake rpm (unit tests): answers the queries skeptical-updater makes from data/rpm/db.bash
|
||||
# shellcheck source=../fakelib.bash
|
||||
. "${BASH_SOURCE[0]%/*}/../fakelib.bash"
|
||||
# shellcheck source=../../data/rpm/db.bash
|
||||
. "$FAKE_DATA/rpm/db.bash"
|
||||
fake_log "$@"
|
||||
|
||||
installed() { [[ -n ${EVR[$1]:-} ]] && ! fake_purged "$1"; }
|
||||
all=0 whatprov=0 list=0 qf=''
|
||||
names=()
|
||||
while (($#)); do
|
||||
case $1 in
|
||||
-q) ;;
|
||||
-qa) all=1 ;;
|
||||
-ql) list=1 ;;
|
||||
--whatprovides) whatprov=1 ;;
|
||||
--qf | --queryformat)
|
||||
qf=$2
|
||||
shift
|
||||
;;
|
||||
*) names+=("$1") ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
rc=0
|
||||
if ((all)); then
|
||||
for p in "${!EVR[@]}"; do
|
||||
installed "$p" || continue
|
||||
case $qf in
|
||||
*'%{NAME}.%{ARCH}'*)
|
||||
for v in ${EVR[$p]}; do
|
||||
printf '%s.%s\t%s\n' "$p" "${ARCH[$p]}" "$v"
|
||||
done
|
||||
;;
|
||||
*'%{REQUIRENAME}'*'%{=NAME}'*)
|
||||
while IFS= read -r r; do [[ -n $r ]] && printf '%s\t%s\n' "$r" "$p"; done <<<"${REQUIRES[$p]:-}"
|
||||
;;
|
||||
*) printf '%s\n' "$p" ;;
|
||||
esac
|
||||
done
|
||||
exit 0
|
||||
fi
|
||||
if ((whatprov)); then
|
||||
for cap in "${names[@]}"; do
|
||||
hit=0
|
||||
for p in "${!EVR[@]}"; do
|
||||
installed "$p" || continue
|
||||
if grep -qxF -- "$cap" <<<"${PROVIDES[$p]:-}"$'\n'"${FILES[$p]:-}"; then
|
||||
printf '%s\n' "$p"
|
||||
hit=1
|
||||
fi
|
||||
done
|
||||
if ((!hit)); then
|
||||
echo "no package provides $cap"
|
||||
rc=1
|
||||
fi
|
||||
done
|
||||
exit "$rc"
|
||||
fi
|
||||
for p in "${names[@]}"; do
|
||||
if ! installed "$p"; then
|
||||
echo "package $p is not installed"
|
||||
rc=1
|
||||
continue
|
||||
fi
|
||||
if ((list)); then
|
||||
if [[ -n ${FILES[$p]:-} ]]; then printf '%s\n' "${FILES[$p]}"; else echo "(contains no files)"; fi
|
||||
continue
|
||||
fi
|
||||
case $qf in
|
||||
*'%{REQUIRENAME}'*) [[ -z ${REQUIRES[$p]:-} ]] || printf '%s\n' "${REQUIRES[$p]}" ;;
|
||||
*'%{PROVIDENAME}'*) [[ -z ${PROVIDES[$p]:-} ]] || printf '%s\n' "${PROVIDES[$p]}" ;;
|
||||
*'%{SUMMARY}'*) printf '%s\n' "${SUMMARY[$p]:-dummy summary of $p}" ;;
|
||||
*) printf '%s-%s.%s\n' "$p" "${EVR[$p]##* }" "${ARCH[$p]}" ;;
|
||||
esac
|
||||
done
|
||||
exit "$rc"
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
# Copyright (C) 2026 Bryan Joshua Pedini
|
||||
|
||||
# fake sudo (unit tests): logs the command it was asked to run, runs nothing
|
||||
# shellcheck source=../fakelib.bash
|
||||
. "${BASH_SOURCE[0]%/*}/../fakelib.bash"
|
||||
fake_log "$@"
|
||||
exit 0
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
# Copyright (C) 2026 Bryan Joshua Pedini
|
||||
|
||||
# fake yum (unit tests): yum is the dnf binary on EL9+
|
||||
FAKE_AS=yum exec "${BASH_SOURCE[0]%/*}/../dnf/dnf" "$@"
|
||||
Executable
+105
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env bash
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
# Copyright (C) 2026 Bryan Joshua Pedini
|
||||
|
||||
# fake zypper (unit tests). FAKE_ZYPPER_LU=fixture|real|none picks the list-updates XML,
|
||||
# FAKE_ZYPPER_DUP=fixture|empty|removes the `dist-upgrade --dry-run` plan (data/zypper/dup-*.xml).
|
||||
# addlock writes real zypp lock entries into the locksfile.path of $ZYPP_CONF (default
|
||||
# /etc/zypp/locks is never touched: the fake refuses); update/dist-upgrade log that file.
|
||||
# shellcheck source=../fakelib.bash
|
||||
. "${BASH_SOURCE[0]%/*}/../fakelib.bash"
|
||||
fake_log "${ZYPP_CONF:+ZYPP_CONF=$ZYPP_CONF }$*"
|
||||
D=$FAKE_DATA/zypper
|
||||
|
||||
locksfile() {
|
||||
local f=${ZYPP_CONF:-} line l=
|
||||
[[ -n $f && -r $f ]] || return 1
|
||||
while IFS= read -r line; do
|
||||
[[ $line =~ ^[[:space:]]*locksfile\.path[[:space:]]*=[[:space:]]*(.*)$ ]] && l=${BASH_REMATCH[1]}
|
||||
done <"$f"
|
||||
[[ -n $l ]] || return 1
|
||||
printf '%s' "$l"
|
||||
}
|
||||
|
||||
pos=()
|
||||
for a in "$@"; do case $a in -*) ;; *) pos+=("$a") ;; esac; done
|
||||
verb=${pos[0]:-}
|
||||
args=("${pos[@]:1}")
|
||||
xml_remove() { # the install-summary of `remove --dry-run` for the given names
|
||||
local n
|
||||
printf "<?xml version='1.0'?>\n<stream>\n<install-summary download-size=\"0\" space-usage-diff=\"-30\" packages-to-change=\"%s\">\n<to-remove>\n" "$#"
|
||||
for n in "$@"; do printf '<solvable type="package" name="%s" edition="1.0-1" arch="noarch" repository="@System" summary="dummy %s"><description>d</description></solvable>\n' "$n" "$n"; done
|
||||
printf '</to-remove>\n</install-summary>\n</stream>\n'
|
||||
}
|
||||
dry=0
|
||||
case " $* " in *' --dry-run '*) dry=1 ;; esac
|
||||
|
||||
case $verb in
|
||||
refresh)
|
||||
rc=$(fake_rc FAKE_ZYPPER_REFRESH_RC 0)
|
||||
((rc == 0)) || echo "Could not refresh the repositories because of errors." >&2
|
||||
exit "$rc"
|
||||
;;
|
||||
list-updates) cat "$D/lu-${FAKE_ZYPPER_LU:-fixture}.xml" ;;
|
||||
info)
|
||||
p=${args[-1]}
|
||||
if [[ -f $D/info-$p.txt ]]; then cat "$D/info-$p.txt"; else
|
||||
echo "package '$p' not found."
|
||||
exit 104
|
||||
fi
|
||||
;;
|
||||
addlock)
|
||||
l=$(locksfile) || {
|
||||
echo "fake zypper: addlock without a private ZYPP_CONF/locksfile.path" >&2
|
||||
exit 98
|
||||
}
|
||||
for p in "${args[@]}"; do printf '\ntype: package\nmatch_type: glob\ncase_sensitive: on\nsolvable_name: %s\n\n' "$p" >>"$l"; done
|
||||
;;
|
||||
dist-upgrade | dup | update)
|
||||
if ((dry)); then
|
||||
cat "$D/dup-${FAKE_ZYPPER_DUP:-fixture}.xml"
|
||||
exit 0
|
||||
fi
|
||||
if l=$(locksfile); then { echo "--- locks seen by $verb:"; cat "$l"; echo "--- end"; } >>"$FAKE_LOG"; fi
|
||||
if [[ -n ${ZYPP_CONF:-} && -n ${SKEPTICAL_UPDATER_STATE_DIR:-} && -e $SKEPTICAL_UPDATER_STATE_DIR/zypper.lock ]]; then
|
||||
if flock -n "$SKEPTICAL_UPDATER_STATE_DIR/zypper.lock" true; then l=free; else l=held; fi
|
||||
echo "zypper.lock during $verb: $l" >>"$FAKE_LOG"
|
||||
fi
|
||||
if [[ -n ${FAKE_ZYPPER_UPGRADE_KILL:-} ]]; then
|
||||
kill -s "$FAKE_ZYPPER_UPGRADE_KILL" "$PPID"
|
||||
sleep 0.2
|
||||
fi
|
||||
exit "$(fake_rc FAKE_ZYPPER_UPGRADE_RC 0)"
|
||||
;;
|
||||
remove)
|
||||
if ((dry)); then
|
||||
set -- "${args[@]}"
|
||||
# FAKE_ZYPPER_CLEAN="pkg:dep" (unneeded dependency) / FAKE_ZYPPER_EXTRA="pkg:other" join the plan
|
||||
more=()
|
||||
for r in ${FAKE_ZYPPER_CLEAN:-} ${FAKE_ZYPPER_EXTRA:-}; do [[ ${r%%:*} == "${1:-}" ]] && more+=("${r#*:}"); done
|
||||
case ${1:-} in
|
||||
su-dummy-b) xml_remove su-dummy-a su-dummy-b "${more[@]}" ;;
|
||||
su-dummy-*) xml_remove "$1" "${more[@]}" ;;
|
||||
*)
|
||||
echo "'$1' not found in package names. Trying capabilities."
|
||||
echo "No provider of '$1' found."
|
||||
exit 104
|
||||
;;
|
||||
esac
|
||||
exit 0
|
||||
fi
|
||||
for p in "${args[@]}"; do
|
||||
case $p in su-dummy-*) ;; *)
|
||||
echo "'$p' not found in package names. Trying capabilities."
|
||||
echo "No provider of '$p' found."
|
||||
exit 104
|
||||
;;
|
||||
esac
|
||||
done
|
||||
fake_mark_purged "${args[@]}"
|
||||
;;
|
||||
*)
|
||||
echo "fake zypper: unhandled: $*" >&2
|
||||
exit 99
|
||||
;;
|
||||
esac
|
||||
@@ -0,0 +1,344 @@
|
||||
#!/usr/bin/env bats
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
# Copyright (C) 2026 Bryan Joshua Pedini
|
||||
|
||||
# The interactive grammar (stdin is the command channel; no TTY): apt backend, fixture rows
|
||||
# [1] su-dummy-a [2] su-dummy-b [3] su-dummy-c (su-dummy-a depends on su-dummy-b).
|
||||
|
||||
load test_helper
|
||||
|
||||
setup() { common_setup; }
|
||||
|
||||
UPGRADE='apt-get -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold -o DPkg::Lock::Timeout=60 --with-new-pkgs upgrade'
|
||||
|
||||
@test "the session shows the list and the prompt help; q quits with exit 0, nothing upgraded" {
|
||||
run skup --no-refresh <<<'q'
|
||||
assert_success
|
||||
assert_line --index 0 'Backend: apt (Debian GNU/Linux 12 (bookworm))'
|
||||
assert_line '3 upgradable, 0 trusted (hidden, use --all to show), 3 to review:'
|
||||
assert_line ' [1] su-dummy-a 1.0 -> 2.0 su-fixture:su-repo'
|
||||
assert_line '<enter> upgrade | N? what is it | N?? full info | N- exclude this run | Nx purge now | N+ trust forever'
|
||||
assert_line '> q'
|
||||
assert_line 'Quit: nothing was upgraded.'
|
||||
log_lacks '^apt-get -y'
|
||||
log_lacks '^apt-mark (un)?hold'
|
||||
}
|
||||
|
||||
@test "an empty line proceeds: the native upgrade runs, exit 0" {
|
||||
run skup <<<''
|
||||
assert_success
|
||||
assert_line 'Excluded from this run: none'
|
||||
assert_line 'Upgrade finished.'
|
||||
log_has 'apt-get update -qq -o APT::Update::Error-Mode=any'
|
||||
log_has "$UPGRADE"
|
||||
}
|
||||
|
||||
@test "a whitespace-only line also proceeds" {
|
||||
run skup --no-refresh <<<' '
|
||||
assert_success
|
||||
log_has "$UPGRADE"
|
||||
}
|
||||
|
||||
@test "the exit status of the native upgrade is passed through" {
|
||||
FAKE_APT_UPGRADE_RC=100 run skup --no-refresh <<<''
|
||||
assert_failure 100
|
||||
assert_output --partial 'the upgrade failed: apt exited with status 100'
|
||||
}
|
||||
|
||||
@test "EOF aborts with exit 2 without upgrading" {
|
||||
run skup --no-refresh <<<'1?'
|
||||
assert_failure 2
|
||||
assert_output --partial 'end of input: aborted, nothing was upgraded'
|
||||
log_lacks '^apt-get -y'
|
||||
}
|
||||
|
||||
@test "EOF straight away (empty stdin) also aborts with exit 2" {
|
||||
run skup --no-refresh </dev/null
|
||||
assert_failure 2
|
||||
log_lacks '^apt-get -y'
|
||||
}
|
||||
|
||||
@test "a last line without a newline is still executed before EOF" {
|
||||
run skup --no-refresh < <(printf '2-')
|
||||
assert_failure 2
|
||||
assert_line 'excluded from this run: su-dummy-b'
|
||||
}
|
||||
|
||||
@test "N? prints the description and the installed packages that depend on it" {
|
||||
run skup --no-refresh <<<$'2?\nq'
|
||||
assert_success
|
||||
assert_line 'su-dummy-b: dummy package su-dummy-b for skeptical-updater tests'
|
||||
assert_line ' required by (installed): su-dummy-a'
|
||||
}
|
||||
|
||||
@test "N?? prints the native full info" {
|
||||
run skup --no-refresh <<<$'1??\nq'
|
||||
assert_success
|
||||
assert_line 'Package: su-dummy-a'
|
||||
assert_line 'Depends: su-dummy-b (>= 2.0)'
|
||||
log_has 'apt-cache show --no-all-versions su-dummy-a'
|
||||
}
|
||||
|
||||
@test "N- toggles the exclusion and the '-' marker" {
|
||||
run skup --no-refresh <<<$'2-\nl\n2-\nl\n2-\n'
|
||||
assert_success
|
||||
assert_line 'excluded from this run: su-dummy-b'
|
||||
assert_line 're-included: su-dummy-b'
|
||||
assert_line '-[2] su-dummy-b 1.0 -> 2.0 su-fixture:su-repo'
|
||||
assert_line ' [2] su-dummy-b 1.0 -> 2.0 su-fixture:su-repo'
|
||||
assert_line 'Excluded from this run: su-dummy-b'
|
||||
log_has 'apt-mark hold su-dummy-b'
|
||||
log_has 'apt-mark unhold su-dummy-b'
|
||||
}
|
||||
|
||||
@test "Nx asks y/N on the same stdin; y purges, the row leaves, numbers stay stable" {
|
||||
run skup --no-refresh <<<$'3x\ny\nl\n3?\nq'
|
||||
assert_success
|
||||
assert_line 'Purge su-dummy-c now? [y/N] y'
|
||||
assert_line 'purged: su-dummy-c'
|
||||
log_has 'apt-get -y purge su-dummy-c'
|
||||
log_lacks 'autoremove --purge'
|
||||
assert_line '2 upgradable, 0 trusted (hidden, use --all to show), 2 to review:'
|
||||
assert_line ' [2] su-dummy-b 1.0 -> 2.0 su-fixture:su-repo'
|
||||
[[ $(grep -c '^ \[3\] su-dummy-c' <<<"$output") -eq 1 ]]
|
||||
assert_output --partial 'no package [3] in the list (l to redraw)'
|
||||
}
|
||||
|
||||
@test "Nx answered with N, n, empty or anything else does not purge" {
|
||||
for answer in N n '' maybe; do
|
||||
: >"$FAKE_LOG"
|
||||
run skup --no-refresh <<<"3x"$'\n'"$answer"$'\nq'
|
||||
assert_success
|
||||
assert_line 'not purged: su-dummy-c'
|
||||
log_lacks '^apt-get -y'
|
||||
done
|
||||
}
|
||||
|
||||
@test "Nx: a failing purge keeps the row" {
|
||||
FAKE_APT_PURGE_RC=100 run skup --no-refresh <<<$'3x\nyes\nl\nq'
|
||||
assert_success
|
||||
assert_output --partial 'purge of su-dummy-c failed (apt exited 100); the row stays in the list'
|
||||
assert_line ' [3] su-dummy-c 1.0 -> 2.0 su-fixture:su-repo'
|
||||
}
|
||||
|
||||
@test "Nx: EOF at the confirmation aborts with exit 2 and purges nothing" {
|
||||
run skup --no-refresh <<<'3x'
|
||||
assert_failure 2
|
||||
log_lacks '^apt-get -y'
|
||||
}
|
||||
|
||||
@test "Nx on a package other installed packages depend on is refused, like pacman -Rns; the row stays" {
|
||||
run skup --no-refresh <<<$'2x\nl\nq'
|
||||
assert_success
|
||||
assert_line 'not purged: su-dummy-b is required by: su-dummy-a - purge the dependents first'
|
||||
refute_output --partial '[y/N]'
|
||||
assert_line '3 upgradable, 0 trusted (hidden, use --all to show), 3 to review:'
|
||||
[[ $(grep -c '^ \[2\] su-dummy-b' <<<"$output") -eq 2 ]]
|
||||
log_lacks '^apt-get -y'
|
||||
}
|
||||
|
||||
@test "Nx removes the target with the dependencies only it needed, listed in the prompt" {
|
||||
FAKE_APT_AUTO_ORPHANS=su-dummy-a:su-dummy-b run skup --no-refresh <<<$'1x\ny\nl\nq'
|
||||
assert_success
|
||||
assert_line 'Purge su-dummy-a now? This also removes: su-dummy-b [y/N] y'
|
||||
assert_line 'purged: su-dummy-a su-dummy-b'
|
||||
assert_line '1 upgradable, 0 trusted (hidden, use --all to show), 1 to review:'
|
||||
assert_line ' [3] su-dummy-c 1.0 -> 2.0 su-fixture:su-repo'
|
||||
}
|
||||
|
||||
@test "Nx: rows the purge took with it unannounced leave the list too (re-listed afterwards)" {
|
||||
FAKE_APT_PURGE_ALSO=su-dummy-a run skup --no-refresh <<<$'3x\ny\nl\nq'
|
||||
assert_success
|
||||
assert_line ' (su-dummy-a is no longer upgradable, removed from the list)'
|
||||
assert_line '1 upgradable, 0 trusted (hidden, use --all to show), 1 to review:'
|
||||
assert_line ' [2] su-dummy-b 1.0 -> 2.0 su-fixture:su-repo'
|
||||
}
|
||||
|
||||
@test "Nx on a new dependency (old version -, not installed) is refused" {
|
||||
FAKE_APT_SIM=real run skup --no-refresh <<<$'9x\nq'
|
||||
assert_success
|
||||
assert_line 'su-dummy-newdep is not installed (the upgrade would install it as a new dependency): nothing to purge; use 9- to exclude it from this run.'
|
||||
refute_output --partial '[y/N]'
|
||||
log_lacks 'purge'
|
||||
}
|
||||
|
||||
@test "Nx refused by the package manager asks nothing" {
|
||||
FAKE_APT_SIM_PURGE_RC=100 run skup --no-refresh <<<$'3x\nq'
|
||||
assert_success
|
||||
assert_output --partial 'E: Unable to correct problems, you have held broken packages.'
|
||||
assert_output --partial 'cannot purge su-dummy-c: apt refuses (see above); nothing was removed'
|
||||
refute_output --partial '[y/N]'
|
||||
log_lacks '^apt-get -y'
|
||||
}
|
||||
|
||||
@test "purging the last packages ends the session cleanly (exit 0)" {
|
||||
export FAKE_APT_AUTO_ORPHANS=su-dummy-a:su-dummy-b
|
||||
run skup --no-refresh <<<$'3x\ny\n1x\ny'
|
||||
assert_success
|
||||
assert_line 'purged: su-dummy-c'
|
||||
assert_line 'purged: su-dummy-a su-dummy-b'
|
||||
assert_line 'Nothing left to upgrade.'
|
||||
log_lacks "^apt-get -y -o"
|
||||
}
|
||||
|
||||
@test "purging every listed row keeps the session while trusted (hidden) rows remain" {
|
||||
write_state "$HEADER" "apt${T}su-dummy-c${T}user${T}2026-01-01T00:00:00Z"
|
||||
FAKE_APT_AUTO_ORPHANS=su-dummy-a:su-dummy-b run skup --no-refresh <<<$'1x\ny\nl\nq'
|
||||
assert_success
|
||||
assert_line '1 upgradable, 1 trusted (hidden, use --all to show), 0 to review:'
|
||||
assert_line 'Quit: nothing was upgraded.'
|
||||
}
|
||||
|
||||
@test "N+ trusts with the closure: rows leave, numbers stay, the hidden count grows" {
|
||||
run skup --no-refresh <<<$'1+\nl\nq'
|
||||
assert_success
|
||||
assert_line 'trusted su-dummy-a (+1 dependency: su-dummy-b)'
|
||||
assert_line '3 upgradable, 2 trusted (hidden, use --all to show), 1 to review:'
|
||||
assert_line ' [3] su-dummy-c 1.0 -> 2.0 su-fixture:su-repo'
|
||||
# [1] and [2] only appear in the first listing
|
||||
[[ $(grep -c '^ \[1\] su-dummy-a' <<<"$output") -eq 1 ]]
|
||||
[[ $(grep -c '^ \[2\] su-dummy-b' <<<"$output") -eq 1 ]]
|
||||
run cut -f1-3 "$STATE_FILE"
|
||||
assert_line "apt${T}su-dummy-a${T}user"
|
||||
assert_line "apt${T}su-dummy-b${T}dep:su-dummy-a"
|
||||
}
|
||||
|
||||
@test "trusting a package re-includes the excluded rows that this trust hides" {
|
||||
run skup --no-refresh <<<$'2-\n1+\n\n'
|
||||
assert_success
|
||||
assert_line 'trusted su-dummy-a (+1 dependency: su-dummy-b)'
|
||||
assert_line ' (su-dummy-b is trusted now: no longer excluded from this run)'
|
||||
assert_line 'Excluded from this run: none'
|
||||
log_lacks '^apt-mark hold'
|
||||
}
|
||||
|
||||
@test "with --all a trusted row stays visible, so its exclusion stays too" {
|
||||
run skup --no-refresh --all <<<$'2-\n1+\n\n'
|
||||
assert_success
|
||||
refute_output --partial 'no longer excluded'
|
||||
assert_line 'Excluded from this run: su-dummy-b'
|
||||
}
|
||||
|
||||
@test "trusted packages are hidden with a count; numbering covers the listed rows only" {
|
||||
write_state "$HEADER" "apt${T}su-dummy-a${T}user${T}2026-01-01T00:00:00Z"
|
||||
run skup --no-refresh <<<$'1?\nq'
|
||||
assert_success
|
||||
assert_line '3 upgradable, 1 trusted (hidden, use --all to show), 2 to review:'
|
||||
assert_line ' [1] su-dummy-b 1.0 -> 2.0 su-fixture:su-repo'
|
||||
assert_line ' [2] su-dummy-c 1.0 -> 2.0 su-fixture:su-repo'
|
||||
assert_line 'su-dummy-b: dummy package su-dummy-b for skeptical-updater tests'
|
||||
}
|
||||
|
||||
@test "--all lists trusted packages too, marked with '='" {
|
||||
write_state "$HEADER" "apt${T}su-dummy-a${T}user${T}2026-01-01T00:00:00Z"
|
||||
run skup --no-refresh --all <<<$'2+\nl\nq'
|
||||
assert_success
|
||||
assert_line '3 upgradable, 1 trusted (marked with =), 2 to review:'
|
||||
assert_line '=[1] su-dummy-a 1.0 -> 2.0 su-fixture:su-repo'
|
||||
assert_line ' [2] su-dummy-b 1.0 -> 2.0 su-fixture:su-repo'
|
||||
assert_line 'trusted su-dummy-b'
|
||||
assert_line '3 upgradable, 2 trusted (marked with =), 1 to review:'
|
||||
assert_line '=[2] su-dummy-b 1.0 -> 2.0 su-fixture:su-repo'
|
||||
}
|
||||
|
||||
@test "trusted packages are upgraded silently (never excluded)" {
|
||||
write_state "$HEADER" "apt${T}su-dummy-a${T}user${T}2026-01-01T00:00:00Z"
|
||||
run skup --no-refresh <<<''
|
||||
assert_success
|
||||
assert_line 'Excluded from this run: none'
|
||||
log_lacks '^apt-mark hold'
|
||||
log_has "$UPGRADE"
|
||||
}
|
||||
|
||||
@test "everything trusted: 0 to review, the prompt is still offered" {
|
||||
write_state "$HEADER" "apt${T}su-dummy-a${T}user${T}2026-01-01T00:00:00Z" \
|
||||
"apt${T}su-dummy-b${T}user${T}2026-01-01T00:00:00Z" "apt${T}su-dummy-c${T}user${T}2026-01-01T00:00:00Z"
|
||||
run skup --no-refresh <<<''
|
||||
assert_success
|
||||
assert_line '3 upgradable, 3 trusted (hidden, use --all to show), 0 to review:'
|
||||
log_has "$UPGRADE"
|
||||
}
|
||||
|
||||
@test "*+ trusts every listed package" {
|
||||
run skup --no-refresh <<<$'*+\nl\nq'
|
||||
assert_success
|
||||
assert_line 'trusted su-dummy-a (+1 dependency: su-dummy-b)'
|
||||
assert_line 'trusted su-dummy-b'
|
||||
assert_line 'trusted su-dummy-c'
|
||||
assert_line '3 upgradable, 3 trusted (hidden, use --all to show), 0 to review:'
|
||||
run cut -f2,3 "$STATE_FILE"
|
||||
assert_line "su-dummy-b${T}user"
|
||||
}
|
||||
|
||||
@test "*- excludes every listed package; *- again re-includes them" {
|
||||
run skup --no-refresh <<<$'*-\nl\n*-\nl\n*-\n'
|
||||
assert_success
|
||||
assert_line 'excluded all 3 listed packages from this run'
|
||||
assert_line 're-included all 3 listed packages'
|
||||
assert_line '-[1] su-dummy-a 1.0 -> 2.0 su-fixture:su-repo'
|
||||
assert_line 'Excluded from this run: su-dummy-a su-dummy-b su-dummy-c'
|
||||
log_has 'apt-mark hold su-dummy-a su-dummy-b su-dummy-c'
|
||||
}
|
||||
|
||||
@test "several tokens on one line run in order" {
|
||||
run skup --no-refresh <<<$'1? 2- 3?\nl\nq'
|
||||
assert_success
|
||||
assert_line 'su-dummy-a: dummy package su-dummy-a for skeptical-updater tests'
|
||||
assert_line 'excluded from this run: su-dummy-b'
|
||||
assert_line 'su-dummy-c: dummy package su-dummy-c for skeptical-updater tests'
|
||||
assert_line '-[2] su-dummy-b 1.0 -> 2.0 su-fixture:su-repo'
|
||||
}
|
||||
|
||||
@test "an unknown token prints an error, runs nothing from that line and re-prompts" {
|
||||
run skup --no-refresh <<<$'2- bogus\nl\n1+ 3y\nq'
|
||||
assert_success
|
||||
assert_output --partial "unknown command 'bogus' (h for help)"
|
||||
assert_output --partial "unknown command '3y' (h for help)"
|
||||
assert_line ' [2] su-dummy-b 1.0 -> 2.0 su-fixture:su-repo'
|
||||
refute_output --partial 'excluded from this run'
|
||||
[[ ! -e $STATE_FILE ]]
|
||||
}
|
||||
|
||||
@test "numbers that are not in the list are rejected" {
|
||||
run skup --no-refresh <<<$'9?\n0-\n4x\nq'
|
||||
assert_success
|
||||
assert_output --partial 'no package [9] in the list (l to redraw)'
|
||||
assert_output --partial 'no package [0] in the list (l to redraw)'
|
||||
assert_output --partial 'no package [4] in the list (l to redraw)'
|
||||
log_lacks '^apt-get -y'
|
||||
}
|
||||
|
||||
@test "h and ? print the help" {
|
||||
run skup --no-refresh <<<$'h\n?\nq'
|
||||
assert_success
|
||||
[[ $(grep -c 'N?? full native info' <<<"$output") -eq 2 ]]
|
||||
assert_line 'Several tokens can be given on one line, e.g. "1? 3- 4+".'
|
||||
}
|
||||
|
||||
@test "l redraws the list" {
|
||||
run skup --no-refresh <<<$'l\nq'
|
||||
assert_success
|
||||
[[ $(grep -c '^3 upgradable' <<<"$output") -eq 2 ]]
|
||||
}
|
||||
|
||||
@test "q in the middle of a line stops there (later tokens never run)" {
|
||||
run skup --no-refresh <<<$'1? q 3x'
|
||||
assert_success
|
||||
assert_line 'su-dummy-a: dummy package su-dummy-a for skeptical-updater tests'
|
||||
refute_output --partial 'Purge'
|
||||
}
|
||||
|
||||
@test "nothing to upgrade: no prompt, exit 0" {
|
||||
FAKE_APT_SIM=none run skup --no-refresh </dev/null
|
||||
assert_success
|
||||
assert_line 'Nothing to upgrade.'
|
||||
refute_output --partial '<enter> upgrade'
|
||||
}
|
||||
|
||||
@test "held (kept back) rows can be reviewed like the others" {
|
||||
echo su-dummy-c >"$FAKE_DIR/holds"
|
||||
FAKE_APT_SIM=held run skup --no-refresh <<<$'3?\nq'
|
||||
assert_success
|
||||
assert_line ' [3] su-dummy-c 1.0 -> 2.0 file:/srv/su-repo ./ [held]'
|
||||
assert_line 'su-dummy-c: dummy package su-dummy-c for skeptical-updater tests'
|
||||
}
|
||||
@@ -0,0 +1,401 @@
|
||||
#!/usr/bin/env bats
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
# Copyright (C) 2026 Bryan Joshua Pedini
|
||||
|
||||
# Every backend parser, fed with the real outputs recorded in discovery/*.md
|
||||
# (replayed by the fakes in tests/unit/fakes/, data in tests/unit/data/).
|
||||
|
||||
# each @test runs in its own subshell: per-test exports are intended
|
||||
# shellcheck disable=SC2030,SC2031
|
||||
|
||||
load test_helper
|
||||
|
||||
setup() { common_setup; }
|
||||
|
||||
# ---- apt ----------------------------------------------------------------------------------------
|
||||
|
||||
@test "apt: Inst lines (all the edge cases) and the kept-back block (held, versions from policy)" {
|
||||
FAKE_APT_SIM=real run skup list --no-refresh
|
||||
assert_success
|
||||
assert_line '10 upgradable, 0 trusted (hidden, use --all to show), 10 to review:'
|
||||
assert_line --regexp '^ \[1\] libaudit-common +1:3\.1\.2-2\.1build1\.1 +-> +1:3\.1\.2-2\.1ubuntu0\.1 +Ubuntu:24\.04/noble-updates$'
|
||||
assert_line --regexp '^ \[2\] libaudit1 +1:3\.1\.2-2\.1build1\.1 +-> +1:3\.1\.2-2\.1ubuntu0\.1 +Ubuntu:24\.04/noble-updates$'
|
||||
assert_line --regexp '^ \[3\] libc6:i386 +2\.36-9 +-> +2\.36-9\+deb12u10 +Debian-Security:12/oldstable-security, Debian:12\.12/oldstable$'
|
||||
assert_line --regexp '^ \[4\] libssl3t64 +3\.0\.13-0ubuntu3\.15 +-> +3\.0\.13-0ubuntu3\.16 +Ubuntu:24\.04/noble-updates, Ubuntu:24\.04/noble-security$'
|
||||
assert_line --regexp '^ \[5\] perl-base +5\.36\.0-7\+deb12u3 +-> +5\.36\.0-7\+deb12u4 +http://deb\.debian\.org/debian-security bookworm-security/main amd64 \[held\]$'
|
||||
assert_line --regexp '^ \[6\] su-dummy-c +1\.0 +-> +2\.0 +file:/srv/su-repo \./ \[held\]$'
|
||||
assert_line --regexp '^ \[7\] su-dummy-e +1\.0 +-> +2\.0 +file:/srv/su-repo \./ \[held\]$'
|
||||
assert_line --regexp '^ \[8\] su-dummy-m +1\.0 +-> +2\.0 +su-fixture:su-repo$'
|
||||
assert_line --regexp '^ \[9\] su-dummy-newdep +- +-> +1\.0 +su-fixture:su-repo$'
|
||||
assert_line --regexp '^ \[10\] su-dummy-slow +0\.9 +-> +1\.0$'
|
||||
refute_output --partial 'su-dummy-f'
|
||||
log_has 'apt-get -s --with-new-pkgs upgrade'
|
||||
log_has 'apt-cache policy perl-base su-dummy-c su-dummy-e'
|
||||
}
|
||||
|
||||
@test "apt: a held package from the fixture shows up with the held flag" {
|
||||
FAKE_APT_SIM=held run skup list --no-refresh
|
||||
assert_success
|
||||
assert_line --regexp '^ \[1\] su-dummy-a +1\.0 +-> +2\.0 +su-fixture:su-repo$'
|
||||
assert_line --regexp '^ \[3\] su-dummy-c +1\.0 +-> +2\.0 +file:/srv/su-repo \./ \[held\]$'
|
||||
}
|
||||
|
||||
@test "apt: nothing to upgrade" {
|
||||
FAKE_APT_SIM=none run skup list --no-refresh
|
||||
assert_success
|
||||
assert_line 'Nothing to upgrade.'
|
||||
log_lacks '^apt-cache policy'
|
||||
}
|
||||
|
||||
@test "apt: description = candidate Description; reverse deps deduplicated, '|' stripped" {
|
||||
run skup info su-dummy-b
|
||||
assert_success
|
||||
assert_output "$(printf '%s\n' 'su-dummy-b: dummy package su-dummy-b for skeptical-updater tests' ' required by (installed): su-dummy-a')"
|
||||
log_has 'apt-cache show --no-all-versions su-dummy-b'
|
||||
log_has 'apt-cache rdepends --installed --no-recommends --no-suggests --no-conflicts --no-breaks --no-replaces --no-enhances su-dummy-b'
|
||||
run skup info gpgv
|
||||
assert_success
|
||||
assert_line 'gpgv: GNU privacy guard - signature verification tool'
|
||||
assert_line ' required by (installed): apt'
|
||||
}
|
||||
|
||||
@test "apt: the description also comes from Description-<lang> fields (Translation files)" {
|
||||
mkdir -p "$BATS_TEST_TMPDIR/data/apt/show"
|
||||
cp -r "$FAKE_DATA"/. "$BATS_TEST_TMPDIR/data/"
|
||||
sed -i 's/^Description: /Description-en: /' "$BATS_TEST_TMPDIR/data/apt/show/su-dummy-a"
|
||||
sed -i 's/^Description: /Description-pt_BR: /' "$BATS_TEST_TMPDIR/data/apt/show/su-dummy-b"
|
||||
FAKE_DATA=$BATS_TEST_TMPDIR/data run skup info su-dummy-a
|
||||
assert_success
|
||||
assert_line 'su-dummy-a: dummy package su-dummy-a for skeptical-updater tests'
|
||||
FAKE_DATA=$BATS_TEST_TMPDIR/data run skup info su-dummy-b
|
||||
assert_line 'su-dummy-b: dummy package su-dummy-b for skeptical-updater tests'
|
||||
# Description-md5 is not a description
|
||||
sed -i '/^Description-en: /d' "$BATS_TEST_TMPDIR/data/apt/show/su-dummy-a"
|
||||
FAKE_DATA=$BATS_TEST_TMPDIR/data run skup info su-dummy-a
|
||||
assert_line 'su-dummy-a: (no description available)'
|
||||
}
|
||||
|
||||
@test "apt: info of an unknown package exits 1" {
|
||||
run skup info no-such-pkg-xyz
|
||||
assert_failure 1
|
||||
assert_line 'no-such-pkg-xyz: (no description available: unknown package?)'
|
||||
}
|
||||
|
||||
@test "apt: show is apt-cache show --no-all-versions (the candidate)" {
|
||||
run skup show su-dummy-a
|
||||
assert_success
|
||||
assert_line 'Package: su-dummy-a'
|
||||
assert_line 'Version: 2.0'
|
||||
assert_line 'Depends: su-dummy-b (>= 2.0)'
|
||||
run skup show no-such-pkg-xyz
|
||||
assert_failure 1
|
||||
assert_output --partial "no package information for 'no-such-pkg-xyz' (apt exited 100)"
|
||||
}
|
||||
|
||||
# ---- pacman -------------------------------------------------------------------------------------
|
||||
|
||||
@test "pacman: -Qu joined with the FIRST -Sl repo; [ignored] => held" {
|
||||
use_fakes pacman
|
||||
FAKE_PACMAN_QU=real run skup list --no-refresh
|
||||
assert_success
|
||||
assert_line '6 upgradable, 0 trusted (hidden, use --all to show), 6 to review:'
|
||||
assert_line ' [1] ca-certificates-mozilla 3.130-1 -> 3.131-1 core'
|
||||
assert_line --regexp '^ \[2\] expat +2\.8\.5-1 +-> +2\.9\.0-1 +core \[held\]$'
|
||||
assert_line --regexp '^ \[3\] glib2 +2\.88\.3-1 +-> +2\.90\.1-1 +core \[held\]$'
|
||||
assert_line --regexp '^ \[5\] su-dummy-b +1\.0-1 +-> +2\.0-1 +su-dummy$'
|
||||
refute_output --partial 'su-dummy2'
|
||||
}
|
||||
|
||||
@test "pacman: rows survive an empty 'pacman -Sl' (no repo column instead of a broken join)" {
|
||||
use_fakes pacman
|
||||
FAKE_PACMAN_SL=empty run skup list --no-refresh
|
||||
assert_success
|
||||
assert_line '3 upgradable, 0 trusted (hidden, use --all to show), 3 to review:'
|
||||
assert_line ' [1] su-dummy-a 1.0-1 -> 2.0-1'
|
||||
}
|
||||
|
||||
@test "pacman: packages only the transaction (-Sup) shows become a note" {
|
||||
use_fakes pacman
|
||||
FAKE_PACMAN_SUP_EXTRA='su-dummy-new' run skup list --no-refresh
|
||||
assert_success
|
||||
assert_line 'Note: the upgrade also installs (new dependencies or replacements): su-dummy-new'
|
||||
}
|
||||
|
||||
@test "pacman: nothing in -Qu but pending work in -Sup: the upgrade is still offered" {
|
||||
use_fakes pacman
|
||||
FAKE_PACMAN_QU=none FAKE_PACMAN_SUP_EXTRA='su-dummy-new' run skup --no-refresh <<<''
|
||||
assert_success
|
||||
assert_line '0 to review, but pacman reports pending changes (see the notes): <enter> runs the upgrade.'
|
||||
log_has 'pacman -Su --noconfirm'
|
||||
}
|
||||
|
||||
@test "pacman: nothing upgradable (-Qu exits 1 with no output)" {
|
||||
use_fakes pacman
|
||||
FAKE_PACMAN_QU=none run skup list --no-refresh
|
||||
assert_success
|
||||
assert_line 'Nothing to upgrade.'
|
||||
}
|
||||
|
||||
@test "pacman: description from -Qi, falling back to -Si for packages that are not installed" {
|
||||
use_fakes pacman
|
||||
run skup info su-dummy-a
|
||||
assert_success
|
||||
assert_line 'su-dummy-a: Skeptical-updater dummy package A'
|
||||
assert_line ' required by (installed): nothing'
|
||||
run skup info su-dummy-new
|
||||
assert_success
|
||||
assert_line 'su-dummy-new: Skeptical-updater dummy package NEW'
|
||||
}
|
||||
|
||||
@test "pacman: reverse deps from 'Required By', wrapped continuation lines joined" {
|
||||
use_fakes pacman
|
||||
run skup info glibc
|
||||
assert_success
|
||||
assert_line 'glibc: GNU C Library'
|
||||
assert_line ' required by (installed): bash, gcc-libs, ncurses, readline'
|
||||
run skup info su-dummy-b
|
||||
assert_line ' required by (installed): su-dummy-a'
|
||||
}
|
||||
|
||||
@test "pacman: the closure resolves provides (sonames, sh) and version constraints" {
|
||||
use_fakes pacman
|
||||
run skup trust bash
|
||||
assert_success
|
||||
assert_output 'trusted bash (+8 dependencies: filesystem gcc-libs glibc iana-etc linux-api-headers ncurses readline tzdata)'
|
||||
run skup trust su-dummy-a
|
||||
assert_output 'trusted su-dummy-a (+1 dependency: su-dummy-b)'
|
||||
}
|
||||
|
||||
@test "pacman: show prints the installed (-Qi) and the candidate (-Si) records" {
|
||||
use_fakes pacman
|
||||
run skup show su-dummy-a
|
||||
assert_success
|
||||
assert_line 'Install Reason : Explicitly installed'
|
||||
assert_line 'Repository : su-dummy'
|
||||
assert_line 'Version : 2.0-1'
|
||||
log_has 'pacman -Qi -- su-dummy-a'
|
||||
log_has 'pacman -Si -- su-dummy-a'
|
||||
run skup show nope
|
||||
assert_failure 1
|
||||
}
|
||||
|
||||
@test "pacman: every query runs with COLUMNS=0 (no wrapping)" {
|
||||
use_fakes pacman
|
||||
export COLUMNS=20
|
||||
run skup info glibc
|
||||
run skup list --no-refresh
|
||||
run skup trust bash
|
||||
run grep -v ' 0$' "$FAKE_DIR/pacman-columns"
|
||||
assert_output ''
|
||||
}
|
||||
|
||||
# ---- dnf ----------------------------------------------------------------------------------------
|
||||
|
||||
@test "dnf4: check-update (blank line, padding, Obsoleting section), old version from rpm -qa" {
|
||||
use_fakes dnf rpm
|
||||
run skup list --no-refresh
|
||||
assert_success
|
||||
assert_line '6 upgradable, 0 trusted (hidden, use --all to show), 6 to review:'
|
||||
assert_line ' [1] gnutls 3.8.10-4.el9_8 -> 3.8.10-9.el9_8 baseos'
|
||||
# installonly: the highest installed version is the old one
|
||||
assert_line --regexp '^ \[2\] kernel-core +5\.14\.0-611\.5\.el9 +-> +5\.14\.0-612\.1\.el9 +baseos$'
|
||||
assert_line --regexp '^ \[3\] su-dummy-a +1\.0-1 +-> +2\.0-1 +su-dummy$'
|
||||
assert_line --regexp '^ \[5\] su-dummy-c +1\.0-1 +-> +2\.0-1 +su-dummy$'
|
||||
# epoch kept on both sides
|
||||
assert_line --regexp '^ \[6\] vim-minimal +2:8\.2\.2637-26\.el9_8\.21 +-> +2:8\.2\.2637-26\.el9_8\.23 +baseos$'
|
||||
refute_line --regexp '\] su-dummy-new'
|
||||
refute_output --partial 'su-dummy-old'
|
||||
refute_output --partial 'Obsoleting'
|
||||
assert_line 'Note: the upgrade also installs (obsoleting installed packages): su-dummy-new'
|
||||
}
|
||||
|
||||
@test "dnf: check-update exit 100 with only obsoleting packages: 0 rows, but the upgrade is offered" {
|
||||
use_fakes dnf rpm
|
||||
FAKE_DNF_CHECK=obsoletes run skup --no-refresh <<<''
|
||||
assert_success
|
||||
assert_line '0 upgradable, 0 trusted (hidden, use --all to show), 0 to review:'
|
||||
assert_line 'Note: the upgrade also installs (obsoleting installed packages): su-dummy-new'
|
||||
assert_line '0 to review, but dnf reports pending changes (see the notes): <enter> runs the upgrade.'
|
||||
refute_output --partial 'Nothing to upgrade.'
|
||||
log_has 'dnf -y --setopt=skip_if_unavailable=0 --setopt=*.metadata_expire=-1 upgrade'
|
||||
}
|
||||
|
||||
@test "dnf5: the 'Upgrades' header is skipped" {
|
||||
use_fakes dnf rpm
|
||||
FAKE_DNF_MAJOR=5 run skup list --no-refresh
|
||||
assert_success
|
||||
assert_line '5 upgradable, 0 trusted (hidden, use --all to show), 5 to review:'
|
||||
assert_line --regexp '^ \[1\] ca-certificates +2025\.2\.80_v9\.0\.305-91\.el9 +-> +2026\.2\.90_v9\.0\.317-1\.fc44 +updates$'
|
||||
assert_line --regexp '^ \[2\] curl +8\.18\.0-1\.fc44 +-> +8\.18\.0-12\.fc44 +updates$'
|
||||
refute_output --partial 'Upgrades'
|
||||
}
|
||||
|
||||
@test "dnf: check-update exit 0 means nothing to upgrade, exit 1 is an error (exit 1)" {
|
||||
use_fakes dnf rpm
|
||||
FAKE_DNF_CHECK_RC=0 run skup list --no-refresh
|
||||
assert_success
|
||||
assert_line 'Nothing to upgrade.'
|
||||
FAKE_DNF_CHECK_RC=1 run skup list --no-refresh
|
||||
assert_failure 1
|
||||
assert_output --partial 'dnf check-update failed (exit 1)'
|
||||
assert_output --partial 'could not list the upgradable packages'
|
||||
}
|
||||
|
||||
@test "dnf: description from repoquery (candidate), rpm fallback when not upgradable" {
|
||||
use_fakes dnf rpm
|
||||
run skup info su-dummy-a
|
||||
assert_success
|
||||
assert_line 'su-dummy-a: skeptical-updater dummy su-dummy-a (v2.0)'
|
||||
run skup info su-dummy-d
|
||||
assert_success
|
||||
assert_line 'su-dummy-d: skeptical-updater dummy su-dummy-d (v1.0)'
|
||||
assert_line ' required by (installed): nothing'
|
||||
FAKE_DNF_MAJOR=5 run skup info su-dummy-a
|
||||
assert_line 'su-dummy-a: skeptical-updater dummy su-dummy-a (v2.0)'
|
||||
}
|
||||
|
||||
@test "rpm reverse deps: provides + owned files joined with every requirement (rich deps too)" {
|
||||
use_fakes dnf rpm
|
||||
run skup info su-dummy-b
|
||||
assert_line ' required by (installed): su-dummy-a, su-dummy-d'
|
||||
run skup info su-dummy-c
|
||||
assert_line ' required by (installed): su-dummy-d'
|
||||
run skup info bash
|
||||
assert_line ' required by (installed): filesystem, glibc-common, redhat-rpm-config, rpm, su-dummy-d'
|
||||
}
|
||||
|
||||
@test "rpm closure: file deps, rich deps without their condition, rpmlib() dropped" {
|
||||
use_fakes dnf rpm
|
||||
run skup trust su-dummy-d
|
||||
assert_success
|
||||
assert_output 'trusted su-dummy-d (+11 dependencies: basesystem bash filesystem glibc glibc-common glibc-gconv-extra ncurses-libs setup su-dummy-a su-dummy-b su-dummy-c)'
|
||||
refute_output --partial 'redhat-rpm-config'
|
||||
refute_output --partial 'rpmlib'
|
||||
run skup trust su-dummy-a
|
||||
assert_output 'trusted su-dummy-a (its installed dependency was already trusted)'
|
||||
}
|
||||
|
||||
@test "dnf4: a removal-table row wrapped onto two lines (long name) is one package" {
|
||||
use_fakes dnf rpm
|
||||
export FAKE_RPM_NO_D=1
|
||||
FAKE_DNF_UNUSED=su-dummy-a:su-dummy-b FAKE_DNF_WRAP=1 run skup --no-refresh <<<$'3x\ny\nq'
|
||||
assert_success
|
||||
assert_output --partial 'Purge su-dummy-a now? This also removes: su-dummy-b [y/N]'
|
||||
refute_output --partial 'noarch'
|
||||
log_has 'dnf -y remove su-dummy-a su-dummy-b'
|
||||
}
|
||||
|
||||
@test "dnf5: a wrapped removal-table row is one package as well" {
|
||||
use_fakes dnf rpm
|
||||
export FAKE_RPM_NO_D=1
|
||||
FAKE_DNF_MAJOR=5 FAKE_DNF_UNUSED=su-dummy-a:su-dummy-b FAKE_DNF_WRAP=1 run skup --no-refresh <<<$'3x\ny\nq'
|
||||
assert_success
|
||||
assert_output --partial 'Purge su-dummy-a now? This also removes: su-dummy-b [y/N]'
|
||||
log_has 'dnf -y remove su-dummy-a su-dummy-b'
|
||||
}
|
||||
|
||||
@test "dnf: show is dnf -q info" {
|
||||
use_fakes dnf rpm
|
||||
run skup show su-dummy-a
|
||||
assert_success
|
||||
assert_line 'Available Upgrades'
|
||||
assert_line 'Version : 2.0'
|
||||
log_has 'dnf -q info su-dummy-a'
|
||||
}
|
||||
|
||||
# ---- zypper -------------------------------------------------------------------------------------
|
||||
|
||||
@test "zypper: --xmlout list-updates (one-line XML, multi-line descriptions, last record, entities)" {
|
||||
use_fakes zypper rpm
|
||||
FAKE_ZYPPER_LU=real run skup list --no-refresh
|
||||
assert_success
|
||||
assert_line '4 upgradable, 0 trusted (hidden, use --all to show), 4 to review:'
|
||||
assert_line ' [1] openSUSE-build-key 1.0-68.1 -> 1.0-69.1 repo-update'
|
||||
assert_line --regexp '^ \[2\] su-dummy-a +1\.0-1 +-> +2\.0-1 +su-dummy$'
|
||||
assert_line --regexp '^ \[3\] su-dummy-b +1\.0-1 +-> +2\.0-1 +openSUSE:repo-oss & co$'
|
||||
assert_line --regexp '^ \[4\] su-dummy-c +1\.0-1 +-> +2\.0-1 +su-dummy$'
|
||||
log_has 'zypper --non-interactive --xmlout list-updates'
|
||||
}
|
||||
|
||||
@test "zypper on Tumbleweed (root): the review list is the dist-upgrade plan" {
|
||||
use_fakes zypper rpm
|
||||
export SKEPTICAL_UPDATER_OS_RELEASE=$FAKE_DATA/os-release-tumbleweed
|
||||
run skup list --no-refresh
|
||||
assert_success
|
||||
log_has 'zypper --non-interactive --no-refresh --xmlout dist-upgrade --dry-run'
|
||||
log_lacks 'list-updates'
|
||||
assert_line '3 upgradable, 0 trusted (hidden, use --all to show), 3 to review:'
|
||||
assert_line ' [1] su-dummy-a 1.0-1 -> 2.0-1 su-dummy'
|
||||
refute_output --partial 'openSUSE-release'
|
||||
}
|
||||
|
||||
@test "zypper on Tumbleweed: new packages are rows (old -), removals are a note" {
|
||||
use_fakes zypper rpm
|
||||
export SKEPTICAL_UPDATER_OS_RELEASE=$FAKE_DATA/os-release-tumbleweed
|
||||
FAKE_ZYPPER_DUP=removes run skup list --no-refresh
|
||||
assert_success
|
||||
assert_line ' [2] su-dummy-newdep - -> 1.0-1 su-dummy'
|
||||
assert_line 'Note: the dist-upgrade also removes: libcurl-mini4'
|
||||
}
|
||||
|
||||
@test "zypper on Tumbleweed: a plan with only removals is not 'Nothing to upgrade'" {
|
||||
use_fakes zypper rpm
|
||||
export SKEPTICAL_UPDATER_OS_RELEASE=$FAKE_DATA/os-release-tumbleweed
|
||||
FAKE_ZYPPER_DUP=onlyremove run skup --no-refresh <<<''
|
||||
assert_success
|
||||
assert_line '0 to review, but zypper reports pending changes (see the notes): <enter> runs the upgrade.'
|
||||
log_has 'zypper --non-interactive --no-refresh dist-upgrade'
|
||||
FAKE_ZYPPER_DUP=empty run skup --no-refresh </dev/null
|
||||
assert_success
|
||||
assert_line 'Nothing to upgrade.'
|
||||
}
|
||||
|
||||
@test "zypper on Tumbleweed as non-root: list-updates with a note (the plan needs root)" {
|
||||
use_fakes zypper rpm
|
||||
export SKEPTICAL_UPDATER_OS_RELEASE=$FAKE_DATA/os-release-tumbleweed
|
||||
SKEPTICAL_UPDATER_EUID=1000 run skup list --no-refresh
|
||||
assert_success
|
||||
log_has 'zypper --non-interactive --xmlout list-updates'
|
||||
log_lacks 'dist-upgrade'
|
||||
assert_line 'Note: not root: this is "zypper list-updates"; the dist-upgrade plan (which may differ) needs root'
|
||||
}
|
||||
|
||||
@test "zypper: an empty update list" {
|
||||
use_fakes zypper rpm
|
||||
FAKE_ZYPPER_LU=none run skup list --no-refresh
|
||||
assert_success
|
||||
assert_line 'Nothing to upgrade.'
|
||||
}
|
||||
|
||||
@test "zypper: description from 'zypper info' Summary, rpm fallback when zypper exits 104" {
|
||||
use_fakes zypper rpm
|
||||
run skup info su-dummy-a
|
||||
assert_success
|
||||
assert_line 'su-dummy-a: skeptical-updater dummy su-dummy-a (v2.0)'
|
||||
run skup info su-dummy-d
|
||||
assert_success
|
||||
assert_line 'su-dummy-d: skeptical-updater dummy su-dummy-d (v1.0)'
|
||||
run skup info nothing-like-this
|
||||
assert_failure 1
|
||||
}
|
||||
|
||||
@test "zypper: show is zypper info --requires; unknown package => exit 1" {
|
||||
use_fakes zypper rpm
|
||||
run skup show su-dummy-a
|
||||
assert_success
|
||||
assert_line 'Information for package su-dummy-a:'
|
||||
log_has 'zypper --non-interactive --quiet info --requires su-dummy-a'
|
||||
run skup show nothing-like-this
|
||||
assert_failure 1
|
||||
assert_output --partial "(zypper exited 104)"
|
||||
}
|
||||
|
||||
@test "zypper: reverse deps and closure use the same pure-rpm code" {
|
||||
use_fakes zypper rpm
|
||||
run skup info su-dummy-c
|
||||
assert_line ' required by (installed): su-dummy-d'
|
||||
run skup trust su-dummy-a
|
||||
assert_output 'trusted su-dummy-a (+1 dependency: su-dummy-b)'
|
||||
run cut -f1 "$STATE_FILE"
|
||||
assert_line zypper
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
#!/usr/bin/env bats
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
# Copyright (C) 2026 Bryan Joshua Pedini
|
||||
|
||||
# Robustness: the apt hold restore survives a closed stdout and repeated signals, locks and
|
||||
# merge-on-save of the statefile, an inaccessible state directory.
|
||||
|
||||
# each @test runs in its own subshell: per-test exports are intended
|
||||
# shellcheck disable=SC2030,SC2031
|
||||
|
||||
load test_helper
|
||||
|
||||
setup() { common_setup; }
|
||||
|
||||
# nobody_env: a world-reachable copy of the test environment under /tmp for running the applet as
|
||||
# nobody (setpriv): $W/bin (tools), $W/fake (log), $W/tmp; sets W
|
||||
nobody_env() {
|
||||
((EUID == 0)) || skip "needs root to switch to an unprivileged user"
|
||||
command -v setpriv >/dev/null 2>&1 || skip "setpriv not available"
|
||||
W=$(mktemp -d /tmp/su-nobody.XXXXXX)
|
||||
chmod 0755 "$W"
|
||||
mkdir -p "$W/bin" "$W/fake" "$W/tmp" "$W/state"
|
||||
chmod 0777 "$W/fake" "$W/tmp"
|
||||
for t in "$SYSBIN"/*; do ln -s "$(readlink "$t")" "$W/bin/${t##*/}"; done
|
||||
: >"$W/fake/log"
|
||||
chmod 0666 "$W/fake/log"
|
||||
}
|
||||
# nobody CMD-ARGS...: run the applet as nobody (no flock in PATH: the mkdir lock fallback)
|
||||
# shellcheck disable=SC2329 # invoked through `run`
|
||||
nobody() {
|
||||
rm -f "$W/bin/flock"
|
||||
setpriv --reuid=65534 --regid=65534 --clear-groups -- env -u SKEPTICAL_UPDATER_EUID \
|
||||
PATH="$FAKES/apt:$W/bin" FAKE_DIR="$W/fake" FAKE_LOG="$W/fake/log" TMPDIR="$W/tmp" \
|
||||
SKEPTICAL_UPDATER_STATE_DIR="$W/state" "$SU_BIN" "$@"
|
||||
}
|
||||
|
||||
@test "a closed stdout (the tee reading it was killed) does not stop the hold restore" {
|
||||
FAKE_KILL_READER=1 run bash -c '
|
||||
printf "2-\n\n" | env PATH="$1" "$2" --no-refresh |
|
||||
{ echo "$BASHPID" >"$3/reader.pid"; while IFS= read -r _; do :; done; }
|
||||
echo "applet status ${PIPESTATUS[1]}"' _ "$TEST_PATH" "$SU_BIN" "$FAKE_DIR"
|
||||
assert_line 'applet status 141'
|
||||
log_has 'apt-mark hold su-dummy-b'
|
||||
log_has 'apt-mark unhold su-dummy-b'
|
||||
run cat "$FAKE_DIR/holds"
|
||||
assert_output ''
|
||||
[[ ! -e $STATE/holds.added ]]
|
||||
}
|
||||
|
||||
@test "a second signal while the exit handler restores the holds does not abort the restore" {
|
||||
# 1st TERM during the upgrade starts the exit handler, a 2nd TERM arrives during its apt-mark unhold
|
||||
FAKE_APT_UPGRADE_KILL=TERM FAKE_APT_MARK_UNHOLD_SIGNAL=TERM \
|
||||
env PATH="$TEST_PATH" "$SU_BIN" --no-refresh <<<$'2-\n' >"$BATS_TEST_TMPDIR/out" 2>&1 &
|
||||
pid=$!
|
||||
echo "$pid" >"$FAKE_DIR/applet.pid"
|
||||
rc=0
|
||||
wait "$pid" || rc=$?
|
||||
[[ $rc -eq 143 ]] || fail "exit status $rc: $(cat "$BATS_TEST_TMPDIR/out")"
|
||||
log_has 'apt-mark unhold su-dummy-b'
|
||||
run cat "$FAKE_DIR/holds"
|
||||
assert_output ''
|
||||
[[ ! -e $STATE/holds.added ]] || fail "the journal was left behind: $(cat "$STATE/holds.added")"
|
||||
}
|
||||
|
||||
@test "the package manager does not inherit our lock descriptors" {
|
||||
[[ -d /proc/self/fd ]] || skip "no /proc"
|
||||
run skup --no-refresh <<<$'2-\n'
|
||||
assert_success
|
||||
log_has 'lock fds in the upgrade: 0'
|
||||
}
|
||||
|
||||
@test "statefile writes merge: a row another process added meanwhile is kept" {
|
||||
write_state "$HEADER" "apt${T}su-dummy-c${T}user${T}2026-01-01T00:00:00Z"
|
||||
# while this session computes the closure of su-dummy-a, another process trusts zsh
|
||||
FAKE_STATE_APPEND="apt${T}zsh${T}user${T}2026-02-02T00:00:00Z" run skup --no-refresh <<<$'1+\nq'
|
||||
assert_success
|
||||
run cut -f2,3 "$STATE_FILE"
|
||||
assert_line "su-dummy-a${T}user"
|
||||
assert_line "su-dummy-b${T}dep:su-dummy-a"
|
||||
assert_line "su-dummy-c${T}user"
|
||||
assert_line "zsh${T}user"
|
||||
}
|
||||
|
||||
@test "statefile writes merge: a row another process removed meanwhile stays removed" {
|
||||
write_state "$HEADER" "apt${T}su-dummy-c${T}user${T}2026-01-01T00:00:00Z"
|
||||
FAKE_STATE_SED='/su-dummy-c/d' run skup trust su-dummy-a
|
||||
assert_success
|
||||
run cut -f2 "$STATE_FILE"
|
||||
refute_line su-dummy-c
|
||||
assert_line su-dummy-a
|
||||
}
|
||||
|
||||
@test "statefile writes merge: a removal applies only to the row as this session saw it" {
|
||||
write_state "$HEADER" "apt${T}su-dummy-a${T}user${T}2026-01-01T00:00:00Z" \
|
||||
"apt${T}su-dummy-b${T}dep:su-dummy-a${T}2026-01-01T00:00:00Z" \
|
||||
"apt${T}stale${T}dep:su-dummy-a${T}2026-01-01T00:00:00Z"
|
||||
# refresh drops "stale" (no longer in the closure); meanwhile another process made it a user row
|
||||
FAKE_STATE_SED='s/^apt\tstale\tdep:su-dummy-a/apt\tstale\tuser/' run skup refresh
|
||||
assert_success
|
||||
assert_output 'refreshed the dependency closures of 1 trusted root: 0 added, 1 removed'
|
||||
run cut -f2,3 "$STATE_FILE"
|
||||
assert_line "stale${T}user"
|
||||
assert_line "su-dummy-b${T}dep:su-dummy-a"
|
||||
}
|
||||
|
||||
@test "the statefile lock is flock(1) on trusted.lock, released after the write" {
|
||||
run skup trust su-dummy-c
|
||||
assert_success
|
||||
[[ -f $STATE/trusted.lock ]]
|
||||
flock -n "$STATE/trusted.lock" true
|
||||
}
|
||||
|
||||
@test "without flock(1) a mkdir lock is used and removed again" {
|
||||
rm -f "$SYSBIN/flock"
|
||||
run skup trust su-dummy-c
|
||||
assert_success
|
||||
[[ ! -e $STATE/trusted.lock ]]
|
||||
[[ ! -e $STATE/trusted.lock.d ]]
|
||||
run cut -f2 "$STATE_FILE"
|
||||
assert_line su-dummy-c
|
||||
}
|
||||
|
||||
@test "mkdir lock: a stale lock (dead owner) is taken over, a live one is respected" {
|
||||
rm -f "$SYSBIN/flock"
|
||||
mkdir -p "$STATE/holds.lock.d"
|
||||
echo su-dummy-b >"$STATE/holds.added"
|
||||
echo su-dummy-b >"$FAKE_DIR/holds"
|
||||
# live owner: this test's own shell
|
||||
echo "$$" >"$STATE/holds.lock.d/pid"
|
||||
run skup list --no-refresh
|
||||
assert_success
|
||||
assert_output --partial 'another skeptical-updater run is using'
|
||||
[[ -e $STATE/holds.added ]]
|
||||
# dead owner
|
||||
sleep 0 &
|
||||
dead=$!
|
||||
wait "$dead"
|
||||
echo "$dead" >"$STATE/holds.lock.d/pid"
|
||||
run skup list --no-refresh
|
||||
assert_success
|
||||
assert_line 'Released temporary holds left over by an interrupted run: su-dummy-b'
|
||||
[[ ! -e $STATE/holds.added ]]
|
||||
[[ ! -e $STATE/holds.lock.d ]]
|
||||
}
|
||||
|
||||
@test "mkdir lock: an owner of another user (kill -0 fails with EPERM) counts as alive" {
|
||||
nobody_env
|
||||
chown 65534 "$W/state"
|
||||
# the lock is held by this test's shell (root): nobody cannot signal it
|
||||
mkdir "$W/state/trusted.lock.d"
|
||||
echo "$$" >"$W/state/trusted.lock.d/pid"
|
||||
chown -R 65534 "$W/state/trusted.lock.d"
|
||||
SKEPTICAL_UPDATER_LOCK_WAIT=1 run nobody trust su-dummy-c
|
||||
assert_failure 1
|
||||
assert_output --partial "another skeptical-updater is writing $W/state/trusted.tsv"
|
||||
[[ $(cat "$W/state/trusted.lock.d/pid") == "$$" ]]
|
||||
[[ ! -e $W/state/trusted.tsv ]]
|
||||
rm -rf "$W"
|
||||
}
|
||||
|
||||
@test "mkdir lock: a stale lock nobody can delete is moved aside (taken over), without a busy loop" {
|
||||
nobody_env
|
||||
chown 65534 "$W/state"
|
||||
sleep 0 &
|
||||
dead=$!
|
||||
wait "$dead"
|
||||
# a dead owner, and the lock directory belongs to root: nobody cannot delete it, only rename it
|
||||
mkdir "$W/state/trusted.lock.d"
|
||||
echo "$dead" >"$W/state/trusted.lock.d/pid"
|
||||
SECONDS=0
|
||||
SKEPTICAL_UPDATER_LOCK_WAIT=2 run timeout 30 bash -c "$(declare -f nobody); W=$W FAKES=$FAKES SU_BIN=$SU_BIN; nobody trust su-dummy-c"
|
||||
assert_success
|
||||
assert_output 'trusted su-dummy-c'
|
||||
((SECONDS < 20))
|
||||
[[ ! -e $W/state/trusted.lock.d ]]
|
||||
rm -rf "$W"
|
||||
}
|
||||
|
||||
@test "mkdir lock: a live lock that never goes away ends in the timeout" {
|
||||
rm -f "$SYSBIN/flock"
|
||||
mkdir -p "$STATE/trusted.lock.d"
|
||||
echo "$$" >"$STATE/trusted.lock.d/pid"
|
||||
SECONDS=0
|
||||
SKEPTICAL_UPDATER_LOCK_WAIT=2 run timeout 30 env PATH="$TEST_PATH" "$SU_BIN" trust su-dummy-c
|
||||
assert_failure 1
|
||||
assert_output --partial 'another skeptical-updater is writing'
|
||||
((SECONDS < 20))
|
||||
[[ $(cat "$STATE/trusted.lock.d/pid") == "$$" ]]
|
||||
}
|
||||
|
||||
@test "mkdir lock: the lock directory appears with its pid, no temporary directory is left behind" {
|
||||
rm -f "$SYSBIN/flock"
|
||||
FAKE_STATE_APPEND='' run skup trust su-dummy-c
|
||||
assert_success
|
||||
run ls -A "$STATE"
|
||||
assert_output 'trusted.tsv'
|
||||
}
|
||||
|
||||
@test "mkdir lock: two takeovers of one stale lock - the late one finds the new live lock and leaves it" {
|
||||
rm -f "$SYSBIN/flock"
|
||||
mkdir -p "$STATE/holds.lock.d"
|
||||
sleep 0 &
|
||||
dead=$!
|
||||
wait "$dead"
|
||||
echo "$dead" >"$STATE/holds.lock.d/pid"
|
||||
echo su-dummy-b >"$STATE/holds.added"
|
||||
echo su-dummy-b >"$FAKE_DIR/holds"
|
||||
# "C" sees the dead owner, then pauses before its takeover ...
|
||||
SKEPTICAL_UPDATER_LOCK_WAIT=2 SKEPTICAL_UPDATER_LOCK_TAKEOVER_PAUSE=2 env PATH="$TEST_PATH" "$SU_BIN" --no-refresh <<<$'2-\n' >"$BATS_TEST_TMPDIR/c.out" 2>&1 &
|
||||
c=$!
|
||||
sleep 1
|
||||
# ... meanwhile "B" took the stale lock over: the lock now belongs to a live process (this shell)
|
||||
rm -rf "$STATE/holds.lock.d"
|
||||
mkdir "$STATE/holds.lock.d"
|
||||
echo "$$" >"$STATE/holds.lock.d/pid"
|
||||
rc=0
|
||||
wait "$c" || rc=$?
|
||||
# C must not have removed B's live lock (it does not get it, and neither do its holds)
|
||||
[[ $(cat "$STATE/holds.lock.d/pid") == "$$" ]] || fail "the live lock was taken: $(cat "$BATS_TEST_TMPDIR/c.out")"
|
||||
run ls -A "$STATE"
|
||||
refute_output --partial 'stale'
|
||||
rm -rf "$STATE/holds.lock.d"
|
||||
}
|
||||
|
||||
@test "mkdir lock: temporary and renamed lock directories of crashed acquirers are removed" {
|
||||
rm -f "$SYSBIN/flock"
|
||||
mkdir -p "$STATE/.trusted.lock.Crash1" "$STATE/.trusted.lock.Crash2" "$STATE/trusted.lock.d.stale.999999" "$STATE/.trusted.lock.Live01"
|
||||
sleep 0 &
|
||||
dead=$!
|
||||
wait "$dead"
|
||||
echo "$dead" >"$STATE/.trusted.lock.Crash1/pid"
|
||||
touch -d '2 hours ago' "$STATE/.trusted.lock.Crash2"
|
||||
echo "$dead" >"$STATE/trusted.lock.d.stale.999999/pid"
|
||||
echo "$$" >"$STATE/.trusted.lock.Live01/pid"
|
||||
run skup trust su-dummy-c
|
||||
assert_success
|
||||
run ls -A "$STATE"
|
||||
assert_output "$(printf '%s\n' .trusted.lock.Live01 trusted.tsv)"
|
||||
}
|
||||
|
||||
@test "a state directory that cannot be entered is an error, not 'no trusted packages'" {
|
||||
((EUID == 0)) || skip "needs root to switch to an unprivileged user"
|
||||
command -v setpriv >/dev/null 2>&1 || skip "setpriv not available"
|
||||
w=$(mktemp -d /tmp/su-nobody.XXXXXX)
|
||||
chmod 0755 "$w"
|
||||
mkdir -p "$w/bin" "$w/fake" "$w/tmp" "$w/state"
|
||||
chmod 0777 "$w/fake" "$w/tmp"
|
||||
for t in "$SYSBIN"/*; do ln -s "$(readlink "$t")" "$w/bin/${t##*/}"; done
|
||||
: >"$w/fake/log"
|
||||
chmod 0666 "$w/fake/log"
|
||||
printf '%s\n' "$HEADER" >"$w/state/trusted.tsv"
|
||||
chmod 0000 "$w/state"
|
||||
nobody() {
|
||||
setpriv --reuid=65534 --regid=65534 --clear-groups -- env -u SKEPTICAL_UPDATER_EUID \
|
||||
PATH="$FAKES/apt:$w/bin" FAKE_DIR="$w/fake" FAKE_LOG="$w/fake/log" TMPDIR="$w/tmp" \
|
||||
SKEPTICAL_UPDATER_STATE_DIR="$w/state" "$SU_BIN" "$@"
|
||||
}
|
||||
run nobody trusted
|
||||
assert_failure 1
|
||||
assert_output --partial "cannot access the state directory $w/state (permission denied)"
|
||||
run nobody list --no-refresh
|
||||
assert_failure 1
|
||||
assert_output --partial "cannot access the state directory $w/state (permission denied)"
|
||||
# a readable directory with an unreadable statefile
|
||||
chmod 0755 "$w/state"
|
||||
chmod 0600 "$w/state/trusted.tsv"
|
||||
run nobody trusted
|
||||
assert_failure 1
|
||||
assert_output --partial "cannot read the statefile $w/state/trusted.tsv (permission denied)"
|
||||
rm -rf "$w"
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
#!/usr/bin/env bats
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
# Copyright (C) 2026 Bryan Joshua Pedini
|
||||
|
||||
# Root handling: what needs root, sudo re-exec with identical arguments, clear failures,
|
||||
# and the commands that must work unprivileged.
|
||||
|
||||
load test_helper
|
||||
|
||||
setup() {
|
||||
common_setup
|
||||
export SKEPTICAL_UPDATER_EUID=1000
|
||||
}
|
||||
|
||||
@test "the interactive upgrade as non-root without sudo fails clearly (exit 1)" {
|
||||
run skup <<<''
|
||||
assert_failure 1
|
||||
assert_output --partial 'upgrading needs root and sudo is not available; run skeptical-updater as root (or use --dry-run)'
|
||||
[[ -z $(cat "$FAKE_LOG") ]]
|
||||
}
|
||||
|
||||
@test "list with a refresh needs root; without sudo it fails clearly" {
|
||||
run skup list
|
||||
assert_failure 1
|
||||
assert_output --partial 'refreshing the package lists needs root (use --no-refresh)'
|
||||
log_lacks 'update'
|
||||
}
|
||||
|
||||
@test "list --no-refresh works unprivileged" {
|
||||
run skup list --no-refresh
|
||||
assert_success
|
||||
assert_line '3 upgradable, 0 trusted (hidden, use --all to show), 3 to review:'
|
||||
}
|
||||
|
||||
@test "trusted, info and show work unprivileged" {
|
||||
write_state "$HEADER" "apt${T}su-dummy-a${T}user${T}2026-10-09T09:40:12Z"
|
||||
run skup trusted
|
||||
assert_success
|
||||
assert_line "apt${T}su-dummy-a${T}user${T}2026-10-09T09:40:12Z"
|
||||
run skup info su-dummy-b
|
||||
assert_success
|
||||
assert_line ' required by (installed): su-dummy-a'
|
||||
run skup show su-dummy-a
|
||||
assert_success
|
||||
assert_line 'Package: su-dummy-a'
|
||||
}
|
||||
|
||||
@test "--dry-run works unprivileged: the refresh is printed, not run" {
|
||||
run skup --dry-run <<<''
|
||||
assert_success
|
||||
assert_line 'Refreshing package lists... skipped (not root), the command would be:'
|
||||
assert_line '+ apt-get update -qq -o APT::Update::Error-Mode=any'
|
||||
assert_line '+ apt-get -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold -o DPkg::Lock::Timeout=60 --with-new-pkgs upgrade'
|
||||
log_lacks '^apt-get update'
|
||||
log_lacks '^apt-get -y'
|
||||
}
|
||||
|
||||
@test "with sudo available, non-root re-execs through sudo: this file itself, same arguments, hidden marker" {
|
||||
use_fakes apt sudo
|
||||
run skup --all list
|
||||
assert_success
|
||||
log_has "sudo -- $SU_BIN --sudo-reexec --state-dir=$STATE --all list"
|
||||
log_lacks '^apt-get'
|
||||
}
|
||||
|
||||
@test "the sudo re-exec carries --no-color when NO_COLOR is set" {
|
||||
use_fakes apt sudo
|
||||
unset SKEPTICAL_UPDATER_STATE_DIR
|
||||
NO_COLOR=1 run skup --backend=apt
|
||||
assert_success
|
||||
log_has "sudo -- $SU_BIN --sudo-reexec --no-color --backend=apt"
|
||||
}
|
||||
|
||||
@test "an explicit --state-dir is passed through unchanged" {
|
||||
use_fakes apt sudo
|
||||
run skup --state-dir=/srv/x list
|
||||
assert_success
|
||||
log_has "sudo -- $SU_BIN --sudo-reexec --state-dir=/srv/x list"
|
||||
}
|
||||
|
||||
@test "a copy that is not executable and not in PATH is re-run through bash with its absolute path" {
|
||||
use_fakes apt sudo
|
||||
mkdir -p "$BATS_TEST_TMPDIR/copy"
|
||||
cp "$SU_BIN" "$BATS_TEST_TMPDIR/copy/su-copy"
|
||||
chmod 0644 "$BATS_TEST_TMPDIR/copy/su-copy"
|
||||
cd "$BATS_TEST_TMPDIR/copy"
|
||||
run env PATH="$TEST_PATH" bash su-copy list
|
||||
assert_success
|
||||
log_matches "^sudo -- /[^ ]*/bash $BATS_TEST_TMPDIR/copy/su-copy --sudo-reexec --state-dir=$STATE list\$"
|
||||
}
|
||||
|
||||
@test "an executable copy invoked through bash is still re-run directly (sudoers rules match the file)" {
|
||||
use_fakes apt sudo
|
||||
mkdir -p "$BATS_TEST_TMPDIR/copy"
|
||||
cp "$SU_BIN" "$BATS_TEST_TMPDIR/copy/su-copy"
|
||||
chmod 0755 "$BATS_TEST_TMPDIR/copy/su-copy"
|
||||
run env PATH="$TEST_PATH" bash "$BATS_TEST_TMPDIR/copy/su-copy" list
|
||||
assert_success
|
||||
log_has "sudo -- $BATS_TEST_TMPDIR/copy/su-copy --sudo-reexec --state-dir=$STATE list"
|
||||
}
|
||||
|
||||
@test "after a sudo re-exec that did not make us root, there is no second re-exec" {
|
||||
use_fakes apt sudo
|
||||
run skup --sudo-reexec list
|
||||
assert_failure 1
|
||||
assert_output --partial 're-executed through sudo but still not running as root (euid 1000); not trying again'
|
||||
log_lacks '^sudo'
|
||||
}
|
||||
|
||||
@test "--sudo-reexec is only recognised as the first argument and is never forwarded" {
|
||||
run skup list --sudo-reexec
|
||||
assert_failure 1
|
||||
assert_output --partial 'unknown option: --sudo-reexec'
|
||||
SKEPTICAL_UPDATER_EUID=0 run skup --sudo-reexec list --no-refresh
|
||||
assert_success
|
||||
log_has 'apt-get -s --with-new-pkgs upgrade'
|
||||
log_lacks 'sudo-reexec'
|
||||
}
|
||||
|
||||
@test "trust into a writable state dir does not need root" {
|
||||
use_fakes apt sudo
|
||||
run skup trust su-dummy-c
|
||||
assert_success
|
||||
assert_output 'trusted su-dummy-c'
|
||||
log_lacks '^sudo'
|
||||
}
|
||||
|
||||
@test "trust into a non-writable state dir re-execs through sudo" {
|
||||
ro=$BATS_TEST_DIRNAME/no-such-state-dir
|
||||
[[ -w $BATS_TEST_DIRNAME ]] && skip "the repository is writable here (the suite mounts it read-only in Docker)"
|
||||
use_fakes apt sudo
|
||||
run skup --state-dir="$ro" trust su-dummy-c
|
||||
assert_success
|
||||
log_has "sudo -- $SU_BIN --sudo-reexec --state-dir=$ro trust su-dummy-c"
|
||||
}
|
||||
|
||||
@test "--dry-run never re-execs through sudo" {
|
||||
use_fakes apt sudo
|
||||
run skup --dry-run list
|
||||
assert_success
|
||||
log_lacks '^sudo'
|
||||
}
|
||||
|
||||
@test "as root nothing is re-executed" {
|
||||
use_fakes apt sudo
|
||||
SKEPTICAL_UPDATER_EUID=0 run skup list
|
||||
assert_success
|
||||
log_lacks '^sudo'
|
||||
log_has 'apt-get update -qq -o APT::Update::Error-Mode=any'
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
#!/usr/bin/env bats
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
# Copyright (C) 2026 Bryan Joshua Pedini
|
||||
|
||||
# The statefile: creation, format, sorting, dedupe, comments, atomic writes, trust/untrust/refresh.
|
||||
|
||||
load test_helper
|
||||
|
||||
setup() { common_setup; }
|
||||
|
||||
@test "trust creates the directory (0755) and the file (0644) with the header" {
|
||||
run skup trust su-dummy-c
|
||||
assert_success
|
||||
assert_output 'trusted su-dummy-c'
|
||||
[[ $(stat -c %a "$STATE") == 755 ]]
|
||||
[[ $(stat -c %a "$STATE_FILE") == 644 ]]
|
||||
run cat "$STATE_FILE"
|
||||
assert_line --index 0 "$HEADER"
|
||||
assert_line --index 1 --regexp "^apt${T}su-dummy-c${T}user${T}${DATE_RE}\$"
|
||||
[[ ${#lines[@]} -eq 2 ]]
|
||||
}
|
||||
|
||||
@test "intermediate directories are created too" {
|
||||
run skup --state-dir="$BATS_TEST_TMPDIR/a/b/c" trust su-dummy-c
|
||||
assert_success
|
||||
[[ -f $BATS_TEST_TMPDIR/a/b/c/trusted.tsv ]]
|
||||
[[ $(stat -c %a "$BATS_TEST_TMPDIR/a/b/c") == 755 ]]
|
||||
}
|
||||
|
||||
@test "trust writes the user row and dep:<root> rows for the installed closure" {
|
||||
run skup trust su-dummy-a
|
||||
assert_success
|
||||
assert_output 'trusted su-dummy-a (+1 dependency: su-dummy-b)'
|
||||
run cat "$STATE_FILE"
|
||||
assert_line --index 1 --regexp "^apt${T}su-dummy-a${T}user${T}${DATE_RE}\$"
|
||||
assert_line --index 2 --regexp "^apt${T}su-dummy-b${T}dep:su-dummy-a${T}${DATE_RE}\$"
|
||||
log_has "dpkg-query -W -f \${db:Status-Abbrev}\\t\${Package}\\t\${Pre-Depends}\\t\${Depends}\\t\${Provides}\\n"
|
||||
}
|
||||
|
||||
@test "the apt closure follows Provides (virtual packages) and alternatives" {
|
||||
run skup trust su-dummy-w
|
||||
assert_success
|
||||
assert_output 'trusted su-dummy-w (+1 dependency: su-dummy-v)'
|
||||
}
|
||||
|
||||
@test "the apt closure walks real chains through virtual packages (base-files -> awk -> mawk)" {
|
||||
run skup trust base-files
|
||||
assert_success
|
||||
assert_output 'trusted base-files (+4 dependencies: gcc-12-base libc6 libgcc-s1 mawk)'
|
||||
run cut -f2,3 "$STATE_FILE"
|
||||
assert_line "mawk${T}dep:base-files"
|
||||
assert_line "gcc-12-base${T}dep:base-files"
|
||||
}
|
||||
|
||||
@test "a not-installed (rc) package is not part of a closure" {
|
||||
run skup trust su-dummy-f
|
||||
assert_success
|
||||
assert_output 'trusted su-dummy-f'
|
||||
}
|
||||
|
||||
@test "records are sorted on (backend, package); comments are kept in order" {
|
||||
write_state '# my own note' \
|
||||
"pacman${T}zsh${T}user${T}2026-01-01T00:00:00Z" \
|
||||
"apt${T}zsh${T}user${T}2026-01-01T00:00:00Z" \
|
||||
'# second note' \
|
||||
"apt${T}foo-bar${T}user${T}2026-01-01T00:00:00Z" \
|
||||
"apt${T}foo${T}dep:zsh${T}2026-01-01T00:00:00Z" \
|
||||
"dnf${T}abc${T}user${T}2026-01-01T00:00:00Z"
|
||||
run skup trust su-dummy-c
|
||||
assert_success
|
||||
run cat "$STATE_FILE"
|
||||
assert_line --index 0 '# my own note'
|
||||
assert_line --index 1 '# second note'
|
||||
assert_line --index 2 "apt${T}foo${T}dep:zsh${T}2026-01-01T00:00:00Z"
|
||||
assert_line --index 3 "apt${T}foo-bar${T}user${T}2026-01-01T00:00:00Z"
|
||||
assert_line --index 4 --regexp "^apt${T}su-dummy-c${T}user${T}"
|
||||
assert_line --index 5 "apt${T}zsh${T}user${T}2026-01-01T00:00:00Z"
|
||||
assert_line --index 6 "dnf${T}abc${T}user${T}2026-01-01T00:00:00Z"
|
||||
assert_line --index 7 "pacman${T}zsh${T}user${T}2026-01-01T00:00:00Z"
|
||||
[[ ${#lines[@]} -eq 8 ]]
|
||||
}
|
||||
|
||||
@test "duplicates are collapsed: a user row wins over a dep row, else the first one" {
|
||||
write_state "$HEADER" \
|
||||
"apt${T}b${T}dep:x${T}2026-01-01T00:00:00Z" \
|
||||
"apt${T}b${T}user${T}2026-02-02T00:00:00Z" \
|
||||
"apt${T}c${T}dep:x${T}2026-03-03T00:00:00Z" \
|
||||
"apt${T}c${T}dep:y${T}2026-04-04T00:00:00Z"
|
||||
run skup trust su-dummy-c
|
||||
assert_success
|
||||
run cat "$STATE_FILE"
|
||||
assert_line "apt${T}b${T}user${T}2026-02-02T00:00:00Z"
|
||||
assert_line "apt${T}c${T}dep:x${T}2026-03-03T00:00:00Z"
|
||||
refute_line --partial 'dep:y'
|
||||
refute_line --partial "b${T}dep:x"
|
||||
}
|
||||
|
||||
@test "a user row is never downgraded to dep:" {
|
||||
run skup trust su-dummy-b
|
||||
run skup trust su-dummy-a
|
||||
assert_success
|
||||
assert_output 'trusted su-dummy-a (its installed dependency was already trusted)'
|
||||
run cut -f2,3 "$STATE_FILE"
|
||||
assert_line "su-dummy-b${T}user"
|
||||
refute_line --partial 'dep:'
|
||||
}
|
||||
|
||||
@test "an existing dep: row is upgraded to user, keeping one row" {
|
||||
run skup trust su-dummy-a
|
||||
run skup trust su-dummy-b
|
||||
assert_success
|
||||
assert_output 'trusted su-dummy-b'
|
||||
run cut -f2,3 "$STATE_FILE"
|
||||
assert_line "su-dummy-b${T}user"
|
||||
[[ $(grep -c su-dummy-b "$STATE_FILE") -eq 1 ]]
|
||||
}
|
||||
|
||||
@test "trusting an already trusted package reports it and changes nothing" {
|
||||
run skup trust su-dummy-c
|
||||
before=$(cat "$STATE_FILE")
|
||||
run skup trust su-dummy-c
|
||||
assert_success
|
||||
assert_output 'su-dummy-c was already trusted'
|
||||
[[ $(cat "$STATE_FILE") == "$before" ]]
|
||||
}
|
||||
|
||||
@test "malformed lines are kept verbatim, with a warning" {
|
||||
write_state "$HEADER" "apt${T}only-two" "apt${T}x${T}weird${T}2026-01-01T00:00:00Z"
|
||||
run skup trust su-dummy-c
|
||||
assert_success
|
||||
assert_output --partial "trusted.tsv:2: malformed line kept verbatim: apt${T}only-two"
|
||||
assert_output --partial "trusted.tsv:3: malformed line kept verbatim"
|
||||
run cat "$STATE_FILE"
|
||||
assert_line "apt${T}only-two"
|
||||
assert_line "apt${T}x${T}weird${T}2026-01-01T00:00:00Z"
|
||||
}
|
||||
|
||||
@test "blank lines and CRLF line ends are tolerated" {
|
||||
printf '%s\r\n\r\n%s\r\n\n' "$HEADER" "apt${T}su-dummy-a${T}user${T}2026-01-01T00:00:00Z" >"$BATS_TEST_TMPDIR/st"
|
||||
mkdir -p "$STATE"
|
||||
mv "$BATS_TEST_TMPDIR/st" "$STATE_FILE"
|
||||
run skup list --no-refresh
|
||||
assert_line '3 upgradable, 1 trusted (hidden, use --all to show), 2 to review:'
|
||||
}
|
||||
|
||||
@test "writes are atomic (new file renamed over the old one) and leave no temp file" {
|
||||
run skup trust su-dummy-c
|
||||
ino1=$(stat -c %i "$STATE_FILE")
|
||||
run skup trust su-dummy-a
|
||||
assert_success
|
||||
ino2=$(stat -c %i "$STATE_FILE")
|
||||
[[ $ino1 != "$ino2" ]]
|
||||
run ls -A "$STATE"
|
||||
assert_output "$(printf '%s\n' trusted.lock trusted.tsv)"
|
||||
}
|
||||
|
||||
@test "a failing write leaves the old file intact" {
|
||||
run skup trust su-dummy-c
|
||||
before=$(cat "$STATE_FILE")
|
||||
# make the state dir read-only (root ignores directory modes: skip then)
|
||||
chmod 0555 "$STATE"
|
||||
if [[ -w $STATE ]]; then
|
||||
chmod 0755 "$STATE"
|
||||
skip "running as root: directory modes are not enforced"
|
||||
fi
|
||||
run skup trust su-dummy-a
|
||||
chmod 0755 "$STATE"
|
||||
assert_failure 1
|
||||
[[ $(cat "$STATE_FILE") == "$before" ]]
|
||||
}
|
||||
|
||||
@test "untrust removes the row for PKG and every dep:PKG row, nothing else" {
|
||||
write_state "$HEADER" \
|
||||
"apt${T}su-dummy-a${T}user${T}2026-01-01T00:00:00Z" \
|
||||
"apt${T}su-dummy-b${T}dep:su-dummy-a${T}2026-01-01T00:00:00Z" \
|
||||
"apt${T}x${T}dep:y${T}2026-01-01T00:00:00Z" \
|
||||
"apt${T}su-dummy-c${T}user${T}2026-01-01T00:00:00Z" \
|
||||
"pacman${T}su-dummy-a${T}user${T}2026-01-01T00:00:00Z" \
|
||||
"pacman${T}su-dummy-b${T}dep:su-dummy-a${T}2026-01-01T00:00:00Z"
|
||||
run skup untrust su-dummy-a
|
||||
assert_success
|
||||
assert_output 'untrusted su-dummy-a (2 rows removed)'
|
||||
run cat "$STATE_FILE"
|
||||
assert_output "$(printf '%s\n' "$HEADER" \
|
||||
"apt${T}su-dummy-c${T}user${T}2026-01-01T00:00:00Z" \
|
||||
"apt${T}x${T}dep:y${T}2026-01-01T00:00:00Z" \
|
||||
"pacman${T}su-dummy-a${T}user${T}2026-01-01T00:00:00Z" \
|
||||
"pacman${T}su-dummy-b${T}dep:su-dummy-a${T}2026-01-01T00:00:00Z")"
|
||||
}
|
||||
|
||||
@test "untrust of a dependency-only package removes its dep row" {
|
||||
run skup trust su-dummy-a
|
||||
run skup untrust su-dummy-b
|
||||
assert_success
|
||||
assert_output 'untrusted su-dummy-b (1 row removed)'
|
||||
run cut -f2 "$STATE_FILE"
|
||||
assert_line su-dummy-a
|
||||
refute_line su-dummy-b
|
||||
}
|
||||
|
||||
@test "untrust of a package that is not trusted warns, exit 0" {
|
||||
run skup untrust nope
|
||||
assert_success
|
||||
assert_output --partial 'warning: nope is not trusted (no row for backend apt)'
|
||||
[[ ! -e $STATE_FILE ]]
|
||||
}
|
||||
|
||||
@test "refresh recomputes closures: adds missing rows, drops stale ones, keeps the rest" {
|
||||
write_state "$HEADER" \
|
||||
"apt${T}su-dummy-a${T}user${T}2026-01-01T00:00:00Z" \
|
||||
"apt${T}su-dummy-b${T}dep:su-dummy-a${T}2026-01-01T00:00:00Z" \
|
||||
"apt${T}stale${T}dep:su-dummy-a${T}2026-01-01T00:00:00Z" \
|
||||
"apt${T}orphan${T}dep:gone${T}2026-01-01T00:00:00Z" \
|
||||
"apt${T}su-dummy-w${T}user${T}2026-01-01T00:00:00Z" \
|
||||
"pacman${T}stale${T}dep:su-dummy-a${T}2026-01-01T00:00:00Z"
|
||||
run skup refresh
|
||||
assert_success
|
||||
assert_output 'refreshed the dependency closures of 2 trusted roots: 1 added, 2 removed'
|
||||
run cat "$STATE_FILE"
|
||||
assert_line "apt${T}su-dummy-b${T}dep:su-dummy-a${T}2026-01-01T00:00:00Z"
|
||||
assert_line --regexp "^apt${T}su-dummy-v${T}dep:su-dummy-w${T}${DATE_RE}\$"
|
||||
refute_line --partial "apt${T}stale"
|
||||
refute_line --partial 'orphan'
|
||||
assert_line "pacman${T}stale${T}dep:su-dummy-a${T}2026-01-01T00:00:00Z"
|
||||
assert_line "apt${T}su-dummy-a${T}user${T}2026-01-01T00:00:00Z"
|
||||
}
|
||||
|
||||
@test "refresh with no user roots for this backend does nothing" {
|
||||
write_state "$HEADER" "pacman${T}x${T}user${T}2026-01-01T00:00:00Z"
|
||||
run skup refresh
|
||||
assert_success
|
||||
assert_output 'No trusted root packages for backend apt: nothing to refresh.'
|
||||
}
|
||||
|
||||
@test "trusted prints the statefile verbatim" {
|
||||
write_state "$HEADER" '# note' "apt${T}x${T}user${T}2026-01-01T00:00:00Z"
|
||||
run skup trusted
|
||||
assert_success
|
||||
assert_output "$(printf '%s\n' "$HEADER" '# note' "apt${T}x${T}user${T}2026-01-01T00:00:00Z")"
|
||||
}
|
||||
|
||||
@test "trusted without a statefile says so, exit 0" {
|
||||
run skup trusted
|
||||
assert_success
|
||||
assert_output "No trusted packages yet ($STATE_FILE does not exist)."
|
||||
}
|
||||
|
||||
@test "only rows of the current backend hide packages" {
|
||||
write_state "$HEADER" "pacman${T}su-dummy-a${T}user${T}2026-01-01T00:00:00Z"
|
||||
run skup list --no-refresh
|
||||
assert_line '3 upgradable, 0 trusted (hidden, use --all to show), 3 to review:'
|
||||
assert_line --regexp '^ \[1\] su-dummy-a '
|
||||
}
|
||||
|
||||
@test "the reason does not matter for hiding: dep rows hide too" {
|
||||
write_state "$HEADER" "apt${T}su-dummy-b${T}dep:whatever${T}2026-01-01T00:00:00Z"
|
||||
run skup list --no-refresh
|
||||
assert_line '3 upgradable, 1 trusted (hidden, use --all to show), 2 to review:'
|
||||
refute_output --partial 'su-dummy-b'
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
# Copyright (C) 2026 Bryan Joshua Pedini
|
||||
|
||||
# tests/unit/test_helper.bash - common setup for the unit tests (sourced via `load test_helper`).
|
||||
# The applet runs with a hermetic PATH: fake package managers (tests/unit/fakes/<dir>) in front
|
||||
# of a directory with symlinks to the few system tools it needs. No root, no network, no real
|
||||
# package manager; everything is written under $BATS_TEST_TMPDIR.
|
||||
# shellcheck shell=bash
|
||||
|
||||
load '../vendor/bats-support/load'
|
||||
load '../vendor/bats-assert/load'
|
||||
|
||||
SU_BIN=$(cd "$BATS_TEST_DIRNAME/../.." && pwd)/skeptical-updater
|
||||
FAKES=$BATS_TEST_DIRNAME/fakes
|
||||
export FAKE_DATA=$BATS_TEST_DIRNAME/data
|
||||
SYS_TOOLS=(bash env cat sort sed grep tr cut join awk mktemp mv mkdir chmod rm cp ln date ls head tail wc touch sleep kill flock readlink timeout stat sha256sum)
|
||||
|
||||
common_setup() {
|
||||
SYSBIN=$BATS_TEST_TMPDIR/sysbin
|
||||
mkdir -p "$SYSBIN"
|
||||
local t p
|
||||
for t in "${SYS_TOOLS[@]}"; do
|
||||
p=$(type -P "$t") || continue
|
||||
ln -sf "$p" "$SYSBIN/$t"
|
||||
done
|
||||
export FAKE_DIR=$BATS_TEST_TMPDIR/fake
|
||||
mkdir -p "$FAKE_DIR"
|
||||
: >"$FAKE_DIR/holds"
|
||||
export FAKE_LOG=$FAKE_DIR/log
|
||||
: >"$FAKE_LOG"
|
||||
export STATE=$BATS_TEST_TMPDIR/state
|
||||
export STATE_FILE=$STATE/trusted.tsv
|
||||
export SKEPTICAL_UPDATER_STATE_DIR=$STATE
|
||||
export SKEPTICAL_UPDATER_EUID=0
|
||||
export SKEPTICAL_UPDATER_OS_RELEASE=$FAKE_DATA/os-release-debian12
|
||||
export TMPDIR=$BATS_TEST_TMPDIR
|
||||
export FAKE_PACMAN_DBPATH=$FAKE_DIR/pacman-db
|
||||
mkdir -p "$FAKE_PACMAN_DBPATH/sync" "$FAKE_PACMAN_DBPATH/local"
|
||||
unset NO_COLOR ZYPP_CONF COLUMNS SKEPTICAL_UPDATER_LOCK_WAIT
|
||||
use_fakes apt
|
||||
}
|
||||
|
||||
# use_fakes DIR...: the applet sees only these fake dirs + the system tools
|
||||
use_fakes() {
|
||||
local d p=
|
||||
for d in "$@"; do p+=$FAKES/$d:; done
|
||||
TEST_PATH=$p$SYSBIN
|
||||
}
|
||||
|
||||
# skup ARGS...: run the applet (use as `run skup ...`, stdin is passed through)
|
||||
skup() { env PATH="$TEST_PATH" "$SU_BIN" "$@"; }
|
||||
|
||||
# shellcheck disable=SC2034 # used by the .bats files
|
||||
HEADER='# skeptical-updater trusted packages. Columns: backend package reason added(UTC ISO-8601)'
|
||||
# shellcheck disable=SC2034
|
||||
DATE_RE='[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z'
|
||||
# shellcheck disable=SC2034
|
||||
T=$'\t'
|
||||
|
||||
# write_state LINE...: create the statefile with exactly these lines
|
||||
write_state() {
|
||||
mkdir -p "$STATE"
|
||||
printf '%s\n' "$@" >"$STATE_FILE"
|
||||
}
|
||||
|
||||
# log_has LINE: the fake log contains exactly this line
|
||||
log_has() {
|
||||
grep -qxF -- "$1" "$FAKE_LOG" || {
|
||||
printf 'expected log line: %s\n--- fake log:\n%s\n' "$1" "$(cat "$FAKE_LOG")" >&2
|
||||
return 1
|
||||
}
|
||||
}
|
||||
# log_matches REGEX: some fake log line matches
|
||||
log_matches() {
|
||||
grep -qE -- "$1" "$FAKE_LOG" || {
|
||||
printf 'expected a log line matching: %s\n--- fake log:\n%s\n' "$1" "$(cat "$FAKE_LOG")" >&2
|
||||
return 1
|
||||
}
|
||||
}
|
||||
# log_lacks REGEX: no fake log line matches
|
||||
log_lacks() {
|
||||
if grep -qE -- "$1" "$FAKE_LOG"; then
|
||||
printf 'unexpected log line matching: %s\n--- fake log:\n%s\n' "$1" "$(cat "$FAKE_LOG")" >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
Reference in new issue
Block a user