use crate::lighting::parser::*;
use crate::lighting::tempo::TempoTransition;
use std::time::Duration;
#[test]
fn test_end_to_end_measure_to_time_conversion() {
let content = r#"tempo {
start: 0.0s
bpm: 120
time_signature: 4/4
}
show "Measure Conversion Test" {
@1/1
front_wash: static color: "blue", duration: 5s
@2/1
back_wash: static color: "red", duration: 5s
@4/1
side_wash: static color: "green", duration: 5s
}"#;
let result = parse_light_shows(content);
if let Err(e) = &result {
println!("Parser error: {}", e);
println!("Full error details: {:?}", e);
}
assert!(result.is_ok(), "Show should parse successfully");
let shows = result.unwrap();
let show = shows.get("Measure Conversion Test").unwrap();
assert_eq!(show.cues.len(), 3);
assert_eq!(show.cues[0].time.as_secs_f64(), 0.0);
assert_eq!(show.cues[1].time.as_secs_f64(), 2.0);
assert_eq!(show.cues[2].time.as_secs_f64(), 6.0);
}
#[test]
fn test_end_to_end_fractional_beat_conversion() {
let content = r#"tempo {
start: 0.0s
bpm: 120
time_signature: 4/4
}
show "Fractional Beat Test" {
@1/1
front_wash: static color: "blue", duration: 5s
@1/2
back_wash: static color: "red", duration: 5s
@1/2.5
side_wash: static color: "green", duration: 5s
}"#;
let result = parse_light_shows(content);
if let Err(e) = &result {
println!("Parse error: {}", e);
}
assert!(result.is_ok());
let shows = result.unwrap();
let show = shows.get("Fractional Beat Test").unwrap();
assert_eq!(show.cues.len(), 3);
assert_eq!(show.cues[0].time.as_secs_f64(), 0.0);
let time1 = show.cues[1].time.as_secs_f64();
let time2 = show.cues[2].time.as_secs_f64();
println!(
"Fractional beat test: beat 2 = {}s (expected 0.5s), beat 2.5 = {}s (expected 0.75s)",
time1, time2
);
assert!((time1 - 0.5).abs() < 0.001, "Expected 0.5s, got {}s", time1);
assert!(
(time2 - 0.75).abs() < 0.001,
"Expected 0.75s, got {}s",
time2
);
}
#[test]
fn test_end_to_end_beat_duration_conversion() {
let content = r#"tempo {
start: 0.0s
bpm: 120
time_signature: 4/4
}
show "Beat Duration Test" {
@1/1
front_wash: static color: "blue", duration: 4beats
}"#;
let result = parse_light_shows(content);
if let Err(e) = &result {
println!("Parse error: {}", e);
}
assert!(result.is_ok());
let shows = result.unwrap();
let show = shows.get("Beat Duration Test").unwrap();
let effect = &show.cues[0].effects[0];
let duration = effect.effect_type.duration();
assert!(
(duration.as_secs_f64() - 2.0).abs() < 0.001,
"4 beats should be 2.0s at 120 BPM"
);
}
#[test]
fn test_end_to_end_measure_duration_conversion() {
let content = r#"tempo {
start: 0.0s
bpm: 120
time_signature: 4/4
}
show "Measure Duration Test" {
@1/1
front_wash: static color: "blue", duration: 2measures
}"#;
let result = parse_light_shows(content);
if let Err(e) = &result {
println!("Parse error: {}", e);
}
assert!(result.is_ok());
let shows = result.unwrap();
let show = shows.get("Measure Duration Test").unwrap();
let effect = &show.cues[0].effects[0];
let duration = effect.effect_type.duration();
assert!(
(duration.as_secs_f64() - 4.0).abs() < 0.001,
"2 measures should be 4.0s at 120 BPM in 4/4"
);
}
#[test]
fn test_end_to_end_tempo_change_affects_timing() {
let content = r#"tempo {
start: 0.0s
bpm: 120
time_signature: 4/4
changes: [
@8/1 { bpm: 60 }
]
}
show "Tempo Change Test" {
@4/1
front_wash: static color: "blue", duration: 5s
@8/1
back_wash: static color: "red", duration: 5s
@12/1
side_wash: static color: "green", duration: 5s
}"#;
let result = parse_light_shows(content);
if let Err(e) = &result {
println!("Parse error: {}", e);
}
assert!(result.is_ok());
let shows = result.unwrap();
let show = shows.get("Tempo Change Test").unwrap();
assert_eq!(show.cues.len(), 3);
assert!((show.cues[0].time.as_secs_f64() - 6.0).abs() < 0.001);
assert!((show.cues[1].time.as_secs_f64() - 14.0).abs() < 0.001);
assert!((show.cues[2].time.as_secs_f64() - 30.0).abs() < 0.001);
}
#[test]
fn test_end_to_end_time_signature_change_affects_timing() {
let content = r#"tempo {
start: 0.0s
bpm: 120
time_signature: 4/4
changes: [
@4/1 { time_signature: 3/4 }
]
}
show "Time Signature Change Test" {
@4/1
front_wash: static color: "blue", duration: 5s
@5/1
back_wash: static color: "red", duration: 5s
}"#;
let result = parse_light_shows(content);
if let Err(e) = &result {
println!("Parse error: {}", e);
}
assert!(result.is_ok());
let shows = result.unwrap();
let show = shows.get("Time Signature Change Test").unwrap();
assert_eq!(show.cues.len(), 2);
let time0 = show.cues[0].time.as_secs_f64();
let time1 = show.cues[1].time.as_secs_f64();
println!(
"Time sig change test: measure 4 = {}s (expected 6.0s), measure 5 = {}s (expected 7.5s)",
time0, time1
);
assert!((time0 - 6.0).abs() < 0.001, "Expected 6.0s, got {}s", time0);
assert!((time1 - 7.5).abs() < 0.001, "Expected 7.5s, got {}s", time1);
}
#[test]
fn test_end_to_end_beat_duration_with_tempo_change() {
let content = r#"tempo {
start: 0.0s
bpm: 120
time_signature: 4/4
changes: [
@4/1 { bpm: 60 }
]
}
show "Beat Duration Tempo Change Test" {
@2/1
front_wash: static color: "blue", duration: 4beats
@5/1
back_wash: static color: "red", duration: 4beats
}"#;
let result = parse_light_shows(content);
if let Err(e) = &result {
println!("Parse error: {}", e);
}
assert!(result.is_ok());
let shows = result.unwrap();
let show = shows.get("Beat Duration Tempo Change Test").unwrap();
let effect1 = &show.cues[0].effects[0];
let duration1 = effect1.effect_type.duration();
assert!(
(duration1.as_secs_f64() - 2.0).abs() < 0.001,
"4 beats at 120 BPM should be 2.0s"
);
let cue1_time = show.cues[1].time;
let effect2 = &show.cues[1].effects[0];
let duration2 = effect2.effect_type.duration();
let actual_duration = duration2.as_secs_f64();
println!("Beat duration with tempo change test: cue 0 at @2/1 (time={:?}), cue 1 at @5/1 (time={:?}), duration = {}s (expected 4.0s at 60 BPM)", show.cues[0].time, cue1_time, actual_duration);
if let Some(tm) = &show.tempo_map {
let bpm_at_cue0 = tm.bpm_at_time(show.cues[0].time, 0.0);
let bpm_at_cue1 = tm.bpm_at_time(cue1_time, 0.0);
println!(
"BPM at cue 0 time {:?} = {}, BPM at cue 1 time {:?} = {}",
show.cues[0].time, bpm_at_cue0, cue1_time, bpm_at_cue1
);
println!("Tempo changes: {:?}", tm.changes);
for change in &tm.changes {
if let Some(change_time) = change.position.absolute_time() {
println!(
" Change at {:?}: bpm={:?}, transition={:?}",
change_time, change.bpm, change.transition
);
}
}
}
assert!(
(actual_duration - 4.0).abs() < 0.001,
"4 beats at 60 BPM should be 4.0s, got {}s",
actual_duration
);
}
#[test]
fn test_end_to_end_up_time_and_down_time_with_beats() {
let content = r#"tempo {
start: 0.0s
bpm: 120
time_signature: 4/4
}
show "Beat Fade Times Test" {
@1/1
front_wash: static color: "blue", up_time: 2beats, down_time: 2beats, hold_time: 5s
}"#;
let result = parse_light_shows(content);
if let Err(e) = &result {
println!("Parse error: {}", e);
}
assert!(result.is_ok());
let shows = result.unwrap();
let show = shows.get("Beat Fade Times Test").unwrap();
let effect = &show.cues[0].effects[0];
assert!(effect.up_time.is_some());
assert!(effect.down_time.is_some());
assert!((effect.up_time.unwrap().as_secs_f64() - 1.0).abs() < 0.001);
assert!((effect.down_time.unwrap().as_secs_f64() - 1.0).abs() < 0.001);
}
#[test]
fn test_end_to_end_complex_tempo_changes() {
let content = r#"tempo {
start: 0.0s
bpm: 120
time_signature: 4/4
changes: [
@4/1 { bpm: 140 },
@8/1 { bpm: 100 },
@12/1 { time_signature: 3/4 }
]
}
show "Complex Tempo Test" {
@1/1
front_wash: static color: "blue", duration: 5s
@4/1
back_wash: static color: "red", duration: 5s
@8/1
side_wash: static color: "green", duration: 5s
@12/1
top_wash: static color: "yellow", duration: 5s
@13/1
bottom_wash: static color: "purple", duration: 5s
}"#;
let result = parse_light_shows(content);
if let Err(e) = &result {
println!("Parse error: {}", e);
}
assert!(result.is_ok());
let shows = result.unwrap();
let show = shows.get("Complex Tempo Test").unwrap();
assert_eq!(show.cues.len(), 5);
assert!((show.cues[0].time.as_secs_f64() - 0.0).abs() < 0.001);
assert!((show.cues[1].time.as_secs_f64() - 6.0).abs() < 0.001);
assert!(show.cues[2].time.as_secs_f64() > 12.0);
assert!(show.cues[2].time.as_secs_f64() < 14.0);
assert!(show.cues[3].time.as_secs_f64() > show.cues[2].time.as_secs_f64());
assert!(show.cues[4].time.as_secs_f64() > show.cues[3].time.as_secs_f64());
}
#[test]
fn test_end_to_end_non_zero_start_offset() {
let content = r#"tempo {
start: 5.0s
bpm: 120
time_signature: 4/4
}
show "Start Offset Test" {
@1/1
front_wash: static color: "blue", duration: 5s
}"#;
let result = parse_light_shows(content);
if let Err(e) = &result {
println!("Parse error: {}", e);
}
assert!(result.is_ok());
let shows = result.unwrap();
let show = shows.get("Start Offset Test").unwrap();
let actual_time = show.cues[0].time.as_secs_f64();
if let Some(tm) = &show.tempo_map {
println!(
"Start offset test: tempo_map.start_offset = {:?}, expected 5.0s, got {}s",
tm.start_offset, actual_time
);
}
assert!(
(actual_time - 5.0).abs() < 0.001,
"Expected 5.0s, got {}s",
actual_time
);
}
#[test]
fn test_end_to_end_measure_notation_without_tempo_error() {
let content = r#"show "No Tempo Test" {
@1/1
front_wash: static color: "blue", duration: 5s
}"#;
let result = parse_light_shows(content);
assert!(
result.is_err(),
"Measure notation should require tempo section"
);
if let Err(e) = result {
assert!(
e.to_string().contains("tempo"),
"Error should mention tempo"
);
}
}
#[test]
fn test_end_to_end_beat_duration_without_tempo_error() {
let content = r#"show "No Tempo Duration Test" {
@00:00.000
front_wash: static color: "blue", duration: 4beats
}"#;
let result = parse_light_shows(content);
if result.is_ok() {
println!("WARNING: Parsing succeeded, but should have failed");
}
assert!(
result.is_err(),
"Beat durations should require tempo section"
);
if let Err(e) = result {
let err_msg = e.to_string();
println!("Error message: {}", err_msg);
assert!(
err_msg.contains("tempo") || err_msg.contains("Beat"),
"Error should mention tempo or beats, got: {}",
err_msg
);
}
}
#[test]
fn test_end_to_end_tempo_map_is_present() {
let content = r#"tempo {
start: 0.0s
bpm: 120
time_signature: 4/4
}
show "Tempo Map Test" {
@1/1
front_wash: static color: "blue", duration: 5s
}"#;
let result = parse_light_shows(content);
if let Err(e) = &result {
println!("Parse error: {}", e);
}
assert!(result.is_ok());
let shows = result.unwrap();
let show = shows.get("Tempo Map Test").unwrap();
assert!(show.tempo_map.is_some(), "Tempo map should be present");
let tempo_map = show.tempo_map.as_ref().unwrap();
assert_eq!(tempo_map.initial_bpm, 120.0);
assert_eq!(tempo_map.initial_time_signature.numerator, 4);
assert_eq!(tempo_map.initial_time_signature.denominator, 4);
}
#[test]
fn test_end_to_end_mixed_absolute_and_measure_timing() {
let content = r#"tempo {
start: 0.0s
bpm: 120
time_signature: 4/4
}
show "Mixed Timing Test" {
@00:00.000
front_wash: static color: "blue", duration: 5s
@1/1
back_wash: static color: "red", duration: 5s
@00:02.000
side_wash: static color: "green", duration: 5s
@2/1
top_wash: static color: "yellow", duration: 5s
}"#;
let result = parse_light_shows(content);
if let Err(e) = &result {
println!("Parse error: {}", e);
}
assert!(result.is_ok());
let shows = result.unwrap();
let show = shows.get("Mixed Timing Test").unwrap();
assert_eq!(show.cues.len(), 4);
assert!((show.cues[0].time.as_secs_f64() - 0.0).abs() < 0.001);
assert!((show.cues[1].time.as_secs_f64() - 0.0).abs() < 0.001);
assert!((show.cues[2].time.as_secs_f64() - 2.0).abs() < 0.001);
assert!((show.cues[3].time.as_secs_f64() - 2.0).abs() < 0.001);
}
#[test]
fn test_end_to_end_gradual_tempo_transition() {
let content = r#"tempo {
start: 0.0s
bpm: 120
time_signature: 4/4
changes: [
@4/1 { bpm: 140, transition: 4 }
]
}
show "Gradual Transition Test" {
@4/1
front_wash: static color: "blue", duration: 5s
@6/1
back_wash: static color: "red", duration: 5s
}"#;
let result = parse_light_shows(content);
if let Err(e) = &result {
println!("Parse error: {}", e);
}
assert!(result.is_ok());
let shows = result.unwrap();
let show = shows.get("Gradual Transition Test").unwrap();
assert!(show.tempo_map.is_some());
let tempo_map = show.tempo_map.as_ref().unwrap();
assert_eq!(tempo_map.changes.len(), 1);
match tempo_map.changes[0].transition {
TempoTransition::Beats(beats, _) => assert_eq!(beats, 4.0),
_ => panic!("Expected Beats transition"),
}
}
#[test]
fn test_end_to_end_bpm_interpolation_during_gradual_transition() {
let content = r#"tempo {
start: 0.0s
bpm: 120
time_signature: 4/4
changes: [
@4/1 { bpm: 180, transition: 4 }
]
}
show "BPM Interpolation Test" {
@4/1
front_wash: static color: "blue", duration: 5s
}"#;
let result = parse_light_shows(content);
assert!(result.is_ok());
let shows = result.unwrap();
let show = shows.get("BPM Interpolation Test").unwrap();
let tempo_map = show.tempo_map.as_ref().unwrap();
let change_time = tempo_map.changes[0].position.absolute_time().unwrap();
let bpm_start = tempo_map.bpm_at_time(change_time, 0.0);
assert!(
(bpm_start - 120.0).abs() < 0.1,
"BPM at transition start should be 120"
);
let mid_time = change_time + Duration::from_secs(1); let bpm_mid = tempo_map.bpm_at_time(mid_time, 0.0);
assert!(
(bpm_mid - 150.0).abs() < 1.0,
"BPM at transition midpoint should be ~150, got {}",
bpm_mid
);
let end_time = change_time + Duration::from_secs(3); let bpm_end = tempo_map.bpm_at_time(end_time, 0.0);
assert!(
(bpm_end - 180.0).abs() < 0.1,
"BPM after transition should be 180"
);
}
#[test]
fn test_end_to_end_file_level_tempo_applies_to_multiple_shows() {
let content = r#"tempo {
start: 0.0s
bpm: 120
time_signature: 4/4
}
show "Show 1" {
@1/1
front_wash: static color: "blue", duration: 5s
}
show "Show 2" {
@2/1
back_wash: static color: "red", duration: 5s
}"#;
let result = parse_light_shows(content);
assert!(result.is_ok());
let shows = result.unwrap();
let show1 = shows.get("Show 1").unwrap();
let show2 = shows.get("Show 2").unwrap();
assert!(show1.tempo_map.is_some(), "Show 1 should have tempo map");
assert!(show2.tempo_map.is_some(), "Show 2 should have tempo map");
assert_eq!(show1.tempo_map.as_ref().unwrap().initial_bpm, 120.0);
assert_eq!(show2.tempo_map.as_ref().unwrap().initial_bpm, 120.0);
assert!((show1.cues[0].time.as_secs_f64() - 0.0).abs() < 0.001);
assert!((show2.cues[0].time.as_secs_f64() - 2.0).abs() < 0.001);
}
#[test]
fn test_end_to_end_show_specific_tempo_overrides_global() {
let content = r#"tempo {
start: 0.0s
bpm: 120
time_signature: 4/4
}
show "Show With Own Tempo" {
tempo {
start: 0.0s
bpm: 60
time_signature: 4/4
}
@1/1
front_wash: static color: "blue", duration: 5s
}
show "Show Using Global Tempo" {
@1/1
back_wash: static color: "red", duration: 5s
}"#;
let result = parse_light_shows(content);
if let Err(e) = &result {
println!("Parse error: {}", e);
}
assert!(result.is_ok(), "Parsing should succeed");
let shows = result.unwrap();
let show1 = shows.get("Show With Own Tempo").unwrap();
let show2 = shows.get("Show Using Global Tempo").unwrap();
assert_eq!(show1.tempo_map.as_ref().unwrap().initial_bpm, 60.0);
assert_eq!(show2.tempo_map.as_ref().unwrap().initial_bpm, 120.0);
let show1_time = show1.cues[0].time.as_secs_f64();
let show2_time = show2.cues[0].time.as_secs_f64();
assert!(
(show1_time - 0.0).abs() < 0.001,
"Show 1 measure 1/1 should be 0.0s"
);
assert!(
(show2_time - 0.0).abs() < 0.001,
"Show 2 measure 1/1 should be 0.0s"
);
assert_eq!(show1.tempo_map.as_ref().unwrap().initial_bpm, 60.0);
assert_eq!(show2.tempo_map.as_ref().unwrap().initial_bpm, 120.0);
}
#[test]
fn test_end_to_end_beat_duration_during_gradual_transition() {
let content = r#"tempo {
start: 0.0s
bpm: 120
time_signature: 4/4
changes: [
@4/1 { bpm: 180, transition: 4 }
]
}
show "Beat Duration During Transition" {
@4/1
front_wash: static color: "blue", duration: 2beats
}"#;
let result = parse_light_shows(content);
assert!(result.is_ok());
let shows = result.unwrap();
let show = shows.get("Beat Duration During Transition").unwrap();
let effect = &show.cues[0].effects[0];
let duration = effect.effect_type.duration();
assert!(
duration.as_secs_f64() > 0.85 && duration.as_secs_f64() < 0.95,
"2 beats during transition should integrate through curve: expected ~0.899s, got {}s",
duration.as_secs_f64()
);
}
#[test]
fn test_end_to_end_absolute_time_tempo_changes() {
let content = r#"tempo {
start: 0.0s
bpm: 120
time_signature: 4/4
changes: [
@00:06.000 { bpm: 60 }
]
}
show "Absolute Time Tempo Change" {
@1/1
front_wash: static color: "blue", duration: 5s
@4/1
back_wash: static color: "red", duration: 5s
@8/1
side_wash: static color: "green", duration: 5s
}"#;
let result = parse_light_shows(content);
assert!(result.is_ok());
let shows = result.unwrap();
let show = shows.get("Absolute Time Tempo Change").unwrap();
let tempo_map = show.tempo_map.as_ref().unwrap();
assert!((show.cues[0].time.as_secs_f64() - 0.0).abs() < 0.001);
assert!((show.cues[1].time.as_secs_f64() - 6.0).abs() < 0.001);
let measure8_time = show.cues[2].time.as_secs_f64();
println!("Measure 8 time: {}s (expected ~14.0s, but calculation may vary with absolute time tempo changes)", measure8_time);
assert!(
measure8_time > show.cues[1].time.as_secs_f64(),
"Measure 8 should be after measure 4, got {}s",
measure8_time
);
assert_eq!(tempo_map.changes.len(), 1);
let change_time = tempo_map.changes[0].position.absolute_time().unwrap();
assert!((change_time.as_secs_f64() - 6.0).abs() < 0.001);
}
#[test]
fn test_end_to_end_duration_spanning_tempo_change() {
let content = r#"tempo {
start: 0.0s
bpm: 120
time_signature: 4/4
changes: [
@4/1 { bpm: 60 }
]
}
show "Duration Spanning Change" {
@3/1
front_wash: static color: "blue", duration: 8beats
}"#;
let result = parse_light_shows(content);
assert!(result.is_ok());
let shows = result.unwrap();
let show = shows.get("Duration Spanning Change").unwrap();
let effect = &show.cues[0].effects[0];
let duration = effect.effect_type.duration();
let expected_duration = 4.0 * 60.0 / 120.0 + 4.0 * 60.0 / 60.0; assert!(
(duration.as_secs_f64() - expected_duration).abs() < 0.01,
"Duration should integrate through tempo change: expected ~{}s, got {}s",
expected_duration,
duration.as_secs_f64()
);
}
#[test]
fn test_end_to_end_duration_spanning_gradual_tempo_transition() {
let content = r#"tempo {
start: 0.0s
bpm: 120
time_signature: 4/4
changes: [
@1/3 { bpm: 180, transition: 4 }
]
}
show "Duration Spanning Gradual Transition" {
@1/1
front_wash: static color: "blue", duration: 8beats
}"#;
let result = parse_light_shows(content);
assert!(result.is_ok());
let shows = result.unwrap();
let show = shows.get("Duration Spanning Gradual Transition").unwrap();
let effect = &show.cues[0].effects[0];
let duration = effect.effect_type.duration();
let time_before = 2.0 * 60.0 / 120.0; let avg_bpm_during_transition = (120.0 + 180.0) / 2.0; let transition_time = 4.0 * 60.0 / avg_bpm_during_transition; let time_after = 2.0 * 60.0 / 180.0; let expected_duration = time_before + transition_time + time_after;
assert!(
(duration.as_secs_f64() - expected_duration).abs() < 0.1,
"Duration should integrate through gradual transition: expected ~{}s, got {}s",
expected_duration,
duration.as_secs_f64()
);
}
#[test]
fn test_end_to_end_duration_starting_mid_transition() {
let content = r#"tempo {
start: 0.0s
bpm: 120
time_signature: 4/4
changes: [
@1/1 { bpm: 180, transition: 4 }
]
}
show "Duration Mid Transition" {
@1/2.5
front_wash: static color: "blue", duration: 2beats
}"#;
let result = parse_light_shows(content);
assert!(result.is_ok());
let shows = result.unwrap();
let show = shows.get("Duration Mid Transition").unwrap();
let effect = &show.cues[0].effects[0];
let duration = effect.effect_type.duration();
assert!(
duration.as_secs_f64() > 0.6 && duration.as_secs_f64() < 0.9,
"Duration starting mid-transition should integrate correctly: got {}s",
duration.as_secs_f64()
);
}
#[test]
fn test_end_to_end_pulse_duration_spanning_tempo_change() {
let content = r#"tempo {
start: 0.0s
bpm: 120
time_signature: 4/4
changes: [
@4/1 { bpm: 60 }
]
}
show "Pulse Duration Spanning Change" {
@3/1
front_wash: pulse color: "blue", frequency: 2, duration: 8beats
}"#;
let result = parse_light_shows(content);
assert!(result.is_ok());
let shows = result.unwrap();
let show = shows.get("Pulse Duration Spanning Change").unwrap();
let effect = &show.cues[0].effects[0];
let duration = effect.effect_type.duration();
let expected_duration = 4.0 * 60.0 / 120.0 + 4.0 * 60.0 / 60.0; assert!(
(duration.as_secs_f64() - expected_duration).abs() < 0.01,
"Pulse duration should integrate through tempo change: expected ~{}s, got {}s",
expected_duration,
duration.as_secs_f64()
);
}
#[test]
fn test_end_to_end_strobe_duration_spanning_tempo_change() {
let content = r#"tempo {
start: 0.0s
bpm: 120
time_signature: 4/4
changes: [
@4/1 { bpm: 60 }
]
}
show "Strobe Duration Spanning Change" {
@3/1
front_wash: strobe frequency: 4, duration: 8beats
}"#;
let result = parse_light_shows(content);
assert!(result.is_ok());
let shows = result.unwrap();
let show = shows.get("Strobe Duration Spanning Change").unwrap();
let effect = &show.cues[0].effects[0];
let duration = effect.effect_type.duration();
let expected_duration = 4.0 * 60.0 / 120.0 + 4.0 * 60.0 / 60.0; assert!(
(duration.as_secs_f64() - expected_duration).abs() < 0.01,
"Strobe duration should integrate through tempo change: expected ~{}s, got {}s",
expected_duration,
duration.as_secs_f64()
);
}
#[test]
fn test_end_to_end_pulse_duration_spanning_gradual_transition() {
let content = r#"tempo {
start: 0.0s
bpm: 120
time_signature: 4/4
changes: [
@1/3 { bpm: 180, transition: 4 }
]
}
show "Pulse Duration Spanning Gradual Transition" {
@1/1
front_wash: pulse color: "blue", frequency: 2, duration: 8beats
}"#;
let result = parse_light_shows(content);
assert!(result.is_ok());
let shows = result.unwrap();
let show = shows
.get("Pulse Duration Spanning Gradual Transition")
.unwrap();
let effect = &show.cues[0].effects[0];
let duration = effect.effect_type.duration();
let time_before = 2.0 * 60.0 / 120.0; let avg_bpm_during_transition = (120.0 + 180.0) / 2.0; let transition_time = 4.0 * 60.0 / avg_bpm_during_transition; let time_after = 2.0 * 60.0 / 180.0; let expected_duration = time_before + transition_time + time_after;
assert!(
(duration.as_secs_f64() - expected_duration).abs() < 0.1,
"Pulse duration should integrate through gradual transition: expected ~{}s, got {}s",
expected_duration,
duration.as_secs_f64()
);
}
#[test]
fn test_end_to_end_strobe_duration_spanning_gradual_transition() {
let content = r#"tempo {
start: 0.0s
bpm: 120
time_signature: 4/4
changes: [
@1/3 { bpm: 180, transition: 4 }
]
}
show "Strobe Duration Spanning Gradual Transition" {
@1/1
front_wash: strobe frequency: 4, duration: 8beats
}"#;
let result = parse_light_shows(content);
assert!(result.is_ok());
let shows = result.unwrap();
let show = shows
.get("Strobe Duration Spanning Gradual Transition")
.unwrap();
let effect = &show.cues[0].effects[0];
let duration = effect.effect_type.duration();
let time_before = 2.0 * 60.0 / 120.0; let avg_bpm_during_transition = (120.0 + 180.0) / 2.0; let transition_time = 4.0 * 60.0 / avg_bpm_during_transition; let time_after = 2.0 * 60.0 / 180.0; let expected_duration = time_before + transition_time + time_after;
assert!(
(duration.as_secs_f64() - expected_duration).abs() < 0.1,
"Strobe duration should integrate through gradual transition: expected ~{}s, got {}s",
expected_duration,
duration.as_secs_f64()
);
}
#[test]
fn test_end_to_end_measure_based_transition() {
let content = r#"tempo {
start: 0.0s
bpm: 120
time_signature: 4/4
changes: [
@4/1 { bpm: 180, transition: 2m }
]
}
show "Measure Transition Test" {
@4/1
front_wash: static color: "blue", duration: 5s
}"#;
let result = parse_light_shows(content);
assert!(result.is_ok());
let shows = result.unwrap();
let show = shows.get("Measure Transition Test").unwrap();
let tempo_map = show.tempo_map.as_ref().unwrap();
assert_eq!(tempo_map.changes.len(), 1);
match tempo_map.changes[0].transition {
TempoTransition::Measures(m, _) => assert_eq!(m, 2.0),
_ => panic!("Expected Measures transition"),
}
let change_time = tempo_map.changes[0].position.absolute_time().unwrap();
let bpm_start = tempo_map.bpm_at_time(change_time, 0.0);
assert!((bpm_start - 120.0).abs() < 0.1);
let mid_time = change_time + Duration::from_secs(2); let bpm_mid = tempo_map.bpm_at_time(mid_time, 0.0);
assert!(
(bpm_mid - 150.0).abs() < 1.0,
"BPM at transition midpoint should be ~150, got {}",
bpm_mid
);
let end_time = change_time + Duration::from_secs(5); let bpm_end = tempo_map.bpm_at_time(end_time, 0.0);
assert!((bpm_end - 180.0).abs() < 0.1);
}
#[test]
fn test_end_to_end_multiple_file_level_tempo_sections() {
let content = r#"tempo {
start: 0.0s
bpm: 120
time_signature: 4/4
}
tempo {
start: 0.0s
bpm: 60
time_signature: 4/4
}
show "Multiple Tempo Test" {
@1/1
front_wash: static color: "blue", duration: 5s
}"#;
let result = parse_light_shows(content);
assert!(result.is_ok());
let shows = result.unwrap();
let show = shows.get("Multiple Tempo Test").unwrap();
assert!(show.tempo_map.is_some());
assert_eq!(show.tempo_map.as_ref().unwrap().initial_bpm, 60.0);
}
#[test]
fn test_end_to_end_multiple_tempo_sections_in_show() {
let content = r#"show "Multiple Show Tempo" {
tempo {
start: 0.0s
bpm: 120
time_signature: 4/4
}
tempo {
start: 0.0s
bpm: 60
time_signature: 4/4
}
@1/1
front_wash: static color: "blue", duration: 5s
}"#;
let result = parse_light_shows(content);
assert!(result.is_ok());
let shows = result.unwrap();
let show = shows.get("Multiple Show Tempo").unwrap();
assert!(show.tempo_map.is_some());
assert_eq!(show.tempo_map.as_ref().unwrap().initial_bpm, 60.0);
}
#[test]
fn test_end_to_end_fractional_measure_duration() {
let content = r#"tempo {
start: 0.0s
bpm: 120
time_signature: 4/4
}
show "Fractional Measure Duration" {
@1/1
front_wash: static color: "blue", duration: 1.5measures
}"#;
let result = parse_light_shows(content);
assert!(result.is_ok());
let shows = result.unwrap();
let show = shows.get("Fractional Measure Duration").unwrap();
let effect = &show.cues[0].effects[0];
let duration = effect.effect_type.duration();
assert!(
(duration.as_secs_f64() - 3.0).abs() < 0.001,
"1.5 measures should be 3.0s at 120 BPM in 4/4, got {}s",
duration.as_secs_f64()
);
}
#[test]
fn test_end_to_end_consecutive_gradual_transitions() {
let content = r#"tempo {
start: 0.0s
bpm: 120
time_signature: 4/4
changes: [
@4/1 { bpm: 140, transition: 2 },
@6/1 { bpm: 160, transition: 2 }
]
}
show "Consecutive Transitions" {
@4/1
front_wash: static color: "blue", duration: 5s
@6/1
back_wash: static color: "red", duration: 5s
}"#;
let result = parse_light_shows(content);
assert!(result.is_ok());
let shows = result.unwrap();
let show = shows.get("Consecutive Transitions").unwrap();
let tempo_map = show.tempo_map.as_ref().unwrap();
assert_eq!(tempo_map.changes.len(), 2);
let change1_time = tempo_map.changes[0].position.absolute_time().unwrap();
let change2_time = tempo_map.changes[1].position.absolute_time().unwrap();
let bpm_before = tempo_map.bpm_at_time(change1_time - Duration::from_millis(100), 0.0);
assert!((bpm_before - 120.0).abs() < 0.1);
let bpm_after1 = tempo_map.bpm_at_time(change1_time + Duration::from_secs(2), 0.0);
assert!((bpm_after1 - 140.0).abs() < 1.0);
let bpm_after2 = tempo_map.bpm_at_time(change2_time + Duration::from_secs(2), 0.0);
assert!((bpm_after2 - 160.0).abs() < 1.0);
}
#[test]
fn test_end_to_end_measure_transition_with_time_signature_change() {
let content = r#"tempo {
start: 0.0s
bpm: 120
time_signature: 4/4
changes: [
@4/1 { bpm: 140, transition: 2m },
@5/1 { time_signature: 3/4 }
]
}
show "Measure Transition Time Sig Change" {
@4/1
front_wash: static color: "blue", duration: 5s
}"#;
let result = parse_light_shows(content);
assert!(result.is_ok());
let shows = result.unwrap();
let show = shows.get("Measure Transition Time Sig Change").unwrap();
let tempo_map = show.tempo_map.as_ref().unwrap();
let change_time = tempo_map.changes[0].position.absolute_time().unwrap();
let bpm_after = tempo_map.bpm_at_time(change_time + Duration::from_secs(5), 0.0);
assert!((bpm_after - 140.0).abs() < 1.0);
}
#[test]
fn test_end_to_end_empty_tempo_section_with_measure_timing() {
let content = r#"tempo {
}
show "Empty Tempo Test" {
@1/1
front_wash: static color: "blue", duration: 5s
@2/1
back_wash: static color: "red", duration: 5s
}"#;
let result = parse_light_shows(content);
assert!(
result.is_ok(),
"Empty tempo section should work with defaults"
);
let shows = result.unwrap();
let show = shows.get("Empty Tempo Test").unwrap();
assert!(show.tempo_map.is_some());
let tempo_map = show.tempo_map.as_ref().unwrap();
assert_eq!(tempo_map.initial_bpm, 120.0);
assert_eq!(tempo_map.initial_time_signature.numerator, 4);
assert_eq!(tempo_map.initial_time_signature.denominator, 4);
assert!((show.cues[0].time.as_secs_f64() - 0.0).abs() < 0.001);
assert!((show.cues[1].time.as_secs_f64() - 2.0).abs() < 0.001);
}
#[test]
fn test_end_to_end_incomplete_tempo_section_with_measure_timing() {
let content = r#"tempo {
start: 0.0s
}
show "Incomplete Tempo Test" {
@1/1
front_wash: static color: "blue", duration: 5s
}"#;
let result = parse_light_shows(content);
assert!(
result.is_ok(),
"Incomplete tempo section should use defaults"
);
let shows = result.unwrap();
let show = shows.get("Incomplete Tempo Test").unwrap();
assert!(show.tempo_map.is_some());
let tempo_map = show.tempo_map.as_ref().unwrap();
assert_eq!(tempo_map.initial_bpm, 120.0); assert_eq!(tempo_map.initial_time_signature.numerator, 4); assert_eq!(tempo_map.initial_time_signature.denominator, 4); }
#[test]
fn test_end_to_end_negative_start_offset_rejected() {
let content = r#"tempo {
start: -5.0s
bpm: 120
time_signature: 4/4
}
show "Negative Start Test" {
@1/1
front_wash: static color: "blue", duration: 5s
}"#;
let result = parse_light_shows(content);
assert!(
result.is_err(),
"Negative start offset should fail to parse"
);
if let Err(e) = result {
let error_msg = e.to_string();
println!("Error message: {}", error_msg);
assert!(
error_msg.contains("parse") || error_msg.contains("DSL") || error_msg.contains("error"),
"Error should indicate parsing failure"
);
}
}
#[test]
fn test_t_end_to_end_very_high_measure_numbers() {
let content = r#"tempo {
start: 0.0s
bpm: 120
time_signature: 4/4
}
show "High Measures Test" {
@1000/1
front_wash: static color: "blue", duration: 5s
@5000/1
back_wash: static color: "red", duration: 5s
}"#;
let result = parse_light_shows(content);
assert!(result.is_ok(), "High measure numbers should work");
let shows = result.unwrap();
let show = shows.get("High Measures Test").unwrap();
let time1 = show.cues[0].time.as_secs_f64();
let time2 = show.cues[1].time.as_secs_f64();
assert!(
time1 > 1990.0 && time1 < 2010.0,
"Measure 1000 should be around 1998s, got {}s",
time1
);
assert!(
time2 > 9990.0 && time2 < 10010.0,
"Measure 5000 should be around 9998s, got {}s",
time2
);
assert!(time2 > time1, "Measure 5000 should be after measure 1000");
}
#[test]
fn test_end_to_end_transition_spanning_multiple_changes() {
let content = r#"tempo {
start: 0.0s
bpm: 120
time_signature: 4/4
changes: [
@4/1 { bpm: 140, transition: 8 },
@7/1 { bpm: 160 },
@10/1 { time_signature: 3/4 }
]
}
show "Transition Spanning Changes" {
@4/1
front_wash: static color: "blue", duration: 5s
@10/1
back_wash: static color: "red", duration: 5s
}"#;
let result = parse_light_shows(content);
assert!(result.is_ok());
let shows = result.unwrap();
let show = shows.get("Transition Spanning Changes").unwrap();
let tempo_map = show.tempo_map.as_ref().unwrap();
assert_eq!(tempo_map.changes.len(), 3);
let change1_time = tempo_map.changes[0].position.absolute_time().unwrap();
let change2_time = tempo_map.changes[1].position.absolute_time().unwrap();
let early_time = change1_time + Duration::from_secs(1); let bpm_early = tempo_map.bpm_at_time(early_time, 0.0);
assert!(
(bpm_early - 125.0).abs() < 2.0,
"BPM early in transition should be ~125, got {}",
bpm_early
);
let mid_time = change1_time + Duration::from_secs(2); let bpm_mid = tempo_map.bpm_at_time(mid_time, 0.0);
assert!(
(bpm_mid - 130.0).abs() < 2.0,
"BPM at transition midpoint should be ~130, got {}",
bpm_mid
);
let after_transition = change1_time + Duration::from_secs(5); let bpm_after_transition = tempo_map.bpm_at_time(after_transition, 0.0);
assert!(
(bpm_after_transition - 140.0).abs() < 1.0,
"BPM after transition completes should be 140, got {}",
bpm_after_transition
);
let after_change2 = change2_time + Duration::from_millis(100);
let bpm_after2 = tempo_map.bpm_at_time(after_change2, 0.0);
assert!((bpm_after2 - 160.0).abs() < 0.1);
}