Skip to main content

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
7pub use 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, 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<NextOffer>>,
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(convert::convert_df_offer(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::from_untyped(
159                driver_outgoing_dir_server.into_channel(),
160            )),
161            config: self
162                .config_vmo
163                .as_ref()
164                .and_then(|v| v.duplicate_handle(fidl::Rights::SAME_RIGHTS).ok()),
165            url: self.url.clone(),
166            node_offers: self.offers.clone(),
167            ..DriverStartArgs::default()
168        };
169
170        let mut driver =
171            DriverUnderTest::new(self, self.fdf_env_environment.clone(), driver_outgoing, node_id)
172                .await;
173        // If the driver fails to start we will drop it here and allow it to run the destroy hook.
174        driver.start_driver(start_args).await?;
175        Ok(driver)
176    }
177}
178
179impl<D> Drop for TestHarness<D> {
180    fn drop(&mut self) {
181        let (shutdown_tx, shutdown_rx) = mpsc::channel();
182        self.driver.take().expect("driver").shutdown(move |driver_ref| {
183            // SAFTEY: we created this through Box::into_raw below inside of new.
184            let driver_value = unsafe { Box::from_raw(driver_ref.0 as *mut u32) };
185            assert_eq!(*driver_value, 0x1234);
186            shutdown_tx.send(()).unwrap();
187        });
188
189        shutdown_rx.recv().unwrap();
190
191        self.fdf_env_environment.destroy_all_dispatchers();
192        self.fdf_env_environment.reset();
193    }
194}
195
196mod convert {
197    use {
198        fidl_fuchsia_component_decl as decl, fidl_fuchsia_driver_framework as df,
199        fidl_next_fuchsia_component_decl as decl_next,
200        fidl_next_fuchsia_driver_framework as df_next,
201    };
202
203    pub fn convert_df_offer(offer: df::Offer) -> df_next::Offer {
204        match offer {
205            df::Offer::DictionaryOffer(o) => df_next::Offer::DictionaryOffer(convert_offer(o)),
206            df::Offer::ZirconTransport(o) => df_next::Offer::ZirconTransport(convert_offer(o)),
207            df::Offer::DriverTransport(o) => df_next::Offer::DriverTransport(convert_offer(o)),
208            df::Offer::__SourceBreaking { unknown_ordinal } => {
209                df_next::Offer::UnknownOrdinal_(unknown_ordinal)
210            }
211        }
212    }
213
214    fn convert_offer(offer: decl::Offer) -> decl_next::Offer {
215        match offer {
216            decl::Offer::Service(o) => decl_next::Offer::Service(decl_next::OfferService {
217                source: o.source.map(convert_ref),
218                source_name: o.source_name,
219                target: o.target.map(convert_ref),
220                target_name: o.target_name,
221                source_instance_filter: o.source_instance_filter,
222                renamed_instances: o
223                    .renamed_instances
224                    .map(|v| v.into_iter().map(convert_name_mapping).collect()),
225                availability: o.availability.map(convert_availability),
226                source_dictionary: o.source_dictionary,
227                dependency_type: o.dependency_type.map(convert_dependency_type),
228            }),
229            decl::Offer::Protocol(o) => decl_next::Offer::Protocol(decl_next::OfferProtocol {
230                source: o.source.map(convert_ref),
231                source_name: o.source_name,
232                target: o.target.map(convert_ref),
233                target_name: o.target_name,
234                dependency_type: o.dependency_type.map(convert_dependency_type),
235                availability: o.availability.map(convert_availability),
236                source_dictionary: o.source_dictionary,
237            }),
238            decl::Offer::Directory(o) => decl_next::Offer::Directory(decl_next::OfferDirectory {
239                source: o.source.map(convert_ref),
240                source_name: o.source_name,
241                target: o.target.map(convert_ref),
242                target_name: o.target_name,
243                availability: o.availability.map(convert_availability),
244                source_dictionary: o.source_dictionary,
245                dependency_type: o.dependency_type.map(convert_dependency_type),
246                rights: o.rights.map(convert_rights),
247                subdir: o.subdir,
248            }),
249            decl::Offer::Storage(o) => decl_next::Offer::Storage(decl_next::OfferStorage {
250                source_name: o.source_name,
251                source: o.source.map(convert_ref),
252                target: o.target.map(convert_ref),
253                target_name: o.target_name,
254                availability: o.availability.map(convert_availability),
255            }),
256            decl::Offer::Runner(o) => decl_next::Offer::Runner(decl_next::OfferRunner {
257                source: o.source.map(convert_ref),
258                source_name: o.source_name,
259                target: o.target.map(convert_ref),
260                target_name: o.target_name,
261                source_dictionary: o.source_dictionary,
262            }),
263            decl::Offer::Resolver(o) => decl_next::Offer::Resolver(decl_next::OfferResolver {
264                source: o.source.map(convert_ref),
265                source_name: o.source_name,
266                target: o.target.map(convert_ref),
267                target_name: o.target_name,
268                source_dictionary: o.source_dictionary,
269            }),
270            decl::Offer::EventStream(o) => {
271                decl_next::Offer::EventStream(decl_next::OfferEventStream {
272                    source: o.source.map(convert_ref),
273                    source_name: o.source_name,
274                    scope: o.scope.map(|v| v.into_iter().map(convert_ref).collect()),
275                    target: o.target.map(convert_ref),
276                    target_name: o.target_name,
277                    availability: o.availability.map(convert_availability),
278                })
279            }
280            decl::Offer::Dictionary(o) => {
281                decl_next::Offer::Dictionary(decl_next::OfferDictionary {
282                    source: o.source.map(convert_ref),
283                    source_name: o.source_name,
284                    target: o.target.map(convert_ref),
285                    target_name: o.target_name,
286                    dependency_type: o.dependency_type.map(convert_dependency_type),
287                    availability: o.availability.map(convert_availability),
288                    source_dictionary: o.source_dictionary,
289                })
290            }
291            decl::Offer::Config(o) => decl_next::Offer::Config(decl_next::OfferConfiguration {
292                source: o.source.map(convert_ref),
293                source_name: o.source_name,
294                target: o.target.map(convert_ref),
295                target_name: o.target_name,
296                availability: o.availability.map(convert_availability),
297                source_dictionary: o.source_dictionary,
298            }),
299            decl::Offer::__SourceBreaking { unknown_ordinal } => {
300                decl_next::Offer::UnknownOrdinal_(unknown_ordinal)
301            }
302        }
303    }
304
305    fn convert_ref(ref_: decl::Ref) -> decl_next::Ref {
306        match ref_ {
307            decl::Ref::Parent(_) => decl_next::Ref::Parent(()),
308            decl::Ref::Self_(_) => decl_next::Ref::Self_(()),
309            decl::Ref::Child(child_ref) => decl_next::Ref::Child(decl_next::ChildRef {
310                name: child_ref.name,
311                collection: child_ref.collection,
312            }),
313            decl::Ref::Collection(collection_ref) => {
314                decl_next::Ref::Collection(decl_next::CollectionRef { name: collection_ref.name })
315            }
316            decl::Ref::Framework(_) => decl_next::Ref::Framework(()),
317            decl::Ref::Capability(capability_ref) => {
318                decl_next::Ref::Capability(decl_next::CapabilityRef { name: capability_ref.name })
319            }
320            decl::Ref::Debug(_) => decl_next::Ref::Debug(()),
321            decl::Ref::VoidType(_) => decl_next::Ref::VoidType(()),
322            decl::Ref::Environment(_) => decl_next::Ref::Environment(()),
323            decl::Ref::__SourceBreaking { unknown_ordinal } => {
324                decl_next::Ref::UnknownOrdinal_(unknown_ordinal)
325            }
326        }
327    }
328
329    fn convert_name_mapping(name_mapping: decl::NameMapping) -> decl_next::NameMapping {
330        fidl_next_fuchsia_component_decl::NameMapping {
331            source_name: name_mapping.source_name,
332            target_name: name_mapping.target_name,
333        }
334    }
335
336    fn convert_availability(availability: decl::Availability) -> decl_next::Availability {
337        match availability {
338            decl::Availability::Required => decl_next::Availability::Required,
339            decl::Availability::Optional => decl_next::Availability::Optional,
340            decl::Availability::SameAsTarget => decl_next::Availability::SameAsTarget,
341            decl::Availability::Transitional => decl_next::Availability::Transitional,
342        }
343    }
344
345    fn convert_dependency_type(dependency_type: decl::DependencyType) -> decl_next::DependencyType {
346        match dependency_type {
347            decl::DependencyType::Strong => decl_next::DependencyType::Strong,
348            decl::DependencyType::Weak => decl_next::DependencyType::Weak,
349        }
350    }
351
352    fn convert_rights(rights: fidl_fuchsia_io::Operations) -> fidl_next_fuchsia_io::Operations {
353        fidl_next_fuchsia_io::Operations::from_bits_retain(rights.bits())
354    }
355}
356
357#[cfg(test)]
358mod tests {
359    use super::*;
360    use crate::{Node, NodeBuilder, ServiceInstance, ServiceOffer};
361    use fidl_next::{Request, Responder};
362    use fidl_next_fuchsia_examples::echo::{EchoString, SendString};
363    use futures::StreamExt;
364    use futures::lock::Mutex;
365    use log::info;
366    use {fidl_next_fuchsia_examples as fexample, fuchsia_async as fasync};
367
368    struct EchoServer;
369
370    impl fexample::EchoServerHandler<zx::Channel> for EchoServer {
371        async fn echo_string(
372            &mut self,
373            request: Request<EchoString, zx::Channel>,
374            responder: Responder<EchoString, zx::Channel>,
375        ) {
376            info!("ECHO: {}", request.payload().value);
377            responder.respond("resp").await.unwrap();
378        }
379
380        async fn send_string(&mut self, _request: Request<SendString, zx::Channel>) {}
381    }
382
383    struct Service {
384        scope: fasync::ScopeHandle,
385    }
386
387    impl fexample::EchoServiceHandler for Service {
388        fn regular_echo(&self, server_end: NextServerEnd<fexample::Echo>) {
389            server_end.spawn_on(EchoServer, &self.scope);
390        }
391
392        fn reversed_echo(&self, _server_end: NextServerEnd<fexample::Echo>) {}
393    }
394
395    #[allow(dead_code)]
396    struct TestDriver {
397        node: Node,
398        scope: fasync::Scope,
399        tmp: Mutex<String>,
400    }
401
402    impl TestDriver {
403        async fn set_tmp(&self, resp: &str) {
404            let mut tmp = self.tmp.lock().await;
405            *tmp = resp.to_string();
406        }
407
408        async fn get_tmp(&self) -> String {
409            let tmp = self.tmp.lock().await;
410            tmp.to_string()
411        }
412    }
413
414    impl Driver for TestDriver {
415        const NAME: &'static str = "test-driver";
416
417        async fn start(mut context: crate::DriverContext) -> Result<Self, Status> {
418            let service_proxy: ServiceInstance<fexample::EchoService> =
419                context.incoming.service().connect_next()?;
420            let (client_end, server_end) = fidl_next::fuchsia::create_channel();
421            service_proxy.regular_echo(server_end).unwrap();
422            let client = client_end.spawn();
423            let resp =
424                client.echo_string("echo from driver").await.map_err(|_| Status::IO_REFUSED)?;
425            assert_eq!("resp", resp.response.as_str());
426
427            let scope = fasync::Scope::new_with_name("test driver scope");
428            let mut outgoing = ServiceFs::new();
429            let offer = ServiceOffer::<fexample::EchoService>::new_next()
430                .add_named_next(&mut outgoing, "default", Service { scope: scope.to_handle() })
431                .build_zircon_offer_next();
432            context.serve_outgoing(&mut outgoing)?;
433            scope.spawn(outgoing.collect());
434
435            let node = context.take_node()?;
436            let child_node = NodeBuilder::new("transport-child")
437                .add_property("prop", "val")
438                .add_offer(offer)
439                .build();
440            node.add_child(child_node).await?;
441
442            info!("TestDriver started");
443            Ok(Self { node, scope, tmp: Mutex::new("NA".to_string()) })
444        }
445
446        async fn stop(&self) {
447            info!("TestDriver stopped. Tmp: '{}'", *self.tmp.lock().await);
448        }
449    }
450
451    #[fuchsia::test]
452    async fn test_basic() {
453        let scope = fasync::Scope::new_with_name("test scope");
454        let mut service_fs = ServiceFs::new();
455        let offer = ServiceOffer::<fexample::EchoService>::new_next()
456            .add_named_next(&mut service_fs, "default", Service { scope: scope.to_handle() })
457            .build_zircon_offer_next();
458        let mut harness = TestHarness::<TestDriver>::new()
459            .set_driver_incoming(service_fs)
460            .set_url("test_url")
461            .add_offer(offer);
462
463        let start_result = harness.start_driver().await;
464        let started_driver = start_result.expect("success");
465        let driver = started_driver.get_driver().expect("failed to get driver");
466        driver.set_tmp("my_temp_var").await;
467        assert_eq!("my_temp_var", driver.get_tmp().await);
468
469        let service_proxy: ServiceInstance<fexample::EchoService> =
470            started_driver.driver_outgoing().service().connect_next().unwrap();
471        let (client_end, server_end) = fidl_next::fuchsia::create_channel();
472        service_proxy.regular_echo(server_end).unwrap();
473        let client = client_end.spawn();
474        let resp = client.echo_string("echo to driver").await.unwrap();
475        assert_eq!("resp", resp.response.as_str());
476        started_driver.stop_driver().await;
477    }
478
479    #[fuchsia::test]
480    async fn test_multiple_start_stop() {
481        let scope = fasync::Scope::new_with_name("test scope");
482        let mut service_fs = ServiceFs::new();
483        let offer = ServiceOffer::<fexample::EchoService>::new_next()
484            .add_named_next(&mut service_fs, "default", Service { scope: scope.to_handle() })
485            .build_zircon_offer_next();
486        let mut harness = TestHarness::<TestDriver>::new()
487            .set_driver_incoming(service_fs)
488            .set_url("test_url")
489            .add_offer(offer);
490
491        for i in 1..=3 {
492            let start_result = harness.start_driver().await;
493            let started_driver = start_result.expect("success");
494            let driver = started_driver.get_driver().expect("failed to get driver");
495            driver.set_tmp(format!("my_temp_var_{}", i).as_str()).await;
496            assert_eq!(format!("my_temp_var_{}", i), driver.get_tmp().await);
497
498            let service_proxy: ServiceInstance<fexample::EchoService> =
499                started_driver.driver_outgoing().service().connect_next().unwrap();
500            let (client_end, server_end) = fidl_next::fuchsia::create_channel();
501            service_proxy.regular_echo(server_end).unwrap();
502            let client = client_end.spawn();
503            let resp = client.echo_string("echo to driver").await.unwrap();
504            assert_eq!("resp", resp.response.as_str());
505            started_driver.stop_driver().await;
506        }
507    }
508
509    #[fuchsia::test]
510    async fn test_no_start() {
511        let _harness = TestHarness::<TestDriver>::default();
512    }
513
514    #[fuchsia::test]
515    async fn test_start_fail() {
516        let mut harness = TestHarness::<TestDriver>::new();
517        let start_result = harness.start_driver().await;
518        assert_eq!(start_result.err(), Some(Status::IO_REFUSED));
519    }
520}