blob: 28c8ff7aa1763795f9a39e3e2884519c927542a5 [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use {
crate::model::{
actions::{Action, ActionKey, ActionsManager},
component::instance::{InstanceState, ResolvedInstanceState},
component::ComponentInstance,
},
async_trait::async_trait,
cm_rust::{
CapabilityDecl, ChildRef, CollectionDecl, DependencyType, DictionaryDecl, DictionarySource,
EnvironmentDecl, ExposeDecl, OfferConfigurationDecl, OfferDecl, OfferDictionaryDecl,
OfferDirectoryDecl, OfferProtocolDecl, OfferResolverDecl, OfferRunnerDecl,
OfferServiceDecl, OfferSource, OfferStorageDecl, OfferTarget, RegistrationDeclCommon,
RegistrationSource, SourcePath, StorageDecl, StorageDirectorySource, UseConfigurationDecl,
UseDecl, UseDirectoryDecl, UseEventStreamDecl, UseProtocolDecl, UseRunnerDecl,
UseServiceDecl, UseSource, UseStorageDecl,
},
cm_types::{IterablePath, Name},
errors::ActionError,
futures::future::select_all,
moniker::{ChildName, ChildNameBase},
std::collections::{HashMap, HashSet},
std::fmt,
std::iter,
std::sync::Arc,
tracing::*,
};
/// Shuts down all component instances in this component (stops them and guarantees they will never
/// be started again).
pub struct ShutdownAction {
shutdown_type: ShutdownType,
}
/// Indicates the type of shutdown being performed.
#[derive(Clone, Copy)]
pub enum ShutdownType {
/// An individual component instance was shut down. For example, this is used when
/// a component instance is destroyed.
Instance,
/// The entire system under this component_manager was shutdown on behalf of
/// a call to SystemController/Shutdown.
System,
}
impl ShutdownAction {
pub fn new(shutdown_type: ShutdownType) -> Self {
Self { shutdown_type }
}
}
#[async_trait]
impl Action for ShutdownAction {
async fn handle(self, component: Arc<ComponentInstance>) -> Result<(), ActionError> {
do_shutdown(&component, self.shutdown_type).await
}
fn key(&self) -> ActionKey {
ActionKey::Shutdown
}
}
async fn shutdown_component(
target: ShutdownInfo,
shutdown_type: ShutdownType,
) -> Result<ComponentRef, ActionError> {
match target.ref_ {
ComponentRef::Self_ => {
// TODO: Put `self` in a "shutting down" state so that if it creates
// new instances after this point, they are created in a shut down
// state.
//
// NOTE: we cannot register a `StopAction { shutdown: true }` action because
// that would be overridden by any concurrent `StopAction { shutdown: false }`.
// More over, for reasons detailed in
// https://fxrev.dev/I8ccfa1deed368f2ccb77cde0d713f3af221f7450, an in-progress
// Shutdown action will block Stop actions, so registering the latter will deadlock.
target.component.stop_instance_internal(true).await?;
}
ComponentRef::Child(_) => {
ActionsManager::register(target.component, ShutdownAction::new(shutdown_type)).await?;
}
ComponentRef::Capability(_) => {
// This is just an intermediate node that exists to track dependencies on storage
// and dictionary capabilities from Self, which aren't associated with the running
// program. Nothing to do.
}
}
Ok(target.ref_.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<ComponentRef, Vec<ComponentRef>>,
/// A map from providers of capabilities to those components which use the
/// capabilities
source_to_targets: HashMap<ComponentRef, ShutdownInfo>,
/// The type of shutdown being performed. For debug purposes.
shutdown_type: ShutdownType,
}
/// 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(
instance: &Arc<ComponentInstance>,
state: &ResolvedInstanceState,
shutdown_type: ShutdownType,
) -> ShutdownJob {
// `dependency_map` represents the dependency relationships between the
// nodes in this realm (the children, and the component itself).
// `dependency_map` maps server => clients (a.k.a. provider => consumers,
// or source => targets)
let dependency_map = process_component_dependencies(state);
let mut source_to_targets: HashMap<ComponentRef, ShutdownInfo> = HashMap::new();
for (source, targets) in dependency_map {
let component = match &source {
ComponentRef::Self_ => instance.clone(),
ComponentRef::Child(moniker) => {
state.get_child(&moniker).expect("component not found in children").clone()
}
ComponentRef::Capability(_) => instance.clone(),
};
source_to_targets.insert(
source.clone(),
ShutdownInfo { ref_: source, dependents: targets, component },
);
}
// `target_to_sources` is the inverse of `source_to_targets`, and maps a target to all of
// its dependencies. This inverse mapping gives us a way to do quick lookups when updating
// `source_to_targets` as we shutdown components in execute().
let mut target_to_sources: HashMap<ComponentRef, Vec<ComponentRef>> = HashMap::new();
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.ref_.clone());
}
}
let new_job = ShutdownJob { source_to_targets, target_to_sources, shutdown_type };
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<(), ActionError> {
// 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 component_ref in
self.source_to_targets.keys().map(|key| key.clone()).collect::<Vec<_>>()
{
let no_dependents = {
let info =
self.source_to_targets.get(&component_ref).expect("key disappeared from map");
info.dependents.is_empty()
};
if no_dependents {
stop_targets.push(
self.source_to_targets
.remove(&component_ref)
.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, self.shutdown_type)));
}
let (component_ref, _, remaining) = select_all(futs).await;
futs = remaining;
let component_ref = component_ref?;
// Look up the dependencies of the component that stopped
match self.target_to_sources.remove(&component_ref) {
Some(sources) => {
for source in sources {
let ready_to_stop = {
if let Some(info) = self.source_to_targets.get_mut(&source) {
info.dependents.remove(&component_ref);
// Have all of this components dependents stopped?
info.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 '{:?}'",
component_ref, source
);
}
};
// This components had zero remaining dependents
if ready_to_stop {
stop_targets.push(
self.source_to_targets
.remove(&source)
.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(())
}
}
pub async fn do_shutdown(
component: &Arc<ComponentInstance>,
shutdown_type: ShutdownType,
) -> Result<(), ActionError> {
// Keep logs short to preserve as much as possible in the crash report
// NS: Shutdown of {moniker} was no-op
// RS: Beginning shutdown of resolved component {moniker}
// US: Beginning shutdown of unresolved component {moniker}
// FS: Finished shutdown of {moniker}
// ES: Errored shutdown of {moniker}
{
let state = component.lock_state().await;
match *state {
InstanceState::Resolved(ref s) | InstanceState::Started(ref s, _) => {
if matches!(shutdown_type, ShutdownType::System) {
info!("=RS {}", component.moniker);
}
let mut shutdown_job = ShutdownJob::new(component, s, shutdown_type).await;
drop(state);
Box::pin(shutdown_job.execute()).await.map_err(|err| {
warn!("=ES {}", component.moniker);
err
})?;
if matches!(shutdown_type, ShutdownType::System) {
info!("=FS {}", component.moniker);
}
return Ok(());
}
InstanceState::Shutdown(_, _) => {
if matches!(shutdown_type, ShutdownType::System) {
info!("=NS {}", component.moniker);
}
return Ok(());
}
InstanceState::New | InstanceState::Unresolved(_) | InstanceState::Destroyed => {}
}
}
// Control flow arrives here if the component isn't resolved.
// TODO: Put this component in a "shutting down" state so that if it creates new instances
// after this point, they are created in a shut down state.
if let ShutdownType::System = shutdown_type {
info!("=US {}", component.moniker);
}
component.stop_instance_internal(true).await.map_err(|err| {
warn!("=ES {}", component.moniker);
err
})?;
if matches!(shutdown_type, ShutdownType::System) {
info!("=FS {}", component.moniker);
}
Ok(())
}
/// Identifies a component in this realm. This can either be the component
/// itself, one of its children, or a capability (used only as an intermediate node for dependency
/// tracking).
#[derive(Debug, Eq, PartialEq, Hash, Clone)]
pub enum ComponentRef {
Self_,
Child(ChildName),
// A capability defined by this component (this is either a dictionary or storage capability).
Capability(Name),
}
impl From<ChildName> for ComponentRef {
fn from(moniker: ChildName) -> Self {
Self::Child(moniker)
}
}
/// 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 ref_: ComponentRef,
/// The components that this component offers capabilities to
pub dependents: HashSet<ComponentRef>,
pub component: Arc<ComponentInstance>,
}
impl fmt::Debug for ShutdownInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{{server: {{{:?}}}, ", self.component.moniker.to_string())?;
write!(f, "clients: [")?;
for dep in &self.dependents {
write!(f, "{{{:?}}}, ", dep)?;
}
write!(f, "]}}")?;
Ok(())
}
}
/// Trait exposing all component state necessary to compute shutdown order.
///
/// This trait largely mirrors `ComponentDecl`, but will reflect changes made to
/// the component's state at runtime (e.g., dynamically created children,
/// dynamic offers).
///
/// In production, this will probably only be implemented for
/// `ResolvedInstanceState`, but exposing this trait allows for easier testing.
pub trait Component {
/// Current view of this component's `uses` declarations.
fn uses(&self) -> Vec<UseDecl>;
/// Current view of this component's `exposes` declarations.
#[allow(dead_code)]
fn exposes(&self) -> Vec<ExposeDecl>;
/// Current view of this component's `offers` declarations.
fn offers(&self) -> Vec<OfferDecl>;
/// Current view of this component's `capabilities` declarations.
fn capabilities(&self) -> Vec<CapabilityDecl>;
/// Current view of this component's `collections` declarations.
#[allow(dead_code)]
fn collections(&self) -> Vec<CollectionDecl>;
/// Current view of this component's `environments` declarations.
fn environments(&self) -> Vec<EnvironmentDecl>;
/// Returns metadata about each child of this component.
fn children(&self) -> Vec<Child>;
/// Returns the live child that has the given `name` and `collection`, or
/// returns `None` if none match. In the case of dynamic children, it's
/// possible for multiple children to match a given `name` and `collection`,
/// but at most one of them can be live.
///
/// Note: `name` is a `&str` because it could be either a `Name` or `LongName`.
fn find_child(&self, name: &str, collection: Option<&Name>) -> Option<Child> {
self.children().into_iter().find(|child| {
child.moniker.name().as_str() == name && child.moniker.collection() == collection
})
}
}
/// Child metadata necessary to compute shutdown order.
///
/// A `Component` returns information about its children by returning a vector
/// of these.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct Child {
/// The moniker identifying the name of the child, complete with
/// `instance_id`.
pub moniker: ChildName,
/// Name of the environment associated with this child, if any.
pub environment_name: Option<Name>,
}
/// For a given Component, identify capability dependencies between the
/// component itself and its children. A map is returned which maps from a
/// "source" component (represented by a `ComponentRef`) to a set of "target"
/// components to which the source component provides capabilities. The targets
/// must be shut down before the source.
pub fn process_component_dependencies(
instance: &impl Component,
) -> HashMap<ComponentRef, HashSet<ComponentRef>> {
// We build up the set of (source, target) dependency edges from a variety
// of sources.
let mut dependencies = Dependencies::new();
dependencies.extend(get_dependencies_from_offers(instance).into_iter());
dependencies.extend(get_dependencies_from_environments(instance).into_iter());
dependencies.extend(get_dependencies_from_uses(instance).into_iter());
dependencies.extend(get_dependencies_from_capabilities(instance).into_iter());
// Next, we want to find any children that `self` transitively depends on,
// either directly or through other children. Any child that `self` doesn't
// transitively depend on implicitly depends on `self`.
//
// TODO(82689): This logic is likely unnecessary, as it deals with children
// that have no direct dependency links with their parent.
let self_dependencies_closure = dependency_closure(&dependencies, ComponentRef::Self_);
let implicit_edges = instance.children().into_iter().filter_map(|child| {
let component_ref = child.moniker.into();
if self_dependencies_closure.contains(&component_ref) {
None
} else {
Some((ComponentRef::Self_, component_ref))
}
});
dependencies.extend(implicit_edges);
dependencies.finalize(instance)
}
/// Given a dependency graph represented as a set of `edges`, find the set of
/// all nodes that the `start` node depends on, directly or indirectly. This
/// includes `start` itself.
fn dependency_closure(edges: &Dependencies, start: ComponentRef) -> HashSet<ComponentRef> {
let mut res = HashSet::new();
res.insert(start);
loop {
let mut entries_added = false;
for (source, target) in edges.iter() {
if !res.contains(target) {
continue;
}
if res.insert(source.clone()) {
entries_added = true
}
}
if !entries_added {
return res;
}
}
}
/// Data structure used to represent the shutdown dependency graph.
///
/// Once fully constructed, call [Dependencies::normalize] to get the list of shutdown dependency
/// edges as `(ComponentRef, ComponentRef)` pairs.
struct Dependencies {
inner: HashMap<ComponentRef, HashSet<ComponentRef>>,
}
impl fmt::Debug for Dependencies {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self.inner)
}
}
impl Dependencies {
fn new() -> Self {
Self { inner: HashMap::new() }
}
fn insert(&mut self, k: ComponentRef, v: ComponentRef) {
self.inner.entry(k).or_insert(HashSet::new()).insert(v);
}
fn extend(&mut self, edges: impl Iterator<Item = (ComponentRef, ComponentRef)>) {
for (k, v) in edges {
self.insert(k, v);
}
}
fn iter(&self) -> impl Iterator<Item = (&ComponentRef, &ComponentRef)> {
self.inner.iter().map(|(k, v)| iter::zip(iter::repeat(k), v.iter())).flatten()
}
fn into_iter(self) -> impl Iterator<Item = (ComponentRef, ComponentRef)> {
self.inner.into_iter().map(|(k, v)| iter::zip(iter::repeat(k), v.into_iter())).flatten()
}
fn finalize(
mut self,
instance: &impl Component,
) -> HashMap<ComponentRef, HashSet<ComponentRef>> {
// To ensure the shutdown job visits all the components in this realm, we need to make sure
// that every node is in the dependency list even if there were no routes into them.
self.inner.entry(ComponentRef::Self_).or_insert(HashSet::new());
for child in instance.children() {
self.inner.entry(child.moniker.into()).or_insert(HashSet::new());
}
for c in instance.capabilities() {
match c {
CapabilityDecl::Dictionary(d) => {
self.inner.entry(ComponentRef::Capability(d.name)).or_insert(HashSet::new());
}
CapabilityDecl::Storage(s) => {
self.inner.entry(ComponentRef::Capability(s.name)).or_insert(HashSet::new());
}
_ => {}
}
}
self.inner
}
}
/// Return the set of dependency relationships that can be derived from the
/// component's use declarations. For use declarations, `self` is always the
/// target.
fn get_dependencies_from_uses(instance: &impl Component) -> Dependencies {
let mut edges = Dependencies::new();
for use_ in &instance.uses() {
match use_ {
UseDecl::Protocol(UseProtocolDecl { dependency_type, .. })
| UseDecl::Service(UseServiceDecl { dependency_type, .. })
| UseDecl::Directory(UseDirectoryDecl { dependency_type, .. }) => {
if dependency_type == &DependencyType::Weak {
// Weak dependencies are ignored when determining shutdown ordering
continue;
}
}
UseDecl::Storage(_)
| UseDecl::EventStream(_)
| UseDecl::Runner(_)
| UseDecl::Config(_) => {
// Any other capability type cannot be marked as weak, so we can proceed
}
}
let dep = match use_ {
UseDecl::Service(UseServiceDecl { source, .. })
| UseDecl::Protocol(UseProtocolDecl { source, .. })
| UseDecl::Directory(UseDirectoryDecl { source, .. })
| UseDecl::EventStream(UseEventStreamDecl { source, .. })
| UseDecl::Config(UseConfigurationDecl { source, .. })
| UseDecl::Runner(UseRunnerDecl { source, .. }) => match source {
UseSource::Child(name) => instance
.find_child(name.as_str(), None)
.map(|child| ComponentRef::from(child.moniker.clone())),
UseSource::Self_ => {
if use_.is_from_dictionary() {
let path = use_.source_path();
let dictionary =
path.iter_segments().next().expect("must contain at least one segment");
Some(ComponentRef::Capability(dictionary.clone()))
} else {
// Self is the other node, no need to add a loop.
None
}
}
_ => None,
},
UseDecl::Storage(UseStorageDecl { .. }) => {
// source is always parent.
None
}
};
if let Some(dep) = dep {
edges.insert(dep, ComponentRef::Self_);
}
}
edges
}
/// Return the set of dependency relationships that can be derived from the
/// component's offer declarations. This includes both static and dynamic offers.
fn get_dependencies_from_offers(instance: &impl Component) -> Dependencies {
let mut edges = Dependencies::new();
for offer_decl in instance.offers() {
if let Some((sources, targets)) = get_dependency_from_offer(instance, &offer_decl) {
for source in sources.iter() {
for target in targets.iter() {
edges.insert(source.clone(), target.clone());
}
}
}
}
edges
}
/// Extracts a list of sources and a list of targets from a single `OfferDecl`,
/// or returns `None` if the offer has no impact on shutdown ordering. The
/// `Component` provides context that may be necessary to understand the
/// `OfferDecl`. Note that a single offer can have multiple sources/targets; for
/// instance, targeting a collection targets all the children within that
/// collection.
fn get_dependency_from_offer(
instance: &impl Component,
offer_decl: &OfferDecl,
) -> Option<(Vec<ComponentRef>, Vec<ComponentRef>)> {
// We only care about dependencies where the provider of the dependency is
// `self` or another child, otherwise the capability comes from the parent
// or component manager itself in which case the relationship is not
// relevant for ordering here.
match offer_decl {
OfferDecl::Protocol(OfferProtocolDecl {
dependency_type: DependencyType::Strong,
source,
target,
..
})
| OfferDecl::Directory(OfferDirectoryDecl {
dependency_type: DependencyType::Strong,
source,
target,
..
})
| OfferDecl::Config(OfferConfigurationDecl { source, target, .. })
| OfferDecl::Runner(OfferRunnerDecl { source, target, .. })
| OfferDecl::Resolver(OfferResolverDecl { source, target, .. })
| OfferDecl::Storage(OfferStorageDecl { source, target, .. })
| OfferDecl::Dictionary(OfferDictionaryDecl {
dependency_type: DependencyType::Strong,
source,
target,
..
}) => Some((
find_offer_sources(offer_decl, instance, source),
find_offer_targets(instance, target),
)),
OfferDecl::Service(OfferServiceDecl { source, target, .. }) => Some((
find_service_offer_sources(offer_decl, instance, source),
find_offer_targets(instance, target),
)),
OfferDecl::Protocol(OfferProtocolDecl {
dependency_type: DependencyType::Weak, ..
})
| OfferDecl::Directory(OfferDirectoryDecl {
dependency_type: DependencyType::Weak, ..
})
| OfferDecl::Dictionary(OfferDictionaryDecl {
dependency_type: DependencyType::Weak,
..
}) => {
// weak dependencies are ignored by this algorithm, because weak
// dependencies can be broken arbitrarily.
None
}
OfferDecl::EventStream(_) => {
// Event streams aren't tracked as dependencies for shutdown.
None
}
}
}
fn find_service_offer_sources(
offer: &OfferDecl,
instance: &impl Component,
source: &OfferSource,
) -> Vec<ComponentRef> {
// if the offer source is a collection, collect all children in the
// collection, otherwise defer to the "regular" method for this
match source {
OfferSource::Collection(collection_name) => instance
.children()
.into_iter()
.filter_map(|child| {
if let Some(child_collection) = child.moniker.collection() {
if child_collection == collection_name {
Some(child.moniker.clone().into())
} else {
None
}
} else {
None
}
})
.collect(),
_ => find_offer_sources(offer, instance, source),
}
}
/// Given a `Component` instance and an `OfferSource`, return the names of
/// components that match that `source`.
fn find_offer_sources(
offer: &OfferDecl,
instance: &impl Component,
source: &OfferSource,
) -> Vec<ComponentRef> {
match source {
OfferSource::Child(ChildRef { name, collection }) => {
match instance.find_child(name.as_str(), collection.as_ref()) {
Some(child) => vec![child.moniker.clone().into()],
None => {
error!(
"offer source doesn't exist: (name: {:?}, collection: {:?})",
name, collection
);
vec![]
}
}
}
OfferSource::Self_ => {
if offer.is_from_dictionary()
|| matches!(offer, OfferDecl::Dictionary(_))
|| matches!(offer, OfferDecl::Storage(_))
{
let path = offer.source_path();
let name = path.iter_segments().next().expect("must contain at least one segment");
vec![ComponentRef::Capability(name.clone())]
} else {
vec![ComponentRef::Self_]
}
}
OfferSource::Collection(_) => {
// TODO(https://fxbug.dev/42165590): Consider services routed from collections
// in shutdown order.
vec![]
}
OfferSource::Parent | OfferSource::Framework => {
// Capabilities offered by the parent or provided by the framework
// (based on some other capability) are not relevant.
vec![]
}
OfferSource::Capability(_) => {
// OfferSource::Capability(_) is used for the `StorageAdmin`
// capability. This capability is implemented by component_manager,
// and is therefore similar to a Framework capability, but it also
// depends on the underlying storage that's being administrated. In
// theory, we could add an edge from the source of that storage to
// the target of the`StorageAdmin` capability... but honestly it's
// very complex and confusing and doesn't seem worth it.
//
// We may want to reconsider this someday.
vec![]
}
OfferSource::Void => {
// Offer sources that are intentionally omitted will never match any components
vec![]
}
}
}
/// Given a `Component` instance and an `OfferTarget`, return the names of
/// components that match that `target`.
fn find_offer_targets(instance: &impl Component, target: &OfferTarget) -> Vec<ComponentRef> {
match target {
OfferTarget::Child(ChildRef { name, collection }) => {
match instance.find_child(name.as_str(), collection.as_ref()) {
Some(child) => vec![child.moniker.into()],
None => {
error!(
"offer target doesn't exist: (name: {:?}, collection: {:?})",
name, collection
);
vec![]
}
}
}
OfferTarget::Collection(collection) => instance
.children()
.into_iter()
.filter(|child| child.moniker.collection() == Some(collection))
.map(|child| child.moniker.into())
.collect(),
OfferTarget::Capability(capability) => {
// `capability` must be a dictionary.
vec![ComponentRef::Capability(capability.clone())]
}
}
}
/// Return the set of dependency relationships that can be derived from the
/// component's environment configuration. Children assigned to an environment
/// depend on components that contribute to that environment.
fn get_dependencies_from_environments(instance: &impl Component) -> Dependencies {
let env_to_sources: HashMap<Name, HashSet<ComponentRef>> = instance
.environments()
.into_iter()
.map(|env| {
let sources = find_environment_sources(instance, &env);
(env.name, sources)
})
.collect();
let mut edges = Dependencies::new();
for child in &instance.children() {
if let Some(env_name) = &child.environment_name {
if let Some(source_children) = env_to_sources.get(env_name) {
for source in source_children {
edges.insert(source.clone(), child.moniker.clone().into());
}
} else {
error!(
"environment `{}` from child `{}` is not a valid environment",
env_name, child.moniker
)
}
}
}
edges
}
/// Return the set of dependency relationships that can be derived from the
/// component's capabilities declarations. For use declarations, `self` is always the
/// target.
fn get_dependencies_from_capabilities(instance: &impl Component) -> Dependencies {
let mut edges = Dependencies::new();
for capability in &instance.capabilities() {
match capability {
CapabilityDecl::Dictionary(DictionaryDecl {
name,
source: Some(source),
source_dictionary: Some(source_dictionary),
..
}) => {
let source = match source {
DictionarySource::Parent => None,
DictionarySource::Child(ChildRef { name, collection }) => {
match instance.find_child(name.as_str(), collection.as_ref()) {
Some(child) => Some(child.moniker.clone().into()),
None => {
error!(
"dictionary source doesn't exist: (name: {:?}, collection: {:?})",
name, collection
);
None
}
}
}
DictionarySource::Self_ => {
let dictionary = source_dictionary
.iter_segments()
.next()
.expect("must contain at least one segment");
Some(ComponentRef::Capability(dictionary.clone()))
}
DictionarySource::Program => Some(ComponentRef::Self_),
};
if let Some(source) = source {
edges.insert(source, ComponentRef::Capability(name.clone()));
}
}
CapabilityDecl::Storage(StorageDecl { name, source, .. }) => {
let source = match source {
StorageDirectorySource::Parent => None,
StorageDirectorySource::Child(name) => {
match instance.find_child(name.as_str(), None) {
Some(child) => Some(child.moniker.clone().into()),
None => {
error!("storage source doesn't exist: (name: {:?})", name);
None
}
}
}
StorageDirectorySource::Self_ => Some(ComponentRef::Self_),
};
if let Some(source) = source {
edges.insert(source, ComponentRef::Capability(name.clone()));
}
}
_ => {}
}
}
edges
}
/// Given a `Component` instance and an environment, return the names of
/// components that provide runners, resolvers, or debug_capabilities for that
/// environment.
fn find_environment_sources(
instance: &impl Component,
env: &EnvironmentDecl,
) -> HashSet<ComponentRef> {
// Get all the `RegistrationSources` for the runners, resolvers, and
// debug_capabilities in this environment.
let registration_sources = env
.runners
.iter()
.map(|r| &r.source)
.chain(env.resolvers.iter().map(|r| &r.source))
.chain(env.debug_capabilities.iter().map(|r| r.source()));
// Turn the shutdown-relevant sources into `ComponentRef`s.
registration_sources
.flat_map(|source| match source {
RegistrationSource::Self_ => vec![ComponentRef::Self_],
RegistrationSource::Child(child_name) => {
match instance.find_child(child_name.as_str(), None) {
Some(child) => vec![child.moniker.into()],
None => {
error!(
"source for environment {:?} doesn't exist: (name: {:?})",
env.name, child_name,
);
vec![]
}
}
}
RegistrationSource::Parent => vec![],
})
.collect()
}
#[cfg(test)]
mod tests {
use {
super::*,
crate::model::{
actions::{test_utils::MockAction, StopAction},
component::StartReason,
testing::{
test_helpers::{
component_decl_with_test_runner, execution_is_shut_down, has_child,
ActionsTest, ComponentInfo,
},
test_hook::Lifecycle,
},
},
async_utils::PollExt,
cm_rust::{ComponentDecl, DependencyType, ExposeSource, ExposeTarget},
cm_rust_testing::*,
cm_types::AllowedOffers,
errors::StopActionError,
fidl_fuchsia_component as fcomponent, fidl_fuchsia_component_decl as fdecl,
fuchsia_async as fasync,
maplit::{btreeset, hashmap, hashset},
moniker::Moniker,
std::collections::BTreeSet,
test_case::test_case,
};
/// Implementation of `super::Component` based on a `ComponentDecl` and
/// minimal information about runtime state.
struct FakeComponent {
decl: ComponentDecl,
dynamic_children: Vec<Child>,
dynamic_offers: Vec<OfferDecl>,
}
impl FakeComponent {
/// Returns a `FakeComponent` with no dynamic children or offers.
fn from_decl(decl: ComponentDecl) -> Self {
Self { decl, dynamic_children: vec![], dynamic_offers: vec![] }
}
}
impl Component for FakeComponent {
fn uses(&self) -> Vec<UseDecl> {
self.decl.uses.clone()
}
fn exposes(&self) -> Vec<ExposeDecl> {
self.decl.exposes.clone()
}
fn offers(&self) -> Vec<OfferDecl> {
self.decl.offers.iter().cloned().chain(self.dynamic_offers.iter().cloned()).collect()
}
fn capabilities(&self) -> Vec<CapabilityDecl> {
self.decl.capabilities.clone()
}
fn collections(&self) -> Vec<cm_rust::CollectionDecl> {
self.decl.collections.clone()
}
fn environments(&self) -> Vec<cm_rust::EnvironmentDecl> {
self.decl.environments.clone()
}
fn children(&self) -> Vec<Child> {
self.decl
.children
.iter()
.map(|c| Child {
moniker: ChildName::try_new(c.name.as_str(), None)
.expect("children should have valid monikers"),
environment_name: c.environment.clone(),
})
.chain(self.dynamic_children.iter().cloned())
.collect()
}
}
// TODO(jmatt) Add tests for all capability types
/// Returns a `ComponentRef` for a child by parsing the moniker. Panics if
/// the moniker is malformed.
fn child(moniker: &str) -> ComponentRef {
ChildName::try_from(moniker).unwrap().into()
}
fn capability(name: &str) -> ComponentRef {
ComponentRef::Capability(name.parse().unwrap())
}
#[fuchsia::test]
fn test_service_from_self() {
let decl = ComponentDeclBuilder::new()
.offer(
OfferBuilder::protocol()
.name("serviceSelf")
.source(OfferSource::Self_)
.target_static_child("childA"),
)
.child_default("childA")
.build();
pretty_assertions::assert_eq!(
hashmap! {
ComponentRef::Self_ => hashset![child("childA")],
child("childA") => hashset![],
},
process_component_dependencies(&FakeComponent::from_decl(decl))
)
}
#[test_case(DependencyType::Weak)]
fn test_weak_service_from_self(weak_dep: DependencyType) {
let decl = ComponentDeclBuilder::new()
.offer(
OfferBuilder::protocol()
.name("serviceSelf")
.source(OfferSource::Self_)
.target_static_child("childA")
.dependency(weak_dep),
)
.child_default("childA")
.build();
pretty_assertions::assert_eq!(
hashmap! {
ComponentRef::Self_ => hashset![child("childA")],
child("childA") => hashset![],
},
process_component_dependencies(&FakeComponent::from_decl(decl))
)
}
#[fuchsia::test]
fn test_service_from_child() {
let decl = ComponentDeclBuilder::new()
.expose(
ExposeBuilder::protocol()
.name("serviceFromChild")
.source_static_child("childA")
.target(ExposeTarget::Parent),
)
.child_default("childA")
.build();
pretty_assertions::assert_eq!(
hashmap! {
ComponentRef::Self_ => hashset![child("childA")],
child("childA") => hashset![],
},
process_component_dependencies(&FakeComponent::from_decl(decl))
)
}
#[fuchsia::test]
fn test_single_dependency() {
let decl = ComponentDeclBuilder::new()
.offer(
OfferBuilder::protocol()
.name("serviceParent")
.source(OfferSource::Self_)
.target_static_child("childA"),
)
.offer(
OfferBuilder::protocol()
.name("childBOffer")
.target_name("serviceSibling")
.source_static_child("childB")
.target_static_child("childA"),
)
.child_default("childA")
.child_default("childB")
.build();
pretty_assertions::assert_eq!(
hashmap! {
ComponentRef::Self_ => hashset![child("childA"), child("childB")],
child("childA") => hashset![],
child("childB") => hashset![child("childA")],
},
process_component_dependencies(&FakeComponent::from_decl(decl))
)
}
#[fuchsia::test]
fn test_dictionary_dependency() {
let decl = ComponentDeclBuilder::new()
.dictionary_default("dict")
.protocol_default("serviceA")
.offer(
OfferBuilder::protocol()
.name("serviceA")
.source(OfferSource::Self_)
.target(OfferTarget::Capability("dict".parse().unwrap())),
)
.offer(
OfferBuilder::protocol()
.name("serviceB")
.source_static_child("childA")
.target(OfferTarget::Capability("dict".parse().unwrap())),
)
.offer(
OfferBuilder::protocol()
.name("serviceB")
.source(OfferSource::Self_)
.from_dictionary("dict")
.target_static_child("childB"),
)
.child_default("childA")
.child_default("childB")
.build();
pretty_assertions::assert_eq!(
hashmap! {
ComponentRef::Self_ => hashset![child("childA"), child("childB"), capability("dict")],
child("childA") => hashset![capability("dict")],
capability("dict") => hashset![child("childB")],
child("childB") => hashset![],
},
process_component_dependencies(&FakeComponent::from_decl(decl))
)
}
#[fuchsia::test]
fn test_dictionary_dependency_with_extension() {
let decl = ComponentDeclBuilder::new()
// Chain together two dictionaries with extension.
.capability(
CapabilityBuilder::dictionary()
.name("dict")
.source_dictionary(DictionarySource::Self_, "other_dict"),
)
.capability(CapabilityBuilder::dictionary().name("other_dict").source_dictionary(
DictionarySource::Child(ChildRef {
name: "childA".parse().unwrap(),
collection: None,
}),
"remote/dict",
))
.offer(
OfferBuilder::protocol()
.name("serviceA")
.source_static_child("childB")
.target(OfferTarget::Capability("other_dict".parse().unwrap())),
)
.offer(
OfferBuilder::dictionary()
.name("dict")
.source(OfferSource::Self_)
.target_static_child("childC"),
)
.child_default("childA")
.child_default("childB")
.child_default("childC")
.build();
pretty_assertions::assert_eq!(
hashmap! {
ComponentRef::Self_ => hashset![child("childA"), child("childB"), child("childC")],
child("childA") => hashset![capability("other_dict")],
child("childB") => hashset![capability("other_dict")],
capability("other_dict") => hashset![capability("dict")],
capability("dict") => hashset![child("childC")],
child("childC") => hashset![],
},
process_component_dependencies(&FakeComponent::from_decl(decl))
)
}
#[fuchsia::test]
fn test_environment_with_runner_from_parent() {
let decl = ComponentDeclBuilder::new()
.environment(EnvironmentBuilder::new().name("env").runner(
cm_rust::RunnerRegistration {
source: RegistrationSource::Parent,
source_name: "foo".parse().unwrap(),
target_name: "foo".parse().unwrap(),
},
))
.child_default("childA")
.child(ChildBuilder::new().name("childB").environment("env"))
.build();
pretty_assertions::assert_eq!(
hashmap! {
ComponentRef::Self_ => hashset![child("childA"), child("childB")],
child("childA") => hashset![],
child("childB") => hashset![],
},
process_component_dependencies(&FakeComponent::from_decl(decl))
)
}
#[fuchsia::test]
fn test_environment_with_runner_from_self() {
let decl = ComponentDeclBuilder::new()
.environment(EnvironmentBuilder::new().name("env").runner(
cm_rust::RunnerRegistration {
source: RegistrationSource::Self_,
source_name: "foo".parse().unwrap(),
target_name: "foo".parse().unwrap(),
},
))
.child_default("childA")
.child(ChildBuilder::new().name("childB").environment("env"))
.build();
pretty_assertions::assert_eq!(
hashmap! {
ComponentRef::Self_ => hashset![child("childA"), child("childB")],
child("childA") => hashset![],
child("childB") => hashset![],
},
process_component_dependencies(&FakeComponent::from_decl(decl))
)
}
#[fuchsia::test]
fn test_environment_with_runner_from_child() {
let decl = ComponentDeclBuilder::new()
.environment(EnvironmentBuilder::new().name("env").runner(
cm_rust::RunnerRegistration {
source: RegistrationSource::Child("childA".to_string()),
source_name: "foo".parse().unwrap(),
target_name: "foo".parse().unwrap(),
},
))
.child_default("childA")
.child(ChildBuilder::new().name("childB").environment("env"))
.build();
pretty_assertions::assert_eq!(
hashmap! {
ComponentRef::Self_ => hashset![child("childA"), child("childB")],
child("childA") => hashset![child("childB")],
child("childB") => hashset![],
},
process_component_dependencies(&FakeComponent::from_decl(decl))
)
}
#[fuchsia::test]
fn test_environment_with_runner_from_child_to_collection() {
let decl = ComponentDeclBuilder::new()
.environment(EnvironmentBuilder::new().name("env").runner(
cm_rust::RunnerRegistration {
source: RegistrationSource::Child("childA".to_string()),
source_name: "foo".parse().unwrap(),
target_name: "foo".parse().unwrap(),
},
))
.collection(CollectionBuilder::new().name("coll").environment("env"))
.child_default("childA")
.build();
let instance = FakeComponent {
decl,
dynamic_children: vec![
// NOTE: The environment must be set in the `Child`, even though
// it can theoretically be inferred from the collection
// declaration.
Child {
moniker: "coll:dyn1".try_into().unwrap(),
environment_name: Some("env".parse().unwrap()),
},
Child {
moniker: "coll:dyn2".try_into().unwrap(),
environment_name: Some("env".parse().unwrap()),
},
],
dynamic_offers: vec![],
};
pretty_assertions::assert_eq!(
hashmap! {
ComponentRef::Self_ => hashset![
child("childA"),
child("coll:dyn1"),
child("coll:dyn2"),
],
child("childA") => hashset![
child("coll:dyn1"),
child("coll:dyn2"),
],
child("coll:dyn1") => hashset![],
child("coll:dyn2") => hashset![],
},
process_component_dependencies(&instance)
)
}
#[fuchsia::test]
fn test_chained_environments() {
let decl = ComponentDeclBuilder::new()
.environment(EnvironmentBuilder::new().name("env").runner(
cm_rust::RunnerRegistration {
source: RegistrationSource::Child("childA".to_string()),
source_name: "foo".parse().unwrap(),
target_name: "foo".parse().unwrap(),
},
))
.environment(EnvironmentBuilder::new().name("env2").runner(
cm_rust::RunnerRegistration {
source: RegistrationSource::Child("childB".to_string()),
source_name: "bar".parse().unwrap(),
target_name: "bar".parse().unwrap(),
},
))
.child_default("childA")
.child(ChildBuilder::new().name("childB").environment("env"))
.child(ChildBuilder::new().name("childC").environment("env2"))
.build();
pretty_assertions::assert_eq!(
hashmap! {
ComponentRef::Self_ => hashset![
child("childA"),
child("childB"),
child("childC")
],
child("childA") => hashset![child("childB")],
child("childB") => hashset![child("childC")],
child("childC") => hashset![],
},
process_component_dependencies(&FakeComponent::from_decl(decl))
)
}
#[fuchsia::test]
fn test_environment_and_offer() {
let decl = ComponentDeclBuilder::new()
.offer(
OfferBuilder::protocol()
.name("childBOffer")
.target_name("serviceSibling")
.source_static_child("childB")
.target_static_child("childC"),
)
.environment(EnvironmentBuilder::new().name("env").runner(
cm_rust::RunnerRegistration {
source: RegistrationSource::Child("childA".into()),
source_name: "foo".parse().unwrap(),
target_name: "foo".parse().unwrap(),
},
))
.child_default("childA")
.child(ChildBuilder::new().name("childB").environment("env"))
.child_default("childC")
.build();
pretty_assertions::assert_eq!(
hashmap! {
ComponentRef::Self_ => hashset![
child("childA"),
child("childB"),
child("childC")
],
child("childA") => hashset![child("childB")],
child("childB") => hashset![child("childC")],
child("childC") => hashset![],
},
process_component_dependencies(&FakeComponent::from_decl(decl))
)
}
#[fuchsia::test]
fn test_environment_with_resolver_from_parent() {
let decl = ComponentDeclBuilder::new()
.environment(EnvironmentBuilder::new().name("resolver_env").resolver(
cm_rust::ResolverRegistration {
source: RegistrationSource::Parent,
resolver: "foo".parse().unwrap(),
scheme: "httweeeeees".into(),
},
))
.child_default("childA")
.child(ChildBuilder::new().name("childB").environment("resolver_env"))
.build();
pretty_assertions::assert_eq!(
hashmap! {
ComponentRef::Self_ => hashset![child("childA"), child("childB")],
child("childA") => hashset![],
child("childB") => hashset![],
},
process_component_dependencies(&FakeComponent::from_decl(decl))
)
}
#[fuchsia::test]
fn test_environment_with_resolver_from_child() {
let decl = ComponentDeclBuilder::new()
.environment(EnvironmentBuilder::new().name("resolver_env").resolver(
cm_rust::ResolverRegistration {
source: RegistrationSource::Child("childA".to_string()),
resolver: "foo".parse().unwrap(),
scheme: "httweeeeees".into(),
},
))
.child_default("childA")
.child(ChildBuilder::new().name("childB").environment("resolver_env"))
.build();
pretty_assertions::assert_eq!(
hashmap! {
ComponentRef::Self_ => hashset![child("childA"), child("childB")],
child("childA") => hashset![child("childB")],
child("childB") => hashset![],
},
process_component_dependencies(&FakeComponent::from_decl(decl))
)
}
// add test where B depends on A via environment and C depends on B via environment
#[fuchsia::test]
fn test_environment_with_chain_of_resolvers() {
let decl = ComponentDeclBuilder::new()
.environment(EnvironmentBuilder::new().name("env1").resolver(
cm_rust::ResolverRegistration {
source: RegistrationSource::Child("childA".to_string()),
resolver: "foo".parse().unwrap(),
scheme: "httweeeeees".into(),
},
))
.environment(EnvironmentBuilder::new().name("env2").resolver(
cm_rust::ResolverRegistration {
source: RegistrationSource::Child("childB".to_string()),
resolver: "bar".parse().unwrap(),
scheme: "httweeeeee".into(),
},
))
.child_default("childA")
.child(ChildBuilder::new().name("childB").environment("env1"))
.child(ChildBuilder::new().name("childC").environment("env2"))
.build();
pretty_assertions::assert_eq!(
hashmap! {
ComponentRef::Self_ => hashset![
child("childA"),
child("childB"),
child("childC")
],
child("childA") => hashset![child("childB")],
child("childB") => hashset![child("childC")],
child("childC") => hashset![],
},
process_component_dependencies(&FakeComponent::from_decl(decl))
)
}
#[fuchsia::test]
fn test_environment_with_resolver_and_runner_from_child() {
let decl = ComponentDeclBuilder::new()
.environment(
EnvironmentBuilder::new()
.name("multi_env")
.resolver(cm_rust::ResolverRegistration {
source: RegistrationSource::Child("childA".to_string()),
resolver: "foo".parse().unwrap(),
scheme: "httweeeeees".into(),
})
.runner(cm_rust::RunnerRegistration {
source: RegistrationSource::Child("childB".to_string()),
source_name: "bar".parse().unwrap(),
target_name: "bar".parse().unwrap(),
}),
)
.child_default("childA")
.child_default("childB")
.child(ChildBuilder::new().name("childC").environment("multi_env"))
.build();
pretty_assertions::assert_eq!(
hashmap! {
ComponentRef::Self_ => hashset![
child("childA"),
child("childB"),
child("childC")
],
child("childA") => hashset![child("childC")],
child("childB") => hashset![child("childC")],
child("childC") => hashset![],
},
process_component_dependencies(&FakeComponent::from_decl(decl))
)
}
#[fuchsia::test]
fn test_environment_with_collection_resolver_from_child() {
let decl = ComponentDeclBuilder::new()
.environment(EnvironmentBuilder::new().name("resolver_env").resolver(
cm_rust::ResolverRegistration {
source: RegistrationSource::Child("childA".to_string()),
resolver: "foo".parse().unwrap(),
scheme: "httweeeeees".into(),
},
))
.child_default("childA")
.collection(CollectionBuilder::new().name("coll").environment("resolver_env"))
.build();
let instance = FakeComponent {
decl,
dynamic_children: vec![
// NOTE: The environment must be set in the `Child`, even though
// it can theoretically be inferred from the collection declaration.
Child {
moniker: "coll:dyn1".try_into().unwrap(),
environment_name: Some("resolver_env".parse().unwrap()),
},
Child {
moniker: "coll:dyn2".try_into().unwrap(),
environment_name: Some("resolver_env".parse().unwrap()),
},
],
dynamic_offers: vec![],
};
pretty_assertions::assert_eq!(
hashmap! {
ComponentRef::Self_ => hashset![
child("childA"),
child("coll:dyn1"),
child("coll:dyn2"),
],
child("childA") => hashset![child("coll:dyn1"), child("coll:dyn2")],
child("coll:dyn1") => hashset![],
child("coll:dyn2") => hashset![],
},
process_component_dependencies(&instance)
)
}
#[fuchsia::test]
fn test_dynamic_offers_within_collection() {
let decl = ComponentDeclBuilder::new()
.child_default("childA")
.collection_default("coll")
.offer(
OfferBuilder::directory()
.name("some_dir")
.source(OfferSource::Child(ChildRef {
name: "childA".parse().unwrap(),
collection: None,
}))
.target(OfferTarget::Collection("coll".parse().unwrap())),
)
.build();
let instance = FakeComponent {
decl,
dynamic_children: vec![
Child { moniker: "coll:dyn1".try_into().unwrap(), environment_name: None },
Child { moniker: "coll:dyn2".try_into().unwrap(), environment_name: None },
Child { moniker: "coll:dyn3".try_into().unwrap(), environment_name: None },
Child { moniker: "coll:dyn4".try_into().unwrap(), environment_name: None },
],
dynamic_offers: vec![
OfferBuilder::protocol()
.name("test.protocol")
.target_name("test.protocol")
.source(OfferSource::Child(ChildRef {
name: "dyn1".parse().unwrap(),
collection: Some("coll".parse().unwrap()),
}))
.target(OfferTarget::Child(ChildRef {
name: "dyn2".parse().unwrap(),
collection: Some("coll".parse().unwrap()),
}))
.build(),
OfferBuilder::protocol()
.name("test.protocol")
.target_name("test.protocol")
.source(OfferSource::Child(ChildRef {
name: "dyn1".parse().unwrap(),
collection: Some("coll".parse().unwrap()),
}))
.target(OfferTarget::Child(ChildRef {
name: "dyn3".parse().unwrap(),
collection: Some("coll".parse().unwrap()),
}))
.build(),
],
};
pretty_assertions::assert_eq!(
hashmap! {
ComponentRef::Self_ => hashset![
child("childA"),
child("coll:dyn1"),
child("coll:dyn2"),
child("coll:dyn3"),
child("coll:dyn4"),
],
child("childA") => hashset![
child("coll:dyn1"),
child("coll:dyn2"),
child("coll:dyn3"),
child("coll:dyn4"),
],
child("coll:dyn1") => hashset![child("coll:dyn2"), child("coll:dyn3")],
child("coll:dyn2") => hashset![],
child("coll:dyn3") => hashset![],
child("coll:dyn4") => hashset![],
},
process_component_dependencies(&instance)
)
}
#[fuchsia::test]
fn test_dynamic_offers_between_collections() {
let decl = ComponentDeclBuilder::new()
.collection_default("coll1")
.collection_default("coll2")
.build();
let instance = FakeComponent {
decl,
dynamic_children: vec![
Child { moniker: "coll1:dyn1".try_into().unwrap(), environment_name: None },
Child { moniker: "coll1:dyn2".try_into().unwrap(), environment_name: None },
Child { moniker: "coll2:dyn1".try_into().unwrap(), environment_name: None },
Child { moniker: "coll2:dyn2".try_into().unwrap(), environment_name: None },
],
dynamic_offers: vec![
OfferBuilder::protocol()
.name("test.protocol")
.source(OfferSource::Child(ChildRef {
name: "dyn1".parse().unwrap(),
collection: Some("coll1".parse().unwrap()),
}))
.target(OfferTarget::Child(ChildRef {
name: "dyn1".parse().unwrap(),
collection: Some("coll2".parse().unwrap()),
}))
.build(),
OfferBuilder::protocol()
.name("test.protocol")
.source(OfferSource::Child(ChildRef {
name: "dyn2".parse().unwrap(),
collection: Some("coll2".parse().unwrap()),
}))
.target(OfferTarget::Child(ChildRef {
name: "dyn1".parse().unwrap(),
collection: Some("coll1".parse().unwrap()),
}))
.build(),
],
};
pretty_assertions::assert_eq!(
hashmap! {
ComponentRef::Self_ => hashset![
child("coll1:dyn1"),
child("coll1:dyn2"),
child("coll2:dyn1"),
child("coll2:dyn2"),
],
child("coll1:dyn1") => hashset![child("coll2:dyn1")],
child("coll1:dyn2") => hashset![],
child("coll2:dyn1") => hashset![],
child("coll2:dyn2") => hashset![child("coll1:dyn1")],
},
process_component_dependencies(&instance)
)
}
#[fuchsia::test]
fn test_dynamic_offer_from_parent() {
let decl = ComponentDeclBuilder::new().collection_default("coll").build();
let instance = FakeComponent {
decl,
dynamic_children: vec![
Child { moniker: "coll:dyn1".try_into().unwrap(), environment_name: None },
Child { moniker: "coll:dyn2".try_into().unwrap(), environment_name: None },
],
dynamic_offers: vec![OfferBuilder::protocol()
.name("test.protocol")
.source(OfferSource::Parent)
.target(OfferTarget::Child(ChildRef {
name: "dyn1".parse().unwrap(),
collection: Some("coll".parse().unwrap()),
}))
.build()],
};
pretty_assertions::assert_eq!(
hashmap! {
ComponentRef::Self_ => hashset![
child("coll:dyn1"),
child("coll:dyn2"),
],
child("coll:dyn1") => hashset![],
child("coll:dyn2") => hashset![],
},
process_component_dependencies(&instance)
)
}
#[fuchsia::test]
fn test_dynamic_offer_from_self() {
let decl = ComponentDeclBuilder::new().collection_default("coll").build();
let instance = FakeComponent {
decl,
dynamic_children: vec![
Child { moniker: "coll:dyn1".try_into().unwrap(), environment_name: None },
Child { moniker: "coll:dyn2".try_into().unwrap(), environment_name: None },
],
dynamic_offers: vec![OfferBuilder::protocol()
.name("test.protocol")
.source(OfferSource::Self_)
.target(OfferTarget::Child(ChildRef {
name: "dyn1".parse().unwrap(),
collection: Some("coll".parse().unwrap()),
}))
.build()],
};
pretty_assertions::assert_eq!(
hashmap! {
ComponentRef::Self_ => hashset![
child("coll:dyn1"),
child("coll:dyn2"),
],
child("coll:dyn1") => hashset![],
child("coll:dyn2") => hashset![],
},
process_component_dependencies(&instance)
)
}
#[fuchsia::test]
fn test_dynamic_offer_from_static_child() {
let decl = ComponentDeclBuilder::new()
.child_default("childA")
.child_default("childB")
.collection_default("coll")
.build();
let instance = FakeComponent {
decl,
dynamic_children: vec![
Child { moniker: "coll:dyn1".try_into().unwrap(), environment_name: None },
Child { moniker: "coll:dyn2".try_into().unwrap(), environment_name: None },
],
dynamic_offers: vec![OfferBuilder::protocol()
.name("test.protocol")
.source_static_child("childA")
.target(OfferTarget::Child(ChildRef {
name: "dyn1".parse().unwrap(),
collection: Some("coll".parse().unwrap()),
}))
.build()],
};
pretty_assertions::assert_eq!(
hashmap! {
ComponentRef::Self_ => hashset![
child("childA"),
child("childB"),
child("coll:dyn1"),
child("coll:dyn2"),
],
child("childA") => hashset![child("coll:dyn1")],
child("childB") => hashset![],
child("coll:dyn1") => hashset![],
child("coll:dyn2") => hashset![],
},
process_component_dependencies(&instance)
)
}
#[test_case(DependencyType::Weak)]
fn test_single_weak_dependency(weak_dep: DependencyType) {
let decl = ComponentDeclBuilder::new()
.offer(
OfferBuilder::protocol()
.name("serviceSelf")
.source(OfferSource::Self_)
.target_static_child("childA")
.dependency(weak_dep.clone()),
)
.offer(
OfferBuilder::protocol()
.name("childBOffer")
.target_name("serviceSibling")
.source_static_child("childB")
.target_static_child("childA")
.dependency(weak_dep.clone()),
)
.child_default("childA")
.child_default("childB")
.build();
pretty_assertions::assert_eq!(
hashmap! {
ComponentRef::Self_ => hashset![child("childA"), child("childB")],
child("childA") => hashset![],
child("childB") => hashset![],
},
process_component_dependencies(&FakeComponent::from_decl(decl))
)
}
#[fuchsia::test]
fn test_multiple_dependencies_same_source() {
let decl = ComponentDeclBuilder::new()
.offer(
OfferBuilder::protocol()
.name("serviceSelf")
.source(OfferSource::Self_)
.target_static_child("childA"),
)
.offer(
OfferBuilder::protocol()
.name("childBOffer")
.target_name("serviceSibling")
.source_static_child("childB")
.target_static_child("childA"),
)
.offer(
OfferBuilder::protocol()
.name("childBOtherOffer")
.target_name("serviceOtherSibling")
.source_static_child("childB")
.target_static_child("childA"),
)
.child_default("childA")
.child_default("childB")
.build();
pretty_assertions::assert_eq!(
hashmap! {
ComponentRef::Self_ => hashset![child("childA"), child("childB")],
child("childA") => hashset![],
child("childB") => hashset![child("childA")],
},
process_component_dependencies(&FakeComponent::from_decl(decl))
)
}
#[fuchsia::test]
fn test_multiple_dependents_same_source() {
let decl = ComponentDeclBuilder::new()
.offer(
OfferBuilder::protocol()
.name("childBOffer")
.target_name("serviceSibling")
.source_static_child("childB")
.target_static_child("childA"),
)
.offer(
OfferBuilder::protocol()
.name("childBToC")
.target_name("serviceSibling")
.source_static_child("childB")
.target_static_child("childC"),
)
.child_default("childA")
.child_default("childB")
.child_default("childC")
.build();
pretty_assertions::assert_eq!(
hashmap! {
ComponentRef::Self_ => hashset![
child("childA"),
child("childB"),
child("childC"),
],
child("childA") => hashset![],
child("childB") => hashset![child("childA"), child("childC")],
child("childC") => hashset![],
},
process_component_dependencies(&FakeComponent::from_decl(decl))
)
}
#[test_case(DependencyType::Weak)]
fn test_multiple_dependencies(weak_dep: DependencyType) {
let decl = ComponentDeclBuilder::new()
.offer(
OfferBuilder::protocol()
.name("childBOffer")
.target_name("serviceSibling")
.source_static_child("childA")
.target_static_child("childC"),
)
.offer(
OfferBuilder::protocol()
.name("childBToC")
.target_name("serviceSibling")
.source_static_child("childB")
.target_static_child("childC"),
)
.offer(
OfferBuilder::protocol()
.name("childCToA")
.target_name("serviceSibling")
.source_static_child("childC")
.target_static_child("childA")
.dependency(weak_dep),
)
.child_default("childA")
.child_default("childB")
.child_default("childC")
.build();
pretty_assertions::assert_eq!(
hashmap! {
ComponentRef::Self_ => hashset![
child("childA"),
child("childB"),
child("childC"),
],
child("childA") => hashset![child("childC")],
child("childB") => hashset![child("childC")],
child("childC") => hashset![],
},
process_component_dependencies(&FakeComponent::from_decl(decl))
)
}
#[fuchsia::test]
fn test_component_is_source_and_target() {
let decl = ComponentDeclBuilder::new()
.offer(
OfferBuilder::protocol()
.name("childBOffer")
.target_name("serviceSibling")
.source_static_child("childA")
.target_static_child("childB"),
)
.offer(
OfferBuilder::protocol()
.name("childBToC")
.target_name("serviceSibling")
.source_static_child("childB")
.target_static_child("childC"),
)
.child_default("childA")
.child_default("childB")
.child_default("childC")
.build();
pretty_assertions::assert_eq!(
hashmap! {
ComponentRef::Self_ => hashset![
child("childA"),
child("childB"),
child("childC"),
],
child("childA") => hashset![child("childB")],
child("childB") => hashset![child("childC")],
child("childC") => hashset![],
},
process_component_dependencies(&FakeComponent::from_decl(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
/// \ /
/// *>~~>*
#[fuchsia::test]
fn test_complex_routing() {
let decl = ComponentDeclBuilder::new()
.offer(
OfferBuilder::protocol()
.name("childAService")
.source_static_child("childA")
.target_static_child("childB"),
)
.offer(
OfferBuilder::protocol()
.name("childAService")
.source_static_child("childA")
.target_static_child("childC"),
)
.offer(
OfferBuilder::protocol()
.name("childBService")
.source_static_child("childB")
.target_static_child("childD"),
)
.offer(
OfferBuilder::protocol()
.name("childAService")
.source_static_child("childC")
.target_static_child("childD"),
)
.offer(
OfferBuilder::protocol()
.name("childAService")
.source_static_child("childC")
.target_static_child("childE"),
)
.child_default("childA")
.child_default("childB")
.child_default("childC")
.child_default("childD")
.child_default("childE")
.build();
pretty_assertions::assert_eq!(
hashmap! {
ComponentRef::Self_ => hashset![
child("childA"),
child("childB"),
child("childC"),
child("childD"),
child("childE"),
],
child("childA") => hashset![child("childB"), child("childC")],
child("childB") => hashset![child("childD")],
child("childC") => hashset![child("childD"), child("childE")],
child("childD") => hashset![],
child("childE") => hashset![],
},
process_component_dependencies(&FakeComponent::from_decl(decl))
)
}
#[fuchsia::test]
fn test_target_does_not_exist() {
// This declaration is invalid because the offer target doesn't exist
let decl = ComponentDeclBuilder::new()
.offer(
OfferBuilder::protocol()
.name("childBOffer")
.target_name("serviceSibling")
.source_static_child("childA")
.target_static_child("childB"),
)
.child_default("childA")
.build();
pretty_assertions::assert_eq!(
hashmap! {
ComponentRef::Self_ => hashset![child("childA")],
child("childA") => hashset![],
},
process_component_dependencies(&FakeComponent::from_decl(decl))
);
}
#[fuchsia::test]
fn test_service_from_collection() {
let decl = ComponentDeclBuilder::new()
.collection_default("coll")
.child_default("static_child")
.offer(
OfferBuilder::service()
.name("service_capability")
.source(OfferSource::Collection("coll".parse().unwrap()))
.target_static_child("static_child"),
)
.build();
let dynamic_child = ChildName::try_new("dynamic_child", Some("coll")).unwrap();
let mut fake = FakeComponent::from_decl(decl);
fake.dynamic_children
.push(Child { moniker: dynamic_child.clone(), environment_name: None });
pretty_assertions::assert_eq!(
hashmap! {
ComponentRef::Self_ => hashset![
ComponentRef::Child(dynamic_child.clone()),child("static_child")
],
ComponentRef::Child(dynamic_child) => hashset![child("static_child")],
child("static_child") => hashset![],
},
process_component_dependencies(&fake)
);
}
#[fuchsia::test]
fn test_service_from_collection_with_multiple_instances() {
let decl = ComponentDeclBuilder::new()
.collection_default("coll")
.child_default("static_child")
.offer(
OfferBuilder::service()
.name("service_capability")
.source(OfferSource::Collection("coll".parse().unwrap()))
.target_static_child("static_child"),
)
.build();
let dynamic_child1 = ChildName::try_new("dynamic_child1", Some("coll")).unwrap();
let dynamic_child2 = ChildName::try_new("dynamic_child2", Some("coll")).unwrap();
let mut fake = FakeComponent::from_decl(decl);
fake.dynamic_children
.push(Child { moniker: dynamic_child1.clone(), environment_name: None });
fake.dynamic_children
.push(Child { moniker: dynamic_child2.clone(), environment_name: None });
pretty_assertions::assert_eq!(
hashmap! {
ComponentRef::Self_ => hashset![
ComponentRef::Child(dynamic_child1.clone()),
ComponentRef::Child(dynamic_child2.clone()),
child("static_child")
],
ComponentRef::Child(dynamic_child1) => hashset![child("static_child")],
ComponentRef::Child(dynamic_child2) => hashset![child("static_child")],
child("static_child") => hashset![],
},
process_component_dependencies(&fake)
);
}
#[fuchsia::test]
fn test_service_dependency_between_collections() {
let decl = ComponentDeclBuilder::new()
.collection_default("coll1")
.collection_default("coll2")
.offer(
OfferBuilder::service()
.name("fuchsia.service.FakeService")
.source(OfferSource::Collection("coll1".parse().unwrap()))
.target(OfferTarget::Collection("coll2".parse().unwrap())),
)
.build();
let source_child1 = ChildName::try_new("source_child1", Some("coll1")).unwrap();
let source_child2 = ChildName::try_new("source_child2", Some("coll1")).unwrap();
let target_child1 = ChildName::try_new("target_child1", Some("coll2")).unwrap();
let target_child2 = ChildName::try_new("target_child2", Some("coll2")).unwrap();
let mut fake = FakeComponent::from_decl(decl);
fake.dynamic_children
.push(Child { moniker: source_child1.clone(), environment_name: None });
fake.dynamic_children
.push(Child { moniker: source_child2.clone(), environment_name: None });
fake.dynamic_children
.push(Child { moniker: target_child1.clone(), environment_name: None });
fake.dynamic_children
.push(Child { moniker: target_child2.clone(), environment_name: None });
pretty_assertions::assert_eq! {
hashmap! {
ComponentRef::Self_ => hashset![
ComponentRef::Child(source_child1.clone()),
ComponentRef::Child(source_child2.clone()),
ComponentRef::Child(target_child1.clone()),
ComponentRef::Child(target_child2.clone()),
],
ComponentRef::Child(source_child1) => hashset! [
ComponentRef::Child(target_child1.clone()),
ComponentRef::Child(target_child2.clone()),
],
ComponentRef::Child(source_child2) => hashset! [
ComponentRef::Child(target_child1.clone()),
ComponentRef::Child(target_child2.clone()),
],
ComponentRef::Child(target_child1) => hashset![],
ComponentRef::Child(target_child2) => hashset![],
},
process_component_dependencies(&fake)
};
}
#[fuchsia::test]
fn test_source_does_not_exist() {
// This declaration is invalid because the offer target doesn't exist
let decl = ComponentDeclBuilder::new()
.offer(
OfferBuilder::protocol()
.name("childBOffer")
.target_name("serviceSibling")
.source_static_child("childB")
.target_static_child("childA"),
)
.child_default("childA")
.build();
pretty_assertions::assert_eq!(
hashmap! {
ComponentRef::Self_ => hashset![child("childA")],
child("childA") => hashset![],
},
process_component_dependencies(&FakeComponent::from_decl(decl))
);
}
#[fuchsia::test]
fn test_use_from_child() {
let decl = ComponentDeclBuilder::new()
.offer(
OfferBuilder::protocol()
.name("serviceSelf")
.source(OfferSource::Self_)
.target_static_child("childA")
.dependency(DependencyType::Weak),
)
.child_default("childA")
.use_(UseBuilder::protocol().name("test.protocol").source_static_child("childA"))
.build();
pretty_assertions::assert_eq!(
hashmap! {
ComponentRef::Self_ => hashset![],
child("childA") => hashset![ComponentRef::Self_],
},
process_component_dependencies(&FakeComponent::from_decl(decl))
);
}
#[fuchsia::test]
fn test_use_from_dictionary() {
let decl = ComponentDeclBuilder::new()
.dictionary_default("dict")
.offer(
OfferBuilder::protocol()
.name("weakService")
.source(OfferSource::Self_)
.target_static_child("childA")
.dependency(DependencyType::Weak),
)
.offer(
OfferBuilder::protocol()
.name("serviceA")
.source_static_child("childA")
.target(OfferTarget::Capability("dict".parse().unwrap())),
)
.child_default("childA")
.use_(
UseBuilder::protocol()
.name("serviceA")
.source(UseSource::Self_)
.from_dictionary("dict"),
)
.build();
pretty_assertions::assert_eq!(
hashmap! {
ComponentRef::Self_ => hashset![],
child("childA") => hashset![capability("dict")],
capability("dict") => hashset![ComponentRef::Self_],
},
process_component_dependencies(&FakeComponent::from_decl(decl))
);
}
#[fuchsia::test]
fn test_use_runner_from_child() {
let decl = ComponentDeclBuilder::new()
.child_default("childA")
.use_(UseBuilder::runner().name("test.runner").source_static_child("childA"))
.build();
pretty_assertions::assert_eq!(
hashmap! {
ComponentRef::Self_ => hashset![],
child("childA") => hashset![ComponentRef::Self_],
},
process_component_dependencies(&FakeComponent::from_decl(decl))
);
}
#[fuchsia::test]
fn test_use_from_some_children() {
let decl = ComponentDeclBuilder::new()
.offer(
OfferBuilder::protocol()
.name("serviceSelf")
.source(OfferSource::Self_)
.target_static_child("childA")
.dependency(DependencyType::Weak),
)
.child_default("childA")
.child_default("childB")
.use_(UseBuilder::protocol().name("test.protocol").source_static_child("childA"))
.build();
pretty_assertions::assert_eq!(
hashmap! {
// childB is a dependent because we consider all children
// dependent, unless the component uses something from the
// child.
ComponentRef::Self_ => hashset![child("childB")],
child("childA") => hashset![ComponentRef::Self_],
child("childB") => hashset![],
},
process_component_dependencies(&FakeComponent::from_decl(decl))
);
}
#[fuchsia::test]
fn test_use_from_child_offer_storage() {
let decl = ComponentDeclBuilder::new()
.capability(
CapabilityBuilder::storage()
.name("cdata")
.source(StorageDirectorySource::Child("childB".into()))
.backing_dir("directory"),
)
.capability(
CapabilityBuilder::storage()
.name("pdata")
.source(StorageDirectorySource::Parent)
.backing_dir("directory"),
)
.offer(
OfferBuilder::storage()
.name("cdata")
.source(OfferSource::Self_)
.target_static_child("childA"),
)
.offer(
OfferBuilder::storage()
.name("pdata")
.source(OfferSource::Self_)
.target_static_child("childA"),
)
.child_default("childA")
.child_default("childB")
.use_(UseBuilder::protocol().name("test.protocol").source_static_child("childA"))
.build();
pretty_assertions::assert_eq!(
hashmap! {
ComponentRef::Self_ => hashset![],
child("childA") => hashset![ComponentRef::Self_],
child("childB") => hashset![capability("cdata")],
capability("pdata") => hashset![child("childA")],
capability("cdata") => hashset![child("childA")],
},
process_component_dependencies(&FakeComponent::from_decl(decl))
)
}
#[fuchsia::test]
fn test_use_from_child_weak() {
let decl = ComponentDeclBuilder::new()
.offer(
OfferBuilder::protocol()
.name("serviceSelf")
.source(OfferSource::Self_)
.target_static_child("childA"),
)
.child_default("childA")
.use_(
UseBuilder::protocol()
.name("test.protocol")
.source_static_child("childA")
.dependency(DependencyType::Weak),
)
.build();
pretty_assertions::assert_eq!(
hashmap! {
ComponentRef::Self_ => hashset![child("childA")],
child("childA") => hashset![],
},
process_component_dependencies(&FakeComponent::from_decl(decl))
)
}
#[fuchsia::test]
fn test_use_from_some_children_weak() {
let decl = ComponentDeclBuilder::new()
.offer(
OfferBuilder::protocol()
.name("serviceSelf")
.source(OfferSource::Self_)
.target_static_child("childA")
.dependency(DependencyType::Weak),
)
.child_default("childA")
.child_default("childB")
.use_(UseBuilder::protocol().name("test.protocol").source_static_child("childA"))
.use_(
UseBuilder::protocol()
.name("test.protocol2")
.source_static_child("childB")
.dependency(DependencyType::Weak),
)
.build();
pretty_assertions::assert_eq!(
hashmap! {
// childB is a dependent because its use-from-child has a 'weak' dependency.
ComponentRef::Self_ => hashset![child("childB")],
child("childA") => hashset![ComponentRef::Self_],
child("childB") => hashset![],
},
process_component_dependencies(&FakeComponent::from_decl(decl))
)
}
#[fuchsia::test]
fn test_resolver_capability_creates_dependency() {
let decl = ComponentDeclBuilder::new()
.offer(
OfferBuilder::resolver()
.name("resolver")
.source_static_child("childA")
.target_static_child("childB"),
)
.child_default("childA")
.child_default("childB")
.build();
pretty_assertions::assert_eq!(
hashmap! {
ComponentRef::Self_ => hashset![child("childA"), child("childB")],
child("childA") => hashset![child("childB")],
child("childB") => hashset![],
},
process_component_dependencies(&FakeComponent::from_decl(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;
let (mock_shutdown_barrier, mock_shutdown_action) = MockAction::new(ActionKey::Shutdown);
// Register some actions, and get notifications.
let shutdown_notifier = action_set.register_no_wait(mock_shutdown_action).await;
let stop_notifier = action_set.register_no_wait(StopAction::new(false)).await;
drop(action_set);
// The stop action should be blocked on the shutdown action completing.
assert!(stop_notifier.fut.peek().is_none());
// allow the shutdown action to finish running
mock_shutdown_barrier.send(Ok(())).unwrap();
shutdown_notifier.await.expect("shutdown failed unexpectedly");
// The stop action should now finish running.
stop_notifier.await.expect("stop failed unexpectedly");
}
#[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;
let (mock_shutdown_barrier, mock_shutdown_action) = MockAction::new(ActionKey::Shutdown);
// Register some actions, and get notifications.
let shutdown_notifier = action_set.register_no_wait(mock_shutdown_action).await;
let stop_notifier_1 = action_set.register_no_wait(StopAction::new(false)).await;
let stop_notifier_2 = action_set.register_no_wait(StopAction::new(false)).await;
drop(action_set);
// The stop action should be blocked on the shutdown action completing.
assert!(stop_notifier_1.fut.peek().is_none());
assert!(stop_notifier_2.fut.peek().is_none());
// allow the shutdown action to finish running
mock_shutdown_barrier.send(Ok(())).unwrap();
shutdown_notifier.await.expect("shutdown failed unexpectedly");
// The stop actions should now finish running.
stop_notifier_1.await.expect("stop failed unexpectedly");
stop_notifier_2.await.expect("stop failed unexpectedly");
}
#[fuchsia::test]
async fn shutdown_one_component() {
let components = vec![
("root", ComponentDeclBuilder::new().child_default("a").build()),
("a", component_decl_with_test_runner()),
];
let test = ActionsTest::new("root", components, None).await;
let root = test.model.root();
// Start the component. This should cause the component to have an `Execution`.
let component = test.look_up(vec!["a"].try_into().unwrap()).await;
root.start_instance(&component.moniker, &StartReason::Eager)
.await
.expect("could not start a");
assert!(component.is_started().await);
let a_info = ComponentInfo::new(component.clone()).await;
// Register shutdown action, and wait for it. Component should shut down (no more
// `Execution`).
ActionsManager::register(
a_info.component.clone(),
ShutdownAction::new(ShutdownType::Instance),
)
.await
.expect("shutdown failed");
a_info.check_is_shut_down(&test.runner).await;
// Trying to start the component should fail because it's shut down.
root.start_instance(&a_info.component.moniker, &StartReason::Eager)
.await
.expect_err("successfully bound to a after shutdown");
// Shut down the component again. This succeeds, but has no additional effect.
ActionsManager::register(
a_info.component.clone(),
ShutdownAction::new(ShutdownType::Instance),
)
.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().child_default("container").build()),
(
"container",
ComponentDeclBuilder::new().collection_default("coll").child_default("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"].try_into().unwrap())).await;
let root = test.model.root();
// Create dynamic instances in "coll".
test.create_dynamic_child("coll", "a").await;
test.create_dynamic_child("coll", "b").await;
// Start the components. This should cause them to have an `Execution`.
let component_container = test.look_up(vec!["container"].try_into().unwrap()).await;
let component_a = test.look_up(vec!["container", "coll:a"].try_into().unwrap()).await;
let component_b = test.look_up(vec!["container", "coll:b"].try_into().unwrap()).await;
let component_c = test.look_up(vec!["container", "c"].try_into().unwrap()).await;
root.start_instance(&component_container.moniker, &StartReason::Eager)
.await
.expect("could not start container");
root.start_instance(&component_a.moniker, &StartReason::Eager)
.await
.expect("could not start coll:a");
root.start_instance(&component_b.moniker, &StartReason::Eager)
.await
.expect("could not start coll:b");
root.start_instance(&component_c.moniker, &StartReason::Eager)
.await
.expect("could not start c");
assert!(component_container.is_started().await);
assert!(component_a.is_started().await);
assert!(component_b.is_started().await);
assert!(component_c.is_started().await);
assert!(has_child(&component_container, "coll:a").await);
assert!(has_child(&component_container, "coll:b").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.
ActionsManager::register(
component_container_info.component.clone(),
ShutdownAction::new(ShutdownType::Instance),
)
.await
.expect("shutdown failed");
component_container_info.check_is_shut_down(&test.runner).await;
assert!(!has_child(&component_container_info.component, "coll:a").await);
assert!(!has_child(&component_container_info.component, "coll:b").await);
assert!(has_child(&component_container_info.component, "c").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", "c"].try_into().unwrap()),
Lifecycle::Stop(vec!["container", "coll:a"].try_into().unwrap()),
Lifecycle::Stop(vec!["container", "coll:b"].try_into().unwrap()),
];
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", "coll:a"].try_into().unwrap()),
Lifecycle::Destroy(vec!["container", "coll:b"].try_into().unwrap()),
];
assert_eq!(next, expected);
}
}
#[fuchsia::test]
async fn shutdown_dynamic_offers() {
let components = vec![
("root", ComponentDeclBuilder::new().child_default("container").build()),
(
"container",
ComponentDeclBuilder::new()
.collection(
CollectionBuilder::new()
.name("coll")
.allowed_offers(AllowedOffers::StaticAndDynamic),
)
.child_default("c")
.offer(
OfferBuilder::protocol()
.name("static_offer_source")
.target_name("static_offer_target")
.source(OfferSource::Child(ChildRef {
name: "c".parse().unwrap(),
collection: None,
}))
.target(OfferTarget::Collection("coll".parse().unwrap())),
)
.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"].try_into().unwrap())).await;
let root = test.model.root();
// Create dynamic instances in "coll".
test.create_dynamic_child("coll", "a").await;
test.create_dynamic_child_with_args(
"coll",
"b",
fcomponent::CreateChildArgs {
dynamic_offers: Some(vec![fdecl::Offer::Protocol(fdecl::OfferProtocol {
source: Some(fdecl::Ref::Child(fdecl::ChildRef {
name: "a".into(),
collection: Some("coll".parse().unwrap()),
})),
source_name: Some("dyn_offer_source_name".to_string()),
target_name: Some("dyn_offer_target_name".to_string()),
dependency_type: Some(fdecl::DependencyType::Strong),
..Default::default()
})]),
..Default::default()
},
)
.await
.expect("failed to create child");
// Start the components. This should cause them to have an `Execution`.
let component_container = test.look_up(vec!["container"].try_into().unwrap()).await;
let component_a = test.look_up(vec!["container", "coll:a"].try_into().unwrap()).await;
let component_b = test.look_up(vec!["container", "coll:b"].try_into().unwrap()).await;
let component_c = test.look_up(vec!["container", "c"].try_into().unwrap()).await;
root.start_instance(&component_container.moniker, &StartReason::Eager)
.await
.expect("could not start container");
root.start_instance(&component_a.moniker, &StartReason::Eager)
.await
.expect("could not start coll:a");
root.start_instance(&component_b.moniker, &StartReason::Eager)
.await
.expect("could not start coll:b");
root.start_instance(&component_c.moniker, &StartReason::Eager)
.await
.expect("could not start c");
assert!(component_container.is_started().await);
assert!(component_a.is_started().await);
assert!(component_b.is_started().await);
assert!(component_c.is_started().await);
assert!(has_child(&component_container, "coll:a").await);
assert!(has_child(&component_container, "coll:b").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.
ActionsManager::register(
component_container_info.component.clone(),
ShutdownAction::new(ShutdownType::Instance),
)
.await
.expect("shutdown failed");
component_container_info.check_is_shut_down(&test.runner).await;
assert!(!has_child(&component_container_info.component, "coll:a").await);
assert!(!has_child(&component_container_info.component, "coll:b").await);
assert!(has_child(&component_container_info.component, "c").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();
pretty_assertions::assert_eq!(
vec![
Lifecycle::Stop(vec!["container", "coll:b"].try_into().unwrap()),
Lifecycle::Stop(vec!["container", "coll:a"].try_into().unwrap()),
Lifecycle::Stop(vec!["container", "c"].try_into().unwrap()),
],
events.drain(0..3).collect::<Vec<_>>()
);
// The order here is nondeterministic.
pretty_assertions::assert_eq!(
btreeset![
Lifecycle::Destroy(vec!["container", "coll:b"].try_into().unwrap()),
Lifecycle::Destroy(vec!["container", "coll:a"].try_into().unwrap()),
],
events.drain(0..2).collect::<BTreeSet<_>>()
);
pretty_assertions::assert_eq!(
vec![Lifecycle::Stop(vec!["container"].try_into().unwrap()),],
events
);
}
}
#[fuchsia::test]
async fn shutdown_not_started() {
let components = vec![
("root", ComponentDeclBuilder::new().child_default("a").build()),
("a", ComponentDeclBuilder::new().child_default("b").build()),
("b", component_decl_with_test_runner()),
];
let test = ActionsTest::new("root", components, None).await;
let component_a = test.look_up(vec!["a"].try_into().unwrap()).await;
let component_b = test.look_up(vec!["a", "b"].try_into().unwrap()).await;
assert!(!component_a.is_started().await);
assert!(!component_b.is_started().await);
// Register shutdown action on "a", and wait for it.
ActionsManager::register(component_a.clone(), ShutdownAction::new(ShutdownType::Instance))
.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.
ActionsManager::register(component_a.clone(), ShutdownAction::new(ShutdownType::Instance))
.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().child_default("a").build()),
("a", ComponentDeclBuilder::new().child_default("b").build()),
("b", ComponentDeclBuilder::new().child_default("c").build()),
("c", component_decl_with_test_runner()),
];
let test = ActionsTest::new("root", components, None).await;
let root = test.model.root();
let component_a = test.look_up(vec!["a"].try_into().unwrap()).await;
root.start_instance(&component_a.moniker, &StartReason::Eager)
.await
.expect("could not start a");
assert!(component_a.is_started().await);
// Register shutdown action on "a", and wait for it.
ActionsManager::register(component_a.clone(), ShutdownAction::new(ShutdownType::Instance))
.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::Shutdown(ref state, _) => {
state.children.get(&"b".try_into().unwrap()).expect("child b not found").clone()
}
_ => panic!("not shutdown"),
}
};
assert!(execution_is_shut_down(&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"].try_into().unwrap())]);
}
}
/// Shut down `a`:
/// a
/// \
/// b
/// / \
/// c d
#[fuchsia::test]
async fn shutdown_hierarchy() {
let components = vec![
("root", ComponentDeclBuilder::new().child_default("a").build()),
("a", ComponentDeclBuilder::new().child(ChildBuilder::new().name("b").eager()).build()),
(
"b",
ComponentDeclBuilder::new()
.child(ChildBuilder::new().name("c").eager())
.child(ChildBuilder::new().name("d").eager())
.build(),
),
("c", component_decl_with_test_runner()),
("d", component_decl_with_test_runner()),
];
let test = ActionsTest::new("root", components, None).await;
let root = test.model.root();
let component_a = test.look_up(vec!["a"].try_into().unwrap()).await;
let component_b = test.look_up(vec!["a", "b"].try_into().unwrap()).await;
let component_c = test.look_up(vec!["a", "b", "c"].try_into().unwrap()).await;
let component_d = test.look_up(vec!["a", "b", "d"].try_into().unwrap()).await;
// Component startup was eager, so they should all have an `Execution`.
root.start_instance(&component_a.moniker, &StartReason::Eager)
.await
.expect("could not start a");
assert!(component_a.is_started().await);
assert!(component_b.is_started().await);
assert!(component_c.is_started().await);
assert!(component_d.is_started().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.
ActionsManager::register(
component_a_info.component.clone(),
ShutdownAction::new(ShutdownType::Instance),
)
.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", "b", "c"].try_into().unwrap()),
Lifecycle::Stop(vec!["a", "b", "d"].try_into().unwrap()),
];
assert_eq!(first, expected);
assert_eq!(
events,
vec![
Lifecycle::Stop(vec!["a", "b"].try_into().unwrap()),
Lifecycle::Stop(vec!["a"].try_into().unwrap())
]
);
}
}
/// 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().child_default("a").build()),
("a", ComponentDeclBuilder::new().child(ChildBuilder::new().name("b").eager()).build()),
(
"b",
ComponentDeclBuilder::new()
.child(ChildBuilder::new().name("c").eager())
.child(ChildBuilder::new().name("d").eager())
.child(ChildBuilder::new().name("e").eager())
.offer(
OfferBuilder::protocol()
.name("serviceD")
.source_static_child("d")
.target_static_child("c"),
)
.offer(
OfferBuilder::protocol()
.name("serviceD")
.source_static_child("d")
.target_static_child("e"),
)
.build(),
),
(
"c",
ComponentDeclBuilder::new().use_(UseBuilder::protocol().name("serviceD")).build(),
),
(
"d",
ComponentDeclBuilder::new()
.protocol_default("serviceD")
.expose(ExposeBuilder::protocol().name("serviceD").source(ExposeSource::Self_))
.build(),
),
(
"e",
ComponentDeclBuilder::new().use_(UseBuilder::protocol().name("serviceD")).build(),
),
];
let test = ActionsTest::new("root", components, None).await;
let root = test.model.root();
let component_a = test.look_up(vec!["a"].try_into().unwrap()).await;
let component_b = test.look_up(vec!["a", "b"].try_into().unwrap()).await;
let component_c = test.look_up(vec!["a", "b", "c"].try_into().unwrap()).await;
let component_d = test.look_up(vec!["a", "b", "d"].try_into().unwrap()).await;
let component_e = test.look_up(vec!["a", "b", "e"].try_into().unwrap()).await;
// Component startup was eager, so they should all have an `Execution`.
root.start_instance(&component_a.moniker, &StartReason::Eager)
.await
.expect("could not start a");
assert!(component_a.is_started().await);
assert!(component_b.is_started().await);
assert!(component_c.is_started().await);
assert!(component_d.is_started().await);
assert!(component_e.is_started().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.
ActionsManager::register(
component_a_info.component.clone(),
ShutdownAction::new(ShutdownType::Instance),
)
.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", "b", "c"].try_into().unwrap()),
Lifecycle::Stop(vec!["a", "b", "e"].try_into().unwrap()),
];
assert_eq!(first, expected);
let next: Vec<_> = events.drain(0..1).collect();
expected = vec![Lifecycle::Stop(vec!["a", "b", "d"].try_into().unwrap())];
assert_eq!(next, expected);
assert_eq!(
events,
vec![
Lifecycle::Stop(vec!["a", "b"].try_into().unwrap()),
Lifecycle::Stop(vec!["a"].try_into().unwrap())
]
);
}
}
/// 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().child_default("a").build()),
("a", ComponentDeclBuilder::new().child(ChildBuilder::new().name("b").eager()).build()),
(
"b",
ComponentDeclBuilder::new()
.child(ChildBuilder::new().name("c").eager())
.child(ChildBuilder::new().name("d").eager())
.child(ChildBuilder::new().name("e").eager())
.child(ChildBuilder::new().name("f").eager())
.offer(
OfferBuilder::protocol()
.name("serviceD")
.source_static_child("d")
.target_static_child("c"),
)
.offer(
OfferBuilder::protocol()
.name("serviceD")
.source_static_child("d")
.target_static_child("e"),
)
.offer(
OfferBuilder::protocol()
.name("serviceE")
.source_static_child("e")
.target_static_child("f"),
)
.build(),
),
(
"c",
ComponentDeclBuilder::new().use_(UseBuilder::protocol().name("serviceD")).build(),
),
(
"d",
ComponentDeclBuilder::new()
.protocol_default("serviceD")
.expose(ExposeBuilder::protocol().name("serviceD").source(ExposeSource::Self_))
.build(),
),
(
"e",
ComponentDeclBuilder::new()
.protocol_default("serviceE")
.use_(UseBuilder::protocol().name("serviceD"))
.expose(ExposeBuilder::protocol().name("serviceE").source(ExposeSource::Self_))
.build(),
),
(
"f",
ComponentDeclBuilder::new().use_(UseBuilder::protocol().name("serviceE")).build(),
),
];
let moniker_a: Moniker = vec!["a"].try_into().unwrap();
let moniker_b: Moniker = vec!["a", "b"].try_into().unwrap();
let moniker_c: Moniker = vec!["a", "b", "c"].try_into().unwrap();
let moniker_d: Moniker = vec!["a", "b", "d"].try_into().unwrap();
let moniker_e: Moniker = vec!["a", "b", "e"].try_into().unwrap();
let moniker_f: Moniker = vec!["a", "b", "f"].try_into().unwrap();
let test = ActionsTest::new("root", components, None).await;
let root = test.model.root();
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`.
root.start_instance(&component_a.moniker, &StartReason::Eager)
.await
.expect("could not start a");
assert!(component_a.is_started().await);
assert!(component_b.is_started().await);
assert!(component_c.is_started().await);
assert!(component_d.is_started().await);
assert!(component_e.is_started().await);
assert!(component_f.is_started().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.
ActionsManager::register(
component_a_info.component.clone(),
ShutdownAction::new(ShutdownType::Instance),
)
.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<Moniker, Vec<Moniker>> = 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().child_default("a").build()),
("a", ComponentDeclBuilder::new().child(ChildBuilder::new().name("b").eager()).build()),
(
"b",
ComponentDeclBuilder::new()
.child(ChildBuilder::new().name("c").eager())
.child(ChildBuilder::new().name("d").eager())
.child(ChildBuilder::new().name("e").eager())
.child(ChildBuilder::new().name("f").eager())
.offer(
OfferBuilder::protocol()
.name("serviceD")
.source_static_child("d")
.target_static_child("c"),
)
.offer(
OfferBuilder::protocol()
.name("serviceD")
.source_static_child("d")
.target_static_child("e"),
)
.offer(
OfferBuilder::protocol()
.name("serviceD")
.source_static_child("d")
.target_static_child("f"),
)
.offer(
OfferBuilder::protocol()
.name("serviceE")
.source_static_child("e")
.target_static_child("f"),
)
.build(),
),
(
"c",
ComponentDeclBuilder::new().use_(UseBuilder::protocol().name("serviceD")).build(),
),
(
"d",
ComponentDeclBuilder::new()
.protocol_default("serviceD")
.expose(ExposeBuilder::protocol().name("serviceD").source(ExposeSource::Self_))
.build(),
),
(
"e",
ComponentDeclBuilder::new()
.protocol_default("serviceE")
.use_(UseBuilder::protocol().name("serviceE"))
.expose(ExposeBuilder::protocol().name("serviceE").source(ExposeSource::Self_))
.build(),
),
(
"f",
ComponentDeclBuilder::new()
.use_(UseBuilder::protocol().name("serviceE"))
.use_(UseBuilder::protocol().name("serviceD"))
.build(),
),
];
let moniker_a: Moniker = vec!["a"].try_into().unwrap();
let moniker_b: Moniker = vec!["a", "b"].try_into().unwrap();
let moniker_c: Moniker = vec!["a", "b", "c"].try_into().unwrap();
let moniker_d: Moniker = vec!["a", "b", "d"].try_into().unwrap();
let moniker_e: Moniker = vec!["a", "b", "e"].try_into().unwrap();
let moniker_f: Moniker = vec!["a", "b", "f"].try_into().unwrap();
let test = ActionsTest::new("root", components, None).await;
let root = test.model.root();
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`.
root.start_instance(&component_a.moniker, &StartReason::Eager)
.await
.expect("could not start a");
assert!(component_a.is_started().await);
assert!(component_b.is_started().await);
assert!(component_c.is_started().await);
assert!(component_d.is_started().await);
assert!(component_e.is_started().await);
assert!(component_f.is_started().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.
ActionsManager::register(
component_a_info.component.clone(),
ShutdownAction::new(ShutdownType::Instance),
)
.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<Moniker, Vec<Moniker>> = 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).unwrap_or_else(|| {
panic!("{} 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().child_default("a").build()),
("a", ComponentDeclBuilder::new().child(ChildBuilder::new().name("b").eager()).build()),
(
"b",
ComponentDeclBuilder::new()
.child(ChildBuilder::new().name("c").eager())
.child(ChildBuilder::new().name("d").eager())
.offer(
OfferBuilder::protocol()
.name("serviceC")
.source_static_child("c")
.target_static_child("d"),
)
.build(),
),
(
"c",
ComponentDeclBuilder::new()
.protocol_default("serviceC")
.expose(ExposeBuilder::protocol().name("serviceC").source(ExposeSource::Self_))
.build(),
),
(
"d",
ComponentDeclBuilder::new().use_(UseBuilder::protocol().name("serviceC")).build(),
),
];
let test = ActionsTest::new("root", components, None).await;
let root = test.model.root();
let component_a = test.look_up(vec!["a"].try_into().unwrap()).await;
let component_b = test.look_up(vec!["a", "b"].try_into().unwrap()).await;
let component_c = test.look_up(vec!["a", "b", "c"].try_into().unwrap()).await;
let component_d = test.look_up(vec!["a", "b", "d"].try_into().unwrap()).await;
// Component startup was eager, so they should all have an `Execution`.
root.start_instance(&component_a.moniker, &StartReason::Eager)
.await
.expect("could not start 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.
ActionsManager::register(
component_a_info.component.clone(),
ShutdownAction::new(ShutdownType::Instance),
)
.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", "b", "d"].try_into().unwrap()),
Lifecycle::Stop(vec!["a", "b", "c"].try_into().unwrap()),
Lifecycle::Stop(vec!["a", "b"].try_into().unwrap()),
Lifecycle::Stop(vec!["a"].try_into().unwrap()),
];
assert_eq!(events, expected);
}
}
/// Shut down `a`:
/// a
/// \
/// b
/// / \
/// c-->d
/// In this case D uses a resource exposed by C, routed through a dictionary defined by B
#[fuchsia::test]
async fn shutdown_with_dictionary_dependency() {
let components = vec![
("root", ComponentDeclBuilder::new().child_default("a").build()),
("a", ComponentDeclBuilder::new().child(ChildBuilder::new().name("b").eager()).build()),
(
"b",
ComponentDeclBuilder::new()
.dictionary_default("dict")
.child(ChildBuilder::new().name("c").eager())
.child(ChildBuilder::new().name("d").eager())
.offer(
OfferBuilder::protocol()
.name("serviceC")
.source_static_child("c")
.target(OfferTarget::Capability("dict".parse().unwrap())),
)
.offer(
OfferBuilder::protocol()
.name("serviceC")
.source(OfferSource::Self_)
.from_dictionary("dict")
.target_static_child("d"),
)
.build(),
),
(
"c",
ComponentDeclBuilder::new()
.protocol_default("serviceC")
.expose(ExposeBuilder::protocol().name("serviceC").source(ExposeSource::Self_))
.build(),
),
(
"d",
ComponentDeclBuilder::new().use_(UseBuilder::protocol().name("serviceC")).build(),
),
];
let test = ActionsTest::new("root", components, None).await;
let root = test.model.root();
let component_a = test.look_up(vec!["a"].try_into().unwrap()).await;
let component_b = test.look_up(vec!["a", "b"].try_into().unwrap()).await;
let component_c = test.look_up(vec!["a", "b", "c"].try_into().unwrap()).await;
let component_d = test.look_up(vec!["a", "b", "d"].try_into().unwrap()).await;
// Component startup was eager, so they should all have an `Execution`.
root.start_instance(&component_a.moniker, &StartReason::Eager)
.await
.expect("could not start 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.
ActionsManager::register(
component_a_info.component.clone(),
ShutdownAction::new(ShutdownType::Instance),
)
.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", "b", "d"].try_into().unwrap()),
Lifecycle::Stop(vec!["a", "b", "c"].try_into().unwrap()),
Lifecycle::Stop(vec!["a", "b"].try_into().unwrap()),
Lifecycle::Stop(vec!["a"].try_into().unwrap()),
];
assert_eq!(events, expected);
}
}
/// Shut down `a`:
/// a (a use b)
/// / \
/// b c
/// In this case, c shuts down first, then a, then b.
#[fuchsia::test]
async fn shutdown_use_from_child() {
let components = vec![
("root", ComponentDeclBuilder::new().child_default("a").build()),
(
"a",
ComponentDeclBuilder::new()
.child(ChildBuilder::new().name("b").eager())
.child(ChildBuilder::new().name("c").eager())
.use_(UseBuilder::protocol().source_static_child("b").name("serviceC"))
.build(),
),
(
"b",
ComponentDeclBuilder::new()
.protocol_default("serviceC")
.expose(ExposeBuilder::protocol().name("serviceC").source(ExposeSource::Self_))
.build(),
),
("c", ComponentDeclBuilder::new().build()),
];
let test = ActionsTest::new("root", components, None).await;
let root = test.model.root();
let component_a = test.look_up(vec!["a"].try_into().unwrap()).await;
let component_b = test.look_up(vec!["a", "b"].try_into().unwrap()).await;
let component_c = test.look_up(vec!["a", "c"].try_into().unwrap()).await;
// Component startup was eager, so they should all have an `Execution`.
root.start_instance(&component_a.moniker, &StartReason::Eager)
.await
.expect("could not start 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;
// Register shutdown action on "a", and wait for it. This should cause all components
// to shut down.
ActionsManager::register(
component_a_info.component.clone(),
ShutdownAction::new(ShutdownType::Instance),
)
.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;
{
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", "c"].try_into().unwrap()),
Lifecycle::Stop(vec!["a"].try_into().unwrap()),
Lifecycle::Stop(vec!["a", "b"].try_into().unwrap()),
];
assert_eq!(events, expected);
}
}
/// Shut down `a`:
/// a (a uses runner from b)
/// / \
/// b c
/// In this case, c shuts down first, then a, then b.
#[fuchsia::test]
async fn test_shutdown_use_runner_from_child() {
let components = vec![
("root", ComponentDeclBuilder::new().child_default("a").build()),
(
"a",
ComponentDeclBuilder::new()
.child(ChildBuilder::new().name("b").eager())
.child(ChildBuilder::new().name("c").eager())
.use_(UseBuilder::runner().source_static_child("b").name("test.runner"))
.build(),
),
(
"b",
ComponentDeclBuilder::new()
.expose(ExposeBuilder::runner().name("test.runner").source(ExposeSource::Self_))
.build(),
),
("c", ComponentDeclBuilder::new().build()),
];
let test = ActionsTest::new("root", components, None).await;
let root = test.model.root();
let component_a = test.look_up(vec!["a"].try_into().unwrap()).await;
let component_b = test.look_up(vec!["a", "b"].try_into().unwrap()).await;
let component_c = test.look_up(vec!["a", "c"].try_into().unwrap()).await;
// Component startup was eager, so they should all have an `Execution`.
root.start_instance(&component_a.moniker, &StartReason::Eager)
.await
.expect("could not start 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;
// Register shutdown action on "a", and wait for it. This should cause all components
// to shut down.
ActionsManager::register(
component_a_info.component.clone(),
ShutdownAction::new(ShutdownType::Instance),
)
.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;
{
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", "c"].try_into().unwrap()),
Lifecycle::Stop(vec!["a"].try_into().unwrap()),
Lifecycle::Stop(vec!["a", "b"].try_into().unwrap()),
];
assert_eq!(events, expected);
}
}
/// Shut down `a`:
/// a (a use b, and b use c)
/// / \
/// b c
/// In this case, a shuts down first, then b, then c.
#[fuchsia::test]
async fn shutdown_use_from_child_that_uses_from_sibling() {
let components = vec![
("root", ComponentDeclBuilder::new().child_default("a").build()),
(
"a",
ComponentDeclBuilder::new()
.child(ChildBuilder::new().name("b").eager())
.child(ChildBuilder::new().name("c").eager())
.use_(UseBuilder::protocol().source_static_child("b").name("serviceB"))
.offer(
OfferBuilder::protocol()
.name("serviceC")
.target_name("serviceB")
.source_static_child("c")
.target_static_child("b"),
)
.build(),
),
(
"b",
ComponentDeclBuilder::new()
.protocol_default("serviceB")
.expose(ExposeBuilder::protocol().name("serviceB").source(ExposeSource::Self_))
.use_(UseBuilder::protocol().name("serviceC"))
.build(),
),
(
"c",
ComponentDeclBuilder::new()
.protocol_default("serviceC")
.expose(ExposeBuilder::protocol().name("serviceC").source(ExposeSource::Self_))
.build(),
),
];
let test = ActionsTest::new("root", components, None).await;
let root = test.model.root();
let component_a = test.look_up(vec!["a"].try_into().unwrap()).await;
let component_b = test.look_up(vec!["a", "b"].try_into().unwrap()).await;
let component_c = test.look_up(vec!["a", "c"].try_into().unwrap()).await;
// Component startup was eager, so they should all have an `Execution`.
root.start_instance(&component_a.moniker, &StartReason::Eager)
.await
.expect("could not start 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;
// Register shutdown action on "a", and wait for it. This should cause all components
// to shut down.
ActionsManager::register(
component_a_info.component.clone(),
ShutdownAction::new(ShutdownType::Instance),
)
.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;
{
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"].try_into().unwrap()),
Lifecycle::Stop(vec!["a", "b"].try_into().unwrap()),
Lifecycle::Stop(vec!["a", "c"].try_into().unwrap()),
];
assert_eq!(events, expected);
}
}
/// Shut down `a`:
/// a (a use b weak)
/// / \
/// b c
/// In this case, b or c shutdown first (arbitrary order), then a.
#[fuchsia::test]
async fn shutdown_use_from_child_weak() {
let components = vec![
("root", ComponentDeclBuilder::new().child_default("a").build()),
(
"a",
ComponentDeclBuilder::new()
.child(ChildBuilder::new().name("b").eager())
.child(ChildBuilder::new().name("c").eager())
.use_(
UseBuilder::protocol()
.source_static_child("b")
.name("serviceC")
.dependency(DependencyType::Weak),
)
.build(),
),
(
"b",
ComponentDeclBuilder::new()
.protocol_default("serviceC")
.expose(ExposeBuilder::protocol().name("serviceC").source(ExposeSource::Self_))
.build(),
),
("c", ComponentDeclBuilder::new().build()),
];
let test = ActionsTest::new("root", components, None).await;
let root = test.model.root();
let component_a = test.look_up(vec!["a"].try_into().unwrap()).await;
let component_b = test.look_up(vec!["a", "b"].try_into().unwrap()).await;
let component_c = test.look_up(vec!["a", "c"].try_into().unwrap()).await;
// Component startup was eager, so they should all have an `Execution`.
root.start_instance(&component_a.moniker, &StartReason::Eager)
.await
.expect("could not start 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;
// Register shutdown action on "a", and wait for it. This should cause all components
// to shut down.
ActionsManager::register(
component_a_info.component.clone(),
ShutdownAction::new(ShutdownType::Instance),
)
.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;
{
let events: Vec<_> = test
.test_hook
.lifecycle()
.into_iter()
.filter(|e| match e {
Lifecycle::Stop(_) => true,
_ => false,
})
.collect();
let expected1: Vec<_> = vec![
Lifecycle::Stop(vec!["a", "c"].try_into().unwrap()),
Lifecycle::Stop(vec!["a", "b"].try_into().unwrap()),
Lifecycle::Stop(vec!["a"].try_into().unwrap()),
];
let expected2: Vec<_> = vec![
Lifecycle::Stop(vec!["a", "b"].try_into().unwrap()),
Lifecycle::Stop(vec!["a", "c"].try_into().unwrap()),
Lifecycle::Stop(vec!["a"].try_into().unwrap()),
];
assert!(events == expected1 || events == expected2);
}
}
/// 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().child_default("a").build()),
("a", ComponentDeclBuilder::new().child_default("b").build()),
("b", ComponentDeclBuilder::new().child_default("b").build()),
];
let test = ActionsTest::new("root", components, None).await;
let root = test.model.root();
let component_a = test.look_up(vec!["a"].try_into().unwrap()).await;
let component_b = test.look_up(vec!["a", "b"].try_into().unwrap()).await;
let component_b2 = test.look_up(vec!["a", "b", "b"].try_into().unwrap()).await;
// Start second `b`.
root.start_instance(&component_a.moniker, &StartReason::Eager)
.await
.expect("could not start b2");
root.start_instance(&component_b.moniker, &StartReason::Eager)
.await
.expect("could not start b2");
root.start_instance(&component_b2.moniker, &StartReason::Eager)
.await
.expect("could not start b2");
assert!(component_a.is_started().await);
assert!(component_b.is_started().await);
assert!(component_b2.is_started().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.
ActionsManager::register(
component_a_info.component.clone(),
ShutdownAction::new(ShutdownType::Instance),
)
.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", "b", "b"].try_into().unwrap()),
Lifecycle::Stop(vec!["a", "b"].try_into().unwrap()),
Lifecycle::Stop(vec!["a"].try_into().unwrap())
]
);
}
}
/// Shut down `a`:
/// a
/// \
/// b
/// / \
/// c d
///
/// `a` fails to finish shutdown the first time, but succeeds the second time.
#[fuchsia::test]
fn shutdown_error() {
let mut executor = fasync::TestExecutor::new();
let mut test_body = Box::pin(async move {
let components = vec![
("root", ComponentDeclBuilder::new().child_default("a").build()),
(
"a",
ComponentDeclBuilder::new()
.child(ChildBuilder::new().name("b").eager())
.build(),
),
(
"b",
ComponentDeclBuilder::new()
.child(ChildBuilder::new().name("c").eager())
.child(ChildBuilder::new().name("d").eager())
.build(),
),
("c", component_decl_with_test_runner()),
("d", component_decl_with_test_runner()),
];
let test = ActionsTest::new("root", components, None).await;
let root = test.model.root();
let component_a = test.look_up(vec!["a"].try_into().unwrap()).await;
let component_b = test.look_up(vec!["a", "b"].try_into().unwrap()).await;
let component_c = test.look_up(vec!["a", "b", "c"].try_into().unwrap()).await;
let component_d = test.look_up(vec!["a", "b", "d"].try_into().unwrap()).await;
// Component startup was eager, so they should all have an `Execution`.
root.start_instance(&component_a.moniker, &StartReason::Eager)
.await
.expect("could not start a");
assert!(component_a.is_started().await);
assert!(component_b.is_started().await);
assert!(component_c.is_started().await);
assert!(component_d.is_started().await);
let component_a_info = ComponentInfo::new(component_a.clone()).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.clone()).await;
// Mock a failure to shutdown "d".
let (shutdown_completer, mock_shutdown_action) = MockAction::new(ActionKey::Shutdown);
let _shutdown_notifier =
component_d.lock_actions().await.register_no_wait(mock_shutdown_action).await;
// Register shutdown action on "a", and wait for it. "d" fails to shutdown, so "a"
// fails too. The state of "c" is unknown at this point. The shutdown of stop targets
// occur simultaneously. "c" could've shutdown before "d" or it might not have.
let a_shutdown_notifier = component_a
.lock_actions()
.await
.register_no_wait(ShutdownAction::new(ShutdownType::Instance))
.await;
// We need to wait for the shutdown action of "b" to register a shutdown action on "d",
// which will be deduplicated with the shutdown action we registered on "d" earlier.
_ = fasync::TestExecutor::poll_until_stalled(std::future::pending::<()>()).await;
// Now we can allow the mock shutdown action to complete with an error, and wait for
// our destroy child call to finish.
shutdown_completer
.send(Err(ActionError::StopError { err: StopActionError::GetParentFailed }))
.unwrap();
a_shutdown_notifier.await.expect_err("shutdown succeeded unexpectedly");
component_a_info.check_not_shut_down(&test.runner).await;
component_b_info.check_not_shut_down(&test.runner).await;
component_d_info.check_not_shut_down(&test.runner).await;
// Register shutdown action on "a" again. Without our mock action queued up on it,
// "d"'s shutdown succeeds, and "a" is shutdown this time.
ActionsManager::register(
component_a_info.component.clone(),
ShutdownAction::new(ShutdownType::Instance),
)
.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", "b", "c"].try_into().unwrap()),
Lifecycle::Stop(vec!["a", "b", "d"].try_into().unwrap()),
];
assert_eq!(first, expected);
assert_eq!(
events,
vec![
Lifecycle::Stop(vec!["a", "b"].try_into().unwrap()),
Lifecycle::Stop(vec!["a"].try_into().unwrap())
]
);
}
});
executor.run_until_stalled(&mut test_body).unwrap();
}
}