use std::net::Ipv4Addr;
use bacnet_server::server::BACnetServer;
use bacnet_transport::bip::BipTransport;
use bacnet_types::enums::{ObjectType, PropertyIdentifier};
use bacnet_types::primitives::ObjectIdentifier;
use ratatui::crossterm::event::KeyCode;
use tokio::sync::mpsc;
use super::capture::capturing;
use super::network::{plan, Net};
use super::site::bounded;
use super::*;
use crate::tui::capture::{worker as capture_worker, CaptureCommand, CaptureEvent, Direction};
use crate::tui::worker::{EventSink, EVENT_CHANNEL_CAPACITY};
async fn lab_server(instance: u32) -> BACnetServer<BipTransport> {
BACnetServer::bip_builder()
.device(super::lab::lab_identity(instance))
.interface(Ipv4Addr::LOCALHOST)
.port(0)
.broadcast_address(Ipv4Addr::LOCALHOST)
.database(super::lab::lab_database(instance))
.build()
.await
.unwrap()
}
fn filter(app: &mut App, text: &str) {
press(app, KeyCode::Char('/'));
type_text(app, text);
press(app, KeyCode::Enter);
}
#[tokio::test]
async fn a_read_property_over_bip_shows_the_tapped_request_and_response() {
let mut server = lab_server(4321).await;
let target = server.local_mac().to_vec();
let mut client = crate::transport::build_bip_client(&plan(None).args)
.await
.unwrap();
let (sink, mut events) = EventSink::channel(EVENT_CHANNEL_CAPACITY);
let (start, commands) = mpsc::channel(4);
let mut app = capturing();
let ai1 = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
let exchange = async {
start.send(CaptureCommand::Start).await.unwrap();
loop {
let event = events.recv().await.unwrap();
let started = matches!(event, WorkerEvent::Capture(CaptureEvent::Started { .. }));
worker(&mut app, event);
if started {
break;
}
}
let value = client.read_property(&target, ai1, PropertyIdentifier::PRESENT_VALUE, None);
assert!(value.await.is_ok());
let refused = client.read_property(&target, ai1, PropertyIdentifier::FILE_SIZE, None);
assert!(refused.await.is_err());
while app.capture.ring.len() < 4 {
worker(&mut app, events.recv().await.unwrap());
}
drop(start);
};
let pump = capture_worker::run(&client, &sink, commands, AddressStyle::Bip);
bounded(async { tokio::join!(pump, exchange) }).await;
let ring = &app.capture.ring;
let frames: Vec<_> = (1..=4).map(|n| ring.get(n).unwrap().clone()).collect();
let directions: Vec<Direction> = frames.iter().map(|f| f.direction).collect();
assert_eq!(
directions,
[Direction::Out, Direction::In, Direction::Out, Direction::In]
);
let server_address = crate::output::format_mac(&target);
assert_eq!(frames[0].destination, server_address);
assert_eq!(frames[1].source, server_address);
let pdus: Vec<_> = frames
.iter()
.map(|f| f.summary.pdu.clone().unwrap())
.collect();
assert_eq!(
pdus,
[
"Confirmed-Request",
"Complex-ACK",
"Confirmed-Request",
"Error"
]
);
let invoke = frames[0].summary.invoke_id.unwrap();
assert_eq!(frames[1].summary.invoke_id, Some(invoke));
assert!(
frames.iter().all(|f| f.udp.is_some()),
"endpoints for export"
);
press(&mut app, KeyCode::Home);
press(&mut app, KeyCode::Down);
app.settle();
let detail = app.capture.detail.as_ref().unwrap();
assert_eq!(detail.number, 2);
let labels = flat_labels(&detail.fields);
assert!(
labels.contains(&format!("Invoke ID: {invoke}")),
"{labels:?}"
);
assert!(
labels.contains(&"Service: read-property".to_string()),
"{labels:?}"
);
assert!(labels.iter().any(|l| l == "Real: 72.5"), "{labels:?}");
filter(&mut app, "svc:ReadProperty errors");
assert_eq!(app.capture.visible().collect::<Vec<_>>(), [4]);
client.stop().await.unwrap();
server.stop().await.unwrap();
}
fn flat_labels(fields: &[crate::decode::tree::Field]) -> Vec<String> {
let mut out = Vec::new();
for field in fields {
out.push(field.label.clone());
out.extend(flat_labels(&field.children));
}
out
}
#[tokio::test]
async fn the_session_tap_reaches_the_screen_through_the_coordinator() {
let mut net = Net::start(plan(None)).await;
net.press(KeyCode::Char('7')).await;
net.pump_until("the tap", |app| !app.capture.sources.is_empty())
.await;
net.press(KeyCode::Char('1')).await;
net.press(KeyCode::Char('d')).await;
net.press(KeyCode::Enter).await;
net.press(KeyCode::Enter).await;
net.pump_until("the Who-Is", |app| app.capture.ring.len() > 0)
.await;
let first = net.app.capture.ring.get(1).unwrap();
assert_eq!(first.summary.service.as_deref(), Some("who-is"));
assert_eq!(first.direction, Direction::Out);
assert!(first.summary.link.contains("broadcast"), "{first:?}");
net.press(KeyCode::Char('x')).await;
net.pump_until("down", |app| app.link == crate::tui::app::Link::Down)
.await;
assert!(net.app.capture.sources.is_empty(), "the tap went with it");
assert_eq!(
net.app.capture.ring.len() as u64,
net.app.capture.ring.next() - 1
);
net.press(KeyCode::Char('c')).await;
net.pump_until("the tap again", |app| !app.capture.sources.is_empty())
.await;
net.stop().await;
}
#[tokio::test(start_paused = true)]
async fn the_monitor_feeds_the_capture_and_still_writes_nothing() {
use super::mstp_monitor::{acceptance_line, Run};
let mut run = Run::start(acceptance_line(), None, false).await;
run.listen_for(Duration::ZERO).await;
run.press(KeyCode::Char('7')).await;
run.listen_for(Duration::from_secs(6)).await;
let capture = &run.app.capture;
assert_eq!(
capture.sources,
[("MS/TP monitor, every frame on the line".to_string(), true)]
);
let links: std::collections::BTreeSet<String> = (1..capture.ring.next())
.filter_map(|n| capture.ring.get(n))
.map(|f| f.summary.link.clone())
.collect();
for link in [
"token",
"bacnet-data-expecting-reply",
"bacnet-data-not-expecting-reply",
] {
assert!(links.contains(link), "{link}: {links:?}");
}
assert_eq!(capture.lost, 0);
let token = (1..capture.ring.next())
.filter_map(|n| capture.ring.get(n))
.find(|f| f.summary.link == "token")
.unwrap();
assert_eq!(&token.bytes[..2], &[0x55, 0xFF], "from the preamble");
assert_eq!(token.direction, Direction::Wire);
assert_eq!(run.quit().await, b"", "the monitor wrote to the line");
}