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