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