Skip to main content

log_command/
filter.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 crate::log_formatter::{LogData, LogEntry};
6use crate::{InstanceGetter, LogCommand, LogError};
7use diagnostics_data::{LogsData, Severity};
8use flex_fuchsia_diagnostics::LogInterestSelector;
9use moniker::{EXTENDED_MONIKER_COMPONENT_MANAGER_STR, ExtendedMoniker, Moniker};
10use selectors::SelectorExt;
11use std::borrow::Cow;
12use std::str::FromStr;
13use std::sync::LazyLock;
14use zx_types::zx_koid_t;
15
16static KLOG: &str = "klog";
17static KLOG_MONIKER: LazyLock<ExtendedMoniker> =
18    LazyLock::new(|| ExtendedMoniker::try_from(KLOG).unwrap());
19
20struct MonikerFilters {
21    queries: Vec<String>,
22    matched_monikers: Vec<String>,
23}
24
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct FuzzyMatchWarning {
27    pub query: String,
28    pub resolved: String,
29}
30
31/// Disambiguates candidate monikers matching a query by prioritizing exact matches,
32/// path suffix matches, contiguous segment matches, and ancestor monikers over descendants.
33pub fn disambiguate_queried_monikers(query: &str, instances: Vec<Moniker>) -> Vec<Moniker> {
34    if instances.len() <= 1 {
35        return instances;
36    }
37
38    let normalized_query = query
39        .strip_prefix("./")
40        .or_else(|| query.strip_prefix('/'))
41        .unwrap_or(query)
42        .trim_end_matches('/');
43
44    let exact_full_matches: Vec<&Moniker> = instances
45        .iter()
46        .filter(|m| {
47            let s = m.as_str();
48            s == normalized_query
49                || s == query
50                || (query == "/" && m.is_root())
51                || (normalized_query.is_empty() && m.is_root())
52        })
53        .collect();
54
55    if !exact_full_matches.is_empty() {
56        return exact_full_matches.into_iter().cloned().collect();
57    }
58
59    let query_segments: Vec<&str> = normalized_query.split('/').collect();
60    let q_len = query_segments.len();
61
62    let segment_matches = |seg: &moniker::BorrowedChildName, q_seg: &str| -> bool {
63        seg.as_ref() == q_seg || seg.name().as_str() == q_seg
64    };
65
66    let matches_suffix = |m: &Moniker| -> bool {
67        let path = m.path();
68        if path.len() < q_len {
69            return false;
70        }
71        let suffix = &path[path.len() - q_len..];
72        suffix.iter().zip(&query_segments).all(|(seg, q_seg)| segment_matches(seg, q_seg))
73    };
74
75    let try_select_unique_root =
76        |candidates: Vec<&Moniker>| -> Vec<Moniker> {
77            if candidates.len() <= 1 {
78                return candidates.into_iter().cloned().collect();
79            }
80            if let Some(root) = candidates.iter().min_by_key(|m| m.path().len()).filter(|&root| {
81                candidates.iter().all(|other| other == root || other.has_prefix(root))
82            }) {
83                return vec![(*root).clone()];
84            }
85            candidates.into_iter().cloned().collect()
86        };
87
88    let suffix_matches: Vec<&Moniker> = instances.iter().filter(|m| matches_suffix(m)).collect();
89
90    if !suffix_matches.is_empty() {
91        let exact_suffix_matches: Vec<&Moniker> = suffix_matches
92            .iter()
93            .filter(|&m| {
94                let path = m.path();
95                let suffix = &path[path.len() - q_len..];
96                suffix.iter().zip(&query_segments).all(|(seg, q_seg)| seg.as_ref() == *q_seg)
97            })
98            .copied()
99            .collect();
100
101        let candidates =
102            if !exact_suffix_matches.is_empty() { exact_suffix_matches } else { suffix_matches };
103
104        return try_select_unique_root(candidates);
105    }
106
107    let segment_subpath_matches: Vec<&Moniker> = instances
108        .iter()
109        .filter(|m| {
110            let path = m.path();
111            if path.len() < q_len {
112                return false;
113            }
114            path.windows(q_len).any(|window| {
115                window.iter().zip(&query_segments).all(|(seg, q_seg)| segment_matches(seg, q_seg))
116            })
117        })
118        .collect();
119
120    if !segment_subpath_matches.is_empty() {
121        return try_select_unique_root(segment_subpath_matches);
122    }
123
124    try_select_unique_root(instances.iter().collect())
125}
126
127impl MonikerFilters {
128    fn new(queries: Vec<String>) -> Self {
129        Self { queries, matched_monikers: vec![] }
130    }
131
132    async fn expand_monikers(
133        &mut self,
134        getter: &impl InstanceGetter,
135    ) -> Result<Vec<FuzzyMatchWarning>, LogError> {
136        self.matched_monikers = vec![];
137        self.matched_monikers.reserve(self.queries.len());
138        let mut warnings = vec![];
139        for query in &self.queries {
140            if query == KLOG {
141                self.matched_monikers.push(query.clone());
142                continue;
143            }
144
145            let instances = getter.get_monikers_from_query(query).await?;
146            let mut instances = disambiguate_queried_monikers(query, instances);
147            if instances.len() > 1 {
148                return Err(LogError::too_many_fuzzy_matches(
149                    instances.into_iter().map(|i| i.to_string()),
150                    query,
151                ));
152            }
153            match instances.pop() {
154                Some(instance) => {
155                    let instance_str = instance.to_string();
156                    let is_exact_match = query == &instance_str
157                        || query.strip_prefix('/') == Some(instance_str.as_str())
158                        || instance_str.strip_prefix('/') == Some(query.as_str());
159
160                    if !is_exact_match {
161                        warnings.push(FuzzyMatchWarning {
162                            query: query.clone(),
163                            resolved: instance_str.clone(),
164                        });
165                    }
166                    self.matched_monikers.push(instance_str);
167                }
168                None => return Err(LogError::SearchParameterNotFound(query.to_string())),
169            }
170        }
171        Ok(warnings)
172    }
173}
174
175/// A struct that holds the criteria for filtering logs.
176pub struct LogFilterCriteria {
177    /// The minimum severity of logs to include.
178    min_severity: Severity,
179    /// Filter by string.
180    filters: Vec<String>,
181    /// Monikers to include in logs.
182    moniker_filters: MonikerFilters,
183    /// Exclude by string.
184    excludes: Vec<String>,
185    /// Exclude by regular expressions.
186    exclude_regexes: Vec<regex_lite::Regex>,
187    /// The tags to include.
188    tags: Vec<String>,
189    /// The tags to exclude.
190    exclude_tags: Vec<String>,
191    /// Filter by PID
192    pid: Option<zx_koid_t>,
193    /// Filter by TID
194    tid: Option<zx_koid_t>,
195    /// Log interest selectors used to filter severity on a per-component basis
196    /// Overrides min_severity for components matching the selector.
197    /// In the event of an ambiguous match, the lowest severity is used.
198    interest_selectors: Vec<LogInterestSelector>,
199    /// True if case sensitive, false otherwise
200    case_sensitive: bool,
201}
202
203impl Default for LogFilterCriteria {
204    fn default() -> Self {
205        Self {
206            min_severity: Severity::Info,
207            filters: vec![],
208            excludes: vec![],
209            exclude_regexes: vec![],
210            tags: vec![],
211            moniker_filters: MonikerFilters::new(vec![]),
212            exclude_tags: vec![],
213            pid: None,
214            tid: None,
215            case_sensitive: false,
216            interest_selectors: vec![],
217        }
218    }
219}
220
221// Convert a string to lowercase if needed for case insensitive comparisons.
222// If case_sensitive is false, the conversion is performed.
223fn convert_to_lowercase_if_needed<'a>(input: &'a str, case_sensitive: bool) -> Cow<'a, str> {
224    if case_sensitive { Cow::Borrowed(input) } else { Cow::Owned(input.to_lowercase()) }
225}
226
227impl LogFilterCriteria {
228    pub fn try_from(cmd: LogCommand) -> Result<Self, LogError> {
229        let case_sensitive = cmd.case_sensitive();
230        let min_severity = cmd.severity();
231        let pid = cmd.pid();
232        let tid = cmd.tid();
233        let kernel = cmd.kernel();
234
235        let mut exclude_regexes = Vec::new();
236        for pattern in cmd.exclude_regex() {
237            let re =
238                regex_lite::RegexBuilder::new(pattern).case_insensitive(!case_sensitive).build()?;
239            exclude_regexes.push(re);
240        }
241        if let Some(path) = cmd.exclude_regex_file() {
242            let content = std::fs::read_to_string(path)?;
243            for line in content.lines() {
244                let line = line.trim();
245                if line.is_empty() || line.starts_with('#') {
246                    continue;
247                }
248                let re = regex_lite::RegexBuilder::new(line)
249                    .case_insensitive(!case_sensitive)
250                    .build()?;
251                exclude_regexes.push(re);
252            }
253        }
254
255        let mut filters = cmd.filters;
256        if kernel {
257            filters.component.push(KLOG.to_string());
258        }
259        let moniker_filters = MonikerFilters::new(filters.component);
260
261        Ok(Self {
262            min_severity,
263            filters: filters.filter,
264            tags: filters
265                .tag
266                .into_iter()
267                .map(|value| convert_to_lowercase_if_needed(&value, case_sensitive).to_string())
268                .collect(),
269            excludes: filters.exclude,
270            exclude_regexes,
271            moniker_filters,
272            exclude_tags: filters.exclude_tags,
273            pid,
274            case_sensitive,
275            tid,
276            interest_selectors: cmd.set_severity.into_iter().flatten().collect(),
277        })
278    }
279}
280
281impl From<LogCommand> for LogFilterCriteria {
282    fn from(cmd: LogCommand) -> Self {
283        Self::try_from(cmd).expect("Failed to convert LogCommand to LogFilterCriteria")
284    }
285}
286
287impl LogFilterCriteria {
288    /// Sets the minimum severity of logs to include.
289    pub fn set_min_severity(&mut self, severity: Severity) {
290        self.min_severity = severity;
291    }
292
293    pub async fn expand_monikers(
294        &mut self,
295        getter: &impl InstanceGetter,
296    ) -> Result<Vec<FuzzyMatchWarning>, LogError> {
297        self.moniker_filters.expand_monikers(getter).await
298    }
299
300    /// Sets the tags to include.
301    pub fn set_tags<I, S>(&mut self, tags: I)
302    where
303        I: IntoIterator<Item = S>,
304        S: Into<String>,
305    {
306        self.tags = tags.into_iter().map(|value| value.into()).collect();
307    }
308
309    /// Sets the tags to exclude.
310    pub fn set_exclude_tags<I, S>(&mut self, tags: I)
311    where
312        I: IntoIterator<Item = S>,
313        S: Into<String>,
314    {
315        self.exclude_tags = tags.into_iter().map(|value| value.into()).collect();
316    }
317
318    /// Returns true if the given `LogEntry` matches the filter criteria.
319    pub fn matches(&self, entry: &LogEntry) -> bool {
320        match entry {
321            LogEntry { data: LogData::TargetLog(data), .. } => self.match_filters_to_log_data(data),
322        }
323    }
324
325    /// Returns true if the given 'LogsData' matches the filter string by
326    /// message, moniker, or component URL.
327    fn matches_filter_string(
328        filter_string: &str,
329        message: &str,
330        log: &LogsData,
331        case_sensitive: bool,
332    ) -> bool {
333        // Convert strings to lower-case if needed
334        let filter_string = convert_to_lowercase_if_needed(filter_string, case_sensitive);
335        let message = convert_to_lowercase_if_needed(message, case_sensitive);
336        let file_path =
337            log.file_path().map(|value| convert_to_lowercase_if_needed(value, case_sensitive));
338        let component_url = log
339            .metadata
340            .component_url
341            .as_ref()
342            .map(|value| convert_to_lowercase_if_needed(value.as_str(), case_sensitive));
343        let moniker_str = log.moniker.to_string();
344        let moniker = convert_to_lowercase_if_needed(&moniker_str, case_sensitive);
345
346        message.contains(&*filter_string)
347            || file_path.is_some_and(|s| s.contains(&*filter_string))
348            || component_url.as_ref().is_some_and(|s| s.contains(&*filter_string))
349            || moniker.contains(&*filter_string)
350    }
351
352    // TODO(b/303315896): If/when debuglog is structured remove this.
353    fn parse_tags(value: &str) -> Vec<&str> {
354        let mut tags = Vec::new();
355        let mut current = value;
356        if !current.starts_with('[') {
357            return tags;
358        }
359        loop {
360            match current.find('[') {
361                Some(opening_index) => {
362                    current = &current[opening_index + 1..];
363                }
364                None => return tags,
365            }
366            match current.find(']') {
367                Some(closing_index) => {
368                    tags.push(&current[..closing_index]);
369                    current = &current[closing_index + 1..];
370                }
371                None => return tags,
372            }
373        }
374    }
375
376    fn match_synthetic_klog_tags(&self, klog_str: &str, case_sensitive: bool) -> bool {
377        let tags = Self::parse_tags(klog_str)
378            .into_iter()
379            .map(|value| convert_to_lowercase_if_needed(value, case_sensitive))
380            .collect::<Vec<_>>();
381        self.tags.iter().any(|f| {
382            tags.iter().any(|t| convert_to_lowercase_if_needed(t, case_sensitive).contains(f))
383        })
384    }
385
386    /// Returns true if the given `LogsData` matches the moniker string.
387    fn matches_filter_by_moniker_string(filter_string: &str, log: &LogsData) -> bool {
388        let Ok(filter_moniker) = ExtendedMoniker::from_str(filter_string) else {
389            return false;
390        };
391        filter_moniker == log.moniker
392    }
393
394    /// Returns true if the given `LogsData` matches the filter criteria.
395    fn match_filters_to_log_data(&self, data: &LogsData) -> bool {
396        let min_severity = self
397            .interest_selectors
398            .iter()
399            .filter(|s| data.moniker.matches_component_selector(&s.selector).unwrap_or(false))
400            .filter_map(|selector| selector.interest.min_severity)
401            .min()
402            .unwrap_or_else(|| self.min_severity.into());
403        if data.metadata.severity < min_severity {
404            return false;
405        }
406
407        if let Some(pid) = self.pid
408            && data.pid() != Some(pid)
409        {
410            return false;
411        }
412
413        if let Some(tid) = self.tid
414            && data.tid() != Some(tid)
415        {
416            return false;
417        }
418
419        if !self.moniker_filters.matched_monikers.is_empty()
420            && !self
421                .moniker_filters
422                .matched_monikers
423                .iter()
424                .any(|f| Self::matches_filter_by_moniker_string(f, data))
425        {
426            return false;
427        }
428
429        let msg = data.msg().unwrap_or("");
430
431        if !self.filters.is_empty()
432            && !self
433                .filters
434                .iter()
435                .any(|f| Self::matches_filter_string(f, msg, data, self.case_sensitive))
436        {
437            return false;
438        }
439
440        if self
441            .excludes
442            .iter()
443            .any(|f| Self::matches_filter_string(f, msg, data, self.case_sensitive))
444        {
445            return false;
446        }
447        if self.exclude_regexes.iter().any(|r| {
448            r.is_match(msg)
449                || data.file_path().is_some_and(|f| r.is_match(f))
450                || data.metadata.component_url.as_ref().is_some_and(|url| r.is_match(url.as_str()))
451                || r.is_match(data.moniker.as_ref())
452        }) {
453            return false;
454        }
455        if !self.tags.is_empty()
456            && !self.tags.iter().any(|query_tag| {
457                let has_tag = data
458                    .tags()
459                    .map(|t| {
460                        t.iter().any(|value| {
461                            convert_to_lowercase_if_needed(value, self.case_sensitive) == *query_tag
462                        })
463                    })
464                    .unwrap_or(false);
465                let moniker_has_tag =
466                    moniker_contains_in_last_segment(&data.moniker, query_tag, self.case_sensitive);
467                has_tag || moniker_has_tag
468            })
469        {
470            if data.moniker == *KLOG_MONIKER {
471                return self
472                    .match_synthetic_klog_tags(data.msg().unwrap_or(""), self.case_sensitive);
473            }
474            return false;
475        }
476
477        !self.exclude_tags.iter().any(|excluded_tag| {
478            let has_tag = data.tags().map(|tag| tag.contains(excluded_tag)).unwrap_or(false);
479            let moniker_has_tag =
480                moniker_contains_in_last_segment(&data.moniker, excluded_tag, self.case_sensitive);
481            has_tag || moniker_has_tag
482        })
483    }
484}
485
486fn moniker_contains_in_last_segment(
487    moniker: &ExtendedMoniker,
488    query_tag: &str,
489    case_sensitive: bool,
490) -> bool {
491    let query_tag = convert_to_lowercase_if_needed(query_tag, case_sensitive);
492    match moniker {
493        ExtendedMoniker::ComponentInstance(moniker) => moniker
494            .leaf()
495            .map(|segment| {
496                convert_to_lowercase_if_needed(segment.as_ref(), case_sensitive)
497                    .contains(&*query_tag)
498            })
499            .unwrap_or(false),
500        ExtendedMoniker::ComponentManager => {
501            EXTENDED_MONIKER_COMPONENT_MANAGER_STR.contains(&*query_tag)
502        }
503    }
504}
505
506#[cfg(test)]
507mod test {
508    use crate::LogFilterArgs;
509    use diagnostics_data::{ExtendedMoniker, Timestamp};
510    use selectors::parse_log_interest_selector;
511
512    use crate::log_socket_stream::OneOrMany;
513    use crate::{LogSubCommand, RawDumpCommand};
514
515    use super::*;
516
517    fn empty_dump_command() -> LogCommand {
518        LogCommand {
519            sub_command: Some(LogSubCommand::Dump(RawDumpCommand::default())),
520            ..LogCommand::default()
521        }
522    }
523
524    fn make_log_entry(log_data: LogData) -> LogEntry {
525        LogEntry { data: log_data }
526    }
527
528    #[fuchsia::test]
529    async fn test_criteria_tag_filter_filters_moniker() {
530        let cmd = LogCommand {
531            filters: LogFilterArgs { tag: vec!["testcomponent".to_string()], ..Default::default() },
532            ..empty_dump_command()
533        };
534        let criteria = LogFilterCriteria::from(cmd);
535
536        assert!(
537            criteria.matches(&make_log_entry(
538                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
539                    timestamp: Timestamp::from_nanos(0),
540                    component_url: Some("".into()),
541                    moniker: "my/testcomponent".try_into().unwrap(),
542                    severity: diagnostics_data::Severity::Error,
543                })
544                .set_message("included")
545                .add_tag("tag1")
546                .add_tag("tag2")
547                .build()
548                .into()
549            ))
550        );
551    }
552
553    #[fuchsia::test]
554    async fn test_criteria_exclude_tag_filters_moniker() {
555        let cmd = LogCommand {
556            filters: LogFilterArgs {
557                exclude_tags: vec!["testcomponent".to_string()],
558                ..Default::default()
559            },
560            ..empty_dump_command()
561        };
562        let criteria = LogFilterCriteria::from(cmd);
563        assert!(
564            !criteria.matches(&make_log_entry(
565                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
566                    timestamp: Timestamp::from_nanos(0),
567                    component_url: Some("".into()),
568                    moniker: "my/testcomponent".try_into().unwrap(),
569                    severity: diagnostics_data::Severity::Error,
570                })
571                .set_message("excluded")
572                .add_tag("tag1")
573                .add_tag("tag2")
574                .build()
575                .into()
576            ))
577        );
578    }
579
580    #[fuchsia::test]
581    async fn test_criteria_tag_filter() {
582        let cmd = LogCommand {
583            filters: LogFilterArgs {
584                tag: vec!["tag1".to_string()],
585                exclude_tags: vec!["tag3".to_string()],
586                ..Default::default()
587            },
588            ..empty_dump_command()
589        };
590        let criteria = LogFilterCriteria::from(cmd);
591
592        assert!(
593            criteria.matches(&make_log_entry(
594                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
595                    timestamp: Timestamp::from_nanos(0),
596                    component_url: Some("".into()),
597                    moniker: ExtendedMoniker::ComponentManager,
598                    severity: diagnostics_data::Severity::Error,
599                })
600                .set_message("included")
601                .add_tag("tag1")
602                .add_tag("tag2")
603                .build()
604                .into()
605            ))
606        );
607
608        assert!(
609            !criteria.matches(&make_log_entry(
610                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
611                    timestamp: Timestamp::from_nanos(0),
612                    component_url: Some("".into()),
613                    moniker: ExtendedMoniker::ComponentManager,
614                    severity: diagnostics_data::Severity::Error,
615                })
616                .set_message("included")
617                .add_tag("tag2")
618                .build()
619                .into()
620            ))
621        );
622        assert!(
623            !criteria.matches(&make_log_entry(
624                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
625                    timestamp: Timestamp::from_nanos(0),
626                    component_url: Some("".into()),
627                    moniker: ExtendedMoniker::ComponentManager,
628                    severity: diagnostics_data::Severity::Error,
629                })
630                .set_message("included")
631                .add_tag("tag1")
632                .add_tag("tag3")
633                .build()
634                .into()
635            ))
636        );
637    }
638
639    #[fuchsia::test]
640    async fn test_per_component_severity() {
641        let cmd = LogCommand {
642            sub_command: Some(LogSubCommand::Dump(RawDumpCommand::default())),
643            set_severity: vec![OneOrMany::One(
644                parse_log_interest_selector("test_selector#DEBUG").unwrap(),
645            )],
646            ..LogCommand::default()
647        };
648        let expectations = [
649            ("test_selector", diagnostics_data::Severity::Debug, true),
650            ("other_selector", diagnostics_data::Severity::Debug, false),
651            ("other_selector", diagnostics_data::Severity::Info, true),
652        ];
653        let criteria = LogFilterCriteria::from(cmd);
654        assert_eq!(criteria.min_severity, Severity::Info);
655        for (moniker, severity, is_included) in expectations {
656            let entry = make_log_entry(
657                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
658                    timestamp: Timestamp::from_nanos(0),
659                    component_url: Some("".into()),
660                    moniker: moniker.try_into().unwrap(),
661                    severity,
662                })
663                .set_message("message")
664                .add_tag("tag1")
665                .add_tag("tag2")
666                .build()
667                .into(),
668            );
669            assert_eq!(criteria.matches(&entry), is_included);
670        }
671    }
672
673    #[fuchsia::test]
674    async fn test_per_component_severity_uses_min_match() {
675        let severities = [
676            diagnostics_data::Severity::Info,
677            diagnostics_data::Severity::Trace,
678            diagnostics_data::Severity::Debug,
679        ];
680
681        let cmd = LogCommand {
682            sub_command: Some(LogSubCommand::Dump(RawDumpCommand::default())),
683            set_severity: vec![
684                OneOrMany::One(parse_log_interest_selector("test_selector#INFO").unwrap()),
685                OneOrMany::One(parse_log_interest_selector("test_selector#TRACE").unwrap()),
686                OneOrMany::One(parse_log_interest_selector("test_selector#DEBUG").unwrap()),
687            ],
688            ..LogCommand::default()
689        };
690        let criteria = LogFilterCriteria::from(cmd);
691
692        for severity in severities {
693            let entry = make_log_entry(
694                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
695                    timestamp: Timestamp::from_nanos(0),
696                    component_url: Some("".into()),
697                    moniker: "test_selector".try_into().unwrap(),
698                    severity,
699                })
700                .set_message("message")
701                .add_tag("tag1")
702                .add_tag("tag2")
703                .build()
704                .into(),
705            );
706            assert!(criteria.matches(&entry));
707        }
708    }
709
710    #[fuchsia::test]
711    async fn test_criteria_tag_filter_legacy() {
712        let cmd = LogCommand {
713            filters: LogFilterArgs {
714                tag: vec!["tag1".to_string()],
715                exclude_tags: vec!["tag3".to_string()],
716                ..Default::default()
717            },
718            ..empty_dump_command()
719        };
720        let criteria = LogFilterCriteria::from(cmd);
721
722        assert!(
723            criteria.matches(&make_log_entry(
724                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
725                    timestamp: Timestamp::from_nanos(0),
726                    component_url: Some("".into()),
727                    moniker: ExtendedMoniker::ComponentManager,
728                    severity: diagnostics_data::Severity::Error,
729                })
730                .set_message("included")
731                .add_tag("tag1")
732                .add_tag("tag2")
733                .build()
734                .into()
735            ))
736        );
737
738        assert!(
739            !criteria.matches(&make_log_entry(
740                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
741                    timestamp: Timestamp::from_nanos(0),
742                    component_url: Some("".into()),
743                    moniker: ExtendedMoniker::ComponentManager,
744                    severity: diagnostics_data::Severity::Error,
745                })
746                .set_message("included")
747                .add_tag("tag2")
748                .build()
749                .into()
750            ))
751        );
752        assert!(
753            !criteria.matches(&make_log_entry(
754                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
755                    timestamp: Timestamp::from_nanos(0),
756                    component_url: Some("".into()),
757                    moniker: ExtendedMoniker::ComponentManager,
758                    severity: diagnostics_data::Severity::Error,
759                })
760                .set_message("included")
761                .add_tag("tag1")
762                .add_tag("tag3")
763                .build()
764                .into()
765            ))
766        );
767    }
768
769    #[fuchsia::test]
770    async fn test_severity_filter_with_debug() {
771        let mut cmd = empty_dump_command();
772        cmd.filters.severity = Some(Severity::Trace);
773        let criteria = LogFilterCriteria::from(cmd);
774
775        assert!(
776            criteria.matches(&make_log_entry(
777                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
778                    timestamp: Timestamp::from_nanos(0),
779                    component_url: Some("".into()),
780                    moniker: "included/moniker".try_into().unwrap(),
781                    severity: diagnostics_data::Severity::Error,
782                })
783                .set_message("included message")
784                .build()
785                .into()
786            ))
787        );
788        assert!(
789            criteria.matches(&make_log_entry(
790                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
791                    timestamp: Timestamp::from_nanos(0),
792                    component_url: Some("".into()),
793                    moniker: "included/moniker".try_into().unwrap(),
794                    severity: diagnostics_data::Severity::Info,
795                })
796                .set_message("different message")
797                .build()
798                .into()
799            ))
800        );
801        assert!(
802            criteria.matches(&make_log_entry(
803                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
804                    timestamp: Timestamp::from_nanos(0),
805                    component_url: Some("".into()),
806                    moniker: "other/moniker".try_into().unwrap(),
807                    severity: diagnostics_data::Severity::Debug,
808                })
809                .set_message("included message")
810                .build()
811                .into()
812            ))
813        );
814    }
815
816    #[fuchsia::test]
817    async fn test_pid_filter() {
818        let mut cmd = empty_dump_command();
819        cmd.filters.pid = Some(123);
820        let criteria = LogFilterCriteria::from(cmd);
821
822        assert!(
823            criteria.matches(&make_log_entry(
824                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
825                    timestamp: Timestamp::from_nanos(0),
826                    component_url: Some("".into()),
827                    moniker: "included/moniker".try_into().unwrap(),
828                    severity: diagnostics_data::Severity::Error,
829                })
830                .set_message("included message")
831                .set_pid(123)
832                .build()
833                .into()
834            ))
835        );
836        assert!(
837            !criteria.matches(&make_log_entry(
838                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
839                    timestamp: Timestamp::from_nanos(0),
840                    component_url: Some("".into()),
841                    moniker: "included/moniker".try_into().unwrap(),
842                    severity: diagnostics_data::Severity::Error,
843                })
844                .set_message("included message")
845                .set_pid(456)
846                .build()
847                .into()
848            ))
849        );
850    }
851
852    struct FakeInstanceGetter;
853    impl InstanceGetter for FakeInstanceGetter {
854        async fn get_monikers_from_query(
855            &self,
856            query: &str,
857        ) -> Result<Vec<moniker::Moniker>, LogError> {
858            Ok(vec![moniker::Moniker::try_from(query).unwrap()])
859        }
860    }
861
862    #[fuchsia::test]
863    async fn test_criteria_component_filter() {
864        let cmd = LogCommand {
865            filters: LogFilterArgs {
866                component: vec!["/core/network/netstack".to_string()],
867                ..Default::default()
868            },
869            ..empty_dump_command()
870        };
871
872        let mut criteria = LogFilterCriteria::from(cmd);
873        criteria.expand_monikers(&FakeInstanceGetter).await.unwrap();
874
875        assert!(
876            !criteria.matches(&make_log_entry(
877                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
878                    timestamp: Timestamp::from_nanos(0),
879                    component_url: Some("".into()),
880                    moniker: "bootstrap/archivist".try_into().unwrap(),
881                    severity: diagnostics_data::Severity::Error,
882                })
883                .set_message("excluded")
884                .build()
885                .into()
886            ))
887        );
888
889        assert!(
890            criteria.matches(&make_log_entry(
891                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
892                    timestamp: Timestamp::from_nanos(0),
893                    component_url: Some("".into()),
894                    moniker: "core/network/netstack".try_into().unwrap(),
895                    severity: diagnostics_data::Severity::Error,
896                })
897                .set_message("included")
898                .build()
899                .into()
900            ))
901        );
902
903        assert!(
904            !criteria.matches(&make_log_entry(
905                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
906                    timestamp: Timestamp::from_nanos(0),
907                    component_url: Some("".into()),
908                    moniker: "core/network/dhcp".try_into().unwrap(),
909                    severity: diagnostics_data::Severity::Error,
910                })
911                .set_message("included")
912                .build()
913                .into()
914            ))
915        );
916    }
917
918    #[fuchsia::test]
919    async fn test_criteria_component_filter_not_found() {
920        let cmd = LogCommand {
921            filters: LogFilterArgs {
922                component: vec!["non_existent_component".to_string()],
923                ..Default::default()
924            },
925            ..empty_dump_command()
926        };
927
928        struct FakeInstanceGetterEmpty;
929        impl InstanceGetter for FakeInstanceGetterEmpty {
930            async fn get_monikers_from_query(
931                &self,
932                _query: &str,
933            ) -> Result<Vec<moniker::Moniker>, LogError> {
934                Ok(vec![])
935            }
936        }
937
938        let mut criteria = LogFilterCriteria::from(cmd);
939        let result = criteria.expand_monikers(&FakeInstanceGetterEmpty).await;
940
941        assert!(result.is_err());
942        let err = result.unwrap_err();
943        assert!(matches!(err, LogError::SearchParameterNotFound(_)));
944
945        let error_message = format!("{}", err);
946        assert_eq!(
947            error_message,
948            "No running components were found matching non_existent_component. Please ensure the component is running and the moniker is correct. Run 'ffx component list' to see running components."
949        );
950    }
951
952    #[fuchsia::test]
953    async fn test_tid_filter() {
954        let mut cmd = empty_dump_command();
955        cmd.filters.tid = Some(123);
956        let criteria = LogFilterCriteria::from(cmd);
957
958        assert!(
959            criteria.matches(&make_log_entry(
960                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
961                    timestamp: Timestamp::from_nanos(0),
962                    component_url: Some("".into()),
963                    moniker: "included/moniker".try_into().unwrap(),
964                    severity: diagnostics_data::Severity::Error,
965                })
966                .set_message("included message")
967                .set_tid(123)
968                .build()
969                .into()
970            ))
971        );
972        assert!(
973            !criteria.matches(&make_log_entry(
974                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
975                    timestamp: Timestamp::from_nanos(0),
976                    component_url: Some("".into()),
977                    moniker: "included/moniker".try_into().unwrap(),
978                    severity: diagnostics_data::Severity::Error,
979                })
980                .set_message("included message")
981                .set_tid(456)
982                .build()
983                .into()
984            ))
985        );
986    }
987
988    #[fuchsia::test]
989    async fn test_setter_functions() {
990        let mut filter = LogFilterCriteria::default();
991        filter.set_min_severity(Severity::Error);
992        assert_eq!(filter.min_severity, Severity::Error);
993        filter.set_tags(["tag1"]);
994        assert_eq!(filter.tags, ["tag1"]);
995        filter.set_exclude_tags(["tag2"]);
996        assert_eq!(filter.exclude_tags, ["tag2"]);
997    }
998
999    #[fuchsia::test]
1000    async fn test_criteria_moniker_message_and_severity_matches() {
1001        let cmd = LogCommand {
1002            filters: LogFilterArgs {
1003                filter: vec!["included".to_string()],
1004                exclude: vec!["not this".to_string()],
1005                severity: Some(Severity::Error),
1006                ..Default::default()
1007            },
1008            ..empty_dump_command()
1009        };
1010        let criteria = LogFilterCriteria::from(cmd);
1011
1012        assert!(
1013            criteria.matches(&make_log_entry(
1014                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1015                    timestamp: Timestamp::from_nanos(0),
1016                    component_url: Some("".into()),
1017                    moniker: "included/moniker".try_into().unwrap(),
1018                    severity: diagnostics_data::Severity::Error,
1019                })
1020                .set_message("included message")
1021                .build()
1022                .into()
1023            ))
1024        );
1025        assert!(
1026            criteria.matches(&make_log_entry(
1027                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1028                    timestamp: Timestamp::from_nanos(0),
1029                    component_url: Some("".into()),
1030                    moniker: "included/moniker".try_into().unwrap(),
1031                    severity: diagnostics_data::Severity::Fatal,
1032                })
1033                .set_message("included message")
1034                .build()
1035                .into()
1036            ))
1037        );
1038        assert!(
1039            criteria.matches(&make_log_entry(
1040                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1041                    timestamp: Timestamp::from_nanos(0),
1042                    component_url: Some("".into()),
1043                    // Include a "/" prefix on the moniker to test filter permissiveness.
1044                    moniker: "included/moniker".try_into().unwrap(),
1045                    severity: diagnostics_data::Severity::Fatal,
1046                })
1047                .set_message("included message")
1048                .build()
1049                .into()
1050            ))
1051        );
1052        assert!(
1053            !criteria.matches(&make_log_entry(
1054                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1055                    timestamp: Timestamp::from_nanos(0),
1056                    component_url: Some("".into()),
1057                    moniker: "not/this/moniker".try_into().unwrap(),
1058                    severity: diagnostics_data::Severity::Error,
1059                })
1060                .set_message("different message")
1061                .build()
1062                .into()
1063            ))
1064        );
1065        assert!(
1066            !criteria.matches(&make_log_entry(
1067                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1068                    timestamp: Timestamp::from_nanos(0),
1069                    component_url: Some("".into()),
1070                    moniker: "included/moniker".try_into().unwrap(),
1071                    severity: diagnostics_data::Severity::Warn,
1072                })
1073                .set_message("included message")
1074                .build()
1075                .into()
1076            ))
1077        );
1078        assert!(
1079            !criteria.matches(&make_log_entry(
1080                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1081                    timestamp: Timestamp::from_nanos(0),
1082                    component_url: Some("".into()),
1083                    moniker: "other/moniker".try_into().unwrap(),
1084                    severity: diagnostics_data::Severity::Error,
1085                })
1086                .set_message("not this message")
1087                .build()
1088                .into()
1089            ))
1090        );
1091        assert!(
1092            !criteria.matches(&make_log_entry(
1093                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1094                    timestamp: Timestamp::from_nanos(0),
1095                    component_url: Some("".into()),
1096                    moniker: "included/moniker".try_into().unwrap(),
1097                    severity: diagnostics_data::Severity::Error,
1098                })
1099                .set_message("not this message")
1100                .build()
1101                .into()
1102            ))
1103        );
1104    }
1105
1106    #[fuchsia::test]
1107    async fn test_criteria_klog_only() {
1108        let cmd = LogCommand {
1109            filters: LogFilterArgs { tag: vec!["component_manager".into()], ..Default::default() },
1110            ..empty_dump_command()
1111        };
1112        let criteria = LogFilterCriteria::from(cmd);
1113
1114        assert!(
1115            criteria.matches(&make_log_entry(
1116                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1117                    timestamp: Timestamp::from_nanos(0),
1118                    component_url: Some("".into()),
1119                    moniker: "klog".try_into().unwrap(),
1120                    severity: diagnostics_data::Severity::Error,
1121                })
1122                .set_message("[component_manager] included message")
1123                .build()
1124                .into()
1125            ))
1126        );
1127        assert!(
1128            !criteria.matches(&make_log_entry(
1129                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1130                    timestamp: Timestamp::from_nanos(0),
1131                    component_url: Some("".into()),
1132                    moniker: "klog".try_into().unwrap(),
1133                    severity: diagnostics_data::Severity::Error,
1134                })
1135                .set_message("excluded message[component_manager]")
1136                .build()
1137                .into()
1138            ))
1139        );
1140        assert!(
1141            criteria.matches(&make_log_entry(
1142                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1143                    timestamp: Timestamp::from_nanos(0),
1144                    component_url: Some("".into()),
1145                    moniker: "klog".try_into().unwrap(),
1146                    severity: diagnostics_data::Severity::Error,
1147                })
1148                .set_message("[tag0][component_manager] included message")
1149                .build()
1150                .into()
1151            ))
1152        );
1153        assert!(
1154            !criteria.matches(&make_log_entry(
1155                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1156                    timestamp: Timestamp::from_nanos(0),
1157                    component_url: Some("".into()),
1158                    moniker: "klog".try_into().unwrap(),
1159                    severity: diagnostics_data::Severity::Error,
1160                })
1161                .set_message("[other] excluded message")
1162                .build()
1163                .into()
1164            ))
1165        );
1166        assert!(
1167            !criteria.matches(&make_log_entry(
1168                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1169                    timestamp: Timestamp::from_nanos(0),
1170                    component_url: Some("".into()),
1171                    moniker: "klog".try_into().unwrap(),
1172                    severity: diagnostics_data::Severity::Error,
1173                })
1174                .set_message("no tags, excluded")
1175                .build()
1176                .into()
1177            ))
1178        );
1179        assert!(
1180            !criteria.matches(&make_log_entry(
1181                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1182                    timestamp: Timestamp::from_nanos(0),
1183                    component_url: Some("".into()),
1184                    moniker: "other/moniker".try_into().unwrap(),
1185                    severity: diagnostics_data::Severity::Error,
1186                })
1187                .set_message("[component_manager] excluded message")
1188                .build()
1189                .into()
1190            ))
1191        );
1192    }
1193
1194    #[fuchsia::test]
1195    async fn test_criteria_klog_tag_hack() {
1196        let cmd = LogCommand {
1197            filters: LogFilterArgs { kernel: true, ..Default::default() },
1198            ..empty_dump_command()
1199        };
1200        let mut criteria = LogFilterCriteria::from(cmd);
1201
1202        criteria.expand_monikers(&FakeInstanceGetter).await.unwrap();
1203
1204        assert!(
1205            criteria.matches(&make_log_entry(
1206                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1207                    timestamp: Timestamp::from_nanos(0),
1208                    component_url: Some("".into()),
1209                    moniker: "klog".try_into().unwrap(),
1210                    severity: diagnostics_data::Severity::Error,
1211                })
1212                .set_message("included message")
1213                .build()
1214                .into()
1215            ))
1216        );
1217        assert!(
1218            !criteria.matches(&make_log_entry(
1219                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1220                    timestamp: Timestamp::from_nanos(0),
1221                    component_url: Some("".into()),
1222                    moniker: "other/moniker".try_into().unwrap(),
1223                    severity: diagnostics_data::Severity::Error,
1224                })
1225                .set_message("included message")
1226                .build()
1227                .into()
1228            ))
1229        );
1230    }
1231
1232    #[test]
1233    fn filter_fiters_filename() {
1234        let cmd = LogCommand {
1235            filters: LogFilterArgs { filter: vec!["sometestfile".into()], ..Default::default() },
1236            ..empty_dump_command()
1237        };
1238        let criteria = LogFilterCriteria::from(cmd);
1239
1240        assert!(
1241            criteria.matches(&make_log_entry(
1242                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1243                    timestamp: Timestamp::from_nanos(0),
1244                    component_url: Some("".into()),
1245                    moniker: "core/last_segment".try_into().unwrap(),
1246                    severity: diagnostics_data::Severity::Error,
1247                })
1248                .set_file("sometestfile")
1249                .set_message("hello world")
1250                .build()
1251                .into()
1252            ))
1253        );
1254    }
1255
1256    #[fuchsia::test]
1257    async fn test_empty_criteria() {
1258        let cmd = empty_dump_command();
1259        let criteria = LogFilterCriteria::from(cmd);
1260
1261        assert!(
1262            criteria.matches(&make_log_entry(
1263                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1264                    timestamp: Timestamp::from_nanos(0),
1265                    component_url: Some("".into()),
1266                    moniker: "included/moniker".try_into().unwrap(),
1267                    severity: diagnostics_data::Severity::Error,
1268                })
1269                .set_message("included message")
1270                .build()
1271                .into()
1272            ))
1273        );
1274        assert!(
1275            criteria.matches(&make_log_entry(
1276                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1277                    timestamp: Timestamp::from_nanos(0),
1278                    component_url: Some("".into()),
1279                    moniker: "included/moniker".try_into().unwrap(),
1280                    severity: diagnostics_data::Severity::Info,
1281                })
1282                .set_message("different message")
1283                .build()
1284                .into()
1285            ))
1286        );
1287        assert!(
1288            !criteria.matches(&make_log_entry(
1289                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1290                    timestamp: Timestamp::from_nanos(0),
1291                    component_url: Some("".into()),
1292                    moniker: "other/moniker".try_into().unwrap(),
1293                    severity: diagnostics_data::Severity::Debug,
1294                })
1295                .set_message("included message")
1296                .build()
1297                .into()
1298            ))
1299        );
1300
1301        let entry = make_log_entry(
1302            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1303                timestamp: Timestamp::from_nanos(0),
1304                component_url: Some("".into()),
1305                moniker: "other/moniker".try_into().unwrap(),
1306                severity: diagnostics_data::Severity::Debug,
1307            })
1308            .set_message("included message")
1309            .build()
1310            .into(),
1311        );
1312
1313        assert!(!criteria.matches(&entry));
1314    }
1315
1316    #[test]
1317    fn filter_fiters_case_sensitivity() {
1318        // Case-insensitive by default
1319        let cmd = LogCommand {
1320            filters: LogFilterArgs { filter: vec!["sometestfile".into()], ..Default::default() },
1321            ..empty_dump_command()
1322        };
1323        let criteria = LogFilterCriteria::from(cmd);
1324
1325        let entry_0 = make_log_entry(
1326            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1327                timestamp: Timestamp::from_nanos(0),
1328                component_url: Some("".into()),
1329                moniker: "core/last_segment".try_into().unwrap(),
1330                severity: diagnostics_data::Severity::Error,
1331            })
1332            .set_file("sometestfile")
1333            .set_message("hello world")
1334            .build()
1335            .into(),
1336        );
1337
1338        let entry_1 = make_log_entry(
1339            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1340                timestamp: Timestamp::from_nanos(0),
1341                component_url: Some("".into()),
1342                moniker: "core/last_segment".try_into().unwrap(),
1343                severity: diagnostics_data::Severity::Error,
1344            })
1345            .set_file("someTESTfile")
1346            .set_message("hello world")
1347            .build()
1348            .into(),
1349        );
1350        assert!(criteria.matches(&entry_0));
1351        assert!(criteria.matches(&entry_1));
1352
1353        // Case-sensitive
1354        let cmd = LogCommand {
1355            filters: LogFilterArgs {
1356                filter: vec!["sometestfile".into()],
1357                case_sensitive: true,
1358                ..Default::default()
1359            },
1360            ..empty_dump_command()
1361        };
1362        let criteria = LogFilterCriteria::from(cmd);
1363
1364        assert!(criteria.matches(&entry_0));
1365        assert!(!criteria.matches(&entry_1));
1366    }
1367
1368    #[test]
1369    fn filter_fiters_case_sensitivity_for_tags() {
1370        // Case-insensitive by default
1371        let cmd = LogCommand {
1372            filters: LogFilterArgs { tag: vec!["someTAG".into()], ..Default::default() },
1373            ..empty_dump_command()
1374        };
1375        let criteria = LogFilterCriteria::from(cmd);
1376
1377        let entry_0 = make_log_entry(
1378            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1379                timestamp: Timestamp::from_nanos(0),
1380                component_url: Some("".into()),
1381                moniker: "core/last_segment".try_into().unwrap(),
1382                severity: diagnostics_data::Severity::Error,
1383            })
1384            .add_tag("someTAG")
1385            .set_message("hello world")
1386            .build()
1387            .into(),
1388        );
1389
1390        let entry_1 = make_log_entry(
1391            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1392                timestamp: Timestamp::from_nanos(0),
1393                component_url: Some("".into()),
1394                moniker: "core/last_segment".try_into().unwrap(),
1395                severity: diagnostics_data::Severity::Error,
1396            })
1397            .add_tag("SomeTaG")
1398            .set_message("hello world")
1399            .build()
1400            .into(),
1401        );
1402        assert!(criteria.matches(&entry_0));
1403        assert!(criteria.matches(&entry_1));
1404
1405        // Case-sensitive
1406        let cmd = LogCommand {
1407            filters: LogFilterArgs {
1408                tag: vec!["someTAG".into()],
1409                case_sensitive: true,
1410                ..Default::default()
1411            },
1412            ..empty_dump_command()
1413        };
1414        let criteria = LogFilterCriteria::from(cmd);
1415
1416        assert!(criteria.matches(&entry_0));
1417        assert!(!criteria.matches(&entry_1));
1418    }
1419
1420    #[test]
1421    fn filter_fiters_case_sensitivity_for_tags_including_moniker() {
1422        // Case-insensitive by default
1423        let cmd = LogCommand {
1424            filters: LogFilterArgs { tag: vec!["someTAG".into()], ..Default::default() },
1425            ..empty_dump_command()
1426        };
1427        let criteria = LogFilterCriteria::from(cmd);
1428
1429        let entry_0 = make_log_entry(
1430            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1431                timestamp: Timestamp::from_nanos(0),
1432                component_url: Some("".into()),
1433                moniker: "core/someTAG".try_into().unwrap(),
1434                severity: diagnostics_data::Severity::Error,
1435            })
1436            .set_message("hello world")
1437            .build()
1438            .into(),
1439        );
1440
1441        let entry_1 = make_log_entry(
1442            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1443                timestamp: Timestamp::from_nanos(0),
1444                component_url: Some("".into()),
1445                moniker: "core/SomeTaG".try_into().unwrap(),
1446                severity: diagnostics_data::Severity::Error,
1447            })
1448            .set_message("hello world")
1449            .build()
1450            .into(),
1451        );
1452        assert!(criteria.matches(&entry_0));
1453        assert!(criteria.matches(&entry_1));
1454
1455        // Case-sensitive
1456        let cmd = LogCommand {
1457            filters: LogFilterArgs {
1458                tag: vec!["someTAG".into()],
1459                case_sensitive: true,
1460                ..Default::default()
1461            },
1462            ..empty_dump_command()
1463        };
1464        let criteria = LogFilterCriteria::from(cmd);
1465
1466        assert!(criteria.matches(&entry_0));
1467        assert!(!criteria.matches(&entry_1));
1468    }
1469
1470    #[test]
1471    fn tag_matches_moniker_last_segment() {
1472        // When the tags are empty, the last segment of the moniker is treated as the tag.
1473        let cmd = LogCommand {
1474            filters: LogFilterArgs { tag: vec!["last_segment".to_string()], ..Default::default() },
1475            ..empty_dump_command()
1476        };
1477        let criteria = LogFilterCriteria::from(cmd);
1478
1479        assert!(
1480            criteria.matches(&make_log_entry(
1481                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1482                    timestamp: Timestamp::from_nanos(0),
1483                    component_url: Some("".into()),
1484                    moniker: "core/last_segment".try_into().unwrap(),
1485                    severity: diagnostics_data::Severity::Error,
1486                })
1487                .set_message("hello world")
1488                .build()
1489                .into()
1490            ))
1491        );
1492    }
1493
1494    #[test]
1495    fn test_criteria_exclude_regex() {
1496        let cmd = LogCommand {
1497            filters: LogFilterArgs {
1498                exclude_regex: vec!["foo.*bar".to_string(), "baz".to_string()],
1499                ..Default::default()
1500            },
1501            ..empty_dump_command()
1502        };
1503        let criteria = LogFilterCriteria::try_from(cmd).unwrap();
1504
1505        let entry_match_msg = make_log_entry(
1506            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1507                timestamp: Timestamp::from_nanos(0),
1508                component_url: Some("".into()),
1509                moniker: "core/test".try_into().unwrap(),
1510                severity: diagnostics_data::Severity::Error,
1511            })
1512            .set_message("hello foo middle bar world")
1513            .build()
1514            .into(),
1515        );
1516
1517        let entry_match_moniker = make_log_entry(
1518            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1519                timestamp: Timestamp::from_nanos(0),
1520                component_url: Some("".into()),
1521                moniker: "core/baz".try_into().unwrap(),
1522                severity: diagnostics_data::Severity::Error,
1523            })
1524            .set_message("hello world")
1525            .build()
1526            .into(),
1527        );
1528
1529        let entry_no_match = make_log_entry(
1530            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1531                timestamp: Timestamp::from_nanos(0),
1532                component_url: Some("".into()),
1533                moniker: "core/test".try_into().unwrap(),
1534                severity: diagnostics_data::Severity::Error,
1535            })
1536            .set_message("hello world")
1537            .build()
1538            .into(),
1539        );
1540
1541        assert!(!criteria.matches(&entry_match_msg));
1542        assert!(!criteria.matches(&entry_match_moniker));
1543        assert!(criteria.matches(&entry_no_match));
1544    }
1545
1546    #[test]
1547    fn test_criteria_exclude_regex_file() {
1548        let temp_dir = std::env::temp_dir();
1549        let file_path = temp_dir.join("exclude_patterns.txt");
1550        std::fs::write(&file_path, "# comment\n\nfoo.*bar\nbaz\n").unwrap();
1551
1552        let cmd = LogCommand {
1553            filters: LogFilterArgs {
1554                exclude_regex_file: Some(file_path.to_string_lossy().to_string()),
1555                ..Default::default()
1556            },
1557            ..empty_dump_command()
1558        };
1559        let criteria = LogFilterCriteria::try_from(cmd).unwrap();
1560
1561        let entry_match_msg = make_log_entry(
1562            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1563                timestamp: Timestamp::from_nanos(0),
1564                component_url: Some("".into()),
1565                moniker: "core/test".try_into().unwrap(),
1566                severity: diagnostics_data::Severity::Error,
1567            })
1568            .set_message("hello foo middle bar world")
1569            .build()
1570            .into(),
1571        );
1572
1573        let entry_no_match = make_log_entry(
1574            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1575                timestamp: Timestamp::from_nanos(0),
1576                component_url: Some("".into()),
1577                moniker: "core/test".try_into().unwrap(),
1578                severity: diagnostics_data::Severity::Error,
1579            })
1580            .set_message("hello world")
1581            .build()
1582            .into(),
1583        );
1584
1585        assert!(!criteria.matches(&entry_match_msg), "entry_match_msg should be filtered out");
1586        assert!(criteria.matches(&entry_no_match), "entry_no_match should NOT be filtered out");
1587
1588        std::fs::remove_file(file_path).ok();
1589    }
1590
1591    #[test]
1592    fn test_disambiguate_queried_monikers_exact_full_match() {
1593        let instances = vec![
1594            Moniker::try_from("bootstrap/archivist").unwrap(),
1595            Moniker::try_from("bootstrap/archivist/archivist-pipelines").unwrap(),
1596        ];
1597        assert_eq!(
1598            disambiguate_queried_monikers("bootstrap/archivist", instances.clone()),
1599            vec![Moniker::try_from("bootstrap/archivist").unwrap()]
1600        );
1601        assert_eq!(
1602            disambiguate_queried_monikers("/bootstrap/archivist", instances),
1603            vec![Moniker::try_from("bootstrap/archivist").unwrap()]
1604        );
1605    }
1606
1607    #[test]
1608    fn test_disambiguate_queried_monikers_exact_leaf_match() {
1609        let instances = vec![
1610            Moniker::try_from("bootstrap/archivist").unwrap(),
1611            Moniker::try_from("bootstrap/archivist/archivist-pipelines").unwrap(),
1612        ];
1613        assert_eq!(
1614            disambiguate_queried_monikers("archivist", instances),
1615            vec![Moniker::try_from("bootstrap/archivist").unwrap()]
1616        );
1617
1618        let instances_with_extra = vec![
1619            Moniker::try_from("core/network/netstack").unwrap(),
1620            Moniker::try_from("core/network/netstack/child").unwrap(),
1621            Moniker::try_from("core/network/netstack-extra").unwrap(),
1622        ];
1623        assert_eq!(
1624            disambiguate_queried_monikers("netstack", instances_with_extra),
1625            vec![Moniker::try_from("core/network/netstack").unwrap()]
1626        );
1627    }
1628
1629    #[test]
1630    fn test_disambiguate_queried_monikers_exact_path_suffix_match() {
1631        let instances = vec![
1632            Moniker::try_from("core/network/netstack").unwrap(),
1633            Moniker::try_from("core/network/netstack/child").unwrap(),
1634        ];
1635        assert_eq!(
1636            disambiguate_queried_monikers("network/netstack", instances),
1637            vec![Moniker::try_from("core/network/netstack").unwrap()]
1638        );
1639    }
1640
1641    #[test]
1642    fn test_disambiguate_queried_monikers_trailing_slash() {
1643        let instances = vec![
1644            Moniker::try_from("bootstrap/archivist").unwrap(),
1645            Moniker::try_from("bootstrap/archivist/archivist-pipelines").unwrap(),
1646        ];
1647        assert_eq!(
1648            disambiguate_queried_monikers("archivist/", instances),
1649            vec![Moniker::try_from("bootstrap/archivist").unwrap()]
1650        );
1651    }
1652
1653    #[test]
1654    fn test_disambiguate_queried_monikers_ambiguous_distinct_roots() {
1655        let instances = vec![
1656            Moniker::try_from("bootstrap/archivist").unwrap(),
1657            Moniker::try_from("bootstrap/archivist/archivist-pipelines").unwrap(),
1658            Moniker::try_from("core/archivist").unwrap(),
1659        ];
1660        // archivist matches leaf of bootstrap/archivist and core/archivist, but not archivist-pipelines
1661        assert_eq!(
1662            disambiguate_queried_monikers("archivist", instances),
1663            vec![
1664                Moniker::try_from("bootstrap/archivist").unwrap(),
1665                Moniker::try_from("core/archivist").unwrap(),
1666            ]
1667        );
1668    }
1669
1670    #[test]
1671    fn test_disambiguate_queried_monikers_nested_root_preference() {
1672        let instances = vec![
1673            Moniker::try_from("bootstrap/archivist/pipelines").unwrap(),
1674            Moniker::try_from("bootstrap/archivist/pipelines/sub").unwrap(),
1675        ];
1676        assert_eq!(
1677            disambiguate_queried_monikers("archivist", instances),
1678            vec![Moniker::try_from("bootstrap/archivist/pipelines").unwrap()]
1679        );
1680    }
1681
1682    struct MultiFakeInstanceGetter {
1683        map: std::collections::HashMap<String, Vec<Moniker>>,
1684    }
1685
1686    impl InstanceGetter for MultiFakeInstanceGetter {
1687        async fn get_monikers_from_query(&self, query: &str) -> Result<Vec<Moniker>, LogError> {
1688            Ok(self.map.get(query).cloned().unwrap_or_default())
1689        }
1690    }
1691
1692    #[fuchsia::test]
1693    async fn test_expand_monikers_resolves_archivist_subcomponents() {
1694        let mut map = std::collections::HashMap::new();
1695        map.insert(
1696            "archivist".to_string(),
1697            vec![
1698                Moniker::try_from("bootstrap/archivist").unwrap(),
1699                Moniker::try_from("bootstrap/archivist/archivist-pipelines").unwrap(),
1700            ],
1701        );
1702        let getter = MultiFakeInstanceGetter { map };
1703
1704        let cmd = LogCommand {
1705            filters: LogFilterArgs {
1706                component: vec!["archivist".to_string()],
1707                ..Default::default()
1708            },
1709            ..empty_dump_command()
1710        };
1711
1712        let mut criteria = LogFilterCriteria::from(cmd);
1713        let warnings = criteria.expand_monikers(&getter).await.unwrap();
1714
1715        assert_eq!(
1716            warnings,
1717            vec![FuzzyMatchWarning {
1718                query: "archivist".to_string(),
1719                resolved: "bootstrap/archivist".to_string(),
1720            }]
1721        );
1722
1723        assert!(
1724            criteria.matches(&make_log_entry(
1725                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1726                    timestamp: Timestamp::from_nanos(0),
1727                    component_url: Some("".into()),
1728                    moniker: "bootstrap/archivist".try_into().unwrap(),
1729                    severity: diagnostics_data::Severity::Info,
1730                })
1731                .set_message("log from archivist")
1732                .build()
1733                .into()
1734            ))
1735        );
1736
1737        assert!(
1738            !criteria.matches(&make_log_entry(
1739                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1740                    timestamp: Timestamp::from_nanos(0),
1741                    component_url: Some("".into()),
1742                    moniker: "bootstrap/archivist/archivist-pipelines".try_into().unwrap(),
1743                    severity: diagnostics_data::Severity::Info,
1744                })
1745                .set_message("log from pipeline")
1746                .build()
1747                .into()
1748            ))
1749        );
1750    }
1751}