use crate::protocol::{Telegram, address::GroupAddress};
use async_trait::async_trait;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use tokio::sync::RwLock;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct CallbackHandle(u64);
impl CallbackHandle {
fn new(id: u64) -> Self {
Self(id)
}
#[must_use]
pub fn id(&self) -> u64 {
self.0
}
}
impl std::fmt::Display for CallbackHandle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "CallbackHandle({})", self.0)
}
}
#[derive(Clone, Default)]
pub enum TelegramFilter {
GroupAddresses(Vec<GroupAddress>),
AddressRange(GroupAddress, GroupAddress),
Custom(Arc<dyn Fn(&Telegram) -> bool + Send + Sync>),
#[default]
All,
}
impl std::fmt::Debug for TelegramFilter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TelegramFilter::GroupAddresses(addresses) => {
f.debug_tuple("GroupAddresses").field(addresses).finish()
}
TelegramFilter::AddressRange(start, end) => f
.debug_tuple("AddressRange")
.field(start)
.field(end)
.finish(),
TelegramFilter::Custom(_) => f.debug_tuple("Custom").field(&"<function>").finish(),
TelegramFilter::All => f.debug_tuple("All").finish(),
}
}
}
impl TelegramFilter {
#[must_use]
pub fn matches(&self, telegram: &Telegram) -> bool {
match self {
TelegramFilter::GroupAddresses(addresses) => {
if let crate::protocol::address::Address::Group(group_addr) = telegram.destination {
addresses.contains(&group_addr)
} else {
false
}
}
TelegramFilter::AddressRange(start, end) => {
if let crate::protocol::address::Address::Group(group_addr) = telegram.destination {
group_addr.raw() >= start.raw() && group_addr.raw() <= end.raw()
} else {
false
}
}
TelegramFilter::Custom(filter_fn) => filter_fn(telegram),
TelegramFilter::All => true,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConnectionState {
Disconnected,
Connecting,
Connected,
Disconnecting,
Error,
}
impl std::fmt::Display for ConnectionState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ConnectionState::Disconnected => write!(f, "disconnected"),
ConnectionState::Connecting => write!(f, "connecting"),
ConnectionState::Connected => write!(f, "connected"),
ConnectionState::Disconnecting => write!(f, "disconnecting"),
ConnectionState::Error => write!(f, "error"),
}
}
}
#[async_trait]
pub trait TelegramCallbackFn {
async fn call(&self, telegram: &Telegram);
}
#[async_trait]
pub trait ConnectionCallbackFn {
async fn call(&self, state: ConnectionState);
}
struct SyncTelegramCb<F>(F);
struct SyncConnectionCb<F>(F);
#[async_trait]
impl<F: Fn(&Telegram) + Send + Sync> TelegramCallbackFn for SyncTelegramCb<F> {
async fn call(&self, telegram: &Telegram) {
(self.0)(telegram);
}
}
#[async_trait]
impl<F: Fn(ConnectionState) + Send + Sync> ConnectionCallbackFn for SyncConnectionCb<F> {
async fn call(&self, state: ConnectionState) {
(self.0)(state);
}
}
struct TelegramCallbackEntry {
id: CallbackHandle,
callback: Box<dyn TelegramCallbackFn + Send + Sync>,
filter: TelegramFilter,
include_outgoing: bool,
}
struct ConnectionCallbackEntry {
id: CallbackHandle,
callback: Box<dyn ConnectionCallbackFn + Send + Sync>,
}
pub struct EventHandler {
telegram_callbacks: Arc<RwLock<Vec<TelegramCallbackEntry>>>,
connection_callbacks: Arc<RwLock<Vec<ConnectionCallbackEntry>>>,
next_id: Arc<AtomicU64>,
}
impl EventHandler {
#[must_use]
pub fn new() -> Self {
Self {
telegram_callbacks: Arc::new(RwLock::new(Vec::new())),
connection_callbacks: Arc::new(RwLock::new(Vec::new())),
next_id: Arc::new(AtomicU64::new(1)),
}
}
fn generate_handle(&self) -> CallbackHandle {
let id = self.next_id.fetch_add(1, Ordering::SeqCst);
CallbackHandle::new(id)
}
pub async fn register_telegram_callback<F>(&self, callback: F) -> CallbackHandle
where
F: TelegramCallbackFn + Send + Sync + 'static,
{
self.register_telegram_callback_filtered(callback, TelegramFilter::All, false)
.await
}
pub async fn register_telegram_callback_filtered<F>(
&self,
callback: F,
filter: TelegramFilter,
include_outgoing: bool,
) -> CallbackHandle
where
F: TelegramCallbackFn + Send + Sync + 'static,
{
let handle = self.generate_handle();
let entry = TelegramCallbackEntry {
id: handle,
callback: Box::new(callback),
filter,
include_outgoing,
};
let mut callbacks = self.telegram_callbacks.write().await;
callbacks.push(entry);
handle
}
pub async fn register_telegram_callback_sync<F>(&self, callback: F) -> CallbackHandle
where
F: Fn(&Telegram) + Send + Sync + 'static,
{
self.register_telegram_callback_filtered(
SyncTelegramCb(callback),
TelegramFilter::All,
false,
)
.await
}
pub async fn register_telegram_callback_sync_filtered<F>(
&self,
callback: F,
filter: TelegramFilter,
include_outgoing: bool,
) -> CallbackHandle
where
F: Fn(&Telegram) + Send + Sync + 'static,
{
self.register_telegram_callback_filtered(SyncTelegramCb(callback), filter, include_outgoing)
.await
}
pub async fn register_connection_callback_sync<F>(&self, callback: F) -> CallbackHandle
where
F: Fn(ConnectionState) + Send + Sync + 'static,
{
self.register_connection_callback(SyncConnectionCb(callback))
.await
}
pub async fn register_connection_callback<F>(&self, callback: F) -> CallbackHandle
where
F: ConnectionCallbackFn + Send + Sync + 'static,
{
let handle = self.generate_handle();
let entry = ConnectionCallbackEntry {
id: handle,
callback: Box::new(callback),
};
let mut callbacks = self.connection_callbacks.write().await;
callbacks.push(entry);
handle
}
pub async fn unregister_telegram_callback(&self, handle: CallbackHandle) -> bool {
let mut callbacks = self.telegram_callbacks.write().await;
if let Some(pos) = callbacks.iter().position(|entry| entry.id == handle) {
callbacks.remove(pos);
true
} else {
false
}
}
pub async fn unregister_connection_callback(&self, handle: CallbackHandle) -> bool {
let mut callbacks = self.connection_callbacks.write().await;
if let Some(pos) = callbacks.iter().position(|entry| entry.id == handle) {
callbacks.remove(pos);
true
} else {
false
}
}
pub async fn unregister_callback(&self, handle: CallbackHandle) -> bool {
let telegram_removed = self.unregister_telegram_callback(handle).await;
let connection_removed = self.unregister_connection_callback(handle).await;
telegram_removed || connection_removed
}
pub async fn register_telegram_callback_boxed(
&self,
callback: Box<dyn TelegramCallbackFn + Send + Sync>,
filter: TelegramFilter,
include_outgoing: bool,
) -> CallbackHandle {
let handle = self.generate_handle();
let entry = TelegramCallbackEntry {
id: handle,
callback,
filter,
include_outgoing,
};
let mut callbacks = self.telegram_callbacks.write().await;
callbacks.push(entry);
handle
}
pub async fn register_connection_callback_boxed(
&self,
callback: Box<dyn ConnectionCallbackFn + Send + Sync>,
) -> CallbackHandle {
let handle = self.generate_handle();
let entry = ConnectionCallbackEntry {
id: handle,
callback,
};
let mut callbacks = self.connection_callbacks.write().await;
callbacks.push(entry);
handle
}
pub async fn notify_telegram_received(&self, telegram: &Telegram) {
let callbacks = self.telegram_callbacks.read().await;
for entry in callbacks.iter() {
if !entry.include_outgoing
&& telegram.direction == crate::protocol::telegram::Direction::Outgoing
{
continue;
}
if !entry.filter.matches(telegram) {
continue;
}
match tokio::time::timeout(
std::time::Duration::from_millis(50), entry.callback.call(telegram),
)
.await
{
Ok(()) => {
log::debug!("Telegram callback {} executed successfully", entry.id);
}
Err(_) => {
log::warn!(
"Telegram callback {} timed out after 50ms for telegram from {} to {}",
entry.id,
telegram.source,
telegram.destination
);
}
}
}
}
pub async fn notify_connection_state_changed(&self, state: ConnectionState) {
let callbacks = self.connection_callbacks.read().await;
for entry in callbacks.iter() {
if (tokio::time::timeout(
std::time::Duration::from_secs(5),
entry.callback.call(state),
)
.await)
.is_err()
{
log::warn!("Connection callback {} timed out", entry.id);
}
}
}
pub async fn telegram_callback_count(&self) -> usize {
self.telegram_callbacks.read().await.len()
}
pub async fn connection_callback_count(&self) -> usize {
self.connection_callbacks.read().await.len()
}
pub async fn total_callback_count(&self) -> usize {
let telegram_count = self.telegram_callback_count().await;
let connection_count = self.connection_callback_count().await;
telegram_count + connection_count
}
pub async fn clear_all_callbacks(&self) {
let mut telegram_callbacks = self.telegram_callbacks.write().await;
let mut connection_callbacks = self.connection_callbacks.write().await;
telegram_callbacks.clear();
connection_callbacks.clear();
}
}
impl Default for EventHandler {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
use proptest::test_runner::Config as ProptestConfig;
use std::collections::HashSet;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use tokio::time::{Duration, sleep};
struct AsyncWrapper {
counter: Arc<AtomicUsize>,
}
#[async_trait]
impl ConnectionCallbackFn for AsyncWrapper {
async fn call(&self, _state: ConnectionState) {
sleep(Duration::from_millis(10)).await;
self.counter.fetch_add(1, Ordering::SeqCst);
}
}
struct OrderCallback {
order: Arc<RwLock<Vec<i32>>>,
value: i32,
}
#[async_trait]
impl ConnectionCallbackFn for OrderCallback {
async fn call(&self, _state: ConnectionState) {
let mut order = self.order.write().await;
order.push(self.value);
}
}
struct SlowCallback {
counter: Arc<AtomicUsize>,
}
#[async_trait]
impl TelegramCallbackFn for SlowCallback {
async fn call(&self, _telegram: &Telegram) {
self.counter.fetch_add(1, Ordering::SeqCst);
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
}
}
struct AsyncCallback {
counter: Arc<AtomicUsize>,
order_tracker: Arc<tokio::sync::RwLock<Vec<String>>>,
id: usize,
}
#[async_trait]
impl ConnectionCallbackFn for AsyncCallback {
async fn call(&self, _state: ConnectionState) {
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
self.counter.fetch_add(1, Ordering::SeqCst);
self.order_tracker
.write()
.await
.push(format!("async_{}", self.id));
}
}
struct AsyncTelegramCallback {
counter: Arc<AtomicUsize>,
}
#[async_trait]
impl TelegramCallbackFn for AsyncTelegramCallback {
async fn call(&self, _telegram: &Telegram) {
tokio::time::sleep(tokio::time::Duration::from_millis(5)).await;
self.counter.fetch_add(1, Ordering::SeqCst);
}
}
struct SyncTelegramCallback {
counter: Arc<AtomicUsize>,
}
#[async_trait]
impl TelegramCallbackFn for SyncTelegramCallback {
async fn call(&self, _telegram: &Telegram) {
self.counter.fetch_add(1, Ordering::SeqCst);
}
}
struct SyncConnectionCallback {
counter: Arc<AtomicUsize>,
}
#[async_trait]
impl ConnectionCallbackFn for SyncConnectionCallback {
async fn call(&self, _state: crate::application::callbacks::ConnectionState) {
self.counter.fetch_add(1, Ordering::SeqCst);
}
}
#[tokio::test]
async fn test_callback_handle_uniqueness() {
let handler = EventHandler::new();
let telegram_handle_a = handler.register_telegram_callback_sync(|_| {}).await;
let telegram_handle_b = handler.register_telegram_callback_sync(|_| {}).await;
let connection_handle = handler.register_connection_callback_sync(|_| {}).await;
assert_ne!(telegram_handle_a, telegram_handle_b);
assert_ne!(telegram_handle_a, connection_handle);
assert_ne!(telegram_handle_b, connection_handle);
}
#[test]
fn property_callback_handle_uniqueness() {
let rt = tokio::runtime::Runtime::new().unwrap();
proptest!(ProptestConfig::with_cases(20), |(
// Generate number of callbacks to register for each type
telegram_count in 1usize..10usize,
connection_count in 1usize..10usize,
)| {
rt.block_on(async {
let handler = EventHandler::new();
let mut all_handles = HashSet::new();
for _ in 0..telegram_count {
let handle = handler.register_telegram_callback_sync(|_| {}).await;
assert!(all_handles.insert(handle), "Duplicate handle found: {handle}");
}
for _ in 0..connection_count {
let handle = handler.register_connection_callback_sync(|_| {}).await;
assert!(all_handles.insert(handle), "Duplicate handle found: {handle}");
}
let expected_total = telegram_count + connection_count;
assert_eq!(all_handles.len(), expected_total);
assert_eq!(handler.total_callback_count().await, expected_total);
assert_eq!(handler.telegram_callback_count().await, telegram_count);
assert_eq!(handler.connection_callback_count().await, connection_count);
});
});
}
#[tokio::test]
async fn test_callback_registration_and_storage() {
let handler = EventHandler::new();
assert_eq!(handler.total_callback_count().await, 0);
let _handle1 = handler.register_telegram_callback_sync(|_| {}).await;
assert_eq!(handler.telegram_callback_count().await, 1);
assert_eq!(handler.total_callback_count().await, 1);
let _handle2 = handler.register_connection_callback_sync(|_| {}).await;
assert_eq!(handler.connection_callback_count().await, 1);
assert_eq!(handler.total_callback_count().await, 2);
}
#[tokio::test]
async fn test_callback_unregistration() {
let handler = EventHandler::new();
let telegram_handle = handler.register_telegram_callback_sync(|_| {}).await;
let connection_handle = handler.register_connection_callback_sync(|_| {}).await;
assert_eq!(handler.total_callback_count().await, 2);
assert!(handler.unregister_telegram_callback(telegram_handle).await);
assert_eq!(handler.telegram_callback_count().await, 0);
assert_eq!(handler.total_callback_count().await, 1);
assert!(
handler
.unregister_connection_callback(connection_handle)
.await
);
assert_eq!(handler.connection_callback_count().await, 0);
assert_eq!(handler.total_callback_count().await, 0);
}
#[tokio::test]
async fn test_invalid_handle_unregistration() {
let handler = EventHandler::new();
let fake_handle = CallbackHandle::new(999);
assert!(!handler.unregister_telegram_callback(fake_handle).await);
assert!(!handler.unregister_connection_callback(fake_handle).await);
assert!(!handler.unregister_callback(fake_handle).await);
}
#[tokio::test]
async fn test_sync_callback_execution() {
let handler = EventHandler::new();
let counter = Arc::new(AtomicUsize::new(0));
let counter_clone = counter.clone();
let _handle = handler
.register_connection_callback_sync(move |_state| {
counter_clone.fetch_add(1, Ordering::SeqCst);
})
.await;
handler
.notify_connection_state_changed(ConnectionState::Connected)
.await;
assert_eq!(counter.load(Ordering::SeqCst), 1);
handler
.notify_connection_state_changed(ConnectionState::Disconnected)
.await;
assert_eq!(counter.load(Ordering::SeqCst), 2);
}
#[tokio::test]
async fn test_async_callback_execution() {
let handler = EventHandler::new();
let counter = Arc::new(AtomicUsize::new(0));
let _handle = handler
.register_connection_callback(AsyncWrapper {
counter: counter.clone(),
})
.await;
handler
.notify_connection_state_changed(ConnectionState::Connected)
.await;
assert_eq!(counter.load(Ordering::SeqCst), 1);
}
#[tokio::test]
async fn test_multiple_callbacks_execution_order() {
let handler = EventHandler::new();
let execution_order = Arc::new(RwLock::new(Vec::new()));
let _handle1 = handler
.register_connection_callback(OrderCallback {
order: execution_order.clone(),
value: 1,
})
.await;
let _handle2 = handler
.register_connection_callback(OrderCallback {
order: execution_order.clone(),
value: 2,
})
.await;
let _handle3 = handler
.register_connection_callback(OrderCallback {
order: execution_order.clone(),
value: 3,
})
.await;
handler
.notify_connection_state_changed(ConnectionState::Connected)
.await;
let order = execution_order.read().await;
assert_eq!(*order, vec![1, 2, 3]);
}
#[tokio::test]
async fn test_clear_all_callbacks() {
let handler = EventHandler::new();
let _handle1 = handler.register_telegram_callback_sync(|_| {}).await;
let _handle2 = handler.register_connection_callback_sync(|_| {}).await;
assert_eq!(handler.total_callback_count().await, 2);
handler.clear_all_callbacks().await;
assert_eq!(handler.total_callback_count().await, 0);
}
#[test]
fn property_telegram_callback_invocation() {
use crate::protocol::{
address::{Address, GroupAddress, IndividualAddress},
telegram::{Direction, Telegram, TelegramType},
};
use std::collections::HashMap;
use std::sync::atomic::{AtomicUsize, Ordering};
let rt = tokio::runtime::Runtime::new().unwrap();
proptest!(ProptestConfig::with_cases(20), |(
// Generate telegram properties
source_raw in 0u16..=0xFFFF,
dest_main in 0u8..=GroupAddress::MAX_MAIN,
dest_middle in 0u8..=GroupAddress::MAX_MIDDLE,
dest_sub in 0u8..=255u8,
payload in prop::collection::vec(0u8..=255u8, 0..5),
is_outgoing in prop::bool::ANY,
callback_configs in prop::collection::vec(
(prop::bool::ANY, prop::bool::ANY), 1..5usize
),
)| {
let _ = rt.block_on(async {
let handler = EventHandler::new();
let mut callback_counters = HashMap::new();
let source = IndividualAddress::from_raw(source_raw);
let destination = Address::Group(GroupAddress::from_parts(dest_main, dest_middle, dest_sub).unwrap());
let direction = if is_outgoing { Direction::Outgoing } else { Direction::Incoming };
let telegram = Telegram {
source,
destination,
payload,
priority: crate::protocol::telegram::Priority::Normal,
direction,
telegram_type: TelegramType::GroupValueWrite,
gateway_id: None,
timestamp: std::time::SystemTime::now(),
};
for (i, (has_filter, include_outgoing)) in callback_configs.iter().enumerate() {
let counter = Arc::new(AtomicUsize::new(0));
callback_counters.insert(i, counter.clone());
let filter = if *has_filter {
if let Address::Group(group_addr) = telegram.destination {
TelegramFilter::GroupAddresses(vec![group_addr])
} else {
TelegramFilter::All
}
} else {
TelegramFilter::All
};
let counter_clone = counter.clone();
let _handle = handler.register_telegram_callback_sync_filtered(
move |_telegram| {
counter_clone.fetch_add(1, Ordering::SeqCst);
},
filter,
*include_outgoing
).await;
}
handler.notify_telegram_received(&telegram).await;
for (i, (has_filter, include_outgoing)) in callback_configs.iter().enumerate() {
let counter = callback_counters.get(&i).unwrap();
let count = counter.load(Ordering::SeqCst);
let should_invoke = if is_outgoing && !include_outgoing {
false
} else if *has_filter {
true
} else {
true
};
if should_invoke {
prop_assert_eq!(count, 1, "Callback {} should have been invoked exactly once", i);
} else {
prop_assert_eq!(count, 0, "Callback {} should not have been invoked", i);
}
}
Ok(())
});
});
}
#[test]
fn property_telegram_filtering() {
use crate::protocol::{
address::{Address, GroupAddress, IndividualAddress},
telegram::{Direction, Telegram, TelegramType},
};
use std::sync::atomic::{AtomicUsize, Ordering};
let rt = tokio::runtime::Runtime::new().unwrap();
proptest!(ProptestConfig::with_cases(20), |(
// Generate telegram properties
source_raw in 0u16..=0xFFFF,
dest_main in 0u8..=GroupAddress::MAX_MAIN,
dest_middle in 0u8..=GroupAddress::MAX_MIDDLE,
dest_sub in 0u8..=255u8,
payload in prop::collection::vec(0u8..=255u8, 0..5),
filter_addresses in prop::collection::vec(
(0u8..=GroupAddress::MAX_MAIN, 0u8..=GroupAddress::MAX_MIDDLE, 0u8..=GroupAddress::MAX_SUB), 1..3usize
),
)| {
let _ = rt.block_on(async {
let handler = EventHandler::new();
let source = IndividualAddress::from_raw(source_raw);
let telegram_addr = GroupAddress::from_parts(dest_main, dest_middle, dest_sub).unwrap();
let destination = Address::Group(telegram_addr);
let telegram = Telegram {
source,
destination,
payload,
priority: crate::protocol::telegram::Priority::Normal,
direction: Direction::Incoming,
telegram_type: TelegramType::GroupValueWrite,
gateway_id: None,
timestamp: std::time::SystemTime::now(),
};
let mut callback_counters = Vec::new();
let mut expected_invocations = Vec::new();
for (filter_main, filter_middle, filter_sub) in &filter_addresses {
let counter = Arc::new(AtomicUsize::new(0));
callback_counters.push(counter.clone());
let filter_addr = GroupAddress::from_parts(*filter_main, *filter_middle, *filter_sub).unwrap();
let filter = TelegramFilter::GroupAddresses(vec![filter_addr]);
let should_match = filter_addr == telegram_addr;
expected_invocations.push(should_match);
let counter_clone = counter.clone();
let _handle = handler.register_telegram_callback_sync_filtered(
move |_telegram| {
counter_clone.fetch_add(1, Ordering::SeqCst);
},
filter,
false ).await;
}
let all_counter = Arc::new(AtomicUsize::new(0));
let all_counter_clone = all_counter.clone();
let _all_handle = handler.register_telegram_callback_sync_filtered(
move |_telegram| {
all_counter_clone.fetch_add(1, Ordering::SeqCst);
},
TelegramFilter::All,
false
).await;
handler.notify_telegram_received(&telegram).await;
for (i, expected) in expected_invocations.iter().enumerate() {
let counter = &callback_counters[i];
let count = counter.load(Ordering::SeqCst);
if *expected {
prop_assert_eq!(count, 1, "Callback {} with matching filter should have been invoked", i);
} else {
prop_assert_eq!(count, 0, "Callback {} with non-matching filter should not have been invoked", i);
}
}
prop_assert_eq!(all_counter.load(Ordering::SeqCst), 1, "Callback with All filter should always be invoked");
Ok(())
});
});
}
#[test]
fn property_callback_ordering() {
use crate::protocol::{
address::{Address, GroupAddress, IndividualAddress},
telegram::{Direction, Telegram, TelegramType},
};
use std::sync::Arc;
use tokio::sync::RwLock;
let rt = tokio::runtime::Runtime::new().unwrap();
proptest!(ProptestConfig::with_cases(20), |(
// Generate telegram properties
source_raw in 0u16..=0xFFFF,
dest_main in 0u8..=GroupAddress::MAX_MAIN,
dest_middle in 0u8..=GroupAddress::MAX_MIDDLE,
dest_sub in 0u8..=255u8,
payload in prop::collection::vec(0u8..=255u8, 0..5),
callback_count in 2usize..5usize,
)| {
let _ = rt.block_on(async {
let handler = EventHandler::new();
let execution_order = Arc::new(RwLock::new(Vec::new()));
let source = IndividualAddress::from_raw(source_raw);
let destination = Address::Group(GroupAddress::from_parts(dest_main, dest_middle, dest_sub).unwrap());
let telegram = Telegram {
source,
destination,
payload,
priority: crate::protocol::telegram::Priority::Normal,
direction: Direction::Incoming,
telegram_type: TelegramType::GroupValueWrite,
gateway_id: None,
timestamp: std::time::SystemTime::now(),
};
for i in 0..callback_count {
let order_clone = execution_order.clone();
let _handle = handler.register_telegram_callback_sync(move |_telegram| {
let order_clone = order_clone.clone();
tokio::spawn(async move {
let mut order = order_clone.write().await;
order.push(i);
});
}).await;
}
handler.notify_telegram_received(&telegram).await;
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
let order = execution_order.read().await;
prop_assert_eq!(order.len(), callback_count, "All callbacks should have been executed");
for (i, &executed_index) in order.iter().enumerate() {
prop_assert_eq!(executed_index, i, "Callback {} should have been executed in position {}", executed_index, i);
}
Ok(())
});
});
}
#[test]
fn property_error_isolation() {
use crate::protocol::{
address::{Address, GroupAddress, IndividualAddress},
telegram::{Direction, Telegram, TelegramType},
};
use std::sync::atomic::{AtomicUsize, Ordering};
let rt = tokio::runtime::Runtime::new().unwrap();
proptest!(ProptestConfig::with_cases(20), |(
// Generate telegram properties
source_raw in 0u16..=0xFFFF,
dest_main in 0u8..=GroupAddress::MAX_MAIN,
dest_middle in 0u8..=GroupAddress::MAX_MIDDLE,
dest_sub in 0u8..=255u8,
payload in prop::collection::vec(0u8..=255u8, 0..5),
callback_count in 3usize..5usize,
slow_callback_index in 0usize..2usize, )| {
let _ = rt.block_on(async {
let handler = EventHandler::new();
let success_counter = Arc::new(AtomicUsize::new(0));
let slow_callback_counter = Arc::new(AtomicUsize::new(0));
let source = IndividualAddress::from_raw(source_raw);
let destination = Address::Group(GroupAddress::from_parts(dest_main, dest_middle, dest_sub).unwrap());
let telegram = Telegram {
source,
destination,
payload,
priority: crate::protocol::telegram::Priority::Normal,
direction: Direction::Incoming,
telegram_type: TelegramType::GroupValueWrite,
gateway_id: None,
timestamp: std::time::SystemTime::now(),
};
for i in 0..callback_count {
if i == slow_callback_index {
let slow_counter = slow_callback_counter.clone();
let _handle = handler.register_telegram_callback_filtered(
SlowCallback { counter: slow_counter },
TelegramFilter::All,
false
).await;
} else {
let counter_clone = success_counter.clone();
let _handle = handler.register_telegram_callback_sync(move |_telegram| {
counter_clone.fetch_add(1, Ordering::SeqCst);
}).await;
}
}
handler.notify_telegram_received(&telegram).await;
let fast_callback_count = callback_count - 1; let success_calls = success_counter.load(Ordering::SeqCst);
prop_assert_eq!(success_calls, fast_callback_count, "All fast callbacks should have been executed");
let slow_calls = slow_callback_counter.load(Ordering::SeqCst);
prop_assert_eq!(slow_calls, 1, "The slow callback should have been attempted");
Ok(())
});
});
}
#[test]
fn property_connection_state_callback_invocation() {
use std::collections::HashMap;
use std::sync::atomic::{AtomicUsize, Ordering};
let rt = tokio::runtime::Runtime::new().unwrap();
proptest!(ProptestConfig::with_cases(20), |(
// Generate connection state sequences
state_sequence in prop::collection::vec(
prop::sample::select(vec![
ConnectionState::Disconnected,
ConnectionState::Connecting,
ConnectionState::Connected,
ConnectionState::Disconnecting,
ConnectionState::Error,
]),
1..5usize
),
callback_count in 1usize..5usize,
)| {
let _ = rt.block_on(async {
let handler = EventHandler::new();
let mut callback_counters = HashMap::new();
let mut received_states = HashMap::new();
for i in 0..callback_count {
let counter = Arc::new(AtomicUsize::new(0));
let states = Arc::new(tokio::sync::RwLock::new(Vec::new()));
callback_counters.insert(i, counter.clone());
received_states.insert(i, states.clone());
let counter_clone = counter.clone();
let states_clone = states.clone();
let _handle = handler.register_connection_callback_sync(move |state| {
counter_clone.fetch_add(1, Ordering::SeqCst);
let states_clone = states_clone.clone();
tokio::spawn(async move {
states_clone.write().await.push(state);
});
}).await;
}
for state in &state_sequence {
handler.notify_connection_state_changed(*state).await;
}
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
for i in 0..callback_count {
let counter = callback_counters.get(&i).unwrap();
let count = counter.load(Ordering::SeqCst);
prop_assert_eq!(count, state_sequence.len(),
"Connection callback {} should have been invoked {} times", i, state_sequence.len());
let states = received_states.get(&i).unwrap();
let received = states.read().await;
prop_assert_eq!(received.len(), state_sequence.len(),
"Callback {} should have received {} states", i, state_sequence.len());
for (j, expected_state) in state_sequence.iter().enumerate() {
prop_assert_eq!(received[j], *expected_state,
"Callback {} should have received state {:?} at position {}", i, expected_state, j);
}
}
Ok(())
});
});
}
#[test]
fn property_callback_unregistration() {
use crate::protocol::{
address::{Address, GroupAddress, IndividualAddress},
telegram::{Direction, Telegram, TelegramType},
};
use std::sync::atomic::{AtomicUsize, Ordering};
let rt = tokio::runtime::Runtime::new().unwrap();
proptest!(ProptestConfig::with_cases(20), |(
// Generate telegram properties
source_raw in 0u16..=0xFFFF,
dest_main in 0u8..=GroupAddress::MAX_MAIN,
dest_middle in 0u8..=GroupAddress::MAX_MIDDLE,
dest_sub in 0u8..=255u8,
payload in prop::collection::vec(0u8..=255u8, 0..5),
telegram_callback_count in 1usize..3usize,
connection_callback_count in 1usize..3usize,
)| {
let _ = rt.block_on(async {
let handler = EventHandler::new();
let telegram_counters = Arc::new(tokio::sync::RwLock::new(Vec::new()));
let connection_counters = Arc::new(tokio::sync::RwLock::new(Vec::new()));
let mut telegram_handles = Vec::new();
let mut connection_handles = Vec::new();
for _i in 0..telegram_callback_count {
let counter = Arc::new(AtomicUsize::new(0));
telegram_counters.write().await.push(counter.clone());
let counter_clone = counter.clone();
let handle = handler.register_telegram_callback_sync(move |_telegram| {
counter_clone.fetch_add(1, Ordering::SeqCst);
}).await;
telegram_handles.push(handle);
}
for _i in 0..connection_callback_count {
let counter = Arc::new(AtomicUsize::new(0));
connection_counters.write().await.push(counter.clone());
let counter_clone = counter.clone();
let handle = handler.register_connection_callback_sync(move |_state| {
counter_clone.fetch_add(1, Ordering::SeqCst);
}).await;
connection_handles.push(handle);
}
prop_assert_eq!(handler.telegram_callback_count().await, telegram_callback_count);
prop_assert_eq!(handler.connection_callback_count().await, connection_callback_count);
let source = IndividualAddress::from_raw(source_raw);
let destination = Address::Group(GroupAddress::from_parts(dest_main, dest_middle, dest_sub).unwrap());
let telegram = Telegram {
source,
destination,
payload,
priority: crate::protocol::telegram::Priority::Normal,
direction: Direction::Incoming,
telegram_type: TelegramType::GroupValueWrite,
gateway_id: None,
timestamp: std::time::SystemTime::now(),
};
handler.notify_telegram_received(&telegram).await;
handler.notify_connection_state_changed(ConnectionState::Connected).await;
let telegram_counters_read = telegram_counters.read().await;
for (i, counter) in telegram_counters_read.iter().enumerate() {
prop_assert_eq!(counter.load(Ordering::SeqCst), 1, "Telegram callback {} should have been invoked", i);
}
drop(telegram_counters_read);
let connection_counters_read = connection_counters.read().await;
for (i, counter) in connection_counters_read.iter().enumerate() {
prop_assert_eq!(counter.load(Ordering::SeqCst), 1, "Connection callback {} should have been invoked", i);
}
drop(connection_counters_read);
let telegram_unregister_count = std::cmp::max(1, telegram_callback_count / 2);
let connection_unregister_count = std::cmp::max(1, connection_callback_count / 2);
for (i, &handle) in telegram_handles.iter().enumerate().take(telegram_unregister_count) {
prop_assert!(handler.unregister_telegram_callback(handle).await, "Should successfully unregister telegram callback {}", i);
}
for (i, &handle) in connection_handles.iter().enumerate().take(connection_unregister_count) {
prop_assert!(handler.unregister_connection_callback(handle).await, "Should successfully unregister connection callback {}", i);
}
prop_assert_eq!(handler.telegram_callback_count().await, telegram_callback_count - telegram_unregister_count);
prop_assert_eq!(handler.connection_callback_count().await, connection_callback_count - connection_unregister_count);
handler.notify_telegram_received(&telegram).await;
handler.notify_connection_state_changed(ConnectionState::Disconnected).await;
let telegram_counters_read = telegram_counters.read().await;
for i in 0..telegram_callback_count {
let counter = &telegram_counters_read[i];
let expected_count = if i < telegram_unregister_count { 1 } else { 2 };
prop_assert_eq!(counter.load(Ordering::SeqCst), expected_count,
"Telegram callback {} should have been invoked {} times", i, expected_count);
}
drop(telegram_counters_read);
let connection_counters_read = connection_counters.read().await;
for i in 0..connection_callback_count {
let counter = &connection_counters_read[i];
let expected_count = if i < connection_unregister_count { 1 } else { 2 };
prop_assert_eq!(counter.load(Ordering::SeqCst), expected_count,
"Connection callback {} should have been invoked {} times", i, expected_count);
}
Ok(())
});
});
}
#[test]
fn property_thread_safety() {
use std::sync::atomic::{AtomicUsize, Ordering};
let rt = tokio::runtime::Runtime::new().unwrap();
proptest!(ProptestConfig::with_cases(10), |( // Reduced cases for performance
// Generate concurrent operation parameters
thread_count in 2usize..5usize,
operations_per_thread in 2usize..5usize,
notification_count in 1usize..3usize,
)| {
let _ = rt.block_on(async {
let handler = Arc::new(EventHandler::new());
let execution_counter = Arc::new(AtomicUsize::new(0));
let registration_counter = Arc::new(AtomicUsize::new(0));
let handles_storage = Arc::new(tokio::sync::RwLock::new(Vec::new()));
let mut thread_handles = Vec::new();
for _thread_id in 0..thread_count {
let handler_clone = handler.clone();
let execution_counter_clone = execution_counter.clone();
let registration_counter_clone = registration_counter.clone();
let handles_storage_clone = handles_storage.clone();
let thread_handle = tokio::spawn(async move {
let mut local_handles = Vec::new();
for _op_id in 0..operations_per_thread {
let exec_counter_telegram = execution_counter_clone.clone();
let exec_counter_connection = execution_counter_clone.clone();
let reg_counter = registration_counter_clone.clone();
let telegram_handle = handler_clone.register_telegram_callback_sync(move |_telegram| {
exec_counter_telegram.fetch_add(1, Ordering::SeqCst);
}).await;
local_handles.push(telegram_handle);
let connection_handle = handler_clone.register_connection_callback_sync(move |_state| {
exec_counter_connection.fetch_add(1, Ordering::SeqCst);
}).await;
local_handles.push(connection_handle);
reg_counter.fetch_add(2, Ordering::SeqCst);
tokio::time::sleep(tokio::time::Duration::from_millis(1)).await;
}
handles_storage_clone.write().await.extend(local_handles);
});
thread_handles.push(thread_handle);
}
for handle in thread_handles {
handle.await.unwrap();
}
let expected_registrations = thread_count * operations_per_thread * 2; let actual_registrations = registration_counter.load(Ordering::SeqCst);
prop_assert_eq!(actual_registrations, expected_registrations,
"All registrations should complete successfully");
let total_callbacks = handler.total_callback_count().await;
prop_assert_eq!(total_callbacks, expected_registrations,
"Handler should have correct total callback count");
let expected_per_type = thread_count * operations_per_thread;
prop_assert_eq!(handler.telegram_callback_count().await, expected_per_type,
"Should have correct telegram callback count");
prop_assert_eq!(handler.connection_callback_count().await, expected_per_type,
"Should have correct connection callback count");
let mut notification_handles = Vec::new();
for _ in 0..notification_count {
let handler_clone = handler.clone();
let notification_handle = tokio::spawn(async move {
use crate::protocol::{address::{Address, GroupAddress, IndividualAddress}, telegram::{Telegram, Direction, TelegramType}};
let telegram = Telegram {
source: IndividualAddress::from_raw(0x1234),
destination: Address::Group(GroupAddress::new(0, 1, 1)),
payload: vec![0x01],
priority: crate::protocol::telegram::Priority::Normal,
direction: Direction::Incoming,
telegram_type: TelegramType::GroupValueWrite,
gateway_id: None,
timestamp: std::time::SystemTime::now(),
};
let telegram_notify = handler_clone.notify_telegram_received(&telegram);
let connection_notify = handler_clone.notify_connection_state_changed(crate::application::callbacks::ConnectionState::Connected);
tokio::join!(telegram_notify, connection_notify);
});
notification_handles.push(notification_handle);
}
for handle in notification_handles {
handle.await.unwrap();
}
let expected_executions = notification_count * expected_per_type * 2; let actual_executions = execution_counter.load(Ordering::SeqCst);
prop_assert_eq!(actual_executions, expected_executions,
"All callbacks should have been executed the correct number of times");
let stored_handles = handles_storage.read().await;
let unregister_count = std::cmp::min(stored_handles.len() / 2, 10);
let mut unregister_handles = Vec::new();
for i in 0..unregister_count {
let handler_clone = handler.clone();
let handle_to_unregister = stored_handles[i];
let unregister_handle = tokio::spawn(async move {
handler_clone.unregister_callback(handle_to_unregister).await
});
unregister_handles.push(unregister_handle);
}
let mut successful_unregistrations = 0;
for handle in unregister_handles {
if handle.await.unwrap() {
successful_unregistrations += 1;
}
}
prop_assert_eq!(successful_unregistrations, unregister_count,
"All unregistration attempts should succeed");
let final_callback_count = handler.total_callback_count().await;
let expected_final_count = expected_registrations - unregister_count;
prop_assert_eq!(final_callback_count, expected_final_count,
"Final callback count should be correct after unregistrations");
Ok(())
});
});
}
#[test]
#[allow(clippy::similar_names)]
fn property_sync_async_callback_support() {
use crate::protocol::{
address::{Address, GroupAddress, IndividualAddress},
telegram::{Direction, Telegram, TelegramType},
};
use std::sync::atomic::{AtomicUsize, Ordering};
let rt = tokio::runtime::Runtime::new().unwrap();
proptest!(ProptestConfig::with_cases(20), |(
// Generate callback configurations
sync_callback_count in 1usize..4usize,
async_callback_count in 1usize..4usize,
notification_count in 1usize..3usize,
)| {
let _ = rt.block_on(async {
let handler = EventHandler::new();
let sync_execution_counter = Arc::new(AtomicUsize::new(0));
let async_execution_counter = Arc::new(AtomicUsize::new(0));
let execution_order = Arc::new(tokio::sync::RwLock::new(Vec::new()));
let mut callback_handles = Vec::new();
for i in 0..sync_callback_count {
let sync_counter = sync_execution_counter.clone();
let order_tracker = execution_order.clone();
let handle = handler.register_connection_callback_sync(move |_state| {
sync_counter.fetch_add(1, Ordering::SeqCst);
let order_tracker = order_tracker.clone();
tokio::spawn(async move {
order_tracker.write().await.push(format!("sync_{i}"));
});
}).await;
callback_handles.push(handle);
}
for i in 0..async_callback_count {
let async_counter = async_execution_counter.clone();
let order_tracker = execution_order.clone();
let handle = handler.register_connection_callback(AsyncCallback {
counter: async_counter.clone(),
order_tracker: order_tracker.clone(),
id: i,
}).await;
callback_handles.push(handle);
}
let total_expected = sync_callback_count + async_callback_count;
prop_assert_eq!(handler.connection_callback_count().await, total_expected,
"Should have registered all callbacks");
for _ in 0..notification_count {
handler.notify_connection_state_changed(ConnectionState::Connected).await;
}
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
let sync_executions = sync_execution_counter.load(Ordering::SeqCst);
let async_executions = async_execution_counter.load(Ordering::SeqCst);
let expected_sync_executions = sync_callback_count * notification_count;
let expected_async_executions = async_callback_count * notification_count;
prop_assert_eq!(sync_executions, expected_sync_executions,
"Sync callbacks should be executed correct number of times");
prop_assert_eq!(async_executions, expected_async_executions,
"Async callbacks should be executed correct number of times");
let order = execution_order.read().await;
let total_expected_executions = total_expected * notification_count;
prop_assert_eq!(order.len(), total_expected_executions,
"Should have correct total number of executions");
for notification_idx in 0..notification_count {
let start_idx = notification_idx * total_expected;
let end_idx = start_idx + total_expected;
if end_idx <= order.len() {
let notification_order = &order[start_idx..end_idx];
for i in 0..sync_callback_count {
if i < notification_order.len() {
let expected = format!("sync_{i}");
prop_assert_eq!(¬ification_order[i], &expected,
"Sync callback {} should execute in correct order for notification {}", i, notification_idx);
}
}
for i in 0..async_callback_count {
let order_idx = sync_callback_count + i;
if order_idx < notification_order.len() {
let expected = format!("async_{i}");
prop_assert_eq!(¬ification_order[order_idx], &expected,
"Async callback {} should execute in correct order for notification {}", i, notification_idx);
}
}
}
}
let telegram_sync_counter = Arc::new(AtomicUsize::new(0));
let telegram_async_counter = Arc::new(AtomicUsize::new(0));
let sync_counter_clone = telegram_sync_counter.clone();
let _sync_handle = handler.register_telegram_callback_sync(move |_telegram| {
sync_counter_clone.fetch_add(1, Ordering::SeqCst);
}).await;
let _async_handle = handler.register_telegram_callback_filtered(
AsyncTelegramCallback { counter: telegram_async_counter.clone() },
TelegramFilter::All,
false
).await;
let telegram = Telegram {
source: IndividualAddress::from_raw(0x1234),
destination: Address::Group(GroupAddress::new(0, 1, 1)),
payload: vec![0x01],
priority: crate::protocol::telegram::Priority::Normal,
direction: Direction::Incoming,
telegram_type: TelegramType::GroupValueWrite,
gateway_id: None,
timestamp: std::time::SystemTime::now(),
};
for _ in 0..notification_count {
handler.notify_telegram_received(&telegram).await;
}
let telegram_sync_executions = telegram_sync_counter.load(Ordering::SeqCst);
let telegram_async_executions = telegram_async_counter.load(Ordering::SeqCst);
prop_assert_eq!(telegram_sync_executions, notification_count,
"Sync telegram callback should be executed correct number of times");
prop_assert_eq!(telegram_async_executions, notification_count,
"Async telegram callback should be executed correct number of times");
Ok(())
});
});
}
#[test]
fn property_graceful_invalid_unregistration() {
let rt = tokio::runtime::Runtime::new().unwrap();
proptest!(ProptestConfig::with_cases(20), |(
// Generate invalid handle IDs
invalid_handle_ids in prop::collection::vec(1u64..=u64::MAX, 1..5),
valid_callback_count in 0usize..3usize,
)| {
let _ = rt.block_on(async {
let handler = EventHandler::new();
let mut valid_handles = Vec::new();
for _ in 0..valid_callback_count {
let handle = handler.register_telegram_callback_sync(|_| {}).await;
valid_handles.push(handle);
}
let initial_count = handler.total_callback_count().await;
prop_assert_eq!(initial_count, valid_callback_count);
for &invalid_id in &invalid_handle_ids {
let invalid_handle = CallbackHandle::new(invalid_id);
let telegram_result = handler.unregister_telegram_callback(invalid_handle).await;
let connection_result = handler.unregister_connection_callback(invalid_handle).await;
let unified_result = handler.unregister_callback(invalid_handle).await;
prop_assert!(!telegram_result, "Unregistering invalid telegram handle should return false");
prop_assert!(!connection_result, "Unregistering invalid connection handle should return false");
prop_assert!(!unified_result, "Unregistering invalid handle with unified method should return false");
}
prop_assert_eq!(handler.total_callback_count().await, valid_callback_count,
"Valid callbacks should remain registered after invalid unregistration attempts");
for handle in valid_handles {
prop_assert!(handler.unregister_callback(handle).await,
"Should be able to unregister valid handle");
}
prop_assert_eq!(handler.total_callback_count().await, 0,
"All callbacks should be unregistered after valid unregistration");
Ok(())
});
});
}
#[test]
fn property_builder_callback_transfer() {
use crate::application::knx::Knx;
use crate::protocol::{
address::{Address, GroupAddress, IndividualAddress},
telegram::{Direction, Telegram, TelegramType},
};
use crate::transport::ConnectionType;
use std::sync::atomic::{AtomicUsize, Ordering};
let rt = tokio::runtime::Runtime::new().unwrap();
proptest!(ProptestConfig::with_cases(10), |( // Reduced cases for performance
// Generate callback configurations
telegram_callback_count in 1usize..3usize,
connection_callback_count in 1usize..3usize,
// Generate test data
source_raw in 0u16..=0xFFFF,
dest_main in 0u8..=GroupAddress::MAX_MAIN,
dest_middle in 0u8..=GroupAddress::MAX_MIDDLE,
dest_sub in 0u8..=255u8,
payload in prop::collection::vec(0u8..=255u8, 0..3),
)| {
let _ = rt.block_on(async {
let telegram_counters = Arc::new(tokio::sync::RwLock::new(Vec::new()));
let connection_counters = Arc::new(tokio::sync::RwLock::new(Vec::new()));
let mut builder = Knx::builder()
.connection_type(ConnectionType::Routing)
.memory_limit_mb(32);
for _i in 0..telegram_callback_count {
let counter = Arc::new(AtomicUsize::new(0));
telegram_counters.write().await.push(counter.clone());
let counter_clone = counter.clone();
builder = builder.telegram_callback(SyncTelegramCallback { counter: counter_clone }).unwrap();
}
for _i in 0..connection_callback_count {
let counter = Arc::new(AtomicUsize::new(0));
connection_counters.write().await.push(counter.clone());
let counter_clone = counter.clone();
builder = builder.connection_callback(SyncConnectionCallback { counter: counter_clone }).unwrap();
}
let knx = builder.build().await.unwrap();
let expected_total = telegram_callback_count + connection_callback_count;
prop_assert_eq!(knx.total_callback_count().await, expected_total,
"All callbacks should be transferred from builder to Knx instance");
prop_assert_eq!(knx.telegram_callback_count().await, telegram_callback_count,
"Telegram callbacks should be transferred correctly");
prop_assert_eq!(knx.connection_callback_count().await, connection_callback_count,
"Connection callbacks should be transferred correctly");
let source = IndividualAddress::from_raw(source_raw);
let destination = Address::Group(GroupAddress::from_parts(dest_main, dest_middle, dest_sub).unwrap());
let telegram = Telegram {
source,
destination,
payload,
priority: crate::protocol::telegram::Priority::Normal,
direction: Direction::Incoming,
telegram_type: TelegramType::GroupValueWrite,
gateway_id: None,
timestamp: std::time::SystemTime::now(),
};
knx.test_notify_telegram_received(&telegram).await;
knx.test_notify_connection_state_changed(crate::application::callbacks::ConnectionState::Connected).await;
let telegram_counters_read = telegram_counters.read().await;
for (i, counter) in telegram_counters_read.iter().enumerate() {
prop_assert_eq!(counter.load(Ordering::SeqCst), 1,
"Builder-registered telegram callback {} should be executed", i);
}
drop(telegram_counters_read);
let connection_counters_read = connection_counters.read().await;
for (i, counter) in connection_counters_read.iter().enumerate() {
prop_assert_eq!(counter.load(Ordering::SeqCst), 1,
"Builder-registered connection callback {} should be executed", i);
}
drop(connection_counters_read);
knx.clear_all_callbacks().await;
prop_assert_eq!(knx.total_callback_count().await, 0,
"Builder-registered callbacks should be removable");
knx.test_notify_telegram_received(&telegram).await;
knx.test_notify_connection_state_changed(crate::application::callbacks::ConnectionState::Disconnected).await;
let telegram_counters_read = telegram_counters.read().await;
for (i, counter) in telegram_counters_read.iter().enumerate() {
prop_assert_eq!(counter.load(Ordering::SeqCst), 1,
"Telegram callback {} should not be executed after clearing", i);
}
drop(telegram_counters_read);
let connection_counters_read = connection_counters.read().await;
for (i, counter) in connection_counters_read.iter().enumerate() {
prop_assert_eq!(counter.load(Ordering::SeqCst), 1,
"Connection callback {} should not be executed after clearing", i);
}
Ok(())
});
});
}
#[test]
fn property_registration_error_handling() {
use crate::application::knx::Knx;
use crate::error::KnxError;
use crate::transport::ConnectionType;
let rt = tokio::runtime::Runtime::new().unwrap();
proptest!(ProptestConfig::with_cases(10), |( // Reduced cases for performance
// Generate callback counts that will exceed the limit
excessive_callback_count in 1001usize..1010usize, // Above MAX_BUILDER_CALLBACKS (1000)
normal_callback_count in 1usize..10usize,
)| {
let _ = rt.block_on(async {
#[derive(Clone)]
struct TestCallback;
#[async_trait]
impl TelegramCallbackFn for TestCallback {
async fn call(&self, _telegram: &Telegram) {}
}
#[derive(Clone)]
struct TestConnectionCallback;
#[async_trait]
impl ConnectionCallbackFn for TestConnectionCallback {
async fn call(&self, _state: crate::application::callbacks::ConnectionState) {}
}
let mut builder = Knx::builder()
.connection_type(ConnectionType::Routing)
.memory_limit_mb(32);
let mut successful_registrations = 0;
let mut first_error: Option<KnxError> = None;
for _i in 0..excessive_callback_count {
match builder.telegram_callback(TestCallback) {
Ok(new_builder) => {
builder = new_builder;
successful_registrations += 1;
}
Err(e) => {
if first_error.is_none() {
first_error = Some(e);
}
break;
}
}
}
prop_assert!(first_error.is_some(), "Should get an error when exceeding callback limit");
if let Some(error) = first_error {
let error_message = error.to_string();
prop_assert!(error_message.contains("Maximum number"),
"Error message should mention maximum number: {}", error_message);
prop_assert!(error_message.contains("1000"),
"Error message should mention the limit: {}", error_message);
}
prop_assert_eq!(successful_registrations, 1000,
"Should successfully register exactly 1000 callbacks before failing");
let mut test_builder = Knx::builder()
.connection_type(ConnectionType::Routing)
.memory_limit_mb(32);
for _i in 0..successful_registrations {
test_builder = test_builder.telegram_callback(TestCallback).unwrap();
}
let knx_result = test_builder.build().await;
prop_assert!(knx_result.is_ok(), "Should be able to build Knx instance after callback registration error");
if let Ok(knx) = knx_result {
prop_assert_eq!(knx.total_callback_count().await, successful_registrations,
"Knx instance should have all successfully registered callbacks");
}
let mut builder2 = Knx::builder()
.connection_type(ConnectionType::Routing)
.memory_limit_mb(32);
for _i in 0..normal_callback_count {
builder2 = builder2.telegram_callback(TestCallback).unwrap();
}
let remaining_slots = 1000 - normal_callback_count;
let mut connection_registrations = 0;
let mut connection_error: Option<KnxError> = None;
for _i in 0..=remaining_slots { match builder2.connection_callback(TestConnectionCallback) {
Ok(new_builder) => {
builder2 = new_builder;
connection_registrations += 1;
}
Err(e) => {
connection_error = Some(e);
break;
}
}
}
if remaining_slots > 0 {
prop_assert_eq!(connection_registrations, remaining_slots,
"Should register exactly the remaining slots");
prop_assert!(connection_error.is_some(),
"Should get error when trying to register beyond limit");
}
let mut limit_builder = Knx::builder()
.connection_type(ConnectionType::Routing)
.memory_limit_mb(32);
for _i in 0..1000 {
limit_builder = limit_builder.telegram_callback(TestCallback).unwrap();
}
let connection_result = limit_builder.connection_callback(TestConnectionCallback);
prop_assert!(connection_result.is_err(),
"Should get error when trying to register callback beyond limit");
if let Err(error) = connection_result {
let error_message = error.to_string();
prop_assert!(error_message.contains("Maximum number"),
"Connection callback error should be descriptive: {}", error_message);
}
let mut final_builder = Knx::builder()
.connection_type(ConnectionType::Routing)
.memory_limit_mb(32);
let test_callback_count = std::cmp::min(normal_callback_count, 5);
for _i in 0..test_callback_count {
final_builder = final_builder.telegram_callback(TestCallback).unwrap();
}
let final_knx_result = final_builder.build().await;
prop_assert!(final_knx_result.is_ok(),
"Should be able to build Knx instance even after callback registration errors");
if let Ok(final_knx) = final_knx_result {
let total_callbacks = final_knx.total_callback_count().await;
prop_assert_eq!(total_callbacks, test_callback_count,
"Final Knx instance should have exactly {} callbacks, got {}",
test_callback_count, total_callbacks);
}
Ok(())
});
});
}
}