| // Copyright 2018 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| use { |
| crate::{ |
| constants::{EMULATOR_DEVICE_DIR, EMULATOR_DRIVER_PATH, HOST_DEVICE_DIR}, |
| device_watcher::{DeviceFile, DeviceWatcher, WatchFilter}, |
| util::open_rdwr, |
| }, |
| anyhow::{format_err, Context, Error}, |
| fidl_fuchsia_bluetooth_test::{EmulatorSettings, HciEmulatorProxy}, |
| fidl_fuchsia_device::ControllerProxy, |
| fidl_fuchsia_device_test::{DeviceProxy, RootDeviceProxy, CONTROL_DEVICE, MAX_DEVICE_NAME_LEN}, |
| fidl_fuchsia_hardware_bluetooth::EmulatorProxy, |
| fuchsia_async::{self as fasync, DurationExt, TimeoutExt}, |
| fuchsia_syslog::fx_log_err, |
| fuchsia_zircon::{self as zx, DurationNum}, |
| futures::TryFutureExt, |
| std::{fs::File, path::PathBuf}, |
| }; |
| |
| fn watch_timeout() -> zx::Duration { |
| zx::Duration::from_seconds(10) |
| } |
| |
| /// Represents a bt-hci device emulator. Instances of this type can be used manage the |
| /// bt-hci-emulator driver within the test device hierarchy. The associated driver instance gets |
| /// unbound and all bt-hci and bt-emulator device instances destroyed when |
| /// `destroy_and_wait()` resolves successfully. |
| /// `destroy_and_wait()` MUST be called for proper clean up of the emulator device. |
| pub struct Emulator { |
| /// This will have a value when the emulator is instantiated and will be reset to None |
| /// in `destroy_and_wait()`. This is so the destructor can assert that the TestDevice has been |
| /// destroyed. |
| dev: Option<TestDevice>, |
| emulator: HciEmulatorProxy, |
| } |
| |
| impl Emulator { |
| /// Returns the default settings. |
| // TODO(armansito): Consider defining a library type for EmulatorSettings. |
| pub fn default_settings() -> EmulatorSettings { |
| EmulatorSettings { |
| address: None, |
| hci_config: None, |
| extended_advertising: None, |
| acl_buffer_settings: None, |
| le_acl_buffer_settings: None, |
| } |
| } |
| |
| /// Publish a new bt-emulator device and return a handle to it. No corresponding bt-hci device |
| /// will be published; to do so it must be explicitly configured and created with a call to |
| /// `publish()` |
| pub async fn create(name: &str) -> Result<Emulator, Error> { |
| let dev = TestDevice::create(name) |
| .await |
| .context(format!("Error creating hci-emulator test device '{}'", name))?; |
| let emulator = dev |
| .bind() |
| .await |
| .context(format!("Error binding hci-emulator test device '{}'", name))?; |
| Ok(Emulator { dev: Some(dev), emulator: emulator }) |
| } |
| |
| /// Publish a bt-emulator and a bt-hci device using the default emulator settings. |
| pub async fn create_and_publish(name: &str) -> Result<Emulator, Error> { |
| let fake_dev = Emulator::create(name).await?; |
| fake_dev.publish(Self::default_settings()).await?; |
| Ok(fake_dev) |
| } |
| |
| /// Sends a publish message to the emulator. This is a convenience method that internally |
| /// handles the FIDL binding error. |
| pub async fn publish(&self, settings: EmulatorSettings) -> Result<(), Error> { |
| let result = self.emulator().publish(settings).await?; |
| result.map_err(|e| format_err!("failed to publish bt-hci device: {:#?}", e)) |
| } |
| |
| /// Sends a publish message emulator and returns a Future that resolves when a bt-host device is |
| /// published. Note that this requires the bt-host driver to be installed. On success, returns a |
| /// `DeviceFile` that represents the bt-host device. |
| pub async fn publish_and_wait_for_host( |
| &self, |
| settings: EmulatorSettings, |
| ) -> Result<DeviceFile, Error> { |
| let mut watcher = DeviceWatcher::new(HOST_DEVICE_DIR, watch_timeout()).await?; |
| let _ = self.publish(settings).await?; |
| let topo = PathBuf::from(fdio::device_get_topo_path(self.file())?); |
| watcher.watch_new(&topo, WatchFilter::AddedOrExisting).await |
| } |
| |
| /// Sends the test device a destroy message which will unbind the driver. |
| /// This will wait for the test device to be unpublished from devfs. |
| pub async fn destroy_and_wait(&mut self) -> Result<(), Error> { |
| let mut watcher = DeviceWatcher::new(CONTROL_DEVICE, watch_timeout()).await?; |
| let topo_path = PathBuf::from(fdio::device_get_topo_path(self.file())?); |
| let fake_dev = self.dev.take(); |
| fake_dev.expect("attempted to destroy an already destroyed emulator device").destroy()?; |
| watcher.watch_removed(&topo_path).await |
| } |
| |
| /// Returns a reference to the underlying file. |
| pub fn file(&self) -> &File { |
| &self.dev.as_ref().expect("emulator device accessed after it was destroyed!").0 |
| } |
| |
| /// Returns a reference to the fuchsia.bluetooth.test.HciEmulator protocol proxy. |
| pub fn emulator(&self) -> &HciEmulatorProxy { |
| &self.emulator |
| } |
| } |
| |
| impl Drop for Emulator { |
| fn drop(&mut self) { |
| if self.dev.is_some() { |
| fx_log_err!("Did not call destroy() on Emulator"); |
| } |
| } |
| } |
| |
| // Represents the test device. `destroy()` MUST be called explicitly to remove the device. |
| // The device will be removed asynchronously so the caller cannot rely on synchronous |
| // execution of destroy() to know about device removal. Instead, the caller should watch for the |
| // device path to be removed. |
| struct TestDevice(File); |
| |
| impl TestDevice { |
| // Creates a new device as a child of the root test device. This device will act as the parent |
| // of our fake HCI device. If successful, `name` will act as the final fragment of the device |
| // path, for example "/dev/test/test/{name}". |
| async fn create(name: &str) -> Result<TestDevice, Error> { |
| if name.len() > (MAX_DEVICE_NAME_LEN as usize) { |
| return Err(format_err!( |
| "Device name '{}' too long (must be {} or fewer chars)", |
| name, |
| MAX_DEVICE_NAME_LEN |
| )); |
| } |
| |
| // Connect to the test control device and obtain a channel to the RootDevice capability. |
| let control_dev = |
| open_rdwr(CONTROL_DEVICE).context(format!("Error opening file {}", CONTROL_DEVICE))?; |
| let root_device = RootDeviceProxy::new(fasync::Channel::from_channel( |
| fdio::clone_channel(&control_dev)?, |
| )?); |
| |
| let (local, remote) = zx::Channel::create()?; |
| |
| // Create a device with the requested name. |
| let (status, _) = root_device |
| .create_device(name, Some(remote)) |
| .map_err(Error::from) |
| .on_timeout(10.seconds().after_now(), || { |
| Err(format_err!("timed out waiting to create bt-hci-emulator device {}", name)) |
| }) |
| .await?; |
| zx::Status::ok(status).context(format!("Error creating test device '{}'", name))?; |
| |
| Ok(TestDevice(fdio::create_fd(zx::Handle::from(local))?)) |
| } |
| |
| // Send the test device a destroy message which will unbind the driver. |
| fn destroy(&mut self) -> Result<(), Error> { |
| let channel = fdio::clone_channel(&self.0)?; |
| let device = DeviceProxy::new(fasync::Channel::from_channel(channel)?); |
| Ok(device.destroy()?) |
| } |
| |
| // Bind the bt-hci-emulator driver and obtain the HciEmulator protocol channel. |
| async fn bind(&self) -> Result<HciEmulatorProxy, Error> { |
| let channel = fdio::clone_channel(&self.0)?; |
| let controller = ControllerProxy::new(fasync::Channel::from_channel(channel)?); |
| |
| let _res = controller |
| .bind(EMULATOR_DRIVER_PATH) |
| .map_err(Error::from) |
| .on_timeout(10.seconds().after_now(), || { |
| Err(format_err!( |
| "timed out waiting for emulator to bind bt-hci-fake device {:?}", |
| self.0 |
| )) |
| }) |
| .await?; |
| |
| // Wait until a bt-emulator device gets published under our test device. |
| let topo_path = PathBuf::from(fdio::device_get_topo_path(&self.0)?); |
| let mut watcher = DeviceWatcher::new(EMULATOR_DEVICE_DIR, watch_timeout()).await?; |
| let emulator_dev = watcher.watch_new(&topo_path, WatchFilter::AddedOrExisting).await?; |
| |
| // Connect to the bt-emulator device. |
| let channel = fdio::clone_channel(emulator_dev.file())?; |
| let emulator = EmulatorProxy::new(fasync::Channel::from_channel(channel)?); |
| |
| // Open a HciEmulator protocol channel. |
| let (proxy, remote) = zx::Channel::create()?; |
| emulator.open(remote)?; |
| Ok(HciEmulatorProxy::new(fasync::Channel::from_channel(proxy)?)) |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use {super::*, crate::constants::HCI_DEVICE_DIR, fidl_fuchsia_bluetooth_test::EmulatorError}; |
| |
| fn default_settings() -> EmulatorSettings { |
| EmulatorSettings { |
| address: None, |
| hci_config: None, |
| extended_advertising: None, |
| acl_buffer_settings: None, |
| le_acl_buffer_settings: None, |
| } |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| #[ignore] // TODO(35077) Re-enable once test flake is resolved |
| async fn test_publish_lifecycle() { |
| // We use these watchers to verify the addition and removal of these devices as tied to the |
| // lifetime of the Emulator instance we create below. |
| let mut hci_watcher: DeviceWatcher; |
| let mut emul_watcher: DeviceWatcher; |
| let hci_dev: DeviceFile; |
| let emul_dev: DeviceFile; |
| |
| { |
| let mut fake_dev = |
| Emulator::create("publish-test-0").await.expect("Failed to construct Emulator"); |
| let topo_path = fdio::device_get_topo_path(&fake_dev.file()) |
| .expect("Failed to obtain topological path for Emulator"); |
| let topo_path = PathBuf::from(topo_path); |
| |
| // A bt-emulator device should already exist by now. |
| emul_watcher = DeviceWatcher::new(EMULATOR_DEVICE_DIR, watch_timeout()) |
| .await |
| .expect("Failed to create bt-emulator device watcher"); |
| emul_dev = emul_watcher |
| .watch_existing(&topo_path) |
| .await |
| .expect("Expected bt-emulator device to have been published"); |
| |
| // Send a publish message to the device. This call should succeed and result in a new |
| // bt-hci device. (Note: it is important for `hci_watcher` to get constructed here since |
| // our expectation is based on the `ADD_FILE` event). |
| hci_watcher = DeviceWatcher::new(HCI_DEVICE_DIR, watch_timeout()) |
| .await |
| .expect("Failed to create bt-hci device watcher"); |
| let _ = fake_dev |
| .publish(default_settings()) |
| .await |
| .expect("Failed to send Publish message to emulator device"); |
| hci_dev = hci_watcher |
| .watch_new(&topo_path, WatchFilter::AddedOnly) |
| .await |
| .expect("Expected a new bt-hci device"); |
| |
| // Once a device is published, it should not be possible to publish again while the |
| // HciEmulator channel is open. |
| let result = fake_dev |
| .emulator() |
| .publish(default_settings()) |
| .await |
| .expect("Failed to send second Publish message to emulator device"); |
| assert_eq!(Err(EmulatorError::HciAlreadyPublished), result); |
| |
| fake_dev.destroy_and_wait().await.expect("Expected test device to be removed"); |
| } |
| |
| // Both devices should be destroyed when `fake_dev` gets dropped. |
| let _ = hci_watcher |
| .watch_removed(hci_dev.path()) |
| .await |
| .expect("Expected bt-hci device to get removed"); |
| let _ = emul_watcher |
| .watch_removed(emul_dev.path()) |
| .await |
| .expect("Expected bt-emulator device to get removed"); |
| } |
| } |