use super::common::*;
#[cfg(test)]
use crate::lighting::effects::*;
use crate::lighting::engine::EffectEngine;
use std::collections::HashMap;
use std::time::Duration;
#[test]
fn test_chase_pattern_linear_left_to_right() {
let mut engine = EffectEngine::new();
for i in 1..=4 {
let mut channels = HashMap::new();
channels.insert("dimmer".to_string(), 1);
channels.insert("red".to_string(), 2);
channels.insert("green".to_string(), 3);
channels.insert("blue".to_string(), 4);
let fixture = FixtureInfo::new(
format!("fixture_{}", i),
1,
(i - 1) * 4 + 1,
"RGB_Par".to_string(),
channels,
Some(20.0),
);
engine.register_fixture(fixture);
}
let chase_effect = create_effect_with_layering(
"chase_linear_ltr".to_string(),
EffectType::Chase {
pattern: ChasePattern::Linear,
speed: TempoAwareSpeed::Fixed(2.0), direction: ChaseDirection::LeftToRight,
transition: CycleTransition::Snap,
duration: Duration::from_secs(10),
},
vec![
"fixture_1".to_string(),
"fixture_2".to_string(),
"fixture_3".to_string(),
"fixture_4".to_string(),
],
EffectLayer::Background,
BlendMode::Replace,
);
engine.start_effect(chase_effect).unwrap();
let commands = engine.update(Duration::from_millis(0), None).unwrap();
let fixture1_cmd = commands.iter().find(|cmd| cmd.channel == 1).unwrap();
assert_eq!(fixture1_cmd.value, 255);
let commands = engine.update(Duration::from_millis(125), None).unwrap();
let fixture2_cmd = commands.iter().find(|cmd| cmd.channel == 5).unwrap(); assert_eq!(fixture2_cmd.value, 255);
let commands = engine.update(Duration::from_millis(125), None).unwrap(); let fixture3_cmd = commands.iter().find(|cmd| cmd.channel == 9).unwrap(); assert_eq!(fixture3_cmd.value, 255);
let commands = engine.update(Duration::from_millis(125), None).unwrap(); let fixture4_cmd = commands.iter().find(|cmd| cmd.channel == 13).unwrap(); assert_eq!(fixture4_cmd.value, 255);
let commands = engine.update(Duration::from_millis(125), None).unwrap(); let fixture1_cmd = commands.iter().find(|cmd| cmd.channel == 1).unwrap();
assert_eq!(fixture1_cmd.value, 255); }
#[test]
fn test_chase_pattern_linear_right_to_left() {
let mut engine = EffectEngine::new();
for i in 1..=4 {
let mut channels = HashMap::new();
channels.insert("dimmer".to_string(), 1);
channels.insert("red".to_string(), 2);
channels.insert("green".to_string(), 3);
channels.insert("blue".to_string(), 4);
let fixture = FixtureInfo::new(
format!("fixture_{}", i),
1,
(i - 1) * 4 + 1,
"RGB_Par".to_string(),
channels,
Some(20.0),
);
engine.register_fixture(fixture);
}
let chase_effect = create_effect_with_layering(
"chase_linear_rtl".to_string(),
EffectType::Chase {
pattern: ChasePattern::Linear,
speed: TempoAwareSpeed::Fixed(2.0), direction: ChaseDirection::RightToLeft,
transition: CycleTransition::Snap,
duration: Duration::from_secs(10),
},
vec![
"fixture_1".to_string(),
"fixture_2".to_string(),
"fixture_3".to_string(),
"fixture_4".to_string(),
],
EffectLayer::Background,
BlendMode::Replace,
);
engine.start_effect(chase_effect).unwrap();
let commands = engine.update(Duration::from_millis(0), None).unwrap();
let fixture4_cmd = commands.iter().find(|cmd| cmd.channel == 13).unwrap(); assert_eq!(fixture4_cmd.value, 255);
let commands = engine.update(Duration::from_millis(125), None).unwrap();
let fixture3_cmd = commands.iter().find(|cmd| cmd.channel == 9).unwrap(); assert_eq!(fixture3_cmd.value, 255);
let commands = engine.update(Duration::from_millis(125), None).unwrap(); let fixture2_cmd = commands.iter().find(|cmd| cmd.channel == 5).unwrap(); assert_eq!(fixture2_cmd.value, 255);
let commands = engine.update(Duration::from_millis(125), None).unwrap(); let fixture1_cmd = commands.iter().find(|cmd| cmd.channel == 1).unwrap();
assert_eq!(fixture1_cmd.value, 255); }
#[test]
fn test_chase_pattern_snake() {
let mut engine = EffectEngine::new();
for i in 1..=4 {
let mut channels = HashMap::new();
channels.insert("dimmer".to_string(), 1);
channels.insert("red".to_string(), 2);
channels.insert("green".to_string(), 3);
channels.insert("blue".to_string(), 4);
let fixture = FixtureInfo::new(
format!("fixture_{}", i),
1,
(i - 1) * 4 + 1,
"RGB_Par".to_string(),
channels,
Some(20.0),
);
engine.register_fixture(fixture);
}
let chase_effect = create_effect_with_layering(
"chase_snake".to_string(),
EffectType::Chase {
pattern: ChasePattern::Snake,
speed: TempoAwareSpeed::Fixed(2.0), direction: ChaseDirection::LeftToRight,
transition: CycleTransition::Snap,
duration: Duration::from_secs(10),
},
vec![
"fixture_1".to_string(),
"fixture_2".to_string(),
"fixture_3".to_string(),
"fixture_4".to_string(),
],
EffectLayer::Background,
BlendMode::Replace,
);
engine.start_effect(chase_effect).unwrap();
let commands = engine.update(Duration::from_millis(0), None).unwrap();
let fixture1_cmd = commands.iter().find(|cmd| cmd.channel == 1).unwrap();
assert_eq!(fixture1_cmd.value, 255);
let commands = engine.update(Duration::from_millis(125), None).unwrap();
let fixture2_cmd = commands.iter().find(|cmd| cmd.channel == 5).unwrap();
assert_eq!(fixture2_cmd.value, 255);
let commands = engine.update(Duration::from_millis(125), None).unwrap(); let fixture3_cmd = commands.iter().find(|cmd| cmd.channel == 9).unwrap();
assert_eq!(fixture3_cmd.value, 255);
let commands = engine.update(Duration::from_millis(125), None).unwrap(); let fixture4_cmd = commands.iter().find(|cmd| cmd.channel == 13).unwrap();
assert_eq!(fixture4_cmd.value, 255);
let commands = engine.update(Duration::from_millis(125), None).unwrap(); let fixture3_cmd = commands.iter().find(|cmd| cmd.channel == 9).unwrap();
assert_eq!(fixture3_cmd.value, 255);
let commands = engine.update(Duration::from_millis(125), None).unwrap(); let fixture2_cmd = commands.iter().find(|cmd| cmd.channel == 5).unwrap();
assert_eq!(fixture2_cmd.value, 255);
let commands = engine.update(Duration::from_millis(125), None).unwrap(); let fixture1_cmd = commands.iter().find(|cmd| cmd.channel == 1).unwrap();
assert_eq!(fixture1_cmd.value, 255); }
#[test]
fn test_chase_pattern_random() {
let mut engine = EffectEngine::new();
for i in 1..=4 {
let mut channels = HashMap::new();
channels.insert("dimmer".to_string(), 1);
channels.insert("red".to_string(), 2);
channels.insert("green".to_string(), 3);
channels.insert("blue".to_string(), 4);
let fixture = FixtureInfo::new(
format!("fixture_{}", i),
1,
(i - 1) * 4 + 1,
"RGB_Par".to_string(),
channels,
Some(20.0),
);
engine.register_fixture(fixture);
}
let chase_effect = create_effect_with_layering(
"chase_random".to_string(),
EffectType::Chase {
pattern: ChasePattern::Random,
speed: TempoAwareSpeed::Fixed(2.0), direction: ChaseDirection::LeftToRight, transition: CycleTransition::Snap,
duration: Duration::from_secs(10),
},
vec![
"fixture_1".to_string(),
"fixture_2".to_string(),
"fixture_3".to_string(),
"fixture_4".to_string(),
],
EffectLayer::Background,
BlendMode::Replace,
);
engine.start_effect(chase_effect).unwrap();
let commands = engine.update(Duration::from_millis(0), None).unwrap();
let on_fixtures: Vec<_> = commands.iter().filter(|cmd| cmd.value == 255).collect();
assert_eq!(on_fixtures.len(), 1);
let commands = engine.update(Duration::from_millis(125), None).unwrap();
let on_fixtures: Vec<_> = commands.iter().filter(|cmd| cmd.value == 255).collect();
assert_eq!(on_fixtures.len(), 1);
let commands = engine.update(Duration::from_millis(125), None).unwrap(); let on_fixtures: Vec<_> = commands.iter().filter(|cmd| cmd.value == 255).collect();
assert_eq!(on_fixtures.len(), 1); }
#[test]
fn test_chase_direction_vertical() {
let mut engine = EffectEngine::new();
for i in 1..=4 {
let mut channels = HashMap::new();
channels.insert("dimmer".to_string(), 1);
channels.insert("red".to_string(), 2);
channels.insert("green".to_string(), 3);
channels.insert("blue".to_string(), 4);
let fixture = FixtureInfo::new(
format!("fixture_{}", i),
1,
(i - 1) * 4 + 1,
"RGB_Par".to_string(),
channels,
Some(20.0),
);
engine.register_fixture(fixture);
}
let chase_effect = create_effect_with_layering(
"chase_ttb".to_string(),
EffectType::Chase {
pattern: ChasePattern::Linear,
speed: TempoAwareSpeed::Fixed(2.0),
direction: ChaseDirection::TopToBottom,
transition: CycleTransition::Snap,
duration: Duration::from_secs(10),
},
vec![
"fixture_1".to_string(),
"fixture_2".to_string(),
"fixture_3".to_string(),
"fixture_4".to_string(),
],
EffectLayer::Background,
BlendMode::Replace,
);
engine.start_effect(chase_effect).unwrap();
let commands = engine.update(Duration::from_millis(0), None).unwrap();
let fixture1_cmd = commands.iter().find(|cmd| cmd.channel == 1).unwrap();
assert_eq!(fixture1_cmd.value, 255); }
#[test]
fn test_chase_direction_circular() {
let mut engine = EffectEngine::new();
for i in 1..=4 {
let mut channels = HashMap::new();
channels.insert("dimmer".to_string(), 1);
channels.insert("red".to_string(), 2);
channels.insert("green".to_string(), 3);
channels.insert("blue".to_string(), 4);
let fixture = FixtureInfo::new(
format!("fixture_{}", i),
1,
(i - 1) * 4 + 1,
"RGB_Par".to_string(),
channels,
Some(20.0),
);
engine.register_fixture(fixture);
}
let chase_effect = create_effect_with_layering(
"chase_cw".to_string(),
EffectType::Chase {
pattern: ChasePattern::Linear,
speed: TempoAwareSpeed::Fixed(2.0),
direction: ChaseDirection::Clockwise,
transition: CycleTransition::Snap,
duration: Duration::from_secs(10),
},
vec![
"fixture_1".to_string(),
"fixture_2".to_string(),
"fixture_3".to_string(),
"fixture_4".to_string(),
],
EffectLayer::Background,
BlendMode::Replace,
);
engine.start_effect(chase_effect).unwrap();
let commands = engine.update(Duration::from_millis(0), None).unwrap();
let fixture1_cmd = commands.iter().find(|cmd| cmd.channel == 1).unwrap();
assert_eq!(fixture1_cmd.value, 255); }
#[test]
fn test_chase_speed_variations() {
let mut engine = EffectEngine::new();
for i in 1..=3 {
let mut channels = HashMap::new();
channels.insert("dimmer".to_string(), 1);
channels.insert("red".to_string(), 2);
channels.insert("green".to_string(), 3);
channels.insert("blue".to_string(), 4);
let fixture = FixtureInfo::new(
format!("fixture_{}", i),
1,
(i - 1) * 4 + 1,
"RGB_Par".to_string(),
channels,
Some(20.0),
);
engine.register_fixture(fixture);
}
let slow_chase = create_effect_with_layering(
"chase_slow".to_string(),
EffectType::Chase {
pattern: ChasePattern::Linear,
speed: TempoAwareSpeed::Fixed(0.5), direction: ChaseDirection::LeftToRight,
transition: CycleTransition::Snap,
duration: Duration::from_secs(10),
},
vec![
"fixture_1".to_string(),
"fixture_2".to_string(),
"fixture_3".to_string(),
],
EffectLayer::Background,
BlendMode::Replace,
);
engine.start_effect(slow_chase).unwrap();
let commands = engine.update(Duration::from_millis(0), None).unwrap();
let fixture1_cmd = commands.iter().find(|cmd| cmd.channel == 1).unwrap();
assert_eq!(fixture1_cmd.value, 255);
let commands = engine.update(Duration::from_millis(600), None).unwrap();
let fixture1_cmd = commands.iter().find(|cmd| cmd.channel == 1).unwrap();
assert_eq!(fixture1_cmd.value, 255);
let commands = engine.update(Duration::from_millis(600), None).unwrap(); let fixture2_cmd = commands.iter().find(|cmd| cmd.channel == 5).unwrap();
assert_eq!(fixture2_cmd.value, 255); }
#[test]
fn test_chase_single_fixture() {
let mut engine = EffectEngine::new();
let mut channels = HashMap::new();
channels.insert("dimmer".to_string(), 1);
channels.insert("red".to_string(), 2);
channels.insert("green".to_string(), 3);
channels.insert("blue".to_string(), 4);
let fixture = FixtureInfo::new(
"single_fixture".to_string(),
1,
1,
"RGB_Par".to_string(),
channels,
Some(20.0),
);
engine.register_fixture(fixture);
let chase_effect = create_effect_with_layering(
"chase_single".to_string(),
EffectType::Chase {
pattern: ChasePattern::Linear,
speed: TempoAwareSpeed::Fixed(2.0),
direction: ChaseDirection::LeftToRight,
transition: CycleTransition::Snap,
duration: Duration::from_secs(10),
},
vec!["single_fixture".to_string()],
EffectLayer::Background,
BlendMode::Replace,
);
engine.start_effect(chase_effect).unwrap();
let commands = engine.update(Duration::from_millis(0), None).unwrap();
let fixture_cmd = commands.iter().find(|cmd| cmd.channel == 1).unwrap();
assert_eq!(fixture_cmd.value, 255);
let commands = engine.update(Duration::from_millis(500), None).unwrap();
let fixture_cmd = commands.iter().find(|cmd| cmd.channel == 1).unwrap();
assert_eq!(fixture_cmd.value, 255); }
#[test]
fn test_chase_rgb_only_fixtures() {
let mut engine = EffectEngine::new();
for i in 1..=3 {
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(
format!("rgb_fixture_{}", i),
1,
(i - 1) * 3 + 1,
"RGB_Par".to_string(),
channels,
None,
);
engine.register_fixture(fixture);
}
let chase_effect = create_effect_with_layering(
"chase_rgb".to_string(),
EffectType::Chase {
pattern: ChasePattern::Linear,
speed: TempoAwareSpeed::Fixed(2.0),
direction: ChaseDirection::LeftToRight,
transition: CycleTransition::Snap,
duration: Duration::from_secs(10),
},
vec![
"rgb_fixture_1".to_string(),
"rgb_fixture_2".to_string(),
"rgb_fixture_3".to_string(),
],
EffectLayer::Background,
BlendMode::Replace,
);
engine.start_effect(chase_effect).unwrap();
let commands = engine.update(Duration::from_millis(0), None).unwrap();
let red_cmd = commands.iter().find(|cmd| cmd.channel == 1).unwrap();
let green_cmd = commands.iter().find(|cmd| cmd.channel == 2).unwrap();
let blue_cmd = commands.iter().find(|cmd| cmd.channel == 3).unwrap();
assert_eq!(red_cmd.value, 255);
assert_eq!(green_cmd.value, 255);
assert_eq!(blue_cmd.value, 255);
let commands = engine.update(Duration::from_millis(167), None).unwrap();
let red_cmd = commands.iter().find(|cmd| cmd.channel == 4).unwrap(); let green_cmd = commands.iter().find(|cmd| cmd.channel == 5).unwrap(); let blue_cmd = commands.iter().find(|cmd| cmd.channel == 6).unwrap(); assert_eq!(red_cmd.value, 255);
assert_eq!(green_cmd.value, 255);
assert_eq!(blue_cmd.value, 255);
}
#[test]
fn test_chase_effect_crossfade() {
let mut engine = EffectEngine::new();
for i in 1..=4 {
let mut channels = HashMap::new();
channels.insert("dimmer".to_string(), i);
let fixture = FixtureInfo::new(
format!("fixture_{}", i),
1,
i,
"Dimmer".to_string(),
channels,
None,
);
engine.register_fixture(fixture);
}
let mut chase_effect = create_effect_with_timing(
"chase_test".to_string(),
EffectType::Chase {
pattern: ChasePattern::Linear,
speed: TempoAwareSpeed::Fixed(1.0), direction: ChaseDirection::LeftToRight,
transition: CycleTransition::Snap,
duration: Duration::from_secs(10),
},
vec![
"fixture_1".to_string(),
"fixture_2".to_string(),
"fixture_3".to_string(),
"fixture_4".to_string(),
],
EffectLayer::Midground,
BlendMode::Replace,
Some(Duration::from_secs(1)), Some(Duration::from_secs(1)), );
chase_effect.hold_time = Some(Duration::from_secs(2));
engine.start_effect(chase_effect).unwrap();
let commands = engine.update(Duration::from_millis(500), None).unwrap();
let active_fixture = commands.iter().find(|cmd| cmd.value > 0);
assert!(active_fixture.is_some());
if let Some(cmd) = active_fixture {
assert!(cmd.value > 0 && cmd.value < 255); }
let commands = engine.update(Duration::from_secs(2), None).unwrap();
let active_fixture = commands.iter().find(|cmd| cmd.value > 0);
assert!(active_fixture.is_some());
if let Some(cmd) = active_fixture {
assert_eq!(cmd.value, 255); }
let commands = engine.update(Duration::from_millis(1000), None).unwrap(); let active_fixture = commands.iter().find(|cmd| cmd.value > 0);
assert!(active_fixture.is_some());
if let Some(cmd) = active_fixture {
assert!(cmd.value > 0 && cmd.value < 255); }
let commands = engine.update(Duration::from_millis(500), None).unwrap(); assert!(commands.is_empty()); }
#[test]
fn test_random_chase_pattern_visibility() {
let mut engine = EffectEngine::new();
for i in 1..=8 {
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(
format!("Brick{}", i),
1,
(i - 1) * 4 + 1,
"Astera-PixelBrick".to_string(),
channels,
Some(25.0),
);
engine.register_fixture(fixture);
}
let mut random_chase = EffectInstance::new(
"random_chase".to_string(),
EffectType::Chase {
pattern: ChasePattern::Random,
speed: TempoAwareSpeed::Fixed(3.0), direction: ChaseDirection::LeftToRight,
transition: CycleTransition::Snap,
duration: Duration::from_secs(10),
},
vec![
"Brick1".to_string(),
"Brick2".to_string(),
"Brick3".to_string(),
"Brick4".to_string(),
"Brick5".to_string(),
"Brick6".to_string(),
"Brick7".to_string(),
"Brick8".to_string(),
],
None,
Some(Duration::from_secs(4)), None,
);
random_chase.layer = EffectLayer::Background;
random_chase.blend_mode = BlendMode::Replace;
engine.start_effect(random_chase).unwrap();
let mut total_commands = 0;
let mut active_fixtures: std::collections::HashSet<usize> = std::collections::HashSet::new();
for _step in 0..20 {
let cmds = engine.update(Duration::from_millis(50), None).unwrap();
total_commands += cmds.len();
for cmd in cmds {
if cmd.value > 0 {
for i in 1..=8 {
let expected_address = (i - 1) * 4 + 1;
if cmd.universe == 1
&& cmd.channel >= expected_address
&& cmd.channel < expected_address + 4
{
active_fixtures.insert(i as usize);
}
}
}
}
}
assert!(
total_commands > 0,
"Expected some DMX commands, got {}",
total_commands
);
assert!(active_fixtures.len() > 1,
"Expected multiple fixtures to be active (pattern advancing), but only {} fixture(s) were active: {:?}",
active_fixtures.len(), active_fixtures);
let fixture_order: Vec<usize> = active_fixtures.iter().copied().collect();
let is_sequential = fixture_order.windows(2).all(|w| w[1] == w[0] + 1);
assert!(
!is_sequential || fixture_order.len() < 3,
"Pattern appears to be sequential (not random). Active fixtures: {:?}",
fixture_order
);
}