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() } const EPSILON: f64 = 1e-9; fn approx_eq(a: f64, b: f64) -> bool { (a - b).abs() < EPSILON } // At 120 BPM, speed 1.0: stepdur = 60/120/4/1 = 0.125s #[test] fn stepdur_baseline() { let f = run("stepdur"); assert!(approx_eq(stack_float(&f), 0.125)); } #[test] fn emit_no_delta() { let outputs = expect_outputs(r#""kick" s emit"#, 1); let deltas = get_deltas(&outputs); assert!( approx_eq(deltas[0], 0.0), "emit at start should have delta 0" ); } #[test] fn at_half() { // at 0.5 in root window (0..0.125) => delta = 0.5 * 0.125 = 0.0625 let outputs = expect_outputs(r#""kick" s 0.5 at"#, 1); let deltas = get_deltas(&outputs); assert!( approx_eq(deltas[0], 0.0625), "at 0.5 should be delta 0.0625, got {}", deltas[0] ); } #[test] fn at_quarter() { let outputs = expect_outputs(r#""kick" s 0.25 at"#, 1); let deltas = get_deltas(&outputs); assert!( approx_eq(deltas[0], 0.03125), "at 0.25 should be delta 0.03125, got {}", deltas[0] ); } #[test] fn at_zero() { let outputs = expect_outputs(r#""kick" s 0.0 at"#, 1); let deltas = get_deltas(&outputs); assert!(approx_eq(deltas[0], 0.0), "at 0.0 should be delta 0"); } #[test] fn div_2_each() { // 2 subdivisions: deltas at 0 and 0.0625 (half of 0.125) let outputs = expect_outputs(r#""kick" s 2 div each"#, 2); let deltas = get_deltas(&outputs); assert!(approx_eq(deltas[0], 0.0), "first subdivision at 0"); assert!( approx_eq(deltas[1], 0.0625), "second subdivision at 0.0625, got {}", deltas[1] ); } #[test] fn div_4_each() { // 4 subdivisions: 0, 0.03125, 0.0625, 0.09375 let outputs = expect_outputs(r#""kick" s 4 div each"#, 4); let deltas = get_deltas(&outputs); let expected = [0.0, 0.03125, 0.0625, 0.09375]; for (i, (got, exp)) in deltas.iter().zip(expected.iter()).enumerate() { assert!( approx_eq(*got, *exp), "subdivision {}: expected {}, got {}", i, exp, got ); } } #[test] fn div_3_each() { // 3 subdivisions: 0, 0.125/3, 2*0.125/3 let outputs = expect_outputs(r#""kick" s 3 div each"#, 3); let deltas = get_deltas(&outputs); let step = 0.125 / 3.0; assert!(approx_eq(deltas[0], 0.0)); assert!(approx_eq(deltas[1], step), "got {}", deltas[1]); assert!(approx_eq(deltas[2], 2.0 * step), "got {}", deltas[2]); } #[test] fn window_full() { // window 0.0 1.0 is the full step, same as root let outputs = expect_outputs(r#"0.0 1.0 window "kick" s 0.5 at"#, 1); let deltas = get_deltas(&outputs); assert!(approx_eq(deltas[0], 0.0625), "full window at 0.5 = 0.0625"); } #[test] fn window_first_half() { // window 0.0 0.5 restricts to first half (0..0.0625) // at 0.5 within that = 0.25 of full step = 0.03125 let outputs = expect_outputs(r#"0.0 0.5 window "kick" s 0.5 at"#, 1); let deltas = get_deltas(&outputs); assert!( approx_eq(deltas[0], 0.03125), "first-half window at 0.5 = 0.03125, got {}", deltas[0] ); } #[test] fn window_second_half() { // window 0.5 1.0 restricts to second half (0.0625..0.125) // at 0.0 within that = start of second half = 0.0625 let outputs = expect_outputs(r#"0.5 1.0 window "kick" s 0.0 at"#, 1); let deltas = get_deltas(&outputs); assert!( approx_eq(deltas[0], 0.0625), "second-half window at 0.0 = 0.0625, got {}", deltas[0] ); } #[test] fn window_second_half_middle() { // window 0.5 1.0, at 0.5 within that = 0.75 of full step = 0.09375 let outputs = expect_outputs(r#"0.5 1.0 window "kick" s 0.5 at"#, 1); let deltas = get_deltas(&outputs); assert!(approx_eq(deltas[0], 0.09375), "got {}", deltas[0]); } #[test] fn nested_windows() { // window 0.0 0.5, then window 0.5 1.0 within that // outer: 0..0.0625, inner: 0.5..1.0 of that = 0.03125..0.0625 // at 0.0 in inner = 0.03125 let outputs = expect_outputs(r#"0.0 0.5 window 0.5 1.0 window "kick" s 0.0 at"#, 1); let deltas = get_deltas(&outputs); assert!( approx_eq(deltas[0], 0.03125), "nested window at 0.0 = 0.03125, got {}", deltas[0] ); } #[test] fn window_pop_sequence() { // First in window 0.0 0.5 at 0.0 -> delta 0 // Pop, then in window 0.5 1.0 at 0.0 -> delta 0.0625 let outputs = expect_outputs( r#"0.0 0.5 window "kick" s 0.0 at pop 0.5 1.0 window "snare" s 0.0 at"#, 2, ); let deltas = get_deltas(&outputs); assert!(approx_eq(deltas[0], 0.0), "first window start"); assert!( approx_eq(deltas[1], 0.0625), "second window start, got {}", deltas[1] ); } #[test] fn div_in_window() { // window 0.0 0.5 (duration 0.0625), then div 2 each // subdivisions at 0 and 0.03125 let outputs = expect_outputs(r#"0.0 0.5 window "kick" s 2 div each"#, 2); let deltas = get_deltas(&outputs); assert!(approx_eq(deltas[0], 0.0)); assert!(approx_eq(deltas[1], 0.03125), "got {}", deltas[1]); } #[test] fn tempo_affects_stepdur() { // At 60 BPM: stepdur = 60/60/4/1 = 0.25 let ctx = ctx_with(|c| c.tempo = 60.0); let f = forth(); f.evaluate("stepdur", &ctx).unwrap(); assert!(approx_eq(stack_float(&f), 0.25)); } #[test] fn speed_affects_stepdur() { // At 120 BPM, speed 2.0: stepdur = 60/120/4/2 = 0.0625 let ctx = ctx_with(|c| c.speed = 2.0); let f = forth(); f.evaluate("stepdur", &ctx).unwrap(); assert!(approx_eq(stack_float(&f), 0.0625)); } #[test] fn div_each_at_different_tempo() { // At 60 BPM: stepdur = 0.25, so div 2 each => 0, 0.125 let ctx = ctx_with(|c| c.tempo = 60.0); let f = forth(); let outputs = f.evaluate(r#""kick" s 2 div each"#, &ctx).unwrap(); let deltas = get_deltas(&outputs); assert!(approx_eq(deltas[0], 0.0)); assert!(approx_eq(deltas[1], 0.125), "got {}", deltas[1]); }