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};
8use chrono_english::{parse_date_string, Dialect};
9use component_debug::query::get_instances_from_query;
10use diagnostics_data::Severity;
11use errors::{ffx_bail, FfxError};
12use fidl_fuchsia_diagnostics::{LogInterestSelector, LogSettingsProxy};
13use fidl_fuchsia_sys2::RealmQueryProxy;
14pub use log_socket_stream::OneOrMany;
15use moniker::Moniker;
16use selectors::{sanitize_moniker_for_selectors, SelectorExt};
17use std::borrow::Cow;
18use std::io::Write;
19use std::ops::Deref;
20use std::str::FromStr;
21use std::string::FromUtf8Error;
22use std::time::Duration;
23use thiserror::Error;
24mod filter;
25mod log_formatter;
26mod log_socket_stream;
27pub use log_formatter::{
28    dump_logs_from_socket, BootTimeAccessor, DefaultLogFormatter, FormatterError, LogData,
29    LogEntry, Symbolize, Timestamp, WriterContainer, TIMESTAMP_FORMAT,
30};
31pub use log_socket_stream::{JsonDeserializeError, LogsDataStream};
32
33// Subcommand for ffx log (either watch or dump).
34#[derive(ArgsInfo, FromArgs, Clone, PartialEq, Debug)]
35#[argh(subcommand)]
36pub enum LogSubCommand {
37    Watch(WatchCommand),
38    Dump(DumpCommand),
39    SetSeverity(SetSeverityCommand),
40}
41
42#[derive(ArgsInfo, FromArgs, Clone, PartialEq, Debug, Default)]
43/// Sets the severity, but doesn't view any logs.
44#[argh(subcommand, name = "set-severity")]
45pub struct SetSeverityCommand {
46    /// if true, doesn't persist the interest setting
47    /// and blocks forever, keeping the connection open.
48    /// Interest settings will be reset when the command exits.
49    #[argh(switch)]
50    pub no_persist: bool,
51
52    /// if enabled, selectors will be passed directly to Archivist without any filtering.
53    /// If disabled and no matching components are found, the user will be prompted to
54    /// either enable this or be given a list of selectors to choose from.
55    #[argh(switch)]
56    pub force: bool,
57
58    /// configure the log settings on the target device for components matching
59    /// the given selector. This modifies the minimum log severity level emitted
60    /// by components during the logging session.
61    /// Specify using the format <component-selector>#<log-level>, with level
62    /// as one of FATAL|ERROR|WARN|INFO|DEBUG|TRACE.
63    /// May be repeated.
64    #[argh(positional, from_str_fn(log_interest_selector))]
65    pub interest_selector: Vec<OneOrMany<LogInterestSelector>>,
66}
67
68#[derive(ArgsInfo, FromArgs, Clone, PartialEq, Debug)]
69/// Watches for and prints logs from a target. Default if no sub-command is specified.
70#[argh(subcommand, name = "watch")]
71pub struct WatchCommand {}
72
73#[derive(ArgsInfo, FromArgs, Clone, PartialEq, Debug)]
74/// Dumps all log from a given target's session.
75#[argh(subcommand, name = "dump")]
76pub struct DumpCommand {}
77
78pub fn parse_time(value: &str) -> Result<DetailedDateTime, String> {
79    parse_date_string(value, Local::now(), Dialect::Us)
80        .map(|time| DetailedDateTime { time, is_now: value == "now" })
81        .map_err(|e| format!("invalid date string: {e}"))
82}
83
84/// Parses a duration from a string. The input is in seconds
85/// and the output is a Rust duration.
86pub fn parse_seconds_string_as_duration(value: &str) -> Result<Duration, String> {
87    Ok(Duration::from_secs(
88        value.parse().map_err(|e| format!("value '{value}' is not a number: {e}"))?,
89    ))
90}
91
92// Time format for displaying logs
93#[derive(Clone, Debug, PartialEq)]
94pub enum TimeFormat {
95    // UTC time
96    Utc,
97    // Local time
98    Local,
99    // Boot time
100    Boot,
101}
102
103impl std::str::FromStr for TimeFormat {
104    type Err = String;
105
106    fn from_str(s: &str) -> Result<Self, Self::Err> {
107        let lower = s.to_ascii_lowercase();
108        match lower.as_str() {
109            "local" => Ok(TimeFormat::Local),
110            "utc" => Ok(TimeFormat::Utc),
111            "boot" => Ok(TimeFormat::Boot),
112            _ => Err(format!("'{s}' is not a valid value: must be one of 'local', 'utc', 'boot'")),
113        }
114    }
115}
116
117/// Date/time structure containing a "now"
118/// field, set if it should be interpreted as the
119/// current time (used to call Subscribe instead of SnapshotThenSubscribe).
120#[derive(PartialEq, Clone, Debug)]
121pub struct DetailedDateTime {
122    /// The absolute timestamp as specified by the user
123    /// or the current timestamp if 'now' is specified.
124    pub time: DateTime<Local>,
125    /// Whether or not the DateTime was "now".
126    /// If the DateTime is "now", logs will be collected in subscribe
127    /// mode, instead of SnapshotThenSubscribe.
128    pub is_now: bool,
129}
130
131impl Deref for DetailedDateTime {
132    type Target = DateTime<Local>;
133
134    fn deref(&self) -> &Self::Target {
135        &self.time
136    }
137}
138
139#[derive(Clone, PartialEq, Debug)]
140pub enum SymbolizeMode {
141    /// Disable all symbolization
142    Off,
143    /// Use prettified symbolization
144    Pretty,
145    /// Use classic (non-prettified) symbolization
146    Classic,
147}
148
149impl SymbolizeMode {
150    pub fn is_prettification_disabled(&self) -> bool {
151        matches!(self, SymbolizeMode::Classic)
152    }
153
154    pub fn is_symbolize_disabled(&self) -> bool {
155        matches!(self, SymbolizeMode::Off)
156    }
157}
158
159#[derive(ArgsInfo, FromArgs, Clone, Debug, PartialEq)]
160#[argh(
161    subcommand,
162    name = "log",
163    description = "Display logs from a target device",
164    note = "Logs are retrieve from the target at the moment this command is called.
165
166You may see some additional information attached to the log line:
167
168- `dropped=N`: this means that N logs attributed to the component were dropped when the component
169  wrote to the log socket. This can happen when archivist cannot keep up with the rate of logs being
170  emitted by the component and the component filled the log socket buffer in the kernel.
171
172- `rolled=N`: this means that N logs rolled out from the archivist buffer and ffx never saw them.
173  This can happen when more logs are being ingested by the archivist across all components and the
174  ffx couldn't retrieve them fast enough.
175
176Symbolization is performed in the background using the symbolizer host tool. You can pass
177additional arguments to the symbolizer tool (for example, to add a remote symbol server) using:
178  $ ffx config set proactive_log.symbolize.extra_args \"--symbol-server gs://some-url/path --symbol-server gs://some-other-url/path ...\"
179
180To learn more about configuring the log viewer, visit https://fuchsia.dev/fuchsia-src/development/tools/ffx/commands/log",
181    example = "\
182Dump the most recent logs and stream new ones as they happen:
183  $ ffx log
184
185Stream new logs starting from the current time, filtering for severity of at least \"WARN\":
186  $ ffx log --severity warn --since now
187
188Stream logs where the source moniker, component url and message do not include \"sys\":
189  $ ffx log --exclude sys
190
191Stream ERROR logs with source moniker, component url or message containing either
192\"netstack\" or \"remote-control.cm\", but not containing \"sys\":
193  $ ffx log --severity error --filter netstack --filter remote-control.cm --exclude sys
194
195Dump all available logs where the source moniker, component url, or message contains
196\"remote-control\":
197  $ ffx log --filter remote-control dump
198
199Dump all logs from the last 30 minutes logged before 5 minutes ago:
200  $ ffx log --since \"30m ago\" --until \"5m ago\" dump
201
202Enable DEBUG logs from the \"core/audio\" component while logs are streaming:
203  $ ffx log --set-severity core/audio#DEBUG"
204)]
205pub struct LogCommand {
206    #[argh(subcommand)]
207    pub sub_command: Option<LogSubCommand>,
208
209    /// filter for a string in either the message, component or url.
210    /// May be repeated.
211    #[argh(option)]
212    pub filter: Vec<String>,
213
214    /// DEPRECATED: use --component
215    #[argh(option)]
216    pub moniker: Vec<String>,
217
218    /// fuzzy search for a component by moniker or url.
219    /// May be repeated.
220    #[argh(option)]
221    pub component: Vec<String>,
222
223    /// exclude a string in either the message, component or url.
224    /// May be repeated.
225    #[argh(option)]
226    pub exclude: Vec<String>,
227
228    /// filter for only logs with a given tag. May be repeated.
229    #[argh(option)]
230    pub tag: Vec<String>,
231
232    /// exclude logs with a given tag. May be repeated.
233    #[argh(option)]
234    pub exclude_tags: Vec<String>,
235
236    /// set the minimum severity. Accepted values (from lower to higher) are: trace, debug, info,
237    /// warn (or warning), error, fatal. This field is case insensitive.
238    #[argh(option, default = "Severity::Info")]
239    pub severity: Severity,
240
241    /// outputs only kernel logs, unless combined with --component.
242    #[argh(switch)]
243    pub kernel: bool,
244
245    /// show only logs after a certain time (exclusive)
246    #[argh(option, from_str_fn(parse_time))]
247    pub since: Option<DetailedDateTime>,
248
249    /// show only logs after a certain time (as a boot
250    /// timestamp: seconds from the target's boot time).
251    #[argh(option, from_str_fn(parse_seconds_string_as_duration))]
252    pub since_boot: Option<Duration>,
253
254    /// show only logs until a certain time (exclusive)
255    #[argh(option, from_str_fn(parse_time))]
256    pub until: Option<DetailedDateTime>,
257
258    /// show only logs until a certain time (as a a boot
259    /// timestamp: seconds since the target's boot time).
260    #[argh(option, from_str_fn(parse_seconds_string_as_duration))]
261    pub until_boot: Option<Duration>,
262
263    /// hide the tag field from output (does not exclude any log messages)
264    #[argh(switch)]
265    pub hide_tags: bool,
266
267    /// hide the file and line number field from output (does not exclude any log messages)
268    #[argh(switch)]
269    pub hide_file: bool,
270
271    /// disable coloring logs according to severity.
272    /// Note that you can permanently disable this with
273    /// `ffx config set log_cmd.color false`
274    #[argh(switch)]
275    pub no_color: bool,
276
277    /// if enabled, text filtering options are case-sensitive
278    /// this applies to --filter, --exclude, --tag, and --exclude-tag.
279    #[argh(switch)]
280    pub case_sensitive: bool,
281
282    /// shows process-id and thread-id in log output
283    #[argh(switch)]
284    pub show_metadata: bool,
285
286    /// shows the full moniker in log output. By default this is false and only the last segment
287    /// of the moniker is printed.
288    #[argh(switch)]
289    pub show_full_moniker: bool,
290
291    /// how to display log timestamps.
292    /// Options are "utc", "local", or "boot" (i.e. nanos since target boot).
293    /// Default is boot.
294    #[argh(option, default = "TimeFormat::Boot")]
295    pub clock: TimeFormat,
296
297    /// configure symbolization options. Valid options are:
298    /// - pretty (default): pretty concise symbolization
299    /// - off: disables all symbolization
300    /// - classic: traditional, non-prettified symbolization
301    #[cfg(not(target_os = "fuchsia"))]
302    #[argh(option, default = "SymbolizeMode::Pretty")]
303    pub symbolize: SymbolizeMode,
304
305    /// configure the log settings on the target device for components matching
306    /// the given selector. This modifies the minimum log severity level emitted
307    /// by components during the logging session.
308    /// Specify using the format <component-selector>#<log-level>, with level
309    /// as one of FATAL|ERROR|WARN|INFO|DEBUG|TRACE.
310    /// May be repeated and it's also possible to pass multiple comma-separated
311    /// strings per invocation.
312    /// Cannot be used in conjunction with --set-severity-persist.
313    #[argh(option, from_str_fn(log_interest_selector))]
314    pub set_severity: Vec<OneOrMany<LogInterestSelector>>,
315
316    /// filters by pid
317    #[argh(option)]
318    pub pid: Option<u64>,
319
320    /// filters by tid
321    #[argh(option)]
322    pub tid: Option<u64>,
323
324    /// if enabled, selectors will be passed directly to Archivist without any filtering.
325    /// If disabled and no matching components are found, the user will be prompted to
326    /// either enable this or be given a list of selectors to choose from.
327    /// This applies to both --set-severity and --set-severity-persist.
328    #[argh(switch)]
329    pub force_set_severity: bool,
330
331    /// enables structured JSON logs.
332    #[cfg(target_os = "fuchsia")]
333    #[argh(switch)]
334    pub json: bool,
335}
336
337impl Default for LogCommand {
338    fn default() -> Self {
339        LogCommand {
340            filter: vec![],
341            moniker: vec![],
342            component: vec![],
343            exclude: vec![],
344            tag: vec![],
345            exclude_tags: vec![],
346            hide_tags: false,
347            hide_file: false,
348            clock: TimeFormat::Boot,
349            no_color: false,
350            kernel: false,
351            severity: Severity::Info,
352            show_metadata: false,
353            force_set_severity: false,
354            since: None,
355            since_boot: None,
356            until: None,
357            case_sensitive: false,
358            until_boot: None,
359            sub_command: None,
360            set_severity: vec![],
361            show_full_moniker: false,
362            pid: None,
363            tid: None,
364            #[cfg(target_os = "fuchsia")]
365            json: false,
366            #[cfg(not(target_os = "fuchsia"))]
367            symbolize: SymbolizeMode::Pretty,
368        }
369    }
370}
371
372/// Result returned from processing logs
373#[derive(PartialEq, Debug)]
374pub enum LogProcessingResult {
375    /// The caller should exit
376    Exit,
377    /// The caller should continue processing logs
378    Continue,
379}
380
381impl FromStr for SymbolizeMode {
382    type Err = anyhow::Error;
383
384    fn from_str(s: &str) -> Result<Self, Self::Err> {
385        let s = s.to_lowercase();
386        match s.as_str() {
387            "off" => Ok(SymbolizeMode::Off),
388            "pretty" => Ok(SymbolizeMode::Pretty),
389            "classic" => Ok(SymbolizeMode::Classic),
390            other => Err(format_err!("invalid symbolize flag: {}", other)),
391        }
392    }
393}
394
395#[derive(Error, Debug)]
396pub enum LogError {
397    #[error(transparent)]
398    UnknownError(#[from] anyhow::Error),
399    #[error("No boot timestamp")]
400    NoBootTimestamp,
401    #[error(transparent)]
402    IOError(#[from] std::io::Error),
403    #[error("Cannot use dump with --since now")]
404    DumpWithSinceNow,
405    #[error("No symbolizer configuration provided")]
406    NoSymbolizerConfig,
407    #[error(transparent)]
408    FfxError(#[from] FfxError),
409    #[error(transparent)]
410    Utf8Error(#[from] FromUtf8Error),
411    #[error(transparent)]
412    FidlError(#[from] fidl::Error),
413    #[error(transparent)]
414    FormatterError(#[from] FormatterError),
415    #[error("Deprecated flag: `{flag}`, use: `{new_flag}`")]
416    DeprecatedFlag { flag: &'static str, new_flag: &'static str },
417    #[error(
418        "Fuzzy matching failed due to too many matches, please re-try with one of these:\n{0}"
419    )]
420    FuzzyMatchTooManyMatches(String),
421    #[error("No running components were found matching {0}")]
422    SearchParameterNotFound(String),
423}
424
425impl LogError {
426    fn too_many_fuzzy_matches(matches: impl Iterator<Item = String>) -> Self {
427        let mut result = String::new();
428        for component in matches {
429            result.push_str(&component);
430            result.push('\n');
431        }
432
433        Self::FuzzyMatchTooManyMatches(result)
434    }
435}
436
437/// Trait used to get available instances given a moniker query.
438#[async_trait::async_trait(?Send)]
439pub trait InstanceGetter {
440    async fn get_monikers_from_query(&self, query: &str) -> Result<Vec<Moniker>, LogError>;
441}
442
443#[async_trait::async_trait(?Send)]
444impl InstanceGetter for RealmQueryProxy {
445    async fn get_monikers_from_query(&self, query: &str) -> Result<Vec<Moniker>, LogError> {
446        Ok(get_instances_from_query(query, self)
447            .await?
448            .into_iter()
449            .map(|value| value.moniker)
450            .collect())
451    }
452}
453
454impl LogCommand {
455    async fn map_interest_selectors<'a>(
456        realm_query: &impl InstanceGetter,
457        interest_selectors: impl Iterator<Item = &'a LogInterestSelector>,
458    ) -> Result<impl Iterator<Item = Cow<'a, LogInterestSelector>>, LogError> {
459        let selectors = Self::get_selectors_and_monikers(interest_selectors);
460        let mut translated_selectors = vec![];
461        for (moniker, selector) in selectors {
462            // Attempt to translate to a single instance
463            let instances = realm_query.get_monikers_from_query(moniker.as_str()).await?;
464            // If exactly one match, perform rewrite
465            if instances.len() == 1 {
466                let mut translated_selector = selector.clone();
467                translated_selector.selector = instances[0].clone().into_component_selector();
468                translated_selectors.push((Cow::Owned(translated_selector), instances));
469            } else {
470                translated_selectors.push((Cow::Borrowed(selector), instances));
471            }
472        }
473        if translated_selectors.iter().any(|(_, matches)| matches.len() > 1) {
474            let mut err_output = vec![];
475            writeln!(
476                &mut err_output,
477                "WARN: One or more of your selectors appears to be ambiguous"
478            )?;
479            writeln!(&mut err_output, "and may not match any components on your system.\n")?;
480            writeln!(
481                &mut err_output,
482                "If this is unintentional you can explicitly match using the"
483            )?;
484            writeln!(&mut err_output, "following command:\n")?;
485            writeln!(&mut err_output, "ffx log \\")?;
486            let mut output = vec![];
487            for (oselector, instances) in translated_selectors {
488                for selector in instances {
489                    writeln!(
490                        output,
491                        "\t--set-severity {}#{} \\",
492                        sanitize_moniker_for_selectors(selector.to_string().as_str())
493                            .replace("\\", "\\\\"),
494                        format!("{:?}", oselector.interest.min_severity.unwrap()).to_uppercase()
495                    )?;
496                }
497            }
498            // Intentionally ignored, removes the newline, space, and \
499            let _ = output.pop();
500            let _ = output.pop();
501            let _ = output.pop();
502
503            writeln!(&mut err_output, "{}", String::from_utf8(output).unwrap())?;
504            writeln!(&mut err_output, "\nIf this is intentional, you can disable this with")?;
505            writeln!(&mut err_output, "ffx log --force-set-severity.")?;
506
507            ffx_bail!("{}", String::from_utf8(err_output)?);
508        }
509        Ok(translated_selectors.into_iter().map(|(selector, _)| selector))
510    }
511
512    pub fn validate_cmd_flags_with_warnings(&mut self) -> Result<Vec<&'static str>, LogError> {
513        let mut warnings = vec![];
514
515        if !self.moniker.is_empty() {
516            warnings.push("WARNING: --moniker is deprecated, use --component instead");
517            if self.component.is_empty() {
518                self.component = std::mem::take(&mut self.moniker);
519            } else {
520                warnings.push("WARNING: ignoring --moniker arguments in favor of --component");
521            }
522        }
523
524        Ok(warnings)
525    }
526
527    /// Sets interest based on configured selectors.
528    /// If a single ambiguous match is found, the monikers in the selectors
529    /// are automatically re-written.
530    pub async fn maybe_set_interest(
531        &self,
532        log_settings_client: &LogSettingsProxy,
533        realm_query: &impl InstanceGetter,
534    ) -> Result<(), LogError> {
535        let (set_severity, force_set_severity, persist) =
536            if let Some(LogSubCommand::SetSeverity(options)) = &self.sub_command {
537                // No other argument can exist in conjunction with SetSeverity
538                let default_cmd = LogCommand {
539                    sub_command: Some(LogSubCommand::SetSeverity(options.clone())),
540                    ..Default::default()
541                };
542                if &default_cmd != self {
543                    ffx_bail!("Cannot combine set-severity with other options.");
544                }
545                (&options.interest_selector, options.force, !options.no_persist)
546            } else {
547                (&self.set_severity, self.force_set_severity, false)
548            };
549        if persist {
550            let selectors = if force_set_severity {
551                set_severity.clone().into_iter().flatten().collect::<Vec<_>>()
552            } else {
553                let new_selectors =
554                    Self::map_interest_selectors(realm_query, set_severity.iter().flatten())
555                        .await?
556                        .map(|s| s.into_owned())
557                        .collect::<Vec<_>>();
558                if new_selectors.is_empty() {
559                    set_severity.clone().into_iter().flatten().collect::<Vec<_>>()
560                } else {
561                    new_selectors
562                }
563            };
564            log_settings_client
565                .set_component_interest(
566                    &fidl_fuchsia_diagnostics::LogSettingsSetComponentInterestRequest {
567                        selectors: Some(selectors),
568                        persist: Some(true),
569                        ..Default::default()
570                    },
571                )
572                .await?;
573        } else if !set_severity.is_empty() {
574            let selectors = if force_set_severity {
575                set_severity.clone().into_iter().flatten().collect::<Vec<_>>()
576            } else {
577                let new_selectors =
578                    Self::map_interest_selectors(realm_query, set_severity.iter().flatten())
579                        .await?
580                        .map(|s| s.into_owned())
581                        .collect::<Vec<_>>();
582                if new_selectors.is_empty() {
583                    set_severity.clone().into_iter().flatten().collect::<Vec<_>>()
584                } else {
585                    new_selectors
586                }
587            };
588
589            log_settings_client.set_interest(&selectors).await?;
590        }
591
592        Ok(())
593    }
594
595    fn get_selectors_and_monikers<'a>(
596        interest_selectors: impl Iterator<Item = &'a LogInterestSelector>,
597    ) -> Vec<(String, &'a LogInterestSelector)> {
598        let mut selectors = vec![];
599        for selector in interest_selectors {
600            let segments = selector.selector.moniker_segments.as_ref().unwrap();
601            let mut full_moniker = String::new();
602            for segment in segments {
603                match segment {
604                    fidl_fuchsia_diagnostics::StringSelector::ExactMatch(segment) => {
605                        if full_moniker.is_empty() {
606                            full_moniker.push_str(segment);
607                        } else {
608                            full_moniker.push('/');
609                            full_moniker.push_str(segment);
610                        }
611                    }
612                    _ => {
613                        // If the user passed a non-exact match we assume they
614                        // know what they're doing and skip this logic.
615                        return vec![];
616                    }
617                }
618            }
619            selectors.push((full_moniker, selector));
620        }
621        selectors
622    }
623}
624
625impl TopLevelCommand for LogCommand {}
626
627fn log_interest_selector(s: &str) -> Result<OneOrMany<LogInterestSelector>, String> {
628    if s.contains(",") {
629        let many: Result<Vec<LogInterestSelector>, String> = s
630            .split(",")
631            .map(|value| selectors::parse_log_interest_selector(value).map_err(|e| e.to_string()))
632            .collect();
633        Ok(OneOrMany::Many(many?))
634    } else {
635        Ok(OneOrMany::One(selectors::parse_log_interest_selector(s).map_err(|s| s.to_string())?))
636    }
637}
638
639#[cfg(test)]
640mod test {
641    use super::*;
642    use assert_matches::assert_matches;
643    use async_trait::async_trait;
644    use fidl::endpoints::create_proxy;
645    use fidl_fuchsia_diagnostics::{LogSettingsMarker, LogSettingsRequest};
646    use futures_util::future::Either;
647    use futures_util::stream::FuturesUnordered;
648    use futures_util::StreamExt;
649    use selectors::parse_log_interest_selector;
650
651    #[derive(Default)]
652    struct FakeInstanceGetter {
653        output: Vec<Moniker>,
654        expected_selector: Option<String>,
655    }
656
657    #[async_trait(?Send)]
658    impl InstanceGetter for FakeInstanceGetter {
659        async fn get_monikers_from_query(&self, query: &str) -> Result<Vec<Moniker>, LogError> {
660            if let Some(expected) = &self.expected_selector {
661                assert_eq!(expected, query);
662            }
663            Ok(self.output.clone())
664        }
665    }
666
667    #[fuchsia::test]
668    async fn test_symbolize_mode_from_str() {
669        assert_matches!(SymbolizeMode::from_str("off"), Ok(value) if value == SymbolizeMode::Off);
670        assert_matches!(
671            SymbolizeMode::from_str("pretty"),
672            Ok(value) if value == SymbolizeMode::Pretty
673        );
674        assert_matches!(
675            SymbolizeMode::from_str("classic"),
676            Ok(value) if value == SymbolizeMode::Classic
677        );
678    }
679
680    #[fuchsia::test]
681    async fn maybe_set_interest_errors_additional_arguments_passed_to_set_interest() {
682        let (settings_proxy, settings_server) = create_proxy::<LogSettingsMarker>();
683        let getter = FakeInstanceGetter {
684            expected_selector: Some("ambiguous_selector".into()),
685            output: vec![
686                Moniker::try_from("core/some/ambiguous_selector:thing/test").unwrap(),
687                Moniker::try_from("core/other/ambiguous_selector:thing/test").unwrap(),
688            ],
689        };
690        // Main should return an error
691
692        let cmd = LogCommand {
693            sub_command: Some(LogSubCommand::SetSeverity(SetSeverityCommand {
694                interest_selector: vec![OneOrMany::One(
695                    parse_log_interest_selector("ambiguous_selector#INFO").unwrap(),
696                )],
697                force: false,
698                no_persist: false,
699            })),
700            hide_file: true,
701            ..LogCommand::default()
702        };
703        let mut set_interest_result = None;
704
705        let mut scheduler = FuturesUnordered::new();
706        scheduler.push(Either::Left(async {
707            set_interest_result = Some(cmd.maybe_set_interest(&settings_proxy, &getter).await);
708            drop(settings_proxy);
709        }));
710        scheduler.push(Either::Right(async {
711            let request = settings_server.into_stream().next().await;
712            // The channel should be closed without sending any requests.
713            assert_matches!(request, None);
714        }));
715        while scheduler.next().await.is_some() {}
716        drop(scheduler);
717
718        let error = format!("{}", set_interest_result.unwrap().unwrap_err());
719
720        const EXPECTED_INTEREST_ERROR: &str = "Cannot combine set-severity with other options.";
721        assert_eq!(error, EXPECTED_INTEREST_ERROR);
722    }
723
724    #[fuchsia::test]
725    async fn maybe_set_interest_errors_if_ambiguous_selector() {
726        let (settings_proxy, settings_server) = create_proxy::<LogSettingsMarker>();
727        let getter = FakeInstanceGetter {
728            expected_selector: Some("ambiguous_selector".into()),
729            output: vec![
730                Moniker::try_from("core/some/ambiguous_selector:thing/test").unwrap(),
731                Moniker::try_from("core/other/ambiguous_selector:thing/test").unwrap(),
732            ],
733        };
734        // Main should return an error
735
736        let cmd = LogCommand {
737            sub_command: Some(LogSubCommand::Dump(DumpCommand {})),
738            set_severity: vec![OneOrMany::One(
739                parse_log_interest_selector("ambiguous_selector#INFO").unwrap(),
740            )],
741            ..LogCommand::default()
742        };
743        let mut set_interest_result = None;
744
745        let mut scheduler = FuturesUnordered::new();
746        scheduler.push(Either::Left(async {
747            set_interest_result = Some(cmd.maybe_set_interest(&settings_proxy, &getter).await);
748            drop(settings_proxy);
749        }));
750        scheduler.push(Either::Right(async {
751            let request = settings_server.into_stream().next().await;
752            // The channel should be closed without sending any requests.
753            assert_matches!(request, None);
754        }));
755        while scheduler.next().await.is_some() {}
756        drop(scheduler);
757
758        let error = format!("{}", set_interest_result.unwrap().unwrap_err());
759
760        const EXPECTED_INTEREST_ERROR: &str = r#"WARN: One or more of your selectors appears to be ambiguous
761and may not match any components on your system.
762
763If this is unintentional you can explicitly match using the
764following command:
765
766ffx log \
767	--set-severity core/some/ambiguous_selector\\:thing/test#INFO \
768	--set-severity core/other/ambiguous_selector\\:thing/test#INFO
769
770If this is intentional, you can disable this with
771ffx log --force-set-severity.
772"#;
773        assert_eq!(error, EXPECTED_INTEREST_ERROR);
774    }
775
776    #[fuchsia::test]
777    async fn logger_translates_selector_if_one_match() {
778        let cmd = LogCommand {
779            sub_command: Some(LogSubCommand::Dump(DumpCommand {})),
780            set_severity: vec![OneOrMany::One(
781                parse_log_interest_selector("ambiguous_selector#INFO").unwrap(),
782            )],
783            ..LogCommand::default()
784        };
785        let mut set_interest_result = None;
786        let getter = FakeInstanceGetter {
787            expected_selector: Some("ambiguous_selector".into()),
788            output: vec![Moniker::try_from("core/some/ambiguous_selector").unwrap()],
789        };
790        let mut scheduler = FuturesUnordered::new();
791        let (settings_proxy, settings_server) = create_proxy::<LogSettingsMarker>();
792        scheduler.push(Either::Left(async {
793            set_interest_result = Some(cmd.maybe_set_interest(&settings_proxy, &getter).await);
794            drop(settings_proxy);
795        }));
796        scheduler.push(Either::Right(async {
797            let request = settings_server.into_stream().next().await;
798            let (selectors, responder) = assert_matches!(
799                request,
800                Some(Ok(LogSettingsRequest::SetInterest { selectors, responder })) =>
801                (selectors, responder)
802            );
803            responder.send().unwrap();
804            assert_eq!(
805                selectors,
806                vec![parse_log_interest_selector("core/some/ambiguous_selector#INFO").unwrap()]
807            );
808        }));
809        while scheduler.next().await.is_some() {}
810        drop(scheduler);
811        assert_matches!(set_interest_result, Some(Ok(())));
812    }
813
814    #[fuchsia::test]
815    async fn logger_uses_specified_selectors_if_no_results_returned() {
816        let cmd = LogCommand {
817            sub_command: Some(LogSubCommand::Dump(DumpCommand {})),
818            set_severity: vec![OneOrMany::One(
819                parse_log_interest_selector("core/something/a:b/elements:main/otherstuff:*#DEBUG")
820                    .unwrap(),
821            )],
822            ..LogCommand::default()
823        };
824        let mut set_interest_result = None;
825        let getter = FakeInstanceGetter {
826            expected_selector: Some("core/something/a:b/elements:main/otherstuff:*#DEBUG".into()),
827            output: vec![],
828        };
829        let scheduler = FuturesUnordered::new();
830        let (settings_proxy, settings_server) = create_proxy::<LogSettingsMarker>();
831        scheduler.push(Either::Left(async {
832            set_interest_result = Some(cmd.maybe_set_interest(&settings_proxy, &getter).await);
833            drop(settings_proxy);
834        }));
835        scheduler.push(Either::Right(async {
836            let request = settings_server.into_stream().next().await;
837            let (selectors, responder) = assert_matches!(
838                request,
839                Some(Ok(LogSettingsRequest::SetInterest { selectors, responder })) =>
840                (selectors, responder)
841            );
842            responder.send().unwrap();
843            assert_eq!(
844                selectors,
845                vec![parse_log_interest_selector(
846                    "core/something/a:b/elements:main/otherstuff:*#DEBUG"
847                )
848                .unwrap()]
849            );
850        }));
851        scheduler.map(|_| Ok(())).forward(futures::sink::drain()).await.unwrap();
852        assert_matches!(set_interest_result, Some(Ok(())));
853    }
854
855    #[fuchsia::test]
856    async fn logger_prints_ignores_ambiguity_if_force_set_severity_is_used() {
857        let cmd = LogCommand {
858            sub_command: Some(LogSubCommand::SetSeverity(SetSeverityCommand {
859                no_persist: true,
860                interest_selector: vec![OneOrMany::One(
861                    parse_log_interest_selector("ambiguous_selector#INFO").unwrap(),
862                )],
863                force: true,
864            })),
865            ..LogCommand::default()
866        };
867        let getter = FakeInstanceGetter {
868            expected_selector: Some("ambiguous_selector".into()),
869            output: vec![
870                Moniker::try_from("core/some/ambiguous_selector:thing/test").unwrap(),
871                Moniker::try_from("core/other/ambiguous_selector:thing/test").unwrap(),
872            ],
873        };
874        let mut set_interest_result = None;
875        let mut scheduler = FuturesUnordered::new();
876        let (settings_proxy, settings_server) = create_proxy::<LogSettingsMarker>();
877        scheduler.push(Either::Left(async {
878            set_interest_result = Some(cmd.maybe_set_interest(&settings_proxy, &getter).await);
879            drop(settings_proxy);
880        }));
881        scheduler.push(Either::Right(async {
882            let request = settings_server.into_stream().next().await;
883            let (selectors, responder) = assert_matches!(
884                request,
885                Some(Ok(LogSettingsRequest::SetInterest { selectors, responder })) =>
886                (selectors, responder)
887            );
888            responder.send().unwrap();
889            assert_eq!(
890                selectors,
891                vec![parse_log_interest_selector("ambiguous_selector#INFO").unwrap()]
892            );
893        }));
894        while scheduler.next().await.is_some() {}
895        drop(scheduler);
896        assert_matches!(set_interest_result, Some(Ok(())));
897    }
898
899    #[fuchsia::test]
900    async fn logger_prints_ignores_ambiguity_if_force_set_severity_is_used_persistent() {
901        let cmd = LogCommand {
902            sub_command: Some(LogSubCommand::SetSeverity(SetSeverityCommand {
903                no_persist: false,
904                interest_selector: vec![log_socket_stream::OneOrMany::One(
905                    parse_log_interest_selector("ambiguous_selector#INFO").unwrap(),
906                )],
907                force: true,
908            })),
909            ..LogCommand::default()
910        };
911        let getter = FakeInstanceGetter {
912            expected_selector: Some("ambiguous_selector".into()),
913            output: vec![
914                Moniker::try_from("core/some/ambiguous_selector:thing/test").unwrap(),
915                Moniker::try_from("core/other/ambiguous_selector:thing/test").unwrap(),
916            ],
917        };
918        let mut set_interest_result = None;
919        let mut scheduler = FuturesUnordered::new();
920        let (settings_proxy, settings_server) = create_proxy::<LogSettingsMarker>();
921        scheduler.push(Either::Left(async {
922            set_interest_result = Some(cmd.maybe_set_interest(&settings_proxy, &getter).await);
923            drop(settings_proxy);
924        }));
925        scheduler.push(Either::Right(async {
926            let request = settings_server.into_stream().next().await;
927            let (payload, responder) = assert_matches!(
928                request,
929                Some(Ok(LogSettingsRequest::SetComponentInterest { payload, responder })) =>
930                (payload, responder)
931            );
932            assert_eq!(payload.persist, Some(true));
933            responder.send().unwrap();
934            assert_eq!(
935                payload.selectors.unwrap(),
936                vec![parse_log_interest_selector("ambiguous_selector#INFO").unwrap()]
937            );
938        }));
939        while scheduler.next().await.is_some() {}
940        drop(scheduler);
941        assert_matches!(set_interest_result, Some(Ok(())));
942    }
943
944    #[fuchsia::test]
945    async fn logger_prints_ignores_ambiguity_if_machine_output_is_used() {
946        let cmd = LogCommand {
947            sub_command: Some(LogSubCommand::Dump(DumpCommand {})),
948            set_severity: vec![OneOrMany::One(
949                parse_log_interest_selector("ambiguous_selector#INFO").unwrap(),
950            )],
951            force_set_severity: true,
952            ..LogCommand::default()
953        };
954        let getter = FakeInstanceGetter {
955            expected_selector: Some("ambiguous_selector".into()),
956            output: vec![
957                Moniker::try_from("core/some/collection:thing/test").unwrap(),
958                Moniker::try_from("core/other/collection:thing/test").unwrap(),
959            ],
960        };
961        let mut set_interest_result = None;
962        let mut scheduler = FuturesUnordered::new();
963        let (settings_proxy, settings_server) = create_proxy::<LogSettingsMarker>();
964        scheduler.push(Either::Left(async {
965            set_interest_result = Some(cmd.maybe_set_interest(&settings_proxy, &getter).await);
966            drop(settings_proxy);
967        }));
968        scheduler.push(Either::Right(async {
969            let request = settings_server.into_stream().next().await;
970            let (selectors, responder) = assert_matches!(
971                request,
972                Some(Ok(LogSettingsRequest::SetInterest { selectors, responder })) =>
973                (selectors, responder)
974            );
975            responder.send().unwrap();
976            assert_eq!(
977                selectors,
978                vec![parse_log_interest_selector("ambiguous_selector#INFO").unwrap()]
979            );
980        }));
981        while scheduler.next().await.is_some() {}
982        drop(scheduler);
983        assert_matches!(set_interest_result, Some(Ok(())));
984    }
985    #[test]
986    fn test_parse_selector() {
987        assert_eq!(
988            log_interest_selector("core/audio#DEBUG").unwrap(),
989            OneOrMany::One(parse_log_interest_selector("core/audio#DEBUG").unwrap())
990        );
991    }
992
993    #[test]
994    fn test_parse_selector_with_commas() {
995        assert_eq!(
996            log_interest_selector("core/audio#DEBUG,bootstrap/archivist#TRACE").unwrap(),
997            OneOrMany::Many(vec![
998                parse_log_interest_selector("core/audio#DEBUG").unwrap(),
999                parse_log_interest_selector("bootstrap/archivist#TRACE").unwrap()
1000            ])
1001        );
1002    }
1003
1004    #[test]
1005    fn test_parse_time() {
1006        assert!(parse_time("now").unwrap().is_now);
1007        let date_string = "04/20/2020";
1008        let res = parse_time(date_string).unwrap();
1009        assert!(!res.is_now);
1010        assert_eq!(
1011            res.date_naive(),
1012            parse_date_string(date_string, Local::now(), Dialect::Us).unwrap().date_naive()
1013        );
1014    }
1015}