Skip to main content

fake_pdev/
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#![warn(missing_docs)]
6
7//! fake_pdev provides a fake platform-device implementation that can be used in unit tests.
8
9use fake_bti::FakeBti;
10use fidl_fuchsia_driver_framework as fdf;
11use fidl_next::{Request, Responder};
12use fidl_next_fuchsia_hardware_platform_device::{self as fdevice, DeviceServerHandler};
13use fuchsia_async as fasync;
14use fuchsia_component::server::ServiceFs;
15use fuchsia_sync::Mutex;
16use std::collections::HashMap;
17use std::sync::Arc;
18use zx::Status;
19
20#[derive(Default)]
21/// Holds resources used to create a `FakePDev` instance.
22pub struct Config {
23    /// If true, a BTI will be generated lazily if it does not exist.
24    pub use_fake_bti: bool,
25    /// If true, an SMC will be generated lazily if it does not exist.
26    pub use_fake_smc: bool,
27    /// If true, an interrupt will be generated lazily if it does not exist.
28    pub use_fake_irq: bool,
29    /// Key is the index of the MMIO.
30    pub mmios: HashMap<u32, fdevice::natural::Mmio>,
31    /// Maps the name of an MMIO to the index of the MMIO. The key is the name of the MMIO and the
32    /// value is the index of the MMIO.
33    pub mmio_names: HashMap<String, u32>,
34    /// Key is the index of the interrupt.
35    pub irqs: HashMap<u32, zx::Interrupt>,
36    /// Maps the name of an interrupt to the index of the interrupt. The key is the name of the
37    /// interrupt and the value is the index of the interrupt.
38    pub irq_names: HashMap<String, u32>,
39    /// Key is the index of the BTI.
40    pub btis: HashMap<u32, zx::Bti>,
41    /// Maps the name of an BTI to the index of the BTI. The key is the name of the BTI and the
42    /// value is the index of the BTI.
43    pub bti_names: HashMap<String, u32>,
44    /// Key is the index of the SMC.
45    pub smcs: HashMap<u32, zx::Resource>,
46    /// The info to pass provide to `GetNodeDeviceInfo()`.
47    pub device_info: Option<fdevice::natural::NodeDeviceInfo>,
48    /// The info to pass provide to `GetBoardInfo()`.
49    pub board_info: Option<fdevice::natural::BoardInfo>,
50    /// The power elements to provide to `GetPowerConfiguration()`.
51    pub power_elements: Vec<fidl_next_fuchsia_hardware_power::natural::PowerElementConfiguration>,
52}
53
54struct FakePDevState {
55    config: Config,
56    metadata: HashMap<String, Vec<u8>>,
57}
58
59#[derive(Clone)]
60/// A fake implementation of the fuchsia.hardware.platform.device.Device protocol.
61pub struct FakePDev {
62    state: Arc<Mutex<FakePDevState>>,
63}
64
65impl Default for FakePDev {
66    fn default() -> Self {
67        Self::new()
68    }
69}
70
71impl FakePDev {
72    /// Creates a new `FakePDev` with an empty config and metadata.
73    pub fn new() -> Self {
74        Self {
75            state: Arc::new(Mutex::new(FakePDevState {
76                config: Config::default(),
77                metadata: HashMap::new(),
78            })),
79        }
80    }
81
82    /// Sets the config after the `FakePDev` has been created.
83    pub fn set_config(&self, config: Config) {
84        self.state.lock().config = config;
85    }
86
87    /// Adds the given metadata to be provided through `GetMetadata()`.
88    pub fn add_metadata(&self, id: &str, data: Vec<u8>) {
89        self.state.lock().metadata.insert(id.to_string(), data);
90    }
91
92    /// Serves fuchsia.hardware.platform.device.Service with the given `ServiceFs` and instance name.
93    pub fn serve(
94        &self,
95        service_fs: &mut ServiceFs<fuchsia_component::server::ServiceObj<'static, ()>>,
96        scope: fasync::ScopeHandle,
97        instance_name: &str,
98    ) -> fdf::Offer {
99        let state_clone = self.state.clone();
100
101        fdf_component::ServiceOffer::<fdevice::Service>::new_next()
102            .add_default_named_next(
103                service_fs,
104                instance_name,
105                FakePDevService { state: state_clone, scope },
106            )
107            .build_zircon_offer_next()
108    }
109}
110
111struct FakePDevService {
112    state: Arc<Mutex<FakePDevState>>,
113    scope: fasync::ScopeHandle,
114}
115
116impl fdevice::ServiceHandler for FakePDevService {
117    fn device(&self, server_end: fidl_next::ServerEnd<fdevice::Device>) {
118        server_end.spawn_on(FakePDevServer { state: self.state.clone() }, &self.scope);
119    }
120}
121
122struct FakePDevServer {
123    state: Arc<Mutex<FakePDevState>>,
124}
125
126impl FakePDevServer {
127    fn duplicate_mmio(mmio: &fdevice::natural::Mmio) -> fdevice::natural::Mmio {
128        let dup_vmo =
129            mmio.vmo.as_ref().map(|v| v.duplicate_handle(zx::Rights::SAME_RIGHTS).unwrap());
130        fdevice::natural::Mmio {
131            offset: mmio.offset,
132            size: mmio.size,
133            vmo: dup_vmo.map(Into::into),
134        }
135    }
136}
137
138impl DeviceServerHandler for FakePDevServer {
139    async fn get_mmio_by_id(
140        &mut self,
141        request: Request<fdevice::device::GetMmioById>,
142        responder: Responder<fdevice::device::GetMmioById>,
143    ) {
144        let index = request.payload().index;
145        let mmio_clone =
146            self.state.lock().config.mmios.get(&index).map(FakePDevServer::duplicate_mmio);
147        if let Some(mmio) = mmio_clone {
148            let _ = responder.respond(mmio).await;
149        } else {
150            let _ = responder.respond_err(Status::NOT_FOUND).await;
151        }
152    }
153
154    async fn get_mmio_by_name(
155        &mut self,
156        request: Request<fdevice::device::GetMmioByName>,
157        responder: Responder<fdevice::device::GetMmioByName>,
158    ) {
159        let name = request.payload().name.as_str().to_string();
160        let mmio_clone = {
161            let state = self.state.lock();
162            state
163                .config
164                .mmio_names
165                .get(&name)
166                .and_then(|idx| state.config.mmios.get(idx))
167                .map(FakePDevServer::duplicate_mmio)
168        };
169
170        let _ = responder.respond_with(mmio_clone.ok_or(Status::NOT_FOUND)).await;
171    }
172
173    async fn get_interrupt_by_id(
174        &mut self,
175        request: Request<fdevice::device::GetInterruptById>,
176        responder: Responder<fdevice::device::GetInterruptById>,
177    ) {
178        let index = request.payload().index;
179        let (irq_res, use_fake) = {
180            let state = self.state.lock();
181            let irq_res =
182                state.config.irqs.get(&index).map(|i| i.duplicate_handle(zx::Rights::SAME_RIGHTS));
183            (irq_res, state.config.use_fake_irq)
184        };
185
186        let res: Result<zx::Interrupt, zx::Status> = if let Some(res) = irq_res {
187            res
188        } else if use_fake {
189            zx::VirtualInterrupt::create_virtual().map(|irq| zx::Interrupt::from(irq.into_handle()))
190        } else {
191            Err(Status::NOT_FOUND)
192        };
193
194        match res {
195            Ok(irq) => {
196                let _ = responder.respond(irq).await;
197            }
198            Err(e) => {
199                let _ = responder.respond_err(e).await;
200            }
201        }
202    }
203
204    async fn get_interrupt_by_name(
205        &mut self,
206        request: Request<fdevice::device::GetInterruptByName>,
207        responder: Responder<fdevice::device::GetInterruptByName>,
208    ) {
209        let name = request.payload().name.as_str().to_string();
210        let (irq_res, use_fake) = {
211            let state = self.state.lock();
212            let irq_res = state
213                .config
214                .irq_names
215                .get(&name)
216                .and_then(|idx| state.config.irqs.get(idx))
217                .map(|i| i.duplicate_handle(zx::Rights::SAME_RIGHTS));
218            (irq_res, state.config.use_fake_irq)
219        };
220
221        let res = if let Some(res) = irq_res {
222            res
223        } else if use_fake {
224            zx::VirtualInterrupt::create_virtual().map(|irq| zx::Interrupt::from(irq.into_handle()))
225        } else {
226            Err(Status::NOT_FOUND)
227        };
228
229        match res {
230            Ok(irq) => {
231                let _ = responder.respond(irq).await;
232            }
233            Err(e) => {
234                let _ = responder.respond_err(e).await;
235            }
236        }
237    }
238
239    async fn get_bti_by_id(
240        &mut self,
241        request: Request<fdevice::device::GetBtiById>,
242        responder: Responder<fdevice::device::GetBtiById>,
243    ) {
244        let index = request.payload().index;
245        let (bti_res, use_fake) = {
246            let state = self.state.lock();
247            let bti_res =
248                state.config.btis.get(&index).map(|b| b.duplicate_handle(zx::Rights::SAME_RIGHTS));
249            (bti_res, state.config.use_fake_bti)
250        };
251
252        let res = if let Some(res) = bti_res {
253            res
254        } else if use_fake {
255            FakeBti::create().and_then(|fake| fake.duplicate_handle(zx::Rights::SAME_RIGHTS))
256        } else {
257            Err(Status::NOT_FOUND)
258        };
259
260        match res {
261            Ok(bti) => {
262                let _ = responder.respond(bti).await;
263            }
264            Err(status) => {
265                let _ = responder.respond_err(status).await;
266            }
267        }
268    }
269
270    async fn get_bti_by_name(
271        &mut self,
272        request: Request<fdevice::device::GetBtiByName>,
273        responder: Responder<fdevice::device::GetBtiByName>,
274    ) {
275        let name = request.payload().name.as_str().to_string();
276        let (bti_res, use_fake) = {
277            let state = self.state.lock();
278            let bti_res = state
279                .config
280                .bti_names
281                .get(&name)
282                .and_then(|idx| state.config.btis.get(idx))
283                .map(|b| b.duplicate_handle(zx::Rights::SAME_RIGHTS));
284            (bti_res, state.config.use_fake_bti)
285        };
286
287        let res = if let Some(res) = bti_res {
288            res
289        } else if use_fake {
290            FakeBti::create().and_then(|fake| fake.duplicate_handle(zx::Rights::SAME_RIGHTS))
291        } else {
292            Err(Status::NOT_FOUND)
293        };
294
295        match res {
296            Ok(bti) => {
297                let _ = responder.respond(bti).await;
298            }
299            Err(status) => {
300                let _ = responder.respond_err(status).await;
301            }
302        }
303    }
304
305    async fn get_smc_by_id(
306        &mut self,
307        request: Request<fdevice::device::GetSmcById>,
308        responder: Responder<fdevice::device::GetSmcById>,
309    ) {
310        let index = request.payload().index;
311        let smc_res = self
312            .state
313            .lock()
314            .config
315            .smcs
316            .get(&index)
317            .map(|s| s.duplicate_handle(zx::Rights::SAME_RIGHTS));
318
319        let res = smc_res.unwrap_or(Err(Status::NOT_FOUND));
320
321        match res {
322            Ok(dup) => {
323                let _ = responder.respond(dup).await;
324            }
325            Err(status) => {
326                let _ = responder.respond_err(status).await;
327            }
328        }
329    }
330
331    async fn get_smc_by_name(
332        &mut self,
333        _request: Request<fdevice::device::GetSmcByName>,
334        responder: Responder<fdevice::device::GetSmcByName>,
335    ) {
336        let _ = responder.respond_err(Status::NOT_FOUND).await;
337    }
338
339    async fn get_power_configuration(
340        &mut self,
341        responder: Responder<fdevice::device::GetPowerConfiguration>,
342    ) {
343        let power_elements = self.state.lock().config.power_elements.clone();
344        let _ = responder.respond(power_elements).await;
345    }
346
347    async fn get_node_device_info(
348        &mut self,
349        responder: Responder<fdevice::device::GetNodeDeviceInfo>,
350    ) {
351        let device_info = self.state.lock().config.device_info.clone();
352        let _ = responder.respond_with(device_info.ok_or(Status::NOT_SUPPORTED)).await;
353    }
354
355    async fn get_board_info(&mut self, responder: Responder<fdevice::device::GetBoardInfo>) {
356        let board_info = self.state.lock().config.board_info.clone();
357        let _ = responder.respond_with(board_info.ok_or(Status::NOT_SUPPORTED)).await;
358    }
359
360    async fn get_metadata(
361        &mut self,
362        request: Request<fdevice::device::GetMetadata>,
363        responder: Responder<fdevice::device::GetMetadata>,
364    ) {
365        let metadata = self.state.lock().metadata.get(request.payload().id.as_str()).cloned();
366        if let Some(data) = metadata {
367            let _ = responder.respond(data).await;
368        } else {
369            let _ = responder.respond_err(Status::NOT_FOUND).await;
370        }
371    }
372}
373
374#[cfg(test)]
375mod tests {
376    use super::*;
377    use fidl_next::fuchsia::create_channel;
378
379    async fn run_test_with_config<F, Fut>(config: Config, test_func: F)
380    where
381        F: FnOnce(fidl_next::Client<fdevice::Device>, FakePDev) -> Fut,
382        Fut: std::future::Future<Output = ()>,
383    {
384        let fake_pdev = FakePDev::new();
385        fake_pdev.set_config(config);
386
387        let (client_end, server_end) = create_channel::<fdevice::Device>();
388        let server = FakePDevServer { state: fake_pdev.state.clone() };
389        let scope = fasync::Scope::new_with_name("test");
390        server_end.spawn_on(server, &scope);
391
392        let client = client_end.spawn();
393        test_func(client, fake_pdev).await;
394    }
395
396    #[fuchsia::test]
397    async fn test_get_mmios() {
398        let mut mmios = HashMap::new();
399        let vmo = zx::Vmo::create(11).unwrap();
400        mmios.insert(
401            5,
402            fdevice::natural::Mmio { offset: Some(10), size: Some(11), vmo: Some(vmo.into()) },
403        );
404        let mut mmio_names = HashMap::new();
405        mmio_names.insert("test-name".to_string(), 5);
406
407        run_test_with_config(
408            Config { mmios, mmio_names, ..Default::default() },
409            |client, _| async move {
410                // By ID
411                let res = client.get_mmio_by_id(5).await.unwrap();
412                assert!(res.is_ok());
413                let mmio = res.unwrap();
414                assert_eq!(mmio.offset, Some(10));
415                assert_eq!(mmio.size, Some(11));
416
417                let res_err = client.get_mmio_by_id(4).await.unwrap();
418                assert!(res_err.is_err());
419
420                // By Name
421                let res_name = client.get_mmio_by_name("test-name").await.unwrap();
422                assert!(res_name.is_ok());
423                let mmio_name = res_name.unwrap();
424                assert_eq!(mmio_name.offset, Some(10));
425                assert_eq!(mmio_name.size, Some(11));
426
427                let res_name_err = client.get_mmio_by_name("unknown-name").await.unwrap();
428                assert!(res_name_err.is_err());
429            },
430        )
431        .await;
432    }
433
434    #[fuchsia::test]
435    async fn test_invalid_mmio() {
436        let mut mmios = HashMap::new();
437        mmios.insert(
438            5,
439            fdevice::natural::Mmio {
440                offset: Some(10),
441                size: Some(11),
442                vmo: None, // Invalid mmio handle
443            },
444        );
445        run_test_with_config(Config { mmios, ..Default::default() }, |client, _| async move {
446            let res = client.get_mmio_by_id(5).await.unwrap();
447            assert!(res.is_ok());
448            assert!(res.unwrap().vmo.is_none());
449        })
450        .await;
451    }
452
453    #[fuchsia::test]
454    async fn test_get_irqs() {
455        let mut irqs = HashMap::new();
456        let irq = zx::VirtualInterrupt::create_virtual().unwrap();
457        irqs.insert(5, zx::Interrupt::from(zx::NullableHandle::from(irq.into_handle())));
458        let mut irq_names = HashMap::new();
459        irq_names.insert("test-name".to_string(), 5);
460
461        run_test_with_config(
462            Config { irqs, irq_names, ..Default::default() },
463            |client, _| async move {
464                let res = client.get_interrupt_by_id(5, 0).await.unwrap();
465                assert!(res.is_ok());
466
467                let res_err = client.get_interrupt_by_id(4, 0).await.unwrap();
468                assert!(res_err.is_err());
469
470                let res_name = client.get_interrupt_by_name("test-name", 0).await.unwrap();
471                assert!(res_name.is_ok());
472            },
473        )
474        .await;
475    }
476
477    #[fuchsia::test]
478    async fn test_get_btis() {
479        let mut btis = HashMap::new();
480        let bti = FakeBti::create().unwrap();
481        btis.insert(5, bti.duplicate_handle(zx::Rights::SAME_RIGHTS).unwrap());
482
483        run_test_with_config(Config { btis, ..Default::default() }, |client, _| async move {
484            let res = client.get_bti_by_id(5).await.unwrap();
485            assert!(res.is_ok());
486
487            let res_err = client.get_bti_by_id(4).await.unwrap();
488            assert!(res_err.is_err());
489        })
490        .await;
491    }
492
493    #[fuchsia::test]
494    async fn test_get_smc() {
495        unsafe extern "C" {
496            fn fake_root_resource_create(out: *mut zx::sys::zx_handle_t) -> zx::sys::zx_status_t;
497        }
498        let mut raw = zx::sys::ZX_HANDLE_INVALID;
499        unsafe {
500            assert_eq!(fake_root_resource_create(&mut raw), zx::sys::ZX_OK);
501        }
502        let smc = unsafe { zx::Resource::from(zx::Handle::from_raw(raw).unwrap()) };
503
504        let mut smcs = HashMap::new();
505        smcs.insert(5, smc);
506
507        run_test_with_config(Config { smcs, ..Default::default() }, |client, _| async move {
508            let res = client.get_smc_by_id(5).await.unwrap();
509            assert!(res.is_ok());
510
511            let res_err = client.get_smc_by_id(4).await.unwrap();
512            assert!(res_err.is_err());
513        })
514        .await;
515    }
516
517    #[fuchsia::test]
518    async fn test_get_device_info() {
519        let device_info = Some(fdevice::natural::NodeDeviceInfo {
520            vid: Some(1),
521            pid: Some(1),
522            name: Some("test device".to_string()),
523            ..Default::default()
524        });
525        run_test_with_config(
526            Config { device_info, ..Default::default() },
527            |client, _| async move {
528                let res = client.get_node_device_info().await.unwrap();
529                assert!(res.is_ok());
530                let info = res.unwrap();
531                assert_eq!(info.vid, Some(1));
532                assert_eq!(info.pid, Some(1));
533                assert_eq!(info.name, Some("test device".to_string()));
534            },
535        )
536        .await;
537    }
538
539    #[fuchsia::test]
540    async fn test_get_board_info() {
541        let board_info =
542            Some(fdevice::natural::BoardInfo { vid: Some(1), pid: Some(1), ..Default::default() });
543        run_test_with_config(Config { board_info, ..Default::default() }, |client, _| async move {
544            let res = client.get_board_info().await.unwrap();
545            assert!(res.is_ok());
546            let info = res.unwrap();
547            assert_eq!(info.vid, Some(1));
548            assert_eq!(info.pid, Some(1));
549        })
550        .await;
551    }
552
553    #[fuchsia::test]
554    async fn test_get_power_configuration() {
555        let power_elements =
556            vec![fidl_next_fuchsia_hardware_power::natural::PowerElementConfiguration {
557                element: Some(fidl_next_fuchsia_hardware_power::natural::PowerElement {
558                    name: Some("test power element".to_string()),
559                    ..Default::default()
560                }),
561                ..Default::default()
562            }];
563        run_test_with_config(
564            Config { power_elements, ..Default::default() },
565            |client, _| async move {
566                let res = client.get_power_configuration().await.unwrap();
567                assert!(res.is_ok());
568                let configs = res.unwrap().config;
569                assert_eq!(configs.len(), 1);
570                assert_eq!(
571                    configs[0].element.as_ref().unwrap().name,
572                    Some("test power element".to_string())
573                );
574            },
575        )
576        .await;
577    }
578
579    #[fuchsia::test]
580    async fn test_get_metadata() {
581        run_test_with_config(Default::default(), |client, fake_pdev| async move {
582            fake_pdev.add_metadata("test_id", vec![1, 2, 3, 4]);
583            let res = client.get_metadata("test_id").await.unwrap();
584            assert!(res.is_ok());
585            assert_eq!(res.unwrap().metadata, vec![1, 2, 3, 4]);
586
587            let res_err = client.get_metadata("unknown_id").await.unwrap();
588            assert!(res_err.is_err());
589        })
590        .await;
591    }
592}