Skip to main content

wlan_telemetry/processors/
connect_disconnect.rs

1// Copyright 2024 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use crate::processors::toggle_events::ClientConnectionsToggleEvent;
6use crate::util::cobalt_logger::log_cobalt_batch;
7use derivative::Derivative;
8use fidl_fuchsia_metrics::{MetricEvent, MetricEventPayload};
9use fuchsia_inspect::Node as InspectNode;
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<BoundedListNode>,
158    disconnect_events_node: Mutex<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        time_matrix_client: &S,
173    ) -> Self {
174        let connect_events = inspect_node.create_child("connect_events");
175        let disconnect_events = inspect_node.create_child("disconnect_events");
176        let this = Self {
177            cobalt_proxy,
178            connection_state: Arc::new(Mutex::new(ConnectionState::Idle(IdleState {}))),
179            connect_events_node: Mutex::new(BoundedListNode::new(
180                connect_events,
181                INSPECT_CONNECT_EVENTS_LIMIT,
182            )),
183            disconnect_events_node: Mutex::new(BoundedListNode::new(
184                disconnect_events,
185                INSPECT_DISCONNECT_EVENTS_LIMIT,
186            )),
187            inspect_metadata_node: Mutex::new(InspectMetadataNode::new(inspect_metadata_node)),
188            time_series_stats: ConnectDisconnectTimeSeries::new(
189                time_matrix_client,
190                inspect_metadata_path,
191            ),
192            successive_connect_attempt_failures: AtomicUsize::new(0),
193            last_connect_failure_at: Arc::new(Mutex::new(None)),
194            last_disconnect_at: Arc::new(Mutex::new(None)),
195        };
196        this.log_connection_state();
197        this
198    }
199
200    fn update_connection_state(&self, state: ConnectionState) {
201        *self.connection_state.lock() = state;
202        self.log_connection_state();
203    }
204
205    fn log_connection_state(&self) {
206        let wlan_connectivity_state_id = self.connection_state.lock().to_id() as u64;
207        self.time_series_stats.log_wlan_connectivity_state(1 << wlan_connectivity_state_id);
208    }
209
210    pub async fn handle_connect_attempt(
211        &self,
212        result: fidl_ieee80211::StatusCode,
213        bss: &BssDescription,
214    ) {
215        let mut flushed_successive_failures = None;
216        let mut downtime_duration = None;
217        if result == fidl_ieee80211::StatusCode::Success {
218            self.update_connection_state(ConnectionState::Connected(ConnectedState {}));
219            flushed_successive_failures =
220                Some(self.successive_connect_attempt_failures.swap(0, Ordering::SeqCst));
221            downtime_duration =
222                self.last_disconnect_at.lock().map(|t| fasync::MonotonicInstant::now() - t);
223        } else {
224            self.update_connection_state(ConnectionState::Idle(IdleState {}));
225            let _prev = self.successive_connect_attempt_failures.fetch_add(1, Ordering::SeqCst);
226            let _prev = self.last_connect_failure_at.lock().replace(fasync::BootInstant::now());
227        }
228
229        self.log_connect_attempt_inspect(result, bss);
230        self.log_connect_attempt_cobalt(result, flushed_successive_failures, downtime_duration)
231            .await;
232    }
233
234    fn log_connect_attempt_inspect(
235        &self,
236        result: fidl_ieee80211::StatusCode,
237        bss: &BssDescription,
238    ) {
239        if result == fidl_ieee80211::StatusCode::Success {
240            let mut inspect_metadata_node = self.inspect_metadata_node.lock();
241            let connected_network = InspectConnectedNetwork::from(bss);
242            let connected_network_id =
243                inspect_metadata_node.connected_networks.insert(connected_network) as u64;
244
245            self.time_series_stats.log_connected_networks(1 << connected_network_id);
246
247            inspect_log!(self.connect_events_node.lock(), {
248                network_id: connected_network_id,
249            });
250        }
251    }
252
253    #[allow(clippy::vec_init_then_push, reason = "mass allow for https://fxbug.dev/381896734")]
254    async fn log_connect_attempt_cobalt(
255        &self,
256        result: fidl_ieee80211::StatusCode,
257        flushed_successive_failures: Option<usize>,
258        downtime_duration: Option<zx::MonotonicDuration>,
259    ) {
260        let mut metric_events = vec![];
261        metric_events.push(MetricEvent {
262            metric_id: metrics::CONNECT_ATTEMPT_BREAKDOWN_BY_STATUS_CODE_METRIC_ID,
263            event_codes: vec![result.into_primitive() as u32],
264            payload: MetricEventPayload::Count(1),
265        });
266
267        if let Some(failures) = flushed_successive_failures {
268            metric_events.push(MetricEvent {
269                metric_id: metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID,
270                event_codes: vec![],
271                payload: MetricEventPayload::IntegerValue(failures as i64),
272            });
273        }
274
275        if let Some(duration) = downtime_duration {
276            metric_events.push(MetricEvent {
277                metric_id: metrics::DOWNTIME_POST_DISCONNECT_METRIC_ID,
278                event_codes: vec![],
279                payload: MetricEventPayload::IntegerValue(duration.into_millis()),
280            });
281        }
282
283        log_cobalt_batch!(self.cobalt_proxy, &metric_events, "log_connect_attempt_cobalt");
284    }
285
286    pub async fn log_disconnect(&self, info: &DisconnectInfo) {
287        self.update_connection_state(ConnectionState::Disconnected(DisconnectedState {}));
288        let _prev = self.last_disconnect_at.lock().replace(fasync::MonotonicInstant::now());
289        self.log_disconnect_inspect(info);
290        self.log_disconnect_cobalt(info).await;
291    }
292
293    fn log_disconnect_inspect(&self, info: &DisconnectInfo) {
294        let mut inspect_metadata_node = self.inspect_metadata_node.lock();
295        let connected_network = InspectConnectedNetwork::from(&*info.original_bss_desc);
296        let connected_network_id =
297            inspect_metadata_node.connected_networks.insert(connected_network) as u64;
298        let disconnect_source = InspectDisconnectSource::from(&info.disconnect_source);
299        let disconnect_source_id =
300            inspect_metadata_node.disconnect_sources.insert(disconnect_source) as u64;
301        inspect_log!(self.disconnect_events_node.lock(), {
302            connected_duration: info.connected_duration.into_nanos(),
303            disconnect_source_id: disconnect_source_id,
304            network_id: connected_network_id,
305            rssi_dbm: info.current_rssi_dbm,
306            snr_db: info.current_snr_db,
307            channel: format!("{}", info.current_channel),
308        });
309
310        self.time_series_stats.log_disconnected_networks(1 << connected_network_id);
311        self.time_series_stats.log_disconnect_sources(1 << disconnect_source_id);
312    }
313
314    async fn log_disconnect_cobalt(&self, info: &DisconnectInfo) {
315        let mut metric_events = vec![];
316        metric_events.push(MetricEvent {
317            metric_id: metrics::TOTAL_DISCONNECT_COUNT_METRIC_ID,
318            event_codes: vec![],
319            payload: MetricEventPayload::Count(1),
320        });
321
322        if info.disconnect_source.should_log_for_mobile_device() {
323            metric_events.push(MetricEvent {
324                metric_id: metrics::DISCONNECT_OCCURRENCE_FOR_MOBILE_DEVICE_METRIC_ID,
325                event_codes: vec![],
326                payload: MetricEventPayload::Count(1),
327            });
328        }
329
330        metric_events.push(MetricEvent {
331            metric_id: metrics::CONNECTED_DURATION_ON_DISCONNECT_METRIC_ID,
332            event_codes: vec![],
333            payload: MetricEventPayload::IntegerValue(info.connected_duration.into_millis()),
334        });
335
336        metric_events.push(MetricEvent {
337            metric_id: metrics::DISCONNECT_BREAKDOWN_BY_REASON_CODE_METRIC_ID,
338            event_codes: vec![
339                u32::from(info.disconnect_source.cobalt_reason_code()),
340                info.disconnect_source.as_cobalt_disconnect_source() as u32,
341            ],
342            payload: MetricEventPayload::Count(1),
343        });
344
345        log_cobalt_batch!(self.cobalt_proxy, &metric_events, "log_disconnect_cobalt");
346    }
347
348    pub async fn handle_periodic_telemetry(&self) {
349        let mut metric_events = vec![];
350        let now = fasync::BootInstant::now();
351        if let Some(failed_at) = *self.last_connect_failure_at.lock()
352            && now - failed_at >= SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_TIMEOUT
353        {
354            let failures = self.successive_connect_attempt_failures.swap(0, Ordering::SeqCst);
355            if failures > 0 {
356                metric_events.push(MetricEvent {
357                    metric_id: metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID,
358                    event_codes: vec![],
359                    payload: MetricEventPayload::IntegerValue(failures as i64),
360                });
361            }
362        }
363
364        log_cobalt_batch!(self.cobalt_proxy, &metric_events, "handle_periodic_telemetry");
365    }
366
367    pub async fn handle_suspend_imminent(&self) {
368        let mut metric_events = vec![];
369
370        let flushed_successive_failures =
371            self.successive_connect_attempt_failures.swap(0, Ordering::SeqCst);
372        if flushed_successive_failures > 0 {
373            metric_events.push(MetricEvent {
374                metric_id: metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID,
375                event_codes: vec![],
376                payload: MetricEventPayload::IntegerValue(flushed_successive_failures as i64),
377            });
378        }
379
380        log_cobalt_batch!(self.cobalt_proxy, &metric_events, "handle_suspend_imminent");
381    }
382
383    pub async fn handle_iface_destroyed(&self) {
384        self.update_connection_state(ConnectionState::Idle(IdleState {}));
385    }
386
387    pub async fn handle_client_connections_toggle(&self, event: &ClientConnectionsToggleEvent) {
388        if event == &ClientConnectionsToggleEvent::Disabled {
389            self.update_connection_state(ConnectionState::Idle(IdleState {}));
390        }
391    }
392}
393
394struct InspectMetadataNode {
395    connected_networks: LruCacheNode<InspectConnectedNetwork>,
396    disconnect_sources: LruCacheNode<InspectDisconnectSource>,
397}
398
399impl InspectMetadataNode {
400    const CONNECTED_NETWORKS: &'static str = "connected_networks";
401    const DISCONNECT_SOURCES: &'static str = "disconnect_sources";
402
403    fn new(inspect_node: &InspectNode) -> Self {
404        let connected_networks = inspect_node.create_child(Self::CONNECTED_NETWORKS);
405        let disconnect_sources = inspect_node.create_child(Self::DISCONNECT_SOURCES);
406        Self {
407            connected_networks: LruCacheNode::new(
408                connected_networks,
409                INSPECT_CONNECTED_NETWORKS_ID_LIMIT,
410            ),
411            disconnect_sources: LruCacheNode::new(
412                disconnect_sources,
413                INSPECT_DISCONNECT_SOURCES_ID_LIMIT,
414            ),
415        }
416    }
417}
418
419#[derive(Debug, Clone)]
420struct ConnectDisconnectTimeSeries {
421    wlan_connectivity_states: InspectedTimeMatrix<u64>,
422    connected_networks: InspectedTimeMatrix<u64>,
423    disconnected_networks: InspectedTimeMatrix<u64>,
424    disconnect_sources: InspectedTimeMatrix<u64>,
425}
426
427impl ConnectDisconnectTimeSeries {
428    pub fn new<S: InspectSender>(client: &S, inspect_metadata_path: &str) -> Self {
429        let wlan_connectivity_states = client.inspect_time_matrix_with_metadata(
430            "wlan_connectivity_states",
431            TimeMatrix::<Union<u64>, LastSample>::new(
432                SamplingProfile::highly_granular(),
433                LastSample::or(0),
434            ),
435            BitSetMap::from_ordered(["idle", "disconnected", "connected"]),
436        );
437        let connected_networks = client.inspect_time_matrix_with_metadata(
438            "connected_networks",
439            TimeMatrix::<Union<u64>, ConstantSample>::new(
440                SamplingProfile::granular(),
441                ConstantSample::default(),
442            ),
443            BitSetNode::from_path(format!(
444                "{}/{}",
445                inspect_metadata_path,
446                InspectMetadataNode::CONNECTED_NETWORKS
447            )),
448        );
449        let disconnected_networks = client.inspect_time_matrix_with_metadata(
450            "disconnected_networks",
451            TimeMatrix::<Union<u64>, ConstantSample>::new(
452                SamplingProfile::granular(),
453                ConstantSample::default(),
454            ),
455            // This time matrix shares its bit labels with `connected_networks`.
456            BitSetNode::from_path(format!(
457                "{}/{}",
458                inspect_metadata_path,
459                InspectMetadataNode::CONNECTED_NETWORKS
460            )),
461        );
462        let disconnect_sources = client.inspect_time_matrix_with_metadata(
463            "disconnect_sources",
464            TimeMatrix::<Union<u64>, ConstantSample>::new(
465                SamplingProfile::granular(),
466                ConstantSample::default(),
467            ),
468            BitSetNode::from_path(format!(
469                "{}/{}",
470                inspect_metadata_path,
471                InspectMetadataNode::DISCONNECT_SOURCES,
472            )),
473        );
474        Self {
475            wlan_connectivity_states,
476            connected_networks,
477            disconnected_networks,
478            disconnect_sources,
479        }
480    }
481
482    fn log_wlan_connectivity_state(&self, data: u64) {
483        self.wlan_connectivity_states.fold_or_log_error(data);
484    }
485    fn log_connected_networks(&self, data: u64) {
486        self.connected_networks.fold_or_log_error(data);
487    }
488    fn log_disconnected_networks(&self, data: u64) {
489        self.disconnected_networks.fold_or_log_error(data);
490    }
491    fn log_disconnect_sources(&self, data: u64) {
492        self.disconnect_sources.fold_or_log_error(data);
493    }
494}
495
496pub trait DisconnectSourceExt {
497    fn should_log_for_mobile_device(&self) -> bool;
498    fn cobalt_reason_code(&self) -> u16;
499    fn as_cobalt_disconnect_source(
500        &self,
501    ) -> metrics::ConnectivityWlanMetricDimensionDisconnectSource;
502}
503
504impl DisconnectSourceExt for fidl_sme::DisconnectSource {
505    fn should_log_for_mobile_device(&self) -> bool {
506        match self {
507            fidl_sme::DisconnectSource::Ap(_) => true,
508            fidl_sme::DisconnectSource::Mlme(cause)
509                if cause.reason_code != fidl_ieee80211::ReasonCode::MlmeLinkFailed =>
510            {
511                true
512            }
513            _ => false,
514        }
515    }
516
517    fn cobalt_reason_code(&self) -> u16 {
518        let cobalt_disconnect_reason_code = match self {
519            fidl_sme::DisconnectSource::Ap(cause) | fidl_sme::DisconnectSource::Mlme(cause) => {
520                cause.reason_code.into_primitive()
521            }
522            fidl_sme::DisconnectSource::User(reason) => *reason as u16,
523        };
524        // This `max_event_code: 1000` is set in the metrics registry, but doesn't show up in the
525        // generated bindings.
526        const REASON_CODE_MAX: u16 = 1000;
527        std::cmp::min(cobalt_disconnect_reason_code, REASON_CODE_MAX)
528    }
529
530    fn as_cobalt_disconnect_source(
531        &self,
532    ) -> metrics::ConnectivityWlanMetricDimensionDisconnectSource {
533        use metrics::ConnectivityWlanMetricDimensionDisconnectSource as DS;
534        match self {
535            fidl_sme::DisconnectSource::Ap(..) => DS::Ap,
536            fidl_sme::DisconnectSource::User(..) => DS::User,
537            fidl_sme::DisconnectSource::Mlme(..) => DS::Mlme,
538        }
539    }
540}
541
542#[cfg(test)]
543mod tests {
544    use super::*;
545    use crate::testing::*;
546    use diagnostics_assertions::{
547        AnyBoolProperty, AnyBytesProperty, AnyNumericProperty, AnyStringProperty, assert_data_tree,
548    };
549
550    use futures::task::Poll;
551    use ieee80211_testutils::{BSSID_REGEX, SSID_REGEX};
552    use rand::Rng;
553    use std::pin::pin;
554    use test_case::test_case;
555    use windowed_stats::experimental::clock::Timed;
556    use windowed_stats::experimental::inspect::TimeMatrixClient;
557    use windowed_stats::experimental::testing::TimeMatrixCall;
558    use wlan_common::channel::{Cbw, Channel};
559    use wlan_common::{fake_bss_description, random_bss_description};
560
561    #[fuchsia::test]
562    fn log_connect_attempt_then_inspect_data_tree_contains_time_matrix_metadata() {
563        let mut harness = setup_test();
564
565        let client =
566            TimeMatrixClient::new(harness.inspect_node.create_child("wlan_connect_disconnect"));
567        let logger = ConnectDisconnectLogger::new(
568            harness.cobalt_proxy.clone(),
569            &harness.inspect_node,
570            &harness.inspect_metadata_node,
571            &harness.inspect_metadata_path,
572            &client,
573        );
574        let bss = random_bss_description!();
575        let mut log_connect_attempt =
576            pin!(logger.handle_connect_attempt(fidl_ieee80211::StatusCode::Success, &bss));
577        assert!(
578            harness.run_until_stalled_drain_cobalt_events(&mut log_connect_attempt).is_ready(),
579            "`log_connect_attempt` did not complete",
580        );
581
582        let tree = harness.get_inspect_data_tree();
583        assert_data_tree!(
584            @executor harness.exec,
585            tree,
586            root: contains {
587                test_stats: contains {
588                    wlan_connect_disconnect: contains {
589                        wlan_connectivity_states: {
590                            "type": "bitset",
591                            "data": AnyBytesProperty,
592                            metadata: {
593                                index: {
594                                    "0": "idle",
595                                    "1": "disconnected",
596                                    "2": "connected",
597                                },
598                            },
599                        },
600                        connected_networks: {
601                            "type": "bitset",
602                            "data": AnyBytesProperty,
603                            metadata: {
604                                "index_node_path": "root/test_stats/metadata/connected_networks",
605                            },
606                        },
607                        disconnected_networks: {
608                            "type": "bitset",
609                            "data": AnyBytesProperty,
610                            metadata: {
611                                "index_node_path": "root/test_stats/metadata/connected_networks",
612                            },
613                        },
614                        disconnect_sources: {
615                            "type": "bitset",
616                            "data": AnyBytesProperty,
617                            metadata: {
618                                "index_node_path": "root/test_stats/metadata/disconnect_sources",
619                            },
620                        },
621                    },
622                },
623            }
624        );
625    }
626
627    #[fuchsia::test]
628    fn test_log_connect_attempt_inspect() {
629        let mut test_helper = setup_test();
630        let logger = ConnectDisconnectLogger::new(
631            test_helper.cobalt_proxy.clone(),
632            &test_helper.inspect_node,
633            &test_helper.inspect_metadata_node,
634            &test_helper.inspect_metadata_path,
635            &test_helper.mock_time_matrix_client,
636        );
637
638        // Log the event
639        let bss_description = random_bss_description!();
640        let mut test_fut = pin!(
641            logger.handle_connect_attempt(fidl_ieee80211::StatusCode::Success, &bss_description)
642        );
643        assert_eq!(
644            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
645            Poll::Ready(())
646        );
647
648        // Validate Inspect data
649        let data = test_helper.get_inspect_data_tree();
650        assert_data_tree!(@executor test_helper.exec, data, root: contains {
651            test_stats: contains {
652                metadata: contains {
653                    connected_networks: contains {
654                        "0": {
655                            "@time": AnyNumericProperty,
656                            "data": contains {
657                                bssid: &*BSSID_REGEX,
658                                ssid: &*SSID_REGEX,
659                            }
660                        }
661                    },
662                },
663                connect_events: {
664                    "0": {
665                        "@time": AnyNumericProperty,
666                        network_id: 0u64,
667                    }
668                }
669            }
670        });
671
672        let mut time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
673        assert_eq!(
674            &time_matrix_calls.drain::<u64>("wlan_connectivity_states")[..],
675            &[TimeMatrixCall::Fold(Timed::now(1 << 0)), TimeMatrixCall::Fold(Timed::now(1 << 2)),]
676        );
677        assert_eq!(
678            &time_matrix_calls.drain::<u64>("connected_networks")[..],
679            &[TimeMatrixCall::Fold(Timed::now(1 << 0))]
680        );
681    }
682
683    #[fuchsia::test]
684    fn test_log_connect_attempt_cobalt() {
685        let mut test_helper = setup_test();
686        let logger = ConnectDisconnectLogger::new(
687            test_helper.cobalt_proxy.clone(),
688            &test_helper.inspect_node,
689            &test_helper.inspect_metadata_node,
690            &test_helper.inspect_metadata_path,
691            &test_helper.mock_time_matrix_client,
692        );
693
694        // Generate BSS Description
695        let bss_description = random_bss_description!(Wpa2,
696            channel: Channel::new(157, Cbw::Cbw40),
697            bssid: [0x00, 0xf6, 0x20, 0x03, 0x04, 0x05],
698        );
699
700        // Log the event
701        let mut test_fut = pin!(
702            logger.handle_connect_attempt(fidl_ieee80211::StatusCode::Success, &bss_description)
703        );
704        assert_eq!(
705            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
706            Poll::Ready(())
707        );
708
709        // Validate Cobalt data
710        let breakdowns_by_status_code = test_helper
711            .get_logged_metrics(metrics::CONNECT_ATTEMPT_BREAKDOWN_BY_STATUS_CODE_METRIC_ID);
712        assert_eq!(breakdowns_by_status_code.len(), 1);
713        assert_eq!(
714            breakdowns_by_status_code[0].event_codes,
715            vec![fidl_ieee80211::StatusCode::Success.into_primitive() as u32]
716        );
717        assert_eq!(breakdowns_by_status_code[0].payload, MetricEventPayload::Count(1));
718    }
719
720    #[fuchsia::test]
721    fn test_successive_connect_attempt_failures_cobalt_zero_failures() {
722        let mut test_helper = setup_test();
723        let logger = ConnectDisconnectLogger::new(
724            test_helper.cobalt_proxy.clone(),
725            &test_helper.inspect_node,
726            &test_helper.inspect_metadata_node,
727            &test_helper.inspect_metadata_path,
728            &test_helper.mock_time_matrix_client,
729        );
730
731        let bss_description = random_bss_description!(Wpa2);
732        let mut test_fut = pin!(
733            logger.handle_connect_attempt(fidl_ieee80211::StatusCode::Success, &bss_description)
734        );
735        assert_eq!(
736            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
737            Poll::Ready(())
738        );
739
740        let metrics =
741            test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
742        assert_eq!(metrics.len(), 1);
743        assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(0));
744    }
745
746    #[test_case(1; "one_failure")]
747    #[test_case(2; "two_failures")]
748    #[fuchsia::test(add_test_attr = false)]
749    fn test_successive_connect_attempt_failures_cobalt_one_failure_then_success(n_failures: usize) {
750        let mut test_helper = setup_test();
751        let logger = ConnectDisconnectLogger::new(
752            test_helper.cobalt_proxy.clone(),
753            &test_helper.inspect_node,
754            &test_helper.inspect_metadata_node,
755            &test_helper.inspect_metadata_path,
756            &test_helper.mock_time_matrix_client,
757        );
758
759        let bss_description = random_bss_description!(Wpa2);
760        for _i in 0..n_failures {
761            let mut test_fut = pin!(logger.handle_connect_attempt(
762                fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
763                &bss_description
764            ));
765            assert_eq!(
766                test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
767                Poll::Ready(())
768            );
769        }
770
771        let metrics =
772            test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
773        assert!(metrics.is_empty());
774
775        let mut test_fut = pin!(
776            logger.handle_connect_attempt(fidl_ieee80211::StatusCode::Success, &bss_description)
777        );
778        assert_eq!(
779            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
780            Poll::Ready(())
781        );
782
783        let metrics =
784            test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
785        assert_eq!(metrics.len(), 1);
786        assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(n_failures as i64));
787
788        // Verify subsequent successes would report 0 failures
789        test_helper.clear_cobalt_events();
790        let mut test_fut = pin!(
791            logger.handle_connect_attempt(fidl_ieee80211::StatusCode::Success, &bss_description)
792        );
793        assert_eq!(
794            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
795            Poll::Ready(())
796        );
797        let metrics =
798            test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
799        assert_eq!(metrics.len(), 1);
800        assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(0));
801    }
802
803    #[test_case(1; "one_failure")]
804    #[test_case(2; "two_failures")]
805    #[fuchsia::test(add_test_attr = false)]
806    fn test_successive_connect_attempt_failures_cobalt_one_failure_then_timeout(n_failures: usize) {
807        let mut test_helper = setup_test();
808        let logger = ConnectDisconnectLogger::new(
809            test_helper.cobalt_proxy.clone(),
810            &test_helper.inspect_node,
811            &test_helper.inspect_metadata_node,
812            &test_helper.inspect_metadata_path,
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.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.mock_time_matrix_client,
898        );
899
900        let bss_description = random_bss_description!(Wpa2);
901        for _i in 0..n_failures {
902            let mut test_fut = pin!(logger.handle_connect_attempt(
903                fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
904                &bss_description
905            ));
906            assert_eq!(
907                test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
908                Poll::Ready(())
909            );
910        }
911
912        let mut test_fut = pin!(logger.handle_suspend_imminent());
913        assert_eq!(
914            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
915            Poll::Ready(())
916        );
917
918        let metrics =
919            test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
920        assert_eq!(metrics.len(), 1);
921        assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(n_failures as i64));
922
923        test_helper.clear_cobalt_events();
924        let mut test_fut = pin!(logger.handle_suspend_imminent());
925        assert_eq!(
926            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
927            Poll::Ready(())
928        );
929
930        // Count of successive failures shouldn't be logged again since it was already logged
931        let metrics =
932            test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
933        assert!(metrics.is_empty());
934    }
935
936    #[fuchsia::test]
937    fn test_log_disconnect_inspect() {
938        let mut test_helper = setup_test();
939        let logger = ConnectDisconnectLogger::new(
940            test_helper.cobalt_proxy.clone(),
941            &test_helper.inspect_node,
942            &test_helper.inspect_metadata_node,
943            &test_helper.inspect_metadata_path,
944            &test_helper.mock_time_matrix_client,
945        );
946
947        // Log the event
948        let bss_description = fake_bss_description!(Open);
949        let channel = bss_description.channel;
950        let disconnect_info = DisconnectInfo {
951            iface_id: 32,
952            connected_duration: zx::BootDuration::from_seconds(30),
953            is_sme_reconnecting: false,
954            disconnect_source: fidl_sme::DisconnectSource::Ap(fidl_sme::DisconnectCause {
955                mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
956                reason_code: fidl_ieee80211::ReasonCode::UnspecifiedReason,
957            }),
958            original_bss_desc: Box::new(bss_description),
959            current_rssi_dbm: -30,
960            current_snr_db: 25,
961            current_channel: channel,
962        };
963        let mut test_fut = pin!(logger.log_disconnect(&disconnect_info));
964        assert_eq!(
965            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
966            Poll::Ready(())
967        );
968
969        // Validate Inspect data
970        let data = test_helper.get_inspect_data_tree();
971        assert_data_tree!(@executor test_helper.exec, data, root: contains {
972            test_stats: contains {
973                metadata: {
974                    connected_networks: {
975                        "0": {
976                            "@time": AnyNumericProperty,
977                            "data": {
978                                bssid: &*BSSID_REGEX,
979                                ssid: &*SSID_REGEX,
980                                ht_cap: AnyBytesProperty,
981                                vht_cap: AnyBytesProperty,
982                                protection: "Open",
983                                is_wmm_assoc: AnyBoolProperty,
984                                wmm_param: AnyBytesProperty,
985                            }
986                        }
987                    },
988                    disconnect_sources: {
989                        "0": {
990                            "@time": AnyNumericProperty,
991                            "data": {
992                                source: "ap",
993                                reason: "UnspecifiedReason",
994                                mlme_event_name: "DeauthenticateIndication",
995                            }
996                        }
997                    },
998                },
999                disconnect_events: {
1000                    "0": {
1001                        "@time": AnyNumericProperty,
1002                        connected_duration: zx::BootDuration::from_seconds(30).into_nanos(),
1003                        disconnect_source_id: 0u64,
1004                        network_id: 0u64,
1005                        rssi_dbm: -30i64,
1006                        snr_db: 25i64,
1007                        channel: AnyStringProperty,
1008                    }
1009                }
1010            }
1011        });
1012
1013        let mut time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
1014        assert_eq!(
1015            &time_matrix_calls.drain::<u64>("wlan_connectivity_states")[..],
1016            &[TimeMatrixCall::Fold(Timed::now(1 << 0)), TimeMatrixCall::Fold(Timed::now(1 << 1)),]
1017        );
1018        assert_eq!(
1019            &time_matrix_calls.drain::<u64>("disconnected_networks")[..],
1020            &[TimeMatrixCall::Fold(Timed::now(1 << 0))]
1021        );
1022        assert_eq!(
1023            &time_matrix_calls.drain::<u64>("disconnect_sources")[..],
1024            &[TimeMatrixCall::Fold(Timed::now(1 << 0))]
1025        );
1026    }
1027
1028    #[fuchsia::test]
1029    fn test_log_disconnect_cobalt() {
1030        let mut test_helper = setup_test();
1031        let logger = ConnectDisconnectLogger::new(
1032            test_helper.cobalt_proxy.clone(),
1033            &test_helper.inspect_node,
1034            &test_helper.inspect_metadata_node,
1035            &test_helper.inspect_metadata_path,
1036            &test_helper.mock_time_matrix_client,
1037        );
1038
1039        // Log the event
1040        let disconnect_info = DisconnectInfo {
1041            connected_duration: zx::BootDuration::from_millis(300_000),
1042            disconnect_source: fidl_sme::DisconnectSource::Ap(fidl_sme::DisconnectCause {
1043                mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
1044                reason_code: fidl_ieee80211::ReasonCode::ApInitiated,
1045            }),
1046            ..fake_disconnect_info()
1047        };
1048        let mut test_fut = pin!(logger.log_disconnect(&disconnect_info));
1049        assert_eq!(
1050            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1051            Poll::Ready(())
1052        );
1053
1054        let disconnect_count_metrics =
1055            test_helper.get_logged_metrics(metrics::TOTAL_DISCONNECT_COUNT_METRIC_ID);
1056        assert_eq!(disconnect_count_metrics.len(), 1);
1057        assert_eq!(disconnect_count_metrics[0].payload, MetricEventPayload::Count(1));
1058
1059        let connected_duration_metrics =
1060            test_helper.get_logged_metrics(metrics::CONNECTED_DURATION_ON_DISCONNECT_METRIC_ID);
1061        assert_eq!(connected_duration_metrics.len(), 1);
1062        assert_eq!(
1063            connected_duration_metrics[0].payload,
1064            MetricEventPayload::IntegerValue(300_000)
1065        );
1066
1067        let disconnect_by_reason_metrics =
1068            test_helper.get_logged_metrics(metrics::DISCONNECT_BREAKDOWN_BY_REASON_CODE_METRIC_ID);
1069        assert_eq!(disconnect_by_reason_metrics.len(), 1);
1070        assert_eq!(disconnect_by_reason_metrics[0].payload, MetricEventPayload::Count(1));
1071        assert_eq!(disconnect_by_reason_metrics[0].event_codes.len(), 2);
1072        assert_eq!(
1073            disconnect_by_reason_metrics[0].event_codes[0],
1074            fidl_ieee80211::ReasonCode::ApInitiated.into_primitive() as u32
1075        );
1076        assert_eq!(
1077            disconnect_by_reason_metrics[0].event_codes[1],
1078            metrics::ConnectivityWlanMetricDimensionDisconnectSource::Ap as u32
1079        );
1080    }
1081
1082    #[test_case(
1083        fidl_sme::DisconnectSource::Ap(fidl_sme::DisconnectCause {
1084            mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
1085            reason_code: fidl_ieee80211::ReasonCode::UnspecifiedReason,
1086        }),
1087        true;
1088        "ap_disconnect_source"
1089    )]
1090    #[test_case(
1091        fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
1092            mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
1093            reason_code: fidl_ieee80211::ReasonCode::UnspecifiedReason,
1094        }),
1095        true;
1096        "mlme_disconnect_source_not_link_failed"
1097    )]
1098    #[test_case(
1099        fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
1100            mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
1101            reason_code: fidl_ieee80211::ReasonCode::MlmeLinkFailed,
1102        }),
1103        false;
1104        "mlme_link_failed"
1105    )]
1106    #[test_case(
1107        fidl_sme::DisconnectSource::User(fidl_sme::UserDisconnectReason::Unknown),
1108        false;
1109        "user_disconnect_source"
1110    )]
1111    #[fuchsia::test(add_test_attr = false)]
1112    fn test_log_disconnect_for_mobile_device_cobalt(
1113        disconnect_source: fidl_sme::DisconnectSource,
1114        should_log: bool,
1115    ) {
1116        let mut test_helper = setup_test();
1117        let logger = ConnectDisconnectLogger::new(
1118            test_helper.cobalt_proxy.clone(),
1119            &test_helper.inspect_node,
1120            &test_helper.inspect_metadata_node,
1121            &test_helper.inspect_metadata_path,
1122            &test_helper.mock_time_matrix_client,
1123        );
1124
1125        // Log the event
1126        let disconnect_info = DisconnectInfo { disconnect_source, ..fake_disconnect_info() };
1127        let mut test_fut = pin!(logger.log_disconnect(&disconnect_info));
1128        assert_eq!(
1129            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1130            Poll::Ready(())
1131        );
1132
1133        let metrics = test_helper
1134            .get_logged_metrics(metrics::DISCONNECT_OCCURRENCE_FOR_MOBILE_DEVICE_METRIC_ID);
1135        if should_log {
1136            assert_eq!(metrics.len(), 1);
1137            assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
1138        } else {
1139            assert!(metrics.is_empty());
1140        }
1141    }
1142
1143    #[fuchsia::test]
1144    fn test_log_downtime_post_disconnect_on_reconnect() {
1145        let mut test_helper = setup_test();
1146        let logger = ConnectDisconnectLogger::new(
1147            test_helper.cobalt_proxy.clone(),
1148            &test_helper.inspect_node,
1149            &test_helper.inspect_metadata_node,
1150            &test_helper.inspect_metadata_path,
1151            &test_helper.mock_time_matrix_client,
1152        );
1153
1154        // Connect at 15th second
1155        test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(15_000_000_000));
1156        let bss_description = random_bss_description!(Wpa2);
1157        let mut test_fut = pin!(
1158            logger.handle_connect_attempt(fidl_ieee80211::StatusCode::Success, &bss_description)
1159        );
1160        assert_eq!(
1161            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1162            Poll::Ready(())
1163        );
1164
1165        // Verify no downtime metric is logged on first successful connect
1166        let metrics = test_helper.get_logged_metrics(metrics::DOWNTIME_POST_DISCONNECT_METRIC_ID);
1167        assert!(metrics.is_empty());
1168
1169        // Disconnect at 25th second
1170        test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(25_000_000_000));
1171        let disconnect_info = fake_disconnect_info();
1172        let mut test_fut = pin!(logger.log_disconnect(&disconnect_info));
1173        assert_eq!(
1174            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1175            Poll::Ready(())
1176        );
1177
1178        // Reconnect at 60th second
1179        test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(60_000_000_000));
1180        let mut test_fut = pin!(
1181            logger.handle_connect_attempt(fidl_ieee80211::StatusCode::Success, &bss_description)
1182        );
1183        assert_eq!(
1184            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1185            Poll::Ready(())
1186        );
1187
1188        // Verify that downtime metric is logged
1189        let metrics = test_helper.get_logged_metrics(metrics::DOWNTIME_POST_DISCONNECT_METRIC_ID);
1190        assert_eq!(metrics.len(), 1);
1191        assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(35_000));
1192    }
1193
1194    #[fuchsia::test]
1195    fn test_log_iface_destroyed() {
1196        let mut test_helper = setup_test();
1197        let logger = ConnectDisconnectLogger::new(
1198            test_helper.cobalt_proxy.clone(),
1199            &test_helper.inspect_node,
1200            &test_helper.inspect_metadata_node,
1201            &test_helper.inspect_metadata_path,
1202            &test_helper.mock_time_matrix_client,
1203        );
1204
1205        // Log connect event to move state to connected
1206        let bss_description = random_bss_description!();
1207        let mut test_fut = pin!(
1208            logger.handle_connect_attempt(fidl_ieee80211::StatusCode::Success, &bss_description)
1209        );
1210        assert_eq!(
1211            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1212            Poll::Ready(())
1213        );
1214
1215        // Log iface destroyed event to move state to idle
1216        let mut test_fut = pin!(logger.handle_iface_destroyed());
1217        assert_eq!(
1218            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1219            Poll::Ready(())
1220        );
1221
1222        let mut time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
1223        assert_eq!(
1224            &time_matrix_calls.drain::<u64>("wlan_connectivity_states")[..],
1225            &[
1226                TimeMatrixCall::Fold(Timed::now(1 << 0)),
1227                TimeMatrixCall::Fold(Timed::now(1 << 2)),
1228                TimeMatrixCall::Fold(Timed::now(1 << 0))
1229            ]
1230        );
1231    }
1232
1233    #[fuchsia::test]
1234    fn test_log_disable_client_connections() {
1235        let mut test_helper = setup_test();
1236        let logger = ConnectDisconnectLogger::new(
1237            test_helper.cobalt_proxy.clone(),
1238            &test_helper.inspect_node,
1239            &test_helper.inspect_metadata_node,
1240            &test_helper.inspect_metadata_path,
1241            &test_helper.mock_time_matrix_client,
1242        );
1243
1244        // Log connect event to move state to connected
1245        let bss_description = random_bss_description!();
1246        let mut test_fut = pin!(
1247            logger.handle_connect_attempt(fidl_ieee80211::StatusCode::Success, &bss_description)
1248        );
1249        assert_eq!(
1250            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1251            Poll::Ready(())
1252        );
1253
1254        // Disable client connections to move state to idle
1255        let mut test_fut =
1256            pin!(logger.handle_client_connections_toggle(&ClientConnectionsToggleEvent::Disabled));
1257        assert_eq!(
1258            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1259            Poll::Ready(())
1260        );
1261
1262        let mut time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
1263        assert_eq!(
1264            &time_matrix_calls.drain::<u64>("wlan_connectivity_states")[..],
1265            &[
1266                TimeMatrixCall::Fold(Timed::now(1 << 0)),
1267                TimeMatrixCall::Fold(Timed::now(1 << 2)),
1268                TimeMatrixCall::Fold(Timed::now(1 << 0))
1269            ]
1270        );
1271    }
1272
1273    fn fake_disconnect_info() -> DisconnectInfo {
1274        let bss_description = random_bss_description!(Wpa2);
1275        let channel = bss_description.channel;
1276        DisconnectInfo {
1277            iface_id: 1,
1278            connected_duration: zx::BootDuration::from_hours(6),
1279            is_sme_reconnecting: false,
1280            disconnect_source: fidl_sme::DisconnectSource::User(
1281                fidl_sme::UserDisconnectReason::Unknown,
1282            ),
1283            original_bss_desc: bss_description.into(),
1284            current_rssi_dbm: -30,
1285            current_snr_db: 25,
1286            current_channel: channel,
1287        }
1288    }
1289}