| // 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. |
| |
| //! The "Action" concept represents an asynchronous activity on a realm that should eventually |
| //! complete. |
| //! |
| //! Actions decouple the "what" of what needs to happen to a component from the "how". Several |
| //! client APIs may induce operations on a realm's state that complete asynchronously. These |
| //! operations could depend on each other in various ways. |
| //! |
| //! A key property of actions is idempotency. If two equal actions are registered on a realm, the |
| //! work for that action is performed only once. This means that two distinct call sites can |
| //! register the same action, and be guaranteed the work is not repeated. |
| //! |
| //! Here are a couple examples: |
| //! - A `Shutdown` FIDL call must shut down every component instance in the tree, in |
| //! dependency order. For this to happen every component must shut down, but not before its |
| //! downstream dependencies have shut down. |
| //! - A `Realm.DestroyChild` FIDL call returns right after a child component is marked deleted. |
| //! However, in order to actually delete the child, a sequence of events must happen: |
| //! * All instances in the realm must be shut down (see above) |
| //! * The component instance's persistent storage must be erased, if any. |
| //! * The component's parent must remove it as a child. |
| //! |
| //! Note the interdependencies here -- destroying a component also requires shutdown, for example. |
| //! |
| //! These processes could be implemented through a chain of futures in the vicinity of the API |
| //! call. However, this doesn't scale well, because it requires distributed state handling and is |
| //! prone to races. Actions solve this problem by allowing client code to just specify the actions |
| //! that need to eventually be fulfilled. The actual business logic to perform the actions can be |
| //! implemented by the realm itself in a coordinated manner. |
| //! |
| //! `DestroyChild()` is an example of how this can work. For simplicity, suppose it's called on a |
| //! component with no children of its own. This might cause a chain of events like the following: |
| //! |
| //! - Before it returns, the `DestroyChild` FIDL handler registers the `DeleteChild` action on the |
| //! containing realm for child being destroyed. |
| //! - This results in a call to `Action::handle` for the realm. In response to |
| //! `DestroyChild`, `Action::handle()` spawns a future that sets a `Destroy` action on the child. |
| //! Note that `Action::handle()` is not async, it always spawns any work that might block |
| //! in a future. |
| //! - `Action::handle()` is called on the child. In response to `Destroy`, it sets a `Shutdown` |
| //! action on itself (the component instance must be stopped before it is destroyed). |
| //! - `Action::handle()` is called on the child again, in response to `Shutdown`. It turns out the |
| //! instance is still running, so the `Shutdown` future tells the instance to stop. When this |
| //! completes, the `Shutdown` action is finished. |
| //! - The future that was spawned for `Destroy` is notified that `Shutdown` completes, so it cleans |
| //! up the instance's resources and finishes the `Destroy` action. |
| //! - When the work for `Destroy` completes, the future spawned for `DestroyChild` deletes the |
| //! child and marks `DestroyChild` finished, which will notify the client that the action is |
| //! complete. |
| |
| mod resolve; |
| mod shutdown; |
| pub mod start; |
| mod stop; |
| |
| use { |
| crate::model::{ |
| error::ModelError, |
| hooks::{Event, EventPayload}, |
| realm::{BindReason, Component, Realm, RealmState}, |
| }, |
| async_trait::async_trait, |
| fuchsia_async as fasync, |
| futures::{ |
| channel::oneshot, |
| future::{join_all, pending, BoxFuture, FutureExt, Shared}, |
| Future, |
| }, |
| moniker::ChildMoniker, |
| std::any::Any, |
| std::collections::HashMap, |
| std::fmt::Debug, |
| std::hash::Hash, |
| std::sync::Arc, |
| }; |
| |
| /// A action on a realm that must eventually be fulfilled. |
| #[async_trait] |
| pub trait Action: Send + Sync + 'static { |
| type Output: Send + Sync + Clone + Debug; |
| async fn handle(&self, realm: &Arc<Realm>) -> Self::Output; |
| fn key(&self) -> ActionKey; |
| } |
| |
| /// Dispatches a `Discovered` event for a component instance. This action should be registered |
| /// when a component instance is created. |
| pub struct DiscoverAction {} |
| |
| impl DiscoverAction { |
| pub fn new() -> Self { |
| Self {} |
| } |
| } |
| |
| #[async_trait] |
| impl Action for DiscoverAction { |
| type Output = Result<(), ModelError>; |
| async fn handle(&self, realm: &Arc<Realm>) -> Self::Output { |
| do_discover(realm).await |
| } |
| fn key(&self) -> ActionKey { |
| ActionKey::Discover |
| } |
| } |
| |
| /// Resolves a component instance's declaration and initializes its state. |
| pub struct ResolveAction {} |
| |
| impl ResolveAction { |
| pub fn new() -> Self { |
| Self {} |
| } |
| } |
| |
| #[async_trait] |
| impl Action for ResolveAction { |
| type Output = Result<Component, ModelError>; |
| async fn handle(&self, realm: &Arc<Realm>) -> Self::Output { |
| resolve::do_resolve(realm).await |
| } |
| fn key(&self) -> ActionKey { |
| ActionKey::Resolve |
| } |
| } |
| |
| /// Starts a component instance. |
| pub struct StartAction { |
| bind_reason: BindReason, |
| } |
| |
| impl StartAction { |
| pub fn new(bind_reason: BindReason) -> Self { |
| Self { bind_reason } |
| } |
| } |
| |
| #[async_trait] |
| impl Action for StartAction { |
| type Output = Result<(), ModelError>; |
| async fn handle(&self, realm: &Arc<Realm>) -> Self::Output { |
| start::do_start(realm, &self.bind_reason).await |
| } |
| fn key(&self) -> ActionKey { |
| ActionKey::Start |
| } |
| } |
| |
| /// Stops a component instance. |
| pub struct StopAction {} |
| |
| impl StopAction { |
| pub fn new() -> Self { |
| Self {} |
| } |
| } |
| |
| #[async_trait] |
| impl Action for StopAction { |
| type Output = Result<(), ModelError>; |
| async fn handle(&self, realm: &Arc<Realm>) -> Self::Output { |
| stop::do_stop(realm).await |
| } |
| fn key(&self) -> ActionKey { |
| ActionKey::Stop |
| } |
| } |
| |
| /// Marks a child of a realm as deleting. |
| pub struct MarkDeletingAction { |
| moniker: ChildMoniker, |
| } |
| |
| impl MarkDeletingAction { |
| pub fn new(moniker: ChildMoniker) -> Self { |
| Self { moniker } |
| } |
| } |
| |
| #[async_trait] |
| impl Action for MarkDeletingAction { |
| type Output = Result<(), ModelError>; |
| async fn handle(&self, realm: &Arc<Realm>) -> Self::Output { |
| do_mark_deleting(realm, self.moniker.clone()).await |
| } |
| fn key(&self) -> ActionKey { |
| ActionKey::MarkDeleting(self.moniker.clone()) |
| } |
| } |
| |
| /// Completely deletes the given child of a realm. |
| pub struct DeleteChildAction { |
| moniker: ChildMoniker, |
| } |
| |
| impl DeleteChildAction { |
| pub fn new(moniker: ChildMoniker) -> Self { |
| Self { moniker } |
| } |
| } |
| |
| #[async_trait] |
| impl Action for DeleteChildAction { |
| type Output = Result<(), ModelError>; |
| async fn handle(&self, realm: &Arc<Realm>) -> Self::Output { |
| do_delete_child(realm, self.moniker.clone()).await |
| } |
| fn key(&self) -> ActionKey { |
| ActionKey::DeleteChild(self.moniker.clone()) |
| } |
| } |
| |
| /// Destroy this component instance, including all instances nested in its realm. |
| pub struct DestroyAction {} |
| |
| impl DestroyAction { |
| pub fn new() -> Self { |
| Self {} |
| } |
| } |
| |
| #[async_trait] |
| impl Action for DestroyAction { |
| type Output = Result<(), ModelError>; |
| async fn handle(&self, realm: &Arc<Realm>) -> Self::Output { |
| do_destroy(realm).await |
| } |
| fn key(&self) -> ActionKey { |
| ActionKey::Destroy |
| } |
| } |
| |
| /// Shuts down all component instances in this realm (stops them and guarantees they will never |
| /// be started again). |
| pub struct ShutdownAction {} |
| |
| impl ShutdownAction { |
| pub fn new() -> Self { |
| Self {} |
| } |
| } |
| |
| #[async_trait] |
| impl Action for ShutdownAction { |
| type Output = Result<(), ModelError>; |
| async fn handle(&self, realm: &Arc<Realm>) -> Self::Output { |
| shutdown::do_shutdown(realm).await |
| } |
| fn key(&self) -> ActionKey { |
| ActionKey::Shutdown |
| } |
| } |
| |
| /// A key that uniquely identifies an action. |
| #[derive(Debug, Clone, PartialEq, Eq, Hash)] |
| pub enum ActionKey { |
| Discover, |
| Resolve, |
| Start, |
| Stop, |
| Shutdown, |
| MarkDeleting(ChildMoniker), |
| DeleteChild(ChildMoniker), |
| Destroy, |
| } |
| |
| /// A set of actions on a realm that must be completed. |
| /// |
| /// Each action is mapped to a future that returns when the action is complete. |
| pub struct ActionSet { |
| rep: HashMap<ActionKey, Box<dyn Any + Send + Sync>>, |
| } |
| |
| type ActionNotifier<Output> = Shared<BoxFuture<'static, Output>>; |
| |
| /// Represents a task that implements an action. |
| pub(crate) struct ActionTask<A> |
| where |
| A: Action, |
| { |
| tx: oneshot::Sender<A::Output>, |
| fut: BoxFuture<'static, A::Output>, |
| } |
| |
| impl<A> ActionTask<A> |
| where |
| A: Action, |
| { |
| fn new(tx: oneshot::Sender<A::Output>, fut: BoxFuture<'static, A::Output>) -> Self { |
| Self { tx, fut } |
| } |
| |
| /// Runs the action in a separate task and signals the `ActionNotifier` when it completes. |
| pub fn spawn(self) { |
| fasync::Task::spawn(async move { |
| let _ = self.tx.send(self.fut.await); |
| }) |
| .detach(); |
| } |
| } |
| |
| impl ActionSet { |
| pub fn new() -> Self { |
| ActionSet { rep: HashMap::new() } |
| } |
| |
| pub fn contains(&self, key: &ActionKey) -> bool { |
| self.rep.contains_key(key) |
| } |
| |
| /// Registers an action in the set, returning when the action is finished (which may represent |
| /// a task that's already running for this action). |
| pub async fn register<A>(realm: Arc<Realm>, action: A) -> A::Output |
| where |
| A: Action, |
| { |
| let rx = { |
| let mut actions = realm.lock_actions().await; |
| actions.register_no_wait(&realm, action) |
| }; |
| rx.await |
| } |
| |
| /// Registers an action in the set, but does not wait for it to complete, instead returning a |
| /// future that can be used to wait on the task. This function is a no-op if the task is |
| /// already registered. |
| /// |
| /// REQUIRES: `self` is the `ActionSet` contained in `realm`. |
| pub fn register_no_wait<A>( |
| &mut self, |
| realm: &Arc<Realm>, |
| action: A, |
| ) -> impl Future<Output = A::Output> |
| where |
| A: Action, |
| { |
| let (task, rx) = self.register_inner(realm, action); |
| if let Some(task) = task { |
| task.spawn(); |
| } |
| rx |
| } |
| |
| /// Removes an action from the set, completing it. |
| async fn finish<'a>(realm: &Arc<Realm>, key: &'a ActionKey) { |
| let mut action_set = realm.lock_actions().await; |
| action_set.rep.remove(key); |
| } |
| |
| /// Registers, but does not execute, an action. |
| /// |
| /// Returns: |
| /// - An object that implements the action if it was scheduled for the first time. The caller |
| /// should call spawn() on it. |
| /// - A future to listen on the completion of the action. |
| #[must_use] |
| pub(crate) fn register_inner<'a, A>( |
| &'a mut self, |
| realm: &Arc<Realm>, |
| action: A, |
| ) -> (Option<ActionTask<A>>, ActionNotifier<A::Output>) |
| where |
| A: Action, |
| { |
| let key = action.key(); |
| if let Some(rx) = self.rep.get(&key) { |
| let rx = rx |
| .downcast_ref::<ActionNotifier<A::Output>>() |
| .expect("action notifier has unexpected type"); |
| let rx = rx.clone(); |
| (None, rx) |
| } else { |
| let blocking_action = match key { |
| ActionKey::Shutdown => self.rep.get(&ActionKey::Stop), |
| ActionKey::Stop => self.rep.get(&ActionKey::Shutdown), |
| _ => None, |
| }; |
| let realm = realm.clone(); |
| let handle_action = async move { |
| let res = action.handle(&realm).await; |
| Self::finish(&realm, &action.key()).await; |
| res |
| }; |
| let fut = if let Some(blocking_action) = blocking_action { |
| let blocking_action = blocking_action |
| .downcast_ref::<ActionNotifier<A::Output>>() |
| .expect("action notifier has unexpected type") |
| .clone(); |
| async move { |
| let _ = blocking_action.await; |
| handle_action.await |
| } |
| .boxed() |
| } else { |
| handle_action.boxed() |
| }; |
| let (tx, rx) = oneshot::channel(); |
| let task = ActionTask::new(tx, fut); |
| let rx = async move { |
| match rx.await { |
| Ok(res) => res, |
| Err(_) => { |
| // Normally we won't get here but this can happen if the sender's task is |
| // cancelled because, for example, component manager exited and the executor |
| // was torn down. |
| let () = pending().await; |
| unreachable!(); |
| } |
| } |
| } |
| .boxed() |
| .shared(); |
| self.rep.insert(key, Box::new(rx.clone())); |
| (Some(task), rx) |
| } |
| } |
| } |
| |
| async fn do_discover(realm: &Arc<Realm>) -> Result<(), ModelError> { |
| let is_discovered = { |
| let state = realm.lock_state().await; |
| match *state { |
| RealmState::New => false, |
| _ => true, |
| } |
| }; |
| if is_discovered { |
| return Ok(()); |
| } |
| let event = Event::new(&realm, Ok(EventPayload::Discovered)); |
| realm.hooks.dispatch(&event).await?; |
| { |
| let mut state = realm.lock_state().await; |
| assert!(matches!(*state, RealmState::New), "Realm in unexpected state after discover"); |
| *state = RealmState::Discovered |
| } |
| Ok(()) |
| } |
| |
| async fn do_mark_deleting(realm: &Arc<Realm>, moniker: ChildMoniker) -> Result<(), ModelError> { |
| let partial_moniker = moniker.to_partial(); |
| let child_realm = { |
| let state = realm.lock_state().await; |
| let state = state.get_resolved().expect("do_mark_deleting: not resolved"); |
| state.get_live_child_realm(&partial_moniker).map(|r| r.clone()) |
| }; |
| if let Some(child_realm) = child_realm { |
| let event = Event::new(&child_realm, Ok(EventPayload::MarkedForDestruction)); |
| child_realm.hooks.dispatch(&event).await?; |
| let mut state = realm.lock_state().await; |
| let state = state.get_resolved_mut().expect("do_mark_deleting: not resolved"); |
| state.mark_child_realm_deleting(&partial_moniker); |
| } else { |
| // Child already marked deleting. Nothing to do. |
| } |
| Ok(()) |
| } |
| |
| async fn do_delete_child(realm: &Arc<Realm>, moniker: ChildMoniker) -> Result<(), ModelError> { |
| // Some paths may have already marked the child deleting before scheduling the DeleteChild |
| // action, in which case this is a no-op. |
| ActionSet::register(realm.clone(), MarkDeletingAction::new(moniker.clone())).await?; |
| |
| // The child may not exist or may already be deleted by a previous DeleteChild action. |
| let child_realm = { |
| let state = realm.lock_state().await; |
| let state = state.get_resolved().expect("do_delete_child: not resolved"); |
| state.all_child_realms().get(&moniker).map(|r| r.clone()) |
| }; |
| if let Some(child_realm) = child_realm { |
| ActionSet::register(child_realm.clone(), DestroyAction::new()).await?; |
| { |
| let mut state = realm.lock_state().await; |
| state |
| .get_resolved_mut() |
| .expect("do_delete_child: not resolved") |
| .remove_child_realm(&moniker); |
| } |
| let event = Event::new(&child_realm, Ok(EventPayload::Destroyed)); |
| child_realm.hooks.dispatch(&event).await?; |
| } |
| |
| Ok(()) |
| } |
| |
| async fn do_destroy(realm: &Arc<Realm>) -> Result<(), ModelError> { |
| // For destruction to behave correctly, the component has to be shut down first. |
| ActionSet::register(realm.clone(), ShutdownAction::new()).await?; |
| |
| let nfs = if let Some(state) = realm.lock_state().await.get_resolved() { |
| let mut nfs = vec![]; |
| for (m, _) in state.all_child_realms().iter() { |
| let realm = realm.clone(); |
| let nf = ActionSet::register(realm, DeleteChildAction::new(m.clone())); |
| nfs.push(nf); |
| } |
| nfs |
| } else { |
| // Component was never resolved. No explicit cleanup is required for children. |
| vec![] |
| }; |
| let results = join_all(nfs).await; |
| ok_or_first_error(results)?; |
| |
| // Now that all children have been destroyed, destroy the parent. |
| realm.destroy_instance().await?; |
| Ok(()) |
| } |
| |
| fn ok_or_first_error(results: Vec<Result<(), ModelError>>) -> Result<(), ModelError> { |
| results.into_iter().fold(Ok(()), |acc, r| acc.and_then(|_| r)) |
| } |
| |
| #[cfg(test)] |
| pub mod tests { |
| use super::*; |
| use { |
| crate::model::{ |
| binding::Binder, |
| hooks::{EventType, Hook, HooksRegistration}, |
| realm::BindReason, |
| testing::{ |
| test_helpers::{ |
| component_decl_with_test_runner, execution_is_shut_down, has_child, |
| ActionsTest, ComponentDeclBuilder, ComponentInfo, |
| }, |
| test_hook::Lifecycle, |
| }, |
| }, |
| async_trait::async_trait, |
| cm_rust::{ |
| CapabilityPath, DependencyType, ExposeDecl, ExposeProtocolDecl, ExposeSource, |
| ExposeTarget, OfferDecl, OfferProtocolDecl, OfferServiceSource, OfferTarget, |
| ProtocolDecl, UseDecl, UseProtocolDecl, UseSource, |
| }, |
| fuchsia_async as fasync, |
| matches::assert_matches, |
| moniker::{AbsoluteMoniker, PartialMoniker}, |
| std::{convert::TryFrom, sync::Weak}, |
| }; |
| |
| async fn register_action_in_new_task<A>( |
| action: A, |
| realm: Arc<Realm>, |
| responder: oneshot::Sender<A::Output>, |
| res: A::Output, |
| ) where |
| A: Action, |
| { |
| let (starter_tx, starter_rx) = oneshot::channel(); |
| fasync::Task::spawn(async move { |
| let mut action_set = realm.lock_actions().await; |
| |
| // Register action, and get the future. Use `register_inner` so that we can control |
| // when to notify the listener. |
| let (task, rx) = action_set.register_inner(&realm, action); |
| |
| // Signal to test that action is registered. |
| starter_tx.send(()).unwrap(); |
| |
| // Drop `action_set` to release the lock. |
| drop(action_set); |
| |
| if let Some(task) = task { |
| // Notify the listeners, but don't actually run the action since this test tests |
| // action registration and not the actions themselves. |
| task.tx.send(res).unwrap(); |
| } |
| let res = rx.await; |
| |
| // If the future completed successfully then we will get to this point. |
| responder.send(res).expect("failed to send response"); |
| }) |
| .detach(); |
| starter_rx.await.expect("Unable to receive start signal"); |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn action_set() { |
| let test = ActionsTest::new("root", vec![], None).await; |
| let realm = test.model.root_realm.clone(); |
| |
| let (tx1, rx1) = oneshot::channel(); |
| register_action_in_new_task(DestroyAction::new(), realm.clone(), tx1, Ok(())).await; |
| let (tx2, rx2) = oneshot::channel(); |
| register_action_in_new_task( |
| ShutdownAction::new(), |
| realm.clone(), |
| tx2, |
| Err(ModelError::ComponentInvalid), |
| ) |
| .await; |
| let (tx3, rx3) = oneshot::channel(); |
| register_action_in_new_task(DestroyAction::new(), realm.clone(), tx3, Ok(())).await; |
| |
| // Complete actions, while checking notifications. |
| ActionSet::finish(&realm, &ActionKey::Destroy).await; |
| assert_matches!(rx1.await.expect("Unable to receive result of Notification"), Ok(())); |
| assert_matches!(rx3.await.expect("Unable to receive result of Notification"), Ok(())); |
| |
| ActionSet::finish(&realm, &ActionKey::Shutdown).await; |
| assert_matches!( |
| rx2.await.expect("Unable to receive result of Notification"), |
| Err(ModelError::ComponentInvalid) |
| ); |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn action_shutdown_blocks_stop() { |
| let test = ActionsTest::new("root", vec![], None).await; |
| let realm = test.model.root_realm.clone(); |
| let mut action_set = realm.lock_actions().await; |
| |
| // Register some actions, and get notifications. Use `register_inner` so we can register |
| // the action without immediately running it. |
| let (task1, nf1) = action_set.register_inner(&realm, ShutdownAction::new()); |
| let (task2, nf2) = action_set.register_inner(&realm, StopAction::new()); |
| |
| drop(action_set); |
| |
| // Complete actions, while checking futures. |
| ActionSet::finish(&realm, &ActionKey::Shutdown).await; |
| |
| // nf2 should be blocked on task1 completing. |
| assert!(nf1.peek().is_none()); |
| assert!(nf2.peek().is_none()); |
| task1.unwrap().tx.send(Ok(())).unwrap(); |
| task2.unwrap().spawn(); |
| nf1.await.unwrap(); |
| nf2.await.unwrap(); |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn action_shutdown_stop_stop() { |
| let test = ActionsTest::new("root", vec![], None).await; |
| let realm = test.model.root_realm.clone(); |
| let mut action_set = realm.lock_actions().await; |
| |
| // Register some actions, and get notifications. Use `register_inner` so we can register |
| // the action without immediately running it. |
| let (task1, nf1) = action_set.register_inner(&realm, ShutdownAction::new()); |
| let (task2, nf2) = action_set.register_inner(&realm, StopAction::new()); |
| let (task3, nf3) = action_set.register_inner(&realm, StopAction::new()); |
| |
| drop(action_set); |
| |
| // Complete actions, while checking notifications. |
| ActionSet::finish(&realm, &ActionKey::Shutdown).await; |
| |
| // nf2 and nf3 should be blocked on task1 completing. |
| assert!(nf1.peek().is_none()); |
| assert!(nf2.peek().is_none()); |
| task1.unwrap().tx.send(Ok(())).unwrap(); |
| task2.unwrap().spawn(); |
| assert!(task3.is_none()); |
| nf1.await.unwrap(); |
| nf2.await.unwrap(); |
| nf3.await.unwrap(); |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn shutdown_one_component() { |
| let components = vec![ |
| ("root", ComponentDeclBuilder::new().add_lazy_child("a").build()), |
| ("a", component_decl_with_test_runner()), |
| ]; |
| let test = ActionsTest::new("root", components, None).await; |
| // Bind to the component, causing it to start. This should cause the realm to have an |
| // `Execution`. |
| let realm = test.look_up(vec!["a:0"].into()).await; |
| test.model.bind(&realm.abs_moniker, &BindReason::Eager).await.expect("could not bind to a"); |
| assert!(is_executing(&realm).await); |
| let a_info = ComponentInfo::new(realm.clone()).await; |
| |
| // Register shutdown action, and wait for it. Component should shut down (no more |
| // `Execution`). |
| ActionSet::register(a_info.realm.clone(), ShutdownAction::new()) |
| .await |
| .expect("shutdown failed"); |
| a_info.check_is_shut_down(&test.runner).await; |
| |
| // Trying to bind to the component should fail because it's shut down. |
| test.model |
| .bind(&a_info.realm.abs_moniker, &BindReason::Eager) |
| .await |
| .expect_err("successfully bound to a after shutdown"); |
| |
| // Shut down the component again. This succeeds, but has no additional effect. |
| ActionSet::register(a_info.realm.clone(), ShutdownAction::new()) |
| .await |
| .expect("shutdown failed"); |
| &a_info.check_is_shut_down(&test.runner).await; |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn shutdown_collection() { |
| let components = vec![ |
| ("root", ComponentDeclBuilder::new().add_lazy_child("container").build()), |
| ( |
| "container", |
| ComponentDeclBuilder::new() |
| .add_transient_collection("coll") |
| .add_lazy_child("c") |
| .build(), |
| ), |
| ("a", component_decl_with_test_runner()), |
| ("b", component_decl_with_test_runner()), |
| ("c", component_decl_with_test_runner()), |
| ]; |
| let test = ActionsTest::new("root", components, Some(vec!["container:0"].into())).await; |
| |
| // Create dynamic instances in "coll". |
| test.create_dynamic_child("coll", "a").await; |
| test.create_dynamic_child("coll", "b").await; |
| |
| // Bind to the components, causing them to start. This should cause them to have an |
| // `Execution`. |
| let realm_container = test.look_up(vec!["container:0"].into()).await; |
| let realm_a = test.look_up(vec!["container:0", "coll:a:1"].into()).await; |
| let realm_b = test.look_up(vec!["container:0", "coll:b:2"].into()).await; |
| let realm_c = test.look_up(vec!["container:0", "c:0"].into()).await; |
| test.model |
| .bind(&realm_container.abs_moniker, &BindReason::Eager) |
| .await |
| .expect("could not bind to container"); |
| test.model |
| .bind(&realm_a.abs_moniker, &BindReason::Eager) |
| .await |
| .expect("could not bind to coll:a"); |
| test.model |
| .bind(&realm_b.abs_moniker, &BindReason::Eager) |
| .await |
| .expect("could not bind to coll:b"); |
| test.model |
| .bind(&realm_c.abs_moniker, &BindReason::Eager) |
| .await |
| .expect("could not bind to coll:b"); |
| assert!(is_executing(&realm_container).await); |
| assert!(is_executing(&realm_a).await); |
| assert!(is_executing(&realm_b).await); |
| assert!(is_executing(&realm_c).await); |
| assert!(has_child(&realm_container, "coll:a:1").await); |
| assert!(has_child(&realm_container, "coll:b:2").await); |
| |
| let realm_a_info = ComponentInfo::new(realm_a).await; |
| let realm_b_info = ComponentInfo::new(realm_b).await; |
| let realm_container_info = ComponentInfo::new(realm_container).await; |
| |
| // Register shutdown action, and wait for it. Components should shut down (no more |
| // `Execution`). Also, the instances in the collection should have been destroyed because |
| // they were transient. |
| ActionSet::register(realm_container_info.realm.clone(), ShutdownAction::new()) |
| .await |
| .expect("shutdown failed"); |
| realm_container_info.check_is_shut_down(&test.runner).await; |
| assert!(!has_child(&realm_container_info.realm, "coll:a:1").await); |
| assert!(!has_child(&realm_container_info.realm, "coll:b:2").await); |
| assert!(has_child(&realm_container_info.realm, "c:0").await); |
| realm_a_info.check_is_shut_down(&test.runner).await; |
| realm_b_info.check_is_shut_down(&test.runner).await; |
| |
| // Verify events. |
| { |
| let mut events: Vec<_> = test |
| .test_hook |
| .lifecycle() |
| .into_iter() |
| .filter(|e| match e { |
| Lifecycle::Stop(_) | Lifecycle::Destroy(_) => true, |
| _ => false, |
| }) |
| .collect(); |
| // The leaves could be stopped in any order. |
| let mut next: Vec<_> = events.drain(0..3).collect(); |
| next.sort_unstable(); |
| let expected: Vec<_> = vec![ |
| Lifecycle::Stop(vec!["container:0", "c:0"].into()), |
| Lifecycle::Stop(vec!["container:0", "coll:a:1"].into()), |
| Lifecycle::Stop(vec!["container:0", "coll:b:2"].into()), |
| ]; |
| assert_eq!(next, expected); |
| |
| // These components were destroyed because they lived in a transient collection. |
| let mut next: Vec<_> = events.drain(0..2).collect(); |
| next.sort_unstable(); |
| let expected: Vec<_> = vec![ |
| Lifecycle::Destroy(vec!["container:0", "coll:a:1"].into()), |
| Lifecycle::Destroy(vec!["container:0", "coll:b:2"].into()), |
| ]; |
| assert_eq!(next, expected); |
| } |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn shutdown_not_started() { |
| let components = vec![ |
| ("root", ComponentDeclBuilder::new().add_lazy_child("a").build()), |
| ("a", ComponentDeclBuilder::new().add_lazy_child("b").build()), |
| ("b", component_decl_with_test_runner()), |
| ]; |
| let test = ActionsTest::new("root", components, None).await; |
| let realm_a = test.look_up(vec!["a:0"].into()).await; |
| let realm_b = test.look_up(vec!["a:0", "b:0"].into()).await; |
| assert!(!is_executing(&realm_a).await); |
| assert!(!is_executing(&realm_b).await); |
| |
| // Register shutdown action on "a", and wait for it. |
| ActionSet::register(realm_a.clone(), ShutdownAction::new()).await.expect("shutdown failed"); |
| assert!(execution_is_shut_down(&realm_a).await); |
| assert!(execution_is_shut_down(&realm_b).await); |
| |
| // Now "a" is shut down. There should be no events though because the component was |
| // never started. |
| ActionSet::register(realm_a.clone(), ShutdownAction::new()).await.expect("shutdown failed"); |
| assert!(execution_is_shut_down(&realm_a).await); |
| assert!(execution_is_shut_down(&realm_b).await); |
| { |
| let events: Vec<_> = test |
| .test_hook |
| .lifecycle() |
| .into_iter() |
| .filter(|e| match e { |
| Lifecycle::Stop(_) => true, |
| _ => false, |
| }) |
| .collect(); |
| assert_eq!(events, Vec::<Lifecycle>::new()); |
| } |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn shutdown_not_resolved() { |
| let components = vec![ |
| ("root", ComponentDeclBuilder::new().add_lazy_child("a").build()), |
| ("a", ComponentDeclBuilder::new().add_lazy_child("b").build()), |
| ("b", ComponentDeclBuilder::new().add_lazy_child("c").build()), |
| ("c", component_decl_with_test_runner()), |
| ]; |
| let test = ActionsTest::new("root", components, None).await; |
| let realm_a = test.look_up(vec!["a:0"].into()).await; |
| test.model |
| .bind(&realm_a.abs_moniker, &BindReason::Eager) |
| .await |
| .expect("could not bind to a"); |
| assert!(is_executing(&realm_a).await); |
| |
| // Register shutdown action on "a", and wait for it. |
| ActionSet::register(realm_a.clone(), ShutdownAction::new()).await.expect("shutdown failed"); |
| assert!(execution_is_shut_down(&realm_a).await); |
| // Get realm_b without resolving it. |
| let realm_b = { |
| let state = realm_a.lock_state().await; |
| let state = state.get_resolved().unwrap(); |
| state.get_live_child_realm(&PartialMoniker::from("b")).expect("child b not found") |
| }; |
| assert!(execution_is_shut_down(&realm_b).await); |
| assert!(is_unresolved(&realm_b).await); |
| |
| // Now "a" is shut down. There should be no event for "b" because it was never started |
| // (or resolved). |
| { |
| let events: Vec<_> = test |
| .test_hook |
| .lifecycle() |
| .into_iter() |
| .filter(|e| match e { |
| Lifecycle::Stop(_) => true, |
| _ => false, |
| }) |
| .collect(); |
| assert_eq!(events, vec![Lifecycle::Stop(vec!["a:0"].into())]); |
| } |
| } |
| |
| /// Shut down `a`: |
| /// a |
| /// \ |
| /// b |
| /// / \ |
| /// c d |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn shutdown_hierarchy() { |
| let components = vec![ |
| ("root", ComponentDeclBuilder::new().add_lazy_child("a").build()), |
| ("a", ComponentDeclBuilder::new().add_eager_child("b").build()), |
| ("b", ComponentDeclBuilder::new().add_eager_child("c").add_eager_child("d").build()), |
| ("c", component_decl_with_test_runner()), |
| ("d", component_decl_with_test_runner()), |
| ]; |
| let test = ActionsTest::new("root", components, None).await; |
| let realm_a = test.look_up(vec!["a:0"].into()).await; |
| let realm_b = test.look_up(vec!["a:0", "b:0"].into()).await; |
| let realm_c = test.look_up(vec!["a:0", "b:0", "c:0"].into()).await; |
| let realm_d = test.look_up(vec!["a:0", "b:0", "d:0"].into()).await; |
| |
| // Component startup was eager, so they should all have an `Execution`. |
| test.model |
| .bind(&realm_a.abs_moniker, &BindReason::Eager) |
| .await |
| .expect("could not bind to a"); |
| assert!(is_executing(&realm_a).await); |
| assert!(is_executing(&realm_b).await); |
| assert!(is_executing(&realm_c).await); |
| assert!(is_executing(&realm_d).await); |
| |
| let realm_a_info = ComponentInfo::new(realm_a).await; |
| let realm_b_info = ComponentInfo::new(realm_b).await; |
| let realm_c_info = ComponentInfo::new(realm_c).await; |
| let realm_d_info = ComponentInfo::new(realm_d).await; |
| |
| // Register shutdown action on "a", and wait for it. This should cause all components |
| // to shut down, in bottom-up order. |
| ActionSet::register(realm_a_info.realm.clone(), ShutdownAction::new()) |
| .await |
| .expect("shutdown failed"); |
| realm_a_info.check_is_shut_down(&test.runner).await; |
| realm_b_info.check_is_shut_down(&test.runner).await; |
| realm_c_info.check_is_shut_down(&test.runner).await; |
| realm_d_info.check_is_shut_down(&test.runner).await; |
| { |
| let mut events: Vec<_> = test |
| .test_hook |
| .lifecycle() |
| .into_iter() |
| .filter(|e| match e { |
| Lifecycle::Stop(_) => true, |
| _ => false, |
| }) |
| .collect(); |
| let mut first: Vec<_> = events.drain(0..2).collect(); |
| first.sort_unstable(); |
| let expected: Vec<_> = vec![ |
| Lifecycle::Stop(vec!["a:0", "b:0", "c:0"].into()), |
| Lifecycle::Stop(vec!["a:0", "b:0", "d:0"].into()), |
| ]; |
| assert_eq!(first, expected); |
| assert_eq!( |
| events, |
| vec![ |
| Lifecycle::Stop(vec!["a:0", "b:0"].into()), |
| Lifecycle::Stop(vec!["a:0"].into()) |
| ] |
| ); |
| } |
| } |
| |
| /// Shut down `a`: |
| /// a |
| /// \ |
| /// b |
| /// / | \ |
| /// c<-d->e |
| /// In this case C and E use a service provided by d |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn shutdown_with_multiple_deps() { |
| let components = vec![ |
| ("root", ComponentDeclBuilder::new().add_lazy_child("a").build()), |
| ("a", ComponentDeclBuilder::new().add_eager_child("b").build()), |
| ( |
| "b", |
| ComponentDeclBuilder::new() |
| .add_eager_child("c") |
| .add_eager_child("d") |
| .add_eager_child("e") |
| .offer(OfferDecl::Protocol(OfferProtocolDecl { |
| source: OfferServiceSource::Child("d".to_string()), |
| source_name: "serviceD".into(), |
| target_name: "serviceD".into(), |
| target: OfferTarget::Child("c".to_string()), |
| dependency_type: DependencyType::Strong, |
| })) |
| .offer(OfferDecl::Protocol(OfferProtocolDecl { |
| source: OfferServiceSource::Child("d".to_string()), |
| source_name: "serviceD".into(), |
| target_name: "serviceD".into(), |
| target: OfferTarget::Child("e".to_string()), |
| dependency_type: DependencyType::Strong, |
| })) |
| .build(), |
| ), |
| ( |
| "c", |
| ComponentDeclBuilder::new() |
| .use_(UseDecl::Protocol(UseProtocolDecl { |
| source: UseSource::Parent, |
| source_name: "serviceD".into(), |
| target_path: CapabilityPath::try_from("/svc/serviceD").unwrap(), |
| })) |
| .build(), |
| ), |
| ( |
| "d", |
| ComponentDeclBuilder::new() |
| .protocol(ProtocolDecl { |
| name: "serviceD".into(), |
| source_path: "/svc/serviceD".parse().unwrap(), |
| }) |
| .expose(ExposeDecl::Protocol(ExposeProtocolDecl { |
| source: ExposeSource::Self_, |
| source_name: "serviceD".into(), |
| target_name: "serviceD".into(), |
| target: ExposeTarget::Parent, |
| })) |
| .build(), |
| ), |
| ( |
| "e", |
| ComponentDeclBuilder::new() |
| .use_(UseDecl::Protocol(UseProtocolDecl { |
| source: UseSource::Parent, |
| source_name: "serviceD".into(), |
| target_path: CapabilityPath::try_from("/svc/serviceD").unwrap(), |
| })) |
| .build(), |
| ), |
| ]; |
| let test = ActionsTest::new("root", components, None).await; |
| let realm_a = test.look_up(vec!["a:0"].into()).await; |
| let realm_b = test.look_up(vec!["a:0", "b:0"].into()).await; |
| let realm_c = test.look_up(vec!["a:0", "b:0", "c:0"].into()).await; |
| let realm_d = test.look_up(vec!["a:0", "b:0", "d:0"].into()).await; |
| let realm_e = test.look_up(vec!["a:0", "b:0", "e:0"].into()).await; |
| |
| // Component startup was eager, so they should all have an `Execution`. |
| test.model |
| .bind(&realm_a.abs_moniker, &BindReason::Eager) |
| .await |
| .expect("could not bind to a"); |
| assert!(is_executing(&realm_a).await); |
| assert!(is_executing(&realm_b).await); |
| assert!(is_executing(&realm_c).await); |
| assert!(is_executing(&realm_d).await); |
| assert!(is_executing(&realm_e).await); |
| |
| let realm_a_info = ComponentInfo::new(realm_a).await; |
| let realm_b_info = ComponentInfo::new(realm_b).await; |
| let realm_c_info = ComponentInfo::new(realm_c).await; |
| let realm_d_info = ComponentInfo::new(realm_d).await; |
| let realm_e_info = ComponentInfo::new(realm_e).await; |
| |
| // Register shutdown action on "a", and wait for it. This should cause all components |
| // to shut down, in bottom-up order. |
| ActionSet::register(realm_a_info.realm.clone(), ShutdownAction::new()) |
| .await |
| .expect("shutdown failed"); |
| realm_a_info.check_is_shut_down(&test.runner).await; |
| realm_b_info.check_is_shut_down(&test.runner).await; |
| realm_c_info.check_is_shut_down(&test.runner).await; |
| realm_d_info.check_is_shut_down(&test.runner).await; |
| realm_e_info.check_is_shut_down(&test.runner).await; |
| |
| { |
| let mut events: Vec<_> = test |
| .test_hook |
| .lifecycle() |
| .into_iter() |
| .filter(|e| match e { |
| Lifecycle::Stop(_) => true, |
| _ => false, |
| }) |
| .collect(); |
| let mut first: Vec<_> = events.drain(0..2).collect(); |
| first.sort_unstable(); |
| let mut expected: Vec<_> = vec![ |
| Lifecycle::Stop(vec!["a:0", "b:0", "c:0"].into()), |
| Lifecycle::Stop(vec!["a:0", "b:0", "e:0"].into()), |
| ]; |
| assert_eq!(first, expected); |
| |
| let next: Vec<_> = events.drain(0..1).collect(); |
| expected = vec![Lifecycle::Stop(vec!["a:0", "b:0", "d:0"].into())]; |
| assert_eq!(next, expected); |
| |
| assert_eq!( |
| events, |
| vec![ |
| Lifecycle::Stop(vec!["a:0", "b:0"].into()), |
| Lifecycle::Stop(vec!["a:0"].into()) |
| ] |
| ); |
| } |
| } |
| |
| /// Shut down `a`: |
| /// a |
| /// \ |
| /// b |
| /// / / \ \ |
| /// c<-d->e->f |
| /// In this case C and E use a service provided by D and |
| /// F uses a service provided by E, shutdown order should be |
| /// {F}, {C, E}, {D}, {B}, {A} |
| /// Note that C must stop before D, but may stop before or after |
| /// either of F and E. |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn shutdown_with_multiple_out_and_longer_chain() { |
| let components = vec![ |
| ("root", ComponentDeclBuilder::new().add_lazy_child("a").build()), |
| ("a", ComponentDeclBuilder::new().add_eager_child("b").build()), |
| ( |
| "b", |
| ComponentDeclBuilder::new() |
| .add_eager_child("c") |
| .add_eager_child("d") |
| .add_eager_child("e") |
| .add_eager_child("f") |
| .offer(OfferDecl::Protocol(OfferProtocolDecl { |
| source: OfferServiceSource::Child("d".to_string()), |
| source_name: "serviceD".into(), |
| target_name: "serviceD".into(), |
| target: OfferTarget::Child("c".to_string()), |
| dependency_type: DependencyType::Strong, |
| })) |
| .offer(OfferDecl::Protocol(OfferProtocolDecl { |
| source: OfferServiceSource::Child("d".to_string()), |
| source_name: "serviceD".into(), |
| target_name: "serviceD".into(), |
| target: OfferTarget::Child("e".to_string()), |
| dependency_type: DependencyType::Strong, |
| })) |
| .offer(OfferDecl::Protocol(OfferProtocolDecl { |
| source: OfferServiceSource::Child("e".to_string()), |
| source_name: "serviceE".into(), |
| target_name: "serviceE".into(), |
| target: OfferTarget::Child("f".to_string()), |
| dependency_type: DependencyType::Strong, |
| })) |
| .build(), |
| ), |
| ( |
| "c", |
| ComponentDeclBuilder::new() |
| .use_(UseDecl::Protocol(UseProtocolDecl { |
| source: UseSource::Parent, |
| source_name: "serviceD".into(), |
| target_path: CapabilityPath::try_from("/svc/serviceD").unwrap(), |
| })) |
| .build(), |
| ), |
| ( |
| "d", |
| ComponentDeclBuilder::new() |
| .protocol(ProtocolDecl { |
| name: "serviceD".into(), |
| source_path: "/svc/serviceD".parse().unwrap(), |
| }) |
| .expose(ExposeDecl::Protocol(ExposeProtocolDecl { |
| source: ExposeSource::Self_, |
| source_name: "serviceD".into(), |
| target_name: "serviceD".into(), |
| target: ExposeTarget::Parent, |
| })) |
| .build(), |
| ), |
| ( |
| "e", |
| ComponentDeclBuilder::new() |
| .protocol(ProtocolDecl { |
| name: "serviceE".into(), |
| source_path: "/svc/serviceE".parse().unwrap(), |
| }) |
| .use_(UseDecl::Protocol(UseProtocolDecl { |
| source: UseSource::Parent, |
| source_name: "serviceD".into(), |
| target_path: CapabilityPath::try_from("/svc/serviceD").unwrap(), |
| })) |
| .expose(ExposeDecl::Protocol(ExposeProtocolDecl { |
| source: ExposeSource::Self_, |
| source_name: "serviceE".into(), |
| target_name: "serviceE".into(), |
| target: ExposeTarget::Parent, |
| })) |
| .build(), |
| ), |
| ( |
| "f", |
| ComponentDeclBuilder::new() |
| .use_(UseDecl::Protocol(UseProtocolDecl { |
| source: UseSource::Parent, |
| source_name: "serviceE".into(), |
| target_path: CapabilityPath::try_from("/svc/serviceE").unwrap(), |
| })) |
| .build(), |
| ), |
| ]; |
| let moniker_a: AbsoluteMoniker = vec!["a:0"].into(); |
| let moniker_b: AbsoluteMoniker = vec!["a:0", "b:0"].into(); |
| let moniker_c: AbsoluteMoniker = vec!["a:0", "b:0", "c:0"].into(); |
| let moniker_d: AbsoluteMoniker = vec!["a:0", "b:0", "d:0"].into(); |
| let moniker_e: AbsoluteMoniker = vec!["a:0", "b:0", "e:0"].into(); |
| let moniker_f: AbsoluteMoniker = vec!["a:0", "b:0", "f:0"].into(); |
| let test = ActionsTest::new("root", components, None).await; |
| let realm_a = test.look_up(moniker_a.clone()).await; |
| let realm_b = test.look_up(moniker_b.clone()).await; |
| let realm_c = test.look_up(moniker_c.clone()).await; |
| let realm_d = test.look_up(moniker_d.clone()).await; |
| let realm_e = test.look_up(moniker_e.clone()).await; |
| let realm_f = test.look_up(moniker_f.clone()).await; |
| |
| // Component startup was eager, so they should all have an `Execution`. |
| test.model |
| .bind(&realm_a.abs_moniker, &BindReason::Eager) |
| .await |
| .expect("could not bind to a"); |
| assert!(is_executing(&realm_a).await); |
| assert!(is_executing(&realm_b).await); |
| assert!(is_executing(&realm_c).await); |
| assert!(is_executing(&realm_d).await); |
| assert!(is_executing(&realm_e).await); |
| assert!(is_executing(&realm_f).await); |
| |
| let realm_a_info = ComponentInfo::new(realm_a).await; |
| let realm_b_info = ComponentInfo::new(realm_b).await; |
| let realm_c_info = ComponentInfo::new(realm_c).await; |
| let realm_d_info = ComponentInfo::new(realm_d).await; |
| let realm_e_info = ComponentInfo::new(realm_e).await; |
| let realm_f_info = ComponentInfo::new(realm_f).await; |
| |
| // Register shutdown action on "a", and wait for it. This should cause all components |
| // to shut down, in bottom-up order. |
| ActionSet::register(realm_a_info.realm.clone(), ShutdownAction::new()) |
| .await |
| .expect("shutdown failed"); |
| realm_a_info.check_is_shut_down(&test.runner).await; |
| realm_b_info.check_is_shut_down(&test.runner).await; |
| realm_c_info.check_is_shut_down(&test.runner).await; |
| realm_d_info.check_is_shut_down(&test.runner).await; |
| realm_e_info.check_is_shut_down(&test.runner).await; |
| realm_f_info.check_is_shut_down(&test.runner).await; |
| |
| let mut comes_after: HashMap<AbsoluteMoniker, Vec<AbsoluteMoniker>> = HashMap::new(); |
| comes_after.insert(moniker_a.clone(), vec![moniker_b.clone()]); |
| // technically we could just depend on 'D' since it is the last of b's |
| // children, but we add all the children for resilence against the |
| // future |
| comes_after.insert( |
| moniker_b.clone(), |
| vec![moniker_c.clone(), moniker_d.clone(), moniker_e.clone(), moniker_f.clone()], |
| ); |
| comes_after.insert(moniker_d.clone(), vec![moniker_c.clone(), moniker_e.clone()]); |
| comes_after.insert(moniker_c.clone(), vec![]); |
| comes_after.insert(moniker_e.clone(), vec![moniker_f.clone()]); |
| comes_after.insert(moniker_f.clone(), vec![]); |
| { |
| let events: Vec<_> = test |
| .test_hook |
| .lifecycle() |
| .into_iter() |
| .filter(|e| match e { |
| Lifecycle::Stop(_) => true, |
| _ => false, |
| }) |
| .collect(); |
| |
| for e in events { |
| match e { |
| Lifecycle::Stop(moniker) => match comes_after.remove(&moniker) { |
| Some(dependents) => { |
| for d in dependents { |
| if comes_after.contains_key(&d) { |
| panic!("{} stopped before its dependent {}", moniker, d); |
| } |
| } |
| } |
| None => { |
| panic!("{} was unknown or shut down more than once", moniker); |
| } |
| }, |
| _ => { |
| panic!("Unexpected lifecycle type"); |
| } |
| } |
| } |
| } |
| } |
| |
| /// Shut down `a`: |
| /// a |
| /// |
| /// | |
| /// |
| /// +---- b ----+ |
| /// / \ |
| /// / / \ \ |
| /// |
| /// c <~~ d ~~> e ~~> f |
| /// \ / |
| /// +~~>~~+ |
| /// In this case C and E use a service provided by D and |
| /// F uses a services provided by E and D, shutdown order should be F must |
| /// stop before E and {C,E,F} must stop before D. C may stop before or |
| /// after either of {F, E}. |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn shutdown_with_multiple_out_multiple_in() { |
| let components = vec![ |
| ("root", ComponentDeclBuilder::new().add_lazy_child("a").build()), |
| ("a", ComponentDeclBuilder::new().add_eager_child("b").build()), |
| ( |
| "b", |
| ComponentDeclBuilder::new() |
| .add_eager_child("c") |
| .add_eager_child("d") |
| .add_eager_child("e") |
| .add_eager_child("f") |
| .offer(OfferDecl::Protocol(OfferProtocolDecl { |
| source: OfferServiceSource::Child("d".to_string()), |
| source_name: "serviceD".into(), |
| target_name: "serviceD".into(), |
| target: OfferTarget::Child("c".to_string()), |
| dependency_type: DependencyType::Strong, |
| })) |
| .offer(OfferDecl::Protocol(OfferProtocolDecl { |
| source: OfferServiceSource::Child("d".to_string()), |
| source_name: "serviceD".into(), |
| target_name: "serviceD".into(), |
| target: OfferTarget::Child("e".to_string()), |
| dependency_type: DependencyType::Strong, |
| })) |
| .offer(OfferDecl::Protocol(OfferProtocolDecl { |
| source: OfferServiceSource::Child("d".to_string()), |
| source_name: "serviceD".into(), |
| target_name: "serviceD".into(), |
| target: OfferTarget::Child("f".to_string()), |
| dependency_type: DependencyType::Strong, |
| })) |
| .offer(OfferDecl::Protocol(OfferProtocolDecl { |
| source: OfferServiceSource::Child("e".to_string()), |
| source_name: "serviceE".into(), |
| target_name: "serviceE".into(), |
| target: OfferTarget::Child("f".to_string()), |
| dependency_type: DependencyType::Strong, |
| })) |
| .build(), |
| ), |
| ( |
| "c", |
| ComponentDeclBuilder::new() |
| .use_(UseDecl::Protocol(UseProtocolDecl { |
| source: UseSource::Parent, |
| source_name: "serviceD".into(), |
| target_path: CapabilityPath::try_from("/svc/serviceD").unwrap(), |
| })) |
| .build(), |
| ), |
| ( |
| "d", |
| ComponentDeclBuilder::new() |
| .protocol(ProtocolDecl { |
| name: "serviceD".into(), |
| source_path: "/svc/serviceD".parse().unwrap(), |
| }) |
| .expose(ExposeDecl::Protocol(ExposeProtocolDecl { |
| source: ExposeSource::Self_, |
| source_name: "serviceD".into(), |
| target_name: "serviceD".into(), |
| target: ExposeTarget::Parent, |
| })) |
| .build(), |
| ), |
| ( |
| "e", |
| ComponentDeclBuilder::new() |
| .protocol(ProtocolDecl { |
| name: "serviceE".into(), |
| source_path: "/svc/serviceE".parse().unwrap(), |
| }) |
| .use_(UseDecl::Protocol(UseProtocolDecl { |
| source: UseSource::Parent, |
| source_name: "serviceE".into(), |
| target_path: CapabilityPath::try_from("/svc/serviceE").unwrap(), |
| })) |
| .expose(ExposeDecl::Protocol(ExposeProtocolDecl { |
| source: ExposeSource::Self_, |
| source_name: "serviceE".into(), |
| target_name: "serviceE".into(), |
| target: ExposeTarget::Parent, |
| })) |
| .build(), |
| ), |
| ( |
| "f", |
| ComponentDeclBuilder::new() |
| .use_(UseDecl::Protocol(UseProtocolDecl { |
| source: UseSource::Parent, |
| source_name: "serviceE".into(), |
| target_path: CapabilityPath::try_from("/svc/serviceE").unwrap(), |
| })) |
| .use_(UseDecl::Protocol(UseProtocolDecl { |
| source: UseSource::Parent, |
| source_name: "serviceD".into(), |
| target_path: CapabilityPath::try_from("/svc/serviceD").unwrap(), |
| })) |
| .build(), |
| ), |
| ]; |
| let moniker_a: AbsoluteMoniker = vec!["a:0"].into(); |
| let moniker_b: AbsoluteMoniker = vec!["a:0", "b:0"].into(); |
| let moniker_c: AbsoluteMoniker = vec!["a:0", "b:0", "c:0"].into(); |
| let moniker_d: AbsoluteMoniker = vec!["a:0", "b:0", "d:0"].into(); |
| let moniker_e: AbsoluteMoniker = vec!["a:0", "b:0", "e:0"].into(); |
| let moniker_f: AbsoluteMoniker = vec!["a:0", "b:0", "f:0"].into(); |
| let test = ActionsTest::new("root", components, None).await; |
| let realm_a = test.look_up(moniker_a.clone()).await; |
| let realm_b = test.look_up(moniker_b.clone()).await; |
| let realm_c = test.look_up(moniker_c.clone()).await; |
| let realm_d = test.look_up(moniker_d.clone()).await; |
| let realm_e = test.look_up(moniker_e.clone()).await; |
| let realm_f = test.look_up(moniker_f.clone()).await; |
| |
| // Component startup was eager, so they should all have an `Execution`. |
| test.model |
| .bind(&realm_a.abs_moniker, &BindReason::Eager) |
| .await |
| .expect("could not bind to a"); |
| assert!(is_executing(&realm_a).await); |
| assert!(is_executing(&realm_b).await); |
| assert!(is_executing(&realm_c).await); |
| assert!(is_executing(&realm_d).await); |
| assert!(is_executing(&realm_e).await); |
| assert!(is_executing(&realm_f).await); |
| |
| let realm_a_info = ComponentInfo::new(realm_a).await; |
| let realm_b_info = ComponentInfo::new(realm_b).await; |
| let realm_c_info = ComponentInfo::new(realm_c).await; |
| let realm_d_info = ComponentInfo::new(realm_d).await; |
| let realm_e_info = ComponentInfo::new(realm_e).await; |
| let realm_f_info = ComponentInfo::new(realm_f).await; |
| |
| // Register shutdown action on "a", and wait for it. This should cause all components |
| // to shut down, in bottom-up order. |
| ActionSet::register(realm_a_info.realm.clone(), ShutdownAction::new()) |
| .await |
| .expect("shutdown failed"); |
| realm_a_info.check_is_shut_down(&test.runner).await; |
| realm_b_info.check_is_shut_down(&test.runner).await; |
| realm_c_info.check_is_shut_down(&test.runner).await; |
| realm_d_info.check_is_shut_down(&test.runner).await; |
| realm_e_info.check_is_shut_down(&test.runner).await; |
| realm_f_info.check_is_shut_down(&test.runner).await; |
| |
| let mut comes_after: HashMap<AbsoluteMoniker, Vec<AbsoluteMoniker>> = HashMap::new(); |
| comes_after.insert(moniker_a.clone(), vec![moniker_b.clone()]); |
| // technically we could just depend on 'D' since it is the last of b's |
| // children, but we add all the children for resilence against the |
| // future |
| comes_after.insert( |
| moniker_b.clone(), |
| vec![moniker_c.clone(), moniker_d.clone(), moniker_e.clone(), moniker_f.clone()], |
| ); |
| comes_after.insert( |
| moniker_d.clone(), |
| vec![moniker_c.clone(), moniker_e.clone(), moniker_f.clone()], |
| ); |
| comes_after.insert(moniker_c.clone(), vec![]); |
| comes_after.insert(moniker_e.clone(), vec![moniker_f.clone()]); |
| comes_after.insert(moniker_f.clone(), vec![]); |
| { |
| let events: Vec<_> = test |
| .test_hook |
| .lifecycle() |
| .into_iter() |
| .filter(|e| match e { |
| Lifecycle::Stop(_) => true, |
| _ => false, |
| }) |
| .collect(); |
| |
| for e in events { |
| match e { |
| Lifecycle::Stop(moniker) => { |
| let dependents = comes_after.remove(&moniker).expect(&format!( |
| "{} was unknown or shut down more than once", |
| moniker |
| )); |
| for d in dependents { |
| if comes_after.contains_key(&d) { |
| panic!("{} stopped before its dependent {}", moniker, d); |
| } |
| } |
| } |
| _ => { |
| panic!("Unexpected lifecycle type"); |
| } |
| } |
| } |
| } |
| } |
| |
| /// Shut down `a`: |
| /// a |
| /// \ |
| /// b |
| /// / \ |
| /// c-->d |
| /// In this case D uses a resource exposed by C |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn shutdown_with_dependency() { |
| let components = vec![ |
| ("root", ComponentDeclBuilder::new().add_lazy_child("a").build()), |
| ("a", ComponentDeclBuilder::new().add_eager_child("b").build()), |
| ( |
| "b", |
| ComponentDeclBuilder::new() |
| .add_eager_child("c") |
| .add_eager_child("d") |
| .offer(OfferDecl::Protocol(OfferProtocolDecl { |
| source: OfferServiceSource::Child("c".to_string()), |
| source_name: "serviceC".into(), |
| target_name: "serviceC".into(), |
| target: OfferTarget::Child("d".to_string()), |
| dependency_type: DependencyType::Strong, |
| })) |
| .build(), |
| ), |
| ( |
| "c", |
| ComponentDeclBuilder::new() |
| .protocol(ProtocolDecl { |
| name: "serviceC".into(), |
| source_path: "/svc/serviceC".parse().unwrap(), |
| }) |
| .expose(ExposeDecl::Protocol(ExposeProtocolDecl { |
| source: ExposeSource::Self_, |
| source_name: "serviceC".into(), |
| target_name: "serviceC".into(), |
| target: ExposeTarget::Parent, |
| })) |
| .build(), |
| ), |
| ( |
| "d", |
| ComponentDeclBuilder::new() |
| .use_(UseDecl::Protocol(UseProtocolDecl { |
| source: UseSource::Parent, |
| source_name: "serviceC".into(), |
| target_path: CapabilityPath::try_from("/svc/serviceC").unwrap(), |
| })) |
| .build(), |
| ), |
| ]; |
| let test = ActionsTest::new("root", components, None).await; |
| let realm_a = test.look_up(vec!["a:0"].into()).await; |
| let realm_b = test.look_up(vec!["a:0", "b:0"].into()).await; |
| let realm_c = test.look_up(vec!["a:0", "b:0", "c:0"].into()).await; |
| let realm_d = test.look_up(vec!["a:0", "b:0", "d:0"].into()).await; |
| |
| // Component startup was eager, so they should all have an `Execution`. |
| test.model |
| .bind(&realm_a.abs_moniker, &BindReason::Eager) |
| .await |
| .expect("could not bind to a"); |
| |
| let realm_a_info = ComponentInfo::new(realm_a).await; |
| let realm_b_info = ComponentInfo::new(realm_b).await; |
| let realm_c_info = ComponentInfo::new(realm_c).await; |
| let realm_d_info = ComponentInfo::new(realm_d).await; |
| |
| // Register shutdown action on "a", and wait for it. This should cause all components |
| // to shut down, in bottom-up and dependency order. |
| ActionSet::register(realm_a_info.realm.clone(), ShutdownAction::new()) |
| .await |
| .expect("shutdown failed"); |
| realm_a_info.check_is_shut_down(&test.runner).await; |
| realm_b_info.check_is_shut_down(&test.runner).await; |
| realm_c_info.check_is_shut_down(&test.runner).await; |
| realm_d_info.check_is_shut_down(&test.runner).await; |
| |
| { |
| let events: Vec<_> = test |
| .test_hook |
| .lifecycle() |
| .into_iter() |
| .filter(|e| match e { |
| Lifecycle::Stop(_) => true, |
| _ => false, |
| }) |
| .collect(); |
| let expected: Vec<_> = vec![ |
| Lifecycle::Stop(vec!["a:0", "b:0", "d:0"].into()), |
| Lifecycle::Stop(vec!["a:0", "b:0", "c:0"].into()), |
| Lifecycle::Stop(vec!["a:0", "b:0"].into()), |
| Lifecycle::Stop(vec!["a:0"].into()), |
| ]; |
| assert_eq!(events, expected); |
| } |
| } |
| |
| /// Shut down `b`: |
| /// a |
| /// \ |
| /// b |
| /// \ |
| /// b |
| /// \ |
| /// ... |
| /// |
| /// `b` is a child of itself, but shutdown should still be able to complete. |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn shutdown_self_referential() { |
| let components = vec![ |
| ("root", ComponentDeclBuilder::new().add_lazy_child("a").build()), |
| ("a", ComponentDeclBuilder::new().add_lazy_child("b").build()), |
| ("b", ComponentDeclBuilder::new().add_lazy_child("b").build()), |
| ]; |
| let test = ActionsTest::new("root", components, None).await; |
| let realm_a = test.look_up(vec!["a:0"].into()).await; |
| let realm_b = test.look_up(vec!["a:0", "b:0"].into()).await; |
| let realm_b2 = test.look_up(vec!["a:0", "b:0", "b:0"].into()).await; |
| |
| // Bind to second `b`. |
| test.model |
| .bind(&realm_a.abs_moniker, &BindReason::Eager) |
| .await |
| .expect("could not bind to b2"); |
| test.model |
| .bind(&realm_b.abs_moniker, &BindReason::Eager) |
| .await |
| .expect("could not bind to b2"); |
| test.model |
| .bind(&realm_b2.abs_moniker, &BindReason::Eager) |
| .await |
| .expect("could not bind to b2"); |
| assert!(is_executing(&realm_a).await); |
| assert!(is_executing(&realm_b).await); |
| assert!(is_executing(&realm_b2).await); |
| |
| let realm_a_info = ComponentInfo::new(realm_a).await; |
| let realm_b_info = ComponentInfo::new(realm_b).await; |
| let realm_b2_info = ComponentInfo::new(realm_b2).await; |
| |
| // Register shutdown action on "a", and wait for it. This should cause all components |
| // to shut down, in bottom-up and dependency order. |
| ActionSet::register(realm_a_info.realm.clone(), ShutdownAction::new()) |
| .await |
| .expect("shutdown failed"); |
| realm_a_info.check_is_shut_down(&test.runner).await; |
| realm_b_info.check_is_shut_down(&test.runner).await; |
| realm_b2_info.check_is_shut_down(&test.runner).await; |
| { |
| let events: Vec<_> = test |
| .test_hook |
| .lifecycle() |
| .into_iter() |
| .filter(|e| match e { |
| Lifecycle::Stop(_) => true, |
| _ => false, |
| }) |
| .collect(); |
| assert_eq!( |
| events, |
| vec![ |
| Lifecycle::Stop(vec!["a:0", "b:0", "b:0"].into()), |
| Lifecycle::Stop(vec!["a:0", "b:0"].into()), |
| Lifecycle::Stop(vec!["a:0"].into()) |
| ] |
| ); |
| } |
| } |
| |
| /// Shut down `a`: |
| /// a |
| /// \ |
| /// b |
| /// / \ |
| /// c d |
| /// |
| /// `b` fails to finish shutdown the first time, but succeeds the second time. |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn shutdown_error() { |
| struct StopErrorHook { |
| moniker: AbsoluteMoniker, |
| } |
| |
| impl StopErrorHook { |
| fn new(moniker: AbsoluteMoniker) -> Self { |
| Self { moniker } |
| } |
| |
| fn hooks(self: &Arc<Self>) -> Vec<HooksRegistration> { |
| vec![HooksRegistration::new( |
| "StopErrorHook", |
| vec![EventType::Stopped], |
| Arc::downgrade(self) as Weak<dyn Hook>, |
| )] |
| } |
| |
| async fn on_shutdown_instance_async( |
| &self, |
| target_moniker: &AbsoluteMoniker, |
| ) -> Result<(), ModelError> { |
| if *target_moniker == self.moniker { |
| return Err(ModelError::unsupported("ouch")); |
| } |
| Ok(()) |
| } |
| } |
| |
| #[async_trait] |
| impl Hook for StopErrorHook { |
| async fn on(self: Arc<Self>, event: &Event) -> Result<(), ModelError> { |
| if let Ok(EventPayload::Stopped { .. }) = event.result { |
| self.on_shutdown_instance_async(&event.target_moniker).await?; |
| } |
| Ok(()) |
| } |
| } |
| |
| let components = vec![ |
| ("root", ComponentDeclBuilder::new().add_lazy_child("a").build()), |
| ("a", ComponentDeclBuilder::new().add_eager_child("b").build()), |
| ("b", ComponentDeclBuilder::new().add_eager_child("c").add_eager_child("d").build()), |
| ("c", component_decl_with_test_runner()), |
| ("d", component_decl_with_test_runner()), |
| ]; |
| let error_hook = Arc::new(StopErrorHook::new(vec!["a:0", "b:0"].into())); |
| let test = ActionsTest::new_with_hooks("root", components, None, error_hook.hooks()).await; |
| let realm_a = test.look_up(vec!["a:0"].into()).await; |
| let realm_b = test.look_up(vec!["a:0", "b:0"].into()).await; |
| let realm_c = test.look_up(vec!["a:0", "b:0", "c:0"].into()).await; |
| let realm_d = test.look_up(vec!["a:0", "b:0", "d:0"].into()).await; |
| |
| // Component startup was eager, so they should all have an `Execution`. |
| test.model |
| .bind(&realm_a.abs_moniker, &BindReason::Eager) |
| .await |
| .expect("could not bind to a"); |
| assert!(is_executing(&realm_a).await); |
| assert!(is_executing(&realm_b).await); |
| assert!(is_executing(&realm_c).await); |
| assert!(is_executing(&realm_d).await); |
| |
| let realm_a_info = ComponentInfo::new(realm_a).await; |
| let realm_b_info = ComponentInfo::new(realm_b).await; |
| let realm_c_info = ComponentInfo::new(realm_c).await; |
| let realm_d_info = ComponentInfo::new(realm_d).await; |
| |
| // Register shutdown action on "a", and wait for it. "b"'s realm shuts down, but "b" |
| // returns an error so "a" does not. |
| ActionSet::register(realm_a_info.realm.clone(), ShutdownAction::new()) |
| .await |
| .expect_err("shutdown succeeded unexpectedly"); |
| realm_a_info.check_not_shut_down(&test.runner).await; |
| realm_b_info.check_is_shut_down(&test.runner).await; |
| realm_c_info.check_is_shut_down(&test.runner).await; |
| realm_d_info.check_is_shut_down(&test.runner).await; |
| { |
| let mut events: Vec<_> = test |
| .test_hook |
| .lifecycle() |
| .into_iter() |
| .filter(|e| match e { |
| Lifecycle::Stop(_) => true, |
| _ => false, |
| }) |
| .collect(); |
| // The leaves could be stopped in any order. |
| let mut first: Vec<_> = events.drain(0..2).collect(); |
| first.sort_unstable(); |
| let expected: Vec<_> = vec![ |
| Lifecycle::Stop(vec!["a:0", "b:0", "c:0"].into()), |
| Lifecycle::Stop(vec!["a:0", "b:0", "d:0"].into()), |
| ]; |
| assert_eq!(first, expected); |
| assert_eq!(events, vec![Lifecycle::Stop(vec!["a:0", "b:0"].into())],); |
| } |
| |
| // Register shutdown action on "a" again. "b"'s shutdown succeeds (it's a no-op), and |
| // "a" is allowed to shut down this time. |
| ActionSet::register(realm_a_info.realm.clone(), ShutdownAction::new()) |
| .await |
| .expect("shutdown failed"); |
| realm_a_info.check_is_shut_down(&test.runner).await; |
| realm_b_info.check_is_shut_down(&test.runner).await; |
| realm_c_info.check_is_shut_down(&test.runner).await; |
| realm_d_info.check_is_shut_down(&test.runner).await; |
| { |
| let mut events: Vec<_> = test |
| .test_hook |
| .lifecycle() |
| .into_iter() |
| .filter(|e| match e { |
| Lifecycle::Stop(_) => true, |
| _ => false, |
| }) |
| .collect(); |
| // The leaves could be stopped in any order. |
| let mut first: Vec<_> = events.drain(0..2).collect(); |
| first.sort_unstable(); |
| let expected: Vec<_> = vec![ |
| Lifecycle::Stop(vec!["a:0", "b:0", "c:0"].into()), |
| Lifecycle::Stop(vec!["a:0", "b:0", "d:0"].into()), |
| ]; |
| assert_eq!(first, expected); |
| assert_eq!( |
| events, |
| vec![ |
| Lifecycle::Stop(vec!["a:0", "b:0"].into()), |
| Lifecycle::Stop(vec!["a:0"].into()) |
| ] |
| ); |
| } |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn mark_deleting() { |
| let components = vec![ |
| ("root", ComponentDeclBuilder::new().add_lazy_child("a").build()), |
| ("a", component_decl_with_test_runner()), |
| ]; |
| let test = ActionsTest::new("root", components, None).await; |
| |
| // Register `mark_deleting` action, and wait for it. Component should be marked deleted. |
| let realm_root = test.look_up(vec![].into()).await; |
| ActionSet::register(realm_root.clone(), MarkDeletingAction::new("a:0".into())) |
| .await |
| .expect("mark delete failed"); |
| assert!(is_deleting(&realm_root, "a:0".into()).await); |
| { |
| let events: Vec<_> = test |
| .test_hook |
| .lifecycle() |
| .into_iter() |
| .filter(|e| match e { |
| Lifecycle::PreDestroy(_) | Lifecycle::Destroy(_) => true, |
| _ => false, |
| }) |
| .collect(); |
| assert_eq!(events, vec![Lifecycle::PreDestroy(vec!["a:0"].into())],); |
| } |
| |
| // Execute action again, same state and no new events. |
| ActionSet::register(realm_root.clone(), MarkDeletingAction::new("a:0".into())) |
| .await |
| .expect("mark delete failed"); |
| assert!(is_deleting(&realm_root, "a:0".into()).await); |
| { |
| let events: Vec<_> = test |
| .test_hook |
| .lifecycle() |
| .into_iter() |
| .filter(|e| match e { |
| Lifecycle::PreDestroy(_) | Lifecycle::Destroy(_) => true, |
| _ => false, |
| }) |
| .collect(); |
| assert_eq!(events, vec![Lifecycle::PreDestroy(vec!["a:0"].into())],); |
| } |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn mark_deleting_in_collection() { |
| let components = vec![ |
| ("root", ComponentDeclBuilder::new().add_transient_collection("coll").build()), |
| ("a", component_decl_with_test_runner()), |
| ("b", component_decl_with_test_runner()), |
| ]; |
| let test = ActionsTest::new("root", components, Some(vec![].into())).await; |
| |
| // Create dynamic instances in "coll". |
| test.create_dynamic_child("coll", "a").await; |
| test.create_dynamic_child("coll", "b").await; |
| |
| // Register `mark_deleting` action for "a" only. |
| let realm_root = test.look_up(vec![].into()).await; |
| ActionSet::register(realm_root.clone(), MarkDeletingAction::new("coll:a:1".into())) |
| .await |
| .expect("mark delete failed"); |
| assert!(is_deleting(&realm_root, "coll:a:1".into()).await); |
| assert!(!is_deleting(&realm_root, "coll:b:2".into()).await); |
| |
| // Register `mark_deleting` action for "b". |
| ActionSet::register(realm_root.clone(), MarkDeletingAction::new("coll:b:1".into())) |
| .await |
| .expect("mark delete failed"); |
| assert!(is_deleting(&realm_root, "coll:a:1".into()).await); |
| assert!(is_deleting(&realm_root, "coll:b:2".into()).await); |
| { |
| let events: Vec<_> = test |
| .test_hook |
| .lifecycle() |
| .into_iter() |
| .filter(|e| match e { |
| Lifecycle::PreDestroy(_) | Lifecycle::Destroy(_) => true, |
| _ => false, |
| }) |
| .collect(); |
| assert_eq!( |
| events, |
| vec![ |
| Lifecycle::PreDestroy(vec!["coll:a:1"].into()), |
| Lifecycle::PreDestroy(vec!["coll:b:2"].into()) |
| ], |
| ); |
| } |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn destroy_one_component() { |
| let components = vec![ |
| ("root", ComponentDeclBuilder::new().add_lazy_child("a").build()), |
| ("a", component_decl_with_test_runner()), |
| ]; |
| let test = ActionsTest::new("root", components, None).await; |
| // Bind to the component, causing it to start. This should cause the realm to have an |
| // `Execution`. |
| let realm_root = test.look_up(vec![].into()).await; |
| let realm_a = test.look_up(vec!["a:0"].into()).await; |
| test.model |
| .bind(&realm_a.abs_moniker, &BindReason::Eager) |
| .await |
| .expect("could not bind to a"); |
| assert!(is_executing(&realm_a).await); |
| |
| // Register delete child action, and wait for it. Component should be destroyed. |
| ActionSet::register(realm_root.clone(), DeleteChildAction::new("a:0".into())) |
| .await |
| .expect("destroy failed"); |
| assert!(is_destroyed(&realm_root, &realm_a).await); |
| { |
| let events: Vec<_> = test |
| .test_hook |
| .lifecycle() |
| .into_iter() |
| .filter(|e| match e { |
| Lifecycle::Stop(_) | Lifecycle::Destroy(_) => true, |
| _ => false, |
| }) |
| .collect(); |
| assert_eq!( |
| events, |
| vec![Lifecycle::Stop(vec!["a:0"].into()), Lifecycle::Destroy(vec!["a:0"].into())], |
| ); |
| } |
| |
| // Trying to bind to the component should fail because it's shut down. |
| test.model |
| .bind(&realm_a.abs_moniker, &BindReason::Eager) |
| .await |
| .expect_err("successfully bound to a after shutdown"); |
| |
| // Destroy the component again. This succeeds, but has no additional effect. |
| ActionSet::register(realm_root.clone(), DeleteChildAction::new("a:0".into())) |
| .await |
| .expect("destroy failed"); |
| assert!(is_destroyed(&realm_root, &realm_a).await); |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn destroy_collection() { |
| let components = vec![ |
| ("root", ComponentDeclBuilder::new().add_lazy_child("container").build()), |
| ("container", ComponentDeclBuilder::new().add_transient_collection("coll").build()), |
| ("a", component_decl_with_test_runner()), |
| ("b", component_decl_with_test_runner()), |
| ]; |
| let test = ActionsTest::new("root", components, Some(vec!["container:0"].into())).await; |
| |
| // Create dynamic instances in "coll". |
| test.create_dynamic_child("coll", "a").await; |
| test.create_dynamic_child("coll", "b").await; |
| |
| // Bind to the components, causing them to start. This should cause them to have an |
| // `Execution`. |
| let realm_root = test.look_up(vec![].into()).await; |
| let realm_container = test.look_up(vec!["container:0"].into()).await; |
| let realm_a = test.look_up(vec!["container:0", "coll:a:1"].into()).await; |
| let realm_b = test.look_up(vec!["container:0", "coll:b:2"].into()).await; |
| test.model |
| .bind(&realm_container.abs_moniker, &BindReason::Eager) |
| .await |
| .expect("could not bind to container"); |
| test.model |
| .bind(&realm_a.abs_moniker, &BindReason::Eager) |
| .await |
| .expect("could not bind to coll:a"); |
| test.model |
| .bind(&realm_b.abs_moniker, &BindReason::Eager) |
| .await |
| .expect("could not bind to coll:b"); |
| assert!(is_executing(&realm_container).await); |
| assert!(is_executing(&realm_a).await); |
| assert!(is_executing(&realm_b).await); |
| |
| // Register delete child action, and wait for it. Components should be destroyed. |
| let realm_container = test.look_up(vec!["container:0"].into()).await; |
| ActionSet::register(realm_root.clone(), DeleteChildAction::new("container:0".into())) |
| .await |
| .expect("destroy failed"); |
| assert!(is_destroyed(&realm_root, &realm_container).await); |
| assert!(is_destroyed(&realm_container, &realm_a).await); |
| assert!(is_destroyed(&realm_container, &realm_b).await); |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn destroy_already_shut_down() { |
| let components = vec![ |
| ("root", ComponentDeclBuilder::new().add_lazy_child("a").build()), |
| ("a", ComponentDeclBuilder::new().add_lazy_child("b").build()), |
| ("b", component_decl_with_test_runner()), |
| ]; |
| let test = ActionsTest::new("root", components, None).await; |
| let realm_root = test.look_up(vec![].into()).await; |
| let realm_a = test.look_up(vec!["a:0"].into()).await; |
| let realm_b = test.look_up(vec!["a:0", "b:0"].into()).await; |
| |
| // Register shutdown action on "a", and wait for it. This should cause all components |
| // to shut down, in bottom-up order. |
| ActionSet::register(realm_a.clone(), ShutdownAction::new()).await.expect("shutdown failed"); |
| assert!(execution_is_shut_down(&realm_a.clone()).await); |
| assert!(execution_is_shut_down(&realm_b.clone()).await); |
| |
| // Now delete child "a". This should cause all components to be destroyed. |
| ActionSet::register(realm_root.clone(), DeleteChildAction::new("a:0".into())) |
| .await |
| .expect("destroy failed"); |
| assert!(is_destroyed(&realm_root, &realm_a).await); |
| assert!(is_destroyed(&realm_a, &realm_b).await); |
| |
| // Check order of events. |
| { |
| let events: Vec<_> = test |
| .test_hook |
| .lifecycle() |
| .into_iter() |
| .filter(|e| match e { |
| Lifecycle::Stop(_) | Lifecycle::Destroy(_) => true, |
| _ => false, |
| }) |
| .collect(); |
| assert_eq!( |
| events, |
| vec![ |
| Lifecycle::Destroy(vec!["a:0", "b:0"].into()), |
| Lifecycle::Destroy(vec!["a:0"].into()), |
| ] |
| ); |
| } |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn destroy_not_resolved() { |
| let components = vec![ |
| ("root", ComponentDeclBuilder::new().add_lazy_child("a").build()), |
| ("a", ComponentDeclBuilder::new().add_lazy_child("b").build()), |
| ("b", ComponentDeclBuilder::new().add_lazy_child("c").build()), |
| ("c", component_decl_with_test_runner()), |
| ]; |
| let test = ActionsTest::new("root", components, None).await; |
| let realm_root = test.look_up(vec![].into()).await; |
| let realm_a = test.look_up(vec!["a:0"].into()).await; |
| test.model |
| .bind(&realm_a.abs_moniker, &BindReason::Eager) |
| .await |
| .expect("could not bind to a"); |
| assert!(is_executing(&realm_a).await); |
| // Get realm_b without resolving it. |
| let realm_b = { |
| let state = realm_a.lock_state().await; |
| let state = state.get_resolved().unwrap(); |
| state.get_live_child_realm(&PartialMoniker::from("b")).expect("child b not found") |
| }; |
| |
| // Register delete action on "a", and wait for it. |
| ActionSet::register(realm_root.clone(), DeleteChildAction::new("a:0".into())) |
| .await |
| .expect("destroy failed"); |
| assert!(is_destroyed(&realm_root, &realm_a).await); |
| assert!(is_unresolved(&realm_b).await); |
| |
| // Now "a" is destroyed. Expect destroy events for "a" and "b". |
| { |
| let events: Vec<_> = test |
| .test_hook |
| .lifecycle() |
| .into_iter() |
| .filter(|e| match e { |
| Lifecycle::Stop(_) | Lifecycle::Destroy(_) => true, |
| _ => false, |
| }) |
| .collect(); |
| assert_eq!( |
| events, |
| vec![ |
| Lifecycle::Stop(vec!["a:0"].into()), |
| Lifecycle::Destroy(vec!["a:0", "b:0"].into()), |
| Lifecycle::Destroy(vec!["a:0"].into()) |
| ] |
| ); |
| } |
| } |
| |
| /// Delete "a" as child of root: |
| /// |
| /// /\ |
| /// x a* |
| /// \ |
| /// b |
| /// / \ |
| /// c d |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn destroy_hierarchy() { |
| let components = vec![ |
| ("root", ComponentDeclBuilder::new().add_lazy_child("a").add_lazy_child("x").build()), |
| ("a", ComponentDeclBuilder::new().add_eager_child("b").build()), |
| ("b", ComponentDeclBuilder::new().add_eager_child("c").add_eager_child("d").build()), |
| ("c", component_decl_with_test_runner()), |
| ("d", component_decl_with_test_runner()), |
| ("x", component_decl_with_test_runner()), |
| ]; |
| let test = ActionsTest::new("root", components, None).await; |
| let realm_root = test.look_up(vec![].into()).await; |
| let realm_a = test.look_up(vec!["a:0"].into()).await; |
| let realm_b = test.look_up(vec!["a:0", "b:0"].into()).await; |
| let realm_c = test.look_up(vec!["a:0", "b:0", "c:0"].into()).await; |
| let realm_d = test.look_up(vec!["a:0", "b:0", "d:0"].into()).await; |
| let realm_x = test.look_up(vec!["x:0"].into()).await; |
| |
| // Component startup was eager, so they should all have an `Execution`. |
| test.model |
| .bind(&realm_a.abs_moniker, &BindReason::Eager) |
| .await |
| .expect("could not bind to a"); |
| test.model |
| .bind(&realm_x.abs_moniker, &BindReason::Eager) |
| .await |
| .expect("could not bind to x"); |
| assert!(is_executing(&realm_a).await); |
| assert!(is_executing(&realm_b).await); |
| assert!(is_executing(&realm_c).await); |
| assert!(is_executing(&realm_d).await); |
| assert!(is_executing(&realm_x).await); |
| |
| // Register destroy action on "a", and wait for it. This should cause all components |
| // in "a"'s realm to be shut down and destroyed, in bottom-up order, but "x" is still |
| // running. |
| ActionSet::register(realm_root.clone(), DeleteChildAction::new("a:0".into())) |
| .await |
| .expect("delete child failed"); |
| assert!(is_destroyed(&realm_root, &realm_a).await); |
| assert!(is_destroyed(&realm_a, &realm_b).await); |
| assert!(is_destroyed(&realm_b, &realm_c).await); |
| assert!(is_destroyed(&realm_b, &realm_d).await); |
| assert!(is_executing(&realm_x).await); |
| { |
| // Expect only "x" as child of root. |
| let state = realm_root.lock_state().await; |
| let children: Vec<_> = state |
| .get_resolved() |
| .unwrap() |
| .all_child_realms() |
| .keys() |
| .map(|m| m.clone()) |
| .collect(); |
| assert_eq!(children, vec!["x:0".into()]); |
| } |
| { |
| let mut events: Vec<_> = test |
| .test_hook |
| .lifecycle() |
| .into_iter() |
| .filter(|e| match e { |
| Lifecycle::Stop(_) | Lifecycle::Destroy(_) => true, |
| _ => false, |
| }) |
| .collect(); |
| |
| // The leaves could be stopped in any order. |
| let mut first: Vec<_> = events.drain(0..2).collect(); |
| first.sort_unstable(); |
| assert_eq!( |
| first, |
| vec![ |
| Lifecycle::Stop(vec!["a:0", "b:0", "c:0"].into()), |
| Lifecycle::Stop(vec!["a:0", "b:0", "d:0"].into()) |
| ] |
| ); |
| let next: Vec<_> = events.drain(0..2).collect(); |
| assert_eq!( |
| next, |
| vec![ |
| Lifecycle::Stop(vec!["a:0", "b:0"].into()), |
| Lifecycle::Stop(vec!["a:0"].into()) |
| ] |
| ); |
| |
| // The leaves could be destroyed in any order. |
| let mut first: Vec<_> = events.drain(0..2).collect(); |
| first.sort_unstable(); |
| assert_eq!( |
| first, |
| vec![ |
| Lifecycle::Destroy(vec!["a:0", "b:0", "c:0"].into()), |
| Lifecycle::Destroy(vec!["a:0", "b:0", "d:0"].into()) |
| ] |
| ); |
| assert_eq!( |
| events, |
| vec![ |
| Lifecycle::Destroy(vec!["a:0", "b:0"].into()), |
| Lifecycle::Destroy(vec!["a:0"].into()) |
| ] |
| ); |
| } |
| } |
| |
| /// Destroy `b`: |
| /// a |
| /// \ |
| /// b |
| /// \ |
| /// b |
| /// \ |
| /// ... |
| /// |
| /// `b` is a child of itself, but destruction should still be able to complete. |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn destroy_self_referential() { |
| let components = vec![ |
| ("root", ComponentDeclBuilder::new().add_lazy_child("a").build()), |
| ("a", ComponentDeclBuilder::new().add_lazy_child("b").build()), |
| ("b", ComponentDeclBuilder::new().add_lazy_child("b").build()), |
| ]; |
| let test = ActionsTest::new("root", components, None).await; |
| let realm_root = test.look_up(vec![].into()).await; |
| let realm_a = test.look_up(vec!["a:0"].into()).await; |
| let realm_b = test.look_up(vec!["a:0", "b:0"].into()).await; |
| let realm_b2 = test.look_up(vec!["a:0", "b:0", "b:0"].into()).await; |
| |
| // Bind to second `b`. |
| test.model |
| .bind(&realm_a.abs_moniker, &BindReason::Eager) |
| .await |
| .expect("could not bind to b2"); |
| test.model |
| .bind(&realm_b.abs_moniker, &BindReason::Eager) |
| .await |
| .expect("could not bind to b2"); |
| test.model |
| .bind(&realm_b2.abs_moniker, &BindReason::Eager) |
| .await |
| .expect("could not bind to b2"); |
| assert!(is_executing(&realm_a).await); |
| assert!(is_executing(&realm_b).await); |
| assert!(is_executing(&realm_b2).await); |
| |
| // Register destroy action on "a", and wait for it. This should cause all components |
| // that were started to be destroyed, in bottom-up order. |
| ActionSet::register(realm_root.clone(), DeleteChildAction::new("a:0".into())) |
| .await |
| .expect("delete child failed"); |
| assert!(is_destroyed(&realm_root, &realm_a).await); |
| assert!(is_destroyed(&realm_a, &realm_b).await); |
| assert!(is_destroyed(&realm_b, &realm_b2).await); |
| { |
| let state = realm_root.lock_state().await; |
| let children: Vec<_> = state |
| .get_resolved() |
| .unwrap() |
| .all_child_realms() |
| .keys() |
| .map(|m| m.clone()) |
| .collect(); |
| assert_eq!(children, Vec::<ChildMoniker>::new()); |
| } |
| { |
| let events: Vec<_> = test |
| .test_hook |
| .lifecycle() |
| .into_iter() |
| .filter(|e| match e { |
| Lifecycle::Stop(_) | Lifecycle::Destroy(_) => true, |
| _ => false, |
| }) |
| .collect(); |
| assert_eq!( |
| events, |
| vec![ |
| Lifecycle::Stop(vec!["a:0", "b:0", "b:0"].into()), |
| Lifecycle::Stop(vec!["a:0", "b:0"].into()), |
| Lifecycle::Stop(vec!["a:0"].into()), |
| // This component instance is never resolved but we still invoke the Destroy |
| // hook on it. |
| Lifecycle::Destroy(vec!["a:0", "b:0", "b:0", "b:0"].into()), |
| Lifecycle::Destroy(vec!["a:0", "b:0", "b:0"].into()), |
| Lifecycle::Destroy(vec!["a:0", "b:0"].into()), |
| Lifecycle::Destroy(vec!["a:0"].into()) |
| ] |
| ); |
| } |
| } |
| |
| /// Destroy `a`: |
| /// |
| /// a* |
| /// \ |
| /// b |
| /// / \ |
| /// c d |
| /// |
| /// `a` fails to destroy the first time, but succeeds the second time. |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn destroy_error() { |
| struct DestroyErrorHook { |
| moniker: AbsoluteMoniker, |
| } |
| |
| impl DestroyErrorHook { |
| fn new(moniker: AbsoluteMoniker) -> Self { |
| Self { moniker } |
| } |
| |
| fn hooks(self: &Arc<Self>) -> Vec<HooksRegistration> { |
| vec![HooksRegistration::new( |
| "DestroyErrorHook", |
| vec![EventType::Destroyed], |
| Arc::downgrade(self) as Weak<dyn Hook>, |
| )] |
| } |
| |
| async fn on_destroyed_async( |
| &self, |
| target_moniker: &AbsoluteMoniker, |
| ) -> Result<(), ModelError> { |
| if *target_moniker == self.moniker { |
| return Err(ModelError::unsupported("ouch")); |
| } |
| Ok(()) |
| } |
| } |
| |
| #[async_trait] |
| impl Hook for DestroyErrorHook { |
| async fn on(self: Arc<Self>, event: &Event) -> Result<(), ModelError> { |
| if let Ok(EventPayload::Destroyed) = event.result { |
| self.on_destroyed_async(&event.target_moniker).await?; |
| } |
| Ok(()) |
| } |
| } |
| |
| let components = vec![ |
| ("root", ComponentDeclBuilder::new().add_lazy_child("a").build()), |
| ("a", ComponentDeclBuilder::new().add_eager_child("b").build()), |
| ("b", ComponentDeclBuilder::new().add_eager_child("c").add_eager_child("d").build()), |
| ("c", component_decl_with_test_runner()), |
| ("d", component_decl_with_test_runner()), |
| ]; |
| // The destroy hook is invoked just after the component instance is removed from the |
| // list of children. Therefore, to cause destruction of `a` to fail, fail removal of |
| // `/a/b`. |
| let error_hook = Arc::new(DestroyErrorHook::new(vec!["a:0", "b:0"].into())); |
| let test = ActionsTest::new_with_hooks("root", components, None, error_hook.hooks()).await; |
| let realm_root = test.look_up(vec![].into()).await; |
| let realm_a = test.look_up(vec!["a:0"].into()).await; |
| let realm_b = test.look_up(vec!["a:0", "b:0"].into()).await; |
| let realm_c = test.look_up(vec!["a:0", "b:0", "c:0"].into()).await; |
| let realm_d = test.look_up(vec!["a:0", "b:0", "d:0"].into()).await; |
| |
| // Component startup was eager, so they should all have an `Execution`. |
| test.model |
| .bind(&realm_a.abs_moniker, &BindReason::Eager) |
| .await |
| .expect("could not bind to a"); |
| assert!(is_executing(&realm_a).await); |
| assert!(is_executing(&realm_b).await); |
| assert!(is_executing(&realm_c).await); |
| assert!(is_executing(&realm_d).await); |
| |
| // Register delete action on "a", and wait for it. "b"'s realm is deleted, but "b" |
| // returns an error so the delete action on "a" does not succeed. |
| ActionSet::register(realm_root.clone(), DeleteChildAction::new("a:0".into())) |
| .await |
| .expect_err("destroy succeeded unexpectedly"); |
| assert!(has_child(&realm_root, "a:0").await); |
| assert!(is_destroyed(&realm_a, &realm_b).await); |
| assert!(is_destroyed(&realm_b, &realm_c).await); |
| assert!(is_destroyed(&realm_b, &realm_d).await); |
| { |
| let mut events: Vec<_> = test |
| .test_hook |
| .lifecycle() |
| .into_iter() |
| .filter(|e| match e { |
| Lifecycle::Destroy(_) => true, |
| _ => false, |
| }) |
| .collect(); |
| // The leaves could be stopped in any order. |
| let mut first: Vec<_> = events.drain(0..2).collect(); |
| first.sort_unstable(); |
| let expected: Vec<_> = vec![ |
| Lifecycle::Destroy(vec!["a:0", "b:0", "c:0"].into()), |
| Lifecycle::Destroy(vec!["a:0", "b:0", "d:0"].into()), |
| ]; |
| assert_eq!(first, expected); |
| assert_eq!(events, vec![Lifecycle::Destroy(vec!["a:0", "b:0"].into())]); |
| } |
| |
| // Register destroy action on "a:0" again. "b:0"'s delete succeeds, and "a:0" is deleted this |
| // time. |
| ActionSet::register(realm_root.clone(), DeleteChildAction::new("a:0".into())) |
| .await |
| .expect("destroy failed"); |
| assert!(!has_child(&realm_root, "a:0").await); |
| assert!(is_destroyed(&realm_a, &realm_b).await); |
| assert!(is_destroyed(&realm_b, &realm_c).await); |
| assert!(is_destroyed(&realm_b, &realm_d).await); |
| { |
| let mut events: Vec<_> = test |
| .test_hook |
| .lifecycle() |
| .into_iter() |
| .filter(|e| match e { |
| Lifecycle::Destroy(_) => true, |
| _ => false, |
| }) |
| .collect(); |
| // The leaves could be stopped in any order. |
| let mut first: Vec<_> = events.drain(0..2).collect(); |
| first.sort_unstable(); |
| let expected: Vec<_> = vec![ |
| Lifecycle::Destroy(vec!["a:0", "b:0", "c:0"].into()), |
| Lifecycle::Destroy(vec!["a:0", "b:0", "d:0"].into()), |
| ]; |
| assert_eq!(first, expected); |
| assert_eq!( |
| events, |
| vec![ |
| Lifecycle::Destroy(vec!["a:0", "b:0"].into()), |
| Lifecycle::Destroy(vec!["a:0"].into()) |
| ] |
| ); |
| } |
| } |
| |
| async fn is_executing(realm: &Realm) -> bool { |
| realm.lock_execution().await.runtime.is_some() |
| } |
| |
| async fn is_deleting(realm: &Realm, moniker: ChildMoniker) -> bool { |
| let partial = moniker.to_partial(); |
| let state = realm.lock_state().await; |
| let state = state.get_resolved().unwrap(); |
| state.get_live_child_realm(&partial).is_none() |
| && state.get_child_instance(&moniker).is_some() |
| } |
| |
| /// Verifies that a child realm is deleted by checking its RealmState |
| /// and verifying that it does not exist in the RealmState of its parent |
| async fn is_destroyed(parent_realm: &Realm, child_realm: &Realm) -> bool { |
| let child_moniker = child_realm.abs_moniker.leaf().expect("Root realm cannot be destroyed"); |
| let partial_moniker = child_moniker.to_partial(); |
| |
| // Verify the parent-child relationship |
| assert_eq!(parent_realm.abs_moniker.child(child_moniker.clone()), child_realm.abs_moniker); |
| |
| let parent_state = parent_realm.lock_state().await; |
| let parent_state = parent_state.get_resolved().unwrap(); |
| |
| let child_state = child_realm.lock_state().await; |
| let child_state = child_state.get_resolved().unwrap(); |
| let child_execution = child_realm.lock_execution().await; |
| |
| let found_partial_moniker = parent_state |
| .live_child_realms() |
| .find(|(curr_partial_moniker, _)| **curr_partial_moniker == partial_moniker); |
| let found_child_moniker = parent_state.all_child_realms().get(child_moniker); |
| |
| found_partial_moniker.is_none() |
| && found_child_moniker.is_none() |
| && child_execution.runtime.is_none() |
| && child_execution.is_shut_down() |
| && child_state.all_child_realms().is_empty() |
| } |
| |
| async fn is_unresolved(realm: &Realm) -> bool { |
| let state = realm.lock_state().await; |
| let execution = realm.lock_execution().await; |
| execution.runtime.is_none() && state.get_resolved().is_none() |
| } |
| } |