252 lines
7.7 KiB
Rust
252 lines
7.7 KiB
Rust
use super::harness::*;
|
|
use std::collections::HashMap;
|
|
|
|
fn parse_params(output: &str) -> HashMap<String, f64> {
|
|
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::<f64>() {
|
|
params.insert(parts[i].to_string(), v);
|
|
}
|
|
i += 2;
|
|
}
|
|
params
|
|
}
|
|
|
|
fn get_deltas(outputs: &[String]) -> Vec<f64> {
|
|
outputs
|
|
.iter()
|
|
.map(|o| parse_params(o).get("delta").copied().unwrap_or(0.0))
|
|
.collect()
|
|
}
|
|
|
|
fn get_durs(outputs: &[String]) -> Vec<f64> {
|
|
outputs
|
|
.iter()
|
|
.map(|o| parse_params(o).get("dur").copied().unwrap_or(0.0))
|
|
.collect()
|
|
}
|
|
|
|
fn get_sounds(outputs: &[String]) -> Vec<String> {
|
|
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()
|
|
}
|
|
|
|
fn get_param(outputs: &[String], param: &str) -> Vec<f64> {
|
|
outputs
|
|
.iter()
|
|
.map(|o| parse_params(o).get(param).copied().unwrap_or(0.0))
|
|
.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.125), "dur should be step_duration (0.125), 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 2 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 3 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 2 at "kick" s . "hat" s ."#, 4);
|
|
let sounds = get_sounds(&outputs);
|
|
assert_eq!(sounds, vec!["kick", "kick", "hat", "hat"]);
|
|
}
|
|
|
|
#[test]
|
|
fn tcycle_basic() {
|
|
let outputs = expect_outputs(r#"0 0.5 0.75 3 at 60 64 67 3 tcycle note sine s ."#, 3);
|
|
let notes = get_param(&outputs, "note");
|
|
assert_eq!(notes, vec![60.0, 64.0, 67.0]);
|
|
}
|
|
|
|
#[test]
|
|
fn tcycle_wraps() {
|
|
let outputs = expect_outputs(r#"0 0.33 0.67 3 at 60 64 2 tcycle note sine s ."#, 3);
|
|
let notes = get_param(&outputs, "note");
|
|
assert_eq!(notes, vec![60.0, 64.0, 60.0]);
|
|
}
|
|
|
|
#[test]
|
|
fn tcycle_with_sound() {
|
|
let outputs = expect_outputs(r#"0 0.5 2 at kick hat 2 tcycle s ."#, 2);
|
|
let sounds = get_sounds(&outputs);
|
|
assert_eq!(sounds, vec!["kick", "hat"]);
|
|
}
|
|
|
|
#[test]
|
|
fn tcycle_multiple_params() {
|
|
let outputs = expect_outputs(r#"0 0.5 0.75 3 at 60 64 67 3 tcycle note 0.5 1.0 2 tcycle gain sine s ."#, 3);
|
|
let notes = get_param(&outputs, "note");
|
|
let gains = get_param(&outputs, "gain");
|
|
assert_eq!(notes, vec![60.0, 64.0, 67.0]);
|
|
assert_eq!(gains, vec![0.5, 1.0, 0.5]);
|
|
}
|
|
|
|
#[test]
|
|
fn at_reset_with_zero() {
|
|
let outputs = expect_outputs(r#"0 0.5 2 at "kick" s . 0.0 at "hat" s ."#, 3);
|
|
let sounds = get_sounds(&outputs);
|
|
assert_eq!(sounds, vec!["kick", "kick", "hat"]);
|
|
}
|
|
|
|
#[test]
|
|
fn tcycle_records_selected_spans() {
|
|
use cagire::forth::ExecutionTrace;
|
|
|
|
let f = forth();
|
|
let mut trace = ExecutionTrace::default();
|
|
let script = r#"0 0.5 2 at kick hat 2 tcycle s ."#;
|
|
f.evaluate_with_trace(script, &default_ctx(), &mut trace).unwrap();
|
|
|
|
// Should have 4 selected spans:
|
|
// - 2 for at deltas (0 and 0.5)
|
|
// - 2 for tcycle sound values (kick and hat)
|
|
assert_eq!(trace.selected_spans.len(), 4, "expected 4 selected spans (2 at + 2 tcycle)");
|
|
}
|
|
|
|
#[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 3 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..trace.selected_spans[0].end], "0");
|
|
assert_eq!(&script[trace.selected_spans[2].start..trace.selected_spans[2].end], "0.5");
|
|
assert_eq!(&script[trace.selected_spans[4].start..trace.selected_spans[4].end], "0.75");
|
|
}
|