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_fuchsia_io as fio;
17use fidl_next::{ClientEnd as NextClientEnd, ServerEnd as NextServerEnd};
18use fidl_next_fuchsia_component_runner::natural::ComponentNamespaceEntry;
19use fidl_next_fuchsia_driver_framework::DriverStartArgs;
20use fidl_next_fuchsia_driver_framework::natural::Offer as NextOffer;
21use fuchsia_async as fasync;
22use fuchsia_component::directory::open_directory_async;
23use fuchsia_component::server::{ServiceFs, ServiceObj};
24use futures::StreamExt;
25use std::marker::PhantomData;
26use std::sync::{Arc, Weak, mpsc};
27use zx::{HandleBased, Status};
28
29/// The main test harness for running a driver unit test.
30pub struct TestHarness<D> {
31    fdf_env_environment: Arc<Environment>,
32    node_manager: Arc<NodeManager>,
33    driver: Option<fdf_env::Driver<u32>>,
34    dispatcher: AutoReleaseDispatcher,
35    driver_incoming_dir: ClientEnd<fio::DirectoryMarker>,
36    config_vmo: Option<zx::Vmo>,
37    url: Option<String>,
38    offers: Option<Vec<NextOffer>>,
39    scope: fasync::Scope,
40    power_element_args: Option<fidl_fuchsia_driver_framework::PowerElementArgs>,
41    _d: PhantomData<D>,
42}
43
44impl<D: Driver> Default for TestHarness<D> {
45    fn default() -> Self {
46        Self::new()
47    }
48}
49
50impl<D: Driver> TestHarness<D> {
51    /// Creates a new `TestHarness` without a customized driver incoming ServiceFs.
52    pub fn new() -> Self {
53        let scope = fasync::Scope::new();
54        let mut driver_incoming = ServiceFs::new();
55        let env = Arc::new(Environment::start(0).unwrap());
56        let node_manager = NodeManager::new();
57        driver_incoming.dir("svc").add_service_connector(logsink_connector);
58
59        let (driver_incoming_dir_client, driver_incoming_dir_server) = zx::Channel::create();
60        driver_incoming.serve_connection(driver_incoming_dir_server.into()).unwrap();
61        let driver_incoming_dir = driver_incoming_dir_client.into();
62
63        scope.spawn(async move {
64            driver_incoming.collect::<()>().await;
65        });
66
67        // Leak this to a raw, we will reconstitue a Box inside drop.
68        let driver_value_ptr = Box::into_raw(Box::new(0x1234_u32));
69        let driver = env.new_driver(driver_value_ptr);
70        let env_clone = env.clone();
71        let dispatcher_builder =
72            DispatcherBuilder::new().name("test_harness").shutdown_observer(move |dispatcher| {
73                // We verify that the dispatcher has no tasks left queued in it,
74                // just because this is testing code.
75                assert!(!env_clone.dispatcher_has_queued_tasks(dispatcher.as_dispatcher_ref()));
76            });
77        let dispatcher =
78            AutoReleaseDispatcher::from(driver.new_dispatcher(dispatcher_builder).unwrap());
79        let driver = Some(driver);
80
81        Self {
82            fdf_env_environment: env,
83            node_manager,
84            driver,
85            dispatcher,
86            driver_incoming_dir,
87            config_vmo: None,
88            url: None,
89            offers: None,
90            scope,
91            power_element_args: None,
92            _d: PhantomData,
93        }
94    }
95
96    /// Sets the driver incoming ServiceFs. Consumes and returns self to allow chaining.
97    pub fn set_driver_incoming(
98        mut self,
99        mut driver_incoming: ServiceFs<ServiceObj<'static, ()>>,
100    ) -> Self {
101        driver_incoming.dir("svc").add_service_connector(logsink_connector);
102
103        let (driver_incoming_dir_client, driver_incoming_dir_server) = zx::Channel::create();
104        driver_incoming.serve_connection(driver_incoming_dir_server.into()).unwrap();
105        let driver_incoming_dir = driver_incoming_dir_client.into();
106        self.scope.spawn(async move {
107            driver_incoming.collect::<()>().await;
108        });
109
110        self.driver_incoming_dir = driver_incoming_dir;
111        self
112    }
113
114    /// Sets the configuration vmo for the driver. Consumes and returns self to allow chaining.
115    pub fn set_config(mut self, config: zx::Vmo) -> Self {
116        self.config_vmo = Some(config);
117        self
118    }
119
120    /// Sets the url for the driver. Consumes and returns self to allow chaining.
121    pub fn set_url(mut self, url: &str) -> Self {
122        self.url = Some(url.to_string());
123        self
124    }
125
126    /// Adds an offer to the driver's start args. Consumes and returns self to allow chaining.
127    pub fn add_offer(mut self, offer: Offer) -> Self {
128        self.offers.get_or_insert_default().push(convert::convert_df_offer(offer));
129        self
130    }
131
132    /// Sets the power element args for the driver. Consumes and returns self to allow chaining.
133    pub fn set_power_element_args(
134        mut self,
135        args: fidl_fuchsia_driver_framework::PowerElementArgs,
136    ) -> Self {
137        self.power_element_args = Some(args);
138        self
139    }
140
141    /// Gets a driver dispatcher that can be used to run test side driver transport client/servers.
142    pub fn dispatcher(&self) -> WeakDispatcher {
143        WeakDispatcher::from(&self.dispatcher)
144    }
145
146    pub(crate) fn node_manager(&self) -> Weak<NodeManager> {
147        Arc::downgrade(&self.node_manager)
148    }
149
150    /// Starts the driver under test.
151    pub async fn start_driver(&mut self) -> Result<DriverUnderTest<'_, D>, Status> {
152        let (node_client, node_server) = zx::Channel::create();
153        let node_id = self.node_manager.create_root_node(node_server.into());
154
155        let (driver_outgoing_dir_client, driver_outgoing_dir_server) =
156            fidl::endpoints::create_endpoints();
157        let driver_outgoing = Incoming::from(driver_outgoing_dir_client);
158
159        let driver_incoming_svc =
160            open_directory_async(&self.driver_incoming_dir, "svc", fio::R_STAR_DIR).unwrap();
161
162        let start_args = DriverStartArgs {
163            node: Some(NextClientEnd::from_untyped(node_client)),
164            incoming: Some(vec![ComponentNamespaceEntry {
165                path: Some("/svc".to_string()),
166                directory: Some(NextClientEnd::from_untyped(
167                    driver_incoming_svc.into_channel().unwrap().into(),
168                )),
169            }]),
170            outgoing_dir: Some(NextServerEnd::from_untyped(
171                driver_outgoing_dir_server.into_channel(),
172            )),
173            config: self
174                .config_vmo
175                .as_ref()
176                .and_then(|v| v.duplicate_handle(fidl::Rights::SAME_RIGHTS).ok()),
177            url: self.url.clone(),
178            node_offers: self.offers.clone(),
179            power_element_args: self.power_element_args.take().map(|args| {
180                fidl_next_fuchsia_driver_framework::natural::PowerElementArgs {
181                    control_client: args
182                        .control_client
183                        .map(|c| NextClientEnd::from_untyped(c.into_channel())),
184                    runner_server: args
185                        .runner_server
186                        .map(|s| NextServerEnd::from_untyped(s.into_channel())),
187                    lessor_client: args
188                        .lessor_client
189                        .map(|c| NextClientEnd::from_untyped(c.into_channel())),
190                    token: args.token,
191                }
192            }),
193            ..DriverStartArgs::default()
194        };
195
196        let mut driver =
197            DriverUnderTest::new(self, self.fdf_env_environment.clone(), driver_outgoing, node_id)
198                .await;
199        // If the driver fails to start we will drop it here and allow it to run the destroy hook.
200        driver.start_driver(start_args).await?;
201        Ok(driver)
202    }
203}
204
205impl<D> Drop for TestHarness<D> {
206    fn drop(&mut self) {
207        let (shutdown_tx, shutdown_rx) = mpsc::channel();
208        self.driver.take().expect("driver").shutdown(move |driver_ref| {
209            // SAFTEY: we created this through Box::into_raw below inside of new.
210            let driver_value = unsafe { Box::from_raw(driver_ref.0 as *mut u32) };
211            assert_eq!(*driver_value, 0x1234);
212            shutdown_tx.send(()).unwrap();
213        });
214
215        shutdown_rx.recv().unwrap();
216
217        self.fdf_env_environment.destroy_all_dispatchers();
218        self.fdf_env_environment.reset();
219    }
220}
221
222mod convert {
223    use fidl_fuchsia_component_decl as decl;
224    use fidl_fuchsia_driver_framework as df;
225    use fidl_next_fuchsia_component_decl as decl_next;
226    use fidl_next_fuchsia_driver_framework as df_next;
227
228    pub fn convert_df_offer(offer: df::Offer) -> df_next::Offer {
229        match offer {
230            df::Offer::DictionaryOffer(o) => df_next::Offer::DictionaryOffer(convert_offer(o)),
231            df::Offer::ZirconTransport(o) => df_next::Offer::ZirconTransport(convert_offer(o)),
232            df::Offer::DriverTransport(o) => df_next::Offer::DriverTransport(convert_offer(o)),
233            df::Offer::__SourceBreaking { unknown_ordinal } => {
234                df_next::Offer::UnknownOrdinal_(unknown_ordinal)
235            }
236        }
237    }
238
239    fn convert_offer(offer: decl::Offer) -> decl_next::Offer {
240        match offer {
241            decl::Offer::Service(o) => decl_next::Offer::Service(decl_next::OfferService {
242                source: o.source.map(convert_ref),
243                source_name: o.source_name,
244                target: o.target.map(convert_ref),
245                target_name: o.target_name,
246                source_instance_filter: o.source_instance_filter,
247                renamed_instances: o
248                    .renamed_instances
249                    .map(|v| v.into_iter().map(convert_name_mapping).collect()),
250                availability: o.availability.map(convert_availability),
251                source_dictionary: o.source_dictionary,
252                dependency_type: o.dependency_type.map(convert_dependency_type),
253            }),
254            decl::Offer::Protocol(o) => decl_next::Offer::Protocol(decl_next::OfferProtocol {
255                source: o.source.map(convert_ref),
256                source_name: o.source_name,
257                target: o.target.map(convert_ref),
258                target_name: o.target_name,
259                dependency_type: o.dependency_type.map(convert_dependency_type),
260                availability: o.availability.map(convert_availability),
261                source_dictionary: o.source_dictionary,
262            }),
263            decl::Offer::Directory(o) => decl_next::Offer::Directory(decl_next::OfferDirectory {
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                availability: o.availability.map(convert_availability),
269                source_dictionary: o.source_dictionary,
270                dependency_type: o.dependency_type.map(convert_dependency_type),
271                rights: o.rights.map(convert_rights),
272                subdir: o.subdir,
273            }),
274            decl::Offer::Storage(o) => decl_next::Offer::Storage(decl_next::OfferStorage {
275                source_name: o.source_name,
276                source: o.source.map(convert_ref),
277                target: o.target.map(convert_ref),
278                target_name: o.target_name,
279                availability: o.availability.map(convert_availability),
280            }),
281            decl::Offer::Runner(o) => decl_next::Offer::Runner(decl_next::OfferRunner {
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                source_dictionary: o.source_dictionary,
287            }),
288            decl::Offer::Resolver(o) => decl_next::Offer::Resolver(decl_next::OfferResolver {
289                source: o.source.map(convert_ref),
290                source_name: o.source_name,
291                target: o.target.map(convert_ref),
292                target_name: o.target_name,
293                source_dictionary: o.source_dictionary,
294            }),
295            decl::Offer::EventStream(o) => {
296                decl_next::Offer::EventStream(decl_next::OfferEventStream {
297                    source: o.source.map(convert_ref),
298                    source_name: o.source_name,
299                    scope: o.scope.map(|v| v.into_iter().map(convert_ref).collect()),
300                    target: o.target.map(convert_ref),
301                    target_name: o.target_name,
302                    availability: o.availability.map(convert_availability),
303                })
304            }
305            decl::Offer::Dictionary(o) => {
306                decl_next::Offer::Dictionary(decl_next::OfferDictionary {
307                    source: o.source.map(convert_ref),
308                    source_name: o.source_name,
309                    target: o.target.map(convert_ref),
310                    target_name: o.target_name,
311                    dependency_type: o.dependency_type.map(convert_dependency_type),
312                    availability: o.availability.map(convert_availability),
313                    source_dictionary: o.source_dictionary,
314                })
315            }
316            decl::Offer::Config(o) => decl_next::Offer::Config(decl_next::OfferConfiguration {
317                source: o.source.map(convert_ref),
318                source_name: o.source_name,
319                target: o.target.map(convert_ref),
320                target_name: o.target_name,
321                availability: o.availability.map(convert_availability),
322                source_dictionary: o.source_dictionary,
323            }),
324            decl::Offer::__SourceBreaking { unknown_ordinal } => {
325                decl_next::Offer::UnknownOrdinal_(unknown_ordinal)
326            }
327        }
328    }
329
330    fn convert_ref(ref_: decl::Ref) -> decl_next::Ref {
331        match ref_ {
332            decl::Ref::Parent(_) => decl_next::Ref::Parent(()),
333            decl::Ref::Self_(_) => decl_next::Ref::Self_(()),
334            decl::Ref::Child(child_ref) => decl_next::Ref::Child(decl_next::ChildRef {
335                name: child_ref.name,
336                collection: child_ref.collection,
337            }),
338            decl::Ref::Collection(collection_ref) => {
339                decl_next::Ref::Collection(decl_next::CollectionRef { name: collection_ref.name })
340            }
341            decl::Ref::Framework(_) => decl_next::Ref::Framework(()),
342            decl::Ref::Capability(capability_ref) => {
343                decl_next::Ref::Capability(decl_next::CapabilityRef { name: capability_ref.name })
344            }
345            decl::Ref::Debug(_) => decl_next::Ref::Debug(()),
346            decl::Ref::VoidType(_) => decl_next::Ref::VoidType(()),
347            decl::Ref::Environment(_) => decl_next::Ref::Environment(()),
348            decl::Ref::__SourceBreaking { unknown_ordinal } => {
349                decl_next::Ref::UnknownOrdinal_(unknown_ordinal)
350            }
351        }
352    }
353
354    fn convert_name_mapping(name_mapping: decl::NameMapping) -> decl_next::NameMapping {
355        fidl_next_fuchsia_component_decl::NameMapping {
356            source_name: name_mapping.source_name,
357            target_name: name_mapping.target_name,
358        }
359    }
360
361    fn convert_availability(availability: decl::Availability) -> decl_next::Availability {
362        match availability {
363            decl::Availability::Required => decl_next::Availability::Required,
364            decl::Availability::Optional => decl_next::Availability::Optional,
365            decl::Availability::SameAsTarget => decl_next::Availability::SameAsTarget,
366            decl::Availability::Transitional => decl_next::Availability::Transitional,
367        }
368    }
369
370    fn convert_dependency_type(dependency_type: decl::DependencyType) -> decl_next::DependencyType {
371        match dependency_type {
372            decl::DependencyType::Strong => decl_next::DependencyType::Strong,
373            decl::DependencyType::Weak => decl_next::DependencyType::Weak,
374        }
375    }
376
377    fn convert_rights(rights: fidl_fuchsia_io::Operations) -> fidl_next_fuchsia_io::Operations {
378        fidl_next_fuchsia_io::Operations::from_bits_retain(rights.bits())
379    }
380}
381
382#[cfg(test)]
383mod tests {
384    use super::*;
385    use crate::{Node, NodeBuilder, ServiceInstance, ServiceOffer};
386    use fidl_next::{Request, Responder};
387    use fidl_next_fuchsia_examples as fexample;
388    use fidl_next_fuchsia_examples::echo::{EchoString, SendString};
389    use fuchsia_async as fasync;
390    use futures::StreamExt;
391    use futures::lock::Mutex;
392    use log::info;
393
394    struct EchoServer;
395
396    impl fexample::EchoServerHandler<zx::Channel> for EchoServer {
397        async fn echo_string(
398            &mut self,
399            request: Request<EchoString, zx::Channel>,
400            responder: Responder<EchoString, zx::Channel>,
401        ) {
402            info!("ECHO: {}", request.payload().value);
403            responder.respond("resp").await.unwrap();
404        }
405
406        async fn send_string(&mut self, _request: Request<SendString, zx::Channel>) {}
407    }
408
409    struct Service {
410        scope: fasync::ScopeHandle,
411    }
412
413    impl fexample::EchoServiceHandler for Service {
414        fn regular_echo(&self, server_end: NextServerEnd<fexample::Echo>) {
415            server_end.spawn_on(EchoServer, &self.scope);
416        }
417
418        fn reversed_echo(&self, _server_end: NextServerEnd<fexample::Echo>) {}
419    }
420
421    #[allow(dead_code)]
422    struct TestDriver {
423        node: Node,
424        scope: fasync::Scope,
425        tmp: Mutex<String>,
426    }
427
428    impl TestDriver {
429        async fn set_tmp(&self, resp: &str) {
430            let mut tmp = self.tmp.lock().await;
431            *tmp = resp.to_string();
432        }
433
434        async fn get_tmp(&self) -> String {
435            let tmp = self.tmp.lock().await;
436            tmp.to_string()
437        }
438    }
439
440    impl Driver for TestDriver {
441        const NAME: &'static str = "test-driver";
442
443        async fn start(mut context: crate::DriverContext) -> Result<Self, Status> {
444            let service_proxy: ServiceInstance<fexample::EchoService> =
445                context.incoming.service().connect_next()?;
446            let (client_end, server_end) = fidl_next::fuchsia::create_channel();
447            service_proxy.regular_echo(server_end).unwrap();
448            let client = client_end.spawn();
449            let resp =
450                client.echo_string("echo from driver").await.map_err(|_| Status::IO_REFUSED)?;
451            assert_eq!("resp", resp.response.as_str());
452
453            let scope = fasync::Scope::new_with_name("test driver scope");
454            let mut outgoing = ServiceFs::new();
455            let offer = ServiceOffer::<fexample::EchoService>::new_next()
456                .add_named_next(&mut outgoing, "default", Service { scope: scope.to_handle() })
457                .build_zircon_offer_next();
458            context.serve_outgoing(&mut outgoing)?;
459            scope.spawn(outgoing.collect());
460
461            let node = context.take_node()?;
462            let child_node = NodeBuilder::new("transport-child")
463                .add_property("prop", "val")
464                .add_offer(offer)
465                .build();
466            node.add_child(child_node).await?;
467
468            info!("TestDriver started");
469            Ok(Self { node, scope, tmp: Mutex::new("NA".to_string()) })
470        }
471
472        async fn stop(&self) {
473            info!("TestDriver stopped. Tmp: '{}'", *self.tmp.lock().await);
474        }
475    }
476
477    #[fuchsia::test]
478    async fn test_basic() {
479        let scope = fasync::Scope::new_with_name("test scope");
480        let mut service_fs = ServiceFs::new();
481        let offer = ServiceOffer::<fexample::EchoService>::new_next()
482            .add_named_next(&mut service_fs, "default", Service { scope: scope.to_handle() })
483            .build_zircon_offer_next();
484        let mut harness = TestHarness::<TestDriver>::new()
485            .set_driver_incoming(service_fs)
486            .set_url("test_url")
487            .add_offer(offer);
488
489        let start_result = harness.start_driver().await;
490        let started_driver = start_result.expect("success");
491        let driver = started_driver.get_driver().expect("failed to get driver");
492        driver.set_tmp("my_temp_var").await;
493        assert_eq!("my_temp_var", driver.get_tmp().await);
494
495        let service_proxy: ServiceInstance<fexample::EchoService> =
496            started_driver.driver_outgoing().service().connect_next().unwrap();
497        let (client_end, server_end) = fidl_next::fuchsia::create_channel();
498        service_proxy.regular_echo(server_end).unwrap();
499        let client = client_end.spawn();
500        let resp = client.echo_string("echo to driver").await.unwrap();
501        assert_eq!("resp", resp.response.as_str());
502        started_driver.stop_driver().await;
503    }
504
505    #[fuchsia::test]
506    async fn test_multiple_start_stop() {
507        let scope = fasync::Scope::new_with_name("test scope");
508        let mut service_fs = ServiceFs::new();
509        let offer = ServiceOffer::<fexample::EchoService>::new_next()
510            .add_named_next(&mut service_fs, "default", Service { scope: scope.to_handle() })
511            .build_zircon_offer_next();
512        let mut harness = TestHarness::<TestDriver>::new()
513            .set_driver_incoming(service_fs)
514            .set_url("test_url")
515            .add_offer(offer);
516
517        for i in 1..=3 {
518            let start_result = harness.start_driver().await;
519            let started_driver = start_result.expect("success");
520            let driver = started_driver.get_driver().expect("failed to get driver");
521            driver.set_tmp(format!("my_temp_var_{}", i).as_str()).await;
522            assert_eq!(format!("my_temp_var_{}", i), driver.get_tmp().await);
523
524            let service_proxy: ServiceInstance<fexample::EchoService> =
525                started_driver.driver_outgoing().service().connect_next().unwrap();
526            let (client_end, server_end) = fidl_next::fuchsia::create_channel();
527            service_proxy.regular_echo(server_end).unwrap();
528            let client = client_end.spawn();
529            let resp = client.echo_string("echo to driver").await.unwrap();
530            assert_eq!("resp", resp.response.as_str());
531            started_driver.stop_driver().await;
532        }
533    }
534
535    #[fuchsia::test]
536    async fn test_no_start() {
537        let _harness = TestHarness::<TestDriver>::default();
538    }
539
540    #[fuchsia::test]
541    async fn test_start_fail() {
542        let mut harness = TestHarness::<TestDriver>::new();
543        let start_result = harness.start_driver().await;
544        assert_eq!(start_result.err(), Some(Status::IO_REFUSED));
545    }
546}