blob: 5d2f2500cf479c2a37f5ec7e1329412965caa2dc [file] [log] [blame] [edit]
// Copyright 2022 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::fake_appmgr::{CreateComponentFn, FakeAppmgr},
anyhow::{anyhow, Context, Error},
fidl::endpoints::{create_endpoints, create_proxy, ServerEnd},
fidl_fuchsia_element as felement, fidl_fuchsia_examples as fexamples, fidl_fuchsia_io as fio,
fidl_fuchsia_modular as fmodular, fidl_fuchsia_modular_internal as fmodular_internal,
fidl_fuchsia_sys as fsys, fidl_fuchsia_ui_app as fapp, fuchsia_async as fasync,
fuchsia_component::{
client::connect_to_protocol_at_dir_root,
server::{ServiceFs, ServiceObj},
},
fuchsia_component_test::{
Capability, ChildOptions, ChildRef, LocalComponentHandles, RealmBuilder, Ref, Route,
},
fuchsia_scenic as scenic, fuchsia_zircon as zx,
fuchsia_zircon::Peered,
futures::{channel::mpsc, prelude::*},
std::collections::HashMap,
std::sync::Arc,
vfs::{directory::entry::DirectoryEntry, file::vmo::read_only_static, pseudo_directory},
};
mod fake_appmgr;
const SESSIONMGR_URL: &str = "#meta/sessionmgr.cm";
const MOCK_COBALT_URL: &str = "#meta/mock_cobalt.cm";
const TEST_SESSION_SHELL_URL: &str =
"fuchsia-pkg://fuchsia.com/test_session_shell#meta/test_session_shell.cmx";
const TEST_MOD_URL: &str = "fuchsia-pkg://fuchsia.com/test_mod#meta/test_mod.cmx";
struct TestFixture {
pub builder: RealmBuilder,
pub sessionmgr: ChildRef,
}
impl TestFixture {
async fn new() -> Result<TestFixture, Error> {
let builder = RealmBuilder::new().await?;
// Add mock_cobalt to the realm.
let mock_cobalt =
builder.add_child("mock_cobalt", MOCK_COBALT_URL, ChildOptions::new()).await?;
builder
.add_route(
Route::new()
.capability(Capability::protocol_by_name("fuchsia.logger.LogSink"))
.from(Ref::parent())
.to(&mock_cobalt),
)
.await?;
// Add sessionmgr to the realm.
let sessionmgr =
builder.add_child("sessionmgr", SESSIONMGR_URL, ChildOptions::new()).await?;
builder
.add_route(
Route::new()
.capability(Capability::protocol_by_name(
"fuchsia.metrics.MetricEventLoggerFactory",
))
.from(&mock_cobalt)
.to(&sessionmgr),
)
.await?;
builder
.add_route(
Route::new()
.capability(Capability::protocol_by_name("fuchsia.logger.LogSink"))
.capability(Capability::protocol_by_name("fuchsia.tracing.provider.Registry"))
.from(Ref::parent())
.to(&sessionmgr),
)
.await?;
// Expose sessionmgr's protocols to the test.
builder
.add_route(
Route::new()
.capability(Capability::protocol::<fmodular_internal::SessionmgrMarker>())
.from(&sessionmgr)
.to(Ref::parent()),
)
.await?;
// Add a placeholder component and routes for capabilities that are not
// expected to be used in this test scenario.
let placeholder = builder
.add_local_child(
"placeholder",
|_: LocalComponentHandles| Box::pin(async move { Ok(()) }),
ChildOptions::new(),
)
.await?;
builder
.add_route(
Route::new()
.capability(Capability::protocol_by_name("fuchsia.intl.PropertyProvider"))
.from(&placeholder)
.to(&sessionmgr),
)
.await?;
return Ok(TestFixture { builder, sessionmgr });
}
async fn with_config<Bytes>(self, config: Bytes) -> Result<TestFixture, Error>
where
Bytes: AsRef<[u8]> + Send + Sync + 'static,
{
let config_data_dir = pseudo_directory! {
"startup.config" => read_only_static(config),
};
// Add a local component that provides the `config-data` directory to the realm.
let config_data_server = self
.builder
.add_local_child(
"config-data-server",
move |handles| {
let proxy = spawn_vfs(config_data_dir.clone());
async move {
let _ = &handles;
let mut fs = ServiceFs::new();
fs.add_remote("config-data", proxy);
fs.serve_connection(handles.outgoing_dir)
.expect("failed to serve config-data ServiceFs");
fs.collect::<()>().await;
Ok::<(), anyhow::Error>(())
}
.boxed()
},
ChildOptions::new(),
)
.await?;
self.builder
.add_route(
Route::new()
.capability(
Capability::directory("config-data")
.path("/config-data")
.rights(fio::R_STAR_DIR),
)
.from(&config_data_server)
.to(&self.sessionmgr),
)
.await?;
Ok(self)
}
async fn with_fake_appmgr(self, fake_appmgr: Arc<FakeAppmgr>) -> Result<TestFixture, Error> {
let appmgr = self
.builder
.add_local_child(
"appmgr",
move |handles| Box::pin(fake_appmgr.clone().serve_local_child(handles)),
ChildOptions::new(),
)
.await?;
self.builder
.add_route(
Route::new()
.capability(Capability::protocol::<fsys::EnvironmentMarker>())
.capability(Capability::protocol::<fsys::LauncherMarker>())
.from(&appmgr)
.to(&self.sessionmgr),
)
.await?;
Ok(self)
}
}
// Returns a `DirectoryProxy` that serves the directory entry `dir`.
fn spawn_vfs(dir: Arc<dyn DirectoryEntry>) -> fio::DirectoryProxy {
let (client_end, server_end) = create_endpoints::<fio::DirectoryMarker>().unwrap();
let scope = vfs::execution_scope::ExecutionScope::new();
dir.open(
scope,
fio::OpenFlags::RIGHT_READABLE,
0,
vfs::path::Path::dot(),
ServerEnd::new(server_end.into_channel()),
);
client_end.into_proxy().unwrap()
}
// Tests that sessionmgr starts and attempts to launch the session shell.
#[fuchsia::test]
async fn test_launch_sessionmgr() -> Result<(), Error> {
let fixture = TestFixture::new()
.await?
.with_config(
r#"{
"basemgr": {
"enable_cobalt": false,
"session_shells": [
{
"url": "fuchsia-pkg://fuchsia.com/test_session_shell#meta/test_session_shell.cmx"
}
]
},
"sessionmgr": {
"enable_cobalt": false
}
}"#,
)
.await?;
let (mut shell_launched_sender, shell_launched_receiver) = mpsc::channel(1);
let mock_session_shell: CreateComponentFn = Box::new(move |launch_info: fsys::LaunchInfo| {
let mut outgoing_fs = ServiceFs::<ServiceObj<'_, ()>>::new();
outgoing_fs
.serve_connection(launch_info.directory_request.unwrap())
.expect("failed to serve outgoing fs");
fasync::Task::local(outgoing_fs.collect::<()>()).detach();
shell_launched_sender.try_send(()).expect("failed to send shell launched");
});
let mut mock_v1_components = HashMap::new();
mock_v1_components.insert(TEST_SESSION_SHELL_URL.to_string(), mock_session_shell);
let fake_appmgr = FakeAppmgr::new(mock_v1_components);
let fixture = fixture.with_fake_appmgr(fake_appmgr).await?;
let instance = fixture.builder.build().await?;
let (session_context_client_end, _session_context_server_end) =
create_endpoints::<fmodular_internal::SessionContextMarker>()?;
let (_services_from_sessionmgr, services_from_sessionmgr_server_end) =
create_proxy::<fio::DirectoryMarker>()?;
let mut link_token_pair = scenic::flatland::ViewCreationTokenPair::new()?;
let mut services_for_agents_fs = ServiceFs::<ServiceObj<'_, ()>>::new();
let sessionmgr_proxy = instance
.root
.connect_to_protocol_at_exposed_dir::<fmodular_internal::SessionmgrMarker>()?;
sessionmgr_proxy.initialize(
"test_session_id",
session_context_client_end,
&mut services_for_agents_fs.host_services_list()?,
services_from_sessionmgr_server_end,
&mut link_token_pair.view_creation_token,
)?;
fasync::Task::local(services_for_agents_fs.collect()).detach();
let () = shell_launched_receiver
.take(1)
.next()
.await
.ok_or_else(|| anyhow!("expected shell launched message"))?;
instance.destroy().await?;
Ok(())
}
// Tests that an agent (in this case, the session shell) can connect to a protocol exposed by a
// v2 component through the `fuchsia.modular.Agent` protocol.
#[fuchsia::test]
async fn test_v2_modular_agents() -> Result<(), Error> {
let fixture = TestFixture::new()
.await?
.with_config(
r#"{
"basemgr": {
"enable_cobalt": false,
"session_shells": [
{
"url": "fuchsia-pkg://fuchsia.com/test_session_shell#meta/test_session_shell.cmx"
}
]
},
"sessionmgr": {
"enable_cobalt": false,
"agent_service_index": [
{
"service_name": "fuchsia.examples.Echo",
"agent_url": "fuchsia-pkg://fuchsia.com/test_agent#meta/test_agent.cmx"
}
],
"v2_modular_agents": [
{
"service_name": "fuchsia.modular.Agent.test_agent",
"agent_url": "fuchsia-pkg://fuchsia.com/test_agent#meta/test_agent.cmx"
}
]
}
}"#,
)
.await?;
let (shell_called_echo_sender, shell_called_echo_receiver) = mpsc::channel(1);
let mock_session_shell: CreateComponentFn = Box::new(move |launch_info: fsys::LaunchInfo| {
let mut shell_called_echo_sender = shell_called_echo_sender.clone();
fasync::Task::local(async move {
let mut outgoing_fs = ServiceFs::<ServiceObj<'_, ()>>::new();
outgoing_fs
.serve_connection(launch_info.directory_request.unwrap())
.expect("failed to serve outgoing fs");
let svc = launch_info.additional_services.unwrap();
assert!(svc.names.contains(&"fuchsia.examples.Echo".to_string()));
let provider =
svc.provider.unwrap().into_proxy().expect("failed to create ServiceProvider proxy");
let (echo, echo_server_end) =
create_proxy::<fexamples::EchoMarker>().expect("failed to create Echo endpoints");
provider
.connect_to_service("fuchsia.examples.Echo", echo_server_end.into_channel())
.expect("failed to call ConnectToService");
let result = echo.echo_string("hello").await.expect("failed to call EchoString");
assert_eq!("hello", result);
shell_called_echo_sender.try_send(()).expect("failed to send shell called echo");
})
.detach();
});
let mut mock_v1_components = HashMap::new();
mock_v1_components.insert(TEST_SESSION_SHELL_URL.to_string(), mock_session_shell);
let fake_appmgr = FakeAppmgr::new(mock_v1_components);
let fixture = fixture.with_fake_appmgr(fake_appmgr).await?;
let instance = fixture.builder.build().await?;
let (session_context_client_end, _session_context_server_end) =
create_endpoints::<fmodular_internal::SessionContextMarker>()?;
let (_services_from_sessionmgr, services_from_sessionmgr_server_end) =
create_proxy::<fio::DirectoryMarker>()?;
let mut link_token_pair = scenic::flatland::ViewCreationTokenPair::new()?;
// `fuchsia.modular.Agent.test_agent` represents a v2 component whose Agent protocol
// is routed to sessionmgr through /svc_for_v1_sessionmgr.
let mut services_for_agents_fs = ServiceFs::new();
services_for_agents_fs.add_fidl_service_at(
"fuchsia.modular.Agent.test_agent",
move |stream: fmodular::AgentRequestStream| {
fasync::Task::local(async move { serve_echo_agent(stream).await }).detach();
},
);
let sessionmgr_proxy = instance
.root
.connect_to_protocol_at_exposed_dir::<fmodular_internal::SessionmgrMarker>()?;
sessionmgr_proxy.initialize(
"test_session_id",
session_context_client_end,
&mut services_for_agents_fs.host_services_list()?,
services_from_sessionmgr_server_end,
&mut link_token_pair.view_creation_token,
)?;
fasync::Task::local(services_for_agents_fs.collect()).detach();
let () = shell_called_echo_receiver
.take(1)
.next()
.await
.ok_or_else(|| anyhow!("expected shell called echo message"))?;
instance.destroy().await?;
Ok(())
}
#[fuchsia::test]
async fn test_v2_session_shell() -> Result<(), Error> {
let fixture = TestFixture::new()
.await?
.with_config(
r#"{
"basemgr": {
"enable_cobalt": false
},
"sessionmgr": {
"enable_cobalt": false,
"present_mods_as_stories": true
}
}"#,
)
.await?;
// The mock module, TEST_MOD_URL, serves fuchsia.ui.app.ViewProvider and signals the received
// ViewCreationToken channel.
let mock_mod: CreateComponentFn = Box::new(move |launch_info: fsys::LaunchInfo| {
fasync::Task::local(async move {
let mut outgoing_fs = ServiceFs::new();
outgoing_fs.add_fidl_service(move |stream: fapp::ViewProviderRequestStream| {
fasync::Task::local(
stream
.try_for_each(move |req| {
match req {
fapp::ViewProviderRequest::CreateView2 {
args,
..
} => {
args.view_creation_token.unwrap().value
.signal_peer(zx::Signals::NONE, zx::Signals::USER_0)
.expect("Signalling viewport_creation_token");
}
_ => panic!("expected sessionmgr to get mod view through CreateViewWithViewRef")
};
futures::future::ready(Ok(()))
})
.unwrap_or_else(|e| {
panic!("error serving ViewProvider: {:?}", e)
}),
)
.detach()
});
outgoing_fs
.serve_connection(launch_info.directory_request.unwrap())
.expect("failed to serve outgoing fs");
outgoing_fs.collect::<()>().await;
})
.detach();
});
let mut mock_v1_components = HashMap::new();
mock_v1_components.insert(TEST_MOD_URL.to_string(), mock_mod);
let fake_appmgr = FakeAppmgr::new(mock_v1_components);
let fixture = fixture.with_fake_appmgr(fake_appmgr).await?;
let instance = fixture.builder.build().await?;
let (session_context_client_end, _session_context_server_end) =
create_endpoints::<fmodular_internal::SessionContextMarker>()?;
let (services_from_sessionmgr, services_from_sessionmgr_server_end) =
create_proxy::<fio::DirectoryMarker>()?;
let mut link_token_pair = scenic::flatland::ViewCreationTokenPair::new()?;
let (view_spec_sender, view_spec_receiver) = mpsc::channel(1);
// `fuchsia.element.GraphicalPresenter` is served via `services_for_agents_fs`
// to simulate a v2 session shell component.
let mut services_for_agents_fs = ServiceFs::new();
services_for_agents_fs.add_fidl_service(
move |stream: felement::GraphicalPresenterRequestStream| {
let view_spec_sender = view_spec_sender.clone();
fasync::Task::local(async move {
serve_graphical_presenter(stream, view_spec_sender).await
})
.detach();
},
);
let sessionmgr_proxy = instance
.root
.connect_to_protocol_at_exposed_dir::<fmodular_internal::SessionmgrMarker>()?;
sessionmgr_proxy.initialize(
"test_session_id",
session_context_client_end,
&mut services_for_agents_fs.host_services_list()?,
services_from_sessionmgr_server_end,
&mut link_token_pair.view_creation_token,
)?;
fasync::Task::local(services_for_agents_fs.collect()).detach();
// Create a story to launch a mod.
let puppet_master =
connect_to_protocol_at_dir_root::<fmodular::PuppetMasterMarker>(&services_from_sessionmgr)?;
let (story_puppet_master, story_puppet_master_server_end) =
create_proxy::<fmodular::StoryPuppetMasterMarker>()?;
puppet_master.control_story("test_story", story_puppet_master_server_end)?;
// Launch the test mod.
story_puppet_master.enqueue(
&mut vec![fmodular::StoryCommand::AddMod(fmodular::AddMod {
mod_name: vec![],
mod_name_transitional: Some("test_mod".to_string()),
surface_relation: fmodular::SurfaceRelation {
arrangement: fmodular::SurfaceArrangement::None,
dependency: fmodular::SurfaceDependency::None,
emphasis: 1.0,
},
surface_parent_mod_name: None,
intent: fmodular::Intent {
action: None,
handler: Some(TEST_MOD_URL.to_string()),
parameters: None,
},
})]
.iter_mut(),
)?;
let execute_result = story_puppet_master.execute().await?;
assert_eq!(fmodular::ExecuteStatus::Ok, execute_result.status);
let view_spec = view_spec_receiver
.take(1)
.next()
.await
.ok_or_else(|| anyhow!("expected to receive ViewSpec"))?;
// The viewport creation token from graphical presenter should be signalled by the mock module,
// ensuring that both have endpoints for the view creation channel pair.
fasync::OnSignals::new(&view_spec.viewport_creation_token.unwrap().value, zx::Signals::USER_0)
.await
.unwrap();
instance.destroy().await?;
Ok(())
}
// Serves the `fuchsia.element.GraphicalPresenter` protocol.
async fn serve_graphical_presenter(
mut stream: felement::GraphicalPresenterRequestStream,
mut view_spec_sender: mpsc::Sender<felement::ViewSpec>,
) {
let mut view_controller_requests = vec![];
while let Some(felement::GraphicalPresenterRequest::PresentView {
view_spec,
view_controller_request,
responder,
..
}) = stream.try_next().await.expect("failed to serve GraphicalPresenter")
{
// Save the ViewController request so sessionmgr doesn't think that the view was dismissed.
view_controller_requests.push(view_controller_request);
view_spec_sender.try_send(view_spec).expect("failed to send ViewSpec");
let _ = responder.send(&mut Ok(()));
}
}
// Serves the `fuchsia.modular.Agent` protocol that exposes the `fuchsia.examples.Echo`
// protocol through a `ServiceProvider`.
async fn serve_echo_agent(mut stream: fmodular::AgentRequestStream) {
while let Some(fmodular::AgentRequest::Connect { services, .. }) =
stream.try_next().await.expect("failed to serve Agent")
{
fasync::Task::local(async move {
let stream = services.into_stream().expect("failed to create ServiceProvider stream");
serve_echo_serviceprovider(stream).await
})
.detach();
}
}
// Serves the `fuchsia.sys.ServiceProvider` protocol that exposes the
// `fuchsia.examples.Echo` protocol.
async fn serve_echo_serviceprovider(mut stream: fsys::ServiceProviderRequestStream) {
while let Some(fsys::ServiceProviderRequest::ConnectToService {
service_name, channel, ..
}) = stream.try_next().await.expect("failed to serve ServiceProvider")
{
assert_eq!("fuchsia.examples.Echo", service_name);
fasync::Task::local(async move {
let stream: fexamples::EchoRequestStream =
ServerEnd::<fexamples::EchoMarker>::new(channel)
.into_stream()
.expect("failed to create ServiceProvider stream");
serve_echo(stream).await.expect("failed to serve Echo")
})
.detach();
}
}
// Serves the `fuchsia.examples.Echo` protocol.
async fn serve_echo(stream: fexamples::EchoRequestStream) -> Result<(), Error> {
stream
.map(|result| result.context("failed request"))
.try_for_each(|request| async move {
match request {
fexamples::EchoRequest::EchoString { value, responder } => {
responder.send(&value).context("error sending response")?;
}
fexamples::EchoRequest::SendString { value, control_handle } => {
control_handle.send_on_string(&value).context("error sending event")?;
}
}
Ok(())
})
.await
}