fdf_component/testing/
harness.rs

1// Copyright 2025 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//! The harness provides a way to spin up drivers for unit testing.
6
7use crate::testing::dut::DriverUnderTest;
8use crate::testing::logsink_connector;
9use crate::testing::node::NodeManager;
10use crate::{Driver, Incoming};
11use anyhow::Result;
12use fdf::{AutoReleaseDispatcher, DispatcherBuilder, WeakDispatcher};
13use fdf_env::Environment;
14use fidl::endpoints::{ClientEnd, Proxy};
15use fidl_fuchsia_driver_framework::Offer;
16use fidl_next::{ClientEnd as NextClientEnd, CompatFrom, ServerEnd as NextServerEnd};
17use fidl_next_fuchsia_component_runner::natural::ComponentNamespaceEntry;
18use fidl_next_fuchsia_driver_framework::DriverStartArgs;
19use fidl_next_fuchsia_driver_framework::natural::Offer as NextOffer;
20use fuchsia_component::directory::open_directory_async;
21use fuchsia_component::server::{ServiceFs, ServiceObj};
22use futures::StreamExt;
23use std::marker::PhantomData;
24use std::sync::{Arc, Weak, mpsc};
25use zx::{HandleBased, Status};
26use {fidl_fuchsia_io as fio, fuchsia_async as fasync};
27
28/// The main test harness for running a driver unit test.
29pub struct TestHarness<D> {
30    fdf_env_environment: Arc<Environment>,
31    node_manager: Arc<NodeManager>,
32    driver: Option<fdf_env::Driver<u32>>,
33    dispatcher: AutoReleaseDispatcher,
34    driver_incoming_dir: ClientEnd<fio::DirectoryMarker>,
35    config_vmo: Option<zx::Vmo>,
36    url: Option<String>,
37    offers: Option<Vec<Offer>>,
38    scope: fasync::Scope,
39    _d: PhantomData<D>,
40}
41
42impl<D: Driver> Default for TestHarness<D> {
43    fn default() -> Self {
44        Self::new()
45    }
46}
47
48impl<D: Driver> TestHarness<D> {
49    /// Creates a new `TestHarness` without a customized driver incoming ServiceFs.
50    pub fn new() -> Self {
51        let scope = fasync::Scope::new();
52        let mut driver_incoming = ServiceFs::new();
53        let env = Arc::new(Environment::start(0).unwrap());
54        let node_manager = NodeManager::new();
55        driver_incoming.dir("svc").add_service_connector(logsink_connector);
56
57        let (driver_incoming_dir_client, driver_incoming_dir_server) = zx::Channel::create();
58        driver_incoming.serve_connection(driver_incoming_dir_server.into()).unwrap();
59        let driver_incoming_dir = driver_incoming_dir_client.into();
60
61        scope.spawn(async move {
62            driver_incoming.collect::<()>().await;
63        });
64
65        // Leak this to a raw, we will reconstitue a Box inside drop.
66        let driver_value_ptr = Box::into_raw(Box::new(0x1234_u32));
67        let driver = env.new_driver(driver_value_ptr);
68        let env_clone = env.clone();
69        let dispatcher_builder =
70            DispatcherBuilder::new().name("test_harness").shutdown_observer(move |dispatcher| {
71                // We verify that the dispatcher has no tasks left queued in it,
72                // just because this is testing code.
73                assert!(!env_clone.dispatcher_has_queued_tasks(dispatcher.as_dispatcher_ref()));
74            });
75        let dispatcher =
76            AutoReleaseDispatcher::from(driver.new_dispatcher(dispatcher_builder).unwrap());
77        let driver = Some(driver);
78
79        Self {
80            fdf_env_environment: env,
81            node_manager,
82            driver,
83            dispatcher,
84            driver_incoming_dir,
85            config_vmo: None,
86            url: None,
87            offers: None,
88            scope,
89            _d: PhantomData,
90        }
91    }
92
93    /// Sets the driver incoming ServiceFs. Consumes and returns self to allow chaining.
94    pub fn set_driver_incoming(
95        mut self,
96        mut driver_incoming: ServiceFs<ServiceObj<'static, ()>>,
97    ) -> Self {
98        driver_incoming.dir("svc").add_service_connector(logsink_connector);
99
100        let (driver_incoming_dir_client, driver_incoming_dir_server) = zx::Channel::create();
101        driver_incoming.serve_connection(driver_incoming_dir_server.into()).unwrap();
102        let driver_incoming_dir = driver_incoming_dir_client.into();
103        self.scope.spawn(async move {
104            driver_incoming.collect::<()>().await;
105        });
106
107        self.driver_incoming_dir = driver_incoming_dir;
108        self
109    }
110
111    /// Sets the configuration vmo for the driver. Consumes and returns self to allow chaining.
112    pub fn set_config(mut self, config: zx::Vmo) -> Self {
113        self.config_vmo = Some(config);
114        self
115    }
116
117    /// Sets the url for the driver. Consumes and returns self to allow chaining.
118    pub fn set_url(mut self, url: &str) -> Self {
119        self.url = Some(url.to_string());
120        self
121    }
122
123    /// Adds an offer to the driver's start args. Consumes and returns self to allow chaining.
124    pub fn add_offer(mut self, offer: Offer) -> Self {
125        self.offers.get_or_insert_default().push(offer);
126        self
127    }
128
129    /// Gets a driver dispatcher that can be used to run test side driver transport client/servers.
130    pub fn dispatcher(&self) -> WeakDispatcher {
131        WeakDispatcher::from(&self.dispatcher)
132    }
133
134    pub(crate) fn node_manager(&self) -> Weak<NodeManager> {
135        Arc::downgrade(&self.node_manager)
136    }
137
138    /// Starts the driver under test.
139    pub async fn start_driver(&mut self) -> Result<DriverUnderTest<'_, D>, Status> {
140        let (node_client, node_server) = zx::Channel::create();
141        let node_id = self.node_manager.create_root_node(node_server.into());
142
143        let (driver_outgoing_dir_client, driver_outgoing_dir_server) =
144            fidl::endpoints::create_endpoints();
145        let driver_outgoing = Incoming::from(driver_outgoing_dir_client);
146
147        let driver_incoming_svc =
148            open_directory_async(&self.driver_incoming_dir, "svc", fio::R_STAR_DIR).unwrap();
149
150        let start_args = DriverStartArgs {
151            node: Some(NextClientEnd::from_untyped(node_client)),
152            incoming: Some(vec![ComponentNamespaceEntry {
153                path: Some("/svc".to_string()),
154                directory: Some(NextClientEnd::from_untyped(
155                    driver_incoming_svc.into_channel().unwrap().into(),
156                )),
157            }]),
158            outgoing_dir: Some(NextServerEnd::compat_from(driver_outgoing_dir_server)),
159            config: self
160                .config_vmo
161                .as_ref()
162                .and_then(|v| v.duplicate_handle(fidl::Rights::SAME_RIGHTS).ok()),
163            url: self.url.clone(),
164            node_offers: self
165                .offers
166                .as_ref()
167                .map(|o| o.clone().into_iter().map(NextOffer::compat_from).collect()),
168            ..DriverStartArgs::default()
169        };
170
171        let mut driver =
172            DriverUnderTest::new(self, self.fdf_env_environment.clone(), driver_outgoing, node_id)
173                .await;
174        // If the driver fails to start we will drop it here and allow it to run the destroy hook.
175        driver.start_driver(start_args).await?;
176        Ok(driver)
177    }
178}
179
180impl<D> Drop for TestHarness<D> {
181    fn drop(&mut self) {
182        let (shutdown_tx, shutdown_rx) = mpsc::channel();
183        self.driver.take().expect("driver").shutdown(move |driver_ref| {
184            // SAFTEY: we created this through Box::into_raw below inside of new.
185            let driver_value = unsafe { Box::from_raw(driver_ref.0 as *mut u32) };
186            assert_eq!(*driver_value, 0x1234);
187            shutdown_tx.send(()).unwrap();
188        });
189
190        shutdown_rx.recv().unwrap();
191
192        self.fdf_env_environment.destroy_all_dispatchers();
193        self.fdf_env_environment.reset();
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200    use crate::{Node, NodeBuilder, ServiceInstance, ServiceOffer};
201    use fidl_next::{Request, Responder};
202    use fidl_next_fuchsia_examples::echo::{EchoString, SendString};
203    use futures::StreamExt;
204    use futures::lock::Mutex;
205    use log::info;
206    use {fidl_next_fuchsia_examples as fexample, fuchsia_async as fasync};
207
208    struct EchoServer;
209
210    impl fexample::EchoServerHandler<zx::Channel> for EchoServer {
211        async fn echo_string(
212            &mut self,
213            request: Request<EchoString, zx::Channel>,
214            responder: Responder<EchoString, zx::Channel>,
215        ) {
216            info!("ECHO: {}", request.payload().value);
217            responder.respond("resp").await.unwrap();
218        }
219
220        async fn send_string(&mut self, _request: Request<SendString, zx::Channel>) {}
221    }
222
223    struct Service {
224        scope: fasync::ScopeHandle,
225    }
226
227    impl fexample::EchoServiceHandler for Service {
228        fn regular_echo(&self, server_end: NextServerEnd<fexample::Echo>) {
229            server_end.spawn_on(EchoServer, &self.scope).detach_on_drop();
230        }
231
232        fn reversed_echo(&self, _server_end: NextServerEnd<fexample::Echo>) {}
233    }
234
235    #[allow(dead_code)]
236    struct TestDriver {
237        node: Node,
238        scope: fasync::Scope,
239        tmp: Mutex<String>,
240    }
241
242    impl TestDriver {
243        async fn set_tmp(&self, resp: &str) {
244            let mut tmp = self.tmp.lock().await;
245            *tmp = resp.to_string();
246        }
247
248        async fn get_tmp(&self) -> String {
249            let tmp = self.tmp.lock().await;
250            tmp.to_string()
251        }
252    }
253
254    impl Driver for TestDriver {
255        const NAME: &'static str = "test-driver";
256
257        async fn start(mut context: crate::DriverContext) -> Result<Self, Status> {
258            let service_proxy: ServiceInstance<fexample::EchoService> =
259                context.incoming.service().connect_next()?;
260            let (client_end, server_end) = fidl_next::fuchsia::create_channel();
261            service_proxy.regular_echo(server_end).unwrap();
262            let client = client_end.spawn();
263            let resp =
264                client.echo_string("echo from driver").await.map_err(|_| Status::IO_REFUSED)?;
265            assert_eq!("resp", resp.response.as_str());
266
267            let scope = fasync::Scope::new_with_name("test driver scope");
268            let mut outgoing = ServiceFs::new();
269            let offer = ServiceOffer::<fexample::EchoService>::new_next()
270                .add_named_next(&mut outgoing, "default", Service { scope: scope.to_handle() })
271                .build_zircon_offer_next();
272            context.serve_outgoing(&mut outgoing)?;
273            scope.spawn(outgoing.collect());
274
275            let node = context.take_node()?;
276            let child_node = NodeBuilder::new("transport-child")
277                .add_property("prop", "val")
278                .add_offer(offer)
279                .build();
280            node.add_child(child_node).await?;
281
282            info!("TestDriver started");
283            Ok(Self { node, scope, tmp: Mutex::new("NA".to_string()) })
284        }
285
286        async fn stop(&self) {
287            info!("TestDriver stopped. Tmp: '{}'", *self.tmp.lock().await);
288        }
289    }
290
291    #[fuchsia::test]
292    async fn test_basic() {
293        let scope = fasync::Scope::new_with_name("test scope");
294        let mut service_fs = ServiceFs::new();
295        let offer = ServiceOffer::<fexample::EchoService>::new_next()
296            .add_named_next(&mut service_fs, "default", Service { scope: scope.to_handle() })
297            .build_zircon_offer_next();
298        let mut harness = TestHarness::<TestDriver>::new()
299            .set_driver_incoming(service_fs)
300            .set_url("test_url")
301            .add_offer(offer);
302
303        let start_result = harness.start_driver().await;
304        let started_driver = start_result.expect("success");
305        let driver = started_driver.get_driver().expect("failed to get driver");
306        driver.set_tmp("my_temp_var").await;
307        assert_eq!("my_temp_var", driver.get_tmp().await);
308
309        let service_proxy: ServiceInstance<fexample::EchoService> =
310            started_driver.driver_outgoing().service().connect_next().unwrap();
311        let (client_end, server_end) = fidl_next::fuchsia::create_channel();
312        service_proxy.regular_echo(server_end).unwrap();
313        let client = client_end.spawn();
314        let resp = client.echo_string("echo to driver").await.unwrap();
315        assert_eq!("resp", resp.response.as_str());
316        started_driver.stop_driver().await;
317    }
318
319    #[fuchsia::test]
320    async fn test_multiple_start_stop() {
321        let scope = fasync::Scope::new_with_name("test scope");
322        let mut service_fs = ServiceFs::new();
323        let offer = ServiceOffer::<fexample::EchoService>::new_next()
324            .add_named_next(&mut service_fs, "default", Service { scope: scope.to_handle() })
325            .build_zircon_offer_next();
326        let mut harness = TestHarness::<TestDriver>::new()
327            .set_driver_incoming(service_fs)
328            .set_url("test_url")
329            .add_offer(offer);
330
331        for i in 1..=3 {
332            let start_result = harness.start_driver().await;
333            let started_driver = start_result.expect("success");
334            let driver = started_driver.get_driver().expect("failed to get driver");
335            driver.set_tmp(format!("my_temp_var_{}", i).as_str()).await;
336            assert_eq!(format!("my_temp_var_{}", i), driver.get_tmp().await);
337
338            let service_proxy: ServiceInstance<fexample::EchoService> =
339                started_driver.driver_outgoing().service().connect_next().unwrap();
340            let (client_end, server_end) = fidl_next::fuchsia::create_channel();
341            service_proxy.regular_echo(server_end).unwrap();
342            let client = client_end.spawn();
343            let resp = client.echo_string("echo to driver").await.unwrap();
344            assert_eq!("resp", resp.response.as_str());
345            started_driver.stop_driver().await;
346        }
347    }
348
349    #[fuchsia::test]
350    async fn test_no_start() {
351        let _harness = TestHarness::<TestDriver>::default();
352    }
353
354    #[fuchsia::test]
355    async fn test_start_fail() {
356        let mut harness = TestHarness::<TestDriver>::new();
357        let start_result = harness.start_driver().await;
358        assert_eq!(start_result.err(), Some(Status::IO_REFUSED));
359    }
360}