blob: 1ba0e7c6315be3b12f1031ae3de50a8e3e9525af [file]
// Copyright 2026 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.
//! This library provides a runtime harness for the TRF (Test Realm Factory) codegen tool.
//! It offers utilities for interacting with test realms, connecting to mock control
//! proxies, and managing component lifecycle events in test environments.
use anyhow::Context as _;
use fidl::endpoints::DiscoverableProtocolMarker;
use fidl_fuchsia_io as fio;
use fidl_fuchsia_testing_harness::RealmProxy_Proxy;
use fidl_fuchsia_trf_factory::{ConfigOverride, CreateRealmRequest, FactoryMarker};
use futures::channel::mpsc;
use futures::stream::Stream;
use std::pin::Pin;
use std::sync::Mutex;
use std::task::{Context, Poll};
/// Trait implemented by mock control markers to connect to mock control proxies.
pub trait MockControlMarker {
/// The control proxy type associated with this mock.
type ControlProxy;
/// Connects to the mock control proxy from the incoming namespace.
fn connect_from_namespace() -> Result<Self::ControlProxy, anyhow::Error>;
}
impl<P> MockControlMarker for P
where
P: DiscoverableProtocolMarker,
{
type ControlProxy = P::Proxy;
fn connect_from_namespace() -> Result<Self::ControlProxy, anyhow::Error> {
fuchsia_component::client::connect_to_protocol::<P>()
.context("failed to connect to mock control proxy from namespace")
}
}
/// Represents component lifecycle events.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LifecycleEvent {
Started(String),
Stopped(String),
}
/// Stream of component lifecycle events.
pub struct LifecycleStream {
receiver: mpsc::UnboundedReceiver<LifecycleEvent>,
}
impl LifecycleStream {
/// Creates a new `LifecycleStream` backed by an unbounded receiver.
pub fn new(receiver: mpsc::UnboundedReceiver<LifecycleEvent>) -> Self {
Self { receiver }
}
/// Asynchronously receives the next lifecycle event.
pub async fn next_event(&mut self) -> Option<LifecycleEvent> {
use futures::StreamExt as _;
self.receiver.next().await
}
}
impl Stream for LifecycleStream {
type Item = LifecycleEvent;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
Pin::new(&mut self.receiver).poll_next(cx)
}
}
/// Runtime harness wrapper providing helper operations for test realms.
pub struct TestRealm {
realm_proxy: Option<RealmProxy_Proxy>,
lifecycle_senders: Mutex<Vec<mpsc::UnboundedSender<LifecycleEvent>>>,
}
impl Default for TestRealm {
fn default() -> Self {
Self { realm_proxy: None, lifecycle_senders: Mutex::new(Vec::new()) }
}
}
impl TestRealm {
/// Constructs a new `TestRealm` using a newly created realm factory instance.
pub async fn create(overrides: Vec<ConfigOverride>) -> Result<Self, anyhow::Error> {
let factory = fuchsia_component::client::connect_to_protocol::<FactoryMarker>()
.context("failed to connect to Factory")?;
let proxy_client_end = factory
.create_realm(CreateRealmRequest { overrides: Some(overrides), ..Default::default() })
.await?
.map_err(|e| anyhow::format_err!("Factory error: {:?}", e))?;
let proxy = proxy_client_end.into_proxy();
Ok(Self { realm_proxy: Some(proxy), lifecycle_senders: Mutex::new(Vec::new()) })
}
/// Connects to a FIDL protocol exposed by the component under test in the test realm.
pub async fn connect_to_protocol<P: DiscoverableProtocolMarker>(
&self,
) -> Result<P::Proxy, anyhow::Error> {
if let Some(proxy) = &self.realm_proxy {
let (client_end, server_end) = fidl::endpoints::create_proxy::<P>();
proxy
.connect_to_named_protocol(P::PROTOCOL_NAME, server_end.into_channel())
.await
.context("failed to send connect_to_named_protocol request")?
.map_err(|e| anyhow::anyhow!("OperationError: {:?}", e))?;
Ok(client_end)
} else {
fuchsia_component::client::connect_to_protocol::<P>()
.context("failed to connect to protocol from incoming namespace")
}
}
/// Connects to a mock control proxy.
pub fn get_control<M: MockControlMarker>(&self) -> Result<M::ControlProxy, anyhow::Error> {
M::connect_from_namespace()
}
/// Opens an isolated directory at the given path within the exposed directory of the realm.
pub async fn get_directory(&self, path: &str) -> Result<fio::DirectoryProxy, anyhow::Error> {
let canonical_path = fuchsia_fs::canonicalize_path(path);
if let Some(proxy) = &self.realm_proxy {
let (client_end, server_end) = fidl::endpoints::create_proxy::<fio::DirectoryMarker>();
proxy
.open_service(canonical_path, server_end.into_channel())
.await
.context("failed to send open_service request")?
.map_err(|e| anyhow::anyhow!("OperationError: {:?}", e))?;
Ok(client_end)
} else {
let directory_proxy = fuchsia_fs::directory::open_in_namespace(
canonical_path,
fuchsia_fs::PERM_READABLE | fuchsia_fs::PERM_WRITABLE,
)
.context("failed to open directory in namespace")?;
Ok(directory_proxy)
}
}
/// Subscribes to lifecycle events emitted by the test environment.
pub fn subscribe_to_lifecycle(&self) -> Result<LifecycleStream, anyhow::Error> {
let (sender, receiver) = mpsc::unbounded();
self.lifecycle_senders.lock().unwrap().push(sender);
Ok(LifecycleStream::new(receiver))
}
/// Emits a lifecycle event to all subscribed lifecycle streams.
pub fn emit_lifecycle_event(&self, event: LifecycleEvent) {
let mut senders = self.lifecycle_senders.lock().unwrap();
senders.retain(|sender| sender.unbounded_send(event.clone()).is_ok());
}
pub async fn stop_component(&self, name: &str) -> Result<(), anyhow::Error> {
let lifecycle =
self.connect_to_protocol::<fidl_fuchsia_sys2::LifecycleControllerMarker>().await?;
lifecycle
.stop_instance(&format!("./{}", name))
.await?
.map_err(|e| anyhow::format_err!("{:?}", e))?;
self.emit_lifecycle_event(LifecycleEvent::Stopped(name.to_string()));
Ok(())
}
pub async fn is_running(&self, name: &str) -> Result<bool, anyhow::Error> {
let query = self.connect_to_protocol::<fidl_fuchsia_sys2::RealmQueryMarker>().await?;
let info = query
.get_instance(&format!("./{}", name))
.await?
.map_err(|e| anyhow::format_err!("{:?}", e))?;
if let Some(resolved) = info.resolved_info {
Ok(resolved.execution_info.is_some())
} else {
Ok(false)
}
}
pub async fn start_component(&self, name: &str) -> Result<(), anyhow::Error> {
let lifecycle =
self.connect_to_protocol::<fidl_fuchsia_sys2::LifecycleControllerMarker>().await?;
let (_, binder) = fidl::endpoints::create_endpoints();
lifecycle
.start_instance(&format!("./{}", name), binder)
.await?
.map_err(|e| anyhow::format_err!("{:?}", e))?;
self.emit_lifecycle_event(LifecycleEvent::Started(name.to_string()));
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use fidl::endpoints::{ServerEnd, create_proxy_and_stream};
use fidl_fuchsia_sys2 as fsys2;
use fidl_fuchsia_testing_harness::{RealmProxy_Marker, RealmProxy_Request};
use fidl_fuchsia_trf_factory::FactoryMarker;
use futures::StreamExt;
#[fuchsia::test]
async fn test_lifecycle_stream_and_realm_emit() {
let realm = TestRealm::default();
let mut stream = realm.subscribe_to_lifecycle().unwrap();
realm.emit_lifecycle_event(LifecycleEvent::Started("hello".to_string()));
realm.emit_lifecycle_event(LifecycleEvent::Stopped("hello".to_string()));
assert_eq!(stream.next_event().await, Some(LifecycleEvent::Started("hello".to_string())));
assert_eq!(stream.next_event().await, Some(LifecycleEvent::Stopped("hello".to_string())));
}
#[fuchsia::test]
async fn test_create_failure_no_namespace() {
let res = TestRealm::create(vec![]).await;
assert!(res.is_err());
}
#[fuchsia::test]
async fn test_get_control_success_lazy() {
let realm = TestRealm::default();
let control = realm.get_control::<FactoryMarker>();
assert!(control.is_ok());
}
#[fuchsia::test]
async fn test_connect_to_protocol_no_proxy() {
let realm = TestRealm::default();
let res = realm.connect_to_protocol::<FactoryMarker>().await;
assert!(res.is_ok());
}
#[fuchsia::test]
async fn test_get_directory_no_proxy() {
let realm = TestRealm::default();
let dir = realm.get_directory("/pkg").await;
assert!(dir.is_err()); // /pkg is not writable
}
#[fuchsia::test]
async fn test_connect_to_protocol_with_proxy() {
let (proxy, mut stream) = create_proxy_and_stream::<RealmProxy_Marker>();
let realm =
TestRealm { realm_proxy: Some(proxy), lifecycle_senders: Mutex::new(Vec::new()) };
let task = fuchsia_async::Task::local(async move {
if let Some(Ok(RealmProxy_Request::ConnectToNamedProtocol {
protocol,
responder,
..
})) = stream.next().await
{
assert_eq!(protocol, fsys2::LifecycleControllerMarker::PROTOCOL_NAME);
let _ = responder.send(Ok(()));
}
});
let res = realm.connect_to_protocol::<fsys2::LifecycleControllerMarker>().await;
assert!(res.is_ok());
task.await;
}
#[fuchsia::test]
async fn test_get_directory_with_proxy() {
let (proxy, mut stream) = create_proxy_and_stream::<RealmProxy_Marker>();
let realm =
TestRealm { realm_proxy: Some(proxy), lifecycle_senders: Mutex::new(Vec::new()) };
let task = fuchsia_async::Task::local(async move {
if let Some(Ok(RealmProxy_Request::OpenService { responder, .. })) = stream.next().await
{
let _ = responder.send(Ok(()));
}
});
let res = realm.get_directory("/some/path").await;
assert!(res.is_ok());
task.await;
}
#[fuchsia::test]
async fn test_start_and_stop_component() {
let (proxy, mut stream) = create_proxy_and_stream::<RealmProxy_Marker>();
let realm =
TestRealm { realm_proxy: Some(proxy), lifecycle_senders: Mutex::new(Vec::new()) };
let task = fuchsia_async::Task::local(async move {
if let Some(Ok(RealmProxy_Request::ConnectToNamedProtocol {
protocol,
server_end,
responder,
})) = stream.next().await
{
assert_eq!(protocol, fsys2::LifecycleControllerMarker::PROTOCOL_NAME);
let _ = responder.send(Ok(()));
let mut lc_stream =
ServerEnd::<fsys2::LifecycleControllerMarker>::new(server_end).into_stream();
if let Some(Ok(fsys2::LifecycleControllerRequest::StartInstance {
moniker,
responder,
..
})) = lc_stream.next().await
{
assert_eq!(moniker, "./test_start");
let _ = responder.send(Ok(()));
}
}
if let Some(Ok(RealmProxy_Request::ConnectToNamedProtocol {
protocol,
server_end,
responder,
})) = stream.next().await
{
assert_eq!(protocol, fsys2::LifecycleControllerMarker::PROTOCOL_NAME);
let _ = responder.send(Ok(()));
let mut lc_stream =
ServerEnd::<fsys2::LifecycleControllerMarker>::new(server_end).into_stream();
if let Some(Ok(fsys2::LifecycleControllerRequest::StopInstance {
moniker,
responder,
..
})) = lc_stream.next().await
{
assert_eq!(moniker, "./test_stop");
let _ = responder.send(Ok(()));
}
}
});
realm.start_component("test_start").await.expect("start failed");
realm.stop_component("test_stop").await.expect("stop failed");
task.await;
}
}