| // 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. |
| |
| use { |
| crate::model::{ |
| actions::{Action, ActionKey, ActionSet}, |
| component::{ComponentInstance, InstanceState, ResolvedInstanceState}, |
| error::ModelError, |
| }, |
| async_trait::async_trait, |
| cm_rust::{ |
| CapabilityDecl, CapabilityName, ComponentDecl, DependencyType, OfferDecl, OfferSource, |
| OfferTarget, RegistrationSource, StorageDirectorySource, |
| }, |
| futures::future::select_all, |
| maplit::hashset, |
| moniker::{ChildMoniker, PartialMoniker}, |
| std::collections::{HashMap, HashSet}, |
| std::fmt, |
| std::sync::Arc, |
| }; |
| |
| /// Shuts down all component instances in this component (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, component: &Arc<ComponentInstance>) -> Self::Output { |
| do_shutdown(component).await |
| } |
| fn key(&self) -> ActionKey { |
| ActionKey::Shutdown |
| } |
| } |
| |
| /// A DependencyNode represents a provider or user of a capability. This |
| /// may be either a component or a component collection. |
| #[derive(Debug, PartialEq, Eq, Hash, Clone, PartialOrd, Ord)] |
| pub enum DependencyNode { |
| Child(String), |
| Collection(String), |
| } |
| |
| /// Examines a group of StorageDecls looking for one whose name matches the |
| /// String passed in and whose source is a child. `None` is returned if either |
| /// no declaration has the specified name or the declaration represents an |
| /// offer from Self or Parent. |
| fn find_storage_provider( |
| capabilities: &Vec<CapabilityDecl>, |
| name: &CapabilityName, |
| ) -> Option<String> { |
| for decl in capabilities { |
| match decl { |
| CapabilityDecl::Storage(decl) if &decl.name == name => match &decl.source { |
| StorageDirectorySource::Child(child) => { |
| return Some(child.to_string()); |
| } |
| StorageDirectorySource::Self_ | StorageDirectorySource::Parent => { |
| return None; |
| } |
| }, |
| _ => {} |
| } |
| } |
| None |
| } |
| |
| async fn shutdown_component(child: ShutdownInfo) -> Result<ChildMoniker, ModelError> { |
| ActionSet::register(child.component, ShutdownAction::new()).await?; |
| Ok(child.moniker.clone()) |
| } |
| |
| /// Structure which holds bidirectional capability maps used during the |
| /// shutdown process. |
| struct ShutdownJob { |
| /// A map from users of capabilities to the components that provide those |
| /// capabilities |
| target_to_sources: HashMap<ChildMoniker, Vec<ChildMoniker>>, |
| /// A map from providers of capabilities to those components which use the |
| /// capabilities |
| source_to_targets: HashMap<ChildMoniker, ShutdownInfo>, |
| } |
| |
| /// ShutdownJob encapsulates the logic and state require to shutdown a component. |
| impl ShutdownJob { |
| /// Creates a new ShutdownJob by examining the Component's declaration and |
| /// runtime state to build up the necessary data structures to stop |
| /// components in the component in dependency order. |
| pub async fn new(state: &ResolvedInstanceState) -> ShutdownJob { |
| // `children` represents the dependency relationships between the |
| // children as expressed in the component's declaration. |
| // This representation must be reconciled with the runtime state of the |
| // component. This means mapping children in the declaration with the one |
| // or more children that may exist in collections and one or more |
| // instances with a matching PartialMoniker that may exist. |
| let children = process_component_dependencies(state.decl()); |
| let mut source_to_targets: HashMap<ChildMoniker, ShutdownInfo> = HashMap::new(); |
| |
| for (child_name, sibling_deps) in children { |
| let deps = get_child_monikers(&sibling_deps, state); |
| |
| let singleton_child_set = hashset![child_name]; |
| // The shutdown target may be a collection, if so this will expand |
| // the collection out into a list of all its members, otherwise it |
| // contains a single component. |
| let matching_children: Vec<_> = |
| get_child_monikers(&singleton_child_set, state).into_iter().collect(); |
| for child in matching_children { |
| let component = |
| state.get_child(&child).expect("component not found in children").clone(); |
| |
| source_to_targets.insert( |
| child.clone(), |
| ShutdownInfo { moniker: child, dependents: deps.clone(), component }, |
| ); |
| } |
| } |
| |
| let mut target_to_sources: HashMap<ChildMoniker, Vec<ChildMoniker>> = HashMap::new(); |
| // Look at each of the children |
| for provider in source_to_targets.values() { |
| // All listed siblings are ones that depend on this child |
| // and all those siblings must stop before this one |
| for consumer in &provider.dependents { |
| // Make or update a map entry for the consumer that points to the |
| // list of siblings that offer it capabilities |
| target_to_sources |
| .entry(consumer.clone()) |
| .or_insert(vec![]) |
| .push(provider.moniker.clone()); |
| } |
| } |
| let new_job = ShutdownJob { source_to_targets, target_to_sources }; |
| return new_job; |
| } |
| |
| /// Perform shutdown of the Component that was used to create this ShutdownJob A Component must |
| /// wait to shut down until all its children are shut down. The shutdown procedure looks at |
| /// the children, if any, and determines the dependency relationships of the children. |
| pub async fn execute(&mut self) -> Result<(), ModelError> { |
| // Relationship maps are maintained to track dependencies. A map is |
| // maintained both from a Component to its dependents and from a Component to |
| // that Component's dependencies. With this dependency tracking, the |
| // children of the Component can be shut down progressively in dependency |
| // order. |
| // |
| // The progressive shutdown of Component is performed in this order: |
| // Note: These steps continue until the shutdown process is no longer |
| // asynchronously waiting for any shut downs to complete. |
| // * Identify the one or more Component that have no dependents |
| // * A shutdown action is set to the identified components. During the |
| // shut down process, the result of the process is received |
| // asynchronously. |
| // * After a Component is shut down, the Component are removed from the list |
| // of dependents of the Component on which they had a dependency. |
| // * The list of Component is checked again to see which Component have no |
| // remaining dependents. |
| |
| // Look for any children that have no dependents |
| let mut stop_targets = vec![]; |
| |
| for moniker in self.source_to_targets.keys().map(|key| key.clone()).collect::<Vec<_>>() { |
| let no_dependents = { |
| let info = self.source_to_targets.get(&moniker).expect("key disappeared from map"); |
| info.dependents.is_empty() |
| }; |
| if no_dependents { |
| stop_targets.push( |
| self.source_to_targets.remove(&moniker).expect("key disappeared from map"), |
| ); |
| } |
| } |
| |
| let mut futs = vec![]; |
| // Continue while we have new stop targets or unfinished futures |
| while !stop_targets.is_empty() || !futs.is_empty() { |
| for target in stop_targets.drain(..) { |
| futs.push(Box::pin(shutdown_component(target))); |
| } |
| |
| let (moniker, _, remaining) = select_all(futs).await; |
| futs = remaining; |
| |
| let moniker = moniker?; |
| |
| // Look up the dependencies of the component that stopped |
| match self.target_to_sources.remove(&moniker) { |
| Some(vec) => { |
| for dep_moniker in vec { |
| let ready_to_stop = { |
| if let Some(child) = self.source_to_targets.get_mut(&dep_moniker) { |
| child.dependents.remove(&moniker); |
| // Have all of this components dependents stopped? |
| child.dependents.is_empty() |
| } else { |
| // The component that provided a capability to |
| // the stopped component doesn't exist or |
| // somehow already stopped. This is unexpected. |
| panic!( |
| "The component '{}' appears to have stopped before its \ |
| dependency '{}'", |
| moniker, dep_moniker |
| ); |
| } |
| }; |
| |
| // This components had zero remaining dependents |
| if ready_to_stop { |
| stop_targets.push( |
| self.source_to_targets |
| .remove(&dep_moniker) |
| .expect("A key that was just available has disappeared."), |
| ); |
| } |
| } |
| } |
| None => { |
| // Oh well, component didn't have any dependencies |
| } |
| } |
| } |
| |
| // We should have stopped all children, if not probably there is a |
| // dependency cycle |
| if !self.source_to_targets.is_empty() { |
| panic!( |
| "Something failed, all children should have been removed! {:?}", |
| self.source_to_targets |
| ); |
| } |
| Ok(()) |
| } |
| } |
| |
| async fn do_shutdown(component: &Arc<ComponentInstance>) -> Result<(), ModelError> { |
| { |
| let state = component.lock_state().await; |
| { |
| let exec_state = component.lock_execution().await; |
| if exec_state.is_shut_down() { |
| return Ok(()); |
| } |
| } |
| match *state { |
| InstanceState::Resolved(ref s) => { |
| let mut shutdown_job = ShutdownJob::new(s).await; |
| drop(state); |
| Box::pin(shutdown_job.execute()).await?; |
| } |
| InstanceState::New | InstanceState::Discovered | InstanceState::Destroyed => {} |
| } |
| } |
| // Now that all children have shut down, shut down the parent. |
| // TODO: Put the parent in a "shutting down" state so that if it creates new instances |
| // after this point, they are created in a shut down state. |
| component.stop_instance(true).await?; |
| |
| Ok(()) |
| } |
| |
| /// Used to track information during the shutdown process. The dependents |
| /// are all the component which must stop before the component represented |
| /// by this struct. |
| struct ShutdownInfo { |
| // TODO(jmatt) reduce visibility of fields |
| /// The identifier for this component |
| pub moniker: ChildMoniker, |
| /// The components that this component offers capabilities to |
| pub dependents: HashSet<ChildMoniker>, |
| pub component: Arc<ComponentInstance>, |
| } |
| |
| impl fmt::Debug for ShutdownInfo { |
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| write!(f, "moniker: '{:?}'", self.moniker) |
| } |
| } |
| |
| /// Given a set of DependencyNodes, find all the ChildMonikers in the supplied |
| /// Component that match. |
| fn get_child_monikers( |
| child_names: &HashSet<DependencyNode>, |
| component_state: &ResolvedInstanceState, |
| ) -> HashSet<ChildMoniker> { |
| let mut deps: HashSet<ChildMoniker> = HashSet::new(); |
| let components = component_state.all_children(); |
| |
| for child in child_names { |
| match child { |
| DependencyNode::Child(name) => { |
| let dep_moniker = PartialMoniker::new(name.to_string(), None); |
| let matching_children = component_state.get_all_child_monikers(&dep_moniker); |
| for m in matching_children { |
| deps.insert(m); |
| } |
| } |
| DependencyNode::Collection(name) => { |
| for moniker in components.keys() { |
| match moniker.collection() { |
| Some(m) => { |
| if m == name { |
| deps.insert(moniker.clone()); |
| } |
| } |
| None => {} |
| } |
| } |
| } |
| } |
| } |
| deps |
| } |
| |
| /// Maps a dependency node (child or collection) to the nodes that depend on it. |
| pub type DependencyMap = HashMap<DependencyNode, HashSet<DependencyNode>>; |
| |
| /// For a given ComponentDecl, parse it, identify capability dependencies |
| /// between children and collections in the ComponentDecl. A map is returned |
| /// which maps from a child to a set of other children to which that child |
| /// provides capabilities. The siblings to which the child offers capabilities |
| /// must be shut down before that child. This function panics if there is a |
| /// capability routing where either the source or target is not present in this |
| /// ComponentDecl. Panics are not expected because ComponentDecls should be |
| /// validated before this function is called. |
| pub fn process_component_dependencies(decl: &ComponentDecl) -> DependencyMap { |
| let mut dependency_map: DependencyMap = decl |
| .children |
| .iter() |
| .map(|c| (DependencyNode::Child(c.name.clone()), HashSet::new())) |
| .collect(); |
| dependency_map.extend( |
| decl.collections |
| .iter() |
| .map(|c| (DependencyNode::Collection(c.name.clone()), HashSet::new())), |
| ); |
| |
| get_dependencies_from_offers(decl, &mut dependency_map); |
| get_dependencies_from_environments(decl, &mut dependency_map); |
| dependency_map |
| } |
| |
| /// Loops through all the offer declarations to determine which siblings |
| /// provide capabilities to other siblings. |
| fn get_dependencies_from_offers(decl: &ComponentDecl, dependency_map: &mut DependencyMap) { |
| for dep in &decl.offers { |
| // Identify the source and target of the offer. We only care about |
| // dependencies where the provider of the dependency is another child, |
| // otherwise the capability comes from the parent or component manager |
| // itself in which case the relationship is not relevant for ordering |
| // here. |
| let source_target_pairs = match dep { |
| OfferDecl::Protocol(svc_offer) => { |
| if svc_offer.dependency_type == DependencyType::WeakForMigration { |
| // weak dependencies are ignored by this algorithm, because weak dependencies |
| // can be broken arbitrarily. |
| continue; |
| } |
| match &svc_offer.source { |
| OfferSource::Child(source) => match &svc_offer.target { |
| OfferTarget::Child(target) => vec![( |
| DependencyNode::Child(source.clone()), |
| DependencyNode::Child(target.clone()), |
| )], |
| OfferTarget::Collection(target) => vec![( |
| DependencyNode::Child(source.clone()), |
| DependencyNode::Collection(target.clone()), |
| )], |
| }, |
| _ => { |
| // Capabilities offered by the parent, routed in from the component, or |
| // provided by the framework (based on some other capability) are not |
| // relevant. |
| continue; |
| } |
| } |
| } |
| OfferDecl::Service(svc_offers) => { |
| let mut pairs = vec![]; |
| for svc_offer in &svc_offers.sources { |
| match &svc_offer.source { |
| OfferSource::Child(source) => match &svc_offers.target { |
| OfferTarget::Child(target) => pairs.push(( |
| DependencyNode::Child(source.clone()), |
| DependencyNode::Child(target.clone()), |
| )), |
| OfferTarget::Collection(target) => pairs.push(( |
| DependencyNode::Child(source.clone()), |
| DependencyNode::Collection(target.clone()), |
| )), |
| }, |
| _ => { |
| // Capabilities offered by the parent, routed in from the component, or |
| // provided by the framework (based on some other capability) are not |
| // relevant. |
| continue; |
| } |
| } |
| } |
| pairs |
| } |
| OfferDecl::Directory(dir_offer) => { |
| if dir_offer.dependency_type == DependencyType::WeakForMigration { |
| // weak dependencies are ignored by this algorithm, because weak dependencies |
| // can be broken arbitrarily. |
| continue; |
| } |
| match &dir_offer.source { |
| OfferSource::Child(source) => match &dir_offer.target { |
| OfferTarget::Child(target) => vec![( |
| DependencyNode::Child(source.clone()), |
| DependencyNode::Child(target.clone()), |
| )], |
| OfferTarget::Collection(target) => vec![( |
| DependencyNode::Child(source.clone()), |
| DependencyNode::Collection(target.clone()), |
| )], |
| }, |
| _ => { |
| // Capabilities offered by the parent or routed in from |
| // the component are not relevant. |
| continue; |
| } |
| } |
| } |
| OfferDecl::Storage(s) => { |
| match &s.source { |
| OfferSource::Self_ => { |
| match find_storage_provider(&decl.capabilities, &s.source_name) { |
| Some(storage_source) => match &s.target { |
| OfferTarget::Child(target) => vec![( |
| DependencyNode::Child(storage_source.clone()), |
| DependencyNode::Child(target.clone()), |
| )], |
| OfferTarget::Collection(target) => vec![( |
| DependencyNode::Child(storage_source.clone()), |
| DependencyNode::Collection(target.clone()), |
| )], |
| }, |
| None => { |
| // The storage offer is not from a child, so it |
| // can be ignored. |
| continue; |
| } |
| } |
| } |
| _ => { |
| // Capabilities coming from the parent aren't tracked. |
| continue; |
| } |
| } |
| } |
| OfferDecl::Runner(runner_offer) => { |
| match &runner_offer.source { |
| OfferSource::Child(source) => match &runner_offer.target { |
| OfferTarget::Child(target) => vec![( |
| DependencyNode::Child(source.clone()), |
| DependencyNode::Child(target.clone()), |
| )], |
| OfferTarget::Collection(target) => vec![( |
| DependencyNode::Child(source.clone()), |
| DependencyNode::Collection(target.clone()), |
| )], |
| }, |
| _ => { |
| // Capabilities coming from the parent aren't tracked. |
| continue; |
| } |
| } |
| } |
| OfferDecl::Resolver(resolver_offer) => { |
| match &resolver_offer.source { |
| OfferSource::Child(source) => match &resolver_offer.target { |
| OfferTarget::Child(target) => vec![( |
| DependencyNode::Child(source.clone()), |
| DependencyNode::Child(target.clone()), |
| )], |
| OfferTarget::Collection(target) => vec![( |
| DependencyNode::Child(source.clone()), |
| DependencyNode::Collection(target.clone()), |
| )], |
| }, |
| _ => { |
| // Capabilities coming from the parent aren't tracked. |
| continue; |
| } |
| } |
| } |
| OfferDecl::Event(_) => { |
| // Events aren't tracked as dependencies for shutdown. |
| continue; |
| } |
| }; |
| |
| for (capability_provider, capability_target) in source_target_pairs { |
| if !dependency_map.contains_key(&capability_target) { |
| panic!( |
| "This capability routing seems invalid, the target \ |
| does not exist in this component. Source: {:?} Target: {:?}", |
| capability_provider, capability_target, |
| ); |
| } |
| |
| let sibling_deps = dependency_map.get_mut(&capability_provider).expect(&format!( |
| "This capability routing seems invalid, the source \ |
| does not exist in this component. Source: {:?} Target: {:?}", |
| capability_provider, capability_target, |
| )); |
| sibling_deps.insert(capability_target); |
| } |
| } |
| } |
| |
| /// Loops through all the child and collection declarations to determine what siblings provide |
| /// capabilities to other siblings through an environment. |
| fn get_dependencies_from_environments(decl: &ComponentDecl, dependency_map: &mut DependencyMap) { |
| let mut env_source_children = HashMap::new(); |
| for env in &decl.environments { |
| env_source_children.insert(&env.name, vec![]); |
| for runner in &env.runners { |
| if let RegistrationSource::Child(source_child) = &runner.source { |
| env_source_children.get_mut(&env.name).unwrap().push(source_child); |
| } |
| } |
| } |
| |
| for dest_child in &decl.children { |
| if let Some(env_name) = dest_child.environment.as_ref() { |
| for source_child in env_source_children.get(env_name).expect(&format!( |
| "environment `{}` from child `{}` is not a valid environment", |
| env_name, dest_child.name, |
| )) { |
| dependency_map |
| .entry(DependencyNode::Child((*source_child).clone())) |
| .or_insert(HashSet::new()) |
| .insert(DependencyNode::Child(dest_child.name.clone())); |
| } |
| } |
| } |
| for dest_collection in &decl.collections { |
| if let Some(env_name) = dest_collection.environment.as_ref() { |
| for source_child in env_source_children.get(env_name).expect(&format!( |
| "environment `{}` from collection `{}` is not a valid environment", |
| env_name, dest_collection.name, |
| )) { |
| dependency_map |
| .entry(DependencyNode::Child((*source_child).clone())) |
| .or_insert(HashSet::new()) |
| .insert(DependencyNode::Collection(dest_collection.name.clone())); |
| } |
| } |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use { |
| super::*, |
| crate::model::{ |
| actions::{ |
| test_utils::{is_executing, is_unresolved}, |
| StopAction, |
| }, |
| binding::Binder, |
| component::BindReason, |
| hooks::{self, EventPayload, EventType, Hook, HooksRegistration}, |
| testing::{ |
| test_helpers::{ |
| component_decl_with_test_runner, default_component_decl, |
| execution_is_shut_down, has_child, ActionsTest, ChildDeclBuilder, |
| CollectionDeclBuilder, ComponentDeclBuilder, ComponentInfo, |
| EnvironmentDeclBuilder, |
| }, |
| test_hook::Lifecycle, |
| }, |
| }, |
| async_trait::async_trait, |
| cm_rust::{ |
| CapabilityName, CapabilityPath, ChildDecl, DependencyType, ExposeDecl, |
| ExposeProtocolDecl, ExposeSource, ExposeTarget, OfferDecl, OfferProtocolDecl, |
| OfferResolverDecl, OfferSource, OfferTarget, ProtocolDecl, UseDecl, UseProtocolDecl, |
| UseSource, |
| }, |
| fidl_fuchsia_sys2 as fsys, |
| moniker::{AbsoluteMoniker, PartialMoniker}, |
| std::collections::HashMap, |
| std::{convert::TryFrom, sync::Weak}, |
| }; |
| |
| // TODO(jmatt) Add tests for all capability types |
| |
| /// Validates that actual looks like expected and panics if they don't. |
| /// `expected` must be sorted and so must the second member of each |
| /// tuple in the vec. |
| fn validate_results( |
| expected: Vec<(DependencyNode, Vec<DependencyNode>)>, |
| mut actual: HashMap<DependencyNode, HashSet<DependencyNode>>, |
| ) { |
| let mut actual_sorted: Vec<(DependencyNode, Vec<DependencyNode>)> = actual |
| .drain() |
| .map(|(k, v)| { |
| let mut new_vec = Vec::new(); |
| new_vec.extend(v.into_iter()); |
| new_vec.sort_unstable(); |
| (k, new_vec) |
| }) |
| .collect(); |
| actual_sorted.sort_unstable(); |
| assert_eq!(expected, actual_sorted); |
| } |
| |
| #[test] |
| fn test_service_from_parent() { |
| let decl = ComponentDecl { |
| offers: vec![OfferDecl::Protocol(OfferProtocolDecl { |
| source: OfferSource::Self_, |
| source_name: "serviceParent".into(), |
| target_name: "serviceParent".into(), |
| target: OfferTarget::Child("childA".to_string()), |
| dependency_type: DependencyType::Strong, |
| })], |
| children: vec![ChildDecl { |
| name: "childA".to_string(), |
| url: "ignored:///child".to_string(), |
| startup: fsys::StartupMode::Lazy, |
| environment: None, |
| }], |
| ..default_component_decl() |
| }; |
| |
| let mut expected: Vec<(DependencyNode, Vec<DependencyNode>)> = Vec::new(); |
| expected.push((DependencyNode::Child("childA".to_string()), vec![])); |
| validate_results(expected, process_component_dependencies(&decl)); |
| } |
| |
| #[test] |
| fn test_weak_service_from_parent() { |
| let decl = ComponentDecl { |
| offers: vec![OfferDecl::Protocol(OfferProtocolDecl { |
| source: OfferSource::Self_, |
| source_name: "serviceParent".into(), |
| target_name: "serviceParent".into(), |
| target: OfferTarget::Child("childA".to_string()), |
| dependency_type: DependencyType::WeakForMigration, |
| })], |
| children: vec![ChildDecl { |
| name: "childA".to_string(), |
| url: "ignored:///child".to_string(), |
| startup: fsys::StartupMode::Lazy, |
| environment: None, |
| }], |
| ..default_component_decl() |
| }; |
| |
| let mut expected: Vec<(DependencyNode, Vec<DependencyNode>)> = Vec::new(); |
| expected.push((DependencyNode::Child("childA".to_string()), vec![])); |
| validate_results(expected, process_component_dependencies(&decl)); |
| } |
| |
| #[test] |
| fn test_service_from_child() { |
| let decl = ComponentDecl { |
| exposes: vec![ExposeDecl::Protocol(ExposeProtocolDecl { |
| target: ExposeTarget::Parent, |
| source_name: "serviceFromChild".into(), |
| target_name: "serviceFromChild".into(), |
| source: ExposeSource::Child("childA".to_string()), |
| })], |
| children: vec![ChildDecl { |
| name: "childA".to_string(), |
| url: "ignored:///child".to_string(), |
| startup: fsys::StartupMode::Lazy, |
| environment: None, |
| }], |
| ..default_component_decl() |
| }; |
| |
| let mut expected: Vec<(DependencyNode, Vec<DependencyNode>)> = Vec::new(); |
| expected.push((DependencyNode::Child("childA".to_string()), vec![])); |
| validate_results(expected, process_component_dependencies(&decl)); |
| } |
| |
| #[test] |
| fn test_single_dependency() { |
| let child_a = ChildDecl { |
| name: "childA".to_string(), |
| url: "ignored:///child".to_string(), |
| startup: fsys::StartupMode::Lazy, |
| environment: None, |
| }; |
| let child_b = ChildDecl { |
| name: "childB".to_string(), |
| url: "ignored:///child".to_string(), |
| startup: fsys::StartupMode::Lazy, |
| environment: None, |
| }; |
| let decl = ComponentDecl { |
| offers: vec![ |
| OfferDecl::Protocol(OfferProtocolDecl { |
| source: OfferSource::Self_, |
| source_name: "serviceParent".into(), |
| target_name: "serviceParent".into(), |
| target: OfferTarget::Child("childA".to_string()), |
| dependency_type: DependencyType::Strong, |
| }), |
| OfferDecl::Protocol(OfferProtocolDecl { |
| source: OfferSource::Child("childB".to_string()), |
| source_name: "childBOffer".into(), |
| target_name: "serviceSibling".into(), |
| target: OfferTarget::Child("childA".to_string()), |
| dependency_type: DependencyType::Strong, |
| }), |
| ], |
| children: vec![child_a.clone(), child_b.clone()], |
| ..default_component_decl() |
| }; |
| |
| let mut expected: Vec<(DependencyNode, Vec<DependencyNode>)> = Vec::new(); |
| let mut v = vec![DependencyNode::Child(child_a.name.clone())]; |
| v.sort_unstable(); |
| expected.push((DependencyNode::Child(child_b.name.clone()), v)); |
| expected.push((DependencyNode::Child(child_a.name.clone()), vec![])); |
| expected.sort_unstable(); |
| |
| validate_results(expected, process_component_dependencies(&decl)); |
| } |
| |
| #[test] |
| fn test_environment_with_runner_from_parent() { |
| let decl = ComponentDecl { |
| environments: vec![EnvironmentDeclBuilder::new() |
| .name("env") |
| .add_runner(cm_rust::RunnerRegistration { |
| source: RegistrationSource::Parent, |
| source_name: "foo".into(), |
| target_name: "foo".into(), |
| }) |
| .build()], |
| children: vec![ |
| ChildDeclBuilder::new_lazy_child("childA").build(), |
| ChildDeclBuilder::new_lazy_child("childB").environment("env").build(), |
| ], |
| ..default_component_decl() |
| }; |
| |
| let mut expected: Vec<(DependencyNode, Vec<DependencyNode>)> = Vec::new(); |
| expected.push((DependencyNode::Child("childA".to_string()), vec![])); |
| expected.push((DependencyNode::Child("childB".to_string()), vec![])); |
| validate_results(expected, process_component_dependencies(&decl)); |
| } |
| |
| #[test] |
| fn test_environment_with_runner_from_child() { |
| let decl = ComponentDecl { |
| environments: vec![EnvironmentDeclBuilder::new() |
| .name("env") |
| .add_runner(cm_rust::RunnerRegistration { |
| source: RegistrationSource::Child("childA".into()), |
| source_name: "foo".into(), |
| target_name: "foo".into(), |
| }) |
| .build()], |
| children: vec![ |
| ChildDeclBuilder::new_lazy_child("childA").build(), |
| ChildDeclBuilder::new_lazy_child("childB").environment("env").build(), |
| ], |
| ..default_component_decl() |
| }; |
| |
| let mut expected: Vec<(DependencyNode, Vec<DependencyNode>)> = Vec::new(); |
| expected.push(( |
| DependencyNode::Child("childA".to_string()), |
| vec![DependencyNode::Child("childB".to_string())], |
| )); |
| expected.push((DependencyNode::Child("childB".to_string()), vec![])); |
| validate_results(expected, process_component_dependencies(&decl)); |
| } |
| |
| #[test] |
| fn test_environment_with_runner_from_child_to_collection() { |
| let decl = ComponentDecl { |
| environments: vec![EnvironmentDeclBuilder::new() |
| .name("env") |
| .add_runner(cm_rust::RunnerRegistration { |
| source: RegistrationSource::Child("childA".into()), |
| source_name: "foo".into(), |
| target_name: "foo".into(), |
| }) |
| .build()], |
| collections: vec![CollectionDeclBuilder::new().name("coll").environment("env").build()], |
| ..default_component_decl() |
| }; |
| |
| let mut expected: Vec<(DependencyNode, Vec<DependencyNode>)> = Vec::new(); |
| expected.push(( |
| DependencyNode::Child("childA".to_string()), |
| vec![DependencyNode::Collection("coll".to_string())], |
| )); |
| expected.push((DependencyNode::Collection("coll".to_string()), vec![])); |
| validate_results(expected, process_component_dependencies(&decl)); |
| } |
| |
| #[test] |
| fn test_chained_environments() { |
| let decl = ComponentDecl { |
| environments: vec![ |
| EnvironmentDeclBuilder::new() |
| .name("env") |
| .add_runner(cm_rust::RunnerRegistration { |
| source: RegistrationSource::Child("childA".into()), |
| source_name: "foo".into(), |
| target_name: "foo".into(), |
| }) |
| .build(), |
| EnvironmentDeclBuilder::new() |
| .name("env2") |
| .add_runner(cm_rust::RunnerRegistration { |
| source: RegistrationSource::Child("childB".into()), |
| source_name: "bar".into(), |
| target_name: "bar".into(), |
| }) |
| .build(), |
| ], |
| children: vec![ |
| ChildDeclBuilder::new_lazy_child("childA").build(), |
| ChildDeclBuilder::new_lazy_child("childB").environment("env").build(), |
| ChildDeclBuilder::new_lazy_child("childC").environment("env2").build(), |
| ], |
| ..default_component_decl() |
| }; |
| |
| let mut expected: Vec<(DependencyNode, Vec<DependencyNode>)> = Vec::new(); |
| expected.push(( |
| DependencyNode::Child("childA".to_string()), |
| vec![DependencyNode::Child("childB".to_string())], |
| )); |
| expected.push(( |
| DependencyNode::Child("childB".to_string()), |
| vec![DependencyNode::Child("childC".to_string())], |
| )); |
| expected.push((DependencyNode::Child("childC".to_string()), vec![])); |
| validate_results(expected, process_component_dependencies(&decl)); |
| } |
| |
| #[test] |
| fn test_environment_and_offer() { |
| let decl = ComponentDecl { |
| offers: vec![OfferDecl::Protocol(OfferProtocolDecl { |
| source: OfferSource::Child("childB".to_string()), |
| source_name: "childBOffer".into(), |
| target_name: "serviceSibling".into(), |
| target: OfferTarget::Child("childC".to_string()), |
| dependency_type: DependencyType::Strong, |
| })], |
| environments: vec![EnvironmentDeclBuilder::new() |
| .name("env") |
| .add_runner(cm_rust::RunnerRegistration { |
| source: RegistrationSource::Child("childA".into()), |
| source_name: "foo".into(), |
| target_name: "foo".into(), |
| }) |
| .build()], |
| children: vec![ |
| ChildDeclBuilder::new_lazy_child("childA").build(), |
| ChildDeclBuilder::new_lazy_child("childB").environment("env").build(), |
| ChildDeclBuilder::new_lazy_child("childC").build(), |
| ], |
| ..default_component_decl() |
| }; |
| |
| let mut expected: Vec<(DependencyNode, Vec<DependencyNode>)> = Vec::new(); |
| expected.push(( |
| DependencyNode::Child("childA".to_string()), |
| vec![DependencyNode::Child("childB".to_string())], |
| )); |
| expected.push(( |
| DependencyNode::Child("childB".to_string()), |
| vec![DependencyNode::Child("childC".to_string())], |
| )); |
| expected.push((DependencyNode::Child("childC".to_string()), vec![])); |
| validate_results(expected, process_component_dependencies(&decl)); |
| } |
| |
| #[test] |
| fn test_single_weak_dependency() { |
| let child_a = ChildDecl { |
| name: "childA".to_string(), |
| url: "ignored:///child".to_string(), |
| startup: fsys::StartupMode::Lazy, |
| environment: None, |
| }; |
| let child_b = ChildDecl { |
| name: "childB".to_string(), |
| url: "ignored:///child".to_string(), |
| startup: fsys::StartupMode::Lazy, |
| environment: None, |
| }; |
| let decl = ComponentDecl { |
| offers: vec![ |
| OfferDecl::Protocol(OfferProtocolDecl { |
| source: OfferSource::Self_, |
| source_name: "serviceParent".into(), |
| target_name: "serviceParent".into(), |
| target: OfferTarget::Child("childA".to_string()), |
| dependency_type: DependencyType::WeakForMigration, |
| }), |
| OfferDecl::Protocol(OfferProtocolDecl { |
| source: OfferSource::Child("childB".to_string()), |
| source_name: "childBOffer".into(), |
| target_name: "serviceSibling".into(), |
| target: OfferTarget::Child("childA".to_string()), |
| dependency_type: DependencyType::WeakForMigration, |
| }), |
| ], |
| children: vec![child_a.clone(), child_b.clone()], |
| ..default_component_decl() |
| }; |
| |
| let mut expected: Vec<(DependencyNode, Vec<DependencyNode>)> = Vec::new(); |
| expected.push((DependencyNode::Child(child_b.name.clone()), vec![])); |
| expected.push((DependencyNode::Child(child_a.name.clone()), vec![])); |
| expected.sort_unstable(); |
| |
| validate_results(expected, process_component_dependencies(&decl)); |
| } |
| |
| #[test] |
| fn test_multiple_dependencies_same_source() { |
| let child_a = ChildDecl { |
| name: "childA".to_string(), |
| url: "ignored:///child".to_string(), |
| startup: fsys::StartupMode::Lazy, |
| environment: None, |
| }; |
| let child_b = ChildDecl { |
| name: "childB".to_string(), |
| url: "ignored:///child".to_string(), |
| startup: fsys::StartupMode::Lazy, |
| environment: None, |
| }; |
| let decl = ComponentDecl { |
| offers: vec![ |
| OfferDecl::Protocol(OfferProtocolDecl { |
| source: OfferSource::Self_, |
| source_name: "serviceParent".into(), |
| target_name: "serviceParent".into(), |
| target: OfferTarget::Child("childA".to_string()), |
| dependency_type: DependencyType::Strong, |
| }), |
| OfferDecl::Protocol(OfferProtocolDecl { |
| source: OfferSource::Child("childB".to_string()), |
| source_name: "childBOffer".into(), |
| target_name: "serviceSibling".into(), |
| target: OfferTarget::Child("childA".to_string()), |
| dependency_type: DependencyType::Strong, |
| }), |
| OfferDecl::Protocol(OfferProtocolDecl { |
| source: OfferSource::Child("childB".to_string()), |
| source_name: "childBOtherOffer".into(), |
| target_name: "serviceOtherSibling".into(), |
| target: OfferTarget::Child("childA".to_string()), |
| dependency_type: DependencyType::Strong, |
| }), |
| ], |
| children: vec![child_a.clone(), child_b.clone()], |
| ..default_component_decl() |
| }; |
| |
| let mut expected: Vec<(DependencyNode, Vec<DependencyNode>)> = Vec::new(); |
| let mut v = vec![DependencyNode::Child(child_a.name.clone())]; |
| v.sort_unstable(); |
| expected.push((DependencyNode::Child(child_b.name.clone()), v)); |
| expected.push((DependencyNode::Child(child_a.name.clone()), vec![])); |
| expected.sort_unstable(); |
| |
| validate_results(expected, process_component_dependencies(&decl)); |
| } |
| |
| #[test] |
| fn test_multiple_dependents_same_source() { |
| let child_a = ChildDecl { |
| name: "childA".to_string(), |
| url: "ignored:///child".to_string(), |
| startup: fsys::StartupMode::Lazy, |
| environment: None, |
| }; |
| let child_b = ChildDecl { |
| name: "childB".to_string(), |
| url: "ignored:///child".to_string(), |
| startup: fsys::StartupMode::Lazy, |
| environment: None, |
| }; |
| let child_c = ChildDecl { |
| name: "childC".to_string(), |
| url: "ignored:///child".to_string(), |
| startup: fsys::StartupMode::Lazy, |
| environment: None, |
| }; |
| let decl = ComponentDecl { |
| offers: vec![ |
| OfferDecl::Protocol(OfferProtocolDecl { |
| source: OfferSource::Child("childB".to_string()), |
| source_name: "childBOffer".into(), |
| target_name: "serviceSibling".into(), |
| target: OfferTarget::Child("childA".to_string()), |
| dependency_type: DependencyType::Strong, |
| }), |
| OfferDecl::Protocol(OfferProtocolDecl { |
| source: OfferSource::Child("childB".to_string()), |
| source_name: "childBToC".into(), |
| target_name: "serviceSibling".into(), |
| target: OfferTarget::Child("childC".to_string()), |
| dependency_type: DependencyType::Strong, |
| }), |
| ], |
| children: vec![child_a.clone(), child_b.clone(), child_c.clone()], |
| |
| ..default_component_decl() |
| }; |
| |
| let mut expected: Vec<(DependencyNode, Vec<DependencyNode>)> = Vec::new(); |
| let mut v = vec![ |
| DependencyNode::Child(child_a.name.clone()), |
| DependencyNode::Child(child_c.name.clone()), |
| ]; |
| v.sort_unstable(); |
| expected.push((DependencyNode::Child(child_b.name.clone()), v)); |
| expected.push((DependencyNode::Child(child_a.name.clone()), vec![])); |
| expected.push((DependencyNode::Child(child_c.name.clone()), vec![])); |
| expected.sort_unstable(); |
| validate_results(expected, process_component_dependencies(&decl)); |
| } |
| |
| #[test] |
| fn test_multiple_dependencies() { |
| let child_a = ChildDecl { |
| name: "childA".to_string(), |
| url: "ignored:///child".to_string(), |
| startup: fsys::StartupMode::Lazy, |
| environment: None, |
| }; |
| let child_b = ChildDecl { |
| name: "childB".to_string(), |
| url: "ignored:///child".to_string(), |
| startup: fsys::StartupMode::Lazy, |
| environment: None, |
| }; |
| let child_c = ChildDecl { |
| name: "childC".to_string(), |
| url: "ignored:///child".to_string(), |
| startup: fsys::StartupMode::Lazy, |
| environment: None, |
| }; |
| let decl = ComponentDecl { |
| offers: vec![ |
| OfferDecl::Protocol(OfferProtocolDecl { |
| source: OfferSource::Child("childA".to_string()), |
| source_name: "childBOffer".into(), |
| target_name: "serviceSibling".into(), |
| target: OfferTarget::Child("childC".to_string()), |
| dependency_type: DependencyType::Strong, |
| }), |
| OfferDecl::Protocol(OfferProtocolDecl { |
| source: OfferSource::Child("childB".to_string()), |
| source_name: "childBToC".into(), |
| target_name: "serviceSibling".into(), |
| target: OfferTarget::Child("childC".to_string()), |
| dependency_type: DependencyType::Strong, |
| }), |
| OfferDecl::Protocol(OfferProtocolDecl { |
| source: OfferSource::Child("childC".to_string()), |
| source_name: "childCToA".into(), |
| target_name: "serviceSibling".into(), |
| target: OfferTarget::Child("childA".to_string()), |
| dependency_type: DependencyType::WeakForMigration, |
| }), |
| ], |
| children: vec![child_a.clone(), child_b.clone(), child_c.clone()], |
| ..default_component_decl() |
| }; |
| |
| let mut expected: Vec<(DependencyNode, Vec<DependencyNode>)> = Vec::new(); |
| expected.push(( |
| DependencyNode::Child(child_b.name.clone()), |
| vec![DependencyNode::Child(child_c.name.clone())], |
| )); |
| expected.push(( |
| DependencyNode::Child(child_a.name.clone()), |
| vec![DependencyNode::Child(child_c.name.clone())], |
| )); |
| expected.push((DependencyNode::Child(child_c.name.clone()), vec![])); |
| expected.sort_unstable(); |
| |
| validate_results(expected, process_component_dependencies(&decl)); |
| } |
| |
| #[test] |
| fn test_component_is_source_and_target() { |
| let child_a = ChildDecl { |
| name: "childA".to_string(), |
| url: "ignored:///child".to_string(), |
| startup: fsys::StartupMode::Lazy, |
| environment: None, |
| }; |
| let child_b = ChildDecl { |
| name: "childB".to_string(), |
| url: "ignored:///child".to_string(), |
| startup: fsys::StartupMode::Lazy, |
| environment: None, |
| }; |
| let child_c = ChildDecl { |
| name: "childC".to_string(), |
| url: "ignored:///child".to_string(), |
| startup: fsys::StartupMode::Lazy, |
| environment: None, |
| }; |
| let decl = ComponentDecl { |
| offers: vec![ |
| OfferDecl::Protocol(OfferProtocolDecl { |
| source: OfferSource::Child("childA".to_string()), |
| source_name: "childBOffer".into(), |
| target_name: "serviceSibling".into(), |
| target: OfferTarget::Child("childB".to_string()), |
| dependency_type: DependencyType::Strong, |
| }), |
| OfferDecl::Protocol(OfferProtocolDecl { |
| source: OfferSource::Child("childB".to_string()), |
| source_name: "childBToC".into(), |
| target_name: "serviceSibling".into(), |
| target: OfferTarget::Child("childC".to_string()), |
| dependency_type: DependencyType::Strong, |
| }), |
| ], |
| children: vec![child_a.clone(), child_b.clone(), child_c.clone()], |
| ..default_component_decl() |
| }; |
| |
| let mut expected: Vec<(DependencyNode, Vec<DependencyNode>)> = Vec::new(); |
| |
| expected.push(( |
| DependencyNode::Child(child_a.name.clone()), |
| vec![DependencyNode::Child(child_b.name.clone())], |
| )); |
| expected.push(( |
| DependencyNode::Child(child_b.name.clone()), |
| vec![DependencyNode::Child(child_c.name.clone())], |
| )); |
| expected.push((DependencyNode::Child(child_c.name.clone()), vec![])); |
| expected.sort_unstable(); |
| validate_results(expected, process_component_dependencies(&decl)); |
| } |
| |
| /// Tests a graph that looks like the below, tildes indicate a |
| /// capability route. Route point toward the target of the capability |
| /// offer. The manifest constructed is for 'P'. |
| /// P |
| /// ___|___ |
| /// / / | \ \ |
| /// e<~c<~a~>b~>d |
| /// \ / |
| /// *>~~>* |
| #[test] |
| fn test_complex_routing() { |
| let child_a = ChildDecl { |
| name: "childA".to_string(), |
| url: "ignored:///child".to_string(), |
| startup: fsys::StartupMode::Lazy, |
| environment: None, |
| }; |
| let child_b = ChildDecl { |
| name: "childB".to_string(), |
| url: "ignored:///child".to_string(), |
| startup: fsys::StartupMode::Lazy, |
| environment: None, |
| }; |
| let child_c = ChildDecl { |
| name: "childC".to_string(), |
| url: "ignored:///child".to_string(), |
| startup: fsys::StartupMode::Lazy, |
| environment: None, |
| }; |
| let child_d = ChildDecl { |
| name: "childD".to_string(), |
| url: "ignored:///child".to_string(), |
| startup: fsys::StartupMode::Lazy, |
| environment: None, |
| }; |
| let child_e = ChildDecl { |
| name: "childE".to_string(), |
| url: "ignored:///child".to_string(), |
| startup: fsys::StartupMode::Lazy, |
| environment: None, |
| }; |
| let decl = ComponentDecl { |
| offers: vec![ |
| OfferDecl::Protocol(OfferProtocolDecl { |
| source: OfferSource::Child("childA".to_string()), |
| source_name: "childAService".into(), |
| target_name: "childAService".into(), |
| target: OfferTarget::Child("childB".to_string()), |
| dependency_type: DependencyType::Strong, |
| }), |
| OfferDecl::Protocol(OfferProtocolDecl { |
| source: OfferSource::Child("childA".to_string()), |
| source_name: "childAService".into(), |
| target_name: "childAService".into(), |
| target: OfferTarget::Child("childC".to_string()), |
| dependency_type: DependencyType::Strong, |
| }), |
| OfferDecl::Protocol(OfferProtocolDecl { |
| source: OfferSource::Child("childB".to_string()), |
| source_name: "childBService".into(), |
| target_name: "childBService".into(), |
| target: OfferTarget::Child("childD".to_string()), |
| dependency_type: DependencyType::Strong, |
| }), |
| OfferDecl::Protocol(OfferProtocolDecl { |
| source: OfferSource::Child("childC".to_string()), |
| source_name: "childAService".into(), |
| target_name: "childAService".into(), |
| target: OfferTarget::Child("childD".to_string()), |
| dependency_type: DependencyType::Strong, |
| }), |
| OfferDecl::Protocol(OfferProtocolDecl { |
| source: OfferSource::Child("childC".to_string()), |
| source_name: "childAService".into(), |
| target_name: "childAService".into(), |
| target: OfferTarget::Child("childE".to_string()), |
| dependency_type: DependencyType::Strong, |
| }), |
| ], |
| children: vec![ |
| child_a.clone(), |
| child_b.clone(), |
| child_c.clone(), |
| child_d.clone(), |
| child_e.clone(), |
| ], |
| ..default_component_decl() |
| }; |
| |
| let mut expected: Vec<(DependencyNode, Vec<DependencyNode>)> = Vec::new(); |
| expected.push(( |
| DependencyNode::Child(child_a.name.clone()), |
| vec![ |
| DependencyNode::Child(child_b.name.clone()), |
| DependencyNode::Child(child_c.name.clone()), |
| ], |
| )); |
| expected.push(( |
| DependencyNode::Child(child_b.name.clone()), |
| vec![DependencyNode::Child(child_d.name.clone())], |
| )); |
| expected.push(( |
| DependencyNode::Child(child_c.name.clone()), |
| vec![ |
| DependencyNode::Child(child_d.name.clone()), |
| DependencyNode::Child(child_e.name.clone()), |
| ], |
| )); |
| expected.push((DependencyNode::Child(child_d.name.clone()), vec![])); |
| expected.push((DependencyNode::Child(child_e.name.clone()), vec![])); |
| expected.sort_unstable(); |
| validate_results(expected, process_component_dependencies(&decl)); |
| } |
| |
| #[test] |
| #[should_panic] |
| fn test_target_does_not_exist() { |
| let child_a = ChildDecl { |
| name: "childA".to_string(), |
| url: "ignored:///child".to_string(), |
| startup: fsys::StartupMode::Lazy, |
| environment: None, |
| }; |
| // This declaration is invalid because the offer target doesn't exist |
| let decl = ComponentDecl { |
| offers: vec![OfferDecl::Protocol(OfferProtocolDecl { |
| source: OfferSource::Child("childA".to_string()), |
| source_name: "childBOffer".into(), |
| target_name: "serviceSibling".into(), |
| target: OfferTarget::Child("childB".to_string()), |
| dependency_type: DependencyType::Strong, |
| })], |
| children: vec![child_a.clone()], |
| ..default_component_decl() |
| }; |
| |
| process_component_dependencies(&decl); |
| } |
| |
| #[test] |
| #[should_panic] |
| fn test_source_does_not_exist() { |
| let child_a = ChildDecl { |
| name: "childA".to_string(), |
| url: "ignored:///child".to_string(), |
| startup: fsys::StartupMode::Lazy, |
| environment: None, |
| }; |
| // This declaration is invalid because the offer target doesn't exist |
| let decl = ComponentDecl { |
| offers: vec![OfferDecl::Protocol(OfferProtocolDecl { |
| source: OfferSource::Child("childB".to_string()), |
| source_name: "childBOffer".into(), |
| target_name: "serviceSibling".into(), |
| target: OfferTarget::Child("childA".to_string()), |
| dependency_type: DependencyType::Strong, |
| })], |
| children: vec![child_a.clone()], |
| ..default_component_decl() |
| }; |
| |
| process_component_dependencies(&decl); |
| } |
| |
| #[test] |
| fn test_resolver_capability_creates_dependency() { |
| let child_a = ChildDecl { |
| name: "childA".to_string(), |
| url: "ignored:///child".to_string(), |
| startup: fsys::StartupMode::Lazy, |
| environment: None, |
| }; |
| let child_b = ChildDecl { |
| name: "childB".to_string(), |
| url: "ignored:///child".to_string(), |
| startup: fsys::StartupMode::Lazy, |
| environment: None, |
| }; |
| let decl = ComponentDecl { |
| offers: vec![OfferDecl::Resolver(OfferResolverDecl { |
| source: OfferSource::Child("childA".to_string()), |
| source_name: CapabilityName::try_from("resolver").unwrap(), |
| target_name: CapabilityName::try_from("resolver").unwrap(), |
| target: OfferTarget::Child("childB".to_string()), |
| })], |
| children: vec![child_a.clone(), child_b.clone()], |
| ..default_component_decl() |
| }; |
| |
| let mut expected = vec![ |
| ( |
| DependencyNode::Child(child_a.name.clone()), |
| vec![DependencyNode::Child(child_b.name.clone())], |
| ), |
| (DependencyNode::Child(child_b.name.clone()), vec![]), |
| ]; |
| expected.sort_unstable(); |
| validate_results(expected, process_component_dependencies(&decl)); |
| } |
| |
| #[fuchsia::test] |
| async fn action_shutdown_blocks_stop() { |
| let test = ActionsTest::new("root", vec![], None).await; |
| let component = test.model.root.clone(); |
| let mut action_set = component.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(&component, ShutdownAction::new()); |
| let (task2, nf2) = action_set.register_inner(&component, StopAction::new()); |
| |
| drop(action_set); |
| |
| // Complete actions, while checking futures. |
| ActionSet::finish(&component, &ActionKey::Shutdown).await; |
| |
| // nf2 should be blocked on task1 completing. |
| assert!(nf1.fut.peek().is_none()); |
| assert!(nf2.fut.peek().is_none()); |
| task1.unwrap().tx.send(Ok(())).unwrap(); |
| task2.unwrap().spawn(); |
| nf1.await.unwrap(); |
| nf2.await.unwrap(); |
| } |
| |
| #[fuchsia::test] |
| async fn action_shutdown_stop_stop() { |
| let test = ActionsTest::new("root", vec![], None).await; |
| let component = test.model.root.clone(); |
| let mut action_set = component.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(&component, ShutdownAction::new()); |
| let (task2, nf2) = action_set.register_inner(&component, StopAction::new()); |
| let (task3, nf3) = action_set.register_inner(&component, StopAction::new()); |
| |
| drop(action_set); |
| |
| // Complete actions, while checking notifications. |
| ActionSet::finish(&component, &ActionKey::Shutdown).await; |
| |
| // nf2 and nf3 should be blocked on task1 completing. |
| assert!(nf1.fut.peek().is_none()); |
| assert!(nf2.fut.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::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 component to have an |
| // `Execution`. |
| let component = test.look_up(vec!["a:0"].into()).await; |
| test.model |
| .bind(&component.abs_moniker, &BindReason::Eager) |
| .await |
| .expect("could not bind to a"); |
| assert!(is_executing(&component).await); |
| let a_info = ComponentInfo::new(component.clone()).await; |
| |
| // Register shutdown action, and wait for it. Component should shut down (no more |
| // `Execution`). |
| ActionSet::register(a_info.component.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.component.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.component.clone(), ShutdownAction::new()) |
| .await |
| .expect("shutdown failed"); |
| &a_info.check_is_shut_down(&test.runner).await; |
| } |
| |
| #[fuchsia::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 component_container = test.look_up(vec!["container:0"].into()).await; |
| let component_a = test.look_up(vec!["container:0", "coll:a:1"].into()).await; |
| let component_b = test.look_up(vec!["container:0", "coll:b:2"].into()).await; |
| let component_c = test.look_up(vec!["container:0", "c:0"].into()).await; |
| test.model |
| .bind(&component_container.abs_moniker, &BindReason::Eager) |
| .await |
| .expect("could not bind to container"); |
| test.model |
| .bind(&component_a.abs_moniker, &BindReason::Eager) |
| .await |
| .expect("could not bind to coll:a"); |
| test.model |
| .bind(&component_b.abs_moniker, &BindReason::Eager) |
| .await |
| .expect("could not bind to coll:b"); |
| test.model |
| .bind(&component_c.abs_moniker, &BindReason::Eager) |
| .await |
| .expect("could not bind to coll:b"); |
| assert!(is_executing(&component_container).await); |
| assert!(is_executing(&component_a).await); |
| assert!(is_executing(&component_b).await); |
| assert!(is_executing(&component_c).await); |
| assert!(has_child(&component_container, "coll:a:1").await); |
| assert!(has_child(&component_container, "coll:b:2").await); |
| |
| let component_a_info = ComponentInfo::new(component_a).await; |
| let component_b_info = ComponentInfo::new(component_b).await; |
| let component_container_info = ComponentInfo::new(component_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(component_container_info.component.clone(), ShutdownAction::new()) |
| .await |
| .expect("shutdown failed"); |
| component_container_info.check_is_shut_down(&test.runner).await; |
| assert!(!has_child(&component_container_info.component, "coll:a:1").await); |
| assert!(!has_child(&component_container_info.component, "coll:b:2").await); |
| assert!(has_child(&component_container_info.component, "c:0").await); |
| component_a_info.check_is_shut_down(&test.runner).await; |
| component_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::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 component_a = test.look_up(vec!["a:0"].into()).await; |
| let component_b = test.look_up(vec!["a:0", "b:0"].into()).await; |
| assert!(!is_executing(&component_a).await); |
| assert!(!is_executing(&component_b).await); |
| |
| // Register shutdown action on "a", and wait for it. |
| ActionSet::register(component_a.clone(), ShutdownAction::new()) |
| .await |
| .expect("shutdown failed"); |
| assert!(execution_is_shut_down(&component_a).await); |
| assert!(execution_is_shut_down(&component_b).await); |
| |
| // Now "a" is shut down. There should be no events though because the component was |
| // never started. |
| ActionSet::register(component_a.clone(), ShutdownAction::new()) |
| .await |
| .expect("shutdown failed"); |
| assert!(execution_is_shut_down(&component_a).await); |
| assert!(execution_is_shut_down(&component_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::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 component_a = test.look_up(vec!["a:0"].into()).await; |
| test.model |
| .bind(&component_a.abs_moniker, &BindReason::Eager) |
| .await |
| .expect("could not bind to a"); |
| assert!(is_executing(&component_a).await); |
| |
| // Register shutdown action on "a", and wait for it. |
| ActionSet::register(component_a.clone(), ShutdownAction::new()) |
| .await |
| .expect("shutdown failed"); |
| assert!(execution_is_shut_down(&component_a).await); |
| // Get component without resolving it. |
| let component_b = { |
| let state = component_a.lock_state().await; |
| match *state { |
| InstanceState::Resolved(ref s) => { |
| s.get_live_child(&PartialMoniker::from("b")).expect("child b not found") |
| } |
| _ => panic!("not resolved"), |
| } |
| }; |
| assert!(execution_is_shut_down(&component_b).await); |
| assert!(is_unresolved(&component_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::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 component_a = test.look_up(vec!["a:0"].into()).await; |
| let component_b = test.look_up(vec!["a:0", "b:0"].into()).await; |
| let component_c = test.look_up(vec!["a:0", "b:0", "c:0"].into()).await; |
| let component_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(&component_a.abs_moniker, &BindReason::Eager) |
| .await |
| .expect("could not bind to a"); |
| assert!(is_executing(&component_a).await); |
| assert!(is_executing(&component_b).await); |
| assert!(is_executing(&component_c).await); |
| assert!(is_executing(&component_d).await); |
| |
| let component_a_info = ComponentInfo::new(component_a).await; |
| let component_b_info = ComponentInfo::new(component_b).await; |
| let component_c_info = ComponentInfo::new(component_c).await; |
| let component_d_info = ComponentInfo::new(component_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(component_a_info.component.clone(), ShutdownAction::new()) |
| .await |
| .expect("shutdown failed"); |
| component_a_info.check_is_shut_down(&test.runner).await; |
| component_b_info.check_is_shut_down(&test.runner).await; |
| component_c_info.check_is_shut_down(&test.runner).await; |
| component_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::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: OfferSource::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: OfferSource::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 component_a = test.look_up(vec!["a:0"].into()).await; |
| let component_b = test.look_up(vec!["a:0", "b:0"].into()).await; |
| let component_c = test.look_up(vec!["a:0", "b:0", "c:0"].into()).await; |
| let component_d = test.look_up(vec!["a:0", "b:0", "d:0"].into()).await; |
| let component_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(&component_a.abs_moniker, &BindReason::Eager) |
| .await |
| .expect("could not bind to a"); |
| assert!(is_executing(&component_a).await); |
| assert!(is_executing(&component_b).await); |
| assert!(is_executing(&component_c).await); |
| assert!(is_executing(&component_d).await); |
| assert!(is_executing(&component_e).await); |
| |
| let component_a_info = ComponentInfo::new(component_a).await; |
| let component_b_info = ComponentInfo::new(component_b).await; |
| let component_c_info = ComponentInfo::new(component_c).await; |
| let component_d_info = ComponentInfo::new(component_d).await; |
| let component_e_info = ComponentInfo::new(component_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(component_a_info.component.clone(), ShutdownAction::new()) |
| .await |
| .expect("shutdown failed"); |
| component_a_info.check_is_shut_down(&test.runner).await; |
| component_b_info.check_is_shut_down(&test.runner).await; |
| component_c_info.check_is_shut_down(&test.runner).await; |
| component_d_info.check_is_shut_down(&test.runner).await; |
| component_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::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: OfferSource::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: OfferSource::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: OfferSource::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 component_a = test.look_up(moniker_a.clone()).await; |
| let component_b = test.look_up(moniker_b.clone()).await; |
| let component_c = test.look_up(moniker_c.clone()).await; |
| let component_d = test.look_up(moniker_d.clone()).await; |
| let component_e = test.look_up(moniker_e.clone()).await; |
| let component_f = test.look_up(moniker_f.clone()).await; |
| |
| // Component startup was eager, so they should all have an `Execution`. |
| test.model |
| .bind(&component_a.abs_moniker, &BindReason::Eager) |
| .await |
| .expect("could not bind to a"); |
| assert!(is_executing(&component_a).await); |
| assert!(is_executing(&component_b).await); |
| assert!(is_executing(&component_c).await); |
| assert!(is_executing(&component_d).await); |
| assert!(is_executing(&component_e).await); |
| assert!(is_executing(&component_f).await); |
| |
| let component_a_info = ComponentInfo::new(component_a).await; |
| let component_b_info = ComponentInfo::new(component_b).await; |
| let component_c_info = ComponentInfo::new(component_c).await; |
| let component_d_info = ComponentInfo::new(component_d).await; |
| let component_e_info = ComponentInfo::new(component_e).await; |
| let component_f_info = ComponentInfo::new(component_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(component_a_info.component.clone(), ShutdownAction::new()) |
| .await |
| .expect("shutdown failed"); |
| component_a_info.check_is_shut_down(&test.runner).await; |
| component_b_info.check_is_shut_down(&test.runner).await; |
| component_c_info.check_is_shut_down(&test.runner).await; |
| component_d_info.check_is_shut_down(&test.runner).await; |
| component_e_info.check_is_shut_down(&test.runner).await; |
| component_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::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: OfferSource::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: OfferSource::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: OfferSource::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: OfferSource::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 component_a = test.look_up(moniker_a.clone()).await; |
| let component_b = test.look_up(moniker_b.clone()).await; |
| let component_c = test.look_up(moniker_c.clone()).await; |
| let component_d = test.look_up(moniker_d.clone()).await; |
| let component_e = test.look_up(moniker_e.clone()).await; |
| let component_f = test.look_up(moniker_f.clone()).await; |
| |
| // Component startup was eager, so they should all have an `Execution`. |
| test.model |
| .bind(&component_a.abs_moniker, &BindReason::Eager) |
| .await |
| .expect("could not bind to a"); |
| assert!(is_executing(&component_a).await); |
| assert!(is_executing(&component_b).await); |
| assert!(is_executing(&component_c).await); |
| assert!(is_executing(&component_d).await); |
| assert!(is_executing(&component_e).await); |
| assert!(is_executing(&component_f).await); |
| |
| let component_a_info = ComponentInfo::new(component_a).await; |
| let component_b_info = ComponentInfo::new(component_b).await; |
| let component_c_info = ComponentInfo::new(component_c).await; |
| let component_d_info = ComponentInfo::new(component_d).await; |
| let component_e_info = ComponentInfo::new(component_e).await; |
| let component_f_info = ComponentInfo::new(component_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(component_a_info.component.clone(), ShutdownAction::new()) |
| .await |
| .expect("shutdown failed"); |
| component_a_info.check_is_shut_down(&test.runner).await; |
| component_b_info.check_is_shut_down(&test.runner).await; |
| component_c_info.check_is_shut_down(&test.runner).await; |
| component_d_info.check_is_shut_down(&test.runner).await; |
| component_e_info.check_is_shut_down(&test.runner).await; |
| component_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::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: OfferSource::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 component_a = test.look_up(vec!["a:0"].into()).await; |
| let component_b = test.look_up(vec!["a:0", "b:0"].into()).await; |
| let component_c = test.look_up(vec!["a:0", "b:0", "c:0"].into()).await; |
| let component_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(&component_a.abs_moniker, &BindReason::Eager) |
| .await |
| .expect("could not bind to a"); |
| |
| let component_a_info = ComponentInfo::new(component_a).await; |
| let component_b_info = ComponentInfo::new(component_b).await; |
| let component_c_info = ComponentInfo::new(component_c).await; |
| let component_d_info = ComponentInfo::new(component_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(component_a_info.component.clone(), ShutdownAction::new()) |
| .await |
| .expect("shutdown failed"); |
| component_a_info.check_is_shut_down(&test.runner).await; |
| component_b_info.check_is_shut_down(&test.runner).await; |
| component_c_info.check_is_shut_down(&test.runner).await; |
| component_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::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 component_a = test.look_up(vec!["a:0"].into()).await; |
| let component_b = test.look_up(vec!["a:0", "b:0"].into()).await; |
| let component_b2 = test.look_up(vec!["a:0", "b:0", "b:0"].into()).await; |
| |
| // Bind to second `b`. |
| test.model |
| .bind(&component_a.abs_moniker, &BindReason::Eager) |
| .await |
| .expect("could not bind to b2"); |
| test.model |
| .bind(&component_b.abs_moniker, &BindReason::Eager) |
| .await |
| .expect("could not bind to b2"); |
| test.model |
| .bind(&component_b2.abs_moniker, &BindReason::Eager) |
| .await |
| .expect("could not bind to b2"); |
| assert!(is_executing(&component_a).await); |
| assert!(is_executing(&component_b).await); |
| assert!(is_executing(&component_b2).await); |
| |
| let component_a_info = ComponentInfo::new(component_a).await; |
| let component_b_info = ComponentInfo::new(component_b).await; |
| let component_b2_info = ComponentInfo::new(component_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(component_a_info.component.clone(), ShutdownAction::new()) |
| .await |
| .expect("shutdown failed"); |
| component_a_info.check_is_shut_down(&test.runner).await; |
| component_b_info.check_is_shut_down(&test.runner).await; |
| component_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::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: &hooks::Event) -> Result<(), ModelError> { |
| let target_moniker = event |
| .target_moniker |
| .unwrap_instance_moniker_or(ModelError::UnexpectedComponentManagerMoniker)?; |
| if let Ok(EventPayload::Stopped { .. }) = event.result { |
| self.on_shutdown_instance_async(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 component_a = test.look_up(vec!["a:0"].into()).await; |
| let component_b = test.look_up(vec!["a:0", "b:0"].into()).await; |
| let component_c = test.look_up(vec!["a:0", "b:0", "c:0"].into()).await; |
| let component_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(&component_a.abs_moniker, &BindReason::Eager) |
| .await |
| .expect("could not bind to a"); |
| assert!(is_executing(&component_a).await); |
| assert!(is_executing(&component_b).await); |
| assert!(is_executing(&component_c).await); |
| assert!(is_executing(&component_d).await); |
| |
| let component_a_info = ComponentInfo::new(component_a).await; |
| let component_b_info = ComponentInfo::new(component_b).await; |
| let component_c_info = ComponentInfo::new(component_c).await; |
| let component_d_info = ComponentInfo::new(component_d).await; |
| |
| // Register shutdown action on "a", and wait for it. "b"'s component shuts down, but "b" |
| // returns an error so "a" does not. |
| ActionSet::register(component_a_info.component.clone(), ShutdownAction::new()) |
| .await |
| .expect_err("shutdown succeeded unexpectedly"); |
| component_a_info.check_not_shut_down(&test.runner).await; |
| component_b_info.check_is_shut_down(&test.runner).await; |
| component_c_info.check_is_shut_down(&test.runner).await; |
| component_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(component_a_info.component.clone(), ShutdownAction::new()) |
| .await |
| .expect("shutdown failed"); |
| component_a_info.check_is_shut_down(&test.runner).await; |
| component_b_info.check_is_shut_down(&test.runner).await; |
| component_c_info.check_is_shut_down(&test.runner).await; |
| component_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()) |
| ] |
| ); |
| } |
| } |
| } |