use std::collections::VecDeque;
use crate::docker::{ComposeAction, ComposeOp};
const MAX_LINES: usize = 5000;
pub struct Job {
pub id: u64,
pub title: String,
pub project: String,
pub lines: VecDeque<String>,
pub ended: Option<Option<String>>,
}
impl Job {
fn new(id: u64, title: String, project: String) -> Self {
Self {
id,
title,
project,
lines: VecDeque::new(),
ended: None,
}
}
}
#[derive(Default)]
pub struct ComposeJobs {
pub run: Option<Job>,
pub logs: Option<Job>,
}
impl ComposeJobs {
pub fn requested(&mut self, op: &ComposeOp) -> Option<u64> {
match op {
ComposeOp::Run {
job,
target,
action,
} => {
let project = target.label();
let title = match action {
ComposeAction::Up => format!("Starting {project}"),
ComposeAction::Rebuild(Some(service)) => {
format!("Rebuilding {service} in {project}")
}
ComposeAction::Rebuild(None) => format!("Rebuilding {project}"),
ComposeAction::PullRecreate => format!("Pulling and recreating {project}"),
};
self.run = Some(Job::new(*job, title, project));
None
}
ComposeOp::Logs { job, project } => {
let old = self.logs.take().filter(|j| j.ended.is_none()).map(|j| j.id);
self.logs = Some(Job::new(
*job,
format!("Logs of {project}"),
project.clone(),
));
old
}
ComposeOp::Cancel { job } => {
if self.logs.as_ref().is_some_and(|j| j.id == *job) {
self.logs = None;
}
None
}
}
}
fn job(&mut self, id: u64) -> Option<&mut Job> {
[self.run.as_mut(), self.logs.as_mut()]
.into_iter()
.flatten()
.find(|j| j.id == id)
}
pub fn output(&mut self, id: u64, lines: Vec<String>) {
if let Some(job) = self.job(id) {
job.lines.extend(lines);
while job.lines.len() > MAX_LINES {
job.lines.pop_front();
}
}
}
pub fn done(&mut self, id: u64, error: Option<String>) -> Option<(String, bool)> {
let is_run = self.run.as_ref().is_some_and(|j| j.id == id);
let job = self.job(id)?;
let notice = is_run.then(|| match &error {
None => (format!("{}: done", job.title), false),
Some(e) => (format!("{}: {e}", job.title), true),
});
job.ended = Some(error);
notice
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::docker::ComposeTarget;
#[test]
fn follows_jobs() {
let mut jobs = ComposeJobs::default();
let target = ComposeTarget {
name: Some("shop".into()),
files: Vec::new(),
dir: Default::default(),
};
jobs.requested(&ComposeOp::Run {
job: 1,
target,
action: ComposeAction::Rebuild(Some("web".into())),
});
jobs.output(1, vec!["building".into()]);
assert_eq!(jobs.run.as_ref().unwrap().title, "Rebuilding web in shop");
assert_eq!(
jobs.done(1, None),
Some(("Rebuilding web in shop: done".into(), false))
);
let logs = |job| ComposeOp::Logs {
job,
project: "shop".into(),
};
assert_eq!(jobs.requested(&logs(2)), None);
assert_eq!(jobs.requested(&logs(3)), Some(2));
assert_eq!(jobs.done(3, None), None, "logs ending isn't news");
}
}