Skip to main content

trf_codegen_runtime/
lib.rs

1// Copyright 2026 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5//! This library provides a runtime harness for the TRF (Test Realm Factory) codegen tool.
6//! It offers utilities for interacting with test realms, connecting to mock control
7//! proxies, and managing component lifecycle events in test environments.
8
9use anyhow::Context as _;
10use fidl::endpoints::DiscoverableProtocolMarker;
11use fidl_fuchsia_io as fio;
12use fidl_fuchsia_testing_harness::RealmProxy_Proxy;
13use fidl_fuchsia_trf_factory::{ConfigOverride, CreateRealmRequest, FactoryMarker};
14use futures::channel::mpsc;
15use futures::stream::Stream;
16use std::pin::Pin;
17use std::sync::Mutex;
18use std::task::{Context, Poll};
19
20/// Trait implemented by mock control markers to connect to mock control proxies.
21pub trait MockControlMarker {
22    /// The control proxy type associated with this mock.
23    type ControlProxy;
24
25    /// Connects to the mock control proxy from the incoming namespace.
26    fn connect_from_namespace() -> Result<Self::ControlProxy, anyhow::Error>;
27}
28
29impl<P> MockControlMarker for P
30where
31    P: DiscoverableProtocolMarker,
32{
33    type ControlProxy = P::Proxy;
34
35    fn connect_from_namespace() -> Result<Self::ControlProxy, anyhow::Error> {
36        fuchsia_component::client::connect_to_protocol::<P>()
37            .context("failed to connect to mock control proxy from namespace")
38    }
39}
40
41/// Represents component lifecycle events.
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub enum LifecycleEvent {
44    Started(String),
45    Stopped(String),
46}
47
48/// Stream of component lifecycle events.
49pub struct LifecycleStream {
50    receiver: mpsc::UnboundedReceiver<LifecycleEvent>,
51}
52
53impl LifecycleStream {
54    /// Creates a new `LifecycleStream` backed by an unbounded receiver.
55    pub fn new(receiver: mpsc::UnboundedReceiver<LifecycleEvent>) -> Self {
56        Self { receiver }
57    }
58
59    /// Asynchronously receives the next lifecycle event.
60    pub async fn next_event(&mut self) -> Option<LifecycleEvent> {
61        use futures::StreamExt as _;
62        self.receiver.next().await
63    }
64}
65
66impl Stream for LifecycleStream {
67    type Item = LifecycleEvent;
68
69    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
70        Pin::new(&mut self.receiver).poll_next(cx)
71    }
72}
73
74/// Runtime harness wrapper providing helper operations for test realms.
75pub struct TestRealm {
76    realm_proxy: Option<RealmProxy_Proxy>,
77    lifecycle_senders: Mutex<Vec<mpsc::UnboundedSender<LifecycleEvent>>>,
78}
79
80impl Default for TestRealm {
81    fn default() -> Self {
82        Self { realm_proxy: None, lifecycle_senders: Mutex::new(Vec::new()) }
83    }
84}
85
86impl TestRealm {
87    /// Constructs a new `TestRealm` using a newly created realm factory instance.
88    pub async fn create(overrides: Vec<ConfigOverride>) -> Result<Self, anyhow::Error> {
89        let factory = fuchsia_component::client::connect_to_protocol::<FactoryMarker>()
90            .context("failed to connect to Factory")?;
91        let proxy_client_end = factory
92            .create_realm(CreateRealmRequest { overrides: Some(overrides), ..Default::default() })
93            .await?
94            .map_err(|e| anyhow::format_err!("Factory error: {:?}", e))?;
95        let proxy = proxy_client_end.into_proxy();
96        Ok(Self { realm_proxy: Some(proxy), lifecycle_senders: Mutex::new(Vec::new()) })
97    }
98
99    /// Connects to a FIDL protocol exposed by the component under test in the test realm.
100    pub async fn connect_to_protocol<P: DiscoverableProtocolMarker>(
101        &self,
102    ) -> Result<P::Proxy, anyhow::Error> {
103        if let Some(proxy) = &self.realm_proxy {
104            let (client_end, server_end) = fidl::endpoints::create_proxy::<P>();
105            proxy
106                .connect_to_named_protocol(P::PROTOCOL_NAME, server_end.into_channel())
107                .await
108                .context("failed to send connect_to_named_protocol request")?
109                .map_err(|e| anyhow::anyhow!("OperationError: {:?}", e))?;
110            Ok(client_end)
111        } else {
112            fuchsia_component::client::connect_to_protocol::<P>()
113                .context("failed to connect to protocol from incoming namespace")
114        }
115    }
116
117    /// Connects to a mock control proxy.
118    pub fn get_control<M: MockControlMarker>(&self) -> Result<M::ControlProxy, anyhow::Error> {
119        M::connect_from_namespace()
120    }
121
122    /// Opens an isolated directory at the given path within the exposed directory of the realm.
123    pub async fn get_directory(&self, path: &str) -> Result<fio::DirectoryProxy, anyhow::Error> {
124        let canonical_path = fuchsia_fs::canonicalize_path(path);
125        if let Some(proxy) = &self.realm_proxy {
126            let (client_end, server_end) = fidl::endpoints::create_proxy::<fio::DirectoryMarker>();
127            proxy
128                .open_service(canonical_path, server_end.into_channel())
129                .await
130                .context("failed to send open_service request")?
131                .map_err(|e| anyhow::anyhow!("OperationError: {:?}", e))?;
132            Ok(client_end)
133        } else {
134            let directory_proxy = fuchsia_fs::directory::open_in_namespace(
135                canonical_path,
136                fuchsia_fs::PERM_READABLE | fuchsia_fs::PERM_WRITABLE,
137            )
138            .context("failed to open directory in namespace")?;
139            Ok(directory_proxy)
140        }
141    }
142
143    /// Subscribes to lifecycle events emitted by the test environment.
144    pub fn subscribe_to_lifecycle(&self) -> Result<LifecycleStream, anyhow::Error> {
145        let (sender, receiver) = mpsc::unbounded();
146        self.lifecycle_senders.lock().unwrap().push(sender);
147        Ok(LifecycleStream::new(receiver))
148    }
149
150    /// Emits a lifecycle event to all subscribed lifecycle streams.
151    pub fn emit_lifecycle_event(&self, event: LifecycleEvent) {
152        let mut senders = self.lifecycle_senders.lock().unwrap();
153        senders.retain(|sender| sender.unbounded_send(event.clone()).is_ok());
154    }
155
156    pub async fn stop_component(&self, name: &str) -> Result<(), anyhow::Error> {
157        let lifecycle =
158            self.connect_to_protocol::<fidl_fuchsia_sys2::LifecycleControllerMarker>().await?;
159        lifecycle
160            .stop_instance(&format!("./{}", name))
161            .await?
162            .map_err(|e| anyhow::format_err!("{:?}", e))?;
163        self.emit_lifecycle_event(LifecycleEvent::Stopped(name.to_string()));
164        Ok(())
165    }
166
167    pub async fn is_running(&self, name: &str) -> Result<bool, anyhow::Error> {
168        let query = self.connect_to_protocol::<fidl_fuchsia_sys2::RealmQueryMarker>().await?;
169        let info = query
170            .get_instance(&format!("./{}", name))
171            .await?
172            .map_err(|e| anyhow::format_err!("{:?}", e))?;
173        if let Some(resolved) = info.resolved_info {
174            Ok(resolved.execution_info.is_some())
175        } else {
176            Ok(false)
177        }
178    }
179
180    pub async fn start_component(&self, name: &str) -> Result<(), anyhow::Error> {
181        let lifecycle =
182            self.connect_to_protocol::<fidl_fuchsia_sys2::LifecycleControllerMarker>().await?;
183        let (_, binder) = fidl::endpoints::create_endpoints();
184        lifecycle
185            .start_instance(&format!("./{}", name), binder)
186            .await?
187            .map_err(|e| anyhow::format_err!("{:?}", e))?;
188        self.emit_lifecycle_event(LifecycleEvent::Started(name.to_string()));
189        Ok(())
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196    use fidl::endpoints::{ServerEnd, create_proxy_and_stream};
197    use fidl_fuchsia_sys2 as fsys2;
198    use fidl_fuchsia_testing_harness::{RealmProxy_Marker, RealmProxy_Request};
199    use fidl_fuchsia_trf_factory::FactoryMarker;
200    use futures::StreamExt;
201
202    #[fuchsia::test]
203    async fn test_lifecycle_stream_and_realm_emit() {
204        let realm = TestRealm::default();
205        let mut stream = realm.subscribe_to_lifecycle().unwrap();
206
207        realm.emit_lifecycle_event(LifecycleEvent::Started("hello".to_string()));
208        realm.emit_lifecycle_event(LifecycleEvent::Stopped("hello".to_string()));
209
210        assert_eq!(stream.next_event().await, Some(LifecycleEvent::Started("hello".to_string())));
211        assert_eq!(stream.next_event().await, Some(LifecycleEvent::Stopped("hello".to_string())));
212    }
213
214    #[fuchsia::test]
215    async fn test_create_failure_no_namespace() {
216        let res = TestRealm::create(vec![]).await;
217        assert!(res.is_err());
218    }
219
220    #[fuchsia::test]
221    async fn test_get_control_success_lazy() {
222        let realm = TestRealm::default();
223        let control = realm.get_control::<FactoryMarker>();
224        assert!(control.is_ok());
225    }
226
227    #[fuchsia::test]
228    async fn test_connect_to_protocol_no_proxy() {
229        let realm = TestRealm::default();
230        let res = realm.connect_to_protocol::<FactoryMarker>().await;
231        assert!(res.is_ok());
232    }
233
234    #[fuchsia::test]
235    async fn test_get_directory_no_proxy() {
236        let realm = TestRealm::default();
237        let dir = realm.get_directory("/pkg").await;
238        assert!(dir.is_err()); // /pkg is not writable
239    }
240
241    #[fuchsia::test]
242    async fn test_connect_to_protocol_with_proxy() {
243        let (proxy, mut stream) = create_proxy_and_stream::<RealmProxy_Marker>();
244        let realm =
245            TestRealm { realm_proxy: Some(proxy), lifecycle_senders: Mutex::new(Vec::new()) };
246
247        let task = fuchsia_async::Task::local(async move {
248            if let Some(Ok(RealmProxy_Request::ConnectToNamedProtocol {
249                protocol,
250                responder,
251                ..
252            })) = stream.next().await
253            {
254                assert_eq!(protocol, fsys2::LifecycleControllerMarker::PROTOCOL_NAME);
255                let _ = responder.send(Ok(()));
256            }
257        });
258
259        let res = realm.connect_to_protocol::<fsys2::LifecycleControllerMarker>().await;
260        assert!(res.is_ok());
261        task.await;
262    }
263
264    #[fuchsia::test]
265    async fn test_get_directory_with_proxy() {
266        let (proxy, mut stream) = create_proxy_and_stream::<RealmProxy_Marker>();
267        let realm =
268            TestRealm { realm_proxy: Some(proxy), lifecycle_senders: Mutex::new(Vec::new()) };
269
270        let task = fuchsia_async::Task::local(async move {
271            if let Some(Ok(RealmProxy_Request::OpenService { responder, .. })) = stream.next().await
272            {
273                let _ = responder.send(Ok(()));
274            }
275        });
276
277        let res = realm.get_directory("/some/path").await;
278        assert!(res.is_ok());
279        task.await;
280    }
281
282    #[fuchsia::test]
283    async fn test_start_and_stop_component() {
284        let (proxy, mut stream) = create_proxy_and_stream::<RealmProxy_Marker>();
285        let realm =
286            TestRealm { realm_proxy: Some(proxy), lifecycle_senders: Mutex::new(Vec::new()) };
287
288        let task = fuchsia_async::Task::local(async move {
289            if let Some(Ok(RealmProxy_Request::ConnectToNamedProtocol {
290                protocol,
291                server_end,
292                responder,
293            })) = stream.next().await
294            {
295                assert_eq!(protocol, fsys2::LifecycleControllerMarker::PROTOCOL_NAME);
296                let _ = responder.send(Ok(()));
297                let mut lc_stream =
298                    ServerEnd::<fsys2::LifecycleControllerMarker>::new(server_end).into_stream();
299                if let Some(Ok(fsys2::LifecycleControllerRequest::StartInstance {
300                    moniker,
301                    responder,
302                    ..
303                })) = lc_stream.next().await
304                {
305                    assert_eq!(moniker, "./test_start");
306                    let _ = responder.send(Ok(()));
307                }
308            }
309            if let Some(Ok(RealmProxy_Request::ConnectToNamedProtocol {
310                protocol,
311                server_end,
312                responder,
313            })) = stream.next().await
314            {
315                assert_eq!(protocol, fsys2::LifecycleControllerMarker::PROTOCOL_NAME);
316                let _ = responder.send(Ok(()));
317                let mut lc_stream =
318                    ServerEnd::<fsys2::LifecycleControllerMarker>::new(server_end).into_stream();
319                if let Some(Ok(fsys2::LifecycleControllerRequest::StopInstance {
320                    moniker,
321                    responder,
322                    ..
323                })) = lc_stream.next().await
324                {
325                    assert_eq!(moniker, "./test_stop");
326                    let _ = responder.send(Ok(()));
327                }
328            }
329        });
330
331        realm.start_component("test_start").await.expect("start failed");
332        realm.stop_component("test_stop").await.expect("stop failed");
333        task.await;
334    }
335}