Skip to main content

wlan_hw_sim/
test_utils.rs

1// Copyright 2018 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::event::{self, Handler};
6use crate::netdevice_helper;
7use crate::wlancfg_helper::{NetworkConfigBuilder, start_ap_and_wait_for_confirmation};
8use fidl::endpoints::{Proxy, create_endpoints, create_proxy};
9use fidl_fuchsia_driver_test as fidl_driver_test;
10use fidl_fuchsia_wlan_policy as fidl_policy;
11use fidl_fuchsia_wlan_tap as wlantap;
12use fidl_test_wlan_realm as fidl_realm;
13use fuchsia_async::{DurationExt, MonotonicInstant, TimeoutExt, Timer};
14use fuchsia_component::client::{connect_to_protocol, connect_to_protocol_at};
15use futures::channel::oneshot;
16use futures::{FutureExt, StreamExt};
17use ieee80211::{MacAddr, MacAddrBytes};
18use log::{debug, info, warn};
19use realm_client::{InstalledNamespace, extend_namespace};
20use std::fmt::Display;
21use std::future::Future;
22use std::pin::Pin;
23use std::sync::Arc;
24use std::task::{Context, Poll};
25use test_realm_helpers::tracing::Tracing;
26use wlan_common::test_utils::ExpectWithin;
27use wlantap_client::Wlantap;
28
29/// Percent of a timeout duration past which we log a warning.
30const TIMEOUT_WARN_THRESHOLD: f64 = 0.8;
31
32// Struct that allows a test suite to interact with the test realm.
33//
34// If the test suite needs to connect to a protocol exposed by the test realm, it MUST use the
35// context's realm_proxy and cannot use fuchsia_component::client::connect_to_protocol.
36//
37// Similarly, if the test suite needs to connect to /dev hosted by the test realm, it must use the
38// context's devfs. There is currently no way to access any other directories in the test realm. If
39// the test suite needs to access any other directories, the test realm factory implementation and
40// FIDL API will need to be changed.
41//
42// Example:
43//
44// // Create a new test realm context
45// let ctx = ctx::new(fidl_realm::WlanConfig{ ..Default::default() };
46//
47// // Connect to a protocol
48// let protocol_proxy = ctx.test_realm_proxy()
49//   .connect_to_protocol::<fidl_fuchsia_protocol::Protocol>()
50//   .await?;
51//
52// // Connect to dev/class/network in the test realm
53// let (directory, directory_server) =
54//      create_proxy::<fidl_fuchsia_io::DirectoryMarker>();
55//  fdio::service_connect_at(
56//     ctx.devfs().as_channel().as_ref(),
57//     "class/network",
58//     directory_server.into_channel(),
59//  )?;
60pub struct TestRealmContext {
61    // The test namespace, which allows the test suite to connect to protocols exposed by
62    // the test realm. The test namespace must outlive any dependencies on components that
63    // run within it.
64    test_ns: Arc<InstalledNamespace>,
65
66    // A directory proxy connected to "/dev" in the test realm.
67    devfs: fidl_fuchsia_io::DirectoryProxy,
68    _tracing: Tracing,
69}
70
71impl TestRealmContext {
72    // Connect to the test realm factory to create and start a new test realm and return the test
73    // realm context. This will also start the driver test realm.
74    //
75    // Panics if any errors occur when the realm factory is being created.
76    pub async fn new(config: fidl_realm::WlanConfig) -> Arc<Self> {
77        let realm_factory = connect_to_protocol::<fidl_realm::RealmFactoryMarker>()
78            .expect("Could not connect to realm factory protocol");
79
80        let (dict_1, dict_2) = zx::EventPair::create();
81        let (devfs_proxy, devfs_server) = create_proxy();
82
83        // Create the test realm for this test. This returns a
84        // `fuchsia.component.sandbox/Dictionary`, which is then consumed by `extend_namespace`
85        // to turn it into a directory installed in this component's namespace at
86        // `test_ns.prefix()`.
87        let trace_manager_hermeticity = config.trace_manager_hermeticity.clone();
88        let options = fidl_realm::RealmOptions {
89            devfs_server_end: Some(devfs_server),
90            wlan_config: Some(config),
91            ..Default::default()
92        };
93        let _ = realm_factory.create_realm3(options, dict_1).await.expect("Could not create realm");
94        let test_ns =
95            Arc::new(extend_namespace(realm_factory, dict_2).await.expect("failed to extend ns"));
96
97        // Start the driver test realm
98        let driver_test_realm_proxy =
99            connect_to_protocol_at::<fidl_driver_test::RealmMarker>(&*test_ns)
100                .expect("Failed to connect to driver test realm");
101
102        let (pkg_client, pkg_server) = create_endpoints();
103        fuchsia_fs::directory::open_channel_in_namespace(
104            "/pkg",
105            fidl_fuchsia_io::PERM_READABLE | fidl_fuchsia_io::PERM_EXECUTABLE,
106            pkg_server,
107        )
108        .expect("Could not open /pkg");
109
110        let test_component = fidl_fuchsia_component_resolution::Component {
111            package: Some(fidl_fuchsia_component_resolution::Package {
112                directory: Some(pkg_client),
113                ..Default::default()
114            }),
115            ..Default::default()
116        };
117
118        driver_test_realm_proxy
119            .start(fidl_driver_test::RealmArgs {
120                test_component: Some(test_component),
121                ..Default::default()
122            })
123            .await
124            .expect("FIDL error when starting driver test realm")
125            .expect("Driver test realm server returned an error");
126
127        let _tracing = match trace_manager_hermeticity {
128            Some(fidl_realm::TraceManagerHermeticity::Hermetic) | None => {
129                Tracing::start_at(Arc::clone(&test_ns)).await.unwrap()
130            }
131            Some(fidl_realm::TraceManagerHermeticity::NonHermetic) => {
132                Tracing::start_non_hermetic(test_ns.prefix().strip_prefix("/").unwrap())
133                    .await
134                    .unwrap()
135            }
136        };
137
138        Arc::new(Self { test_ns, devfs: devfs_proxy, _tracing })
139    }
140
141    pub fn test_ns_prefix(&self) -> &str {
142        self.test_ns.prefix()
143    }
144
145    pub fn devfs(&self) -> &fidl_fuchsia_io::DirectoryProxy {
146        &self.devfs
147    }
148}
149
150type EventStream = wlantap::WlantapPhyEventStream;
151pub struct TestHelper {
152    ctx: Arc<TestRealmContext>,
153    netdevice_task_handles: Vec<fuchsia_async::Task<()>>,
154    _wlantap: Wlantap,
155    proxy: Arc<wlantap::WlantapPhyProxy>,
156    event_stream: Option<EventStream>,
157}
158struct TestHelperFuture<H, F>
159where
160    H: Handler<(), wlantap::WlantapPhyEvent>,
161    F: Future + Unpin,
162{
163    event_stream: Option<EventStream>,
164    handler: H,
165    future: F,
166}
167impl<H, F> Unpin for TestHelperFuture<H, F>
168where
169    H: Handler<(), wlantap::WlantapPhyEvent>,
170    F: Future + Unpin,
171{
172}
173impl<H, F> Future for TestHelperFuture<H, F>
174where
175    H: Handler<(), wlantap::WlantapPhyEvent>,
176    F: Future + Unpin,
177{
178    type Output = (F::Output, EventStream);
179    /// Polls the |event_stream| and invokes the |handler| for each event until |future| is ready.
180    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
181        let helper = &mut *self;
182        let stream = helper.event_stream.as_mut().unwrap();
183        loop {
184            // Always poll for completion of the main future before processing
185            // the next event in the stream. This gives priority to the main
186            // future to avoid silently dropping events that should be processed
187            // by the next phase of a test. For example, every test begins with
188            // a phase that waits for the arrival of the `WlanSoftmacStart`
189            // event and should process exactly that one event from the stream
190            // and return `Poll::Ready`.
191            if let Poll::Ready(x) = helper.future.poll_unpin(cx) {
192                return Poll::Ready((x, helper.event_stream.take().unwrap()));
193            }
194
195            match stream.poll_next_unpin(cx) {
196                Poll::Ready(optional_result) => {
197                    let event = optional_result
198                        .expect("Unexpected end of the WlantapPhy event stream")
199                        .expect("WlantapPhy event stream returned an error");
200                    helper.handler.call(&mut (), &event);
201                }
202                Poll::Pending => {
203                    debug!(
204                        "Main future poll response is pending and there are no events to process."
205                    );
206                    return Poll::Pending;
207                }
208            }
209        }
210    }
211}
212impl TestHelper {
213    // Create a client TestHelper with a new TestRealmContext.
214    // NOTE: if a test case creates multiple TestHelpers that should all share the same test realm,
215    // it should use TestHelper::begin_test_with_context.
216    pub async fn begin_test(
217        phy_config: wlantap::WlantapPhyConfig,
218        realm_config: fidl_realm::WlanConfig,
219    ) -> Self {
220        let ctx = TestRealmContext::new(realm_config).await;
221        Self::begin_test_with_context(ctx, phy_config).await
222    }
223
224    // Create client TestHelper with a given TestRealmContext.
225    // If a test case creates multiple TestHelpers that must refer to the same instance of WLAN
226    // components, then all TestHelpers must use a copy of the same TestRealmContext.
227    //
228    // Example:
229    //
230    // // Create a new test realm context
231    // let ctx = TestRealmContext::new(fidl_realm::WlanConfig{ ..Default::default() };
232    //
233    // // Create both helpers with copies of the same context
234    // let helper1 = TestHelper::begin_test_with_context(
235    //    ctx.clone(),
236    //    default_wlantap_client_config(),
237    // ).await;
238    //
239    // let helper2 = TestHelper::begin_test_with_context(
240    //    ctx.clone(),
241    //    default_wlantap_client_config()).await;
242    pub async fn begin_test_with_context(
243        ctx: Arc<TestRealmContext>,
244        config: wlantap::WlantapPhyConfig,
245    ) -> Self {
246        let mut helper = TestHelper::create_phy_and_helper(config, ctx).await;
247        helper.wait_for_wlan_softmac_start().await;
248        helper
249    }
250
251    // Create an AP TestHelper with a new TestRealmContext.
252    // NOTE: if a test case creates multiple TestHelpers that should all share the same test realm,
253    // it should use TestHelper::begin_ap_test_with_context.
254    pub async fn begin_ap_test(
255        phy_config: wlantap::WlantapPhyConfig,
256        network_config: NetworkConfigBuilder,
257        realm_config: fidl_realm::WlanConfig,
258    ) -> Self {
259        let ctx = TestRealmContext::new(realm_config).await;
260        Self::begin_ap_test_with_context(ctx, phy_config, network_config).await
261    }
262
263    // Create AP TestHelper with a given TestRealmContext.
264    // If a test case creates multiple TestHelpers that must refer to the same instance of WLAN
265    // components, then all TestHelpers must use a copy of the same TestRealmContext.
266    //
267    // Example:
268    //
269    // // Create a new test realm context
270    // let ctx = TestRealmContext::new(fidl_realm::WlanConfig{ ..Default::default() };
271    //
272    // // Create both helpers with copies of the same context
273    // let helper1 = TestHelper::begin_ap_test_with_context(
274    //    ctx.clone(),
275    //    default_wlantap_client_config(),
276    //    network_config1,
277    // ).await;
278    //
279    // let helper2 = TestHelper::begin_ap_test_with_context(
280    //    ctx.clone(),
281    //    default_wlantap_client_config(),
282    //    network_config2
283    // ).await;
284    pub async fn begin_ap_test_with_context(
285        ctx: Arc<TestRealmContext>,
286        config: wlantap::WlantapPhyConfig,
287        network_config: NetworkConfigBuilder,
288    ) -> Self {
289        let mut helper = TestHelper::create_phy_and_helper(config, ctx).await;
290        start_ap_and_wait_for_confirmation(helper.ctx.test_ns_prefix(), network_config).await;
291        helper.wait_for_wlan_softmac_start().await;
292        helper
293    }
294
295    async fn create_phy_and_helper(
296        config: wlantap::WlantapPhyConfig,
297        ctx: Arc<TestRealmContext>,
298    ) -> Self {
299        // Trigger creation of wlantap serviced phy and iface for testing.
300        let wlantap =
301            Wlantap::open_from_devfs(&ctx.devfs).await.expect("Failed to open wlantapctl");
302        let proxy = wlantap.create_phy(config).await.expect("Failed to create wlantap PHY");
303        let event_stream = Some(proxy.take_event_stream());
304        TestHelper {
305            ctx,
306            netdevice_task_handles: vec![],
307            _wlantap: wlantap,
308            proxy: Arc::new(proxy),
309            event_stream,
310        }
311    }
312
313    async fn wait_for_wlan_softmac_start(&mut self) {
314        let (sender, receiver) = oneshot::channel::<()>();
315        self.run_until_complete_or_timeout(
316            zx::MonotonicDuration::from_seconds(12),
317            "receive a WlanSoftmacStart event",
318            event::on_start_mac(event::once(|_, _| sender.send(()))),
319            receiver,
320        )
321        .await
322        .unwrap_or_else(|oneshot::Canceled| panic!());
323    }
324
325    /// Returns a clone of the `Arc<wlantap::WlantapPhyProxy>` as a convenience for passing
326    /// the proxy to futures. Tests must drop every `Arc<wlantap::WlantapPhyProxy>` returned from this
327    /// method before dropping the TestHelper. Otherwise, TestHelper::drop() cannot synchronously
328    /// block on WlantapPhy.Shutdown().
329    pub fn proxy(&self) -> Arc<wlantap::WlantapPhyProxy> {
330        Arc::clone(&self.proxy)
331    }
332
333    pub fn test_ns_prefix(&self) -> &str {
334        self.ctx.test_ns_prefix()
335    }
336
337    pub fn devfs(&self) -> &fidl_fuchsia_io::DirectoryProxy {
338        self.ctx.devfs()
339    }
340
341    pub async fn start_netdevice_session(
342        &mut self,
343        mac: MacAddr,
344    ) -> (netdevice_client::Session, netdevice_client::Port) {
345        let mac = fidl_fuchsia_net::MacAddress { octets: mac.to_array() };
346        let (client, port) = netdevice_helper::create_client(self.devfs(), mac)
347            .await
348            .expect("failed to create netdevice client");
349        let (session, task_handle) = netdevice_helper::start_session(client, port).await;
350        self.netdevice_task_handles.push(task_handle);
351        (session, port)
352    }
353
354    /// Will run the main future until it completes or when it has run past the specified duration.
355    /// Note that any events that are observed on the event stream will be passed to the
356    /// |event_handler| closure first before making progress on the main future.
357    /// So if a test generates many events each of which requires significant computational time in
358    /// the event handler, the main future may not be able to complete in time.
359    pub async fn run_until_complete_or_timeout<H, F>(
360        &mut self,
361        timeout: zx::MonotonicDuration,
362        context: impl Display,
363        handler: H,
364        future: F,
365    ) -> F::Output
366    where
367        H: Handler<(), wlantap::WlantapPhyEvent>,
368        F: Future + Unpin,
369    {
370        info!("Running main future until completion or timeout with event handler: {}", context);
371        let start_time = zx::MonotonicInstant::get();
372        let (item, stream) = TestHelperFuture {
373            event_stream: Some(self.event_stream.take().unwrap()),
374            handler,
375            future,
376        }
377        .expect_within(timeout, format!("Main future timed out: {}", context))
378        .await;
379        let end_time = zx::MonotonicInstant::get();
380        let elapsed = end_time - start_time;
381        let elapsed_seconds = elapsed.into_seconds_f64();
382        let elapsed_ratio = elapsed_seconds / timeout.into_seconds_f64();
383        if elapsed_ratio < TIMEOUT_WARN_THRESHOLD {
384            info!("Main future completed in {:.2} seconds: {}", elapsed_seconds, context);
385        } else {
386            warn!(
387                "Main future completed in {:.2} seconds ({:.1}% of timeout): {}",
388                elapsed_seconds,
389                elapsed_ratio * 100.,
390                context,
391            );
392        }
393        self.event_stream = Some(stream);
394        item
395    }
396}
397impl Drop for TestHelper {
398    fn drop(&mut self) {
399        // Drop each fuchsia_async::Task driving each
400        // netdevice_client::Session in the reverse order the test
401        // created them.
402        while let Some(task_handle) = self.netdevice_task_handles.pop() {
403            drop(task_handle);
404        }
405
406        // Create a placeholder proxy to swap into place of self.proxy. This allows this
407        // function to create a synchronous proxy from the real proxy.
408        let (placeholder_proxy, _server_end) =
409            fidl::endpoints::create_proxy::<wlantap::WlantapPhyMarker>();
410        let mut proxy = Arc::new(placeholder_proxy);
411        std::mem::swap(&mut self.proxy, &mut proxy);
412
413        // Drop the event stream so the WlantapPhyProxy can be converted
414        // back into a channel. Conversion from a proxy into a channel fails
415        // otherwise.
416        let event_stream = self.event_stream.take();
417        drop(event_stream);
418
419        let sync_proxy = wlantap::WlantapPhySynchronousProxy::new(
420            // Arc::into_inner() should succeed in a properly constructed test. Using a WlantapPhyProxy
421            // returned from TestHelper beyond the lifetime of TestHelper is not supported.
422            Arc::<wlantap::WlantapPhyProxy>::into_inner(proxy)
423                .expect("Outstanding references to WlantapPhyProxy! Failed to drop TestHelper.")
424                .into_channel()
425                .expect("failed to get fidl::AsyncChannel from proxy")
426                .into_zx_channel(),
427        );
428
429        // TODO(b/307808624): At this point in the shutdown, we should
430        // stop wlancfg first and destroy all ifaces through
431        // fuchsia.wlan.device.service/DeviceMonitor.DestroyIface().
432        // This test framework does not currently support stopping
433        // individual components. If instead we drop the
434        // TestRealmProxy, and thus stop both wlancfg and
435        // wlandevicemonitor, wlandevicemonitor which will drop the
436        // GenericSme channel before graceful destruction of the
437        // iface. Dropping the GenericSme channel for an existing
438        // iface is considered an error because doing so prevents
439        // future communication with the iface.
440        //
441        // In lieu of stopping wlancfg first, we instead shutdown the
442        // phy device via WlantapPhy.Shutdown() which will block until
443        // both the phy and any remaining ifaces are shutdown. We
444        // first shutdown the phy to prevent any automated CreateIface
445        // calls from wlancfg after removing the iface.
446        sync_proxy
447            .shutdown(zx::MonotonicInstant::INFINITE)
448            .expect("Failed to shutdown WlantapPhy gracefully.");
449    }
450}
451
452pub struct RetryWithBackoff {
453    deadline: MonotonicInstant,
454    prev_delay: zx::MonotonicDuration,
455    next_delay: zx::MonotonicDuration,
456    max_delay: zx::MonotonicDuration,
457}
458impl RetryWithBackoff {
459    pub fn new(timeout: zx::MonotonicDuration) -> Self {
460        RetryWithBackoff {
461            deadline: MonotonicInstant::after(timeout),
462            prev_delay: zx::MonotonicDuration::from_millis(0),
463            next_delay: zx::MonotonicDuration::from_millis(1),
464            max_delay: zx::MonotonicDuration::INFINITE,
465        }
466    }
467    pub fn infinite_with_max_interval(max_delay: zx::MonotonicDuration) -> Self {
468        Self {
469            deadline: MonotonicInstant::INFINITE,
470            max_delay,
471            ..Self::new(zx::MonotonicDuration::from_nanos(0))
472        }
473    }
474
475    /// Return Err if the deadline was exceeded when this function was called.
476    /// Otherwise, sleep for a little longer (following Fibonacci series) or up
477    /// to the deadline, whichever is soonest. If a sleep occurred, this function
478    /// returns Ok. The value contained in both Ok and Err is the zx::MonotonicDuration
479    /// until or after the deadline when the function returns.
480    async fn sleep_unless_after_deadline_(
481        &mut self,
482        verbose: bool,
483    ) -> Result<zx::MonotonicDuration, zx::MonotonicDuration> {
484        // Add an inner scope up to just after Timer::new to ensure all
485        // time assignments are dropped after the sleep occurs. This
486        // prevents misusing them after the sleep since they are all
487        // no longer correct after the clock moves.
488        {
489            if MonotonicInstant::after(zx::MonotonicDuration::from_millis(0)) > self.deadline {
490                if verbose {
491                    info!("Skipping sleep. Deadline exceeded.");
492                }
493                return Err(self.deadline - MonotonicInstant::now());
494            }
495
496            let sleep_deadline =
497                std::cmp::min(MonotonicInstant::after(self.next_delay), self.deadline);
498            if verbose {
499                let micros = sleep_deadline.into_nanos() / 1_000;
500                info!("Sleeping until {}.{} 😴", micros / 1_000_000, micros % 1_000_000);
501            }
502
503            Timer::new(sleep_deadline).await;
504        }
505
506        // If the next delay interval exceeds max_delay (even if by overflow),
507        // then saturate at max_delay.
508        if self.next_delay < self.max_delay {
509            let next_delay = std::cmp::min(
510                self.max_delay,
511                zx::MonotonicDuration::from_nanos(
512                    self.prev_delay.into_nanos().saturating_add(self.next_delay.into_nanos()),
513                ),
514            );
515            self.prev_delay = self.next_delay;
516            self.next_delay = next_delay;
517        }
518
519        Ok(self.deadline - MonotonicInstant::now())
520    }
521
522    pub async fn sleep_unless_after_deadline(
523        &mut self,
524    ) -> Result<zx::MonotonicDuration, zx::MonotonicDuration> {
525        self.sleep_unless_after_deadline_(false).await
526    }
527
528    pub async fn sleep_unless_after_deadline_verbose(
529        &mut self,
530    ) -> Result<zx::MonotonicDuration, zx::MonotonicDuration> {
531        self.sleep_unless_after_deadline_(true).await
532    }
533}
534
535/// TODO(https://fxbug.dev/42164608): This function strips the `timestamp_nanos` field
536/// from each `fidl_fuchsia_wlan_policy::ScanResult` entry since the `timestamp_nanos`
537/// field is undefined.
538pub fn strip_timestamp_nanos_from_scan_results(
539    mut scan_result_list: Vec<fidl_fuchsia_wlan_policy::ScanResult>,
540) -> Vec<fidl_fuchsia_wlan_policy::ScanResult> {
541    for scan_result in &mut scan_result_list {
542        scan_result
543            .entries
544            .as_mut()
545            .unwrap()
546            .sort_by(|a, b| a.bssid.as_ref().unwrap().cmp(&b.bssid.as_ref().unwrap()));
547        for entry in scan_result.entries.as_mut().unwrap() {
548            // TODO(https://fxbug.dev/42164608): Strip timestamp_nanos since it's not implemented.
549            entry.timestamp_nanos.take();
550        }
551    }
552    scan_result_list
553}
554
555/// Sort a list of scan results by the `id` and `bssid` fields.
556///
557/// This function will panic if either of the `id` or `entries` fields
558/// are `None`.
559pub fn sort_policy_scan_result_list(
560    mut scan_result_list: Vec<fidl_fuchsia_wlan_policy::ScanResult>,
561) -> Vec<fidl_fuchsia_wlan_policy::ScanResult> {
562    scan_result_list
563        .sort_by(|a, b| a.id.as_ref().expect("empty id").cmp(&b.id.as_ref().expect("empty id")));
564    scan_result_list
565}
566
567/// Returns a map with the scan results returned by the policy layer. The map is
568/// keyed by the `id` field of each `fidl_fuchsia_policy::ScanResult`.
569///
570/// This function will panic if the `id` field is ever `None` or if policy returns
571/// the same `id` twice. Both of these are invariants we expect the policy layer
572/// to uphold.
573pub async fn policy_scan_for_networks<'a>(
574    client_controller: fidl_policy::ClientControllerProxy,
575) -> Vec<fidl_policy::ScanResult> {
576    // Request a scan from the policy layer.
577    let (scan_proxy, server_end) = create_proxy();
578    client_controller.scan_for_networks(server_end).expect("requesting scan");
579    let mut scan_result_list = Vec::new();
580    loop {
581        let proxy_result = scan_proxy.get_next().await.expect("getting scan results");
582        let next_scan_result_list = proxy_result.expect("scanning failed");
583        if next_scan_result_list.is_empty() {
584            break;
585        }
586        scan_result_list.extend(next_scan_result_list);
587    }
588    sort_policy_scan_result_list(strip_timestamp_nanos_from_scan_results(scan_result_list))
589}
590
591/// This function returns `Ok(r)`, where `r` is the return value from `main_future`,
592/// if `main_future` completes before the `timeout` duration. Otherwise, `Err(())` is returned.
593pub async fn timeout_after<R, F: Future<Output = R> + Unpin>(
594    timeout: zx::MonotonicDuration,
595    main_future: &mut F,
596) -> Result<R, ()> {
597    async { Ok(main_future.await) }.on_timeout(timeout.after_now(), || Err(())).await
598}