use super::*;
use crate::delegate::records::DelegationRegistry;
async fn spawn(home: &std::path::Path, script: &str) -> (Arc<DelegationRegistry>, Arc<LiveChild>) {
let registry = Arc::new(DelegationRegistry::for_test(&scv_client::Layout::new(home)));
let pending = registry.begin("fake", "session", home, Some(("fake-1", 1)));
let child = LiveChild::spawn(
LiveSpec {
executable: "sh".into(),
args: vec!["-c".into(), script.into()],
cwd: home.to_owned(),
environment: pending.environment.clone(),
activity: None,
max_line_bytes: 1024,
},
Some((Arc::clone(®istry), pending)),
tokio_util::sync::CancellationToken::new(),
)
.await
.unwrap();
(registry, child)
}
fn zombie(pid: u32) -> bool {
std::fs::read_to_string(format!("/proc/{pid}/stat"))
.ok()
.and_then(|stat| {
let state = stat.rsplit_once(") ")?.1.chars().next()?;
Some(state == 'Z')
})
.unwrap_or(false)
}
async fn settles(registry: &DelegationRegistry, child: &LiveChild) {
tokio::time::timeout(Duration::from_secs(20), async {
while child.is_running() || !registry.list(true).is_empty() {
tokio::time::sleep(Duration::from_millis(20)).await;
}
})
.await
.expect("the exited child was collected and forgotten");
assert!(!zombie(child.pid), "the exited child was left a zombie");
}
#[tokio::test]
async fn a_child_exiting_between_turns_is_collected_and_forgotten() {
let home = tempfile::tempdir().unwrap();
let (registry, child) = spawn(home.path(), "sleep 0.2").await;
assert!(child.is_running());
assert_eq!(registry.list(true).len(), 1);
settles(®istry, &child).await;
tokio::time::timeout(Duration::from_secs(5), child.close())
.await
.unwrap();
}
#[tokio::test]
async fn killing_an_idle_child_frees_it_at_once() {
let home = tempfile::tempdir().unwrap();
let (registry, child) = spawn(home.path(), "exec sleep 30").await;
let handle = registry.list(true)[0].record.handle.clone();
registry.kill(&handle).await.unwrap();
settles(®istry, &child).await;
}
#[tokio::test]
async fn close_stops_a_running_child_and_its_record() {
let home = tempfile::tempdir().unwrap();
let (registry, child) = spawn(home.path(), "trap '' TERM; sleep 30").await;
child.close().await;
assert!(!child.is_running());
assert_eq!(registry.list(true), []);
assert!(!zombie(child.pid));
}
#[tokio::test]
async fn a_child_is_recorded_idle_between_turns_and_at_work_during_one() {
let home = tempfile::tempdir().unwrap();
let (registry, child) = spawn(home.path(), "exec sleep 30").await;
let entry = || {
let mut entries = registry.list(true);
assert_eq!(entries.len(), 1);
entries.remove(0)
};
assert!(entry().working());
let turn = child.begin_turn(2);
assert_eq!(entry().record.turn, Some(2));
assert!(entry().working());
drop(turn);
let idle = entry();
assert!(idle.record.idle_since_unix.is_some());
assert!(idle.processes > 0);
assert!(!idle.working());
child.set_background_jobs(2);
assert_eq!(entry().record.background_jobs, Some(2));
assert!(entry().working());
child.set_background_jobs(0);
assert_eq!(entry().record.background_jobs, None);
assert!(!entry().working());
let turn = child.begin_turn(3);
assert_eq!(entry().record.idle_since_unix, None);
assert!(entry().working());
child.close().await;
drop(turn);
assert_eq!(registry.list(true), []);
}
#[tokio::test]
async fn a_write_the_child_can_take_beats_a_passed_deadline_or_a_fired_token() {
let home = tempfile::tempdir().unwrap();
let input = home.path().join("input");
let (_registry, child) = spawn(home.path(), &format!("exec cat > '{}'", input.display())).await;
child.send(&serde_json::json!({"n": 1})).await.unwrap();
let fired = CancellationToken::new();
fired.cancel();
for n in 2..=20 {
child
.send_until(&serde_json::json!({"n": n}), Some(Instant::now()), &fired)
.await
.unwrap();
}
child.close().await;
let expected: String = (1..=20).map(|n| format!("{{\"n\":{n}}}\n")).collect();
assert_eq!(std::fs::read_to_string(&input).unwrap(), expected);
}
#[tokio::test]
async fn an_owed_message_a_child_never_reads_is_bounded_and_ends_its_input() {
let home = tempfile::tempdir().unwrap();
let (_registry, child) = spawn(home.path(), "exec sleep 30").await;
let large = serde_json::json!({"text": "x".repeat(1 << 20)});
let started = Instant::now();
let error = child.send(&large).await.unwrap_err();
assert_eq!(
error.kind,
scv_core::ToolFailure::Limit,
"{}",
error.message
);
assert!(started.elapsed() < SEND_GRACE + Duration::from_secs(3));
let error = child.send(&serde_json::json!({"n": 1})).await.unwrap_err();
assert!(
error.message.contains("input is closed"),
"{}",
error.message
);
child.close().await;
}