1use std::ops::ControlFlow;
12use std::sync::{Arc, LazyLock};
13use std::time::Duration;
14
15use btleplug::api::{Central, Manager as _, Peripheral as _, ScanFilter};
16use btleplug::platform::{Adapter, Manager, Peripheral, PeripheralId};
17use tokio::runtime::Handle;
18use tokio::sync::RwLock;
19use tokio::time::sleep;
20use tokio_util::sync::CancellationToken;
21use tracing::{debug, info, warn};
22
23static MANAGER: RwLock<Option<Manager>> = RwLock::const_new(None);
29
30async fn shared_manager() -> Result<Manager> {
32 {
34 let guard = MANAGER.read().await;
35 if let Some(m) = guard.as_ref() {
36 return Ok(m.clone());
37 }
38 }
39 let mut guard = MANAGER.write().await;
41 if let Some(m) = guard.as_ref() {
43 return Ok(m.clone());
44 }
45 let m = crate::runtime::run(Manager::new()).await??;
50 *guard = Some(m.clone());
51 Ok(m)
52}
53
54async fn reset_manager() {
59 let mut guard = MANAGER.write().await;
60 if guard.take().is_some() {
61 warn!("BLE manager reset — next operation will create a new D-Bus connection");
62 }
63}
64
65use crate::error::{Error, Result};
66use crate::util::create_identifier;
67use crate::uuid::{MANUFACTURER_ID, SAF_TEHNIKA_SERVICE_NEW, SAF_TEHNIKA_SERVICE_OLD};
68use aranet_types::DeviceType;
69
70#[derive(Debug, Clone)]
72pub enum FindProgress {
73 CacheHit,
77 ScanAttempt {
79 attempt: u32,
81 total: u32,
83 duration_secs: u64,
85 },
86 Found { attempt: u32 },
88 RetryNeeded { attempt: u32 },
90}
91
92pub type ProgressCallback = Box<dyn Fn(FindProgress) + Send + Sync>;
94
95#[derive(Debug, Clone)]
97pub struct DiscoveredDevice {
98 pub name: Option<String>,
100 pub id: PeripheralId,
102 pub address: String,
104 pub identifier: String,
106 pub rssi: Option<i16>,
108 pub device_type: Option<DeviceType>,
110 pub is_aranet: bool,
112 pub manufacturer_data: Option<Vec<u8>>,
114}
115
116#[derive(Debug, Clone)]
118pub struct ScanOptions {
119 pub duration: Duration,
121 pub filter_aranet_only: bool,
123 pub use_service_filter: bool,
126}
127
128impl Default for ScanOptions {
129 fn default() -> Self {
130 Self {
131 duration: Duration::from_secs(5),
132 filter_aranet_only: true,
133 use_service_filter: false,
136 }
137 }
138}
139
140impl ScanOptions {
141 pub fn new() -> Self {
143 Self::default()
144 }
145
146 pub fn duration(mut self, duration: Duration) -> Self {
148 self.duration = duration;
149 self
150 }
151
152 pub fn duration_secs(mut self, secs: u64) -> Self {
154 self.duration = Duration::from_secs(secs);
155 self
156 }
157
158 pub fn filter_aranet_only(mut self, filter: bool) -> Self {
160 self.filter_aranet_only = filter;
161 self
162 }
163
164 pub fn all_devices(self) -> Self {
166 self.filter_aranet_only(false)
167 }
168
169 pub fn use_service_filter(mut self, enable: bool) -> Self {
177 self.use_service_filter = enable;
178 self
179 }
180
181 pub fn optimized() -> Self {
185 Self {
186 duration: Duration::from_secs(3),
187 filter_aranet_only: true,
188 use_service_filter: true,
189 }
190 }
191}
192
193static ADAPTER: RwLock<Option<Adapter>> = RwLock::const_new(None);
208
209pub async fn get_adapter() -> Result<Adapter> {
216 if cfg!(target_os = "macos") {
217 cached_adapter().await
218 } else {
219 crate::runtime::run(create_adapter()).await?
220 }
221}
222
223async fn cached_adapter() -> Result<Adapter> {
224 let cached = ADAPTER.read().await.clone();
225 if let Some(adapter) = cached
226 && adapter_thread_is_running(&adapter).await
227 {
228 return Ok(adapter);
229 }
230 let mut guard = ADAPTER.write().await;
231 if let Some(adapter) = guard.as_ref() {
232 if adapter_thread_is_running(adapter).await {
233 return Ok(adapter.clone());
234 }
235 warn!("CoreBluetooth adapter thread has stopped; creating a new adapter");
236 }
237 let adapter = crate::runtime::run(create_adapter()).await??;
238 *guard = Some(adapter.clone());
239 Ok(adapter)
240}
241
242async fn adapter_thread_is_running(adapter: &Adapter) -> bool {
250 let adapter = adapter.clone();
251 let state = crate::runtime::run(async move {
254 tokio::time::timeout(Duration::from_secs(2), adapter.adapter_state()).await
255 })
256 .await;
257 match state {
258 Ok(Ok(Err(e))) => {
259 debug!("Cached Bluetooth adapter is unusable: {e}");
260 false
261 }
262 _ => true,
263 }
264}
265
266async fn create_adapter() -> Result<Adapter> {
267 use crate::error::DeviceNotFoundReason;
268
269 let manager = shared_manager().await?;
270 let adapters = match manager.adapters().await {
271 Ok(a) => a,
272 Err(e) => {
273 reset_manager().await;
276 return Err(e.into());
277 }
278 };
279
280 adapters
281 .into_iter()
282 .next()
283 .ok_or(Error::DeviceNotFound(DeviceNotFoundReason::NoAdapter))
284}
285
286pub(crate) trait ScanControl: Clone + Send + Sync + 'static {
289 fn start(&self, filter: ScanFilter) -> impl Future<Output = Result<()>> + Send;
292
293 fn stop(&self) -> impl Future<Output = Result<()>> + Send;
295}
296
297impl ScanControl for Adapter {
298 async fn start(&self, filter: ScanFilter) -> Result<()> {
299 Central::start_scan(self, filter).await?;
300 Ok(())
301 }
302
303 async fn stop(&self) -> Result<()> {
304 Central::stop_scan(self).await?;
305 Ok(())
306 }
307}
308
309#[derive(Clone, Default)]
317pub(crate) struct ScanLock(Arc<tokio::sync::Mutex<()>>);
318
319impl ScanLock {
320 pub(crate) async fn acquire(&self) -> ScanPermit {
322 ScanPermit {
323 _guard: Arc::clone(&self.0).lock_owned().await,
324 }
325 }
326}
327
328pub(crate) struct ScanPermit {
336 _guard: tokio::sync::OwnedMutexGuard<()>,
337}
338
339static SCAN_LOCK: LazyLock<ScanLock> = LazyLock::new(ScanLock::default);
340
341pub(crate) fn scan_lock() -> &'static ScanLock {
344 &SCAN_LOCK
345}
346
347async fn scan_window<S: ScanControl>(
355 runtime: &Handle,
356 scanner: &S,
357 permit: ScanPermit,
358 filter: ScanFilter,
359 duration: Duration,
360) -> Result<()> {
361 let scanner = scanner.clone();
362 let cancel = CancellationToken::new();
363 let _end_early_on_drop = cancel.clone().drop_guard();
365 let window = runtime.spawn(async move {
366 let _permit = permit; if cancel.is_cancelled() {
368 return Ok(());
369 }
370 scanner.start(filter).await?; let _ = cancel.run_until_cancelled(sleep(duration)).await;
372 let stopped = scanner.stop().await;
373 if let Err(e) = &stopped {
374 warn!("Failed to stop the Bluetooth scan: {e}");
376 }
377 stopped
378 });
379 window.await.map_err(std::io::Error::from)?
380}
381
382pub(crate) async fn run_scan<S: ScanControl>(
386 scanner: &S,
387 permit: ScanPermit,
388 filter: ScanFilter,
389 duration: Duration,
390) -> Result<()> {
391 scan_window(
392 &crate::runtime::handle()?,
393 scanner,
394 permit,
395 filter,
396 duration,
397 )
398 .await
399}
400
401pub async fn scan_for_devices() -> Result<Vec<DiscoveredDevice>> {
413 scan_with_options(ScanOptions::default()).await
414}
415
416pub async fn scan_with_options(options: ScanOptions) -> Result<Vec<DiscoveredDevice>> {
418 let adapter = get_adapter().await?;
419 scan_with_adapter(&adapter, options).await
420}
421
422pub async fn scan_with_retry(
445 options: ScanOptions,
446 max_retries: u32,
447 retry_on_empty: bool,
448) -> Result<Vec<DiscoveredDevice>> {
449 let mut attempt = 0;
450 let mut delay = Duration::from_millis(500);
451
452 loop {
453 match scan_with_options(options.clone()).await {
454 Ok(devices) if devices.is_empty() && retry_on_empty && attempt < max_retries => {
455 attempt += 1;
456 warn!(
457 "No devices found, retrying ({}/{})...",
458 attempt, max_retries
459 );
460 sleep(delay).await;
461 delay = delay.saturating_mul(2).min(Duration::from_secs(5));
462 }
463 Ok(devices) => return Ok(devices),
464 Err(e) if attempt < max_retries => {
465 attempt += 1;
466 warn!(
467 "Scan failed ({}), retrying ({}/{})...",
468 e, attempt, max_retries
469 );
470 sleep(delay).await;
471 delay = delay.saturating_mul(2).min(Duration::from_secs(5));
472 }
473 Err(e) => return Err(e),
474 }
475 }
476}
477
478pub async fn scan_with_adapter(
480 adapter: &Adapter,
481 options: ScanOptions,
482) -> Result<Vec<DiscoveredDevice>> {
483 info!(
484 "Starting BLE scan for {} seconds (service_filter={})...",
485 options.duration.as_secs(),
486 options.use_service_filter
487 );
488
489 let scan_filter = if options.use_service_filter {
491 aranet_service_filter()
492 } else {
493 ScanFilter::default()
494 };
495
496 let permit = scan_lock().acquire().await;
497 run_scan(adapter, permit, scan_filter, options.duration).await?;
498
499 let peripherals = adapter.peripherals().await?;
501 let mut discovered = Vec::new();
502
503 for peripheral in peripherals {
504 match process_peripheral(&peripheral, options.filter_aranet_only).await {
505 Ok(Some(device)) => {
506 info!("Found Aranet device: {:?}", device.name);
507 discovered.push(device);
508 }
509 Ok(None) => {
510 }
512 Err(e) => {
513 debug!("Error processing peripheral: {}", e);
514 }
515 }
516 }
517
518 info!("Scan complete. Found {} device(s)", discovered.len());
519 Ok(discovered)
520}
521
522async fn process_peripheral(
524 peripheral: &Peripheral,
525 filter_aranet_only: bool,
526) -> Result<Option<DiscoveredDevice>> {
527 let properties = peripheral.properties().await?;
528 let properties = match properties {
529 Some(p) => p,
530 None => return Ok(None),
531 };
532
533 let id = peripheral.id();
534 let address = properties.address.to_string();
535 let name = properties.local_name.clone();
536 let rssi = properties.rssi;
537
538 let is_aranet = is_aranet_device(&properties);
540
541 if filter_aranet_only && !is_aranet {
542 return Ok(None);
543 }
544
545 let device_type = name.as_ref().and_then(|n| DeviceType::from_name(n));
547
548 let manufacturer_data = properties.manufacturer_data.get(&MANUFACTURER_ID).cloned();
550
551 let identifier = create_identifier(&address, &id);
554
555 Ok(Some(DiscoveredDevice {
556 name,
557 id,
558 address,
559 identifier,
560 rssi,
561 device_type,
562 is_aranet,
563 manufacturer_data,
564 }))
565}
566
567fn is_aranet_device(properties: &btleplug::api::PeripheralProperties) -> bool {
569 if properties.manufacturer_data.contains_key(&MANUFACTURER_ID) {
571 return true;
572 }
573
574 for service_uuid in properties.service_data.keys() {
576 if *service_uuid == SAF_TEHNIKA_SERVICE_NEW || *service_uuid == SAF_TEHNIKA_SERVICE_OLD {
577 return true;
578 }
579 }
580
581 for service_uuid in &properties.services {
583 if *service_uuid == SAF_TEHNIKA_SERVICE_NEW || *service_uuid == SAF_TEHNIKA_SERVICE_OLD {
584 return true;
585 }
586 }
587
588 if let Some(name) = &properties.local_name {
590 let name_lower = name.to_lowercase();
591 if name_lower.contains("aranet") {
592 return true;
593 }
594 }
595
596 false
597}
598
599fn aranet_service_filter() -> ScanFilter {
602 ScanFilter {
603 services: vec![SAF_TEHNIKA_SERVICE_NEW, SAF_TEHNIKA_SERVICE_OLD],
604 }
605}
606
607const FILTER_SEARCH_WINDOWS: bool = cfg!(target_os = "macos");
628
629fn search_filter(aranet_only: bool) -> ScanFilter {
633 if aranet_only {
634 aranet_service_filter()
635 } else {
636 ScanFilter::default()
637 }
638}
639
640fn after_missed_scan(
653 query: &str,
654 similar: Vec<String>,
655 attempt: u32,
656 max_attempts: u32,
657) -> ControlFlow<Error> {
658 use crate::error::DeviceNotFoundReason;
659
660 if !similar.is_empty() {
661 return ControlFlow::Break(Error::DeviceNotFound(DeviceNotFoundReason::NoExactMatch {
662 identifier: query.to_string(),
663 similar,
664 }));
665 }
666 if attempt < max_attempts {
667 return ControlFlow::Continue(());
668 }
669 ControlFlow::Break(Error::device_not_found(query))
670}
671
672pub async fn find_device(identifier: &str) -> Result<(Adapter, Peripheral)> {
706 find_device_with_options(identifier, ScanOptions::default()).await
707}
708
709pub async fn find_device_with_options(
725 identifier: &str,
726 options: ScanOptions,
727) -> Result<(Adapter, Peripheral)> {
728 find_device_with_progress(identifier, options, None).await
729}
730
731pub async fn find_device_with_adapter(
739 adapter: &Adapter,
740 identifier: &str,
741 options: ScanOptions,
742) -> Result<Peripheral> {
743 find_device_with_adapter_progress(adapter, identifier, options, None).await
744}
745
746pub async fn find_device_with_adapter_progress(
751 adapter: &Adapter,
752 identifier: &str,
753 options: ScanOptions,
754 progress: Option<ProgressCallback>,
755) -> Result<Peripheral> {
756 let query = parse_query(identifier)?;
757
758 info!("Looking for device: {}", identifier);
759
760 if let Search::Found(peripheral) = search_known_peripherals(adapter, query).await? {
761 info!("Found device in cache (no scan needed)");
762 if let Some(ref cb) = progress {
763 cb(FindProgress::CacheHit);
764 }
765 return Ok(peripheral);
766 }
767
768 let max_attempts: u32 = 3;
769 let base_duration = options.duration.as_millis() as u64 / 2;
770 let base_duration = Duration::from_millis(base_duration.max(2000));
771
772 let mut attempt = 0;
775 loop {
776 attempt += 1;
777 let scan_duration = base_duration * attempt;
778 let duration_secs = scan_duration.as_secs();
779
780 let permit = scan_lock().acquire().await;
781 if let Search::Found(peripheral) = search_known_peripherals(adapter, query).await? {
783 info!("Found device while waiting to scan");
784 if let Some(ref cb) = progress {
785 cb(FindProgress::CacheHit);
786 }
787 return Ok(peripheral);
788 }
789
790 info!(
791 "Scan attempt {}/{} ({}s)...",
792 attempt, max_attempts, duration_secs
793 );
794 let filter = search_filter(FILTER_SEARCH_WINDOWS);
795 debug!(
796 "Scan attempt {}/{} ({}s, {})",
797 attempt,
798 max_attempts,
799 duration_secs,
800 if filter.services.is_empty() {
801 "all devices"
802 } else {
803 "Aranet sensors only"
804 }
805 );
806
807 if let Some(ref cb) = progress {
808 cb(FindProgress::ScanAttempt {
809 attempt,
810 total: max_attempts,
811 duration_secs,
812 });
813 }
814
815 run_scan(adapter, permit, filter, scan_duration).await?;
816
817 let similar = match search_known_peripherals(adapter, query).await? {
818 Search::Found(peripheral) => {
819 info!("Found device on attempt {}", attempt);
820 if let Some(ref cb) = progress {
821 cb(FindProgress::Found { attempt });
822 }
823 return Ok(peripheral);
824 }
825 Search::Missing { similar } => similar,
826 };
827
828 match after_missed_scan(query, similar, attempt, max_attempts) {
829 ControlFlow::Break(error) => {
830 warn!(
831 "Device not found after {} of {} attempts: {}",
832 attempt, max_attempts, identifier
833 );
834 return Err(error);
835 }
836 ControlFlow::Continue(()) => {
837 warn!("Device not found, retrying...");
838 if let Some(ref cb) = progress {
839 cb(FindProgress::RetryNeeded { attempt });
840 }
841 }
842 }
843 }
844}
845
846pub async fn find_device_with_progress(
854 identifier: &str,
855 options: ScanOptions,
856 progress: Option<ProgressCallback>,
857) -> Result<(Adapter, Peripheral)> {
858 parse_query(identifier)?;
860 let adapter = get_adapter().await?;
861 let peripheral =
862 find_device_with_adapter_progress(&adapter, identifier, options, progress).await?;
863 Ok((adapter, peripheral))
864}
865
866const UNKNOWN_ADDRESS: &str = "00:00:00:00:00:00";
869
870#[derive(Debug, Clone, PartialEq, Eq)]
872struct KnownPeripheral {
873 identifier: String,
876 peripheral_id: String,
880 address: String,
882 name: Option<String>,
884}
885
886#[derive(Debug, PartialEq, Eq)]
888enum Lookup {
889 Found(usize),
891 Ambiguous(Vec<usize>),
893 NotFound { similar: Vec<usize> },
896}
897
898fn parse_query(identifier: &str) -> Result<&str> {
901 let query = identifier.trim();
902 if query.is_empty() {
903 return Err(Error::invalid_config("device identifier is empty"));
904 }
905 Ok(query)
906}
907
908fn lookup(query: &str, known: &[KnownPeripheral]) -> Lookup {
925 let query = query.to_lowercase();
926 let bare_query = query.replace(':', "");
927
928 let by_identifier = matching(known, |peripheral| {
929 peripheral.identifier.to_lowercase() == query
930 || peripheral.peripheral_id.to_lowercase() == query
931 || (peripheral.address != UNKNOWN_ADDRESS
932 && peripheral.address.to_lowercase().replace(':', "") == bare_query)
933 });
934 if let Some(found) = decide(by_identifier) {
935 return found;
936 }
937
938 let by_name = matching(known, |peripheral| {
939 peripheral
940 .name
941 .as_deref()
942 .is_some_and(|name| name_matches(name, &query))
943 });
944 if let Some(found) = decide(by_name) {
945 return found;
946 }
947
948 let mut similar = matching(known, |peripheral| {
949 peripheral.name.as_deref().is_some_and(|name| {
950 let name = name.to_lowercase();
951 name.contains("aranet") && name.contains(&query)
952 })
953 });
954 similar.sort_by_key(|&index| (&known[index].name, &known[index].identifier));
955 Lookup::NotFound { similar }
956}
957
958fn matching(known: &[KnownPeripheral], predicate: impl Fn(&KnownPeripheral) -> bool) -> Vec<usize> {
960 known
961 .iter()
962 .enumerate()
963 .filter_map(|(index, peripheral)| predicate(peripheral).then_some(index))
964 .collect()
965}
966
967fn decide(indices: Vec<usize>) -> Option<Lookup> {
969 match indices.len() {
970 0 => None,
971 1 => Some(Lookup::Found(indices[0])),
972 _ => Some(Lookup::Ambiguous(indices)),
973 }
974}
975
976fn name_matches(name: &str, query: &str) -> bool {
979 let name = name.trim().to_lowercase();
980 name == query
981 || name
982 .strip_suffix(']')
983 .and_then(|combined| combined.rsplit_once(" ["))
984 .is_some_and(|(gap, advertised)| gap.trim() == query || advertised.trim() == query)
985}
986
987#[derive(Debug, PartialEq, Eq)]
989enum Search<T> {
990 Found(T),
992 Missing { similar: Vec<String> },
995}
996
997fn resolve(query: &str, known: &[KnownPeripheral]) -> Result<Search<usize>> {
1004 use crate::error::DeviceNotFoundReason;
1005
1006 match lookup(query, known) {
1007 Lookup::Found(index) => Ok(Search::Found(index)),
1008 Lookup::Ambiguous(indices) => {
1009 let mut candidates: Vec<String> = indices
1010 .iter()
1011 .map(|&index| {
1012 let device = &known[index];
1013 let name = device.name.as_deref().unwrap_or("unnamed");
1014 format!("{name} ({})", device.identifier)
1015 })
1016 .collect();
1017 candidates.sort();
1018 Err(Error::DeviceNotFound(DeviceNotFoundReason::Ambiguous {
1019 identifier: query.to_string(),
1020 candidates,
1021 }))
1022 }
1023 Lookup::NotFound { similar } => {
1024 let mut names: Vec<String> = similar
1025 .iter()
1026 .filter_map(|&index| known[index].name.as_deref())
1027 .map(|name| name.trim().to_string())
1028 .collect();
1029 names.sort();
1030 names.dedup();
1031 Ok(Search::Missing { similar: names })
1032 }
1033 }
1034}
1035
1036async fn search_known_peripherals(adapter: &Adapter, query: &str) -> Result<Search<Peripheral>> {
1039 let mut peripherals = Vec::new();
1040 let mut known = Vec::new();
1041 for peripheral in adapter.peripherals().await? {
1042 if let Ok(Some(props)) = peripheral.properties().await {
1043 let id = peripheral.id();
1044 let address = props.address.to_string();
1045 known.push(KnownPeripheral {
1046 identifier: create_identifier(&address, &id),
1047 peripheral_id: id.to_string(),
1048 address,
1049 name: props.local_name,
1050 });
1051 peripherals.push(peripheral);
1052 }
1053 }
1054
1055 match resolve(query, &known)? {
1056 Search::Found(index) => {
1057 let device = &known[index];
1058 debug!("Matched {:?} ({})", device.name, device.identifier);
1059 Ok(Search::Found(peripherals.swap_remove(index)))
1060 }
1061 Search::Missing { similar } => Ok(Search::Missing { similar }),
1062 }
1063}
1064
1065#[cfg(test)]
1066mod tests {
1067 use super::*;
1068
1069 use std::sync::atomic::{AtomicBool, Ordering};
1070
1071 use futures::FutureExt;
1072
1073 use crate::error::DeviceNotFoundReason;
1074 use crate::test_support::within;
1075
1076 #[test]
1079 fn test_scan_options_default() {
1080 let options = ScanOptions::default();
1081 assert_eq!(options.duration, Duration::from_secs(5));
1082 assert!(options.filter_aranet_only);
1083 }
1084
1085 #[test]
1086 fn test_scan_options_new() {
1087 let options = ScanOptions::new();
1088 assert_eq!(options.duration, Duration::from_secs(5));
1089 assert!(options.filter_aranet_only);
1090 }
1091
1092 #[test]
1093 fn test_scan_options_duration() {
1094 let options = ScanOptions::new().duration(Duration::from_secs(10));
1095 assert_eq!(options.duration, Duration::from_secs(10));
1096 }
1097
1098 #[test]
1099 fn test_scan_options_duration_secs() {
1100 let options = ScanOptions::new().duration_secs(15);
1101 assert_eq!(options.duration, Duration::from_secs(15));
1102 }
1103
1104 #[test]
1105 fn test_scan_options_filter_aranet_only() {
1106 let options = ScanOptions::new().filter_aranet_only(false);
1107 assert!(!options.filter_aranet_only);
1108
1109 let options = ScanOptions::new().filter_aranet_only(true);
1110 assert!(options.filter_aranet_only);
1111 }
1112
1113 #[test]
1114 fn test_scan_options_all_devices() {
1115 let options = ScanOptions::new().all_devices();
1116 assert!(!options.filter_aranet_only);
1117 }
1118
1119 #[test]
1120 fn test_scan_options_chaining() {
1121 let options = ScanOptions::new()
1122 .duration_secs(20)
1123 .filter_aranet_only(false);
1124
1125 assert_eq!(options.duration, Duration::from_secs(20));
1126 assert!(!options.filter_aranet_only);
1127 }
1128
1129 #[test]
1130 fn test_scan_options_clone() {
1131 let options1 = ScanOptions::new().duration_secs(8);
1132 let options2 = options1.clone();
1133
1134 assert_eq!(options1.duration, options2.duration);
1135 assert_eq!(options1.filter_aranet_only, options2.filter_aranet_only);
1136 }
1137
1138 #[test]
1139 fn test_scan_options_debug() {
1140 let options = ScanOptions::new();
1141 let debug = format!("{:?}", options);
1142 assert!(debug.contains("ScanOptions"));
1143 assert!(debug.contains("duration"));
1144 assert!(debug.contains("filter_aranet_only"));
1145 }
1146
1147 #[test]
1150 fn test_find_progress_cache_hit() {
1151 let progress = FindProgress::CacheHit;
1152 let debug = format!("{:?}", progress);
1153 assert!(debug.contains("CacheHit"));
1154 }
1155
1156 #[test]
1157 fn test_find_progress_scan_attempt() {
1158 let progress = FindProgress::ScanAttempt {
1159 attempt: 2,
1160 total: 3,
1161 duration_secs: 5,
1162 };
1163
1164 if let FindProgress::ScanAttempt {
1165 attempt,
1166 total,
1167 duration_secs,
1168 } = progress
1169 {
1170 assert_eq!(attempt, 2);
1171 assert_eq!(total, 3);
1172 assert_eq!(duration_secs, 5);
1173 } else {
1174 panic!("Expected ScanAttempt variant");
1175 }
1176 }
1177
1178 #[test]
1179 fn test_find_progress_found() {
1180 let progress = FindProgress::Found { attempt: 1 };
1181 assert!(matches!(progress, FindProgress::Found { attempt: 1 }));
1182 }
1183
1184 #[test]
1185 fn test_find_progress_retry_needed() {
1186 let progress = FindProgress::RetryNeeded { attempt: 2 };
1187 assert!(matches!(progress, FindProgress::RetryNeeded { attempt: 2 }));
1188 }
1189
1190 #[test]
1191 fn test_find_progress_clone() {
1192 let progress1 = FindProgress::ScanAttempt {
1193 attempt: 1,
1194 total: 3,
1195 duration_secs: 4,
1196 };
1197 let progress2 = progress1.clone();
1198
1199 assert!(matches!(
1200 (&progress1, &progress2),
1201 (
1202 FindProgress::ScanAttempt {
1203 attempt: 1,
1204 total: 3,
1205 duration_secs: 4,
1206 },
1207 FindProgress::ScanAttempt {
1208 attempt: 1,
1209 total: 3,
1210 duration_secs: 4,
1211 },
1212 )
1213 ));
1214 }
1215
1216 #[cfg(target_os = "linux")]
1229 #[test]
1230 #[ignore = "needs a system D-Bus (BlueZ not required)"]
1231 fn manager_still_answers_after_its_first_runtime_shuts_down() {
1232 let runtime = || {
1233 tokio::runtime::Builder::new_current_thread()
1234 .enable_all()
1235 .build()
1236 .unwrap()
1237 };
1238
1239 let first = runtime();
1240 let manager = first.block_on(shared_manager()).expect("manager");
1241 drop(first);
1242
1243 let second = runtime();
1244 let answered = second.block_on(async {
1245 tokio::time::timeout(Duration::from_secs(5), manager.adapters()).await
1246 });
1247 assert!(
1248 answered.is_ok(),
1249 "the manager's D-Bus connection died with the runtime that created it"
1250 );
1251 }
1252
1253 const TEST_LIMIT: Duration = Duration::from_secs(600);
1257
1258 fn secs(n: u64) -> Duration {
1259 Duration::from_secs(n)
1260 }
1261
1262 #[derive(Clone)]
1266 struct FakeScanner {
1267 started_at: tokio::time::Instant,
1268 log: Arc<std::sync::Mutex<Vec<(Duration, &'static str)>>>,
1269 scanning: Arc<AtomicBool>,
1270 fail_start: bool,
1271 stop_latency: Duration,
1274 on_stop: Option<std::sync::mpsc::Sender<()>>,
1275 }
1276
1277 impl FakeScanner {
1278 fn new() -> Self {
1279 Self {
1280 started_at: tokio::time::Instant::now(),
1281 log: Arc::default(),
1282 scanning: Arc::default(),
1283 fail_start: false,
1284 stop_latency: Duration::ZERO,
1285 on_stop: None,
1286 }
1287 }
1288
1289 fn record(&self, event: &'static str) {
1290 self.log
1291 .lock()
1292 .unwrap()
1293 .push((self.started_at.elapsed(), event));
1294 }
1295
1296 fn log(&self) -> Vec<(Duration, &'static str)> {
1297 self.log.lock().unwrap().clone()
1298 }
1299
1300 fn is_scanning(&self) -> bool {
1301 self.scanning.load(Ordering::SeqCst)
1302 }
1303 }
1304
1305 impl ScanControl for FakeScanner {
1306 async fn start(&self, _filter: ScanFilter) -> Result<()> {
1307 if self.fail_start {
1308 return Err(Error::InvalidData("start failed".into()));
1309 }
1310 if self.scanning.swap(true, Ordering::SeqCst) {
1311 return Err(Error::InvalidData("org.bluez.Error.InProgress".into()));
1312 }
1313 self.record("start");
1314 Ok(())
1315 }
1316
1317 async fn stop(&self) -> Result<()> {
1318 self.record("stop");
1319 sleep(self.stop_latency).await;
1320 self.scanning.store(false, Ordering::SeqCst);
1321 if let Some(on_stop) = &self.on_stop {
1322 let _ = on_stop.send(());
1323 }
1324 Ok(())
1325 }
1326 }
1327
1328 #[tokio::test(start_paused = true)]
1329 async fn scan_window_stops_the_scan_when_the_window_ends() {
1330 within(TEST_LIMIT, async {
1331 let rt = tokio::runtime::Handle::current();
1332 let lock = ScanLock::default();
1333 let scanner = FakeScanner::new();
1334
1335 let permit = lock.acquire().await;
1336 scan_window(&rt, &scanner, permit, ScanFilter::default(), secs(5))
1337 .await
1338 .unwrap();
1339
1340 assert_eq!(scanner.log(), [(secs(0), "start"), (secs(5), "stop")]);
1341 assert!(!scanner.is_scanning());
1342 })
1343 .await;
1344 }
1345
1346 #[tokio::test(start_paused = true)]
1347 async fn dropping_the_caller_stops_the_scan_immediately() {
1348 within(TEST_LIMIT, async {
1349 let rt = tokio::runtime::Handle::current();
1350 let lock = ScanLock::default();
1351 let scanner = FakeScanner::new();
1352
1353 let permit = lock.acquire().await;
1354 let window = scan_window(&rt, &scanner, permit, ScanFilter::default(), secs(5));
1355 assert!(tokio::time::timeout(secs(1), window).await.is_err());
1356 tokio::time::sleep(Duration::from_millis(10)).await;
1357
1358 assert_eq!(scanner.log(), [(secs(0), "start"), (secs(1), "stop")]);
1359 assert!(!scanner.is_scanning());
1360 })
1361 .await;
1362 }
1363
1364 #[tokio::test(start_paused = true)]
1365 async fn aborting_the_task_stops_the_scan() {
1366 within(TEST_LIMIT, async {
1367 let rt = tokio::runtime::Handle::current();
1368 let lock = ScanLock::default();
1369 let scanner = FakeScanner::new();
1370
1371 let task = tokio::spawn({
1373 let scanner = scanner.clone();
1374 let lock = lock.clone();
1375 async move {
1376 let permit = lock.acquire().await;
1377 scan_window(&rt, &scanner, permit, ScanFilter::default(), secs(5)).await
1378 }
1379 });
1380 tokio::time::sleep(secs(1)).await;
1381 task.abort();
1382 assert!(task.await.unwrap_err().is_cancelled());
1383 tokio::time::sleep(Duration::from_millis(10)).await;
1384
1385 assert_eq!(scanner.log(), [(secs(0), "start"), (secs(1), "stop")]);
1386 assert!(!scanner.is_scanning());
1387 })
1388 .await;
1389 }
1390
1391 #[tokio::test(start_paused = true)]
1392 async fn failed_start_releases_the_permit_without_stopping() {
1393 within(TEST_LIMIT, async {
1394 let rt = tokio::runtime::Handle::current();
1395 let lock = ScanLock::default();
1396 let scanner = FakeScanner {
1397 fail_start: true,
1398 ..FakeScanner::new()
1399 };
1400
1401 let permit = lock.acquire().await;
1402 let result = scan_window(&rt, &scanner, permit, ScanFilter::default(), secs(5)).await;
1403
1404 assert!(matches!(result, Err(Error::InvalidData(ref m)) if m == "start failed"));
1405 assert!(
1406 scanner.log().is_empty(),
1407 "a scan that never started must not be stopped"
1408 );
1409 within(secs(1), lock.acquire()).await;
1410 })
1411 .await;
1412 }
1413
1414 #[test]
1415 fn scan_stops_after_the_callers_runtime_shuts_down() {
1416 let (tx, rx) = std::sync::mpsc::channel();
1417 let caller = tokio::runtime::Builder::new_current_thread()
1418 .enable_all()
1419 .build()
1420 .unwrap();
1421
1422 caller.block_on(async {
1423 let lock = ScanLock::default();
1424 let scanner = FakeScanner {
1425 on_stop: Some(tx),
1426 ..FakeScanner::new()
1427 };
1428 let permit = lock.acquire().await;
1429 let window = run_scan(&scanner, permit, ScanFilter::default(), secs(10));
1431 let started = async {
1432 while !scanner.is_scanning() {
1433 tokio::time::sleep(Duration::from_millis(1)).await;
1434 }
1435 };
1436 tokio::select! {
1439 result = window => panic!("the 10 s window ended early: {result:?}"),
1440 started = tokio::time::timeout(secs(5), started) => {
1441 started.expect("the scan never started");
1442 }
1443 }
1444 });
1445 drop(caller);
1446
1447 rx.recv_timeout(secs(5))
1448 .expect("scan never stopped after the caller's runtime shut down");
1449 }
1450
1451 #[tokio::test(start_paused = true)]
1452 async fn concurrent_scans_run_one_after_another() {
1453 within(TEST_LIMIT, async {
1454 let rt = tokio::runtime::Handle::current();
1455 let lock = ScanLock::default();
1456 let scanner = FakeScanner {
1457 stop_latency: Duration::from_millis(100),
1458 ..FakeScanner::new()
1459 };
1460 let scan = || async {
1461 let permit = lock.acquire().await;
1462 scan_window(&rt, &scanner, permit, ScanFilter::default(), secs(2)).await
1463 };
1464
1465 let (first, second) = tokio::join!(scan(), scan());
1466
1467 first.expect("first scan");
1468 second.expect("second scan should wait for the first instead of failing");
1469 assert_eq!(
1470 scanner.log(),
1471 [
1472 (secs(0), "start"),
1473 (secs(2), "stop"),
1474 (Duration::from_millis(2100), "start"),
1475 (Duration::from_millis(4100), "stop"),
1476 ]
1477 );
1478 })
1479 .await;
1480 }
1481
1482 #[tokio::test(start_paused = true)]
1483 async fn next_scan_waits_until_a_cancelled_scan_has_stopped() {
1484 within(TEST_LIMIT, async {
1485 let rt = tokio::runtime::Handle::current();
1486 let lock = ScanLock::default();
1487 let scanner = FakeScanner::new();
1488
1489 let first = tokio::time::timeout(secs(1), async {
1490 let permit = lock.acquire().await;
1491 scan_window(&rt, &scanner, permit, ScanFilter::default(), secs(5)).await
1492 });
1493 let second = async {
1494 tokio::task::yield_now().await;
1495 let permit = lock.acquire().await;
1496 scan_window(&rt, &scanner, permit, ScanFilter::default(), secs(2)).await
1497 };
1498 let (first, second) = tokio::join!(first, second);
1499
1500 assert!(first.is_err(), "the first scan should have been cancelled");
1501 second.expect("the second scan should start after the first has stopped");
1502 assert_eq!(
1503 scanner.log(),
1504 [
1505 (secs(0), "start"),
1506 (secs(1), "stop"),
1507 (secs(1), "start"),
1508 (secs(3), "stop"),
1509 ]
1510 );
1511 })
1512 .await;
1513 }
1514
1515 #[tokio::test(start_paused = true)]
1516 async fn cancelling_while_waiting_for_the_permit_never_starts_a_scan() {
1517 within(TEST_LIMIT, async {
1518 let rt = tokio::runtime::Handle::current();
1519 let lock = ScanLock::default();
1520 let scanner = FakeScanner::new();
1521
1522 let permit = lock.acquire().await;
1523 let first = tokio::spawn({
1524 let (rt, scanner) = (rt.clone(), scanner.clone());
1525 async move {
1526 scan_window(&rt, &scanner, permit, ScanFilter::default(), secs(5)).await
1527 }
1528 });
1529 let second = tokio::time::timeout(secs(1), async {
1530 let permit = lock.acquire().await;
1531 scan_window(&rt, &scanner, permit, ScanFilter::default(), secs(1)).await
1532 });
1533 assert!(
1534 second.await.is_err(),
1535 "the second scan should still be waiting for the permit"
1536 );
1537 tokio::time::sleep(secs(6)).await;
1538 first.await.unwrap().unwrap();
1539 assert_eq!(scanner.log(), [(secs(0), "start"), (secs(5), "stop")]);
1540
1541 let permit = lock.acquire().await;
1544 let window = scan_window(&rt, &scanner, permit, ScanFilter::default(), secs(1));
1545 assert!(window.now_or_never().is_none());
1546 tokio::time::sleep(Duration::from_millis(10)).await;
1547 assert_eq!(scanner.log(), [(secs(0), "start"), (secs(5), "stop")]);
1548 within(secs(1), lock.acquire()).await;
1549 })
1550 .await;
1551 }
1552
1553 fn search_filters(windows: u32, aranet_only: bool) -> Vec<ScanFilter> {
1558 (1..=windows).map(|_| search_filter(aranet_only)).collect()
1559 }
1560
1561 #[test]
1562 fn search_filter_asks_only_for_aranet_sensors_in_every_window() {
1563 let aranet = ScanFilter {
1564 services: vec![SAF_TEHNIKA_SERVICE_NEW, SAF_TEHNIKA_SERVICE_OLD],
1565 };
1566 for windows in 1..=3 {
1567 assert_eq!(
1568 search_filters(windows, true),
1569 vec![aranet.clone(); windows as usize],
1570 "{windows}-window search"
1571 );
1572 }
1573 }
1574
1575 #[test]
1576 fn search_filter_asks_for_every_device_when_not_filtering() {
1577 for windows in 1..=3 {
1578 assert_eq!(
1579 search_filters(windows, false),
1580 vec![ScanFilter::default(); windows as usize],
1581 "{windows}-window search"
1582 );
1583 }
1584 }
1585
1586 #[test]
1589 fn a_search_fails_after_the_first_scan_that_finds_only_similar_names() {
1590 for attempt in 1..=3 {
1593 let similar = vec!["Aranet4 12345".to_string(), "Aranet4 1ABCD".to_string()];
1594 match after_missed_scan("Aranet4", similar, attempt, 3) {
1595 ControlFlow::Break(Error::DeviceNotFound(DeviceNotFoundReason::NoExactMatch {
1596 identifier,
1597 similar,
1598 })) => {
1599 assert_eq!(identifier, "Aranet4", "attempt {attempt}");
1600 assert_eq!(
1601 similar,
1602 ["Aranet4 12345", "Aranet4 1ABCD"],
1603 "attempt {attempt}"
1604 );
1605 }
1606 other => panic!("attempt {attempt} of 3: {other:?}"),
1607 }
1608 }
1609 }
1610
1611 #[test]
1612 fn a_search_without_similar_names_scans_until_its_last_attempt() {
1613 for attempt in 1..3 {
1614 let decision = after_missed_scan("Aranet4 12345", Vec::new(), attempt, 3);
1615 assert!(
1616 matches!(decision, ControlFlow::Continue(())),
1617 "attempt {attempt} of 3: {decision:?}"
1618 );
1619 }
1620 match after_missed_scan("Aranet4 12345", Vec::new(), 3, 3) {
1621 ControlFlow::Break(Error::DeviceNotFound(DeviceNotFoundReason::NotFound {
1622 identifier,
1623 })) => assert_eq!(identifier, "Aranet4 12345"),
1624 other => panic!("attempt 3 of 3: {other:?}"),
1625 }
1626 }
1627
1628 const UUID_1: &str = "1f8893bf-9f7e-02b4-ef4a-7718f4f5d4be";
1633 const UUID_2: &str = "387c18c7-299f-cc32-d01c-6cf29a8d3ca5";
1634 const UUID_3: &str = "5b0e4c1d-7a3f-4e2b-9c6d-8f1a2b3c4d5e";
1635 const UUID_4: &str = "c4a7e2d9-3b1f-4c8e-a6d5-2f9b8e7c1a04";
1636
1637 fn known(
1638 identifier: &str,
1639 peripheral_id: &str,
1640 address: &str,
1641 name: Option<&str>,
1642 ) -> KnownPeripheral {
1643 KnownPeripheral {
1644 identifier: identifier.to_string(),
1645 peripheral_id: peripheral_id.to_string(),
1646 address: address.to_string(),
1647 name: name.map(str::to_string),
1648 }
1649 }
1650
1651 fn on_macos(uuid: &str, name: &str) -> KnownPeripheral {
1653 known(uuid, uuid, UNKNOWN_ADDRESS, Some(name))
1654 }
1655
1656 fn on_linux(address: &str, name: &str) -> KnownPeripheral {
1659 let device_id = format!("hci0/dev_{}", address.replace(':', "_"));
1660 known(address, &device_id, address, Some(name))
1661 }
1662
1663 fn outcome(query: &str, known: &[KnownPeripheral]) -> (&'static str, Vec<String>) {
1666 let (kind, indices) = match lookup(query, known) {
1667 Lookup::Found(index) => ("found", vec![index]),
1668 Lookup::Ambiguous(indices) => ("ambiguous", indices),
1669 Lookup::NotFound { similar } => ("not found", similar),
1670 };
1671 let mut identifiers: Vec<String> = indices
1672 .iter()
1673 .map(|&index| known[index].identifier.clone())
1674 .collect();
1675 if kind == "ambiguous" {
1676 identifiers.sort();
1678 }
1679 (kind, identifiers)
1680 }
1681
1682 fn candidates(query: &str, known: &[KnownPeripheral]) -> Vec<String> {
1684 match resolve(query, known) {
1685 Err(Error::DeviceNotFound(crate::error::DeviceNotFoundReason::Ambiguous {
1686 identifier,
1687 candidates,
1688 })) => {
1689 assert_eq!(identifier, query);
1690 candidates
1691 }
1692 other => panic!("{query:?} is not ambiguous: {other:?}"),
1693 }
1694 }
1695
1696 #[test]
1697 fn lookup_does_not_match_part_of_a_name() {
1698 let devices = [
1699 on_macos(UUID_1, "Aranet4 12345"),
1700 on_macos(UUID_2, "Aranet4 1ABCD"),
1701 ];
1702 assert_eq!(
1703 lookup("Aranet4 1", &devices),
1704 Lookup::NotFound {
1705 similar: vec![0, 1]
1706 }
1707 );
1708 }
1709
1710 #[test]
1711 fn lookup_does_not_match_part_of_a_uuid() {
1712 let devices = [on_macos(UUID_1, "Aranet2 2751B")];
1713 for query in ["4", "1f8893bf"] {
1714 assert_eq!(
1715 lookup(query, &devices),
1716 Lookup::NotFound { similar: vec![] },
1717 "{query:?}"
1718 );
1719 }
1720 }
1721
1722 #[test]
1723 fn lookup_ignores_linux_object_path_fragments() {
1724 let devices = [on_linux("AA:BB:CC:DD:EE:FF", "Aranet4 12345")];
1725 for query in ["hci0", "dev"] {
1726 assert_eq!(
1727 lookup(query, &devices),
1728 Lookup::NotFound { similar: vec![] },
1729 "{query:?}"
1730 );
1731 }
1732 }
1733
1734 #[test]
1735 fn lookup_matches_the_bluez_device_id_display_form() {
1736 let devices = [
1739 on_linux("11:22:33:44:55:66", "Aranet2 2751B"),
1740 on_linux("AA:BB:CC:DD:EE:FF", "Aranet4 12345"),
1741 ];
1742 for query in ["hci0/dev_AA_BB_CC_DD_EE_FF", "hci0/dev_aa_bb_cc_dd_ee_ff"] {
1743 assert_eq!(lookup(query, &devices), Lookup::Found(1), "{query:?}");
1744 }
1745 }
1746
1747 #[test]
1748 fn lookup_matches_the_full_name_ignoring_case_and_whitespace() {
1749 let devices = [
1750 on_macos(UUID_2, "AranetRn+ 306B8"),
1751 on_macos(UUID_1, "Aranet2 2751B"),
1752 ];
1753 let query = parse_query(" aranet2 2751b ").unwrap();
1754 assert_eq!(lookup(query, &devices), Lookup::Found(1));
1755 }
1756
1757 #[test]
1758 fn lookup_matches_a_uuid_in_any_case() {
1759 let devices = [
1760 on_macos(UUID_2, "AranetRn+ 306B8"),
1761 on_macos(UUID_1, "Aranet2 2751B"),
1762 ];
1763 assert_eq!(
1764 lookup("1F8893BF-9F7E-02B4-EF4A-7718F4F5D4BE", &devices),
1765 Lookup::Found(1)
1766 );
1767 }
1768
1769 #[test]
1770 fn lookup_matches_an_address_with_or_without_colons() {
1771 let devices = [
1772 on_linux("11:22:33:44:55:66", "Aranet2 2751B"),
1773 on_linux("AA:BB:CC:DD:EE:FF", "Aranet4 12345"),
1774 ];
1775 for query in ["aa:bb:cc:dd:ee:ff", "AABBCCDDEEFF"] {
1776 assert_eq!(lookup(query, &devices), Lookup::Found(1), "{query:?}");
1777 }
1778 }
1779
1780 #[test]
1781 fn lookup_never_matches_the_all_zero_address() {
1782 let devices = [on_macos(UUID_1, "Aranet2 2751B")];
1783 for query in ["00:00:00:00:00:00", "000000000000"] {
1784 assert_eq!(
1785 lookup(query, &devices),
1786 Lookup::NotFound { similar: vec![] },
1787 "{query:?}"
1788 );
1789 }
1790 }
1791
1792 #[test]
1793 fn lookup_matches_either_half_of_a_corebluetooth_combined_name() {
1794 let devices = [
1795 on_macos(UUID_2, "AranetRn+ 306B8"),
1796 on_macos(UUID_1, "Kitchen [Aranet4 1A2B3]"),
1797 ];
1798 for query in ["Aranet4 1A2B3", "kitchen", "Kitchen [Aranet4 1A2B3]"] {
1799 assert_eq!(lookup(query, &devices), Lookup::Found(1), "{query:?}");
1800 }
1801 }
1802
1803 #[test]
1804 fn lookup_prefers_an_identifier_match_over_a_name_match() {
1805 let devices = [
1806 on_macos(UUID_1, "AA:BB:CC:DD:EE:FF"),
1807 on_linux("AA:BB:CC:DD:EE:FF", "Aranet4 12345"),
1808 ];
1809 assert_eq!(lookup("aa:bb:cc:dd:ee:ff", &devices), Lookup::Found(1));
1810 }
1811
1812 #[test]
1813 fn lookup_reports_duplicate_names_as_ambiguous() {
1814 let devices = [
1815 on_macos(UUID_1, "Aranet4 12345"),
1816 on_macos(UUID_2, "Aranet4 12345"),
1817 ];
1818 assert_eq!(
1819 lookup("Aranet4 12345", &devices),
1820 Lookup::Ambiguous(vec![0, 1])
1821 );
1822 }
1823
1824 #[test]
1825 fn lookup_suggests_only_aranet_names() {
1826 let devices = [
1828 on_macos(UUID_1, "Aranet4 12345"),
1829 on_macos(UUID_2, "Standing desk"),
1830 on_macos(UUID_3, "Aranet2 2751B"),
1831 ];
1832 assert_eq!(
1833 lookup("an", &devices),
1834 Lookup::NotFound {
1835 similar: vec![2, 0]
1836 }
1837 );
1838 }
1839
1840 fn orders(n: usize) -> Vec<Vec<usize>> {
1842 if n == 0 {
1843 return vec![Vec::new()];
1844 }
1845 let mut all = Vec::new();
1846 for shorter in orders(n - 1) {
1847 for at in 0..n {
1848 let mut order = shorter.clone();
1849 order.insert(at, n - 1);
1850 all.push(order);
1851 }
1852 }
1853 all
1854 }
1855
1856 #[test]
1857 fn lookup_result_is_independent_of_order() {
1858 let devices = [
1859 on_macos(UUID_1, "Aranet4 12345"),
1860 on_macos(UUID_2, "Aranet4 1ABCD"),
1861 on_macos(UUID_3, "Aranet2 2751B"),
1862 on_macos(UUID_4, "Aranet4 12345"),
1864 ];
1865 let cases = [
1866 ("Aranet4 1", "not found", vec![UUID_1, UUID_4, UUID_2]),
1867 ("aranet2 2751b", "found", vec![UUID_3]),
1868 ("Aranet4", "not found", vec![UUID_1, UUID_4, UUID_2]),
1869 ("Aranet4 12345", "ambiguous", vec![UUID_1, UUID_4]),
1870 ];
1871 for (query, kind, identifiers) in cases {
1872 let outcomes: Vec<(&str, Vec<String>)> = orders(devices.len())
1873 .iter()
1874 .map(|order| {
1875 let shuffled: Vec<KnownPeripheral> =
1876 order.iter().map(|&index| devices[index].clone()).collect();
1877 outcome(query, &shuffled)
1878 })
1879 .collect();
1880 assert!(
1881 outcomes.iter().all(|answer| *answer == outcomes[0]),
1882 "the answer for {query:?} depends on the order: {outcomes:?}"
1883 );
1884 assert_eq!(outcomes[0].0, kind, "{query:?}");
1885 assert_eq!(outcomes[0].1, identifiers, "{query:?}");
1886 }
1887 }
1888
1889 #[test]
1890 fn resolve_lists_candidates_and_similar_names_sorted() {
1891 let same_name = [
1893 on_macos(UUID_2, "Aranet4 12345"),
1894 on_macos(UUID_1, "Aranet4 12345"),
1895 ];
1896 assert_eq!(
1897 candidates("Aranet4 12345", &same_name),
1898 [
1899 format!("Aranet4 12345 ({UUID_1})"),
1900 format!("Aranet4 12345 ({UUID_2})"),
1901 ]
1902 );
1903
1904 let one_address = [
1906 known(
1907 "AA:BB:CC:DD:EE:FF",
1908 "hci1/dev_AA_BB_CC_DD_EE_FF",
1909 "AA:BB:CC:DD:EE:FF",
1910 None,
1911 ),
1912 on_linux("AA:BB:CC:DD:EE:FF", "Aranet4 12345"),
1913 ];
1914 assert_eq!(
1915 candidates("aa:bb:cc:dd:ee:ff", &one_address),
1916 [
1917 "Aranet4 12345 (AA:BB:CC:DD:EE:FF)",
1918 "unnamed (AA:BB:CC:DD:EE:FF)",
1919 ]
1920 );
1921
1922 let similar = [
1924 on_macos(UUID_1, "Aranet4 1ABCD "),
1925 on_macos(UUID_2, "Aranet4 12345"),
1926 on_macos(UUID_3, "Aranet4 1ABCD"),
1927 ];
1928 assert_eq!(
1929 resolve("aranet4 1", &similar).unwrap(),
1930 Search::Missing {
1931 similar: vec!["Aranet4 12345".to_string(), "Aranet4 1ABCD".to_string()]
1932 }
1933 );
1934 }
1935
1936 #[test]
1937 fn parse_query_rejects_empty_and_blank_identifiers() {
1938 for identifier in ["", " ", "\t\n"] {
1939 assert!(
1940 matches!(parse_query(identifier), Err(Error::InvalidConfig(_))),
1941 "{identifier:?}"
1942 );
1943 }
1944 assert_eq!(parse_query(" x ").unwrap(), "x");
1945 }
1946
1947 #[tokio::test]
1948 async fn find_device_rejects_an_empty_identifier_without_bluetooth() {
1949 let found = within(Duration::from_secs(5), find_device("")).await;
1956 assert!(
1957 matches!(found, Err(Error::InvalidConfig(_))),
1958 "{:?}",
1959 found.err()
1960 );
1961
1962 let connected = within(Duration::from_secs(5), crate::device::Device::connect(" ")).await;
1963 assert!(
1964 matches!(connected, Err(Error::InvalidConfig(_))),
1965 "{:?}",
1966 connected.err()
1967 );
1968 }
1969
1970 }