blob: 00b39e96985b984e6b14d3c491dacf9cc083d7e7 [file] [log] [blame]
// Copyright 2023 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::{bail, format_err, Result},
fdio::Namespace,
fidl::endpoints::{
create_endpoints, ClientEnd, DiscoverableProtocolMarker, Proxy, ServiceMarker, ServiceProxy,
},
fidl_fuchsia_component as fcomponent, fidl_fuchsia_component_sandbox as fsandbox,
fidl_fuchsia_io as fio,
fidl_fuchsia_testing_harness::{RealmProxy_Marker, RealmProxy_Proxy},
fuchsia_component::client::connect_to_protocol,
std::fmt::Debug,
uuid::Uuid,
};
pub mod error;
pub use error::Error;
/// A thin wrapper that represents the namespace created by [extend_namespace].
///
/// Users can obtain the path to the namespace from [InstalledNamespace::prefix] and pass that
/// to capability connection APIs such as [fuchsia-component]'s [client::connect_to_protocol_at]
/// to access capabilities in the namespace.
///
/// Furthermore, the [InstalledNamespace] acts as an RAII container for the capabilities. When
/// the [InstalledNamespace] is dropped, the test realm factory server may free state associated
/// with serving those capabilities. Therefore, the test should only drop this once it no longer
/// needs to connect to the capabilities or needs activity performed on their behalf.
pub struct InstalledNamespace {
prefix: String,
/// This is not used, but it keeps the RealmFactory connection alive.
///
/// The RealmFactory server may use this connection to pin the lifetime of the realm created
/// for the test.
_realm_factory: fidl::AsyncChannel,
}
impl InstalledNamespace {
pub fn prefix(&self) -> &str {
&self.prefix
}
}
impl Drop for InstalledNamespace {
fn drop(&mut self) {
let Ok(namespace) = Namespace::installed() else {
return;
};
let _ = namespace.unbind(&self.prefix);
}
}
/// Converts the given dictionary to a namespace and adds it this component's namespace,
/// thinly wrapped by the returned [InstalledNamespace].
///
/// Users can obtain the path to the namespace from [InstalledNamespace::prefix] and pass that
/// to capability connection APIs such as [fuchsia-component]'s [client::connect_to_protocol_at]
/// to access capabilities in the namespace.
///
/// Furthermore, the [InstalledNamespace] acts as an RAII container for the capabilities. When
/// the [InstalledNamespace] is dropped, the test realm factory server may free state associated
/// with serving those capabilities. Therefore, the test should only drop this once it no longer
/// needs to connect to the capabilities or needs activity performed on their behalf.
pub async fn extend_namespace<T>(
realm_factory: T,
dictionary: ClientEnd<fsandbox::DictionaryMarker>,
) -> Result<InstalledNamespace>
where
T: Proxy + Debug,
{
let namespace_proxy = connect_to_protocol::<fcomponent::NamespaceMarker>()?;
// TODO: What should we use for the namespace's unique id? Could also
// consider an atomic counter, or the name of the test
let prefix = format!("/dict-{}", Uuid::new_v4());
let dicts = vec![fcomponent::NamespaceInputEntry { path: prefix.clone().into(), dictionary }];
let mut namespace_entries =
namespace_proxy.create(dicts).await?.map_err(|e| format_err!("{:?}", e))?;
let namespace = Namespace::installed()?;
let count = namespace_entries.len();
if count != 1 {
bail!(
"namespace {prefix} should have exactly one entry but it has {count}. This suggests a \
bug in the namespace protocol. {namespace_entries:?}"
);
}
let entry = namespace_entries.remove(0);
if entry.path.is_none() || entry.directory.is_none() {
bail!(
"namespace {prefix} contains incomplete entry. This suggests a bug in the namespace \
protocol {entry:?}"
);
}
if entry.path.as_ref().unwrap() != &prefix {
bail!(
"namespace {prefix} does not match path. This suggests a bug in the namespace protocol. \
{entry:?}"
);
}
namespace.bind(&prefix, entry.directory.unwrap())?;
Ok(InstalledNamespace { prefix, _realm_factory: realm_factory.into_channel().unwrap() })
}
// RealmProxyClient is a client for fuchsia.testing.harness.RealmProxy.
//
// The calling component must have a handle to the RealmProxy protocol in
// order to use this struct. Once the caller has connected to the RealmProxy
// service, they can access the other services in the proxied test realm by
// calling [connect_to_protocol].
//
// # Example Usage
//
// ```
// let realm_proxy = RealmProxyClient::connect()?;
// let echo = realm_proxy.connect_to_protocol::<EchoMarker>().await?;
// ```
pub struct RealmProxyClient {
inner: RealmProxy_Proxy,
}
impl From<RealmProxy_Proxy> for RealmProxyClient {
fn from(value: RealmProxy_Proxy) -> Self {
Self { inner: value }
}
}
impl From<ClientEnd<RealmProxy_Marker>> for RealmProxyClient {
fn from(value: ClientEnd<RealmProxy_Marker>) -> Self {
let inner = value.into_proxy().expect("ClientEnd::into_proxy");
Self { inner }
}
}
impl RealmProxyClient {
// Connects to the RealmProxy service.
pub fn connect() -> Result<Self, anyhow::Error> {
let inner = connect_to_protocol::<RealmProxy_Marker>()?;
Ok(Self { inner })
}
// Connects to the protocol marked by [T] via the proxy.
//
// Returns an error if the connection fails.
pub async fn connect_to_protocol<T: DiscoverableProtocolMarker>(
&self,
) -> Result<T::Proxy, anyhow::Error> {
self.connect_to_named_protocol::<T>(T::PROTOCOL_NAME).await
}
// Connects to the protocol with the given name, via the proxy.
//
// Returns an error if the connection fails.
pub async fn connect_to_named_protocol<T: DiscoverableProtocolMarker>(
&self,
protocol_name: &str,
) -> Result<T::Proxy, anyhow::Error> {
let (client, server) = create_endpoints::<T>();
let res =
self.inner.connect_to_named_protocol(protocol_name, server.into_channel()).await?;
if let Some(op_err) = res.err() {
bail!("{:?}", op_err);
}
Ok(client.into_proxy()?)
}
// Opens the given service capability, via the proxy.
//
// See https://fuchsia.dev/fuchsia-src/concepts/components/v2/capabilities/service
// for more information about service capabilities.
//
// Returns an error if the connection fails.
pub async fn open_service<T: ServiceMarker>(
&self,
) -> Result<fio::DirectoryProxy, anyhow::Error> {
let (client, server) = create_endpoints::<fio::DirectoryMarker>();
let res = self.inner.open_service(T::SERVICE_NAME, server.into_channel()).await?;
if let Some(op_err) = res.err() {
bail!("{:?}", op_err);
}
Ok(client.into_proxy()?)
}
// Connects to the given service instance, via the proxy.
//
// See https://fuchsia.dev/fuchsia-src/concepts/components/v2/capabilities/service
// for more information about service capabilities.
//
// Returns an error if the connection fails.
pub async fn connect_to_service_instance<T: ServiceMarker>(
&self,
instance: &str,
) -> Result<T::Proxy, anyhow::Error> {
let (client, server) = create_endpoints::<fio::DirectoryMarker>();
let res = self
.inner
.connect_to_service_instance(T::SERVICE_NAME, instance, server.into_channel())
.await?;
if let Some(op_err) = res.err() {
bail!("{:?}", op_err);
}
Ok(T::Proxy::from_member_opener(Box::new(
fuchsia_component::client::ServiceInstanceDirectory(client.into_proxy()?),
)))
}
}