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