Skip to main content

wlan_telemetry/processors/
connect_disconnect.rs

1// Copyright 2024 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use crate::convert::{convert_channel_band, convert_security_type};
6use crate::processors::toggle_events::ClientConnectionsToggleEvent;
7use crate::util::cobalt_logger::log_cobalt_batch;
8use derivative::Derivative;
9use fidl_fuchsia_metrics::{MetricEvent, MetricEventPayload};
10use fidl_fuchsia_wlan_ieee80211 as fidl_ieee80211;
11use fidl_fuchsia_wlan_sme as fidl_sme;
12use fuchsia_async as fasync;
13use fuchsia_inspect::Node as InspectNode;
14use fuchsia_inspect_contrib::id_enum::IdEnum;
15use fuchsia_inspect_contrib::inspect_log;
16use fuchsia_inspect_contrib::nodes::{BoundedListNode, LruCacheNode};
17use fuchsia_inspect_derive::Unit;
18use fuchsia_sync::Mutex;
19use ieee80211::OuiFmt;
20use std::sync::Arc;
21use std::sync::atomic::{AtomicUsize, Ordering};
22use strum_macros::{Display, EnumIter};
23use windowed_stats::experimental::inspect::{InspectSender, InspectedTimeMatrix};
24use windowed_stats::experimental::series::interpolation::{ConstantSample, LastSample};
25use windowed_stats::experimental::series::metadata::{BitSetMap, BitSetNode};
26use windowed_stats::experimental::series::statistic::Union;
27use windowed_stats::experimental::series::{SamplingProfile, TimeMatrix};
28use wlan_common::bss::BssDescription;
29use wlan_common::channel::Channel;
30use wlan_legacy_metrics_registry as metrics;
31use zx;
32
33const INSPECT_CONNECT_EVENTS_LIMIT: usize = 10;
34const INSPECT_DISCONNECT_EVENTS_LIMIT: usize = 20;
35const INSPECT_CONNECT_ATTEMPT_RESULTS_LIMIT: usize = 50;
36const INSPECT_CONNECTED_NETWORKS_ID_LIMIT: usize = 16;
37const INSPECT_DISCONNECT_SOURCES_ID_LIMIT: usize = 32;
38const INSPECT_CONNECT_ATTEMPT_RESULTS_ID_LIMIT: usize = 32;
39const SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_TIMEOUT: zx::BootDuration =
40    zx::BootDuration::from_minutes(2);
41
42#[derive(Clone, Debug, Display, EnumIter)]
43enum ConnectionState {
44    Idle(IdleState),
45    Connected(ConnectedState),
46    Disconnected(DisconnectedState),
47    ConnectFailed(ConnectFailedState),
48    FailedToStart(FailedToStartState),
49    FailedToStop(FailedToStopState),
50    PnoScanFailedIdle(PnoScanFailedIdleState),
51}
52
53// Update the ConnectDisconnectTimeSeries BitSetMap when making changes to this enum.
54impl IdEnum for ConnectionState {
55    type Id = u8;
56    fn to_id(&self) -> Self::Id {
57        match self {
58            Self::Idle(_) => 0,
59            Self::Disconnected(_) => 1,
60            Self::ConnectFailed(_) => 2,
61            Self::Connected(_) => 3,
62            Self::FailedToStart(_) => 4,
63            Self::FailedToStop(_) => 5,
64            Self::PnoScanFailedIdle(_) => 6,
65        }
66    }
67}
68
69#[derive(Clone, Debug, Default)]
70struct IdleState {}
71
72#[derive(Clone, Debug, Default)]
73struct ConnectedState {}
74
75#[derive(Clone, Debug, Default)]
76struct DisconnectedState {}
77
78#[derive(Clone, Debug, Default)]
79struct ConnectFailedState {}
80
81#[derive(Clone, Debug, Default)]
82struct FailedToStartState {}
83
84#[derive(Clone, Debug, Default)]
85struct FailedToStopState {}
86
87#[derive(Clone, Debug, Default)]
88struct PnoScanFailedIdleState {}
89
90#[derive(Derivative, Unit)]
91#[derivative(PartialEq, Eq, Hash)]
92struct InspectConnectedNetwork {
93    bssid: String,
94    ssid: String,
95    protection: String,
96    ht_cap: Option<Vec<u8>>,
97    vht_cap: Option<Vec<u8>>,
98    #[derivative(PartialEq = "ignore")]
99    #[derivative(Hash = "ignore")]
100    wsc: Option<InspectNetworkWsc>,
101    is_wmm_assoc: bool,
102    wmm_param: Option<Vec<u8>>,
103}
104
105impl From<&BssDescription> for InspectConnectedNetwork {
106    fn from(bss_description: &BssDescription) -> Self {
107        Self {
108            bssid: bss_description.bssid.to_string(),
109            ssid: bss_description.ssid.to_string(),
110            protection: format!("{:?}", bss_description.protection()),
111            ht_cap: bss_description.raw_ht_cap().map(|cap| cap.bytes.into()),
112            vht_cap: bss_description.raw_vht_cap().map(|cap| cap.bytes.into()),
113            wsc: bss_description.probe_resp_wsc().as_ref().map(InspectNetworkWsc::from),
114            is_wmm_assoc: bss_description.find_wmm_param().is_some(),
115            wmm_param: bss_description.find_wmm_param().map(|bytes| bytes.into()),
116        }
117    }
118}
119
120#[derive(PartialEq, Unit, Hash)]
121struct InspectNetworkWsc {
122    device_name: String,
123    manufacturer: String,
124    model_name: String,
125    model_number: String,
126}
127
128impl From<&wlan_common::ie::wsc::ProbeRespWsc> for InspectNetworkWsc {
129    fn from(wsc: &wlan_common::ie::wsc::ProbeRespWsc) -> Self {
130        Self {
131            device_name: String::from_utf8_lossy(&wsc.device_name[..]).to_string(),
132            manufacturer: String::from_utf8_lossy(&wsc.manufacturer[..]).to_string(),
133            model_name: String::from_utf8_lossy(&wsc.model_name[..]).to_string(),
134            model_number: String::from_utf8_lossy(&wsc.model_number[..]).to_string(),
135        }
136    }
137}
138
139#[derive(PartialEq, Eq, Unit, Hash)]
140struct InspectConnectAttemptResult {
141    status_code: u16,
142    result: String,
143}
144
145#[derive(PartialEq, Eq, Unit, Hash)]
146struct InspectDisconnectSource {
147    source: String,
148    reason: String,
149    mlme_event_name: Option<String>,
150}
151
152impl From<&fidl_sme::DisconnectSource> for InspectDisconnectSource {
153    fn from(disconnect_source: &fidl_sme::DisconnectSource) -> Self {
154        match disconnect_source {
155            fidl_sme::DisconnectSource::User(reason) => Self {
156                source: "user".to_string(),
157                reason: format!("{reason:?}"),
158                mlme_event_name: None,
159            },
160            fidl_sme::DisconnectSource::Ap(cause) => Self {
161                source: "ap".to_string(),
162                reason: format!("{:?}", cause.reason_code),
163                mlme_event_name: Some(format!("{:?}", cause.mlme_event_name)),
164            },
165            fidl_sme::DisconnectSource::Mlme(cause) => Self {
166                source: "mlme".to_string(),
167                reason: format!("{:?}", cause.reason_code),
168                mlme_event_name: Some(format!("{:?}", cause.mlme_event_name)),
169            },
170        }
171    }
172}
173
174#[derive(Clone, Debug, PartialEq)]
175pub struct DisconnectInfo {
176    pub iface_id: u16,
177    pub connected_duration: zx::BootDuration,
178    pub is_sme_reconnecting: bool,
179    pub disconnect_source: fidl_sme::DisconnectSource,
180    pub original_bss_desc: Box<BssDescription>,
181    pub current_rssi_dbm: i8,
182    pub current_snr_db: i8,
183    pub current_channel: Channel,
184}
185
186pub struct ConnectDisconnectLogger {
187    connection_state: Arc<Mutex<ConnectionState>>,
188    cobalt_proxy: fidl_fuchsia_metrics::MetricEventLoggerProxy,
189    connect_events_node: Mutex<BoundedListNode>,
190    disconnect_events_node: Mutex<BoundedListNode>,
191    connect_attempt_results_node: Mutex<BoundedListNode>,
192    inspect_metadata_node: Mutex<InspectMetadataNode>,
193    time_series_stats: ConnectDisconnectTimeSeries,
194    successive_connect_attempt_failures: AtomicUsize,
195    last_connect_failure_at: Arc<Mutex<Option<fasync::BootInstant>>>,
196    last_disconnect_at: Arc<Mutex<Option<fasync::MonotonicInstant>>>,
197}
198
199impl ConnectDisconnectLogger {
200    pub fn new<S: InspectSender>(
201        cobalt_proxy: fidl_fuchsia_metrics::MetricEventLoggerProxy,
202        inspect_node: &InspectNode,
203        inspect_metadata_node: &InspectNode,
204        inspect_metadata_path: &str,
205        time_matrix_client: &S,
206    ) -> Self {
207        let connect_events = inspect_node.create_child("connect_events");
208        let disconnect_events = inspect_node.create_child("disconnect_events");
209        let connect_attempt_results = inspect_node.create_child("connect_attempt_results");
210        let this = Self {
211            cobalt_proxy,
212            connection_state: Arc::new(Mutex::new(ConnectionState::Idle(IdleState {}))),
213            connect_events_node: Mutex::new(BoundedListNode::new(
214                connect_events,
215                INSPECT_CONNECT_EVENTS_LIMIT,
216            )),
217            disconnect_events_node: Mutex::new(BoundedListNode::new(
218                disconnect_events,
219                INSPECT_DISCONNECT_EVENTS_LIMIT,
220            )),
221            connect_attempt_results_node: Mutex::new(BoundedListNode::new(
222                connect_attempt_results,
223                INSPECT_CONNECT_ATTEMPT_RESULTS_LIMIT,
224            )),
225            inspect_metadata_node: Mutex::new(InspectMetadataNode::new(inspect_metadata_node)),
226            time_series_stats: ConnectDisconnectTimeSeries::new(
227                time_matrix_client,
228                inspect_metadata_path,
229            ),
230            successive_connect_attempt_failures: AtomicUsize::new(0),
231            last_connect_failure_at: Arc::new(Mutex::new(None)),
232            last_disconnect_at: Arc::new(Mutex::new(None)),
233        };
234        this.log_connection_state();
235        this
236    }
237
238    fn update_connection_state(&self, state: ConnectionState) {
239        *self.connection_state.lock() = state;
240        self.log_connection_state();
241    }
242
243    fn log_connection_state(&self) {
244        let wlan_connectivity_state_id = self.connection_state.lock().to_id() as u64;
245        self.time_series_stats.log_wlan_connectivity_state(1 << wlan_connectivity_state_id);
246    }
247
248    pub fn is_connected(&self) -> bool {
249        matches!(*self.connection_state.lock(), ConnectionState::Connected(_))
250    }
251
252    pub async fn handle_connect_attempt(
253        &self,
254        result: fidl_ieee80211::StatusCode,
255        bss: &BssDescription,
256        is_credential_rejected: bool,
257    ) {
258        let mut flushed_successive_failures = None;
259        let mut downtime_duration = None;
260        if result == fidl_ieee80211::StatusCode::Success {
261            self.update_connection_state(ConnectionState::Connected(ConnectedState {}));
262            flushed_successive_failures =
263                Some(self.successive_connect_attempt_failures.swap(0, Ordering::SeqCst));
264            downtime_duration =
265                self.last_disconnect_at.lock().map(|t| fasync::MonotonicInstant::now() - t);
266        } else if is_credential_rejected {
267            self.update_connection_state(ConnectionState::Idle(IdleState {}));
268            let _prev = self.successive_connect_attempt_failures.fetch_add(1, Ordering::SeqCst);
269            let _prev = self.last_connect_failure_at.lock().replace(fasync::BootInstant::now());
270        } else {
271            self.update_connection_state(ConnectionState::ConnectFailed(ConnectFailedState {}));
272            let _prev = self.successive_connect_attempt_failures.fetch_add(1, Ordering::SeqCst);
273            let _prev = self.last_connect_failure_at.lock().replace(fasync::BootInstant::now());
274        }
275
276        self.log_connect_attempt_inspect(result, bss);
277        self.log_connect_attempt_cobalt(result, flushed_successive_failures, downtime_duration)
278            .await;
279        if result == fidl_ieee80211::StatusCode::Success {
280            self.log_device_connected_cobalt_metrics(bss).await;
281        }
282    }
283
284    fn log_connect_attempt_inspect(
285        &self,
286        result: fidl_ieee80211::StatusCode,
287        bss: &BssDescription,
288    ) {
289        let mut inspect_metadata_node = self.inspect_metadata_node.lock();
290        let connect_result_id =
291            inspect_metadata_node.connect_attempt_results.insert(InspectConnectAttemptResult {
292                status_code: result.into_primitive(),
293                result: format!("{:?}", result),
294            }) as u64;
295        self.time_series_stats.log_connect_attempt_results(1 << connect_result_id);
296
297        inspect_log!(self.connect_attempt_results_node.lock(), {
298            result: format!("{:?}", result),
299            ssid: bss.ssid.to_string(),
300            bssid: bss.bssid.to_string(),
301            protection: format!("{:?}", bss.protection()),
302        });
303
304        if result == fidl_ieee80211::StatusCode::Success {
305            let connected_network = InspectConnectedNetwork::from(bss);
306            let connected_network_id =
307                inspect_metadata_node.connected_networks.insert(connected_network) as u64;
308
309            self.time_series_stats.log_connected_networks(1 << connected_network_id);
310
311            inspect_log!(self.connect_events_node.lock(), {
312                network_id: connected_network_id,
313            });
314        }
315    }
316
317    #[allow(clippy::vec_init_then_push, reason = "mass allow for https://fxbug.dev/381896734")]
318    async fn log_connect_attempt_cobalt(
319        &self,
320        result: fidl_ieee80211::StatusCode,
321        flushed_successive_failures: Option<usize>,
322        downtime_duration: Option<zx::MonotonicDuration>,
323    ) {
324        let mut metric_events = vec![];
325        metric_events.push(MetricEvent {
326            metric_id: metrics::CONNECT_ATTEMPT_BREAKDOWN_BY_STATUS_CODE_METRIC_ID,
327            event_codes: vec![result.into_primitive() as u32],
328            payload: MetricEventPayload::Count(1),
329        });
330
331        if let Some(failures) = flushed_successive_failures {
332            metric_events.push(MetricEvent {
333                metric_id: metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID,
334                event_codes: vec![],
335                payload: MetricEventPayload::IntegerValue(failures as i64),
336            });
337        }
338
339        if let Some(duration) = downtime_duration {
340            metric_events.push(MetricEvent {
341                metric_id: metrics::DOWNTIME_POST_DISCONNECT_METRIC_ID,
342                event_codes: vec![],
343                payload: MetricEventPayload::IntegerValue(duration.into_millis()),
344            });
345        }
346
347        log_cobalt_batch!(self.cobalt_proxy, &metric_events, "log_connect_attempt_cobalt");
348    }
349
350    async fn log_device_connected_cobalt_metrics(&self, bss: &BssDescription) {
351        let mut metric_events = vec![];
352        metric_events.push(MetricEvent {
353            metric_id: metrics::NUMBER_OF_CONNECTED_DEVICES_METRIC_ID,
354            event_codes: vec![],
355            payload: MetricEventPayload::Count(1),
356        });
357
358        let security_type_dim = convert_security_type(&bss.protection());
359        metric_events.push(MetricEvent {
360            metric_id: metrics::CONNECTED_NETWORK_SECURITY_TYPE_METRIC_ID,
361            event_codes: vec![security_type_dim as u32],
362            payload: MetricEventPayload::Count(1),
363        });
364
365        if bss.supports_uapsd() {
366            metric_events.push(MetricEvent {
367                metric_id: metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_APSD_METRIC_ID,
368                event_codes: vec![],
369                payload: MetricEventPayload::Count(1),
370            });
371        }
372
373        if let Some(rm_enabled_cap) = bss.rm_enabled_cap() {
374            if rm_enabled_cap.link_measurement_enabled() {
375                metric_events.push(MetricEvent {
376                    metric_id:
377                        metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_LINK_MEASUREMENT_METRIC_ID,
378                    event_codes: vec![],
379                    payload: MetricEventPayload::Count(1),
380                });
381            }
382            if rm_enabled_cap.neighbor_report_enabled() {
383                metric_events.push(MetricEvent {
384                    metric_id:
385                        metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_NEIGHBOR_REPORT_METRIC_ID,
386                    event_codes: vec![],
387                    payload: MetricEventPayload::Count(1),
388                });
389            }
390        }
391
392        if bss.supports_ft() {
393            metric_events.push(MetricEvent {
394                metric_id: metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_FT_METRIC_ID,
395                event_codes: vec![],
396                payload: MetricEventPayload::Count(1),
397            });
398        }
399
400        if let Some(cap) = bss.ext_cap().and_then(|cap| cap.ext_caps_octet_3)
401            && cap.bss_transition()
402        {
403            metric_events.push(MetricEvent {
404                    metric_id: metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_BSS_TRANSITION_MANAGEMENT_METRIC_ID,
405                    event_codes: vec![],
406                    payload: MetricEventPayload::Count(1),
407                });
408        }
409
410        metric_events.push(MetricEvent {
411            metric_id: metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID,
412            event_codes: vec![bss.channel.primary as u32],
413            payload: MetricEventPayload::Count(1),
414        });
415
416        let channel_band_dim = convert_channel_band(bss.channel.primary);
417        metric_events.push(MetricEvent {
418            metric_id: metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_CHANNEL_BAND_METRIC_ID,
419            event_codes: vec![channel_band_dim as u32],
420            payload: MetricEventPayload::Count(1),
421        });
422
423        let oui_string = bss.bssid.to_oui_uppercase("");
424        metric_events.push(MetricEvent {
425            metric_id: metrics::DEVICE_CONNECTED_TO_AP_OUI_2_METRIC_ID,
426            event_codes: vec![],
427            payload: MetricEventPayload::StringValue(oui_string),
428        });
429
430        log_cobalt_batch!(self.cobalt_proxy, &metric_events, "log_device_connected_cobalt_metrics");
431    }
432
433    pub async fn log_disconnect(&self, info: &DisconnectInfo) {
434        // Mobile devices can be considered idle if they disconnect for reasons associated with
435        // going out of range or are commanded to disconnect by upper layers.
436        //
437        // TODO(500107852): Update this logic to account for non-mobile devices when such devices
438        // use the telemetry library.
439        if !info.disconnect_source.should_log_for_mobile_device() {
440            self.update_connection_state(ConnectionState::Idle(IdleState {}));
441        } else {
442            self.update_connection_state(ConnectionState::Disconnected(DisconnectedState {}));
443        }
444        let _prev = self.last_disconnect_at.lock().replace(fasync::MonotonicInstant::now());
445        self.log_disconnect_inspect(info);
446        self.log_disconnect_cobalt(info).await;
447    }
448
449    fn log_disconnect_inspect(&self, info: &DisconnectInfo) {
450        let mut inspect_metadata_node = self.inspect_metadata_node.lock();
451        let connected_network = InspectConnectedNetwork::from(&*info.original_bss_desc);
452        let connected_network_id =
453            inspect_metadata_node.connected_networks.insert(connected_network) as u64;
454        let disconnect_source = InspectDisconnectSource::from(&info.disconnect_source);
455        let disconnect_source_id =
456            inspect_metadata_node.disconnect_sources.insert(disconnect_source) as u64;
457        inspect_log!(self.disconnect_events_node.lock(), {
458            connected_duration: info.connected_duration.into_nanos(),
459            disconnect_source_id: disconnect_source_id,
460            network_id: connected_network_id,
461            rssi_dbm: info.current_rssi_dbm,
462            snr_db: info.current_snr_db,
463            channel: format!("{}", info.current_channel),
464        });
465
466        self.time_series_stats.log_disconnected_networks(1 << connected_network_id);
467        self.time_series_stats.log_disconnect_sources(1 << disconnect_source_id);
468    }
469
470    async fn log_disconnect_cobalt(&self, info: &DisconnectInfo) {
471        let mut metric_events = vec![];
472        metric_events.push(MetricEvent {
473            metric_id: metrics::TOTAL_DISCONNECT_COUNT_METRIC_ID,
474            event_codes: vec![],
475            payload: MetricEventPayload::Count(1),
476        });
477
478        if info.disconnect_source.should_log_for_mobile_device() {
479            metric_events.push(MetricEvent {
480                metric_id: metrics::DISCONNECT_OCCURRENCE_FOR_MOBILE_DEVICE_METRIC_ID,
481                event_codes: vec![],
482                payload: MetricEventPayload::Count(1),
483            });
484        }
485
486        metric_events.push(MetricEvent {
487            metric_id: metrics::CONNECTED_DURATION_ON_DISCONNECT_METRIC_ID,
488            event_codes: vec![],
489            payload: MetricEventPayload::IntegerValue(info.connected_duration.into_millis()),
490        });
491
492        metric_events.push(MetricEvent {
493            metric_id: metrics::DISCONNECT_BREAKDOWN_BY_REASON_CODE_METRIC_ID,
494            event_codes: vec![
495                u32::from(info.disconnect_source.cobalt_reason_code()),
496                info.disconnect_source.as_cobalt_disconnect_source() as u32,
497            ],
498            payload: MetricEventPayload::Count(1),
499        });
500
501        log_cobalt_batch!(self.cobalt_proxy, &metric_events, "log_disconnect_cobalt");
502    }
503
504    pub async fn handle_periodic_telemetry(&self) {
505        let mut metric_events = vec![];
506        let now = fasync::BootInstant::now();
507        if let Some(failed_at) = *self.last_connect_failure_at.lock()
508            && now - failed_at >= SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_TIMEOUT
509        {
510            let failures = self.successive_connect_attempt_failures.swap(0, Ordering::SeqCst);
511            if failures > 0 {
512                metric_events.push(MetricEvent {
513                    metric_id: metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID,
514                    event_codes: vec![],
515                    payload: MetricEventPayload::IntegerValue(failures as i64),
516                });
517            }
518        }
519
520        log_cobalt_batch!(self.cobalt_proxy, &metric_events, "handle_periodic_telemetry");
521    }
522
523    pub async fn handle_suspend_imminent(&self) {
524        let mut metric_events = vec![];
525
526        let flushed_successive_failures =
527            self.successive_connect_attempt_failures.swap(0, Ordering::SeqCst);
528        if flushed_successive_failures > 0 {
529            metric_events.push(MetricEvent {
530                metric_id: metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID,
531                event_codes: vec![],
532                payload: MetricEventPayload::IntegerValue(flushed_successive_failures as i64),
533            });
534        }
535
536        log_cobalt_batch!(self.cobalt_proxy, &metric_events, "handle_suspend_imminent");
537    }
538
539    pub async fn handle_iface_destroyed(&self) {
540        self.update_connection_state(ConnectionState::Idle(IdleState {}));
541    }
542
543    pub async fn handle_client_connections_toggle(&self, event: &ClientConnectionsToggleEvent) {
544        if event == &ClientConnectionsToggleEvent::Disabled {
545            self.update_connection_state(ConnectionState::Idle(IdleState {}));
546        }
547    }
548
549    pub async fn handle_pno_scan_failure(&self) {
550        let mut metric_events = vec![MetricEvent {
551            metric_id: metrics::PNO_SCAN_FAILURE_OCCURRENCE_METRIC_ID,
552            event_codes: vec![],
553            payload: MetricEventPayload::Count(1),
554        }];
555
556        let state = self.connection_state.lock().clone();
557        match state {
558            ConnectionState::Idle(_)
559            | ConnectionState::Disconnected(_)
560            | ConnectionState::ConnectFailed(_)
561            | ConnectionState::PnoScanFailedIdle(_) => {
562                metric_events.push(MetricEvent {
563                    metric_id: metrics::PNO_SCAN_FAILURE_WHILE_NOT_CONNECTED_OCCURRENCE_METRIC_ID,
564                    event_codes: vec![],
565                    payload: MetricEventPayload::Count(1),
566                });
567
568                // PNO scan failures while not connected indicate that the system is looking for
569                // networks to connect to but it is unable to.  In this case, we should transition
570                // to the PnoScanFailedIdle state to flag a period of potential connectivity loss.
571                self.update_connection_state(ConnectionState::PnoScanFailedIdle(
572                    PnoScanFailedIdleState {},
573                ));
574            }
575            ConnectionState::Connected(_)
576            | ConnectionState::FailedToStart(_)
577            | ConnectionState::FailedToStop(_) => {
578                // PNO scan failures while connected will not affect the current connectivity state.
579                // If WLAN has already failed to start or failed to stop, the state should remain
580                // unchanged until a different failure or successful connection occurs.
581            }
582        }
583
584        log_cobalt_batch!(self.cobalt_proxy, &metric_events, "handle_pno_scan_failure");
585    }
586    pub async fn handle_client_connections_failed_to_start(&self) {
587        self.update_connection_state(ConnectionState::FailedToStart(FailedToStartState {}));
588    }
589
590    pub async fn handle_client_connections_failed_to_stop(&self) {
591        self.update_connection_state(ConnectionState::FailedToStop(FailedToStopState {}));
592    }
593}
594
595struct InspectMetadataNode {
596    connected_networks: LruCacheNode<InspectConnectedNetwork>,
597    disconnect_sources: LruCacheNode<InspectDisconnectSource>,
598    connect_attempt_results: LruCacheNode<InspectConnectAttemptResult>,
599}
600
601impl InspectMetadataNode {
602    const CONNECTED_NETWORKS: &'static str = "connected_networks";
603    const DISCONNECT_SOURCES: &'static str = "disconnect_sources";
604    const CONNECT_ATTEMPT_RESULTS: &'static str = "connect_attempt_results";
605
606    fn new(inspect_node: &InspectNode) -> Self {
607        let connected_networks = inspect_node.create_child(Self::CONNECTED_NETWORKS);
608        let disconnect_sources = inspect_node.create_child(Self::DISCONNECT_SOURCES);
609        let connect_attempt_results = inspect_node.create_child(Self::CONNECT_ATTEMPT_RESULTS);
610        Self {
611            connected_networks: LruCacheNode::new(
612                connected_networks,
613                INSPECT_CONNECTED_NETWORKS_ID_LIMIT,
614            ),
615            disconnect_sources: LruCacheNode::new(
616                disconnect_sources,
617                INSPECT_DISCONNECT_SOURCES_ID_LIMIT,
618            ),
619            connect_attempt_results: LruCacheNode::new(
620                connect_attempt_results,
621                INSPECT_CONNECT_ATTEMPT_RESULTS_ID_LIMIT,
622            ),
623        }
624    }
625}
626
627#[derive(Debug, Clone)]
628struct ConnectDisconnectTimeSeries {
629    wlan_connectivity_states: InspectedTimeMatrix<u64>,
630    connected_networks: InspectedTimeMatrix<u64>,
631    disconnected_networks: InspectedTimeMatrix<u64>,
632    disconnect_sources: InspectedTimeMatrix<u64>,
633    connect_attempt_results: InspectedTimeMatrix<u64>,
634}
635
636impl ConnectDisconnectTimeSeries {
637    pub fn new<S: InspectSender>(client: &S, inspect_metadata_path: &str) -> Self {
638        let wlan_connectivity_states = client.inspect_time_matrix_with_metadata(
639            "wlan_connectivity_states",
640            TimeMatrix::<Union<u64>, LastSample>::new(
641                SamplingProfile::highly_granular(),
642                LastSample::or(0),
643            ),
644            // Update the ConnectionState IdEnum trait when making changes to this list.
645            BitSetMap::from_ordered(Self::wlan_connectivity_states_bitset_map().iter().copied()),
646        );
647        let connected_networks = client.inspect_time_matrix_with_metadata(
648            "connected_networks",
649            TimeMatrix::<Union<u64>, ConstantSample>::new(
650                SamplingProfile::granular(),
651                ConstantSample::default(),
652            ),
653            BitSetNode::from_path(format!(
654                "{}/{}",
655                inspect_metadata_path,
656                InspectMetadataNode::CONNECTED_NETWORKS
657            )),
658        );
659        let disconnected_networks = client.inspect_time_matrix_with_metadata(
660            "disconnected_networks",
661            TimeMatrix::<Union<u64>, ConstantSample>::new(
662                SamplingProfile::granular(),
663                ConstantSample::default(),
664            ),
665            // This time matrix shares its bit labels with `connected_networks`.
666            BitSetNode::from_path(format!(
667                "{}/{}",
668                inspect_metadata_path,
669                InspectMetadataNode::CONNECTED_NETWORKS
670            )),
671        );
672        let disconnect_sources = client.inspect_time_matrix_with_metadata(
673            "disconnect_sources",
674            TimeMatrix::<Union<u64>, ConstantSample>::new(
675                SamplingProfile::granular(),
676                ConstantSample::default(),
677            ),
678            BitSetNode::from_path(format!(
679                "{}/{}",
680                inspect_metadata_path,
681                InspectMetadataNode::DISCONNECT_SOURCES,
682            )),
683        );
684        let connect_attempt_results = client.inspect_time_matrix_with_metadata(
685            "connect_attempt_results",
686            TimeMatrix::<Union<u64>, ConstantSample>::new(
687                SamplingProfile::granular(),
688                ConstantSample::default(),
689            ),
690            BitSetNode::from_path(format!(
691                "{}/{}",
692                inspect_metadata_path,
693                InspectMetadataNode::CONNECT_ATTEMPT_RESULTS,
694            )),
695        );
696        Self {
697            wlan_connectivity_states,
698            connected_networks,
699            disconnected_networks,
700            disconnect_sources,
701            connect_attempt_results,
702        }
703    }
704
705    // TODO(https://fxbug.dev/504712259): Update BitSetMap to accept the enum type
706    // it's associated with rather than constructing bit labels separately like this
707    fn wlan_connectivity_states_bitset_map() -> &'static [&'static str] {
708        &[
709            "idle",
710            "disconnected",
711            "connect_failed",
712            "connected",
713            "start_failure",
714            "stop_failure",
715            "pno_scan_failed",
716        ]
717    }
718
719    fn log_wlan_connectivity_state(&self, data: u64) {
720        self.wlan_connectivity_states.fold_or_log_error(data);
721    }
722    fn log_connected_networks(&self, data: u64) {
723        self.connected_networks.fold_or_log_error(data);
724    }
725    fn log_disconnected_networks(&self, data: u64) {
726        self.disconnected_networks.fold_or_log_error(data);
727    }
728    fn log_disconnect_sources(&self, data: u64) {
729        self.disconnect_sources.fold_or_log_error(data);
730    }
731    fn log_connect_attempt_results(&self, data: u64) {
732        self.connect_attempt_results.fold_or_log_error(data);
733    }
734}
735
736pub trait DisconnectSourceExt {
737    fn should_log_for_mobile_device(&self) -> bool;
738    fn cobalt_reason_code(&self) -> u16;
739    fn as_cobalt_disconnect_source(
740        &self,
741    ) -> metrics::ConnectivityWlanMetricDimensionDisconnectSource;
742}
743
744impl DisconnectSourceExt for fidl_sme::DisconnectSource {
745    fn should_log_for_mobile_device(&self) -> bool {
746        match self {
747            fidl_sme::DisconnectSource::Ap(_) => true,
748            fidl_sme::DisconnectSource::Mlme(cause)
749                if cause.reason_code != fidl_ieee80211::ReasonCode::MlmeLinkFailed =>
750            {
751                true
752            }
753            _ => false,
754        }
755    }
756
757    fn cobalt_reason_code(&self) -> u16 {
758        let cobalt_disconnect_reason_code = match self {
759            fidl_sme::DisconnectSource::Ap(cause) | fidl_sme::DisconnectSource::Mlme(cause) => {
760                cause.reason_code.into_primitive()
761            }
762            fidl_sme::DisconnectSource::User(reason) => *reason as u16,
763        };
764        // This `max_event_code: 1000` is set in the metrics registry, but doesn't show up in the
765        // generated bindings.
766        const REASON_CODE_MAX: u16 = 1000;
767        std::cmp::min(cobalt_disconnect_reason_code, REASON_CODE_MAX)
768    }
769
770    fn as_cobalt_disconnect_source(
771        &self,
772    ) -> metrics::ConnectivityWlanMetricDimensionDisconnectSource {
773        use metrics::ConnectivityWlanMetricDimensionDisconnectSource as DS;
774        match self {
775            fidl_sme::DisconnectSource::Ap(..) => DS::Ap,
776            fidl_sme::DisconnectSource::User(..) => DS::User,
777            fidl_sme::DisconnectSource::Mlme(..) => DS::Mlme,
778        }
779    }
780}
781
782#[cfg(test)]
783mod tests {
784    use super::*;
785    use crate::testing::*;
786    use assert_matches::assert_matches;
787    use diagnostics_assertions::{
788        AnyBoolProperty, AnyBytesProperty, AnyNumericProperty, AnyStringProperty, assert_data_tree,
789    };
790    use futures::task::Poll;
791    use ieee80211_testutils::{BSSID_REGEX, SSID_REGEX};
792    use rand::Rng;
793    use std::pin::pin;
794    use strum::IntoEnumIterator;
795    use test_case::test_case;
796    use windowed_stats::experimental::clock::Timed;
797    use windowed_stats::experimental::inspect::TimeMatrixClient;
798    use windowed_stats::experimental::testing::TimeMatrixCall;
799    use wlan_common::channel::{Cbw, Channel};
800    use wlan_common::ie::IeType;
801    use wlan_common::test_utils::fake_stas::IesOverrides;
802    use wlan_common::{fake_bss_description, random_bss_description};
803
804    #[fuchsia::test]
805    fn log_connect_attempt_then_inspect_data_tree_contains_time_matrix_metadata() {
806        let mut harness = setup_test();
807
808        let client =
809            TimeMatrixClient::new(harness.inspect_node.create_child("wlan_connect_disconnect"));
810        let logger = ConnectDisconnectLogger::new(
811            harness.cobalt_proxy.clone(),
812            &harness.inspect_node,
813            &harness.inspect_metadata_node,
814            &harness.inspect_metadata_path,
815            &client,
816        );
817        let bss = random_bss_description!();
818        let mut log_connect_attempt =
819            pin!(logger.handle_connect_attempt(fidl_ieee80211::StatusCode::Success, &bss, false));
820        assert!(
821            harness.run_until_stalled_drain_cobalt_events(&mut log_connect_attempt).is_ready(),
822            "`log_connect_attempt` did not complete",
823        );
824
825        let tree = harness.get_inspect_data_tree();
826        assert_data_tree!(
827            @executor harness.exec,
828            tree,
829            root: contains {
830                test_stats: contains {
831                    wlan_connect_disconnect: contains {
832                        wlan_connectivity_states: {
833                            "type": "bitset",
834                            "data": AnyBytesProperty,
835                            metadata: {
836                                index: {
837                                    "0": "idle",
838                                    "1": "disconnected",
839                                    "2": "connect_failed",
840                                    "3": "connected",
841                                    "4": "start_failure",
842                                    "5": "stop_failure",
843                                    "6": "pno_scan_failed",
844                                },
845                            },
846                        },
847                        connected_networks: {
848                            "type": "bitset",
849                            "data": AnyBytesProperty,
850                            metadata: {
851                                "index_node_path": "root/test_stats/metadata/connected_networks",
852                            },
853                        },
854                        disconnected_networks: {
855                            "type": "bitset",
856                            "data": AnyBytesProperty,
857                            metadata: {
858                                "index_node_path": "root/test_stats/metadata/connected_networks",
859                            },
860                        },
861                        disconnect_sources: {
862                            "type": "bitset",
863                            "data": AnyBytesProperty,
864                            metadata: {
865                                "index_node_path": "root/test_stats/metadata/disconnect_sources",
866                            },
867                        },
868                        connect_attempt_results: {
869                            "type": "bitset",
870                            "data": AnyBytesProperty,
871                            metadata: {
872                                "index_node_path": "root/test_stats/metadata/connect_attempt_results",
873                            },
874                        },
875                    },
876                },
877            }
878        );
879    }
880
881    #[fuchsia::test]
882    fn test_log_connect_attempt_inspect() {
883        let mut test_helper = setup_test();
884        let logger = ConnectDisconnectLogger::new(
885            test_helper.cobalt_proxy.clone(),
886            &test_helper.inspect_node,
887            &test_helper.inspect_metadata_node,
888            &test_helper.inspect_metadata_path,
889            &test_helper.mock_time_matrix_client,
890        );
891
892        // Log the event
893        let bss_description = random_bss_description!();
894        let mut test_fut = pin!(logger.handle_connect_attempt(
895            fidl_ieee80211::StatusCode::Success,
896            &bss_description,
897            false
898        ));
899        assert_eq!(
900            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
901            Poll::Ready(())
902        );
903
904        // Validate Inspect data
905        let data = test_helper.get_inspect_data_tree();
906        assert_data_tree!(@executor test_helper.exec, data, root: contains {
907            test_stats: contains {
908                metadata: contains {
909                    connected_networks: contains {
910                        "0": {
911                            "@time": AnyNumericProperty,
912                            "data": contains {
913                                bssid: &*BSSID_REGEX,
914                                ssid: &*SSID_REGEX,
915                            }
916                        }
917                    },
918                    connect_attempt_results: contains {
919                        "0": {
920                            "@time": AnyNumericProperty,
921                            "data": contains {
922                                status_code: 0u64,
923                                result: "Success",
924                            }
925                        }
926                    },
927                },
928                connect_events: {
929                    "0": {
930                        "@time": AnyNumericProperty,
931                        network_id: 0u64,
932                    }
933                },
934                connect_attempt_results: {
935                    "0": {
936                        "@time": AnyNumericProperty,
937                        result: "Success",
938                        ssid: &*SSID_REGEX,
939                        bssid: &*BSSID_REGEX,
940                        protection: AnyStringProperty,
941                    }
942                }
943            }
944        });
945
946        let mut time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
947        assert_eq!(
948            &time_matrix_calls.drain::<u64>("wlan_connectivity_states")[..],
949            &[TimeMatrixCall::Fold(Timed::now(1 << 0)), TimeMatrixCall::Fold(Timed::now(1 << 3)),]
950        );
951        assert_eq!(
952            &time_matrix_calls.drain::<u64>("connected_networks")[..],
953            &[TimeMatrixCall::Fold(Timed::now(1 << 0))]
954        );
955        assert_eq!(
956            &time_matrix_calls.drain::<u64>("connect_attempt_results")[..],
957            &[TimeMatrixCall::Fold(Timed::now(1 << 0))]
958        );
959    }
960
961    #[fuchsia::test]
962    fn test_log_connect_attempt_cobalt() {
963        let mut test_helper = setup_test();
964        let logger = ConnectDisconnectLogger::new(
965            test_helper.cobalt_proxy.clone(),
966            &test_helper.inspect_node,
967            &test_helper.inspect_metadata_node,
968            &test_helper.inspect_metadata_path,
969            &test_helper.mock_time_matrix_client,
970        );
971
972        // Generate BSS Description
973        let bss_description = random_bss_description!(Wpa2,
974            channel: Channel::new(157, Cbw::Cbw40),
975            bssid: [0x00, 0xf6, 0x20, 0x03, 0x04, 0x05],
976        );
977
978        // Log the event
979        let mut test_fut = pin!(logger.handle_connect_attempt(
980            fidl_ieee80211::StatusCode::Success,
981            &bss_description,
982            false
983        ));
984        assert_eq!(
985            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
986            Poll::Ready(())
987        );
988
989        // Validate Cobalt data
990        let breakdowns_by_status_code = test_helper
991            .get_logged_metrics(metrics::CONNECT_ATTEMPT_BREAKDOWN_BY_STATUS_CODE_METRIC_ID);
992        assert_eq!(breakdowns_by_status_code.len(), 1);
993        assert_eq!(
994            breakdowns_by_status_code[0].event_codes,
995            vec![fidl_ieee80211::StatusCode::Success.into_primitive() as u32]
996        );
997        assert_eq!(breakdowns_by_status_code[0].payload, MetricEventPayload::Count(1));
998
999        let metrics_devices =
1000            test_helper.get_logged_metrics(metrics::NUMBER_OF_CONNECTED_DEVICES_METRIC_ID);
1001        assert_eq!(metrics_devices.len(), 1);
1002        assert_eq!(metrics_devices[0].payload, MetricEventPayload::Count(1));
1003
1004        let metrics_security =
1005            test_helper.get_logged_metrics(metrics::CONNECTED_NETWORK_SECURITY_TYPE_METRIC_ID);
1006        assert_eq!(metrics_security.len(), 1);
1007        assert_eq!(metrics_security[0].event_codes, vec![5]); // Wpa2Personal
1008
1009        let metrics_channel = test_helper.get_logged_metrics(
1010            metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID,
1011        );
1012        assert_eq!(metrics_channel.len(), 1);
1013        assert_eq!(metrics_channel[0].event_codes, vec![157]);
1014
1015        let metrics_band = test_helper.get_logged_metrics(
1016            metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_CHANNEL_BAND_METRIC_ID,
1017        );
1018        assert_eq!(metrics_band.len(), 1);
1019        assert_eq!(metrics_band[0].event_codes, vec![2]); // Band5Ghz
1020
1021        let metrics_oui =
1022            test_helper.get_logged_metrics(metrics::DEVICE_CONNECTED_TO_AP_OUI_2_METRIC_ID);
1023        assert_eq!(metrics_oui.len(), 1);
1024        assert_eq!(metrics_oui[0].payload, MetricEventPayload::StringValue("00F620".to_string()));
1025    }
1026
1027    #[fuchsia::test]
1028    fn test_successive_connect_attempt_failures_cobalt_zero_failures() {
1029        let mut test_helper = setup_test();
1030        let logger = ConnectDisconnectLogger::new(
1031            test_helper.cobalt_proxy.clone(),
1032            &test_helper.inspect_node,
1033            &test_helper.inspect_metadata_node,
1034            &test_helper.inspect_metadata_path,
1035            &test_helper.mock_time_matrix_client,
1036        );
1037
1038        let bss_description = random_bss_description!(Wpa2);
1039        let mut test_fut = pin!(logger.handle_connect_attempt(
1040            fidl_ieee80211::StatusCode::Success,
1041            &bss_description,
1042            false
1043        ));
1044        assert_eq!(
1045            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1046            Poll::Ready(())
1047        );
1048
1049        let metrics =
1050            test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
1051        assert_eq!(metrics.len(), 1);
1052        assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(0));
1053    }
1054
1055    #[fuchsia::test]
1056    fn test_log_device_connected_metrics_capabilities() {
1057        let mut test_helper = setup_test();
1058        let logger = ConnectDisconnectLogger::new(
1059            test_helper.cobalt_proxy.clone(),
1060            &test_helper.inspect_node,
1061            &test_helper.inspect_metadata_node,
1062            &test_helper.inspect_metadata_path,
1063            &test_helper.mock_time_matrix_client,
1064        );
1065
1066        let wmm_info = vec![0x80]; // U-APSD enabled
1067        #[rustfmt::skip]
1068        let rm_enabled_capabilities = vec![
1069            0x03, // link measurement and neighbor report enabled
1070            0x00, 0x00, 0x00, 0x00,
1071        ];
1072        #[rustfmt::skip]
1073        let ext_capabilities = vec![
1074            0x04, 0x00,
1075            0x08, // BSS transition supported
1076            0x00, 0x00, 0x00, 0x00, 0x40
1077        ];
1078
1079        let bss_description = fake_bss_description!(Wpa2,
1080            ies_overrides: IesOverrides::new()
1081                .remove(IeType::WMM_PARAM)
1082                .set(IeType::WMM_INFO, wmm_info)
1083                .set(IeType::RM_ENABLED_CAPABILITIES, rm_enabled_capabilities)
1084                .set(IeType::MOBILITY_DOMAIN, vec![0x00; 3])
1085                .set(IeType::EXT_CAPABILITIES, ext_capabilities),
1086        );
1087
1088        let mut test_fut = pin!(logger.handle_connect_attempt(
1089            fidl_ieee80211::StatusCode::Success,
1090            &bss_description,
1091            false
1092        ));
1093        assert_eq!(
1094            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1095            Poll::Ready(())
1096        );
1097
1098        let metrics = test_helper
1099            .get_logged_metrics(metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_APSD_METRIC_ID);
1100        assert_eq!(metrics.len(), 1);
1101        assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
1102
1103        let metrics = test_helper.get_logged_metrics(
1104            metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_BSS_TRANSITION_MANAGEMENT_METRIC_ID,
1105        );
1106        assert_eq!(metrics.len(), 1);
1107        assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
1108
1109        let metrics = test_helper.get_logged_metrics(
1110            metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_LINK_MEASUREMENT_METRIC_ID,
1111        );
1112        assert_eq!(metrics.len(), 1);
1113        assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
1114
1115        let metrics = test_helper.get_logged_metrics(
1116            metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_NEIGHBOR_REPORT_METRIC_ID,
1117        );
1118        assert_eq!(metrics.len(), 1);
1119        assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
1120    }
1121
1122    #[test_case(1; "one_failure")]
1123    #[test_case(2; "two_failures")]
1124    #[fuchsia::test(add_test_attr = false)]
1125    fn test_successive_connect_attempt_failures_cobalt_one_failure_then_success(n_failures: usize) {
1126        let mut test_helper = setup_test();
1127        let logger = ConnectDisconnectLogger::new(
1128            test_helper.cobalt_proxy.clone(),
1129            &test_helper.inspect_node,
1130            &test_helper.inspect_metadata_node,
1131            &test_helper.inspect_metadata_path,
1132            &test_helper.mock_time_matrix_client,
1133        );
1134
1135        let bss_description = random_bss_description!(Wpa2);
1136        for _i in 0..n_failures {
1137            let mut test_fut = pin!(logger.handle_connect_attempt(
1138                fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
1139                &bss_description,
1140                false
1141            ));
1142            assert_eq!(
1143                test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1144                Poll::Ready(())
1145            );
1146        }
1147
1148        let metrics =
1149            test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
1150        assert!(metrics.is_empty());
1151
1152        let mut test_fut = pin!(logger.handle_connect_attempt(
1153            fidl_ieee80211::StatusCode::Success,
1154            &bss_description,
1155            false
1156        ));
1157        assert_eq!(
1158            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1159            Poll::Ready(())
1160        );
1161
1162        let metrics =
1163            test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
1164        assert_eq!(metrics.len(), 1);
1165        assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(n_failures as i64));
1166
1167        // Verify subsequent successes would report 0 failures
1168        test_helper.clear_cobalt_events();
1169        let mut test_fut = pin!(logger.handle_connect_attempt(
1170            fidl_ieee80211::StatusCode::Success,
1171            &bss_description,
1172            false
1173        ));
1174        assert_eq!(
1175            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1176            Poll::Ready(())
1177        );
1178        let metrics =
1179            test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
1180        assert_eq!(metrics.len(), 1);
1181        assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(0));
1182    }
1183
1184    #[test_case(1; "one_failure")]
1185    #[test_case(2; "two_failures")]
1186    #[fuchsia::test(add_test_attr = false)]
1187    fn test_successive_connect_attempt_failures_cobalt_one_failure_then_timeout(n_failures: usize) {
1188        let mut test_helper = setup_test();
1189        let logger = ConnectDisconnectLogger::new(
1190            test_helper.cobalt_proxy.clone(),
1191            &test_helper.inspect_node,
1192            &test_helper.inspect_metadata_node,
1193            &test_helper.inspect_metadata_path,
1194            &test_helper.mock_time_matrix_client,
1195        );
1196
1197        let bss_description = random_bss_description!(Wpa2);
1198        for _i in 0..n_failures {
1199            let mut test_fut = pin!(logger.handle_connect_attempt(
1200                fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
1201                &bss_description,
1202                false
1203            ));
1204            assert_eq!(
1205                test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1206                Poll::Ready(())
1207            );
1208        }
1209
1210        test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(60_000_000_000));
1211        let mut test_fut = pin!(logger.handle_periodic_telemetry());
1212        assert_eq!(
1213            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1214            Poll::Ready(())
1215        );
1216
1217        // Not enough time has passed, so successive_connect_attempt_failures is not flushed yet
1218        let metrics =
1219            test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
1220        assert!(metrics.is_empty());
1221
1222        test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(120_000_000_000));
1223        let mut test_fut = pin!(logger.handle_periodic_telemetry());
1224        assert_eq!(
1225            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1226            Poll::Ready(())
1227        );
1228
1229        let metrics =
1230            test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
1231        assert_eq!(metrics.len(), 1);
1232        assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(n_failures as i64));
1233
1234        // Verify timeout fires only once
1235        test_helper.clear_cobalt_events();
1236        test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(240_000_000_000));
1237        let mut test_fut = pin!(logger.handle_periodic_telemetry());
1238        assert_eq!(
1239            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1240            Poll::Ready(())
1241        );
1242        let metrics =
1243            test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
1244        assert!(metrics.is_empty());
1245    }
1246
1247    #[fuchsia::test]
1248    fn test_zero_successive_connect_attempt_failures_on_suspend() {
1249        let mut test_helper = setup_test();
1250        let logger = ConnectDisconnectLogger::new(
1251            test_helper.cobalt_proxy.clone(),
1252            &test_helper.inspect_node,
1253            &test_helper.inspect_metadata_node,
1254            &test_helper.inspect_metadata_path,
1255            &test_helper.mock_time_matrix_client,
1256        );
1257
1258        let mut test_fut = pin!(logger.handle_suspend_imminent());
1259        assert_eq!(
1260            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1261            Poll::Ready(())
1262        );
1263
1264        let metrics =
1265            test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
1266        assert!(metrics.is_empty());
1267    }
1268
1269    #[test_case(1; "one_failure")]
1270    #[test_case(2; "two_failures")]
1271    #[fuchsia::test(add_test_attr = false)]
1272    fn test_one_or_more_successive_connect_attempt_failures_on_suspend(n_failures: usize) {
1273        let mut test_helper = setup_test();
1274        let logger = ConnectDisconnectLogger::new(
1275            test_helper.cobalt_proxy.clone(),
1276            &test_helper.inspect_node,
1277            &test_helper.inspect_metadata_node,
1278            &test_helper.inspect_metadata_path,
1279            &test_helper.mock_time_matrix_client,
1280        );
1281
1282        let bss_description = random_bss_description!(Wpa2);
1283        for _i in 0..n_failures {
1284            let mut test_fut = pin!(logger.handle_connect_attempt(
1285                fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
1286                &bss_description,
1287                false
1288            ));
1289            assert_eq!(
1290                test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1291                Poll::Ready(())
1292            );
1293        }
1294
1295        let mut test_fut = pin!(logger.handle_suspend_imminent());
1296        assert_eq!(
1297            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1298            Poll::Ready(())
1299        );
1300
1301        let metrics =
1302            test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
1303        assert_eq!(metrics.len(), 1);
1304        assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(n_failures as i64));
1305
1306        test_helper.clear_cobalt_events();
1307        let mut test_fut = pin!(logger.handle_suspend_imminent());
1308        assert_eq!(
1309            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1310            Poll::Ready(())
1311        );
1312
1313        // Count of successive failures shouldn't be logged again since it was already logged
1314        let metrics =
1315            test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
1316        assert!(metrics.is_empty());
1317
1318        // Verify that the connection state has transitioned to ConnectFailed
1319        assert_matches!(*logger.connection_state.lock(), ConnectionState::ConnectFailed(_));
1320    }
1321
1322    #[fuchsia::test]
1323    fn test_log_disconnect_inspect() {
1324        let mut test_helper = setup_test();
1325        let logger = ConnectDisconnectLogger::new(
1326            test_helper.cobalt_proxy.clone(),
1327            &test_helper.inspect_node,
1328            &test_helper.inspect_metadata_node,
1329            &test_helper.inspect_metadata_path,
1330            &test_helper.mock_time_matrix_client,
1331        );
1332
1333        // Log the event
1334        let bss_description = fake_bss_description!(Open);
1335        let channel = bss_description.channel;
1336        let disconnect_info = DisconnectInfo {
1337            iface_id: 32,
1338            connected_duration: zx::BootDuration::from_seconds(30),
1339            is_sme_reconnecting: false,
1340            disconnect_source: fidl_sme::DisconnectSource::Ap(fidl_sme::DisconnectCause {
1341                mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
1342                reason_code: fidl_ieee80211::ReasonCode::UnspecifiedReason,
1343            }),
1344            original_bss_desc: Box::new(bss_description),
1345            current_rssi_dbm: -30,
1346            current_snr_db: 25,
1347            current_channel: channel,
1348        };
1349        let mut test_fut = pin!(logger.log_disconnect(&disconnect_info));
1350        assert_eq!(
1351            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1352            Poll::Ready(())
1353        );
1354
1355        // Validate Inspect data
1356        let data = test_helper.get_inspect_data_tree();
1357        assert_data_tree!(@executor test_helper.exec, data, root: contains {
1358            test_stats: contains {
1359                metadata: contains {
1360                    connected_networks: {
1361                        "0": {
1362                            "@time": AnyNumericProperty,
1363                            "data": {
1364                                bssid: &*BSSID_REGEX,
1365                                ssid: &*SSID_REGEX,
1366                                ht_cap: AnyBytesProperty,
1367                                vht_cap: AnyBytesProperty,
1368                                protection: "Open",
1369                                is_wmm_assoc: AnyBoolProperty,
1370                                wmm_param: AnyBytesProperty,
1371                            }
1372                        }
1373                    },
1374                    disconnect_sources: {
1375                        "0": {
1376                            "@time": AnyNumericProperty,
1377                            "data": {
1378                                source: "ap",
1379                                reason: "UnspecifiedReason",
1380                                mlme_event_name: "DeauthenticateIndication",
1381                            }
1382                        }
1383                    },
1384                },
1385                disconnect_events: {
1386                    "0": {
1387                        "@time": AnyNumericProperty,
1388                        connected_duration: zx::BootDuration::from_seconds(30).into_nanos(),
1389                        disconnect_source_id: 0u64,
1390                        network_id: 0u64,
1391                        rssi_dbm: -30i64,
1392                        snr_db: 25i64,
1393                        channel: AnyStringProperty,
1394                    }
1395                }
1396            }
1397        });
1398
1399        let mut time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
1400        assert_eq!(
1401            &time_matrix_calls.drain::<u64>("wlan_connectivity_states")[..],
1402            &[TimeMatrixCall::Fold(Timed::now(1 << 0)), TimeMatrixCall::Fold(Timed::now(1 << 1)),]
1403        );
1404        assert_eq!(
1405            &time_matrix_calls.drain::<u64>("disconnected_networks")[..],
1406            &[TimeMatrixCall::Fold(Timed::now(1 << 0))]
1407        );
1408        assert_eq!(
1409            &time_matrix_calls.drain::<u64>("disconnect_sources")[..],
1410            &[TimeMatrixCall::Fold(Timed::now(1 << 0))]
1411        );
1412    }
1413
1414    #[fuchsia::test]
1415    fn test_log_disconnect_cobalt() {
1416        let mut test_helper = setup_test();
1417        let logger = ConnectDisconnectLogger::new(
1418            test_helper.cobalt_proxy.clone(),
1419            &test_helper.inspect_node,
1420            &test_helper.inspect_metadata_node,
1421            &test_helper.inspect_metadata_path,
1422            &test_helper.mock_time_matrix_client,
1423        );
1424
1425        // Log the event
1426        let disconnect_info = DisconnectInfo {
1427            connected_duration: zx::BootDuration::from_millis(300_000),
1428            disconnect_source: fidl_sme::DisconnectSource::Ap(fidl_sme::DisconnectCause {
1429                mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
1430                reason_code: fidl_ieee80211::ReasonCode::ApInitiated,
1431            }),
1432            ..fake_disconnect_info()
1433        };
1434        let mut test_fut = pin!(logger.log_disconnect(&disconnect_info));
1435        assert_eq!(
1436            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1437            Poll::Ready(())
1438        );
1439
1440        let disconnect_count_metrics =
1441            test_helper.get_logged_metrics(metrics::TOTAL_DISCONNECT_COUNT_METRIC_ID);
1442        assert_eq!(disconnect_count_metrics.len(), 1);
1443        assert_eq!(disconnect_count_metrics[0].payload, MetricEventPayload::Count(1));
1444
1445        let connected_duration_metrics =
1446            test_helper.get_logged_metrics(metrics::CONNECTED_DURATION_ON_DISCONNECT_METRIC_ID);
1447        assert_eq!(connected_duration_metrics.len(), 1);
1448        assert_eq!(
1449            connected_duration_metrics[0].payload,
1450            MetricEventPayload::IntegerValue(300_000)
1451        );
1452
1453        let disconnect_by_reason_metrics =
1454            test_helper.get_logged_metrics(metrics::DISCONNECT_BREAKDOWN_BY_REASON_CODE_METRIC_ID);
1455        assert_eq!(disconnect_by_reason_metrics.len(), 1);
1456        assert_eq!(disconnect_by_reason_metrics[0].payload, MetricEventPayload::Count(1));
1457        assert_eq!(disconnect_by_reason_metrics[0].event_codes.len(), 2);
1458        assert_eq!(
1459            disconnect_by_reason_metrics[0].event_codes[0],
1460            fidl_ieee80211::ReasonCode::ApInitiated.into_primitive() as u32
1461        );
1462        assert_eq!(
1463            disconnect_by_reason_metrics[0].event_codes[1],
1464            metrics::ConnectivityWlanMetricDimensionDisconnectSource::Ap as u32
1465        );
1466    }
1467
1468    #[test_case(
1469        fidl_sme::DisconnectSource::Ap(fidl_sme::DisconnectCause {
1470            mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
1471            reason_code: fidl_ieee80211::ReasonCode::UnspecifiedReason,
1472        }),
1473        true;
1474        "ap_disconnect_source"
1475    )]
1476    #[test_case(
1477        fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
1478            mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
1479            reason_code: fidl_ieee80211::ReasonCode::UnspecifiedReason,
1480        }),
1481        true;
1482        "mlme_disconnect_source_not_link_failed"
1483    )]
1484    #[test_case(
1485        fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
1486            mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
1487            reason_code: fidl_ieee80211::ReasonCode::MlmeLinkFailed,
1488        }),
1489        false;
1490        "mlme_link_failed"
1491    )]
1492    #[test_case(
1493        fidl_sme::DisconnectSource::User(fidl_sme::UserDisconnectReason::Unknown),
1494        false;
1495        "user_disconnect_source"
1496    )]
1497    #[fuchsia::test(add_test_attr = false)]
1498    fn test_log_disconnect_for_mobile_device_cobalt(
1499        disconnect_source: fidl_sme::DisconnectSource,
1500        should_log: bool,
1501    ) {
1502        let mut test_helper = setup_test();
1503        let logger = ConnectDisconnectLogger::new(
1504            test_helper.cobalt_proxy.clone(),
1505            &test_helper.inspect_node,
1506            &test_helper.inspect_metadata_node,
1507            &test_helper.inspect_metadata_path,
1508            &test_helper.mock_time_matrix_client,
1509        );
1510
1511        // Log the event
1512        let disconnect_info = DisconnectInfo { disconnect_source, ..fake_disconnect_info() };
1513        let mut test_fut = pin!(logger.log_disconnect(&disconnect_info));
1514        assert_eq!(
1515            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1516            Poll::Ready(())
1517        );
1518
1519        let metrics = test_helper
1520            .get_logged_metrics(metrics::DISCONNECT_OCCURRENCE_FOR_MOBILE_DEVICE_METRIC_ID);
1521        if should_log {
1522            assert_eq!(metrics.len(), 1);
1523            assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
1524            assert_matches!(*logger.connection_state.lock(), ConnectionState::Disconnected(_));
1525        } else {
1526            assert!(metrics.is_empty());
1527            assert_matches!(*logger.connection_state.lock(), ConnectionState::Idle(_));
1528        }
1529    }
1530
1531    #[fuchsia::test]
1532    fn test_log_downtime_post_disconnect_on_reconnect() {
1533        let mut test_helper = setup_test();
1534        let logger = ConnectDisconnectLogger::new(
1535            test_helper.cobalt_proxy.clone(),
1536            &test_helper.inspect_node,
1537            &test_helper.inspect_metadata_node,
1538            &test_helper.inspect_metadata_path,
1539            &test_helper.mock_time_matrix_client,
1540        );
1541
1542        // Connect at 15th second
1543        test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(15_000_000_000));
1544        let bss_description = random_bss_description!(Wpa2);
1545        let mut test_fut = pin!(logger.handle_connect_attempt(
1546            fidl_ieee80211::StatusCode::Success,
1547            &bss_description,
1548            false
1549        ));
1550        assert_eq!(
1551            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1552            Poll::Ready(())
1553        );
1554
1555        // Verify no downtime metric is logged on first successful connect
1556        let metrics = test_helper.get_logged_metrics(metrics::DOWNTIME_POST_DISCONNECT_METRIC_ID);
1557        assert!(metrics.is_empty());
1558
1559        // Verify that the connection state has transitioned to Connected
1560        assert_matches!(*logger.connection_state.lock(), ConnectionState::Connected(_));
1561
1562        // Disconnect at 25th second
1563        test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(25_000_000_000));
1564        let disconnect_info = DisconnectInfo {
1565            connected_duration: zx::BootDuration::from_millis(300_000),
1566            disconnect_source: fidl_sme::DisconnectSource::Ap(fidl_sme::DisconnectCause {
1567                mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
1568                reason_code: fidl_ieee80211::ReasonCode::ApInitiated,
1569            }),
1570            ..fake_disconnect_info()
1571        };
1572        let mut test_fut = pin!(logger.log_disconnect(&disconnect_info));
1573        assert_eq!(
1574            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1575            Poll::Ready(())
1576        );
1577
1578        // Verify that the connection state has transitioned to Disconnected
1579        assert_matches!(*logger.connection_state.lock(), ConnectionState::Disconnected(_));
1580
1581        // Reconnect at 60th second
1582        test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(60_000_000_000));
1583        let mut test_fut = pin!(logger.handle_connect_attempt(
1584            fidl_ieee80211::StatusCode::Success,
1585            &bss_description,
1586            false
1587        ));
1588        assert_eq!(
1589            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1590            Poll::Ready(())
1591        );
1592
1593        // Verify that downtime metric is logged
1594        let metrics = test_helper.get_logged_metrics(metrics::DOWNTIME_POST_DISCONNECT_METRIC_ID);
1595        assert_eq!(metrics.len(), 1);
1596        assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(35_000));
1597
1598        // Verify that the connection state has transitioned to Connected
1599        assert_matches!(*logger.connection_state.lock(), ConnectionState::Connected(_));
1600    }
1601
1602    #[fuchsia::test]
1603    fn test_log_iface_destroyed() {
1604        let mut test_helper = setup_test();
1605        let logger = ConnectDisconnectLogger::new(
1606            test_helper.cobalt_proxy.clone(),
1607            &test_helper.inspect_node,
1608            &test_helper.inspect_metadata_node,
1609            &test_helper.inspect_metadata_path,
1610            &test_helper.mock_time_matrix_client,
1611        );
1612
1613        // Log connect event to move state to connected
1614        let bss_description = random_bss_description!();
1615        let mut test_fut = pin!(logger.handle_connect_attempt(
1616            fidl_ieee80211::StatusCode::Success,
1617            &bss_description,
1618            false
1619        ));
1620        assert_eq!(
1621            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1622            Poll::Ready(())
1623        );
1624
1625        // Verify that the connection state has transitioned to Connected
1626        assert_matches!(*logger.connection_state.lock(), ConnectionState::Connected(_));
1627
1628        // Log iface destroyed event to move state to idle
1629        let mut test_fut = pin!(logger.handle_iface_destroyed());
1630        assert_eq!(
1631            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1632            Poll::Ready(())
1633        );
1634
1635        let mut time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
1636        assert_eq!(
1637            &time_matrix_calls.drain::<u64>("wlan_connectivity_states")[..],
1638            &[
1639                TimeMatrixCall::Fold(Timed::now(1 << 0)),
1640                TimeMatrixCall::Fold(Timed::now(1 << 3)),
1641                TimeMatrixCall::Fold(Timed::now(1 << 0))
1642            ]
1643        );
1644
1645        // Verify that the connection state has transitioned to Idle
1646        assert_matches!(*logger.connection_state.lock(), ConnectionState::Idle(_));
1647    }
1648
1649    #[fuchsia::test]
1650    fn test_log_disable_client_connections() {
1651        let mut test_helper = setup_test();
1652        let logger = ConnectDisconnectLogger::new(
1653            test_helper.cobalt_proxy.clone(),
1654            &test_helper.inspect_node,
1655            &test_helper.inspect_metadata_node,
1656            &test_helper.inspect_metadata_path,
1657            &test_helper.mock_time_matrix_client,
1658        );
1659
1660        // Log connect event to move state to connected
1661        let bss_description = random_bss_description!();
1662        let mut test_fut = pin!(logger.handle_connect_attempt(
1663            fidl_ieee80211::StatusCode::Success,
1664            &bss_description,
1665            false
1666        ));
1667        assert_eq!(
1668            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1669            Poll::Ready(())
1670        );
1671
1672        // Verify that the connection state has transitioned to Connected
1673        assert_matches!(*logger.connection_state.lock(), ConnectionState::Connected(_));
1674
1675        // Disable client connections to move state to idle
1676        let mut test_fut =
1677            pin!(logger.handle_client_connections_toggle(&ClientConnectionsToggleEvent::Disabled));
1678        assert_eq!(
1679            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1680            Poll::Ready(())
1681        );
1682
1683        let mut time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
1684        assert_eq!(
1685            &time_matrix_calls.drain::<u64>("wlan_connectivity_states")[..],
1686            &[
1687                TimeMatrixCall::Fold(Timed::now(1 << 0)),
1688                TimeMatrixCall::Fold(Timed::now(1 << 3)),
1689                TimeMatrixCall::Fold(Timed::now(1 << 0))
1690            ]
1691        );
1692
1693        // Verify that the connection state has transitioned to Idle
1694        assert_matches!(*logger.connection_state.lock(), ConnectionState::Idle(_));
1695    }
1696
1697    #[fuchsia::test]
1698    fn test_wlan_connectivity_states_credential_rejected() {
1699        let mut test_helper = setup_test();
1700        let logger = ConnectDisconnectLogger::new(
1701            test_helper.cobalt_proxy.clone(),
1702            &test_helper.inspect_node,
1703            &test_helper.inspect_metadata_node,
1704            &test_helper.inspect_metadata_path,
1705            &test_helper.mock_time_matrix_client,
1706        );
1707
1708        // Log connect failure with credential rejected to move state to idle
1709        let bss_description = random_bss_description!();
1710        let mut test_fut = pin!(logger.handle_connect_attempt(
1711            fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
1712            &bss_description,
1713            true
1714        ));
1715        assert_eq!(
1716            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1717            Poll::Ready(())
1718        );
1719
1720        assert_matches!(*logger.connection_state.lock(), ConnectionState::Idle(_));
1721    }
1722
1723    #[fuchsia::test]
1724    fn test_wlan_connectivity_states_failed_to_start() {
1725        let mut test_helper = setup_test();
1726        let logger = ConnectDisconnectLogger::new(
1727            test_helper.cobalt_proxy.clone(),
1728            &test_helper.inspect_node,
1729            &test_helper.inspect_metadata_node,
1730            &test_helper.inspect_metadata_path,
1731            &test_helper.mock_time_matrix_client,
1732        );
1733
1734        let mut test_fut = pin!(logger.handle_client_connections_failed_to_start());
1735        assert_eq!(
1736            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1737            Poll::Ready(())
1738        );
1739
1740        let mut time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
1741        assert_eq!(
1742            &time_matrix_calls.drain::<u64>("wlan_connectivity_states")[..],
1743            &[
1744                TimeMatrixCall::Fold(Timed::now(1 << 0)), // Initialization
1745                TimeMatrixCall::Fold(Timed::now(1 << 4)), // FailedToStart ID is 4 -> bit 1 << 4
1746            ]
1747        );
1748        assert_matches!(*logger.connection_state.lock(), ConnectionState::FailedToStart(_));
1749    }
1750
1751    #[fuchsia::test]
1752    fn test_wlan_connectivity_states_failed_to_stop() {
1753        let mut test_helper = setup_test();
1754        let logger = ConnectDisconnectLogger::new(
1755            test_helper.cobalt_proxy.clone(),
1756            &test_helper.inspect_node,
1757            &test_helper.inspect_metadata_node,
1758            &test_helper.inspect_metadata_path,
1759            &test_helper.mock_time_matrix_client,
1760        );
1761
1762        let mut test_fut = pin!(logger.handle_client_connections_failed_to_stop());
1763        assert_eq!(
1764            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1765            Poll::Ready(())
1766        );
1767
1768        let mut time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
1769        assert_eq!(
1770            &time_matrix_calls.drain::<u64>("wlan_connectivity_states")[..],
1771            &[
1772                TimeMatrixCall::Fold(Timed::now(1 << 0)), // Initialization
1773                TimeMatrixCall::Fold(Timed::now(1 << 5)), // FailedToStop ID is 5 -> bit 1 << 5
1774            ]
1775        );
1776
1777        assert_matches!(*logger.connection_state.lock(), ConnectionState::FailedToStop(_));
1778    }
1779
1780    #[test_case(ConnectionState::Idle(IdleState {}))]
1781    #[test_case(ConnectionState::Disconnected(DisconnectedState {}))]
1782    #[test_case(ConnectionState::ConnectFailed(ConnectFailedState {}))]
1783    #[test_case(ConnectionState::PnoScanFailedIdle(PnoScanFailedIdleState {}))]
1784    fn test_connectivity_state_transition_on_pno_scan_failure(initial_state: ConnectionState) {
1785        let mut test_helper = setup_test();
1786        let logger = ConnectDisconnectLogger::new(
1787            test_helper.cobalt_proxy.clone(),
1788            &test_helper.inspect_node,
1789            &test_helper.inspect_metadata_node,
1790            &test_helper.inspect_metadata_path,
1791            &test_helper.mock_time_matrix_client,
1792        );
1793
1794        // Transition to initial state
1795        *logger.connection_state.lock() = initial_state.clone();
1796
1797        // Log a PNO scan failure
1798        let mut test_fut = pin!(logger.handle_pno_scan_failure());
1799        assert_matches!(
1800            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1801            Poll::Ready(())
1802        );
1803
1804        // Verify the metrics were logged
1805        let metric_events = test_helper
1806            .get_logged_metrics(metrics::PNO_SCAN_FAILURE_WHILE_NOT_CONNECTED_OCCURRENCE_METRIC_ID);
1807        assert_eq!(metric_events.len(), 1);
1808        assert_eq!(metric_events[0].payload, MetricEventPayload::Count(1));
1809
1810        let metric_events =
1811            test_helper.get_logged_metrics(metrics::PNO_SCAN_FAILURE_OCCURRENCE_METRIC_ID);
1812        assert_eq!(metric_events.len(), 1);
1813        assert_eq!(metric_events[0].payload, MetricEventPayload::Count(1));
1814
1815        // Verify the time matrix shows the PNO scan failure state
1816        let mut time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
1817        assert_eq!(
1818            *time_matrix_calls.drain::<u64>("wlan_connectivity_states")[..].last().unwrap(),
1819            TimeMatrixCall::Fold(Timed::now(1 << 6)), // PnoScanFailedIdle ID is 6 -> bit 1 << 6
1820        );
1821
1822        // A PNO scan failure should cause a transition to PnoScanFailedIdle
1823        assert_matches!(*logger.connection_state.lock(), ConnectionState::PnoScanFailedIdle(_));
1824    }
1825
1826    #[test_case(ConnectionState::Connected(ConnectedState {}))]
1827    #[test_case(ConnectionState::FailedToStart(FailedToStartState {}))]
1828    #[test_case(ConnectionState::FailedToStop(FailedToStopState {}))]
1829    fn test_no_connectivity_state_transition_on_pno_scan_failure(initial_state: ConnectionState) {
1830        let mut test_helper = setup_test();
1831        let logger = ConnectDisconnectLogger::new(
1832            test_helper.cobalt_proxy.clone(),
1833            &test_helper.inspect_node,
1834            &test_helper.inspect_metadata_node,
1835            &test_helper.inspect_metadata_path,
1836            &test_helper.mock_time_matrix_client,
1837        );
1838
1839        // Transition to initial state
1840        *logger.connection_state.lock() = initial_state.clone();
1841
1842        // Log a PNO scan failure
1843        let mut test_fut = pin!(logger.handle_pno_scan_failure());
1844        assert_matches!(
1845            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1846            Poll::Ready(())
1847        );
1848
1849        // Verify the metrics were logged
1850        let metric_events =
1851            test_helper.get_logged_metrics(metrics::PNO_SCAN_FAILURE_OCCURRENCE_METRIC_ID);
1852        assert_eq!(metric_events.len(), 1);
1853        assert_eq!(metric_events[0].payload, MetricEventPayload::Count(1));
1854
1855        // State should not change
1856        assert_eq!(logger.connection_state.lock().to_id(), initial_state.to_id());
1857    }
1858
1859    #[fuchsia::test]
1860    fn test_wlan_connectivity_states_bitset_map_size() {
1861        let enum_variant_count = ConnectionState::iter().count();
1862        let bitset_map_size =
1863            ConnectDisconnectTimeSeries::wlan_connectivity_states_bitset_map().len();
1864        assert_eq!(enum_variant_count, bitset_map_size);
1865    }
1866
1867    fn fake_disconnect_info() -> DisconnectInfo {
1868        let bss_description = random_bss_description!(Wpa2);
1869        let channel = bss_description.channel;
1870        DisconnectInfo {
1871            iface_id: 1,
1872            connected_duration: zx::BootDuration::from_hours(6),
1873            is_sme_reconnecting: false,
1874            disconnect_source: fidl_sme::DisconnectSource::User(
1875                fidl_sme::UserDisconnectReason::Unknown,
1876            ),
1877            original_bss_desc: bss_description.into(),
1878            current_rssi_dbm: -30,
1879            current_snr_db: 25,
1880            current_channel: channel,
1881        }
1882    }
1883}