1use crate::convert::{convert_channel_band, convert_security_type};
6use crate::processors::toggle_events::ClientConnectionsToggleEvent;
7use crate::util::cobalt_logger::log_cobalt_batch;
8use derivative::Derivative;
9use fidl_fuchsia_metrics::{MetricEvent, MetricEventPayload};
10use fidl_fuchsia_wlan_ieee80211 as fidl_ieee80211;
11use fidl_fuchsia_wlan_sme as fidl_sme;
12use fuchsia_async as fasync;
13use fuchsia_inspect::Node as InspectNode;
14use fuchsia_inspect_contrib::id_enum::IdEnum;
15use fuchsia_inspect_contrib::inspect_log;
16use fuchsia_inspect_contrib::nodes::{BoundedListNode, LruCacheNode};
17use fuchsia_inspect_derive::Unit;
18use fuchsia_sync::Mutex;
19use ieee80211::OuiFmt;
20use std::sync::Arc;
21use std::sync::atomic::{AtomicUsize, Ordering};
22use strum_macros::{Display, EnumIter};
23use windowed_stats::experimental::inspect::{InspectSender, InspectedTimeMatrix};
24use windowed_stats::experimental::series::interpolation::{ConstantSample, LastSample};
25use windowed_stats::experimental::series::metadata::{BitSetMap, BitSetNode};
26use windowed_stats::experimental::series::statistic::Union;
27use windowed_stats::experimental::series::{SamplingProfile, TimeMatrix};
28use wlan_common::bss::BssDescription;
29use wlan_common::channel::Channel;
30use wlan_legacy_metrics_registry as metrics;
31use zx;
32
33const INSPECT_CONNECT_EVENTS_LIMIT: usize = 10;
34const INSPECT_DISCONNECT_EVENTS_LIMIT: usize = 20;
35const INSPECT_CONNECT_ATTEMPT_RESULTS_LIMIT: usize = 50;
36const INSPECT_CONNECTED_NETWORKS_ID_LIMIT: usize = 16;
37const INSPECT_DISCONNECT_SOURCES_ID_LIMIT: usize = 32;
38const INSPECT_CONNECT_ATTEMPT_RESULTS_ID_LIMIT: usize = 32;
39const SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_TIMEOUT: zx::BootDuration =
40 zx::BootDuration::from_minutes(2);
41
42#[derive(Clone, Debug, Display, EnumIter)]
43enum ConnectionState {
44 Idle(IdleState),
45 Connected(ConnectedState),
46 Disconnected(DisconnectedState),
47 ConnectFailed(ConnectFailedState),
48 FailedToStart(FailedToStartState),
49 FailedToStop(FailedToStopState),
50 PnoScanFailedIdle(PnoScanFailedIdleState),
51}
52
53impl IdEnum for ConnectionState {
55 type Id = u8;
56 fn to_id(&self) -> Self::Id {
57 match self {
58 Self::Idle(_) => 0,
59 Self::Disconnected(_) => 1,
60 Self::ConnectFailed(_) => 2,
61 Self::Connected(_) => 3,
62 Self::FailedToStart(_) => 4,
63 Self::FailedToStop(_) => 5,
64 Self::PnoScanFailedIdle(_) => 6,
65 }
66 }
67}
68
69#[derive(Clone, Debug, Default)]
70struct IdleState {}
71
72#[derive(Clone, Debug, Default)]
73struct ConnectedState {}
74
75#[derive(Clone, Debug, Default)]
76struct DisconnectedState {}
77
78#[derive(Clone, Debug, Default)]
79struct ConnectFailedState {}
80
81#[derive(Clone, Debug, Default)]
82struct FailedToStartState {}
83
84#[derive(Clone, Debug, Default)]
85struct FailedToStopState {}
86
87#[derive(Clone, Debug, Default)]
88struct PnoScanFailedIdleState {}
89
90#[derive(Derivative, Unit)]
91#[derivative(PartialEq, Eq, Hash)]
92struct InspectConnectedNetwork {
93 bssid: String,
94 ssid: String,
95 protection: String,
96 ht_cap: Option<Vec<u8>>,
97 vht_cap: Option<Vec<u8>>,
98 #[derivative(PartialEq = "ignore")]
99 #[derivative(Hash = "ignore")]
100 wsc: Option<InspectNetworkWsc>,
101 is_wmm_assoc: bool,
102 wmm_param: Option<Vec<u8>>,
103}
104
105impl From<&BssDescription> for InspectConnectedNetwork {
106 fn from(bss_description: &BssDescription) -> Self {
107 Self {
108 bssid: bss_description.bssid.to_string(),
109 ssid: bss_description.ssid.to_string(),
110 protection: format!("{:?}", bss_description.protection()),
111 ht_cap: bss_description.raw_ht_cap().map(|cap| cap.bytes.into()),
112 vht_cap: bss_description.raw_vht_cap().map(|cap| cap.bytes.into()),
113 wsc: bss_description.probe_resp_wsc().as_ref().map(InspectNetworkWsc::from),
114 is_wmm_assoc: bss_description.find_wmm_param().is_some(),
115 wmm_param: bss_description.find_wmm_param().map(|bytes| bytes.into()),
116 }
117 }
118}
119
120#[derive(PartialEq, Unit, Hash)]
121struct InspectNetworkWsc {
122 device_name: String,
123 manufacturer: String,
124 model_name: String,
125 model_number: String,
126}
127
128impl From<&wlan_common::ie::wsc::ProbeRespWsc> for InspectNetworkWsc {
129 fn from(wsc: &wlan_common::ie::wsc::ProbeRespWsc) -> Self {
130 Self {
131 device_name: String::from_utf8_lossy(&wsc.device_name[..]).to_string(),
132 manufacturer: String::from_utf8_lossy(&wsc.manufacturer[..]).to_string(),
133 model_name: String::from_utf8_lossy(&wsc.model_name[..]).to_string(),
134 model_number: String::from_utf8_lossy(&wsc.model_number[..]).to_string(),
135 }
136 }
137}
138
139#[derive(PartialEq, Eq, Unit, Hash)]
140struct InspectConnectAttemptResult {
141 status_code: u16,
142 result: String,
143}
144
145#[derive(PartialEq, Eq, Unit, Hash)]
146struct InspectDisconnectSource {
147 source: String,
148 reason: String,
149 mlme_event_name: Option<String>,
150}
151
152impl From<&fidl_sme::DisconnectSource> for InspectDisconnectSource {
153 fn from(disconnect_source: &fidl_sme::DisconnectSource) -> Self {
154 match disconnect_source {
155 fidl_sme::DisconnectSource::User(reason) => Self {
156 source: "user".to_string(),
157 reason: format!("{reason:?}"),
158 mlme_event_name: None,
159 },
160 fidl_sme::DisconnectSource::Ap(cause) => Self {
161 source: "ap".to_string(),
162 reason: format!("{:?}", cause.reason_code),
163 mlme_event_name: Some(format!("{:?}", cause.mlme_event_name)),
164 },
165 fidl_sme::DisconnectSource::Mlme(cause) => Self {
166 source: "mlme".to_string(),
167 reason: format!("{:?}", cause.reason_code),
168 mlme_event_name: Some(format!("{:?}", cause.mlme_event_name)),
169 },
170 }
171 }
172}
173
174#[derive(Clone, Debug, PartialEq)]
175pub struct DisconnectInfo {
176 pub iface_id: u16,
177 pub connected_duration: zx::BootDuration,
178 pub is_sme_reconnecting: bool,
179 pub disconnect_source: fidl_sme::DisconnectSource,
180 pub original_bss_desc: Box<BssDescription>,
181 pub current_rssi_dbm: i8,
182 pub current_snr_db: i8,
183 pub current_channel: Channel,
184}
185
186pub struct ConnectDisconnectLogger {
187 connection_state: Arc<Mutex<ConnectionState>>,
188 cobalt_proxy: fidl_fuchsia_metrics::MetricEventLoggerProxy,
189 connect_events_node: Mutex<BoundedListNode>,
190 disconnect_events_node: Mutex<BoundedListNode>,
191 connect_attempt_results_node: Mutex<BoundedListNode>,
192 inspect_metadata_node: Mutex<InspectMetadataNode>,
193 time_series_stats: ConnectDisconnectTimeSeries,
194 successive_connect_attempt_failures: AtomicUsize,
195 last_connect_failure_at: Arc<Mutex<Option<fasync::BootInstant>>>,
196 last_disconnect_at: Arc<Mutex<Option<fasync::MonotonicInstant>>>,
197}
198
199impl ConnectDisconnectLogger {
200 pub fn new<S: InspectSender>(
201 cobalt_proxy: fidl_fuchsia_metrics::MetricEventLoggerProxy,
202 inspect_node: &InspectNode,
203 inspect_metadata_node: &InspectNode,
204 inspect_metadata_path: &str,
205 time_matrix_client: &S,
206 ) -> Self {
207 let connect_events = inspect_node.create_child("connect_events");
208 let disconnect_events = inspect_node.create_child("disconnect_events");
209 let connect_attempt_results = inspect_node.create_child("connect_attempt_results");
210 let this = Self {
211 cobalt_proxy,
212 connection_state: Arc::new(Mutex::new(ConnectionState::Idle(IdleState {}))),
213 connect_events_node: Mutex::new(BoundedListNode::new(
214 connect_events,
215 INSPECT_CONNECT_EVENTS_LIMIT,
216 )),
217 disconnect_events_node: Mutex::new(BoundedListNode::new(
218 disconnect_events,
219 INSPECT_DISCONNECT_EVENTS_LIMIT,
220 )),
221 connect_attempt_results_node: Mutex::new(BoundedListNode::new(
222 connect_attempt_results,
223 INSPECT_CONNECT_ATTEMPT_RESULTS_LIMIT,
224 )),
225 inspect_metadata_node: Mutex::new(InspectMetadataNode::new(inspect_metadata_node)),
226 time_series_stats: ConnectDisconnectTimeSeries::new(
227 time_matrix_client,
228 inspect_metadata_path,
229 ),
230 successive_connect_attempt_failures: AtomicUsize::new(0),
231 last_connect_failure_at: Arc::new(Mutex::new(None)),
232 last_disconnect_at: Arc::new(Mutex::new(None)),
233 };
234 this.log_connection_state();
235 this
236 }
237
238 fn update_connection_state(&self, state: ConnectionState) {
239 *self.connection_state.lock() = state;
240 self.log_connection_state();
241 }
242
243 fn log_connection_state(&self) {
244 let wlan_connectivity_state_id = self.connection_state.lock().to_id() as u64;
245 self.time_series_stats.log_wlan_connectivity_state(1 << wlan_connectivity_state_id);
246 }
247
248 pub fn is_connected(&self) -> bool {
249 matches!(*self.connection_state.lock(), ConnectionState::Connected(_))
250 }
251
252 pub async fn handle_connect_attempt(
253 &self,
254 result: fidl_ieee80211::StatusCode,
255 bss: &BssDescription,
256 is_credential_rejected: bool,
257 ) {
258 let mut flushed_successive_failures = None;
259 let mut downtime_duration = None;
260 if result == fidl_ieee80211::StatusCode::Success {
261 self.update_connection_state(ConnectionState::Connected(ConnectedState {}));
262 flushed_successive_failures =
263 Some(self.successive_connect_attempt_failures.swap(0, Ordering::SeqCst));
264 downtime_duration =
265 self.last_disconnect_at.lock().map(|t| fasync::MonotonicInstant::now() - t);
266 } else if is_credential_rejected {
267 self.update_connection_state(ConnectionState::Idle(IdleState {}));
268 let _prev = self.successive_connect_attempt_failures.fetch_add(1, Ordering::SeqCst);
269 let _prev = self.last_connect_failure_at.lock().replace(fasync::BootInstant::now());
270 } else {
271 self.update_connection_state(ConnectionState::ConnectFailed(ConnectFailedState {}));
272 let _prev = self.successive_connect_attempt_failures.fetch_add(1, Ordering::SeqCst);
273 let _prev = self.last_connect_failure_at.lock().replace(fasync::BootInstant::now());
274 }
275
276 self.log_connect_attempt_inspect(result, bss);
277 self.log_connect_attempt_cobalt(result, flushed_successive_failures, downtime_duration)
278 .await;
279 if result == fidl_ieee80211::StatusCode::Success {
280 self.log_device_connected_cobalt_metrics(bss).await;
281 }
282 }
283
284 fn log_connect_attempt_inspect(
285 &self,
286 result: fidl_ieee80211::StatusCode,
287 bss: &BssDescription,
288 ) {
289 let mut inspect_metadata_node = self.inspect_metadata_node.lock();
290 let connect_result_id =
291 inspect_metadata_node.connect_attempt_results.insert(InspectConnectAttemptResult {
292 status_code: result.into_primitive(),
293 result: format!("{:?}", result),
294 }) as u64;
295 self.time_series_stats.log_connect_attempt_results(1 << connect_result_id);
296
297 inspect_log!(self.connect_attempt_results_node.lock(), {
298 result: format!("{:?}", result),
299 ssid: bss.ssid.to_string(),
300 bssid: bss.bssid.to_string(),
301 protection: format!("{:?}", bss.protection()),
302 });
303
304 if result == fidl_ieee80211::StatusCode::Success {
305 let connected_network = InspectConnectedNetwork::from(bss);
306 let connected_network_id =
307 inspect_metadata_node.connected_networks.insert(connected_network) as u64;
308
309 self.time_series_stats.log_connected_networks(1 << connected_network_id);
310
311 inspect_log!(self.connect_events_node.lock(), {
312 network_id: connected_network_id,
313 });
314 }
315 }
316
317 #[allow(clippy::vec_init_then_push, reason = "mass allow for https://fxbug.dev/381896734")]
318 async fn log_connect_attempt_cobalt(
319 &self,
320 result: fidl_ieee80211::StatusCode,
321 flushed_successive_failures: Option<usize>,
322 downtime_duration: Option<zx::MonotonicDuration>,
323 ) {
324 let mut metric_events = vec![];
325 metric_events.push(MetricEvent {
326 metric_id: metrics::CONNECT_ATTEMPT_BREAKDOWN_BY_STATUS_CODE_METRIC_ID,
327 event_codes: vec![result.into_primitive() as u32],
328 payload: MetricEventPayload::Count(1),
329 });
330
331 if let Some(failures) = flushed_successive_failures {
332 metric_events.push(MetricEvent {
333 metric_id: metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID,
334 event_codes: vec![],
335 payload: MetricEventPayload::IntegerValue(failures as i64),
336 });
337 }
338
339 if let Some(duration) = downtime_duration {
340 metric_events.push(MetricEvent {
341 metric_id: metrics::DOWNTIME_POST_DISCONNECT_METRIC_ID,
342 event_codes: vec![],
343 payload: MetricEventPayload::IntegerValue(duration.into_millis()),
344 });
345 }
346
347 log_cobalt_batch!(self.cobalt_proxy, &metric_events, "log_connect_attempt_cobalt");
348 }
349
350 async fn log_device_connected_cobalt_metrics(&self, bss: &BssDescription) {
351 let mut metric_events = vec![];
352 metric_events.push(MetricEvent {
353 metric_id: metrics::NUMBER_OF_CONNECTED_DEVICES_METRIC_ID,
354 event_codes: vec![],
355 payload: MetricEventPayload::Count(1),
356 });
357
358 let security_type_dim = convert_security_type(&bss.protection());
359 metric_events.push(MetricEvent {
360 metric_id: metrics::CONNECTED_NETWORK_SECURITY_TYPE_METRIC_ID,
361 event_codes: vec![security_type_dim as u32],
362 payload: MetricEventPayload::Count(1),
363 });
364
365 if bss.supports_uapsd() {
366 metric_events.push(MetricEvent {
367 metric_id: metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_APSD_METRIC_ID,
368 event_codes: vec![],
369 payload: MetricEventPayload::Count(1),
370 });
371 }
372
373 if let Some(rm_enabled_cap) = bss.rm_enabled_cap() {
374 if rm_enabled_cap.link_measurement_enabled() {
375 metric_events.push(MetricEvent {
376 metric_id:
377 metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_LINK_MEASUREMENT_METRIC_ID,
378 event_codes: vec![],
379 payload: MetricEventPayload::Count(1),
380 });
381 }
382 if rm_enabled_cap.neighbor_report_enabled() {
383 metric_events.push(MetricEvent {
384 metric_id:
385 metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_NEIGHBOR_REPORT_METRIC_ID,
386 event_codes: vec![],
387 payload: MetricEventPayload::Count(1),
388 });
389 }
390 }
391
392 if bss.supports_ft() {
393 metric_events.push(MetricEvent {
394 metric_id: metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_FT_METRIC_ID,
395 event_codes: vec![],
396 payload: MetricEventPayload::Count(1),
397 });
398 }
399
400 if let Some(cap) = bss.ext_cap().and_then(|cap| cap.ext_caps_octet_3)
401 && cap.bss_transition()
402 {
403 metric_events.push(MetricEvent {
404 metric_id: metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_BSS_TRANSITION_MANAGEMENT_METRIC_ID,
405 event_codes: vec![],
406 payload: MetricEventPayload::Count(1),
407 });
408 }
409
410 metric_events.push(MetricEvent {
411 metric_id: metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID,
412 event_codes: vec![bss.channel.primary as u32],
413 payload: MetricEventPayload::Count(1),
414 });
415
416 let channel_band_dim = convert_channel_band(bss.channel.primary);
417 metric_events.push(MetricEvent {
418 metric_id: metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_CHANNEL_BAND_METRIC_ID,
419 event_codes: vec![channel_band_dim as u32],
420 payload: MetricEventPayload::Count(1),
421 });
422
423 let oui_string = bss.bssid.to_oui_uppercase("");
424 metric_events.push(MetricEvent {
425 metric_id: metrics::DEVICE_CONNECTED_TO_AP_OUI_2_METRIC_ID,
426 event_codes: vec![],
427 payload: MetricEventPayload::StringValue(oui_string),
428 });
429
430 log_cobalt_batch!(self.cobalt_proxy, &metric_events, "log_device_connected_cobalt_metrics");
431 }
432
433 pub async fn log_disconnect(&self, info: &DisconnectInfo) {
434 if !info.disconnect_source.should_log_for_mobile_device() {
440 self.update_connection_state(ConnectionState::Idle(IdleState {}));
441 } else {
442 self.update_connection_state(ConnectionState::Disconnected(DisconnectedState {}));
443 }
444 let _prev = self.last_disconnect_at.lock().replace(fasync::MonotonicInstant::now());
445 self.log_disconnect_inspect(info);
446 self.log_disconnect_cobalt(info).await;
447 }
448
449 fn log_disconnect_inspect(&self, info: &DisconnectInfo) {
450 let mut inspect_metadata_node = self.inspect_metadata_node.lock();
451 let connected_network = InspectConnectedNetwork::from(&*info.original_bss_desc);
452 let connected_network_id =
453 inspect_metadata_node.connected_networks.insert(connected_network) as u64;
454 let disconnect_source = InspectDisconnectSource::from(&info.disconnect_source);
455 let disconnect_source_id =
456 inspect_metadata_node.disconnect_sources.insert(disconnect_source) as u64;
457 inspect_log!(self.disconnect_events_node.lock(), {
458 connected_duration: info.connected_duration.into_nanos(),
459 disconnect_source_id: disconnect_source_id,
460 network_id: connected_network_id,
461 rssi_dbm: info.current_rssi_dbm,
462 snr_db: info.current_snr_db,
463 channel: format!("{}", info.current_channel),
464 });
465
466 self.time_series_stats.log_disconnected_networks(1 << connected_network_id);
467 self.time_series_stats.log_disconnect_sources(1 << disconnect_source_id);
468 }
469
470 async fn log_disconnect_cobalt(&self, info: &DisconnectInfo) {
471 let mut metric_events = vec![];
472 metric_events.push(MetricEvent {
473 metric_id: metrics::TOTAL_DISCONNECT_COUNT_METRIC_ID,
474 event_codes: vec![],
475 payload: MetricEventPayload::Count(1),
476 });
477
478 if info.disconnect_source.should_log_for_mobile_device() {
479 metric_events.push(MetricEvent {
480 metric_id: metrics::DISCONNECT_OCCURRENCE_FOR_MOBILE_DEVICE_METRIC_ID,
481 event_codes: vec![],
482 payload: MetricEventPayload::Count(1),
483 });
484 }
485
486 metric_events.push(MetricEvent {
487 metric_id: metrics::CONNECTED_DURATION_ON_DISCONNECT_METRIC_ID,
488 event_codes: vec![],
489 payload: MetricEventPayload::IntegerValue(info.connected_duration.into_millis()),
490 });
491
492 metric_events.push(MetricEvent {
493 metric_id: metrics::DISCONNECT_BREAKDOWN_BY_REASON_CODE_METRIC_ID,
494 event_codes: vec![
495 u32::from(info.disconnect_source.cobalt_reason_code()),
496 info.disconnect_source.as_cobalt_disconnect_source() as u32,
497 ],
498 payload: MetricEventPayload::Count(1),
499 });
500
501 log_cobalt_batch!(self.cobalt_proxy, &metric_events, "log_disconnect_cobalt");
502 }
503
504 pub async fn handle_periodic_telemetry(&self) {
505 let mut metric_events = vec![];
506 let now = fasync::BootInstant::now();
507 if let Some(failed_at) = *self.last_connect_failure_at.lock()
508 && now - failed_at >= SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_TIMEOUT
509 {
510 let failures = self.successive_connect_attempt_failures.swap(0, Ordering::SeqCst);
511 if failures > 0 {
512 metric_events.push(MetricEvent {
513 metric_id: metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID,
514 event_codes: vec![],
515 payload: MetricEventPayload::IntegerValue(failures as i64),
516 });
517 }
518 }
519
520 log_cobalt_batch!(self.cobalt_proxy, &metric_events, "handle_periodic_telemetry");
521 }
522
523 pub async fn handle_suspend_imminent(&self) {
524 let mut metric_events = vec![];
525
526 let flushed_successive_failures =
527 self.successive_connect_attempt_failures.swap(0, Ordering::SeqCst);
528 if flushed_successive_failures > 0 {
529 metric_events.push(MetricEvent {
530 metric_id: metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID,
531 event_codes: vec![],
532 payload: MetricEventPayload::IntegerValue(flushed_successive_failures as i64),
533 });
534 }
535
536 log_cobalt_batch!(self.cobalt_proxy, &metric_events, "handle_suspend_imminent");
537 }
538
539 pub async fn handle_iface_destroyed(&self) {
540 self.update_connection_state(ConnectionState::Idle(IdleState {}));
541 }
542
543 pub async fn handle_client_connections_toggle(&self, event: &ClientConnectionsToggleEvent) {
544 if event == &ClientConnectionsToggleEvent::Disabled {
545 self.update_connection_state(ConnectionState::Idle(IdleState {}));
546 }
547 }
548
549 pub async fn handle_pno_scan_failure(&self) {
550 let mut metric_events = vec![MetricEvent {
551 metric_id: metrics::PNO_SCAN_FAILURE_OCCURRENCE_METRIC_ID,
552 event_codes: vec![],
553 payload: MetricEventPayload::Count(1),
554 }];
555
556 let state = self.connection_state.lock().clone();
557 match state {
558 ConnectionState::Idle(_)
559 | ConnectionState::Disconnected(_)
560 | ConnectionState::ConnectFailed(_)
561 | ConnectionState::PnoScanFailedIdle(_) => {
562 metric_events.push(MetricEvent {
563 metric_id: metrics::PNO_SCAN_FAILURE_WHILE_NOT_CONNECTED_OCCURRENCE_METRIC_ID,
564 event_codes: vec![],
565 payload: MetricEventPayload::Count(1),
566 });
567
568 self.update_connection_state(ConnectionState::PnoScanFailedIdle(
572 PnoScanFailedIdleState {},
573 ));
574 }
575 ConnectionState::Connected(_)
576 | ConnectionState::FailedToStart(_)
577 | ConnectionState::FailedToStop(_) => {
578 }
582 }
583
584 log_cobalt_batch!(self.cobalt_proxy, &metric_events, "handle_pno_scan_failure");
585 }
586 pub async fn handle_client_connections_failed_to_start(&self) {
587 self.update_connection_state(ConnectionState::FailedToStart(FailedToStartState {}));
588 }
589
590 pub async fn handle_client_connections_failed_to_stop(&self) {
591 self.update_connection_state(ConnectionState::FailedToStop(FailedToStopState {}));
592 }
593}
594
595struct InspectMetadataNode {
596 connected_networks: LruCacheNode<InspectConnectedNetwork>,
597 disconnect_sources: LruCacheNode<InspectDisconnectSource>,
598 connect_attempt_results: LruCacheNode<InspectConnectAttemptResult>,
599}
600
601impl InspectMetadataNode {
602 const CONNECTED_NETWORKS: &'static str = "connected_networks";
603 const DISCONNECT_SOURCES: &'static str = "disconnect_sources";
604 const CONNECT_ATTEMPT_RESULTS: &'static str = "connect_attempt_results";
605
606 fn new(inspect_node: &InspectNode) -> Self {
607 let connected_networks = inspect_node.create_child(Self::CONNECTED_NETWORKS);
608 let disconnect_sources = inspect_node.create_child(Self::DISCONNECT_SOURCES);
609 let connect_attempt_results = inspect_node.create_child(Self::CONNECT_ATTEMPT_RESULTS);
610 Self {
611 connected_networks: LruCacheNode::new(
612 connected_networks,
613 INSPECT_CONNECTED_NETWORKS_ID_LIMIT,
614 ),
615 disconnect_sources: LruCacheNode::new(
616 disconnect_sources,
617 INSPECT_DISCONNECT_SOURCES_ID_LIMIT,
618 ),
619 connect_attempt_results: LruCacheNode::new(
620 connect_attempt_results,
621 INSPECT_CONNECT_ATTEMPT_RESULTS_ID_LIMIT,
622 ),
623 }
624 }
625}
626
627#[derive(Debug, Clone)]
628struct ConnectDisconnectTimeSeries {
629 wlan_connectivity_states: InspectedTimeMatrix<u64>,
630 connected_networks: InspectedTimeMatrix<u64>,
631 disconnected_networks: InspectedTimeMatrix<u64>,
632 disconnect_sources: InspectedTimeMatrix<u64>,
633 connect_attempt_results: InspectedTimeMatrix<u64>,
634}
635
636impl ConnectDisconnectTimeSeries {
637 pub fn new<S: InspectSender>(client: &S, inspect_metadata_path: &str) -> Self {
638 let wlan_connectivity_states = client.inspect_time_matrix_with_metadata(
639 "wlan_connectivity_states",
640 TimeMatrix::<Union<u64>, LastSample>::new(
641 SamplingProfile::highly_granular(),
642 LastSample::or(0),
643 ),
644 BitSetMap::from_ordered(Self::wlan_connectivity_states_bitset_map().iter().copied()),
646 );
647 let connected_networks = client.inspect_time_matrix_with_metadata(
648 "connected_networks",
649 TimeMatrix::<Union<u64>, ConstantSample>::new(
650 SamplingProfile::granular(),
651 ConstantSample::default(),
652 ),
653 BitSetNode::from_path(format!(
654 "{}/{}",
655 inspect_metadata_path,
656 InspectMetadataNode::CONNECTED_NETWORKS
657 )),
658 );
659 let disconnected_networks = client.inspect_time_matrix_with_metadata(
660 "disconnected_networks",
661 TimeMatrix::<Union<u64>, ConstantSample>::new(
662 SamplingProfile::granular(),
663 ConstantSample::default(),
664 ),
665 BitSetNode::from_path(format!(
667 "{}/{}",
668 inspect_metadata_path,
669 InspectMetadataNode::CONNECTED_NETWORKS
670 )),
671 );
672 let disconnect_sources = client.inspect_time_matrix_with_metadata(
673 "disconnect_sources",
674 TimeMatrix::<Union<u64>, ConstantSample>::new(
675 SamplingProfile::granular(),
676 ConstantSample::default(),
677 ),
678 BitSetNode::from_path(format!(
679 "{}/{}",
680 inspect_metadata_path,
681 InspectMetadataNode::DISCONNECT_SOURCES,
682 )),
683 );
684 let connect_attempt_results = client.inspect_time_matrix_with_metadata(
685 "connect_attempt_results",
686 TimeMatrix::<Union<u64>, ConstantSample>::new(
687 SamplingProfile::granular(),
688 ConstantSample::default(),
689 ),
690 BitSetNode::from_path(format!(
691 "{}/{}",
692 inspect_metadata_path,
693 InspectMetadataNode::CONNECT_ATTEMPT_RESULTS,
694 )),
695 );
696 Self {
697 wlan_connectivity_states,
698 connected_networks,
699 disconnected_networks,
700 disconnect_sources,
701 connect_attempt_results,
702 }
703 }
704
705 fn wlan_connectivity_states_bitset_map() -> &'static [&'static str] {
708 &[
709 "idle",
710 "disconnected",
711 "connect_failed",
712 "connected",
713 "start_failure",
714 "stop_failure",
715 "pno_scan_failed",
716 ]
717 }
718
719 fn log_wlan_connectivity_state(&self, data: u64) {
720 self.wlan_connectivity_states.fold_or_log_error(data);
721 }
722 fn log_connected_networks(&self, data: u64) {
723 self.connected_networks.fold_or_log_error(data);
724 }
725 fn log_disconnected_networks(&self, data: u64) {
726 self.disconnected_networks.fold_or_log_error(data);
727 }
728 fn log_disconnect_sources(&self, data: u64) {
729 self.disconnect_sources.fold_or_log_error(data);
730 }
731 fn log_connect_attempt_results(&self, data: u64) {
732 self.connect_attempt_results.fold_or_log_error(data);
733 }
734}
735
736pub trait DisconnectSourceExt {
737 fn should_log_for_mobile_device(&self) -> bool;
738 fn cobalt_reason_code(&self) -> u16;
739 fn as_cobalt_disconnect_source(
740 &self,
741 ) -> metrics::ConnectivityWlanMetricDimensionDisconnectSource;
742}
743
744impl DisconnectSourceExt for fidl_sme::DisconnectSource {
745 fn should_log_for_mobile_device(&self) -> bool {
746 match self {
747 fidl_sme::DisconnectSource::Ap(_) => true,
748 fidl_sme::DisconnectSource::Mlme(cause)
749 if cause.reason_code != fidl_ieee80211::ReasonCode::MlmeLinkFailed =>
750 {
751 true
752 }
753 _ => false,
754 }
755 }
756
757 fn cobalt_reason_code(&self) -> u16 {
758 let cobalt_disconnect_reason_code = match self {
759 fidl_sme::DisconnectSource::Ap(cause) | fidl_sme::DisconnectSource::Mlme(cause) => {
760 cause.reason_code.into_primitive()
761 }
762 fidl_sme::DisconnectSource::User(reason) => *reason as u16,
763 };
764 const REASON_CODE_MAX: u16 = 1000;
767 std::cmp::min(cobalt_disconnect_reason_code, REASON_CODE_MAX)
768 }
769
770 fn as_cobalt_disconnect_source(
771 &self,
772 ) -> metrics::ConnectivityWlanMetricDimensionDisconnectSource {
773 use metrics::ConnectivityWlanMetricDimensionDisconnectSource as DS;
774 match self {
775 fidl_sme::DisconnectSource::Ap(..) => DS::Ap,
776 fidl_sme::DisconnectSource::User(..) => DS::User,
777 fidl_sme::DisconnectSource::Mlme(..) => DS::Mlme,
778 }
779 }
780}
781
782#[cfg(test)]
783mod tests {
784 use super::*;
785 use crate::testing::*;
786 use assert_matches::assert_matches;
787 use diagnostics_assertions::{
788 AnyBoolProperty, AnyBytesProperty, AnyNumericProperty, AnyStringProperty, assert_data_tree,
789 };
790 use futures::task::Poll;
791 use ieee80211_testutils::{BSSID_REGEX, SSID_REGEX};
792 use rand::Rng;
793 use std::pin::pin;
794 use strum::IntoEnumIterator;
795 use test_case::test_case;
796 use windowed_stats::experimental::clock::Timed;
797 use windowed_stats::experimental::inspect::TimeMatrixClient;
798 use windowed_stats::experimental::testing::TimeMatrixCall;
799 use wlan_common::channel::{Cbw, Channel};
800 use wlan_common::ie::IeType;
801 use wlan_common::test_utils::fake_stas::IesOverrides;
802 use wlan_common::{fake_bss_description, random_bss_description};
803
804 #[fuchsia::test]
805 fn log_connect_attempt_then_inspect_data_tree_contains_time_matrix_metadata() {
806 let mut harness = setup_test();
807
808 let client =
809 TimeMatrixClient::new(harness.inspect_node.create_child("wlan_connect_disconnect"));
810 let logger = ConnectDisconnectLogger::new(
811 harness.cobalt_proxy.clone(),
812 &harness.inspect_node,
813 &harness.inspect_metadata_node,
814 &harness.inspect_metadata_path,
815 &client,
816 );
817 let bss = random_bss_description!();
818 let mut log_connect_attempt =
819 pin!(logger.handle_connect_attempt(fidl_ieee80211::StatusCode::Success, &bss, false));
820 assert!(
821 harness.run_until_stalled_drain_cobalt_events(&mut log_connect_attempt).is_ready(),
822 "`log_connect_attempt` did not complete",
823 );
824
825 let tree = harness.get_inspect_data_tree();
826 assert_data_tree!(
827 @executor harness.exec,
828 tree,
829 root: contains {
830 test_stats: contains {
831 wlan_connect_disconnect: contains {
832 wlan_connectivity_states: {
833 "type": "bitset",
834 "data": AnyBytesProperty,
835 metadata: {
836 index: {
837 "0": "idle",
838 "1": "disconnected",
839 "2": "connect_failed",
840 "3": "connected",
841 "4": "start_failure",
842 "5": "stop_failure",
843 "6": "pno_scan_failed",
844 },
845 },
846 },
847 connected_networks: {
848 "type": "bitset",
849 "data": AnyBytesProperty,
850 metadata: {
851 "index_node_path": "root/test_stats/metadata/connected_networks",
852 },
853 },
854 disconnected_networks: {
855 "type": "bitset",
856 "data": AnyBytesProperty,
857 metadata: {
858 "index_node_path": "root/test_stats/metadata/connected_networks",
859 },
860 },
861 disconnect_sources: {
862 "type": "bitset",
863 "data": AnyBytesProperty,
864 metadata: {
865 "index_node_path": "root/test_stats/metadata/disconnect_sources",
866 },
867 },
868 connect_attempt_results: {
869 "type": "bitset",
870 "data": AnyBytesProperty,
871 metadata: {
872 "index_node_path": "root/test_stats/metadata/connect_attempt_results",
873 },
874 },
875 },
876 },
877 }
878 );
879 }
880
881 #[fuchsia::test]
882 fn test_log_connect_attempt_inspect() {
883 let mut test_helper = setup_test();
884 let logger = ConnectDisconnectLogger::new(
885 test_helper.cobalt_proxy.clone(),
886 &test_helper.inspect_node,
887 &test_helper.inspect_metadata_node,
888 &test_helper.inspect_metadata_path,
889 &test_helper.mock_time_matrix_client,
890 );
891
892 let bss_description = random_bss_description!();
894 let mut test_fut = pin!(logger.handle_connect_attempt(
895 fidl_ieee80211::StatusCode::Success,
896 &bss_description,
897 false
898 ));
899 assert_eq!(
900 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
901 Poll::Ready(())
902 );
903
904 let data = test_helper.get_inspect_data_tree();
906 assert_data_tree!(@executor test_helper.exec, data, root: contains {
907 test_stats: contains {
908 metadata: contains {
909 connected_networks: contains {
910 "0": {
911 "@time": AnyNumericProperty,
912 "data": contains {
913 bssid: &*BSSID_REGEX,
914 ssid: &*SSID_REGEX,
915 }
916 }
917 },
918 connect_attempt_results: contains {
919 "0": {
920 "@time": AnyNumericProperty,
921 "data": contains {
922 status_code: 0u64,
923 result: "Success",
924 }
925 }
926 },
927 },
928 connect_events: {
929 "0": {
930 "@time": AnyNumericProperty,
931 network_id: 0u64,
932 }
933 },
934 connect_attempt_results: {
935 "0": {
936 "@time": AnyNumericProperty,
937 result: "Success",
938 ssid: &*SSID_REGEX,
939 bssid: &*BSSID_REGEX,
940 protection: AnyStringProperty,
941 }
942 }
943 }
944 });
945
946 let mut time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
947 assert_eq!(
948 &time_matrix_calls.drain::<u64>("wlan_connectivity_states")[..],
949 &[TimeMatrixCall::Fold(Timed::now(1 << 0)), TimeMatrixCall::Fold(Timed::now(1 << 3)),]
950 );
951 assert_eq!(
952 &time_matrix_calls.drain::<u64>("connected_networks")[..],
953 &[TimeMatrixCall::Fold(Timed::now(1 << 0))]
954 );
955 assert_eq!(
956 &time_matrix_calls.drain::<u64>("connect_attempt_results")[..],
957 &[TimeMatrixCall::Fold(Timed::now(1 << 0))]
958 );
959 }
960
961 #[fuchsia::test]
962 fn test_log_connect_attempt_cobalt() {
963 let mut test_helper = setup_test();
964 let logger = ConnectDisconnectLogger::new(
965 test_helper.cobalt_proxy.clone(),
966 &test_helper.inspect_node,
967 &test_helper.inspect_metadata_node,
968 &test_helper.inspect_metadata_path,
969 &test_helper.mock_time_matrix_client,
970 );
971
972 let bss_description = random_bss_description!(Wpa2,
974 channel: Channel::new(157, Cbw::Cbw40),
975 bssid: [0x00, 0xf6, 0x20, 0x03, 0x04, 0x05],
976 );
977
978 let mut test_fut = pin!(logger.handle_connect_attempt(
980 fidl_ieee80211::StatusCode::Success,
981 &bss_description,
982 false
983 ));
984 assert_eq!(
985 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
986 Poll::Ready(())
987 );
988
989 let breakdowns_by_status_code = test_helper
991 .get_logged_metrics(metrics::CONNECT_ATTEMPT_BREAKDOWN_BY_STATUS_CODE_METRIC_ID);
992 assert_eq!(breakdowns_by_status_code.len(), 1);
993 assert_eq!(
994 breakdowns_by_status_code[0].event_codes,
995 vec![fidl_ieee80211::StatusCode::Success.into_primitive() as u32]
996 );
997 assert_eq!(breakdowns_by_status_code[0].payload, MetricEventPayload::Count(1));
998
999 let metrics_devices =
1000 test_helper.get_logged_metrics(metrics::NUMBER_OF_CONNECTED_DEVICES_METRIC_ID);
1001 assert_eq!(metrics_devices.len(), 1);
1002 assert_eq!(metrics_devices[0].payload, MetricEventPayload::Count(1));
1003
1004 let metrics_security =
1005 test_helper.get_logged_metrics(metrics::CONNECTED_NETWORK_SECURITY_TYPE_METRIC_ID);
1006 assert_eq!(metrics_security.len(), 1);
1007 assert_eq!(metrics_security[0].event_codes, vec![5]); let metrics_channel = test_helper.get_logged_metrics(
1010 metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID,
1011 );
1012 assert_eq!(metrics_channel.len(), 1);
1013 assert_eq!(metrics_channel[0].event_codes, vec![157]);
1014
1015 let metrics_band = test_helper.get_logged_metrics(
1016 metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_CHANNEL_BAND_METRIC_ID,
1017 );
1018 assert_eq!(metrics_band.len(), 1);
1019 assert_eq!(metrics_band[0].event_codes, vec![2]); let metrics_oui =
1022 test_helper.get_logged_metrics(metrics::DEVICE_CONNECTED_TO_AP_OUI_2_METRIC_ID);
1023 assert_eq!(metrics_oui.len(), 1);
1024 assert_eq!(metrics_oui[0].payload, MetricEventPayload::StringValue("00F620".to_string()));
1025 }
1026
1027 #[fuchsia::test]
1028 fn test_successive_connect_attempt_failures_cobalt_zero_failures() {
1029 let mut test_helper = setup_test();
1030 let logger = ConnectDisconnectLogger::new(
1031 test_helper.cobalt_proxy.clone(),
1032 &test_helper.inspect_node,
1033 &test_helper.inspect_metadata_node,
1034 &test_helper.inspect_metadata_path,
1035 &test_helper.mock_time_matrix_client,
1036 );
1037
1038 let bss_description = random_bss_description!(Wpa2);
1039 let mut test_fut = pin!(logger.handle_connect_attempt(
1040 fidl_ieee80211::StatusCode::Success,
1041 &bss_description,
1042 false
1043 ));
1044 assert_eq!(
1045 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1046 Poll::Ready(())
1047 );
1048
1049 let metrics =
1050 test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
1051 assert_eq!(metrics.len(), 1);
1052 assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(0));
1053 }
1054
1055 #[fuchsia::test]
1056 fn test_log_device_connected_metrics_capabilities() {
1057 let mut test_helper = setup_test();
1058 let logger = ConnectDisconnectLogger::new(
1059 test_helper.cobalt_proxy.clone(),
1060 &test_helper.inspect_node,
1061 &test_helper.inspect_metadata_node,
1062 &test_helper.inspect_metadata_path,
1063 &test_helper.mock_time_matrix_client,
1064 );
1065
1066 let wmm_info = vec![0x80]; #[rustfmt::skip]
1068 let rm_enabled_capabilities = vec![
1069 0x03, 0x00, 0x00, 0x00, 0x00,
1071 ];
1072 #[rustfmt::skip]
1073 let ext_capabilities = vec![
1074 0x04, 0x00,
1075 0x08, 0x00, 0x00, 0x00, 0x00, 0x40
1077 ];
1078
1079 let bss_description = fake_bss_description!(Wpa2,
1080 ies_overrides: IesOverrides::new()
1081 .remove(IeType::WMM_PARAM)
1082 .set(IeType::WMM_INFO, wmm_info)
1083 .set(IeType::RM_ENABLED_CAPABILITIES, rm_enabled_capabilities)
1084 .set(IeType::MOBILITY_DOMAIN, vec![0x00; 3])
1085 .set(IeType::EXT_CAPABILITIES, ext_capabilities),
1086 );
1087
1088 let mut test_fut = pin!(logger.handle_connect_attempt(
1089 fidl_ieee80211::StatusCode::Success,
1090 &bss_description,
1091 false
1092 ));
1093 assert_eq!(
1094 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1095 Poll::Ready(())
1096 );
1097
1098 let metrics = test_helper
1099 .get_logged_metrics(metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_APSD_METRIC_ID);
1100 assert_eq!(metrics.len(), 1);
1101 assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
1102
1103 let metrics = test_helper.get_logged_metrics(
1104 metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_BSS_TRANSITION_MANAGEMENT_METRIC_ID,
1105 );
1106 assert_eq!(metrics.len(), 1);
1107 assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
1108
1109 let metrics = test_helper.get_logged_metrics(
1110 metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_LINK_MEASUREMENT_METRIC_ID,
1111 );
1112 assert_eq!(metrics.len(), 1);
1113 assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
1114
1115 let metrics = test_helper.get_logged_metrics(
1116 metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_NEIGHBOR_REPORT_METRIC_ID,
1117 );
1118 assert_eq!(metrics.len(), 1);
1119 assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
1120 }
1121
1122 #[test_case(1; "one_failure")]
1123 #[test_case(2; "two_failures")]
1124 #[fuchsia::test(add_test_attr = false)]
1125 fn test_successive_connect_attempt_failures_cobalt_one_failure_then_success(n_failures: usize) {
1126 let mut test_helper = setup_test();
1127 let logger = ConnectDisconnectLogger::new(
1128 test_helper.cobalt_proxy.clone(),
1129 &test_helper.inspect_node,
1130 &test_helper.inspect_metadata_node,
1131 &test_helper.inspect_metadata_path,
1132 &test_helper.mock_time_matrix_client,
1133 );
1134
1135 let bss_description = random_bss_description!(Wpa2);
1136 for _i in 0..n_failures {
1137 let mut test_fut = pin!(logger.handle_connect_attempt(
1138 fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
1139 &bss_description,
1140 false
1141 ));
1142 assert_eq!(
1143 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1144 Poll::Ready(())
1145 );
1146 }
1147
1148 let metrics =
1149 test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
1150 assert!(metrics.is_empty());
1151
1152 let mut test_fut = pin!(logger.handle_connect_attempt(
1153 fidl_ieee80211::StatusCode::Success,
1154 &bss_description,
1155 false
1156 ));
1157 assert_eq!(
1158 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1159 Poll::Ready(())
1160 );
1161
1162 let metrics =
1163 test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
1164 assert_eq!(metrics.len(), 1);
1165 assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(n_failures as i64));
1166
1167 test_helper.clear_cobalt_events();
1169 let mut test_fut = pin!(logger.handle_connect_attempt(
1170 fidl_ieee80211::StatusCode::Success,
1171 &bss_description,
1172 false
1173 ));
1174 assert_eq!(
1175 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1176 Poll::Ready(())
1177 );
1178 let metrics =
1179 test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
1180 assert_eq!(metrics.len(), 1);
1181 assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(0));
1182 }
1183
1184 #[test_case(1; "one_failure")]
1185 #[test_case(2; "two_failures")]
1186 #[fuchsia::test(add_test_attr = false)]
1187 fn test_successive_connect_attempt_failures_cobalt_one_failure_then_timeout(n_failures: usize) {
1188 let mut test_helper = setup_test();
1189 let logger = ConnectDisconnectLogger::new(
1190 test_helper.cobalt_proxy.clone(),
1191 &test_helper.inspect_node,
1192 &test_helper.inspect_metadata_node,
1193 &test_helper.inspect_metadata_path,
1194 &test_helper.mock_time_matrix_client,
1195 );
1196
1197 let bss_description = random_bss_description!(Wpa2);
1198 for _i in 0..n_failures {
1199 let mut test_fut = pin!(logger.handle_connect_attempt(
1200 fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
1201 &bss_description,
1202 false
1203 ));
1204 assert_eq!(
1205 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1206 Poll::Ready(())
1207 );
1208 }
1209
1210 test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(60_000_000_000));
1211 let mut test_fut = pin!(logger.handle_periodic_telemetry());
1212 assert_eq!(
1213 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1214 Poll::Ready(())
1215 );
1216
1217 let metrics =
1219 test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
1220 assert!(metrics.is_empty());
1221
1222 test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(120_000_000_000));
1223 let mut test_fut = pin!(logger.handle_periodic_telemetry());
1224 assert_eq!(
1225 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1226 Poll::Ready(())
1227 );
1228
1229 let metrics =
1230 test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
1231 assert_eq!(metrics.len(), 1);
1232 assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(n_failures as i64));
1233
1234 test_helper.clear_cobalt_events();
1236 test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(240_000_000_000));
1237 let mut test_fut = pin!(logger.handle_periodic_telemetry());
1238 assert_eq!(
1239 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1240 Poll::Ready(())
1241 );
1242 let metrics =
1243 test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
1244 assert!(metrics.is_empty());
1245 }
1246
1247 #[fuchsia::test]
1248 fn test_zero_successive_connect_attempt_failures_on_suspend() {
1249 let mut test_helper = setup_test();
1250 let logger = ConnectDisconnectLogger::new(
1251 test_helper.cobalt_proxy.clone(),
1252 &test_helper.inspect_node,
1253 &test_helper.inspect_metadata_node,
1254 &test_helper.inspect_metadata_path,
1255 &test_helper.mock_time_matrix_client,
1256 );
1257
1258 let mut test_fut = pin!(logger.handle_suspend_imminent());
1259 assert_eq!(
1260 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1261 Poll::Ready(())
1262 );
1263
1264 let metrics =
1265 test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
1266 assert!(metrics.is_empty());
1267 }
1268
1269 #[test_case(1; "one_failure")]
1270 #[test_case(2; "two_failures")]
1271 #[fuchsia::test(add_test_attr = false)]
1272 fn test_one_or_more_successive_connect_attempt_failures_on_suspend(n_failures: usize) {
1273 let mut test_helper = setup_test();
1274 let logger = ConnectDisconnectLogger::new(
1275 test_helper.cobalt_proxy.clone(),
1276 &test_helper.inspect_node,
1277 &test_helper.inspect_metadata_node,
1278 &test_helper.inspect_metadata_path,
1279 &test_helper.mock_time_matrix_client,
1280 );
1281
1282 let bss_description = random_bss_description!(Wpa2);
1283 for _i in 0..n_failures {
1284 let mut test_fut = pin!(logger.handle_connect_attempt(
1285 fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
1286 &bss_description,
1287 false
1288 ));
1289 assert_eq!(
1290 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1291 Poll::Ready(())
1292 );
1293 }
1294
1295 let mut test_fut = pin!(logger.handle_suspend_imminent());
1296 assert_eq!(
1297 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1298 Poll::Ready(())
1299 );
1300
1301 let metrics =
1302 test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
1303 assert_eq!(metrics.len(), 1);
1304 assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(n_failures as i64));
1305
1306 test_helper.clear_cobalt_events();
1307 let mut test_fut = pin!(logger.handle_suspend_imminent());
1308 assert_eq!(
1309 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1310 Poll::Ready(())
1311 );
1312
1313 let metrics =
1315 test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
1316 assert!(metrics.is_empty());
1317
1318 assert_matches!(*logger.connection_state.lock(), ConnectionState::ConnectFailed(_));
1320 }
1321
1322 #[fuchsia::test]
1323 fn test_log_disconnect_inspect() {
1324 let mut test_helper = setup_test();
1325 let logger = ConnectDisconnectLogger::new(
1326 test_helper.cobalt_proxy.clone(),
1327 &test_helper.inspect_node,
1328 &test_helper.inspect_metadata_node,
1329 &test_helper.inspect_metadata_path,
1330 &test_helper.mock_time_matrix_client,
1331 );
1332
1333 let bss_description = fake_bss_description!(Open);
1335 let channel = bss_description.channel;
1336 let disconnect_info = DisconnectInfo {
1337 iface_id: 32,
1338 connected_duration: zx::BootDuration::from_seconds(30),
1339 is_sme_reconnecting: false,
1340 disconnect_source: fidl_sme::DisconnectSource::Ap(fidl_sme::DisconnectCause {
1341 mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
1342 reason_code: fidl_ieee80211::ReasonCode::UnspecifiedReason,
1343 }),
1344 original_bss_desc: Box::new(bss_description),
1345 current_rssi_dbm: -30,
1346 current_snr_db: 25,
1347 current_channel: channel,
1348 };
1349 let mut test_fut = pin!(logger.log_disconnect(&disconnect_info));
1350 assert_eq!(
1351 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1352 Poll::Ready(())
1353 );
1354
1355 let data = test_helper.get_inspect_data_tree();
1357 assert_data_tree!(@executor test_helper.exec, data, root: contains {
1358 test_stats: contains {
1359 metadata: contains {
1360 connected_networks: {
1361 "0": {
1362 "@time": AnyNumericProperty,
1363 "data": {
1364 bssid: &*BSSID_REGEX,
1365 ssid: &*SSID_REGEX,
1366 ht_cap: AnyBytesProperty,
1367 vht_cap: AnyBytesProperty,
1368 protection: "Open",
1369 is_wmm_assoc: AnyBoolProperty,
1370 wmm_param: AnyBytesProperty,
1371 }
1372 }
1373 },
1374 disconnect_sources: {
1375 "0": {
1376 "@time": AnyNumericProperty,
1377 "data": {
1378 source: "ap",
1379 reason: "UnspecifiedReason",
1380 mlme_event_name: "DeauthenticateIndication",
1381 }
1382 }
1383 },
1384 },
1385 disconnect_events: {
1386 "0": {
1387 "@time": AnyNumericProperty,
1388 connected_duration: zx::BootDuration::from_seconds(30).into_nanos(),
1389 disconnect_source_id: 0u64,
1390 network_id: 0u64,
1391 rssi_dbm: -30i64,
1392 snr_db: 25i64,
1393 channel: AnyStringProperty,
1394 }
1395 }
1396 }
1397 });
1398
1399 let mut time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
1400 assert_eq!(
1401 &time_matrix_calls.drain::<u64>("wlan_connectivity_states")[..],
1402 &[TimeMatrixCall::Fold(Timed::now(1 << 0)), TimeMatrixCall::Fold(Timed::now(1 << 1)),]
1403 );
1404 assert_eq!(
1405 &time_matrix_calls.drain::<u64>("disconnected_networks")[..],
1406 &[TimeMatrixCall::Fold(Timed::now(1 << 0))]
1407 );
1408 assert_eq!(
1409 &time_matrix_calls.drain::<u64>("disconnect_sources")[..],
1410 &[TimeMatrixCall::Fold(Timed::now(1 << 0))]
1411 );
1412 }
1413
1414 #[fuchsia::test]
1415 fn test_log_disconnect_cobalt() {
1416 let mut test_helper = setup_test();
1417 let logger = ConnectDisconnectLogger::new(
1418 test_helper.cobalt_proxy.clone(),
1419 &test_helper.inspect_node,
1420 &test_helper.inspect_metadata_node,
1421 &test_helper.inspect_metadata_path,
1422 &test_helper.mock_time_matrix_client,
1423 );
1424
1425 let disconnect_info = DisconnectInfo {
1427 connected_duration: zx::BootDuration::from_millis(300_000),
1428 disconnect_source: fidl_sme::DisconnectSource::Ap(fidl_sme::DisconnectCause {
1429 mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
1430 reason_code: fidl_ieee80211::ReasonCode::ApInitiated,
1431 }),
1432 ..fake_disconnect_info()
1433 };
1434 let mut test_fut = pin!(logger.log_disconnect(&disconnect_info));
1435 assert_eq!(
1436 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1437 Poll::Ready(())
1438 );
1439
1440 let disconnect_count_metrics =
1441 test_helper.get_logged_metrics(metrics::TOTAL_DISCONNECT_COUNT_METRIC_ID);
1442 assert_eq!(disconnect_count_metrics.len(), 1);
1443 assert_eq!(disconnect_count_metrics[0].payload, MetricEventPayload::Count(1));
1444
1445 let connected_duration_metrics =
1446 test_helper.get_logged_metrics(metrics::CONNECTED_DURATION_ON_DISCONNECT_METRIC_ID);
1447 assert_eq!(connected_duration_metrics.len(), 1);
1448 assert_eq!(
1449 connected_duration_metrics[0].payload,
1450 MetricEventPayload::IntegerValue(300_000)
1451 );
1452
1453 let disconnect_by_reason_metrics =
1454 test_helper.get_logged_metrics(metrics::DISCONNECT_BREAKDOWN_BY_REASON_CODE_METRIC_ID);
1455 assert_eq!(disconnect_by_reason_metrics.len(), 1);
1456 assert_eq!(disconnect_by_reason_metrics[0].payload, MetricEventPayload::Count(1));
1457 assert_eq!(disconnect_by_reason_metrics[0].event_codes.len(), 2);
1458 assert_eq!(
1459 disconnect_by_reason_metrics[0].event_codes[0],
1460 fidl_ieee80211::ReasonCode::ApInitiated.into_primitive() as u32
1461 );
1462 assert_eq!(
1463 disconnect_by_reason_metrics[0].event_codes[1],
1464 metrics::ConnectivityWlanMetricDimensionDisconnectSource::Ap as u32
1465 );
1466 }
1467
1468 #[test_case(
1469 fidl_sme::DisconnectSource::Ap(fidl_sme::DisconnectCause {
1470 mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
1471 reason_code: fidl_ieee80211::ReasonCode::UnspecifiedReason,
1472 }),
1473 true;
1474 "ap_disconnect_source"
1475 )]
1476 #[test_case(
1477 fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
1478 mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
1479 reason_code: fidl_ieee80211::ReasonCode::UnspecifiedReason,
1480 }),
1481 true;
1482 "mlme_disconnect_source_not_link_failed"
1483 )]
1484 #[test_case(
1485 fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
1486 mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
1487 reason_code: fidl_ieee80211::ReasonCode::MlmeLinkFailed,
1488 }),
1489 false;
1490 "mlme_link_failed"
1491 )]
1492 #[test_case(
1493 fidl_sme::DisconnectSource::User(fidl_sme::UserDisconnectReason::Unknown),
1494 false;
1495 "user_disconnect_source"
1496 )]
1497 #[fuchsia::test(add_test_attr = false)]
1498 fn test_log_disconnect_for_mobile_device_cobalt(
1499 disconnect_source: fidl_sme::DisconnectSource,
1500 should_log: bool,
1501 ) {
1502 let mut test_helper = setup_test();
1503 let logger = ConnectDisconnectLogger::new(
1504 test_helper.cobalt_proxy.clone(),
1505 &test_helper.inspect_node,
1506 &test_helper.inspect_metadata_node,
1507 &test_helper.inspect_metadata_path,
1508 &test_helper.mock_time_matrix_client,
1509 );
1510
1511 let disconnect_info = DisconnectInfo { disconnect_source, ..fake_disconnect_info() };
1513 let mut test_fut = pin!(logger.log_disconnect(&disconnect_info));
1514 assert_eq!(
1515 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1516 Poll::Ready(())
1517 );
1518
1519 let metrics = test_helper
1520 .get_logged_metrics(metrics::DISCONNECT_OCCURRENCE_FOR_MOBILE_DEVICE_METRIC_ID);
1521 if should_log {
1522 assert_eq!(metrics.len(), 1);
1523 assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
1524 assert_matches!(*logger.connection_state.lock(), ConnectionState::Disconnected(_));
1525 } else {
1526 assert!(metrics.is_empty());
1527 assert_matches!(*logger.connection_state.lock(), ConnectionState::Idle(_));
1528 }
1529 }
1530
1531 #[fuchsia::test]
1532 fn test_log_downtime_post_disconnect_on_reconnect() {
1533 let mut test_helper = setup_test();
1534 let logger = ConnectDisconnectLogger::new(
1535 test_helper.cobalt_proxy.clone(),
1536 &test_helper.inspect_node,
1537 &test_helper.inspect_metadata_node,
1538 &test_helper.inspect_metadata_path,
1539 &test_helper.mock_time_matrix_client,
1540 );
1541
1542 test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(15_000_000_000));
1544 let bss_description = random_bss_description!(Wpa2);
1545 let mut test_fut = pin!(logger.handle_connect_attempt(
1546 fidl_ieee80211::StatusCode::Success,
1547 &bss_description,
1548 false
1549 ));
1550 assert_eq!(
1551 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1552 Poll::Ready(())
1553 );
1554
1555 let metrics = test_helper.get_logged_metrics(metrics::DOWNTIME_POST_DISCONNECT_METRIC_ID);
1557 assert!(metrics.is_empty());
1558
1559 assert_matches!(*logger.connection_state.lock(), ConnectionState::Connected(_));
1561
1562 test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(25_000_000_000));
1564 let disconnect_info = DisconnectInfo {
1565 connected_duration: zx::BootDuration::from_millis(300_000),
1566 disconnect_source: fidl_sme::DisconnectSource::Ap(fidl_sme::DisconnectCause {
1567 mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
1568 reason_code: fidl_ieee80211::ReasonCode::ApInitiated,
1569 }),
1570 ..fake_disconnect_info()
1571 };
1572 let mut test_fut = pin!(logger.log_disconnect(&disconnect_info));
1573 assert_eq!(
1574 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1575 Poll::Ready(())
1576 );
1577
1578 assert_matches!(*logger.connection_state.lock(), ConnectionState::Disconnected(_));
1580
1581 test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(60_000_000_000));
1583 let mut test_fut = pin!(logger.handle_connect_attempt(
1584 fidl_ieee80211::StatusCode::Success,
1585 &bss_description,
1586 false
1587 ));
1588 assert_eq!(
1589 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1590 Poll::Ready(())
1591 );
1592
1593 let metrics = test_helper.get_logged_metrics(metrics::DOWNTIME_POST_DISCONNECT_METRIC_ID);
1595 assert_eq!(metrics.len(), 1);
1596 assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(35_000));
1597
1598 assert_matches!(*logger.connection_state.lock(), ConnectionState::Connected(_));
1600 }
1601
1602 #[fuchsia::test]
1603 fn test_log_iface_destroyed() {
1604 let mut test_helper = setup_test();
1605 let logger = ConnectDisconnectLogger::new(
1606 test_helper.cobalt_proxy.clone(),
1607 &test_helper.inspect_node,
1608 &test_helper.inspect_metadata_node,
1609 &test_helper.inspect_metadata_path,
1610 &test_helper.mock_time_matrix_client,
1611 );
1612
1613 let bss_description = random_bss_description!();
1615 let mut test_fut = pin!(logger.handle_connect_attempt(
1616 fidl_ieee80211::StatusCode::Success,
1617 &bss_description,
1618 false
1619 ));
1620 assert_eq!(
1621 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1622 Poll::Ready(())
1623 );
1624
1625 assert_matches!(*logger.connection_state.lock(), ConnectionState::Connected(_));
1627
1628 let mut test_fut = pin!(logger.handle_iface_destroyed());
1630 assert_eq!(
1631 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1632 Poll::Ready(())
1633 );
1634
1635 let mut time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
1636 assert_eq!(
1637 &time_matrix_calls.drain::<u64>("wlan_connectivity_states")[..],
1638 &[
1639 TimeMatrixCall::Fold(Timed::now(1 << 0)),
1640 TimeMatrixCall::Fold(Timed::now(1 << 3)),
1641 TimeMatrixCall::Fold(Timed::now(1 << 0))
1642 ]
1643 );
1644
1645 assert_matches!(*logger.connection_state.lock(), ConnectionState::Idle(_));
1647 }
1648
1649 #[fuchsia::test]
1650 fn test_log_disable_client_connections() {
1651 let mut test_helper = setup_test();
1652 let logger = ConnectDisconnectLogger::new(
1653 test_helper.cobalt_proxy.clone(),
1654 &test_helper.inspect_node,
1655 &test_helper.inspect_metadata_node,
1656 &test_helper.inspect_metadata_path,
1657 &test_helper.mock_time_matrix_client,
1658 );
1659
1660 let bss_description = random_bss_description!();
1662 let mut test_fut = pin!(logger.handle_connect_attempt(
1663 fidl_ieee80211::StatusCode::Success,
1664 &bss_description,
1665 false
1666 ));
1667 assert_eq!(
1668 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1669 Poll::Ready(())
1670 );
1671
1672 assert_matches!(*logger.connection_state.lock(), ConnectionState::Connected(_));
1674
1675 let mut test_fut =
1677 pin!(logger.handle_client_connections_toggle(&ClientConnectionsToggleEvent::Disabled));
1678 assert_eq!(
1679 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1680 Poll::Ready(())
1681 );
1682
1683 let mut time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
1684 assert_eq!(
1685 &time_matrix_calls.drain::<u64>("wlan_connectivity_states")[..],
1686 &[
1687 TimeMatrixCall::Fold(Timed::now(1 << 0)),
1688 TimeMatrixCall::Fold(Timed::now(1 << 3)),
1689 TimeMatrixCall::Fold(Timed::now(1 << 0))
1690 ]
1691 );
1692
1693 assert_matches!(*logger.connection_state.lock(), ConnectionState::Idle(_));
1695 }
1696
1697 #[fuchsia::test]
1698 fn test_wlan_connectivity_states_credential_rejected() {
1699 let mut test_helper = setup_test();
1700 let logger = ConnectDisconnectLogger::new(
1701 test_helper.cobalt_proxy.clone(),
1702 &test_helper.inspect_node,
1703 &test_helper.inspect_metadata_node,
1704 &test_helper.inspect_metadata_path,
1705 &test_helper.mock_time_matrix_client,
1706 );
1707
1708 let bss_description = random_bss_description!();
1710 let mut test_fut = pin!(logger.handle_connect_attempt(
1711 fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
1712 &bss_description,
1713 true
1714 ));
1715 assert_eq!(
1716 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1717 Poll::Ready(())
1718 );
1719
1720 assert_matches!(*logger.connection_state.lock(), ConnectionState::Idle(_));
1721 }
1722
1723 #[fuchsia::test]
1724 fn test_wlan_connectivity_states_failed_to_start() {
1725 let mut test_helper = setup_test();
1726 let logger = ConnectDisconnectLogger::new(
1727 test_helper.cobalt_proxy.clone(),
1728 &test_helper.inspect_node,
1729 &test_helper.inspect_metadata_node,
1730 &test_helper.inspect_metadata_path,
1731 &test_helper.mock_time_matrix_client,
1732 );
1733
1734 let mut test_fut = pin!(logger.handle_client_connections_failed_to_start());
1735 assert_eq!(
1736 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1737 Poll::Ready(())
1738 );
1739
1740 let mut time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
1741 assert_eq!(
1742 &time_matrix_calls.drain::<u64>("wlan_connectivity_states")[..],
1743 &[
1744 TimeMatrixCall::Fold(Timed::now(1 << 0)), TimeMatrixCall::Fold(Timed::now(1 << 4)), ]
1747 );
1748 assert_matches!(*logger.connection_state.lock(), ConnectionState::FailedToStart(_));
1749 }
1750
1751 #[fuchsia::test]
1752 fn test_wlan_connectivity_states_failed_to_stop() {
1753 let mut test_helper = setup_test();
1754 let logger = ConnectDisconnectLogger::new(
1755 test_helper.cobalt_proxy.clone(),
1756 &test_helper.inspect_node,
1757 &test_helper.inspect_metadata_node,
1758 &test_helper.inspect_metadata_path,
1759 &test_helper.mock_time_matrix_client,
1760 );
1761
1762 let mut test_fut = pin!(logger.handle_client_connections_failed_to_stop());
1763 assert_eq!(
1764 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1765 Poll::Ready(())
1766 );
1767
1768 let mut time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
1769 assert_eq!(
1770 &time_matrix_calls.drain::<u64>("wlan_connectivity_states")[..],
1771 &[
1772 TimeMatrixCall::Fold(Timed::now(1 << 0)), TimeMatrixCall::Fold(Timed::now(1 << 5)), ]
1775 );
1776
1777 assert_matches!(*logger.connection_state.lock(), ConnectionState::FailedToStop(_));
1778 }
1779
1780 #[test_case(ConnectionState::Idle(IdleState {}))]
1781 #[test_case(ConnectionState::Disconnected(DisconnectedState {}))]
1782 #[test_case(ConnectionState::ConnectFailed(ConnectFailedState {}))]
1783 #[test_case(ConnectionState::PnoScanFailedIdle(PnoScanFailedIdleState {}))]
1784 fn test_connectivity_state_transition_on_pno_scan_failure(initial_state: ConnectionState) {
1785 let mut test_helper = setup_test();
1786 let logger = ConnectDisconnectLogger::new(
1787 test_helper.cobalt_proxy.clone(),
1788 &test_helper.inspect_node,
1789 &test_helper.inspect_metadata_node,
1790 &test_helper.inspect_metadata_path,
1791 &test_helper.mock_time_matrix_client,
1792 );
1793
1794 *logger.connection_state.lock() = initial_state.clone();
1796
1797 let mut test_fut = pin!(logger.handle_pno_scan_failure());
1799 assert_matches!(
1800 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1801 Poll::Ready(())
1802 );
1803
1804 let metric_events = test_helper
1806 .get_logged_metrics(metrics::PNO_SCAN_FAILURE_WHILE_NOT_CONNECTED_OCCURRENCE_METRIC_ID);
1807 assert_eq!(metric_events.len(), 1);
1808 assert_eq!(metric_events[0].payload, MetricEventPayload::Count(1));
1809
1810 let metric_events =
1811 test_helper.get_logged_metrics(metrics::PNO_SCAN_FAILURE_OCCURRENCE_METRIC_ID);
1812 assert_eq!(metric_events.len(), 1);
1813 assert_eq!(metric_events[0].payload, MetricEventPayload::Count(1));
1814
1815 let mut time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
1817 assert_eq!(
1818 *time_matrix_calls.drain::<u64>("wlan_connectivity_states")[..].last().unwrap(),
1819 TimeMatrixCall::Fold(Timed::now(1 << 6)), );
1821
1822 assert_matches!(*logger.connection_state.lock(), ConnectionState::PnoScanFailedIdle(_));
1824 }
1825
1826 #[test_case(ConnectionState::Connected(ConnectedState {}))]
1827 #[test_case(ConnectionState::FailedToStart(FailedToStartState {}))]
1828 #[test_case(ConnectionState::FailedToStop(FailedToStopState {}))]
1829 fn test_no_connectivity_state_transition_on_pno_scan_failure(initial_state: ConnectionState) {
1830 let mut test_helper = setup_test();
1831 let logger = ConnectDisconnectLogger::new(
1832 test_helper.cobalt_proxy.clone(),
1833 &test_helper.inspect_node,
1834 &test_helper.inspect_metadata_node,
1835 &test_helper.inspect_metadata_path,
1836 &test_helper.mock_time_matrix_client,
1837 );
1838
1839 *logger.connection_state.lock() = initial_state.clone();
1841
1842 let mut test_fut = pin!(logger.handle_pno_scan_failure());
1844 assert_matches!(
1845 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1846 Poll::Ready(())
1847 );
1848
1849 let metric_events =
1851 test_helper.get_logged_metrics(metrics::PNO_SCAN_FAILURE_OCCURRENCE_METRIC_ID);
1852 assert_eq!(metric_events.len(), 1);
1853 assert_eq!(metric_events[0].payload, MetricEventPayload::Count(1));
1854
1855 assert_eq!(logger.connection_state.lock().to_id(), initial_state.to_id());
1857 }
1858
1859 #[fuchsia::test]
1860 fn test_wlan_connectivity_states_bitset_map_size() {
1861 let enum_variant_count = ConnectionState::iter().count();
1862 let bitset_map_size =
1863 ConnectDisconnectTimeSeries::wlan_connectivity_states_bitset_map().len();
1864 assert_eq!(enum_variant_count, bitset_map_size);
1865 }
1866
1867 fn fake_disconnect_info() -> DisconnectInfo {
1868 let bss_description = random_bss_description!(Wpa2);
1869 let channel = bss_description.channel;
1870 DisconnectInfo {
1871 iface_id: 1,
1872 connected_duration: zx::BootDuration::from_hours(6),
1873 is_sme_reconnecting: false,
1874 disconnect_source: fidl_sme::DisconnectSource::User(
1875 fidl_sme::UserDisconnectReason::Unknown,
1876 ),
1877 original_bss_desc: bss_description.into(),
1878 current_rssi_dbm: -30,
1879 current_snr_db: 25,
1880 current_channel: channel,
1881 }
1882 }
1883}