blob: 0d480707ab734607775f0d6a9b8fb00ab5dbb65d [file] [log] [blame]
// 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");
}
}