use crate::lighting::effects::*;
use crate::lighting::engine::tests::common::create_test_fixture;
use crate::lighting::engine::EffectEngine;
use std::collections::HashMap;
use std::time::Duration;
#[test]
fn test_clear_layer() {
let mut engine = EffectEngine::new();
let fixture = create_test_fixture("test_fixture", 1, 1);
engine.register_fixture(fixture);
let bg_effect = EffectInstance::new(
"bg_effect".to_string(),
EffectType::Static {
parameters: {
let mut p = HashMap::new();
p.insert("dimmer".to_string(), 0.5);
p
},
duration: Duration::from_secs(5),
},
vec!["test_fixture".to_string()],
None,
None,
None,
);
let mut fg_effect = EffectInstance::new(
"fg_effect".to_string(),
EffectType::Static {
parameters: {
let mut p = HashMap::new();
p.insert("dimmer".to_string(), 1.0);
p
},
duration: Duration::from_secs(5),
},
vec!["test_fixture".to_string()],
None,
None,
None,
);
fg_effect.layer = EffectLayer::Foreground;
engine.start_effect(bg_effect).unwrap();
engine.start_effect(fg_effect).unwrap();
assert_eq!(engine.active_effects_count(), 2);
engine.clear_layer(EffectLayer::Foreground);
assert_eq!(engine.active_effects_count(), 1);
assert!(engine.has_effect("bg_effect"));
assert!(!engine.has_effect("fg_effect"));
}
#[test]
fn test_freeze_unfreeze_layer() {
let mut engine = EffectEngine::new();
let mut channels = HashMap::new();
channels.insert("red".to_string(), 1);
channels.insert("green".to_string(), 2);
channels.insert("blue".to_string(), 3);
let fixture = FixtureInfo::new(
"rgb_fixture".to_string(),
1,
1,
"RGB".to_string(),
channels,
None,
);
engine.register_fixture(fixture);
let effect = EffectInstance::new(
"bg_effect".to_string(),
EffectType::Rainbow {
speed: TempoAwareSpeed::Fixed(1.0), saturation: 1.0,
brightness: 1.0,
duration: Duration::from_secs(10),
},
vec!["rgb_fixture".to_string()],
None,
None,
None,
);
engine.start_effect(effect).unwrap();
let _commands1 = engine.update(Duration::from_millis(250), None).unwrap();
let commands_before_freeze = engine.update(Duration::from_millis(10), None).unwrap();
assert!(!commands_before_freeze.is_empty());
engine.freeze_layer(EffectLayer::Background);
assert!(engine.is_layer_frozen(EffectLayer::Background));
let commands_frozen1 = engine
.update(Duration::from_millis(100), None)
.unwrap()
.to_vec();
let commands_frozen2 = engine
.update(Duration::from_millis(100), None)
.unwrap()
.to_vec();
let commands_frozen3 = engine
.update(Duration::from_millis(500), None)
.unwrap()
.to_vec();
assert!(!commands_frozen1.is_empty());
assert!(!commands_frozen2.is_empty());
assert!(!commands_frozen3.is_empty());
let mut vals1: Vec<u8> = commands_frozen1.iter().map(|c| c.value).collect();
let mut vals2: Vec<u8> = commands_frozen2.iter().map(|c| c.value).collect();
let mut vals3: Vec<u8> = commands_frozen3.iter().map(|c| c.value).collect();
vals1.sort();
vals2.sort();
vals3.sort();
assert_eq!(
vals1, vals2,
"Frozen layer should produce same values: {:?} vs {:?}",
vals1, vals2
);
assert_eq!(
vals2, vals3,
"Frozen layer should produce same values: {:?} vs {:?}",
vals2, vals3
);
engine.unfreeze_layer(EffectLayer::Background);
assert!(!engine.is_layer_frozen(EffectLayer::Background));
let commands_after1 = engine
.update(Duration::from_millis(100), None)
.unwrap()
.to_vec();
let commands_after2 = engine
.update(Duration::from_millis(200), None)
.unwrap()
.to_vec();
assert!(!commands_after1.is_empty());
assert!(!commands_after2.is_empty());
let mut vals_after1: Vec<u8> = commands_after1.iter().map(|c| c.value).collect();
let mut vals_after2: Vec<u8> = commands_after2.iter().map(|c| c.value).collect();
vals_after1.sort();
vals_after2.sort();
assert_ne!(
vals_after1, vals_after2,
"After unfreezing, effect should animate: {:?} vs {:?}",
vals_after1, vals_after2
);
}
#[test]
fn test_release_frozen_layer_maintains_animation_continuity() {
let mut engine = EffectEngine::new();
let mut channels = HashMap::new();
channels.insert("red".to_string(), 1);
channels.insert("green".to_string(), 2);
channels.insert("blue".to_string(), 3);
let fixture = FixtureInfo::new(
"rgb_fixture".to_string(),
1,
1,
"RGB".to_string(),
channels,
None,
);
engine.register_fixture(fixture);
let effect = EffectInstance::new(
"rainbow".to_string(),
EffectType::Rainbow {
speed: TempoAwareSpeed::Fixed(1.0), saturation: 1.0,
brightness: 1.0,
duration: Duration::from_secs(10),
},
vec!["rgb_fixture".to_string()],
None,
None,
None,
);
engine.start_effect(effect).unwrap();
engine.update(Duration::from_millis(250), None).unwrap();
let _commands_before_freeze = engine.update(Duration::from_millis(10), None).unwrap();
engine.freeze_layer(EffectLayer::Background);
engine.update(Duration::from_millis(500), None).unwrap();
engine.update(Duration::from_millis(500), None).unwrap();
let commands_frozen = engine.update(Duration::from_millis(10), None).unwrap();
let mut frozen_sorted: Vec<_> = commands_frozen
.iter()
.map(|c| (c.channel, c.value))
.collect();
frozen_sorted.sort_by_key(|(ch, _)| *ch);
let vals_frozen: Vec<u8> = frozen_sorted.iter().map(|(_, v)| *v).collect();
engine.release_layer_with_time(EffectLayer::Background, Some(Duration::from_secs(2)));
let commands_after_release = engine.update(Duration::from_millis(10), None).unwrap();
let mut after_release_sorted: Vec<_> = commands_after_release
.iter()
.map(|c| (c.channel, c.value))
.collect();
after_release_sorted.sort_by_key(|(ch, _)| *ch);
let vals_after_release: Vec<u8> = after_release_sorted.iter().map(|(_, v)| *v).collect();
let max_diff: i16 = vals_frozen
.iter()
.zip(vals_after_release.iter())
.map(|(a, b)| (*a as i16 - *b as i16).abs())
.max()
.unwrap_or(0);
assert!(
max_diff < 30,
"Release of frozen layer caused animation discontinuity! \
Frozen: {:?}, After release: {:?}, Max diff: {}. \
Effect should continue from frozen state, not jump forward.",
vals_frozen,
vals_after_release,
max_diff
);
engine.update(Duration::from_millis(1000), None).unwrap();
let commands_mid_fade = engine.update(Duration::from_millis(10), None).unwrap();
let mut mid_fade_sorted: Vec<_> = commands_mid_fade
.iter()
.map(|c| (c.channel, c.value))
.collect();
mid_fade_sorted.sort_by_key(|(ch, _)| *ch);
let vals_mid_fade: Vec<u8> = mid_fade_sorted.iter().map(|(_, v)| *v).collect();
let avg_mid: f64 =
vals_mid_fade.iter().map(|v| *v as f64).sum::<f64>() / vals_mid_fade.len() as f64;
let avg_frozen: f64 =
vals_frozen.iter().map(|v| *v as f64).sum::<f64>() / vals_frozen.len() as f64;
assert!(
avg_mid < avg_frozen * 0.8,
"Effect should be fading: frozen avg={:.1}, mid-fade avg={:.1}",
avg_frozen,
avg_mid
);
}
#[test]
fn test_layer_intensity_master() {
let mut engine = EffectEngine::new();
let fixture = create_test_fixture("test_fixture", 1, 1);
engine.register_fixture(fixture);
let effect = EffectInstance::new(
"test_effect".to_string(),
EffectType::Static {
parameters: {
let mut p = HashMap::new();
p.insert("dimmer".to_string(), 1.0);
p
},
duration: Duration::from_secs(5),
},
vec!["test_fixture".to_string()],
None,
None,
None,
);
engine.start_effect(effect).unwrap();
let commands_full = engine.update(Duration::from_millis(16), None).unwrap();
assert_eq!(commands_full.len(), 1);
let full_value = commands_full[0].value;
assert_eq!(full_value, 255);
engine.set_layer_intensity_master(EffectLayer::Background, 0.5);
assert!((engine.get_layer_intensity_master(EffectLayer::Background) - 0.5).abs() < 0.01);
let commands_half = engine.update(Duration::from_millis(16), None).unwrap();
assert_eq!(commands_half.len(), 1);
let half_value = commands_half[0].value;
assert_eq!(half_value, 127); }
#[test]
fn test_layer_speed_master() {
let mut engine = EffectEngine::new();
let fixture = create_test_fixture("test_fixture", 1, 1);
engine.register_fixture(fixture);
engine.set_layer_speed_master(EffectLayer::Background, 2.0);
assert!((engine.get_layer_speed_master(EffectLayer::Background) - 2.0).abs() < 0.01);
engine.set_layer_speed_master(EffectLayer::Background, 0.5);
assert!((engine.get_layer_speed_master(EffectLayer::Background) - 0.5).abs() < 0.01);
engine.set_layer_speed_master(EffectLayer::Background, 1.0);
assert!((engine.get_layer_speed_master(EffectLayer::Background) - 1.0).abs() < 0.01);
}
#[test]
fn test_release_layer_fade_behavior() {
let mut engine = EffectEngine::new();
let fixture = create_test_fixture("test_fixture", 1, 1);
engine.register_fixture(fixture);
let effect = EffectInstance::new(
"bg_effect".to_string(),
EffectType::Static {
parameters: {
let mut p = HashMap::new();
p.insert("dimmer".to_string(), 1.0);
p
},
duration: Duration::from_secs(5),
},
vec!["test_fixture".to_string()],
None,
None,
None,
);
engine.start_effect(effect).unwrap();
let commands_before = engine.update(Duration::from_millis(16), None).unwrap();
assert_eq!(commands_before.len(), 1);
assert_eq!(commands_before[0].value, 255);
engine.release_layer_with_time(EffectLayer::Background, Some(Duration::from_secs(1)));
let commands_start = engine.update(Duration::from_millis(16), None).unwrap();
assert!(!commands_start.is_empty());
let commands_mid = engine.update(Duration::from_millis(500), None).unwrap();
if !commands_mid.is_empty() {
assert!(
commands_mid[0].value < 200,
"Should be fading: {}",
commands_mid[0].value
);
}
}
#[test]
fn test_layer_commands_edge_cases() {
let mut engine = EffectEngine::new();
let fixture = create_test_fixture("test_fixture", 1, 1);
engine.register_fixture(fixture);
engine.clear_layer(EffectLayer::Foreground);
assert_eq!(engine.active_effects_count(), 0);
engine.release_layer(EffectLayer::Midground);
engine.freeze_layer(EffectLayer::Background);
engine.freeze_layer(EffectLayer::Background);
assert!(engine.is_layer_frozen(EffectLayer::Background));
engine.unfreeze_layer(EffectLayer::Foreground);
engine.set_layer_intensity_master(EffectLayer::Background, 0.5);
engine.set_layer_intensity_master(EffectLayer::Background, 0.75);
assert!((engine.get_layer_intensity_master(EffectLayer::Background) - 0.75).abs() < 0.01);
engine.set_layer_intensity_master(EffectLayer::Background, 1.5); assert!((engine.get_layer_intensity_master(EffectLayer::Background) - 1.0).abs() < 0.01);
engine.set_layer_intensity_master(EffectLayer::Background, -0.5); assert!((engine.get_layer_intensity_master(EffectLayer::Background) - 0.0).abs() < 0.01);
}
#[test]
fn test_speed_master_affects_effect_progression() {
let mut engine = EffectEngine::new();
let fixture = create_test_fixture("test_fixture", 1, 1);
engine.register_fixture(fixture);
let effect = EffectInstance::new(
"pulse".to_string(),
EffectType::Pulse {
base_level: 0.5,
pulse_amplitude: 0.5,
frequency: TempoAwareFrequency::Fixed(1.0), duration: Duration::from_secs(5),
},
vec!["test_fixture".to_string()],
None,
None,
None,
);
engine.start_effect(effect).unwrap();
let cmd1 = engine.update(Duration::from_millis(100), None).unwrap();
assert!(!cmd1.is_empty());
let _initial_value = cmd1[0].value;
engine.set_layer_speed_master(EffectLayer::Background, 0.0);
let cmd2 = engine
.update(Duration::from_millis(500), None)
.unwrap()
.to_vec();
let cmd3 = engine
.update(Duration::from_millis(500), None)
.unwrap()
.to_vec();
assert!(!cmd2.is_empty());
assert!(!cmd3.is_empty());
let val2 = cmd2[0].value;
let val3 = cmd3[0].value;
assert_eq!(
val2, val3,
"Speed=0 should produce consistent values: {} vs {}",
val2, val3
);
}
#[test]
fn test_speed_master_resume_from_zero() {
let mut engine = EffectEngine::new();
let fixture = create_test_fixture("test_fixture", 1, 1);
engine.register_fixture(fixture);
let effect = EffectInstance::new(
"pulse".to_string(),
EffectType::Pulse {
base_level: 0.5,
pulse_amplitude: 0.5,
frequency: TempoAwareFrequency::Fixed(1.0),
duration: Duration::from_secs(5),
},
vec!["test_fixture".to_string()],
None,
None,
None,
);
engine.start_effect(effect).unwrap();
engine.update(Duration::from_millis(250), None).unwrap();
engine.set_layer_speed_master(EffectLayer::Background, 0.0);
let frozen_cmd = engine.update(Duration::from_millis(100), None).unwrap();
let frozen_val = frozen_cmd[0].value;
engine.update(Duration::from_millis(500), None).unwrap();
engine.set_layer_speed_master(EffectLayer::Background, 1.0);
let resume_cmd1 = engine
.update(Duration::from_millis(100), None)
.unwrap()
.to_vec();
let resume_cmd2 = engine
.update(Duration::from_millis(100), None)
.unwrap()
.to_vec();
let val1 = resume_cmd1[0].value;
let val2 = resume_cmd2[0].value;
assert!(!resume_cmd1.is_empty());
assert!(!resume_cmd2.is_empty());
let changed = frozen_val != val1 || frozen_val != val2 || val1 != val2;
assert!(
changed,
"Effect should progress after resume: frozen={}, val1={}, val2={}",
frozen_val, val1, val2
);
}
#[test]
fn test_multiple_layers_independent() {
let mut engine = EffectEngine::new();
let fixture = create_test_fixture("test_fixture", 1, 1);
engine.register_fixture(fixture);
let mut bg_effect = EffectInstance::new(
"bg".to_string(),
EffectType::Static {
parameters: {
let mut p = HashMap::new();
p.insert("dimmer".to_string(), 1.0);
p
},
duration: Duration::from_secs(5),
},
vec!["test_fixture".to_string()],
None,
None,
None,
);
bg_effect.layer = EffectLayer::Background;
let mut mid_effect = EffectInstance::new(
"mid".to_string(),
EffectType::Static {
parameters: {
let mut p = HashMap::new();
p.insert("dimmer".to_string(), 0.8);
p
},
duration: Duration::from_secs(5),
},
vec!["test_fixture".to_string()],
None,
None,
None,
);
mid_effect.layer = EffectLayer::Midground;
engine.start_effect(bg_effect).unwrap();
engine.start_effect(mid_effect).unwrap();
engine.set_layer_intensity_master(EffectLayer::Background, 0.5);
engine.set_layer_intensity_master(EffectLayer::Midground, 1.0);
engine.freeze_layer(EffectLayer::Background);
assert!(engine.is_layer_frozen(EffectLayer::Background));
assert!(!engine.is_layer_frozen(EffectLayer::Midground));
engine.clear_layer(EffectLayer::Midground);
assert_eq!(engine.active_effects_count(), 1);
assert!(engine.has_effect("bg"));
assert!(!engine.has_effect("mid"));
}