use std::sync::{
Arc, Mutex,
atomic::{AtomicUsize, Ordering},
};
use cordis_core::{App, Error, Plugin, PluginContext, Result, ServiceKey};
trait Marker: Send + Sync {}
struct MarkerKey;
impl ServiceKey for MarkerKey {
type Value = dyn Marker;
const NAME: &'static str = "marker";
}
struct MarkerValue;
impl Marker for MarkerValue {}
struct RollbackPlugin;
impl Plugin for RollbackPlugin {
type Config = Arc<AtomicUsize>;
fn name(&self) -> &'static str {
"rollback-plugin"
}
async fn apply(&self, ctx: PluginContext, calls: Arc<Self::Config>) -> Result<()> {
let marker: Arc<dyn Marker> = Arc::new(MarkerValue);
ctx.provide::<MarkerKey>(marker)?;
ctx.on::<RollbackEvent, _, _>(move |_, _| {
let calls = calls.clone();
async move {
calls.fetch_add(1, Ordering::SeqCst);
Ok(())
}
})?;
Err(Error::cleanup("apply failed"))
}
}
struct RollbackEvent;
#[tokio::test]
async fn failed_apply_rolls_back_all_registered_effects() -> Result<()> {
let app = App::new();
let calls = Arc::new(AtomicUsize::new(0));
let plugin = app.install(RollbackPlugin, calls.clone()).await?;
assert!(matches!(
plugin.status(),
cordis_core::PluginStatus::Failed {
phase: cordis_core::FailurePhase::Apply,
message,
..
} if message.contains("apply failed")
));
assert!(app.context().try_get::<MarkerKey>().is_none());
app.context().emit(RollbackEvent).await?;
assert_eq!(calls.load(Ordering::SeqCst), 0);
plugin.dispose().await
}
struct CleanupOrderPlugin;
impl Plugin for CleanupOrderPlugin {
type Config = Arc<Mutex<Vec<u8>>>;
fn name(&self) -> &'static str {
"cleanup-order"
}
async fn apply(&self, ctx: PluginContext, log: Arc<Self::Config>) -> Result<()> {
for value in [1, 2, 3] {
let log = log.clone();
ctx.defer(move || async move {
log.lock().unwrap().push(value);
Ok(())
})?;
}
Ok(())
}
}
#[tokio::test]
async fn cleanup_runs_in_reverse_registration_order() -> Result<()> {
let app = App::new();
let log = Arc::new(Mutex::new(Vec::new()));
let scope = app.install(CleanupOrderPlugin, log.clone()).await?;
scope.dispose().await?;
assert_eq!(*log.lock().unwrap(), vec![3, 2, 1]);
Ok(())
}
struct FailingCleanupPlugin;
impl Plugin for FailingCleanupPlugin {
type Config = Arc<Mutex<Vec<u8>>>;
fn name(&self) -> &'static str {
"failing-cleanup"
}
async fn apply(&self, ctx: PluginContext, log: Arc<Self::Config>) -> Result<()> {
let first = log.clone();
ctx.defer(move || async move {
first.lock().unwrap().push(1);
Err(Error::cleanup("first registered failure"))
})?;
let second = log.clone();
ctx.defer(move || async move {
second.lock().unwrap().push(2);
Err(Error::cleanup("first executed failure"))
})?;
ctx.defer(move || async move {
log.lock().unwrap().push(3);
Ok(())
})?;
Ok(())
}
}
#[tokio::test]
async fn cleanup_continues_after_errors_and_returns_first_execution_error() -> Result<()> {
let app = App::new();
let log = Arc::new(Mutex::new(Vec::new()));
let scope = app.install(FailingCleanupPlugin, log.clone()).await?;
let error = scope.dispose().await.unwrap_err();
assert!(matches!(error, Error::Cleanup(message) if message == "first executed failure"));
assert_eq!(*log.lock().unwrap(), vec![3, 2, 1]);
Ok(())
}
struct CountCleanup;
impl Plugin for CountCleanup {
type Config = Arc<AtomicUsize>;
fn name(&self) -> &'static str {
"count-cleanup"
}
async fn apply(&self, ctx: PluginContext, count: Arc<Self::Config>) -> Result<()> {
ctx.defer(move || async move {
count.fetch_add(1, Ordering::SeqCst);
Ok(())
})
}
}
#[tokio::test]
async fn explicit_dispose_then_app_shutdown_does_not_cleanup_twice() -> Result<()> {
let app = App::new();
let count = Arc::new(AtomicUsize::new(0));
let scope = app.install(CountCleanup, count.clone()).await?;
scope.dispose().await?;
app.shutdown().await?;
assert_eq!(count.load(Ordering::SeqCst), 1);
Ok(())
}
#[tokio::test]
async fn plugin_scope_exposes_stable_name_and_nonzero_id() -> Result<()> {
let app = App::new();
let first = app
.install(CountCleanup, Arc::new(AtomicUsize::new(0)))
.await?;
let second = app
.install(CountCleanup, Arc::new(AtomicUsize::new(0)))
.await?;
assert_eq!(first.name(), "count-cleanup");
assert_ne!(first.id(), second.id());
second.dispose().await?;
first.dispose().await
}