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