use denet::ProcessMonitor;
use std::process::{Command, Stdio};
use std::time::Duration;
#[test]
#[cfg(target_os = "linux")]
fn threads_are_not_counted_as_child_processes() {
let script = r#"
import threading, time
stop = time.time() + 8
def spin():
while time.time() < stop:
pass
ts = [threading.Thread(target=spin, daemon=True) for _ in range(8)]
for t in ts: t.start()
for t in ts: t.join()
"#;
let mut child = Command::new("python3")
.arg("-c")
.arg(script)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.expect("failed to spawn python3");
let pid = child.id() as usize;
let mut monitor =
ProcessMonitor::from_pid(pid, Duration::from_millis(100), Duration::from_millis(500))
.expect("failed to create ProcessMonitor");
std::thread::sleep(Duration::from_millis(1500));
let _ = monitor.sample_tree_metrics();
std::thread::sleep(Duration::from_millis(500));
let mut max_children = 0usize;
let mut max_process_count = 0usize;
let mut worst_ratio: f32 = 0.0;
let mut saw_nonzero_parent = false;
for _ in 0..5 {
let tree = monitor.sample_tree_metrics();
let parent = tree.parent.expect("parent metrics should exist");
let agg = tree.aggregated.expect("aggregated metrics should exist");
max_children = max_children.max(tree.children.len());
max_process_count = max_process_count.max(agg.process_count);
if parent.cpu_usage > 1.0 {
saw_nonzero_parent = true;
let ratio = agg.cpu_usage / parent.cpu_usage;
worst_ratio = worst_ratio.max(ratio);
}
println!(
"parent_cpu={:.1}% agg_cpu={:.1}% children={} process_count={} thread_count={}",
parent.cpu_usage,
agg.cpu_usage,
tree.children.len(),
agg.process_count,
agg.thread_count,
);
std::thread::sleep(Duration::from_millis(300));
}
let _ = child.kill();
let _ = child.wait();
assert_eq!(
max_children, 0,
"threads of the monitored process must not be reported as children"
);
assert_eq!(
max_process_count, 1,
"aggregate process_count must stay 1 for a single multi-threaded process"
);
if saw_nonzero_parent {
assert!(
worst_ratio < 1.5,
"aggregate CPU should not exceed parent CPU (ratio={worst_ratio:.2}); \
this indicates threads are being double-counted as children",
);
}
}
#[test]
#[cfg(target_os = "linux")]
fn real_subprocesses_are_still_detected() {
const N_CHILDREN: usize = 3;
let script = format!(
r#"
import subprocess, sys, time
child_code = "t=__import__('time').time()+8\nwhile __import__('time').time()<t: pass"
kids = [subprocess.Popen([sys.executable, "-c", child_code]) for _ in range({N_CHILDREN})]
time.sleep(7)
for k in kids:
k.terminate()
k.wait()
"#
);
let mut parent = Command::new("python3")
.arg("-c")
.arg(&script)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.expect("failed to spawn python3 parent");
let parent_pid = parent.id() as usize;
let mut monitor = ProcessMonitor::from_pid(
parent_pid,
Duration::from_millis(100),
Duration::from_millis(500),
)
.expect("failed to create ProcessMonitor");
std::thread::sleep(Duration::from_millis(2000));
let _ = monitor.sample_tree_metrics();
std::thread::sleep(Duration::from_millis(500));
let mut max_children_seen = 0usize;
let mut max_agg_cpu: f32 = 0.0;
let mut max_process_count = 0usize;
for _ in 0..5 {
let tree = monitor.sample_tree_metrics();
let agg = tree.aggregated.expect("aggregated metrics should exist");
max_children_seen = max_children_seen.max(tree.children.len());
max_process_count = max_process_count.max(agg.process_count);
max_agg_cpu = max_agg_cpu.max(agg.cpu_usage);
println!(
"children={} process_count={} agg_cpu={:.1}% children_pids={:?}",
tree.children.len(),
agg.process_count,
agg.cpu_usage,
tree.children.iter().map(|c| c.pid).collect::<Vec<_>>(),
);
std::thread::sleep(Duration::from_millis(400));
}
let _ = parent.kill();
let _ = parent.wait();
assert_eq!(
max_children_seen, N_CHILDREN,
"parent should have exactly {N_CHILDREN} real subprocesses detected",
);
assert_eq!(
max_process_count,
N_CHILDREN + 1,
"aggregate process_count should be parent + {N_CHILDREN} children",
);
assert!(
max_agg_cpu > 150.0,
"aggregate CPU should reflect {N_CHILDREN} busy children (got {max_agg_cpu:.1}%)",
);
}