mod common;
use anyhow::Result;
use common::failure_dump::read_dump_skip_placeholder;
use ktstr::assert::{AssertDetail, AssertResult, DetailKind};
use ktstr::prelude::{VmResult, post_vm_skip};
use ktstr::scenario::ops::{HoldSpec, Step, await_accessor_ready, execute_steps};
use ktstr::test_support::{Scheduler, SchedulerSpec};
const KTSTR_SCHED: Scheduler =
Scheduler::named("ktstr_sched").binary(SchedulerSpec::Discover("scx-ktstr"));
fn run_stalled_workload(ctx: &ktstr::scenario::Ctx) -> Result<AssertResult> {
await_accessor_ready();
let steps = vec![Step {
setup: vec![ctx.cgroup_def("cg_0")].into(),
ops: vec![],
hold: HoldSpec::FULL,
}];
execute_steps(ctx, steps)
}
fn scenario_dsq_and_rq_walker_populates_failure_dump(
ctx: &ktstr::scenario::Ctx,
) -> Result<AssertResult> {
run_stalled_workload(ctx)
}
fn check_dsq_and_rq_walker(result: &VmResult) -> Result<()> {
let dump = read_dump_skip_placeholder(result)?;
if let Some(reason) = dump.get("scx_walker_unavailable").and_then(|v| v.as_str()) {
return Err(post_vm_skip(format!(
"scx walker unavailable ({reason}) — the DSQ / rq->scx walk could \
not run, so its capture cannot be verified"
)));
}
let dsq_states: &[serde_json::Value] = match dump.get("dsq_states") {
Some(s) => s
.as_array()
.map(|a| a.as_slice())
.ok_or_else(|| anyhow::anyhow!("dsq_states is present but not an array: {s}"))?,
None => &[],
};
if dsq_states.is_empty() {
anyhow::bail!(
"dsq_states is empty (absent or zero-length) despite the walker \
reporting available. The walker resolved BTF offsets and \
translated *scx_root but the IDR walk yielded no DSQs."
);
}
let rq_scx_states: &[serde_json::Value] = match dump.get("rq_scx_states") {
Some(s) => s
.as_array()
.map(|a| a.as_slice())
.ok_or_else(|| anyhow::anyhow!("rq_scx_states is present but not an array: {s}"))?,
None => &[],
};
if rq_scx_states.is_empty() {
anyhow::bail!(
"rq_scx_states is empty (absent or zero-length). Per-CPU rq->scx \
walk failed wholesale — every CPU's percpu translation errored \
or the offsets were unavailable."
);
}
eprintln!(
"scx walker captured {} DSQ entries and {} rq->scx entries from \
frozen-VM walk",
dsq_states.len(),
rq_scx_states.len(),
);
Ok(())
}
fn scenario_perf_counters_capture_populates_dump(
ctx: &ktstr::scenario::Ctx,
) -> Result<AssertResult> {
run_stalled_workload(ctx)
}
fn check_perf_counters_capture(result: &VmResult) -> Result<()> {
let dump = read_dump_skip_placeholder(result)?;
let vcpu_perf: &[serde_json::Value] = match dump.get("vcpu_perf_at_freeze") {
Some(v) => v
.as_array()
.map(|a| a.as_slice())
.ok_or_else(|| anyhow::anyhow!("vcpu_perf_at_freeze present but not an array: {v}"))?,
None => &[],
};
if vcpu_perf.is_empty() {
return Err(post_vm_skip(
"vcpu_perf_at_freeze is empty — perf_event_open(exclude_host=1) is \
unavailable on this host (raise it with `sysctl \
kernel.perf_event_paranoid=2` or lower); the perf capture cannot \
be verified here",
));
}
let populated: Vec<&serde_json::Value> =
vcpu_perf.iter().filter(|slot| slot.is_object()).collect();
if populated.is_empty() {
anyhow::bail!(
"vcpu_perf_at_freeze has {} entries but every slot is null \
(read(2) failed for every vCPU). Capture wiring may be broken: \
check perf_event_attr.exclude_host and that the per-vCPU fd \
remained valid through freeze.",
vcpu_perf.len(),
);
}
eprintln!(
"vcpu_perf_at_freeze: {}/{} vCPUs reported a non-null \
perf_event_open(exclude_host=1) sample at freeze",
populated.len(),
vcpu_perf.len(),
);
Ok(())
}
fn scenario_event_counter_timeline(ctx: &ktstr::scenario::Ctx) -> Result<AssertResult> {
run_stalled_workload(ctx)
}
fn check_event_counter_timeline(result: &VmResult) -> Result<()> {
let monitor = result.monitor.as_ref().ok_or_else(|| {
anyhow::anyhow!("VmResult.monitor is None — the monitor sampler produced no report")
})?;
if monitor.samples.is_empty() {
return Err(post_vm_skip(
"monitor recorded no samples — the per-tick loop did not tick before \
the stall fired (load-starved); the SCX_EV_* capture cannot be \
verified",
));
}
let with_events = monitor
.samples
.iter()
.filter(|s| s.cpus.iter().any(|c| c.event_counters.is_some()))
.count();
anyhow::ensure!(
with_events > 0,
"no monitor sample carried SCX_EV_* event counters across {} samples — \
the per-tick capture resolved no ScxEventCounters offsets from BTF, or \
scx_root was unset on every CPU at every tick",
monitor.samples.len(),
);
eprintln!(
"event-counter capture: {}/{} monitor samples carried SCX_EV_* counters",
with_events,
monitor.samples.len(),
);
Ok(())
}
fn scenario_sched_deadline_real_setattr(ctx: &ktstr::scenario::Ctx) -> Result<AssertResult> {
use ktstr::workload::{AffinityIntent, SchedPolicy, WorkType, WorkloadConfig, WorkloadHandle};
use std::time::Duration;
let config = WorkloadConfig {
num_workers: 1,
work_type: WorkType::SpinWait,
affinity: AffinityIntent::Inherit,
sched_policy: SchedPolicy::Deadline {
runtime: Duration::from_micros(500),
deadline: Duration::from_millis(1),
period: Duration::from_millis(10),
},
..Default::default()
};
let mut handle = WorkloadHandle::spawn(&config)?;
handle.start();
std::thread::sleep(ctx.duration);
let reports = handle.stop_and_collect();
let mut result = AssertResult::pass();
if reports.is_empty() {
result.record_fail(AssertDetail::new(
DetailKind::Other,
"SCHED_DEADLINE worker produced no report — sched_setattr likely \
rejected the params"
.to_string(),
));
return Ok(result);
}
let r = &reports[0];
if !r.completed {
result.record_fail(AssertDetail::new(
DetailKind::Other,
format!(
"SCHED_DEADLINE worker reported completed=false (sentinel) — \
sched_setattr returned an error or the worker died before \
the work loop. exit_info={:?}, work_units={}",
r.exit_info, r.work_units,
),
));
return Ok(result);
}
if r.work_units == 0 {
result.record_fail(AssertDetail::new(
DetailKind::Other,
format!(
"SCHED_DEADLINE worker reported work_units=0 — the SCHED_DL \
band did not grant any run time within the declared period. \
wall_time_ns={}, cpu_time_ns={}",
r.wall_time_ns, r.cpu_time_ns,
),
));
return Ok(result);
}
result.note(format!(
"SCHED_DEADLINE worker completed cleanly: tid={}, work_units={}, \
wall_time_ns={}, cpu_time_ns={} — sched_setattr(2) syscall \
path verified end-to-end on real kernel",
r.tid, r.work_units, r.wall_time_ns, r.cpu_time_ns,
));
Ok(result)
}
fn scenario_failure_dump_trigger_minimal_invariants(
ctx: &ktstr::scenario::Ctx,
) -> Result<AssertResult> {
run_stalled_workload(ctx)
}
fn check_failure_dump_trigger(result: &VmResult) -> Result<()> {
let dump = read_dump_skip_placeholder(result)?;
let schema = dump
.get("schema")
.and_then(|s| s.as_str())
.ok_or_else(|| anyhow::anyhow!("dump JSON missing top-level `schema` field"))?;
if schema != "single" {
anyhow::bail!(
"schema discriminant is {schema:?}, expected \"single\". \
A rename or refactor of the schema constant must update \
every consumer that pins this string."
);
}
let maps = dump
.get("maps")
.and_then(|m| m.as_array())
.ok_or_else(|| anyhow::anyhow!("dump JSON missing top-level `maps` array"))?;
if maps.is_empty() {
anyhow::bail!(
"dump JSON `maps` array is empty — BPF map enumeration did not \
find a single map after the SCX_EXIT_ERROR_STALL freeze. The \
scheduler always loads at least the .bss + arena maps; an \
empty map list means dump_state's IDR walk is broken."
);
}
let vcpu_regs = dump
.get("vcpu_regs")
.and_then(|v| v.as_array())
.ok_or_else(|| anyhow::anyhow!("dump JSON missing top-level `vcpu_regs` array"))?;
if vcpu_regs.is_empty() {
anyhow::bail!(
"dump JSON `vcpu_regs` array is empty — freeze rendezvous \
collected no vCPU snapshots. Either the rendezvous timed out \
before any vCPU completed handle_freeze, or the regs-attach \
callback was never registered."
);
}
eprintln!(
"failure-dump trigger pipeline produced schema={schema:?}, \
{} maps, {} vcpu_regs entries — full-stack capture path \
verified end-to-end",
maps.len(),
vcpu_regs.len(),
);
Ok(())
}
const KTSTR_DISK_DEFAULT: ktstr::prelude::DiskConfig = ktstr::prelude::DiskConfig {
capacity_mib: 256,
filesystem: ktstr::prelude::Filesystem::Raw,
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,
};
const KTSTR_DISK_READ_ONLY: ktstr::prelude::DiskConfig = ktstr::prelude::DiskConfig {
capacity_mib: 256,
filesystem: ktstr::prelude::Filesystem::Raw,
throttle: ktstr::prelude::DiskThrottle {
iops: None,
bytes_per_sec: None,
iops_burst_capacity: None,
bytes_burst_capacity: None,
},
read_only: true,
name: None,
no_auto_mount: false,
};
fn scenario_disk_default_appears_at_dev_vda(_ctx: &ktstr::scenario::Ctx) -> Result<AssertResult> {
use std::fs::OpenOptions;
use std::os::unix::fs::FileTypeExt;
use std::os::unix::io::AsRawFd;
let path = std::path::Path::new("/dev/vda");
let metadata = std::fs::metadata(path).map_err(|e| {
anyhow::anyhow!(
"/dev/vda missing in guest: {e}. The virtio-blk device was \
not attached, the guest kernel does not have CONFIG_VIRTIO_BLK, \
or the MMIO probe failed before devtmpfs populated /dev/vda."
)
})?;
let ftype = metadata.file_type();
if !ftype.is_block_device() {
anyhow::bail!(
"/dev/vda exists but is not a block device (file_type={ftype:?}). \
devtmpfs created the node but the underlying device is not \
a real virtio-blk; check the kernel-side virtio probe path."
);
}
let file = OpenOptions::new()
.read(true)
.open(path)
.map_err(|e| anyhow::anyhow!("open /dev/vda for capacity probe: {e}"))?;
let mut size_bytes: u64 = 0;
let rc = unsafe { libc::ioctl(file.as_raw_fd(), 0x80081272, &mut size_bytes as *mut u64) };
if rc != 0 {
let errno = std::io::Error::last_os_error();
anyhow::bail!(
"BLKGETSIZE64 on /dev/vda returned {rc} (errno={errno}). The \
kernel did not surface a capacity through the virtio config \
space — possible config-space layout mismatch."
);
}
let expected_bytes = (KTSTR_DISK_DEFAULT.capacity_mib as u64) << 20;
if size_bytes != expected_bytes {
anyhow::bail!(
"BLKGETSIZE64 on /dev/vda reported {size_bytes} bytes; \
expected {expected_bytes} ({} MiB). The host advertised \
a different capacity than the test configured.",
KTSTR_DISK_DEFAULT.capacity_mib,
);
}
let mut result = AssertResult::pass();
result.note(format!(
"/dev/vda is a block device with capacity {size_bytes} bytes \
({} MiB), matching the configured DiskConfig",
KTSTR_DISK_DEFAULT.capacity_mib,
));
Ok(result)
}
fn scenario_disk_write_read_roundtrip(_ctx: &ktstr::scenario::Ctx) -> Result<AssertResult> {
use std::fs::OpenOptions;
use std::io::{Read, Seek, SeekFrom, Write};
use std::os::unix::fs::OpenOptionsExt;
const SECTOR_SIZE: usize = 512;
const PATTERN_BYTE: u8 = 0xA5;
#[repr(align(512))]
struct AlignedSector([u8; SECTOR_SIZE]);
let path = std::path::Path::new("/dev/vda");
let mut file = OpenOptions::new()
.read(true)
.write(true)
.open(path)
.map_err(|e| {
anyhow::anyhow!(
"open /dev/vda for read+write: {e}. The disk should be \
attached read-write by default; if the host advertised \
VIRTIO_BLK_F_RO unexpectedly the kernel would refuse \
O_WRONLY on the device node."
)
})?;
let pattern = [PATTERN_BYTE; SECTOR_SIZE];
file.seek(SeekFrom::Start(0))
.map_err(|e| anyhow::anyhow!("seek to sector 0 for write: {e}"))?;
file.write_all(&pattern)
.map_err(|e| anyhow::anyhow!("write pattern to sector 0: {e}"))?;
file.sync_all()
.map_err(|e| anyhow::anyhow!("fsync /dev/vda after write: {e}"))?;
let mut readback = OpenOptions::new()
.read(true)
.custom_flags(libc::O_DIRECT)
.open(path)
.map_err(|e| anyhow::anyhow!("re-open /dev/vda for O_DIRECT readback: {e}"))?;
let mut buf = AlignedSector([0u8; SECTOR_SIZE]);
readback
.seek(SeekFrom::Start(0))
.map_err(|e| anyhow::anyhow!("seek to sector 0 for read: {e}"))?;
readback
.read_exact(&mut buf.0)
.map_err(|e| anyhow::anyhow!("O_DIRECT read sector 0: {e}"))?;
if buf.0 != pattern {
let first_bad = buf
.0
.iter()
.zip(pattern.iter())
.position(|(a, b)| a != b)
.unwrap_or(0);
anyhow::bail!(
"/dev/vda sector 0 readback mismatch: byte {first_bad} \
read=0x{:02X} expected=0x{:02X}. The first 16 bytes \
read back as {:02X?}, expected {:02X?}.",
buf.0[first_bad],
pattern[first_bad],
&buf.0[..16.min(buf.0.len())],
&pattern[..16.min(pattern.len())],
);
}
let mut result = AssertResult::pass();
result.note(format!(
"{SECTOR_SIZE}-byte pattern written to sector 0 round-tripped \
cleanly through virtio-blk write+fsync+read"
));
Ok(result)
}
fn scenario_disk_read_only_rejects_write(_ctx: &ktstr::scenario::Ctx) -> Result<AssertResult> {
use std::fs::OpenOptions;
use std::io::Write;
let path = std::path::Path::new("/dev/vda");
{
let _r = OpenOptions::new()
.read(true)
.open(path)
.map_err(|e| anyhow::anyhow!("open /dev/vda read-only: {e}"))?;
}
let ro = std::fs::read_to_string("/sys/block/vda/ro").map_err(|e| {
anyhow::anyhow!(
"read /sys/block/vda/ro: {e} — /dev/vda likely did not \
register (virtio_blk probe failed, or it is not a block \
device)"
)
})?;
anyhow::ensure!(
ro.trim() == "1",
"/sys/block/vda/ro is {ro:?}, expected \"1\" — the read-only chain \
broke at the host (VIRTIO_BLK_F_RO not advertised) or the guest \
(driver did not mark the gendisk read-only)"
);
let mut fd = OpenOptions::new().write(true).open(path).map_err(|e| {
anyhow::anyhow!(
"open(/dev/vda, O_WRONLY) failed with {e} — the kernel admits \
write-opens on a read-only bdev (rejection is at write time), \
so this open must succeed"
)
})?;
match fd.write(&[0u8]) {
Ok(0) => anyhow::bail!(
"write(&[0u8]) to /dev/vda returned Ok(0) (zero progress) — \
expected Err(EPERM); the kernel took the write fd but \
reported no bytes written instead of rejecting the write"
),
Ok(n) => anyhow::bail!(
"write({n} bytes) to /dev/vda SUCCEEDED on a read-only disk — \
the kernel did not reject the write; VIRTIO_BLK_F_RO is not \
honored end-to-end"
),
Err(e) => {
let raw_errno = e.raw_os_error();
anyhow::ensure!(
raw_errno == Some(libc::EPERM),
"write to /dev/vda failed errno={raw_errno:?}, expected EPERM \
({}) — rejected for a reason other than read-only (EIO would \
mean the host's in-device IOERR gate fired; EBADF/EFAULT a \
test bug)",
libc::EPERM
);
}
}
let mut result = AssertResult::pass();
result.note(
"/sys/block/vda/ro==1 and write() returned EPERM — VIRTIO_BLK_F_RO \
is honored end-to-end (read-only gendisk, write-time rejection)",
);
Ok(result)
}
fn assert_virtio_blk_counters_nonzero_after_roundtrip(
result: &ktstr::prelude::VmResult,
) -> Result<()> {
let counters = result.virtio_blk_counters.as_ref().ok_or_else(|| {
anyhow::anyhow!(
"result.virtio_blk_counters is None despite the entry attaching \
a DiskConfig — the snapshot-at-assignment seam in \
freeze_coord did not populate VmResult.virtio_blk_counters, \
or the device was never wired into the run state at all"
)
})?;
anyhow::ensure!(
counters.reads_completed >= 1,
"reads_completed = {} (expected >= 1 after the readback in \
scenario_disk_write_read_roundtrip): the guest performed a 512-byte \
pread of /dev/vda sector 0 but no read landed on the host counter — \
the device never observed the request OR the counter was snapshotted \
before record_read fired",
counters.reads_completed,
);
anyhow::ensure!(
counters.writes_completed >= 1,
"writes_completed = {} (expected >= 1 after the pattern write): the \
guest performed a 512-byte pwrite + fsync but no write landed on the \
host counter",
counters.writes_completed,
);
anyhow::ensure!(
counters.bytes_read >= 512,
"bytes_read = {} (expected >= 512 = one full sector readback): the \
guest issued the readback but the byte total never reached the \
counter (record_read parameter wrong, or the read never reached the \
backing-file `pread`)",
counters.bytes_read,
);
anyhow::ensure!(
counters.bytes_written >= 512,
"bytes_written = {} (expected >= 512 = one full sector pattern \
write): the guest issued the pwrite + fsync but the byte total never \
reached the counter (record_write parameter wrong, or the write never \
reached the backing-file `pwrite`)",
counters.bytes_written,
);
anyhow::ensure!(
counters.flushes_completed >= 1,
"flushes_completed = {} (expected >= 1 after fsync): the guest \
issued sync_all on /dev/vda which translates to VIRTIO_BLK_T_FLUSH \
per the virtio-spec + drivers/block/virtio_blk.c REQ_OP_FLUSH path, \
but no flush landed on the host counter — the handle_flush / \
record_flush wiring may be broken or the snapshot fired before \
the flush completion",
counters.flushes_completed,
);
const SANE_REQ_CAP: u64 = 1000;
const SANE_BYTES_CAP: u64 = 64 * 1024 * 1024;
anyhow::ensure!(
counters.writes_completed < SANE_REQ_CAP,
"writes_completed = {} exceeds sanity cap {SANE_REQ_CAP}: the \
operator issued 1 write; counter-bump regression suspected \
(multiplication / loop / unit confusion)",
counters.writes_completed,
);
anyhow::ensure!(
counters.bytes_written < SANE_BYTES_CAP,
"bytes_written = {} exceeds sanity cap {SANE_BYTES_CAP} (64 MiB) \
on a single 512-byte operator write: counter-bump regression \
suspected (bytes-as-bits, multi-counting, or byte-count parameter \
mismatch with record_write)",
counters.bytes_written,
);
anyhow::ensure!(
counters.reads_completed < SANE_REQ_CAP,
"reads_completed = {} exceeds sanity cap {SANE_REQ_CAP}: the \
operator issued 1 O_DIRECT read; kernel partition probes plausibly \
add a handful more. Above {SANE_REQ_CAP} indicates a \
counter-bump regression",
counters.reads_completed,
);
anyhow::ensure!(
counters.bytes_read < SANE_BYTES_CAP,
"bytes_read = {} exceeds sanity cap {SANE_BYTES_CAP} (64 MiB) \
on a single 512-byte O_DIRECT read + a handful of partition \
probes: counter-bump regression suspected",
counters.bytes_read,
);
Ok(())
}
#[ktstr::distributed_slice(ktstr::test_support::KTSTR_TESTS)]
#[linkme(crate = ktstr::linkme)]
static __KTSTR_ENTRY_DSQ_RQ_WALKER: ktstr::test_support::KtstrTestEntry =
ktstr::test_support::KtstrTestEntry {
name: "vm_integration_dsq_and_rq_walker",
func: scenario_dsq_and_rq_walker_populates_failure_dump,
scheduler: &KTSTR_SCHED,
extra_sched_args: &["--stall-after=1"],
watchdog_timeout: std::time::Duration::from_secs(3),
duration: std::time::Duration::from_secs(10),
expect_err: true,
post_vm_unconditional: Some(check_dsq_and_rq_walker),
..ktstr::test_support::KtstrTestEntry::DEFAULT
};
#[ktstr::distributed_slice(ktstr::test_support::KTSTR_TESTS)]
#[linkme(crate = ktstr::linkme)]
static __KTSTR_ENTRY_PERF_COUNTERS: ktstr::test_support::KtstrTestEntry =
ktstr::test_support::KtstrTestEntry {
name: "vm_integration_perf_counters_capture",
func: scenario_perf_counters_capture_populates_dump,
scheduler: &KTSTR_SCHED,
extra_sched_args: &["--stall-after=1"],
watchdog_timeout: std::time::Duration::from_secs(3),
duration: std::time::Duration::from_secs(10),
expect_err: true,
post_vm_unconditional: Some(check_perf_counters_capture),
..ktstr::test_support::KtstrTestEntry::DEFAULT
};
#[ktstr::distributed_slice(ktstr::test_support::KTSTR_TESTS)]
#[linkme(crate = ktstr::linkme)]
static __KTSTR_ENTRY_EVENT_TIMELINE: ktstr::test_support::KtstrTestEntry =
ktstr::test_support::KtstrTestEntry {
name: "vm_integration_event_counter_timeline",
func: scenario_event_counter_timeline,
scheduler: &KTSTR_SCHED,
extra_sched_args: &["--stall-after=1"],
watchdog_timeout: std::time::Duration::from_secs(3),
duration: std::time::Duration::from_secs(15),
expect_err: true,
post_vm_unconditional: Some(check_event_counter_timeline),
..ktstr::test_support::KtstrTestEntry::DEFAULT
};
#[ktstr::distributed_slice(ktstr::test_support::KTSTR_TESTS)]
#[linkme(crate = ktstr::linkme)]
static __KTSTR_ENTRY_DEADLINE: ktstr::test_support::KtstrTestEntry =
ktstr::test_support::KtstrTestEntry {
name: "vm_integration_sched_deadline",
func: scenario_sched_deadline_real_setattr,
scheduler: &KTSTR_SCHED,
extra_sched_args: &[],
watchdog_timeout: std::time::Duration::from_secs(3),
duration: std::time::Duration::from_millis(500),
expect_err: false,
..ktstr::test_support::KtstrTestEntry::DEFAULT
};
#[ktstr::distributed_slice(ktstr::test_support::KTSTR_TESTS)]
#[linkme(crate = ktstr::linkme)]
static __KTSTR_ENTRY_DUMP_TRIGGER: ktstr::test_support::KtstrTestEntry =
ktstr::test_support::KtstrTestEntry {
name: "vm_integration_failure_dump_trigger",
func: scenario_failure_dump_trigger_minimal_invariants,
scheduler: &KTSTR_SCHED,
extra_sched_args: &["--stall-after=1"],
watchdog_timeout: std::time::Duration::from_secs(3),
duration: std::time::Duration::from_secs(10),
expect_err: true,
post_vm_unconditional: Some(check_failure_dump_trigger),
..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 — VM 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 = "requires KVM, ../linux, scx-ktstr, kernel BTF"]
fn vm_integration_dsq_and_rq_walker() {
drive_ktstr_test("vm_integration_dsq_and_rq_walker");
}
#[test]
#[ignore = "requires KVM, ../linux, scx-ktstr, kernel.perf_event_paranoid <= 2 (CAP_PERFMON or root)"]
fn vm_integration_perf_counters_capture() {
drive_ktstr_test("vm_integration_perf_counters_capture");
}
#[test]
#[ignore = "requires KVM, ../linux, scx-ktstr"]
fn vm_integration_event_counter_timeline() {
drive_ktstr_test("vm_integration_event_counter_timeline");
}
#[test]
#[ignore = "requires KVM, ../linux, scx-ktstr, CONFIG_SCHED_DEADLINE in guest"]
fn vm_integration_sched_deadline() {
drive_ktstr_test("vm_integration_sched_deadline");
}
#[test]
#[ignore = "requires KVM, ../linux, scx-ktstr"]
fn vm_integration_failure_dump_trigger() {
drive_ktstr_test("vm_integration_failure_dump_trigger");
}
#[ktstr::distributed_slice(ktstr::test_support::KTSTR_TESTS)]
#[linkme(crate = ktstr::linkme)]
static __KTSTR_ENTRY_DISK_DEFAULT: ktstr::test_support::KtstrTestEntry =
ktstr::test_support::KtstrTestEntry {
name: "vm_integration_disk_default_appears",
func: scenario_disk_default_appears_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_DEFAULT),
..ktstr::test_support::KtstrTestEntry::DEFAULT
};
#[ktstr::distributed_slice(ktstr::test_support::KTSTR_TESTS)]
#[linkme(crate = ktstr::linkme)]
static __KTSTR_ENTRY_DISK_ROUNDTRIP: ktstr::test_support::KtstrTestEntry =
ktstr::test_support::KtstrTestEntry {
name: "vm_integration_disk_write_read_roundtrip",
func: scenario_disk_write_read_roundtrip,
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_DEFAULT),
..ktstr::test_support::KtstrTestEntry::DEFAULT
};
#[ktstr::distributed_slice(ktstr::test_support::KTSTR_TESTS)]
#[linkme(crate = ktstr::linkme)]
static __KTSTR_ENTRY_DISK_READ_ONLY: ktstr::test_support::KtstrTestEntry =
ktstr::test_support::KtstrTestEntry {
name: "vm_integration_disk_read_only_rejects_write",
func: scenario_disk_read_only_rejects_write,
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_READ_ONLY),
..ktstr::test_support::KtstrTestEntry::DEFAULT
};
#[ktstr::distributed_slice(ktstr::test_support::KTSTR_TESTS)]
#[linkme(crate = ktstr::linkme)]
static __KTSTR_ENTRY_DISK_COUNTERS_HANDOFF: ktstr::test_support::KtstrTestEntry =
ktstr::test_support::KtstrTestEntry {
name: "vm_integration_disk_virtio_blk_counters_handoff",
func: scenario_disk_write_read_roundtrip,
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_DEFAULT),
post_vm: Some(assert_virtio_blk_counters_nonzero_after_roundtrip),
..ktstr::test_support::KtstrTestEntry::DEFAULT
};
#[test]
#[ignore = "requires KVM, ../linux, CONFIG_VIRTIO_BLK in guest"]
fn vm_integration_disk_default_appears() {
drive_ktstr_test("vm_integration_disk_default_appears");
}
#[test]
#[ignore = "requires KVM, ../linux, CONFIG_VIRTIO_BLK in guest"]
fn vm_integration_disk_write_read_roundtrip() {
drive_ktstr_test("vm_integration_disk_write_read_roundtrip");
}
#[test]
#[ignore = "requires KVM, ../linux, CONFIG_VIRTIO_BLK in guest"]
fn vm_integration_disk_read_only_rejects_write() {
drive_ktstr_test("vm_integration_disk_read_only_rejects_write");
}
#[test]
#[ignore = "requires KVM, ../linux, CONFIG_VIRTIO_BLK in guest"]
fn vm_integration_disk_virtio_blk_counters_handoff() {
drive_ktstr_test("vm_integration_disk_virtio_blk_counters_handoff");
}