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(decl) = &offer {
121                let cm_rust::offer::OfferEventStreamDecl { target_name, source, scope, .. } =
122                    &**decl;
123
124                if *target_name == CAPABILITY_REQUESTED_EVENT
125                    && source == &cm_rust::offer::OfferSource::Parent
126                    && scope
127                        .as_ref()
128                        .map(|s| {
129                            s.iter().any(|s| match s {
130                                cm_rust::EventScope::Collection(s) => s.as_str() == test_collection,
131                                _ => false,
132                            })
133                        })
134                        .unwrap_or(false)
135                {
136                    capability_requested =
137                        capability_requested || *target_name == CAPABILITY_REQUESTED_EVENT;
138                }
139            }
140            offers.push(offer.native_into_fidl());
141        }
142    }
143    if !capability_requested {
144        return Err(RealmValidationError::CapabilityRequested);
145    }
146    Ok(offers)
147}
148
149pub async fn parse_provided_realm(
150    lifecycle_controller: &fsys::LifecycleControllerProxy,
151    realm_query: &fsys::RealmQueryProxy,
152    realm_str: &str,
153) -> Result<Realm, RealmError> {
154    let (mut moniker, mut test_collection) = match realm_str.rsplit_once('/') {
155        Some(s) => s,
156        None => {
157            return Err(RealmError::InvalidRealmStr);
158        }
159    };
160    // Support old way of parsing realm.
161    if test_collection.contains(":") {
162        (moniker, test_collection) = match realm_str.rsplit_once(':') {
163            Some(s @ (moniker_head, collection_name)) => {
164                eprintln!(
165                    "You are using old realm format. Please switch to standard realm moniker format: '{}/{}'",
166                    moniker_head, collection_name
167                );
168                s
169            }
170            None => {
171                return Err(RealmError::InvalidRealmStr);
172            }
173        };
174    }
175    if moniker == "" {
176        return Err(RealmError::InvalidRealmStr);
177    }
178    let moniker = Moniker::try_from(moniker)?;
179
180    component_debug::lifecycle::resolve_instance(&lifecycle_controller, &moniker)
181        .await
182        .map_err(RealmError::InstanceNotResolved)?;
183
184    let manifest = component_debug::realm::get_resolved_declaration(&moniker, &realm_query).await?;
185
186    let offers = validate_and_get_offers(manifest, test_collection)?;
187
188    let (exposed_dir, server_end) = realm_query.domain().create_proxy();
189    realm_query
190        .open_directory(moniker.as_ref(), fsys::OpenDirType::ExposedDir, server_end)
191        .await?
192        .map_err(RealmError::ConnectExposedDir)?;
193
194    Ok(Realm {
195        exposed_dir,
196        offers,
197        realm_str: realm_str.to_string(),
198        test_collection: test_collection.to_owned(),
199    })
200}
201
202#[cfg(test)]
203mod test {
204    use super::*;
205    use assert_matches::assert_matches;
206    use cm_rust::FidlIntoNative;
207
208    #[fuchsia::test]
209    async fn valid_realm() {
210        let lifecycle_controller =
211            fuchsia_component::client::connect_to_protocol::<fsys::LifecycleControllerMarker>()
212                .unwrap();
213        let realm_query =
214            fuchsia_component::client::connect_to_protocol::<fsys::RealmQueryMarker>().unwrap();
215        let realm =
216            parse_provided_realm(&lifecycle_controller, &realm_query, "/test_realm/echo_test_coll")
217                .await
218                .unwrap();
219
220        assert_eq!(realm.test_collection, "echo_test_coll");
221        assert_eq!(realm.realm_str, "/test_realm/echo_test_coll");
222
223        let offers: Vec<cm_rust::OfferDecl> =
224            realm.offers.into_iter().map(|o| o.fidl_into_native()).collect::<Vec<_>>();
225        assert_eq!(offers.len(), 3, "{:?}", offers);
226        offers.iter().for_each(|o| {
227            assert_eq!(
228                o.target(),
229                &cm_rust::offer::OfferTarget::Collection("echo_test_coll".parse().unwrap())
230            )
231        });
232        assert!(offers.iter().any(|o| *o.target_name() == CAPABILITY_REQUESTED_EVENT));
233        assert!(offers.iter().any(|o| *o.target_name() == "fidl.examples.routing.echo.Echo"));
234
235        let realm = parse_provided_realm(
236            &lifecycle_controller,
237            &realm_query,
238            "/test_realm/hermetic_test_coll",
239        )
240        .await
241        .unwrap();
242
243        assert_eq!(realm.test_collection, "hermetic_test_coll");
244        assert_eq!(realm.realm_str, "/test_realm/hermetic_test_coll");
245
246        let offers = realm.offers.into_iter().map(|o| o.fidl_into_native()).collect::<Vec<_>>();
247        assert_eq!(offers.len(), 2, "{:?}", offers);
248        offers.iter().for_each(|o: &cm_rust::OfferDecl| {
249            assert_eq!(
250                o.target(),
251                &cm_rust::offer::OfferTarget::Collection("hermetic_test_coll".parse().unwrap())
252            )
253        });
254        assert!(offers.iter().any(|o| *o.target_name() == CAPABILITY_REQUESTED_EVENT));
255    }
256
257    #[fuchsia::test]
258    async fn invalid_realm() {
259        let lifecycle_controller =
260            fuchsia_component::client::connect_to_protocol::<fsys::LifecycleControllerMarker>()
261                .unwrap();
262        let realm_query =
263            fuchsia_component::client::connect_to_protocol::<fsys::RealmQueryMarker>().unwrap();
264        assert_matches!(
265            parse_provided_realm(
266                &lifecycle_controller,
267                &realm_query,
268                "/nonexistent_realm/test_coll"
269            )
270            .await,
271            Err(RealmError::InstanceNotResolved(_))
272        );
273
274        assert_matches!(
275            parse_provided_realm(&lifecycle_controller, &realm_query, "/test_realm").await,
276            Err(RealmError::InvalidRealmStr)
277        );
278
279        assert_matches!(
280            parse_provided_realm(&lifecycle_controller, &realm_query, "/test_realm/invalid_col")
281                .await,
282            Err(RealmError::Validation(RealmValidationError::TestCollectionNotFound(_)))
283        );
284
285        assert_matches!(
286            parse_provided_realm(
287                &lifecycle_controller,
288                &realm_query,
289                "/test_realm/no_capability_requested_event"
290            )
291            .await,
292            Err(RealmError::Validation(RealmValidationError::CapabilityRequested))
293        );
294
295        assert_matches!(
296            parse_provided_realm(
297                &lifecycle_controller,
298                &realm_query,
299                "/no_realm_protocol_realm/hermetic_test_coll"
300            )
301            .await,
302            Err(RealmError::Validation(RealmValidationError::RealmProtocol))
303        );
304    }
305}