From 3283aa0dc24ca428554dcad795077f993d6ec486 Mon Sep 17 00:00:00 2001 From: Johannes Leupolz Date: Fri, 16 Jan 2026 10:53:51 +0000 Subject: [PATCH] Add infrastructure to add integration tests with podman --- docs/TESTS.md | 13 ++ vuinputd-tests/Cargo.toml | 8 +- vuinputd-tests/podman/Containerfile | 8 + .../src/bin/{bwrap-ipc.rs => test-ipc.rs} | 4 +- vuinputd-tests/src/bin/test-ok.rs | 7 + vuinputd-tests/src/bwrap.rs | 55 +---- vuinputd-tests/src/ipc.rs | 62 ++++++ vuinputd-tests/src/lib.rs | 2 + vuinputd-tests/src/podman.rs | 190 ++++++++++++++++++ vuinputd-tests/tests/integration_tests.rs | 2 +- 10 files changed, 294 insertions(+), 57 deletions(-) create mode 100644 vuinputd-tests/podman/Containerfile rename vuinputd-tests/src/bin/{bwrap-ipc.rs => test-ipc.rs} (90%) create mode 100644 vuinputd-tests/src/bin/test-ok.rs create mode 100644 vuinputd-tests/src/ipc.rs create mode 100644 vuinputd-tests/src/podman.rs diff --git a/docs/TESTS.md b/docs/TESTS.md index 142c634..27d6518 100644 --- a/docs/TESTS.md +++ b/docs/TESTS.md @@ -2,12 +2,25 @@ ## Integration tests +### With bubblewrap + Install bubblewrap: `apt-get install bubblewrap`. Run with `cargo test -p vuinputd-tests --features "requires-privileges requires-uinput requires-bwrap"`. +### With podman +Install podman: +`apt-get install podman`. + +Create test container for podman +``` +cargo build -p vuinputd-tests +podman build -t vuinputd-tests -f vuinputd-tests/podman/Containerfile . +``` + +Run with `cargo test -p vuinputd-tests --features "requires-privileges requires-uinput requires-podman"`. ## Performance tests diff --git a/vuinputd-tests/Cargo.toml b/vuinputd-tests/Cargo.toml index e6da9c4..5352278 100644 --- a/vuinputd-tests/Cargo.toml +++ b/vuinputd-tests/Cargo.toml @@ -3,11 +3,14 @@ name = "vuinputd-tests" version = "0.1.0" edition = "2021" +[[bin]] +name = "test-ipc" + [[bin]] name = "test-keyboard" [[bin]] -name = "bwrap-ipc" +name = "test-ok" [dependencies] uinput-ioctls = { path = "../uinput-ioctls" } @@ -22,4 +25,5 @@ serde_json = "1.0" [features] requires-privileges = [] requires-uinput = [] -requires-bwrap = [] \ No newline at end of file +requires-bwrap = [] +requires-podman = [] \ No newline at end of file diff --git a/vuinputd-tests/podman/Containerfile b/vuinputd-tests/podman/Containerfile new file mode 100644 index 0000000..283e593 --- /dev/null +++ b/vuinputd-tests/podman/Containerfile @@ -0,0 +1,8 @@ +# Build from project root +# > cargo build -p vuinputd-tests +# > podman build -t vuinputd-tests -f vuinputd-tests/podman/Containerfile . + +FROM ubuntu:24.04 +COPY target/debug/test-ipc /test-ipc +COPY target/debug/test-keyboard /test-keyboard +COPY target/debug/test-ok /test-ok diff --git a/vuinputd-tests/src/bin/bwrap-ipc.rs b/vuinputd-tests/src/bin/test-ipc.rs similarity index 90% rename from vuinputd-tests/src/bin/bwrap-ipc.rs rename to vuinputd-tests/src/bin/test-ipc.rs index cd7a509..dcb7cf9 100644 --- a/vuinputd-tests/src/bin/bwrap-ipc.rs +++ b/vuinputd-tests/src/bin/test-ipc.rs @@ -5,10 +5,10 @@ use core::panic; use std::time::Duration; -use vuinputd_tests::bwrap::SandboxChildIpc; +use vuinputd_tests::ipc::SandboxChildIpc; fn main() { - println!("starting bwrap-ipc"); + println!("starting test-ipc"); let ipc = unsafe { SandboxChildIpc::from_fd() }; let incoming = ipc diff --git a/vuinputd-tests/src/bin/test-ok.rs b/vuinputd-tests/src/bin/test-ok.rs new file mode 100644 index 0000000..379f7b8 --- /dev/null +++ b/vuinputd-tests/src/bin/test-ok.rs @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT +// +// Author: Johannes Leupolz + +fn main() { + println!("test-ok"); +} diff --git a/vuinputd-tests/src/bwrap.rs b/vuinputd-tests/src/bwrap.rs index 2514fee..8ccdcbe 100644 --- a/vuinputd-tests/src/bwrap.rs +++ b/vuinputd-tests/src/bwrap.rs @@ -3,16 +3,17 @@ // Author: Johannes Leupolz use std::io; -use std::os::fd::{FromRawFd, IntoRawFd, OwnedFd, RawFd}; +use std::os::fd::{FromRawFd, IntoRawFd, OwnedFd}; use std::os::unix::net::UnixDatagram; use std::os::unix::process::CommandExt; use std::process::{Command, Output}; -use std::time::Duration; use nix::errno::Errno; use nix::sys::socket::{socketpair, AddressFamily, SockFlag, SockType}; use nix::unistd::close; +use crate::ipc::{SandboxChildIpc, SandboxIpc}; + /// Check if bubblewrap is available. pub fn bwrap_available() -> bool { Command::new("bwrap") @@ -22,56 +23,6 @@ pub fn bwrap_available() -> bool { .unwrap_or(false) } -/// IPC handle kept by the parent. -pub struct SandboxIpc { - sock: UnixDatagram, -} - -impl SandboxIpc { - pub fn recv(&self, read_timeout: Option) -> io::Result> { - let mut buf = vec![0u8; 4096]; - self.sock.set_read_timeout(read_timeout)?; - let n = self.sock.recv(&mut buf)?; - buf.truncate(n); - Ok(buf) - } - - pub fn send(&self, data: &[u8]) -> io::Result<()> { - self.sock.send(data)?; - Ok(()) - } -} - -/// IPC handle inside the container. -pub struct SandboxChildIpc { - sock: UnixDatagram, -} - -impl SandboxChildIpc { - /// FD number is fixed and known. - pub const FD: RawFd = 3; - - /// # Safety - /// Must only be called once in the child. - pub unsafe fn from_fd() -> Self { - let sock = UnixDatagram::from_raw_fd(Self::FD); - Self { sock } - } - - pub fn send(&self, data: &[u8]) -> io::Result<()> { - self.sock.send(data)?; - Ok(()) - } - - pub fn recv(&self, read_timeout: Option) -> io::Result> { - let mut buf = vec![0u8; 4096]; - self.sock.set_read_timeout(read_timeout)?; - let n = self.sock.recv(&mut buf)?; - buf.truncate(n); - Ok(buf) - } -} - /// Builder for bubblewrap invocations. #[derive(Default)] pub struct BwrapBuilder { diff --git a/vuinputd-tests/src/ipc.rs b/vuinputd-tests/src/ipc.rs new file mode 100644 index 0000000..d7c7208 --- /dev/null +++ b/vuinputd-tests/src/ipc.rs @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: MIT +// +// Author: Johannes Leupolz + +use std::{ + io, + os::{ + fd::{FromRawFd, RawFd}, + unix::net::UnixDatagram, + }, + time::Duration, +}; + +/// IPC handle kept by the parent. +pub struct SandboxIpc { + pub sock: UnixDatagram, +} + +impl SandboxIpc { + pub fn recv(&self, read_timeout: Option) -> io::Result> { + let mut buf = vec![0u8; 4096]; + self.sock.set_read_timeout(read_timeout)?; + let n = self.sock.recv(&mut buf)?; + buf.truncate(n); + Ok(buf) + } + + pub fn send(&self, data: &[u8]) -> io::Result<()> { + self.sock.send(data)?; + Ok(()) + } +} + +/// IPC handle inside the container. +pub struct SandboxChildIpc { + sock: UnixDatagram, +} + +impl SandboxChildIpc { + /// FD number is fixed and known. + pub const FD: RawFd = 3; + + /// # Safety + /// Must only be called once in the child. + pub unsafe fn from_fd() -> Self { + let sock = UnixDatagram::from_raw_fd(Self::FD); + Self { sock } + } + + pub fn send(&self, data: &[u8]) -> io::Result<()> { + self.sock.send(data)?; + Ok(()) + } + + pub fn recv(&self, read_timeout: Option) -> io::Result> { + let mut buf = vec![0u8; 4096]; + self.sock.set_read_timeout(read_timeout)?; + let n = self.sock.recv(&mut buf)?; + buf.truncate(n); + Ok(buf) + } +} diff --git a/vuinputd-tests/src/lib.rs b/vuinputd-tests/src/lib.rs index 9e87813..65112a5 100644 --- a/vuinputd-tests/src/lib.rs +++ b/vuinputd-tests/src/lib.rs @@ -3,5 +3,7 @@ // Author: Johannes Leupolz pub mod bwrap; +pub mod ipc; +pub mod podman; pub mod run_vuinputd; pub mod test_log; diff --git a/vuinputd-tests/src/podman.rs b/vuinputd-tests/src/podman.rs new file mode 100644 index 0000000..7a5d102 --- /dev/null +++ b/vuinputd-tests/src/podman.rs @@ -0,0 +1,190 @@ +// SPDX-License-Identifier: MIT +// +// Author: Johannes Leupolz + +use nix::errno::Errno; +use nix::sys::socket::{AddressFamily, SockFlag, SockType}; +use nix::unistd::close; +use std::io; +use std::os::fd::{FromRawFd, IntoRawFd, OwnedFd}; +use std::os::unix::net::UnixDatagram; +use std::os::unix::process::CommandExt; +use std::process::{Command, Output}; + +use crate::ipc::{SandboxChildIpc, SandboxIpc}; + +/// Check if podman is available. +pub fn podman_available() -> bool { + Command::new("podman") + .arg("--version") + .output() + .map(|o| o.status.success()) + .unwrap_or(false) +} + +/// Builder for podman run invocations. +#[derive(Default)] +pub struct PodmanBuilder { + args: Vec, + ipc_child_fd: Option, +} + +impl PodmanBuilder { + pub fn new() -> Self { + Self::default() + } + + /// `podman run` + fn run_cmd(mut self) -> Self { + self.args.push("run".into()); + self + } + + pub fn rm(mut self) -> Self { + self.args.push("--rm".into()); + self + } + + pub fn detach(mut self) -> Self { + self.args.push("--detach".into()); + self + } + + pub fn name(mut self, name: &str) -> Self { + self.args.push("--name".into()); + self.args.push(name.into()); + self + } + + pub fn tty(mut self) -> Self { + self.args.push("--tty".into()); + self + } + + pub fn interactive(mut self) -> Self { + self.args.push("--interactive".into()); + self + } + + pub fn device(mut self, spec: &str) -> Self { + self.args.push("--device".into()); + self.args.push(spec.into()); + self + } + + pub fn volume(mut self, spec: &str) -> Self { + self.args.push("-v".into()); + self.args.push(spec.into()); + self + } + + pub fn publish(mut self, spec: &str) -> Self { + self.args.push("--publish".into()); + self.args.push(spec.into()); + self + } + + pub fn env(mut self, key: &str, value: &str) -> Self { + self.args.push("-e".into()); + self.args.push(format!("{key}={value}")); + self + } + + pub fn group_add(mut self, group: u32) -> Self { + self.args.push("--group-add".into()); + self.args.push(group.to_string()); + self + } + + pub fn security_opt(mut self, opt: &str) -> Self { + self.args.push("--security-opt".into()); + self.args.push(opt.into()); + self + } + + /// Enable bidirectional IPC using a Unix seqpacket socketpair. + pub fn with_ipc(mut self) -> io::Result<(Self, SandboxIpc)> { + let (parent, child) = nix::sys::socket::socketpair( + AddressFamily::Unix, + SockType::SeqPacket, + None, + SockFlag::empty(), + ) + .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + + // Parent side + let parent_sock = unsafe { UnixDatagram::from_raw_fd(parent.into_raw_fd()) }; + + // Child side must become FD 3 inside container + self.ipc_child_fd = Some(child); + + Ok((self, SandboxIpc { sock: parent_sock })) + } + + /// Final image reference + pub fn image(mut self, image: &str) -> Self { + self.args.push(image.into()); + self + } + + /// Optional command override inside the container + pub fn command(mut self, cmd: &[&str]) -> Self { + self.args.extend(cmd.iter().map(|s| s.to_string())); + self + } + + pub fn run(mut self) -> io::Result { + println!("Arguments for podman: {:?}", &self.args); + + let mut cmd = Command::new("podman"); + + if let Some(fd) = self.ipc_child_fd.take() { + // give up ownership of ipc_child_fd in host process. + let fd = fd.into_raw_fd(); + + // Move child FD to 3. Note that the FD 3 needs to be linked at the + // beginning of the child program. + unsafe { + cmd.pre_exec(move || { + let res = libc::dup2(fd, SandboxChildIpc::FD); + Errno::result(res) + .map(drop) + .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + close(fd).ok(); + Ok(()) + }) + }; + } + + cmd.args(&self.args).output() + } +} + +#[cfg(feature = "requires-podman")] +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn podman_builder_smoke() { + if !podman_available() { + panic!("podman not available"); + } + + let out = PodmanBuilder::new() + .run_cmd() + .rm() + //.detach() + .name(&format!("vuinputd-podman-tests")) + .image("localhost/vuinputd-tests:latest") + .command(&["/test-ok"]) + .run() + .unwrap(); + + println!("Output"); + println!("stdout: {}", str::from_utf8(&out.stdout).unwrap()); + println!("stderr: {}", str::from_utf8(&out.stderr).unwrap()); + + assert!(out.status.success()); + } +} diff --git a/vuinputd-tests/tests/integration_tests.rs b/vuinputd-tests/tests/integration_tests.rs index cef9ce6..6100df1 100644 --- a/vuinputd-tests/tests/integration_tests.rs +++ b/vuinputd-tests/tests/integration_tests.rs @@ -26,7 +26,7 @@ fn test_bwrap_simple() { #[cfg(all(feature = "requires-privileges", feature = "requires-bwrap"))] #[test] fn test_bwrap_ipc() { - let bwrap_ipc = env!("CARGO_BIN_EXE_bwrap-ipc"); + let bwrap_ipc = env!("CARGO_BIN_EXE_test-ipc"); let (builder, ipc) = bwrap::BwrapBuilder::new() .unshare_all()