3 Commits
7 changed files with 313 additions and 78 deletions

No files matched your search

+9 -2
View File
@@ -59,7 +59,7 @@ options:
none (default: paru, else yay, when installed; other backends ignore it)
--no-aur same as --aur-helper=none
-h, --help this help
-V, --version print the version
-v, --version print the version
```
Exit status: `0` ok, `1` usage or backend error, `2` aborted (end of input),
@@ -131,7 +131,12 @@ package (pacman backend with paru or yay, see below) and a space on every other
One line, whitespace-separated tokens; `N` is the number in brackets. Numbers are
stable for the whole session (rows that leave the list leave a gap). Every token
of a line is checked for syntax first: an unknown token rejects the line. A number
that is not listed prints an error and the remaining commands still run.
or package name that is not listed prints an error and the remaining commands still run.
For `?` and `??`, a listed package name can replace the number, for example
`su-dummy-a?` or `su-dummy-a??`. A digits-only reference is interpreted as a number.
Names are restricted to these information commands to avoid unnecessary parsing
complexity: package names may contain `-` or end in `x`, which conflicts with the
other command suffixes.
| token | action |
|-------|--------|
@@ -148,6 +153,8 @@ that is not listed prints an error and the remaining commands still run.
| end of input | abort without upgrading (exit 2) |
Several tokens may share a line (`1? 3- 4+`); they run left to right. Consecutive
`Nx` commands such as `5x 30x 41x` are simulated and confirmed together. Another
command ends the group; a dependent can be included with its dependency. Consecutive
`N+` commands are processed together: selected packages required by another selected
package are recorded as dependencies, regardless of their order. `*+` works the same
way. With `--all`
+5
View File
@@ -126,6 +126,11 @@ Refreshing package lists... done.
Command grammar (one line, whitespace-separated tokens, each `N` is the number in brackets):
Unknown tokens reject the line; nonexistent numbers report an error and are skipped.
For `?` and `??`, the exact name of a listed package may replace `N`; a digits-only
reference is treated as a number. Missing or no longer listed names are reported and
skipped. Other commands accept only numbers: package names can contain `-` or end in
`x`, and limiting name references to information commands avoids unnecessary parsing
complexity.
Consecutive `N+` commands and `*+` trust the selected dependency roots, recording
other selected packages in their closures as dependencies regardless of input order.
+147 -62
View File
@@ -6,7 +6,7 @@
# and zypper: list what would be upgraded, interrogate every package, remember the
# ones you trust, then run the native upgrade (with per-run exclusions).
#
# Version: 0.2.0
# Version: 0.4.0
#
# Specification: docs/DESIGN.md. Single file, bash >= 4.3, no runtime dependency
# beyond the distro's package manager and coreutils/sed/grep (awk only on the apt
@@ -23,7 +23,7 @@ export LC_ALL=C DEBIAN_FRONTEND=noninteractive
# never read from or written to a file (no HISTFILE; `history -r`/`-w`/`-a` are never used).
unset HISTFILE
readonly SU_VERSION=0.2.0
readonly SU_VERSION=0.4.0
readonly SU_PROG=skeptical-updater
readonly SU_STATE_HEADER='# skeptical-updater trusted packages. Columns: backend package reason added(UTC ISO-8601)'
readonly SU_PROMPT_HELP='<enter> upgrade | N? what is it | N?? full info | N- exclude this run | Nx purge now | N+ trust forever'
@@ -105,7 +105,7 @@ options:
none (default: paru, else yay, when installed; other backends ignore it)
--no-aur same as --aur-helper=none
-h, --help this help
-V, --version print the version
-v, --version print the version
interactive commands (one line, whitespace-separated tokens, N = number in brackets):
<empty line> proceed: run the native upgrade with the current exclusions
@@ -114,9 +114,11 @@ interactive commands (one line, whitespace-separated tokens, N = number in brack
that depend on it - installed packages only
N?? full native info: the installed record; lines starting with '*' come
from the upstream candidate (differing values, fields it adds)
For N? and N??, a listed package name may be used instead of N.
N- exclude from this run (toggle); shown with a '-' marker
Nx purge it now with its now-unneeded dependencies (shown, asks y/N); refused
when other installed packages depend on it, like pacman -Rns
consecutive Nx commands are purged together after one confirmation
N+ trust it forever (with its installed dependency closure)
*+ / *- apply + / - to every listed package
l redraw the list
@@ -138,10 +140,12 @@ session_help() {
<enter> upgrade now, skipping the rows marked '-'
N? what is it: description, install reason + installed packages that need it
N?? full native info: installed record, '*' lines from the upstream candidate
A listed package name may replace N for ? and ?? (e.g. okular?).
N- exclude N from this run (toggle)
Nx purge N now: lists every package the purge removes, then asks y/N.
x refuses when other installed packages depend on the target, like
pacman -Rns; purge the dependents first
consecutive Nx commands are purged together after one confirmation
N+ trust N (and its installed dependencies) forever
*+ *- trust / exclude every listed package (*- again re-includes them)
l redraw the list (a '*' after the number marks an AUR package)
@@ -197,7 +201,7 @@ parse_args() {
usage
exit 0
;;
-V | --version)
-v | --version)
printf '%s %s\n' "$SU_PROG" "$SU_VERSION"
exit 0
;;
@@ -970,20 +974,21 @@ apt_closure() {
# is system-wide (it also takes pre-existing orphans, e.g. old kernels: verified), so the
# orphans that were already autoremovable before are subtracted and the rest named explicitly.
apt_purge_plan() {
local pkg=$1 pre sim w name rest
local -A before=()
local -a args=("$pkg")
local pre sim w name rest
local -A before=() requested=()
local -a args=("$@")
for name in "$@"; do requested[$name]=1; done
pre=$(apt-get -s autoremove </dev/null 2>/dev/null) || pre=''
while read -r w name rest; do
[[ $w == Remv || $w == Purg ]] && before[$name]=1
done <<<"$pre"
sim=$(apt-get -s purge --auto-remove "$pkg" </dev/null 2>&1) || {
sim=$(apt-get -s purge --auto-remove "$@" </dev/null 2>&1) || {
printf '%s\n' "$sim" >&2
return 1
}
while read -r w name rest; do
[[ $w == Remv || $w == Purg ]] || continue
[[ -z ${before[$name]+x} && $name != "$pkg" ]] && args+=("$name")
[[ -z ${before[$name]+x} && -z ${requested[$name]+x} ]] && args+=("$name")
done <<<"$sim"
sim=$(apt-get -s purge "${args[@]}" </dev/null 2>&1) || {
printf '%s\n' "$sim" >&2
@@ -1505,7 +1510,7 @@ pacman_closure() {
# On refusal pacman explains on stdout (":: removing X breaks dependency ..."): show all of it.
pacman_purge_plan() {
local out rc=0 l
out=$(COLUMNS=0 pacman -Rs --print --print-format '%n' -- "$1" </dev/null 2>&1) || rc=$?
out=$(COLUMNS=0 pacman -Rs --print --print-format '%n' -- "$@" </dev/null 2>&1) || rc=$?
if ((rc)); then
printf '%s\n' "$out" >&2
return "$rc"
@@ -1906,7 +1911,7 @@ dnf_purge_plan() {
local out line in=0 wrapped=0
local -a f=()
local arch_re='^(noarch|x86_64|x86_64_v[0-9]|aarch64|i[3-6]86|ppc64le|ppc64|s390x|armv7hl|armv7l|riscv64|loongarch64|src)$'
out=$("$DNF" --assumeno remove "$1" </dev/null 2>&1) || true
out=$("$DNF" --assumeno remove "$@" </dev/null 2>&1) || true
if [[ $out != *'Transaction Summary'* ]]; then
if [[ $out == *'Nothing to do'* ]]; then return 0; fi
printf '%s\n' "$out" >&2
@@ -2089,7 +2094,7 @@ zypper_reason() {
}
zypper_purge_plan() {
local xml rc=0 sec name rest
xml=$(zypper --non-interactive --no-refresh --xmlout remove --dry-run --clean-deps "$1" </dev/null 2>&1) || rc=$?
xml=$(zypper --non-interactive --no-refresh --xmlout remove --dry-run --clean-deps "$@" </dev/null 2>&1) || rc=$?
if ((rc)); then
[[ $rc != 104 ]] || say "zypper: no installed package provides '$1' (exit 104)" >&2
((rc == 104)) || printf '%s\n' "$xml" >&2
@@ -2629,66 +2634,103 @@ num_row() { # num_row N -> $REPLY = row index of a listed row, rc 1 if N is not
REPLY=${N_IDX[n]}
}
# session_purge IDX: pacman's -Rns principle on every backend - a package that other installed
# session_purge IDX...: pacman's -Rns principle on every backend - a package that other installed
# packages still depend on is never purged (they are listed, as N? does; nothing cascades);
# otherwise the package goes with the dependencies it alone needed, as the y/N prompt lists.
session_purge() {
local i=$1 name=${R_NAME[$1]} rc=0 out line p prompt rd d
local -a plan=() others=() extra=()
local -A still=() gone=() mine=()
if [[ ${R_OLD[i]} == - ]]; then
say "$name is not installed (the upgrade would install it as a new dependency): nothing to purge; use $((R_NUM[i]))- to exclude it from this run."
return 0
fi
rd=$(be rdepends "$name") || rd=''
rd=$(printf '%s\n' "$rd" | sed '/^$/d' | tr '\n' ' ')
rd=${rd% }
if [[ -n $rd ]]; then
say "not purged: $name is required by: ${rd// /, } - purge the dependents first"
return 0
fi
out=$(be purge_plan "$name" | sort -u) || rc=$?
local i name rd out rc=0 p prompt idx dep changed
local -a targets=() target_names=() total_plan=() total_others=() extra=() rdeps=()
local -A purge_selected=() rejected=() reverse=() gone=() allowed=() still=()
local line
for idx in "$@"; do purge_selected[${R_NAME[idx]}]=$idx; done
mapfile -t targets < <(printf '%s\n' "${purge_selected[@]}" | sort -n)
for i in "${targets[@]}"; do
name=${R_NAME[i]}
if [[ ${R_OLD[i]} == - ]]; then
say "$name is not installed (the upgrade would install it as a new dependency): nothing to purge; use $((R_NUM[i]))- to exclude it from this run."
rejected[$name]=1
continue
fi
rd=$(be rdepends "$name") || rd=''
reverse[$name]=$(printf '%s\n' "$rd" | sed '/^$/d' | tr '\n' ' ')
done
# A refused dependent also blocks its dependencies, including through a chain.
changed=1
while ((changed)); do
changed=0
for i in "${targets[@]}"; do
name=${R_NAME[i]}
[[ -z ${rejected[$name]+x} ]] || continue
read -r -a rdeps <<<"${reverse[$name]}"
for dep in "${rdeps[@]}"; do
if [[ -z ${purge_selected[$dep]+x} || -n ${rejected[$dep]+x} ]]; then
rd=${reverse[$name]% }
say "not purged: $name is required by: ${rd// /, } - purge the dependents first"
rejected[$name]=1
changed=1
break
fi
done
done
done
for i in "${targets[@]}"; do
name=${R_NAME[i]}
[[ -z ${rejected[$name]+x} ]] || continue
target_names+=("$name")
allowed[$name]=1
closure_of "$name"
for dep in ${CLOSURE[@]+"${CLOSURE[@]}"}; do allowed[$dep]=1; done
done
((${#target_names[@]})) || return 0
out=$(be purge_plan "${target_names[@]}" | sort -u) || rc=$?
if ((rc)); then
err "cannot purge $name: $(be label) refuses (see above); nothing was removed"
err "cannot purge ${target_names[*]}: $(be label) refuses (see above); nothing was removed"
return 0
fi
while IFS= read -r p; do
[[ -n $p && -z ${gone[$p]+x} ]] || continue
[[ -n $p ]] || continue
total_plan+=("$p")
gone[$p]=1
plan+=("$p")
[[ $p == "$name" ]] || others+=("$p")
[[ -n ${purge_selected[$p]+x} && -z ${rejected[$p]+x} ]] || total_others+=("$p")
[[ -n ${allowed[$p]+x} && -z ${rejected[$p]+x} ]] || extra+=("$p")
done <<<"$out"
if ((${#plan[@]} == 0)); then
say "$name is not installed: nothing to purge."
if ((${#total_plan[@]} == 0)); then
say "${target_names[*]} is not installed: nothing to purge."
return 0
fi
# belt and braces: the plan may only hold the target and its own (installed) dependencies;
# anything else means a dependent the reverse-dependency query missed (e.g. through a virtual)
if ((${#others[@]})); then
closure_of "$name"
for d in ${CLOSURE[@]+"${CLOSURE[@]}"}; do mine[$d]=1; done
for p in "${others[@]}"; do [[ -n ${mine[$p]+x} ]] || extra+=("$p"); done
if ((${#extra[@]})); then
err "cannot purge $name: $(be label) would also remove $(join_by ', ' "${extra[@]}"), which $name does not depend on (dependents of it?); nothing was removed"
for name in "${target_names[@]}"; do
if [[ -z ${gone[$name]+x} ]]; then
err "cannot purge $name: $(be label) did not include it in the plan; nothing was removed"
return 0
fi
done
# belt and braces: no package outside the targets and their dependencies may be removed.
if ((${#extra[@]})); then
err "cannot purge ${target_names[*]}: $(be label) would also remove $(join_by ', ' "${extra[@]}"), which ${target_names[*]} does not depend on (dependents of it?); nothing was removed"
return 0
fi
prompt="Purge $name now?"
((${#others[@]} == 0)) || prompt+=" This also removes: $(join_by ', ' "${others[@]}")"
prompt="Purge ${target_names[*]} now?"
((${#total_others[@]} == 0)) || prompt+=" This also removes: $(join_by ', ' "${total_others[@]}")"
read_line "$prompt [y/N] "
case $REPLY in
y | Y | yes | YES | Yes) ;;
*)
say "not purged: $name"
say "not purged: ${target_names[*]}"
return 0
;;
esac
be purge "${plan[@]}" || rc=$?
rc=0
be purge "${total_plan[@]}" || rc=$?
if ((rc)); then
err "purge of $name failed ($(be label) exited $rc); the row stays in the list"
if ((${#target_names[@]} == 1)); then
err "purge of ${target_names[0]} failed ($(be label) exited $rc); the row stays in the list"
else
err "purge of ${target_names[*]} failed ($(be label) exited $rc); the rows stay in the list"
fi
return 0
fi
say "purged: ${plan[*]}"
say "purged: ${total_plan[*]}"
for ((i = 0; i < ${#R_NAME[@]}; i++)); do
if [[ -n ${gone[${R_NAME[i]}]+x} ]]; then
R_GONE[i]=1
@@ -2766,18 +2808,31 @@ session_toggle_all() {
SESSION_PROCEED=0
UPGRADE_FAILED_MSG= # set by <be>_upgrade when the failing step is not the package manager's own
session_flush_purges() {
local -n pb=$1
if ((${#pb[@]})); then
session_purge "${pb[@]}"
pb=()
if (($(rows_left) == 0 && ${#R_NOTES[@]} == 0)); then
say "Nothing left to upgrade."
exit 0
fi
fi
}
session_line() {
local tok n op i
local -a toks=() batch=()
local -A initial=()
local tok n op i name
local -a toks=() batch=() purge_batch=()
local -A initial=() named=()
local re='^([0-9]{1,6})(\?\?|\?|-|x|\+)$'
local name_re='^([A-Za-z0-9_+@][A-Za-z0-9_+.@:~-]*)(\?\?|\?)$'
read -r -a toks <<<"$1" || true
if ((${#toks[@]} == 0)); then
SESSION_PROCEED=1
return 0
fi
for tok in "${toks[@]}"; do
if [[ ! $tok =~ $re ]]; then
if [[ ! $tok =~ $re && ! $tok =~ $name_re ]]; then
case $tok in
'*+' | '*-' | l | q | h | '?') ;;
*)
@@ -2788,9 +2843,30 @@ session_line() {
fi
done
for ((n = 1; n <= N_MAX; n++)); do
if num_row "$n"; then initial[$n]=$REPLY; fi
if num_row "$n"; then
initial[$n]=$REPLY
named[${R_NAME[REPLY]}]=$REPLY
fi
done
for tok in "${toks[@]}"; do
if [[ $tok =~ $re ]]; then
op=${BASH_REMATCH[2]}
if [[ $op == x ]]; then
if ((${#batch[@]})); then session_trust "${batch[@]}"; batch=(); fi
elif [[ $op == + ]]; then
session_flush_purges purge_batch
else
if ((${#batch[@]})); then session_trust "${batch[@]}"; batch=(); fi
session_flush_purges purge_batch
fi
elif [[ $tok =~ $name_re ]]; then
if ((${#batch[@]})); then session_trust "${batch[@]}"; batch=(); fi
session_flush_purges purge_batch
else
if ((${#batch[@]})); then session_trust "${batch[@]}"; batch=(); fi
session_flush_purges purge_batch
fi
if [[ $tok =~ $re ]]; then
n=$((10#${BASH_REMATCH[1]}))
op=${BASH_REMATCH[2]}
@@ -2803,7 +2879,10 @@ session_line() {
batch+=("$i")
continue
fi
if ((${#batch[@]})); then session_trust "${batch[@]}"; batch=(); fi
if [[ $op == x ]]; then
purge_batch+=("$i")
continue
fi
case $op in
'?') print_info "${R_NAME[i]}" "${R_OLD[i]}" || true ;;
'??') print_full_info "${R_NAME[i]}" "${R_OLD[i]}" || true ;;
@@ -2816,16 +2895,21 @@ session_line() {
say "excluded from this run: ${R_NAME[i]}"
fi
;;
x)
session_purge "$i"
if (($(rows_left) == 0 && ${#R_NOTES[@]} == 0)); then
say "Nothing left to upgrade."
exit 0
fi
;;
esac
elif [[ $tok =~ $name_re ]]; then
name=${BASH_REMATCH[1]}
op=${BASH_REMATCH[2]}
if [[ -z ${named[$name]+x} ]] || ((R_GONE[${named[$name]:-0}])); then
err "no package '$name' in the list (l to redraw)"
continue
fi
i=${named[$name]}
if [[ $op == '?' ]]; then
print_info "${R_NAME[i]}" "${R_OLD[i]}" || true
else
print_full_info "${R_NAME[i]}" "${R_OLD[i]}" || true
fi
else
if ((${#batch[@]})); then session_trust "${batch[@]}"; batch=(); fi
case $tok in
'*+')
local -a idx=()
@@ -2845,6 +2929,7 @@ session_line() {
fi
done
if ((${#batch[@]})); then session_trust "${batch[@]}"; fi
session_flush_purges purge_batch
}
cmd_interactive() {
+10
View File
@@ -785,3 +785,13 @@ zypp_conf() { # a "real" zypp.conf + locks file with one user lock, in the test
refute_output --partial 'skeptical-updater.'
[[ ! -e $STATE/zypper-private ]]
}
@test "pacman: dependent and dependency are simulated and purged together" {
use_fakes pacman
run skup --no-refresh <<<$'2x 1x\ny\nq'
assert_success
assert_line 'Purge su-dummy-a su-dummy-b now? [y/N] y'
assert_line 'purged: su-dummy-a su-dummy-b'
log_has 'pacman -Rs --print --print-format %n -- su-dummy-a su-dummy-b'
log_has 'pacman -Rns --noconfirm -- su-dummy-a su-dummy-b'
}
+6 -4
View File
@@ -24,12 +24,14 @@ setup() { common_setup; }
assert_line 'usage: skeptical-updater [options] [command]'
}
@test "--version and -V print the version" {
@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'
assert_output --regexp '^skeptical-updater [0-9]+\.[0-9]+\.[0-9]+(\+(alpha|beta|rc)[.-]?[0-9]+)?$'
local version=$output
run skup -v
assert_success
assert_output "$version"
}
@test "an unknown option is a usage error (exit 1)" {
+9 -10
View File
@@ -137,21 +137,20 @@ case $op in
-Rs)
((print)) || exit 98
tg=${targets[0]}
if [[ -n ${FAKE_PACMAN_RS_REFUSE:-} || $tg == su-dummy-b ]]; then
if [[ -n ${FAKE_PACMAN_RS_REFUSE:-} || " ${targets[*]} " == *' su-dummy-b '* && " ${targets[*]} " != *' su-dummy-a '* ]]; 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 | pfetch | su-dummy-aur) echo "$tg" ;; # (the last two: foreign, AUR)
*)
echo "error: target not found: $tg" >&2
exit 1
;;
esac
for r in ${FAKE_PACMAN_RS_EXTRA:-}; do [[ ${r%%:*} == "$tg" ]] && echo "${r#*:}"; done
for tg in "${targets[@]}"; do
case $tg in
su-dummy-a) printf '%s\n' su-dummy-a su-dummy-b ;;
su-dummy-b | su-dummy-c | glib2 | expat | ca-certificates-mozilla | pfetch | su-dummy-aur) 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
done
;;
-Rns)
rc=$(fake_rc FAKE_PACMAN_RNS_RC 0)
+127
View File
@@ -101,6 +101,54 @@ UPGRADE='apt-get -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--forc
log_has 'apt-cache show --no-all-versions su-dummy-a'
}
@test "listed package names support both information commands alongside numeric commands" {
run skup --no-refresh <<<$'su-dummy-a? 2? su-dummy-b?? 3-\nq'
assert_success
assert_line 'su-dummy-a: dummy package su-dummy-a for skeptical-updater tests'
assert_line 'su-dummy-b: dummy package su-dummy-b for skeptical-updater tests'
assert_line ' Package: su-dummy-b'
assert_line 'excluded from this run: su-dummy-c'
log_has 'dpkg-query -s -- su-dummy-b'
}
@test "unknown or hidden package names report an error and do not stop later commands" {
write_state "$HEADER" "apt${T}su-dummy-a${T}user${T}2026-01-01T00:00:00Z"
run skup --no-refresh <<<$'missing? su-dummy-a?? 2?\nq'
assert_success
assert_output --partial "no package 'missing' in the list"
assert_output --partial "no package 'su-dummy-a' in the list"
assert_line 'su-dummy-c: dummy package su-dummy-c for skeptical-updater tests'
}
@test "names work for trusted rows shown with --all and new dependencies" {
write_state "$HEADER" "apt${T}su-dummy-a${T}user${T}2026-01-01T00:00:00Z"
run skup --no-refresh --all <<<$'su-dummy-a?\nq'
assert_success
assert_line 'su-dummy-a: dummy package su-dummy-a for skeptical-updater tests'
FAKE_APT_SIM=real run skup --no-refresh <<<$'su-dummy-newdep? su-dummy-newdep??\nq'
assert_success
assert_line 'reason: not installed'
log_lacks '^dpkg-query -s -- su-dummy-newdep'
log_has 'apt-cache show --no-all-versions su-dummy-newdep'
}
@test "a package removed during a line cannot be queried by name" {
run skup --no-refresh <<<$'3x su-dummy-c? 2?\ny\nq'
assert_success
assert_output --partial "no package 'su-dummy-c' in the list"
assert_line 'su-dummy-b: dummy package su-dummy-b for skeptical-updater tests'
}
@test "package names are not accepted for trust, exclude or purge" {
for token in su-dummy-a+ su-dummy-a- su-dummy-ax; do
run skup --no-refresh <<<"$token"$'\nq'
assert_success
assert_output --partial "unknown command '$token'"
[[ ! -e $STATE_FILE ]]
log_lacks '^apt-get -y purge'
done
}
@test "N?? on a new dependency (old version -) never looks for an installed record" {
FAKE_APT_SIM=real run skup --no-refresh <<<$'9??\nq'
assert_success
@@ -409,3 +457,82 @@ UPGRADE='apt-get -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--forc
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'
}
@test "Nx: consecutive purges use one transaction including orphans" {
FAKE_APT_AUTO_ORPHANS=su-dummy-a:su-dummy-b run skup --no-refresh <<<$'1x 3x\ny\nq'
assert_success
assert_line 'Purge su-dummy-a su-dummy-c now? This also removes: su-dummy-b [y/N] y'
assert_line 'purged: su-dummy-a su-dummy-b su-dummy-c'
assert_line 'Nothing left to upgrade.'
log_has 'apt-get -y purge su-dummy-a su-dummy-b su-dummy-c'
}
@test "Nx: other commands flush the batch before resolving removed rows" {
FAKE_APT_AUTO_ORPHANS=su-dummy-a:su-dummy-b run skup --no-refresh <<<$'1x 2? 3x\ny\ny\nq'
assert_success
assert_line 'purged: su-dummy-a su-dummy-b'
assert_output --partial 'no package [2] in the list'
assert_line 'Purge su-dummy-c now? [y/N] y'
assert_line 'purged: su-dummy-c'
}
@test "Nx: a trust command cannot act on a removed row" {
FAKE_APT_AUTO_ORPHANS=su-dummy-a:su-dummy-b run skup --no-refresh <<<$'1x 2+\ny\nq'
assert_success
assert_output --partial 'no package [2] in the list'
assert_line 'purged: su-dummy-a su-dummy-b'
}
@test "Nx: duplicate targets are purged once" {
run skup --no-refresh <<<$'3x 3x\ny\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'
}
@test "Nx: a refused target does not prevent other purges" {
run skup --no-refresh <<<$'2x 3x\ny\nl\nq'
assert_success
assert_line 'not purged: su-dummy-b is required by: su-dummy-a - purge the dependents first'
assert_line 'purged: su-dummy-c'
log_has 'apt-get -y purge su-dummy-c'
}
@test "Nx: invalid numbers do not interrupt a batch" {
FAKE_APT_AUTO_ORPHANS=su-dummy-a:su-dummy-b run skup --no-refresh <<<$'1x 9x 3x\ny\nq'
assert_success
assert_output --partial 'no package [9] in the list'
assert_line 'purged: su-dummy-a su-dummy-b su-dummy-c'
log_has 'apt-get -y purge su-dummy-a su-dummy-b su-dummy-c'
}
@test "Nx: rejecting the prompt purges nothing" {
run skup --no-refresh <<<$'1x 3x\nn\nl\nq'
assert_success
assert_line 'not purged: su-dummy-a su-dummy-c'
assert_line '3 upgradable, 0 trusted (hidden, use --all to show), 3 to review:'
log_lacks '^apt-get -y purge'
}
@test "Nx: failed transaction keeps all rows" {
FAKE_APT_PURGE_RC=100 run skup --no-refresh <<<$'1x 3x\nyes\nl\nq'
assert_success
assert_output --partial 'purge of su-dummy-a su-dummy-c failed (apt exited 100); the rows stay in the list'
assert_line '3 upgradable, 0 trusted (hidden, use --all to show), 3 to review:'
}
@test "Nx: a dependent and dependency are planned together" {
FAKE_APT_AUTO_ORPHANS=su-dummy-a:su-dummy-b run skup --no-refresh <<<$'2x 1x\ny\nq'
assert_success
assert_line 'Purge su-dummy-a su-dummy-b now? [y/N] y'
assert_line 'purged: su-dummy-a su-dummy-b'
log_has 'apt-get -y purge su-dummy-a su-dummy-b'
}
@test "Nx: unexpected removals never leak into another target's purge" {
FAKE_APT_SIM_EXTRA=su-dummy-a:su-dummy-c run skup --no-refresh <<<$'1x 2x\ny\nq'
assert_success
assert_output --partial 'would also remove su-dummy-c'
log_lacks '^apt-get -y purge'
}