use std::collections::HashMap;
use std::fmt;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use anyhow::Result;
use hm_plugin_protocol::{BuildEvent, ExecutorInput, StepResult};
use tokio_util::sync::CancellationToken;
use crate::orchestrator::archive::ArchiveStore;
use crate::orchestrator::docker_client::DockerClient;
use crate::orchestrator::events::EventBus;
pub mod docker;
#[derive(Clone, Debug)]
pub struct RunContext {
pub docker: DockerClient,
pub event_bus: Arc<EventBus>,
pub archives: Arc<ArchiveStore>,
pub cancel: CancellationToken,
}
pub trait StepRunner: Send + Sync + fmt::Debug {
fn name(&self) -> &str;
fn execute(
&self,
ctx: &RunContext,
input: ExecutorInput,
) -> Pin<Box<dyn Future<Output = Result<StepResult>> + Send + '_>>;
}
pub trait OutputRenderer: Send + fmt::Debug {
fn on_event(&mut self, event: &BuildEvent);
}
#[derive(Default)]
pub struct RunnerRegistry {
runners: HashMap<String, Arc<dyn StepRunner>>,
default: Option<String>,
}
impl RunnerRegistry {
#[must_use]
pub fn new() -> Self {
Self {
runners: HashMap::new(),
default: None,
}
}
pub fn register(&mut self, runner: Arc<dyn StepRunner>, is_default: bool) {
let name = runner.name().to_owned();
if is_default {
self.default = Some(name.clone());
}
self.runners.insert(name, runner);
}
#[must_use]
pub fn resolve(&self, name: Option<&str>) -> Option<Arc<dyn StepRunner>> {
let key = name.or(self.default.as_deref())?;
self.runners.get(key).cloned()
}
#[must_use]
pub fn default_runner_name(&self) -> Option<&str> {
self.default.as_deref()
}
#[must_use]
pub fn runner_names(&self) -> Vec<&str> {
let mut names: Vec<&str> = self.runners.keys().map(String::as_str).collect();
names.sort_unstable();
names
}
}
impl fmt::Debug for RunnerRegistry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("RunnerRegistry")
.field("runners", &self.runners.keys().collect::<Vec<_>>())
.field("default", &self.default)
.finish()
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
#[derive(Debug)]
struct StubRunner {
runner_name: String,
}
impl StubRunner {
fn new(name: &str) -> Self {
Self {
runner_name: name.to_owned(),
}
}
}
impl StepRunner for StubRunner {
fn name(&self) -> &str {
&self.runner_name
}
fn execute(
&self,
_ctx: &RunContext,
_input: ExecutorInput,
) -> Pin<Box<dyn Future<Output = Result<StepResult>> + Send + '_>> {
Box::pin(async {
Ok(StepResult {
exit_code: 0,
committed_snapshot: None,
artifacts: vec![],
})
})
}
}
#[test]
fn resolve_by_name() {
let mut reg = RunnerRegistry::new();
reg.register(Arc::new(StubRunner::new("docker")), false);
reg.register(Arc::new(StubRunner::new("local")), false);
let runner = reg.resolve(Some("docker")).unwrap();
assert_eq!(runner.name(), "docker");
let runner = reg.resolve(Some("local")).unwrap();
assert_eq!(runner.name(), "local");
assert!(reg.resolve(Some("nope")).is_none());
}
#[test]
fn resolve_default() {
let mut reg = RunnerRegistry::new();
reg.register(Arc::new(StubRunner::new("docker")), true);
reg.register(Arc::new(StubRunner::new("local")), false);
let runner = reg.resolve(None).unwrap();
assert_eq!(runner.name(), "docker");
assert_eq!(reg.default_runner_name(), Some("docker"));
}
#[test]
fn no_default_returns_none() {
let mut reg = RunnerRegistry::new();
reg.register(Arc::new(StubRunner::new("docker")), false);
assert!(reg.resolve(None).is_none());
assert!(reg.default_runner_name().is_none());
}
#[test]
fn runner_names_sorted() {
let mut reg = RunnerRegistry::new();
reg.register(Arc::new(StubRunner::new("zeta")), false);
reg.register(Arc::new(StubRunner::new("alpha")), false);
reg.register(Arc::new(StubRunner::new("mid")), false);
assert_eq!(reg.runner_names(), vec!["alpha", "mid", "zeta"]);
}
#[test]
fn debug_impl() {
let reg = RunnerRegistry::new();
let _ = format!("{reg:?}");
}
}