Skip to main content

iquery/commands/
utils.rs

1// Copyright 2020 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::commands::types::DiagnosticsProvider;
6use crate::types::Error;
7use anyhow::anyhow;
8use cm_rust::{ExposeDeclCommon, ExposeSource, SourceName};
9
10#[cfg(not(feature = "fdomain"))]
11use component_debug;
12#[cfg(feature = "fdomain")]
13use component_debug_fdomain as component_debug;
14
15use component_debug::dirs::*;
16use component_debug::realm::*;
17use flex_client::ProxyHasDomain;
18use flex_client::fidl::DiscoverableProtocolMarker;
19use flex_fuchsia_diagnostics::{All, ArchiveAccessorMarker, Selector, TreeNames};
20
21#[cfg(not(feature = "fdomain"))]
22use fuchsia_fs;
23#[cfg(feature = "fdomain")]
24use fuchsia_fs_fdomain as fuchsia_fs;
25
26use flex_fuchsia_io as fio;
27use flex_fuchsia_sys2 as fsys2;
28use fuchsia_fs::directory;
29use moniker::Moniker;
30
31const ACCESSORS_DICTIONARY: &str = "diagnostics-accessors";
32
33/// Attempt to connect to the `fuchsia.diagnostics.*ArchiveAccessor` with the selector
34/// specified.
35pub async fn connect_accessor<P: DiscoverableProtocolMarker>(
36    moniker: &Moniker,
37    accessor_name: &str,
38    proxy: &fsys2::RealmQueryProxy,
39) -> Result<P::Proxy, Error> {
40    let proxy = connect_to_instance_protocol_at_path::<P>(
41        moniker,
42        OpenDirType::Exposed,
43        &format!("{ACCESSORS_DICTIONARY}/{accessor_name}"),
44        proxy,
45    )
46    .await
47    .map_err(|e| Error::ConnectToProtocol(accessor_name.to_string(), anyhow!("{:?}", e)))?;
48    Ok(proxy)
49}
50
51async fn list_accessors(
52    realm: &fsys2::RealmQueryProxy,
53    moniker: &Moniker,
54) -> Option<Vec<fuchsia_fs::directory::DirEntry>> {
55    let Ok(exposed_dir) = open_instance_directory(moniker, OpenDirType::Exposed, realm).await
56    else {
57        return None;
58    };
59
60    let (dir_proxy, server_end) = realm.domain().create_proxy::<fio::DirectoryMarker>();
61    exposed_dir
62        .open(
63            ACCESSORS_DICTIONARY,
64            fio::Flags::PROTOCOL_DIRECTORY | fio::PERM_READABLE,
65            &Default::default(),
66            server_end.into_channel(),
67        )
68        .expect("Open call failed!");
69
70    directory::readdir(&dir_proxy).await.ok()
71}
72
73/// Searches for a diagnostics `ArchiveAccessor` across all components in the
74/// topology.
75///
76/// Unlike [`fuzzy_search`], which queries component instances by their moniker,
77/// URL, or ID, this function specifically inspects capabilities exposed under
78/// the `diagnostics-accessors` dictionary starting with
79/// `fuchsia.diagnostics.ArchiveAccessor`.
80///
81/// Query format:
82/// - `<moniker_query>:<protocol_query>`: Fuzzy matches on both the component
83///   moniker and the accessor protocol name.
84/// - `<moniker_query>`: Fuzzy matches on the moniker and matches any accessor
85///   protocol exposed by that component.
86///
87/// Disambiguation:
88/// - Exact moniker matches take precedence over partial substring moniker
89///   matches.
90/// - Exact protocol matches take precedence over partial substring protocol
91///   matches.
92/// - If resolution cannot be narrowed to a single accessor, returns
93///   [`Error::FuzzyMatchTooManyMatches`].
94pub async fn fuzzy_search_accessors(
95    query: &str,
96    realm_query: &fsys2::RealmQueryProxy,
97) -> Result<(Moniker, String), Error> {
98    let (moniker_query, protocol_query) = query.rsplit_once(':').unwrap_or((query, ""));
99    let mut matching_monikers_and_protocols: Vec<_> = get_accessor_selectors(realm_query)
100        .await?
101        .iter()
102        .filter_map(|i| i.rsplit_once(":"))
103        .filter(|(m, p)| m.contains(moniker_query) && p.contains(protocol_query))
104        .filter_map(|(m, p)| Moniker::parse_str(m).ok().map(|moniker| (moniker, p.to_string())))
105        .collect();
106    if matching_monikers_and_protocols.is_empty() {
107        return Err(Error::SearchParameterNotFound(query.to_string()));
108    } else if matching_monikers_and_protocols.len() > 1 {
109        let mut moniker_exact_match: Vec<_> = matching_monikers_and_protocols
110            .iter()
111            .filter(|(m, _)| m.to_string() == moniker_query)
112            .cloned()
113            .collect();
114        // Return early if the moniker uniquely matches exactly one component.
115        if moniker_exact_match.len() == 1 {
116            return Ok(moniker_exact_match.pop().unwrap());
117        } else if moniker_exact_match.len() > 1 {
118            // If multiple accessors match under the exact moniker, disambiguate by exact protocol.
119            moniker_exact_match.retain(|(_, p)| p == protocol_query);
120            if moniker_exact_match.len() == 1 {
121                return Ok(moniker_exact_match.pop().unwrap());
122            }
123        }
124
125        // If the moniker was a fuzzy query, attempt to disambiguate by exact protocol across
126        // components.
127        let mut protocol_exact_match: Vec<_> = matching_monikers_and_protocols
128            .iter()
129            .filter(|(_, p)| p == protocol_query)
130            .cloned()
131            .collect();
132        if protocol_exact_match.len() == 1 {
133            return Ok(protocol_exact_match.pop().unwrap());
134        }
135
136        return Err(Error::FuzzyMatchTooManyMatches(
137            matching_monikers_and_protocols.into_iter().map(|(m, p)| format!("{m}:{p}")).collect(),
138        ));
139    }
140
141    Ok(matching_monikers_and_protocols.pop().unwrap())
142}
143
144/// Searches for a single component instance in the topology matching `query`.
145///
146/// Unlike [`fuzzy_search_accessors`], which inspects exposed diagnostics
147/// accessor protocols, this function matches `query` against component
148/// identifiers (moniker, component URL, and instance ID) across all instances
149/// in the realm without inspecting exposed capabilities or protocols.
150///
151/// If multiple instances match, returns [`Error::FuzzyMatchTooManyMatches`].
152async fn fuzzy_search(
153    query: &str,
154    realm_query: &fsys2::RealmQueryProxy,
155) -> Result<Instance, Error> {
156    let mut instances = component_debug::query::get_instances_from_query(query, realm_query)
157        .await
158        .map_err(Error::FuzzyMatchRealmQuery)?;
159    if instances.is_empty() {
160        return Err(Error::SearchParameterNotFound(query.to_string()));
161    } else if instances.len() > 1 {
162        return Err(Error::FuzzyMatchTooManyMatches(
163            instances.into_iter().map(|i| i.moniker.to_string()).collect(),
164        ));
165    }
166
167    Ok(instances.pop().unwrap())
168}
169
170pub async fn process_fuzzy_inputs<P: DiagnosticsProvider>(
171    queries: impl IntoIterator<Item = String>,
172    provider: &P,
173) -> Result<Vec<Selector>, Error> {
174    let mut queries = queries.into_iter().peekable();
175    if queries.peek().is_none() {
176        return Ok(vec![]);
177    }
178
179    let realm_query = provider.realm_query();
180    let mut results = vec![];
181    for value in queries {
182        match fuzzy_search(&value, realm_query).await {
183            // try again in case this is a fully escaped moniker or selector
184            Err(Error::SearchParameterNotFound(_)) => {
185                // In case they included a tree-selector segment, attempt to parse but don't bail
186                // on failure
187                if let Ok(selector) = selectors::parse_verbose(&value) {
188                    results.push(selector);
189                } else {
190                    // Note the lack of `sanitize_moniker_for_selectors`. `value` is assumed to
191                    // either
192                    //   A) Be a component that isn't running; therefore the selector being
193                    //      right or wrong is irrelevant
194                    //   B) Already be sanitized by the caller
195                    let selector_string = format!("{value}:root");
196                    results.push(
197                        selectors::parse_verbose(&selector_string)
198                            .map_err(|e| Error::ParseSelector(selector_string, e.into()))?,
199                    )
200                }
201            }
202            Err(e) => return Err(e),
203            Ok(instance) => {
204                let selector_string = format!(
205                    "{}:root",
206                    selectors::sanitize_moniker_for_selectors(instance.moniker.as_ref()),
207                );
208                results.push(
209                    selectors::parse_verbose(&selector_string)
210                        .map_err(|e| Error::ParseSelector(selector_string, e.into()))?,
211                )
212            }
213        }
214    }
215
216    Ok(results)
217}
218
219/// Returns the selectors for a component whose url, manifest, or moniker contains the
220/// `component` string.
221pub async fn process_component_query_with_partial_selectors<P: DiagnosticsProvider>(
222    component: &str,
223    tree_selectors: impl Iterator<Item = String>,
224    provider: &P,
225) -> Result<Vec<Selector>, Error> {
226    let mut tree_selectors = tree_selectors.into_iter().peekable();
227    let realm_query = provider.realm_query();
228    let instance = fuzzy_search(component, realm_query).await?;
229
230    let mut results = vec![];
231    if tree_selectors.peek().is_none() {
232        let selector_string = format!(
233            "{}:root",
234            selectors::sanitize_moniker_for_selectors(instance.moniker.as_ref())
235        );
236        results
237            .push(selectors::parse_verbose(&selector_string).map_err(Error::PartialSelectorHint)?);
238    } else {
239        for s in tree_selectors {
240            let selector_string = format!(
241                "{}:{}",
242                selectors::sanitize_moniker_for_selectors(instance.moniker.as_ref()),
243                s
244            );
245            results.push(
246                selectors::parse_verbose(&selector_string).map_err(Error::PartialSelectorHint)?,
247            )
248        }
249    }
250
251    Ok(results)
252}
253
254fn add_tree_name(selector: &mut Selector, tree_name: String) -> Result<(), Error> {
255    match selector.tree_names {
256        None => selector.tree_names = Some(TreeNames::Some(vec![tree_name])),
257        Some(ref mut names) => match names {
258            TreeNames::Some(names) => {
259                if !names.iter().any(|n| n == &tree_name) {
260                    names.push(tree_name)
261                }
262            }
263            TreeNames::All(_) => {}
264            TreeNames::__SourceBreaking { unknown_ordinal } => {
265                let unknown_ordinal = *unknown_ordinal;
266                return Err(Error::InvalidSelector(format!(
267                    "selector had invalid TreeNames variant {unknown_ordinal}: {selector:?}",
268                )));
269            }
270        },
271    }
272    Ok(())
273}
274
275/// Expand selectors with a tree name. If a tree name is given, the selectors will be guaranteed to
276/// include the tree name given unless they already have a tree name set. If no tree name is given
277/// and the selectors carry no tree name, then they'll be updated to target all tree names
278/// associated with the component.
279pub fn ensure_tree_field_is_set(
280    selectors: &mut Vec<Selector>,
281    tree_name: Option<String>,
282) -> Result<(), Error> {
283    if selectors.is_empty() {
284        let Some(tree_name) = tree_name else {
285            return Ok(());
286        };
287
288        // Safety: "**:*" is a valid selector
289        let mut selector = selectors::parse_verbose("**:*").unwrap();
290        selector.tree_names = Some(TreeNames::Some(vec![tree_name]));
291        selectors.push(selector);
292        return Ok(());
293    }
294
295    for selector in selectors.iter_mut() {
296        if let Some(tree_name) = &tree_name {
297            add_tree_name(selector, tree_name.clone())?;
298        } else if selector.tree_names.is_none() {
299            selector.tree_names = Some(TreeNames::All(All {}))
300        }
301    }
302
303    Ok(())
304}
305
306/// Get all the exposed `ArchiveAccessor` from any child component which
307/// directly exposes them or places them in its outgoing directory.
308pub async fn get_accessor_selectors(
309    realm_query: &fsys2::RealmQueryProxy,
310) -> Result<Vec<String>, Error> {
311    let mut result = vec![];
312    let instances = get_all_instances(realm_query).await?;
313    for instance in instances {
314        match get_resolved_declaration(&instance.moniker, realm_query).await {
315            Err(GetDeclarationError::InstanceNotFound(_))
316            | Err(GetDeclarationError::InstanceNotResolved(_)) => continue,
317            Err(err) => return Err(err.into()),
318            Ok(decl) => {
319                for capability in decl.capabilities {
320                    let capability_name = capability.name().to_string();
321                    if capability_name != ACCESSORS_DICTIONARY {
322                        continue;
323                    }
324                    if !decl.exposes.iter().any(|expose| {
325                        expose.source_name() == capability.name()
326                            && *expose.source() == ExposeSource::Self_
327                    }) {
328                        continue;
329                    }
330
331                    let Some(entries) = list_accessors(realm_query, &instance.moniker).await else {
332                        continue;
333                    };
334
335                    for entry in entries {
336                        let directory::DirEntry { name, kind: fio::DirentType::Service } = entry
337                        else {
338                            continue;
339                        };
340                        // This skips .host accessors intentionally.
341                        if !name.starts_with(ArchiveAccessorMarker::PROTOCOL_NAME) {
342                            continue;
343                        }
344                        result.push(format!("{}:{name}", instance.moniker));
345                    }
346                }
347            }
348        }
349    }
350    result.sort();
351    Ok(result)
352}
353
354#[cfg(test)]
355mod test {
356    use super::*;
357    use assert_matches::assert_matches;
358    use iquery_test_support::{MockRealmQuery, MockRealmQueryBuilder};
359    use selectors::parse_verbose;
360    use std::rc::Rc;
361
362    #[fuchsia::test]
363    async fn test_get_accessors() {
364        let fake_realm_query = Rc::new(MockRealmQuery::default());
365        let realm_query = Rc::clone(&fake_realm_query).get_proxy().await;
366
367        let res = get_accessor_selectors(&realm_query).await;
368
369        assert_matches!(res, Ok(_));
370
371        assert_eq!(
372            res.unwrap(),
373            vec![
374                String::from("example/component:fuchsia.diagnostics.ArchiveAccessor"),
375                String::from("foo/bar/thing:instance:fuchsia.diagnostics.ArchiveAccessor.feedback"),
376                String::from("foo/component:fuchsia.diagnostics.ArchiveAccessor.feedback"),
377            ]
378        );
379    }
380
381    #[fuchsia::test]
382    fn test_ensure_tree_field_is_set() {
383        let name = Some("abc".to_string());
384        let expected = vec![
385            parse_verbose("core/one:[name=abc]root").unwrap(),
386            parse_verbose("core/one:[name=xyz, name=abc]root").unwrap(),
387        ];
388
389        let mut actual = vec![
390            parse_verbose("core/one:root").unwrap(),
391            parse_verbose("core/one:[name=xyz]root").unwrap(),
392        ];
393        ensure_tree_field_is_set(&mut actual, name.clone()).unwrap();
394        assert_eq!(actual, expected);
395    }
396
397    #[fuchsia::test]
398    fn test_ensure_tree_field_is_set_noop_when_tree_names_set() {
399        let expected = vec![
400            parse_verbose("core/one:[...]root").unwrap(),
401            parse_verbose("core/one:[name=xyz]root").unwrap(),
402        ];
403        let mut actual = vec![
404            parse_verbose("core/one:root").unwrap(),
405            parse_verbose("core/one:[name=xyz]root").unwrap(),
406        ];
407        ensure_tree_field_is_set(&mut actual, None).unwrap();
408        assert_eq!(actual, expected);
409    }
410
411    #[fuchsia::test]
412    fn test_ensure_tree_field_is_set_noop_on_empty_vec_no_name() {
413        let mut actual = vec![];
414        ensure_tree_field_is_set(&mut actual, None).unwrap();
415        assert_eq!(actual, vec![]);
416    }
417
418    #[fuchsia::test]
419    fn test_ensure_tree_field_is_set_all_components_when_empty_and_name() {
420        let expected = vec![parse_verbose("**:[name=abc]*").unwrap()];
421        let mut actual = vec![];
422        let name = Some("abc".to_string());
423        ensure_tree_field_is_set(&mut actual, name).unwrap();
424        assert_eq!(actual, expected);
425    }
426
427    struct FakeProvider {
428        realm_query: fsys2::RealmQueryProxy,
429    }
430
431    impl FakeProvider {
432        async fn new(monikers: &'static [&'static str]) -> Self {
433            let mut builder = MockRealmQueryBuilder::default();
434            for name in monikers {
435                builder = builder.when(name).moniker(name).add();
436            }
437            let realm_query_proxy = Rc::new(builder.build()).get_proxy().await;
438            Self { realm_query: realm_query_proxy }
439        }
440    }
441
442    impl DiagnosticsProvider for FakeProvider {
443        async fn snapshot(
444            &self,
445            _: Option<&str>,
446            _: impl IntoIterator<Item = Selector>,
447        ) -> Result<Vec<diagnostics_data::Data<diagnostics_data::Inspect>>, Error> {
448            unreachable!("unimplemented");
449        }
450
451        async fn get_accessor_paths(&self) -> Result<Vec<String>, Error> {
452            unreachable!("unimplemented");
453        }
454
455        fn realm_query(&self) -> &fsys2::RealmQueryProxy {
456            &self.realm_query
457        }
458    }
459
460    #[fuchsia::test]
461    async fn test_process_fuzzy_inputs_success() {
462        let actual = process_fuzzy_inputs(
463            ["moniker1".to_string()],
464            &FakeProvider::new(&["core/moniker1", "core/moniker2"]).await,
465        )
466        .await
467        .unwrap();
468
469        let expected = vec![parse_verbose("core/moniker1:root").unwrap()];
470
471        assert_eq!(actual, expected);
472
473        let actual = process_fuzzy_inputs(
474            ["moniker1:collection".to_string()],
475            &FakeProvider::new(&["core/moniker1:collection", "core/moniker1", "core/moniker2"])
476                .await,
477        )
478        .await
479        .unwrap();
480
481        let expected = vec![parse_verbose(r"core/moniker1\:collection:root").unwrap()];
482
483        assert_eq!(actual, expected);
484
485        let actual = process_fuzzy_inputs(
486            [r"core/moniker1\:collection".to_string()],
487            &FakeProvider::new(&["core/moniker1:collection"]).await,
488        )
489        .await
490        .unwrap();
491
492        let expected = vec![parse_verbose(r"core/moniker1\:collection:root").unwrap()];
493
494        assert_eq!(actual, expected);
495
496        let actual = process_fuzzy_inputs(
497            ["core/moniker1:root:prop".to_string()],
498            &FakeProvider::new(&["core/moniker1:collection", "core/moniker1"]).await,
499        )
500        .await
501        .unwrap();
502
503        let expected = vec![parse_verbose(r"core/moniker1:root:prop").unwrap()];
504
505        assert_eq!(actual, expected);
506
507        let actual = process_fuzzy_inputs(
508            ["core/moniker1".to_string(), "core/moniker2".to_string()],
509            &FakeProvider::new(&["core/moniker1", "core/moniker2"]).await,
510        )
511        .await
512        .unwrap();
513
514        let expected = vec![
515            parse_verbose(r"core/moniker1:root").unwrap(),
516            parse_verbose(r"core/moniker2:root").unwrap(),
517        ];
518
519        assert_eq!(actual, expected);
520
521        let actual = process_fuzzy_inputs(
522            ["moniker1".to_string(), "moniker2".to_string()],
523            &FakeProvider::new(&["core/moniker1"]).await,
524        )
525        .await
526        .unwrap();
527
528        let expected = vec![
529            parse_verbose(r"core/moniker1:root").unwrap(),
530            // fallback is to assume that moniker2 is a valid moniker
531            parse_verbose("moniker2:root").unwrap(),
532        ];
533
534        assert_eq!(actual, expected);
535
536        let actual = process_fuzzy_inputs(
537            ["core/moniker1:root:prop".to_string(), "core/moniker2".to_string()],
538            &FakeProvider::new(&["core/moniker1", "core/moniker2"]).await,
539        )
540        .await
541        .unwrap();
542
543        let expected = vec![
544            parse_verbose(r"core/moniker1:root:prop").unwrap(),
545            parse_verbose(r"core/moniker2:root").unwrap(),
546        ];
547
548        assert_eq!(actual, expected);
549    }
550
551    #[fuchsia::test]
552    async fn test_process_fuzzy_inputs_failures() {
553        let actual =
554            process_fuzzy_inputs(["moniker ".to_string()], &FakeProvider::new(&["moniker"]).await)
555                .await;
556
557        assert_matches!(actual, Err(Error::ParseSelector(_, _)));
558
559        let actual = process_fuzzy_inputs(
560            ["moniker".to_string()],
561            &FakeProvider::new(&["core/moniker1", "core/moniker2"]).await,
562        )
563        .await;
564
565        assert_matches!(actual, Err(Error::FuzzyMatchTooManyMatches(_)));
566    }
567
568    #[fuchsia::test]
569    async fn test_fuzzy_component_search() {
570        let actual = process_component_query_with_partial_selectors(
571            "moniker1",
572            [].into_iter(),
573            &FakeProvider::new(&["core/moniker1", "core/moniker2"]).await,
574        )
575        .await
576        .unwrap();
577
578        let expected = vec![parse_verbose(r"core/moniker1:root").unwrap()];
579
580        assert_eq!(actual, expected);
581
582        let actual = process_component_query_with_partial_selectors(
583            "moniker1",
584            ["root/foo:bar".to_string()].into_iter(),
585            &FakeProvider::new(&["core/moniker1", "core/moniker2"]).await,
586        )
587        .await
588        .unwrap();
589
590        let expected = vec![parse_verbose(r"core/moniker1:root/foo:bar").unwrap()];
591
592        assert_eq!(actual, expected);
593
594        let actual = process_component_query_with_partial_selectors(
595            "moniker1",
596            ["root/foo:bar".to_string()].into_iter(),
597            &FakeProvider::new(&["core/moniker2", "core/moniker3"]).await,
598        )
599        .await;
600
601        assert_matches!(actual, Err(Error::SearchParameterNotFound(_)));
602    }
603
604    #[fuchsia::test]
605    async fn test_fuzzy_search_accessors_exact_match() {
606        let fake_realm_query = Rc::new(MockRealmQuery::default());
607        let realm_query = Rc::clone(&fake_realm_query).get_proxy().await;
608
609        let (moniker, protocol) = fuzzy_search_accessors(
610            "example/component:fuchsia.diagnostics.ArchiveAccessor",
611            &realm_query,
612        )
613        .await
614        .unwrap();
615        assert_eq!(moniker, Moniker::parse_str("example/component").unwrap());
616        assert_eq!(protocol, "fuchsia.diagnostics.ArchiveAccessor");
617    }
618
619    #[fuchsia::test]
620    async fn test_fuzzy_search_accessors_fuzzy_match_moniker_and_protocol() {
621        let fake_realm_query = Rc::new(MockRealmQuery::default());
622        let realm_query = Rc::clone(&fake_realm_query).get_proxy().await;
623
624        let (moniker, protocol) =
625            fuzzy_search_accessors("example:ArchiveAccessor", &realm_query).await.unwrap();
626        assert_eq!(moniker, Moniker::parse_str("example/component").unwrap());
627        assert_eq!(protocol, "fuchsia.diagnostics.ArchiveAccessor");
628    }
629
630    #[fuchsia::test]
631    async fn test_fuzzy_search_accessors_moniker_with_colon() {
632        let fake_realm_query = Rc::new(MockRealmQuery::default());
633        let realm_query = Rc::clone(&fake_realm_query).get_proxy().await;
634
635        let (moniker, protocol) =
636            fuzzy_search_accessors("thing:instance:feedback", &realm_query).await.unwrap();
637        assert_eq!(moniker, Moniker::parse_str("foo/bar/thing:instance").unwrap());
638        assert_eq!(protocol, "fuchsia.diagnostics.ArchiveAccessor.feedback");
639    }
640
641    #[fuchsia::test]
642    async fn test_fuzzy_search_accessors_disambiguation_by_exact_moniker() {
643        let fake_realm_query = Rc::new(
644            MockRealmQueryBuilder::new()
645                .when("example/component")
646                .moniker("./example/component")
647                .accessors(&["fuchsia.diagnostics.ArchiveAccessor"])
648                .add()
649                .when("example/component/subcomponent")
650                .moniker("./example/component/subcomponent")
651                .accessors(&["fuchsia.diagnostics.ArchiveAccessor"])
652                .add()
653                .build(),
654        );
655        let realm_query = Rc::clone(&fake_realm_query).get_proxy().await;
656
657        let (moniker, protocol) = fuzzy_search_accessors(
658            "example/component:fuchsia.diagnostics.ArchiveAccessor",
659            &realm_query,
660        )
661        .await
662        .unwrap();
663        assert_eq!(moniker, Moniker::parse_str("example/component").unwrap());
664        assert_eq!(protocol, "fuchsia.diagnostics.ArchiveAccessor");
665
666        let (moniker, protocol) =
667            fuzzy_search_accessors("example/component:ArchiveAccessor", &realm_query)
668                .await
669                .unwrap();
670        assert_eq!(moniker, Moniker::parse_str("example/component").unwrap());
671        assert_eq!(protocol, "fuchsia.diagnostics.ArchiveAccessor");
672    }
673
674    #[fuchsia::test]
675    async fn test_fuzzy_search_accessors_disambiguation_by_exact_protocol() {
676        let fake_realm_query = Rc::new(MockRealmQuery::default());
677        let realm_query = Rc::clone(&fake_realm_query).get_proxy().await;
678
679        // MockRealmQuery::default() uses `MockRealmQueryBuilder::prefilled()` as the source of
680        // truth, which sets up the following monikers and protocols:
681        // - `example/component`: `fuchsia.diagnostics.ArchiveAccessor`
682        // - `other/component`: `fuchsia.io.SomeOtherThing`, `fuchsia.io.MagicStuff`
683        // - `foo/component`: `fuchsia.diagnostics.ArchiveAccessor.feedback`
684        // - `foo/bar/thing:instance`: `fuchsia.diagnostics.ArchiveAccessor.feedback`
685        //
686        // Both "example/component" and "foo/component" match the moniker query "component",
687        // but only "example/component" exposes the exact protocol "fuchsia.diagnostics.ArchiveAccessor"
688        // ("foo/component" exposes "fuchsia.diagnostics.ArchiveAccessor.feedback").
689        let (moniker, protocol) =
690            fuzzy_search_accessors("component:fuchsia.diagnostics.ArchiveAccessor", &realm_query)
691                .await
692                .unwrap();
693        assert_eq!(moniker, Moniker::parse_str("example/component").unwrap());
694        assert_eq!(protocol, "fuchsia.diagnostics.ArchiveAccessor");
695    }
696
697    #[fuchsia::test]
698    async fn test_fuzzy_search_accessors_disambiguation_by_both_exact_moniker_and_protocol() {
699        let fake_realm_query = Rc::new(
700            MockRealmQueryBuilder::new()
701                .when("example/component")
702                .moniker("./example/component")
703                .accessors(&[
704                    "fuchsia.diagnostics.ArchiveAccessor",
705                    "fuchsia.diagnostics.ArchiveAccessor.feedback",
706                ])
707                .add()
708                .when("example/component/subcomponent")
709                .moniker("./example/component/subcomponent")
710                .accessors(&[
711                    "fuchsia.diagnostics.ArchiveAccessor",
712                    "fuchsia.diagnostics.ArchiveAccessor.feedback",
713                ])
714                .add()
715                .build(),
716        );
717        let realm_query = Rc::clone(&fake_realm_query).get_proxy().await;
718
719        let (moniker, protocol) = fuzzy_search_accessors(
720            "example/component:fuchsia.diagnostics.ArchiveAccessor",
721            &realm_query,
722        )
723        .await
724        .unwrap();
725        assert_eq!(moniker, Moniker::parse_str("example/component").unwrap());
726        assert_eq!(protocol, "fuchsia.diagnostics.ArchiveAccessor");
727    }
728
729    #[fuchsia::test]
730    async fn test_fuzzy_search_accessors_no_protocol_single_accessor() {
731        let fake_realm_query = Rc::new(MockRealmQuery::default());
732        let realm_query = Rc::clone(&fake_realm_query).get_proxy().await;
733
734        let (moniker, protocol) =
735            fuzzy_search_accessors("example/component", &realm_query).await.unwrap();
736        assert_eq!(moniker, Moniker::parse_str("example/component").unwrap());
737        assert_eq!(protocol, "fuchsia.diagnostics.ArchiveAccessor");
738    }
739
740    #[fuchsia::test]
741    async fn test_fuzzy_search_accessors_no_protocol_multiple_accessors() {
742        let fake_realm_query = Rc::new(
743            MockRealmQueryBuilder::new()
744                .when("example/component")
745                .moniker("./example/component")
746                .accessors(&[
747                    "fuchsia.diagnostics.ArchiveAccessor",
748                    "fuchsia.diagnostics.ArchiveAccessor.feedback",
749                ])
750                .add()
751                .build(),
752        );
753        let realm_query = Rc::clone(&fake_realm_query).get_proxy().await;
754
755        let res = fuzzy_search_accessors("example/component", &realm_query).await;
756        assert_matches!(
757            res,
758            Err(Error::FuzzyMatchTooManyMatches(matches)) if matches.0 == [
759                "example/component:fuchsia.diagnostics.ArchiveAccessor",
760                "example/component:fuchsia.diagnostics.ArchiveAccessor.feedback",
761            ]
762        );
763    }
764
765    #[fuchsia::test]
766    async fn test_fuzzy_search_accessors_moniker_not_found() {
767        let fake_realm_query = Rc::new(MockRealmQuery::default());
768        let realm_query = Rc::clone(&fake_realm_query).get_proxy().await;
769
770        let res = fuzzy_search_accessors("nonexistent:ArchiveAccessor", &realm_query).await;
771        assert_matches!(res, Err(Error::SearchParameterNotFound(query)) if query == "nonexistent:ArchiveAccessor");
772    }
773
774    #[fuchsia::test]
775    async fn test_fuzzy_search_accessors_protocol_not_found() {
776        let fake_realm_query = Rc::new(MockRealmQuery::default());
777        let realm_query = Rc::clone(&fake_realm_query).get_proxy().await;
778
779        let res = fuzzy_search_accessors("example:nonexistent", &realm_query).await;
780        assert_matches!(res, Err(Error::SearchParameterNotFound(query)) if query == "example:nonexistent");
781    }
782
783    #[fuchsia::test]
784    async fn test_fuzzy_search_accessors_too_many_matches_fuzzy_protocol() {
785        let fake_realm_query = Rc::new(MockRealmQuery::default());
786        let realm_query = Rc::clone(&fake_realm_query).get_proxy().await;
787
788        let res = fuzzy_search_accessors("foo:feedback", &realm_query).await;
789        assert_matches!(
790            res,
791            Err(Error::FuzzyMatchTooManyMatches(matches)) if matches.0 == [
792                "foo/bar/thing:instance:fuchsia.diagnostics.ArchiveAccessor.feedback",
793                "foo/component:fuchsia.diagnostics.ArchiveAccessor.feedback",
794            ]
795        );
796    }
797
798    #[fuchsia::test]
799    async fn test_fuzzy_search_accessors_too_many_matches_exact_protocol() {
800        let fake_realm_query = Rc::new(MockRealmQuery::default());
801        let realm_query = Rc::clone(&fake_realm_query).get_proxy().await;
802
803        let res = fuzzy_search_accessors(
804            "foo:fuchsia.diagnostics.ArchiveAccessor.feedback",
805            &realm_query,
806        )
807        .await;
808        assert_matches!(
809            res,
810            Err(Error::FuzzyMatchTooManyMatches(matches)) if matches.0 == [
811                "foo/bar/thing:instance:fuchsia.diagnostics.ArchiveAccessor.feedback",
812                "foo/component:fuchsia.diagnostics.ArchiveAccessor.feedback",
813            ]
814        );
815    }
816}