use std::time::Duration;
use futures_util::StreamExt;
use libguix::{Guix, ProgressEvent};
async fn drain_with_timeout(op: &mut libguix::Operation, total: Duration) -> Vec<ProgressEvent> {
let deadline = tokio::time::Instant::now() + total;
let mut out = Vec::new();
loop {
let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
if remaining.is_zero() {
break;
}
match tokio::time::timeout(remaining, op.events_mut().next()).await {
Ok(Some(batch)) => out.extend(batch),
Ok(None) => break,
Err(_) => break,
}
}
out
}
#[tokio::test(flavor = "multi_thread")]
async fn pull_repl_spawns_against_real_guix() {
let g = match Guix::discover().await {
Ok(g) => g,
Err(e) => {
eprintln!("skipping: `guix` not available: {e}");
return;
}
};
let mut op = match g.pull().user() {
Ok(op) => op,
Err(e) => {
eprintln!("skipping: pull_repl spawn failed: {e}");
return;
}
};
let cancel = op.take_cancel().expect("cancel handle");
let events = drain_with_timeout(&mut op, Duration::from_secs(3)).await;
let _ = cancel.cancel().await;
let mut tail = Vec::new();
while let Some(batch) = op.events_mut().next().await {
tail.extend(batch);
}
let mut all = events;
all.extend(tail);
eprintln!("pull_repl produced {} events", all.len());
for e in all.iter().take(5) {
eprintln!(" - {e:?}");
}
assert!(
matches!(all.last(), Some(ProgressEvent::ExitSummary { .. })),
"expected ExitSummary as final event, got {:?}",
all.last()
);
}
#[tokio::test(flavor = "multi_thread")]
async fn pull_repl_fd3_pipe_carries_events() {
let dir = tempfile::tempdir().expect("tempdir");
let bin_dir = dir.path().join("bin");
std::fs::create_dir_all(&bin_dir).expect("mkdir bin");
let guix_path = bin_dir.join("guix");
let script = r#"#!/bin/sh
if [ "$1" = "--version" ]; then
echo "guix (Guix) 9999-01-01.00"
exit 0
fi
cat > /dev/null
{
printf '(build-started "/gnu/store/abc-foo.drv" "-" "x86_64-linux" "")\n'
printf '(download-started "/gnu/store/xyz" "https://ci/x" "12345")\n'
printf '(substituter-started "/gnu/store/xyz" "substitute")\n'
printf '(build-succeeded "/gnu/store/abc-foo.drv")\n'
} >&3
exit 0
"#;
std::fs::write(&guix_path, script).expect("rewrite");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(&guix_path).unwrap().permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&guix_path, perms).expect("chmod");
}
let old_profile = std::env::var_os("GUIX_PROFILE");
std::env::set_var("GUIX_PROFILE", dir.path());
let g = match Guix::discover().await {
Ok(g) => g,
Err(e) => {
if let Some(p) = old_profile {
std::env::set_var("GUIX_PROFILE", p);
} else {
std::env::remove_var("GUIX_PROFILE");
}
panic!("fake-guix discover failed: {e}");
}
};
assert_eq!(
g.binary(),
guix_path.as_path(),
"discover should resolve our fake guix"
);
let mut op = g.pull().user().expect("pull().user()");
let drain_fut = async {
let mut out = Vec::new();
while let Some(batch) = op.events_mut().next().await {
out.extend(batch);
}
out
};
let events = tokio::time::timeout(Duration::from_secs(15), drain_fut)
.await
.expect("drain timed out");
if let Some(p) = old_profile {
std::env::set_var("GUIX_PROFILE", p);
} else {
std::env::remove_var("GUIX_PROFILE");
}
for e in &events {
eprintln!("event: {e:?}");
}
let kinds: Vec<&'static str> = events
.iter()
.map(|e| match e {
ProgressEvent::BuildStart { .. } => "BuildStart",
ProgressEvent::BuildDone { .. } => "BuildDone",
ProgressEvent::BuildFailed { .. } => "BuildFailed",
ProgressEvent::SubstituteDownload { .. } => "SubstituteDownload",
ProgressEvent::Line { .. } => "Line",
ProgressEvent::ExitSummary { .. } => "ExitSummary",
_ => "Other",
})
.collect();
eprintln!("got event kinds: {kinds:?}");
assert!(
kinds.contains(&"BuildStart"),
"expected BuildStart, kinds={kinds:?}"
);
assert!(
kinds.contains(&"SubstituteDownload"),
"expected SubstituteDownload, kinds={kinds:?}"
);
assert!(
kinds.contains(&"BuildDone"),
"expected BuildDone, kinds={kinds:?}"
);
assert!(
kinds.contains(&"ExitSummary"),
"expected ExitSummary, kinds={kinds:?}"
);
}