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