component_debug/
query.rs

1// Copyright 2022 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 crate::realm::{get_all_instances, Instance};
6use anyhow::{bail, Result};
7use moniker::Moniker;
8
9use flex_fuchsia_sys2 as fsys;
10
11/// Retrieves a list of CML instances that match a given string query.
12///
13/// The string query can be a partial match on the following properties:
14/// * component moniker
15/// * component URL
16/// * component instance ID
17pub async fn get_instances_from_query(
18    query: &str,
19    realm_query: &fsys::RealmQueryProxy,
20) -> Result<Vec<Instance>> {
21    let instances = get_all_instances(realm_query).await?;
22    let query_moniker = Moniker::parse_str(&query).ok();
23
24    // Try and find instances that contain the query in any of the identifiers
25    // (moniker, URL, instance ID).
26    let mut filtered_instances: Vec<Instance> = instances
27        .into_iter()
28        .filter(|i| {
29            let url_match = i.url.contains(&query);
30            let moniker_match = i.moniker.to_string().contains(&query);
31            let normalized_query_moniker_match =
32                matches!(&query_moniker, Some(m) if i.moniker.to_string().contains(&m.to_string()));
33            let id_match = i.instance_id.as_ref().map_or(false, |id| id.contains(&query));
34            url_match || moniker_match || normalized_query_moniker_match || id_match
35        })
36        .collect();
37
38    // For stability sort the list by moniker.
39    filtered_instances.sort_by_key(|i| i.moniker.to_string());
40
41    // If the query is an exact-match of any of the results, return that
42    // result only.
43    if let Some(m) = query_moniker {
44        if let Some(matched) = filtered_instances.iter().find(|i| i.moniker == m) {
45            return Ok(vec![matched.clone()]);
46        }
47    }
48
49    Ok(filtered_instances)
50}
51
52/// Retrieves exactly one instance matching a given string query.
53///
54/// The string query can be a partial match on the following properties:
55/// * component moniker
56/// * component URL
57/// * component instance ID
58///
59/// If more than one instance matches the query, an error is thrown.
60/// If no instance matches the query, an error is thrown.
61pub async fn get_single_instance_from_query(
62    query: &str,
63    realm_query: &fsys::RealmQueryProxy,
64) -> Result<Instance> {
65    // Get all instance monikers that match the query and ensure there is only one.
66    let mut instances = get_instances_from_query(&query, &realm_query).await?;
67    if instances.len() > 1 {
68        let monikers: Vec<String> = instances.into_iter().map(|i| i.moniker.to_string()).collect();
69        let monikers = monikers.join("\n");
70        bail!("The query {:?} matches more than one component instance:\n{}\n\nTo avoid ambiguity, use one of the above monikers instead.", query, monikers);
71    }
72    if instances.is_empty() {
73        bail!("No matching component instance found for query {:?}.", query);
74    }
75    let instance = instances.remove(0);
76    Ok(instance)
77}
78
79/// Retrieves a list of CML instance monikers that will match a given string query.
80///
81/// The string query can be a partial match on the following properties:
82/// * component moniker
83/// * component URL
84/// * component instance ID
85// TODO(https://fxbug.dev/42066079): `ffx component show` should use this method to get
86// component monikers when CMX support has been deprecated.
87pub async fn get_cml_monikers_from_query(
88    query: &str,
89    realm_query: &fsys::RealmQueryProxy,
90) -> Result<Vec<Moniker>> {
91    // Special-case the root moniker since it will substring match every moniker
92    // below.
93    let query_moniker = Moniker::parse_str(&query).ok();
94    if let Some(m) = &query_moniker {
95        if m.is_root() {
96            return Ok(vec![m.clone()]);
97        }
98    }
99
100    let instances = get_instances_from_query(query, realm_query).await?;
101    let monikers: Vec<Moniker> = instances.into_iter().map(|i| i.moniker).collect();
102
103    // If the query is an exact-match of any of the results, return that
104    // result only.
105    if let Some(m) = query_moniker {
106        if monikers.contains(&m) {
107            return Ok(vec![m]);
108        }
109    }
110
111    Ok(monikers)
112}
113
114/// Retrieves exactly one CML instance moniker that will match a given string query.
115///
116/// The string query can be a partial match on the following properties:
117/// * component moniker
118/// * component URL
119/// * component instance ID
120///
121/// If more than one instance matches the query, an error is thrown.
122/// If no instance matches the query, an error is thrown.
123pub async fn get_cml_moniker_from_query(
124    query: &str,
125    realm_query: &fsys::RealmQueryProxy,
126) -> Result<Moniker> {
127    // Get all instance monikers that match the query and ensure there is only one.
128    let mut monikers = get_cml_monikers_from_query(&query, &realm_query).await?;
129    if monikers.len() > 1 {
130        let monikers: Vec<String> = monikers.into_iter().map(|m| m.to_string()).collect();
131        let monikers = monikers.join("\n");
132        bail!("The query {:?} matches more than one component instance:\n{}\n\nTo avoid ambiguity, use one of the above monikers instead.", query, monikers);
133    }
134    if monikers.is_empty() {
135        bail!("No matching component instance found for query {:?}.", query);
136    }
137    let moniker = monikers.remove(0);
138    Ok(moniker)
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144    use crate::test_utils::serve_realm_query_instances;
145
146    fn setup_fake_realm_query() -> fsys::RealmQueryProxy {
147        setup_fake_realm_query_with_entries(vec![
148            ("/core/foo", "#meta/1bar.cm", "123456"),
149            ("/core/boo", "#meta/2bar.cm", "456789"),
150        ])
151    }
152
153    fn setup_fake_realm_query_with_entries(
154        entries: Vec<(&str, &str, &str)>,
155    ) -> fsys::RealmQueryProxy {
156        let instances = entries
157            .iter()
158            .map(|(moniker, url, instance_id)| fsys::Instance {
159                moniker: Some(moniker.to_string()),
160                url: Some(url.to_string()),
161                instance_id: Some(instance_id.to_string()),
162                resolved_info: None,
163                ..Default::default()
164            })
165            .collect::<Vec<_>>();
166        serve_realm_query_instances(instances)
167    }
168
169    #[fuchsia_async::run_singlethreaded(test)]
170    async fn test_get_cml_monikers_from_query_exact_match_and_prefixes() {
171        let realm_query = setup_fake_realm_query_with_entries(vec![
172            ("/", "#meta/1.cm", "1"),
173            ("/core", "#meta/2.cm", "2"),
174            ("/core:one", "#meta/3.cm", "3"),
175            ("/core:one/child", "#meta/4.cm", "4"),
176        ]);
177
178        assert_eq!(
179            get_cml_monikers_from_query("/", &realm_query).await.unwrap(),
180            vec![Moniker::parse_str("/").unwrap()]
181        );
182
183        assert_eq!(
184            get_cml_monikers_from_query("/core", &realm_query).await.unwrap(),
185            vec![Moniker::parse_str("/core").unwrap()]
186        );
187
188        assert_eq!(
189            get_cml_monikers_from_query("/core:one", &realm_query).await.unwrap(),
190            vec![Moniker::parse_str("/core:one").unwrap()]
191        );
192
193        assert_eq!(
194            get_cml_monikers_from_query("/core:o", &realm_query).await.unwrap(),
195            vec![
196                Moniker::parse_str("/core:one").unwrap(),
197                Moniker::parse_str("/core:one/child").unwrap(),
198            ]
199        );
200    }
201
202    #[fuchsia_async::run_singlethreaded(test)]
203    async fn test_get_cml_monikers_from_query_moniker_more_than_1() {
204        let realm_query = setup_fake_realm_query();
205        let results = get_cml_monikers_from_query("core", &realm_query).await.unwrap();
206        assert_eq!(
207            results,
208            vec![
209                Moniker::parse_str("/core/boo").unwrap(),
210                Moniker::parse_str("/core/foo").unwrap()
211            ]
212        );
213    }
214
215    #[fuchsia_async::run_singlethreaded(test)]
216    async fn test_get_cml_monikers_from_query_moniker_exactly_1() {
217        let realm_query = setup_fake_realm_query();
218        let results = get_cml_monikers_from_query("foo", &realm_query).await.unwrap();
219        assert_eq!(results, vec![Moniker::parse_str("/core/foo").unwrap()]);
220    }
221
222    #[fuchsia_async::run_singlethreaded(test)]
223    async fn test_get_cml_monikers_from_query_url_more_than_1() {
224        let realm_query = setup_fake_realm_query();
225        let results = get_cml_monikers_from_query("bar.cm", &realm_query).await.unwrap();
226        assert_eq!(
227            results,
228            vec![
229                Moniker::parse_str("/core/boo").unwrap(),
230                Moniker::parse_str("/core/foo").unwrap()
231            ]
232        );
233    }
234
235    #[fuchsia_async::run_singlethreaded(test)]
236    async fn test_get_cml_monikers_from_query_url_exactly_1() {
237        let realm_query = setup_fake_realm_query();
238        let results = get_cml_monikers_from_query("2bar.cm", &realm_query).await.unwrap();
239        assert_eq!(results, vec![Moniker::parse_str("/core/boo").unwrap()]);
240    }
241
242    #[fuchsia_async::run_singlethreaded(test)]
243    async fn test_get_cml_monikers_from_query_id_more_than_1() {
244        let realm_query = setup_fake_realm_query();
245        let results = get_cml_monikers_from_query("456", &realm_query).await.unwrap();
246        assert_eq!(
247            results,
248            vec![
249                Moniker::parse_str("/core/boo").unwrap(),
250                Moniker::parse_str("/core/foo").unwrap()
251            ]
252        );
253    }
254
255    #[fuchsia_async::run_singlethreaded(test)]
256    async fn test_get_cml_monikers_from_query_id_exactly_1() {
257        let realm_query = setup_fake_realm_query();
258        let results = get_cml_monikers_from_query("123", &realm_query).await.unwrap();
259        assert_eq!(results, vec![Moniker::parse_str("/core/foo").unwrap()]);
260    }
261
262    #[fuchsia_async::run_singlethreaded(test)]
263    async fn test_get_cml_monikers_from_query_no_results() {
264        let realm_query = setup_fake_realm_query();
265        let results = get_cml_monikers_from_query("qwerty", &realm_query).await.unwrap();
266        assert_eq!(results.len(), 0);
267    }
268
269    #[fuchsia_async::run_singlethreaded(test)]
270    async fn test_get_cml_moniker_from_query_no_match() {
271        let realm_query = setup_fake_realm_query();
272        get_cml_moniker_from_query("qwerty", &realm_query).await.unwrap_err();
273    }
274
275    #[fuchsia_async::run_singlethreaded(test)]
276    async fn test_get_cml_moniker_from_query_multiple_match() {
277        let realm_query = setup_fake_realm_query();
278        get_cml_moniker_from_query("bar.cm", &realm_query).await.unwrap_err();
279    }
280
281    #[fuchsia_async::run_singlethreaded(test)]
282    async fn test_get_cml_moniker_from_query_moniker_single_match() {
283        let realm_query = setup_fake_realm_query();
284        let moniker = get_cml_moniker_from_query("foo", &realm_query).await.unwrap();
285        assert_eq!(moniker, Moniker::parse_str("/core/foo").unwrap());
286
287        let realm_query = setup_fake_realm_query();
288        let moniker = get_cml_moniker_from_query("/core/foo", &realm_query).await.unwrap();
289        assert_eq!(moniker, Moniker::parse_str("/core/foo").unwrap());
290    }
291
292    #[fuchsia_async::run_singlethreaded(test)]
293    async fn test_get_cml_moniker_from_url_moniker_single_match() {
294        let realm_query = setup_fake_realm_query();
295        let moniker = get_cml_moniker_from_query("2bar.cm", &realm_query).await.unwrap();
296        assert_eq!(moniker, Moniker::parse_str("/core/boo").unwrap());
297    }
298
299    #[fuchsia_async::run_singlethreaded(test)]
300    async fn test_get_cml_moniker_from_url_id_single_match() {
301        let realm_query = setup_fake_realm_query();
302        let moniker = get_cml_moniker_from_query("123", &realm_query).await.unwrap();
303        assert_eq!(moniker, Moniker::parse_str("/core/foo").unwrap());
304    }
305}