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