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