use super::*;
#[test]
fn test_parse_feather_wavenet() {
let json_str = r#"{
"version": "0.5.4",
"architecture": "WaveNet",
"config": {
"layers": [
{
"input_size": 1, "condition_size": 1, "head_size": 4,
"channels": 8, "kernel_size": 3, "dilations": [1,2,4,8,16,32,64],
"activation": "Tanh", "gated": false, "head_bias": false
},
{
"input_size": 1, "condition_size": 1, "head_size": 4,
"channels": 8, "kernel_size": 3, "dilations": [128,256,512,1,2,4,8,16,32,64,128,256,512],
"activation": "Tanh", "gated": false, "head_bias": true
}
],
"head": null,
"head_scale": 0.02
},
"weights": [0.0123, -0.456, 1.0, 2.0],
"sample_rate": 48000,
"metadata": {
"name": "Super Twin",
"modeled_by": "John Doe",
"gear_make": "Fender",
"input_level_dbu": 12.0,
"output_level_dbu": 11.5,
"loudness": -18.0
}
}"#;
let parsed = parse_nam_json(json_str).expect("Failed to parse simulated NAM JSON");
assert_eq!(parsed.architecture, "WaveNet");
assert_eq!(parsed.weights.len(), 4);
assert_eq!(parsed.sample_rate.unwrap(), 48000.0);
let meta = parsed.metadata.as_ref().unwrap();
assert_eq!(meta.input_level_dbu.unwrap(), 12.0);
assert_eq!(meta.output_level_dbu.unwrap(), 11.5);
assert_eq!(meta.loudness.unwrap(), -18.0);
assert_eq!(meta.name.as_deref(), Some("Super Twin"));
assert_eq!(meta.modeled_by.as_deref(), Some("John Doe"));
assert_eq!(meta.gear_make.as_deref(), Some("Fender"));
let topo = get_wavenet_topology(&parsed);
assert_eq!(
topo,
WavenetTopologyResult::Known(NamWavenetTopology::Feather)
);
}
#[test]
fn test_parse_lstm() {
let json_str = r#"{
"version": "0.5.4",
"architecture": "LSTM",
"config": {
"num_layers": 2,
"hidden_size": 16,
"layers": []
},
"weights": [0.1, 0.2]
}"#;
let parsed = parse_nam_json(json_str).expect("Failed to parse LSTM NAM JSON");
assert_eq!(parsed.architecture, "LSTM");
let topo = get_lstm_topology(&parsed);
assert_eq!(topo, Ok(Some((2, 16))));
}
fn make_wavenet_json(
channels: usize,
dils_0: &[usize],
dils_1: &[usize],
head_size: usize,
) -> String {
let d0: Vec<String> = dils_0.iter().map(|d| d.to_string()).collect();
let d1: Vec<String> = dils_1.iter().map(|d| d.to_string()).collect();
format!(
r#"{{
"version": "0.5.4",
"architecture": "WaveNet",
"config": {{
"layers": [
{{
"channels": {channels}, "kernel_size": 3, "head_size": {head_size},
"dilations": [{}],
"gated": false, "head_bias": false
}},
{{
"channels": {channels}, "kernel_size": 3, "head_size": {head_size},
"dilations": [{}],
"gated": false, "head_bias": true
}}
],
"head": null, "head_scale": 0.02
}},
"weights": [0.0]
}}"#,
d0.join(","),
d1.join(",")
)
}
#[test]
fn test_topology_standard() {
let std_d = [1, 2, 4, 8, 16, 32, 64, 128, 256, 512];
let json = make_wavenet_json(16, &std_d, &std_d, 8);
let parsed = parse_nam_json(&json).unwrap();
assert_eq!(
get_wavenet_topology(&parsed),
WavenetTopologyResult::Known(NamWavenetTopology::Standard)
);
}
#[test]
fn test_topology_lite() {
let d0 = [1, 2, 4, 8, 16, 32, 64];
let d1 = [128, 256, 512, 1, 2, 4, 8, 16, 32, 64, 128, 256, 512];
let json = make_wavenet_json(12, &d0, &d1, 6);
let parsed = parse_nam_json(&json).unwrap();
assert_eq!(
get_wavenet_topology(&parsed),
WavenetTopologyResult::Known(NamWavenetTopology::Lite)
);
}
#[test]
fn test_topology_nano() {
let d0 = [1, 2, 4, 8, 16, 32, 64];
let d1 = [128, 256, 512, 1, 2, 4, 8, 16, 32, 64, 128, 256, 512];
let json = make_wavenet_json(4, &d0, &d1, 2);
let parsed = parse_nam_json(&json).unwrap();
assert_eq!(
get_wavenet_topology(&parsed),
WavenetTopologyResult::Known(NamWavenetTopology::Nano)
);
}
#[test]
fn test_topology_invalid_channels() {
let std_d = [1, 2, 4, 8, 16, 32, 64, 128, 256, 512];
let json = make_wavenet_json(10, &std_d, &std_d, 5);
let parsed = parse_nam_json(&json).unwrap();
let result = get_wavenet_topology(&parsed);
assert!(
matches!(result, WavenetTopologyResult::Free(_)),
"10-channel WaveNet should be Free (valid A1, not in catalog), got: {:?}",
result
);
if let WavenetTopologyResult::Free(ref geom) = result {
assert_eq!(geom.channels, vec![10, 10]);
assert_eq!(geom.kernel_size, 3);
assert_eq!(geom.kernel_sizes, vec![3, 3]);
assert_eq!(geom.head_sizes, vec![5, 5]);
assert_eq!(geom.num_arrays, 2);
}
}
#[test]
fn test_topology_free_geometry() {
let dils = [1, 2, 4, 8, 16, 32];
let json = make_wavenet_json(14, &dils, &dils, 7);
let parsed = parse_nam_json(&json).unwrap();
let result = get_wavenet_topology(&parsed);
assert!(
matches!(result, WavenetTopologyResult::Free(_)),
"14-channel WaveNet with custom dilations should be Free, got: {:?}",
result
);
if let WavenetTopologyResult::Free(ref geom) = result {
assert_eq!(geom.channels, vec![14, 14]);
assert_eq!(geom.kernel_size, 3);
assert_eq!(geom.kernel_sizes, vec![3, 3]);
assert_eq!(geom.head_sizes, vec![7, 7]);
assert_eq!(geom.num_arrays, 2);
assert_eq!(geom.dilations.len(), 2);
}
}
#[test]
fn test_topology_accepts_f2_multi_condition_as_free() {
let json = r#"{
"version": "0.5.4",
"architecture": "WaveNet",
"config": {
"layers": [
{
"channels": 8, "kernel_size": 3, "head_size": 4,
"condition_size": 2,
"dilations": [1,2,4,8],
"gated": false, "head_bias": false
},
{
"channels": 8, "kernel_size": 3, "head_size": 4,
"dilations": [1,2,4,8],
"gated": false, "head_bias": true
}
],
"head": null, "head_scale": 0.02
},
"weights": [0.0]
}"#;
let parsed = parse_nam_json(json).unwrap();
let result = get_wavenet_topology(&parsed);
assert!(
matches!(result, WavenetTopologyResult::Free(_)),
"condition_size=2 should be Free (dynamic engine), got: {:?}",
result
);
if let WavenetTopologyResult::Free(ref geom) = result {
assert_eq!(geom.condition_size, 2);
}
}
#[test]
fn test_topology_feather_with_condition_dsp_routes_to_free() {
let json = r#"{
"version": "0.5.4",
"architecture": "WaveNet",
"config": {
"layers": [
{
"input_size": 1, "condition_size": 1, "head_size": 4,
"channels": 8, "kernel_size": 3, "dilations": [1,2,4,8,16,32,64],
"activation": "Tanh", "gated": false, "head_bias": false
},
{
"input_size": 1, "condition_size": 1, "head_size": 4,
"channels": 8, "kernel_size": 3, "dilations": [128,256,512,1,2,4,8,16,32,64,128,256,512],
"activation": "Tanh", "gated": false, "head_bias": true
}
],
"head": null, "head_scale": 0.02,
"condition_dsp": {
"version": "0.5.4",
"architecture": "WaveNet",
"config": {
"layers": [
{
"input_size": 1, "condition_size": 1, "head_size": 1,
"channels": 4, "kernel_size": 3, "dilations": [1,2,4,8],
"activation": "Tanh", "gated": false, "head_bias": true
}
],
"head": null, "head_scale": 0.02
},
"weights": [0.0]
}
},
"weights": [0.0]
}"#;
let parsed = parse_nam_json(json).expect("Failed to parse Feather with condition_dsp");
let result = get_wavenet_topology(&parsed);
match &result {
WavenetTopologyResult::Known(sku) => {
panic!(
"Feather with condition_dsp was incorrectly mapped to catalog SKU {:?} — \
should be Free (dynamic engine). condition_dsp={:?}",
sku, parsed.config.condition_dsp
);
}
WavenetTopologyResult::Free(geom) => {
assert_eq!(geom.channels, vec![8, 8]);
}
WavenetTopologyResult::Rejected(reason) => {
panic!(
"Feather with condition_dsp was Rejected: {} — should be Free (dynamic engine)",
reason
);
}
}
}
#[test]
fn test_topology_f6_post_stack_head_accepted() {
let json = r#"{
"version": "0.5.4",
"architecture": "WaveNet",
"config": {
"layers": [
{
"channels": 8, "kernel_size": 3, "head_size": 4,
"dilations": [1,2,4,8],
"gated": false, "head_bias": false
},
{
"channels": 8, "kernel_size": 3, "head_size": 4,
"dilations": [1,2,4,8],
"gated": false, "head_bias": true
}
],
"head": { "channels": 4, "bias": false, "out_channels": 1, "activation": "Tanh", "kernel_size": 1 },
"head_scale": 0.02
},
"weights": [0.0]
}"#;
let parsed = parse_nam_json(json).unwrap();
let result = get_wavenet_topology(&parsed);
assert!(
matches!(
result,
WavenetTopologyResult::Known(_) | WavenetTopologyResult::Free(_)
),
"post-stack head (F6) should now be accepted, got: {:?}",
result
);
}
#[test]
fn test_topology_rejected_missing_head_size() {
let json = r#"{
"version": "0.5.4",
"architecture": "WaveNet",
"config": {
"layers": [
{
"channels": 8, "kernel_size": 3,
"dilations": [1,2,4,8],
"gated": false, "head_bias": false
},
{
"channels": 8, "kernel_size": 3,
"dilations": [1,2,4,8],
"gated": false, "head_bias": true
}
],
"head": null, "head_scale": 0.02
},
"weights": [0.0]
}"#;
let parsed = parse_nam_json(json).unwrap();
let result = get_wavenet_topology(&parsed);
assert!(
matches!(result, WavenetTopologyResult::Rejected(ref msg) if msg.contains("head_size")),
"missing head_size should be Rejected, got: {:?}",
result
);
}
#[test]
fn test_topology_free_different_channels_per_array() {
let json = r#"{
"version": "0.5.4",
"architecture": "WaveNet",
"config": {
"layers": [
{
"channels": 8, "kernel_size": 3, "head_size": 4,
"dilations": [1,2,4,8],
"gated": false, "head_bias": false
},
{
"channels": 4, "kernel_size": 3, "head_size": 1,
"dilations": [1,2,4,8],
"gated": false, "head_bias": true
}
],
"head": null, "head_scale": 0.02
},
"weights": [0.0]
}"#;
let parsed = parse_nam_json(json).unwrap();
let result = get_wavenet_topology(&parsed);
assert!(
matches!(result, WavenetTopologyResult::Free(_)),
"different channels per layer array is valid WaveNet cascading, got: {:?}",
result
);
if let WavenetTopologyResult::Free(ref geom) = result {
assert_eq!(geom.channels, vec![8, 4]);
assert_eq!(geom.head_sizes, vec![4, 1]);
}
}
#[test]
fn test_topology_rejected_non_wavenet() {
let json = r#"{
"version": "0.5.4",
"architecture": "LSTM",
"config": { "num_layers": 2, "hidden_size": 16, "layers": [] },
"weights": [0.0]
}"#;
let parsed = parse_nam_json(json).unwrap();
let result = get_wavenet_topology(&parsed);
assert!(
matches!(result, WavenetTopologyResult::Rejected(_)),
"non-WaveNet should be Rejected, got: {:?}",
result
);
}
#[test]
fn test_lstm_accepts_mono_channels() {
let json = r#"{
"version": "0.5.4",
"architecture": "LSTM",
"config": {
"num_layers": 2,
"hidden_size": 16,
"in_channels": 1,
"out_channels": 1,
"layers": []
},
"weights": [0.1, 0.2]
}"#;
let parsed = parse_nam_json(json).expect("parse");
assert_eq!(get_lstm_topology(&parsed), Ok(Some((2, 16))));
}
#[test]
fn test_lstm_rejects_multi_in_channels() {
let json = r#"{
"version": "0.5.4",
"architecture": "LSTM",
"config": {
"num_layers": 2,
"hidden_size": 16,
"in_channels": 2,
"layers": []
},
"weights": [0.1, 0.2]
}"#;
let parsed = parse_nam_json(json).expect("parse");
let err = get_lstm_topology(&parsed).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("in_channels=2"),
"Expected multi-channel error for in_channels=2, got: {msg}"
);
}
#[test]
fn test_lstm_rejects_multi_out_channels() {
let json = r#"{
"version": "0.5.4",
"architecture": "LSTM",
"config": {
"num_layers": 2,
"hidden_size": 16,
"out_channels": 2,
"layers": []
},
"weights": [0.1, 0.2]
}"#;
let parsed = parse_nam_json(json).expect("parse");
let err = get_lstm_topology(&parsed).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("out_channels=2"),
"Expected multi-channel error for out_channels=2, got: {msg}"
);
}
#[test]
fn test_lstm_accepts_absent_channels() {
let json = r#"{
"version": "0.5.4",
"architecture": "LSTM",
"config": {
"num_layers": 1,
"hidden_size": 8,
"layers": []
},
"weights": [0.0]
}"#;
let parsed = parse_nam_json(json).expect("parse");
assert_eq!(get_lstm_topology(&parsed), Ok(Some((1, 8))));
}
#[test]
fn test_parse_truncated_json() {
let truncated = r#"{"version": "0.5.4", "architecture": "WaveNet", "config": {"#;
let result = parse_nam_json(truncated);
assert!(
result.is_err(),
"Truncated JSON should return Err, but got Ok"
);
}
#[test]
fn test_parse_missing_architecture() {
let json = r#"{
"version": "0.5.4",
"config": { "layers": [] },
"weights": [0.1, 0.2]
}"#;
let result = parse_nam_json(json);
assert!(
result.is_err(),
"JSON without 'architecture' should return Err, but got Ok"
);
}
#[test]
fn test_parse_missing_weights() {
let json = r#"{
"version": "0.5.4",
"architecture": "LSTM",
"config": { "num_layers": 1, "hidden_size": 8, "layers": [] }
}"#;
let result = parse_nam_json(json);
assert!(
result.is_err(),
"JSON without 'weights' should return Err, but got Ok"
);
}
#[test]
fn test_parse_empty_weights() {
let json = r#"{
"version": "0.5.4",
"architecture": "LSTM",
"config": { "num_layers": 1, "hidden_size": 8, "layers": [] },
"weights": []
}"#;
let result = parse_nam_json(json);
assert!(
result.is_ok(),
"JSON with empty weights should be accepted by the parser (dispatcher rejects later)"
);
let data = result.unwrap();
assert_eq!(data.weights.len(), 0);
}
#[test]
fn test_parse_malformed_config() {
let json = r#"{
"version": "0.5.4",
"architecture": "WaveNet",
"config": "not_an_object",
"weights": [0.1]
}"#;
let result = parse_nam_json(json);
assert!(
result.is_err(),
"JSON with config as string should return Err, but got Ok"
);
}
#[test]
fn test_forward_compat_unknown_field_in_metadata() {
let json = r#"{
"version": "0.5.4",
"architecture": "WaveNet",
"config": {
"layers": [
{
"input_size": 1, "condition_size": 1, "head_size": 4,
"channels": 8, "kernel_size": 3, "dilations": [1,2,4,8,16,32,64],
"activation": "Tanh", "gated": false, "head_bias": false
},
{
"input_size": 1, "condition_size": 1, "head_size": 4,
"channels": 8, "kernel_size": 3,
"dilations": [128,256,512,1,2,4,8,16,32,64,128,256,512],
"activation": "Tanh", "gated": false, "head_bias": true
}
],
"head": null,
"head_scale": 0.02
},
"weights": [0.0123, -0.456],
"sample_rate": 48000,
"metadata": {
"name": "Test",
"creator_email": "dev@example.com",
"future_field": {"nested": 42}
}
}"#;
let result = parse_nam_json(json);
assert!(
result.is_ok(),
"JSON with unknown field in metadata should load (forward-compat)"
);
let data = result.unwrap();
assert_eq!(
data.metadata.as_ref().unwrap().name.as_deref(),
Some("Test")
);
}
#[test]
fn test_reject_deeply_nested_training() {
let inner = r#"{"a":"#.repeat(20);
let outer = "}".repeat(20);
let training_json = format!(r#"{{"a":{}"x"{}"#, inner, outer);
let json = format!(
r#"{{
"version": "0.5.4",
"architecture": "LSTM",
"config": {{ "num_layers": 1, "hidden_size": 8, "layers": [] }},
"weights": [0.1, 0.2],
"metadata": {{
"training": {}
}}
}}"#,
training_json
);
let result = parse_nam_json(&json);
assert!(
result.is_err(),
"JSON with 20-level deep nested training should be rejected"
);
}
#[test]
fn test_weights_within_limit() {
let count = 1000usize;
let weights_str: String = std::iter::once("0.0")
.cycle()
.take(count)
.collect::<Vec<&str>>()
.join(",");
let json = format!(
r#"{{
"version": "0.5.4",
"architecture": "LSTM",
"config": {{ "num_layers": 1, "hidden_size": 8, "layers": [] }},
"weights": [{}]
}}"#,
weights_str
);
let result = parse_nam_json(&json);
assert!(
result.is_ok(),
"JSON with {} weights should load (within limit)",
count
);
assert_eq!(result.unwrap().weights.len(), count);
}
#[test]
fn test_forward_compat_unknown_field_in_config() {
let json = r#"{
"version": "0.5.4",
"architecture": "WaveNet",
"config": {
"layers": [],
"head": null,
"future_config_key": "should_be_ignored"
},
"weights": [0.1, 0.2]
}"#;
let result = parse_nam_json(json);
assert!(
result.is_ok(),
"JSON with unknown field in config should load (forward-compat)"
);
}
#[test]
fn test_forward_compat_unknown_field_at_root() {
let json = r#"{
"version": "0.5.4",
"architecture": "LSTM",
"config": { "num_layers": 1, "hidden_size": 8, "layers": [] },
"weights": [0.1, 0.2],
"future_root_key": "should_be_ignored"
}"#;
let result = parse_nam_json(json);
assert!(
result.is_ok(),
"JSON with unknown field at root should load (forward-compat)"
);
}
#[test]
fn test_weights_exceed_limit_fast_rejection() {
let test_limit = 10_000; use std::io::Write;
let dir = std::env::temp_dir();
let path = dir.join("nam_test_exceed_weights_small.json");
let mut f = std::fs::File::create(&path).unwrap();
write!(f, r#"{{"version":"0.5.4","architecture":"LSTM","config":{{"num_layers":1,"hidden_size":8,"layers":[]}},"weights":["#).unwrap();
for i in 0..test_limit {
if i > 0 {
write!(f, ",").unwrap();
}
write!(f, "0.0").unwrap();
}
write!(f, "]}}").unwrap();
f.flush().unwrap();
drop(f);
let content = std::fs::read_to_string(&path).unwrap();
let result = parse_nam_json(&content);
std::fs::remove_file(&path).ok();
assert!(result.is_ok(), "10k weights should load (within limit)");
assert_eq!(result.unwrap().weights.len(), test_limit);
}
#[test]
fn test_parse_semver() {
assert_eq!(parse_semver("0.5.4"), Some((0, 5, 4)));
assert_eq!(parse_semver("0.6.0"), Some((0, 6, 0)));
assert_eq!(parse_semver("0.9"), Some((0, 9, 0)));
assert_eq!(parse_semver("1.0.0-rc1"), Some((1, 0, 0)));
assert_eq!(parse_semver("2.0"), Some((2, 0, 0)));
assert_eq!(parse_semver("0.10.2"), Some((0, 10, 2)));
assert_eq!(parse_semver("v0.6.0"), Some((0, 6, 0)));
assert_eq!(parse_semver(" V1.2.3 "), Some((1, 2, 3)));
assert_eq!(parse_semver("invalid"), None);
}
fn make_version_json(version: &str) -> String {
format!(
r#"{{
"version": "{version}",
"architecture": "LSTM",
"config": {{ "num_layers": 1, "hidden_size": 8, "layers": [] }},
"weights": [0.0]
}}"#
)
}
#[test]
fn test_version_exact_minimum_accepted() {
let json = make_version_json("0.5.0");
assert!(parse_nam_json(&json).is_ok());
}
#[test]
fn test_version_exact_maximum_accepted() {
let json = make_version_json("0.7.0");
assert!(parse_nam_json(&json).is_ok());
}
#[test]
fn test_version_0_7_1_partial_compatibility() {
let json = make_version_json("0.7.1");
assert!(parse_nam_json(&json).is_ok());
}
#[test]
fn test_version_0_4_9_rejected() {
let json = make_version_json("0.4.9");
let err = parse_nam_json(&json).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("below minimum"),
"Expected 'below minimum' error for 0.4.9, got: {msg}"
);
}
#[test]
fn test_version_0_8_0_rejected() {
let json = make_version_json("0.8.0");
let err = parse_nam_json(&json).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("exceeds maximum"),
"Expected 'exceeds maximum' error for 0.8.0, got: {msg}"
);
}
#[test]
fn test_version_missing_rejected() {
let json = r#"{
"architecture": "LSTM",
"config": { "num_layers": 1, "hidden_size": 8, "layers": [] },
"weights": [0.0]
}"#;
let err = parse_nam_json(json).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("version field is required"),
"Expected 'version field is required' error, got: {msg}"
);
}
#[test]
fn test_version_invalid_format_rejected() {
let json = make_version_json("invalid");
let err = parse_nam_json(&json).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("not valid SemVer"),
"Expected 'not valid SemVer' error, got: {msg}"
);
}
#[test]
fn test_version_major_nonzero_rejected() {
let json = make_version_json("1.0.0");
let err = parse_nam_json(&json).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("exceeds maximum"),
"Expected 'exceeds maximum' for 1.0.0, got: {msg}"
);
}
#[test]
fn test_version_0_5_4_accepted() {
let json = make_version_json("0.5.4");
assert!(parse_nam_json(&json).is_ok());
}
#[test]
fn test_version_0_6_0_accepted() {
let json = make_version_json("0.6.0");
assert!(parse_nam_json(&json).is_ok());
}
#[test]
fn test_version_v_prefix_accepted() {
let json = make_version_json("v0.5.4");
assert!(parse_nam_json(&json).is_ok());
}
#[test]
fn test_version_with_suffix_accepted() {
let json = make_version_json("0.5.4-rc1");
assert!(parse_nam_json(&json).is_ok());
}
#[test]
fn test_is_wavenet_a2_versions() {
use crate::models::a2::A2_DILATIONS;
let mut model = NamModelData {
version: None,
architecture: "WaveNet".to_string(),
config: NamConfig {
layers: vec![],
head: None,
head_scale: None,
num_layers: None,
hidden_size: None,
receptive_field: None,
bias: None,
submodels: None,
..Default::default()
},
weights: vec![],
sample_rate: None,
metadata: None,
weights_layout: WeightsLayout::Original,
};
assert!(!model.is_wavenet_a2());
model.version = Some("0.6.0".to_string());
assert!(!model.is_wavenet_a2());
model.version = Some("0.9.1".to_string());
assert!(!model.is_wavenet_a2());
model.version = Some("2.0".to_string());
assert!(!model.is_wavenet_a2());
model.version = Some("0.5.4".to_string());
model.config.layers = vec![NamLayerConfig {
input_size: None,
condition_size: None,
head_size: None,
channels: None,
kernel_size: None,
dilations: None,
activation: Some("ReLU".to_string()),
gated: None,
head_bias: None,
..Default::default()
}];
assert!(model.is_wavenet_a2());
model.version = Some("0.5.4".to_string());
model.config.layers = vec![NamLayerConfig {
input_size: Some(1),
condition_size: Some(1),
head_size: None,
channels: Some(3),
kernel_size: None,
dilations: Some(A2_DILATIONS.to_vec()),
activation: Some("LeakyReLU".to_string()),
gated: None,
head_bias: None,
..Default::default()
}];
assert!(model.is_wavenet_a2());
model.config.layers = vec![NamLayerConfig {
input_size: Some(1),
condition_size: Some(1),
head_size: None,
channels: Some(8),
kernel_size: None,
dilations: Some(A2_DILATIONS.to_vec()),
activation: Some("LeakyReLU".to_string()),
gated: None,
head_bias: None,
..Default::default()
}];
assert!(model.is_wavenet_a2());
}
fn make_submodel_entry(max_value: f32, _idx: usize) -> String {
format!(
r#"{{
"max_value": {max_value},
"model": {{
"version": "0.5.4",
"architecture": "WaveNet",
"config": {{
"layers": [
{{
"input_size": 1, "condition_size": 1, "head_size": 4,
"channels": 8, "kernel_size": 3,
"dilations": [1,2,4,8,16,32,64],
"activation": "Tanh", "gated": false, "head_bias": false
}}
],
"head": null
}},
"weights": [0.0],
"sample_rate": 48000
}}
}}"#
)
}
fn make_nested_container_entry(max_value: f32) -> String {
let outer_entry = make_submodel_entry(max_value, 0);
format!(
r#"{{
"max_value": {max_value},
"model": {{
"version": "0.7.0",
"architecture": "SlimmableContainer",
"config": {{
"layers": [],
"head": null,
"submodels": [{}]
}},
"weights": [0.1, 0.2],
"sample_rate": 48000
}}
}}"#,
outer_entry
)
}
fn make_container_json(submodels_str: &str) -> String {
format!(
r#"{{
"version": "0.7.0",
"architecture": "SlimmableContainer",
"config": {{
"layers": [],
"head": null,
"submodels": [{submodels_str}]
}},
"weights": [0.0],
"sample_rate": 48000
}}"#
)
}
#[test]
fn test_container_valid_submodels() {
let entries: Vec<String> = (0..2)
.map(|i| make_submodel_entry(0.5 * (i as f32 + 1.0), i))
.collect();
let json = make_container_json(&entries.join(","));
let result = parse_nam_json(&json);
assert!(
result.is_ok(),
"Valid container with 2 submodels should parse"
);
let data = result.unwrap();
assert_eq!(data.architecture, "SlimmableContainer");
assert_eq!(data.config.submodels.as_ref().unwrap().len(), 2);
}
#[test]
fn test_container_exact_limit_submodels() {
let entries: Vec<String> = (0..8)
.map(|i| make_submodel_entry(0.1 * (i as f32 + 1.0), i))
.collect();
let json = make_container_json(&entries.join(","));
let result = parse_nam_json(&json);
assert!(
result.is_ok(),
"Container with 8 submodels (exact limit) should parse"
);
}
#[test]
fn test_reject_too_many_submodels() {
let entries: Vec<String> = (0..9)
.map(|i| make_submodel_entry(0.1 * (i as f32 + 1.0), i))
.collect();
let json = make_container_json(&entries.join(","));
let result = parse_nam_json(&json);
assert!(
result.is_err(),
"Container with 9 submodels should be rejected (exceeds max 8)"
);
}
#[test]
fn test_accept_nested_container() {
let nested = make_nested_container_entry(1.0);
let json = make_container_json(&nested);
let result = parse_nam_json(&json);
assert!(
result.is_ok(),
"Nested container inside submodel should now be accepted by the deserializer"
);
}
#[test]
fn test_reject_empty_submodels() {
let json = make_container_json("");
let result = parse_nam_json(&json);
assert!(
result.is_ok(),
"Empty submodels array is syntactically valid JSON"
);
}
fn make_valid_wavenet_json() -> NamModelData {
let json = r#"{
"version": "0.5.4",
"architecture": "WaveNet",
"config": {
"layers": [
{
"input_size": 1, "condition_size": 1, "head_size": 4,
"channels": 4, "kernel_size": 3, "dilations": [1,2,4,8,16,32,64],
"activation": "Tanh", "gated": false, "head_bias": false
},
{
"input_size": 1, "condition_size": 1, "head_size": 4,
"channels": 4, "kernel_size": 3, "dilations": [128,256,512,1,2,4,8,16,32,64,128,256,512],
"activation": "Tanh", "gated": false, "head_bias": true
}
],
"head": null,
"head_scale": 0.02
},
"weights": [0.0],
"metadata": {}
}"#;
parse_nam_json(json).expect("Valid fixture should parse")
}
#[test]
fn test_topology_accepts_non_null_head() {
let json = r#"{
"version": "0.5.4",
"architecture": "WaveNet",
"config": {
"layers": [
{
"input_size": 1, "condition_size": 1, "head_size": 4,
"channels": 4, "kernel_size": 3, "dilations": [1,2,4,8,16,32,64],
"activation": "Tanh", "gated": false, "head_bias": false
},
{
"input_size": 1, "condition_size": 1, "head_size": 4,
"channels": 4, "kernel_size": 3, "dilations": [128,256,512,1,2,4,8,16,32,64,128,256,512],
"activation": "Tanh", "gated": false, "head_bias": true
}
],
"head": { "channels": 4, "bias": false, "out_channels": 1, "activation": "Tanh", "kernel_size": 1 },
"head_scale": 0.02
},
"weights": [0.0],
"metadata": {}
}"#;
let data = parse_nam_json(json).expect("Fixture should parse");
assert!(
data.config.head.as_ref().is_some_and(|h| !h.is_null()),
"head should be present and non-null"
);
let result = get_wavenet_topology(&data);
assert!(
matches!(
result,
WavenetTopologyResult::Known(_) | WavenetTopologyResult::Free(_)
),
"get_wavenet_topology should accept WaveNet model with post-stack head, got: {result:?}"
);
}
#[test]
fn test_topology_accepts_null_head() {
let data = make_valid_wavenet_json();
let result = get_wavenet_topology(&data);
assert!(
matches!(
result,
WavenetTopologyResult::Known(_) | WavenetTopologyResult::Free(_)
),
"get_wavenet_topology should accept WaveNet model with null head, got: {result:?}"
);
}
use crate::loader::nam_json::validation::{
MAX_HIDDEN_SIZE, MAX_LSTM_LAYERS, MAX_WAVENET_FREE_CHANNELS,
};
#[test]
fn test_lstm_rejects_zero_layers() {
let json = r#"{
"version": "0.5.4",
"architecture": "LSTM",
"config": {
"num_layers": 0,
"hidden_size": 8,
"layers": []
},
"weights": [0.0]
}"#;
let parsed = parse_nam_json(json).expect("parse");
assert_eq!(get_lstm_topology(&parsed), Ok(None));
}
#[test]
fn test_lstm_rejects_num_layers_too_high() {
let json = format!(
r#"{{"version": "0.5.4", "architecture": "LSTM", "config": {{"num_layers": {}, "hidden_size": 8, "layers": []}}, "weights": [0.0]}}"#,
MAX_LSTM_LAYERS + 1
);
let parsed = parse_nam_json(&json).expect("parse");
assert_eq!(get_lstm_topology(&parsed), Ok(None));
}
#[test]
fn test_lstm_rejects_hidden_size_too_high() {
let json = format!(
r#"{{"version": "0.5.4", "architecture": "LSTM", "config": {{"num_layers": 2, "hidden_size": {}, "layers": []}}, "weights": [0.0]}}"#,
crate::loader::nam_json::MAX_HIDDEN_SIZE + 1
);
assert!(parse_nam_json(&json).is_err());
}
#[test]
fn test_lstm_accepts_max_bounds() {
let json = format!(
r#"{{"version": "0.5.4", "architecture": "LSTM", "config": {{"num_layers": {}, "hidden_size": {}, "layers": []}}, "weights": [0.0]}}"#,
MAX_LSTM_LAYERS, MAX_HIDDEN_SIZE
);
let parsed = parse_nam_json(&json).expect("parse");
assert_eq!(
get_lstm_topology(&parsed),
Ok(Some((MAX_LSTM_LAYERS, MAX_HIDDEN_SIZE)))
);
}
fn make_wavenet_channels_json(channels: usize) -> String {
let d0 = [1, 2, 4, 8, 16, 32, 64];
let d1 = [128, 256, 512, 1, 2, 4, 8, 16, 32, 64, 128, 256, 512];
make_wavenet_json_collect_fmt(channels, &d0, &d1, 4)
}
fn make_wavenet_json_collect_fmt(
channels: usize,
dils_0: &[usize],
dils_1: &[usize],
head_size: usize,
) -> String {
let d0_s: Vec<String> = dils_0.iter().map(|d| d.to_string()).collect();
let d1_s: Vec<String> = dils_1.iter().map(|d| d.to_string()).collect();
format!(
r#"{{
"version": "0.5.4",
"architecture": "WaveNet",
"config": {{
"layers": [
{{
"channels": {channels}, "kernel_size": 3, "head_size": {head_size},
"dilations": [{}],
"gated": false, "head_bias": false
}},
{{
"channels": {channels}, "kernel_size": 3, "head_size": {head_size},
"dilations": [{}],
"gated": false, "head_bias": true
}}
],
"head": null, "head_scale": 0.02
}},
"weights": [0.0]
}}"#,
d0_s.join(","),
d1_s.join(",")
)
}
#[test]
fn test_wavenet_free_rejects_channels_too_high() {
let json = make_wavenet_channels_json(MAX_WAVENET_FREE_CHANNELS + 1);
let parsed = parse_nam_json(&json).expect("parse");
let result = get_wavenet_topology(&parsed);
assert!(
matches!(result, WavenetTopologyResult::Rejected(ref msg) if msg.contains("OOM/DoS")),
"Expected Rejected(OOM/DoS), got: {result:?}"
);
}
#[test]
fn test_wavenet_free_accepts_max_channels() {
let json = make_wavenet_channels_json(MAX_WAVENET_FREE_CHANNELS);
let parsed = parse_nam_json(&json).expect("parse");
let result = get_wavenet_topology(&parsed);
assert!(
matches!(result, WavenetTopologyResult::Free(_)),
"Expected Free geometry at max channels, got: {result:?}"
);
}
fn make_a1_wavenet_base_json() -> String {
r#"{
"version": "0.5.4",
"architecture": "WaveNet",
"config": {
"layers": [
{
"channels": 8, "kernel_size": 3, "head_size": 4,
"dilations": [1,2,4,8,16,32,64],
"gated": false, "head_bias": false
},
{
"channels": 8, "kernel_size": 3, "head_size": 4,
"dilations": [128,256,512,1,2,4,8,16,32,64,128,256,512],
"gated": false, "head_bias": true
}
],
"head": null, "head_scale": 0.02
},
"weights": [0.0]
}"#
.to_string()
}
#[test]
fn test_wavenet_a1_rejects_gated_true() {
let json = r#"{
"version": "0.5.4",
"architecture": "WaveNet",
"config": {
"layers": [
{
"channels": 8, "kernel_size": 3, "head_size": 4,
"dilations": [1,2,4,8,16,32,64],
"gated": true, "head_bias": false
},
{
"channels": 8, "kernel_size": 3, "head_size": 4,
"dilations": [128,256,512,1,2,4,8,16,32,64,128,256,512],
"gated": false, "head_bias": true
}
],
"head": null, "head_scale": 0.02
},
"weights": [0.0]
}"#;
let parsed = parse_nam_json(json).expect("parse");
let result = get_wavenet_topology(&parsed);
assert!(
matches!(result, WavenetTopologyResult::Rejected(ref msg) if msg.contains("gated=true")),
"Expected Rejected(gated=true), got: {result:?}"
);
}
#[test]
fn test_wavenet_a1_accepts_gated_false() {
let json = make_a1_wavenet_base_json();
let parsed = parse_nam_json(&json).expect("parse");
let result = get_wavenet_topology(&parsed);
assert!(
matches!(
result,
WavenetTopologyResult::Known(_) | WavenetTopologyResult::Free(_)
),
"Standard A1 with gated=false should be Known or Free, got: {result:?}"
);
}
#[test]
fn test_wavenet_a1_rejects_gating_mode_non_none() {
let json = r#"{
"version": "0.5.4",
"architecture": "WaveNet",
"config": {
"layers": [
{
"channels": 16, "kernel_size": 3, "head_size": 8,
"dilations": [1,2,4,8,16,32,64,128,256,512],
"gated": false, "head_bias": false,
"gating_mode": ["none","add","none","none","none","none","none","none","none","none"]
},
{
"channels": 16, "kernel_size": 3, "head_size": 8,
"dilations": [1,2,4,8,16,32,64,128,256,512],
"gated": false, "head_bias": true
}
],
"head": null, "head_scale": 0.02
},
"weights": [0.0]
}"#;
let parsed = parse_nam_json(json).expect("parse");
let result = get_wavenet_topology(&parsed);
assert!(
matches!(result, WavenetTopologyResult::Rejected(ref msg) if msg.contains("gating_mode")),
"Expected Rejected(gating_mode), got: {result:?}"
);
}
#[test]
fn test_wavenet_a1_rejects_head1x1_active() {
let json = r#"{
"version": "0.5.4",
"architecture": "WaveNet",
"config": {
"layers": [
{
"channels": 16, "kernel_size": 3, "head_size": 8,
"dilations": [1,2,4,8,16,32,64,128,256,512],
"gated": false, "head_bias": false,
"head1x1": {"active": true}
},
{
"channels": 16, "kernel_size": 3, "head_size": 8,
"dilations": [1,2,4,8,16,32,64,128,256,512],
"gated": false, "head_bias": true
}
],
"head": null, "head_scale": 0.02
},
"weights": [0.0]
}"#;
let parsed = parse_nam_json(json).expect("parse");
let result = get_wavenet_topology(&parsed);
assert!(
matches!(result, WavenetTopologyResult::Rejected(ref msg) if msg.contains("head1x1")),
"Expected Rejected(head1x1), got: {result:?}"
);
}
#[test]
fn test_wavenet_a1_rejects_layer1x1_active() {
let json = r#"{
"version": "0.5.4",
"architecture": "WaveNet",
"config": {
"layers": [
{
"channels": 16, "kernel_size": 3, "head_size": 8,
"dilations": [1,2,4,8,16,32,64,128,256,512],
"gated": false, "head_bias": false,
"layer1x1": {"active": true, "groups": 1}
},
{
"channels": 16, "kernel_size": 3, "head_size": 8,
"dilations": [1,2,4,8,16,32,64,128,256,512],
"gated": false, "head_bias": true
}
],
"head": null, "head_scale": 0.02
},
"weights": [0.0]
}"#;
let parsed = parse_nam_json(json).expect("parse");
let result = get_wavenet_topology(&parsed);
assert!(
matches!(result, WavenetTopologyResult::Rejected(ref msg) if msg.contains("layer1x1")),
"Expected Rejected(layer1x1), got: {result:?}"
);
}
#[test]
fn test_wavenet_a1_rejects_film_active() {
let json = r#"{
"version": "0.5.4",
"architecture": "WaveNet",
"config": {
"layers": [
{
"channels": 16, "kernel_size": 3, "head_size": 8,
"dilations": [1,2,4,8,16,32,64,128,256,512],
"gated": false, "head_bias": false,
"conv_pre_film": {"active": true}
},
{
"channels": 16, "kernel_size": 3, "head_size": 8,
"dilations": [1,2,4,8,16,32,64,128,256,512],
"gated": false, "head_bias": true
}
],
"head": null, "head_scale": 0.02
},
"weights": [0.0]
}"#;
let parsed = parse_nam_json(json).expect("parse");
let result = get_wavenet_topology(&parsed);
assert!(
matches!(result, WavenetTopologyResult::Rejected(ref msg) if msg.contains("conv_pre_film") && msg.contains("A2 feature")),
"Expected Rejected(FiLM), got: {result:?}"
);
}
use crate::loader::dispatcher::wavenet::build_wavenet;
fn make_a2_dyn_json(channels: usize, bottleneck: usize) -> String {
let kernel_sizes = "6,6,6,6,6,6,6,6,6,6,6,6,6,6,15,15,6,6,6,6,6,6,6";
let dilations = "1,3,7,17,41,101,239,1,3,7,17,41,101,239,1,13,1,3,7,17,41,101,239";
let activations: String = (0..23)
.map(|_| r#"{"type":"LeakyReLU","negative_slope":0.01}"#)
.collect::<Vec<_>>()
.join(",");
format!(
r#"{{
"version": "0.6.0",
"architecture": "WaveNet",
"config": {{
"in_channels": 1,
"head_scale": 0.02,
"head": null,
"layers": [{{
"input_size": 1,
"condition_size": 1,
"channels": {channels},
"bottleneck": {bottleneck},
"head": {{"out_channels": 1, "kernel_size": 16, "bias": true}},
"kernel_sizes": [{kernel_sizes}],
"dilations": [{dilations}],
"activation": [{activations}],
"gating_mode": ["none","none","none","none","none","none","none","none","none","none","none","none","none","none","none","none","none","none","none","none","none","none","none"],
"head1x1": {{"active": false}},
"layer1x1": {{"active": true, "groups": 1}},
"groups_input": 1,
"groups_input_mixin": 1
}}]
}},
"weights": [0.0],
"sample_rate": 48000
}}"#
)
}
#[test]
fn test_a2_dyn_rejects_channels_too_high() {
use crate::loader::nam_json::validation::MAX_A2_DYN_CHANNELS;
let json = make_a2_dyn_json(MAX_A2_DYN_CHANNELS + 1, 16);
let parsed = parse_nam_json(&json).expect("parse");
let err = match build_wavenet(&parsed) {
Err(e) => e.to_string(),
Ok(_) => String::new(),
};
assert!(
err.contains("OOM/DoS"),
"Expected OOM/DoS error, got: {err}"
);
}
#[test]
fn test_a2_dyn_rejects_bottleneck_too_high() {
use crate::loader::nam_json::validation::MAX_A2_DYN_BOTTLENECK;
let json = make_a2_dyn_json(16, MAX_A2_DYN_BOTTLENECK + 1);
let parsed = parse_nam_json(&json).expect("parse");
let err = match build_wavenet(&parsed) {
Err(e) => e.to_string(),
Ok(_) => String::new(),
};
assert!(
err.contains("OOM/DoS"),
"Expected OOM/DoS error, got: {err}"
);
}
#[test]
fn test_a2_dyn_accepts_max_channels_and_bottleneck() {
use crate::loader::nam_json::validation::{MAX_A2_DYN_BOTTLENECK, MAX_A2_DYN_CHANNELS};
let json = make_a2_dyn_json(MAX_A2_DYN_CHANNELS, MAX_A2_DYN_BOTTLENECK);
let parsed = parse_nam_json(&json).expect("parse");
let err_msg = match build_wavenet(&parsed) {
Err(e) => e.to_string(),
Ok(_) => panic!("expected error (at least weight count mismatch)"),
};
assert!(
!err_msg.contains("OOM/DoS"),
"Max channels/bottleneck should not trigger OOM/DoS rejection, got: {err_msg}"
);
}
use crate::loader::loaded_model_pair::LoadedModelPair;
use crate::loader::nam_json::NamMetadata;
fn make_metadata(
loudness_val: Option<f32>,
in_level: Option<f32>,
out_level: Option<f32>,
) -> NamMetadata {
NamMetadata {
loudness: loudness_val,
input_level_dbu: in_level,
output_level_dbu: out_level,
..Default::default()
}
}
fn make_pair(meta: Option<NamMetadata>) -> LoadedModelPair {
LoadedModelPair {
model_l: None,
model_r: None,
input_mult_adj: 1.0,
output_mult_adj: 1.0,
sample_rate: 48000,
architecture: "LSTM".to_string(),
topology: "2x16".to_string(),
metadata: meta,
weights_layout: "Original".to_string(),
}
}
#[test]
fn test_metadata_all_present() {
let meta = make_metadata(Some(-18.0), Some(12.0), Some(11.5));
let pair = make_pair(Some(meta));
assert_eq!(pair.loudness(), Some(-18.0));
assert_eq!(pair.input_level_dbu(), Some(12.0));
assert_eq!(pair.output_level_dbu(), Some(11.5));
assert!(pair.has_loudness());
assert!(pair.has_input_level_dbu());
assert!(pair.has_output_level_dbu());
}
#[test]
fn test_metadata_all_absent() {
let meta = make_metadata(None, None, None);
let pair = make_pair(Some(meta));
assert_eq!(pair.loudness(), None);
assert_eq!(pair.input_level_dbu(), None);
assert_eq!(pair.output_level_dbu(), None);
assert!(!pair.has_loudness());
assert!(!pair.has_input_level_dbu());
assert!(!pair.has_output_level_dbu());
}
#[test]
fn test_metadata_none() {
let pair = make_pair(None);
assert_eq!(pair.loudness(), None);
assert_eq!(pair.input_level_dbu(), None);
assert_eq!(pair.output_level_dbu(), None);
assert!(!pair.has_loudness());
assert!(!pair.has_input_level_dbu());
assert!(!pair.has_output_level_dbu());
}
#[test]
fn test_metadata_partial_only_loudness() {
let meta = make_metadata(Some(-24.0), None, None);
let pair = make_pair(Some(meta));
assert_eq!(pair.loudness(), Some(-24.0));
assert_eq!(pair.input_level_dbu(), None);
assert_eq!(pair.output_level_dbu(), None);
assert!(pair.has_loudness());
assert!(!pair.has_input_level_dbu());
assert!(!pair.has_output_level_dbu());
}
#[test]
fn test_metadata_partial_only_input() {
let meta = make_metadata(None, Some(6.0), None);
let pair = make_pair(Some(meta));
assert_eq!(pair.loudness(), None);
assert_eq!(pair.input_level_dbu(), Some(6.0));
assert_eq!(pair.output_level_dbu(), None);
assert!(!pair.has_loudness());
assert!(pair.has_input_level_dbu());
assert!(!pair.has_output_level_dbu());
}
#[test]
fn test_metadata_partial_only_output() {
let meta = make_metadata(None, None, Some(-3.0));
let pair = make_pair(Some(meta));
assert_eq!(pair.loudness(), None);
assert_eq!(pair.input_level_dbu(), None);
assert_eq!(pair.output_level_dbu(), Some(-3.0));
assert!(!pair.has_loudness());
assert!(!pair.has_input_level_dbu());
assert!(pair.has_output_level_dbu());
}
fn make_linear_json(implementation: &str, receptive_field: usize) -> String {
format!(
r#"{{
"version": "0.5.4",
"architecture": "Linear",
"config": {{
"layers": [],
"head": null,
"receptive_field": {receptive_field},
"bias": true,
"implementation": "{implementation}"
}},
"weights": [0.0, 1.0]
}}"#
)
}
#[test]
fn test_linear_implementation_case_insensitive_roundtrip() {
let json = make_linear_json("auto", 128);
let parsed = parse_nam_json(&json).expect("parse");
let (rf, has_bias, imp) = get_linear_topology(&parsed).expect("Linear topology");
assert_eq!(rf, 128);
assert!(has_bias);
assert_eq!(imp, LinearImplementation::Auto);
}
#[test]
fn test_linear_implementation_all_variants_lowercase() {
for (input, expected) in &[
("auto", LinearImplementation::Auto),
("direct", LinearImplementation::Direct),
("fft", LinearImplementation::Fft),
] {
let json = make_linear_json(input, 64);
let parsed = parse_nam_json(&json).expect("parse");
let (_, _, imp) = get_linear_topology(&parsed).expect("Linear topology");
assert_eq!(
imp, *expected,
"implementation=\"{input}\" should parse as {expected:?}, got {imp:?}"
);
}
}
#[test]
fn test_linear_implementation_mixed_case_roundtrip() {
for (input, expected) in &[
("Auto", LinearImplementation::Auto),
("AUTO", LinearImplementation::Auto),
("Direct", LinearImplementation::Direct),
("DIRECT", LinearImplementation::Direct),
("Fft", LinearImplementation::Fft),
("FFT", LinearImplementation::Fft),
] {
let json = make_linear_json(input, 32);
let parsed = parse_nam_json(&json).expect("parse");
let (_, _, imp) = get_linear_topology(&parsed).expect("Linear topology");
assert_eq!(
imp, *expected,
"implementation=\"{input}\" should parse as {expected:?}, got {imp:?}"
);
}
}
#[test]
fn test_linear_implementation_missing_defaults_to_auto() {
let json = r#"{
"version": "0.5.4",
"architecture": "Linear",
"config": {
"layers": [],
"head": null,
"receptive_field": 256,
"bias": false
},
"weights": [0.0]
}"#;
let parsed = parse_nam_json(json).expect("parse");
let (_, _, imp) = get_linear_topology(&parsed).expect("Linear topology");
assert_eq!(imp, LinearImplementation::Auto);
}
#[test]
fn test_linear_implementation_invalid_falls_back_to_auto() {
let json = make_linear_json("legacy", 100);
let parsed = parse_nam_json(&json).expect("parse");
let (_, _, imp) = get_linear_topology(&parsed).expect("Linear topology");
assert_eq!(imp, LinearImplementation::Auto);
}
#[test]
fn test_reject_object_activation_fail_closed() {
let json = r#"{
"version": "0.5.4",
"architecture": "WaveNet",
"config": {
"layers": [
{
"input_size": 1, "condition_size": 1, "head_size": 4,
"channels": 8, "kernel_size": 3, "dilations": [1,2,4,8,16,32,64],
"activation": {"type": "Softsign"}, "gated": false, "head_bias": false
}
],
"head": null,
"head_scale": 0.02
},
"weights": [0.0, 0.0],
"sample_rate": 48000
}"#;
let parsed = parse_nam_json(json).expect("object activation must be accepted (S13.2)");
assert_eq!(parsed.config.layers[0].activation, None);
assert!(parsed.config.layers[0].layer_raw.is_some());
}
#[test]
fn test_reject_bool_activation_fail_closed() {
let json = r#"{
"version": "0.5.4",
"architecture": "WaveNet",
"config": {
"layers": [
{
"input_size": 1, "condition_size": 1, "head_size": 4,
"channels": 8, "kernel_size": 3, "dilations": [1,2,4,8,16,32,64],
"activation": true, "gated": false, "head_bias": false
}
],
"head": null,
"head_scale": 0.02
},
"weights": [0.0, 0.0],
"sample_rate": 48000
}"#;
let err = parse_nam_json(json).expect_err("bool activation should fail closed");
let msg = err.to_string();
assert!(
msg.contains("unsupported activation format"),
"expected 'unsupported activation format' error, got: {msg}"
);
}