use super::harness::*; use std::collections::HashMap; fn parse_params(output: &str) -> HashMap { let mut params = HashMap::new(); let parts: Vec<&str> = output.trim_start_matches('/').split('/').collect(); let mut i = 0; while i + 1 < parts.len() { if let Ok(v) = parts[i + 1].parse::() { params.insert(parts[i].to_string(), v); } i += 2; } params } fn get_deltas(outputs: &[String]) -> Vec { outputs .iter() .map(|o| parse_params(o).get("delta").copied().unwrap_or(0.0)) .collect() } fn get_durs(outputs: &[String]) -> Vec { outputs .iter() .map(|o| parse_params(o).get("dur").copied().unwrap_or(0.0)) .collect() } fn get_sounds(outputs: &[String]) -> Vec { outputs .iter() .map(|o| { let parts: Vec<&str> = o.trim_start_matches('/').split('/').collect(); if parts.len() >= 2 && parts[0] == "sound" { parts[1].to_string() } else { String::new() } }) .collect() } const EPSILON: f64 = 1e-9; fn approx_eq(a: f64, b: f64) -> bool { (a - b).abs() < EPSILON } #[test] fn stepdur_baseline() { let f = run("stepdur"); assert!(approx_eq(stack_float(&f), 0.125)); } #[test] fn single_emit() { let outputs = expect_outputs(r#""kick" s ."#, 1); let deltas = get_deltas(&outputs); assert!(approx_eq(deltas[0], 0.0), "single emit at start should have delta 0"); } #[test] fn multiple_emits_all_at_zero() { let outputs = expect_outputs(r#""kick" s . . . ."#, 4); let deltas = get_deltas(&outputs); for (i, delta) in deltas.iter().enumerate() { assert!(approx_eq(*delta, 0.0), "emit {}: expected delta 0, got {}", i, delta); } } #[test] fn sound_persists() { let outputs = expect_outputs(r#""kick" s . . "hat" s . ."#, 4); let sounds = get_sounds(&outputs); assert_eq!(sounds[0], "kick"); assert_eq!(sounds[1], "kick"); assert_eq!(sounds[2], "hat"); assert_eq!(sounds[3], "hat"); } #[test] fn alternating_sounds() { let outputs = expect_outputs(r#""kick" s . "snare" s . "kick" s . "snare" s ."#, 4); let sounds = get_sounds(&outputs); assert_eq!(sounds, vec!["kick", "snare", "kick", "snare"]); } #[test] fn dur_is_step_duration() { let outputs = expect_outputs(r#""kick" s ."#, 1); let durs = get_durs(&outputs); assert!(approx_eq(durs[0], 0.5), "dur should be 4 * step_duration (0.5), got {}", durs[0]); } #[test] fn cycle_picks_by_runs() { for runs in 0..4 { let ctx = ctx_with(|c| c.runs = runs); let f = forth(); let outputs = f.evaluate(r#""kick" s { . } { } 2 cycle"#, &ctx).unwrap(); if runs % 2 == 0 { assert_eq!(outputs.len(), 1, "runs={}: emit should be picked", runs); } else { assert_eq!(outputs.len(), 0, "runs={}: no-op should be picked", runs); } } } #[test] fn pcycle_picks_by_iter() { for iter in 0..4 { let ctx = ctx_with(|c| c.iter = iter); let f = forth(); let outputs = f.evaluate(r#""kick" s { . } { } 2 pcycle"#, &ctx).unwrap(); if iter % 2 == 0 { assert_eq!(outputs.len(), 1, "iter={}: emit should be picked", iter); } else { assert_eq!(outputs.len(), 0, "iter={}: no-op should be picked", iter); } } } #[test] fn cycle_with_sounds() { for runs in 0..3 { let ctx = ctx_with(|c| c.runs = runs); let f = forth(); let outputs = f.evaluate( r#"{ "kick" s . } { "hat" s . } { "snare" s . } 3 cycle"#, &ctx ).unwrap(); assert_eq!(outputs.len(), 1, "runs={}: expected 1 output", runs); let sounds = get_sounds(&outputs); let expected = ["kick", "hat", "snare"][runs % 3]; assert_eq!(sounds[0], expected, "runs={}: expected {}", runs, expected); } } #[test] fn at_single_delta() { let outputs = expect_outputs(r#"0.5 at "kick" s ."#, 1); let deltas = get_deltas(&outputs); let step_dur = 0.125; assert!(approx_eq(deltas[0], 0.5 * step_dur), "expected delta at 0.5 of step, got {}", deltas[0]); } #[test] fn at_list_deltas() { let outputs = expect_outputs(r#"0 0.5 at "kick" s ."#, 2); let deltas = get_deltas(&outputs); let step_dur = 0.125; assert!(approx_eq(deltas[0], 0.0), "expected delta 0, got {}", deltas[0]); assert!(approx_eq(deltas[1], 0.5 * step_dur), "expected delta at 0.5 of step, got {}", deltas[1]); } #[test] fn at_three_deltas() { let outputs = expect_outputs(r#"0 0.33 0.67 at "kick" s ."#, 3); let deltas = get_deltas(&outputs); let step_dur = 0.125; assert!(approx_eq(deltas[0], 0.0), "expected delta 0"); assert!((deltas[1] - 0.33 * step_dur).abs() < 0.001, "expected delta at 0.33 of step"); assert!((deltas[2] - 0.67 * step_dur).abs() < 0.001, "expected delta at 0.67 of step"); } #[test] fn at_persists_across_emits() { let outputs = expect_outputs(r#"0 0.5 at "kick" s . "hat" s ."#, 4); let sounds = get_sounds(&outputs); assert_eq!(sounds, vec!["kick", "kick", "hat", "hat"]); } #[test] fn at_reset_with_zero() { let outputs = expect_outputs(r#"0 0.5 at "kick" s . 0.0 at "hat" s ."#, 3); let sounds = get_sounds(&outputs); assert_eq!(sounds, vec!["kick", "kick", "hat"]); } #[test] fn clear_resets_at_deltas() { let outputs = expect_outputs(r#"0 0.5 at "kick" s . clear "hat" s ."#, 3); let sounds = get_sounds(&outputs); assert_eq!(sounds, vec!["kick", "kick", "hat"]); let deltas = get_deltas(&outputs); assert!(approx_eq(deltas[2], 0.0), "after clear, hat should emit at delta 0, got {}", deltas[2]); } #[test] fn at_records_selected_spans() { use cagire::forth::ExecutionTrace; let f = forth(); let mut trace = ExecutionTrace::default(); let script = r#"0 0.5 0.75 at "kick" s ."#; f.evaluate_with_trace(script, &default_ctx(), &mut trace).unwrap(); // Should have 6 selected spans: 3 for at deltas + 3 for sound (one per emit) assert_eq!(trace.selected_spans.len(), 6, "expected 6 selected spans (3 at + 3 sound)"); // Verify at delta spans (even indices: 0, 2, 4) assert_eq!(&script[trace.selected_spans[0].start as usize..trace.selected_spans[0].end as usize], "0"); assert_eq!(&script[trace.selected_spans[2].start as usize..trace.selected_spans[2].end as usize], "0.5"); assert_eq!(&script[trace.selected_spans[4].start as usize..trace.selected_spans[4].end as usize], "0.75"); }