use std::assert_matches;
use super::event::{ExtendedDpiEvent, decode_event};
use super::types::{
DpiCalibrationCorrection, DpiDirection, DpiRange, Lod, SensorCapabilities, SetDpiParameters,
parse_dpi_list, parse_dpi_ranges, parse_lod_list, terminated_word_len,
};
use crate::protocol::v20::{ErrorType, Hidpp20Error};
#[test]
fn set_parameters_carry_y_and_lod_only_for_advertised_capabilities() {
let cases = [
(SensorCapabilities::empty(), 0, Lod::NotSupported),
(SensorCapabilities::DPI_Y, 1600, Lod::NotSupported),
(SensorCapabilities::LOD, 0, Lod::High),
(
SensorCapabilities::DPI_Y | SensorCapabilities::LOD,
1600,
Lod::High,
),
(
SensorCapabilities::CALIBRATION | SensorCapabilities::PROFILE,
0,
Lod::NotSupported,
),
];
for (capabilities, dpi_y, lod) in cases {
assert_eq!(
SetDpiParameters::for_sensor(capabilities, 1200, 1600, Lod::High),
SetDpiParameters {
dpi_x: 1200,
dpi_y,
lod,
},
"{capabilities:?}"
);
}
}
#[test]
fn parses_fixed_dpi_ranges_pws_example() {
let stream = [0x01, 0x90, 0x03, 0x20, 0x04, 0xb0, 0x00, 0x00];
assert_eq!(
parse_dpi_ranges(&stream).unwrap(),
[
DpiRange::Fixed(400),
DpiRange::Fixed(800),
DpiRange::Fixed(1200),
]
);
}
#[test]
fn parses_stepped_dpi_ranges_gaming_example() {
let stream = [
0x00, 0x64, 0xe0, 0x01, 0x03, 0xe8, 0xe0, 0x64, 0x7d, 0x00, 0x00, 0x00,
];
assert_eq!(
parse_dpi_ranges(&stream).unwrap(),
[
DpiRange::Stepped {
from: 100,
to: 1000,
step: 1
},
DpiRange::Stepped {
from: 1000,
to: 32000,
step: 100
},
]
);
}
#[test]
fn parses_dpi_ranges_split_across_pages() {
let page0 = [
0x00, 0x64, 0xe0, 0x01, 0x00, 0xc8, 0xe0, 0x02, 0x01, 0xf4, 0xe0, 0x05, 0x03,
];
let page1 = [
0xe8, 0xe0, 0x0a, 0x07, 0xd0, 0xe0, 0x14, 0x13, 0x88, 0x00, 0x00, 0x00, 0x00,
];
assert!(terminated_word_len(&page0).is_none());
let mut stream = page0.to_vec();
stream.extend_from_slice(&page1);
assert!(terminated_word_len(&stream).is_some());
assert_eq!(
parse_dpi_ranges(&stream).unwrap(),
[
DpiRange::Stepped {
from: 100,
to: 200,
step: 1
},
DpiRange::Stepped {
from: 200,
to: 500,
step: 2
},
DpiRange::Stepped {
from: 500,
to: 1000,
step: 5
},
DpiRange::Stepped {
from: 1000,
to: 2000,
step: 10
},
DpiRange::Stepped {
from: 2000,
to: 5000,
step: 20
},
]
);
}
#[test]
fn parses_range_followed_by_fixed_value() {
let stream = [0x00, 0x64, 0xe0, 0x01, 0x00, 0xc8, 0x01, 0x90, 0x00, 0x00];
assert_eq!(
parse_dpi_ranges(&stream).unwrap(),
[
DpiRange::Stepped {
from: 100,
to: 200,
step: 1
},
DpiRange::Fixed(400),
]
);
}
#[test]
fn rejects_hyphen_without_preceding_value() {
let stream = [0xe0, 0x01, 0x01, 0x90, 0x00, 0x00];
assert_matches!(
parse_dpi_ranges(&stream),
Err(Hidpp20Error::UnsupportedResponse)
);
}
#[test]
fn rejects_hyphen_without_following_value() {
let stream = [0x01, 0x90, 0xe0, 0x01, 0x00, 0x00];
assert_matches!(
parse_dpi_ranges(&stream),
Err(Hidpp20Error::UnsupportedResponse)
);
}
#[test]
fn rejects_zero_step_unused_marker() {
let stream = [0x01, 0x90, 0xe0, 0x00, 0x04, 0xb0, 0x00, 0x00];
assert_matches!(
parse_dpi_ranges(&stream),
Err(Hidpp20Error::UnsupportedResponse)
);
}
#[test]
fn rejects_descending_stepped_range() {
let stream = [0x06, 0x40, 0xe0, 0x32, 0x01, 0x90, 0x00, 0x00];
assert_matches!(
parse_dpi_ranges(&stream),
Err(Hidpp20Error::UnsupportedResponse)
);
}
#[test]
fn rejects_unterminated_dpi_ranges() {
let stream = [0x01, 0x90, 0x03, 0x20];
assert_matches!(
parse_dpi_ranges(&stream),
Err(Hidpp20Error::UnsupportedResponse)
);
}
#[test]
fn parses_dpi_list_with_terminator() {
let bytes = [0x01, 0x90, 0x03, 0x20, 0x06, 0x40, 0x00, 0x00];
assert_eq!(parse_dpi_list(&bytes), [400, 800, 1600]);
}
#[test]
fn parses_dpi_list_filling_payload() {
let bytes = [0x01, 0x90, 0x03, 0x20];
assert_eq!(parse_dpi_list(&bytes), [400, 800]);
}
#[test]
fn parses_lod_list() {
let bytes = [1, 2, 3, 0, 0, 0];
assert_eq!(
parse_lod_list(&bytes, 3).unwrap(),
[Lod::Low, Lod::Medium, Lod::High]
);
}
#[test]
fn rejects_unknown_lod_value() {
let bytes = [9];
assert_matches!(
parse_lod_list(&bytes, 1),
Err(Hidpp20Error::UnsupportedResponse)
);
}
#[test]
fn rejects_lod_list_longer_than_payload() {
let bytes = [1, 2];
assert_matches!(
parse_lod_list(&bytes, 3),
Err(Hidpp20Error::UnsupportedResponse)
);
}
#[test]
fn decodes_parameters_changed_event() {
let mut payload = [0; 16];
payload[0] = 1;
payload[1..3].copy_from_slice(&800u16.to_be_bytes());
payload[3..5].copy_from_slice(&1600u16.to_be_bytes());
payload[5] = 2;
let ExtendedDpiEvent::ParametersChanged(event) = decode_event(0, &payload).unwrap() else {
panic!("expected a parameters-changed event");
};
assert_eq!(event.sensor_index, 1);
assert_eq!(event.dpi_x, 800);
assert_eq!(event.dpi_y, 1600);
assert_eq!(event.lod, Some(Lod::Medium));
}
#[test]
fn decodes_calibration_completed_event() {
let mut payload = [0; 16];
payload[1] = 1;
payload[2..4].copy_from_slice(&100i16.to_be_bytes());
payload[4..6].copy_from_slice(&(-1i16).to_be_bytes());
let ExtendedDpiEvent::CalibrationCompleted(event) = decode_event(1, &payload).unwrap() else {
panic!("expected a calibration-completed event");
};
assert_eq!(event.direction, Some(DpiDirection::Y));
assert_eq!(event.correction, 100);
assert_eq!(event.delta, -1);
assert!(!event.failed());
}
#[test]
fn flags_failed_calibration_event() {
let mut payload = [0; 16];
payload[2..4].copy_from_slice(&i16::MIN.to_be_bytes());
let ExtendedDpiEvent::CalibrationCompleted(event) = decode_event(1, &payload).unwrap() else {
panic!("expected a calibration-completed event");
};
assert!(event.failed());
}
#[test]
fn ignores_unknown_event_sub_id() {
assert!(decode_event(7, &[0; 16]).is_none());
}
#[test]
fn keeps_event_with_unknown_lod() {
let mut payload = [0; 16];
payload[0] = 3; payload[5] = 9;
let ExtendedDpiEvent::ParametersChanged(changed) =
decode_event(0, &payload).expect("event kept")
else {
panic!("expected ParametersChanged");
};
assert_eq!(changed.lod, None);
assert_eq!(changed.sensor_index, 3);
}
#[test]
fn known_sub_id_survives_any_field_byte() {
for byte in 0..=u8::MAX {
for (sub_id, pos) in [(0u8, 5usize), (1, 1)] {
let mut payload = [0; 16];
payload[0] = 3; payload[pos] = byte;
assert!(
decode_event(sub_id, &payload).is_some(),
"dropped sub {sub_id} for payload[{pos}]={byte:#04x}"
);
}
}
}
#[test]
fn encodes_calibration_correction_sentinels() {
assert_eq!(
DpiCalibrationCorrection::Adjust(100).to_wire().unwrap(),
100
);
assert_eq!(
DpiCalibrationCorrection::Adjust(-512).to_wire().unwrap(),
-512
);
assert_eq!(DpiCalibrationCorrection::RevertToOob.to_wire().unwrap(), 0);
assert_eq!(
DpiCalibrationCorrection::RevertToProfile.to_wire().unwrap(),
i16::MIN
);
}
#[test]
fn rejects_out_of_range_calibration_corrections() {
for correction in [
DpiCalibrationCorrection::Adjust(-1024),
DpiCalibrationCorrection::Adjust(1024),
DpiCalibrationCorrection::Adjust(i16::MIN),
] {
assert_matches!(
correction.to_wire(),
Err(Hidpp20Error::Feature(ErrorType::InvalidArgument))
);
}
}
const SENSOR: u8 = 1;
fn answer<T, F>(
function: u8,
call: impl FnOnce(super::ExtendedDpiFeature) -> F,
reply: impl FnOnce(crate::protocol::v20::MessageHeader) -> crate::protocol::v20::Message,
) -> Result<T, Hidpp20Error>
where
F: Future<Output = Result<T, Hidpp20Error>>,
{
use std::sync::Arc;
use crate::channel::mock::{MockRawHidChannel, channel_with_reader};
use crate::feature::CreatableFeature;
use crate::nibble::U4;
use crate::protocol::v20::MessageHeader;
futures::executor::block_on(async {
let (raw, handle) = MockRawHidChannel::new();
let channel = Arc::new(channel_with_reader(raw).await);
let mut pending = Box::pin(call(super::ExtendedDpiFeature::new(channel, 3, 8)));
assert!(futures::poll!(pending.as_mut()).is_pending());
let header = MessageHeader {
device_index: 3,
feature_index: 8,
function_id: U4::from_lo(function),
software_id: U4::from_lo(1),
};
handle.send_incoming(reply(header).into()).await;
pending.await
})
}
const PER_SENSOR_FUNCTIONS: [u8; 9] = [1, 3, 4, 5, 6, 7, 8, 9, 10];
fn per_sensor_call(
function: u8,
reply: impl FnOnce(crate::protocol::v20::MessageHeader) -> crate::protocol::v20::Message,
) -> Result<(), Hidpp20Error> {
use super::types::{CalibrationType, LedHoldType, ShowDpiStatus, StartDpiCalibration};
answer(
function,
|dpi| async move {
match function {
1 => dpi.get_sensor_capabilities(SENSOR).await.map(|_| ()),
3 => dpi
.get_sensor_dpi_list(SENSOR, DpiDirection::X)
.await
.map(|_| ()),
4 => dpi.get_sensor_lod_list(SENSOR, 1).await.map(|_| ()),
5 => dpi.get_sensor_dpi_parameters(SENSOR).await.map(|_| ()),
6 => {
let params = SetDpiParameters {
dpi_x: 800,
dpi_y: 0,
lod: Lod::NotSupported,
};
dpi.set_sensor_dpi_parameters(SENSOR, params).await
}
7 => {
let params = ShowDpiStatus {
dpi_level: 1,
led_hold_type: LedHoldType::TimerBased,
button_num: 0,
};
dpi.show_sensor_dpi_status(SENSOR, params).await
}
8 => dpi.get_dpi_calibration_info(SENSOR).await.map(|_| ()),
9 => {
let params = StartDpiCalibration {
direction: DpiDirection::X,
expected_count: 100,
calib_type: CalibrationType::Hardware,
start_timeout: 1,
hw_process_timeout: 1,
sw_process_timeout: 1,
};
dpi.start_dpi_calibration(SENSOR, params).await
}
10 => {
let correction = DpiCalibrationCorrection::Adjust(5);
dpi.set_dpi_calibration(SENSOR, DpiDirection::X, correction)
.await
}
_ => unreachable!("function {function} takes no sensor"),
}
},
reply,
)
}
fn valid_reply(function: u8, sensor: u8) -> [u8; 16] {
let mut payload = [0; 16];
payload[0] = sensor;
match function {
3 => payload[2..4].copy_from_slice(&800u16.to_be_bytes()),
4 => payload[1] = Lod::Low.into(),
5 => {
payload[1..3].copy_from_slice(&800u16.to_be_bytes());
payload[3..5].copy_from_slice(&1000u16.to_be_bytes());
}
_ => {}
}
payload
}
#[test]
fn per_sensor_calls_take_a_reply_for_their_sensor() {
use crate::protocol::v20::Message;
for function in PER_SENSOR_FUNCTIONS {
let reply = valid_reply(function, SENSOR);
let result = per_sensor_call(function, |header| Message::Long(header, reply));
assert!(result.is_ok(), "function {function}: {result:?}");
}
}
#[test]
fn per_sensor_calls_refuse_a_reply_for_another_sensor() {
use crate::protocol::v20::Message;
for function in PER_SENSOR_FUNCTIONS {
let reply = valid_reply(function, SENSOR + 1);
let result = per_sensor_call(function, |header| Message::Long(header, reply));
assert_matches!(
result,
Err(Hidpp20Error::UnsupportedResponse),
"function {function}"
);
}
}
#[test]
fn dpi_list_refuses_a_reply_for_another_direction() {
use crate::protocol::v20::Message;
let mut reply = valid_reply(3, SENSOR);
reply[1] = DpiDirection::Y.into();
let result = per_sensor_call(3, |header| Message::Long(header, reply));
assert_matches!(result, Err(Hidpp20Error::UnsupportedResponse));
}
#[test]
fn parameter_and_calibration_reads_refuse_a_short_reply() {
use crate::protocol::v20::Message;
for function in [5, 8] {
let result = per_sensor_call(function, |header| {
Message::Short(header, [SENSOR, 0x03, 0x20])
});
assert_matches!(
result,
Err(Hidpp20Error::UnsupportedResponse),
"function {function}"
);
}
}
#[test]
fn parameters_decode_from_a_reply_for_their_sensor() {
use crate::protocol::v20::Message;
let reply = valid_reply(5, SENSOR);
let params = answer(
5,
|dpi| async move { dpi.get_sensor_dpi_parameters(SENSOR).await },
|header| Message::Long(header, reply),
)
.expect("a long reply for the sensor decodes");
assert_eq!(
(
params.sensor_index,
params.dpi_x,
params.default_dpi_x,
params.lod
),
(SENSOR, 800, 1000, Lod::NotSupported)
);
}
#[test]
fn parameters_broadcast_is_exactly_the_parameters_changed_event() {
use super::ExtendedDpiFeature;
for function in 0..=0x0f {
let changed = matches!(
decode_event(function, &[0; 16]),
Some(ExtendedDpiEvent::ParametersChanged(_))
);
assert_eq!(
ExtendedDpiFeature::is_parameters_broadcast(function),
changed,
"function {function}"
);
}
}