use anyhow::Result;
use ktstr::assert::{AssertDetail, AssertResult, DetailKind};
use ktstr::test_support::{Scheduler, SchedulerSpec};
const KTSTR_SCHED: Scheduler =
Scheduler::new("ktstr_sched").binary(SchedulerSpec::Discover("scx-ktstr"));
const BTRFS_SUPER_MAGIC: i64 = 0x9123_683e;
const KTSTR_DISK_BTRFS: ktstr::prelude::DiskConfig = ktstr::prelude::DiskConfig {
capacity_mb: 256,
filesystem: ktstr::prelude::Filesystem::Btrfs,
throttle: ktstr::prelude::DiskThrottle {
iops: None,
bytes_per_sec: None,
iops_burst_capacity: None,
bytes_burst_capacity: None,
},
read_only: false,
name: None,
no_auto_mount: false,
};
fn scenario_btrfs_filesystem_visible_at_dev_vda(
_ctx: &ktstr::scenario::Ctx,
) -> Result<AssertResult> {
use std::ffi::CString;
let mount_point = CString::new("/mnt/disk0").expect("/mnt/disk0 contains no nul bytes");
let mut buf: libc::statfs = unsafe { std::mem::zeroed() };
let rc = unsafe { libc::statfs(mount_point.as_ptr(), &mut buf) };
if rc != 0 {
let errno = std::io::Error::last_os_error();
anyhow::bail!(
"statfs(/mnt/disk0) failed: {errno}. The disk-template \
auto-mount must succeed before the scenario runs — \
check that the framework wired Filesystem::Btrfs through \
ensure_template and that the guest kernel has \
CONFIG_BTRFS_FS."
);
}
let fs_type = buf.f_type;
if fs_type != BTRFS_SUPER_MAGIC {
anyhow::bail!(
"/mnt/disk0 has statfs.f_type=0x{fs_type:x}, expected \
BTRFS_SUPER_MAGIC=0x{BTRFS_SUPER_MAGIC:x}. The \
disk-template lifecycle did not produce a btrfs \
filesystem on /dev/vda — possible failures: \
(a) the template-build VM ran but mkfs.btrfs reported \
success without formatting, (b) FICLONE produced a \
zeroed image instead of a clone, (c) the guest mounted \
a different filesystem (check dmesg for mount errors).",
);
}
let mut result = AssertResult::pass();
result.details.push(AssertDetail::new(
DetailKind::Other,
format!(
"/mnt/disk0 statfs.f_type=0x{fs_type:x} matches \
BTRFS_SUPER_MAGIC — the disk-template lifecycle \
(build, atomic install, FICLONE) produced a \
pre-formatted btrfs filesystem on /dev/vda"
),
));
Ok(result)
}
fn scenario_ficlone_clone_writable_and_fresh(_ctx: &ktstr::scenario::Ctx) -> Result<AssertResult> {
use std::fs;
let mount = std::path::Path::new("/mnt/disk0");
if !mount.is_dir() {
anyhow::bail!(
"/mnt/disk0 is not a directory — auto-mount of the \
btrfs disk-template clone failed. Check guest dmesg \
for mount errors and verify CONFIG_BTRFS_FS in the \
guest kernel.",
);
}
let entries_before: Vec<String> = fs::read_dir(mount)
.map_err(|e| anyhow::anyhow!("read_dir({mount:?}): {e}"))?
.filter_map(|e| e.ok())
.map(|e| e.file_name().to_string_lossy().to_string())
.collect();
let pid = std::process::id();
let sentinel_name = format!("sentinel-{pid}");
if entries_before.contains(&sentinel_name) {
anyhow::bail!(
"/mnt/disk0/{sentinel_name} already exists before this \
scenario wrote it. FICLONE did not produce a fresh \
clone — either the template-build VM left state behind, \
or the per-test fan-out reused a stranded debris file \
instead of cloning fresh. Pre-existing entries: \
{entries_before:?}"
);
}
let sentinel_path = mount.join(&sentinel_name);
fs::write(&sentinel_path, b"DISK_TEMPLATE_E2E_SENTINEL").map_err(|e| {
anyhow::anyhow!(
"write {sentinel_path:?}: {e}. The FICLONE clone is \
not writable — possible causes: (a) read_only flag \
accidentally on, (b) btrfs filesystem corrupt, \
(c) ENOSPC on the per-test backing file."
)
})?;
let body = fs::read(&sentinel_path).map_err(|e| {
anyhow::anyhow!(
"read {sentinel_path:?} after write: {e}. The btrfs \
filesystem accepted the write but the readback failed."
)
})?;
if body != b"DISK_TEMPLATE_E2E_SENTINEL" {
anyhow::bail!(
"{sentinel_path:?} readback mismatch: got {body:?}, \
expected DISK_TEMPLATE_E2E_SENTINEL. The btrfs \
filesystem accepted the write but the readback \
returned different content — possible FICLONE \
extent-sharing bug.",
);
}
let mut result = AssertResult::pass();
result.details.push(AssertDetail::new(
DetailKind::Other,
format!(
"FICLONE clone produced a fresh writable btrfs filesystem \
at /mnt/disk0; {sentinel_name} did not pre-exist, was \
written successfully, and read back byte-identical"
),
));
Ok(result)
}
#[ktstr::__private::linkme::distributed_slice(ktstr::test_support::KTSTR_TESTS)]
#[linkme(crate = ktstr::__private::linkme)]
static __KTSTR_ENTRY_BTRFS_TEMPLATE_BUILD: ktstr::test_support::KtstrTestEntry =
ktstr::test_support::KtstrTestEntry {
name: "disk_template_e2e_btrfs_template_build",
func: scenario_btrfs_filesystem_visible_at_dev_vda,
scheduler: &KTSTR_SCHED,
extra_sched_args: &[],
watchdog_timeout: std::time::Duration::from_secs(3),
duration: std::time::Duration::from_millis(500),
expect_err: false,
disk: Some(KTSTR_DISK_BTRFS),
..ktstr::test_support::KtstrTestEntry::DEFAULT
};
#[ktstr::__private::linkme::distributed_slice(ktstr::test_support::KTSTR_TESTS)]
#[linkme(crate = ktstr::__private::linkme)]
static __KTSTR_ENTRY_FICLONE_CLONE_ISOLATED: ktstr::test_support::KtstrTestEntry =
ktstr::test_support::KtstrTestEntry {
name: "disk_template_e2e_ficlone_clone_isolated",
func: scenario_ficlone_clone_writable_and_fresh,
scheduler: &KTSTR_SCHED,
extra_sched_args: &[],
watchdog_timeout: std::time::Duration::from_secs(3),
duration: std::time::Duration::from_millis(500),
expect_err: false,
disk: Some(KTSTR_DISK_BTRFS),
..ktstr::test_support::KtstrTestEntry::DEFAULT
};
const CARGO_KTSTR_BINARY: &str = env!("CARGO_BIN_EXE_cargo-ktstr");
fn linux_source_dir() -> std::path::PathBuf {
let crate_root = env!("CARGO_MANIFEST_DIR");
std::path::PathBuf::from(crate_root)
.join("..")
.join("linux")
}
fn drive_ktstr_test(scenario_name: &str) {
let source = linux_source_dir();
assert!(
source.is_dir(),
"../linux source tree missing — disk-template E2E tests \
need a kernel source tree. Expected: {}",
source.display(),
);
let output = std::process::Command::new(CARGO_KTSTR_BINARY)
.arg("ktstr")
.arg("test")
.arg("--kernel")
.arg(&source)
.arg("--")
.arg("--filter")
.arg(scenario_name)
.output()
.expect("spawn cargo-ktstr test");
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
output.status.success(),
"cargo ktstr test --filter {scenario_name} failed (exit={:?})\n\
STDOUT:\n{stdout}\n\nSTDERR:\n{stderr}",
output.status.code(),
);
}
#[test]
#[ignore = "VM integration test (~30-90s with cold cache template \
build); requires KVM, ../linux, mkfs.btrfs on PATH, \
btrfs/xfs cache dir, CONFIG_BTRFS_FS in guest. Run via \
`cargo nextest run --run-ignored all` or \
`cargo ktstr test --kernel ../linux \
--filter disk_template_e2e_btrfs_template_build`."]
fn disk_template_e2e_btrfs_template_build() {
drive_ktstr_test("disk_template_e2e_btrfs_template_build");
}
#[test]
#[ignore = "VM integration test (~10-90s); requires KVM, ../linux, \
mkfs.btrfs on PATH, btrfs/xfs cache dir, CONFIG_BTRFS_FS \
in guest. Run via `cargo nextest run --run-ignored all` \
or `cargo ktstr test --kernel ../linux \
--filter disk_template_e2e_ficlone_clone_isolated`."]
fn disk_template_e2e_ficlone_clone_isolated() {
drive_ktstr_test("disk_template_e2e_ficlone_clone_isolated");
}