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