use std::{
path::{Path, PathBuf},
process::Command,
sync::Arc,
};
use anyhow::{Context, Result, anyhow};
pub use vmrunner_macros::test;
#[derive(Clone, Debug)]
pub struct TestSetup {
test_name: String,
source_system_rootfs_path: PathBuf,
system_rootfs_path: PathBuf,
state_directory_owner: Option<Arc<TestStateDirectory>>,
}
impl PartialEq for TestSetup {
fn eq(&self, other: &Self) -> bool {
self.test_name == other.test_name
&& self.source_system_rootfs_path == other.source_system_rootfs_path
&& self.system_rootfs_path == other.system_rootfs_path
}
}
impl Eq for TestSetup {}
#[derive(Debug)]
struct TestStateDirectory {
directory: tempfile::TempDir,
system_rootfs_path: PathBuf,
}
impl TestStateDirectory {
fn new(test_name: &str, source_system_rootfs_path: &Path) -> Result<Self> {
let state_parent_directory = source_system_rootfs_path
.parent()
.map(|parent| parent.join("test-state"))
.unwrap_or_else(|| std::env::temp_dir().join("vmrunner-test-state"));
fs_err::create_dir_all(&state_parent_directory).with_context(|| {
format!(
"create vmrunner test state parent '{}'",
state_parent_directory.display()
)
})?;
let safe_test_name = test_name
.chars()
.map(|ch| {
if ch.is_ascii_alphanumeric() || ch == '-' || ch == '_' {
ch
} else {
'-'
}
})
.collect::<String>();
let directory = tempfile::Builder::new()
.prefix(&format!("{safe_test_name}-"))
.tempdir_in(&state_parent_directory)
.with_context(|| {
format!(
"create vmrunner test state directory under '{}'",
state_parent_directory.display()
)
})?;
let system_rootfs_path = directory.path().join("system-rootfs");
fs_err::create_dir_all(&system_rootfs_path).with_context(|| {
format!(
"create per-test system rootfs directory '{}'",
system_rootfs_path.display()
)
})?;
Self::clone_system_rootfs_tree(source_system_rootfs_path, &system_rootfs_path)?;
Ok(Self {
directory,
system_rootfs_path,
})
}
fn system_rootfs_path(&self) -> PathBuf {
self.system_rootfs_path.clone()
}
fn path(&self) -> &Path {
self.directory.path()
}
fn clone_system_rootfs_tree(
source_system_rootfs_path: &Path,
destination_system_rootfs_path: &Path,
) -> Result<()> {
let status = Command::new("cp")
.arg("-a")
.arg("--reflink=auto")
.arg(source_system_rootfs_path.join("."))
.arg(destination_system_rootfs_path)
.status()
.with_context(|| {
format!(
"clone system rootfs '{}' into per-test system rootfs '{}'",
source_system_rootfs_path.display(),
destination_system_rootfs_path.display()
)
})?;
if !status.success() {
return Err(anyhow!(
"cp failed while cloning system rootfs '{}' into '{}' with {status}",
source_system_rootfs_path.display(),
destination_system_rootfs_path.display()
));
}
Ok(())
}
}
impl TestSetup {
pub fn new_with_system_rootfs(
test_name: impl Into<String>,
source_system_rootfs_path: impl Into<PathBuf>,
) -> Self {
let source_system_rootfs_path = source_system_rootfs_path.into();
Self {
test_name: test_name.into(),
source_system_rootfs_path: source_system_rootfs_path.clone(),
system_rootfs_path: source_system_rootfs_path,
state_directory_owner: None,
}
}
pub fn new_with_isolated_system_rootfs(
test_name: impl Into<String>,
source_system_rootfs_path: impl Into<PathBuf>,
) -> Result<Self> {
let test_name = test_name.into();
let source_system_rootfs_path = source_system_rootfs_path.into();
let state_directory = Arc::new(TestStateDirectory::new(
&test_name,
&source_system_rootfs_path,
)?);
let system_rootfs_path = state_directory.system_rootfs_path();
Ok(Self {
test_name,
source_system_rootfs_path,
system_rootfs_path,
state_directory_owner: Some(state_directory),
})
}
pub fn test_name(&self) -> &str {
&self.test_name
}
pub fn system_rootfs_path(&self) -> &Path {
&self.system_rootfs_path
}
pub fn source_system_rootfs_path(&self) -> &Path {
&self.source_system_rootfs_path
}
pub fn state_directory_path(&self) -> Option<&Path> {
self.state_directory_owner
.as_ref()
.map(|state| state.path())
}
}
#[cfg(target_os = "linux")]
pub fn run_current_test_in_unshare_child(test_name: &str) -> Result<bool> {
run_current_test_with_unshare_args(
test_name,
"VMRUNNER_UNSHARE_CHILD_TEST",
&["--user", "--map-root-user", "--net", "--fork", "--"],
"user+network namespace",
)
}
#[cfg(target_os = "linux")]
pub fn run_current_test_in_userns_child(test_name: &str) -> Result<bool> {
run_current_test_with_unshare_args(
test_name,
"VMRUNNER_USERNS_CHILD_TEST",
&["--user", "--map-root-user", "--fork", "--"],
"user namespace",
)
}
#[cfg(target_os = "linux")]
fn run_current_test_with_unshare_args(
test_name: &str,
child_env: &str,
unshare_args: &[&str],
description: &str,
) -> Result<bool> {
if std::env::var(child_env).as_deref() == Ok(test_name) {
return Ok(false);
}
let current_test_binary = std::env::current_exe().context("resolve current test binary")?;
let status = Command::new("unshare")
.args(unshare_args)
.arg(current_test_binary)
.arg("--exact")
.arg(test_name)
.arg("--nocapture")
.env(child_env, test_name)
.status()
.with_context(|| format!("launch unshare child test process in {description}"))?;
if !status.success() {
return Err(anyhow!(
"unshare child test process in {description} failed with {status}"
));
}
Ok(true)
}
#[cfg(not(target_os = "linux"))]
pub fn run_current_test_in_unshare_child(_test_name: &str) -> Result<bool> {
Err(anyhow!(
"user+network namespaces are only supported on Linux"
))
}
#[cfg(not(target_os = "linux"))]
pub fn run_current_test_in_userns_child(_test_name: &str) -> Result<bool> {
Err(anyhow!("user namespaces are only supported on Linux"))
}