Skip to content

Commit c257a8f

Browse files
committed
macos test
1 parent 3596e0d commit c257a8f

File tree

1 file changed

+130
-116
lines changed

1 file changed

+130
-116
lines changed

src/lib.rs

Lines changed: 130 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ fn handle_stream_activity(
156156
"Activity detected"
157157
);
158158
let new_deadline = calculate_new_deadline(timeouts.absolute_deadline, timeouts.activity);
159-
159+
160160
if *current_deadline < timeouts.absolute_deadline && new_deadline != *current_deadline {
161161
debug!(old = ?*current_deadline, new = ?new_deadline, "Updating deadline");
162162
*current_deadline = new_deadline;
@@ -337,7 +337,7 @@ async fn run_command_loop(
337337
biased; // Prioritize checking exit status
338338

339339
// 1. Check for process exit
340-
result = state.child.wait(), if can_check_exit => {
340+
result = async { state.child.wait().await }, if can_check_exit => {
341341
state.exit_status = match result {
342342
Ok(status) => {
343343
debug!(status = %status, "Process exited naturally");
@@ -495,6 +495,14 @@ pub async fn run_command_with_timeout(
495495
let mut std_cmd = std::mem::replace(&mut command, StdCommand::new("")); // Take ownership temporarily
496496
unsafe {
497497
std_cmd.pre_exec(|| {
498+
#[cfg(target_os = "macos")]
499+
{
500+
// Disable SA_RESTART flag for SIGCHLD handler
501+
let mut sa: libc::sigaction = std::mem::zeroed();
502+
libc::sigaction(libc::SIGCHLD, std::ptr::null(), &mut sa);
503+
sa.sa_flags &= !libc::SA_RESTART;
504+
libc::sigaction(libc::SIGCHLD, &sa, std::ptr::null_mut());
505+
}
498506
// libc::setpgid(0, 0) makes the new process its own group leader.
499507
// Pass 0 for both pid and pgid to achieve this for the calling process.
500508
if libc::setpgid(0, 0) == 0 {
@@ -513,7 +521,6 @@ pub async fn run_command_with_timeout(
513521

514522
// Main execution loop
515523
run_command_loop(&mut state, &timeout_config).await?;
516-
let end_time = Instant::now();
517524

518525
// Drain remaining output after loop exit (natural exit or timeout break)
519526
debug!("Command loop finished. Draining remaining output streams.");
@@ -523,24 +530,25 @@ pub async fn run_command_with_timeout(
523530
&mut state.stdout_read_buffer,
524531
"stdout",
525532
)
526-
.await?;
533+
.await?;
527534
drain_reader(
528535
&mut state.stderr_reader,
529536
&mut state.stderr_buffer,
530537
&mut state.stderr_read_buffer,
531538
"stderr",
532539
)
533-
.await?;
534-
let duration = end_time.duration_since(start_time);
540+
.await?;
535541

536542
// Post-loop processing: Final wait if killed and status not yet obtained
537543
let final_exit_status = finalize_exit_status(
538544
&mut state.child,
539545
state.exit_status, // Use status potentially set in loop
540546
state.timed_out,
541547
)
542-
.await?;
548+
.await?;
543549

550+
let end_time = Instant::now();
551+
let duration = end_time.duration_since(start_time);
544552

545553
debug!(
546554
duration = ?duration,
@@ -584,7 +592,7 @@ mod tests {
584592
fn run_async_test<F, Fut>(test_fn: F)
585593
where
586594
F: FnOnce() -> Fut,
587-
Fut: std::future::Future<Output = ()>,
595+
Fut: std::future::Future<Output=()>,
588596
{
589597
setup_tracing();
590598
let rt = Runtime::new().unwrap();
@@ -855,7 +863,7 @@ mod tests {
855863
fn test_min_timeout_greater_than_max_timeout() {
856864
run_async_test(|| async {
857865
let cmd = StdCommand::new("echo"); // removed mut
858-
// cmd.arg("test"); // Don't need args
866+
// cmd.arg("test"); // Don't need args
859867

860868
let min_timeout = Duration::from_secs(2);
861869
let max_timeout = Duration::from_secs(1); // Invalid config
@@ -876,7 +884,7 @@ mod tests {
876884
fn test_zero_activity_timeout() {
877885
run_async_test(|| async {
878886
let cmd = StdCommand::new("echo"); // removed mut
879-
// cmd.arg("test"); // Don't need args
887+
// cmd.arg("test"); // Don't need args
880888

881889
let min_timeout = Duration::from_millis(100);
882890
let max_timeout = Duration::from_secs(1);
@@ -924,8 +932,8 @@ mod tests {
924932
run_async_test(|| async {
925933
let mut cmd = StdCommand::new("sh");
926934
// Continuously output numbers for ~2 seconds, sleeping shortly
927-
cmd.arg("-c")
928-
.arg("i=0; while [ $i -lt 20 ]; do echo $i; i=$((i+1)); sleep 0.1; done");
935+
cmd.arg("-c")
936+
.arg("i=0; while [ $i -lt 20 ]; do echo $i; i=$((i+1)); /bin/sleep 0.1; done"); // Use absolute sleep path
929937
let min_timeout = Duration::from_millis(50);
930938
let max_timeout = Duration::from_secs(10);
931939
let activity_timeout = Duration::from_millis(500); // activity > sleep
@@ -947,10 +955,17 @@ mod tests {
947955
result.duration > Duration::from_secs(2),
948956
"Duration should be > 2s"
949957
); // 20 * 0.1s
958+
#[cfg(not(target_os = "macos"))]
950959
assert!(
951960
result.duration < Duration::from_secs(3),
952961
"Duration should be < 3s"
953962
);
963+
#[cfg(target_os = "macos")]
964+
assert!(
965+
result.duration < Duration::from_secs(3) + Duration::from_millis(300),
966+
"Duration should account for macOS signal latency"
967+
);
968+
954969
});
955970
}
956971

@@ -996,132 +1011,131 @@ mod tests {
9961011

9971012
// ----- tests for calculate_new_deadline -----
9981013
#[test]
999-
fn test_calculate_new_deadline_absolute_deadline_passed() {
1000-
let absolute_deadline = Instant::now() - Duration::from_secs(1); // Already passed
1001-
let activity_timeout = Duration::from_secs(5);
1014+
fn test_calculate_new_deadline_absolute_deadline_passed() {
1015+
let absolute_deadline = Instant::now() - Duration::from_secs(1); // Already passed
1016+
let activity_timeout = Duration::from_secs(5);
10021017

1003-
let new_deadline = calculate_new_deadline(absolute_deadline, activity_timeout);
1018+
let new_deadline = calculate_new_deadline(absolute_deadline, activity_timeout);
10041019

1005-
assert_eq!(
1006-
new_deadline, absolute_deadline,
1007-
"New deadline should be the absolute deadline when it has already passed"
1008-
);
1009-
}
1020+
assert_eq!(
1021+
new_deadline, absolute_deadline,
1022+
"New deadline should be the absolute deadline when it has already passed"
1023+
);
1024+
}
10101025

1011-
#[test]
1012-
fn test_calculate_new_deadline_activity_timeout_before_absolute_deadline() {
1013-
let absolute_deadline = Instant::now() + Duration::from_secs(10);
1014-
let activity_timeout = Duration::from_secs(5);
1026+
#[test]
1027+
fn test_calculate_new_deadline_activity_timeout_before_absolute_deadline() {
1028+
let absolute_deadline = Instant::now() + Duration::from_secs(10);
1029+
let activity_timeout = Duration::from_secs(5);
10151030

1016-
let new_deadline = calculate_new_deadline(absolute_deadline, activity_timeout);
1031+
let new_deadline = calculate_new_deadline(absolute_deadline, activity_timeout);
10171032

1018-
assert!(
1019-
new_deadline <= absolute_deadline,
1020-
"New deadline should not exceed the absolute deadline"
1021-
);
1022-
assert!(
1023-
new_deadline > Instant::now(),
1024-
"New deadline should be in the future"
1025-
);
1026-
}
1033+
assert!(
1034+
new_deadline <= absolute_deadline,
1035+
"New deadline should not exceed the absolute deadline"
1036+
);
1037+
assert!(
1038+
new_deadline > Instant::now(),
1039+
"New deadline should be in the future"
1040+
);
1041+
}
10271042
// ----- tests for handle_stream_activity -----
10281043
#[test]
1029-
fn test_handle_stream_activity_updates_deadline() {
1030-
let mut current_deadline = Instant::now() + Duration::from_secs(5);
1031-
let timeouts = TimeoutConfig {
1032-
minimum: Duration::from_secs(1),
1033-
maximum: Duration::from_secs(10),
1034-
activity: Duration::from_secs(3),
1035-
start_time: Instant::now(),
1036-
absolute_deadline: Instant::now() + Duration::from_secs(10),
1037-
};
1038-
1039-
handle_stream_activity(10, "stdout", &mut current_deadline, &timeouts);
1040-
1041-
assert!(
1042-
current_deadline > Instant::now(),
1043-
"Current deadline should be updated to a future time"
1044-
);
1045-
assert!(
1046-
current_deadline <= timeouts.absolute_deadline,
1047-
"Current deadline should not exceed the absolute deadline"
1048-
);
1049-
}
1050-
1051-
#[test]
1052-
fn test_handle_stream_activity_no_update_at_absolute_limit() {
1053-
let absolute_deadline = Instant::now() + Duration::from_secs(5);
1054-
let mut current_deadline = absolute_deadline; // Already at the absolute limit
1055-
let timeouts = TimeoutConfig {
1056-
minimum: Duration::from_secs(1),
1057-
maximum: Duration::from_secs(10),
1058-
activity: Duration::from_secs(3),
1059-
start_time: Instant::now(),
1060-
absolute_deadline,
1061-
};
1062-
1063-
handle_stream_activity(10, "stderr", &mut current_deadline, &timeouts);
1064-
1065-
assert_eq!(
1066-
current_deadline, absolute_deadline,
1067-
"Current deadline should remain unchanged when at the absolute limit"
1068-
);
1069-
}
1070-
1071-
// ----- tests for run_command_loop -----
1072-
#[test]
1073-
fn test_run_command_loop_exits_on_process_finish() {
1074-
run_async_test(|| async {
1075-
let mut cmd = StdCommand::new("echo");
1076-
cmd.arg("Test");
1077-
1044+
fn test_handle_stream_activity_updates_deadline() {
1045+
let mut current_deadline = Instant::now() + Duration::from_secs(5);
10781046
let timeouts = TimeoutConfig {
10791047
minimum: Duration::from_secs(1),
1080-
maximum: Duration::from_secs(5),
1081-
activity: Duration::from_secs(2),
1048+
maximum: Duration::from_secs(10),
1049+
activity: Duration::from_secs(3),
10821050
start_time: Instant::now(),
1083-
absolute_deadline: Instant::now() + Duration::from_secs(5),
1051+
absolute_deadline: Instant::now() + Duration::from_secs(10),
10841052
};
10851053

1086-
let mut state = spawn_command_and_setup_state(&mut cmd, timeouts.absolute_deadline)
1087-
.expect("Failed to spawn command");
1088-
1089-
let result = run_command_loop(&mut state, &timeouts).await;
1054+
handle_stream_activity(10, "stdout", &mut current_deadline, &timeouts);
10901055

1091-
assert!(result.is_ok(), "Command loop should exit without errors");
10921056
assert!(
1093-
state.exit_status.is_some(),
1094-
"Exit status should be set when process finishes naturally"
1057+
current_deadline > Instant::now(),
1058+
"Current deadline should be updated to a future time"
10951059
);
1096-
});
1097-
}
1098-
1099-
#[test]
1100-
fn test_run_command_loop_exits_on_timeout() {
1101-
run_async_test(|| async {
1102-
let mut cmd = StdCommand::new("sleep");
1103-
cmd.arg("5");
1060+
assert!(
1061+
current_deadline <= timeouts.absolute_deadline,
1062+
"Current deadline should not exceed the absolute deadline"
1063+
);
1064+
}
11041065

1066+
#[test]
1067+
fn test_handle_stream_activity_no_update_at_absolute_limit() {
1068+
let absolute_deadline = Instant::now() + Duration::from_secs(5);
1069+
let mut current_deadline = absolute_deadline; // Already at the absolute limit
11051070
let timeouts = TimeoutConfig {
11061071
minimum: Duration::from_secs(1),
1107-
maximum: Duration::from_secs(2), // Short timeout
1108-
activity: Duration::from_secs(10),
1072+
maximum: Duration::from_secs(10),
1073+
activity: Duration::from_secs(3),
11091074
start_time: Instant::now(),
1110-
absolute_deadline: Instant::now() + Duration::from_secs(2),
1075+
absolute_deadline,
11111076
};
11121077

1113-
let mut state = spawn_command_and_setup_state(&mut cmd, timeouts.absolute_deadline)
1114-
.expect("Failed to spawn command");
1078+
handle_stream_activity(10, "stderr", &mut current_deadline, &timeouts);
11151079

1116-
let result = run_command_loop(&mut state, &timeouts).await;
1117-
1118-
assert!(result.is_ok(), "Command loop should exit without errors");
1119-
assert!(
1120-
state.exit_status.is_none(),
1121-
"Exit status should be None when process is killed due to timeout"
1080+
assert_eq!(
1081+
current_deadline, absolute_deadline,
1082+
"Current deadline should remain unchanged when at the absolute limit"
11221083
);
1123-
assert!(state.timed_out, "State should indicate that the process timed out");
1124-
});
1125-
}
1084+
}
1085+
1086+
// ----- tests for run_command_loop -----
1087+
#[test]
1088+
fn test_run_command_loop_exits_on_process_finish() {
1089+
run_async_test(|| async {
1090+
let mut cmd = StdCommand::new("echo");
1091+
cmd.arg("Test");
1092+
1093+
let timeouts = TimeoutConfig {
1094+
minimum: Duration::from_secs(1),
1095+
maximum: Duration::from_secs(5),
1096+
activity: Duration::from_secs(2),
1097+
start_time: Instant::now(),
1098+
absolute_deadline: Instant::now() + Duration::from_secs(5),
1099+
};
1100+
1101+
let mut state = spawn_command_and_setup_state(&mut cmd, timeouts.absolute_deadline)
1102+
.expect("Failed to spawn command");
11261103

1104+
let result = run_command_loop(&mut state, &timeouts).await;
1105+
1106+
assert!(result.is_ok(), "Command loop should exit without errors");
1107+
assert!(
1108+
state.exit_status.is_some(),
1109+
"Exit status should be set when process finishes naturally"
1110+
);
1111+
});
1112+
}
1113+
1114+
#[test]
1115+
fn test_run_command_loop_exits_on_timeout() {
1116+
run_async_test(|| async {
1117+
let mut cmd = StdCommand::new("sleep");
1118+
cmd.arg("5");
1119+
1120+
let timeouts = TimeoutConfig {
1121+
minimum: Duration::from_secs(1),
1122+
maximum: Duration::from_secs(2), // Short timeout
1123+
activity: Duration::from_secs(10),
1124+
start_time: Instant::now(),
1125+
absolute_deadline: Instant::now() + Duration::from_secs(2),
1126+
};
1127+
1128+
let mut state = spawn_command_and_setup_state(&mut cmd, timeouts.absolute_deadline)
1129+
.expect("Failed to spawn command");
1130+
1131+
let result = run_command_loop(&mut state, &timeouts).await;
1132+
1133+
assert!(result.is_ok(), "Command loop should exit without errors");
1134+
assert!(
1135+
state.exit_status.is_none(),
1136+
"Exit status should be None when process is killed due to timeout"
1137+
);
1138+
assert!(state.timed_out, "State should indicate that the process timed out");
1139+
});
1140+
}
11271141
} // end tests mod

0 commit comments

Comments
 (0)