use std::time::Duration;
use libguix::Guix;
#[tokio::test]
async fn interrupt_cancels_in_flight_eval_and_repl_survives() {
let g = match Guix::discover().await {
Ok(g) => g,
Err(e) => {
tracing::warn!("skipping: `guix` not available: {e}");
return;
}
};
let g = g.with_repl_timeout(Duration::from_secs(60));
let repl = g.repl().await.expect("repl spawn");
let _ = repl.eval("(+ 1 1)").await.expect("warmup eval");
let repl_for_slow = repl.clone();
let slow = tokio::spawn(async move {
repl_for_slow
.eval("(let loop ((i 0)) (loop (+ i 1)))")
.await
});
tokio::time::sleep(Duration::from_millis(200)).await;
repl.interrupt().expect("kill should succeed");
let res = tokio::time::timeout(Duration::from_secs(5), slow)
.await
.expect("slow eval did not return within 5s of SIGINT")
.expect("slow eval task panicked");
assert!(
res.is_err(),
"slow eval should have returned Err after interrupt, got Ok: {res:?}"
);
let v = repl.eval("(+ 1 2)").await.expect("follow-up eval");
assert_eq!(v.to_string(), "3");
}
#[tokio::test]
async fn interrupt_during_idle_is_safe() {
let g = match Guix::discover().await {
Ok(g) => g,
Err(e) => {
tracing::warn!("skipping: `guix` not available: {e}");
return;
}
};
let g = g.with_repl_timeout(Duration::from_secs(30));
let repl = g.repl().await.expect("repl spawn");
let _ = repl.eval("(+ 1 1)").await.expect("warmup eval");
repl.interrupt().expect("idle interrupt should succeed");
tokio::time::sleep(Duration::from_millis(100)).await;
let v = repl
.eval("(+ 1 2)")
.await
.expect("post-idle-interrupt eval");
assert_eq!(v.to_string(), "3");
}
#[tokio::test]
async fn interrupt_repeatedly_during_idle_is_safe() {
let g = match Guix::discover().await {
Ok(g) => g,
Err(e) => {
tracing::warn!("skipping: `guix` not available: {e}");
return;
}
};
let g = g.with_repl_timeout(Duration::from_secs(30));
let repl = g.repl().await.expect("repl spawn");
let _ = repl.eval("(+ 1 1)").await.expect("warmup eval");
for _ in 0..10 {
repl.interrupt().expect("idle interrupt should succeed");
}
tokio::time::sleep(Duration::from_millis(100)).await;
let v = repl.eval("(+ 5 6)").await.expect("follow-up eval");
assert_eq!(v.to_string(), "11");
}
#[tokio::test]
async fn warmup_loads_submodules_so_fold_packages_is_safe_to_interrupt() {
let g = match Guix::discover().await {
Ok(g) => g,
Err(e) => {
tracing::warn!("skipping: `guix` not available: {e}");
return;
}
};
let g = g.with_repl_timeout(Duration::from_secs(90));
let repl = g.repl().await.expect("repl spawn");
repl.warmup().await.expect("warmup should succeed");
let repl_for_slow = repl.clone();
let slow = tokio::spawn(async move {
repl_for_slow
.eval_with_modules(
&["(gnu packages)", "(guix packages)"],
"(fold-packages
(lambda (_ acc)
(let loop ((i 0))
(if (< i 100000) (loop (+ i 1)) acc)))
0)",
)
.await
});
tokio::time::sleep(Duration::from_millis(200)).await;
repl.interrupt().expect("kill should succeed");
let res = tokio::time::timeout(Duration::from_secs(10), slow)
.await
.expect("slow fold-packages did not return within 10s of SIGINT")
.expect("slow eval task panicked");
assert!(
res.is_err(),
"slow fold-packages should have returned Err after interrupt, got Ok: {res:?}"
);
let v = repl.eval("(+ 1 2)").await.expect("post-interrupt eval");
assert_eq!(v.to_string(), "3");
let count = repl
.eval_with_modules(
&["(gnu packages)", "(guix packages)"],
"(fold-packages (lambda (_ acc) (+ acc 1)) 0)",
)
.await
.expect("post-interrupt fold-packages should succeed");
let n: i64 = count
.to_string()
.parse()
.unwrap_or_else(|_| panic!("expected integer package count, got {count:?}"));
assert!(
n > 1000,
"expected a healthy package count (>1000), got {n} — module cache likely corrupted"
);
}