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::util::cobalt_logger::log_cobalt_batch;
6use derivative::Derivative;
7use fidl_fuchsia_metrics::{MetricEvent, MetricEventPayload};
8use fuchsia_inspect::Node as InspectNode;
9use fuchsia_inspect_contrib::id_enum::IdEnum;
10use fuchsia_inspect_contrib::inspect_log;
11use fuchsia_inspect_contrib::nodes::{BoundedListNode, LruCacheNode};
12use fuchsia_inspect_derive::Unit;
13use fuchsia_sync::Mutex;
14use std::sync::Arc;
15use std::sync::atomic::{AtomicUsize, Ordering};
16use strum_macros::{Display, EnumIter};
17use windowed_stats::experimental::inspect::{InspectSender, InspectedTimeMatrix};
18use windowed_stats::experimental::series::interpolation::{ConstantSample, LastSample};
19use windowed_stats::experimental::series::metadata::{BitSetMap, BitSetNode};
20use windowed_stats::experimental::series::statistic::Union;
21use windowed_stats::experimental::series::{SamplingProfile, TimeMatrix};
22use wlan_common::bss::BssDescription;
23use wlan_common::channel::Channel;
24use {
25    fidl_fuchsia_wlan_ieee80211 as fidl_ieee80211, fidl_fuchsia_wlan_sme as fidl_sme,
26    fuchsia_async as fasync, wlan_legacy_metrics_registry as metrics, zx,
27};
28
29const INSPECT_CONNECT_EVENTS_LIMIT: usize = 10;
30const INSPECT_DISCONNECT_EVENTS_LIMIT: usize = 10;
31const INSPECT_CONNECTED_NETWORKS_ID_LIMIT: usize = 16;
32const INSPECT_DISCONNECT_SOURCES_ID_LIMIT: usize = 32;
33const SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_TIMEOUT: zx::BootDuration =
34    zx::BootDuration::from_minutes(2);
35
36#[derive(Debug, Display, EnumIter)]
37enum ConnectionState {
38    Idle(IdleState),
39    Connected(ConnectedState),
40    Disconnected(DisconnectedState),
41}
42
43impl IdEnum for ConnectionState {
44    type Id = u8;
45    fn to_id(&self) -> Self::Id {
46        match self {
47            Self::Idle(_) => 0,
48            Self::Disconnected(_) => 1,
49            Self::Connected(_) => 2,
50        }
51    }
52}
53
54#[derive(Debug, Default)]
55struct IdleState {}
56
57#[derive(Debug, Default)]
58struct ConnectedState {}
59
60#[derive(Debug, Default)]
61struct DisconnectedState {}
62
63#[derive(Derivative, Unit)]
64#[derivative(PartialEq, Eq, Hash)]
65struct InspectConnectedNetwork {
66    bssid: String,
67    ssid: String,
68    protection: String,
69    ht_cap: Option<Vec<u8>>,
70    vht_cap: Option<Vec<u8>>,
71    #[derivative(PartialEq = "ignore")]
72    #[derivative(Hash = "ignore")]
73    wsc: Option<InspectNetworkWsc>,
74    is_wmm_assoc: bool,
75    wmm_param: Option<Vec<u8>>,
76}
77
78impl From<&BssDescription> for InspectConnectedNetwork {
79    fn from(bss_description: &BssDescription) -> Self {
80        Self {
81            bssid: bss_description.bssid.to_string(),
82            ssid: bss_description.ssid.to_string(),
83            protection: format!("{:?}", bss_description.protection()),
84            ht_cap: bss_description.raw_ht_cap().map(|cap| cap.bytes.into()),
85            vht_cap: bss_description.raw_vht_cap().map(|cap| cap.bytes.into()),
86            wsc: bss_description.probe_resp_wsc().as_ref().map(InspectNetworkWsc::from),
87            is_wmm_assoc: bss_description.find_wmm_param().is_some(),
88            wmm_param: bss_description.find_wmm_param().map(|bytes| bytes.into()),
89        }
90    }
91}
92
93#[derive(PartialEq, Unit, Hash)]
94struct InspectNetworkWsc {
95    device_name: String,
96    manufacturer: String,
97    model_name: String,
98    model_number: String,
99}
100
101impl From<&wlan_common::ie::wsc::ProbeRespWsc> for InspectNetworkWsc {
102    fn from(wsc: &wlan_common::ie::wsc::ProbeRespWsc) -> Self {
103        Self {
104            device_name: String::from_utf8_lossy(&wsc.device_name[..]).to_string(),
105            manufacturer: String::from_utf8_lossy(&wsc.manufacturer[..]).to_string(),
106            model_name: String::from_utf8_lossy(&wsc.model_name[..]).to_string(),
107            model_number: String::from_utf8_lossy(&wsc.model_number[..]).to_string(),
108        }
109    }
110}
111
112#[derive(PartialEq, Eq, Unit, Hash)]
113struct InspectDisconnectSource {
114    source: String,
115    reason: String,
116    mlme_event_name: Option<String>,
117}
118
119impl From<&fidl_sme::DisconnectSource> for InspectDisconnectSource {
120    fn from(disconnect_source: &fidl_sme::DisconnectSource) -> Self {
121        match disconnect_source {
122            fidl_sme::DisconnectSource::User(reason) => Self {
123                source: "user".to_string(),
124                reason: format!("{reason:?}"),
125                mlme_event_name: None,
126            },
127            fidl_sme::DisconnectSource::Ap(cause) => Self {
128                source: "ap".to_string(),
129                reason: format!("{:?}", cause.reason_code),
130                mlme_event_name: Some(format!("{:?}", cause.mlme_event_name)),
131            },
132            fidl_sme::DisconnectSource::Mlme(cause) => Self {
133                source: "mlme".to_string(),
134                reason: format!("{:?}", cause.reason_code),
135                mlme_event_name: Some(format!("{:?}", cause.mlme_event_name)),
136            },
137        }
138    }
139}
140
141#[derive(Clone, Debug, PartialEq)]
142pub struct DisconnectInfo {
143    pub iface_id: u16,
144    pub connected_duration: zx::BootDuration,
145    pub is_sme_reconnecting: bool,
146    pub disconnect_source: fidl_sme::DisconnectSource,
147    pub original_bss_desc: Box<BssDescription>,
148    pub current_rssi_dbm: i8,
149    pub current_snr_db: i8,
150    pub current_channel: Channel,
151}
152
153pub struct ConnectDisconnectLogger {
154    connection_state: Arc<Mutex<ConnectionState>>,
155    cobalt_proxy: fidl_fuchsia_metrics::MetricEventLoggerProxy,
156    connect_events_node: Mutex<BoundedListNode>,
157    disconnect_events_node: Mutex<BoundedListNode>,
158    inspect_metadata_node: Mutex<InspectMetadataNode>,
159    time_series_stats: ConnectDisconnectTimeSeries,
160    successive_connect_attempt_failures: AtomicUsize,
161    last_connect_failure_at: Arc<Mutex<Option<fasync::BootInstant>>>,
162    last_disconnect_at: Arc<Mutex<Option<fasync::MonotonicInstant>>>,
163}
164
165impl ConnectDisconnectLogger {
166    pub fn new<S: InspectSender>(
167        cobalt_proxy: fidl_fuchsia_metrics::MetricEventLoggerProxy,
168        inspect_node: &InspectNode,
169        inspect_metadata_node: &InspectNode,
170        inspect_metadata_path: &str,
171        time_matrix_client: &S,
172    ) -> Self {
173        let connect_events = inspect_node.create_child("connect_events");
174        let disconnect_events = inspect_node.create_child("disconnect_events");
175        let this = Self {
176            cobalt_proxy,
177            connection_state: Arc::new(Mutex::new(ConnectionState::Idle(IdleState {}))),
178            connect_events_node: Mutex::new(BoundedListNode::new(
179                connect_events,
180                INSPECT_CONNECT_EVENTS_LIMIT,
181            )),
182            disconnect_events_node: Mutex::new(BoundedListNode::new(
183                disconnect_events,
184                INSPECT_DISCONNECT_EVENTS_LIMIT,
185            )),
186            inspect_metadata_node: Mutex::new(InspectMetadataNode::new(inspect_metadata_node)),
187            time_series_stats: ConnectDisconnectTimeSeries::new(
188                time_matrix_client,
189                inspect_metadata_path,
190            ),
191            successive_connect_attempt_failures: AtomicUsize::new(0),
192            last_connect_failure_at: Arc::new(Mutex::new(None)),
193            last_disconnect_at: Arc::new(Mutex::new(None)),
194        };
195        this.log_connection_state();
196        this
197    }
198
199    fn update_connection_state(&self, state: ConnectionState) {
200        *self.connection_state.lock() = state;
201        self.log_connection_state();
202    }
203
204    fn log_connection_state(&self) {
205        let wlan_connectivity_state_id = self.connection_state.lock().to_id() as u64;
206        self.time_series_stats.log_wlan_connectivity_state(1 << wlan_connectivity_state_id);
207    }
208
209    pub async fn handle_connect_attempt(
210        &self,
211        result: fidl_ieee80211::StatusCode,
212        bss: &BssDescription,
213    ) {
214        let mut flushed_successive_failures = None;
215        let mut downtime_duration = None;
216        if result == fidl_ieee80211::StatusCode::Success {
217            self.update_connection_state(ConnectionState::Connected(ConnectedState {}));
218            flushed_successive_failures =
219                Some(self.successive_connect_attempt_failures.swap(0, Ordering::SeqCst));
220            downtime_duration =
221                self.last_disconnect_at.lock().map(|t| fasync::MonotonicInstant::now() - t);
222        } else {
223            self.update_connection_state(ConnectionState::Idle(IdleState {}));
224            let _prev = self.successive_connect_attempt_failures.fetch_add(1, Ordering::SeqCst);
225            let _prev = self.last_connect_failure_at.lock().replace(fasync::BootInstant::now());
226        }
227
228        self.log_connect_attempt_inspect(result, bss);
229        self.log_connect_attempt_cobalt(result, flushed_successive_failures, downtime_duration)
230            .await;
231    }
232
233    fn log_connect_attempt_inspect(
234        &self,
235        result: fidl_ieee80211::StatusCode,
236        bss: &BssDescription,
237    ) {
238        if result == fidl_ieee80211::StatusCode::Success {
239            let mut inspect_metadata_node = self.inspect_metadata_node.lock();
240            let connected_network = InspectConnectedNetwork::from(bss);
241            let connected_network_id =
242                inspect_metadata_node.connected_networks.insert(connected_network) as u64;
243
244            self.time_series_stats.log_connected_networks(1 << connected_network_id);
245
246            inspect_log!(self.connect_events_node.lock(), {
247                network_id: connected_network_id,
248            });
249        }
250    }
251
252    #[allow(clippy::vec_init_then_push, reason = "mass allow for https://fxbug.dev/381896734")]
253    async fn log_connect_attempt_cobalt(
254        &self,
255        result: fidl_ieee80211::StatusCode,
256        flushed_successive_failures: Option<usize>,
257        downtime_duration: Option<zx::MonotonicDuration>,
258    ) {
259        let mut metric_events = vec![];
260        metric_events.push(MetricEvent {
261            metric_id: metrics::CONNECT_ATTEMPT_BREAKDOWN_BY_STATUS_CODE_METRIC_ID,
262            event_codes: vec![result.into_primitive() as u32],
263            payload: MetricEventPayload::Count(1),
264        });
265
266        if let Some(failures) = flushed_successive_failures {
267            metric_events.push(MetricEvent {
268                metric_id: metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID,
269                event_codes: vec![],
270                payload: MetricEventPayload::IntegerValue(failures as i64),
271            });
272        }
273
274        if let Some(duration) = downtime_duration {
275            metric_events.push(MetricEvent {
276                metric_id: metrics::DOWNTIME_POST_DISCONNECT_METRIC_ID,
277                event_codes: vec![],
278                payload: MetricEventPayload::IntegerValue(duration.into_millis()),
279            });
280        }
281
282        log_cobalt_batch!(self.cobalt_proxy, &metric_events, "log_connect_attempt_cobalt");
283    }
284
285    pub async fn log_disconnect(&self, info: &DisconnectInfo) {
286        self.update_connection_state(ConnectionState::Disconnected(DisconnectedState {}));
287        let _prev = self.last_disconnect_at.lock().replace(fasync::MonotonicInstant::now());
288        self.log_disconnect_inspect(info);
289        self.log_disconnect_cobalt(info).await;
290    }
291
292    fn log_disconnect_inspect(&self, info: &DisconnectInfo) {
293        let mut inspect_metadata_node = self.inspect_metadata_node.lock();
294        let connected_network = InspectConnectedNetwork::from(&*info.original_bss_desc);
295        let connected_network_id =
296            inspect_metadata_node.connected_networks.insert(connected_network) as u64;
297        let disconnect_source = InspectDisconnectSource::from(&info.disconnect_source);
298        let disconnect_source_id =
299            inspect_metadata_node.disconnect_sources.insert(disconnect_source) as u64;
300        inspect_log!(self.disconnect_events_node.lock(), {
301            connected_duration: info.connected_duration.into_nanos(),
302            disconnect_source_id: disconnect_source_id,
303            network_id: connected_network_id,
304            rssi_dbm: info.current_rssi_dbm,
305            snr_db: info.current_snr_db,
306            channel: format!("{}", info.current_channel),
307        });
308
309        self.time_series_stats.log_disconnected_networks(1 << connected_network_id);
310        self.time_series_stats.log_disconnect_sources(1 << disconnect_source_id);
311    }
312
313    async fn log_disconnect_cobalt(&self, info: &DisconnectInfo) {
314        let mut metric_events = vec![];
315        metric_events.push(MetricEvent {
316            metric_id: metrics::TOTAL_DISCONNECT_COUNT_METRIC_ID,
317            event_codes: vec![],
318            payload: MetricEventPayload::Count(1),
319        });
320
321        if info.disconnect_source.should_log_for_mobile_device() {
322            metric_events.push(MetricEvent {
323                metric_id: metrics::DISCONNECT_OCCURRENCE_FOR_MOBILE_DEVICE_METRIC_ID,
324                event_codes: vec![],
325                payload: MetricEventPayload::Count(1),
326            });
327        }
328
329        metric_events.push(MetricEvent {
330            metric_id: metrics::CONNECTED_DURATION_ON_DISCONNECT_METRIC_ID,
331            event_codes: vec![],
332            payload: MetricEventPayload::IntegerValue(info.connected_duration.into_millis()),
333        });
334
335        metric_events.push(MetricEvent {
336            metric_id: metrics::DISCONNECT_BREAKDOWN_BY_REASON_CODE_METRIC_ID,
337            event_codes: vec![
338                u32::from(info.disconnect_source.cobalt_reason_code()),
339                info.disconnect_source.as_cobalt_disconnect_source() as u32,
340            ],
341            payload: MetricEventPayload::Count(1),
342        });
343
344        log_cobalt_batch!(self.cobalt_proxy, &metric_events, "log_disconnect_cobalt");
345    }
346
347    pub async fn handle_periodic_telemetry(&self) {
348        let mut metric_events = vec![];
349        let now = fasync::BootInstant::now();
350        if let Some(failed_at) = *self.last_connect_failure_at.lock()
351            && now - failed_at >= SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_TIMEOUT
352        {
353            let failures = self.successive_connect_attempt_failures.swap(0, Ordering::SeqCst);
354            if failures > 0 {
355                metric_events.push(MetricEvent {
356                    metric_id: metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID,
357                    event_codes: vec![],
358                    payload: MetricEventPayload::IntegerValue(failures as i64),
359                });
360            }
361        }
362
363        log_cobalt_batch!(self.cobalt_proxy, &metric_events, "handle_periodic_telemetry");
364    }
365
366    pub async fn handle_suspend_imminent(&self) {
367        let mut metric_events = vec![];
368
369        let flushed_successive_failures =
370            self.successive_connect_attempt_failures.swap(0, Ordering::SeqCst);
371        if flushed_successive_failures > 0 {
372            metric_events.push(MetricEvent {
373                metric_id: metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID,
374                event_codes: vec![],
375                payload: MetricEventPayload::IntegerValue(flushed_successive_failures as i64),
376            });
377        }
378
379        log_cobalt_batch!(self.cobalt_proxy, &metric_events, "handle_suspend_imminent");
380    }
381}
382
383struct InspectMetadataNode {
384    connected_networks: LruCacheNode<InspectConnectedNetwork>,
385    disconnect_sources: LruCacheNode<InspectDisconnectSource>,
386}
387
388impl InspectMetadataNode {
389    const CONNECTED_NETWORKS: &'static str = "connected_networks";
390    const DISCONNECT_SOURCES: &'static str = "disconnect_sources";
391
392    fn new(inspect_node: &InspectNode) -> Self {
393        let connected_networks = inspect_node.create_child(Self::CONNECTED_NETWORKS);
394        let disconnect_sources = inspect_node.create_child(Self::DISCONNECT_SOURCES);
395        Self {
396            connected_networks: LruCacheNode::new(
397                connected_networks,
398                INSPECT_CONNECTED_NETWORKS_ID_LIMIT,
399            ),
400            disconnect_sources: LruCacheNode::new(
401                disconnect_sources,
402                INSPECT_DISCONNECT_SOURCES_ID_LIMIT,
403            ),
404        }
405    }
406}
407
408#[derive(Debug, Clone)]
409struct ConnectDisconnectTimeSeries {
410    wlan_connectivity_states: InspectedTimeMatrix<u64>,
411    connected_networks: InspectedTimeMatrix<u64>,
412    disconnected_networks: InspectedTimeMatrix<u64>,
413    disconnect_sources: InspectedTimeMatrix<u64>,
414}
415
416impl ConnectDisconnectTimeSeries {
417    pub fn new<S: InspectSender>(client: &S, inspect_metadata_path: &str) -> Self {
418        let wlan_connectivity_states = client.inspect_time_matrix_with_metadata(
419            "wlan_connectivity_states",
420            TimeMatrix::<Union<u64>, LastSample>::new(
421                SamplingProfile::highly_granular(),
422                LastSample::or(0),
423            ),
424            BitSetMap::from_ordered(["idle", "disconnected", "connected"]),
425        );
426        let connected_networks = client.inspect_time_matrix_with_metadata(
427            "connected_networks",
428            TimeMatrix::<Union<u64>, ConstantSample>::new(
429                SamplingProfile::granular(),
430                ConstantSample::default(),
431            ),
432            BitSetNode::from_path(format!(
433                "{}/{}",
434                inspect_metadata_path,
435                InspectMetadataNode::CONNECTED_NETWORKS
436            )),
437        );
438        let disconnected_networks = client.inspect_time_matrix_with_metadata(
439            "disconnected_networks",
440            TimeMatrix::<Union<u64>, ConstantSample>::new(
441                SamplingProfile::granular(),
442                ConstantSample::default(),
443            ),
444            // This time matrix shares its bit labels with `connected_networks`.
445            BitSetNode::from_path(format!(
446                "{}/{}",
447                inspect_metadata_path,
448                InspectMetadataNode::CONNECTED_NETWORKS
449            )),
450        );
451        let disconnect_sources = client.inspect_time_matrix_with_metadata(
452            "disconnect_sources",
453            TimeMatrix::<Union<u64>, ConstantSample>::new(
454                SamplingProfile::granular(),
455                ConstantSample::default(),
456            ),
457            BitSetNode::from_path(format!(
458                "{}/{}",
459                inspect_metadata_path,
460                InspectMetadataNode::DISCONNECT_SOURCES,
461            )),
462        );
463        Self {
464            wlan_connectivity_states,
465            connected_networks,
466            disconnected_networks,
467            disconnect_sources,
468        }
469    }
470
471    fn log_wlan_connectivity_state(&self, data: u64) {
472        self.wlan_connectivity_states.fold_or_log_error(data);
473    }
474    fn log_connected_networks(&self, data: u64) {
475        self.connected_networks.fold_or_log_error(data);
476    }
477    fn log_disconnected_networks(&self, data: u64) {
478        self.disconnected_networks.fold_or_log_error(data);
479    }
480    fn log_disconnect_sources(&self, data: u64) {
481        self.disconnect_sources.fold_or_log_error(data);
482    }
483}
484
485pub trait DisconnectSourceExt {
486    fn should_log_for_mobile_device(&self) -> bool;
487    fn cobalt_reason_code(&self) -> u16;
488    fn as_cobalt_disconnect_source(
489        &self,
490    ) -> metrics::ConnectivityWlanMetricDimensionDisconnectSource;
491}
492
493impl DisconnectSourceExt for fidl_sme::DisconnectSource {
494    fn should_log_for_mobile_device(&self) -> bool {
495        match self {
496            fidl_sme::DisconnectSource::Ap(_) => true,
497            fidl_sme::DisconnectSource::Mlme(cause)
498                if cause.reason_code != fidl_ieee80211::ReasonCode::MlmeLinkFailed =>
499            {
500                true
501            }
502            _ => false,
503        }
504    }
505
506    fn cobalt_reason_code(&self) -> u16 {
507        let cobalt_disconnect_reason_code = match self {
508            fidl_sme::DisconnectSource::Ap(cause) | fidl_sme::DisconnectSource::Mlme(cause) => {
509                cause.reason_code.into_primitive()
510            }
511            fidl_sme::DisconnectSource::User(reason) => *reason as u16,
512        };
513        // This `max_event_code: 1000` is set in the metrics registry, but doesn't show up in the
514        // generated bindings.
515        const REASON_CODE_MAX: u16 = 1000;
516        std::cmp::min(cobalt_disconnect_reason_code, REASON_CODE_MAX)
517    }
518
519    fn as_cobalt_disconnect_source(
520        &self,
521    ) -> metrics::ConnectivityWlanMetricDimensionDisconnectSource {
522        use metrics::ConnectivityWlanMetricDimensionDisconnectSource as DS;
523        match self {
524            fidl_sme::DisconnectSource::Ap(..) => DS::Ap,
525            fidl_sme::DisconnectSource::User(..) => DS::User,
526            fidl_sme::DisconnectSource::Mlme(..) => DS::Mlme,
527        }
528    }
529}
530
531#[cfg(test)]
532mod tests {
533    use super::*;
534    use crate::testing::*;
535    use diagnostics_assertions::{
536        AnyBoolProperty, AnyBytesProperty, AnyNumericProperty, AnyStringProperty, assert_data_tree,
537    };
538
539    use futures::task::Poll;
540    use ieee80211_testutils::{BSSID_REGEX, SSID_REGEX};
541    use rand::Rng;
542    use std::pin::pin;
543    use test_case::test_case;
544    use windowed_stats::experimental::clock::Timed;
545    use windowed_stats::experimental::inspect::TimeMatrixClient;
546    use windowed_stats::experimental::testing::TimeMatrixCall;
547    use wlan_common::channel::{Cbw, Channel};
548    use wlan_common::{fake_bss_description, random_bss_description};
549
550    #[fuchsia::test]
551    fn log_connect_attempt_then_inspect_data_tree_contains_time_matrix_metadata() {
552        let mut harness = setup_test();
553
554        let client =
555            TimeMatrixClient::new(harness.inspect_node.create_child("wlan_connect_disconnect"));
556        let logger = ConnectDisconnectLogger::new(
557            harness.cobalt_proxy.clone(),
558            &harness.inspect_node,
559            &harness.inspect_metadata_node,
560            &harness.inspect_metadata_path,
561            &client,
562        );
563        let bss = random_bss_description!();
564        let mut log_connect_attempt =
565            pin!(logger.handle_connect_attempt(fidl_ieee80211::StatusCode::Success, &bss));
566        assert!(
567            harness.run_until_stalled_drain_cobalt_events(&mut log_connect_attempt).is_ready(),
568            "`log_connect_attempt` did not complete",
569        );
570
571        let tree = harness.get_inspect_data_tree();
572        assert_data_tree!(
573            @executor harness.exec,
574            tree,
575            root: contains {
576                test_stats: contains {
577                    wlan_connect_disconnect: contains {
578                        wlan_connectivity_states: {
579                            "type": "bitset",
580                            "data": AnyBytesProperty,
581                            metadata: {
582                                index: {
583                                    "0": "idle",
584                                    "1": "disconnected",
585                                    "2": "connected",
586                                },
587                            },
588                        },
589                        connected_networks: {
590                            "type": "bitset",
591                            "data": AnyBytesProperty,
592                            metadata: {
593                                "index_node_path": "root/test_stats/metadata/connected_networks",
594                            },
595                        },
596                        disconnected_networks: {
597                            "type": "bitset",
598                            "data": AnyBytesProperty,
599                            metadata: {
600                                "index_node_path": "root/test_stats/metadata/connected_networks",
601                            },
602                        },
603                        disconnect_sources: {
604                            "type": "bitset",
605                            "data": AnyBytesProperty,
606                            metadata: {
607                                "index_node_path": "root/test_stats/metadata/disconnect_sources",
608                            },
609                        },
610                    },
611                },
612            }
613        );
614    }
615
616    #[fuchsia::test]
617    fn test_log_connect_attempt_inspect() {
618        let mut test_helper = setup_test();
619        let logger = ConnectDisconnectLogger::new(
620            test_helper.cobalt_proxy.clone(),
621            &test_helper.inspect_node,
622            &test_helper.inspect_metadata_node,
623            &test_helper.inspect_metadata_path,
624            &test_helper.mock_time_matrix_client,
625        );
626
627        // Log the event
628        let bss_description = random_bss_description!();
629        let mut test_fut = pin!(
630            logger.handle_connect_attempt(fidl_ieee80211::StatusCode::Success, &bss_description)
631        );
632        assert_eq!(
633            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
634            Poll::Ready(())
635        );
636
637        // Validate Inspect data
638        let data = test_helper.get_inspect_data_tree();
639        assert_data_tree!(@executor test_helper.exec, data, root: contains {
640            test_stats: contains {
641                metadata: contains {
642                    connected_networks: contains {
643                        "0": {
644                            "@time": AnyNumericProperty,
645                            "data": contains {
646                                bssid: &*BSSID_REGEX,
647                                ssid: &*SSID_REGEX,
648                            }
649                        }
650                    },
651                },
652                connect_events: {
653                    "0": {
654                        "@time": AnyNumericProperty,
655                        network_id: 0u64,
656                    }
657                }
658            }
659        });
660
661        let mut time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
662        assert_eq!(
663            &time_matrix_calls.drain::<u64>("wlan_connectivity_states")[..],
664            &[TimeMatrixCall::Fold(Timed::now(1 << 0)), TimeMatrixCall::Fold(Timed::now(1 << 2)),]
665        );
666        assert_eq!(
667            &time_matrix_calls.drain::<u64>("connected_networks")[..],
668            &[TimeMatrixCall::Fold(Timed::now(1 << 0))]
669        );
670    }
671
672    #[fuchsia::test]
673    fn test_log_connect_attempt_cobalt() {
674        let mut test_helper = setup_test();
675        let logger = ConnectDisconnectLogger::new(
676            test_helper.cobalt_proxy.clone(),
677            &test_helper.inspect_node,
678            &test_helper.inspect_metadata_node,
679            &test_helper.inspect_metadata_path,
680            &test_helper.mock_time_matrix_client,
681        );
682
683        // Generate BSS Description
684        let bss_description = random_bss_description!(Wpa2,
685            channel: Channel::new(157, Cbw::Cbw40),
686            bssid: [0x00, 0xf6, 0x20, 0x03, 0x04, 0x05],
687        );
688
689        // Log the event
690        let mut test_fut = pin!(
691            logger.handle_connect_attempt(fidl_ieee80211::StatusCode::Success, &bss_description)
692        );
693        assert_eq!(
694            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
695            Poll::Ready(())
696        );
697
698        // Validate Cobalt data
699        let breakdowns_by_status_code = test_helper
700            .get_logged_metrics(metrics::CONNECT_ATTEMPT_BREAKDOWN_BY_STATUS_CODE_METRIC_ID);
701        assert_eq!(breakdowns_by_status_code.len(), 1);
702        assert_eq!(
703            breakdowns_by_status_code[0].event_codes,
704            vec![fidl_ieee80211::StatusCode::Success.into_primitive() as u32]
705        );
706        assert_eq!(breakdowns_by_status_code[0].payload, MetricEventPayload::Count(1));
707    }
708
709    #[fuchsia::test]
710    fn test_successive_connect_attempt_failures_cobalt_zero_failures() {
711        let mut test_helper = setup_test();
712        let logger = ConnectDisconnectLogger::new(
713            test_helper.cobalt_proxy.clone(),
714            &test_helper.inspect_node,
715            &test_helper.inspect_metadata_node,
716            &test_helper.inspect_metadata_path,
717            &test_helper.mock_time_matrix_client,
718        );
719
720        let bss_description = random_bss_description!(Wpa2);
721        let mut test_fut = pin!(
722            logger.handle_connect_attempt(fidl_ieee80211::StatusCode::Success, &bss_description)
723        );
724        assert_eq!(
725            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
726            Poll::Ready(())
727        );
728
729        let metrics =
730            test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
731        assert_eq!(metrics.len(), 1);
732        assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(0));
733    }
734
735    #[test_case(1; "one_failure")]
736    #[test_case(2; "two_failures")]
737    #[fuchsia::test(add_test_attr = false)]
738    fn test_successive_connect_attempt_failures_cobalt_one_failure_then_success(n_failures: usize) {
739        let mut test_helper = setup_test();
740        let logger = ConnectDisconnectLogger::new(
741            test_helper.cobalt_proxy.clone(),
742            &test_helper.inspect_node,
743            &test_helper.inspect_metadata_node,
744            &test_helper.inspect_metadata_path,
745            &test_helper.mock_time_matrix_client,
746        );
747
748        let bss_description = random_bss_description!(Wpa2);
749        for _i in 0..n_failures {
750            let mut test_fut = pin!(logger.handle_connect_attempt(
751                fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
752                &bss_description
753            ));
754            assert_eq!(
755                test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
756                Poll::Ready(())
757            );
758        }
759
760        let metrics =
761            test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
762        assert!(metrics.is_empty());
763
764        let mut test_fut = pin!(
765            logger.handle_connect_attempt(fidl_ieee80211::StatusCode::Success, &bss_description)
766        );
767        assert_eq!(
768            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
769            Poll::Ready(())
770        );
771
772        let metrics =
773            test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
774        assert_eq!(metrics.len(), 1);
775        assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(n_failures as i64));
776
777        // Verify subsequent successes would report 0 failures
778        test_helper.clear_cobalt_events();
779        let mut test_fut = pin!(
780            logger.handle_connect_attempt(fidl_ieee80211::StatusCode::Success, &bss_description)
781        );
782        assert_eq!(
783            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
784            Poll::Ready(())
785        );
786        let metrics =
787            test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
788        assert_eq!(metrics.len(), 1);
789        assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(0));
790    }
791
792    #[test_case(1; "one_failure")]
793    #[test_case(2; "two_failures")]
794    #[fuchsia::test(add_test_attr = false)]
795    fn test_successive_connect_attempt_failures_cobalt_one_failure_then_timeout(n_failures: usize) {
796        let mut test_helper = setup_test();
797        let logger = ConnectDisconnectLogger::new(
798            test_helper.cobalt_proxy.clone(),
799            &test_helper.inspect_node,
800            &test_helper.inspect_metadata_node,
801            &test_helper.inspect_metadata_path,
802            &test_helper.mock_time_matrix_client,
803        );
804
805        let bss_description = random_bss_description!(Wpa2);
806        for _i in 0..n_failures {
807            let mut test_fut = pin!(logger.handle_connect_attempt(
808                fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
809                &bss_description
810            ));
811            assert_eq!(
812                test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
813                Poll::Ready(())
814            );
815        }
816
817        test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(60_000_000_000));
818        let mut test_fut = pin!(logger.handle_periodic_telemetry());
819        assert_eq!(
820            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
821            Poll::Ready(())
822        );
823
824        // Not enough time has passed, so successive_connect_attempt_failures is not flushed yet
825        let metrics =
826            test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
827        assert!(metrics.is_empty());
828
829        test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(120_000_000_000));
830        let mut test_fut = pin!(logger.handle_periodic_telemetry());
831        assert_eq!(
832            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
833            Poll::Ready(())
834        );
835
836        let metrics =
837            test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
838        assert_eq!(metrics.len(), 1);
839        assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(n_failures as i64));
840
841        // Verify timeout fires only once
842        test_helper.clear_cobalt_events();
843        test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(240_000_000_000));
844        let mut test_fut = pin!(logger.handle_periodic_telemetry());
845        assert_eq!(
846            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
847            Poll::Ready(())
848        );
849        let metrics =
850            test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
851        assert!(metrics.is_empty());
852    }
853
854    #[fuchsia::test]
855    fn test_zero_successive_connect_attempt_failures_on_suspend() {
856        let mut test_helper = setup_test();
857        let logger = ConnectDisconnectLogger::new(
858            test_helper.cobalt_proxy.clone(),
859            &test_helper.inspect_node,
860            &test_helper.inspect_metadata_node,
861            &test_helper.inspect_metadata_path,
862            &test_helper.mock_time_matrix_client,
863        );
864
865        let mut test_fut = pin!(logger.handle_suspend_imminent());
866        assert_eq!(
867            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
868            Poll::Ready(())
869        );
870
871        let metrics =
872            test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
873        assert!(metrics.is_empty());
874    }
875
876    #[test_case(1; "one_failure")]
877    #[test_case(2; "two_failures")]
878    #[fuchsia::test(add_test_attr = false)]
879    fn test_one_or_more_successive_connect_attempt_failures_on_suspend(n_failures: usize) {
880        let mut test_helper = setup_test();
881        let logger = ConnectDisconnectLogger::new(
882            test_helper.cobalt_proxy.clone(),
883            &test_helper.inspect_node,
884            &test_helper.inspect_metadata_node,
885            &test_helper.inspect_metadata_path,
886            &test_helper.mock_time_matrix_client,
887        );
888
889        let bss_description = random_bss_description!(Wpa2);
890        for _i in 0..n_failures {
891            let mut test_fut = pin!(logger.handle_connect_attempt(
892                fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
893                &bss_description
894            ));
895            assert_eq!(
896                test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
897                Poll::Ready(())
898            );
899        }
900
901        let mut test_fut = pin!(logger.handle_suspend_imminent());
902        assert_eq!(
903            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
904            Poll::Ready(())
905        );
906
907        let metrics =
908            test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
909        assert_eq!(metrics.len(), 1);
910        assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(n_failures as i64));
911
912        test_helper.clear_cobalt_events();
913        let mut test_fut = pin!(logger.handle_suspend_imminent());
914        assert_eq!(
915            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
916            Poll::Ready(())
917        );
918
919        // Count of successive failures shouldn't be logged again since it was already logged
920        let metrics =
921            test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
922        assert!(metrics.is_empty());
923    }
924
925    #[fuchsia::test]
926    fn test_log_disconnect_inspect() {
927        let mut test_helper = setup_test();
928        let logger = ConnectDisconnectLogger::new(
929            test_helper.cobalt_proxy.clone(),
930            &test_helper.inspect_node,
931            &test_helper.inspect_metadata_node,
932            &test_helper.inspect_metadata_path,
933            &test_helper.mock_time_matrix_client,
934        );
935
936        // Log the event
937        let bss_description = fake_bss_description!(Open);
938        let channel = bss_description.channel;
939        let disconnect_info = DisconnectInfo {
940            iface_id: 32,
941            connected_duration: zx::BootDuration::from_seconds(30),
942            is_sme_reconnecting: false,
943            disconnect_source: fidl_sme::DisconnectSource::Ap(fidl_sme::DisconnectCause {
944                mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
945                reason_code: fidl_ieee80211::ReasonCode::UnspecifiedReason,
946            }),
947            original_bss_desc: Box::new(bss_description),
948            current_rssi_dbm: -30,
949            current_snr_db: 25,
950            current_channel: channel,
951        };
952        let mut test_fut = pin!(logger.log_disconnect(&disconnect_info));
953        assert_eq!(
954            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
955            Poll::Ready(())
956        );
957
958        // Validate Inspect data
959        let data = test_helper.get_inspect_data_tree();
960        assert_data_tree!(@executor test_helper.exec, data, root: contains {
961            test_stats: contains {
962                metadata: {
963                    connected_networks: {
964                        "0": {
965                            "@time": AnyNumericProperty,
966                            "data": {
967                                bssid: &*BSSID_REGEX,
968                                ssid: &*SSID_REGEX,
969                                ht_cap: AnyBytesProperty,
970                                vht_cap: AnyBytesProperty,
971                                protection: "Open",
972                                is_wmm_assoc: AnyBoolProperty,
973                                wmm_param: AnyBytesProperty,
974                            }
975                        }
976                    },
977                    disconnect_sources: {
978                        "0": {
979                            "@time": AnyNumericProperty,
980                            "data": {
981                                source: "ap",
982                                reason: "UnspecifiedReason",
983                                mlme_event_name: "DeauthenticateIndication",
984                            }
985                        }
986                    },
987                },
988                disconnect_events: {
989                    "0": {
990                        "@time": AnyNumericProperty,
991                        connected_duration: zx::BootDuration::from_seconds(30).into_nanos(),
992                        disconnect_source_id: 0u64,
993                        network_id: 0u64,
994                        rssi_dbm: -30i64,
995                        snr_db: 25i64,
996                        channel: AnyStringProperty,
997                    }
998                }
999            }
1000        });
1001
1002        let mut time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
1003        assert_eq!(
1004            &time_matrix_calls.drain::<u64>("wlan_connectivity_states")[..],
1005            &[TimeMatrixCall::Fold(Timed::now(1 << 0)), TimeMatrixCall::Fold(Timed::now(1 << 1)),]
1006        );
1007        assert_eq!(
1008            &time_matrix_calls.drain::<u64>("disconnected_networks")[..],
1009            &[TimeMatrixCall::Fold(Timed::now(1 << 0))]
1010        );
1011        assert_eq!(
1012            &time_matrix_calls.drain::<u64>("disconnect_sources")[..],
1013            &[TimeMatrixCall::Fold(Timed::now(1 << 0))]
1014        );
1015    }
1016
1017    #[fuchsia::test]
1018    fn test_log_disconnect_cobalt() {
1019        let mut test_helper = setup_test();
1020        let logger = ConnectDisconnectLogger::new(
1021            test_helper.cobalt_proxy.clone(),
1022            &test_helper.inspect_node,
1023            &test_helper.inspect_metadata_node,
1024            &test_helper.inspect_metadata_path,
1025            &test_helper.mock_time_matrix_client,
1026        );
1027
1028        // Log the event
1029        let disconnect_info = DisconnectInfo {
1030            connected_duration: zx::BootDuration::from_millis(300_000),
1031            disconnect_source: fidl_sme::DisconnectSource::Ap(fidl_sme::DisconnectCause {
1032                mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
1033                reason_code: fidl_ieee80211::ReasonCode::ApInitiated,
1034            }),
1035            ..fake_disconnect_info()
1036        };
1037        let mut test_fut = pin!(logger.log_disconnect(&disconnect_info));
1038        assert_eq!(
1039            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1040            Poll::Ready(())
1041        );
1042
1043        let disconnect_count_metrics =
1044            test_helper.get_logged_metrics(metrics::TOTAL_DISCONNECT_COUNT_METRIC_ID);
1045        assert_eq!(disconnect_count_metrics.len(), 1);
1046        assert_eq!(disconnect_count_metrics[0].payload, MetricEventPayload::Count(1));
1047
1048        let connected_duration_metrics =
1049            test_helper.get_logged_metrics(metrics::CONNECTED_DURATION_ON_DISCONNECT_METRIC_ID);
1050        assert_eq!(connected_duration_metrics.len(), 1);
1051        assert_eq!(
1052            connected_duration_metrics[0].payload,
1053            MetricEventPayload::IntegerValue(300_000)
1054        );
1055
1056        let disconnect_by_reason_metrics =
1057            test_helper.get_logged_metrics(metrics::DISCONNECT_BREAKDOWN_BY_REASON_CODE_METRIC_ID);
1058        assert_eq!(disconnect_by_reason_metrics.len(), 1);
1059        assert_eq!(disconnect_by_reason_metrics[0].payload, MetricEventPayload::Count(1));
1060        assert_eq!(disconnect_by_reason_metrics[0].event_codes.len(), 2);
1061        assert_eq!(
1062            disconnect_by_reason_metrics[0].event_codes[0],
1063            fidl_ieee80211::ReasonCode::ApInitiated.into_primitive() as u32
1064        );
1065        assert_eq!(
1066            disconnect_by_reason_metrics[0].event_codes[1],
1067            metrics::ConnectivityWlanMetricDimensionDisconnectSource::Ap as u32
1068        );
1069    }
1070
1071    #[test_case(
1072        fidl_sme::DisconnectSource::Ap(fidl_sme::DisconnectCause {
1073            mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
1074            reason_code: fidl_ieee80211::ReasonCode::UnspecifiedReason,
1075        }),
1076        true;
1077        "ap_disconnect_source"
1078    )]
1079    #[test_case(
1080        fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
1081            mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
1082            reason_code: fidl_ieee80211::ReasonCode::UnspecifiedReason,
1083        }),
1084        true;
1085        "mlme_disconnect_source_not_link_failed"
1086    )]
1087    #[test_case(
1088        fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
1089            mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
1090            reason_code: fidl_ieee80211::ReasonCode::MlmeLinkFailed,
1091        }),
1092        false;
1093        "mlme_link_failed"
1094    )]
1095    #[test_case(
1096        fidl_sme::DisconnectSource::User(fidl_sme::UserDisconnectReason::Unknown),
1097        false;
1098        "user_disconnect_source"
1099    )]
1100    #[fuchsia::test(add_test_attr = false)]
1101    fn test_log_disconnect_for_mobile_device_cobalt(
1102        disconnect_source: fidl_sme::DisconnectSource,
1103        should_log: bool,
1104    ) {
1105        let mut test_helper = setup_test();
1106        let logger = ConnectDisconnectLogger::new(
1107            test_helper.cobalt_proxy.clone(),
1108            &test_helper.inspect_node,
1109            &test_helper.inspect_metadata_node,
1110            &test_helper.inspect_metadata_path,
1111            &test_helper.mock_time_matrix_client,
1112        );
1113
1114        // Log the event
1115        let disconnect_info = DisconnectInfo { disconnect_source, ..fake_disconnect_info() };
1116        let mut test_fut = pin!(logger.log_disconnect(&disconnect_info));
1117        assert_eq!(
1118            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1119            Poll::Ready(())
1120        );
1121
1122        let metrics = test_helper
1123            .get_logged_metrics(metrics::DISCONNECT_OCCURRENCE_FOR_MOBILE_DEVICE_METRIC_ID);
1124        if should_log {
1125            assert_eq!(metrics.len(), 1);
1126            assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
1127        } else {
1128            assert!(metrics.is_empty());
1129        }
1130    }
1131
1132    #[fuchsia::test]
1133    fn test_log_downtime_post_disconnect_on_reconnect() {
1134        let mut test_helper = setup_test();
1135        let logger = ConnectDisconnectLogger::new(
1136            test_helper.cobalt_proxy.clone(),
1137            &test_helper.inspect_node,
1138            &test_helper.inspect_metadata_node,
1139            &test_helper.inspect_metadata_path,
1140            &test_helper.mock_time_matrix_client,
1141        );
1142
1143        // Connect at 15th second
1144        test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(15_000_000_000));
1145        let bss_description = random_bss_description!(Wpa2);
1146        let mut test_fut = pin!(
1147            logger.handle_connect_attempt(fidl_ieee80211::StatusCode::Success, &bss_description)
1148        );
1149        assert_eq!(
1150            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1151            Poll::Ready(())
1152        );
1153
1154        // Verify no downtime metric is logged on first successful connect
1155        let metrics = test_helper.get_logged_metrics(metrics::DOWNTIME_POST_DISCONNECT_METRIC_ID);
1156        assert!(metrics.is_empty());
1157
1158        // Disconnect at 25th second
1159        test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(25_000_000_000));
1160        let disconnect_info = fake_disconnect_info();
1161        let mut test_fut = pin!(logger.log_disconnect(&disconnect_info));
1162        assert_eq!(
1163            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1164            Poll::Ready(())
1165        );
1166
1167        // Reconnect at 60th second
1168        test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(60_000_000_000));
1169        let mut test_fut = pin!(
1170            logger.handle_connect_attempt(fidl_ieee80211::StatusCode::Success, &bss_description)
1171        );
1172        assert_eq!(
1173            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1174            Poll::Ready(())
1175        );
1176
1177        // Verify that downtime metric is logged
1178        let metrics = test_helper.get_logged_metrics(metrics::DOWNTIME_POST_DISCONNECT_METRIC_ID);
1179        assert_eq!(metrics.len(), 1);
1180        assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(35_000));
1181    }
1182
1183    fn fake_disconnect_info() -> DisconnectInfo {
1184        let bss_description = random_bss_description!(Wpa2);
1185        let channel = bss_description.channel;
1186        DisconnectInfo {
1187            iface_id: 1,
1188            connected_duration: zx::BootDuration::from_hours(6),
1189            is_sme_reconnecting: false,
1190            disconnect_source: fidl_sme::DisconnectSource::User(
1191                fidl_sme::UserDisconnectReason::Unknown,
1192            ),
1193            original_bss_desc: bss_description.into(),
1194            current_rssi_dbm: -30,
1195            current_snr_db: 25,
1196            current_channel: channel,
1197        }
1198    }
1199}