use std::path::PathBuf;
fn workspace_root() -> PathBuf {
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
manifest_dir
.parent()
.expect("no parent of crates/flutmax-cli")
.parent()
.expect("no grandparent")
.to_path_buf()
}
fn read_fixture(name: &str) -> String {
let path = workspace_root().join("tests/e2e/fixtures").join(name);
std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("failed to read fixture {}: {}", path.display(), e))
}
fn read_pattern(name: &str) -> String {
let path = workspace_root()
.join("tests/e2e/fixtures/patterns")
.join(name);
std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("failed to read pattern {}: {}", path.display(), e))
}
fn compile_and_validate(source: &str, label: &str) {
let json = flutmax_cli::compile(source)
.unwrap_or_else(|e| panic!("compilation failed for {}: {}", label, e));
let report = flutmax_validate::validate_str(&json, &format!("{}.maxpat", label));
let real_errors: Vec<_> = report
.errors
.iter()
.filter(|e| e.severity == flutmax_validate::Severity::Error)
.filter(|e| {
if e.layer == "static" && e.message.contains("Signal outlet") {
if let Some(dest_start) = e.message.rfind("of '") {
let dest_name = &e.message[dest_start + 4..e.message.len() - 1];
if dest_name.starts_with("obj-") {
return false; }
}
}
true
})
.collect();
assert!(
real_errors.is_empty(),
"Validation errors for {}:\n{}",
label,
report
);
let parsed: serde_json::Value =
serde_json::from_str(&json).expect("output should be valid JSON");
assert!(
parsed.get("patcher").is_some(),
"missing 'patcher' key for {}",
label
);
let boxes = parsed["patcher"]["boxes"]
.as_array()
.expect("missing boxes array");
assert!(!boxes.is_empty(), "boxes should not be empty for {}", label);
}
#[test]
fn compile_validate_l1_sine() {
compile_and_validate(&read_fixture("L1_sine.flutmax"), "L1_sine");
}
#[test]
fn compile_validate_l2_simple_synth() {
compile_and_validate(&read_fixture("L2_simple_synth.flutmax"), "L2_simple_synth");
}
#[test]
fn compile_validate_l3_trigger_fanout() {
compile_and_validate(
&read_fixture("L3_trigger_fanout.flutmax"),
"L3_trigger_fanout",
);
}
#[test]
fn compile_validate_l3b_control_fanout() {
compile_and_validate(
&read_fixture("L3b_control_fanout.flutmax"),
"L3b_control_fanout",
);
}
#[test]
fn compile_validate_pattern_fm_synth() {
compile_and_validate(
&read_pattern("pattern_fm_synth.flutmax"),
"pattern_fm_synth",
);
}
#[test]
fn compile_validate_pattern_stereo_mixer() {
compile_and_validate(
&read_pattern("pattern_stereo_mixer.flutmax"),
"pattern_stereo_mixer",
);
}
#[test]
fn compile_validate_pattern_subtractive_synth() {
compile_and_validate(
&read_pattern("pattern_subtractive_synth.flutmax"),
"pattern_subtractive_synth",
);
}
#[test]
fn compile_validate_pattern_multi_osc() {
compile_and_validate(
&read_pattern("pattern_multi_osc.flutmax"),
"pattern_multi_osc",
);
}
#[test]
fn compile_validate_minimal_sine() {
let source = "out 0 (audio): signal;\nwire osc = cycle~(440);\nout[0] = osc;";
compile_and_validate(source, "minimal_sine");
}
#[test]
fn compile_validate_control_only() {
let source = r#"
in 0 (val): float;
out 0 (result): float;
wire doubled = mul(val, 2);
out[0] = doubled;
"#;
compile_and_validate(source, "control_only");
}
#[test]
fn compile_validate_signal_chain() {
let source = r#"
in 0 (freq): float;
out 0 (audio): signal;
wire osc = cycle~(freq);
wire filtered = biquad~(osc, 1.0, 0.0, 0.0, 0.0, 0.0);
wire amp = mul~(filtered, 0.3);
out[0] = amp;
"#;
compile_and_validate(source, "signal_chain");
}
#[test]
fn compile_validate_multi_outlet() {
let source = r#"
in 0 (freq): float;
out 0 (left): signal;
out 1 (right): signal;
wire osc = cycle~(freq);
out[0] = osc;
out[1] = osc;
"#;
compile_and_validate(source, "multi_outlet");
}
#[test]
fn compile_validate_many_wires() {
let mut source = String::new();
source.push_str("out 0 (audio): signal;\n");
source.push_str("wire w0 = cycle~(440);\n");
for i in 1..50 {
source.push_str(&format!("wire w{} = mul~(w{}, 0.99);\n", i, i - 1));
}
source.push_str("out[0] = w49;\n");
compile_and_validate(&source, "many_wires");
}
#[test]
fn compile_validate_all_arithmetic_operators() {
let source = r#"
in 0 (a): float;
in 1 (b): float;
out 0 (result): float;
wire sum = add(a, b);
wire diff = sub(a, b);
wire product = mul(a, b);
wire quotient = dvd(a, b);
wire remainder = mod(a, b);
out[0] = sum;
"#;
compile_and_validate(source, "all_arithmetic");
}
#[test]
fn compile_validate_all_signal_operators() {
let source = r#"
in 0 (a): signal;
in 1 (b): signal;
out 0 (result): signal;
wire sum = add~(a, b);
wire diff = sub~(a, b);
wire product = mul~(a, b);
wire quotient = dvd~(a, b);
out[0] = sum;
"#;
compile_and_validate(source, "all_signal_operators");
}
#[test]
fn compile_validate_nested_calls() {
let source = r#"
out 0 (audio): signal;
wire sig = biquad~(cycle~(440), 1000, 0.7);
out[0] = sig;
"#;
compile_and_validate(source, "nested_calls");
}
#[test]
fn compile_validate_phasor_synth() {
let source = r#"
in 0 (freq): float;
in 1 (phase): float;
out 0 (audio): signal;
wire saw = phasor~(freq, phase);
wire amp = mul~(saw, 0.5);
out[0] = amp;
"#;
compile_and_validate(source, "phasor_synth");
}
#[test]
fn compile_validate_noise_source() {
let source = r#"
out 0 (audio): signal;
wire n = noise~();
wire amp = mul~(n, 0.1);
out[0] = amp;
"#;
compile_and_validate(source, "noise_source");
}
#[test]
fn verify_l1_sine_box_count() {
let source = read_fixture("L1_sine.flutmax");
let json = flutmax_cli::compile(&source).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
let boxes = parsed["patcher"]["boxes"].as_array().unwrap();
assert_eq!(boxes.len(), 2, "L1_sine should have 2 boxes");
}
#[test]
fn verify_l2_simple_synth_box_count() {
let source = read_fixture("L2_simple_synth.flutmax");
let json = flutmax_cli::compile(&source).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
let boxes = parsed["patcher"]["boxes"].as_array().unwrap();
assert_eq!(boxes.len(), 4, "L2_simple_synth should have 4 boxes");
}
#[test]
fn compile_invalid_source_returns_error() {
let source = "wire a = ;";
let result = flutmax_cli::compile(source);
assert!(result.is_err(), "invalid source should produce an error");
}
#[test]
fn compile_undefined_ref_returns_error() {
let source = r#"
out 0 (audio): signal;
wire osc = cycle~(undefined_var);
out[0] = osc;
"#;
let result = flutmax_cli::compile(source);
assert!(
result.is_err(),
"undefined reference should produce an error"
);
}
#[test]
fn compile_empty_source_succeeds() {
let result = flutmax_cli::compile("");
assert!(result.is_ok(), "empty source should compile");
let json = result.unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert!(parsed.get("patcher").is_some());
}
#[test]
fn compile_signal_to_control_type_error() {
let source = r#"
out 0 (result): float;
wire osc = cycle~(440);
wire val = print(osc);
out[0] = val;
"#;
let result = flutmax_cli::compile(source);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("E001"));
}