use motorcortex_rust::{Error, ErrorLevel, ParameterTreeHashMsg, get_hash, get_hash_size};
use prost::Message;
use std::time::{SystemTime, UNIX_EPOCH};
#[test]
fn test_hash_values() {
let error_hash = get_hash::<Error>();
let parameter_tree_hash = get_hash::<ParameterTreeHashMsg>();
let hash_size = get_hash_size();
assert_eq!(hash_size, 4);
assert_eq!(error_hash, 0xcb54b842);
assert_eq!(parameter_tree_hash, 0xc8cef26b);
}
#[test]
fn test_proto_encode() {
let error_msg = Error {
timestamp: SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs(),
error_number: 1,
error_level: ErrorLevel::EmergencyStop as u32,
subsystem: 2,
info: 10,
};
let mut buf = Vec::new();
Message::encode(&error_msg, &mut buf).unwrap();
let decoded_msg = Error::decode(&buf[..]).unwrap();
assert_eq!(error_msg, decoded_msg);
}
mod version {
#[test]
fn version_is_non_empty() {
assert!(!env!("CARGO_PKG_VERSION").is_empty());
}
#[test]
fn version_is_semver_like() {
let version = env!("CARGO_PKG_VERSION");
let major = version.split('.').next().expect("CARGO_PKG_VERSION is empty");
assert!(
major.chars().all(|c| c.is_ascii_digit()),
"expected numeric major component, got {version:?}"
);
}
}
mod parameter_tree {
use motorcortex_rust::{
ParameterInfo, ParameterTree, ParameterTreeMsg, ParameterType, StatusCode,
};
fn info(path: &str, data_type: u32, param_type: ParameterType) -> ParameterInfo {
ParameterInfo {
id: 1,
data_type,
data_size: 0,
number_of_elements: 1,
flags: 0,
permissions: 0,
param_type: param_type as i32,
group_id: 0,
unit: 0,
path: path.to_string(),
}
}
fn msg(status: StatusCode, params: Vec<ParameterInfo>) -> ParameterTreeMsg {
ParameterTreeMsg {
header: None,
params,
hash: 0,
status: status as i32,
}
}
#[test]
fn empty_after_new() {
let tree = ParameterTree::new();
assert!(tree.get_parameter_info("anything").is_none());
assert!(tree.get_parameter_data_type("anything").is_none());
assert!(!tree.has_parameter("anything"));
}
#[test]
fn from_message_populates_lookup() {
let tree = ParameterTree::from_message(msg(
StatusCode::Ok,
vec![
info("root/x", 7, ParameterType::Parameter),
info("root/y", 11, ParameterType::Parameter),
],
))
.expect("OK status should yield Some(tree)");
assert!(tree.has_parameter("root/x"));
assert_eq!(tree.get_parameter_data_type("root/x"), Some(7));
assert_eq!(tree.get_parameter_data_type("root/y"), Some(11));
assert_eq!(tree.get_parameter_info("root/x").unwrap().data_type, 7);
}
#[test]
fn from_message_returns_none_on_non_ok_status() {
let tree = ParameterTree::from_message(msg(
StatusCode::Failed,
vec![info("root/x", 7, ParameterType::Parameter)],
));
assert!(tree.is_none());
}
#[test]
fn unknown_path_returns_none() {
let tree = ParameterTree::from_message(msg(
StatusCode::Ok,
vec![info("root/x", 7, ParameterType::Parameter)],
))
.unwrap();
assert!(tree.get_parameter_info("root/missing").is_none());
assert!(tree.get_parameter_data_type("root/missing").is_none());
assert!(!tree.has_parameter("root/missing"));
}
#[test]
fn parameters_iterator_filters_non_parameter_types() {
let tree = ParameterTree::from_message(msg(
StatusCode::Ok,
vec![
info("root/leaf", 7, ParameterType::Parameter),
info("root/in", 7, ParameterType::Input),
info("root/out", 7, ParameterType::Output),
],
))
.unwrap();
let paths: Vec<&str> = tree.parameters().map(|(p, _)| p).collect();
assert_eq!(paths, vec!["root/leaf"]);
}
}
mod primitive_types {
use motorcortex_rust::{GetParameterValue, SetParameterValue};
#[test]
fn i8_round_trip() {
for v in [i8::MIN, -1i8, 0, 1, i8::MAX] {
let bytes = v.to_bytes_as_i8();
assert_eq!(bytes, v.to_le_bytes().to_vec());
assert_eq!(i8::from_bytes_as_i8(&bytes).unwrap(), v);
}
}
#[test]
fn u8_round_trip() {
for v in [0u8, 1, 127, u8::MAX] {
let bytes = v.to_bytes_as_u8();
assert_eq!(bytes, v.to_le_bytes().to_vec());
assert_eq!(u8::from_bytes_as_u8(&bytes).unwrap(), v);
}
}
#[test]
fn i16_round_trip() {
for v in [i16::MIN, -1i16, 0, 1, i16::MAX] {
let bytes = v.to_bytes_as_i16();
assert_eq!(bytes, v.to_le_bytes().to_vec());
assert_eq!(i16::from_bytes_as_i16(&bytes).unwrap(), v);
}
}
#[test]
fn i32_round_trip() {
for v in [i32::MIN, -1i32, 0, 1, i32::MAX] {
let bytes = v.to_bytes_as_i32();
assert_eq!(bytes, v.to_le_bytes().to_vec());
assert_eq!(i32::from_bytes_as_i32(&bytes).unwrap(), v);
}
}
#[test]
fn u32_round_trip() {
for v in [0u32, 1, u32::MAX] {
let bytes = v.to_bytes_as_u32();
assert_eq!(bytes, v.to_le_bytes().to_vec());
assert_eq!(u32::from_bytes_as_u32(&bytes).unwrap(), v);
}
}
#[test]
fn i64_round_trip() {
for v in [i64::MIN, -1i64, 0, 1, i64::MAX] {
let bytes = v.to_bytes_as_i64();
assert_eq!(bytes, v.to_le_bytes().to_vec());
assert_eq!(i64::from_bytes_as_i64(&bytes).unwrap(), v);
}
}
#[test]
fn u64_round_trip() {
for v in [0u64, 1, u64::MAX] {
let bytes = v.to_bytes_as_u64();
assert_eq!(bytes, v.to_le_bytes().to_vec());
assert_eq!(u64::from_bytes_as_u64(&bytes).unwrap(), v);
}
}
#[test]
fn f32_round_trip() {
for v in [-1.5f32, 0.0, 0.125, 42.5] {
let bytes = v.to_bytes_as_f32();
assert_eq!(bytes.len(), 4);
assert_eq!(f32::from_bytes_as_f32(&bytes).unwrap(), v);
}
}
#[test]
fn f64_round_trip() {
for v in [-1.5f64, 0.0, 0.125, std::f64::consts::PI] {
let bytes = v.to_bytes_as_f64();
assert_eq!(bytes.len(), 8);
assert_eq!(f64::from_bytes_as_f64(&bytes).unwrap(), v);
}
}
#[test]
fn bool_round_trip() {
for v in [true, false] {
let bytes = v.to_bytes_as_bool();
assert_eq!(bytes.len(), 1);
assert_eq!(bool::from_bytes_as_bool(&bytes).unwrap(), v);
}
}
#[test]
fn string_encodes_as_utf8_bytes() {
let s = String::from("Hallo robot!");
let bytes = s.to_bytes_as_string();
assert_eq!(bytes, s.as_bytes());
assert_eq!(String::from_bytes_as_string(&bytes).unwrap(), s);
}
#[test]
fn string_round_trips_non_ascii() {
let s = String::from("π ≈ 3.14, αβγ");
let bytes = s.to_bytes_as_string();
assert_eq!(String::from_bytes_as_string(&bytes).unwrap(), s);
}
#[test]
fn fixed_array_encodes_elementwise_i32() {
let arr: [i32; 3] = [1, 2, 3];
let bytes = arr.to_bytes_as_i32();
let expected: Vec<u8> = arr.iter().flat_map(|v| v.to_le_bytes()).collect();
assert_eq!(bytes, expected);
}
#[test]
fn vec_encodes_elementwise_f64() {
let vec: Vec<f64> = vec![1.5, 2.5, 3.5];
let bytes = vec.to_bytes_as_f64();
let expected: Vec<u8> = vec.iter().flat_map(|v| v.to_le_bytes()).collect();
assert_eq!(bytes, expected);
}
}
mod message_types {
use motorcortex_rust::{LoginMsg, ParameterTreeMsg, StatusCode, StatusMsg};
use prost::Message;
#[test]
fn login_msg_round_trip() {
let msg = LoginMsg {
header: None,
login: "operator".into(),
password: "operat".into(),
};
let mut buf = Vec::new();
msg.encode(&mut buf).unwrap();
let decoded = LoginMsg::decode(&buf[..]).unwrap();
assert_eq!(msg, decoded);
}
#[test]
fn status_msg_round_trip() {
let msg = StatusMsg {
header: None,
status: StatusCode::Ok as i32,
};
let mut buf = Vec::new();
msg.encode(&mut buf).unwrap();
let decoded = StatusMsg::decode(&buf[..]).unwrap();
assert_eq!(msg, decoded);
assert_eq!(decoded.status, StatusCode::Ok as i32);
}
#[test]
fn parameter_tree_msg_round_trip() {
let msg = ParameterTreeMsg {
header: None,
params: vec![],
hash: 0x1245,
status: StatusCode::Ok as i32,
};
let mut buf = Vec::new();
msg.encode(&mut buf).unwrap();
let decoded = ParameterTreeMsg::decode(&buf[..]).unwrap();
assert_eq!(decoded.hash, 0x1245);
assert_eq!(decoded.status, StatusCode::Ok as i32);
}
}
mod status_code {
use motorcortex_rust::StatusCode;
const ALL: &[StatusCode] = &[
StatusCode::Ok,
StatusCode::ReadOnlyMode,
StatusCode::Failed,
StatusCode::FailedToDecode,
StatusCode::SubListIsFull,
StatusCode::WrongParameterPath,
StatusCode::FailedToSetRequestedFrq,
StatusCode::FailedToOpenFile,
StatusCode::GroupListIsFull,
StatusCode::WrongPassword,
StatusCode::UserNotLoggedIn,
StatusCode::PermissionDenied,
];
#[test]
fn ok_is_zero() {
assert_eq!(StatusCode::Ok as i32, 0);
}
#[test]
fn ok_str_name_is_stable() {
assert_eq!(StatusCode::Ok.as_str_name(), "OK");
}
#[test]
fn str_name_round_trip() {
for &code in ALL {
let name = code.as_str_name();
assert!(!name.is_empty(), "{code:?} has empty str_name");
assert_eq!(
StatusCode::from_str_name(name),
Some(code),
"{code:?} → {name:?} did not round-trip"
);
}
}
#[test]
fn unknown_str_name_returns_none() {
assert_eq!(StatusCode::from_str_name("NOT_A_CODE"), None);
}
}
mod init_threads {
use motorcortex_rust::{init_threads, init_threads_with_defaults};
#[test]
fn defaults_do_not_panic() {
init_threads_with_defaults();
}
#[test]
fn custom_counts_do_not_panic() {
init_threads(4, 2, 2, 2);
}
#[test]
fn idempotent() {
init_threads_with_defaults();
init_threads_with_defaults();
}
}
mod logger {
use motorcortex_rust::{LogLevel, init_debug_logger, init_logger};
#[test]
fn log_level_equality() {
assert_eq!(LogLevel::Debug, LogLevel::Debug);
assert_ne!(LogLevel::Debug, LogLevel::Info);
}
#[test]
fn init_logger_all_levels_do_not_panic() {
for level in [
LogLevel::None,
LogLevel::Debug,
LogLevel::Info,
LogLevel::Warn,
LogLevel::Error,
] {
init_logger(level);
}
}
#[test]
fn init_debug_logger_does_not_panic() {
init_debug_logger();
}
}
mod error_display {
use motorcortex_rust::{Error, MotorcortexError, StatusCode};
use prost::Message;
#[test]
fn display_covers_every_variant() {
let cases = [
MotorcortexError::Connection("c".into()),
MotorcortexError::Encode("e".into()),
MotorcortexError::Decode("d".into()),
MotorcortexError::ParameterNotFound("root/x".into()),
MotorcortexError::Status(StatusCode::Failed),
MotorcortexError::Io("i".into()),
MotorcortexError::Subscription("s".into()),
];
for err in &cases {
let displayed = format!("{err}");
let debugged = format!("{err:?}");
assert!(!displayed.is_empty(), "empty Display for {err:?}");
assert!(!debugged.is_empty(), "empty Debug for {err:?}");
}
}
#[test]
fn from_prost_decode_error() {
let err = Error::decode(&[0xFFu8][..]).expect_err("0xFF must fail to decode");
let converted: MotorcortexError = err.into();
assert!(matches!(converted, MotorcortexError::Decode(_)));
}
}
mod set_parameter_value_coverage {
use motorcortex_rust::SetParameterValue;
macro_rules! exercise {
($v:expr) => {{
let v = $v;
let _ = v.to_bytes_as_bool();
let _ = v.to_bytes_as_i8();
let _ = v.to_bytes_as_u8();
let _ = v.to_bytes_as_i16();
let _ = v.to_bytes_as_u16();
let _ = v.to_bytes_as_i32();
let _ = v.to_bytes_as_u32();
let _ = v.to_bytes_as_i64();
let _ = v.to_bytes_as_u64();
let _ = v.to_bytes_as_f32();
let _ = v.to_bytes_as_f64();
let _ = v.to_bytes_as_string();
}};
}
#[test]
fn scalars() {
exercise!(-1i8);
exercise!(200u8);
exercise!(-300i16);
exercise!(300u16);
exercise!(-1i32);
exercise!(1u32);
exercise!(-1i64);
exercise!(1u64);
exercise!(1.5f32);
exercise!(1.5f64);
exercise!(true);
exercise!(false);
exercise!("hello"); exercise!(String::from("hello"));
}
#[test]
fn arrays() {
exercise!([-1i8, 0]);
exercise!([200u8, 0]);
exercise!([-1i16, 0]);
exercise!([1u16, 0]);
exercise!([-1i32, 0]);
exercise!([1u32, 0]);
exercise!([-1i64, 0]);
exercise!([1u64, 0]);
exercise!([1.5f32, 0.0]);
exercise!([1.5f64, 0.0]);
exercise!([true, false]);
}
#[test]
fn vecs() {
exercise!(vec![-1i8, 0]);
exercise!(vec![200u8, 0]);
exercise!(vec![-1i16, 0]);
exercise!(vec![1u16, 0]);
exercise!(vec![-1i32, 0]);
exercise!(vec![1u32, 0]);
exercise!(vec![-1i64, 0]);
exercise!(vec![1u64, 0]);
exercise!(vec![1.5f32, 0.0]);
exercise!(vec![1.5f64, 0.0]);
exercise!(vec![true, false]);
}
}
mod get_parameter_value_coverage {
use motorcortex_rust::GetParameterValue;
const B1: &[u8] = &[1];
const B2: &[u8] = &[0, 0];
const B4: &[u8] = &[0, 0, 0, 0];
const B8: &[u8] = &[0, 0, 0, 0, 0, 0, 0, 0];
const B2X2: &[u8] = &[0, 0];
const B2X2_16: &[u8] = &[0, 0, 0, 0];
const B2X2_32: &[u8] = &[0, 0, 0, 0, 0, 0, 0, 0];
const B2X2_64: &[u8] = &[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
const STR: &[u8] = b"hi";
macro_rules! exercise_scalar {
($t:ty) => {{
let _ = <$t>::from_bytes_as_bool(B1);
let _ = <$t>::from_bytes_as_i8(B1);
let _ = <$t>::from_bytes_as_u8(B1);
let _ = <$t>::from_bytes_as_i16(B2);
let _ = <$t>::from_bytes_as_u16(B2);
let _ = <$t>::from_bytes_as_i32(B4);
let _ = <$t>::from_bytes_as_u32(B4);
let _ = <$t>::from_bytes_as_i64(B8);
let _ = <$t>::from_bytes_as_u64(B8);
let _ = <$t>::from_bytes_as_f32(B4);
let _ = <$t>::from_bytes_as_f64(B8);
let _ = <$t>::from_bytes_as_string(STR);
}};
}
macro_rules! exercise_array {
($t:ty) => {{
let _ = <[$t; 2]>::from_bytes_as_bool(B2X2);
let _ = <[$t; 2]>::from_bytes_as_i8(B2X2);
let _ = <[$t; 2]>::from_bytes_as_u8(B2X2);
let _ = <[$t; 2]>::from_bytes_as_i16(B2X2_16);
let _ = <[$t; 2]>::from_bytes_as_u16(B2X2_16);
let _ = <[$t; 2]>::from_bytes_as_i32(B2X2_32);
let _ = <[$t; 2]>::from_bytes_as_u32(B2X2_32);
let _ = <[$t; 2]>::from_bytes_as_i64(B2X2_64);
let _ = <[$t; 2]>::from_bytes_as_u64(B2X2_64);
let _ = <[$t; 2]>::from_bytes_as_f32(B2X2_32);
let _ = <[$t; 2]>::from_bytes_as_f64(B2X2_64);
let _ = <[$t; 2]>::from_bytes_as_string(STR);
}};
}
macro_rules! exercise_vec {
($t:ty) => {{
let _ = <Vec<$t>>::from_bytes_as_bool(B2X2);
let _ = <Vec<$t>>::from_bytes_as_i8(B2X2);
let _ = <Vec<$t>>::from_bytes_as_u8(B2X2);
let _ = <Vec<$t>>::from_bytes_as_i16(B2X2_16);
let _ = <Vec<$t>>::from_bytes_as_u16(B2X2_16);
let _ = <Vec<$t>>::from_bytes_as_i32(B2X2_32);
let _ = <Vec<$t>>::from_bytes_as_u32(B2X2_32);
let _ = <Vec<$t>>::from_bytes_as_i64(B2X2_64);
let _ = <Vec<$t>>::from_bytes_as_u64(B2X2_64);
let _ = <Vec<$t>>::from_bytes_as_f32(B2X2_32);
let _ = <Vec<$t>>::from_bytes_as_f64(B2X2_64);
let _ = <Vec<$t>>::from_bytes_as_string(STR);
}};
}
#[test]
fn scalars() {
exercise_scalar!(i8);
exercise_scalar!(u8);
exercise_scalar!(i16);
exercise_scalar!(u16);
exercise_scalar!(i32);
exercise_scalar!(u32);
exercise_scalar!(i64);
exercise_scalar!(u64);
exercise_scalar!(f32);
exercise_scalar!(f64);
exercise_scalar!(bool);
exercise_scalar!(String);
}
#[test]
fn arrays() {
exercise_array!(i8);
exercise_array!(u8);
exercise_array!(i16);
exercise_array!(u16);
exercise_array!(i32);
exercise_array!(u32);
exercise_array!(i64);
exercise_array!(u64);
exercise_array!(f32);
exercise_array!(f64);
exercise_array!(bool);
}
#[test]
fn vecs() {
exercise_vec!(i8);
exercise_vec!(u8);
exercise_vec!(i16);
exercise_vec!(u16);
exercise_vec!(i32);
exercise_vec!(u32);
exercise_vec!(i64);
exercise_vec!(u64);
exercise_vec!(f32);
exercise_vec!(f64);
exercise_vec!(bool);
}
}
mod parameters_coverage {
use motorcortex_rust::Parameters;
#[test]
fn str_becomes_single_element_vec() {
assert_eq!("root/x".into_vec(), vec!["root/x".to_string()]);
}
#[test]
fn vec_of_strings_passes_through() {
let input = vec!["a".to_string(), "b".to_string()];
assert_eq!(input.clone().into_vec(), input);
}
#[test]
fn slice_of_strs_maps_to_string_vec() {
let slice: &[&str] = &["a", "b"];
assert_eq!(slice.into_vec(), vec!["a".to_string(), "b".to_string()]);
}
#[test]
fn array_of_strs_maps_to_string_vec() {
let arr: [&str; 3] = ["a", "b", "c"];
assert_eq!(
arr.into_vec(),
vec!["a".to_string(), "b".to_string(), "c".to_string()]
);
}
}
mod timespec {
use motorcortex_rust::TimeSpec;
#[test]
fn field_access() {
let ts = TimeSpec {
sec: 3,
nsec: 250_000_000,
};
assert_eq!(ts.sec, 3);
assert_eq!(ts.nsec, 250_000_000);
}
#[test]
fn from_buffer_reads_little_endian_layout() {
let mut buffer = Vec::with_capacity(16);
buffer.extend_from_slice(&7i64.to_le_bytes());
buffer.extend_from_slice(&42i64.to_le_bytes());
let ts = TimeSpec::from_buffer(&buffer).expect("16 bytes must decode");
assert_eq!(ts.sec, 7);
assert_eq!(ts.nsec, 42);
}
#[test]
fn from_buffer_rejects_short_input() {
let short = [0u8; 8];
assert!(TimeSpec::from_buffer(&short).is_none());
assert!(TimeSpec::from_buffer(&[]).is_none());
}
#[test]
fn to_utc_date_time_matches_unix_epoch() {
let ts = TimeSpec { sec: 0, nsec: 0 };
let dt = ts.to_utc_date_time();
assert_eq!(dt.timestamp(), 0);
assert_eq!(dt.timestamp_subsec_nanos(), 0);
}
#[test]
fn to_utc_date_time_preserves_sec_and_nsec() {
let ts = TimeSpec {
sec: 1_700_000_000,
nsec: 123_456_789,
};
let dt = ts.to_utc_date_time();
assert_eq!(dt.timestamp(), 1_700_000_000);
assert_eq!(dt.timestamp_subsec_nanos(), 123_456_789);
}
}