Skip to main content

hci_emulator_client/
lib.rs

1// Copyright 2018 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
5use anyhow::{Context as _, Error, format_err};
6use fidl::endpoints::Proxy as _;
7use fidl_fuchsia_device::{ControllerMarker, ControllerProxy};
8use fidl_fuchsia_hardware_bluetooth::{
9    EmulatorError, EmulatorMarker, EmulatorProxy, EmulatorSettings, VirtualControllerMarker,
10};
11use fidl_fuchsia_io::DirectoryProxy;
12use fuchsia_async::{DurationExt as _, TimeoutExt as _};
13use fuchsia_bluetooth::constants::{DEV_DIR, INTEGRATION_TIMEOUT as WATCH_TIMEOUT};
14
15use futures::TryFutureExt as _;
16use log::error;
17
18pub mod types;
19
20const EMULATOR_DEVICE_DIR: &str = "class/bt-emulator";
21
22/// Returns true if `topological_path` is that of the emulator node `name` published directly
23/// under `<DEV_DIR>/<emulator_device_path>`. The whole last path component must match:
24/// `emulator-1` must not match `emulator-10`.
25fn is_emulator_node_path(topological_path: &str, emulator_device_path: &str, name: &str) -> bool {
26    topological_path
27        .strip_prefix(DEV_DIR)
28        .and_then(|path| path.strip_prefix('/'))
29        .and_then(|path| path.strip_prefix(emulator_device_path))
30        .and_then(|path| path.strip_prefix('/'))
31        .and_then(|path| path.strip_prefix(name))
32        .is_some_and(str::is_empty)
33}
34
35/// Represents a bt-hci device emulator. Instances of this type can be used manage the
36/// bt-hci-emulator driver within the test device hierarchy. The associated driver instance gets
37/// unbound and all bt-hci and bt-emulator device instances destroyed when
38/// `destroy_and_wait()` resolves successfully.
39/// `destroy_and_wait()` MUST be called for proper clean up of the emulator device.
40pub struct Emulator {
41    /// This will have a value when the emulator is instantiated and will be reset to None
42    /// in `destroy_and_wait()`. This is so the destructor can assert that the TestDevice has been
43    /// destroyed.
44    dev: Option<TestDevice>,
45}
46
47impl Emulator {
48    /// Returns the default settings.
49    // TODO(armansito): Consider defining a library type for EmulatorSettings.
50    pub fn default_settings() -> EmulatorSettings {
51        EmulatorSettings {
52            address: None,
53            hci_config: None,
54            extended_advertising: None,
55            acl_buffer_settings: None,
56            le_acl_buffer_settings: None,
57            ..Default::default()
58        }
59    }
60
61    /// Publish a new bt-emulator device and return a handle to it. No corresponding bt-hci device
62    /// will be published; to do so it must be explicitly configured and created with a call to
63    /// `publish()`. If `realm` is present, the device will be created inside it, otherwise it will
64    /// be created using the `/dev` directory in the component's namespace.
65    pub async fn create(dev_directory: DirectoryProxy) -> Result<Emulator, Error> {
66        let dev = TestDevice::create(dev_directory)
67            .await
68            .context(format!("Error creating test device"))?;
69        Ok(Emulator { dev: Some(dev) })
70    }
71
72    /// Publish a bt-emulator and a bt-hci device using the default emulator settings. If `realm`
73    /// is present, the device will be created inside it, otherwise it will be created using the
74    /// `/dev` directory in the component's namespace.
75    pub async fn create_and_publish(dev_directory: DirectoryProxy) -> Result<Emulator, Error> {
76        let fake_dev = Self::create(dev_directory).await?;
77        fake_dev.publish(Self::default_settings()).await?;
78        Ok(fake_dev)
79    }
80
81    /// Sends a publish message to the emulator. This is a convenience method that internally
82    /// handles the FIDL binding error.
83    pub async fn publish(&self, settings: EmulatorSettings) -> Result<(), Error> {
84        let dev = self.dev.as_ref().expect("emulator device accessed after it was destroyed!");
85        dev.emulator
86            .publish(&settings)
87            .await
88            .context("publish transport")?
89            .map_err(|e: EmulatorError| format_err!("failed to publish bt-hci device: {:#?}", e))
90    }
91
92    /// Sends the test device a destroy message which will unbind the driver.
93    /// This will wait for the test device to be unpublished from devfs.
94    pub async fn destroy_and_wait(&mut self) -> Result<(), Error> {
95        self.dev
96            .take()
97            .expect("attempted to destroy an already destroyed emulator device")
98            .destroy_and_wait()
99            .await
100    }
101
102    pub async fn get_topological_path(&self) -> Result<String, Error> {
103        let dev = self.dev.as_ref().expect("emulator device accessed after it was destroyed!");
104        dev.get_topological_path().await
105    }
106
107    pub fn emulator(&self) -> &EmulatorProxy {
108        &self.dev.as_ref().unwrap().emulator
109    }
110}
111
112impl Drop for Emulator {
113    fn drop(&mut self) {
114        if self.dev.is_some() {
115            error!("Did not call destroy() on Emulator");
116        }
117    }
118}
119
120/// Represents the test device. `destroy()` MUST be called explicitly to remove the device.
121/// The device will be removed asynchronously so the caller cannot rely on synchronous
122/// execution of destroy() to know about device removal. Instead, the caller should watch for the
123/// device path to be removed.
124struct TestDevice {
125    #[cfg(test)]
126    dev_directory: DirectoryProxy,
127    controller: ControllerProxy,
128    emulator: EmulatorProxy,
129}
130
131impl TestDevice {
132    /// Creates a new device as a child of the emulator controller device
133    async fn create(dev_directory: DirectoryProxy) -> Result<TestDevice, Error> {
134        // 0x30 => fuchsia.platform.BIND_PLATFORM_DEV_DID.BT_HCI_EMULATOR
135        let emulator_device_path: &str = "sys/platform/bt-hci-emulator";
136        let virtual_controller_device_path: String =
137            emulator_device_path.to_owned() + "/bt_hci_virtual";
138
139        let controller = device_watcher::recursive_wait_and_open::<VirtualControllerMarker>(
140            &dev_directory,
141            virtual_controller_device_path.as_str(),
142        )
143        .await
144        .with_context(|| format!("failed to open {}", virtual_controller_device_path))?;
145
146        let name = controller
147            .create_emulator()
148            .map_err(Error::from)
149            .on_timeout(WATCH_TIMEOUT.after_now(), || {
150                Err(format_err!("timed out waiting for emulator to create test device"))
151            })
152            .await?
153            .map_err(zx::Status::err_from_raw)?
154            .ok_or_else(|| {
155                format_err!("name absent from EmulatorController::Create FIDL response")
156            })?;
157
158        let emulator_dir = fuchsia_fs::directory::open_directory_async(
159            &dev_directory,
160            EMULATOR_DEVICE_DIR,
161            fuchsia_fs::Flags::empty(),
162        )?;
163
164        // Wait until a bt-emulator device gets published under our test device.
165        let directory = device_watcher::wait_for_device_with(
166            &emulator_dir,
167            |device_watcher::DeviceInfo { filename, topological_path }| {
168                is_emulator_node_path(&topological_path, emulator_device_path, &name).then(|| {
169                    fuchsia_fs::directory::open_directory_async(
170                        &emulator_dir,
171                        filename,
172                        fuchsia_fs::Flags::empty(),
173                    )
174                })
175            },
176        )
177        .on_timeout(WATCH_TIMEOUT, || Err(format_err!("timed out waiting for device to appear")))
178        .await??;
179
180        let controller = fuchsia_component::client::connect_to_named_protocol_at_dir_root::<
181            ControllerMarker,
182        >(&directory, fidl_fuchsia_device_fs::DEVICE_CONTROLLER_NAME)?;
183        let emulator = fuchsia_component::client::connect_to_named_protocol_at_dir_root::<
184            EmulatorMarker,
185        >(&directory, fidl_fuchsia_device_fs::DEVICE_PROTOCOL_NAME)?;
186
187        Ok(Self {
188            #[cfg(test)]
189            dev_directory,
190            controller,
191            emulator,
192        })
193    }
194
195    /// Sends the test device a destroy message which will unbind the driver.
196    /// This will wait for the test device to be unpublished from devfs.
197    pub async fn destroy_and_wait(&mut self) -> Result<(), Error> {
198        let () = self.controller.schedule_unbind().await?.map_err(zx::Status::err_from_raw)?;
199        let _: (zx::Signals, zx::Signals) = futures::future::try_join(
200            self.controller.as_channel().on_closed(),
201            self.emulator.as_channel().on_closed(),
202        )
203        .await?;
204        Ok(())
205    }
206
207    pub async fn get_topological_path(&self) -> Result<String, Error> {
208        self.controller
209            .get_topological_path()
210            .await
211            .context("get topological path transport")?
212            .map_err(zx::Status::err_from_raw)
213            .context("get topological path")
214    }
215}
216
217#[cfg(test)]
218mod tests {
219    use super::*;
220    use fidl_fuchsia_driver_test as fdt;
221    use fuchsia_component_test::RealmBuilder;
222    use fuchsia_driver_test::{DriverTestRealmBuilder, DriverTestRealmInstance};
223
224    #[fuchsia::test]
225    fn emulator_node_path_matches_whole_component() {
226        const DEV: &str = "sys/platform/bt-hci-emulator";
227        assert!(is_emulator_node_path(
228            "/dev/sys/platform/bt-hci-emulator/emulator-1",
229            DEV,
230            "emulator-1"
231        ));
232        assert!(!is_emulator_node_path(
233            "/dev/sys/platform/bt-hci-emulator/emulator-10",
234            DEV,
235            "emulator-1"
236        ));
237        assert!(!is_emulator_node_path(
238            "/dev/sys/platform/bt-hci-emulator/emulator",
239            DEV,
240            "emulator-1"
241        ));
242        assert!(!is_emulator_node_path(
243            "/dev/sys/platform/bt-hci-emulator/emulator-1/x",
244            DEV,
245            "emulator-1"
246        ));
247        assert!(!is_emulator_node_path("/dev/sys/platform/other/emulator-1", DEV, "emulator-1"));
248    }
249
250    fn default_settings() -> EmulatorSettings {
251        EmulatorSettings {
252            address: None,
253            hci_config: None,
254            extended_advertising: None,
255            acl_buffer_settings: None,
256            le_acl_buffer_settings: None,
257            ..Default::default()
258        }
259    }
260
261    #[fuchsia::test]
262    async fn test_publish_lifecycle() {
263        // We need to resolve our test component manually. Eventually component framework could provide
264        // an introspection way of resolving your own component.
265        // This isn't exactly correct because if the test is running in ctf, the root package will not
266        // be called "hci-emulator-client-tests".
267        let resolved = {
268            let client = fuchsia_component::client::connect_to_protocol_at_path::<
269                fidl_fuchsia_component_resolution::ResolverMarker,
270            >("/svc/fuchsia.component.resolution.Resolver-hermetic")
271            .unwrap();
272            client
273            .resolve(
274                "fuchsia-pkg://fuchsia.com/hci-emulator-client-tests#meta/hci-emulator-client-tests.cm",
275            )
276            .await
277            .unwrap()
278            .expect("Failed to resolve root component")
279        };
280
281        // We use these watchers to verify the addition and removal of these devices as tied to the
282        // lifetime of the Emulator instance we create below.
283        let emul_dev: EmulatorProxy;
284        let realm = RealmBuilder::new().await.expect("realm builder");
285        let _: &RealmBuilder =
286            realm.driver_test_realm_setup().await.expect("driver test realm setup");
287        let realm = realm.build().await.expect("failed to build realm");
288        let args = fdt::RealmArgs {
289            root_driver: Some("fuchsia-boot:///platform-bus#meta/platform-bus.cm".to_string()),
290            software_devices: Some(vec![fidl_fuchsia_driver_test::SoftwareDevice {
291                device_name: "bt-hci-emulator".to_string(),
292                device_id: 48,
293            }]),
294            test_component: Some(resolved),
295            ..Default::default()
296        };
297        realm.driver_test_realm_start(args).await.expect("driver test realm start");
298
299        let dev_dir = realm.driver_test_realm_connect_to_dev().unwrap();
300        let mut fake_dev = Emulator::create(dev_dir).await.expect("Failed to construct Emulator");
301        let dev = fake_dev.dev.as_ref().expect("emulator device exists");
302        let topo = dev
303            .get_topological_path()
304            .await
305            .expect("Failed to obtain topological path for Emulator");
306        let TestDevice { dev_directory, controller: _, emulator: _ } = dev;
307
308        // A bt-emulator device should already exist by now.
309        let emulator_dir = fuchsia_fs::directory::open_directory_async(
310            &dev_directory,
311            EMULATOR_DEVICE_DIR,
312            fuchsia_fs::Flags::empty(),
313        )
314        .expect("open emulator directory");
315        emul_dev = device_watcher::wait_for_device_with(
316            &emulator_dir,
317            |device_watcher::DeviceInfo { filename, topological_path }| {
318                (topological_path == topo).then(|| {
319                    fuchsia_component::client::connect_to_named_protocol_at_dir_root::<
320                        EmulatorMarker,
321                    >(&emulator_dir, filename)
322                    .expect("failed to connect to device")
323                })
324            },
325        )
326        .on_timeout(WATCH_TIMEOUT, || panic!("timed out waiting for device to appear"))
327        .await
328        .expect("failed to watch for device");
329
330        // Send a publish message to the device. This call should succeed and result in a new
331        // bt-hci device.
332        let () = fake_dev
333            .publish(default_settings())
334            .await
335            .expect("Failed to send Publish message to emulator device");
336
337        // Once a device is published, it should not be possible to publish again while the
338        // Emulator is open.
339        let dev = fake_dev.dev.as_ref().expect("emulator device exists");
340        let result = dev
341            .emulator
342            .publish(&default_settings())
343            .await
344            .expect("Failed to send second Publish message to emulator device");
345        assert_eq!(Err(EmulatorError::HciAlreadyPublished), result);
346
347        fake_dev.destroy_and_wait().await.expect("Expected test device to be removed");
348
349        // Emulator should be destroyed when `fake_dev` gets dropped
350        let _ = emul_dev
351            .as_channel()
352            .on_closed()
353            .on_timeout(WATCH_TIMEOUT, || panic!("timed out waiting for device to close"))
354            .await
355            .expect("on closed");
356    }
357}