use mecha10_core::prelude::*;
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::Mutex;
use tokio::time::{sleep, timeout};
#[derive(Debug, Clone)]
struct SuccessNode {
#[allow(dead_code)]
config: SuccessConfig,
run_count: Arc<AtomicU32>,
shutdown_count: Arc<AtomicU32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct SuccessConfig {
value: i32,
}
#[async_trait]
impl NodeImpl for SuccessNode {
type Config = SuccessConfig;
async fn init(config: Self::Config) -> Result<Self> {
Ok(Self {
config,
run_count: Arc::new(AtomicU32::new(0)),
shutdown_count: Arc::new(AtomicU32::new(0)),
})
}
async fn run(&mut self, _ctx: &Context) -> Result<()> {
self.run_count.fetch_add(1, Ordering::SeqCst);
Ok(())
}
async fn shutdown(&mut self) -> Result<()> {
self.shutdown_count.fetch_add(1, Ordering::SeqCst);
Ok(())
}
async fn health_check(&self) -> HealthStatus {
HealthStatus::healthy("SuccessNode is healthy")
}
}
#[derive(Debug)]
struct FailInitNode;
#[derive(Debug, Clone, Serialize, Deserialize)]
struct FailInitConfig;
#[async_trait]
impl NodeImpl for FailInitNode {
type Config = FailInitConfig;
async fn init(_config: Self::Config) -> Result<Self> {
Err(Mecha10Error::NodeInit("Initialization failed".to_string()))
}
async fn run(&mut self, _ctx: &Context) -> Result<()> {
Ok(())
}
}
#[derive(Debug, Clone)]
struct FailRunNode {
should_fail: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct FailRunConfig {
should_fail: bool,
}
#[async_trait]
impl NodeImpl for FailRunNode {
type Config = FailRunConfig;
async fn init(config: Self::Config) -> Result<Self> {
Ok(Self {
should_fail: config.should_fail,
})
}
async fn run(&mut self, _ctx: &Context) -> Result<()> {
if self.should_fail {
Err(Mecha10Error::Runtime("Run failed".to_string()))
} else {
Ok(())
}
}
}
#[derive(Debug, Clone)]
struct FailShutdownNode {
shutdown_error: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct FailShutdownConfig {
shutdown_error: bool,
}
#[async_trait]
impl NodeImpl for FailShutdownNode {
type Config = FailShutdownConfig;
async fn init(config: Self::Config) -> Result<Self> {
Ok(Self {
shutdown_error: config.shutdown_error,
})
}
async fn run(&mut self, _ctx: &Context) -> Result<()> {
Ok(())
}
async fn shutdown(&mut self) -> Result<()> {
if self.shutdown_error {
Err(Mecha10Error::Shutdown("Shutdown failed".to_string()))
} else {
Ok(())
}
}
}
#[derive(Debug, Clone)]
struct TimedNode {
duration_ms: u64,
started: Arc<AtomicBool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct TimedConfig {
duration_ms: u64,
}
#[async_trait]
impl NodeImpl for TimedNode {
type Config = TimedConfig;
async fn init(config: Self::Config) -> Result<Self> {
Ok(Self {
duration_ms: config.duration_ms,
started: Arc::new(AtomicBool::new(false)),
})
}
async fn run(&mut self, ctx: &Context) -> Result<()> {
self.started.store(true, Ordering::SeqCst);
sleep(Duration::from_millis(self.duration_ms)).await;
if ctx.is_shutdown().await {
return Ok(());
}
Ok(())
}
}
#[derive(Debug, Clone)]
struct HealthyNode {
healthy: Arc<Mutex<bool>>,
failure_count: Arc<AtomicU32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct HealthyConfig {
initially_healthy: bool,
}
#[async_trait]
impl NodeImpl for HealthyNode {
type Config = HealthyConfig;
async fn init(config: Self::Config) -> Result<Self> {
Ok(Self {
healthy: Arc::new(Mutex::new(config.initially_healthy)),
failure_count: Arc::new(AtomicU32::new(0)),
})
}
async fn run(&mut self, ctx: &Context) -> Result<()> {
while !ctx.is_shutdown().await {
sleep(Duration::from_millis(10)).await;
}
Ok(())
}
async fn health_check(&self) -> HealthStatus {
let is_healthy = *self.healthy.lock().await;
if is_healthy {
HealthStatus {
healthy: true,
last_check: now_micros(),
failure_count: 0,
message: Some("Node is healthy".to_string()),
}
} else {
let failures = self.failure_count.fetch_add(1, Ordering::SeqCst) + 1;
HealthStatus {
healthy: false,
last_check: now_micros(),
failure_count: failures,
message: Some("Node is unhealthy".to_string()),
}
}
}
}
#[derive(Debug, Clone)]
struct RateConfigNode {
config: RateNodeConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct RateNodeConfig {
rate_hz: f32,
}
impl RateConfig for RateNodeConfig {
fn rate_hz(&self) -> f32 {
self.rate_hz
}
}
#[async_trait]
impl NodeImpl for RateConfigNode {
type Config = RateNodeConfig;
async fn init(config: Self::Config) -> Result<Self> {
Ok(Self { config })
}
async fn run(&mut self, ctx: &Context) -> Result<()> {
let mut interval = self.config.create_interval();
for _ in 0..3 {
interval.tick().await;
if ctx.is_shutdown().await {
break;
}
}
Ok(())
}
}
async fn create_test_context() -> Context {
std::env::set_var("REDIS_URL", "redis://localhost:6379");
Context::builder("test_node")
.with_redis("redis://localhost:6379")
.build()
.await
.expect("Failed to create test context - is Redis running on localhost:6379?")
}
#[tokio::test]
async fn test_node_init_success() {
let config = SuccessConfig { value: 42 };
let node = Node::<SuccessNode>::from_config(config).await;
assert!(node.is_ok(), "Node initialization should succeed");
let node = node.unwrap();
let state = node.state().await;
assert_eq!(state, NodeState::Starting);
}
#[tokio::test]
async fn test_node_init_failure() {
let config = FailInitConfig;
let result = Node::<FailInitNode>::from_config(config).await;
assert!(result.is_err(), "Node initialization should fail");
if let Err(e) = result {
assert!(e.to_string().contains("Initialization failed"));
}
}
#[tokio::test]
async fn test_node_init_with_config_validation() {
let config = SuccessConfig { value: 123 };
let node = Node::<SuccessNode>::from_config(config).await;
assert!(node.is_ok());
}
#[tokio::test]
#[ignore = "Requires Redis"]
async fn test_node_state_starting_to_running() {
let config = SuccessConfig { value: 42 };
let node = Node::<SuccessNode>::from_config(config).await.unwrap();
assert_eq!(node.state().await, NodeState::Starting);
let ctx = create_test_context().await;
let result = node.start(ctx).await;
assert!(result.is_ok());
assert_eq!(node.state().await, NodeState::Stopped);
}
#[tokio::test]
#[ignore = "Requires Redis"]
async fn test_node_state_running_to_stopped_on_success() {
let config = SuccessConfig { value: 42 };
let node = Node::<SuccessNode>::from_config(config).await.unwrap();
let ctx = create_test_context().await;
let result = node.start(ctx).await;
assert!(result.is_ok());
assert_eq!(node.state().await, NodeState::Stopped);
}
#[tokio::test]
#[ignore = "Requires Redis"]
async fn test_node_state_crashed_on_run_error() {
let config = FailRunConfig { should_fail: true };
let node = Node::<FailRunNode>::from_config(config).await.unwrap();
let ctx = create_test_context().await;
let result = node.start(ctx).await;
assert!(result.is_err(), "Node should fail during run");
assert_eq!(node.state().await, NodeState::Stopped);
}
#[tokio::test]
#[ignore = "Requires Redis"]
async fn test_node_state_stopping_to_stopped() {
let config = TimedConfig { duration_ms: 100 };
let node = Node::<TimedNode>::from_config(config).await.unwrap();
let ctx = create_test_context().await;
let node_clone = node.clone();
let ctx_clone = ctx.clone();
tokio::spawn(async move {
let _ = node_clone.start(ctx_clone).await;
});
sleep(Duration::from_millis(10)).await;
let shutdown_result = node.shutdown().await;
assert!(shutdown_result.is_ok());
assert_eq!(node.state().await, NodeState::Stopped);
}
#[tokio::test]
#[ignore = "Requires Redis"]
async fn test_node_run_executes() {
let config = SuccessConfig { value: 42 };
let node = Node::<SuccessNode>::from_config(config).await.unwrap();
let ctx = create_test_context().await;
let result = node.start(ctx).await;
assert!(result.is_ok());
}
#[tokio::test]
#[ignore = "Requires Redis"]
async fn test_node_run_with_duration() {
let config = TimedConfig { duration_ms: 50 };
let node = Node::<TimedNode>::from_config(config).await.unwrap();
let ctx = create_test_context().await;
let start = Instant::now();
let result = node.start(ctx).await;
let elapsed = start.elapsed();
assert!(result.is_ok());
assert!(elapsed >= Duration::from_millis(50));
assert!(elapsed < Duration::from_millis(200));
}
#[tokio::test]
#[ignore = "Requires Redis"]
async fn test_node_run_error_propagates() {
let config = FailRunConfig { should_fail: true };
let node = Node::<FailRunNode>::from_config(config).await.unwrap();
let ctx = create_test_context().await;
let result = node.start(ctx).await;
assert!(result.is_err());
if let Err(e) = result {
assert!(e.to_string().contains("Run failed"));
}
}
#[tokio::test]
async fn test_node_shutdown_success() {
let config = SuccessConfig { value: 42 };
let node = Node::<SuccessNode>::from_config(config).await.unwrap();
let result = node.shutdown().await;
assert!(result.is_ok());
assert_eq!(node.state().await, NodeState::Stopped);
}
#[tokio::test]
async fn test_node_shutdown_error_still_transitions() {
let config = FailShutdownConfig { shutdown_error: true };
let node = Node::<FailShutdownNode>::from_config(config).await.unwrap();
let result = node.shutdown().await;
assert!(result.is_err());
assert_eq!(node.state().await, NodeState::Stopped);
if let Err(e) = result {
assert!(e.to_string().contains("Shutdown failed"));
}
}
#[tokio::test]
async fn test_node_double_shutdown() {
let config = SuccessConfig { value: 42 };
let node = Node::<SuccessNode>::from_config(config).await.unwrap();
let result1 = node.shutdown().await;
assert!(result1.is_ok());
let result2 = node.shutdown().await;
assert!(result2.is_ok());
assert_eq!(node.state().await, NodeState::Stopped);
}
#[tokio::test]
#[ignore = "Requires Redis"]
async fn test_node_shutdown_called_after_start() {
let config = SuccessConfig { value: 42 };
let node = Node::<SuccessNode>::from_config(config).await.unwrap();
let ctx = create_test_context().await;
let result = node.start(ctx).await;
assert!(result.is_ok());
assert_eq!(node.state().await, NodeState::Stopped);
}
#[tokio::test]
async fn test_node_health_check_default() {
let config = SuccessConfig { value: 42 };
let node = Node::<SuccessNode>::from_config(config).await.unwrap();
let health = node.health_check().await;
assert!(health.healthy);
assert!(health.message.is_some());
}
#[tokio::test]
async fn test_node_health_check_custom() {
let config = HealthyConfig {
initially_healthy: true,
};
let node = Node::<HealthyNode>::from_config(config).await.unwrap();
let health = node.health_check().await;
assert!(health.healthy);
assert_eq!(health.failure_count, 0);
}
#[tokio::test]
async fn test_node_health_check_unhealthy() {
let config = HealthyConfig {
initially_healthy: false,
};
let node = Node::<HealthyNode>::from_config(config).await.unwrap();
let health = node.health_check().await;
assert!(!health.healthy);
assert!(health.failure_count > 0);
}
#[tokio::test]
async fn test_node_health_check_multiple_calls() {
let config = HealthyConfig {
initially_healthy: false,
};
let node = Node::<HealthyNode>::from_config(config).await.unwrap();
let health1 = node.health_check().await;
assert!(!health1.healthy);
let failures1 = health1.failure_count;
let health2 = node.health_check().await;
assert!(!health2.healthy);
assert!(health2.failure_count > failures1);
}
#[tokio::test]
#[ignore = "Requires Redis"]
async fn test_node_is_healthy() {
let config = SuccessConfig { value: 42 };
let node = Node::<SuccessNode>::from_config(config).await.unwrap();
assert!(node.is_healthy().await);
let ctx = create_test_context().await;
let _ = node.start(ctx).await;
assert!(!node.is_healthy().await);
}
#[test]
fn test_health_status_ext_healthy() {
let status = HealthStatus::healthy("All systems operational");
assert!(status.healthy);
assert_eq!(status.failure_count, 0);
assert_eq!(status.message, Some("All systems operational".to_string()));
assert!(status.last_check > 0);
}
#[test]
fn test_health_status_ext_unhealthy() {
let status = HealthStatus::unhealthy("Connection failed");
assert!(!status.healthy);
assert_eq!(status.failure_count, 1);
assert_eq!(status.message, Some("Connection failed".to_string()));
assert!(status.last_check > 0);
}
#[test]
fn test_health_status_ext_with_string() {
let status = HealthStatus::healthy("OK".to_string());
assert!(status.healthy);
assert_eq!(status.message, Some("OK".to_string()));
}
#[test]
fn test_rate_config_rate_hz() {
let config = RateNodeConfig { rate_hz: 30.0 };
assert_eq!(config.rate_hz(), 30.0);
}
#[test]
fn test_rate_config_period() {
let config = RateNodeConfig { rate_hz: 10.0 };
let period = config.period();
let expected = Duration::from_millis(100);
let diff = if period > expected {
period - expected
} else {
expected - period
};
assert!(
diff < Duration::from_micros(1),
"Period should be approximately 100ms, got {:?}",
period
);
}
#[test]
fn test_rate_config_period_high_frequency() {
let config = RateNodeConfig { rate_hz: 100.0 };
let period = config.period();
assert_eq!(period, Duration::from_millis(10));
}
#[tokio::test]
async fn test_rate_config_create_interval() {
let config = RateNodeConfig { rate_hz: 10.0 };
let mut interval = config.create_interval();
let start = Instant::now();
interval.tick().await;
interval.tick().await;
let elapsed = start.elapsed();
assert!(elapsed >= Duration::from_millis(90));
assert!(elapsed < Duration::from_millis(150));
}
#[tokio::test]
#[ignore = "Requires Redis"]
async fn test_rate_config_in_node() {
let config = RateNodeConfig { rate_hz: 100.0 };
let node = Node::<RateConfigNode>::from_config(config).await.unwrap();
let ctx = create_test_context().await;
let start = Instant::now();
let result = node.start(ctx).await;
let elapsed = start.elapsed();
assert!(result.is_ok());
assert!(elapsed >= Duration::from_millis(20));
assert!(elapsed < Duration::from_millis(100));
}
#[test]
fn test_health_reporting_config_default() {
let config = HealthReportingConfig::default();
assert_eq!(config.priority.interval(), Duration::from_secs(5));
assert!(config.enabled);
}
#[test]
fn test_health_reporting_config_priority_levels() {
let critical = HealthReportingConfig::critical();
assert_eq!(critical.priority.interval(), Duration::from_secs(1));
assert!(critical.enabled);
let normal = HealthReportingConfig::normal();
assert_eq!(normal.priority.interval(), Duration::from_secs(5));
assert!(normal.enabled);
let background = HealthReportingConfig::background();
assert_eq!(background.priority.interval(), Duration::from_secs(30));
assert!(background.enabled);
}
#[test]
fn test_health_reporting_config_clone() {
let config1 = HealthReportingConfig::critical();
let config2 = config1.clone();
assert_eq!(config1.priority.interval(), config2.priority.interval());
assert_eq!(config1.enabled, config2.enabled);
}
#[tokio::test]
#[ignore = "Requires Redis"]
async fn test_node_concurrent_operations() {
let config = TimedConfig { duration_ms: 100 };
let node = Node::<TimedNode>::from_config(config).await.unwrap();
let ctx = create_test_context().await;
let node_clone = node.clone();
let ctx_clone = ctx.clone();
let handle = tokio::spawn(async move { node_clone.start(ctx_clone).await });
sleep(Duration::from_millis(10)).await;
let _health = node.health_check().await;
let _ = handle.await;
}
#[tokio::test]
#[ignore = "Requires Redis"]
async fn test_node_state_query_during_lifecycle() {
let config = TimedConfig { duration_ms: 50 };
let node = Node::<TimedNode>::from_config(config).await.unwrap();
assert_eq!(node.state().await, NodeState::Starting);
let ctx = create_test_context().await;
let node_clone = node.clone();
let ctx_clone = ctx.clone();
tokio::spawn(async move {
let _ = node_clone.start(ctx_clone).await;
});
sleep(Duration::from_millis(10)).await;
for _ in 0..5 {
let _ = node.state().await;
sleep(Duration::from_millis(5)).await;
}
sleep(Duration::from_millis(100)).await;
assert_eq!(node.state().await, NodeState::Stopped);
}
#[tokio::test]
#[ignore = "Requires Redis"]
async fn test_multiple_nodes_same_process() {
let config1 = SuccessConfig { value: 1 };
let config2 = SuccessConfig { value: 2 };
let config3 = SuccessConfig { value: 3 };
let node1 = Node::<SuccessNode>::from_config(config1).await.unwrap();
let node2 = Node::<SuccessNode>::from_config(config2).await.unwrap();
let node3 = Node::<SuccessNode>::from_config(config3).await.unwrap();
let ctx1 = create_test_context().await;
let ctx2 = create_test_context().await;
let ctx3 = create_test_context().await;
let handle1 = tokio::spawn({
let node = node1.clone();
async move { node.start(ctx1).await }
});
let handle2 = tokio::spawn({
let node = node2.clone();
async move { node.start(ctx2).await }
});
let handle3 = tokio::spawn({
let node = node3.clone();
async move { node.start(ctx3).await }
});
let results = tokio::join!(handle1, handle2, handle3);
assert!(results.0.unwrap().is_ok());
assert!(results.1.unwrap().is_ok());
assert!(results.2.unwrap().is_ok());
assert_eq!(node1.state().await, NodeState::Stopped);
assert_eq!(node2.state().await, NodeState::Stopped);
assert_eq!(node3.state().await, NodeState::Stopped);
}
#[tokio::test]
async fn test_node_clone_shares_state() {
let config = SuccessConfig { value: 42 };
let node1 = Node::<SuccessNode>::from_config(config).await.unwrap();
let node2 = node1.clone();
assert_eq!(node1.state().await, node2.state().await);
let _ = node1.shutdown().await;
assert_eq!(node2.state().await, NodeState::Stopped);
}
#[tokio::test]
#[ignore = "Requires Redis"]
async fn test_node_respects_context_shutdown() {
let config = TimedConfig { duration_ms: 1000 };
let node = Node::<TimedNode>::from_config(config).await.unwrap();
let ctx = create_test_context().await;
let node_clone = node.clone();
let ctx_clone = ctx.clone();
let handle = tokio::spawn(async move { node_clone.start(ctx_clone).await });
sleep(Duration::from_millis(50)).await;
ctx.shutdown().await;
let result = timeout(Duration::from_millis(200), handle).await;
assert!(result.is_ok(), "Node should respond to shutdown signal");
}
#[test]
fn test_node_types_are_debug() {
let config = SuccessConfig { value: 42 };
let debug_str = format!("{:?}", config);
assert!(debug_str.contains("SuccessConfig"));
}
#[tokio::test]
#[ignore = "Requires Redis"]
async fn test_complete_node_lifecycle() {
let config = HealthyConfig {
initially_healthy: true,
};
let node = Node::<HealthyNode>::from_config(config).await;
assert!(node.is_ok(), "Initialization should succeed");
let node = node.unwrap();
assert_eq!(node.state().await, NodeState::Starting);
assert!(node.is_healthy().await);
let health = node.health_check().await;
assert!(health.healthy);
let ctx = create_test_context().await;
let node_clone = node.clone();
let ctx_clone = ctx.clone();
let handle = tokio::spawn(async move { node_clone.start(ctx_clone).await });
sleep(Duration::from_millis(20)).await;
let state = node.state().await;
assert!(
state == NodeState::Running || state == NodeState::Starting,
"Node should be running or starting"
);
let health = node.health_check().await;
assert!(health.healthy);
ctx.shutdown().await;
let result = timeout(Duration::from_secs(1), handle).await;
assert!(result.is_ok(), "Node should shut down gracefully");
assert_eq!(node.state().await, NodeState::Stopped);
assert!(!node.is_healthy().await);
}
#[tokio::test]
async fn test_rapid_node_creation() {
let mut nodes = Vec::new();
for i in 0..10 {
let config = SuccessConfig { value: i };
let node = Node::<SuccessNode>::from_config(config).await;
assert!(node.is_ok());
nodes.push(node.unwrap());
}
assert_eq!(nodes.len(), 10);
}
#[tokio::test]
async fn test_rapid_health_checks() {
let config = SuccessConfig { value: 42 };
let node = Node::<SuccessNode>::from_config(config).await.unwrap();
for _ in 0..100 {
let health = node.health_check().await;
assert!(health.healthy);
}
}
#[tokio::test]
async fn test_rapid_state_queries() {
let config = SuccessConfig { value: 42 };
let node = Node::<SuccessNode>::from_config(config).await.unwrap();
for _ in 0..100 {
let state = node.state().await;
assert_eq!(state, NodeState::Starting);
}
}