| // Copyright 2019 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| use { |
| super::*, |
| crate::{ |
| client::{ConnectFailure, ConnectResult}, |
| Ssid, |
| }, |
| fidl_fuchsia_wlan_mlme as fidl_mlme, fuchsia_zircon as zx, |
| std::collections::VecDeque, |
| thiserror::Error, |
| wlan_rsn::{ |
| key::exchange::Key, |
| rsna::{SecAssocStatus, SecAssocUpdate, UpdateSink}, |
| }, |
| }; |
| |
| #[derive(Default)] |
| pub(crate) struct StatsCollector { |
| discovery_scan_stats: Option<PendingScanStats>, |
| /// Track successive connect attempts to the same SSID. This resets when attempt succeeds |
| /// or when attempting to connect to a different SSID from a previous attempt. |
| connect_attempts: Option<ConnectAttempts>, |
| /// Track the most recent disconnection. Intended to be sent out on connection success |
| /// so that client can compute time gap between last disconnect until reconnect. |
| /// This is cleared out as soon as a connection attempt succeeds. |
| previous_disconnect_info: Option<PreviousDisconnectInfo>, |
| } |
| |
| impl StatsCollector { |
| pub fn report_join_scan_started( |
| &mut self, |
| req: fidl_mlme::ScanRequest, |
| is_connected: bool, |
| ) -> Result<(), StatsError> { |
| let now = now(); |
| let pending_scan_stats = |
| PendingScanStats { scan_start_at: now, req, scan_start_while_connected: is_connected }; |
| self.connect_stats()?.pending_scan_stats.replace(pending_scan_stats); |
| Ok(()) |
| } |
| |
| /// Report the start of a new discovery scan so that StatsCollector will begin collecting |
| /// stats for it. If there's an existing pending scan stats, evict and return it. |
| pub fn report_discovery_scan_started( |
| &mut self, |
| req: fidl_mlme::ScanRequest, |
| is_connected: bool, |
| ) -> Option<PendingScanStats> { |
| let now = now(); |
| let pending_scan_stats = |
| PendingScanStats { scan_start_at: now, req, scan_start_while_connected: is_connected }; |
| self.discovery_scan_stats.replace(pending_scan_stats) |
| } |
| |
| pub fn report_discovery_scan_ended( |
| &mut self, |
| result: ScanResult, |
| bss_list: Option<&Vec<fidl_mlme::BssDescription>>, |
| ) -> Result<ScanStats, StatsError> { |
| let now = now(); |
| let pending_stats = self.discovery_scan_stats.take().ok_or(StatsError::NoPendingScan)?; |
| |
| let scan_stats = ScanStats { |
| scan_type: pending_stats.req.scan_type, |
| scan_start_while_connected: pending_stats.scan_start_while_connected, |
| scan_start_at: pending_stats.scan_start_at, |
| scan_end_at: now, |
| result, |
| bss_count: bss_list.map(|bss_list| bss_list.len()).unwrap_or(0), |
| }; |
| Ok(scan_stats) |
| } |
| |
| pub fn report_join_scan_ended( |
| &mut self, |
| result: ScanResult, |
| bss_count: usize, |
| ) -> Result<(), StatsError> { |
| let stats = ScanEndStats { scan_end_at: now(), result, bss_count }; |
| self.connect_stats()?.scan_end_stats.replace(stats); |
| Ok(()) |
| } |
| |
| /// Report the start of a new connect attempt so that StatsCollector will begin collecting |
| /// stats for it. If there's an existing pending connect stats, evict and return it. |
| pub fn report_connect_started(&mut self, ssid: Ssid) -> Option<PendingConnectStats> { |
| match self.connect_attempts.as_mut() { |
| Some(connect_attempts) => connect_attempts.update_with_new_attempt(ssid), |
| None => { |
| self.connect_attempts = Some(ConnectAttempts::new(ssid)); |
| None |
| } |
| } |
| } |
| |
| pub fn report_candidate_network( |
| &mut self, |
| desc: fidl_mlme::BssDescription, |
| ) -> Result<(), StatsError> { |
| self.connect_stats()?.candidate_network.replace(desc); |
| Ok(()) |
| } |
| |
| pub fn report_auth_started(&mut self) -> Result<(), StatsError> { |
| self.connect_stats()?.auth_start_at.replace(now()); |
| Ok(()) |
| } |
| |
| pub fn report_assoc_started(&mut self) -> Result<(), StatsError> { |
| let pending_stats = self.connect_stats()?; |
| let now = now(); |
| pending_stats.auth_end_at.replace(now); |
| pending_stats.assoc_start_at.replace(now); |
| Ok(()) |
| } |
| |
| pub fn report_assoc_success(&mut self) -> Result<(), StatsError> { |
| self.connect_stats()?.assoc_end_at.replace(now()); |
| Ok(()) |
| } |
| |
| pub fn report_rsna_started(&mut self) -> Result<(), StatsError> { |
| self.connect_stats()?.rsna_start_at.replace(now()); |
| Ok(()) |
| } |
| |
| pub fn report_rsna_established(&mut self) -> Result<(), StatsError> { |
| self.connect_stats()?.rsna_end_at.replace(now()); |
| Ok(()) |
| } |
| |
| /// Report updates derived from the supplicant. Used to record progress of establish RSNA step. |
| pub fn report_supplicant_updates( |
| &mut self, |
| update_sink: &UpdateSink, |
| ) -> Result<(), StatsError> { |
| let supplicant_progress = |
| self.connect_stats()?.supplicant_progress.get_or_insert(SupplicantProgress::default()); |
| for update in update_sink { |
| match update { |
| SecAssocUpdate::Status(status) => match status { |
| SecAssocStatus::PmkSaEstablished => { |
| supplicant_progress.pmksa_established = true |
| } |
| SecAssocStatus::EssSaEstablished => { |
| supplicant_progress.esssa_established = true |
| } |
| _ => (), |
| }, |
| SecAssocUpdate::Key(key) => match key { |
| Key::Ptk(..) => supplicant_progress.ptksa_established = true, |
| Key::Gtk(..) => supplicant_progress.gtksa_established = true, |
| _ => (), |
| }, |
| _ => (), |
| } |
| } |
| Ok(()) |
| } |
| |
| pub fn report_supplicant_error(&mut self, error: anyhow::Error) -> Result<(), StatsError> { |
| self.connect_stats()?.supplicant_error.replace(error); |
| Ok(()) |
| } |
| |
| pub fn report_key_exchange_timeout(&mut self) -> Result<(), StatsError> { |
| self.connect_stats()?.num_rsna_key_frame_exchange_timeout += 1; |
| Ok(()) |
| } |
| |
| pub fn report_connect_finished( |
| &mut self, |
| result: ConnectResult, |
| ) -> Result<ConnectStats, StatsError> { |
| let mut connect_attempts = |
| self.connect_attempts.take().ok_or(StatsError::NoPendingConnect)?; |
| let stats = self.finalize_connect_stats(&mut connect_attempts, result.clone())?; |
| |
| if result != ConnectResult::Success { |
| // Successive connect attempts are still tracked unless connection is successful |
| self.connect_attempts.replace(connect_attempts); |
| } |
| Ok(stats) |
| } |
| |
| fn connect_stats(&mut self) -> Result<&mut PendingConnectStats, StatsError> { |
| self.connect_attempts |
| .as_mut() |
| .and_then(|a| a.stats.as_mut()) |
| .ok_or(StatsError::NoPendingConnect) |
| } |
| |
| fn finalize_connect_stats( |
| &mut self, |
| connect_attempts: &mut ConnectAttempts, |
| result: ConnectResult, |
| ) -> Result<ConnectStats, StatsError> { |
| let now = now(); |
| let pending_stats = connect_attempts.handle_result(&result, now)?; |
| let previous_disconnect_info = match &result { |
| ConnectResult::Success => self.previous_disconnect_info.take(), |
| _ => None, |
| }; |
| |
| Ok(ConnectStats { |
| connect_start_at: pending_stats.connect_start_at, |
| connect_end_at: now, |
| scan_start_stats: pending_stats.pending_scan_stats.map(|stats| ScanStartStats { |
| scan_start_at: stats.scan_start_at, |
| scan_type: stats.req.scan_type, |
| scan_start_while_connected: stats.scan_start_while_connected, |
| }), |
| scan_end_stats: pending_stats.scan_end_stats, |
| auth_start_at: pending_stats.auth_start_at, |
| auth_end_at: pending_stats.auth_end_at, |
| assoc_start_at: pending_stats.assoc_start_at, |
| assoc_end_at: pending_stats.assoc_end_at, |
| rsna_start_at: pending_stats.rsna_start_at, |
| rsna_end_at: pending_stats.rsna_end_at, |
| result, |
| candidate_network: pending_stats.candidate_network, |
| supplicant_error: pending_stats.supplicant_error, |
| supplicant_progress: pending_stats.supplicant_progress, |
| num_rsna_key_frame_exchange_timeout: pending_stats.num_rsna_key_frame_exchange_timeout, |
| attempts: connect_attempts.attempts, |
| last_ten_failures: connect_attempts.last_ten_failures.iter().cloned().collect(), |
| previous_disconnect_info, |
| }) |
| } |
| |
| pub fn report_disconnect(&mut self, ssid: Ssid, source: DisconnectSource) { |
| self.previous_disconnect_info.replace(PreviousDisconnectInfo { |
| ssid, |
| disconnect_source: source, |
| disconnect_at: now(), |
| }); |
| } |
| } |
| |
| fn now() -> zx::Time { |
| zx::Time::get_monotonic() |
| } |
| |
| struct ConnectAttempts { |
| ssid: Ssid, |
| attempts: u32, |
| last_ten_failures: VecDeque<ConnectFailure>, |
| stats: Option<PendingConnectStats>, |
| } |
| |
| impl ConnectAttempts { |
| const MAX_FAILURES_TRACKED: usize = 10; |
| |
| pub fn new(ssid: Ssid) -> Self { |
| Self { |
| ssid, |
| attempts: 1, |
| last_ten_failures: VecDeque::with_capacity(Self::MAX_FAILURES_TRACKED), |
| stats: Some(PendingConnectStats::new(now())), |
| } |
| } |
| |
| /// Increment attempt counter if same SSID. Otherwise, reset and start tracking connect |
| /// attempts for the new SSID. |
| /// |
| /// Additionally, evict and return any existing PendingConnectStats. |
| pub fn update_with_new_attempt(&mut self, ssid: Ssid) -> Option<PendingConnectStats> { |
| if self.ssid == ssid { |
| self.attempts += 1; |
| } else { |
| self.ssid = ssid; |
| self.attempts = 1; |
| self.last_ten_failures.clear(); |
| } |
| self.stats.replace(PendingConnectStats::new(now())) |
| } |
| |
| pub fn handle_result( |
| &mut self, |
| result: &ConnectResult, |
| now: zx::Time, |
| ) -> Result<PendingConnectStats, StatsError> { |
| let mut pending_stats = self.stats.take().ok_or(StatsError::NoPendingConnect)?; |
| match result { |
| ConnectResult::Failed(failure) => match failure { |
| ConnectFailure::ScanFailure(code) => { |
| pending_stats.scan_end_stats.replace(ScanEndStats { |
| scan_end_at: now, |
| result: ScanResult::Failed(*code), |
| bss_count: 0, |
| }); |
| } |
| ConnectFailure::AuthenticationFailure(..) => { |
| pending_stats.auth_end_at.replace(now); |
| } |
| ConnectFailure::AssociationFailure(..) => { |
| pending_stats.assoc_end_at.replace(now); |
| } |
| ConnectFailure::EstablishRsnaFailure(..) => { |
| pending_stats.rsna_end_at.replace(now); |
| } |
| _ => (), |
| }, |
| _ => (), |
| } |
| |
| if let ConnectResult::Failed(failure) = result { |
| if self.last_ten_failures.len() >= Self::MAX_FAILURES_TRACKED { |
| self.last_ten_failures.pop_front(); |
| } |
| self.last_ten_failures.push_back(failure.clone()); |
| } |
| |
| Ok(pending_stats) |
| } |
| } |
| |
| #[derive(Debug, Error)] |
| pub(crate) enum StatsError { |
| #[error("no current pending connect")] |
| NoPendingConnect, |
| #[error("no current pending scan")] |
| NoPendingScan, |
| } |
| |
| pub(crate) struct PendingScanStats { |
| scan_start_at: zx::Time, |
| req: fidl_mlme::ScanRequest, |
| scan_start_while_connected: bool, |
| } |
| |
| pub(crate) struct PendingConnectStats { |
| connect_start_at: zx::Time, |
| pending_scan_stats: Option<PendingScanStats>, |
| scan_end_stats: Option<ScanEndStats>, |
| auth_start_at: Option<zx::Time>, |
| auth_end_at: Option<zx::Time>, |
| assoc_start_at: Option<zx::Time>, |
| assoc_end_at: Option<zx::Time>, |
| rsna_start_at: Option<zx::Time>, |
| rsna_end_at: Option<zx::Time>, |
| candidate_network: Option<fidl_mlme::BssDescription>, |
| supplicant_error: Option<anyhow::Error>, |
| supplicant_progress: Option<SupplicantProgress>, |
| num_rsna_key_frame_exchange_timeout: u32, |
| } |
| |
| impl PendingConnectStats { |
| fn new(connect_start_at: zx::Time) -> Self { |
| Self { |
| connect_start_at, |
| pending_scan_stats: None, |
| scan_end_stats: None, |
| auth_start_at: None, |
| auth_end_at: None, |
| assoc_start_at: None, |
| assoc_end_at: None, |
| rsna_start_at: None, |
| rsna_end_at: None, |
| candidate_network: None, |
| supplicant_error: None, |
| supplicant_progress: None, |
| num_rsna_key_frame_exchange_timeout: 0, |
| } |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use { |
| super::*, |
| crate::client::{ |
| test_utils::fake_scan_request, EstablishRsnaFailure, EstablishRsnaFailureReason, |
| SelectNetworkFailure, |
| }, |
| anyhow::format_err, |
| wlan_common::{assert_variant, fake_bss}, |
| wlan_rsn::auth, |
| }; |
| |
| const SSID: &[u8; 3] = b"foo"; |
| |
| #[test] |
| fn test_discovery_scan_stats_lifecycle() { |
| let mut stats_collector = StatsCollector::default(); |
| let req = fake_scan_request(); |
| let is_connected = true; |
| assert!(stats_collector.report_discovery_scan_started(req, is_connected).is_none()); |
| |
| let bss_desc = fake_bss!(Open, ssid: SSID.to_vec(), rates: vec![12]); |
| let stats = |
| stats_collector.report_discovery_scan_ended(ScanResult::Success, Some(&vec![bss_desc])); |
| assert_variant!(stats, Ok(scan_stats) => { |
| assert!(scan_stats.scan_time().into_nanos() > 0); |
| assert_eq!(scan_stats.scan_type, fidl_mlme::ScanTypes::Active); |
| assert_eq!(scan_stats.scan_start_while_connected, is_connected); |
| assert_eq!(scan_stats.result, ScanResult::Success); |
| assert_eq!(scan_stats.bss_count, 1); |
| }) |
| } |
| |
| #[test] |
| fn test_connect_stats_lifecycle() { |
| let mut stats_collector = StatsCollector::default(); |
| let stats = simulate_connect_lifecycle(&mut stats_collector); |
| |
| assert_variant!(stats, Ok(stats) => { |
| assert!(stats.connect_time().into_nanos() > 0); |
| assert_variant!(stats.join_scan_stats(), Some(scan_stats) => { |
| assert!(scan_stats.scan_time().into_nanos() > 0); |
| assert_eq!(scan_stats.scan_type, fidl_mlme::ScanTypes::Active); |
| assert_eq!(scan_stats.scan_start_while_connected, false); |
| assert_eq!(scan_stats.result, ScanResult::Success); |
| assert_eq!(scan_stats.bss_count, 1); |
| }); |
| assert!(stats.auth_time().is_some()); |
| assert!(stats.assoc_time().is_some()); |
| assert!(stats.rsna_time().is_some()); |
| assert_eq!(stats.result, ConnectResult::Success); |
| let bss_desc = fake_bss!(Wpa2, ssid: SSID.to_vec()); |
| assert_eq!(stats.candidate_network, Some(bss_desc)); |
| }); |
| } |
| |
| #[test] |
| fn test_connect_stats_finalized_midway() { |
| let mut stats_collector = StatsCollector::default(); |
| |
| assert!(stats_collector.report_connect_started(b"foo".to_vec()).is_none()); |
| let scan_req = fake_scan_request(); |
| assert!(stats_collector.report_join_scan_started(scan_req, false).is_ok()); |
| assert!(stats_collector.report_join_scan_ended(ScanResult::Success, 1).is_ok()); |
| let bss_desc = fake_bss!(Wpa2, ssid: SSID.to_vec()); |
| assert!(stats_collector.report_candidate_network(bss_desc).is_ok()); |
| assert!(stats_collector.report_auth_started().is_ok()); |
| let result = ConnectResult::Failed(ConnectFailure::AuthenticationFailure( |
| fidl_mlme::AuthenticateResultCodes::Refused, |
| )); |
| let stats = stats_collector.report_connect_finished(result.clone()); |
| |
| assert_variant!(stats, Ok(stats) => { |
| assert!(stats.connect_time().into_nanos() > 0); |
| assert!(stats.join_scan_stats().is_some()); |
| assert!(stats.auth_time().is_some()); |
| assert!(stats.assoc_time().is_none()); |
| assert!(stats.rsna_time().is_none()); |
| assert_eq!(stats.result, result); |
| assert!(stats.candidate_network.is_some()); |
| }); |
| } |
| |
| #[test] |
| fn test_connect_stats_establish_rsna_failure() { |
| let mut stats_collector = StatsCollector::default(); |
| |
| assert!(stats_collector.report_connect_started(b"foo".to_vec()).is_none()); |
| // Connecting should complete other steps first before starting establish RSNA step, |
| // but for testing starting with RSNA step right away is sufficient. |
| assert!(stats_collector.report_rsna_started().is_ok()); |
| let update_sink = vec![SecAssocUpdate::Status(SecAssocStatus::PmkSaEstablished)]; |
| assert!(stats_collector.report_supplicant_updates(&update_sink).is_ok()); |
| assert!(stats_collector.report_key_exchange_timeout().is_ok()); |
| assert!(stats_collector.report_key_exchange_timeout().is_ok()); |
| |
| assert!(stats_collector.report_supplicant_error(format_err!("blah")).is_ok()); |
| let stats = stats_collector.report_connect_finished( |
| EstablishRsnaFailure { |
| auth_method: Some(auth::MethodName::Psk), |
| reason: EstablishRsnaFailureReason::OverallTimeout, |
| } |
| .into(), |
| ); |
| |
| assert_variant!(stats, Ok(stats) => { |
| assert!(stats.supplicant_error.is_some()); |
| assert_variant!(stats.supplicant_progress, Some(SupplicantProgress { |
| pmksa_established: true, |
| ptksa_established: false, |
| gtksa_established: false, |
| esssa_established: false, |
| })); |
| assert_eq!(stats.num_rsna_key_frame_exchange_timeout, 2); |
| }); |
| } |
| |
| #[test] |
| fn test_consecutive_connect_attempts_stats() { |
| let mut stats_collector = StatsCollector::default(); |
| |
| assert!(stats_collector.report_connect_started(b"foo".to_vec()).is_none()); |
| let failure1: ConnectFailure = SelectNetworkFailure::NoScanResultWithSsid.into(); |
| let stats = stats_collector.report_connect_finished(failure1.clone().into()); |
| assert_variant!(stats, Ok(stats) => { |
| assert_eq!(stats.attempts, 1); |
| assert_eq!(stats.last_ten_failures, &[failure1.clone()]) |
| }); |
| |
| assert!(stats_collector.report_connect_started(b"foo".to_vec()).is_none()); |
| let failure2: ConnectFailure = EstablishRsnaFailure { |
| auth_method: Some(auth::MethodName::Psk), |
| reason: EstablishRsnaFailureReason::OverallTimeout, |
| } |
| .into(); |
| let stats = stats_collector.report_connect_finished(failure2.clone().into()); |
| assert_variant!(stats, Ok(stats) => { |
| assert_eq!(stats.attempts, 2); |
| assert_eq!(stats.last_ten_failures, &[failure1.clone(), failure2.clone()]); |
| }); |
| |
| assert!(stats_collector.report_connect_started(b"foo".to_vec()).is_none()); |
| let stats = stats_collector.report_connect_finished(ConnectResult::Success); |
| assert_variant!(stats, Ok(stats) => { |
| assert_eq!(stats.attempts, 3); |
| assert_eq!(stats.last_ten_failures, &[failure1.clone(), failure2.clone()]); |
| }); |
| |
| // After a successful connection, new connect attempts tracking is reset |
| assert!(stats_collector.report_connect_started(b"foo".to_vec()).is_none()); |
| let stats = stats_collector.report_connect_finished(ConnectResult::Success); |
| assert_variant!(stats, Ok(stats) => { |
| assert_eq!(stats.attempts, 1); |
| assert_eq!(stats.last_ten_failures, &[]) |
| }); |
| } |
| |
| #[test] |
| fn test_consecutive_connect_attempts_different_ssid_resets_stats() { |
| let mut stats_collector = StatsCollector::default(); |
| |
| assert!(stats_collector.report_connect_started(b"foo".to_vec()).is_none()); |
| let failure1: ConnectFailure = SelectNetworkFailure::NoScanResultWithSsid.into(); |
| let _stats = stats_collector.report_connect_finished(failure1.clone().into()); |
| |
| assert!(stats_collector.report_connect_started(b"bar".to_vec()).is_none()); |
| let failure2: ConnectFailure = EstablishRsnaFailure { |
| auth_method: Some(auth::MethodName::Psk), |
| reason: EstablishRsnaFailureReason::OverallTimeout, |
| } |
| .into(); |
| let stats = stats_collector.report_connect_finished(failure2.clone().into()); |
| assert_variant!(stats, Ok(stats) => { |
| assert_eq!(stats.attempts, 1); |
| assert_eq!(stats.last_ten_failures, &[failure2.clone()]); |
| }); |
| } |
| |
| #[test] |
| fn test_consecutive_connect_attempts_only_ten_failures_are_tracked() { |
| let mut stats_collector = StatsCollector::default(); |
| for i in 1..=20 { |
| assert!(stats_collector.report_connect_started(b"foo".to_vec()).is_none()); |
| let stats = stats_collector |
| .report_connect_finished(SelectNetworkFailure::NoScanResultWithSsid.into()); |
| assert_variant!(stats, Ok(stats) => { |
| assert_eq!(stats.attempts, i); |
| assert_eq!(stats.last_ten_failures.len(), std::cmp::min(i as usize, 10)); |
| }); |
| } |
| } |
| |
| #[test] |
| fn test_disconnect_then_reconnect() { |
| let mut stats_collector = StatsCollector::default(); |
| let stats = simulate_connect_lifecycle(&mut stats_collector); |
| assert_variant!(stats, Ok(stats) => stats.previous_disconnect_info.is_none()); |
| |
| stats_collector.report_disconnect(b"foo".to_vec(), DisconnectSource::User); |
| let stats = simulate_connect_lifecycle(&mut stats_collector); |
| assert_variant!(stats, Ok(stats) => { |
| assert_variant!(stats.previous_disconnect_info, Some(info) => { |
| assert_eq!(info.disconnect_source, DisconnectSource::User); |
| }) |
| }); |
| } |
| |
| #[test] |
| fn test_disconnect_then_connect_fails_before_succeeding() { |
| let mut stats_collector = StatsCollector::default(); |
| |
| // Connects then disconnect |
| let stats = simulate_connect_lifecycle(&mut stats_collector); |
| assert_variant!(stats, Ok(stats) => stats.previous_disconnect_info.is_none()); |
| stats_collector.report_disconnect(b"foo".to_vec(), DisconnectSource::User); |
| |
| // Attempt to connect but fails |
| assert!(stats_collector.report_connect_started(b"foo".to_vec()).is_none()); |
| let scan_req = fake_scan_request(); |
| assert!(stats_collector.report_join_scan_started(scan_req, false).is_ok()); |
| let failure = ConnectFailure::ScanFailure(fidl_mlme::ScanResultCodes::InternalError).into(); |
| let stats = stats_collector.report_connect_finished(failure); |
| |
| // Previous disconnect info should not be reported on fail attempt |
| assert_variant!(stats, Ok(stats) => stats.previous_disconnect_info.is_none()); |
| |
| // Connect now succeeds, hence previous disconnect info is reported |
| let stats = simulate_connect_lifecycle(&mut stats_collector); |
| assert_variant!(stats, Ok(stats) => { |
| assert_variant!(stats.previous_disconnect_info, Some(info) => { |
| assert_eq!(info.disconnect_source, DisconnectSource::User); |
| }) |
| }); |
| } |
| |
| #[test] |
| fn test_no_pending_discovery_scan_stats() { |
| let mut stats_collector = StatsCollector::default(); |
| let bss_desc = fake_bss!(Wpa2, ssid: SSID.to_vec()); |
| let stats = |
| stats_collector.report_discovery_scan_ended(ScanResult::Success, Some(&vec![bss_desc])); |
| assert_variant!(stats, Err(StatsError::NoPendingScan)); |
| } |
| |
| #[test] |
| fn test_no_pending_connect_stats() { |
| assert_variant!( |
| StatsCollector::default().report_join_scan_ended(ScanResult::Success, 1), |
| Err(StatsError::NoPendingConnect) |
| ); |
| let bss_desc = fake_bss!(Wpa2, ssid: SSID.to_vec()); |
| assert_variant!( |
| StatsCollector::default().report_candidate_network(bss_desc), |
| Err(StatsError::NoPendingConnect) |
| ); |
| assert_variant!( |
| StatsCollector::default().report_auth_started(), |
| Err(StatsError::NoPendingConnect) |
| ); |
| assert_variant!( |
| StatsCollector::default().report_assoc_started(), |
| Err(StatsError::NoPendingConnect) |
| ); |
| assert_variant!( |
| StatsCollector::default().report_assoc_success(), |
| Err(StatsError::NoPendingConnect) |
| ); |
| assert_variant!( |
| StatsCollector::default().report_rsna_started(), |
| Err(StatsError::NoPendingConnect) |
| ); |
| assert_variant!( |
| StatsCollector::default().report_rsna_established(), |
| Err(StatsError::NoPendingConnect) |
| ); |
| assert_variant!( |
| StatsCollector::default().report_connect_finished(ConnectResult::Success), |
| Err(StatsError::NoPendingConnect) |
| ); |
| } |
| |
| fn simulate_connect_lifecycle( |
| stats_collector: &mut StatsCollector, |
| ) -> Result<ConnectStats, StatsError> { |
| assert!(stats_collector.report_connect_started(SSID.to_vec()).is_none()); |
| let scan_req = fake_scan_request(); |
| assert!(stats_collector.report_join_scan_started(scan_req, false).is_ok()); |
| assert!(stats_collector.report_join_scan_ended(ScanResult::Success, 1).is_ok()); |
| let bss_desc = fake_bss!(Wpa2, ssid: SSID.to_vec()); |
| assert!(stats_collector.report_candidate_network(bss_desc).is_ok()); |
| assert!(stats_collector.report_auth_started().is_ok()); |
| assert!(stats_collector.report_assoc_started().is_ok()); |
| assert!(stats_collector.report_assoc_success().is_ok()); |
| assert!(stats_collector.report_rsna_started().is_ok()); |
| assert!(stats_collector.report_rsna_established().is_ok()); |
| stats_collector.report_connect_finished(ConnectResult::Success) |
| } |
| } |