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