Skip to main content

netstack_testing_common/
lib.rs

1// Copyright 2019 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
5#![warn(missing_docs)]
6
7//! Provides utilities for Netstack integration tests.
8
9pub mod constants;
10pub mod devices;
11pub mod dhcpv4;
12pub mod interfaces;
13pub mod ndp;
14pub mod nud;
15pub mod packets;
16pub mod pcap;
17pub mod ping;
18#[macro_use]
19pub mod realms;
20
21use anyhow::Context as _;
22use component_events::events::EventStream;
23use fidl_fuchsia_netemul as fnetemul;
24use fuchsia_async::{self as fasync, DurationExt as _};
25use futures::future::FutureExt as _;
26use futures::stream::{Stream, StreamExt as _, TryStreamExt as _};
27use futures::{Future, select};
28use std::pin::pin;
29
30use crate::realms::TestSandboxExt as _;
31
32/// An alias for `Result<T, anyhow::Error>`.
33pub type Result<T = ()> = std::result::Result<T, anyhow::Error>;
34
35/// Extra time to use when waiting for an async event to occur.
36///
37/// A large timeout to help prevent flakes.
38pub const ASYNC_EVENT_POSITIVE_CHECK_TIMEOUT: zx::MonotonicDuration =
39    zx::MonotonicDuration::from_seconds(120);
40
41/// Extra time to use when waiting for an async event to not occur.
42///
43/// Since a negative check is used to make sure an event did not happen, its okay to use a
44/// smaller timeout compared to the positive case since execution stall in regards to the
45/// monotonic clock will not affect the expected outcome.
46pub const ASYNC_EVENT_NEGATIVE_CHECK_TIMEOUT: zx::MonotonicDuration =
47    zx::MonotonicDuration::from_seconds(5);
48
49/// The time to wait between two consecutive checks of an event.
50pub const ASYNC_EVENT_CHECK_INTERVAL: zx::MonotonicDuration =
51    zx::MonotonicDuration::from_seconds(1);
52
53/// Returns `true` once the stream yields a `true`.
54///
55/// If the stream never yields `true` or never terminates, `try_any` may never resolve.
56pub async fn try_any<S: Stream<Item = Result<bool>>>(stream: S) -> Result<bool> {
57    let stream = pin!(stream);
58    stream.try_filter(|v| futures::future::ready(*v)).next().await.unwrap_or(Ok(false))
59}
60
61/// Returns `true` if the stream only yields `true`.
62///
63/// If the stream never yields `false` or never terminates, `try_all` may never resolve.
64pub async fn try_all<S: Stream<Item = Result<bool>>>(stream: S) -> Result<bool> {
65    let stream = pin!(stream);
66    stream.try_filter(|v| futures::future::ready(!*v)).next().await.unwrap_or(Ok(true))
67}
68
69/// Asynchronously sleeps for specified `secs` seconds.
70pub async fn sleep(secs: i64) {
71    fasync::Timer::new(zx::MonotonicDuration::from_seconds(secs).after_now()).await;
72}
73
74/// Gets a component event stream yielding component stopped events.
75pub async fn get_component_stopped_event_stream() -> Result<component_events::events::EventStream> {
76    EventStream::open_at_path("/events/stopped")
77        .await
78        .context("failed to subscribe to `Stopped` events")
79}
80
81/// Waits for a `stopped` event to be emitted for a component in a test realm.
82///
83/// Optionally specifies a matcher for the expected exit status of the `stopped`
84/// event.
85pub async fn wait_for_component_stopped_with_stream(
86    event_stream: &mut component_events::events::EventStream,
87    realm: &netemul::TestRealm<'_>,
88    component_moniker: &str,
89    status_matcher: Option<component_events::matcher::ExitStatusMatcher>,
90) -> Result<component_events::events::Stopped> {
91    let matcher = get_child_component_event_matcher(realm, component_moniker)
92        .await
93        .context("get child component matcher")?;
94    matcher.stop(status_matcher).wait::<component_events::events::Stopped>(event_stream).await
95}
96
97/// Like [`wait_for_component_stopped_with_stream`] but retrieves an event
98/// stream for the caller.
99///
100/// Note that this function fails to observe stop events that happen in early
101/// realm creation, which is especially true for eager components.
102pub async fn wait_for_component_stopped(
103    realm: &netemul::TestRealm<'_>,
104    component_moniker: &str,
105    status_matcher: Option<component_events::matcher::ExitStatusMatcher>,
106) -> Result<component_events::events::Stopped> {
107    let mut stream = get_component_stopped_event_stream().await?;
108    wait_for_component_stopped_with_stream(&mut stream, realm, component_moniker, status_matcher)
109        .await
110}
111
112/// Gets an event matcher for `component_moniker` in `realm`.
113pub async fn get_child_component_event_matcher(
114    realm: &netemul::TestRealm<'_>,
115    component_moniker: &str,
116) -> Result<component_events::matcher::EventMatcher> {
117    let realm_moniker = &realm.get_moniker().await.context("calling get moniker")?;
118    let moniker_for_match =
119        format!("./{}/{}/{}", NETEMUL_SANDBOX_MONIKER, realm_moniker, component_moniker);
120    Ok(component_events::matcher::EventMatcher::ok().moniker(moniker_for_match))
121}
122
123/// The name of the netemul sandbox component, which is the parent component of
124/// managed test realms.
125const NETEMUL_SANDBOX_MONIKER: &str = "sandbox";
126
127/// Gets the moniker of a component in a test realm, relative to the root of the
128/// dynamic collection in which it is running.
129pub async fn get_component_moniker<'a>(
130    realm: &netemul::TestRealm<'a>,
131    component: &str,
132) -> Result<String> {
133    let realm_moniker = realm.get_moniker().await.context("calling get moniker")?;
134    Ok([NETEMUL_SANDBOX_MONIKER, &realm_moniker, component].join("/"))
135}
136
137/// Gets inspect data in realm.
138///
139/// Returns the resulting inspect data for `component` filtered by `tree_selector`.
140pub async fn get_inspect_data(
141    realm: &netemul::TestRealm<'_>,
142    component_moniker: impl Into<String>,
143    tree_selector: impl Into<String>,
144) -> Result<diagnostics_hierarchy::DiagnosticsHierarchy> {
145    let moniker = realm.get_moniker().await.context("calling get moniker")?;
146    let realm_moniker = selectors::sanitize_string_for_selectors(&moniker);
147    let mut data = diagnostics_reader::ArchiveReader::inspect()
148        .retry(diagnostics_reader::RetryConfig::MinSchemaCount(1))
149        .add_selector(
150            diagnostics_reader::ComponentSelector::new(vec![
151                NETEMUL_SANDBOX_MONIKER.into(),
152                realm_moniker.into_owned(),
153                component_moniker.into(),
154            ])
155            .with_tree_selector(tree_selector.into()),
156        )
157        .snapshot()
158        .await
159        .context("snapshot did not return any inspect data")?
160        .into_iter()
161        .map(|inspect_data| {
162            inspect_data.payload.ok_or_else(|| {
163                anyhow::anyhow!(
164                    "empty inspect payload, metadata errors: {:?}",
165                    inspect_data.metadata.errors
166                )
167            })
168        });
169
170    let Some(datum) = data.next() else {
171        unreachable!("archive reader RetryConfig specifies non-empty")
172    };
173
174    let data: Vec<_> = data.collect();
175    assert!(
176        data.is_empty(),
177        "expected a single inspect entry; got {:?} and also {:?}",
178        datum,
179        data
180    );
181
182    datum
183}
184
185/// Like [`get_inspect_data`] but returns a single property matched by
186/// `property_selector`.
187pub async fn get_inspect_property(
188    realm: &netemul::TestRealm<'_>,
189    component_moniker: impl Into<String>,
190    property_selector: impl Into<String>,
191) -> Result<diagnostics_hierarchy::Property> {
192    let property_selector = property_selector.into();
193    let hierarchy = get_inspect_data(&realm, component_moniker, property_selector.clone())
194        .await
195        .context("getting hierarchy")?;
196    let property_selector = property_selector.split(&['/', ':']).skip(1).collect::<Vec<_>>();
197    let property = hierarchy
198        .get_property_by_path(&property_selector)
199        .ok_or_else(|| anyhow::anyhow!("property not found in hierarchy: {hierarchy:?}"))?;
200    Ok(property.clone())
201}
202
203/// Sets up a realm with a network with no required services.
204pub async fn setup_network<'a, N: realms::Netstack>(
205    sandbox: &'a netemul::TestSandbox,
206    name: &'a str,
207    metric: Option<u32>,
208) -> Result<(
209    netemul::TestNetwork<'a>,
210    netemul::TestRealm<'a>,
211    netemul::TestInterface<'a>,
212    netemul::TestFakeEndpoint<'a>,
213)> {
214    setup_network_with::<N, _>(
215        sandbox,
216        name,
217        netemul::InterfaceConfig { metric, ..Default::default() },
218        std::iter::empty::<fnetemul::ChildDef>(),
219    )
220    .await
221}
222
223/// Sets up a realm with required services and a network used for tests
224/// requiring manual packet inspection and transmission.
225///
226/// Returns the network, realm, netstack client, interface (added to the
227/// netstack and up) and a fake endpoint used to read and write raw ethernet
228/// packets.
229pub async fn setup_network_with<'a, N: realms::Netstack, I>(
230    sandbox: &'a netemul::TestSandbox,
231    name: &'a str,
232    interface_config: netemul::InterfaceConfig<'a>,
233    children: I,
234) -> Result<(
235    netemul::TestNetwork<'a>,
236    netemul::TestRealm<'a>,
237    netemul::TestInterface<'a>,
238    netemul::TestFakeEndpoint<'a>,
239)>
240where
241    I: IntoIterator,
242    I::Item: Into<fnetemul::ChildDef>,
243{
244    let network = sandbox.create_network(name).await.context("failed to create network")?;
245    let realm = sandbox
246        .create_netstack_realm_with::<N, _, _>(name, children)
247        .context("failed to create netstack realm")?;
248    // It is important that we create the fake endpoint before we join the
249    // network so no frames transmitted by Netstack are lost.
250    let fake_ep = network.create_fake_endpoint()?;
251
252    let iface = realm
253        .join_network_with_if_config(&network, name, interface_config)
254        .await
255        .context("failed to configure networking")?;
256
257    Ok((network, realm, iface, fake_ep))
258}
259
260/// Pauses the fake clock in the given realm.
261pub async fn pause_fake_clock(realm: &netemul::TestRealm<'_>) -> Result<()> {
262    let fake_clock_control = realm
263        .connect_to_protocol::<fidl_fuchsia_testing::FakeClockControlMarker>()
264        .context("failed to connect to FakeClockControl")?;
265    fake_clock_control.pause().await.context("failed to pause time")?;
266    Ok(())
267}
268
269/// Wraps `fut` so that it prints `event_name` and the caller's location to
270/// stderr every `interval` until `fut` completes.
271#[track_caller]
272pub fn annotate<'a, 'b: 'a, T>(
273    fut: impl Future<Output = T> + 'a,
274    interval: std::time::Duration,
275    event_name: &'b str,
276) -> impl Future<Output = T> + 'a {
277    let caller = std::panic::Location::caller();
278
279    async move {
280        let mut fut = pin!(fut.fuse());
281        let event_name = event_name.to_string();
282        let mut print_fut = pin!(
283            futures::stream::repeat(())
284                .for_each(|()| async {
285                    fasync::Timer::new(interval).await;
286                    eprintln!("waiting for {} at {}", event_name, caller);
287                })
288                .fuse()
289        );
290        let result = select! {
291            result = fut => result,
292            () = print_fut => unreachable!("should repeat printing forever"),
293        };
294        eprintln!("completed {} at {}", event_name, caller);
295        result
296    }
297}