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_io::Operations;
17use fidl_fuchsia_logger::LogSinkMarker;
18use fuchsia_bluetooth::constants::{
19    BT_HOST, BT_HOST_COLLECTION, BT_HOST_URL, DEV_DIR, HCI_DEVICE_DIR,
20};
21use fuchsia_component::server::ServiceFs;
22use fuchsia_component_test::{
23    Capability, ChildOptions, LocalComponentHandles, RealmBuilder, RealmInstance, Ref, Route,
24    ScopedInstance,
25};
26use fuchsia_driver_test::{DriverTestRealmBuilder, DriverTestRealmInstance};
27use fuchsia_sync::Mutex;
28use futures::channel::mpsc;
29use futures::{SinkExt, StreamExt};
30use std::sync::Arc;
31use {fidl_fuchsia_driver_test as fdt, fidl_fuchsia_io as fio};
32
33mod constants {
34    pub mod receiver {
35        pub const MONIKER: &str = "receiver";
36    }
37}
38
39pub async fn add_host_routes(
40    builder: &RealmBuilder,
41    to: impl Into<fuchsia_component_test::Ref> + Clone,
42) -> Result<(), Error> {
43    // Route config capabilities from root to bt-init
44    builder
45        .add_capability(cm_rust::CapabilityDecl::Config(cm_rust::ConfigurationDecl {
46            name: "fuchsia.bluetooth.LegacyPairing".parse()?,
47            value: cm_rust::ConfigValue::Single(cm_rust::ConfigSingleValue::Bool(false)),
48        }))
49        .await?;
50    builder
51        .add_capability(cm_rust::CapabilityDecl::Config(cm_rust::ConfigurationDecl {
52            name: "fuchsia.bluetooth.ScoOffloadPathIndex".parse()?,
53            value: cm_rust::ConfigValue::Single(cm_rust::ConfigSingleValue::Uint8(6)),
54        }))
55        .await?;
56    builder
57        .add_capability(cm_rust::CapabilityDecl::Config(cm_rust::ConfigurationDecl {
58            name: "fuchsia.bluetooth.OverrideVendorCapabilitiesVersion".parse()?,
59            value: cm_rust::ConfigValue::Single(cm_rust::ConfigSingleValue::Uint16(0)),
60        }))
61        .await?;
62    builder
63        .add_capability(cm_rust::CapabilityDecl::Config(cm_rust::ConfigurationDecl {
64            name: "fuchsia.power.SuspendEnabled".parse()?,
65            value: cm_rust::ConfigValue::Single(cm_rust::ConfigSingleValue::Bool(false)),
66        }))
67        .await?;
68
69    macro_rules! add_capability_route {
70        ($name:expr) => {
71            builder.add_route(
72                Route::new()
73                    .capability(Capability::configuration($name))
74                    .from(Ref::self_())
75                    .to(to.clone()),
76            )
77        };
78    }
79
80    add_capability_route!("fuchsia.bluetooth.LegacyPairing").await?;
81    add_capability_route!("fuchsia.bluetooth.ScoOffloadPathIndex").await?;
82    add_capability_route!("fuchsia.bluetooth.OverrideVendorCapabilitiesVersion").await?;
83    add_capability_route!("fuchsia.power.SuspendEnabled").await?;
84
85    // Add directory routing between components within CoreRealm
86    builder
87        .add_route(
88            Route::new()
89                .capability(Capability::directory("dev-class").subdir("bt-hci").as_("dev-bt-hci"))
90                .from(Ref::child(fuchsia_driver_test::COMPONENT_NAME))
91                .to(to),
92        )
93        .await?;
94    Ok(())
95}
96
97pub struct HostRealm {
98    realm: RealmInstance,
99    receiver: Mutex<Option<Receiver<ClientEnd<HostMarker>>>>,
100}
101
102impl HostRealm {
103    pub async fn create(test_component: String) -> Result<Self, Error> {
104        // We need to resolve our test component manually. Eventually component framework could provide
105        // an introspection way of resolving your own component.
106        let resolved_test_component = {
107            let client = fuchsia_component::client::connect_to_protocol_at_path::<
108                fidl_fuchsia_component_resolution::ResolverMarker,
109            >("/svc/fuchsia.component.resolution.Resolver-hermetic")
110            .unwrap();
111            client
112                .resolve(test_component.as_str())
113                .await
114                .unwrap()
115                .expect("Failed to resolve test component")
116        };
117
118        let builder = RealmBuilder::new().await?;
119        let _ = builder.driver_test_realm_setup().await?;
120
121        // Mock the fuchsia.bluetooth.host.Receiver API by creating a channel where the client end
122        // of the Host protocol can be extracted from |receiver|.
123        // Note: The word "receiver" is overloaded. One refers to the Receiver API, the other
124        // refers to the receiver end of the mpsc channel.
125        let (sender, receiver) = mpsc::channel(128);
126        let host_receiver = builder
127            .add_local_child(
128                constants::receiver::MONIKER,
129                move |handles| {
130                    let sender_clone = sender.clone();
131                    Box::pin(Self::fake_receiver_component(sender_clone, handles))
132                },
133                ChildOptions::new().eager(),
134            )
135            .await?;
136
137        // Create bt-host collection
138        let mut realm_decl = builder.get_realm_decl().await?;
139        push_box(
140            &mut realm_decl.collections,
141            cm_rust::CollectionDecl {
142                name: BT_HOST_COLLECTION.parse().unwrap(),
143                durability: Durability::SingleRun,
144                environment: None,
145                allowed_offers: cm_types::AllowedOffers::StaticAndDynamic,
146                allow_long_names: false,
147                persistent_storage: None,
148            },
149        );
150        builder.replace_realm_decl(realm_decl).await.unwrap();
151
152        add_host_routes(&builder, Ref::collection(BT_HOST_COLLECTION.to_string())).await?;
153
154        // Route capabilities between realm components and bt-host-collection
155        builder
156            .add_route(
157                Route::new()
158                    .capability(Capability::protocol::<LogSinkMarker>())
159                    .capability(Capability::dictionary("diagnostics"))
160                    .from(Ref::parent())
161                    .to(Ref::collection(BT_HOST_COLLECTION.to_string())),
162            )
163            .await?;
164        builder
165            .add_route(
166                Route::new()
167                    .capability(Capability::protocol::<ReceiverMarker>())
168                    .from(&host_receiver)
169                    .to(Ref::collection(BT_HOST_COLLECTION.to_string())),
170            )
171            .await?;
172        builder
173            .add_route(
174                Route::new()
175                    .capability(Capability::protocol::<RealmMarker>())
176                    .from(Ref::framework())
177                    .to(Ref::parent()),
178            )
179            .await?;
180
181        let instance = builder.build().await?;
182
183        // Start DriverTestRealm
184        let args = fdt::RealmArgs {
185            root_driver: Some(EMULATOR_ROOT_DRIVER_URL.to_string()),
186            software_devices: Some(vec![fidl_fuchsia_driver_test::SoftwareDevice {
187                device_name: "bt-hci-emulator".to_string(),
188                device_id: bind_fuchsia_platform::BIND_PLATFORM_DEV_DID_BT_HCI_EMULATOR,
189            }]),
190            test_component: Some(resolved_test_component),
191            ..Default::default()
192        };
193        instance.driver_test_realm_start(args).await?;
194
195        Ok(Self { realm: instance, receiver: Some(receiver).into() })
196    }
197
198    // Create bt-host component with |filename| and add it to bt-host collection in HostRealm.
199    // Wait for the component to register itself with Receiver and get the client end of the Host
200    // protocol.
201    pub async fn create_bt_host_in_collection(
202        realm: &Arc<HostRealm>,
203        filename: &str,
204    ) -> Result<ClientEnd<HostMarker>, Error> {
205        let component_name = format!("{BT_HOST}_{filename}"); // Name must only contain [a-z0-9-_]
206        let device_path = format!("{DEV_DIR}/{HCI_DEVICE_DIR}/default");
207        let collection_ref = CollectionRef { name: BT_HOST_COLLECTION.to_owned() };
208        let child_decl = Child {
209            name: Some(component_name.to_owned()),
210            url: Some(BT_HOST_URL.to_owned()),
211            startup: Some(StartupMode::Lazy),
212            config_overrides: Some(vec![ConfigOverride {
213                key: Some("device_path".to_string()),
214                value: Some(ConfigValue::Single(ConfigSingleValue::String(device_path))),
215                ..ConfigOverride::default()
216            }]),
217            ..Default::default()
218        };
219
220        let bt_host_offer = Offer::Directory(OfferDirectory {
221            source: Some(CompRef::Child(ChildRef {
222                name: fuchsia_driver_test::COMPONENT_NAME.to_owned(),
223                collection: None,
224            })),
225            source_name: Some("dev-class".to_owned()),
226            target_name: Some("dev-bt-hci-instance".to_owned()),
227            subdir: Some(format!("bt-hci/{filename}")),
228            dependency_type: Some(DependencyType::Strong),
229            rights: Some(
230                Operations::READ_BYTES
231                    | Operations::CONNECT
232                    | Operations::GET_ATTRIBUTES
233                    | Operations::TRAVERSE
234                    | Operations::ENUMERATE,
235            ),
236            ..Default::default()
237        });
238
239        let realm_proxy: RealmProxy =
240            realm.instance().connect_to_protocol_at_exposed_dir().unwrap();
241        let _ = realm_proxy
242            .create_child(
243                &collection_ref,
244                &child_decl,
245                CreateChildArgs { dynamic_offers: Some(vec![bt_host_offer]), ..Default::default() },
246            )
247            .await
248            .map_err(|e| format_err!("{e:?}"))?
249            .map_err(|e| format_err!("{e:?}"))?;
250
251        let host = realm.receiver().next().await.unwrap();
252        Ok(host)
253    }
254
255    async fn fake_receiver_component(
256        sender: mpsc::Sender<ClientEnd<HostMarker>>,
257        handles: LocalComponentHandles,
258    ) -> Result<(), Error> {
259        let mut fs = ServiceFs::new();
260        let _ = fs.dir("svc").add_fidl_service(move |mut req_stream: ReceiverRequestStream| {
261            let mut sender_clone = sender.clone();
262            fuchsia_async::Task::local(async move {
263                let (host_server, _) =
264                    req_stream.next().await.unwrap().unwrap().into_add_host().unwrap();
265                sender_clone.send(host_server).await.expect("Host sent successfully");
266            })
267            .detach()
268        });
269
270        let _ = fs.serve_connection(handles.outgoing_dir)?;
271        fs.collect::<()>().await;
272        Ok(())
273    }
274
275    pub fn instance(&self) -> &ScopedInstance {
276        &self.realm.root
277    }
278
279    pub fn dev(&self) -> Result<fio::DirectoryProxy, Error> {
280        self.realm.driver_test_realm_connect_to_dev()
281    }
282
283    pub fn receiver(&self) -> Receiver<ClientEnd<HostMarker>> {
284        self.receiver.lock().take().unwrap()
285    }
286}