feat: switch to kameo actor system (dropping actix)
Some checks failed
ci/woodpecker/push/cron-docker-builder Pipeline was successful
Rust / build (map[name:stable]) (push) Failing after 2m41s
Rust / build (map[name:nightly]) (push) Failing after 2m48s
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline failed

This commit is contained in:
Paul Campbell 2024-11-22 16:12:44 +00:00
parent d8c2e9a23f
commit a190721ae7
80 changed files with 2357 additions and 1700 deletions

118
Cargo.lock generated
View file

@ -2,63 +2,6 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3 version = 3
[[package]]
name = "actix"
version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de7fa236829ba0841304542f7614c42b80fca007455315c45c785ccfa873a85b"
dependencies = [
"actix-macros",
"actix-rt",
"actix_derive",
"bitflags 2.6.0",
"bytes",
"crossbeam-channel",
"futures-core",
"futures-sink",
"futures-task",
"futures-util",
"log",
"once_cell",
"parking_lot",
"pin-project-lite",
"smallvec",
"tokio",
"tokio-util",
]
[[package]]
name = "actix-macros"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb"
dependencies = [
"quote",
"syn",
]
[[package]]
name = "actix-rt"
version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24eda4e2a6e042aa4e55ac438a2ae052d3b5da0ecf83d7411e1a368946925208"
dependencies = [
"actix-macros",
"futures-core",
"tokio",
]
[[package]]
name = "actix_derive"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6ac1e58cded18cb28ddc17143c4dea5345b3ad575e14f32f66e4054a56eb271"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "addr2line" name = "addr2line"
version = "0.21.0" version = "0.21.0"
@ -807,6 +750,12 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
[[package]]
name = "dyn-clone"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125"
[[package]] [[package]]
name = "either" name = "either"
version = "1.13.0" version = "1.13.0"
@ -1123,8 +1072,6 @@ dependencies = [
name = "git-next" name = "git-next"
version = "0.13.11" version = "0.13.11"
dependencies = [ dependencies = [
"actix",
"actix-rt",
"anyhow", "anyhow",
"assert2", "assert2",
"bon", "bon",
@ -1139,6 +1086,7 @@ dependencies = [
"git-next-core", "git-next-core",
"git-next-forge-forgejo", "git-next-forge-forgejo",
"git-next-forge-github", "git-next-forge-github",
"kameo",
"kxio", "kxio",
"lazy_static", "lazy_static",
"lettre", "lettre",
@ -1171,7 +1119,6 @@ dependencies = [
name = "git-next-core" name = "git-next-core"
version = "0.13.11" version = "0.13.11"
dependencies = [ dependencies = [
"actix",
"assert2", "assert2",
"async-trait", "async-trait",
"derive-with", "derive-with",
@ -2747,6 +2694,36 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "kameo"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62237a96597618543798a36ec723eb75c5ac301e2690243fd600be1f5eb3dd2d"
dependencies = [
"dyn-clone",
"futures",
"itertools",
"kameo_macros",
"once_cell",
"serde",
"tokio",
"tokio-stream",
"tracing",
]
[[package]]
name = "kameo_macros"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bbbd8e8d7b02bc67eae0dcbdb82c0a71cc7cc61734059ee3e7439a1ee1e0e85"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
"uuid",
]
[[package]] [[package]]
name = "kqueue" name = "kqueue"
version = "1.0.8" version = "1.0.8"
@ -4354,6 +4331,7 @@ dependencies = [
"signal-hook-registry", "signal-hook-registry",
"socket2", "socket2",
"tokio-macros", "tokio-macros",
"tracing",
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
@ -4389,6 +4367,17 @@ dependencies = [
"tokio", "tokio",
] ]
[[package]]
name = "tokio-stream"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1"
dependencies = [
"futures-core",
"pin-project-lite",
"tokio",
]
[[package]] [[package]]
name = "tokio-tungstenite" name = "tokio-tungstenite"
version = "0.21.0" version = "0.21.0"
@ -4691,6 +4680,15 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a"
dependencies = [
"getrandom 0.2.15",
]
[[package]] [[package]]
name = "valuable" name = "valuable"
version = "0.1.0" version = "0.1.0"

View file

@ -101,9 +101,8 @@ take-until = "0.2"
notify = "7.0" notify = "7.0"
# Actors # Actors
actix = "0.13" kameo = "0.13"
actix-rt = "2.9" tokio = { version = "1.37", features = ["full"] }
tokio = { version = "1.37", features = ["rt", "macros"] }
# email # email
lettre = "0.11" lettre = "0.11"

View file

@ -50,8 +50,7 @@ git-conventional = { workspace = true }
toml = { workspace = true } toml = { workspace = true }
# Actors # Actors
actix = { workspace = true } kameo = { workspace = true }
actix-rt = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true }
# boilerplate # boilerplate

View file

@ -655,6 +655,18 @@ stateDiagram-v2
forge_github --> core forge_github --> core
``` ```
## Actor Supervision Tree
```mermaid
mindmap
Root
Alerts
FileWatcher
Server
Repo 1
Repo 2
```
## License ## License
`git-next` is released under the [MIT License](./LICENSE). `git-next` is released under the [MIT License](./LICENSE).

View file

@ -1,19 +1,22 @@
// //
use actix::prelude::*; use kameo::message::{Context, Message};
use tracing::debug;
use tracing::{info, Instrument as _};
use crate::alerts::{ use crate::alerts::{
desktop::send_desktop_notification, email::send_email, messages::NotifyUser, desktop::send_desktop_notification, email::send_email, messages::NotifyUser,
webhook::send_webhook, AlertsActor, webhook::send_webhook, AlertsActor,
}; };
impl Handler<NotifyUser> for AlertsActor { impl Message<NotifyUser> for AlertsActor {
type Result = (); type Reply = ();
fn handle(&mut self, msg: NotifyUser, ctx: &mut Self::Context) -> Self::Result { async fn handle(
&mut self,
msg: NotifyUser,
_ctx: Context<'_, Self, Self::Reply>,
) -> Self::Reply {
let Some(shout) = &self.shout else { let Some(shout) = &self.shout else {
info!("No shout config available"); debug!("No shout config available");
return; return;
}; };
let net = self.net.clone(); let net = self.net.clone();
@ -21,7 +24,6 @@ impl Handler<NotifyUser> for AlertsActor {
let Some(user_notification) = self.history.sendable(msg.peel()) else { let Some(user_notification) = self.history.sendable(msg.peel()) else {
return; return;
}; };
async move {
if let Some(webhook_config) = shout.webhook() { if let Some(webhook_config) = shout.webhook() {
send_webhook(&user_notification, webhook_config, &net).await; send_webhook(&user_notification, webhook_config, &net).await;
} }
@ -34,8 +36,4 @@ impl Handler<NotifyUser> for AlertsActor {
} }
} }
} }
.in_current_span()
.into_actor(self)
.wait(ctx);
}
} }

View file

@ -1,12 +1,16 @@
// //
use actix::prelude::*; use kameo::message::{Context, Message};
use crate::alerts::{messages::UpdateShout, AlertsActor}; use crate::alerts::{messages::UpdateShout, AlertsActor};
impl Handler<UpdateShout> for AlertsActor { impl Message<UpdateShout> for AlertsActor {
type Result = (); type Reply = ();
fn handle(&mut self, msg: UpdateShout, _ctx: &mut Self::Context) -> Self::Result { async fn handle(
&mut self,
msg: UpdateShout,
_ctx: Context<'_, Self, Self::Reply>,
) -> Self::Reply {
self.shout.replace(msg.peel()); self.shout.replace(msg.peel());
} }
} }

View file

@ -1,11 +1,15 @@
// //
use actix::prelude::*;
use derive_more::derive::Constructor; use derive_more::derive::Constructor;
use git_next_core::{git::UserNotification, server::Shout}; use git_next_core::{git::UserNotification, server::Shout};
pub use history::History; pub use history::History;
use kameo::{mailbox::unbounded::UnboundedMailbox, Actor};
use crate::{
default_on_actor_link_died, default_on_actor_panic, default_on_actor_start,
default_on_actor_stop,
};
mod desktop; mod desktop;
mod email; mod email;
@ -24,9 +28,12 @@ pub struct AlertsActor {
history: History, // record of alerts sent recently (e.g. 24 hours) history: History, // record of alerts sent recently (e.g. 24 hours)
net: kxio::net::Net, net: kxio::net::Net,
} }
impl Actor for AlertsActor { impl Actor for AlertsActor {
type Context = Context<Self>; type Mailbox = UnboundedMailbox<Self>;
default_on_actor_start!(this, actor_ref);
default_on_actor_panic!(this, actor_ref, err);
default_on_actor_link_died!(this, actor_ref, id, reason);
default_on_actor_stop!(this, actor_ref, reason);
} }
fn short_message(user_notification: &UserNotification) -> String { fn short_message(user_notification: &UserNotification) -> String {

View file

@ -0,0 +1,19 @@
//
use kameo::{actor::ActorRef, Actor};
use crate::{spawn, spawn_in_thread};
/// Adds a spawn mathod to the actor.
pub trait BaseActor: Actor {
async fn spawn<Parent: Actor>(self, parent_actor_ref: ActorRef<Parent>) -> ActorRef<Self> {
spawn!(parent_actor_ref, self)
}
async fn spawn_in_thread<Parent: Actor>(
self,
parent_actor_ref: ActorRef<Parent>,
) -> ActorRef<Self> {
spawn_in_thread!(parent_actor_ref, self)
}
}
impl<T: Actor> BaseActor for T {}

View file

@ -1,50 +1,83 @@
// //
use actix::prelude::*; use std::{path::PathBuf, sync::mpsc::Receiver};
use actix::Recipient; use anyhow::Context;
use anyhow::{Context, Result}; use kameo::{mailbox::unbounded::UnboundedMailbox, message::Message, Actor};
use notify::{event::ModifyKind, Watcher}; use notify::{event::ModifyKind, RecommendedWatcher, Watcher};
use tracing::{error, info}; use tracing::{error, info};
use std::{ use git_next_core::message;
path::PathBuf,
sync::{ use crate::{
atomic::{AtomicBool, Ordering}, default_on_actor_link_died, default_on_actor_panic, default_on_actor_stop, on_actor_start,
Arc, publish, tell, MessageBus,
},
time::Duration,
}; };
#[derive(Debug, Message)] message!(
#[rtype(result = "()")] FileUpdated,
pub struct FileUpdated; "Notification that watched file has been updated"
);
#[derive(Debug, thiserror::Error)] message!(Watch, "Watch for the next event on the file");
pub enum Error {
#[error("io")] pub struct FileWatcherActor {
Io(#[from] std::io::Error), file_updates_bus: MessageBus<FileUpdated>,
file: PathBuf,
event_receiver: Option<Receiver<Result<notify::Event, notify::Error>>>,
watcher: Option<RecommendedWatcher>,
} }
pub fn watch_file(path: PathBuf, recipient: Recipient<FileUpdated>) -> Result<Arc<AtomicBool>> {
impl FileWatcherActor {
pub const fn new(file_updates_bus: MessageBus<FileUpdated>, file: PathBuf) -> Self {
Self {
file_updates_bus,
file,
event_receiver: None,
watcher: None,
}
}
}
impl Actor for FileWatcherActor {
type Mailbox = UnboundedMailbox<Self>;
on_actor_start!(this, actor_ref, {
//
let (tx, rx) = std::sync::mpsc::channel(); let (tx, rx) = std::sync::mpsc::channel();
let shutdown = Arc::new(AtomicBool::default()); this.event_receiver.replace(rx);
let mut handler = notify::recommended_watcher(tx).context("file watcher")?; let mut watcher = notify::recommended_watcher(tx).context("file watcher")?;
handler watcher
.watch(&path, notify::RecursiveMode::NonRecursive) .watch(&this.file, notify::RecursiveMode::NonRecursive)
.with_context(|| format!("Watching: {path:?}"))?; .with_context(|| format!("Watching: {:?}", this.file))?;
let thread_shutdown = shutdown.clone(); this.watcher.replace(watcher);
actix_rt::task::spawn_blocking(move || {
loop { tell!("file_watcher", actor_ref, Watch)?;
if thread_shutdown.load(Ordering::Relaxed) {
drop(handler); Ok(())
break; });
default_on_actor_panic!(this, actor_ref, err);
default_on_actor_link_died!(this, actor_ref, id, reason);
default_on_actor_stop!(this, actor_ref, reason);
} }
for result in rx.try_iter() {
impl Message<Watch> for FileWatcherActor {
type Reply = color_eyre::Result<()>;
async fn handle(
&mut self,
msg: Watch,
ctx: kameo::message::Context<'_, Self, Self::Reply>,
) -> Self::Reply {
if let Some(rx) = &self.event_receiver {
while let Ok(result) = rx.recv() {
match result { match result {
Ok(event) => match event.kind { Ok(event) => match event.kind {
notify::EventKind::Modify(ModifyKind::Data(_)) => { notify::EventKind::Modify(ModifyKind::Data(_)) => {
info!("File modified"); info!("===================================================================================");
recipient.do_send(FileUpdated); info!(?event, "File modified");
publish!("file_updates_bus", self.file_updates_bus, FileUpdated)?;
break; break;
} }
notify::EventKind::Modify(_) notify::EventKind::Modify(_)
@ -55,12 +88,12 @@ pub fn watch_file(path: PathBuf, recipient: Recipient<FileUpdated>) -> Result<Ar
| notify::EventKind::Other => { /* do nothing */ } | notify::EventKind::Other => { /* do nothing */ }
}, },
Err(err) => { Err(err) => {
error!(?err, "Watching file: {path:?}"); error!(?err, "Watching file: {:?}", self.file);
} }
} }
} }
std::thread::sleep(Duration::from_millis(1000));
} }
}); tell!("file_watcher", ctx.actor_ref(), msg)?;
Ok(shutdown) Ok(())
}
} }

View file

@ -0,0 +1,201 @@
//
/// Called when the actor starts, before it processes any messages.
///
/// Messages sent internally by the actor during `on_start` are prioritized and processed
/// before any externally sent messages, even if external messages are received first.
///
/// This ensures that the actor can properly initialize before handling external messages.
///
/// # Example
///
/// ```rust
/// impl Actor for ServerActor {
/// type Mailbox = UnboundedMailbox<Self>;
///
/// on_actor_start!(this, actor_ref, {
/// // handle start here
/// Ok(())
/// });
/// }
/// ```
#[macro_export]
macro_rules! on_actor_start {
($this:ident, $actor_ref:ident, $body:expr) => {
#[allow(unused_variables)]
#[tracing::instrument(skip_all)]
async fn on_start(
&mut self,
actor_ref: kameo::actor::ActorRef<Self>,
) -> std::result::Result<(), kameo::error::BoxError> {
tracing::debug!(?actor_ref, "{}", <Self as Actor>::name());
let $this = self;
let $actor_ref = actor_ref;
$body
}
};
}
#[macro_export]
macro_rules! default_on_actor_start {
($this:ident, $actor_ref:ident) => {
$crate::on_actor_start!($this, $actor_ref, { Ok(()) });
};
}
/// Called swhen the actor encounters a panic or an error during "tell" message handling.
///
/// This method gives the actor an opportunity to clean up or reset its state and determine
/// whether it should be stopped or continue processing messages.
///
/// # Parameters
/// - `err`: The panic or error that occurred.
///
/// # Returns
/// - `Some(ActorStopReason)`: Stops the actor.
/// - `None`: Allows the actor to continue processing messages.
///
/// # Example
///
/// ```rust
/// impl Actor for ServerActor {
/// type Mailbox = UnboundedMailbox<Self>;
///
/// on_actor_panic!(this, actor_ref, err, {
/// // handle panic here
/// Ok(())
/// });
/// }
/// ```
#[macro_export]
macro_rules! on_actor_panic {
($this:ident, $actor_ref:ident, $err:ident, $body:expr) => {
#[allow(unused_variables)]
#[tracing::instrument(skip_all)]
async fn on_panic(
&mut self,
actor_ref: kameo::actor::WeakActorRef<Self>,
err: kameo::error::PanicError,
) -> std::result::Result<Option<kameo::error::ActorStopReason>, kameo::error::BoxError> {
tracing::debug!(?actor_ref, %err, "{}", <Self as Actor>::name());
let $this = self;
let $actor_ref = actor_ref;
let $err = err;
$body
}
};
}
#[macro_export]
macro_rules! default_on_actor_panic {
($this:ident, $actor_ref:ident, $err:ident) => {
$crate::on_actor_panic!($this, $actor_ref, $err, {
Ok(Some(kameo::error::ActorStopReason::Panicked($err)))
});
};
}
/// Called when a linked actor dies.
///
/// By default, the actor will stop if the reason for the linked actor's death is anything other
/// than `Normal`. You can customize this behavior in the implementation.
///
/// # Returns
/// Whether the actor should stop or continue processing messages.
///
/// # Example
///
/// ```rust
/// impl Actor for ServerActor {
/// type Mailbox = UnboundedMailbox<Self>;
///
/// on_actor_link_died!(this, actor_ref, id, reason, {
/// // handle link death here
/// Ok(())
/// });
/// }
/// ```
#[macro_export]
macro_rules! on_actor_link_died {
($this:ident, $actor_ref:ident, $id:ident, $reason:ident, $body:expr) => {
#[allow(unused_variables)]
#[tracing::instrument(skip_all)]
async fn on_link_died(
&mut self,
actor_ref: kameo::actor::WeakActorRef<Self>,
id: kameo::actor::ActorID,
reason: kameo::error::ActorStopReason,
) -> std::result::Result<Option<kameo::error::ActorStopReason>, kameo::error::BoxError> {
tracing::debug!(?actor_ref, %id, %reason, "{}", <Self as Actor>::name());
let $this = self;
let $actor_ref = actor_ref;
let $id = id;
let $reason = reason;
$body
}
};
}
#[macro_export]
macro_rules! default_on_actor_link_died {
($this:ident, $actor_ref:ident, $id:ident, $reason:ident) => {
$crate::on_actor_link_died!($this, $actor_ref, $id, $reason, {
match &$reason {
kameo::error::ActorStopReason::Normal => Ok(None),
kameo::error::ActorStopReason::Killed
| kameo::error::ActorStopReason::Panicked(_)
| kameo::error::ActorStopReason::LinkDied { .. } => {
Ok(Some(kameo::error::ActorStopReason::LinkDied {
id: $id,
reason: Box::new($reason),
}))
}
}
});
};
}
/// Called before the actor stops.
///
/// This allows the actor to perform any necessary cleanup or release resources before being fully stopped.
///
/// # Parameters
/// - `reason`: The reason why the actor is being stopped.
///
/// # Example
///
/// ```rust
/// impl Actor for ServerActor {
/// type Mailbox = UnboundedMailbox<Self>;
///
/// on_actor_stop!(this, actor_ref, id, reason, {
/// // handle stop here
/// Ok(())
/// });
/// }
/// ```
#[macro_export]
macro_rules! on_actor_stop {
($this:ident, $actor_ref:ident, $reason:ident, $body:expr) => {
#[allow(unused_variables)]
#[tracing::instrument(skip_all)]
async fn on_stop(
&mut self,
actor_ref: kameo::actor::WeakActorRef<Self>,
reason: kameo::error::ActorStopReason,
) -> std::result::Result<(), kameo::error::BoxError> {
tracing::debug!(?actor_ref, %reason, "{}", <Self as Actor>::name());
let $this = self;
let $actor_ref = actor_ref;
let $reason = reason;
$body
}
};
}
#[macro_export]
macro_rules! default_on_actor_stop {
($this:ident, $actor_ref:ident, $reason:ident) => {
$crate::on_actor_stop!($this, $actor_ref, $reason, { Ok(()) });
};
}

View file

@ -0,0 +1,3 @@
mod actor;
mod send;
mod spawn;

View file

@ -0,0 +1,53 @@
//
#[macro_export]
macro_rules! tell {
($actor_ref:expr, $message:expr) => {
tell!(stringify!($actor_ref), $actor_ref, $message)
};
($actor_name:expr, $actor_ref:expr, $message:expr) => {{
tracing::debug!(actor = $actor_name, msg = stringify!($message), "send");
$actor_ref.tell($message).await
}};
}
#[macro_export]
macro_rules! subscribe {
($message_bus:expr, $actor_ref:expr) => {
subscribe!(
stringify!($message_bus),
$message_bus,
stringify!($actor_ref),
$actor_ref
)
};
($message_bus:expr, $actor_name:expr, $actor_ref:expr) => {
subscribe!(
stringify!($message_bus),
$message_bus,
$actor_name,
$actor_ref
)
};
($bus_name:expr, $message_bus:expr, $actor_ref:expr) => {
subscribe!($bus_name, $message_bus, stringify!($actor_ref), $actor_ref)
};
($bus_name:expr, $message_bus:expr, $actor_name:expr, $actor_ref:expr) => {{
tracing::debug!(msg_bus = $bus_name, actor = $actor_name, "subscribe");
$message_bus
.tell(kameo::actor::pubsub::Subscribe($actor_ref))
.await
}};
}
#[macro_export]
macro_rules! publish {
($message_bus:expr, $message:expr) => {
publish!(stringify!($message_bus), $message_bus, $message)
};
($bus_name:expr, $message_bus:expr, $message:expr) => {{
tracing::debug!(bus = $bus_name, msg = stringify!($message), "publish");
$message_bus
.tell(kameo::actor::pubsub::Publish($message))
.await
}};
}

View file

@ -0,0 +1,25 @@
//
/// spawns a new actor and sets up bi-directional links
#[macro_export]
macro_rules! spawn {
($parent:expr, $actor:expr) => {{
tracing::debug!("spawning : {}", stringify!($actor));
let new_actor_ref = kameo::spawn($actor);
new_actor_ref.link(&$parent).await;
$parent.link(&new_actor_ref).await;
tracing::debug!("spawned : {}", stringify!($actor));
new_actor_ref
}};
}
#[macro_export]
macro_rules! spawn_in_thread {
($parent:expr, $actor:expr) => {{
tracing::debug!("spawning in thread : {}", stringify!($actor));
let new_actor_ref = kameo::actor::spawn_in_thread($actor);
new_actor_ref.link(&$parent).await;
$parent.link(&new_actor_ref).await;
tracing::debug!("spawned in thread : {}", stringify!($actor));
new_actor_ref
}};
}

View file

@ -2,10 +2,13 @@
#![allow(clippy::module_name_repetitions)] #![allow(clippy::module_name_repetitions)]
mod alerts; mod alerts;
mod base_actor;
mod file_watcher; mod file_watcher;
mod forge; mod forge;
mod init; mod init;
mod macros;
mod repo; mod repo;
mod root;
mod server; mod server;
#[cfg(feature = "tui")] #[cfg(feature = "tui")]
@ -13,9 +16,11 @@ mod tui;
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
mod webhook; mod webhook;
use git_next_core::git; use git_next_core::git;
use kameo::actor::{pubsub::PubSub, ActorRef};
use std::path::PathBuf; use std::path::PathBuf;
@ -23,6 +28,8 @@ use clap::Parser;
use color_eyre::Result; use color_eyre::Result;
use kxio::{fs, net}; use kxio::{fs, net};
pub type MessageBus<T> = ActorRef<PubSub<T>>;
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[clap(version = clap::crate_version!(), author = clap::crate_authors!(), about = clap::crate_description!())] #[clap(version = clap::crate_version!(), author = clap::crate_authors!(), about = clap::crate_description!())]
struct Commands { struct Commands {
@ -47,6 +54,8 @@ enum Server {
} }
fn main() -> Result<()> { fn main() -> Result<()> {
color_eyre::install()?;
let fs = fs::new(PathBuf::default()); let fs = fs::new(PathBuf::default());
let net = net::new(); let net = net::new();
let repository_factory = git::repository::factory::real(); let repository_factory = git::repository::factory::real();
@ -59,16 +68,16 @@ fn main() -> Result<()> {
#[cfg(not(feature = "tui"))] #[cfg(not(feature = "tui"))]
Server::Start {} => server::start( Server::Start {} => server::start(
false, false,
fs, &fs,
net, &net,
repository_factory, repository_factory,
std::time::Duration::from_secs(10), std::time::Duration::from_secs(10),
), ),
#[cfg(feature = "tui")] #[cfg(feature = "tui")]
Server::Start { ui } => server::start( Server::Start { ui } => server::start(
ui, ui,
fs, &fs,
net, &net,
repository_factory, repository_factory,
std::time::Duration::from_secs(10), std::time::Duration::from_secs(10),
), ),

View file

@ -20,7 +20,7 @@ pub fn advance_next(
commit: Option<Commit>, commit: Option<Commit>,
force: git_next_core::git::push::Force, force: git_next_core::git::push::Force,
repo_details: &RepoDetails, repo_details: &RepoDetails,
repo_config: RepoConfig, repo_config: &RepoConfig,
open_repository: &dyn OpenRepositoryLike, open_repository: &dyn OpenRepositoryLike,
message_token: MessageToken, message_token: MessageToken,
) -> Result<MessageToken> { ) -> Result<MessageToken> {

View file

@ -1,8 +1,8 @@
// //
use actix::prelude::*;
use git_next_core::{git, RepoConfigSource}; use git_next_core::{git, RepoConfigSource};
use color_eyre::Result;
use kameo::message::{Context, Message};
use tracing::warn; use tracing::warn;
use crate::{ use crate::{
@ -15,45 +15,55 @@ use crate::{
server::actor::messages::RepoUpdate, server::actor::messages::RepoUpdate,
}; };
impl Handler<AdvanceMain> for RepoActor { impl Message<AdvanceMain> for RepoActor {
type Result = (); type Reply = Result<()>;
#[tracing::instrument(name = "RepoActor::AdvanceMainTo", skip_all, fields(repo = %self.repo_details, commit = ?msg))] #[tracing::instrument(name = "RepoActor::AdvanceMainTo", skip_all, fields(repo = %self.repo_details, commit = ?msg))]
fn handle(&mut self, msg: AdvanceMain, ctx: &mut Self::Context) -> Self::Result { async fn handle(
&mut self,
msg: AdvanceMain,
ctx: Context<'_, Self, Self::Reply>,
) -> Self::Reply {
let Some(repo_config) = self.repo_details.repo_config.clone() else { let Some(repo_config) = self.repo_details.repo_config.clone() else {
warn!("No config loaded"); warn!("No config loaded");
return; return Ok(());
}; };
let Some(open_repository) = &self.open_repository else { let Some(open_repository) = &self.open_repository else {
return; return Ok(());
}; };
let repo_details = self.repo_details.clone();
let addr = ctx.address();
let message_token = self.message_token;
let commit = msg.peel(); let commit = msg.peel();
self.update_tui(RepoUpdate::AdvancingMain { self.update_tui(RepoUpdate::AdvancingMain {
commit: commit.clone(), commit: commit.clone(),
}); })
.await?;
if let Err(err) = advance_main(commit, &repo_details, &repo_config, &**open_repository) { if let Err(err) = advance_main(commit, &self.repo_details, &repo_config, &**open_repository)
{
warn!("advance main: {err}"); warn!("advance main: {err}");
self.alert_tui(format!("advance main: {err}")); self.alert_tui(format!("advance main: {err}")).await?;
} else { } else {
self.update_tui(RepoUpdate::MainUpdated); self.update_tui(RepoUpdate::MainUpdated).await?;
if let Some(open_repository) = &self.open_repository { if let Some(open_repository) = &self.open_repository {
match open_repository.fetch() { match open_repository.fetch() {
Ok(()) => self.update_tui_log(git::graph::log(&self.repo_details)), Ok(()) => {
Err(err) => self.alert_tui(format!("fetching: {err}")), self.update_tui_log(git::graph::log(&self.repo_details))
.await?;
}
Err(err) => self.alert_tui(format!("fetching: {err}")).await?,
} }
} }
match repo_config.source() { match repo_config.source() {
RepoConfigSource::Repo => { RepoConfigSource::Repo => {
do_send(&addr, LoadConfigFromRepo, self.log.as_ref()); do_send(&ctx.actor_ref(), LoadConfigFromRepo, self.log.as_ref()).await?;
} }
RepoConfigSource::Server => { RepoConfigSource::Server => {
do_send(&addr, ValidateRepo::new(message_token), self.log.as_ref()); do_send(
&ctx.actor_ref(),
ValidateRepo::new(self.message_token),
self.log.as_ref(),
)
.await?;
} }
} }
} }
Ok(())
} }
} }

View file

@ -1,8 +1,9 @@
// //
use actix::prelude::*; use color_eyre::Result;
use kameo::message::{Context, Message};
use tracing::warn;
use git_next_core::git; use git_next_core::git;
use tracing::{warn, Instrument};
use crate::{ use crate::{
repo::{ repo::{
@ -14,15 +15,19 @@ use crate::{
server::actor::messages::RepoUpdate, server::actor::messages::RepoUpdate,
}; };
impl Handler<AdvanceNext> for RepoActor { impl Message<AdvanceNext> for RepoActor {
type Result = (); type Reply = Result<()>;
fn handle(&mut self, msg: AdvanceNext, ctx: &mut Self::Context) -> Self::Result { async fn handle(
&mut self,
msg: AdvanceNext,
ctx: Context<'_, Self, Self::Reply>,
) -> Self::Reply {
let Some(repo_config) = &self.repo_details.repo_config else { let Some(repo_config) = &self.repo_details.repo_config else {
return; return Ok(());
}; };
let Some(open_repository) = &self.open_repository else { let Some(open_repository) = &self.open_repository else {
return; return Ok(());
}; };
let AdvanceNextPayload { let AdvanceNextPayload {
@ -30,45 +35,44 @@ impl Handler<AdvanceNext> for RepoActor {
main, main,
dev_commit_history, dev_commit_history,
} = msg.peel(); } = msg.peel();
let repo_details = self.repo_details.clone();
let repo_config = repo_config.clone();
let addr = ctx.address();
let (commit, force) = find_next_commit_on_dev(&next, &main, &dev_commit_history); let (commit, force) = find_next_commit_on_dev(&next, &main, &dev_commit_history);
if let Some(commit) = &commit { if let Some(commit) = &commit {
self.update_tui(RepoUpdate::AdvancingNext { self.update_tui(RepoUpdate::AdvancingNext {
commit: commit.clone(), commit: commit.clone(),
force: force.clone(), force: force.clone(),
}); })
.await?;
}; };
match advance_next( match advance_next(
commit, commit,
force, force,
&repo_details, &self.repo_details,
repo_config, repo_config,
&**open_repository, &**open_repository,
self.message_token, self.message_token,
) { ) {
Ok(message_token) => { Ok(message_token) => {
self.update_tui(RepoUpdate::NextUpdated); self.update_tui(RepoUpdate::NextUpdated).await?;
match open_repository.fetch() { match open_repository.fetch() {
Ok(()) => self.update_tui_log(git::graph::log(&self.repo_details)), Ok(()) => {
Err(err) => self.alert_tui(format!("fetching: {err}")), self.update_tui_log(git::graph::log(&self.repo_details))
.await?;
}
Err(err) => self.alert_tui(format!("fetching: {err}")).await?,
} }
// INFO: pause to allow any CI checks to be started // INFO: pause to allow any CI checks to be started
let sleep_duration = self.sleep_duration; tokio::time::sleep(self.sleep_duration).await;
let log = self.log.clone(); Ok(do_send(
async move { &ctx.actor_ref(),
actix_rt::time::sleep(sleep_duration).await; ValidateRepo::new(message_token),
do_send(&addr, ValidateRepo::new(message_token), log.as_ref()); self.log.as_ref(),
} )
.in_current_span() .await?)
.into_actor(self)
.wait(ctx);
} }
Err(err) => { Err(err) => {
warn!("advance next: {err}"); warn!("advance next: {err}");
self.alert_tui(err.to_string()); self.alert_tui(err.to_string()).await?;
Ok(())
} }
} }
} }

View file

@ -1,7 +1,7 @@
// //
use actix::prelude::*; use color_eyre::Result;
use kameo::message::{Context, Message};
use tracing::{debug, warn, Instrument as _}; use tracing::{debug, warn};
use crate::{ use crate::{
repo::{ repo::{
@ -12,30 +12,30 @@ use crate::{
server::actor::messages::RepoUpdate, server::actor::messages::RepoUpdate,
}; };
impl Handler<CheckCIStatus> for RepoActor { impl Message<CheckCIStatus> for RepoActor {
type Result = (); type Reply = Result<()>;
fn handle(&mut self, msg: CheckCIStatus, ctx: &mut Self::Context) -> Self::Result { async fn handle(
crate::repo::logger(self.log.as_ref(), "start: CheckCIStatus"); &mut self,
msg: CheckCIStatus,
let addr = ctx.address(); ctx: Context<'_, Self, Self::Reply>,
let forge = self.forge.duplicate(); ) -> Self::Reply {
crate::repo::logger(self.log.as_ref(), "start: CheckCIStatus").await;
let next = msg.peel(); let next = msg.peel();
let log = self.log.clone(); self.update_tui(RepoUpdate::CheckingCI).await?;
self.update_tui(RepoUpdate::CheckingCI);
// get the status - pass, fail, pending (all others map to fail, e.g. error) // get the status - pass, fail, pending (all others map to fail, e.g. error)
async move { match self.forge.commit_status(&next).await {
match forge.commit_status(&next).await {
Ok(status) => { Ok(status) => {
debug!("got status: {status:?}"); debug!("got status: {status:?}");
do_send(&addr, ReceiveCIStatus::new((next, status)), log.as_ref()); do_send(
&ctx.actor_ref(),
ReceiveCIStatus::new((next, status)),
self.log.as_ref(),
)
.await?;
} }
Err(err) => warn!(?err, "fetching commit status"), Err(err) => warn!(?err, "fetching commit status"),
} }
} Ok(())
.in_current_span()
.into_actor(self)
.wait(ctx);
} }
} }

View file

@ -1,8 +1,9 @@
// //
use actix::prelude::*; use color_eyre::Result;
use kameo::message::{Context, Message};
use tracing::{debug, instrument, warn};
use git_next_core::git; use git_next_core::git;
use tracing::{debug, instrument, warn};
use crate::{ use crate::{
repo::{ repo::{
@ -13,31 +14,38 @@ use crate::{
server::actor::messages::RepoUpdate, server::actor::messages::RepoUpdate,
}; };
impl Handler<CloneRepo> for RepoActor { impl Message<CloneRepo> for RepoActor {
type Result = (); type Reply = Result<()>;
#[instrument(name = "RepoActor::CloneRepo", skip_all, fields(repo = %self.repo_details))] #[instrument(name = "RepoActor::CloneRepo", skip_all, fields(repo = %self.repo_details))]
fn handle(&mut self, _msg: CloneRepo, ctx: &mut Self::Context) -> Self::Result { async fn handle(
logger(self.log.as_ref(), "Handler: CloneRepo: start"); &mut self,
self.update_tui(RepoUpdate::Opening); _msg: CloneRepo,
ctx: Context<'_, Self, Self::Reply>,
) -> Self::Reply {
logger(self.log.as_ref(), "Handler: CloneRepo: start").await;
self.update_tui(RepoUpdate::Opening).await?;
debug!("Handler: CloneRepo: start"); debug!("Handler: CloneRepo: start");
match git::repository::open(&*self.repository_factory, &self.repo_details) { match git::repository::open(&*self.repository_factory, &self.repo_details) {
Ok(repository) => { Ok(repository) => {
logger(self.log.as_ref(), "open okay"); logger(self.log.as_ref(), "open okay").await;
debug!("open okay"); debug!("open okay");
self.update_tui(RepoUpdate::Opened); self.update_tui(RepoUpdate::Opened).await?;
self.open_repository.replace(repository); self.open_repository.replace(repository);
if self.repo_details.repo_config.is_none() { if self.repo_details.repo_config.is_none() {
do_send(&ctx.address(), LoadConfigFromRepo, self.log.as_ref()); debug!("no repo config, need to load from repo");
do_send(&ctx.actor_ref(), LoadConfigFromRepo, self.log.as_ref()).await?;
} else { } else {
do_send(&ctx.address(), RegisterWebhook::new(), self.log.as_ref()); debug!("have repo config");
do_send(&ctx.actor_ref(), RegisterWebhook::new(), self.log.as_ref()).await?;
} }
} }
Err(err) => { Err(err) => {
logger(self.log.as_ref(), "open failed"); logger(self.log.as_ref(), "open failed").await;
warn!("Could not open repo: {err:?}"); warn!("Could not open repo: {err:?}");
self.alert_tui(err.to_string()); self.alert_tui(err.to_string()).await?;
} }
} }
debug!("Handler: CloneRepo: finish"); debug!("Handler: CloneRepo: finish");
Ok(())
} }
} }

View file

@ -1,10 +1,10 @@
// //
use actix::prelude::*; use color_eyre::Result;
use kameo::message::{Context, Message};
use tracing::{debug, instrument};
use git_next_core::git::UserNotification; use git_next_core::git::UserNotification;
use tracing::{debug, instrument, Instrument as _};
use crate::{ use crate::{
repo::{ repo::{
do_send, load, do_send, load,
@ -14,41 +14,43 @@ use crate::{
server::actor::messages::RepoUpdate, server::actor::messages::RepoUpdate,
}; };
impl Handler<LoadConfigFromRepo> for RepoActor { impl Message<LoadConfigFromRepo> for RepoActor {
type Result = (); type Reply = Result<()>;
#[instrument(name = "Repocrate::repo::LoadConfigFromRepo", skip_all, fields(repo = %self.repo_details))] #[instrument(name = "Repocrate::repo::LoadConfigFromRepo", skip_all, fields(repo = %self.repo_details))]
fn handle(&mut self, _msg: LoadConfigFromRepo, ctx: &mut Self::Context) -> Self::Result { async fn handle(
&mut self,
_msg: LoadConfigFromRepo,
ctx: Context<'_, Self, Self::Reply>,
) -> Self::Reply {
debug!("Handler: LoadConfigFromRepo: start"); debug!("Handler: LoadConfigFromRepo: start");
self.update_tui(RepoUpdate::LoadingConfigFromRepo); self.update_tui(RepoUpdate::LoadingConfigFromRepo).await?;
let Some(open_repository) = &self.open_repository else { let Some(open_repository) = &self.open_repository else {
return; return Ok(());
}; };
let open_repository = open_repository.duplicate(); match load::config_from_repository(&self.repo_details, &**open_repository).await {
let repo_details = self.repo_details.clone();
let forge_alias = repo_details.forge.forge_alias().clone();
let repo_alias = repo_details.repo_alias.clone();
let addr = ctx.address();
let notify_user_recipient = self.notify_user_recipient.clone();
let log = self.log.clone();
async move {
match load::config_from_repository(repo_details, &*open_repository).await {
Ok(repo_config) => { Ok(repo_config) => {
do_send(&addr, ReceiveRepoConfig::new(repo_config), log.as_ref()); do_send(
&ctx.actor_ref(),
ReceiveRepoConfig::new(repo_config),
self.log.as_ref(),
)
.await?;
} }
Err(err) => notify_user( Err(err) => {
notify_user_recipient.as_ref(), notify_user(
self.notify_user_recipient.as_ref(),
UserNotification::RepoConfigLoadFailure { UserNotification::RepoConfigLoadFailure {
forge_alias, forge_alias: self.repo_details.forge.forge_alias().clone(),
repo_alias, repo_alias: self.repo_details.repo_alias.clone(),
reason: err.to_string(), reason: err.to_string(),
}, },
log.as_ref(), self.log.as_ref(),
), )
.await?;
} }
} }
.in_current_span()
.into_actor(self)
.wait(ctx);
debug!("Handler: LoadConfigFromRepo: finish"); debug!("Handler: LoadConfigFromRepo: finish");
Ok(())
} }
} }

View file

@ -1,8 +1,9 @@
// //
use actix::prelude::*; use color_eyre::Result;
use kameo::message::{Context, Message};
use tracing::debug;
use git_next_core::git::{forge::commit::Status, graph, UserNotification}; use git_next_core::git::{forge::commit::Status, graph, UserNotification};
use tracing::{debug, Instrument};
use crate::{ use crate::{
repo::{ repo::{
@ -13,64 +14,64 @@ use crate::{
server::actor::messages::RepoUpdate, server::actor::messages::RepoUpdate,
}; };
impl Handler<ReceiveCIStatus> for RepoActor { impl Message<ReceiveCIStatus> for RepoActor {
type Result = (); type Reply = Result<()>;
fn handle(&mut self, msg: ReceiveCIStatus, ctx: &mut Self::Context) -> Self::Result { async fn handle(
logger(self.log.as_ref(), "start: ReceiveCIStatus"); &mut self,
msg: ReceiveCIStatus,
ctx: Context<'_, Self, Self::Reply>,
) -> Self::Reply {
logger(self.log.as_ref(), "start: ReceiveCIStatus").await;
let (next, status) = msg.peel(); let (next, status) = msg.peel();
self.update_tui(RepoUpdate::ReceiveCIStatus { self.update_tui(RepoUpdate::ReceiveCIStatus {
status: status.clone(), status: status.clone(),
}); })
.await?;
debug!(?status, ""); debug!(?status, "");
let graph_log = graph::log(&self.repo_details); let graph_log = graph::log(&self.repo_details);
self.update_tui_log(graph_log.clone()); self.update_tui_log(graph_log.clone()).await?;
let addr = ctx.address();
let forge_alias = self.repo_details.forge.forge_alias().clone();
let repo_alias = self.repo_details.repo_alias.clone();
let message_token = self.message_token;
let sleep_duration = self.sleep_duration;
match status { match status {
Status::Pass => { Status::Pass => {
do_send(&addr, AdvanceMain::new(next), self.log.as_ref()); do_send(&ctx.actor_ref(), AdvanceMain::new(next), self.log.as_ref()).await?;
} }
Status::Pending => { Status::Pending => {
let log = self.log.clone(); tokio::time::sleep(self.sleep_duration).await;
async move { do_send(
actix_rt::time::sleep(sleep_duration).await; &ctx.actor_ref(),
do_send(&addr, ValidateRepo::new(message_token), log.as_ref()); ValidateRepo::new(self.message_token),
} self.log.as_ref(),
.in_current_span() )
.into_actor(self) .await?;
.wait(ctx);
} }
Status::Fail => { Status::Fail => {
tracing::warn!("Checks have failed"); tracing::warn!("Checks have failed");
notify_user( notify_user(
self.notify_user_recipient.as_ref(), self.notify_user_recipient.as_ref(),
UserNotification::CICheckFailed { UserNotification::CICheckFailed {
forge_alias, forge_alias: self.repo_details.forge.forge_alias().clone(),
repo_alias, repo_alias: self.repo_details.repo_alias.clone(),
commit: next, commit: next,
log: graph_log, log: graph_log,
}, },
self.log.as_ref(), self.log.as_ref(),
); )
let log = self.log.clone(); .await?;
async move {
debug!("sleeping before retrying..."); debug!("sleeping before retrying...");
logger(log.as_ref(), "before sleep"); logger(self.log.clone().as_ref(), "before sleep").await;
actix_rt::time::sleep(sleep_duration).await; tokio::time::sleep(self.sleep_duration).await;
logger(log.as_ref(), "after sleep"); logger(self.log.clone().as_ref(), "after sleep").await;
do_send(&addr, ValidateRepo::new(message_token), log.as_ref()); do_send(
&ctx.actor_ref(),
ValidateRepo::new(self.message_token),
self.log.as_ref(),
)
.await?;
} }
.in_current_span() Status::Error(err) => {
.into_actor(self) tracing::warn!(?err, "Check CI Status");
.wait(ctx);
}
Status::Error(_) => todo!(),
} }
} }
Ok(())
}
} }

View file

@ -1,5 +1,6 @@
// //
use actix::prelude::*; use color_eyre::Result;
use kameo::message::{Context, Message};
use tracing::instrument; use tracing::instrument;
use crate::{ use crate::{
@ -11,16 +12,22 @@ use crate::{
server::actor::messages::RepoUpdate, server::actor::messages::RepoUpdate,
}; };
impl Handler<ReceiveRepoConfig> for RepoActor { impl Message<ReceiveRepoConfig> for RepoActor {
type Result = (); type Reply = Result<()>;
#[instrument(name = "RepoActor::ReceiveRepoConfig", skip_all, fields(repo = %self.repo_details, branches = ?msg))] #[instrument(name = "RepoActor::ReceiveRepoConfig", skip_all, fields(repo = %self.repo_details, branches = ?msg))]
fn handle(&mut self, msg: ReceiveRepoConfig, ctx: &mut Self::Context) -> Self::Result { async fn handle(
&mut self,
msg: ReceiveRepoConfig,
ctx: Context<'_, Self, Self::Reply>,
) -> Self::Reply {
let repo_config = msg.peel(); let repo_config = msg.peel();
self.update_tui(RepoUpdate::ReceiveRepoConfig { self.update_tui(RepoUpdate::ReceiveRepoConfig {
repo_config: repo_config.clone(), repo_config: repo_config.clone(),
}); })
.await?;
self.repo_details.repo_config.replace(repo_config); self.repo_details.repo_config.replace(repo_config);
self.update_tui_branches(); self.update_tui_branches().await?;
do_send(&ctx.address(), RegisterWebhook::new(), self.log.as_ref()); do_send(&ctx.actor_ref(), RegisterWebhook::new(), self.log.as_ref()).await?;
Ok(())
} }
} }

View file

@ -1,7 +1,7 @@
// //
use actix::prelude::*; use color_eyre::Result;
use kameo::message::{Context, Message};
use tracing::{debug, error, Instrument as _}; use tracing::{debug, error};
use crate::{ use crate::{
repo::{ repo::{
@ -14,51 +14,52 @@ use crate::{
use git_next_core::git::UserNotification; use git_next_core::git::UserNotification;
impl Handler<RegisterWebhook> for RepoActor { impl Message<RegisterWebhook> for RepoActor {
type Result = (); type Reply = Result<()>;
fn handle(&mut self, _msg: RegisterWebhook, ctx: &mut Self::Context) -> Self::Result { async fn handle(
&mut self,
_msg: RegisterWebhook,
ctx: Context<'_, Self, Self::Reply>,
) -> Self::Reply {
if self.webhook_id.is_none() { if self.webhook_id.is_none() {
let forge_alias = self.repo_details.forge.forge_alias().clone(); self.update_tui(RepoUpdate::RegisteringWebhook).await?;
let repo_alias = self.repo_details.repo_alias.clone();
let repo_listen_url = self
.listen_url
.repo_url(forge_alias.clone(), repo_alias.clone());
let forge = self.forge.duplicate();
let addr = ctx.address();
let notify_user_recipient = self.notify_user_recipient.clone();
let log = self.log.clone();
self.update_tui(RepoUpdate::RegisteringWebhook);
debug!("registering webhook"); debug!("registering webhook");
async move { match self
match forge.register_webhook(&repo_listen_url).await { .forge
.register_webhook(&self.listen_url.repo_url(
self.repo_details.forge.forge_alias().clone(),
self.repo_details.repo_alias.clone(),
))
.await
{
Ok(registered_webhook) => { Ok(registered_webhook) => {
debug!(?registered_webhook, "webhook registered"); debug!(?registered_webhook, "webhook registered");
do_send( do_send(
&addr, &ctx.actor_ref(),
WebhookRegistered::from(registered_webhook), WebhookRegistered::from(registered_webhook),
log.as_ref(), self.log.as_ref(),
); )
.await?;
} }
Err(err) => { Err(err) => {
error!(?err, "failed to register webhook"); error!(?err, "failed to register webhook");
notify_user( notify_user(
notify_user_recipient.as_ref(), self.notify_user_recipient.clone().as_ref(),
UserNotification::WebhookRegistration { UserNotification::WebhookRegistration {
forge_alias, forge_alias: self.repo_details.forge.forge_alias().clone(),
repo_alias, repo_alias: self.repo_details.repo_alias.clone(),
reason: err.to_string(), reason: err.to_string(),
}, },
log.as_ref(), self.log.as_ref(),
); )
.await?;
} }
} }
}
.in_current_span()
.into_actor(self)
.wait(ctx);
} else { } else {
self.alert_tui("already have a webhook id - cant register webhook"); self.alert_tui("already have a webhook id - cant register webhook")
} .await?;
}
Ok(())
} }
} }

View file

@ -1,32 +1,31 @@
// //
use actix::prelude::*; use color_eyre::Result;
use kameo::message::{Context, Message};
use tracing::{debug, warn, Instrument as _}; use tracing::{debug, warn};
use crate::{ use crate::{
repo::{messages::UnRegisterWebhook, RepoActor}, repo::{messages::UnRegisterWebhook, RepoActor},
server::actor::messages::RepoUpdate, server::actor::messages::RepoUpdate,
}; };
impl Handler<UnRegisterWebhook> for RepoActor { impl Message<UnRegisterWebhook> for RepoActor {
type Result = (); type Reply = Result<()>;
fn handle(&mut self, _msg: UnRegisterWebhook, ctx: &mut Self::Context) -> Self::Result { async fn handle(
&mut self,
_msg: UnRegisterWebhook,
_ctx: Context<'_, Self, Self::Reply>,
) -> Self::Reply {
let Some(webhook_id) = self.webhook_id.take() else { let Some(webhook_id) = self.webhook_id.take() else {
return; return Ok(());
}; };
self.update_tui(RepoUpdate::UnregisteringWebhook); self.update_tui(RepoUpdate::UnregisteringWebhook).await?;
let forge = self.forge.duplicate();
debug!("unregistering webhook"); debug!("unregistering webhook");
async move { match self.forge.unregister_webhook(&webhook_id).await {
match forge.unregister_webhook(&webhook_id).await {
Ok(()) => debug!("unregistered webhook"), Ok(()) => debug!("unregistered webhook"),
Err(err) => warn!(?err, "unregistering webhook"), Err(err) => warn!(?err, "unregistering webhook"),
} }
}
.in_current_span()
.into_actor(self)
.wait(ctx);
debug!("unregistering webhook done"); debug!("unregistering webhook done");
Ok(())
} }
} }

View file

@ -1,7 +1,9 @@
// //
use actix::prelude::*; use std::collections::HashMap;
use tracing::{info, instrument, Instrument as _}; use color_eyre::Result;
use kameo::message::{Context, Message};
use tracing::{debug, info, instrument, warn};
use crate::{ use crate::{
repo::{ repo::{
@ -11,20 +13,23 @@ use crate::{
}, },
server::actor::messages::RepoUpdate, server::actor::messages::RepoUpdate,
}; };
use git_next_core::git::{ use git_next_core::git::{
push::Force, push::Force,
validation::positions::{validate, Error, Positions}, validation::positions::{validate, Error, Positions},
UserNotification, UserNotification,
}; };
use git_next_core::s;
impl Handler<ValidateRepo> for RepoActor { impl Message<ValidateRepo> for RepoActor {
type Result = (); type Reply = Result<()>;
#[instrument(name = "RepoActor::ValidateRepo", skip_all, fields(repo = %self.repo_details, token = %&*msg))] #[instrument(name = "RepoActor::ValidateRepo", skip_all, fields(repo = %self.repo_details, token = %&*msg))]
fn handle(&mut self, msg: ValidateRepo, ctx: &mut Self::Context) -> Self::Result { async fn handle(
logger(self.log.as_ref(), "start: ValidateRepo"); &mut self,
msg: ValidateRepo,
ctx: Context<'_, Self, Self::Reply>,
) -> Self::Reply {
logger(self.log.as_ref(), "start: ValidateRepo").await;
// Message Token - make sure we are only triggered for the latest/current token // Message Token - make sure we are only triggered for the latest/current token
match self.token_status(msg.peel()) { match self.token_status(msg.peel()) {
TokenStatus::Current => {} // do nothing TokenStatus::Current => {} // do nothing
@ -32,38 +37,38 @@ impl Handler<ValidateRepo> for RepoActor {
logger( logger(
self.log.as_ref(), self.log.as_ref(),
format!("discarded: old message token: {}", self.message_token), format!("discarded: old message token: {}", self.message_token),
); )
return; // message is expired .await;
return Ok(()); // message is expired
} }
TokenStatus::New(message_token) => { TokenStatus::New(message_token) => {
self.message_token = message_token; self.message_token = message_token;
logger( logger(
self.log.as_ref(), self.log.as_ref(),
format!("new message token: {}", self.message_token), format!("new message token: {}", self.message_token),
); )
.await;
} }
} }
logger( logger(
self.log.as_ref(), self.log.as_ref(),
format!("accepted token: {}", self.message_token), format!("accepted token: {}", self.message_token),
); )
.await;
self.update_tui(RepoUpdate::ValidateRepo); self.update_tui(RepoUpdate::ValidateRepo).await?;
// Repository positions // Repository positions
let Some(ref open_repository) = self.open_repository else { let Some(ref open_repository) = self.open_repository else {
logger(self.log.as_ref(), "no open repository"); logger(self.log.as_ref(), "no open repository").await;
self.alert_tui("repo not open"); self.alert_tui("repo not open").await?;
return; return Ok(());
}; };
logger(self.log.as_ref(), "have open repository"); logger(self.log.as_ref(), "have open repository").await;
let Some(repo_config) = self.repo_details.repo_config.clone() else { let Some(repo_config) = self.repo_details.repo_config.clone() else {
logger(self.log.as_ref(), "no repo config"); logger(self.log.as_ref(), "no repo config").await;
self.alert_tui("no repo config"); self.alert_tui("no repo config").await?;
return; return Ok(());
}; };
logger(self.log.as_ref(), "have repo config"); logger(self.log.as_ref(), "have repo config").await;
match validate(&**open_repository, &self.repo_details, &repo_config) { match validate(&**open_repository, &self.repo_details, &repo_config) {
Ok(( Ok((
Positions { Positions {
@ -75,70 +80,88 @@ impl Handler<ValidateRepo> for RepoActor {
}, },
git_log, git_log,
)) => { )) => {
info!(%main, %next, %dev, "positions"); let mut positions = HashMap::new();
self.update_tui_log(git_log); positions
.entry(s!(s!(main.sha())[0..7]))
.or_insert_with(Vec::new)
.extend(["main"]);
positions
.entry(s!(s!(next.sha())[0..7]))
.or_insert_with(Vec::new)
.extend(["next"]);
positions
.entry(s!(s!(dev.sha())[0..7]))
.or_insert_with(Vec::new)
.extend(["dev"]);
info!(?positions, "");
self.update_tui_log(git_log).await?;
if next_is_valid && next != main { if next_is_valid && next != main {
info!("Checking CI"); info!("Checking CI");
do_send(&ctx.address(), CheckCIStatus::new(next), self.log.as_ref()); do_send(
&ctx.actor_ref(),
CheckCIStatus::new(next),
self.log.as_ref(),
)
.await?;
} else if next != dev { } else if next != dev {
info!("Advance next"); info!("Advance next");
self.update_tui(RepoUpdate::AdvancingNext { self.update_tui(RepoUpdate::AdvancingNext {
commit: next.clone(), commit: next.clone(),
force: Force::No, force: Force::No,
}); })
.await?;
do_send( do_send(
&ctx.address(), &ctx.actor_ref(),
AdvanceNext::new(AdvanceNextPayload { AdvanceNext::new(AdvanceNextPayload {
next, next,
main, main,
dev_commit_history, dev_commit_history,
}), }),
self.log.as_ref(), self.log.as_ref(),
); )
.await?;
} else { } else {
info!("do nothing"); info!("do nothing");
self.update_tui(RepoUpdate::Okay { main, next, dev }); self.update_tui(RepoUpdate::Okay { main, next, dev })
.await?;
} }
} }
Err(Error::Retryable(message)) => { Err(Error::Retryable(message)) => {
info!(?message, "Retryable"); warn!(?message, "Retryable");
self.alert_tui(format!("retryable: {message}")); self.alert_tui(format!("retryable: {message}")).await?;
logger(self.log.as_ref(), message); logger(self.log.as_ref(), message).await;
let addr = ctx.address(); debug!("sleeping before retrying...");
let message_token = self.message_token; tokio::time::sleep(self.sleep_duration).await;
let sleep_duration = self.sleep_duration; do_send(
let log = self.log.clone(); &ctx.actor_ref(),
async move { ValidateRepo::new(self.message_token),
info!("sleeping before retrying..."); self.log.as_ref(),
logger(log.as_ref(), "before sleep"); )
actix_rt::time::sleep(sleep_duration).await; .await?;
logger(log.as_ref(), "after sleep");
do_send(&addr, ValidateRepo::new(message_token), log.as_ref());
}
.in_current_span()
.into_actor(self)
.wait(ctx);
} }
Err(Error::UserIntervention(user_notification)) => { Err(Error::UserIntervention(user_notification)) => {
info!(?user_notification, "User Intervention"); warn!(?user_notification, "User Intervention");
self.alert_tui(format!("USER INTERVENTION: {user_notification}")); self.alert_tui(format!("USER INTERVENTION: {user_notification}"))
.await?;
if let UserNotification::CICheckFailed { log, .. } if let UserNotification::CICheckFailed { log, .. }
| UserNotification::DevNotBasedOnMain { log, .. } = &user_notification | UserNotification::DevNotBasedOnMain { log, .. } = &user_notification
{ {
self.update_tui_log(log.clone()); self.update_tui_log(log.clone()).await?;
} }
notify_user( notify_user(
self.notify_user_recipient.as_ref(), self.notify_user_recipient.as_ref(),
user_notification, user_notification,
self.log.as_ref(), self.log.as_ref(),
); )
.await?;
} }
Err(Error::NonRetryable(message)) => { Err(Error::NonRetryable(message)) => {
info!(?message, "NonRetryable"); warn!(?message, "NonRetryable");
self.alert_tui(format!("Error: {message}")); self.alert_tui(format!("Error: {message}")).await?;
logger(self.log.as_ref(), message); logger(self.log.as_ref(), message).await;
} }
} }
Ok(())
} }
} }

View file

@ -1,8 +1,14 @@
// //
use actix::prelude::*; use color_eyre::Result;
use kameo::message::{Context, Message};
use tracing::{info, instrument, warn}; use tracing::{info, instrument, warn};
use git_next_core::{
git::{Commit, ForgeLike},
webhook::{push::Branch, Push},
BranchName, WebhookAuth,
};
use crate::{ use crate::{
repo::{ repo::{
do_send, logger, do_send, logger,
@ -12,21 +18,19 @@ use crate::{
server::actor::messages::RepoUpdate, server::actor::messages::RepoUpdate,
}; };
use git_next_core::{ impl Message<WebhookNotification> for RepoActor {
git::{Commit, ForgeLike}, type Reply = Result<()>;
webhook::{push::Branch, Push},
BranchName, WebhookAuth,
};
impl Handler<WebhookNotification> for RepoActor {
type Result = ();
#[instrument(name = "RepoActor::WebhookMessage", skip_all, fields(token = %self.message_token, repo = %self.repo_details))] #[instrument(name = "RepoActor::WebhookMessage", skip_all, fields(token = %self.message_token, repo = %self.repo_details))]
fn handle(&mut self, msg: WebhookNotification, ctx: &mut Self::Context) -> Self::Result { async fn handle(
&mut self,
msg: WebhookNotification,
ctx: Context<'_, Self, Self::Reply>,
) -> Self::Reply {
let Some(config) = &self.repo_details.repo_config else { let Some(config) = &self.repo_details.repo_config else {
logger(self.log.as_ref(), "server has no repo config"); logger(self.log.as_ref(), "server has no repo config").await;
warn!("No repo config"); warn!("No repo config");
return; return Ok(());
}; };
if validate_notification( if validate_notification(
&msg, &msg,
@ -34,72 +38,78 @@ impl Handler<WebhookNotification> for RepoActor {
&*self.forge, &*self.forge,
self.log.as_ref(), self.log.as_ref(),
) )
.await
.is_err() .is_err()
{ {
return; return Ok(());
} }
let body = msg.body(); match self.forge.parse_webhook_body(msg.body()) {
match self.forge.parse_webhook_body(body) {
Err(err) => { Err(err) => {
logger(self.log.as_ref(), "message parse error - not a push"); logger(self.log.as_ref(), "message parse error - not a push").await;
warn!(?err, "Not a 'push'"); warn!(?err, "Not a 'push'");
return; return Ok(());
} }
Ok(push) => match push.branch(config.branches()) { Ok(push) => match push.branch(config.branches()) {
None => { None => {
logger(self.log.as_ref(), "unknown branch"); logger(self.log.as_ref(), "unknown branch").await;
warn!( warn!(
?push, ?push,
"Unrecognised branch, we should be filtering to only the ones we want" "Unrecognised branch, we should be filtering to only the ones we want"
); );
return; return Ok(());
} }
Some(Branch::Main) => { Some(Branch::Main) => {
self.update_tui(RepoUpdate::WebhookReceived { self.update_tui(RepoUpdate::WebhookReceived {
branch: Branch::Main, branch: Branch::Main,
push: push.clone(), push: push.clone(),
}); })
.await?;
if handle_push( if handle_push(
push, push,
&config.branches().main(), &config.branches().main(),
&mut self.last_main_commit, &mut self.last_main_commit,
self.log.as_ref(), self.log.as_ref(),
) )
.await
.is_err() .is_err()
{ {
return; return Ok(());
}; };
} }
Some(Branch::Next) => { Some(Branch::Next) => {
self.update_tui(RepoUpdate::WebhookReceived { self.update_tui(RepoUpdate::WebhookReceived {
branch: Branch::Next, branch: Branch::Next,
push: push.clone(), push: push.clone(),
}); })
.await?;
if handle_push( if handle_push(
push, push,
&config.branches().next(), &config.branches().next(),
&mut self.last_next_commit, &mut self.last_next_commit,
self.log.as_ref(), self.log.as_ref(),
) )
.await
.is_err() .is_err()
{ {
return; return Ok(());
}; };
} }
Some(Branch::Dev) => { Some(Branch::Dev) => {
self.update_tui(RepoUpdate::WebhookReceived { self.update_tui(RepoUpdate::WebhookReceived {
branch: Branch::Dev, branch: Branch::Dev,
push: push.clone(), push: push.clone(),
}); })
.await?;
if handle_push( if handle_push(
push, push,
&config.branches().dev(), &config.branches().dev(),
&mut self.last_dev_commit, &mut self.last_dev_commit,
self.log.as_ref(), self.log.as_ref(),
) )
.await
.is_err() .is_err()
{ {
return; return Ok(());
}; };
} }
}, },
@ -110,27 +120,29 @@ impl Handler<WebhookNotification> for RepoActor {
"New commit" "New commit"
); );
do_send( do_send(
&ctx.address(), &ctx.actor_ref(),
ValidateRepo::new(message_token), ValidateRepo::new(message_token),
self.log.as_ref(), self.log.as_ref(),
); )
.await?;
Ok(())
} }
} }
fn validate_notification( async fn validate_notification(
msg: &WebhookNotification, msg: &WebhookNotification,
webhook_auth: Option<&WebhookAuth>, webhook_auth: Option<&WebhookAuth>,
forge: &dyn ForgeLike, forge: &dyn ForgeLike,
log: Option<&ActorLog>, log: Option<&ActorLog>,
) -> Result<(), ()> { ) -> Result<(), ()> {
let Some(expected_authorization) = webhook_auth else { let Some(expected_authorization) = webhook_auth else {
logger(log, "server has no auth token"); logger(log, "server has no auth token").await;
warn!("Don't know what authorization to expect"); warn!("Don't know what authorization to expect");
return Err(()); return Err(());
}; };
if !forge.is_message_authorised(msg, expected_authorization) { if !forge.is_message_authorised(msg, expected_authorization) {
logger(log, "message authorisation is invalid"); logger(log, "message authorisation is invalid").await;
warn!( warn!(
"Invalid authorization - expected {}", "Invalid authorization - expected {}",
expected_authorization expected_authorization
@ -138,22 +150,22 @@ fn validate_notification(
return Err(()); return Err(());
} }
if forge.should_ignore_message(msg) { if forge.should_ignore_message(msg) {
logger(log, "forge sent ignorable message"); logger(log, "forge sent ignorable message").await;
return Err(()); return Err(());
} }
Ok(()) Ok(())
} }
fn handle_push( async fn handle_push(
push: Push, push: Push,
branch: &BranchName, branch: &BranchName,
last_commit: &mut Option<Commit>, last_commit: &mut Option<Commit>,
log: Option<&ActorLog>, log: Option<&ActorLog>,
) -> Result<(), ()> { ) -> Result<(), ()> {
logger(log, format!("message is for {branch} branch")); logger(log, format!("message is for {branch} branch")).await;
let commit = Commit::from(push); let commit = Commit::from(push);
if last_commit.as_ref() == Some(&commit) { if last_commit.as_ref() == Some(&commit) {
logger(log, format!("not a new commit on {branch}")); logger(log, format!("not a new commit on {branch}")).await;
info!( info!(
%branch , %branch ,
%commit, %commit,

View file

@ -1,5 +1,6 @@
// //
use actix::prelude::*; use color_eyre::Result;
use kameo::message::{Context, Message};
use tracing::instrument; use tracing::instrument;
use crate::{ use crate::{
@ -11,17 +12,24 @@ use crate::{
server::actor::messages::RepoUpdate, server::actor::messages::RepoUpdate,
}; };
impl Handler<WebhookRegistered> for RepoActor { impl Message<WebhookRegistered> for RepoActor {
type Result = (); type Reply = Result<()>;
#[instrument(name = "RepoActor::WebhookRegistered", skip_all, fields(repo = %self.repo_details, webhook_id = %msg.webhook_id()))] #[instrument(name = "RepoActor::WebhookRegistered", skip_all, fields(repo = %self.repo_details, webhook_id = %msg.webhook_id()))]
fn handle(&mut self, msg: WebhookRegistered, ctx: &mut Self::Context) -> Self::Result { async fn handle(
self.update_tui(RepoUpdate::RegisteredWebhook); &mut self,
msg: WebhookRegistered,
ctx: Context<'_, Self, Self::Reply>,
) -> Self::Reply {
self.update_tui(RepoUpdate::RegisteredWebhook).await?;
self.webhook_id.replace(msg.webhook_id().clone()); self.webhook_id.replace(msg.webhook_id().clone());
self.webhook_auth.replace(msg.webhook_auth().clone()); self.webhook_auth.replace(msg.webhook_auth().clone());
do_send( do_send(
&ctx.address(), &ctx.actor_ref(),
ValidateRepo::new(self.message_token), ValidateRepo::new(self.message_token),
self.log.as_ref(), self.log.as_ref(),
); )
.await?;
Ok(())
} }
} }

View file

@ -12,7 +12,7 @@ use tracing::{info, instrument};
/// Loads the [RepoConfig] from the `.git-next.toml` file in the repository /// Loads the [RepoConfig] from the `.git-next.toml` file in the repository
#[instrument(skip_all, fields(branch = %repo_details.branch))] #[instrument(skip_all, fields(branch = %repo_details.branch))]
pub async fn config_from_repository( pub async fn config_from_repository(
repo_details: RepoDetails, repo_details: &RepoDetails,
open_repository: &dyn OpenRepositoryLike, open_repository: &dyn OpenRepositoryLike,
) -> Result<RepoConfig> { ) -> Result<RepoConfig> {
info!("Loading .git-next.toml from repo"); info!("Loading .git-next.toml from repo");

View file

@ -1,13 +1,12 @@
// //
use actix::prelude::*; use std::sync::Arc;
use crate::{ use color_eyre::{eyre::eyre, Result};
alerts::messages::NotifyUser,
server::{actor::messages::RepoUpdate, ServerActor},
};
use derive_more::Deref; use derive_more::Deref;
use kameo::{actor::ActorRef, mailbox::unbounded::UnboundedMailbox, Actor};
use kxio::net::Net; use kxio::net::Net;
use tracing::{info, instrument, warn, Instrument}; use tokio::sync::RwLock;
use tracing::{debug, info, instrument, warn};
use git_next_core::{ use git_next_core::{
git::{ git::{
@ -19,6 +18,16 @@ use git_next_core::{
WebhookAuth, WebhookId, WebhookAuth, WebhookId,
}; };
use crate::{
alerts::{messages::NotifyUser, AlertsActor},
default_on_actor_link_died, default_on_actor_panic, default_on_actor_start, on_actor_stop,
server::{
actor::messages::{RepoUpdate, ServerUpdate},
ServerActor,
},
tell,
};
mod branch; mod branch;
pub mod handlers; pub mod handlers;
mod load; mod load;
@ -29,14 +38,21 @@ mod notifications;
pub mod tests; pub mod tests;
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub struct ActorLog(std::sync::Arc<std::sync::RwLock<Vec<String>>>); pub struct ActorLog(Arc<RwLock<Vec<String>>>);
impl Deref for ActorLog { impl Deref for ActorLog {
type Target = std::sync::Arc<std::sync::RwLock<Vec<String>>>; type Target = Arc<RwLock<Vec<String>>>;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
&self.0 &self.0
} }
} }
impl ActorLog {
pub async fn log(&self, message: impl Into<String> + Send) {
let message = message.into();
debug!(%message, "log");
self.write().await.push(message);
}
}
/// An actor that represents a Git Repository. /// An actor that represents a Git Repository.
/// ///
@ -45,6 +61,7 @@ impl Deref for ActorLog {
#[derive(Debug, derive_more::Display, derive_with::With)] #[derive(Debug, derive_more::Display, derive_with::With)]
#[display("{}:{}:{}", generation, repo_details.forge.forge_alias(), repo_details.repo_alias)] #[display("{}:{}:{}", generation, repo_details.forge.forge_alias(), repo_details.repo_alias)]
pub struct RepoActor { pub struct RepoActor {
ui: bool,
sleep_duration: std::time::Duration, sleep_duration: std::time::Duration,
generation: git::Generation, generation: git::Generation,
message_token: messages::MessageToken, message_token: messages::MessageToken,
@ -60,12 +77,13 @@ pub struct RepoActor {
net: Net, net: Net,
forge: Box<dyn git::ForgeLike>, forge: Box<dyn git::ForgeLike>,
log: Option<ActorLog>, log: Option<ActorLog>,
notify_user_recipient: Option<Recipient<NotifyUser>>, notify_user_recipient: Option<ActorRef<AlertsActor>>,
server_addr: Option<Addr<ServerActor>>, server_actor_ref: ActorRef<ServerActor>,
} }
impl RepoActor { impl RepoActor {
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub fn new( pub fn new(
ui: bool,
repo_details: git::RepoDetails, repo_details: git::RepoDetails,
forge: Box<dyn git::ForgeLike>, forge: Box<dyn git::ForgeLike>,
listen_url: ListenUrl, listen_url: ListenUrl,
@ -73,11 +91,12 @@ impl RepoActor {
net: Net, net: Net,
repository_factory: Box<dyn RepositoryFactory>, repository_factory: Box<dyn RepositoryFactory>,
sleep_duration: std::time::Duration, sleep_duration: std::time::Duration,
notify_user_recipient: Option<Recipient<NotifyUser>>, notify_user_recipient: Option<ActorRef<AlertsActor>>,
server_addr: Option<Addr<ServerActor>>, server_actor_ref: ActorRef<ServerActor>,
) -> Self { ) -> Self {
let message_token = messages::MessageToken::default(); let message_token = messages::MessageToken::default();
Self { Self {
ui,
generation, generation,
message_token, message_token,
repo_details, repo_details,
@ -94,111 +113,114 @@ impl RepoActor {
sleep_duration, sleep_duration,
log: None, log: None,
notify_user_recipient, notify_user_recipient,
server_addr, server_actor_ref,
} }
} }
fn update_tui_branches(&self) { async fn update_tui_branches(&self) -> Result<()> {
if cfg!(feature = "tui") { if cfg!(feature = "tui") {
use crate::server::actor::messages::RepoUpdate; use crate::server::actor::messages::RepoUpdate;
let Some(repo_config) = &self.repo_details.repo_config else { let Some(repo_config) = &self.repo_details.repo_config else {
return; return Ok(());
}; };
let branches = repo_config.branches().clone(); let branches = repo_config.branches().clone();
self.update_tui(RepoUpdate::Branches { branches }); self.update_tui(RepoUpdate::Branches { branches }).await?;
} }
Ok(())
} }
#[allow(unused_variables)] #[allow(unused_variables)]
fn update_tui_log(&self, log: git::graph::Log) { async fn update_tui_log(&self, log: git::graph::Log) -> Result<()> {
if cfg!(feature = "tui") { if cfg!(feature = "tui") {
self.update_tui(RepoUpdate::Log { log }); self.update_tui(RepoUpdate::Log { log }).await?;
} }
Ok(())
} }
#[allow(unused_variables)] #[allow(unused_variables)]
fn alert_tui(&self, alert: impl Into<String>) { async fn alert_tui(&self, alert: impl Into<String> + Send) -> Result<()> {
if cfg!(feature = "tui") { if cfg!(feature = "tui") {
self.update_tui(RepoUpdate::Alert { self.update_tui(RepoUpdate::Alert {
alert: alert.into(), alert: alert.into(),
}); })
.await?;
} }
Ok(())
} }
#[allow(unused_variables)] #[allow(unused_variables)]
fn update_tui(&self, repo_update: RepoUpdate) { #[instrument(skip_all)]
if cfg!(feature = "tui") { async fn update_tui(&self, repo_update: RepoUpdate) -> Result<()> {
let Some(server_addr) = &self.server_addr else { if cfg!(feature = "tui") && self.ui {
return; tell!(
}; "server",
self.server_actor_ref,
let update = crate::server::actor::messages::ServerUpdate::RepoUpdate { ServerUpdate::RepoUpdate {
forge_alias: self.repo_details.forge.forge_alias().clone(), forge_alias: self.repo_details.forge.forge_alias().clone(),
repo_alias: self.repo_details.repo_alias.clone(), repo_alias: self.repo_details.repo_alias.clone(),
repo_update, repo_update,
};
server_addr.do_send(update);
} }
)?;
}
Ok(())
} }
} }
impl Actor for RepoActor { impl Actor for RepoActor {
type Context = Context<Self>; type Mailbox = UnboundedMailbox<Self>;
#[instrument(name = "RepoActor::stopping", skip_all, fields(repo = %self.repo_details))]
fn stopping(&mut self, ctx: &mut Self::Context) -> Running { default_on_actor_start!(this, actor_ref);
tracing::debug!("stopping"); default_on_actor_panic!(this, actor_ref, err);
default_on_actor_link_died!(this, actor_ref, id, reason);
on_actor_stop!(this, actor_ref, reason, {
info!("Checking webhook"); info!("Checking webhook");
match self.webhook_id.take() { if let Some(webhook_id) = this.webhook_id.take() {
Some(webhook_id) => {
tracing::warn!("stopping - unregistering webhook"); tracing::warn!("stopping - unregistering webhook");
info!(%webhook_id, "Unregistring webhook"); info!(%webhook_id, "Unregistring webhook");
let forge = self.forge.duplicate(); let forge = this.forge.duplicate();
async move {
if let Err(err) = forge.unregister_webhook(&webhook_id).await { if let Err(err) = forge.unregister_webhook(&webhook_id).await {
warn!("unregistering webhook: {err}"); warn!("unregistering webhook: {err}");
} }
} }
.in_current_span() Ok(())
.into_actor(self) });
.wait(ctx);
Running::Continue
}
None => Running::Stop,
}
}
} }
pub fn do_send<M>(addr: &Addr<RepoActor>, msg: M, log: Option<&ActorLog>) pub async fn do_send<M>(
repo_actor_ref: &ActorRef<RepoActor>,
msg: M,
log: Option<&ActorLog>,
) -> Result<()>
where where
M: actix::Message + Send + 'static + std::fmt::Debug, M: Send + Sync + 'static + std::fmt::Debug,
RepoActor: actix::Handler<M>, RepoActor: kameo::message::Message<M>,
<M as actix::Message>::Result: Send, <RepoActor as kameo::message::Message<M>>::Reply: Send + Sync + 'static + std::fmt::Debug,
<<RepoActor as kameo::message::Message<M>>::Reply as kameo::Reply>::Error: std::fmt::Debug,
{ {
let log_message = format!("send: {msg:?}"); let log_message = format!("send: {msg:?}");
info!(log_message); logger(log, log_message.clone()).await;
logger(log, log_message);
if cfg!(not(test)) { if cfg!(not(test)) {
// #[cfg(not(test))] tell!(repo_actor_ref, msg).map_err(|e| eyre!(format!("error: {log_message}: {e:?}")))?;
addr.do_send(msg);
} }
Ok(())
} }
pub fn logger(log: Option<&ActorLog>, message: impl Into<String>) { pub async fn logger(log: Option<&ActorLog>, message: impl Into<String> + Send) {
if let Some(log) = log { if let Some(log) = log {
let message: String = message.into(); log.log(message).await;
tracing::debug!(message);
let _ = log.write().map(|mut l| l.push(message));
} }
} }
pub fn notify_user( pub async fn notify_user(
recipient: Option<&Recipient<NotifyUser>>, recipient: Option<&ActorRef<AlertsActor>>,
user_notification: UserNotification, user_notification: UserNotification,
log: Option<&ActorLog>, log: Option<&ActorLog>,
) { ) -> Result<()> {
let msg = NotifyUser::from(user_notification); let msg = NotifyUser::from(user_notification);
let log_message = format!("send: {msg:?}"); let log_message = format!("send: {msg:?}");
tracing::debug!(log_message); debug!(log_message);
logger(log, log_message); logger(log, log_message).await;
if let Some(recipient) = &recipient { if let Some(recipient) = &recipient {
recipient.do_send(msg); tell!("alerts", recipient, msg)?;
} }
Ok(())
} }

View file

@ -8,7 +8,7 @@ fn advance_next_sut(
main: &Commit, main: &Commit,
dev_commit_history: &[Commit], dev_commit_history: &[Commit],
repo_details: &RepoDetails, repo_details: &RepoDetails,
repo_config: RepoConfig, repo_config: &RepoConfig,
open_repository: &dyn OpenRepositoryLike, open_repository: &dyn OpenRepositoryLike,
message_token: MessageToken, message_token: MessageToken,
) -> branch::Result<MessageToken> { ) -> branch::Result<MessageToken> {
@ -43,7 +43,7 @@ mod when_at_dev {
main, main,
dev_commit_history, dev_commit_history,
&repo_details, &repo_details,
repo_config, &repo_config,
&open_repository, &open_repository,
message_token, message_token,
) )
@ -78,7 +78,7 @@ mod can_advance {
main, main,
dev_commit_history, dev_commit_history,
&repo_details, &repo_details,
repo_config, &repo_config,
&open_repository, &open_repository,
message_token, message_token,
) )
@ -109,7 +109,7 @@ mod can_advance {
main, main,
dev_commit_history, dev_commit_history,
&repo_details, &repo_details,
repo_config, &repo_config,
&open_repository, &open_repository,
message_token, message_token,
) )
@ -149,7 +149,7 @@ mod can_advance {
main, main,
dev_commit_history, dev_commit_history,
&repo_details, &repo_details,
repo_config, &repo_config,
&open_repository, &open_repository,
message_token, message_token,
) )
@ -181,7 +181,7 @@ mod can_advance {
main, main,
dev_commit_history, dev_commit_history,
&repo_details, &repo_details,
repo_config, &repo_config,
&open_repository, &open_repository,
message_token, message_token,
) )

View file

@ -10,7 +10,7 @@ mod advance_next;
use crate::git; use crate::git;
use crate::repo::branch; use crate::repo::branch;
#[actix_rt::test] #[tokio::test]
async fn test_find_next_commit_on_dev_when_next_is_at_main() { async fn test_find_next_commit_on_dev_when_next_is_at_main() {
let next = given::a_commit(); // and main let next = given::a_commit(); // and main
let expected = given::a_commit(); let expected = given::a_commit();
@ -27,7 +27,7 @@ async fn test_find_next_commit_on_dev_when_next_is_at_main() {
assert_eq!(force, Force::No, "should not try to force"); assert_eq!(force, Force::No, "should not try to force");
} }
#[actix_rt::test] #[tokio::test]
async fn test_find_next_commit_on_dev_when_next_is_not_on_dev() { async fn test_find_next_commit_on_dev_when_next_is_not_on_dev() {
let next = given::a_commit(); let next = given::a_commit();
let main = given::a_commit(); let main = given::a_commit();

View file

@ -1,7 +1,14 @@
use crate::{
alerts::{AlertsActor, History},
server::{actor::messages::ServerUpdate, ServerActor},
};
// //
use super::*; use super::*;
use git_next_core::server::ListenUrl; use git_next_core::server::ListenUrl;
use kameo::actor::{pubsub::PubSub, ActorRef};
use kxio::{fs::FileSystem, net::Net};
pub fn has_all_valid_remote_defaults( pub fn has_all_valid_remote_defaults(
open_repository: &mut MockOpenRepositoryLike, open_repository: &mut MockOpenRepositoryLike,
@ -66,7 +73,7 @@ pub fn a_name() -> String {
let one_char = || CHARSET[rng.gen_range(0..CHARSET.len())] as char; let one_char = || CHARSET[rng.gen_range(0..CHARSET.len())] as char;
iter::repeat_with(one_char).take(len).collect() iter::repeat_with(one_char).take(len).collect()
} }
generate(5) generate(7)
} }
pub fn maybe_a_number() -> Option<u32> { pub fn maybe_a_number() -> Option<u32> {
@ -197,6 +204,7 @@ pub fn a_repo_actor(
repo_details: RepoDetails, repo_details: RepoDetails,
repository_factory: Box<dyn RepositoryFactory>, repository_factory: Box<dyn RepositoryFactory>,
forge: Box<dyn ForgeLike>, forge: Box<dyn ForgeLike>,
server_actor: ActorRef<ServerActor>,
net: kxio::net::Net, net: kxio::net::Net,
) -> (RepoActor, ActorLog) { ) -> (RepoActor, ActorLog) {
let listen_url = given::a_listen_url(); let listen_url = given::a_listen_url();
@ -205,6 +213,7 @@ pub fn a_repo_actor(
let actors_log = log.clone(); let actors_log = log.clone();
( (
RepoActor::new( RepoActor::new(
false,
repo_details, repo_details,
forge, forge,
listen_url, listen_url,
@ -213,13 +222,34 @@ pub fn a_repo_actor(
repository_factory, repository_factory,
std::time::Duration::from_nanos(1), std::time::Duration::from_nanos(1),
None, None,
None, server_actor,
) )
.with_log(actors_log), .with_log(actors_log),
log, log,
) )
} }
pub fn a_server_actor(fs: FileSystem, net: Net) -> ActorRef<ServerActor> {
let alerts_actor_ref = kameo::spawn(AlertsActor::new(
None,
History::new(Duration::from_secs(0)),
net.clone(),
));
let server_updates_actor_ref = kameo::spawn(PubSub::<ServerUpdate>::new());
let repo_factor = Box::new(MockRepositoryFactory::new());
let duration = Duration::from_secs(0);
let server_actor = ServerActor::new(
false, // ui
fs,
net,
alerts_actor_ref,
server_updates_actor_ref,
repo_factor,
duration,
);
kameo::spawn(server_actor)
}
pub fn a_hostname() -> Hostname { pub fn a_hostname() -> Hostname {
Hostname::new(given::a_name()) Hostname::new(given::a_name())
} }

View file

@ -1,7 +1,9 @@
use crate::{repo::messages::AdvanceMain, tell};
// //
use super::*; use super::*;
#[actix::test] #[tokio::test]
async fn when_repo_config_should_fetch_then_push_then_revalidate() -> TestResult { async fn when_repo_config_should_fetch_then_push_then_revalidate() -> TestResult {
//given //given
let fs = given::a_filesystem(); let fs = given::a_filesystem();
@ -37,21 +39,16 @@ async fn when_repo_config_should_fetch_then_push_then_revalidate() -> TestResult
repo_details, repo_details,
given::a_forge(), given::a_forge(),
); );
addr.send(crate::repo::messages::AdvanceMain::new(next_commit.clone())) tell!(addr, AdvanceMain::new(next_commit.clone()))?;
.await?;
System::current().stop();
//then //then
tracing::debug!(?log, ""); tracing::debug!(?log, "");
log.read().map_err(|e| e.to_string()).map(|l| { log.require_message_containing("send: LoadConfigFromRepo")
assert!(l .await?;
.iter()
.any(|message| message.contains("send: LoadConfigFromRepo")));
})?;
Ok(()) Ok(())
} }
#[actix::test] #[test_log::test(tokio::test)]
async fn when_app_config_should_fetch_then_push_then_revalidate() -> TestResult { async fn when_app_config_should_fetch_then_push_then_revalidate() -> TestResult {
//given //given
let fs = given::a_filesystem(); let fs = given::a_filesystem();
@ -87,12 +84,10 @@ async fn when_app_config_should_fetch_then_push_then_revalidate() -> TestResult
repo_details, repo_details,
given::a_forge(), given::a_forge(),
); );
addr.send(crate::repo::messages::AdvanceMain::new(next_commit.clone())) tell!(addr, AdvanceMain::new(next_commit.clone()))?;
.await?;
System::current().stop();
//then //then
tracing::debug!(?log, ""); tracing::debug!(?log, "log");
log.require_message_containing("send: ValidateRepo")?; log.require_message_containing("send: ValidateRepo").await?;
Ok(()) Ok(())
} }

View file

@ -1,11 +1,14 @@
use std::time::Duration; use std::time::Duration;
use crate::repo::messages::AdvanceNextPayload; use crate::{
repo::messages::{AdvanceNext, AdvanceNextPayload},
tell,
};
// //
use super::*; use super::*;
#[test_log::test(actix::test)] #[test_log::test(tokio::test)]
async fn should_fetch_then_push_then_revalidate() -> TestResult { async fn should_fetch_then_push_then_revalidate() -> TestResult {
//given //given
let fs = given::a_filesystem(); let fs = given::a_filesystem();
@ -40,19 +43,18 @@ async fn should_fetch_then_push_then_revalidate() -> TestResult {
repo_details, repo_details,
given::a_forge(), given::a_forge(),
); );
addr.send(crate::repo::messages::AdvanceNext::new( tell!(
AdvanceNextPayload { addr,
AdvanceNext::new(AdvanceNextPayload {
next: next_commit.clone(), next: next_commit.clone(),
main: next_commit.clone(), main: next_commit.clone(),
dev_commit_history, dev_commit_history,
}, })
)) )?;
.await?; tokio::time::sleep(Duration::from_millis(9)).await;
actix_rt::time::sleep(Duration::from_millis(9)).await;
System::current().stop();
//then //then
tracing::debug!(?log, ""); tracing::debug!(?log, "");
log.require_message_containing("send: ValidateRepo")?; log.require_message_containing("send: ValidateRepo").await?;
Ok(()) Ok(())
} }

View file

@ -1,18 +1,19 @@
use crate::{repo::messages::CheckCIStatus, tell};
// //
use super::*; use super::*;
#[actix::test] #[tokio::test]
async fn should_passthrough_to_receive_ci_status() -> TestResult { async fn should_passthrough_to_receive_ci_status() -> TestResult {
//given //given
let fs = given::a_filesystem(); let fs = given::a_filesystem();
let (open_repository, repo_details) = given::an_open_repository(&fs); let (open_repository, repo_details) = given::an_open_repository(&fs);
let next_commit = given::a_named_commit("next"); let next_commit = given::a_named_commit("next");
let mut forge = git::MockForgeLike::new(); let mut forge = git::MockForgeLike::new();
when::commit_status( forge
&mut forge, .expect_commit_status()
next_commit.clone(), .with(mockall::predicate::eq(next_commit.clone()))
git::forge::commit::Status::Pass, .return_once(|_| Ok(git::forge::commit::Status::Pass));
);
//when //when
let (addr, log) = when::start_actor_with_open_repository( let (addr, log) = when::start_actor_with_open_repository(
@ -20,18 +21,11 @@ async fn should_passthrough_to_receive_ci_status() -> TestResult {
repo_details, repo_details,
Box::new(forge), Box::new(forge),
); );
addr.send(crate::repo::messages::CheckCIStatus::new( tell!(addr, CheckCIStatus::new(next_commit.clone()))?;
next_commit.clone(),
))
.await?;
System::current().stop();
//then //then
tracing::debug!(?log, ""); tracing::debug!(?log, "");
log.read().map_err(|e| e.to_string()).map(|l| { log.require_message_containing("send: ReceiveCIStatus")
assert!(l .await?;
.iter()
.any(|message| message.contains("send: ReceiveCIStatus")));
})?;
Ok(()) Ok(())
} }

View file

@ -1,7 +1,11 @@
use kxio::net::Net;
use crate::tell;
// //
use super::*; use super::*;
#[actix::test] #[test_log::test(tokio::test)]
async fn should_clone() -> TestResult { async fn should_clone() -> TestResult {
//given //given
let fs = given::a_filesystem(); let fs = given::a_filesystem();
@ -23,13 +27,19 @@ async fn should_clone() -> TestResult {
let _ = cloned_ref.write().map(|mut l| l.push(())); let _ = cloned_ref.write().map(|mut l| l.push(()));
Ok(Box::new(open_repository)) Ok(Box::new(open_repository))
}); });
let net: Net = given::a_network().into();
//when //when
let (addr, _log) = when::start_actor(repository_factory, repo_details, given::a_forge()); let (addr, _log) = when::start_actor(
addr.send(CloneRepo::new()).await?; repository_factory,
System::current().stop(); repo_details,
given::a_forge(),
fs.as_real(),
net,
);
tell!(addr, CloneRepo::new())?;
//then //then
tick(1).await;
cloned cloned
.read() .read()
.map_err(|e| e.to_string()) .map_err(|e| e.to_string())
@ -38,7 +48,7 @@ async fn should_clone() -> TestResult {
Ok(()) Ok(())
} }
#[actix::test] #[tokio::test]
async fn should_open() -> TestResult { async fn should_open() -> TestResult {
//given //given
let fs = given::a_filesystem(); let fs = given::a_filesystem();
@ -60,13 +70,20 @@ async fn should_open() -> TestResult {
Ok(Box::new(open_repository)) Ok(Box::new(open_repository))
}); });
fs.dir(&repo_details.gitdir).create()?; fs.dir(&repo_details.gitdir).create()?;
let net: Net = given::a_network().into();
//when //when
let (addr, _log) = when::start_actor(repository_factory, repo_details, given::a_forge()); let (addr, _log) = when::start_actor(
addr.send(CloneRepo::new()).await?; repository_factory,
System::current().stop(); repo_details,
given::a_forge(),
fs.as_real(),
net,
);
tell!(addr, CloneRepo::new())?;
//then //then
tick(1).await;
opened opened
.read() .read()
.map_err(|e| e.to_string()) .map_err(|e| e.to_string())
@ -78,7 +95,7 @@ async fn should_open() -> TestResult {
/// The server config can optionally include the names of the main, next and dev /// The server config can optionally include the names of the main, next and dev
/// branches. When it doesn't we should load the `.git-next.yaml` from from the /// branches. When it doesn't we should load the `.git-next.yaml` from from the
/// repo and get the branch names from there by sending a [LoadConfigFromRepo] message. /// repo and get the branch names from there by sending a [LoadConfigFromRepo] message.
#[actix::test] #[tokio::test]
async fn when_server_has_no_repo_config_should_send_load_from_repo() -> TestResult { async fn when_server_has_no_repo_config_should_send_load_from_repo() -> TestResult {
//given //given
let fs = given::a_filesystem(); let fs = given::a_filesystem();
@ -95,21 +112,28 @@ async fn when_server_has_no_repo_config_should_send_load_from_repo() -> TestResu
let mut repository_factory = MockRepositoryFactory::new(); let mut repository_factory = MockRepositoryFactory::new();
expect::open_repository(&mut repository_factory, open_repository); expect::open_repository(&mut repository_factory, open_repository);
fs.dir(&repo_details.gitdir).create()?; fs.dir(&repo_details.gitdir).create()?;
let net: Net = given::a_network().into();
//when //when
let (addr, log) = when::start_actor(repository_factory, repo_details, given::a_forge()); let (addr, log) = when::start_actor(
addr.send(CloneRepo::new()).await?; repository_factory,
System::current().stop(); repo_details,
given::a_forge(),
fs.as_real(),
net,
);
tell!(addr, CloneRepo::new())?;
//then //then
log.require_message_containing("send: LoadConfigFromRepo")?; log.require_message_containing("send: LoadConfigFromRepo")
.await?;
Ok(()) Ok(())
} }
/// The server config can optionally include the names of the main, next and dev /// The server config can optionally include the names of the main, next and dev
/// branches. When it does we should register the webhook by sending [RegisterWebhook] message. /// branches. When it does we should register the webhook by sending [RegisterWebhook] message.
#[actix::test] #[test_log::test(tokio::test)]
async fn when_server_has_repo_config_should_send_register_webhook() -> TestResult { async fn when_server_has_repo_config_should_send_register_webhook() -> TestResult {
//given //given
let fs = given::a_filesystem(); let fs = given::a_filesystem();
@ -124,71 +148,23 @@ async fn when_server_has_repo_config_should_send_register_webhook() -> TestResul
let mut repository_factory = MockRepositoryFactory::new(); let mut repository_factory = MockRepositoryFactory::new();
expect::open_repository(&mut repository_factory, open_repository); expect::open_repository(&mut repository_factory, open_repository);
fs.dir(&repo_details.gitdir).create()?; fs.dir(&repo_details.gitdir).create()?;
let net: Net = given::a_network().into();
//when //when
let (addr, log) = when::start_actor(repository_factory, repo_details, given::a_forge()); let (addr, log) = when::start_actor(
addr.send(CloneRepo::new()).await?; repository_factory,
System::current().stop(); repo_details,
given::a_forge(),
//then fs.as_real(),
log.require_message_containing("send: RegisterWebhook")?; net,
Ok(())
}
#[test_log::test(actix::test)]
async fn opened_repo_with_no_default_push_should_not_proceed() -> TestResult {
//given
let fs = given::a_filesystem();
let (mut open_repository, repo_details) = given::an_open_repository(&fs);
open_repository
.expect_fetch()
.times(1)
.return_once(|| Ok(()));
given::has_remote_defaults(
&mut open_repository,
HashMap::from([
(Direction::Push, None),
(Direction::Fetch, repo_details.remote_url()),
]),
); );
tell!(addr, CloneRepo::new())?;
let mut repository_factory = MockRepositoryFactory::new();
expect::open_repository(&mut repository_factory, open_repository);
fs.dir(&repo_details.gitdir).create()?;
//when
let (addr, log) = when::start_actor(repository_factory, repo_details, given::a_forge());
addr.send(CloneRepo::new()).await?;
System::current().stop();
//then //then
log.require_message_containing("open failed")?; tick(1).await;
debug!(?log, "");
Ok(()) log.require_message_containing("send: RegisterWebhook")
} .await?;
#[test_log::test(actix::test)]
async fn opened_repo_with_no_default_fetch_should_not_proceed() -> TestResult {
//given
let fs = given::a_filesystem();
let (mut open_repository, repo_details) = given::an_open_repository(&fs);
open_repository
.expect_fetch()
.times(1)
.return_once(|| Err(git::fetch::Error::NoFetchRemoteFound));
let mut repository_factory = MockRepositoryFactory::new();
expect::open_repository(&mut repository_factory, open_repository);
fs.dir(&repo_details.gitdir).create()?;
//when
let (addr, log) = when::start_actor(repository_factory, repo_details, given::a_forge());
addr.send(CloneRepo::new()).await?;
System::current().stop();
//then
log.require_message_containing("open failed")?;
Ok(()) Ok(())
} }

View file

@ -1,17 +1,17 @@
use crate::{repo::messages::LoadConfigFromRepo, tell};
// //
use super::*; use super::*;
#[actix::test] #[tokio::test]
async fn when_read_file_ok_should_send_config_loaded() -> TestResult { async fn when_read_file_ok_should_send_config_loaded() -> TestResult {
//given //given
let fs = given::a_filesystem(); let fs = given::a_filesystem();
let (mut open_repository, repo_details) = given::an_open_repository(&fs); let (mut open_repository, repo_details) = given::an_open_repository(&fs);
let mut load_config_open_repo = MockOpenRepositoryLike::new(); // let mut load_config_open_repo = MockOpenRepositoryLike::new();
let branches = given::repo_branches(); let branches = given::repo_branches();
let remote_branches = vec![branches.main(), branches.next(), branches.dev()]; let remote_branches = vec![branches.main(), branches.next(), branches.dev()];
load_config_open_repo open_repository.expect_read_file().return_once(move |_, _| {
.expect_read_file()
.return_once(move |_, _| {
Ok(format!( Ok(format!(
r#" r#"
[branches] [branches]
@ -25,58 +25,46 @@ async fn when_read_file_ok_should_send_config_loaded() -> TestResult {
)) ))
}); });
load_config_open_repo open_repository
.expect_remote_branches() .expect_remote_branches()
.return_once(|| Ok(remote_branches)); .return_once(|| Ok(remote_branches));
open_repository
.expect_duplicate()
.return_once(|| Box::new(load_config_open_repo));
//when //when
let (addr, log) = when::start_actor_with_open_repository( let (addr, log) = when::start_actor_with_open_repository(
Box::new(open_repository), Box::new(open_repository),
repo_details, repo_details,
given::a_forge(), given::a_forge(),
); );
addr.send(crate::repo::messages::LoadConfigFromRepo::new()) tell!(addr, LoadConfigFromRepo::new())?;
.await?;
System::current().stop();
//then //then
tracing::debug!(?log, ""); tracing::debug!(?log, "");
log.require_message_containing("send: ReceiveRepoConfig")?; log.require_message_containing("send: ReceiveRepoConfig")
log.no_message_contains("send: NotifyUsers")?; .await?;
log.no_message_contains("send: NotifyUsers").await?;
Ok(()) Ok(())
} }
#[actix::test] #[tokio::test]
async fn when_read_file_err_should_notify_user() -> TestResult { async fn when_read_file_err_should_notify_user() -> TestResult {
//given //given
let fs = given::a_filesystem(); let fs = given::a_filesystem();
let (mut open_repository, repo_details) = given::an_open_repository(&fs); let (mut open_repository, repo_details) = given::an_open_repository(&fs);
let mut load_config_open_repo = MockOpenRepositoryLike::new(); open_repository
load_config_open_repo
.expect_read_file() .expect_read_file()
.return_once(move |_, _| Err(git::file::Error::FileNotFound)); .return_once(move |_, _| Err(git::file::Error::FileNotFound));
open_repository
.expect_duplicate()
.return_once(|| Box::new(load_config_open_repo));
//when //when
let (addr, log) = when::start_actor_with_open_repository( let (addr, log) = when::start_actor_with_open_repository(
Box::new(open_repository), Box::new(open_repository),
repo_details, repo_details,
given::a_forge(), given::a_forge(),
); );
addr.send(crate::repo::messages::LoadConfigFromRepo::new()) tell!(addr, LoadConfigFromRepo::new())?;
.await?;
System::current().stop();
//then //then
tracing::debug!(?log, ""); tracing::debug!(?log, "");
log.require_message_containing("send: NotifyUser")?; log.require_message_containing("send: NotifyUser").await?;
log.no_message_contains("send: ReceiveRepoConfig")?; log.no_message_contains("send: ReceiveRepoConfig").await?;
Ok(()) Ok(())
} }

View file

@ -1,7 +1,9 @@
// //
use crate::{repo::messages::ReceiveRepoConfig, tell};
use super::*; use super::*;
#[actix::test] #[tokio::test]
async fn should_store_repo_config_in_actor() -> TestResult { async fn should_store_repo_config_in_actor() -> TestResult {
//given //given
let fs = given::a_filesystem(); let fs = given::a_filesystem();
@ -15,16 +17,15 @@ async fn should_store_repo_config_in_actor() -> TestResult {
repo_details, repo_details,
given::a_forge(), given::a_forge(),
); );
addr.send(crate::repo::messages::ReceiveRepoConfig::new( tell!(addr, ReceiveRepoConfig::new(new_repo_config.clone()))?;
new_repo_config.clone(),
))
.await?;
System::current().stop();
//then //then
tracing::debug!(?log, ""); tracing::debug!(?log, "");
let reo_actor_view = addr.send(ExamineActor).await?; let reo_actor_view = addr
.ask(ExamineActor)
.await
.map_err(|e| format!("examine actor: {e:?}"))?;
assert_eq!( assert_eq!(
reo_actor_view.repo_details.repo_config, reo_actor_view.repo_details.repo_config,
Some(new_repo_config) Some(new_repo_config)
@ -32,7 +33,7 @@ async fn should_store_repo_config_in_actor() -> TestResult {
Ok(()) Ok(())
} }
#[test_log::test(actix::test)] #[test_log::test(tokio::test)]
async fn should_register_webhook() -> TestResult { async fn should_register_webhook() -> TestResult {
//given //given
let fs = given::a_filesystem(); let fs = given::a_filesystem();
@ -46,14 +47,11 @@ async fn should_register_webhook() -> TestResult {
repo_details, repo_details,
given::a_forge(), given::a_forge(),
); );
addr.send(crate::repo::messages::ReceiveRepoConfig::new( tell!(addr, ReceiveRepoConfig::new(new_repo_config.clone()))?;
new_repo_config.clone(),
))
.await?;
System::current().stop();
//then //then
tracing::debug!(?log, ""); tracing::debug!(?log, "");
log.require_message_containing("send: RegisterWebhook")?; log.require_message_containing("send: RegisterWebhook")
.await?;
Ok(()) Ok(())
} }

View file

@ -1,9 +1,13 @@
use std::time::Duration; use std::time::Duration;
use git::forge::commit::Status;
use crate::{repo::messages::ReceiveCIStatus, tell};
// //
use super::*; use super::*;
#[test_log::test(actix::test)] #[test_log::test(tokio::test)]
async fn when_pass_should_advance_main_to_next() -> TestResult { async fn when_pass_should_advance_main_to_next() -> TestResult {
//given //given
let fs = given::a_filesystem(); let fs = given::a_filesystem();
@ -16,24 +20,19 @@ async fn when_pass_should_advance_main_to_next() -> TestResult {
repo_details, repo_details,
given::a_forge(), given::a_forge(),
); );
addr.send(crate::repo::messages::ReceiveCIStatus::new(( tell!(
next_commit.clone(), addr,
git::forge::commit::Status::Pass, ReceiveCIStatus::new((next_commit.clone(), Status::Pass))
))) )?;
.await?;
System::current().stop();
//then //then
tracing::debug!(?log, ""); tracing::debug!(?log, "");
log.read().map_err(|e| e.to_string()).map(|l| { log.require_message_containing(format!("send: AdvanceMain({next_commit:?})"))
let expected = format!("send: AdvanceMain({next_commit:?})"); .await?;
tracing::debug!(%expected,"");
assert!(l.iter().any(|message| message.contains(&expected)));
})?;
Ok(()) Ok(())
} }
#[test_log::test(actix::test)] #[test_log::test(tokio::test)]
async fn when_pending_should_recheck_ci_status() -> TestResult { async fn when_pending_should_recheck_ci_status() -> TestResult {
//given //given
let fs = given::a_filesystem(); let fs = given::a_filesystem();
@ -46,21 +45,19 @@ async fn when_pending_should_recheck_ci_status() -> TestResult {
repo_details, repo_details,
given::a_forge(), given::a_forge(),
); );
addr.send(crate::repo::messages::ReceiveCIStatus::new(( tell!(
next_commit.clone(), addr,
git::forge::commit::Status::Pending, ReceiveCIStatus::new((next_commit.clone(), Status::Pending))
))) )?;
.await?; tokio::time::sleep(Duration::from_millis(9)).await;
actix_rt::time::sleep(Duration::from_millis(9)).await;
System::current().stop();
//then //then
tracing::debug!(?log, ""); tracing::debug!(?log, "");
log.require_message_containing("send: ValidateRepo")?; log.require_message_containing("send: ValidateRepo").await?;
Ok(()) Ok(())
} }
#[test_log::test(actix::test)] #[test_log::test(tokio::test)]
async fn when_fail_should_recheck_after_delay() -> TestResult { async fn when_fail_should_recheck_after_delay() -> TestResult {
//given //given
let fs = given::a_filesystem(); let fs = given::a_filesystem();
@ -73,20 +70,18 @@ async fn when_fail_should_recheck_after_delay() -> TestResult {
repo_details, repo_details,
given::a_forge(), given::a_forge(),
); );
addr.send(crate::repo::messages::ReceiveCIStatus::new(( tell!(
next_commit.clone(), addr,
git::forge::commit::Status::Fail, ReceiveCIStatus::new((next_commit.clone(), Status::Fail))
))) )?;
.await?; tokio::time::sleep(Duration::from_millis(9)).await;
actix_rt::time::sleep(Duration::from_millis(9)).await;
System::current().stop();
//then //then
log.require_message_containing("send: ValidateRepo")?; log.require_message_containing("send: ValidateRepo").await?;
Ok(()) Ok(())
} }
#[test_log::test(actix::test)] #[test_log::test(tokio::test)]
async fn when_fail_should_notify_user() -> TestResult { async fn when_fail_should_notify_user() -> TestResult {
//given //given
let fs = given::a_filesystem(); let fs = given::a_filesystem();
@ -99,14 +94,12 @@ async fn when_fail_should_notify_user() -> TestResult {
repo_details, repo_details,
given::a_forge(), given::a_forge(),
); );
addr.send(crate::repo::messages::ReceiveCIStatus::new(( tell!(
next_commit.clone(), addr,
git::forge::commit::Status::Fail, ReceiveCIStatus::new((next_commit.clone(), Status::Fail))
))) )?;
.await?;
System::current().stop();
//then //then
log.require_message_containing("send: NotifyUser")?; log.require_message_containing("send: NotifyUser").await?;
Ok(()) Ok(())
} }

View file

@ -1,71 +1,57 @@
use crate::{repo::messages::RegisterWebhook, tell};
// //
use super::*; use super::*;
#[actix::test] #[tokio::test]
async fn when_registered_ok_should_send_webhook_registered() -> TestResult { async fn when_registered_ok_should_send_webhook_registered() -> TestResult {
//given //given
let fs = given::a_filesystem(); let fs = given::a_filesystem();
let (open_repository, repo_details) = given::an_open_repository(&fs); let (open_repository, repo_details) = given::an_open_repository(&fs);
let registered_webhook = given::a_registered_webhook(); let registered_webhook = given::a_registered_webhook();
let mut my_forge = git::MockForgeLike::new(); let mut forge = git::MockForgeLike::new();
my_forge forge
.expect_register_webhook() .expect_register_webhook()
.return_once(move |_| Ok(registered_webhook)); .return_once(move |_| Ok(registered_webhook));
let mut forge = git::MockForgeLike::new();
forge.expect_duplicate().return_once(|| Box::new(my_forge));
//when //when
let (addr, log) = when::start_actor_with_open_repository( let (addr, log) = when::start_actor_with_open_repository(
Box::new(open_repository), Box::new(open_repository),
repo_details, repo_details,
Box::new(forge), Box::new(forge),
); );
addr.send(crate::repo::messages::RegisterWebhook::new()) tell!(addr, RegisterWebhook::new())?;
.await?;
System::current().stop();
//then //then
tracing::debug!(?log, ""); tracing::debug!(?log, "");
log.read().map_err(|e| e.to_string()).map(|l| { log.require_message_containing("send: WebhookRegistered")
assert!(l .await?;
.iter()
.any(|message| message.contains("send: WebhookRegistered")));
})?;
Ok(()) Ok(())
} }
#[actix::test] #[tokio::test]
async fn when_registered_error_should_send_notify_user() -> TestResult { async fn when_registered_error_should_send_notify_user() -> TestResult {
//given //given
let fs = given::a_filesystem(); let fs = given::a_filesystem();
let (open_repository, repo_details) = given::an_open_repository(&fs); let (open_repository, repo_details) = given::an_open_repository(&fs);
let mut my_forge = git::MockForgeLike::new(); let mut forge = git::MockForgeLike::new();
my_forge.expect_register_webhook().return_once(move |_| { forge.expect_register_webhook().return_once(move |_| {
Err(git::forge::webhook::Error::FailedToRegister( Err(git::forge::webhook::Error::FailedToRegister(
"foo".to_string(), "foo".to_string(),
)) ))
}); });
let mut forge = git::MockForgeLike::new();
forge.expect_duplicate().return_once(|| Box::new(my_forge));
//when //when
let (addr, log) = when::start_actor_with_open_repository( let (addr, log) = when::start_actor_with_open_repository(
Box::new(open_repository), Box::new(open_repository),
repo_details, repo_details,
Box::new(forge), Box::new(forge),
); );
addr.send(crate::repo::messages::RegisterWebhook::new()) tell!(addr, crate::repo::messages::RegisterWebhook::new())?;
.await?;
System::current().stop();
//then //then
tracing::debug!(?log, ""); tracing::debug!(?log, "");
log.read() log.require_message_containing("send: NotifyUser").await?;
.map_err(|e| e.to_string())
.map(|l| assert!(l.iter().any(|message| message.contains("send: NotifyUser"))))?;
Ok(()) Ok(())
} }

View file

@ -1,9 +1,14 @@
use crate::repo::messages::{AdvanceNext, AdvanceNextPayload};
// //
use kxio::net::Net;
use crate::{
repo::messages::{AdvanceNext, AdvanceNextPayload, ValidateRepo},
tell,
};
use super::*; use super::*;
#[test_log::test(actix::test)] #[test_log::test(tokio::test)]
async fn repo_with_next_not_an_ancestor_of_dev_and_dev_on_main_should_be_reset_to_main( async fn repo_with_next_not_an_ancestor_of_dev_and_dev_on_main_should_be_reset_to_main(
) -> TestResult { ) -> TestResult {
//given //given
@ -41,18 +46,15 @@ async fn repo_with_next_not_an_ancestor_of_dev_and_dev_on_main_should_be_reset_t
repo_details, repo_details,
given::a_forge(), given::a_forge(),
); );
addr.send(crate::repo::messages::ValidateRepo::new( tell!(addr, ValidateRepo::new(MessageToken::default()))?;
MessageToken::default(),
))
.await?;
System::current().stop();
//then //then
log.require_message_containing(format!("Branch {} has been reset", branches.next()))?; log.require_message_containing(format!("Branch {} has been reset", branches.next()))
.await?;
Ok(()) Ok(())
} }
#[test_log::test(actix::test)] #[test_log::test(tokio::test)]
async fn repo_with_next_not_an_ancestor_of_dev_and_dev_ahead_of_main_should_be_reset_to_dev( async fn repo_with_next_not_an_ancestor_of_dev_and_dev_ahead_of_main_should_be_reset_to_dev(
) -> TestResult { ) -> TestResult {
//given //given
@ -92,11 +94,10 @@ async fn repo_with_next_not_an_ancestor_of_dev_and_dev_ahead_of_main_should_be_r
repo_details, repo_details,
given::a_forge(), given::a_forge(),
); );
addr.send(crate::repo::messages::ValidateRepo::new( tell!(
MessageToken::default(), addr,
)) crate::repo::messages::ValidateRepo::new(MessageToken::default())
.await?; )?;
System::current().stop();
//then //then
let expected = AdvanceNext::new(AdvanceNextPayload { let expected = AdvanceNext::new(AdvanceNextPayload {
@ -104,11 +105,12 @@ async fn repo_with_next_not_an_ancestor_of_dev_and_dev_ahead_of_main_should_be_r
main: main_commit, main: main_commit,
dev_commit_history: dev_branch_log, dev_commit_history: dev_branch_log,
}); });
log.require_message_containing(format!("send: {expected:?}",))?; log.require_message_containing(format!("send: {expected:?}",))
.await?;
Ok(()) Ok(())
} }
#[test_log::test(actix::test)] #[test_log::test(tokio::test)]
async fn repo_with_next_not_on_or_near_main_should_be_reset() -> TestResult { async fn repo_with_next_not_on_or_near_main_should_be_reset() -> TestResult {
//given //given
let fs = given::a_filesystem(); let fs = given::a_filesystem();
@ -145,18 +147,15 @@ async fn repo_with_next_not_on_or_near_main_should_be_reset() -> TestResult {
repo_details, repo_details,
given::a_forge(), given::a_forge(),
); );
addr.send(crate::repo::messages::ValidateRepo::new( tell!(addr, ValidateRepo::new(MessageToken::default()))?;
MessageToken::default(),
))
.await?;
System::current().stop();
//then //then
log.require_message_containing(format!("Branch {} has been reset", branches.next()))?; log.require_message_containing(format!("Branch {} has been reset", branches.next()))
.await?;
Ok(()) Ok(())
} }
#[test_log::test(actix::test)] #[test_log::test(tokio::test)]
async fn repo_with_next_not_based_on_main_should_be_reset() -> TestResult { async fn repo_with_next_not_based_on_main_should_be_reset() -> TestResult {
//given //given
let fs = given::a_filesystem(); let fs = given::a_filesystem();
@ -193,18 +192,15 @@ async fn repo_with_next_not_based_on_main_should_be_reset() -> TestResult {
repo_details, repo_details,
given::a_forge(), given::a_forge(),
); );
addr.send(crate::repo::messages::ValidateRepo::new( tell!(addr, ValidateRepo::new(MessageToken::default()))?;
MessageToken::default(),
))
.await?;
System::current().stop();
//then //then
log.require_message_containing(format!("Branch {} has been reset", branches.next()))?; log.require_message_containing(format!("Branch {} has been reset", branches.next()))
.await?;
Ok(()) Ok(())
} }
#[test_log::test(actix::test)] #[test_log::test(tokio::test)]
async fn repo_with_next_ahead_of_main_should_check_ci_status() -> TestResult { async fn repo_with_next_ahead_of_main_should_check_ci_status() -> TestResult {
//given //given
let fs = given::a_filesystem(); let fs = given::a_filesystem();
@ -240,18 +236,15 @@ async fn repo_with_next_ahead_of_main_should_check_ci_status() -> TestResult {
repo_details, repo_details,
given::a_forge(), given::a_forge(),
); );
addr.send(crate::repo::messages::ValidateRepo::new( tell!(addr, ValidateRepo::new(MessageToken::default()))?;
MessageToken::default(),
))
.await?;
System::current().stop();
//then //then
log.require_message_containing(format!("send: CheckCIStatus({next_commit:?})"))?; log.require_message_containing(format!("send: CheckCIStatus({next_commit:?})"))
.await?;
Ok(()) Ok(())
} }
#[test_log::test(actix::test)] #[test_log::test(tokio::test)]
async fn repo_with_dev_and_next_on_main_should_do_nothing() -> TestResult { async fn repo_with_dev_and_next_on_main_should_do_nothing() -> TestResult {
// Do nothing, when the situation changes we will hear about it via a webhook // Do nothing, when the situation changes we will hear about it via a webhook
//given //given
@ -288,18 +281,14 @@ async fn repo_with_dev_and_next_on_main_should_do_nothing() -> TestResult {
repo_details, repo_details,
given::a_forge(), given::a_forge(),
); );
addr.send(crate::repo::messages::ValidateRepo::new( tell!(addr, ValidateRepo::new(MessageToken::default()))?;
MessageToken::default(),
))
.await?;
System::current().stop();
//then //then
log.no_message_contains("send:")?; log.no_message_contains("send:").await?;
Ok(()) Ok(())
} }
#[test_log::test(actix::test)] #[test_log::test(tokio::test)]
async fn repo_with_dev_ahead_of_next_should_advance_next() -> TestResult { async fn repo_with_dev_ahead_of_next_should_advance_next() -> TestResult {
//given //given
let fs = given::a_filesystem(); let fs = given::a_filesystem();
@ -337,11 +326,7 @@ async fn repo_with_dev_ahead_of_next_should_advance_next() -> TestResult {
repo_details, repo_details,
given::a_forge(), given::a_forge(),
); );
addr.send(crate::repo::messages::ValidateRepo::new( tell!(addr, ValidateRepo::new(MessageToken::default()))?;
MessageToken::default(),
))
.await?;
System::current().stop();
//then //then
let expected = AdvanceNext::new(AdvanceNextPayload { let expected = AdvanceNext::new(AdvanceNextPayload {
@ -349,11 +334,12 @@ async fn repo_with_dev_ahead_of_next_should_advance_next() -> TestResult {
main: main_commit, main: main_commit,
dev_commit_history: dev_branch_log, dev_commit_history: dev_branch_log,
}); });
log.require_message_containing(format!("send: {expected:?}"))?; log.require_message_containing(format!("send: {expected:?}"))
.await?;
Ok(()) Ok(())
} }
#[test_log::test(actix::test)] #[test_log::test(tokio::test)]
async fn repo_with_dev_not_ahead_of_main_should_notify_user() -> TestResult { async fn repo_with_dev_not_ahead_of_main_should_notify_user() -> TestResult {
//given //given
let fs = given::a_filesystem(); let fs = given::a_filesystem();
@ -391,96 +377,87 @@ async fn repo_with_dev_not_ahead_of_main_should_notify_user() -> TestResult {
repo_details, repo_details,
given::a_forge(), given::a_forge(),
); );
addr.send(crate::repo::messages::ValidateRepo::new( tell!(addr, ValidateRepo::new(MessageToken::default()))?;
MessageToken::default(),
))
.await?;
System::current().stop();
//then //then
log.require_message_containing("send: NotifyUser")?; log.require_message_containing("send: NotifyUser").await?;
Ok(()) Ok(())
} }
#[test_log::test(actix::test)] #[test_log::test(tokio::test)]
async fn should_accept_message_with_current_token() -> TestResult { async fn should_accept_message_with_current_token() -> TestResult {
//given //given
let fs = given::a_filesystem(); let fs = given::a_filesystem();
let repo_details = given::repo_details(&fs); let repo_details = given::repo_details(&fs);
let net: Net = given::a_network().into();
//when let server_actor_ref = given::a_server_actor(fs.as_real(), net.clone());
let (actor, log) = given::a_repo_actor( let (actor, log) = given::a_repo_actor(
repo_details, repo_details,
git::repository::factory::mock(), git::repository::factory::mock(),
given::a_forge(), given::a_forge(),
given::a_network().into(), server_actor_ref,
net,
); );
let actor = actor.with_message_token(MessageToken::new(2_u32)); let actor = actor.with_message_token(MessageToken::new(2_u32));
let addr = actor.start(); let addr = kameo::spawn(actor);
addr.send(crate::repo::messages::ValidateRepo::new(MessageToken::new( //
2_u32, //when
))) tell!(addr, ValidateRepo::new(MessageToken::new(2_u32)))?;
.await?;
System::current().stop();
//then //then
log.require_message_containing("accepted token: 2")?; log.require_message_containing("accepted token: 2").await?;
Ok(()) Ok(())
} }
#[test_log::test(actix::test)] #[test_log::test(tokio::test)]
async fn should_accept_message_with_new_token() -> TestResult { async fn should_accept_message_with_new_token() -> TestResult {
//given //given
let fs = given::a_filesystem(); let fs = given::a_filesystem();
let repo_details = given::repo_details(&fs); let repo_details = given::repo_details(&fs);
let net: Net = given::a_network().into();
//when //when
let (actor, log) = given::a_repo_actor( let (actor, log) = given::a_repo_actor(
repo_details, repo_details,
git::repository::factory::mock(), git::repository::factory::mock(),
given::a_forge(), given::a_forge(),
given::a_network().into(), given::a_server_actor(fs.as_real(), net.clone()),
net,
); );
let actor = actor.with_message_token(MessageToken::new(2_u32)); let actor = actor.with_message_token(MessageToken::new(2_u32));
let addr = actor.start(); let addr = kameo::spawn(actor);
addr.send(crate::repo::messages::ValidateRepo::new(MessageToken::new( tell!(addr, ValidateRepo::new(MessageToken::new(3_u32)))?;
3_u32,
)))
.await?;
System::current().stop();
//then //then
log.require_message_containing("accepted token: 3")?; log.require_message_containing("accepted token: 3").await?;
Ok(()) Ok(())
} }
#[test_log::test(actix::test)] #[test_log::test(tokio::test)]
async fn should_reject_message_with_expired_token() -> TestResult { async fn should_reject_message_with_expired_token() -> TestResult {
//given //given
let fs = given::a_filesystem(); let fs = given::a_filesystem();
let repo_details = given::repo_details(&fs); let repo_details = given::repo_details(&fs);
let net: Net = given::a_network().into();
//when //when
let (actor, log) = given::a_repo_actor( let (actor, log) = given::a_repo_actor(
repo_details, repo_details,
git::repository::factory::mock(), git::repository::factory::mock(),
given::a_forge(), given::a_forge(),
given::a_network().into(), given::a_server_actor(fs.as_real(), net.clone()),
net,
); );
let actor = actor.with_message_token(MessageToken::new(4_u32)); let actor = actor.with_message_token(MessageToken::new(4_u32));
let addr = actor.start(); let addr = kameo::spawn(actor);
addr.send(crate::repo::messages::ValidateRepo::new(MessageToken::new( tell!(addr, ValidateRepo::new(MessageToken::new(3_u32)))?;
3_u32,
)))
.await?;
System::current().stop();
//then //then
log.no_message_contains("accepted token")?; log.no_message_contains("accepted token").await?;
Ok(()) Ok(())
} }
#[test_log::test(actix::test)] #[test_log::test(tokio::test)]
// NOTE: failed then passed on retry: count = 6 // NOTE: failed then passed on retry: count = 6
async fn should_send_validate_repo_when_retryable_error() -> TestResult { async fn should_send_validate_repo_when_retryable_error() -> TestResult {
//given //given
@ -497,20 +474,16 @@ async fn should_send_validate_repo_when_retryable_error() -> TestResult {
repo_details, repo_details,
given::a_forge(), given::a_forge(),
); );
addr.send(crate::repo::messages::ValidateRepo::new( tell!(addr, ValidateRepo::new(MessageToken::default()))?;
MessageToken::default(), tokio::time::sleep(std::time::Duration::from_millis(2)).await;
))
.await?;
actix_rt::time::sleep(std::time::Duration::from_millis(2)).await;
System::current().stop();
//then //then
log.require_message_containing("accepted token: 0")?; log.require_message_containing("accepted token: 0").await?;
log.require_message_containing("send: ValidateRepo")?; log.require_message_containing("send: ValidateRepo").await?;
Ok(()) Ok(())
} }
#[test_log::test(actix::test)] #[test_log::test(tokio::test)]
async fn should_send_notify_user_when_non_retryable_error() -> TestResult { async fn should_send_notify_user_when_non_retryable_error() -> TestResult {
//given //given
let fs = given::a_filesystem(); let fs = given::a_filesystem();
@ -546,15 +519,11 @@ async fn should_send_notify_user_when_non_retryable_error() -> TestResult {
repo_details, repo_details,
given::a_forge(), given::a_forge(),
); );
addr.send(crate::repo::messages::ValidateRepo::new( tell!(addr, ValidateRepo::new(MessageToken::default()))?;
MessageToken::default(), tokio::time::sleep(std::time::Duration::from_millis(1)).await;
))
.await?;
actix_rt::time::sleep(std::time::Duration::from_millis(1)).await;
System::current().stop();
//then //then
log.require_message_containing("accepted token")?; log.require_message_containing("accepted token").await?;
log.require_message_containing("send: NotifyUser")?; log.require_message_containing("send: NotifyUser").await?;
Ok(()) Ok(())
} }

View file

@ -1,7 +1,11 @@
use kxio::net::Net;
use crate::{repo::messages::WebhookNotification, tell};
// //
use super::*; use super::*;
#[actix::test] #[tokio::test]
async fn when_no_expected_auth_token_drop_notification() -> TestResult { async fn when_no_expected_auth_token_drop_notification() -> TestResult {
//given //given
let fs = given::a_filesystem(); let fs = given::a_filesystem();
@ -12,30 +16,30 @@ async fn when_no_expected_auth_token_drop_notification() -> TestResult {
let body = Body::new(String::new()); let body = Body::new(String::new());
let forge_notification = ForgeNotification::new(forge_alias, repo_alias, headers, body); let forge_notification = ForgeNotification::new(forge_alias, repo_alias, headers, body);
let repository_factory = MockRepositoryFactory::new(); let repository_factory = MockRepositoryFactory::new();
let net: Net = given::a_network().into();
let (actor, log) = given::a_repo_actor( let (actor, log) = given::a_repo_actor(
repo_details, repo_details,
Box::new(repository_factory), Box::new(repository_factory),
given::a_forge(), given::a_forge(),
given::a_network().into(), given::a_server_actor(fs.as_real(), net.clone()),
net,
); );
let actor = actor.with_webhook_auth(None); let actor = actor.with_webhook_auth(None);
//when //when
actor tell!(
.start() kameo::spawn(actor),
.send(crate::repo::messages::WebhookNotification::new( WebhookNotification::new(forge_notification)
forge_notification, )?;
))
.await?;
System::current().stop();
//then //then
log.no_message_contains("send")?; log.no_message_contains("send").await?;
log.require_message_containing("server has no auth token")?; log.require_message_containing("server has no auth token")
.await?;
Ok(()) Ok(())
} }
#[actix::test] #[tokio::test]
async fn when_no_repo_config_drop_notification() -> TestResult { async fn when_no_repo_config_drop_notification() -> TestResult {
//given //given
let fs = given::a_filesystem(); let fs = given::a_filesystem();
@ -46,30 +50,30 @@ async fn when_no_repo_config_drop_notification() -> TestResult {
let body = Body::new(String::new()); let body = Body::new(String::new());
let forge_notification = ForgeNotification::new(forge_alias, repo_alias, headers, body); let forge_notification = ForgeNotification::new(forge_alias, repo_alias, headers, body);
let repository_factory = MockRepositoryFactory::new(); let repository_factory = MockRepositoryFactory::new();
let net: Net = given::a_network().into();
let (actor, log) = given::a_repo_actor( let (actor, log) = given::a_repo_actor(
repo_details, repo_details,
Box::new(repository_factory), Box::new(repository_factory),
given::a_forge(), given::a_forge(),
given::a_network().into(), given::a_server_actor(fs.as_real(), net.clone()),
net,
); );
let actor = actor.with_webhook_auth(Some(given::a_webhook_auth())); let actor = actor.with_webhook_auth(Some(given::a_webhook_auth()));
//when //when
actor tell!(
.start() kameo::spawn(actor),
.send(crate::repo::messages::WebhookNotification::new( WebhookNotification::new(forge_notification)
forge_notification, )?;
))
.await?;
System::current().stop();
//then //then
log.no_message_contains("send")?; log.no_message_contains("send").await?;
log.require_message_containing("server has no repo config")?; log.require_message_containing("server has no repo config")
.await?;
Ok(()) Ok(())
} }
#[actix::test] #[tokio::test]
async fn when_message_auth_is_invalid_drop_notification() -> TestResult { async fn when_message_auth_is_invalid_drop_notification() -> TestResult {
//given //given
let fs = given::a_filesystem(); let fs = given::a_filesystem();
@ -84,30 +88,30 @@ async fn when_message_auth_is_invalid_drop_notification() -> TestResult {
forge forge
.expect_is_message_authorised() .expect_is_message_authorised()
.return_once(|_, _| false); // is not valid .return_once(|_, _| false); // is not valid
let net: Net = given::a_network().into();
let (actor, log) = given::a_repo_actor( let (actor, log) = given::a_repo_actor(
repo_details, repo_details,
Box::new(repository_factory), Box::new(repository_factory),
forge, forge,
given::a_network().into(), given::a_server_actor(fs.as_real(), net.clone()),
net,
); );
let actor = actor.with_webhook_auth(Some(given::a_webhook_auth())); let actor = actor.with_webhook_auth(Some(given::a_webhook_auth()));
//when //when
actor tell!(
.start() kameo::spawn(actor),
.send(crate::repo::messages::WebhookNotification::new( WebhookNotification::new(forge_notification)
forge_notification, )?;
))
.await?;
System::current().stop();
//then //then
log.no_message_contains("send")?; log.no_message_contains("send").await?;
log.require_message_containing("message authorisation is invalid")?; log.require_message_containing("message authorisation is invalid")
.await?;
Ok(()) Ok(())
} }
#[actix::test] #[tokio::test]
async fn when_message_is_ignorable_drop_notification() -> TestResult { async fn when_message_is_ignorable_drop_notification() -> TestResult {
//given //given
let fs = given::a_filesystem(); let fs = given::a_filesystem();
@ -126,30 +130,30 @@ async fn when_message_is_ignorable_drop_notification() -> TestResult {
forge forge
.expect_parse_webhook_body() .expect_parse_webhook_body()
.return_once(|_| Err(git::forge::webhook::Error::NetworkResponseEmpty)); .return_once(|_| Err(git::forge::webhook::Error::NetworkResponseEmpty));
let net: Net = given::a_network().into();
let (actor, log) = given::a_repo_actor( let (actor, log) = given::a_repo_actor(
repo_details, repo_details,
Box::new(repository_factory), Box::new(repository_factory),
forge, forge,
given::a_network().into(), given::a_server_actor(fs.as_real(), net.clone()),
net,
); );
let actor = actor.with_webhook_auth(Some(given::a_webhook_auth())); let actor = actor.with_webhook_auth(Some(given::a_webhook_auth()));
//when //when
actor tell!(
.start() kameo::spawn(actor),
.send(crate::repo::messages::WebhookNotification::new( WebhookNotification::new(forge_notification)
forge_notification, )?;
))
.await?;
System::current().stop();
//then //then
log.no_message_contains("send")?; log.no_message_contains("send").await?;
log.require_message_containing("forge sent ignorable message")?; log.require_message_containing("forge sent ignorable message")
.await?;
Ok(()) Ok(())
} }
#[actix::test] #[tokio::test]
async fn when_message_is_not_a_push_drop_notification() -> TestResult { async fn when_message_is_not_a_push_drop_notification() -> TestResult {
//given //given
let fs = given::a_filesystem(); let fs = given::a_filesystem();
@ -168,30 +172,30 @@ async fn when_message_is_not_a_push_drop_notification() -> TestResult {
forge forge
.expect_parse_webhook_body() .expect_parse_webhook_body()
.return_once(|_| Err(git::forge::webhook::Error::NetworkResponseEmpty)); .return_once(|_| Err(git::forge::webhook::Error::NetworkResponseEmpty));
let net: Net = given::a_network().into();
let (actor, log) = given::a_repo_actor( let (actor, log) = given::a_repo_actor(
repo_details, repo_details,
Box::new(repository_factory), Box::new(repository_factory),
forge, forge,
given::a_network().into(), given::a_server_actor(fs.as_real(), net.clone()),
net,
); );
let actor = actor.with_webhook_auth(Some(given::a_webhook_auth())); let actor = actor.with_webhook_auth(Some(given::a_webhook_auth()));
//when //when
actor tell!(
.start() kameo::spawn(actor),
.send(crate::repo::messages::WebhookNotification::new( WebhookNotification::new(forge_notification)
forge_notification, )?;
))
.await?;
System::current().stop();
//then //then
log.no_message_contains("send")?; log.no_message_contains("send").await?;
log.require_message_containing("message parse error - not a push")?; log.require_message_containing("message parse error - not a push")
.await?;
Ok(()) Ok(())
} }
#[actix::test] #[tokio::test]
async fn when_message_is_push_on_unknown_branch_drop_notification() -> TestResult { async fn when_message_is_push_on_unknown_branch_drop_notification() -> TestResult {
//given //given
let fs = given::a_filesystem(); let fs = given::a_filesystem();
@ -214,32 +218,31 @@ async fn when_message_is_push_on_unknown_branch_drop_notification() -> TestResul
.return_once(|_, _| true); // is valid .return_once(|_, _| true); // is valid
forge.expect_should_ignore_message().returning(|_| false); forge.expect_should_ignore_message().returning(|_| false);
forge.expect_parse_webhook_body().return_once(|_| Ok(push)); forge.expect_parse_webhook_body().return_once(|_| Ok(push));
let net: Net = given::a_network().into();
let (actor, log) = given::a_repo_actor( let (actor, log) = given::a_repo_actor(
repo_details, repo_details,
Box::new(repository_factory), Box::new(repository_factory),
forge, forge,
given::a_network().into(), given::a_server_actor(fs.as_real(), net.clone()),
net,
); );
let actor = actor let actor = actor
.with_webhook_auth(Some(given::a_webhook_auth())) .with_webhook_auth(Some(given::a_webhook_auth()))
.with_last_main_commit(commit); .with_last_main_commit(commit);
//when //when
actor tell!(
.start() kameo::spawn(actor),
.send(crate::repo::messages::WebhookNotification::new( WebhookNotification::new(forge_notification)
forge_notification, )?;
))
.await?;
System::current().stop();
//then //then
log.no_message_contains("send")?; log.no_message_contains("send").await?;
log.require_message_containing("unknown branch")?; log.require_message_containing("unknown branch").await?;
Ok(()) Ok(())
} }
#[actix::test] #[tokio::test]
async fn when_message_is_push_already_seen_commit_to_main() -> TestResult { async fn when_message_is_push_already_seen_commit_to_main() -> TestResult {
//given //given
let fs = given::a_filesystem(); let fs = given::a_filesystem();
@ -263,32 +266,32 @@ async fn when_message_is_push_already_seen_commit_to_main() -> TestResult {
.return_once(|_, _| true); // is valid .return_once(|_, _| true); // is valid
forge.expect_should_ignore_message().returning(|_| false); forge.expect_should_ignore_message().returning(|_| false);
forge.expect_parse_webhook_body().return_once(|_| Ok(push)); forge.expect_parse_webhook_body().return_once(|_| Ok(push));
let net: Net = given::a_network().into();
let (actor, log) = given::a_repo_actor( let (actor, log) = given::a_repo_actor(
repo_details, repo_details,
Box::new(repository_factory), Box::new(repository_factory),
forge, forge,
given::a_network().into(), given::a_server_actor(fs.as_real(), net.clone()),
net,
); );
let actor = actor let actor = actor
.with_webhook_auth(Some(given::a_webhook_auth())) .with_webhook_auth(Some(given::a_webhook_auth()))
.with_last_main_commit(commit); .with_last_main_commit(commit);
//when //when
actor tell!(
.start() kameo::spawn(actor),
.send(crate::repo::messages::WebhookNotification::new( WebhookNotification::new(forge_notification)
forge_notification, )?;
))
.await?;
System::current().stop();
//then //then
log.no_message_contains("send")?; log.no_message_contains("send").await?;
log.require_message_containing(format!("not a new commit on {main}"))?; log.require_message_containing(format!("not a new commit on {main}"))
.await?;
Ok(()) Ok(())
} }
#[actix::test] #[tokio::test]
async fn when_message_is_push_already_seen_commit_to_next() -> TestResult { async fn when_message_is_push_already_seen_commit_to_next() -> TestResult {
//given //given
let fs = given::a_filesystem(); let fs = given::a_filesystem();
@ -301,9 +304,9 @@ async fn when_message_is_push_already_seen_commit_to_next() -> TestResult {
let forge_notification = ForgeNotification::new(forge_alias, repo_alias, headers, body); let forge_notification = ForgeNotification::new(forge_alias, repo_alias, headers, body);
let repository_factory = MockRepositoryFactory::new(); let repository_factory = MockRepositoryFactory::new();
let commit = given::a_commit(); let commit = given::a_commit();
let next = repo_config.branches().next(); let next_branch = repo_config.branches().next();
let push = given::a_push() let push = given::a_push()
.with_branch(next.clone()) .with_branch(next_branch.clone())
.with_sha(commit.sha().to_string()) .with_sha(commit.sha().to_string())
.with_message(commit.message().to_string()); .with_message(commit.message().to_string());
let mut forge = given::a_forge(); let mut forge = given::a_forge();
@ -312,32 +315,32 @@ async fn when_message_is_push_already_seen_commit_to_next() -> TestResult {
.return_once(|_, _| true); // is valid .return_once(|_, _| true); // is valid
forge.expect_should_ignore_message().returning(|_| false); forge.expect_should_ignore_message().returning(|_| false);
forge.expect_parse_webhook_body().return_once(|_| Ok(push)); forge.expect_parse_webhook_body().return_once(|_| Ok(push));
let net: Net = given::a_network().into();
let (actor, log) = given::a_repo_actor( let (actor, log) = given::a_repo_actor(
repo_details, repo_details,
Box::new(repository_factory), Box::new(repository_factory),
forge, forge,
given::a_network().into(), given::a_server_actor(fs.as_real(), net.clone()),
net,
); );
let actor = actor let actor = actor
.with_webhook_auth(Some(given::a_webhook_auth())) .with_webhook_auth(Some(given::a_webhook_auth()))
.with_last_next_commit(commit); .with_last_next_commit(commit);
//when //when
actor tell!(
.start() kameo::spawn(actor),
.send(crate::repo::messages::WebhookNotification::new( WebhookNotification::new(forge_notification)
forge_notification, )?;
))
.await?;
System::current().stop();
//then //then
log.no_message_contains("send")?; log.no_message_contains("send").await?;
log.require_message_containing(format!("not a new commit on {next}"))?; log.require_message_containing(format!("not a new commit on {next_branch}"))
.await?;
Ok(()) Ok(())
} }
#[actix::test] #[tokio::test]
async fn when_message_is_push_already_seen_commit_to_dev() -> TestResult { async fn when_message_is_push_already_seen_commit_to_dev() -> TestResult {
//given //given
let fs = given::a_filesystem(); let fs = given::a_filesystem();
@ -361,32 +364,32 @@ async fn when_message_is_push_already_seen_commit_to_dev() -> TestResult {
.return_once(|_, _| true); // is valid .return_once(|_, _| true); // is valid
forge.expect_should_ignore_message().returning(|_| false); forge.expect_should_ignore_message().returning(|_| false);
forge.expect_parse_webhook_body().return_once(|_| Ok(push)); forge.expect_parse_webhook_body().return_once(|_| Ok(push));
let net: Net = given::a_network().into();
let (actor, log) = given::a_repo_actor( let (actor, log) = given::a_repo_actor(
repo_details, repo_details,
Box::new(repository_factory), Box::new(repository_factory),
forge, forge,
given::a_network().into(), given::a_server_actor(fs.as_real(), net.clone()),
net,
); );
let actor = actor let actor = actor
.with_webhook_auth(Some(given::a_webhook_auth())) .with_webhook_auth(Some(given::a_webhook_auth()))
.with_last_dev_commit(commit); .with_last_dev_commit(commit);
//when //when
actor tell!(
.start() kameo::spawn(actor),
.send(crate::repo::messages::WebhookNotification::new( WebhookNotification::new(forge_notification)
forge_notification, )?;
))
.await?;
System::current().stop();
//then //then
log.no_message_contains("send")?; log.no_message_contains("send").await?;
log.require_message_containing(format!("not a new commit on {dev}"))?; log.require_message_containing(format!("not a new commit on {dev}"))
.await?;
Ok(()) Ok(())
} }
#[actix::test] #[tokio::test]
async fn when_message_is_push_new_commit_to_main_should_stash_and_validate_repo() -> TestResult { async fn when_message_is_push_new_commit_to_main_should_stash_and_validate_repo() -> TestResult {
//given //given
let fs = given::a_filesystem(); let fs = given::a_filesystem();
@ -409,32 +412,33 @@ async fn when_message_is_push_new_commit_to_main_should_stash_and_validate_repo(
.return_once(|_, _| true); // is valid .return_once(|_, _| true); // is valid
forge.expect_should_ignore_message().returning(|_| false); forge.expect_should_ignore_message().returning(|_| false);
forge.expect_parse_webhook_body().return_once(|_| Ok(push)); forge.expect_parse_webhook_body().return_once(|_| Ok(push));
let net: Net = given::a_network().into();
let (actor, log) = given::a_repo_actor( let (actor, log) = given::a_repo_actor(
repo_details, repo_details,
Box::new(repository_factory), Box::new(repository_factory),
forge, forge,
given::a_network().into(), given::a_server_actor(fs.as_real(), net.clone()),
net,
); );
let actor = actor let actor = actor
.with_webhook_auth(Some(given::a_webhook_auth())) .with_webhook_auth(Some(given::a_webhook_auth()))
.with_last_main_commit(given::a_commit()); .with_last_main_commit(given::a_commit());
//when //when
let addr = actor.start(); let addr = kameo::spawn(actor);
addr.send(crate::repo::messages::WebhookNotification::new( tell!(addr, WebhookNotification::new(forge_notification))?;
forge_notification,
))
.await?;
System::current().stop();
//then //then
let view = addr.send(ExamineActor).await?; let view = addr
.ask(ExamineActor)
.await
.map_err(|e| format!("examine actor: {e:?}"))?;
assert_eq!(view.last_main_commit, Some(push_commit)); assert_eq!(view.last_main_commit, Some(push_commit));
log.require_message_containing("send: ValidateRepo")?; log.require_message_containing("send: ValidateRepo").await?;
Ok(()) Ok(())
} }
#[actix::test] #[tokio::test]
async fn when_message_is_push_new_commit_to_next_should_stash_and_validate_repo() -> TestResult { async fn when_message_is_push_new_commit_to_next_should_stash_and_validate_repo() -> TestResult {
//given //given
let fs = given::a_filesystem(); let fs = given::a_filesystem();
@ -457,32 +461,33 @@ async fn when_message_is_push_new_commit_to_next_should_stash_and_validate_repo(
.return_once(|_, _| true); // is valid .return_once(|_, _| true); // is valid
forge.expect_should_ignore_message().returning(|_| false); forge.expect_should_ignore_message().returning(|_| false);
forge.expect_parse_webhook_body().return_once(|_| Ok(push)); forge.expect_parse_webhook_body().return_once(|_| Ok(push));
let net: Net = given::a_network().into();
let (actor, log) = given::a_repo_actor( let (actor, log) = given::a_repo_actor(
repo_details, repo_details,
Box::new(repository_factory), Box::new(repository_factory),
forge, forge,
given::a_network().into(), given::a_server_actor(fs.as_real(), net.clone()),
net,
); );
let actor = actor let actor = actor
.with_webhook_auth(Some(given::a_webhook_auth())) .with_webhook_auth(Some(given::a_webhook_auth()))
.with_last_next_commit(given::a_commit()); .with_last_next_commit(given::a_commit());
//when //when
let addr = actor.start(); let addr = kameo::spawn(actor);
addr.send(crate::repo::messages::WebhookNotification::new( tell!(addr, WebhookNotification::new(forge_notification))?;
forge_notification,
))
.await?;
System::current().stop();
//then //then
let view = addr.send(ExamineActor).await?; let view = addr
.ask(ExamineActor)
.await
.map_err(|e| format!("examine actor: {e:?}"))?;
assert_eq!(view.last_next_commit, Some(push_commit)); assert_eq!(view.last_next_commit, Some(push_commit));
log.require_message_containing("send: ValidateRepo")?; log.require_message_containing("send: ValidateRepo").await?;
Ok(()) Ok(())
} }
#[actix::test] #[tokio::test]
async fn when_message_is_push_new_commit_to_dev_should_stash_and_validate_repo() -> TestResult { async fn when_message_is_push_new_commit_to_dev_should_stash_and_validate_repo() -> TestResult {
//given //given
let fs = given::a_filesystem(); let fs = given::a_filesystem();
@ -505,27 +510,28 @@ async fn when_message_is_push_new_commit_to_dev_should_stash_and_validate_repo()
.return_once(|_, _| true); // is valid .return_once(|_, _| true); // is valid
forge.expect_should_ignore_message().returning(|_| false); forge.expect_should_ignore_message().returning(|_| false);
forge.expect_parse_webhook_body().return_once(|_| Ok(push)); forge.expect_parse_webhook_body().return_once(|_| Ok(push));
let net: Net = given::a_network().into();
let (actor, log) = given::a_repo_actor( let (actor, log) = given::a_repo_actor(
repo_details, repo_details,
Box::new(repository_factory), Box::new(repository_factory),
forge, forge,
given::a_network().into(), given::a_server_actor(fs.as_real(), net.clone()),
net,
); );
let actor = actor let actor = actor
.with_webhook_auth(Some(given::a_webhook_auth())) .with_webhook_auth(Some(given::a_webhook_auth()))
.with_last_dev_commit(given::a_commit()); .with_last_dev_commit(given::a_commit());
//when //when
let addr = actor.start(); let addr = kameo::spawn(actor);
addr.send(crate::repo::messages::WebhookNotification::new( tell!(addr, WebhookNotification::new(forge_notification))?;
forge_notification,
))
.await?;
System::current().stop();
//then //then
let view = addr.send(ExamineActor).await?; let view = addr
.ask(ExamineActor)
.await
.map_err(|e| format!("examine actor: {e:?}"))?;
assert_eq!(view.last_dev_commit, Some(push_commit)); assert_eq!(view.last_dev_commit, Some(push_commit));
log.require_message_containing("send: ValidateRepo")?; log.require_message_containing("send: ValidateRepo").await?;
Ok(()) Ok(())
} }

View file

@ -1,7 +1,9 @@
use crate::{repo::messages::WebhookRegistered, tell};
// //
use super::*; use super::*;
#[actix::test] #[tokio::test]
async fn should_store_webhook_details() -> TestResult { async fn should_store_webhook_details() -> TestResult {
//given //given
let fs = given::a_filesystem(); let fs = given::a_filesystem();
@ -15,21 +17,23 @@ async fn should_store_webhook_details() -> TestResult {
repo_details, repo_details,
given::a_forge(), given::a_forge(),
); );
addr.send(crate::repo::messages::WebhookRegistered::new(( tell!(
webhook_id.clone(), addr,
webhook_auth.clone(), WebhookRegistered::new((webhook_id.clone(), webhook_auth.clone()))
))) )?;
.await?;
System::current().stop();
//then //then
let view = addr.send(ExamineActor).await?; // let view = addr.ask(ExamineActor).await.expect("examine actor");
let view = addr
.ask(ExamineActor)
.await
.map_err(|e| format!("examine actor: {e:?}"))?;
assert_eq!(view.webhook_id, Some(webhook_id)); assert_eq!(view.webhook_id, Some(webhook_id));
assert_eq!(view.webhook_auth, Some(webhook_auth)); assert_eq!(view.webhook_auth, Some(webhook_auth));
Ok(()) Ok(())
} }
#[actix::test] #[tokio::test]
async fn should_send_validate_repo_message() -> TestResult { async fn should_send_validate_repo_message() -> TestResult {
//given //given
let fs = given::a_filesystem(); let fs = given::a_filesystem();
@ -43,14 +47,12 @@ async fn should_send_validate_repo_message() -> TestResult {
repo_details, repo_details,
given::a_forge(), given::a_forge(),
); );
addr.send(crate::repo::messages::WebhookRegistered::new(( tell!(
webhook_id.clone(), addr,
webhook_auth.clone(), WebhookRegistered::new((webhook_id.clone(), webhook_auth.clone()))
))) )?;
.await?;
System::current().stop();
//then //then
log.require_message_containing("send: ValidateRepo")?; log.require_message_containing("send: ValidateRepo").await?;
Ok(()) Ok(())
} }

View file

@ -4,7 +4,7 @@ use super::*;
use crate::git::file; use crate::git::file;
use crate::repo::load; use crate::repo::load;
#[actix::test] #[tokio::test]
async fn when_file_not_found_should_error() -> TestResult { async fn when_file_not_found_should_error() -> TestResult {
//given //given
let fs = given::a_filesystem(); let fs = given::a_filesystem();
@ -13,7 +13,7 @@ async fn when_file_not_found_should_error() -> TestResult {
.expect_read_file() .expect_read_file()
.returning(|_, _| Err(file::Error::FileNotFound)); .returning(|_, _| Err(file::Error::FileNotFound));
//when //when
let_assert!(Err(err) = load::config_from_repository(repo_details, &open_repository).await); let_assert!(Err(err) = load::config_from_repository(&repo_details, &open_repository).await);
//then //then
debug!("Got: {err:?}"); debug!("Got: {err:?}");
assert!(matches!( assert!(matches!(
@ -22,7 +22,7 @@ async fn when_file_not_found_should_error() -> TestResult {
)); ));
Ok(()) Ok(())
} }
#[actix::test] #[tokio::test]
async fn when_file_format_invalid_should_error() -> TestResult { async fn when_file_format_invalid_should_error() -> TestResult {
//given //given
let fs = given::a_filesystem(); let fs = given::a_filesystem();
@ -32,14 +32,14 @@ async fn when_file_format_invalid_should_error() -> TestResult {
.expect_read_file() .expect_read_file()
.return_once(move |_, _| Ok(contents)); .return_once(move |_, _| Ok(contents));
//when //when
let_assert!(Err(err) = load::config_from_repository(repo_details, &open_repository).await); let_assert!(Err(err) = load::config_from_repository(&repo_details, &open_repository).await);
//then //then
debug!("Got: {err:?}"); debug!("Got: {err:?}");
assert!(matches!(err, load::Error::Toml(_))); assert!(matches!(err, load::Error::Toml(_)));
Ok(()) Ok(())
} }
#[actix::test] #[tokio::test]
async fn when_main_branch_is_missing_should_error() -> TestResult { async fn when_main_branch_is_missing_should_error() -> TestResult {
//given //given
let fs = given::a_filesystem(); let fs = given::a_filesystem();
@ -64,14 +64,14 @@ async fn when_main_branch_is_missing_should_error() -> TestResult {
.expect_remote_branches() .expect_remote_branches()
.return_once(move || Ok(branches)); .return_once(move || Ok(branches));
//when //when
let_assert!(Err(err) = load::config_from_repository(repo_details, &open_repository).await); let_assert!(Err(err) = load::config_from_repository(&repo_details, &open_repository).await);
//then //then
debug!("Got: {err:?}"); debug!("Got: {err:?}");
assert!(matches!(err, load::Error::BranchNotFound(branch) if branch == main)); assert!(matches!(err, load::Error::BranchNotFound(branch) if branch == main));
Ok(()) Ok(())
} }
#[actix::test] #[tokio::test]
async fn when_next_branch_is_missing_should_error() -> TestResult { async fn when_next_branch_is_missing_should_error() -> TestResult {
//given //given
let fs = given::a_filesystem(); let fs = given::a_filesystem();
@ -96,14 +96,14 @@ async fn when_next_branch_is_missing_should_error() -> TestResult {
.expect_remote_branches() .expect_remote_branches()
.return_once(move || Ok(branches)); .return_once(move || Ok(branches));
//when //when
let_assert!(Err(err) = load::config_from_repository(repo_details, &open_repository).await); let_assert!(Err(err) = load::config_from_repository(&repo_details, &open_repository).await);
//then //then
debug!("Got: {err:?}"); debug!("Got: {err:?}");
assert!(matches!(err, load::Error::BranchNotFound(branch) if branch == next)); assert!(matches!(err, load::Error::BranchNotFound(branch) if branch == next));
Ok(()) Ok(())
} }
#[actix::test] #[tokio::test]
async fn when_dev_branch_is_missing_should_error() -> TestResult { async fn when_dev_branch_is_missing_should_error() -> TestResult {
//given //given
let fs = given::a_filesystem(); let fs = given::a_filesystem();
@ -128,14 +128,14 @@ async fn when_dev_branch_is_missing_should_error() -> TestResult {
.expect_remote_branches() .expect_remote_branches()
.return_once(move || Ok(branches)); .return_once(move || Ok(branches));
//when //when
let_assert!(Err(err) = load::config_from_repository(repo_details, &open_repository).await); let_assert!(Err(err) = load::config_from_repository(&repo_details, &open_repository).await);
//then //then
debug!("Got: {err:?}"); debug!("Got: {err:?}");
assert!(matches!(err, load::Error::BranchNotFound(branch) if branch == dev)); assert!(matches!(err, load::Error::BranchNotFound(branch) if branch == dev));
Ok(()) Ok(())
} }
#[actix::test] #[tokio::test]
async fn when_valid_file_should_return_repo_config() -> TestResult { async fn when_valid_file_should_return_repo_config() -> TestResult {
//given //given
let fs = given::a_filesystem(); let fs = given::a_filesystem();
@ -161,7 +161,7 @@ async fn when_valid_file_should_return_repo_config() -> TestResult {
.expect_remote_branches() .expect_remote_branches()
.return_once(move || Ok(branches)); .return_once(move || Ok(branches));
//when //when
let_assert!(Ok(result) = load::config_from_repository(repo_details, &open_repository).await); let_assert!(Ok(result) = load::config_from_repository(&repo_details, &open_repository).await);
//then //then
debug!("Got: {result:?}"); debug!("Got: {result:?}");
assert_eq!(result, repo_config); assert_eq!(result, repo_config);

View file

@ -1,6 +1,4 @@
// //
use actix::prelude::*;
use crate::{ use crate::{
git, git,
repo::{ repo::{
@ -12,7 +10,6 @@ use crate::{
use git_next_core::{ use git_next_core::{
git::{ git::{
commit::Sha, commit::Sha,
forge::commit::Status,
repository::{ repository::{
factory::{mock, MockRepositoryFactory, RepositoryFactory}, factory::{mock, MockRepositoryFactory, RepositoryFactory},
open::{MockOpenRepositoryLike, OpenRepositoryLike}, open::{MockOpenRepositoryLike, OpenRepositoryLike},
@ -28,12 +25,17 @@ use git_next_core::{
}; };
use assert2::let_assert; use assert2::let_assert;
use kameo::{
message::{Context, Message},
Reply,
};
use mockall::predicate::eq; use mockall::predicate::eq;
use tracing::{debug, error}; use tracing::{debug, error};
use std::{ use std::{
collections::{BTreeMap, HashMap}, collections::{BTreeMap, HashMap},
sync::{Arc, RwLock}, sync::{Arc, RwLock},
time::Duration,
}; };
type TestResult = Result<(), Box<dyn std::error::Error>>; type TestResult = Result<(), Box<dyn std::error::Error>>;
@ -45,33 +47,43 @@ mod handlers;
mod load; mod load;
mod when; mod when;
pub async fn tick(millis: u64) {
tokio::time::sleep(Duration::from_millis(millis)).await;
}
impl ActorLog { impl ActorLog {
pub fn no_message_contains(&self, needle: impl AsRef<str> + std::fmt::Display) -> TestResult { pub async fn no_message_contains(
if self.find_in_messages(needle.as_ref())? { &self,
needle: impl AsRef<str> + Send + std::fmt::Display,
) -> TestResult {
if self.find_in_messages(needle.as_ref()).await? {
error!(?self, ""); error!(?self, "");
panic!("found unexpected message: {needle}"); panic!("found unexpected message: {needle}");
} }
Ok(()) Ok(())
} }
pub fn require_message_containing( pub async fn require_message_containing(
&self, &self,
needle: impl AsRef<str> + std::fmt::Display, needle: impl AsRef<str> + Send + std::fmt::Display,
) -> TestResult { ) -> TestResult {
if !self.find_in_messages(needle.as_ref())? { if !self.find_in_messages(needle.as_ref()).await? {
error!(?self, ""); error!(?self, "");
panic!("expected message not found: {needle}"); panic!("expected message not found: {needle}");
} }
Ok(()) Ok(())
} }
fn find_in_messages( async fn find_in_messages(
&self, &self,
needle: impl AsRef<str>, needle: impl AsRef<str> + Send,
) -> Result<bool, Box<dyn std::error::Error>> { ) -> Result<bool, Box<dyn std::error::Error>> {
// Very short sleep to allow tests to get a chance to tick
// This should be enough for most tests.
tokio::time::sleep(Duration::from_millis(1)).await;
let found = self let found = self
.read() .read()
.map_err(|e| e.to_string())? .await
.iter() .iter()
.any(|message| message.contains(needle.as_ref())); .any(|message| message.contains(needle.as_ref()));
Ok(found) Ok(found)
@ -79,15 +91,19 @@ impl ActorLog {
} }
message!(ExamineActor => RepoActorView, "Request a view of the current state of the [RepoActor]."); message!(ExamineActor => RepoActorView, "Request a view of the current state of the [RepoActor].");
impl Handler<ExamineActor> for RepoActor { impl Message<ExamineActor> for RepoActor {
type Result = RepoActorView; type Reply = RepoActorView;
fn handle(&mut self, _msg: ExamineActor, _ctx: &mut Self::Context) -> Self::Result { async fn handle(
&mut self,
_msg: ExamineActor,
_ctx: Context<'_, Self, Self::Reply>,
) -> Self::Reply {
let repo_actor: &Self = self; let repo_actor: &Self = self;
Self::Result::from(repo_actor) Self::Reply::from(repo_actor)
} }
} }
#[derive(Debug, MessageResponse)] #[derive(Debug, Reply)]
pub struct RepoActorView { pub struct RepoActorView {
pub repo_details: RepoDetails, pub repo_details: RepoDetails,
pub webhook_id: Option<WebhookId>, // INFO: if [None] then no webhook is configured pub webhook_id: Option<WebhookId>, // INFO: if [None] then no webhook is configured

View file

@ -1,37 +1,37 @@
// //
use kameo::actor::ActorRef;
use kxio::{fs::FileSystem, net::Net};
use crate::server::ServerActor;
use super::*; use super::*;
pub fn start_actor( pub fn start_actor(
repository_factory: MockRepositoryFactory, repository_factory: MockRepositoryFactory,
repo_details: RepoDetails, repo_details: RepoDetails,
forge: Box<dyn ForgeLike>, forge: Box<dyn ForgeLike>,
) -> (actix::Addr<RepoActor>, ActorLog) { fs: FileSystem,
net: Net,
) -> (ActorRef<RepoActor>, ActorLog) {
let (actor, log) = given::a_repo_actor( let (actor, log) = given::a_repo_actor(
repo_details, repo_details,
Box::new(repository_factory), Box::new(repository_factory),
forge, forge,
given::a_network().into(), given::a_server_actor(fs, net.clone()),
net,
); );
(actor.start(), log) (kameo::spawn(actor), log)
} }
pub fn start_actor_with_open_repository( pub fn start_actor_with_open_repository(
open_repository: Box<dyn OpenRepositoryLike>, open_repository: Box<dyn OpenRepositoryLike>,
repo_details: RepoDetails, repo_details: RepoDetails,
forge: Box<dyn ForgeLike>, forge: Box<dyn ForgeLike>,
) -> (actix::Addr<RepoActor>, ActorLog) { ) -> (ActorRef<RepoActor>, ActorLog) {
let (actor, log) = given::a_repo_actor(repo_details, mock(), forge, given::a_network().into()); let fs = given::a_filesystem();
let net: Net = given::a_network().into();
let server_actor_ref: ActorRef<ServerActor> = given::a_server_actor(fs.as_real(), net.clone());
let (actor, log) = given::a_repo_actor(repo_details, mock(), forge, server_actor_ref, net);
let actor = actor.with_open_repository(Some(open_repository)); let actor = actor.with_open_repository(Some(open_repository));
(actor.start(), log) (kameo::spawn(actor), log)
}
pub fn commit_status(forge: &mut MockForgeLike, commit: Commit, status: Status) {
let mut commit_status_forge = MockForgeLike::new();
commit_status_forge
.expect_commit_status()
.with(mockall::predicate::eq(commit))
.return_once(|_| Ok(status));
forge
.expect_duplicate()
.return_once(move || Box::new(commit_status_forge));
} }

221
crates/cli/src/root.rs Normal file
View file

@ -0,0 +1,221 @@
//
// Root actor for all other actors - supervises them all
use std::time::Duration;
use color_eyre::Result;
use derive_more::derive::Constructor;
use git_next_core::{git::RepositoryFactory, s};
use kameo::{
actor::{pubsub::PubSub, ActorRef},
mailbox::unbounded::UnboundedMailbox,
message::{Context, Message},
Actor,
};
use kxio::{fs::FileSystem, net::Net};
use crate::{
alerts::{AlertsActor, History},
base_actor::BaseActor,
default_on_actor_panic, default_on_actor_start,
file_watcher::{FileUpdated, FileWatcherActor},
on_actor_link_died, on_actor_stop, publish,
server::{actor::messages::ServerUpdate, ServerActor},
subscribe,
tui::Tui,
MessageBus,
};
#[derive(Debug)]
pub struct RootActor {
ui: bool,
fs: FileSystem,
net: Net,
sleep_duration: std::time::Duration,
tx_shutdown: tokio::sync::mpsc::Sender<String>,
alerts_actor_ref: Option<ActorRef<AlertsActor>>,
server_updates_bus: Option<MessageBus<ServerUpdate>>,
server_actor_ref: Option<ActorRef<ServerActor>>,
file_updates_bus: Option<MessageBus<FileUpdated>>,
file_watcher_actor_ref: Option<ActorRef<FileWatcherActor>>,
#[cfg(feature = "tui")]
tui_actor_ref: Option<ActorRef<Tui>>,
}
impl RootActor {
pub const fn new(
ui: bool,
fs: FileSystem,
net: Net,
sleep_duration: std::time::Duration,
tx_shutdown: tokio::sync::mpsc::Sender<String>,
) -> Self {
Self {
ui,
fs,
net,
sleep_duration,
tx_shutdown,
alerts_actor_ref: None,
server_updates_bus: None,
server_actor_ref: None,
file_updates_bus: None,
file_watcher_actor_ref: None,
#[cfg(feature = "tui")]
tui_actor_ref: None,
}
}
}
const A_DAY: Duration = Duration::from_secs(24 * 60 * 60);
type RootContext<'a> = Context<'a, RootActor, Result<()>>;
#[derive(Constructor, Debug)]
pub struct Start {
repo: Box<dyn RepositoryFactory>,
}
impl Message<Start> for RootActor {
type Reply = Result<()>;
async fn handle(&mut self, msg: Start, ctx: RootContext<'_>) -> Self::Reply {
let alerts_actor_ref = self.start_alerts_actor(&ctx).await?;
let server_updates_bus = self.start_server_updates_bus(&ctx).await;
let file_updates_bus = self.start_file_updates_bus(&ctx).await;
self.start_server_actor(
&ctx,
alerts_actor_ref,
server_updates_bus.clone(),
file_updates_bus.clone(),
msg.repo,
)
.await?;
self.start_file_watcher_actor(&ctx, file_updates_bus.clone())
.await;
#[cfg(feature = "tui")]
if self.ui {
self.start_tui_actor(&ctx, server_updates_bus).await?;
}
// trigger initial config file to load
publish!(file_updates_bus, FileUpdated)?;
Ok(())
}
}
impl RootActor {
async fn start_alerts_actor(&mut self, ctx: &RootContext<'_>) -> Result<ActorRef<AlertsActor>> {
let actor_ref = AlertsActor::new(None, History::new(A_DAY), self.net.clone())
.spawn(ctx.actor_ref())
.await;
self.alerts_actor_ref.replace(actor_ref.clone());
Ok(actor_ref)
}
async fn start_file_watcher_actor(
&mut self,
ctx: &RootContext<'_>,
file_updates_bus: MessageBus<FileUpdated>,
) -> ActorRef<FileWatcherActor> {
let actor_ref = FileWatcherActor::new(
file_updates_bus,
self.fs.base().join("git-next-server.toml"),
)
.spawn_in_thread(ctx.actor_ref())
.await;
self.file_watcher_actor_ref.replace(actor_ref.clone());
actor_ref
}
async fn start_server_updates_bus(
&mut self,
ctx: &RootContext<'_>,
) -> MessageBus<ServerUpdate> {
let actor_ref = PubSub::<ServerUpdate>::new().spawn(ctx.actor_ref()).await;
self.server_updates_bus.replace(actor_ref.clone());
actor_ref
}
async fn start_file_updates_bus(&mut self, ctx: &RootContext<'_>) -> MessageBus<FileUpdated> {
let actor_ref = PubSub::<FileUpdated>::new().spawn(ctx.actor_ref()).await;
self.file_updates_bus.replace(actor_ref.clone());
actor_ref
}
async fn start_server_actor(
&mut self,
ctx: &RootContext<'_>,
alerts_actor_ref: ActorRef<AlertsActor>,
server_updates_bus: MessageBus<ServerUpdate>,
file_updates_bus: MessageBus<FileUpdated>,
repo: Box<dyn RepositoryFactory>,
) -> Result<ActorRef<ServerActor>> {
let actor_ref = ServerActor::new(
self.ui,
self.fs.clone(),
self.net.clone(),
alerts_actor_ref,
server_updates_bus,
repo,
self.sleep_duration,
)
.spawn(ctx.actor_ref())
.await;
subscribe!(file_updates_bus, "server", actor_ref.clone())?;
self.server_actor_ref.replace(actor_ref.clone());
Ok(actor_ref)
}
#[cfg(feature = "tui")]
async fn start_tui_actor(
&mut self,
ctx: &RootContext<'_>,
server_updates_bus: MessageBus<ServerUpdate>,
) -> Result<ActorRef<Tui>> {
let actor_ref = Tui::new().spawn_in_thread(ctx.actor_ref()).await;
self.tui_actor_ref.replace(actor_ref.clone());
subscribe!(server_updates_bus, actor_ref.clone())?;
Ok(actor_ref)
}
}
impl Actor for RootActor {
type Mailbox = UnboundedMailbox<Self>;
default_on_actor_start!(this, actor_ref);
default_on_actor_panic!(this, actor_ref, err);
on_actor_link_died!(this, actor_ref, id, reason, {
this.tx_shutdown.send(s!("link died")).await?;
match &reason {
kameo::error::ActorStopReason::Normal => Ok(None),
kameo::error::ActorStopReason::Killed
| kameo::error::ActorStopReason::Panicked(_)
| kameo::error::ActorStopReason::LinkDied { .. } => {
Ok(Some(kameo::error::ActorStopReason::LinkDied {
id,
reason: Box::new(reason),
}))
}
}
});
on_actor_stop!(this, actor_ref, reason, {
#[allow(clippy::expect_used)]
this.tx_shutdown
.send(s!("stopping"))
.await
.expect("send shutdown");
Ok(())
});
}

View file

@ -1,20 +1,36 @@
// //
use actix::prelude::*; use color_eyre::Result;
use kameo::message::{Context, Message};
use git_next_core::server::AppConfig; use git_next_core::server::AppConfig;
use tracing::debug;
use crate::{ use crate::{
file_watcher::FileUpdated, file_watcher::FileUpdated,
server::actor::{messages::ReceiveAppConfig, ServerActor}, server::actor::{messages::ReceiveAppConfig, ServerActor},
tell,
}; };
impl Handler<FileUpdated> for ServerActor { impl Message<FileUpdated> for ServerActor {
type Result = (); type Reply = Result<()>;
fn handle(&mut self, _msg: FileUpdated, ctx: &mut Self::Context) -> Self::Result { async fn handle(
&mut self,
_msg: FileUpdated,
ctx: Context<'_, Self, Self::Reply>,
) -> Self::Reply {
debug!("FileUpdated");
match AppConfig::load(&self.fs) { match AppConfig::load(&self.fs) {
Ok(app_config) => self.do_send(ReceiveAppConfig::new(app_config), ctx), Ok(app_config) => Ok(tell!(
Err(err) => self.abort(ctx, format!("Failed to load config file. Error: {err}")), "server",
}; ctx.actor_ref(),
ReceiveAppConfig::new(app_config)
)?),
Err(err) => {
tracing::error!("Failed to load config file. Error: {err}");
ctx.actor_ref().kill();
Ok(())
}
}
} }
} }

View file

@ -4,4 +4,3 @@ mod receive_valid_app_config;
mod server_update; mod server_update;
mod shutdown; mod shutdown;
mod shutdown_trigger; mod shutdown_trigger;
mod subscribe_updates;

View file

@ -1,35 +1,46 @@
use actix::prelude::*; //
use color_eyre::Result;
use kameo::message::{Context, Message};
use crate::server::actor::{ use crate::{
server::actor::{
messages::{ReceiveAppConfig, ReceiveValidAppConfig, ValidAppConfig}, messages::{ReceiveAppConfig, ReceiveValidAppConfig, ValidAppConfig},
ServerActor, ServerActor,
},
tell,
}; };
impl Handler<ReceiveAppConfig> for ServerActor { impl Message<ReceiveAppConfig> for ServerActor {
type Result = (); type Reply = Result<()>;
#[allow(clippy::cognitive_complexity)] #[allow(clippy::cognitive_complexity)]
fn handle(&mut self, msg: ReceiveAppConfig, ctx: &mut Self::Context) -> Self::Result { async fn handle(
tracing::info!("recieved server config"); &mut self,
msg: ReceiveAppConfig,
ctx: Context<'_, Self, Self::Reply>,
) -> Self::Reply {
let Ok(socket_addr) = msg.listen_socket_addr() else { let Ok(socket_addr) = msg.listen_socket_addr() else {
return self.abort(ctx, "Unable to parse http.addr"); self.abort(&ctx, "Unable to parse http.addr").await?;
return Ok(());
}; };
let Some(server_storage) = self.server_storage(&msg) else { let Some(server_storage) = self.server_storage(&msg) else {
return self.abort(ctx, "Server storage not available"); self.abort(&ctx, "Server storage not available").await?;
return Ok(());
}; };
if msg.listen().url().ends_with('/') { if msg.listen().url().ends_with('/') {
return self.abort(ctx, "webhook.url must not end with a '/'"); self.abort(&ctx, "webhook.url must not end with a '/'")
.await?;
return Ok(());
} }
tell!(
self.do_send( "server",
ctx.actor_ref(),
ReceiveValidAppConfig::new(ValidAppConfig::new( ReceiveValidAppConfig::new(ValidAppConfig::new(
msg.peel(), msg.peel(),
socket_addr, socket_addr,
server_storage, server_storage,
)), ))
ctx, )?;
); Ok(())
} }
} }

View file

@ -1,43 +1,47 @@
// //
use actix::prelude::*; use color_eyre::Result;
use kameo::{
actor::ActorRef,
message::{Context, Message},
};
use git_next_core::{ForgeAlias, RepoAlias}; use git_next_core::{ForgeAlias, RepoAlias};
use tracing::info;
use crate::{ use crate::{
alerts::messages::UpdateShout, alerts::messages::UpdateShout,
base_actor::BaseActor as _,
repo::{messages::CloneRepo, RepoActor}, repo::{messages::CloneRepo, RepoActor},
server::actor::{ server::actor::{
messages::{ReceiveValidAppConfig, ServerUpdate, ValidAppConfig}, messages::{ReceiveValidAppConfig, ServerUpdate, ValidAppConfig},
ServerActor, ServerActor,
}, },
webhook::{ spawn, tell,
messages::ShutdownWebhook, webhook::{self, router::AddWebhookRecipient, WebhookActor},
router::{AddWebhookRecipient, WebhookRouterActor},
WebhookActor,
},
}; };
impl Handler<ReceiveValidAppConfig> for ServerActor { impl Message<ReceiveValidAppConfig> for ServerActor {
type Result = (); type Reply = Result<()>;
fn handle(&mut self, msg: ReceiveValidAppConfig, ctx: &mut Self::Context) -> Self::Result { async fn handle(
&mut self,
msg: ReceiveValidAppConfig,
ctx: Context<'_, Self, Self::Reply>,
) -> Self::Reply {
let ValidAppConfig { let ValidAppConfig {
app_config, app_config,
socket_address, socket_address,
storage: server_storage, storage: server_storage,
} = msg.peel(); } = msg.peel();
// shutdown any existing webhook actor // shutdown any existing webhook actor
if let Some(webhook_actor_addr) = self.webhook_actor_addr.take() { if let Some(webhook_actor_ref) = self.webhook_actor_ref.take() {
webhook_actor_addr.do_send(ShutdownWebhook); webhook_actor_ref.kill();
} }
self.generation.inc(); self.generation.inc();
// Webhook Server // Webhook Server
info!("Starting Webhook Server..."); if let Some(webhook_router_actor_ref) = &self.webhook_router_actor_ref {
let webhook_router = WebhookRouterActor::default().start();
let listen_url = app_config.listen().url(); let listen_url = app_config.listen().url();
let notify_user_recipient = self.alerts.clone().recipient(); let server_actor_ref = ctx.actor_ref();
let server_addr = Some(ctx.address());
// Forge Actors // Forge Actors
for (forge_alias, forge_config) in app_config.forges() { for (forge_alias, forge_config) in app_config.forges() {
let repo_actors = self let repo_actors = self
@ -46,52 +50,62 @@ impl Handler<ReceiveValidAppConfig> for ServerActor {
forge_alias.clone(), forge_alias.clone(),
&server_storage, &server_storage,
listen_url, listen_url,
&notify_user_recipient, &self.alerts,
server_addr.clone(), server_actor_ref.clone(),
) )
.into_iter() .into_iter()
.map(start_repo_actor) .map(|repo_actor_tuple| {
start_repo_actor(repo_actor_tuple, server_actor_ref.clone())
})
.collect::<Vec<_>>(); .collect::<Vec<_>>();
repo_actors for repo_actor in repo_actors {
.iter() let (repo_alias, repo_actor_ref) = repo_actor.await?;
.map(|(repo_alias, addr)| { tell!(
webhook_router_actor_ref,
AddWebhookRecipient::new( AddWebhookRecipient::new(
forge_alias.clone(), forge_alias.clone(),
repo_alias.clone(), repo_alias.clone(),
addr.clone().recipient(), repo_actor_ref.clone(),
) )
}) )?;
.for_each(|msg| webhook_router.do_send(msg));
for (repo_alias, addr) in repo_actors {
self.repo_actors self.repo_actors
.insert((forge_alias.clone(), repo_alias), addr); .insert((forge_alias.clone(), repo_alias), repo_actor_ref);
} }
} }
let webhook_actor_addr = let webhook_actor_ref = spawn!(
WebhookActor::new(socket_address, webhook_router.recipient()).start(); ctx.actor_ref(),
self.webhook_actor_addr.replace(webhook_actor_addr); WebhookActor::new(socket_address, webhook_router_actor_ref.clone())
);
tell!(webhook_actor_ref, webhook::Start)?;
self.webhook_actor_ref.replace(webhook_actor_ref);
}
let shout = app_config.shout().clone(); let shout = app_config.shout().clone();
self.app_config.replace(app_config.clone()); self.app_config.replace(app_config.clone());
self.do_send( tell!(
"server",
ctx.actor_ref(),
ServerUpdate::AppConfigLoaded { ServerUpdate::AppConfigLoaded {
app_config: ValidAppConfig { app_config: ValidAppConfig {
app_config, app_config,
socket_address, socket_address,
storage: server_storage, storage: server_storage,
}, },
}, }
ctx, )?;
); tell!("alert", self.alerts, UpdateShout::new(shout))?;
self.alerts.do_send(UpdateShout::new(shout)); Ok(())
} }
} }
fn start_repo_actor(actor: (ForgeAlias, RepoAlias, RepoActor)) -> (RepoAlias, Addr<RepoActor>) { async fn start_repo_actor(
let (forge_name, repo_alias, actor) = actor; actor: (ForgeAlias, RepoAlias, RepoActor),
server_actor_ref: ActorRef<ServerActor>,
) -> Result<(RepoAlias, ActorRef<RepoActor>)> {
let (forge_name, repo_alias, repo_actor) = actor;
let span = tracing::info_span!("start_repo_actor", forge = %forge_name, repo = %repo_alias); let span = tracing::info_span!("start_repo_actor", forge = %forge_name, repo = %repo_alias);
let _guard = span.enter(); let _guard = span.enter();
let addr = actor.start(); let repo_actor_ref = repo_actor.spawn(server_actor_ref).await;
addr.do_send(CloneRepo); tell!(repo_actor_ref, CloneRepo)?;
tracing::info!("Started"); Ok((repo_alias, repo_actor_ref))
(repo_alias, addr)
} }

View file

@ -1,14 +1,20 @@
use actix::Handler;
// //
use crate::server::{actor::messages::ServerUpdate, ServerActor}; use kameo::message::{Context, Message};
impl Handler<ServerUpdate> for ServerActor { use crate::{
type Result = (); publish,
server::{actor::messages::ServerUpdate, ServerActor},
};
fn handle(&mut self, msg: ServerUpdate, _ctx: &mut Self::Context) -> Self::Result { impl Message<ServerUpdate> for ServerActor {
self.subscribers.iter().for_each(move |subscriber| { type Reply = color_eyre::Result<()>;
subscriber.do_send(msg.clone());
}); async fn handle(
&mut self,
msg: ServerUpdate,
_ctx: Context<'_, Self, Self::Reply>,
) -> Self::Reply {
publish!("server_updates_bus", self.server_updates_bus, msg)?;
Ok(())
} }
} }

View file

@ -1,30 +1,35 @@
//- //
use color_eyre::Result;
use actix::prelude::*; use kameo::message::{Context, Message};
use tracing::debug; use tracing::{debug, info};
use crate::{ use crate::{
repo::messages::UnRegisterWebhook, repo::messages::UnRegisterWebhook,
server::actor::{messages::Shutdown, ServerActor}, server::actor::{messages::Shutdown, ServerActor},
webhook::messages::ShutdownWebhook, tell,
}; };
impl Handler<Shutdown> for ServerActor { impl Message<Shutdown> for ServerActor {
type Result = (); type Reply = Result<()>;
fn handle(&mut self, _msg: Shutdown, _ctx: &mut Self::Context) -> Self::Result { async fn handle(
self.repo_actors &mut self,
.iter() _msg: Shutdown,
.for_each(|((forge_alias, repo_alias), addr)| { _ctx: Context<'_, Self, Self::Reply>,
) -> Self::Reply {
for ((forge_alias, repo_alias), repo_actor_ref) in &self.repo_actors {
debug!(%forge_alias, %repo_alias, "removing webhook"); debug!(%forge_alias, %repo_alias, "removing webhook");
addr.do_send(UnRegisterWebhook::new()); tell!(repo_actor_ref, UnRegisterWebhook::new())?;
debug!(%forge_alias, %repo_alias, "removed webhook"); debug!(%forge_alias, %repo_alias, "removed webhook");
}); repo_actor_ref.kill();
info!(%forge_alias, %repo_alias, "killed repo actor");
}
debug!("server shutdown"); debug!("server shutdown");
if let Some(webhook) = self.webhook_actor_addr.take() { if let Some(webhook_actor_ref) = self.webhook_actor_ref.take() {
debug!("shutting down webhook"); debug!("shutting down webhook");
webhook.do_send(ShutdownWebhook); webhook_actor_ref.kill();
debug!("webhook shutdown"); debug!("webhook shutdown");
} }
Ok(())
} }
} }

View file

@ -1,12 +1,16 @@
// //
use actix::Handler; use kameo::message::{Context, Message};
use crate::server::{actor::messages::ShutdownTrigger, ServerActor}; use crate::server::{actor::messages::ShutdownTrigger, ServerActor};
impl Handler<ShutdownTrigger> for ServerActor { impl Message<ShutdownTrigger> for ServerActor {
type Result = (); type Reply = ();
fn handle(&mut self, msg: ShutdownTrigger, _ctx: &mut Self::Context) -> Self::Result { async fn handle(
&mut self,
msg: ShutdownTrigger,
_ctx: Context<'_, Self, Self::Reply>,
) -> Self::Reply {
self.shutdown_trigger.replace(msg.peel()); self.shutdown_trigger.replace(msg.peel());
} }
} }

View file

@ -1,10 +0,0 @@
use crate::server::actor::{messages::SubscribeToUpdates, ServerActor};
//
impl actix::Handler<SubscribeToUpdates> for ServerActor {
type Result = ();
fn handle(&mut self, msg: SubscribeToUpdates, _ctx: &mut Self::Context) -> Self::Result {
self.subscribers.push(msg.peel());
}
}

View file

@ -1,6 +1,8 @@
// //
use actix::{Message, Recipient}; use std::net::SocketAddr;
use derive_more::Constructor; use derive_more::Constructor;
use tokio::sync::mpsc::Sender;
use git_next_core::{ use git_next_core::{
git::{self, forge::commit::Status, graph::Log, Commit}, git::{self, forge::commit::Status, graph::Log, Commit},
@ -10,8 +12,6 @@ use git_next_core::{
ForgeAlias, RepoAlias, RepoBranches, RepoConfig, ForgeAlias, RepoAlias, RepoBranches, RepoConfig,
}; };
use std::net::SocketAddr;
// receive server config // receive server config
message!( message!(
ReceiveAppConfig, ReceiveAppConfig,
@ -38,8 +38,7 @@ message!(
message!(Shutdown, "Notification to shutdown the server actor"); message!(Shutdown, "Notification to shutdown the server actor");
#[derive(Clone, Debug, PartialEq, Eq, Message)] #[derive(Clone, Debug, PartialEq, Eq)]
#[rtype(result = "()")]
pub enum ServerUpdate { pub enum ServerUpdate {
/// List of all configured forges and aliases /// List of all configured forges and aliases
AppConfigLoaded { app_config: ValidAppConfig }, AppConfigLoaded { app_config: ValidAppConfig },
@ -96,18 +95,11 @@ pub enum RepoUpdate {
MainUpdated, MainUpdated,
} }
message!(
SubscribeToUpdates,
Recipient<ServerUpdate>,
"Subscribe to receive updates from the server"
);
/// Sends a channel to be used to shutdown the server /// Sends a channel to be used to shutdown the server
#[derive(Message, Constructor)] #[derive(Constructor)]
#[rtype(result = "()")] pub struct ShutdownTrigger(Sender<String>);
pub struct ShutdownTrigger(std::sync::mpsc::Sender<String>);
impl ShutdownTrigger { impl ShutdownTrigger {
pub fn peel(self) -> std::sync::mpsc::Sender<String> { pub fn peel(self) -> Sender<String> {
self.0 self.0
} }
} }

View file

@ -1,7 +1,33 @@
// //
use actix::prelude::*; use std::{
collections::BTreeMap,
path::PathBuf,
sync::{Arc, RwLock},
};
use kameo::{actor::ActorRef, mailbox::unbounded::UnboundedMailbox, message::Context, Actor};
use kxio::{fs::FileSystem, net::Net};
use tokio::sync::mpsc::Sender;
use tracing::{error, instrument, warn};
use git_next_core::{
git::{repository::factory::RepositoryFactory, Generation, RepoDetails},
s,
server::{self, AppConfig, ListenUrl, Storage},
ForgeAlias, ForgeConfig, GitDir, RepoAlias, ServerRepoConfig, StoragePathType,
};
use crate::{
alerts::AlertsActor,
forge::Forge,
on_actor_link_died, on_actor_panic, on_actor_start, on_actor_stop,
repo::RepoActor,
spawn, tell,
webhook::{router::WebhookRouterActor, WebhookActor},
MessageBus,
};
use messages::{ReceiveAppConfig, ServerUpdate, Shutdown}; use messages::{ReceiveAppConfig, ServerUpdate, Shutdown};
use tracing::error;
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
@ -9,25 +35,6 @@ mod tests;
mod handlers; mod handlers;
pub mod messages; pub mod messages;
use crate::{
alerts::messages::NotifyUser, alerts::AlertsActor, forge::Forge, repo::RepoActor,
webhook::WebhookActor,
};
use git_next_core::{
git::{repository::factory::RepositoryFactory, Generation, RepoDetails},
server::{self, AppConfig, ListenUrl, Storage},
ForgeAlias, ForgeConfig, GitDir, RepoAlias, ServerRepoConfig, StoragePathType,
};
use kxio::{fs::FileSystem, net::Net};
use std::{
collections::BTreeMap,
path::PathBuf,
sync::{Arc, RwLock},
};
#[derive(Debug, derive_more::Display, derive_more::From)] #[derive(Debug, derive_more::Display, derive_more::From)]
pub enum Error { pub enum Error {
#[display("Failed to create data directories")] #[display("Failed to create data directories")]
@ -41,65 +48,112 @@ pub enum Error {
Config(server::Error), Config(server::Error),
Io(std::io::Error), Io(std::io::Error),
General(String),
} }
impl std::error::Error for Error {}
type Result<T> = core::result::Result<T, Error>; type Result<T> = core::result::Result<T, Error>;
#[allow(clippy::module_name_repetitions)] #[allow(clippy::module_name_repetitions)]
#[derive(derive_with::With)] #[derive(derive_with::With)]
#[with(message_log)] #[with(message_log)]
pub struct ServerActor { pub struct ServerActor {
ui: bool,
app_config: Option<AppConfig>, app_config: Option<AppConfig>,
generation: Generation, generation: Generation,
webhook_actor_addr: Option<Addr<WebhookActor>>, webhook_actor_ref: Option<ActorRef<WebhookActor>>,
fs: FileSystem, fs: FileSystem,
net: Net, net: Net,
alerts: Addr<AlertsActor>, alerts: ActorRef<AlertsActor>,
repository_factory: Box<dyn RepositoryFactory>, repository_factory: Box<dyn RepositoryFactory>,
sleep_duration: std::time::Duration, sleep_duration: std::time::Duration,
repo_actors: BTreeMap<(ForgeAlias, RepoAlias), Addr<RepoActor>>, repo_actors: BTreeMap<(ForgeAlias, RepoAlias), ActorRef<RepoActor>>,
shutdown_trigger: Option<std::sync::mpsc::Sender<String>>, shutdown_trigger: Option<Sender<String>>,
subscribers: Vec<Recipient<ServerUpdate>>, server_updates_bus: MessageBus<ServerUpdate>,
webhook_router_actor_ref: Option<ActorRef<WebhookRouterActor>>,
// testing // testing
message_log: Option<Arc<RwLock<Vec<String>>>>, message_log: Option<Arc<RwLock<Vec<String>>>>,
} }
impl Actor for ServerActor { impl Actor for ServerActor {
type Context = Context<Self>; type Mailbox = UnboundedMailbox<Self>;
on_actor_start!(this, actor_ref, {
let webhook_router = spawn!(actor_ref, WebhookRouterActor::default());
this.webhook_router_actor_ref.replace(webhook_router);
Ok(())
});
on_actor_panic!(this, actor_ref, err, {
warn!(%err, "paniced: {}", <Self as Actor>::name());
Ok(Some(kameo::error::ActorStopReason::Panicked(err)))
});
on_actor_link_died!(this, actor_ref, id, reason, {
match &reason {
kameo::error::ActorStopReason::Killed | kameo::error::ActorStopReason::Normal => {
Ok(None)
}
kameo::error::ActorStopReason::Panicked(_)
| kameo::error::ActorStopReason::LinkDied { .. } => {
if let Some(actor_ref) = actor_ref.upgrade() {
tell!(actor_ref, Shutdown)?;
}
if let Some(trigger) = this.shutdown_trigger.take() {
trigger
.send(s!("link died"))
.await
.map_err(|e| format!("failed sending shutdown trigger: {e:?}"))?;
}
Ok(Some(kameo::error::ActorStopReason::LinkDied {
id,
reason: Box::new(reason),
}))
}
}
});
on_actor_stop!(this, actor_ref, reason, Ok(()));
} }
impl ServerActor { impl ServerActor {
pub fn new( pub fn new(
ui: bool,
fs: FileSystem, fs: FileSystem,
net: Net, net: Net,
alerts: Addr<AlertsActor>, alerts: ActorRef<AlertsActor>,
server_updates_bus: MessageBus<ServerUpdate>,
repo: Box<dyn RepositoryFactory>, repo: Box<dyn RepositoryFactory>,
sleep_duration: std::time::Duration, sleep_duration: std::time::Duration,
) -> Self { ) -> Self {
let generation = Generation::default();
Self { Self {
ui,
app_config: None, app_config: None,
generation, generation: Generation::default(),
webhook_actor_addr: None, webhook_actor_ref: None,
fs, fs,
net, net,
alerts, alerts,
repository_factory: repo, repository_factory: repo,
shutdown_trigger: None, shutdown_trigger: None,
subscribers: Vec::default(), server_updates_bus,
webhook_router_actor_ref: None,
sleep_duration, sleep_duration,
repo_actors: BTreeMap::new(), repo_actors: BTreeMap::new(),
message_log: None, message_log: None,
} }
} }
fn create_forge_data_directories( fn create_forge_data_directories(
&self, &self,
app_config: &AppConfig, app_config: &AppConfig,
server_dir: &std::path::Path, server_dir: &std::path::Path,
) -> Result<()> { ) -> Result<()> {
for (forge_name, _forge_config) in app_config.forges() { for (forge_name, _forge_config) in app_config.forges() {
let forge_dir: PathBuf = (&forge_name).into(); let path = server_dir.join(PathBuf::from(&forge_name));
let path = server_dir.join(&forge_dir);
let path_handle = self.fs.path(&path); let path_handle = self.fs.path(&path);
if path_handle.exists()? { if path_handle.exists()? {
if !path_handle.is_dir()? { if !path_handle.is_dir()? {
@ -110,45 +164,37 @@ impl ServerActor {
self.fs.dir(&path).create_all()?; self.fs.dir(&path).create_all()?;
} }
} }
Ok(()) Ok(())
} }
#[instrument(skip_all)]
fn create_forge_repos( fn create_forge_repos(
&self, &self,
forge_config: &ForgeConfig, forge_config: &ForgeConfig,
forge_name: ForgeAlias, forge_name: ForgeAlias,
server_storage: &Storage, server_storage: &Storage,
listen_url: &ListenUrl, listen_url: &ListenUrl,
notify_user_recipient: &Recipient<NotifyUser>, notify_user_recipient: &ActorRef<AlertsActor>,
server_addr: Option<Addr<Self>>, server_actor_ref: ActorRef<Self>,
) -> Vec<(ForgeAlias, RepoAlias, RepoActor)> { ) -> Vec<(ForgeAlias, RepoAlias, RepoActor)> {
let span = tracing::info!(%forge_name, %forge_config, "");
tracing::info_span!("create_forge_repos", name = %forge_name, config = %forge_config);
let _guard = span.enter();
tracing::info!("Creating Forge");
let mut repos = vec![];
let creator = self.create_actor( let creator = self.create_actor(
forge_name, forge_name,
forge_config.clone(), forge_config.clone(),
server_storage, server_storage,
listen_url, listen_url,
server_addr, server_actor_ref,
); );
for (repo_alias, server_repo_config) in forge_config.repos() { forge_config
let forge_repo = creator(( .repos()
.map(|(repo_alias, server_repo_config)| {
creator((
repo_alias, repo_alias,
server_repo_config, server_repo_config,
notify_user_recipient.clone(), notify_user_recipient.clone(),
)); ))
tracing::info!( })
alias = %forge_repo.1, .collect::<Vec<_>>()
"Created Repo"
);
repos.push(forge_repo);
}
repos
} }
fn create_actor( fn create_actor(
@ -157,9 +203,9 @@ impl ServerActor {
forge_config: ForgeConfig, forge_config: ForgeConfig,
server_storage: &Storage, server_storage: &Storage,
listen_url: &ListenUrl, listen_url: &ListenUrl,
server_addr: Option<Addr<Self>>, server_actor_ref: ActorRef<Self>,
) -> impl Fn( ) -> impl Fn(
(RepoAlias, &ServerRepoConfig, Recipient<NotifyUser>), (RepoAlias, &ServerRepoConfig, ActorRef<AlertsActor>),
) -> (ForgeAlias, RepoAlias, RepoActor) { ) -> (ForgeAlias, RepoAlias, RepoActor) {
let server_storage = server_storage.clone(); let server_storage = server_storage.clone();
let listen_url = listen_url.clone(); let listen_url = listen_url.clone();
@ -167,12 +213,11 @@ impl ServerActor {
let repository_factory = self.repository_factory.duplicate(); let repository_factory = self.repository_factory.duplicate();
let generation = self.generation; let generation = self.generation;
let sleep_duration = self.sleep_duration; let sleep_duration = self.sleep_duration;
let ui = self.ui;
move |(repo_alias, server_repo_config, notify_user_recipient)| { move |(repo_alias, server_repo_config, notify_user_recipient)| {
let span = tracing::info_span!("create_actor", alias = %repo_alias, config = %server_repo_config); let span = tracing::info_span!("create_actor", alias = %repo_alias, config = %server_repo_config);
let _guard = span.enter(); let _guard = span.enter();
tracing::info!("Creating Repo"); let gitdir = server_repo_config.gitdir().unwrap_or_else(|| {
let gitdir = server_repo_config.gitdir().map_or_else(
|| {
GitDir::new( GitDir::new(
server_storage server_storage
.path() .path()
@ -180,9 +225,7 @@ impl ServerActor {
.join(repo_alias.to_string()), .join(repo_alias.to_string()),
StoragePathType::Internal, StoragePathType::Internal,
) )
}, });
|gitdir| gitdir,
);
// INFO: can't canonicalise gitdir as the path needs to exist to do that and we may not // INFO: can't canonicalise gitdir as the path needs to exist to do that and we may not
// have cloned the repo yet // have cloned the repo yet
let repo_details = RepoDetails::new( let repo_details = RepoDetails::new(
@ -194,8 +237,8 @@ impl ServerActor {
gitdir, gitdir,
); );
let forge = Forge::create(repo_details.clone(), net.clone()); let forge = Forge::create(repo_details.clone(), net.clone());
tracing::info!("Starting Repo Actor");
let actor = RepoActor::new( let actor = RepoActor::new(
ui,
repo_details, repo_details,
forge, forge,
listen_url.clone(), listen_url.clone(),
@ -204,7 +247,7 @@ impl ServerActor {
repository_factory.duplicate(), repository_factory.duplicate(),
sleep_duration, sleep_duration,
Some(notify_user_recipient), Some(notify_user_recipient),
server_addr.clone(), server_actor_ref.clone(),
); );
(forge_name.clone(), repo_alias, actor) (forge_name.clone(), repo_alias, actor)
} }
@ -231,31 +274,43 @@ impl ServerActor {
} }
/// Attempts to gracefully shutdown the server before stopping the system. /// Attempts to gracefully shutdown the server before stopping the system.
fn abort(&mut self, ctx: &<Self as actix::Actor>::Context, message: impl Into<String>) { async fn abort(
self.do_send(crate::server::actor::messages::Shutdown, ctx); &mut self,
ctx: &Context<'_, Self, color_eyre::Result<()>>,
message: impl Into<String> + Send,
) -> Result<()> {
let message = message.into();
error!(%message, "aborting");
self.do_send(Shutdown, ctx).await?;
if let Some(t) = self.shutdown_trigger.take() { if let Some(t) = self.shutdown_trigger.take() {
let _ = t.send(message.into()); t.send(message)
} else { .await
error!("{}", message.into()); .map_err(|e| format!("failed sending shutdown trigger: {e:?}"))?;
self.do_send(Shutdown, ctx);
// System::current().stop_with_code(1);
} }
Ok(())
} }
fn do_send<M>(&self, msg: M, ctx: &<Self as actix::Actor>::Context) async fn do_send<M>(
&self,
msg: M,
ctx: &Context<'_, Self, color_eyre::Result<()>>,
) -> Result<()>
where where
M: actix::Message + Send + 'static + std::fmt::Debug, M: Send + Sync + 'static + std::fmt::Debug,
Self: actix::Handler<M>, Self: kameo::message::Message<M>,
<M as actix::Message>::Result: Send, <Self as kameo::message::Message<M>>::Reply: Send + Sync + 'static + std::fmt::Debug,
<<Self as kameo::message::Message<M>>::Reply as kameo::Reply>::Error: std::fmt::Debug,
{ {
if let Some(message_log) = &self.message_log {
let log_message = format!("send: {msg:?}"); let log_message = format!("send: {msg:?}");
if let Some(message_log) = &self.message_log {
if let Ok(mut log) = message_log.write() { if let Ok(mut log) = message_log.write() {
log.push(log_message); log.push(log_message.clone());
} }
} }
if cfg!(not(test)) { if cfg!(not(test)) {
ctx.address().do_send(msg); tell!(ctx.actor_ref(), msg)
} .map_err(|e| format!("failed sending: {log_message}: {e:?}"))?;
}
Ok(())
} }
} }

View file

@ -1,10 +1,10 @@
//
use std::time::Duration; use std::time::Duration;
use actix::prelude::*; use kameo::actor::ActorRef;
use crate::alerts::{AlertsActor, History}; use crate::alerts::{AlertsActor, History};
//
pub fn a_filesystem() -> kxio::fs::TempFileSystem { pub fn a_filesystem() -> kxio::fs::TempFileSystem {
#[allow(clippy::expect_used)] #[allow(clippy::expect_used)]
kxio::fs::temp().expect("temp fs") kxio::fs::temp().expect("temp fs")
@ -14,6 +14,10 @@ pub fn a_network() -> kxio::net::MockNet {
kxio::net::mock() kxio::net::mock()
} }
pub fn an_alerts_actor(net: kxio::net::Net) -> Addr<AlertsActor> { pub fn an_alerts_actor(net: kxio::net::Net) -> ActorRef<AlertsActor> {
AlertsActor::new(None, History::new(Duration::from_millis(1)), net).start() kameo::spawn(AlertsActor::new(
None,
History::new(Duration::from_millis(1)),
net,
))
} }

View file

@ -1,19 +1,25 @@
// //
use actix::prelude::*;
use crate::server::actor::{tests::given, ReceiveAppConfig, ServerActor};
use git_next_core::{
git,
server::{AppConfig, Http, Listen, ListenUrl, Shout, Storage},
};
use std::{ use std::{
collections::BTreeMap, collections::BTreeMap,
sync::{Arc, RwLock}, sync::{Arc, RwLock},
}; };
#[test_log::test(actix::test)] use kameo::actor::pubsub::PubSub;
async fn when_webhook_url_has_trailing_slash_should_not_send() {
use git_next_core::{
git,
server::{AppConfig, Http, Listen, ListenUrl, Shout, Storage},
};
use crate::{
server::actor::{tests::given, ReceiveAppConfig, ServerActor},
tell,
};
type TestResult = Result<(), Box<dyn std::error::Error>>;
#[test_log::test(tokio::test)]
async fn when_webhook_url_has_trailing_slash_should_not_send() -> TestResult {
//given //given
// parameters // parameters
let fs = given::a_filesystem(); let fs = given::a_filesystem();
@ -22,8 +28,18 @@ async fn when_webhook_url_has_trailing_slash_should_not_send() {
let repo = git::repository::factory::mock(); let repo = git::repository::factory::mock();
let duration = std::time::Duration::from_millis(1); let duration = std::time::Duration::from_millis(1);
let file_update_subs = kameo::spawn(PubSub::new());
// sut // sut
let server = ServerActor::new(fs.as_real(), net.into(), alerts, repo, duration); let server = ServerActor::new(
false, // ui
fs.as_real(),
net.into(),
alerts,
file_update_subs,
repo,
duration,
);
// collaborators // collaborators
let listen = Listen::new( let listen = Listen::new(
@ -39,13 +55,11 @@ async fn when_webhook_url_has_trailing_slash_should_not_send() {
let server = server.with_message_log(Some(message_log.clone())); let server = server.with_message_log(Some(message_log.clone()));
//when //when
server.start().do_send(ReceiveAppConfig::new(AppConfig::new( tell!(
listen, kameo::spawn(server),
shout, ReceiveAppConfig::new(AppConfig::new(listen, shout, server_storage, repos,))
server_storage, )?;
repos, tokio::time::sleep(std::time::Duration::from_millis(1)).await;
)));
actix_rt::time::sleep(std::time::Duration::from_millis(1)).await;
//then //then
// INFO: assert that ReceiveValidServerConfig is NOT sent // INFO: assert that ReceiveValidServerConfig is NOT sent
@ -53,4 +67,6 @@ async fn when_webhook_url_has_trailing_slash_should_not_send() {
assert!(message_log.read().iter().any(|log| !log assert!(message_log.read().iter().any(|log| !log
.iter() .iter()
.any(|line| line == "send: ReceiveValidServerConfig"))); .any(|line| line == "send: ReceiveValidServerConfig")));
Ok(())
} }

View file

@ -1,35 +1,22 @@
// //
use std::{path::PathBuf, sync::Arc};
use color_eyre::{eyre::Context, Result};
use kxio::{fs::FileSystem, net::Net};
use tokio::sync::RwLock;
use tracing::info;
use git_next_core::git::RepositoryFactory;
use crate::{root::RootActor, tell};
pub use actor::ServerActor;
pub mod actor; pub mod actor;
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
use actix::prelude::*;
use actix_rt::signal;
use actor::messages::ShutdownTrigger;
use crate::{
alerts::{AlertsActor, History},
file_watcher::{watch_file, FileUpdated},
};
#[allow(clippy::module_name_repetitions)]
pub use actor::ServerActor;
use git_next_core::git::RepositoryFactory;
use color_eyre::{eyre::Context, Result};
use kxio::{fs::FileSystem, net::Net};
use tracing::info;
use std::{
path::PathBuf,
sync::{atomic::Ordering, mpsc::channel, Arc, RwLock},
time::Duration,
};
const A_DAY: Duration = Duration::from_secs(24 * 60 * 60);
pub fn init(fs: &FileSystem) -> Result<()> { pub fn init(fs: &FileSystem) -> Result<()> {
let file_name = "git-next-server.toml"; let file_name = "git-next-server.toml";
let pathbuf = PathBuf::from(file_name); let pathbuf = PathBuf::from(file_name);
@ -51,8 +38,8 @@ pub fn init(fs: &FileSystem) -> Result<()> {
#[allow(clippy::too_many_lines)] #[allow(clippy::too_many_lines)]
pub fn start( pub fn start(
ui: bool, ui: bool,
fs: FileSystem, fs: &FileSystem,
net: Net, net: &Net,
repo: Box<dyn RepositoryFactory>, repo: Box<dyn RepositoryFactory>,
sleep_duration: std::time::Duration, sleep_duration: std::time::Duration,
) -> Result<()> { ) -> Result<()> {
@ -66,111 +53,48 @@ pub fn start(
} }
let shutdown_message_holder: Arc<RwLock<Option<String>>> = Arc::new(RwLock::new(None)); let shutdown_message_holder: Arc<RwLock<Option<String>>> = Arc::new(RwLock::new(None));
let shutdown_message_holder_exec = shutdown_message_holder.clone(); let shutdown_message_holder_clone = shutdown_message_holder.clone();
let file_watcher_err_holder: Arc<RwLock<Option<anyhow::Error>>> = Arc::new(RwLock::new(None));
let file_watcher_err_holder_exec = file_watcher_err_holder.clone();
let execution = async move {
info!("Starting Alert Dispatcher...");
let alerts_addr = AlertsActor::new(None, History::new(A_DAY), net.clone()).start();
info!("Starting Server..."); #[allow(clippy::expect_used)]
let server = tokio::runtime::Runtime::new()?.block_on(async {
ServerActor::new(fs.clone(), net.clone(), alerts_addr, repo, sleep_duration).start(); let (tx_shutdown, mut rx_shutdown) = tokio::sync::mpsc::channel::<String>(1);
info!("Starting File Watcher..."); let root_actor_ref = kameo::spawn(RootActor::new(
let watch_file = watch_file("git-next-server.toml".into(), server.clone().recipient()); ui,
let fw_shutdown = match watch_file { fs.clone(),
Ok(fw_shutdown) => fw_shutdown, net.clone(),
Err(err) => { sleep_duration,
// shutdown now tx_shutdown,
server.do_send(crate::server::actor::messages::Shutdown); ));
actix_rt::time::sleep(std::time::Duration::from_millis(10)).await; tell!("root", root_actor_ref, crate::root::Start::new(repo)).expect("start root actor");
System::current().stop();
let _ = file_watcher_err_holder_exec
.write()
.map(|mut o| o.replace(err));
return;
}
};
let (tx_shutdown, rx_shutdown) = channel::<String>();
if ui {
#[cfg(feature = "tui")]
{
use crate::server::actor::messages::SubscribeToUpdates;
use crate::tui;
let tui_addr = tui::Tui::new(tx_shutdown.clone()).start();
server.do_send(SubscribeToUpdates::new(tui_addr.clone().recipient()));
server.do_send(ShutdownTrigger::new(tx_shutdown));
server.do_send(FileUpdated); // update file after ui subscription in place
loop {
let _ = tui_addr.send(tui::Tick).await;
if let Ok(message) = rx_shutdown.try_recv() {
let _ = shutdown_message_holder_exec
.write()
.map(|mut o| o.replace(message));
break;
}
actix_rt::time::sleep(Duration::from_millis(16)).await;
}
}
} else {
server.do_send(ShutdownTrigger::new(tx_shutdown.clone()));
server.do_send(FileUpdated);
info!("Server running - Press Ctrl-C to stop..."); info!("Server running - Press Ctrl-C to stop...");
tokio::select! { tokio::select! {
_r = signal::ctrl_c() => { _r = tokio::signal::ctrl_c() => {
info!("Ctrl-C received, shutting down..."); info!("Ctrl-C received, shutting down...");
} }
_x = async move { _x = async move {
loop{ loop{
if let Ok(message) = rx_shutdown.try_recv() { if let Some(message) = rx_shutdown.recv().await {
let _ = shutdown_message_holder_exec info!("rx shutdown received: {message}");
.write() let _ = shutdown_message_holder_clone.write().await.replace(message);
.map(|mut o| o.replace(message)); tokio::time::sleep(std::time::Duration::from_millis(100)).await;
break; break;
} }
actix_rt::task::yield_now().await;
} }
} => { } => {
info!("signaled shutdown"); info!("signaled shutdown");
} }
}; };
} });
// shutdown
fw_shutdown.store(true, Ordering::Relaxed);
server.do_send(crate::server::actor::messages::Shutdown);
actix_rt::time::sleep(std::time::Duration::from_millis(10)).await;
System::current().stop();
};
let system = System::new();
Arbiter::current().spawn(execution);
system.run()?;
// check for error from server thread
#[allow(clippy::unwrap_used)]
if let Some(err) = &*shutdown_message_holder.read().unwrap() {
#[cfg(feature = "tui")] #[cfg(feature = "tui")]
if ui { if ui {
ratatui::restore(); ratatui::restore();
} }
if !err.is_empty() {
return Err(color_eyre::eyre::eyre!(format!("{err}")));
}
}
// check for error from file watcher thread if let Some(ref message) = *shutdown_message_holder.blocking_write() {
#[allow(clippy::unwrap_used)] info!(%message, "shutdown");
if let Some(err) = &*file_watcher_err_holder.read().unwrap() {
#[cfg(feature = "tui")]
if ui {
ratatui::restore();
}
return Err(color_eyre::eyre::eyre!(format!("{err}")));
} }
Ok(()) Ok(())

View file

@ -38,44 +38,3 @@ mod init {
Ok(()) Ok(())
} }
} }
mod file_watcher {
use std::{sync::atomic::Ordering, time::Duration};
use actix::{Actor, Context, Handler};
use rstest::*;
use crate::file_watcher::{self, FileUpdated};
use super::TestResult;
#[rstest]
#[actix::test]
#[timeout(Duration::from_millis(80))]
async fn should_not_block_calling_thread() -> TestResult {
let fs = kxio::fs::temp()?;
let path = fs.base().join("file");
fs.file(&path).write("foo")?;
let listener = Listener;
let l_addr = listener.start();
let recipient = l_addr.recipient();
let fw_shutdown = file_watcher::watch_file(path, recipient)?;
std::thread::sleep(Duration::from_millis(10));
fw_shutdown.store(true, Ordering::Relaxed);
Ok(()) // was not blocked
}
struct Listener;
impl Actor for Listener {
type Context = Context<Self>;
}
impl Handler<FileUpdated> for Listener {
type Result = ();
fn handle(&mut self, _msg: FileUpdated, _ctx: &mut Self::Context) -> Self::Result {
// todo!()
}
}
}

View file

@ -1,6 +1,7 @@
// //
use actix::Handler; use kameo::message::{Context, Message};
use ratatui::style::Color; use ratatui::style::Color;
use tracing::debug;
use crate::{ use crate::{
server::actor::messages::{RepoUpdate, ServerUpdate}, server::actor::messages::{RepoUpdate, ServerUpdate},
@ -12,10 +13,14 @@ static PREP: Color = Color::Gray;
static ACTING: Color = Color::LightBlue; static ACTING: Color = Color::LightBlue;
static WARN: Color = Color::Red; static WARN: Color = Color::Red;
impl Handler<ServerUpdate> for Tui { impl Message<ServerUpdate> for Tui {
type Result = (); type Reply = ();
fn handle(&mut self, msg: ServerUpdate, _ctx: &mut Self::Context) -> Self::Result { async fn handle(
&mut self,
msg: ServerUpdate,
_ctx: Context<'_, Self, Self::Reply>,
) -> Self::Reply {
self.state.tap(); self.state.tap();
match msg { match msg {
ServerUpdate::AppConfigLoaded { app_config } => { ServerUpdate::AppConfigLoaded { app_config } => {
@ -29,12 +34,23 @@ impl Handler<ServerUpdate> for Tui {
} => { } => {
if let ServerState::Configured { forges } = &mut self.state.mode { if let ServerState::Configured { forges } = &mut self.state.mode {
let Some(forge_state) = forges.get_mut(&forge_alias) else { let Some(forge_state) = forges.get_mut(&forge_alias) else {
debug!("ServerState::Configured: no forge state available");
return; return;
}; };
let Some(repo_state) = forge_state.repos.get_mut(&repo_alias) else { let Some(repo_state) = forge_state.repos.get_mut(&repo_alias) else {
debug!("ServerState::Configured: no repo state available");
return; return;
}; };
repo_state.clear_alert(); repo_state.clear_alert();
debug!(?repo_update, "ServerState::Configured: RepoUpdate");
handle_repo_update(repo_update, repo_state);
}
}
}
}
}
fn handle_repo_update(repo_update: RepoUpdate, repo_state: &mut crate::tui::actor::RepoState) {
match repo_update { match repo_update {
RepoUpdate::Branches { branches } => { RepoUpdate::Branches { branches } => {
repo_state.update_branches(branches); repo_state.update_branches(branches);
@ -42,7 +58,9 @@ impl Handler<ServerUpdate> for Tui {
RepoUpdate::Log { log } => { RepoUpdate::Log { log } => {
repo_state.update_log(log); repo_state.update_log(log);
} }
RepoUpdate::ValidateRepo => repo_state.update_message("polling...", ACTING), RepoUpdate::ValidateRepo => {
repo_state.update_message("polling...", ACTING);
}
RepoUpdate::Okay { main, next, dev } => { RepoUpdate::Okay { main, next, dev } => {
repo_state.clear_alert(); repo_state.clear_alert();
repo_state.update_message("okay", OKAY); repo_state.update_message("okay", OKAY);
@ -55,15 +73,13 @@ impl Handler<ServerUpdate> for Tui {
repo_state.update_message("Checking CI status", ACTING); repo_state.update_message("Checking CI status", ACTING);
} }
RepoUpdate::AdvancingNext { commit, force: _ } => { RepoUpdate::AdvancingNext { commit, force: _ } => {
repo_state repo_state.update_message(format!("advancing next to {commit}"), ACTING);
.update_message(format!("advancing next to {commit}"), ACTING);
} }
RepoUpdate::NextUpdated => { RepoUpdate::NextUpdated => {
repo_state.update_message("next updated - pause while CI starts", OKAY); repo_state.update_message("next updated - pause while CI starts", OKAY);
} }
RepoUpdate::AdvancingMain { commit } => { RepoUpdate::AdvancingMain { commit } => {
repo_state repo_state.update_message(format!("advancing main to {commit}"), ACTING);
.update_message(format!("advancing main to {commit}"), ACTING);
} }
RepoUpdate::MainUpdated => { RepoUpdate::MainUpdated => {
repo_state.update_message("main updated", OKAY); repo_state.update_message("main updated", OKAY);
@ -90,15 +106,10 @@ impl Handler<ServerUpdate> for Tui {
repo_state.update_message("unregistering webhook...", PREP); repo_state.update_message("unregistering webhook...", PREP);
} }
RepoUpdate::WebhookReceived { branch, push: _ } => { RepoUpdate::WebhookReceived { branch, push: _ } => {
repo_state repo_state.update_message(format!("webhook update: {branch:?}"), ACTING);
.update_message(format!("webhook update: {branch:?}"), ACTING);
} }
RepoUpdate::RegisteredWebhook => { RepoUpdate::RegisteredWebhook => {
repo_state.update_message("registered webhook", PREP); repo_state.update_message("registered webhook", PREP);
} }
} }
} }
}
}
}
}

View file

@ -1,15 +1,25 @@
// //
use actix::Handler; use std::time::Duration;
use crate::tui::actor::{messages::Tick, Tui}; use color_eyre::Result;
use kameo::message::{Context, Message};
impl Handler<Tick> for Tui { use crate::{
type Result = std::io::Result<()>; tell,
tui::actor::{messages::Tick, Tui},
};
fn handle(&mut self, _msg: Tick, ctx: &mut Self::Context) -> Self::Result { impl Message<Tick> for Tui {
type Reply = Result<()>;
async fn handle(&mut self, _msg: Tick, ctx: Context<'_, Self, Self::Reply>) -> Self::Reply {
self.state.tap(); self.state.tap();
self.draw()?; self.draw()?;
self.handle_input(ctx)?; self.handle_input(&ctx.actor_ref())?;
tokio::time::sleep(Duration::from_millis(16)).await;
tell!(ctx.actor_ref(), Tick)?;
Ok(()) Ok(())
} }
} }

View file

@ -6,61 +6,85 @@ mod model;
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
use std::sync::mpsc::Sender; use color_eyre::Result;
use kameo::{
use actix::{Actor, ActorContext as _, Context}; actor::{ActorRef, WeakActorRef},
error::{ActorStopReason, BoxError, PanicError},
pub use model::*; mailbox::unbounded::UnboundedMailbox,
Actor,
};
use ratatui::{ use ratatui::{
crossterm::event::{self, KeyCode, KeyEventKind}, crossterm::event::{self, KeyCode, KeyEventKind},
DefaultTerminal, DefaultTerminal,
}; };
use tui_scrollview::ScrollViewState; use tui_scrollview::ScrollViewState;
pub use model::*;
use crate::tell;
use super::Tick;
#[derive(Debug)] #[derive(Debug)]
pub struct Tui { pub struct Tui {
terminal: Option<DefaultTerminal>, terminal: Option<DefaultTerminal>,
signal_shutdown: Sender<String>,
pub state: State, pub state: State,
scroll_view_state: ScrollViewState, scroll_view_state: ScrollViewState,
} }
impl Actor for Tui { impl Actor for Tui {
type Context = Context<Self>; type Mailbox = UnboundedMailbox<Self>;
fn started(&mut self, _ctx: &mut Self::Context) {
async fn on_start(&mut self, actor_ref: ActorRef<Self>) -> Result<(), BoxError> {
self.terminal.replace(ratatui::init()); self.terminal.replace(ratatui::init());
tell!(actor_ref, Tick)?;
Ok(())
} }
fn stopped(&mut self, _ctx: &mut Self::Context) {
async fn on_stop(
&mut self,
_actor_ref: WeakActorRef<Self>,
_reason: ActorStopReason,
) -> Result<(), BoxError> {
self.terminal.take(); self.terminal.take();
ratatui::restore(); ratatui::restore();
Ok(())
}
async fn on_panic(
&mut self,
_actor_ref: WeakActorRef<Self>,
err: PanicError,
) -> Result<Option<ActorStopReason>, BoxError> {
self.terminal.take();
ratatui::restore();
Ok(Some(ActorStopReason::Panicked(err)))
} }
} }
impl Tui { impl Tui {
pub fn new(signal_shutdown: Sender<String>) -> Self { pub fn new() -> Self {
Self { Self {
terminal: None, terminal: None,
signal_shutdown,
state: State::initial(), state: State::initial(),
scroll_view_state: ScrollViewState::default(), scroll_view_state: ScrollViewState::default(),
} }
} }
fn draw(&mut self) -> std::io::Result<()> { fn draw(&mut self) -> std::io::Result<()> {
let t = self.terminal.take(); if let Some(terminal) = &mut self.terminal {
let scroll_view_state = &mut self.scroll_view_state;
let state = &self.state;
if let Some(mut terminal) = t {
terminal.draw(|frame| { terminal.draw(|frame| {
frame.render_stateful_widget(state, frame.area(), scroll_view_state); frame.render_stateful_widget(
&self.state,
frame.area(),
&mut self.scroll_view_state,
);
})?; })?;
self.terminal = Some(terminal);
} else { } else {
eprintln!("No terminal setup"); eprintln!("No terminal setup");
} }
Ok(()) Ok(())
} }
fn handle_input(&mut self, ctx: &mut <Self as actix::Actor>::Context) -> std::io::Result<()> { fn handle_input(&mut self, actor_tui: &ActorRef<Self>) -> Result<()> {
if event::poll(std::time::Duration::from_millis(16))? { if event::poll(std::time::Duration::from_millis(16))? {
let event::Event::Key(key) = event::read()? else { let event::Event::Key(key) = event::read()? else {
return Ok(()); return Ok(());
@ -70,10 +94,7 @@ impl Tui {
} }
match key.code { match key.code {
KeyCode::Char('q') => { KeyCode::Char('q') => {
ctx.stop(); actor_tui.kill();
if let Err(err) = self.signal_shutdown.send(String::new()) {
tracing::error!(?err, "Failed to signal shutdown");
}
} }
KeyCode::Char('j') | KeyCode::Down => self.scroll_view_state.scroll_down(), KeyCode::Char('j') | KeyCode::Down => self.scroll_view_state.scroll_down(),
KeyCode::Char('k') | KeyCode::Up => self.scroll_view_state.scroll_up(), KeyCode::Char('k') | KeyCode::Up => self.scroll_view_state.scroll_up(),

View file

@ -1,4 +1,6 @@
// //
use std::{collections::BTreeMap, fmt::Display, time::Instant};
use ratatui::{ use ratatui::{
layout::Alignment, layout::Alignment,
prelude::{Buffer, Rect}, prelude::{Buffer, Rect},
@ -7,15 +9,13 @@ use ratatui::{
text::{Line, Span}, text::{Line, Span},
widgets::{Block, Paragraph, StatefulWidget, Widget}, widgets::{Block, Paragraph, StatefulWidget, Widget},
}; };
use tracing::info;
use tui_scrollview::ScrollViewState;
use git_next_core::{ use git_next_core::{
git::{self, graph::Log, Commit}, git::{self, graph::Log, Commit},
ForgeAlias, RepoAlias, RepoBranches, ForgeAlias, RepoAlias, RepoBranches,
}; };
use tracing::info;
use tui_scrollview::ScrollViewState;
use std::{collections::BTreeMap, fmt::Display, time::Instant};
use crate::{server::actor::messages::ValidAppConfig, tui::components::ConfiguredAppWidget}; use crate::{server::actor::messages::ValidAppConfig, tui::components::ConfiguredAppWidget};

View file

@ -1 +0,0 @@
mod shutdown_webhook;

View file

@ -1,13 +0,0 @@
//
use actix::prelude::*;
use crate::webhook::{messages::ShutdownWebhook, WebhookActor};
impl Handler<ShutdownWebhook> for WebhookActor {
type Result = ();
fn handle(&mut self, _msg: ShutdownWebhook, ctx: &mut Self::Context) -> Self::Result {
self.spawn_handle.take();
ctx.stop();
}
}

View file

@ -1,4 +0,0 @@
//
use git_next_core::message;
message!(ShutdownWebhook, "Request to shutdown the Webhook actor");

View file

@ -1,43 +1,56 @@
// //
use actix::prelude::*;
mod handlers;
pub mod messages;
pub mod router;
mod server;
use crate::repo::messages::WebhookNotification;
use std::net::SocketAddr; use std::net::SocketAddr;
use tracing::Instrument; use color_eyre::Result;
use kameo::{actor::ActorRef, mailbox::unbounded::UnboundedMailbox, message::Message, Actor};
use router::WebhookRouterActor;
use crate::{
default_on_actor_link_died, default_on_actor_panic, default_on_actor_start,
default_on_actor_stop,
};
pub mod router;
mod server;
#[allow(clippy::module_name_repetitions)] #[allow(clippy::module_name_repetitions)]
#[derive(Debug)] #[derive(Debug)]
pub struct WebhookActor { pub struct WebhookActor {
socket_addr: SocketAddr, socket_addr: SocketAddr,
span: tracing::Span, message_receiver: ActorRef<WebhookRouterActor>,
spawn_handle: Option<actix::SpawnHandle>,
message_receiver: Recipient<WebhookNotification>,
} }
impl WebhookActor { impl WebhookActor {
pub fn new(socket_addr: SocketAddr, message_receiver: Recipient<WebhookNotification>) -> Self { pub const fn new(
let span = tracing::info_span!("WebhookActor"); socket_addr: SocketAddr,
message_receiver: ActorRef<WebhookRouterActor>,
) -> Self {
Self { Self {
socket_addr, socket_addr,
span,
message_receiver, message_receiver,
spawn_handle: None,
} }
} }
} }
impl Actor for WebhookActor { impl Actor for WebhookActor {
type Context = actix::Context<Self>; type Mailbox = UnboundedMailbox<Self>;
fn started(&mut self, ctx: &mut Self::Context) {
let _gaurd = self.span.enter(); default_on_actor_start!(this, actor_ref);
let address: Recipient<WebhookNotification> = self.message_receiver.clone(); default_on_actor_panic!(this, actor_ref, err);
let server = server::start(self.socket_addr, address); default_on_actor_link_died!(this, actor_ref, id, reason);
let spawn_handle = ctx.spawn(server.in_current_span().into_actor(self)); default_on_actor_stop!(this, actor_ref, reason);
self.spawn_handle.replace(spawn_handle); }
#[derive(Debug)]
pub struct Start;
impl Message<Start> for WebhookActor {
type Reply = Result<()>;
async fn handle(
&mut self,
_msg: Start,
_ctx: kameo::message::Context<'_, Self, Self::Reply>,
) -> Self::Reply {
server::start(self.socket_addr, self.message_receiver.clone()).await?;
Ok(())
} }
} }

View file

@ -1,26 +1,28 @@
// //
use actix::prelude::*;
use derive_more::Constructor;
use tracing::{debug, info, warn};
use std::collections::BTreeMap; use std::collections::BTreeMap;
use crate::repo::messages::WebhookNotification; use derive_more::Constructor;
use kameo::{
actor::ActorRef,
message::{Context, Message},
Actor,
};
use tracing::{debug, warn};
use git_next_core::{ForgeAlias, RepoAlias}; use git_next_core::{ForgeAlias, RepoAlias};
use crate::{
repo::{messages::WebhookNotification, RepoActor},
tell,
};
#[derive(Actor)]
pub struct WebhookRouterActor { pub struct WebhookRouterActor {
span: tracing::Span, span: tracing::Span,
recipients: BTreeMap<ForgeAlias, BTreeMap<RepoAlias, Recipient<WebhookNotification>>>, recipients: BTreeMap<ForgeAlias, BTreeMap<RepoAlias, ActorRef<RepoActor>>>,
} }
impl Default for WebhookRouterActor { impl Default for WebhookRouterActor {
fn default() -> Self { fn default() -> Self {
Self::new()
}
}
impl WebhookRouterActor {
pub fn new() -> Self {
let span = tracing::info_span!("WebhookRouter"); let span = tracing::info_span!("WebhookRouter");
Self { Self {
span, span,
@ -28,43 +30,45 @@ impl WebhookRouterActor {
} }
} }
} }
impl Actor for WebhookRouterActor { impl Message<WebhookNotification> for WebhookRouterActor {
type Context = Context<Self>; type Reply = color_eyre::Result<()>;
}
impl Handler<WebhookNotification> for WebhookRouterActor { async fn handle(
type Result = (); &mut self,
msg: WebhookNotification,
fn handle(&mut self, msg: WebhookNotification, _ctx: &mut Self::Context) -> Self::Result { _ctx: Context<'_, Self, Self::Reply>,
) -> Self::Reply {
let _gaurd = self.span.enter(); let _gaurd = self.span.enter();
let forge_alias = msg.forge_alias(); let forge_alias = msg.forge_alias();
let repo_alias = msg.repo_alias(); let repo_alias = msg.repo_alias();
debug!(forge = %forge_alias, repo = %repo_alias, "Router..."); debug!(forge = %forge_alias, repo = %repo_alias, "Router...");
let Some(forge_repos) = self.recipients.get(forge_alias) else { let Some(forge_repos) = self.recipients.get(forge_alias) else {
warn!(forge = %forge_alias, "No forge repos found"); warn!(forge = %forge_alias, "No forge repos found");
return; return Ok(());
}; };
let Some(recipient) = forge_repos.get(repo_alias) else { let Some(recipient) = forge_repos.get(repo_alias) else {
debug!(forge = %forge_alias, repo = %repo_alias, "No recipient found"); debug!(forge = %forge_alias, repo = %repo_alias, "No recipient found");
return; return Ok(());
}; };
recipient.do_send(msg); tell!(recipient, msg)?;
Ok(())
} }
} }
#[derive(Message, Constructor)] #[derive(Constructor, Debug)]
#[rtype(result = "()")]
pub struct AddWebhookRecipient { pub struct AddWebhookRecipient {
pub forge_alias: ForgeAlias, pub forge_alias: ForgeAlias,
pub repo_alias: RepoAlias, pub repo_alias: RepoAlias,
pub recipient: Recipient<WebhookNotification>, pub recipient: ActorRef<RepoActor>,
} }
impl Handler<AddWebhookRecipient> for WebhookRouterActor { impl Message<AddWebhookRecipient> for WebhookRouterActor {
type Result = (); type Reply = ();
fn handle(&mut self, msg: AddWebhookRecipient, _ctx: &mut Self::Context) -> Self::Result { async fn handle(
let _gaurd = self.span.enter(); &mut self,
info!(forge = %msg.forge_alias, repo = %msg.repo_alias, "Register Recipient"); msg: AddWebhookRecipient,
_ctx: Context<'_, Self, Self::Reply>,
) -> Self::Reply {
if !self.recipients.contains_key(&msg.forge_alias) { if !self.recipients.contains_key(&msg.forge_alias) {
self.recipients self.recipients
.insert(msg.forge_alias.clone(), BTreeMap::new()); .insert(msg.forge_alias.clone(), BTreeMap::new());

View file

@ -1,18 +1,17 @@
// //
use actix::prelude::*;
use std::{collections::BTreeMap, net::SocketAddr}; use std::{collections::BTreeMap, net::SocketAddr};
use color_eyre::Result;
use kameo::actor::ActorRef;
use tracing::{info, warn}; use tracing::{info, warn};
use crate::repo::messages::WebhookNotification;
use git_next_core::{webhook, ForgeAlias, ForgeNotification, RepoAlias}; use git_next_core::{webhook, ForgeAlias, ForgeNotification, RepoAlias};
pub async fn start( use crate::{repo::messages::WebhookNotification, tell};
socket_addr: SocketAddr,
address: actix::prelude::Recipient<WebhookNotification>, use super::router::WebhookRouterActor;
) {
pub async fn start(socket_addr: SocketAddr, address: ActorRef<WebhookRouterActor>) -> Result<()> {
// start webhook server // start webhook server
use warp::Filter; use warp::Filter;
// Define the Warp route to handle incoming HTTP requests // Define the Warp route to handle incoming HTTP requests
@ -23,37 +22,28 @@ pub async fn start(
.and(warp::header::headers_cloned()) .and(warp::header::headers_cloned())
.and(warp::body::bytes()) .and(warp::body::bytes())
.and_then( .and_then(
|recipient: Recipient<WebhookNotification>, |recipient: ActorRef<WebhookRouterActor>,
forge_alias: String, forge_alias: String,
repo_alias: String, repo_alias: String,
// query: String, // query: String,
headers: warp::http::HeaderMap, headers: warp::http::HeaderMap,
body: bytes::Bytes| async move { body: bytes::Bytes| async move {
info!("POST received"); info!("POST received");
let forge_alias = ForgeAlias::new(forge_alias); let msg = WebhookNotification::new(ForgeNotification::new(
let repo_alias = RepoAlias::new(repo_alias); ForgeAlias::new(forge_alias),
let bytes = body.to_vec(); RepoAlias::new(repo_alias),
let body = webhook::forge_notification::Body::new( headers
String::from_utf8_lossy(&bytes).to_string(),
);
let headers = headers
.into_iter() .into_iter()
.filter_map(|(k, v)| { .filter_map(|(k, v)| {
k.map(|k| (k.to_string(), v.to_str().unwrap_or_default().to_string())) k.map(|k| (k.to_string(), v.to_str().unwrap_or_default().to_string()))
}) })
.collect::<BTreeMap<String, String>>(); .collect::<BTreeMap<String, String>>(),
let message = WebhookNotification::new(ForgeNotification::new( webhook::forge_notification::Body::new(
forge_alias, String::from_utf8_lossy(&body).to_string(),
repo_alias, ),
headers,
body,
)); ));
recipient tell!(recipient, msg)
.try_send(message) .map(|()| warp::reply::with_status("OK", warp::http::StatusCode::OK))
.map(|()| {
info!("Message sent ok");
warp::reply::with_status("OK", warp::http::StatusCode::OK)
})
.map_err(|e| { .map_err(|e| {
warn!("Unknown error: {:?}", e); warn!("Unknown error: {:?}", e);
warp::reject() warp::reject()
@ -63,5 +53,9 @@ pub async fn start(
// Start the server // Start the server
info!("Starting webhook server: {}", socket_addr); info!("Starting webhook server: {}", socket_addr);
warp::serve(route).run(socket_addr).await; let (socket, f) = warp::serve(route).try_bind_ephemeral(socket_addr)?;
info!("Server bound to {socket}");
f.await;
Ok(())
} }

View file

@ -27,9 +27,6 @@ tracing = { workspace = true }
# fs/network # fs/network
kxio = { workspace = true } kxio = { workspace = true }
# Actors
actix = { workspace = true }
# TOML parsing # TOML parsing
serde = { workspace = true } serde = { workspace = true }
toml = { workspace = true } toml = { workspace = true }

View file

@ -352,3 +352,13 @@ impl SmtpConfig {
&self.hostname &self.hostname
} }
} }
#[cfg(test)]
mod tests {
const fn is_sendable<T: Send>() {}
#[test]
const fn normal() {
is_sendable::<super::AppConfig>();
}
}

View file

@ -50,14 +50,11 @@ pub fn open(
let open_repository = if repo_details.gitdir.exists() { let open_repository = if repo_details.gitdir.exists() {
info!("Local copy found - opening..."); info!("Local copy found - opening...");
let repo = repository_factory.open(repo_details)?; repository_factory.open(repo_details)?
repo.fetch()?;
repo
} else { } else {
info!("Local copy not found - cloning..."); info!("Local copy not found - cloning...");
repository_factory.git_clone(repo_details)? repository_factory.git_clone(repo_details)?
}; };
info!("Validating...");
validate_default_remotes(&*open_repository, repo_details) validate_default_remotes(&*open_repository, repo_details)
.map_err(|e| Error::Validation(s!(e)))?; .map_err(|e| Error::Validation(s!(e)))?;
Ok(open_repository) Ok(open_repository)

View file

@ -63,7 +63,7 @@ pub(crate) fn test(
#[allow(clippy::module_name_repetitions)] #[allow(clippy::module_name_repetitions)]
#[mockall::automock] #[mockall::automock]
pub trait OpenRepositoryLike: std::fmt::Debug + Sync { pub trait OpenRepositoryLike: std::fmt::Debug + Sync + Send {
/// Creates a clone of the `OpenRepositoryLike`. /// Creates a clone of the `OpenRepositoryLike`.
fn duplicate(&self) -> Box<dyn OpenRepositoryLike>; fn duplicate(&self) -> Box<dyn OpenRepositoryLike>;

View file

@ -4,8 +4,6 @@ use crate::{
s, RemoteUrl, s, RemoteUrl,
}; };
use tracing::info;
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
pub fn validate_default_remotes( pub fn validate_default_remotes(
open_repository: &dyn OpenRepositoryLike, open_repository: &dyn OpenRepositoryLike,
@ -22,7 +20,6 @@ pub fn validate_default_remotes(
"Unable to build forge url" "Unable to build forge url"
))); )));
}; };
info!(config = %remote_url, push = %push_remote, fetch = %fetch_remote, "Check remotes match");
if !remote_url.matches(&push_remote) { if !remote_url.matches(&push_remote) {
return Err(Error::MismatchDefaultPushRemote { return Err(Error::MismatchDefaultPushRemote {
found: push_remote, found: push_remote,

View file

@ -2,26 +2,14 @@
macro_rules! message { macro_rules! message {
($name:ident, $value:ty, $docs:literal) => { ($name:ident, $value:ty, $docs:literal) => {
git_next_core::newtype!($name, $value, $docs); git_next_core::newtype!($name, $value, $docs);
impl actix::prelude::Message for $name {
type Result = ();
}
}; };
($name:ident, $docs:literal) => { ($name:ident, $docs:literal) => {
git_next_core::newtype!($name, $docs); git_next_core::newtype!($name, $docs);
impl actix::prelude::Message for $name {
type Result = ();
}
}; };
($name:ident, $value:ty => $result:ty, $docs:literal) => { ($name:ident, $value:ty => $result:ty, $docs:literal) => {
git_next_core::newtype!($name, $value, $docs); git_next_core::newtype!($name, $value, $docs);
impl actix::prelude::Message for $name {
type Result = $result;
}
}; };
($name:ident => $result:ty, $docs:literal) => { ($name:ident => $result:ty, $docs:literal) => {
git_next_core::newtype!($name, $docs); git_next_core::newtype!($name, $docs);
impl actix::prelude::Message for $name {
type Result = $result;
}
}; };
} }

View file

@ -1,3 +1,4 @@
use git_next_core::git::forge::webhook::Error;
// //
use git_next_core::{git, server::RepoListenUrl, RegisteredWebhook, WebhookAuth, WebhookId}; use git_next_core::{git, server::RepoListenUrl, RegisteredWebhook, WebhookAuth, WebhookId};
@ -9,14 +10,14 @@ use tracing::{info, instrument, warn};
use crate::webhook; use crate::webhook;
use crate::webhook::Hook; use crate::webhook::Hook;
#[instrument(skip_all)] #[instrument(skip_all, fields(forge = %repo_details.forge.forge_alias(), repo = %repo_details.repo_alias))]
pub async fn register( pub async fn register(
repo_details: &git::RepoDetails, repo_details: &git::RepoDetails,
repo_listen_url: &RepoListenUrl, repo_listen_url: &RepoListenUrl,
net: &Net, net: &Net,
) -> git::forge::webhook::Result<RegisteredWebhook> { ) -> git::forge::webhook::Result<RegisteredWebhook> {
let Some(repo_config) = repo_details.repo_config.clone() else { let Some(repo_config) = repo_details.repo_config.clone() else {
return Err(git::forge::webhook::Error::NoRepoConfig); return Err(Error::NoRepoConfig);
}; };
// remove any lingering webhooks for the same URL // remove any lingering webhooks for the same URL
@ -51,17 +52,17 @@ pub async fn register(
let Ok(hook) = response.json::<Hook>().await else { let Ok(hook) = response.json::<Hook>().await else {
#[cfg(not(tarpaulin_include))] #[cfg(not(tarpaulin_include))]
// request response is Json so response_body never returns None // request response is Json so response_body never returns None
return Err(git::forge::webhook::Error::NetworkResponseEmpty); return Err(Error::NetworkResponseEmpty);
}; };
info!(webhook_id = %hook.id, "Webhook registered"); info!(webhook_id = %hook.id, "ok");
Ok(RegisteredWebhook::new( Ok(RegisteredWebhook::new(
WebhookId::new(format!("{}", hook.id)), WebhookId::new(format!("{}", hook.id)),
authorisation, authorisation,
)) ))
} }
Err(e) => { Err(e) => {
warn!("Failed to register webhook"); warn!(?e, "failed");
Err(git::forge::webhook::Error::FailedToRegister(e.to_string())) Err(Error::FailedToRegister(e.to_string()))
} }
} }
// Ok(()) // Ok(())

View file

@ -3,6 +3,7 @@ build:
set -e set -e
cargo fmt cargo fmt
cargo fmt --check cargo fmt --check
cargo machete
cargo hack clippy cargo hack clippy
cargo hack build cargo hack build
cargo hack test cargo hack test

3
quickfix-564 Normal file
View file

@ -0,0 +1,3 @@
crates/cli/src/server/actor/mod.rs ┃74┃pub fn new(
crates/cli/src/server/actor/tests/receive_app_config.rs ┃31┃let server = ServerActor::new(
crates/cli/src/server/mod.rs ┃83┃let server = kameo::spqawn(ServerActor::new(