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