Skip to main content

run_test_suite_lib/
realm.rs

1// Copyright 2023 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 cm_rust::offer::OfferDeclCommon;
6use cm_rust::{ExposeDeclCommon, NativeIntoFidl};
7use flex_client::ProxyHasDomain;
8use flex_client::fidl::{ClientEnd, DiscoverableProtocolMarker};
9use flex_fuchsia_component as fcomponent;
10use flex_fuchsia_component_decl::Offer;
11use flex_fuchsia_io as fio;
12use flex_fuchsia_sys2 as fsys;
13use moniker::Moniker;
14use thiserror::Error;
15const CAPABILITY_REQUESTED_EVENT: &str = "capability_requested";
16
17#[derive(Debug, Error)]
18pub enum RealmValidationError {
19    #[error("Realm should expose {}", fcomponent::RealmMarker::PROTOCOL_NAME)]
20    RealmProtocol,
21
22    #[error(
23        "Realm should offer {} event stream to the test collection",
24        CAPABILITY_REQUESTED_EVENT
25    )]
26    CapabilityRequested,
27
28    #[error("The realm does not contain '{0}' named collection")]
29    TestCollectionNotFound(String),
30}
31
32#[derive(Debug, Error)]
33pub enum RealmError {
34    #[error(transparent)]
35    Fidl(#[from] fidl::Error),
36
37    #[error(transparent)]
38    Validation(#[from] RealmValidationError),
39
40    #[error("Invalid realm, it should contain test collection: /realm/collection")]
41    InvalidRealmStr,
42
43    #[error("cannot resolve provided realm: {0:?}")]
44    InstanceNotResolved(component_debug::lifecycle::ResolveError),
45
46    #[error(transparent)]
47    BadMoniker(#[from] moniker::MonikerError),
48
49    #[error("Cannot connect to exposed directory: {0:?}")]
50    ConnectExposedDir(fsys::OpenError),
51
52    #[error(transparent)]
53    GetManifest(#[from] component_debug::realm::GetDeclarationError),
54}
55
56pub struct Realm {
57    exposed_dir: fio::DirectoryProxy,
58    offers: Vec<flex_fuchsia_component_decl::Offer>,
59    realm_str: String,
60    test_collection: String,
61}
62
63impl std::fmt::Debug for Realm {
64    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65        f.write_fmt(format_args!("realm for moniker {}:{}", self.realm_str, self.test_collection))
66    }
67}
68
69impl PartialEq for Realm {
70    fn eq(&self, other: &Self) -> bool {
71        self.realm_str == other.realm_str && self.test_collection == other.test_collection
72    }
73}
74
75impl Realm {
76    pub fn get_realm_client(&self) -> Result<ClientEnd<fcomponent::RealmMarker>, fidl::Error> {
77        let (realm_client, server_end) =
78            self.exposed_dir.domain().create_endpoints::<fcomponent::RealmMarker>();
79        self.exposed_dir.open(
80            fcomponent::RealmMarker::PROTOCOL_NAME,
81            fio::Flags::PROTOCOL_SERVICE,
82            &Default::default(),
83            server_end.into_channel(),
84        )?;
85        Ok(realm_client)
86    }
87
88    pub fn offers(&self) -> Vec<flex_fuchsia_component_decl::Offer> {
89        self.offers.clone()
90    }
91
92    pub fn collection<'a>(&'a self) -> &'a str {
93        self.test_collection.as_str()
94    }
95}
96
97fn validate_and_get_offers(
98    manifest: cm_rust::ComponentDecl,
99    test_collection: &str,
100) -> Result<Vec<Offer>, RealmValidationError> {
101    let collection_found = manifest.collections.iter().any(|c| c.name == test_collection);
102    if !collection_found {
103        return Err(RealmValidationError::TestCollectionNotFound(test_collection.to_string()));
104    }
105
106    let exposes_realm_protocol =
107        manifest.exposes.iter().any(|e| *e.target_name() == fcomponent::RealmMarker::PROTOCOL_NAME);
108    if !exposes_realm_protocol {
109        return Err(RealmValidationError::RealmProtocol);
110    }
111
112    let mut capability_requested = false;
113    let mut offers = vec![];
114    for offer in manifest.offers {
115        if let cm_rust::offer::OfferTarget::Collection(collection) = &offer.target() {
116            if collection.as_str() != test_collection {
117                continue;
118            }
119
120            if let cm_rust::offer::OfferDecl::EventStream(cm_rust::offer::OfferEventStreamDecl {
121                target_name,
122                source,
123                scope,
124                ..
125            }) = &offer
126            {
127                if *target_name == CAPABILITY_REQUESTED_EVENT
128                    && source == &cm_rust::offer::OfferSource::Parent
129                    && scope
130                        .as_ref()
131                        .map(|s| {
132                            s.iter().any(|s| match s {
133                                cm_rust::EventScope::Collection(s) => s.as_str() == test_collection,
134                                _ => false,
135                            })
136                        })
137                        .unwrap_or(false)
138                {
139                    capability_requested =
140                        capability_requested || *target_name == CAPABILITY_REQUESTED_EVENT;
141                }
142            }
143            offers.push(offer.native_into_fidl());
144        }
145    }
146    if !capability_requested {
147        return Err(RealmValidationError::CapabilityRequested);
148    }
149    Ok(offers)
150}
151
152pub async fn parse_provided_realm(
153    lifecycle_controller: &fsys::LifecycleControllerProxy,
154    realm_query: &fsys::RealmQueryProxy,
155    realm_str: &str,
156) -> Result<Realm, RealmError> {
157    let (mut moniker, mut test_collection) = match realm_str.rsplit_once('/') {
158        Some(s) => s,
159        None => {
160            return Err(RealmError::InvalidRealmStr);
161        }
162    };
163    // Support old way of parsing realm.
164    if test_collection.contains(":") {
165        (moniker, test_collection) = match realm_str.rsplit_once(':') {
166            Some(s @ (moniker_head, collection_name)) => {
167                eprintln!(
168                    "You are using old realm format. Please switch to standard realm moniker format: '{}/{}'",
169                    moniker_head, collection_name
170                );
171                s
172            }
173            None => {
174                return Err(RealmError::InvalidRealmStr);
175            }
176        };
177    }
178    if moniker == "" {
179        return Err(RealmError::InvalidRealmStr);
180    }
181    let moniker = Moniker::try_from(moniker)?;
182
183    component_debug::lifecycle::resolve_instance(&lifecycle_controller, &moniker)
184        .await
185        .map_err(RealmError::InstanceNotResolved)?;
186
187    let manifest = component_debug::realm::get_resolved_declaration(&moniker, &realm_query).await?;
188
189    let offers = validate_and_get_offers(manifest, test_collection)?;
190
191    let (exposed_dir, server_end) = realm_query.domain().create_proxy();
192    realm_query
193        .open_directory(moniker.as_ref(), fsys::OpenDirType::ExposedDir, server_end)
194        .await?
195        .map_err(RealmError::ConnectExposedDir)?;
196
197    Ok(Realm {
198        exposed_dir,
199        offers,
200        realm_str: realm_str.to_string(),
201        test_collection: test_collection.to_owned(),
202    })
203}
204
205#[cfg(test)]
206mod test {
207    use super::*;
208    use assert_matches::assert_matches;
209    use cm_rust::FidlIntoNative;
210
211    #[fuchsia::test]
212    async fn valid_realm() {
213        let lifecycle_controller =
214            fuchsia_component::client::connect_to_protocol::<fsys::LifecycleControllerMarker>()
215                .unwrap();
216        let realm_query =
217            fuchsia_component::client::connect_to_protocol::<fsys::RealmQueryMarker>().unwrap();
218        let realm =
219            parse_provided_realm(&lifecycle_controller, &realm_query, "/test_realm/echo_test_coll")
220                .await
221                .unwrap();
222
223        assert_eq!(realm.test_collection, "echo_test_coll");
224        assert_eq!(realm.realm_str, "/test_realm/echo_test_coll");
225
226        let offers = realm.offers.into_iter().map(|o| o.fidl_into_native()).collect::<Vec<_>>();
227        assert_eq!(offers.len(), 3, "{:?}", offers);
228        offers.iter().for_each(|o| {
229            assert_eq!(
230                o.target(),
231                &cm_rust::offer::OfferTarget::Collection("echo_test_coll".parse().unwrap())
232            )
233        });
234        assert!(offers.iter().any(|o| *o.target_name() == CAPABILITY_REQUESTED_EVENT));
235        assert!(offers.iter().any(|o| *o.target_name() == "fidl.examples.routing.echo.Echo"));
236
237        let realm = parse_provided_realm(
238            &lifecycle_controller,
239            &realm_query,
240            "/test_realm/hermetic_test_coll",
241        )
242        .await
243        .unwrap();
244
245        assert_eq!(realm.test_collection, "hermetic_test_coll");
246        assert_eq!(realm.realm_str, "/test_realm/hermetic_test_coll");
247
248        let offers = realm.offers.into_iter().map(|o| o.fidl_into_native()).collect::<Vec<_>>();
249        assert_eq!(offers.len(), 2, "{:?}", offers);
250        offers.iter().for_each(|o| {
251            assert_eq!(
252                o.target(),
253                &cm_rust::offer::OfferTarget::Collection("hermetic_test_coll".parse().unwrap())
254            )
255        });
256        assert!(offers.iter().any(|o| *o.target_name() == CAPABILITY_REQUESTED_EVENT));
257    }
258
259    #[fuchsia::test]
260    async fn invalid_realm() {
261        let lifecycle_controller =
262            fuchsia_component::client::connect_to_protocol::<fsys::LifecycleControllerMarker>()
263                .unwrap();
264        let realm_query =
265            fuchsia_component::client::connect_to_protocol::<fsys::RealmQueryMarker>().unwrap();
266        assert_matches!(
267            parse_provided_realm(
268                &lifecycle_controller,
269                &realm_query,
270                "/nonexistent_realm/test_coll"
271            )
272            .await,
273            Err(RealmError::InstanceNotResolved(_))
274        );
275
276        assert_matches!(
277            parse_provided_realm(&lifecycle_controller, &realm_query, "/test_realm").await,
278            Err(RealmError::InvalidRealmStr)
279        );
280
281        assert_matches!(
282            parse_provided_realm(&lifecycle_controller, &realm_query, "/test_realm/invalid_col")
283                .await,
284            Err(RealmError::Validation(RealmValidationError::TestCollectionNotFound(_)))
285        );
286
287        assert_matches!(
288            parse_provided_realm(
289                &lifecycle_controller,
290                &realm_query,
291                "/test_realm/no_capability_requested_event"
292            )
293            .await,
294            Err(RealmError::Validation(RealmValidationError::CapabilityRequested))
295        );
296
297        assert_matches!(
298            parse_provided_realm(
299                &lifecycle_controller,
300                &realm_query,
301                "/no_realm_protocol_realm/hermetic_test_coll"
302            )
303            .await,
304            Err(RealmError::Validation(RealmValidationError::RealmProtocol))
305        );
306    }
307}