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