Skip to main content

wlan_sme/client/
scan.rs

1// Copyright 2021 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use crate::client::inspect;
6use crate::responder::Responder;
7use crate::{Error, MlmeRequest, MlmeSink};
8use fidl_fuchsia_wlan_common as fidl_common;
9use fidl_fuchsia_wlan_ieee80211 as fidl_ieee80211;
10use fidl_fuchsia_wlan_mlme as fidl_mlme;
11use fidl_fuchsia_wlan_sme as fidl_sme;
12use fuchsia_inspect::NumericProperty;
13use futures::channel::mpsc;
14use ieee80211::{Bssid, Ssid};
15use log::warn;
16use std::collections::{HashMap, HashSet, hash_map};
17use std::mem;
18use std::sync::{Arc, LazyLock};
19use wlan_common::bss::BssDescription;
20use wlan_common::channel::{Bandwidth, Channel};
21use wlan_common::ie::IesMerger;
22
23type ScanTxnId = u64;
24
25const PASSIVE_SCAN_CHANNEL_MS: u32 = 200;
26const ACTIVE_SCAN_PROBE_DELAY_MS: u32 = 5;
27const ACTIVE_SCAN_CHANNEL_MS: u32 = 75;
28
29// A "user"-initiated scan request for the purpose of discovering available networks
30#[derive(Debug, PartialEq)]
31pub struct DiscoveryScan<T> {
32    tokens: Vec<T>,
33    scan_request: fidl_sme::ScanRequest,
34}
35
36impl<T> DiscoveryScan<T> {
37    pub fn new(token: T, scan_request: fidl_sme::ScanRequest) -> Self {
38        Self { tokens: vec![token], scan_request }
39    }
40
41    pub fn matches(&self, scan: &DiscoveryScan<T>) -> bool {
42        self.scan_request == scan.scan_request
43    }
44
45    pub fn merges(&mut self, mut scan: DiscoveryScan<T>) {
46        self.tokens.append(&mut scan.tokens)
47    }
48}
49/// Client end of a scheduled scan session.
50pub struct ScheduledScanReceiver {
51    scan_results_receiver: mpsc::UnboundedReceiver<fidl::Vmo>,
52    pub(crate) txn_id: ScanTxnId,
53    mlme_sink: MlmeSink,
54    stopped_by_firmware: bool,
55}
56impl ScheduledScanReceiver {
57    fn new(
58        scan_results_receiver: mpsc::UnboundedReceiver<fidl::Vmo>,
59        txn_id: ScanTxnId,
60        mlme_sink: MlmeSink,
61    ) -> Self {
62        Self { scan_results_receiver, txn_id, mlme_sink, stopped_by_firmware: false }
63    }
64}
65impl futures::stream::Stream for ScheduledScanReceiver {
66    type Item = fidl::Vmo;
67
68    fn poll_next(
69        mut self: std::pin::Pin<&mut Self>,
70        cx: &mut std::task::Context<'_>,
71    ) -> std::task::Poll<Option<Self::Item>> {
72        let poll_result = std::pin::Pin::new(&mut self.scan_results_receiver).poll_next(cx);
73        if let std::task::Poll::Ready(None) = poll_result {
74            self.stopped_by_firmware = true;
75        }
76        poll_result
77    }
78}
79impl Drop for ScheduledScanReceiver {
80    fn drop(&mut self) {
81        // If the firmware already stopped the scheduled scan, we do not need to send a stop
82        // command. Sending it anyway could result in MLME returning ZX_ERR_NOT_FOUND and
83        // logging errors.
84        if !self.stopped_by_firmware {
85            let mlme_req = fidl_mlme::MlmeStopScheduledScanRequest { txn_id: self.txn_id };
86            let (responder, _) = Responder::new();
87            self.mlme_sink.send(MlmeRequest::StopScheduledScan(mlme_req, responder));
88        }
89    }
90}
91
92/// Represents the internal state of an active scheduled scan. Used to accumulate streamed scheduled
93/// scan results and send them via VMO when ready.
94pub(crate) struct ScheduledScanState {
95    scan_results_sender: mpsc::UnboundedSender<fidl::Vmo>,
96    bss_map: std::collections::HashMap<
97        Bssid,
98        (fidl_ieee80211::BssDescription, wlan_common::ie::IesMerger),
99    >,
100}
101impl ScheduledScanState {
102    fn new(scan_results_sender: mpsc::UnboundedSender<fidl::Vmo>) -> Self {
103        Self { scan_results_sender, bss_map: HashMap::new() }
104    }
105}
106pub struct ScanScheduler<T> {
107    // The currently running scan. We assume that MLME can handle a single concurrent scan
108    // regardless of its own state.
109    current: ScanState<T>,
110    // Pending discovery requests from the user
111    pending_discovery: Vec<DiscoveryScan<T>>,
112    device_info: Arc<fidl_mlme::DeviceInfo>,
113    spectrum_management_support: fidl_common::SpectrumManagementSupport,
114    // Map of active scheduled scan transaction IDs to their internal states.
115    pub(crate) scheduled_scan_receivers: HashMap<ScanTxnId, ScheduledScanState>,
116    last_mlme_txn_id: ScanTxnId,
117}
118
119#[derive(Debug)]
120enum ScanState<T> {
121    NotScanning,
122    ScanningToDiscover {
123        cmd: DiscoveryScan<T>,
124        mlme_txn_id: ScanTxnId,
125        bss_map: HashMap<Bssid, (fidl_ieee80211::BssDescription, IesMerger)>,
126    },
127}
128
129#[derive(Debug)]
130pub struct ScanEnd<T> {
131    pub tokens: Vec<T>,
132    pub result_code: fidl_mlme::ScanResultCode,
133    pub bss_description_list: Vec<BssDescription>,
134}
135
136impl<T> ScanScheduler<T> {
137    pub fn new(
138        device_info: Arc<fidl_mlme::DeviceInfo>,
139        spectrum_management_support: fidl_common::SpectrumManagementSupport,
140    ) -> Self {
141        ScanScheduler {
142            current: ScanState::NotScanning,
143            pending_discovery: Vec::new(),
144            device_info,
145            spectrum_management_support,
146            scheduled_scan_receivers: HashMap::new(),
147            last_mlme_txn_id: 0,
148        }
149    }
150
151    // Initiate a "discovery" scan. The scan might or might not begin immediately.
152    // The request can be merged with any pending or ongoing requests.
153    // If a ScanRequest is returned, the caller is responsible for forwarding it to MLME.
154    pub fn enqueue_scan_to_discover(
155        &mut self,
156        s: DiscoveryScan<T>,
157    ) -> Option<fidl_mlme::ScanRequest> {
158        if let ScanState::ScanningToDiscover { cmd, .. } = &mut self.current
159            && cmd.matches(&s)
160        {
161            cmd.merges(s);
162            return None;
163        }
164        if let Some(scan_cmd) = self.pending_discovery.iter_mut().find(|cmd| cmd.matches(&s)) {
165            scan_cmd.merges(s);
166            return None;
167        }
168        self.pending_discovery.push(s);
169        self.start_next_scan()
170    }
171
172    // Returns a unique transaction ID for the next MLME transaction.
173    fn get_next_mlme_txn_id(&mut self) -> ScanTxnId {
174        self.last_mlme_txn_id += 1;
175        self.last_mlme_txn_id
176    }
177
178    pub(crate) fn start_scheduled_scan(
179        &mut self,
180        req: fidl_common::ScheduledScanRequest,
181        mlme_sink: MlmeSink,
182        responder: Responder<Result<(), i32>>,
183    ) -> ScheduledScanReceiver {
184        // Send start request to MLME with a new transaction ID
185        let txn_id = self.get_next_mlme_txn_id();
186        let mlme_req = fidl_mlme::MlmeStartScheduledScanRequest { txn_id, req };
187        mlme_sink.send(MlmeRequest::StartScheduledScan(mlme_req, responder));
188
189        // Create a channel to process scan results streamed from MLME
190        let (sender, receiver) = mpsc::unbounded();
191        let _ = self.scheduled_scan_receivers.insert(txn_id, ScheduledScanState::new(sender));
192        ScheduledScanReceiver::new(receiver, txn_id, mlme_sink)
193    }
194
195    // Should be called for every OnScanResult event received from MLME.
196    pub fn on_mlme_scan_result(&mut self, msg: fidl_mlme::ScanResult) -> Result<(), Error> {
197        // First check if this belongs to a scheduled scan session.
198        if let Some(session) = self.scheduled_scan_receivers.get_mut(&msg.txn_id) {
199            maybe_insert_bss(&mut session.bss_map, msg.bss);
200            return Ok(());
201        }
202
203        match &mut self.current {
204            ScanState::NotScanning => Err(Error::ScanResultNotScanning),
205            ScanState::ScanningToDiscover { mlme_txn_id, .. } if *mlme_txn_id != msg.txn_id => {
206                Err(Error::ScanResultWrongTxnId)
207            }
208            ScanState::ScanningToDiscover { bss_map, .. } => {
209                maybe_insert_bss(bss_map, msg.bss);
210                Ok(())
211            }
212        }
213    }
214
215    pub(crate) fn on_scheduled_scan_matches_available(
216        &mut self,
217        txn_id: ScanTxnId,
218        sme_inspect: &Arc<inspect::SmeTree>,
219        cfg: &crate::client::ClientConfig,
220        device_info: &fidl_mlme::DeviceInfo,
221        security_support: &fidl_common::SecuritySupport,
222    ) {
223        if let Some(session) = self.scheduled_scan_receivers.get_mut(&txn_id) {
224            let bss_map = std::mem::take(&mut session.bss_map);
225            let bss_description_list = convert_bss_map(bss_map, None::<Ssid>, sme_inspect);
226            let results_fidl = bss_description_list
227                .into_iter()
228                .map(|bss_description| {
229                    cfg.create_scan_result(
230                        // TODO(https://fxbug.dev/42164608): ScanEnd drops the timestamp from MLME
231                        zx::MonotonicInstant::from_nanos(0),
232                        bss_description,
233                        device_info,
234                        security_support,
235                    )
236                })
237                .map(Into::into)
238                .collect::<Vec<_>>();
239
240            match wlan_common::scan::write_vmo(results_fidl) {
241                Ok(vmo) => {
242                    let _ = session.scan_results_sender.unbounded_send(vmo);
243                }
244                Err(e) => {
245                    log::error!("Failed to write VMO for sched scan results: {:?}", e);
246                }
247            }
248        }
249    }
250
251    pub(crate) fn on_scheduled_scan_stopped_by_firmware(&mut self, txn_id: ScanTxnId) {
252        let _ = self.scheduled_scan_receivers.remove(&txn_id);
253    }
254
255    // Should be called for every OnScanEnd event received from MLME.
256    // If a ScanRequest is returned, the caller is responsible for forwarding it to MLME.
257    pub fn on_mlme_scan_end(
258        &mut self,
259        msg: fidl_mlme::ScanEnd,
260        sme_inspect: &Arc<inspect::SmeTree>,
261    ) -> Result<(ScanEnd<T>, Option<fidl_mlme::ScanRequest>), Error> {
262        match mem::replace(&mut self.current, ScanState::NotScanning) {
263            ScanState::NotScanning => Err(Error::ScanEndNotScanning),
264            ScanState::ScanningToDiscover { mlme_txn_id, .. } if mlme_txn_id != msg.txn_id => {
265                Err(Error::ScanEndWrongTxnId)
266            }
267            ScanState::ScanningToDiscover { cmd, bss_map, .. } => {
268                let scan_end = ScanEnd {
269                    tokens: cmd.tokens,
270                    result_code: msg.code,
271                    bss_description_list: convert_bss_map(bss_map, None::<Ssid>, sme_inspect),
272                };
273
274                let request = self.start_next_scan();
275                Ok((scan_end, request))
276            }
277        }
278    }
279
280    fn start_next_scan(&mut self) -> Option<fidl_mlme::ScanRequest> {
281        let has_pending = !self.pending_discovery.is_empty();
282        (matches!(self.current, ScanState::NotScanning) && has_pending).then(|| {
283            let txn_id = self.get_next_mlme_txn_id();
284            let scan_cmd = self.pending_discovery.remove(0);
285            let request = new_discovery_scan_request(
286                txn_id,
287                &scan_cmd,
288                &self.device_info,
289                self.spectrum_management_support.clone(),
290            );
291            self.current = ScanState::ScanningToDiscover {
292                cmd: scan_cmd,
293                mlme_txn_id: txn_id,
294                bss_map: HashMap::new(),
295            };
296            request
297        })
298    }
299}
300
301fn maybe_insert_bss(
302    bss_map: &mut HashMap<Bssid, (fidl_ieee80211::BssDescription, IesMerger)>,
303    mut fidl_bss: fidl_ieee80211::BssDescription,
304) {
305    let mut ies = vec![];
306    std::mem::swap(&mut ies, &mut fidl_bss.ies);
307
308    match bss_map.entry(Bssid::from(fidl_bss.bssid)) {
309        hash_map::Entry::Occupied(mut entry) => {
310            let (existing_bss, ies_merger) = entry.get_mut();
311
312            if (fidl_bss.primary != existing_bss.primary)
313                && (fidl_bss.rssi_dbm < existing_bss.rssi_dbm)
314            {
315                // Assume `fidl_bss` is from an "echo" Beacon frame from the same BSSID
316                return;
317            }
318
319            ies_merger.merge(&ies[..]);
320            if ies_merger.buffer_overflow() {
321                warn!(
322                    "Not merging some IEs due to running out of buffer. BSSID: {}",
323                    Bssid::from(fidl_bss.bssid)
324                );
325            }
326            *existing_bss = fidl_bss;
327        }
328        hash_map::Entry::Vacant(entry) => {
329            let _ = entry.insert((fidl_bss, IesMerger::new(ies)));
330        }
331    }
332}
333
334fn convert_bss_map(
335    bss_map: HashMap<Bssid, (fidl_ieee80211::BssDescription, IesMerger)>,
336    ssid_selector: Option<Ssid>,
337    sme_inspect: &Arc<inspect::SmeTree>,
338) -> Vec<BssDescription> {
339    let bss_description_list =
340        bss_map.into_iter().filter_map(|(_bssid, (mut bss, mut ies_merger))| {
341            let _ = sme_inspect.scan_merge_ie_failures.add(ies_merger.merge_ie_failures() as u64);
342
343            let mut ies = ies_merger.finalize();
344            std::mem::swap(&mut ies, &mut bss.ies);
345            let bss: Option<BssDescription> = bss.try_into().ok();
346            if bss.is_none() {
347                let _ = sme_inspect.scan_discard_fidl_bss.add(1);
348            }
349            bss
350        });
351
352    match ssid_selector {
353        None => bss_description_list.collect(),
354        Some(ssid) => bss_description_list.filter(|v| v.ssid == ssid).collect(),
355    }
356}
357
358fn new_scan_request(
359    mlme_txn_id: ScanTxnId,
360    scan_request: fidl_sme::ScanRequest,
361    ssid_list: Vec<Ssid>,
362    device_info: &fidl_mlme::DeviceInfo,
363    spectrum_management_support: fidl_common::SpectrumManagementSupport,
364) -> fidl_mlme::ScanRequest {
365    let scan_req = fidl_mlme::ScanRequest {
366        txn_id: mlme_txn_id,
367        scan_type: fidl_mlme::ScanTypes::Passive,
368        probe_delay: 0,
369        // TODO(https://fxbug.dev/42169913): SME silently ignores unsupported channels
370        channel_list: get_primary_channels_for_scan(
371            device_info,
372            spectrum_management_support,
373            &scan_request,
374        ),
375        ssid_list: ssid_list.into_iter().map(Ssid::into).collect(),
376        min_channel_time: PASSIVE_SCAN_CHANNEL_MS,
377        max_channel_time: PASSIVE_SCAN_CHANNEL_MS,
378    };
379    match scan_request {
380        fidl_sme::ScanRequest::Active(active_scan_params) => fidl_mlme::ScanRequest {
381            scan_type: fidl_mlme::ScanTypes::Active,
382            ssid_list: active_scan_params.ssids,
383            probe_delay: ACTIVE_SCAN_PROBE_DELAY_MS,
384            min_channel_time: ACTIVE_SCAN_CHANNEL_MS,
385            max_channel_time: ACTIVE_SCAN_CHANNEL_MS,
386            ..scan_req
387        },
388        fidl_sme::ScanRequest::Passive(_) => scan_req,
389    }
390}
391
392fn new_discovery_scan_request<T>(
393    mlme_txn_id: ScanTxnId,
394    discovery_scan: &DiscoveryScan<T>,
395    device_info: &fidl_mlme::DeviceInfo,
396    spectrum_management_support: fidl_common::SpectrumManagementSupport,
397) -> fidl_mlme::ScanRequest {
398    new_scan_request(
399        mlme_txn_id,
400        discovery_scan.scan_request.clone(),
401        vec![],
402        device_info,
403        spectrum_management_support,
404    )
405}
406
407/// Returns channels at the intersection of
408///
409///   - CANDIDATE_PRIMARY_CHANNELS
410///   - This device's primary channels.
411///   - The requested channels (for an active scan only).
412///
413/// When a device does not support DFS, 5 GHz channels are excluded for active scans.
414/// Every 5 GHz channel requires DFS support in at least one regulatory domain, or is otherwise
415/// not allowed in some regulatory domain. This function cautiously excludes 5 GHz channels
416/// for active scans on those devices to ensure accordance with each the regulatory domain's DFS
417/// requirements. The wlan-sme library is the common component in every WLAN interface and
418/// is therefore a sensible place for this filter.
419///
420/// TODO(https://fxbug.dev/42144530): Known quirks about this implementation.
421fn get_primary_channels_for_scan(
422    device_info: &fidl_mlme::DeviceInfo,
423    spectrum_management_support: fidl_common::SpectrumManagementSupport,
424    scan_request: &fidl_sme::ScanRequest,
425) -> Vec<fidl_ieee80211::ChannelNumber> {
426    let mut primary_channels: HashSet<u8> = HashSet::new();
427    for band in &device_info.bands {
428        primary_channels.extend(band.primary_channels.iter().map(|c| c.number));
429    }
430
431    let requested_channels = match scan_request {
432        fidl_sme::ScanRequest::Active(options) => &options.channels[..],
433        fidl_sme::ScanRequest::Passive(options) => &options.channels[..],
434    };
435    let channels: Vec<fidl_ieee80211::ChannelNumber> = CANDIDATE_PRIMARY_CHANNELS
436        .iter()
437        .filter(|channel| primary_channels.contains(&channel.primary))
438        .filter(|channel| {
439            // Avoid active scans on 5 GHz channels on a non-DFS device. There is no 5 GHz
440            // channel that is valid in all regulatory domains.
441            if let &fidl_sme::ScanRequest::Passive(_) = scan_request {
442                return true;
443            };
444            if channel.band == fidl_ieee80211::WlanBand::FiveGhz {
445                return spectrum_management_support
446                    .dfs
447                    .as_ref()
448                    .and_then(|dfs| dfs.supported)
449                    .unwrap_or(false);
450            };
451            true
452        })
453        .filter(|channel| {
454            // If there are any channels specified by the caller, only include those channels.
455            if !requested_channels.is_empty() {
456                return requested_channels.contains(&channel.primary);
457            }
458            true
459        })
460        .copied()
461        .map(|channel| channel.into())
462        .collect();
463
464    if channels.is_empty() {
465        if !requested_channels.is_empty() {
466            warn!("All channels are filtered out. Requested channels: {:?}", requested_channels);
467        } else {
468            warn!("All channels are filtered out.");
469        };
470    }
471
472    channels
473}
474
475// The following constructs the Channel list at runtime once and leaks its contents
476// as a static reference. Firmware will reject channels if they are not allowed by
477// the current regulatory region.
478static CANDIDATE_PRIMARY_CHANNELS: LazyLock<&'static [Channel]> = LazyLock::new(|| {
479    let channels_two_ghz = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14];
480    let channels_five_ghz = vec![
481        36, 40, 44, 48, 52, 56, 60, 64, 100, 104, 108, 112, 116, 120, 124, 128, 132, 136, 140, 144,
482        149, 153, 157, 161, 165,
483    ];
484
485    let mut channels_to_scan = Vec::new();
486    for channel in channels_two_ghz {
487        channels_to_scan.push(Channel::new(
488            channel,
489            Bandwidth::Cbw20,
490            fidl_ieee80211::WlanBand::TwoGhz,
491        ));
492    }
493    for channel in channels_five_ghz {
494        channels_to_scan.push(Channel::new(
495            channel,
496            Bandwidth::Cbw20,
497            fidl_ieee80211::WlanBand::FiveGhz,
498        ));
499    }
500
501    channels_to_scan.leak()
502});
503
504#[cfg(test)]
505mod tests {
506    use super::*;
507    use crate::test_utils;
508    use assert_matches::assert_matches;
509    use fidl_ieee80211::WlanBand::{FiveGhz, TwoGhz};
510    use fuchsia_inspect::Inspector;
511
512    use ieee80211::MacAddr;
513    use regex::bytes::Regex;
514    use std::fmt::Write;
515    use std::sync::LazyLock;
516    use test_case::test_case;
517    use wlan_common::test_utils::fake_capabilities::fake_5ghz_band_capability;
518    use wlan_common::test_utils::fake_features::fake_spectrum_management_support_empty;
519    use wlan_common::{fake_bss_description, fake_fidl_bss_description};
520
521    static CLIENT_ADDR: LazyLock<MacAddr> =
522        LazyLock::new(|| [0x7A, 0xE7, 0x76, 0xD9, 0xF2, 0x67].into());
523
524    impl ScheduledScanReceiver {
525        pub(crate) fn try_next(&mut self) -> Result<Option<fidl::Vmo>, mpsc::TryRecvError> {
526            let res = self.scan_results_receiver.try_next();
527            if let Ok(None) = res {
528                self.stopped_by_firmware = true;
529            }
530            res
531        }
532    }
533
534    fn passive_discovery_scan(token: i32) -> DiscoveryScan<i32> {
535        DiscoveryScan::new(
536            token,
537            fidl_sme::ScanRequest::Passive(fidl_sme::PassiveScanRequest { channels: vec![] }),
538        )
539    }
540
541    #[test]
542    fn discovery_scan() {
543        let mut sched = create_sched();
544        let _next_txn_id = 0;
545        let (_inspector, sme_inspect) = sme_inspect();
546        let req = sched
547            .enqueue_scan_to_discover(passive_discovery_scan(10))
548            .expect("expected a ScanRequest");
549        let txn_id = req.txn_id;
550        sched
551            .on_mlme_scan_result(fidl_mlme::ScanResult {
552                txn_id,
553                timestamp_nanos: zx::MonotonicInstant::get().into_nanos(),
554                bss: fidl_ieee80211::BssDescription {
555                    bssid: [1; 6],
556                    ..fake_fidl_bss_description!(Open, ssid: Ssid::try_from("foo").unwrap())
557                },
558            })
559            .expect("expect scan result received");
560        assert_matches!(
561            sched.on_mlme_scan_result(fidl_mlme::ScanResult {
562                txn_id: txn_id + 100, // mismatching transaction id
563                timestamp_nanos: zx::MonotonicInstant::get().into_nanos(),
564                bss: fidl_ieee80211::BssDescription {
565                    bssid: [2; 6],
566                    ..fake_fidl_bss_description!(Open, ssid: Ssid::try_from("bar").unwrap())
567                },
568            },),
569            Err(Error::ScanResultWrongTxnId)
570        );
571        sched
572            .on_mlme_scan_result(fidl_mlme::ScanResult {
573                txn_id,
574                timestamp_nanos: zx::MonotonicInstant::get().into_nanos(),
575                bss: fidl_ieee80211::BssDescription {
576                    bssid: [3; 6],
577                    ..fake_fidl_bss_description!(Open, ssid: Ssid::try_from("qux").unwrap())
578                },
579            })
580            .expect("expect scan result received");
581        let (scan_end, mlme_req) = assert_matches!(
582            sched.on_mlme_scan_end(
583                fidl_mlme::ScanEnd { txn_id, code: fidl_mlme::ScanResultCode::Success },
584                &sme_inspect),
585            Ok((scan_end, mlme_req)) => (scan_end, mlme_req)
586        );
587        assert!(mlme_req.is_none());
588        let (tokens, bss_description_list) = assert_matches!(
589            scan_end,
590            ScanEnd {
591                tokens,
592                result_code: fidl_mlme::ScanResultCode::Success,
593                bss_description_list
594            } => (tokens, bss_description_list),
595            "expected discovery scan to be completed successfully"
596        );
597        assert_eq!(vec![10], tokens);
598        let mut ssid_list =
599            bss_description_list.into_iter().map(|bss| bss.ssid).collect::<Vec<_>>();
600        ssid_list.sort();
601        assert_eq!(vec![Ssid::try_from("foo").unwrap(), Ssid::try_from("qux").unwrap()], ssid_list);
602    }
603
604    #[test_case(vec![
605        fake_fidl_bss_description!(Open, ssid: Ssid::try_from("bar").unwrap()),
606        fake_fidl_bss_description!(Open, ssid: Ssid::try_from("baz").unwrap()),
607    ], vec![fake_bss_description!(Open, ssid: Ssid::try_from("baz").unwrap())] ;
608                "when latest BSS Description is new")]
609    #[test_case(vec![
610        fake_fidl_bss_description!(Open, rssi_dbm: -36, channel: Channel::new(149, Bandwidth::Cbw20, FiveGhz)),
611        fake_fidl_bss_description!(Open, rssi_dbm: -84, channel: Channel::new(165, Bandwidth::Cbw20, FiveGhz)),
612    ], vec![fake_bss_description!(Open, rssi_dbm: -36, channel: Channel::new(149, Bandwidth::Cbw20, FiveGhz))] ;
613                "when strong signal is first")]
614    #[test_case(vec![
615        fake_fidl_bss_description!(Open, rssi_dbm: -84, channel: Channel::new(64, Bandwidth::Cbw20, FiveGhz)),
616        fake_fidl_bss_description!(Open, rssi_dbm: -36, channel: Channel::new(50, Bandwidth::Cbw20, FiveGhz)),
617        fake_fidl_bss_description!(Open, rssi_dbm: -80, channel: Channel::new(36, Bandwidth::Cbw20, FiveGhz)),
618    ], vec![fake_bss_description!(Open, rssi_dbm: -36, channel: Channel::new(50, Bandwidth::Cbw20, FiveGhz))];
619                "when strong signal is middle")]
620    #[test_case(vec![
621        fake_fidl_bss_description!(Open, rssi_dbm: -84, channel: Channel::new(64, Bandwidth::Cbw20, FiveGhz)),
622        fake_fidl_bss_description!(Open, rssi_dbm: -80, channel: Channel::new(36, Bandwidth::Cbw20, FiveGhz)),
623        fake_fidl_bss_description!(Open, rssi_dbm: -36, channel: Channel::new(50, Bandwidth::Cbw20, FiveGhz)),
624    ], vec![fake_bss_description!(Open, rssi_dbm: -36, channel: Channel::new(50, Bandwidth::Cbw20, FiveGhz))];
625                "when strong signal is last")]
626    #[test_case(vec![
627        fake_fidl_bss_description!(Open, rssi_dbm: -84, ssid: Ssid::try_from("bar").unwrap(),
628                                   channel: Channel::new(149, Bandwidth::Cbw20, FiveGhz)),
629        fake_fidl_bss_description!(Open, rssi_dbm: -36, ssid: Ssid::try_from("bar").unwrap(),
630                                   channel: Channel::new(165, Bandwidth::Cbw20, FiveGhz)),
631        fake_fidl_bss_description!(Open, rssi_dbm: -40, ssid: Ssid::try_from("baz").unwrap(),
632                                   channel: Channel::new(165, Bandwidth::Cbw20, FiveGhz)),
633    ], vec![fake_bss_description!(Open, rssi_dbm: -40, ssid: Ssid::try_from("baz").unwrap(),
634                                  channel: Channel::new(165, Bandwidth::Cbw20, FiveGhz))];
635                "overwrite latest chosen channel")]
636    fn deduplicate_by_bssid(
637        bss_description_list_from_mlme: Vec<fidl_ieee80211::BssDescription>,
638        returned_bss_description_list: Vec<BssDescription>,
639    ) {
640        let mut sched = create_sched();
641        let _next_txn_id = 0;
642        let (_inspector, sme_inspect) = sme_inspect();
643        let req = sched
644            .enqueue_scan_to_discover(passive_discovery_scan(10))
645            .expect("expected a ScanRequest");
646        let txn_id = req.txn_id;
647        for bss in bss_description_list_from_mlme {
648            sched
649                .on_mlme_scan_result(fidl_mlme::ScanResult {
650                    txn_id,
651                    timestamp_nanos: zx::MonotonicInstant::get().into_nanos(),
652                    bss,
653                })
654                .expect("expect scan result received");
655        }
656        let (scan_end, mlme_req) = assert_matches!(
657            sched.on_mlme_scan_end(
658                fidl_mlme::ScanEnd { txn_id, code: fidl_mlme::ScanResultCode::Success },
659                &sme_inspect),
660            Ok((scan_end, mlme_req)) => (scan_end, mlme_req)
661        );
662        assert!(mlme_req.is_none());
663        let (tokens, bss_description_list) = assert_matches!(
664            scan_end,
665            ScanEnd {
666                tokens,
667                result_code: fidl_mlme::ScanResultCode::Success,
668                bss_description_list
669            } => (tokens, bss_description_list),
670            "expected discovery scan to be completed successfully"
671        );
672        assert_eq!(vec![10], tokens);
673        assert_eq!(bss_description_list, returned_bss_description_list);
674    }
675
676    #[test]
677    fn discovery_scan_merge_ies() {
678        let mut sched = create_sched();
679        let _next_txn_id = 0;
680        let (_inspector, sme_inspect) = sme_inspect();
681        let req = sched
682            .enqueue_scan_to_discover(passive_discovery_scan(10))
683            .expect("expected a ScanRequest");
684        let txn_id = req.txn_id;
685
686        let mut bss = fake_fidl_bss_description!(Open, ssid: Ssid::try_from("ssid").unwrap());
687        // Add an extra IE so we can distinguish this result.
688        let ie_marker1 = &[0xdd, 0x07, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee];
689        bss.ies.extend_from_slice(ie_marker1);
690        sched
691            .on_mlme_scan_result(fidl_mlme::ScanResult {
692                txn_id,
693                timestamp_nanos: zx::MonotonicInstant::get().into_nanos(),
694                bss,
695            })
696            .expect("expect scan result received");
697
698        let mut bss = fake_fidl_bss_description!(Open, ssid: Ssid::try_from("ssid").unwrap());
699        // Add an extra IE so we can distinguish this result.
700        let ie_marker2 = &[0xdd, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff];
701        bss.ies.extend_from_slice(ie_marker2);
702        sched
703            .on_mlme_scan_result(fidl_mlme::ScanResult {
704                txn_id,
705                timestamp_nanos: zx::MonotonicInstant::get().into_nanos(),
706                bss,
707            })
708            .expect("expect scan result received");
709        let (scan_end, mlme_req) = assert_matches!(
710            sched.on_mlme_scan_end(
711                fidl_mlme::ScanEnd { txn_id, code: fidl_mlme::ScanResultCode::Success },
712                &sme_inspect),
713            Ok((scan_end, mlme_req)) => (scan_end, mlme_req)
714        );
715        assert!(mlme_req.is_none());
716        let (tokens, bss_description_list) = assert_matches!(
717            scan_end,
718            ScanEnd {
719                tokens,
720                result_code: fidl_mlme::ScanResultCode::Success,
721                bss_description_list
722            } => (tokens, bss_description_list),
723            "expected discovery scan to be completed successfully"
724        );
725        assert_eq!(vec![10], tokens);
726
727        assert_eq!(bss_description_list.len(), 1);
728        // Verify that both IEs are processed.
729        assert!(slice_contains(bss_description_list[0].ies(), ie_marker1));
730        assert!(slice_contains(bss_description_list[0].ies(), ie_marker2));
731    }
732
733    fn slice_contains(slice: &[u8], subslice: &[u8]) -> bool {
734        // https://github.com/rust-lang/regex/issues/451#issuecomment-367987989
735        let re = {
736            let mut re_string = String::with_capacity(6 + subslice.len() * 4);
737            re_string += "(?-u:";
738            for b in subslice {
739                write!(re_string, "\\x{b:02X}").unwrap();
740            }
741            re_string += ")";
742            Regex::new(&re_string).unwrap()
743        };
744        re.is_match(slice)
745    }
746
747    #[test_case(&[1, 2, 3], &[] => true; "vacuous")]
748    #[test_case(&[1, 2, 3], &[1u8] => true; "one byte")]
749    #[test_case(&[1, 2, 3], &[2u8, 3] => true; "multiple bytes")]
750    #[test_case(&[1, 1, 1], &[1u8, 1] => true; "multiple matches")]
751    #[test_case(&[1, 2, 3], &[0u8] => false; "no match")]
752    #[test_case(&[1, 2, 3], &[1u8, 2, 3, 4] => false; "too large")]
753    #[test_case(&[0x87, 0x77, 0x78], &[0x77, 0x77] => false; "misaligned match")]
754    fn slice_contains_test(slice: &[u8], subslice: &[u8]) -> bool {
755        slice_contains(slice, subslice)
756    }
757
758    #[test]
759    fn test_passive_discovery_scan_args() {
760        let mut sched = create_sched();
761        let _next_txn_id = 0;
762        let req = sched
763            .enqueue_scan_to_discover(passive_discovery_scan(10))
764            .expect("expected a ScanRequest");
765        assert_eq!(req.txn_id, 1);
766        assert_eq!(req.scan_type, fidl_mlme::ScanTypes::Passive);
767        assert_eq!(
768            req.channel_list.into_iter().collect::<HashSet<_>>(),
769            CANDIDATE_PRIMARY_CHANNELS.iter().copied().map(|c| c.into()).collect::<HashSet<_>>()
770        );
771        assert_eq!(req.ssid_list, Vec::<Vec<u8>>::new());
772        assert_eq!(req.probe_delay, 0);
773        assert_eq!(req.min_channel_time, 200);
774        assert_eq!(req.max_channel_time, 200);
775    }
776
777    #[test_case(true, HashSet::from([
778        fidl_ieee80211::ChannelNumber { number: 1, band: TwoGhz },
779        fidl_ieee80211::ChannelNumber { number: 36, band: FiveGhz },
780        fidl_ieee80211::ChannelNumber { number: 165, band: FiveGhz },
781    ]); "dfs_enabled")]
782    #[test_case(false, HashSet::from([
783        fidl_ieee80211::ChannelNumber { number: 1, band: TwoGhz },
784    ]); "dfs_disabled")]
785    fn test_active_discovery_scan_args_empty(
786        dfs_supported: bool,
787        expected_channels: HashSet<fidl_ieee80211::ChannelNumber>,
788    ) {
789        let device_info = device_info_with_channel(vec![1, 36, 165]);
790        let mut spectrum_management = fake_spectrum_management_support_empty();
791        if dfs_supported {
792            spectrum_management.dfs.get_or_insert_with(Default::default).supported = Some(true);
793        }
794        let mut sched: ScanScheduler<i32> =
795            ScanScheduler::new(Arc::new(device_info), spectrum_management);
796        let _next_txn_id = 0;
797        let scan_cmd = DiscoveryScan::new(
798            10,
799            fidl_sme::ScanRequest::Active(fidl_sme::ActiveScanRequest {
800                ssids: vec![],
801                channels: vec![],
802            }),
803        );
804        let req = sched.enqueue_scan_to_discover(scan_cmd).expect("expected a ScanRequest");
805
806        assert_eq!(req.txn_id, 1);
807        assert_eq!(req.scan_type, fidl_mlme::ScanTypes::Active);
808        assert_eq!(req.channel_list.into_iter().collect::<HashSet<_>>(), expected_channels);
809        assert_eq!(req.ssid_list, Vec::<Vec<u8>>::new());
810        assert_eq!(req.probe_delay, 5);
811        assert_eq!(req.min_channel_time, 75);
812        assert_eq!(req.max_channel_time, 75);
813    }
814
815    #[test]
816    fn test_active_discovery_scan_args_filled() {
817        let device_info = device_info_with_channel(vec![1, 36, 165]);
818        let mut sched: ScanScheduler<i32> =
819            ScanScheduler::new(Arc::new(device_info), fake_spectrum_management_support_empty());
820        let _next_txn_id = 0;
821        let ssid1: Vec<u8> = Ssid::try_from("ssid1").unwrap().into();
822        let ssid2: Vec<u8> = Ssid::try_from("ssid2").unwrap().into();
823        let scan_cmd = DiscoveryScan::new(
824            10,
825            fidl_sme::ScanRequest::Active(fidl_sme::ActiveScanRequest {
826                ssids: vec![ssid1.clone(), ssid2.clone()],
827                // TODO(https://fxbug.dev/42169913): SME silently ignores unsupported channels
828                channels: vec![1, 20, 100],
829            }),
830        );
831        let req = sched.enqueue_scan_to_discover(scan_cmd).expect("expected a ScanRequest");
832
833        assert_eq!(req.txn_id, 1);
834        assert_eq!(req.scan_type, fidl_mlme::ScanTypes::Active);
835        assert_eq!(
836            req.channel_list,
837            vec![fidl_ieee80211::ChannelNumber { number: 1, band: TwoGhz }]
838        );
839        assert_eq!(req.ssid_list, vec![ssid1, ssid2]);
840        assert_eq!(req.probe_delay, 5);
841        assert_eq!(req.min_channel_time, 75);
842        assert_eq!(req.max_channel_time, 75);
843    }
844
845    #[test]
846    fn test_passive_discovery_scan_args_filled() {
847        // Set up the device that can operate on channels 1, 36, and 165.
848        let device_info = device_info_with_channel(vec![1, 36, 165]);
849        let mut sched: ScanScheduler<i32> =
850            ScanScheduler::new(Arc::new(device_info), fake_spectrum_management_support_empty());
851        // Request a scan using only some of the supported channels.
852        let scan_cmd = DiscoveryScan::new(
853            10,
854            fidl_sme::ScanRequest::Passive(fidl_sme::PassiveScanRequest { channels: vec![1, 36] }),
855        );
856        let _next_txn_id = 0;
857        let req = sched.enqueue_scan_to_discover(scan_cmd).expect("expected a ScanRequest");
858
859        assert_eq!(req.txn_id, 1);
860        assert_eq!(req.scan_type, fidl_mlme::ScanTypes::Passive);
861        // Verify that only the requested channels are included.
862        assert_eq!(
863            req.channel_list.into_iter().collect::<HashSet<_>>(),
864            HashSet::from([
865                fidl_ieee80211::ChannelNumber { number: 1, band: TwoGhz },
866                fidl_ieee80211::ChannelNumber { number: 36, band: FiveGhz },
867            ])
868        );
869        assert_eq!(req.ssid_list, Vec::<Vec<u8>>::new());
870        assert_eq!(req.probe_delay, 0);
871        assert_eq!(req.min_channel_time, 200);
872        assert_eq!(req.max_channel_time, 200);
873    }
874
875    #[test]
876    fn test_passive_discovery_scan_args_unsupported_filtered() {
877        let device_info = device_info_with_channel(vec![1, 36]);
878        let mut sched: ScanScheduler<i32> =
879            ScanScheduler::new(Arc::new(device_info), fake_spectrum_management_support_empty());
880        let _next_txn_id = 0;
881        // Request a scan that includes a channel not supported by the device.
882        let scan_cmd = DiscoveryScan::new(
883            10,
884            fidl_sme::ScanRequest::Passive(fidl_sme::PassiveScanRequest {
885                channels: vec![1, 6, 36],
886            }),
887        );
888        let req = sched.enqueue_scan_to_discover(scan_cmd).expect("expected a ScanRequest");
889
890        assert_eq!(req.txn_id, 1);
891        assert_eq!(req.scan_type, fidl_mlme::ScanTypes::Passive);
892        // Verify that the unsupported channel 6 was filtered out.
893        assert_eq!(
894            req.channel_list.into_iter().collect::<HashSet<_>>(),
895            HashSet::from([
896                fidl_ieee80211::ChannelNumber { number: 1, band: TwoGhz },
897                fidl_ieee80211::ChannelNumber { number: 36, band: FiveGhz },
898            ])
899        );
900    }
901
902    #[test]
903    fn test_passive_discovery_scan_args_invalid_filtered() {
904        let device_info = device_info_with_channel(vec![1, 200]);
905        let mut sched: ScanScheduler<i32> =
906            ScanScheduler::new(Arc::new(device_info), fake_spectrum_management_support_empty());
907        let _next_txn_id = 0;
908        // Request a scan that includes an invalid channel.
909        let scan_cmd = DiscoveryScan::new(
910            10,
911            fidl_sme::ScanRequest::Passive(fidl_sme::PassiveScanRequest { channels: vec![1, 200] }),
912        );
913        let req = sched.enqueue_scan_to_discover(scan_cmd).expect("expected a ScanRequest");
914
915        assert_eq!(req.txn_id, 1);
916        assert_eq!(req.scan_type, fidl_mlme::ScanTypes::Passive);
917        // Verify that the invalid channel 200 was filtered out and the valid channel is included.
918        assert_eq!(
919            req.channel_list,
920            vec![fidl_ieee80211::ChannelNumber { number: 1, band: TwoGhz }]
921        );
922    }
923
924    #[test]
925    fn test_passive_discovery_scan_args_empty_list() {
926        let device_info = device_info_with_channel(vec![1, 36, 165]);
927        let mut sched: ScanScheduler<i32> =
928            ScanScheduler::new(Arc::new(device_info), fake_spectrum_management_support_empty());
929        let _next_txn_id = 0;
930        let scan_cmd = DiscoveryScan::new(
931            10,
932            fidl_sme::ScanRequest::Passive(fidl_sme::PassiveScanRequest { channels: vec![] }),
933        );
934        let req = sched.enqueue_scan_to_discover(scan_cmd).expect("expected a ScanRequest");
935
936        assert_eq!(req.txn_id, 1);
937        assert_eq!(req.scan_type, fidl_mlme::ScanTypes::Passive);
938        assert_eq!(
939            req.channel_list.into_iter().collect::<HashSet<_>>(),
940            HashSet::from([
941                fidl_ieee80211::ChannelNumber { number: 1, band: TwoGhz },
942                fidl_ieee80211::ChannelNumber { number: 36, band: FiveGhz },
943                fidl_ieee80211::ChannelNumber { number: 165, band: FiveGhz },
944            ])
945        );
946    }
947
948    #[test]
949    fn test_discovery_scans_dedupe_single_group() {
950        let mut sched = create_sched();
951        let _next_txn_id = 0;
952        let (_inspector, sme_inspect) = sme_inspect();
953
954        // Post one scan command, expect a message to MLME
955        let mlme_req = sched
956            .enqueue_scan_to_discover(passive_discovery_scan(10))
957            .expect("expected a ScanRequest");
958        let txn_id = mlme_req.txn_id;
959
960        // Report a scan result
961        sched
962            .on_mlme_scan_result(fidl_mlme::ScanResult {
963                txn_id,
964                timestamp_nanos: zx::MonotonicInstant::get().into_nanos(),
965                bss: fidl_ieee80211::BssDescription {
966                    bssid: [1; 6],
967                    ..fake_fidl_bss_description!(Open, ssid: Ssid::try_from("foo").unwrap())
968                },
969            })
970            .expect("expect scan result received");
971
972        // Post another command. It should not issue another request to the MLME since
973        // there is already an on-going one
974        assert!(sched.enqueue_scan_to_discover(passive_discovery_scan(20)).is_none());
975
976        // Report another scan result and the end of the scan transaction
977        sched
978            .on_mlme_scan_result(fidl_mlme::ScanResult {
979                txn_id,
980                timestamp_nanos: zx::MonotonicInstant::get().into_nanos(),
981                bss: fidl_ieee80211::BssDescription {
982                    bssid: [2; 6],
983                    ..fake_fidl_bss_description!(Open, ssid: Ssid::try_from("bar").unwrap())
984                },
985            })
986            .expect("expect scan result received");
987        let (scan_end, mlme_req) = assert_matches!(
988            sched.on_mlme_scan_end(
989                fidl_mlme::ScanEnd { txn_id, code: fidl_mlme::ScanResultCode::Success },
990                &sme_inspect),
991            Ok((scan_end, mlme_req)) => (scan_end, mlme_req)
992        );
993
994        // We don't expect another request to the MLME
995        assert!(mlme_req.is_none());
996
997        // Expect a discovery result with both tokens and both SSIDs
998        assert_discovery_scan_result(
999            scan_end,
1000            vec![10, 20],
1001            vec![Ssid::try_from("bar").unwrap(), Ssid::try_from("foo").unwrap()],
1002        );
1003    }
1004
1005    #[test]
1006    fn test_discovery_scans_dedupe_multiple_groups() {
1007        let mut sched = create_sched();
1008        let (_inspector, sme_inspect) = sme_inspect();
1009
1010        // Post a passive scan command, expect a message to MLME
1011        let mlme_req = sched
1012            .enqueue_scan_to_discover(passive_discovery_scan(10))
1013            .expect("expected a ScanRequest");
1014        let txn_id = mlme_req.txn_id;
1015
1016        // Post an active scan command, which should be enqueued until the previous one finishes
1017        let scan_cmd = DiscoveryScan::new(
1018            20,
1019            fidl_sme::ScanRequest::Active(fidl_sme::ActiveScanRequest {
1020                ssids: vec![],
1021                channels: vec![],
1022            }),
1023        );
1024        assert!(sched.enqueue_scan_to_discover(scan_cmd).is_none());
1025
1026        // Post a passive scan command. It should be merged with the ongoing one and so should not
1027        // issue another request to MLME
1028        assert!(sched.enqueue_scan_to_discover(passive_discovery_scan(30)).is_none());
1029
1030        // Post an active scan command. It should be merged with the active scan command that's
1031        // still enqueued, and so should not issue another request to MLME
1032        let scan_cmd = DiscoveryScan::new(
1033            40,
1034            fidl_sme::ScanRequest::Active(fidl_sme::ActiveScanRequest {
1035                ssids: vec![],
1036                channels: vec![],
1037            }),
1038        );
1039        assert!(sched.enqueue_scan_to_discover(scan_cmd).is_none());
1040
1041        // Report scan result and scan end
1042        sched
1043            .on_mlme_scan_result(fidl_mlme::ScanResult {
1044                txn_id,
1045                timestamp_nanos: zx::MonotonicInstant::get().into_nanos(),
1046                bss: fidl_ieee80211::BssDescription {
1047                    bssid: [1; 6],
1048                    ..fake_fidl_bss_description!(Open, ssid: Ssid::try_from("foo").unwrap())
1049                },
1050            })
1051            .expect("expect scan result received");
1052        let (scan_end, mlme_req) = assert_matches!(
1053            sched.on_mlme_scan_end(
1054                fidl_mlme::ScanEnd { txn_id, code: fidl_mlme::ScanResultCode::Success },
1055                &sme_inspect),
1056            Ok((scan_end, mlme_req)) => (scan_end, mlme_req)
1057        );
1058
1059        // Expect discovery result with 1st and 3rd tokens
1060        assert_discovery_scan_result(scan_end, vec![10, 30], vec![Ssid::try_from("foo").unwrap()]);
1061
1062        // Next mlme_req should be an active scan request
1063        assert!(mlme_req.is_some());
1064        let mlme_req = mlme_req.unwrap();
1065        assert_eq!(mlme_req.scan_type, fidl_mlme::ScanTypes::Active);
1066        let txn_id = mlme_req.txn_id;
1067
1068        // Report scan result and scan end
1069        sched
1070            .on_mlme_scan_result(fidl_mlme::ScanResult {
1071                txn_id,
1072                timestamp_nanos: zx::MonotonicInstant::get().into_nanos(),
1073                bss: fidl_ieee80211::BssDescription {
1074                    bssid: [2; 6],
1075                    ..fake_fidl_bss_description!(Open, ssid: Ssid::try_from("bar").unwrap())
1076                },
1077            })
1078            .expect("expect scan result received");
1079        let (scan_end, mlme_req) = assert_matches!(
1080            sched.on_mlme_scan_end(
1081                fidl_mlme::ScanEnd { txn_id, code: fidl_mlme::ScanResultCode::Success },
1082                &sme_inspect),
1083            Ok((scan_end, mlme_req)) => (scan_end, mlme_req)
1084        );
1085
1086        // Expect discovery result with 2nd and 4th tokens
1087        assert_discovery_scan_result(scan_end, vec![20, 40], vec![Ssid::try_from("bar").unwrap()]);
1088
1089        // We don't expect another request to the MLME
1090        assert!(mlme_req.is_none());
1091    }
1092
1093    #[test]
1094    fn test_discovery_scan_result_wrong_txn_id() {
1095        let mut sched = create_sched();
1096        let _next_txn_id = 0;
1097
1098        // Post a passive scan command, expect a message to MLME
1099        let mlme_req = sched
1100            .enqueue_scan_to_discover(passive_discovery_scan(10))
1101            .expect("expected a ScanRequest");
1102        let txn_id = mlme_req.txn_id;
1103
1104        // Report scan result with wrong txn id
1105        assert_matches!(
1106            sched.on_mlme_scan_result(fidl_mlme::ScanResult {
1107                txn_id: txn_id + 1,
1108                timestamp_nanos: zx::MonotonicInstant::get().into_nanos(),
1109                bss: fidl_ieee80211::BssDescription {
1110                    bssid: [1; 6],
1111                    ..fake_fidl_bss_description!(Open, ssid: Ssid::try_from("foo").unwrap())
1112                },
1113            },),
1114            Err(Error::ScanResultWrongTxnId)
1115        );
1116    }
1117
1118    #[test]
1119    fn test_discovery_scan_result_not_scanning() {
1120        let mut sched = create_sched();
1121        assert_matches!(
1122            sched.on_mlme_scan_result(fidl_mlme::ScanResult {
1123                txn_id: 0,
1124                timestamp_nanos: zx::MonotonicInstant::get().into_nanos(),
1125                bss: fidl_ieee80211::BssDescription {
1126                    bssid: [1; 6],
1127                    ..fake_fidl_bss_description!(Open, ssid: Ssid::try_from("foo").unwrap())
1128                },
1129            },),
1130            Err(Error::ScanResultNotScanning)
1131        );
1132    }
1133
1134    #[test]
1135    fn test_discovery_scan_end_wrong_txn_id() {
1136        let mut sched = create_sched();
1137        let _next_txn_id = 0;
1138        let (_inspector, sme_inspect) = sme_inspect();
1139
1140        // Post a passive scan command, expect a message to MLME
1141        let mlme_req = sched
1142            .enqueue_scan_to_discover(passive_discovery_scan(10))
1143            .expect("expected a ScanRequest");
1144        let txn_id = mlme_req.txn_id;
1145
1146        assert_matches!(
1147            sched.on_mlme_scan_end(
1148                fidl_mlme::ScanEnd { txn_id: txn_id + 1, code: fidl_mlme::ScanResultCode::Success },
1149                &sme_inspect
1150            ),
1151            Err(Error::ScanEndWrongTxnId)
1152        );
1153    }
1154
1155    #[test]
1156    fn test_discovery_scan_end_not_scanning() {
1157        let mut sched = create_sched();
1158        let _next_txn_id = 0;
1159        let (_inspector, sme_inspect) = sme_inspect();
1160        assert_matches!(
1161            sched.on_mlme_scan_end(
1162                fidl_mlme::ScanEnd { txn_id: 0, code: fidl_mlme::ScanResultCode::Success },
1163                &sme_inspect
1164            ),
1165            Err(Error::ScanEndNotScanning)
1166        );
1167    }
1168
1169    fn assert_discovery_scan_result(
1170        scan_end: ScanEnd<i32>,
1171        expected_tokens: Vec<i32>,
1172        expected_ssids: Vec<Ssid>,
1173    ) {
1174        let (tokens, bss_description_list) = assert_matches!(
1175            scan_end,
1176            ScanEnd {
1177                tokens,
1178                result_code: fidl_mlme::ScanResultCode::Success,
1179                bss_description_list
1180            } => (tokens, bss_description_list),
1181            "expected discovery scan to be completed successfully"
1182        );
1183        assert_eq!(tokens, expected_tokens);
1184        let mut ssid_list =
1185            bss_description_list.into_iter().map(|bss| bss.ssid.clone()).collect::<Vec<_>>();
1186        ssid_list.sort();
1187        assert_eq!(ssid_list, expected_ssids);
1188    }
1189
1190    fn create_sched() -> ScanScheduler<i32> {
1191        ScanScheduler::new(
1192            Arc::new(test_utils::fake_device_info(*CLIENT_ADDR)),
1193            fake_spectrum_management_support_empty(),
1194        )
1195    }
1196
1197    fn device_info_with_channel(operating_channels: Vec<u8>) -> fidl_mlme::DeviceInfo {
1198        fidl_mlme::DeviceInfo {
1199            bands: vec![fidl_mlme::BandCapability {
1200                primary_channels: operating_channels
1201                    .into_iter()
1202                    .map(|n| fidl_ieee80211::ChannelNumber {
1203                        number: n,
1204                        band: fidl_ieee80211::WlanBand::FiveGhz,
1205                    })
1206                    .collect(),
1207                ..fake_5ghz_band_capability()
1208            }],
1209            ..test_utils::fake_device_info(*CLIENT_ADDR)
1210        }
1211    }
1212
1213    fn sme_inspect() -> (Inspector, Arc<inspect::SmeTree>) {
1214        let inspector = Inspector::default();
1215        let sme_inspect = Arc::new(inspect::SmeTree::new(
1216            inspector.clone(),
1217            inspector.root().create_child("usme"),
1218            &test_utils::fake_device_info([1u8; 6].into()),
1219            &fake_spectrum_management_support_empty(),
1220        ));
1221        (inspector, sme_inspect)
1222    }
1223
1224    #[test]
1225    fn test_scan_scheduler_routing() {
1226        let mut sched = create_sched();
1227        let (mlme_sink, mut _mlme_stream) = mpsc::unbounded();
1228        let mlme_sink = MlmeSink::new(mlme_sink);
1229
1230        let (responder, _receiver) = Responder::new();
1231        let mut stream = sched.start_scheduled_scan(
1232            fidl_common::ScheduledScanRequest { ..Default::default() },
1233            mlme_sink,
1234            responder,
1235        );
1236        let sched_txn_id = stream.txn_id;
1237        assert_eq!(sched_txn_id, 1);
1238
1239        // Enqueue discovery scan
1240        let req = sched.enqueue_scan_to_discover(passive_discovery_scan(10)).unwrap();
1241        let disc_txn_id = req.txn_id;
1242        assert_eq!(disc_txn_id, 2);
1243
1244        // Send scan result for scheduled scan (txn_id 1)
1245        let bss1 = fake_fidl_bss_description!(Open, ssid: Ssid::try_from("scheduled").unwrap());
1246        sched
1247            .on_mlme_scan_result(fidl_mlme::ScanResult {
1248                txn_id: sched_txn_id,
1249                timestamp_nanos: 1000,
1250                bss: bss1.clone(),
1251            })
1252            .unwrap();
1253
1254        // Send scan result for discovery scan (txn_id 2)
1255        let bss2 = fake_fidl_bss_description!(Open, ssid: Ssid::try_from("discovery").unwrap());
1256        sched
1257            .on_mlme_scan_result(fidl_mlme::ScanResult {
1258                txn_id: disc_txn_id,
1259                timestamp_nanos: 2000,
1260                bss: bss2.clone(),
1261            })
1262            .unwrap();
1263
1264        // Trigger matches available for scheduled scan
1265        let (_inspector, sme_inspect) = sme_inspect();
1266        let cfg = crate::client::ClientConfig::default();
1267        let device_info = test_utils::fake_device_info(*CLIENT_ADDR);
1268        let security_support = wlan_common::test_utils::fake_features::fake_security_support();
1269        sched.on_scheduled_scan_matches_available(
1270            sched_txn_id,
1271            &sme_inspect,
1272            &cfg,
1273            &device_info,
1274            &security_support,
1275        );
1276
1277        // Verify scheduled scan receiver got the matches
1278        assert_matches!(
1279            stream.try_next(),
1280            Ok(Some(scan_results)) => {
1281                let results = wlan_common::scan::read_vmo(scan_results).unwrap();
1282                assert_eq!(results.len(), 1);
1283                let parsed_bss = wlan_common::bss::BssDescription::try_from(results[0].bss_description.clone()).unwrap();
1284                assert_eq!(parsed_bss.ssid, Ssid::try_from("scheduled").unwrap());
1285            }
1286        );
1287
1288        // Verify discovery scan state has the match
1289        if let ScanState::ScanningToDiscover { bss_map, .. } = &sched.current {
1290            assert!(bss_map.contains_key(&Bssid::from(bss2.bssid)));
1291        } else {
1292            panic!("Expected ScanState::ScanningToDiscover");
1293        }
1294    }
1295}