use proptest::prelude::*;
use proptest::test_runner::Config as ProptestConfig;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::time::Duration;
use tokio::time::timeout;
use crate::application::knx::{Knx, KnxState};
use crate::protocol::address::IndividualAddress;
use crate::transport::{BackoffConfig, ConnectionConfig, ConnectionType, TcpConfig};
fn arb_connection_config() -> impl Strategy<Value = ConnectionConfig> {
(
prop_oneof![Just(ConnectionType::Routing),],
(1u8..=15u8, 1u8..=15u8, 1u8..=255u8),
100u64..=500u64,
)
.prop_map(
|(connection_type, (area, line, device), timeout_ms)| ConnectionConfig {
connection_type,
gateway_ip: None,
gateway_port: Some(3671),
local_ip: None,
individual_address: IndividualAddress::new(area, line, device),
security: None,
timeout_ms,
auto_reconnect: false,
reconnect_backoff: BackoffConfig::default(),
tcp_config: TcpConfig::default(),
},
)
}
#[derive(Debug)]
struct ResourceTracker {
allocated: AtomicUsize,
cleanup_called: AtomicBool,
}
impl ResourceTracker {
fn new() -> Self {
Self {
allocated: AtomicUsize::new(0),
cleanup_called: AtomicBool::new(false),
}
}
fn allocate(&self) {
self.allocated.fetch_add(1, Ordering::SeqCst);
}
fn deallocate(&self) {
self.allocated.fetch_sub(1, Ordering::SeqCst);
}
fn mark_cleanup(&self) {
self.cleanup_called.store(true, Ordering::SeqCst);
}
fn is_clean(&self) -> bool {
self.allocated.load(Ordering::SeqCst) == 0
}
}
proptest! {
#![proptest_config(ProptestConfig {
cases: 20,
max_shrink_iters: 10,
timeout: 30000,
.. ProptestConfig::default()
})]
#[test]
fn test_async_operation_cancellation_safety(
config in arb_connection_config(),
cancel_after_ms in 1u64..50u64,
) {
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let tracker = Arc::new(ResourceTracker::new());
let tracker_clone = tracker.clone();
let Ok(knx) = Knx::new(config.clone()).await else {
return Ok(());
};
let initial_state = knx.state().await;
prop_assert_eq!(
initial_state,
KnxState::Disconnected,
"Knx should start in Disconnected state"
);
tracker.allocate();
let cancel_duration = Duration::from_millis(cancel_after_ms);
let _connect_result = timeout(cancel_duration, knx.connect()).await;
let state_after_connect = knx.state().await;
prop_assert!(
state_after_connect == KnxState::Connected ||
state_after_connect == KnxState::Disconnected ||
state_after_connect == KnxState::Connecting ||
state_after_connect == KnxState::Error,
"State should be valid after connect attempt/cancellation, got: {:?}",
state_after_connect
);
if state_after_connect == KnxState::Connected {
let _disconnect_result = timeout(cancel_duration, knx.disconnect()).await;
let state_after_disconnect = knx.state().await;
prop_assert!(
state_after_disconnect == KnxState::Disconnected ||
state_after_disconnect == KnxState::Disconnecting ||
state_after_disconnect == KnxState::Connected,
"State should be valid after disconnect attempt/cancellation, got: {:?}",
state_after_disconnect
);
}
tracker.deallocate();
tracker.mark_cleanup();
prop_assert!(
tracker_clone.is_clean(),
"All resources should be cleaned up after cancellation"
);
Ok(())
})?;
}
#[test]
fn test_concurrent_operation_cancellation(
config in arb_connection_config(),
num_operations in 2usize..5usize,
cancel_after_ms in 5u64..30u64,
) {
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let knx = match Knx::new(config.clone()).await {
Ok(x) => Arc::new(x),
Err(_) => return Ok(()),
};
let completed = Arc::new(AtomicUsize::new(0));
let cancelled = Arc::new(AtomicUsize::new(0));
let mut handles = Vec::new();
for _ in 0..num_operations {
let knx_clone = knx.clone();
let completed_clone = completed.clone();
let cancelled_clone = cancelled.clone();
let cancel_duration = Duration::from_millis(cancel_after_ms);
let handle = tokio::spawn(async move {
match timeout(cancel_duration, knx_clone.state()).await {
Ok(_) => {
completed_clone.fetch_add(1, Ordering::SeqCst);
}
Err(_) => {
cancelled_clone.fetch_add(1, Ordering::SeqCst);
}
}
});
handles.push(handle);
}
for handle in handles {
let _ = handle.await;
}
let total = completed.load(Ordering::SeqCst) + cancelled.load(Ordering::SeqCst);
prop_assert_eq!(
total,
num_operations,
"All operations should be either completed or cancelled"
);
let final_state = knx.state().await;
prop_assert!(
final_state == KnxState::Disconnected ||
final_state == KnxState::Connected ||
final_state == KnxState::Connecting ||
final_state == KnxState::Disconnecting ||
final_state == KnxState::Error,
"Knx should be in a valid state after concurrent operations, got: {:?}",
final_state
);
Ok(())
})?;
}
}
#[cfg(test)]
mod unit_tests {
use super::*;
#[tokio::test]
async fn test_knx_creation_with_routing_config() {
let config = ConnectionConfig {
connection_type: ConnectionType::Routing,
gateway_ip: None,
gateway_port: Some(3671),
local_ip: None,
individual_address: IndividualAddress::new(1, 1, 240),
security: None,
timeout_ms: 5000,
auto_reconnect: true,
reconnect_backoff: BackoffConfig::default(),
tcp_config: TcpConfig::default(),
};
let knx = Knx::new(config).await.unwrap();
assert_eq!(knx.state().await, KnxState::Disconnected);
}
#[tokio::test]
async fn test_knx_state_transitions() {
let config = ConnectionConfig {
connection_type: ConnectionType::Routing,
gateway_ip: None,
gateway_port: Some(3671),
local_ip: None,
individual_address: IndividualAddress::new(1, 1, 240),
security: None,
timeout_ms: 5000,
auto_reconnect: false,
reconnect_backoff: BackoffConfig::default(),
tcp_config: TcpConfig::default(),
};
let knx = Knx::new(config).await.unwrap();
assert_eq!(knx.state().await, KnxState::Disconnected);
assert!(!knx.is_connected().await);
let _connect_result = knx.connect().await;
let state = knx.state().await;
assert!(
state == KnxState::Connected
|| state == KnxState::Connecting
|| state == KnxState::Disconnected
|| state == KnxState::Error,
"State should be valid after connect attempt, got: {state:?}"
);
}
#[tokio::test]
async fn test_cancellation_during_connect() {
let config = ConnectionConfig {
connection_type: ConnectionType::Routing,
gateway_ip: None,
gateway_port: Some(3671),
local_ip: None,
individual_address: IndividualAddress::new(1, 1, 240),
security: None,
timeout_ms: 5000,
auto_reconnect: false,
reconnect_backoff: BackoffConfig::default(),
tcp_config: TcpConfig::default(),
};
let knx = Knx::new(config).await.unwrap();
let _result = timeout(Duration::from_millis(1), knx.connect()).await;
let state = knx.state().await;
assert!(
state == KnxState::Disconnected
|| state == KnxState::Connecting
|| state == KnxState::Connected
|| state == KnxState::Error,
"State should be valid after cancellation, got: {state:?}"
);
}
#[tokio::test]
async fn test_knx_builder() {
let knx = Knx::builder()
.connection_type(ConnectionType::Routing)
.timeout_ms(3000)
.auto_reconnect(false)
.build()
.await
.unwrap();
assert_eq!(knx.state().await, KnxState::Disconnected);
assert_eq!(knx.config().connection_type, ConnectionType::Routing);
assert_eq!(knx.config().timeout_ms, 3000);
assert!(!knx.config().auto_reconnect);
}
#[tokio::test]
async fn test_knx_shutdown_flag() {
let config = ConnectionConfig {
connection_type: ConnectionType::Routing,
gateway_ip: None,
gateway_port: Some(3671),
local_ip: None,
individual_address: IndividualAddress::new(1, 1, 240),
security: None,
timeout_ms: 5000,
auto_reconnect: false,
reconnect_backoff: BackoffConfig::default(),
tcp_config: TcpConfig::default(),
};
let knx = Knx::new(config).await.unwrap();
assert!(!knx.is_shutting_down());
knx.stop().await;
assert!(!knx.is_shutting_down());
}
#[tokio::test]
async fn test_knx_config_access() {
let config = ConnectionConfig {
connection_type: ConnectionType::Routing,
gateway_ip: None,
gateway_port: Some(3672),
local_ip: None,
individual_address: IndividualAddress::new(2, 3, 4),
security: None,
timeout_ms: 10000,
auto_reconnect: true,
reconnect_backoff: BackoffConfig::default(),
tcp_config: TcpConfig::default(),
};
let knx = Knx::new(config.clone()).await.unwrap();
assert_eq!(knx.config().connection_type, ConnectionType::Routing);
assert_eq!(knx.config().gateway_port, Some(3672));
assert_eq!(knx.config().timeout_ms, 10000);
assert!(knx.config().auto_reconnect);
}
}