mod common;
use std::time::Duration;
use common::seat_block::{
SeatBlock, task_ended, task_failed, wait_for_completion, wait_while_running,
};
use common::{Judges, fixture, home_lock};
use magi::daemon::{self, Opts, Stop};
use magi::queue::{Queue, Source, Task, TaskStatus};
fn write_config(dir: &std::path::Path, config: &magi::config::Config) -> std::path::PathBuf {
let path = dir.join("magi.toml");
std::fs::write(&path, toml::to_string(config).expect("serialize config")).unwrap();
path
}
fn daemon_config(fx: &common::Fixture) -> magi::config::Config {
let mut config = fx.config.clone();
config.graph.implementers = 1;
config.graph.reviewers = 1;
config.disk.min_free_bytes = 0;
config.disk.auto_fold = false;
config.disk.cache_limit_bytes = 0;
config
}
common::e2e! {
async fn an_urgent_task_starts_alongside_an_already_running_task() {
let home = home_lock().await;
let mut fx = fixture(home, Judges::Unanimous, false);
let block = SeatBlock::install(&mut fx, "impl-A", "implement");
let config = daemon_config(&fx);
let config_path = write_config(fx.tmp.path(), &config);
let queue = Queue::open();
let mut ordinary = Task::new(
"ordinary task".to_owned(),
"create note.txt".to_owned(),
fx.repo.clone(),
Source::Human,
);
queue.put(&mut ordinary).expect("file the ordinary task");
let stop = Stop::new();
let opts = Opts {
repo: fx.repo.clone(),
config: Some(config_path),
once: false,
poll: Duration::from_millis(20),
worktrees_root: Some(fx.tmp.path().join("wt")),
..Opts::default()
};
let mut serve = tokio::spawn(daemon::serve_until(opts, stop.clone()));
block
.started(&mut serve, || task_ended(&queue, &ordinary.id))
.await;
assert_eq!(
queue.get(&ordinary.id).unwrap().status,
TaskStatus::Running,
"the ordinary task is claimed and running before the urgent task exists"
);
let mut urgent = Task::new(
"urgent task".to_owned(),
"create note.txt".to_owned(),
fx.repo.clone(),
Source::Human,
);
urgent.urgent = true;
queue.put(&mut urgent).expect("file the urgent task");
wait_while_running(
&mut serve,
|| {
queue
.get(&urgent.id)
.is_ok_and(|t| t.status == TaskStatus::Running)
},
|| task_ended(&queue, &ordinary.id).or_else(|| task_ended(&queue, &urgent.id)),
"the urgent task never joined the blocked ordinary run in flight",
)
.await;
assert_eq!(
queue.get(&ordinary.id).unwrap().status,
TaskStatus::Running,
"the existing run was not touched, let alone restarted, by the urgent dispatch"
);
assert_eq!(queue.get(&ordinary.id).unwrap().runs.len(), 1);
block.release();
wait_for_completion(
&mut serve,
|| {
queue
.get(&ordinary.id)
.is_ok_and(|t| t.status == TaskStatus::Done)
&& queue
.get(&urgent.id)
.is_ok_and(|t| t.status == TaskStatus::Done)
},
|| task_failed(&queue, &ordinary.id).or_else(|| task_failed(&queue, &urgent.id)),
"both tasks never reached a terminal status",
)
.await;
stop.stop();
tokio::time::timeout(Duration::from_secs(60), serve)
.await
.expect("the daemon loop did not stop in time")
.expect("the daemon task panicked")
.expect("the daemon loop returned an error");
let after_ordinary = queue.get(&ordinary.id).unwrap();
assert_eq!(
after_ordinary.status,
TaskStatus::Done,
"{after_ordinary:?}"
);
assert_eq!(
after_ordinary.runs.len(),
1,
"the ordinary run was never recompeted, only ever the one run: {after_ordinary:?}"
);
let after_urgent = queue.get(&urgent.id).unwrap();
assert_eq!(after_urgent.status, TaskStatus::Done, "{after_urgent:?}");
assert_eq!(after_urgent.runs.len(), 1);
}
}
common::e2e! {
async fn an_ordinary_task_still_waits_for_the_ordinary_slot_to_free() {
let home = home_lock().await;
let mut fx = fixture(home, Judges::Unanimous, false);
let block = SeatBlock::install(&mut fx, "impl-A", "implement");
let config = daemon_config(&fx);
let config_path = write_config(fx.tmp.path(), &config);
let queue = Queue::open();
let mut ordinary = Task::new(
"ordinary task".to_owned(),
"create note.txt".to_owned(),
fx.repo.clone(),
Source::Human,
);
queue.put(&mut ordinary).expect("file the ordinary task");
let stop = Stop::new();
let opts = Opts {
repo: fx.repo.clone(),
config: Some(config_path),
once: false,
poll: Duration::from_millis(20),
worktrees_root: Some(fx.tmp.path().join("wt")),
..Opts::default()
};
let mut serve = tokio::spawn(daemon::serve_until(opts, stop.clone()));
block
.started(&mut serve, || task_ended(&queue, &ordinary.id))
.await;
let mut plain = Task::new(
"second ordinary task".to_owned(),
"create note.txt".to_owned(),
fx.repo.clone(),
Source::Human,
);
queue.put(&mut plain).expect("file the second task");
for _ in 0..10 {
tokio::select! {
biased;
out = &mut serve => {
panic!("daemon exited while checking plain task stays queued: {out:?}");
}
_ = tokio::time::sleep(Duration::from_millis(20)) => {}
}
if let Some(reason) =
task_ended(&queue, &ordinary.id).or_else(|| task_ended(&queue, &plain.id))
{
panic!("task settled while ordinary run was still blocked: {reason}");
}
assert_eq!(
queue.get(&plain.id).unwrap().status,
TaskStatus::Queued,
"an ordinary task must not dispatch while the one ordinary slot \
is already spent on the blocked task"
);
}
block.release();
wait_for_completion(
&mut serve,
|| {
queue
.get(&ordinary.id)
.is_ok_and(|t| t.status == TaskStatus::Done)
},
|| task_failed(&queue, &ordinary.id).or_else(|| task_failed(&queue, &plain.id)),
"the ordinary task never finished",
)
.await;
wait_for_completion(
&mut serve,
|| {
queue
.get(&plain.id)
.is_ok_and(|t| t.status == TaskStatus::Done)
},
|| task_failed(&queue, &plain.id),
"the second ordinary task never started once the slot freed",
)
.await;
stop.stop();
tokio::time::timeout(Duration::from_secs(60), serve)
.await
.expect("the daemon loop did not stop in time")
.expect("the daemon task panicked")
.expect("the daemon loop returned an error");
let after_ordinary = queue.get(&ordinary.id).unwrap();
assert_eq!(
after_ordinary.status,
TaskStatus::Done,
"{after_ordinary:?}"
);
let after_plain = queue.get(&plain.id).unwrap();
assert_eq!(after_plain.status, TaskStatus::Done, "{after_plain:?}");
}
}