#![cfg(target_os = "linux")]
use gander_core::PlatformSampler;
const HELPER_ENV: &str = "GANDER_THREAD_HELPER";
fn spawn_threaded() -> Option<std::process::Child> {
let exe = std::env::current_exe().ok()?;
let child = std::process::Command::new(exe)
.args(["--exact", "helper_spawns_threads", "--nocapture", "--ignored"])
.env(HELPER_ENV, "1")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
.ok()?;
Some(child)
}
#[test]
#[ignore]
fn helper_spawns_threads() {
if std::env::var_os(HELPER_ENV).is_none() {
return;
}
let mut handles = Vec::new();
for _ in 0..4 {
handles.push(std::thread::spawn(|| {
std::thread::sleep(std::time::Duration::from_secs(30));
}));
}
for h in handles {
let _ = h.join();
}
}
#[test]
fn threads_are_off_by_default() {
let mut s = gander_core::Sampler::default();
let (_snap, rows) = s.sample();
assert!(
rows.iter().all(|r| !r.is_thread),
"the default sample must contain processes only"
);
assert!(<gander_core::Sampler as PlatformSampler>::supports_threads());
}
#[test]
fn turning_threads_on_adds_thread_rows_with_a_leader() {
let mut child = spawn_threaded();
std::thread::sleep(std::time::Duration::from_millis(600));
let leader = child.as_ref().map(|c| c.id());
let mut s = gander_core::Sampler::default();
PlatformSampler::set_scan_options(&mut s, gander_core::ScanOptions::none().with_threads(true));
let (_snap, rows) = s.sample();
if let Some(mut c) = child.take() {
let _ = c.kill();
let _ = c.wait();
}
let threads: Vec<_> = rows.iter().filter(|r| r.is_thread).collect();
assert!(
!threads.is_empty(),
"a machine with a threaded helper must report threads"
);
for t in &threads {
assert_ne!(t.pid, t.tgid, "the leader must not be duplicated as a thread");
assert!(
rows.iter().any(|r| r.pid == t.tgid && !r.is_thread),
"thread {} has no leader {} in the same sample",
t.pid,
t.tgid
);
}
if let Some(leader) = leader {
if rows.iter().any(|r| r.pid == leader) {
let own = rows.iter().filter(|r| r.tgid == leader && r.is_thread).count();
assert!(own > 1, "the threaded helper should have >1 thread, saw {own}");
}
}
}
#[test]
fn thread_rows_carry_their_own_cpu_and_inherit_their_process_identity() {
let mut s = gander_core::Sampler::default();
PlatformSampler::set_scan_options(&mut s, gander_core::ScanOptions::none().with_threads(true));
let (_snap, rows) = s.sample();
assert!(!rows.is_empty());
let thread = match rows.iter().find(|r| r.is_thread) {
Some(t) => t,
None => return, };
let leader = rows
.iter()
.find(|r| r.pid == thread.tgid && !r.is_thread)
.expect("leader row");
assert_eq!(thread.uid, leader.uid, "uid comes from the process");
assert_eq!(thread.nlwp, 1, "a thread owns one task");
assert!(thread.percent_cpu.is_finite() && thread.percent_cpu >= 0.0);
assert!(
thread.flags.contains(gander_core::row::RowFlags::USERLAND_THREAD),
"threads must be flagged so `H` can filter them"
);
assert_eq!(thread.comm.as_str(), thread.comm.as_str());
}
#[test]
fn hiding_threads_restores_the_process_only_view() {
let mut s = gander_core::Sampler::default();
PlatformSampler::set_scan_options(&mut s, gander_core::ScanOptions::none().with_threads(true));
let (_snap, with_threads) = s.sample();
PlatformSampler::set_scan_options(&mut s, gander_core::ScanOptions::none().with_threads(false));
let (_snap, without) = s.sample();
assert!(!without.iter().any(|r| r.is_thread));
assert!(
with_threads.len() >= without.len(),
"adding threads cannot shrink the table ({} vs {})",
with_threads.len(),
without.len()
);
}