test: Add a way to quickly iterate on an integration test
authorDaan De Meyer <daan.j.demeyer@gmail.com>
Fri, 2 Aug 2024 14:25:03 +0000 (16:25 +0200)
committerLuca Boccassi <bluca@debian.org>
Thu, 15 Aug 2024 13:04:41 +0000 (14:04 +0100)
Rebuilding the integration test every time is very slow. Let's
introduce a way to iterate on an integration test without rebuilding
the image every time. By making a btrfs snapshot before we run the
integration test, we can then systemctl soft-reboot after running
the test to restore the rootfs to a pristine state before running
the test again.

As /run/nextroot will get nuked on reboot or soft-reboot, we introduce
a tmpfiles snippet to make sure it is recreated every (soft-)reboot
and adapt the existing tests to deal with this new symlink.

(cherry picked from commit af153e36ae67c242251951c12d6d6b6ae4783845)

mkosi.extra/usr/lib/tmpfiles.d/snapshot.conf [new file with mode: 0644]
test/README.testsuite
test/integration-test-setup.sh [new file with mode: 0755]
test/meson.build
test/test.service.in
test/units/TEST-09-REBOOT.sh
test/units/TEST-82-SOFTREBOOT.sh

diff --git a/mkosi.extra/usr/lib/tmpfiles.d/snapshot.conf b/mkosi.extra/usr/lib/tmpfiles.d/snapshot.conf
new file mode 100644 (file)
index 0000000..fea660e
--- /dev/null
@@ -0,0 +1,3 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+L /run/nextroot - - - - /snapshot
index c1b048a93d7aaba20edee0eeb1094aa61b99bdb6..585bddd3dd7343e73b7fc0f947fc78c35420c9d4 100644 (file)
@@ -86,6 +86,45 @@ mkosi in the systemd reposistory, so any local modifications to the mkosi
 configuration (e.g. in `mkosi.local.conf`) are automatically picked up and used
 by the integration tests as well.
 
+## Iterating on an integration test
+
+To iterate on an integration test, let's first get a shell in the integration test environment by running
+the following:
+
+```shell
+$ meson compile -C build mkosi && SYSTEMD_INTEGRATION_TESTS=1 TEST_SHELL=1 meson test -C build --no-rebuild -i TEST-01-BASIC
+```
+
+This will get us a shell in the integration test environment after booting the machine without running the
+integration test itself. After booting, we can verify the integration test passes by running it manually,
+for example with `systemctl start TEST-01-BASIC`.
+
+Now you can extend the test in whatever way you like to add more coverage of existing features or to add
+coverage for a new feature. Once you've finished writing the logic and want to rerun the test, run the
+the following on the host:
+
+```shell
+$ mkosi -t none
+```
+
+This will rebuild the distribution packages without rebuilding the entire integration test image. Next, run
+the following in the integration test machine:
+
+```shell
+$ systemctl soft-reboot
+$ systemctl start TEST-01-BASIC
+```
+
+A soft-reboot is required to make sure all the leftover state from the previous run of the test is cleaned
+up by soft-rebooting into the btrfs snapshot we made before running the test. After the soft-reboot,
+re-running the test will first install the new packages we just built, make a new snapshot and finally run
+the test again. You can keep running the loop of `mkosi -t none`, `systemctl soft-reboot` and
+`systemctl start ...` until the changes to the integration test are working.
+
+If you're debugging a failing integration test (running `meson test --interactive` without `TEST_SHELL`),
+there's no need to run `systemctl start ...`, running `systemctl soft-reboot` on its own is sufficient to
+rerun the test.
+
 ## Running the integration tests the old fashioned way
 
 The extended testsuite only works with UID=0. It consists of the subdirectories
diff --git a/test/integration-test-setup.sh b/test/integration-test-setup.sh
new file mode 100755 (executable)
index 0000000..71f576f
--- /dev/null
@@ -0,0 +1,56 @@
+#!/bin/bash
+# SPDX-License-Identifier: LGPL-2.1-or-later
+set -e
+
+case "$1" in
+    setup)
+        if [[ -f "$STATE_DIRECTORY/inprogress" ]]; then
+            exit 0
+        fi
+
+        if [[ -d /snapshot ]]; then
+            echo "Run systemctl soft-reboot first to make sure the test runs within a pristine rootfs" >&2
+            exit 1
+        fi
+
+        . /usr/lib/os-release
+
+        if test -n "$(shopt -s nullglob; echo /work/build/*.{rpm,deb,pkg.tar})"; then
+            case "$ID" in
+                arch)
+                    pacman --upgrade --needed --noconfirm /work/build/*.pkg.tar
+                    ;;
+                debian|ubuntu)
+                    apt-get install /work/build/*.deb
+                    ;;
+                opensuse*)
+                    zypper --non-interactive install --allow-unsigned-rpm /work/build/*.rpm
+                    ;;
+                centos|fedora)
+                    dnf upgrade --assumeyes --disablerepo="*" /work/build/*.rpm
+                    ;;
+                *)
+                    echo "Unknown distribution $ID" >&2
+                    exit 1
+            esac
+        fi
+
+        # TODO: Use a proper flat btrfs subvolume layout once we can create subvolumes without privileged in
+        # systemd-repart (see https://github.com/systemd/systemd/pull/33498). Until that's possible, we nest
+        # snapshots within each other.
+        if command -v btrfs >/dev/null && [[ "$(stat --file-system --format %T /)" == "btrfs" ]]; then
+            btrfs subvolume snapshot / /snapshot
+        fi
+
+        touch "$STATE_DIRECTORY/inprogress"
+        ;;
+    finalize)
+        # If we're rebooting, the test does a reboot as part of its execution and we shouldn't remove /inprogress.
+        if ! [[ "$(systemctl list-jobs)" =~ reboot.target|kexec.target|soft-reboot.target ]]; then
+            rm -f "$STATE_DIRECTORY/inprogress"
+        fi
+        ;;
+    *)
+        echo "Unknown verb $1" >&2
+        exit 1
+esac
index 173d90cc8e453d128446d859b74ae29487360eb2..6acff3750833db1fc7e41480155170c3b82c6680 100644 (file)
@@ -142,9 +142,11 @@ endif
 ############################################################
 
 if install_tests
-        install_data('run-unit-tests.py',
-                     install_mode : 'rwxr-xr-x',
-                     install_dir : testsdir)
+        foreach script : ['integration-test-setup.sh', 'run-unit-tests.py']
+                install_data(script,
+                             install_mode : 'rwxr-xr-x',
+                             install_dir : testsdir)
+        endforeach
 endif
 
 ############################################################
index 790c513da4338e7073277523eca50af9d22a42e4..ef9dba30c45c0c8e544bd3c29020ea164cd3ac71 100644 (file)
@@ -4,9 +4,12 @@ Description=%N
 Wants=basic.target network.target @wants@
 After=basic.target network.target @after@
 Before=getty-pre.target
+StateDirectory=%N
 
 [Service]
 ExecStartPre=rm -f /failed /testok
+ExecStartPre=/usr/lib/systemd/tests/integration-test-setup.sh setup
 ExecStart=@command@
+ExecStopPost=/usr/lib/systemd/tests/integration-test-setup.sh finalize
 Type=oneshot
 MemoryAccounting=@memory-accounting@
index 85630b618bcfef74e0c2d6c43204610a52e0fc21..014ea31db6e0977548cb43040f7e96f018023f2e 100755 (executable)
@@ -17,7 +17,11 @@ systemd-cat journalctl --list-boots
 run_subtests
 
 if [[ "$REBOOT_COUNT" -lt "$NUM_REBOOT" ]]; then
+    SYSTEMCTL_SKIP_AUTO_SOFT_REBOOT=1
+    export SYSTEMCTL_SKIP_AUTO_SOFT_REBOOT
     systemctl_final reboot
+    # Now block until the reboot killing spree kills us.
+    exec sleep infinity
 elif [[ "$REBOOT_COUNT" -gt "$NUM_REBOOT" ]]; then
     assert_not_reached
 fi
index f86bc929d046fccaed9ff94bade8d0c8071dd2db..9f3d4066c3aa7e08ce052c11d52195714c8f3fea 100755 (executable)
@@ -19,6 +19,21 @@ at_exit() {
 
 trap at_exit EXIT
 
+# Because this test tests soft-reboot, we have to get rid of the symlink we put at
+# /run/nextroot to allow rebooting into the previous snapshot if the test fails for
+# the duration of the test. However, let's make sure we put the symlink back in place
+# if the test fails.
+if [[ -L /run/nextroot ]]; then
+    at_error() {
+        mountpoint -q /run/nextroot && umount -R /run/nextroot
+        rm -rf /run/nextroot
+        ln -sf /snapshot /run/nextroot
+    }
+
+    trap at_error ERR
+    rm -f /run/nextroot
+fi
+
 systemd-analyze log-level debug
 
 export SYSTEMD_LOG_LEVEL=debug