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