use crate::lighting::effects::Color;
use crate::lighting::parser::utils::parse_time_string;
use crate::lighting::parser::*;
use std::time::Duration;
#[test]
fn test_parse_multiple_shows() {
let content = r#"show "Show 1" {
@00:00.000
front_wash: static color: "blue", dimmer: 60%, duration: 5s
}
show "Show 2" {
@00:00.000
back_wash: static color: "red", dimmer: 80%, duration: 5s
}"#;
let result = parse_light_shows(content);
if let Err(e) = &result {
println!("Parser error: {}", e);
}
assert!(result.is_ok());
let shows = result.unwrap();
println!("Found {} shows", shows.len());
assert_eq!(shows.len(), 2);
assert!(shows.contains_key("Show 1"));
assert!(shows.contains_key("Show 2"));
}
#[test]
fn test_parse_invalid_syntax() {
let content = r#"show "Invalid Show" {
@00:00.000
front_wash: invalid_effect color: "blue"
}"#;
let result = parse_light_shows(content);
assert!(result.is_err());
}
#[test]
fn test_parse_malformed_timing() {
let content = r#"show "Invalid Timing" {
@invalid_time
front_wash: static color: "blue", dimmer: 60%, duration: 5s
}"#;
let result = parse_light_shows(content);
assert!(result.is_err());
}
#[test]
fn test_parse_empty_show() {
let content = r#"show "Empty Show" {
}"#;
let result = parse_light_shows(content);
if let Err(e) = &result {
println!("Parse error: {}", e);
}
assert!(result.is_ok());
let shows = result.unwrap();
assert_eq!(shows.len(), 1);
let show = &shows["Empty Show"];
assert_eq!(show.cues.len(), 0);
}
#[test]
fn test_color_parsing() {
let red = Color::from_hex("#ff0000").unwrap();
assert_eq!(red.r, 255);
assert_eq!(red.g, 0);
assert_eq!(red.b, 0);
let blue = Color::from_name("blue").unwrap();
assert_eq!(blue.r, 0);
assert_eq!(blue.g, 0);
assert_eq!(blue.b, 255);
assert!(Color::from_hex("invalid").is_err());
assert!(Color::from_name("invalid").is_err());
}
#[test]
fn test_time_parsing() {
let time1 = parse_time_string("01:30.500").unwrap();
assert_eq!(time1.as_millis(), 90500);
let time2 = parse_time_string("30.500").unwrap();
assert_eq!(time2.as_millis(), 30500);
let time3 = parse_time_string("00:00.000").unwrap();
assert_eq!(time3.as_millis(), 0);
}
#[test]
fn test_parse_crossfade_example() {
let content = r#"show "Crossfade Test" {
@00:00.000
front_wash: static color: "blue", up_time: 2s, down_time: 1s, hold_time: 5s
}"#;
let result = parse_light_shows(content);
if let Err(e) = &result {
println!("Parser error: {}", e);
}
assert!(result.is_ok());
let shows = result.unwrap();
assert_eq!(shows.len(), 1);
let show = shows.get("Crossfade Test").unwrap();
assert_eq!(show.name, "Crossfade Test");
assert_eq!(show.cues.len(), 1);
let cue = &show.cues[0];
assert_eq!(cue.time, Duration::from_millis(0));
assert_eq!(cue.effects.len(), 1);
let effect = &cue.effects[0];
assert_eq!(effect.groups, vec!["front_wash"]);
assert_eq!(effect.up_time, Some(Duration::from_secs(2)));
assert_eq!(effect.down_time, Some(Duration::from_secs(1)));
println!(
"Timing parsing works! up_time: {:?}, down_time: {:?}",
effect.up_time, effect.down_time
);
}
#[test]
fn test_parse_zero_fade() {
let content = r#"show "Zero Fade Test" {
@00:00.000
front_wash: static color: "blue", up_time: 0s, down_time: 0s, hold_time: 5s
}"#;
let result = parse_light_shows(content);
if let Err(e) = &result {
println!("Parser error: {}", e);
}
assert!(result.is_ok());
let shows = result.unwrap();
let show = shows.get("Zero Fade Test").unwrap();
let effect = &show.cues[0].effects[0];
assert_eq!(effect.up_time, Some(Duration::from_secs(0)));
assert_eq!(effect.down_time, Some(Duration::from_secs(0)));
println!(
"Zero timing parsing works! up_time: {:?}, down_time: {:?}",
effect.up_time, effect.down_time
);
}
#[test]
fn test_parse_layering_partial() {
let content = r#"show "Effect Layering Demo" {
@00:00.000
# Background layer: Static blue color with 2 second fade in
front_wash: static color: "blue", dimmer: 100%, layer: background, blend_mode: replace, up_time: 2s, hold_time: 5s
}"#;
let result = parse_light_shows(content);
if let Err(e) = &result {
println!("Parser error: {}", e);
}
assert!(result.is_ok());
let shows = result.unwrap();
let show = shows.get("Effect Layering Demo").unwrap();
let effect = &show.cues[0].effects[0];
assert_eq!(effect.up_time, Some(Duration::from_secs(2)));
println!(
"Layering partial parsing works! up_time: {:?}",
effect.up_time
);
}
#[test]
fn test_parse_layering_2lines() {
let content = r#"show "Effect Layering Demo" {
@00:00.000
# Background layer: Static blue color with 2 second fade in
front_wash: static color: "blue", dimmer: 100%, layer: background, blend_mode: replace, up_time: 2s, hold_time: 5s
@00:02.000
# Midground layer: Dimmer effect that slowly dims the blue with crossfades
front_wash: dimmer start_level: 1.0, end_level: 0.5, duration: 5s, layer: midground, blend_mode: multiply, up_time: 1s, down_time: 1s
}"#;
let result = parse_light_shows(content);
if let Err(e) = &result {
println!("Parser error: {}", e);
}
assert!(result.is_ok());
let shows = result.unwrap();
let show = shows.get("Effect Layering Demo").unwrap();
assert_eq!(show.cues.len(), 2);
println!("Layering 2 lines parsing works!");
}
#[test]
fn test_parse_layering_3lines() {
let content = r#"show "Effect Layering Demo" {
@00:00.000
# Background layer: Static blue color with 2 second fade in
front_wash: static color: "blue", dimmer: 100%, layer: background, blend_mode: replace, up_time: 2s, hold_time: 5s
@00:02.000
# Midground layer: Dimmer effect that slowly dims the blue with crossfades
front_wash: dimmer start_level: 1.0, end_level: 0.5, duration: 5s, layer: midground, blend_mode: multiply, up_time: 1s, down_time: 1s
@00:04.000
# Foreground layer: Strobe effect on top of the dimmed blue with crossfades
front_wash: strobe frequency: 2, layer: foreground, blend_mode: overlay, up_time: 0.5s, down_time: 0.5s, duration: 6s
}"#;
let result = parse_light_shows(content);
if let Err(e) = &result {
println!("Parser error: {}", e);
}
assert!(result.is_ok());
let shows = result.unwrap();
let show = shows.get("Effect Layering Demo").unwrap();
assert_eq!(show.cues.len(), 3);
println!("Layering 3 lines parsing works!");
}
#[test]
fn test_parse_strobe_simple() {
let content = r#"show "Test" {
@00:00.000
front_wash: strobe frequency: 2, up_time: 0.5s, down_time: 0.5s, hold_time: 5s
}"#;
let result = parse_light_shows(content);
if let Err(e) = &result {
println!("Parser error: {}", e);
}
assert!(result.is_ok());
let shows = result.unwrap();
let show = shows.get("Test").unwrap();
let effect = &show.cues[0].effects[0];
assert_eq!(effect.up_time, Some(Duration::from_millis(500)));
assert_eq!(effect.down_time, Some(Duration::from_millis(500)));
println!(
"Strobe simple parsing works! up_time: {:?}, down_time: {:?}",
effect.up_time, effect.down_time
);
}
#[test]
fn test_parse_strobe_no_crossfade() {
let content = r#"show "Test" {
@00:00.000
front_wash: strobe frequency: 2, duration: 5s
}"#;
let result = parse_light_shows(content);
if let Err(e) = &result {
println!("Parser error: {}", e);
}
assert!(result.is_ok());
let shows = result.unwrap();
let show = shows.get("Test").unwrap();
let effect = &show.cues[0].effects[0];
assert_eq!(effect.up_time, None);
assert_eq!(effect.down_time, None);
println!("Strobe no crossfade parsing works!");
}
#[test]
fn test_parse_strobe_crossfade_minimal() {
let content = r#"show "Test" {
@00:00.000
front_wash: strobe frequency: 2, up_time: 0.5s, hold_time: 5s
}"#;
let result = parse_light_shows(content);
if let Err(e) = &result {
println!("Parser error: {}", e);
}
assert!(result.is_ok());
let shows = result.unwrap();
let show = shows.get("Test").unwrap();
let effect = &show.cues[0].effects[0];
assert_eq!(effect.up_time, Some(Duration::from_millis(500)));
assert_eq!(effect.down_time, None);
println!(
"Strobe timing minimal parsing works! up_time: {:?}",
effect.up_time
);
}
#[test]
fn test_parse_static_crossfade() {
let content = r#"show "Test" {
@00:00.000
front_wash: static color: "blue", up_time: 0.5s, down_time: 0.5s, hold_time: 5s
}"#;
let result = parse_light_shows(content);
if let Err(e) = &result {
println!("Parser error: {}", e);
}
assert!(result.is_ok());
let shows = result.unwrap();
let show = shows.get("Test").unwrap();
let effect = &show.cues[0].effects[0];
assert_eq!(effect.up_time, Some(Duration::from_millis(500)));
assert_eq!(effect.down_time, Some(Duration::from_millis(500)));
println!(
"Static timing parsing works! up_time: {:?}, down_time: {:?}",
effect.up_time, effect.down_time
);
}
#[test]
fn test_parse_fade_in_only() {
let content = r#"show "Test" {
@00:00.000
front_wash: static color: "blue", up_time: 0.5s, hold_time: 5s
}"#;
let result = parse_light_shows(content);
if let Err(e) = &result {
println!("Parser error: {}", e);
}
assert!(result.is_ok());
let shows = result.unwrap();
let show = shows.get("Test").unwrap();
let effect = &show.cues[0].effects[0];
assert_eq!(effect.up_time, Some(Duration::from_millis(500)));
assert_eq!(effect.down_time, None);
println!("Up time only parsing works! up_time: {:?}", effect.up_time);
}
#[test]
fn test_parse_fade_in_simple() {
let content = r#"show "Test" {
@00:00.000
front_wash: static color: "blue", up_time: 2s, hold_time: 5s
}"#;
let result = parse_light_shows(content);
if let Err(e) = &result {
println!("Parser error: {}", e);
}
assert!(result.is_ok());
let shows = result.unwrap();
let show = shows.get("Test").unwrap();
let effect = &show.cues[0].effects[0];
assert_eq!(effect.up_time, Some(Duration::from_secs(2)));
assert_eq!(effect.down_time, None);
println!(
"Up time simple parsing works! up_time: {:?}",
effect.up_time
);
}
#[test]
fn test_inline_loop() {
let content = r#"show "Test Show" {
tempo {
bpm: 120
time_signature: 4/4
}
@0.0
loop {
@0.0
effect: static, color: "red", duration: 1s
@1.0
effect: static, color: "blue", duration: 1s
}, repeats: 3
}"#;
let result = parse_light_shows(content);
if let Err(e) = &result {
println!("Parser error: {}", e);
}
assert!(result.is_ok());
let shows = result.unwrap();
let show = &shows["Test Show"];
assert!(
show.cues.len() >= 6,
"Expected at least 6 cues, got {}",
show.cues.len()
);
let cue_times: Vec<Duration> = show.cues.iter().map(|c| c.time).collect();
println!("Cue times: {:?}", cue_times);
println!("Number of cues: {}", show.cues.len());
for (i, cue) in show.cues.iter().enumerate() {
println!(
"Cue {}: time={:?}, effects={}",
i,
cue.time,
cue.effects.len()
);
for (j, effect) in cue.effects.iter().enumerate() {
println!(" Effect {}: {:?}", j, effect.effect_type);
}
}
assert_eq!(
show.cues.len(),
6,
"Expected 6 cues, got {}",
show.cues.len()
);
let expected_times = vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0];
let actual_times: Vec<f64> = show.cues.iter().map(|c| c.time.as_secs_f64()).collect();
for expected in expected_times {
assert!(
actual_times.iter().any(|&t| (t - expected).abs() < 0.01),
"Expected a cue at time {}, but got times: {:?}",
expected,
actual_times
);
}
}
#[test]
fn test_inline_loop_respects_base_offset() {
let content = r#"show "Offset Loop" {
tempo { bpm: 120 }
@2.5
loop {
@0.0
band: static, color: "red", duration: 0.5s
@0.5
band: static, color: "blue", duration: 0.5s
}, repeats: 2
}"#;
let shows = parse_light_shows(content).expect("parse should succeed");
let show = &shows["Offset Loop"];
let times: Vec<f64> = show.cues.iter().map(|c| c.time.as_secs_f64()).collect();
let expected = vec![2.5, 3.0, 3.5, 4.0];
for t in expected {
assert!(
times.iter().any(|&actual| (actual - t).abs() < 0.01),
"expected cue at {t}, got {times:?}"
);
}
assert_eq!(times.len(), 4, "expected 4 cues, got {}", times.len());
}
#[test]
fn test_inline_loop_respects_tempo_for_durations() {
let content = r#"show "Tempo Loop" {
tempo { bpm: 120 }
@0.0
loop {
@0.0
band: static, color: "red", duration: 2beats
}, repeats: 2
}"#;
let shows = parse_light_shows(content).expect("parse should succeed");
let show = &shows["Tempo Loop"];
let times: Vec<f64> = show.cues.iter().map(|c| c.time.as_secs_f64()).collect();
let expected_times = vec![0.0, 1.0];
for t in expected_times {
assert!(
times.iter().any(|&actual| (actual - t).abs() < 0.01),
"expected cue at {t}, got {times:?}"
);
}
assert_eq!(times.len(), 2, "expected 2 cues, got {}", times.len());
let dur = show.cues[0].effects[0].total_duration();
assert!(
(dur.as_secs_f64() - 1.0).abs() < 0.01,
"expected duration ~1.0s, got {:?}",
dur
);
}
#[test]
fn test_inline_loop_inside_sequence_preserves_times() {
let content = r#"tempo { bpm: 120 }
sequence "LoopSeq" {
@0.0
loop {
@0.0
effect: static, color: "red", duration: 0.5s
@0.5
effect: static, color: "blue", duration: 0.5s
}, repeats: 2
}
show "UseSeq" {
@0.0
sequence "LoopSeq"
}"#;
let shows = parse_light_shows(content).expect("parse should succeed");
let show = &shows["UseSeq"];
let times: Vec<f64> = show.cues.iter().map(|c| c.time.as_secs_f64()).collect();
let expected = vec![0.0, 0.5, 1.0, 1.5];
for t in expected {
assert!(
times.iter().any(|&actual| (actual - t).abs() < 0.01),
"expected cue at {t}, got {times:?}"
);
}
assert_eq!(times.len(), 4, "expected 4 cues, got {}", times.len());
}
#[test]
fn test_inline_loop_measure_time_positions() {
let content = r#"show "Measure Loop" {
tempo {
bpm: 120
time_signature: 4/4
}
@0.0
loop {
@1/1
effect: static, color: "red", duration: 1s
@1/3
effect: static, color: "blue", duration: 1s
}, repeats: 2
}"#;
let shows = parse_light_shows(content).expect("parse should succeed");
let show = &shows["Measure Loop"];
let times: Vec<f64> = show.cues.iter().map(|c| c.time.as_secs_f64()).collect();
let expected = [0.0, 1.0, 2.0, 3.0];
for t in expected.iter() {
assert!(
times.iter().any(|&actual| (actual - t).abs() < 0.01),
"expected cue at {t}, got {times:?}"
);
}
assert_eq!(times.len(), 4, "expected 4 cues, got {}", times.len());
}
#[test]
fn test_base_effects_with_inline_loop_out_of_order() {
let content = r#"show "Base Effects Loop" {
tempo { bpm: 120 }
@1.0
effect: static, color: "red", duration: 0.5s
loop {
@0.5
effect: static, color: "blue", duration: 0.5s
@0.0
effect: static, color: "green", duration: 0.5s
}, repeats: 1
}"#;
let shows = parse_light_shows(content).expect("parse should succeed");
let show = &shows["Base Effects Loop"];
let times: Vec<f64> = show.cues.iter().map(|c| c.time.as_secs_f64()).collect();
println!("Cue times: {:?}", times);
let cue_at_1_0 = show
.cues
.iter()
.find(|c| (c.time.as_secs_f64() - 1.0).abs() < 0.01);
assert!(cue_at_1_0.is_some(), "Expected a cue at time 1.0");
let cue = cue_at_1_0.unwrap();
assert!(
cue.effects.len() >= 2,
"Expected at least 2 effects at time 1.0 (base + loop), got {}",
cue.effects.len()
);
let cue_at_1_5 = show
.cues
.iter()
.find(|c| (c.time.as_secs_f64() - 1.5).abs() < 0.01);
assert!(cue_at_1_5.is_some(), "Expected a cue at time 1.5");
assert!(
cue_at_1_5.unwrap().effects.len() == 1,
"Cue at 1.5 should only have the loop blue effect, not the base red effect"
);
}
#[test]
fn test_inline_loop_merging_consistent_between_show_and_sequence() {
let show_content = r#"show "Show Test" {
tempo { bpm: 120 }
@0.0
effect: static, color: "red", duration: 0.5s
loop {
@0.0
effect: static, color: "green", duration: 0.5s
}, repeats: 1
}"#;
let sequence_content = r#"tempo { bpm: 120 }
sequence "Seq Test" {
@0.0
effect: static, color: "red", duration: 0.5s
loop {
@0.0
effect: static, color: "green", duration: 0.5s
}, repeats: 1
}
show "Use Seq" {
@0.0
sequence "Seq Test"
}"#;
let shows = parse_light_shows(show_content).expect("show parse should succeed");
let show = &shows["Show Test"];
let seq_shows = parse_light_shows(sequence_content).expect("sequence parse should succeed");
let seq_show = &seq_shows["Use Seq"];
let show_cue_at_0_0 = show
.cues
.iter()
.find(|c| (c.time.as_secs_f64() - 0.0).abs() < 0.01);
let seq_cue_at_0_0 = seq_show
.cues
.iter()
.find(|c| (c.time.as_secs_f64() - 0.0).abs() < 0.01);
assert!(
show_cue_at_0_0.is_some(),
"Show should have a cue at time 0.0"
);
assert!(
seq_cue_at_0_0.is_some(),
"Sequence should have a cue at time 0.0"
);
let show_cue = show_cue_at_0_0.unwrap();
let seq_cue = seq_cue_at_0_0.unwrap();
assert_eq!(
show_cue.effects.len(),
2,
"Show cue at 0.0 should have 2 effects (base + loop merged), got {}",
show_cue.effects.len()
);
assert_eq!(
seq_cue.effects.len(),
2,
"Sequence cue at 0.0 should have 2 effects (base + loop merged), got {}",
seq_cue.effects.len()
);
assert_eq!(
show.cues.len(),
seq_show.cues.len(),
"Show and sequence should produce the same number of cues"
);
}
#[test]
fn test_static_effect_duration_includes_up_and_down_time() {
let content = r#"show "Duration Test" {
tempo { bpm: 120 }
@0.0
effect: static, color: "red", duration: 1s, up_time: 0.5s, down_time: 0.3s
}"#;
let shows = parse_light_shows(content).expect("parse should succeed");
let show = &shows["Duration Test"];
assert_eq!(show.cues.len(), 1, "Should have one cue");
let effect = &show.cues[0].effects[0];
let total_duration = effect.total_duration();
let expected_duration = Duration::from_secs_f64(1.8);
assert!(
(total_duration.as_secs_f64() - expected_duration.as_secs_f64()).abs() < 0.01,
"Expected total duration ~1.8s, got {:?}",
total_duration
);
}
#[test]
fn test_inline_loop_with_static_duration_and_up_time() {
let content = r#"show "Loop Duration Test" {
tempo { bpm: 120 }
@0.0
loop {
@0.0
effect: static, color: "red", duration: 1s, up_time: 0.5s, down_time: 0.3s
}, repeats: 2
}"#;
let shows = parse_light_shows(content).expect("parse should succeed");
let show = &shows["Loop Duration Test"];
let times: Vec<f64> = show.cues.iter().map(|c| c.time.as_secs_f64()).collect();
let expected_times = vec![0.0, 1.8];
for expected in expected_times {
assert!(
times.iter().any(|&actual| (actual - expected).abs() < 0.01),
"Expected a cue at time {}, got times: {:?}",
expected,
times
);
}
assert_eq!(
times.len(),
2,
"Expected 2 cues (one per iteration), got {}",
times.len()
);
}