1use 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 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}