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