use crate::event::Event;
use crate::wire::{op, BayFeatures, BayStatus, BayUid, DeviceFeature, V2IP_PORT_VIDEO};
use crate::testing::{bay_config_rec, link_rec, poisoned, stream_rec, uid_n};
use super::Harness;
#[test]
fn discovery_builds_a_device_from_its_configuration() {
let mut h = Harness::new(1);
let sender = h.sender;
let source = uid_n(9);
h.hello(
0x27,
"FF88",
"AB1234",
DeviceFeature::V2IP_SOURCE | DeviceFeature::V2IP_SINK,
);
assert_eq!(h.device().serial(), "AB1234");
assert!(h.device().is_v2ip());
assert_eq!(
h.device().hello.address,
Some(std::net::Ipv4Addr::new(10, 8, 8, 9))
);
let mut cfg = bay_config_rec(
0,
0,
0,
"Input 1",
"Apple TV",
BayStatus::NONE,
BayFeatures::V2IP_SOURCE_LOCAL,
);
cfg.extend(bay_config_rec(
1,
1,
0,
"Output 1",
"Living Room",
BayStatus::NONE,
BayFeatures::V2IP_SINK_LOCAL,
));
h.feed(op::SYS_BAY_CONFIG, &cfg);
assert_eq!(h.device().bays.len(), 2);
let input = h.bay(0);
assert!(input.is_input() && input.is_v2ip_source());
assert_eq!(input.user_name(), "Apple TV");
let mut sources = stream_rec(
source,
"239.1.1.1",
"239.1.1.2",
"239.1.1.3",
V2IP_PORT_VIDEO,
);
sources.extend(stream_rec(
uid_n(10),
"239.2.2.1",
"239.2.2.2",
"239.2.2.3",
V2IP_PORT_VIDEO,
));
h.feed(op::SYS_BAY_V2IP_SOURCES, &sources);
let advertised = h
.device()
.v2ip_sources
.clone()
.expect("no stream addresses");
assert_eq!(advertised.len(), 2);
assert_eq!(advertised[1].uid, uid_n(10));
assert_eq!(
advertised[1].video.ip,
std::net::Ipv4Addr::new(239, 2, 2, 1)
);
assert_eq!(advertised[1].audio.port, V2IP_PORT_VIDEO);
let device = h.device();
let streams = device
.v2ip_source_for(device.bay(0).expect("input bay"))
.expect("no stream addresses");
assert_eq!(streams.video.ip, std::net::Ipv4Addr::new(239, 1, 1, 1));
assert_eq!(streams.audio.ip, std::net::Ipv4Addr::new(239, 1, 1, 2));
assert_eq!(
h.state.link_key(BayUid::new(sender, 0)),
Some(BayUid::new(source, 0))
);
assert!(!h.complete());
h.feed(op::SYS_LINKS, &link_rec(0, "AMP00001", "Zone 1", 0));
assert!(h.complete());
assert!(h.saw(|e| matches!(e, Event::DeviceConfigComplete { .. })));
}
#[test]
fn mirror_and_mesh_reports() {
let mut h = Harness::new(2);
let sender = h.sender;
let master = uid_n(7);
h.hello(
0x27,
"ONEIP",
"RX0001",
DeviceFeature::V2IP_SINK | DeviceFeature::MESH,
);
h.feed(
op::SYS_BAY_CONFIG,
&bay_config_rec(
0,
1,
0,
"Output 1",
"TV",
BayStatus::NONE,
BayFeatures::V2IP_SINK_LOCAL,
),
);
let mut mp = poisoned(32);
mp[0..16].copy_from_slice(sender.as_bytes());
mp[16..32].copy_from_slice(master.as_bytes());
h.feed(op::BAY_MIRROR_STATUS, &mp);
let mirror = h.bay(0).mirror;
assert!(mirror.is_mirroring());
assert_eq!(mirror.target.map(|b| b.device), Some(master));
assert!(h.saw(|e| matches!(e, Event::MirrorStatusChanged { .. })));
let mut mesh = poisoned(40);
mesh[0] = 0xFF;
mesh[4..20].copy_from_slice(master.as_bytes());
h.feed(op::MESH_OPERATION, &mesh);
assert_eq!(h.device().mesh_master, master);
assert!(h.saw(|e| matches!(e, Event::MeshMasterChanged { .. })));
}
#[test]
fn a_mesh_report_below_its_own_gate_is_not_a_master() {
let mut h = Harness::new(3);
let master = uid_n(77);
h.hello(
0x28,
"ONEIP",
"MG0001",
DeviceFeature::V2IP_SINK | DeviceFeature::MESH,
);
let mut mesh = poisoned(40);
mesh[0] = 0xFF;
mesh[4..20].copy_from_slice(master.as_bytes());
h.feed(op::MESH_OPERATION, &mesh[..36]);
assert_eq!(
h.device().mesh_master,
crate::wire::DeviceUid::ZERO,
"a frame short of the struct named a master"
);
h.feed_proto(op::MESH_OPERATION, 0x19, &mesh);
assert_eq!(
h.device().mesh_master,
crate::wire::DeviceUid::ZERO,
"a frame stamped below the accept gate named a master"
);
h.feed_proto(op::MESH_OPERATION, 0x1A, &mesh);
assert_eq!(h.device().mesh_master, master);
}
#[test]
fn a_frame_from_an_unknown_sender_is_not_acted_on() {
use crate::testing::hello_payload;
let mut h = Harness::new(12);
let sink = h.sender;
let stranger = uid_n(99);
h.hello(
0x28,
"ONEIP",
"SG0001",
DeviceFeature::V2IP_SINK | DeviceFeature::MESH,
);
let mut switch = poisoned(40);
switch[0..16].copy_from_slice(sink.as_bytes());
switch[16..20].copy_from_slice(&[239, 1, 1, 1]);
switch[24..28].copy_from_slice(&[239, 1, 1, 2]);
h.feed_as(stranger, op::V2IP_MANUAL_SRC_SWITCH, &switch);
assert!(
h.device().v2ip_sink.is_none(),
"a stranger's route request was applied to the sink"
);
h.feed_as(
stranger,
op::SYS_HELLO,
&hello_payload(0x28, "ONEIP", "SG0002", "4.8.0", DeviceFeature::V2IP_SOURCE),
);
h.feed_as(stranger, op::V2IP_MANUAL_SRC_SWITCH, &switch);
assert!(
h.device().v2ip_sink.is_some(),
"the hello exemption did not register the sender"
);
}
fn source_device(n: u8) -> Harness {
let mut h = Harness::new(n);
h.hello(
0x27,
"FF88",
"PG0004",
DeviceFeature::V2IP_SOURCE | DeviceFeature::V2IP_SINK,
);
h
}
fn source_page(first: u16, total: u16, records: &[Vec<u8>]) -> Vec<u8> {
let mut p = Vec::new();
p.extend_from_slice(&first.to_le_bytes());
p.extend_from_slice(&total.to_le_bytes());
p.extend_from_slice(&[0; 4]);
for r in records {
p.extend_from_slice(r);
}
p
}
fn source_rec(n: u8) -> Vec<u8> {
stream_rec(
uid_n(n),
&format!("239.9.{n}.1"),
&format!("239.9.{n}.2"),
&format!("239.9.{n}.3"),
V2IP_PORT_VIDEO,
)
}
#[test]
fn a_source_list_of_an_unplaceable_length_is_refused() {
let mut h = source_device(31);
let mut odd = source_rec(41);
odd.extend_from_slice(&[0; 4]);
h.feed(op::SYS_BAY_V2IP_SOURCES, &odd);
assert!(
h.device().v2ip_sources.is_none(),
"a record read at the wrong offset was reported as a source"
);
}
#[test]
fn source_pages_are_placed_by_their_stated_position() {
let mut h = source_device(32);
h.feed(
op::SYS_BAY_V2IP_SOURCES,
&source_page(2, 4, &[source_rec(43), source_rec(44)]),
);
assert!(
h.device().v2ip_sources.is_none(),
"a list was reported with its first records still missing"
);
h.feed(
op::SYS_BAY_V2IP_SOURCES,
&source_page(0, 4, &[source_rec(41), source_rec(42)]),
);
let sources = h.device().v2ip_sources.clone().expect("no sources");
assert_eq!(
sources.iter().map(|s| s.uid).collect::<Vec<_>>(),
vec![uid_n(41), uid_n(42), uid_n(43), uid_n(44)],
"pages were ordered by arrival rather than by position"
);
}
#[test]
fn a_page_does_not_replace_the_list_it_is_part_of() {
let mut h = source_device(33);
h.feed(
op::SYS_BAY_V2IP_SOURCES,
&[source_rec(41), source_rec(42), source_rec(43)].concat(),
);
assert_eq!(h.device().v2ip_sources.as_ref().map(Vec::len), Some(3));
h.feed(
op::SYS_BAY_V2IP_SOURCES,
&source_page(1, 3, &[source_rec(52)]),
);
let sources = h.device().v2ip_sources.clone().expect("the list was lost");
assert_eq!(
sources.iter().map(|s| s.uid).collect::<Vec<_>>(),
vec![uid_n(41), uid_n(52), uid_n(43)],
"a page was read as the whole list"
);
}
#[test]
fn a_device_that_has_not_sent_its_bays_is_not_fully_described() {
let mut h = Harness::new(34);
h.hello(0x28, "FF88", "PG0005", DeviceFeature::VIDEO_ROUTING);
assert!(
!h.complete(),
"a device was fully described on its hello alone"
);
assert!(
!h.saw(|e| matches!(e, Event::DeviceConfigComplete { .. })),
"completion was announced before anything was known"
);
h.feed(
op::SYS_BAY_CONFIG_SECONDARY,
&bay_config_rec(
2,
1,
0,
"Output 2",
"TV",
BayStatus::NONE,
BayFeatures::HDMI_OUT,
),
);
h.feed(op::SYS_LINKS, &link_rec(2, "AMP00001", "Zone 2", 0));
assert!(
!h.complete(),
"the secondary list was taken for the configuration"
);
h.feed(
op::SYS_BAY_CONFIG,
&bay_config_rec(
1,
1,
0,
"Output 1",
"TV",
BayStatus::NONE,
BayFeatures::HDMI_OUT,
),
);
assert!(h.complete());
assert!(h.saw(|e| matches!(e, Event::DeviceConfigComplete { .. })));
}