use std::borrow::Cow;
use std::collections::HashMap;
use std::collections::HashSet;
use std::env;
use std::path::Component;
use std::path::Path;
use std::path::PathBuf;
#[cfg(test)]
use std::sync::OnceLock;
use std::time::Duration;
use std::time::Instant;
use sysinfo::Pid;
use sysinfo::ProcessRefreshKind;
use sysinfo::ProcessesToUpdate;
use sysinfo::Signal;
use sysinfo::System;
use sysinfo::UpdateKind;
use tui_pane::RollingMean;
use super::constants::ANCESTOR_WALK_CAP;
use super::constants::CARGO_BIN_DIR;
use super::constants::MIN_HEX_HASH_LEN;
use crate::constants::CARGO_COMMAND_NAME;
use crate::project::AbsolutePath;
use crate::tui::panes::RunTargetKind;
use crate::tui::startup_services::StartupEffect;
use crate::tui::startup_services::StartupServices;
pub struct RunningTargetsPoller {
system: System,
last_poll: Option<Instant>,
poll_interval: Duration,
snapshot: RunningTargets,
startup_services: StartupServices,
install_bin_dir: Option<AbsolutePath>,
first_seen: HashMap<u32, Instant>,
cpu_history: HashMap<u32, RollingMean>,
}
#[derive(Default)]
pub struct RunningTargets {
by_key: HashMap<RunningKey, RunningTarget>,
children: Vec<ChildProcess>,
}
struct RunningTarget {
member_dir: AbsolutePath,
instances: Vec<RunningInstance>,
}
#[derive(Clone, Copy)]
pub struct RunningInstance {
pub pid: u32,
pub cpu_percent: f32,
pub memory_bytes: u64,
pub profile: RunProfile,
pub first_seen: Instant,
pub create_time: u64,
pub parent_pid: Option<u32>,
}
pub struct ChildProcess {
pub pid: u32,
pub name: String,
pub cpu_percent: f32,
pub memory_bytes: u64,
pub first_seen: Instant,
pub create_time: u64,
pub parent_pid: u32,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RunProfile {
Debug,
Release,
Installed,
}
impl RunProfile {
pub const fn label(self) -> &'static str {
match self {
Self::Debug => "debug",
Self::Release => "release",
Self::Installed => CARGO_COMMAND_NAME,
}
}
pub const fn is_installed(self) -> bool { matches!(self, Self::Installed) }
}
#[derive(Hash, Eq, PartialEq, Clone, Debug)]
pub struct RunningKey {
pub target_dir: AbsolutePath,
pub run_target_kind: RunTargetKind,
pub name: String,
}
pub struct ProjectTargetSlice<'a> {
pub target_dir: &'a AbsolutePath,
pub workspace_root: &'a AbsolutePath,
pub bench_names: &'a HashSet<String>,
pub bin_names: &'a HashSet<String>,
pub member_dirs: &'a HashMap<(RunTargetKind, String), AbsolutePath>,
}
impl ProjectTargetSlice<'_> {
fn member_dir(&self, run_target_kind: RunTargetKind, name: &str) -> AbsolutePath {
self.member_dirs
.get(&(run_target_kind, name.to_string()))
.unwrap_or(self.workspace_root)
.clone()
}
}
impl RunningTargetsPoller {
pub fn new(poll_interval: Duration, startup_services: StartupServices) -> Self {
Self {
system: System::new(),
last_poll: None,
poll_interval,
snapshot: RunningTargets::default(),
startup_services,
install_bin_dir: cargo_install_bin_dir(),
first_seen: HashMap::new(),
cpu_history: HashMap::new(),
}
}
pub fn tick(&mut self, now: Instant, projects: &[ProjectTargetSlice<'_>]) -> &RunningTargets {
let effect = self.startup_services.running_targets_polling_effect();
if effect == StartupEffect::Suppressed {
self.startup_services.record_running_targets_polling(effect);
return &self.snapshot;
}
if self
.last_poll
.is_some_and(|last| now.duration_since(last) < self.poll_interval)
{
return &self.snapshot;
}
self.startup_services.record_running_targets_polling(effect);
self.last_poll = Some(now);
self.system.refresh_processes_specifics(
ProcessesToUpdate::All,
true,
ProcessRefreshKind::nothing()
.with_exe(UpdateKind::Always)
.with_cwd(UpdateKind::Always)
.with_cpu()
.with_memory(),
);
let mut by_key = self.collect_instances(now, projects);
let links = |pid: u32| {
self.system
.process(Pid::from_u32(pid))
.map(|process| ParentLink {
parent: process.parent().map(Pid::as_u32),
start_time: process.start_time(),
})
};
let tracked_keys: HashMap<u32, RunningKey> = by_key
.iter()
.flat_map(|(key, target)| target.instances.iter().map(|inst| (inst.pid, key.clone())))
.collect();
let tracked: HashSet<u32> = tracked_keys.keys().copied().collect();
for (key, target) in &mut by_key {
target.instances.sort_by_key(|inst| inst.pid);
for inst in &mut target.instances {
inst.parent_pid =
shown_parent_for_instance(&links, inst.pid, &tracked, &tracked_keys, key);
}
}
let mut children = Vec::new();
for (pid, process) in self.system.processes() {
let pid = pid.as_u32();
if tracked.contains(&pid) {
continue;
}
let Some(parent_pid) = shown_parent(&links, pid, &tracked) else {
continue;
};
let first_seen = *self.first_seen.entry(pid).or_insert(now);
let cpu = smoothed_cpu(&mut self.cpu_history, pid, process.cpu_usage());
children.push(ChildProcess {
pid,
name: process.name().to_string_lossy().into_owned(),
cpu_percent: cpu,
memory_bytes: process.memory(),
first_seen,
create_time: process.start_time(),
parent_pid,
});
}
let live: HashSet<u32> = tracked
.iter()
.copied()
.chain(children.iter().map(|child| child.pid))
.collect();
self.first_seen.retain(|pid, _| live.contains(pid));
self.cpu_history.retain(|pid, _| live.contains(pid));
self.snapshot = RunningTargets { by_key, children };
&self.snapshot
}
fn collect_instances(
&mut self,
now: Instant,
projects: &[ProjectTargetSlice<'_>],
) -> HashMap<RunningKey, RunningTarget> {
let install_bin_dir = self.install_bin_dir.as_ref().map(AbsolutePath::as_path);
let mut by_key: HashMap<RunningKey, RunningTarget> = HashMap::new();
for (pid, process) in self.system.processes() {
let Some(exe) = process.exe() else {
tracing::debug!(pid = pid.as_u32(), "running_targets_exe_unavailable");
continue;
};
let exe = if exe.is_absolute() {
Cow::Borrowed(exe)
} else {
process
.cwd()
.map_or(Cow::Borrowed(exe), |cwd| Cow::Owned(cwd.join(exe)))
};
let pid = pid.as_u32();
let cpu = process.cpu_usage();
let memory = process.memory();
let create_time = process.start_time();
if let Some((key, profile, member_dir)) = classify_exe(&exe, projects) {
let first_seen = *self.first_seen.entry(pid).or_insert(now);
let cpu = smoothed_cpu(&mut self.cpu_history, pid, cpu);
push_instance(
&mut by_key,
key,
member_dir,
instance(pid, cpu, memory, profile, first_seen, create_time),
);
} else {
let keys = installed_bin_keys(&exe, projects, install_bin_dir);
if keys.is_empty() {
continue;
}
let first_seen = *self.first_seen.entry(pid).or_insert(now);
let cpu = smoothed_cpu(&mut self.cpu_history, pid, cpu);
for (key, member_dir) in keys {
push_instance(
&mut by_key,
key,
member_dir,
instance(
pid,
cpu,
memory,
RunProfile::Installed,
first_seen,
create_time,
),
);
}
}
}
by_key
}
pub const fn snapshot(&self) -> &RunningTargets { &self.snapshot }
#[cfg(test)]
pub fn set_snapshot_for_test(&mut self, snapshot: RunningTargets) { self.snapshot = snapshot; }
pub fn kill(&mut self, pid: u32, create_time: u64) -> bool {
self.system.refresh_processes_specifics(
ProcessesToUpdate::Some(&[Pid::from_u32(pid)]),
true,
ProcessRefreshKind::nothing(),
);
self.system
.process(Pid::from_u32(pid))
.filter(|process| process.start_time() == create_time)
.is_some_and(|process| process.kill_with(Signal::Term).unwrap_or(false))
}
pub fn drop_instances(&mut self, pids: &[u32]) {
self.snapshot.drop_pids(pids);
for pid in pids {
self.first_seen.remove(pid);
self.cpu_history.remove(pid);
}
}
}
impl RunningTargets {
pub fn iter_targets(
&self,
) -> impl Iterator<Item = (&RunningKey, &AbsolutePath, &[RunningInstance])> {
self.by_key
.iter()
.map(|(key, target)| (key, &target.member_dir, target.instances.as_slice()))
}
pub fn has_instances(&self) -> bool { !self.by_key.is_empty() }
pub fn child_processes(&self) -> &[ChildProcess] { &self.children }
fn drop_pids(&mut self, pids: &[u32]) {
for target in self.by_key.values_mut() {
target.instances.retain(|inst| !pids.contains(&inst.pid));
}
self.by_key.retain(|_, target| !target.instances.is_empty());
self.children.retain(|child| !pids.contains(&child.pid));
}
#[cfg(test)]
pub fn from_pairs(pairs: Vec<(RunningKey, Vec<RunningInstance>)>) -> Self {
Self {
by_key: pairs
.into_iter()
.map(|(key, instances)| {
let member_dir = key
.target_dir
.as_path()
.parent()
.map_or_else(|| key.target_dir.clone(), AbsolutePath::from);
(
key,
RunningTarget {
member_dir,
instances,
},
)
})
.collect(),
children: Vec::new(),
}
}
#[cfg(test)]
pub fn with_children(mut self, children: Vec<ChildProcess>) -> Self {
self.children = children;
self
}
}
#[cfg(test)]
impl ChildProcess {
pub fn for_test(pid: u32, name: &str, parent_pid: u32) -> Self {
Self {
pid,
name: name.to_string(),
cpu_percent: 0.0,
memory_bytes: 0,
first_seen: test_instant_at(pid),
create_time: u64::from(pid),
parent_pid,
}
}
}
#[cfg(test)]
impl RunningInstance {
pub fn for_test(pid: u32, profile: RunProfile) -> Self {
Self {
pid,
cpu_percent: 0.0,
memory_bytes: 0,
profile,
first_seen: test_instant_at(pid),
create_time: u64::from(pid),
parent_pid: None,
}
}
pub const fn with_parent(mut self, parent: u32) -> Self {
self.parent_pid = Some(parent);
self
}
pub const fn with_metrics(mut self, cpu_percent: f32, memory_bytes: u64) -> Self {
self.cpu_percent = cpu_percent;
self.memory_bytes = memory_bytes;
self
}
}
#[cfg(test)]
pub fn test_instant_at(order: u32) -> Instant {
static BASE: OnceLock<Instant> = OnceLock::new();
*BASE.get_or_init(Instant::now) + Duration::from_secs(u64::from(order))
}
const fn instance(
pid: u32,
cpu: f32,
memory: u64,
profile: RunProfile,
first_seen: Instant,
create_time: u64,
) -> RunningInstance {
RunningInstance {
pid,
cpu_percent: cpu,
memory_bytes: memory,
profile,
first_seen,
create_time,
parent_pid: None,
}
}
#[derive(Clone, Copy)]
struct ParentLink {
parent: Option<u32>,
start_time: u64,
}
fn nearest_tracked_ancestor(
links: &impl Fn(u32) -> Option<ParentLink>,
pid: u32,
tracked: &HashSet<u32>,
) -> Option<u32> {
let mut current = pid;
let mut child_start = links(pid)?.start_time;
for _ in 0..ANCESTOR_WALK_CAP {
let parent = links(current)?.parent?;
if parent == current {
return None;
}
let link = links(parent)?;
if link.start_time > child_start {
return None;
}
if tracked.contains(&parent) {
return Some(parent);
}
current = parent;
child_start = link.start_time;
}
None
}
fn shown_parent(
links: &impl Fn(u32) -> Option<ParentLink>,
pid: u32,
tracked: &HashSet<u32>,
) -> Option<u32> {
nearest_tracked_ancestor(links, pid, tracked)?;
links(pid)?.parent
}
fn shown_parent_for_instance(
links: &impl Fn(u32) -> Option<ParentLink>,
pid: u32,
tracked: &HashSet<u32>,
tracked_keys: &HashMap<u32, RunningKey>,
key: &RunningKey,
) -> Option<u32> {
let ancestor = nearest_tracked_ancestor(links, pid, tracked)?;
(tracked_keys.get(&ancestor) == Some(key)).then(|| links(pid)?.parent)?
}
fn smoothed_cpu(history: &mut HashMap<u32, RollingMean>, pid: u32, sample: f32) -> f32 {
history.entry(pid).or_default().push(sample)
}
fn push_instance(
by_key: &mut HashMap<RunningKey, RunningTarget>,
key: RunningKey,
member_dir: AbsolutePath,
inst: RunningInstance,
) {
by_key
.entry(key)
.or_insert_with(|| RunningTarget {
member_dir,
instances: Vec::new(),
})
.instances
.push(inst);
}
fn classify_exe(
exe: &Path,
projects: &[ProjectTargetSlice<'_>],
) -> Option<(RunningKey, RunProfile, AbsolutePath)> {
for slice in projects {
if let Ok(rest) = exe.strip_prefix(slice.target_dir.as_path())
&& let Some((run_target_kind, name, profile)) = classify_tail(rest, slice.bench_names)
{
let member_dir = slice.member_dir(run_target_kind, &name);
let key = RunningKey {
target_dir: slice.target_dir.clone(),
run_target_kind,
name,
};
return Some((key, profile, member_dir));
}
}
None
}
fn installed_bin_keys(
exe: &Path,
projects: &[ProjectTargetSlice<'_>],
install_bin_dir: Option<&Path>,
) -> Vec<(RunningKey, AbsolutePath)> {
let Some(bin_dir) = install_bin_dir else {
return Vec::new();
};
if exe.parent() != Some(bin_dir) {
return Vec::new();
}
let Some(stem) = exe.file_stem().and_then(|s| s.to_str()) else {
return Vec::new();
};
projects
.iter()
.filter(|slice| slice.bin_names.contains(stem))
.map(|slice| {
(
RunningKey {
target_dir: slice.target_dir.clone(),
run_target_kind: RunTargetKind::Binary,
name: stem.to_string(),
},
slice.member_dir(RunTargetKind::Binary, stem),
)
})
.collect()
}
fn classify_tail(
rest: &Path,
bench_names: &HashSet<String>,
) -> Option<(RunTargetKind, String, RunProfile)> {
let segments: Vec<&str> = rest
.components()
.filter_map(|c| match c {
Component::Normal(s) => s.to_str(),
_ => None,
})
.collect();
match segments.as_slice() {
[profile, name] => {
parse_profile(profile).map(|profile| (RunTargetKind::Binary, (*name).into(), profile))
},
[profile, "examples", name] => {
parse_profile(profile).map(|profile| (RunTargetKind::Example, (*name).into(), profile))
},
[profile, "deps", basename] => {
let profile = parse_profile(profile)?;
parse_bench_basename(basename, bench_names)
.map(|name| (RunTargetKind::Bench, name, profile))
},
_ => None,
}
}
const fn parse_profile(s: &str) -> Option<RunProfile> {
match s.as_bytes() {
b"debug" => Some(RunProfile::Debug),
b"release" => Some(RunProfile::Release),
_ => None,
}
}
fn cargo_install_bin_dir() -> Option<AbsolutePath> {
let base = env::var_os("CARGO_INSTALL_ROOT")
.map(PathBuf::from)
.or_else(|| env::var_os("CARGO_HOME").map(PathBuf::from))
.or_else(|| env::var_os("HOME").map(|home| PathBuf::from(home).join(".cargo")))?;
let bin = base.join(CARGO_BIN_DIR);
let canonical = bin.canonicalize().unwrap_or(bin);
Some(AbsolutePath::from(canonical))
}
fn parse_bench_basename(basename: &str, bench_names: &HashSet<String>) -> Option<String> {
let mut best: Option<String> = None;
for (i, ch) in basename.char_indices() {
if ch != '-' {
continue;
}
let name = &basename[..i];
let hash = &basename[i + 1..];
if !is_hex_hash(hash) {
continue;
}
if !bench_names.contains(name) {
continue;
}
if best.as_ref().is_none_or(|b| name.len() > b.len()) {
best = Some(name.to_string());
}
}
best
}
fn is_hex_hash(s: &str) -> bool {
s.len() >= MIN_HEX_HASH_LEN
&& s.bytes()
.all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
}
#[cfg(test)]
#[allow(
clippy::expect_used,
reason = "tests should panic on unexpected values"
)]
mod tests {
use std::path::Path;
use std::path::PathBuf;
use tui_pane::CPU_SMOOTHING_WINDOW_POLLS;
use super::*;
fn no_member_dirs() -> HashMap<(RunTargetKind, String), AbsolutePath> { HashMap::new() }
fn slice<'a>(
dir: &'a AbsolutePath,
bench_names: &'a HashSet<String>,
bin_names: &'a HashSet<String>,
member_dirs: &'a HashMap<(RunTargetKind, String), AbsolutePath>,
) -> ProjectTargetSlice<'a> {
ProjectTargetSlice {
target_dir: dir,
workspace_root: dir,
bench_names,
bin_names,
member_dirs,
}
}
fn exe_path(path: &str) -> PathBuf { crate::project::normalize_test_path(Path::new(path)) }
fn names(names: &[&str]) -> HashSet<String> { names.iter().map(|s| (*s).to_string()).collect() }
fn running_key(target_dir: &str, run_target_kind: RunTargetKind, name: &str) -> RunningKey {
RunningKey {
target_dir: AbsolutePath::from(PathBuf::from(target_dir)),
run_target_kind,
name: name.to_string(),
}
}
#[test]
fn debug_bin() {
let dir = AbsolutePath::from(PathBuf::from("/tmp/ws/target"));
let (benches, bins, members) = (names(&[]), names(&[]), no_member_dirs());
let s = slice(&dir, &benches, &bins, &members);
let exe = exe_path("/tmp/ws/target/debug/foo");
let (key, profile, _member) =
classify_exe(&exe, std::slice::from_ref(&s)).expect("matches");
assert!(matches!(key.run_target_kind, RunTargetKind::Binary));
assert_eq!(key.name, "foo");
assert_eq!(key.target_dir, dir);
assert_eq!(profile, RunProfile::Debug);
}
#[test]
fn release_example() {
let dir = AbsolutePath::from(PathBuf::from("/tmp/ws/target"));
let (benches, bins, members) = (names(&[]), names(&[]), no_member_dirs());
let s = slice(&dir, &benches, &bins, &members);
let exe = exe_path("/tmp/ws/target/release/examples/bar");
let (key, profile, _member) =
classify_exe(&exe, std::slice::from_ref(&s)).expect("matches");
assert!(matches!(key.run_target_kind, RunTargetKind::Example));
assert_eq!(key.name, "bar");
assert_eq!(profile, RunProfile::Release);
}
#[test]
fn bench_with_known_name() {
let dir = AbsolutePath::from(PathBuf::from("/tmp/ws/target"));
let (benches, bins, members) = (names(&["baz"]), names(&[]), no_member_dirs());
let s = slice(&dir, &benches, &bins, &members);
let exe = exe_path("/tmp/ws/target/debug/deps/baz-0123456789abcdef");
let (key, _, _) = classify_exe(&exe, std::slice::from_ref(&s)).expect("matches");
assert!(matches!(key.run_target_kind, RunTargetKind::Bench));
assert_eq!(key.name, "baz");
}
#[test]
fn bench_rejects_short_hash() {
let dir = AbsolutePath::from(PathBuf::from("/tmp/ws/target"));
let (benches, bins, members) = (names(&["baz"]), names(&[]), no_member_dirs());
let s = slice(&dir, &benches, &bins, &members);
let exe = exe_path("/tmp/ws/target/debug/deps/baz-shorthash");
assert!(classify_exe(&exe, std::slice::from_ref(&s)).is_none());
}
#[test]
fn deps_entry_not_in_bench_set_is_unrecognized() {
let dir = AbsolutePath::from(PathBuf::from("/tmp/ws/target"));
let (benches, bins, members) = (names(&["baz"]), names(&[]), no_member_dirs());
let s = slice(&dir, &benches, &bins, &members);
let exe = exe_path("/tmp/ws/target/debug/deps/other-0123456789abcdef");
assert!(classify_exe(&exe, std::slice::from_ref(&s)).is_none());
}
#[test]
fn longest_bench_name_wins() {
let dir = AbsolutePath::from(PathBuf::from("/tmp/ws/target"));
let (benches, bins, members) = (names(&["my", "my-bench"]), names(&[]), no_member_dirs());
let s = slice(&dir, &benches, &bins, &members);
let exe = exe_path("/tmp/ws/target/debug/deps/my-bench-0123456789abcdef");
let (key, _, _) = classify_exe(&exe, std::slice::from_ref(&s)).expect("matches");
assert!(matches!(key.run_target_kind, RunTargetKind::Bench));
assert_eq!(key.name, "my-bench");
}
#[test]
fn outside_target_dir_does_not_match() {
let dir = AbsolutePath::from(PathBuf::from("/tmp/ws/target"));
let (benches, bins, members) = (names(&[]), names(&[]), no_member_dirs());
let s = slice(&dir, &benches, &bins, &members);
let exe = exe_path("/usr/bin/ls");
assert!(classify_exe(&exe, std::slice::from_ref(&s)).is_none());
}
#[test]
fn build_artifact_under_target_ignored() {
let dir = AbsolutePath::from(PathBuf::from("/tmp/ws/target"));
let (benches, bins, members) = (names(&[]), names(&[]), no_member_dirs());
let s = slice(&dir, &benches, &bins, &members);
let exe = exe_path("/tmp/ws/target/debug/build/foo-1234567890abcdef/build-script-build");
assert!(classify_exe(&exe, std::slice::from_ref(&s)).is_none());
}
#[test]
fn installed_bin_in_cargo_dir_matches_as_cargo_profile() {
let dir = AbsolutePath::from(PathBuf::from("/tmp/ws/target"));
let (benches, bins, members) = (names(&[]), names(&["cargo-port"]), no_member_dirs());
let s = slice(&dir, &benches, &bins, &members);
let bin_dir = exe_path("/home/me/.cargo/bin");
let exe = exe_path("/home/me/.cargo/bin/cargo-port");
let keys = installed_bin_keys(&exe, std::slice::from_ref(&s), Some(&bin_dir));
assert_eq!(keys.len(), 1);
let (key, member_dir) = &keys[0];
assert!(matches!(key.run_target_kind, RunTargetKind::Binary));
assert_eq!(key.name, "cargo-port");
assert_eq!(key.target_dir, dir);
assert_eq!(*member_dir, dir);
}
#[test]
fn installed_bin_attributed_to_every_project_declaring_it() {
let primary = AbsolutePath::from(PathBuf::from("/tmp/main/target"));
let worktree = AbsolutePath::from(PathBuf::from("/tmp/wt/target"));
let (benches, bins, members) = (names(&[]), names(&["cargo-port"]), no_member_dirs());
let slices = [
slice(&primary, &benches, &bins, &members),
slice(&worktree, &benches, &bins, &members),
];
let bin_dir = exe_path("/home/me/.cargo/bin");
let exe = exe_path("/home/me/.cargo/bin/cargo-port");
let dirs: HashSet<AbsolutePath> = installed_bin_keys(&exe, &slices, Some(&bin_dir))
.into_iter()
.map(|(key, _)| key.target_dir)
.collect();
assert_eq!(dirs, HashSet::from([primary, worktree]));
}
#[test]
fn classified_exe_resolves_its_member_dir() {
let dir = AbsolutePath::from(PathBuf::from("/tmp/ws/target"));
let member = AbsolutePath::from(PathBuf::from("/tmp/ws/crates/foo"));
let (benches, bins) = (names(&[]), names(&[]));
let members = HashMap::from([((RunTargetKind::Binary, "foo".to_string()), member.clone())]);
let s = slice(&dir, &benches, &bins, &members);
let exe = exe_path("/tmp/ws/target/debug/foo");
let (_, _, member_dir) = classify_exe(&exe, std::slice::from_ref(&s)).expect("matches");
assert_eq!(member_dir, member);
}
#[test]
fn unknown_target_falls_back_to_the_workspace_root() {
let dir = AbsolutePath::from(PathBuf::from("/tmp/ws/target"));
let (benches, bins, members) = (names(&[]), names(&[]), no_member_dirs());
let s = slice(&dir, &benches, &bins, &members);
let exe = exe_path("/tmp/ws/target/debug/stale");
let (_, _, member_dir) = classify_exe(&exe, std::slice::from_ref(&s)).expect("matches");
assert_eq!(member_dir, dir);
}
#[test]
fn drop_instances_evicts_the_first_seen_entry() {
let mut poller =
RunningTargetsPoller::new(Duration::from_secs(1), StartupServices::production());
poller.first_seen.insert(42, test_instant_at(0));
poller.first_seen.insert(43, test_instant_at(1));
poller.drop_instances(&[42]);
assert!(!poller.first_seen.contains_key(&42));
assert!(poller.first_seen.contains_key(&43));
}
fn links_from(table: Vec<(u32, Option<u32>, u64)>) -> impl Fn(u32) -> Option<ParentLink> {
move |pid| {
table
.iter()
.find(|(candidate, _, _)| *candidate == pid)
.map(|(_, parent, start_time)| ParentLink {
parent: *parent,
start_time: *start_time,
})
}
}
#[test]
fn ancestor_walk_finds_a_direct_parent() {
let links = links_from(vec![(10, Some(1), 100), (20, Some(10), 200)]);
let tracked = HashSet::from([10, 20]);
assert_eq!(nearest_tracked_ancestor(&links, 20, &tracked), Some(10));
}
#[test]
fn ancestor_walk_crosses_untracked_intermediates() {
let links = links_from(vec![
(10, Some(1), 100),
(15, Some(10), 150),
(20, Some(15), 200),
]);
let tracked = HashSet::from([10, 20]);
assert_eq!(nearest_tracked_ancestor(&links, 20, &tracked), Some(10));
}
#[test]
fn ancestor_walk_rejects_a_reused_pid_by_start_time() {
let links = links_from(vec![(10, Some(1), 900), (20, Some(10), 200)]);
let tracked = HashSet::from([10, 20]);
assert_eq!(nearest_tracked_ancestor(&links, 20, &tracked), None);
}
#[test]
fn ancestor_walk_stops_when_the_chain_leaves_the_table() {
let links = links_from(vec![(20, Some(15), 200)]);
let tracked = HashSet::from([10, 20]);
assert_eq!(nearest_tracked_ancestor(&links, 20, &tracked), None);
}
#[test]
fn ancestor_walk_stops_on_a_self_parented_process() {
let links = links_from(vec![(0, Some(0), 0), (20, Some(0), 200)]);
let tracked = HashSet::from([20]);
assert_eq!(nearest_tracked_ancestor(&links, 20, &tracked), None);
}
#[test]
fn ancestor_walk_is_depth_capped() {
let depth = u32::try_from(ANCESTOR_WALK_CAP).expect("cap fits u32") + 2;
let mut table: Vec<(u32, Option<u32>, u64)> =
(1..=depth).map(|pid| (pid, Some(pid - 1), 0)).collect();
table.push((0, None, 0));
let links = links_from(table);
let tracked = HashSet::from([0, depth]);
assert_eq!(nearest_tracked_ancestor(&links, depth, &tracked), None);
}
#[test]
fn shown_parent_is_the_direct_parent_on_a_tracked_chain() {
let links = links_from(vec![
(10, Some(1), 100),
(20, Some(10), 150),
(30, Some(20), 200),
]);
let tracked = HashSet::from([10, 30]);
assert_eq!(shown_parent(&links, 30, &tracked), Some(20));
assert_eq!(shown_parent(&links, 20, &tracked), Some(10));
}
#[test]
fn shown_parent_is_none_for_an_independently_started_process() {
let links = links_from(vec![(1, None, 0), (10, Some(1), 100)]);
let tracked = HashSet::from([10]);
assert_eq!(shown_parent(&links, 10, &tracked), None);
}
#[test]
fn tracked_instance_does_not_nest_under_unrelated_tracked_parent() {
let links = links_from(vec![(10, Some(1), 100), (20, Some(10), 200)]);
let parent_key = running_key(
"/tmp/cargo-port/target",
RunTargetKind::Binary,
"cargo-port",
);
let child_key = running_key("/tmp/hana/target", RunTargetKind::Example, "units");
let tracked_keys = HashMap::from([(10, parent_key), (20, child_key.clone())]);
let tracked = HashSet::from([10, 20]);
assert_eq!(
shown_parent_for_instance(&links, 20, &tracked, &tracked_keys, &child_key),
None,
);
}
#[test]
fn tracked_instance_keeps_same_target_outline_parent() {
let links = links_from(vec![
(10, Some(1), 100),
(15, Some(10), 150),
(20, Some(15), 200),
]);
let key = running_key(
"/tmp/cargo-mend/target",
RunTargetKind::Binary,
"cargo-mend",
);
let tracked_keys = HashMap::from([(10, key.clone()), (20, key.clone())]);
let tracked = HashSet::from([10, 20]);
assert_eq!(
shown_parent_for_instance(&links, 20, &tracked, &tracked_keys, &key),
Some(15),
);
}
#[test]
fn smoothed_cpu_averages_the_window() {
let mut history = HashMap::new();
assert!((smoothed_cpu(&mut history, 7, 20.0) - 20.0).abs() < f32::EPSILON);
assert!((smoothed_cpu(&mut history, 7, 10.0) - 15.0).abs() < f32::EPSILON);
}
#[test]
fn smoothed_cpu_window_drops_the_oldest_sample() {
let mut history = HashMap::new();
for _ in 0..CPU_SMOOTHING_WINDOW_POLLS {
smoothed_cpu(&mut history, 7, 0.0);
}
let mut mean = 0.0;
for _ in 0..CPU_SMOOTHING_WINDOW_POLLS {
mean = smoothed_cpu(&mut history, 7, 50.0);
}
assert!((mean - 50.0).abs() < f32::EPSILON);
}
#[test]
fn smoothed_cpu_tracks_pids_independently() {
let mut history = HashMap::new();
smoothed_cpu(&mut history, 7, 40.0);
assert!((smoothed_cpu(&mut history, 8, 10.0) - 10.0).abs() < f32::EPSILON);
}
#[test]
fn drop_instances_evicts_the_cpu_history_entry() {
let mut poller =
RunningTargetsPoller::new(Duration::from_secs(1), StartupServices::production());
smoothed_cpu(&mut poller.cpu_history, 42, 10.0);
smoothed_cpu(&mut poller.cpu_history, 43, 10.0);
poller.drop_instances(&[42]);
assert!(!poller.cpu_history.contains_key(&42));
assert!(poller.cpu_history.contains_key(&43));
}
#[test]
fn installed_bin_not_in_bin_set_does_not_match() {
let dir = AbsolutePath::from(PathBuf::from("/tmp/ws/target"));
let (benches, bins, members) = (names(&[]), names(&["cargo-port"]), no_member_dirs());
let s = slice(&dir, &benches, &bins, &members);
let bin_dir = exe_path("/home/me/.cargo/bin");
let exe = exe_path("/home/me/.cargo/bin/ripgrep");
assert!(installed_bin_keys(&exe, std::slice::from_ref(&s), Some(&bin_dir)).is_empty());
}
#[test]
fn bin_outside_cargo_dir_does_not_match_as_installed() {
let dir = AbsolutePath::from(PathBuf::from("/tmp/ws/target"));
let (benches, bins, members) = (names(&[]), names(&["cargo-port"]), no_member_dirs());
let s = slice(&dir, &benches, &bins, &members);
let bin_dir = exe_path("/home/me/.cargo/bin");
let exe = exe_path("/usr/local/bin/cargo-port");
assert!(installed_bin_keys(&exe, std::slice::from_ref(&s), Some(&bin_dir)).is_empty());
}
}