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