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_tempo_aware_speed_adapts_to_tempo_changes() {
use crate::lighting::tempo::{
TempoChange, TempoChangePosition, TempoMap, TempoTransition, TimeSignature,
};
let mut engine = EffectEngine::new();
let fixture = create_test_fixture("test_fixture", 1, 1);
engine.register_fixture(fixture);
let tempo_map = TempoMap::new(
Duration::ZERO,
120.0,
TimeSignature::new(4, 4),
vec![TempoChange {
position: TempoChangePosition::Time(Duration::from_secs(4)),
original_measure_beat: None,
bpm: Some(60.0),
time_signature: None,
transition: TempoTransition::Snap,
}],
);
engine.set_tempo_map(Some(tempo_map));
let colors = vec![
Color::new(255, 0, 0), Color::new(0, 255, 0), Color::new(0, 0, 255), ];
let effect = EffectInstance::new(
"tempo_aware_cycle".to_string(),
EffectType::ColorCycle {
colors,
speed: TempoAwareSpeed::Measures(1.0), direction: CycleDirection::Forward,
transition: CycleTransition::Snap,
duration: Duration::from_secs(10),
},
vec!["test_fixture".to_string()],
None,
None,
None,
);
engine.start_effect(effect).unwrap();
let commands = engine.update(Duration::from_millis(100), None).unwrap();
assert!(!commands.is_empty(), "Effect should generate commands");
engine.update(Duration::from_secs(4), None).unwrap(); let commands_after = engine.update(Duration::from_millis(100), None).unwrap();
assert!(
!commands_after.is_empty(),
"Effect should still generate commands after tempo change"
);
let commands_later = engine.update(Duration::from_millis(1000), None).unwrap(); assert!(
!commands_later.is_empty(),
"Effect should continue running after tempo change"
);
}
#[test]
fn test_tempo_aware_frequency_adapts_to_tempo_changes() {
use crate::lighting::tempo::{
TempoChange, TempoChangePosition, TempoMap, TempoTransition, TimeSignature,
};
let mut engine = EffectEngine::new();
let fixture = create_test_fixture("test_fixture", 1, 1);
engine.register_fixture(fixture);
let tempo_map = TempoMap::new(
Duration::ZERO,
120.0,
TimeSignature::new(4, 4),
vec![TempoChange {
position: TempoChangePosition::Time(Duration::from_secs(2)),
original_measure_beat: None,
bpm: Some(60.0),
time_signature: None,
transition: TempoTransition::Snap,
}],
);
engine.set_tempo_map(Some(tempo_map));
let mut bg_params = HashMap::new();
bg_params.insert("red".to_string(), 1.0);
bg_params.insert("green".to_string(), 1.0);
bg_params.insert("blue".to_string(), 1.0);
let bg_effect = EffectInstance::new(
"bg".to_string(),
EffectType::Static {
parameters: bg_params,
duration: Duration::from_secs(5),
},
vec!["test_fixture".to_string()],
None,
None,
None,
);
engine.start_effect(bg_effect).unwrap();
engine.update(Duration::from_millis(10), None).unwrap();
let effect = EffectInstance::new(
"tempo_aware_strobe".to_string(),
EffectType::Strobe {
frequency: TempoAwareFrequency::Beats(1.0), 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(100), None).unwrap();
let strobe_before = commands_before.iter().find(|cmd| cmd.channel == 6);
assert!(
strobe_before.is_some(),
"Strobe should generate commands before tempo change"
);
engine.update(Duration::from_secs(2), None).unwrap(); let commands_after = engine.update(Duration::from_millis(100), None).unwrap();
assert!(
!commands_after.is_empty(),
"Effect should still generate commands after tempo change"
);
}
#[test]
fn test_tempo_aware_chase_adapts_to_tempo_changes() {
use crate::lighting::tempo::{
TempoChange, TempoChangePosition, TempoMap, TempoTransition, TimeSignature,
};
let mut engine = EffectEngine::new();
let fixture1 = create_test_fixture("fixture1", 1, 1);
let fixture2 = create_test_fixture("fixture2", 1, 6);
let fixture3 = create_test_fixture("fixture3", 1, 11);
engine.register_fixture(fixture1);
engine.register_fixture(fixture2);
engine.register_fixture(fixture3);
let tempo_map = TempoMap::new(
Duration::ZERO,
120.0,
TimeSignature::new(4, 4),
vec![TempoChange {
position: TempoChangePosition::Time(Duration::from_secs(3)),
original_measure_beat: None,
bpm: Some(60.0),
time_signature: None,
transition: TempoTransition::Snap,
}],
);
engine.set_tempo_map(Some(tempo_map));
let effect = EffectInstance::new(
"tempo_aware_chase".to_string(),
EffectType::Chase {
pattern: ChasePattern::Linear,
speed: TempoAwareSpeed::Measures(1.0), direction: ChaseDirection::LeftToRight,
transition: CycleTransition::Snap,
duration: Duration::from_secs(10),
},
vec![
"fixture1".to_string(),
"fixture2".to_string(),
"fixture3".to_string(),
],
None,
None,
None,
);
engine.start_effect(effect).unwrap();
let commands_before = engine.update(Duration::from_millis(100), None).unwrap();
assert!(
!commands_before.is_empty(),
"Chase should generate commands before tempo change"
);
engine.update(Duration::from_secs(3), None).unwrap(); let commands_after = engine.update(Duration::from_millis(100), None).unwrap();
assert!(
!commands_after.is_empty(),
"Chase should still generate commands after tempo change"
);
let commands_later = engine.update(Duration::from_millis(1000), None).unwrap();
assert!(
!commands_later.is_empty(),
"Chase should continue running after tempo change"
);
}
#[test]
fn test_tempo_aware_chase_beats_speed_never_zero() {
use crate::lighting::tempo::{
TempoChange, TempoChangePosition, TempoMap, TempoTransition, TimeSignature,
};
let mut engine = EffectEngine::new();
let fixture1 = create_test_fixture("fixture1", 1, 1);
let fixture2 = create_test_fixture("fixture2", 1, 6);
let fixture3 = create_test_fixture("fixture3", 1, 11);
engine.register_fixture(fixture1);
engine.register_fixture(fixture2);
engine.register_fixture(fixture3);
let tempo_map = TempoMap::new(
Duration::ZERO,
120.0,
TimeSignature::new(4, 4),
vec![TempoChange {
position: TempoChangePosition::Time(Duration::from_secs(3)),
original_measure_beat: None,
bpm: Some(60.0),
time_signature: None,
transition: TempoTransition::Snap,
}],
);
engine.set_tempo_map(Some(tempo_map));
let effect = EffectInstance::new(
"tempo_aware_chase_beats".to_string(),
EffectType::Chase {
pattern: ChasePattern::Linear,
speed: TempoAwareSpeed::Beats(0.5),
direction: ChaseDirection::LeftToRight,
transition: CycleTransition::Snap,
duration: Duration::from_secs(10),
},
vec![
"fixture1".to_string(),
"fixture2".to_string(),
"fixture3".to_string(),
],
None,
None,
None,
);
engine.start_effect(effect).unwrap();
let commands_before = engine.update(Duration::from_millis(100), None).unwrap();
assert!(
!commands_before.is_empty(),
"Chase with beats-based speed should generate commands before tempo change"
);
engine.update(Duration::from_secs(3), None).unwrap(); let commands_after = engine.update(Duration::from_millis(100), None).unwrap(); assert!(
!commands_after.is_empty(),
"Chase with beats-based speed should still generate commands after tempo change"
);
let commands_later = engine.update(Duration::from_millis(1000), None).unwrap();
assert!(
!commands_later.is_empty(),
"Chase with beats-based speed should continue running after tempo change"
);
}
#[test]
fn test_chase_after_tempo_change_with_measure_offset() {
use crate::lighting::tempo::{
TempoChange, TempoChangePosition, TempoMap, TempoTransition, TimeSignature,
};
let mut engine = EffectEngine::new();
let fixture1 = create_test_fixture("fixture1", 1, 1);
let fixture2 = create_test_fixture("fixture2", 1, 6);
let fixture3 = create_test_fixture("fixture3", 1, 11);
let fixture4 = create_test_fixture("fixture4", 1, 16);
engine.register_fixture(fixture1);
engine.register_fixture(fixture2);
engine.register_fixture(fixture3);
engine.register_fixture(fixture4);
let tempo_map = TempoMap::new(
Duration::from_secs_f64(1.5), 160.0, TimeSignature::new(4, 4),
vec![TempoChange {
position: TempoChangePosition::MeasureBeat(68, 1.0),
original_measure_beat: Some((68, 1.0)),
bpm: Some(120.0),
time_signature: None,
transition: TempoTransition::Snap,
}],
);
engine.set_tempo_map(Some(tempo_map.clone()));
let measure_offset = 8;
let random_chase_time = tempo_map
.measure_to_time_with_offset(70, 1.0, measure_offset, 0.0)
.expect("Should be able to calculate time for measure 70/1");
let linear_chase_time = tempo_map
.measure_to_time_with_offset(74, 1.0, measure_offset, 0.0)
.expect("Should be able to calculate time for measure 74/1");
let random_chase = EffectInstance::new(
"random_chase".to_string(),
EffectType::Chase {
pattern: ChasePattern::Random,
speed: TempoAwareSpeed::Beats(1.0),
direction: ChaseDirection::LeftToRight,
transition: CycleTransition::Snap,
duration: Duration::from_secs(10),
},
vec![
"fixture1".to_string(),
"fixture2".to_string(),
"fixture3".to_string(),
"fixture4".to_string(),
],
None,
None,
None,
);
let linear_chase = EffectInstance::new(
"linear_chase".to_string(),
EffectType::Chase {
pattern: ChasePattern::Linear,
speed: TempoAwareSpeed::Beats(0.5),
direction: ChaseDirection::RightToLeft,
transition: CycleTransition::Snap,
duration: Duration::from_secs(10),
},
vec![
"fixture1".to_string(),
"fixture2".to_string(),
"fixture3".to_string(),
"fixture4".to_string(),
],
None,
None,
None,
);
let time_before_random = random_chase_time - Duration::from_millis(10);
engine.update(time_before_random, None).unwrap();
engine.start_effect(random_chase).unwrap();
let commands_at_start = engine
.update(Duration::from_millis(26), None) .unwrap();
assert!(
!commands_at_start.is_empty(),
"Random chase should generate commands immediately after start"
);
let commands_during = engine.update(Duration::from_millis(100), None).unwrap();
assert!(
!commands_during.is_empty(),
"Random chase should continue generating commands during execution"
);
let elapsed_so_far = time_before_random + Duration::from_millis(126);
let time_before_linear = linear_chase_time - Duration::from_millis(10);
let delta_to_linear = time_before_linear.saturating_sub(elapsed_so_far);
engine.update(delta_to_linear, None).unwrap();
engine.start_effect(linear_chase).unwrap();
let commands_linear_start = engine
.update(Duration::from_millis(26), None) .unwrap();
assert!(
!commands_linear_start.is_empty(),
"Linear chase should generate commands immediately after start (at measure 74/1)"
);
let commands_linear_during = engine.update(Duration::from_millis(100), None).unwrap();
assert!(
!commands_linear_during.is_empty(),
"Linear chase should continue generating commands during execution"
);
let commands_linear_later = engine.update(Duration::from_millis(500), None).unwrap();
assert!(
!commands_linear_later.is_empty(),
"Linear chase should continue generating commands well into its execution"
);
let current_speed = TempoAwareSpeed::Beats(0.5).to_cycles_per_second(
Some(&tempo_map),
linear_chase_time + Duration::from_millis(100),
);
assert!(
current_speed > 0.0,
"Chase speed should never be zero; got speed={} at time after tempo change",
current_speed
);
}
#[test]
fn test_chase_timing_edge_cases_after_tempo_change() {
use crate::lighting::tempo::{
TempoChange, TempoChangePosition, TempoMap, TempoTransition, TimeSignature,
};
let mut engine = EffectEngine::new();
let fixture1 = create_test_fixture("fixture1", 1, 1);
let fixture2 = create_test_fixture("fixture2", 1, 6);
let fixture3 = create_test_fixture("fixture3", 1, 11);
engine.register_fixture(fixture1);
engine.register_fixture(fixture2);
engine.register_fixture(fixture3);
let tempo_map = TempoMap::new(
Duration::from_secs_f64(1.5),
160.0,
TimeSignature::new(4, 4),
vec![TempoChange {
position: TempoChangePosition::MeasureBeat(68, 1.0),
original_measure_beat: Some((68, 1.0)),
bpm: Some(120.0),
time_signature: None,
transition: TempoTransition::Snap,
}],
);
engine.set_tempo_map(Some(tempo_map.clone()));
let measure_offset = 8;
let linear_chase_time = tempo_map
.measure_to_time_with_offset(74, 1.0, measure_offset, 0.0)
.expect("Should be able to calculate time for measure 74/1");
let test_times = [
linear_chase_time - Duration::from_millis(1),
linear_chase_time,
linear_chase_time + Duration::from_nanos(1),
linear_chase_time + Duration::from_millis(1),
linear_chase_time + Duration::from_millis(10),
linear_chase_time + Duration::from_millis(100),
linear_chase_time + Duration::from_millis(500),
];
for (i, test_time) in test_times.iter().enumerate() {
let speed = TempoAwareSpeed::Beats(0.5).to_cycles_per_second(Some(&tempo_map), *test_time);
assert!(
speed > 0.0,
"Speed should never be zero at test point {} (time={:?}): got speed={}",
i,
test_time,
speed
);
}
}
#[test]
fn test_tempo_aware_rainbow_adapts_to_tempo_changes() {
use crate::lighting::tempo::{
TempoChange, TempoChangePosition, TempoMap, TempoTransition, TimeSignature,
};
let mut engine = EffectEngine::new();
let fixture = create_test_fixture("test_fixture", 1, 1);
engine.register_fixture(fixture);
let tempo_map = TempoMap::new(
Duration::ZERO,
120.0,
TimeSignature::new(4, 4),
vec![TempoChange {
position: TempoChangePosition::Time(Duration::from_millis(2500)),
original_measure_beat: None,
bpm: Some(60.0),
time_signature: None,
transition: TempoTransition::Snap,
}],
);
engine.set_tempo_map(Some(tempo_map));
let effect = EffectInstance::new(
"tempo_aware_rainbow".to_string(),
EffectType::Rainbow {
speed: TempoAwareSpeed::Beats(2.0), saturation: 1.0,
brightness: 1.0,
duration: Duration::from_secs(10),
},
vec!["test_fixture".to_string()],
None,
None,
None,
);
engine.start_effect(effect).unwrap();
let commands_before = engine.update(Duration::from_millis(100), None).unwrap();
assert!(
!commands_before.is_empty(),
"Rainbow should generate commands before tempo change"
);
engine.update(Duration::from_millis(2500), None).unwrap(); let commands_after = engine.update(Duration::from_millis(100), None).unwrap();
assert!(
!commands_after.is_empty(),
"Rainbow should still generate commands after tempo change"
);
let commands_later = engine.update(Duration::from_millis(1000), None).unwrap();
assert!(
!commands_later.is_empty(),
"Rainbow should continue running after tempo change"
);
}
#[test]
fn test_tempo_aware_pulse_adapts_to_tempo_changes() {
use crate::lighting::tempo::{
TempoChange, TempoChangePosition, TempoMap, TempoTransition, TimeSignature,
};
let mut engine = EffectEngine::new();
let fixture = create_test_fixture("test_fixture", 1, 1);
engine.register_fixture(fixture);
let tempo_map = TempoMap::new(
Duration::ZERO,
120.0,
TimeSignature::new(4, 4),
vec![TempoChange {
position: TempoChangePosition::Time(Duration::from_millis(1500)),
original_measure_beat: None,
bpm: Some(60.0),
time_signature: None,
transition: TempoTransition::Snap,
}],
);
engine.set_tempo_map(Some(tempo_map));
let effect = EffectInstance::new(
"tempo_aware_pulse".to_string(),
EffectType::Pulse {
base_level: 0.5,
pulse_amplitude: 0.5,
frequency: TempoAwareFrequency::Beats(1.0), 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(100), None).unwrap();
assert!(
!commands_before.is_empty(),
"Pulse should generate commands before tempo change"
);
engine.update(Duration::from_millis(1500), None).unwrap(); let commands_after = engine.update(Duration::from_millis(100), None).unwrap();
assert!(
!commands_after.is_empty(),
"Pulse should still generate commands after tempo change"
);
let commands_later = engine.update(Duration::from_millis(1000), None).unwrap();
assert!(
!commands_later.is_empty(),
"Pulse should continue running after tempo change"
);
}