test: cover subscribe mode of ListCandidateDevices

testcase_varlink_subscribe_devices in TEST-58-REPART spawns a varlinkctl
subscriber as a transient Type=notify service so systemd-run only returns
once varlinkctl has received the first reply (the "ready" sentinel or the
first "add"), guaranteeing the server-side sd-device monitor is up before
the test triggers any uevents. Loopback devices are created and detached
via systemd-dissect --attach / --detach against small ext4 images.

Covers:
  1. "ready" sentinel arrives even on an empty initial set
  2. losetup-style attach => "add" event with the expected node
  3. detach => "remove" event with the expected node
  4. 5 add/remove cycles in a row stay in sync
  5. two coexisting loop devices each produce their own add/remove
  6. a device that exists before the subscription starts shows up in
     the initial enumeration, and "ready" arrives only after it
  7. without subscribe:true the reply omits the "action" field
     (back-compat for older clients)

The case is skipped inside containers since nspawn --private-network
filters the kernel uevent socket away from the subscribed server.
This commit is contained in:
Lennart Poettering
2026-06-01 11:26:15 +02:00
parent 81bd798b65
commit cfb67049ce
+168
View File
@@ -1843,6 +1843,174 @@ testcase_varlink_list_devices() {
varlinkctl call /run/systemd/io.systemd.Repart --graceful=io.systemd.Repart.NoCandidateDevices --collect io.systemd.Repart.ListCandidateDevices '{"ignoreEmpty":true,"ignoreRoot":true}'
}
testcase_varlink_subscribe_devices() {
local imgs subscribe_log pre_existing_log loop loop2 mark non_sub_output
local sub_unit="test-repart-subscribe.service"
local pre_unit="test-repart-subscribe-pre.service"
# The uevent monitor in the spawned systemd-repart only sees events from the host's kernel
# uevent namespace, which an nspawn container with --private-network filters away. Loopback
# creation also fails inside many container setups.
if systemd-detect-virt --container >/dev/null 2>&1; then
echo "Skipping subscribe tests inside container."
return
fi
REPART="$(which systemd-repart)"
imgs="$(mktemp --directory "/var/tmp/test-repart.subscribe.XXXXXXXXXX")"
subscribe_log="$(mktemp "/var/tmp/test-repart.subscribe-log.XXXXXXXXXX")"
pre_existing_log="$(mktemp "/var/tmp/test-repart.pre-log.XXXXXXXXXX")"
loop=""
loop2=""
# Single-quoted trap body so $loop / $loop2 are expanded at trap-fire time, not now.
# shellcheck disable=SC2016
trap '
systemctl stop "$sub_unit" "$pre_unit" 2>/dev/null || true
[[ -n "${loop:-}" ]] && systemd-dissect --detach "$loop" 2>/dev/null || true
[[ -n "${loop2:-}" ]] && systemd-dissect --detach "$loop2" 2>/dev/null || true
rm -rf "$imgs" "$subscribe_log" "$subscribe_log.err" "$pre_existing_log" "$pre_existing_log.err"
' RETURN
# systemd-dissect --attach requires a dissectable image; a plain ext4 single-filesystem image
# is the simplest thing it accepts.
truncate -s 50M "$imgs/loop.img"
truncate -s 50M "$imgs/loop2.img"
mkfs.ext4 -F -q -L test-repart-1 "$imgs/loop.img"
mkfs.ext4 -F -q -L test-repart-2 "$imgs/loop2.img"
# Make sure no stale unit is sitting around from a previous failed run, then arrange cleanup.
systemctl reset-failed "$sub_unit" "$pre_unit" 2>/dev/null || true
systemctl stop "$sub_unit" "$pre_unit" 2>/dev/null || true
# Start a varlinkctl subscriber as a transient Type=notify service. systemd-run blocks until the
# service notifies READY=1 -- which varlinkctl does on receipt of the first reply (see
# src/varlinkctl/varlinkctl.c reply_callback). For us that's either the first "add" of the
# initial enumeration or the "ready" sentinel; either way we are guaranteed the server-side
# monitor is up before we trigger any uevents.
start_subscriber() {
local unit="$1" log="$2" params="$3"
systemd-run \
--quiet \
--unit="$unit" \
--collect \
--service-type=notify \
--property=StandardOutput="truncate:$log" \
--property=StandardError="truncate:$log.err" \
-- \
varlinkctl --more --timeout=infinity --json=short call \
"$REPART" io.systemd.Repart.ListCandidateDevices "$params"
}
# Poll for a regex match in $1 of the named log file ($3, default $subscribe_log), starting from
# byte offset $2. Prints the new byte offset (post-match) on stdout so the caller can advance.
expect_event() {
local pat="$1" mark_="$2" file="${3:-$subscribe_log}"
local deadline_=$((SECONDS + UDEVADM_WAIT_TIMEOUT))
local cur
while (( SECONDS < deadline_ )); do
cur=$(stat -c%s "$file" 2>/dev/null || echo 0)
if (( cur > mark_ )) && \
tail -c +"$((mark_ + 1))" "$file" | grep -a -E "$pat" >/dev/null; then
printf '%s\n' "$cur"
return 0
fi
sleep 0.1
done
echo "FAIL: pattern '$pat' did not appear within ${UDEVADM_WAIT_TIMEOUT}s. Output past offset $mark_:" >&2
tail -c +"$((mark_ + 1))" "$file" >&2 || true
return 1
}
# === Test 1: subscribing produces a "ready" sentinel even on an empty initial set ===
start_subscriber "$sub_unit" "$subscribe_log" \
'{"ignoreRoot":true,"ignoreEmpty":true,"subscribe":true}'
mark=$(expect_event '"action":"ready"' 0) || return 1
# === Test 2: a freshly-added loopback produces an "add" event ===
loop="$(systemd-dissect --attach --loop-ref=test-repart-1 "$imgs/loop.img")"
udevadm wait --timeout="$UDEVADM_WAIT_TIMEOUT" --settle "$loop"
mark=$(expect_event "\"action\":\"add\".*\"node\":\"$loop\"" "$mark") || return 1
# === Test 3: detaching the loopback produces a "remove" event ===
systemd-dissect --detach "$loop"
udevadm wait --timeout="$UDEVADM_WAIT_TIMEOUT" --removed --settle "$loop"
mark=$(expect_event "\"action\":\"remove\".*\"node\":\"$loop\"" "$mark") || return 1
loop=""
# === Test 4: five add/remove cycles in a row stay in sync ===
for _ in 1 2 3 4 5; do
loop="$(systemd-dissect --attach --loop-ref=test-repart-1 "$imgs/loop.img")"
udevadm wait --timeout="$UDEVADM_WAIT_TIMEOUT" --settle "$loop"
mark=$(expect_event "\"action\":\"add\".*\"node\":\"$loop\"" "$mark") || return 1
systemd-dissect --detach "$loop"
udevadm wait --timeout="$UDEVADM_WAIT_TIMEOUT" --removed --settle "$loop"
mark=$(expect_event "\"action\":\"remove\".*\"node\":\"$loop\"" "$mark") || return 1
loop=""
done
# === Test 5: two coexisting loop devices each produce their own events ===
# Set them up (and tear them down) one at a time and wait for each device in turn, so events
# appear in a strict order and we can advance our mark monotonically.
loop="$(systemd-dissect --attach --loop-ref=test-repart-1 "$imgs/loop.img")"
udevadm wait --timeout="$UDEVADM_WAIT_TIMEOUT" --settle "$loop"
mark=$(expect_event "\"action\":\"add\".*\"node\":\"$loop\"" "$mark") || return 1
loop2="$(systemd-dissect --attach --loop-ref=test-repart-2 "$imgs/loop2.img")"
udevadm wait --timeout="$UDEVADM_WAIT_TIMEOUT" --settle "$loop2"
mark=$(expect_event "\"action\":\"add\".*\"node\":\"$loop2\"" "$mark") || return 1
systemd-dissect --detach "$loop2"
udevadm wait --timeout="$UDEVADM_WAIT_TIMEOUT" --removed --settle "$loop2"
mark=$(expect_event "\"action\":\"remove\".*\"node\":\"$loop2\"" "$mark") || return 1
loop2=""
systemd-dissect --detach "$loop"
udevadm wait --timeout="$UDEVADM_WAIT_TIMEOUT" --removed --settle "$loop"
mark=$(expect_event "\"action\":\"remove\".*\"node\":\"$loop\"" "$mark") || return 1
loop=""
# Tear down the first subscriber before starting the next one.
systemctl stop "$sub_unit"
# === Test 6: a device that exists *before* subscribe starts shows up in the initial enumeration,
# and "ready" arrives only after it ===
loop="$(systemd-dissect --attach --loop-ref=test-repart-1 "$imgs/loop.img")"
udevadm wait --timeout="$UDEVADM_WAIT_TIMEOUT" --settle "$loop"
start_subscriber "$pre_unit" "$pre_existing_log" \
'{"ignoreRoot":true,"subscribe":true}'
# systemd-run returned (Type=notify => first reply already received), but "ready" may not be
# written yet -- poll for it.
expect_event '"action":"ready"' 0 "$pre_existing_log" >/dev/null || return 1
# Everything up to (but excluding) "ready" is the initial enumeration; it must contain $loop.
if ! sed -n '/"action":"ready"/q;p' "$pre_existing_log" | \
grep -a -E "\"action\":\"add\".*\"node\":\"$loop\"" >/dev/null; then
echo "FAIL: pre-existing loop device $loop not in initial enumeration:" >&2
cat "$pre_existing_log" >&2
return 1
fi
systemctl stop "$pre_unit"
# === Test 7: without subscribe the reply has no "action" field (back-compat for older clients) ===
non_sub_output="$(varlinkctl --collect --json=short call \
"$REPART" --graceful=io.systemd.Repart.NoCandidateDevices \
io.systemd.Repart.ListCandidateDevices '{"ignoreRoot":true}')"
assert_not_in '"action"' "$non_sub_output"
# Sanity-check: our $loop *is* in that output (so the assertion above wasn't vacuous).
assert_in "\"node\":\"$loop\"" "$non_sub_output"
systemd-dissect --detach "$loop"
loop=""
}
testcase_get_size() {
local defs