summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLars Wirzenius <liw@liw.fi>2024-02-02 17:32:52 +0200
committerLars Wirzenius <liw@liw.fi>2024-02-02 17:42:38 +0200
commit7c562e1cbf5e356bb62c5f1eccc57bb88c44e7c5 (patch)
tree530bc7b03393bbf38b77c4f582fada416bbc890f
parent1c90e35cef8d04745536199ec37d81b7b9597a73 (diff)
downloadradicle-ci-broker-7c562e1cbf5e356bb62c5f1eccc57bb88c44e7c5.tar.gz
feat(src/broker.rs): capture CI broker business logic
Implement a type to capture the business logic of the CI broker in a way that is reasonable to test in isolation rather than as part of a full Radicle network. Signed-off-by: Lars Wirzenius <liw@liw.fi>
-rw-r--r--src/broker.rs166
1 files changed, 166 insertions, 0 deletions
diff --git a/src/broker.rs b/src/broker.rs
new file mode 100644
index 0000000..5058a7f
--- /dev/null
+++ b/src/broker.rs
@@ -0,0 +1,166 @@
+//! The business logic of the CI broker.
+//!
+//! This is type and module of its own to facilitate automated
+//! testing.
+
+use std::collections::HashMap;
+
+use radicle::identity::RepoId;
+
+use crate::{adapter::Adapter, error::BrokerError, msg::Request, run::Run};
+
+/// A CI broker.
+///
+/// The broker gets repository change events from the local Radicle
+/// node, and executes the appropriate adapter to run CI on the
+/// repository.
+#[derive(Debug, Default)]
+pub struct Broker {
+ default_adapter: Option<Adapter>,
+ adapters: HashMap<RepoId, Adapter>,
+}
+
+impl Broker {
+ pub fn set_default_adapter(&mut self, adapter: &Adapter) {
+ self.default_adapter = Some(adapter.clone());
+ }
+
+ pub fn default_adapter(&self) -> Option<&Adapter> {
+ self.default_adapter.as_ref()
+ }
+
+ pub fn set_repository_adapter(&mut self, rid: &RepoId, adapter: &Adapter) {
+ self.adapters.insert(*rid, adapter.clone());
+ }
+
+ pub fn adapter(&self, rid: &RepoId) -> Option<&Adapter> {
+ self.adapters.get(rid).or(self.default_adapter.as_ref())
+ }
+
+ #[allow(clippy::result_large_err)]
+ pub fn execute_ci(&self, trigger: &Request) -> Result<Run, BrokerError> {
+ let adapter = match trigger {
+ Request::Trigger {
+ common,
+ push: _,
+ patch: _,
+ } => {
+ let rid = &common.repository.id;
+ if let Some(adapter) = self.adapter(rid) {
+ adapter
+ } else {
+ return Err(BrokerError::NoAdapter(*rid));
+ }
+ }
+ };
+
+ let mut run = Run::default();
+ adapter.run(trigger, &mut run)?;
+
+ Ok(run)
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use tempfile::tempdir;
+
+ use super::{Adapter, Broker, RepoId};
+ use crate::{
+ msg::{RunId, RunResult},
+ run::RunState,
+ test::{mock_adapter, trigger_request, TestResult},
+ };
+
+ fn rid() -> RepoId {
+ const RID: &str = "rad:zwTxygwuz5LDGBq255RA2CbNGrz8";
+ RepoId::from_urn(RID).unwrap()
+ }
+
+ fn rid2() -> RepoId {
+ const RID: &str = "rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5";
+ RepoId::from_urn(RID).unwrap()
+ }
+
+ #[test]
+ fn has_no_adapters_initially() -> TestResult<()> {
+ let broker = Broker::default();
+ let rid = rid();
+ assert_eq!(broker.adapter(&rid), None);
+ Ok(())
+ }
+
+ #[test]
+ fn adds_adapter() -> TestResult<()> {
+ let mut broker = Broker::default();
+ let adapter = Adapter::default();
+ let rid = rid();
+ broker.set_repository_adapter(&rid, &adapter);
+ assert_eq!(broker.adapter(&rid), Some(&adapter));
+ Ok(())
+ }
+
+ #[test]
+ fn does_not_find_unknown_repo() -> TestResult<()> {
+ let mut broker = Broker::default();
+ let adapter = Adapter::default();
+ let rid = rid();
+ let rid2 = rid2();
+ broker.set_repository_adapter(&rid, &adapter);
+ assert_eq!(broker.adapter(&rid2), None);
+ Ok(())
+ }
+
+ #[test]
+ fn does_not_have_a_default_adapter_initially() -> TestResult<()> {
+ let broker = Broker::default();
+ assert_eq!(broker.default_adapter(), None);
+ Ok(())
+ }
+
+ #[test]
+ fn sets_a_default_adapter_initially() -> TestResult<()> {
+ let mut broker = Broker::default();
+ let adapter = Adapter::default();
+ broker.set_default_adapter(&adapter);
+ assert_eq!(broker.default_adapter(), Some(&adapter));
+ Ok(())
+ }
+
+ #[test]
+ fn finds_default_adapter_for_unknown_repo() -> TestResult<()> {
+ let mut broker = Broker::default();
+ let adapter = Adapter::default();
+ broker.set_default_adapter(&adapter);
+
+ let rid = rid();
+ assert_eq!(broker.adapter(&rid), Some(&adapter));
+ Ok(())
+ }
+
+ #[test]
+ fn exectues_adapter() -> TestResult<()> {
+ const ADAPTER: &str = r#"#!/bin/bash
+echo '{"response":"triggered","run_id":{"id":"xyzzy"}}'
+echo '{"response":"finished","result":"success"}'
+"#;
+
+ let tmp = tempdir()?;
+ let bin = tmp.path().join("adapter.sh");
+ let adapter = mock_adapter(&bin, ADAPTER)?;
+
+ let mut broker = Broker::default();
+ broker.set_default_adapter(&adapter);
+
+ let trigger = trigger_request()?;
+
+ let x = broker.execute_ci(&trigger);
+ assert!(x.is_ok());
+ let run = x.unwrap();
+ assert_eq!(run.adapter_run_id(), Some(&RunId::from("xyzzy")));
+ assert_eq!(run.state(), RunState::Finished);
+ assert_eq!(run.result(), Some(&RunResult::Success));
+
+ Ok(())
+ }
+}