use std::ffi::{c_char, CStr, CString};
use std::ptr;
use mx_remote::{V2ipDecoderDetail, V2ipDecoderFormat, V2ipDecoderReason, V2ipDecoderReport};
use mx_remote_ffi::*;
fn uid_n(n: u8) -> mxr_uid_t {
let mut bytes = [0u8; 16];
bytes[0] = n;
bytes[15] = 0xa5;
mxr_uid_t { bytes }
}
fn client(name: &CStr, uid: &CStr) -> *mut mxr_remote_t {
let config = mxr_config_t {
target_ip: ptr::null(),
port: 0,
broadcast: false,
local_ip: ptr::null(),
interface: ptr::null(),
name: name.as_ptr(),
uid: uid.as_ptr(),
uid_path: ptr::null(),
};
let handle = unsafe { mxr_remote_new(&config, ptr::null(), ptr::null_mut()) };
assert!(
!handle.is_null(),
"the client was not created: {}",
last_error()
);
handle
}
fn last_error() -> String {
unsafe { CStr::from_ptr(mxr_last_error()) }
.to_string_lossy()
.into_owned()
}
fn field(bytes: &[c_char]) -> String {
let text: Vec<u8> = bytes
.iter()
.take_while(|b| **b != 0)
.map(|b| *b as u8)
.collect();
String::from_utf8_lossy(&text).into_owned()
}
#[test]
fn an_identifier_survives_the_trip_out_to_text_and_back() {
let uid = uid_n(7);
let mut text = [0 as c_char; MXR_UID_STRING_LEN];
let rc = unsafe { mxr_uid_to_string(uid, text.as_mut_ptr(), text.len()) };
assert_eq!(rc, mxr_result_t::MXR_OK);
let mut back = mxr_uid_t::default();
let rc = unsafe { mxr_uid_from_string(text.as_ptr(), &mut back) };
assert_eq!(rc, mxr_result_t::MXR_OK);
assert_eq!(
back,
uid,
"{} did not read back as it was written",
field(&text)
);
}
#[test]
fn a_buffer_too_short_for_an_identifier_is_refused_rather_than_filled() {
let mut text = [0 as c_char; MXR_UID_STRING_LEN - 1];
let rc = unsafe { mxr_uid_to_string(uid_n(1), text.as_mut_ptr(), text.len()) };
assert_eq!(rc, mxr_result_t::MXR_ERR_INVALID_ARGUMENT);
assert_eq!(text[0], 0, "a refused call still wrote to the buffer");
}
#[test]
fn text_that_is_not_an_identifier_is_refused_rather_than_read_as_zero() {
let text = CString::new("not a uid").expect("no NUL");
let mut out = uid_n(3);
let rc = unsafe { mxr_uid_from_string(text.as_ptr(), &mut out) };
assert_eq!(rc, mxr_result_t::MXR_ERR_INVALID_ARGUMENT);
assert_eq!(out, uid_n(3), "a refused parse still wrote an identifier");
assert!(
last_error().contains("not a uid"),
"the failure did not say what could not be read: {}",
last_error()
);
}
#[test]
fn the_empty_identifier_is_the_one_the_protocol_spells_absence_with() {
assert!(mxr_uid_is_zero(mxr_uid_t::default()));
assert!(!mxr_uid_is_zero(uid_n(1)));
}
#[test]
fn a_null_handle_is_an_argument_error_rather_than_a_crash() {
let mut info = std::mem::MaybeUninit::<mxr_device_info_t>::zeroed();
let rc = unsafe { mxr_device(ptr::null(), uid_n(1), info.as_mut_ptr()) };
assert_eq!(rc, mxr_result_t::MXR_ERR_INVALID_ARGUMENT);
assert!(!last_error().is_empty(), "the failure said nothing");
let count = unsafe { mxr_devices(ptr::null(), ptr::null_mut(), 0) };
assert_eq!(count, 0);
let rc = unsafe { mxr_power_on(ptr::null(), mxr_bay_uid_t::default()) };
assert_eq!(rc, mxr_result_t::MXR_ERR_INVALID_ARGUMENT);
unsafe { mxr_remote_close(ptr::null()) };
unsafe { mxr_remote_free(ptr::null_mut()) };
}
#[test]
fn a_client_reports_the_identifier_and_name_it_was_given() {
let name = CString::new("test client").expect("no NUL");
let uid_text = CString::new("00000007.00000000.00000000.a5000000").expect("no NUL");
let handle = client(&name, &uid_text);
let mut uid = mxr_uid_t::default();
let rc = unsafe { mxr_remote_uid(handle, &mut uid) };
assert_eq!(rc, mxr_result_t::MXR_OK);
assert_eq!(uid, uid_n(7), "the configured identifier was not kept");
let mut buf = [0 as c_char; 32];
let rc = unsafe { mxr_remote_name(handle, buf.as_mut_ptr(), buf.len()) };
assert_eq!(rc, mxr_result_t::MXR_OK);
assert_eq!(field(&buf), "test client");
unsafe { mxr_remote_free(handle) };
}
#[test]
fn a_client_that_was_never_started_has_no_target() {
let name = CString::new("unstarted").expect("no NUL");
let uid_text = CString::new("00000001.00000000.00000000.00000000").expect("no NUL");
let handle = client(&name, &uid_text);
let mut port = 0u16;
let rc = unsafe { mxr_remote_target(handle, ptr::null_mut(), 0, &mut port) };
assert_eq!(rc, mxr_result_t::MXR_ERR_NOT_CONNECTED);
unsafe { mxr_remote_free(handle) };
}
#[test]
fn a_command_for_a_device_never_heard_from_is_not_found() {
let name = CString::new("no devices").expect("no NUL");
let uid_text = CString::new("00000002.00000000.00000000.00000000").expect("no NUL");
let handle = client(&name, &uid_text);
let rc = unsafe { mxr_reboot(handle, uid_n(9)) };
assert_eq!(rc, mxr_result_t::MXR_ERR_NOT_FOUND);
assert!(
last_error().contains("no device"),
"the failure did not say what was missing: {}",
last_error()
);
unsafe { mxr_remote_free(handle) };
}
#[test]
fn a_configuration_that_cannot_be_read_yields_no_client() {
let bad = CString::new("300.1.1.1").expect("no NUL");
let config = mxr_config_t {
target_ip: ptr::null(),
port: 0,
broadcast: false,
local_ip: bad.as_ptr(),
interface: ptr::null(),
name: ptr::null(),
uid: ptr::null(),
uid_path: ptr::null(),
};
let handle = unsafe { mxr_remote_new(&config, ptr::null(), ptr::null_mut()) };
assert!(
handle.is_null(),
"a client was built from an unreadable address"
);
assert!(
last_error().contains("local_ip"),
"the failure did not name the field: {}",
last_error()
);
}
#[test]
fn a_list_call_reports_its_length_when_given_nowhere_to_write() {
let name = CString::new("sizing").expect("no NUL");
let uid_text = CString::new("00000003.00000000.00000000.00000000").expect("no NUL");
let handle = client(&name, &uid_text);
let count = unsafe { mxr_devices(handle, ptr::null_mut(), 0) };
assert_eq!(count, 0, "a client that has heard nothing listed a device");
unsafe { mxr_remote_free(handle) };
}
#[test]
fn the_version_is_the_crate_version() {
let version = unsafe { CStr::from_ptr(mxr_version()) };
assert_eq!(version.to_str().expect("ASCII"), env!("CARGO_PKG_VERSION"));
}
#[test]
fn a_subsystem_of_a_device_never_heard_from_is_not_found() {
let name = CString::new("subsystems").expect("no NUL");
let uid_text = CString::new("00000004.00000000.00000000.00000000").expect("no NUL");
let handle = client(&name, &uid_text);
let mut stats = std::mem::MaybeUninit::<mxr_v2ip_stats_t>::zeroed();
let rc = unsafe { mxr_v2ip_stats(handle, uid_n(4), stats.as_mut_ptr()) };
assert_eq!(rc, mxr_result_t::MXR_ERR_NOT_FOUND);
assert!(
last_error().contains("no device"),
"the failure did not say the device was missing: {}",
last_error()
);
let count = unsafe { mxr_audio_endpoints(handle, uid_n(4), ptr::null_mut(), 0) };
assert_eq!(count, 0);
unsafe { mxr_remote_free(handle) };
}
#[test]
fn a_subsystem_read_without_somewhere_to_write_is_an_argument_error() {
let name = CString::new("no output").expect("no NUL");
let uid_text = CString::new("00000005.00000000.00000000.00000000").expect("no NUL");
let handle = client(&name, &uid_text);
let rc = unsafe { mxr_multiviewer_status(handle, uid_n(5), ptr::null_mut()) };
assert_eq!(rc, mxr_result_t::MXR_ERR_INVALID_ARGUMENT);
unsafe { mxr_remote_free(handle) };
}
struct Bit {
name: String,
value: u64,
}
fn workspace_source(path: &str) -> String {
let full = format!("{}/../{path}", env!("CARGO_MANIFEST_DIR"));
let text =
std::fs::read_to_string(&full).unwrap_or_else(|e| panic!("{full} could not be read: {e}"));
text.replace("\r\n", "\n")
}
fn value_of(expr: &str, whose: &str) -> u64 {
let expr = expr.trim();
if let Some(hex) = expr.strip_prefix("0x") {
return u64::from_str_radix(hex, 16)
.unwrap_or_else(|_| panic!("{whose}: {expr} is not a hexadecimal literal"));
}
if let Some((lhs, rhs)) = expr.split_once("<<") {
let base: u64 = lhs
.trim()
.parse()
.unwrap_or_else(|_| panic!("{whose}: {expr}"));
let shift: u32 = rhs
.trim()
.parse()
.unwrap_or_else(|_| panic!("{whose}: {expr}"));
return base << shift;
}
expr.parse()
.unwrap_or_else(|_| panic!("{whose}: {expr} is neither a literal nor a shift"))
}
fn core_bits(source: &str, block: &str) -> Vec<Bit> {
let start = source
.find(&format!("\n {block} {{"))
.or_else(|| source.find(&format!("\n {block}: ")))
.unwrap_or_else(|| panic!("{block} is not in the core source under that name"));
let body = &source[start..];
let end = body
.find("\n }\n}")
.unwrap_or_else(|| panic!("{block} has no end"));
read_bits(&body[..end], block, |line| {
let (name, expr) = line.strip_suffix(';')?.split_once(" = ")?;
Some((name.to_owned(), expr.to_owned()))
})
}
fn core_consts(source: &str, ty: &str) -> Vec<Bit> {
read_bits(source, ty, |line| {
let rest = line.strip_prefix("pub const ")?;
let (name, rest) = rest.split_once(": Self = Self(")?;
Some((name.to_owned(), rest.strip_suffix(");")?.to_owned()))
})
}
fn header_bits(source: &str, prefix: &str, not: &[&str]) -> Vec<Bit> {
read_bits(source, prefix, |line| {
let rest = line.strip_prefix("pub const ")?;
let (name, rest) = rest.split_once(": ")?;
let expr = rest.split_once(" = ")?.1.strip_suffix(';')?;
Some((name.to_owned(), expr.to_owned()))
})
.into_iter()
.filter(|b| b.name.starts_with(prefix) && !not.iter().any(|n| b.name.starts_with(n)))
.collect()
}
fn read_bits(
source: &str,
whose: &str,
parse: impl Fn(&str) -> Option<(String, String)>,
) -> Vec<Bit> {
source
.lines()
.map(str::trim)
.filter_map(&parse)
.map(|(name, expr)| Bit {
value: value_of(&expr, whose),
name,
})
.collect()
}
#[test]
fn every_core_bit_reaches_the_header_at_its_own_value() {
let enums = workspace_source("mx-remote/src/wire/enums.rs");
let audio = workspace_source("mx-remote/src/types/audio.rs");
let header = workspace_source("mx-remote-ffi/src/bits.rs");
let lists: [(&str, Vec<Bit>, &[&str], usize); 15] = [
("MXR_FEATURE_", core_bits(&enums, "DeviceFeature"), &[], 27),
(
"MXR_BAY_",
core_bits(&enums, "BayFeatures"),
&["MXR_BAY_STATUS_"],
17,
),
("MXR_BAY_STATUS_", core_bits(&enums, "BayStatus"), &[], 18),
("MXR_KEY_", core_bits(&enums, "RcKey"), &[], 48),
("MXR_AUDIO_", core_consts(&audio, "AudioFeatures"), &[], 15),
(
"MXR_MV_VIEW_MODE_",
core_bits(&enums, "MultiviewerViewMode"),
&[],
9,
),
(
"MXR_MV_PIP_POSITION_",
core_bits(&enums, "MultiviewerPipPosition"),
&[],
5,
),
(
"MXR_MV_PIP_SIZE_",
core_bits(&enums, "MultiviewerPipSize"),
&[],
4,
),
(
"MXR_MV_OUTPUT_",
core_bits(&enums, "MultiviewerOutputMode"),
&[],
15,
),
(
"MXR_MV_HDCP_",
core_bits(&enums, "MultiviewerHdcpMode"),
&[],
4,
),
(
"MXR_MV_",
core_bits(&enums, "MultiviewerEdidTemplate"),
&[
"MXR_MV_VIEW_MODE_",
"MXR_MV_PIP_",
"MXR_MV_OUTPUT_",
"MXR_MV_HDCP_",
"MXR_MV_ITC_",
"MXR_MV_ASPECT_",
"MXR_MV_BOOL_",
"MXR_MV_SOURCE_",
],
20,
),
(
"MXR_MV_ITC_",
core_bits(&enums, "MultiviewerItcMode"),
&[],
3,
),
(
"MXR_MV_ASPECT_",
core_bits(&enums, "MultiviewerAspectRatio"),
&[],
3,
),
("MXR_MV_BOOL_", core_bits(&enums, "MultiviewerBool"), &[], 3),
(
"MXR_MV_SOURCE_",
core_bits(&enums, "MultiviewerSource"),
&[],
5,
),
];
for (prefix, core, not, minimum) in lists {
assert!(
core.len() >= minimum,
"{prefix}: found {} constants in the core crate, expected at least {minimum}; \
the pattern that reads them has stopped matching",
core.len()
);
let mine = header_bits(&header, prefix, not);
assert_eq!(
mine.len(),
core.len(),
"{prefix}: the header names {} of the core crate's {}",
mine.len(),
core.len()
);
for bit in &core {
let name = format!("{prefix}{}", bit.name);
let found = mine
.iter()
.find(|b| b.name == name)
.unwrap_or_else(|| panic!("{name} is in the core crate and not in the header"));
assert_eq!(found.value, bit.value, "{name} differs between the two");
}
}
}
#[test]
fn the_signal_type_accessors_read_a_packed_word() {
let word = 16 | (1 << 8) | (2 << 13);
assert_eq!(mxr_signal_type_svd(word), 16);
assert_eq!(mxr_signal_type_colour_space(word), 1);
assert_eq!(mxr_signal_type_bpp_index(word), 2);
assert_eq!(mxr_signal_type_bpp(word), 10);
assert!(mxr_signal_type_is_set(word));
assert!(!mxr_signal_type_is_set(5 << 13));
assert!(!mxr_signal_type_is_set(0));
assert_eq!(mxr_signal_type_bpp(5 << 13), 0);
let status = MXR_BAY_STATUS_SIGNAL_DETECTED | (3 << 16) | (2 << 22);
assert_eq!(mxr_bay_status_rc_type(status), 3);
assert_eq!(mxr_bay_status_hdcp(status), 2);
}
#[test]
fn the_new_entry_points_refuse_a_null_handle() {
let route = mxr_v2ip_route_t {
video: mxr_stream_addr_t {
ip: ptr::null(),
port: 0,
},
audio: mxr_stream_addr_t {
ip: ptr::null(),
port: 0,
},
anc: mxr_stream_addr_t {
ip: ptr::null(),
port: 0,
},
};
let mut edid = [0u8; MXR_EDID_LEN];
let mut audio = std::mem::MaybeUninit::<mxr_audio_details_t>::zeroed();
let mut frames = 0u64;
let calls = unsafe {
[
mxr_select_source_addr(ptr::null(), mxr_bay_uid_t::default(), &route, ptr::null()),
mxr_send_key(ptr::null(), mxr_bay_uid_t::default(), MXR_KEY_PLAY),
mxr_request_edid(ptr::null(), uid_n(1), true),
mxr_request_signal_status(ptr::null(), uid_n(1)),
mxr_bay_audio_details(ptr::null(), mxr_bay_uid_t::default(), audio.as_mut_ptr()),
mxr_device_edid(ptr::null(), uid_n(1), true, edid.as_mut_ptr(), edid.len()),
mxr_frames_received(ptr::null(), &mut frames),
]
};
for (n, rc) in calls.into_iter().enumerate() {
assert_eq!(rc, mxr_result_t::MXR_ERR_INVALID_ARGUMENT, "call {n}");
}
assert!(!last_error().is_empty(), "the failure said nothing");
}
#[test]
fn the_new_reads_separate_an_unknown_device_from_an_unreported_value() {
let remote = client(c"abi-new-reads", c"00000021.00000000.00000000.000000a5");
let mut frames = 1234u64;
assert_eq!(
unsafe { mxr_frames_received(remote, &mut frames) },
mxr_result_t::MXR_OK
);
assert_eq!(frames, 0, "a client with no socket has heard nothing");
let mut edid = [0u8; MXR_EDID_LEN];
let rc = unsafe { mxr_device_edid(remote, uid_n(9), true, edid.as_mut_ptr(), edid.len()) };
assert_eq!(rc, mxr_result_t::MXR_ERR_NOT_REPORTED);
let rc = unsafe { mxr_device_edid(remote, uid_n(9), true, edid.as_mut_ptr(), 8) };
assert_eq!(rc, mxr_result_t::MXR_ERR_INVALID_ARGUMENT);
let mut audio = std::mem::MaybeUninit::<mxr_audio_details_t>::zeroed();
let rc = unsafe { mxr_bay_audio_details(remote, mxr_bay_uid_t::default(), audio.as_mut_ptr()) };
assert_eq!(rc, mxr_result_t::MXR_ERR_NOT_FOUND, "no such bay");
unsafe { mxr_remote_free(remote) };
}
#[test]
fn a_route_with_an_unparseable_address_is_an_argument_error() {
let remote = client(c"abi-bad-route", c"00000022.00000000.00000000.000000a5");
let bad = CString::new("not an address").expect("no NUL");
let route = mxr_v2ip_route_t {
video: mxr_stream_addr_t {
ip: bad.as_ptr(),
port: 0,
},
audio: mxr_stream_addr_t {
ip: ptr::null(),
port: 0,
},
anc: mxr_stream_addr_t {
ip: ptr::null(),
port: 0,
},
};
let rc =
unsafe { mxr_select_source_addr(remote, mxr_bay_uid_t::default(), &route, ptr::null()) };
assert_eq!(rc, mxr_result_t::MXR_ERR_INVALID_ARGUMENT);
assert!(
last_error().contains("not an address"),
"the failure did not name the address it could not read: {}",
last_error()
);
let rc = unsafe {
mxr_select_source_addr(remote, mxr_bay_uid_t::default(), ptr::null(), ptr::null())
};
assert_eq!(rc, mxr_result_t::MXR_ERR_INVALID_ARGUMENT);
unsafe { mxr_remote_free(remote) };
}
#[test]
fn only_an_answered_decoder_carries_a_reading() {
let answered: mxr_v2ip_decoder_t = V2ipDecoderDetail::Answered(V2ipDecoderReport {
reason: V2ipDecoderReason::PTP_UNLOCKED,
blocking: true,
width: 3840,
height: 2160,
format: V2ipDecoderFormat::YCBCR_420,
updates: 600,
flags: 1 << 8,
blocked_count: 100_009,
})
.into();
assert_eq!(
answered.detail,
mxr_v2ip_decoder_detail_t::MXR_V2IP_DECODER_ANSWERED
);
assert_eq!(answered.reason, 8);
assert!(answered.blocking);
assert_eq!((answered.width, answered.height), (3840, 2160));
assert_eq!(answered.format, 3);
assert_eq!(answered.updates, 600);
assert_eq!(answered.flags, 1 << 8);
assert_eq!(answered.blocked_count, 100_009);
for (detail, expected) in [
(
V2ipDecoderDetail::Absent,
mxr_v2ip_decoder_detail_t::MXR_V2IP_DECODER_ABSENT,
),
(
V2ipDecoderDetail::NeverAnswered,
mxr_v2ip_decoder_detail_t::MXR_V2IP_DECODER_NEVER_ANSWERED,
),
] {
let empty: mxr_v2ip_decoder_t = detail.into();
assert_eq!(empty.detail, expected);
assert!(!empty.blocking);
assert_eq!(
(
u32::from(empty.reason),
u32::from(empty.width),
u32::from(empty.height),
u32::from(empty.format),
u32::from(empty.updates),
empty.flags,
empty.blocked_count,
),
(0, 0, 0, 0, 0, 0, 0),
"{expected:?} carried a reading it does not have"
);
}
}