This introduces a functional test of vhost-user-bridge.

The test runs vhost-user-bridge and launches a guest VM that connects
to the internet through it. The test succeeds if and only if an attempt
to connect to a hard-coded well-known URL succeeds.

Signed-off-by: Yodel Eldar <[email protected]>
---

This patch introduces a functional test of vhost-user-bridge by
automating the testing described in its initial commit, 8e3b0cbb72,
with adjustments like using hubports (formerly the vlan parameter) and
memfd for the memory backend; hugepages are also omitted to avoid
requiring root privileges on the host.

The test configures networking within the guest by invoking udhcpc, then
makes an http request via wget to a well-known URL, example.org, that
has a low risk of requiring https for connections (a limitation of the
the test). An assert on the retcode of wget determines success/failure.

Please let me know if there are objections to the use of wget's retcode
as the test's condition; determining wget success through its output is
straightforward ("remote file exists"), but out of concern of some
unknown failure message (besides "bad address") locking up the test,
I've resorted to checking the retcode instead; perhaps, this violates
some convention?

Also, I figured checking for memfd support on the host was unnecessary
in 2026 for the intended users of the test, but perhaps not?

The guest's kernel contains an integrated initramfs and was built with
buildroot; an attempt to ensure bit-for-bit reproducibility was made by
building it via Containerfile based on a snapshot container image and
use of the BR2_REPRODUCIBLE option of buildroot, but the latter feature
is "experimental," so future builds may differ slightly (though the
image in the repo will be left untouched). The image and associated
build files are hosted on my personal account here:
        https://github.com/yodel/vhost-user-bridge-test
and will continue to be well into the future, but if there's some other
preferred location for the asset, please let me know?

Lastly, special thanks to Cédric for inspiring me to write the test in
"<[email protected]>".

Thanks,
Yodel

 .../x86_64/test_vhost_user_bridge.py          | 124 ++++++++++++++++++
 1 file changed, 124 insertions(+)
 create mode 100755 tests/functional/x86_64/test_vhost_user_bridge.py

diff --git a/tests/functional/x86_64/test_vhost_user_bridge.py 
b/tests/functional/x86_64/test_vhost_user_bridge.py
new file mode 100755
index 0000000000..61afdbceec
--- /dev/null
+++ b/tests/functional/x86_64/test_vhost_user_bridge.py
@@ -0,0 +1,124 @@
+#!/usr/bin/env python3
+#
+# Copyright (c) 2025 Software Freedom Conservancy, Inc.
+#
+# Author: Yodel Eldar <[email protected]>
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+"""
+Test vhost-user-bridge (vubr) functionality:
+
+    1) Run vhost-user-bridge on the host.
+    2) Launch a guest VM:
+        a) Instantiate a unix domain socket to the vubr-created path
+        b) Instantiate a vhost-user net backend on top of that socket
+        c) Expose vhost-user with a virtio-net-pci interface
+        d) Instantiate UDP socket and user-mode net backends
+        e) Hub the UDP and user-mode backends
+    3) Run udhcpc in the guest to auto-configure networking.
+    4) Run wget in the guest and check its retcode to test internet 
connectivity
+
+The test fails if wget returns 1 and succeeds on 0.
+"""
+
+import os
+import subprocess
+from qemu_test import Asset, QemuSystemTest, which
+from qemu_test import exec_command_and_wait_for_pattern
+from qemu_test import is_readable_executable_file
+from qemu_test import wait_for_console_pattern
+from qemu_test.ports import Ports
+
+class VhostUserBridge(QemuSystemTest):
+
+    ASSET_KERNEL_INITRAMFS = Asset(
+        
"https://github.com/yodel/vhost-user-bridge-test/raw/refs/heads/main/bzImage";,
+        "3790bf35e4ddfe062425bca45e923df5a5ee4de44e456d6b00cf47f04991d549")
+
+    def configure_vm(self, ud_socket_path, lport, rport):
+        kernel_path = self.ASSET_KERNEL_INITRAMFS.fetch()
+
+        self.require_accelerator("kvm")
+        self.require_netdev("vhost-user")
+        self.require_netdev("socket")
+        self.require_netdev("hubport")
+        self.require_netdev("user")
+        self.require_device("virtio-net-pci")
+        self.set_machine("q35")
+        self.vm.set_console()
+        self.vm.add_args(
+            "-cpu",      "host",
+            "-accel",    "kvm",
+            "-kernel",   kernel_path,
+            "-append",   "console=ttyS0",
+            "-smp",      "2",
+            "-m",        "128M",
+            "-object",   "memory-backend-memfd,id=mem0,"
+                         "size=128M,share=on,prealloc=on",
+            "-numa",     "node,memdev=mem0",
+            "-chardev", f"socket,id=char0,path={ud_socket_path}",
+            "-netdev",   "vhost-user,id=vhost0,chardev=char0,vhostforce=on",
+            "-device",   "virtio-net-pci,netdev=vhost0",
+            "-netdev",  f"socket,id=udp0,udp=localhost:{lport},"
+                        f"localaddr=localhost:{rport}",
+            "-netdev",   "hubport,id=hub0,hubid=0,netdev=udp0",
+            "-netdev",   "user,id=user0",
+            "-netdev",   "hubport,id=hub1,hubid=0,netdev=user0"
+        )
+
+    def assemble_vubr_args(self, vubr_path, ud_socket_path, lport, rport):
+        vubr_args = []
+
+        if (stdbuf_path := which("stdbuf")) is None:
+            self.log.info("Could not find stdbuf: vhost-user-bridge "
+                          "log lines may appear out of order")
+        else:
+            vubr_args += [stdbuf_path, "-o0", "-e0"]
+
+        vubr_args += [vubr_path, "-u", f"{ud_socket_path}",
+                      "-l", f"127.0.0.1:{lport}", "-r", f"127.0.0.1:{rport}"]
+
+        return vubr_args
+
+    def test_vhost_user_bridge(self):
+        prompt = "~ # "
+
+        vubr_path = self.build_file("tests", "vhost-user-bridge")
+        if is_readable_executable_file(vubr_path) is None:
+            self.skipTest("Could not find a readable and executable "
+                          "vhost-user-bridge")
+
+        with Ports() as ports:
+            sock_dir = self.socket_dir()
+            ud_socket_path = os.path.join(sock_dir.name, "vubr-test.sock")
+            lport, rport = ports.find_free_ports(2)
+
+            self.configure_vm(ud_socket_path, lport, rport)
+
+            vubr_log_path = self.log_file("vhost-user-bridge.log")
+            self.log.info("For the vhost-user-bridge application log,"
+                         f" see: {vubr_log_path}")
+
+            vubr_args = self.assemble_vubr_args(vubr_path, ud_socket_path,
+                                                lport, rport)
+
+            with open(vubr_log_path, "w") as vubr_log, \
+                 subprocess.Popen(vubr_args, stdin=subprocess.DEVNULL,
+                                  stdout=vubr_log, stderr=subprocess.STDOUT):
+                self.vm.launch()
+
+                wait_for_console_pattern(self, prompt)
+                exec_command_and_wait_for_pattern(self, "udhcpc -nt 1", prompt)
+                exec_command_and_wait_for_pattern(self,
+                    "wget -qT 2 --spider example.org", prompt)
+
+                try:
+                    exec_command_and_wait_for_pattern(self, "echo $?", "0", 
"1")
+                except AssertionError:
+                    self.log.error("Unable to confirm internet connectivity")
+                    raise
+                finally:
+                    self.vm.shutdown()
+
+if __name__ == '__main__':
+    QemuSystemTest.main()
-- 
2.52.0


Reply via email to