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.into_raw()).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        if let Some(mmio) = mmio_clone {
170            let _ = responder.respond(mmio).await;
171        } else {
172            let _ = responder.respond_err(Status::NOT_FOUND.into_raw()).await;
173        }
174    }
175
176    async fn get_interrupt_by_id(
177        &mut self,
178        request: Request<fdevice::device::GetInterruptById>,
179        responder: Responder<fdevice::device::GetInterruptById>,
180    ) {
181        let index = request.payload().index;
182        let (irq_res, use_fake) = {
183            let state = self.state.lock();
184            let irq_res =
185                state.config.irqs.get(&index).map(|i| i.duplicate_handle(zx::Rights::SAME_RIGHTS));
186            (irq_res, state.config.use_fake_irq)
187        };
188
189        let res: Result<zx::Interrupt, zx::Status> = if let Some(res) = irq_res {
190            res
191        } else if use_fake {
192            zx::VirtualInterrupt::create_virtual().map(|irq| zx::Interrupt::from(irq.into_handle()))
193        } else {
194            Err(Status::NOT_FOUND)
195        };
196
197        match res {
198            Ok(irq) => {
199                let _ = responder.respond(irq).await;
200            }
201            Err(e) => {
202                let _ = responder.respond_err(e.into_raw()).await;
203            }
204        }
205    }
206
207    async fn get_interrupt_by_name(
208        &mut self,
209        request: Request<fdevice::device::GetInterruptByName>,
210        responder: Responder<fdevice::device::GetInterruptByName>,
211    ) {
212        let name = request.payload().name.as_str().to_string();
213        let (irq_res, use_fake) = {
214            let state = self.state.lock();
215            let irq_res = state
216                .config
217                .irq_names
218                .get(&name)
219                .and_then(|idx| state.config.irqs.get(idx))
220                .map(|i| i.duplicate_handle(zx::Rights::SAME_RIGHTS));
221            (irq_res, state.config.use_fake_irq)
222        };
223
224        let res = if let Some(res) = irq_res {
225            res
226        } else if use_fake {
227            zx::VirtualInterrupt::create_virtual().map(|irq| zx::Interrupt::from(irq.into_handle()))
228        } else {
229            Err(Status::NOT_FOUND)
230        };
231
232        match res {
233            Ok(irq) => {
234                let _ = responder.respond(irq).await;
235            }
236            Err(e) => {
237                let _ = responder.respond_err(e.into_raw()).await;
238            }
239        }
240    }
241
242    async fn get_bti_by_id(
243        &mut self,
244        request: Request<fdevice::device::GetBtiById>,
245        responder: Responder<fdevice::device::GetBtiById>,
246    ) {
247        let index = request.payload().index;
248        let (bti_res, use_fake) = {
249            let state = self.state.lock();
250            let bti_res =
251                state.config.btis.get(&index).map(|b| b.duplicate_handle(zx::Rights::SAME_RIGHTS));
252            (bti_res, state.config.use_fake_bti)
253        };
254
255        let res = if let Some(res) = bti_res {
256            res
257        } else if use_fake {
258            FakeBti::create().and_then(|fake| fake.duplicate_handle(zx::Rights::SAME_RIGHTS))
259        } else {
260            Err(Status::NOT_FOUND)
261        };
262
263        match res {
264            Ok(bti) => {
265                let _ = responder.respond(bti).await;
266            }
267            Err(status) => {
268                let _ = responder.respond_err(status.into_raw()).await;
269            }
270        }
271    }
272
273    async fn get_bti_by_name(
274        &mut self,
275        request: Request<fdevice::device::GetBtiByName>,
276        responder: Responder<fdevice::device::GetBtiByName>,
277    ) {
278        let name = request.payload().name.as_str().to_string();
279        let (bti_res, use_fake) = {
280            let state = self.state.lock();
281            let bti_res = state
282                .config
283                .bti_names
284                .get(&name)
285                .and_then(|idx| state.config.btis.get(idx))
286                .map(|b| b.duplicate_handle(zx::Rights::SAME_RIGHTS));
287            (bti_res, state.config.use_fake_bti)
288        };
289
290        let res = if let Some(res) = bti_res {
291            res
292        } else if use_fake {
293            FakeBti::create().and_then(|fake| fake.duplicate_handle(zx::Rights::SAME_RIGHTS))
294        } else {
295            Err(Status::NOT_FOUND)
296        };
297
298        match res {
299            Ok(bti) => {
300                let _ = responder.respond(bti).await;
301            }
302            Err(status) => {
303                let _ = responder.respond_err(status.into_raw()).await;
304            }
305        }
306    }
307
308    async fn get_smc_by_id(
309        &mut self,
310        request: Request<fdevice::device::GetSmcById>,
311        responder: Responder<fdevice::device::GetSmcById>,
312    ) {
313        let index = request.payload().index;
314        let smc_res = self
315            .state
316            .lock()
317            .config
318            .smcs
319            .get(&index)
320            .map(|s| s.duplicate_handle(zx::Rights::SAME_RIGHTS));
321
322        let res = smc_res.unwrap_or(Err(Status::NOT_FOUND));
323
324        match res {
325            Ok(dup) => {
326                let _ = responder.respond(dup).await;
327            }
328            Err(status) => {
329                let _ = responder.respond_err(status.into_raw()).await;
330            }
331        }
332    }
333
334    async fn get_smc_by_name(
335        &mut self,
336        _request: Request<fdevice::device::GetSmcByName>,
337        responder: Responder<fdevice::device::GetSmcByName>,
338    ) {
339        let _ = responder.respond_err(Status::NOT_FOUND.into_raw()).await;
340    }
341
342    async fn get_power_configuration(
343        &mut self,
344        responder: Responder<fdevice::device::GetPowerConfiguration>,
345    ) {
346        let power_elements = self.state.lock().config.power_elements.clone();
347        let _ = responder.respond(power_elements).await;
348    }
349
350    async fn get_node_device_info(
351        &mut self,
352        responder: Responder<fdevice::device::GetNodeDeviceInfo>,
353    ) {
354        let device_info = self.state.lock().config.device_info.clone();
355        if let Some(info) = device_info {
356            let _ = responder.respond(info).await;
357        } else {
358            let _ = responder.respond_err(Status::NOT_SUPPORTED.into_raw()).await;
359        }
360    }
361
362    async fn get_board_info(&mut self, responder: Responder<fdevice::device::GetBoardInfo>) {
363        let board_info = self.state.lock().config.board_info.clone();
364        if let Some(info) = board_info {
365            let _ = responder.respond(info).await;
366        } else {
367            let _ = responder.respond_err(Status::NOT_SUPPORTED.into_raw()).await;
368        }
369    }
370
371    async fn get_metadata(
372        &mut self,
373        request: Request<fdevice::device::GetMetadata>,
374        responder: Responder<fdevice::device::GetMetadata>,
375    ) {
376        let metadata = self.state.lock().metadata.get(request.payload().id.as_str()).cloned();
377        if let Some(data) = metadata {
378            let _ = responder.respond(data).await;
379        } else {
380            let _ = responder.respond_err(Status::NOT_FOUND.into_raw()).await;
381        }
382    }
383}
384
385#[cfg(test)]
386mod tests {
387    use super::*;
388    use fidl_next::fuchsia::create_channel;
389
390    async fn run_test_with_config<F, Fut>(config: Config, test_func: F)
391    where
392        F: FnOnce(fidl_next::Client<fdevice::Device>, FakePDev) -> Fut,
393        Fut: std::future::Future<Output = ()>,
394    {
395        let fake_pdev = FakePDev::new();
396        fake_pdev.set_config(config);
397
398        let (client_end, server_end) = create_channel::<fdevice::Device>();
399        let server = FakePDevServer { state: fake_pdev.state.clone() };
400        let scope = fasync::Scope::new_with_name("test");
401        server_end.spawn_on(server, &scope);
402
403        let client = client_end.spawn();
404        test_func(client, fake_pdev).await;
405    }
406
407    #[fuchsia::test]
408    async fn test_get_mmios() {
409        let mut mmios = HashMap::new();
410        let vmo = zx::Vmo::create(11).unwrap();
411        mmios.insert(
412            5,
413            fdevice::natural::Mmio { offset: Some(10), size: Some(11), vmo: Some(vmo.into()) },
414        );
415        let mut mmio_names = HashMap::new();
416        mmio_names.insert("test-name".to_string(), 5);
417
418        run_test_with_config(
419            Config { mmios, mmio_names, ..Default::default() },
420            |client, _| async move {
421                // By ID
422                let res = client.get_mmio_by_id(5).await.unwrap();
423                assert!(res.is_ok());
424                let mmio = res.unwrap();
425                assert_eq!(mmio.offset, Some(10));
426                assert_eq!(mmio.size, Some(11));
427
428                let res_err = client.get_mmio_by_id(4).await.unwrap();
429                assert!(res_err.is_err());
430
431                // By Name
432                let res_name = client.get_mmio_by_name("test-name").await.unwrap();
433                assert!(res_name.is_ok());
434                let mmio_name = res_name.unwrap();
435                assert_eq!(mmio_name.offset, Some(10));
436                assert_eq!(mmio_name.size, Some(11));
437
438                let res_name_err = client.get_mmio_by_name("unknown-name").await.unwrap();
439                assert!(res_name_err.is_err());
440            },
441        )
442        .await;
443    }
444
445    #[fuchsia::test]
446    async fn test_invalid_mmio() {
447        let mut mmios = HashMap::new();
448        mmios.insert(
449            5,
450            fdevice::natural::Mmio {
451                offset: Some(10),
452                size: Some(11),
453                vmo: None, // Invalid mmio handle
454            },
455        );
456        run_test_with_config(Config { mmios, ..Default::default() }, |client, _| async move {
457            let res = client.get_mmio_by_id(5).await.unwrap();
458            assert!(res.is_ok());
459            assert!(res.unwrap().vmo.is_none());
460        })
461        .await;
462    }
463
464    #[fuchsia::test]
465    async fn test_get_irqs() {
466        let mut irqs = HashMap::new();
467        let irq = zx::VirtualInterrupt::create_virtual().unwrap();
468        irqs.insert(5, zx::Interrupt::from(zx::NullableHandle::from(irq.into_handle())));
469        let mut irq_names = HashMap::new();
470        irq_names.insert("test-name".to_string(), 5);
471
472        run_test_with_config(
473            Config { irqs, irq_names, ..Default::default() },
474            |client, _| async move {
475                let res = client.get_interrupt_by_id(5, 0).await.unwrap();
476                assert!(res.is_ok());
477
478                let res_err = client.get_interrupt_by_id(4, 0).await.unwrap();
479                assert!(res_err.is_err());
480
481                let res_name = client.get_interrupt_by_name("test-name", 0).await.unwrap();
482                assert!(res_name.is_ok());
483            },
484        )
485        .await;
486    }
487
488    #[fuchsia::test]
489    async fn test_get_btis() {
490        let mut btis = HashMap::new();
491        let bti = FakeBti::create().unwrap();
492        btis.insert(5, bti.duplicate_handle(zx::Rights::SAME_RIGHTS).unwrap());
493
494        run_test_with_config(Config { btis, ..Default::default() }, |client, _| async move {
495            let res = client.get_bti_by_id(5).await.unwrap();
496            assert!(res.is_ok());
497
498            let res_err = client.get_bti_by_id(4).await.unwrap();
499            assert!(res_err.is_err());
500        })
501        .await;
502    }
503
504    #[fuchsia::test]
505    async fn test_get_smc() {
506        unsafe extern "C" {
507            fn fake_root_resource_create(out: *mut zx::sys::zx_handle_t) -> zx::sys::zx_status_t;
508        }
509        let mut raw = zx::sys::ZX_HANDLE_INVALID;
510        unsafe {
511            assert_eq!(fake_root_resource_create(&mut raw), zx::sys::ZX_OK);
512        }
513        let smc = unsafe { zx::Resource::from(zx::Handle::from_raw(raw).unwrap()) };
514
515        let mut smcs = HashMap::new();
516        smcs.insert(5, smc);
517
518        run_test_with_config(Config { smcs, ..Default::default() }, |client, _| async move {
519            let res = client.get_smc_by_id(5).await.unwrap();
520            assert!(res.is_ok());
521
522            let res_err = client.get_smc_by_id(4).await.unwrap();
523            assert!(res_err.is_err());
524        })
525        .await;
526    }
527
528    #[fuchsia::test]
529    async fn test_get_device_info() {
530        let device_info = Some(fdevice::natural::NodeDeviceInfo {
531            vid: Some(1),
532            pid: Some(1),
533            name: Some("test device".to_string()),
534            ..Default::default()
535        });
536        run_test_with_config(
537            Config { device_info, ..Default::default() },
538            |client, _| async move {
539                let res = client.get_node_device_info().await.unwrap();
540                assert!(res.is_ok());
541                let info = res.unwrap();
542                assert_eq!(info.vid, Some(1));
543                assert_eq!(info.pid, Some(1));
544                assert_eq!(info.name, Some("test device".to_string()));
545            },
546        )
547        .await;
548    }
549
550    #[fuchsia::test]
551    async fn test_get_board_info() {
552        let board_info =
553            Some(fdevice::natural::BoardInfo { vid: Some(1), pid: Some(1), ..Default::default() });
554        run_test_with_config(Config { board_info, ..Default::default() }, |client, _| async move {
555            let res = client.get_board_info().await.unwrap();
556            assert!(res.is_ok());
557            let info = res.unwrap();
558            assert_eq!(info.vid, Some(1));
559            assert_eq!(info.pid, Some(1));
560        })
561        .await;
562    }
563
564    #[fuchsia::test]
565    async fn test_get_power_configuration() {
566        let power_elements =
567            vec![fidl_next_fuchsia_hardware_power::natural::PowerElementConfiguration {
568                element: Some(fidl_next_fuchsia_hardware_power::natural::PowerElement {
569                    name: Some("test power element".to_string()),
570                    ..Default::default()
571                }),
572                ..Default::default()
573            }];
574        run_test_with_config(
575            Config { power_elements, ..Default::default() },
576            |client, _| async move {
577                let res = client.get_power_configuration().await.unwrap();
578                assert!(res.is_ok());
579                let configs = res.unwrap().config;
580                assert_eq!(configs.len(), 1);
581                assert_eq!(
582                    configs[0].element.as_ref().unwrap().name,
583                    Some("test power element".to_string())
584                );
585            },
586        )
587        .await;
588    }
589
590    #[fuchsia::test]
591    async fn test_get_metadata() {
592        run_test_with_config(Default::default(), |client, fake_pdev| async move {
593            fake_pdev.add_metadata("test_id", vec![1, 2, 3, 4]);
594            let res = client.get_metadata("test_id").await.unwrap();
595            assert!(res.is_ok());
596            assert_eq!(res.unwrap().metadata, vec![1, 2, 3, 4]);
597
598            let res_err = client.get_metadata("unknown_id").await.unwrap();
599            assert!(res_err.is_err());
600        })
601        .await;
602    }
603}