Skip to content

Commit 1f6367f

Browse files
committed
chore: add exectrack tests
1 parent f43a667 commit 1f6367f

File tree

1 file changed

+255
-0
lines changed

1 file changed

+255
-0
lines changed
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
use anyhow::Context;
2+
use exectrack::{HierarchyBuilder, Tracker};
3+
use std::process::Command;
4+
5+
/// Helper to track a command and build its process hierarchy
6+
pub fn track_command(
7+
command: &str,
8+
args: &[&str],
9+
) -> anyhow::Result<(
10+
runner_shared::artifacts::ProcessHierarchy,
11+
std::thread::JoinHandle<()>,
12+
)> {
13+
// Create tracker FIRST, before spawning the child
14+
let mut tracker = Tracker::new()?;
15+
16+
// Now spawn the child
17+
let mut child = Command::new(command)
18+
.args(args)
19+
.spawn()
20+
.context("Failed to spawn command")?;
21+
let root_pid = child.id() as i32;
22+
23+
// Track the child process
24+
let rx = tracker.track(root_pid)?;
25+
26+
// Build hierarchy from events in a separate thread (like the CLI does)
27+
let hierarchy_thread = std::thread::spawn(move || {
28+
let mut builder = HierarchyBuilder::new(root_pid);
29+
for event in rx {
30+
builder.process_event(&event);
31+
}
32+
builder.into_hierarchy()
33+
});
34+
35+
// Wait for child to complete
36+
let _ = child.wait()?;
37+
38+
// Drop tracker to close the event channel
39+
drop(tracker);
40+
41+
// Get the hierarchy
42+
let hierarchy = hierarchy_thread.join().unwrap();
43+
44+
eprintln!("Tracked {} processes", hierarchy.processes.len());
45+
46+
// Return a dummy thread handle
47+
let thread_handle = std::thread::spawn(|| {});
48+
49+
Ok((hierarchy, thread_handle))
50+
}
51+
52+
// ============================================================================
53+
// INTEGRATION TESTS - CHILD PROCESS TRACKING
54+
// ============================================================================
55+
56+
/// Test that a single process (no children) is tracked correctly
57+
#[test_log::test]
58+
fn test_single_process_no_children() -> anyhow::Result<()> {
59+
let (hierarchy, thread_handle) = track_command("sleep", &["1"])?;
60+
61+
// Should have the root process
62+
assert!(
63+
hierarchy.processes.contains_key(&hierarchy.root_pid),
64+
"Root process should be tracked"
65+
);
66+
67+
// Should have no children
68+
assert!(
69+
hierarchy.children.is_empty(),
70+
"Single process should have no children"
71+
);
72+
73+
thread_handle.join().unwrap();
74+
Ok(())
75+
}
76+
77+
/// Test that bash spawning a single child process is tracked
78+
#[test_log::test]
79+
fn test_bash_single_child() -> anyhow::Result<()> {
80+
// Use a subshell to force a fork
81+
let (hierarchy, thread_handle) = track_command("bash", &["-c", "(sleep 0.5)"])?;
82+
83+
eprintln!("Hierarchy: {hierarchy:#?}");
84+
85+
// Should have at least 2 processes (bash + sleep or bash + subshell)
86+
assert!(
87+
hierarchy.processes.len() >= 2,
88+
"Expected at least 2 processes, got {}",
89+
hierarchy.processes.len()
90+
);
91+
92+
// Should have parent-child relationships
93+
assert!(
94+
!hierarchy.children.is_empty(),
95+
"Expected parent-child relationships to be tracked"
96+
);
97+
98+
thread_handle.join().unwrap();
99+
Ok(())
100+
}
101+
102+
/// Test that bash spawning multiple children is tracked
103+
#[test_log::test]
104+
fn test_bash_multiple_children() -> anyhow::Result<()> {
105+
let (hierarchy, thread_handle) = track_command("bash", &["-c", "sleep 0.5 & sleep 1"])?;
106+
107+
eprintln!("Hierarchy: {hierarchy:#?}");
108+
109+
// Should have at least 2 processes (bash + at least one sleep)
110+
// Note: May not capture all children due to timing
111+
assert!(
112+
hierarchy.processes.len() >= 2,
113+
"Expected at least 2 processes (bash + children), got {}",
114+
hierarchy.processes.len()
115+
);
116+
117+
// Should have parent-child relationships
118+
assert!(
119+
!hierarchy.children.is_empty(),
120+
"Expected parent-child relationships to be tracked"
121+
);
122+
123+
// Find the bash process
124+
let bash_pids: Vec<_> = hierarchy
125+
.processes
126+
.iter()
127+
.filter(|(_, meta)| meta.name.contains("bash"))
128+
.map(|(pid, _)| pid)
129+
.collect();
130+
131+
assert!(
132+
!bash_pids.is_empty(),
133+
"Expected to find bash process in hierarchy"
134+
);
135+
136+
// Check that bash has at least 1 child
137+
for bash_pid in bash_pids {
138+
if let Some(children) = hierarchy.children.get(bash_pid) {
139+
eprintln!("Bash PID {} has {} children", bash_pid, children.len());
140+
if !children.is_empty() {
141+
// Found the parent with children
142+
thread_handle.join().unwrap();
143+
return Ok(());
144+
}
145+
}
146+
}
147+
148+
panic!("Expected bash to have at least 1 child");
149+
}
150+
151+
/// Test nested process hierarchy (bash -> bash -> sleep)
152+
#[test_log::test]
153+
fn test_nested_process_hierarchy() -> anyhow::Result<()> {
154+
let (hierarchy, thread_handle) = track_command("bash", &["-c", "bash -c '(sleep 0.5)'"])?;
155+
156+
eprintln!("Hierarchy: {hierarchy:#?}");
157+
158+
// Should have at least 2 processes (may have exec optimization)
159+
assert!(
160+
hierarchy.processes.len() >= 2,
161+
"Expected at least 2 processes, got {}",
162+
hierarchy.processes.len()
163+
);
164+
165+
// Should have parent-child relationships
166+
assert!(
167+
!hierarchy.children.is_empty(),
168+
"Expected parent-child relationships to be tracked"
169+
);
170+
171+
thread_handle.join().unwrap();
172+
Ok(())
173+
}
174+
175+
/// Test that exit codes are captured
176+
#[test_log::test]
177+
fn test_exit_code_capture() -> anyhow::Result<()> {
178+
let (hierarchy, thread_handle) = track_command("bash", &["-c", "exit 42"])?;
179+
180+
eprintln!("Hierarchy: {hierarchy:#?}");
181+
182+
// Find any process with an exit code
183+
let has_exit_code = hierarchy
184+
.processes
185+
.values()
186+
.any(|meta| meta.exit_code.is_some());
187+
188+
assert!(
189+
has_exit_code,
190+
"Expected at least one process to have an exit code"
191+
);
192+
193+
thread_handle.join().unwrap();
194+
Ok(())
195+
}
196+
197+
/// Test with sh instead of bash
198+
#[test_log::test]
199+
fn test_sh_with_children() -> anyhow::Result<()> {
200+
// Use subshell to force fork
201+
let (hierarchy, thread_handle) = track_command("sh", &["-c", "(sleep 0.5)"])?;
202+
203+
eprintln!("Hierarchy: {hierarchy:#?}");
204+
205+
// Should have at least 2 processes (sh + sleep or subshell)
206+
assert!(
207+
hierarchy.processes.len() >= 2,
208+
"Expected at least 2 processes (sh + sleep), got {}",
209+
hierarchy.processes.len()
210+
);
211+
212+
thread_handle.join().unwrap();
213+
Ok(())
214+
}
215+
216+
/// Test process names are captured correctly
217+
#[test_log::test]
218+
fn test_process_names_captured() -> anyhow::Result<()> {
219+
let (hierarchy, thread_handle) = track_command("sleep", &["0.5"])?;
220+
221+
eprintln!("Hierarchy: {hierarchy:#?}");
222+
223+
// Should find processes with expected names
224+
let has_sleep = hierarchy
225+
.processes
226+
.values()
227+
.any(|meta| meta.name == "sleep");
228+
229+
assert!(has_sleep, "Expected to find 'sleep' process in hierarchy");
230+
231+
thread_handle.join().unwrap();
232+
Ok(())
233+
}
234+
235+
/// Test that start and stop times are recorded
236+
#[test_log::test]
237+
fn test_timestamps_recorded() -> anyhow::Result<()> {
238+
let (hierarchy, thread_handle) = track_command("sleep", &["0.1"])?;
239+
240+
eprintln!("Hierarchy: {hierarchy:#?}");
241+
242+
// Check that processes have timestamps
243+
for (pid, meta) in &hierarchy.processes {
244+
eprintln!("PID {} has start time: {} ns", pid, meta.start_time);
245+
// Start time should be non-zero (nanoseconds since epoch)
246+
assert!(
247+
meta.start_time > 0,
248+
"Process {} should have valid start time",
249+
pid
250+
);
251+
}
252+
253+
thread_handle.join().unwrap();
254+
Ok(())
255+
}

0 commit comments

Comments
 (0)