1use anyhow::format_err;
6use argh::{ArgsInfo, FromArgs, TopLevelCommand};
7use chrono::{DateTime, Local, Utc};
8use chrono_english::{Dialect, parse_date_string};
9#[cfg(not(any(feature = "fdomain", feature = "ctf")))]
10use component_debug::query::get_instances_from_query;
11#[cfg(feature = "fdomain")]
12use component_debug_fdomain::query::get_instances_from_query;
13use diagnostics_data::Severity;
14use errors::{FfxError, ffx_bail};
15use flex_fuchsia_diagnostics::{LogInterestSelector, LogSettingsProxy};
16use flex_fuchsia_sys2::RealmQueryProxy;
17pub use log_socket_stream::OneOrMany;
18use moniker::Moniker;
19use selectors::{SelectorExt, sanitize_moniker_for_selectors};
20use std::borrow::Cow;
21use std::io::Write;
22use std::ops::Deref;
23use std::str::FromStr;
24use std::string::FromUtf8Error;
25use std::time::Duration;
26use thiserror::Error;
27mod filter;
28#[cfg(not(feature = "fdomain"))]
29pub mod fxt_streamer;
30mod log_formatter;
31mod log_socket_stream;
32pub use log_formatter::{
33 BootTimeAccessor, DefaultLogFormatter, FormatterError, LogData, LogEntry, Symbolize,
34 TIMESTAMP_FORMAT, Timestamp, WriterContainer, dump_logs_from_socket,
35};
36pub use log_socket_stream::{JsonDeserializeError, LogsDataStream};
37
38#[cfg(not(feature = "fdomain"))]
39pub use log_formatter::dump_fxt_logs_from_socket;
40
41#[derive(ArgsInfo, FromArgs, Clone, PartialEq, Debug)]
43#[argh(subcommand)]
44pub enum LogSubCommand {
45 Watch(RawWatchCommand),
46 Dump(RawDumpCommand),
47 SetSeverity(SetSeverityCommand),
48}
49
50#[derive(ArgsInfo, FromArgs, Clone, PartialEq, Debug, Default)]
51#[argh(subcommand, name = "set-severity")]
53pub struct SetSeverityCommand {
54 #[argh(switch)]
58 pub no_persist: bool,
59
60 #[argh(switch)]
64 pub force: bool,
65
66 #[argh(positional, from_str_fn(log_interest_selector))]
73 pub interest_selector: Vec<OneOrMany<LogInterestSelector>>,
74}
75
76pub fn parse_time(value: &str) -> Result<DetailedDateTime, String> {
77 parse_date_string(value, Local::now(), Dialect::Us)
78 .map(|time| DetailedDateTime { time, is_now: value == "now" })
79 .map_err(|e| format!("invalid date string: {e}"))
80}
81
82pub fn parse_utc_time(value: &str) -> Result<DetailedDateTime, String> {
84 parse_date_string(value, Utc::now(), Dialect::Us)
85 .map(|time| DetailedDateTime { time: time.into(), is_now: value == "now" })
86 .map_err(|e| format!("invalid date string: {e}"))
87}
88
89pub fn parse_seconds_string_as_duration(value: &str) -> Result<Duration, String> {
92 Ok(Duration::from_secs(
93 value.parse().map_err(|e| format!("value '{value}' is not a number: {e}"))?,
94 ))
95}
96
97#[derive(Clone, Debug, PartialEq)]
99pub enum TimeFormat {
100 Utc,
102 Local,
104 Boot,
106}
107
108impl std::str::FromStr for TimeFormat {
109 type Err = String;
110
111 fn from_str(s: &str) -> Result<Self, Self::Err> {
112 let lower = s.to_ascii_lowercase();
113 match lower.as_str() {
114 "local" => Ok(TimeFormat::Local),
115 "utc" => Ok(TimeFormat::Utc),
116 "boot" => Ok(TimeFormat::Boot),
117 _ => Err(format!("'{s}' is not a valid value: must be one of 'local', 'utc', 'boot'")),
118 }
119 }
120}
121
122#[derive(Clone, Debug, PartialEq)]
124pub enum LogEncoding {
125 Json,
126 Fxt,
127}
128
129impl std::str::FromStr for LogEncoding {
130 type Err = String;
131
132 fn from_str(s: &str) -> Result<Self, Self::Err> {
133 let lower = s.to_ascii_lowercase();
134 match lower.as_str() {
135 "json" => Ok(LogEncoding::Json),
136 "fxt" => Ok(LogEncoding::Fxt),
137 _ => Err(format!("'{s}' is not a valid value: must be one of 'json', 'fxt'")),
138 }
139 }
140}
141
142#[derive(PartialEq, Clone, Debug)]
146pub struct DetailedDateTime {
147 pub time: DateTime<Local>,
150 pub is_now: bool,
154}
155
156impl Deref for DetailedDateTime {
157 type Target = DateTime<Local>;
158
159 fn deref(&self) -> &Self::Target {
160 &self.time
161 }
162}
163
164#[derive(Clone, PartialEq, Debug)]
165pub enum SymbolizeMode {
166 Off,
168 Pretty,
170 Classic,
172}
173
174impl SymbolizeMode {
175 pub fn is_prettification_disabled(&self) -> bool {
176 matches!(self, SymbolizeMode::Classic)
177 }
178
179 pub fn is_symbolize_disabled(&self) -> bool {
180 matches!(self, SymbolizeMode::Off)
181 }
182}
183
184#[doc(hidden)]
186macro_rules! overlay_field {
187 (Vec, $self:ident, $child:ident, $field:ident) => {
188 $self.$field.extend($child.$field.into_iter());
189 };
190 (Option, $self:ident, $child:ident, $field:ident) => {
191 if $child.$field.is_some() {
192 $self.$field = $child.$field;
193 }
194 };
195 (bool, $self:ident, $child:ident, $field:ident) => {
196 $self.$field |= $child.$field;
197 };
198}
199
200#[doc(hidden)]
216macro_rules! __define_log_filter_args_helper {
217 (
218 [ $(#[$attr:meta])* ]
219 [ $(#[$cfg:meta])* ]
220 [ $($fields:tt)* ]
221 #[cfg $($cfg_args:tt)*]
222 $($rest:tt)*
223 ) => {
224 __define_log_filter_args_helper! {
225 [ $(#[$attr])* #[cfg $($cfg_args)*] ]
226 [ $(#[$cfg])* #[cfg $($cfg_args)*] ]
227 [ $($fields)* ]
228 $($rest)*
229 }
230 };
231
232 (
233 [ $(#[$attr:meta])* ]
234 [ $(#[$cfg:meta])* ]
235 [ $($fields:tt)* ]
236 #[$other_attr:meta]
237 $($rest:tt)*
238 ) => {
239 __define_log_filter_args_helper! {
240 [ $(#[$attr])* #[$other_attr] ]
241 [ $(#[$cfg])* ]
242 [ $($fields)* ]
243 $($rest)*
244 }
245 };
246
247 (
248 [ $(#[$attr:meta])* ]
249 [ $(#[$cfg:meta])* ]
250 [ $($fields:tt)* ]
251 $vis:vis $field:ident : $ty_outer:ident $( < $ty_inner:ty > )? $(, $($rest:tt)*)?
252 ) => {
253 __define_log_filter_args_helper! {
254 [ ]
255 [ ]
256 [
257 $($fields)*
258 (
259 [ $(#[$attr])* ]
260 [ $(#[$cfg])* ]
261 $vis $field : $ty_outer $( < $ty_inner > )?
262 )
263 ]
264 $($($rest)*)?
265 }
266 };
267
268 (
269 [ ]
270 [ ]
271 [
272 $(
273 (
274 [ $(#[$all_attr:meta])* ]
275 [ $(#[$cfg_attr:meta])* ]
276 $vis:vis $field:ident : $ty_outer:ident $( < $ty_inner:ty > )?
277 )
278 )*
279 ]
280 ) => {
281 #[derive(Clone, Debug, PartialEq)]
283 pub struct LogFilterArgs {
284 $(
285 $(#[$cfg_attr])*
286 $vis $field: $ty_outer $( < $ty_inner > )?,
287 )*
288 }
289
290 impl Default for LogFilterArgs {
291 fn default() -> Self {
292 LogFilterArgs {
293 $(
294 $(#[$cfg_attr])*
295 $field: Default::default(),
296 )*
297 }
298 }
299 }
300
301 impl LogFilterArgs {
302 pub fn merge(&mut self, other: LogFilterArgs) {
304 $(
305 $(#[$cfg_attr])*
306 overlay_field!($ty_outer, self, other, $field);
307 )*
308 }
309 }
310
311 #[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
312 #[argh(
314 subcommand,
315 name = "log",
316 description = "Display logs from a target device",
317 note = "Logs are retrieved from the target at the moment this command is called.\n\nYou may see some additional information attached to the log line:\n\n- `dropped=N`: this means that N logs attributed to the component were dropped when the component\n wrote to the log socket. This can happen when archivist cannot keep up with the rate of logs being\n emitted by the component and the component filled the log socket buffer in the kernel.\n\n- `rolled=N`: this means that N logs rolled out from the archivist buffer and ffx never saw them.\n This can happen when more logs are being ingested by the archivist across all components and the\n ffx couldn't retrieve them fast enough.\n\nSymbolization is performed in the background using the symbolizer host tool. You can pass\nadditional arguments to the symbolizer tool (for example, to add a remote symbol server) using:\n $ ffx config set proactive_log.symbolize.extra_args \"--symbol-server gs://some-url/path --symbol-server gs://some-other-url/path ...\"\n\nTo learn more about configuring the log viewer, visit https://fuchsia.dev/fuchsia-src/development/tools/ffx/commands/log",
318 example = "Dump the most recent logs and stream new ones as they happen:\n $ ffx log\n\nStream new logs starting from the current time, filtering for severity of at least \"WARN\":\n $ ffx log --severity warn --since now\n\nStream logs where the source moniker, component url and message do not include \"sys\":\n $ ffx log --exclude sys\n\nStream ERROR logs with source moniker, component url or message containing either\n\"netstack\" or \"remote-control.cm\", but not containing \"sys\":\n $ ffx log --severity error --filter netstack --filter remote-control.cm --exclude sys\n\nDump all available logs where the source moniker, component url, or message contains\n\"remote-control\":\n $ ffx log --filter remote-control dump\n\nDump all logs from the last 30 minutes logged before 5 minutes ago:\n $ ffx log --since \"30m ago\" --until \"5m ago\" dump\n\nEnable DEBUG logs from the \"core/audio\" component while logs are streaming:\n $ ffx log --set-severity core/audio#DEBUG"
319 )]
320 pub struct RawLogCommand {
321 #[argh(subcommand)]
322 pub sub_command: Option<LogSubCommand>,
323
324 #[argh(switch, hidden_help)]
328 pub dump: bool,
329
330 #[argh(option, from_str_fn(log_interest_selector))]
339 pub set_severity: Vec<OneOrMany<LogInterestSelector>>,
340
341 $(
342 $(#[$all_attr])*
343 $vis $field: $ty_outer $( < $ty_inner > )?,
344 )*
345 }
346
347 impl RawLogCommand {
348 pub fn into_log_command(self) -> LogCommand {
349 LogCommand {
350 sub_command: self.sub_command,
351 dump: self.dump,
352 set_severity: self.set_severity,
353 filters: LogFilterArgs {
354 $(
355 $(#[$cfg_attr])*
356 $field: self.$field,
357 )*
358 },
359 }
360 }
361 }
362
363 #[derive(ArgsInfo, FromArgs, Clone, PartialEq, Debug)]
364 #[argh(subcommand, name = "dump")]
366 pub struct RawDumpCommand {
367 #[argh(option)]
369 pub tail: Option<usize>,
370
371 $(
372 $(#[$all_attr])*
373 $vis $field: $ty_outer $( < $ty_inner > )?,
374 )*
375 }
376
377 impl Default for RawDumpCommand {
378 fn default() -> Self {
379 let filters = LogFilterArgs::default();
380 Self {
381 tail: None,
382 $(
383 $(#[$cfg_attr])*
384 $field: filters.$field,
385 )*
386 }
387 }
388 }
389
390 impl RawDumpCommand {
391 pub fn into_filter_args(self) -> LogFilterArgs {
392 LogFilterArgs {
393 $(
394 $(#[$cfg_attr])*
395 $field: self.$field,
396 )*
397 }
398 }
399 }
400
401 #[derive(ArgsInfo, FromArgs, Clone, PartialEq, Debug)]
402 #[argh(subcommand, name = "watch")]
404 pub struct RawWatchCommand {
405 $(
406 $(#[$all_attr])*
407 $vis $field: $ty_outer $( < $ty_inner > )?,
408 )*
409 }
410
411 impl Default for RawWatchCommand {
412 fn default() -> Self {
413 let filters = LogFilterArgs::default();
414 Self {
415 $(
416 $(#[$cfg_attr])*
417 $field: filters.$field,
418 )*
419 }
420 }
421 }
422
423 impl RawWatchCommand {
424 pub fn into_filter_args(self) -> LogFilterArgs {
425 LogFilterArgs {
426 $(
427 $(#[$cfg_attr])*
428 $field: self.$field,
429 )*
430 }
431 }
432 }
433 };
434}
435
436macro_rules! define_log_filter_args {
469 ($($tokens:tt)*) => {
470 __define_log_filter_args_helper! {
471 [ ]
472 [ ]
473 [ ]
474 $($tokens)*
475 }
476 };
477}
478
479define_log_filter_args! {
480 #[argh(option)]
483 pub filter: Vec<String>,
484
485 #[argh(option)]
487 pub moniker: Vec<String>,
488
489 #[argh(option)]
492 pub component: Vec<String>,
493
494 #[argh(option)]
497 pub exclude: Vec<String>,
498
499 #[argh(option)]
501 pub exclude_regex: Vec<String>,
502
503 #[argh(option)]
505 pub exclude_regex_file: Option<String>,
506
507 #[argh(option)]
509 pub tag: Vec<String>,
510
511 #[argh(option)]
513 pub exclude_tags: Vec<String>,
514
515 #[argh(option)]
518 pub severity: Option<Severity>,
519
520 #[argh(switch)]
522 pub kernel: bool,
523
524 #[argh(option, from_str_fn(parse_time))]
526 pub since: Option<DetailedDateTime>,
527
528 #[argh(option, from_str_fn(parse_seconds_string_as_duration))]
531 pub since_boot: Option<Duration>,
532
533 #[argh(option, from_str_fn(parse_time))]
535 pub until: Option<DetailedDateTime>,
536
537 #[argh(option, from_str_fn(parse_seconds_string_as_duration))]
540 pub until_boot: Option<Duration>,
541
542 #[argh(switch)]
544 pub hide_tags: bool,
545
546 #[argh(switch)]
548 pub hide_file: bool,
549
550 #[argh(switch)]
554 pub no_color: bool,
555
556 #[argh(switch)]
559 pub case_sensitive: bool,
560
561 #[argh(switch)]
563 pub show_metadata: bool,
564
565 #[argh(switch)]
568 pub show_full_moniker: bool,
569
570 #[argh(switch)]
572 pub prefer_url_component_name: bool,
573
574 #[argh(switch)]
576 pub hide_moniker: bool,
577
578 #[argh(option)]
582 pub clock: Option<TimeFormat>,
583
584 #[cfg(not(target_os = "fuchsia"))]
589 #[argh(option)]
590 pub symbolize: Option<SymbolizeMode>,
591
592 #[argh(option)]
594 pub pid: Option<u64>,
595
596 #[argh(option)]
598 pub tid: Option<u64>,
599
600 #[argh(switch)]
605 pub force_set_severity: bool,
606
607 #[cfg(target_os = "fuchsia")]
610 #[argh(option)]
611 pub encoding: Option<LogEncoding>,
612
613 #[cfg(target_os = "fuchsia")]
615 #[argh(switch)]
616 pub json: bool,
617
618 #[cfg(not(target_os = "fuchsia"))]
620 #[argh(switch)]
621 pub disable_reconnect: bool,
622}
623
624#[derive(Default, Clone, Debug, PartialEq)]
625pub struct LogCommand {
627 pub sub_command: Option<LogSubCommand>,
628 pub dump: bool,
629 pub set_severity: Vec<OneOrMany<LogInterestSelector>>,
630 pub filters: LogFilterArgs,
631}
632
633impl LogCommand {
634 pub fn merge_subcommand(&mut self) {
636 match &self.sub_command {
637 Some(LogSubCommand::Dump(raw_dump)) => {
638 self.filters.merge(raw_dump.clone().into_filter_args());
639 }
640 Some(LogSubCommand::Watch(raw_watch)) => {
641 self.filters.merge(raw_watch.clone().into_filter_args());
642 }
643 _ => {}
644 }
645 }
646}
647
648impl FromArgs for LogCommand {
649 fn from_args(command_name: &[&str], args: &[&str]) -> Result<Self, argh::EarlyExit> {
650 let cli = RawLogCommand::from_args(command_name, args)?;
651 let mut cmd = cli.into_log_command();
652 cmd.merge_subcommand();
653 Ok(cmd)
654 }
655}
656
657impl ArgsInfo for LogCommand {
658 fn get_args_info() -> argh::CommandInfoWithArgs {
659 RawLogCommand::get_args_info()
660 }
661}
662
663impl argh::SubCommand for LogCommand {
664 const COMMAND: &'static argh::CommandInfo = RawLogCommand::COMMAND;
665}
666
667#[derive(PartialEq, Debug)]
669pub enum LogProcessingResult {
670 Exit,
672 Continue,
674}
675
676impl FromStr for SymbolizeMode {
677 type Err = anyhow::Error;
678
679 fn from_str(s: &str) -> Result<Self, Self::Err> {
680 let s = s.to_lowercase();
681 match s.as_str() {
682 "off" => Ok(SymbolizeMode::Off),
683 "pretty" => Ok(SymbolizeMode::Pretty),
684 "classic" => Ok(SymbolizeMode::Classic),
685 other => Err(format_err!("invalid symbolize flag: {}", other)),
686 }
687 }
688}
689
690#[derive(Error, Debug)]
691pub enum LogError {
692 #[error(transparent)]
693 UnknownError(#[from] anyhow::Error),
694 #[error("No boot timestamp")]
695 NoBootTimestamp,
696 #[error(transparent)]
697 IOError(#[from] std::io::Error),
698 #[error(transparent)]
699 RegexError(#[from] regex_lite::Error),
700 #[error("Cannot use dump with --since now")]
701 DumpWithSinceNow,
702 #[error("No symbolizer configuration provided")]
703 NoSymbolizerConfig,
704 #[error(transparent)]
705 FfxError(#[from] FfxError),
706 #[error(transparent)]
707 Utf8Error(#[from] FromUtf8Error),
708 #[error(transparent)]
709 FidlError(#[from] fidl::Error),
710 #[error(transparent)]
711 FormatterError(#[from] FormatterError),
712 #[error("Deprecated flag: `{flag}`, use: `{new_flag}`")]
713 DeprecatedFlag { flag: &'static str, new_flag: &'static str },
714 #[error("Fuzzy matching failed due to too many matches, please re-try with one of these:\n{0}")]
715 FuzzyMatchTooManyMatches(String),
716 #[error(
717 "No running components were found matching {0}. Please ensure the component is running and the moniker is correct. Run 'ffx component list' to see running components."
718 )]
719 SearchParameterNotFound(String),
720}
721
722impl LogError {
723 fn too_many_fuzzy_matches(matches: impl Iterator<Item = String>) -> Self {
724 let mut result = String::new();
725 for component in matches {
726 result.push_str(&component);
727 result.push('\n');
728 }
729
730 Self::FuzzyMatchTooManyMatches(result)
731 }
732
733 pub fn is_broken_pipe(&self) -> bool {
734 match self {
735 LogError::IOError(error) => error.kind() == std::io::ErrorKind::BrokenPipe,
736 LogError::FormatterError(formatter_error) => formatter_error.is_broken_pipe(),
737 LogError::UnknownError(err) => {
738 if let Some(writer_err) = err.downcast_ref::<writer::Error>() {
739 writer_err.is_broken_pipe()
740 } else if let Some(io_err) = err.downcast_ref::<std::io::Error>() {
741 io_err.kind() == std::io::ErrorKind::BrokenPipe
742 } else {
743 false
744 }
745 }
746
747 LogError::NoBootTimestamp
748 | LogError::DumpWithSinceNow
749 | LogError::NoSymbolizerConfig
750 | LogError::RegexError(_)
751 | LogError::FfxError(_)
752 | LogError::Utf8Error(_)
753 | LogError::FidlError(_)
754 | LogError::DeprecatedFlag { .. }
755 | LogError::FuzzyMatchTooManyMatches(_)
756 | LogError::SearchParameterNotFound(_) => false,
757 }
758 }
759}
760
761#[async_trait::async_trait(?Send)]
763pub trait InstanceGetter {
764 async fn get_monikers_from_query(&self, query: &str) -> Result<Vec<Moniker>, LogError>;
765}
766
767#[cfg(not(feature = "ctf"))]
768#[async_trait::async_trait(?Send)]
769impl InstanceGetter for RealmQueryProxy {
770 async fn get_monikers_from_query(&self, query: &str) -> Result<Vec<Moniker>, LogError> {
771 Ok(get_instances_from_query(query, self)
772 .await?
773 .into_iter()
774 .map(|value| value.moniker)
775 .collect())
776 }
777}
778
779#[cfg(feature = "ctf")]
780#[async_trait::async_trait(?Send)]
781impl InstanceGetter for RealmQueryProxy {
782 async fn get_monikers_from_query(&self, _query: &str) -> Result<Vec<Moniker>, LogError> {
783 unreachable!("get_monikers_from_query is not supported in CTF tests.");
784 }
785}
786
787impl LogCommand {
788 #[must_use]
790 pub fn severity(&self) -> Severity {
791 self.filters.severity.unwrap_or(Severity::Info)
792 }
793
794 #[must_use]
796 pub fn clock(&self) -> TimeFormat {
797 self.filters.clock.clone().unwrap_or(TimeFormat::Boot)
798 }
799
800 #[cfg(not(target_os = "fuchsia"))]
802 #[must_use]
803 pub fn symbolize(&self) -> SymbolizeMode {
804 self.filters.symbolize.clone().unwrap_or(SymbolizeMode::Pretty)
805 }
806
807 #[cfg(target_os = "fuchsia")]
809 #[must_use]
810 pub fn encoding(&self) -> LogEncoding {
811 self.filters.encoding.clone().unwrap_or(LogEncoding::Json)
812 }
813
814 #[must_use]
816 pub fn filter(&self) -> &[String] {
817 &self.filters.filter
818 }
819
820 #[must_use]
822 pub fn moniker(&self) -> &[String] {
823 &self.filters.moniker
824 }
825
826 #[must_use]
828 pub fn component(&self) -> &[String] {
829 &self.filters.component
830 }
831
832 #[must_use]
834 pub fn exclude(&self) -> &[String] {
835 &self.filters.exclude
836 }
837
838 #[must_use]
840 pub fn exclude_regex(&self) -> &[String] {
841 &self.filters.exclude_regex
842 }
843
844 #[must_use]
846 pub fn tag(&self) -> &[String] {
847 &self.filters.tag
848 }
849
850 #[must_use]
852 pub fn exclude_tags(&self) -> &[String] {
853 &self.filters.exclude_tags
854 }
855
856 #[must_use]
858 pub fn hide_tags(&self) -> bool {
859 self.filters.hide_tags
860 }
861
862 #[must_use]
864 pub fn no_color(&self) -> bool {
865 self.filters.no_color
866 }
867
868 pub fn set_no_color(&mut self, no_color: bool) {
870 self.filters.no_color = no_color;
871 }
872
873 #[must_use]
875 pub fn show_metadata(&self) -> bool {
876 self.filters.show_metadata
877 }
878
879 #[must_use]
881 pub fn hide_file(&self) -> bool {
882 self.filters.hide_file
883 }
884
885 #[must_use]
887 pub fn hide_moniker(&self) -> bool {
888 self.filters.hide_moniker
889 }
890
891 #[must_use]
893 pub fn show_full_moniker(&self) -> bool {
894 self.filters.show_full_moniker
895 }
896
897 #[must_use]
899 pub fn prefer_url_component_name(&self) -> bool {
900 self.filters.prefer_url_component_name
901 }
902
903 #[must_use]
905 pub fn since(&self) -> Option<&DetailedDateTime> {
906 self.filters.since.as_ref()
907 }
908
909 #[must_use]
911 pub fn until(&self) -> Option<&DetailedDateTime> {
912 self.filters.until.as_ref()
913 }
914
915 #[must_use]
917 pub fn since_boot(&self) -> Option<Duration> {
918 self.filters.since_boot
919 }
920
921 #[must_use]
923 pub fn until_boot(&self) -> Option<Duration> {
924 self.filters.until_boot
925 }
926
927 #[cfg(target_os = "fuchsia")]
929 #[must_use]
930 pub fn json(&self) -> bool {
931 self.filters.json
932 }
933
934 #[must_use]
936 pub fn exclude_regex_file(&self) -> Option<&str> {
937 self.filters.exclude_regex_file.as_deref()
938 }
939
940 #[must_use]
942 pub fn kernel(&self) -> bool {
943 self.filters.kernel
944 }
945
946 #[must_use]
948 pub fn force_set_severity(&self) -> bool {
949 self.filters.force_set_severity
950 }
951
952 #[must_use]
954 pub fn case_sensitive(&self) -> bool {
955 self.filters.case_sensitive
956 }
957
958 #[must_use]
960 pub fn pid(&self) -> Option<u64> {
961 self.filters.pid
962 }
963
964 #[must_use]
966 pub fn tid(&self) -> Option<u64> {
967 self.filters.tid
968 }
969
970 #[cfg(not(target_os = "fuchsia"))]
972 #[must_use]
973 pub fn disable_reconnect(&self) -> bool {
974 self.filters.disable_reconnect
975 }
976
977 async fn map_interest_selectors<'a>(
978 realm_query: &impl InstanceGetter,
979 interest_selectors: impl Iterator<Item = &'a LogInterestSelector>,
980 ) -> Result<impl Iterator<Item = Cow<'a, LogInterestSelector>>, LogError> {
981 let selectors = Self::get_selectors_and_monikers(interest_selectors);
982 let mut translated_selectors = vec![];
983 for (moniker, selector) in selectors {
984 let instances = realm_query.get_monikers_from_query(moniker.as_str()).await?;
986 if instances.len() == 1 {
988 let mut translated_selector = selector.clone();
989 translated_selector.selector = instances[0].clone().into_component_selector();
990 translated_selectors.push((Cow::Owned(translated_selector), instances));
991 } else {
992 translated_selectors.push((Cow::Borrowed(selector), instances));
993 }
994 }
995 if translated_selectors.iter().any(|(_, matches)| matches.len() > 1) {
996 let mut err_output = vec![];
997 writeln!(
998 &mut err_output,
999 "WARN: One or more of your selectors appears to be ambiguous"
1000 )?;
1001 writeln!(&mut err_output, "and may not match any components on your system.\n")?;
1002 writeln!(
1003 &mut err_output,
1004 "If this is unintentional you can explicitly match using the"
1005 )?;
1006 writeln!(&mut err_output, "following command:\n")?;
1007 writeln!(&mut err_output, "ffx log \\")?;
1008 let mut output = vec![];
1009 for (oselector, instances) in translated_selectors {
1010 for selector in instances {
1011 writeln!(
1012 output,
1013 "\t--set-severity {}#{} \\",
1014 sanitize_moniker_for_selectors(selector.to_string().as_str())
1015 .replace("\\", "\\\\"),
1016 format!("{:?}", oselector.interest.min_severity.unwrap()).to_uppercase()
1017 )?;
1018 }
1019 }
1020 let _ = output.pop();
1022 let _ = output.pop();
1023 let _ = output.pop();
1024
1025 writeln!(&mut err_output, "{}", String::from_utf8(output).unwrap())?;
1026 writeln!(&mut err_output, "\nIf this is intentional, you can disable this with")?;
1027 writeln!(&mut err_output, "ffx log --force-set-severity.")?;
1028
1029 ffx_bail!("{}", String::from_utf8(err_output)?);
1030 }
1031 Ok(translated_selectors.into_iter().map(|(selector, _)| selector))
1032 }
1033
1034 pub fn validate_cmd_flags_with_warnings(&mut self) -> Result<Vec<&'static str>, LogError> {
1035 let mut warnings = vec![];
1036
1037 if !self.filters.moniker.is_empty() {
1038 warnings.push("WARNING: --moniker is deprecated, use --component instead");
1039 if self.filters.component.is_empty() {
1040 self.filters.component = std::mem::take(&mut self.filters.moniker);
1041 } else {
1042 warnings.push("WARNING: ignoring --moniker arguments in favor of --component");
1043 }
1044 }
1045
1046 Ok(warnings)
1047 }
1048
1049 pub async fn maybe_set_interest(
1053 &self,
1054 log_settings_client: &LogSettingsProxy,
1055 realm_query: &impl InstanceGetter,
1056 ) -> Result<(), LogError> {
1057 let (set_severity, force_set_severity, persist) =
1058 if let Some(LogSubCommand::SetSeverity(options)) = &self.sub_command {
1059 let default_cmd = LogCommand {
1061 sub_command: Some(LogSubCommand::SetSeverity(options.clone())),
1062 ..Default::default()
1063 };
1064 if &default_cmd != self {
1065 ffx_bail!("Cannot combine set-severity with other options.");
1066 }
1067 (&options.interest_selector, options.force, !options.no_persist)
1068 } else {
1069 (&self.set_severity, self.filters.force_set_severity, false)
1070 };
1071
1072 if persist || !set_severity.is_empty() {
1073 let selectors = if force_set_severity {
1074 set_severity.clone().into_iter().flatten().collect::<Vec<_>>()
1075 } else {
1076 let new_selectors =
1077 Self::map_interest_selectors(realm_query, set_severity.iter().flatten())
1078 .await?
1079 .map(|s| s.into_owned())
1080 .collect::<Vec<_>>();
1081 if new_selectors.is_empty() {
1082 set_severity.clone().into_iter().flatten().collect::<Vec<_>>()
1083 } else {
1084 new_selectors
1085 }
1086 };
1087 log_settings_client
1088 .set_component_interest(
1089 &flex_fuchsia_diagnostics::LogSettingsSetComponentInterestRequest {
1090 selectors: Some(selectors),
1091 persist: Some(persist),
1092 ..Default::default()
1093 },
1094 )
1095 .await?;
1096 }
1097
1098 Ok(())
1099 }
1100
1101 fn get_selectors_and_monikers<'a>(
1102 interest_selectors: impl Iterator<Item = &'a LogInterestSelector>,
1103 ) -> Vec<(String, &'a LogInterestSelector)> {
1104 let mut selectors = vec![];
1105 for selector in interest_selectors {
1106 let segments = selector.selector.moniker_segments.as_ref().unwrap();
1107 let mut full_moniker = String::new();
1108 for segment in segments {
1109 match segment {
1110 flex_fuchsia_diagnostics::StringSelector::ExactMatch(segment) => {
1111 if full_moniker.is_empty() {
1112 full_moniker.push_str(segment);
1113 } else {
1114 full_moniker.push('/');
1115 full_moniker.push_str(segment);
1116 }
1117 }
1118 _ => {
1119 return vec![];
1122 }
1123 }
1124 }
1125 selectors.push((full_moniker, selector));
1126 }
1127 selectors
1128 }
1129}
1130
1131impl TopLevelCommand for LogCommand {}
1132
1133fn log_interest_selector(s: &str) -> Result<OneOrMany<LogInterestSelector>, String> {
1134 if s.contains(",") {
1135 let many: Result<Vec<LogInterestSelector>, String> = s
1136 .split(",")
1137 .map(|value| selectors::parse_log_interest_selector(value).map_err(|e| e.to_string()))
1138 .collect();
1139 Ok(OneOrMany::Many(many?))
1140 } else {
1141 Ok(OneOrMany::One(selectors::parse_log_interest_selector(s).map_err(|s| s.to_string())?))
1142 }
1143}
1144
1145#[cfg(test)]
1146mod test {
1147 use super::*;
1148 use assert_matches::assert_matches;
1149 use async_trait::async_trait;
1150 use fidl::endpoints::create_proxy;
1151 use flex_fuchsia_diagnostics::{LogSettingsMarker, LogSettingsRequest};
1152 use futures_util::StreamExt;
1153 use futures_util::future::Either;
1154 use futures_util::stream::FuturesUnordered;
1155 use selectors::parse_log_interest_selector;
1156
1157 #[derive(Default)]
1158 struct FakeInstanceGetter {
1159 output: Vec<Moniker>,
1160 expected_selector: Option<String>,
1161 }
1162
1163 #[async_trait(?Send)]
1164 impl InstanceGetter for FakeInstanceGetter {
1165 async fn get_monikers_from_query(&self, query: &str) -> Result<Vec<Moniker>, LogError> {
1166 if let Some(expected) = &self.expected_selector {
1167 assert_eq!(expected, query);
1168 }
1169 Ok(self.output.clone())
1170 }
1171 }
1172
1173 #[fuchsia::test]
1174 async fn test_symbolize_mode_from_str() {
1175 assert_matches!(SymbolizeMode::from_str("off"), Ok(value) if value == SymbolizeMode::Off);
1176 assert_matches!(
1177 SymbolizeMode::from_str("pretty"),
1178 Ok(value) if value == SymbolizeMode::Pretty
1179 );
1180 assert_matches!(
1181 SymbolizeMode::from_str("classic"),
1182 Ok(value) if value == SymbolizeMode::Classic
1183 );
1184 }
1185
1186 #[fuchsia::test]
1187 async fn maybe_set_interest_errors_additional_arguments_passed_to_set_interest() {
1188 let (settings_proxy, settings_server) = create_proxy::<LogSettingsMarker>();
1189 let getter = FakeInstanceGetter {
1190 expected_selector: Some("ambiguous_selector".into()),
1191 output: vec![
1192 Moniker::try_from("core/some/ambiguous_selector:thing/test").unwrap(),
1193 Moniker::try_from("core/other/ambiguous_selector:thing/test").unwrap(),
1194 ],
1195 };
1196 let cmd = LogCommand {
1199 sub_command: Some(LogSubCommand::SetSeverity(SetSeverityCommand {
1200 interest_selector: vec![OneOrMany::One(
1201 parse_log_interest_selector("ambiguous_selector#INFO").unwrap(),
1202 )],
1203 force: false,
1204 no_persist: false,
1205 })),
1206 filters: LogFilterArgs { hide_file: true, ..LogFilterArgs::default() },
1207 ..LogCommand::default()
1208 };
1209 let mut set_interest_result = None;
1210
1211 let mut scheduler = FuturesUnordered::new();
1212 scheduler.push(Either::Left(async {
1213 set_interest_result = Some(cmd.maybe_set_interest(&settings_proxy, &getter).await);
1214 drop(settings_proxy);
1215 }));
1216 scheduler.push(Either::Right(async {
1217 let request = settings_server.into_stream().next().await;
1218 assert_matches!(request, None);
1220 }));
1221 while scheduler.next().await.is_some() {}
1222 drop(scheduler);
1223
1224 let error = format!("{}", set_interest_result.unwrap().unwrap_err());
1225
1226 const EXPECTED_INTEREST_ERROR: &str = "Cannot combine set-severity with other options.";
1227 assert_eq!(error, EXPECTED_INTEREST_ERROR);
1228 }
1229
1230 #[fuchsia::test]
1231 async fn maybe_set_interest_errors_if_ambiguous_selector() {
1232 let (settings_proxy, settings_server) = create_proxy::<LogSettingsMarker>();
1233 let getter = FakeInstanceGetter {
1234 expected_selector: Some("ambiguous_selector".into()),
1235 output: vec![
1236 Moniker::try_from("core/some/ambiguous_selector:thing/test").unwrap(),
1237 Moniker::try_from("core/other/ambiguous_selector:thing/test").unwrap(),
1238 ],
1239 };
1240 let cmd = LogCommand {
1243 sub_command: Some(LogSubCommand::Dump(RawDumpCommand::default())),
1244 set_severity: vec![OneOrMany::One(
1245 parse_log_interest_selector("ambiguous_selector#INFO").unwrap(),
1246 )],
1247 ..LogCommand::default()
1248 };
1249 let mut set_interest_result = None;
1250
1251 let mut scheduler = FuturesUnordered::new();
1252 scheduler.push(Either::Left(async {
1253 set_interest_result = Some(cmd.maybe_set_interest(&settings_proxy, &getter).await);
1254 drop(settings_proxy);
1255 }));
1256 scheduler.push(Either::Right(async {
1257 let request = settings_server.into_stream().next().await;
1258 assert_matches!(request, None);
1260 }));
1261 while scheduler.next().await.is_some() {}
1262 drop(scheduler);
1263
1264 let error = format!("{}", set_interest_result.unwrap().unwrap_err());
1265
1266 const EXPECTED_INTEREST_ERROR: &str = r#"WARN: One or more of your selectors appears to be ambiguous
1267and may not match any components on your system.
1268
1269If this is unintentional you can explicitly match using the
1270following command:
1271
1272ffx log \
1273 --set-severity core/some/ambiguous_selector\\:thing/test#INFO \
1274 --set-severity core/other/ambiguous_selector\\:thing/test#INFO
1275
1276If this is intentional, you can disable this with
1277ffx log --force-set-severity.
1278"#;
1279 assert_eq!(error, EXPECTED_INTEREST_ERROR);
1280 }
1281
1282 #[fuchsia::test]
1283 async fn logger_translates_selector_if_one_match() {
1284 let cmd = LogCommand {
1285 sub_command: Some(LogSubCommand::Dump(RawDumpCommand::default())),
1286 set_severity: vec![OneOrMany::One(
1287 parse_log_interest_selector("ambiguous_selector#INFO").unwrap(),
1288 )],
1289 ..LogCommand::default()
1290 };
1291 let mut set_interest_result = None;
1292 let getter = FakeInstanceGetter {
1293 expected_selector: Some("ambiguous_selector".into()),
1294 output: vec![Moniker::try_from("core/some/ambiguous_selector").unwrap()],
1295 };
1296 let mut scheduler = FuturesUnordered::new();
1297 let (settings_proxy, settings_server) = create_proxy::<LogSettingsMarker>();
1298 scheduler.push(Either::Left(async {
1299 set_interest_result = Some(cmd.maybe_set_interest(&settings_proxy, &getter).await);
1300 drop(settings_proxy);
1301 }));
1302 scheduler.push(Either::Right(async {
1303 let request = settings_server.into_stream().next().await;
1304 let (payload, responder) = assert_matches!(
1305 request,
1306 Some(Ok(LogSettingsRequest::SetComponentInterest { payload, responder })) =>
1307 (payload, responder)
1308 );
1309 responder.send().unwrap();
1310 assert_eq!(
1311 payload.selectors,
1312 Some(vec![
1313 parse_log_interest_selector("core/some/ambiguous_selector#INFO").unwrap()
1314 ])
1315 );
1316 }));
1317 while scheduler.next().await.is_some() {}
1318 drop(scheduler);
1319 assert_matches!(set_interest_result, Some(Ok(())));
1320 }
1321
1322 #[fuchsia::test]
1323 async fn logger_uses_specified_selectors_if_no_results_returned() {
1324 let cmd = LogCommand {
1325 sub_command: Some(LogSubCommand::Dump(RawDumpCommand::default())),
1326 set_severity: vec![OneOrMany::One(
1327 parse_log_interest_selector("core/something/a:b/elements:main/otherstuff:*#DEBUG")
1328 .unwrap(),
1329 )],
1330 ..LogCommand::default()
1331 };
1332 let mut set_interest_result = None;
1333 let getter = FakeInstanceGetter {
1334 expected_selector: Some("core/something/a:b/elements:main/otherstuff:*#DEBUG".into()),
1335 output: vec![],
1336 };
1337 let scheduler = FuturesUnordered::new();
1338 let (settings_proxy, settings_server) = create_proxy::<LogSettingsMarker>();
1339 scheduler.push(Either::Left(async {
1340 set_interest_result = Some(cmd.maybe_set_interest(&settings_proxy, &getter).await);
1341 drop(settings_proxy);
1342 }));
1343 scheduler.push(Either::Right(async {
1344 let request = settings_server.into_stream().next().await;
1345 let (payload, responder) = assert_matches!(
1346 request,
1347 Some(Ok(LogSettingsRequest::SetComponentInterest { payload, responder })) =>
1348 (payload, responder)
1349 );
1350 responder.send().unwrap();
1351 assert_eq!(
1352 payload.selectors,
1353 Some(vec![
1354 parse_log_interest_selector(
1355 "core/something/a:b/elements:main/otherstuff:*#DEBUG"
1356 )
1357 .unwrap()
1358 ])
1359 );
1360 }));
1361 scheduler.map(|_| Ok(())).forward(futures::sink::drain()).await.unwrap();
1362 assert_matches!(set_interest_result, Some(Ok(())));
1363 }
1364
1365 #[fuchsia::test]
1366 async fn logger_prints_ignores_ambiguity_if_force_set_severity_is_used() {
1367 let cmd = LogCommand {
1368 sub_command: Some(LogSubCommand::SetSeverity(SetSeverityCommand {
1369 no_persist: true,
1370 interest_selector: vec![OneOrMany::One(
1371 parse_log_interest_selector("ambiguous_selector#INFO").unwrap(),
1372 )],
1373 force: true,
1374 })),
1375 ..LogCommand::default()
1376 };
1377 let getter = FakeInstanceGetter {
1378 expected_selector: Some("ambiguous_selector".into()),
1379 output: vec![
1380 Moniker::try_from("core/some/ambiguous_selector:thing/test").unwrap(),
1381 Moniker::try_from("core/other/ambiguous_selector:thing/test").unwrap(),
1382 ],
1383 };
1384 let mut set_interest_result = None;
1385 let mut scheduler = FuturesUnordered::new();
1386 let (settings_proxy, settings_server) = create_proxy::<LogSettingsMarker>();
1387 scheduler.push(Either::Left(async {
1388 set_interest_result = Some(cmd.maybe_set_interest(&settings_proxy, &getter).await);
1389 drop(settings_proxy);
1390 }));
1391 scheduler.push(Either::Right(async {
1392 let request = settings_server.into_stream().next().await;
1393 let (payload, responder) = assert_matches!(
1394 request,
1395 Some(Ok(LogSettingsRequest::SetComponentInterest { payload, responder })) =>
1396 (payload, responder)
1397 );
1398 responder.send().unwrap();
1399 assert_eq!(
1400 payload.selectors,
1401 Some(vec![parse_log_interest_selector("ambiguous_selector#INFO").unwrap()])
1402 );
1403 }));
1404 while scheduler.next().await.is_some() {}
1405 drop(scheduler);
1406 assert_matches!(set_interest_result, Some(Ok(())));
1407 }
1408
1409 #[fuchsia::test]
1410 async fn logger_prints_ignores_ambiguity_if_force_set_severity_is_used_persistent() {
1411 let cmd = LogCommand {
1412 sub_command: Some(LogSubCommand::SetSeverity(SetSeverityCommand {
1413 no_persist: false,
1414 interest_selector: vec![log_socket_stream::OneOrMany::One(
1415 parse_log_interest_selector("ambiguous_selector#INFO").unwrap(),
1416 )],
1417 force: true,
1418 })),
1419 ..LogCommand::default()
1420 };
1421 let getter = FakeInstanceGetter {
1422 expected_selector: Some("ambiguous_selector".into()),
1423 output: vec![
1424 Moniker::try_from("core/some/ambiguous_selector:thing/test").unwrap(),
1425 Moniker::try_from("core/other/ambiguous_selector:thing/test").unwrap(),
1426 ],
1427 };
1428 let mut set_interest_result = None;
1429 let mut scheduler = FuturesUnordered::new();
1430 let (settings_proxy, settings_server) = create_proxy::<LogSettingsMarker>();
1431 scheduler.push(Either::Left(async {
1432 set_interest_result = Some(cmd.maybe_set_interest(&settings_proxy, &getter).await);
1433 drop(settings_proxy);
1434 }));
1435 scheduler.push(Either::Right(async {
1436 let request = settings_server.into_stream().next().await;
1437 let (payload, responder) = assert_matches!(
1438 request,
1439 Some(Ok(LogSettingsRequest::SetComponentInterest { payload, responder })) =>
1440 (payload, responder)
1441 );
1442 responder.send().unwrap();
1443 assert_eq!(
1444 payload.selectors,
1445 Some(vec![parse_log_interest_selector("ambiguous_selector#INFO").unwrap()])
1446 );
1447 assert_eq!(payload.persist, Some(true));
1448 }));
1449 while scheduler.next().await.is_some() {}
1450 drop(scheduler);
1451 assert_matches!(set_interest_result, Some(Ok(())));
1452 }
1453
1454 #[fuchsia::test]
1455 async fn logger_prints_ignores_ambiguity_if_machine_output_is_used() {
1456 let cmd = LogCommand {
1457 sub_command: Some(LogSubCommand::Dump(RawDumpCommand::default())),
1458 set_severity: vec![OneOrMany::One(
1459 parse_log_interest_selector("ambiguous_selector#INFO").unwrap(),
1460 )],
1461 filters: LogFilterArgs { force_set_severity: true, ..LogFilterArgs::default() },
1462 ..LogCommand::default()
1463 };
1464 let getter = FakeInstanceGetter {
1465 expected_selector: Some("ambiguous_selector".into()),
1466 output: vec![
1467 Moniker::try_from("core/some/collection:thing/test").unwrap(),
1468 Moniker::try_from("core/other/collection:thing/test").unwrap(),
1469 ],
1470 };
1471 let mut set_interest_result = None;
1472 let mut scheduler = FuturesUnordered::new();
1473 let (settings_proxy, settings_server) = create_proxy::<LogSettingsMarker>();
1474 scheduler.push(Either::Left(async {
1475 set_interest_result = Some(cmd.maybe_set_interest(&settings_proxy, &getter).await);
1476 drop(settings_proxy);
1477 }));
1478 scheduler.push(Either::Right(async {
1479 let request = settings_server.into_stream().next().await;
1480 let (payload, responder) = assert_matches!(
1481 request,
1482 Some(Ok(LogSettingsRequest::SetComponentInterest { payload, responder })) =>
1483 (payload, responder)
1484 );
1485 responder.send().unwrap();
1486 assert_eq!(
1487 payload.selectors,
1488 Some(vec![parse_log_interest_selector("ambiguous_selector#INFO").unwrap()])
1489 );
1490 }));
1491 while scheduler.next().await.is_some() {}
1492 drop(scheduler);
1493 assert_matches!(set_interest_result, Some(Ok(())));
1494 }
1495 #[test]
1496 fn test_parse_selector() {
1497 assert_eq!(
1498 log_interest_selector("core/audio#DEBUG").unwrap(),
1499 OneOrMany::One(parse_log_interest_selector("core/audio#DEBUG").unwrap())
1500 );
1501 }
1502
1503 #[test]
1504 fn test_parse_selector_with_commas() {
1505 assert_eq!(
1506 log_interest_selector("core/audio#DEBUG,bootstrap/archivist#TRACE").unwrap(),
1507 OneOrMany::Many(vec![
1508 parse_log_interest_selector("core/audio#DEBUG").unwrap(),
1509 parse_log_interest_selector("bootstrap/archivist#TRACE").unwrap()
1510 ])
1511 );
1512 }
1513
1514 #[test]
1515 fn test_parse_time() {
1516 assert!(parse_time("now").unwrap().is_now);
1517 let date_string = "04/20/2020";
1518 let res = parse_time(date_string).unwrap();
1519 assert!(!res.is_now);
1520 assert_eq!(
1521 res.date_naive(),
1522 parse_date_string(date_string, Local::now(), Dialect::Us).unwrap().date_naive()
1523 );
1524 }
1525
1526 #[test]
1527 fn test_log_error_is_broken_pipe() {
1528 assert!(
1529 LogError::IOError(std::io::Error::new(std::io::ErrorKind::BrokenPipe, "broken pipe"))
1530 .is_broken_pipe()
1531 );
1532 assert!(
1533 LogError::UnknownError(anyhow::Error::new(std::io::Error::new(
1534 std::io::ErrorKind::BrokenPipe,
1535 "broken pipe"
1536 )))
1537 .is_broken_pipe()
1538 );
1539 assert!(!LogError::IOError(std::io::Error::other("other")).is_broken_pipe());
1540 assert!(!LogError::NoBootTimestamp.is_broken_pipe());
1541 }
1542
1543 #[test]
1544 fn test_raw_dump_command_into_filter_args() {
1545 let raw_dump = RawDumpCommand {
1546 tail: Some(42),
1547 filter: vec!["foo".to_string()],
1548 severity: Some(Severity::Warn),
1549 hide_tags: true,
1550 ..RawDumpCommand::default()
1551 };
1552 let filters = raw_dump.into_filter_args();
1553 assert_eq!(filters.filter, vec!["foo".to_string()]);
1554 assert_eq!(filters.severity, Some(Severity::Warn));
1555 assert!(filters.hide_tags);
1556 }
1557
1558 #[test]
1559 fn test_raw_watch_command_into_filter_args() {
1560 let raw_watch = RawWatchCommand {
1561 tag: vec!["my_tag".to_string()],
1562 no_color: true,
1563 ..RawWatchCommand::default()
1564 };
1565 let filters = raw_watch.into_filter_args();
1566 assert_eq!(filters.tag, vec!["my_tag".to_string()]);
1567 assert!(filters.no_color);
1568 }
1569
1570 #[test]
1571 fn test_merge_subcommand_dump_overlay() {
1572 let cmd = LogCommand::from_args(
1573 &["log"],
1574 &[
1575 "--severity",
1576 "info",
1577 "--filter",
1578 "top_filter",
1579 "dump",
1580 "--tail",
1581 "10",
1582 "--severity",
1583 "debug",
1584 "--filter",
1585 "sub_filter",
1586 ],
1587 )
1588 .unwrap();
1589
1590 assert_eq!(cmd.severity(), Severity::Debug);
1591 assert_eq!(cmd.filter(), &["top_filter".to_string(), "sub_filter".to_string()]);
1592 if let Some(LogSubCommand::Dump(dump)) = &cmd.sub_command {
1593 assert_eq!(dump.tail, Some(10));
1594 } else {
1595 panic!("expected Dump subcommand");
1596 }
1597 }
1598
1599 #[test]
1600 fn test_merge_subcommand_watch_overlay() {
1601 let cmd = LogCommand::from_args(
1602 &["log"],
1603 &["--tag", "t1", "watch", "--tag", "t2", "--hide-tags"],
1604 )
1605 .unwrap();
1606
1607 assert_eq!(cmd.tag(), &["t1".to_string(), "t2".to_string()]);
1608 assert!(cmd.hide_tags());
1609 }
1610
1611 #[test]
1612 fn test_merge_subcommand_validate_warnings_on_merged_moniker() {
1613 let mut cmd =
1614 LogCommand::from_args(&["log"], &["dump", "--moniker", "my_moniker"]).unwrap();
1615
1616 assert_eq!(cmd.moniker(), &["my_moniker".to_string()]);
1617 let warnings = cmd.validate_cmd_flags_with_warnings().unwrap();
1618 assert!(!warnings.is_empty());
1619 assert_eq!(cmd.component(), &["my_moniker".to_string()]);
1620 assert!(cmd.moniker().is_empty());
1621 }
1622
1623 #[cfg(not(target_os = "fuchsia"))]
1624 #[fuchsia::test]
1625 async fn test_symbolize_accessor() {
1626 let cmd_default = LogCommand::from_args(&["ffx", "log"], &["dump"]).unwrap();
1627 assert_eq!(cmd_default.symbolize(), SymbolizeMode::Pretty);
1628
1629 let cmd_custom =
1630 LogCommand::from_args(&["ffx", "log"], &["dump", "--symbolize", "off"]).unwrap();
1631 assert_eq!(cmd_custom.symbolize(), SymbolizeMode::Off);
1632 }
1633
1634 #[cfg(target_os = "fuchsia")]
1635 #[fuchsia::test]
1636 async fn test_encoding_accessor() {
1637 let cmd_default = LogCommand::from_args(&["ffx", "log"], &["dump"]).unwrap();
1638 assert_eq!(cmd_default.encoding(), LogEncoding::Json);
1639
1640 let cmd_custom =
1641 LogCommand::from_args(&["ffx", "log"], &["dump", "--encoding", "fxt"]).unwrap();
1642 assert_eq!(cmd_custom.encoding(), LogEncoding::Fxt);
1643 }
1644
1645 #[fuchsia::test]
1646 async fn test_subcommand_moniker_deprecation() {
1647 let mut cmd =
1648 LogCommand::from_args(&["ffx", "log"], &["dump", "--moniker", "foo"]).unwrap();
1649 assert_eq!(cmd.filters.moniker, vec!["foo"]);
1650
1651 let warnings = cmd.validate_cmd_flags_with_warnings().unwrap();
1652 assert_eq!(warnings, vec!["WARNING: --moniker is deprecated, use --component instead"]);
1653 assert_eq!(cmd.filters.component, vec!["foo"]);
1654 assert!(cmd.filters.moniker.is_empty());
1655
1656 let mut cmd_both = LogCommand::from_args(
1657 &["ffx", "log"],
1658 &["dump", "--moniker", "foo", "--component", "bar"],
1659 )
1660 .unwrap();
1661 let warnings_both = cmd_both.validate_cmd_flags_with_warnings().unwrap();
1662 assert_eq!(
1663 warnings_both,
1664 vec![
1665 "WARNING: --moniker is deprecated, use --component instead",
1666 "WARNING: ignoring --moniker arguments in favor of --component"
1667 ]
1668 );
1669 assert_eq!(cmd_both.filters.component, vec!["bar"]);
1670 }
1671
1672 #[test]
1673 fn test_subcommand_enum_overrides() {
1674 let cmd =
1675 LogCommand::from_args(&["log"], &["--severity", "warn", "dump", "--severity", "info"])
1676 .unwrap();
1677 assert_eq!(cmd.severity(), Severity::Info);
1678 assert_eq!(cmd.filters.severity, Some(Severity::Info));
1679 }
1680
1681 #[test]
1682 fn test_merge_subcommand_no_subcommand_or_set_severity() {
1683 let initial_filters = LogFilterArgs {
1684 severity: Some(Severity::Warn),
1685 filter: vec!["test_filter".into()],
1686 no_color: true,
1687 ..Default::default()
1688 };
1689 let mut cmd_none = LogCommand {
1690 sub_command: None,
1691 dump: false,
1692 set_severity: vec![],
1693 filters: initial_filters.clone(),
1694 };
1695 cmd_none.merge_subcommand();
1696 assert_eq!(cmd_none.filters, initial_filters);
1697 assert_eq!(cmd_none.sub_command, None);
1698
1699 let set_severity_cmd =
1700 SetSeverityCommand { no_persist: true, force: true, interest_selector: vec![] };
1701 let mut cmd_set_sev = LogCommand {
1702 sub_command: Some(LogSubCommand::SetSeverity(set_severity_cmd.clone())),
1703 dump: false,
1704 set_severity: vec![],
1705 filters: initial_filters.clone(),
1706 };
1707 cmd_set_sev.merge_subcommand();
1708 assert_eq!(cmd_set_sev.filters, initial_filters);
1709 assert_eq!(cmd_set_sev.sub_command, Some(LogSubCommand::SetSeverity(set_severity_cmd)));
1710 }
1711
1712 #[test]
1713 fn test_macro_raw_command_conversions() {
1714 #[cfg(not(target_os = "fuchsia"))]
1715 let raw_dump = RawDumpCommand::from_args(
1716 &["dump"],
1717 &[
1718 "--tail",
1719 "50",
1720 "--severity",
1721 "error",
1722 "--filter",
1723 "dump_filter",
1724 "--symbolize",
1725 "off",
1726 "--disable-reconnect",
1727 ],
1728 )
1729 .unwrap();
1730
1731 #[cfg(target_os = "fuchsia")]
1732 let raw_dump = RawDumpCommand::from_args(
1733 &["dump"],
1734 &[
1735 "--tail",
1736 "50",
1737 "--severity",
1738 "error",
1739 "--filter",
1740 "dump_filter",
1741 "--encoding",
1742 "fxt",
1743 "--json",
1744 ],
1745 )
1746 .unwrap();
1747
1748 assert_eq!(raw_dump.tail, Some(50));
1749 let dump_filters = raw_dump.into_filter_args();
1750 assert_eq!(dump_filters.severity, Some(Severity::Error));
1751 assert_eq!(dump_filters.filter, vec!["dump_filter"]);
1752 #[cfg(not(target_os = "fuchsia"))]
1753 {
1754 assert_eq!(dump_filters.symbolize, Some(SymbolizeMode::Off));
1755 assert!(dump_filters.disable_reconnect);
1756 }
1757 #[cfg(target_os = "fuchsia")]
1758 {
1759 assert_eq!(dump_filters.encoding, Some(LogEncoding::Fxt));
1760 assert!(dump_filters.json);
1761 }
1762
1763 #[cfg(not(target_os = "fuchsia"))]
1764 let raw_watch = RawWatchCommand::from_args(
1765 &["watch"],
1766 &["--severity", "warn", "--tag", "watch_tag", "--symbolize", "classic"],
1767 )
1768 .unwrap();
1769
1770 #[cfg(target_os = "fuchsia")]
1771 let raw_watch = RawWatchCommand::from_args(
1772 &["watch"],
1773 &["--severity", "warn", "--tag", "watch_tag", "--encoding", "json"],
1774 )
1775 .unwrap();
1776
1777 let watch_filters = raw_watch.into_filter_args();
1778 assert_eq!(watch_filters.severity, Some(Severity::Warn));
1779 assert_eq!(watch_filters.tag, vec!["watch_tag"]);
1780 #[cfg(not(target_os = "fuchsia"))]
1781 assert_eq!(watch_filters.symbolize, Some(SymbolizeMode::Classic));
1782 #[cfg(target_os = "fuchsia")]
1783 assert_eq!(watch_filters.encoding, Some(LogEncoding::Json));
1784 }
1785
1786 #[test]
1787 fn test_dump_help_text() {
1788 let help_err = RawDumpCommand::from_args(&["dump"], &["--help"]).unwrap_err();
1789 let help_output = help_err.output;
1790 assert!(help_output.contains("--tail"), "dump help should include --tail");
1791 assert!(help_output.contains("--severity"), "dump help should include --severity");
1792 assert!(help_output.contains("--filter"), "dump help should include --filter");
1793 assert!(help_output.contains("--since"), "dump help should include --since");
1794 assert!(help_output.contains("--until"), "dump help should include --until");
1795 }
1796
1797 #[test]
1798 fn test_watch_help_text() {
1799 let help_err = RawWatchCommand::from_args(&["watch"], &["--help"]).unwrap_err();
1800 let help_output = help_err.output;
1801 assert!(help_output.contains("--severity"), "watch help should include --severity");
1802 assert!(help_output.contains("--filter"), "watch help should include --filter");
1803 assert!(help_output.contains("--since"), "watch help should include --since");
1804 assert!(help_output.contains("--until"), "watch help should include --until");
1805 }
1806
1807 #[test]
1808 fn test_option_field_subcommand_overlay() {
1809 let cmd_sub_some = LogCommand::from_args(
1811 &["log"],
1812 &[
1813 "dump",
1814 "--pid",
1815 "123",
1816 "--tid",
1817 "456",
1818 "--since",
1819 "10m ago",
1820 "--until",
1821 "5m ago",
1822 "--exclude-regex-file",
1823 "/path/sub.txt",
1824 ],
1825 )
1826 .unwrap();
1827
1828 assert_eq!(cmd_sub_some.pid(), Some(123));
1829 assert_eq!(cmd_sub_some.tid(), Some(456));
1830 assert!(cmd_sub_some.since().is_some());
1831 assert!(cmd_sub_some.until().is_some());
1832 assert_eq!(cmd_sub_some.exclude_regex_file(), Some("/path/sub.txt"));
1833
1834 let cmd_sub_override = LogCommand::from_args(
1836 &["log"],
1837 &[
1838 "--pid",
1839 "11",
1840 "--tid",
1841 "22",
1842 "--since",
1843 "20m ago",
1844 "--until",
1845 "15m ago",
1846 "--exclude-regex-file",
1847 "/path/top.txt",
1848 "dump",
1849 "--pid",
1850 "123",
1851 "--tid",
1852 "456",
1853 "--since",
1854 "10m ago",
1855 "--until",
1856 "5m ago",
1857 "--exclude-regex-file",
1858 "/path/sub.txt",
1859 ],
1860 )
1861 .unwrap();
1862
1863 assert_eq!(cmd_sub_override.pid(), Some(123));
1864 assert_eq!(cmd_sub_override.tid(), Some(456));
1865 assert_eq!(cmd_sub_override.exclude_regex_file(), Some("/path/sub.txt"));
1866 assert!(cmd_sub_override.since().is_some());
1867 assert!(cmd_sub_override.until().is_some());
1868
1869 let cmd_top_some = LogCommand::from_args(
1871 &["log"],
1872 &[
1873 "--pid",
1874 "11",
1875 "--tid",
1876 "22",
1877 "--since",
1878 "20m ago",
1879 "--until",
1880 "15m ago",
1881 "--exclude-regex-file",
1882 "/path/top.txt",
1883 "dump",
1884 ],
1885 )
1886 .unwrap();
1887
1888 assert_eq!(cmd_top_some.pid(), Some(11));
1889 assert_eq!(cmd_top_some.tid(), Some(22));
1890 assert_eq!(cmd_top_some.exclude_regex_file(), Some("/path/top.txt"));
1891 assert!(cmd_top_some.since().is_some());
1892 assert!(cmd_top_some.until().is_some());
1893 }
1894
1895 #[test]
1896 fn test_enum_subcommand_overlay_and_preservation() {
1897 #[cfg(not(target_os = "fuchsia"))]
1899 let cmd = LogCommand::from_args(
1900 &["log"],
1901 &["--severity", "warn", "--clock", "local", "--symbolize", "off", "dump"],
1902 )
1903 .unwrap();
1904
1905 #[cfg(target_os = "fuchsia")]
1906 let cmd = LogCommand::from_args(
1907 &["log"],
1908 &["--severity", "warn", "--clock", "local", "--encoding", "fxt", "dump"],
1909 )
1910 .unwrap();
1911
1912 assert_eq!(cmd.severity(), Severity::Warn);
1913 assert_eq!(cmd.clock(), TimeFormat::Local);
1914 #[cfg(not(target_os = "fuchsia"))]
1915 assert_eq!(cmd.symbolize(), SymbolizeMode::Off);
1916 #[cfg(target_os = "fuchsia")]
1917 assert_eq!(cmd.encoding(), LogEncoding::Fxt);
1918
1919 #[cfg(not(target_os = "fuchsia"))]
1921 let cmd_override = LogCommand::from_args(
1922 &["log"],
1923 &[
1924 "--severity",
1925 "warn",
1926 "--clock",
1927 "local",
1928 "--symbolize",
1929 "off",
1930 "dump",
1931 "--severity",
1932 "error",
1933 "--clock",
1934 "utc",
1935 "--symbolize",
1936 "classic",
1937 ],
1938 )
1939 .unwrap();
1940
1941 #[cfg(target_os = "fuchsia")]
1942 let cmd_override = LogCommand::from_args(
1943 &["log"],
1944 &[
1945 "--severity",
1946 "warn",
1947 "--clock",
1948 "local",
1949 "dump",
1950 "--severity",
1951 "error",
1952 "--clock",
1953 "utc",
1954 "--encoding",
1955 "fxt",
1956 ],
1957 )
1958 .unwrap();
1959
1960 assert_eq!(cmd_override.severity(), Severity::Error);
1961 assert_eq!(cmd_override.clock(), TimeFormat::Utc);
1962 #[cfg(not(target_os = "fuchsia"))]
1963 assert_eq!(cmd_override.symbolize(), SymbolizeMode::Classic);
1964 #[cfg(target_os = "fuchsia")]
1965 assert_eq!(cmd_override.encoding(), LogEncoding::Fxt);
1966 }
1967
1968 #[test]
1969 fn test_boolean_flag_cumulative_or() {
1970 let cmd = LogCommand::from_args(
1971 &["log"],
1972 &["--no-color", "--case-sensitive", "dump", "--hide-file", "--kernel"],
1973 )
1974 .unwrap();
1975
1976 assert!(cmd.no_color());
1977 assert!(cmd.case_sensitive());
1978 assert!(cmd.hide_file());
1979 assert!(cmd.kernel());
1980 assert!(!cmd.hide_tags());
1981 assert!(!cmd.show_metadata());
1982
1983 let cmd_both = LogCommand::from_args(&["log"], &["--kernel", "dump", "--kernel"]).unwrap();
1985
1986 assert!(cmd_both.kernel());
1987 }
1988
1989 #[test]
1990 fn test_log_command_accessors() {
1991 let default_cmd = LogCommand::default();
1993 assert_eq!(default_cmd.pid(), None);
1994 assert_eq!(default_cmd.tid(), None);
1995 assert!(!default_cmd.hide_file());
1996 #[cfg(target_os = "fuchsia")]
1997 assert!(!default_cmd.json());
1998 assert!(default_cmd.exclude().is_empty());
1999 assert!(default_cmd.exclude_regex().is_empty());
2000 assert_eq!(default_cmd.since(), None);
2001 assert_eq!(default_cmd.until(), None);
2002 assert!(!default_cmd.kernel());
2003 assert!(!default_cmd.case_sensitive());
2004 #[cfg(not(target_os = "fuchsia"))]
2005 assert!(!default_cmd.disable_reconnect());
2006
2007 #[cfg(not(target_os = "fuchsia"))]
2009 let args = &[
2010 "--pid",
2011 "123",
2012 "--tid",
2013 "456",
2014 "--hide-file",
2015 "--exclude",
2016 "bad_tag",
2017 "--exclude",
2018 "other_tag",
2019 "--exclude-regex",
2020 "^error.*",
2021 "--since",
2022 "10m ago",
2023 "--until",
2024 "5m ago",
2025 "--kernel",
2026 "--case-sensitive",
2027 "--disable-reconnect",
2028 ];
2029
2030 #[cfg(target_os = "fuchsia")]
2031 let args = &[
2032 "--pid",
2033 "123",
2034 "--tid",
2035 "456",
2036 "--hide-file",
2037 "--exclude",
2038 "bad_tag",
2039 "--exclude",
2040 "other_tag",
2041 "--exclude-regex",
2042 "^error.*",
2043 "--since",
2044 "10m ago",
2045 "--until",
2046 "5m ago",
2047 "--kernel",
2048 "--case-sensitive",
2049 "--json",
2050 ];
2051
2052 let cmd = LogCommand::from_args(&["log"], args).unwrap();
2053
2054 assert_eq!(cmd.pid(), Some(123));
2055 assert_eq!(cmd.tid(), Some(456));
2056 assert!(cmd.hide_file());
2057 #[cfg(target_os = "fuchsia")]
2058 assert!(cmd.json());
2059 assert_eq!(cmd.exclude(), &["bad_tag".to_string(), "other_tag".to_string()]);
2060 assert_eq!(cmd.exclude_regex(), &["^error.*".to_string()]);
2061 assert!(cmd.since().is_some());
2062 assert!(cmd.until().is_some());
2063 assert!(cmd.kernel());
2064 assert!(cmd.case_sensitive());
2065 #[cfg(not(target_os = "fuchsia"))]
2066 assert!(cmd.disable_reconnect());
2067 }
2068}