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