use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use btleplug::api::CentralState;
use rand::rngs::StdRng;
use rand::{Rng, SeedableRng};
use super::helpers::*;
use crate::error::Error;
use crate::handler::{Handler, OnDisconnectHandler};
use crate::mock::{ConnectBehaviour, DeviceHandle, FailKind, OpBehaviour};
use crate::models::{ScanFilter, WriteType};
#[derive(Debug, Clone, Copy)]
enum Step {
Connect,
Disconnect,
DropLink,
DropLinkSilently,
Hide,
Show,
PowerOff,
PowerOn,
NotifyBurst,
SpuriousConnect,
SpuriousDisconnect,
WriteRead,
Subscribe,
FlakyConnects,
SlowDiscovery,
HangingConnect,
}
const STEPS: &[Step] = &[
Step::Connect,
Step::Connect,
Step::Connect,
Step::Disconnect,
Step::Disconnect,
Step::DropLink,
Step::DropLink,
Step::DropLinkSilently,
Step::Hide,
Step::Show,
Step::PowerOff,
Step::PowerOn,
Step::NotifyBurst,
Step::SpuriousConnect,
Step::SpuriousDisconnect,
Step::WriteRead,
Step::WriteRead,
Step::Subscribe,
Step::FlakyConnects,
Step::SlowDiscovery,
Step::HangingConnect,
];
fn env_u64(name: &str, default: u64) -> u64 {
std::env::var(name)
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(default)
}
struct Model {
established: usize,
callbacks: Arc<AtomicUsize>,
handler_connected: bool,
notified: Arc<Mutex<usize>>,
}
async fn connect_step(
handler: &'static Handler,
device: &DeviceHandle,
model: &mut Model,
) -> Result<(), Error> {
let callbacks = model.callbacks.clone();
let result = handler
.connect(
&device.address_string(),
OnDisconnectHandler::from_sync(move || {
callbacks.fetch_add(1, Ordering::SeqCst);
}),
false,
)
.await;
if result.is_ok() {
if !model.handler_connected {
model.established += 1;
}
model.handler_connected = true;
}
result
}
async fn check_invariants(handler: &Handler, device: &DeviceHandle, model: &mut Model, step: &str) {
let connected = handler.is_connected();
if connected {
assert!(
device.is_connected(),
"[{step}] handler connected but device link is down"
);
assert!(
handler.connected_device().await.is_ok(),
"[{step}] connected but connected_device() fails"
);
} else {
assert!(
matches!(
handler.connected_device().await,
Err(Error::NoDeviceConnected)
),
"[{step}] disconnected but connected_device() returns a device"
);
assert!(
matches!(handler.mtu().await, Err(Error::NoDeviceConnected)),
"[{step}] disconnected but mtu() works"
);
if model.handler_connected {
model.handler_connected = false;
}
}
let callbacks = model.callbacks.load(Ordering::SeqCst);
let ended = model.established - usize::from(model.handler_connected);
assert_eq!(
callbacks, ended,
"[{step}] disconnect callback ran {callbacks} times for {ended} ended connections"
);
}
async fn quiesce(handler: &Handler, device: &DeviceHandle) {
let _ = wait_until(Duration::from_secs(3), || {
handler.is_connected() == device.is_connected()
})
.await;
settle().await;
}
#[tokio::test(start_paused = true)]
async fn randomized_connection_stress() {
let seed = env_u64("BLEC_STRESS_SEED", 1);
let iters = env_u64("BLEC_STRESS_ITERS", 300);
eprintln!("stress test: BLEC_STRESS_SEED={seed} BLEC_STRESS_ITERS={iters}");
let mut rng = StdRng::seed_from_u64(seed);
let (world, handler) = test_world();
let device = stress_device(&world);
let other = other_device(&world);
let mut model = Model {
established: 0,
callbacks: Arc::new(AtomicUsize::new(0)),
handler_connected: false,
notified: Arc::new(Mutex::new(0)),
};
let mut history: Vec<String> = vec![];
scan(handler, ScanFilter::None).await.unwrap();
for i in 0..iters {
let step = STEPS[rng.gen_range(0..STEPS.len())];
let label = format!("#{i} {step:?}");
history.push(label.clone());
match step {
Step::Connect => {
if world.adapter_state() == CentralState::PoweredOff {
world.set_adapter_state(CentralState::PoweredOn);
}
let _ = connect_step(handler, &device, &mut model).await;
}
Step::Disconnect => {
let _ = handler.disconnect().await;
}
Step::DropLink => {
device.drop_link();
}
Step::DropLinkSilently => {
device.drop_link_silently();
let _ = handler.recv_data(CHARAC, None).await;
if handler.is_connected() && !device.is_connected() {
let _ = handler.disconnect().await;
}
}
Step::Hide => device.set_in_range(false),
Step::Show => device.set_in_range(true),
Step::PowerOff => world.set_adapter_state(CentralState::PoweredOff),
Step::PowerOn => world.set_adapter_state(CentralState::PoweredOn),
Step::NotifyBurst => {
let n = rng.gen_range(1..200);
for k in 0..n {
device.notify(SERVICE, CHARAC_NOTIFY, vec![k as u8]);
}
}
Step::SpuriousConnect => other.complete_connect(),
Step::SpuriousDisconnect => {
other.emit_disconnected_event();
device.emit_disconnected_event();
}
Step::WriteRead => {
let data: Vec<u8> = (0..rng.gen_range(1..50)).map(|_| rng.gen()).collect();
let write = handler
.send_data(CHARAC, Some(SERVICE), &data, WriteType::WithResponse)
.await;
let read = handler.recv_data(CHARAC, Some(SERVICE)).await;
if let (Ok(()), Ok(read)) = (write, read) {
assert_eq!(read, data, "[{label}] read back different data");
}
}
Step::Subscribe => {
let notified = model.notified.clone();
let _ = handler
.subscribe(CHARAC_NOTIFY, None, move |_data| {
*notified.lock().unwrap() += 1;
})
.await;
}
Step::FlakyConnects => {
let fails = rng.gen_range(1..4);
device.fail_next_connects(fails, FailKind::NotConnected);
let _ = connect_step(handler, &device, &mut model).await;
device.clear_connect_queue();
}
Step::SlowDiscovery => {
device.set_discover_behaviour(OpBehaviour::Delay(Duration::from_millis(
rng.gen_range(0..1500),
)));
let _ = connect_step(handler, &device, &mut model).await;
device.set_discover_behaviour(OpBehaviour::Ok);
}
Step::HangingConnect => {
device.set_connect_behaviour(ConnectBehaviour::Hang);
let _ = connect_step(handler, &device, &mut model).await;
device.set_connect_behaviour(ConnectBehaviour::Ok);
if rng.gen_bool(0.5) {
device.complete_connect();
}
}
}
quiesce(handler, &device).await;
if other.is_connected() {
assert!(
wait_until(Duration::from_secs(1), || !other.is_connected()).await,
"[{label}] unrequested link to the other device kept open"
);
}
check_invariants(handler, &device, &mut model, &label).await;
}
device.set_in_range(true);
if world.adapter_state() == CentralState::PoweredOff {
world.set_adapter_state(CentralState::PoweredOn);
}
if handler.is_connected() {
handler.disconnect().await.unwrap();
}
scan(handler, ScanFilter::None).await.unwrap();
connect_step(handler, &device, &mut model)
.await
.unwrap_or_else(|e| panic!("final connect failed: {e}\nhistory: {history:#?}"));
handler.disconnect().await.unwrap();
quiesce(handler, &device).await;
check_invariants(handler, &device, &mut model, "final").await;
eprintln!(
"stress test done: {} connections established, {} notifications received",
model.established,
*model.notified.lock().unwrap()
);
}