1use 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
31pub 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
175pub struct LogFilterCriteria {
177 min_severity: Severity,
179 filters: Vec<String>,
181 moniker_filters: MonikerFilters,
183 excludes: Vec<String>,
185 exclude_regexes: Vec<regex_lite::Regex>,
187 tags: Vec<String>,
189 exclude_tags: Vec<String>,
191 pid: Option<zx_koid_t>,
193 tid: Option<zx_koid_t>,
195 interest_selectors: Vec<LogInterestSelector>,
199 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
221fn 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 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 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 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 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 fn matches_filter_string(
328 filter_string: &str,
329 message: &str,
330 log: &LogsData,
331 case_sensitive: bool,
332 ) -> bool {
333 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 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 = ¤t[opening_index + 1..];
363 }
364 None => return tags,
365 }
366 match current.find(']') {
367 Some(closing_index) => {
368 tags.push(¤t[..closing_index]);
369 current = ¤t[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 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 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 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 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 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 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 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 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 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 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 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}