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::{Cbw, 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.channel.primary != existing_bss.channel.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_operating_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_OPERATING_CHANNELS
410///   - This device's operating 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_operating_channels_for_scan(
422    device_info: &fidl_mlme::DeviceInfo,
423    spectrum_management_support: fidl_common::SpectrumManagementSupport,
424    scan_request: &fidl_sme::ScanRequest,
425) -> Vec<u8> {
426    let mut operating_channels: HashSet<u8> = HashSet::new();
427    for band in &device_info.bands {
428        operating_channels.extend(&band.operating_channels);
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<u8> = CANDIDATE_OPERATING_CHANNELS
436        .iter()
437        .filter(|channel| operating_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.is_5ghz() {
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        .map(|channel| channel.primary)
461        .collect();
462
463    if channels.is_empty() {
464        if !requested_channels.is_empty() {
465            warn!("All channels are filtered out. Requested channels: {:?}", requested_channels);
466        } else {
467            warn!("All channels are filtered out.");
468        };
469    }
470
471    channels
472}
473
474// The following constructs the Channel list at runtime once and leaks its contents
475// as a static reference. Firmware will reject channels if they are not allowed by
476// the current regulatory region.
477static CANDIDATE_OPERATING_CHANNELS: LazyLock<&'static [Channel]> = LazyLock::new(|| {
478    #[rustfmt::skip]
479    let channels = vec![
480        // 2.4 GHz
481        1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
482        // 5 GHz
483        36, 40, 44, 48, 52, 56, 60, 64, 100, 104, 108,
484        112, 116, 120, 124, 128, 132, 136, 140, 144,
485        149, 153, 157, 161, 165,
486    ];
487
488    channels.iter().map(|primary| Channel::new(*primary, Cbw::Cbw20)).collect::<Vec<_>>().leak()
489});
490
491#[cfg(test)]
492mod tests {
493    use super::*;
494    use crate::test_utils;
495    use assert_matches::assert_matches;
496    use fuchsia_inspect::Inspector;
497
498    use ieee80211::MacAddr;
499    use regex::bytes::Regex;
500    use std::fmt::Write;
501    use std::sync::LazyLock;
502    use test_case::test_case;
503    use wlan_common::test_utils::fake_capabilities::fake_5ghz_band_capability;
504    use wlan_common::test_utils::fake_features::fake_spectrum_management_support_empty;
505    use wlan_common::{fake_bss_description, fake_fidl_bss_description};
506
507    static CLIENT_ADDR: LazyLock<MacAddr> =
508        LazyLock::new(|| [0x7A, 0xE7, 0x76, 0xD9, 0xF2, 0x67].into());
509
510    impl ScheduledScanReceiver {
511        pub(crate) fn try_next(&mut self) -> Result<Option<fidl::Vmo>, mpsc::TryRecvError> {
512            let res = self.scan_results_receiver.try_next();
513            if let Ok(None) = res {
514                self.stopped_by_firmware = true;
515            }
516            res
517        }
518    }
519
520    fn passive_discovery_scan(token: i32) -> DiscoveryScan<i32> {
521        DiscoveryScan::new(
522            token,
523            fidl_sme::ScanRequest::Passive(fidl_sme::PassiveScanRequest { channels: vec![] }),
524        )
525    }
526
527    #[test]
528    fn discovery_scan() {
529        let mut sched = create_sched();
530        let _next_txn_id = 0;
531        let (_inspector, sme_inspect) = sme_inspect();
532        let req = sched
533            .enqueue_scan_to_discover(passive_discovery_scan(10))
534            .expect("expected a ScanRequest");
535        let txn_id = req.txn_id;
536        sched
537            .on_mlme_scan_result(fidl_mlme::ScanResult {
538                txn_id,
539                timestamp_nanos: zx::MonotonicInstant::get().into_nanos(),
540                bss: fidl_ieee80211::BssDescription {
541                    bssid: [1; 6],
542                    ..fake_fidl_bss_description!(Open, ssid: Ssid::try_from("foo").unwrap())
543                },
544            })
545            .expect("expect scan result received");
546        assert_matches!(
547            sched.on_mlme_scan_result(fidl_mlme::ScanResult {
548                txn_id: txn_id + 100, // mismatching transaction id
549                timestamp_nanos: zx::MonotonicInstant::get().into_nanos(),
550                bss: fidl_ieee80211::BssDescription {
551                    bssid: [2; 6],
552                    ..fake_fidl_bss_description!(Open, ssid: Ssid::try_from("bar").unwrap())
553                },
554            },),
555            Err(Error::ScanResultWrongTxnId)
556        );
557        sched
558            .on_mlme_scan_result(fidl_mlme::ScanResult {
559                txn_id,
560                timestamp_nanos: zx::MonotonicInstant::get().into_nanos(),
561                bss: fidl_ieee80211::BssDescription {
562                    bssid: [3; 6],
563                    ..fake_fidl_bss_description!(Open, ssid: Ssid::try_from("qux").unwrap())
564                },
565            })
566            .expect("expect scan result received");
567        let (scan_end, mlme_req) = assert_matches!(
568            sched.on_mlme_scan_end(
569                fidl_mlme::ScanEnd { txn_id, code: fidl_mlme::ScanResultCode::Success },
570                &sme_inspect),
571            Ok((scan_end, mlme_req)) => (scan_end, mlme_req)
572        );
573        assert!(mlme_req.is_none());
574        let (tokens, bss_description_list) = assert_matches!(
575            scan_end,
576            ScanEnd {
577                tokens,
578                result_code: fidl_mlme::ScanResultCode::Success,
579                bss_description_list
580            } => (tokens, bss_description_list),
581            "expected discovery scan to be completed successfully"
582        );
583        assert_eq!(vec![10], tokens);
584        let mut ssid_list =
585            bss_description_list.into_iter().map(|bss| bss.ssid).collect::<Vec<_>>();
586        ssid_list.sort();
587        assert_eq!(vec![Ssid::try_from("foo").unwrap(), Ssid::try_from("qux").unwrap()], ssid_list);
588    }
589
590    #[test_case(vec![
591        fake_fidl_bss_description!(Open, ssid: Ssid::try_from("bar").unwrap()),
592        fake_fidl_bss_description!(Open, ssid: Ssid::try_from("baz").unwrap()),
593    ], vec![fake_bss_description!(Open, ssid: Ssid::try_from("baz").unwrap())] ;
594                "when latest BSS Description is new")]
595    #[test_case(vec![
596        fake_fidl_bss_description!(Open, rssi_dbm: -36, channel: Channel::new(149, Cbw::Cbw20)),
597        fake_fidl_bss_description!(Open, rssi_dbm: -84, channel: Channel::new(165, Cbw::Cbw20)),
598    ], vec![fake_bss_description!(Open, rssi_dbm: -36, channel: Channel::new(149, Cbw::Cbw20))] ;
599                "when strong signal is first")]
600    #[test_case(vec![
601        fake_fidl_bss_description!(Open, rssi_dbm: -84, channel: Channel::new(64, Cbw::Cbw20)),
602        fake_fidl_bss_description!(Open, rssi_dbm: -36, channel: Channel::new(50, Cbw::Cbw20)),
603        fake_fidl_bss_description!(Open, rssi_dbm: -80, channel: Channel::new(36, Cbw::Cbw20)),
604    ], vec![fake_bss_description!(Open, rssi_dbm: -36, channel: Channel::new(50, Cbw::Cbw20))];
605                "when strong signal is middle")]
606    #[test_case(vec![
607        fake_fidl_bss_description!(Open, rssi_dbm: -84, channel: Channel::new(64, Cbw::Cbw20)),
608        fake_fidl_bss_description!(Open, rssi_dbm: -80, channel: Channel::new(36, Cbw::Cbw20)),
609        fake_fidl_bss_description!(Open, rssi_dbm: -36, channel: Channel::new(50, Cbw::Cbw20)),
610    ], vec![fake_bss_description!(Open, rssi_dbm: -36, channel: Channel::new(50, Cbw::Cbw20))];
611                "when strong signal is last")]
612    #[test_case(vec![
613        fake_fidl_bss_description!(Open, rssi_dbm: -84, ssid: Ssid::try_from("bar").unwrap(),
614                                   channel: Channel::new(149, Cbw::Cbw20)),
615        fake_fidl_bss_description!(Open, rssi_dbm: -36, ssid: Ssid::try_from("bar").unwrap(),
616                                   channel: Channel::new(165, Cbw::Cbw20)),
617        fake_fidl_bss_description!(Open, rssi_dbm: -40, ssid: Ssid::try_from("baz").unwrap(),
618                                   channel: Channel::new(165, Cbw::Cbw20)),
619    ], vec![fake_bss_description!(Open, rssi_dbm: -40, ssid: Ssid::try_from("baz").unwrap(),
620                                  channel: Channel::new(165, Cbw::Cbw20))];
621                "overwrite latest chosen channel")]
622    fn deduplicate_by_bssid(
623        bss_description_list_from_mlme: Vec<fidl_ieee80211::BssDescription>,
624        returned_bss_description_list: Vec<BssDescription>,
625    ) {
626        let mut sched = create_sched();
627        let _next_txn_id = 0;
628        let (_inspector, sme_inspect) = sme_inspect();
629        let req = sched
630            .enqueue_scan_to_discover(passive_discovery_scan(10))
631            .expect("expected a ScanRequest");
632        let txn_id = req.txn_id;
633        for bss in bss_description_list_from_mlme {
634            sched
635                .on_mlme_scan_result(fidl_mlme::ScanResult {
636                    txn_id,
637                    timestamp_nanos: zx::MonotonicInstant::get().into_nanos(),
638                    bss,
639                })
640                .expect("expect scan result received");
641        }
642        let (scan_end, mlme_req) = assert_matches!(
643            sched.on_mlme_scan_end(
644                fidl_mlme::ScanEnd { txn_id, code: fidl_mlme::ScanResultCode::Success },
645                &sme_inspect),
646            Ok((scan_end, mlme_req)) => (scan_end, mlme_req)
647        );
648        assert!(mlme_req.is_none());
649        let (tokens, bss_description_list) = assert_matches!(
650            scan_end,
651            ScanEnd {
652                tokens,
653                result_code: fidl_mlme::ScanResultCode::Success,
654                bss_description_list
655            } => (tokens, bss_description_list),
656            "expected discovery scan to be completed successfully"
657        );
658        assert_eq!(vec![10], tokens);
659        assert_eq!(bss_description_list, returned_bss_description_list);
660    }
661
662    #[test]
663    fn discovery_scan_merge_ies() {
664        let mut sched = create_sched();
665        let _next_txn_id = 0;
666        let (_inspector, sme_inspect) = sme_inspect();
667        let req = sched
668            .enqueue_scan_to_discover(passive_discovery_scan(10))
669            .expect("expected a ScanRequest");
670        let txn_id = req.txn_id;
671
672        let mut bss = fake_fidl_bss_description!(Open, ssid: Ssid::try_from("ssid").unwrap());
673        // Add an extra IE so we can distinguish this result.
674        let ie_marker1 = &[0xdd, 0x07, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee];
675        bss.ies.extend_from_slice(ie_marker1);
676        sched
677            .on_mlme_scan_result(fidl_mlme::ScanResult {
678                txn_id,
679                timestamp_nanos: zx::MonotonicInstant::get().into_nanos(),
680                bss,
681            })
682            .expect("expect scan result received");
683
684        let mut bss = fake_fidl_bss_description!(Open, ssid: Ssid::try_from("ssid").unwrap());
685        // Add an extra IE so we can distinguish this result.
686        let ie_marker2 = &[0xdd, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff];
687        bss.ies.extend_from_slice(ie_marker2);
688        sched
689            .on_mlme_scan_result(fidl_mlme::ScanResult {
690                txn_id,
691                timestamp_nanos: zx::MonotonicInstant::get().into_nanos(),
692                bss,
693            })
694            .expect("expect scan result received");
695        let (scan_end, mlme_req) = assert_matches!(
696            sched.on_mlme_scan_end(
697                fidl_mlme::ScanEnd { txn_id, code: fidl_mlme::ScanResultCode::Success },
698                &sme_inspect),
699            Ok((scan_end, mlme_req)) => (scan_end, mlme_req)
700        );
701        assert!(mlme_req.is_none());
702        let (tokens, bss_description_list) = assert_matches!(
703            scan_end,
704            ScanEnd {
705                tokens,
706                result_code: fidl_mlme::ScanResultCode::Success,
707                bss_description_list
708            } => (tokens, bss_description_list),
709            "expected discovery scan to be completed successfully"
710        );
711        assert_eq!(vec![10], tokens);
712
713        assert_eq!(bss_description_list.len(), 1);
714        // Verify that both IEs are processed.
715        assert!(slice_contains(bss_description_list[0].ies(), ie_marker1));
716        assert!(slice_contains(bss_description_list[0].ies(), ie_marker2));
717    }
718
719    fn slice_contains(slice: &[u8], subslice: &[u8]) -> bool {
720        // https://github.com/rust-lang/regex/issues/451#issuecomment-367987989
721        let re = {
722            let mut re_string = String::with_capacity(6 + subslice.len() * 4);
723            re_string += "(?-u:";
724            for b in subslice {
725                write!(re_string, "\\x{b:02X}").unwrap();
726            }
727            re_string += ")";
728            Regex::new(&re_string).unwrap()
729        };
730        re.is_match(slice)
731    }
732
733    #[test_case(&[1, 2, 3], &[] => true; "vacuous")]
734    #[test_case(&[1, 2, 3], &[1u8] => true; "one byte")]
735    #[test_case(&[1, 2, 3], &[2u8, 3] => true; "multiple bytes")]
736    #[test_case(&[1, 1, 1], &[1u8, 1] => true; "multiple matches")]
737    #[test_case(&[1, 2, 3], &[0u8] => false; "no match")]
738    #[test_case(&[1, 2, 3], &[1u8, 2, 3, 4] => false; "too large")]
739    #[test_case(&[0x87, 0x77, 0x78], &[0x77, 0x77] => false; "misaligned match")]
740    fn slice_contains_test(slice: &[u8], subslice: &[u8]) -> bool {
741        slice_contains(slice, subslice)
742    }
743
744    #[test]
745    fn test_passive_discovery_scan_args() {
746        let mut sched = create_sched();
747        let _next_txn_id = 0;
748        let req = sched
749            .enqueue_scan_to_discover(passive_discovery_scan(10))
750            .expect("expected a ScanRequest");
751        assert_eq!(req.txn_id, 1);
752        assert_eq!(req.scan_type, fidl_mlme::ScanTypes::Passive);
753        assert_eq!(
754            req.channel_list.into_iter().collect::<HashSet<_>>(),
755            CANDIDATE_OPERATING_CHANNELS.iter().map(|c| c.primary).collect::<HashSet<_>>()
756        );
757        assert_eq!(req.ssid_list, Vec::<Vec<u8>>::new());
758        assert_eq!(req.probe_delay, 0);
759        assert_eq!(req.min_channel_time, 200);
760        assert_eq!(req.max_channel_time, 200);
761    }
762
763    #[test_case(true, HashSet::from([1, 36, 165]); "dfs_enabled")]
764    #[test_case(false, HashSet::from([1]); "dfs_disabled")]
765    fn test_active_discovery_scan_args_empty(dfs_supported: bool, expected_channels: HashSet<u8>) {
766        let device_info = device_info_with_channel(vec![1, 36, 165]);
767        let mut spectrum_management = fake_spectrum_management_support_empty();
768        if dfs_supported {
769            spectrum_management.dfs.get_or_insert_with(Default::default).supported = Some(true);
770        }
771        let mut sched: ScanScheduler<i32> =
772            ScanScheduler::new(Arc::new(device_info), spectrum_management);
773        let _next_txn_id = 0;
774        let scan_cmd = DiscoveryScan::new(
775            10,
776            fidl_sme::ScanRequest::Active(fidl_sme::ActiveScanRequest {
777                ssids: vec![],
778                channels: vec![],
779            }),
780        );
781        let req = sched.enqueue_scan_to_discover(scan_cmd).expect("expected a ScanRequest");
782
783        assert_eq!(req.txn_id, 1);
784        assert_eq!(req.scan_type, fidl_mlme::ScanTypes::Active);
785        assert_eq!(req.channel_list.into_iter().collect::<HashSet<_>>(), expected_channels);
786        assert_eq!(req.ssid_list, Vec::<Vec<u8>>::new());
787        assert_eq!(req.probe_delay, 5);
788        assert_eq!(req.min_channel_time, 75);
789        assert_eq!(req.max_channel_time, 75);
790    }
791
792    #[test]
793    fn test_active_discovery_scan_args_filled() {
794        let device_info = device_info_with_channel(vec![1, 36, 165]);
795        let mut sched: ScanScheduler<i32> =
796            ScanScheduler::new(Arc::new(device_info), fake_spectrum_management_support_empty());
797        let _next_txn_id = 0;
798        let ssid1: Vec<u8> = Ssid::try_from("ssid1").unwrap().into();
799        let ssid2: Vec<u8> = Ssid::try_from("ssid2").unwrap().into();
800        let scan_cmd = DiscoveryScan::new(
801            10,
802            fidl_sme::ScanRequest::Active(fidl_sme::ActiveScanRequest {
803                ssids: vec![ssid1.clone(), ssid2.clone()],
804                // TODO(https://fxbug.dev/42169913): SME silently ignores unsupported channels
805                channels: vec![1, 20, 100],
806            }),
807        );
808        let req = sched.enqueue_scan_to_discover(scan_cmd).expect("expected a ScanRequest");
809
810        assert_eq!(req.txn_id, 1);
811        assert_eq!(req.scan_type, fidl_mlme::ScanTypes::Active);
812        assert_eq!(req.channel_list, vec![1]);
813        assert_eq!(req.ssid_list, vec![ssid1, ssid2]);
814        assert_eq!(req.probe_delay, 5);
815        assert_eq!(req.min_channel_time, 75);
816        assert_eq!(req.max_channel_time, 75);
817    }
818
819    #[test]
820    fn test_passive_discovery_scan_args_filled() {
821        // Set up the device that can operate on channels 1, 36, and 165.
822        let device_info = device_info_with_channel(vec![1, 36, 165]);
823        let mut sched: ScanScheduler<i32> =
824            ScanScheduler::new(Arc::new(device_info), fake_spectrum_management_support_empty());
825        // Request a scan using only some of the supported channels.
826        let scan_cmd = DiscoveryScan::new(
827            10,
828            fidl_sme::ScanRequest::Passive(fidl_sme::PassiveScanRequest { channels: vec![1, 36] }),
829        );
830        let _next_txn_id = 0;
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::Passive);
835        // Verify that only the requested channels are included.
836        assert_eq!(req.channel_list.into_iter().collect::<HashSet<_>>(), HashSet::from([1, 36]));
837        assert_eq!(req.ssid_list, Vec::<Vec<u8>>::new());
838        assert_eq!(req.probe_delay, 0);
839        assert_eq!(req.min_channel_time, 200);
840        assert_eq!(req.max_channel_time, 200);
841    }
842
843    #[test]
844    fn test_passive_discovery_scan_args_unsupported_filtered() {
845        let device_info = device_info_with_channel(vec![1, 36]);
846        let mut sched: ScanScheduler<i32> =
847            ScanScheduler::new(Arc::new(device_info), fake_spectrum_management_support_empty());
848        let _next_txn_id = 0;
849        // Request a scan that includes a channel not supported by the device.
850        let scan_cmd = DiscoveryScan::new(
851            10,
852            fidl_sme::ScanRequest::Passive(fidl_sme::PassiveScanRequest {
853                channels: vec![1, 6, 36],
854            }),
855        );
856        let req = sched.enqueue_scan_to_discover(scan_cmd).expect("expected a ScanRequest");
857
858        assert_eq!(req.txn_id, 1);
859        assert_eq!(req.scan_type, fidl_mlme::ScanTypes::Passive);
860        // Verify that the unsupported channel 6 was filtered out.
861        assert_eq!(req.channel_list.into_iter().collect::<HashSet<_>>(), HashSet::from([1, 36]));
862    }
863
864    #[test]
865    fn test_passive_discovery_scan_args_invalid_filtered() {
866        let device_info = device_info_with_channel(vec![1, 200]);
867        let mut sched: ScanScheduler<i32> =
868            ScanScheduler::new(Arc::new(device_info), fake_spectrum_management_support_empty());
869        let _next_txn_id = 0;
870        // Request a scan that includes an invalid channel.
871        let scan_cmd = DiscoveryScan::new(
872            10,
873            fidl_sme::ScanRequest::Passive(fidl_sme::PassiveScanRequest { channels: vec![1, 200] }),
874        );
875        let req = sched.enqueue_scan_to_discover(scan_cmd).expect("expected a ScanRequest");
876
877        assert_eq!(req.txn_id, 1);
878        assert_eq!(req.scan_type, fidl_mlme::ScanTypes::Passive);
879        // Verify that the invalid channel 200 was filtered out and the valid channel is included.
880        assert_eq!(req.channel_list, vec![1]);
881    }
882
883    #[test]
884    fn test_passive_discovery_scan_args_empty_list() {
885        let device_info = device_info_with_channel(vec![1, 36, 165]);
886        let mut sched: ScanScheduler<i32> =
887            ScanScheduler::new(Arc::new(device_info), fake_spectrum_management_support_empty());
888        let _next_txn_id = 0;
889        let scan_cmd = DiscoveryScan::new(
890            10,
891            fidl_sme::ScanRequest::Passive(fidl_sme::PassiveScanRequest { channels: vec![] }),
892        );
893        let req = sched.enqueue_scan_to_discover(scan_cmd).expect("expected a ScanRequest");
894
895        assert_eq!(req.txn_id, 1);
896        assert_eq!(req.scan_type, fidl_mlme::ScanTypes::Passive);
897        assert_eq!(
898            req.channel_list.into_iter().collect::<HashSet<_>>(),
899            HashSet::from([1, 36, 165])
900        );
901    }
902
903    #[test]
904    fn test_discovery_scans_dedupe_single_group() {
905        let mut sched = create_sched();
906        let _next_txn_id = 0;
907        let (_inspector, sme_inspect) = sme_inspect();
908
909        // Post one scan command, expect a message to MLME
910        let mlme_req = sched
911            .enqueue_scan_to_discover(passive_discovery_scan(10))
912            .expect("expected a ScanRequest");
913        let txn_id = mlme_req.txn_id;
914
915        // Report a scan result
916        sched
917            .on_mlme_scan_result(fidl_mlme::ScanResult {
918                txn_id,
919                timestamp_nanos: zx::MonotonicInstant::get().into_nanos(),
920                bss: fidl_ieee80211::BssDescription {
921                    bssid: [1; 6],
922                    ..fake_fidl_bss_description!(Open, ssid: Ssid::try_from("foo").unwrap())
923                },
924            })
925            .expect("expect scan result received");
926
927        // Post another command. It should not issue another request to the MLME since
928        // there is already an on-going one
929        assert!(sched.enqueue_scan_to_discover(passive_discovery_scan(20)).is_none());
930
931        // Report another scan result and the end of the scan transaction
932        sched
933            .on_mlme_scan_result(fidl_mlme::ScanResult {
934                txn_id,
935                timestamp_nanos: zx::MonotonicInstant::get().into_nanos(),
936                bss: fidl_ieee80211::BssDescription {
937                    bssid: [2; 6],
938                    ..fake_fidl_bss_description!(Open, ssid: Ssid::try_from("bar").unwrap())
939                },
940            })
941            .expect("expect scan result received");
942        let (scan_end, mlme_req) = assert_matches!(
943            sched.on_mlme_scan_end(
944                fidl_mlme::ScanEnd { txn_id, code: fidl_mlme::ScanResultCode::Success },
945                &sme_inspect),
946            Ok((scan_end, mlme_req)) => (scan_end, mlme_req)
947        );
948
949        // We don't expect another request to the MLME
950        assert!(mlme_req.is_none());
951
952        // Expect a discovery result with both tokens and both SSIDs
953        assert_discovery_scan_result(
954            scan_end,
955            vec![10, 20],
956            vec![Ssid::try_from("bar").unwrap(), Ssid::try_from("foo").unwrap()],
957        );
958    }
959
960    #[test]
961    fn test_discovery_scans_dedupe_multiple_groups() {
962        let mut sched = create_sched();
963        let (_inspector, sme_inspect) = sme_inspect();
964
965        // Post a passive scan command, expect a message to MLME
966        let mlme_req = sched
967            .enqueue_scan_to_discover(passive_discovery_scan(10))
968            .expect("expected a ScanRequest");
969        let txn_id = mlme_req.txn_id;
970
971        // Post an active scan command, which should be enqueued until the previous one finishes
972        let scan_cmd = DiscoveryScan::new(
973            20,
974            fidl_sme::ScanRequest::Active(fidl_sme::ActiveScanRequest {
975                ssids: vec![],
976                channels: vec![],
977            }),
978        );
979        assert!(sched.enqueue_scan_to_discover(scan_cmd).is_none());
980
981        // Post a passive scan command. It should be merged with the ongoing one and so should not
982        // issue another request to MLME
983        assert!(sched.enqueue_scan_to_discover(passive_discovery_scan(30)).is_none());
984
985        // Post an active scan command. It should be merged with the active scan command that's
986        // still enqueued, and so should not issue another request to MLME
987        let scan_cmd = DiscoveryScan::new(
988            40,
989            fidl_sme::ScanRequest::Active(fidl_sme::ActiveScanRequest {
990                ssids: vec![],
991                channels: vec![],
992            }),
993        );
994        assert!(sched.enqueue_scan_to_discover(scan_cmd).is_none());
995
996        // Report scan result and scan end
997        sched
998            .on_mlme_scan_result(fidl_mlme::ScanResult {
999                txn_id,
1000                timestamp_nanos: zx::MonotonicInstant::get().into_nanos(),
1001                bss: fidl_ieee80211::BssDescription {
1002                    bssid: [1; 6],
1003                    ..fake_fidl_bss_description!(Open, ssid: Ssid::try_from("foo").unwrap())
1004                },
1005            })
1006            .expect("expect scan result received");
1007        let (scan_end, mlme_req) = assert_matches!(
1008            sched.on_mlme_scan_end(
1009                fidl_mlme::ScanEnd { txn_id, code: fidl_mlme::ScanResultCode::Success },
1010                &sme_inspect),
1011            Ok((scan_end, mlme_req)) => (scan_end, mlme_req)
1012        );
1013
1014        // Expect discovery result with 1st and 3rd tokens
1015        assert_discovery_scan_result(scan_end, vec![10, 30], vec![Ssid::try_from("foo").unwrap()]);
1016
1017        // Next mlme_req should be an active scan request
1018        assert!(mlme_req.is_some());
1019        let mlme_req = mlme_req.unwrap();
1020        assert_eq!(mlme_req.scan_type, fidl_mlme::ScanTypes::Active);
1021        let txn_id = mlme_req.txn_id;
1022
1023        // Report scan result and scan end
1024        sched
1025            .on_mlme_scan_result(fidl_mlme::ScanResult {
1026                txn_id,
1027                timestamp_nanos: zx::MonotonicInstant::get().into_nanos(),
1028                bss: fidl_ieee80211::BssDescription {
1029                    bssid: [2; 6],
1030                    ..fake_fidl_bss_description!(Open, ssid: Ssid::try_from("bar").unwrap())
1031                },
1032            })
1033            .expect("expect scan result received");
1034        let (scan_end, mlme_req) = assert_matches!(
1035            sched.on_mlme_scan_end(
1036                fidl_mlme::ScanEnd { txn_id, code: fidl_mlme::ScanResultCode::Success },
1037                &sme_inspect),
1038            Ok((scan_end, mlme_req)) => (scan_end, mlme_req)
1039        );
1040
1041        // Expect discovery result with 2nd and 4th tokens
1042        assert_discovery_scan_result(scan_end, vec![20, 40], vec![Ssid::try_from("bar").unwrap()]);
1043
1044        // We don't expect another request to the MLME
1045        assert!(mlme_req.is_none());
1046    }
1047
1048    #[test]
1049    fn test_discovery_scan_result_wrong_txn_id() {
1050        let mut sched = create_sched();
1051        let _next_txn_id = 0;
1052
1053        // Post a passive scan command, expect a message to MLME
1054        let mlme_req = sched
1055            .enqueue_scan_to_discover(passive_discovery_scan(10))
1056            .expect("expected a ScanRequest");
1057        let txn_id = mlme_req.txn_id;
1058
1059        // Report scan result with wrong txn id
1060        assert_matches!(
1061            sched.on_mlme_scan_result(fidl_mlme::ScanResult {
1062                txn_id: txn_id + 1,
1063                timestamp_nanos: zx::MonotonicInstant::get().into_nanos(),
1064                bss: fidl_ieee80211::BssDescription {
1065                    bssid: [1; 6],
1066                    ..fake_fidl_bss_description!(Open, ssid: Ssid::try_from("foo").unwrap())
1067                },
1068            },),
1069            Err(Error::ScanResultWrongTxnId)
1070        );
1071    }
1072
1073    #[test]
1074    fn test_discovery_scan_result_not_scanning() {
1075        let mut sched = create_sched();
1076        assert_matches!(
1077            sched.on_mlme_scan_result(fidl_mlme::ScanResult {
1078                txn_id: 0,
1079                timestamp_nanos: zx::MonotonicInstant::get().into_nanos(),
1080                bss: fidl_ieee80211::BssDescription {
1081                    bssid: [1; 6],
1082                    ..fake_fidl_bss_description!(Open, ssid: Ssid::try_from("foo").unwrap())
1083                },
1084            },),
1085            Err(Error::ScanResultNotScanning)
1086        );
1087    }
1088
1089    #[test]
1090    fn test_discovery_scan_end_wrong_txn_id() {
1091        let mut sched = create_sched();
1092        let _next_txn_id = 0;
1093        let (_inspector, sme_inspect) = sme_inspect();
1094
1095        // Post a passive scan command, expect a message to MLME
1096        let mlme_req = sched
1097            .enqueue_scan_to_discover(passive_discovery_scan(10))
1098            .expect("expected a ScanRequest");
1099        let txn_id = mlme_req.txn_id;
1100
1101        assert_matches!(
1102            sched.on_mlme_scan_end(
1103                fidl_mlme::ScanEnd { txn_id: txn_id + 1, code: fidl_mlme::ScanResultCode::Success },
1104                &sme_inspect
1105            ),
1106            Err(Error::ScanEndWrongTxnId)
1107        );
1108    }
1109
1110    #[test]
1111    fn test_discovery_scan_end_not_scanning() {
1112        let mut sched = create_sched();
1113        let _next_txn_id = 0;
1114        let (_inspector, sme_inspect) = sme_inspect();
1115        assert_matches!(
1116            sched.on_mlme_scan_end(
1117                fidl_mlme::ScanEnd { txn_id: 0, code: fidl_mlme::ScanResultCode::Success },
1118                &sme_inspect
1119            ),
1120            Err(Error::ScanEndNotScanning)
1121        );
1122    }
1123
1124    fn assert_discovery_scan_result(
1125        scan_end: ScanEnd<i32>,
1126        expected_tokens: Vec<i32>,
1127        expected_ssids: Vec<Ssid>,
1128    ) {
1129        let (tokens, bss_description_list) = assert_matches!(
1130            scan_end,
1131            ScanEnd {
1132                tokens,
1133                result_code: fidl_mlme::ScanResultCode::Success,
1134                bss_description_list
1135            } => (tokens, bss_description_list),
1136            "expected discovery scan to be completed successfully"
1137        );
1138        assert_eq!(tokens, expected_tokens);
1139        let mut ssid_list =
1140            bss_description_list.into_iter().map(|bss| bss.ssid.clone()).collect::<Vec<_>>();
1141        ssid_list.sort();
1142        assert_eq!(ssid_list, expected_ssids);
1143    }
1144
1145    fn create_sched() -> ScanScheduler<i32> {
1146        ScanScheduler::new(
1147            Arc::new(test_utils::fake_device_info(*CLIENT_ADDR)),
1148            fake_spectrum_management_support_empty(),
1149        )
1150    }
1151
1152    fn device_info_with_channel(operating_channels: Vec<u8>) -> fidl_mlme::DeviceInfo {
1153        fidl_mlme::DeviceInfo {
1154            bands: vec![fidl_mlme::BandCapability {
1155                operating_channels,
1156                ..fake_5ghz_band_capability()
1157            }],
1158            ..test_utils::fake_device_info(*CLIENT_ADDR)
1159        }
1160    }
1161
1162    fn sme_inspect() -> (Inspector, Arc<inspect::SmeTree>) {
1163        let inspector = Inspector::default();
1164        let sme_inspect = Arc::new(inspect::SmeTree::new(
1165            inspector.clone(),
1166            inspector.root().create_child("usme"),
1167            &test_utils::fake_device_info([1u8; 6].into()),
1168            &fake_spectrum_management_support_empty(),
1169        ));
1170        (inspector, sme_inspect)
1171    }
1172
1173    #[test]
1174    fn test_scan_scheduler_routing() {
1175        let mut sched = create_sched();
1176        let (mlme_sink, mut _mlme_stream) = mpsc::unbounded();
1177        let mlme_sink = MlmeSink::new(mlme_sink);
1178
1179        let (responder, _receiver) = Responder::new();
1180        let mut stream = sched.start_scheduled_scan(
1181            fidl_common::ScheduledScanRequest { ..Default::default() },
1182            mlme_sink,
1183            responder,
1184        );
1185        let sched_txn_id = stream.txn_id;
1186        assert_eq!(sched_txn_id, 1);
1187
1188        // Enqueue discovery scan
1189        let req = sched.enqueue_scan_to_discover(passive_discovery_scan(10)).unwrap();
1190        let disc_txn_id = req.txn_id;
1191        assert_eq!(disc_txn_id, 2);
1192
1193        // Send scan result for scheduled scan (txn_id 1)
1194        let bss1 = fake_fidl_bss_description!(Open, ssid: Ssid::try_from("scheduled").unwrap());
1195        sched
1196            .on_mlme_scan_result(fidl_mlme::ScanResult {
1197                txn_id: sched_txn_id,
1198                timestamp_nanos: 1000,
1199                bss: bss1.clone(),
1200            })
1201            .unwrap();
1202
1203        // Send scan result for discovery scan (txn_id 2)
1204        let bss2 = fake_fidl_bss_description!(Open, ssid: Ssid::try_from("discovery").unwrap());
1205        sched
1206            .on_mlme_scan_result(fidl_mlme::ScanResult {
1207                txn_id: disc_txn_id,
1208                timestamp_nanos: 2000,
1209                bss: bss2.clone(),
1210            })
1211            .unwrap();
1212
1213        // Trigger matches available for scheduled scan
1214        let (_inspector, sme_inspect) = sme_inspect();
1215        let cfg = crate::client::ClientConfig::default();
1216        let device_info = test_utils::fake_device_info(*CLIENT_ADDR);
1217        let security_support = wlan_common::test_utils::fake_features::fake_security_support();
1218        sched.on_scheduled_scan_matches_available(
1219            sched_txn_id,
1220            &sme_inspect,
1221            &cfg,
1222            &device_info,
1223            &security_support,
1224        );
1225
1226        // Verify scheduled scan receiver got the matches
1227        assert_matches!(
1228            stream.try_next(),
1229            Ok(Some(scan_results)) => {
1230                let results = wlan_common::scan::read_vmo(scan_results).unwrap();
1231                assert_eq!(results.len(), 1);
1232                let parsed_bss = wlan_common::bss::BssDescription::try_from(results[0].bss_description.clone()).unwrap();
1233                assert_eq!(parsed_bss.ssid, Ssid::try_from("scheduled").unwrap());
1234            }
1235        );
1236
1237        // Verify discovery scan state has the match
1238        if let ScanState::ScanningToDiscover { bss_map, .. } = &sched.current {
1239            assert!(bss_map.contains_key(&Bssid::from(bss2.bssid)));
1240        } else {
1241            panic!("Expected ScanState::ScanningToDiscover");
1242        }
1243    }
1244}