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