diff --git a/src/dsp408/mapper/encoding.rs b/src/dsp408/mapper/encoding.rs index 22b0ad9..89624dd 100644 --- a/src/dsp408/mapper/encoding.rs +++ b/src/dsp408/mapper/encoding.rs @@ -72,3 +72,185 @@ pub fn decode_float16(lo: u8, hi: u8) -> Result { Ok(result) } } + +#[cfg(test)] +mod tests { + use super::*; + + // ------ STRING TESTS ------ + + #[test] + fn test_encode_ascii_exact_length() { + let result = encode_ascii("hello", 5, b' '); + assert_eq!(result.unwrap(), b"hello"); + } + + #[test] + fn test_encode_ascii_pads_short_string() { + let result = encode_ascii("hi", 5, b'\0'); + + assert_eq!( + result.unwrap(), + vec![b'h', b'i', 0, 0, 0] + ); + } + + #[test] + fn test_encode_ascii_rejects_zero_length() { + let result = encode_ascii("hello", 0, b' '); + + assert!(result.is_err()); + assert_eq!( + result.unwrap_err().to_string(), + "Invalid string length: 0" + ); + } + + #[test] + fn test_encode_ascii_rejects_non_ascii() { + let result = encode_ascii("héllo", 10, b' '); + + assert!(result.is_err()); + assert_eq!( + result.unwrap_err().to_string(), + "String must contain ASCII characters only" + ); + } + + #[test] + fn test_encode_ascii_rejects_too_long_string() { + let result = encode_ascii("abcdef", 5, b' '); + + assert!(result.is_err()); + assert_eq!( + result.unwrap_err().to_string(), + "String too long: got 6, expected 5" + ); + } + + #[test] + fn test_decode_ascii_without_strip_keeps_padding() { + let raw = b"abc "; + + let result = decode_ascii(raw, false); + + assert_eq!(result.unwrap(), "abc "); + } + + #[test] + fn test_decode_ascii_strip_removes_spaces() { + let raw = b"abc "; + + let result = decode_ascii(raw, true); + + assert_eq!(result.unwrap(), "abc"); + } + + #[test] + fn test_decode_ascii_strip_removes_null_bytes() { + let raw = b"abc\0\0"; + + let result = decode_ascii(raw, true); + + assert_eq!(result.unwrap(), "abc"); + } + + #[test] + fn test_decode_ascii_rejects_non_ascii() { + let raw = vec![0xff, 0xfe]; + + let result = decode_ascii(&raw, false); + + assert!(result.is_err()); + assert_eq!( + result.unwrap_err().to_string(), + "Invalid ASCII data" + ); + } + + + // ------ FLOAT TESTS ------ + + #[test] + fn test_decode_float16_zero() { + // IEEE754 half precision: 0x0000 + let result = decode_float16(0x00, 0x00); + + assert_eq!(result.unwrap(), 0.0); + } + + #[test] + fn test_decode_float16_negative_zero() { + // IEEE754 half precision: 0x8000 + let result = decode_float16(0x00, 0x80); + + assert_eq!(result.unwrap(), -0.0); + } + + #[test] + fn test_decode_float16_one() { + // IEEE754 half precision: 1.0 = 0x3c00 + let result = decode_float16(0x00, 0x3c); + + assert_eq!(result.unwrap(), 1.0); + } + + #[test] + fn test_decode_float16_negative_one() { + // IEEE754 half precision: -1.0 = 0xbc00 + let result = decode_float16(0x00, 0xbc); + + assert_eq!(result.unwrap(), -1.0); + } + + #[test] + fn test_decode_float16_fraction() { + // IEEE754 half precision: 0.5 = 0x3800 + let result = decode_float16(0x00, 0x38); + + assert_eq!(result.unwrap(), 0.5); + } + + #[test] + fn test_decode_float16_subnormal() { + // Smallest positive subnormal half float = 0x0001 + let result = decode_float16(0x01, 0x00); + + let value = result.unwrap(); + + assert!(value > 0.0); + assert!(value < 0.0001); + } + + #[test] + fn test_decode_float16_positive_infinity() { + // +inf = 0x7c00 + let result = decode_float16(0x00, 0x7c); + + assert_eq!(result.unwrap(), f32::INFINITY); + } + + #[test] + fn test_decode_float16_negative_infinity() { + // -inf = 0xfc00 + let result = decode_float16(0x00, 0xfc); + + assert_eq!(result.unwrap(), f32::NEG_INFINITY); + } + + #[test] + fn test_decode_float16_nan() { + // NaN = exponent all ones + non-zero mantissa + let result = decode_float16(0x01, 0x7c); + + assert!(result.unwrap().is_nan()); + } + + #[test] + fn test_decode_float16_large_number() { + // 65504 = largest finite IEEE754 half float = 0x7bff + let result = decode_float16(0xff, 0x7b); + + assert_eq!(result.unwrap(), 65504.0); + } +} \ No newline at end of file diff --git a/src/dsp408/mapper/response.rs b/src/dsp408/mapper/response.rs index c24bd10..5cbb920 100644 --- a/src/dsp408/mapper/response.rs +++ b/src/dsp408/mapper/response.rs @@ -193,42 +193,318 @@ pub fn map_response(frame: &Frame) -> Result mod tests { use super::*; + fn frame(command: u8, payload: Vec) -> Frame { + Frame { command, payload } + } + #[test] fn acknowledgement() { - let frame = Frame { command: 0x01, payload: vec![] }; + let result = map_response(&frame(0x01, vec![])).unwrap(); + assert_eq!( - map_response(&frame).unwrap(), - DeviceResponseType::Acknowledgement(Acknowledgement { success: true }) + result, + DeviceResponseType::Acknowledgement(Acknowledgement { + success: true + }) + ); + } + + #[test] + fn not_implemented() { + let result = map_response(&frame(0x02, vec![])).unwrap(); + + assert_eq!( + result, + DeviceResponseType::NotImplemented(NotImplemented) + ); + } + + #[test] + fn handshake_ack() { + let result = map_response(&frame(0x10, vec![0x01])).unwrap(); + + assert_eq!( + result, + DeviceResponseType::HandshakeAck(HandshakeAck { + ack: Some(1) + }) + ); + } + + #[test] + fn handshake_ack_invalid_payload_length() { + let result = map_response(&frame(0x10, vec![1, 2])); + + assert!(result.is_err()); + } + + #[test] + fn device_name() { + let result = map_response(&frame(0x13, b"TestDevice".to_vec())) + .unwrap(); + + assert_eq!( + result, + DeviceResponseType::DeviceInfo(DeviceInfo { + name: "TestDevice".into() + }) + ); + } + + #[test] + fn device_name_missing_payload() { + assert!( + map_response(&frame(0x13, vec![])) + .is_err() + ); + } + + #[test] + fn device_name_invalid_ascii() { + assert!( + map_response(&frame(0x13, vec![0xff])) + .is_err() + ); + } + + #[test] + fn current_preset() { + let result = map_response(&frame(0x14, vec![5])) + .unwrap(); + + assert_eq!( + result, + DeviceResponseType::CurrentPreset(CurrentPreset { + index: 5 + }) ); } #[test] fn current_preset_wrong_length_errors() { - let frame = Frame { command: 0x14, payload: vec![1, 2] }; - assert!(map_response(&frame).is_err()); + assert!( + map_response(&frame(0x14, vec![1, 2])) + .is_err() + ); } #[test] - fn unknown_command_falls_through() { - let frame = Frame { command: 0x99, payload: vec![0xAA] }; - match map_response(&frame).unwrap() { - DeviceResponseType::UnknownResponse(r) => { - assert_eq!(r.cmd, "0x99"); - assert_eq!(r.raw, vec![0xAA]); + fn modified_preset() { + let payload = vec![0xff; 20]; + + let result = map_response(&frame(0x22, payload)) + .unwrap(); + + match result { + DeviceResponseType::ModifiedPreset(p) => { + assert_eq!(p.modified.len(), 20); + assert!(p.modified.iter().all(|v| *v)); } - other => panic!("expected UnknownResponse, got {:?}", other), + other => panic!("unexpected {:?}", other), } } + #[test] + fn modified_preset_invalid_length() { + assert!( + map_response(&frame(0x22, vec![0; 19])) + .is_err() + ); + } + + #[test] + fn config_chunk() { + let result = map_response(&frame( + 0x24, + vec![3, 0xaa, 0xbb], + )) + .unwrap(); + + assert_eq!( + result, + DeviceResponseType::ConfigChunk(ConfigChunk { + sub_index: 3, + data: vec![0xaa, 0xbb], + }) + ); + } + #[test] fn config_chunk_empty_payload_defaults_to_zero() { - let frame = Frame { command: 0x24, payload: vec![] }; - match map_response(&frame).unwrap() { - DeviceResponseType::ConfigChunk(c) => { - assert_eq!(c.sub_index, 0); - assert_eq!(c.data, Vec::::new()); + let result = map_response(&frame(0x24, vec![])) + .unwrap(); + + assert_eq!( + result, + DeviceResponseType::ConfigChunk(ConfigChunk { + sub_index: 0, + data: vec![], + }) + ); + } + + #[test] + fn config_chunk_invalid_index() { + assert!( + map_response(&frame(0x24, vec![0x1d])) + .is_err() + ); + } + + #[test] + fn preset_name() { + let result = map_response(&frame( + 0x29, + vec![2, b'A', b'm', b'p'], + )) + .unwrap(); + + assert_eq!( + result, + DeviceResponseType::PresetName(PresetName { + index: 2, + name: "Amp".into(), + }) + ); + } + + #[test] + fn preset_name_missing_index() { + assert!( + map_response(&frame(0x29, vec![])) + .is_err() + ); + } + + #[test] + fn preset_name_invalid_ascii() { + assert!( + map_response(&frame(0x29, vec![1, 0xff])) + .is_err() + ); + } + + #[test] + fn device_flags_locked() { + let mut payload = vec![0; 6]; + payload[5] = 1; + + let result = map_response(&frame(0x2c, payload)) + .unwrap(); + + assert_eq!( + result, + DeviceResponseType::DeviceFlags(DeviceFlags { + is_locked: true + }) + ); + } + + #[test] + fn device_flags_invalid_length() { + assert!( + map_response(&frame(0x2c, vec![0; 5])) + .is_err() + ); + } + + #[test] + fn device_flags_invalid_lock_flag() { + let mut payload = vec![0; 6]; + payload[5] = 2; + + assert!( + map_response(&frame(0x2c, payload)) + .is_err() + ); + } + + #[test] + fn authentication_success() { + let result = map_response(&frame( + 0x2d, + vec![0, 1], + )) + .unwrap(); + + assert_eq!( + result, + DeviceResponseType::AuthenticationResult( + AuthenticationResult { + success: true + } + ) + ); + } + + #[test] + fn authentication_missing_result() { + assert!( + map_response(&frame(0x2d, vec![0])) + .is_err() + ); + } + + #[test] + fn authentication_invalid_flag() { + assert!( + map_response(&frame(0x2d, vec![0, 2])) + .is_err() + ); + } + + #[test] + fn meters_invalid_length() { + assert!( + map_response(&frame(0x40, vec![0; 10])) + .is_err() + ); + } + + #[test] + fn meters_decode() { + // 38 bytes required. + // First 36 bytes are meter values, remaining 2 bytes ignored. + let mut payload = vec![0; 38]; + + // half float 1.0 = 0x3c00 + for i in (0..36).step_by(3) { + payload[i] = 0x00; + payload[i + 1] = 0x3c; + } + + let result = map_response(&frame(0x40, payload)) + .unwrap(); + + match result { + DeviceResponseType::Meters(m) => { + assert_eq!(m.levels.len(), 12); + + for level in m.levels { + assert!(level.is_finite()); + } } - other => panic!("expected ConfigChunk, got {:?}", other), + other => panic!("unexpected {:?}", other), + } + } + + #[test] + fn unknown_command_falls_through() { + let result = map_response(&frame( + 0x99, + vec![0xaa], + )) + .unwrap(); + + match result { + DeviceResponseType::UnknownResponse(r) => { + assert_eq!(r.cmd, "0x99"); + assert_eq!(r.raw, vec![0xaa]); + } + other => panic!( + "expected UnknownResponse, got {:?}", + other + ), } } } diff --git a/src/dsp408/protocol/commands.rs b/src/dsp408/protocol/commands.rs index 6b0441b..241f9b5 100644 --- a/src/dsp408/protocol/commands.rs +++ b/src/dsp408/protocol/commands.rs @@ -273,6 +273,7 @@ pub fn build_peq_band( validate_raw_frequency(raw_freq)?; validate_raw_peq_q(raw_q)?; validate_raw_peq_filter(raw_filter_type)?; + validate_raw_bool(bypass)?; let gain = split_u16(raw_gain); let freq = split_u16(raw_freq); @@ -321,6 +322,7 @@ pub fn build_mute( )->Result,ValidationError>{ validate_raw_channel(channel)?; + validate_raw_bool(muted)?; Ok(build_frame( 0x35, @@ -338,6 +340,7 @@ pub fn build_inverted_gain( )->Result,ValidationError>{ validate_raw_channel(channel)?; + validate_raw_bool(inverted)?; Ok(build_frame( 0x36, @@ -427,6 +430,7 @@ pub fn build_bypass_peq( ) -> Result, ValidationError> { validate_raw_channel(channel)?; + validate_raw_bool(bypass)?; Ok(build_frame( 0x3C, @@ -587,6 +591,7 @@ pub fn build_set_geq_bypass( ) -> Result, ValidationError> { validate_raw_input_channel(raw_channel)?; + validate_raw_bool(bypass)?; Ok(build_frame( 0x49, @@ -596,3 +601,455 @@ pub fn build_set_geq_bypass( ], )) } + +#[cfg(test)] +mod tests { + use super::*; + use super::super::constants::*; + + fn assert_frame( + frame: Vec, + expected_command: u8, + expected_payload: &[u8], + ) { + assert_eq!(frame[5], expected_command); + assert_eq!(&frame[6..frame.len() - 3], expected_payload); + } + + #[test] + fn test_split_u16() { + assert_eq!(split_u16(0x1234), [0x34, 0x12]); + assert_eq!(split_u16(0x0000), [0x00, 0x00]); + assert_eq!(split_u16(0xffff), [0xff, 0xff]); + } + + #[test] + fn test_simple_commands() { + assert_frame( + build_handshake(), + 0x10, + &[], + ); + + assert_frame( + build_disconnect(), + 0x11, + &[], + ); + + assert_frame( + build_acknowledgement(), + 0x12, + &[], + ); + + assert_frame( + build_request_device_name(), + 0x13, + &[], + ); + + assert_frame( + build_get_current_preset(), + 0x14, + &[], + ); + + assert_frame( + build_preset_modification_status(), + 0x22, + &[], + ); + + assert_frame( + build_factory_reset(), + 0x23, + &[], + ); + + assert_frame( + build_get_device_flags(), + 0x2c, + &[], + ); + + assert_frame( + build_get_meter_levels(), + 0x40, + &[], + ); + } + + #[test] + fn test_preset_commands() { + assert_frame( + build_recall(5).unwrap(), + 0x20, + &[5], + ); + + assert_frame( + build_store_current_configuration(3).unwrap(), + 0x21, + &[3], + ); + + assert_frame( + build_get_preset_name(2).unwrap(), + 0x29, + &[2], + ); + + assert_frame( + build_config_chunk(1).unwrap(), + 0x27, + &[1], + ); + + assert_frame( + build_store_current_preset_name(b"TEST ").unwrap(), + 0x26, + b"TEST ", + ); + } + + #[test] + fn test_device_lock_commands() { + assert_frame( + build_unlock_device(b"1234").unwrap(), + 0x2d, + b"1234", + ); + + assert_frame( + build_lock_device(b"1234").unwrap(), + 0x2f, + b"1234", + ); + } + + #[test] + fn test_set_delay_unit() { + assert_frame( + build_set_delay_unit(1).unwrap(), + 0x15, + &[1], + ); + } + + #[test] + fn test_copy_channel() { + assert_frame( + build_copy_channel(1, 3).unwrap(), + 0x2a, + &[1, 3], + ); + } + + #[test] + fn test_compressor_frame() { + assert_frame( + build_set_compressor( + 5, + 1, + 300, + 400, + 2, + 11, + ).unwrap(), + 0x30, + &[ + 5, + 1, 0, + 44, 1, + 144, 1, + 2, 0, + 11, 0, + ], + ); + } + + #[test] + fn test_filters() { + let freq = *FREQUENCY_RAW_RANGE.end(); + + assert_frame( + build_low_pass( + *CHANNELS_RANGE.start(), + freq, + *CROSSOVER_FILTER_RAW_RANGE.start(), + ) + .unwrap(), + 0x31, + &[ + *CHANNELS_RANGE.start(), + (freq & 0xff) as u8, + (freq >> 8) as u8, + *CROSSOVER_FILTER_RAW_RANGE.start(), + ], + ); + + assert_frame( + build_high_pass( + *CHANNELS_RANGE.end(), + freq, + *CROSSOVER_FILTER_RAW_RANGE.end(), + ) + .unwrap(), + 0x32, + &[ + *CHANNELS_RANGE.end(), + (freq & 0xff) as u8, + (freq >> 8) as u8, + *CROSSOVER_FILTER_RAW_RANGE.end(), + ], + ); + } + + #[test] + fn test_peq_band() { + let gain = *EQ_GAIN_RAW_RANGE.end(); + let freq = *FREQUENCY_RAW_RANGE.end(); + + assert_frame( + build_peq_band( + *INPUT_CHANNELS.start(), + *PEQ_INPUT_BAND_RANGE.end(), + gain, + freq, + *PEQ_Q_RAW_RANGE.end(), + *PEQ_FILTER_RAW_RANGE.end(), + 1, + ) + .unwrap(), + 0x33, + &[ + *INPUT_CHANNELS.start(), + *PEQ_INPUT_BAND_RANGE.end(), + (gain & 0xff) as u8, + (gain >> 8) as u8, + (freq & 0xff) as u8, + (freq >> 8) as u8, + *PEQ_Q_RAW_RANGE.end(), + *PEQ_FILTER_RAW_RANGE.end(), + 1, + ], + ); + } + + #[test] + fn test_gain_mute_invert() { + let gain = *CHANNEL_GAIN_RAW_RANGE.end(); + + assert_frame( + build_gain(*CHANNELS_RANGE.end(), gain).unwrap(), + 0x34, + &[ + *CHANNELS_RANGE.end(), + (gain & 0xff) as u8, + (gain >> 8) as u8, + ], + ); + + assert_frame( + build_mute(*CHANNELS_RANGE.start(), 1).unwrap(), + 0x35, + &[*CHANNELS_RANGE.start(), 1], + ); + + assert_frame( + build_inverted_gain(*CHANNELS_RANGE.end(), 1).unwrap(), + 0x36, + &[*CHANNELS_RANGE.end(), 1], + ); + } + + #[test] + fn test_delay_and_input() { + assert_frame( + build_set_delay( + *CHANNELS_RANGE.end(), + *DELAY_RAW_RANGE.end(), + ) + .unwrap(), + 0x38, + &[ + *CHANNELS_RANGE.end(), + 0x00, + *DELAY_RAW_RANGE.end(), + ], + ); + + assert_frame( + build_set_input_source( + *INPUT_SOURCE_TYPE_RAW_RANGE.end(), + *DISCRETE_FREQUENCY_RAW_RANGE.end(), + ) + .unwrap(), + 0x39, + &[ + *INPUT_SOURCE_TYPE_RAW_RANGE.end(), + *DISCRETE_FREQUENCY_RAW_RANGE.end(), + ], + ); + } + + #[test] + fn test_routing_commands() { + assert_frame( + build_matrix_routing( + *OUTPUT_CHANNELS.start(), + MATRIX_INPUT_A_MASK | MATRIX_INPUT_C_MASK, + ) + .unwrap(), + 0x3A, + &[ + *OUTPUT_CHANNELS.start(), + MATRIX_INPUT_A_MASK | MATRIX_INPUT_C_MASK, + ], + ); + + let source = *INPUT_CHANNELS.start(); + let mask = input_link_mask(source).unwrap(); + + assert_frame( + build_link_channel(source, mask).unwrap(), + 0x3B, + &[source, mask], + ); + + assert_frame( + build_bypass_peq(*CHANNELS_RANGE.end(), 1).unwrap(), + 0x3C, + &[*CHANNELS_RANGE.end(), 1], + ); + } + + #[test] + fn test_channel_name() { + let name = [b'A'; CHANNEL_NAME_LENGTH]; + + let mut expected = vec![*CHANNELS_RANGE.start()]; + expected.extend_from_slice(&name); + + assert_frame( + build_store_channel_name( + *CHANNELS_RANGE.start(), + &name, + ) + .unwrap(), + 0x3D, + &expected, + ); + } + + #[test] + fn test_gate_and_limiter() { + let threshold = *GATE_THRESHOLD_RAW_RANGE.end(); + let attack = *ATTACK_RAW_RANGE.end(); + let hold = *HOLD_RAW_RANGE.end(); + let release = *RELEASE_RAW_RANGE.end(); + + assert_frame( + build_set_gate( + *INPUT_CHANNELS.end(), + threshold, + attack, + hold, + release, + ) + .unwrap(), + 0x3E, + &[ + *INPUT_CHANNELS.end(), + (attack & 0xff) as u8, + (attack >> 8) as u8, + (release & 0xff) as u8, + (release >> 8) as u8, + (hold & 0xff) as u8, + (hold >> 8) as u8, + (threshold & 0xff) as u8, + (threshold >> 8) as u8, + ], + ); + + let threshold = *THRESHOLD_RAW_RANGE.end(); + + assert_frame( + build_set_limiter( + *OUTPUT_CHANNELS.end(), + threshold, + attack, + release, + ) + .unwrap(), + 0x3F, + &[ + *OUTPUT_CHANNELS.end(), + (attack & 0xff) as u8, + (attack >> 8) as u8, + (release & 0xff) as u8, + (release >> 8) as u8, + 0x00, + 0x00, + (threshold & 0xff) as u8, + (threshold >> 8) as u8, + ], + ); + } + + #[test] + fn test_matrix_gain() { + let gain = *MATRIX_GAIN_RAW_RANGE.end(); + + assert_frame( + build_matrix_gain( + *INPUT_CHANNELS.end(), + *OUTPUT_CHANNELS.end(), + gain, + ) + .unwrap(), + 0x41, + &[ + *OUTPUT_CHANNELS.end(), + *INPUT_CHANNELS.end(), + (gain & 0xff) as u8, + (gain >> 8) as u8, + ], + ); + } + + #[test] + fn test_geq_commands() { + let gain = *EQ_GAIN_RAW_RANGE.end(); + + assert_frame( + build_set_geq_band( + *INPUT_CHANNELS.start(), + *DISCRETE_FREQUENCY_RAW_RANGE.end(), + gain, + ) + .unwrap(), + 0x48, + &[ + *INPUT_CHANNELS.start(), + *DISCRETE_FREQUENCY_RAW_RANGE.end(), + (gain & 0xff) as u8, + (gain >> 8) as u8, + ], + ); + + assert_frame( + build_set_geq_bypass( + *INPUT_CHANNELS.end(), + 1, + ) + .unwrap(), + 0x49, + &[ + *INPUT_CHANNELS.end(), + 1, + ], + ); + } +} \ No newline at end of file diff --git a/src/dsp408/protocol/frame.rs b/src/dsp408/protocol/frame.rs index b7cfdbe..146e09a 100644 --- a/src/dsp408/protocol/frame.rs +++ b/src/dsp408/protocol/frame.rs @@ -101,19 +101,81 @@ mod tests { use super::*; #[test] - fn build_then_extract_round_trips() { - let cmd = 0x34; - let args = [0x01, 0x02, 0x03]; - let frame = build_frame(cmd, &args); + fn test_checksum() { + let payload = [0x00, 0x01, 0x03, 0x10, 0x20, 0x30]; - let decoded = extract_frame(&frame).unwrap(); - assert_eq!(decoded.command, cmd); - assert_eq!(decoded.payload, vec![0x01, 0x02, 0x03]); + // 0x01 ^ 0x00 ^ 0x01 ^ 0x03 ^ 0x10 ^ 0x20 ^ 0x30 + assert_eq!(checksum(&payload), 0x03); } #[test] - fn too_short_is_an_error() { - let err = extract_frame(&[0x10, 0x02, 0x01]).unwrap_err(); - assert!(err.0.contains("too short")); + fn test_build_frame() { + let frame = build_frame(0x05, &[0xAA, 0xBB]); + + let expected_without_checksum = vec![ + 0x10, 0x02, // header + 0x00, 0x01, 0x03, // device + version + length + 0x05, 0xAA, 0xBB, // command + args + 0x10, 0x03, // footer + ]; + + assert_eq!(&frame[..frame.len() - 1], expected_without_checksum); + + let expected_checksum = checksum(&frame[2..frame.len() - 3]); + assert_eq!(frame[frame.len() - 1], expected_checksum); } -} \ No newline at end of file + + #[test] + fn test_extract_frame_invalid_header() { + let mut frame = build_frame(0x01, &[]); + frame[0] = 0x11; + + let result = extract_frame(&frame); + + assert!(result.is_err()); + } + + #[test] + fn test_extract_frame_invalid_checksum() { + let mut frame = build_frame(0x01, &[]); + let last = frame.len() - 1; + + frame[last] ^= 0xFF; + + let result = extract_frame(&frame); + + assert!(result.is_err()); + } + + #[test] + fn test_extract_frame_not_response() { + let mut frame = build_frame(0x01, &[]); + + // data[2] == 0 means request frame according to your parser + frame[2] = 0x00; + + let result = extract_frame(&frame); + + assert!(result.is_err()); + } + + #[test] + fn test_extract_frame_invalid_device_id() { + let mut frame = build_frame(0x01, &[]); + + frame[3] = 0xFF; + + let result = extract_frame(&frame); + + assert!(result.is_err()); + } + + #[test] + fn test_extract_frame_too_short() { + let data = vec![0x10, 0x02, 0x01]; + + let result = extract_frame(&data); + + assert!(result.is_err()); + } +} diff --git a/src/dsp408/protocol/validators.rs b/src/dsp408/protocol/validators.rs index 4e08d4d..80a1f6c 100644 --- a/src/dsp408/protocol/validators.rs +++ b/src/dsp408/protocol/validators.rs @@ -313,3 +313,495 @@ pub fn validate_raw_matrix_gain(raw_gain: u16) -> Result<(), ValidationError> { || format!("Invalid matrix gain: {:#06x}", raw_gain), ) } + +pub fn validate_raw_bool(value: u8) -> Result<(), ValidationError> { + validate_range( + value, + 0..=1, + || format!("Invalid bool value: {:#04x}", value), + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + // ---- validate_raw_delay_unit: 0x00..=0x03 ---- + #[test] + fn delay_unit_accepts_low_and_high_bounds() { + assert!(validate_raw_delay_unit(0x00).is_ok()); + assert!(validate_raw_delay_unit(0x03).is_ok()); + } + + #[test] + fn delay_unit_rejects_just_above_max() { + assert!(validate_raw_delay_unit(0x04).is_err()); + } + + // ---- validate_raw_preset_index: 0..=20 ---- + #[test] + fn preset_index_accepts_bounds() { + assert!(validate_raw_preset_index(0).is_ok()); + assert!(validate_raw_preset_index(PRESET_INDEX_MAX).is_ok()); + } + + #[test] + fn preset_index_rejects_above_max() { + assert!(validate_raw_preset_index(PRESET_INDEX_MAX + 1).is_err()); + } + + // ---- validate_raw_store_preset_index: 1..=20 (0 reserved, unlike get/plain) ---- + #[test] + fn store_preset_index_rejects_zero() { + assert!(validate_raw_store_preset_index(0).is_err()); + } + + #[test] + fn store_preset_index_accepts_one_through_max() { + assert!(validate_raw_store_preset_index(1).is_ok()); + assert!(validate_raw_store_preset_index(PRESET_INDEX_MAX).is_ok()); + } + + #[test] + fn store_preset_index_rejects_above_max() { + assert!(validate_raw_store_preset_index(PRESET_INDEX_MAX + 1).is_err()); + } + + // ---- validate_raw_get_preset_index: 0..20 (max itself excluded) ---- + #[test] + fn get_preset_index_accepts_zero_and_just_below_max() { + assert!(validate_raw_get_preset_index(0).is_ok()); + assert!(validate_raw_get_preset_index(PRESET_INDEX_MAX - 1).is_ok()); + } + + #[test] + fn get_preset_index_rejects_max_itself() { + // Unlike validate_raw_preset_index, this range is exclusive of PRESET_INDEX_MAX. + assert!(validate_raw_get_preset_index(PRESET_INDEX_MAX).is_err()); + } + + // ---- validate_raw_preset_name: 14 ascii bytes, space-padded ---- + #[test] + fn preset_name_accepts_correctly_padded_ascii() { + let mut name = b"MyPreset".to_vec(); + name.resize(PRESET_NAME_LENGTH, PRESET_NAME_PADDING); + assert!(validate_raw_preset_name(&name).is_ok()); + } + + #[test] + fn preset_name_rejects_wrong_length() { + let name = b"TooShort".to_vec(); + assert!(validate_raw_preset_name(&name).is_err()); + } + + #[test] + fn preset_name_rejects_non_ascii_byte() { + let mut name = b"Bad".to_vec(); + name.push(0x01); // control char, not printable ascii, not the space padding byte + name.resize(PRESET_NAME_LENGTH, PRESET_NAME_PADDING); + assert!(validate_raw_preset_name(&name).is_err()); + } + + // ---- validate_raw_config_chunk_index: 0..=0x1C ---- + #[test] + fn config_chunk_index_accepts_bounds() { + assert!(validate_raw_config_chunk_index(0x00).is_ok()); + assert!(validate_raw_config_chunk_index(CONFIG_CHUNK_INDEX_MAX).is_ok()); + } + + #[test] + fn config_chunk_index_rejects_above_max() { + assert!(validate_raw_config_chunk_index(CONFIG_CHUNK_INDEX_MAX + 1).is_err()); + } + + // ---- validate_raw_channel: 0..=0x0B ---- + #[test] + fn channel_accepts_full_range() { + assert!(validate_raw_channel(0x00).is_ok()); + assert!(validate_raw_channel(0x0B).is_ok()); + } + + #[test] + fn channel_rejects_above_max() { + assert!(validate_raw_channel(0x0C).is_err()); + } + + // ---- validate_raw_input_channel: 0..=0x03 ---- + #[test] + fn input_channel_accepts_its_own_range() { + assert!(validate_raw_input_channel(0x00).is_ok()); + assert!(validate_raw_input_channel(0x03).is_ok()); + } + + #[test] + fn input_channel_rejects_an_output_channel() { + assert!(validate_raw_input_channel(0x04).is_err()); + } + + // ---- validate_raw_output_channel: 0x04..=0x0B ---- + #[test] + fn output_channel_accepts_its_own_range() { + assert!(validate_raw_output_channel(0x04).is_ok()); + assert!(validate_raw_output_channel(0x0B).is_ok()); + } + + #[test] + fn output_channel_rejects_an_input_channel() { + assert!(validate_raw_output_channel(0x03).is_err()); + } + + // ---- validate_raw_copy_channels ---- + #[test] + fn copy_channels_allows_input_to_input() { + assert!(validate_raw_copy_channels(0x00, 0x01).is_ok()); + } + + #[test] + fn copy_channels_allows_output_to_output() { + assert!(validate_raw_copy_channels(0x04, 0x05).is_ok()); + } + + #[test] + fn copy_channels_rejects_same_source_and_destination() { + assert!(validate_raw_copy_channels(0x01, 0x01).is_err()); + } + + #[test] + fn copy_channels_rejects_mixed_input_and_output() { + assert!(validate_raw_copy_channels(0x01, 0x05).is_err()); + } + + #[test] + fn copy_channels_rejects_out_of_range_source() { + assert!(validate_raw_copy_channels(0x0C, 0x01).is_err()); + } + + #[test] + fn copy_channels_rejects_out_of_range_destination() { + assert!(validate_raw_copy_channels(0x01, 0x0C).is_err()); + } + + // ---- validate_raw_password: 4 bytes, null-padded ---- + #[test] + fn password_accepts_null_padded_ascii() { + let mut pw = b"ab".to_vec(); + pw.resize(PASSWORD_LENGTH, PASSWORD_PADDING); + assert!(validate_raw_password(&pw).is_ok()); + } + + #[test] + fn password_rejects_wrong_length() { + assert!(validate_raw_password(b"abcde").is_err()); + } + + #[test] + fn password_rejects_non_ascii_non_padding_byte() { + let pw = vec![b'a', b'b', 0x01, 0x00]; + assert!(validate_raw_password(&pw).is_err()); + } + + // ---- validate_raw_compressor_ratio: 0..=0x000F ---- + #[test] + fn compressor_ratio_accepts_bounds() { + assert!(validate_raw_compressor_ratio(0x0000).is_ok()); + assert!(validate_raw_compressor_ratio(0x000F).is_ok()); + } + + #[test] + fn compressor_ratio_rejects_above_max() { + assert!(validate_raw_compressor_ratio(0x0010).is_err()); + } + + // ---- validate_raw_attack: 0..=0x03E6 ---- + #[test] + fn attack_accepts_bounds() { + assert!(validate_raw_attack(0x0000).is_ok()); + assert!(validate_raw_attack(0x03E6).is_ok()); + } + + #[test] + fn attack_rejects_above_max() { + assert!(validate_raw_attack(0x03E7).is_err()); + } + + // ---- validate_raw_release: 0x0009..=0x0BB7 ---- + #[test] + fn release_rejects_below_min() { + assert!(validate_raw_release(0x0008).is_err()); + } + + #[test] + fn release_accepts_bounds() { + assert!(validate_raw_release(0x0009).is_ok()); + assert!(validate_raw_release(0x0BB7).is_ok()); + } + + #[test] + fn release_rejects_above_max() { + assert!(validate_raw_release(0x0BB8).is_err()); + } + + // ---- validate_raw_compressor_knee: 0..=0x000C ---- + #[test] + fn compressor_knee_accepts_bounds() { + assert!(validate_raw_compressor_knee(0x0000).is_ok()); + assert!(validate_raw_compressor_knee(0x000C).is_ok()); + } + + #[test] + fn compressor_knee_rejects_above_max() { + assert!(validate_raw_compressor_knee(0x000D).is_err()); + } + + // ---- validate_raw_threshold: 0..=0x00DC ---- + #[test] + fn threshold_accepts_bounds() { + assert!(validate_raw_threshold(0x0000).is_ok()); + assert!(validate_raw_threshold(0x00DC).is_ok()); + } + + #[test] + fn threshold_rejects_above_max() { + assert!(validate_raw_threshold(0x00DD).is_err()); + } + + // ---- validate_raw_frequency: 0..=0x012C ---- + #[test] + fn frequency_accepts_bounds() { + assert!(validate_raw_frequency(0x0000).is_ok()); + assert!(validate_raw_frequency(0x012C).is_ok()); + } + + #[test] + fn frequency_rejects_above_max() { + assert!(validate_raw_frequency(0x012D).is_err()); + } + + // ---- validate_raw_crossover_filter: 0..=0x14 ---- + #[test] + fn crossover_filter_accepts_bounds() { + assert!(validate_raw_crossover_filter(0x00).is_ok()); + assert!(validate_raw_crossover_filter(0x14).is_ok()); + } + + #[test] + fn crossover_filter_rejects_above_max() { + assert!(validate_raw_crossover_filter(0x15).is_err()); + } + + // ---- validate_raw_peq_band: depends on whether channel is input or output ---- + #[test] + fn peq_band_accepts_max_band_for_input_channel() { + assert!(validate_raw_peq_band(0x00, 0x07).is_ok()); // input max band + } + + #[test] + fn peq_band_rejects_above_max_for_input_channel() { + assert!(validate_raw_peq_band(0x00, 0x08).is_err()); + } + + #[test] + fn peq_band_accepts_max_band_for_output_channel() { + assert!(validate_raw_peq_band(0x04, 0x08).is_ok()); // output max band (one higher than input) + } + + #[test] + fn peq_band_rejects_above_max_for_output_channel() { + assert!(validate_raw_peq_band(0x04, 0x09).is_err()); + } + + #[test] + fn peq_band_is_unchecked_for_out_of_range_channel() { + // Neither branch fires when the channel is outside both INPUT_CHANNELS and + // OUTPUT_CHANNELS, so the function falls through to Ok(()) regardless of band. + // This documents that behavior rather than asserting it's necessarily "correct". + assert!(validate_raw_peq_band(0x0C, 0xFF).is_ok()); + } + + // ---- validate_raw_peq_q: 0..=0x64 ---- + #[test] + fn peq_q_accepts_bounds() { + assert!(validate_raw_peq_q(0x00).is_ok()); + assert!(validate_raw_peq_q(0x64).is_ok()); + } + + #[test] + fn peq_q_rejects_above_max() { + assert!(validate_raw_peq_q(0x65).is_err()); + } + + // ---- validate_raw_peq_filter: 0..=0x08 ---- + #[test] + fn peq_filter_accepts_bounds() { + assert!(validate_raw_peq_filter(0x00).is_ok()); + assert!(validate_raw_peq_filter(0x08).is_ok()); + } + + #[test] + fn peq_filter_rejects_above_max() { + assert!(validate_raw_peq_filter(0x09).is_err()); + } + + // ---- validate_raw_eq_gain: 0..=0x00F0 ---- + #[test] + fn eq_gain_accepts_bounds() { + assert!(validate_raw_eq_gain(0x0000).is_ok()); + assert!(validate_raw_eq_gain(0x00F0).is_ok()); + } + + #[test] + fn eq_gain_rejects_above_max() { + assert!(validate_raw_eq_gain(0x00F1).is_err()); + } + + // ---- validate_raw_gain: 0..=0x0190 ---- + #[test] + fn gain_accepts_bounds() { + assert!(validate_raw_gain(0x0000).is_ok()); + assert!(validate_raw_gain(0x0190).is_ok()); + } + + #[test] + fn gain_rejects_above_max() { + assert!(validate_raw_gain(0x0191).is_err()); + } + + // ---- validate_raw_channel_delay: 0..=0xFF (the full u8 range) ---- + #[test] + fn channel_delay_accepts_the_entire_u8_range() { + // The range spans all possible u8 values, so there is no invalid input to test; + // this asserts that fact rather than a boundary rejection. + assert!(validate_raw_channel_delay(0x00).is_ok()); + assert!(validate_raw_channel_delay(0xFF).is_ok()); + } + + // ---- validate_raw_input_source: 0..=0x03 ---- + #[test] + fn input_source_accepts_bounds() { + assert!(validate_raw_input_source(0x00).is_ok()); + assert!(validate_raw_input_source(0x03).is_ok()); + } + + #[test] + fn input_source_rejects_above_max() { + assert!(validate_raw_input_source(0x04).is_err()); + } + + // ---- validate_raw_discrete_frequency: 0..=0x1E ---- + #[test] + fn discrete_frequency_accepts_bounds() { + assert!(validate_raw_discrete_frequency(0x00).is_ok()); + assert!(validate_raw_discrete_frequency(0x1E).is_ok()); + } + + #[test] + fn discrete_frequency_rejects_above_max() { + assert!(validate_raw_discrete_frequency(0x1F).is_err()); + } + + // ---- validate_raw_matrix_bitmask: 0..=0x0F ---- + #[test] + fn matrix_bitmask_accepts_bounds() { + assert!(validate_raw_matrix_bitmask(0x00).is_ok()); + assert!(validate_raw_matrix_bitmask(0x0F).is_ok()); + } + + #[test] + fn matrix_bitmask_rejects_above_max() { + assert!(validate_raw_matrix_bitmask(0x10).is_err()); + } + + // ---- validate_raw_link_channel ---- + #[test] + fn link_channel_input_a_accepts_full_allowed_mask() { + // Channel 0x00 (A) allows linking to A|B|C|D = 0x0F per input_link_mask. + assert!(validate_raw_link_channel(0x00, 0x0F).is_ok()); + } + + #[test] + fn link_channel_input_a_rejects_bit_outside_allowed_mask() { + assert!(validate_raw_link_channel(0x00, 0x10).is_err()); + } + + #[test] + fn link_channel_input_d_only_allows_its_own_bit() { + // Channel 0x03 (D) only allows mask 0x08 per input_link_mask. + assert!(validate_raw_link_channel(0x03, 0x08).is_ok()); + assert!(validate_raw_link_channel(0x03, 0x04).is_err()); + } + + #[test] + fn link_channel_output_first_accepts_full_mask() { + // Channel 0x04 allows mask 0xFF per output_link_mask. + assert!(validate_raw_link_channel(0x04, 0xFF).is_ok()); + } + + #[test] + fn link_channel_output_last_only_allows_its_own_bit() { + // Channel 0x0B allows only mask 0x80 per output_link_mask. + assert!(validate_raw_link_channel(0x0B, 0x80).is_ok()); + assert!(validate_raw_link_channel(0x0B, 0x01).is_err()); + } + + #[test] + fn link_channel_rejects_out_of_range_source_channel() { + assert!(validate_raw_link_channel(0x0C, 0x01).is_err()); + } + + // ---- validate_raw_channel_name: 8 ascii bytes, null-padded ---- + #[test] + fn channel_name_accepts_correctly_padded_ascii() { + let mut name = b"Kick".to_vec(); + name.resize(CHANNEL_NAME_LENGTH, CHANNEL_NAME_PADDING); + assert!(validate_raw_channel_name(&name).is_ok()); + } + + #[test] + fn channel_name_rejects_wrong_length() { + assert!(validate_raw_channel_name(b"TooLongName").is_err()); + } + + #[test] + fn channel_name_rejects_non_ascii_byte() { + let mut name = b"Kick".to_vec(); + name.push(0x01); + name.resize(CHANNEL_NAME_LENGTH, CHANNEL_NAME_PADDING); + assert!(validate_raw_channel_name(&name).is_err()); + } + + // ---- validate_raw_gate_threshold: 0..=0x00B4 ---- + #[test] + fn gate_threshold_accepts_bounds() { + assert!(validate_raw_gate_threshold(0x0000).is_ok()); + assert!(validate_raw_gate_threshold(0x00B4).is_ok()); + } + + #[test] + fn gate_threshold_rejects_above_max() { + assert!(validate_raw_gate_threshold(0x00B5).is_err()); + } + + // ---- validate_raw_hold: 0..=0x03E6 ---- + #[test] + fn hold_accepts_bounds() { + assert!(validate_raw_hold(0x0000).is_ok()); + assert!(validate_raw_hold(0x03E6).is_ok()); + } + + #[test] + fn hold_rejects_above_max() { + assert!(validate_raw_hold(0x03E7).is_err()); + } + + // ---- validate_raw_matrix_gain: 0..=0x0118 ---- + #[test] + fn matrix_gain_accepts_bounds() { + assert!(validate_raw_matrix_gain(0x0000).is_ok()); + assert!(validate_raw_matrix_gain(0x0118).is_ok()); + } + + #[test] + fn matrix_gain_rejects_above_max() { + assert!(validate_raw_matrix_gain(0x0119).is_err()); + } +}