Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions crates/xchecker-tui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,8 @@ fn render_details(f: &mut Frame, app: &TuiApp, area: Rect) {
Some(s) => s,
None => {
let empty = Paragraph::new("No spec selected")
.style(Style::default().fg(Color::DarkGray))
.alignment(Alignment::Center)
.block(Block::default().borders(Borders::ALL).title(" Details "));
f.render_widget(empty, area);
return;
Expand Down Expand Up @@ -685,6 +687,7 @@ fn render_footer(f: &mut Frame, app: &TuiApp, area: Rect) {

let footer = Paragraph::new(help_text)
.style(Style::default().fg(Color::DarkGray))
.alignment(Alignment::Center)
.block(Block::default().borders(Borders::ALL).title(" Help "));
f.render_widget(footer, area);
}
Expand Down
52 changes: 31 additions & 21 deletions tests/test_unix_process_termination.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,10 @@ type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
// Helper Functions
// ============================================================================

/// Check if a process is still running
fn is_process_running(pid: u32) -> bool {
use nix::sys::signal::kill;
use nix::unistd::Pid;

let pid = Pid::from_raw(pid as i32);
// Signal 0 (None) doesn't send a signal but checks if the process exists
kill(pid, None).is_ok()
/// Check if a process is still running via child process handle
/// This avoids false positives from zombie processes where kill(pid, 0) returns true
fn is_process_running_via_child(child: &mut tokio::process::Child) -> bool {
child.try_wait().unwrap().is_none()
}

/// Create a test script that spawns child processes
Expand Down Expand Up @@ -97,7 +93,10 @@ async fn test_process_group_creation() -> Result<()> {
let pid = child.id().expect("Failed to get child PID");

// Check that the process is running
assert!(is_process_running(pid), "Process should be running");
assert!(
is_process_running_via_child(&mut child),
"Process should be running"
);

// Get the process group ID
let pgid = unsafe { libc::getpgid(pid as i32) };
Expand Down Expand Up @@ -130,7 +129,7 @@ async fn test_sigterm_then_sigkill_sequence() -> Result<()> {
// Spawn a process that ignores SIGTERM (to test SIGKILL)
let mut cmd = CommandSpec::new("sh")
.arg("-c")
.arg("trap '' TERM; sleep 30") // Ignore SIGTERM, sleep for 30 seconds
.arg("trap '' TERM; while true; do sleep 1; done") // Ignore SIGTERM robustly
.to_tokio_command();
cmd.stdin(Stdio::null())
.stdout(Stdio::null())
Expand All @@ -153,10 +152,13 @@ async fn test_sigterm_then_sigkill_sequence() -> Result<()> {

// Verify process is running
assert!(
is_process_running(pid),
is_process_running_via_child(&mut child),
"Process should be running initially"
);

// Wait for the shell trap to be registered
sleep(Duration::from_millis(1000)).await;

// Send SIGTERM (process will ignore it)
killpg(pgid, Signal::SIGTERM)?;

Expand All @@ -165,7 +167,7 @@ async fn test_sigterm_then_sigkill_sequence() -> Result<()> {

// Process should still be running (it ignored SIGTERM)
assert!(
is_process_running(pid),
is_process_running_via_child(&mut child),
"Process should still be running after SIGTERM"
);

Expand All @@ -177,7 +179,7 @@ async fn test_sigterm_then_sigkill_sequence() -> Result<()> {

// Process should now be terminated
assert!(
!is_process_running(pid),
!is_process_running_via_child(&mut child),
"Process should be terminated after SIGKILL"
);

Expand Down Expand Up @@ -218,7 +220,7 @@ async fn test_graceful_termination_with_sigterm() -> Result<()> {

// Verify process is running
assert!(
is_process_running(pid),
is_process_running_via_child(&mut child),
"Process should be running initially"
);

Expand All @@ -230,7 +232,7 @@ async fn test_graceful_termination_with_sigterm() -> Result<()> {

// Process should be terminated (sleep responds to SIGTERM)
assert!(
!is_process_running(pid),
!is_process_running_via_child(&mut child),
"Process should be terminated after SIGTERM"
);

Expand Down Expand Up @@ -284,7 +286,7 @@ async fn test_process_group_termination() -> Result<()> {

// Verify parent is running
assert!(
is_process_running(parent_pid),
is_process_running_via_child(&mut child),
"Parent process should be running"
);

Expand All @@ -299,7 +301,7 @@ async fn test_process_group_termination() -> Result<()> {

// Verify parent is terminated
assert!(
!is_process_running(parent_pid),
!is_process_running_via_child(&mut child),
"Parent process should be terminated"
);

Expand Down Expand Up @@ -327,7 +329,9 @@ async fn test_runner_timeout_terminates_process_group() -> Result<()> {
create_test_script(script_path.to_str().unwrap(), 60)?;

// Create a runner with a short timeout
let runner = Runner::native();
let mut runner = Runner::native();
// Use bash as a fallback to avoid "claude not found" in CI
runner.wsl_options.claude_path = Some("bash".to_string());

// Execute with a very short timeout (1 second)
let timeout_duration = Some(Duration::from_secs(1));
Expand Down Expand Up @@ -392,7 +396,10 @@ async fn test_timeout_grace_period() -> Result<()> {
let pgid = Pid::from_raw(pid as i32);

// Verify process is running
assert!(is_process_running(pid), "Process should be running");
assert!(
is_process_running_via_child(&mut child),
"Process should be running"
);

// Simulate the timeout sequence from Runner
// 1. Send SIGTERM
Expand All @@ -418,7 +425,7 @@ async fn test_timeout_grace_period() -> Result<()> {

// Process should be terminated
assert!(
!is_process_running(pid),
!is_process_running_via_child(&mut child),
"Process should be terminated after SIGKILL"
);

Expand Down Expand Up @@ -464,7 +471,10 @@ async fn test_terminate_already_dead_process() -> Result<()> {
let _ = child.wait().await;

// Verify process is not running
assert!(!is_process_running(pid), "Process should have exited");
assert!(
!is_process_running_via_child(&mut child),
"Process should have exited"
);

// Try to terminate (should not panic or error)
let result = killpg(pgid, Signal::SIGTERM);
Expand Down
Loading