1use anyhow::{format_err, Context as _, Error};
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, HCI_DEVICE_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
22pub struct Emulator {
28 dev: Option<TestDevice>,
32}
33
34impl Emulator {
35 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 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 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 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 pub async fn publish_and_wait_for_device_path(
80 &self,
81 settings: EmulatorSettings,
82 ) -> Result<String, Error> {
83 let () = self.publish(settings).await?;
84 let dev = self.dev.as_ref().expect("emulator device accessed after it was destroyed!");
85 let topo = dev.get_topological_path().await?;
86 let TestDevice { dev_directory, controller: _, emulator: _ } = dev;
87 let hci_dir = fuchsia_fs::directory::open_directory_async(
88 dev_directory,
89 HCI_DEVICE_DIR,
90 fuchsia_fs::Flags::empty(),
91 )?;
92
93 let hci_device_path = device_watcher::wait_for_device_with(
94 &hci_dir,
95 |device_watcher::DeviceInfo { filename, topological_path }| {
96 topological_path.starts_with(&topo).then(|| filename.to_string())
97 },
98 )
99 .on_timeout(WATCH_TIMEOUT, || Err(format_err!("timed out waiting for device to appear")))
100 .await?;
101
102 Ok(hci_device_path)
103 }
104
105 pub async fn destroy_and_wait(&mut self) -> Result<(), Error> {
108 self.dev
109 .take()
110 .expect("attempted to destroy an already destroyed emulator device")
111 .destroy_and_wait()
112 .await
113 }
114
115 pub async fn get_topological_path(&self) -> Result<String, Error> {
116 let dev = self.dev.as_ref().expect("emulator device accessed after it was destroyed!");
117 dev.get_topological_path().await
118 }
119
120 pub fn emulator(&self) -> &EmulatorProxy {
121 &self.dev.as_ref().unwrap().emulator
122 }
123}
124
125impl Drop for Emulator {
126 fn drop(&mut self) {
127 if self.dev.is_some() {
128 error!("Did not call destroy() on Emulator");
129 }
130 }
131}
132
133struct TestDevice {
138 dev_directory: DirectoryProxy,
139 controller: ControllerProxy,
140 emulator: EmulatorProxy,
141}
142
143impl TestDevice {
144 async fn create(dev_directory: DirectoryProxy) -> Result<TestDevice, Error> {
146 let emulator_device_path: &str = "sys/platform/bt-hci-emulator";
148 let virtual_controller_device_path: String =
149 emulator_device_path.to_owned() + "/bt_hci_virtual";
150
151 let controller = device_watcher::recursive_wait_and_open::<VirtualControllerMarker>(
152 &dev_directory,
153 virtual_controller_device_path.as_str(),
154 )
155 .await
156 .with_context(|| format!("failed to open {}", virtual_controller_device_path))?;
157
158 let name = controller
159 .create_emulator()
160 .map_err(Error::from)
161 .on_timeout(WATCH_TIMEOUT.after_now(), || {
162 Err(format_err!("timed out waiting for emulator to create test device"))
163 })
164 .await?
165 .map_err(zx::Status::from_raw)?
166 .ok_or_else(|| {
167 format_err!("name absent from EmulatorController::Create FIDL response")
168 })?;
169
170 let emulator_dir = fuchsia_fs::directory::open_directory_async(
171 &dev_directory,
172 EMULATOR_DEVICE_DIR,
173 fuchsia_fs::Flags::empty(),
174 )?;
175
176 let directory = device_watcher::wait_for_device_with(
178 &emulator_dir,
179 |device_watcher::DeviceInfo { filename, topological_path }| {
180 let topological_path = topological_path.strip_prefix(DEV_DIR)?;
181 let topological_path = topological_path.strip_prefix('/')?;
182 let topological_path = topological_path.strip_prefix(emulator_device_path)?;
183 let topological_path = topological_path.strip_prefix('/')?;
184 let topological_path = topological_path.strip_prefix(&name)?;
185 let _: &str = topological_path;
186 Some(fuchsia_fs::directory::open_directory_async(
187 &emulator_dir,
188 filename,
189 fuchsia_fs::Flags::empty(),
190 ))
191 },
192 )
193 .on_timeout(WATCH_TIMEOUT, || Err(format_err!("timed out waiting for device to appear")))
194 .await??;
195
196 let controller = fuchsia_component::client::connect_to_named_protocol_at_dir_root::<
197 ControllerMarker,
198 >(&directory, fidl_fuchsia_device_fs::DEVICE_CONTROLLER_NAME)?;
199 let emulator = fuchsia_component::client::connect_to_named_protocol_at_dir_root::<
200 EmulatorMarker,
201 >(&directory, fidl_fuchsia_device_fs::DEVICE_PROTOCOL_NAME)?;
202
203 Ok(Self { dev_directory, controller, emulator })
204 }
205
206 pub async fn destroy_and_wait(&mut self) -> Result<(), Error> {
209 let () = self.controller.schedule_unbind().await?.map_err(zx::Status::from_raw)?;
210 let _: (zx::Signals, zx::Signals) = futures::future::try_join(
211 self.controller.as_channel().on_closed(),
212 self.emulator.as_channel().on_closed(),
213 )
214 .await?;
215 Ok(())
216 }
217
218 pub async fn get_topological_path(&self) -> Result<String, Error> {
219 self.controller
220 .get_topological_path()
221 .await
222 .context("get topological path transport")?
223 .map_err(zx::Status::from_raw)
224 .context("get topological path")
225 }
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231 use fidl_fuchsia_driver_test as fdt;
232 use fuchsia_component_test::RealmBuilder;
233 use fuchsia_driver_test::{DriverTestRealmBuilder, DriverTestRealmInstance};
234
235 fn default_settings() -> EmulatorSettings {
236 EmulatorSettings {
237 address: None,
238 hci_config: None,
239 extended_advertising: None,
240 acl_buffer_settings: None,
241 le_acl_buffer_settings: None,
242 ..Default::default()
243 }
244 }
245
246 #[fuchsia::test]
247 async fn test_publish_lifecycle() {
248 let resolved = {
253 let client = fuchsia_component::client::connect_to_protocol_at_path::<
254 fidl_fuchsia_component_resolution::ResolverMarker,
255 >("/svc/fuchsia.component.resolution.Resolver-hermetic")
256 .unwrap();
257 client
258 .resolve(
259 "fuchsia-pkg://fuchsia.com/hci-emulator-client-tests#meta/hci-emulator-client-tests.cm",
260 )
261 .await
262 .unwrap()
263 .expect("Failed to resolve root component")
264 };
265
266 let emul_dev: EmulatorProxy;
269 let realm = RealmBuilder::new().await.expect("realm builder");
270 let _: &RealmBuilder =
271 realm.driver_test_realm_setup().await.expect("driver test realm setup");
272 let realm = realm.build().await.expect("failed to build realm");
273 let args = fdt::RealmArgs {
274 root_driver: Some("fuchsia-boot:///platform-bus#meta/platform-bus.cm".to_string()),
275 software_devices: Some(vec![fidl_fuchsia_driver_test::SoftwareDevice {
276 device_name: "bt-hci-emulator".to_string(),
277 device_id: 48,
278 }]),
279 test_component: Some(resolved),
280 ..Default::default()
281 };
282 realm.driver_test_realm_start(args).await.expect("driver test realm start");
283
284 let dev_dir = realm.driver_test_realm_connect_to_dev().unwrap();
285 let mut fake_dev = Emulator::create(dev_dir).await.expect("Failed to construct Emulator");
286 let dev = fake_dev.dev.as_ref().expect("emulator device exists");
287 let topo = dev
288 .get_topological_path()
289 .await
290 .expect("Failed to obtain topological path for Emulator");
291 let TestDevice { dev_directory, controller: _, emulator: _ } = dev;
292
293 let emulator_dir = fuchsia_fs::directory::open_directory_async(
295 &dev_directory,
296 EMULATOR_DEVICE_DIR,
297 fuchsia_fs::Flags::empty(),
298 )
299 .expect("open emulator directory");
300 emul_dev = device_watcher::wait_for_device_with(
301 &emulator_dir,
302 |device_watcher::DeviceInfo { filename, topological_path }| {
303 topological_path.starts_with(&topo).then(|| {
304 fuchsia_component::client::connect_to_named_protocol_at_dir_root::<
305 EmulatorMarker,
306 >(&emulator_dir, filename)
307 .expect("failed to connect to device")
308 })
309 },
310 )
311 .on_timeout(WATCH_TIMEOUT, || panic!("timed out waiting for device to appear"))
312 .await
313 .expect("failed to watch for device");
314
315 let () = fake_dev
318 .publish(default_settings())
319 .await
320 .expect("Failed to send Publish message to emulator device");
321
322 let dev = fake_dev.dev.as_ref().expect("emulator device exists");
325 let result = dev
326 .emulator
327 .publish(&default_settings())
328 .await
329 .expect("Failed to send second Publish message to emulator device");
330 assert_eq!(Err(EmulatorError::HciAlreadyPublished), result);
331
332 fake_dev.destroy_and_wait().await.expect("Expected test device to be removed");
333
334 let _ = emul_dev
336 .as_channel()
337 .on_closed()
338 .on_timeout(WATCH_TIMEOUT, || panic!("timed out waiting for device to close"))
339 .await
340 .expect("on closed");
341 }
342}