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:
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user