blob: 53cc17a8a2dc4399215188d8655030c6f4bfaebe [file] [log] [blame]
// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use anyhow::Error;
use assert_matches::assert_matches;
use cm_rust::OfferDeclCommon;
use fidl::endpoints::DiscoverableProtocolMarker;
use fuchsia_component::server as fserver;
use fuchsia_component_test::new::{
Capability, ChildOptions, LocalComponentHandles, RealmBuilder, RealmInstance, Ref, Route,
};
use futures::channel::mpsc;
use futures::{FutureExt, SinkExt, StreamExt, TryStreamExt};
use {
fidl_fidl_examples_routing_echo as fecho, fidl_fuchsia_component as fcomponent,
fidl_fuchsia_component_decl as fdecl, fidl_fuchsia_io as fio, fuchsia_async as fasync,
};
#[fuchsia::test]
async fn routing_succeeds_with_dynamic_offer() {
let (_realm_instance, _local_components_task, mut success_receiver) =
new_realm(vec![fdecl::Offer::Protocol(fdecl::OfferProtocol {
source: Some(fdecl::Ref::Child(fdecl::ChildRef {
name: "child_a".to_string(),
collection: None,
})),
source_name: Some(fecho::EchoMarker::PROTOCOL_NAME.to_string()),
target_name: Some(fecho::EchoMarker::PROTOCOL_NAME.to_string()),
dependency_type: Some(fdecl::DependencyType::Strong),
availability: Some(fdecl::Availability::Required),
// It is important that `target` is not set
..Default::default()
})])
.await;
// Now let's create the realm that we'll dynamically create inside of the parent realm
assert_matches!(
success_receiver.next().await,
Some(true),
"failed to receive success signal from local component"
);
}
#[fuchsia::test]
async fn routing_fails_without_dynamic_offer() {
let (_realm_instance, _local_components_task, mut success_receiver) = new_realm(vec![]).await;
// Now let's create the realm that we'll dynamically create inside of the parent realm
assert_matches!(
success_receiver.next().await,
Some(false),
"local component claims it succeeded when it should not have"
);
}
async fn new_realm(
dynamic_offers: Vec<fdecl::Offer>,
) -> (RealmInstance, fasync::Task<()>, mpsc::Receiver<bool>) {
let (success_sender, success_receiver) = mpsc::channel(1);
// Create a child realm that holds an echo client. We're going to create this dynamically
// ourselves, so that we can set dynamic offers when its created.
let builder = RealmBuilder::new().await.unwrap();
let child_b = builder
.add_local_child(
"child_b",
move |h| echo_client_mock(h, success_sender.clone()).boxed(),
ChildOptions::new().eager(),
)
.await
.unwrap();
builder
.add_route(
Route::new()
.capability(Capability::protocol::<fecho::EchoMarker>())
.from(Ref::parent())
.to(&child_b),
)
.await
.unwrap();
builder
.add_route(
Route::new()
.capability(
Capability::protocol::<fcomponent::BinderMarker>()
// We want the realm_user to be able to bind directly to this child, but
// realm builder will "helpfully" add an expose for
// `fuchsia.component.Binder` to the realm. We'll need to rename this route
// so as to not clash with the implicit one.
.as_("fuchsia.component.Binder2"),
)
.from(&child_b)
.to(Ref::parent()),
)
.await
.unwrap();
let (child_realm_url, child_b_execution) = builder.initialize().await.unwrap();
// Now create the realm that has an echo server, a collection, and a local component that will
// exercise the realm protocol for us (since getting protocols across a nested component
// manager boundary is complicated).
let builder = RealmBuilder::new().await.unwrap();
let child_a = builder
.add_local_child("child_a", move |h| echo_server_mock(h).boxed(), ChildOptions::new())
.await
.unwrap();
let realm_user = builder
.add_local_child(
"realm_user",
move |h| realm_user(h, child_realm_url.clone(), dynamic_offers.clone()).boxed(),
ChildOptions::new().eager(),
)
.await
.unwrap();
builder
.add_route(
Route::new()
.capability(Capability::protocol::<fcomponent::RealmMarker>())
.from(Ref::framework())
.to(&realm_user),
)
.await
.unwrap();
// We want child_a to declare and expose this capability, but we want to offer it to a dynamic
// child using dynamic offers. Let's accomplish this by routing it to the collection with realm
// builder but then manually deleting the offer that gets added in the realm decl.
builder
.add_route(
Route::new()
.capability(Capability::protocol::<fecho::EchoMarker>())
.from(&child_a)
.to(Ref::collection("dynamic_children")),
)
.await
.unwrap();
let mut realm_decl = builder.get_realm_decl().await.unwrap();
realm_decl.collections.push(cm_rust::CollectionDecl {
name: "dynamic_children".parse().unwrap(),
durability: fdecl::Durability::Transient,
environment: None,
allowed_offers: cm_types::AllowedOffers::StaticAndDynamic,
allow_long_names: false,
persistent_storage: None,
});
realm_decl.offers = realm_decl
.offers
.into_iter()
.filter(|o| {
o.target() != &cm_rust::OfferTarget::Collection("dynamic_children".parse().unwrap())
})
.collect();
builder.replace_realm_decl(realm_decl).await.unwrap();
builder
.add_route(
Route::new()
.capability(Capability::protocol::<fcomponent::RealmMarker>())
.from(Ref::framework())
.to(Ref::parent()),
)
.await
.unwrap();
(
builder.build_in_nested_component_manager("#meta/component_manager.cm").await.unwrap(),
child_b_execution,
success_receiver,
)
}
// Uses the fuchsia.component.Realm protocol to create a new dynamic child with a dynamic offer
// from `child_a` for the echo protocol, and binds to the new child.
async fn realm_user(
handles: LocalComponentHandles,
child_realm_url: String,
dynamic_offers: Vec<fdecl::Offer>,
) -> Result<(), Error> {
let realm_proxy = handles.connect_to_protocol::<fcomponent::RealmMarker>().unwrap();
let collection_ref = fdecl::CollectionRef { name: "dynamic_children".to_string() };
realm_proxy
.create_child(
&collection_ref,
&fdecl::Child {
name: Some("child_realm".to_string()),
url: Some(child_realm_url),
startup: Some(fdecl::StartupMode::Lazy),
..Default::default()
},
fcomponent::CreateChildArgs {
dynamic_offers: Some(dynamic_offers),
..Default::default()
},
)
.await
.expect("FIDL error for create child")
.expect("component manager error for create child");
let child_ref = fdecl::ChildRef {
name: "child_realm".to_string(),
collection: Some("dynamic_children".to_string()),
};
let (exposed_proxy, exposed_server_end) =
fidl::endpoints::create_proxy::<fio::DirectoryMarker>().unwrap();
realm_proxy
.open_exposed_dir(&child_ref, exposed_server_end)
.await
.expect("FIDL error for open exposed directory")
.expect("component manager error for open exposed directory");
let _binder_node = fuchsia_fs::directory::open_node(
&exposed_proxy,
"fuchsia.component.Binder2",
fuchsia_fs::OpenFlags::NOT_DIRECTORY,
)
.await
.unwrap();
Ok(())
}
async fn echo_server_mock(handles: LocalComponentHandles) -> Result<(), Error> {
let mut fs = fserver::ServiceFs::new();
let mut tasks = vec![];
fs.dir("svc").add_fidl_service(move |mut stream: fecho::EchoRequestStream| {
tasks.push(fasync::Task::local(async move {
while let Some(fecho::EchoRequest::EchoString { value, responder }) =
stream.try_next().await.expect("failed to serve echo service")
{
responder.send(value.as_ref().map(|s| &**s)).expect("failed to send echo response");
}
}));
});
fs.serve_connection(handles.outgoing_dir)?;
fs.collect::<()>().await;
Ok(())
}
async fn echo_client_mock(
handles: LocalComponentHandles,
mut send_echo_client_results: mpsc::Sender<bool>,
) -> Result<(), Error> {
let echo_str = "Hello, hippos!";
let echo = handles.connect_to_protocol::<fecho::EchoMarker>()?;
match echo.echo_string(Some(echo_str)).await {
Ok(response) if response == Some(echo_str.to_string()) => {
send_echo_client_results.send(true).await.expect("failed to send results");
}
_ => {
send_echo_client_results.send(false).await.expect("failed to send results");
}
}
Ok(())
}