Init
This commit is contained in:
35
tests/forth.rs
Normal file
35
tests/forth.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
#[path = "forth/harness.rs"]
|
||||
mod harness;
|
||||
|
||||
#[path = "forth/arithmetic.rs"]
|
||||
mod arithmetic;
|
||||
|
||||
#[path = "forth/comparison.rs"]
|
||||
mod comparison;
|
||||
|
||||
#[path = "forth/context.rs"]
|
||||
mod context;
|
||||
|
||||
#[path = "forth/control_flow.rs"]
|
||||
mod control_flow;
|
||||
|
||||
#[path = "forth/errors.rs"]
|
||||
mod errors;
|
||||
|
||||
#[path = "forth/randomness.rs"]
|
||||
mod randomness;
|
||||
|
||||
#[path = "forth/sound.rs"]
|
||||
mod sound;
|
||||
|
||||
#[path = "forth/stack.rs"]
|
||||
mod stack;
|
||||
|
||||
#[path = "forth/temporal.rs"]
|
||||
mod temporal;
|
||||
|
||||
#[path = "forth/variables.rs"]
|
||||
mod variables;
|
||||
|
||||
#[path = "forth/quotations.rs"]
|
||||
mod quotations;
|
||||
152
tests/forth/arithmetic.rs
Normal file
152
tests/forth/arithmetic.rs
Normal file
@@ -0,0 +1,152 @@
|
||||
use super::harness::*;
|
||||
|
||||
#[test]
|
||||
fn add_integers() {
|
||||
expect_int("2 3 +", 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_floats() {
|
||||
expect_int("2.5 3.5 +", 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_mixed() {
|
||||
expect_float("2 3.5 +", 5.5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sub() {
|
||||
expect_int("10 3 -", 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sub_negative() {
|
||||
expect_int("3 10 -", -7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mul() {
|
||||
expect_int("4 5 *", 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mul_floats() {
|
||||
expect_int("2.5 4 *", 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn div() {
|
||||
expect_int("10 2 /", 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn div_float_result() {
|
||||
expect_float("7 2 /", 3.5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn modulo() {
|
||||
expect_int("7 3 mod", 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn modulo_exact() {
|
||||
expect_int("9 3 mod", 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn neg_int() {
|
||||
expect_int("5 neg", -5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn neg_float() {
|
||||
expect_float("3.5 neg", -3.5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn neg_double() {
|
||||
expect_int("-5 neg", 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn abs_positive() {
|
||||
expect_int("5 abs", 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn abs_negative() {
|
||||
expect_int("-5 abs", 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn abs_float() {
|
||||
expect_float("-3.5 abs", 3.5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn floor() {
|
||||
expect_int("3.7 floor", 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn floor_negative() {
|
||||
expect_int("-3.2 floor", -4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ceil() {
|
||||
expect_int("3.2 ceil", 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ceil_negative() {
|
||||
expect_int("-3.7 ceil", -3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_down() {
|
||||
expect_int("3.4 round", 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_up() {
|
||||
expect_int("3.6 round", 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_half() {
|
||||
expect_int("3.5 round", 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn min() {
|
||||
expect_int("3 5 min", 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn min_reverse() {
|
||||
expect_int("5 3 min", 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max() {
|
||||
expect_int("3 5 max", 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max_reverse() {
|
||||
expect_int("5 3 max", 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chain() {
|
||||
// (2 + 3) * 4 - 1 = 19
|
||||
expect_int("2 3 + 4 * 1 -", 19);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn underflow() {
|
||||
expect_error("1 +", "stack underflow");
|
||||
}
|
||||
136
tests/forth/comparison.rs
Normal file
136
tests/forth/comparison.rs
Normal file
@@ -0,0 +1,136 @@
|
||||
use super::harness::*;
|
||||
|
||||
#[test]
|
||||
fn eq_true() {
|
||||
expect_int("3 3 =", 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eq_false() {
|
||||
expect_int("3 4 =", 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eq_mixed_types() {
|
||||
expect_int("3.0 3 =", 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ne_true() {
|
||||
expect_int("3 4 <>", 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ne_false() {
|
||||
expect_int("3 3 <>", 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lt_true() {
|
||||
expect_int("2 3 lt", 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lt_equal() {
|
||||
expect_int("3 3 lt", 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lt_false() {
|
||||
expect_int("4 3 lt", 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gt_true() {
|
||||
expect_int("4 3 gt", 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gt_equal() {
|
||||
expect_int("3 3 gt", 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gt_false() {
|
||||
expect_int("2 3 gt", 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn le_less() {
|
||||
expect_int("2 3 <=", 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn le_equal() {
|
||||
expect_int("3 3 <=", 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn le_greater() {
|
||||
expect_int("4 3 <=", 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ge_greater() {
|
||||
expect_int("4 3 >=", 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ge_equal() {
|
||||
expect_int("3 3 >=", 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ge_less() {
|
||||
expect_int("2 3 >=", 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn and_tt() {
|
||||
expect_int("1 1 and", 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn and_tf() {
|
||||
expect_int("1 0 and", 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn and_ff() {
|
||||
expect_int("0 0 and", 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn or_tt() {
|
||||
expect_int("1 1 or", 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn or_tf() {
|
||||
expect_int("1 0 or", 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn or_ff() {
|
||||
expect_int("0 0 or", 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn not_true() {
|
||||
expect_int("1 not", 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn not_false() {
|
||||
expect_int("0 not", 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn truthy_nonzero() {
|
||||
expect_int("5 not", 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn truthy_negative() {
|
||||
expect_int("-1 not", 0);
|
||||
}
|
||||
99
tests/forth/context.rs
Normal file
99
tests/forth/context.rs
Normal file
@@ -0,0 +1,99 @@
|
||||
use super::harness::*;
|
||||
|
||||
#[test]
|
||||
fn step() {
|
||||
let ctx = ctx_with(|c| c.step = 7);
|
||||
let f = run_ctx("step", &ctx);
|
||||
assert_eq!(stack_int(&f), 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn beat() {
|
||||
let ctx = ctx_with(|c| c.beat = 4.5);
|
||||
let f = run_ctx("beat", &ctx);
|
||||
assert!((stack_float(&f) - 4.5).abs() < 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pattern() {
|
||||
let ctx = ctx_with(|c| c.pattern = 3);
|
||||
let f = run_ctx("pattern", &ctx);
|
||||
assert_eq!(stack_int(&f), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tempo() {
|
||||
let ctx = ctx_with(|c| c.tempo = 140.0);
|
||||
let f = run_ctx("tempo", &ctx);
|
||||
assert!((stack_float(&f) - 140.0).abs() < 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn phase() {
|
||||
let ctx = ctx_with(|c| c.phase = 0.25);
|
||||
let f = run_ctx("phase", &ctx);
|
||||
assert!((stack_float(&f) - 0.25).abs() < 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slot() {
|
||||
let ctx = ctx_with(|c| c.slot = 5);
|
||||
let f = run_ctx("slot", &ctx);
|
||||
assert_eq!(stack_int(&f), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runs() {
|
||||
let ctx = ctx_with(|c| c.runs = 10);
|
||||
let f = run_ctx("runs", &ctx);
|
||||
assert_eq!(stack_int(&f), 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn iter() {
|
||||
let ctx = ctx_with(|c| c.iter = 5);
|
||||
let f = run_ctx("iter", &ctx);
|
||||
assert_eq!(stack_int(&f), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_true_on_zero() {
|
||||
let ctx = ctx_with(|c| c.iter = 0);
|
||||
let f = run_ctx("4 every", &ctx);
|
||||
assert_eq!(stack_int(&f), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_true_on_multiple() {
|
||||
let ctx = ctx_with(|c| c.iter = 8);
|
||||
let f = run_ctx("4 every", &ctx);
|
||||
assert_eq!(stack_int(&f), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_false_between() {
|
||||
for i in 1..4 {
|
||||
let ctx = ctx_with(|c| c.iter = i);
|
||||
let f = run_ctx("4 every", &ctx);
|
||||
assert_eq!(stack_int(&f), 0, "iter={} should be false", i);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_zero_count() {
|
||||
expect_error("0 every", "every count must be > 0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stepdur() {
|
||||
// stepdur = 60.0 / tempo / 4.0 / speed = 60 / 120 / 4 / 1 = 0.125
|
||||
let f = run("stepdur");
|
||||
assert!((stack_float(&f) - 0.125).abs() < 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn context_in_computation() {
|
||||
let ctx = ctx_with(|c| c.step = 3);
|
||||
let f = run_ctx("60 step +", &ctx);
|
||||
assert_eq!(stack_int(&f), 63);
|
||||
}
|
||||
64
tests/forth/control_flow.rs
Normal file
64
tests/forth/control_flow.rs
Normal file
@@ -0,0 +1,64 @@
|
||||
use super::harness::*;
|
||||
|
||||
#[test]
|
||||
fn if_then_true() {
|
||||
expect_int("1 if 42 then", 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn if_then_false() {
|
||||
let f = run("0 if 42 then");
|
||||
assert!(f.stack().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn if_then_with_base() {
|
||||
expect_int("100 0 if 50 + then", 100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn if_else_true() {
|
||||
expect_int("1 if 42 else 99 then", 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn if_else_false() {
|
||||
expect_int("0 if 42 else 99 then", 99);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_tt() {
|
||||
expect_int("1 if 1 if 100 else 200 then else 300 then", 100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_tf() {
|
||||
expect_int("1 if 0 if 100 else 200 then else 300 then", 200);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_f() {
|
||||
expect_int("0 if 1 if 100 else 200 then else 300 then", 300);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn if_with_computation() {
|
||||
expect_int("3 2 gt if 42 else 99 then", 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_then() {
|
||||
expect_error("1 if 42", "missing 'then'");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deeply_nested() {
|
||||
expect_int("1 if 1 if 1 if 42 then then then", 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chained_conditionals() {
|
||||
// First if leaves nothing, second if runs
|
||||
let f = run("0 if 1 then 1 if 42 then");
|
||||
assert_eq!(stack_int(&f), 42);
|
||||
}
|
||||
106
tests/forth/errors.rs
Normal file
106
tests/forth/errors.rs
Normal file
@@ -0,0 +1,106 @@
|
||||
use super::harness::*;
|
||||
|
||||
#[test]
|
||||
fn empty_script() {
|
||||
expect_error("", "empty script");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn whitespace_only() {
|
||||
expect_error(" \n\t ", "empty script");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_word() {
|
||||
expect_error("foobar", "unknown word");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_not_number() {
|
||||
expect_error(r#""hello" neg"#, "expected number");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_expects_string() {
|
||||
expect_error("42 get", "expected string");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_expects_string() {
|
||||
expect_error("1 2 set", "expected string");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn comment_ignored() {
|
||||
expect_int("1 (this is a comment) 2 +", 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiline_comment() {
|
||||
expect_int("1 (multi\nline\ncomment) 2 +", 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn negative_literal() {
|
||||
expect_int("-5", -5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn float_literal() {
|
||||
let f = run("3.14159");
|
||||
let val = stack_float(&f);
|
||||
assert!((val - 3.14159).abs() < 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_with_spaces() {
|
||||
let f = run(r#""hello world" "x" set "x" get"#);
|
||||
match stack_top(&f) {
|
||||
seq::model::forth::Value::Str(s, _) => assert_eq!(s, "hello world"),
|
||||
other => panic!("expected string, got {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_count() {
|
||||
// [ 1 2 3 ] => stack: 1 2 3 3 (items + count on top)
|
||||
let f = run("[ 1 2 3 ]");
|
||||
assert_eq!(stack_int(&f), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_empty() {
|
||||
expect_int("[ ]", 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_preserves_values() {
|
||||
// [ 10 20 ] => stack: 10 20 2
|
||||
// drop => 10 20
|
||||
// + => 30
|
||||
expect_int("[ 10 20 ] drop +", 30);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conditional_based_on_step() {
|
||||
let ctx0 = ctx_with(|c| c.step = 0);
|
||||
let ctx1 = ctx_with(|c| c.step = 1);
|
||||
|
||||
let f0 = run_ctx("step 2 mod 0 = if 100 else 200 then", &ctx0);
|
||||
let f1 = run_ctx("step 2 mod 0 = if 100 else 200 then", &ctx1);
|
||||
|
||||
assert_eq!(stack_int(&f0), 100);
|
||||
assert_eq!(stack_int(&f1), 200);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accumulator() {
|
||||
let f = forth();
|
||||
let ctx = default_ctx();
|
||||
f.evaluate(r#"0 "acc" set"#, &ctx).unwrap();
|
||||
for _ in 0..5 {
|
||||
f.clear_stack();
|
||||
f.evaluate(r#""acc" get 1 + dup "acc" set"#, &ctx).unwrap();
|
||||
}
|
||||
assert_eq!(stack_int(&f), 5);
|
||||
}
|
||||
138
tests/forth/harness.rs
Normal file
138
tests/forth/harness.rs
Normal file
@@ -0,0 +1,138 @@
|
||||
use rand::rngs::StdRng;
|
||||
use rand::SeedableRng;
|
||||
use seq::model::forth::{Forth, Rng, StepContext, Value, Variables};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
pub fn default_ctx() -> StepContext {
|
||||
StepContext {
|
||||
step: 0,
|
||||
beat: 0.0,
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
tempo: 120.0,
|
||||
phase: 0.0,
|
||||
slot: 0,
|
||||
runs: 0,
|
||||
iter: 0,
|
||||
speed: 1.0,
|
||||
fill: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ctx_with(f: impl FnOnce(&mut StepContext)) -> StepContext {
|
||||
let mut ctx = default_ctx();
|
||||
f(&mut ctx);
|
||||
ctx
|
||||
}
|
||||
|
||||
pub fn new_vars() -> Variables {
|
||||
Arc::new(Mutex::new(HashMap::new()))
|
||||
}
|
||||
|
||||
pub fn seeded_rng(seed: u64) -> Rng {
|
||||
Arc::new(Mutex::new(StdRng::seed_from_u64(seed)))
|
||||
}
|
||||
|
||||
pub fn forth() -> Forth {
|
||||
Forth::new(new_vars(), seeded_rng(42))
|
||||
}
|
||||
|
||||
pub fn forth_seeded(seed: u64) -> Forth {
|
||||
Forth::new(new_vars(), seeded_rng(seed))
|
||||
}
|
||||
|
||||
pub fn run(script: &str) -> Forth {
|
||||
let f = forth();
|
||||
f.evaluate(script, &default_ctx()).unwrap();
|
||||
f
|
||||
}
|
||||
|
||||
pub fn run_ctx(script: &str, ctx: &StepContext) -> Forth {
|
||||
let f = forth();
|
||||
f.evaluate(script, ctx).unwrap();
|
||||
f
|
||||
}
|
||||
|
||||
pub fn stack_top(f: &Forth) -> Value {
|
||||
f.stack().pop().expect("stack empty")
|
||||
}
|
||||
|
||||
pub fn stack_int(f: &Forth) -> i64 {
|
||||
match stack_top(f) {
|
||||
Value::Int(i, _) => i,
|
||||
other => panic!("expected Int, got {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stack_float(f: &Forth) -> f64 {
|
||||
match stack_top(f) {
|
||||
Value::Float(x, _) => x,
|
||||
Value::Int(i, _) => i as f64,
|
||||
other => panic!("expected number, got {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expect_stack(script: &str, expected: &[Value]) {
|
||||
let f = run(script);
|
||||
let stack = f.stack();
|
||||
assert_eq!(stack, expected, "script: {}", script);
|
||||
}
|
||||
|
||||
pub fn expect_int(script: &str, expected: i64) {
|
||||
expect_stack(script, &[Value::Int(expected, None)]);
|
||||
}
|
||||
|
||||
pub fn expect_float(script: &str, expected: f64) {
|
||||
let f = run(script);
|
||||
let stack = f.stack();
|
||||
assert_eq!(stack.len(), 1, "expected single value on stack");
|
||||
let val = stack_float(&f);
|
||||
assert!(
|
||||
(val - expected).abs() < 1e-9,
|
||||
"expected {}, got {}",
|
||||
expected,
|
||||
val
|
||||
);
|
||||
}
|
||||
|
||||
pub fn expect_floats_close(script: &str, expected: f64, epsilon: f64) {
|
||||
let f = run(script);
|
||||
let val = stack_float(&f);
|
||||
assert!(
|
||||
(val - expected).abs() < epsilon,
|
||||
"expected ~{}, got {}",
|
||||
expected,
|
||||
val
|
||||
);
|
||||
}
|
||||
|
||||
pub fn expect_error(script: &str, expected_substr: &str) {
|
||||
let f = forth();
|
||||
let result = f.evaluate(script, &default_ctx());
|
||||
assert!(result.is_err(), "expected error for '{}'", script);
|
||||
let err = result.unwrap_err();
|
||||
assert!(
|
||||
err.contains(expected_substr),
|
||||
"error '{}' does not contain '{}'",
|
||||
err,
|
||||
expected_substr
|
||||
);
|
||||
}
|
||||
|
||||
pub fn expect_outputs(script: &str, count: usize) -> Vec<String> {
|
||||
let f = forth();
|
||||
let outputs = f.evaluate(script, &default_ctx()).unwrap();
|
||||
assert_eq!(outputs.len(), count, "expected {} outputs", count);
|
||||
outputs
|
||||
}
|
||||
|
||||
pub fn expect_output_contains(script: &str, substr: &str) {
|
||||
let outputs = expect_outputs(script, 1);
|
||||
assert!(
|
||||
outputs[0].contains(substr),
|
||||
"output '{}' does not contain '{}'",
|
||||
outputs[0],
|
||||
substr
|
||||
);
|
||||
}
|
||||
184
tests/forth/quotations.rs
Normal file
184
tests/forth/quotations.rs
Normal file
@@ -0,0 +1,184 @@
|
||||
use super::harness::*;
|
||||
|
||||
#[test]
|
||||
fn quotation_on_stack() {
|
||||
// Quotation should be pushable to stack
|
||||
let f = forth();
|
||||
let result = f.evaluate("{ 1 2 + }", &default_ctx());
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn when_true_executes() {
|
||||
let f = run("{ 42 } 1 ?");
|
||||
assert_eq!(stack_int(&f), 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn when_false_skips() {
|
||||
let f = run("99 { 42 } 0 ?");
|
||||
// Stack should still have 99, quotation not executed
|
||||
assert_eq!(stack_int(&f), 99);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn when_with_arithmetic() {
|
||||
let f = run("10 { 5 + } 1 ?");
|
||||
assert_eq!(stack_int(&f), 15);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn when_with_every() {
|
||||
// iter=0, every 2 should be true
|
||||
let ctx = ctx_with(|c| c.iter = 0);
|
||||
let f = run_ctx("{ 100 } 2 every ?", &ctx);
|
||||
assert_eq!(stack_int(&f), 100);
|
||||
|
||||
// iter=1, every 2 should be false
|
||||
let ctx = ctx_with(|c| c.iter = 1);
|
||||
let f = run_ctx("50 { 100 } 2 every ?", &ctx);
|
||||
assert_eq!(stack_int(&f), 50); // quotation not executed
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn when_with_chance_deterministic() {
|
||||
// 1.0 chance always executes quotation
|
||||
let f = run("{ 42 } 1.0 chance");
|
||||
assert_eq!(stack_int(&f), 42);
|
||||
|
||||
// 0.0 chance never executes quotation
|
||||
let f = run("99 { 42 } 0.0 chance");
|
||||
assert_eq!(stack_int(&f), 99);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_quotations() {
|
||||
let f = run("{ { 42 } 1 ? } 1 ?");
|
||||
assert_eq!(stack_int(&f), 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quotation_with_param() {
|
||||
let outputs = expect_outputs(r#""kick" s { 2 distort } 1 ? emit"#, 1);
|
||||
assert!(outputs[0].contains("distort/2"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quotation_skips_param() {
|
||||
let outputs = expect_outputs(r#""kick" s { 2 distort } 0 ? emit"#, 1);
|
||||
assert!(!outputs[0].contains("distort"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quotation_with_emit() {
|
||||
// When true, emit should fire
|
||||
let outputs = expect_outputs(r#""kick" s { emit } 1 ?"#, 1);
|
||||
assert!(outputs[0].contains("kick"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quotation_skips_emit() {
|
||||
// When false, emit should not fire
|
||||
let f = forth();
|
||||
let outputs = f
|
||||
.evaluate(r#""kick" s { emit } 0 ?"#, &default_ctx())
|
||||
.unwrap();
|
||||
// Should have 1 output from implicit emit at end (since cmd is set but not emitted)
|
||||
assert_eq!(outputs.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_quotation_error() {
|
||||
expect_error("42 1 ?", "expected quotation");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unclosed_quotation_error() {
|
||||
expect_error("{ 1 2", "missing }");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unexpected_close_error() {
|
||||
expect_error("1 2 }", "unexpected }");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_with_quotation_integration() {
|
||||
// Simulating: { 2 distort } 2 every ?
|
||||
// On even iterations, distort is applied
|
||||
for iter in 0..4 {
|
||||
let ctx = ctx_with(|c| c.iter = iter);
|
||||
let f = forth();
|
||||
let outputs = f
|
||||
.evaluate(r#""kick" s { 2 distort } 2 every ? emit"#, &ctx)
|
||||
.unwrap();
|
||||
if iter % 2 == 0 {
|
||||
assert!(
|
||||
outputs[0].contains("distort/2"),
|
||||
"iter {} should have distort",
|
||||
iter
|
||||
);
|
||||
} else {
|
||||
assert!(
|
||||
!outputs[0].contains("distort"),
|
||||
"iter {} should not have distort",
|
||||
iter
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Unless (!?) tests
|
||||
|
||||
#[test]
|
||||
fn unless_false_executes() {
|
||||
let f = run("{ 42 } 0 !?");
|
||||
assert_eq!(stack_int(&f), 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unless_true_skips() {
|
||||
let f = run("99 { 42 } 1 !?");
|
||||
assert_eq!(stack_int(&f), 99);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unless_with_every() {
|
||||
// iter=0, every 2 is true, so unless skips
|
||||
let ctx = ctx_with(|c| c.iter = 0);
|
||||
let f = run_ctx("50 { 100 } 2 every !?", &ctx);
|
||||
assert_eq!(stack_int(&f), 50);
|
||||
|
||||
// iter=1, every 2 is false, so unless executes
|
||||
let ctx = ctx_with(|c| c.iter = 1);
|
||||
let f = run_ctx("{ 100 } 2 every !?", &ctx);
|
||||
assert_eq!(stack_int(&f), 100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn when_and_unless_complementary() {
|
||||
// Using both ? and !? for if-else like behavior
|
||||
for iter in 0..4 {
|
||||
let ctx = ctx_with(|c| c.iter = iter);
|
||||
let f = forth();
|
||||
let outputs = f
|
||||
.evaluate(
|
||||
r#""kick" s { 2 distort } 2 every ? { 4 distort } 2 every !? emit"#,
|
||||
&ctx,
|
||||
)
|
||||
.unwrap();
|
||||
if iter % 2 == 0 {
|
||||
assert!(
|
||||
outputs[0].contains("distort/2"),
|
||||
"iter {} should have distort/2",
|
||||
iter
|
||||
);
|
||||
} else {
|
||||
assert!(
|
||||
outputs[0].contains("distort/4"),
|
||||
"iter {} should have distort/4",
|
||||
iter
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
118
tests/forth/randomness.rs
Normal file
118
tests/forth/randomness.rs
Normal file
@@ -0,0 +1,118 @@
|
||||
use super::harness::*;
|
||||
|
||||
#[test]
|
||||
fn rand_in_range() {
|
||||
let f = forth_seeded(12345);
|
||||
f.evaluate("0 10 rand", &default_ctx()).unwrap();
|
||||
let val = stack_float(&f);
|
||||
assert!(val >= 0.0 && val < 10.0, "rand {} not in [0, 10)", val);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rand_deterministic() {
|
||||
let f1 = forth_seeded(99);
|
||||
let f2 = forth_seeded(99);
|
||||
f1.evaluate("0 100 rand", &default_ctx()).unwrap();
|
||||
f2.evaluate("0 100 rand", &default_ctx()).unwrap();
|
||||
assert_eq!(f1.stack(), f2.stack());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rrand_inclusive() {
|
||||
let f = forth_seeded(42);
|
||||
for _ in 0..20 {
|
||||
f.clear_stack();
|
||||
f.evaluate("1 3 rrand", &default_ctx()).unwrap();
|
||||
let val = stack_int(&f);
|
||||
assert!(val >= 1 && val <= 3, "rrand {} not in [1, 3]", val);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seed_resets() {
|
||||
let f1 = forth_seeded(1);
|
||||
f1.evaluate("42 seed 0 100 rand", &default_ctx()).unwrap();
|
||||
let f2 = forth_seeded(999);
|
||||
f2.evaluate("42 seed 0 100 rand", &default_ctx()).unwrap();
|
||||
assert_eq!(f1.stack(), f2.stack());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn coin_binary() {
|
||||
let f = forth_seeded(42);
|
||||
f.evaluate("coin", &default_ctx()).unwrap();
|
||||
let val = stack_int(&f);
|
||||
assert!(val == 0 || val == 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chance_zero() {
|
||||
// 0.0 probability should never execute the quotation
|
||||
let f = run("99 { 42 } 0.0 chance");
|
||||
assert_eq!(stack_int(&f), 99); // quotation not executed, 99 still on stack
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chance_one() {
|
||||
// 1.0 probability should always execute the quotation
|
||||
let f = run("{ 42 } 1.0 chance");
|
||||
assert_eq!(stack_int(&f), 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn choose_from_list() {
|
||||
let f = forth_seeded(42);
|
||||
f.evaluate("10 20 30 3 choose", &default_ctx()).unwrap();
|
||||
let val = stack_int(&f);
|
||||
assert!(val == 10 || val == 20 || val == 30);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn choose_underflow() {
|
||||
expect_error("1 2 5 choose", "stack underflow");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cycle_deterministic() {
|
||||
for runs in 0..6 {
|
||||
let ctx = ctx_with(|c| c.runs = runs);
|
||||
let f = run_ctx("10 20 30 3 cycle", &ctx);
|
||||
let expected = [10, 20, 30][runs % 3];
|
||||
assert_eq!(stack_int(&f), expected, "cycle at runs={}", runs);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cycle_zero_count() {
|
||||
expect_error("1 2 3 0 cycle", "cycle count must be > 0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mtof_a4() {
|
||||
expect_float("69 mtof", 440.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mtof_octave() {
|
||||
expect_float("81 mtof", 880.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mtof_c4() {
|
||||
expect_floats_close("60 mtof", 261.6255653, 0.001);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ftom_440() {
|
||||
expect_float("440 ftom", 69.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ftom_880() {
|
||||
expect_float("880 ftom", 81.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mtof_ftom_roundtrip() {
|
||||
expect_float("60 mtof ftom", 60.0);
|
||||
}
|
||||
125
tests/forth/sound.rs
Normal file
125
tests/forth/sound.rs
Normal file
@@ -0,0 +1,125 @@
|
||||
use super::harness::*;
|
||||
|
||||
#[test]
|
||||
fn basic_emit() {
|
||||
let outputs = expect_outputs(r#""kick" sound emit"#, 1);
|
||||
assert!(outputs[0].contains("sound/kick"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alias_s() {
|
||||
let outputs = expect_outputs(r#""snare" s emit"#, 1);
|
||||
assert!(outputs[0].contains("sound/snare"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_params() {
|
||||
let outputs = expect_outputs(r#""kick" s 440 freq 0.5 gain emit"#, 1);
|
||||
assert!(outputs[0].contains("sound/kick"));
|
||||
assert!(outputs[0].contains("freq/440"));
|
||||
assert!(outputs[0].contains("gain/0.5"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_dur() {
|
||||
let outputs = expect_outputs(r#""kick" s emit"#, 1);
|
||||
assert!(outputs[0].contains("dur/"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_delaytime() {
|
||||
let outputs = expect_outputs(r#""kick" s emit"#, 1);
|
||||
assert!(outputs[0].contains("delaytime/"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_no_sound() {
|
||||
expect_error("emit", "no sound set");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn implicit_emit() {
|
||||
let outputs = expect_outputs(r#""kick" s 440 freq"#, 1);
|
||||
assert!(outputs[0].contains("sound/kick"));
|
||||
assert!(outputs[0].contains("freq/440"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_emits() {
|
||||
let outputs = expect_outputs(r#""kick" s emit "snare" s emit"#, 2);
|
||||
assert!(outputs[0].contains("sound/kick"));
|
||||
assert!(outputs[1].contains("sound/snare"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subdivide_each() {
|
||||
let outputs = expect_outputs(r#""kick" s 4 div each"#, 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn window_pop() {
|
||||
let outputs = expect_outputs(
|
||||
r#"0.0 0.5 window "kick" s emit pop 0.5 1.0 window "snare" s emit"#,
|
||||
2,
|
||||
);
|
||||
assert!(outputs[0].contains("sound/kick"));
|
||||
assert!(outputs[1].contains("sound/snare"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pop_root_fails() {
|
||||
expect_error("pop", "cannot pop root time context");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subdivide_zero() {
|
||||
expect_error(r#""kick" s 0 div each"#, "subdivide count must be > 0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_without_div() {
|
||||
expect_error(r#""kick" s each"#, "each requires subdivide first");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn envelope_params() {
|
||||
let outputs = expect_outputs(
|
||||
r#""synth" s 0.01 attack 0.1 decay 0.7 sustain 0.3 release emit"#,
|
||||
1,
|
||||
);
|
||||
assert!(outputs[0].contains("attack/0.01"));
|
||||
assert!(outputs[0].contains("decay/0.1"));
|
||||
assert!(outputs[0].contains("sustain/0.7"));
|
||||
assert!(outputs[0].contains("release/0.3"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filter_params() {
|
||||
let outputs = expect_outputs(r#""synth" s 2000 lpf 0.5 lpq emit"#, 1);
|
||||
assert!(outputs[0].contains("lpf/2000"));
|
||||
assert!(outputs[0].contains("lpq/0.5"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adsr_sets_all_envelope_params() {
|
||||
let outputs = expect_outputs(r#""synth" s 0.01 0.1 0.5 0.3 adsr emit"#, 1);
|
||||
assert!(outputs[0].contains("attack/0.01"));
|
||||
assert!(outputs[0].contains("decay/0.1"));
|
||||
assert!(outputs[0].contains("sustain/0.5"));
|
||||
assert!(outputs[0].contains("release/0.3"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ad_sets_attack_decay_sustain_zero() {
|
||||
let outputs = expect_outputs(r#""synth" s 0.01 0.1 ad emit"#, 1);
|
||||
assert!(outputs[0].contains("attack/0.01"));
|
||||
assert!(outputs[0].contains("decay/0.1"));
|
||||
assert!(outputs[0].contains("sustain/0"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bank_param() {
|
||||
let outputs = expect_outputs(r#""loop" s "a" bank emit"#, 1);
|
||||
assert!(outputs[0].contains("sound/loop"));
|
||||
assert!(outputs[0].contains("bank/a"));
|
||||
}
|
||||
94
tests/forth/stack.rs
Normal file
94
tests/forth/stack.rs
Normal file
@@ -0,0 +1,94 @@
|
||||
use super::harness::*;
|
||||
use seq::model::forth::Value;
|
||||
|
||||
fn int(n: i64) -> Value {
|
||||
Value::Int(n, None)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dup() {
|
||||
expect_stack("3 dup", &[int(3), int(3)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dup_underflow() {
|
||||
expect_error("dup", "stack underflow");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drop() {
|
||||
expect_stack("1 2 drop", &[int(1)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drop_underflow() {
|
||||
expect_error("drop", "stack underflow");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swap() {
|
||||
expect_stack("1 2 swap", &[int(2), int(1)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swap_underflow() {
|
||||
expect_error("1 swap", "stack underflow");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn over() {
|
||||
expect_stack("1 2 over", &[int(1), int(2), int(1)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn over_underflow() {
|
||||
expect_error("1 over", "stack underflow");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rot() {
|
||||
expect_stack("1 2 3 rot", &[int(2), int(3), int(1)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rot_underflow() {
|
||||
expect_error("1 2 rot", "stack underflow");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nip() {
|
||||
expect_stack("1 2 nip", &[int(2)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nip_underflow() {
|
||||
expect_error("1 nip", "stack underflow");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tuck() {
|
||||
expect_stack("1 2 tuck", &[int(2), int(1), int(2)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tuck_underflow() {
|
||||
expect_error("1 tuck", "stack underflow");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stack_persists() {
|
||||
let f = forth();
|
||||
let ctx = default_ctx();
|
||||
f.evaluate("1 2 3", &ctx).unwrap();
|
||||
assert_eq!(f.stack(), vec![int(1), int(2), int(3)]);
|
||||
f.evaluate("4 5", &ctx).unwrap();
|
||||
assert_eq!(f.stack(), vec![int(1), int(2), int(3), int(4), int(5)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_stack() {
|
||||
let f = forth();
|
||||
f.evaluate("1 2 3", &default_ctx()).unwrap();
|
||||
f.clear_stack();
|
||||
assert!(f.stack().is_empty());
|
||||
}
|
||||
229
tests/forth/temporal.rs
Normal file
229
tests/forth/temporal.rs
Normal file
@@ -0,0 +1,229 @@
|
||||
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()
|
||||
}
|
||||
|
||||
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]);
|
||||
}
|
||||
39
tests/forth/variables.rs
Normal file
39
tests/forth/variables.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
use super::harness::*;
|
||||
|
||||
#[test]
|
||||
fn set_get() {
|
||||
expect_int(r#"42 "x" set "x" get"#, 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_nonexistent() {
|
||||
expect_int(r#""novar" get"#, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn persistence_across_evals() {
|
||||
let f = forth();
|
||||
let ctx = default_ctx();
|
||||
f.evaluate(r#"10 "counter" set"#, &ctx).unwrap();
|
||||
f.clear_stack();
|
||||
f.evaluate(r#""counter" get 1 +"#, &ctx).unwrap();
|
||||
assert_eq!(stack_int(&f), 11);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overwrite() {
|
||||
expect_int(r#"1 "x" set 99 "x" set "x" get"#, 99);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_vars() {
|
||||
let f = run(r#"10 "a" set 20 "b" set "a" get "b" get +"#);
|
||||
assert_eq!(stack_int(&f), 30);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn float_var() {
|
||||
let f = run(r#"3.14 "pi" set "pi" get"#);
|
||||
let val = stack_float(&f);
|
||||
assert!((val - 3.14).abs() < 1e-9);
|
||||
}
|
||||
Reference in New Issue
Block a user