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