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