use crate::config::DebtmapConfig;
use crate::env::{AnalysisEnv, RealEnv};
use crate::errors::{errors_to_anyhow, AnalysisError};
use stillwater::effect::prelude::*;
use stillwater::{BoxedEffect, NonEmptyVec, Validation};
pub type AnalysisErrors = NonEmptyVec<AnalysisError>;
pub type AnalysisEffect<T> = BoxedEffect<T, AnalysisError, RealEnv>;
pub type AnalysisValidation<T> = Validation<T, AnalysisErrors>;
pub fn validation_success<T>(value: T) -> AnalysisValidation<T> {
Validation::Success(value)
}
pub fn validation_failure<T>(error: AnalysisError) -> AnalysisValidation<T> {
Validation::Failure(NonEmptyVec::new(error, Vec::new()))
}
pub fn validation_failures<T>(errors: Vec<AnalysisError>) -> AnalysisValidation<T> {
let nev =
NonEmptyVec::from_vec(errors).expect("validation_failures requires at least one error");
Validation::Failure(nev)
}
pub fn effect_pure<T: Send + 'static>(value: T) -> AnalysisEffect<T> {
pure(value).boxed()
}
pub fn effect_fail<T: Send + 'static>(error: AnalysisError) -> AnalysisEffect<T> {
fail(error).boxed()
}
pub fn effect_from_fn<T, F>(f: F) -> AnalysisEffect<T>
where
T: Send + 'static,
F: FnOnce(&RealEnv) -> Result<T, AnalysisError> + Send + 'static,
{
from_fn(f).boxed()
}
pub fn run_effect<T: Send + 'static>(
effect: AnalysisEffect<T>,
config: DebtmapConfig,
) -> anyhow::Result<T> {
let env = RealEnv::new(config);
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|e| anyhow::anyhow!("Failed to create tokio runtime: {}", e))?;
rt.block_on(effect.run(&env)).map_err(Into::into)
}
pub fn run_effect_with_env<T: Send + 'static, E: AnalysisEnv + Sync + 'static>(
effect: BoxedEffect<T, AnalysisError, E>,
env: &E,
) -> anyhow::Result<T> {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|e| anyhow::anyhow!("Failed to create tokio runtime: {}", e))?;
rt.block_on(effect.run(env)).map_err(Into::into)
}
pub async fn run_effect_async<T: Send + 'static>(
effect: AnalysisEffect<T>,
config: DebtmapConfig,
) -> anyhow::Result<T> {
let env = RealEnv::new(config);
effect.run(&env).await.map_err(Into::into)
}
pub fn run_validation<T>(validation: AnalysisValidation<T>) -> anyhow::Result<T> {
match validation {
Validation::Success(value) => Ok(value),
Validation::Failure(errors) => Err(errors_to_anyhow(errors.into_vec())),
}
}
pub fn combine_validations<T>(validations: Vec<AnalysisValidation<T>>) -> AnalysisValidation<Vec<T>>
where
T: Clone,
{
let mut successes = Vec::new();
let mut failures: Vec<AnalysisError> = Vec::new();
for v in validations {
match v {
Validation::Success(value) => successes.push(value),
Validation::Failure(errors) => {
for err in errors {
failures.push(err);
}
}
}
}
if failures.is_empty() {
Validation::Success(successes)
} else {
Validation::Failure(NonEmptyVec::from_vec(failures).expect("failures cannot be empty here"))
}
}
pub fn validation_map<T, U, F>(validation: AnalysisValidation<T>, f: F) -> AnalysisValidation<U>
where
F: FnOnce(T) -> U,
{
match validation {
Validation::Success(value) => Validation::Success(f(value)),
Validation::Failure(errors) => Validation::Failure(errors),
}
}
use crate::config::{EntropyConfig, ScoringWeights, ThresholdsConfig};
use stillwater::Effect;
use std::sync::Arc;
#[derive(Clone)]
pub struct SharedFn<F>(Arc<F>);
impl<F> SharedFn<F> {
fn new(f: F) -> Self {
Self(Arc::new(f))
}
}
pub fn asks_config<U, Env, F>(f: F) -> impl Effect<Output = U, Error = AnalysisError, Env = Env>
where
U: Send + 'static,
Env: AnalysisEnv + Clone + Send + Sync + 'static,
F: Fn(&DebtmapConfig) -> U + Send + Sync + 'static,
{
let shared = SharedFn::new(f);
stillwater::asks(move |env: &Env| (shared.0)(env.config()))
}
pub fn asks_thresholds<U, Env, F>(f: F) -> impl Effect<Output = U, Error = AnalysisError, Env = Env>
where
U: Send + 'static,
Env: AnalysisEnv + Clone + Send + Sync + 'static,
F: Fn(Option<&ThresholdsConfig>) -> U + Send + Sync + 'static,
{
let shared = SharedFn::new(f);
stillwater::asks(move |env: &Env| (shared.0)(env.config().thresholds.as_ref()))
}
pub fn asks_scoring<U, Env, F>(f: F) -> impl Effect<Output = U, Error = AnalysisError, Env = Env>
where
U: Send + 'static,
Env: AnalysisEnv + Clone + Send + Sync + 'static,
F: Fn(Option<&ScoringWeights>) -> U + Send + Sync + 'static,
{
let shared = SharedFn::new(f);
stillwater::asks(move |env: &Env| (shared.0)(env.config().scoring.as_ref()))
}
pub fn asks_entropy<U, Env, F>(f: F) -> impl Effect<Output = U, Error = AnalysisError, Env = Env>
where
U: Send + 'static,
Env: AnalysisEnv + Clone + Send + Sync + 'static,
F: Fn(Option<&EntropyConfig>) -> U + Send + Sync + 'static,
{
let shared = SharedFn::new(f);
stillwater::asks(move |env: &Env| (shared.0)(env.config().entropy.as_ref()))
}
pub fn local_with_config<Inner, F, Env>(
f: F,
inner: Inner,
) -> impl Effect<Output = Inner::Output, Error = Inner::Error, Env = Env>
where
Env: AnalysisEnv + Clone + Send + Sync + 'static,
F: Fn(&DebtmapConfig) -> DebtmapConfig + Send + Sync + 'static,
Inner: Effect<Env = Env>,
{
let shared = SharedFn::new(f);
stillwater::local(
move |env: &Env| {
let new_config = (shared.0)(env.config());
env.clone().with_config(new_config)
},
inner,
)
}
pub fn ask_env<Env>() -> stillwater::effect::reader::Ask<AnalysisError, Env>
where
Env: Clone + Send + Sync + 'static,
{
stillwater::ask::<AnalysisError, Env>()
}
use crate::config::RetryConfig;
use log::{error, info, warn};
use std::time::Instant;
pub fn with_retry<T, F>(effect_factory: F, retry_config: RetryConfig) -> AnalysisEffect<T>
where
T: Send + 'static,
F: Fn() -> AnalysisEffect<T> + Send + Sync + 'static,
{
from_async(move |env: &RealEnv| {
let env = env.clone();
let config = retry_config.clone();
let factory = SharedFn::new(effect_factory);
async move {
let start = Instant::now();
let mut attempt = 0u32;
let mut last_error: Option<AnalysisError> = None;
loop {
let effect = (factory.0)();
match effect.run(&env).await {
Ok(value) => {
if attempt > 0 {
info!("Operation succeeded after {} retry attempt(s)", attempt);
}
return Ok(value);
}
Err(e) => {
let elapsed = start.elapsed();
if e.is_retryable() && config.should_retry(attempt, elapsed) {
attempt += 1;
warn!(
"Retrying operation (attempt {}/{}): {}",
attempt, config.max_retries, e
);
let delay = config.delay_for_attempt(attempt);
tokio::time::sleep(delay).await;
let _ = last_error.insert(e);
} else {
if attempt > 0 {
error!(
"Operation failed after {} retry attempt(s): {}",
attempt, e
);
}
return Err(e);
}
}
}
}
}
})
.boxed()
}
pub fn with_retry_from_env<T, F>(effect_factory: F) -> AnalysisEffect<T>
where
T: Send + 'static,
F: Fn() -> AnalysisEffect<T> + Send + Sync + Clone + 'static,
{
from_async(move |env: &RealEnv| {
let env = env.clone();
let factory = effect_factory.clone();
async move {
let config = env.config().retry.clone().unwrap_or_default();
if !config.enabled {
return factory().run(&env).await;
}
let start = Instant::now();
let mut attempt = 0u32;
loop {
let effect = factory();
match effect.run(&env).await {
Ok(value) => {
if attempt > 0 {
info!("Operation succeeded after {} retry attempt(s)", attempt);
}
return Ok(value);
}
Err(e) => {
let elapsed = start.elapsed();
if e.is_retryable() && config.should_retry(attempt, elapsed) {
attempt += 1;
warn!(
"Retrying operation (attempt {}/{}): {}",
attempt, config.max_retries, e
);
let delay = config.delay_for_attempt(attempt);
tokio::time::sleep(delay).await;
} else {
if attempt > 0 {
error!(
"Operation failed after {} retry attempt(s): {}",
attempt, e
);
}
return Err(e);
}
}
}
}
}
})
.boxed()
}
pub fn is_retry_enabled(config: &DebtmapConfig) -> bool {
config.retry.as_ref().map(|r| r.enabled).unwrap_or(true) }
pub fn get_retry_config(config: &DebtmapConfig) -> RetryConfig {
config.retry.clone().unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validation_success() {
let v: AnalysisValidation<i32> = validation_success(42);
assert!(v.is_success());
match v {
Validation::Success(n) => assert_eq!(n, 42),
Validation::Failure(_) => panic!("Expected success"),
}
}
#[test]
fn test_validation_failure() {
let v: AnalysisValidation<i32> =
validation_failure(AnalysisError::validation("test error"));
assert!(v.is_failure());
}
#[test]
fn test_validation_failures() {
let errors = vec![
AnalysisError::validation("error 1"),
AnalysisError::validation("error 2"),
];
let v: AnalysisValidation<i32> = validation_failures(errors);
assert!(v.is_failure());
match v {
Validation::Failure(nev) => {
let vec: Vec<_> = nev.into_iter().collect();
assert_eq!(vec.len(), 2);
}
_ => panic!("Expected failure"),
}
}
#[test]
fn test_run_validation_success() {
let v = validation_success(42);
let result = run_validation(v);
assert_eq!(result.unwrap(), 42);
}
#[test]
fn test_run_validation_failure() {
let v: AnalysisValidation<i32> =
validation_failure(AnalysisError::validation("test error"));
let result = run_validation(v);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("test error"));
}
#[test]
fn test_combine_validations_all_success() {
let validations = vec![
validation_success(1),
validation_success(2),
validation_success(3),
];
let result = combine_validations(validations);
match result {
Validation::Success(values) => assert_eq!(values, vec![1, 2, 3]),
_ => panic!("Expected success"),
}
}
#[test]
fn test_combine_validations_accumulates_errors() {
let validations = vec![
validation_success(1),
validation_failure(AnalysisError::validation("error 1")),
validation_success(3),
validation_failure(AnalysisError::validation("error 2")),
];
let result: AnalysisValidation<Vec<i32>> = combine_validations(validations);
match result {
Validation::Failure(errors) => {
let vec: Vec<_> = errors.into_iter().collect();
assert_eq!(vec.len(), 2);
}
_ => panic!("Expected failure with accumulated errors"),
}
}
#[test]
fn test_validation_map_success() {
let v = validation_success(21);
let v2 = validation_map(v, |n| n * 2);
match v2 {
Validation::Success(n) => assert_eq!(n, 42),
_ => panic!("Expected success"),
}
}
#[test]
fn test_validation_map_failure() {
let v: AnalysisValidation<i32> = validation_failure(AnalysisError::validation("error"));
let v2: AnalysisValidation<i32> = validation_map(v, |n| n * 2);
assert!(v2.is_failure());
}
#[test]
fn test_effect_pure() {
let effect = effect_pure(42);
let result = run_effect(effect, DebtmapConfig::default());
assert_eq!(result.unwrap(), 42);
}
#[test]
fn test_effect_fail() {
let effect: AnalysisEffect<i32> = effect_fail(AnalysisError::validation("test"));
let result = run_effect(effect, DebtmapConfig::default());
assert!(result.is_err());
}
#[test]
fn test_run_effect() {
let effect = effect_pure(42);
let result = run_effect(effect, DebtmapConfig::default());
assert_eq!(result.unwrap(), 42);
}
#[tokio::test]
async fn test_asks_config_returns_config_value() {
use crate::config::IgnoreConfig;
let config = DebtmapConfig {
ignore: Some(IgnoreConfig {
patterns: vec!["test/**".to_string()],
}),
..Default::default()
};
let env = RealEnv::new(config);
let effect = asks_config::<Vec<String>, RealEnv, _>(|config| config.get_ignore_patterns());
let patterns = effect.run(&env).await.unwrap();
assert_eq!(patterns, vec!["test/**".to_string()]);
}
#[tokio::test]
async fn test_asks_config_with_default_config() {
let env = RealEnv::default();
let effect = asks_config::<Vec<String>, RealEnv, _>(|config| config.get_ignore_patterns());
let patterns = effect.run(&env).await.unwrap();
assert!(patterns.is_empty()); }
#[tokio::test]
async fn test_asks_thresholds_with_thresholds() {
use crate::config::ThresholdsConfig;
let config = DebtmapConfig {
thresholds: Some(ThresholdsConfig {
complexity: Some(15),
max_file_length: Some(500),
..Default::default()
}),
..Default::default()
};
let env = RealEnv::new(config);
let effect = asks_thresholds::<Option<u32>, RealEnv, _>(|thresholds| {
thresholds.and_then(|t| t.complexity)
});
let complexity = effect.run(&env).await.unwrap();
assert_eq!(complexity, Some(15));
}
#[tokio::test]
async fn test_asks_thresholds_without_thresholds() {
let env = RealEnv::default();
let effect = asks_thresholds::<Option<u32>, RealEnv, _>(|thresholds| {
thresholds.and_then(|t| t.complexity)
});
let complexity = effect.run(&env).await.unwrap();
assert_eq!(complexity, None);
}
#[tokio::test]
async fn test_asks_scoring_with_weights() {
let config = DebtmapConfig {
scoring: Some(ScoringWeights {
coverage: 0.6,
complexity: 0.3,
dependency: 0.1,
..Default::default()
}),
..Default::default()
};
let env = RealEnv::new(config);
let effect =
asks_scoring::<f64, RealEnv, _>(|scoring| scoring.map(|s| s.coverage).unwrap_or(0.5));
let coverage = effect.run(&env).await.unwrap();
assert!((coverage - 0.6).abs() < 0.001);
}
#[tokio::test]
async fn test_asks_entropy_enabled() {
let config = DebtmapConfig {
entropy: Some(EntropyConfig {
enabled: false,
..Default::default()
}),
..Default::default()
};
let env = RealEnv::new(config);
let effect =
asks_entropy::<bool, RealEnv, _>(|entropy| entropy.map(|e| e.enabled).unwrap_or(true));
let enabled = effect.run(&env).await.unwrap();
assert!(!enabled);
}
#[tokio::test]
async fn test_local_with_config_modifies_config() {
use crate::config::IgnoreConfig;
let original_config = DebtmapConfig {
ignore: Some(IgnoreConfig {
patterns: vec!["original/**".to_string()],
}),
..Default::default()
};
let env = RealEnv::new(original_config);
let inner = asks_config::<Vec<String>, RealEnv, _>(|config| config.get_ignore_patterns());
let effect = local_with_config(
|_config| DebtmapConfig {
ignore: Some(IgnoreConfig {
patterns: vec!["modified/**".to_string()],
}),
..Default::default()
},
inner,
);
let patterns = effect.run(&env).await.unwrap();
assert_eq!(patterns, vec!["modified/**".to_string()]);
}
#[tokio::test]
async fn test_local_with_config_restores_after() {
use crate::config::IgnoreConfig;
let original_config = DebtmapConfig {
ignore: Some(IgnoreConfig {
patterns: vec!["original/**".to_string()],
}),
..Default::default()
};
let env = RealEnv::new(original_config.clone());
let inner = asks_config::<Vec<String>, RealEnv, _>(|config| config.get_ignore_patterns());
let modified_effect = local_with_config(
|_| DebtmapConfig {
ignore: Some(IgnoreConfig {
patterns: vec!["modified/**".to_string()],
}),
..Default::default()
},
inner,
);
let _ = modified_effect.run(&env).await.unwrap();
let check_effect =
asks_config::<Vec<String>, RealEnv, _>(|config| config.get_ignore_patterns());
let patterns = check_effect.run(&env).await.unwrap();
assert_eq!(patterns, vec!["original/**".to_string()]);
}
#[tokio::test]
async fn test_ask_env_returns_cloned_env() {
let config = DebtmapConfig::default();
let env = RealEnv::new(config);
let effect = ask_env::<RealEnv>();
let cloned_env = effect.run(&env).await.unwrap();
assert_eq!(
format!("{:?}", cloned_env.config()),
format!("{:?}", env.config())
);
}
#[tokio::test]
async fn test_reader_pattern_composition() {
use stillwater::EffectExt;
let config = DebtmapConfig {
scoring: Some(ScoringWeights {
coverage: 0.6,
complexity: 0.4,
dependency: 0.0,
..Default::default()
}),
..Default::default()
};
let env = RealEnv::new(config);
let coverage_effect =
asks_scoring::<f64, RealEnv, _>(|scoring| scoring.map(|s| s.coverage).unwrap_or(0.5));
let complexity_effect = asks_scoring::<f64, RealEnv, _>(|scoring| {
scoring.map(|s| s.complexity).unwrap_or(0.35)
});
let combined =
coverage_effect.and_then(move |cov| complexity_effect.map(move |comp| cov + comp));
let sum = combined.run(&env).await.unwrap();
assert!((sum - 1.0).abs() < 0.001); }
use std::sync::atomic::{AtomicUsize, Ordering};
#[tokio::test]
async fn test_with_retry_succeeds_first_attempt() {
let config = RetryConfig::default();
let env = RealEnv::default();
let effect = with_retry(|| effect_pure(42), config);
let result = effect.run(&env).await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), 42);
}
#[tokio::test]
async fn test_with_retry_succeeds_after_transient_failure() {
let config = RetryConfig {
max_retries: 3,
base_delay_ms: 10, jitter_factor: 0.0,
..Default::default()
};
let env = RealEnv::default();
let attempt_count = Arc::new(AtomicUsize::new(0));
let attempt_clone = attempt_count.clone();
let effect = with_retry(
move || {
let count = attempt_clone.fetch_add(1, Ordering::SeqCst);
if count < 2 {
effect_fail(AnalysisError::io("Resource busy"))
} else {
effect_pure("success".to_string())
}
},
config,
);
let result = effect.run(&env).await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), "success");
assert_eq!(attempt_count.load(Ordering::SeqCst), 3);
}
#[tokio::test]
async fn test_with_retry_fails_on_permanent_error() {
let config = RetryConfig {
max_retries: 3,
base_delay_ms: 10,
jitter_factor: 0.0,
..Default::default()
};
let env = RealEnv::default();
let attempt_count = Arc::new(AtomicUsize::new(0));
let attempt_clone = attempt_count.clone();
let effect: AnalysisEffect<String> = with_retry(
move || {
attempt_clone.fetch_add(1, Ordering::SeqCst);
effect_fail(AnalysisError::parse("Syntax error"))
},
config,
);
let result = effect.run(&env).await;
assert!(result.is_err());
assert_eq!(attempt_count.load(Ordering::SeqCst), 1);
}
#[tokio::test]
async fn test_with_retry_exhausts_retries() {
let config = RetryConfig {
max_retries: 2,
base_delay_ms: 10,
jitter_factor: 0.0,
..Default::default()
};
let env = RealEnv::default();
let attempt_count = Arc::new(AtomicUsize::new(0));
let attempt_clone = attempt_count.clone();
let effect: AnalysisEffect<String> = with_retry(
move || {
attempt_clone.fetch_add(1, Ordering::SeqCst);
effect_fail(AnalysisError::io("Resource busy"))
},
config,
);
let result = effect.run(&env).await;
assert!(result.is_err());
assert_eq!(attempt_count.load(Ordering::SeqCst), 3);
}
#[tokio::test]
async fn test_with_retry_disabled() {
let config = RetryConfig::disabled();
let env = RealEnv::default();
let attempt_count = Arc::new(AtomicUsize::new(0));
let attempt_clone = attempt_count.clone();
let effect: AnalysisEffect<String> = with_retry(
move || {
attempt_clone.fetch_add(1, Ordering::SeqCst);
effect_fail(AnalysisError::io("Resource busy"))
},
config,
);
let result = effect.run(&env).await;
assert!(result.is_err());
assert_eq!(attempt_count.load(Ordering::SeqCst), 1);
}
#[tokio::test]
async fn test_with_retry_from_env_uses_config() {
let config = DebtmapConfig {
retry: Some(RetryConfig {
enabled: true,
max_retries: 2,
base_delay_ms: 10,
jitter_factor: 0.0,
..Default::default()
}),
..Default::default()
};
let env = RealEnv::new(config);
let attempt_count = Arc::new(AtomicUsize::new(0));
let attempt_clone = attempt_count.clone();
let factory = move || {
let count = attempt_clone.fetch_add(1, Ordering::SeqCst);
if count < 1 {
effect_fail(AnalysisError::io("Resource busy"))
} else {
effect_pure("success".to_string())
}
};
let effect = with_retry_from_env(factory);
let result = effect.run(&env).await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), "success");
assert_eq!(attempt_count.load(Ordering::SeqCst), 2);
}
#[tokio::test]
async fn test_with_retry_from_env_disabled() {
let config = DebtmapConfig {
retry: Some(RetryConfig::disabled()),
..Default::default()
};
let env = RealEnv::new(config);
let attempt_count = Arc::new(AtomicUsize::new(0));
let attempt_clone = attempt_count.clone();
let factory = move || {
attempt_clone.fetch_add(1, Ordering::SeqCst);
effect_fail::<String>(AnalysisError::io("Resource busy"))
};
let effect = with_retry_from_env(factory);
let result = effect.run(&env).await;
assert!(result.is_err());
assert_eq!(attempt_count.load(Ordering::SeqCst), 1);
}
#[test]
fn test_is_retry_enabled_default() {
let config = DebtmapConfig::default();
assert!(is_retry_enabled(&config));
}
#[test]
fn test_is_retry_enabled_explicit_true() {
let config = DebtmapConfig {
retry: Some(RetryConfig {
enabled: true,
..Default::default()
}),
..Default::default()
};
assert!(is_retry_enabled(&config));
}
#[test]
fn test_is_retry_enabled_explicit_false() {
let config = DebtmapConfig {
retry: Some(RetryConfig::disabled()),
..Default::default()
};
assert!(!is_retry_enabled(&config));
}
#[test]
fn test_get_retry_config_default() {
let config = DebtmapConfig::default();
let retry_config = get_retry_config(&config);
assert!(retry_config.enabled);
assert_eq!(retry_config.max_retries, 3);
}
#[test]
fn test_get_retry_config_custom() {
let config = DebtmapConfig {
retry: Some(RetryConfig {
enabled: true,
max_retries: 5,
base_delay_ms: 200,
..Default::default()
}),
..Default::default()
};
let retry_config = get_retry_config(&config);
assert!(retry_config.enabled);
assert_eq!(retry_config.max_retries, 5);
assert_eq!(retry_config.base_delay_ms, 200);
}
#[tokio::test]
async fn test_with_retry_stops_on_timeout() {
let config = RetryConfig {
max_retries: 100,
base_delay_ms: 50,
timeout_seconds: 0,
jitter_factor: 0.0,
..Default::default()
};
let env = RealEnv::default();
let attempt_count = Arc::new(AtomicUsize::new(0));
let attempt_clone = attempt_count.clone();
let effect: AnalysisEffect<String> = with_retry(
move || {
attempt_clone.fetch_add(1, Ordering::SeqCst);
effect_fail(AnalysisError::io("Resource busy"))
},
config,
);
let result = effect.run(&env).await;
assert!(result.is_err());
assert_eq!(attempt_count.load(Ordering::SeqCst), 1);
}
#[tokio::test]
async fn test_with_retry_with_constant_strategy() {
use crate::config::retry::RetryStrategy;
let config = RetryConfig {
max_retries: 3,
base_delay_ms: 5,
strategy: RetryStrategy::Constant,
jitter_factor: 0.0,
..Default::default()
};
let env = RealEnv::default();
let attempt_count = Arc::new(AtomicUsize::new(0));
let attempt_clone = attempt_count.clone();
let effect = with_retry(
move || {
let count = attempt_clone.fetch_add(1, Ordering::SeqCst);
if count < 2 {
effect_fail(AnalysisError::io("Resource busy"))
} else {
effect_pure("success with constant strategy".to_string())
}
},
config,
);
let result = effect.run(&env).await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), "success with constant strategy");
assert_eq!(attempt_count.load(Ordering::SeqCst), 3);
}
#[tokio::test]
async fn test_with_retry_with_linear_strategy() {
use crate::config::retry::RetryStrategy;
let config = RetryConfig {
max_retries: 3,
base_delay_ms: 5,
strategy: RetryStrategy::Linear,
jitter_factor: 0.0,
..Default::default()
};
let env = RealEnv::default();
let attempt_count = Arc::new(AtomicUsize::new(0));
let attempt_clone = attempt_count.clone();
let effect = with_retry(
move || {
let count = attempt_clone.fetch_add(1, Ordering::SeqCst);
if count < 1 {
effect_fail(AnalysisError::io("Resource busy"))
} else {
effect_pure("success with linear strategy".to_string())
}
},
config,
);
let result = effect.run(&env).await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), "success with linear strategy");
assert_eq!(attempt_count.load(Ordering::SeqCst), 2);
}
#[tokio::test]
async fn test_with_retry_with_fibonacci_strategy() {
use crate::config::retry::RetryStrategy;
let config = RetryConfig {
max_retries: 3,
base_delay_ms: 5,
strategy: RetryStrategy::Fibonacci,
jitter_factor: 0.0,
..Default::default()
};
let env = RealEnv::default();
let attempt_count = Arc::new(AtomicUsize::new(0));
let attempt_clone = attempt_count.clone();
let effect = with_retry(
move || {
let count = attempt_clone.fetch_add(1, Ordering::SeqCst);
if count < 1 {
effect_fail(AnalysisError::io("Resource busy"))
} else {
effect_pure("success with fibonacci strategy".to_string())
}
},
config,
);
let result = effect.run(&env).await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), "success with fibonacci strategy");
assert_eq!(attempt_count.load(Ordering::SeqCst), 2);
}
#[tokio::test]
async fn test_with_retry_with_jitter() {
let config = RetryConfig {
max_retries: 3,
base_delay_ms: 5,
jitter_factor: 0.5,
..Default::default()
};
let env = RealEnv::default();
let attempt_count = Arc::new(AtomicUsize::new(0));
let attempt_clone = attempt_count.clone();
let effect = with_retry(
move || {
let count = attempt_clone.fetch_add(1, Ordering::SeqCst);
if count < 1 {
effect_fail(AnalysisError::io("Resource busy"))
} else {
effect_pure("success with jitter".to_string())
}
},
config,
);
let result = effect.run(&env).await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), "success with jitter");
assert_eq!(attempt_count.load(Ordering::SeqCst), 2);
}
}