1use chrono::{Local, TimeZone, Utc};
11use diagnostics_hierarchy::HierarchyMatcher;
12use fidl_fuchsia_diagnostics::{DataType, Selector};
13use fidl_fuchsia_inspect as finspect;
14use flyweights::FlyStr;
15use itertools::Itertools;
16use moniker::EXTENDED_MONIKER_COMPONENT_MANAGER_STR;
17use selectors::SelectorExt;
18use serde::de::{DeserializeOwned, Deserializer};
19use serde::{Deserialize, Serialize, Serializer};
20use std::borrow::{Borrow, Cow};
21use std::cmp::Ordering;
22use std::fmt;
23use std::hash::Hash;
24use std::ops::Deref;
25use std::str::FromStr;
26use std::sync::LazyLock;
27use std::time::Duration;
28use termion::{color, style};
29use thiserror::Error;
30
31pub use diagnostics_hierarchy::{DiagnosticsHierarchy, Property, hierarchy};
32pub use diagnostics_log_types_serde::Severity;
33pub use moniker::ExtendedMoniker;
34
35#[cfg(target_os = "fuchsia")]
36#[doc(hidden)]
37pub mod logs_legacy;
38
39#[cfg(feature = "json_schema")]
40use schemars::JsonSchema;
41
42const SCHEMA_VERSION: u64 = 1;
43const MICROS_IN_SEC: u128 = 1000000;
44const ROOT_MONIKER_REPR: &str = "<root>";
45
46static DEFAULT_TREE_NAME: LazyLock<FlyStr> =
47 LazyLock::new(|| FlyStr::new(finspect::DEFAULT_TREE_NAME));
48
49#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Hash, Eq)]
52#[serde(rename_all = "lowercase")]
53pub enum InspectHandleName {
54 Name(FlyStr),
57
58 Filename(FlyStr),
61}
62
63impl std::fmt::Display for InspectHandleName {
64 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65 write!(f, "{}", self.as_ref())
66 }
67}
68
69impl InspectHandleName {
70 pub fn name(n: impl Into<FlyStr>) -> Self {
72 Self::Name(n.into())
73 }
74
75 pub fn filename(n: impl Into<FlyStr>) -> Self {
77 Self::Filename(n.into())
78 }
79
80 pub fn as_name(&self) -> Option<&str> {
82 if let Self::Name(n) = self { Some(n.as_str()) } else { None }
83 }
84
85 pub fn as_filename(&self) -> Option<&str> {
87 if let Self::Filename(f) = self { Some(f.as_str()) } else { None }
88 }
89}
90
91impl AsRef<str> for InspectHandleName {
92 fn as_ref(&self) -> &str {
93 match self {
94 Self::Filename(f) => f.as_str(),
95 Self::Name(n) => n.as_str(),
96 }
97 }
98}
99
100#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
102#[derive(Default, Deserialize, Serialize, Clone, Debug, PartialEq, Eq)]
103pub enum DataSource {
104 #[default]
105 Unknown,
106 Inspect,
107 Logs,
108}
109
110pub trait MetadataError {
111 fn dropped_payload() -> Self;
112 fn message(&self) -> Option<&str>;
113}
114
115pub trait Metadata: DeserializeOwned + Serialize + Clone + Send {
116 type Error: Clone + MetadataError;
118
119 fn timestamp(&self) -> Timestamp;
121
122 fn set_timestamp(&mut self, timestamp: Timestamp);
124
125 fn errors(&self) -> Option<&[Self::Error]>;
127
128 fn set_errors(&mut self, errors: Vec<Self::Error>);
130
131 fn has_errors(&self) -> bool {
133 self.errors().map(|e| !e.is_empty()).unwrap_or_default()
134 }
135
136 fn merge(&mut self, other: Self) {
139 if self.timestamp() < other.timestamp() {
140 self.set_timestamp(other.timestamp());
141 }
142
143 if let Some(more) = other.errors() {
144 let mut errs = Vec::from(self.errors().unwrap_or_default());
145 errs.extend_from_slice(more);
146 self.set_errors(errs);
147 }
148 }
149}
150
151pub trait DiagnosticsData {
153 type Metadata: Metadata;
155
156 type Key: AsRef<str> + Clone + DeserializeOwned + Eq + FromStr + Hash + Send + 'static;
158
159 const DATA_TYPE: DataType;
161}
162
163#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
165pub struct Inspect;
166
167impl DiagnosticsData for Inspect {
168 type Metadata = InspectMetadata;
169 type Key = String;
170 const DATA_TYPE: DataType = DataType::Inspect;
171}
172
173impl Metadata for InspectMetadata {
174 type Error = InspectError;
175
176 fn timestamp(&self) -> Timestamp {
177 self.timestamp
178 }
179
180 fn set_timestamp(&mut self, timestamp: Timestamp) {
181 self.timestamp = timestamp;
182 }
183
184 fn errors(&self) -> Option<&[Self::Error]> {
185 self.errors.as_deref()
186 }
187
188 fn set_errors(&mut self, errors: Vec<Self::Error>) {
189 self.errors = Some(errors);
190 }
191}
192
193#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
195pub struct Logs;
196
197impl DiagnosticsData for Logs {
198 type Metadata = LogsMetadata;
199 type Key = LogsField;
200 const DATA_TYPE: DataType = DataType::Logs;
201}
202
203impl Metadata for LogsMetadata {
204 type Error = LogError;
205
206 fn timestamp(&self) -> Timestamp {
207 self.timestamp
208 }
209
210 fn set_timestamp(&mut self, timestamp: Timestamp) {
211 self.timestamp = timestamp;
212 }
213
214 fn errors(&self) -> Option<&[Self::Error]> {
215 self.errors.as_deref()
216 }
217
218 fn set_errors(&mut self, errors: Vec<Self::Error>) {
219 self.errors = Some(errors);
220 }
221}
222
223pub fn serialize_timestamp<S>(timestamp: &Timestamp, serializer: S) -> Result<S::Ok, S::Error>
224where
225 S: Serializer,
226{
227 serializer.serialize_i64(timestamp.into_nanos())
228}
229
230pub fn deserialize_timestamp<'de, D>(deserializer: D) -> Result<Timestamp, D::Error>
231where
232 D: Deserializer<'de>,
233{
234 let nanos = i64::deserialize(deserializer)?;
235 Ok(Timestamp::from_nanos(nanos))
236}
237
238#[cfg(target_os = "fuchsia")]
239mod zircon {
240 pub type Timestamp = zx::BootInstant;
241
242 pub fn unapply_mono_to_boot_offset(timestamp: Timestamp) -> zx::MonotonicInstant {
247 let mono_now = zx::MonotonicInstant::get();
248 let boot_now = zx::BootInstant::get();
249
250 let mono_to_boot_offset_nanos = boot_now.into_nanos() - mono_now.into_nanos();
251 zx::MonotonicInstant::from_nanos(timestamp.into_nanos() - mono_to_boot_offset_nanos)
252 }
253}
254
255#[cfg(target_os = "fuchsia")]
256pub use zircon::Timestamp;
257#[cfg(target_os = "fuchsia")]
258pub use zircon::unapply_mono_to_boot_offset;
259
260#[cfg(not(target_os = "fuchsia"))]
261mod host {
262 use serde::{Deserialize, Serialize};
263 use std::fmt;
264 use std::ops::Add;
265 use std::time::Duration;
266
267 #[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
268 pub struct Timestamp(i64);
269
270 impl Timestamp {
271 pub fn into_nanos(self) -> i64 {
273 self.0
274 }
275
276 pub fn from_nanos(nanos: i64) -> Self {
278 Self(nanos)
279 }
280 }
281
282 impl Add<Duration> for Timestamp {
283 type Output = Timestamp;
284 fn add(self, rhs: Duration) -> Self::Output {
285 Timestamp(self.0 + rhs.as_nanos() as i64)
286 }
287 }
288
289 impl fmt::Display for Timestamp {
290 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
291 write!(f, "{}", self.0)
292 }
293 }
294}
295
296#[cfg(not(target_os = "fuchsia"))]
297pub use host::Timestamp;
298
299#[cfg(feature = "json_schema")]
300impl JsonSchema for Timestamp {
301 fn schema_name() -> String {
302 "integer".to_owned()
303 }
304
305 fn json_schema(generator: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
306 i64::json_schema(generator)
307 }
308}
309
310#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
313pub struct InspectMetadata {
314 #[serde(skip_serializing_if = "Option::is_none")]
316 pub errors: Option<Vec<InspectError>>,
317
318 #[serde(flatten)]
320 pub name: InspectHandleName,
321
322 pub component_url: FlyStr,
324
325 #[serde(serialize_with = "serialize_timestamp", deserialize_with = "deserialize_timestamp")]
327 pub timestamp: Timestamp,
328
329 #[serde(skip_serializing_if = "std::ops::Not::not")]
332 #[serde(default)]
333 pub escrowed: bool,
334}
335
336impl InspectMetadata {
337 pub fn component_url(&self) -> &str {
340 self.component_url.as_str()
341 }
342}
343
344#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
347#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
348pub struct LogsMetadata {
349 #[serde(skip_serializing_if = "Option::is_none")]
352 pub errors: Option<Vec<LogError>>,
353
354 #[serde(skip_serializing_if = "Option::is_none")]
356 pub component_url: Option<FlyStr>,
357
358 #[serde(serialize_with = "serialize_timestamp", deserialize_with = "deserialize_timestamp")]
360 pub timestamp: Timestamp,
361
362 #[serde(
366 serialize_with = "diagnostics_log_types_serde::severity::serialize",
367 deserialize_with = "diagnostics_log_types_serde::severity::deserialize"
368 )]
369 pub severity: Severity,
370
371 #[serde(skip_serializing_if = "Option::is_none")]
374 raw_severity: Option<u8>,
375
376 #[serde(skip_serializing_if = "Option::is_none")]
378 pub tags: Option<Vec<String>>,
379
380 #[serde(skip_serializing_if = "Option::is_none")]
382 pub pid: Option<u64>,
383
384 #[serde(skip_serializing_if = "Option::is_none")]
386 pub tid: Option<u64>,
387
388 #[serde(skip_serializing_if = "Option::is_none")]
390 pub file: Option<String>,
391
392 #[serde(skip_serializing_if = "Option::is_none")]
394 pub line: Option<u64>,
395
396 #[serde(skip)]
400 dropped: Option<u64>,
401
402 #[serde(skip)]
406 size_bytes: Option<usize>,
407}
408
409impl LogsMetadata {
410 pub fn component_url(&self) -> Option<&str> {
412 self.component_url.as_ref().map(|s| s.as_str())
413 }
414
415 pub fn raw_severity(&self) -> u8 {
417 match self.raw_severity {
418 Some(s) => s,
419 None => self.severity as u8,
420 }
421 }
422}
423
424#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
426pub struct Data<D: DiagnosticsData> {
427 #[serde(default)]
429 pub data_source: DataSource,
431
432 #[serde(bound(
434 deserialize = "D::Metadata: DeserializeOwned",
435 serialize = "D::Metadata: Serialize"
436 ))]
437 pub metadata: D::Metadata,
438
439 #[serde(deserialize_with = "moniker_deserialize", serialize_with = "moniker_serialize")]
441 pub moniker: ExtendedMoniker,
442
443 pub payload: Option<DiagnosticsHierarchy<D::Key>>,
445
446 #[serde(default)]
448 pub version: u64,
449}
450
451fn moniker_deserialize<'de, D>(deserializer: D) -> Result<ExtendedMoniker, D::Error>
452where
453 D: serde::Deserializer<'de>,
454{
455 let moniker_str = String::deserialize(deserializer)?;
456 ExtendedMoniker::parse_str(&moniker_str).map_err(serde::de::Error::custom)
457}
458
459fn moniker_serialize<S>(moniker: &ExtendedMoniker, s: S) -> Result<S::Ok, S::Error>
460where
461 S: Serializer,
462{
463 s.collect_str(moniker)
464}
465
466impl<D> Data<D>
467where
468 D: DiagnosticsData,
469{
470 pub fn drop_payload(&mut self) {
472 self.metadata.set_errors(vec![
473 <<D as DiagnosticsData>::Metadata as Metadata>::Error::dropped_payload(),
474 ]);
475 self.payload = None;
476 }
477
478 pub fn sort_payload(&mut self) {
480 if let Some(payload) = &mut self.payload {
481 payload.sort();
482 }
483 }
484
485 pub fn merge(&mut self, other: Self) {
487 let Data { data_source, metadata, moniker, payload, version } = other;
488
489 if self.data_source != data_source || self.moniker != moniker || self.version != version {
490 return;
492 }
493
494 self.metadata.merge(metadata);
495
496 match (&mut self.payload, payload) {
497 (Some(existing), Some(more)) => {
498 existing.merge(more);
499 }
500 (None, Some(payload)) => {
501 self.payload = Some(payload);
502 }
503 _ => {}
504 }
505 }
506
507 pub fn filter<'a>(
510 mut self,
511 selectors: impl IntoIterator<Item = &'a Selector>,
512 ) -> Result<Option<Self>, Error> {
513 let Some(hierarchy) = self.payload else {
514 return Ok(None);
515 };
516 let matching_selectors =
517 match self.moniker.match_against_selectors(selectors).collect::<Result<Vec<_>, _>>() {
518 Ok(selectors) if selectors.is_empty() => return Ok(None),
519 Ok(selectors) => selectors,
520 Err(e) => {
521 return Err(Error::Internal(e));
522 }
523 };
524
525 let matcher: HierarchyMatcher =
527 matching_selectors.try_into().map_err(|e| Error::Internal(anyhow::Error::from(e)))?;
528
529 self.payload = match diagnostics_hierarchy::filter_hierarchy(hierarchy, &matcher) {
530 Some(hierarchy) => Some(hierarchy),
531 None => return Ok(None),
532 };
533 Ok(Some(self))
534 }
535}
536
537#[derive(Debug, Error)]
539pub enum Error {
540 #[error(transparent)]
541 Internal(#[from] anyhow::Error),
542}
543
544pub type InspectData = Data<Inspect>;
546
547pub type LogsData = Data<Logs>;
549
550pub type LogsHierarchy = DiagnosticsHierarchy<LogsField>;
552
553pub type LogsProperty = Property<LogsField>;
555
556impl Data<Inspect> {
557 pub fn name(&self) -> &str {
559 self.metadata.name.as_ref()
560 }
561}
562
563pub struct InspectDataBuilder {
564 data: Data<Inspect>,
565}
566
567impl InspectDataBuilder {
568 pub fn new(
569 moniker: ExtendedMoniker,
570 component_url: impl Into<FlyStr>,
571 timestamp: impl Into<Timestamp>,
572 ) -> Self {
573 Self {
574 data: Data {
575 data_source: DataSource::Inspect,
576 moniker,
577 payload: None,
578 version: 1,
579 metadata: InspectMetadata {
580 errors: None,
581 name: InspectHandleName::name(DEFAULT_TREE_NAME.clone()),
582 component_url: component_url.into(),
583 timestamp: timestamp.into(),
584 escrowed: false,
585 },
586 },
587 }
588 }
589
590 pub fn escrowed(mut self, escrowed: bool) -> Self {
591 self.data.metadata.escrowed = escrowed;
592 self
593 }
594
595 pub fn with_hierarchy(
596 mut self,
597 hierarchy: DiagnosticsHierarchy<<Inspect as DiagnosticsData>::Key>,
598 ) -> Self {
599 self.data.payload = Some(hierarchy);
600 self
601 }
602
603 pub fn with_errors(mut self, errors: Vec<InspectError>) -> Self {
604 self.data.metadata.errors = Some(errors);
605 self
606 }
607
608 pub fn with_name(mut self, name: InspectHandleName) -> Self {
609 self.data.metadata.name = name;
610 self
611 }
612
613 pub fn build(self) -> Data<Inspect> {
614 self.data
615 }
616}
617
618pub struct LogsDataBuilder {
621 errors: Vec<LogError>,
623 msg: Option<String>,
625 tags: Vec<String>,
627 pid: Option<u64>,
629 tid: Option<u64>,
631 file: Option<String>,
633 line: Option<u64>,
635 args: BuilderArgs,
637 keys: Vec<Property<LogsField>>,
639 raw_severity: Option<u8>,
641}
642
643pub struct BuilderArgs {
645 pub moniker: ExtendedMoniker,
647 pub timestamp: Timestamp,
649 pub component_url: Option<FlyStr>,
651 pub severity: Severity,
653}
654
655impl LogsDataBuilder {
656 pub fn new(args: BuilderArgs) -> Self {
658 LogsDataBuilder {
659 args,
660 errors: vec![],
661 msg: None,
662 file: None,
663 line: None,
664 pid: None,
665 tags: vec![],
666 tid: None,
667 keys: vec![],
668 raw_severity: None,
669 }
670 }
671
672 #[must_use = "You must call build on your builder to consume its result"]
674 pub fn set_moniker(mut self, value: ExtendedMoniker) -> Self {
675 self.args.moniker = value;
676 self
677 }
678
679 #[must_use = "You must call build on your builder to consume its result"]
681 pub fn set_url(mut self, value: Option<FlyStr>) -> Self {
682 self.args.component_url = value;
683 self
684 }
685
686 #[must_use = "You must call build on your builder to consume its result"]
691 pub fn set_dropped(mut self, value: u64) -> Self {
692 if value == 0 {
693 return self;
694 }
695 let val = self.errors.iter_mut().find_map(|error| {
696 if let LogError::DroppedLogs { count } = error { Some(count) } else { None }
697 });
698 if let Some(v) = val {
699 *v = value;
700 } else {
701 self.errors.push(LogError::DroppedLogs { count: value });
702 }
703 self
704 }
705
706 pub fn set_raw_severity(mut self, severity: u8) -> Self {
708 self.raw_severity = Some(severity);
709 self
710 }
711
712 #[must_use = "You must call build on your builder to consume its result"]
717 pub fn set_rolled_out(mut self, value: u64) -> Self {
718 if value == 0 {
719 return self;
720 }
721 let val = self.errors.iter_mut().find_map(|error| {
722 if let LogError::RolledOutLogs { count } = error { Some(count) } else { None }
723 });
724 if let Some(v) = val {
725 *v = value;
726 } else {
727 self.errors.push(LogError::RolledOutLogs { count: value });
728 }
729 self
730 }
731
732 pub fn set_severity(mut self, severity: Severity) -> Self {
734 self.args.severity = severity;
735 self.raw_severity = None;
736 self
737 }
738
739 #[must_use = "You must call build on your builder to consume its result"]
741 pub fn set_pid(mut self, value: u64) -> Self {
742 self.pid = Some(value);
743 self
744 }
745
746 #[must_use = "You must call build on your builder to consume its result"]
748 pub fn set_tid(mut self, value: u64) -> Self {
749 self.tid = Some(value);
750 self
751 }
752
753 pub fn build(self) -> LogsData {
755 let mut args = vec![];
756 if let Some(msg) = self.msg {
757 args.push(LogsProperty::String(LogsField::MsgStructured, msg));
758 }
759 let mut payload_fields = vec![DiagnosticsHierarchy::new("message", args, vec![])];
760 if !self.keys.is_empty() {
761 let val = DiagnosticsHierarchy::new("keys", self.keys, vec![]);
762 payload_fields.push(val);
763 }
764 let mut payload = LogsHierarchy::new("root", vec![], payload_fields);
765 payload.sort();
766 let (raw_severity, severity) =
767 self.raw_severity.map(Severity::parse_exact).unwrap_or((None, self.args.severity));
768 let mut ret = LogsData::for_logs(
769 self.args.moniker,
770 Some(payload),
771 self.args.timestamp,
772 self.args.component_url,
773 severity,
774 self.errors,
775 );
776 ret.metadata.raw_severity = raw_severity;
777 ret.metadata.file = self.file;
778 ret.metadata.line = self.line;
779 ret.metadata.pid = self.pid;
780 ret.metadata.tid = self.tid;
781 ret.metadata.tags = Some(self.tags);
782 ret
783 }
784
785 #[must_use = "You must call build on your builder to consume its result"]
787 pub fn add_error(mut self, error: LogError) -> Self {
788 self.errors.push(error);
789 self
790 }
791
792 #[must_use = "You must call build on your builder to consume its result"]
794 pub fn set_message(mut self, msg: impl Into<String>) -> Self {
795 self.msg = Some(msg.into());
796 self
797 }
798
799 #[must_use = "You must call build on your builder to consume its result"]
801 pub fn set_file(mut self, file: impl Into<String>) -> Self {
802 self.file = Some(file.into());
803 self
804 }
805
806 #[must_use = "You must call build on your builder to consume its result"]
808 pub fn set_line(mut self, line: u64) -> Self {
809 self.line = Some(line);
810 self
811 }
812
813 #[must_use = "You must call build on your builder to consume its result"]
815 pub fn add_key(mut self, kvp: Property<LogsField>) -> Self {
816 self.keys.push(kvp);
817 self
818 }
819
820 #[must_use = "You must call build on your builder to consume its result"]
822 pub fn add_tag(mut self, tag: impl Into<String>) -> Self {
823 self.tags.push(tag.into());
824 self
825 }
826}
827
828impl Data<Logs> {
829 pub fn for_logs(
831 moniker: ExtendedMoniker,
832 payload: Option<LogsHierarchy>,
833 timestamp: impl Into<Timestamp>,
834 component_url: Option<FlyStr>,
835 severity: impl Into<Severity>,
836 errors: Vec<LogError>,
837 ) -> Self {
838 let errors = if errors.is_empty() { None } else { Some(errors) };
839
840 Data {
841 moniker,
842 version: SCHEMA_VERSION,
843 data_source: DataSource::Logs,
844 payload,
845 metadata: LogsMetadata {
846 timestamp: timestamp.into(),
847 component_url,
848 severity: severity.into(),
849 raw_severity: None,
850 errors,
851 file: None,
852 line: None,
853 pid: None,
854 tags: None,
855 tid: None,
856 dropped: None,
857 size_bytes: None,
858 },
859 }
860 }
861
862 pub fn set_raw_severity(&mut self, raw_severity: u8) {
865 self.metadata.raw_severity = Some(raw_severity);
866 self.metadata.severity = Severity::from(raw_severity);
867 }
868
869 pub fn set_severity(&mut self, severity: Severity) {
871 self.metadata.severity = severity;
872 self.metadata.raw_severity = None;
873 }
874
875 pub fn msg(&self) -> Option<&str> {
877 self.payload_message().as_ref().and_then(|p| {
878 p.properties.iter().find_map(|property| match property {
879 LogsProperty::String(LogsField::MsgStructured, msg) => Some(msg.as_str()),
880 _ => None,
881 })
882 })
883 }
884
885 pub fn msg_mut(&mut self) -> Option<&mut String> {
887 self.payload_message_mut().and_then(|p| {
888 p.properties.iter_mut().find_map(|property| match property {
889 LogsProperty::String(LogsField::MsgStructured, msg) => Some(msg),
890 _ => None,
891 })
892 })
893 }
894
895 pub fn payload_message(&self) -> Option<&DiagnosticsHierarchy<LogsField>> {
897 self.payload
898 .as_ref()
899 .and_then(|p| p.children.iter().find(|property| property.name.as_str() == "message"))
900 }
901
902 pub fn payload_keys(&self) -> Option<&DiagnosticsHierarchy<LogsField>> {
904 self.payload
905 .as_ref()
906 .and_then(|p| p.children.iter().find(|property| property.name.as_str() == "keys"))
907 }
908
909 pub fn metadata(&self) -> &LogsMetadata {
910 &self.metadata
911 }
912
913 pub fn payload_keys_strings(&self) -> Box<dyn Iterator<Item = String> + Send + '_> {
915 let maybe_iter = self.payload_keys().map(|p| {
916 Box::new(p.properties.iter().filter_map(|property| match property {
917 LogsProperty::String(LogsField::Tag, _tag) => None,
918 LogsProperty::String(LogsField::ProcessId, _tag) => None,
919 LogsProperty::String(LogsField::ThreadId, _tag) => None,
920 LogsProperty::String(LogsField::Dropped, _tag) => None,
921 LogsProperty::String(LogsField::Msg, _tag) => None,
922 LogsProperty::String(LogsField::FilePath, _tag) => None,
923 LogsProperty::String(LogsField::LineNumber, _tag) => None,
924 LogsProperty::String(
925 key @ (LogsField::Other(_) | LogsField::MsgStructured),
926 value,
927 ) => Some(format!("{key}={value}")),
928 LogsProperty::Bytes(key @ (LogsField::Other(_) | LogsField::MsgStructured), _) => {
929 Some(format!("{key} = <bytes>"))
930 }
931 LogsProperty::Int(
932 key @ (LogsField::Other(_) | LogsField::MsgStructured),
933 value,
934 ) => Some(format!("{key}={value}")),
935 LogsProperty::Uint(
936 key @ (LogsField::Other(_) | LogsField::MsgStructured),
937 value,
938 ) => Some(format!("{key}={value}")),
939 LogsProperty::Double(
940 key @ (LogsField::Other(_) | LogsField::MsgStructured),
941 value,
942 ) => Some(format!("{key}={value}")),
943 LogsProperty::Bool(
944 key @ (LogsField::Other(_) | LogsField::MsgStructured),
945 value,
946 ) => Some(format!("{key}={value}")),
947 LogsProperty::DoubleArray(
948 key @ (LogsField::Other(_) | LogsField::MsgStructured),
949 value,
950 ) => Some(format!("{key}={value:?}")),
951 LogsProperty::IntArray(
952 key @ (LogsField::Other(_) | LogsField::MsgStructured),
953 value,
954 ) => Some(format!("{key}={value:?}")),
955 LogsProperty::UintArray(
956 key @ (LogsField::Other(_) | LogsField::MsgStructured),
957 value,
958 ) => Some(format!("{key}={value:?}")),
959 LogsProperty::StringList(
960 key @ (LogsField::Other(_) | LogsField::MsgStructured),
961 value,
962 ) => Some(format!("{key}={value:?}")),
963 _ => None,
964 }))
965 });
966 match maybe_iter {
967 Some(i) => Box::new(i),
968 None => Box::new(std::iter::empty()),
969 }
970 }
971
972 pub fn payload_message_mut(&mut self) -> Option<&mut DiagnosticsHierarchy<LogsField>> {
974 self.payload.as_mut().and_then(|p| {
975 p.children.iter_mut().find(|property| property.name.as_str() == "message")
976 })
977 }
978
979 pub fn file_path(&self) -> Option<&str> {
981 self.metadata.file.as_deref()
982 }
983
984 pub fn line_number(&self) -> Option<&u64> {
986 self.metadata.line.as_ref()
987 }
988
989 pub fn pid(&self) -> Option<u64> {
991 self.metadata.pid
992 }
993
994 pub fn tid(&self) -> Option<u64> {
996 self.metadata.tid
997 }
998
999 pub fn tags(&self) -> Option<&Vec<String>> {
1001 self.metadata.tags.as_ref()
1002 }
1003
1004 pub fn severity(&self) -> Severity {
1006 self.metadata.severity
1007 }
1008
1009 pub fn dropped_logs(&self) -> Option<u64> {
1011 self.metadata.errors.as_ref().and_then(|errors| {
1012 errors.iter().find_map(|e| match e {
1013 LogError::DroppedLogs { count } => Some(*count),
1014 _ => None,
1015 })
1016 })
1017 }
1018
1019 pub fn rolled_out_logs(&self) -> Option<u64> {
1021 self.metadata.errors.as_ref().and_then(|errors| {
1022 errors.iter().find_map(|e| match e {
1023 LogError::RolledOutLogs { count } => Some(*count),
1024 _ => None,
1025 })
1026 })
1027 }
1028
1029 pub fn component_name_by_url(&self) -> Cow<'_, str> {
1030 if let Some(url_str) = &self.metadata.component_url {
1031 let last_part = url_str.rsplit('/').next().unwrap_or(url_str);
1032 return Cow::Owned(last_part.to_string());
1033 }
1034 self.component_name()
1036 }
1037
1038 pub fn component_name(&self) -> Cow<'_, str> {
1040 match &self.moniker {
1041 ExtendedMoniker::ComponentManager => {
1042 Cow::Borrowed(EXTENDED_MONIKER_COMPONENT_MANAGER_STR)
1043 }
1044 ExtendedMoniker::ComponentInstance(moniker) => {
1045 if moniker.is_root() {
1046 Cow::Borrowed(ROOT_MONIKER_REPR)
1047 } else {
1048 Cow::Owned(moniker.leaf().unwrap().to_string())
1049 }
1050 }
1051 }
1052 }
1053}
1054
1055#[derive(Clone, Copy, Debug)]
1057pub struct LogTextDisplayOptions {
1058 pub show_moniker: bool,
1060
1061 pub show_full_moniker: bool,
1063
1064 pub prefer_url_component_name: bool,
1066
1067 pub show_metadata: bool,
1069
1070 pub show_tags: bool,
1072
1073 pub show_file: bool,
1075
1076 pub color: LogTextColor,
1078
1079 pub time_format: LogTimeDisplayFormat,
1081}
1082
1083impl Default for LogTextDisplayOptions {
1084 fn default() -> Self {
1085 Self {
1086 show_moniker: true,
1087 show_full_moniker: true,
1088 prefer_url_component_name: false,
1089 show_metadata: true,
1090 show_tags: true,
1091 show_file: true,
1092 color: Default::default(),
1093 time_format: Default::default(),
1094 }
1095 }
1096}
1097
1098#[derive(Clone, Copy, Debug, Default)]
1100pub enum LogTextColor {
1101 #[default]
1103 None,
1104
1105 BySeverity,
1107
1108 Highlight,
1110}
1111
1112impl LogTextColor {
1113 fn begin_record(&self, f: &mut fmt::Formatter<'_>, severity: Severity) -> fmt::Result {
1114 match self {
1115 LogTextColor::BySeverity => match severity {
1116 Severity::Fatal => {
1117 write!(f, "{}{}", color::Bg(color::Red), color::Fg(color::White))?
1118 }
1119 Severity::Error => write!(f, "{}", color::Fg(color::Red))?,
1120 Severity::Warn => write!(f, "{}", color::Fg(color::Yellow))?,
1121 Severity::Info => (),
1122 Severity::Debug => write!(f, "{}", color::Fg(color::LightBlue))?,
1123 Severity::Trace => write!(f, "{}", color::Fg(color::LightMagenta))?,
1124 },
1125 LogTextColor::Highlight => write!(f, "{}", color::Fg(color::LightYellow))?,
1126 LogTextColor::None => {}
1127 }
1128 Ok(())
1129 }
1130
1131 fn begin_lost_message_counts(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1132 if let LogTextColor::BySeverity = self {
1133 write!(f, "{}", color::Fg(color::Yellow))?;
1135 }
1136 Ok(())
1137 }
1138
1139 fn end_record(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1140 match self {
1141 LogTextColor::BySeverity | LogTextColor::Highlight => write!(f, "{}", style::Reset)?,
1142 LogTextColor::None => {}
1143 };
1144 Ok(())
1145 }
1146}
1147
1148#[derive(Clone, Copy, Debug, PartialEq)]
1150pub enum Timezone {
1151 Local,
1153
1154 Utc,
1156}
1157
1158impl Timezone {
1159 fn format(&self, seconds: i64, rem_nanos: u32) -> impl std::fmt::Display {
1160 const TIMESTAMP_FORMAT: &str = "%Y-%m-%d %H:%M:%S.%3f";
1161 match self {
1162 Timezone::Local => {
1163 Local.timestamp_opt(seconds, rem_nanos).unwrap().format(TIMESTAMP_FORMAT)
1164 }
1165 Timezone::Utc => {
1166 Utc.timestamp_opt(seconds, rem_nanos).unwrap().format(TIMESTAMP_FORMAT)
1167 }
1168 }
1169 }
1170}
1171
1172#[derive(Clone, Copy, Debug, Default)]
1174pub enum LogTimeDisplayFormat {
1175 #[default]
1177 Original,
1178
1179 WallTime {
1181 tz: Timezone,
1183
1184 offset: i64,
1187 },
1188}
1189
1190impl LogTimeDisplayFormat {
1191 fn write_timestamp(&self, f: &mut fmt::Formatter<'_>, time: Timestamp) -> fmt::Result {
1192 const NANOS_IN_SECOND: i64 = 1_000_000_000;
1193
1194 match self {
1195 Self::Original | Self::WallTime { offset: 0, .. } => {
1198 let time: Duration =
1199 Duration::from_nanos(time.into_nanos().try_into().unwrap_or(0));
1200 write!(f, "[{:05}.{:06}]", time.as_secs(), time.as_micros() % MICROS_IN_SEC)?;
1201 }
1202 Self::WallTime { tz, offset } => {
1203 let adjusted = time.into_nanos() + offset;
1204 let seconds = adjusted / NANOS_IN_SECOND;
1205 let rem_nanos = (adjusted % NANOS_IN_SECOND) as u32;
1206 let formatted = tz.format(seconds, rem_nanos);
1207 write!(f, "[{formatted}]")?;
1208 }
1209 }
1210 Ok(())
1211 }
1212}
1213
1214pub struct LogTextPresenter<'a> {
1216 log: &'a Data<Logs>,
1218
1219 options: LogTextDisplayOptions,
1221}
1222
1223impl<'a> LogTextPresenter<'a> {
1224 pub fn new(log: &'a Data<Logs>, options: LogTextDisplayOptions) -> Self {
1228 Self { log, options }
1229 }
1230}
1231
1232impl fmt::Display for Data<Logs> {
1233 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1234 LogTextPresenter::new(self, Default::default()).fmt(f)
1235 }
1236}
1237
1238impl Deref for LogTextPresenter<'_> {
1239 type Target = Data<Logs>;
1240 fn deref(&self) -> &Self::Target {
1241 self.log
1242 }
1243}
1244
1245impl fmt::Display for LogTextPresenter<'_> {
1246 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1247 self.options.color.begin_record(f, self.log.severity())?;
1248 self.options.time_format.write_timestamp(f, self.metadata.timestamp)?;
1249
1250 if self.options.show_metadata {
1251 match self.pid() {
1252 Some(pid) => write!(f, "[{pid}]")?,
1253 None => write!(f, "[]")?,
1254 }
1255 match self.tid() {
1256 Some(tid) => write!(f, "[{tid}]")?,
1257 None => write!(f, "[]")?,
1258 }
1259 }
1260
1261 let moniker = if self.options.show_full_moniker {
1262 match &self.moniker {
1263 ExtendedMoniker::ComponentManager => {
1264 Cow::Borrowed(EXTENDED_MONIKER_COMPONENT_MANAGER_STR)
1265 }
1266 ExtendedMoniker::ComponentInstance(instance) => {
1267 if instance.is_root() {
1268 Cow::Borrowed(ROOT_MONIKER_REPR)
1269 } else {
1270 Cow::Owned(instance.to_string())
1271 }
1272 }
1273 }
1274 } else {
1275 if self.options.prefer_url_component_name {
1276 self.component_name_by_url()
1277 } else {
1278 self.component_name()
1279 }
1280 };
1281 if self.options.show_moniker {
1282 write!(f, "[{moniker}]")?;
1283 }
1284
1285 if self.options.show_tags {
1286 match &self.metadata.tags {
1287 Some(tags) if !tags.is_empty() => {
1288 let mut filtered =
1289 tags.iter().filter(|tag| *tag != moniker.as_ref()).peekable();
1290 if filtered.peek().is_some() {
1291 write!(f, "[{}]", filtered.join(","))?;
1292 }
1293 }
1294 _ => {}
1295 }
1296 }
1297
1298 write!(f, " {}:", self.metadata.severity)?;
1299
1300 if self.options.show_file {
1301 match (&self.metadata.file, &self.metadata.line) {
1302 (Some(file), Some(line)) => write!(f, " [{file}({line})]")?,
1303 (Some(file), None) => write!(f, " [{file}]")?,
1304 _ => (),
1305 }
1306 }
1307
1308 if let Some(mut msg) = self.msg() {
1309 if let Some(nul) = msg.find("\0") {
1310 msg = &msg[0..nul];
1311 }
1312 write!(f, " {msg}")?;
1313 } else {
1314 write!(f, " <missing message>")?;
1315 }
1316 for kvp in self.payload_keys_strings() {
1317 write!(f, " {kvp}")?;
1318 }
1319
1320 let dropped = self.log.dropped_logs().unwrap_or_default();
1321 let rolled = self.log.rolled_out_logs().unwrap_or_default();
1322 if dropped != 0 || rolled != 0 {
1323 self.options.color.begin_lost_message_counts(f)?;
1324 if dropped != 0 {
1325 write!(f, " [dropped={dropped}]")?;
1326 }
1327 if rolled != 0 {
1328 write!(f, " [rolled={rolled}]")?;
1329 }
1330 }
1331
1332 self.options.color.end_record(f)?;
1333
1334 Ok(())
1335 }
1336}
1337
1338impl Eq for Data<Logs> {}
1339
1340impl PartialOrd for Data<Logs> {
1341 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1342 Some(self.cmp(other))
1343 }
1344}
1345
1346impl Ord for Data<Logs> {
1347 fn cmp(&self, other: &Self) -> Ordering {
1348 self.metadata.timestamp.cmp(&other.metadata.timestamp)
1349 }
1350}
1351
1352#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, PartialOrd, Ord, Serialize)]
1359pub enum LogsField {
1360 ProcessId,
1361 ThreadId,
1362 Dropped,
1363 Tag,
1364 Msg,
1365 MsgStructured,
1366 FilePath,
1367 LineNumber,
1368 Other(String),
1369}
1370
1371impl fmt::Display for LogsField {
1372 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1373 match self {
1374 LogsField::ProcessId => write!(f, "pid"),
1375 LogsField::ThreadId => write!(f, "tid"),
1376 LogsField::Dropped => write!(f, "num_dropped"),
1377 LogsField::Tag => write!(f, "tag"),
1378 LogsField::Msg => write!(f, "message"),
1379 LogsField::MsgStructured => write!(f, "value"),
1380 LogsField::FilePath => write!(f, "file_path"),
1381 LogsField::LineNumber => write!(f, "line_number"),
1382 LogsField::Other(name) => write!(f, "{name}"),
1383 }
1384 }
1385}
1386
1387pub const PID_LABEL: &str = "pid";
1391pub const TID_LABEL: &str = "tid";
1393pub const DROPPED_LABEL: &str = "num_dropped";
1395pub const TAG_LABEL: &str = "tag";
1397pub const MESSAGE_LABEL_STRUCTURED: &str = "value";
1399pub const MESSAGE_LABEL: &str = "message";
1401pub const FILE_PATH_LABEL: &str = "file";
1403pub const LINE_NUMBER_LABEL: &str = "line";
1405
1406impl AsRef<str> for LogsField {
1407 fn as_ref(&self) -> &str {
1408 match self {
1409 Self::ProcessId => PID_LABEL,
1410 Self::ThreadId => TID_LABEL,
1411 Self::Dropped => DROPPED_LABEL,
1412 Self::Tag => TAG_LABEL,
1413 Self::Msg => MESSAGE_LABEL,
1414 Self::FilePath => FILE_PATH_LABEL,
1415 Self::LineNumber => LINE_NUMBER_LABEL,
1416 Self::MsgStructured => MESSAGE_LABEL_STRUCTURED,
1417 Self::Other(str) => str.as_str(),
1418 }
1419 }
1420}
1421
1422impl<T> From<T> for LogsField
1423where
1424 T: Deref<Target = str>,
1426{
1427 fn from(s: T) -> Self {
1428 match s.as_ref() {
1429 PID_LABEL => Self::ProcessId,
1430 TID_LABEL => Self::ThreadId,
1431 DROPPED_LABEL => Self::Dropped,
1432 TAG_LABEL => Self::Tag,
1433 MESSAGE_LABEL => Self::Msg,
1434 FILE_PATH_LABEL => Self::FilePath,
1435 LINE_NUMBER_LABEL => Self::LineNumber,
1436 MESSAGE_LABEL_STRUCTURED => Self::MsgStructured,
1437 _ => Self::Other(s.to_string()),
1438 }
1439 }
1440}
1441
1442impl FromStr for LogsField {
1443 type Err = ();
1444 fn from_str(s: &str) -> Result<Self, Self::Err> {
1445 Ok(Self::from(s))
1446 }
1447}
1448
1449#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
1452#[derive(Clone, Deserialize, Debug, Eq, PartialEq, Serialize)]
1453pub enum LogError {
1454 #[serde(rename = "dropped_logs")]
1457 DroppedLogs { count: u64 },
1458 #[serde(rename = "rolled_out_logs")]
1461 RolledOutLogs { count: u64 },
1462 #[serde(rename = "parse_record")]
1463 FailedToParseRecord(String),
1464 #[serde(rename = "other")]
1465 Other { message: String },
1466}
1467
1468const DROPPED_PAYLOAD_MSG: &str = "Schema failed to fit component budget.";
1469
1470impl MetadataError for LogError {
1471 fn dropped_payload() -> Self {
1472 Self::Other { message: DROPPED_PAYLOAD_MSG.into() }
1473 }
1474
1475 fn message(&self) -> Option<&str> {
1476 match self {
1477 Self::FailedToParseRecord(msg) => Some(msg.as_str()),
1478 Self::Other { message } => Some(message.as_str()),
1479 _ => None,
1480 }
1481 }
1482}
1483
1484#[derive(Debug, PartialEq, Clone, Eq)]
1487pub struct InspectError {
1488 pub message: String,
1489}
1490
1491impl MetadataError for InspectError {
1492 fn dropped_payload() -> Self {
1493 Self { message: "Schema failed to fit component budget.".into() }
1494 }
1495
1496 fn message(&self) -> Option<&str> {
1497 Some(self.message.as_str())
1498 }
1499}
1500
1501impl fmt::Display for InspectError {
1502 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1503 write!(f, "{}", self.message)
1504 }
1505}
1506
1507impl Borrow<str> for InspectError {
1508 fn borrow(&self) -> &str {
1509 &self.message
1510 }
1511}
1512
1513impl Serialize for InspectError {
1514 fn serialize<S: Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
1515 self.message.serialize(ser)
1516 }
1517}
1518
1519impl<'de> Deserialize<'de> for InspectError {
1520 fn deserialize<D>(de: D) -> Result<Self, D::Error>
1521 where
1522 D: Deserializer<'de>,
1523 {
1524 let message = String::deserialize(de)?;
1525 Ok(Self { message })
1526 }
1527}
1528
1529#[cfg(test)]
1530mod tests {
1531 use super::*;
1532 use diagnostics_hierarchy::hierarchy;
1533 use selectors::FastError;
1534 use serde_json::json;
1535 use test_case::test_case;
1536
1537 const TEST_URL: &str = "fuchsia-pkg://test";
1538
1539 #[fuchsia::test]
1540 fn test_canonical_json_inspect_formatting() {
1541 let mut hierarchy = hierarchy! {
1542 root: {
1543 x: "foo",
1544 }
1545 };
1546
1547 hierarchy.sort();
1548 let json_schema = InspectDataBuilder::new(
1549 "a/b/c/d".try_into().unwrap(),
1550 TEST_URL,
1551 Timestamp::from_nanos(123456i64),
1552 )
1553 .with_hierarchy(hierarchy)
1554 .with_name(InspectHandleName::filename("test_file_plz_ignore.inspect"))
1555 .build();
1556
1557 let result_json =
1558 serde_json::to_value(&json_schema).expect("serialization should succeed.");
1559
1560 let expected_json = json!({
1561 "moniker": "a/b/c/d",
1562 "version": 1,
1563 "data_source": "Inspect",
1564 "payload": {
1565 "root": {
1566 "x": "foo"
1567 }
1568 },
1569 "metadata": {
1570 "component_url": TEST_URL,
1571 "filename": "test_file_plz_ignore.inspect",
1572 "timestamp": 123456,
1573 }
1574 });
1575
1576 pretty_assertions::assert_eq!(result_json, expected_json, "golden diff failed.");
1577 }
1578
1579 #[fuchsia::test]
1580 fn test_errorful_json_inspect_formatting() {
1581 let json_schema = InspectDataBuilder::new(
1582 "a/b/c/d".try_into().unwrap(),
1583 TEST_URL,
1584 Timestamp::from_nanos(123456i64),
1585 )
1586 .with_name(InspectHandleName::filename("test_file_plz_ignore.inspect"))
1587 .with_errors(vec![InspectError { message: "too much fun being had.".to_string() }])
1588 .build();
1589
1590 let result_json =
1591 serde_json::to_value(&json_schema).expect("serialization should succeed.");
1592
1593 let expected_json = json!({
1594 "moniker": "a/b/c/d",
1595 "version": 1,
1596 "data_source": "Inspect",
1597 "payload": null,
1598 "metadata": {
1599 "component_url": TEST_URL,
1600 "errors": ["too much fun being had."],
1601 "filename": "test_file_plz_ignore.inspect",
1602 "timestamp": 123456,
1603 }
1604 });
1605
1606 pretty_assertions::assert_eq!(result_json, expected_json, "golden diff failed.");
1607 }
1608
1609 fn parse_selectors(strings: Vec<&str>) -> Vec<Selector> {
1610 strings
1611 .iter()
1612 .map(|s| match selectors::parse_selector::<FastError>(s) {
1613 Ok(selector) => selector,
1614 Err(e) => panic!("Couldn't parse selector {s}: {e}"),
1615 })
1616 .collect::<Vec<_>>()
1617 }
1618
1619 #[fuchsia::test]
1620 fn test_filter_returns_none_on_empty_hierarchy() {
1621 let data = InspectDataBuilder::new(
1622 "a/b/c/d".try_into().unwrap(),
1623 TEST_URL,
1624 Timestamp::from_nanos(123456i64),
1625 )
1626 .build();
1627 let selectors = parse_selectors(vec!["a/b/c/d:foo"]);
1628 assert_eq!(data.filter(&selectors).expect("Filter OK"), None);
1629 }
1630
1631 #[fuchsia::test]
1632 fn test_filter_returns_none_on_selector_mismatch() {
1633 let mut hierarchy = hierarchy! {
1634 root: {
1635 x: "foo",
1636 }
1637 };
1638 hierarchy.sort();
1639 let data = InspectDataBuilder::new(
1640 "b/c/d/e".try_into().unwrap(),
1641 TEST_URL,
1642 Timestamp::from_nanos(123456i64),
1643 )
1644 .with_hierarchy(hierarchy)
1645 .build();
1646 let selectors = parse_selectors(vec!["a/b/c/d:foo"]);
1647 assert_eq!(data.filter(&selectors).expect("Filter OK"), None);
1648 }
1649
1650 #[fuchsia::test]
1651 fn test_filter_returns_none_on_data_mismatch() {
1652 let mut hierarchy = hierarchy! {
1653 root: {
1654 x: "foo",
1655 }
1656 };
1657 hierarchy.sort();
1658 let data = InspectDataBuilder::new(
1659 "a/b/c/d".try_into().unwrap(),
1660 TEST_URL,
1661 Timestamp::from_nanos(123456i64),
1662 )
1663 .with_hierarchy(hierarchy)
1664 .build();
1665 let selectors = parse_selectors(vec!["a/b/c/d:foo"]);
1666
1667 assert_eq!(data.filter(&selectors).expect("FIlter OK"), None);
1668 }
1669
1670 #[fuchsia::test]
1671 fn test_filter_returns_matching_data() {
1672 let mut hierarchy = hierarchy! {
1673 root: {
1674 x: "foo",
1675 y: "bar",
1676 }
1677 };
1678 hierarchy.sort();
1679 let data = InspectDataBuilder::new(
1680 "a/b/c/d".try_into().unwrap(),
1681 TEST_URL,
1682 Timestamp::from_nanos(123456i64),
1683 )
1684 .with_name(InspectHandleName::filename("test_file_plz_ignore.inspect"))
1685 .with_hierarchy(hierarchy)
1686 .build();
1687 let selectors = parse_selectors(vec!["a/b/c/d:root:x"]);
1688
1689 let expected_json = json!({
1690 "moniker": "a/b/c/d",
1691 "version": 1,
1692 "data_source": "Inspect",
1693 "payload": {
1694 "root": {
1695 "x": "foo"
1696 }
1697 },
1698 "metadata": {
1699 "component_url": TEST_URL,
1700 "filename": "test_file_plz_ignore.inspect",
1701 "timestamp": 123456,
1702 }
1703 });
1704
1705 let result_json = serde_json::to_value(data.filter(&selectors).expect("Filter Ok"))
1706 .expect("serialization should succeed.");
1707
1708 pretty_assertions::assert_eq!(result_json, expected_json, "golden diff failed.");
1709 }
1710
1711 #[fuchsia::test]
1712 fn default_builder_test() {
1713 let builder = LogsDataBuilder::new(BuilderArgs {
1714 component_url: Some("url".into()),
1715 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
1716 severity: Severity::Info,
1717 timestamp: Timestamp::from_nanos(0),
1718 });
1719 let expected_json = json!({
1721 "moniker": "moniker",
1722 "version": 1,
1723 "data_source": "Logs",
1724 "payload": {
1725 "root":
1726 {
1727 "message":{}
1728 }
1729 },
1730 "metadata": {
1731 "component_url": "url",
1732 "severity": "INFO",
1733 "tags": [],
1734
1735 "timestamp": 0,
1736 }
1737 });
1738 let result_json =
1739 serde_json::to_value(builder.build()).expect("serialization should succeed.");
1740 pretty_assertions::assert_eq!(result_json, expected_json, "golden diff failed.");
1741 }
1742
1743 #[fuchsia::test]
1744 fn regular_message_test() {
1745 let builder = LogsDataBuilder::new(BuilderArgs {
1746 component_url: Some("url".into()),
1747 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
1748 severity: Severity::Info,
1749 timestamp: Timestamp::from_nanos(0),
1750 })
1751 .set_message("app")
1752 .set_file("test file.cc")
1753 .set_line(420)
1754 .set_pid(1001)
1755 .set_tid(200)
1756 .set_dropped(2)
1757 .add_tag("You're")
1758 .add_tag("IT!")
1759 .add_key(LogsProperty::String(LogsField::Other("key".to_string()), "value".to_string()));
1760 let expected_json = json!({
1762 "moniker": "moniker",
1763 "version": 1,
1764 "data_source": "Logs",
1765 "payload": {
1766 "root":
1767 {
1768 "keys":{
1769 "key":"value"
1770 },
1771 "message":{
1772 "value":"app"
1773 }
1774 }
1775 },
1776 "metadata": {
1777 "errors": [],
1778 "component_url": "url",
1779 "errors": [{"dropped_logs":{"count":2}}],
1780 "file": "test file.cc",
1781 "line": 420,
1782 "pid": 1001,
1783 "severity": "INFO",
1784 "tags": ["You're", "IT!"],
1785 "tid": 200,
1786
1787 "timestamp": 0,
1788 }
1789 });
1790 let result_json =
1791 serde_json::to_value(builder.build()).expect("serialization should succeed.");
1792 pretty_assertions::assert_eq!(result_json, expected_json, "golden diff failed.");
1793 }
1794
1795 #[fuchsia::test]
1796 fn display_for_logs() {
1797 let data = LogsDataBuilder::new(BuilderArgs {
1798 timestamp: Timestamp::from_nanos(12345678000i64),
1799 component_url: Some(FlyStr::from("fake-url")),
1800 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
1801 severity: Severity::Info,
1802 })
1803 .set_pid(123)
1804 .set_tid(456)
1805 .set_message("some message".to_string())
1806 .set_file("some_file.cc".to_string())
1807 .set_line(420)
1808 .add_tag("foo")
1809 .add_tag("bar")
1810 .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
1811 .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
1812 .build();
1813
1814 assert_eq!(
1815 "[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test",
1816 format!("{data}")
1817 )
1818 }
1819
1820 #[fuchsia::test]
1821 fn display_for_logs_with_duplicate_moniker() {
1822 let data = LogsDataBuilder::new(BuilderArgs {
1823 timestamp: Timestamp::from_nanos(12345678000i64),
1824 component_url: Some(FlyStr::from("fake-url")),
1825 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
1826 severity: Severity::Info,
1827 })
1828 .set_pid(123)
1829 .set_tid(456)
1830 .set_message("some message".to_string())
1831 .set_file("some_file.cc".to_string())
1832 .set_line(420)
1833 .add_tag("moniker")
1834 .add_tag("bar")
1835 .add_tag("moniker")
1836 .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
1837 .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
1838 .build();
1839
1840 assert_eq!(
1841 "[00012.345678][123][456][moniker][bar] INFO: [some_file.cc(420)] some message test=property value=test",
1842 format!("{data}")
1843 )
1844 }
1845
1846 #[fuchsia::test]
1847 fn display_for_logs_with_duplicate_moniker_and_no_other_tags() {
1848 let data = LogsDataBuilder::new(BuilderArgs {
1849 timestamp: Timestamp::from_nanos(12345678000i64),
1850 component_url: Some(FlyStr::from("fake-url")),
1851 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
1852 severity: Severity::Info,
1853 })
1854 .set_pid(123)
1855 .set_tid(456)
1856 .set_message("some message".to_string())
1857 .set_file("some_file.cc".to_string())
1858 .set_line(420)
1859 .add_tag("moniker")
1860 .add_tag("moniker")
1861 .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
1862 .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
1863 .build();
1864
1865 assert_eq!(
1866 "[00012.345678][123][456][moniker] INFO: [some_file.cc(420)] some message test=property value=test",
1867 format!("{data}")
1868 )
1869 }
1870
1871 #[fuchsia::test]
1872 fn display_for_logs_partial_moniker() {
1873 let data = LogsDataBuilder::new(BuilderArgs {
1874 timestamp: Timestamp::from_nanos(12345678000i64),
1875 component_url: Some(FlyStr::from("fake-url")),
1876 moniker: ExtendedMoniker::parse_str("test/moniker").unwrap(),
1877 severity: Severity::Info,
1878 })
1879 .set_pid(123)
1880 .set_tid(456)
1881 .set_message("some message".to_string())
1882 .set_file("some_file.cc".to_string())
1883 .set_line(420)
1884 .add_tag("foo")
1885 .add_tag("bar")
1886 .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
1887 .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
1888 .build();
1889
1890 assert_eq!(
1891 "[00012.345678][123][456][fake-url][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test",
1892 format!(
1893 "{}",
1894 LogTextPresenter::new(
1895 &data,
1896 LogTextDisplayOptions {
1897 show_full_moniker: false,
1898 prefer_url_component_name: true,
1899 ..Default::default()
1900 }
1901 )
1902 )
1903 )
1904 }
1905
1906 #[fuchsia::test]
1907 fn display_for_logs_exclude_metadata() {
1908 let data = LogsDataBuilder::new(BuilderArgs {
1909 timestamp: Timestamp::from_nanos(12345678000i64),
1910 component_url: Some(FlyStr::from("fake-url")),
1911 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
1912 severity: Severity::Info,
1913 })
1914 .set_pid(123)
1915 .set_tid(456)
1916 .set_message("some message".to_string())
1917 .set_file("some_file.cc".to_string())
1918 .set_line(420)
1919 .add_tag("foo")
1920 .add_tag("bar")
1921 .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
1922 .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
1923 .build();
1924
1925 assert_eq!(
1926 "[00012.345678][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test",
1927 format!(
1928 "{}",
1929 LogTextPresenter::new(
1930 &data,
1931 LogTextDisplayOptions { show_metadata: false, ..Default::default() }
1932 )
1933 )
1934 )
1935 }
1936
1937 #[fuchsia::test]
1938 fn display_for_logs_exclude_tags() {
1939 let data = LogsDataBuilder::new(BuilderArgs {
1940 timestamp: Timestamp::from_nanos(12345678000i64),
1941 component_url: Some(FlyStr::from("fake-url")),
1942 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
1943 severity: Severity::Info,
1944 })
1945 .set_pid(123)
1946 .set_tid(456)
1947 .set_message("some message".to_string())
1948 .set_file("some_file.cc".to_string())
1949 .set_line(420)
1950 .add_tag("foo")
1951 .add_tag("bar")
1952 .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
1953 .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
1954 .build();
1955
1956 assert_eq!(
1957 "[00012.345678][123][456][moniker] INFO: [some_file.cc(420)] some message test=property value=test",
1958 format!(
1959 "{}",
1960 LogTextPresenter::new(
1961 &data,
1962 LogTextDisplayOptions { show_tags: false, ..Default::default() }
1963 )
1964 )
1965 )
1966 }
1967
1968 #[fuchsia::test]
1969 fn display_for_logs_exclude_file() {
1970 let data = LogsDataBuilder::new(BuilderArgs {
1971 timestamp: Timestamp::from_nanos(12345678000i64),
1972 component_url: Some(FlyStr::from("fake-url")),
1973 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
1974 severity: Severity::Info,
1975 })
1976 .set_pid(123)
1977 .set_tid(456)
1978 .set_message("some message".to_string())
1979 .set_file("some_file.cc".to_string())
1980 .set_line(420)
1981 .add_tag("foo")
1982 .add_tag("bar")
1983 .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
1984 .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
1985 .build();
1986
1987 assert_eq!(
1988 "[00012.345678][123][456][moniker][foo,bar] INFO: some message test=property value=test",
1989 format!(
1990 "{}",
1991 LogTextPresenter::new(
1992 &data,
1993 LogTextDisplayOptions { show_file: false, ..Default::default() }
1994 )
1995 )
1996 )
1997 }
1998
1999 #[fuchsia::test]
2000 fn display_for_logs_include_color_by_severity() {
2001 let data = LogsDataBuilder::new(BuilderArgs {
2002 timestamp: Timestamp::from_nanos(12345678000i64),
2003 component_url: Some(FlyStr::from("fake-url")),
2004 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
2005 severity: Severity::Error,
2006 })
2007 .set_pid(123)
2008 .set_tid(456)
2009 .set_message("some message".to_string())
2010 .set_file("some_file.cc".to_string())
2011 .set_line(420)
2012 .add_tag("foo")
2013 .add_tag("bar")
2014 .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
2015 .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
2016 .build();
2017
2018 assert_eq!(
2019 format!(
2020 "{}[00012.345678][123][456][moniker][foo,bar] ERROR: [some_file.cc(420)] some message test=property value=test{}",
2021 color::Fg(color::Red),
2022 style::Reset
2023 ),
2024 format!(
2025 "{}",
2026 LogTextPresenter::new(
2027 &data,
2028 LogTextDisplayOptions { color: LogTextColor::BySeverity, ..Default::default() }
2029 )
2030 )
2031 )
2032 }
2033
2034 #[fuchsia::test]
2035 fn display_for_logs_highlight_line() {
2036 let data = LogsDataBuilder::new(BuilderArgs {
2037 timestamp: Timestamp::from_nanos(12345678000i64),
2038 component_url: Some(FlyStr::from("fake-url")),
2039 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
2040 severity: Severity::Info,
2041 })
2042 .set_pid(123)
2043 .set_tid(456)
2044 .set_message("some message".to_string())
2045 .set_file("some_file.cc".to_string())
2046 .set_line(420)
2047 .add_tag("foo")
2048 .add_tag("bar")
2049 .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
2050 .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
2051 .build();
2052
2053 assert_eq!(
2054 format!(
2055 "{}[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test{}",
2056 color::Fg(color::LightYellow),
2057 style::Reset
2058 ),
2059 LogTextPresenter::new(
2060 &data,
2061 LogTextDisplayOptions { color: LogTextColor::Highlight, ..Default::default() }
2062 )
2063 .to_string()
2064 )
2065 }
2066
2067 #[fuchsia::test]
2068 fn display_for_logs_with_wall_time() {
2069 let data = LogsDataBuilder::new(BuilderArgs {
2070 timestamp: Timestamp::from_nanos(12345678000i64),
2071 component_url: Some(FlyStr::from("fake-url")),
2072 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
2073 severity: Severity::Info,
2074 })
2075 .set_pid(123)
2076 .set_tid(456)
2077 .set_message("some message".to_string())
2078 .set_file("some_file.cc".to_string())
2079 .set_line(420)
2080 .add_tag("foo")
2081 .add_tag("bar")
2082 .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
2083 .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
2084 .build();
2085
2086 assert_eq!(
2087 "[1970-01-01 00:00:12.345][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test",
2088 LogTextPresenter::new(
2089 &data,
2090 LogTextDisplayOptions {
2091 time_format: LogTimeDisplayFormat::WallTime { tz: Timezone::Utc, offset: 1 },
2092 ..Default::default()
2093 }
2094 )
2095 .to_string()
2096 );
2097
2098 assert_eq!(
2099 "[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test",
2100 LogTextPresenter::new(
2101 &data,
2102 LogTextDisplayOptions {
2103 time_format: LogTimeDisplayFormat::WallTime { tz: Timezone::Utc, offset: 0 },
2104 ..Default::default()
2105 }
2106 )
2107 .to_string(),
2108 "should fall back to monotonic if offset is 0"
2109 );
2110 }
2111
2112 #[fuchsia::test]
2113 fn display_for_logs_with_dropped_count() {
2114 let data = LogsDataBuilder::new(BuilderArgs {
2115 timestamp: Timestamp::from_nanos(12345678000i64),
2116 component_url: Some(FlyStr::from("fake-url")),
2117 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
2118 severity: Severity::Info,
2119 })
2120 .set_dropped(5)
2121 .set_pid(123)
2122 .set_tid(456)
2123 .set_message("some message".to_string())
2124 .set_file("some_file.cc".to_string())
2125 .set_line(420)
2126 .add_tag("foo")
2127 .add_tag("bar")
2128 .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
2129 .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
2130 .build();
2131
2132 assert_eq!(
2133 "[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test [dropped=5]",
2134 format!("{}", LogTextPresenter::new(&data, LogTextDisplayOptions::default())),
2135 );
2136
2137 assert_eq!(
2138 format!(
2139 "[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test{} [dropped=5]{}",
2140 color::Fg(color::Yellow),
2141 style::Reset
2142 ),
2143 LogTextPresenter::new(
2144 &data,
2145 LogTextDisplayOptions { color: LogTextColor::BySeverity, ..Default::default() }
2146 )
2147 .to_string()
2148 );
2149 }
2150
2151 #[fuchsia::test]
2152 fn display_for_logs_with_rolled_count() {
2153 let data = LogsDataBuilder::new(BuilderArgs {
2154 timestamp: Timestamp::from_nanos(12345678000i64),
2155 component_url: Some(FlyStr::from("fake-url")),
2156 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
2157 severity: Severity::Info,
2158 })
2159 .set_rolled_out(10)
2160 .set_pid(123)
2161 .set_tid(456)
2162 .set_message("some message".to_string())
2163 .set_file("some_file.cc".to_string())
2164 .set_line(420)
2165 .add_tag("foo")
2166 .add_tag("bar")
2167 .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
2168 .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
2169 .build();
2170
2171 assert_eq!(
2172 "[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test [rolled=10]",
2173 format!("{}", LogTextPresenter::new(&data, LogTextDisplayOptions::default())),
2174 );
2175
2176 assert_eq!(
2177 format!(
2178 "[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test{} [rolled=10]{}",
2179 color::Fg(color::Yellow),
2180 style::Reset
2181 ),
2182 LogTextPresenter::new(
2183 &data,
2184 LogTextDisplayOptions { color: LogTextColor::BySeverity, ..Default::default() }
2185 )
2186 .to_string()
2187 );
2188 }
2189
2190 #[fuchsia::test]
2191 fn display_for_logs_with_dropped_and_rolled_counts() {
2192 let data = LogsDataBuilder::new(BuilderArgs {
2193 timestamp: Timestamp::from_nanos(12345678000i64),
2194 component_url: Some(FlyStr::from("fake-url")),
2195 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
2196 severity: Severity::Info,
2197 })
2198 .set_dropped(5)
2199 .set_rolled_out(10)
2200 .set_pid(123)
2201 .set_tid(456)
2202 .set_message("some message".to_string())
2203 .set_file("some_file.cc".to_string())
2204 .set_line(420)
2205 .add_tag("foo")
2206 .add_tag("bar")
2207 .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
2208 .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
2209 .build();
2210
2211 assert_eq!(
2212 "[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test [dropped=5] [rolled=10]",
2213 format!("{}", LogTextPresenter::new(&data, LogTextDisplayOptions::default())),
2214 );
2215
2216 assert_eq!(
2217 format!(
2218 "[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test{} [dropped=5] [rolled=10]{}",
2219 color::Fg(color::Yellow),
2220 style::Reset
2221 ),
2222 LogTextPresenter::new(
2223 &data,
2224 LogTextDisplayOptions { color: LogTextColor::BySeverity, ..Default::default() }
2225 )
2226 .to_string()
2227 );
2228 }
2229
2230 #[fuchsia::test]
2231 fn display_for_logs_no_tags() {
2232 let data = LogsDataBuilder::new(BuilderArgs {
2233 timestamp: Timestamp::from_nanos(12345678000i64),
2234 component_url: Some(FlyStr::from("fake-url")),
2235 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
2236 severity: Severity::Info,
2237 })
2238 .set_pid(123)
2239 .set_tid(456)
2240 .set_message("some message".to_string())
2241 .build();
2242
2243 assert_eq!("[00012.345678][123][456][moniker] INFO: some message", format!("{data}"))
2244 }
2245
2246 #[fuchsia::test]
2247 fn size_bytes_deserialize_backwards_compatibility() {
2248 let original_json = json!({
2249 "moniker": "a/b",
2250 "version": 1,
2251 "data_source": "Logs",
2252 "payload": {
2253 "root": {
2254 "message":{}
2255 }
2256 },
2257 "metadata": {
2258 "component_url": "url",
2259 "severity": "INFO",
2260 "tags": [],
2261
2262 "timestamp": 123,
2263 }
2264 });
2265 let expected_data = LogsDataBuilder::new(BuilderArgs {
2266 component_url: Some("url".into()),
2267 moniker: ExtendedMoniker::parse_str("a/b").unwrap(),
2268 severity: Severity::Info,
2269 timestamp: Timestamp::from_nanos(123),
2270 })
2271 .build();
2272 let original_data: LogsData = serde_json::from_value(original_json).unwrap();
2273 assert_eq!(original_data, expected_data);
2274 assert_eq!(original_data.metadata.size_bytes, None);
2276 }
2277
2278 #[fuchsia::test]
2279 fn display_for_logs_with_null_terminator() {
2280 let data = LogsDataBuilder::new(BuilderArgs {
2281 timestamp: Timestamp::from_nanos(12345678000i64),
2282 component_url: Some(FlyStr::from("fake-url")),
2283 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
2284 severity: Severity::Info,
2285 })
2286 .set_pid(123)
2287 .set_tid(456)
2288 .set_message("some message\0garbage".to_string())
2289 .set_file("some_file.cc".to_string())
2290 .set_line(420)
2291 .add_tag("foo")
2292 .add_tag("bar")
2293 .build();
2294
2295 assert_eq!(
2296 "[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message",
2297 format!("{data}")
2298 )
2299 }
2300
2301 #[fuchsia::test]
2302 fn dropped_deserialize_backwards_compatibility() {
2303 let original_json = json!({
2304 "moniker": "a/b",
2305 "version": 1,
2306 "data_source": "Logs",
2307 "payload": {
2308 "root": {
2309 "message":{}
2310 }
2311 },
2312 "metadata": {
2313 "dropped": 0,
2314 "component_url": "url",
2315 "severity": "INFO",
2316 "tags": [],
2317
2318 "timestamp": 123,
2319 }
2320 });
2321 let expected_data = LogsDataBuilder::new(BuilderArgs {
2322 component_url: Some("url".into()),
2323 moniker: ExtendedMoniker::parse_str("a/b").unwrap(),
2324 severity: Severity::Info,
2325 timestamp: Timestamp::from_nanos(123),
2326 })
2327 .build();
2328 let original_data: LogsData = serde_json::from_value(original_json).unwrap();
2329 assert_eq!(original_data, expected_data);
2330 assert_eq!(original_data.metadata.dropped, None);
2332 }
2333
2334 #[fuchsia::test]
2335 fn severity_aliases() {
2336 assert_eq!(Severity::from_str("warn").unwrap(), Severity::Warn);
2337 assert_eq!(Severity::from_str("warning").unwrap(), Severity::Warn);
2338 }
2339
2340 #[fuchsia::test]
2341 fn test_metadata_merge() {
2342 let mut meta = InspectMetadata {
2343 errors: Some(vec![InspectError { message: "error1".to_string() }]),
2344 name: InspectHandleName::name("test"),
2345 component_url: "fuchsia-pkg://test".into(),
2346 timestamp: Timestamp::from_nanos(100),
2347 escrowed: false,
2348 };
2349
2350 meta.merge(InspectMetadata {
2351 errors: Some(vec![InspectError { message: "error2".to_string() }]),
2352 name: InspectHandleName::name("test"),
2353 component_url: "fuchsia-pkg://test".into(),
2354 timestamp: Timestamp::from_nanos(200),
2355 escrowed: false,
2356 });
2357
2358 assert_eq!(
2359 meta,
2360 InspectMetadata {
2361 errors: Some(vec![
2362 InspectError { message: "error1".to_string() },
2363 InspectError { message: "error2".to_string() },
2364 ]),
2365 name: InspectHandleName::name("test"),
2366 component_url: "fuchsia-pkg://test".into(),
2367 timestamp: Timestamp::from_nanos(200),
2368 escrowed: false,
2369 }
2370 );
2371 }
2372
2373 #[fuchsia::test]
2374 fn test_metadata_merge_older_timestamp_noop() {
2375 let mut meta = InspectMetadata {
2376 errors: None,
2377 name: InspectHandleName::name("test"),
2378 component_url: TEST_URL.into(),
2379 timestamp: Timestamp::from_nanos(200),
2380 escrowed: false,
2381 };
2382 meta.merge(InspectMetadata {
2383 errors: None,
2384 name: InspectHandleName::name("test"),
2385 component_url: TEST_URL.into(),
2386 timestamp: Timestamp::from_nanos(100),
2387 escrowed: false,
2388 });
2389 assert_eq!(
2390 meta,
2391 InspectMetadata {
2392 errors: None,
2393 name: InspectHandleName::name("test"),
2394 component_url: TEST_URL.into(),
2395 timestamp: Timestamp::from_nanos(200),
2396 escrowed: false,
2397 }
2398 );
2399 }
2400
2401 fn new_test_data(moniker: &str, payload_val: Option<&str>, timestamp: i64) -> InspectData {
2402 let mut builder = InspectDataBuilder::new(
2403 moniker.try_into().unwrap(),
2404 TEST_URL,
2405 Timestamp::from_nanos(timestamp),
2406 );
2407 if let Some(val) = payload_val {
2408 builder = builder.with_hierarchy(hierarchy! { root: { "key": val } });
2409 }
2410 builder.build()
2411 }
2412
2413 #[fuchsia::test]
2414 fn test_data_merge() {
2415 let mut data = new_test_data("a/b/c", Some("val1"), 100);
2416 let mut other = new_test_data("a/b/c", Some("val2"), 200);
2417 other.metadata.errors = Some(vec![InspectError { message: "error".into() }]);
2418
2419 data.merge(other);
2420
2421 let expected_payload = hierarchy! { root: { "key": "val2" } };
2422 assert_eq!(data.payload, Some(expected_payload));
2423 assert_eq!(data.metadata.timestamp, Timestamp::from_nanos(200));
2424 assert_eq!(data.metadata.errors, Some(vec![InspectError { message: "error".into() }]));
2425 }
2426
2427 #[test_case(new_test_data("a/b/d", Some("v2"), 100); "different moniker")]
2428 #[test_case(
2429 {
2430 let mut d = new_test_data("a/b/c", Some("v2"), 100);
2431 d.version = 2;
2432 d
2433 }; "different version")]
2434 #[test_case(
2435 {
2436 let mut d = new_test_data("a/b/c", Some("v2"), 100);
2437 d.data_source = DataSource::Logs;
2438 d
2439 }; "different data source")]
2440 #[fuchsia::test]
2441 fn test_data_merge_noop(other: InspectData) {
2442 let mut data = new_test_data("a/b/c", Some("v1"), 100);
2443 let original = data.clone();
2444 data.merge(other);
2445 assert_eq!(data, original);
2446 }
2447
2448 #[test_case(None, Some("val2"), Some("val2") ; "none_with_some")]
2449 #[test_case(Some("val1"), None, Some("val1") ; "some_with_none")]
2450 #[test_case(Some("val1"), Some("val2"), Some("val2") ; "some_with_some")]
2451 #[fuchsia::test]
2452 fn test_data_merge_payloads(
2453 payload: Option<&str>,
2454 other_payload: Option<&str>,
2455 expected: Option<&str>,
2456 ) {
2457 let mut data = new_test_data("a/b/c", payload, 100);
2458 let other = new_test_data("a/b/c", other_payload, 100);
2459
2460 data.merge(other);
2461 assert_eq!(data, new_test_data("a/b/c", expected, 100));
2462 }
2463}