use super::*;
use request_reassembly::{
expect_positive_ack, inject_routed_segment, present_value, recv_apdu, send_apdu,
send_segment_with_window, sent_routed_frame, split_into, start_reassembly_server,
start_routed_reassembly_server, write_property_payload,
};
pub(super) async fn next_routed_apdu(
sent: &SendLog,
index: &mut usize,
router: &MacAddr,
remote: &NpduAddress,
) -> Apdu {
tokio::time::timeout(Duration::from_secs(2), async {
while sent_count(sent) <= *index {
tokio::task::yield_now().await;
}
})
.await
.expect("timed out waiting for routed reply");
let (npdu, link_destination) = sent_routed_frame(sent, *index);
*index += 1;
assert_eq!(&link_destination, router);
assert_eq!(npdu.destination.as_ref(), Some(remote));
decode_apdu(npdu.payload).unwrap()
}
pub(super) fn assert_positive_ack(reply: Apdu, invoke_id: u8, seq: u8) {
match reply {
Apdu::SegmentAck(ack) => {
assert!(ack.sent_by_server && !ack.negative_ack);
assert_eq!(ack.invoke_id, invoke_id);
assert_eq!(ack.sequence_number, seq);
assert_eq!(ack.actual_window_size, 1);
}
other => panic!("expected positive SegmentAck, got {other:?}"),
}
}
pub(super) fn assert_server_abort(reply: Apdu, invoke_id: u8, reason: AbortReason) {
match reply {
Apdu::Abort(abort) => {
assert!(abort.sent_by_server);
assert_eq!(abort.invoke_id, invoke_id);
assert_eq!(abort.abort_reason, reason);
}
other => panic!("expected server Abort {reason:?}, got {other:?}"),
}
}
#[tokio::test]
async fn request_peer_quota_sixteen_admitted_seventeenth_refused_other_peer_admitted() {
let (server, incoming, sent) = start_routed_reassembly_server().await;
let router_a = test_mac(30);
let router_b = test_mac(31);
let remote = routed_address(400, 0x40);
let other_peer = routed_address(400, 0x41);
let mut index = 0;
for invoke_id in 0..16 {
let router = if invoke_id % 2 == 0 {
&router_a
} else {
&router_b
};
inject_routed_segment(&incoming, router, &remote, invoke_id, 0, true, &[1]).await;
assert_positive_ack(
next_routed_apdu(&sent, &mut index, router, &remote).await,
invoke_id,
0,
);
}
for invoke_id in 16..24 {
let router = if invoke_id % 2 == 0 {
&router_b
} else {
&router_a
};
inject_routed_segment(&incoming, router, &remote, invoke_id, 0, true, &[2]).await;
assert_server_abort(
next_routed_apdu(&sent, &mut index, router, &remote).await,
invoke_id,
AbortReason::OUT_OF_RESOURCES,
);
}
inject_routed_segment(&incoming, &router_b, &other_peer, 16, 0, true, &[3]).await;
assert_positive_ack(
next_routed_apdu(&sent, &mut index, &router_b, &other_peer).await,
16,
0,
);
assert_eq!(present_value(&server).await, "");
assert_eq!(
sent_count(&sent),
index,
"no service response to partial requests"
);
}
#[tokio::test]
async fn request_peer_quota_denial_is_stateless_and_active_transfers_complete() {
let (server, client, mut rx) = start_reassembly_server(Segmentation::BOTH).await;
let text = "all-sixteen-survive-denial-and-duplicates";
let chunks = split_into(&write_property_payload(text), 3);
for invoke_id in 0..16 {
send_segment_with_window(&client, invoke_id, 0, 1, true, &chunks[0]).await;
expect_positive_ack(&mut rx, invoke_id, 0).await;
}
for invoke_id in 16..32 {
send_segment_with_window(
&client,
invoke_id,
0,
1,
false,
&write_property_payload("denied-write"),
)
.await;
assert_server_abort(
recv_apdu(&mut rx, "quota denial").await,
invoke_id,
AbortReason::OUT_OF_RESOURCES,
);
send_segment_with_window(&client, invoke_id, 1, 1, false, &chunks[2]).await;
assert_server_abort(
recv_apdu(&mut rx, "denied invoke has no state").await,
invoke_id,
AbortReason::INVALID_APDU_IN_THIS_STATE,
);
}
assert_eq!(present_value(&server).await, "");
for invoke_id in 0..16 {
send_segment_with_window(&client, invoke_id, 1, 1, true, &chunks[1]).await;
expect_positive_ack(&mut rx, invoke_id, 1).await;
send_segment_with_window(&client, invoke_id, 0, 1, true, &[0xEE]).await;
assert!(
matches!(recv_apdu(&mut rx, "duplicate zero at quota").await,
Apdu::SegmentAck(ack) if ack.sent_by_server && ack.negative_ack
&& ack.invoke_id == invoke_id && ack.sequence_number == 1)
);
}
for invoke_id in 0..16 {
send_segment_with_window(&client, invoke_id, 2, 1, false, &chunks[2]).await;
expect_positive_ack(&mut rx, invoke_id, 2).await;
assert!(
matches!(recv_apdu(&mut rx, "unaffected active request completes").await,
Apdu::SimpleAck(ack) if ack.invoke_id == invoke_id)
);
assert_eq!(present_value(&server).await, text);
send_segment_with_window(&client, 100 + invoke_id, 0, 1, true, &chunks[0]).await;
expect_positive_ack(&mut rx, 100 + invoke_id, 0).await;
send_segment_with_window(&client, 200, 0, 1, true, &chunks[0]).await;
assert_server_abort(
recv_apdu(&mut rx, "refilled quota").await,
200,
AbortReason::OUT_OF_RESOURCES,
);
}
assert!(
tokio::time::timeout(Duration::from_millis(100), rx.recv())
.await
.is_err()
);
}
#[tokio::test]
async fn request_peer_quota_wrapped_zero_uses_existing_segment_cap_and_frees_slot() {
let (server, client, mut rx) = start_reassembly_server(Segmentation::BOTH).await;
for invoke_id in 1..16 {
send_segment_with_window(&client, invoke_id, 0, 1, true, &[1]).await;
expect_positive_ack(&mut rx, invoke_id, 0).await;
}
for seq in 0..=255 {
send_segment_with_window(&client, 0, seq, 1, true, &[2]).await;
expect_positive_ack(&mut rx, 0, seq).await;
}
send_segment_with_window(&client, 0, 0, 1, true, &[3]).await;
assert_server_abort(
recv_apdu(&mut rx, "wrapped zero at full peer quota").await,
0,
AbortReason::BUFFER_OVERFLOW,
);
send_segment_with_window(&client, 0, 1, 1, false, &[3]).await;
assert_server_abort(
recv_apdu(&mut rx, "overflow removed session").await,
0,
AbortReason::INVALID_APDU_IN_THIS_STATE,
);
send_segment_with_window(&client, 16, 0, 1, true, &[4]).await;
expect_positive_ack(&mut rx, 16, 0).await;
assert_eq!(present_value(&server).await, "");
}
#[tokio::test]
async fn request_peer_quota_abort_and_save_failure_release_only_owned_slots() {
let (server, client, mut rx) = start_reassembly_server(Segmentation::BOTH).await;
for invoke_id in 0..16 {
send_segment_with_window(&client, invoke_id, 0, 1, true, &[1]).await;
expect_positive_ack(&mut rx, invoke_id, 0).await;
}
for sent_by_server in [true, false] {
send_apdu(
&client,
&Apdu::Abort(AbortPdu {
sent_by_server,
invoke_id: 0,
abort_reason: AbortReason::OTHER,
}),
)
.await;
send_segment_with_window(&client, 16, 0, 1, true, &[2]).await;
let reply = recv_apdu(&mut rx, "admission after directional Abort").await;
if sent_by_server {
assert_server_abort(reply, 16, AbortReason::OUT_OF_RESOURCES);
} else {
assert_positive_ack(reply, 16, 0);
}
}
send_segment_with_window(&client, 0, 1, 1, true, &[2]).await;
assert_server_abort(
recv_apdu(&mut rx, "peer Abort removed inbound").await,
0,
AbortReason::INVALID_APDU_IN_THIS_STATE,
);
send_segment_with_window(&client, 1, 1, 1, true, &[0; 1477]).await;
assert_server_abort(
recv_apdu(&mut rx, "failed next save").await,
1,
AbortReason::BUFFER_OVERFLOW,
);
send_segment_with_window(&client, 1, 2, 1, true, &[2]).await;
assert_server_abort(
recv_apdu(&mut rx, "failed next save removed inbound").await,
1,
AbortReason::INVALID_APDU_IN_THIS_STATE,
);
for invoke_id in 17..20 {
send_segment_with_window(&client, invoke_id, 0, 1, true, &[0; 1477]).await;
assert_server_abort(
recv_apdu(&mut rx, "failed initial save").await,
invoke_id,
AbortReason::BUFFER_OVERFLOW,
);
send_segment_with_window(&client, invoke_id, 1, 1, true, &[2]).await;
assert_server_abort(
recv_apdu(&mut rx, "failed first save has no state").await,
invoke_id,
AbortReason::INVALID_APDU_IN_THIS_STATE,
);
}
send_segment_with_window(&client, 20, 0, 1, true, &[3]).await;
expect_positive_ack(&mut rx, 20, 0).await;
send_segment_with_window(&client, 21, 0, 1, true, &[0; 1477]).await;
assert_server_abort(
recv_apdu(&mut rx, "quota before attempted first save").await,
21,
AbortReason::OUT_OF_RESOURCES,
);
assert_eq!(present_value(&server).await, "");
assert!(
tokio::time::timeout(Duration::from_millis(100), rx.recv())
.await
.is_err()
);
}
async fn send_initial_with_raw_window(
client: &bacnet_transport::loopback::LoopbackTransport,
invoke_id: u8,
window: u8,
) {
use bacnet_encoding::npdu::{Npdu, encode_npdu};
let mut buf = BytesMut::new();
encode_apdu(
&mut buf,
&Apdu::ConfirmedRequest(ConfirmedRequestPdu {
segmented: true,
more_follows: true,
segmented_response_accepted: true,
max_segments: None,
max_apdu_length: 1476,
invoke_id,
sequence_number: Some(0),
proposed_window_size: Some(1),
service_choice: ConfirmedServiceChoice::WRITE_PROPERTY,
service_request: Bytes::from(vec![0; 1477]),
}),
)
.unwrap();
buf[4] = window;
let npdu = Npdu {
payload: buf.freeze(),
..Npdu::default()
};
let mut buf = BytesMut::new();
encode_npdu(&mut buf, &npdu).unwrap();
client.send_unicast(&buf, &[0x02]).await.unwrap();
}
#[tokio::test]
async fn request_peer_quota_preserves_window_and_support_precedence() {
let (server, client, mut rx) = start_reassembly_server(Segmentation::BOTH).await;
for invoke_id in 0..16 {
send_segment_with_window(&client, invoke_id, 0, 1, true, &[1]).await;
expect_positive_ack(&mut rx, invoke_id, 0).await;
}
for window in [0, 128, 255] {
send_initial_with_raw_window(&client, 16, window).await;
assert_server_abort(
recv_apdu(&mut rx, "invalid window before quota").await,
16,
AbortReason::WINDOW_SIZE_OUT_OF_RANGE,
);
}
send_segment_with_window(&client, 16, 0, 127, true, &[1]).await;
assert_server_abort(
recv_apdu(&mut rx, "valid window reaches quota").await,
16,
AbortReason::OUT_OF_RESOURCES,
);
assert_eq!(present_value(&server).await, "");
for segmentation in [Segmentation::NONE, Segmentation::TRANSMIT] {
let (server, client, mut rx) = start_reassembly_server(segmentation).await;
for invoke_id in 0..=16 {
send_initial_with_raw_window(&client, invoke_id, 0).await;
assert_server_abort(
recv_apdu(&mut rx, "unsupported before window and quota").await,
invoke_id,
AbortReason::SEGMENTATION_NOT_SUPPORTED,
);
}
assert_eq!(present_value(&server).await, "");
}
}