Skip to main content

log_command/
lib.rs

1// Copyright 2023 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use 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::future::Future;
22use std::io::Write;
23use std::ops::Deref;
24use std::str::FromStr;
25use std::string::FromUtf8Error;
26use std::time::Duration;
27use thiserror::Error;
28mod filter;
29pub mod fxt_streamer;
30mod log_formatter;
31mod log_socket_stream;
32pub use filter::{FuzzyMatchWarning, LogFilterCriteria, disambiguate_queried_monikers};
33pub use log_formatter::{
34    BootTimeAccessor, DefaultLogFormatter, FormatterError, LogData, LogEntry, NoOpSymbolizer,
35    Symbolize, TIMESTAMP_FORMAT, Timestamp, WriterContainer, dump_logs_from_socket,
36};
37pub use log_socket_stream::{JsonDeserializeError, LogsDataStream};
38
39pub use log_formatter::dump_fxt_logs_from_socket;
40
41// Subcommand for ffx log (either watch or dump).
42#[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/// Sets the severity, but doesn't view any logs.
52#[argh(subcommand, name = "set-severity")]
53pub struct SetSeverityCommand {
54    /// if true, doesn't persist the interest setting
55    /// and blocks forever, keeping the connection open.
56    /// Interest settings will be reset when the command exits.
57    #[argh(switch)]
58    pub no_persist: bool,
59
60    /// if enabled, selectors will be passed directly to Archivist without any filtering.
61    /// If disabled and no matching components are found, the user will be prompted to
62    /// either enable this or be given a list of selectors to choose from.
63    #[argh(switch)]
64    pub force: bool,
65
66    /// configure the log settings on the target device for components matching
67    /// the given selector. This modifies the minimum log severity level emitted
68    /// by components during the logging session.
69    /// Specify using the format <component-selector>#<log-level>, with level
70    /// as one of FATAL|ERROR|WARN|INFO|DEBUG|TRACE.
71    /// May be repeated.
72    #[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
82/// Parses a time string that defaults to UTC. The time returned will be in the local time zone.
83pub 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
89/// Parses a duration from a string. The input is in seconds
90/// and the output is a Rust duration.
91pub 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// Time format for displaying logs
98#[derive(Clone, Debug, PartialEq)]
99pub enum TimeFormat {
100    // UTC time
101    Utc,
102    // Local time
103    Local,
104    // Boot time
105    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/// Encoding format for retrieving logs from archivist
123#[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/// Date/time structure containing a "now"
143/// field, set if it should be interpreted as the
144/// current time (used to call Subscribe instead of SnapshotThenSubscribe).
145#[derive(PartialEq, Clone, Debug)]
146pub struct DetailedDateTime {
147    /// The absolute timestamp as specified by the user
148    /// or the current timestamp if 'now' is specified.
149    pub time: DateTime<Local>,
150    /// Whether or not the DateTime was "now".
151    /// If the DateTime is "now", logs will be collected in subscribe
152    /// mode, instead of SnapshotThenSubscribe.
153    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    /// Disable all symbolization
167    Off,
168    /// Use prettified symbolization
169    Pretty,
170    /// Use classic (non-prettified) symbolization
171    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/// Helper macro to merge individual `LogFilterArgs` fields based on outer field type.
185#[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/// Helper Token-Tree (TT) Muncher macro for [`define_log_filter_args!`](define_log_filter_args!).
201///
202/// # Purpose
203/// Rust pattern destructuring and struct field initialization cannot contain documentation
204/// comments (`///`) or non-`cfg` attributes (`#[argh(...)]`). Doing so produces syntax errors.
205/// However, conditionally compiled fields (`#[cfg(...)]`) *must* be preserved in pattern
206/// destructuring and struct literals so that conditional flags compile correctly.
207///
208/// This macro uses a Token-Tree (TT) Muncher pattern to inspect all field attributes, stripping non-`cfg`
209/// attributes (`#[argh]`, `#[doc]`) while preserving `#[cfg(...)]` annotations.
210///
211/// # TT-Muncher Stages
212/// - `[ $(#[$attr])* ]`: Accumulated attributes for the field being processed.
213/// - `[ $(#[$cfg])* ]`: Accumulated `#[cfg(...)]` attributes for the field being processed.
214/// - `[ $($fields)* ]`: Output tuples of stripped fields `( [attrs] [cfgs] vis field : ty )`.
215#[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        /// Container for log filtering and display arguments.
282        #[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            /// Merges `other` filter arguments into `self`, extending list filters and overlaying non-default scalar/flag values.
303            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        /// Raw command line arguments for `ffx log` before subcommand flag merging.
313        #[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            /// configure the log settings on the target device for components matching
325            /// the given selector. This modifies the minimum log severity level emitted
326            /// by components during the logging session.
327            /// Specify using the format <component-selector>#<log-level>, with level
328            /// as one of FATAL|ERROR|WARN|INFO|DEBUG|TRACE.
329            /// May be repeated and it's also possible to pass multiple comma-separated
330            /// strings per invocation.
331            /// Cannot be used in conjunction with the set-severity subcommand.
332            #[argh(option, from_str_fn(log_interest_selector))]
333            pub set_severity: Vec<OneOrMany<LogInterestSelector>>,
334
335            $(
336                $(#[$all_attr])*
337                $vis $field: $ty_outer $( < $ty_inner > )?,
338            )*
339        }
340
341        impl RawLogCommand {
342            pub fn into_log_command(self) -> LogCommand {
343                LogCommand {
344                    sub_command: self.sub_command,
345                    set_severity: self.set_severity,
346                    filters: LogFilterArgs {
347                        $(
348                            $(#[$cfg_attr])*
349                            $field: self.$field,
350                        )*
351                    },
352                }
353            }
354        }
355
356        #[derive(ArgsInfo, FromArgs, Clone, PartialEq, Debug)]
357        /// Dumps all logs from a given target's session.
358        #[argh(subcommand, name = "dump")]
359        pub struct RawDumpCommand {
360            /// return only the last N log lines.
361            #[argh(option)]
362            pub tail: Option<usize>,
363
364            $(
365                $(#[$all_attr])*
366                $vis $field: $ty_outer $( < $ty_inner > )?,
367            )*
368        }
369
370        impl Default for RawDumpCommand {
371            fn default() -> Self {
372                let filters = LogFilterArgs::default();
373                Self {
374                    tail: None,
375                    $(
376                        $(#[$cfg_attr])*
377                        $field: filters.$field,
378                    )*
379                }
380            }
381        }
382
383        impl RawDumpCommand {
384            pub fn into_filter_args(self) -> LogFilterArgs {
385                LogFilterArgs {
386                    $(
387                        $(#[$cfg_attr])*
388                        $field: self.$field,
389                    )*
390                }
391            }
392        }
393
394        #[derive(ArgsInfo, FromArgs, Clone, PartialEq, Debug)]
395        /// Watches for and prints logs from a target. Default if no sub-command is specified.
396        #[argh(subcommand, name = "watch")]
397        pub struct RawWatchCommand {
398            $(
399                $(#[$all_attr])*
400                $vis $field: $ty_outer $( < $ty_inner > )?,
401            )*
402        }
403
404        impl Default for RawWatchCommand {
405            fn default() -> Self {
406                let filters = LogFilterArgs::default();
407                Self {
408                    $(
409                        $(#[$cfg_attr])*
410                        $field: filters.$field,
411                    )*
412                }
413            }
414        }
415
416        impl RawWatchCommand {
417            pub fn into_filter_args(self) -> LogFilterArgs {
418                LogFilterArgs {
419                    $(
420                        $(#[$cfg_attr])*
421                        $field: self.$field,
422                    )*
423                }
424            }
425        }
426    };
427}
428
429/// Macro to define `LogFilterArgs` and all derivative raw CLI command structs.
430///
431/// # Purpose
432/// `ffx log` supports filtering options both at the root command level (e.g. `ffx log --severity warn`)
433/// and at the subcommand level (e.g. `ffx log dump --severity warn`).
434///
435/// `argh` requires CLI flags to be defined directly on the struct corresponding to a command/subcommand.
436/// To avoid manually duplicating 30+ filter flags across `RawLogCommand`, `RawDumpCommand`, and `RawWatchCommand`,
437/// this macro generates:
438/// 1. `LogFilterArgs`: Container struct holding all active filter criteria.
439/// 2. `RawLogCommand`: Top-level CLI `argh` parser struct.
440/// 3. `RawDumpCommand`: `dump` subcommand `argh` parser struct.
441/// 4. `RawWatchCommand`: `watch` subcommand `argh` parser struct.
442/// 5. Conversion methods (`into_log_command`, `into_filter_args`).
443/// 6. `LogFilterArgs::merge()`: Method to merge subcommand filter overrides.
444///
445/// # Adding New Log Filter Flags
446/// To add a new CLI flag to `ffx log`:
447/// 1. Locate the `define_log_filter_args!` invocation in `src/diagnostics/lib/log-command/src/lib.rs`.
448/// 2. Add the field with doc comments (`///`) and `argh` attributes (`#[argh(option)]` or `#[argh(switch)]`).
449/// 3. The macro will automatically propagate the field to `LogFilterArgs`, all `Raw*Command` structs,
450///    their respective `into_*` conversion functions, and `LogFilterArgs::merge()`.
451/// 4. Add an encapsulated getter accessor method to `impl LogCommand`.
452///
453/// # Syntax
454/// ```rust,ignore
455/// define_log_filter_args! {
456///     /// Filter description for CLI help output.
457///     #[argh(option)]
458///     pub my_flag: Option<String>,
459/// }
460/// ```
461macro_rules! define_log_filter_args {
462    ($($tokens:tt)*) => {
463        __define_log_filter_args_helper! {
464            [ ]
465            [ ]
466            [ ]
467            $($tokens)*
468        }
469    };
470}
471
472define_log_filter_args! {
473    /// filter for a string in either the message, component or url.
474    /// May be repeated.
475    #[argh(option)]
476    pub filter: Vec<String>,
477
478    /// DEPRECATED: use --component
479    #[argh(option)]
480    pub moniker: Vec<String>,
481
482    /// fuzzy search for a component by moniker or url.
483    /// May be repeated.
484    #[argh(option)]
485    pub component: Vec<String>,
486
487    /// exclude a string in either the message, component or url.
488    /// May be repeated.
489    #[argh(option)]
490    pub exclude: Vec<String>,
491
492    /// exclude logs matching a regular expression. May be repeated.
493    #[argh(option)]
494    pub exclude_regex: Vec<String>,
495
496    /// path to a file containing regular expressions, one per line, to exclude.
497    #[argh(option)]
498    pub exclude_regex_file: Option<String>,
499
500    /// filter for only logs with a given tag. May be repeated.
501    #[argh(option)]
502    pub tag: Vec<String>,
503
504    /// exclude logs with a given tag. May be repeated.
505    #[argh(option)]
506    pub exclude_tags: Vec<String>,
507
508    /// set the minimum severity. Accepted values (from lower to higher) are: trace, debug, info,
509    /// warn (or warning), error, fatal. This field is case insensitive.
510    #[argh(option)]
511    pub severity: Option<Severity>,
512
513    /// outputs only kernel logs, unless combined with --component.
514    #[argh(switch)]
515    pub kernel: bool,
516
517    /// show only logs after a certain time (exclusive)
518    #[argh(option, from_str_fn(parse_time))]
519    pub since: Option<DetailedDateTime>,
520
521    /// show only logs after a certain time (as a boot
522    /// timestamp: seconds from the target's boot time).
523    #[argh(option, from_str_fn(parse_seconds_string_as_duration))]
524    pub since_boot: Option<Duration>,
525
526    /// show only logs until a certain time (exclusive)
527    #[argh(option, from_str_fn(parse_time))]
528    pub until: Option<DetailedDateTime>,
529
530    /// show only logs until a certain time (as a boot
531    /// timestamp: seconds since the target's boot time).
532    #[argh(option, from_str_fn(parse_seconds_string_as_duration))]
533    pub until_boot: Option<Duration>,
534
535    /// hide the tag field from output (does not exclude any log messages)
536    #[argh(switch)]
537    pub hide_tags: bool,
538
539    /// hide the file and line number field from output (does not exclude any log messages)
540    #[argh(switch)]
541    pub hide_file: bool,
542
543    /// disable coloring logs according to severity.
544    /// Note that you can permanently disable this with
545    /// `ffx config set log_cmd.color false`
546    #[argh(switch)]
547    pub no_color: bool,
548
549    /// if enabled, text filtering options are case-sensitive
550    /// this applies to --filter, --exclude, --tag, and --exclude-tags.
551    #[argh(switch)]
552    pub case_sensitive: bool,
553
554    /// shows process-id and thread-id in log output
555    #[argh(switch)]
556    pub show_metadata: bool,
557
558    /// shows the full moniker in log output. By default this is false and only the last segment
559    /// of the moniker is printed.
560    #[argh(switch)]
561    pub show_full_moniker: bool,
562
563    /// if enabled, prefer using the component URL for the component name over the moniker.
564    #[argh(switch)]
565    pub prefer_url_component_name: bool,
566
567    /// hide the moniker field from output (does not exclude any log messages)
568    #[argh(switch)]
569    pub hide_moniker: bool,
570
571    /// how to display log timestamps.
572    /// Options are "utc", "local", or "boot" (i.e. nanos since target boot).
573    /// Default is boot.
574    #[argh(option)]
575    pub clock: Option<TimeFormat>,
576
577    /// configure symbolization options. Valid options are:
578    /// - pretty (default): pretty concise symbolization
579    /// - off: disables all symbolization
580    /// - classic: traditional, non-prettified symbolization
581    #[cfg(not(target_os = "fuchsia"))]
582    #[argh(option)]
583    pub symbolize: Option<SymbolizeMode>,
584
585    /// filters by pid
586    #[argh(option)]
587    pub pid: Option<u64>,
588
589    /// filters by tid
590    #[argh(option)]
591    pub tid: Option<u64>,
592
593    /// if enabled, selectors will be passed directly to Archivist without any filtering.
594    /// If disabled and no matching components are found, the user will be prompted to
595    /// either enable this or be given a list of selectors to choose from.
596    /// This applies to both --set-severity and the set-severity subcommand.
597    #[argh(switch)]
598    pub force_set_severity: bool,
599
600    /// EXPERIMENTAL/SUBJECT TO REMOVAL: select the encoding used to retrieve logs from the
601    /// archivist. Options are "json" or "fxt". Default is "json".
602    #[cfg(target_os = "fuchsia")]
603    #[argh(option)]
604    pub encoding: Option<LogEncoding>,
605
606    /// enables structured JSON logs.
607    #[cfg(target_os = "fuchsia")]
608    #[argh(switch)]
609    pub json: bool,
610
611    /// disable automatic reconnect
612    #[cfg(not(target_os = "fuchsia"))]
613    #[argh(switch)]
614    pub disable_reconnect: bool,
615}
616
617#[derive(Default, Clone, Debug, PartialEq)]
618/// Consolidated log command representation containing merged filter criteria and subcommands.
619pub struct LogCommand {
620    pub sub_command: Option<LogSubCommand>,
621    pub set_severity: Vec<OneOrMany<LogInterestSelector>>,
622    pub filters: LogFilterArgs,
623}
624
625impl LogCommand {
626    /// Merges subcommand filter overrides (e.g. `dump` or `watch`) into `self.filters`.
627    pub fn merge_subcommand(&mut self) {
628        match &self.sub_command {
629            Some(LogSubCommand::Dump(raw_dump)) => {
630                self.filters.merge(raw_dump.clone().into_filter_args());
631            }
632            Some(LogSubCommand::Watch(raw_watch)) => {
633                self.filters.merge(raw_watch.clone().into_filter_args());
634            }
635            _ => {}
636        }
637    }
638}
639
640impl FromArgs for LogCommand {
641    fn from_args(command_name: &[&str], args: &[&str]) -> Result<Self, argh::EarlyExit> {
642        let cli = match RawLogCommand::from_args(command_name, args) {
643            Ok(cli) => cli,
644            Err(mut early_exit) => {
645                if early_exit.status.is_err() {
646                    if args.iter().any(|arg| arg.starts_with("--dump")) {
647                        early_exit.output.push_str(
648                            "\nNote: 'dump' is a sub-command of 'ffx log', not a flag. Use: ffx log dump [options]\n",
649                        );
650                    } else if args.contains(&"dump") {
651                        if args.iter().any(|arg| arg.starts_with("--limit")) {
652                            early_exit.output.push_str(
653                                "\nNote: 'ffx log dump' does not take --limit. To limit output lines, use '--tail <count>' or pipe to head/tail.\n",
654                            );
655                        }
656                        if args.iter().any(|arg| arg.starts_with("--grep")) {
657                            early_exit.output.push_str(
658                                "\nNote: 'ffx log dump' does not take --grep. To filter log snapshot output, use '--filter <pattern>' or pipe to grep: ffx log dump | grep <pattern>\n",
659                            );
660                        }
661                    }
662                }
663                return Err(early_exit);
664            }
665        };
666        let mut cmd = cli.into_log_command();
667        cmd.merge_subcommand();
668        Ok(cmd)
669    }
670}
671
672impl ArgsInfo for LogCommand {
673    fn get_args_info() -> argh::CommandInfoWithArgs {
674        RawLogCommand::get_args_info()
675    }
676}
677
678impl argh::SubCommand for LogCommand {
679    const COMMAND: &'static argh::CommandInfo = RawLogCommand::COMMAND;
680}
681
682/// Result returned from processing logs
683#[derive(PartialEq, Debug)]
684pub enum LogProcessingResult {
685    /// The caller should exit
686    Exit,
687    /// The caller should continue processing logs
688    Continue,
689}
690
691impl FromStr for SymbolizeMode {
692    type Err = anyhow::Error;
693
694    fn from_str(s: &str) -> Result<Self, Self::Err> {
695        let s = s.to_lowercase();
696        match s.as_str() {
697            "off" => Ok(SymbolizeMode::Off),
698            "pretty" => Ok(SymbolizeMode::Pretty),
699            "classic" => Ok(SymbolizeMode::Classic),
700            other => Err(format_err!("invalid symbolize flag: {}", other)),
701        }
702    }
703}
704
705#[derive(Error, Debug)]
706pub enum LogError {
707    #[error(transparent)]
708    UnknownError(#[from] anyhow::Error),
709    #[error("No boot timestamp")]
710    NoBootTimestamp,
711    #[error(transparent)]
712    IOError(#[from] std::io::Error),
713    #[error(transparent)]
714    RegexError(#[from] regex_lite::Error),
715    #[error("Cannot use dump with --since now")]
716    DumpWithSinceNow,
717    #[error("No symbolizer configuration provided")]
718    NoSymbolizerConfig,
719    #[error(transparent)]
720    FfxError(#[from] FfxError),
721    #[error(transparent)]
722    Utf8Error(#[from] FromUtf8Error),
723    #[error(transparent)]
724    FidlError(#[from] fidl::Error),
725    #[error(transparent)]
726    FormatterError(#[from] FormatterError),
727    #[error("Deprecated flag: `{flag}`, use: `{new_flag}`")]
728    DeprecatedFlag { flag: &'static str, new_flag: &'static str },
729    #[error(
730        "Fuzzy matching for '{query}' failed due to too many matches, please re-try with one of these:\n{matches}"
731    )]
732    FuzzyMatchTooManyMatches { query: String, matches: String },
733    #[error(
734        "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."
735    )]
736    SearchParameterNotFound(String),
737}
738
739impl LogError {
740    fn too_many_fuzzy_matches(matches: impl Iterator<Item = String>, query: &str) -> Self {
741        let mut result = String::new();
742        for component in matches {
743            result.push_str(&component);
744            result.push('\n');
745        }
746
747        Self::FuzzyMatchTooManyMatches { matches: result, query: query.to_string() }
748    }
749
750    pub fn is_broken_pipe(&self) -> bool {
751        match self {
752            LogError::IOError(error) => error.kind() == std::io::ErrorKind::BrokenPipe,
753            LogError::FormatterError(formatter_error) => formatter_error.is_broken_pipe(),
754            LogError::UnknownError(err) => {
755                if let Some(writer_err) = err.downcast_ref::<writer::Error>() {
756                    writer_err.is_broken_pipe()
757                } else if let Some(io_err) = err.downcast_ref::<std::io::Error>() {
758                    io_err.kind() == std::io::ErrorKind::BrokenPipe
759                } else {
760                    false
761                }
762            }
763
764            LogError::NoBootTimestamp
765            | LogError::DumpWithSinceNow
766            | LogError::NoSymbolizerConfig
767            | LogError::RegexError(_)
768            | LogError::FfxError(_)
769            | LogError::Utf8Error(_)
770            | LogError::FidlError(_)
771            | LogError::DeprecatedFlag { .. }
772            | LogError::FuzzyMatchTooManyMatches { .. }
773            | LogError::SearchParameterNotFound(_) => false,
774        }
775    }
776}
777
778/// Trait used to get available instances given a moniker query.
779pub trait InstanceGetter {
780    fn get_monikers_from_query(
781        &self,
782        query: &str,
783    ) -> impl Future<Output = Result<Vec<Moniker>, LogError>>;
784}
785
786#[cfg(not(feature = "ctf"))]
787impl InstanceGetter for RealmQueryProxy {
788    async fn get_monikers_from_query(&self, query: &str) -> Result<Vec<Moniker>, LogError> {
789        Ok(get_instances_from_query(query, self)
790            .await?
791            .into_iter()
792            .map(|value| value.moniker)
793            .collect())
794    }
795}
796
797#[cfg(feature = "ctf")]
798impl InstanceGetter for RealmQueryProxy {
799    async fn get_monikers_from_query(&self, _query: &str) -> Result<Vec<Moniker>, LogError> {
800        unreachable!("get_monikers_from_query is not supported in CTF tests.");
801    }
802}
803
804impl LogCommand {
805    /// Returns the minimum log severity.
806    #[must_use]
807    pub fn severity(&self) -> Severity {
808        self.filters.severity.unwrap_or(Severity::Info)
809    }
810
811    /// Returns the timestamp display format.
812    #[must_use]
813    pub fn clock(&self) -> TimeFormat {
814        self.filters.clock.clone().unwrap_or(TimeFormat::Boot)
815    }
816
817    /// Returns the symbolization mode.
818    #[cfg(not(target_os = "fuchsia"))]
819    #[must_use]
820    pub fn symbolize(&self) -> SymbolizeMode {
821        self.filters.symbolize.clone().unwrap_or(SymbolizeMode::Pretty)
822    }
823
824    /// Returns the log encoding format.
825    #[cfg(target_os = "fuchsia")]
826    #[must_use]
827    pub fn encoding(&self) -> LogEncoding {
828        self.filters.encoding.clone().unwrap_or(LogEncoding::Json)
829    }
830
831    /// Returns the log text filter patterns.
832    #[must_use]
833    pub fn filter(&self) -> &[String] {
834        &self.filters.filter
835    }
836
837    /// Returns the deprecated moniker filter patterns.
838    #[must_use]
839    pub fn moniker(&self) -> &[String] {
840        &self.filters.moniker
841    }
842
843    /// Returns the component filter patterns.
844    #[must_use]
845    pub fn component(&self) -> &[String] {
846        &self.filters.component
847    }
848
849    /// Returns the text exclusion patterns.
850    #[must_use]
851    pub fn exclude(&self) -> &[String] {
852        &self.filters.exclude
853    }
854
855    /// Returns the regular expression exclusion patterns.
856    #[must_use]
857    pub fn exclude_regex(&self) -> &[String] {
858        &self.filters.exclude_regex
859    }
860
861    /// Returns the tag filter patterns.
862    #[must_use]
863    pub fn tag(&self) -> &[String] {
864        &self.filters.tag
865    }
866
867    /// Returns the tag exclusion patterns.
868    #[must_use]
869    pub fn exclude_tags(&self) -> &[String] {
870        &self.filters.exclude_tags
871    }
872
873    /// Returns whether tags are hidden from output.
874    #[must_use]
875    pub fn hide_tags(&self) -> bool {
876        self.filters.hide_tags
877    }
878
879    /// Returns whether colored output is disabled.
880    #[must_use]
881    pub fn no_color(&self) -> bool {
882        self.filters.no_color
883    }
884
885    /// Sets whether colored output is disabled.
886    pub fn set_no_color(&mut self, no_color: bool) {
887        self.filters.no_color = no_color;
888    }
889
890    /// Returns whether PID and TID metadata are displayed.
891    #[must_use]
892    pub fn show_metadata(&self) -> bool {
893        self.filters.show_metadata
894    }
895
896    /// Returns whether file and line number locations are hidden.
897    #[must_use]
898    pub fn hide_file(&self) -> bool {
899        self.filters.hide_file
900    }
901
902    /// Returns whether monikers are hidden from output.
903    #[must_use]
904    pub fn hide_moniker(&self) -> bool {
905        self.filters.hide_moniker
906    }
907
908    /// Returns whether full monikers are displayed.
909    #[must_use]
910    pub fn show_full_moniker(&self) -> bool {
911        self.filters.show_full_moniker
912    }
913
914    /// Returns whether component URL is preferred over moniker for display.
915    #[must_use]
916    pub fn prefer_url_component_name(&self) -> bool {
917        self.filters.prefer_url_component_name
918    }
919
920    /// Returns the starting timestamp filter.
921    #[must_use]
922    pub fn since(&self) -> Option<&DetailedDateTime> {
923        self.filters.since.as_ref()
924    }
925
926    /// Returns the ending timestamp filter.
927    #[must_use]
928    pub fn until(&self) -> Option<&DetailedDateTime> {
929        self.filters.until.as_ref()
930    }
931
932    /// Returns the starting boot duration filter.
933    #[must_use]
934    pub fn since_boot(&self) -> Option<Duration> {
935        self.filters.since_boot
936    }
937
938    /// Returns the ending boot duration filter.
939    #[must_use]
940    pub fn until_boot(&self) -> Option<Duration> {
941        self.filters.until_boot
942    }
943
944    /// Returns whether JSON output is enabled.
945    #[cfg(target_os = "fuchsia")]
946    #[must_use]
947    pub fn json(&self) -> bool {
948        self.filters.json
949    }
950
951    /// Returns the path to the regex exclusion file, if set.
952    #[must_use]
953    pub fn exclude_regex_file(&self) -> Option<&str> {
954        self.filters.exclude_regex_file.as_deref()
955    }
956
957    /// Returns whether only kernel logs should be displayed.
958    #[must_use]
959    pub fn kernel(&self) -> bool {
960        self.filters.kernel
961    }
962
963    /// Returns whether severity selectors bypass ambiguity checks.
964    #[must_use]
965    pub fn force_set_severity(&self) -> bool {
966        self.filters.force_set_severity
967    }
968
969    /// Returns whether text filtering is case-sensitive.
970    #[must_use]
971    pub fn case_sensitive(&self) -> bool {
972        self.filters.case_sensitive
973    }
974
975    /// Returns the process ID filter, if set.
976    #[must_use]
977    pub fn pid(&self) -> Option<u64> {
978        self.filters.pid
979    }
980
981    /// Returns the thread ID filter, if set.
982    #[must_use]
983    pub fn tid(&self) -> Option<u64> {
984        self.filters.tid
985    }
986
987    /// Returns whether automatic reconnection is disabled.
988    #[cfg(not(target_os = "fuchsia"))]
989    #[must_use]
990    pub fn disable_reconnect(&self) -> bool {
991        self.filters.disable_reconnect
992    }
993
994    async fn map_interest_selectors<'a>(
995        realm_query: &impl InstanceGetter,
996        interest_selectors: impl Iterator<Item = &'a LogInterestSelector>,
997    ) -> Result<impl Iterator<Item = Cow<'a, LogInterestSelector>>, LogError> {
998        let selectors = Self::get_selectors_and_monikers(interest_selectors);
999        let mut translated_selectors = vec![];
1000        for (moniker, selector) in selectors {
1001            // Attempt to translate to a single instance
1002            let instances = realm_query.get_monikers_from_query(moniker.as_str()).await?;
1003            let instances = disambiguate_queried_monikers(moniker.as_str(), instances);
1004            // If exactly one match, perform rewrite
1005            if instances.len() == 1 {
1006                let mut translated_selector = selector.clone();
1007                translated_selector.selector = instances[0].clone().into_component_selector();
1008                translated_selectors.push((Cow::Owned(translated_selector), instances));
1009            } else {
1010                translated_selectors.push((Cow::Borrowed(selector), instances));
1011            }
1012        }
1013        if translated_selectors.iter().any(|(_, matches)| matches.len() > 1) {
1014            let mut err_output = vec![];
1015            writeln!(
1016                &mut err_output,
1017                "WARN: One or more of your selectors appears to be ambiguous"
1018            )?;
1019            writeln!(&mut err_output, "and may not match any components on your system.\n")?;
1020            writeln!(
1021                &mut err_output,
1022                "If this is unintentional you can explicitly match using the"
1023            )?;
1024            writeln!(&mut err_output, "following command:\n")?;
1025            writeln!(&mut err_output, "ffx log \\")?;
1026            let mut output = vec![];
1027            for (oselector, instances) in translated_selectors {
1028                for selector in instances {
1029                    writeln!(
1030                        output,
1031                        "\t--set-severity {}#{} \\",
1032                        sanitize_moniker_for_selectors(selector.to_string().as_str())
1033                            .replace("\\", "\\\\"),
1034                        format!("{:?}", oselector.interest.min_severity.unwrap()).to_uppercase()
1035                    )?;
1036                }
1037            }
1038            // Intentionally ignored, removes the newline, space, and \
1039            let _ = output.pop();
1040            let _ = output.pop();
1041            let _ = output.pop();
1042
1043            writeln!(&mut err_output, "{}", String::from_utf8(output).unwrap())?;
1044            writeln!(&mut err_output, "\nIf this is intentional, you can disable this with")?;
1045            writeln!(&mut err_output, "ffx log --force-set-severity.")?;
1046
1047            ffx_bail!("{}", String::from_utf8(err_output)?);
1048        }
1049        Ok(translated_selectors.into_iter().map(|(selector, _)| selector))
1050    }
1051
1052    pub fn validate_cmd_flags_with_warnings(&mut self) -> Result<Vec<&'static str>, LogError> {
1053        let mut warnings = vec![];
1054
1055        if !self.filters.moniker.is_empty() {
1056            warnings.push("WARNING: --moniker is deprecated, use --component instead");
1057            if self.filters.component.is_empty() {
1058                self.filters.component = std::mem::take(&mut self.filters.moniker);
1059            } else {
1060                warnings.push("WARNING: ignoring --moniker arguments in favor of --component");
1061            }
1062        }
1063
1064        Ok(warnings)
1065    }
1066
1067    /// Sets interest based on configured selectors.
1068    /// If a single ambiguous match is found, the monikers in the selectors
1069    /// are automatically re-written.
1070    pub async fn maybe_set_interest(
1071        &self,
1072        log_settings_client: &LogSettingsProxy,
1073        realm_query: &impl InstanceGetter,
1074    ) -> Result<(), LogError> {
1075        let (set_severity, force_set_severity, persist) =
1076            if let Some(LogSubCommand::SetSeverity(options)) = &self.sub_command {
1077                // No other argument can exist in conjunction with SetSeverity
1078                let default_cmd = LogCommand {
1079                    sub_command: Some(LogSubCommand::SetSeverity(options.clone())),
1080                    ..Default::default()
1081                };
1082                if &default_cmd != self {
1083                    ffx_bail!("Cannot combine set-severity with other options.");
1084                }
1085                (&options.interest_selector, options.force, !options.no_persist)
1086            } else {
1087                (&self.set_severity, self.filters.force_set_severity, false)
1088            };
1089
1090        if persist || !set_severity.is_empty() {
1091            let selectors = if force_set_severity {
1092                set_severity.clone().into_iter().flatten().collect::<Vec<_>>()
1093            } else {
1094                let new_selectors =
1095                    Self::map_interest_selectors(realm_query, set_severity.iter().flatten())
1096                        .await?
1097                        .map(|s| s.into_owned())
1098                        .collect::<Vec<_>>();
1099                if new_selectors.is_empty() {
1100                    set_severity.clone().into_iter().flatten().collect::<Vec<_>>()
1101                } else {
1102                    new_selectors
1103                }
1104            };
1105            log_settings_client
1106                .set_component_interest(
1107                    &flex_fuchsia_diagnostics::LogSettingsSetComponentInterestRequest {
1108                        selectors: Some(selectors),
1109                        persist: Some(persist),
1110                        ..Default::default()
1111                    },
1112                )
1113                .await?;
1114        }
1115
1116        Ok(())
1117    }
1118
1119    fn get_selectors_and_monikers<'a>(
1120        interest_selectors: impl Iterator<Item = &'a LogInterestSelector>,
1121    ) -> Vec<(String, &'a LogInterestSelector)> {
1122        let mut selectors = vec![];
1123        for selector in interest_selectors {
1124            let segments = selector.selector.moniker_segments.as_ref().unwrap();
1125            let mut full_moniker = String::new();
1126            for segment in segments {
1127                match segment {
1128                    flex_fuchsia_diagnostics::StringSelector::ExactMatch(segment) => {
1129                        if full_moniker.is_empty() {
1130                            full_moniker.push_str(segment);
1131                        } else {
1132                            full_moniker.push('/');
1133                            full_moniker.push_str(segment);
1134                        }
1135                    }
1136                    _ => {
1137                        // If the user passed a non-exact match we assume they
1138                        // know what they're doing and skip this logic.
1139                        return vec![];
1140                    }
1141                }
1142            }
1143            selectors.push((full_moniker, selector));
1144        }
1145        selectors
1146    }
1147}
1148
1149impl TopLevelCommand for LogCommand {}
1150
1151fn log_interest_selector(s: &str) -> Result<OneOrMany<LogInterestSelector>, String> {
1152    if s.contains(",") {
1153        let many: Result<Vec<LogInterestSelector>, String> = s
1154            .split(",")
1155            .map(|value| selectors::parse_log_interest_selector(value).map_err(|e| e.to_string()))
1156            .collect();
1157        Ok(OneOrMany::Many(many?))
1158    } else {
1159        Ok(OneOrMany::One(selectors::parse_log_interest_selector(s).map_err(|s| s.to_string())?))
1160    }
1161}
1162
1163#[cfg(test)]
1164mod test {
1165    use super::*;
1166    use assert_matches::assert_matches;
1167    use fidl::endpoints::create_proxy;
1168    use flex_fuchsia_diagnostics::{LogSettingsMarker, LogSettingsRequest};
1169    use futures_util::StreamExt;
1170    use futures_util::future::Either;
1171    use futures_util::stream::FuturesUnordered;
1172    use selectors::parse_log_interest_selector;
1173
1174    #[derive(Default)]
1175    struct FakeInstanceGetter {
1176        output: Vec<Moniker>,
1177        expected_selector: Option<String>,
1178    }
1179
1180    impl InstanceGetter for FakeInstanceGetter {
1181        async fn get_monikers_from_query(&self, query: &str) -> Result<Vec<Moniker>, LogError> {
1182            if let Some(expected) = &self.expected_selector {
1183                assert_eq!(expected, query);
1184            }
1185            Ok(self.output.clone())
1186        }
1187    }
1188
1189    #[fuchsia::test]
1190    async fn test_symbolize_mode_from_str() {
1191        assert_matches!(SymbolizeMode::from_str("off"), Ok(value) if value == SymbolizeMode::Off);
1192        assert_matches!(
1193            SymbolizeMode::from_str("pretty"),
1194            Ok(value) if value == SymbolizeMode::Pretty
1195        );
1196        assert_matches!(
1197            SymbolizeMode::from_str("classic"),
1198            Ok(value) if value == SymbolizeMode::Classic
1199        );
1200    }
1201
1202    #[fuchsia::test]
1203    async fn maybe_set_interest_errors_additional_arguments_passed_to_set_interest() {
1204        let (settings_proxy, settings_server) = create_proxy::<LogSettingsMarker>();
1205        let getter = FakeInstanceGetter {
1206            expected_selector: Some("ambiguous_selector".into()),
1207            output: vec![
1208                Moniker::try_from("core/some/ambiguous_selector:thing/test").unwrap(),
1209                Moniker::try_from("core/other/ambiguous_selector:thing/test").unwrap(),
1210            ],
1211        };
1212        // Main should return an error
1213
1214        let cmd = LogCommand {
1215            sub_command: Some(LogSubCommand::SetSeverity(SetSeverityCommand {
1216                interest_selector: vec![OneOrMany::One(
1217                    parse_log_interest_selector("ambiguous_selector#INFO").unwrap(),
1218                )],
1219                force: false,
1220                no_persist: false,
1221            })),
1222            filters: LogFilterArgs { hide_file: true, ..LogFilterArgs::default() },
1223            ..LogCommand::default()
1224        };
1225        let mut set_interest_result = None;
1226
1227        let mut scheduler = FuturesUnordered::new();
1228        scheduler.push(Either::Left(async {
1229            set_interest_result = Some(cmd.maybe_set_interest(&settings_proxy, &getter).await);
1230            drop(settings_proxy);
1231        }));
1232        scheduler.push(Either::Right(async {
1233            let request = settings_server.into_stream().next().await;
1234            // The channel should be closed without sending any requests.
1235            assert_matches!(request, None);
1236        }));
1237        while scheduler.next().await.is_some() {}
1238        drop(scheduler);
1239
1240        let error = format!("{}", set_interest_result.unwrap().unwrap_err());
1241
1242        const EXPECTED_INTEREST_ERROR: &str = "Cannot combine set-severity with other options.";
1243        assert_eq!(error, EXPECTED_INTEREST_ERROR);
1244    }
1245
1246    #[fuchsia::test]
1247    async fn maybe_set_interest_errors_if_ambiguous_selector() {
1248        let (settings_proxy, settings_server) = create_proxy::<LogSettingsMarker>();
1249        let getter = FakeInstanceGetter {
1250            expected_selector: Some("ambiguous_selector".into()),
1251            output: vec![
1252                Moniker::try_from("core/some/ambiguous_selector:thing/test").unwrap(),
1253                Moniker::try_from("core/other/ambiguous_selector:thing/test").unwrap(),
1254            ],
1255        };
1256        // Main should return an error
1257
1258        let cmd = LogCommand {
1259            sub_command: Some(LogSubCommand::Dump(RawDumpCommand::default())),
1260            set_severity: vec![OneOrMany::One(
1261                parse_log_interest_selector("ambiguous_selector#INFO").unwrap(),
1262            )],
1263            ..LogCommand::default()
1264        };
1265        let mut set_interest_result = None;
1266
1267        let mut scheduler = FuturesUnordered::new();
1268        scheduler.push(Either::Left(async {
1269            set_interest_result = Some(cmd.maybe_set_interest(&settings_proxy, &getter).await);
1270            drop(settings_proxy);
1271        }));
1272        scheduler.push(Either::Right(async {
1273            let request = settings_server.into_stream().next().await;
1274            // The channel should be closed without sending any requests.
1275            assert_matches!(request, None);
1276        }));
1277        while scheduler.next().await.is_some() {}
1278        drop(scheduler);
1279
1280        let error = format!("{}", set_interest_result.unwrap().unwrap_err());
1281
1282        const EXPECTED_INTEREST_ERROR: &str = r#"WARN: One or more of your selectors appears to be ambiguous
1283and may not match any components on your system.
1284
1285If this is unintentional you can explicitly match using the
1286following command:
1287
1288ffx log \
1289	--set-severity core/some/ambiguous_selector\\:thing/test#INFO \
1290	--set-severity core/other/ambiguous_selector\\:thing/test#INFO
1291
1292If this is intentional, you can disable this with
1293ffx log --force-set-severity.
1294"#;
1295        assert_eq!(error, EXPECTED_INTEREST_ERROR);
1296    }
1297
1298    #[fuchsia::test]
1299    async fn logger_translates_selector_if_one_match() {
1300        let cmd = LogCommand {
1301            sub_command: Some(LogSubCommand::Dump(RawDumpCommand::default())),
1302            set_severity: vec![OneOrMany::One(
1303                parse_log_interest_selector("ambiguous_selector#INFO").unwrap(),
1304            )],
1305            ..LogCommand::default()
1306        };
1307        let mut set_interest_result = None;
1308        let getter = FakeInstanceGetter {
1309            expected_selector: Some("ambiguous_selector".into()),
1310            output: vec![Moniker::try_from("core/some/ambiguous_selector").unwrap()],
1311        };
1312        let mut scheduler = FuturesUnordered::new();
1313        let (settings_proxy, settings_server) = create_proxy::<LogSettingsMarker>();
1314        scheduler.push(Either::Left(async {
1315            set_interest_result = Some(cmd.maybe_set_interest(&settings_proxy, &getter).await);
1316            drop(settings_proxy);
1317        }));
1318        scheduler.push(Either::Right(async {
1319            let request = settings_server.into_stream().next().await;
1320            let (payload, responder) = assert_matches!(
1321                request,
1322                Some(Ok(LogSettingsRequest::SetComponentInterest { payload, responder })) =>
1323                (payload, responder)
1324            );
1325            responder.send().unwrap();
1326            assert_eq!(
1327                payload.selectors,
1328                Some(vec![
1329                    parse_log_interest_selector("core/some/ambiguous_selector#INFO").unwrap()
1330                ])
1331            );
1332        }));
1333        while scheduler.next().await.is_some() {}
1334        drop(scheduler);
1335        assert_matches!(set_interest_result, Some(Ok(())));
1336    }
1337
1338    #[fuchsia::test]
1339    async fn logger_translates_selector_disambiguating_subcomponents() {
1340        let cmd = LogCommand {
1341            sub_command: Some(LogSubCommand::Dump(RawDumpCommand::default())),
1342            set_severity: vec![OneOrMany::One(
1343                parse_log_interest_selector("archivist#INFO").unwrap(),
1344            )],
1345            ..LogCommand::default()
1346        };
1347        let mut set_interest_result = None;
1348        let getter = FakeInstanceGetter {
1349            expected_selector: Some("archivist".into()),
1350            output: vec![
1351                Moniker::try_from("bootstrap/archivist").unwrap(),
1352                Moniker::try_from("bootstrap/archivist/archivist-pipelines").unwrap(),
1353            ],
1354        };
1355        let mut scheduler = FuturesUnordered::new();
1356        let (settings_proxy, settings_server) = create_proxy::<LogSettingsMarker>();
1357        scheduler.push(Either::Left(async {
1358            set_interest_result = Some(cmd.maybe_set_interest(&settings_proxy, &getter).await);
1359            drop(settings_proxy);
1360        }));
1361        scheduler.push(Either::Right(async {
1362            let request = settings_server.into_stream().next().await;
1363            let (payload, responder) = assert_matches!(
1364                request,
1365                Some(Ok(LogSettingsRequest::SetComponentInterest { payload, responder })) =>
1366                (payload, responder)
1367            );
1368            responder.send().unwrap();
1369            assert_eq!(
1370                payload.selectors,
1371                Some(vec![parse_log_interest_selector("bootstrap/archivist#INFO").unwrap()])
1372            );
1373        }));
1374        while scheduler.next().await.is_some() {}
1375        drop(scheduler);
1376        assert_matches!(set_interest_result, Some(Ok(())));
1377    }
1378
1379    #[fuchsia::test]
1380    async fn logger_uses_specified_selectors_if_no_results_returned() {
1381        let cmd = LogCommand {
1382            sub_command: Some(LogSubCommand::Dump(RawDumpCommand::default())),
1383            set_severity: vec![OneOrMany::One(
1384                parse_log_interest_selector("core/something/a:b/elements:main/otherstuff:*#DEBUG")
1385                    .unwrap(),
1386            )],
1387            ..LogCommand::default()
1388        };
1389        let mut set_interest_result = None;
1390        let getter = FakeInstanceGetter {
1391            expected_selector: Some("core/something/a:b/elements:main/otherstuff:*#DEBUG".into()),
1392            output: vec![],
1393        };
1394        let scheduler = FuturesUnordered::new();
1395        let (settings_proxy, settings_server) = create_proxy::<LogSettingsMarker>();
1396        scheduler.push(Either::Left(async {
1397            set_interest_result = Some(cmd.maybe_set_interest(&settings_proxy, &getter).await);
1398            drop(settings_proxy);
1399        }));
1400        scheduler.push(Either::Right(async {
1401            let request = settings_server.into_stream().next().await;
1402            let (payload, responder) = assert_matches!(
1403                request,
1404                Some(Ok(LogSettingsRequest::SetComponentInterest { payload, responder })) =>
1405                (payload, responder)
1406            );
1407            responder.send().unwrap();
1408            assert_eq!(
1409                payload.selectors,
1410                Some(vec![
1411                    parse_log_interest_selector(
1412                        "core/something/a:b/elements:main/otherstuff:*#DEBUG"
1413                    )
1414                    .unwrap()
1415                ])
1416            );
1417        }));
1418        scheduler.map(|_| Ok(())).forward(futures::sink::drain()).await.unwrap();
1419        assert_matches!(set_interest_result, Some(Ok(())));
1420    }
1421
1422    #[fuchsia::test]
1423    async fn logger_prints_ignores_ambiguity_if_force_set_severity_is_used() {
1424        let cmd = LogCommand {
1425            sub_command: Some(LogSubCommand::SetSeverity(SetSeverityCommand {
1426                no_persist: true,
1427                interest_selector: vec![OneOrMany::One(
1428                    parse_log_interest_selector("ambiguous_selector#INFO").unwrap(),
1429                )],
1430                force: true,
1431            })),
1432            ..LogCommand::default()
1433        };
1434        let getter = FakeInstanceGetter {
1435            expected_selector: Some("ambiguous_selector".into()),
1436            output: vec![
1437                Moniker::try_from("core/some/ambiguous_selector:thing/test").unwrap(),
1438                Moniker::try_from("core/other/ambiguous_selector:thing/test").unwrap(),
1439            ],
1440        };
1441        let mut set_interest_result = None;
1442        let mut scheduler = FuturesUnordered::new();
1443        let (settings_proxy, settings_server) = create_proxy::<LogSettingsMarker>();
1444        scheduler.push(Either::Left(async {
1445            set_interest_result = Some(cmd.maybe_set_interest(&settings_proxy, &getter).await);
1446            drop(settings_proxy);
1447        }));
1448        scheduler.push(Either::Right(async {
1449            let request = settings_server.into_stream().next().await;
1450            let (payload, responder) = assert_matches!(
1451                request,
1452                Some(Ok(LogSettingsRequest::SetComponentInterest { payload, responder })) =>
1453                (payload, responder)
1454            );
1455            responder.send().unwrap();
1456            assert_eq!(
1457                payload.selectors,
1458                Some(vec![parse_log_interest_selector("ambiguous_selector#INFO").unwrap()])
1459            );
1460        }));
1461        while scheduler.next().await.is_some() {}
1462        drop(scheduler);
1463        assert_matches!(set_interest_result, Some(Ok(())));
1464    }
1465
1466    #[fuchsia::test]
1467    async fn logger_prints_ignores_ambiguity_if_force_set_severity_is_used_persistent() {
1468        let cmd = LogCommand {
1469            sub_command: Some(LogSubCommand::SetSeverity(SetSeverityCommand {
1470                no_persist: false,
1471                interest_selector: vec![log_socket_stream::OneOrMany::One(
1472                    parse_log_interest_selector("ambiguous_selector#INFO").unwrap(),
1473                )],
1474                force: true,
1475            })),
1476            ..LogCommand::default()
1477        };
1478        let getter = FakeInstanceGetter {
1479            expected_selector: Some("ambiguous_selector".into()),
1480            output: vec![
1481                Moniker::try_from("core/some/ambiguous_selector:thing/test").unwrap(),
1482                Moniker::try_from("core/other/ambiguous_selector:thing/test").unwrap(),
1483            ],
1484        };
1485        let mut set_interest_result = None;
1486        let mut scheduler = FuturesUnordered::new();
1487        let (settings_proxy, settings_server) = create_proxy::<LogSettingsMarker>();
1488        scheduler.push(Either::Left(async {
1489            set_interest_result = Some(cmd.maybe_set_interest(&settings_proxy, &getter).await);
1490            drop(settings_proxy);
1491        }));
1492        scheduler.push(Either::Right(async {
1493            let request = settings_server.into_stream().next().await;
1494            let (payload, responder) = assert_matches!(
1495                request,
1496                Some(Ok(LogSettingsRequest::SetComponentInterest { payload, responder })) =>
1497                (payload, responder)
1498            );
1499            responder.send().unwrap();
1500            assert_eq!(
1501                payload.selectors,
1502                Some(vec![parse_log_interest_selector("ambiguous_selector#INFO").unwrap()])
1503            );
1504            assert_eq!(payload.persist, Some(true));
1505        }));
1506        while scheduler.next().await.is_some() {}
1507        drop(scheduler);
1508        assert_matches!(set_interest_result, Some(Ok(())));
1509    }
1510
1511    #[fuchsia::test]
1512    async fn logger_prints_ignores_ambiguity_if_machine_output_is_used() {
1513        let cmd = LogCommand {
1514            sub_command: Some(LogSubCommand::Dump(RawDumpCommand::default())),
1515            set_severity: vec![OneOrMany::One(
1516                parse_log_interest_selector("ambiguous_selector#INFO").unwrap(),
1517            )],
1518            filters: LogFilterArgs { force_set_severity: true, ..LogFilterArgs::default() },
1519        };
1520        let getter = FakeInstanceGetter {
1521            expected_selector: Some("ambiguous_selector".into()),
1522            output: vec![
1523                Moniker::try_from("core/some/collection:thing/test").unwrap(),
1524                Moniker::try_from("core/other/collection:thing/test").unwrap(),
1525            ],
1526        };
1527        let mut set_interest_result = None;
1528        let mut scheduler = FuturesUnordered::new();
1529        let (settings_proxy, settings_server) = create_proxy::<LogSettingsMarker>();
1530        scheduler.push(Either::Left(async {
1531            set_interest_result = Some(cmd.maybe_set_interest(&settings_proxy, &getter).await);
1532            drop(settings_proxy);
1533        }));
1534        scheduler.push(Either::Right(async {
1535            let request = settings_server.into_stream().next().await;
1536            let (payload, responder) = assert_matches!(
1537                request,
1538                Some(Ok(LogSettingsRequest::SetComponentInterest { payload, responder })) =>
1539                (payload, responder)
1540            );
1541            responder.send().unwrap();
1542            assert_eq!(
1543                payload.selectors,
1544                Some(vec![parse_log_interest_selector("ambiguous_selector#INFO").unwrap()])
1545            );
1546        }));
1547        while scheduler.next().await.is_some() {}
1548        drop(scheduler);
1549        assert_matches!(set_interest_result, Some(Ok(())));
1550    }
1551    #[test]
1552    fn test_parse_selector() {
1553        assert_eq!(
1554            log_interest_selector("core/audio#DEBUG").unwrap(),
1555            OneOrMany::One(parse_log_interest_selector("core/audio#DEBUG").unwrap())
1556        );
1557    }
1558
1559    #[test]
1560    fn test_parse_selector_with_commas() {
1561        assert_eq!(
1562            log_interest_selector("core/audio#DEBUG,bootstrap/archivist#TRACE").unwrap(),
1563            OneOrMany::Many(vec![
1564                parse_log_interest_selector("core/audio#DEBUG").unwrap(),
1565                parse_log_interest_selector("bootstrap/archivist#TRACE").unwrap()
1566            ])
1567        );
1568    }
1569
1570    #[test]
1571    fn test_parse_time() {
1572        assert!(parse_time("now").unwrap().is_now);
1573        let date_string = "04/20/2020";
1574        let res = parse_time(date_string).unwrap();
1575        assert!(!res.is_now);
1576        assert_eq!(
1577            res.date_naive(),
1578            parse_date_string(date_string, Local::now(), Dialect::Us).unwrap().date_naive()
1579        );
1580    }
1581
1582    #[test]
1583    fn test_log_error_is_broken_pipe() {
1584        assert!(
1585            LogError::IOError(std::io::Error::new(std::io::ErrorKind::BrokenPipe, "broken pipe"))
1586                .is_broken_pipe()
1587        );
1588        assert!(
1589            LogError::UnknownError(anyhow::Error::new(std::io::Error::new(
1590                std::io::ErrorKind::BrokenPipe,
1591                "broken pipe"
1592            )))
1593            .is_broken_pipe()
1594        );
1595        assert!(!LogError::IOError(std::io::Error::other("other")).is_broken_pipe());
1596        assert!(!LogError::NoBootTimestamp.is_broken_pipe());
1597    }
1598
1599    #[test]
1600    fn test_raw_dump_command_into_filter_args() {
1601        let raw_dump = RawDumpCommand {
1602            tail: Some(42),
1603            filter: vec!["foo".to_string()],
1604            severity: Some(Severity::Warn),
1605            hide_tags: true,
1606            ..RawDumpCommand::default()
1607        };
1608        let filters = raw_dump.into_filter_args();
1609        assert_eq!(filters.filter, vec!["foo".to_string()]);
1610        assert_eq!(filters.severity, Some(Severity::Warn));
1611        assert!(filters.hide_tags);
1612    }
1613
1614    #[test]
1615    fn test_raw_watch_command_into_filter_args() {
1616        let raw_watch = RawWatchCommand {
1617            tag: vec!["my_tag".to_string()],
1618            no_color: true,
1619            ..RawWatchCommand::default()
1620        };
1621        let filters = raw_watch.into_filter_args();
1622        assert_eq!(filters.tag, vec!["my_tag".to_string()]);
1623        assert!(filters.no_color);
1624    }
1625
1626    #[test]
1627    fn test_merge_subcommand_dump_overlay() {
1628        let cmd = LogCommand::from_args(
1629            &["log"],
1630            &[
1631                "--severity",
1632                "info",
1633                "--filter",
1634                "top_filter",
1635                "dump",
1636                "--tail",
1637                "10",
1638                "--severity",
1639                "debug",
1640                "--filter",
1641                "sub_filter",
1642            ],
1643        )
1644        .unwrap();
1645
1646        assert_eq!(cmd.severity(), Severity::Debug);
1647        assert_eq!(cmd.filter(), &["top_filter".to_string(), "sub_filter".to_string()]);
1648        if let Some(LogSubCommand::Dump(dump)) = &cmd.sub_command {
1649            assert_eq!(dump.tail, Some(10));
1650        } else {
1651            panic!("expected Dump subcommand");
1652        }
1653    }
1654
1655    #[test]
1656    fn test_merge_subcommand_watch_overlay() {
1657        let cmd = LogCommand::from_args(
1658            &["log"],
1659            &["--tag", "t1", "watch", "--tag", "t2", "--hide-tags"],
1660        )
1661        .unwrap();
1662
1663        assert_eq!(cmd.tag(), &["t1".to_string(), "t2".to_string()]);
1664        assert!(cmd.hide_tags());
1665    }
1666
1667    #[test]
1668    fn test_merge_subcommand_validate_warnings_on_merged_moniker() {
1669        let mut cmd =
1670            LogCommand::from_args(&["log"], &["dump", "--moniker", "my_moniker"]).unwrap();
1671
1672        assert_eq!(cmd.moniker(), &["my_moniker".to_string()]);
1673        let warnings = cmd.validate_cmd_flags_with_warnings().unwrap();
1674        assert!(!warnings.is_empty());
1675        assert_eq!(cmd.component(), &["my_moniker".to_string()]);
1676        assert!(cmd.moniker().is_empty());
1677    }
1678
1679    #[cfg(not(target_os = "fuchsia"))]
1680    #[fuchsia::test]
1681    async fn test_symbolize_accessor() {
1682        let cmd_default = LogCommand::from_args(&["ffx", "log"], &["dump"]).unwrap();
1683        assert_eq!(cmd_default.symbolize(), SymbolizeMode::Pretty);
1684
1685        let cmd_custom =
1686            LogCommand::from_args(&["ffx", "log"], &["dump", "--symbolize", "off"]).unwrap();
1687        assert_eq!(cmd_custom.symbolize(), SymbolizeMode::Off);
1688    }
1689
1690    #[cfg(target_os = "fuchsia")]
1691    #[fuchsia::test]
1692    async fn test_encoding_accessor() {
1693        let cmd_default = LogCommand::from_args(&["ffx", "log"], &["dump"]).unwrap();
1694        assert_eq!(cmd_default.encoding(), LogEncoding::Json);
1695
1696        let cmd_custom =
1697            LogCommand::from_args(&["ffx", "log"], &["dump", "--encoding", "fxt"]).unwrap();
1698        assert_eq!(cmd_custom.encoding(), LogEncoding::Fxt);
1699    }
1700
1701    #[fuchsia::test]
1702    async fn test_subcommand_moniker_deprecation() {
1703        let mut cmd =
1704            LogCommand::from_args(&["ffx", "log"], &["dump", "--moniker", "foo"]).unwrap();
1705        assert_eq!(cmd.filters.moniker, vec!["foo"]);
1706
1707        let warnings = cmd.validate_cmd_flags_with_warnings().unwrap();
1708        assert_eq!(warnings, vec!["WARNING: --moniker is deprecated, use --component instead"]);
1709        assert_eq!(cmd.filters.component, vec!["foo"]);
1710        assert!(cmd.filters.moniker.is_empty());
1711
1712        let mut cmd_both = LogCommand::from_args(
1713            &["ffx", "log"],
1714            &["dump", "--moniker", "foo", "--component", "bar"],
1715        )
1716        .unwrap();
1717        let warnings_both = cmd_both.validate_cmd_flags_with_warnings().unwrap();
1718        assert_eq!(
1719            warnings_both,
1720            vec![
1721                "WARNING: --moniker is deprecated, use --component instead",
1722                "WARNING: ignoring --moniker arguments in favor of --component"
1723            ]
1724        );
1725        assert_eq!(cmd_both.filters.component, vec!["bar"]);
1726    }
1727
1728    #[test]
1729    fn test_subcommand_enum_overrides() {
1730        let cmd =
1731            LogCommand::from_args(&["log"], &["--severity", "warn", "dump", "--severity", "info"])
1732                .unwrap();
1733        assert_eq!(cmd.severity(), Severity::Info);
1734        assert_eq!(cmd.filters.severity, Some(Severity::Info));
1735    }
1736
1737    #[test]
1738    fn test_merge_subcommand_no_subcommand_or_set_severity() {
1739        let initial_filters = LogFilterArgs {
1740            severity: Some(Severity::Warn),
1741            filter: vec!["test_filter".into()],
1742            no_color: true,
1743            ..Default::default()
1744        };
1745        let mut cmd_none = LogCommand {
1746            sub_command: None,
1747            set_severity: vec![],
1748            filters: initial_filters.clone(),
1749        };
1750        cmd_none.merge_subcommand();
1751        assert_eq!(cmd_none.filters, initial_filters);
1752        assert_eq!(cmd_none.sub_command, None);
1753
1754        let set_severity_cmd =
1755            SetSeverityCommand { no_persist: true, force: true, interest_selector: vec![] };
1756        let mut cmd_set_sev = LogCommand {
1757            sub_command: Some(LogSubCommand::SetSeverity(set_severity_cmd.clone())),
1758            set_severity: vec![],
1759            filters: initial_filters.clone(),
1760        };
1761        cmd_set_sev.merge_subcommand();
1762        assert_eq!(cmd_set_sev.filters, initial_filters);
1763        assert_eq!(cmd_set_sev.sub_command, Some(LogSubCommand::SetSeverity(set_severity_cmd)));
1764    }
1765
1766    #[test]
1767    fn test_macro_raw_command_conversions() {
1768        #[cfg(not(target_os = "fuchsia"))]
1769        let raw_dump = RawDumpCommand::from_args(
1770            &["dump"],
1771            &[
1772                "--tail",
1773                "50",
1774                "--severity",
1775                "error",
1776                "--filter",
1777                "dump_filter",
1778                "--symbolize",
1779                "off",
1780                "--disable-reconnect",
1781            ],
1782        )
1783        .unwrap();
1784
1785        #[cfg(target_os = "fuchsia")]
1786        let raw_dump = RawDumpCommand::from_args(
1787            &["dump"],
1788            &[
1789                "--tail",
1790                "50",
1791                "--severity",
1792                "error",
1793                "--filter",
1794                "dump_filter",
1795                "--encoding",
1796                "fxt",
1797                "--json",
1798            ],
1799        )
1800        .unwrap();
1801
1802        assert_eq!(raw_dump.tail, Some(50));
1803        let dump_filters = raw_dump.into_filter_args();
1804        assert_eq!(dump_filters.severity, Some(Severity::Error));
1805        assert_eq!(dump_filters.filter, vec!["dump_filter"]);
1806        #[cfg(not(target_os = "fuchsia"))]
1807        {
1808            assert_eq!(dump_filters.symbolize, Some(SymbolizeMode::Off));
1809            assert!(dump_filters.disable_reconnect);
1810        }
1811        #[cfg(target_os = "fuchsia")]
1812        {
1813            assert_eq!(dump_filters.encoding, Some(LogEncoding::Fxt));
1814            assert!(dump_filters.json);
1815        }
1816
1817        #[cfg(not(target_os = "fuchsia"))]
1818        let raw_watch = RawWatchCommand::from_args(
1819            &["watch"],
1820            &["--severity", "warn", "--tag", "watch_tag", "--symbolize", "classic"],
1821        )
1822        .unwrap();
1823
1824        #[cfg(target_os = "fuchsia")]
1825        let raw_watch = RawWatchCommand::from_args(
1826            &["watch"],
1827            &["--severity", "warn", "--tag", "watch_tag", "--encoding", "json"],
1828        )
1829        .unwrap();
1830
1831        let watch_filters = raw_watch.into_filter_args();
1832        assert_eq!(watch_filters.severity, Some(Severity::Warn));
1833        assert_eq!(watch_filters.tag, vec!["watch_tag"]);
1834        #[cfg(not(target_os = "fuchsia"))]
1835        assert_eq!(watch_filters.symbolize, Some(SymbolizeMode::Classic));
1836        #[cfg(target_os = "fuchsia")]
1837        assert_eq!(watch_filters.encoding, Some(LogEncoding::Json));
1838    }
1839
1840    #[test]
1841    fn test_dump_help_text() {
1842        let help_err = RawDumpCommand::from_args(&["dump"], &["--help"]).unwrap_err();
1843        let help_output = help_err.output;
1844        assert!(help_output.contains("--tail"), "dump help should include --tail");
1845        assert!(help_output.contains("--severity"), "dump help should include --severity");
1846        assert!(help_output.contains("--filter"), "dump help should include --filter");
1847        assert!(help_output.contains("--since"), "dump help should include --since");
1848        assert!(help_output.contains("--until"), "dump help should include --until");
1849    }
1850
1851    #[test]
1852    fn test_watch_help_text() {
1853        let help_err = RawWatchCommand::from_args(&["watch"], &["--help"]).unwrap_err();
1854        let help_output = help_err.output;
1855        assert!(help_output.contains("--severity"), "watch help should include --severity");
1856        assert!(help_output.contains("--filter"), "watch help should include --filter");
1857        assert!(help_output.contains("--since"), "watch help should include --since");
1858        assert!(help_output.contains("--until"), "watch help should include --until");
1859    }
1860
1861    #[test]
1862    fn test_option_field_subcommand_overlay() {
1863        // 1. Subcommand `Some` overrides top-level `None`
1864        let cmd_sub_some = LogCommand::from_args(
1865            &["log"],
1866            &[
1867                "dump",
1868                "--pid",
1869                "123",
1870                "--tid",
1871                "456",
1872                "--since",
1873                "10m ago",
1874                "--until",
1875                "5m ago",
1876                "--exclude-regex-file",
1877                "/path/sub.txt",
1878            ],
1879        )
1880        .unwrap();
1881
1882        assert_eq!(cmd_sub_some.pid(), Some(123));
1883        assert_eq!(cmd_sub_some.tid(), Some(456));
1884        assert!(cmd_sub_some.since().is_some());
1885        assert!(cmd_sub_some.until().is_some());
1886        assert_eq!(cmd_sub_some.exclude_regex_file(), Some("/path/sub.txt"));
1887
1888        // 2. Subcommand `Some` overrides top-level `Some`
1889        let cmd_sub_override = LogCommand::from_args(
1890            &["log"],
1891            &[
1892                "--pid",
1893                "11",
1894                "--tid",
1895                "22",
1896                "--since",
1897                "20m ago",
1898                "--until",
1899                "15m ago",
1900                "--exclude-regex-file",
1901                "/path/top.txt",
1902                "dump",
1903                "--pid",
1904                "123",
1905                "--tid",
1906                "456",
1907                "--since",
1908                "10m ago",
1909                "--until",
1910                "5m ago",
1911                "--exclude-regex-file",
1912                "/path/sub.txt",
1913            ],
1914        )
1915        .unwrap();
1916
1917        assert_eq!(cmd_sub_override.pid(), Some(123));
1918        assert_eq!(cmd_sub_override.tid(), Some(456));
1919        assert_eq!(cmd_sub_override.exclude_regex_file(), Some("/path/sub.txt"));
1920        assert!(cmd_sub_override.since().is_some());
1921        assert!(cmd_sub_override.until().is_some());
1922
1923        // 3. Subcommand `None` preserves top-level `Some`
1924        let cmd_top_some = LogCommand::from_args(
1925            &["log"],
1926            &[
1927                "--pid",
1928                "11",
1929                "--tid",
1930                "22",
1931                "--since",
1932                "20m ago",
1933                "--until",
1934                "15m ago",
1935                "--exclude-regex-file",
1936                "/path/top.txt",
1937                "dump",
1938            ],
1939        )
1940        .unwrap();
1941
1942        assert_eq!(cmd_top_some.pid(), Some(11));
1943        assert_eq!(cmd_top_some.tid(), Some(22));
1944        assert_eq!(cmd_top_some.exclude_regex_file(), Some("/path/top.txt"));
1945        assert!(cmd_top_some.since().is_some());
1946        assert!(cmd_top_some.until().is_some());
1947    }
1948
1949    #[test]
1950    fn test_enum_subcommand_overlay_and_preservation() {
1951        // Top-level non-default enum preserved when subcommand uses defaults
1952        #[cfg(not(target_os = "fuchsia"))]
1953        let cmd = LogCommand::from_args(
1954            &["log"],
1955            &["--severity", "warn", "--clock", "local", "--symbolize", "off", "dump"],
1956        )
1957        .unwrap();
1958
1959        #[cfg(target_os = "fuchsia")]
1960        let cmd = LogCommand::from_args(
1961            &["log"],
1962            &["--severity", "warn", "--clock", "local", "--encoding", "fxt", "dump"],
1963        )
1964        .unwrap();
1965
1966        assert_eq!(cmd.severity(), Severity::Warn);
1967        assert_eq!(cmd.clock(), TimeFormat::Local);
1968        #[cfg(not(target_os = "fuchsia"))]
1969        assert_eq!(cmd.symbolize(), SymbolizeMode::Off);
1970        #[cfg(target_os = "fuchsia")]
1971        assert_eq!(cmd.encoding(), LogEncoding::Fxt);
1972
1973        // Subcommand non-default enum overrides top-level non-default enum
1974        #[cfg(not(target_os = "fuchsia"))]
1975        let cmd_override = LogCommand::from_args(
1976            &["log"],
1977            &[
1978                "--severity",
1979                "warn",
1980                "--clock",
1981                "local",
1982                "--symbolize",
1983                "off",
1984                "dump",
1985                "--severity",
1986                "error",
1987                "--clock",
1988                "utc",
1989                "--symbolize",
1990                "classic",
1991            ],
1992        )
1993        .unwrap();
1994
1995        #[cfg(target_os = "fuchsia")]
1996        let cmd_override = LogCommand::from_args(
1997            &["log"],
1998            &[
1999                "--severity",
2000                "warn",
2001                "--clock",
2002                "local",
2003                "dump",
2004                "--severity",
2005                "error",
2006                "--clock",
2007                "utc",
2008                "--encoding",
2009                "fxt",
2010            ],
2011        )
2012        .unwrap();
2013
2014        assert_eq!(cmd_override.severity(), Severity::Error);
2015        assert_eq!(cmd_override.clock(), TimeFormat::Utc);
2016        #[cfg(not(target_os = "fuchsia"))]
2017        assert_eq!(cmd_override.symbolize(), SymbolizeMode::Classic);
2018        #[cfg(target_os = "fuchsia")]
2019        assert_eq!(cmd_override.encoding(), LogEncoding::Fxt);
2020    }
2021
2022    #[test]
2023    fn test_boolean_flag_cumulative_or() {
2024        let cmd = LogCommand::from_args(
2025            &["log"],
2026            &["--no-color", "--case-sensitive", "dump", "--hide-file", "--kernel"],
2027        )
2028        .unwrap();
2029
2030        assert!(cmd.no_color());
2031        assert!(cmd.case_sensitive());
2032        assert!(cmd.hide_file());
2033        assert!(cmd.kernel());
2034        assert!(!cmd.hide_tags());
2035        assert!(!cmd.show_metadata());
2036
2037        // Verify OR combining when set on both top-level and subcommand
2038        let cmd_both = LogCommand::from_args(&["log"], &["--kernel", "dump", "--kernel"]).unwrap();
2039
2040        assert!(cmd_both.kernel());
2041    }
2042
2043    #[test]
2044    fn test_log_command_accessors() {
2045        // Test default state
2046        let default_cmd = LogCommand::default();
2047        assert_eq!(default_cmd.pid(), None);
2048        assert_eq!(default_cmd.tid(), None);
2049        assert!(!default_cmd.hide_file());
2050        #[cfg(target_os = "fuchsia")]
2051        assert!(!default_cmd.json());
2052        assert!(default_cmd.exclude().is_empty());
2053        assert!(default_cmd.exclude_regex().is_empty());
2054        assert_eq!(default_cmd.since(), None);
2055        assert_eq!(default_cmd.until(), None);
2056        assert!(!default_cmd.kernel());
2057        assert!(!default_cmd.case_sensitive());
2058        #[cfg(not(target_os = "fuchsia"))]
2059        assert!(!default_cmd.disable_reconnect());
2060
2061        // Test with non-default values set via CLI args
2062        #[cfg(not(target_os = "fuchsia"))]
2063        let args = &[
2064            "--pid",
2065            "123",
2066            "--tid",
2067            "456",
2068            "--hide-file",
2069            "--exclude",
2070            "bad_tag",
2071            "--exclude",
2072            "other_tag",
2073            "--exclude-regex",
2074            "^error.*",
2075            "--since",
2076            "10m ago",
2077            "--until",
2078            "5m ago",
2079            "--kernel",
2080            "--case-sensitive",
2081            "--disable-reconnect",
2082        ];
2083
2084        #[cfg(target_os = "fuchsia")]
2085        let args = &[
2086            "--pid",
2087            "123",
2088            "--tid",
2089            "456",
2090            "--hide-file",
2091            "--exclude",
2092            "bad_tag",
2093            "--exclude",
2094            "other_tag",
2095            "--exclude-regex",
2096            "^error.*",
2097            "--since",
2098            "10m ago",
2099            "--until",
2100            "5m ago",
2101            "--kernel",
2102            "--case-sensitive",
2103            "--json",
2104        ];
2105
2106        let cmd = LogCommand::from_args(&["log"], args).unwrap();
2107
2108        assert_eq!(cmd.pid(), Some(123));
2109        assert_eq!(cmd.tid(), Some(456));
2110        assert!(cmd.hide_file());
2111        #[cfg(target_os = "fuchsia")]
2112        assert!(cmd.json());
2113        assert_eq!(cmd.exclude(), &["bad_tag".to_string(), "other_tag".to_string()]);
2114        assert_eq!(cmd.exclude_regex(), &["^error.*".to_string()]);
2115        assert!(cmd.since().is_some());
2116        assert!(cmd.until().is_some());
2117        assert!(cmd.kernel());
2118        assert!(cmd.case_sensitive());
2119        #[cfg(not(target_os = "fuchsia"))]
2120        assert!(cmd.disable_reconnect());
2121    }
2122
2123    #[test]
2124    fn test_dump_diagnostics_unsupported_flags() {
2125        let err = LogCommand::from_args(&["ffx", "log"], &["--dump"]).unwrap_err();
2126        assert!(err.status.is_err());
2127        assert!(err.output.contains("Unrecognized argument: --dump"));
2128        assert!(err.output.contains(
2129            "Note: 'dump' is a sub-command of 'ffx log', not a flag. Use: ffx log dump [options]"
2130        ));
2131
2132        let err = LogCommand::from_args(&["ffx", "log"], &["dump", "--limit", "100"]).unwrap_err();
2133        assert!(err.status.is_err());
2134        assert!(err.output.contains("Unrecognized argument: --limit"));
2135        assert!(err.output.contains(
2136            "Note: 'ffx log dump' does not take --limit. To limit output lines, use '--tail <count>' or pipe to head/tail."
2137        ));
2138
2139        let err =
2140            LogCommand::from_args(&["ffx", "log"], &["dump", "--limit", "100", "--limit", "200"])
2141                .unwrap_err();
2142        assert!(err.status.is_err());
2143        assert_eq!(err.output.matches("Note: 'ffx log dump' does not take --limit.").count(), 1);
2144
2145        let err = LogCommand::from_args(&["ffx", "log"], &["dump", "--grep", "hello"]).unwrap_err();
2146        assert!(err.status.is_err());
2147        assert!(err.output.contains("Unrecognized argument: --grep"));
2148        assert!(err.output.contains(
2149            "Note: 'ffx log dump' does not take --grep. To filter log snapshot output, use '--filter <pattern>' or pipe to grep: ffx log dump | grep <pattern>"
2150        ));
2151    }
2152}