blob: 2132fefc39c686dd551dcff39706a6b0c0fecbcc [file] [log] [blame]
// Copyright 2020 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be found in the LICENSE file.
use {
anyhow::{format_err, Context, Error},
fdio,
fidl::endpoints::DiscoverableService,
fidl_fuchsia_inspect::TreeMarker,
fidl_fuchsia_sys::{
ComponentControllerEvent, EnvironmentControllerEvent, EnvironmentControllerProxy,
EnvironmentMarker, EnvironmentOptions, EnvironmentProxy, LauncherProxy,
ServiceProviderMarker,
},
fidl_fuchsia_sys_internal::{
ComponentEventListenerMarker, ComponentEventListenerRequest,
ComponentEventListenerRequestStream, ComponentEventProviderMarker, SourceIdentity,
},
fuchsia_async::{self as fasync, DurationExt, TimeoutExt},
fuchsia_component::client::{connect_to_service, launch, App},
fuchsia_inspect::{assert_inspect_tree, reader},
fuchsia_zircon::DurationNum,
futures::{
future,
stream::{StreamExt, TryStreamExt},
},
lazy_static::lazy_static,
maplit::hashset,
regex::Regex,
std::collections::HashSet,
};
lazy_static! {
static ref TEST_COMPONENT: String = "test_component.cmx".to_string();
static ref TEST_COMPONENT_WITH_REALM: String = "test_component_with_subrealm.cmx".to_string();
static ref TEST_COMPONENT_URL: String =
"fuchsia-pkg://fuchsia.com/component_events_integration_tests#meta/test_component.cmx"
.to_string();
static ref TEST_COMPONENT_WITH_REALM_URL: String =
"fuchsia-pkg://fuchsia.com/component_events_integration_tests#meta/test_component_with_subrealm.cmx"
.to_string();
static ref SELF_COMPONENT: String = "component_events_integration_test.cmx".to_string();
static ref TEST_COMPONENT_REALM_PATH: Vec<String> = vec!["diagnostics_test".to_string()];
static ref TIMEOUT_SECONDS: i64 = 1;
}
struct ComponentEventsTest {
listener_request_stream: Option<ComponentEventListenerRequestStream>,
diagnostics_test_env: EnvironmentProxy,
_diagnostics_test_env_ctrl: EnvironmentControllerProxy,
}
impl ComponentEventsTest {
pub async fn new(env: &EnvironmentProxy, with_listener: bool) -> Result<Self, Error> {
let (diagnostics_test_env, ctrl) =
create_nested_environment(&env, "diagnostics_test").await?;
let listener_request_stream = if with_listener {
Some(listen_for_component_events(&env).context("get component event provider")?)
} else {
None
};
let mut test = Self {
listener_request_stream,
diagnostics_test_env,
_diagnostics_test_env_ctrl: ctrl,
};
test.consume_self_events().await.expect("failed to consume self events");
Ok(test)
}
// Asserts that we receive the expected events about this component itself.
async fn consume_self_events(&mut self) -> Result<(), Error> {
match &mut self.listener_request_stream {
None => {}
Some(listener_request_stream) => {
let self_realm_path = Vec::new();
let request =
listener_request_stream.try_next().await.context("Fetch self start")?;
if let Some(ComponentEventListenerRequest::OnStart { component, .. }) = request {
self.assert_identity(component, &SELF_COMPONENT, &self_realm_path);
} else {
return Err(format_err!("Expected start event for self"));
}
}
}
Ok(())
}
/// Asserts a start event for the test_component.
async fn assert_on_start(&mut self, name: &str, realm_path: &[String]) -> Result<(), Error> {
match &mut self.listener_request_stream {
None => {}
Some(listener_request_stream) => {
let request =
listener_request_stream.try_next().await.context("Fetch test start")?;
if let Some(ComponentEventListenerRequest::OnStart { component, .. }) = request {
self.assert_identity(component, name, realm_path);
} else {
return Err(format_err!("Expected start event. Got: {:?}", request));
}
}
}
Ok(())
}
/// Asserts a stop event for the test_component.
async fn assert_on_stop(&mut self) -> Result<(), Error> {
match &mut self.listener_request_stream {
None => {}
Some(listener_request_stream) => {
let request =
listener_request_stream.try_next().await.context("Fetch test stop")?;
if let Some(ComponentEventListenerRequest::OnStop { component, .. }) = request {
self.assert_identity(component, &*TEST_COMPONENT, &*TEST_COMPONENT_REALM_PATH);
} else {
return Err(format_err!("Expected stop event. Got: {:?}", request));
}
}
}
Ok(())
}
/// Utility function to assert that a `SourceIdentity` is the expected one.
fn assert_identity(
&self,
component: SourceIdentity,
name: &str,
expected_realm_path: &[String],
) {
assert_eq!(component.component_name, Some(name.to_string()));
assert_eq!(
component.component_url,
Some(format!(
"fuchsia-pkg://fuchsia.com/component_events_integration_tests#meta/{}",
name
))
);
let instance_id = component.instance_id.expect("no instance id");
assert!(Regex::new("[0-9]+").unwrap().is_match(&instance_id));
let realm_path = component.realm_path.unwrap();
assert_eq!(realm_path, expected_realm_path);
}
}
async fn create_nested_environment(
parent: &EnvironmentProxy,
label: &str,
) -> Result<(EnvironmentProxy, EnvironmentControllerProxy), Error> {
let (new_env, new_env_server_end) =
fidl::endpoints::create_proxy::<EnvironmentMarker>().context("could not create proxy")?;
let (controller, controller_server_end) =
fidl::endpoints::create_proxy().context("could not create proxy")?;
let mut env_options = EnvironmentOptions {
inherit_parent_services: true,
use_parent_runners: true,
kill_on_oom: true,
delete_storage_on_death: true,
};
parent
.create_nested_environment(
new_env_server_end,
controller_server_end,
label,
None,
&mut env_options,
)
.context("could not create isolated environment")?;
let EnvironmentControllerEvent::OnCreated {} = controller
.take_event_stream()
.next()
.await
.unwrap()
.expect("failed to get env created event");
Ok((new_env, controller))
}
fn listen_for_component_events(
env: &EnvironmentProxy,
) -> Result<ComponentEventListenerRequestStream, Error> {
let (service_provider, server_end) =
fidl::endpoints::create_proxy::<ServiceProviderMarker>().context("create proxy")?;
env.get_services(server_end)?;
let (event_provider, server_end) =
fidl::endpoints::create_proxy::<ComponentEventProviderMarker>().context("create proxy")?;
service_provider.connect_to_service(
ComponentEventProviderMarker::SERVICE_NAME,
server_end.into_channel(),
)?;
let (events_client_end, listener_request_stream) =
fidl::endpoints::create_request_stream::<ComponentEventListenerMarker>()
.context("Create request stream")?;
event_provider.set_listener(events_client_end)?;
Ok(listener_request_stream)
}
fn get_launcher(env: &EnvironmentProxy) -> Result<LauncherProxy, Error> {
let (launcher, launcher_server_end) =
fidl::endpoints::create_proxy().context("could not create proxy")?;
env.get_launcher(launcher_server_end).context("could not get isolated environment launcher")?;
Ok(launcher)
}
// Asserts that we receive the expected events about a component that contains an
// out/diagnostics directory: START, STOP, OUT_DIR_READY.
#[fasync::run_singlethreaded(test)]
async fn test_with_diagnostics() -> Result<(), Error> {
let env = connect_to_service::<EnvironmentMarker>().expect("connect to current environment");
let mut test = ComponentEventsTest::new(&env, true).await?;
let arguments = vec!["with-diagnostics".to_string()];
let launcher = get_launcher(&test.diagnostics_test_env).context("get launcher")?;
let mut app = launch(&launcher, TEST_COMPONENT_URL.to_string(), Some(arguments))
.context("start test component")?;
test.assert_on_start(&*TEST_COMPONENT, &*TEST_COMPONENT_REALM_PATH).await?;
let request = test
.listener_request_stream
.as_mut()
.unwrap()
.try_next()
.await
.context("Fetch test dir ready")?;
if let Some(ComponentEventListenerRequest::OnDiagnosticsDirReady {
component, directory, ..
}) = request
{
test.assert_identity(component, &*TEST_COMPONENT, &*TEST_COMPONENT_REALM_PATH);
let (tree, server_end) =
fidl::endpoints::create_proxy::<TreeMarker>().expect("Create Tree proxy");
fdio::service_connect_at(
directory.channel(),
TreeMarker::SERVICE_NAME,
server_end.into_channel(),
)
.expect("Connect to Tree service");
let hierarchy = reader::read(&tree).await.context("Get inspect hierarchy")?;
assert_inspect_tree!(hierarchy, root: {
"fuchsia.inspect.Health": contains {
status: "OK",
}
});
} else {
return Err(format_err!("Expected diagnostics directory ready event. Got: {:?}", request));
}
app.kill().context("Kill app")?;
test.assert_on_stop().await?;
Ok(())
}
// Asserts that we receive the expected events about a component that doesn't contain an
// out/diagnostics directory: START, STOP.
#[fasync::run_singlethreaded(test)]
async fn test_without_diagnostics() -> Result<(), Error> {
let env = connect_to_service::<EnvironmentMarker>().expect("connect to current environment");
let mut test = ComponentEventsTest::new(&env, true).await?;
let launcher = get_launcher(&test.diagnostics_test_env).context("get launcher")?;
let mut app = launch(&launcher, TEST_COMPONENT_URL.to_string(), None)?;
test.assert_on_start(&*TEST_COMPONENT, &*TEST_COMPONENT_REALM_PATH).await?;
let request = test
.listener_request_stream
.as_mut()
.unwrap()
.try_next()
.on_timeout(TIMEOUT_SECONDS.seconds().after_now(), || Ok(None))
.await
.context("Fetch start event on a")?;
if let Some(ComponentEventListenerRequest::OnDiagnosticsDirReady { .. }) = request {
return Err(format_err!(
"Unexpected diagnostics dir ready event for component without diagnostics"
));
}
app.kill().context("Kill app")?;
test.assert_on_stop().await?;
Ok(())
}
async fn launch_app(
env: &EnvironmentProxy,
url: String,
args: Option<Vec<String>>,
) -> Result<App, Error> {
let app = launch(&get_launcher(&env)?, url, args)?;
let event_stream = app.controller().take_event_stream();
event_stream
.try_filter_map(|event| {
let event = match event {
ComponentControllerEvent::OnDirectoryReady {} => Some(event),
_ => None,
};
future::ready(Ok(event))
})
.next()
.await;
Ok(app)
}
// Tests that START events for components that exist before the listener is attached are
// propagated only to the first ancestor realm with a listener attached.
// This test does the following:
// 1. Create a realm hierarchy under the existing "test_env". This hierarchy looks like
// "test_env -> A -> sub -> comp"
// '-> B -> sub -> comp"
// 2. Start a component in A (that listens for events)
// 3. Start a component in B (no event listening)
// 4. Listen for events
// 5. Even though there's a component running under A, we are never notified about it given that
// given that there's already a listener there.
// TODO(fxbug.dev/66572): reenable
#[ignore]
#[fasync::run_singlethreaded(test)]
async fn test_register_listener_in_subrealm() -> Result<(), Error> {
let env = connect_to_service::<EnvironmentMarker>().expect("connect to current environment");
let mut test = ComponentEventsTest::new(&env, false).await?;
let (env_a, _a_ctrl) = create_nested_environment(&env, "a").await?;
let (env_b, _b_ctrl) = create_nested_environment(&env, "b").await?;
// start our test component in environment a
let arguments = vec!["with-event-listener".to_string()];
let _app_a = launch_app(&env_a, TEST_COMPONENT_WITH_REALM_URL.to_string(), Some(arguments))
.await
.context("launch on a")?;
let _app_b = launch_app(&env_b, TEST_COMPONENT_WITH_REALM_URL.to_string(), None)
.await
.context("launch on b")?;
// Listen for start events. Only events expected are for: self, the component under B,
// and the test component under B.
test.listener_request_stream =
Some(listen_for_component_events(&env).context("get component event provider")?);
let mut events = HashSet::new();
for _ in 0..3 {
let request = test
.listener_request_stream
.as_mut()
.unwrap()
.try_next()
.await
.context("Fetch self start")?;
if let Some(ComponentEventListenerRequest::OnStart { component, .. }) = request {
events.insert((component.component_name.unwrap(), component.realm_path.unwrap()));
} else {
panic!("Expected start event. Got: {:?}", request);
}
}
assert_eq!(
events,
hashset! {
(SELF_COMPONENT.to_string(), vec![]),
(TEST_COMPONENT.to_string(), vec!["b".to_string(), "sub".to_string()]),
(TEST_COMPONENT_WITH_REALM.to_string(), vec!["b".to_string()]),
}
);
// Verify the listener on A is not notified about anything given that B already
// is listening for events.
let request = test
.listener_request_stream
.as_mut()
.unwrap()
.try_next()
.on_timeout(TIMEOUT_SECONDS.seconds().after_now(), || Ok(None))
.await
.context("Fetch start event on a")?;
if let Some(ComponentEventListenerRequest::OnStart { .. }) = request {
return Err(format_err!("Unexpected start event for test component on A"));
}
Ok(())
}