1use crate::config::DeviceMobility;
6use crate::convert::{
7 convert_channel_band, convert_is_owe_transition, convert_rssi_bucket, convert_security_type,
8 convert_snr_bucket,
9};
10use crate::processors::toggle_events::ClientConnectionsToggleEvent;
11use crate::util::cobalt_logger::{FilteredCobaltLogger, log_cobalt_batch};
12use fidl_fuchsia_metrics::{MetricEvent, MetricEventPayload};
13use fidl_fuchsia_wlan_ieee80211 as fidl_ieee80211;
14use fidl_fuchsia_wlan_sme as fidl_sme;
15use fuchsia_async as fasync;
16use fuchsia_inspect::Node as InspectNode;
17use fuchsia_inspect_contrib::id_enum::IdEnum;
18use fuchsia_inspect_contrib::inspect_log;
19use fuchsia_inspect_contrib::nodes::{BoundedListNode, LruCacheNode};
20use fuchsia_inspect_derive::Unit;
21use fuchsia_sync::Mutex;
22use ieee80211::OuiFmt;
23use std::collections::HashMap;
24use std::sync::Arc;
25use std::sync::atomic::{AtomicUsize, Ordering};
26use strum_macros::{Display, EnumCount};
27use windowed_stats::experimental::inspect::{InspectSender, InspectedTimeMatrix};
28use windowed_stats::experimental::series::interpolation::{ConstantSample, LastSample};
29use windowed_stats::experimental::series::metadata::{BitsetMap, BitsetNode};
30use windowed_stats::experimental::series::statistic::Union;
31use windowed_stats::experimental::series::{SamplingProfile, TimeMatrix};
32use wlan_common::bss::BssDescription;
33use wlan_common::channel::Channel;
34use wlan_legacy_metrics_registry as metrics;
35use zx;
36
37const INSPECT_CONNECT_EVENTS_LIMIT: usize = 10;
38const INSPECT_DISCONNECT_EVENTS_LIMIT: usize = 20;
39const INSPECT_CONNECT_ATTEMPT_RESULTS_LIMIT: usize = 50;
40const INSPECT_CONNECTED_NETWORKS_ID_LIMIT: usize = 16;
41const INSPECT_DISCONNECT_SOURCES_ID_LIMIT: usize = 32;
42const INSPECT_CONNECT_ATTEMPT_RESULTS_ID_LIMIT: usize = 32;
43const SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_TIMEOUT: zx::BootDuration =
44 zx::BootDuration::from_minutes(2);
45const DAILY_METRICS_LOG_INTERVAL: zx::BootDuration = zx::BootDuration::from_hours(24);
46
47#[derive(Clone, Debug, Display, EnumCount)]
48enum ConnectionState {
49 Idle(IdleState),
50 Connected(ConnectedState),
51 Disconnected(DisconnectedState),
52 ConnectFailed(ConnectFailedState),
53 FailedToStart(FailedToStartState),
54 FailedToStop(FailedToStopState),
55 PnoScanFailedIdle(PnoScanFailedIdleState),
56}
57
58impl IdEnum for ConnectionState {
60 type Id = u8;
61 fn to_id(&self) -> Self::Id {
62 match self {
63 Self::Idle(_) => 0,
64 Self::Disconnected(_) => 1,
65 Self::ConnectFailed(_) => 2,
66 Self::Connected(_) => 3,
67 Self::FailedToStart(_) => 4,
68 Self::FailedToStop(_) => 5,
69 Self::PnoScanFailedIdle(_) => 6,
70 }
71 }
72}
73
74#[derive(Clone, Debug)]
75struct IdleState {}
76
77#[derive(Clone, Debug, PartialEq)]
78struct ConnectedState {
79 bss: Box<BssDescription>,
80 is_owe_transition: bool,
81}
82
83#[derive(Clone, Debug)]
84struct DisconnectedState {}
85
86#[derive(Clone, Debug)]
87struct ConnectFailedState {}
88
89#[derive(Clone, Debug)]
90struct FailedToStartState {}
91
92#[derive(Clone, Debug)]
93struct FailedToStopState {}
94
95#[derive(Clone, Debug)]
96struct PnoScanFailedIdleState {}
97
98#[derive(PartialEq, Eq, Unit, Hash)]
99struct InspectConnectedNetwork {
100 bssid: String,
101 ssid: String,
102 protection: String,
103 ht_cap: Option<Vec<u8>>,
104 vht_cap: Option<Vec<u8>>,
105 is_wmm_assoc: bool,
106 wmm_param: Option<Vec<u8>>,
107}
108
109impl From<&BssDescription> for InspectConnectedNetwork {
110 fn from(bss_description: &BssDescription) -> Self {
111 Self {
112 bssid: bss_description.bssid.to_string(),
113 ssid: bss_description.ssid.to_string(),
114 protection: format!("{:?}", bss_description.protection()),
115 ht_cap: bss_description.raw_ht_cap().map(|cap| cap.bytes.into()),
116 vht_cap: bss_description.raw_vht_cap().map(|cap| cap.bytes.into()),
117 is_wmm_assoc: bss_description.find_wmm_param().is_some(),
118 wmm_param: bss_description.find_wmm_param().map(|bytes| bytes.into()),
119 }
120 }
121}
122
123#[derive(PartialEq, Eq, Unit, Hash)]
124struct InspectConnectAttemptResult {
125 status_code: u16,
126 result: String,
127}
128
129#[derive(PartialEq, Eq, Unit, Hash)]
130struct InspectDisconnectSource {
131 source: String,
132 reason: String,
133 mlme_event_name: Option<String>,
134}
135
136impl From<&fidl_sme::DisconnectSource> for InspectDisconnectSource {
137 fn from(disconnect_source: &fidl_sme::DisconnectSource) -> Self {
138 match disconnect_source {
139 fidl_sme::DisconnectSource::User(reason) => Self {
140 source: "user".to_string(),
141 reason: format!("{reason:?}"),
142 mlme_event_name: None,
143 },
144 fidl_sme::DisconnectSource::Ap(cause) => Self {
145 source: "ap".to_string(),
146 reason: format!("{:?}", cause.reason_code),
147 mlme_event_name: Some(format!("{:?}", cause.mlme_event_name)),
148 },
149 fidl_sme::DisconnectSource::Mlme(cause) => Self {
150 source: "mlme".to_string(),
151 reason: format!("{:?}", cause.reason_code),
152 mlme_event_name: Some(format!("{:?}", cause.mlme_event_name)),
153 },
154 }
155 }
156}
157
158#[derive(Clone, Debug, PartialEq)]
159pub struct DisconnectInfo {
160 pub iface_id: u16,
161 pub connected_duration: zx::BootDuration,
162 pub is_sme_reconnecting: bool,
163 pub disconnect_source: fidl_sme::DisconnectSource,
164 pub original_bss_desc: Box<BssDescription>,
165 pub current_rssi_dbm: i8,
166 pub current_snr_db: i8,
167 pub current_channel: Channel,
168}
169
170pub struct ConnectDisconnectLogger {
171 connection_state: Arc<Mutex<ConnectionState>>,
172 cobalt_proxy: Arc<FilteredCobaltLogger>,
173 connect_events_node: Mutex<BoundedListNode>,
174 disconnect_events_node: Mutex<BoundedListNode>,
175 connect_attempt_results_node: Mutex<BoundedListNode>,
176 inspect_metadata_node: Mutex<InspectMetadataNode>,
177 time_series_stats: ConnectDisconnectTimeSeries,
178 successive_connect_attempt_failures: AtomicUsize,
179 last_connect_failure_at: Arc<Mutex<Option<fasync::BootInstant>>>,
180 last_disconnect_at: Arc<Mutex<Option<fasync::MonotonicInstant>>>,
181 daily_connect_stats: Mutex<DailyConnectStats>,
182 device_mobility: DeviceMobility,
183}
184
185impl ConnectDisconnectLogger {
186 pub fn new<S: InspectSender>(
187 cobalt_proxy: Arc<FilteredCobaltLogger>,
188 inspect_node: &InspectNode,
189 inspect_metadata_node: &InspectNode,
190 inspect_metadata_path: &str,
191 time_matrix_client: &S,
192 device_mobility: DeviceMobility,
193 ) -> Self {
194 let connect_events = inspect_node.create_child("connect_events");
195 let disconnect_events = inspect_node.create_child("disconnect_events");
196 let connect_attempt_results = inspect_node.create_child("connect_attempt_results");
197 let this = Self {
198 cobalt_proxy,
199 connection_state: Arc::new(Mutex::new(ConnectionState::Idle(IdleState {}))),
200 connect_events_node: Mutex::new(BoundedListNode::new(
201 connect_events,
202 INSPECT_CONNECT_EVENTS_LIMIT,
203 )),
204 disconnect_events_node: Mutex::new(BoundedListNode::new(
205 disconnect_events,
206 INSPECT_DISCONNECT_EVENTS_LIMIT,
207 )),
208 connect_attempt_results_node: Mutex::new(BoundedListNode::new(
209 connect_attempt_results,
210 INSPECT_CONNECT_ATTEMPT_RESULTS_LIMIT,
211 )),
212 inspect_metadata_node: Mutex::new(InspectMetadataNode::new(inspect_metadata_node)),
213 time_series_stats: ConnectDisconnectTimeSeries::new(
214 time_matrix_client,
215 inspect_metadata_path,
216 ),
217 successive_connect_attempt_failures: AtomicUsize::new(0),
218 last_connect_failure_at: Arc::new(Mutex::new(None)),
219 last_disconnect_at: Arc::new(Mutex::new(None)),
220 daily_connect_stats: Mutex::new(DailyConnectStats::new(fasync::BootInstant::now())),
221 device_mobility,
222 };
223 this.log_connection_state();
224 this
225 }
226
227 fn update_connection_state(&self, state: ConnectionState) {
228 *self.connection_state.lock() = state;
229 self.log_connection_state();
230 }
231
232 fn log_connection_state(&self) {
233 let wlan_connectivity_state_id = self.connection_state.lock().to_id() as u64;
234 self.time_series_stats.log_wlan_connectivity_state(1 << wlan_connectivity_state_id);
235 }
236
237 pub fn is_connected(&self) -> bool {
238 matches!(*self.connection_state.lock(), ConnectionState::Connected(_))
239 }
240
241 pub async fn handle_connect_attempt(
242 &self,
243 result: fidl_ieee80211::StatusCode,
244 bss: &BssDescription,
245 is_credential_rejected: bool,
246 is_owe_transition: bool,
247 ) {
248 let mut flushed_successive_failures = None;
249 let mut downtime_duration = None;
250 if result == fidl_ieee80211::StatusCode::Success {
251 self.update_connection_state(ConnectionState::Connected(ConnectedState {
252 bss: Box::new(bss.clone()),
253 is_owe_transition,
254 }));
255 flushed_successive_failures =
256 Some(self.successive_connect_attempt_failures.swap(0, Ordering::SeqCst));
257 downtime_duration =
258 self.last_disconnect_at.lock().map(|t| fasync::MonotonicInstant::now() - t);
259 } else if is_credential_rejected {
260 self.update_connection_state(ConnectionState::Idle(IdleState {}));
261 let _prev = self.successive_connect_attempt_failures.fetch_add(1, Ordering::SeqCst);
262 let _prev = self.last_connect_failure_at.lock().replace(fasync::BootInstant::now());
263 } else {
264 self.update_connection_state(ConnectionState::ConnectFailed(ConnectFailedState {}));
265 let _prev = self.successive_connect_attempt_failures.fetch_add(1, Ordering::SeqCst);
266 let _prev = self.last_connect_failure_at.lock().replace(fasync::BootInstant::now());
267 }
268
269 self.log_connect_attempt_inspect(result, bss);
270 self.log_connect_attempt_cobalt(result, flushed_successive_failures, downtime_duration)
271 .await;
272 if result == fidl_ieee80211::StatusCode::Success {
273 self.log_device_connected_cobalt_metrics(bss, is_owe_transition).await;
274 }
275
276 let security_type = convert_security_type(&bss.protection());
277 let primary_channel = bss.channel.primary;
278 let channel_band = convert_channel_band(bss.channel.band);
279 let rssi_bucket = convert_rssi_bucket(bss.rssi_dbm);
280 let snr_bucket = convert_snr_bucket(bss.snr_db);
281 let is_owe_transition_dim = convert_is_owe_transition(is_owe_transition);
282
283 let mut daily_stats = self.daily_connect_stats.lock();
284 daily_stats.connect_per_security_type.entry(security_type).or_default().increment(result);
285 daily_stats
286 .connect_per_primary_channel
287 .entry(primary_channel)
288 .or_default()
289 .increment(result);
290 daily_stats.connect_per_channel_band.entry(channel_band).or_default().increment(result);
291 daily_stats.connect_per_rssi_bucket.entry(rssi_bucket).or_default().increment(result);
292 daily_stats.connect_per_snr_bucket.entry(snr_bucket).or_default().increment(result);
293 daily_stats
294 .connect_per_is_owe_transition
295 .entry(is_owe_transition_dim)
296 .or_default()
297 .increment(result);
298 }
299
300 fn log_connect_attempt_inspect(
301 &self,
302 result: fidl_ieee80211::StatusCode,
303 bss: &BssDescription,
304 ) {
305 let mut inspect_metadata_node = self.inspect_metadata_node.lock();
306 let connect_result_id =
307 inspect_metadata_node.connect_attempt_results.insert(InspectConnectAttemptResult {
308 status_code: result.into_primitive(),
309 result: format!("{:?}", result),
310 }) as u64;
311 self.time_series_stats.log_connect_attempt_results(1 << connect_result_id);
312
313 inspect_log!(self.connect_attempt_results_node.lock(), {
314 result: format!("{:?}", result),
315 ssid: bss.ssid.to_string(),
316 bssid: bss.bssid.to_string(),
317 protection: format!("{:?}", bss.protection()),
318 });
319
320 if result == fidl_ieee80211::StatusCode::Success {
321 let connected_network = InspectConnectedNetwork::from(bss);
322 let connected_network_id =
323 inspect_metadata_node.connected_networks.insert(connected_network) as u64;
324
325 self.time_series_stats.log_connected_networks(1 << connected_network_id);
326
327 inspect_log!(self.connect_events_node.lock(), {
328 network_id: connected_network_id,
329 });
330 }
331 }
332
333 #[allow(clippy::vec_init_then_push, reason = "mass allow for https://fxbug.dev/381896734")]
334 async fn log_connect_attempt_cobalt(
335 &self,
336 result: fidl_ieee80211::StatusCode,
337 flushed_successive_failures: Option<usize>,
338 downtime_duration: Option<zx::MonotonicDuration>,
339 ) {
340 let mut metric_events = vec![];
341 metric_events.push(MetricEvent {
342 metric_id: metrics::CONNECT_ATTEMPT_BREAKDOWN_BY_STATUS_CODE_METRIC_ID,
343 event_codes: vec![result.into_primitive() as u32],
344 payload: MetricEventPayload::Count(1),
345 });
346
347 if let Some(failures) = flushed_successive_failures {
348 metric_events.push(MetricEvent {
349 metric_id: metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID,
350 event_codes: vec![],
351 payload: MetricEventPayload::IntegerValue(failures as i64),
352 });
353 }
354
355 if let Some(duration) = downtime_duration {
356 metric_events.push(MetricEvent {
357 metric_id: metrics::DOWNTIME_POST_DISCONNECT_METRIC_ID,
358 event_codes: vec![],
359 payload: MetricEventPayload::IntegerValue(duration.into_millis()),
360 });
361 }
362
363 log_cobalt_batch!(self.cobalt_proxy, &metric_events, "log_connect_attempt_cobalt");
364 }
365
366 async fn log_device_connected_cobalt_metrics(
367 &self,
368 bss: &BssDescription,
369 is_owe_transition: bool,
370 ) {
371 let mut metric_events = vec![];
372 append_device_connected_cobalt_metrics(&mut metric_events, bss, is_owe_transition);
373 log_cobalt_batch!(self.cobalt_proxy, &metric_events, "log_device_connected_cobalt_metrics");
374 }
375
376 pub async fn handle_channel_switched(&self, channel: Channel) {
377 if let ConnectionState::Connected(ref mut state) = *self.connection_state.lock() {
378 state.bss.channel = channel;
379 }
380 let mut metric_events = vec![];
381 append_device_connected_channel_cobalt_metrics(&mut metric_events, channel);
382 log_cobalt_batch!(self.cobalt_proxy, &metric_events, "handle_channel_switched");
383 }
384
385 pub async fn log_disconnect(&self, info: &DisconnectInfo) {
386 match self.device_mobility {
387 DeviceMobility::Mobile => {
388 if !info.disconnect_source.should_log_for_mobile_device() {
391 self.update_connection_state(ConnectionState::Idle(IdleState {}));
392 } else {
393 self.update_connection_state(ConnectionState::Disconnected(
394 DisconnectedState {},
395 ));
396 }
397 }
398 DeviceMobility::Stationary => {
399 self.update_connection_state(ConnectionState::Disconnected(DisconnectedState {}));
400 }
401 }
402 let _prev = self.last_disconnect_at.lock().replace(fasync::MonotonicInstant::now());
403 self.log_disconnect_inspect(info);
404 self.log_disconnect_cobalt(info).await;
405 }
406
407 fn log_disconnect_inspect(&self, info: &DisconnectInfo) {
408 let mut inspect_metadata_node = self.inspect_metadata_node.lock();
409 let connected_network = InspectConnectedNetwork::from(&*info.original_bss_desc);
410 let connected_network_id =
411 inspect_metadata_node.connected_networks.insert(connected_network) as u64;
412 let disconnect_source = InspectDisconnectSource::from(&info.disconnect_source);
413 let disconnect_source_id =
414 inspect_metadata_node.disconnect_sources.insert(disconnect_source) as u64;
415 inspect_log!(self.disconnect_events_node.lock(), {
416 connected_duration: info.connected_duration.into_nanos(),
417 disconnect_source_id: disconnect_source_id,
418 network_id: connected_network_id,
419 rssi_dbm: info.current_rssi_dbm,
420 snr_db: info.current_snr_db,
421 channel: format!("{}", info.current_channel),
422 });
423
424 self.time_series_stats.log_disconnected_networks(1 << connected_network_id);
425 self.time_series_stats.log_disconnect_sources(1 << disconnect_source_id);
426 }
427
428 async fn log_disconnect_cobalt(&self, info: &DisconnectInfo) {
429 let mut metric_events = vec![];
430 metric_events.push(MetricEvent {
431 metric_id: metrics::TOTAL_DISCONNECT_COUNT_METRIC_ID,
432 event_codes: vec![],
433 payload: MetricEventPayload::Count(1),
434 });
435
436 if self.device_mobility == DeviceMobility::Mobile
437 && info.disconnect_source.should_log_for_mobile_device()
438 {
439 metric_events.push(MetricEvent {
440 metric_id: metrics::DISCONNECT_OCCURRENCE_FOR_MOBILE_DEVICE_METRIC_ID,
441 event_codes: vec![],
442 payload: MetricEventPayload::Count(1),
443 });
444 }
445
446 metric_events.push(MetricEvent {
447 metric_id: metrics::CONNECTED_DURATION_ON_DISCONNECT_METRIC_ID,
448 event_codes: vec![],
449 payload: MetricEventPayload::IntegerValue(info.connected_duration.into_millis()),
450 });
451
452 metric_events.push(MetricEvent {
453 metric_id: metrics::DISCONNECT_BREAKDOWN_BY_REASON_CODE_METRIC_ID,
454 event_codes: vec![
455 u32::from(info.disconnect_source.cobalt_reason_code()),
456 info.disconnect_source.as_cobalt_disconnect_source() as u32,
457 ],
458 payload: MetricEventPayload::Count(1),
459 });
460
461 log_cobalt_batch!(self.cobalt_proxy, &metric_events, "log_disconnect_cobalt");
462 }
463
464 pub async fn handle_periodic_telemetry(&self) {
465 let mut metric_events = vec![];
466 let now = fasync::BootInstant::now();
467 if let Some(failed_at) = *self.last_connect_failure_at.lock()
468 && now - failed_at >= SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_TIMEOUT
469 {
470 let failures = self.successive_connect_attempt_failures.swap(0, Ordering::SeqCst);
471 if failures > 0 {
472 metric_events.push(MetricEvent {
473 metric_id: metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID,
474 event_codes: vec![],
475 payload: MetricEventPayload::IntegerValue(failures as i64),
476 });
477 }
478 }
479
480 {
481 let mut daily_stats = self.daily_connect_stats.lock();
482 if now - daily_stats.last_log_time >= DAILY_METRICS_LOG_INTERVAL {
483 if let ConnectionState::Connected(ref state) = *self.connection_state.lock() {
484 append_device_connected_cobalt_metrics(
485 &mut metric_events,
486 &state.bss,
487 state.is_owe_transition,
488 );
489 }
490
491 for (security_type, counter) in daily_stats.connect_per_security_type.drain() {
492 if counter.total > 0 {
493 let success_rate = counter.success as f64 / counter.total as f64;
494 metric_events.push(MetricEvent {
495 metric_id:
496 metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_SECURITY_TYPE_METRIC_ID,
497 event_codes: vec![security_type as u32],
498 payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(
499 success_rate,
500 )),
501 });
502 }
503 }
504 for (primary_channel, counter) in daily_stats.connect_per_primary_channel.drain() {
505 if counter.total > 0 {
506 let success_rate = counter.success as f64 / counter.total as f64;
507 metric_events.push(MetricEvent {
508 metric_id:
509 metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID,
510 event_codes: vec![primary_channel as u32],
511 payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(
512 success_rate,
513 )),
514 });
515 }
516 }
517 for (channel_band, counter) in daily_stats.connect_per_channel_band.drain() {
518 if counter.total > 0 {
519 let success_rate = counter.success as f64 / counter.total as f64;
520 metric_events.push(MetricEvent {
521 metric_id:
522 metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_CHANNEL_BAND_METRIC_ID,
523 event_codes: vec![channel_band as u32],
524 payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(
525 success_rate,
526 )),
527 });
528 }
529 }
530 for (rssi_bucket, counter) in daily_stats.connect_per_rssi_bucket.drain() {
531 if counter.total > 0 {
532 let success_rate = counter.success as f64 / counter.total as f64;
533 metric_events.push(MetricEvent {
534 metric_id:
535 metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_RSSI_BUCKET_METRIC_ID,
536 event_codes: vec![rssi_bucket as u32],
537 payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(
538 success_rate,
539 )),
540 });
541 }
542 }
543 for (snr_bucket, counter) in daily_stats.connect_per_snr_bucket.drain() {
544 if counter.total > 0 {
545 let success_rate = counter.success as f64 / counter.total as f64;
546 metric_events.push(MetricEvent {
547 metric_id:
548 metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_SNR_BUCKET_METRIC_ID,
549 event_codes: vec![snr_bucket as u32],
550 payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(
551 success_rate,
552 )),
553 });
554 }
555 }
556 for (is_owe_transition, counter) in
557 daily_stats.connect_per_is_owe_transition.drain()
558 {
559 if counter.total > 0 {
560 let success_rate = counter.success as f64 / counter.total as f64;
561 metric_events.push(MetricEvent {
562 metric_id:
563 metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_IS_OWE_TRANSITION_METRIC_ID,
564 event_codes: vec![is_owe_transition as u32],
565 payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(
566 success_rate,
567 )),
568 });
569 }
570 }
571 daily_stats.last_log_time = now;
572 }
573 }
574
575 log_cobalt_batch!(self.cobalt_proxy, &metric_events, "handle_periodic_telemetry");
576 }
577
578 pub async fn handle_suspend_imminent(&self) {
579 let mut metric_events = vec![];
580
581 let flushed_successive_failures =
582 self.successive_connect_attempt_failures.swap(0, Ordering::SeqCst);
583 if flushed_successive_failures > 0 {
584 metric_events.push(MetricEvent {
585 metric_id: metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID,
586 event_codes: vec![],
587 payload: MetricEventPayload::IntegerValue(flushed_successive_failures as i64),
588 });
589 }
590
591 log_cobalt_batch!(self.cobalt_proxy, &metric_events, "handle_suspend_imminent");
592 }
593
594 pub async fn handle_iface_destroyed(&self) {
595 self.update_connection_state(ConnectionState::Idle(IdleState {}));
596 }
597
598 pub async fn handle_client_connections_toggle(&self, event: &ClientConnectionsToggleEvent) {
599 if event == &ClientConnectionsToggleEvent::Disabled {
600 self.update_connection_state(ConnectionState::Idle(IdleState {}));
601 }
602 }
603
604 pub async fn handle_pno_scan_failure(&self) {
605 let mut metric_events = vec![MetricEvent {
606 metric_id: metrics::PNO_SCAN_FAILURE_OCCURRENCE_METRIC_ID,
607 event_codes: vec![],
608 payload: MetricEventPayload::Count(1),
609 }];
610
611 let state = self.connection_state.lock().clone();
612 match state {
613 ConnectionState::Idle(_)
614 | ConnectionState::Disconnected(_)
615 | ConnectionState::ConnectFailed(_)
616 | ConnectionState::PnoScanFailedIdle(_) => {
617 metric_events.push(MetricEvent {
618 metric_id: metrics::PNO_SCAN_FAILURE_WHILE_NOT_CONNECTED_OCCURRENCE_METRIC_ID,
619 event_codes: vec![],
620 payload: MetricEventPayload::Count(1),
621 });
622
623 self.update_connection_state(ConnectionState::PnoScanFailedIdle(
627 PnoScanFailedIdleState {},
628 ));
629 }
630 ConnectionState::Connected(_)
631 | ConnectionState::FailedToStart(_)
632 | ConnectionState::FailedToStop(_) => {
633 }
637 }
638
639 log_cobalt_batch!(self.cobalt_proxy, &metric_events, "handle_pno_scan_failure");
640 }
641 pub async fn handle_client_connections_failed_to_start(&self) {
642 self.update_connection_state(ConnectionState::FailedToStart(FailedToStartState {}));
643 }
644
645 pub async fn handle_client_connections_failed_to_stop(&self) {
646 self.update_connection_state(ConnectionState::FailedToStop(FailedToStopState {}));
647 }
648}
649
650struct InspectMetadataNode {
651 connected_networks: LruCacheNode<InspectConnectedNetwork>,
652 disconnect_sources: LruCacheNode<InspectDisconnectSource>,
653 connect_attempt_results: LruCacheNode<InspectConnectAttemptResult>,
654}
655
656impl InspectMetadataNode {
657 const CONNECTED_NETWORKS: &'static str = "connected_networks";
658 const DISCONNECT_SOURCES: &'static str = "disconnect_sources";
659 const CONNECT_ATTEMPT_RESULTS: &'static str = "connect_attempt_results";
660
661 fn new(inspect_node: &InspectNode) -> Self {
662 let connected_networks = inspect_node.create_child(Self::CONNECTED_NETWORKS);
663 let disconnect_sources = inspect_node.create_child(Self::DISCONNECT_SOURCES);
664 let connect_attempt_results = inspect_node.create_child(Self::CONNECT_ATTEMPT_RESULTS);
665 Self {
666 connected_networks: LruCacheNode::new(
667 connected_networks,
668 INSPECT_CONNECTED_NETWORKS_ID_LIMIT,
669 ),
670 disconnect_sources: LruCacheNode::new(
671 disconnect_sources,
672 INSPECT_DISCONNECT_SOURCES_ID_LIMIT,
673 ),
674 connect_attempt_results: LruCacheNode::new(
675 connect_attempt_results,
676 INSPECT_CONNECT_ATTEMPT_RESULTS_ID_LIMIT,
677 ),
678 }
679 }
680}
681
682#[derive(Debug, Clone)]
683struct ConnectDisconnectTimeSeries {
684 wlan_connectivity_states: InspectedTimeMatrix<u64>,
685 connected_networks: InspectedTimeMatrix<u64>,
686 disconnected_networks: InspectedTimeMatrix<u64>,
687 disconnect_sources: InspectedTimeMatrix<u64>,
688 connect_attempt_results: InspectedTimeMatrix<u64>,
689}
690
691impl ConnectDisconnectTimeSeries {
692 pub fn new<S: InspectSender>(client: &S, inspect_metadata_path: &str) -> Self {
693 let wlan_connectivity_states = client.inspect_time_matrix_with_metadata(
694 "wlan_connectivity_states",
695 TimeMatrix::<Union<u64>, LastSample>::new(
696 SamplingProfile::highly_granular(),
697 LastSample::or(0),
698 ),
699 BitsetMap::from_ordered(Self::wlan_connectivity_states_bitset_map().iter().copied()),
701 );
702 let connected_networks = client.inspect_time_matrix_with_metadata(
703 "connected_networks",
704 TimeMatrix::<Union<u64>, ConstantSample>::new(
705 SamplingProfile::granular(),
706 ConstantSample::default(),
707 ),
708 BitsetNode::from_path(format!(
709 "{}/{}",
710 inspect_metadata_path,
711 InspectMetadataNode::CONNECTED_NETWORKS
712 )),
713 );
714 let disconnected_networks = client.inspect_time_matrix_with_metadata(
715 "disconnected_networks",
716 TimeMatrix::<Union<u64>, ConstantSample>::new(
717 SamplingProfile::granular(),
718 ConstantSample::default(),
719 ),
720 BitsetNode::from_path(format!(
722 "{}/{}",
723 inspect_metadata_path,
724 InspectMetadataNode::CONNECTED_NETWORKS
725 )),
726 );
727 let disconnect_sources = client.inspect_time_matrix_with_metadata(
728 "disconnect_sources",
729 TimeMatrix::<Union<u64>, ConstantSample>::new(
730 SamplingProfile::granular(),
731 ConstantSample::default(),
732 ),
733 BitsetNode::from_path(format!(
734 "{}/{}",
735 inspect_metadata_path,
736 InspectMetadataNode::DISCONNECT_SOURCES,
737 )),
738 );
739 let connect_attempt_results = client.inspect_time_matrix_with_metadata(
740 "connect_attempt_results",
741 TimeMatrix::<Union<u64>, ConstantSample>::new(
742 SamplingProfile::granular(),
743 ConstantSample::default(),
744 ),
745 BitsetNode::from_path(format!(
746 "{}/{}",
747 inspect_metadata_path,
748 InspectMetadataNode::CONNECT_ATTEMPT_RESULTS,
749 )),
750 );
751 Self {
752 wlan_connectivity_states,
753 connected_networks,
754 disconnected_networks,
755 disconnect_sources,
756 connect_attempt_results,
757 }
758 }
759
760 fn wlan_connectivity_states_bitset_map() -> &'static [&'static str] {
763 &[
764 "idle",
765 "disconnected",
766 "connect_failed",
767 "connected",
768 "start_failure",
769 "stop_failure",
770 "pno_scan_failed",
771 ]
772 }
773
774 fn log_wlan_connectivity_state(&self, data: u64) {
775 self.wlan_connectivity_states.fold_or_log_error(data);
776 }
777 fn log_connected_networks(&self, data: u64) {
778 self.connected_networks.fold_or_log_error(data);
779 }
780 fn log_disconnected_networks(&self, data: u64) {
781 self.disconnected_networks.fold_or_log_error(data);
782 }
783 fn log_disconnect_sources(&self, data: u64) {
784 self.disconnect_sources.fold_or_log_error(data);
785 }
786 fn log_connect_attempt_results(&self, data: u64) {
787 self.connect_attempt_results.fold_or_log_error(data);
788 }
789}
790
791pub trait DisconnectSourceExt {
792 fn should_log_for_mobile_device(&self) -> bool;
793 fn cobalt_reason_code(&self) -> u16;
794 fn as_cobalt_disconnect_source(
795 &self,
796 ) -> metrics::ConnectivityWlanMetricDimensionDisconnectSource;
797}
798
799impl DisconnectSourceExt for fidl_sme::DisconnectSource {
800 fn should_log_for_mobile_device(&self) -> bool {
801 match self {
802 fidl_sme::DisconnectSource::Ap(_) => true,
803 fidl_sme::DisconnectSource::Mlme(cause)
804 if cause.reason_code != fidl_ieee80211::ReasonCode::MlmeLinkFailed =>
805 {
806 true
807 }
808 _ => false,
809 }
810 }
811
812 fn cobalt_reason_code(&self) -> u16 {
813 let cobalt_disconnect_reason_code = match self {
814 fidl_sme::DisconnectSource::Ap(cause) | fidl_sme::DisconnectSource::Mlme(cause) => {
815 cause.reason_code.into_primitive()
816 }
817 fidl_sme::DisconnectSource::User(reason) => *reason as u16,
818 };
819 const REASON_CODE_MAX: u16 = 1000;
822 std::cmp::min(cobalt_disconnect_reason_code, REASON_CODE_MAX)
823 }
824
825 fn as_cobalt_disconnect_source(
826 &self,
827 ) -> metrics::ConnectivityWlanMetricDimensionDisconnectSource {
828 use metrics::ConnectivityWlanMetricDimensionDisconnectSource as DS;
829 match self {
830 fidl_sme::DisconnectSource::Ap(..) => DS::Ap,
831 fidl_sme::DisconnectSource::User(..) => DS::User,
832 fidl_sme::DisconnectSource::Mlme(..) => DS::Mlme,
833 }
834 }
835}
836
837#[derive(Debug, Default, Copy, Clone, PartialEq)]
838struct ConnectAttemptsCounter {
839 success: u64,
840 total: u64,
841}
842
843impl ConnectAttemptsCounter {
844 fn increment(&mut self, code: fidl_ieee80211::StatusCode) {
845 self.total += 1;
846 if code == fidl_ieee80211::StatusCode::Success {
847 self.success += 1;
848 }
849 }
850}
851
852struct DailyConnectStats {
853 last_log_time: fasync::BootInstant,
854 connect_per_security_type: HashMap<
855 metrics::SuccessfulConnectBreakdownBySecurityTypeMetricDimensionSecurityType,
856 ConnectAttemptsCounter,
857 >,
858 connect_per_primary_channel: HashMap<u8, ConnectAttemptsCounter>,
859 connect_per_channel_band: HashMap<
860 metrics::SuccessfulConnectBreakdownByChannelBandMetricDimensionChannelBand,
861 ConnectAttemptsCounter,
862 >,
863 connect_per_rssi_bucket:
864 HashMap<metrics::ConnectivityWlanMetricDimensionRssiBucket, ConnectAttemptsCounter>,
865 connect_per_snr_bucket:
866 HashMap<metrics::ConnectivityWlanMetricDimensionSnrBucket, ConnectAttemptsCounter>,
867 connect_per_is_owe_transition: HashMap<
868 metrics::DailyConnectSuccessRateBreakdownByIsOweTransitionMetricDimensionIsOweTransition,
869 ConnectAttemptsCounter,
870 >,
871}
872
873impl DailyConnectStats {
874 fn new(now: fasync::BootInstant) -> Self {
875 Self {
876 last_log_time: now,
877 connect_per_security_type: HashMap::new(),
878 connect_per_primary_channel: HashMap::new(),
879 connect_per_channel_band: HashMap::new(),
880 connect_per_rssi_bucket: HashMap::new(),
881 connect_per_snr_bucket: HashMap::new(),
882 connect_per_is_owe_transition: HashMap::new(),
883 }
884 }
885}
886
887fn float_to_ten_thousandth(value: f64) -> i64 {
890 (value * 10000f64) as i64
891}
892
893fn append_device_connected_cobalt_metrics(
894 metric_events: &mut Vec<MetricEvent>,
895 bss: &BssDescription,
896 is_owe_transition: bool,
897) {
898 metric_events.push(MetricEvent {
899 metric_id: metrics::NUMBER_OF_CONNECTED_DEVICES_METRIC_ID,
900 event_codes: vec![],
901 payload: MetricEventPayload::Count(1),
902 });
903
904 let security_type_dim = convert_security_type(&bss.protection());
905 metric_events.push(MetricEvent {
906 metric_id: metrics::CONNECTED_NETWORK_SECURITY_TYPE_METRIC_ID,
907 event_codes: vec![security_type_dim as u32],
908 payload: MetricEventPayload::Count(1),
909 });
910
911 if bss.supports_uapsd() {
912 metric_events.push(MetricEvent {
913 metric_id: metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_APSD_METRIC_ID,
914 event_codes: vec![],
915 payload: MetricEventPayload::Count(1),
916 });
917 }
918
919 if let Some(rm_enabled_cap) = bss.rm_enabled_cap() {
920 if rm_enabled_cap.link_measurement_enabled() {
921 metric_events.push(MetricEvent {
922 metric_id: metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_LINK_MEASUREMENT_METRIC_ID,
923 event_codes: vec![],
924 payload: MetricEventPayload::Count(1),
925 });
926 }
927 if rm_enabled_cap.neighbor_report_enabled() {
928 metric_events.push(MetricEvent {
929 metric_id: metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_NEIGHBOR_REPORT_METRIC_ID,
930 event_codes: vec![],
931 payload: MetricEventPayload::Count(1),
932 });
933 }
934 }
935
936 if bss.supports_ft() {
937 metric_events.push(MetricEvent {
938 metric_id: metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_FT_METRIC_ID,
939 event_codes: vec![],
940 payload: MetricEventPayload::Count(1),
941 });
942 }
943
944 if let Some(cap) = bss.ext_cap().and_then(|cap| cap.ext_caps_octet_3)
945 && cap.bss_transition()
946 {
947 metric_events.push(MetricEvent {
948 metric_id:
949 metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_BSS_TRANSITION_MANAGEMENT_METRIC_ID,
950 event_codes: vec![],
951 payload: MetricEventPayload::Count(1),
952 });
953 }
954
955 append_device_connected_channel_cobalt_metrics(metric_events, bss.channel);
956
957 let oui_string = bss.bssid.to_oui_uppercase("");
958 metric_events.push(MetricEvent {
959 metric_id: metrics::DEVICE_CONNECTED_TO_AP_OUI_2_METRIC_ID,
960 event_codes: vec![],
961 payload: MetricEventPayload::StringValue(oui_string),
962 });
963
964 let is_owe_transition_dim = convert_is_owe_transition(is_owe_transition);
965 metric_events.push(MetricEvent {
966 metric_id: metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_IS_OWE_TRANSITION_METRIC_ID,
967 event_codes: vec![is_owe_transition_dim as u32],
968 payload: MetricEventPayload::Count(1),
969 });
970}
971
972fn append_device_connected_channel_cobalt_metrics(
973 metric_events: &mut Vec<MetricEvent>,
974 channel: Channel,
975) {
976 metric_events.push(MetricEvent {
977 metric_id: metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID,
978 event_codes: vec![channel.primary as u32],
979 payload: MetricEventPayload::Count(1),
980 });
981
982 let channel_band_dim = convert_channel_band(channel.band);
983 metric_events.push(MetricEvent {
984 metric_id: metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_CHANNEL_BAND_METRIC_ID,
985 event_codes: vec![channel_band_dim as u32],
986 payload: MetricEventPayload::Count(1),
987 });
988}
989
990#[cfg(test)]
991mod tests {
992 use super::*;
993 use crate::testing::*;
994 use assert_matches::assert_matches;
995 use diagnostics_assertions::{
996 AnyBoolProperty, AnyBytesProperty, AnyNumericProperty, AnyStringProperty, assert_data_tree,
997 };
998 use futures::task::Poll;
999 use ieee80211_testutils::{BSSID_REGEX, SSID_REGEX};
1000 use std::pin::pin;
1001 use strum::EnumCount;
1002 use test_case::test_case;
1003 use windowed_stats::experimental::clock::Timed;
1004 use windowed_stats::experimental::inspect::TimeMatrixClient;
1005 use windowed_stats::experimental::testing::TimeMatrixCall;
1006 use wlan_common::channel::{Bandwidth, Channel};
1007 use wlan_common::ie::IeType;
1008 use wlan_common::test_utils::fake_stas::IesOverrides;
1009 use wlan_common::{fake_bss_description, random_bss_description};
1010
1011 #[fuchsia::test]
1012 fn log_connect_attempt_then_inspect_data_tree_contains_time_matrix_metadata() {
1013 let mut harness = setup_test();
1014
1015 let client =
1016 TimeMatrixClient::new(harness.inspect_node.create_child("wlan_connect_disconnect"));
1017 let logger = ConnectDisconnectLogger::new(
1018 harness.filtered_cobalt_logger(),
1019 &harness.inspect_node,
1020 &harness.inspect_metadata_node,
1021 &harness.inspect_metadata_path,
1022 &client,
1023 DeviceMobility::Mobile,
1024 );
1025 let bss = random_bss_description!();
1026 let mut log_connect_attempt = pin!(logger.handle_connect_attempt(
1027 fidl_ieee80211::StatusCode::Success,
1028 &bss,
1029 false,
1030 false
1031 ));
1032 assert!(
1033 harness.run_until_stalled_drain_cobalt_events(&mut log_connect_attempt).is_ready(),
1034 "`log_connect_attempt` did not complete",
1035 );
1036
1037 let tree = harness.get_inspect_data_tree();
1038 assert_data_tree!(
1039 @executor harness.exec,
1040 tree,
1041 root: contains {
1042 test_stats: contains {
1043 wlan_connect_disconnect: contains {
1044 wlan_connectivity_states: {
1045 "type": "bitset",
1046 "data": AnyBytesProperty,
1047 metadata: {
1048 index: {
1049 "0": "idle",
1050 "1": "disconnected",
1051 "2": "connect_failed",
1052 "3": "connected",
1053 "4": "start_failure",
1054 "5": "stop_failure",
1055 "6": "pno_scan_failed",
1056 },
1057 },
1058 },
1059 connected_networks: {
1060 "type": "bitset",
1061 "data": AnyBytesProperty,
1062 metadata: {
1063 "index_node_path": "root/test_stats/metadata/connected_networks",
1064 },
1065 },
1066 disconnected_networks: {
1067 "type": "bitset",
1068 "data": AnyBytesProperty,
1069 metadata: {
1070 "index_node_path": "root/test_stats/metadata/connected_networks",
1071 },
1072 },
1073 disconnect_sources: {
1074 "type": "bitset",
1075 "data": AnyBytesProperty,
1076 metadata: {
1077 "index_node_path": "root/test_stats/metadata/disconnect_sources",
1078 },
1079 },
1080 connect_attempt_results: {
1081 "type": "bitset",
1082 "data": AnyBytesProperty,
1083 metadata: {
1084 "index_node_path": "root/test_stats/metadata/connect_attempt_results",
1085 },
1086 },
1087 },
1088 },
1089 }
1090 );
1091 }
1092
1093 #[fuchsia::test]
1094 fn test_log_connect_attempt_inspect() {
1095 let mut test_helper = setup_test();
1096 let logger = ConnectDisconnectLogger::new(
1097 test_helper.filtered_cobalt_logger(),
1098 &test_helper.inspect_node,
1099 &test_helper.inspect_metadata_node,
1100 &test_helper.inspect_metadata_path,
1101 &test_helper.mock_time_matrix_client,
1102 DeviceMobility::Mobile,
1103 );
1104
1105 let bss_description = random_bss_description!();
1107 let mut test_fut = pin!(logger.handle_connect_attempt(
1108 fidl_ieee80211::StatusCode::Success,
1109 &bss_description,
1110 false,
1111 false
1112 ));
1113 assert_eq!(
1114 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1115 Poll::Ready(())
1116 );
1117
1118 let data = test_helper.get_inspect_data_tree();
1120 assert_data_tree!(@executor test_helper.exec, data, root: contains {
1121 test_stats: contains {
1122 metadata: contains {
1123 connected_networks: contains {
1124 "0": {
1125 "@time": AnyNumericProperty,
1126 "data": contains {
1127 bssid: &*BSSID_REGEX,
1128 ssid: &*SSID_REGEX,
1129 }
1130 }
1131 },
1132 connect_attempt_results: contains {
1133 "0": {
1134 "@time": AnyNumericProperty,
1135 "data": contains {
1136 status_code: 0u64,
1137 result: "Success",
1138 }
1139 }
1140 },
1141 },
1142 connect_events: {
1143 "0": {
1144 "@time": AnyNumericProperty,
1145 network_id: 0u64,
1146 }
1147 },
1148 connect_attempt_results: {
1149 "0": {
1150 "@time": AnyNumericProperty,
1151 result: "Success",
1152 ssid: &*SSID_REGEX,
1153 bssid: &*BSSID_REGEX,
1154 protection: AnyStringProperty,
1155 }
1156 }
1157 }
1158 });
1159
1160 let mut time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
1161 assert_eq!(
1162 &time_matrix_calls.drain::<u64>("wlan_connectivity_states")[..],
1163 &[TimeMatrixCall::Fold(Timed::now(1 << 0)), TimeMatrixCall::Fold(Timed::now(1 << 3)),]
1164 );
1165 assert_eq!(
1166 &time_matrix_calls.drain::<u64>("connected_networks")[..],
1167 &[TimeMatrixCall::Fold(Timed::now(1 << 0))]
1168 );
1169 assert_eq!(
1170 &time_matrix_calls.drain::<u64>("connect_attempt_results")[..],
1171 &[TimeMatrixCall::Fold(Timed::now(1 << 0))]
1172 );
1173 }
1174
1175 #[fuchsia::test]
1176 fn test_log_connect_attempt_cobalt() {
1177 let mut test_helper = setup_test();
1178 let logger = ConnectDisconnectLogger::new(
1179 test_helper.filtered_cobalt_logger(),
1180 &test_helper.inspect_node,
1181 &test_helper.inspect_metadata_node,
1182 &test_helper.inspect_metadata_path,
1183 &test_helper.mock_time_matrix_client,
1184 DeviceMobility::Mobile,
1185 );
1186
1187 let bss_description = random_bss_description!(Wpa2,
1189 channel: Channel::new(157, Bandwidth::Cbw40, fidl_ieee80211::WlanBand::FiveGhz),
1190 bssid: [0x00, 0xf6, 0x20, 0x03, 0x04, 0x05],
1191 );
1192
1193 let mut test_fut = pin!(logger.handle_connect_attempt(
1195 fidl_ieee80211::StatusCode::Success,
1196 &bss_description,
1197 false,
1198 false
1199 ));
1200 assert_eq!(
1201 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1202 Poll::Ready(())
1203 );
1204
1205 let breakdowns_by_status_code = test_helper
1207 .get_logged_metrics(metrics::CONNECT_ATTEMPT_BREAKDOWN_BY_STATUS_CODE_METRIC_ID);
1208 assert_eq!(breakdowns_by_status_code.len(), 1);
1209 assert_eq!(
1210 breakdowns_by_status_code[0].event_codes,
1211 vec![fidl_ieee80211::StatusCode::Success.into_primitive() as u32]
1212 );
1213 assert_eq!(breakdowns_by_status_code[0].payload, MetricEventPayload::Count(1));
1214
1215 let metrics_devices =
1216 test_helper.get_logged_metrics(metrics::NUMBER_OF_CONNECTED_DEVICES_METRIC_ID);
1217 assert_eq!(metrics_devices.len(), 1);
1218 assert_eq!(metrics_devices[0].payload, MetricEventPayload::Count(1));
1219
1220 let metrics_security =
1221 test_helper.get_logged_metrics(metrics::CONNECTED_NETWORK_SECURITY_TYPE_METRIC_ID);
1222 assert_eq!(metrics_security.len(), 1);
1223 assert_eq!(metrics_security[0].event_codes, vec![5]); let metrics_channel = test_helper.get_logged_metrics(
1226 metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID,
1227 );
1228 assert_eq!(metrics_channel.len(), 1);
1229 assert_eq!(metrics_channel[0].event_codes, vec![157]);
1230
1231 let metrics_band = test_helper.get_logged_metrics(
1232 metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_CHANNEL_BAND_METRIC_ID,
1233 );
1234 assert_eq!(metrics_band.len(), 1);
1235 assert_eq!(metrics_band[0].event_codes, vec![2]); let metrics_oui =
1238 test_helper.get_logged_metrics(metrics::DEVICE_CONNECTED_TO_AP_OUI_2_METRIC_ID);
1239 assert_eq!(metrics_oui.len(), 1);
1240 assert_eq!(metrics_oui[0].payload, MetricEventPayload::StringValue("00F620".to_string()));
1241
1242 let metrics_owe_transition = test_helper.get_logged_metrics(
1243 metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_IS_OWE_TRANSITION_METRIC_ID,
1244 );
1245 assert_eq!(metrics_owe_transition.len(), 1);
1246 assert_eq!(
1247 metrics_owe_transition[0].event_codes,
1248 vec![
1249 metrics::DailyConnectSuccessRateBreakdownByIsOweTransitionMetricDimensionIsOweTransition::No
1250 as u32
1251 ]
1252 );
1253 }
1254
1255 #[fuchsia::test]
1256 fn test_handle_channel_switched() {
1257 let mut test_helper = setup_test();
1258 let logger = ConnectDisconnectLogger::new(
1259 test_helper.filtered_cobalt_logger(),
1260 &test_helper.inspect_node,
1261 &test_helper.inspect_metadata_node,
1262 &test_helper.inspect_metadata_path,
1263 &test_helper.mock_time_matrix_client,
1264 DeviceMobility::Mobile,
1265 );
1266
1267 let channel = Channel::new(157, Bandwidth::Cbw40, fidl_ieee80211::WlanBand::FiveGhz);
1268 let mut test_fut = pin!(logger.handle_channel_switched(channel));
1269 assert_eq!(
1270 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1271 Poll::Ready(())
1272 );
1273
1274 let metrics_channel = test_helper.get_logged_metrics(
1275 metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID,
1276 );
1277 assert_eq!(metrics_channel.len(), 1);
1278 assert_eq!(metrics_channel[0].event_codes, vec![157]);
1279 assert_eq!(metrics_channel[0].payload, MetricEventPayload::Count(1));
1280
1281 let metrics_band = test_helper.get_logged_metrics(
1282 metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_CHANNEL_BAND_METRIC_ID,
1283 );
1284 assert_eq!(metrics_band.len(), 1);
1285 assert_eq!(metrics_band[0].event_codes, vec![2]); assert_eq!(metrics_band[0].payload, MetricEventPayload::Count(1));
1287 }
1288
1289 #[fuchsia::test]
1290 fn test_successive_connect_attempt_failures_cobalt_zero_failures() {
1291 let mut test_helper = setup_test();
1292 let logger = ConnectDisconnectLogger::new(
1293 test_helper.filtered_cobalt_logger(),
1294 &test_helper.inspect_node,
1295 &test_helper.inspect_metadata_node,
1296 &test_helper.inspect_metadata_path,
1297 &test_helper.mock_time_matrix_client,
1298 DeviceMobility::Mobile,
1299 );
1300
1301 let bss_description = random_bss_description!(Wpa2);
1302 let mut test_fut = pin!(logger.handle_connect_attempt(
1303 fidl_ieee80211::StatusCode::Success,
1304 &bss_description,
1305 false,
1306 false
1307 ));
1308 assert_eq!(
1309 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1310 Poll::Ready(())
1311 );
1312
1313 let metrics =
1314 test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
1315 assert_eq!(metrics.len(), 1);
1316 assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(0));
1317 }
1318
1319 #[fuchsia::test]
1320 fn test_log_device_connected_metrics_capabilities() {
1321 let mut test_helper = setup_test();
1322 let logger = ConnectDisconnectLogger::new(
1323 test_helper.filtered_cobalt_logger(),
1324 &test_helper.inspect_node,
1325 &test_helper.inspect_metadata_node,
1326 &test_helper.inspect_metadata_path,
1327 &test_helper.mock_time_matrix_client,
1328 DeviceMobility::Mobile,
1329 );
1330
1331 let wmm_info = vec![0x80]; #[rustfmt::skip]
1333 let rm_enabled_capabilities = vec![
1334 0x03, 0x00, 0x00, 0x00, 0x00,
1336 ];
1337 #[rustfmt::skip]
1338 let ext_capabilities = vec![
1339 0x04, 0x00,
1340 0x08, 0x00, 0x00, 0x00, 0x00, 0x40
1342 ];
1343
1344 let bss_description = fake_bss_description!(Wpa2,
1345 ies_overrides: IesOverrides::new()
1346 .remove(IeType::WMM_PARAM)
1347 .set(IeType::WMM_INFO, wmm_info)
1348 .set(IeType::RM_ENABLED_CAPABILITIES, rm_enabled_capabilities)
1349 .set(IeType::MOBILITY_DOMAIN, vec![0x00; 3])
1350 .set(IeType::EXT_CAPABILITIES, ext_capabilities),
1351 );
1352
1353 let mut test_fut = pin!(logger.handle_connect_attempt(
1354 fidl_ieee80211::StatusCode::Success,
1355 &bss_description,
1356 false,
1357 false
1358 ));
1359 assert_eq!(
1360 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1361 Poll::Ready(())
1362 );
1363
1364 let metrics = test_helper
1365 .get_logged_metrics(metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_APSD_METRIC_ID);
1366 assert_eq!(metrics.len(), 1);
1367 assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
1368
1369 let metrics = test_helper.get_logged_metrics(
1370 metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_BSS_TRANSITION_MANAGEMENT_METRIC_ID,
1371 );
1372 assert_eq!(metrics.len(), 1);
1373 assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
1374
1375 let metrics = test_helper.get_logged_metrics(
1376 metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_LINK_MEASUREMENT_METRIC_ID,
1377 );
1378 assert_eq!(metrics.len(), 1);
1379 assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
1380
1381 let metrics = test_helper.get_logged_metrics(
1382 metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_NEIGHBOR_REPORT_METRIC_ID,
1383 );
1384 assert_eq!(metrics.len(), 1);
1385 assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
1386 }
1387
1388 #[test_case(1; "one_failure")]
1389 #[test_case(2; "two_failures")]
1390 #[fuchsia::test(add_test_attr = false)]
1391 fn test_successive_connect_attempt_failures_cobalt_one_failure_then_success(n_failures: usize) {
1392 let mut test_helper = setup_test();
1393 let logger = ConnectDisconnectLogger::new(
1394 test_helper.filtered_cobalt_logger(),
1395 &test_helper.inspect_node,
1396 &test_helper.inspect_metadata_node,
1397 &test_helper.inspect_metadata_path,
1398 &test_helper.mock_time_matrix_client,
1399 DeviceMobility::Mobile,
1400 );
1401
1402 let bss_description = random_bss_description!(Wpa2);
1403 for _i in 0..n_failures {
1404 let mut test_fut = pin!(logger.handle_connect_attempt(
1405 fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
1406 &bss_description,
1407 false,
1408 false
1409 ));
1410 assert_eq!(
1411 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1412 Poll::Ready(())
1413 );
1414 }
1415
1416 let metrics =
1417 test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
1418 assert!(metrics.is_empty());
1419
1420 let mut test_fut = pin!(logger.handle_connect_attempt(
1421 fidl_ieee80211::StatusCode::Success,
1422 &bss_description,
1423 false,
1424 false
1425 ));
1426 assert_eq!(
1427 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1428 Poll::Ready(())
1429 );
1430
1431 let metrics =
1432 test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
1433 assert_eq!(metrics.len(), 1);
1434 assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(n_failures as i64));
1435
1436 test_helper.clear_cobalt_events();
1438 let mut test_fut = pin!(logger.handle_connect_attempt(
1439 fidl_ieee80211::StatusCode::Success,
1440 &bss_description,
1441 false,
1442 false
1443 ));
1444 assert_eq!(
1445 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1446 Poll::Ready(())
1447 );
1448 let metrics =
1449 test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
1450 assert_eq!(metrics.len(), 1);
1451 assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(0));
1452 }
1453
1454 #[test_case(1; "one_failure")]
1455 #[test_case(2; "two_failures")]
1456 #[fuchsia::test(add_test_attr = false)]
1457 fn test_successive_connect_attempt_failures_cobalt_one_failure_then_timeout(n_failures: usize) {
1458 let mut test_helper = setup_test();
1459 let logger = ConnectDisconnectLogger::new(
1460 test_helper.filtered_cobalt_logger(),
1461 &test_helper.inspect_node,
1462 &test_helper.inspect_metadata_node,
1463 &test_helper.inspect_metadata_path,
1464 &test_helper.mock_time_matrix_client,
1465 DeviceMobility::Mobile,
1466 );
1467
1468 let bss_description = random_bss_description!(Wpa2);
1469 for _i in 0..n_failures {
1470 let mut test_fut = pin!(logger.handle_connect_attempt(
1471 fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
1472 &bss_description,
1473 false,
1474 false
1475 ));
1476 assert_eq!(
1477 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1478 Poll::Ready(())
1479 );
1480 }
1481
1482 test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(60_000_000_000));
1483 let mut test_fut = pin!(logger.handle_periodic_telemetry());
1484 assert_eq!(
1485 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1486 Poll::Ready(())
1487 );
1488
1489 let metrics =
1491 test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
1492 assert!(metrics.is_empty());
1493
1494 test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(120_000_000_000));
1495 let mut test_fut = pin!(logger.handle_periodic_telemetry());
1496 assert_eq!(
1497 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1498 Poll::Ready(())
1499 );
1500
1501 let metrics =
1502 test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
1503 assert_eq!(metrics.len(), 1);
1504 assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(n_failures as i64));
1505
1506 test_helper.clear_cobalt_events();
1508 test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(240_000_000_000));
1509 let mut test_fut = pin!(logger.handle_periodic_telemetry());
1510 assert_eq!(
1511 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1512 Poll::Ready(())
1513 );
1514 let metrics =
1515 test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
1516 assert!(metrics.is_empty());
1517 }
1518
1519 #[fuchsia::test]
1520 fn test_daily_connect_success_rate_breakdowns() {
1521 let mut test_helper = setup_test();
1522 let logger = ConnectDisconnectLogger::new(
1523 test_helper.filtered_cobalt_logger(),
1524 &test_helper.inspect_node,
1525 &test_helper.inspect_metadata_node,
1526 &test_helper.inspect_metadata_path,
1527 &test_helper.mock_time_matrix_client,
1528 DeviceMobility::Mobile,
1529 );
1530
1531 let mut bss = random_bss_description!(Wpa2);
1532 bss.channel = Channel::new(6, Bandwidth::Cbw20, fidl_ieee80211::WlanBand::TwoGhz); bss.rssi_dbm = -50; bss.snr_db = 15; let mut test_fut = pin!(logger.handle_connect_attempt(
1538 fidl_ieee80211::StatusCode::Success,
1539 &bss,
1540 false,
1541 true
1542 ));
1543 assert_eq!(
1544 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1545 Poll::Ready(())
1546 );
1547
1548 let mut test_fut = pin!(logger.handle_connect_attempt(
1549 fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
1550 &bss,
1551 false,
1552 true
1553 ));
1554 assert_eq!(
1555 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1556 Poll::Ready(())
1557 );
1558
1559 test_helper.clear_cobalt_events();
1561 test_helper
1562 .exec
1563 .set_fake_time(fasync::MonotonicInstant::from_nanos(24 * 3600 * 1_000_000_000 - 1));
1564 let mut test_fut = pin!(logger.handle_periodic_telemetry());
1565 assert_eq!(
1566 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1567 Poll::Ready(())
1568 );
1569 assert!(
1570 test_helper
1571 .get_logged_metrics(
1572 metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_SECURITY_TYPE_METRIC_ID
1573 )
1574 .is_empty()
1575 );
1576
1577 test_helper
1579 .exec
1580 .set_fake_time(fasync::MonotonicInstant::from_nanos(24 * 3600 * 1_000_000_000));
1581 let mut test_fut = pin!(logger.handle_periodic_telemetry());
1582 assert_eq!(
1583 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1584 Poll::Ready(())
1585 );
1586
1587 let daily_security_metrics = test_helper.get_logged_metrics(
1589 metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_SECURITY_TYPE_METRIC_ID,
1590 );
1591 assert_eq!(daily_security_metrics.len(), 1);
1592 assert_eq!(
1593 daily_security_metrics[0].event_codes,
1594 vec![
1595 metrics::SuccessfulConnectBreakdownBySecurityTypeMetricDimensionSecurityType::Wpa2Personal
1596 as u32
1597 ]
1598 );
1599 assert_eq!(daily_security_metrics[0].payload, MetricEventPayload::IntegerValue(5000));
1600
1601 let daily_channel_metrics = test_helper.get_logged_metrics(
1603 metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID,
1604 );
1605 assert_eq!(daily_channel_metrics.len(), 1);
1606 assert_eq!(daily_channel_metrics[0].event_codes, vec![6]);
1607 assert_eq!(daily_channel_metrics[0].payload, MetricEventPayload::IntegerValue(5000));
1608
1609 let daily_band_metrics = test_helper.get_logged_metrics(
1611 metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_CHANNEL_BAND_METRIC_ID,
1612 );
1613 assert_eq!(daily_band_metrics.len(), 1);
1614 assert_eq!(
1615 daily_band_metrics[0].event_codes,
1616 vec![
1617 metrics::SuccessfulConnectBreakdownByChannelBandMetricDimensionChannelBand::Band2Dot4Ghz
1618 as u32
1619 ]
1620 );
1621 assert_eq!(daily_band_metrics[0].payload, MetricEventPayload::IntegerValue(5000));
1622
1623 let daily_rssi_metrics = test_helper.get_logged_metrics(
1625 metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_RSSI_BUCKET_METRIC_ID,
1626 );
1627 assert_eq!(daily_rssi_metrics.len(), 1);
1628 assert_eq!(
1629 daily_rssi_metrics[0].event_codes,
1630 vec![metrics::ConnectivityWlanMetricDimensionRssiBucket::From50To35 as u32]
1631 );
1632 assert_eq!(daily_rssi_metrics[0].payload, MetricEventPayload::IntegerValue(5000));
1633
1634 let daily_snr_metrics = test_helper.get_logged_metrics(
1636 metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_SNR_BUCKET_METRIC_ID,
1637 );
1638 assert_eq!(daily_snr_metrics.len(), 1);
1639 assert_eq!(
1640 daily_snr_metrics[0].event_codes,
1641 vec![metrics::ConnectivityWlanMetricDimensionSnrBucket::From11To15 as u32]
1642 );
1643 assert_eq!(daily_snr_metrics[0].payload, MetricEventPayload::IntegerValue(5000));
1644
1645 let daily_owe_metrics = test_helper.get_logged_metrics(
1647 metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_IS_OWE_TRANSITION_METRIC_ID,
1648 );
1649 assert_eq!(daily_owe_metrics.len(), 1);
1650 assert_eq!(
1651 daily_owe_metrics[0].event_codes,
1652 vec![
1653 metrics::DailyConnectSuccessRateBreakdownByIsOweTransitionMetricDimensionIsOweTransition::Yes
1654 as u32
1655 ]
1656 );
1657 assert_eq!(daily_owe_metrics[0].payload, MetricEventPayload::IntegerValue(5000));
1658 }
1659
1660 #[fuchsia::test]
1661 fn test_log_device_connected_cobalt_metrics_periodically() {
1662 let mut test_helper = setup_test();
1663 let logger = ConnectDisconnectLogger::new(
1664 test_helper.filtered_cobalt_logger(),
1665 &test_helper.inspect_node,
1666 &test_helper.inspect_metadata_node,
1667 &test_helper.inspect_metadata_path,
1668 &test_helper.mock_time_matrix_client,
1669 DeviceMobility::Mobile,
1670 );
1671
1672 let mut bss = random_bss_description!(Wpa2,
1673 bssid: [0x00, 0xf6, 0x20, 0x03, 0x04, 0x05],
1674 );
1675 bss.channel = Channel::new(6, Bandwidth::Cbw20, fidl_ieee80211::WlanBand::TwoGhz);
1676
1677 let mut test_fut = pin!(logger.handle_connect_attempt(
1679 fidl_ieee80211::StatusCode::Success,
1680 &bss,
1681 false,
1682 false
1683 ));
1684 assert_eq!(
1685 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1686 Poll::Ready(())
1687 );
1688
1689 let connected_metrics =
1690 test_helper.get_logged_metrics(metrics::NUMBER_OF_CONNECTED_DEVICES_METRIC_ID);
1691 assert_eq!(connected_metrics.len(), 1);
1692
1693 test_helper.clear_cobalt_events();
1695 test_helper
1696 .exec
1697 .set_fake_time(fasync::MonotonicInstant::from_nanos(24 * 3600 * 1_000_000_000 - 1));
1698 let mut test_fut = pin!(logger.handle_periodic_telemetry());
1699 assert_eq!(
1700 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1701 Poll::Ready(())
1702 );
1703 assert!(
1704 test_helper
1705 .get_logged_metrics(metrics::NUMBER_OF_CONNECTED_DEVICES_METRIC_ID)
1706 .is_empty()
1707 );
1708
1709 test_helper
1711 .exec
1712 .set_fake_time(fasync::MonotonicInstant::from_nanos(24 * 3600 * 1_000_000_000));
1713 let mut test_fut = pin!(logger.handle_periodic_telemetry());
1714 assert_eq!(
1715 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1716 Poll::Ready(())
1717 );
1718
1719 let connected_metrics =
1720 test_helper.get_logged_metrics(metrics::NUMBER_OF_CONNECTED_DEVICES_METRIC_ID);
1721 assert_eq!(connected_metrics.len(), 1);
1722 assert_eq!(connected_metrics[0].payload, MetricEventPayload::Count(1));
1723
1724 let security_metrics =
1725 test_helper.get_logged_metrics(metrics::CONNECTED_NETWORK_SECURITY_TYPE_METRIC_ID);
1726 assert_eq!(security_metrics.len(), 1);
1727
1728 let channel_metrics = test_helper.get_logged_metrics(
1729 metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID,
1730 );
1731 assert_eq!(channel_metrics.len(), 1);
1732 assert_eq!(channel_metrics[0].event_codes, vec![6]);
1733
1734 let oui_metrics =
1735 test_helper.get_logged_metrics(metrics::DEVICE_CONNECTED_TO_AP_OUI_2_METRIC_ID);
1736 assert_eq!(oui_metrics.len(), 1);
1737 assert_eq!(oui_metrics[0].payload, MetricEventPayload::StringValue("00F620".to_string()));
1738 }
1739
1740 #[fuchsia::test]
1741 fn test_log_device_connected_cobalt_metrics_periodically_channel_switched() {
1742 let mut test_helper = setup_test();
1743 let logger = ConnectDisconnectLogger::new(
1744 test_helper.filtered_cobalt_logger(),
1745 &test_helper.inspect_node,
1746 &test_helper.inspect_metadata_node,
1747 &test_helper.inspect_metadata_path,
1748 &test_helper.mock_time_matrix_client,
1749 DeviceMobility::Mobile,
1750 );
1751
1752 let mut bss = random_bss_description!(Wpa2);
1753 bss.channel = Channel::new(6, Bandwidth::Cbw20, fidl_ieee80211::WlanBand::TwoGhz);
1754
1755 let mut test_fut = pin!(logger.handle_connect_attempt(
1757 fidl_ieee80211::StatusCode::Success,
1758 &bss,
1759 false,
1760 false
1761 ));
1762 assert_eq!(
1763 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1764 Poll::Ready(())
1765 );
1766
1767 let new_channel = Channel::new(36, Bandwidth::Cbw20, fidl_ieee80211::WlanBand::FiveGhz);
1769 let mut test_fut = pin!(logger.handle_channel_switched(new_channel));
1770 assert_eq!(
1771 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1772 Poll::Ready(())
1773 );
1774
1775 test_helper.clear_cobalt_events();
1776
1777 test_helper
1779 .exec
1780 .set_fake_time(fasync::MonotonicInstant::from_nanos(24 * 3600 * 1_000_000_000));
1781 let mut test_fut = pin!(logger.handle_periodic_telemetry());
1782 assert_eq!(
1783 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1784 Poll::Ready(())
1785 );
1786
1787 let channel_metrics = test_helper.get_logged_metrics(
1788 metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID,
1789 );
1790 assert_eq!(channel_metrics.len(), 1);
1791 assert_eq!(channel_metrics[0].event_codes, vec![36]);
1792 }
1793
1794 #[fuchsia::test]
1795 fn test_log_device_connected_cobalt_metrics_periodically_not_connected() {
1796 let mut test_helper = setup_test();
1797 let logger = ConnectDisconnectLogger::new(
1798 test_helper.filtered_cobalt_logger(),
1799 &test_helper.inspect_node,
1800 &test_helper.inspect_metadata_node,
1801 &test_helper.inspect_metadata_path,
1802 &test_helper.mock_time_matrix_client,
1803 DeviceMobility::Mobile,
1804 );
1805
1806 test_helper.clear_cobalt_events();
1807
1808 test_helper
1810 .exec
1811 .set_fake_time(fasync::MonotonicInstant::from_nanos(24 * 3600 * 1_000_000_000));
1812 let mut test_fut = pin!(logger.handle_periodic_telemetry());
1813 assert_eq!(
1814 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1815 Poll::Ready(())
1816 );
1817
1818 assert!(
1819 test_helper
1820 .get_logged_metrics(metrics::NUMBER_OF_CONNECTED_DEVICES_METRIC_ID)
1821 .is_empty()
1822 );
1823 }
1824
1825 #[fuchsia::test]
1826 fn test_log_connect_attempt_cobalt_owe_transition() {
1827 let mut test_helper = setup_test();
1828 let logger = ConnectDisconnectLogger::new(
1829 test_helper.filtered_cobalt_logger(),
1830 &test_helper.inspect_node,
1831 &test_helper.inspect_metadata_node,
1832 &test_helper.inspect_metadata_path,
1833 &test_helper.mock_time_matrix_client,
1834 DeviceMobility::Mobile,
1835 );
1836
1837 let bss_description = random_bss_description!(Wpa2,
1839 channel: Channel::new(157, Bandwidth::Cbw40, fidl_ieee80211::WlanBand::FiveGhz),
1840 bssid: [0x00, 0xf6, 0x20, 0x03, 0x04, 0x05],
1841 );
1842
1843 let mut test_fut = pin!(logger.handle_connect_attempt(
1845 fidl_ieee80211::StatusCode::Success,
1846 &bss_description,
1847 false,
1848 true
1849 ));
1850 assert_eq!(
1851 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1852 Poll::Ready(())
1853 );
1854
1855 let metrics_owe_transition = test_helper.get_logged_metrics(
1856 metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_IS_OWE_TRANSITION_METRIC_ID,
1857 );
1858 assert_eq!(metrics_owe_transition.len(), 1);
1859 assert_eq!(
1860 metrics_owe_transition[0].event_codes,
1861 vec![
1862 metrics::DailyConnectSuccessRateBreakdownByIsOweTransitionMetricDimensionIsOweTransition::Yes
1863 as u32
1864 ]
1865 );
1866 }
1867
1868 #[fuchsia::test]
1869 fn test_zero_successive_connect_attempt_failures_on_suspend() {
1870 let mut test_helper = setup_test();
1871 let logger = ConnectDisconnectLogger::new(
1872 test_helper.filtered_cobalt_logger(),
1873 &test_helper.inspect_node,
1874 &test_helper.inspect_metadata_node,
1875 &test_helper.inspect_metadata_path,
1876 &test_helper.mock_time_matrix_client,
1877 DeviceMobility::Mobile,
1878 );
1879
1880 let mut test_fut = pin!(logger.handle_suspend_imminent());
1881 assert_eq!(
1882 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1883 Poll::Ready(())
1884 );
1885
1886 let metrics =
1887 test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
1888 assert!(metrics.is_empty());
1889 }
1890
1891 #[test_case(1; "one_failure")]
1892 #[test_case(2; "two_failures")]
1893 #[fuchsia::test(add_test_attr = false)]
1894 fn test_one_or_more_successive_connect_attempt_failures_on_suspend(n_failures: usize) {
1895 let mut test_helper = setup_test();
1896 let logger = ConnectDisconnectLogger::new(
1897 test_helper.filtered_cobalt_logger(),
1898 &test_helper.inspect_node,
1899 &test_helper.inspect_metadata_node,
1900 &test_helper.inspect_metadata_path,
1901 &test_helper.mock_time_matrix_client,
1902 DeviceMobility::Mobile,
1903 );
1904
1905 let bss_description = random_bss_description!(Wpa2);
1906 for _i in 0..n_failures {
1907 let mut test_fut = pin!(logger.handle_connect_attempt(
1908 fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
1909 &bss_description,
1910 false,
1911 false
1912 ));
1913 assert_eq!(
1914 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1915 Poll::Ready(())
1916 );
1917 }
1918
1919 let mut test_fut = pin!(logger.handle_suspend_imminent());
1920 assert_eq!(
1921 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1922 Poll::Ready(())
1923 );
1924
1925 let metrics =
1926 test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
1927 assert_eq!(metrics.len(), 1);
1928 assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(n_failures as i64));
1929
1930 test_helper.clear_cobalt_events();
1931 let mut test_fut = pin!(logger.handle_suspend_imminent());
1932 assert_eq!(
1933 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1934 Poll::Ready(())
1935 );
1936
1937 let metrics =
1939 test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
1940 assert!(metrics.is_empty());
1941
1942 assert_matches!(*logger.connection_state.lock(), ConnectionState::ConnectFailed(_));
1944 }
1945
1946 #[fuchsia::test]
1947 fn test_log_disconnect_inspect() {
1948 let mut test_helper = setup_test();
1949 let logger = ConnectDisconnectLogger::new(
1950 test_helper.filtered_cobalt_logger(),
1951 &test_helper.inspect_node,
1952 &test_helper.inspect_metadata_node,
1953 &test_helper.inspect_metadata_path,
1954 &test_helper.mock_time_matrix_client,
1955 DeviceMobility::Mobile,
1956 );
1957
1958 let bss_description = fake_bss_description!(Open);
1960 let channel = bss_description.channel;
1961 let disconnect_info = DisconnectInfo {
1962 iface_id: 32,
1963 connected_duration: zx::BootDuration::from_seconds(30),
1964 is_sme_reconnecting: false,
1965 disconnect_source: fidl_sme::DisconnectSource::Ap(fidl_sme::DisconnectCause {
1966 mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
1967 reason_code: fidl_ieee80211::ReasonCode::UnspecifiedReason,
1968 }),
1969 original_bss_desc: Box::new(bss_description),
1970 current_rssi_dbm: -30,
1971 current_snr_db: 25,
1972 current_channel: channel,
1973 };
1974 let mut test_fut = pin!(logger.log_disconnect(&disconnect_info));
1975 assert_eq!(
1976 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1977 Poll::Ready(())
1978 );
1979
1980 let data = test_helper.get_inspect_data_tree();
1982 assert_data_tree!(@executor test_helper.exec, data, root: contains {
1983 test_stats: contains {
1984 metadata: contains {
1985 connected_networks: {
1986 "0": {
1987 "@time": AnyNumericProperty,
1988 "data": {
1989 bssid: &*BSSID_REGEX,
1990 ssid: &*SSID_REGEX,
1991 ht_cap: AnyBytesProperty,
1992 vht_cap: AnyBytesProperty,
1993 protection: "Open",
1994 is_wmm_assoc: AnyBoolProperty,
1995 wmm_param: AnyBytesProperty,
1996 }
1997 }
1998 },
1999 disconnect_sources: {
2000 "0": {
2001 "@time": AnyNumericProperty,
2002 "data": {
2003 source: "ap",
2004 reason: "UnspecifiedReason",
2005 mlme_event_name: "DeauthenticateIndication",
2006 }
2007 }
2008 },
2009 },
2010 disconnect_events: {
2011 "0": {
2012 "@time": AnyNumericProperty,
2013 connected_duration: zx::BootDuration::from_seconds(30).into_nanos(),
2014 disconnect_source_id: 0u64,
2015 network_id: 0u64,
2016 rssi_dbm: -30i64,
2017 snr_db: 25i64,
2018 channel: AnyStringProperty,
2019 }
2020 }
2021 }
2022 });
2023
2024 let mut time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
2025 assert_eq!(
2026 &time_matrix_calls.drain::<u64>("wlan_connectivity_states")[..],
2027 &[TimeMatrixCall::Fold(Timed::now(1 << 0)), TimeMatrixCall::Fold(Timed::now(1 << 1)),]
2028 );
2029 assert_eq!(
2030 &time_matrix_calls.drain::<u64>("disconnected_networks")[..],
2031 &[TimeMatrixCall::Fold(Timed::now(1 << 0))]
2032 );
2033 assert_eq!(
2034 &time_matrix_calls.drain::<u64>("disconnect_sources")[..],
2035 &[TimeMatrixCall::Fold(Timed::now(1 << 0))]
2036 );
2037 }
2038
2039 #[fuchsia::test]
2040 fn test_log_disconnect_cobalt() {
2041 let mut test_helper = setup_test();
2042 let logger = ConnectDisconnectLogger::new(
2043 test_helper.filtered_cobalt_logger(),
2044 &test_helper.inspect_node,
2045 &test_helper.inspect_metadata_node,
2046 &test_helper.inspect_metadata_path,
2047 &test_helper.mock_time_matrix_client,
2048 DeviceMobility::Mobile,
2049 );
2050
2051 let disconnect_info = DisconnectInfo {
2053 connected_duration: zx::BootDuration::from_millis(300_000),
2054 disconnect_source: fidl_sme::DisconnectSource::Ap(fidl_sme::DisconnectCause {
2055 mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
2056 reason_code: fidl_ieee80211::ReasonCode::ApInitiated,
2057 }),
2058 ..fake_disconnect_info()
2059 };
2060 let mut test_fut = pin!(logger.log_disconnect(&disconnect_info));
2061 assert_eq!(
2062 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
2063 Poll::Ready(())
2064 );
2065
2066 let disconnect_count_metrics =
2067 test_helper.get_logged_metrics(metrics::TOTAL_DISCONNECT_COUNT_METRIC_ID);
2068 assert_eq!(disconnect_count_metrics.len(), 1);
2069 assert_eq!(disconnect_count_metrics[0].payload, MetricEventPayload::Count(1));
2070
2071 let connected_duration_metrics =
2072 test_helper.get_logged_metrics(metrics::CONNECTED_DURATION_ON_DISCONNECT_METRIC_ID);
2073 assert_eq!(connected_duration_metrics.len(), 1);
2074 assert_eq!(
2075 connected_duration_metrics[0].payload,
2076 MetricEventPayload::IntegerValue(300_000)
2077 );
2078
2079 let disconnect_by_reason_metrics =
2080 test_helper.get_logged_metrics(metrics::DISCONNECT_BREAKDOWN_BY_REASON_CODE_METRIC_ID);
2081 assert_eq!(disconnect_by_reason_metrics.len(), 1);
2082 assert_eq!(disconnect_by_reason_metrics[0].payload, MetricEventPayload::Count(1));
2083 assert_eq!(disconnect_by_reason_metrics[0].event_codes.len(), 2);
2084 assert_eq!(
2085 disconnect_by_reason_metrics[0].event_codes[0],
2086 fidl_ieee80211::ReasonCode::ApInitiated.into_primitive() as u32
2087 );
2088 assert_eq!(
2089 disconnect_by_reason_metrics[0].event_codes[1],
2090 metrics::ConnectivityWlanMetricDimensionDisconnectSource::Ap as u32
2091 );
2092 }
2093
2094 #[test_case(
2095 fidl_sme::DisconnectSource::Ap(fidl_sme::DisconnectCause {
2096 mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
2097 reason_code: fidl_ieee80211::ReasonCode::UnspecifiedReason,
2098 }),
2099 true;
2100 "ap_disconnect_source"
2101 )]
2102 #[test_case(
2103 fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
2104 mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
2105 reason_code: fidl_ieee80211::ReasonCode::UnspecifiedReason,
2106 }),
2107 true;
2108 "mlme_disconnect_source_not_link_failed"
2109 )]
2110 #[test_case(
2111 fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
2112 mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
2113 reason_code: fidl_ieee80211::ReasonCode::MlmeLinkFailed,
2114 }),
2115 false;
2116 "mlme_link_failed"
2117 )]
2118 #[test_case(
2119 fidl_sme::DisconnectSource::User(fidl_sme::UserDisconnectReason::Unknown),
2120 false;
2121 "user_disconnect_source"
2122 )]
2123 #[fuchsia::test(add_test_attr = false)]
2124 fn test_log_disconnect_for_mobile_device_cobalt(
2125 disconnect_source: fidl_sme::DisconnectSource,
2126 should_log: bool,
2127 ) {
2128 let mut test_helper = setup_test();
2129 let logger = ConnectDisconnectLogger::new(
2130 test_helper.filtered_cobalt_logger(),
2131 &test_helper.inspect_node,
2132 &test_helper.inspect_metadata_node,
2133 &test_helper.inspect_metadata_path,
2134 &test_helper.mock_time_matrix_client,
2135 DeviceMobility::Mobile,
2136 );
2137
2138 let disconnect_info = DisconnectInfo { disconnect_source, ..fake_disconnect_info() };
2140 let mut test_fut = pin!(logger.log_disconnect(&disconnect_info));
2141 assert_eq!(
2142 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
2143 Poll::Ready(())
2144 );
2145
2146 let metrics = test_helper
2147 .get_logged_metrics(metrics::DISCONNECT_OCCURRENCE_FOR_MOBILE_DEVICE_METRIC_ID);
2148 if should_log {
2149 assert_eq!(metrics.len(), 1);
2150 assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
2151 assert_matches!(*logger.connection_state.lock(), ConnectionState::Disconnected(_));
2152 } else {
2153 assert!(metrics.is_empty());
2154 assert_matches!(*logger.connection_state.lock(), ConnectionState::Idle(_));
2155 }
2156 }
2157
2158 #[test_case(
2159 fidl_sme::DisconnectSource::Ap(fidl_sme::DisconnectCause {
2160 mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
2161 reason_code: fidl_ieee80211::ReasonCode::UnspecifiedReason,
2162 });
2163 "mlme_disconnect_source_not_link_failed"
2164 )]
2165 #[test_case(
2166 fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
2167 mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
2168 reason_code: fidl_ieee80211::ReasonCode::MlmeLinkFailed,
2169 });
2170 "mlme_link_failed"
2171 )]
2172 #[test_case(
2173 fidl_sme::DisconnectSource::User(fidl_sme::UserDisconnectReason::Unknown);
2174 "user_disconnect_source"
2175 )]
2176 #[fuchsia::test(add_test_attr = false)]
2177 fn test_log_disconnect_for_stationary_device(disconnect_source: fidl_sme::DisconnectSource) {
2178 let mut test_helper = setup_test();
2179 let logger = ConnectDisconnectLogger::new(
2180 test_helper.filtered_cobalt_logger(),
2181 &test_helper.inspect_node,
2182 &test_helper.inspect_metadata_node,
2183 &test_helper.inspect_metadata_path,
2184 &test_helper.mock_time_matrix_client,
2185 DeviceMobility::Stationary,
2186 );
2187
2188 let disconnect_info = DisconnectInfo { disconnect_source, ..fake_disconnect_info() };
2190 let mut test_fut = pin!(logger.log_disconnect(&disconnect_info));
2191 assert_eq!(
2192 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
2193 Poll::Ready(())
2194 );
2195
2196 let metrics = test_helper
2197 .get_logged_metrics(metrics::DISCONNECT_OCCURRENCE_FOR_MOBILE_DEVICE_METRIC_ID);
2198 assert!(metrics.is_empty());
2199 assert_matches!(*logger.connection_state.lock(), ConnectionState::Disconnected(_));
2200 }
2201
2202 #[fuchsia::test]
2203 fn test_log_downtime_post_disconnect_on_reconnect() {
2204 let mut test_helper = setup_test();
2205 let logger = ConnectDisconnectLogger::new(
2206 test_helper.filtered_cobalt_logger(),
2207 &test_helper.inspect_node,
2208 &test_helper.inspect_metadata_node,
2209 &test_helper.inspect_metadata_path,
2210 &test_helper.mock_time_matrix_client,
2211 DeviceMobility::Mobile,
2212 );
2213
2214 test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(15_000_000_000));
2216 let bss_description = random_bss_description!(Wpa2);
2217 let mut test_fut = pin!(logger.handle_connect_attempt(
2218 fidl_ieee80211::StatusCode::Success,
2219 &bss_description,
2220 false,
2221 false
2222 ));
2223 assert_eq!(
2224 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
2225 Poll::Ready(())
2226 );
2227
2228 let metrics = test_helper.get_logged_metrics(metrics::DOWNTIME_POST_DISCONNECT_METRIC_ID);
2230 assert!(metrics.is_empty());
2231
2232 assert_matches!(*logger.connection_state.lock(), ConnectionState::Connected(_));
2234
2235 test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(25_000_000_000));
2237 let disconnect_info = DisconnectInfo {
2238 connected_duration: zx::BootDuration::from_millis(300_000),
2239 disconnect_source: fidl_sme::DisconnectSource::Ap(fidl_sme::DisconnectCause {
2240 mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
2241 reason_code: fidl_ieee80211::ReasonCode::ApInitiated,
2242 }),
2243 ..fake_disconnect_info()
2244 };
2245 let mut test_fut = pin!(logger.log_disconnect(&disconnect_info));
2246 assert_eq!(
2247 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
2248 Poll::Ready(())
2249 );
2250
2251 assert_matches!(*logger.connection_state.lock(), ConnectionState::Disconnected(_));
2253
2254 test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(60_000_000_000));
2256 let mut test_fut = pin!(logger.handle_connect_attempt(
2257 fidl_ieee80211::StatusCode::Success,
2258 &bss_description,
2259 false,
2260 false
2261 ));
2262 assert_eq!(
2263 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
2264 Poll::Ready(())
2265 );
2266
2267 let metrics = test_helper.get_logged_metrics(metrics::DOWNTIME_POST_DISCONNECT_METRIC_ID);
2269 assert_eq!(metrics.len(), 1);
2270 assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(35_000));
2271
2272 assert_matches!(*logger.connection_state.lock(), ConnectionState::Connected(_));
2274 }
2275
2276 #[fuchsia::test]
2277 fn test_log_iface_destroyed() {
2278 let mut test_helper = setup_test();
2279 let logger = ConnectDisconnectLogger::new(
2280 test_helper.filtered_cobalt_logger(),
2281 &test_helper.inspect_node,
2282 &test_helper.inspect_metadata_node,
2283 &test_helper.inspect_metadata_path,
2284 &test_helper.mock_time_matrix_client,
2285 DeviceMobility::Mobile,
2286 );
2287
2288 let bss_description = random_bss_description!();
2290 let mut test_fut = pin!(logger.handle_connect_attempt(
2291 fidl_ieee80211::StatusCode::Success,
2292 &bss_description,
2293 false,
2294 false
2295 ));
2296 assert_eq!(
2297 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
2298 Poll::Ready(())
2299 );
2300
2301 assert_matches!(*logger.connection_state.lock(), ConnectionState::Connected(_));
2303
2304 let mut test_fut = pin!(logger.handle_iface_destroyed());
2306 assert_eq!(
2307 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
2308 Poll::Ready(())
2309 );
2310
2311 let mut time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
2312 assert_eq!(
2313 &time_matrix_calls.drain::<u64>("wlan_connectivity_states")[..],
2314 &[
2315 TimeMatrixCall::Fold(Timed::now(1 << 0)),
2316 TimeMatrixCall::Fold(Timed::now(1 << 3)),
2317 TimeMatrixCall::Fold(Timed::now(1 << 0))
2318 ]
2319 );
2320
2321 assert_matches!(*logger.connection_state.lock(), ConnectionState::Idle(_));
2323 }
2324
2325 #[fuchsia::test]
2326 fn test_log_disable_client_connections() {
2327 let mut test_helper = setup_test();
2328 let logger = ConnectDisconnectLogger::new(
2329 test_helper.filtered_cobalt_logger(),
2330 &test_helper.inspect_node,
2331 &test_helper.inspect_metadata_node,
2332 &test_helper.inspect_metadata_path,
2333 &test_helper.mock_time_matrix_client,
2334 DeviceMobility::Mobile,
2335 );
2336
2337 let bss_description = random_bss_description!();
2339 let mut test_fut = pin!(logger.handle_connect_attempt(
2340 fidl_ieee80211::StatusCode::Success,
2341 &bss_description,
2342 false,
2343 false
2344 ));
2345 assert_eq!(
2346 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
2347 Poll::Ready(())
2348 );
2349
2350 assert_matches!(*logger.connection_state.lock(), ConnectionState::Connected(_));
2352
2353 let mut test_fut =
2355 pin!(logger.handle_client_connections_toggle(&ClientConnectionsToggleEvent::Disabled));
2356 assert_eq!(
2357 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
2358 Poll::Ready(())
2359 );
2360
2361 let mut time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
2362 assert_eq!(
2363 &time_matrix_calls.drain::<u64>("wlan_connectivity_states")[..],
2364 &[
2365 TimeMatrixCall::Fold(Timed::now(1 << 0)),
2366 TimeMatrixCall::Fold(Timed::now(1 << 3)),
2367 TimeMatrixCall::Fold(Timed::now(1 << 0))
2368 ]
2369 );
2370
2371 assert_matches!(*logger.connection_state.lock(), ConnectionState::Idle(_));
2373 }
2374
2375 #[fuchsia::test]
2376 fn test_wlan_connectivity_states_credential_rejected() {
2377 let mut test_helper = setup_test();
2378 let logger = ConnectDisconnectLogger::new(
2379 test_helper.filtered_cobalt_logger(),
2380 &test_helper.inspect_node,
2381 &test_helper.inspect_metadata_node,
2382 &test_helper.inspect_metadata_path,
2383 &test_helper.mock_time_matrix_client,
2384 DeviceMobility::Mobile,
2385 );
2386
2387 let bss_description = random_bss_description!();
2389 let mut test_fut = pin!(logger.handle_connect_attempt(
2390 fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
2391 &bss_description,
2392 true,
2393 false
2394 ));
2395 assert_eq!(
2396 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
2397 Poll::Ready(())
2398 );
2399
2400 assert_matches!(*logger.connection_state.lock(), ConnectionState::Idle(_));
2401 }
2402
2403 #[fuchsia::test]
2404 fn test_wlan_connectivity_states_failed_to_start() {
2405 let mut test_helper = setup_test();
2406 let logger = ConnectDisconnectLogger::new(
2407 test_helper.filtered_cobalt_logger(),
2408 &test_helper.inspect_node,
2409 &test_helper.inspect_metadata_node,
2410 &test_helper.inspect_metadata_path,
2411 &test_helper.mock_time_matrix_client,
2412 DeviceMobility::Mobile,
2413 );
2414
2415 let mut test_fut = pin!(logger.handle_client_connections_failed_to_start());
2416 assert_eq!(
2417 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
2418 Poll::Ready(())
2419 );
2420
2421 let mut time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
2422 assert_eq!(
2423 &time_matrix_calls.drain::<u64>("wlan_connectivity_states")[..],
2424 &[
2425 TimeMatrixCall::Fold(Timed::now(1 << 0)), TimeMatrixCall::Fold(Timed::now(1 << 4)), ]
2428 );
2429 assert_matches!(*logger.connection_state.lock(), ConnectionState::FailedToStart(_));
2430 }
2431
2432 #[fuchsia::test]
2433 fn test_wlan_connectivity_states_failed_to_stop() {
2434 let mut test_helper = setup_test();
2435 let logger = ConnectDisconnectLogger::new(
2436 test_helper.filtered_cobalt_logger(),
2437 &test_helper.inspect_node,
2438 &test_helper.inspect_metadata_node,
2439 &test_helper.inspect_metadata_path,
2440 &test_helper.mock_time_matrix_client,
2441 DeviceMobility::Mobile,
2442 );
2443
2444 let mut test_fut = pin!(logger.handle_client_connections_failed_to_stop());
2445 assert_eq!(
2446 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
2447 Poll::Ready(())
2448 );
2449
2450 let mut time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
2451 assert_eq!(
2452 &time_matrix_calls.drain::<u64>("wlan_connectivity_states")[..],
2453 &[
2454 TimeMatrixCall::Fold(Timed::now(1 << 0)), TimeMatrixCall::Fold(Timed::now(1 << 5)), ]
2457 );
2458
2459 assert_matches!(*logger.connection_state.lock(), ConnectionState::FailedToStop(_));
2460 }
2461
2462 #[test_case(ConnectionState::Idle(IdleState {}))]
2463 #[test_case(ConnectionState::Disconnected(DisconnectedState {}))]
2464 #[test_case(ConnectionState::ConnectFailed(ConnectFailedState {}))]
2465 #[test_case(ConnectionState::PnoScanFailedIdle(PnoScanFailedIdleState {}))]
2466 fn test_connectivity_state_transition_on_pno_scan_failure(initial_state: ConnectionState) {
2467 let mut test_helper = setup_test();
2468 let logger = ConnectDisconnectLogger::new(
2469 test_helper.filtered_cobalt_logger(),
2470 &test_helper.inspect_node,
2471 &test_helper.inspect_metadata_node,
2472 &test_helper.inspect_metadata_path,
2473 &test_helper.mock_time_matrix_client,
2474 DeviceMobility::Mobile,
2475 );
2476
2477 *logger.connection_state.lock() = initial_state.clone();
2479
2480 let mut test_fut = pin!(logger.handle_pno_scan_failure());
2482 assert_matches!(
2483 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
2484 Poll::Ready(())
2485 );
2486
2487 let metric_events = test_helper
2489 .get_logged_metrics(metrics::PNO_SCAN_FAILURE_WHILE_NOT_CONNECTED_OCCURRENCE_METRIC_ID);
2490 assert_eq!(metric_events.len(), 1);
2491 assert_eq!(metric_events[0].payload, MetricEventPayload::Count(1));
2492
2493 let metric_events =
2494 test_helper.get_logged_metrics(metrics::PNO_SCAN_FAILURE_OCCURRENCE_METRIC_ID);
2495 assert_eq!(metric_events.len(), 1);
2496 assert_eq!(metric_events[0].payload, MetricEventPayload::Count(1));
2497
2498 let mut time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
2500 assert_eq!(
2501 *time_matrix_calls.drain::<u64>("wlan_connectivity_states")[..].last().unwrap(),
2502 TimeMatrixCall::Fold(Timed::now(1 << 6)), );
2504
2505 assert_matches!(*logger.connection_state.lock(), ConnectionState::PnoScanFailedIdle(_));
2507 }
2508
2509 #[test_case(ConnectionState::Connected(ConnectedState {
2510 bss: Box::new(fake_bss_description!(Wpa2)),
2511 is_owe_transition: false,
2512 }))]
2513 #[test_case(ConnectionState::FailedToStart(FailedToStartState {}))]
2514 #[test_case(ConnectionState::FailedToStop(FailedToStopState {}))]
2515 fn test_no_connectivity_state_transition_on_pno_scan_failure(initial_state: ConnectionState) {
2516 let mut test_helper = setup_test();
2517 let logger = ConnectDisconnectLogger::new(
2518 test_helper.filtered_cobalt_logger(),
2519 &test_helper.inspect_node,
2520 &test_helper.inspect_metadata_node,
2521 &test_helper.inspect_metadata_path,
2522 &test_helper.mock_time_matrix_client,
2523 DeviceMobility::Mobile,
2524 );
2525
2526 *logger.connection_state.lock() = initial_state.clone();
2528
2529 let mut test_fut = pin!(logger.handle_pno_scan_failure());
2531 assert_matches!(
2532 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
2533 Poll::Ready(())
2534 );
2535
2536 let metric_events =
2538 test_helper.get_logged_metrics(metrics::PNO_SCAN_FAILURE_OCCURRENCE_METRIC_ID);
2539 assert_eq!(metric_events.len(), 1);
2540 assert_eq!(metric_events[0].payload, MetricEventPayload::Count(1));
2541
2542 assert_eq!(logger.connection_state.lock().to_id(), initial_state.to_id());
2544 }
2545
2546 #[fuchsia::test]
2547 fn test_wlan_connectivity_states_bitset_map_size() {
2548 let enum_variant_count = ConnectionState::COUNT;
2549 let bitset_map_size =
2550 ConnectDisconnectTimeSeries::wlan_connectivity_states_bitset_map().len();
2551 assert_eq!(enum_variant_count, bitset_map_size);
2552 }
2553
2554 fn fake_disconnect_info() -> DisconnectInfo {
2555 let bss_description = random_bss_description!(Wpa2);
2556 let channel = bss_description.channel;
2557 DisconnectInfo {
2558 iface_id: 1,
2559 connected_duration: zx::BootDuration::from_hours(6),
2560 is_sme_reconnecting: false,
2561 disconnect_source: fidl_sme::DisconnectSource::User(
2562 fidl_sme::UserDisconnectReason::Unknown,
2563 ),
2564 original_bss_desc: bss_description.into(),
2565 current_rssi_dbm: -30,
2566 current_snr_db: 25,
2567 current_channel: channel,
2568 }
2569 }
2570}