From 05cf7fc26c5eda657e0c0c9dd284ebc8aa29b48b Mon Sep 17 00:00:00 2001 From: LucasDLTG Date: Mon, 20 Jul 2026 21:00:36 +0200 Subject: [PATCH] feat: add test for mappers --- src/dsp408/mapper/channel.rs | 531 +++++++++++++++++++++++++++++++++++ src/dsp408/mapper/global.rs | 122 ++++++++ 2 files changed, 653 insertions(+) diff --git a/src/dsp408/mapper/channel.rs b/src/dsp408/mapper/channel.rs index 744d3a5..1491cec 100644 --- a/src/dsp408/mapper/channel.rs +++ b/src/dsp408/mapper/channel.rs @@ -856,3 +856,534 @@ pub fn copy_channel_to_raw(source: types::Channel, destination: types::Channel) Ok((channel_to_raw_value(source), channel_to_raw_value(destination))) } + +#[cfg(test)] +mod tests { + use super::*; + + // ------------------------------------------------------------------ + // CHANNEL + // ------------------------------------------------------------------ + + #[test] + fn channel_round_trip_all_inputs() { + let channels = [ + types::InputChannel::InA, + types::InputChannel::InB, + types::InputChannel::InC, + types::InputChannel::InD, + ]; + for ch in channels { + let raw = input_channel_to_raw(ch).unwrap(); + let decoded = raw_to_channel(raw).unwrap(); + assert_eq!(decoded, types::Channel::Input(ch)); + } + } + + #[test] + fn channel_round_trip_all_outputs() { + let channels = [ + types::OutputChannel::Out1, + types::OutputChannel::Out2, + types::OutputChannel::Out3, + types::OutputChannel::Out4, + types::OutputChannel::Out5, + types::OutputChannel::Out6, + types::OutputChannel::Out7, + types::OutputChannel::Out8, + ]; + for ch in channels { + let raw = output_channel_to_raw(ch).unwrap(); + let decoded = raw_to_channel(raw).unwrap(); + assert_eq!(decoded, types::Channel::Output(ch)); + } + } + + #[test] + fn channel_specific_raw_values() { + assert_eq!(channel_to_raw(types::Channel::Input(types::InputChannel::InA)).unwrap(), 0x00); + assert_eq!(channel_to_raw(types::Channel::Input(types::InputChannel::InD)).unwrap(), 0x03); + assert_eq!(channel_to_raw(types::Channel::Output(types::OutputChannel::Out1)).unwrap(), 0x04); + assert_eq!(channel_to_raw(types::Channel::Output(types::OutputChannel::Out8)).unwrap(), 0x0B); + } + + // ------------------------------------------------------------------ + // NAME + // ------------------------------------------------------------------ + + #[test] + fn channel_name_round_trip() { + let name = "In ABCDE"; + let raw = channel_name_to_raw(name).unwrap(); + assert_eq!(raw.len(), client_constants::CHANNEL_NAME_LENGTH); + let decoded = raw_to_channel_name(&raw).unwrap(); + assert_eq!(decoded, name); + } + + #[test] + fn channel_name_padding_is_applied() { + let raw = channel_name_to_raw(" Kick ").unwrap(); + assert_eq!(raw.len(), client_constants::CHANNEL_NAME_LENGTH); + } + + // ------------------------------------------------------------------ + // ATTACK / RELEASE / HOLD (simple offset-by-one mappings) + // ------------------------------------------------------------------ + + #[test] + fn attack_ms_mapping() { + assert_eq!(attack_ms_to_raw(1).unwrap(), 0); + assert_eq!(attack_ms_to_raw(999).unwrap(), 998); + assert_eq!(raw_to_attack_ms(0).unwrap(), 1); + assert_eq!(raw_to_attack_ms(998).unwrap(), 999); + } + + #[test] + fn attack_ms_round_trip() { + for ms in [1u16, 50, 500, 999] { + let raw = attack_ms_to_raw(ms).unwrap(); + assert_eq!(raw_to_attack_ms(raw).unwrap(), ms); + } + } + + #[test] + fn release_ms_mapping() { + assert_eq!(release_ms_to_raw(10).unwrap(), 9); + assert_eq!(release_ms_to_raw(3000).unwrap(), 2999); + assert_eq!(raw_to_release_ms(9).unwrap(), 10); + assert_eq!(raw_to_release_ms(2999).unwrap(), 3000); + } + + #[test] + fn release_ms_round_trip() { + for ms in [10u16, 100, 1500, 3000] { + let raw = release_ms_to_raw(ms).unwrap(); + assert_eq!(raw_to_release_ms(raw).unwrap(), ms); + } + } + + #[test] + fn hold_ms_mapping() { + assert_eq!(hold_ms_to_raw(10).unwrap(), 9); + assert_eq!(hold_ms_to_raw(999).unwrap(), 998); + assert_eq!(raw_to_hold_ms(9).unwrap(), 10); + assert_eq!(raw_to_hold_ms(998).unwrap(), 999); + } + + #[test] + fn hold_ms_round_trip() { + for ms in [10u16, 250, 999] { + let raw = hold_ms_to_raw(ms).unwrap(); + assert_eq!(raw_to_hold_ms(raw).unwrap(), ms); + } + } + + // ------------------------------------------------------------------ + // GATE THRESHOLD + // ------------------------------------------------------------------ + + #[test] + fn gate_threshold_bounds() { + assert_eq!(gate_threshold_db_to_raw(-90.0).unwrap(), 0); + assert_eq!(gate_threshold_db_to_raw(0.0).unwrap(), 180); // (0 - -90) * 2 + } + + #[test] + fn gate_threshold_round_trip() { + for db in [-90.0f32, -45.5, -20.25, 0.0] { + let raw = gate_threshold_db_to_raw(db).unwrap(); + let decoded = raw_to_gate_threshold_db(raw).unwrap(); + assert!((decoded - (db * 2.0).round() / 2.0).abs() < 1e-4); + } + } + + // ------------------------------------------------------------------ + // EQ GAIN + // ------------------------------------------------------------------ + + #[test] + fn eq_gain_round_trip_endpoints() { + for db in [-12.0f32, 0.0, 12.0] { + let raw = eq_gain_db_to_raw(db).unwrap(); + let decoded = raw_to_eq_gain(raw).unwrap(); + assert!((decoded - db).abs() < 1e-2); + } + } + + #[test] + fn eq_gain_round_trip_arbitrary() { + for db in [-7.3f32, 3.9, 11.99] { + let raw = eq_gain_db_to_raw(db).unwrap(); + let decoded = raw_to_eq_gain(raw).unwrap(); + // allow one quantization step of tolerance + assert!((decoded - db).abs() <= eq_gain_step_db() + 1e-4); + } + } + + // ------------------------------------------------------------------ + // PEQ BAND + // ------------------------------------------------------------------ + + #[test] + fn peq_band_output_all_bands() { + let bands = [ + types::PEQBand::B1, types::PEQBand::B2, types::PEQBand::B3, + types::PEQBand::B4, types::PEQBand::B5, types::PEQBand::B6, + types::PEQBand::B7, types::PEQBand::B8, types::PEQBand::B9, + ]; + let out_channel = types::Channel::Output(types::OutputChannel::Out1); + for (i, band) in bands.into_iter().enumerate() { + assert_eq!(peq_band_to_raw(band, out_channel).unwrap(), i as u8); + } + } + + #[test] + fn peq_band_input_allowed_bands() { + let bands = [ + types::PEQBand::B1, types::PEQBand::B2, types::PEQBand::B3, + types::PEQBand::B4, types::PEQBand::B5, types::PEQBand::B6, + types::PEQBand::B7, types::PEQBand::B8, + ]; + let in_channel = types::Channel::Input(types::InputChannel::InA); + for (i, band) in bands.into_iter().enumerate() { + assert_eq!(peq_band_to_raw(band, in_channel).unwrap(), i as u8); + } + } + + // ------------------------------------------------------------------ + // GAIN (channel gain) + // ------------------------------------------------------------------ + + #[test] + fn gain_round_trip_low_range() { + for db in [-60.0f32, -40.0, -20.5] { + let raw = gain_db_to_raw(db).unwrap(); + let decoded = raw_to_gain(raw).unwrap(); + assert!((decoded - db).abs() < 1e-3); + } + } + + #[test] + fn gain_round_trip_high_range() { + for db in [-19.9f32, 0.0, 12.0] { + let raw = gain_db_to_raw(db).unwrap(); + let decoded = raw_to_gain(raw).unwrap(); + assert!((decoded - db).abs() < 1e-3); + } + } + + #[test] + fn gain_split_boundary_is_low_step() { + let below = gain_db_to_raw(-20.5).unwrap(); + let above = gain_db_to_raw(-19.9).unwrap(); + assert_eq!(raw_to_gain(below).unwrap(), -20.5); + assert!((raw_to_gain(above).unwrap() - (-19.9)).abs() < 1e-3); + } + + // ------------------------------------------------------------------ + // CONTIGUOUS FREQUENCY + // ------------------------------------------------------------------ + + #[test] + fn frequency_round_trip_endpoints() { + for hz in [19.7f32, 20160.0] { + let raw = frequency_hz_to_raw(hz).unwrap(); + let decoded = raw_to_frequency_hz(raw).unwrap(); + // log scale quantization tolerance: within 1% of range + assert!((decoded - hz).abs() / hz < 0.01); + } + } + + #[test] + fn frequency_round_trip_mid() { + let hz = 1000.0f32; + let raw = frequency_hz_to_raw(hz).unwrap(); + let decoded = raw_to_frequency_hz(raw).unwrap(); + assert!((decoded - hz).abs() / hz < 0.02); + } + + // ------------------------------------------------------------------ + // DISCRETE FREQUENCY + // ------------------------------------------------------------------ + + #[test] + fn discrete_frequency_round_trip_all() { + let freqs = [ + types::DiscreteFrequency::F20, types::DiscreteFrequency::F25, + types::DiscreteFrequency::F31_5, types::DiscreteFrequency::F40, + types::DiscreteFrequency::F50, types::DiscreteFrequency::F63, + types::DiscreteFrequency::F80, types::DiscreteFrequency::F100, + types::DiscreteFrequency::F125, types::DiscreteFrequency::F160, + types::DiscreteFrequency::F200, types::DiscreteFrequency::F250, + types::DiscreteFrequency::F315, types::DiscreteFrequency::F400, + types::DiscreteFrequency::F500, types::DiscreteFrequency::F630, + types::DiscreteFrequency::F800, types::DiscreteFrequency::F1000, + types::DiscreteFrequency::F1250, types::DiscreteFrequency::F1600, + types::DiscreteFrequency::F2000, types::DiscreteFrequency::F2500, + types::DiscreteFrequency::F3150, types::DiscreteFrequency::F4000, + types::DiscreteFrequency::F5000, types::DiscreteFrequency::F6300, + types::DiscreteFrequency::F8000, types::DiscreteFrequency::F10000, + types::DiscreteFrequency::F12500, types::DiscreteFrequency::F16000, + types::DiscreteFrequency::F20000, + ]; + for (i, f) in freqs.into_iter().enumerate() { + let raw = frequency_value_to_raw(f).unwrap(); + assert_eq!(raw, i as u8); + assert_eq!(raw_to_frequency_value(raw).unwrap(), f); + } + } + + // ------------------------------------------------------------------ + // CROSSOVER FILTER + // ------------------------------------------------------------------ + + #[test] + fn crossover_filter_round_trip_all() { + let filters = [ + types::CrossoverFilter::Bypass, types::CrossoverFilter::Bw6, + types::CrossoverFilter::Bl6, types::CrossoverFilter::Bw12, + types::CrossoverFilter::Bl12, types::CrossoverFilter::Lk12, + types::CrossoverFilter::Bw18, types::CrossoverFilter::Bl18, + types::CrossoverFilter::Bw24, types::CrossoverFilter::Bl24, + types::CrossoverFilter::Lk24, types::CrossoverFilter::Bw30, + types::CrossoverFilter::Bl30, types::CrossoverFilter::Bw36, + types::CrossoverFilter::Bl36, types::CrossoverFilter::Lk36, + types::CrossoverFilter::Bw42, types::CrossoverFilter::Bl42, + types::CrossoverFilter::Bw48, types::CrossoverFilter::Bl48, + types::CrossoverFilter::Lk48, + ]; + for (i, f) in filters.into_iter().enumerate() { + let raw = crossover_filter_to_raw(f).unwrap(); + assert_eq!(raw, i as u8); + assert_eq!(raw_to_crossover_filter(raw).unwrap(), f); + } + } + + // ------------------------------------------------------------------ + // PEQ Q + // ------------------------------------------------------------------ + + #[test] + fn peq_q_round_trip_peak_filter() { + for q in [0.40f32, 1.0, 10.0, 128.0] { + let raw = peq_q_to_raw(q, types::PEQFilter::Peak).unwrap(); + let decoded = raw_to_peq_q(raw).unwrap(); + // log scale, allow 5% relative tolerance + assert!((decoded - q).abs() / q < 0.05); + } + } + + #[test] + fn peq_q_round_trip_shelf_filter() { + for q in [0.40f32, 1.5, 3.0] { + let raw = peq_q_to_raw(q, types::PEQFilter::LowShelf).unwrap(); + let decoded = raw_to_peq_q(raw).unwrap(); + assert!((decoded - q).abs() / q < 0.05); + } + } + + // ------------------------------------------------------------------ + // PEQ FILTER + // ------------------------------------------------------------------ + + #[test] + fn peq_filter_round_trip_all() { + let filters = [ + types::PEQFilter::Peak, types::PEQFilter::LowShelf, + types::PEQFilter::HighShelf, types::PEQFilter::Lp6Db, + types::PEQFilter::Lp12Db, types::PEQFilter::Hp6Db, + types::PEQFilter::Hp12Db, types::PEQFilter::AllPass1, + types::PEQFilter::AllPass2, + ]; + for (i, f) in filters.into_iter().enumerate() { + let raw = peq_filter_to_raw(f).unwrap(); + assert_eq!(raw, i as u8); + assert_eq!(raw_to_peq_filter(raw).unwrap(), f); + } + } + + // ------------------------------------------------------------------ + // RATIO + // ------------------------------------------------------------------ + + #[test] + fn ratio_round_trip_all() { + let ratios = [ + types::Ratio::Ratio1_1, types::Ratio::Ratio1_1_1, types::Ratio::Ratio1_1_3, + types::Ratio::Ratio1_1_5, types::Ratio::Ratio1_1_7, types::Ratio::Ratio1_2, + types::Ratio::Ratio1_2_5, types::Ratio::Ratio1_3, types::Ratio::Ratio1_3_5, + types::Ratio::Ratio1_4, types::Ratio::Ratio1_5, types::Ratio::Ratio1_6, + types::Ratio::Ratio1_8, types::Ratio::Ratio1_10, types::Ratio::Ratio1_20, + types::Ratio::Limit, + ]; + for (i, r) in ratios.into_iter().enumerate() { + let raw = ratio_to_raw(r).unwrap(); + assert_eq!(raw, i as u16); + assert_eq!(raw_to_ratio(raw).unwrap(), r); + } + } + + // ------------------------------------------------------------------ + // KNEE + // ------------------------------------------------------------------ + + #[test] + fn knee_is_identity_mapping() { + for db in [0u16, 6, 12] { + let raw = knee_db_to_raw(db).unwrap(); + assert_eq!(raw, db); + assert_eq!(raw_to_knee_db(raw).unwrap(), db); + } + } + + // ------------------------------------------------------------------ + // THRESHOLD + // ------------------------------------------------------------------ + + #[test] + fn threshold_round_trip() { + for db in [-90.0f32, -35.5, 0.0, 20.0] { + let raw = threshold_db_to_raw(db).unwrap(); + let decoded = raw_to_threshold_db(raw).unwrap(); + assert!((decoded - (db * 2.0).round() / 2.0).abs() < 1e-4); + } + } + + // ------------------------------------------------------------------ + // LINK + // ------------------------------------------------------------------ + + #[test] + fn link_input_channels() { + let source = types::Channel::Input(types::InputChannel::InA); + let destinations = vec![ + types::Channel::Input(types::InputChannel::InA), + types::Channel::Input(types::InputChannel::InC), + ]; + let mask = link_channels_to_raw(source, &destinations).unwrap(); + assert_eq!(mask, 0b0000_0101); + + let source_raw = channel_to_raw(source).unwrap(); + let decoded = raw_to_link_channels(source_raw, mask).unwrap(); + assert_eq!(decoded, destinations); + } + + #[test] + fn link_output_channels() { + let source = types::Channel::Output(types::OutputChannel::Out2); + let destinations = vec![ + types::Channel::Output(types::OutputChannel::Out2), + types::Channel::Output(types::OutputChannel::Out5), + types::Channel::Output(types::OutputChannel::Out8), + ]; + let mask = link_channels_to_raw(source, &destinations).unwrap(); + assert_eq!(mask, (1 << 1) | (1 << 4) | (1 << 7)); + + let source_raw = channel_to_raw(source).unwrap(); + let decoded = raw_to_link_channels(source_raw, mask).unwrap(); + assert_eq!(decoded, destinations); + } + + // ------------------------------------------------------------------ + // MATRIX GAIN + // ------------------------------------------------------------------ + + #[test] + fn matrix_gain_round_trip() { + for db in [-60.0f32, -30.25, -19.9, 0.0] { + let raw = matrix_gain_db_to_raw(db).unwrap(); + let decoded = raw_to_matrix_gain(raw).unwrap(); + assert!((decoded - db).abs() < 0.6); + } + } + + // ------------------------------------------------------------------ + // MATRIX + // ------------------------------------------------------------------ + + #[test] + fn matrix_round_trip_all_inputs() { + let all = vec![ + types::InputChannel::InA, + types::InputChannel::InB, + types::InputChannel::InC, + types::InputChannel::InD, + ]; + let raw = matrix_to_raw(&all).unwrap(); + let mut decoded = raw_to_matrix(raw).unwrap(); + decoded.sort_by_key(|c| format!("{:?}", c)); + let mut expected = all.clone(); + expected.sort_by_key(|c| format!("{:?}", c)); + assert_eq!(decoded, expected); + } + + #[test] + fn matrix_round_trip_subset() { + let subset = vec![types::InputChannel::InB, types::InputChannel::InD]; + let raw = matrix_to_raw(&subset).unwrap(); + let decoded = raw_to_matrix(raw).unwrap(); + assert_eq!(decoded.len(), 2); + assert!(decoded.contains(&types::InputChannel::InB)); + assert!(decoded.contains(&types::InputChannel::InD)); + } + + #[test] + fn matrix_empty() { + let raw = matrix_to_raw(&[]).unwrap(); + assert_eq!(raw, 0); + assert!(raw_to_matrix(raw).unwrap().is_empty()); + } + + // ------------------------------------------------------------------ + // DELAY + // ------------------------------------------------------------------ + + #[test] + fn delay_round_trip_milliseconds() { + for ms in [0.0f32, 340.0, 680.0] { + let raw = delay_to_raw(ms, types::DelayUnit::Millisecond).unwrap(); + let decoded = raw_to_delay(raw, types::DelayUnit::Millisecond).unwrap(); + assert!((decoded - ms).abs() < 5.0); // quantized to 8-bit resolution + } + } + + #[test] + fn delay_round_trip_meters() { + for m in [0.0f32, 100.0, 233.58] { + let raw = delay_to_raw(m, types::DelayUnit::Meter).unwrap(); + let decoded = raw_to_delay(raw, types::DelayUnit::Meter).unwrap(); + assert!((decoded - m).abs() < 2.0); + } + } + + #[test] + fn delay_round_trip_feet() { + for ft in [0.0f32, 400.0, 766.329] { + let raw = delay_to_raw(ft, types::DelayUnit::Feet).unwrap(); + let decoded = raw_to_delay(raw, types::DelayUnit::Feet).unwrap(); + assert!((decoded - ft).abs() < 5.0); + } + } + + // ------------------------------------------------------------------ + // COPY CHANNEL + // ------------------------------------------------------------------ + + #[test] + fn copy_channel_inputs() { + let source = types::Channel::Input(types::InputChannel::InA); + let dest = types::Channel::Input(types::InputChannel::InC); + let (raw_source, raw_dest) = copy_channel_to_raw(source, dest).unwrap(); + assert_eq!(raw_source, 0x00); + assert_eq!(raw_dest, 0x02); + } + + #[test] + fn copy_channel_outputs() { + let source = types::Channel::Output(types::OutputChannel::Out1); + let dest = types::Channel::Output(types::OutputChannel::Out8); + let (raw_source, raw_dest) = copy_channel_to_raw(source, dest).unwrap(); + assert_eq!(raw_source, 0x04); + assert_eq!(raw_dest, 0x0B); + } +} \ No newline at end of file diff --git a/src/dsp408/mapper/global.rs b/src/dsp408/mapper/global.rs index 273945d..226ec6c 100644 --- a/src/dsp408/mapper/global.rs +++ b/src/dsp408/mapper/global.rs @@ -146,3 +146,125 @@ pub fn password_to_raw(password: &str) -> Result, MapperError> { Ok(encode_ascii(password, client_constants::PASSWORD_LENGTH, client_constants::PASSWORD_PADDING)?) } + +#[cfg(test)] +mod tests { + use super::*; + + // -------- Delay Unit -------- + + #[test] + fn delay_unit_round_trip() { + for unit in [ + types::DelayUnit::Millisecond, + types::DelayUnit::Meter, + types::DelayUnit::Feet, + ] { + let raw = delay_unit_to_raw(unit).unwrap(); + assert_eq!(raw_to_delay_unit(raw).unwrap(), unit); + } + } + + #[test] + fn delay_unit_rejects_invalid_raw() { + assert!(raw_to_delay_unit(0xff).is_err()); + } + + // -------- Preset Index -------- + + #[test] + fn preset_index_to_raw_bounds() { + assert_eq!(preset_index_to_raw(0).unwrap(), 0); + assert_eq!( + preset_index_to_raw(client_constants::PRESET_INDEX_MAX).unwrap(), + client_constants::PRESET_INDEX_MAX as u8, + ); + } + + #[test] + fn preset_index_without_factory_to_raw_bounds() { + assert_eq!(preset_index_without_factory_to_raw(1).unwrap(), 0); + + assert_eq!( + preset_index_without_factory_to_raw(client_constants::PRESET_INDEX_MAX).unwrap(), + client_constants::PRESET_INDEX_MAX as u8 - 1, + ); + } + + #[test] + fn preset_index_rejects_out_of_range() { + assert!(preset_index_to_raw(client_constants::PRESET_INDEX_MAX + 1).is_err()); + assert!(preset_index_without_factory_to_raw(0).is_err()); + } + + // -------- Config Chunk -------- + + #[test] + fn config_chunk_bounds() { + assert_eq!(config_chunk_to_raw(0).unwrap(), 0); + + assert_eq!( + config_chunk_to_raw(client_constants::CONFIG_CHUNK_INDEX_MAX).unwrap(), + client_constants::CONFIG_CHUNK_INDEX_MAX as u8, + ); + } + + #[test] + fn config_chunk_rejects_invalid() { + assert!(config_chunk_to_raw(client_constants::CONFIG_CHUNK_INDEX_MAX + 1).is_err()); + } + + // -------- Preset Name -------- + + #[test] + fn preset_name_round_trip() { + let name = "DSP408 "; + + let raw = preset_name_to_raw(name).unwrap(); + + assert_eq!( + raw_to_preset_name(&raw).unwrap(), + "DSP408", + ); + } + + #[test] + fn preset_name_rejects_invalid_length() { + assert!(preset_name_to_raw("short").is_err()); + } + + // -------- Input Source -------- + + #[test] + fn input_source_round_trip() { + for source in [ + types::InputType::Analog, + types::InputType::PinkNoise, + types::InputType::WhiteNoise, + types::InputType::SineWave, + ] { + let raw = input_source_to_raw(source).unwrap(); + assert_eq!(raw_to_input_source(raw).unwrap(), source); + } + } + + #[test] + fn input_source_rejects_invalid_raw() { + assert!(raw_to_input_source(0xff).is_err()); + } + + // -------- Password -------- + + #[test] + fn _password_to_raw() { + let raw = password_to_raw("1234").unwrap(); + + assert_eq!(raw, b"1234"); + } + + #[test] + fn password_rejects_invalid_length() { + assert!(password_to_raw("123").is_err()); + assert!(password_to_raw("12345").is_err()); + } +} \ No newline at end of file