Skip to main content

bt_test_harness/
host_realm.rs

1// Copyright 2024 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use crate::emulator::EMULATOR_ROOT_DRIVER_URL;
6use crate::host_realm::mpsc::Receiver;
7use anyhow::{Error, format_err};
8use cm_rust::push_box;
9use fidl::endpoints::ClientEnd;
10use fidl_fuchsia_bluetooth_host::{HostMarker, ReceiverMarker, ReceiverRequestStream};
11use fidl_fuchsia_component::{CreateChildArgs, RealmMarker, RealmProxy};
12use fidl_fuchsia_component_decl::{
13    Child, CollectionRef, ConfigOverride, ConfigSingleValue, ConfigValue, Durability, StartupMode,
14};
15use fidl_fuchsia_driver_test as fdt;
16use fidl_fuchsia_hardware_bluetooth as fhbt;
17use fidl_fuchsia_io as fio;
18use fidl_fuchsia_logger::LogSinkMarker;
19use fuchsia_async::TimeoutExt as _;
20use fuchsia_bluetooth::constants::{
21    BT_HOST, BT_HOST_COLLECTION, BT_HOST_URL, BT_SERVICE_DIR, INTEGRATION_TIMEOUT as WATCH_TIMEOUT,
22};
23use fuchsia_component::client::{Service, ServiceInstanceStream};
24use fuchsia_component::server::ServiceFs;
25use fuchsia_component_test::{
26    Capability, ChildOptions, LocalComponentHandles, RealmBuilder, RealmInstance, Ref, Route,
27    ScopedInstance,
28};
29use fuchsia_driver_test::{DriverTestRealmBuilder, DriverTestRealmInstance};
30use futures::channel::mpsc;
31use futures::{SinkExt, StreamExt, TryFutureExt, TryStreamExt};
32use std::sync::Arc;
33use std::sync::atomic::{AtomicUsize, Ordering};
34
35mod constants {
36    pub mod receiver {
37        pub const MONIKER: &str = "receiver";
38    }
39}
40
41pub async fn add_host_routes(
42    builder: &RealmBuilder,
43    to: impl Into<fuchsia_component_test::Ref> + Clone,
44) -> Result<(), Error> {
45    // Route config capabilities from root to bt-init
46    builder
47        .add_capability(cm_rust::CapabilityDecl::Config(cm_rust::ConfigurationDecl {
48            name: "fuchsia.bluetooth.HciCommandTimeout".parse()?,
49            value: cm_rust::ConfigValue::Single(cm_rust::ConfigSingleValue::Uint16(10)),
50        }))
51        .await?;
52    builder
53        .add_capability(cm_rust::CapabilityDecl::Config(cm_rust::ConfigurationDecl {
54            name: "fuchsia.bluetooth.LegacyPairing".parse()?,
55            value: cm_rust::ConfigValue::Single(cm_rust::ConfigSingleValue::Bool(false)),
56        }))
57        .await?;
58    builder
59        .add_capability(cm_rust::CapabilityDecl::Config(cm_rust::ConfigurationDecl {
60            name: "fuchsia.bluetooth.ScoOffloadPathIndex".parse()?,
61            value: cm_rust::ConfigValue::Single(cm_rust::ConfigSingleValue::Uint8(6)),
62        }))
63        .await?;
64    builder
65        .add_capability(cm_rust::CapabilityDecl::Config(cm_rust::ConfigurationDecl {
66            name: "fuchsia.bluetooth.OverrideVendorCapabilitiesVersion".parse()?,
67            value: cm_rust::ConfigValue::Single(cm_rust::ConfigSingleValue::Uint16(0)),
68        }))
69        .await?;
70    builder
71        .add_capability(cm_rust::CapabilityDecl::Config(cm_rust::ConfigurationDecl {
72            name: "fuchsia.power.SuspendEnabled".parse()?,
73            value: cm_rust::ConfigValue::Single(cm_rust::ConfigSingleValue::Bool(false)),
74        }))
75        .await?;
76
77    let le_configs = vec![
78        "fuchsia.bluetooth.LeSlowAdvIntervalMin",
79        "fuchsia.bluetooth.LeSlowAdvIntervalMax",
80        "fuchsia.bluetooth.LeSlowAdvMaxTxPower",
81        "fuchsia.bluetooth.LeFastAdvIntervalMin",
82        "fuchsia.bluetooth.LeFastAdvIntervalMax",
83        "fuchsia.bluetooth.LeFastAdvMaxTxPower",
84        "fuchsia.bluetooth.LeVeryFastAdvIntervalMin",
85        "fuchsia.bluetooth.LeVeryFastAdvIntervalMax",
86        "fuchsia.bluetooth.LeVeryFastAdvMaxTxPower",
87        "fuchsia.bluetooth.LeActiveScanInterval",
88        "fuchsia.bluetooth.LeActiveScanWindow",
89        "fuchsia.bluetooth.LeBatchedScanningEnabled",
90        "fuchsia.bluetooth.LeScanBatchMaxReadDelaySeconds",
91        "fuchsia.bluetooth.LeScanOffloadFiltersEnabled",
92    ];
93
94    builder
95        .add_capability(cm_rust::CapabilityDecl::Config(cm_rust::ConfigurationDecl {
96            name: "fuchsia.bluetooth.LeSlowAdvIntervalMin".parse()?,
97            value: cm_rust::ConfigValue::Single(cm_rust::ConfigSingleValue::Uint16(0)),
98        }))
99        .await?;
100    builder
101        .add_capability(cm_rust::CapabilityDecl::Config(cm_rust::ConfigurationDecl {
102            name: "fuchsia.bluetooth.LeSlowAdvIntervalMax".parse()?,
103            value: cm_rust::ConfigValue::Single(cm_rust::ConfigSingleValue::Uint16(0)),
104        }))
105        .await?;
106    builder
107        .add_capability(cm_rust::CapabilityDecl::Config(cm_rust::ConfigurationDecl {
108            name: "fuchsia.bluetooth.LeSlowAdvMaxTxPower".parse()?,
109            value: cm_rust::ConfigValue::Single(cm_rust::ConfigSingleValue::Int8(127)),
110        }))
111        .await?;
112    builder
113        .add_capability(cm_rust::CapabilityDecl::Config(cm_rust::ConfigurationDecl {
114            name: "fuchsia.bluetooth.LeFastAdvIntervalMin".parse()?,
115            value: cm_rust::ConfigValue::Single(cm_rust::ConfigSingleValue::Uint16(0)),
116        }))
117        .await?;
118    builder
119        .add_capability(cm_rust::CapabilityDecl::Config(cm_rust::ConfigurationDecl {
120            name: "fuchsia.bluetooth.LeFastAdvIntervalMax".parse()?,
121            value: cm_rust::ConfigValue::Single(cm_rust::ConfigSingleValue::Uint16(0)),
122        }))
123        .await?;
124    builder
125        .add_capability(cm_rust::CapabilityDecl::Config(cm_rust::ConfigurationDecl {
126            name: "fuchsia.bluetooth.LeFastAdvMaxTxPower".parse()?,
127            value: cm_rust::ConfigValue::Single(cm_rust::ConfigSingleValue::Int8(127)),
128        }))
129        .await?;
130    builder
131        .add_capability(cm_rust::CapabilityDecl::Config(cm_rust::ConfigurationDecl {
132            name: "fuchsia.bluetooth.LeVeryFastAdvIntervalMin".parse()?,
133            value: cm_rust::ConfigValue::Single(cm_rust::ConfigSingleValue::Uint16(0)),
134        }))
135        .await?;
136    builder
137        .add_capability(cm_rust::CapabilityDecl::Config(cm_rust::ConfigurationDecl {
138            name: "fuchsia.bluetooth.LeVeryFastAdvIntervalMax".parse()?,
139            value: cm_rust::ConfigValue::Single(cm_rust::ConfigSingleValue::Uint16(0)),
140        }))
141        .await?;
142    builder
143        .add_capability(cm_rust::CapabilityDecl::Config(cm_rust::ConfigurationDecl {
144            name: "fuchsia.bluetooth.LeVeryFastAdvMaxTxPower".parse()?,
145            value: cm_rust::ConfigValue::Single(cm_rust::ConfigSingleValue::Int8(127)),
146        }))
147        .await?;
148    builder
149        .add_capability(cm_rust::CapabilityDecl::Config(cm_rust::ConfigurationDecl {
150            name: "fuchsia.bluetooth.LeActiveScanInterval".parse()?,
151            value: cm_rust::ConfigValue::Single(cm_rust::ConfigSingleValue::Uint16(0)),
152        }))
153        .await?;
154    builder
155        .add_capability(cm_rust::CapabilityDecl::Config(cm_rust::ConfigurationDecl {
156            name: "fuchsia.bluetooth.LeActiveScanWindow".parse()?,
157            value: cm_rust::ConfigValue::Single(cm_rust::ConfigSingleValue::Uint16(0)),
158        }))
159        .await?;
160    builder
161        .add_capability(cm_rust::CapabilityDecl::Config(cm_rust::ConfigurationDecl {
162            name: "fuchsia.bluetooth.LeBatchedScanningEnabled".parse()?,
163            value: cm_rust::ConfigValue::Single(cm_rust::ConfigSingleValue::Bool(false)),
164        }))
165        .await?;
166    builder
167        .add_capability(cm_rust::CapabilityDecl::Config(cm_rust::ConfigurationDecl {
168            name: "fuchsia.bluetooth.LeScanBatchMaxReadDelaySeconds".parse()?,
169            value: cm_rust::ConfigValue::Single(cm_rust::ConfigSingleValue::Uint8(3)),
170        }))
171        .await?;
172    builder
173        .add_capability(cm_rust::CapabilityDecl::Config(cm_rust::ConfigurationDecl {
174            name: "fuchsia.bluetooth.LeScanOffloadFiltersEnabled".parse()?,
175            value: cm_rust::ConfigValue::Single(cm_rust::ConfigSingleValue::Bool(false)),
176        }))
177        .await?;
178
179    builder
180        .add_capability(cm_rust::CapabilityDecl::Dictionary(cm_rust::DictionaryDecl {
181            name: "bluetooth-le-config".parse()?,
182            source_path: None,
183        }))
184        .await?;
185
186    for config in le_configs {
187        builder
188            .add_route(
189                Route::new()
190                    .capability(Capability::configuration(config))
191                    .from(Ref::self_())
192                    .to(Ref::capability("bluetooth-le-config")),
193            )
194            .await?;
195    }
196
197    macro_rules! add_capability_route {
198        ($name:expr) => {
199            builder.add_route(
200                Route::new()
201                    .capability(Capability::configuration($name))
202                    .from(Ref::self_())
203                    .to(to.clone()),
204            )
205        };
206    }
207
208    add_capability_route!("fuchsia.bluetooth.HciCommandTimeout").await?;
209    add_capability_route!("fuchsia.bluetooth.LegacyPairing").await?;
210    add_capability_route!("fuchsia.bluetooth.OverrideVendorCapabilitiesVersion").await?;
211    add_capability_route!("fuchsia.bluetooth.ScoOffloadPathIndex").await?;
212    add_capability_route!("fuchsia.power.SuspendEnabled").await?;
213
214    builder
215        .add_route(
216            Route::new()
217                .capability(Capability::dictionary("bluetooth-le-config"))
218                .from(Ref::self_())
219                .to(to.clone()),
220        )
221        .await?;
222    Ok(())
223}
224
225async fn resolve_test_component(
226    test_components: impl IntoIterator<Item = impl AsRef<str>>,
227) -> Result<fidl_fuchsia_component_resolution::Component, Error> {
228    // We need to resolve our test component manually. Eventually component framework could provide
229    // an introspection way of resolving your own component.
230    let resolver = fuchsia_component::client::connect_to_protocol_at_path::<
231        fidl_fuchsia_component_resolution::ResolverMarker,
232    >("/svc/fuchsia.component.resolution.Resolver-hermetic")?;
233
234    let mut resolved_test_component = None;
235    let mut last_err = None;
236
237    for url in test_components {
238        let url_str = url.as_ref();
239        match resolver.resolve(url_str).await {
240            Ok(Ok(component)) => {
241                resolved_test_component = Some(component);
242                break;
243            }
244            Ok(Err(e)) => last_err = Some(format_err!("Failed to resolve {url_str}: {e:?}")),
245            Err(e) => last_err = Some(format_err!("FIDL error resolving {url_str}: {e:?}")),
246        }
247    }
248
249    resolved_test_component.ok_or_else(|| {
250        last_err.unwrap_or_else(|| format_err!("No test component candidate URLs provided"))
251    })
252}
253
254pub struct HostRealm {
255    realm: RealmInstance,
256    receiver: futures::lock::Mutex<Receiver<ClientEnd<HostMarker>>>,
257    service_watcher: futures::lock::Mutex<ServiceInstanceStream<fhbt::ServiceMarker>>,
258    next_host_id: AtomicUsize,
259}
260
261impl HostRealm {
262    pub async fn create(test_component: String) -> Result<Self, Error> {
263        Self::create_with_candidates([test_component]).await
264    }
265
266    /// Attempts to create a [`HostRealm`] by resolving the test component from a list of
267    /// candidate URLs, using the first URL that resolves successfully.
268    pub async fn create_with_candidates(
269        test_components: impl IntoIterator<Item = impl AsRef<str>>,
270    ) -> Result<Self, Error> {
271        let resolved_test_component = resolve_test_component(test_components).await?;
272
273        let builder = RealmBuilder::new().await?;
274        let _ = builder.driver_test_realm_setup().await?;
275
276        // Mock the fuchsia.bluetooth.host.Receiver API by creating a channel where the client end
277        // of the Host protocol can be extracted from |receiver|.
278        // Note: The word "receiver" is overloaded. One refers to the Receiver API, the other
279        // refers to the receiver end of the mpsc channel.
280        let (sender, receiver) = mpsc::channel(128);
281        let host_receiver = builder
282            .add_local_child(
283                constants::receiver::MONIKER,
284                move |handles| {
285                    let sender_clone = sender.clone();
286                    Box::pin(Self::fake_receiver_component(sender_clone, handles))
287                },
288                ChildOptions::new().eager(),
289            )
290            .await?;
291
292        // Create bt-host collection
293        let mut realm_decl = builder.get_realm_decl().await?;
294        push_box(
295            &mut realm_decl.collections,
296            cm_rust::CollectionDecl {
297                name: BT_HOST_COLLECTION.parse().unwrap(),
298                durability: Durability::SingleRun,
299                environment: None,
300                allowed_offers: cm_types::AllowedOffers::StaticAndDynamic,
301                allow_long_names: false,
302                persistent_storage: None,
303            },
304        );
305        builder.replace_realm_decl(realm_decl).await.unwrap();
306
307        add_host_routes(&builder, Ref::collection(BT_HOST_COLLECTION.to_string())).await?;
308
309        let dtr_exposes = vec![Capability::service::<fhbt::ServiceMarker>().into()];
310        let _ = builder.driver_test_realm_add_dtr_exposes(&dtr_exposes).await?;
311
312        // Route capabilities between realm components and bt-host-collection
313        builder
314            .add_route(
315                Route::new()
316                    .capability(Capability::service::<fhbt::ServiceMarker>())
317                    .from(Ref::child(fuchsia_driver_test::COMPONENT_NAME))
318                    .to(Ref::collection(BT_HOST_COLLECTION.to_string())),
319            )
320            .await?;
321        builder
322            .add_route(
323                Route::new()
324                    .capability(Capability::protocol::<LogSinkMarker>())
325                    .capability(Capability::dictionary("diagnostics"))
326                    .from(Ref::parent())
327                    .to(Ref::collection(BT_HOST_COLLECTION.to_string())),
328            )
329            .await?;
330        builder
331            .add_route(
332                Route::new()
333                    .capability(Capability::protocol::<ReceiverMarker>())
334                    .from(&host_receiver)
335                    .to(Ref::collection(BT_HOST_COLLECTION.to_string())),
336            )
337            .await?;
338        builder
339            .add_route(
340                Route::new()
341                    .capability(Capability::protocol::<RealmMarker>())
342                    .from(Ref::framework())
343                    .to(Ref::parent()),
344            )
345            .await?;
346
347        let instance = builder.build().await?;
348
349        // Start DriverTestRealm
350        let args = fdt::RealmArgs {
351            root_driver: Some(EMULATOR_ROOT_DRIVER_URL.to_string()),
352            software_devices: Some(vec![fidl_fuchsia_driver_test::SoftwareDevice {
353                device_name: "bt-hci-emulator".to_string(),
354                device_id: bind_fuchsia_platform::BIND_PLATFORM_DEV_DID_BT_HCI_EMULATOR,
355            }]),
356            test_component: Some(resolved_test_component),
357            dtr_exposes: Some(dtr_exposes),
358            ..Default::default()
359        };
360        instance.driver_test_realm_start(args).await?;
361
362        let service_watcher =
363            Service::open_from_dir(instance.root.get_exposed_dir(), fhbt::ServiceMarker)?
364                .watch()
365                .await?;
366
367        Ok(Self {
368            realm: instance,
369            receiver: futures::lock::Mutex::new(receiver),
370            service_watcher: futures::lock::Mutex::new(service_watcher),
371            next_host_id: AtomicUsize::new(0),
372        })
373    }
374
375    // Wait for a newly published fuchsia.hardware.bluetooth.Service instance, create a bt-host
376    // component for it in the bt-host collection in HostRealm, and wait for the component to
377    // register itself with Receiver and return the client end of the Host protocol.
378    pub async fn create_bt_host_in_collection(
379        realm: &Arc<HostRealm>,
380    ) -> Result<ClientEnd<HostMarker>, Error> {
381        let instance = realm
382            .service_watcher
383            .lock()
384            .await
385            .try_next()
386            .map_err(Error::from)
387            .on_timeout(WATCH_TIMEOUT, || {
388                Err(format_err!(
389                    "timed out waiting for fuchsia.hardware.bluetooth.Service instance"
390                ))
391            })
392            .await?
393            .ok_or_else(|| format_err!("fuchsia.hardware.bluetooth.Service watcher closed"))?;
394        let instance_name = instance.instance_name();
395        let id = realm.next_host_id.fetch_add(1, Ordering::SeqCst);
396        let component_name = format!("{BT_HOST}_{id}_{instance_name}"); // Name must only contain [a-z0-9-_]
397        let device_path = format!("{BT_SERVICE_DIR}/{instance_name}/vendor");
398        let collection_ref = CollectionRef { name: BT_HOST_COLLECTION.to_owned() };
399        let child_decl = Child {
400            name: Some(component_name),
401            url: Some(BT_HOST_URL.to_owned()),
402            startup: Some(StartupMode::Lazy),
403            config_overrides: Some(vec![ConfigOverride {
404                key: Some("device_path".to_string()),
405                value: Some(ConfigValue::Single(ConfigSingleValue::String(device_path))),
406                ..ConfigOverride::default()
407            }]),
408            ..Default::default()
409        };
410
411        let realm_proxy: RealmProxy =
412            realm.instance().connect_to_protocol_at_exposed_dir().unwrap();
413        let _ = realm_proxy
414            .create_child(&collection_ref, &child_decl, CreateChildArgs::default())
415            .await
416            .map_err(|e| format_err!("{e:?}"))?
417            .map_err(|e| format_err!("{e:?}"))?;
418
419        let host = realm.receiver.lock().await.next().await.unwrap();
420        Ok(host)
421    }
422
423    async fn fake_receiver_component(
424        sender: mpsc::Sender<ClientEnd<HostMarker>>,
425        handles: LocalComponentHandles,
426    ) -> Result<(), Error> {
427        let mut fs = ServiceFs::new();
428        let _ = fs.dir("svc").add_fidl_service(move |mut req_stream: ReceiverRequestStream| {
429            let mut sender_clone = sender.clone();
430            fuchsia_async::Task::local(async move {
431                let (host_server, _) =
432                    req_stream.next().await.unwrap().unwrap().into_add_host().unwrap();
433                sender_clone.send(host_server).await.expect("Host sent successfully");
434            })
435            .detach()
436        });
437
438        let _ = fs.serve_connection(handles.outgoing_dir)?;
439        fs.collect::<()>().await;
440        Ok(())
441    }
442
443    pub fn instance(&self) -> &ScopedInstance {
444        &self.realm.root
445    }
446
447    pub fn dev(&self) -> Result<fio::DirectoryProxy, Error> {
448        self.realm.driver_test_realm_connect_to_dev()
449    }
450}