use crate::lighting::parser::*;
use std::time::Duration;
#[test]
fn test_measure_offset() {
let content = r#"tempo {
start: 0.0s
bpm: 120
time_signature: 4/4
}
show "Test" {
@1/1
all_wash: static, color: "blue", duration: 5s, duration: 5s
offset 8 measures
@1/1
all_wash: static, color: "green", duration: 5s, duration: 5s
@8/1
all_wash: static, color: "yellow", duration: 5s, duration: 5s
@9/1
all_wash: static, color: "red", duration: 5s, duration: 5s
}
"#;
let result = parse_light_shows(content);
assert!(
result.is_ok(),
"Failed to parse show with offset: {:?}",
result.err()
);
let shows = result.unwrap();
let show = shows.get("Test").unwrap();
assert!(show.cues.len() >= 4, "Should have at least 4 cues");
let first_cue_time = show.cues[0].time;
let expected_first = Duration::from_secs_f64(0.0);
assert!(
(first_cue_time.as_secs_f64() - expected_first.as_secs_f64()).abs() < 0.001,
"First cue should be at measure 1 (0.0s), got {:?}",
first_cue_time
);
let second_cue_time = show.cues[1].time;
let expected_second = Duration::from_secs_f64(16.0);
assert!(
(second_cue_time.as_secs_f64() - expected_second.as_secs_f64()).abs() < 0.001,
"Second cue should be at playback measure 9 (16.0s), got {:?}",
second_cue_time
);
let third_cue_time = show.cues[2].time;
let expected_third = Duration::from_secs_f64(30.0);
assert!(
(third_cue_time.as_secs_f64() - expected_third.as_secs_f64()).abs() < 0.001,
"Third cue should be at playback measure 16 (30.0s), got {:?}",
third_cue_time
);
let fourth_cue_time = show.cues[3].time;
let expected_fourth = Duration::from_secs_f64(32.0);
assert!(
(fourth_cue_time.as_secs_f64() - expected_fourth.as_secs_f64()).abs() < 0.001,
"Fourth cue should be at playback measure 17 (32.0s), got {:?}",
fourth_cue_time
);
}
#[test]
fn test_reset_measures() {
let content = r#"tempo {
start: 0.0s
bpm: 120
time_signature: 4/4
}
show "Test" {
@1/1
all_wash: static, color: "blue", duration: 5s
offset 8 measures
@1/1
all_wash: static, color: "green", duration: 5s
reset_measures
@1/1
all_wash: static, color: "red", duration: 5s
}
"#;
let result = parse_light_shows(content);
assert!(
result.is_ok(),
"Failed to parse show with reset_measures: {:?}",
result.err()
);
let shows = result.unwrap();
let show = shows.get("Test").unwrap();
assert!(show.cues.len() >= 3, "Should have at least 3 cues");
let first_cue_time = show.cues[0].time;
let second_cue_time = show.cues[1].time;
let third_cue_time = show.cues[2].time;
assert!(
(first_cue_time.as_secs_f64() - 0.0).abs() < 0.001,
"First cue should be at 0.0s, got {:?}",
first_cue_time
);
assert!(
(second_cue_time.as_secs_f64() - 16.0).abs() < 0.001,
"Second cue should be at 16.0s (measure 9), got {:?}",
second_cue_time
);
assert!(
(third_cue_time.as_secs_f64() - 0.0).abs() < 0.001,
"Third cue should be at 0.0s (after reset), got {:?}",
third_cue_time
);
}
#[test]
fn test_measure_offset_accumulation() {
let content = r#"tempo {
start: 0.0s
bpm: 120
time_signature: 4/4
}
show "Test" {
@1/1
all_wash: static, color: "blue", duration: 5s
offset 4 measures
@1/1
all_wash: static, color: "green", duration: 5s
offset 4 measures
@1/1
all_wash: static, color: "red", duration: 5s
}
"#;
let result = parse_light_shows(content);
assert!(
result.is_ok(),
"Failed to parse show with accumulating offsets: {:?}",
result.err()
);
let shows = result.unwrap();
let show = shows.get("Test").unwrap();
assert!(show.cues.len() >= 3, "Should have at least 3 cues");
assert!(
(show.cues[0].time.as_secs_f64() - 0.0).abs() < 0.001,
"First cue should be at 0.0s"
);
assert!(
(show.cues[1].time.as_secs_f64() - 8.0).abs() < 0.001,
"Second cue should be at 8.0s (measure 5)"
);
assert!(
(show.cues[2].time.as_secs_f64() - 16.0).abs() < 0.001,
"Third cue should be at 16.0s (measure 9)"
);
}
#[test]
fn test_measure_offset_in_sequence() {
let content = r#"tempo {
start: 0.0s
bpm: 120
time_signature: 4/4
}
sequence "verse" {
@1/1
all_wash: static, color: "blue", duration: 5s
offset 8 measures
@1/1
all_wash: static, color: "green", duration: 5s
@9/1
all_wash: static, color: "red", duration: 5s
}
show "Test" {
@1/1
sequence "verse"
}
"#;
let result = parse_light_shows(content);
assert!(
result.is_ok(),
"Failed to parse sequence with offset: {:?}",
result.err()
);
let shows = result.unwrap();
let show = shows.get("Test").unwrap();
assert!(show.cues.len() >= 3, "Should have at least 3 cues");
assert!(
(show.cues[0].time.as_secs_f64() - 0.0).abs() < 0.001,
"First cue should be at 0.0s"
);
assert!(
(show.cues[1].time.as_secs_f64() - 16.0).abs() < 0.001,
"Second cue should be at 16.0s (measure 9)"
);
assert!(
(show.cues[2].time.as_secs_f64() - 32.0).abs() < 0.001,
"Third cue should be at 32.0s (measure 17)"
);
}
#[test]
fn test_measure_offset_with_fractional_beats() {
let content = r#"tempo {
start: 0.0s
bpm: 120
time_signature: 4/4
}
show "Test" {
@1/1.5
all_wash: static, color: "blue", duration: 5s
offset 8 measures
@1/2.5
all_wash: static, color: "green", duration: 5s
}
"#;
let result = parse_light_shows(content);
assert!(
result.is_ok(),
"Failed to parse show with offset and fractional beats: {:?}",
result.err()
);
let shows = result.unwrap();
let show = shows.get("Test").unwrap();
assert!(show.cues.len() >= 2, "Should have at least 2 cues");
assert!(
(show.cues[0].time.as_secs_f64() - 0.25).abs() < 0.001,
"First cue should be at 0.25s"
);
assert!(
(show.cues[1].time.as_secs_f64() - 16.75).abs() < 0.001,
"Second cue should be at 16.75s"
);
}
#[test]
fn test_measure_offset_with_tempo_changes() {
let content = r#"tempo {
start: 0.0s
bpm: 120
time_signature: 4/4
changes: [
@8/1 {
bpm: 60
}
]
}
show "Test" {
@1/1
all_wash: static, color: "blue", duration: 5s
offset 8 measures
@1/1
all_wash: static, color: "green", duration: 5s
}
"#;
let result = parse_light_shows(content);
assert!(
result.is_ok(),
"Failed to parse show with offset and tempo change: {:?}",
result.err()
);
let shows = result.unwrap();
let show = shows.get("Test").unwrap();
assert!(show.cues.len() >= 2, "Should have at least 2 cues");
assert!(
(show.cues[0].time.as_secs_f64() - 0.0).abs() < 0.001,
"First cue should be at 0.0s"
);
assert!(
(show.cues[1].time.as_secs_f64() - 16.0).abs() < 0.001,
"Second cue should be at 16.0s (measure 9 at 120 BPM, tempo change is at measure 16)"
);
}
#[test]
fn test_measure_offset_in_same_cue() {
let content = r#"tempo {
start: 1.5s
bpm: 160
time_signature: 4/4
changes: [
@68/1 { bpm: 180 },
@104/1 { bpm: 160 }
]
}
show "Test" {
@70/1
all_wash: static, color: "blue", duration: 5s
@74/1
all_wash: static, color: "green", duration: 5s
offset 5 measures
@70/1
all_wash: static, color: "red", duration: 5s
@74/1
all_wash: static, color: "yellow", duration: 5s
}
"#;
let result = parse_light_shows(content);
assert!(
result.is_ok(),
"Failed to parse show with offset in same cue: {:?}",
result.err()
);
let shows = result.unwrap();
let show = shows.get("Test").unwrap();
assert!(show.cues.len() >= 4, "Should have at least 4 cues");
let first_cue_time = show.cues[0].time;
let expected_first = Duration::from_secs_f64(104.666666667);
assert!(
(first_cue_time.as_secs_f64() - expected_first.as_secs_f64()).abs() < 0.01,
"First cue should be at measure 70 (104.667s), got {:?} ({:.3}s)",
first_cue_time,
first_cue_time.as_secs_f64()
);
let second_cue_time = show.cues[1].time;
let expected_second = Duration::from_secs_f64(110.0);
assert!(
(second_cue_time.as_secs_f64() - expected_second.as_secs_f64()).abs() < 0.01,
"Second cue should be at measure 74 (110.0s), got {:?} ({:.3}s)",
second_cue_time,
second_cue_time.as_secs_f64()
);
let third_cue_time = show.cues[2].time;
let expected_third = Duration::from_secs_f64(111.333333333);
assert!(
(third_cue_time.as_secs_f64() - expected_third.as_secs_f64()).abs() < 0.01,
"Third cue should be at playback measure 75 (~111.33s) after offset 5, got {:?} ({:.3}s)",
third_cue_time,
third_cue_time.as_secs_f64()
);
let fourth_cue_time = show.cues[3].time;
let expected_fourth = Duration::from_secs_f64(116.666666667);
assert!(
(fourth_cue_time.as_secs_f64() - expected_fourth.as_secs_f64()).abs() < 0.01,
"Fourth cue should be at playback measure 79 (~116.67s) after offset 5, got {:?} ({:.3}s)",
fourth_cue_time,
fourth_cue_time.as_secs_f64()
);
}
#[test]
fn test_measure_offset_with_measure_time_in_same_cue() {
let content = r#"tempo {
start: 0.0s
bpm: 120
time_signature: 4/4
}
show "Test" {
@1/1
all_wash: static, color: "blue", duration: 5s
@5/1
all_wash: static, color: "green", duration: 5s
offset 10 measures
@1/1
all_wash: static, color: "red", duration: 5s
}
"#;
let result = parse_light_shows(content);
assert!(
result.is_ok(),
"Failed to parse show with offset and measure time in same cue: {:?}",
result.err()
);
let shows = result.unwrap();
let show = shows.get("Test").unwrap();
assert!(show.cues.len() >= 3, "Should have at least 3 cues");
let first_cue_time = show.cues[0].time;
let expected_first = Duration::from_secs_f64(0.0);
assert!(
(first_cue_time.as_secs_f64() - expected_first.as_secs_f64()).abs() < 0.001,
"First cue should be at measure 1 (0.0s), got {:?} ({:.3}s)",
first_cue_time,
first_cue_time.as_secs_f64()
);
assert!(
!show.cues[0].effects.is_empty(),
"First cue should have effects"
);
let second_cue_time = show.cues[1].time;
let expected_second = Duration::from_secs_f64(8.0);
assert!(
(second_cue_time.as_secs_f64() - expected_second.as_secs_f64()).abs() < 0.001,
"Second cue should be at playback measure 5 (8.0s), offset should NOT apply to current cue, got {:?} ({:.3}s)",
second_cue_time,
second_cue_time.as_secs_f64()
);
assert!(
!show.cues[1].effects.is_empty(),
"Second cue should have effects even though it has an offset command"
);
let third_cue_time = show.cues[2].time;
let expected_third = Duration::from_secs_f64(20.0);
assert!(
(third_cue_time.as_secs_f64() - expected_third.as_secs_f64()).abs() < 0.001,
"Third cue should be at playback measure 11 (20.0s) with offset 10, got {:?} ({:.3}s)",
third_cue_time,
third_cue_time.as_secs_f64()
);
}
#[test]
fn test_offset_timing_at_180_bpm_with_tempo_change() {
let content = r#"tempo {
start: 1.5s
bpm: 160
time_signature: 4/4
changes: [
@68/1 { bpm: 180 },
@104/1 { bpm: 160 }
]
}
show "Test" {
@70/1
all_wash: static, color: "blue", duration: 5s
@74/1
all_wash: static, color: "green", duration: 5s
offset 5 measures
@70/1
all_wash: static, color: "red", duration: 5s
}
"#;
let result = parse_light_shows(content);
assert!(result.is_ok(), "Failed to parse show: {:?}", result.err());
let shows = result.unwrap();
let show = shows.get("Test").unwrap();
assert!(show.cues.len() >= 3, "Should have at least 3 cues");
let second_cue_time = show.cues[1].time; let third_cue_time = show.cues[2].time;
println!(
"Second cue (@74/1) time: {:.3}s",
second_cue_time.as_secs_f64()
);
println!(
"Third cue (@70/1 with offset 5) time: {:.3}s",
third_cue_time.as_secs_f64()
);
let tm = show.tempo_map.as_ref().unwrap();
let calc_74 = tm
.measure_to_time_with_offset(74, 1.0, 0, 0.0)
.unwrap()
.as_secs_f64();
let calc_70_off5 = tm
.measure_to_time_with_offset(70, 1.0, 5, 0.0)
.unwrap()
.as_secs_f64();
println!(
"Calc tempo map: @74/1 = {:.3}s, @70/1 (offset 5) = {:.3}s, diff = {:.3}s",
calc_74,
calc_70_off5,
calc_70_off5 - calc_74
);
let time_diff = third_cue_time.as_secs_f64() - second_cue_time.as_secs_f64();
let expected_diff = 1.333333333;
let actual_measures = time_diff / 1.333333333; println!(
"Time difference: {:.3}s = {:.3} measures at 180 BPM (expected: 1.0 measure)",
time_diff, actual_measures
);
assert!(
(time_diff - expected_diff).abs() < 0.01,
"Time difference between @74/1 and second @70/1 (with offset 5) should be ~1.333s (1 measure at 180 BPM), got {:.3}s (difference: {:.3}s, actual: {:.3} measures)",
time_diff,
time_diff - expected_diff,
actual_measures
);
}
#[test]
fn test_measure_offset_at_start() {
let content = r#"tempo {
start: 0.0s
bpm: 120
time_signature: 4/4
}
show "Test" {
@1/1
offset 8 measures
@1/1
all_wash: static, color: "blue", duration: 5s
}
"#;
let result = parse_light_shows(content);
assert!(
result.is_ok(),
"Failed to parse show with offset in first cue: {:?}",
result.err()
);
let shows = result.unwrap();
let show = shows.get("Test").unwrap();
assert!(show.cues.len() >= 2, "Should have at least 2 cues");
assert!(
(show.cues[0].time.as_secs_f64() - 0.0).abs() < 0.001,
"First cue should be at 0.0s (before offset takes effect)"
);
assert!(
(show.cues[1].time.as_secs_f64() - 16.0).abs() < 0.001,
"Second cue should be at 16.0s (measure 9, after offset)"
);
}
#[test]
fn test_measure_offset_reset_and_reoffset() {
let content = r#"tempo {
start: 0.0s
bpm: 120
time_signature: 4/4
}
show "Test" {
@1/1
all_wash: static, color: "blue", duration: 5s
offset 8 measures
@1/1
all_wash: static, color: "green", duration: 5s
@1/1
all_wash: static, color: "yellow", duration: 5s
reset_measures
offset 4 measures
@1/1
all_wash: static, color: "red", duration: 5s
}
"#;
let result = parse_light_shows(content);
assert!(
result.is_ok(),
"Failed to parse show with reset and reoffset: {:?}",
result.err()
);
let shows = result.unwrap();
let show = shows.get("Test").unwrap();
assert!(show.cues.len() >= 4, "Should have at least 4 cues");
assert!(
(show.cues[0].time.as_secs_f64() - 0.0).abs() < 0.001,
"First cue should be at 0.0s"
);
assert!(
(show.cues[1].time.as_secs_f64() - 16.0).abs() < 0.001,
"Second cue should be at 16.0s (measure 9)"
);
assert!(
(show.cues[2].time.as_secs_f64() - 16.0).abs() < 0.001,
"Third cue should be at 16.0s (measure 9, before reset takes effect)"
);
let fourth_cue_time = show.cues[3].time.as_secs_f64();
assert!(
(fourth_cue_time - 8.0).abs() < 0.001,
"Fourth cue should be at 8.0s (measure 5 after reset and offset 4), got {}",
fourth_cue_time
);
}
#[test]
fn test_measure_offset_with_alternate_endings_scenario() {
let content = r#"tempo {
start: 0.0s
bpm: 120
time_signature: 4/4
}
show "Test" {
@1/1
all_wash: static, color: "blue", duration: 5s
@8/1
all_wash: static, color: "yellow", duration: 5s
offset 8 measures
@1/1
all_wash: static, color: "green", duration: 5s
@7/1
all_wash: static, color: "orange", duration: 5s
@9/1
all_wash: static, color: "red", duration: 5s
@10/1
all_wash: static, color: "purple", duration: 5s
}
"#;
let result = parse_light_shows(content);
assert!(
result.is_ok(),
"Failed to parse show with alternate endings scenario: {:?}",
result.err()
);
let shows = result.unwrap();
let show = shows.get("Test").unwrap();
assert!(show.cues.len() >= 6, "Should have at least 6 cues");
assert!(
(show.cues[0].time.as_secs_f64() - 0.0).abs() < 0.001,
"First cue should be at 0.0s"
);
assert!(
(show.cues[1].time.as_secs_f64() - 14.0).abs() < 0.001,
"Second cue should be at 14.0s (measure 8)"
);
assert!(
(show.cues[2].time.as_secs_f64() - 16.0).abs() < 0.001,
"Third cue should be at 16.0s (measure 9, second repeat)"
);
assert!(
(show.cues[3].time.as_secs_f64() - 28.0).abs() < 0.001,
"Fourth cue should be at 28.0s (measure 15)"
);
assert!(
(show.cues[4].time.as_secs_f64() - 32.0).abs() < 0.001,
"Fifth cue should be at 32.0s (measure 17, alternate ending)"
);
assert!(
(show.cues[5].time.as_secs_f64() - 34.0).abs() < 0.001,
"Sixth cue should be at 34.0s (measure 18, after repeat)"
);
}
#[test]
fn test_esaweg_strobe_cue_generation() {
let content = r##"tempo {
bpm: 110
time_signature: 4/4
changes: [
@30/1 { bpm: 160 },
@136/1 { bpm: 110 },
@188/1 { bpm: 160 }
]
}
show "Esaweg" {
@1/1
offset 1 measures
@221/1
clear()
all_wash: static, color: "#FF4400", layer: background, blend_mode: replace, duration: 5s
@228/1
all_wash: pulse, frequency: 3, layer: foreground, blend_mode: overlay, duration: 5s
@236/1
all_wash: strobe, frequency: 10, layer: foreground, blend_mode: overlay, duration: 5s
@236/4.75
clear()
all_wash: static, color: "black", layer: background, blend_mode: replace, duration: 5s
}"##;
let shows = parse_light_shows(content).expect("Should parse Esaweg show");
let show = shows.get("Esaweg").expect("Should have Esaweg show");
let strobe_cue = show.cues.iter().find(|cue| {
cue.effects.iter().any(|e| {
matches!(
e.effect_type,
crate::lighting::effects::EffectType::Strobe { .. }
)
})
});
assert!(
strobe_cue.is_some(),
"Should have a cue with a strobe effect"
);
let strobe_cue = strobe_cue.unwrap();
let strobe_effect = strobe_cue
.effects
.iter()
.find(|e| {
matches!(
e.effect_type,
crate::lighting::effects::EffectType::Strobe { .. }
)
})
.unwrap();
assert_eq!(strobe_effect.groups, vec!["all_wash"]);
assert_eq!(
strobe_effect.layer,
Some(crate::lighting::effects::EffectLayer::Foreground)
);
assert_eq!(
strobe_effect.blend_mode,
Some(crate::lighting::effects::BlendMode::Overlay)
);
if let crate::lighting::effects::EffectType::Strobe { frequency, .. } =
&strobe_effect.effect_type
{
let hz = frequency.to_hz(None, Duration::ZERO);
assert!(
(hz - 10.0).abs() < 0.001,
"Strobe frequency should be 10Hz, got {}",
hz
);
}
let strobe_time = strobe_cue.time.as_secs_f64();
assert!(
strobe_time > 400.0 && strobe_time < 420.0,
"Strobe cue time should be around 409.9s, got {}",
strobe_time
);
let final_cue = show.cues.last().unwrap();
assert!(
final_cue.time > strobe_cue.time,
"Final cue should come after strobe cue"
);
let window = (final_cue.time - strobe_cue.time).as_secs_f64();
assert!(
(window - 1.40625).abs() < 0.01,
"Strobe window should be ~1.4s, got {}",
window
);
}