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    #[async_trait::async_trait(?Send)]
854    impl InstanceGetter for FakeInstanceGetter {
855        async fn get_monikers_from_query(
856            &self,
857            query: &str,
858        ) -> Result<Vec<moniker::Moniker>, LogError> {
859            Ok(vec![moniker::Moniker::try_from(query).unwrap()])
860        }
861    }
862
863    #[fuchsia::test]
864    async fn test_criteria_component_filter() {
865        let cmd = LogCommand {
866            filters: LogFilterArgs {
867                component: vec!["/core/network/netstack".to_string()],
868                ..Default::default()
869            },
870            ..empty_dump_command()
871        };
872
873        let mut criteria = LogFilterCriteria::from(cmd);
874        criteria.expand_monikers(&FakeInstanceGetter).await.unwrap();
875
876        assert!(
877            !criteria.matches(&make_log_entry(
878                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
879                    timestamp: Timestamp::from_nanos(0),
880                    component_url: Some("".into()),
881                    moniker: "bootstrap/archivist".try_into().unwrap(),
882                    severity: diagnostics_data::Severity::Error,
883                })
884                .set_message("excluded")
885                .build()
886                .into()
887            ))
888        );
889
890        assert!(
891            criteria.matches(&make_log_entry(
892                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
893                    timestamp: Timestamp::from_nanos(0),
894                    component_url: Some("".into()),
895                    moniker: "core/network/netstack".try_into().unwrap(),
896                    severity: diagnostics_data::Severity::Error,
897                })
898                .set_message("included")
899                .build()
900                .into()
901            ))
902        );
903
904        assert!(
905            !criteria.matches(&make_log_entry(
906                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
907                    timestamp: Timestamp::from_nanos(0),
908                    component_url: Some("".into()),
909                    moniker: "core/network/dhcp".try_into().unwrap(),
910                    severity: diagnostics_data::Severity::Error,
911                })
912                .set_message("included")
913                .build()
914                .into()
915            ))
916        );
917    }
918
919    #[fuchsia::test]
920    async fn test_criteria_component_filter_not_found() {
921        let cmd = LogCommand {
922            filters: LogFilterArgs {
923                component: vec!["non_existent_component".to_string()],
924                ..Default::default()
925            },
926            ..empty_dump_command()
927        };
928
929        struct FakeInstanceGetterEmpty;
930        #[async_trait::async_trait(?Send)]
931        impl InstanceGetter for FakeInstanceGetterEmpty {
932            async fn get_monikers_from_query(
933                &self,
934                _query: &str,
935            ) -> Result<Vec<moniker::Moniker>, LogError> {
936                Ok(vec![])
937            }
938        }
939
940        let mut criteria = LogFilterCriteria::from(cmd);
941        let result = criteria.expand_monikers(&FakeInstanceGetterEmpty).await;
942
943        assert!(result.is_err());
944        let err = result.unwrap_err();
945        assert!(matches!(err, LogError::SearchParameterNotFound(_)));
946
947        let error_message = format!("{}", err);
948        assert_eq!(
949            error_message,
950            "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."
951        );
952    }
953
954    #[fuchsia::test]
955    async fn test_tid_filter() {
956        let mut cmd = empty_dump_command();
957        cmd.filters.tid = Some(123);
958        let criteria = LogFilterCriteria::from(cmd);
959
960        assert!(
961            criteria.matches(&make_log_entry(
962                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
963                    timestamp: Timestamp::from_nanos(0),
964                    component_url: Some("".into()),
965                    moniker: "included/moniker".try_into().unwrap(),
966                    severity: diagnostics_data::Severity::Error,
967                })
968                .set_message("included message")
969                .set_tid(123)
970                .build()
971                .into()
972            ))
973        );
974        assert!(
975            !criteria.matches(&make_log_entry(
976                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
977                    timestamp: Timestamp::from_nanos(0),
978                    component_url: Some("".into()),
979                    moniker: "included/moniker".try_into().unwrap(),
980                    severity: diagnostics_data::Severity::Error,
981                })
982                .set_message("included message")
983                .set_tid(456)
984                .build()
985                .into()
986            ))
987        );
988    }
989
990    #[fuchsia::test]
991    async fn test_setter_functions() {
992        let mut filter = LogFilterCriteria::default();
993        filter.set_min_severity(Severity::Error);
994        assert_eq!(filter.min_severity, Severity::Error);
995        filter.set_tags(["tag1"]);
996        assert_eq!(filter.tags, ["tag1"]);
997        filter.set_exclude_tags(["tag2"]);
998        assert_eq!(filter.exclude_tags, ["tag2"]);
999    }
1000
1001    #[fuchsia::test]
1002    async fn test_criteria_moniker_message_and_severity_matches() {
1003        let cmd = LogCommand {
1004            filters: LogFilterArgs {
1005                filter: vec!["included".to_string()],
1006                exclude: vec!["not this".to_string()],
1007                severity: Some(Severity::Error),
1008                ..Default::default()
1009            },
1010            ..empty_dump_command()
1011        };
1012        let criteria = LogFilterCriteria::from(cmd);
1013
1014        assert!(
1015            criteria.matches(&make_log_entry(
1016                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1017                    timestamp: Timestamp::from_nanos(0),
1018                    component_url: Some("".into()),
1019                    moniker: "included/moniker".try_into().unwrap(),
1020                    severity: diagnostics_data::Severity::Error,
1021                })
1022                .set_message("included message")
1023                .build()
1024                .into()
1025            ))
1026        );
1027        assert!(
1028            criteria.matches(&make_log_entry(
1029                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1030                    timestamp: Timestamp::from_nanos(0),
1031                    component_url: Some("".into()),
1032                    moniker: "included/moniker".try_into().unwrap(),
1033                    severity: diagnostics_data::Severity::Fatal,
1034                })
1035                .set_message("included message")
1036                .build()
1037                .into()
1038            ))
1039        );
1040        assert!(
1041            criteria.matches(&make_log_entry(
1042                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1043                    timestamp: Timestamp::from_nanos(0),
1044                    component_url: Some("".into()),
1045                    // Include a "/" prefix on the moniker to test filter permissiveness.
1046                    moniker: "included/moniker".try_into().unwrap(),
1047                    severity: diagnostics_data::Severity::Fatal,
1048                })
1049                .set_message("included message")
1050                .build()
1051                .into()
1052            ))
1053        );
1054        assert!(
1055            !criteria.matches(&make_log_entry(
1056                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1057                    timestamp: Timestamp::from_nanos(0),
1058                    component_url: Some("".into()),
1059                    moniker: "not/this/moniker".try_into().unwrap(),
1060                    severity: diagnostics_data::Severity::Error,
1061                })
1062                .set_message("different message")
1063                .build()
1064                .into()
1065            ))
1066        );
1067        assert!(
1068            !criteria.matches(&make_log_entry(
1069                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1070                    timestamp: Timestamp::from_nanos(0),
1071                    component_url: Some("".into()),
1072                    moniker: "included/moniker".try_into().unwrap(),
1073                    severity: diagnostics_data::Severity::Warn,
1074                })
1075                .set_message("included message")
1076                .build()
1077                .into()
1078            ))
1079        );
1080        assert!(
1081            !criteria.matches(&make_log_entry(
1082                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1083                    timestamp: Timestamp::from_nanos(0),
1084                    component_url: Some("".into()),
1085                    moniker: "other/moniker".try_into().unwrap(),
1086                    severity: diagnostics_data::Severity::Error,
1087                })
1088                .set_message("not this message")
1089                .build()
1090                .into()
1091            ))
1092        );
1093        assert!(
1094            !criteria.matches(&make_log_entry(
1095                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1096                    timestamp: Timestamp::from_nanos(0),
1097                    component_url: Some("".into()),
1098                    moniker: "included/moniker".try_into().unwrap(),
1099                    severity: diagnostics_data::Severity::Error,
1100                })
1101                .set_message("not this message")
1102                .build()
1103                .into()
1104            ))
1105        );
1106    }
1107
1108    #[fuchsia::test]
1109    async fn test_criteria_klog_only() {
1110        let cmd = LogCommand {
1111            filters: LogFilterArgs { tag: vec!["component_manager".into()], ..Default::default() },
1112            ..empty_dump_command()
1113        };
1114        let criteria = LogFilterCriteria::from(cmd);
1115
1116        assert!(
1117            criteria.matches(&make_log_entry(
1118                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1119                    timestamp: Timestamp::from_nanos(0),
1120                    component_url: Some("".into()),
1121                    moniker: "klog".try_into().unwrap(),
1122                    severity: diagnostics_data::Severity::Error,
1123                })
1124                .set_message("[component_manager] included message")
1125                .build()
1126                .into()
1127            ))
1128        );
1129        assert!(
1130            !criteria.matches(&make_log_entry(
1131                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1132                    timestamp: Timestamp::from_nanos(0),
1133                    component_url: Some("".into()),
1134                    moniker: "klog".try_into().unwrap(),
1135                    severity: diagnostics_data::Severity::Error,
1136                })
1137                .set_message("excluded message[component_manager]")
1138                .build()
1139                .into()
1140            ))
1141        );
1142        assert!(
1143            criteria.matches(&make_log_entry(
1144                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1145                    timestamp: Timestamp::from_nanos(0),
1146                    component_url: Some("".into()),
1147                    moniker: "klog".try_into().unwrap(),
1148                    severity: diagnostics_data::Severity::Error,
1149                })
1150                .set_message("[tag0][component_manager] included message")
1151                .build()
1152                .into()
1153            ))
1154        );
1155        assert!(
1156            !criteria.matches(&make_log_entry(
1157                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1158                    timestamp: Timestamp::from_nanos(0),
1159                    component_url: Some("".into()),
1160                    moniker: "klog".try_into().unwrap(),
1161                    severity: diagnostics_data::Severity::Error,
1162                })
1163                .set_message("[other] excluded message")
1164                .build()
1165                .into()
1166            ))
1167        );
1168        assert!(
1169            !criteria.matches(&make_log_entry(
1170                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1171                    timestamp: Timestamp::from_nanos(0),
1172                    component_url: Some("".into()),
1173                    moniker: "klog".try_into().unwrap(),
1174                    severity: diagnostics_data::Severity::Error,
1175                })
1176                .set_message("no tags, excluded")
1177                .build()
1178                .into()
1179            ))
1180        );
1181        assert!(
1182            !criteria.matches(&make_log_entry(
1183                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1184                    timestamp: Timestamp::from_nanos(0),
1185                    component_url: Some("".into()),
1186                    moniker: "other/moniker".try_into().unwrap(),
1187                    severity: diagnostics_data::Severity::Error,
1188                })
1189                .set_message("[component_manager] excluded message")
1190                .build()
1191                .into()
1192            ))
1193        );
1194    }
1195
1196    #[fuchsia::test]
1197    async fn test_criteria_klog_tag_hack() {
1198        let cmd = LogCommand {
1199            filters: LogFilterArgs { kernel: true, ..Default::default() },
1200            ..empty_dump_command()
1201        };
1202        let mut criteria = LogFilterCriteria::from(cmd);
1203
1204        criteria.expand_monikers(&FakeInstanceGetter).await.unwrap();
1205
1206        assert!(
1207            criteria.matches(&make_log_entry(
1208                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1209                    timestamp: Timestamp::from_nanos(0),
1210                    component_url: Some("".into()),
1211                    moniker: "klog".try_into().unwrap(),
1212                    severity: diagnostics_data::Severity::Error,
1213                })
1214                .set_message("included message")
1215                .build()
1216                .into()
1217            ))
1218        );
1219        assert!(
1220            !criteria.matches(&make_log_entry(
1221                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1222                    timestamp: Timestamp::from_nanos(0),
1223                    component_url: Some("".into()),
1224                    moniker: "other/moniker".try_into().unwrap(),
1225                    severity: diagnostics_data::Severity::Error,
1226                })
1227                .set_message("included message")
1228                .build()
1229                .into()
1230            ))
1231        );
1232    }
1233
1234    #[test]
1235    fn filter_fiters_filename() {
1236        let cmd = LogCommand {
1237            filters: LogFilterArgs { filter: vec!["sometestfile".into()], ..Default::default() },
1238            ..empty_dump_command()
1239        };
1240        let criteria = LogFilterCriteria::from(cmd);
1241
1242        assert!(
1243            criteria.matches(&make_log_entry(
1244                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1245                    timestamp: Timestamp::from_nanos(0),
1246                    component_url: Some("".into()),
1247                    moniker: "core/last_segment".try_into().unwrap(),
1248                    severity: diagnostics_data::Severity::Error,
1249                })
1250                .set_file("sometestfile")
1251                .set_message("hello world")
1252                .build()
1253                .into()
1254            ))
1255        );
1256    }
1257
1258    #[fuchsia::test]
1259    async fn test_empty_criteria() {
1260        let cmd = empty_dump_command();
1261        let criteria = LogFilterCriteria::from(cmd);
1262
1263        assert!(
1264            criteria.matches(&make_log_entry(
1265                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1266                    timestamp: Timestamp::from_nanos(0),
1267                    component_url: Some("".into()),
1268                    moniker: "included/moniker".try_into().unwrap(),
1269                    severity: diagnostics_data::Severity::Error,
1270                })
1271                .set_message("included message")
1272                .build()
1273                .into()
1274            ))
1275        );
1276        assert!(
1277            criteria.matches(&make_log_entry(
1278                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1279                    timestamp: Timestamp::from_nanos(0),
1280                    component_url: Some("".into()),
1281                    moniker: "included/moniker".try_into().unwrap(),
1282                    severity: diagnostics_data::Severity::Info,
1283                })
1284                .set_message("different message")
1285                .build()
1286                .into()
1287            ))
1288        );
1289        assert!(
1290            !criteria.matches(&make_log_entry(
1291                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1292                    timestamp: Timestamp::from_nanos(0),
1293                    component_url: Some("".into()),
1294                    moniker: "other/moniker".try_into().unwrap(),
1295                    severity: diagnostics_data::Severity::Debug,
1296                })
1297                .set_message("included message")
1298                .build()
1299                .into()
1300            ))
1301        );
1302
1303        let entry = make_log_entry(
1304            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1305                timestamp: Timestamp::from_nanos(0),
1306                component_url: Some("".into()),
1307                moniker: "other/moniker".try_into().unwrap(),
1308                severity: diagnostics_data::Severity::Debug,
1309            })
1310            .set_message("included message")
1311            .build()
1312            .into(),
1313        );
1314
1315        assert!(!criteria.matches(&entry));
1316    }
1317
1318    #[test]
1319    fn filter_fiters_case_sensitivity() {
1320        // Case-insensitive by default
1321        let cmd = LogCommand {
1322            filters: LogFilterArgs { filter: vec!["sometestfile".into()], ..Default::default() },
1323            ..empty_dump_command()
1324        };
1325        let criteria = LogFilterCriteria::from(cmd);
1326
1327        let entry_0 = make_log_entry(
1328            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1329                timestamp: Timestamp::from_nanos(0),
1330                component_url: Some("".into()),
1331                moniker: "core/last_segment".try_into().unwrap(),
1332                severity: diagnostics_data::Severity::Error,
1333            })
1334            .set_file("sometestfile")
1335            .set_message("hello world")
1336            .build()
1337            .into(),
1338        );
1339
1340        let entry_1 = make_log_entry(
1341            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1342                timestamp: Timestamp::from_nanos(0),
1343                component_url: Some("".into()),
1344                moniker: "core/last_segment".try_into().unwrap(),
1345                severity: diagnostics_data::Severity::Error,
1346            })
1347            .set_file("someTESTfile")
1348            .set_message("hello world")
1349            .build()
1350            .into(),
1351        );
1352        assert!(criteria.matches(&entry_0));
1353        assert!(criteria.matches(&entry_1));
1354
1355        // Case-sensitive
1356        let cmd = LogCommand {
1357            filters: LogFilterArgs {
1358                filter: vec!["sometestfile".into()],
1359                case_sensitive: true,
1360                ..Default::default()
1361            },
1362            ..empty_dump_command()
1363        };
1364        let criteria = LogFilterCriteria::from(cmd);
1365
1366        assert!(criteria.matches(&entry_0));
1367        assert!(!criteria.matches(&entry_1));
1368    }
1369
1370    #[test]
1371    fn filter_fiters_case_sensitivity_for_tags() {
1372        // Case-insensitive by default
1373        let cmd = LogCommand {
1374            filters: LogFilterArgs { tag: vec!["someTAG".into()], ..Default::default() },
1375            ..empty_dump_command()
1376        };
1377        let criteria = LogFilterCriteria::from(cmd);
1378
1379        let entry_0 = make_log_entry(
1380            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1381                timestamp: Timestamp::from_nanos(0),
1382                component_url: Some("".into()),
1383                moniker: "core/last_segment".try_into().unwrap(),
1384                severity: diagnostics_data::Severity::Error,
1385            })
1386            .add_tag("someTAG")
1387            .set_message("hello world")
1388            .build()
1389            .into(),
1390        );
1391
1392        let entry_1 = make_log_entry(
1393            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1394                timestamp: Timestamp::from_nanos(0),
1395                component_url: Some("".into()),
1396                moniker: "core/last_segment".try_into().unwrap(),
1397                severity: diagnostics_data::Severity::Error,
1398            })
1399            .add_tag("SomeTaG")
1400            .set_message("hello world")
1401            .build()
1402            .into(),
1403        );
1404        assert!(criteria.matches(&entry_0));
1405        assert!(criteria.matches(&entry_1));
1406
1407        // Case-sensitive
1408        let cmd = LogCommand {
1409            filters: LogFilterArgs {
1410                tag: vec!["someTAG".into()],
1411                case_sensitive: true,
1412                ..Default::default()
1413            },
1414            ..empty_dump_command()
1415        };
1416        let criteria = LogFilterCriteria::from(cmd);
1417
1418        assert!(criteria.matches(&entry_0));
1419        assert!(!criteria.matches(&entry_1));
1420    }
1421
1422    #[test]
1423    fn filter_fiters_case_sensitivity_for_tags_including_moniker() {
1424        // Case-insensitive by default
1425        let cmd = LogCommand {
1426            filters: LogFilterArgs { tag: vec!["someTAG".into()], ..Default::default() },
1427            ..empty_dump_command()
1428        };
1429        let criteria = LogFilterCriteria::from(cmd);
1430
1431        let entry_0 = make_log_entry(
1432            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1433                timestamp: Timestamp::from_nanos(0),
1434                component_url: Some("".into()),
1435                moniker: "core/someTAG".try_into().unwrap(),
1436                severity: diagnostics_data::Severity::Error,
1437            })
1438            .set_message("hello world")
1439            .build()
1440            .into(),
1441        );
1442
1443        let entry_1 = make_log_entry(
1444            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1445                timestamp: Timestamp::from_nanos(0),
1446                component_url: Some("".into()),
1447                moniker: "core/SomeTaG".try_into().unwrap(),
1448                severity: diagnostics_data::Severity::Error,
1449            })
1450            .set_message("hello world")
1451            .build()
1452            .into(),
1453        );
1454        assert!(criteria.matches(&entry_0));
1455        assert!(criteria.matches(&entry_1));
1456
1457        // Case-sensitive
1458        let cmd = LogCommand {
1459            filters: LogFilterArgs {
1460                tag: vec!["someTAG".into()],
1461                case_sensitive: true,
1462                ..Default::default()
1463            },
1464            ..empty_dump_command()
1465        };
1466        let criteria = LogFilterCriteria::from(cmd);
1467
1468        assert!(criteria.matches(&entry_0));
1469        assert!(!criteria.matches(&entry_1));
1470    }
1471
1472    #[test]
1473    fn tag_matches_moniker_last_segment() {
1474        // When the tags are empty, the last segment of the moniker is treated as the tag.
1475        let cmd = LogCommand {
1476            filters: LogFilterArgs { tag: vec!["last_segment".to_string()], ..Default::default() },
1477            ..empty_dump_command()
1478        };
1479        let criteria = LogFilterCriteria::from(cmd);
1480
1481        assert!(
1482            criteria.matches(&make_log_entry(
1483                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1484                    timestamp: Timestamp::from_nanos(0),
1485                    component_url: Some("".into()),
1486                    moniker: "core/last_segment".try_into().unwrap(),
1487                    severity: diagnostics_data::Severity::Error,
1488                })
1489                .set_message("hello world")
1490                .build()
1491                .into()
1492            ))
1493        );
1494    }
1495
1496    #[test]
1497    fn test_criteria_exclude_regex() {
1498        let cmd = LogCommand {
1499            filters: LogFilterArgs {
1500                exclude_regex: vec!["foo.*bar".to_string(), "baz".to_string()],
1501                ..Default::default()
1502            },
1503            ..empty_dump_command()
1504        };
1505        let criteria = LogFilterCriteria::try_from(cmd).unwrap();
1506
1507        let entry_match_msg = make_log_entry(
1508            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1509                timestamp: Timestamp::from_nanos(0),
1510                component_url: Some("".into()),
1511                moniker: "core/test".try_into().unwrap(),
1512                severity: diagnostics_data::Severity::Error,
1513            })
1514            .set_message("hello foo middle bar world")
1515            .build()
1516            .into(),
1517        );
1518
1519        let entry_match_moniker = make_log_entry(
1520            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1521                timestamp: Timestamp::from_nanos(0),
1522                component_url: Some("".into()),
1523                moniker: "core/baz".try_into().unwrap(),
1524                severity: diagnostics_data::Severity::Error,
1525            })
1526            .set_message("hello world")
1527            .build()
1528            .into(),
1529        );
1530
1531        let entry_no_match = make_log_entry(
1532            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1533                timestamp: Timestamp::from_nanos(0),
1534                component_url: Some("".into()),
1535                moniker: "core/test".try_into().unwrap(),
1536                severity: diagnostics_data::Severity::Error,
1537            })
1538            .set_message("hello world")
1539            .build()
1540            .into(),
1541        );
1542
1543        assert!(!criteria.matches(&entry_match_msg));
1544        assert!(!criteria.matches(&entry_match_moniker));
1545        assert!(criteria.matches(&entry_no_match));
1546    }
1547
1548    #[test]
1549    fn test_criteria_exclude_regex_file() {
1550        let temp_dir = std::env::temp_dir();
1551        let file_path = temp_dir.join("exclude_patterns.txt");
1552        std::fs::write(&file_path, "# comment\n\nfoo.*bar\nbaz\n").unwrap();
1553
1554        let cmd = LogCommand {
1555            filters: LogFilterArgs {
1556                exclude_regex_file: Some(file_path.to_string_lossy().to_string()),
1557                ..Default::default()
1558            },
1559            ..empty_dump_command()
1560        };
1561        let criteria = LogFilterCriteria::try_from(cmd).unwrap();
1562
1563        let entry_match_msg = make_log_entry(
1564            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1565                timestamp: Timestamp::from_nanos(0),
1566                component_url: Some("".into()),
1567                moniker: "core/test".try_into().unwrap(),
1568                severity: diagnostics_data::Severity::Error,
1569            })
1570            .set_message("hello foo middle bar world")
1571            .build()
1572            .into(),
1573        );
1574
1575        let entry_no_match = make_log_entry(
1576            diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1577                timestamp: Timestamp::from_nanos(0),
1578                component_url: Some("".into()),
1579                moniker: "core/test".try_into().unwrap(),
1580                severity: diagnostics_data::Severity::Error,
1581            })
1582            .set_message("hello world")
1583            .build()
1584            .into(),
1585        );
1586
1587        assert!(!criteria.matches(&entry_match_msg), "entry_match_msg should be filtered out");
1588        assert!(criteria.matches(&entry_no_match), "entry_no_match should NOT be filtered out");
1589
1590        std::fs::remove_file(file_path).ok();
1591    }
1592
1593    #[test]
1594    fn test_disambiguate_queried_monikers_exact_full_match() {
1595        let instances = vec![
1596            Moniker::try_from("bootstrap/archivist").unwrap(),
1597            Moniker::try_from("bootstrap/archivist/archivist-pipelines").unwrap(),
1598        ];
1599        assert_eq!(
1600            disambiguate_queried_monikers("bootstrap/archivist", instances.clone()),
1601            vec![Moniker::try_from("bootstrap/archivist").unwrap()]
1602        );
1603        assert_eq!(
1604            disambiguate_queried_monikers("/bootstrap/archivist", instances),
1605            vec![Moniker::try_from("bootstrap/archivist").unwrap()]
1606        );
1607    }
1608
1609    #[test]
1610    fn test_disambiguate_queried_monikers_exact_leaf_match() {
1611        let instances = vec![
1612            Moniker::try_from("bootstrap/archivist").unwrap(),
1613            Moniker::try_from("bootstrap/archivist/archivist-pipelines").unwrap(),
1614        ];
1615        assert_eq!(
1616            disambiguate_queried_monikers("archivist", instances),
1617            vec![Moniker::try_from("bootstrap/archivist").unwrap()]
1618        );
1619
1620        let instances_with_extra = vec![
1621            Moniker::try_from("core/network/netstack").unwrap(),
1622            Moniker::try_from("core/network/netstack/child").unwrap(),
1623            Moniker::try_from("core/network/netstack-extra").unwrap(),
1624        ];
1625        assert_eq!(
1626            disambiguate_queried_monikers("netstack", instances_with_extra),
1627            vec![Moniker::try_from("core/network/netstack").unwrap()]
1628        );
1629    }
1630
1631    #[test]
1632    fn test_disambiguate_queried_monikers_exact_path_suffix_match() {
1633        let instances = vec![
1634            Moniker::try_from("core/network/netstack").unwrap(),
1635            Moniker::try_from("core/network/netstack/child").unwrap(),
1636        ];
1637        assert_eq!(
1638            disambiguate_queried_monikers("network/netstack", instances),
1639            vec![Moniker::try_from("core/network/netstack").unwrap()]
1640        );
1641    }
1642
1643    #[test]
1644    fn test_disambiguate_queried_monikers_trailing_slash() {
1645        let instances = vec![
1646            Moniker::try_from("bootstrap/archivist").unwrap(),
1647            Moniker::try_from("bootstrap/archivist/archivist-pipelines").unwrap(),
1648        ];
1649        assert_eq!(
1650            disambiguate_queried_monikers("archivist/", instances),
1651            vec![Moniker::try_from("bootstrap/archivist").unwrap()]
1652        );
1653    }
1654
1655    #[test]
1656    fn test_disambiguate_queried_monikers_ambiguous_distinct_roots() {
1657        let instances = vec![
1658            Moniker::try_from("bootstrap/archivist").unwrap(),
1659            Moniker::try_from("bootstrap/archivist/archivist-pipelines").unwrap(),
1660            Moniker::try_from("core/archivist").unwrap(),
1661        ];
1662        // archivist matches leaf of bootstrap/archivist and core/archivist, but not archivist-pipelines
1663        assert_eq!(
1664            disambiguate_queried_monikers("archivist", instances),
1665            vec![
1666                Moniker::try_from("bootstrap/archivist").unwrap(),
1667                Moniker::try_from("core/archivist").unwrap(),
1668            ]
1669        );
1670    }
1671
1672    #[test]
1673    fn test_disambiguate_queried_monikers_nested_root_preference() {
1674        let instances = vec![
1675            Moniker::try_from("bootstrap/archivist/pipelines").unwrap(),
1676            Moniker::try_from("bootstrap/archivist/pipelines/sub").unwrap(),
1677        ];
1678        assert_eq!(
1679            disambiguate_queried_monikers("archivist", instances),
1680            vec![Moniker::try_from("bootstrap/archivist/pipelines").unwrap()]
1681        );
1682    }
1683
1684    struct MultiFakeInstanceGetter {
1685        map: std::collections::HashMap<String, Vec<Moniker>>,
1686    }
1687
1688    #[async_trait::async_trait(?Send)]
1689    impl InstanceGetter for MultiFakeInstanceGetter {
1690        async fn get_monikers_from_query(&self, query: &str) -> Result<Vec<Moniker>, LogError> {
1691            Ok(self.map.get(query).cloned().unwrap_or_default())
1692        }
1693    }
1694
1695    #[fuchsia::test]
1696    async fn test_expand_monikers_resolves_archivist_subcomponents() {
1697        let mut map = std::collections::HashMap::new();
1698        map.insert(
1699            "archivist".to_string(),
1700            vec![
1701                Moniker::try_from("bootstrap/archivist").unwrap(),
1702                Moniker::try_from("bootstrap/archivist/archivist-pipelines").unwrap(),
1703            ],
1704        );
1705        let getter = MultiFakeInstanceGetter { map };
1706
1707        let cmd = LogCommand {
1708            filters: LogFilterArgs {
1709                component: vec!["archivist".to_string()],
1710                ..Default::default()
1711            },
1712            ..empty_dump_command()
1713        };
1714
1715        let mut criteria = LogFilterCriteria::from(cmd);
1716        let warnings = criteria.expand_monikers(&getter).await.unwrap();
1717
1718        assert_eq!(
1719            warnings,
1720            vec![FuzzyMatchWarning {
1721                query: "archivist".to_string(),
1722                resolved: "bootstrap/archivist".to_string(),
1723            }]
1724        );
1725
1726        assert!(
1727            criteria.matches(&make_log_entry(
1728                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1729                    timestamp: Timestamp::from_nanos(0),
1730                    component_url: Some("".into()),
1731                    moniker: "bootstrap/archivist".try_into().unwrap(),
1732                    severity: diagnostics_data::Severity::Info,
1733                })
1734                .set_message("log from archivist")
1735                .build()
1736                .into()
1737            ))
1738        );
1739
1740        assert!(
1741            !criteria.matches(&make_log_entry(
1742                diagnostics_data::LogsDataBuilder::new(diagnostics_data::BuilderArgs {
1743                    timestamp: Timestamp::from_nanos(0),
1744                    component_url: Some("".into()),
1745                    moniker: "bootstrap/archivist/archivist-pipelines".try_into().unwrap(),
1746                    severity: diagnostics_data::Severity::Info,
1747                })
1748                .set_message("log from pipeline")
1749                .build()
1750                .into()
1751            ))
1752        );
1753    }
1754}