From: Luca Boccassi Date: Tue, 19 May 2026 17:43:57 +0000 (+0100) Subject: test: add coverage for multi-unit transactions X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=7034441827e71abc63e2476286c53f3feb05473b;p=thirdparty%2Fsystemd.git test: add coverage for multi-unit transactions Co-developed-by: Claude Opus 4.7 --- diff --git a/test/units/TEST-07-PID1.multi-units-transaction.sh b/test/units/TEST-07-PID1.multi-units-transaction.sh new file mode 100755 index 00000000000..d8bf794447a --- /dev/null +++ b/test/units/TEST-07-PID1.multi-units-transaction.sh @@ -0,0 +1,375 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +# Operate multiple units in a single transaction. +# Issue: https://github.com/systemd/systemd/issues/8102 +# +# When 'systemctl start ' is executed, both units must be enqueued in a +# single transaction so that After= ordering is honoured regardless of the order +# of the arguments. Previously each unit was sent to PID1 in its own D-Bus +# request and thus its own transaction, which meant the ordering dependency was +# only effective when the dependee was queued before the dependent. + +MARKER_DIR="$(mktemp -d /tmp/issue8102.marker.XXXXXX)" +MARKER="$MARKER_DIR/done" +SOCK_DIR="$(mktemp -d /tmp/issue8102.sock.XXXXXX)" +SOCK_PATH="$SOCK_DIR/sock" + +at_exit() { + set +e + + systemctl stop issue8102-second.service issue8102-first.service + systemctl stop issue8102-sock-foo.service issue8102-sock-foo.socket + systemctl stop 'issue8102-many@*.service' + systemctl stop issue8102-conflict-a.service issue8102-conflict-b.service + systemctl reset-failed issue8102-second.service issue8102-first.service + systemctl reset-failed issue8102-sock-foo.service issue8102-sock-foo.socket + systemctl reset-failed 'issue8102-many@*.service' + systemctl reset-failed issue8102-conflict-a.service issue8102-conflict-b.service + rm -f /run/systemd/system/issue8102-{first,second}.service + rm -f /run/systemd/system/issue8102-sock-foo.{service,socket} + rm -f /run/systemd/system/issue8102-many@.service + rm -f /run/systemd/system/issue8102-conflict-{a,b}.service + rm -rf "$MARKER_DIR" "$SOCK_DIR" + systemctl daemon-reload +} + +trap at_exit EXIT + +mkdir -p /run/systemd/system + +cat >/run/systemd/system/issue8102-first.service < "$MARKER"' +EOF + +cat >/run/systemd/system/issue8102-second.service </run/systemd/system/issue8102-sock-foo.socket </run/systemd/system/issue8102-sock-foo.service </dev/null +# Wait for the units to come back up after the restart. +# shellcheck disable=SC2016 +timeout 30s bash -c ' + while [[ "$(systemctl show -P ActiveState issue8102-sock-foo.service)" != active ]] || + [[ "$(systemctl show -P ActiveState issue8102-sock-foo.socket)" != active ]]; do + sleep 0.5 + done +' +test -S "$SOCK_PATH" + +# --------------------------------------------------------------------------- +# Argument validation corner cases for EnqueueUnitJobMany(). +# --------------------------------------------------------------------------- + +# Empty units array → the handler must reject the call with an INVALID_ARGS +# error before doing anything. +out=$(busctl call \ + org.freedesktop.systemd1 \ + /org/freedesktop/systemd1 \ + org.freedesktop.systemd1.Manager \ + EnqueueUnitJobMany \ + assst \ + 0 \ + start replace \ + 0 2>&1) && { echo 'busctl unexpectedly succeeded'; exit 1; } +echo "$out" | grep -F "No units specified" >/dev/null + +# Non-zero flags parameter is reserved and must be rejected. +out=$(busctl call \ + org.freedesktop.systemd1 \ + /org/freedesktop/systemd1 \ + org.freedesktop.systemd1.Manager \ + EnqueueUnitJobMany \ + assst \ + 1 issue8102-first.service \ + start replace \ + 1 2>&1) && { echo 'busctl unexpectedly succeeded'; exit 1; } +echo "$out" | grep -F "Invalid flags parameter" >/dev/null + +# Bogus job type → rejected before any job is constructed. +out=$(busctl call \ + org.freedesktop.systemd1 \ + /org/freedesktop/systemd1 \ + org.freedesktop.systemd1.Manager \ + EnqueueUnitJobMany \ + assst \ + 1 issue8102-first.service \ + not-a-real-job-type replace \ + 0 2>&1) && { echo 'busctl unexpectedly succeeded'; exit 1; } +echo "$out" | grep -F "Job type not-a-real-job-type invalid" >/dev/null + +# Bogus job mode → rejected before any job is constructed. +out=$(busctl call \ + org.freedesktop.systemd1 \ + /org/freedesktop/systemd1 \ + org.freedesktop.systemd1.Manager \ + EnqueueUnitJobMany \ + assst \ + 1 issue8102-first.service \ + start not-a-real-mode \ + 0 2>&1) && { echo 'busctl unexpectedly succeeded'; exit 1; } +echo "$out" | grep -F "Job mode not-a-real-mode invalid" >/dev/null + +# Unknown unit must be reported as an error and no partial state should remain +# in the job queue: list one valid + one bogus unit, ensure the call fails and +# that the valid unit has no pending start job afterwards. +systemctl stop issue8102-first.service +[[ "$(systemctl show -P ActiveState issue8102-first.service)" == inactive ]] + +(! busctl call \ + org.freedesktop.systemd1 \ + /org/freedesktop/systemd1 \ + org.freedesktop.systemd1.Manager \ + EnqueueUnitJobMany \ + assst \ + 2 issue8102-first.service issue8102-does-not-exist.service \ + start replace \ + 0 2>/dev/null) + +# busctl is synchronous: by the time the call returns with an error, PID1 has +# already rejected the transaction. Verify the valid unit was not started +# behind our back (the transaction must be all-or-nothing). +[[ "$(systemctl show -P ActiveState issue8102-first.service)" == inactive ]] + +# --------------------------------------------------------------------------- +# Many units in a single transaction. +# --------------------------------------------------------------------------- +# Build a template unit and instantiate a fair number of instances, all +# enqueued via a single EnqueueUnitJobMany() call to exercise the strv path with +# a transaction that anchors many units at once. + +cat >/run/systemd/system/issue8102-many@.service <<'EOF' +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart=/bin/true +EOF +systemctl daemon-reload + +MANY_COUNT=20 +mapfile -t MANY_UNITS < <(for i in $(seq 1 "$MANY_COUNT"); do printf 'issue8102-many@%d.service\n' "$i"; done) +MANY_BUSCTL_ARGS=("$MANY_COUNT" "${MANY_UNITS[@]}") + +busctl call \ + org.freedesktop.systemd1 \ + /org/freedesktop/systemd1 \ + org.freedesktop.systemd1.Manager \ + EnqueueUnitJobMany \ + assst \ + "${MANY_BUSCTL_ARGS[@]}" \ + start replace \ + 0 >/dev/null + +# Wait for every instance to become active. +# shellcheck disable=SC2016 +timeout 30s bash -c ' + for u in "$@"; do + while [[ "$(systemctl show -P ActiveState "$u")" != active ]]; do + sleep 0.2 + done + done +' bash "${MANY_UNITS[@]}" + +# Stop them all in one transaction too, verifying the stop path scales as well. +busctl call \ + org.freedesktop.systemd1 \ + /org/freedesktop/systemd1 \ + org.freedesktop.systemd1.Manager \ + EnqueueUnitJobMany \ + assst \ + "${MANY_BUSCTL_ARGS[@]}" \ + stop replace \ + 0 >/dev/null + +# shellcheck disable=SC2016 +timeout 30s bash -c ' + for u in "$@"; do + while [[ "$(systemctl show -P ActiveState "$u")" != inactive ]]; do + sleep 0.2 + done + done +' bash "${MANY_UNITS[@]}" + +# --------------------------------------------------------------------------- +# Incompatible transaction: two units that Conflict= with each other cannot be +# started together. Both start anchors would force the other unit to stop, so +# the transaction is unsatisfiable regardless of the chosen job mode. The +# handler must report an error and roll back, so that neither unit ends up +# active. +# --------------------------------------------------------------------------- + +cat >/run/systemd/system/issue8102-conflict-a.service </run/systemd/system/issue8102-conflict-b.service </dev/null) + +# Atomicity: neither unit must end up activated by the failed call. busctl is +# synchronous so by the time it returns PID1 has already rolled back. +[[ "$(systemctl show -P ActiveState issue8102-conflict-a.service)" == inactive ]] +[[ "$(systemctl show -P ActiveState issue8102-conflict-b.service)" == inactive ]] + +# --------------------------------------------------------------------------- +# reload-or-try-restart with a mix of reloadable and non-reloadable units. +# The socket cannot reload so it must be restarted, while a unit that does +# implement reload would be reloaded. Both must be active afterwards. +# --------------------------------------------------------------------------- + +systemctl start issue8102-sock-foo.service issue8102-sock-foo.socket +[[ "$(systemctl show -P ActiveState issue8102-sock-foo.service)" == active ]] +[[ "$(systemctl show -P ActiveState issue8102-sock-foo.socket)" == active ]] + +busctl call \ + org.freedesktop.systemd1 \ + /org/freedesktop/systemd1 \ + org.freedesktop.systemd1.Manager \ + EnqueueUnitJobMany \ + assst \ + 2 issue8102-sock-foo.service issue8102-sock-foo.socket \ + reload-or-try-restart replace \ + 0 >/dev/null + +# shellcheck disable=SC2016 +timeout 30s bash -c ' + while [[ "$(systemctl show -P ActiveState issue8102-sock-foo.service)" != active ]] || + [[ "$(systemctl show -P ActiveState issue8102-sock-foo.socket)" != active ]]; do + sleep 0.5 + done +' +test -S "$SOCK_PATH" + +# try-restart on a unit that is not currently running must be a no-op (no error) +# and must not start it. +systemctl stop issue8102-first.service +[[ "$(systemctl show -P ActiveState issue8102-first.service)" == inactive ]] + +busctl call \ + org.freedesktop.systemd1 \ + /org/freedesktop/systemd1 \ + org.freedesktop.systemd1.Manager \ + EnqueueUnitJobMany \ + assst \ + 1 issue8102-first.service \ + try-restart replace \ + 0 >/dev/null + +[[ "$(systemctl show -P ActiveState issue8102-first.service)" == inactive ]]