summaryrefslogtreecommitdiffstats
path: root/debian/tests
diff options
context:
space:
mode:
Diffstat (limited to 'debian/tests')
-rwxr-xr-xdebian/tests/check-log266
-rw-r--r--debian/tests/control55
-rwxr-xr-xdebian/tests/hooks/drop-hostname17
-rwxr-xr-xdebian/tests/hooks/persistent-net32
-rwxr-xr-xdebian/tests/qemu-ata-only24
-rwxr-xr-xdebian/tests/qemu-busybox21
-rwxr-xr-xdebian/tests/qemu-klibc21
-rwxr-xr-xdebian/tests/qemu-net128
-rwxr-xr-xdebian/tests/qemu-panic-shell35
-rwxr-xr-xdebian/tests/qemu-separate-usr37
-rwxr-xr-xdebian/tests/qemu-virtio-only23
-rwxr-xr-xdebian/tests/run-qemu50
-rw-r--r--debian/tests/test-common204
13 files changed, 913 insertions, 0 deletions
diff --git a/debian/tests/check-log b/debian/tests/check-log
new file mode 100755
index 0000000..01b24b9
--- /dev/null
+++ b/debian/tests/check-log
@@ -0,0 +1,266 @@
+#!/usr/bin/python3
+
+# pylint: disable=invalid-name
+# pylint: enable=invalid-name
+
+"""Run given checks on the log output."""
+
+import argparse
+import json
+import pathlib
+import re
+import shlex
+import sys
+import typing
+
+
+class Check:
+ """The Check class contains all the checks that the caller can run."""
+
+ def __init__(self, log: str) -> None:
+ self.errors = 0
+ self.command_outputs = self._extract_command_outputs_from_log(log)
+
+ def _error(self, msg: str) -> None:
+ print("ERROR: " + msg)
+ self.errors += 1
+
+ @staticmethod
+ def _extract_command_outputs_from_log(log: str) -> dict[str, str]:
+ """Extract command outputs from the given log output.
+
+ The output must be framed by a header and a footer line. The header
+ line contains the key surrounded by 10 # characters. The footer
+ line consist of 40 # characters. Returns a mapping from the output
+ key to the command output.
+ """
+ marker = "#" * 10
+ footer = "#" * 40
+ matches = re.findall(
+ f"^{marker} ([^#]+) {marker}\n(.*?\n){footer}$",
+ log,
+ flags=re.DOTALL | re.MULTILINE,
+ )
+ return {m[0]: m[1] for m in matches}
+
+ def get_commands_from_ps_output(self) -> list[str]:
+ """Get list of command from `ps -ww aux` output."""
+ ps_output = self.command_outputs["ps -ww aux"]
+ lines = ps_output.strip().split("\n")[1:]
+ commands = []
+ for line in lines:
+ columns = re.split(r"\s+", line, maxsplit=10)
+ commands.append(columns[10])
+ return commands
+
+ def get_ip_addr(self) -> list[typing.Any]:
+ """Get IP address information from `ip addr` JSON output."""
+ ip_addr = json.loads(self.command_outputs["ip -json addr"])
+ assert isinstance(ip_addr, list)
+ return ip_addr
+
+ def get_ip_route(self) -> list[typing.Any]:
+ """Get IP route information from `ip route` JSON output."""
+ ip_route = json.loads(self.command_outputs["ip -json route"])
+ assert isinstance(ip_route, list)
+ return ip_route
+
+ def run_checks(self, args: list[str]) -> int:
+ """Run the checks and return the number of errors found.
+
+ The methods of this class can be u
+ """
+ if not args:
+ return self.errors
+
+ try:
+ check = getattr(self, args[0])
+ except AttributeError:
+ self._error(f"Check '{args[0]}' not found.")
+ return self.errors
+ check_args = []
+
+ for arg in args[1:]:
+ if not hasattr(self, arg):
+ check_args.append(arg)
+ continue
+
+ check(*check_args)
+ check = getattr(self, arg)
+ check_args = []
+
+ check(*check_args)
+ return self.errors
+
+ def _check_is_subset(self, actual: set[str], expected: set[str]) -> None:
+ """Check that the first dictionary is a subset of the second one.
+
+ Log errors if the sets are different.
+ """
+ unexpected = actual - expected
+ if unexpected:
+ self._error(f"Not expected entries: {unexpected}")
+
+ def _check_is_subdict(
+ self,
+ expected_dict: dict[str, str],
+ actual_dict: dict[str, str],
+ log_prefix: str,
+ ) -> None:
+ """Check that the first dictionary is a subset of the second one.
+
+ Log errors if differences are found.
+ """
+ missing_keys = set(expected_dict.keys()) - set(actual_dict.keys())
+ if missing_keys:
+ self._error(f"{log_prefix}Missing keys: {missing_keys}")
+ for key, expected_value in sorted(expected_dict.items()):
+ actual_value = actual_dict.get(key, "")
+ if expected_value != actual_value:
+ self._error(
+ f"{log_prefix}Value for key '{key}' differs:"
+ f" '{expected_value}' expected, but got '{actual_value}'"
+ )
+
+ # Below are all checks that the user might call.
+
+ def has_hostname(self, hostname_pattern: str) -> None:
+ """Check that the hostname matches the given regular expression."""
+ hostname = self.command_outputs["hostname"].strip()
+ if re.fullmatch(hostname_pattern, hostname):
+ print(f"hostname '{hostname}' matches pattern '{hostname_pattern}'")
+ return
+ self._error(
+ f"hostname '{hostname}' does not match"
+ f" expected pattern '{hostname_pattern}'"
+ )
+
+ def has_interface_mtu(self, device_pattern: str, expected_mtu: str) -> None:
+ """Check that a matching network device has the expected MTU set."""
+ for device in self.get_ip_addr():
+ if not re.fullmatch(device_pattern, device["ifname"]):
+ continue
+ if str(device["mtu"]) == expected_mtu:
+ print(f"device {device['ifname']} has MTU {device['mtu']}")
+ return
+ self._error(
+ f"device {device['ifname']} has MTU {device['mtu']}"
+ f" but expected {expected_mtu}"
+ )
+ return
+ self._error(f"no link found that matches '{device_pattern}'")
+
+ def has_ip_addr(self, family: str, addr_pattern: str, device_pattern: str) -> None:
+ """Check that a matching network device has a matching IP address."""
+ for device in self.get_ip_addr():
+ if not re.fullmatch(device_pattern, device["ifname"]):
+ continue
+ for addr in device["addr_info"]:
+ if addr["family"] != family or addr["scope"] != "global":
+ continue
+ address = f"{addr['local']}/{addr['prefixlen']}"
+ if re.fullmatch(addr_pattern, address):
+ print(f"found addr {address} for {device['ifname']}: {addr}")
+ return
+ self._error(
+ f"addr {address} for {device['ifname']}"
+ f" does not match {addr_pattern}: {addr}"
+ )
+ return
+ name = {"inet": "IPv4", "inet6": "IPv6"}[family]
+ self._error(
+ f"no link found that matches '{device_pattern}' and has an {name} address"
+ )
+
+ def has_ipv4_addr(self, addr_pattern: str, device_pattern: str) -> None:
+ """Check that a matching network device has a matching IPv4 address."""
+ self.has_ip_addr("inet", addr_pattern, device_pattern)
+
+ def has_ipv6_addr(self, addr_pattern: str, device_pattern: str) -> None:
+ """Check that a matching network device has a matching IPv6 address."""
+ self.has_ip_addr("inet6", addr_pattern, device_pattern)
+
+ def has_ipv4_default_route(self, gateway_pattern: str, device_pattern: str) -> None:
+ """Check that the IPv4 default route is via a matching gateway and device."""
+ for route in self.get_ip_route():
+ if route["dst"] != "default":
+ continue
+ if not re.fullmatch(gateway_pattern, route["gateway"]) or not re.fullmatch(
+ device_pattern, route["dev"]
+ ):
+ self._error(
+ f"Default IPv4 route does not match expected gateway pattern"
+ f" '{gateway_pattern}' or dev pattern '{device_pattern}': {route}"
+ )
+ continue
+ print(
+ f"found IPv4 default route via {route['gateway']}"
+ f" for {route['dev']}: {route}"
+ )
+ return
+ self._error("no IPv4 default route found")
+
+ def has_net_conf(self, min_expected_files: str, *expected_net_confs: str) -> None:
+ """Compare the /run/net*.conf files.
+
+ There must be at least `min_expected_files` /run/net*.conf files
+ in the log output and no unexpected one. The format for
+ `expected_net_confs` is `<file name>=<expected content>`.
+ """
+
+ expected = dict(nc.split("=", maxsplit=1) for nc in expected_net_confs)
+ prog = re.compile(r"/run/net[^#]+\.conf")
+ got = {
+ key: value for key, value in self.command_outputs.items() if prog.match(key)
+ }
+
+ if len(got) < int(min_expected_files):
+ self._error(
+ f"Expected at least {min_expected_files} /run/net*.conf files,"
+ f" but got only {len(got)}: {set(got.keys())}"
+ )
+ self._check_is_subset(set(got.keys()), set(expected.keys()))
+
+ for net_dev in sorted(got.keys()):
+ log_prefix = f"{net_dev}: "
+ expected_net_conf = parse_net_conf(expected.get(net_dev, ""))
+ actual_net_conf = parse_net_conf(got.get(net_dev, ""))
+ self._check_is_subdict(expected_net_conf, actual_net_conf, log_prefix)
+ print(f"compared {len(expected_net_conf)} items from {net_dev}")
+
+ def has_no_running_processes(self) -> None:
+ """Check that there are no remaining running processes from the initrd."""
+ processes = drop_kernel_processes(self.get_commands_from_ps_output())
+ if len(processes) == 2:
+ print(f"found only expected init and ps process: {processes}")
+ return
+ self._error(
+ f"Expected only init and ps process, but got {len(processes)}: {processes}"
+ )
+
+
+def drop_kernel_processes(processes: list[str]) -> list[str]:
+ """Return a list of processes with the kernel processes dropped."""
+ return [p for p in processes if not p.startswith("[") or not p.endswith("]")]
+
+
+def parse_net_conf(net_conf: str) -> dict[str, str]:
+ """Parse /run/net*.conf file and return a key to value mapping."""
+ items = shlex.split(net_conf)
+ return dict(item.split("=", maxsplit=1) for item in items)
+
+
+def main(arguments: list[str]) -> int:
+ """Run given checks on the log output. Return number of errors."""
+ parser = argparse.ArgumentParser()
+ parser.add_argument("log_file", metavar="log-file")
+ parser.add_argument("checks", metavar="check", nargs="+")
+ args = parser.parse_args(arguments)
+
+ log = pathlib.Path(args.log_file).read_text(encoding="ascii")
+ check = Check(log)
+ return check.run_checks(args.checks)
+
+
+if __name__ == "__main__":
+ sys.exit(main(sys.argv[1:]))
diff --git a/debian/tests/control b/debian/tests/control
new file mode 100644
index 0000000..f0fd817
--- /dev/null
+++ b/debian/tests/control
@@ -0,0 +1,55 @@
+Tests: qemu-klibc
+Architecture: amd64 armhf s390x
+Depends: genext2fs,
+ ipxe-qemu,
+ klibc-utils,
+ linux-image-generic,
+ qemu-efi-arm [armhf],
+ qemu-kvm,
+ zstd,
+ @
+
+Tests: qemu-busybox
+Architecture: amd64 armhf s390x
+Depends: busybox | busybox-initramfs,
+ genext2fs,
+ ipxe-qemu,
+ klibc-utils,
+ linux-image-generic,
+ qemu-efi-arm [armhf],
+ qemu-kvm,
+ zstd,
+ @
+
+Tests: qemu-ata-only
+Architecture: amd64
+Depends: genext2fs, klibc-utils, linux-image-generic, qemu-kvm, zstd, @
+
+Tests: qemu-virtio-only qemu-separate-usr qemu-panic-shell
+Architecture: amd64 arm64 armhf ppc64el s390x
+Depends: genext2fs,
+ ipxe-qemu,
+ klibc-utils,
+ linux-image-generic,
+ qemu-efi-aarch64 [arm64],
+ qemu-efi-arm [armhf],
+ qemu-kvm,
+ seabios [ppc64el],
+ zstd,
+ @
+
+Tests: qemu-net
+Architecture: amd64 arm64 armhf ppc64el s390x
+Depends: genext2fs,
+ iproute2,
+ ipxe-qemu,
+ klibc-utils,
+ linux-image-generic,
+ procps,
+ python3,
+ qemu-efi-aarch64 [arm64],
+ qemu-efi-arm [armhf],
+ qemu-kvm,
+ seabios [ppc64el],
+ zstd,
+ @
diff --git a/debian/tests/hooks/drop-hostname b/debian/tests/hooks/drop-hostname
new file mode 100755
index 0000000..e7b8500
--- /dev/null
+++ b/debian/tests/hooks/drop-hostname
@@ -0,0 +1,17 @@
+#!/bin/sh
+
+PREREQ=""
+
+prereqs()
+{
+ echo "$PREREQ"
+}
+
+case "$1" in
+prereqs)
+ prereqs
+ exit 0
+ ;;
+esac
+
+rm -f "${DESTDIR}/etc/hostname"
diff --git a/debian/tests/hooks/persistent-net b/debian/tests/hooks/persistent-net
new file mode 100755
index 0000000..819109e
--- /dev/null
+++ b/debian/tests/hooks/persistent-net
@@ -0,0 +1,32 @@
+#!/bin/sh
+
+PREREQ=""
+
+prereqs()
+{
+ echo "$PREREQ"
+}
+
+case "$1" in
+prereqs)
+ prereqs
+ exit 0
+ ;;
+esac
+
+persist_net() {
+ name="$1"
+ mac="$2"
+
+ mkdir -p "${DESTDIR}/etc/systemd/network"
+ cat >"${DESTDIR}/etc/systemd/network/10-persistent-${name}.link" <<EOF
+[Match]
+MACAddress=${mac}
+
+[Link]
+Name=${name}
+EOF
+}
+
+persist_net lan0 "52:54:00:65:43:21"
+persist_net lan1 "52:54:00:12:34:56"
diff --git a/debian/tests/qemu-ata-only b/debian/tests/qemu-ata-only
new file mode 100755
index 0000000..ef742a5
--- /dev/null
+++ b/debian/tests/qemu-ata-only
@@ -0,0 +1,24 @@
+#!/bin/sh -e
+
+# Note: The qemu machines used on arm64, armhf, ppc64el, and s390x have no IDE
+SUPPORTED_FLAVOURS='amd64 generic'
+ROOTDISK_QEMU_IF=ide
+ROOTDISK_LINUX_NAME=sda
+. debian/tests/test-common
+
+cat >>"${CONFDIR}/initramfs.conf" <<EOF
+MODULES=list
+BUSYBOX=n
+FSTYPE=ext2
+EOF
+cat >"${CONFDIR}/modules" <<EOF
+ext2
+ata_piix
+sd_mod
+EOF
+build_initramfs
+
+build_rootfs_ext2
+
+run_qemu
+check_no_network_configuration
diff --git a/debian/tests/qemu-busybox b/debian/tests/qemu-busybox
new file mode 100755
index 0000000..c29c41f
--- /dev/null
+++ b/debian/tests/qemu-busybox
@@ -0,0 +1,21 @@
+#!/bin/sh -e
+
+# The qemu machines on arm64 and ppc64el are too slow for MODULES=most.
+SUPPORTED_FLAVOURS='amd64 armmp s390x generic'
+. debian/tests/test-common
+
+cat >>"${CONFDIR}/initramfs.conf" <<EOF
+MODULES=most
+BUSYBOX=y
+FSTYPE=ext2
+EOF
+build_initramfs
+lsinitramfs "${INITRAMFS}" | grep -qw busybox
+
+build_rootfs_ext2
+
+run_qemu
+check_no_network_configuration
+
+# Check that fsck ran
+grep -q "^/dev/${ROOTDISK_LINUX_NAME}: clean," "${OUTPUT}"
diff --git a/debian/tests/qemu-klibc b/debian/tests/qemu-klibc
new file mode 100755
index 0000000..2111dab
--- /dev/null
+++ b/debian/tests/qemu-klibc
@@ -0,0 +1,21 @@
+#!/bin/sh -e
+
+# The qemu machines on arm64 and ppc64el are too slow for MODULES=most.
+SUPPORTED_FLAVOURS='amd64 armmp s390x generic'
+. debian/tests/test-common
+
+cat >>"${CONFDIR}/initramfs.conf" <<EOF
+MODULES=most
+BUSYBOX=n
+FSTYPE=ext2
+EOF
+build_initramfs
+! lsinitramfs "${INITRAMFS}" | grep -qw busybox
+
+build_rootfs_ext2
+
+run_qemu
+check_no_network_configuration
+
+# Check that fsck ran
+grep -q "^/dev/${ROOTDISK_LINUX_NAME}: clean," "${OUTPUT}"
diff --git a/debian/tests/qemu-net b/debian/tests/qemu-net
new file mode 100755
index 0000000..5ebd538
--- /dev/null
+++ b/debian/tests/qemu-net
@@ -0,0 +1,128 @@
+#!/bin/sh
+set -eu
+
+# Some simple tests of the initramfs network configuration.
+
+# The basic idea is to make an ext2 root image that only ships a /sbin/init to
+# just gather some data and shutdown again and boot it in qemu system
+# emulation (not KVM, so it can be run in the autopkgtest architecture without
+# hoping nested kvm works). Currently it only sets up qemu user networking
+# which limits our ability to be clever. In the long run we should set up a
+# tun and a bridge and specify the mac address of the NICs in the emulated
+# system and run dnsmasq on it so we test ipv6 and can control which ips which
+# nics get and so on -- but this is still better than nothing.
+
+# TODO: Add a test case for classless static routes. This needs support in
+# Qemu first. Following patch should be refreshed:
+# https://lore.kernel.org/all/20180314190814.22631-1-benjamin.drung@profitbricks.com/
+
+SUPPORTED_FLAVOURS='amd64 arm64 armmp powerpc64 s390x generic'
+ROOTDISK_QEMU_IF=virtio
+ROOTDISK_LINUX_NAME=vda
+. debian/tests/test-common
+
+cat >>"${CONFDIR}/initramfs.conf" <<EOF
+MODULES=list
+BUSYBOX=n
+FSTYPE=ext2
+EOF
+cat >"${CONFDIR}/modules" <<EOF
+ext2
+virtio_pci
+virtio_blk
+virtio_net
+EOF
+install -m 755 debian/tests/hooks/drop-hostname "${CONFDIR}/hooks/drop-hostname"
+install -m 755 debian/tests/hooks/persistent-net "${CONFDIR}/hooks/persistent-net"
+build_initramfs
+
+prepare_network_dumping_rootfs
+build_rootfs_ext2
+
+EXPECTED_DHCP_LAN0="
+DEVICE='lan0'
+PROTO='dhcp'
+IPV4ADDR='10.0.3.15'
+IPV4BROADCAST='10.0.3.255'
+IPV4NETMASK='255.255.255.0'
+IPV4GATEWAY='10.0.3.2'
+IPV4DNS0='10.0.3.3'
+HOSTNAME='pizza'
+DNSDOMAIN='example.com'
+ROOTSERVER='10.0.3.2'
+filename='/path/to/bootfile2'
+DOMAINSEARCH='test.'
+"
+EXPECTED_DHCP_LAN1="
+DEVICE='lan1'
+PROTO='dhcp'
+IPV4ADDR='10.0.2.15'
+IPV4BROADCAST='10.0.2.255'
+IPV4NETMASK='255.255.255.0'
+IPV4GATEWAY='10.0.2.2'
+IPV4DNS0='10.0.2.3'
+HOSTNAME='goulash'
+DNSDOMAIN='test'
+ROOTSERVER='10.0.2.2'
+filename='/path/to/bootfile'
+DOMAINSEARCH='example. example.net.'
+"
+
+run_qemu "ip=dhcp"
+check_output "Begin: Waiting up to 180 secs for any network device to become available"
+./debian/tests/check-log "${OUTPUT}" has_no_running_processes \
+ has_hostname "goulash|pizza" \
+ has_interface_mtu "lan[01]" 1500 \
+ has_ipv4_addr "10\.0\.[23]\.15/24" "lan[01]" \
+ has_ipv4_default_route "10\.0\.[23]\.2" "lan[01]" \
+ has_net_conf 1 "/run/net-lan0.conf=${EXPECTED_DHCP_LAN0}" "/run/net-lan1.conf=${EXPECTED_DHCP_LAN1}"
+
+# Test _set_netdev_from_ip_param
+run_qemu "ip=:::::lan1:dhcp"
+check_output "Begin: Waiting up to 180 secs for lan1 to become available"
+./debian/tests/check-log "${OUTPUT}" has_no_running_processes \
+ has_hostname "goulash" \
+ has_interface_mtu "lan1" 1500 \
+ has_ipv4_addr "10\.0\.2\.15/24" "lan1" \
+ has_ipv4_default_route "10\.0\.2\.2" "lan1" \
+ has_net_conf 1 "/run/net-lan1.conf=${EXPECTED_DHCP_LAN1}"
+
+# Test setting the IP address manually
+run_qemu "ip=10.0.2.100::10.0.2.2:255.0.0.0:lasagne:lan1:"
+check_output "Begin: Waiting up to 180 secs for lan1 to become available"
+./debian/tests/check-log "${OUTPUT}" has_no_running_processes \
+ has_hostname "lasagne" \
+ has_interface_mtu "lan1" 1500 \
+ has_ipv4_addr "10\.0\.2\.100/8" "lan1" \
+ has_ipv4_default_route "10\.0\.2\.2" "lan1" \
+ has_net_conf 1 "/run/net-lan1.conf=DEVICE='lan1'
+PROTO='none'
+IPV4ADDR='10.0.2.100'
+IPV4BROADCAST='10.255.255.255'
+IPV4NETMASK='255.0.0.0'
+IPV4GATEWAY='10.0.2.2'
+IPV4DNS0='0.0.0.0'
+HOSTNAME='lasagne'
+DNSDOMAIN=''
+ROOTSERVER='0.0.0.0'
+filename=''
+DOMAINSEARCH=''"
+
+# Test DHCP configuration with BOOTIF specified
+run_qemu "BOOTIF=01-52-54-00-12-34-56 ip=dhcp"
+check_output "Begin: Waiting up to 180 secs for device with address 52:54:00:12:34:56 to become available"
+./debian/tests/check-log "${OUTPUT}" has_no_running_processes \
+ has_hostname "goulash" \
+ has_interface_mtu "lan1" 1500 \
+ has_ipv4_addr "10\.0\.2\.15/24" "lan1" \
+ has_ipv4_default_route "10\.0\.2\.2" "lan1" \
+ has_net_conf 1 "/run/net-lan1.conf=${EXPECTED_DHCP_LAN1}"
+
+run_qemu "ip=on"
+check_output "Begin: Waiting up to 180 secs for any network device to become available"
+./debian/tests/check-log "${OUTPUT}" has_no_running_processes \
+ has_hostname "goulash|pizza" \
+ has_interface_mtu "lan[01]" 1500 \
+ has_ipv4_addr "10\.0\.[23]\.15/24" "lan[01]" \
+ has_ipv4_default_route "10\.0\.[23]\.2" "lan[01]" \
+ has_net_conf 1 "/run/net-lan0.conf=${EXPECTED_DHCP_LAN0}" "/run/net-lan1.conf=${EXPECTED_DHCP_LAN1}"
diff --git a/debian/tests/qemu-panic-shell b/debian/tests/qemu-panic-shell
new file mode 100755
index 0000000..296010f
--- /dev/null
+++ b/debian/tests/qemu-panic-shell
@@ -0,0 +1,35 @@
+#!/bin/sh -e
+
+SUPPORTED_FLAVOURS='amd64 arm64 armmp powerpc64 s390x generic'
+ROOTDISK_QEMU_IF=virtio
+ROOTDISK_LINUX_NAME=nonexistent
+. debian/tests/test-common
+
+cat >>"${CONFDIR}/initramfs.conf" <<EOF
+MODULES=list
+BUSYBOX=n
+FSTYPE=ext2
+EOF
+cat >"${CONFDIR}/modules" <<EOF
+ext2
+virtio_pci
+virtio_blk
+EOF
+build_initramfs
+
+build_rootfs_ext2
+
+run_qemu_nocheck
+check_no_network_configuration
+grep -qF "ALERT! /dev/nonexistent does not exist. Dropping to a shell!" "${OUTPUT}"
+grep -qF "(initramfs) " "${OUTPUT}"
+
+run_qemu_nocheck "panic=-1"
+check_no_network_configuration
+grep -qF "Rebooting automatically due to panic= boot argument" "${OUTPUT}"
+! grep -qF "(initramfs) " "${OUTPUT}"
+
+run_qemu_nocheck "panic=0"
+check_no_network_configuration
+grep -qF "Halting automatically due to panic= boot argument" "${OUTPUT}"
+! grep -qF "(initramfs) " "${OUTPUT}"
diff --git a/debian/tests/qemu-separate-usr b/debian/tests/qemu-separate-usr
new file mode 100755
index 0000000..9a252f6
--- /dev/null
+++ b/debian/tests/qemu-separate-usr
@@ -0,0 +1,37 @@
+#!/bin/sh -e
+
+SUPPORTED_FLAVOURS='amd64 arm64 armmp powerpc64 s390x generic'
+ROOTDISK_QEMU_IF=virtio
+ROOTDISK_LINUX_NAME=vda
+USRDISK="$(mktemp)"
+USRDISK_QEMU_IF=virtio
+USRDISK_LINUX_NAME=vdb
+. debian/tests/test-common
+
+cat >>"${CONFDIR}/initramfs.conf" <<EOF
+MODULES=list
+BUSYBOX=n
+FSTYPE=ext2
+EOF
+cat >"${CONFDIR}/modules" <<EOF
+ext2
+virtio_pci
+virtio_blk
+EOF
+build_initramfs
+
+# Set up /usr filesystem and fstab entry for it
+mkdir -p "${ROOTDIR}/etc"
+echo > "${ROOTDIR}/etc/fstab" "/dev/${USRDISK_LINUX_NAME} /usr ext2 defaults 0 2"
+USRDIR="$(mktemp -d)"
+mv "${ROOTDIR}/usr/"* "${USRDIR}"
+
+build_rootfs_ext2
+build_fs_ext2 "${USRDIR}" "${USRDISK}"
+
+run_qemu
+check_no_network_configuration
+
+# Check that fsck ran on both devices
+grep -q "^/dev/${ROOTDISK_LINUX_NAME}: clean," "${OUTPUT}"
+grep -q "^/dev/${USRDISK_LINUX_NAME}: clean," "${OUTPUT}"
diff --git a/debian/tests/qemu-virtio-only b/debian/tests/qemu-virtio-only
new file mode 100755
index 0000000..79670cd
--- /dev/null
+++ b/debian/tests/qemu-virtio-only
@@ -0,0 +1,23 @@
+#!/bin/sh -e
+
+SUPPORTED_FLAVOURS='amd64 arm64 armmp powerpc64 s390x generic'
+ROOTDISK_QEMU_IF=virtio
+ROOTDISK_LINUX_NAME=vda
+. debian/tests/test-common
+
+cat >>"${CONFDIR}/initramfs.conf" <<EOF
+MODULES=list
+BUSYBOX=n
+FSTYPE=ext2
+EOF
+cat >"${CONFDIR}/modules" <<EOF
+ext2
+virtio_pci
+virtio_blk
+EOF
+build_initramfs
+
+build_rootfs_ext2
+
+run_qemu
+check_no_network_configuration
diff --git a/debian/tests/run-qemu b/debian/tests/run-qemu
new file mode 100755
index 0000000..d3c3e99
--- /dev/null
+++ b/debian/tests/run-qemu
@@ -0,0 +1,50 @@
+#!/bin/sh
+set -eu
+
+# Run qemu-system for the system architecture
+
+if test "$#" -lt 3; then
+ echo "${0##*/}: Error: Not enough parameters." >&2
+ echo "Usage: ${0##*/} kernel initrd append [extra_args]" >&2
+ exit 1
+fi
+
+kernel="$1"
+initrd="$2"
+append="$3"
+shift 3
+
+ARCHITECTURE=$(dpkg --print-architecture)
+
+case "$ARCHITECTURE" in
+arm64)
+ machine="virt,gic-version=max"
+ cpu="max,pauth-impdef=on"
+ efi_code=/usr/share/AAVMF/AAVMF_CODE.fd
+ efi_vars=/usr/share/AAVMF/AAVMF_VARS.fd
+ ;;
+armhf)
+ machine="virt"
+ efi_code=/usr/share/AAVMF/AAVMF32_CODE.fd
+ efi_vars=/usr/share/AAVMF/AAVMF32_VARS.fd
+ console=ttyAMA0
+ ;;
+ppc64el)
+ machine="cap-ccf-assist=off,cap-cfpc=broken,cap-ibs=broken,cap-sbbc=broken"
+ console=hvc0
+ ;;
+esac
+
+if test -f "${efi_vars-}"; then
+ efi_vars_copy="$(mktemp -t "${efi_vars##*/}.XXXXXXXXXX")"
+ cp "$efi_vars" "$efi_vars_copy"
+fi
+
+set -- ${machine:+-machine "${machine}"} -cpu "${cpu-max}" -m 1G \
+ ${efi_code:+-drive "file=${efi_code},if=pflash,format=raw,read-only=on"} \
+ ${efi_vars:+-drive "file=${efi_vars_copy},if=pflash,format=raw"} \
+ -device virtio-rng-pci,rng=rng0 -object rng-random,filename=/dev/urandom,id=rng0 \
+ -nodefaults -no-reboot -kernel "${kernel}" -initrd "${initrd}" "$@" \
+ -append "console=${console:-ttyS0},115200 ro ${append}"
+echo "${0##*/}: qemu-system-${ARCHITECTURE} $*"
+exec "qemu-system-${ARCHITECTURE}" "$@"
diff --git a/debian/tests/test-common b/debian/tests/test-common
new file mode 100644
index 0000000..981aafe
--- /dev/null
+++ b/debian/tests/test-common
@@ -0,0 +1,204 @@
+# -*- mode: sh -*-
+
+# Find kernel flavour and release
+KVER=
+for flavour in $SUPPORTED_FLAVOURS; do
+ KVER="$(dpkg-query -Wf '${Depends}' "linux-image-${flavour}" 2>/dev/null | tr ',' '\n' | sed -n 's/^ *linux-image-\([-a-z0-9+.]*\).*/\1/p')"
+ if [ "$KVER" ]; then
+ break
+ fi
+done
+if [ -z "$KVER" ]; then
+ echo >&2 "E: Test must set SUPPORTED_FLAVOURS and depend on those flavours"
+ exit 2
+fi
+
+case "$(dpkg --print-architecture)" in
+arm64)
+ # The Ubuntu arm64 autopkgtest runs rarely into the 1200 seconds timeout.
+ QEMU_TIMEOUT=1800
+ ;;
+armhf)
+ # qemu-busybox on Ubuntu armhf runs into the 300 seconds timeout.
+ QEMU_TIMEOUT=600
+ ;;
+ppc64el)
+ # Slowest execution seen in Ubuntu ppc64el autopkgtest: 230 seconds
+ QEMU_TIMEOUT=600
+ ;;
+*)
+ QEMU_TIMEOUT=120
+esac
+
+if [ -n "${AUTOPKGTEST_TMP-}" ]; then
+ export TMPDIR="${AUTOPKGTEST_TMP}"
+fi
+
+BASEDIR="$(mktemp -d -t initramfs-test.XXXXXXXXXX)"
+
+# Skeleton configuration directory
+CONFDIR="${BASEDIR}/config"
+mkdir -p "${CONFDIR}"
+cp conf/initramfs.conf "${CONFDIR}/initramfs.conf"
+echo "RESUME=none" >>"${CONFDIR}/initramfs.conf"
+mkdir "${CONFDIR}/hooks"
+touch "${CONFDIR}/modules"
+mkdir "${CONFDIR}/scripts"
+
+# initramfs image file
+INITRAMFS="${BASEDIR}/initrd.img"
+
+# root disk image file
+ROOTDISK="${BASEDIR}/rootdisk.raw"
+
+# root disk interface type (for qemu) and device name (for Linux)
+test -n "${ROOTDISK_QEMU_IF}" || ROOTDISK_QEMU_IF=virtio
+test -n "${ROOTDISK_LINUX_NAME}" || ROOTDISK_LINUX_NAME=vda
+
+# Create a root fs with a trivial userspace
+ROOTDIR="${BASEDIR}/rootdir"
+INIT_MESSAGE='root fs init system started successfully'
+for subdir in "" dev proc run sys usr usr/bin usr/lib usr/lib64 usr/sbin; do
+ mkdir "${ROOTDIR}/${subdir}"
+done
+for subdir in bin lib lib64 sbin; do
+ ln -s "usr/$subdir" "${ROOTDIR}/${subdir}"
+done
+cat >"${ROOTDIR}/sbin/init" <<EOF
+#!/bin/sh -e
+test -b /dev/${ROOTDISK_LINUX_NAME}
+test -d /proc/1
+test -d /run/initramfs
+test -d /sys/class
+test -d /usr/bin
+echo '${INIT_MESSAGE}'
+poweroff
+EOF
+chmod a+x "${ROOTDIR}/sbin/init"
+cp /usr/lib/klibc/bin/sh "${ROOTDIR}/bin/sh"
+cp /usr/lib/klibc/bin/poweroff "${ROOTDIR}/bin/poweroff"
+cp "$(dpkg -L libklibc | grep '/klibc-.*\.so$')" "${ROOTDIR}/lib/"
+
+# VM output file
+OUTPUT="${BASEDIR}/output.log"
+
+prepare_network_dumping_rootfs() {
+ local root_dir="${1-$ROOTDIR}"
+ cat >"${root_dir}/usr/sbin/init" <<EOF
+#!/bin/sh
+echo "I: Executing /usr/sbin/init from root fs"
+# Stop the kernel from spamming the output
+current_printk=\$(sysctl kernel.printk)
+sysctl -w kernel.printk="4 4 1 7"
+# Run twice, once for the human, once for the test harness
+echo "I: ip addr"
+ip addr
+echo "I: ip route"
+ip route
+echo "I: ip -6 route"
+ip -6 route
+for file in /run/net*.conf; do
+ [ -f \$file ] || continue;
+ echo "########## \$file ##########"
+ cat \$file
+ echo "########################################"
+done
+echo "########## hostname ##########"
+cat /proc/sys/kernel/hostname
+echo "########################################"
+echo "########## ip -json addr ##########"
+ip -json addr
+echo "########################################"
+echo "########## ip -json route ##########"
+ip -json route
+echo "########################################"
+echo "########## ip -json -6 route ##########"
+ip -json -6 route
+echo "########################################"
+echo "########## ps -ww aux ##########"
+ps -ww aux
+echo "########################################"
+sysctl -w "\${current_printk}"
+echo '${INIT_MESSAGE}'
+poweroff
+EOF
+
+ . /usr/share/initramfs-tools/hook-functions
+ verbose=y
+ DESTDIR="$root_dir"
+ for binary in /usr/bin/cat /usr/bin/ip /usr/bin/ps /usr/sbin/sysctl; do
+ copy_exec "$binary"
+ done
+}
+
+build_initramfs() {
+ echo "build_initramfs: /usr/sbin/mkinitramfs -d ${CONFDIR} -o ${INITRAMFS} ${KVER}"
+ /usr/sbin/mkinitramfs -d "${CONFDIR}" -o "${INITRAMFS}" "${KVER}"
+}
+
+build_fs_ext2() {
+ local dir="${1}"
+ local disk="${2}"
+
+ # Get directory size
+ local blocks="$(du --summarize "${dir}" | cut -f 1)"
+ local inodes="$(du --summarize --inodes "${dir}" | cut -f 1)"
+
+ # Add fudge factor
+ blocks="$((blocks + 28 + blocks / 4))"
+ inodes="$((inodes + 10))"
+
+ # genext2fs writes status messages to stderr; hide that from
+ # autopkgtest
+ genext2fs 2>&1 -b "${blocks}" -N "${inodes}" -U -d "${dir}" "${disk}"
+}
+
+build_rootfs_ext2() {
+ build_fs_ext2 "${ROOTDIR}" "${ROOTDISK}"
+}
+
+_run_qemu() {
+ local extra_params="$*"
+
+ echo "I: Running qemu (with a timeout of $QEMU_TIMEOUT seconds)..."
+ timeout --foreground "$QEMU_TIMEOUT" \
+ debian/tests/run-qemu /boot/vmlinu*-"${KVER}" "${INITRAMFS}" \
+ "root=/dev/${ROOTDISK_LINUX_NAME} ${extra_params}" -nographic \
+ -drive "file=${ROOTDISK},if=${ROOTDISK_QEMU_IF},media=disk,format=raw" \
+ ${USRDISK:+-drive "file=${USRDISK},if=${USRDISK_QEMU_IF},media=disk,format=raw"} \
+ -device "virtio-net-pci,netdev=lan0,mac=52:54:00:65:43:21" \
+ -netdev "user,id=lan0,net=10.0.3.0/24,ipv6-net=fec7::/48,hostname=pizza,dnssearch=test,domainname=example.com,bootfile=/path/to/bootfile2" \
+ -device "virtio-net-pci,netdev=lan1,mac=52:54:00:12:34:56" \
+ -netdev "user,id=lan1,hostname=goulash,dnssearch=example,dnssearch=example.net,domainname=test,bootfile=/path/to/bootfile" \
+ -chardev stdio,id=char0 -serial chardev:char0 | tee "${OUTPUT}"
+}
+
+run_qemu_nocheck() {
+ # hide error messages from autopkgtest
+ _run_qemu 2>&1 "$@"
+}
+
+run_qemu() {
+ _run_qemu "panic=-1 $*"
+ grep -qF "${INIT_MESSAGE}" "${OUTPUT}"
+}
+
+check_no_output() {
+ local msg="$1"
+ if grep -qF "${msg}" "${OUTPUT}"; then
+ echo >&2 "E: Message '${msg}' found in log output '${OUTPUT}."
+ exit 1
+ fi
+}
+
+check_output() {
+ local msg="$1"
+ if ! grep -qF "${msg}" "${OUTPUT}"; then
+ echo >&2 "E: Message '${msg}' not found in log output '${OUTPUT}."
+ exit 1
+ fi
+}
+
+check_no_network_configuration() {
+ check_no_output "Waiting up to 180 secs for"
+}