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