blob: 9e594a63bdb3cdaf14b5c66086e7e4e1a301e836 [file] [edit]
// Copyright 2023 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use cm_rust::offer::OfferDeclCommon;
use cm_rust::{ExposeDeclCommon, NativeIntoFidl};
use flex_client::ProxyHasDomain;
use flex_client::fidl::{ClientEnd, DiscoverableProtocolMarker};
use flex_fuchsia_component as fcomponent;
use flex_fuchsia_component_decl::Offer;
use flex_fuchsia_io as fio;
use flex_fuchsia_sys2 as fsys;
use moniker::Moniker;
use thiserror::Error;
const CAPABILITY_REQUESTED_EVENT: &str = "capability_requested";
#[derive(Debug, Error)]
pub enum RealmValidationError {
#[error("Realm should expose {}", fcomponent::RealmMarker::PROTOCOL_NAME)]
RealmProtocol,
#[error(
"Realm should offer {} event stream to the test collection",
CAPABILITY_REQUESTED_EVENT
)]
CapabilityRequested,
#[error("The realm does not contain '{0}' named collection")]
TestCollectionNotFound(String),
}
#[derive(Debug, Error)]
pub enum RealmError {
#[error(transparent)]
Fidl(#[from] fidl::Error),
#[error(transparent)]
Validation(#[from] RealmValidationError),
#[error("Invalid realm, it should contain test collection: /realm/collection")]
InvalidRealmStr,
#[error("cannot resolve provided realm: {0:?}")]
InstanceNotResolved(component_debug::lifecycle::ResolveError),
#[error(transparent)]
BadMoniker(#[from] moniker::MonikerError),
#[error("Cannot connect to exposed directory: {0:?}")]
ConnectExposedDir(fsys::OpenError),
#[error(transparent)]
GetManifest(#[from] component_debug::realm::GetDeclarationError),
}
pub struct Realm {
exposed_dir: fio::DirectoryProxy,
offers: Vec<flex_fuchsia_component_decl::Offer>,
realm_str: String,
test_collection: String,
}
impl std::fmt::Debug for Realm {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!("realm for moniker {}:{}", self.realm_str, self.test_collection))
}
}
impl PartialEq for Realm {
fn eq(&self, other: &Self) -> bool {
self.realm_str == other.realm_str && self.test_collection == other.test_collection
}
}
impl Realm {
pub fn get_realm_client(&self) -> Result<ClientEnd<fcomponent::RealmMarker>, fidl::Error> {
let (realm_client, server_end) =
self.exposed_dir.domain().create_endpoints::<fcomponent::RealmMarker>();
self.exposed_dir.open(
fcomponent::RealmMarker::PROTOCOL_NAME,
fio::Flags::PROTOCOL_SERVICE,
&Default::default(),
server_end.into_channel(),
)?;
Ok(realm_client)
}
pub fn offers(&self) -> Vec<flex_fuchsia_component_decl::Offer> {
self.offers.clone()
}
pub fn collection<'a>(&'a self) -> &'a str {
self.test_collection.as_str()
}
}
fn validate_and_get_offers(
manifest: cm_rust::ComponentDecl,
test_collection: &str,
) -> Result<Vec<Offer>, RealmValidationError> {
let collection_found = manifest.collections.iter().any(|c| c.name == test_collection);
if !collection_found {
return Err(RealmValidationError::TestCollectionNotFound(test_collection.to_string()));
}
let exposes_realm_protocol =
manifest.exposes.iter().any(|e| *e.target_name() == fcomponent::RealmMarker::PROTOCOL_NAME);
if !exposes_realm_protocol {
return Err(RealmValidationError::RealmProtocol);
}
let mut capability_requested = false;
let mut offers = vec![];
for offer in manifest.offers {
if let cm_rust::offer::OfferTarget::Collection(collection) = &offer.target() {
if collection.as_str() != test_collection {
continue;
}
if let cm_rust::offer::OfferDecl::EventStream(decl) = &offer {
let cm_rust::offer::OfferEventStreamDecl { target_name, source, scope, .. } =
&**decl;
if *target_name == CAPABILITY_REQUESTED_EVENT
&& source == &cm_rust::offer::OfferSource::Parent
&& scope
.as_ref()
.map(|s| {
s.iter().any(|s| match s {
cm_rust::EventScope::Collection(s) => s.as_str() == test_collection,
_ => false,
})
})
.unwrap_or(false)
{
capability_requested =
capability_requested || *target_name == CAPABILITY_REQUESTED_EVENT;
}
}
offers.push(offer.native_into_fidl());
}
}
if !capability_requested {
return Err(RealmValidationError::CapabilityRequested);
}
Ok(offers)
}
pub async fn parse_provided_realm(
lifecycle_controller: &fsys::LifecycleControllerProxy,
realm_query: &fsys::RealmQueryProxy,
realm_str: &str,
) -> Result<Realm, RealmError> {
let (mut moniker, mut test_collection) = match realm_str.rsplit_once('/') {
Some(s) => s,
None => {
return Err(RealmError::InvalidRealmStr);
}
};
// Support old way of parsing realm.
if test_collection.contains(":") {
(moniker, test_collection) = match realm_str.rsplit_once(':') {
Some(s @ (moniker_head, collection_name)) => {
eprintln!(
"You are using old realm format. Please switch to standard realm moniker format: '{}/{}'",
moniker_head, collection_name
);
s
}
None => {
return Err(RealmError::InvalidRealmStr);
}
};
}
if moniker == "" {
return Err(RealmError::InvalidRealmStr);
}
let moniker = Moniker::try_from(moniker)?;
component_debug::lifecycle::resolve_instance(&lifecycle_controller, &moniker)
.await
.map_err(RealmError::InstanceNotResolved)?;
let manifest = component_debug::realm::get_resolved_declaration(&moniker, &realm_query).await?;
let offers = validate_and_get_offers(manifest, test_collection)?;
let (exposed_dir, server_end) = realm_query.domain().create_proxy();
realm_query
.open_directory(moniker.as_ref(), fsys::OpenDirType::ExposedDir, server_end)
.await?
.map_err(RealmError::ConnectExposedDir)?;
Ok(Realm {
exposed_dir,
offers,
realm_str: realm_str.to_string(),
test_collection: test_collection.to_owned(),
})
}
#[cfg(test)]
mod test {
use super::*;
use assert_matches::assert_matches;
use cm_rust::FidlIntoNative;
#[fuchsia::test]
async fn valid_realm() {
let lifecycle_controller =
fuchsia_component::client::connect_to_protocol::<fsys::LifecycleControllerMarker>()
.unwrap();
let realm_query =
fuchsia_component::client::connect_to_protocol::<fsys::RealmQueryMarker>().unwrap();
let realm =
parse_provided_realm(&lifecycle_controller, &realm_query, "/test_realm/echo_test_coll")
.await
.unwrap();
assert_eq!(realm.test_collection, "echo_test_coll");
assert_eq!(realm.realm_str, "/test_realm/echo_test_coll");
let offers: Vec<cm_rust::OfferDecl> =
realm.offers.into_iter().map(|o| o.fidl_into_native()).collect::<Vec<_>>();
assert_eq!(offers.len(), 3, "{:?}", offers);
offers.iter().for_each(|o| {
assert_eq!(
o.target(),
&cm_rust::offer::OfferTarget::Collection("echo_test_coll".parse().unwrap())
)
});
assert!(offers.iter().any(|o| *o.target_name() == CAPABILITY_REQUESTED_EVENT));
assert!(offers.iter().any(|o| *o.target_name() == "fidl.examples.routing.echo.Echo"));
let realm = parse_provided_realm(
&lifecycle_controller,
&realm_query,
"/test_realm/hermetic_test_coll",
)
.await
.unwrap();
assert_eq!(realm.test_collection, "hermetic_test_coll");
assert_eq!(realm.realm_str, "/test_realm/hermetic_test_coll");
let offers = realm.offers.into_iter().map(|o| o.fidl_into_native()).collect::<Vec<_>>();
assert_eq!(offers.len(), 2, "{:?}", offers);
offers.iter().for_each(|o: &cm_rust::OfferDecl| {
assert_eq!(
o.target(),
&cm_rust::offer::OfferTarget::Collection("hermetic_test_coll".parse().unwrap())
)
});
assert!(offers.iter().any(|o| *o.target_name() == CAPABILITY_REQUESTED_EVENT));
}
#[fuchsia::test]
async fn invalid_realm() {
let lifecycle_controller =
fuchsia_component::client::connect_to_protocol::<fsys::LifecycleControllerMarker>()
.unwrap();
let realm_query =
fuchsia_component::client::connect_to_protocol::<fsys::RealmQueryMarker>().unwrap();
assert_matches!(
parse_provided_realm(
&lifecycle_controller,
&realm_query,
"/nonexistent_realm/test_coll"
)
.await,
Err(RealmError::InstanceNotResolved(_))
);
assert_matches!(
parse_provided_realm(&lifecycle_controller, &realm_query, "/test_realm").await,
Err(RealmError::InvalidRealmStr)
);
assert_matches!(
parse_provided_realm(&lifecycle_controller, &realm_query, "/test_realm/invalid_col")
.await,
Err(RealmError::Validation(RealmValidationError::TestCollectionNotFound(_)))
);
assert_matches!(
parse_provided_realm(
&lifecycle_controller,
&realm_query,
"/test_realm/no_capability_requested_event"
)
.await,
Err(RealmError::Validation(RealmValidationError::CapabilityRequested))
);
assert_matches!(
parse_provided_realm(
&lifecycle_controller,
&realm_query,
"/no_realm_protocol_realm/hermetic_test_coll"
)
.await,
Err(RealmError::Validation(RealmValidationError::RealmProtocol))
);
}
}