use rust_supervisor::error::types::{TaskFailure, TaskFailureKind};
use rust_supervisor::id::types::{ChildId, ChildStartCount, Generation, SupervisorPath};
use rust_supervisor::role::adapter::sidecar::SidecarRoleAdapter;
use rust_supervisor::role::context::sidecar::SidecarContext;
use rust_supervisor::role::lifecycle::RoleLifecyclePhase;
use rust_supervisor::role::result::sidecar::{SidecarError, SidecarResult};
use rust_supervisor::role::traits::sidecar::SidecarRole;
use rust_supervisor::task::context::TaskContext;
use rust_supervisor::task::factory::{TaskFactory, TaskResult};
use std::sync::{Arc, Mutex};
fn task_context(id: &str) -> TaskContext {
let (ctx, _heartbeat) = TaskContext::new(
ChildId::new(id),
SupervisorPath::root().join(id),
Generation::initial(),
ChildStartCount::first(),
);
ctx
}
fn primary_child_id() -> ChildId {
ChildId::new("primary-service")
}
struct RecordingSidecar {
events: Arc<Mutex<Vec<&'static str>>>,
}
impl SidecarRole for RecordingSidecar {
async fn init(&mut self, ctx: &SidecarContext) -> SidecarResult<()> {
let _ = ctx;
self.events.lock().expect("events lock").push("init");
Ok(())
}
async fn run(&mut self, ctx: &SidecarContext) -> SidecarResult<()> {
let _ = ctx;
self.events.lock().expect("events lock").push("run");
Ok(())
}
async fn shutdown(&mut self, ctx: &SidecarContext) -> SidecarResult<()> {
let _ = ctx;
self.events.lock().expect("events lock").push("shutdown");
Ok(())
}
}
#[tokio::test]
async fn sidecar_role_adapter_runs_lifecycle_in_order() {
let events = Arc::new(Mutex::new(Vec::new()));
let sidecar = RecordingSidecar {
events: events.clone(),
};
let adapter = SidecarRoleAdapter::new(sidecar, primary_child_id());
let result = TaskFactory::build(&adapter, task_context("metrics-sidecar")).await;
assert_eq!(result, TaskResult::Succeeded);
assert_eq!(
*events.lock().expect("events lock"),
vec!["init", "run", "shutdown"]
);
}
struct PrimaryAwareSidecar {
seen_primary_id: Arc<Mutex<Option<ChildId>>>,
}
impl SidecarRole for PrimaryAwareSidecar {
async fn run(&mut self, ctx: &SidecarContext) -> SidecarResult<()> {
*self.seen_primary_id.lock().expect("primary id lock") = Some(ctx.primary_id().clone());
Ok(())
}
}
#[tokio::test]
async fn sidecar_context_exposes_primary_child_id() {
let seen_primary_id = Arc::new(Mutex::new(None));
let adapter = SidecarRoleAdapter::new(
PrimaryAwareSidecar {
seen_primary_id: seen_primary_id.clone(),
},
primary_child_id(),
);
let result = TaskFactory::build(&adapter, task_context("primary-aware-sidecar")).await;
assert_eq!(result, TaskResult::Succeeded);
assert_eq!(
*seen_primary_id.lock().expect("primary id lock"),
Some(primary_child_id())
);
}
struct ShutdownAwareSidecar {
seen_shutdown: Arc<Mutex<bool>>,
}
impl SidecarRole for ShutdownAwareSidecar {
async fn run(&mut self, ctx: &SidecarContext) -> SidecarResult<()> {
ctx.wait_shutdown().await;
*self.seen_shutdown.lock().expect("shutdown lock") = ctx.is_shutdown_requested();
Ok(())
}
}
#[tokio::test]
async fn sidecar_context_observes_runtime_shutdown() {
let seen_shutdown = Arc::new(Mutex::new(false));
let adapter = SidecarRoleAdapter::new(
ShutdownAwareSidecar {
seen_shutdown: seen_shutdown.clone(),
},
primary_child_id(),
);
let ctx = task_context("shutdown-sidecar");
ctx.cancel();
let result = TaskFactory::build(&adapter, ctx).await;
assert_eq!(result, TaskResult::Cancelled);
assert!(*seen_shutdown.lock().expect("shutdown lock"));
}
struct FailingSidecar;
impl SidecarRole for FailingSidecar {
async fn run(&mut self, ctx: &SidecarContext) -> SidecarResult<()> {
Err(SidecarError::new(
ctx.child_id().clone(),
ctx.primary_id().clone(),
RoleLifecyclePhase::Run,
"sidecar body failed",
))
}
}
#[tokio::test]
async fn sidecar_error_maps_to_task_failure() {
let adapter = SidecarRoleAdapter::new(FailingSidecar, primary_child_id());
let result = TaskFactory::build(&adapter, task_context("failing-sidecar")).await;
match result {
TaskResult::Failed(TaskFailure {
kind,
category,
message,
}) => {
assert_eq!(kind, TaskFailureKind::Error);
assert_eq!(category, "sidecar_run");
assert!(message.contains("primary=primary-service"));
assert!(message.contains("sidecar body failed"));
}
other => panic!("expected failed task result, got {other:?}"),
}
}