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