blob: b1ed39d715faf64af4f18cdcf9c1f0474913ee2c [file] [log] [blame]
// Copyright 2018 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
mod convert;
use {
crate::{device::IfaceMap, inspect, telemetry::convert::*},
fidl_fuchsia_cobalt::HistogramBucket,
fidl_fuchsia_wlan_stats as fidl_stats,
fidl_fuchsia_wlan_stats::MlmeStats::{ApMlmeStats, ClientMlmeStats},
fuchsia_async as fasync,
fuchsia_cobalt::CobaltSender,
fuchsia_inspect_contrib::{inspect_log, make_inspect_loggable},
fuchsia_zircon as zx,
fuchsia_zircon::DurationNum,
futures::prelude::*,
futures::stream::FuturesUnordered,
log::{error, warn},
parking_lot::Mutex,
std::cmp::PartialOrd,
std::collections::hash_map::Entry,
std::collections::HashMap,
std::default::Default,
std::ops::Sub,
std::sync::Arc,
wlan_common::{bss::BssDescriptionExt, format::MacFmt},
wlan_metrics_registry as metrics,
wlan_sme::client::{
info::{
ConnectStats, ConnectionMilestone, ConnectionPingInfo, DisconnectInfo,
DisconnectSource, ScanStats,
},
AssociationFailure, ConnectFailure, ConnectResult,
},
};
type StatsRef = Arc<Mutex<fidl_stats::IfaceStats>>;
/// How often to request RSSI stats and dispatcher packet counts from MLME.
const REPORT_PERIOD_MINUTES: i64 = 1;
struct ReconnectInfo {
gap_time: zx::Duration,
same_ssid: bool,
}
// Export MLME stats to Cobalt every REPORT_PERIOD_MINUTES.
pub async fn report_telemetry_periodically(ifaces_map: Arc<IfaceMap>, mut sender: CobaltSender) {
// TODO(fxbug.dev/28800): Make this module resilient to Wlanstack2 downtime.
let mut last_reported_stats: HashMap<u16, StatsRef> = HashMap::new();
let mut interval_stream = fasync::Interval::new(REPORT_PERIOD_MINUTES.minutes());
while let Some(_) = interval_stream.next().await {
let mut futures = FuturesUnordered::new();
for (id, iface) in ifaces_map.get_snapshot().iter() {
let id = *id;
let iface = Arc::clone(iface);
let fut = iface.stats_sched.get_stats().map(move |r| (id, iface, r));
futures.push(fut);
}
while let Some((id, _iface, stats_result)) = futures.next().await {
match stats_result {
Ok(current_stats) => match last_reported_stats.entry(id) {
Entry::Vacant(entry) => {
entry.insert(current_stats);
}
Entry::Occupied(mut value) => {
let last_stats = value.get_mut();
report_stats(&last_stats.lock(), &current_stats.lock(), &mut sender);
let _dropped = std::mem::replace(value.get_mut(), current_stats);
}
},
Err(e) => {
last_reported_stats.remove(&id);
error!("Failed to get the stats for iface '{}': {}", id, e);
}
};
}
}
}
fn report_stats(
last_stats: &fidl_stats::IfaceStats,
current_stats: &fidl_stats::IfaceStats,
sender: &mut CobaltSender,
) {
report_mlme_stats(&last_stats.mlme_stats, &current_stats.mlme_stats, sender);
}
fn report_mlme_stats(
last: &Option<Box<fidl_stats::MlmeStats>>,
current: &Option<Box<fidl_stats::MlmeStats>>,
sender: &mut CobaltSender,
) {
if let (Some(ref last), Some(ref current)) = (last, current) {
match (last.as_ref(), current.as_ref()) {
(ClientMlmeStats(last), ClientMlmeStats(current)) => {
report_client_mlme_stats(&last, &current, sender)
}
(ApMlmeStats(_), ApMlmeStats(_)) => {}
_ => error!("Current MLME stats type is different from the last MLME stats type"),
};
}
}
fn report_client_mlme_stats(
last_stats: &fidl_stats::ClientMlmeStats,
current_stats: &fidl_stats::ClientMlmeStats,
sender: &mut CobaltSender,
) {
report_rssi_stats(
metrics::CLIENT_ASSOC_RSSI_METRIC_ID,
&last_stats.assoc_data_rssi,
&current_stats.assoc_data_rssi,
sender,
);
report_rssi_stats(
metrics::CLIENT_BEACON_RSSI_METRIC_ID,
&last_stats.beacon_rssi,
&current_stats.beacon_rssi,
sender,
);
}
fn report_rssi_stats(
rssi_metric_id: u32,
last_stats: &fidl_stats::RssiStats,
current_stats: &fidl_stats::RssiStats,
sender: &mut CobaltSender,
) {
// In the internal stats histogram, hist[x] represents the number of frames
// with RSSI -x. For the Cobalt representation, buckets from -128 to 0 are
// used. When data is sent to Cobalt, the concept of index is utilized.
//
// Shortly, for Cobalt:
// Bucket -128 -> index 0
// Bucket -127 -> index 1
// ...
// Bucket 0 -> index 128
//
// The for loop below converts the stats internal representation to the
// Cobalt representation and prepares the histogram that will be sent.
let mut histogram = Vec::new();
for bin in 0..current_stats.hist.len() {
let diff = get_diff(last_stats.hist[bin], current_stats.hist[bin]);
if diff > 0 {
let entry = HistogramBucket {
index: (fidl_stats::RSSI_BINS - (bin as u8) - 1).into(),
count: diff.into(),
};
histogram.push(entry);
}
}
if !histogram.is_empty() {
sender.log_int_histogram(rssi_metric_id, (), histogram);
}
}
fn get_diff<T>(last_stat: T, current_stat: T) -> T
where
T: Sub<Output = T> + PartialOrd + Default,
{
if current_stat >= last_stat {
current_stat - last_stat
} else {
Default::default()
}
}
pub fn log_scan_stats(
sender: &mut CobaltSender,
inspect_tree: Arc<inspect::WlanstackTree>,
scan_stats: &ScanStats,
is_join_scan: bool,
) {
let (scan_result_dim, error_code_dim) = convert_scan_result(&scan_stats.result);
let scan_type_dim = convert_scan_type(scan_stats.scan_type);
let is_join_scan_dim = convert_bool_dim(is_join_scan);
let client_state_dim = match scan_stats.scan_start_while_connected {
true => metrics::ScanResultMetricDimensionClientState::Connected,
false => metrics::ScanResultMetricDimensionClientState::Idle,
};
sender.log_event_count(
metrics::SCAN_RESULT_METRIC_ID,
[
scan_result_dim as u32,
scan_type_dim as u32,
is_join_scan_dim as u32,
client_state_dim as u32,
],
// Elapsed time during which the count of event has been gathered. We log 0 since
// we don't keep track of this.
0,
// Log one count of scan result.
1,
);
if let Some(error_code_dim) = error_code_dim {
inspect_log!(inspect_tree.client_stats.scan_failures.lock(), {});
sender.log_event_count(
metrics::SCAN_FAILURE_METRIC_ID,
[
error_code_dim as u32,
scan_type_dim as u32,
is_join_scan_dim as u32,
client_state_dim as u32,
],
0,
1,
)
}
let scan_time = scan_stats.scan_time().into_micros();
sender.log_elapsed_time(metrics::SCAN_TIME_METRIC_ID, (), scan_time);
sender.log_elapsed_time(
metrics::SCAN_TIME_PER_RESULT_METRIC_ID,
scan_result_dim as u32,
scan_time,
);
sender.log_elapsed_time(
metrics::SCAN_TIME_PER_SCAN_TYPE_METRIC_ID,
scan_type_dim as u32,
scan_time,
);
sender.log_elapsed_time(
metrics::SCAN_TIME_PER_JOIN_OR_DISCOVERY_METRIC_ID,
is_join_scan_dim as u32,
scan_time,
);
sender.log_elapsed_time(
metrics::SCAN_TIME_PER_CLIENT_STATE_METRIC_ID,
client_state_dim as u32,
scan_time,
);
}
pub fn log_connect_stats(
sender: &mut CobaltSender,
inspect_tree: Arc<inspect::WlanstackTree>,
connect_stats: &ConnectStats,
) {
if let Some(scan_stats) = connect_stats.join_scan_stats() {
log_scan_stats(sender, inspect_tree.clone(), &scan_stats, true);
}
log_connect_attempts_stats(sender, connect_stats);
log_connect_result_stats(sender, connect_stats);
log_time_to_connect_stats(sender, connect_stats);
let reconnect_info = log_connection_gap_time_stats(sender, connect_stats);
if let ConnectResult::Success = connect_stats.result {
inspect_log!(inspect_tree.client_stats.connect.lock(), {
attempts: connect_stats.attempts,
reconnect_info?: reconnect_info.map(|info| make_inspect_loggable!({
gap_time: info.gap_time.into_nanos(),
same_ssid: info.same_ssid,
})),
});
}
}
fn log_connect_attempts_stats(sender: &mut CobaltSender, connect_stats: &ConnectStats) {
// Only log attempts for successful connect. If connect is not successful, or if the expected
// fields for successful connect attempts are not there, early return.
match connect_stats.result {
ConnectResult::Success => (),
_ => return,
}
let is_multi_bss = match &connect_stats.scan_end_stats {
Some(stats) => stats.bss_count > 1,
None => return,
};
let bss = match &connect_stats.candidate_network {
Some(bss) => bss,
None => return,
};
use metrics::ConnectionSuccessWithAttemptsBreakdownMetricDimensionAttempts::*;
let attempts_dim = match connect_stats.attempts {
0 => {
warn!("unexpected 0 attempts in connect stats");
return;
}
1 => One,
2 => Two,
3 => Three,
4 => Four,
5 => Five,
_ => MoreThanFive,
};
let is_multi_bss_dim = convert_bool_dim(is_multi_bss);
let protection_dim = convert_protection(&bss.get_protection());
let channel_band_dim = convert_channel_band(bss.chan.primary);
sender.log_event_count(
metrics::CONNECTION_ATTEMPTS_METRIC_ID,
(), // no dimension
0,
connect_stats.attempts as i64,
);
sender.log_event_count(
metrics::CONNECTION_SUCCESS_WITH_ATTEMPTS_BREAKDOWN_METRIC_ID,
[
attempts_dim as u32,
is_multi_bss_dim as u32,
protection_dim as u32,
channel_band_dim as u32,
],
0,
1,
);
}
fn log_connect_result_stats(sender: &mut CobaltSender, connect_stats: &ConnectStats) {
let oui = connect_stats.candidate_network.as_ref().map(|bss| bss.bssid.to_oui_uppercase(""));
let result_dim = convert_connection_result(&connect_stats.result);
sender.with_component().log_event_count::<_, String, _>(
metrics::CONNECTION_RESULT_METRIC_ID,
result_dim as u32,
oui.clone(),
0,
1,
);
if let ConnectResult::Failed(failure) = &connect_stats.result {
let fail_at_dim = convert_to_fail_at_dim(failure);
let timeout_dim = convert_bool_dim(failure.is_timeout());
sender.with_component().log_event_count::<_, String, _>(
metrics::CONNECTION_FAILURE_METRIC_ID,
[fail_at_dim as u32, timeout_dim as u32],
oui.clone(),
0,
1,
);
if let ConnectFailure::SelectNetworkFailure(select_network_failure) = failure {
let error_reason_dim = convert_select_network_failure(&select_network_failure);
sender.with_component().log_event_count::<_, String, _>(
metrics::NETWORK_SELECTION_FAILURE_METRIC_ID,
error_reason_dim as u32,
oui,
0,
1,
);
}
}
// For the remaining metrics, we expect scan result and candidate network to have been found
let is_multi_bss = match &connect_stats.scan_end_stats {
Some(stats) => stats.bss_count > 1,
None => return,
};
let bss = match &connect_stats.candidate_network {
Some(bss) => bss,
None => return,
};
let oui = bss.bssid.to_oui_uppercase("");
let is_multi_bss_dim = convert_bool_dim(is_multi_bss);
let protection_dim = convert_protection(&bss.get_protection());
let channel_band_dim = convert_channel_band(bss.chan.primary);
let rssi_dim = convert_rssi(bss.rssi_dbm);
let snr_dim = convert_snr(bss.snr_db);
sender.with_component().log_event_count(
metrics::CONNECTION_RESULT_POST_NETWORK_SELECTION_METRIC_ID,
[
result_dim as u32,
is_multi_bss_dim as u32,
protection_dim as u32,
channel_band_dim as u32,
],
oui.clone(),
0,
1,
);
sender.with_component().log_event_count(
metrics::CONNECTION_RESULT_PER_RSSI_METRIC_ID,
[result_dim as u32, rssi_dim as u32],
oui.clone(),
0,
1,
);
sender.with_component().log_event_count(
metrics::CONNECTION_RESULT_PER_SNR_METRIC_ID,
[result_dim as u32, snr_dim as u32],
oui.clone(),
0,
1,
);
match &connect_stats.result {
ConnectResult::Failed(failure) => match failure {
ConnectFailure::AuthenticationFailure(code) => {
let error_code_dim = convert_auth_error_code(*code);
sender.with_component().log_event_count(
metrics::AUTHENTICATION_FAILURE_METRIC_ID,
[
error_code_dim as u32,
is_multi_bss_dim as u32,
channel_band_dim as u32,
protection_dim as u32,
],
oui.clone(),
0,
1,
);
sender.with_component().log_event_count(
metrics::AUTHENTICATION_FAILURE_PER_RSSI_METRIC_ID,
[error_code_dim as u32, rssi_dim as u32, channel_band_dim as u32],
oui,
0,
1,
);
}
ConnectFailure::AssociationFailure(AssociationFailure { code, .. }) => {
let error_code_dim = convert_assoc_error_code(*code);
sender.with_component().log_event_count(
metrics::ASSOCIATION_FAILURE_METRIC_ID,
[error_code_dim as u32, protection_dim as u32],
oui.clone(),
0,
1,
);
sender.with_component().log_event_count(
metrics::ASSOCIATION_FAILURE_PER_RSSI_METRIC_ID,
[error_code_dim as u32, rssi_dim as u32, channel_band_dim as u32],
oui,
0,
1,
);
}
ConnectFailure::EstablishRsnaFailure(..) => {
sender.with_component().log_event_count(
metrics::ESTABLISH_RSNA_FAILURE_METRIC_ID,
protection_dim as u32,
oui,
0,
1,
);
}
// Scan failure is already logged as part of scan stats.
// Select network failure is already logged above.
_ => (),
},
_ => (),
}
}
fn log_time_to_connect_stats(sender: &mut CobaltSender, connect_stats: &ConnectStats) {
let connect_result_dim = convert_connection_result(&connect_stats.result);
let rssi_dim = connect_stats.candidate_network.as_ref().map(|bss| convert_rssi(bss.rssi_dbm));
let connect_time = connect_stats.connect_time().into_micros();
sender.log_elapsed_time(metrics::CONNECTION_SETUP_TIME_METRIC_ID, (), connect_time);
sender.log_elapsed_time(
metrics::CONNECTION_SETUP_TIME_PER_RESULT_METRIC_ID,
connect_result_dim as u32,
connect_time,
);
if let Some(connect_time_without_scan) = connect_stats.connect_time_without_scan() {
sender.log_elapsed_time(
metrics::CONNECTION_SETUP_TIME_WITHOUT_SCAN_METRIC_ID,
(),
connect_time_without_scan.into_micros(),
);
sender.log_elapsed_time(
metrics::CONNECTION_SETUP_TIME_WITHOUT_SCAN_PER_RESULT_METRIC_ID,
connect_result_dim as u32,
connect_time_without_scan.into_micros(),
);
if let Some(rssi_dim) = rssi_dim {
sender.log_elapsed_time(
metrics::CONNECTION_SETUP_TIME_WITHOUT_SCAN_PER_RSSI_METRIC_ID,
rssi_dim as u32,
connect_time_without_scan.into_micros(),
)
}
}
if let Some(time) = connect_stats.connect_queued_time() {
sender.log_elapsed_time(metrics::CONNECTION_QUEUED_TIME_METRIC_ID, (), time.into_micros());
}
if let Some(auth_time) = connect_stats.auth_time() {
sender.log_elapsed_time(
metrics::AUTHENTICATION_TIME_METRIC_ID,
(),
auth_time.into_micros(),
);
if let Some(rssi_dim) = rssi_dim {
sender.log_elapsed_time(
metrics::AUTHENTICATION_TIME_PER_RSSI_METRIC_ID,
rssi_dim as u32,
auth_time.into_micros(),
)
}
}
if let Some(assoc_time) = connect_stats.assoc_time() {
sender.log_elapsed_time(metrics::ASSOCIATION_TIME_METRIC_ID, (), assoc_time.into_micros());
if let Some(rssi_dim) = rssi_dim {
sender.log_elapsed_time(
metrics::ASSOCIATION_TIME_PER_RSSI_METRIC_ID,
rssi_dim as u32,
assoc_time.into_micros(),
)
}
}
if let Some(rsna_time) = connect_stats.rsna_time() {
sender.log_elapsed_time(
metrics::ESTABLISH_RSNA_TIME_METRIC_ID,
(),
rsna_time.into_micros(),
);
if let Some(rssi_dim) = rssi_dim {
sender.log_elapsed_time(
metrics::ESTABLISH_RSNA_TIME_PER_RSSI_METRIC_ID,
rssi_dim as u32,
rsna_time.into_micros(),
)
}
}
}
/// If there was a reconnect, log connection gap time stats to Cobalt. Return the duration
/// and whether the reconnect was to the same SSID as last connected.
fn log_connection_gap_time_stats(
sender: &mut CobaltSender,
connect_stats: &ConnectStats,
) -> Option<ReconnectInfo> {
if connect_stats.result != ConnectResult::Success {
return None;
}
let ssid = match &connect_stats.candidate_network {
Some(bss) => &bss.ssid,
None => {
warn!("No candidate_network in successful connect stats");
return None;
}
};
let mut reconnect_info = None;
if let Some(previous_disconnect_info) = &connect_stats.previous_disconnect_info {
let duration = connect_stats.connect_end_at - previous_disconnect_info.disconnect_at;
let ssids_dim = if ssid == &previous_disconnect_info.ssid {
metrics::ConnectionGapTimeBreakdownMetricDimensionSsids::SameSsid
} else {
metrics::ConnectionGapTimeBreakdownMetricDimensionSsids::DifferentSsids
};
let previous_disconnect_cause_dim =
convert_disconnect_source(&previous_disconnect_info.disconnect_source);
sender.log_elapsed_time(metrics::CONNECTION_GAP_TIME_METRIC_ID, (), duration.into_micros());
sender.log_elapsed_time(
metrics::CONNECTION_GAP_TIME_BREAKDOWN_METRIC_ID,
[ssids_dim as u32, previous_disconnect_cause_dim as u32],
duration.into_micros(),
);
reconnect_info.replace(ReconnectInfo {
gap_time: duration,
same_ssid: ssid == &previous_disconnect_info.ssid,
});
}
reconnect_info
}
pub fn log_connection_ping(sender: &mut CobaltSender, info: &ConnectionPingInfo) {
for milestone in ConnectionMilestone::all().iter() {
if info.connected_duration() >= milestone.min_duration() {
let dur_dim = convert_connected_milestone(milestone);
sender.log_event(metrics::CONNECTION_UPTIME_PING_METRIC_ID, dur_dim as u32);
}
}
if info.reaches_new_milestone() {
let dur_dim = convert_connected_milestone(&info.get_milestone());
sender.log_event_count(
metrics::CONNECTION_COUNT_BY_DURATION_METRIC_ID,
dur_dim as u32,
0,
1,
);
}
}
pub fn log_disconnect(
sender: &mut CobaltSender,
inspect_tree: Arc<inspect::WlanstackTree>,
info: &DisconnectInfo,
) {
inspect_log!(inspect_tree.client_stats.disconnect.lock(), {
connected_duration: info.connected_duration.into_nanos(),
last_rssi: info.last_rssi,
last_snr: info.last_snr,
bssid: info.bssid.to_mac_str(),
bssid_hash: inspect_tree.hasher.hash_mac_addr(info.bssid),
ssid: String::from_utf8_lossy(&info.ssid[..]).to_string(),
ssid_hash: inspect_tree.hasher.hash(&info.ssid[..]),
channel: {
primary: info.channel.primary,
cbw: format!("{:?}", info.channel.cbw.to_fidl().0),
secondary80: info.channel.cbw.to_fidl().1,
},
reason_code: info.reason_code,
disconnect_source: match info.disconnect_source {
DisconnectSource::User => "user",
DisconnectSource::Mlme => "mlme",
DisconnectSource::Ap => "ap",
},
time_since_channel_switch?: info.time_since_channel_switch.map(|d| d.into_nanos()),
});
if let DisconnectSource::Mlme = info.disconnect_source {
use metrics::LostConnectionCountMetricDimensionConnectedTime::*;
let duration_dim = match &info.connected_duration {
x if x < &1.minutes() => LessThanOneMinute,
x if x < &10.minutes() => LessThanTenMinutes,
x if x < &30.minutes() => LessThanThirtyMinutes,
x if x < &1.hour() => LessThanOneHour,
x if x < &3.hours() => LessThanThreeHours,
x if x < &6.hours() => LessThanSixHours,
_ => AtLeastSixHours,
};
let rssi_dim = convert_rssi(info.last_rssi);
sender.with_component().log_event_count(
metrics::LOST_CONNECTION_COUNT_METRIC_ID,
[duration_dim as u32, rssi_dim as u32],
info.bssid.to_oui_uppercase(""),
0,
1,
);
}
use metrics::DisconnectCountBreakdownMetricDimensionConnectedTime::*;
use metrics::DisconnectCountBreakdownMetricDimensionDisconnectSource::*;
use metrics::DisconnectCountBreakdownMetricDimensionRecentChannelSwitch::*;
let connected_time_dim = match &info.connected_duration {
x if x < &1.minutes() => LessThanOneMinute,
x if x < &10.minutes() => LessThanTenMinutes,
x if x < &30.minutes() => LessThanThirtyMinutes,
x if x < &1.hour() => LessThanOneHour,
x if x < &3.hours() => LessThanThreeHours,
x if x < &6.hours() => LessThanSixHours,
_ => AtLeastSixHours,
};
let disconnect_source_dim = match &info.disconnect_source {
DisconnectSource::User => User,
DisconnectSource::Mlme => Mlme,
DisconnectSource::Ap => Ap,
};
let snr_dim = convert_snr(info.last_snr);
let recent_channel_switch_dim = match info.time_since_channel_switch.map(|d| d < 1.minutes()) {
Some(true) => Yes,
_ => No,
};
let channel_band_dim = convert_channel_band(info.channel.primary);
sender.log_event_count(metrics::DISCONNECT_COUNT_METRIC_ID, (), 0, 1);
sender.with_component().log_event_count(
metrics::DISCONNECT_COUNT_BREAKDOWN_METRIC_ID,
[
connected_time_dim as u32,
disconnect_source_dim as u32,
snr_dim as u32,
recent_channel_switch_dim as u32,
channel_band_dim as u32,
],
info.bssid.to_oui_uppercase(""),
0,
1,
);
}
#[cfg(test)]
mod tests {
use {
super::*,
crate::{
device::{self, IfaceDevice},
mlme_query_proxy::MlmeQueryProxy,
stats_scheduler::{self, StatsRequest},
},
fidl::endpoints::create_proxy,
fidl_fuchsia_cobalt::{CobaltEvent, EventPayload},
fidl_fuchsia_wlan_mlme::{self as fidl_mlme, MlmeMarker},
fidl_fuchsia_wlan_stats::{Counter, DispatcherStats, IfaceStats, PacketCounter},
fuchsia_inspect::{assert_inspect_tree, testing::AnyProperty, Inspector},
fuchsia_zircon as zx,
futures::channel::mpsc,
maplit::hashset,
pin_utils::pin_mut,
std::collections::HashSet,
wlan_common::{
assert_variant,
bss::Protection as BssProtection,
channel::{Cbw, Channel},
fake_bss,
},
wlan_sme::client::{
info::{
ConnectStats, DisconnectInfo, DisconnectSource, PreviousDisconnectInfo,
ScanEndStats, ScanResult, ScanStartStats, SupplicantProgress,
},
ConnectFailure, ConnectResult, EstablishRsnaFailure, EstablishRsnaFailureReason,
SelectNetworkFailure,
},
};
const IFACE_ID: u16 = 1;
const DURATION_SINCE_LAST_DISCONNECT: zx::Duration = zx::Duration::from_seconds(10);
#[test]
fn test_report_telemetry_periodically() {
let mut exec = fasync::Executor::new().expect("Failed to create an executor");
let (ifaces_map, stats_requests) = fake_iface_map();
let (cobalt_sender, mut cobalt_receiver) = fake_cobalt_sender();
let telemetry_fut = report_telemetry_periodically(Arc::new(ifaces_map), cobalt_sender);
pin_mut!(telemetry_fut);
// Schedule the first stats request
let _ = exec.run_until_stalled(&mut telemetry_fut);
assert!(exec.wake_next_timer().is_some());
let _ = exec.run_until_stalled(&mut telemetry_fut);
// Provide stats response
let mut nth_req = 0;
let mut stats_server = stats_requests.for_each(move |req| {
nth_req += 1;
future::ready(req.reply(fake_iface_stats(nth_req)))
});
let _ = exec.run_until_stalled(&mut stats_server);
// TODO(fxbug.dev/29730): For some reason, telemetry skips logging the first stats response
// Schedule the next stats request
let _ = exec.run_until_stalled(&mut telemetry_fut);
assert!(exec.wake_next_timer().is_some());
let _ = exec.run_until_stalled(&mut telemetry_fut);
// Provide stats response
let _ = exec.run_until_stalled(&mut stats_server);
// Verify that stats are sent to Cobalt
let _ = exec.run_until_stalled(&mut telemetry_fut);
let mut expected_metrics = vec![
CobaltEvent {
metric_id: metrics::CLIENT_ASSOC_RSSI_METRIC_ID,
event_codes: vec![],
component: None,
payload: EventPayload::IntHistogram(vec![HistogramBucket { index: 128, count: 1 }]),
},
CobaltEvent {
metric_id: metrics::CLIENT_BEACON_RSSI_METRIC_ID,
event_codes: vec![],
component: None,
payload: EventPayload::IntHistogram(vec![HistogramBucket { index: 128, count: 1 }]),
},
];
while let Ok(Some(event)) = cobalt_receiver.try_next() {
let index = expected_metrics.iter().position(|e| *e == event);
assert!(index.is_some(), "unexpected event: {:?}", event);
expected_metrics.remove(index.unwrap());
}
assert!(expected_metrics.is_empty(), "some metrics not logged: {:?}", expected_metrics);
}
#[test]
fn test_log_connect_stats_success() {
let (mut cobalt_sender, mut cobalt_receiver) = fake_cobalt_sender();
let inspect_tree = fake_inspect_tree();
log_connect_stats(&mut cobalt_sender, inspect_tree.clone(), &fake_connect_stats());
let mut expected_metrics = hashset! {
metrics::CONNECTION_ATTEMPTS_METRIC_ID,
metrics::CONNECTION_SUCCESS_WITH_ATTEMPTS_BREAKDOWN_METRIC_ID,
metrics::CONNECTION_RESULT_METRIC_ID,
metrics::CONNECTION_RESULT_POST_NETWORK_SELECTION_METRIC_ID,
metrics::CONNECTION_RESULT_PER_RSSI_METRIC_ID,
metrics::CONNECTION_RESULT_PER_SNR_METRIC_ID,
metrics::SCAN_RESULT_METRIC_ID,
metrics::CONNECTION_SETUP_TIME_METRIC_ID,
metrics::CONNECTION_SETUP_TIME_PER_RESULT_METRIC_ID,
metrics::CONNECTION_SETUP_TIME_WITHOUT_SCAN_METRIC_ID,
metrics::CONNECTION_SETUP_TIME_WITHOUT_SCAN_PER_RESULT_METRIC_ID,
metrics::CONNECTION_SETUP_TIME_WITHOUT_SCAN_PER_RSSI_METRIC_ID,
metrics::SCAN_TIME_METRIC_ID,
metrics::SCAN_TIME_PER_RESULT_METRIC_ID,
metrics::SCAN_TIME_PER_SCAN_TYPE_METRIC_ID,
metrics::SCAN_TIME_PER_JOIN_OR_DISCOVERY_METRIC_ID,
metrics::SCAN_TIME_PER_CLIENT_STATE_METRIC_ID,
metrics::AUTHENTICATION_TIME_METRIC_ID,
metrics::AUTHENTICATION_TIME_PER_RSSI_METRIC_ID,
metrics::ASSOCIATION_TIME_METRIC_ID,
metrics::ASSOCIATION_TIME_PER_RSSI_METRIC_ID,
metrics::ESTABLISH_RSNA_TIME_METRIC_ID,
metrics::ESTABLISH_RSNA_TIME_PER_RSSI_METRIC_ID,
metrics::CONNECTION_QUEUED_TIME_METRIC_ID,
metrics::CONNECTION_GAP_TIME_METRIC_ID,
metrics::CONNECTION_GAP_TIME_BREAKDOWN_METRIC_ID,
};
while let Ok(Some(event)) = cobalt_receiver.try_next() {
assert!(expected_metrics.contains(&event.metric_id), "unexpected event: {:?}", event);
expected_metrics.remove(&event.metric_id);
}
assert!(expected_metrics.is_empty(), "some metrics not logged: {:?}", expected_metrics);
}
#[test]
fn test_log_connect_stats_scan_failure() {
// Note: This mock is not completely correct (e.g. we would not expect time stats for
// later steps to be filled out if connect fails at scan), but for our testing
// purpose, it's sufficient. The same applies for other connect stats failure
// test cases.
let connect_stats = ConnectStats {
result: ConnectFailure::ScanFailure(fidl_mlme::ScanResultCodes::InvalidArgs).into(),
scan_end_stats: Some(ScanEndStats {
scan_end_at: now(),
result: ScanResult::Failed(fidl_mlme::ScanResultCodes::InvalidArgs),
bss_count: 1,
}),
..fake_connect_stats()
};
let expected_metrics_subset = hashset! {
metrics::CONNECTION_RESULT_METRIC_ID,
metrics::CONNECTION_FAILURE_METRIC_ID,
metrics::SCAN_RESULT_METRIC_ID,
metrics::SCAN_FAILURE_METRIC_ID,
};
// These metrics are only logged when connection attempt succeeded.
let unexpected_metrics = hashset! {
metrics::CONNECTION_ATTEMPTS_METRIC_ID,
metrics::CONNECTION_SUCCESS_WITH_ATTEMPTS_BREAKDOWN_METRIC_ID,
};
test_metric_subset(&connect_stats, expected_metrics_subset, unexpected_metrics);
}
#[test]
fn test_log_connect_stats_select_network_failure() {
let connect_stats = ConnectStats {
result: SelectNetworkFailure::NoCompatibleNetwork.into(),
..fake_connect_stats()
};
let expected_metrics_subset = hashset! {
metrics::CONNECTION_RESULT_METRIC_ID,
metrics::CONNECTION_FAILURE_METRIC_ID,
metrics::NETWORK_SELECTION_FAILURE_METRIC_ID,
};
test_metric_subset(&connect_stats, expected_metrics_subset, hashset! {});
}
#[test]
fn test_log_connect_stats_auth_failure() {
let connect_stats = ConnectStats {
result: ConnectFailure::AuthenticationFailure(
fidl_mlme::AuthenticateResultCodes::Refused,
)
.into(),
..fake_connect_stats()
};
let expected_metrics_subset = hashset! {
metrics::CONNECTION_RESULT_METRIC_ID,
metrics::CONNECTION_FAILURE_METRIC_ID,
metrics::AUTHENTICATION_FAILURE_METRIC_ID,
};
test_metric_subset(&connect_stats, expected_metrics_subset, hashset! {});
}
#[test]
fn test_log_connect_stats_assoc_failure() {
let connect_stats = ConnectStats {
result: ConnectFailure::AssociationFailure(AssociationFailure {
bss_protection: BssProtection::Open,
code: fidl_mlme::AssociateResultCodes::RefusedReasonUnspecified,
})
.into(),
..fake_connect_stats()
};
let expected_metrics_subset = hashset! {
metrics::CONNECTION_RESULT_METRIC_ID,
metrics::CONNECTION_FAILURE_METRIC_ID,
metrics::ASSOCIATION_FAILURE_METRIC_ID,
};
test_metric_subset(&connect_stats, expected_metrics_subset, hashset! {});
}
#[test]
fn test_log_connect_stats_establish_rsna_failure() {
let connect_stats = ConnectStats {
result: EstablishRsnaFailure {
auth_method: None,
reason: EstablishRsnaFailureReason::OverallTimeout,
}
.into(),
..fake_connect_stats()
};
let expected_metrics_subset = hashset! {
metrics::CONNECTION_RESULT_METRIC_ID,
metrics::CONNECTION_FAILURE_METRIC_ID,
metrics::ESTABLISH_RSNA_FAILURE_METRIC_ID,
};
test_metric_subset(&connect_stats, expected_metrics_subset, hashset! {});
}
#[test]
fn test_log_connection_ping() {
use metrics::ConnectionCountByDurationMetricDimensionConnectedTime::*;
let (mut cobalt_sender, mut cobalt_receiver) = fake_cobalt_sender();
let start = now();
let ping = ConnectionPingInfo::first_connected(start);
log_connection_ping(&mut cobalt_sender, &ping);
assert_ping_metrics(&mut cobalt_receiver, &[Connected as u32]);
assert_variant!(cobalt_receiver.try_next(), Ok(Some(event)) => {
assert_eq!(event.metric_id, metrics::CONNECTION_COUNT_BY_DURATION_METRIC_ID);
assert_eq!(event.event_codes, vec![Connected as u32])
});
let ping = ping.next_ping(start + 1.minute());
log_connection_ping(&mut cobalt_sender, &ping);
assert_ping_metrics(&mut cobalt_receiver, &[Connected as u32, ConnectedOneMinute as u32]);
assert_variant!(cobalt_receiver.try_next(), Ok(Some(event)) => {
assert_eq!(event.metric_id, metrics::CONNECTION_COUNT_BY_DURATION_METRIC_ID);
assert_eq!(event.event_codes, vec![ConnectedOneMinute as u32])
});
let ping = ping.next_ping(start + 3.minutes());
log_connection_ping(&mut cobalt_sender, &ping);
assert_ping_metrics(&mut cobalt_receiver, &[Connected as u32, ConnectedOneMinute as u32]);
// check that CONNECTION_COUNT_BY_DURATION isn't logged since new milestone is not reached
assert_variant!(cobalt_receiver.try_next(), Ok(None) | Err(..));
let ping = ping.next_ping(start + 10.minutes());
log_connection_ping(&mut cobalt_sender, &ping);
assert_ping_metrics(
&mut cobalt_receiver,
&[Connected as u32, ConnectedOneMinute as u32, ConnectedTenMinute as u32],
);
assert_variant!(cobalt_receiver.try_next(), Ok(Some(event)) => {
assert_eq!(event.metric_id, metrics::CONNECTION_COUNT_BY_DURATION_METRIC_ID);
assert_eq!(event.event_codes, vec![ConnectedTenMinute as u32])
});
}
fn assert_ping_metrics(cobalt_receiver: &mut mpsc::Receiver<CobaltEvent>, milestones: &[u32]) {
for milestone in milestones {
assert_variant!(cobalt_receiver.try_next(), Ok(Some(event)) => {
assert_eq!(event.metric_id, metrics::CONNECTION_UPTIME_PING_METRIC_ID);
assert_eq!(event.event_codes, vec![*milestone]);
});
}
}
#[test]
fn test_log_disconnect_initiated_from_mlme() {
let (mut cobalt_sender, mut cobalt_receiver) = fake_cobalt_sender();
let inspect_tree = fake_inspect_tree();
let disconnect_info =
DisconnectInfo { disconnect_source: DisconnectSource::Mlme, ..fake_disconnect_info() };
log_disconnect(&mut cobalt_sender, inspect_tree.clone(), &disconnect_info);
assert_variant!(cobalt_receiver.try_next(), Ok(Some(event)) => {
assert_eq!(event.metric_id, metrics::LOST_CONNECTION_COUNT_METRIC_ID);
});
assert_variant!(cobalt_receiver.try_next(), Ok(Some(event)) => {
assert_eq!(event.metric_id, metrics::DISCONNECT_COUNT_METRIC_ID);
});
assert_variant!(cobalt_receiver.try_next(), Ok(Some(event)) => {
assert_eq!(event.metric_id, metrics::DISCONNECT_COUNT_BREAKDOWN_METRIC_ID);
assert_eq!(event.event_codes, vec![
metrics::DisconnectCountBreakdownMetricDimensionConnectedTime::LessThanOneMinute as u32,
metrics::DisconnectCountBreakdownMetricDimensionDisconnectSource::Mlme as u32,
metrics::DisconnectCountBreakdownMetricDimensionSnr::From1To10 as u32,
metrics::DisconnectCountBreakdownMetricDimensionRecentChannelSwitch::No as u32,
metrics::DisconnectCountBreakdownMetricDimensionChannelBand::Band2Dot4Ghz as u32
]);
});
}
#[test]
fn test_log_disconnect_initiated_from_user_request() {
let (mut cobalt_sender, mut cobalt_receiver) = fake_cobalt_sender();
let inspect_tree = fake_inspect_tree();
let disconnect_info =
DisconnectInfo { disconnect_source: DisconnectSource::User, ..fake_disconnect_info() };
log_disconnect(&mut cobalt_sender, inspect_tree.clone(), &disconnect_info);
assert_variant!(cobalt_receiver.try_next(), Ok(Some(event)) => {
assert_eq!(event.metric_id, metrics::DISCONNECT_COUNT_METRIC_ID);
});
assert_variant!(cobalt_receiver.try_next(), Ok(Some(event)) => {
assert_eq!(event.metric_id, metrics::DISCONNECT_COUNT_BREAKDOWN_METRIC_ID);
});
}
#[test]
fn test_log_disconnect_initiated_from_ap() {
let (mut cobalt_sender, mut cobalt_receiver) = fake_cobalt_sender();
let inspect_tree = fake_inspect_tree();
let disconnect_info =
DisconnectInfo { disconnect_source: DisconnectSource::Ap, ..fake_disconnect_info() };
log_disconnect(&mut cobalt_sender, inspect_tree.clone(), &disconnect_info);
assert_variant!(cobalt_receiver.try_next(), Ok(Some(event)) => {
assert_eq!(event.metric_id, metrics::DISCONNECT_COUNT_METRIC_ID);
});
assert_variant!(cobalt_receiver.try_next(), Ok(Some(event)) => {
assert_eq!(event.metric_id, metrics::DISCONNECT_COUNT_BREAKDOWN_METRIC_ID);
});
}
#[test]
fn test_inspect_log_connect_stats() {
let (mut cobalt_sender, _cobalt_receiver) = fake_cobalt_sender();
let inspect_tree = fake_inspect_tree();
let connect_stats = fake_connect_stats();
log_connect_stats(&mut cobalt_sender, inspect_tree.clone(), &connect_stats);
assert_inspect_tree!(inspect_tree.inspector, root: contains {
client_stats: contains {
connect: {
"0": {
"@time": AnyProperty,
attempts: 1u64,
reconnect_info: {
gap_time: DURATION_SINCE_LAST_DISCONNECT.into_nanos(),
same_ssid: true,
},
}
}
}
});
}
#[test]
fn test_inspect_log_disconnect_stats() {
let (mut cobalt_sender, _cobalt_receiver) = fake_cobalt_sender();
let inspect_tree = fake_inspect_tree();
let disconnect_info = DisconnectInfo {
connected_duration: 30.seconds(),
bssid: [1u8; 6],
ssid: b"foo".to_vec(),
channel: Channel { primary: 1, cbw: Cbw::Cbw20 },
last_rssi: -90,
last_snr: 1,
reason_code: fidl_mlme::ReasonCode::UnspecifiedReason.into_primitive(),
disconnect_source: DisconnectSource::Mlme,
time_since_channel_switch: Some(zx::Duration::from_nanos(1337i64)),
};
log_disconnect(&mut cobalt_sender, inspect_tree.clone(), &disconnect_info);
assert_inspect_tree!(inspect_tree.inspector, root: contains {
client_stats: contains {
disconnect: {
"0": {
"@time": AnyProperty,
connected_duration: 30.seconds().into_nanos(),
bssid: "01:01:01:01:01:01",
bssid_hash: AnyProperty,
ssid: "foo",
ssid_hash: AnyProperty,
channel: {
primary: 1u64,
cbw: "Cbw20",
secondary80: 0u64,
},
last_rssi: -90i64,
last_snr: 1i64,
reason_code: 1u64,
disconnect_source: "mlme",
time_since_channel_switch: 1337i64,
}
}
}
});
}
#[test]
fn test_inspect_log_scan_failures() {
let (mut cobalt_sender, _cobalt_receiver) = fake_cobalt_sender();
let inspect_tree = fake_inspect_tree();
let now = now();
let scan_stats = ScanStats {
scan_start_at: now - 3.seconds(),
scan_end_at: now,
scan_type: fidl_mlme::ScanTypes::Active,
scan_start_while_connected: true,
result: ScanResult::Failed(fidl_mlme::ScanResultCodes::InvalidArgs),
bss_count: 5,
};
log_scan_stats(&mut cobalt_sender, inspect_tree.clone(), &scan_stats, true);
assert_inspect_tree!(inspect_tree.inspector, root: contains {
client_stats: contains {
scan_failures: {
"0": {
"@time": AnyProperty,
}
}
}
});
}
fn test_metric_subset(
connect_stats: &ConnectStats,
mut expected_metrics_subset: HashSet<u32>,
unexpected_metrics: HashSet<u32>,
) {
let (mut cobalt_sender, mut cobalt_receiver) = fake_cobalt_sender();
let inspect_tree = fake_inspect_tree();
log_connect_stats(&mut cobalt_sender, inspect_tree.clone(), connect_stats);
while let Ok(Some(event)) = cobalt_receiver.try_next() {
assert!(
!unexpected_metrics.contains(&event.metric_id),
"unexpected event: {:?}",
event
);
if expected_metrics_subset.contains(&event.metric_id) {
expected_metrics_subset.remove(&event.metric_id);
}
}
assert!(
expected_metrics_subset.is_empty(),
"some metrics not logged: {:?}",
expected_metrics_subset
);
}
fn now() -> zx::Time {
zx::Time::get_monotonic()
}
fn fake_connect_stats() -> ConnectStats {
let now = now();
ConnectStats {
connect_start_at: now,
connect_end_at: now,
scan_start_stats: Some(ScanStartStats {
scan_start_at: now,
scan_type: fidl_mlme::ScanTypes::Passive,
scan_start_while_connected: false,
}),
scan_end_stats: Some(ScanEndStats {
scan_end_at: now,
result: ScanResult::Success,
bss_count: 1,
}),
auth_start_at: Some(now),
auth_end_at: Some(now),
assoc_start_at: Some(now),
assoc_end_at: Some(now),
rsna_start_at: Some(now),
rsna_end_at: Some(now),
supplicant_error: None,
supplicant_progress: Some(SupplicantProgress {
pmksa_established: true,
ptksa_established: true,
gtksa_established: true,
esssa_established: true,
}),
num_rsna_key_frame_exchange_timeout: 0,
result: ConnectResult::Success,
candidate_network: Some(fake_bss!(Open)),
attempts: 1,
last_ten_failures: vec![],
previous_disconnect_info: Some(PreviousDisconnectInfo {
ssid: fake_bss!(Open).ssid,
disconnect_source: DisconnectSource::User,
disconnect_at: now - DURATION_SINCE_LAST_DISCONNECT,
}),
}
}
fn fake_iface_stats(nth_req: u64) -> IfaceStats {
IfaceStats {
dispatcher_stats: DispatcherStats {
any_packet: fake_packet_counter(nth_req),
mgmt_frame: fake_packet_counter(nth_req),
ctrl_frame: fake_packet_counter(nth_req),
data_frame: fake_packet_counter(nth_req),
},
mlme_stats: Some(Box::new(ClientMlmeStats(fidl_stats::ClientMlmeStats {
svc_msg: fake_packet_counter(nth_req),
data_frame: fake_packet_counter(nth_req),
mgmt_frame: fake_packet_counter(nth_req),
tx_frame: fake_packet_counter(nth_req),
rx_frame: fake_packet_counter(nth_req),
assoc_data_rssi: fake_rssi(nth_req),
beacon_rssi: fake_rssi(nth_req),
noise_floor_histograms: fake_noise_floor_histograms(),
rssi_histograms: fake_rssi_histograms(),
rx_rate_index_histograms: fake_rx_rate_index_histograms(),
snr_histograms: fake_snr_histograms(),
}))),
}
}
fn fake_packet_counter(nth_req: u64) -> PacketCounter {
PacketCounter {
in_: Counter { count: 1 * nth_req, name: "in".to_string() },
out: Counter { count: 2 * nth_req, name: "out".to_string() },
drop: Counter { count: 3 * nth_req, name: "drop".to_string() },
in_bytes: Counter { count: 4 * nth_req, name: "in_bytes".to_string() },
out_bytes: Counter { count: 5 * nth_req, name: "out_bytes".to_string() },
drop_bytes: Counter { count: 6 * nth_req, name: "drop_bytes".to_string() },
}
}
fn fake_rssi(nth_req: u64) -> fidl_stats::RssiStats {
fidl_stats::RssiStats { hist: vec![nth_req] }
}
fn fake_antenna_id() -> Option<Box<fidl_stats::AntennaId>> {
Some(Box::new(fidl_stats::AntennaId { freq: fidl_stats::AntennaFreq::Antenna5G, index: 0 }))
}
fn fake_noise_floor_histograms() -> Vec<fidl_stats::NoiseFloorHistogram> {
vec![fidl_stats::NoiseFloorHistogram {
hist_scope: fidl_stats::HistScope::PerAntenna,
antenna_id: fake_antenna_id(),
// Noise floor bucket_index 165 indicates -90 dBm.
noise_floor_samples: vec![fidl_stats::HistBucket {
bucket_index: 165,
num_samples: 10,
}],
invalid_samples: 0,
}]
}
fn fake_rssi_histograms() -> Vec<fidl_stats::RssiHistogram> {
vec![fidl_stats::RssiHistogram {
hist_scope: fidl_stats::HistScope::PerAntenna,
antenna_id: fake_antenna_id(),
// RSSI bucket_index 225 indicates -30 dBm.
rssi_samples: vec![fidl_stats::HistBucket { bucket_index: 225, num_samples: 10 }],
invalid_samples: 0,
}]
}
fn fake_rx_rate_index_histograms() -> Vec<fidl_stats::RxRateIndexHistogram> {
vec![fidl_stats::RxRateIndexHistogram {
hist_scope: fidl_stats::HistScope::PerAntenna,
antenna_id: fake_antenna_id(),
// Rate bucket_index 74 indicates HT BW40 MCS 14 SGI, which is 802.11n 270 Mb/s.
// Rate bucket_index 75 indicates HT BW40 MCS 15 SGI, which is 802.11n 300 Mb/s.
rx_rate_index_samples: vec![
fidl_stats::HistBucket { bucket_index: 74, num_samples: 5 },
fidl_stats::HistBucket { bucket_index: 75, num_samples: 5 },
],
invalid_samples: 0,
}]
}
fn fake_snr_histograms() -> Vec<fidl_stats::SnrHistogram> {
vec![fidl_stats::SnrHistogram {
hist_scope: fidl_stats::HistScope::PerAntenna,
antenna_id: fake_antenna_id(),
// Signal to noise ratio bucket_index 60 indicates 60 dB.
snr_samples: vec![fidl_stats::HistBucket { bucket_index: 60, num_samples: 10 }],
invalid_samples: 0,
}]
}
fn fake_disconnect_info() -> DisconnectInfo {
DisconnectInfo {
connected_duration: 30.seconds(),
bssid: [1u8; 6],
ssid: b"foo".to_vec(),
channel: Channel { primary: 1, cbw: Cbw::Cbw20 },
last_rssi: -90,
last_snr: 1,
reason_code: fidl_mlme::ReasonCode::UnspecifiedReason.into_primitive(),
disconnect_source: DisconnectSource::Mlme,
time_since_channel_switch: None,
}
}
fn fake_iface_map() -> (IfaceMap, impl Stream<Item = StatsRequest>) {
let (ifaces_map, _watcher) = IfaceMap::new();
let (iface_device, stats_requests) = fake_iface_device();
ifaces_map.insert(IFACE_ID, iface_device);
(ifaces_map, stats_requests)
}
fn fake_iface_device() -> (IfaceDevice, impl Stream<Item = StatsRequest>) {
let (sme_sender, _sme_receiver) = mpsc::unbounded();
let (stats_sched, stats_requests) = stats_scheduler::create_scheduler();
let (proxy, _server) = create_proxy::<MlmeMarker>().expect("Error creating proxy");
let mlme_query = MlmeQueryProxy::new(proxy);
let (shutdown_sender, _) = mpsc::channel(1);
let device_info = fake_device_info();
let iface_device = IfaceDevice {
phy_ownership: device::PhyOwnership { phy_id: 0, phy_assigned_id: 0 },
sme_server: device::SmeServer::Client(sme_sender),
stats_sched,
mlme_query,
device_info,
shutdown_sender,
};
(iface_device, stats_requests)
}
fn fake_device_info() -> fidl_mlme::DeviceInfo {
fidl_mlme::DeviceInfo {
role: fidl_mlme::MacRole::Client,
bands: vec![],
mac_addr: [0xAC; 6],
driver_features: vec![],
qos_capable: false,
}
}
fn fake_cobalt_sender() -> (CobaltSender, mpsc::Receiver<CobaltEvent>) {
const BUFFER_SIZE: usize = 100;
let (sender, receiver) = mpsc::channel(BUFFER_SIZE);
(CobaltSender::new(sender), receiver)
}
fn fake_inspect_tree() -> Arc<inspect::WlanstackTree> {
let inspector = Inspector::new();
Arc::new(inspect::WlanstackTree::new(inspector))
}
}