use crate::lighting::parser::*;
use std::time::Duration;
#[test]
fn test_sequence_definition_and_reference() {
let content = r#"
sequence "color_cycle" {
@0.000
front_wash: static, color: "red", duration: 5s
@2.000
front_wash: static, color: "green", duration: 5s
@4.000
front_wash: static, color: "blue", duration: 5s
}
show "Test Show" {
@0.000
sequence "color_cycle"
@6.000
sequence "color_cycle"
}
"#;
let result = parse_light_shows(content);
assert!(
result.is_ok(),
"Failed to parse shows with sequences: {:?}",
result.err()
);
let shows = result.unwrap();
assert_eq!(shows.len(), 1);
let show = shows.get("Test Show").unwrap();
assert_eq!(show.cues.len(), 6);
assert_eq!(show.cues[0].time, Duration::from_millis(0));
assert_eq!(show.cues[1].time, Duration::from_millis(2000));
assert_eq!(show.cues[2].time, Duration::from_millis(4000));
assert_eq!(show.cues[3].time, Duration::from_millis(6000));
assert_eq!(show.cues[4].time, Duration::from_millis(8000));
assert_eq!(show.cues[5].time, Duration::from_millis(10000));
}
#[test]
fn test_sequence_with_effects_in_same_cue() {
let content = r#"
sequence "simple_sequence" {
@0.000
front_wash: static, color: "red", duration: 5s
@1.000
front_wash: static, color: "blue", duration: 5s
}
show "Test Show" {
@5.000
back_wash: static, color: "green", duration: 5s
sequence "simple_sequence"
}
"#;
let result = parse_light_shows(content);
assert!(result.is_ok(), "Failed to parse shows: {:?}", result.err());
let shows = result.unwrap();
let show = shows.get("Test Show").unwrap();
assert_eq!(show.cues.len(), 2);
assert_eq!(show.cues[0].time, Duration::from_millis(5000));
assert_eq!(show.cues[0].effects.len(), 2);
assert_eq!(show.cues[1].time, Duration::from_millis(6000));
assert_eq!(show.cues[1].effects.len(), 1); }
#[test]
fn test_sequence_not_found_error() {
let content = r#"
show "Test Show" {
@0.000
sequence "nonexistent_sequence"
}
"#;
let result = parse_light_shows(content);
assert!(result.is_err(), "Should fail when sequence is not found");
let error = result.unwrap_err();
assert!(
error.to_string().contains("not found"),
"Error should mention sequence not found"
);
}
#[test]
fn test_sequence_with_measure_based_timing() {
let content = r#"
tempo {
bpm: 120
time_signature: 4/4
}
sequence "measure_based_sequence" {
@1/1
front_wash: static, color: "red", duration: 5s
@1/3
front_wash: static, color: "green", duration: 5s
@2/1
front_wash: static, color: "blue", duration: 5s
}
show "Test Show" {
@0.000
sequence "measure_based_sequence"
}
"#;
let result = parse_light_shows(content);
assert!(
result.is_ok(),
"Failed to parse shows with measure-based sequence: {:?}",
result.err()
);
let shows = result.unwrap();
let show = shows.get("Test Show").unwrap();
assert_eq!(show.cues.len(), 3);
assert_eq!(show.cues[0].time, Duration::from_secs(0));
assert_eq!(show.cues[1].time, Duration::from_secs(1));
assert_eq!(show.cues[2].time, Duration::from_secs(2));
}
#[test]
fn test_sequence_with_own_tempo_and_measure_timing() {
let content = r#"
sequence "sequence_with_tempo" {
tempo {
bpm: 60
time_signature: 4/4
}
@1/1
front_wash: static, color: "red", duration: 5s
@2/1
front_wash: static, color: "blue", duration: 5s
}
show "Test Show" {
@0.000
sequence "sequence_with_tempo"
}
"#;
let result = parse_light_shows(content);
assert!(
result.is_ok(),
"Failed to parse shows with sequence having own tempo: {:?}",
result.err()
);
let shows = result.unwrap();
let show = shows.get("Test Show").unwrap();
assert_eq!(show.cues.len(), 2);
assert_eq!(show.cues[0].time, Duration::from_secs(0));
assert_eq!(show.cues[1].time, Duration::from_secs(4));
}
#[test]
fn test_sequence_measure_timing_with_offset() {
let content = r#"
tempo {
bpm: 120
time_signature: 4/4
}
sequence "measure_sequence" {
@1/1
front_wash: static, color: "red", duration: 5s
@1/3
front_wash: static, color: "green", duration: 5s
}
show "Test Show" {
@10.000
sequence "measure_sequence"
}
"#;
let result = parse_light_shows(content);
assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
let shows = result.unwrap();
let show = shows.get("Test Show").unwrap();
assert_eq!(show.cues.len(), 2);
assert_eq!(show.cues[0].time, Duration::from_secs(10));
assert_eq!(show.cues[1].time, Duration::from_secs(11));
}
#[test]
fn test_sequence_with_comment_after_loop() {
let content = r#"
tempo {
bpm: 120
time_signature: 4/4
}
sequence "test" {
@1/1
front_wash: static color: "red", duration: 5s
}
show "Test" {
@0.000
sequence "test", loop: 3
# Some comment
}
"#;
let result = parse_light_shows(content);
assert!(
result.is_ok(),
"Failed to parse sequence with comment after loop directive: {:?}",
result.err()
);
let shows = result.unwrap();
let show = shows.get("Test").expect("Show 'Test' should exist");
assert_eq!(show.cues.len(), 3);
}
#[test]
fn test_layer_command_with_comment_after_parameter() {
let content = r#"
tempo {
bpm: 120
time_signature: 4/4
}
show "Test" {
@0.000
clear(layer: foreground)
# Some comment
@1.000
release(layer: background, time: 2s)
# Another comment
@2.000
master(layer: midground, intensity: 0.5)
# Yet another comment
}
"#;
let result = parse_light_shows(content);
assert!(
result.is_ok(),
"Failed to parse layer commands with comments: {:?}",
result.err()
);
let shows = result.unwrap();
let show = shows.get("Test").expect("Show 'Test' should exist");
assert_eq!(show.cues.len(), 3);
let first_cue = &show.cues[0];
assert_eq!(first_cue.layer_commands.len(), 1);
let clear_cmd = &first_cue.layer_commands[0];
assert_eq!(
clear_cmd.command_type,
crate::lighting::parser::LayerCommandType::Clear
);
assert_eq!(
clear_cmd.layer,
Some(crate::lighting::effects::EffectLayer::Foreground)
);
let second_cue = &show.cues[1];
assert_eq!(second_cue.layer_commands.len(), 1);
let release_cmd = &second_cue.layer_commands[0];
assert_eq!(
release_cmd.command_type,
crate::lighting::parser::LayerCommandType::Release
);
assert_eq!(
release_cmd.layer,
Some(crate::lighting::effects::EffectLayer::Background)
);
assert!(release_cmd.fade_time.is_some());
let third_cue = &show.cues[2];
assert_eq!(third_cue.layer_commands.len(), 1);
let master_cmd = &third_cue.layer_commands[0];
assert_eq!(
master_cmd.command_type,
crate::lighting::parser::LayerCommandType::Master
);
assert_eq!(
master_cmd.layer,
Some(crate::lighting::effects::EffectLayer::Midground)
);
assert!(master_cmd.intensity.is_some());
assert!((master_cmd.intensity.unwrap() - 0.5).abs() < 0.001);
}
#[test]
fn test_effect_parameter_with_comment_after() {
let content = r#"
tempo {
bpm: 120
time_signature: 4/4
}
show "Test" {
@0.000
front_wash: static, color: "red", layer: foreground, duration: 5s
# Comment after effect
@1.000
back_wash: cycle, speed: 2beats, direction: forward, duration: 10s
# Another comment
}
"#;
let result = parse_light_shows(content);
assert!(
result.is_ok(),
"Failed to parse effects with comments: {:?}",
result.err()
);
let shows = result.unwrap();
let show = shows.get("Test").expect("Show 'Test' should exist");
assert_eq!(show.cues.len(), 2);
let first_cue = &show.cues[0];
assert_eq!(first_cue.effects.len(), 1);
let effect = &first_cue.effects[0];
assert_eq!(
effect.layer,
Some(crate::lighting::effects::EffectLayer::Foreground)
);
let second_cue = &show.cues[1];
assert_eq!(second_cue.effects.len(), 1);
}
#[test]
fn test_sequence_looping_finite() {
let content = r#"
sequence "simple_sequence" {
@0.000
front_wash: static, color: "red", duration: 5s
@1.000
front_wash: static, color: "blue", duration: 5s
}
show "Test Show" {
@0.000
sequence "simple_sequence", loop: 3
}
"#;
let result = parse_light_shows(content);
assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
let shows = result.unwrap();
let show = shows.get("Test Show").unwrap();
assert_eq!(show.cues.len(), 6);
assert_eq!(show.cues[0].time, Duration::from_secs(0));
assert_eq!(show.cues[1].time, Duration::from_secs(1));
assert_eq!(show.cues[2].time, Duration::from_secs(6));
assert_eq!(show.cues[3].time, Duration::from_secs(7));
assert_eq!(show.cues[4].time, Duration::from_secs(12));
assert_eq!(show.cues[5].time, Duration::from_secs(13));
for cue in &show.cues {
for effect in &cue.effects {
assert_eq!(effect.sequence_name, Some("simple_sequence".to_string()));
}
}
assert!(
show.cues[0].stop_sequences.is_empty(),
"First iteration should not stop anything"
);
assert_eq!(
show.cues[2].stop_sequences,
vec!["simple_sequence".to_string()],
"Second iteration should stop previous iteration's effects"
);
assert_eq!(
show.cues[4].stop_sequences,
vec!["simple_sequence".to_string()],
"Third iteration should stop previous iteration's effects"
);
assert!(show.cues[1].stop_sequences.is_empty());
assert!(show.cues[3].stop_sequences.is_empty());
assert!(show.cues[5].stop_sequences.is_empty());
}
#[test]
fn test_sequence_looping_infinite() {
let content = r#"
sequence "infinite_sequence" {
@0.000
front_wash: static, color: "red", duration: 5s
@1.000
front_wash: static, color: "blue", duration: 5s
}
show "Test Show" {
@0.000
sequence "infinite_sequence", loop: loop
}
"#;
let result = parse_light_shows(content);
assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
let shows = result.unwrap();
let show = shows.get("Test Show").unwrap();
assert_eq!(show.cues.len(), 20000);
assert_eq!(show.cues[0].time, Duration::from_secs(0));
assert_eq!(show.cues[1].time, Duration::from_secs(1));
assert_eq!(show.cues[2].time, Duration::from_secs(6)); assert_eq!(show.cues[3].time, Duration::from_secs(7));
}
#[test]
fn test_sequence_looping_once() {
let content = r#"
sequence "once_sequence" {
@0.000
front_wash: static, color: "red", duration: 5s
@1.000
front_wash: static, color: "blue", duration: 5s
}
show "Test Show" {
@0.000
sequence "once_sequence", loop: once
}
"#;
let result = parse_light_shows(content);
assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
let shows = result.unwrap();
let show = shows.get("Test Show").unwrap();
assert_eq!(show.cues.len(), 2);
}
#[test]
fn test_stop_sequence_command() {
let content = r#"
sequence "looping_sequence" {
@0.000
front_wash: static, color: "red", duration: 5s
@1.000
front_wash: static, color: "blue", duration: 5s
}
show "Test Show" {
@0.000
sequence "looping_sequence", loop: loop
@10.000
stop sequence "looping_sequence"
}
"#;
let result = parse_light_shows(content);
assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
let shows = result.unwrap();
let show = shows.get("Test Show").unwrap();
let stop_cue = show
.cues
.iter()
.find(|c| c.time == Duration::from_secs(10) && !c.stop_sequences.is_empty());
let cue_times: Vec<_> = show.cues.iter().map(|c| c.time).collect();
let cues_at_10: Vec<_> = show
.cues
.iter()
.filter(|c| c.time == Duration::from_secs(10))
.map(|c| (c.time, c.stop_sequences.clone(), c.effects.len()))
.collect();
assert!(
stop_cue.is_some(),
"Should have a cue at 10 seconds with stop_sequences. Cue times: {:?}, Cues at 10s: {:?}",
cue_times,
cues_at_10
);
let stop_cue = stop_cue.unwrap();
assert_eq!(
stop_cue.stop_sequences,
vec!["looping_sequence"],
"Stop sequences: {:?}",
stop_cue.stop_sequences
);
}
#[test]
fn test_stop_multiple_sequences() {
let content = r#"
sequence "seq1" {
@0.000
front_wash: static, color: "red", duration: 5s
}
sequence "seq2" {
@0.000
back_wash: static, color: "blue", duration: 5s
}
show "Test Show" {
@0.000
sequence "seq1", loop: loop
sequence "seq2", loop: loop
@5.000
stop sequence "seq1"
stop sequence "seq2"
}
"#;
let result = parse_light_shows(content);
assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
let shows = result.unwrap();
let show = shows.get("Test Show").unwrap();
let cues_at_5: Vec<_> = show
.cues
.iter()
.filter(|c| c.time == Duration::from_secs(5))
.collect();
assert!(!cues_at_5.is_empty(), "Should have cues at 5 seconds");
let all_stops: std::collections::HashSet<_> = cues_at_5
.iter()
.flat_map(|c| c.stop_sequences.iter())
.collect();
assert!(
all_stops.contains(&"seq1".to_string()),
"Should stop seq1 at 5s, got stops: {:?}",
all_stops
);
assert!(
all_stops.contains(&"seq2".to_string()),
"Should stop seq2 at 5s, got stops: {:?}",
all_stops
);
}
#[test]
fn test_nested_sequences() {
let content = r#"
sequence "base_pattern" {
@0.000
front_wash: static, color: "red", duration: 5s
@1.000
front_wash: static, color: "blue", duration: 5s
}
sequence "complex_pattern" {
@0.000
sequence "base_pattern"
back_wash: static, color: "green", duration: 5s
@3.000
sequence "base_pattern"
}
show "Test Show" {
@0.000
sequence "complex_pattern"
}
"#;
let result = parse_light_shows(content);
assert!(
result.is_ok(),
"Failed to parse nested sequences: {:?}",
result.err()
);
let shows = result.unwrap();
let show = shows.get("Test Show").unwrap();
assert_eq!(show.cues.len(), 4);
assert_eq!(show.cues[0].time, Duration::from_secs(0));
assert_eq!(show.cues[1].time, Duration::from_secs(1));
assert_eq!(show.cues[2].time, Duration::from_secs(3));
assert_eq!(show.cues[3].time, Duration::from_secs(4));
}
#[test]
fn test_circular_sequence_reference() {
let content = r#"
sequence "seq_a" {
@0.000
front_wash: static, color: "red", duration: 5s
@1.000
sequence "seq_b"
}
sequence "seq_b" {
@0.000
sequence "seq_a"
}
show "Test Show" {
@0.000
sequence "seq_a"
}
"#;
let result = parse_light_shows(content);
assert!(result.is_err(), "Should fail with circular reference");
let error = result.unwrap_err();
assert!(
error.to_string().contains("Circular sequence reference"),
"Error should mention circular reference: {}",
error
);
}
#[test]
fn test_nested_sequences_measure_timing() {
let content = r#"tempo {
start: 3.0s
bpm: 160
time_signature: 4/4
}
sequence "verse-start" {
@1/1
all_wash: static, color: "white", duration: 5s
}
sequence "verse" {
@1/1
sequence "verse-start", loop: 1
@13/1
all_wash: static, color: "red", duration: 5s
}
show "Test" {
@17/1
sequence "verse"
}
"#;
let result = parse_light_shows(content);
assert!(
result.is_ok(),
"Failed to parse nested sequences with measure timing: {:?}",
result.err()
);
let shows = result.unwrap();
let show = shows.get("Test").unwrap();
let expected_time = Duration::from_secs_f64(27.0);
assert!(!show.cues.is_empty(), "Should have at least one cue");
let first_cue_time = show.cues[0].time;
assert!(
(first_cue_time.as_secs_f64() - expected_time.as_secs_f64()).abs() < 0.001,
"verse-start should start at measure 17 (27.0s), got {:?}",
first_cue_time
);
}
#[test]
fn test_sequence_with_fractional_measure_hold_time() {
let show_content = r##"
tempo {
bpm: 120
time_signature: 4/4
}
sequence "riff-e" {
@1/1
all_wash: static, color: "#B5C637", layer: background, blend_mode: replace, duration: 5s
@1/3
all_wash: static, color: "#8A0303", layer: background, blend_mode: replace, hold_time: 1.5measures
}
show "Test" {
@0.000
sequence "riff-e"
}
"##;
let show_result = parse_light_shows(show_content);
assert!(
show_result.is_ok(),
"Failed to parse show with sequence: {:?}",
show_result.err()
);
let shows_with_sequence = show_result.unwrap();
let show = shows_with_sequence
.get("Test")
.expect("Show 'Test' should exist");
assert!(
show.cues.len() >= 2,
"Show should have at least 2 cues from sequence, got {}",
show.cues.len()
);
let cue_with_hold_time = show
.cues
.iter()
.find(|cue| cue.effects.iter().any(|effect| effect.hold_time.is_some()));
let (sequence_cue, second_effect) = if let Some(cue) = cue_with_hold_time {
let effect = cue
.effects
.iter()
.find(|e| e.hold_time.is_some())
.expect("Should find effect with hold_time");
(cue, effect)
} else {
panic!("Should find a cue with an effect that has hold_time");
};
let hold_time = second_effect
.hold_time
.expect("Second effect should have hold_time");
let expected_hold_time = Duration::from_secs_f64(3.0);
assert!(
(hold_time.as_secs_f64() - expected_hold_time.as_secs_f64()).abs() < 0.001,
"hold_time should be 3.0s (1.5 measures at 120 BPM in 4/4), got {}s",
hold_time.as_secs_f64()
);
let total_duration = second_effect.total_duration();
assert!(
(total_duration.as_secs_f64() - expected_hold_time.as_secs_f64()).abs() < 0.001,
"Total duration should be 3.0s, got {}s",
total_duration.as_secs_f64()
);
let effect_start_time = sequence_cue.time;
let total_duration = second_effect.total_duration();
let effect_completion_time = effect_start_time + total_duration;
let expected_completion_time = Duration::from_secs_f64(4.0);
assert!(
(effect_completion_time.as_secs_f64() - expected_completion_time.as_secs_f64()).abs()
< 0.001,
"Effect should complete at 4.0s (2 measures from sequence start), got {}s",
effect_completion_time.as_secs_f64()
);
}
#[test]
fn test_self_referencing_sequence() {
let content = r#"
sequence "self_ref" {
@0.000
sequence "self_ref"
}
show "Test Show" {
@0.000
sequence "self_ref"
}
"#;
let result = parse_light_shows(content);
assert!(result.is_err(), "Should fail with self-reference");
let error = result.unwrap_err();
assert!(
error.to_string().contains("Circular sequence reference"),
"Error should mention circular reference: {}",
error
);
}
#[test]
fn test_sequence_tempo_rescaling_at_expansion() {
let content = r#"
tempo {
bpm: 110
time_signature: 4/4
changes: [
@3/1 { bpm: 160 }
]
}
sequence "test_seq" {
@1/1
front_wash: static, color: "red", duration: 5s
@1/2
front_wash: static, color: "blue", duration: 5s
}
show "Test Show" {
@5/1
sequence "test_seq", loop: 4
}
"#;
let result = parse_light_shows(content);
assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
let shows = result.unwrap();
let show = shows.get("Test Show").unwrap();
assert_eq!(show.cues.len(), 8);
let expected_base_time = 8.0 * 60.0 / 110.0 + 8.0 * 60.0 / 160.0;
let base_time = show.cues[0].time.as_secs_f64();
assert!(
(base_time - expected_base_time).abs() < 0.001,
"First cue should be at measure 5 ({:.4}s), got {:.4}s",
expected_base_time,
base_time
);
let beat_at_160 = 60.0 / 160.0; let duration_internal = 5.0 + 60.0 / 110.0; let duration_beats = duration_internal * (110.0 / 60.0); let iteration_spacing = duration_beats * beat_at_160;
for i in 0..4 {
let cue_time = show.cues[i * 2].time.as_secs_f64();
let expected = expected_base_time + i as f64 * iteration_spacing;
assert!(
(cue_time - expected).abs() < 0.01,
"Iteration {} first cue should be at {:.4}s, got {:.4}s",
i,
expected,
cue_time
);
}
for i in 0..4 {
let cue_time = show.cues[i * 2 + 1].time.as_secs_f64();
let expected = expected_base_time + i as f64 * iteration_spacing + beat_at_160;
assert!(
(cue_time - expected).abs() < 0.01,
"Iteration {} second cue should be at {:.4}s, got {:.4}s",
i,
expected,
cue_time
);
}
}
#[test]
fn test_sequence_tempo_rescaling_same_tempo() {
let content = r#"
tempo {
bpm: 120
time_signature: 4/4
}
sequence "same_tempo_seq" {
@1/1
front_wash: static, color: "red", duration: 5s
@1/3
front_wash: static, color: "green", duration: 5s
@2/1
front_wash: static, color: "blue", duration: 5s
}
show "Test Show" {
@5/1
sequence "same_tempo_seq"
}
"#;
let result = parse_light_shows(content);
assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
let shows = result.unwrap();
let show = shows.get("Test Show").unwrap();
assert_eq!(show.cues.len(), 3);
let base = 8.0;
assert!(
(show.cues[0].time.as_secs_f64() - base).abs() < 0.001,
"First cue at {:.4}s, expected {:.4}s",
show.cues[0].time.as_secs_f64(),
base
);
assert!(
(show.cues[1].time.as_secs_f64() - (base + 1.0)).abs() < 0.001,
"Second cue at {:.4}s, expected {:.4}s",
show.cues[1].time.as_secs_f64(),
base + 1.0
);
assert!(
(show.cues[2].time.as_secs_f64() - (base + 2.0)).abs() < 0.001,
"Third cue at {:.4}s, expected {:.4}s",
show.cues[2].time.as_secs_f64(),
base + 2.0
);
}