use anyhow::{format_err, Context as _, Error};
use fidl::endpoints::Proxy as _;
use fidl_fuchsia_device::{ControllerMarker, ControllerProxy};
use fidl_fuchsia_hardware_bluetooth::{
EmulatorError, EmulatorMarker, EmulatorProxy, EmulatorSettings, VirtualControllerMarker,
};
use fidl_fuchsia_io::DirectoryProxy;
use fuchsia_async::{DurationExt as _, TimeoutExt as _};
use fuchsia_bluetooth::constants::{DEV_DIR, HCI_DEVICE_DIR, INTEGRATION_TIMEOUT as WATCH_TIMEOUT};
use futures::TryFutureExt as _;
use tracing::error;
pub mod types;
const EMULATOR_DEVICE_DIR: &str = "class/bt-emulator";
pub struct Emulator {
dev: Option<TestDevice>,
}
impl Emulator {
pub fn default_settings() -> EmulatorSettings {
EmulatorSettings {
address: None,
hci_config: None,
extended_advertising: None,
acl_buffer_settings: None,
le_acl_buffer_settings: None,
..Default::default()
}
}
pub async fn create(dev_directory: DirectoryProxy) -> Result<Emulator, Error> {
let dev = TestDevice::create(dev_directory)
.await
.context(format!("Error creating test device"))?;
Ok(Emulator { dev: Some(dev) })
}
pub async fn create_and_publish(dev_directory: DirectoryProxy) -> Result<Emulator, Error> {
let fake_dev = Self::create(dev_directory).await?;
fake_dev.publish(Self::default_settings()).await?;
Ok(fake_dev)
}
pub async fn publish(&self, settings: EmulatorSettings) -> Result<(), Error> {
let dev = self.dev.as_ref().expect("emulator device accessed after it was destroyed!");
dev.emulator
.publish(&settings)
.await
.context("publish transport")?
.map_err(|e: EmulatorError| format_err!("failed to publish bt-hci device: {:#?}", e))
}
pub async fn publish_and_wait_for_device_path(
&self,
settings: EmulatorSettings,
) -> Result<String, Error> {
let () = self.publish(settings).await?;
let dev = self.dev.as_ref().expect("emulator device accessed after it was destroyed!");
let topo = dev.get_topological_path().await?;
let TestDevice { dev_directory, controller: _, emulator: _ } = dev;
let hci_dir = fuchsia_fs::directory::open_directory_async(
dev_directory,
HCI_DEVICE_DIR,
fuchsia_fs::Flags::empty(),
)?;
let hci_device_path = device_watcher::wait_for_device_with(
&hci_dir,
|device_watcher::DeviceInfo { filename, topological_path }| {
topological_path.starts_with(&topo).then(|| filename.to_string())
},
)
.on_timeout(WATCH_TIMEOUT, || Err(format_err!("timed out waiting for device to appear")))
.await?;
Ok(hci_device_path)
}
pub async fn destroy_and_wait(&mut self) -> Result<(), Error> {
self.dev
.take()
.expect("attempted to destroy an already destroyed emulator device")
.destroy_and_wait()
.await
}
pub async fn get_topological_path(&self) -> Result<String, Error> {
let dev = self.dev.as_ref().expect("emulator device accessed after it was destroyed!");
dev.get_topological_path().await
}
pub fn emulator(&self) -> &EmulatorProxy {
&self.dev.as_ref().unwrap().emulator
}
}
impl Drop for Emulator {
fn drop(&mut self) {
if self.dev.is_some() {
error!("Did not call destroy() on Emulator");
}
}
}
struct TestDevice {
dev_directory: DirectoryProxy,
controller: ControllerProxy,
emulator: EmulatorProxy,
}
impl TestDevice {
async fn create(dev_directory: DirectoryProxy) -> Result<TestDevice, Error> {
let emulator_device_path: &str = "sys/platform/bt-hci-emulator";
let virtual_controller_device_path: String =
emulator_device_path.to_owned() + "/bt_hci_virtual";
let controller = device_watcher::recursive_wait_and_open::<VirtualControllerMarker>(
&dev_directory,
virtual_controller_device_path.as_str(),
)
.await
.with_context(|| format!("failed to open {}", virtual_controller_device_path))?;
let name = controller
.create_emulator()
.map_err(Error::from)
.on_timeout(WATCH_TIMEOUT.after_now(), || {
Err(format_err!("timed out waiting for emulator to create test device"))
})
.await?
.map_err(zx::Status::from_raw)?
.ok_or_else(|| {
format_err!("name absent from EmulatorController::Create FIDL response")
})?;
let emulator_dir = fuchsia_fs::directory::open_directory_async(
&dev_directory,
EMULATOR_DEVICE_DIR,
fuchsia_fs::Flags::empty(),
)?;
let directory = device_watcher::wait_for_device_with(
&emulator_dir,
|device_watcher::DeviceInfo { filename, topological_path }| {
let topological_path = topological_path.strip_prefix(DEV_DIR)?;
let topological_path = topological_path.strip_prefix('/')?;
let topological_path = topological_path.strip_prefix(emulator_device_path)?;
let topological_path = topological_path.strip_prefix('/')?;
let topological_path = topological_path.strip_prefix(&name)?;
let _: &str = topological_path;
Some(fuchsia_fs::directory::open_directory_async(
&emulator_dir,
filename,
fuchsia_fs::Flags::empty(),
))
},
)
.on_timeout(WATCH_TIMEOUT, || Err(format_err!("timed out waiting for device to appear")))
.await??;
let controller = fuchsia_component::client::connect_to_named_protocol_at_dir_root::<
ControllerMarker,
>(&directory, fidl_fuchsia_device_fs::DEVICE_CONTROLLER_NAME)?;
let emulator = fuchsia_component::client::connect_to_named_protocol_at_dir_root::<
EmulatorMarker,
>(&directory, fidl_fuchsia_device_fs::DEVICE_PROTOCOL_NAME)?;
Ok(Self { dev_directory, controller, emulator })
}
pub async fn destroy_and_wait(&mut self) -> Result<(), Error> {
let () = self.controller.schedule_unbind().await?.map_err(zx::Status::from_raw)?;
let _: (zx::Signals, zx::Signals) = futures::future::try_join(
self.controller.as_channel().on_closed(),
self.emulator.as_channel().on_closed(),
)
.await?;
Ok(())
}
pub async fn get_topological_path(&self) -> Result<String, Error> {
self.controller
.get_topological_path()
.await
.context("get topological path transport")?
.map_err(zx::Status::from_raw)
.context("get topological path")
}
}
#[cfg(test)]
mod tests {
use super::*;
use fidl_fuchsia_driver_test as fdt;
use fuchsia_component_test::RealmBuilder;
use fuchsia_driver_test::{DriverTestRealmBuilder, DriverTestRealmInstance};
fn default_settings() -> EmulatorSettings {
EmulatorSettings {
address: None,
hci_config: None,
extended_advertising: None,
acl_buffer_settings: None,
le_acl_buffer_settings: None,
..Default::default()
}
}
#[fuchsia::test]
async fn test_publish_lifecycle() {
let resolved = {
let client = fuchsia_component::client::connect_to_protocol_at_path::<
fidl_fuchsia_component_resolution::ResolverMarker,
>("/svc/fuchsia.component.resolution.Resolver-hermetic")
.unwrap();
client
.resolve(
"fuchsia-pkg://fuchsia.com/hci-emulator-client-tests#meta/hci-emulator-client-tests.cm",
)
.await
.unwrap()
.expect("Failed to resolve root component")
};
let emul_dev: EmulatorProxy;
let realm = RealmBuilder::new().await.expect("realm builder");
let _: &RealmBuilder =
realm.driver_test_realm_setup().await.expect("driver test realm setup");
let realm = realm.build().await.expect("failed to build realm");
let args = fdt::RealmArgs {
root_driver: Some("fuchsia-boot:///platform-bus#meta/platform-bus.cm".to_string()),
software_devices: Some(vec![fidl_fuchsia_driver_test::SoftwareDevice {
device_name: "bt-hci-emulator".to_string(),
device_id: 48,
}]),
test_component: Some(resolved),
..Default::default()
};
realm.driver_test_realm_start(args).await.expect("driver test realm start");
let dev_dir = realm.driver_test_realm_connect_to_dev().unwrap();
let mut fake_dev = Emulator::create(dev_dir).await.expect("Failed to construct Emulator");
let dev = fake_dev.dev.as_ref().expect("emulator device exists");
let topo = dev
.get_topological_path()
.await
.expect("Failed to obtain topological path for Emulator");
let TestDevice { dev_directory, controller: _, emulator: _ } = dev;
let emulator_dir = fuchsia_fs::directory::open_directory_async(
&dev_directory,
EMULATOR_DEVICE_DIR,
fuchsia_fs::Flags::empty(),
)
.expect("open emulator directory");
emul_dev = device_watcher::wait_for_device_with(
&emulator_dir,
|device_watcher::DeviceInfo { filename, topological_path }| {
topological_path.starts_with(&topo).then(|| {
fuchsia_component::client::connect_to_named_protocol_at_dir_root::<
EmulatorMarker,
>(&emulator_dir, filename)
.expect("failed to connect to device")
})
},
)
.on_timeout(WATCH_TIMEOUT, || panic!("timed out waiting for device to appear"))
.await
.expect("failed to watch for device");
let () = fake_dev
.publish(default_settings())
.await
.expect("Failed to send Publish message to emulator device");
let dev = fake_dev.dev.as_ref().expect("emulator device exists");
let result = 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");
let _ = emul_dev
.as_channel()
.on_closed()
.on_timeout(WATCH_TIMEOUT, || panic!("timed out waiting for device to close"))
.await
.expect("on closed");
}
}