bacnet-server 0.15.0

BACnet server: APDU dispatch and service handlers
Documentation
use super::*;
use crate::server::test_transport::TestTransport;
use bacnet_client::client::BACnetClient;
use bacnet_objects::file::FileObject;
use bacnet_services::file::{AtomicReadFileRequest, FileAccessMethod};
use bacnet_types::error::RemoteError;

#[tokio::test]
async fn atomic_read_file_all_builders_validate_before_start_or_dial() {
    let default = AtomicReadFileBudget::default();
    assert_eq!(
        (
            default.max_requested_stream_octets,
            default.max_requested_records,
            default.max_service_ack_bytes
        ),
        (16384, 256, 16384)
    );
    assert_eq!(ServerConfig::default().atomic_read_file_budget, default);
    assert!(format!("{:?}", ServerConfig::default()).contains("atomic_read_file_budget"));
    AtomicReadFileBudget {
        max_requested_stream_octets: usize::MAX,
        max_requested_records: usize::MAX,
        max_service_ack_bytes: usize::MAX,
    }
    .validate()
    .unwrap();
    for budget in [
        AtomicReadFileBudget {
            max_requested_stream_octets: 0,
            ..default
        },
        AtomicReadFileBudget {
            max_requested_records: 0,
            ..default
        },
        AtomicReadFileBudget {
            max_service_ack_bytes: 0,
            ..default
        },
    ] {
        let direct = BACnetServer::start(
            ServerConfig {
                atomic_read_file_budget: budget,
                ..Default::default()
            },
            ObjectDatabase::new(),
            TestTransport::never_start(),
        )
        .await
        .err();
        let generic = BACnetServer::generic_builder()
            .transport(TestTransport::never_start())
            .atomic_read_file_budget(budget)
            .build()
            .await
            .err();
        let bip = BACnetServer::bip_builder()
            .port(0)
            .atomic_read_file_budget(budget)
            .build()
            .await
            .err();
        for error in [direct, generic, bip] {
            assert!(
                matches!(error, Some(Error::Encoding(m)) if m.contains("atomic_read_file_max_"))
            );
        }
        #[cfg(feature = "sc-tls")]
        {
            let result = BACnetServer::sc_builder()
                .hub_url("not-a-websocket-url")
                .tls_config(crate::server::sc_builder::test_tls_config())
                .device_uuid(crate::server::sc_builder::TEST_DEVICE_UUID)
                .atomic_read_file_budget(budget)
                .build()
                .await
                .err();
            assert!(
                matches!(result, Some(Error::Encoding(m)) if m.contains("atomic_read_file_max_"))
            );
        }
    }
}

#[tokio::test]
async fn atomic_read_file_within_budget_segmented_complete_ack() {
    use bacnet_services::file::{AtomicReadFileAck, FileReadAckMethod};
    for record in [false, true] {
        for over in [false, true] {
            let mut file = FileObject::new(1, "file", "binary").unwrap();
            if record {
                file.set_file_access_method(
                    bacnet_types::enums::FileAccessMethod::RECORD_ACCESS.to_raw(),
                );
                file.set_records(vec![vec![], vec![42; 300]]);
            } else {
                file.set_data(vec![42; 300]);
            }
            let mut database = ObjectDatabase::new();
            database.add(Box::new(file)).unwrap();
            let size = if record { 312 } else { 309 };
            let mut server = BACnetServer::bip_builder()
                .interface(Ipv4Addr::LOCALHOST)
                .port(0)
                .database(database)
                .device(
                    bacnet_objects::DeviceIdentity::default().with_segmentation(Segmentation::BOTH),
                )
                .atomic_read_file_budget(AtomicReadFileBudget {
                    max_service_ack_bytes: size - usize::from(over),
                    ..Default::default()
                })
                .build()
                .await
                .unwrap();
            let mut client = BACnetClient::bip_builder()
                .interface(Ipv4Addr::LOCALHOST)
                .port(0)
                .max_apdu_length(50)
                .build()
                .await
                .unwrap();
            let mut request = BytesMut::new();
            AtomicReadFileRequest {
                file_identifier: ObjectIdentifier::new(ObjectType::FILE, 1).unwrap(),
                access: if record {
                    FileAccessMethod::Record {
                        file_start_record: 0,
                        requested_record_count: 2,
                    }
                } else {
                    FileAccessMethod::Stream {
                        file_start_position: 0,
                        requested_octet_count: 300,
                    }
                },
            }
            .encode(&mut request);
            let result = client
                .confirmed_request(
                    server.local_mac(),
                    ConfirmedServiceChoice::ATOMIC_READ_FILE,
                    &request,
                )
                .await;
            client.stop().await.unwrap();
            server.stop().await.unwrap();
            if over {
                assert!(
                    matches!(result, Err(Error::Remote(RemoteError::Abort { reason })) if reason == AbortReason::BUFFER_OVERFLOW)
                );
            } else {
                let bytes = result.unwrap();
                assert_eq!(bytes.len(), size);
                let ack = AtomicReadFileAck::decode(&bytes).unwrap();
                assert!(ack.end_of_file);
                match ack.access {
                    FileReadAckMethod::Stream {
                        file_start_position,
                        file_data,
                    } => {
                        assert_eq!(file_start_position, 0);
                        assert_eq!(file_data, vec![42; 300]);
                    }
                    FileReadAckMethod::Record {
                        file_start_record,
                        returned_record_count,
                        file_record_data,
                    } => {
                        assert_eq!(file_start_record, 0);
                        assert_eq!(returned_record_count, 2);
                        assert_eq!(file_record_data, vec![vec![], vec![42; 300]]);
                    }
                }
            }
        }
    }
}

#[tokio::test]
async fn atomic_read_file_default_stream_raw_count_budget_wire() {
    default_count_wire(false).await;
}

#[tokio::test]
async fn atomic_read_file_default_record_raw_count_budget_wire() {
    default_count_wire(true).await;
}

async fn default_count_wire(record: bool) {
    let mut file = FileObject::new(1, "file", "binary").unwrap();
    if record {
        file.set_file_access_method(bacnet_types::enums::FileAccessMethod::RECORD_ACCESS.to_raw());
    }
    let mut database = ObjectDatabase::new();
    database.add(Box::new(file)).unwrap();
    let mut server = BACnetServer::bip_builder()
        .interface(Ipv4Addr::LOCALHOST)
        .port(0)
        .database(database)
        .build()
        .await
        .unwrap();
    let mut client = BACnetClient::bip_builder()
        .interface(Ipv4Addr::LOCALHOST)
        .port(0)
        .build()
        .await
        .unwrap();
    let mut wire = BytesMut::new();
    AtomicReadFileRequest {
        file_identifier: ObjectIdentifier::new(ObjectType::FILE, 1).unwrap(),
        access: if record {
            FileAccessMethod::Record {
                file_start_record: 0,
                requested_record_count: 257,
            }
        } else {
            FileAccessMethod::Stream {
                file_start_position: 0,
                requested_octet_count: 16385,
            }
        },
    }
    .encode(&mut wire);
    let result = client
        .confirmed_request(
            server.local_mac(),
            ConfirmedServiceChoice::ATOMIC_READ_FILE,
            &wire,
        )
        .await;
    client.stop().await.unwrap();
    server.stop().await.unwrap();
    assert!(
        matches!(result, Err(Error::Remote(RemoteError::Abort { reason })) if reason == AbortReason::OUT_OF_RESOURCES),
        "{result:?}"
    );
}