feat: switch to kameo actor system (dropping actix)
All checks were successful
Rust / build (map[name:nightly]) (push) Successful in 7m32s
Rust / build (map[name:stable]) (push) Successful in 14m44s
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 1m14s

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

118
Cargo.lock generated
View file

@ -2,63 +2,6 @@
# It is not intended for manual editing.
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]]
name = "addr2line"
version = "0.21.0"
@ -807,6 +750,12 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
[[package]]
name = "dyn-clone"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125"
[[package]]
name = "either"
version = "1.13.0"
@ -1123,8 +1072,6 @@ dependencies = [
name = "git-next"
version = "0.13.11"
dependencies = [
"actix",
"actix-rt",
"anyhow",
"assert2",
"bon",
@ -1139,6 +1086,7 @@ dependencies = [
"git-next-core",
"git-next-forge-forgejo",
"git-next-forge-github",
"kameo",
"kxio",
"lazy_static",
"lettre",
@ -1171,7 +1119,6 @@ dependencies = [
name = "git-next-core"
version = "0.13.11"
dependencies = [
"actix",
"assert2",
"async-trait",
"derive-with",
@ -2747,6 +2694,36 @@ dependencies = [
"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]]
name = "kqueue"
version = "1.0.8"
@ -4354,6 +4331,7 @@ dependencies = [
"signal-hook-registry",
"socket2",
"tokio-macros",
"tracing",
"windows-sys 0.52.0",
]
@ -4389,6 +4367,17 @@ dependencies = [
"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]]
name = "tokio-tungstenite"
version = "0.21.0"
@ -4691,6 +4680,15 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "valuable"
version = "0.1.0"

View file

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

View file

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

View file

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

View file

@ -1,19 +1,22 @@
//
use actix::prelude::*;
use tracing::{info, Instrument as _};
use kameo::message::{Context, Message};
use tracing::debug;
use crate::alerts::{
desktop::send_desktop_notification, email::send_email, messages::NotifyUser,
webhook::send_webhook, AlertsActor,
};
impl Handler<NotifyUser> for AlertsActor {
type Result = ();
impl Message<NotifyUser> for AlertsActor {
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 {
info!("No shout config available");
debug!("No shout config available");
return;
};
let net = self.net.clone();
@ -21,21 +24,16 @@ impl Handler<NotifyUser> for AlertsActor {
let Some(user_notification) = self.history.sendable(msg.peel()) else {
return;
};
async move {
if let Some(webhook_config) = shout.webhook() {
send_webhook(&user_notification, webhook_config, &net).await;
}
if let Some(email_config) = shout.email() {
send_email(&user_notification, email_config);
}
if let Some(desktop) = shout.desktop() {
if desktop {
send_desktop_notification(&user_notification);
}
if let Some(webhook_config) = shout.webhook() {
send_webhook(&user_notification, webhook_config, &net).await;
}
if let Some(email_config) = shout.email() {
send_email(&user_notification, email_config);
}
if let Some(desktop) = shout.desktop() {
if desktop {
send_desktop_notification(&user_notification);
}
}
.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};
impl Handler<UpdateShout> for AlertsActor {
type Result = ();
impl Message<UpdateShout> for AlertsActor {
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());
}
}

View file

@ -1,11 +1,15 @@
//
use actix::prelude::*;
use derive_more::derive::Constructor;
use git_next_core::{git::UserNotification, server::Shout};
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 email;
@ -24,9 +28,12 @@ pub struct AlertsActor {
history: History, // record of alerts sent recently (e.g. 24 hours)
net: kxio::net::Net,
}
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 {

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, Result};
use notify::{event::ModifyKind, Watcher};
use anyhow::Context;
use kameo::{mailbox::unbounded::UnboundedMailbox, message::Message, Actor};
use notify::{event::ModifyKind, RecommendedWatcher, Watcher};
use tracing::{error, info};
use std::{
path::PathBuf,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
time::Duration,
use git_next_core::message;
use crate::{
default_on_actor_link_died, default_on_actor_panic, default_on_actor_stop, on_actor_start,
publish, tell, MessageBus,
};
#[derive(Debug, Message)]
#[rtype(result = "()")]
pub struct FileUpdated;
message!(
FileUpdated,
"Notification that watched file has been updated"
);
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("io")]
Io(#[from] std::io::Error),
message!(Watch, "Watch for the next event on the file");
pub struct FileWatcherActor {
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>> {
let (tx, rx) = std::sync::mpsc::channel();
let shutdown = Arc::new(AtomicBool::default());
let mut handler = notify::recommended_watcher(tx).context("file watcher")?;
handler
.watch(&path, notify::RecursiveMode::NonRecursive)
.with_context(|| format!("Watching: {path:?}"))?;
let thread_shutdown = shutdown.clone();
actix_rt::task::spawn_blocking(move || {
loop {
if thread_shutdown.load(Ordering::Relaxed) {
drop(handler);
break;
}
for result in rx.try_iter() {
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();
this.event_receiver.replace(rx);
let mut watcher = notify::recommended_watcher(tx).context("file watcher")?;
watcher
.watch(&this.file, notify::RecursiveMode::NonRecursive)
.with_context(|| format!("Watching: {:?}", this.file))?;
this.watcher.replace(watcher);
tell!("file_watcher", actor_ref, Watch)?;
Ok(())
});
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);
}
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 {
Ok(event) => match event.kind {
notify::EventKind::Modify(ModifyKind::Data(_)) => {
info!("File modified");
recipient.do_send(FileUpdated);
info!("===================================================================================");
info!(?event, "File modified");
publish!("file_updates_bus", self.file_updates_bus, FileUpdated)?;
break;
}
notify::EventKind::Modify(_)
@ -55,12 +88,12 @@ pub fn watch_file(path: PathBuf, recipient: Recipient<FileUpdated>) -> Result<Ar
| notify::EventKind::Other => { /* do nothing */ }
},
Err(err) => {
error!(?err, "Watching file: {path:?}");
error!(?err, "Watching file: {:?}", self.file);
}
}
}
std::thread::sleep(Duration::from_millis(1000));
}
});
Ok(shutdown)
tell!("file_watcher", ctx.actor_ref(), msg)?;
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)]
mod alerts;
mod base_actor;
mod file_watcher;
mod forge;
mod init;
mod macros;
mod repo;
mod root;
mod server;
#[cfg(feature = "tui")]
@ -13,9 +16,11 @@ mod tui;
#[cfg(test)]
mod tests;
mod webhook;
use git_next_core::git;
use kameo::actor::{pubsub::PubSub, ActorRef};
use std::path::PathBuf;
@ -23,6 +28,8 @@ use clap::Parser;
use color_eyre::Result;
use kxio::{fs, net};
pub type MessageBus<T> = ActorRef<PubSub<T>>;
#[derive(Parser, Debug)]
#[clap(version = clap::crate_version!(), author = clap::crate_authors!(), about = clap::crate_description!())]
struct Commands {
@ -47,6 +54,8 @@ enum Server {
}
fn main() -> Result<()> {
color_eyre::install()?;
let fs = fs::new(PathBuf::default());
let net = net::new();
let repository_factory = git::repository::factory::real();
@ -59,16 +68,16 @@ fn main() -> Result<()> {
#[cfg(not(feature = "tui"))]
Server::Start {} => server::start(
false,
fs,
net,
&fs,
&net,
repository_factory,
std::time::Duration::from_secs(10),
),
#[cfg(feature = "tui")]
Server::Start { ui } => server::start(
ui,
fs,
net,
&fs,
&net,
repository_factory,
std::time::Duration::from_secs(10),
),

View file

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

View file

@ -1,8 +1,8 @@
//
use actix::prelude::*;
use git_next_core::{git, RepoConfigSource};
use color_eyre::Result;
use kameo::message::{Context, Message};
use tracing::warn;
use crate::{
@ -15,45 +15,55 @@ use crate::{
server::actor::messages::RepoUpdate,
};
impl Handler<AdvanceMain> for RepoActor {
type Result = ();
impl Message<AdvanceMain> for RepoActor {
type Reply = Result<()>;
#[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 {
warn!("No config loaded");
return;
return Ok(());
};
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();
self.update_tui(RepoUpdate::AdvancingMain {
commit: commit.clone(),
});
if let Err(err) = advance_main(commit, &repo_details, &repo_config, &**open_repository) {
})
.await?;
if let Err(err) = advance_main(commit, &self.repo_details, &repo_config, &**open_repository)
{
warn!("advance main: {err}");
self.alert_tui(format!("advance main: {err}"));
self.alert_tui(format!("advance main: {err}")).await?;
} else {
self.update_tui(RepoUpdate::MainUpdated);
self.update_tui(RepoUpdate::MainUpdated).await?;
if let Some(open_repository) = &self.open_repository {
match open_repository.fetch() {
Ok(()) => self.update_tui_log(git::graph::log(&self.repo_details)),
Err(err) => self.alert_tui(format!("fetching: {err}")),
Ok(()) => {
self.update_tui_log(git::graph::log(&self.repo_details))
.await?;
}
Err(err) => self.alert_tui(format!("fetching: {err}")).await?,
}
}
match repo_config.source() {
RepoConfigSource::Repo => {
do_send(&addr, LoadConfigFromRepo, self.log.as_ref());
do_send(&ctx.actor_ref(), LoadConfigFromRepo, self.log.as_ref()).await?;
}
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 tracing::{warn, Instrument};
use crate::{
repo::{
@ -14,15 +15,19 @@ use crate::{
server::actor::messages::RepoUpdate,
};
impl Handler<AdvanceNext> for RepoActor {
type Result = ();
impl Message<AdvanceNext> for RepoActor {
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 {
return;
return Ok(());
};
let Some(open_repository) = &self.open_repository else {
return;
return Ok(());
};
let AdvanceNextPayload {
@ -30,45 +35,44 @@ impl Handler<AdvanceNext> for RepoActor {
main,
dev_commit_history,
} = 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);
if let Some(commit) = &commit {
self.update_tui(RepoUpdate::AdvancingNext {
commit: commit.clone(),
force: force.clone(),
});
})
.await?;
};
match advance_next(
commit,
force,
&repo_details,
&self.repo_details,
repo_config,
&**open_repository,
self.message_token,
) {
Ok(message_token) => {
self.update_tui(RepoUpdate::NextUpdated);
self.update_tui(RepoUpdate::NextUpdated).await?;
match open_repository.fetch() {
Ok(()) => self.update_tui_log(git::graph::log(&self.repo_details)),
Err(err) => self.alert_tui(format!("fetching: {err}")),
Ok(()) => {
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
let sleep_duration = self.sleep_duration;
let log = self.log.clone();
async move {
actix_rt::time::sleep(sleep_duration).await;
do_send(&addr, ValidateRepo::new(message_token), log.as_ref());
}
.in_current_span()
.into_actor(self)
.wait(ctx);
tokio::time::sleep(self.sleep_duration).await;
Ok(do_send(
&ctx.actor_ref(),
ValidateRepo::new(message_token),
self.log.as_ref(),
)
.await?)
}
Err(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 tracing::{debug, warn, Instrument as _};
use color_eyre::Result;
use kameo::message::{Context, Message};
use tracing::{debug, warn};
use crate::{
repo::{
@ -12,30 +12,30 @@ use crate::{
server::actor::messages::RepoUpdate,
};
impl Handler<CheckCIStatus> for RepoActor {
type Result = ();
impl Message<CheckCIStatus> for RepoActor {
type Reply = Result<()>;
fn handle(&mut self, msg: CheckCIStatus, ctx: &mut Self::Context) -> Self::Result {
crate::repo::logger(self.log.as_ref(), "start: CheckCIStatus");
let addr = ctx.address();
let forge = self.forge.duplicate();
async fn handle(
&mut self,
msg: CheckCIStatus,
ctx: Context<'_, Self, Self::Reply>,
) -> Self::Reply {
crate::repo::logger(self.log.as_ref(), "start: CheckCIStatus").await;
let next = msg.peel();
let log = self.log.clone();
self.update_tui(RepoUpdate::CheckingCI);
self.update_tui(RepoUpdate::CheckingCI).await?;
// get the status - pass, fail, pending (all others map to fail, e.g. error)
async move {
match forge.commit_status(&next).await {
Ok(status) => {
debug!("got status: {status:?}");
do_send(&addr, ReceiveCIStatus::new((next, status)), log.as_ref());
}
Err(err) => warn!(?err, "fetching commit status"),
match self.forge.commit_status(&next).await {
Ok(status) => {
debug!("got status: {status:?}");
do_send(
&ctx.actor_ref(),
ReceiveCIStatus::new((next, status)),
self.log.as_ref(),
)
.await?;
}
Err(err) => warn!(?err, "fetching commit status"),
}
.in_current_span()
.into_actor(self)
.wait(ctx);
Ok(())
}
}

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 tracing::{debug, instrument, warn};
use crate::{
repo::{
@ -13,31 +14,38 @@ use crate::{
server::actor::messages::RepoUpdate,
};
impl Handler<CloneRepo> for RepoActor {
type Result = ();
impl Message<CloneRepo> for RepoActor {
type Reply = Result<()>;
#[instrument(name = "RepoActor::CloneRepo", skip_all, fields(repo = %self.repo_details))]
fn handle(&mut self, _msg: CloneRepo, ctx: &mut Self::Context) -> Self::Result {
logger(self.log.as_ref(), "Handler: CloneRepo: start");
self.update_tui(RepoUpdate::Opening);
async fn handle(
&mut self,
_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");
match git::repository::open(&*self.repository_factory, &self.repo_details) {
Ok(repository) => {
logger(self.log.as_ref(), "open okay");
logger(self.log.as_ref(), "open okay").await;
debug!("open okay");
self.update_tui(RepoUpdate::Opened);
self.update_tui(RepoUpdate::Opened).await?;
self.open_repository.replace(repository);
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 {
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) => {
logger(self.log.as_ref(), "open failed");
logger(self.log.as_ref(), "open failed").await;
warn!("Could not open repo: {err:?}");
self.alert_tui(err.to_string());
self.alert_tui(err.to_string()).await?;
}
}
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 tracing::{debug, instrument, Instrument as _};
use crate::{
repo::{
do_send, load,
@ -14,41 +14,43 @@ use crate::{
server::actor::messages::RepoUpdate,
};
impl Handler<LoadConfigFromRepo> for RepoActor {
type Result = ();
impl Message<LoadConfigFromRepo> for RepoActor {
type Reply = Result<()>;
#[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");
self.update_tui(RepoUpdate::LoadingConfigFromRepo);
self.update_tui(RepoUpdate::LoadingConfigFromRepo).await?;
let Some(open_repository) = &self.open_repository else {
return;
return Ok(());
};
let open_repository = open_repository.duplicate();
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) => {
do_send(&addr, ReceiveRepoConfig::new(repo_config), log.as_ref());
}
Err(err) => notify_user(
notify_user_recipient.as_ref(),
match load::config_from_repository(&self.repo_details, &**open_repository).await {
Ok(repo_config) => {
do_send(
&ctx.actor_ref(),
ReceiveRepoConfig::new(repo_config),
self.log.as_ref(),
)
.await?;
}
Err(err) => {
notify_user(
self.notify_user_recipient.as_ref(),
UserNotification::RepoConfigLoadFailure {
forge_alias,
repo_alias,
forge_alias: self.repo_details.forge.forge_alias().clone(),
repo_alias: self.repo_details.repo_alias.clone(),
reason: err.to_string(),
},
log.as_ref(),
),
self.log.as_ref(),
)
.await?;
}
}
.in_current_span()
.into_actor(self)
.wait(ctx);
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 tracing::{debug, Instrument};
use crate::{
repo::{
@ -13,64 +14,64 @@ use crate::{
server::actor::messages::RepoUpdate,
};
impl Handler<ReceiveCIStatus> for RepoActor {
type Result = ();
impl Message<ReceiveCIStatus> for RepoActor {
type Reply = Result<()>;
fn handle(&mut self, msg: ReceiveCIStatus, ctx: &mut Self::Context) -> Self::Result {
logger(self.log.as_ref(), "start: ReceiveCIStatus");
async fn handle(
&mut self,
msg: ReceiveCIStatus,
ctx: Context<'_, Self, Self::Reply>,
) -> Self::Reply {
logger(self.log.as_ref(), "start: ReceiveCIStatus").await;
let (next, status) = msg.peel();
self.update_tui(RepoUpdate::ReceiveCIStatus {
status: status.clone(),
});
})
.await?;
debug!(?status, "");
let graph_log = graph::log(&self.repo_details);
self.update_tui_log(graph_log.clone());
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;
self.update_tui_log(graph_log.clone()).await?;
match status {
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 => {
let log = self.log.clone();
async move {
actix_rt::time::sleep(sleep_duration).await;
do_send(&addr, ValidateRepo::new(message_token), log.as_ref());
}
.in_current_span()
.into_actor(self)
.wait(ctx);
tokio::time::sleep(self.sleep_duration).await;
do_send(
&ctx.actor_ref(),
ValidateRepo::new(self.message_token),
self.log.as_ref(),
)
.await?;
}
Status::Fail => {
tracing::warn!("Checks have failed");
notify_user(
self.notify_user_recipient.as_ref(),
UserNotification::CICheckFailed {
forge_alias,
repo_alias,
forge_alias: self.repo_details.forge.forge_alias().clone(),
repo_alias: self.repo_details.repo_alias.clone(),
commit: next,
log: graph_log,
},
self.log.as_ref(),
);
let log = self.log.clone();
async move {
debug!("sleeping before retrying...");
logger(log.as_ref(), "before sleep");
actix_rt::time::sleep(sleep_duration).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);
)
.await?;
debug!("sleeping before retrying...");
logger(self.log.clone().as_ref(), "before sleep").await;
tokio::time::sleep(self.sleep_duration).await;
logger(self.log.clone().as_ref(), "after sleep").await;
do_send(
&ctx.actor_ref(),
ValidateRepo::new(self.message_token),
self.log.as_ref(),
)
.await?;
}
Status::Error(err) => {
tracing::warn!(?err, "Check CI Status");
}
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 crate::{
@ -11,16 +12,22 @@ use crate::{
server::actor::messages::RepoUpdate,
};
impl Handler<ReceiveRepoConfig> for RepoActor {
type Result = ();
impl Message<ReceiveRepoConfig> for RepoActor {
type Reply = Result<()>;
#[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();
self.update_tui(RepoUpdate::ReceiveRepoConfig {
repo_config: repo_config.clone(),
});
})
.await?;
self.repo_details.repo_config.replace(repo_config);
self.update_tui_branches();
do_send(&ctx.address(), RegisterWebhook::new(), self.log.as_ref());
self.update_tui_branches().await?;
do_send(&ctx.actor_ref(), RegisterWebhook::new(), self.log.as_ref()).await?;
Ok(())
}
}

View file

@ -1,7 +1,7 @@
//
use actix::prelude::*;
use tracing::{debug, error, Instrument as _};
use color_eyre::Result;
use kameo::message::{Context, Message};
use tracing::{debug, error};
use crate::{
repo::{
@ -14,51 +14,52 @@ use crate::{
use git_next_core::git::UserNotification;
impl Handler<RegisterWebhook> for RepoActor {
type Result = ();
impl Message<RegisterWebhook> for RepoActor {
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() {
let forge_alias = self.repo_details.forge.forge_alias().clone();
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);
self.update_tui(RepoUpdate::RegisteringWebhook).await?;
debug!("registering webhook");
async move {
match forge.register_webhook(&repo_listen_url).await {
Ok(registered_webhook) => {
debug!(?registered_webhook, "webhook registered");
do_send(
&addr,
WebhookRegistered::from(registered_webhook),
log.as_ref(),
);
}
Err(err) => {
error!(?err, "failed to register webhook");
notify_user(
notify_user_recipient.as_ref(),
UserNotification::WebhookRegistration {
forge_alias,
repo_alias,
reason: err.to_string(),
},
log.as_ref(),
);
}
match self
.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) => {
debug!(?registered_webhook, "webhook registered");
do_send(
&ctx.actor_ref(),
WebhookRegistered::from(registered_webhook),
self.log.as_ref(),
)
.await?;
}
Err(err) => {
error!(?err, "failed to register webhook");
notify_user(
self.notify_user_recipient.clone().as_ref(),
UserNotification::WebhookRegistration {
forge_alias: self.repo_details.forge.forge_alias().clone(),
repo_alias: self.repo_details.repo_alias.clone(),
reason: err.to_string(),
},
self.log.as_ref(),
)
.await?;
}
}
.in_current_span()
.into_actor(self)
.wait(ctx);
} 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 tracing::{debug, warn, Instrument as _};
use color_eyre::Result;
use kameo::message::{Context, Message};
use tracing::{debug, warn};
use crate::{
repo::{messages::UnRegisterWebhook, RepoActor},
server::actor::messages::RepoUpdate,
};
impl Handler<UnRegisterWebhook> for RepoActor {
type Result = ();
impl Message<UnRegisterWebhook> for RepoActor {
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 {
return;
return Ok(());
};
self.update_tui(RepoUpdate::UnregisteringWebhook);
let forge = self.forge.duplicate();
self.update_tui(RepoUpdate::UnregisteringWebhook).await?;
debug!("unregistering webhook");
async move {
match forge.unregister_webhook(&webhook_id).await {
Ok(()) => debug!("unregistered webhook"),
Err(err) => warn!(?err, "unregistering webhook"),
}
match self.forge.unregister_webhook(&webhook_id).await {
Ok(()) => debug!("unregistered webhook"),
Err(err) => warn!(?err, "unregistering webhook"),
}
.in_current_span()
.into_actor(self)
.wait(ctx);
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::{
repo::{
@ -11,20 +13,23 @@ use crate::{
},
server::actor::messages::RepoUpdate,
};
use git_next_core::git::{
push::Force,
validation::positions::{validate, Error, Positions},
UserNotification,
};
use git_next_core::s;
impl Handler<ValidateRepo> for RepoActor {
type Result = ();
impl Message<ValidateRepo> for RepoActor {
type Reply = Result<()>;
#[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 {
logger(self.log.as_ref(), "start: ValidateRepo");
async fn handle(
&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
match self.token_status(msg.peel()) {
TokenStatus::Current => {} // do nothing
@ -32,38 +37,38 @@ impl Handler<ValidateRepo> for RepoActor {
logger(
self.log.as_ref(),
format!("discarded: old message token: {}", self.message_token),
);
return; // message is expired
)
.await;
return Ok(()); // message is expired
}
TokenStatus::New(message_token) => {
self.message_token = message_token;
logger(
self.log.as_ref(),
format!("new message token: {}", self.message_token),
);
)
.await;
}
}
logger(
self.log.as_ref(),
format!("accepted token: {}", self.message_token),
);
self.update_tui(RepoUpdate::ValidateRepo);
)
.await;
self.update_tui(RepoUpdate::ValidateRepo).await?;
// Repository positions
let Some(ref open_repository) = self.open_repository else {
logger(self.log.as_ref(), "no open repository");
self.alert_tui("repo not open");
return;
logger(self.log.as_ref(), "no open repository").await;
self.alert_tui("repo not open").await?;
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 {
logger(self.log.as_ref(), "no repo config");
self.alert_tui("no repo config");
return;
logger(self.log.as_ref(), "no repo config").await;
self.alert_tui("no repo config").await?;
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) {
Ok((
Positions {
@ -75,70 +80,88 @@ impl Handler<ValidateRepo> for RepoActor {
},
git_log,
)) => {
info!(%main, %next, %dev, "positions");
self.update_tui_log(git_log);
let mut positions = HashMap::new();
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 {
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 {
info!("Advance next");
self.update_tui(RepoUpdate::AdvancingNext {
commit: next.clone(),
force: Force::No,
});
})
.await?;
do_send(
&ctx.address(),
&ctx.actor_ref(),
AdvanceNext::new(AdvanceNextPayload {
next,
main,
dev_commit_history,
}),
self.log.as_ref(),
);
)
.await?;
} else {
info!("do nothing");
self.update_tui(RepoUpdate::Okay { main, next, dev });
self.update_tui(RepoUpdate::Okay { main, next, dev })
.await?;
}
}
Err(Error::Retryable(message)) => {
info!(?message, "Retryable");
self.alert_tui(format!("retryable: {message}"));
logger(self.log.as_ref(), message);
let addr = ctx.address();
let message_token = self.message_token;
let sleep_duration = self.sleep_duration;
let log = self.log.clone();
async move {
info!("sleeping before retrying...");
logger(log.as_ref(), "before sleep");
actix_rt::time::sleep(sleep_duration).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);
warn!(?message, "Retryable");
self.alert_tui(format!("retryable: {message}")).await?;
logger(self.log.as_ref(), message).await;
debug!("sleeping before retrying...");
tokio::time::sleep(self.sleep_duration).await;
do_send(
&ctx.actor_ref(),
ValidateRepo::new(self.message_token),
self.log.as_ref(),
)
.await?;
}
Err(Error::UserIntervention(user_notification)) => {
info!(?user_notification, "User Intervention");
self.alert_tui(format!("USER INTERVENTION: {user_notification}"));
warn!(?user_notification, "User Intervention");
self.alert_tui(format!("USER INTERVENTION: {user_notification}"))
.await?;
if let UserNotification::CICheckFailed { log, .. }
| UserNotification::DevNotBasedOnMain { log, .. } = &user_notification
{
self.update_tui_log(log.clone());
self.update_tui_log(log.clone()).await?;
}
notify_user(
self.notify_user_recipient.as_ref(),
user_notification,
self.log.as_ref(),
);
)
.await?;
}
Err(Error::NonRetryable(message)) => {
info!(?message, "NonRetryable");
self.alert_tui(format!("Error: {message}"));
logger(self.log.as_ref(), message);
warn!(?message, "NonRetryable");
self.alert_tui(format!("Error: {message}")).await?;
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 git_next_core::{
git::{Commit, ForgeLike},
webhook::{push::Branch, Push},
BranchName, WebhookAuth,
};
use crate::{
repo::{
do_send, logger,
@ -12,21 +18,19 @@ use crate::{
server::actor::messages::RepoUpdate,
};
use git_next_core::{
git::{Commit, ForgeLike},
webhook::{push::Branch, Push},
BranchName, WebhookAuth,
};
impl Handler<WebhookNotification> for RepoActor {
type Result = ();
impl Message<WebhookNotification> for RepoActor {
type Reply = Result<()>;
#[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 {
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");
return;
return Ok(());
};
if validate_notification(
&msg,
@ -34,72 +38,78 @@ impl Handler<WebhookNotification> for RepoActor {
&*self.forge,
self.log.as_ref(),
)
.await
.is_err()
{
return;
return Ok(());
}
let body = msg.body();
match self.forge.parse_webhook_body(body) {
match self.forge.parse_webhook_body(msg.body()) {
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'");
return;
return Ok(());
}
Ok(push) => match push.branch(config.branches()) {
None => {
logger(self.log.as_ref(), "unknown branch");
logger(self.log.as_ref(), "unknown branch").await;
warn!(
?push,
"Unrecognised branch, we should be filtering to only the ones we want"
);
return;
return Ok(());
}
Some(Branch::Main) => {
self.update_tui(RepoUpdate::WebhookReceived {
branch: Branch::Main,
push: push.clone(),
});
})
.await?;
if handle_push(
push,
&config.branches().main(),
&mut self.last_main_commit,
self.log.as_ref(),
)
.await
.is_err()
{
return;
return Ok(());
};
}
Some(Branch::Next) => {
self.update_tui(RepoUpdate::WebhookReceived {
branch: Branch::Next,
push: push.clone(),
});
})
.await?;
if handle_push(
push,
&config.branches().next(),
&mut self.last_next_commit,
self.log.as_ref(),
)
.await
.is_err()
{
return;
return Ok(());
};
}
Some(Branch::Dev) => {
self.update_tui(RepoUpdate::WebhookReceived {
branch: Branch::Dev,
push: push.clone(),
});
})
.await?;
if handle_push(
push,
&config.branches().dev(),
&mut self.last_dev_commit,
self.log.as_ref(),
)
.await
.is_err()
{
return;
return Ok(());
};
}
},
@ -110,27 +120,29 @@ impl Handler<WebhookNotification> for RepoActor {
"New commit"
);
do_send(
&ctx.address(),
&ctx.actor_ref(),
ValidateRepo::new(message_token),
self.log.as_ref(),
);
)
.await?;
Ok(())
}
}
fn validate_notification(
async fn validate_notification(
msg: &WebhookNotification,
webhook_auth: Option<&WebhookAuth>,
forge: &dyn ForgeLike,
log: Option<&ActorLog>,
) -> Result<(), ()> {
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");
return Err(());
};
if !forge.is_message_authorised(msg, expected_authorization) {
logger(log, "message authorisation is invalid");
logger(log, "message authorisation is invalid").await;
warn!(
"Invalid authorization - expected {}",
expected_authorization
@ -138,22 +150,22 @@ fn validate_notification(
return Err(());
}
if forge.should_ignore_message(msg) {
logger(log, "forge sent ignorable message");
logger(log, "forge sent ignorable message").await;
return Err(());
}
Ok(())
}
fn handle_push(
async fn handle_push(
push: Push,
branch: &BranchName,
last_commit: &mut Option<Commit>,
log: Option<&ActorLog>,
) -> Result<(), ()> {
logger(log, format!("message is for {branch} branch"));
logger(log, format!("message is for {branch} branch")).await;
let commit = Commit::from(push);
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!(
%branch ,
%commit,

View file

@ -1,5 +1,6 @@
//
use actix::prelude::*;
use color_eyre::Result;
use kameo::message::{Context, Message};
use tracing::instrument;
use crate::{
@ -11,17 +12,24 @@ use crate::{
server::actor::messages::RepoUpdate,
};
impl Handler<WebhookRegistered> for RepoActor {
type Result = ();
impl Message<WebhookRegistered> for RepoActor {
type Reply = Result<()>;
#[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 {
self.update_tui(RepoUpdate::RegisteredWebhook);
async fn handle(
&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_auth.replace(msg.webhook_auth().clone());
do_send(
&ctx.address(),
&ctx.actor_ref(),
ValidateRepo::new(self.message_token),
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
#[instrument(skip_all, fields(branch = %repo_details.branch))]
pub async fn config_from_repository(
repo_details: RepoDetails,
repo_details: &RepoDetails,
open_repository: &dyn OpenRepositoryLike,
) -> Result<RepoConfig> {
info!("Loading .git-next.toml from repo");

View file

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

View file

@ -10,7 +10,7 @@ mod advance_next;
use crate::git;
use crate::repo::branch;
#[actix_rt::test]
#[tokio::test]
async fn test_find_next_commit_on_dev_when_next_is_at_main() {
let next = given::a_commit(); // and main
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");
}
#[actix_rt::test]
#[tokio::test]
async fn test_find_next_commit_on_dev_when_next_is_not_on_dev() {
let next = 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 git_next_core::server::ListenUrl;
use kameo::actor::{pubsub::PubSub, ActorRef};
use kxio::{fs::FileSystem, net::Net};
pub fn has_all_valid_remote_defaults(
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;
iter::repeat_with(one_char).take(len).collect()
}
generate(5)
generate(7)
}
pub fn maybe_a_number() -> Option<u32> {
@ -197,6 +204,7 @@ pub fn a_repo_actor(
repo_details: RepoDetails,
repository_factory: Box<dyn RepositoryFactory>,
forge: Box<dyn ForgeLike>,
server_actor: ActorRef<ServerActor>,
net: kxio::net::Net,
) -> (RepoActor, ActorLog) {
let listen_url = given::a_listen_url();
@ -205,21 +213,43 @@ pub fn a_repo_actor(
let actors_log = log.clone();
(
RepoActor::new(
false,
repo_details,
forge,
listen_url,
generation,
net,
repository_factory,
std::time::Duration::from_nanos(1),
None,
std::time::Duration::from_nanos(0),
None,
server_actor,
)
.with_log(actors_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 {
Hostname::new(given::a_name())
}

View file

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

View file

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

View file

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

View file

@ -1,7 +1,11 @@
use kxio::net::Net;
use crate::tell;
//
use super::*;
#[actix::test]
#[test_log::test(tokio::test)]
async fn should_clone() -> TestResult {
//given
let fs = given::a_filesystem();
@ -23,13 +27,19 @@ async fn should_clone() -> TestResult {
let _ = cloned_ref.write().map(|mut l| l.push(()));
Ok(Box::new(open_repository))
});
let net: Net = given::a_network().into();
//when
let (addr, _log) = when::start_actor(repository_factory, repo_details, given::a_forge());
addr.send(CloneRepo::new()).await?;
System::current().stop();
let (addr, _log) = when::start_actor(
repository_factory,
repo_details,
given::a_forge(),
fs.as_real(),
net,
);
tell!(addr, CloneRepo::new())?;
//then
tick(1).await;
cloned
.read()
.map_err(|e| e.to_string())
@ -38,7 +48,7 @@ async fn should_clone() -> TestResult {
Ok(())
}
#[actix::test]
#[tokio::test]
async fn should_open() -> TestResult {
//given
let fs = given::a_filesystem();
@ -60,13 +70,20 @@ async fn should_open() -> TestResult {
Ok(Box::new(open_repository))
});
fs.dir(&repo_details.gitdir).create()?;
let net: Net = given::a_network().into();
//when
let (addr, _log) = when::start_actor(repository_factory, repo_details, given::a_forge());
addr.send(CloneRepo::new()).await?;
System::current().stop();
let (addr, _log) = when::start_actor(
repository_factory,
repo_details,
given::a_forge(),
fs.as_real(),
net,
);
tell!(addr, CloneRepo::new())?;
//then
tick(1).await;
opened
.read()
.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
/// 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.
#[actix::test]
#[tokio::test]
async fn when_server_has_no_repo_config_should_send_load_from_repo() -> TestResult {
//given
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();
expect::open_repository(&mut repository_factory, open_repository);
fs.dir(&repo_details.gitdir).create()?;
let net: Net = given::a_network().into();
//when
let (addr, log) = when::start_actor(repository_factory, repo_details, given::a_forge());
addr.send(CloneRepo::new()).await?;
System::current().stop();
let (addr, log) = when::start_actor(
repository_factory,
repo_details,
given::a_forge(),
fs.as_real(),
net,
);
tell!(addr, CloneRepo::new())?;
//then
log.require_message_containing("send: LoadConfigFromRepo")?;
log.require_message_containing("send: LoadConfigFromRepo")
.await?;
Ok(())
}
/// 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.
#[actix::test]
#[test_log::test(tokio::test)]
async fn when_server_has_repo_config_should_send_register_webhook() -> TestResult {
//given
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();
expect::open_repository(&mut repository_factory, open_repository);
fs.dir(&repo_details.gitdir).create()?;
let net: Net = given::a_network().into();
//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("send: RegisterWebhook")?;
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()),
]),
let (addr, log) = when::start_actor(
repository_factory,
repo_details,
given::a_forge(),
fs.as_real(),
net,
);
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();
tell!(addr, CloneRepo::new())?;
//then
log.require_message_containing("open failed")?;
Ok(())
}
#[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")?;
tick(1).await;
debug!(?log, "");
log.require_message_containing("send: RegisterWebhook")
.await?;
Ok(())
}

View file

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

View file

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

View file

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

View file

@ -1,71 +1,57 @@
use crate::{repo::messages::RegisterWebhook, tell};
//
use super::*;
#[actix::test]
#[tokio::test]
async fn when_registered_ok_should_send_webhook_registered() -> TestResult {
//given
let fs = given::a_filesystem();
let (open_repository, repo_details) = given::an_open_repository(&fs);
let registered_webhook = given::a_registered_webhook();
let mut my_forge = git::MockForgeLike::new();
my_forge
let mut forge = git::MockForgeLike::new();
forge
.expect_register_webhook()
.return_once(move |_| Ok(registered_webhook));
let mut forge = git::MockForgeLike::new();
forge.expect_duplicate().return_once(|| Box::new(my_forge));
//when
let (addr, log) = when::start_actor_with_open_repository(
Box::new(open_repository),
repo_details,
Box::new(forge),
);
addr.send(crate::repo::messages::RegisterWebhook::new())
.await?;
System::current().stop();
tell!(addr, RegisterWebhook::new())?;
//then
tracing::debug!(?log, "");
log.read().map_err(|e| e.to_string()).map(|l| {
assert!(l
.iter()
.any(|message| message.contains("send: WebhookRegistered")));
})?;
log.require_message_containing("send: WebhookRegistered")
.await?;
Ok(())
}
#[actix::test]
#[tokio::test]
async fn when_registered_error_should_send_notify_user() -> TestResult {
//given
let fs = given::a_filesystem();
let (open_repository, repo_details) = given::an_open_repository(&fs);
let mut my_forge = git::MockForgeLike::new();
my_forge.expect_register_webhook().return_once(move |_| {
let mut forge = git::MockForgeLike::new();
forge.expect_register_webhook().return_once(move |_| {
Err(git::forge::webhook::Error::FailedToRegister(
"foo".to_string(),
))
});
let mut forge = git::MockForgeLike::new();
forge.expect_duplicate().return_once(|| Box::new(my_forge));
//when
let (addr, log) = when::start_actor_with_open_repository(
Box::new(open_repository),
repo_details,
Box::new(forge),
);
addr.send(crate::repo::messages::RegisterWebhook::new())
.await?;
System::current().stop();
tell!(addr, crate::repo::messages::RegisterWebhook::new())?;
//then
tracing::debug!(?log, "");
log.read()
.map_err(|e| e.to_string())
.map(|l| assert!(l.iter().any(|message| message.contains("send: NotifyUser"))))?;
log.require_message_containing("send: NotifyUser").await?;
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::*;
#[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(
) -> TestResult {
//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,
given::a_forge(),
);
addr.send(crate::repo::messages::ValidateRepo::new(
MessageToken::default(),
))
.await?;
System::current().stop();
tell!(addr, ValidateRepo::new(MessageToken::default()))?;
//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(())
}
#[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(
) -> TestResult {
//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,
given::a_forge(),
);
addr.send(crate::repo::messages::ValidateRepo::new(
MessageToken::default(),
))
.await?;
System::current().stop();
tell!(
addr,
crate::repo::messages::ValidateRepo::new(MessageToken::default())
)?;
//then
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,
dev_commit_history: dev_branch_log,
});
log.require_message_containing(format!("send: {expected:?}",))?;
log.require_message_containing(format!("send: {expected:?}",))
.await?;
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 {
//given
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,
given::a_forge(),
);
addr.send(crate::repo::messages::ValidateRepo::new(
MessageToken::default(),
))
.await?;
System::current().stop();
tell!(addr, ValidateRepo::new(MessageToken::default()))?;
//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(())
}
#[test_log::test(actix::test)]
#[test_log::test(tokio::test)]
async fn repo_with_next_not_based_on_main_should_be_reset() -> TestResult {
//given
let fs = given::a_filesystem();
@ -193,18 +192,15 @@ async fn repo_with_next_not_based_on_main_should_be_reset() -> TestResult {
repo_details,
given::a_forge(),
);
addr.send(crate::repo::messages::ValidateRepo::new(
MessageToken::default(),
))
.await?;
System::current().stop();
tell!(addr, ValidateRepo::new(MessageToken::default()))?;
//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(())
}
#[test_log::test(actix::test)]
#[test_log::test(tokio::test)]
async fn repo_with_next_ahead_of_main_should_check_ci_status() -> TestResult {
//given
let fs = given::a_filesystem();
@ -240,18 +236,15 @@ async fn repo_with_next_ahead_of_main_should_check_ci_status() -> TestResult {
repo_details,
given::a_forge(),
);
addr.send(crate::repo::messages::ValidateRepo::new(
MessageToken::default(),
))
.await?;
System::current().stop();
tell!(addr, ValidateRepo::new(MessageToken::default()))?;
//then
log.require_message_containing(format!("send: CheckCIStatus({next_commit:?})"))?;
log.require_message_containing(format!("send: CheckCIStatus({next_commit:?})"))
.await?;
Ok(())
}
#[test_log::test(actix::test)]
#[test_log::test(tokio::test)]
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
//given
@ -288,18 +281,14 @@ async fn repo_with_dev_and_next_on_main_should_do_nothing() -> TestResult {
repo_details,
given::a_forge(),
);
addr.send(crate::repo::messages::ValidateRepo::new(
MessageToken::default(),
))
.await?;
System::current().stop();
tell!(addr, ValidateRepo::new(MessageToken::default()))?;
//then
log.no_message_contains("send:")?;
log.no_message_contains("send:").await?;
Ok(())
}
#[test_log::test(actix::test)]
#[test_log::test(tokio::test)]
async fn repo_with_dev_ahead_of_next_should_advance_next() -> TestResult {
//given
let fs = given::a_filesystem();
@ -337,11 +326,7 @@ async fn repo_with_dev_ahead_of_next_should_advance_next() -> TestResult {
repo_details,
given::a_forge(),
);
addr.send(crate::repo::messages::ValidateRepo::new(
MessageToken::default(),
))
.await?;
System::current().stop();
tell!(addr, ValidateRepo::new(MessageToken::default()))?;
//then
let expected = AdvanceNext::new(AdvanceNextPayload {
@ -349,11 +334,12 @@ async fn repo_with_dev_ahead_of_next_should_advance_next() -> TestResult {
main: main_commit,
dev_commit_history: dev_branch_log,
});
log.require_message_containing(format!("send: {expected:?}"))?;
log.require_message_containing(format!("send: {expected:?}"))
.await?;
Ok(())
}
#[test_log::test(actix::test)]
#[test_log::test(tokio::test)]
async fn repo_with_dev_not_ahead_of_main_should_notify_user() -> TestResult {
//given
let fs = given::a_filesystem();
@ -391,96 +377,87 @@ async fn repo_with_dev_not_ahead_of_main_should_notify_user() -> TestResult {
repo_details,
given::a_forge(),
);
addr.send(crate::repo::messages::ValidateRepo::new(
MessageToken::default(),
))
.await?;
System::current().stop();
tell!(addr, ValidateRepo::new(MessageToken::default()))?;
//then
log.require_message_containing("send: NotifyUser")?;
log.require_message_containing("send: NotifyUser").await?;
Ok(())
}
#[test_log::test(actix::test)]
#[test_log::test(tokio::test)]
async fn should_accept_message_with_current_token() -> TestResult {
//given
let fs = given::a_filesystem();
let repo_details = given::repo_details(&fs);
//when
let net: Net = given::a_network().into();
let server_actor_ref = given::a_server_actor(fs.as_real(), net.clone());
let (actor, log) = given::a_repo_actor(
repo_details,
git::repository::factory::mock(),
given::a_forge(),
given::a_network().into(),
server_actor_ref,
net,
);
let actor = actor.with_message_token(MessageToken::new(2_u32));
let addr = actor.start();
addr.send(crate::repo::messages::ValidateRepo::new(MessageToken::new(
2_u32,
)))
.await?;
System::current().stop();
let addr = kameo::spawn(actor);
//
//when
tell!(addr, ValidateRepo::new(MessageToken::new(2_u32)))?;
//then
log.require_message_containing("accepted token: 2")?;
log.require_message_containing("accepted token: 2").await?;
Ok(())
}
#[test_log::test(actix::test)]
#[test_log::test(tokio::test)]
async fn should_accept_message_with_new_token() -> TestResult {
//given
let fs = given::a_filesystem();
let repo_details = given::repo_details(&fs);
let net: Net = given::a_network().into();
//when
let (actor, log) = given::a_repo_actor(
repo_details,
git::repository::factory::mock(),
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 addr = actor.start();
addr.send(crate::repo::messages::ValidateRepo::new(MessageToken::new(
3_u32,
)))
.await?;
System::current().stop();
let addr = kameo::spawn(actor);
tell!(addr, ValidateRepo::new(MessageToken::new(3_u32)))?;
//then
log.require_message_containing("accepted token: 3")?;
log.require_message_containing("accepted token: 3").await?;
Ok(())
}
#[test_log::test(actix::test)]
#[test_log::test(tokio::test)]
async fn should_reject_message_with_expired_token() -> TestResult {
//given
let fs = given::a_filesystem();
let repo_details = given::repo_details(&fs);
let net: Net = given::a_network().into();
//when
let (actor, log) = given::a_repo_actor(
repo_details,
git::repository::factory::mock(),
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 addr = actor.start();
addr.send(crate::repo::messages::ValidateRepo::new(MessageToken::new(
3_u32,
)))
.await?;
System::current().stop();
let addr = kameo::spawn(actor);
tell!(addr, ValidateRepo::new(MessageToken::new(3_u32)))?;
//then
log.no_message_contains("accepted token")?;
log.no_message_contains("accepted token").await?;
Ok(())
}
#[test_log::test(actix::test)]
#[test_log::test(tokio::test)]
// NOTE: failed then passed on retry: count = 6
async fn should_send_validate_repo_when_retryable_error() -> TestResult {
//given
@ -497,20 +474,16 @@ async fn should_send_validate_repo_when_retryable_error() -> TestResult {
repo_details,
given::a_forge(),
);
addr.send(crate::repo::messages::ValidateRepo::new(
MessageToken::default(),
))
.await?;
actix_rt::time::sleep(std::time::Duration::from_millis(2)).await;
System::current().stop();
tell!(addr, ValidateRepo::new(MessageToken::default()))?;
tokio::time::sleep(std::time::Duration::from_millis(2)).await;
//then
log.require_message_containing("accepted token: 0")?;
log.require_message_containing("send: ValidateRepo")?;
log.require_message_containing("accepted token: 0").await?;
log.require_message_containing("send: ValidateRepo").await?;
Ok(())
}
#[test_log::test(actix::test)]
#[test_log::test(tokio::test)]
async fn should_send_notify_user_when_non_retryable_error() -> TestResult {
//given
let fs = given::a_filesystem();
@ -546,15 +519,11 @@ async fn should_send_notify_user_when_non_retryable_error() -> TestResult {
repo_details,
given::a_forge(),
);
addr.send(crate::repo::messages::ValidateRepo::new(
MessageToken::default(),
))
.await?;
actix_rt::time::sleep(std::time::Duration::from_millis(1)).await;
System::current().stop();
tell!(addr, ValidateRepo::new(MessageToken::default()))?;
tokio::time::sleep(std::time::Duration::from_millis(1)).await;
//then
log.require_message_containing("accepted token")?;
log.require_message_containing("send: NotifyUser")?;
log.require_message_containing("accepted token").await?;
log.require_message_containing("send: NotifyUser").await?;
Ok(())
}

View file

@ -1,7 +1,11 @@
use kxio::net::Net;
use crate::{repo::messages::WebhookNotification, tell};
//
use super::*;
#[actix::test]
#[tokio::test]
async fn when_no_expected_auth_token_drop_notification() -> TestResult {
//given
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 forge_notification = ForgeNotification::new(forge_alias, repo_alias, headers, body);
let repository_factory = MockRepositoryFactory::new();
let net: Net = given::a_network().into();
let (actor, log) = given::a_repo_actor(
repo_details,
Box::new(repository_factory),
given::a_forge(),
given::a_network().into(),
given::a_server_actor(fs.as_real(), net.clone()),
net,
);
let actor = actor.with_webhook_auth(None);
//when
actor
.start()
.send(crate::repo::messages::WebhookNotification::new(
forge_notification,
))
.await?;
System::current().stop();
tell!(
kameo::spawn(actor),
WebhookNotification::new(forge_notification)
)?;
//then
log.no_message_contains("send")?;
log.require_message_containing("server has no auth token")?;
log.no_message_contains("send").await?;
log.require_message_containing("server has no auth token")
.await?;
Ok(())
}
#[actix::test]
#[tokio::test]
async fn when_no_repo_config_drop_notification() -> TestResult {
//given
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 forge_notification = ForgeNotification::new(forge_alias, repo_alias, headers, body);
let repository_factory = MockRepositoryFactory::new();
let net: Net = given::a_network().into();
let (actor, log) = given::a_repo_actor(
repo_details,
Box::new(repository_factory),
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()));
//when
actor
.start()
.send(crate::repo::messages::WebhookNotification::new(
forge_notification,
))
.await?;
System::current().stop();
tell!(
kameo::spawn(actor),
WebhookNotification::new(forge_notification)
)?;
//then
log.no_message_contains("send")?;
log.require_message_containing("server has no repo config")?;
log.no_message_contains("send").await?;
log.require_message_containing("server has no repo config")
.await?;
Ok(())
}
#[actix::test]
#[tokio::test]
async fn when_message_auth_is_invalid_drop_notification() -> TestResult {
//given
let fs = given::a_filesystem();
@ -84,30 +88,30 @@ async fn when_message_auth_is_invalid_drop_notification() -> TestResult {
forge
.expect_is_message_authorised()
.return_once(|_, _| false); // is not valid
let net: Net = given::a_network().into();
let (actor, log) = given::a_repo_actor(
repo_details,
Box::new(repository_factory),
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()));
//when
actor
.start()
.send(crate::repo::messages::WebhookNotification::new(
forge_notification,
))
.await?;
System::current().stop();
tell!(
kameo::spawn(actor),
WebhookNotification::new(forge_notification)
)?;
//then
log.no_message_contains("send")?;
log.require_message_containing("message authorisation is invalid")?;
log.no_message_contains("send").await?;
log.require_message_containing("message authorisation is invalid")
.await?;
Ok(())
}
#[actix::test]
#[tokio::test]
async fn when_message_is_ignorable_drop_notification() -> TestResult {
//given
let fs = given::a_filesystem();
@ -126,30 +130,30 @@ async fn when_message_is_ignorable_drop_notification() -> TestResult {
forge
.expect_parse_webhook_body()
.return_once(|_| Err(git::forge::webhook::Error::NetworkResponseEmpty));
let net: Net = given::a_network().into();
let (actor, log) = given::a_repo_actor(
repo_details,
Box::new(repository_factory),
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()));
//when
actor
.start()
.send(crate::repo::messages::WebhookNotification::new(
forge_notification,
))
.await?;
System::current().stop();
tell!(
kameo::spawn(actor),
WebhookNotification::new(forge_notification)
)?;
//then
log.no_message_contains("send")?;
log.require_message_containing("forge sent ignorable message")?;
log.no_message_contains("send").await?;
log.require_message_containing("forge sent ignorable message")
.await?;
Ok(())
}
#[actix::test]
#[tokio::test]
async fn when_message_is_not_a_push_drop_notification() -> TestResult {
//given
let fs = given::a_filesystem();
@ -168,30 +172,30 @@ async fn when_message_is_not_a_push_drop_notification() -> TestResult {
forge
.expect_parse_webhook_body()
.return_once(|_| Err(git::forge::webhook::Error::NetworkResponseEmpty));
let net: Net = given::a_network().into();
let (actor, log) = given::a_repo_actor(
repo_details,
Box::new(repository_factory),
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()));
//when
actor
.start()
.send(crate::repo::messages::WebhookNotification::new(
forge_notification,
))
.await?;
System::current().stop();
tell!(
kameo::spawn(actor),
WebhookNotification::new(forge_notification)
)?;
//then
log.no_message_contains("send")?;
log.require_message_containing("message parse error - not a push")?;
log.no_message_contains("send").await?;
log.require_message_containing("message parse error - not a push")
.await?;
Ok(())
}
#[actix::test]
#[tokio::test]
async fn when_message_is_push_on_unknown_branch_drop_notification() -> TestResult {
//given
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
forge.expect_should_ignore_message().returning(|_| false);
forge.expect_parse_webhook_body().return_once(|_| Ok(push));
let net: Net = given::a_network().into();
let (actor, log) = given::a_repo_actor(
repo_details,
Box::new(repository_factory),
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()))
.with_last_main_commit(commit);
//when
actor
.start()
.send(crate::repo::messages::WebhookNotification::new(
forge_notification,
))
.await?;
System::current().stop();
tell!(
kameo::spawn(actor),
WebhookNotification::new(forge_notification)
)?;
//then
log.no_message_contains("send")?;
log.require_message_containing("unknown branch")?;
log.no_message_contains("send").await?;
log.require_message_containing("unknown branch").await?;
Ok(())
}
#[actix::test]
#[tokio::test]
async fn when_message_is_push_already_seen_commit_to_main() -> TestResult {
//given
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
forge.expect_should_ignore_message().returning(|_| false);
forge.expect_parse_webhook_body().return_once(|_| Ok(push));
let net: Net = given::a_network().into();
let (actor, log) = given::a_repo_actor(
repo_details,
Box::new(repository_factory),
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()))
.with_last_main_commit(commit);
//when
actor
.start()
.send(crate::repo::messages::WebhookNotification::new(
forge_notification,
))
.await?;
System::current().stop();
tell!(
kameo::spawn(actor),
WebhookNotification::new(forge_notification)
)?;
//then
log.no_message_contains("send")?;
log.require_message_containing(format!("not a new commit on {main}"))?;
log.no_message_contains("send").await?;
log.require_message_containing(format!("not a new commit on {main}"))
.await?;
Ok(())
}
#[actix::test]
#[tokio::test]
async fn when_message_is_push_already_seen_commit_to_next() -> TestResult {
//given
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 repository_factory = MockRepositoryFactory::new();
let commit = given::a_commit();
let next = repo_config.branches().next();
let next_branch = repo_config.branches().next();
let push = given::a_push()
.with_branch(next.clone())
.with_branch(next_branch.clone())
.with_sha(commit.sha().to_string())
.with_message(commit.message().to_string());
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
forge.expect_should_ignore_message().returning(|_| false);
forge.expect_parse_webhook_body().return_once(|_| Ok(push));
let net: Net = given::a_network().into();
let (actor, log) = given::a_repo_actor(
repo_details,
Box::new(repository_factory),
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()))
.with_last_next_commit(commit);
//when
actor
.start()
.send(crate::repo::messages::WebhookNotification::new(
forge_notification,
))
.await?;
System::current().stop();
tell!(
kameo::spawn(actor),
WebhookNotification::new(forge_notification)
)?;
//then
log.no_message_contains("send")?;
log.require_message_containing(format!("not a new commit on {next}"))?;
log.no_message_contains("send").await?;
log.require_message_containing(format!("not a new commit on {next_branch}"))
.await?;
Ok(())
}
#[actix::test]
#[tokio::test]
async fn when_message_is_push_already_seen_commit_to_dev() -> TestResult {
//given
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
forge.expect_should_ignore_message().returning(|_| false);
forge.expect_parse_webhook_body().return_once(|_| Ok(push));
let net: Net = given::a_network().into();
let (actor, log) = given::a_repo_actor(
repo_details,
Box::new(repository_factory),
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()))
.with_last_dev_commit(commit);
//when
actor
.start()
.send(crate::repo::messages::WebhookNotification::new(
forge_notification,
))
.await?;
System::current().stop();
tell!(
kameo::spawn(actor),
WebhookNotification::new(forge_notification)
)?;
//then
log.no_message_contains("send")?;
log.require_message_containing(format!("not a new commit on {dev}"))?;
log.no_message_contains("send").await?;
log.require_message_containing(format!("not a new commit on {dev}"))
.await?;
Ok(())
}
#[actix::test]
#[tokio::test]
async fn when_message_is_push_new_commit_to_main_should_stash_and_validate_repo() -> TestResult {
//given
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
forge.expect_should_ignore_message().returning(|_| false);
forge.expect_parse_webhook_body().return_once(|_| Ok(push));
let net: Net = given::a_network().into();
let (actor, log) = given::a_repo_actor(
repo_details,
Box::new(repository_factory),
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()))
.with_last_main_commit(given::a_commit());
//when
let addr = actor.start();
addr.send(crate::repo::messages::WebhookNotification::new(
forge_notification,
))
.await?;
System::current().stop();
let addr = kameo::spawn(actor);
tell!(addr, WebhookNotification::new(forge_notification))?;
//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));
log.require_message_containing("send: ValidateRepo")?;
log.require_message_containing("send: ValidateRepo").await?;
Ok(())
}
#[actix::test]
#[tokio::test]
async fn when_message_is_push_new_commit_to_next_should_stash_and_validate_repo() -> TestResult {
//given
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
forge.expect_should_ignore_message().returning(|_| false);
forge.expect_parse_webhook_body().return_once(|_| Ok(push));
let net: Net = given::a_network().into();
let (actor, log) = given::a_repo_actor(
repo_details,
Box::new(repository_factory),
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()))
.with_last_next_commit(given::a_commit());
//when
let addr = actor.start();
addr.send(crate::repo::messages::WebhookNotification::new(
forge_notification,
))
.await?;
System::current().stop();
let addr = kameo::spawn(actor);
tell!(addr, WebhookNotification::new(forge_notification))?;
//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));
log.require_message_containing("send: ValidateRepo")?;
log.require_message_containing("send: ValidateRepo").await?;
Ok(())
}
#[actix::test]
#[tokio::test]
async fn when_message_is_push_new_commit_to_dev_should_stash_and_validate_repo() -> TestResult {
//given
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
forge.expect_should_ignore_message().returning(|_| false);
forge.expect_parse_webhook_body().return_once(|_| Ok(push));
let net: Net = given::a_network().into();
let (actor, log) = given::a_repo_actor(
repo_details,
Box::new(repository_factory),
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()))
.with_last_dev_commit(given::a_commit());
//when
let addr = actor.start();
addr.send(crate::repo::messages::WebhookNotification::new(
forge_notification,
))
.await?;
System::current().stop();
let addr = kameo::spawn(actor);
tell!(addr, WebhookNotification::new(forge_notification))?;
//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));
log.require_message_containing("send: ValidateRepo")?;
log.require_message_containing("send: ValidateRepo").await?;
Ok(())
}

View file

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

View file

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

View file

@ -1,6 +1,4 @@
//
use actix::prelude::*;
use crate::{
git,
repo::{
@ -12,7 +10,6 @@ use crate::{
use git_next_core::{
git::{
commit::Sha,
forge::commit::Status,
repository::{
factory::{mock, MockRepositoryFactory, RepositoryFactory},
open::{MockOpenRepositoryLike, OpenRepositoryLike},
@ -28,12 +25,17 @@ use git_next_core::{
};
use assert2::let_assert;
use kameo::{
message::{Context, Message},
Reply,
};
use mockall::predicate::eq;
use tracing::{debug, error};
use std::{
collections::{BTreeMap, HashMap},
sync::{Arc, RwLock},
time::Duration,
};
type TestResult = Result<(), Box<dyn std::error::Error>>;
@ -45,33 +47,43 @@ mod handlers;
mod load;
mod when;
pub async fn tick(millis: u64) {
tokio::time::sleep(Duration::from_millis(millis)).await;
}
impl ActorLog {
pub fn no_message_contains(&self, needle: impl AsRef<str> + std::fmt::Display) -> TestResult {
if self.find_in_messages(needle.as_ref())? {
pub async fn no_message_contains(
&self,
needle: impl AsRef<str> + Send + std::fmt::Display,
) -> TestResult {
if self.find_in_messages(needle.as_ref()).await? {
error!(?self, "");
panic!("found unexpected message: {needle}");
}
Ok(())
}
pub fn require_message_containing(
pub async fn require_message_containing(
&self,
needle: impl AsRef<str> + std::fmt::Display,
needle: impl AsRef<str> + Send + std::fmt::Display,
) -> TestResult {
if !self.find_in_messages(needle.as_ref())? {
if !self.find_in_messages(needle.as_ref()).await? {
error!(?self, "");
panic!("expected message not found: {needle}");
}
Ok(())
}
fn find_in_messages(
async fn find_in_messages(
&self,
needle: impl AsRef<str>,
needle: impl AsRef<str> + Send,
) -> 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(5)).await;
let found = self
.read()
.map_err(|e| e.to_string())?
.await
.iter()
.any(|message| message.contains(needle.as_ref()));
Ok(found)
@ -79,15 +91,19 @@ impl ActorLog {
}
message!(ExamineActor => RepoActorView, "Request a view of the current state of the [RepoActor].");
impl Handler<ExamineActor> for RepoActor {
type Result = RepoActorView;
impl Message<ExamineActor> for RepoActor {
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;
Self::Result::from(repo_actor)
Self::Reply::from(repo_actor)
}
}
#[derive(Debug, MessageResponse)]
#[derive(Debug, Reply)]
pub struct RepoActorView {
pub repo_details: RepoDetails,
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::*;
pub fn start_actor(
repository_factory: MockRepositoryFactory,
repo_details: RepoDetails,
forge: Box<dyn ForgeLike>,
) -> (actix::Addr<RepoActor>, ActorLog) {
fs: FileSystem,
net: Net,
) -> (ActorRef<RepoActor>, ActorLog) {
let (actor, log) = given::a_repo_actor(
repo_details,
Box::new(repository_factory),
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(
open_repository: Box<dyn OpenRepositoryLike>,
repo_details: RepoDetails,
forge: Box<dyn ForgeLike>,
) -> (actix::Addr<RepoActor>, ActorLog) {
let (actor, log) = given::a_repo_actor(repo_details, mock(), forge, given::a_network().into());
) -> (ActorRef<RepoActor>, ActorLog) {
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));
(actor.start(), 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));
(kameo::spawn(actor), log)
}

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};
#[cfg(feature = "tui")]
use crate::tui::Tui;
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, 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 tracing::debug;
use crate::{
file_watcher::FileUpdated,
server::actor::{messages::ReceiveAppConfig, ServerActor},
tell,
};
impl Handler<FileUpdated> for ServerActor {
type Result = ();
impl Message<FileUpdated> for ServerActor {
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) {
Ok(app_config) => self.do_send(ReceiveAppConfig::new(app_config), ctx),
Err(err) => self.abort(ctx, format!("Failed to load config file. Error: {err}")),
};
Ok(app_config) => Ok(tell!(
"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 shutdown;
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::{
messages::{ReceiveAppConfig, ReceiveValidAppConfig, ValidAppConfig},
ServerActor,
use crate::{
server::actor::{
messages::{ReceiveAppConfig, ReceiveValidAppConfig, ValidAppConfig},
ServerActor,
},
tell,
};
impl Handler<ReceiveAppConfig> for ServerActor {
type Result = ();
impl Message<ReceiveAppConfig> for ServerActor {
type Reply = Result<()>;
#[allow(clippy::cognitive_complexity)]
fn handle(&mut self, msg: ReceiveAppConfig, ctx: &mut Self::Context) -> Self::Result {
tracing::info!("recieved server config");
async fn handle(
&mut self,
msg: ReceiveAppConfig,
ctx: Context<'_, Self, Self::Reply>,
) -> Self::Reply {
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 {
return self.abort(ctx, "Server storage not available");
self.abort(&ctx, "Server storage not available").await?;
return Ok(());
};
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(());
}
self.do_send(
tell!(
"server",
ctx.actor_ref(),
ReceiveValidAppConfig::new(ValidAppConfig::new(
msg.peel(),
socket_addr,
server_storage,
)),
ctx,
);
))
)?;
Ok(())
}
}

View file

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

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 {
type Result = ();
use crate::{
publish,
server::{actor::messages::ServerUpdate, ServerActor},
};
fn handle(&mut self, msg: ServerUpdate, _ctx: &mut Self::Context) -> Self::Result {
self.subscribers.iter().for_each(move |subscriber| {
subscriber.do_send(msg.clone());
});
impl Message<ServerUpdate> for ServerActor {
type Reply = color_eyre::Result<()>;
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 actix::prelude::*;
use tracing::debug;
//
use color_eyre::Result;
use kameo::message::{Context, Message};
use tracing::{debug, info};
use crate::{
repo::messages::UnRegisterWebhook,
server::actor::{messages::Shutdown, ServerActor},
webhook::messages::ShutdownWebhook,
tell,
};
impl Handler<Shutdown> for ServerActor {
type Result = ();
impl Message<Shutdown> for ServerActor {
type Reply = Result<()>;
fn handle(&mut self, _msg: Shutdown, _ctx: &mut Self::Context) -> Self::Result {
self.repo_actors
.iter()
.for_each(|((forge_alias, repo_alias), addr)| {
debug!(%forge_alias, %repo_alias, "removing webhook");
addr.do_send(UnRegisterWebhook::new());
debug!(%forge_alias, %repo_alias, "removed webhook");
});
async fn handle(
&mut self,
_msg: Shutdown,
_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");
tell!(repo_actor_ref, UnRegisterWebhook::new())?;
debug!(%forge_alias, %repo_alias, "removed webhook");
repo_actor_ref.kill();
info!(%forge_alias, %repo_alias, "killed repo actor");
}
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");
webhook.do_send(ShutdownWebhook);
webhook_actor_ref.kill();
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};
impl Handler<ShutdownTrigger> for ServerActor {
type Result = ();
impl Message<ShutdownTrigger> for ServerActor {
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());
}
}

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 tokio::sync::mpsc::Sender;
use git_next_core::{
git::{self, forge::commit::Status, graph::Log, Commit},
@ -10,8 +12,6 @@ use git_next_core::{
ForgeAlias, RepoAlias, RepoBranches, RepoConfig,
};
use std::net::SocketAddr;
// receive server config
message!(
ReceiveAppConfig,
@ -38,8 +38,7 @@ message!(
message!(Shutdown, "Notification to shutdown the server actor");
#[derive(Clone, Debug, PartialEq, Eq, Message)]
#[rtype(result = "()")]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ServerUpdate {
/// List of all configured forges and aliases
AppConfigLoaded { app_config: ValidAppConfig },
@ -96,18 +95,11 @@ pub enum RepoUpdate {
MainUpdated,
}
message!(
SubscribeToUpdates,
Recipient<ServerUpdate>,
"Subscribe to receive updates from the server"
);
/// Sends a channel to be used to shutdown the server
#[derive(Message, Constructor)]
#[rtype(result = "()")]
pub struct ShutdownTrigger(std::sync::mpsc::Sender<String>);
#[derive(Constructor)]
pub struct ShutdownTrigger(Sender<String>);
impl ShutdownTrigger {
pub fn peel(self) -> std::sync::mpsc::Sender<String> {
pub fn peel(self) -> Sender<String> {
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 tracing::error;
#[cfg(test)]
mod tests;
@ -9,25 +35,6 @@ mod tests;
mod handlers;
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)]
pub enum Error {
#[display("Failed to create data directories")]
@ -41,65 +48,112 @@ pub enum Error {
Config(server::Error),
Io(std::io::Error),
General(String),
}
impl std::error::Error for Error {}
type Result<T> = core::result::Result<T, Error>;
#[allow(clippy::module_name_repetitions)]
#[derive(derive_with::With)]
#[with(message_log)]
pub struct ServerActor {
ui: bool,
app_config: Option<AppConfig>,
generation: Generation,
webhook_actor_addr: Option<Addr<WebhookActor>>,
webhook_actor_ref: Option<ActorRef<WebhookActor>>,
fs: FileSystem,
net: Net,
alerts: Addr<AlertsActor>,
alerts: ActorRef<AlertsActor>,
repository_factory: Box<dyn RepositoryFactory>,
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>>,
subscribers: Vec<Recipient<ServerUpdate>>,
shutdown_trigger: Option<Sender<String>>,
server_updates_bus: MessageBus<ServerUpdate>,
webhook_router_actor_ref: Option<ActorRef<WebhookRouterActor>>,
// testing
message_log: Option<Arc<RwLock<Vec<String>>>>,
}
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 {
pub fn new(
ui: bool,
fs: FileSystem,
net: Net,
alerts: Addr<AlertsActor>,
alerts: ActorRef<AlertsActor>,
server_updates_bus: MessageBus<ServerUpdate>,
repo: Box<dyn RepositoryFactory>,
sleep_duration: std::time::Duration,
) -> Self {
let generation = Generation::default();
Self {
ui,
app_config: None,
generation,
webhook_actor_addr: None,
generation: Generation::default(),
webhook_actor_ref: None,
fs,
net,
alerts,
repository_factory: repo,
shutdown_trigger: None,
subscribers: Vec::default(),
server_updates_bus,
webhook_router_actor_ref: None,
sleep_duration,
repo_actors: BTreeMap::new(),
message_log: None,
}
}
fn create_forge_data_directories(
&self,
app_config: &AppConfig,
server_dir: &std::path::Path,
) -> Result<()> {
for (forge_name, _forge_config) in app_config.forges() {
let forge_dir: PathBuf = (&forge_name).into();
let path = server_dir.join(&forge_dir);
let path = server_dir.join(PathBuf::from(&forge_name));
let path_handle = self.fs.path(&path);
if path_handle.exists()? {
if !path_handle.is_dir()? {
@ -110,45 +164,37 @@ impl ServerActor {
self.fs.dir(&path).create_all()?;
}
}
Ok(())
}
#[instrument(skip_all)]
fn create_forge_repos(
&self,
forge_config: &ForgeConfig,
forge_name: ForgeAlias,
server_storage: &Storage,
listen_url: &ListenUrl,
notify_user_recipient: &Recipient<NotifyUser>,
server_addr: Option<Addr<Self>>,
notify_user_recipient: &ActorRef<AlertsActor>,
server_actor_ref: ActorRef<Self>,
) -> Vec<(ForgeAlias, RepoAlias, RepoActor)> {
let span =
tracing::info_span!("create_forge_repos", name = %forge_name, config = %forge_config);
let _guard = span.enter();
tracing::info!("Creating Forge");
let mut repos = vec![];
tracing::info!(%forge_name, %forge_config, "");
let creator = self.create_actor(
forge_name,
forge_config.clone(),
server_storage,
listen_url,
server_addr,
server_actor_ref,
);
for (repo_alias, server_repo_config) in forge_config.repos() {
let forge_repo = creator((
repo_alias,
server_repo_config,
notify_user_recipient.clone(),
));
tracing::info!(
alias = %forge_repo.1,
"Created Repo"
);
repos.push(forge_repo);
}
repos
forge_config
.repos()
.map(|(repo_alias, server_repo_config)| {
creator((
repo_alias,
server_repo_config,
notify_user_recipient.clone(),
))
})
.collect::<Vec<_>>()
}
fn create_actor(
@ -157,9 +203,9 @@ impl ServerActor {
forge_config: ForgeConfig,
server_storage: &Storage,
listen_url: &ListenUrl,
server_addr: Option<Addr<Self>>,
server_actor_ref: ActorRef<Self>,
) -> impl Fn(
(RepoAlias, &ServerRepoConfig, Recipient<NotifyUser>),
(RepoAlias, &ServerRepoConfig, ActorRef<AlertsActor>),
) -> (ForgeAlias, RepoAlias, RepoActor) {
let server_storage = server_storage.clone();
let listen_url = listen_url.clone();
@ -167,22 +213,19 @@ impl ServerActor {
let repository_factory = self.repository_factory.duplicate();
let generation = self.generation;
let sleep_duration = self.sleep_duration;
let ui = self.ui;
move |(repo_alias, server_repo_config, notify_user_recipient)| {
let span = tracing::info_span!("create_actor", alias = %repo_alias, config = %server_repo_config);
let _guard = span.enter();
tracing::info!("Creating Repo");
let gitdir = server_repo_config.gitdir().map_or_else(
|| {
GitDir::new(
server_storage
.path()
.join(forge_name.to_string())
.join(repo_alias.to_string()),
StoragePathType::Internal,
)
},
|gitdir| gitdir,
);
let gitdir = server_repo_config.gitdir().unwrap_or_else(|| {
GitDir::new(
server_storage
.path()
.join(forge_name.to_string())
.join(repo_alias.to_string()),
StoragePathType::Internal,
)
});
// INFO: can't canonicalise gitdir as the path needs to exist to do that and we may not
// have cloned the repo yet
let repo_details = RepoDetails::new(
@ -194,8 +237,8 @@ impl ServerActor {
gitdir,
);
let forge = Forge::create(repo_details.clone(), net.clone());
tracing::info!("Starting Repo Actor");
let actor = RepoActor::new(
ui,
repo_details,
forge,
listen_url.clone(),
@ -204,7 +247,7 @@ impl ServerActor {
repository_factory.duplicate(),
sleep_duration,
Some(notify_user_recipient),
server_addr.clone(),
server_actor_ref.clone(),
);
(forge_name.clone(), repo_alias, actor)
}
@ -231,31 +274,43 @@ impl ServerActor {
}
/// Attempts to gracefully shutdown the server before stopping the system.
fn abort(&mut self, ctx: &<Self as actix::Actor>::Context, message: impl Into<String>) {
self.do_send(crate::server::actor::messages::Shutdown, ctx);
async fn abort(
&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() {
let _ = t.send(message.into());
} else {
error!("{}", message.into());
self.do_send(Shutdown, ctx);
// System::current().stop_with_code(1);
t.send(message)
.await
.map_err(|e| format!("failed sending shutdown trigger: {e:?}"))?;
}
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
M: actix::Message + Send + 'static + std::fmt::Debug,
Self: actix::Handler<M>,
<M as actix::Message>::Result: Send,
M: Send + Sync + 'static + std::fmt::Debug,
Self: kameo::message::Message<M>,
<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,
{
let log_message = format!("send: {msg:?}");
if let Some(message_log) = &self.message_log {
let log_message = format!("send: {msg:?}");
if let Ok(mut log) = message_log.write() {
log.push(log_message);
log.push(log_message.clone());
}
}
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 actix::prelude::*;
use kameo::actor::ActorRef;
use crate::alerts::{AlertsActor, History};
//
pub fn a_filesystem() -> kxio::fs::TempFileSystem {
#[allow(clippy::expect_used)]
kxio::fs::temp().expect("temp fs")
@ -14,6 +14,10 @@ pub fn a_network() -> kxio::net::MockNet {
kxio::net::mock()
}
pub fn an_alerts_actor(net: kxio::net::Net) -> Addr<AlertsActor> {
AlertsActor::new(None, History::new(Duration::from_millis(1)), net).start()
pub fn an_alerts_actor(net: kxio::net::Net) -> ActorRef<AlertsActor> {
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::{
collections::BTreeMap,
sync::{Arc, RwLock},
};
#[test_log::test(actix::test)]
async fn when_webhook_url_has_trailing_slash_should_not_send() {
use kameo::actor::pubsub::PubSub;
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
// parameters
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 duration = std::time::Duration::from_millis(1);
let file_update_subs = kameo::spawn(PubSub::new());
// 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
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()));
//when
server.start().do_send(ReceiveAppConfig::new(AppConfig::new(
listen,
shout,
server_storage,
repos,
)));
actix_rt::time::sleep(std::time::Duration::from_millis(1)).await;
tell!(
kameo::spawn(server),
ReceiveAppConfig::new(AppConfig::new(listen, shout, server_storage, repos,))
)?;
tokio::time::sleep(std::time::Duration::from_millis(1)).await;
//then
// 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
.iter()
.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;
#[cfg(test)]
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<()> {
let file_name = "git-next-server.toml";
let pathbuf = PathBuf::from(file_name);
@ -51,8 +38,8 @@ pub fn init(fs: &FileSystem) -> Result<()> {
#[allow(clippy::too_many_lines)]
pub fn start(
ui: bool,
fs: FileSystem,
net: Net,
fs: &FileSystem,
net: &Net,
repo: Box<dyn RepositoryFactory>,
sleep_duration: std::time::Duration,
) -> 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_exec = 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();
let shutdown_message_holder_clone = shutdown_message_holder.clone();
info!("Starting Server...");
let server =
ServerActor::new(fs.clone(), net.clone(), alerts_addr, repo, sleep_duration).start();
#[allow(clippy::expect_used)]
tokio::runtime::Runtime::new()?.block_on(async {
let (tx_shutdown, mut rx_shutdown) = tokio::sync::mpsc::channel::<String>(1);
info!("Starting File Watcher...");
let watch_file = watch_file("git-next-server.toml".into(), server.clone().recipient());
let fw_shutdown = match watch_file {
Ok(fw_shutdown) => fw_shutdown,
Err(err) => {
// shutdown now
server.do_send(crate::server::actor::messages::Shutdown);
actix_rt::time::sleep(std::time::Duration::from_millis(10)).await;
System::current().stop();
let _ = file_watcher_err_holder_exec
.write()
.map(|mut o| o.replace(err));
return;
let root_actor_ref = kameo::spawn(RootActor::new(
ui,
fs.clone(),
net.clone(),
sleep_duration,
tx_shutdown,
));
tell!("root", root_actor_ref, crate::root::Start::new(repo)).expect("start root actor");
info!("Server running - Press Ctrl-C to stop...");
tokio::select! {
_r = tokio::signal::ctrl_c() => {
info!("Ctrl-C received, shutting down...");
}
};
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...");
tokio::select! {
_r = signal::ctrl_c() => {
info!("Ctrl-C received, shutting down...");
}
_x = async move {
_x = async move {
loop{
if let Ok(message) = rx_shutdown.try_recv() {
let _ = shutdown_message_holder_exec
.write()
.map(|mut o| o.replace(message));
if let Some(message) = rx_shutdown.recv().await {
info!("rx shutdown received: {message}");
let _ = shutdown_message_holder_clone.write().await.replace(message);
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
break;
}
actix_rt::task::yield_now().await;
}
} => {
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")]
if ui {
ratatui::restore();
}
if !err.is_empty() {
return Err(color_eyre::eyre::eyre!(format!("{err}")));
}
#[cfg(feature = "tui")]
if ui {
ratatui::restore();
}
// check for error from file watcher thread
#[allow(clippy::unwrap_used)]
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}")));
if let Some(ref message) = *shutdown_message_holder.blocking_write() {
info!(%message, "shutdown");
}
Ok(())

View file

@ -38,44 +38,3 @@ mod init {
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 tracing::debug;
use crate::{
server::actor::messages::{RepoUpdate, ServerUpdate},
@ -12,10 +13,14 @@ static PREP: Color = Color::Gray;
static ACTING: Color = Color::LightBlue;
static WARN: Color = Color::Red;
impl Handler<ServerUpdate> for Tui {
type Result = ();
impl Message<ServerUpdate> for Tui {
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();
match msg {
ServerUpdate::AppConfigLoaded { app_config } => {
@ -29,76 +34,82 @@ impl Handler<ServerUpdate> for Tui {
} => {
if let ServerState::Configured { forges } = &mut self.state.mode {
let Some(forge_state) = forges.get_mut(&forge_alias) else {
debug!("ServerState::Configured: no forge state available");
return;
};
let Some(repo_state) = forge_state.repos.get_mut(&repo_alias) else {
debug!("ServerState::Configured: no repo state available");
return;
};
repo_state.clear_alert();
match repo_update {
RepoUpdate::Branches { branches } => {
repo_state.update_branches(branches);
}
RepoUpdate::Log { log } => {
repo_state.update_log(log);
}
RepoUpdate::ValidateRepo => repo_state.update_message("polling...", ACTING),
RepoUpdate::Okay { main, next, dev } => {
repo_state.clear_alert();
repo_state.update_message("okay", OKAY);
*repo_state = repo_state.clone().ready(main, next, dev);
}
RepoUpdate::Alert { alert } => {
repo_state.alert(alert);
}
RepoUpdate::CheckingCI => {
repo_state.update_message("Checking CI status", ACTING);
}
RepoUpdate::AdvancingNext { commit, force: _ } => {
repo_state
.update_message(format!("advancing next to {commit}"), ACTING);
}
RepoUpdate::NextUpdated => {
repo_state.update_message("next updated - pause while CI starts", OKAY);
}
RepoUpdate::AdvancingMain { commit } => {
repo_state
.update_message(format!("advancing main to {commit}"), ACTING);
}
RepoUpdate::MainUpdated => {
repo_state.update_message("main updated", OKAY);
}
RepoUpdate::Opening => {
repo_state.update_message("opening...", PREP);
}
RepoUpdate::Opened => {
repo_state.update_message("opened", PREP);
}
RepoUpdate::LoadingConfigFromRepo => {
repo_state.update_message("loading config from repo...", PREP);
}
RepoUpdate::ReceiveCIStatus { status } => {
repo_state.update_message(format!("ci status: {status:?}"), WARN);
}
RepoUpdate::ReceiveRepoConfig { repo_config: _ } => {
repo_state.update_message("loaded config from repo", PREP);
}
RepoUpdate::RegisteringWebhook => {
repo_state.update_message("registering webhook...", PREP);
}
RepoUpdate::UnregisteringWebhook => {
repo_state.update_message("unregistering webhook...", PREP);
}
RepoUpdate::WebhookReceived { branch, push: _ } => {
repo_state
.update_message(format!("webhook update: {branch:?}"), ACTING);
}
RepoUpdate::RegisteredWebhook => {
repo_state.update_message("registered webhook", PREP);
}
}
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 {
RepoUpdate::Branches { branches } => {
repo_state.update_branches(branches);
}
RepoUpdate::Log { log } => {
repo_state.update_log(log);
}
RepoUpdate::ValidateRepo => {
repo_state.update_message("polling...", ACTING);
}
RepoUpdate::Okay { main, next, dev } => {
repo_state.clear_alert();
repo_state.update_message("okay", OKAY);
*repo_state = repo_state.clone().ready(main, next, dev);
}
RepoUpdate::Alert { alert } => {
repo_state.alert(alert);
}
RepoUpdate::CheckingCI => {
repo_state.update_message("Checking CI status", ACTING);
}
RepoUpdate::AdvancingNext { commit, force: _ } => {
repo_state.update_message(format!("advancing next to {commit}"), ACTING);
}
RepoUpdate::NextUpdated => {
repo_state.update_message("next updated - pause while CI starts", OKAY);
}
RepoUpdate::AdvancingMain { commit } => {
repo_state.update_message(format!("advancing main to {commit}"), ACTING);
}
RepoUpdate::MainUpdated => {
repo_state.update_message("main updated", OKAY);
}
RepoUpdate::Opening => {
repo_state.update_message("opening...", PREP);
}
RepoUpdate::Opened => {
repo_state.update_message("opened", PREP);
}
RepoUpdate::LoadingConfigFromRepo => {
repo_state.update_message("loading config from repo...", PREP);
}
RepoUpdate::ReceiveCIStatus { status } => {
repo_state.update_message(format!("ci status: {status:?}"), WARN);
}
RepoUpdate::ReceiveRepoConfig { repo_config: _ } => {
repo_state.update_message("loaded config from repo", PREP);
}
RepoUpdate::RegisteringWebhook => {
repo_state.update_message("registering webhook...", PREP);
}
RepoUpdate::UnregisteringWebhook => {
repo_state.update_message("unregistering webhook...", PREP);
}
RepoUpdate::WebhookReceived { branch, push: _ } => {
repo_state.update_message(format!("webhook update: {branch:?}"), ACTING);
}
RepoUpdate::RegisteredWebhook => {
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 {
type Result = std::io::Result<()>;
use crate::{
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.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(())
}
}

View file

@ -6,61 +6,85 @@ mod model;
#[cfg(test)]
mod tests;
use std::sync::mpsc::Sender;
use actix::{Actor, ActorContext as _, Context};
pub use model::*;
use color_eyre::Result;
use kameo::{
actor::{ActorRef, WeakActorRef},
error::{ActorStopReason, BoxError, PanicError},
mailbox::unbounded::UnboundedMailbox,
Actor,
};
use ratatui::{
crossterm::event::{self, KeyCode, KeyEventKind},
DefaultTerminal,
};
use tui_scrollview::ScrollViewState;
pub use model::*;
use crate::tell;
use super::Tick;
#[derive(Debug)]
pub struct Tui {
terminal: Option<DefaultTerminal>,
signal_shutdown: Sender<String>,
pub state: State,
scroll_view_state: ScrollViewState,
}
impl Actor for Tui {
type Context = Context<Self>;
fn started(&mut self, _ctx: &mut Self::Context) {
type Mailbox = UnboundedMailbox<Self>;
async fn on_start(&mut self, actor_ref: ActorRef<Self>) -> Result<(), BoxError> {
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();
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 {
pub fn new(signal_shutdown: Sender<String>) -> Self {
pub fn new() -> Self {
Self {
terminal: None,
signal_shutdown,
state: State::initial(),
scroll_view_state: ScrollViewState::default(),
}
}
fn draw(&mut self) -> std::io::Result<()> {
let t = self.terminal.take();
let scroll_view_state = &mut self.scroll_view_state;
let state = &self.state;
if let Some(mut terminal) = t {
if let Some(terminal) = &mut self.terminal {
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 {
eprintln!("No terminal setup");
}
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))? {
let event::Event::Key(key) = event::read()? else {
return Ok(());
@ -70,10 +94,7 @@ impl Tui {
}
match key.code {
KeyCode::Char('q') => {
ctx.stop();
if let Err(err) = self.signal_shutdown.send(String::new()) {
tracing::error!(?err, "Failed to signal shutdown");
}
actor_tui.kill();
}
KeyCode::Char('j') | KeyCode::Down => self.scroll_view_state.scroll_down(),
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::{
layout::Alignment,
prelude::{Buffer, Rect},
@ -7,15 +9,13 @@ use ratatui::{
text::{Line, Span},
widgets::{Block, Paragraph, StatefulWidget, Widget},
};
use tracing::info;
use tui_scrollview::ScrollViewState;
use git_next_core::{
git::{self, graph::Log, Commit},
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};

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 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)]
#[derive(Debug)]
pub struct WebhookActor {
socket_addr: SocketAddr,
span: tracing::Span,
spawn_handle: Option<actix::SpawnHandle>,
message_receiver: Recipient<WebhookNotification>,
message_receiver: ActorRef<WebhookRouterActor>,
}
impl WebhookActor {
pub fn new(socket_addr: SocketAddr, message_receiver: Recipient<WebhookNotification>) -> Self {
let span = tracing::info_span!("WebhookActor");
pub const fn new(
socket_addr: SocketAddr,
message_receiver: ActorRef<WebhookRouterActor>,
) -> Self {
Self {
socket_addr,
span,
message_receiver,
spawn_handle: None,
}
}
}
impl Actor for WebhookActor {
type Context = actix::Context<Self>;
fn started(&mut self, ctx: &mut Self::Context) {
let _gaurd = self.span.enter();
let address: Recipient<WebhookNotification> = self.message_receiver.clone();
let server = server::start(self.socket_addr, address);
let spawn_handle = ctx.spawn(server.in_current_span().into_actor(self));
self.spawn_handle.replace(spawn_handle);
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);
}
#[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 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 crate::{
repo::{messages::WebhookNotification, RepoActor},
tell,
};
#[derive(Actor)]
pub struct WebhookRouterActor {
span: tracing::Span,
recipients: BTreeMap<ForgeAlias, BTreeMap<RepoAlias, Recipient<WebhookNotification>>>,
recipients: BTreeMap<ForgeAlias, BTreeMap<RepoAlias, ActorRef<RepoActor>>>,
}
impl Default for WebhookRouterActor {
fn default() -> Self {
Self::new()
}
}
impl WebhookRouterActor {
pub fn new() -> Self {
let span = tracing::info_span!("WebhookRouter");
Self {
span,
@ -28,43 +30,45 @@ impl WebhookRouterActor {
}
}
}
impl Actor for WebhookRouterActor {
type Context = Context<Self>;
}
impl Message<WebhookNotification> for WebhookRouterActor {
type Reply = color_eyre::Result<()>;
impl Handler<WebhookNotification> for WebhookRouterActor {
type Result = ();
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 _gaurd = self.span.enter();
let forge_alias = msg.forge_alias();
let repo_alias = msg.repo_alias();
debug!(forge = %forge_alias, repo = %repo_alias, "Router...");
let Some(forge_repos) = self.recipients.get(forge_alias) else {
warn!(forge = %forge_alias, "No forge repos found");
return;
return Ok(());
};
let Some(recipient) = forge_repos.get(repo_alias) else {
debug!(forge = %forge_alias, repo = %repo_alias, "No recipient found");
return;
return Ok(());
};
recipient.do_send(msg);
tell!(recipient, msg)?;
Ok(())
}
}
#[derive(Message, Constructor)]
#[rtype(result = "()")]
#[derive(Constructor, Debug)]
pub struct AddWebhookRecipient {
pub forge_alias: ForgeAlias,
pub repo_alias: RepoAlias,
pub recipient: Recipient<WebhookNotification>,
pub recipient: ActorRef<RepoActor>,
}
impl Handler<AddWebhookRecipient> for WebhookRouterActor {
type Result = ();
impl Message<AddWebhookRecipient> for WebhookRouterActor {
type Reply = ();
fn handle(&mut self, msg: AddWebhookRecipient, _ctx: &mut Self::Context) -> Self::Result {
let _gaurd = self.span.enter();
info!(forge = %msg.forge_alias, repo = %msg.repo_alias, "Register Recipient");
async fn handle(
&mut self,
msg: AddWebhookRecipient,
_ctx: Context<'_, Self, Self::Reply>,
) -> Self::Reply {
if !self.recipients.contains_key(&msg.forge_alias) {
self.recipients
.insert(msg.forge_alias.clone(), BTreeMap::new());

View file

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

View file

@ -352,3 +352,13 @@ impl SmtpConfig {
&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() {
info!("Local copy found - opening...");
let repo = repository_factory.open(repo_details)?;
repo.fetch()?;
repo
repository_factory.open(repo_details)?
} else {
info!("Local copy not found - cloning...");
repository_factory.git_clone(repo_details)?
};
info!("Validating...");
validate_default_remotes(&*open_repository, repo_details)
.map_err(|e| Error::Validation(s!(e)))?;
Ok(open_repository)

View file

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

View file

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

View file

@ -2,26 +2,14 @@
macro_rules! message {
($name:ident, $value:ty, $docs:literal) => {
git_next_core::newtype!($name, $value, $docs);
impl actix::prelude::Message for $name {
type Result = ();
}
};
($name:ident, $docs:literal) => {
git_next_core::newtype!($name, $docs);
impl actix::prelude::Message for $name {
type Result = ();
}
};
($name:ident, $value:ty => $result:ty, $docs:literal) => {
git_next_core::newtype!($name, $value, $docs);
impl actix::prelude::Message for $name {
type Result = $result;
}
};
($name:ident => $result:ty, $docs:literal) => {
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};
@ -9,14 +10,14 @@ use tracing::{info, instrument, warn};
use crate::webhook;
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(
repo_details: &git::RepoDetails,
repo_listen_url: &RepoListenUrl,
net: &Net,
) -> git::forge::webhook::Result<RegisteredWebhook> {
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
@ -51,17 +52,17 @@ pub async fn register(
let Ok(hook) = response.json::<Hook>().await else {
#[cfg(not(tarpaulin_include))]
// 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(
WebhookId::new(format!("{}", hook.id)),
authorisation,
))
}
Err(e) => {
warn!("Failed to register webhook");
Err(git::forge::webhook::Error::FailedToRegister(e.to_string()))
warn!(?e, "failed");
Err(Error::FailedToRegister(e.to_string()))
}
}
// Ok(())

View file

@ -3,6 +3,7 @@ build:
set -e
cargo fmt
cargo fmt --check
cargo machete
cargo hack clippy
cargo hack build
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(