wlancfg_lib/client/scan/
fidl_conversion.rs

1// Copyright 2022 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::types;
6use anyhow::{Error, format_err};
7use fidl::prelude::*;
8use futures::stream::TryStreamExt;
9use ieee80211::MacAddrBytes;
10use log::{debug, info};
11use measure_tape_for_scan_result::Measurable as _;
12use {fidl_fuchsia_wlan_policy as fidl_policy, fidl_fuchsia_wlan_sme as fidl_sme};
13
14// TODO(https://fxbug.dev/42160765): Remove this.
15// Size of FIDL message header and FIDL error-wrapped vector header
16const FIDL_HEADER_AND_ERR_WRAPPED_VEC_HEADER_SIZE: usize = 56;
17
18/// Convert the protection type we receive from the SME in scan results to the Policy layer
19/// security type. This function should only be used when converting to results for the public
20/// FIDL API, and not for internal use within Policy, where we should prefer the detailed SME
21/// security types.
22fn fidl_security_from_sme_protection(
23    protection: fidl_sme::Protection,
24    wpa3_supported: bool,
25) -> Option<fidl_policy::SecurityType> {
26    use fidl_policy::SecurityType;
27    use fidl_sme::Protection::*;
28    match protection {
29        Wpa3Enterprise | Wpa3Personal | Wpa2Wpa3Personal => {
30            Some(if wpa3_supported { SecurityType::Wpa3 } else { SecurityType::Wpa2 })
31        }
32        Wpa2Enterprise
33        | Wpa2Personal
34        | Wpa1Wpa2Personal
35        | Wpa2PersonalTkipOnly
36        | Wpa1Wpa2PersonalTkipOnly => Some(SecurityType::Wpa2),
37        Wpa1 => Some(SecurityType::Wpa),
38        Wep => Some(SecurityType::Wep),
39        // TODO(https://fxbug.dev/462514157): Map Owe and OpenOweTransition to correct security types
40        Owe => Some(SecurityType::None),
41        OpenOweTransition => Some(SecurityType::None),
42        Open => Some(SecurityType::None),
43        Unknown => None,
44    }
45}
46
47#[allow(clippy::ptr_arg, reason = "mass allow for https://fxbug.dev/381896734")]
48pub fn scan_result_to_policy_scan_result(
49    internal_results: &Vec<types::ScanResult>,
50    wpa3_supported: bool,
51) -> Vec<fidl_policy::ScanResult> {
52    let scan_results: Vec<fidl_policy::ScanResult> = internal_results
53        .iter()
54        .filter_map(|internal| {
55            if let Some(security) =
56                fidl_security_from_sme_protection(internal.security_type_detailed, wpa3_supported)
57            {
58                Some(fidl_policy::ScanResult {
59                    id: Some(fidl_policy::NetworkIdentifier {
60                        ssid: internal.ssid.to_vec(),
61                        type_: security,
62                    }),
63                    entries: Some(
64                        internal
65                            .entries
66                            .iter()
67                            .map(|input| {
68                                // Get the frequency. On error, default to Some(0) rather than None
69                                // to protect against consumer code that expects this field to
70                                // always be set.
71                                let frequency = input.channel.get_center_freq().unwrap_or(0);
72                                fidl_policy::Bss {
73                                    bssid: Some(input.bssid.to_array()),
74                                    rssi: Some(input.signal.rssi_dbm),
75                                    frequency: Some(frequency.into()), // u16.into() -> u32
76                                    timestamp_nanos: Some(input.timestamp.into_nanos()),
77                                    ..Default::default()
78                                }
79                            })
80                            .collect(),
81                    ),
82                    compatibility: Some(internal.compatibility),
83                    ..Default::default()
84                })
85            } else {
86                debug!(
87                    "Unknown security type present in scan results ({} BSSs)",
88                    internal.entries.len()
89                );
90                None
91            }
92        })
93        .collect();
94
95    scan_results
96}
97
98/// Send batches of results to the output iterator when getNext() is called on it.
99/// Send empty batch and close the channel when no results are remaining.
100pub async fn send_scan_results_over_fidl(
101    output_iterator: fidl::endpoints::ServerEnd<fidl_policy::ScanResultIteratorMarker>,
102    mut scan_results: &[fidl_policy::ScanResult],
103) -> Result<(), Error> {
104    // Wait to get a request for a chunk of scan results
105    let (mut stream, ctrl) = output_iterator.into_stream_and_control_handle();
106    let mut sent_some_results = false;
107
108    // Verify consumer is expecting results before each batch
109    loop {
110        if let Some(fidl_policy::ScanResultIteratorRequest::GetNext { responder }) =
111            stream.try_next().await?
112        {
113            let mut bytes_used = FIDL_HEADER_AND_ERR_WRAPPED_VEC_HEADER_SIZE;
114            let mut result_count = 0;
115            for result in scan_results {
116                bytes_used += result.measure().num_bytes;
117                if bytes_used > zx::sys::ZX_CHANNEL_MAX_MSG_BYTES as usize {
118                    if result_count == 0 {
119                        return Err(format_err!("Single scan result too large to send via FIDL"));
120                    }
121                    // This result will not fit. Send batch and continue.
122                    break;
123                }
124                result_count += 1;
125            }
126            // It's ok to slice this by index, since we've just calculated the `result_count` above
127            // by iterating through elements of scan_results[]
128            #[expect(clippy::indexing_slicing)]
129            responder.send(Ok(&scan_results[..result_count]))?;
130            // It's ok to slice this by index, since we've just calculated the `result_count` above
131            // by iterating through elements of scan_results[]
132            #[expect(clippy::indexing_slicing)]
133            let remaining_results = &scan_results[result_count..];
134            scan_results = remaining_results;
135            sent_some_results = true;
136
137            // Guarantees empty batch is sent before channel is closed.
138            if result_count == 0 {
139                ctrl.shutdown();
140                return Ok(());
141            }
142        } else {
143            // This will happen if the iterator request stream was closed and we expected to send
144            // another response.
145            if sent_some_results {
146                // Some consumers may not care about all scan results, e.g. if they find the
147                // particular network they were looking for. This is not an error.
148                debug!("Scan result consumer closed channel before consuming all scan results");
149                return Ok(());
150            }
151            return Err(format_err!("Peer closed channel before receiving any scan results"));
152        }
153    }
154}
155
156/// On the next request for results, send an error to the output iterator and
157/// shut it down.
158pub async fn send_scan_error_over_fidl(
159    output_iterator: fidl::endpoints::ServerEnd<fidl_policy::ScanResultIteratorMarker>,
160    error_code: types::ScanError,
161) -> Result<(), fidl::Error> {
162    // Wait to get a request for a chunk of scan results
163    let (mut stream, ctrl) = output_iterator.into_stream_and_control_handle();
164    if let Some(req) = stream.try_next().await? {
165        let fidl_policy::ScanResultIteratorRequest::GetNext { responder } = req;
166        responder.send(Err(error_code))?;
167        ctrl.shutdown();
168    } else {
169        // This will happen if the iterator request stream was closed and we expected to send
170        // another response.
171        info!("Peer closed channel for getting scan results unexpectedly");
172    }
173    Ok(())
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179    use assert_matches::assert_matches;
180    use fuchsia_async as fasync;
181    use futures::task::Poll;
182    use std::pin::pin;
183    use wlan_common::random_fidl_bss_description;
184    use wlan_common::scan::{Compatible, Incompatible};
185    use wlan_common::security::SecurityDescriptor;
186
187    fn generate_test_fidl_data() -> Vec<fidl_policy::ScanResult> {
188        const CENTER_FREQ_CHAN_1: u32 = 2412;
189        const CENTER_FREQ_CHAN_8: u32 = 2447;
190        const CENTER_FREQ_CHAN_11: u32 = 2462;
191        vec![
192            fidl_policy::ScanResult {
193                id: Some(fidl_policy::NetworkIdentifier {
194                    ssid: types::Ssid::try_from("duplicated ssid").unwrap().into(),
195                    type_: fidl_policy::SecurityType::Wpa2,
196                }),
197                entries: Some(vec![
198                    fidl_policy::Bss {
199                        bssid: Some([0, 0, 0, 0, 0, 0]),
200                        rssi: Some(0),
201                        frequency: Some(CENTER_FREQ_CHAN_1),
202                        timestamp_nanos: Some(zx::MonotonicInstant::get().into_nanos()),
203                        ..Default::default()
204                    },
205                    fidl_policy::Bss {
206                        bssid: Some([7, 8, 9, 10, 11, 12]),
207                        rssi: Some(13),
208                        frequency: Some(CENTER_FREQ_CHAN_11),
209                        timestamp_nanos: Some(zx::MonotonicInstant::get().into_nanos()),
210                        ..Default::default()
211                    },
212                ]),
213                compatibility: Some(fidl_policy::Compatibility::Supported),
214                ..Default::default()
215            },
216            fidl_policy::ScanResult {
217                id: Some(fidl_policy::NetworkIdentifier {
218                    ssid: types::Ssid::try_from("unique ssid").unwrap().into(),
219                    type_: fidl_policy::SecurityType::Wpa2,
220                }),
221                entries: Some(vec![fidl_policy::Bss {
222                    bssid: Some([1, 2, 3, 4, 5, 6]),
223                    rssi: Some(7),
224                    frequency: Some(CENTER_FREQ_CHAN_8),
225                    timestamp_nanos: Some(zx::MonotonicInstant::get().into_nanos()),
226                    ..Default::default()
227                }]),
228                compatibility: Some(fidl_policy::Compatibility::Supported),
229                ..Default::default()
230            },
231        ]
232    }
233
234    /// Generate a vector of FIDL scan results, each sized based on the input
235    /// vector parameter. Size, in bytes, must be greater than the baseline scan
236    /// result's size, measure below, and divisible into octets (by 8).
237    fn create_fidl_scan_results_from_size(
238        result_sizes: Vec<usize>,
239    ) -> Vec<fidl_policy::ScanResult> {
240        // Create a baseline result
241        let minimal_scan_result = fidl_policy::ScanResult {
242            id: Some(fidl_policy::NetworkIdentifier {
243                ssid: types::Ssid::empty().into(),
244                type_: fidl_policy::SecurityType::None,
245            }),
246            entries: Some(vec![]),
247            ..Default::default()
248        };
249        let minimal_result_size: usize = minimal_scan_result.measure().num_bytes;
250
251        // Create result with single entry
252        let mut scan_result_with_one_bss = minimal_scan_result.clone();
253        scan_result_with_one_bss.entries = Some(vec![fidl_policy::Bss::default()]);
254
255        // Size of each additional BSS entry to FIDL ScanResult
256        let empty_bss_entry_size: usize =
257            scan_result_with_one_bss.measure().num_bytes - minimal_result_size;
258
259        // Validate size is possible
260        if result_sizes.iter().any(|size| size < &minimal_result_size || !size.is_multiple_of(8)) {
261            panic!(
262                "Invalid size. Requested size must be larger than {minimal_result_size} minimum bytes and divisible into octets (by 8)"
263            );
264        }
265
266        let mut fidl_scan_results = vec![];
267        for size in result_sizes {
268            let mut scan_result = minimal_scan_result.clone();
269
270            let num_bss_for_ap = (size - minimal_result_size) / empty_bss_entry_size;
271            // Every 8 characters for SSID adds 8 bytes (1 octet).
272            let ssid_length =
273                (size - minimal_result_size) - (num_bss_for_ap * empty_bss_entry_size);
274
275            scan_result.id = Some(fidl_policy::NetworkIdentifier {
276                ssid: (0..ssid_length).map(|_| rand::random::<u8>()).collect(),
277                type_: fidl_policy::SecurityType::None,
278            });
279            scan_result.entries = Some(vec![fidl_policy::Bss::default(); num_bss_for_ap]);
280
281            // Validate result measures to expected size.
282            assert_eq!(scan_result.measure().num_bytes, size);
283
284            fidl_scan_results.push(scan_result);
285        }
286        fidl_scan_results
287    }
288
289    #[fuchsia::test]
290    fn scan_result_generate_from_size() {
291        let scan_results = create_fidl_scan_results_from_size(vec![112; 4]);
292        assert_eq!(scan_results.len(), 4);
293        assert!(scan_results.iter().all(|scan_result| scan_result.measure().num_bytes == 112));
294    }
295
296    #[fuchsia::test]
297    fn sme_protection_converts_to_policy_security() {
298        use super::fidl_policy::SecurityType;
299        use super::fidl_sme::Protection;
300        let wpa3_supported = true;
301        let wpa3_not_supported = false;
302        let test_pairs = vec![
303            // Below are pairs when WPA3 is supported.
304            (Protection::Wpa3Enterprise, wpa3_supported, Some(SecurityType::Wpa3)),
305            (Protection::Wpa3Personal, wpa3_supported, Some(SecurityType::Wpa3)),
306            (Protection::Wpa2Wpa3Personal, wpa3_supported, Some(SecurityType::Wpa3)),
307            (Protection::Wpa2Enterprise, wpa3_supported, Some(SecurityType::Wpa2)),
308            (Protection::Wpa2Personal, wpa3_supported, Some(SecurityType::Wpa2)),
309            (Protection::Wpa1Wpa2Personal, wpa3_supported, Some(SecurityType::Wpa2)),
310            (Protection::Wpa2PersonalTkipOnly, wpa3_supported, Some(SecurityType::Wpa2)),
311            (Protection::Wpa1Wpa2PersonalTkipOnly, wpa3_supported, Some(SecurityType::Wpa2)),
312            (Protection::Wpa1, wpa3_supported, Some(SecurityType::Wpa)),
313            (Protection::Wep, wpa3_supported, Some(SecurityType::Wep)),
314            // TODO(https://fxbug.dev/462514157): Map Owe and OpenOweTransition to correct security
315            // types.
316            (Protection::Owe, wpa3_supported, Some(SecurityType::None)),
317            (Protection::OpenOweTransition, wpa3_supported, Some(SecurityType::None)),
318            (Protection::Open, wpa3_supported, Some(SecurityType::None)),
319            (Protection::Unknown, wpa3_supported, None),
320            // Below are pairs when WPA3 is not supported.
321            (Protection::Wpa3Enterprise, wpa3_not_supported, Some(SecurityType::Wpa2)),
322            (Protection::Wpa3Personal, wpa3_not_supported, Some(SecurityType::Wpa2)),
323            (Protection::Wpa2Wpa3Personal, wpa3_not_supported, Some(SecurityType::Wpa2)),
324            (Protection::Wpa2Enterprise, wpa3_not_supported, Some(SecurityType::Wpa2)),
325            (Protection::Wpa2Personal, wpa3_not_supported, Some(SecurityType::Wpa2)),
326            (Protection::Wpa1Wpa2Personal, wpa3_not_supported, Some(SecurityType::Wpa2)),
327            (Protection::Wpa2PersonalTkipOnly, wpa3_not_supported, Some(SecurityType::Wpa2)),
328            (Protection::Wpa1Wpa2PersonalTkipOnly, wpa3_not_supported, Some(SecurityType::Wpa2)),
329            (Protection::Wpa1, wpa3_not_supported, Some(SecurityType::Wpa)),
330            (Protection::Wep, wpa3_not_supported, Some(SecurityType::Wep)),
331            // TODO(https://fxbug.dev/462514157): Map Owe and OpenOweTransition to correct security
332            // types.
333            (Protection::Owe, wpa3_not_supported, Some(SecurityType::None)),
334            (Protection::OpenOweTransition, wpa3_not_supported, Some(SecurityType::None)),
335            (Protection::Open, wpa3_not_supported, Some(SecurityType::None)),
336            (Protection::Unknown, wpa3_not_supported, None),
337        ];
338        for (input, wpa3_capable, output) in test_pairs {
339            assert_eq!(fidl_security_from_sme_protection(input, wpa3_capable), output);
340        }
341    }
342
343    #[fuchsia::test]
344    fn scan_results_converted_correctly() {
345        let fidl_aps = generate_test_fidl_data();
346        let internal_aps = vec![
347            types::ScanResult {
348                ssid: types::Ssid::try_from("duplicated ssid").unwrap(),
349                security_type_detailed: types::SecurityTypeDetailed::Wpa3Personal,
350                entries: vec![
351                    types::Bss {
352                        bssid: types::Bssid::from([0, 0, 0, 0, 0, 0]),
353                        signal: types::Signal { rssi_dbm: 0, snr_db: 1 },
354                        timestamp: zx::MonotonicInstant::from_nanos(
355                            fidl_aps[0].entries.as_ref().unwrap()[0].timestamp_nanos.unwrap(),
356                        ),
357                        channel: types::WlanChan::new(1, types::Cbw::Cbw20),
358                        observation: types::ScanObservation::Passive,
359                        compatibility: Compatible::expect_ok([SecurityDescriptor::WPA3_PERSONAL]),
360                        bss_description: random_fidl_bss_description!(
361                            Wpa3,
362                            bssid: [0, 0, 0, 0, 0, 0],
363                            ssid: types::Ssid::try_from("duplicated ssid").unwrap(),
364                            rssi_dbm: 0,
365                            snr_db: 1,
366                            channel: types::WlanChan::new(1, types::Cbw::Cbw20),
367                        )
368                        .into(),
369                    },
370                    types::Bss {
371                        bssid: types::Bssid::from([7, 8, 9, 10, 11, 12]),
372                        signal: types::Signal { rssi_dbm: 13, snr_db: 3 },
373                        timestamp: zx::MonotonicInstant::from_nanos(
374                            fidl_aps[0].entries.as_ref().unwrap()[1].timestamp_nanos.unwrap(),
375                        ),
376                        channel: types::WlanChan::new(11, types::Cbw::Cbw20),
377                        observation: types::ScanObservation::Passive,
378                        compatibility: Incompatible::unknown(),
379                        bss_description: random_fidl_bss_description!(
380                            Wpa3,
381                            bssid: [7, 8, 9, 10, 11, 12],
382                            ssid: types::Ssid::try_from("duplicated ssid").unwrap(),
383                            rssi_dbm: 13,
384                            snr_db: 3,
385                            channel: types::WlanChan::new(11, types::Cbw::Cbw20),
386                        )
387                        .into(),
388                    },
389                ],
390                compatibility: types::Compatibility::Supported,
391            },
392            types::ScanResult {
393                ssid: types::Ssid::try_from("unique ssid").unwrap(),
394                security_type_detailed: types::SecurityTypeDetailed::Wpa2Personal,
395                entries: vec![types::Bss {
396                    bssid: types::Bssid::from([1, 2, 3, 4, 5, 6]),
397                    signal: types::Signal { rssi_dbm: 7, snr_db: 2 },
398                    timestamp: zx::MonotonicInstant::from_nanos(
399                        fidl_aps[1].entries.as_ref().unwrap()[0].timestamp_nanos.unwrap(),
400                    ),
401                    channel: types::WlanChan::new(8, types::Cbw::Cbw20),
402                    observation: types::ScanObservation::Passive,
403                    compatibility: Compatible::expect_ok([SecurityDescriptor::WPA2_PERSONAL]),
404                    bss_description: random_fidl_bss_description!(
405                        Wpa2,
406                        bssid: [1, 2, 3, 4, 5, 6],
407                        ssid: types::Ssid::try_from("unique ssid").unwrap(),
408                        rssi_dbm: 7,
409                        snr_db: 2,
410                        channel: types::WlanChan::new(8, types::Cbw::Cbw20),
411                    )
412                    .into(),
413                }],
414                compatibility: types::Compatibility::Supported,
415            },
416        ];
417        assert_eq!(fidl_aps, scan_result_to_policy_scan_result(&internal_aps, false));
418    }
419
420    // TODO(https://fxbug.dev/42131757): Separate test case for "empty final vector not consumed" vs "partial ap list"
421    // consumed.
422    #[fuchsia::test]
423    fn partial_scan_result_consumption_has_no_error() {
424        let mut exec = fasync::TestExecutor::new();
425        let scan_results = generate_test_fidl_data();
426
427        // Create an iterator and send scan results
428        let (iter, iter_server) = fidl::endpoints::create_proxy();
429        let send_fut = send_scan_results_over_fidl(iter_server, &scan_results);
430        let mut send_fut = pin!(send_fut);
431
432        // Request a chunk of scan results.
433        let mut output_iter_fut = iter.get_next();
434
435        // Send first chunk of scan results
436        assert_matches!(exec.run_until_stalled(&mut send_fut), Poll::Pending);
437
438        // Make sure the first chunk of results were delivered
439        assert_matches!(exec.run_until_stalled(&mut output_iter_fut), Poll::Ready(result) => {
440            let results = result.expect("Failed to get next scan results").unwrap();
441            assert_eq!(results, scan_results);
442        });
443
444        // Close the channel without getting remaining results
445        // Note: as of the writing of this test, the "remaining results" are just the final message
446        // with an empty vector of networks that signify the end of results. That final empty vector
447        // is still considered part of the results, so this test successfully exercises the
448        // "partial results read" path.
449        drop(output_iter_fut);
450        drop(iter);
451
452        // This should not result in error, since some results were consumed
453        assert_matches!(exec.run_until_stalled(&mut send_fut), Poll::Ready(Ok(())));
454    }
455
456    #[fuchsia::test]
457    fn no_scan_result_consumption_has_error() {
458        let mut exec = fasync::TestExecutor::new();
459        let scan_results = generate_test_fidl_data();
460
461        // Create an iterator and send scan results
462        let (iter, iter_server) = fidl::endpoints::create_proxy();
463        let send_fut = send_scan_results_over_fidl(iter_server, &scan_results);
464        let mut send_fut = pin!(send_fut);
465
466        // Close the channel without getting results
467        drop(iter);
468
469        // This should result in error, since no results were consumed
470        assert_matches!(exec.run_until_stalled(&mut send_fut), Poll::Ready(Err(_)));
471    }
472
473    #[fuchsia::test]
474    fn scan_result_sends_max_message_size() {
475        let mut exec = fasync::TestExecutor::new();
476        let (iter, iter_server) = fidl::endpoints::create_proxy();
477
478        // Create a single scan result at the max allowed size to send in single
479        // FIDL message.
480        let fidl_scan_results = create_fidl_scan_results_from_size(vec![
481            zx::sys::ZX_CHANNEL_MAX_MSG_BYTES as usize
482                - FIDL_HEADER_AND_ERR_WRAPPED_VEC_HEADER_SIZE,
483        ]);
484
485        let send_fut = send_scan_results_over_fidl(iter_server, &fidl_scan_results);
486        let mut send_fut = pin!(send_fut);
487
488        let mut output_iter_fut = iter.get_next();
489
490        assert_matches!(exec.run_until_stalled(&mut send_fut), Poll::Pending);
491
492        assert_matches!(exec.run_until_stalled(&mut output_iter_fut), Poll::Ready(result) => {
493            let results = result.expect("Failed to get next scan results").unwrap();
494            assert_eq!(results, fidl_scan_results);
495        })
496    }
497
498    #[fuchsia::test]
499    fn scan_result_exceeding_max_size_throws_error() {
500        let mut exec = fasync::TestExecutor::new();
501        let (iter, iter_server) = fidl::endpoints::create_proxy();
502
503        // Create a single scan result exceeding the  max allowed size to send in single
504        // FIDL message.
505        let fidl_scan_results = create_fidl_scan_results_from_size(vec![
506            (zx::sys::ZX_CHANNEL_MAX_MSG_BYTES as usize
507                - FIDL_HEADER_AND_ERR_WRAPPED_VEC_HEADER_SIZE)
508                + 8,
509        ]);
510
511        let send_fut = send_scan_results_over_fidl(iter_server, &fidl_scan_results);
512        let mut send_fut = pin!(send_fut);
513
514        let mut output_iter_fut = iter.get_next();
515
516        assert_matches!(exec.run_until_stalled(&mut send_fut), Poll::Ready(Err(_)));
517
518        assert_matches!(exec.run_until_stalled(&mut output_iter_fut), Poll::Ready(Err(_)));
519    }
520
521    #[fuchsia::test]
522    fn scan_result_sends_single_batch() {
523        let mut exec = fasync::TestExecutor::new();
524        let (iter, iter_server) = fidl::endpoints::create_proxy();
525
526        // Create a set of scan results that does not exceed the the max message
527        // size, so it should be sent in a single batch.
528        let fidl_scan_results =
529            create_fidl_scan_results_from_size(vec![
530                zx::sys::ZX_CHANNEL_MAX_MSG_BYTES as usize / 4;
531                3
532            ]);
533
534        let send_fut = send_scan_results_over_fidl(iter_server, &fidl_scan_results);
535        let mut send_fut = pin!(send_fut);
536
537        let mut output_iter_fut = iter.get_next();
538
539        assert_matches!(exec.run_until_stalled(&mut send_fut), Poll::Pending);
540
541        assert_matches!(exec.run_until_stalled(&mut output_iter_fut), Poll::Ready(result) => {
542            let results = result.expect("Failed to get next scan results").unwrap();
543            assert_eq!(results, fidl_scan_results);
544        });
545    }
546
547    #[fuchsia::test]
548    fn scan_result_sends_multiple_batches() {
549        let mut exec = fasync::TestExecutor::new();
550        let (iter, iter_server) = fidl::endpoints::create_proxy();
551
552        // Create a set of scan results that exceed the max FIDL message size, so
553        // they should be split into batches.
554        let fidl_scan_results =
555            create_fidl_scan_results_from_size(vec![
556                zx::sys::ZX_CHANNEL_MAX_MSG_BYTES as usize / 8;
557                8
558            ]);
559
560        let send_fut = send_scan_results_over_fidl(iter_server, &fidl_scan_results);
561        let mut send_fut = pin!(send_fut);
562
563        let mut output_iter_fut = iter.get_next();
564
565        assert_matches!(exec.run_until_stalled(&mut send_fut), Poll::Pending);
566
567        let mut aggregate_results = vec![];
568        assert_matches!(exec.run_until_stalled(&mut output_iter_fut), Poll::Ready(result) => {
569            let results = result.expect("Failed to get next scan results").unwrap();
570            assert_eq!(results.len(), 7);
571            aggregate_results.extend(results);
572        });
573
574        let mut output_iter_fut = iter.get_next();
575        assert_matches!(exec.run_until_stalled(&mut send_fut), Poll::Pending);
576        assert_matches!(exec.run_until_stalled(&mut output_iter_fut), Poll::Ready(result) => {
577            let results = result.expect("Failed to get next scan results").unwrap();
578            assert_eq!(results.len(), 1);
579            aggregate_results.extend(results);
580        });
581        assert_eq!(aggregate_results, fidl_scan_results);
582    }
583}