| // Copyright 2021 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| use anyhow::{format_err, Context, Error}; |
| use component_events::events::{Destroyed, Discovered, Event, EventStream, Started}; |
| use component_events::matcher::EventMatcher; |
| use component_events::sequence::*; |
| use fidl::endpoints::ServiceMarker; |
| use fuchsia_component::client; |
| use fuchsia_component_test::{Capability, ChildOptions, RealmBuilder, Ref, Route, ScopedInstance}; |
| use futures::channel::mpsc; |
| use futures::{FutureExt, SinkExt, StreamExt}; |
| use moniker::ChildName; |
| use test_case::test_case; |
| use tracing::*; |
| use { |
| fidl_fuchsia_component_decl as fdecl, fidl_fuchsia_examples as fecho, |
| fidl_fuchsia_examples_services as fexamples, fidl_fuchsia_io as fio, |
| fidl_fuchsia_sys2 as fsys2, |
| }; |
| |
| const BRANCHES_COLLECTION: &str = "branches"; |
| const BRANCH_ONECOLL_COMPONENT_URL: &str = "#meta/service-routing-branch-onecoll.cm"; |
| const BRANCH_TWOCOLL_COMPONENT_URL: &str = "#meta/service-routing-branch-twocoll.cm"; |
| const A_ONECOLL_MONIKER: &str = "account_providers:a"; |
| const B_ONECOLL_MONIKER: &str = "account_providers:b"; |
| const A_TWOCOLL_MONIKER: &str = "account_providers_1:a"; |
| const B_TWOCOLL_MONIKER: &str = "account_providers_2:b"; |
| const ECHO_URL: &str = "#meta/multi-instance-echo-provider.cm"; |
| |
| struct TestInput { |
| url: &'static str, |
| provider_a_moniker: &'static str, |
| provider_b_moniker: &'static str, |
| } |
| |
| impl TestInput { |
| fn new(test_type: TestType) -> Self { |
| match test_type { |
| TestType::OneCollection => Self { |
| url: BRANCH_ONECOLL_COMPONENT_URL, |
| provider_a_moniker: A_ONECOLL_MONIKER, |
| provider_b_moniker: B_ONECOLL_MONIKER, |
| }, |
| TestType::TwoCollections => Self { |
| url: BRANCH_TWOCOLL_COMPONENT_URL, |
| provider_a_moniker: A_TWOCOLL_MONIKER, |
| provider_b_moniker: B_TWOCOLL_MONIKER, |
| }, |
| } |
| } |
| } |
| |
| #[derive(Debug, Clone, Copy)] |
| enum TestType { |
| OneCollection, |
| TwoCollections, |
| } |
| |
| #[test_case(TestType::OneCollection)] |
| #[test_case(TestType::TwoCollections)] |
| #[fuchsia::test] |
| async fn list_instances_test(test_type: TestType) { |
| let input = TestInput::new(test_type); |
| let branch = start_branch(&input).await.expect("failed to start branch component"); |
| start_provider(&branch, input.provider_a_moniker).await.expect("failed to start provider a"); |
| start_provider(&branch, input.provider_b_moniker).await.expect("failed to start provider b"); |
| |
| // List the instances in the BankAccount service. |
| let service_dir = fuchsia_fs::directory::open_directory( |
| branch.get_exposed_dir(), |
| fexamples::BankAccountMarker::SERVICE_NAME, |
| fio::OpenFlags::empty(), |
| ) |
| .await |
| .expect("failed to open service dir"); |
| |
| let instances: Vec<String> = fuchsia_fs::directory::readdir(&service_dir) |
| .await |
| .expect("failed to read entries from service dir") |
| .into_iter() |
| .map(|dirent| dirent.name) |
| .collect(); |
| verify_instances(instances, 2); |
| } |
| |
| #[test_case(TestType::OneCollection)] |
| #[test_case(TestType::TwoCollections)] |
| #[fuchsia::test] |
| async fn connect_to_instances_test(test_type: TestType) { |
| let input = TestInput::new(test_type); |
| let branch = start_branch(&input).await.expect("failed to start branch component"); |
| start_provider(&branch, input.provider_a_moniker).await.expect("failed to start provider a"); |
| start_provider(&branch, input.provider_b_moniker).await.expect("failed to start provider b"); |
| |
| // List the instances in the BankAccount service. |
| let service_dir = fuchsia_fs::directory::open_directory( |
| branch.get_exposed_dir(), |
| fexamples::BankAccountMarker::SERVICE_NAME, |
| fio::OpenFlags::empty(), |
| ) |
| .await |
| .expect("failed to open service dir"); |
| let instances = fuchsia_fs::directory::readdir(&service_dir) |
| .await |
| .expect("failed to read entries from service dir") |
| .into_iter() |
| .map(|dirent| dirent.name); |
| |
| // Connect to every instance and ensure the protocols are functional. |
| for instance in instances { |
| let proxy = client::connect_to_service_instance_at_dir::<fexamples::BankAccountMarker>( |
| branch.get_exposed_dir(), |
| &instance, |
| ) |
| .expect("failed to connect to service instance"); |
| let read_only_account = proxy.connect_to_read_only().expect("read_only protocol"); |
| let owner = read_only_account.get_owner().await.expect("failed to get owner"); |
| let initial_balance = read_only_account.get_balance().await.expect("failed to get_balance"); |
| info!("retrieved account for owner '{}' with balance ${}", &owner, &initial_balance); |
| |
| let read_write_account = proxy.connect_to_read_write().expect("read_write protocol"); |
| assert_eq!(read_write_account.get_owner().await.expect("failed to get_owner"), owner); |
| assert_eq!( |
| read_write_account.get_balance().await.expect("failed to get_balance"), |
| initial_balance |
| ); |
| } |
| } |
| |
| #[test_case(TestType::OneCollection)] |
| #[test_case(TestType::TwoCollections)] |
| #[fuchsia::test] |
| async fn create_destroy_instance_test(test_type: TestType) { |
| let input = TestInput::new(test_type); |
| let branch = start_branch(&input).await.expect("failed to start branch component"); |
| start_provider(&branch, input.provider_a_moniker).await.expect("failed to start provider a"); |
| start_provider(&branch, input.provider_b_moniker).await.expect("failed to start provider b"); |
| |
| // List the instances in the BankAccount service. |
| let service_dir = fuchsia_fs::directory::open_directory( |
| branch.get_exposed_dir(), |
| fexamples::BankAccountMarker::SERVICE_NAME, |
| fio::OpenFlags::empty(), |
| ) |
| .await |
| .expect("failed to open service dir"); |
| |
| let instances: Vec<String> = fuchsia_fs::directory::readdir(&service_dir) |
| .await |
| .expect("failed to read entries from service dir") |
| .into_iter() |
| .map(|dirent| dirent.name) |
| .collect(); |
| |
| // The aggregated service directory should contain instances from the provider. |
| verify_instances(instances, 2); |
| |
| // Destroy provider a. |
| destroy_provider(&branch, input.provider_a_moniker) |
| .await |
| .expect("failed to destroy provider a"); |
| |
| let instances: Vec<String> = fuchsia_fs::directory::readdir(&service_dir) |
| .await |
| .expect("failed to read entries from service dir") |
| .into_iter() |
| .map(|dirent| dirent.name) |
| .collect(); |
| |
| // The provider's instances should be removed from the aggregated service directory. |
| verify_instances(instances, 1); |
| } |
| |
| #[fuchsia::test] |
| async fn static_aggregate_offer() { |
| let builder = RealmBuilder::new().await.unwrap(); |
| // Add subrealm to test offer from parent. |
| let parent_echo = builder.add_child("echo", ECHO_URL, ChildOptions::new()).await.unwrap(); |
| let subrealm = builder.add_child_realm("realm", ChildOptions::new().eager()).await.unwrap(); |
| // Initialize subrealm from echo component to test "offer from self" |
| let echo_decl = builder.get_component_decl(&parent_echo).await.unwrap(); |
| subrealm.replace_realm_decl(echo_decl).await.unwrap(); |
| builder |
| .add_route( |
| Route::new() |
| .capability(Capability::service::<fecho::EchoServiceMarker>()) |
| .from(&parent_echo) |
| .to(&subrealm), |
| ) |
| .await |
| .unwrap(); |
| let echo = subrealm.add_child("echo", ECHO_URL, ChildOptions::new()).await.unwrap(); |
| let (handles_sender, mut handles_receiver) = mpsc::unbounded(); |
| let consumer = subrealm |
| .add_local_child( |
| "consumer", |
| move |handles| { |
| let mut handles_sender = handles_sender.clone(); |
| async move { |
| handles_sender.send(handles).await.unwrap(); |
| // Keep the component running so that component_manager keeps the namespace alive |
| futures::pending!(); |
| Ok(()) |
| } |
| .boxed() |
| }, |
| ChildOptions::new().eager(), |
| ) |
| .await |
| .unwrap(); |
| subrealm |
| .add_route( |
| Route::new() |
| .capability(Capability::service::<fecho::EchoServiceMarker>()) |
| .from(&echo) |
| .to(&consumer), |
| ) |
| .await |
| .unwrap(); |
| subrealm |
| .add_route( |
| Route::new() |
| .capability(Capability::service::<fecho::EchoServiceMarker>()) |
| .from(Ref::parent()) |
| .to(&consumer), |
| ) |
| .await |
| .unwrap(); |
| subrealm |
| .add_route( |
| Route::new() |
| .capability(Capability::service::<fecho::EchoServiceMarker>()) |
| .from(Ref::self_()) |
| .to(&consumer), |
| ) |
| .await |
| .unwrap(); |
| let _realm = builder.build().await.unwrap(); |
| |
| let handles = handles_receiver.next().await.unwrap(); |
| let echo_svc = handles.open_service::<fecho::EchoServiceMarker>().unwrap(); |
| let instances = fuchsia_fs::directory::readdir(&echo_svc) |
| .await |
| .unwrap() |
| .into_iter() |
| .map(|dirent| dirent.name); |
| // parent, self, child in the aggregate x 3 instances each |
| assert_eq!(instances.len(), 3 * 3); |
| drop(echo_svc); |
| |
| // Connect to every instance and ensure the protocols are functional. |
| for instance in instances { |
| let proxy = |
| handles.connect_to_service_instance::<fecho::EchoServiceMarker>(&instance).unwrap(); |
| |
| let echo_proxy = proxy.connect_to_regular_echo().unwrap(); |
| let res = echo_proxy.echo_string("hello").await.unwrap(); |
| assert!(res.ends_with("hello")); |
| |
| let echo_proxy = proxy.connect_to_reversed_echo().unwrap(); |
| let res = echo_proxy.echo_string("hello").await.unwrap(); |
| assert!(res.ends_with("olleh")); |
| } |
| } |
| |
| #[fuchsia::test] |
| async fn static_aggregate_expose() { |
| let builder = RealmBuilder::new().await.unwrap(); |
| // Add placeholder echo component so we can get its decl. Then initialize a subrealm with this |
| // decl to test "expose from self". |
| let placeholder_echo = |
| builder.add_child("placeholder_echo", ECHO_URL, ChildOptions::new()).await.unwrap(); |
| let subrealm = builder.add_child_realm("realm", ChildOptions::new()).await.unwrap(); |
| let echo_decl = builder.get_component_decl(&placeholder_echo).await.unwrap(); |
| subrealm.replace_realm_decl(echo_decl).await.unwrap(); |
| let echo = subrealm.add_child("echo", ECHO_URL, ChildOptions::new()).await.unwrap(); |
| subrealm |
| .add_route( |
| Route::new() |
| .capability(Capability::service::<fecho::EchoServiceMarker>()) |
| .from(&echo) |
| .to(Ref::parent()), |
| ) |
| .await |
| .unwrap(); |
| subrealm |
| .add_route( |
| Route::new() |
| .capability(Capability::service::<fecho::EchoServiceMarker>()) |
| .from(Ref::self_()) |
| .to(Ref::parent()), |
| ) |
| .await |
| .unwrap(); |
| builder |
| .add_route( |
| Route::new() |
| .capability(Capability::service::<fecho::EchoServiceMarker>()) |
| .from(&subrealm) |
| .to(Ref::parent()), |
| ) |
| .await |
| .unwrap(); |
| let realm = builder.build().await.unwrap(); |
| |
| let exposed_dir = realm.root.get_exposed_dir(); |
| let echo_svc = fuchsia_fs::directory::open_directory( |
| exposed_dir, |
| fecho::EchoServiceMarker::SERVICE_NAME, |
| fio::OpenFlags::empty(), |
| ) |
| .await |
| .unwrap(); |
| let instances = fuchsia_fs::directory::readdir(&echo_svc) |
| .await |
| .unwrap() |
| .into_iter() |
| .map(|dirent| dirent.name); |
| // self, child in the aggregate x 3 instances each |
| assert_eq!(instances.len(), 3 * 2); |
| drop(echo_svc); |
| |
| // Connect to every instance and ensure the protocols are functional. |
| for instance in instances { |
| let proxy = client::connect_to_service_instance_at_dir::<fecho::EchoServiceMarker>( |
| exposed_dir, |
| &instance, |
| ) |
| .unwrap(); |
| |
| let echo_proxy = proxy.connect_to_regular_echo().unwrap(); |
| let res = echo_proxy.echo_string("hello").await.unwrap(); |
| assert!(res.ends_with("hello")); |
| |
| let echo_proxy = proxy.connect_to_reversed_echo().unwrap(); |
| let res = echo_proxy.echo_string("hello").await.unwrap(); |
| assert!(res.ends_with("olleh")); |
| } |
| } |
| |
| /// Starts a branch child component. |
| async fn start_branch(input: &TestInput) -> Result<ScopedInstance, Error> { |
| let event_stream = EventStream::open_at_path("/events/discovered") |
| .await |
| .context("failed to subscribe to EventSource")?; |
| |
| let branch = ScopedInstance::new(BRANCHES_COLLECTION.to_string(), input.url.to_string()) |
| .await |
| .context("failed to create branch component instance")?; |
| branch.start_with_binder_sync().await?; |
| |
| // Wait for the providers to be discovered (created) to ensure that |
| // subsequent calls to `start_provider` can start them. |
| EventSequence::new() |
| .has_subset( |
| vec![ |
| EventMatcher::ok().r#type(Discovered::TYPE).moniker(format!( |
| "./{}:{}/{}", |
| BRANCHES_COLLECTION, |
| branch.child_name(), |
| input.provider_a_moniker, |
| )), |
| EventMatcher::ok().r#type(Discovered::TYPE).moniker(format!( |
| "./{}:{}/{}", |
| BRANCHES_COLLECTION, |
| branch.child_name(), |
| input.provider_b_moniker, |
| )), |
| ], |
| Ordering::Unordered, |
| ) |
| .expect(event_stream) |
| .await |
| .context("event sequence did not match expected")?; |
| |
| Ok(branch) |
| } |
| |
| /// Starts the provider with the name `child_name` in the branch component. |
| async fn start_provider(branch: &ScopedInstance, child_moniker: &str) -> Result<(), Error> { |
| let lifecycle_controller_proxy = |
| client::connect_to_protocol::<fsys2::LifecycleControllerMarker>() |
| .context("failed to connect to LifecycleController")?; |
| |
| let event_stream = EventStream::open_at_path("/events/started") |
| .await |
| .context("failed to subscribe to EventSource")?; |
| |
| let provider_moniker = |
| format!("./{}:{}/{}", BRANCHES_COLLECTION, branch.child_name(), child_moniker); |
| |
| let (_, binder_server) = fidl::endpoints::create_endpoints(); |
| |
| // Start the provider child. |
| lifecycle_controller_proxy |
| .start_instance(&provider_moniker, binder_server) |
| .await? |
| .map_err(|err| format_err!("failed to start provider component: {:?}", err))?; |
| |
| // Wait for the provider to start. |
| EventSequence::new() |
| .has_subset( |
| vec![EventMatcher::ok().r#type(Started::TYPE).moniker(provider_moniker)], |
| Ordering::Unordered, |
| ) |
| .expect(event_stream) |
| .await |
| .context("event sequence did not match expected")?; |
| |
| Ok(()) |
| } |
| |
| /// Destroys a BankAccount provider component with the name `child_name`. |
| async fn destroy_provider(branch: &ScopedInstance, child_moniker: &str) -> Result<(), Error> { |
| info!(%child_moniker, "destroying BankAccount provider"); |
| |
| let lifecycle_controller_proxy = |
| client::connect_to_protocol::<fsys2::LifecycleControllerMarker>() |
| .context("failed to connect to LifecycleController")?; |
| |
| let event_stream = EventStream::open_at_path("/events/destroyed") |
| .await |
| .context("failed to subscribe to EventSource")?; |
| |
| let provider_moniker = |
| format!("./{}:{}/{}", BRANCHES_COLLECTION, branch.child_name(), child_moniker); |
| let parent_moniker = format!("./{}:{}", BRANCHES_COLLECTION, branch.child_name()); |
| |
| // Destroy the provider child. |
| let child_moniker = ChildName::parse(child_moniker).unwrap(); |
| lifecycle_controller_proxy |
| .destroy_instance( |
| &parent_moniker, |
| &fdecl::ChildRef { |
| name: child_moniker.name().to_string(), |
| collection: child_moniker.collection().map(|c| c.to_string()), |
| }, |
| ) |
| .await? |
| .map_err(|err| format_err!("failed to destroy provider component: {:?}", err))?; |
| |
| // Wait for the provider to be destroyed. |
| EventSequence::new() |
| .has_subset( |
| vec![EventMatcher::ok().r#type(Destroyed::TYPE).moniker(provider_moniker)], |
| Ordering::Unordered, |
| ) |
| .expect(event_stream) |
| .await |
| .context("event sequence did not match expected")?; |
| |
| Ok(()) |
| } |
| |
| fn verify_instances(instances: Vec<String>, expected_len: usize) { |
| assert_eq!(instances.len(), expected_len); |
| assert!(instances.iter().all(|id| id.len() == 32 && id.chars().all(|c| c.is_ascii_hexdigit()))); |
| } |