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