Skip to main content

mock_piconet_client/
lib.rs

1// Copyright 2021 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 anyhow::{Context, Error, format_err};
6use cm_rust::{ExposeDecl, ExposeProtocolDecl, ExposeSource, ExposeTarget, append_box};
7use fidl::endpoints::{self as f_end, DiscoverableProtocolMarker};
8use fidl_fuchsia_bluetooth_bredr as bredr;
9use fidl_fuchsia_bluetooth_bredr_test as bredr_test;
10use fidl_fuchsia_component_test as ftest;
11use fidl_fuchsia_logger::LogSinkMarker;
12use fuchsia_async::{self as fasync, DurationExt, TimeoutExt};
13use fuchsia_bluetooth::types as bt_types;
14use fuchsia_component::server::ServiceFs;
15use fuchsia_component_test::{
16    Capability, ChildOptions, LocalComponentHandles, RealmBuilder, RealmInstance, Ref, Route,
17};
18use futures::stream::StreamExt;
19use futures::{TryFutureExt, TryStreamExt};
20use log::info;
21use zx::{self as zx, MonotonicDuration};
22
23/// Timeout for updates over the PeerObserver of a MockPeer.
24///
25/// This time is expected to be:
26///   a) sufficient to avoid flakes due to infra or resource contention
27///   b) short enough to still provide useful feedback in those cases where asynchronous operations
28///      fail
29///   c) short enough to fail before the overall infra-imposed test timeout (currently 5 minutes)
30const TIMEOUT_SECONDS: i64 = 2 * 60;
31
32pub fn peer_observer_timeout() -> MonotonicDuration {
33    MonotonicDuration::from_seconds(TIMEOUT_SECONDS)
34}
35
36static MOCK_PICONET_SERVER_URL: &str = "#meta/mock-piconet-server.cm";
37static PROFILE_INTERPOSER_PREFIX: &str = "profile-interposer";
38static BT_RFCOMM_PREFIX: &str = "bt-rfcomm";
39
40/// Returns the protocol name from the `capability` or an Error if it cannot be retrieved.
41fn protocol_name_from_capability(capability: &ftest::Capability) -> Result<String, Error> {
42    if let ftest::Capability::Protocol(ftest::Protocol { name: Some(name), .. }) = capability {
43        Ok(name.clone())
44    } else {
45        Err(format_err!("Not a protocol capability: {:?}", capability))
46    }
47}
48
49fn expose_decl(name: &str, id: bt_types::PeerId, capability_name: &str) -> ExposeDecl {
50    ExposeDecl::Protocol(ExposeProtocolDecl {
51        source: ExposeSource::Child(name.parse().unwrap()),
52        source_name: capability_name.parse().unwrap(),
53        source_dictionary: Default::default(),
54        target: ExposeTarget::Parent,
55        target_name: capability_path_for_peer_id(id, capability_name).parse().unwrap(),
56        availability: cm_rust::Availability::Required,
57    })
58}
59
60/// Specification data for creating a peer in the mock piconet. This may be a
61/// peer that will be driven by test code or one that is an actual Bluetooth
62/// profile implementation.
63#[derive(Clone, Debug)]
64pub struct PiconetMemberSpec {
65    pub name: String,
66    pub id: bt_types::PeerId,
67    /// The optional hermetic RFCOMM component URL to be used by piconet members
68    /// that need RFCOMM functionality.
69    rfcomm_url: Option<String>,
70    /// Expose declarations for additional capabilities provided by this piconet member. This is
71    /// typically empty for test driven peers (unless an RFCOMM intermediary is specified), and may
72    /// be populated for specs describing an actual Bluetooth profile implementation.
73    /// The exposed capabilities will be available at a unique path associated with this spec (e.g
74    /// `fuchsia.bluetooth.hfp.Hfp-321abc` where `321abc` is the PeerId for this member).
75    /// Note: Only protocol capabilities may be specified here.
76    expose_decls: Vec<ExposeDecl>,
77    observer: Option<bredr_test::PeerObserverProxy>,
78}
79
80impl PiconetMemberSpec {
81    pub fn get_profile_proxy(
82        &self,
83        topology: &RealmInstance,
84    ) -> Result<bredr::ProfileProxy, anyhow::Error> {
85        info!("Received request to get `bredr.Profile` for piconet member: {:?}", self.id);
86        let (client, server) = f_end::create_proxy::<bredr::ProfileMarker>();
87        topology.root.connect_request_to_named_protocol_at_exposed_dir(
88            &capability_path_for_mock::<bredr::ProfileMarker>(self),
89            server.into_channel(),
90        )?;
91        Ok(client)
92    }
93
94    /// Create a PiconetMemberSpec configured to be used with a Profile
95    /// component which is under test.
96    /// `rfcomm_url` is the URL for an optional v2 RFCOMM component that will sit between the
97    /// Profile and the Mock Piconet Server.
98    /// `expose_capabilities` specifies protocol capabilities provided by this Profile component to
99    /// be exposed above the test root.
100    pub fn for_profile(
101        name: String,
102        rfcomm_url: Option<String>,
103        expose_capabilities: Vec<ftest::Capability>,
104    ) -> Result<(Self, bredr_test::PeerObserverRequestStream), Error> {
105        let id = bt_types::PeerId::random();
106        let capability_names = expose_capabilities
107            .iter()
108            .map(protocol_name_from_capability)
109            .collect::<Result<Vec<_>, _>>()?;
110        let expose_decls = capability_names
111            .iter()
112            .map(|capability_name| expose_decl(&name, id, capability_name))
113            .collect();
114        let (peer_proxy, peer_stream) =
115            f_end::create_proxy_and_stream::<bredr_test::PeerObserverMarker>();
116
117        Ok((Self { name, id, rfcomm_url, expose_decls, observer: Some(peer_proxy) }, peer_stream))
118    }
119
120    /// Create a PiconetMemberSpec designed to be used with a peer that will be driven
121    /// by test code.
122    /// `rfcomm_url` is the URL for an optional v2 RFCOMM component that will sit between the
123    /// mock peer and the integration test client.
124    pub fn for_mock_peer(name: String, rfcomm_url: Option<String>) -> Self {
125        let id = bt_types::PeerId::random();
126        // If the RFCOMM URL is specified, then we expect to expose the `bredr.Profile` capability
127        // above the test root at a unique path.
128        let expose_decls = if rfcomm_url.is_some() {
129            let rfcomm_name = bt_rfcomm_moniker_for_member(&name);
130            vec![expose_decl(&rfcomm_name, id, bredr::ProfileMarker::PROTOCOL_NAME)]
131        } else {
132            Vec::new()
133        };
134        Self { name, id, rfcomm_url, expose_decls, observer: None }
135    }
136}
137
138fn capability_path_for_mock<S: DiscoverableProtocolMarker>(mock: &PiconetMemberSpec) -> String {
139    capability_path_for_peer_id(mock.id, S::PROTOCOL_NAME)
140}
141
142fn capability_path_for_peer_id(id: bt_types::PeerId, capability_name: &str) -> String {
143    format!("{}-{}", capability_name, id)
144}
145
146pub struct PiconetMember {
147    id: bt_types::PeerId,
148    profile_svc: bredr::ProfileProxy,
149}
150
151impl PiconetMember {
152    pub fn peer_id(&self) -> bt_types::PeerId {
153        self.id
154    }
155
156    pub fn new_from_spec(
157        mock: PiconetMemberSpec,
158        realm: &RealmInstance,
159    ) -> Result<Self, anyhow::Error> {
160        Ok(Self {
161            id: mock.id,
162            profile_svc: mock
163                .get_profile_proxy(realm)
164                .context("failed to open mock's profile proxy")?,
165        })
166    }
167
168    /// Register a service search using the Profile protocol for services that match `svc_id`.
169    ///
170    /// Returns a stream of search results that can be polled to receive new requests.
171    pub fn register_service_search(
172        &self,
173        svc_id: bredr::ServiceClassProfileIdentifier,
174        attributes: Vec<u16>,
175    ) -> Result<bredr::SearchResultsRequestStream, Error> {
176        let (results_client, results_requests) = f_end::create_request_stream();
177        self.profile_svc.search(bredr::ProfileSearchRequest {
178            service_uuid: Some(svc_id),
179            attr_ids: Some(attributes),
180            results: Some(results_client),
181            ..Default::default()
182        })?;
183        Ok(results_requests)
184    }
185
186    /// Register a service advertisement using the Profile protocol with the provided
187    /// `service_defs`.
188    ///
189    /// Returns a stream of connection requests that can be polled to receive new requests.
190    pub fn register_service_advertisement(
191        &self,
192        service_defs: Vec<bredr::ServiceDefinition>,
193    ) -> Result<bredr::ConnectionReceiverRequestStream, Error> {
194        let (connect_client, connect_requests) = f_end::create_request_stream();
195        let _ = self.profile_svc.advertise(bredr::ProfileAdvertiseRequest {
196            services: Some(service_defs),
197            receiver: Some(connect_client),
198            ..Default::default()
199        });
200
201        Ok(connect_requests)
202    }
203
204    pub async fn make_connection(
205        &self,
206        peer_id: bt_types::PeerId,
207        params: bredr::ConnectParameters,
208    ) -> Result<bredr::Channel, Error> {
209        self.profile_svc
210            .connect(&peer_id.into(), &params)
211            .await?
212            .map_err(|e| format_err!("{:?}", e))
213    }
214}
215
216/// Represents a Bluetooth profile-under-test in the test topology.
217///
218/// Provides helpers designed to observe what the real profile implementation is doing.
219/// Provides access to any capabilities that have been exposed by this profile.
220/// Note: Only capabilities that are specified in the `expose_capabilities` field of the
221///       PiconetHarness::add_profile_with_capabilities() method will be available for connection.
222pub struct BtProfileComponent {
223    observer_stream: bredr_test::PeerObserverRequestStream,
224    profile_id: bt_types::PeerId,
225}
226
227impl BtProfileComponent {
228    pub fn new(stream: bredr_test::PeerObserverRequestStream, id: bt_types::PeerId) -> Self {
229        Self { observer_stream: stream, profile_id: id }
230    }
231
232    pub fn peer_id(&self) -> bt_types::PeerId {
233        self.profile_id
234    }
235
236    /// Connects to the protocol `S` provided by this Profile. Returns the client end on success,
237    /// Error if the capability is not available.
238    pub fn connect_to_protocol<S: DiscoverableProtocolMarker>(
239        &self,
240        topology: &RealmInstance,
241    ) -> Result<S::Proxy, Error> {
242        let (client, server) = f_end::create_proxy::<S>();
243        topology.root.connect_request_to_named_protocol_at_exposed_dir(
244            &capability_path_for_peer_id(self.profile_id, S::PROTOCOL_NAME),
245            server.into_channel(),
246        )?;
247        Ok(client)
248    }
249
250    /// Expects a request over the `PeerObserver` protocol for this MockPeer.
251    ///
252    /// Returns the request if successful.
253    pub async fn expect_observer_request(
254        &mut self,
255    ) -> Result<bredr_test::PeerObserverRequest, Error> {
256        // The Future is gated by a timeout so that tests consistently terminate.
257        self.observer_stream
258            .select_next_some()
259            .map_err(|e| format_err!("{:?}", e))
260            .on_timeout(peer_observer_timeout().after_now(), move || {
261                Err(format_err!("observer timed out"))
262            })
263            .await
264    }
265
266    /// Expects a connection request between the profile under test and the `other` peer.
267    ///
268    /// Returns Ok on success, Error if there was no connection request on the observer or
269    /// the request was for a different peer.
270    pub async fn expect_observer_connection_request(
271        &mut self,
272        other: bt_types::PeerId,
273    ) -> Result<(), Error> {
274        let request = self.expect_observer_request().await?;
275        match request {
276            bredr_test::PeerObserverRequest::PeerConnected { peer_id, responder, .. } => {
277                responder.send().unwrap();
278                if other == peer_id.into() {
279                    Ok(())
280                } else {
281                    Err(format_err!("Connection request for unexpected peer: {:?}", peer_id))
282                }
283            }
284            x => Err(format_err!("Expected PeerConnected but got: {:?}", x)),
285        }
286    }
287
288    /// Expects the profile under test to discover the services of the `other` peer.
289    ///
290    /// Returns Ok on success, Error if there was no ServiceFound request on the observer or
291    /// the request was for a different peer.
292    pub async fn expect_observer_service_found_request(
293        &mut self,
294        other: bt_types::PeerId,
295    ) -> Result<(), Error> {
296        let request = self.expect_observer_request().await?;
297        match request {
298            bredr_test::PeerObserverRequest::ServiceFound { peer_id, responder, .. } => {
299                responder.send().unwrap();
300                if other == peer_id.into() {
301                    Ok(())
302                } else {
303                    Err(format_err!("ServiceFound request for unexpected peer: {:?}", peer_id))
304                }
305            }
306            x => Err(format_err!("Expected PeerConnected but got: {:?}", x)),
307        }
308    }
309}
310
311/// Adds a profile to the test topology.
312///
313/// This also creates a component that sits between the profile and the Mock Piconet Server.
314/// This node acts as a facade between the profile under test and the Server.
315/// If the `spec` contains a channel then `PeerObserver` events will be forwarded to that
316/// channel.
317/// `additional_routes` specifies capability routings for any protocols used/exposed by the
318/// profile.
319async fn add_profile_to_topology<'a>(
320    builder: &RealmBuilder,
321    spec: &'a mut PiconetMemberSpec,
322    server_moniker: String,
323    profile_url: String,
324    additional_routes: Vec<Route>,
325) -> Result<(), Error> {
326    // Specify the interposer component that will provide `Profile` to the profile under test.
327    let mock_piconet_member_name = interposer_name_for_profile(&spec.name);
328    add_mock_piconet_component(
329        builder,
330        mock_piconet_member_name.clone(),
331        spec.id,
332        bredr::ProfileMarker::PROTOCOL_NAME.to_string(),
333        spec.observer.take(),
334    )
335    .await?;
336
337    // If required, specify the RFCOMM intermediary component.
338    let rfcomm_moniker = bt_rfcomm_moniker_for_member(&spec.name);
339    if let Some(url) = &spec.rfcomm_url {
340        add_bt_rfcomm_intermediary(builder, rfcomm_moniker.clone(), url.clone()).await?;
341    }
342
343    // Specify the profile under test.
344    {
345        let _ = builder
346            .add_child(spec.name.to_string(), profile_url, ChildOptions::new().eager())
347            .await?;
348    }
349
350    // Capability routes:
351    //   * If `bt-rfcomm` is specified as an intermediary, `Profile` from mock piconet member
352    //     to `bt-rfcomm` and then from `bt-rfcomm` to profile under test.
353    //     Otherwise, `Profile` directly from mock piconet member to profile under test.
354    //   * `ProfileTest` from Mock Piconet Server to mock piconet member
355    //   * `LogSink` from parent to the profile under test & mock piconet member.
356    //   * Additional capabilities from the profile under test to AboveRoot to be
357    //     accessible via the test realm service directory.
358    {
359        if spec.rfcomm_url.is_some() {
360            builder
361                .add_route(
362                    Route::new()
363                        .capability(Capability::protocol::<bredr::ProfileMarker>())
364                        .from(Ref::child(&mock_piconet_member_name))
365                        .to(Ref::child(&rfcomm_moniker)),
366                )
367                .await?;
368            builder
369                .add_route(
370                    Route::new()
371                        .capability(Capability::protocol::<bredr::ProfileMarker>())
372                        .from(Ref::child(&rfcomm_moniker))
373                        .to(Ref::child(&spec.name)),
374                )
375                .await?;
376        } else {
377            builder
378                .add_route(
379                    Route::new()
380                        .capability(Capability::protocol::<bredr::ProfileMarker>())
381                        .from(Ref::child(&mock_piconet_member_name))
382                        .to(Ref::child(&spec.name)),
383                )
384                .await?;
385        }
386
387        builder
388            .add_route(
389                Route::new()
390                    .capability(Capability::protocol::<bredr_test::ProfileTestMarker>())
391                    .from(Ref::child(&server_moniker))
392                    .to(Ref::child(&mock_piconet_member_name)),
393            )
394            .await?;
395
396        builder
397            .add_route(
398                Route::new()
399                    .capability(Capability::protocol::<LogSinkMarker>())
400                    .from(Ref::parent())
401                    .to(Ref::child(&spec.name))
402                    .to(Ref::child(&mock_piconet_member_name)),
403            )
404            .await?;
405
406        for route in additional_routes {
407            let _ = builder.add_route(route).await?;
408        }
409    }
410    Ok(())
411}
412
413async fn add_mock_piconet_member<'a, 'b>(
414    builder: &RealmBuilder,
415    mock: &'a mut PiconetMemberSpec,
416    server_moniker: String,
417) -> Result<(), Error> {
418    // The capability path of `Profile` is determined by the existence of the RFCOMM intermediary.
419    // - If the RFCOMM intermediary is specified, the mock piconet component will expose `Profile`
420    //   to the RFCOMM intermediary at the standard path. The RFCOMM intermediary will then expose
421    //   `Profile` at a unique path.
422    // - If the RFCOMM intermediary is not specified, the mock piconet component will directly
423    //   expose `Profile` at a unique path.
424    let profile_path = if mock.rfcomm_url.is_some() {
425        bredr::ProfileMarker::PROTOCOL_NAME.to_string()
426    } else {
427        capability_path_for_mock::<bredr::ProfileMarker>(mock)
428    };
429    add_mock_piconet_component(
430        builder,
431        mock.name.to_string(),
432        mock.id,
433        profile_path.clone(),
434        mock.observer.take(),
435    )
436    .await?;
437
438    // If required, specify the RFCOMM intermediary component.
439    let rfcomm_moniker = bt_rfcomm_moniker_for_member(&mock.name);
440    if let Some(url) = &mock.rfcomm_url {
441        add_bt_rfcomm_intermediary(builder, rfcomm_moniker.clone(), url.clone()).await?;
442    }
443
444    // Capability routes:
445    // - `ProfileTest` from the Mock Piconet Server to the mock piconet member component.
446    // - If `bt-rfcomm` is specified as an intermediary, `Profile` from mock piconet member
447    //   to `bt-rfcomm`.
448    //   Note: Exposing `Profile` from `bt-rfcomm` to AboveRoot at a unique path will happen after
449    //   the test realm has been defined and built due to constraints of component decls.
450    // - If `bt-rfcomm` is not specified, route `Profile` directly from mock piconet member to
451    //   AboveRoot at the unique path (e.g fuchsia.bluetooth.bredr.Profile-3 where "3" is the peer
452    //   ID of the mock).
453    {
454        builder
455            .add_route(
456                Route::new()
457                    .capability(Capability::protocol::<bredr_test::ProfileTestMarker>())
458                    .from(Ref::child(&server_moniker))
459                    .to(Ref::child(&mock.name)),
460            )
461            .await?;
462
463        if mock.rfcomm_url.is_some() {
464            builder
465                .add_route(
466                    Route::new()
467                        .capability(Capability::protocol_by_name(profile_path))
468                        .from(Ref::child(&mock.name))
469                        .to(Ref::child(&rfcomm_moniker)),
470                )
471                .await?;
472        } else {
473            builder
474                .add_route(
475                    Route::new()
476                        .capability(Capability::protocol_by_name(profile_path))
477                        .from(Ref::child(&mock.name))
478                        .to(Ref::parent()),
479                )
480                .await?;
481        }
482    }
483
484    Ok(())
485}
486
487/// Add the mock piconet member to the Realm. If observer_src is None a channel
488/// is created and the server end shipped off to a future to be drained.
489async fn add_mock_piconet_component(
490    builder: &RealmBuilder,
491    name: String,
492    id: bt_types::PeerId,
493    profile_svc_path: String,
494    observer_src: Option<bredr_test::PeerObserverProxy>,
495) -> Result<(), Error> {
496    // If there is no observer, make a channel and fill it in. The server end
497    // of the channel is passed to a future which just reads the channel to
498    // completion.
499    let observer = observer_src.unwrap_or_else(|| {
500        let (proxy, stream) = f_end::create_proxy_and_stream::<bredr_test::PeerObserverMarker>();
501        fasync::Task::local(async move {
502            let _ = drain_observer(stream).await;
503        })
504        .detach();
505        proxy
506    });
507
508    builder
509        .add_local_child(
510            name,
511            move |m: LocalComponentHandles| {
512                let observer = observer.clone();
513                Box::pin(piconet_member(m, id, profile_svc_path.clone(), observer))
514            },
515            ChildOptions::new(),
516        )
517        .await
518        .map(|_| ())
519        .map_err(|e| e.into())
520}
521
522/// Drives the mock piconet member. This receives the open request for the
523/// Profile service, attaches it to the Mock Piconet Server, and wires up the
524/// the PeerObserver.
525async fn piconet_member(
526    handles: LocalComponentHandles,
527    id: bt_types::PeerId,
528    profile_svc_path: String,
529    peer_observer: bredr_test::PeerObserverProxy,
530) -> Result<(), Error> {
531    // connect to the profile service to drive the mock peer
532    let pro_test: bredr_test::ProfileTestProxy = handles.connect_to_protocol()?;
533    let mut fs = ServiceFs::new();
534
535    let _ = fs.dir("svc").add_service_at(profile_svc_path, move |chan: zx::Channel| {
536        info!("Received ServiceFs `Profile` connection request for piconet_member: {:?}", id);
537        let profile_test = pro_test.clone();
538        let observer = peer_observer.clone();
539
540        fasync::Task::local(async move {
541            let (client, observer_req_stream) =
542                register_piconet_member(&profile_test, id).await.unwrap();
543
544            let err_str = format!("Couldn't connect to `Profile` for peer {:?}", id);
545
546            let _ = client.connect_proxy_(chan.into()).await.expect(&err_str);
547
548            // keep us running and hold on until termination to keep the mock alive
549            fwd_observer_callbacks(observer_req_stream, &observer, id).await.unwrap();
550        })
551        .detach();
552        Some(())
553    });
554
555    let _ = fs.serve_connection(handles.outgoing_dir).expect("failed to serve service fs");
556    fs.collect::<()>().await;
557
558    Ok(())
559}
560
561/// Use the ProfileTestProxy to register a piconet member with the Bluetooth Profile Test
562/// Server.
563async fn register_piconet_member(
564    profile_test_proxy: &bredr_test::ProfileTestProxy,
565    id: bt_types::PeerId,
566) -> Result<(bredr_test::MockPeerProxy, bredr_test::PeerObserverRequestStream), Error> {
567    info!("Sending RegisterPeer request for peer {:?} to the Mock Piconet Server.", id);
568    let (client, server) = f_end::create_proxy::<bredr_test::MockPeerMarker>();
569    let (observer_client, observer_server) =
570        f_end::create_request_stream::<bredr_test::PeerObserverMarker>();
571
572    profile_test_proxy
573        .register_peer(&id.into(), server, observer_client)
574        .await
575        .context("registering peer failed!")?;
576    Ok((client, observer_server))
577}
578
579fn handle_fidl_err(fidl_err: fidl::Error, ctx: String) -> Result<(), Error> {
580    if fidl_err.is_closed() { Ok(()) } else { Err(anyhow::Error::from(fidl_err).context(ctx)) }
581}
582
583/// Given a request stream and a proxy, forward from one to the other
584async fn fwd_observer_callbacks(
585    mut source_req_stream: bredr_test::PeerObserverRequestStream,
586    observer: &bredr_test::PeerObserverProxy,
587    id: bt_types::PeerId,
588) -> Result<(), Error> {
589    while let Some(req) =
590        source_req_stream.try_next().await.context("reading peer observer failed")?
591    {
592        match req {
593            bredr_test::PeerObserverRequest::ServiceFound {
594                peer_id,
595                protocol,
596                attributes,
597                responder,
598            } => {
599                let proto = match protocol {
600                    Some(desc) => desc,
601                    None => vec![],
602                };
603
604                observer.service_found(&peer_id, Some(&proto), &attributes).await.or_else(|e| {
605                    handle_fidl_err(
606                        e,
607                        format!("unexpected error forwarding observer event for: {}", id),
608                    )
609                })?;
610
611                responder.send().or_else(|e| {
612                    handle_fidl_err(
613                        e,
614                        format!("unexpected error acking observer event for: {}", id),
615                    )
616                })?;
617            }
618
619            bredr_test::PeerObserverRequest::PeerConnected { peer_id, protocol, responder } => {
620                observer.peer_connected(&peer_id, &protocol).await.or_else(|e| {
621                    handle_fidl_err(
622                        e,
623                        format!("unexpected error forwarding observer event for: {}", id),
624                    )
625                })?;
626                responder.send().or_else(|e| {
627                    handle_fidl_err(
628                        e,
629                        format!("unexpected error acking observer event for: {}", id),
630                    )
631                })?;
632            }
633        }
634    }
635    Ok(())
636}
637
638async fn drain_observer(
639    mut stream: bredr_test::PeerObserverRequestStream,
640) -> Result<(), fidl::Error> {
641    while let Some(req) = stream.try_next().await? {
642        match req {
643            bredr_test::PeerObserverRequest::ServiceFound { responder, .. } => {
644                responder.send()?;
645            }
646            bredr_test::PeerObserverRequest::PeerConnected { responder, .. } => {
647                responder.send()?;
648            }
649        }
650    }
651    Ok(())
652}
653
654async fn add_mock_piconet_server(builder: &RealmBuilder) -> String {
655    let name = mock_piconet_server_moniker().to_string();
656
657    let mock_piconet_server = builder
658        .add_child(name.clone(), MOCK_PICONET_SERVER_URL, ChildOptions::new())
659        .await
660        .expect("failed to add");
661
662    builder
663        .add_route(
664            Route::new()
665                .capability(Capability::protocol::<LogSinkMarker>())
666                .from(Ref::parent())
667                .to(&mock_piconet_server),
668        )
669        .await
670        .unwrap();
671    name
672}
673
674/// Adds the `bt-rfcomm` component, identified by the `url`, to the component topology.
675async fn add_bt_rfcomm_intermediary(
676    builder: &RealmBuilder,
677    moniker: String,
678    url: String,
679) -> Result<(), Error> {
680    let bt_rfcomm = builder.add_child(moniker.clone(), url, ChildOptions::new().eager()).await?;
681
682    let _ = builder
683        .add_route(
684            Route::new()
685                .capability(Capability::protocol::<LogSinkMarker>())
686                .from(Ref::parent())
687                .to(&bt_rfcomm),
688        )
689        .await?;
690    Ok(())
691}
692
693fn mock_piconet_server_moniker() -> String {
694    "mock-piconet-server".to_string()
695}
696
697fn bt_rfcomm_moniker_for_member(member_name: &'_ str) -> String {
698    format!("{}-for-{}", BT_RFCOMM_PREFIX, member_name)
699}
700
701fn interposer_name_for_profile(profile_name: &'_ str) -> String {
702    format!("{}-{}", PROFILE_INTERPOSER_PREFIX, profile_name)
703}
704
705/// Represents the topology of a piconet set up by an integration test.
706///
707/// Provides an API to add members to the piconet, define Bluetooth profiles to be run
708/// under test, and specify capability routing in the topology. Bluetooth profiles
709/// specified in the topology _must_ be v2 components.
710///
711/// ### Example Usage:
712///
713/// let harness = PiconetHarness::new().await;
714///
715/// // Add a mock piconet member to be driven by test code.
716/// let spec = harness.add_mock_piconet_member("mock-peer".to_string()).await?;
717/// // Add a Bluetooth Profile (AVRCP) to the topology.
718/// let profile_observer = harness.add_profile("bt-avrcp-profile", AVRCP_URL_V2).await?;
719///
720/// // The topology has been defined and can be built. After this step, it cannot be
721/// // modified (e.g Can't add a new mock piconet member).
722/// let test_topology = test_harness.build().await?;
723///
724/// // Get the test-driven peer from the topology.
725/// let test_driven_peer = PiconetMember::new_from_spec(spec, &test_topology)?;
726///
727/// // Manipulate the test-driven peer to indirectly interact with the profile-under-test.
728/// let search_results = test_driven_peer.register_service_search(..)?;
729/// // Expect some behavior from the profile-under-test.
730/// let req = profile_observer.expect_observer_request().await?;
731/// assert_eq!(req, ..);
732pub struct PiconetHarness {
733    pub builder: RealmBuilder,
734    pub ps_moniker: String,
735    profiles: Vec<PiconetMemberSpec>,
736    piconet_members: Vec<PiconetMemberSpec>,
737}
738
739impl PiconetHarness {
740    pub async fn new() -> Self {
741        let builder = RealmBuilder::new().await.expect("Couldn't create realm builder");
742        let ps_moniker = add_mock_piconet_server(&builder).await;
743        PiconetHarness { builder, ps_moniker, profiles: Vec::new(), piconet_members: Vec::new() }
744    }
745
746    pub async fn add_mock_piconet_members(
747        &mut self,
748        mocks: &'_ mut Vec<PiconetMemberSpec>,
749    ) -> Result<(), Error> {
750        for mock in mocks {
751            self.add_mock_piconet_member_from_spec(mock).await?;
752        }
753        Ok(())
754    }
755
756    pub async fn add_mock_piconet_member(
757        &mut self,
758        name: String,
759        rfcomm_url: Option<String>,
760    ) -> Result<PiconetMemberSpec, Error> {
761        let mut mock = PiconetMemberSpec::for_mock_peer(name, rfcomm_url);
762
763        self.add_mock_piconet_member_from_spec(&mut mock).await?;
764        Ok(mock)
765    }
766
767    async fn add_mock_piconet_member_from_spec(
768        &mut self,
769        mock: &'_ mut PiconetMemberSpec,
770    ) -> Result<(), Error> {
771        add_mock_piconet_member(&self.builder, mock, self.ps_moniker.clone()).await?;
772        self.piconet_members.push(mock.clone());
773        Ok(())
774    }
775
776    /// Updates expose routes specified by the profiles and piconet members.
777    async fn update_routes(&self) -> Result<(), Error> {
778        info!(
779            "Building test realm with profiles: {:?} and piconet members: {:?}",
780            self.profiles, self.piconet_members
781        );
782        let mut root_decl = self.builder.get_realm_decl().await.expect("failed to get root");
783
784        let mut piconet_member_exposes =
785            self.piconet_members.iter().map(|spec| spec.expose_decls.clone()).flatten().collect();
786        let mut profile_member_exposes =
787            self.profiles.iter().map(|spec| spec.expose_decls.clone()).flatten().collect();
788        append_box(&mut root_decl.exposes, &mut piconet_member_exposes);
789        append_box(&mut root_decl.exposes, &mut profile_member_exposes);
790
791        // Update the root decl with the modified `expose` routes.
792        self.builder.replace_realm_decl(root_decl).await.expect("Should be able to set root decl");
793        Ok(())
794    }
795
796    pub async fn build(self) -> Result<RealmInstance, Error> {
797        self.update_routes().await?;
798        self.builder.build().await.map_err(|e| e.into())
799    }
800
801    /// Add a profile with moniker `name` to the test topology. The profile should be
802    /// accessible via the provided `profile_url` and will be launched during the test.
803    ///
804    /// Returns an observer for the launched profile.
805    pub async fn add_profile(
806        &mut self,
807        name: String,
808        profile_url: String,
809    ) -> Result<BtProfileComponent, Error> {
810        self.add_profile_with_capabilities(name, profile_url, None, vec![], vec![]).await
811    }
812
813    /// Add a profile with moniker `name` to the test topology.
814    ///
815    /// `profile_url` specifies the component URL of the profile under test.
816    /// `rfcomm_url` specifies the optional hermetic RFCOMM component URL to be used as an
817    /// intermediary in the test topology.
818    /// `use_capabilities` specifies any capabilities used by the profile that will be
819    /// provided outside the test realm.
820    /// `expose_capabilities` specifies any protocol capabilities provided by the profile to be
821    /// available in the outgoing directory of the test realm root.
822    ///
823    /// Returns an observer for the launched profile.
824    pub async fn add_profile_with_capabilities(
825        &mut self,
826        name: String,
827        profile_url: String,
828        rfcomm_url: Option<String>,
829        use_capabilities: Vec<ftest::Capability>,
830        expose_capabilities: Vec<ftest::Capability>,
831    ) -> Result<BtProfileComponent, Error> {
832        let (mut spec, request_stream) =
833            PiconetMemberSpec::for_profile(name, rfcomm_url, expose_capabilities.clone())?;
834        // Use capabilities can be directly turned into routes.
835        let route = route_from_capabilities(
836            use_capabilities,
837            Ref::parent(),
838            vec![Ref::child(spec.name.clone())],
839        );
840
841        self.add_profile_from_spec(&mut spec, profile_url, vec![route]).await?;
842        Ok(BtProfileComponent::new(request_stream, spec.id))
843    }
844
845    async fn add_profile_from_spec(
846        &mut self,
847        spec: &mut PiconetMemberSpec,
848        profile_url: String,
849        capabilities: Vec<Route>,
850    ) -> Result<(), Error> {
851        add_profile_to_topology(
852            &self.builder,
853            spec,
854            self.ps_moniker.clone(),
855            profile_url,
856            capabilities,
857        )
858        .await?;
859        self.profiles.push(spec.clone());
860        Ok(())
861    }
862}
863
864/// Builds a set of capability routes from `capabilities` that will be routed from
865/// `source` to the `targets`.
866pub fn route_from_capabilities(
867    capabilities: Vec<ftest::Capability>,
868    source: Ref,
869    targets: Vec<Ref>,
870) -> Route {
871    let mut route = Route::new().from(source);
872    for capability in capabilities {
873        route = route.capability(capability);
874    }
875    for target in targets {
876        route = route.to(target);
877    }
878    route
879}
880
881#[cfg(test)]
882mod tests {
883    use super::*;
884    use assert_matches::assert_matches;
885    use cm_rust::{
886        Availability, ChildRef, DependencyType, OfferDecl, OfferProtocolDecl, OfferSource,
887        OfferTarget, UseDecl, UseProtocolDecl, UseSource,
888    };
889    use cm_types::Name;
890    use fidl_fuchsia_component_test as fctest;
891    use fuchsia_component_test::error::Error as RealmBuilderError;
892
893    fn offer_source_static_child(name: &str) -> OfferSource {
894        OfferSource::Child(ChildRef { name: name.parse().unwrap(), collection: None })
895    }
896
897    fn offer_target_static_child(name: &str) -> cm_rust::OfferTarget {
898        OfferTarget::Child(ChildRef { name: name.parse().unwrap(), collection: None })
899    }
900
901    async fn assert_realm_contains(builder: &RealmBuilder, child_name: &str) {
902        let err = builder
903            .add_child(child_name, "test://example-url", ChildOptions::new())
904            .await
905            .expect_err("failed to check realm contents");
906        assert_matches!(
907            err,
908            RealmBuilderError::ServerError(fctest::RealmBuilderError::ChildAlreadyExists)
909        );
910    }
911
912    #[fuchsia::test]
913    async fn test_profile_server_added() {
914        let test_harness = PiconetHarness::new().await;
915        test_harness.update_routes().await.expect("should update routes");
916        assert_realm_contains(&test_harness.builder, &super::mock_piconet_server_moniker()).await;
917        let _ = test_harness.builder.build().await.expect("build failed");
918    }
919
920    #[fuchsia::test]
921    async fn test_add_piconet_member() {
922        let mut test_harness = PiconetHarness::new().await;
923        let member_name = "test-piconet-member";
924        let member_spec = test_harness
925            .add_mock_piconet_member(member_name.to_string(), None)
926            .await
927            .expect("failed to add piconet member");
928        assert_eq!(member_spec.name, member_name);
929
930        test_harness.update_routes().await.expect("should update routes");
931        validate_mock_piconet_member(&test_harness.builder, &member_spec).await;
932        let _profile_test_offer = test_harness.builder.build().await.expect("build failed");
933    }
934
935    #[fuchsia::test]
936    async fn test_add_piconet_member_with_rfcomm() {
937        let mut test_harness = PiconetHarness::new().await;
938        let member_name = "test-piconet-member";
939        let rfcomm_url = "fuchsia-pkg://fuchsia.com/example#meta/bt-rfcomm.cm".to_string();
940        let member_spec = test_harness
941            .add_mock_piconet_member(member_name.to_string(), Some(rfcomm_url))
942            .await
943            .expect("failed to add piconet member");
944        assert_eq!(member_spec.name, member_name);
945
946        test_harness.update_routes().await.expect("should update routes");
947        validate_mock_piconet_member(&test_harness.builder, &member_spec).await;
948
949        // Note: We don't `create()` the test realm because the `rfcomm_url` does not exist which
950        // will cause component resolving to fail.
951    }
952
953    #[fuchsia::test]
954    async fn test_add_multiple_piconet_members() {
955        let mut test_harness = PiconetHarness::new().await;
956        let member1_name = "test-piconet-member".to_string();
957        let member2_name = "test-piconet-member-two".to_string();
958        let mut members = vec![
959            PiconetMemberSpec::for_mock_peer(member1_name, None),
960            PiconetMemberSpec::for_mock_peer(member2_name, None),
961        ];
962
963        test_harness
964            .add_mock_piconet_members(&mut members)
965            .await
966            .expect("failed to add piconet members");
967
968        test_harness.update_routes().await.expect("should update routes");
969
970        for member in &members {
971            validate_mock_piconet_member(&test_harness.builder, member).await;
972        }
973        let _profile_test_offer = test_harness.builder.build().await.expect("build failed");
974    }
975
976    #[fuchsia::test]
977    async fn test_add_multiple_piconet_members_with_rfcomm() {
978        let mut test_harness = PiconetHarness::new().await;
979        let rfcomm_url = "fuchsia-pkg://fuchsia.com/example#meta/bt-rfcomm.cm".to_string();
980        let member1_name = "test-piconet-member-1".to_string();
981        let member2_name = "test-piconet-member-2".to_string();
982        let member3_name = "test-piconet-member-3".to_string();
983        // A combination of RFCOMM and non-RFCOMM piconet members is OK.
984        let mut members = vec![
985            PiconetMemberSpec::for_mock_peer(member1_name, None),
986            PiconetMemberSpec::for_mock_peer(member2_name, Some(rfcomm_url.clone())),
987            PiconetMemberSpec::for_mock_peer(member3_name, Some(rfcomm_url)),
988        ];
989
990        test_harness
991            .add_mock_piconet_members(&mut members)
992            .await
993            .expect("failed to add piconet members");
994
995        test_harness.update_routes().await.expect("should update routes");
996
997        for member in &members {
998            validate_mock_piconet_member(&test_harness.builder, member).await;
999        }
1000
1001        // Note: We don't `create()` the test realm because the `rfcomm_url` does not exist which
1002        // will cause component resolving to fail.
1003    }
1004
1005    async fn validate_profile_routes_for_member_with_rfcomm<'a>(
1006        builder: &RealmBuilder,
1007        member_spec: &'a PiconetMemberSpec,
1008    ) {
1009        // Piconet member should have an expose declaration of Profile.
1010        let profile_capability_name = bredr::ProfileMarker::PROTOCOL_NAME.to_string();
1011        let pico_member_decl = builder
1012            .get_component_decl(member_spec.name.clone())
1013            .await
1014            .expect("piconet member had no decl");
1015        let expose_profile_decl = ExposeProtocolDecl {
1016            source: ExposeSource::Self_,
1017            source_name: profile_capability_name.clone().parse().unwrap(),
1018            source_dictionary: Default::default(),
1019            target: ExposeTarget::Parent,
1020            target_name: profile_capability_name.clone().parse().unwrap(),
1021            availability: cm_rust::Availability::Required,
1022        };
1023        let expose_decl = ExposeDecl::Protocol(expose_profile_decl.clone());
1024        assert!(pico_member_decl.exposes.contains(&expose_decl));
1025
1026        // Root should have an expose declaration for `Profile` at the custom path.
1027        {
1028            let bt_rfcomm_name = super::bt_rfcomm_moniker_for_member(&member_spec.name);
1029            let custom_profile_capability_name =
1030                Name::new(super::capability_path_for_mock::<bredr::ProfileMarker>(&member_spec))
1031                    .unwrap();
1032            let custom_expose_profile_decl = ExposeProtocolDecl {
1033                source: ExposeSource::Child(bt_rfcomm_name.parse().unwrap()),
1034                source_name: profile_capability_name.parse().unwrap(),
1035                source_dictionary: Default::default(),
1036                target: ExposeTarget::Parent,
1037                target_name: custom_profile_capability_name,
1038                availability: cm_rust::Availability::Required,
1039            };
1040            let root_expose_decl = ExposeDecl::Protocol(custom_expose_profile_decl);
1041            let root = builder.get_realm_decl().await.expect("failed to get root");
1042            assert!(root.exposes.contains(&root_expose_decl));
1043        }
1044    }
1045
1046    async fn validate_profile_routes_for_member<'a>(
1047        builder: &RealmBuilder,
1048        member_spec: &'a PiconetMemberSpec,
1049    ) {
1050        // Check that the mock piconet member has an expose declaration for the profile protocol
1051        let pico_member_decl = builder
1052            .get_component_decl(member_spec.name.clone())
1053            .await
1054            .expect("piconet member had no decl");
1055        let profile_capability_name =
1056            Name::new(super::capability_path_for_mock::<bredr::ProfileMarker>(&member_spec))
1057                .unwrap();
1058        let mut expose_proto_decl = ExposeProtocolDecl {
1059            source: ExposeSource::Self_,
1060            source_name: profile_capability_name.clone(),
1061            source_dictionary: Default::default(),
1062            target: ExposeTarget::Parent,
1063            target_name: profile_capability_name,
1064            availability: cm_rust::Availability::Required,
1065        };
1066        let expose_decl = ExposeDecl::Protocol(expose_proto_decl.clone());
1067        assert!(pico_member_decl.exposes.contains(&expose_decl));
1068
1069        // root should have a similar-looking expose declaration for Profile, only the source
1070        // should be the child in question
1071        {
1072            expose_proto_decl.source = ExposeSource::Child(member_spec.name.parse().unwrap());
1073            let root_expose_decl = ExposeDecl::Protocol(expose_proto_decl);
1074            let root = builder.get_realm_decl().await.expect("failed to get root");
1075            assert!(root.exposes.contains(&root_expose_decl));
1076        }
1077    }
1078
1079    async fn validate_mock_piconet_member<'a>(
1080        builder: &RealmBuilder,
1081        member_spec: &'a PiconetMemberSpec,
1082    ) {
1083        // check that the piconet member exists
1084        assert_realm_contains(builder, &member_spec.name).await;
1085
1086        // Validate the `bredr.Profile` related routes.
1087        if member_spec.rfcomm_url.is_some() {
1088            validate_profile_routes_for_member_with_rfcomm(builder, member_spec).await;
1089        } else {
1090            validate_profile_routes_for_member(builder, member_spec).await;
1091        }
1092
1093        // check that the piconet member has a use declaration for ProfileTest
1094        let pico_member_decl = builder
1095            .get_component_decl(member_spec.name.clone())
1096            .await
1097            .expect("piconet member had no decl");
1098        let use_decl = UseDecl::Protocol(UseProtocolDecl {
1099            source: UseSource::Parent,
1100            source_name: bredr_test::ProfileTestMarker::PROTOCOL_NAME.parse().unwrap(),
1101            source_dictionary: Default::default(),
1102            target_path: Some(
1103                format!("/svc/{}", bredr_test::ProfileTestMarker::PROTOCOL_NAME).parse().unwrap(),
1104            ),
1105            numbered_handle: None,
1106            dependency_type: DependencyType::Strong,
1107            availability: Availability::Required,
1108        });
1109        assert!(pico_member_decl.uses.contains(&use_decl));
1110
1111        // Check that the root offers ProfileTest to the piconet member from
1112        // the Mock Piconet Server
1113        let profile_test_name = Name::new(bredr_test::ProfileTestMarker::PROTOCOL_NAME).unwrap();
1114        let root = builder.get_realm_decl().await.expect("failed to get root");
1115        let offer_profile_test = OfferDecl::Protocol(OfferProtocolDecl {
1116            source: offer_source_static_child(&super::mock_piconet_server_moniker()),
1117            source_name: profile_test_name.clone(),
1118            source_dictionary: Default::default(),
1119            target: offer_target_static_child(&member_spec.name),
1120            target_name: profile_test_name,
1121            dependency_type: DependencyType::Strong,
1122            availability: Availability::Required,
1123        });
1124        assert!(root.offers.contains(&offer_profile_test));
1125
1126        // We don't check that the Mock Piconet Server exposes ProfileTest
1127        // because the builder won't actually know if this is true until it
1128        // resolves the component URL. We assume other tests validate the
1129        // Mock Piconet Server has this expose.
1130    }
1131
1132    #[fuchsia::test]
1133    async fn test_add_profile() {
1134        let mut test_harness = PiconetHarness::new().await;
1135        let profile_name = "test-profile-member";
1136        let interposer_name = super::interposer_name_for_profile(profile_name);
1137
1138        // Add a profile with a fake URL
1139        let _profile_member = test_harness
1140            .add_profile(
1141                profile_name.to_string(),
1142                "fuchsia-pkg://fuchsia.com/example#meta/example.cm".to_string(),
1143            )
1144            .await
1145            .expect("failed to add profile");
1146
1147        test_harness.update_routes().await.expect("should update routes");
1148        assert_realm_contains(&test_harness.builder, &profile_name).await;
1149        assert_realm_contains(&test_harness.builder, &interposer_name).await;
1150
1151        // validate routes
1152
1153        // Profile is exposed by interposer
1154        let profile_capability_name = Name::new(bredr::ProfileMarker::PROTOCOL_NAME).unwrap();
1155        let profile_expose = ExposeDecl::Protocol(ExposeProtocolDecl {
1156            source: ExposeSource::Self_,
1157            source_name: profile_capability_name.clone(),
1158            source_dictionary: Default::default(),
1159            target: ExposeTarget::Parent,
1160            target_name: profile_capability_name.clone(),
1161            availability: cm_rust::Availability::Required,
1162        });
1163        let interposer = test_harness
1164            .builder
1165            .get_component_decl(interposer_name.clone())
1166            .await
1167            .expect("interposer not found!");
1168        assert!(interposer.exposes.contains(&profile_expose));
1169
1170        // ProfileTest is used by interposer
1171        let profile_test_name = Name::new(bredr_test::ProfileTestMarker::PROTOCOL_NAME).unwrap();
1172        let profile_test_use = UseDecl::Protocol(UseProtocolDecl {
1173            source: UseSource::Parent,
1174            source_name: profile_test_name.clone(),
1175            source_dictionary: Default::default(),
1176            target_path: Some(
1177                format!("/svc/{}", bredr_test::ProfileTestMarker::PROTOCOL_NAME).parse().unwrap(),
1178            ),
1179            numbered_handle: None,
1180            dependency_type: DependencyType::Strong,
1181            availability: Availability::Required,
1182        });
1183        assert!(interposer.uses.contains(&profile_test_use));
1184
1185        // Profile is offered by root to profile from interposer
1186        let profile_offer = OfferDecl::Protocol(OfferProtocolDecl {
1187            source: offer_source_static_child(&interposer_name),
1188            source_name: profile_capability_name.clone(),
1189            source_dictionary: Default::default(),
1190            target: offer_target_static_child(&profile_name),
1191            target_name: profile_capability_name.clone(),
1192            dependency_type: DependencyType::Strong,
1193            availability: Availability::Required,
1194        });
1195        let root = test_harness.builder.get_realm_decl().await.expect("unable to get root decl");
1196        assert!(root.offers.contains(&profile_offer));
1197
1198        // ProfileTest is offered by root to interposer from Mock Piconet Server
1199        let profile_test_offer = OfferDecl::Protocol(OfferProtocolDecl {
1200            source: offer_source_static_child(&super::mock_piconet_server_moniker()),
1201            source_name: profile_test_name.clone(),
1202            source_dictionary: Default::default(),
1203            target: offer_target_static_child(&interposer_name),
1204            target_name: profile_test_name.clone(),
1205            dependency_type: DependencyType::Strong,
1206            availability: Availability::Required,
1207        });
1208        assert!(root.offers.contains(&profile_test_offer));
1209
1210        // LogSink is offered by test root to interposer and profile.
1211        let log_capability_name = Name::new(LogSinkMarker::PROTOCOL_NAME).unwrap();
1212        let log_offer = OfferDecl::Protocol(OfferProtocolDecl {
1213            source: OfferSource::Parent,
1214            source_name: log_capability_name.clone(),
1215            source_dictionary: Default::default(),
1216            target: offer_target_static_child(&profile_name),
1217            target_name: log_capability_name.clone(),
1218            dependency_type: DependencyType::Strong,
1219            availability: Availability::Required,
1220        });
1221        assert!(root.offers.contains(&log_offer));
1222    }
1223
1224    #[fuchsia::test]
1225    async fn test_add_profile_with_rfcomm() {
1226        let mut test_harness = PiconetHarness::new().await;
1227
1228        let profile_name = "test-profile-member";
1229        let interposer_name = super::interposer_name_for_profile(profile_name);
1230        let bt_rfcomm_name = super::bt_rfcomm_moniker_for_member(profile_name);
1231        let profile_url = "fuchsia-pkg://fuchsia.com/example#meta/example.cm".to_string();
1232        let rfcomm_url = "fuchsia-pkg://fuchsia.com/example#meta/bt-rfcomm.cm".to_string();
1233
1234        // Add a profile with a fake URL
1235        let _profile_member = test_harness
1236            .add_profile_with_capabilities(
1237                profile_name.to_string(),
1238                profile_url,
1239                Some(rfcomm_url),
1240                vec![],
1241                vec![],
1242            )
1243            .await
1244            .expect("failed to add profile");
1245
1246        test_harness.update_routes().await.expect("should update routes");
1247        assert_realm_contains(&test_harness.builder, &profile_name).await;
1248        assert_realm_contains(&test_harness.builder, &interposer_name).await;
1249        assert_realm_contains(&test_harness.builder, &bt_rfcomm_name).await;
1250
1251        // validate routes
1252        let profile_capability_name = Name::new(bredr::ProfileMarker::PROTOCOL_NAME).unwrap();
1253
1254        // `Profile` is offered by root to bt-rfcomm from interposer.
1255        let profile_offer1 = OfferDecl::Protocol(OfferProtocolDecl {
1256            source: offer_source_static_child(&interposer_name),
1257            source_name: profile_capability_name.clone(),
1258            source_dictionary: Default::default(),
1259            target: offer_target_static_child(&bt_rfcomm_name),
1260            target_name: profile_capability_name.clone(),
1261            dependency_type: DependencyType::Strong,
1262            availability: Availability::Required,
1263        });
1264        // `Profile` is offered from bt-rfcomm to profile.
1265        let profile_offer2 = OfferDecl::Protocol(OfferProtocolDecl {
1266            source: offer_source_static_child(&bt_rfcomm_name),
1267            source_name: profile_capability_name.clone(),
1268            source_dictionary: Default::default(),
1269            target: offer_target_static_child(&profile_name),
1270            target_name: profile_capability_name.clone(),
1271            dependency_type: DependencyType::Strong,
1272            availability: Availability::Required,
1273        });
1274        let root = test_harness.builder.get_realm_decl().await.expect("unable to get root decl");
1275        assert!(root.offers.contains(&profile_offer1));
1276        assert!(root.offers.contains(&profile_offer2));
1277    }
1278
1279    #[fuchsia::test]
1280    async fn test_add_profile_with_additional_capabilities() {
1281        let mut test_harness = PiconetHarness::new().await;
1282        let profile_name = "test-profile-member";
1283
1284        // Add a profile with a fake URL and some fake use & expose capabilities.
1285        let fake_cap1 = "Foo".to_string();
1286        let fake_cap2 = "Bar".to_string();
1287        let expose_capabilities = vec![
1288            Capability::protocol_by_name(fake_cap1.clone()).into(),
1289            Capability::protocol_by_name(fake_cap2.clone()).into(),
1290        ];
1291        let fake_cap3 = "Cat".to_string();
1292        let use_capabilities = vec![Capability::protocol_by_name(fake_cap3.clone()).into()];
1293        let profile_member = test_harness
1294            .add_profile_with_capabilities(
1295                profile_name.to_string(),
1296                "fuchsia-pkg://fuchsia.com/example#meta/example.cm".to_string(),
1297                None,
1298                use_capabilities,
1299                expose_capabilities,
1300            )
1301            .await
1302            .expect("failed to add profile");
1303
1304        test_harness.update_routes().await.expect("should update routes");
1305        assert_realm_contains(&test_harness.builder, &profile_name).await;
1306
1307        // Validate the additional capability routes. See `test_add_profile` for validation
1308        // of Profile, ProfileTest, and LogSink routes.
1309
1310        // `Foo` is exposed by the profile to parent.
1311        let fake_capability_expose1 = ExposeDecl::Protocol(ExposeProtocolDecl {
1312            source: ExposeSource::Child(profile_name.parse().unwrap()),
1313            source_name: fake_cap1.clone().parse().unwrap(),
1314            source_dictionary: Default::default(),
1315            target: ExposeTarget::Parent,
1316            target_name: capability_path_for_peer_id(profile_member.peer_id(), &fake_cap1)
1317                .parse()
1318                .unwrap(),
1319            availability: cm_rust::Availability::Required,
1320        });
1321        // `Bar` is exposed by the profile to parent.
1322        let fake_capability_expose2 = ExposeDecl::Protocol(ExposeProtocolDecl {
1323            source: ExposeSource::Child(profile_name.parse().unwrap()),
1324            source_name: fake_cap2.clone().parse().unwrap(),
1325            source_dictionary: Default::default(),
1326            target: ExposeTarget::Parent,
1327            target_name: capability_path_for_peer_id(profile_member.peer_id(), &fake_cap2)
1328                .parse()
1329                .unwrap(),
1330            availability: cm_rust::Availability::Required,
1331        });
1332        // `Cat` is used by the profile and exposed from above the test root.
1333        let fake_capability_offer3 = OfferDecl::Protocol(OfferProtocolDecl {
1334            source: OfferSource::Parent,
1335            source_name: fake_cap3.clone().parse().unwrap(),
1336            source_dictionary: Default::default(),
1337            target: offer_target_static_child(&profile_name),
1338            target_name: fake_cap3.parse().unwrap(),
1339            dependency_type: DependencyType::Strong,
1340            availability: Availability::Required,
1341        });
1342
1343        let root = test_harness.builder.get_realm_decl().await.expect("unable to get root decl");
1344        assert!(root.exposes.contains(&fake_capability_expose1));
1345        assert!(root.exposes.contains(&fake_capability_expose2));
1346        assert!(root.offers.contains(&fake_capability_offer3));
1347    }
1348
1349    #[fuchsia::test]
1350    async fn test_multiple_profiles_with_same_expose_is_ok() {
1351        let mut test_harness = PiconetHarness::new().await;
1352        let profile_name1 = "test-profile-1";
1353        let profile_name2 = "test-profile-2";
1354
1355        // Both profiles expose the same protocol capability.
1356        let fake_cap = "FooBarAmazing".to_string();
1357        let expose_capabilities = vec![Capability::protocol_by_name(fake_cap.clone()).into()];
1358
1359        let profile_member1 = test_harness
1360            .add_profile_with_capabilities(
1361                profile_name1.to_string(),
1362                "fuchsia-pkg://fuchsia.com/example#meta/example-profile1.cm".to_string(),
1363                None,
1364                vec![],
1365                expose_capabilities.clone(),
1366            )
1367            .await
1368            .expect("failed to add profile1");
1369        let profile_member2 = test_harness
1370            .add_profile_with_capabilities(
1371                profile_name2.to_string(),
1372                "fuchsia-pkg://fuchsia.com/example#meta/example-profile2.cm".to_string(),
1373                None,
1374                vec![],
1375                expose_capabilities,
1376            )
1377            .await
1378            .expect("failed to add profile2");
1379
1380        test_harness.update_routes().await.expect("should update routes");
1381        assert_realm_contains(&test_harness.builder, &profile_name1).await;
1382        assert_realm_contains(&test_harness.builder, &profile_name2).await;
1383
1384        // Validate that `fake_cap` is exposed by both profiles, and is OK.
1385        let profile1_expose = ExposeDecl::Protocol(ExposeProtocolDecl {
1386            source: ExposeSource::Child(profile_name1.parse().unwrap()),
1387            source_name: fake_cap.clone().parse().unwrap(),
1388            source_dictionary: Default::default(),
1389            target: ExposeTarget::Parent,
1390            target_name: capability_path_for_peer_id(profile_member1.peer_id(), &fake_cap)
1391                .parse()
1392                .unwrap(),
1393            availability: cm_rust::Availability::Required,
1394        });
1395        let profile2_expose = ExposeDecl::Protocol(ExposeProtocolDecl {
1396            source: ExposeSource::Child(profile_name2.parse().unwrap()),
1397            source_name: fake_cap.clone().parse().unwrap(),
1398            source_dictionary: Default::default(),
1399            target: ExposeTarget::Parent,
1400            target_name: capability_path_for_peer_id(profile_member2.peer_id(), &fake_cap)
1401                .parse()
1402                .unwrap(),
1403            availability: cm_rust::Availability::Required,
1404        });
1405
1406        let root = test_harness.builder.get_realm_decl().await.expect("unable to get root decl");
1407        assert!(root.exposes.contains(&profile1_expose));
1408        assert!(root.exposes.contains(&profile2_expose));
1409    }
1410}