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(&self) -> Cow<'_, str> {
1031 match &self.moniker {
1032 ExtendedMoniker::ComponentManager => {
1033 Cow::Borrowed(EXTENDED_MONIKER_COMPONENT_MANAGER_STR)
1034 }
1035 ExtendedMoniker::ComponentInstance(moniker) => {
1036 if moniker.is_root() {
1037 Cow::Borrowed(ROOT_MONIKER_REPR)
1038 } else {
1039 Cow::Owned(moniker.leaf().unwrap().to_string())
1040 }
1041 }
1042 }
1043 }
1044}
1045
1046#[derive(Clone, Copy, Debug)]
1048pub struct LogTextDisplayOptions {
1049 pub show_full_moniker: bool,
1051
1052 pub show_metadata: bool,
1054
1055 pub show_tags: bool,
1057
1058 pub show_file: bool,
1060
1061 pub color: LogTextColor,
1063
1064 pub time_format: LogTimeDisplayFormat,
1066}
1067
1068impl Default for LogTextDisplayOptions {
1069 fn default() -> Self {
1070 Self {
1071 show_full_moniker: true,
1072 show_metadata: true,
1073 show_tags: true,
1074 show_file: true,
1075 color: Default::default(),
1076 time_format: Default::default(),
1077 }
1078 }
1079}
1080
1081#[derive(Clone, Copy, Debug, Default)]
1083pub enum LogTextColor {
1084 #[default]
1086 None,
1087
1088 BySeverity,
1090
1091 Highlight,
1093}
1094
1095impl LogTextColor {
1096 fn begin_record(&self, f: &mut fmt::Formatter<'_>, severity: Severity) -> fmt::Result {
1097 match self {
1098 LogTextColor::BySeverity => match severity {
1099 Severity::Fatal => {
1100 write!(f, "{}{}", color::Bg(color::Red), color::Fg(color::White))?
1101 }
1102 Severity::Error => write!(f, "{}", color::Fg(color::Red))?,
1103 Severity::Warn => write!(f, "{}", color::Fg(color::Yellow))?,
1104 Severity::Info => (),
1105 Severity::Debug => write!(f, "{}", color::Fg(color::LightBlue))?,
1106 Severity::Trace => write!(f, "{}", color::Fg(color::LightMagenta))?,
1107 },
1108 LogTextColor::Highlight => write!(f, "{}", color::Fg(color::LightYellow))?,
1109 LogTextColor::None => {}
1110 }
1111 Ok(())
1112 }
1113
1114 fn begin_lost_message_counts(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1115 if let LogTextColor::BySeverity = self {
1116 write!(f, "{}", color::Fg(color::Yellow))?;
1118 }
1119 Ok(())
1120 }
1121
1122 fn end_record(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1123 match self {
1124 LogTextColor::BySeverity | LogTextColor::Highlight => write!(f, "{}", style::Reset)?,
1125 LogTextColor::None => {}
1126 };
1127 Ok(())
1128 }
1129}
1130
1131#[derive(Clone, Copy, Debug, PartialEq)]
1133pub enum Timezone {
1134 Local,
1136
1137 Utc,
1139}
1140
1141impl Timezone {
1142 fn format(&self, seconds: i64, rem_nanos: u32) -> impl std::fmt::Display {
1143 const TIMESTAMP_FORMAT: &str = "%Y-%m-%d %H:%M:%S.%3f";
1144 match self {
1145 Timezone::Local => {
1146 Local.timestamp_opt(seconds, rem_nanos).unwrap().format(TIMESTAMP_FORMAT)
1147 }
1148 Timezone::Utc => {
1149 Utc.timestamp_opt(seconds, rem_nanos).unwrap().format(TIMESTAMP_FORMAT)
1150 }
1151 }
1152 }
1153}
1154
1155#[derive(Clone, Copy, Debug, Default)]
1157pub enum LogTimeDisplayFormat {
1158 #[default]
1160 Original,
1161
1162 WallTime {
1164 tz: Timezone,
1166
1167 offset: i64,
1170 },
1171}
1172
1173impl LogTimeDisplayFormat {
1174 fn write_timestamp(&self, f: &mut fmt::Formatter<'_>, time: Timestamp) -> fmt::Result {
1175 const NANOS_IN_SECOND: i64 = 1_000_000_000;
1176
1177 match self {
1178 Self::Original | Self::WallTime { offset: 0, .. } => {
1181 let time: Duration =
1182 Duration::from_nanos(time.into_nanos().try_into().unwrap_or(0));
1183 write!(f, "[{:05}.{:06}]", time.as_secs(), time.as_micros() % MICROS_IN_SEC)?;
1184 }
1185 Self::WallTime { tz, offset } => {
1186 let adjusted = time.into_nanos() + offset;
1187 let seconds = adjusted / NANOS_IN_SECOND;
1188 let rem_nanos = (adjusted % NANOS_IN_SECOND) as u32;
1189 let formatted = tz.format(seconds, rem_nanos);
1190 write!(f, "[{formatted}]")?;
1191 }
1192 }
1193 Ok(())
1194 }
1195}
1196
1197pub struct LogTextPresenter<'a> {
1199 log: &'a Data<Logs>,
1201
1202 options: LogTextDisplayOptions,
1204}
1205
1206impl<'a> LogTextPresenter<'a> {
1207 pub fn new(log: &'a Data<Logs>, options: LogTextDisplayOptions) -> Self {
1211 Self { log, options }
1212 }
1213}
1214
1215impl fmt::Display for Data<Logs> {
1216 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1217 LogTextPresenter::new(self, Default::default()).fmt(f)
1218 }
1219}
1220
1221impl Deref for LogTextPresenter<'_> {
1222 type Target = Data<Logs>;
1223 fn deref(&self) -> &Self::Target {
1224 self.log
1225 }
1226}
1227
1228impl fmt::Display for LogTextPresenter<'_> {
1229 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1230 self.options.color.begin_record(f, self.log.severity())?;
1231 self.options.time_format.write_timestamp(f, self.metadata.timestamp)?;
1232
1233 if self.options.show_metadata {
1234 match self.pid() {
1235 Some(pid) => write!(f, "[{pid}]")?,
1236 None => write!(f, "[]")?,
1237 }
1238 match self.tid() {
1239 Some(tid) => write!(f, "[{tid}]")?,
1240 None => write!(f, "[]")?,
1241 }
1242 }
1243
1244 let moniker = if self.options.show_full_moniker {
1245 match &self.moniker {
1246 ExtendedMoniker::ComponentManager => {
1247 Cow::Borrowed(EXTENDED_MONIKER_COMPONENT_MANAGER_STR)
1248 }
1249 ExtendedMoniker::ComponentInstance(instance) => {
1250 if instance.is_root() {
1251 Cow::Borrowed(ROOT_MONIKER_REPR)
1252 } else {
1253 Cow::Owned(instance.to_string())
1254 }
1255 }
1256 }
1257 } else {
1258 self.component_name()
1259 };
1260 write!(f, "[{moniker}]")?;
1261
1262 if self.options.show_tags {
1263 match &self.metadata.tags {
1264 Some(tags) if !tags.is_empty() => {
1265 let mut filtered =
1266 tags.iter().filter(|tag| *tag != moniker.as_ref()).peekable();
1267 if filtered.peek().is_some() {
1268 write!(f, "[{}]", filtered.join(","))?;
1269 }
1270 }
1271 _ => {}
1272 }
1273 }
1274
1275 write!(f, " {}:", self.metadata.severity)?;
1276
1277 if self.options.show_file {
1278 match (&self.metadata.file, &self.metadata.line) {
1279 (Some(file), Some(line)) => write!(f, " [{file}({line})]")?,
1280 (Some(file), None) => write!(f, " [{file}]")?,
1281 _ => (),
1282 }
1283 }
1284
1285 if let Some(mut msg) = self.msg() {
1286 if let Some(nul) = msg.find("\0") {
1287 msg = &msg[0..nul];
1288 }
1289 write!(f, " {msg}")?;
1290 } else {
1291 write!(f, " <missing message>")?;
1292 }
1293 for kvp in self.payload_keys_strings() {
1294 write!(f, " {kvp}")?;
1295 }
1296
1297 let dropped = self.log.dropped_logs().unwrap_or_default();
1298 let rolled = self.log.rolled_out_logs().unwrap_or_default();
1299 if dropped != 0 || rolled != 0 {
1300 self.options.color.begin_lost_message_counts(f)?;
1301 if dropped != 0 {
1302 write!(f, " [dropped={dropped}]")?;
1303 }
1304 if rolled != 0 {
1305 write!(f, " [rolled={rolled}]")?;
1306 }
1307 }
1308
1309 self.options.color.end_record(f)?;
1310
1311 Ok(())
1312 }
1313}
1314
1315impl Eq for Data<Logs> {}
1316
1317impl PartialOrd for Data<Logs> {
1318 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1319 Some(self.cmp(other))
1320 }
1321}
1322
1323impl Ord for Data<Logs> {
1324 fn cmp(&self, other: &Self) -> Ordering {
1325 self.metadata.timestamp.cmp(&other.metadata.timestamp)
1326 }
1327}
1328
1329#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, PartialOrd, Ord, Serialize)]
1336pub enum LogsField {
1337 ProcessId,
1338 ThreadId,
1339 Dropped,
1340 Tag,
1341 Msg,
1342 MsgStructured,
1343 FilePath,
1344 LineNumber,
1345 Other(String),
1346}
1347
1348impl fmt::Display for LogsField {
1349 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1350 match self {
1351 LogsField::ProcessId => write!(f, "pid"),
1352 LogsField::ThreadId => write!(f, "tid"),
1353 LogsField::Dropped => write!(f, "num_dropped"),
1354 LogsField::Tag => write!(f, "tag"),
1355 LogsField::Msg => write!(f, "message"),
1356 LogsField::MsgStructured => write!(f, "value"),
1357 LogsField::FilePath => write!(f, "file_path"),
1358 LogsField::LineNumber => write!(f, "line_number"),
1359 LogsField::Other(name) => write!(f, "{name}"),
1360 }
1361 }
1362}
1363
1364pub const PID_LABEL: &str = "pid";
1368pub const TID_LABEL: &str = "tid";
1370pub const DROPPED_LABEL: &str = "num_dropped";
1372pub const TAG_LABEL: &str = "tag";
1374pub const MESSAGE_LABEL_STRUCTURED: &str = "value";
1376pub const MESSAGE_LABEL: &str = "message";
1378pub const FILE_PATH_LABEL: &str = "file";
1380pub const LINE_NUMBER_LABEL: &str = "line";
1382
1383impl AsRef<str> for LogsField {
1384 fn as_ref(&self) -> &str {
1385 match self {
1386 Self::ProcessId => PID_LABEL,
1387 Self::ThreadId => TID_LABEL,
1388 Self::Dropped => DROPPED_LABEL,
1389 Self::Tag => TAG_LABEL,
1390 Self::Msg => MESSAGE_LABEL,
1391 Self::FilePath => FILE_PATH_LABEL,
1392 Self::LineNumber => LINE_NUMBER_LABEL,
1393 Self::MsgStructured => MESSAGE_LABEL_STRUCTURED,
1394 Self::Other(str) => str.as_str(),
1395 }
1396 }
1397}
1398
1399impl<T> From<T> for LogsField
1400where
1401 T: Deref<Target = str>,
1403{
1404 fn from(s: T) -> Self {
1405 match s.as_ref() {
1406 PID_LABEL => Self::ProcessId,
1407 TID_LABEL => Self::ThreadId,
1408 DROPPED_LABEL => Self::Dropped,
1409 TAG_LABEL => Self::Tag,
1410 MESSAGE_LABEL => Self::Msg,
1411 FILE_PATH_LABEL => Self::FilePath,
1412 LINE_NUMBER_LABEL => Self::LineNumber,
1413 MESSAGE_LABEL_STRUCTURED => Self::MsgStructured,
1414 _ => Self::Other(s.to_string()),
1415 }
1416 }
1417}
1418
1419impl FromStr for LogsField {
1420 type Err = ();
1421 fn from_str(s: &str) -> Result<Self, Self::Err> {
1422 Ok(Self::from(s))
1423 }
1424}
1425
1426#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
1429#[derive(Clone, Deserialize, Debug, Eq, PartialEq, Serialize)]
1430pub enum LogError {
1431 #[serde(rename = "dropped_logs")]
1434 DroppedLogs { count: u64 },
1435 #[serde(rename = "rolled_out_logs")]
1438 RolledOutLogs { count: u64 },
1439 #[serde(rename = "parse_record")]
1440 FailedToParseRecord(String),
1441 #[serde(rename = "other")]
1442 Other { message: String },
1443}
1444
1445const DROPPED_PAYLOAD_MSG: &str = "Schema failed to fit component budget.";
1446
1447impl MetadataError for LogError {
1448 fn dropped_payload() -> Self {
1449 Self::Other { message: DROPPED_PAYLOAD_MSG.into() }
1450 }
1451
1452 fn message(&self) -> Option<&str> {
1453 match self {
1454 Self::FailedToParseRecord(msg) => Some(msg.as_str()),
1455 Self::Other { message } => Some(message.as_str()),
1456 _ => None,
1457 }
1458 }
1459}
1460
1461#[derive(Debug, PartialEq, Clone, Eq)]
1464pub struct InspectError {
1465 pub message: String,
1466}
1467
1468impl MetadataError for InspectError {
1469 fn dropped_payload() -> Self {
1470 Self { message: "Schema failed to fit component budget.".into() }
1471 }
1472
1473 fn message(&self) -> Option<&str> {
1474 Some(self.message.as_str())
1475 }
1476}
1477
1478impl fmt::Display for InspectError {
1479 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1480 write!(f, "{}", self.message)
1481 }
1482}
1483
1484impl Borrow<str> for InspectError {
1485 fn borrow(&self) -> &str {
1486 &self.message
1487 }
1488}
1489
1490impl Serialize for InspectError {
1491 fn serialize<S: Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
1492 self.message.serialize(ser)
1493 }
1494}
1495
1496impl<'de> Deserialize<'de> for InspectError {
1497 fn deserialize<D>(de: D) -> Result<Self, D::Error>
1498 where
1499 D: Deserializer<'de>,
1500 {
1501 let message = String::deserialize(de)?;
1502 Ok(Self { message })
1503 }
1504}
1505
1506#[cfg(test)]
1507mod tests {
1508 use super::*;
1509 use diagnostics_hierarchy::hierarchy;
1510 use selectors::FastError;
1511 use serde_json::json;
1512 use test_case::test_case;
1513
1514 const TEST_URL: &str = "fuchsia-pkg://test";
1515
1516 #[fuchsia::test]
1517 fn test_canonical_json_inspect_formatting() {
1518 let mut hierarchy = hierarchy! {
1519 root: {
1520 x: "foo",
1521 }
1522 };
1523
1524 hierarchy.sort();
1525 let json_schema = InspectDataBuilder::new(
1526 "a/b/c/d".try_into().unwrap(),
1527 TEST_URL,
1528 Timestamp::from_nanos(123456i64),
1529 )
1530 .with_hierarchy(hierarchy)
1531 .with_name(InspectHandleName::filename("test_file_plz_ignore.inspect"))
1532 .build();
1533
1534 let result_json =
1535 serde_json::to_value(&json_schema).expect("serialization should succeed.");
1536
1537 let expected_json = json!({
1538 "moniker": "a/b/c/d",
1539 "version": 1,
1540 "data_source": "Inspect",
1541 "payload": {
1542 "root": {
1543 "x": "foo"
1544 }
1545 },
1546 "metadata": {
1547 "component_url": TEST_URL,
1548 "filename": "test_file_plz_ignore.inspect",
1549 "timestamp": 123456,
1550 }
1551 });
1552
1553 pretty_assertions::assert_eq!(result_json, expected_json, "golden diff failed.");
1554 }
1555
1556 #[fuchsia::test]
1557 fn test_errorful_json_inspect_formatting() {
1558 let json_schema = InspectDataBuilder::new(
1559 "a/b/c/d".try_into().unwrap(),
1560 TEST_URL,
1561 Timestamp::from_nanos(123456i64),
1562 )
1563 .with_name(InspectHandleName::filename("test_file_plz_ignore.inspect"))
1564 .with_errors(vec![InspectError { message: "too much fun being had.".to_string() }])
1565 .build();
1566
1567 let result_json =
1568 serde_json::to_value(&json_schema).expect("serialization should succeed.");
1569
1570 let expected_json = json!({
1571 "moniker": "a/b/c/d",
1572 "version": 1,
1573 "data_source": "Inspect",
1574 "payload": null,
1575 "metadata": {
1576 "component_url": TEST_URL,
1577 "errors": ["too much fun being had."],
1578 "filename": "test_file_plz_ignore.inspect",
1579 "timestamp": 123456,
1580 }
1581 });
1582
1583 pretty_assertions::assert_eq!(result_json, expected_json, "golden diff failed.");
1584 }
1585
1586 fn parse_selectors(strings: Vec<&str>) -> Vec<Selector> {
1587 strings
1588 .iter()
1589 .map(|s| match selectors::parse_selector::<FastError>(s) {
1590 Ok(selector) => selector,
1591 Err(e) => panic!("Couldn't parse selector {s}: {e}"),
1592 })
1593 .collect::<Vec<_>>()
1594 }
1595
1596 #[fuchsia::test]
1597 fn test_filter_returns_none_on_empty_hierarchy() {
1598 let data = InspectDataBuilder::new(
1599 "a/b/c/d".try_into().unwrap(),
1600 TEST_URL,
1601 Timestamp::from_nanos(123456i64),
1602 )
1603 .build();
1604 let selectors = parse_selectors(vec!["a/b/c/d:foo"]);
1605 assert_eq!(data.filter(&selectors).expect("Filter OK"), None);
1606 }
1607
1608 #[fuchsia::test]
1609 fn test_filter_returns_none_on_selector_mismatch() {
1610 let mut hierarchy = hierarchy! {
1611 root: {
1612 x: "foo",
1613 }
1614 };
1615 hierarchy.sort();
1616 let data = InspectDataBuilder::new(
1617 "b/c/d/e".try_into().unwrap(),
1618 TEST_URL,
1619 Timestamp::from_nanos(123456i64),
1620 )
1621 .with_hierarchy(hierarchy)
1622 .build();
1623 let selectors = parse_selectors(vec!["a/b/c/d:foo"]);
1624 assert_eq!(data.filter(&selectors).expect("Filter OK"), None);
1625 }
1626
1627 #[fuchsia::test]
1628 fn test_filter_returns_none_on_data_mismatch() {
1629 let mut hierarchy = hierarchy! {
1630 root: {
1631 x: "foo",
1632 }
1633 };
1634 hierarchy.sort();
1635 let data = InspectDataBuilder::new(
1636 "a/b/c/d".try_into().unwrap(),
1637 TEST_URL,
1638 Timestamp::from_nanos(123456i64),
1639 )
1640 .with_hierarchy(hierarchy)
1641 .build();
1642 let selectors = parse_selectors(vec!["a/b/c/d:foo"]);
1643
1644 assert_eq!(data.filter(&selectors).expect("FIlter OK"), None);
1645 }
1646
1647 #[fuchsia::test]
1648 fn test_filter_returns_matching_data() {
1649 let mut hierarchy = hierarchy! {
1650 root: {
1651 x: "foo",
1652 y: "bar",
1653 }
1654 };
1655 hierarchy.sort();
1656 let data = InspectDataBuilder::new(
1657 "a/b/c/d".try_into().unwrap(),
1658 TEST_URL,
1659 Timestamp::from_nanos(123456i64),
1660 )
1661 .with_name(InspectHandleName::filename("test_file_plz_ignore.inspect"))
1662 .with_hierarchy(hierarchy)
1663 .build();
1664 let selectors = parse_selectors(vec!["a/b/c/d:root:x"]);
1665
1666 let expected_json = json!({
1667 "moniker": "a/b/c/d",
1668 "version": 1,
1669 "data_source": "Inspect",
1670 "payload": {
1671 "root": {
1672 "x": "foo"
1673 }
1674 },
1675 "metadata": {
1676 "component_url": TEST_URL,
1677 "filename": "test_file_plz_ignore.inspect",
1678 "timestamp": 123456,
1679 }
1680 });
1681
1682 let result_json = serde_json::to_value(data.filter(&selectors).expect("Filter Ok"))
1683 .expect("serialization should succeed.");
1684
1685 pretty_assertions::assert_eq!(result_json, expected_json, "golden diff failed.");
1686 }
1687
1688 #[fuchsia::test]
1689 fn default_builder_test() {
1690 let builder = LogsDataBuilder::new(BuilderArgs {
1691 component_url: Some("url".into()),
1692 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
1693 severity: Severity::Info,
1694 timestamp: Timestamp::from_nanos(0),
1695 });
1696 let expected_json = json!({
1698 "moniker": "moniker",
1699 "version": 1,
1700 "data_source": "Logs",
1701 "payload": {
1702 "root":
1703 {
1704 "message":{}
1705 }
1706 },
1707 "metadata": {
1708 "component_url": "url",
1709 "severity": "INFO",
1710 "tags": [],
1711
1712 "timestamp": 0,
1713 }
1714 });
1715 let result_json =
1716 serde_json::to_value(builder.build()).expect("serialization should succeed.");
1717 pretty_assertions::assert_eq!(result_json, expected_json, "golden diff failed.");
1718 }
1719
1720 #[fuchsia::test]
1721 fn regular_message_test() {
1722 let builder = LogsDataBuilder::new(BuilderArgs {
1723 component_url: Some("url".into()),
1724 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
1725 severity: Severity::Info,
1726 timestamp: Timestamp::from_nanos(0),
1727 })
1728 .set_message("app")
1729 .set_file("test file.cc")
1730 .set_line(420)
1731 .set_pid(1001)
1732 .set_tid(200)
1733 .set_dropped(2)
1734 .add_tag("You're")
1735 .add_tag("IT!")
1736 .add_key(LogsProperty::String(LogsField::Other("key".to_string()), "value".to_string()));
1737 let expected_json = json!({
1739 "moniker": "moniker",
1740 "version": 1,
1741 "data_source": "Logs",
1742 "payload": {
1743 "root":
1744 {
1745 "keys":{
1746 "key":"value"
1747 },
1748 "message":{
1749 "value":"app"
1750 }
1751 }
1752 },
1753 "metadata": {
1754 "errors": [],
1755 "component_url": "url",
1756 "errors": [{"dropped_logs":{"count":2}}],
1757 "file": "test file.cc",
1758 "line": 420,
1759 "pid": 1001,
1760 "severity": "INFO",
1761 "tags": ["You're", "IT!"],
1762 "tid": 200,
1763
1764 "timestamp": 0,
1765 }
1766 });
1767 let result_json =
1768 serde_json::to_value(builder.build()).expect("serialization should succeed.");
1769 pretty_assertions::assert_eq!(result_json, expected_json, "golden diff failed.");
1770 }
1771
1772 #[fuchsia::test]
1773 fn display_for_logs() {
1774 let data = LogsDataBuilder::new(BuilderArgs {
1775 timestamp: Timestamp::from_nanos(12345678000i64),
1776 component_url: Some(FlyStr::from("fake-url")),
1777 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
1778 severity: Severity::Info,
1779 })
1780 .set_pid(123)
1781 .set_tid(456)
1782 .set_message("some message".to_string())
1783 .set_file("some_file.cc".to_string())
1784 .set_line(420)
1785 .add_tag("foo")
1786 .add_tag("bar")
1787 .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
1788 .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
1789 .build();
1790
1791 assert_eq!(
1792 "[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test",
1793 format!("{data}")
1794 )
1795 }
1796
1797 #[fuchsia::test]
1798 fn display_for_logs_with_duplicate_moniker() {
1799 let data = LogsDataBuilder::new(BuilderArgs {
1800 timestamp: Timestamp::from_nanos(12345678000i64),
1801 component_url: Some(FlyStr::from("fake-url")),
1802 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
1803 severity: Severity::Info,
1804 })
1805 .set_pid(123)
1806 .set_tid(456)
1807 .set_message("some message".to_string())
1808 .set_file("some_file.cc".to_string())
1809 .set_line(420)
1810 .add_tag("moniker")
1811 .add_tag("bar")
1812 .add_tag("moniker")
1813 .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
1814 .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
1815 .build();
1816
1817 assert_eq!(
1818 "[00012.345678][123][456][moniker][bar] INFO: [some_file.cc(420)] some message test=property value=test",
1819 format!("{data}")
1820 )
1821 }
1822
1823 #[fuchsia::test]
1824 fn display_for_logs_with_duplicate_moniker_and_no_other_tags() {
1825 let data = LogsDataBuilder::new(BuilderArgs {
1826 timestamp: Timestamp::from_nanos(12345678000i64),
1827 component_url: Some(FlyStr::from("fake-url")),
1828 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
1829 severity: Severity::Info,
1830 })
1831 .set_pid(123)
1832 .set_tid(456)
1833 .set_message("some message".to_string())
1834 .set_file("some_file.cc".to_string())
1835 .set_line(420)
1836 .add_tag("moniker")
1837 .add_tag("moniker")
1838 .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
1839 .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
1840 .build();
1841
1842 assert_eq!(
1843 "[00012.345678][123][456][moniker] INFO: [some_file.cc(420)] some message test=property value=test",
1844 format!("{data}")
1845 )
1846 }
1847
1848 #[fuchsia::test]
1849 fn display_for_logs_partial_moniker() {
1850 let data = LogsDataBuilder::new(BuilderArgs {
1851 timestamp: Timestamp::from_nanos(12345678000i64),
1852 component_url: Some(FlyStr::from("fake-url")),
1853 moniker: ExtendedMoniker::parse_str("test/moniker").unwrap(),
1854 severity: Severity::Info,
1855 })
1856 .set_pid(123)
1857 .set_tid(456)
1858 .set_message("some message".to_string())
1859 .set_file("some_file.cc".to_string())
1860 .set_line(420)
1861 .add_tag("foo")
1862 .add_tag("bar")
1863 .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
1864 .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
1865 .build();
1866
1867 assert_eq!(
1868 "[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test",
1869 format!(
1870 "{}",
1871 LogTextPresenter::new(
1872 &data,
1873 LogTextDisplayOptions { show_full_moniker: false, ..Default::default() }
1874 )
1875 )
1876 )
1877 }
1878
1879 #[fuchsia::test]
1880 fn display_for_logs_exclude_metadata() {
1881 let data = LogsDataBuilder::new(BuilderArgs {
1882 timestamp: Timestamp::from_nanos(12345678000i64),
1883 component_url: Some(FlyStr::from("fake-url")),
1884 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
1885 severity: Severity::Info,
1886 })
1887 .set_pid(123)
1888 .set_tid(456)
1889 .set_message("some message".to_string())
1890 .set_file("some_file.cc".to_string())
1891 .set_line(420)
1892 .add_tag("foo")
1893 .add_tag("bar")
1894 .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
1895 .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
1896 .build();
1897
1898 assert_eq!(
1899 "[00012.345678][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test",
1900 format!(
1901 "{}",
1902 LogTextPresenter::new(
1903 &data,
1904 LogTextDisplayOptions { show_metadata: false, ..Default::default() }
1905 )
1906 )
1907 )
1908 }
1909
1910 #[fuchsia::test]
1911 fn display_for_logs_exclude_tags() {
1912 let data = LogsDataBuilder::new(BuilderArgs {
1913 timestamp: Timestamp::from_nanos(12345678000i64),
1914 component_url: Some(FlyStr::from("fake-url")),
1915 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
1916 severity: Severity::Info,
1917 })
1918 .set_pid(123)
1919 .set_tid(456)
1920 .set_message("some message".to_string())
1921 .set_file("some_file.cc".to_string())
1922 .set_line(420)
1923 .add_tag("foo")
1924 .add_tag("bar")
1925 .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
1926 .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
1927 .build();
1928
1929 assert_eq!(
1930 "[00012.345678][123][456][moniker] INFO: [some_file.cc(420)] some message test=property value=test",
1931 format!(
1932 "{}",
1933 LogTextPresenter::new(
1934 &data,
1935 LogTextDisplayOptions { show_tags: false, ..Default::default() }
1936 )
1937 )
1938 )
1939 }
1940
1941 #[fuchsia::test]
1942 fn display_for_logs_exclude_file() {
1943 let data = LogsDataBuilder::new(BuilderArgs {
1944 timestamp: Timestamp::from_nanos(12345678000i64),
1945 component_url: Some(FlyStr::from("fake-url")),
1946 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
1947 severity: Severity::Info,
1948 })
1949 .set_pid(123)
1950 .set_tid(456)
1951 .set_message("some message".to_string())
1952 .set_file("some_file.cc".to_string())
1953 .set_line(420)
1954 .add_tag("foo")
1955 .add_tag("bar")
1956 .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
1957 .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
1958 .build();
1959
1960 assert_eq!(
1961 "[00012.345678][123][456][moniker][foo,bar] INFO: some message test=property value=test",
1962 format!(
1963 "{}",
1964 LogTextPresenter::new(
1965 &data,
1966 LogTextDisplayOptions { show_file: false, ..Default::default() }
1967 )
1968 )
1969 )
1970 }
1971
1972 #[fuchsia::test]
1973 fn display_for_logs_include_color_by_severity() {
1974 let data = LogsDataBuilder::new(BuilderArgs {
1975 timestamp: Timestamp::from_nanos(12345678000i64),
1976 component_url: Some(FlyStr::from("fake-url")),
1977 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
1978 severity: Severity::Error,
1979 })
1980 .set_pid(123)
1981 .set_tid(456)
1982 .set_message("some message".to_string())
1983 .set_file("some_file.cc".to_string())
1984 .set_line(420)
1985 .add_tag("foo")
1986 .add_tag("bar")
1987 .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
1988 .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
1989 .build();
1990
1991 assert_eq!(
1992 format!(
1993 "{}[00012.345678][123][456][moniker][foo,bar] ERROR: [some_file.cc(420)] some message test=property value=test{}",
1994 color::Fg(color::Red),
1995 style::Reset
1996 ),
1997 format!(
1998 "{}",
1999 LogTextPresenter::new(
2000 &data,
2001 LogTextDisplayOptions { color: LogTextColor::BySeverity, ..Default::default() }
2002 )
2003 )
2004 )
2005 }
2006
2007 #[fuchsia::test]
2008 fn display_for_logs_highlight_line() {
2009 let data = LogsDataBuilder::new(BuilderArgs {
2010 timestamp: Timestamp::from_nanos(12345678000i64),
2011 component_url: Some(FlyStr::from("fake-url")),
2012 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
2013 severity: Severity::Info,
2014 })
2015 .set_pid(123)
2016 .set_tid(456)
2017 .set_message("some message".to_string())
2018 .set_file("some_file.cc".to_string())
2019 .set_line(420)
2020 .add_tag("foo")
2021 .add_tag("bar")
2022 .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
2023 .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
2024 .build();
2025
2026 assert_eq!(
2027 format!(
2028 "{}[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test{}",
2029 color::Fg(color::LightYellow),
2030 style::Reset
2031 ),
2032 LogTextPresenter::new(
2033 &data,
2034 LogTextDisplayOptions { color: LogTextColor::Highlight, ..Default::default() }
2035 )
2036 .to_string()
2037 )
2038 }
2039
2040 #[fuchsia::test]
2041 fn display_for_logs_with_wall_time() {
2042 let data = LogsDataBuilder::new(BuilderArgs {
2043 timestamp: Timestamp::from_nanos(12345678000i64),
2044 component_url: Some(FlyStr::from("fake-url")),
2045 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
2046 severity: Severity::Info,
2047 })
2048 .set_pid(123)
2049 .set_tid(456)
2050 .set_message("some message".to_string())
2051 .set_file("some_file.cc".to_string())
2052 .set_line(420)
2053 .add_tag("foo")
2054 .add_tag("bar")
2055 .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
2056 .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
2057 .build();
2058
2059 assert_eq!(
2060 "[1970-01-01 00:00:12.345][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test",
2061 LogTextPresenter::new(
2062 &data,
2063 LogTextDisplayOptions {
2064 time_format: LogTimeDisplayFormat::WallTime { tz: Timezone::Utc, offset: 1 },
2065 ..Default::default()
2066 }
2067 )
2068 .to_string()
2069 );
2070
2071 assert_eq!(
2072 "[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test",
2073 LogTextPresenter::new(
2074 &data,
2075 LogTextDisplayOptions {
2076 time_format: LogTimeDisplayFormat::WallTime { tz: Timezone::Utc, offset: 0 },
2077 ..Default::default()
2078 }
2079 )
2080 .to_string(),
2081 "should fall back to monotonic if offset is 0"
2082 );
2083 }
2084
2085 #[fuchsia::test]
2086 fn display_for_logs_with_dropped_count() {
2087 let data = LogsDataBuilder::new(BuilderArgs {
2088 timestamp: Timestamp::from_nanos(12345678000i64),
2089 component_url: Some(FlyStr::from("fake-url")),
2090 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
2091 severity: Severity::Info,
2092 })
2093 .set_dropped(5)
2094 .set_pid(123)
2095 .set_tid(456)
2096 .set_message("some message".to_string())
2097 .set_file("some_file.cc".to_string())
2098 .set_line(420)
2099 .add_tag("foo")
2100 .add_tag("bar")
2101 .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
2102 .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
2103 .build();
2104
2105 assert_eq!(
2106 "[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test [dropped=5]",
2107 format!("{}", LogTextPresenter::new(&data, LogTextDisplayOptions::default())),
2108 );
2109
2110 assert_eq!(
2111 format!(
2112 "[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test{} [dropped=5]{}",
2113 color::Fg(color::Yellow),
2114 style::Reset
2115 ),
2116 LogTextPresenter::new(
2117 &data,
2118 LogTextDisplayOptions { color: LogTextColor::BySeverity, ..Default::default() }
2119 )
2120 .to_string()
2121 );
2122 }
2123
2124 #[fuchsia::test]
2125 fn display_for_logs_with_rolled_count() {
2126 let data = LogsDataBuilder::new(BuilderArgs {
2127 timestamp: Timestamp::from_nanos(12345678000i64),
2128 component_url: Some(FlyStr::from("fake-url")),
2129 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
2130 severity: Severity::Info,
2131 })
2132 .set_rolled_out(10)
2133 .set_pid(123)
2134 .set_tid(456)
2135 .set_message("some message".to_string())
2136 .set_file("some_file.cc".to_string())
2137 .set_line(420)
2138 .add_tag("foo")
2139 .add_tag("bar")
2140 .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
2141 .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
2142 .build();
2143
2144 assert_eq!(
2145 "[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test [rolled=10]",
2146 format!("{}", LogTextPresenter::new(&data, LogTextDisplayOptions::default())),
2147 );
2148
2149 assert_eq!(
2150 format!(
2151 "[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test{} [rolled=10]{}",
2152 color::Fg(color::Yellow),
2153 style::Reset
2154 ),
2155 LogTextPresenter::new(
2156 &data,
2157 LogTextDisplayOptions { color: LogTextColor::BySeverity, ..Default::default() }
2158 )
2159 .to_string()
2160 );
2161 }
2162
2163 #[fuchsia::test]
2164 fn display_for_logs_with_dropped_and_rolled_counts() {
2165 let data = LogsDataBuilder::new(BuilderArgs {
2166 timestamp: Timestamp::from_nanos(12345678000i64),
2167 component_url: Some(FlyStr::from("fake-url")),
2168 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
2169 severity: Severity::Info,
2170 })
2171 .set_dropped(5)
2172 .set_rolled_out(10)
2173 .set_pid(123)
2174 .set_tid(456)
2175 .set_message("some message".to_string())
2176 .set_file("some_file.cc".to_string())
2177 .set_line(420)
2178 .add_tag("foo")
2179 .add_tag("bar")
2180 .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
2181 .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
2182 .build();
2183
2184 assert_eq!(
2185 "[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test [dropped=5] [rolled=10]",
2186 format!("{}", LogTextPresenter::new(&data, LogTextDisplayOptions::default())),
2187 );
2188
2189 assert_eq!(
2190 format!(
2191 "[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test{} [dropped=5] [rolled=10]{}",
2192 color::Fg(color::Yellow),
2193 style::Reset
2194 ),
2195 LogTextPresenter::new(
2196 &data,
2197 LogTextDisplayOptions { color: LogTextColor::BySeverity, ..Default::default() }
2198 )
2199 .to_string()
2200 );
2201 }
2202
2203 #[fuchsia::test]
2204 fn display_for_logs_no_tags() {
2205 let data = LogsDataBuilder::new(BuilderArgs {
2206 timestamp: Timestamp::from_nanos(12345678000i64),
2207 component_url: Some(FlyStr::from("fake-url")),
2208 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
2209 severity: Severity::Info,
2210 })
2211 .set_pid(123)
2212 .set_tid(456)
2213 .set_message("some message".to_string())
2214 .build();
2215
2216 assert_eq!("[00012.345678][123][456][moniker] INFO: some message", format!("{data}"))
2217 }
2218
2219 #[fuchsia::test]
2220 fn size_bytes_deserialize_backwards_compatibility() {
2221 let original_json = json!({
2222 "moniker": "a/b",
2223 "version": 1,
2224 "data_source": "Logs",
2225 "payload": {
2226 "root": {
2227 "message":{}
2228 }
2229 },
2230 "metadata": {
2231 "component_url": "url",
2232 "severity": "INFO",
2233 "tags": [],
2234
2235 "timestamp": 123,
2236 }
2237 });
2238 let expected_data = LogsDataBuilder::new(BuilderArgs {
2239 component_url: Some("url".into()),
2240 moniker: ExtendedMoniker::parse_str("a/b").unwrap(),
2241 severity: Severity::Info,
2242 timestamp: Timestamp::from_nanos(123),
2243 })
2244 .build();
2245 let original_data: LogsData = serde_json::from_value(original_json).unwrap();
2246 assert_eq!(original_data, expected_data);
2247 assert_eq!(original_data.metadata.size_bytes, None);
2249 }
2250
2251 #[fuchsia::test]
2252 fn display_for_logs_with_null_terminator() {
2253 let data = LogsDataBuilder::new(BuilderArgs {
2254 timestamp: Timestamp::from_nanos(12345678000i64),
2255 component_url: Some(FlyStr::from("fake-url")),
2256 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
2257 severity: Severity::Info,
2258 })
2259 .set_pid(123)
2260 .set_tid(456)
2261 .set_message("some message\0garbage".to_string())
2262 .set_file("some_file.cc".to_string())
2263 .set_line(420)
2264 .add_tag("foo")
2265 .add_tag("bar")
2266 .build();
2267
2268 assert_eq!(
2269 "[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message",
2270 format!("{data}")
2271 )
2272 }
2273
2274 #[fuchsia::test]
2275 fn dropped_deserialize_backwards_compatibility() {
2276 let original_json = json!({
2277 "moniker": "a/b",
2278 "version": 1,
2279 "data_source": "Logs",
2280 "payload": {
2281 "root": {
2282 "message":{}
2283 }
2284 },
2285 "metadata": {
2286 "dropped": 0,
2287 "component_url": "url",
2288 "severity": "INFO",
2289 "tags": [],
2290
2291 "timestamp": 123,
2292 }
2293 });
2294 let expected_data = LogsDataBuilder::new(BuilderArgs {
2295 component_url: Some("url".into()),
2296 moniker: ExtendedMoniker::parse_str("a/b").unwrap(),
2297 severity: Severity::Info,
2298 timestamp: Timestamp::from_nanos(123),
2299 })
2300 .build();
2301 let original_data: LogsData = serde_json::from_value(original_json).unwrap();
2302 assert_eq!(original_data, expected_data);
2303 assert_eq!(original_data.metadata.dropped, None);
2305 }
2306
2307 #[fuchsia::test]
2308 fn severity_aliases() {
2309 assert_eq!(Severity::from_str("warn").unwrap(), Severity::Warn);
2310 assert_eq!(Severity::from_str("warning").unwrap(), Severity::Warn);
2311 }
2312
2313 #[fuchsia::test]
2314 fn test_metadata_merge() {
2315 let mut meta = InspectMetadata {
2316 errors: Some(vec![InspectError { message: "error1".to_string() }]),
2317 name: InspectHandleName::name("test"),
2318 component_url: "fuchsia-pkg://test".into(),
2319 timestamp: Timestamp::from_nanos(100),
2320 escrowed: false,
2321 };
2322
2323 meta.merge(InspectMetadata {
2324 errors: Some(vec![InspectError { message: "error2".to_string() }]),
2325 name: InspectHandleName::name("test"),
2326 component_url: "fuchsia-pkg://test".into(),
2327 timestamp: Timestamp::from_nanos(200),
2328 escrowed: false,
2329 });
2330
2331 assert_eq!(
2332 meta,
2333 InspectMetadata {
2334 errors: Some(vec![
2335 InspectError { message: "error1".to_string() },
2336 InspectError { message: "error2".to_string() },
2337 ]),
2338 name: InspectHandleName::name("test"),
2339 component_url: "fuchsia-pkg://test".into(),
2340 timestamp: Timestamp::from_nanos(200),
2341 escrowed: false,
2342 }
2343 );
2344 }
2345
2346 #[fuchsia::test]
2347 fn test_metadata_merge_older_timestamp_noop() {
2348 let mut meta = InspectMetadata {
2349 errors: None,
2350 name: InspectHandleName::name("test"),
2351 component_url: TEST_URL.into(),
2352 timestamp: Timestamp::from_nanos(200),
2353 escrowed: false,
2354 };
2355 meta.merge(InspectMetadata {
2356 errors: None,
2357 name: InspectHandleName::name("test"),
2358 component_url: TEST_URL.into(),
2359 timestamp: Timestamp::from_nanos(100),
2360 escrowed: false,
2361 });
2362 assert_eq!(
2363 meta,
2364 InspectMetadata {
2365 errors: None,
2366 name: InspectHandleName::name("test"),
2367 component_url: TEST_URL.into(),
2368 timestamp: Timestamp::from_nanos(200),
2369 escrowed: false,
2370 }
2371 );
2372 }
2373
2374 fn new_test_data(moniker: &str, payload_val: Option<&str>, timestamp: i64) -> InspectData {
2375 let mut builder = InspectDataBuilder::new(
2376 moniker.try_into().unwrap(),
2377 TEST_URL,
2378 Timestamp::from_nanos(timestamp),
2379 );
2380 if let Some(val) = payload_val {
2381 builder = builder.with_hierarchy(hierarchy! { root: { "key": val } });
2382 }
2383 builder.build()
2384 }
2385
2386 #[fuchsia::test]
2387 fn test_data_merge() {
2388 let mut data = new_test_data("a/b/c", Some("val1"), 100);
2389 let mut other = new_test_data("a/b/c", Some("val2"), 200);
2390 other.metadata.errors = Some(vec![InspectError { message: "error".into() }]);
2391
2392 data.merge(other);
2393
2394 let expected_payload = hierarchy! { root: { "key": "val2" } };
2395 assert_eq!(data.payload, Some(expected_payload));
2396 assert_eq!(data.metadata.timestamp, Timestamp::from_nanos(200));
2397 assert_eq!(data.metadata.errors, Some(vec![InspectError { message: "error".into() }]));
2398 }
2399
2400 #[test_case(new_test_data("a/b/d", Some("v2"), 100); "different moniker")]
2401 #[test_case(
2402 {
2403 let mut d = new_test_data("a/b/c", Some("v2"), 100);
2404 d.version = 2;
2405 d
2406 }; "different version")]
2407 #[test_case(
2408 {
2409 let mut d = new_test_data("a/b/c", Some("v2"), 100);
2410 d.data_source = DataSource::Logs;
2411 d
2412 }; "different data source")]
2413 #[fuchsia::test]
2414 fn test_data_merge_noop(other: InspectData) {
2415 let mut data = new_test_data("a/b/c", Some("v1"), 100);
2416 let original = data.clone();
2417 data.merge(other);
2418 assert_eq!(data, original);
2419 }
2420
2421 #[test_case(None, Some("val2"), Some("val2") ; "none_with_some")]
2422 #[test_case(Some("val1"), None, Some("val1") ; "some_with_none")]
2423 #[test_case(Some("val1"), Some("val2"), Some("val2") ; "some_with_some")]
2424 #[fuchsia::test]
2425 fn test_data_merge_payloads(
2426 payload: Option<&str>,
2427 other_payload: Option<&str>,
2428 expected: Option<&str>,
2429 ) {
2430 let mut data = new_test_data("a/b/c", payload, 100);
2431 let other = new_test_data("a/b/c", other_payload, 100);
2432
2433 data.merge(other);
2434 assert_eq!(data, new_test_data("a/b/c", expected, 100));
2435 }
2436}