blob: 972bd564bcb96172afc6bbd742326aff639548f2 [file] [log] [blame]
// Copyright 2019 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.
/// Tests of capability routing in ComponentManager.
///
/// Most routing tests should be defined as methods on the ::routing_test_helpers::CommonRoutingTest
/// type and should be run both in this file (using a CommonRoutingTest<RoutingTestBuilder>) and in
/// the cm_fidl_analyzer_tests crate (using a specialization of CommonRoutingTest for the static
/// routing analyzer). This ensures that the static analyzer's routing verification is consistent
/// with ComponentManager's intended routing behavior.
///
/// However, tests of behavior that is out-of-scope for the static analyzer (e.g. routing to/from
/// dynamic component instances) should be defined here.
use {
crate::{
capability::CapabilitySource,
model::{
actions::{
ActionSet, DestroyAction, ShutdownAction, ShutdownType, StartAction, StopAction,
},
component::{IncomingCapabilities, StartReason},
error::{ActionError, ModelError, ResolveActionError, StartActionError},
hooks::{Event, EventPayload, EventType, Hook, HooksRegistration},
routing::{router::Routable, Route, RouteRequest, RouteSource, RoutingError},
testing::{
echo_service::EchoProtocol, mocks::ControllerActionResponse, out_dir::OutDir,
routing_test_helpers::*, test_helpers::*,
},
},
sandbox_util::DictExt,
},
::routing::{
capability_source::{
AggregateCapability, AggregateInstance, AggregateMember, ComponentCapability,
},
error::ComponentInstanceError,
resolving::ResolverError,
},
assert_matches::assert_matches,
async_trait::async_trait,
async_utils::PollExt,
bedrock_error::{BedrockError, DowncastErrorForTest},
cm_rust::*,
cm_rust_testing::*,
cm_types::RelativePath,
fasync::TestExecutor,
fidl::endpoints::{ClientEnd, ProtocolMarker, ServerEnd},
fidl_fidl_examples_routing_echo as echo, fidl_fuchsia_component as fcomponent,
fidl_fuchsia_component_decl as fdecl, fidl_fuchsia_component_resolution as fresolution,
fidl_fuchsia_component_runner as fcrunner, fidl_fuchsia_component_sandbox as fsandbox,
fidl_fuchsia_io as fio, fidl_fuchsia_mem as fmem, fuchsia_async as fasync,
fuchsia_zircon::{self as zx, AsHandleRef},
futures::{
channel::{mpsc, oneshot},
join,
lock::Mutex,
StreamExt,
},
maplit::btreemap,
moniker::{ChildName, ChildNameBase, Moniker, MonikerBase},
routing::component_instance::ComponentInstanceInterface,
routing_test_helpers::{
default_service_capability, instantiate_common_routing_tests, RoutingTestModel,
},
sandbox::Open,
std::{
collections::HashSet,
pin::pin,
sync::{Arc, Weak},
task::Poll,
},
tracing::warn,
vfs::{execution_scope::ExecutionScope, pseudo_directory, service},
};
instantiate_common_routing_tests! { RoutingTestBuilder }
#[test]
fn namespace_teardown_processes_final_request() {
// We will replace the target component's ExecutionScope with one with a custom executor that
// we can run manually.
let (ehandle_tx, ehandle_rx) = std::sync::mpsc::channel();
let (run_scope_executor_tx, run_scope_executor_rx) = std::sync::mpsc::channel();
// Spawn a new thread for the new executor because there is a one executor per thread rule.
let _scope_executor_thread = std::thread::spawn(move || {
let mut executor = fasync::TestExecutor::new();
ehandle_tx.send(fasync::EHandle::local()).unwrap();
run_scope_executor_rx.recv().unwrap();
executor.run_singlethreaded(std::future::pending::<()>());
});
let mut executor = fasync::TestExecutor::new();
// Run the test until we stop the root component.
let (_test, echo_proxy, mut stop_fut) = executor.run_singlethreaded(async move {
let components = vec![
(
"root",
ComponentDeclBuilder::new()
.use_(
UseBuilder::protocol().name("foo").source(UseSource::Child("leaf".into())),
)
.child(ChildBuilder::new().name("leaf"))
.build(),
),
(
"leaf",
ComponentDeclBuilder::new()
.protocol_default("foo")
.expose(ExposeBuilder::protocol().name("foo").source(ExposeSource::Self_))
.build(),
),
];
let test = RoutingTestBuilder::new("root", components).build().await;
let root_component = test.model.root().clone();
let ehandle = ehandle_rx.recv().unwrap();
let scope = ExecutionScope::build().executor(ehandle).new();
ActionSet::register(
root_component.clone(),
StartAction::new_with_scope(
StartReason::Debug,
None,
IncomingCapabilities::default(),
scope,
),
)
.await
.unwrap();
let resolved_url = RoutingTest::resolved_url("root");
test.mock_runner.wait_for_url(&resolved_url).await;
let root_namespace = test.mock_runner.get_namespace(&resolved_url).unwrap();
let echo_proxy = capability_util::connect_to_svc_in_namespace::<echo::EchoMarker>(
&root_namespace,
&"/svc/foo".parse().unwrap(),
)
.await;
let stop_fut = async move { root_component.stop().await };
(test, echo_proxy, stop_fut)
});
// The future that stops the component should stall at the point it has told the namespace to
// shutdown and is waiting for its tasks to drain. Since we aren't running the ExecutionScope's
// executor yet, that wait should stall.
let mut stop_fut = pin!(stop_fut);
assert!(executor.run_until_stalled(&mut stop_fut).is_pending());
// Now allow the ExecutionScope's executor to run so it can process the namespace request.
run_scope_executor_tx.send(()).unwrap();
// The namespace request should get processed, even though the namespace was told to
// shutdown.
executor.run_singlethreaded(async move {
stop_fut.await.unwrap();
capability_util::call_echo_and_validate_result(echo_proxy, ExpectedResult::Ok).await;
});
}
#[fuchsia::test]
async fn namespace_teardown_rejects_late_request() {
let components = vec![
(
"root",
ComponentDeclBuilder::new()
.use_(UseBuilder::protocol().name("foo").source(UseSource::Child("leaf".into())))
.child(ChildBuilder::new().name("leaf"))
.build(),
),
(
"leaf",
ComponentDeclBuilder::new()
.protocol_default("foo")
.expose(ExposeBuilder::protocol().name("foo").source(ExposeSource::Self_))
.build(),
),
];
let test = RoutingTestBuilder::new("root", components).build().await;
test.start_instance_and_wait_start(&".".parse().unwrap()).await.unwrap();
// Stop the component.
let resolved_url = RoutingTest::resolved_url("root");
let root_namespace = test.mock_runner.get_namespace(&resolved_url).unwrap();
test.model.root().stop().await.unwrap();
// Trying to connect to the stopped component's namespace now should fail.
let echo_proxy = capability_util::connect_to_svc_in_namespace::<echo::EchoMarker>(
&root_namespace,
&"/svc/foo".parse().unwrap(),
)
.await;
capability_util::call_echo_and_validate_result(echo_proxy, ExpectedResult::ErrWithNoEpitaph)
.await;
}
/// a
/// \
/// b
///
/// a: offers service /svc/foo from self as /svc/bar
/// b: uses service /svc/bar as /svc/hippo
///
/// This test verifies that the parent, if subscribed to the CapabilityRequested event will receive
/// if when the child connects to /svc/hippo.
#[fuchsia::test]
async fn capability_requested_event_at_parent() {
let components = vec![
(
"a",
ComponentDeclBuilder::new()
.protocol_default("foo")
.offer(OfferBuilder::protocol()
.name("foo")
.target_name("bar")
.source(OfferSource::Self_)
.target(OfferTarget::static_child("b".to_string()))
)
.use_(UseBuilder::event_stream()
.name("capability_requested")
.path("/events/capability_requested")
.filter(
btreemap! { "name".to_string() => DictionaryValue::Str("foo".to_string()) },
)
)
.child_default("b")
.build(),
),
(
"b",
ComponentDeclBuilder::new()
.use_(
UseBuilder::protocol()
.name("bar")
.path("/svc/hippo")
)
.build(),
),
];
let test = RoutingTestBuilder::new("a", components)
.set_builtin_capabilities(vec![CapabilityDecl::EventStream(EventStreamDecl {
name: "capability_requested".parse().unwrap(),
})])
.build()
.await;
let namespace_root = test.bind_and_get_namespace(Moniker::root()).await;
let event_stream =
capability_util::connect_to_svc_in_namespace::<fcomponent::EventStreamMarker>(
&namespace_root,
&"/events/capability_requested".parse().unwrap(),
)
.await;
let namespace_b = test.bind_and_get_namespace(vec!["b"].try_into().unwrap()).await;
let _echo_proxy = capability_util::connect_to_svc_in_namespace::<echo::EchoMarker>(
&namespace_b,
&"/svc/hippo".parse().unwrap(),
)
.await;
let event = event_stream.get_next().await.unwrap().into_iter().next().unwrap();
// 'b' is the target and 'a' is receiving the event so the moniker
// is '/b'.
assert_matches!(&event,
fcomponent::Event {
header: Some(fcomponent::EventHeader {
moniker: Some(moniker), .. }), ..
} if *moniker == "b".to_string() );
assert_matches!(&event,
fcomponent::Event {
header: Some(fcomponent::EventHeader {
component_url: Some(component_url), .. }), ..
} if *component_url == "test:///b".to_string() );
assert_matches!(&event,
fcomponent::Event {
payload:
Some(fcomponent::EventPayload::CapabilityRequested(
fcomponent::CapabilityRequestedPayload { name: Some(name), .. })), ..}
if *name == "foo".to_string()
);
}
/// a
/// \
/// b
/// / \
/// [c] [d]
/// a: offers service /svc/hippo to b
/// b: offers service /svc/hippo to collection, creates [c]
/// [c]: instance in collection uses service /svc/hippo
/// [d]: ditto, but with /data/hippo
#[fuchsia::test]
async fn use_in_collection() {
let components = vec![
(
"a",
ComponentDeclBuilder::new()
.capability(CapabilityBuilder::directory().name("foo_data").path("/data/foo"))
.protocol_default("foo")
.offer(
OfferBuilder::directory()
.name("foo_data")
.target_name("hippo_data")
.source(OfferSource::Self_)
.target(OfferTarget::static_child("b".to_string()))
.rights(fio::R_STAR_DIR),
)
.offer(
OfferBuilder::protocol()
.name("foo")
.target_name("hippo")
.source(OfferSource::Self_)
.target(OfferTarget::static_child("b".to_string())),
)
.child_default("b")
.build(),
),
(
"b",
ComponentDeclBuilder::new()
.use_(
UseBuilder::protocol()
.source(UseSource::Framework)
.name("fuchsia.component.Realm"),
)
.offer(
OfferBuilder::directory()
.name("hippo_data")
.source(OfferSource::Parent)
.target(OfferTarget::Collection("coll".parse().unwrap()))
.rights(fio::R_STAR_DIR),
)
.offer(
OfferBuilder::protocol()
.name("hippo")
.source(OfferSource::Parent)
.target(OfferTarget::Collection("coll".parse().unwrap())),
)
.collection_default("coll")
.build(),
),
(
"c",
ComponentDeclBuilder::new()
.use_(UseBuilder::directory().name("hippo_data").path("/data/hippo"))
.build(),
),
(
"d",
ComponentDeclBuilder::new()
.use_(UseBuilder::protocol().name("hippo").path("/svc/hippo"))
.build(),
),
];
let test = RoutingTest::new("a", components).await;
test.create_dynamic_child(
&vec!["b"].try_into().unwrap(),
"coll",
ChildDecl {
name: "c".to_string(),
url: "test:///c".to_string(),
startup: fdecl::StartupMode::Lazy,
environment: None,
on_terminate: None,
config_overrides: None,
},
)
.await;
test.create_dynamic_child(
&vec!["b"].try_into().unwrap(),
"coll",
ChildDecl {
name: "d".to_string(),
url: "test:///d".to_string(),
startup: fdecl::StartupMode::Lazy,
environment: None,
on_terminate: None,
config_overrides: None,
},
)
.await;
test.check_use(
vec!["b", "coll:c"].try_into().unwrap(),
CheckUse::default_directory(ExpectedResult::Ok),
)
.await;
test.check_use(
vec!["b", "coll:d"].try_into().unwrap(),
CheckUse::Protocol { path: default_service_capability(), expected_res: ExpectedResult::Ok },
)
.await;
}
/// a
/// \
/// b
/// \
/// [c]
/// a: offers service /svc/hippo to b
/// b: creates [c]
/// [c]: tries to use /svc/hippo, but can't because service was not offered to its collection
#[fuchsia::test]
async fn use_in_collection_not_offered() {
let components = vec![
(
"a",
ComponentDeclBuilder::new()
.capability(CapabilityBuilder::directory().name("foo_data").path("/data/foo"))
.protocol_default("foo")
.offer(
OfferBuilder::directory()
.name("foo_data")
.target_name("hippo_data")
.source(OfferSource::Self_)
.target(OfferTarget::static_child("b".to_string()))
.rights(fio::R_STAR_DIR),
)
.offer(
OfferBuilder::protocol()
.name("foo")
.target_name("hippo")
.source(OfferSource::Self_)
.target(OfferTarget::static_child("b".to_string())),
)
.child_default("b")
.build(),
),
(
"b",
ComponentDeclBuilder::new()
.use_(
UseBuilder::protocol()
.source(UseSource::Framework)
.name("fuchsia.component.Realm"),
)
.collection_default("coll")
.build(),
),
(
"c",
ComponentDeclBuilder::new()
.use_(UseBuilder::directory().name("hippo_data").path("/data/hippo"))
.use_(UseBuilder::protocol().name("hippo"))
.build(),
),
];
let test = RoutingTest::new("a", components).await;
test.create_dynamic_child(
&vec!["b"].try_into().unwrap(),
"coll",
ChildDecl {
name: "c".to_string(),
url: "test:///c".to_string(),
startup: fdecl::StartupMode::Lazy,
environment: None,
on_terminate: None,
config_overrides: None,
},
)
.await;
test.check_use(
vec!["b", "coll:c"].try_into().unwrap(),
CheckUse::default_directory(ExpectedResult::Err(zx::Status::NOT_FOUND)),
)
.await;
test.check_use(
vec!["b", "coll:c"].try_into().unwrap(),
CheckUse::Protocol {
path: default_service_capability(),
expected_res: ExpectedResult::Err(zx::Status::NOT_FOUND),
},
)
.await;
}
/// a
/// \
/// b
/// / \
/// [c] [d]
/// a: offers service /svc/hippo to b
/// b: creates [c] and [d], dynamically offers service /svc/hippo to [c], but not [d].
/// [c]: instance in collection uses service /svc/hippo
/// [d]: instance in collection tries and fails to use service /svc/hippo
#[fuchsia::test]
async fn dynamic_offer_from_parent() {
let components = vec![
(
"a",
ComponentDeclBuilder::new()
.protocol_default("foo")
.offer(
OfferBuilder::protocol()
.name("foo")
.target_name("hippo")
.source(OfferSource::Self_)
.target(OfferTarget::static_child("b".to_string())),
)
.child_default("b")
.build(),
),
(
"b",
ComponentDeclBuilder::new()
.use_(
UseBuilder::protocol()
.source(UseSource::Framework)
.name("fuchsia.component.Realm"),
)
.collection(
CollectionBuilder::new()
.name("coll")
.allowed_offers(cm_types::AllowedOffers::StaticAndDynamic),
)
.build(),
),
("c", ComponentDeclBuilder::new().use_(UseBuilder::protocol().name("hippo")).build()),
("d", ComponentDeclBuilder::new().use_(UseBuilder::protocol().name("hippo")).build()),
];
let test = RoutingTest::new("a", components).await;
test.create_dynamic_child_with_args(
&vec!["b"].try_into().unwrap(),
"coll",
ChildDecl {
name: "c".to_string(),
url: "test:///c".to_string(),
startup: fdecl::StartupMode::Lazy,
environment: None,
on_terminate: None,
config_overrides: None,
},
fcomponent::CreateChildArgs {
dynamic_offers: Some(vec![fdecl::Offer::Protocol(fdecl::OfferProtocol {
source_name: Some("hippo".to_string()),
source: Some(fdecl::Ref::Parent(fdecl::ParentRef)),
target_name: Some("hippo".to_string()),
dependency_type: Some(fdecl::DependencyType::Strong),
..Default::default()
})]),
..Default::default()
},
)
.await;
test.create_dynamic_child(
&vec!["b"].try_into().unwrap(),
"coll",
ChildDecl {
name: "d".to_string(),
url: "test:///d".to_string(),
startup: fdecl::StartupMode::Lazy,
environment: None,
on_terminate: None,
config_overrides: None,
},
)
.await;
test.check_use(
vec!["b", "coll:c"].try_into().unwrap(),
CheckUse::Protocol { path: default_service_capability(), expected_res: ExpectedResult::Ok },
)
.await;
test.check_use(
vec!["b", "coll:d"].try_into().unwrap(),
CheckUse::Protocol {
path: default_service_capability(),
expected_res: ExpectedResult::Err(zx::Status::NOT_FOUND),
},
)
.await;
}
/// a
/// / \
/// [b] [c]
/// a: creates [b]. creates [c] in the same collection, with a dynamic offer from [b].
/// [b]: instance in collection exposes /svc/hippo
/// [c]: instance in collection uses /svc/hippo
#[fuchsia::test]
async fn dynamic_offer_siblings_same_collection() {
let components = vec![
(
"a",
ComponentDeclBuilder::new()
.use_(
UseBuilder::protocol()
.source(UseSource::Framework)
.name("fuchsia.component.Realm"),
)
.collection(
CollectionBuilder::new()
.name("coll")
.allowed_offers(cm_types::AllowedOffers::StaticAndDynamic),
)
.build(),
),
(
"b",
ComponentDeclBuilder::new()
.protocol_default("hippo")
.expose(ExposeBuilder::protocol().name("hippo").source(ExposeSource::Self_))
.build(),
),
("c", ComponentDeclBuilder::new().use_(UseBuilder::protocol().name("hippo")).build()),
];
let test = RoutingTest::new("a", components).await;
test.create_dynamic_child(
&Moniker::root(),
"coll",
ChildDecl {
name: "b".to_string(),
url: "test:///b".to_string(),
startup: fdecl::StartupMode::Lazy,
environment: None,
on_terminate: None,
config_overrides: None,
},
)
.await;
test.create_dynamic_child_with_args(
&Moniker::root(),
"coll",
ChildDecl {
name: "c".to_string(),
url: "test:///c".to_string(),
startup: fdecl::StartupMode::Lazy,
environment: None,
on_terminate: None,
config_overrides: None,
},
fcomponent::CreateChildArgs {
dynamic_offers: Some(vec![fdecl::Offer::Protocol(fdecl::OfferProtocol {
source_name: Some("hippo".to_string()),
source: Some(fdecl::Ref::Child(fdecl::ChildRef {
name: "b".to_string(),
collection: Some("coll".to_string()),
})),
target_name: Some("hippo".to_string()),
dependency_type: Some(fdecl::DependencyType::Strong),
..Default::default()
})]),
..Default::default()
},
)
.await;
test.check_use(
vec!["coll:c"].try_into().unwrap(),
CheckUse::Protocol { path: default_service_capability(), expected_res: ExpectedResult::Ok },
)
.await;
}
/// a
/// / \
/// [b] [c]
/// a: creates [b]. creates [c] in a different collection, with a dynamic offer from [b].
/// [b]: instance in `source_coll` exposes /svc/hippo
/// [c]: instance in `target_coll` uses /svc/hippo
#[fuchsia::test]
async fn dynamic_offer_siblings_cross_collection() {
let components = vec![
(
"a",
ComponentDeclBuilder::new()
.use_(
UseBuilder::protocol()
.source(UseSource::Framework)
.name("fuchsia.component.Realm"),
)
.collection_default("source_coll")
.collection(
CollectionBuilder::new()
.name("target_coll")
.allowed_offers(cm_types::AllowedOffers::StaticAndDynamic),
)
.build(),
),
(
"b",
ComponentDeclBuilder::new()
.protocol_default("hippo")
.expose(ExposeBuilder::protocol().name("hippo").source(ExposeSource::Self_))
.build(),
),
("c", ComponentDeclBuilder::new().use_(UseBuilder::protocol().name("hippo")).build()),
];
let test = RoutingTest::new("a", components).await;
test.create_dynamic_child(
&Moniker::root(),
"source_coll",
ChildDecl {
name: "b".to_string(),
url: "test:///b".to_string(),
startup: fdecl::StartupMode::Lazy,
environment: None,
on_terminate: None,
config_overrides: None,
},
)
.await;
test.create_dynamic_child_with_args(
&Moniker::root(),
"target_coll",
ChildDecl {
name: "c".to_string(),
url: "test:///c".to_string(),
startup: fdecl::StartupMode::Lazy,
environment: None,
on_terminate: None,
config_overrides: None,
},
fcomponent::CreateChildArgs {
dynamic_offers: Some(vec![fdecl::Offer::Protocol(fdecl::OfferProtocol {
source: Some(fdecl::Ref::Child(fdecl::ChildRef {
name: "b".to_string(),
collection: Some("source_coll".to_string()),
})),
source_name: Some("hippo".to_string()),
dependency_type: Some(fdecl::DependencyType::Strong),
target_name: Some("hippo".to_string()),
..Default::default()
})]),
..Default::default()
},
)
.await;
test.check_use(
vec!["target_coll:c"].try_into().unwrap(),
CheckUse::Protocol { path: default_service_capability(), expected_res: ExpectedResult::Ok },
)
.await;
}
/// a
/// / \
/// [b] [c]
/// a: creates [b]. creates [c] in the same collection, with a dynamic offer from [b].
/// [b]: instance in collection exposes /svc/hippo
/// [c]: instance in collection uses /svc/hippo. Can't use it after [b] is destroyed and recreated.
#[fuchsia::test]
async fn dynamic_offer_destroyed_on_source_destruction() {
let components = vec![
(
"a",
ComponentDeclBuilder::new()
.use_(
UseBuilder::protocol()
.source(UseSource::Framework)
.name("fuchsia.component.Realm"),
)
.collection(
CollectionBuilder::new()
.name("coll")
.allowed_offers(cm_types::AllowedOffers::StaticAndDynamic),
)
.build(),
),
(
"b",
ComponentDeclBuilder::new()
.protocol_default("hippo")
.expose(ExposeBuilder::protocol().name("hippo").source(ExposeSource::Self_))
.build(),
),
("c", ComponentDeclBuilder::new().use_(UseBuilder::protocol().name("hippo")).build()),
];
let test = RoutingTest::new("a", components).await;
test.create_dynamic_child(
&Moniker::root(),
"coll",
ChildDecl {
name: "b".to_string(),
url: "test:///b".to_string(),
startup: fdecl::StartupMode::Lazy,
environment: None,
on_terminate: None,
config_overrides: None,
},
)
.await;
test.create_dynamic_child_with_args(
&Moniker::root(),
"coll",
ChildDecl {
name: "c".to_string(),
url: "test:///c".to_string(),
startup: fdecl::StartupMode::Lazy,
environment: None,
on_terminate: None,
config_overrides: None,
},
fcomponent::CreateChildArgs {
dynamic_offers: Some(vec![fdecl::Offer::Protocol(fdecl::OfferProtocol {
source_name: Some("hippo".to_string()),
source: Some(fdecl::Ref::Child(fdecl::ChildRef {
name: "b".to_string(),
collection: Some("coll".to_string()),
})),
target_name: Some("hippo".to_string()),
dependency_type: Some(fdecl::DependencyType::Strong),
..Default::default()
})]),
..Default::default()
},
)
.await;
test.check_use(
vec!["coll:c"].try_into().unwrap(),
CheckUse::Protocol { path: default_service_capability(), expected_res: ExpectedResult::Ok },
)
.await;
test.destroy_dynamic_child(Moniker::root(), "coll", "b").await;
test.create_dynamic_child(
&Moniker::root(),
"coll",
ChildDecl {
name: "b".to_string(),
url: "test:///b".to_string(),
startup: fdecl::StartupMode::Lazy,
environment: None,
on_terminate: None,
config_overrides: None,
},
)
.await;
test.check_use(
vec!["coll:c"].try_into().unwrap(),
CheckUse::Protocol {
path: default_service_capability(),
expected_res: ExpectedResult::Err(zx::Status::NOT_FOUND),
},
)
.await;
}
/// a
/// / \
/// [b] [c]
/// a: creates [b]. creates [c] in the same collection, with a dynamic offer from [b].
/// [b]: instance in collection exposes /data/hippo
/// [c]: instance in collection uses /data/hippo. Can't use it after [c] is destroyed and recreated.
#[fuchsia::test]
async fn dynamic_offer_destroyed_on_target_destruction() {
let components = vec![
(
"a",
ComponentDeclBuilder::new()
.use_(
UseBuilder::protocol()
.source(UseSource::Framework)
.name("fuchsia.component.Realm"),
)
.collection(
CollectionBuilder::new()
.name("coll")
.allowed_offers(cm_types::AllowedOffers::StaticAndDynamic),
)
.build(),
),
(
"b",
ComponentDeclBuilder::new()
.capability(CapabilityBuilder::directory().name("hippo_data").path("/data/foo"))
.expose(
ExposeBuilder::directory()
.name("hippo_data")
.source(ExposeSource::Self_)
.rights(fio::R_STAR_DIR),
)
.build(),
),
(
"c",
ComponentDeclBuilder::new()
.use_(UseBuilder::directory().name("hippo_data").path("/data/hippo"))
.build(),
),
];
let test = RoutingTest::new("a", components).await;
test.create_dynamic_child(
&Moniker::root(),
"coll",
ChildDecl {
name: "b".to_string(),
url: "test:///b".to_string(),
startup: fdecl::StartupMode::Lazy,
environment: None,
on_terminate: None,
config_overrides: None,
},
)
.await;
test.create_dynamic_child_with_args(
&Moniker::root(),
"coll",
ChildDecl {
name: "c".to_string(),
url: "test:///c".to_string(),
startup: fdecl::StartupMode::Lazy,
environment: None,
on_terminate: None,
config_overrides: None,
},
fcomponent::CreateChildArgs {
dynamic_offers: Some(vec![fdecl::Offer::Directory(fdecl::OfferDirectory {
source_name: Some("hippo_data".to_string()),
source: Some(fdecl::Ref::Child(fdecl::ChildRef {
name: "b".to_string(),
collection: Some("coll".to_string()),
})),
target_name: Some("hippo_data".to_string()),
dependency_type: Some(fdecl::DependencyType::Strong),
..Default::default()
})]),
..Default::default()
},
)
.await;
test.check_use(
vec!["coll:c"].try_into().unwrap(),
CheckUse::default_directory(ExpectedResult::Ok),
)
.await;
test.destroy_dynamic_child(Moniker::root(), "coll", "c").await;
test.create_dynamic_child(
&Moniker::root(),
"coll",
ChildDecl {
name: "c".to_string(),
url: "test:///c".to_string(),
startup: fdecl::StartupMode::Lazy,
environment: None,
on_terminate: None,
config_overrides: None,
},
)
.await;
test.check_use(
vec!["coll:c"].try_into().unwrap(),
CheckUse::default_directory(ExpectedResult::Err(zx::Status::NOT_FOUND)),
)
.await;
}
/// a
/// / \
/// b [c]
/// \
/// d
/// a: creates [c], with a dynamic offer from b.
/// b: exposes /svc/hippo
/// [c]: instance in collection, offers /svc/hippo to d.
/// d: static child of dynamic instance [c]. uses /svc/hippo.
#[fuchsia::test]
async fn dynamic_offer_to_static_offer() {
let components = vec![
(
"a",
ComponentDeclBuilder::new()
.use_(
UseBuilder::protocol()
.source(UseSource::Framework)
.name("fuchsia.component.Realm"),
)
.collection(
CollectionBuilder::new()
.name("coll")
.allowed_offers(cm_types::AllowedOffers::StaticAndDynamic),
)
.child_default("b")
.build(),
),
(
"b",
ComponentDeclBuilder::new()
.protocol_default("hippo")
.expose(ExposeBuilder::protocol().name("hippo").source(ExposeSource::Self_))
.build(),
),
(
"c",
ComponentDeclBuilder::new()
.offer(
OfferBuilder::protocol()
.name("hippo")
.source(OfferSource::Parent)
.target(OfferTarget::static_child("d".to_string())),
)
.child_default("d")
.build(),
),
(
"d",
ComponentDeclBuilder::new()
.use_(UseBuilder::protocol().name("hippo"))
.child_default("d")
.build(),
),
];
let test = RoutingTest::new("a", components).await;
test.create_dynamic_child_with_args(
&Moniker::root(),
"coll",
ChildDecl {
name: "c".to_string(),
url: "test:///c".to_string(),
startup: fdecl::StartupMode::Lazy,
environment: None,
on_terminate: None,
config_overrides: None,
},
fcomponent::CreateChildArgs {
dynamic_offers: Some(vec![fdecl::Offer::Protocol(fdecl::OfferProtocol {
source_name: Some("hippo".to_string()),
source: Some(fdecl::Ref::Child(fdecl::ChildRef {
name: "b".to_string(),
collection: None,
})),
target_name: Some("hippo".to_string()),
dependency_type: Some(fdecl::DependencyType::Strong),
..Default::default()
})]),
..Default::default()
},
)
.await;
test.check_use(
vec!["coll:c", "d"].try_into().unwrap(),
CheckUse::Protocol { path: default_service_capability(), expected_res: ExpectedResult::Ok },
)
.await;
}
/// Tests that a dynamic component can use a capability from the dict passed in CreateChildArgs.
///
/// a
/// /
/// [b]
///
/// a: creates [b], with a dict that contains an Open for `hippo`
/// [b]: instance in collection, uses the `hippo` protocol.
#[fuchsia::test]
async fn create_child_with_dict() {
let components = vec![
(
"a",
ComponentDeclBuilder::new()
.use_(
UseBuilder::protocol()
.source(UseSource::Framework)
.name("fuchsia.component.Realm"),
)
.collection_default("coll")
.build(),
),
("b", ComponentDeclBuilder::new().use_(UseBuilder::protocol().name("hippo")).build()),
];
// Create a dictionary with a sender for the `hippo` protocol.
let dict = sandbox::Dict::new();
let (receiver, sender) = sandbox::Receiver::new();
// Serve the `fidl.examples.routing.echo.Echo` protocol on the receiver.
let _task = fasync::Task::spawn(async move {
let mut tasks = fasync::TaskGroup::new();
loop {
let Some(message) = receiver.receive().await else {
return;
};
let server_end = ServerEnd::<echo::EchoMarker>::new(message.channel);
let stream: echo::EchoRequestStream = server_end.into_stream().unwrap();
tasks.add(fasync::Task::spawn(async move {
EchoProtocol::serve(stream).await.expect("failed to serve Echo");
}));
}
});
// CreateChild dictionary entries must be Open capabilities.
// TODO(https://fxbug.dev/319542502): Insert the external Router type, once it exists
let open: sandbox::Open = sender.into();
dict.lock_entries().insert("hippo".parse().unwrap(), sandbox::Capability::Open(open));
let dictionary_client_end: ClientEnd<fsandbox::DictionaryMarker> = dict.into();
let test = RoutingTest::new("a", components).await;
test.create_dynamic_child_with_args(
&Moniker::root(),
"coll",
ChildDecl {
name: "b".to_string(),
url: "test:///b".to_string(),
startup: fdecl::StartupMode::Lazy,
environment: None,
on_terminate: None,
config_overrides: None,
},
fcomponent::CreateChildArgs {
dictionary: Some(dictionary_client_end),
..Default::default()
},
)
.await;
test.check_use(
vec!["coll:b"].try_into().unwrap(),
CheckUse::Protocol { path: default_service_capability(), expected_res: ExpectedResult::Ok },
)
.await;
}
#[fuchsia::test]
async fn destroying_instance_kills_framework_service_task() {
let components = vec![
("a", ComponentDeclBuilder::new().child_default("b").build()),
(
"b",
ComponentDeclBuilder::new()
.use_(
UseBuilder::protocol()
.source(UseSource::Framework)
.name("fuchsia.component.Realm"),
)
.build(),
),
];
let test = RoutingTest::new("a", components).await;
// Connect to `Realm`, which is a framework service.
let namespace = test.bind_and_get_namespace(vec!["b"].try_into().unwrap()).await;
let proxy = capability_util::connect_to_svc_in_namespace::<fcomponent::RealmMarker>(
&namespace,
&"/svc/fuchsia.component.Realm".parse().unwrap(),
)
.await;
// Destroy `b`. This should cause the task hosted for `Realm` to be cancelled.
test.model.root().destroy_child("b".try_into().unwrap(), 0).await.expect("destroy failed");
let mut event_stream = proxy.take_event_stream();
assert_matches!(event_stream.next().await, None);
}
#[fuchsia::test]
async fn destroying_instance_blocks_on_routing() {
// Directories and protocols have a different routing flow so test both.
let components = vec![
(
"a",
ComponentDeclBuilder::new()
.offer(
OfferBuilder::protocol()
.name("foo")
.source(OfferSource::static_child("c".into()))
.target(OfferTarget::static_child("b".into())),
)
.offer(
OfferBuilder::directory()
.name("foo_data")
.source(OfferSource::static_child("c".into()))
.target(OfferTarget::static_child("b".into())),
)
.child_default("b")
.child_default("c")
.build(),
),
(
"b",
ComponentDeclBuilder::new()
.use_(UseBuilder::protocol().name("foo").path("/svc/echo"))
.use_(UseBuilder::directory().name("foo_data").path("/data"))
.build(),
),
(
"c",
ComponentDeclBuilder::new()
.protocol_default("foo")
.capability(CapabilityBuilder::directory().name("foo_data").path("/data/foo"))
.expose(ExposeBuilder::protocol().name("foo").source(ExposeSource::Self_))
.expose(
ExposeBuilder::directory()
.name("foo_data")
.source(ExposeSource::Self_)
.rights(fio::R_STAR_DIR),
)
.build(),
),
];
// Cause resolution for `c` to block until we explicitly tell it to proceed. This is useful
// for coordinating the progress of `echo`'s routing task with destruction.
let builder = RoutingTestBuilder::new("a", components);
let (resolved_tx, resolved_rx) = oneshot::channel::<()>();
let (continue_tx, continue_rx) = oneshot::channel::<()>();
let test = builder.add_blocker("c", resolved_tx, continue_rx).build().await;
// Connect to `echo` in `b`'s namespace to kick off a protocol routing task.
let (_, component_name) = test
.start_and_get_instance(&vec!["b"].try_into().unwrap(), StartReason::Eager, true)
.await
.unwrap();
let component_resolved_url = RoutingTest::resolved_url(&component_name);
let namespace = test.mock_runner.get_namespace(&component_resolved_url).unwrap();
let echo_proxy = capability_util::connect_to_svc_in_namespace::<echo::EchoMarker>(
&namespace,
&"/svc/echo".parse().unwrap(),
)
.await;
// Connect to `data` in `b`'s namespace to kick off a directory routing task.
let dir_proxy =
capability_util::take_dir_from_namespace(&namespace, &"/data".parse().unwrap()).await;
let file_proxy = fuchsia_fs::directory::open_file_no_describe(
&dir_proxy,
"hippo",
fio::OpenFlags::RIGHT_READABLE,
)
.unwrap();
capability_util::add_dir_to_namespace(&namespace, &"/data".parse().unwrap(), dir_proxy).await;
// Destroy `b`.
let root = test.model.root().find_and_maybe_resolve(&Moniker::root()).await.unwrap();
let root_clone = root.clone();
let destroy_nf =
fasync::Task::spawn(
async move { root_clone.destroy_child("b".try_into().unwrap(), 0).await },
);
// Give the destroy action some time to complete. Sleeping is not an ideal testing strategy,
// but it helps add confidence to the test because it makes it more likely the test would
// fail if the destroy action is not correctly blocking on the routing task.
fasync::Timer::new(fasync::Time::after(zx::Duration::from_seconds(5))).await;
// Wait until routing reaches resolution. It should get here because `Destroy` should not
// cancel the routing task.
let _ = resolved_rx.await.unwrap();
// `b` is not yet destroyed.
let state = root.lock_resolved_state().await.unwrap();
state.get_child(&ChildName::parse("b").unwrap()).expect("b was destroyed");
drop(state);
// Let routing complete. This should allow destruction to complete.
continue_tx.send(()).unwrap();
destroy_nf.await.unwrap();
// Verify the connection to `echo` and `data` was bound by the provider.
capability_util::call_echo_and_validate_result(echo_proxy, ExpectedResult::Ok).await;
assert_eq!(fuchsia_fs::file::read_to_string(&file_proxy).await.unwrap(), "hello");
}
/// a
/// \
/// b
///
/// a: declares runner "elf" with service format!("/svc/{}", fcrunner::ComponentRunnerMarker::DEBUG_NAME) from "self".
/// a: registers runner "elf" from self in environment as "hobbit".
/// b: uses runner "hobbit".
#[fuchsia::test]
async fn use_runner_from_parent_environment() {
let components = vec![
(
"a",
ComponentDeclBuilder::new()
.child(ChildBuilder::new().name("b").environment("env"))
.environment(EnvironmentBuilder::new().name("env").runner(RunnerRegistration {
source_name: "elf".parse().unwrap(),
source: RegistrationSource::Self_,
target_name: "hobbit".parse().unwrap(),
}))
.runner_default("elf")
.build(),
),
("b", ComponentDeclBuilder::new_empty_component().add_program("hobbit").build()),
];
// Set up the system.
let (runner_service, mut receiver) =
create_service_directory_entry::<fcrunner::ComponentRunnerMarker>();
let universe = RoutingTestBuilder::new("a", components)
// Component "b" exposes a runner service.
.add_outgoing_path(
"a",
format!("/svc/{}", fcrunner::ComponentRunnerMarker::DEBUG_NAME).parse().unwrap(),
runner_service,
)
.build()
.await;
join!(
// Bind "b". We expect to see a call to our runner service for the new component.
async move {
universe.start_instance(&vec!["b"].try_into().unwrap()).await.unwrap();
},
// Wait for a request, and ensure it has the correct URL.
async move {
assert_eq!(
wait_for_runner_request(&mut receiver).await.resolved_url,
Some("test:///b_resolved".to_string())
);
}
);
}
/// a
/// \
/// [b]
///
/// a: declares runner "elf" with service format!("/svc/{}", fcrunner::ComponentRunnerMarker::DEBUG_NAME) from "self".
/// a: registers runner "elf" from self in environment as "hobbit".
/// b: instance in collection uses runner "hobbit".
#[fuchsia::test]
async fn use_runner_from_environment_in_collection() {
let components = vec![
(
"a",
ComponentDeclBuilder::new()
.collection(CollectionBuilder::new().name("coll").environment("env"))
.environment(EnvironmentBuilder::new().name("env").runner(RunnerRegistration {
source_name: "elf".parse().unwrap(),
source: RegistrationSource::Self_,
target_name: "hobbit".parse().unwrap(),
}))
.use_(
UseBuilder::protocol()
.source(UseSource::Framework)
.name("fuchsia.component.Realm"),
)
.runner_default("elf")
.build(),
),
("b", ComponentDeclBuilder::new_empty_component().add_program("hobbit").build()),
];
// Set up the system.
let (runner_service, mut receiver) =
create_service_directory_entry::<fcrunner::ComponentRunnerMarker>();
let universe = RoutingTestBuilder::new("a", components)
// Component "a" exposes a runner service.
.add_outgoing_path(
"a",
format!("/svc/{}", fcrunner::ComponentRunnerMarker::DEBUG_NAME).parse().unwrap(),
runner_service,
)
.build()
.await;
universe
.create_dynamic_child(
&Moniker::root(),
"coll",
ChildDecl {
name: "b".to_string(),
url: "test:///b".to_string(),
startup: fdecl::StartupMode::Lazy,
environment: None,
on_terminate: None,
config_overrides: None,
},
)
.await;
join!(
// Bind "coll:b". We expect to see a call to our runner service for the new component.
async move {
universe.start_instance(&vec!["coll:b"].try_into().unwrap()).await.unwrap();
},
// Wait for a request, and ensure it has the correct URL.
async move {
assert_eq!(
wait_for_runner_request(&mut receiver).await.resolved_url,
Some("test:///b_resolved".to_string())
);
}
);
}
/// a
/// \
/// b
/// \
/// c
///
/// a: declares runner "elf" as service format!("/svc/{}", fcrunner::ComponentRunnerMarker::DEBUG_NAME) from self.
/// a: offers runner "elf" from self to "b" as "dwarf".
/// b: registers runner "dwarf" from realm in environment as "hobbit".
/// c: uses runner "hobbit".
#[fuchsia::test]
async fn use_runner_from_grandparent_environment() {
let components = vec![
(
"a",
ComponentDeclBuilder::new()
.child_default("b")
.offer(
OfferBuilder::runner()
.name("elf")
.target_name("dwarf")
.source(OfferSource::Self_)
.target(OfferTarget::static_child("b".to_string())),
)
.runner_default("elf")
.build(),
),
(
"b",
ComponentDeclBuilder::new()
.child(ChildBuilder::new().name("c").environment("env"))
.environment(EnvironmentBuilder::new().name("env").runner(RunnerRegistration {
source_name: "dwarf".parse().unwrap(),
source: RegistrationSource::Parent,
target_name: "hobbit".parse().unwrap(),
}))
.build(),
),
("c", ComponentDeclBuilder::new_empty_component().add_program("hobbit").build()),
];
// Set up the system.
let (runner_service, mut receiver) =
create_service_directory_entry::<fcrunner::ComponentRunnerMarker>();
let universe = RoutingTestBuilder::new("a", components)
// Component "a" exposes a runner service.
.add_outgoing_path(
"a",
format!("/svc/{}", fcrunner::ComponentRunnerMarker::DEBUG_NAME).parse().unwrap(),
runner_service,
)
.build()
.await;
join!(
// Bind "c". We expect to see a call to our runner service for the new component.
async move {
universe.start_instance(&vec!["b", "c"].try_into().unwrap()).await.unwrap();
},
// Wait for a request, and ensure it has the correct URL.
async move {
assert_eq!(
wait_for_runner_request(&mut receiver).await.resolved_url,
Some("test:///c_resolved".to_string())
);
}
);
}
/// a
/// / \
/// b c
///
/// a: registers runner "dwarf" from "b" in environment as "hobbit".
/// b: exposes runner "elf" as service format!("/svc/{}", fcrunner::ComponentRunnerMarker::DEBUG_NAME) from self as "dwarf".
/// c: uses runner "hobbit".
#[fuchsia::test]
async fn use_runner_from_sibling_environment() {
let components = vec![
(
"a",
ComponentDeclBuilder::new()
.child_default("b")
.child(ChildBuilder::new().name("c").environment("env"))
.environment(EnvironmentBuilder::new().name("env").runner(RunnerRegistration {
source_name: "dwarf".parse().unwrap(),
source: RegistrationSource::Child("b".to_string()),
target_name: "hobbit".parse().unwrap(),
}))
.build(),
),
(
"b",
ComponentDeclBuilder::new()
.expose(
ExposeBuilder::runner()
.name("elf")
.target_name("dwarf")
.source(ExposeSource::Self_),
)
.runner_default("elf")
.build(),
),
("c", ComponentDeclBuilder::new_empty_component().add_program("hobbit").build()),
];
// Set up the system.
let (runner_service, mut receiver) =
create_service_directory_entry::<fcrunner::ComponentRunnerMarker>();
let universe = RoutingTestBuilder::new("a", components)
// Component "a" exposes a runner service.
.add_outgoing_path(
"b",
format!("/svc/{}", fcrunner::ComponentRunnerMarker::DEBUG_NAME).parse().unwrap(),
runner_service,
)
.build()
.await;
join!(
// Bind "c". We expect to see a call to our runner service for the new component.
async move {
universe.start_instance(&vec!["c"].try_into().unwrap()).await.unwrap();
},
// Wait for a request, and ensure it has the correct URL.
async move {
assert_eq!(
wait_for_runner_request(&mut receiver).await.resolved_url,
Some("test:///c_resolved".to_string())
);
}
);
}
/// a
/// \
/// b
/// \
/// c
///
/// a: declares runner "elf" as service format!("/svc/{}", fcrunner::ComponentRunnerMarker::DEBUG_NAME) from self.
/// a: registers runner "elf" from realm in environment as "hobbit".
/// b: creates environment extending from realm.
/// c: uses runner "hobbit".
#[fuchsia::test]
async fn use_runner_from_inherited_environment() {
let components = vec![
(
"a",
ComponentDeclBuilder::new()
.child(ChildBuilder::new().name("b").environment("env"))
.environment(EnvironmentBuilder::new().name("env").runner(RunnerRegistration {
source_name: "elf".parse().unwrap(),
source: RegistrationSource::Self_,
target_name: "hobbit".parse().unwrap(),
}))
.runner_default("elf")
.build(),
),
(
"b",
ComponentDeclBuilder::new()
.child(ChildBuilder::new().name("c").environment("env"))
.environment(EnvironmentBuilder::new().name("env"))
.build(),
),
("c", ComponentDeclBuilder::new_empty_component().add_program("hobbit").build()),
];
// Set up the system.
let (runner_service, mut receiver) =
create_service_directory_entry::<fcrunner::ComponentRunnerMarker>();
let universe = RoutingTestBuilder::new("a", components)
// Component "a" exposes a runner service.
.add_outgoing_path(
"a",
format!("/svc/{}", fcrunner::ComponentRunnerMarker::DEBUG_NAME).parse().unwrap(),
runner_service,
)
.build()
.await;
join!(
// Bind "c". We expect to see a call to our runner service for the new component.
async move {
universe.start_instance(&vec!["b", "c"].try_into().unwrap()).await.unwrap();
},
// Wait for a request, and ensure it has the correct URL.
async move {
assert_eq!(
wait_for_runner_request(&mut receiver).await.resolved_url,
Some("test:///c_resolved".to_string())
);
}
);
}
/// a
/// \
/// b
///
/// a: declares runner "runner" with service format!("/svc/{}", fcrunner::ComponentRunnerMarker::DEBUG_NAME) from "self".
/// a: registers runner "runner" from self in environment as "hobbit".
/// b: uses runner "runner". Fails due to a FIDL error, conveyed through a Stop after the
/// bind succeeds.
#[fuchsia::test]
async fn use_runner_from_environment_failed() {
let components = vec![
(
"a",
ComponentDeclBuilder::new()
.child(ChildBuilder::new().name("b").environment("env"))
.environment(EnvironmentBuilder::new().name("env").runner(RunnerRegistration {
source_name: "runner".parse().unwrap(),
source: RegistrationSource::Self_,
target_name: "runner".parse().unwrap(),
}))
.runner_default("runner")
// For Stopped event
.use_(UseBuilder::event_stream().name("stopped").path("/events/stopped"))
.build(),
),
("b", ComponentDeclBuilder::new_empty_component().add_program("runner").build()),
];
let runner_service = service::endpoint(|_scope, _channel| {});
// Set a capability provider for the runner that closes the server end.
// `ComponentRunner.Start` to fail.
let test = RoutingTestBuilder::new("a", components)
.set_builtin_capabilities(vec![CapabilityDecl::EventStream(EventStreamDecl {
name: "stopped".parse().unwrap(),
})])
.add_outgoing_path(
"a",
format!("/svc/{}", fcrunner::ComponentRunnerMarker::DEBUG_NAME).parse().unwrap(),
runner_service,
)
.build()
.await;
let namespace_root = test.bind_and_get_namespace(Moniker::root()).await;
let event_stream =
capability_util::connect_to_svc_in_namespace::<fcomponent::EventStreamMarker>(
&namespace_root,
&"/events/stopped".parse().unwrap(),
)
.await;
// Even though we expect the runner to fail, bind should succeed. This is because the failure
// is propagated via the controller channel, separately from the Start action.
test.start_instance(&vec!["b"].try_into().unwrap()).await.unwrap();
// Since the controller should have closed, expect a Stopped event.
let event = event_stream.get_next().await.unwrap().into_iter().next().unwrap();
assert_matches!(&event,
fcomponent::Event {
header: Some(fcomponent::EventHeader {
moniker: Some(moniker),
..
}),
payload:
Some(fcomponent::EventPayload::Stopped(
fcomponent::StoppedPayload {
status: Some(status),
..
}
))
,
..
}
if *moniker == "b".to_string()
&& *status == zx::Status::PEER_CLOSED.into_raw() as i32
);
}
/// a
/// \
/// b
///
/// a: declares runner "elf" with service format!("/svc/{}", fcrunner::ComponentRunnerMarker::DEBUG_NAME) from "self".
/// a: registers runner "elf" from self in environment as "hobbit".
/// b: uses runner "hobbit". Fails because "hobbit" was not in environment.
#[fuchsia::test]
async fn use_runner_from_environment_not_found() {
let components = vec![
(
"a",
ComponentDeclBuilder::new()
.child(ChildBuilder::new().name("b").environment("env"))
.environment(EnvironmentBuilder::new().name("env").runner(RunnerRegistration {
source_name: "elf".parse().unwrap(),
source: RegistrationSource::Self_,
target_name: "dwarf".parse().unwrap(),
}))
.runner_default("elf")
.build(),
),
("b", ComponentDeclBuilder::new_empty_component().add_program("hobbit").build()),
];
// Set up the system.
let (runner_service, _receiver) =
create_service_directory_entry::<fcrunner::ComponentRunnerMarker>();
let universe = RoutingTestBuilder::new("a", components)
// Component "a" exposes a runner service.
.add_outgoing_path(
"a",
format!("/svc/{}", fcrunner::ComponentRunnerMarker::DEBUG_NAME).parse().unwrap(),
runner_service,
)
.build()
.await;
// Bind "b". We expect it to fail because routing failed.
let err = universe.start_instance(&vec!["b"].try_into().unwrap()).await.unwrap_err();
let err = match err {
ModelError::ActionError {
err:
ActionError::StartError {
err: StartActionError::ResolveRunnerError { err, moniker, .. },
},
} if moniker == vec!["b"].try_into().unwrap() => err,
err => panic!("Unexpected error trying to start b: {}", err),
};
assert_matches!(
*err,
BedrockError::RoutingError(err)
if matches!(
err.downcast_for_test::<RoutingError>(),
RoutingError::UseFromEnvironmentNotFound {
moniker,
capability_type,
capability_name,
}
if moniker == &Moniker::try_from(vec!["b"]).unwrap() &&
capability_type == &"runner" &&
capability_name == &"hobbit"
)
);
}
// TODO: Write a test for environment that extends from None. Currently, this is not
// straightforward because resolver routing is not implemented yet, which makes it impossible to
// register a new resolver and have it be usable.
/// a
/// \
/// [b]
/// \
/// c
///
/// a: offers service /svc/foo from self
/// [b]: offers service /svc/foo to c
/// [b]: is destroyed
/// c: uses service /svc/foo, which should fail
#[fuchsia::test]
async fn use_with_destroyed_parent() {
let use_decl = UseBuilder::protocol().name("foo").path("/svc/hippo").build();
let components = vec![
(
"a",
ComponentDeclBuilder::new()
.protocol_default("foo")
.use_(
UseBuilder::protocol()
.source(UseSource::Framework)
.name("fuchsia.component.Realm"),
)
.offer(
OfferBuilder::protocol()
.name("foo")
.source(OfferSource::Self_)
.target(OfferTarget::Collection("coll".parse().unwrap())),
)
.collection_default("coll")
.build(),
),
(
"b",
ComponentDeclBuilder::new()
.offer(
OfferBuilder::protocol()
.name("foo")
.source(OfferSource::Parent)
.target(OfferTarget::static_child("c".to_string())),
)
.child_default("c")
.build(),
),
("c", ComponentDeclBuilder::new().use_(use_decl.clone()).build()),
];
let test = RoutingTest::new("a", components).await;
test.create_dynamic_child(
&Moniker::root(),
"coll",
ChildDecl {
name: "b".to_string(),
url: "test:///b".to_string(),
startup: fdecl::StartupMode::Lazy,
environment: None,
on_terminate: None,
config_overrides: None,
},
)
.await;
// Confirm we can use service from "c".
test.check_use(
vec!["coll:b", "c"].try_into().unwrap(),
CheckUse::Protocol { path: default_service_capability(), expected_res: ExpectedResult::Ok },
)
.await;
// Destroy "b", but preserve a reference to "c" so we can route from it below.
let moniker = vec!["coll:b", "c"].try_into().unwrap();
let realm_c = test
.model
.root()
.find_and_maybe_resolve(&moniker)
.await
.expect("failed to look up realm b");
test.destroy_dynamic_child(Moniker::root(), "coll", "b").await;
// Now attempt to route the service from "c". Should fail because "b" does not exist so we
// cannot follow it.
let UseDecl::Protocol(use_decl) = use_decl else {
unreachable!();
};
let err = RouteRequest::UseProtocol(use_decl)
.route(&realm_c)
.await
.expect_err("routing unexpectedly succeeded");
assert_matches!(
err,
RoutingError::ComponentInstanceError(
ComponentInstanceError::InstanceNotFound { moniker }
) if moniker == vec!["coll:b"].try_into().unwrap()
);
}
/// a
/// / \
/// b c
///
/// b: exposes directory /data/foo from self as /data/bar
/// a: offers directory /data/bar from b as /data/baz to c, which was destroyed (but not removed
/// from the tree yet)
/// c: uses /data/baz as /data/hippo
#[fuchsia::test]
async fn use_from_destroyed_but_not_removed() {
let components = vec![
(
"a",
ComponentDeclBuilder::new()
.offer(
OfferBuilder::protocol()
.name("bar")
.target_name("baz")
.source(OfferSource::static_child("b".to_string()))
.target(OfferTarget::static_child("c".to_string())),
)
.child_default("b")
.child_default("c")
.build(),
),
(
"b",
ComponentDeclBuilder::new()
.capability(CapabilityBuilder::directory().name("foo_data").path("/data/foo"))
.protocol_default("foo")
.expose(
ExposeBuilder::protocol()
.name("foo")
.target_name("bar")
.source(ExposeSource::Self_),
)
.build(),
),
(
"c",
ComponentDeclBuilder::new()
.use_(UseBuilder::protocol().name("baz").path("/svc/hippo"))
.build(),
),
];
let test = RoutingTest::new("a", components).await;
let component_b = test
.model
.root()
.find_and_maybe_resolve(&vec!["b"].try_into().unwrap())
.await
.expect("failed to look up realm b");
// Destroy `b` but keep alive its reference from the parent.
// TODO: If we had a "pre-destroy" event we could delete the child through normal means and
// block on the event instead of explicitly registering actions.
ActionSet::register(component_b.clone(), ShutdownAction::new(ShutdownType::Instance))
.await
.expect("shutdown failed");
ActionSet::register(component_b, DestroyAction::new()).await.expect("destroy failed");
test.check_use(
vec!["c"].try_into().unwrap(),
CheckUse::Protocol {
path: default_service_capability(),
expected_res: ExpectedResult::Err(zx::Status::NOT_FOUND),
},
)
.await;
}
/// a
/// / \
/// b c
///
/// a: creates environment "env" and registers resolver "base" from c.
/// b: resolved by resolver "base" through "env".
/// b: exposes resolver "base" from self.
#[fuchsia::test]
async fn use_resolver_from_parent_environment() {
// Note that we do not define a component "b". This will be resolved by our custom resolver.
let components = vec![
(
"a",
ComponentDeclBuilder::new_empty_component()
.child(ChildBuilder::new().name("b").url("base://b").environment("env"))
.child(ChildBuilder::new().name("c"))
.environment(EnvironmentBuilder::new().name("env").resolver(ResolverRegistration {
resolver: "base".parse().unwrap(),
source: RegistrationSource::Child("c".to_string()),
scheme: "base".parse().unwrap(),
}))
.build(),
),
(
"c",
ComponentDeclBuilder::new()
.expose(ExposeBuilder::resolver().name("base").source(ExposeSource::Self_))
.resolver_default("base")
.build(),
),
];
// Set up the system.
let (resolver_service, mut receiver) =
create_service_directory_entry::<fresolution::ResolverMarker>();
let universe = RoutingTestBuilder::new("a", components)
// Component "c" exposes a resolver service.
.add_outgoing_path(
"c",
"/svc/fuchsia.component.resolution.Resolver".parse().unwrap(),
resolver_service,
)
.build()
.await;
join!(
// Bind "b". We expect to see a call to our resolver service for the new component.
async move {
universe
.start_instance(&vec!["b"].try_into().unwrap())
.await
.expect("failed to start instance b");
},
// Wait for a request, and resolve it.
async {
while let Some(request) = receiver.next().await {
match request {
fresolution::ResolverRequest::Resolve { component_url, responder } => {
assert_eq!(component_url, "base://b");
responder
.send(Ok(fresolution::Component {
url: Some("test://b".into()),
decl: Some(fmem::Data::Bytes(
fidl::persist(&default_component_decl().native_into_fidl())
.unwrap(),
)),
package: None,
// this test only resolves one component_url
resolution_context: None,
abi_revision: Some(
version_history::HISTORY
.get_example_supported_version_for_tests()
.abi_revision
.into(),
),
..Default::default()
}))
.expect("failed to send resolve response");
}
fresolution::ResolverRequest::ResolveWithContext {
component_url,
context,
responder,
} => {
warn!(
"ResolveWithContext({}, {:?}) request is unexpected in this test",
component_url, context
);
responder
.send(Err(fresolution::ResolverError::Internal))
.expect("failed to send resolve response");
}
}
}
}
);
}
/// a
/// \
/// b
/// \
/// c
/// a: creates environment "env" and registers resolver "base" from self.
/// b: has environment "env".
/// c: is resolved by resolver from grandarent.
#[fuchsia::test]
async fn use_resolver_from_grandparent_environment() {
// Note that we do not define a component "c". This will be resolved by our custom resolver.
let components = vec![
(
"a",
ComponentDeclBuilder::new()
.child(ChildBuilder::new().name("b").environment("env"))
.environment(EnvironmentBuilder::new().name("env").resolver(ResolverRegistration {
resolver: "base".parse().unwrap(),
source: RegistrationSource::Self_,
scheme: "base".parse().unwrap(),
}))
.resolver_default("base")
.build(),
),
(
"b",
ComponentDeclBuilder::new_empty_component()
.child(ChildBuilder::new().name("c").url("base://c"))
.build(),
),
];
// Set up the system.
let (resolver_service, mut receiver) =
create_service_directory_entry::<fresolution::ResolverMarker>();
let universe = RoutingTestBuilder::new("a", components)
// Component "c" exposes a resolver service.
.add_outgoing_path(
"a",
"/svc/fuchsia.component.resolution.Resolver".parse().unwrap(),
resolver_service,
)
.build()
.await;
join!(
// Bind "c". We expect to see a call to our resolver service for the new component.
async move {
universe
.start_instance(&vec!["b", "c"].try_into().unwrap())
.await
.expect("failed to start instance c");
},
// Wait for a request, and resolve it.
async {
while let Some(request) = receiver.next().await {
match request {
fresolution::ResolverRequest::Resolve { component_url, responder } => {
assert_eq!(component_url, "base://c");
responder
.send(Ok(fresolution::Component {
url: Some("test://c".into()),
decl: Some(fmem::Data::Bytes(
fidl::persist(&default_component_decl().native_into_fidl())
.unwrap(),
)),
package: None,
// this test only resolves one component_url
resolution_context: None,
abi_revision: Some(
version_history::HISTORY
.get_example_supported_version_for_tests()
.abi_revision
.into(),
),
..Default::default()
}))
.expect("failed to send resolve response");
}
fresolution::ResolverRequest::ResolveWithContext {
component_url,
context,
responder,
} => {
warn!(
"ResolveWithContext({}, {:?}) request is unexpected in this test",
component_url, context
);
responder
.send(Err(fresolution::ResolverError::Internal))
.expect("failed to send resolve response");
}
}
}
}
);
}
/// a
/// / \
/// b c
/// a: creates environment "env" and registers resolver "base" from self.
/// b: has environment "env".
/// c: does NOT have environment "env".
#[fuchsia::test]
async fn resolver_is_not_available() {
// Note that we do not define a component "b" or "c". This will be resolved by our custom resolver.
let components = vec![(
"a",
ComponentDeclBuilder::new()
.child(ChildBuilder::new().name("b").url("base://b").environment("env"))
.child(ChildBuilder::new().name("c").url("base://c"))
.environment(EnvironmentBuilder::new().name("env").resolver(ResolverRegistration {
resolver: "base".parse().unwrap(),
source: RegistrationSource::Self_,
scheme: "base".parse().unwrap(),
}))
.resolver_default("base")
.build(),
)];
// Set up the system.
let (resolver_service, mut receiver) =
create_service_directory_entry::<fresolution::ResolverMarker>();
let universe = RoutingTestBuilder::new("a", components)
// Component "c" exposes a resolver service.
.add_outgoing_path(
"a",
"/svc/fuchsia.component.resolution.Resolver".parse().unwrap(),
resolver_service,
)
.build()
.await;
join!(
// Bind "c". We expect to see a failure that the scheme is not registered.
async move {
match universe.start_instance(&vec!["c"].try_into().unwrap()).await {
Err(ModelError::ActionError {
err:
ActionError::ResolveError {
err:
ResolveActionError::ResolverError {
url,
err: ResolverError::SchemeNotRegistered,
},
},
}) => {
assert_eq!(url, "base://c");
}
_ => {
panic!("expected ResolverError");
}
};
},
// Wait for a request, and resolve it.
async {
while let Some(request) = receiver.next().await {
match request {
fresolution::ResolverRequest::Resolve { component_url, responder } => {
assert_eq!(component_url, "base://b");
responder
.send(Ok(fresolution::Component {
url: Some("test://b".into()),
decl: Some(fmem::Data::Bytes(
fidl::persist(&default_component_decl().native_into_fidl())
.unwrap(),
)),
package: None,
// this test only resolves one component_url
resolution_context: None,
abi_revision: Some(
version_history::HISTORY
.get_example_supported_version_for_tests()
.abi_revision
.into(),
),
..Default::default()
}))
.expect("failed to send resolve response");
}
fresolution::ResolverRequest::ResolveWithContext {
component_url,
context,
responder,
} => {
warn!(
"ResolveWithContext({}, {:?}) request is unexpected in this test",
component_url, context
);
responder
.send(Err(fresolution::ResolverError::Internal))
.expect("failed to send resolve response");
}
}
}
}
);
}
/// a
/// /
/// b
/// a: creates environment "env" and registers resolver "base" from self.
/// b: has environment "env".
#[fuchsia::test]
async fn resolver_component_decl_is_validated() {
// Note that we do not define a component "b". This will be resolved by our custom resolver.
let components = vec![(
"a",
ComponentDeclBuilder::new()
.child(ChildBuilder::new().name("b").url("base://b").environment("env"))
.environment(EnvironmentBuilder::new().name("env").resolver(ResolverRegistration {
resolver: "base".parse().unwrap(),
source: RegistrationSource::Self_,
scheme: "base".parse().unwrap(),
}))
.resolver_default("base")
.build(),
)];
// Set up the system.
let (resolver_service, mut receiver) =
create_service_directory_entry::<fresolution::ResolverMarker>();
let universe = RoutingTestBuilder::new("a", components)
// Component "a" exposes a resolver service.
.add_outgoing_path(
"a",
"/svc/fuchsia.component.resolution.Resolver".parse().unwrap(),
resolver_service,
)
.build()
.await;
join!(
// Bind "b". We expect to see a ResolverError.
async move {
match universe.start_instance(&vec!["b"].try_into().unwrap()).await {
Err(ModelError::ActionError {
err:
ActionError::ResolveError {
err:
ResolveActionError::ResolverError {
url,
err: ResolverError::ManifestInvalid(_),
},
},
}) => {
assert_eq!(url, "base://b");
}
_ => {
panic!("expected ResolverError");
}
};
},
// Wait for a request, and resolve it.
async {
while let Some(request) = receiver.next().await {
match request {
fresolution::ResolverRequest::Resolve { component_url, responder } => {
assert_eq!(component_url, "base://b");
responder
.send(Ok(fresolution::Component {
url: Some("test://b".into()),
decl: Some(fmem::Data::Bytes({
let fidl = fdecl::Component {
exposes: Some(vec![fdecl::Expose::Protocol(
fdecl::ExposeProtocol {
source: Some(fdecl::Ref::Self_(fdecl::SelfRef {})),
..Default::default()
},
)]),
..Default::default()
};
fidl::persist(&fidl).unwrap()
})),
package: None,
// this test only resolves one component_url
resolution_context: None,
abi_revision: Some(
version_history::HISTORY
.get_example_supported_version_for_tests()
.abi_revision
.into(),
),
..Default::default()
}))
.expect("failed to send resolve response");
}
fresolution::ResolverRequest::ResolveWithContext {
component_url,
context,
responder,
} => {
warn!(
"ResolveWithContext({}, {:?}) request is unexpected in this test",
component_url, context
);
responder
.send(Err(fresolution::ResolverError::Internal))
.expect("failed to send resolve response");
}
}
}
}
);
}
enum RouteType {
Offer,
Expose,
}
async fn verify_service_route(
test: &RoutingTest,
use_decl: UseDecl,
target_moniker: &str,
agg_moniker: &str,
child_monikers: &[&str],
route_type: RouteType,
) {
let UseDecl::Service(use_decl) = use_decl else {
unreachable!();
};
let target_moniker: Moniker = target_moniker.try_into().unwrap();
let agg_moniker: Moniker = agg_moniker.try_into().unwrap();
let child_monikers: Vec<_> =
child_monikers.into_iter().map(|m| ChildName::parse(m).unwrap()).collect();
// Test routing directly.
let root = test.model.root();
let target_component = root.find_and_maybe_resolve(&target_moniker).await.unwrap();
let agg_component = root.find_and_maybe_resolve(&agg_moniker).await.unwrap();
let source = RouteRequest::UseService(use_decl).route(&target_component).await.unwrap();
match source {
RouteSource {
source: CapabilitySource::AnonymizedAggregate { members, capability, component, .. },
relative_path: _,
} => {
let collections: Vec<_> = members
.into_iter()
.map(|m| match m {
AggregateMember::Collection(c) => c.to_string(),
_ => panic!("not expected"),
})
.collect();
let unique_colls: HashSet<_> =
child_monikers.iter().map(|c| c.collection().unwrap().to_string()).collect();
let mut unique_colls: Vec<_> = unique_colls.into_iter().collect();
unique_colls.sort();
assert_eq!(collections, unique_colls);
assert_matches!(capability, AggregateCapability::Service(_));
assert_eq!(*capability.source_name(), "foo");
assert!(Arc::ptr_eq(&component.upgrade().unwrap(), &agg_component));
}
_ => panic!("wrong capability source"),
};
// Populate the collection(s) with dynamic children.
for child_moniker in &child_monikers {
let coll = child_moniker.collection().unwrap();
let name = child_moniker.name();
test.create_dynamic_child(&agg_moniker, coll.as_str(), ChildBuilder::new().name(name))
.await;
test.start_instance_and_wait_start(&agg_moniker.child(child_moniker.clone()))
.await
.unwrap();
}
// Use the aggregated service from `target_moniker`.
test.check_use(
target_moniker,
CheckUse::Service {
path: "/svc/foo".parse().unwrap(),
instance: ServiceInstance::Aggregated(child_monikers.len()),
member: "echo".to_string(),
expected_res: ExpectedResult::Ok,
},
)
.await;
match route_type {
RouteType::Offer => {}
RouteType::Expose => {
test.check_use_exposed_dir(
agg_moniker,
CheckUse::Service {
path: "/foo".parse().unwrap(),
instance: ServiceInstance::Aggregated(child_monikers.len()),
member: "echo".to_string(),
expected_res: ExpectedResult::Ok,
},
)
.await;
}
}
}
/// a
/// / \
/// b coll
///
/// root: offer service `foo` from `coll` to b
/// b: route `use service`
#[fuchsia::test]
async fn offer_service_from_collection() {
let use_decl = UseBuilder::service().name("foo").build();
let mut components = vec![
(
"a",
ComponentDeclBuilder::new()
.use_realm()
.offer(
OfferBuilder::service()
.name("foo")
.source(OfferSource::Collection("coll".parse().unwrap()))
.target(OfferTarget::static_child("b".into())),
)
.collection_default("coll")
.child_default("b")
.build(),
),
("b", ComponentDeclBuilder::new().use_(use_decl.clone()).build()),
];
components.extend(["c1", "c2", "c3"].into_iter().map(|ch| {
(
ch,
ComponentDeclBuilder::new()
.expose(ExposeBuilder::service().name("foo").source(ExposeSource::Self_))
.capability(CapabilityBuilder::service().name("foo").path("/svc/foo.service"))
.build(),
)
}));
let test = RoutingTestBuilder::new("a", components).build().await;
verify_service_route(
&test,
use_decl,
"/b",
"/",
&["coll:c1", "coll:c2", "coll:c3"],
RouteType::Offer,
)
.await;
}
/// a
/// / \
/// b (coll1, coll2, coll3)
///
/// root: offer service `foo` from collections to b
/// b: route `use service`
#[fuchsia::test]
async fn offer_service_from_collections() {
let use_decl = UseBuilder::service().name("foo").build();
let mut offers: Vec<_> = ["coll1", "coll2", "coll3"]
.into_iter()
.map(|coll| {
OfferBuilder::service()
.name("foo")
.source(OfferSource::Collection(coll.parse().unwrap()))
.target(OfferTarget::static_child("b".into()))
.build()
})
.collect();
let mut components = vec![
(
"a",
ComponentDeclBuilder::new()
.use_realm()
.offer(offers.remove(0))
.offer(offers.remove(0))
.offer(offers.remove(0))
.collection_default("coll1")
.collection_default("coll2")
.collection_default("coll3")
.child_default("b")
.build(),
),
("b", ComponentDeclBuilder::new().use_(use_decl.clone()).build()),
];
components.extend(["c1", "c2", "c3"].into_iter().map(|ch| {
(
ch,
ComponentDeclBuilder::new()
.expose(ExposeBuilder::service().name("foo").source(ExposeSource::Self_))
.capability(CapabilityBuilder::service().name("foo").path("/svc/foo.service"))
.build(),
)
}));
let test = RoutingTestBuilder::new("a", components).build().await;
verify_service_route(
&test,
use_decl,
"/b",
"/",
&["coll1:c1", "coll2:c2", "coll3:c3"],
RouteType::Offer,
)
.await;
}
/// a
/// / \
/// m (coll1, coll2, coll3)
/// /
/// b
///
/// root: offer service `foo` from coll to b
/// b: route `use service`
#[fuchsia::test]
async fn offer_service_from_collections_multilevel() {
let use_decl = UseBuilder::service().name("foo").build();
let mut offers: Vec<_> = ["coll1", "coll2", "coll3"]
.into_iter()
.map(|coll| {
OfferBuilder::service()
.name("foo")
.source(OfferSource::Collection(coll.parse().unwrap()))
.target(OfferTarget::static_child("m".into()))
.build()
})
.collect();
let mut components = vec![
(
"a",
ComponentDeclBuilder::new()
.use_realm()
.offer(offers.remove(0))
.offer(offers.remove(0))
.offer(offers.remove(0))
.collection_default("coll1")
.collection_default("coll2")
.collection_default("coll3")
.child_default("m")
.build(),
),
(
"m",
ComponentDeclBuilder::new()
.offer(
OfferBuilder::service()
.name("foo")
.source(OfferSource::Parent)
.target(OfferTarget::static_child("b".into())),
)
.child_default("b")
.build(),
),
("b", ComponentDeclBuilder::new().use_(use_decl.clone()).build()),
];
components.extend(["c1", "c2", "c3"].into_iter().map(|ch| {
(
ch,
ComponentDeclBuilder::new()
.expose(ExposeBuilder::service().name("foo").source(ExposeSource::Self_))
.capability(CapabilityBuilder::service().name("foo").path("/svc/foo.service"))
.build(),
)
}));
let test = RoutingTestBuilder::new("a", components).build().await;
verify_service_route(
&test,
use_decl,
"/m/b",
"/",
&["coll1:c1", "coll2:c2", "coll3:c3"],
RouteType::Offer,
)
.await;
}
/// a
/// \
/// b
/// \
/// coll
///
/// b: expose service `foo` from `coll` to b
/// a: route `use service`
#[fuchsia::test]
async fn expose_service_from_collection() {
let use_decl = UseBuilder::service().name("foo").source(UseSource::Child("b".into())).build();
let mut components = vec![
("a", ComponentDeclBuilder::new().use_(use_decl.clone()).child_default("b").build()),
(
"b",
ComponentDeclBuilder::new()
.use_realm()
.expose(
ExposeBuilder::service()
.name("foo")
.source(ExposeSource::Collection("coll".parse().unwrap())),
)
.collection_default("coll")
.build(),
),
];
components.extend(["c1", "c2", "c3"].into_iter().map(|ch| {
(
ch,
ComponentDeclBuilder::new()
.expose(ExposeBuilder::service().name("foo").source(ExposeSource::Self_))
.capability(CapabilityBuilder::service().name("foo").path("/svc/foo.service"))
.build(),
)
}));
let test = RoutingTestBuilder::new("a", components).build().await;
verify_service_route(
&test,
use_decl,
"/",
"/b",
&["coll:c1", "coll:c2", "coll:c3"],
RouteType::Expose,
)
.await;
}
/// a
/// \
/// b
/// \
/// (coll1, coll2, coll3)
///
/// b: expose service `foo` from collections to b
/// a: route `use service`
#[fuchsia::test]
async fn expose_service_from_collections() {
let use_decl = UseBuilder::service().name("foo").source(UseSource::Child("b".into())).build();
let mut exposes: Vec<_> = ["coll1", "coll2", "coll3"]
.into_iter()
.map(|coll| {
ExposeBuilder::service()
.name("foo")
.source(ExposeSource::Collection(coll.parse().unwrap()))
.build()
})
.collect();
let mut components = vec![
("a", ComponentDeclBuilder::new().use_(use_decl.clone()).child_default("b").build()),
(
"b",
ComponentDeclBuilder::new()
.use_realm()
.expose(exposes.remove(0))
.expose(exposes.remove(0))
.expose(exposes.remove(0))
.collection_default("coll1")
.collection_default("coll2")
.collection_default("coll3")
.build(),
),
];
components.extend(["c1", "c2", "c3"].into_iter().map(|ch| {
(
ch,
ComponentDeclBuilder::new()
.expose(ExposeBuilder::service().name("foo").source(ExposeSource::Self_))
.capability(CapabilityBuilder::service().name("foo").path("/svc/foo.service"))
.build(),
)
}));
let test = RoutingTestBuilder::new("a", components).build().await;
verify_service_route(
&test,
use_decl,
"/",
"/b",
&["coll1:c1", "coll2:c2", "coll3:c3"],
RouteType::Expose,
)
.await;
}
/// a
/// \
/// m
/// \
/// b
/// \
/// (coll1, coll2, coll3)
///
/// b: expose service `foo` from collections to b
/// a: route `use service`
#[fuchsia::test]
async fn expose_service_from_collections_multilevel() {
let use_decl = UseBuilder::service().name("foo").source(UseSource::Child("m".into())).build();
let mut exposes: Vec<_> = ["coll1", "coll2", "coll3"]
.into_iter()
.map(|coll| {
ExposeBuilder::service()
.name("foo")
.source(ExposeSource::Collection(coll.parse().unwrap()))
})
.collect();
let mut components = vec![
("a", ComponentDeclBuilder::new().use_(use_decl.clone()).child_default("m").build()),
(
"m",
ComponentDeclBuilder::new()
.expose(
ExposeBuilder::service().name("foo").source(ExposeSource::Child("b".into())),
)
.child_default("b")
.build(),
),
(
"b",
ComponentDeclBuilder::new()
.use_realm()
.expose(exposes.remove(0))
.expose(exposes.remove(0))
.expose(exposes.remove(0))
.collection_default("coll1")
.collection_default("coll2")
.collection_default("coll3")
.build(),
),
];
components.extend(["c1", "c2", "c3"].into_iter().map(|ch| {
(
ch,
ComponentDeclBuilder::new()
.expose(ExposeBuilder::service().name("foo").source(ExposeSource::Self_))
.capability(CapabilityBuilder::service().name("foo").path("/svc/foo.service"))
.build(),
)
}));
let test = RoutingTestBuilder::new("a", components).build().await;
verify_service_route(
&test,
use_decl,
"/",
"/m/b",
&["coll1:c1", "coll2:c2", "coll3:c3"],
RouteType::Expose,
)
.await;
}
/// root
/// / \
/// client (coll1: [service_child_a, non_service_child], coll2: [service_child_b])
///
/// root: offer service `foo` from `(coll1,coll2)` to `client`
/// client: use service
#[fuchsia::test]
async fn list_service_instances_from_collections() {
let use_decl = UseBuilder::service().name("foo").build();
let mut offers: Vec<_> = ["coll1", "coll2"]
.into_iter()
.map(|coll| {
OfferBuilder::service()
.name("foo")
.source(OfferSource::Collection(coll.parse().unwrap()))
.target(OfferTarget::static_child("client".into()))
.build()
})
.collect();
let components = vec![
(
"root",
ComponentDeclBuilder::new()
.use_realm()
.offer(offers.remove(0))
.offer(offers.remove(0))
.collection_default("coll1")
.collection_default("coll2")
.child_default("client")
.build(),
),
("client", ComponentDeclBuilder::new().use_(use_decl.clone()).build()),
(
"service_child_a",
ComponentDeclBuilder::new()
.expose(ExposeBuilder::service().name("foo").source(ExposeSource::Self_))
.capability(CapabilityBuilder::service().name("foo").path("/svc/foo.service"))
.build(),
),
(
"service_child_b",
ComponentDeclBuilder::new()
.expose(ExposeBuilder::service().name("foo").source(ExposeSource::Self_))
.capability(CapabilityBuilder::service().name("foo").path("/svc/foo.service"))
.build(),
),
("non_service_child", ComponentDeclBuilder::new().build()),
];
let test = RoutingTestBuilder::new("root", components).build().await;
// Start a few dynamic children in the collections.
test.create_dynamic_child(
&Moniker::root(),
"coll1",
ChildBuilder::new().name("service_child_a"),
)
.await;
test.create_dynamic_child(
&Moniker::root(),
"coll1",
ChildBuilder::new().name("non_service_child"),
)
.await;
test.create_dynamic_child(
&Moniker::root(),
"coll2",
ChildBuilder::new().name("service_child_b"),
)
.await;
let client_component = test
.model
.root()
.find_and_maybe_resolve(&vec!["client"].try_into().unwrap())
.await
.expect("client instance");
let UseDecl::Service(use_decl) = use_decl else { unreachable!() };
let source = RouteRequest::UseService(use_decl)
.route(&client_component)
.await
.expect("failed to route service");
let aggregate_capability_provider = match source {
RouteSource {
source: CapabilitySource::AnonymizedAggregate { aggregate_capability_provider, .. },
relative_path: _,
} => aggregate_capability_provider,
_ => panic!("bad capability source"),
};
// Check that only the instances that expose the service are listed.
let instances: HashSet<ChildName> = aggregate_capability_provider
.list_instances()
.await
.unwrap()
.into_iter()
.map(|m| match m {
AggregateInstance::Child(c) => c.clone(),
_ => panic!("not expected"),
})
.collect();
assert_eq!(instances.len(), 2);
assert!(instances.contains(&"coll1:service_child_a".try_into().unwrap()));
assert!(instances.contains(&"coll2:service_child_b".try_into().unwrap()));
// Try routing to one of the instances.
let source = aggregate_capability_provider
.route_instance(&AggregateInstance::Child("coll1:service_child_a".try_into().unwrap()))
.await
.expect("failed to route to child");
match source {
CapabilitySource::Component {
capability: ComponentCapability::Service(ServiceDecl { name, source_path }),
component,
} => {
assert_eq!(name, "foo");
assert_eq!(
source_path.expect("source path"),
"/svc/foo.service".parse::<cm_types::Path>().unwrap()
);
assert_eq!(component.moniker, vec!["coll1:service_child_a"].try_into().unwrap());
}
_ => panic!("bad child capability source"),
}
}
/// a
/// / \
/// b c
/// \
/// coll: [foo, bar, baz]
///
/// a: offer service from c to b
/// b: use service
/// c: expose service from collection `coll`
/// foo, bar: expose service to parent
/// baz: does not expose service
#[fuchsia::test]
async fn use_service_from_sibling_collection() {
let components = vec![
(
"a",
ComponentDeclBuilder::new()
.offer(
OfferBuilder::service()
.name("my.service.Service")
.source(OfferSource::static_child("c".to_string()))
.target(OfferTarget::static_child("b".to_string())),
)
.child(ChildBuilder::new().name("b"))
.child(ChildBuilder::new().name("c"))
.build(),
),
(
"b",
ComponentDeclBuilder::new()
.use_(UseBuilder::service().name("my.service.Service"))
.build(),
),
(
"c",
ComponentDeclBuilder::new()
.use_(
UseBuilder::protocol()
.source(UseSource::Framework)
.name("fuchsia.component.Realm"),
)
.expose(
ExposeBuilder::service()
.name("my.service.Service")
.source(ExposeSource::Collection("coll".parse().unwrap())),
)
.collection_default("coll")
.build(),
),
(
"foo",
ComponentDeclBuilder::new()
.expose(
ExposeBuilder::service().name("my.service.Service").source(ExposeSource::Self_),
)
.service_default("my.service.Service")
.build(),
),
(
"bar",
ComponentDeclBuilder::new()
.expose(
ExposeBuilder::service().name("my.service.Service").source(ExposeSource::Self_),
)
.service_default("my.service.Service")
.build(),
),
("baz", ComponentDeclBuilder::new().build()),
];
let (directory_entry, mut receiver) = create_service_directory_entry::<echo::EchoMarker>();
let instance_dir = pseudo_directory! {
"echo" => directory_entry,
};
let test = RoutingTestBuilder::new("a", components)
.add_outgoing_path(
"foo",
"/svc/my.service.Service/default".parse().unwrap(),
instance_dir.clone(),
)
.add_outgoing_path("bar", "/svc/my.service.Service/default".parse().unwrap(), instance_dir)
.build()
.await;
// Populate the collection with dynamic children.
test.create_dynamic_child(
&vec!["c"].try_into().unwrap(),
"coll",
ChildBuilder::new().name("foo"),
)
.await;
test.start_instance_and_wait_start(&vec!["c", "coll:foo"].try_into().unwrap())
.await
.expect("failed to start `foo`");
test.create_dynamic_child(
&vec!["c"].try_into().unwrap(),
"coll",
ChildBuilder::new().name("bar"),
)
.await;
test.start_instance_and_wait_start(&vec!["c", "coll:bar"].try_into().unwrap())
.await
.expect("failed to start `bar`");
test.create_dynamic_child(
&vec!["c"].try_into().unwrap(),
"coll",
ChildBuilder::new().name("baz"),
)
.await;
join!(
async move {
test.check_use(
vec!["b"].try_into().unwrap(),
CheckUse::Service {
path: "/svc/my.service.Service".parse().unwrap(),
instance: ServiceInstance::Aggregated(2),
member: "echo".to_string(),
expected_res: ExpectedResult::Ok,
},
)
.await;
},
async move {
while let Some(echo::EchoRequest::EchoString { value, responder }) =
receiver.next().await
{
responder.send(value.as_ref().map(|v| v.as_str())).expect("failed to send reply")
}
}
);
}
/// a
/// / | \
/// b c d
///
/// a: offer service from c to b with filter parameters set on offer
/// b: expose service
/// c: use service (with filter)
/// d: use service (with instance renamed)
#[fuchsia::test]
async fn use_filtered_service_from_sibling() {
let components = vec![
(
"a",
ComponentDeclBuilder::new()
.offer(
OfferBuilder::service()
.name("my.service.Service")
.source(OfferSource::static_child("b".to_string()))
.target(OfferTarget::static_child("c".to_string()))
.source_instance_filter(vec!["variantinstance".to_string()]),
)
.offer(
OfferBuilder::service()
.name("my.service.Service")
.source(OfferSource::static_child("b".to_string()))
.target(OfferTarget::static_child("d".to_string()))
.renamed_instances(vec![NameMapping {
source_name: "default".to_string(),
target_name: "renamed_default".to_string(),
}]),
)
.child(ChildBuilder::new().name("b"))
.child(ChildBuilder::new().name("c"))
.child(ChildBuilder::new().name("d"))
.build(),
),
(
"b",
ComponentDeclBuilder::new()
.expose(
ExposeBuilder::service().name("my.service.Service").source(ExposeSource::Self_),
)
.service_default("my.service.Service")
.build(),
),
(
"c",
ComponentDeclBuilder::new()
.use_(UseBuilder::service().name("my.service.Service"))
.build(),
),
(
"d",
ComponentDeclBuilder::new()
.use_(UseBuilder::service().name("my.service.Service"))
.build(),
),
];
let (directory_entry, mut receiver) = create_service_directory_entry::<echo::EchoMarker>();
let instance_dir = pseudo_directory! {
"echo" => directory_entry,
};
let test = RoutingTestBuilder::new("a", components)
.add_outgoing_path(
"b",
"/svc/my.service.Service/default".parse().unwrap(),
instance_dir.clone(),
)
.add_outgoing_path(
"b",
"/svc/my.service.Service/variantinstance".parse().unwrap(),
instance_dir,
)
.build()
.await;
// Check that instance c only has access to the filtered service instance.
let namespace_c = test.bind_and_get_namespace(vec!["c"].try_into().unwrap()).await;
let dir_c =
capability_util::take_dir_from_namespace(&namespace_c, &"/svc".parse().unwrap()).await;
let service_dir_c = fuchsia_fs::directory::open_directory(
&dir_c,
"my.service.Service",
fuchsia_fs::OpenFlags::empty(),
)
.await
.expect("failed to open service");
let entries: HashSet<String> = fuchsia_fs::directory::readdir(&service_dir_c)
.await
.expect("failed to read entries")
.into_iter()
.map(|d| d.name)
.collect();
assert_eq!(entries.len(), 1);
assert!(entries.contains("variantinstance"));
capability_util::add_dir_to_namespace(&namespace_c, &"/svc".parse().unwrap(), dir_c).await;
// Check that instance d connects to the renamed instances correctly
let namespace_d = test.bind_and_get_namespace(vec!["d"].try_into().unwrap()).await;
let dir_d =
capability_util::take_dir_from_namespace(&namespace_d, &"/svc".parse().unwrap()).await;
let service_dir_d = fuchsia_fs::directory::open_directory(
&dir_d,
"my.service.Service",
fuchsia_fs::OpenFlags::empty(),
)
.await
.expect("failed to open service");
let entries: HashSet<String> = fuchsia_fs::directory::readdir(&service_dir_d)
.await
.expect("failed to read entries")
.into_iter()
.map(|d| d.name)
.collect();
assert_eq!(entries.len(), 1);
assert!(entries.contains("renamed_default"));
capability_util::add_dir_to_namespace(&namespace_d, &"/svc".parse().unwrap(), dir_d).await;
let _server_handle = fasync::Task::spawn(async move {
while let Some(echo::EchoRequest::EchoString { value, responder }) = receiver.next().await {
responder.send(value.as_ref().map(|v| v.as_str())).expect("failed to send reply");
}
});
test.check_use(
vec!["c"].try_into().unwrap(),
CheckUse::Service {
path: "/svc/my.service.Service".parse().unwrap(),
instance: ServiceInstance::Named("variantinstance".to_string()),
member: "echo".to_string(),
expected_res: ExpectedResult::Ok,
},
)
.await;
test.check_use(
vec!["d"].try_into().unwrap(),
CheckUse::Service {
path: "/svc/my.service.Service".parse().unwrap(),
instance: ServiceInstance::Named("renamed_default".to_string()),
member: "echo".to_string(),
expected_res: ExpectedResult::Ok,
},
)
.await;
}
#[fuchsia::test]
async fn use_filtered_aggregate_service_from_sibling() {
let components = vec![
(
"a",
ComponentDeclBuilder::new()
.offer(
OfferBuilder::service()
.name("my.service.Service")
.source(OfferSource::static_child("b".to_string()))
.target(OfferTarget::static_child("c".to_string()))
.source_instance_filter(vec!["variantinstance".to_string()]),
)
.offer(
OfferBuilder::service()
.name("my.service.Service")
.source(OfferSource::static_child("b".to_string()))
.target(OfferTarget::static_child("c".to_string()))
.source_instance_filter(vec!["renamed_default".to_string()])
.renamed_instances(vec![NameMapping {
source_name: "default".to_string(),
target_name: "renamed_default".to_string(),
}]),
)
.child(ChildBuilder::new().name("b"))
.child(ChildBuilder::new().name("c"))
.build(),
),
(
"b",
ComponentDeclBuilder::new()
.expose(
ExposeBuilder::service().name("my.service.Service").source(ExposeSource::Self_),
)
.service_default("my.service.Service")
.build(),
),
(
"c",
ComponentDeclBuilder::new()
.use_(UseBuilder::service().name("my.service.Service"))
.build(),
),
];
let (directory_entry, mut receiver) = create_service_directory_entry::<echo::EchoMarker>();
let instance_dir = pseudo_directory! {
"echo" => directory_entry,
};
let test = RoutingTestBuilder::new("a", components)
.add_outgoing_path(
"b",
"/svc/my.service.Service/default".parse().unwrap(),
instance_dir.clone(),
)
.add_outgoing_path(
"b",
"/svc/my.service.Service/variantinstance".parse().unwrap(),
instance_dir,
)
.build()
.await;
// Check that instance c only has access to the filtered service instance.
let namespace_c = test.bind_and_get_namespace(vec!["c"].try_into().unwrap()).await;
let dir_c =
capability_util::take_dir_from_namespace(&namespace_c, &"/svc".parse().unwrap()).await;
let service_dir_c = fuchsia_fs::directory::open_directory(
&dir_c,
"my.service.Service",
fuchsia_fs::OpenFlags::empty(),
)
.await
.expect("failed to open service");
let entries: HashSet<String> = fuchsia_fs::directory::readdir(&service_dir_c)
.await
.expect("failed to read entries")
.into_iter()
.map(|d| d.name)
.collect();
assert_eq!(entries.len(), 2);
assert!(entries.contains("variantinstance"));
assert!(entries.contains("renamed_default"));
capability_util::add_dir_to_namespace(&namespace_c, &"/svc".parse().unwrap(), dir_c).await;
let _server_handle = fasync::Task::spawn(async move {
while let Some(echo::EchoRequest::EchoString { value, responder }) = receiver.next().await {
responder.send(value.as_ref().map(|v| v.as_str())).expect("failed to send reply");
}
});
test.check_use(
vec!["c"].try_into().unwrap(),
CheckUse::Service {
path: "/svc/my.service.Service".parse().unwrap(),
instance: ServiceInstance::Named("variantinstance".to_string()),
member: "echo".to_string(),
expected_res: ExpectedResult::Ok,
},
)
.await;
test.check_use(
vec!["c"].try_into().unwrap(),
CheckUse::Service {
path: "/svc/my.service.Service".parse().unwrap(),
instance: ServiceInstance::Named("renamed_default".to_string()),
member: "echo".to_string(),
expected_res: ExpectedResult::Ok,
},
)
.await;
}
#[fuchsia::test]
async fn use_anonymized_aggregate_service() {
let expose_service_decl = ComponentDeclBuilder::new()
.expose(ExposeBuilder::service().name("my.service.Service").source(ExposeSource::Self_))
.service_default("my.service.Service")
.build();
let components = vec![
(
"a",
ComponentDeclBuilder::new()
.offer(
OfferBuilder::service()
.name("my.service.Service")
.source(OfferSource::Self_)
.target(OfferTarget::static_child("b".to_string())),
)
.service_default("my.service.Service")
.child(ChildBuilder::new().name("b"))
.build(),
),
(
"b",
ComponentDeclBuilder::new()
.offer(
OfferBuilder::service()
.name("my.service.Service")
.source(OfferSource::static_child("c".to_string()))
.target(OfferTarget::static_child("e".to_string())),
)
.offer(
OfferBuilder::service()
.name("my.service.Service")
.source(OfferSource::static_child("d".to_string()))
.target(OfferTarget::static_child("e".to_string())),
)
.offer(
OfferBuilder::service()
.name("my.service.Service")
.source(OfferSource::Parent)
.target(OfferTarget::static_child("e".to_string())),
)
.offer(
OfferBuilder::service()
.name("my.service.Service")
.source(OfferSource::Self_)
.target(OfferTarget::static_child("e".to_string())),
)
.service_default("my.service.Service")
.child(ChildBuilder::new().name("c"))
.child(ChildBuilder::new().name("d"))
.child(ChildBuilder::new().name("e"))
.build(),
),
("c", expose_service_decl.clone()),
("d", expose_service_decl),
(
"e",
ComponentDeclBuilder::new()
.use_(UseBuilder::service().name("my.service.Service"))
.build(),
),
];
let (directory_entry, mut receiver) = create_service_directory_entry::<echo::EchoMarker>();
let instance_dir = pseudo_directory! {
"echo" => directory_entry,
};
let test = RoutingTestBuilder::new("a", components)
.add_outgoing_path(
"a",
"/svc/my.service.Service/default".parse().unwrap(),
instance_dir.clone(),
)
.add_outgoing_path(
"b",
"/svc/my.service.Service/default".parse().unwrap(),
instance_dir.clone(),
)
.add_outgoing_path(
"c",
"/svc/my.service.Service/default".parse().unwrap(),
instance_dir.clone(),
)
.add_outgoing_path(
"d",
"/svc/my.service.Service/variantinstance".parse().unwrap(),
instance_dir,
)
.build()
.await;
let _server_handle = fasync::Task::spawn(async move {
while let Some(echo::EchoRequest::EchoString { value, responder }) = receiver.next().await {
responder.send(value.as_ref().map(|v| v.as_str())).unwrap();
}
});
test.check_use(
"b/e".parse().unwrap(),
CheckUse::Service {
path: "/svc/my.service.Service".parse().unwrap(),
instance: ServiceInstance::Aggregated(4),
member: "echo".to_string(),
expected_res: ExpectedResult::Ok,
},
)
.await;
}
/// While the provider component is stopping (waiting on stop timeout), a routing request should
/// still be handled.
#[fuchsia::test(allow_stalls = false)]
async fn source_component_stopping_when_routing() {
// Use mock time in this test.
let initial = fasync::Time::from_nanos(0);
TestExecutor::advance_to(initial).await;
let components = vec![(
"root",
ComponentDeclBuilder::new()
.capability(CapabilityBuilder::protocol().name("foo").path("/svc/foo").build())
.expose(ExposeBuilder::protocol().name("foo").source(ExposeSource::Self_))
.build(),
)];
let test_topology = ActionsTest::new(components[0].0, components, None).await;
let (open_request_tx, mut open_request_rx) = mpsc::unbounded();
let mut root_out_dir = OutDir::new();
root_out_dir.add_entry(
"/svc/foo".parse().unwrap(),
vfs::service::endpoint(move |_scope, channel| {
open_request_tx.unbounded_send(channel).unwrap();
}),
);
test_topology.runner.add_host_fn("test:///root_resolved", root_out_dir.host_fn());
// Configure the component runner to take 3 seconds to stop the component.
let response_delay = zx::Duration::from_seconds(3);
test_topology.runner.add_controller_response(
"test:///root_resolved",
Box::new(move || ControllerActionResponse {
close_channel: true,
delay: Some(response_delay),
}),
);
let root = test_topology.look_up(Moniker::default()).await;
assert!(!root.is_started().await);
// Start the component.
let root = root
.start_instance(&Moniker::root(), &StartReason::Root)
.await
.expect("failed to start root");
test_topology.runner.wait_for_urls(&["test:///root_resolved"]).await;
assert!(root.is_started().await);
// Start to stop the component. This will stall because the framework will be
// waiting the controller to respond.
let mut stop_fut = pin!(ActionSet::register(root.clone(), StopAction::new(false)));
assert_matches!(TestExecutor::poll_until_stalled(&mut stop_fut).await, Poll::Pending);
// Start to request a capability from the component.
let (client_end, server_end) = zx::Channel::create();
let output = root.lock_resolved_state().await.unwrap().component_output_dict.clone();
let route_and_open_fut = async {
// Route the capability.
let entry = output
.get_capability(&RelativePath::new("foo").unwrap())
.unwrap()
.route(crate::model::routing::router::Request {
availability: Availability::Required,
target: root.as_weak().into(),
})
.await
.unwrap()
.try_into_directory_entry()
.unwrap();
// Connect to the capability.
Open::new(entry).open(ExecutionScope::new(), fio::OpenFlags::empty(), ".", server_end);
};
// Both should complete after the response delay has passed.
let ((), stop_result, ()) = futures::join!(
route_and_open_fut,
stop_fut,
TestExecutor::advance_to(initial + response_delay)
);
assert_matches!(stop_result, Ok(()));
// The request should hit the outgoing directory of the component.
let server_end = open_request_rx.next().await.unwrap();
assert_eq!(
client_end.basic_info().unwrap().related_koid,
server_end.basic_info().unwrap().koid
);
assert!(root.is_started().await);
}
/// If the provider component of a capability is stopped before the open request is
/// sent, it should be started again.
#[fuchsia::test]
async fn source_component_stopped_after_routing_before_open() {
let components = vec![(
"root",
ComponentDeclBuilder::new()
.capability(CapabilityBuilder::protocol().name("foo").path("/svc/foo").build())
.expose(ExposeBuilder::protocol().name("foo").source(ExposeSource::Self_))
.build(),
)];
let test_topology = ActionsTest::new(components[0].0, components, None).await;
let (open_request_tx, mut open_request_rx) = mpsc::unbounded();
let mut root_out_dir = OutDir::new();
root_out_dir.add_entry(
"/svc/foo".parse().unwrap(),
vfs::service::endpoint(move |_scope, channel| {
open_request_tx.unbounded_send(channel).unwrap();
}),
);
test_topology.runner.add_host_fn("test:///root_resolved", root_out_dir.host_fn());
let root = test_topology.look_up(Moniker::default()).await;
assert!(!root.is_started().await);
// Request a capability from the component.
let output = root.lock_resolved_state().await.unwrap().component_output_dict.clone();
let open: Open = Open::new(
output
.get_capability(&RelativePath::new("foo").unwrap())
.unwrap()
.route(crate::model::routing::router::Request {
availability: Availability::Required,
target: root.as_weak().into(),
})
.await
.unwrap()
.try_into_directory_entry()
.unwrap(),
);
// It should be started with the capability access start reason.
assert!(root.is_started().await);
assert_matches!(
root.lock_state().await.get_started_state().unwrap().start_reason,
StartReason::AccessCapability { .. }
);
// Stop the component.
root.stop().await.expect("failed to stop");
assert!(!root.is_started().await);
// Connect to the capability. The component should be started again.
let (client_end, server_end) = zx::Channel::create();
open.open(ExecutionScope::new(), fio::OpenFlags::empty(), ".", server_end);
let server_end = open_request_rx.next().await.unwrap();
assert_eq!(
client_end.basic_info().unwrap().related_koid,
server_end.basic_info().unwrap().koid
);
assert!(root.is_started().await);
assert_matches!(
root.lock_state().await.get_started_state().unwrap().start_reason,
StartReason::OutgoingDirectory
);
}
/// If the provider component of a capability is shutdown before the open request is
/// sent, it should not be started again, and the open request should also be closed.
#[fuchsia::test]
async fn source_component_shutdown_after_routing_before_open() {
let components = vec![(
"root",
ComponentDeclBuilder::new()
.capability(CapabilityBuilder::protocol().name("foo").path("/svc/foo").build())
.expose(ExposeBuilder::protocol().name("foo").source(ExposeSource::Self_))
.build(),
)];
let test_topology = ActionsTest::new(components[0].0, components, None).await;
let mut root_out_dir = OutDir::new();
root_out_dir.add_entry(
"/svc/foo".parse().unwrap(),
vfs::service::endpoint(move |_scope, _channel| {
unreachable!();
}),
);
test_topology.runner.add_host_fn("test:///root_resolved", root_out_dir.host_fn());
let root = test_topology.look_up(Moniker::default()).await;
assert!(!root.is_started().await);
// Request a capability from the component.
let output = root.lock_resolved_state().await.unwrap().component_output_dict.clone();
let open: Open = Open::new(
output
.get_capability(&RelativePath::new("foo").unwrap())
.unwrap()
.route(crate::model::routing::router::Request {
availability: Availability::Required,
target: root.as_weak().into(),
})
.await
.unwrap()
.try_into_directory_entry()
.unwrap(),
);
// It should be started with the capability access start reason.
assert!(root.is_started().await);
assert_matches!(
root.lock_state().await.get_started_state().unwrap().start_reason,
StartReason::AccessCapability { .. }
);
// Shutdown the component.
root.shutdown(ShutdownType::Instance).await.expect("failed to stop");
assert!(!root.is_started().await);
// Connect to the capability. The request will fail and the component is not started.
let (client_end, server_end) = zx::Channel::create();
open.open(ExecutionScope::new(), fio::OpenFlags::empty(), ".", server_end);
fasync::OnSignals::new(&client_end, zx::Signals::CHANNEL_PEER_CLOSED).await.unwrap();
assert!(!root.is_started().await);
}
/// A hook on the Resolved event that finishes when receives a value on an mpsc channel.
pub struct BlockingResolvedHook {
receiver: Mutex<mpsc::UnboundedReceiver<Moniker>>,
}
impl BlockingResolvedHook {
fn new() -> (Arc<Self>, mpsc::UnboundedSender<Moniker>) {
let (sender, receiver) = mpsc::unbounded();
(Arc::new(Self { receiver: Mutex::new(receiver) }), sender)
}
}
impl BlockingResolvedHook {
pub fn hooks(self: &Arc<Self>) -> Vec<HooksRegistration> {
vec![HooksRegistration::new(
"BlockingResolvedHook",
vec![EventType::Resolved],
Arc::downgrade(self) as Weak<dyn Hook>,
)]
}
}
#[async_trait]
impl Hook for BlockingResolvedHook {
async fn on(self: Arc<Self>, event: &Event) -> Result<(), ModelError> {
match &event.payload {
EventPayload::Resolved { component, .. } => {
let expected_moniker = self.receiver.lock().await.next().await.unwrap();
assert_eq!(component.moniker, expected_moniker);
}
_ => (),
};
Ok(())
}
}
/// Builds the following realm:
///
/// root
/// / \
/// provider consumer
///
/// root: offers `foo_svc` from provider to consumer, and capability_requested to provider
/// provider: exposes `foo_svc` and capability_requested
/// consumer: uses `foo_svc`
async fn build_realm_for_capability_requested_tests(delivery: DeliveryType) -> RoutingTest {
let components = vec![
(
"root",
ComponentDeclBuilder::new()
.offer(
OfferBuilder::protocol()
.name("foo_svc")
.target_name("foo_svc")
.source(OfferSource::static_child("provider".into()))
.target(OfferTarget::static_child("consumer".to_string()))
.build(),
)
.offer(
OfferBuilder::event_stream()
.name("capability_requested")
.target_name("capability_requested")
.scope(vec![EventScope::Child(ChildRef {
name: "consumer".into(),
collection: None,
})])
.source(OfferSource::Parent)
.target(OfferTarget::static_child("provider".to_string())),
)
.child(ChildBuilder::new().name("provider"))
.child(ChildBuilder::new().name("consumer"))
.build(),
),
(
"provider",
ComponentDeclBuilder::new()
.capability(
CapabilityBuilder::protocol()
.name("foo_svc")
.path("/svc/foo")
.delivery(delivery)
.build(),
)
.use_(
UseBuilder::event_stream()
.source(UseSource::Parent)
.name("capability_requested")
.path("/events/capability_requested")
.filter(btreemap! {
"name".to_string() => DictionaryValue::Str("foo_svc".to_string())
}),
)
// The protocol is exposed from self but in reality would be provided via
// CapabilityRequested, similar to how archivist exposes `fuchsia.logger.LogSink`.
.expose(
ExposeBuilder::protocol()
.name("foo_svc")
.source(ExposeSource::Self_)
.target_name("foo_svc")
.target(ExposeTarget::Parent),
)
.build(),
),
(
"consumer",
ComponentDeclBuilder::new()
.use_(
UseBuilder::protocol()
.source(UseSource::Parent)
.name("foo_svc")
.path("/svc/hippo"),
)
.build(),
),
];
RoutingTestBuilder::new("root", components)
.set_builtin_capabilities(vec![CapabilityDecl::EventStream(EventStreamDecl {
name: "capability_requested".parse().unwrap(),
})])
.build()
.await
}
/// This is a regression test for the flake in https://fxbug.dev/320698181.
///
/// Tests that a protocol capability is "routed" through a capability_requested event
/// rather than the outgoing directory of a component when resolution is slow.
///
/// Capability requested routing needs to wait for the provider to finish resolving
/// before sending the event. This ensures that the static event stream is registered
/// and handles the CapabilityRequested event. If this happens late, for example if the
/// resolver is slow, routing incorrectly falls through to the outgoing dir.
#[fuchsia::test]
fn slow_resolve_races_with_capability_requested() {
// Building the `RoutingTest` may stall so we run it outside of `run_until_stalled`.
let mut executor = fasync::TestExecutor::new();
let test = executor
.run_singlethreaded(build_realm_for_capability_requested_tests(DeliveryType::Immediate));
let mut test_body = Box::pin(async move {
// Add a hook that delays component resolution.
let (blocking_resolved_hook, resolved_hook_sender) = BlockingResolvedHook::new();
test.model.root().hooks.install_front(blocking_resolved_hook.hooks()).await;
// Resolve root.
resolved_hook_sender.unbounded_send(Moniker::root()).unwrap();
// Resolve consumer and get its namespace.
resolved_hook_sender.unbounded_send("consumer".parse().unwrap()).unwrap();
let namespace_consumer = test.bind_and_get_namespace("consumer".parse().unwrap()).await;
let root = test.model.root().clone();
let concurrent_resolve_task = fasync::Task::spawn(async move {
let provider = root.find(&"provider".parse().unwrap()).await.unwrap();
provider.resolve().await.unwrap();
});
// Route and connect to the capability before the provider is resolved.
let _echo_proxy = capability_util::connect_to_svc_in_namespace::<echo::EchoMarker>(
&namespace_consumer,
&"/svc/hippo".parse().unwrap(),
)
.await;
_ = TestExecutor::poll_until_stalled(std::future::pending::<()>()).await;
// Resolve provider and get its namespace.
resolved_hook_sender.unbounded_send("provider".parse().unwrap()).unwrap();
let namespace_provider =
test.bind_and_get_namespace(vec!["provider"].try_into().unwrap()).await;
concurrent_resolve_task.await;
// The provider component should receive a CapabilityRequested event for the
// `foo_svc` protocol. If it doesn't, routing will go to the outgoing directory and fail.
let event_stream = capability_util::connect_to_svc_in_namespace::<
fcomponent::EventStreamMarker,
>(
&namespace_provider, &"/events/capability_requested".parse().unwrap()
)
.await;
let event = event_stream.get_next().await.unwrap().into_iter().next().unwrap();
assert_matches!(&event,
fcomponent::Event {
header: Some(fcomponent::EventHeader {
moniker: Some(moniker), .. }), ..
} if *moniker == ".".to_string() );
assert_matches!(&event,
fcomponent::Event {
header: Some(fcomponent::EventHeader {
component_url: Some(component_url), .. }), ..
} if *component_url == "test:///consumer".to_string() );
assert_matches!(&event,
fcomponent::Event {
payload:
Some(fcomponent::EventPayload::CapabilityRequested(
fcomponent::CapabilityRequestedPayload { name: Some(name), .. })), ..
} if *name == "foo_svc".to_string()
);
});
executor.run_until_stalled(&mut test_body).unwrap();
}
// `b` exposes `echo` with [`DeliveryType::OnReadable`]. `a` uses this capability.
async fn build_realm_for_on_readable_tests() -> RoutingTest {
let components = vec![
(
"a",
ComponentDeclBuilder::new()
.use_(UseBuilder::protocol().name("echo").source(UseSource::Child("b".into())))
.child(ChildBuilder::new().name("b"))
.build(),
),
(
"b",
ComponentDeclBuilder::new()
.capability(
CapabilityBuilder::protocol().name("echo").delivery(DeliveryType::OnReadable),
)
.expose(ExposeBuilder::protocol().name("echo").source(ExposeSource::Self_))
.build(),
),
];
RoutingTestBuilder::new("a", components).build().await
}
#[fuchsia::test]
fn protocol_delivery_on_readable() {
// Building the `RoutingTest` may stall so we run it outside of `run_until_stalled`.
let mut executor = fasync::TestExecutor::new();
let test = executor.run_singlethreaded(build_realm_for_on_readable_tests());
let mut test_body = Box::pin(async move {
let (_, component_name) =
test.start_and_get_instance(&Moniker::root(), StartReason::Eager, true).await.unwrap();
let component_resolved_url = RoutingTest::resolved_url(&component_name);
let namespace = test.mock_runner.get_namespace(&component_resolved_url).unwrap();
// `b` is stopped.
let b = test.model.root().find_and_maybe_resolve(&"b".parse().unwrap()).await.unwrap();
_ = TestExecutor::poll_until_stalled(std::future::pending::<()>()).await;
assert!(!b.is_started().await);
// Open the capability.
let echo_proxy = capability_util::connect_to_svc_in_namespace::<echo::EchoMarker>(
&namespace,
&"/svc/echo".parse().unwrap(),
)
.await;
// `b` is still stopped.
_ = TestExecutor::poll_until_stalled(std::future::pending::<()>()).await;
assert!(!b.is_started().await);
// Make a request. `b` should only then start.
capability_util::call_echo_and_validate_result(echo_proxy, ExpectedResult::Ok).await;
_ = TestExecutor::poll_until_stalled(std::future::pending::<()>()).await;
assert!(b.is_started().await);
});
executor.run_until_stalled(&mut test_body).unwrap();
}
#[fuchsia::test]
fn protocol_delivery_on_readable_bail_when_unresolve() {
// Building the `RoutingTest` may stall so we run it outside of `run_until_stalled`.
let mut executor = fasync::TestExecutor::new();
let test = executor.run_singlethreaded(build_realm_for_on_readable_tests());
let mut test_body = Box::pin(async move {
let (_, component_name) =
test.start_and_get_instance(&Moniker::root(), StartReason::Eager, true).await.unwrap();
let component_resolved_url = RoutingTest::resolved_url(&component_name);
let namespace = test.mock_runner.get_namespace(&component_resolved_url).unwrap();
// `b` is stopped.
let b = test.model.root().find_and_maybe_resolve(&"b".parse().unwrap()).await.unwrap();
_ = TestExecutor::poll_until_stalled(std::future::pending::<()>()).await;
assert!(!b.is_started().await);
// Open the capability.
let echo_proxy = capability_util::connect_to_svc_in_namespace::<echo::EchoMarker>(
&namespace,
&"/svc/echo".parse().unwrap(),
)
.await;
// `b` is still stopped.
_ = TestExecutor::poll_until_stalled(std::future::pending::<()>()).await;
assert!(!b.is_started().await);
// Unresolve b. This should drop the request from `a`.
b.unresolve().await.unwrap();
_ = TestExecutor::poll_until_stalled(std::future::pending::<()>()).await;
// Make a request which should fail.
echo_proxy.echo_string(Some("hippos")).await.expect_err("FIDL call should fail");
_ = TestExecutor::poll_until_stalled(std::future::pending::<()>()).await;
assert!(!b.is_started().await);
});
executor.run_until_stalled(&mut test_body).unwrap();
}
/// If a component provides a capability by monitoring "capability_requested" events,
/// and the capability has declared `DeliveryType::OnReadable`, then the event should
/// be sent to the component only when the client writes to the server endpoint.
#[fuchsia::test]
fn capability_requested_protocol_on_delivery_readable() {
// Building the `RoutingTest` may stall so we run it outside of `run_until_stalled`.
let mut executor = fasync::TestExecutor::new();
let test = executor
.run_singlethreaded(build_realm_for_capability_requested_tests(DeliveryType::OnReadable));
let mut test_body = Box::pin(async move {
test.model.root().resolve().await.unwrap();
let provider = test.model.root().find(&"provider".parse().unwrap()).await.unwrap();
let namespace_consumer = test.bind_and_get_namespace("consumer".parse().unwrap()).await;
let echo_proxy = capability_util::connect_to_svc_in_namespace::<echo::EchoMarker>(
&namespace_consumer,
&"/svc/hippo".parse().unwrap(),
)
.await;
// Provider should remain stopped.
_ = TestExecutor::poll_until_stalled(std::future::pending::<()>()).await;
assert!(!provider.is_started().await);
// Make a request by writing to the channel. Provider should only then start.
let mut echo_call = pin!(echo_proxy.echo_string(Some("hippos")));
TestExecutor::poll_until_stalled(&mut echo_call)
.await
.expect_pending("Request should be buffered in the channel in the event stream");
_ = TestExecutor::poll_until_stalled(std::future::pending::<()>()).await;
assert!(provider.is_started().await);
// After starting, the provider should observe the request in its event stream.
let event_stream = {
let resolved_url = RoutingTest::resolved_url("provider");
let namespace = test.mock_runner.get_namespace(&resolved_url).unwrap();
capability_util::connect_to_svc_in_namespace::<fcomponent::EventStreamMarker>(
&namespace,
&"/events/capability_requested".parse().unwrap(),
)
.await
};
let event = event_stream.get_next().await.unwrap().into_iter().next().unwrap();
let fcomponent::Event {
header:
Some(fcomponent::EventHeader {
moniker: Some(moniker),
component_url: Some(component_url),
..
}),
payload:
Some(fcomponent::EventPayload::CapabilityRequested(
fcomponent::CapabilityRequestedPayload {
name: Some(name),
capability: Some(server_end),
..
},
)),
..
} = event
else {
panic!("Unexpected event: {event:?}");
};
assert_eq!(&moniker, ".");
assert_eq!(&component_url, "test:///consumer");
assert_eq!(&name, "foo_svc");
// Reply to the echo call to test connectivity.
let server_end: ServerEnd<echo::EchoMarker> = server_end.into();
TestExecutor::poll_until_stalled(Box::pin(OutDir::echo_protocol_fn(
server_end.into_stream().unwrap(),
)))
.await
.expect_pending("Server should not finish");
// Receive the reply.
echo_call.await.unwrap();
});
executor.run_until_stalled(&mut test_body).unwrap();
}