#![cfg(test)]
use super::*;
use crate::test_support::TopologyConstraints;
#[test]
fn shell_quote_preserves_safe_strings() {
assert_eq!(shell_quote("simple"), "simple");
assert_eq!(shell_quote("--foo=bar"), "--foo=bar");
assert_eq!(shell_quote("/usr/bin/foo"), "/usr/bin/foo");
assert_eq!(shell_quote("a.b-c_d"), "a.b-c_d");
}
#[test]
fn shell_quote_wraps_special_chars() {
assert_eq!(shell_quote("with space"), "'with space'");
assert_eq!(shell_quote("a;b"), "'a;b'");
assert_eq!(shell_quote("$VAR"), "'$VAR'");
}
#[test]
fn shell_quote_escapes_embedded_single_quotes() {
assert_eq!(shell_quote("don't"), "'don'\\''t'");
}
#[test]
fn shell_quote_empty_string_yields_quoted_empty() {
assert_eq!(shell_quote(""), "''");
}
#[test]
fn shell_quote_tab() {
assert_eq!(shell_quote("a\tb"), "'a\tb'");
}
#[test]
fn shell_quote_newline() {
assert_eq!(shell_quote("a\nb"), "'a\nb'");
}
#[test]
fn shell_quote_backslash() {
assert_eq!(shell_quote(r"a\b"), r"'a\b'");
assert_eq!(shell_quote(r"trail\"), r"'trail\'");
}
#[test]
fn shell_quote_unicode_emoji_and_cjk() {
assert_eq!(shell_quote("test ✅"), "'test ✅'");
assert_eq!(shell_quote("日本語"), "'日本語'");
assert_eq!(shell_quote("héllo"), "'héllo'");
}
#[test]
fn shell_quote_null_byte() {
let s = "a\0b";
let q = shell_quote(s);
assert_eq!(q, "'a\0b'");
}
#[test]
fn shell_quote_mixed_quote_types() {
assert_eq!(shell_quote(r#"he said "don't""#), r#"'he said "don'\''t"'"#);
}
#[test]
fn shell_quote_already_single_quoted() {
assert_eq!(shell_quote("'pre-quoted'"), r"''\''pre-quoted'\'''");
}
#[test]
fn shell_quote_only_single_quote() {
let q = shell_quote("'");
assert_eq!(q, r"''\'''");
}
#[test]
fn shell_quote_carriage_return() {
assert_eq!(shell_quote("a\rb"), "'a\rb'");
assert_eq!(shell_quote("a\r\nb"), "'a\r\nb'");
}
#[test]
fn shell_quote_consecutive_single_quotes() {
assert_eq!(shell_quote("a''b"), r"'a'\'''\''b'");
assert_eq!(shell_quote("'''"), r"''\'''\'''\'''");
}
#[test]
fn shell_quote_tab_with_single_quote() {
assert_eq!(shell_quote("a\t'b"), "'a\t'\\''b'");
}
#[test]
fn shell_quote_low_control_bytes() {
assert_eq!(shell_quote("\x07"), "'\x07'"); assert_eq!(shell_quote("\x08"), "'\x08'"); assert_eq!(shell_quote("\x0b"), "'\x0b'"); assert_eq!(shell_quote("\x0c"), "'\x0c'"); assert_eq!(shell_quote("\x1b[31mred\x1b[0m"), "'\x1b[31mred\x1b[0m'");
}
#[test]
fn shell_quote_safe_set_unquoted() {
for raw in [
"+",
"=",
":",
"/",
".",
"_",
"-",
"abc+def",
"key=value",
"ns:resource",
"/usr/local/bin",
"v1.0.0",
"file_name-1.txt",
] {
assert_eq!(
shell_quote(raw),
raw,
"safe-set input must remain unquoted: {raw:?}"
);
}
}
#[test]
fn shell_quote_long_strings() {
let safe = "a".repeat(1024);
assert_eq!(
shell_quote(&safe),
safe,
"long safe-set string passes through"
);
let with_space = format!("{}{}", "a".repeat(512), " end");
let q = shell_quote(&with_space);
assert!(q.starts_with('\'') && q.ends_with('\''));
assert_eq!(&q[1..q.len() - 1], &with_space);
}
#[test]
fn shell_quote_shell_metacharacters() {
for raw in [
"a&b", "a|b", "a`b`c", "a$b", "a*b", "a?b", "a[b]c", "a{b}c", "a(b)c", "a~b", "a#b", "a!b",
] {
let q = shell_quote(raw);
assert!(
q.starts_with('\'') && q.ends_with('\''),
"metachar input must be wrapped: input={raw:?} output={q:?}"
);
let inner = &q[1..q.len() - 1];
assert_eq!(
inner, raw,
"metachar input must be byte-preserved inside the wrap"
);
}
}
#[test]
fn search_path_for_finds_existing_executable() {
let found = search_path_for("sh");
assert!(found.is_some(), "PATH search for `sh` returned None");
let path = found.unwrap();
assert!(path.is_file(), "resolved path is not a file: {path:?}");
let mode = path.metadata().unwrap().permissions().mode();
assert!(
mode & 0o111 != 0,
"resolved path is not executable: {path:?} mode={mode:o}"
);
}
#[test]
fn search_path_for_returns_none_on_missing() {
let found = search_path_for("definitely-not-a-real-binary-xyzzy-987");
assert!(found.is_none());
}
#[test]
fn search_path_for_skips_non_executable_files() {
let tmp = tempfile::TempDir::new().expect("create temp dir");
let dummy = tmp.path().join("dummy_non_exec");
std::fs::write(&dummy, b"#!/bin/sh\necho hi\n").expect("write dummy");
let mut perms = std::fs::metadata(&dummy).unwrap().permissions();
perms.set_mode(0o644);
std::fs::set_permissions(&dummy, perms).expect("set non-exec perms");
let original_path = std::env::var_os("PATH").unwrap_or_default();
let new_path = {
let mut paths = vec![tmp.path().to_path_buf()];
paths.extend(std::env::split_paths(&original_path));
std::env::join_paths(paths).expect("join paths")
};
unsafe { std::env::set_var("PATH", &new_path) };
let found = search_path_for("dummy_non_exec");
unsafe { std::env::set_var("PATH", &original_path) };
assert!(
found.is_none(),
"non-executable file must NOT match PATH lookup, got: {found:?}",
);
}
fn read_archive_entries(blob: &[u8]) -> Vec<(String, u32, Vec<u8>)> {
use flate2::read::GzDecoder;
use std::io::Read as _;
let gz = GzDecoder::new(blob);
let mut archive = tar::Archive::new(gz);
let mut out = Vec::new();
for entry in archive.entries().expect("read tar entries") {
let mut e = entry.expect("entry");
let name = e.path().expect("entry path").to_string_lossy().into_owned();
let mode = e.header().mode().expect("entry mode");
let mut data = Vec::new();
e.read_to_end(&mut data).expect("read entry body");
out.push((name, mode, data));
}
out
}
#[test]
fn build_archive_no_scheduler_packs_only_ktstr() {
let tmp = tempfile::TempDir::new().expect("temp dir");
let ktstr_path = tmp.path().join("fake-ktstr");
std::fs::write(&ktstr_path, b"#!/bin/sh\necho ktstr-stub\n").expect("write fake ktstr");
let blob = build_archive(&ktstr_path, None, &[]).expect("build_archive");
let entries = read_archive_entries(&blob);
assert_eq!(entries.len(), 1, "expected 1 entry, got: {entries:?}");
let (name, mode, data) = &entries[0];
assert_eq!(name, "ktstr", "entry must be named 'ktstr'");
assert_eq!(*mode, 0o755, "entry must be mode 0o755 (executable)");
assert_eq!(
data.as_slice(),
b"#!/bin/sh\necho ktstr-stub\n",
"entry payload must roundtrip the input file bytes",
);
}
#[test]
fn build_archive_packs_ktstr_scheduler_and_includes() {
let tmp = tempfile::TempDir::new().expect("temp dir");
let ktstr_path = tmp.path().join("fake-ktstr");
let sched_path = tmp.path().join("fake-sched");
let inc_a = tmp.path().join("inc_a.txt");
let inc_b = tmp.path().join("inc_b.txt");
std::fs::write(&ktstr_path, b"K").expect("write ktstr");
std::fs::write(&sched_path, b"S").expect("write scheduler");
std::fs::write(&inc_a, b"A").expect("write inc_a");
std::fs::write(&inc_b, b"B").expect("write inc_b");
let includes = vec![inc_a.clone(), inc_b.clone()];
let blob = build_archive(&ktstr_path, Some(&sched_path), &includes).expect("build_archive");
let entries = read_archive_entries(&blob);
let names: Vec<&str> = entries.iter().map(|(n, _, _)| n.as_str()).collect();
assert_eq!(
names,
vec![
"ktstr",
"scheduler",
"include/inc_a.txt",
"include/inc_b.txt"
],
"entry names and order must match the documented layout",
);
for (name, mode, _) in &entries {
assert_eq!(*mode, 0o755, "entry {name} must be mode 0o755");
}
}
#[test]
fn build_archive_rejects_basename_collision() {
let tmp_a = tempfile::TempDir::new().expect("temp dir a");
let tmp_b = tempfile::TempDir::new().expect("temp dir b");
let inc_1 = tmp_a.path().join("dup.txt");
let inc_2 = tmp_b.path().join("dup.txt");
std::fs::write(&inc_1, b"first").expect("write inc_1");
std::fs::write(&inc_2, b"second").expect("write inc_2");
let ktstr_path = tmp_a.path().join("fake-ktstr");
std::fs::write(&ktstr_path, b"K").expect("write ktstr");
let err = build_archive(&ktstr_path, None, &[inc_1.clone(), inc_2.clone()])
.expect_err("colliding basenames must error");
let msg = format!("{err}");
assert!(
msg.contains("dup.txt"),
"error must name the colliding basename: '{msg}'",
);
assert!(
msg.contains("collision") || msg.contains("collide"),
"error must describe the failure as a collision: '{msg}'",
);
}
#[test]
fn generate_preamble_parses_under_bash_n() {
if which_bash().is_none() {
crate::report::test_skip("no bash on PATH");
return;
}
let entry = KtstrTestEntry {
name: "test_preamble_smoke",
extra_sched_args: &["--foo", "bar baz"],
..KtstrTestEntry::DEFAULT
};
for has_scheduler in [true, false] {
let preamble = generate_preamble(&entry, has_scheduler, &[]);
assert_bash_n_accepts(&preamble, has_scheduler);
}
}
#[test]
fn generate_preamble_interpolates_entry_fields() {
let entry = KtstrTestEntry {
name: "interp_smoke",
duration: std::time::Duration::from_secs(7),
watchdog_timeout: std::time::Duration::from_secs(13),
topology: crate::vmm::topology::Topology {
llcs: 3,
cores_per_llc: 5,
threads_per_core: 2,
numa_nodes: 4,
nodes: None,
distances: None,
},
..KtstrTestEntry::DEFAULT
};
let preamble = generate_preamble(&entry, true, &[]);
assert!(
preamble.contains("KTSTR_TEST_NAME=interp_smoke"),
"preamble must set KTSTR_TEST_NAME from entry.name",
);
assert!(
preamble.contains("NEED_LLCS=3"),
"preamble must set NEED_LLCS from entry.topology.llcs",
);
assert!(
preamble.contains("NEED_CORES_PER_LLC=5"),
"preamble must set NEED_CORES_PER_LLC",
);
assert!(
preamble.contains("NEED_THREADS_PER_CORE=2"),
"preamble must set NEED_THREADS_PER_CORE",
);
assert!(
preamble.contains("NEED_NUMA_NODES=4"),
"preamble must set NEED_NUMA_NODES",
);
assert!(
preamble.contains("TEST_DURATION_SECS=7"),
"preamble must set TEST_DURATION_SECS from entry.duration",
);
assert!(
preamble.contains("TEST_WATCHDOG_SECS=13"),
"preamble must set TEST_WATCHDOG_SECS from entry.watchdog_timeout",
);
}
#[test]
fn generate_preamble_does_not_auto_inject_cell_parent_cgroup_from_cgroup_parent() {
use crate::test_support::{CgroupPath, Scheduler, SchedulerSpec};
static SCHED_WITH_PARENT: Scheduler = Scheduler {
name: "sched_with_parent",
binary: SchedulerSpec::Discover("sched_with_parent_bin"),
sysctls: &[],
kargs: &[],
assert: crate::assert::Assert::NO_OVERRIDES,
cgroup_parent: Some(CgroupPath::new("/ktstr_export_test")),
sched_args: &[],
topology: crate::vmm::topology::Topology {
llcs: 1,
cores_per_llc: 2,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
},
constraints: TopologyConstraints::DEFAULT,
config_file: None,
config_file_def: None,
kernels: &[],
};
let entry = KtstrTestEntry {
name: "no_auto_inject_smoke",
scheduler: &SCHED_WITH_PARENT,
..KtstrTestEntry::DEFAULT
};
let preamble = generate_preamble(&entry, true, &[]);
assert!(
!preamble.contains("--cell-parent-cgroup"),
"preamble must NOT auto-inject --cell-parent-cgroup from \
entry.scheduler.cgroup_parent (the two concerns are \
decoupled); got: {preamble}"
);
}
#[test]
fn generate_preamble_passes_user_supplied_cell_parent_cgroup_through() {
use crate::test_support::{CgroupPath, Scheduler, SchedulerSpec};
static SCHED_WITH_PARENT: Scheduler = Scheduler {
name: "sched_with_parent_user_supplied",
binary: SchedulerSpec::Discover("sched_with_parent_user_supplied_bin"),
sysctls: &[],
kargs: &[],
assert: crate::assert::Assert::NO_OVERRIDES,
cgroup_parent: Some(CgroupPath::new("/auto_inject_path")),
sched_args: &[],
topology: crate::vmm::topology::Topology {
llcs: 1,
cores_per_llc: 2,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
},
constraints: TopologyConstraints::DEFAULT,
config_file: None,
config_file_def: None,
kernels: &[],
};
let entry = KtstrTestEntry {
name: "user_supplied_smoke",
scheduler: &SCHED_WITH_PARENT,
extra_sched_args: &["--cell-parent-cgroup", "/user_supplied_path"],
..KtstrTestEntry::DEFAULT
};
let preamble = generate_preamble(&entry, true, &[]);
assert!(
preamble.contains("/user_supplied_path"),
"preamble must carry the user-supplied path; got: {preamble}"
);
assert!(
!preamble.contains("/auto_inject_path"),
"preamble must NOT inject the scheduler's cgroup_parent \
into argv (decoupled); got: {preamble}"
);
}
#[test]
fn compute_config_export_additions_returns_empty_when_no_config() {
let entry = KtstrTestEntry {
name: "no_config_smoke",
..KtstrTestEntry::DEFAULT
};
let additions =
compute_config_export_additions(&entry).expect("compute additions must not error");
assert!(
additions.is_empty(),
"no config slots set must yield zero additions; got {} addition(s)",
additions.len()
);
}
#[test]
fn config_file_addition_emits_hardcoded_config_arg_with_dir_expansion() {
use crate::test_support::{Scheduler, SchedulerSpec};
let tmp = tempfile::TempDir::new().expect("temp dir");
let host_cfg = tmp.path().join("scheduler-config.json");
std::fs::write(&host_cfg, b"{\"layer_count\": 3}\n").expect("write fixture cfg");
let host_cfg_str: &'static str =
Box::leak(host_cfg.to_string_lossy().into_owned().into_boxed_str());
let sched: &'static Scheduler = Box::leak(Box::new(Scheduler {
name: "config_file_export_test",
binary: SchedulerSpec::Discover("config_file_export_test_bin"),
sysctls: &[],
kargs: &[],
assert: crate::assert::Assert::NO_OVERRIDES,
cgroup_parent: None,
sched_args: &[],
topology: crate::vmm::topology::Topology {
llcs: 1,
cores_per_llc: 1,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
},
constraints: TopologyConstraints::DEFAULT,
config_file: Some(host_cfg_str),
config_file_def: None,
kernels: &[],
}));
let entry = KtstrTestEntry {
name: "config_file_export_smoke",
scheduler: sched,
..KtstrTestEntry::DEFAULT
};
let addition = config_file_addition(&entry)
.expect("config_file_addition must not error")
.expect("config_file set must yield Some");
assert_eq!(
addition.host_path, host_cfg,
"host_path must be the configured scheduler.config_file path verbatim"
);
assert_eq!(
addition.args_shell_prefix, "--config \"$DIR/include/scheduler-config.json\"",
"args_shell_prefix must use the hardcoded --config flag with \
`$DIR/include/<basename>` path expansion and NO leading space \
(the caller manages spacing); got: {:?}",
addition.args_shell_prefix
);
}
#[test]
fn config_content_addition_writes_temp_file_and_substitutes_template() {
use crate::test_support::{Scheduler, SchedulerSpec};
static SCHED_CONFIG_CONTENT: Scheduler = Scheduler {
name: "config_content_export_test",
binary: SchedulerSpec::Discover("config_content_export_test_bin"),
sysctls: &[],
kargs: &[],
assert: crate::assert::Assert::NO_OVERRIDES,
cgroup_parent: None,
sched_args: &[],
topology: crate::vmm::topology::Topology {
llcs: 1,
cores_per_llc: 1,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
},
constraints: TopologyConstraints::DEFAULT,
config_file: None,
config_file_def: Some(("--layered-config {file}", "/include-files/layers.json")),
kernels: &[],
};
const CONTENT: &str = "{\"layers\": [\"foo\", \"bar\"]}\n";
let entry = KtstrTestEntry {
name: "config_content_export_smoke",
scheduler: &SCHED_CONFIG_CONTENT,
config_content: Some(CONTENT),
..KtstrTestEntry::DEFAULT
};
let addition = config_content_addition(&entry)
.expect("config_content_addition must not error")
.expect("config_content + config_file_def set must yield Some");
let written =
std::fs::read_to_string(&addition.host_path).expect("temp file must exist on disk");
assert_eq!(
written,
CONTENT,
"temp file at {} must contain the inline config_content bytes verbatim",
addition.host_path.display()
);
assert_eq!(
addition.args_shell_prefix, "--layered-config \"$DIR/include/layers.json\"",
"args_shell_prefix must substitute `{{file}}` with `\"$DIR/include/<basename>\"` \
where basename is derived from the scheduler's config_file_def guest_path, \
with NO leading space (the caller manages spacing); got: {:?}",
addition.args_shell_prefix
);
}
#[test]
fn config_content_addition_writes_inside_process_scratch_dir() {
use crate::test_support::{Scheduler, SchedulerSpec};
static SCHED_SCRATCH_DIR_PIN: Scheduler = Scheduler {
name: "scratch_dir_pin",
binary: SchedulerSpec::Discover("scratch_dir_pin_bin"),
sysctls: &[],
kargs: &[],
assert: crate::assert::Assert::NO_OVERRIDES,
cgroup_parent: None,
sched_args: &[],
topology: crate::vmm::topology::Topology {
llcs: 1,
cores_per_llc: 1,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
},
constraints: TopologyConstraints::DEFAULT,
config_file: None,
config_file_def: Some(("--config={file}", "/include-files/dirpin.json")),
kernels: &[],
};
let entry = KtstrTestEntry {
name: "scratch_dir_pin_smoke",
scheduler: &SCHED_SCRATCH_DIR_PIN,
config_content: Some("{\"pin\": true}\n"),
..KtstrTestEntry::DEFAULT
};
let addition = config_content_addition(&entry)
.expect("config_content_addition must not error")
.expect("config_content + config_file_def set must yield Some");
let dir = scratch_dir();
assert!(
addition.host_path.starts_with(dir),
"host_path {} must live inside the process scratch_dir {} — a regression \
to bare std::env::temp_dir() would silently restore the symlink-attack \
surface this test guards against",
addition.host_path.display(),
dir.display(),
);
}
#[test]
fn config_content_addition_same_content_same_canonical_path() {
use crate::test_support::{Scheduler, SchedulerSpec};
static SCHED_IDEMPOTENT: Scheduler = Scheduler {
name: "idempotent_pin",
binary: SchedulerSpec::Discover("idempotent_pin_bin"),
sysctls: &[],
kargs: &[],
assert: crate::assert::Assert::NO_OVERRIDES,
cgroup_parent: None,
sched_args: &[],
topology: crate::vmm::topology::Topology {
llcs: 1,
cores_per_llc: 1,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
},
constraints: TopologyConstraints::DEFAULT,
config_file: None,
config_file_def: Some(("--config={file}", "/include-files/idem.json")),
kernels: &[],
};
const CONTENT: &str = "{\"idem\": 42}\n";
let entry = KtstrTestEntry {
name: "idempotent_pin_smoke",
scheduler: &SCHED_IDEMPOTENT,
config_content: Some(CONTENT),
..KtstrTestEntry::DEFAULT
};
let a = config_content_addition(&entry).unwrap().unwrap();
let b = config_content_addition(&entry).unwrap().unwrap();
assert_eq!(
a.host_path,
b.host_path,
"same content must produce same canonical host_path; got {} vs {}",
a.host_path.display(),
b.host_path.display(),
);
let basename = a
.host_path
.file_name()
.and_then(|s| s.to_str())
.expect("host_path must have a UTF-8 basename");
assert!(
basename.starts_with("ktstr-export-config-") && basename.ends_with("idem.json"),
"basename must match the `ktstr-export-config-{{hash:016x}}-<basename>` \
template (with the scheduler's config_file_def basename suffix); got {basename}",
);
}
#[test]
fn config_content_addition_rejects_basename_with_shell_metacharacters() {
use crate::test_support::{Scheduler, SchedulerSpec};
static SCHED_METACHAR: Scheduler = Scheduler {
name: "metachar_pin",
binary: SchedulerSpec::Discover("metachar_pin_bin"),
sysctls: &[],
kargs: &[],
assert: crate::assert::Assert::NO_OVERRIDES,
cgroup_parent: None,
sched_args: &[],
topology: crate::vmm::topology::Topology {
llcs: 1,
cores_per_llc: 1,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
},
constraints: TopologyConstraints::DEFAULT,
config_file: None,
config_file_def: Some(("--config={file}", "/include-files/$evil.json")),
kernels: &[],
};
let entry = KtstrTestEntry {
name: "metachar_pin_smoke",
scheduler: &SCHED_METACHAR,
config_content: Some("ignored\n"),
..KtstrTestEntry::DEFAULT
};
let err = config_content_addition(&entry)
.expect_err("basename with shell metacharacter must be rejected");
let msg = format!("{err}");
assert!(
msg.contains("shell-metacharacter") && msg.contains('$'),
"rejection diagnostic must name the failure mode and the offending \
character; got {msg}",
);
}
#[test]
fn generate_preamble_prepends_config_addition_prefix() {
use crate::test_support::{Scheduler, SchedulerSpec};
static SCHED: Scheduler = Scheduler {
name: "preamble_config_smoke",
binary: SchedulerSpec::Discover("preamble_config_smoke_bin"),
sysctls: &[],
kargs: &[],
assert: crate::assert::Assert::NO_OVERRIDES,
cgroup_parent: None,
sched_args: &[],
topology: crate::vmm::topology::Topology {
llcs: 1,
cores_per_llc: 1,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
},
constraints: TopologyConstraints::DEFAULT,
config_file: None,
config_file_def: None,
kernels: &[],
};
let entry = KtstrTestEntry {
name: "preamble_config_smoke",
scheduler: &SCHED,
..KtstrTestEntry::DEFAULT
};
let additions = vec![ConfigExportAddition {
host_path: PathBuf::from("/tmp/ktstr-export-config-test.json"),
args_shell_prefix: "--config \"$DIR/include/test.json\"".to_string(),
}];
let preamble = generate_preamble(&entry, true, &additions);
assert!(
preamble.contains("--config \"$DIR/include/test.json\""),
"preamble must contain the verbatim args_shell_prefix from the \
config addition; got: {preamble}"
);
}
#[test]
fn generate_preamble_emits_config_addition_before_base_sched_args() {
use crate::test_support::{Scheduler, SchedulerSpec};
static SCHED: Scheduler = Scheduler {
name: "order_pin_test",
binary: SchedulerSpec::Discover("order_pin_test_bin"),
sysctls: &[],
kargs: &[],
assert: crate::assert::Assert::NO_OVERRIDES,
cgroup_parent: None,
sched_args: &["--cell-parent-cgroup", "/order_pin_parent"],
topology: crate::vmm::topology::Topology {
llcs: 1,
cores_per_llc: 1,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
},
constraints: TopologyConstraints::DEFAULT,
config_file: None,
config_file_def: None,
kernels: &[],
};
let entry = KtstrTestEntry {
name: "order_pin_test",
scheduler: &SCHED,
..KtstrTestEntry::DEFAULT
};
let additions = vec![ConfigExportAddition {
host_path: PathBuf::from("/tmp/order-pin-test.json"),
args_shell_prefix: "--config \"$DIR/include/order-pin-test.json\"".to_string(),
}];
let preamble = generate_preamble(&entry, true, &additions);
let config_pos = preamble
.find("--config \"$DIR/include/order-pin-test.json\"")
.expect("preamble must contain the --config arg from the addition");
let cgroup_pos = preamble
.find("--cell-parent-cgroup")
.expect("preamble must contain --cell-parent-cgroup from the explicit sched_args entry");
assert!(
config_pos < cgroup_pos,
"argv ordering parity with eval.rs:1112-1125: `--config` must \
appear BEFORE `--cell-parent-cgroup`. \
config_pos={config_pos}, cgroup_pos={cgroup_pos}, preamble:\n{preamble}"
);
}
#[test]
fn compute_config_export_additions_dual_fire_when_file_and_content_set() {
use crate::test_support::{Scheduler, SchedulerSpec};
let tmp = tempfile::TempDir::new().expect("temp dir");
let host_cfg = tmp.path().join("static-cfg.json");
std::fs::write(&host_cfg, b"{\"static\": true}\n").expect("write fixture cfg");
let host_cfg_str: &'static str =
Box::leak(host_cfg.to_string_lossy().into_owned().into_boxed_str());
let sched: &'static Scheduler = Box::leak(Box::new(Scheduler {
name: "dual_fire_test",
binary: SchedulerSpec::Discover("dual_fire_test_bin"),
sysctls: &[],
kargs: &[],
assert: crate::assert::Assert::NO_OVERRIDES,
cgroup_parent: None,
sched_args: &[],
topology: crate::vmm::topology::Topology {
llcs: 1,
cores_per_llc: 1,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
},
constraints: TopologyConstraints::DEFAULT,
config_file: Some(host_cfg_str),
config_file_def: Some(("--layered-config {file}", "/include-files/layers.json")),
kernels: &[],
}));
let entry = KtstrTestEntry {
name: "dual_fire_smoke",
scheduler: sched,
config_content: Some("{\"layers\": []}\n"),
..KtstrTestEntry::DEFAULT
};
let additions =
compute_config_export_additions(&entry).expect("compute additions must not error");
assert_eq!(
additions.len(),
2,
"both config_file and config_content slots set must yield 2 additions \
(orthogonal — each contributes its own scheduler arg), got {} addition(s)",
additions.len()
);
assert_eq!(
additions[0].args_shell_prefix, "--config \"$DIR/include/static-cfg.json\"",
"first addition must be the config_file source"
);
assert_eq!(
additions[1].args_shell_prefix, "--layered-config \"$DIR/include/layers.json\"",
"second addition must be the config_content source"
);
}
#[test]
fn config_file_addition_rejects_directory_with_actionable_error() {
use crate::test_support::{Scheduler, SchedulerSpec};
let tmp = tempfile::TempDir::new().expect("temp dir");
let dir_path = tmp.path().join("config-as-dir");
std::fs::create_dir(&dir_path).expect("create directory fixture");
let dir_path_str: &'static str =
Box::leak(dir_path.to_string_lossy().into_owned().into_boxed_str());
let sched: &'static Scheduler = Box::leak(Box::new(Scheduler {
name: "directory_reject_test",
binary: SchedulerSpec::Discover("directory_reject_test_bin"),
sysctls: &[],
kargs: &[],
assert: crate::assert::Assert::NO_OVERRIDES,
cgroup_parent: None,
sched_args: &[],
topology: crate::vmm::topology::Topology {
llcs: 1,
cores_per_llc: 1,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
},
constraints: TopologyConstraints::DEFAULT,
config_file: Some(dir_path_str),
config_file_def: None,
kernels: &[],
}));
let entry = KtstrTestEntry {
name: "directory_reject_smoke",
scheduler: sched,
..KtstrTestEntry::DEFAULT
};
let err =
config_file_addition(&entry).expect_err("config_file pointing at a directory must error");
let msg = format!("{err}");
assert!(
msg.contains("is a directory"),
"error must name the directory failure mode; got: {msg}"
);
assert!(
msg.contains("config_file must point at a regular file"),
"error must name the v1 constraint actionably; got: {msg}"
);
}
#[test]
fn config_file_addition_rejects_basename_with_shell_metacharacters() {
use crate::test_support::{Scheduler, SchedulerSpec};
let tmp = tempfile::TempDir::new().expect("temp dir");
let evil_path = tmp.path().join("$evil.json");
if std::fs::write(&evil_path, b"{}\n").is_err() {
crate::report::test_skip("filesystem rejected fixture with $ in basename");
return;
}
let evil_path_str: &'static str =
Box::leak(evil_path.to_string_lossy().into_owned().into_boxed_str());
let sched: &'static Scheduler = Box::leak(Box::new(Scheduler {
name: "metachar_reject_test",
binary: SchedulerSpec::Discover("metachar_reject_test_bin"),
sysctls: &[],
kargs: &[],
assert: crate::assert::Assert::NO_OVERRIDES,
cgroup_parent: None,
sched_args: &[],
topology: crate::vmm::topology::Topology {
llcs: 1,
cores_per_llc: 1,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
},
constraints: TopologyConstraints::DEFAULT,
config_file: Some(evil_path_str),
config_file_def: None,
kernels: &[],
}));
let entry = KtstrTestEntry {
name: "metachar_reject_smoke",
scheduler: sched,
..KtstrTestEntry::DEFAULT
};
let err =
config_file_addition(&entry).expect_err("basename with shell metacharacter must error");
let msg = format!("{err}");
assert!(
msg.contains("shell-metacharacter"),
"error must name the shell-metacharacter constraint; got: {msg}"
);
assert!(
msg.contains('$') || msg.contains("\"$\""),
"error must name the offending character; got: {msg}"
);
}
#[test]
fn archive_basename_matches_args_shell_prefix_for_config_file() {
use crate::test_support::{Scheduler, SchedulerSpec};
let tmp = tempfile::TempDir::new().expect("temp dir");
let ktstr_path = tmp.path().join("fake-ktstr");
std::fs::write(&ktstr_path, b"FAKE_KTSTR").expect("write ktstr stub");
let host_cfg = tmp.path().join("parity-config.json");
std::fs::write(&host_cfg, b"{\"parity\": true}\n").expect("write fixture cfg");
let host_cfg_str: &'static str =
Box::leak(host_cfg.to_string_lossy().into_owned().into_boxed_str());
let sched: &'static Scheduler = Box::leak(Box::new(Scheduler {
name: "basename_parity_test",
binary: SchedulerSpec::Discover("basename_parity_test_bin"),
sysctls: &[],
kargs: &[],
assert: crate::assert::Assert::NO_OVERRIDES,
cgroup_parent: None,
sched_args: &[],
topology: crate::vmm::topology::Topology {
llcs: 1,
cores_per_llc: 1,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
},
constraints: TopologyConstraints::DEFAULT,
config_file: Some(host_cfg_str),
config_file_def: None,
kernels: &[],
}));
let entry = KtstrTestEntry {
name: "basename_parity_smoke",
scheduler: sched,
..KtstrTestEntry::DEFAULT
};
let additions =
compute_config_export_additions(&entry).expect("compute additions must not error");
assert_eq!(additions.len(), 1, "expected exactly 1 addition");
let archive =
build_archive(&ktstr_path, None, &[additions[0].host_path.clone()]).expect("build archive");
let entries = read_archive_entries(&archive);
let archive_names: Vec<&str> = entries.iter().map(|(n, _, _)| n.as_str()).collect();
assert!(
archive_names.contains(&"include/parity-config.json"),
"archive must contain include/parity-config.json (basename derived from \
host_path.file_name()); got: {archive_names:?}"
);
assert!(
additions[0]
.args_shell_prefix
.contains("\"$DIR/include/parity-config.json\""),
"args_shell_prefix must reference the same basename build_archive flattens to; \
got: {:?}",
additions[0].args_shell_prefix
);
}
#[test]
fn runfile_layout_and_archive_roundtrip() {
let tmp = tempfile::TempDir::new().expect("temp dir");
let out = tmp.path().join("smoke.run");
let preamble = "#!/bin/bash\necho hello\n";
let archive: Vec<u8> = (0u8..=255).chain(0u8..=128).collect();
write_runfile(&out, preamble, &archive).expect("write_runfile");
let mode = std::fs::metadata(&out).unwrap().permissions().mode() & 0o777;
assert_eq!(mode, 0o755, "runfile mode must be 0o755");
let raw = std::fs::read_to_string(&out).expect("read runfile");
let marker = "\n__ARCHIVE__\n";
let split_at = raw
.find(marker)
.expect("runfile must contain a __ARCHIVE__ marker line");
assert_eq!(
&raw[..split_at + 1],
preamble,
"preamble must be written verbatim before the marker",
);
let after = &raw[split_at + marker.len()..];
for line in after.lines() {
assert!(
line.len() <= 76,
"base64 line must be <= 76 cols (POSIX MIME width), got {}: {line:?}",
line.len(),
);
}
let joined: String = after.lines().collect();
let decoded = BASE64
.decode(joined.as_bytes())
.expect("base64 decode of runfile tail must succeed");
assert_eq!(
decoded, archive,
"base64 roundtrip must reproduce the input archive bytes",
);
}
#[test]
fn export_pipeline_round_trip_for_eevdf_entry() {
if which_bash().is_none() {
crate::report::test_skip("no bash on PATH");
return;
}
let tmp = tempfile::TempDir::new().expect("temp dir");
let ktstr_path = tmp.path().join("fake-ktstr");
std::fs::write(&ktstr_path, b"FAKE_KTSTR").expect("write ktstr stub");
let inc = tmp.path().join("topology.yaml");
std::fs::write(&inc, b"some: yaml").expect("write include");
let out = tmp.path().join("e2e.run");
let entry = KtstrTestEntry {
name: "export_smoke",
..KtstrTestEntry::DEFAULT
};
let archive =
build_archive(&ktstr_path, None, std::slice::from_ref(&inc)).expect("build archive");
let preamble = generate_preamble(&entry, false, &[]);
write_runfile(&out, &preamble, &archive).expect("write runfile");
let raw = std::fs::read_to_string(&out).expect("read runfile");
let split_at = raw.find("\n__ARCHIVE__\n").expect("marker present");
assert_bash_n_accepts(&raw[..split_at + 1], false);
let entries = read_archive_entries(&archive);
let names: Vec<&str> = entries.iter().map(|(n, _, _)| n.as_str()).collect();
assert_eq!(
names,
vec!["ktstr", "include/topology.yaml"],
"archive must contain ktstr and the single include entry",
);
assert!(
raw[..split_at].contains("KTSTR_TEST_NAME=export_smoke"),
"preamble must name the entry",
);
assert!(
raw[..split_at].contains("TEST_DURATION_SECS=12"),
"preamble must reflect the entry's duration",
);
}
fn assert_bash_n_accepts(script: &str, has_scheduler: bool) {
use std::io::Write as _;
use std::process::{Command, Stdio};
let bash = which_bash().expect("bash should have been checked by caller");
let mut child = Command::new(&bash)
.arg("-n")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn bash -n");
child
.stdin
.as_mut()
.expect("bash stdin")
.write_all(script.as_bytes())
.expect("pipe script to bash");
let output = child.wait_with_output().expect("bash -n wait");
assert!(
output.status.success(),
"bash -n rejected the preamble (has_scheduler={has_scheduler}); \
stderr:\n{}\nscript:\n{script}",
String::from_utf8_lossy(&output.stderr),
);
}
fn which_bash() -> Option<PathBuf> {
search_path_for("bash")
}