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(mut self, selectors: &[Selector]) -> Result<Option<Self>, Error> {
510 let Some(hierarchy) = self.payload else {
511 return Ok(None);
512 };
513 let matching_selectors =
514 match self.moniker.match_against_selectors(selectors).collect::<Result<Vec<_>, _>>() {
515 Ok(selectors) if selectors.is_empty() => return Ok(None),
516 Ok(selectors) => selectors,
517 Err(e) => {
518 return Err(Error::Internal(e));
519 }
520 };
521
522 let matcher: HierarchyMatcher = match matching_selectors.try_into() {
524 Ok(hierarchy_matcher) => hierarchy_matcher,
525 Err(e) => {
526 return Err(Error::Internal(e.into()));
527 }
528 };
529
530 self.payload = match diagnostics_hierarchy::filter_hierarchy(hierarchy, &matcher) {
531 Some(hierarchy) => Some(hierarchy),
532 None => return Ok(None),
533 };
534 Ok(Some(self))
535 }
536}
537
538#[derive(Debug, Error)]
540pub enum Error {
541 #[error(transparent)]
542 Internal(#[from] anyhow::Error),
543}
544
545pub type InspectData = Data<Inspect>;
547
548pub type LogsData = Data<Logs>;
550
551pub type LogsHierarchy = DiagnosticsHierarchy<LogsField>;
553
554pub type LogsProperty = Property<LogsField>;
556
557impl Data<Inspect> {
558 pub fn name(&self) -> &str {
560 self.metadata.name.as_ref()
561 }
562}
563
564pub struct InspectDataBuilder {
565 data: Data<Inspect>,
566}
567
568impl InspectDataBuilder {
569 pub fn new(
570 moniker: ExtendedMoniker,
571 component_url: impl Into<FlyStr>,
572 timestamp: impl Into<Timestamp>,
573 ) -> Self {
574 Self {
575 data: Data {
576 data_source: DataSource::Inspect,
577 moniker,
578 payload: None,
579 version: 1,
580 metadata: InspectMetadata {
581 errors: None,
582 name: InspectHandleName::name(DEFAULT_TREE_NAME.clone()),
583 component_url: component_url.into(),
584 timestamp: timestamp.into(),
585 escrowed: false,
586 },
587 },
588 }
589 }
590
591 pub fn escrowed(mut self, escrowed: bool) -> Self {
592 self.data.metadata.escrowed = escrowed;
593 self
594 }
595
596 pub fn with_hierarchy(
597 mut self,
598 hierarchy: DiagnosticsHierarchy<<Inspect as DiagnosticsData>::Key>,
599 ) -> Self {
600 self.data.payload = Some(hierarchy);
601 self
602 }
603
604 pub fn with_errors(mut self, errors: Vec<InspectError>) -> Self {
605 self.data.metadata.errors = Some(errors);
606 self
607 }
608
609 pub fn with_name(mut self, name: InspectHandleName) -> Self {
610 self.data.metadata.name = name;
611 self
612 }
613
614 pub fn build(self) -> Data<Inspect> {
615 self.data
616 }
617}
618
619pub struct LogsDataBuilder {
622 errors: Vec<LogError>,
624 msg: Option<String>,
626 tags: Vec<String>,
628 pid: Option<u64>,
630 tid: Option<u64>,
632 file: Option<String>,
634 line: Option<u64>,
636 args: BuilderArgs,
638 keys: Vec<Property<LogsField>>,
640 raw_severity: Option<u8>,
642}
643
644pub struct BuilderArgs {
646 pub moniker: ExtendedMoniker,
648 pub timestamp: Timestamp,
650 pub component_url: Option<FlyStr>,
652 pub severity: Severity,
654}
655
656impl LogsDataBuilder {
657 pub fn new(args: BuilderArgs) -> Self {
659 LogsDataBuilder {
660 args,
661 errors: vec![],
662 msg: None,
663 file: None,
664 line: None,
665 pid: None,
666 tags: vec![],
667 tid: None,
668 keys: vec![],
669 raw_severity: None,
670 }
671 }
672
673 #[must_use = "You must call build on your builder to consume its result"]
675 pub fn set_moniker(mut self, value: ExtendedMoniker) -> Self {
676 self.args.moniker = value;
677 self
678 }
679
680 #[must_use = "You must call build on your builder to consume its result"]
682 pub fn set_url(mut self, value: Option<FlyStr>) -> Self {
683 self.args.component_url = value;
684 self
685 }
686
687 #[must_use = "You must call build on your builder to consume its result"]
692 pub fn set_dropped(mut self, value: u64) -> Self {
693 if value == 0 {
694 return self;
695 }
696 let val = self.errors.iter_mut().find_map(|error| {
697 if let LogError::DroppedLogs { count } = error { Some(count) } else { None }
698 });
699 if let Some(v) = val {
700 *v = value;
701 } else {
702 self.errors.push(LogError::DroppedLogs { count: value });
703 }
704 self
705 }
706
707 pub fn set_raw_severity(mut self, severity: u8) -> Self {
709 self.raw_severity = Some(severity);
710 self
711 }
712
713 #[must_use = "You must call build on your builder to consume its result"]
718 pub fn set_rolled_out(mut self, value: u64) -> Self {
719 if value == 0 {
720 return self;
721 }
722 let val = self.errors.iter_mut().find_map(|error| {
723 if let LogError::RolledOutLogs { count } = error { Some(count) } else { None }
724 });
725 if let Some(v) = val {
726 *v = value;
727 } else {
728 self.errors.push(LogError::RolledOutLogs { count: value });
729 }
730 self
731 }
732
733 pub fn set_severity(mut self, severity: Severity) -> Self {
735 self.args.severity = severity;
736 self.raw_severity = None;
737 self
738 }
739
740 #[must_use = "You must call build on your builder to consume its result"]
742 pub fn set_pid(mut self, value: u64) -> Self {
743 self.pid = Some(value);
744 self
745 }
746
747 #[must_use = "You must call build on your builder to consume its result"]
749 pub fn set_tid(mut self, value: u64) -> Self {
750 self.tid = Some(value);
751 self
752 }
753
754 pub fn build(self) -> LogsData {
756 let mut args = vec![];
757 if let Some(msg) = self.msg {
758 args.push(LogsProperty::String(LogsField::MsgStructured, msg));
759 }
760 let mut payload_fields = vec![DiagnosticsHierarchy::new("message", args, vec![])];
761 if !self.keys.is_empty() {
762 let val = DiagnosticsHierarchy::new("keys", self.keys, vec![]);
763 payload_fields.push(val);
764 }
765 let mut payload = LogsHierarchy::new("root", vec![], payload_fields);
766 payload.sort();
767 let (raw_severity, severity) =
768 self.raw_severity.map(Severity::parse_exact).unwrap_or((None, self.args.severity));
769 let mut ret = LogsData::for_logs(
770 self.args.moniker,
771 Some(payload),
772 self.args.timestamp,
773 self.args.component_url,
774 severity,
775 self.errors,
776 );
777 ret.metadata.raw_severity = raw_severity;
778 ret.metadata.file = self.file;
779 ret.metadata.line = self.line;
780 ret.metadata.pid = self.pid;
781 ret.metadata.tid = self.tid;
782 ret.metadata.tags = Some(self.tags);
783 ret
784 }
785
786 #[must_use = "You must call build on your builder to consume its result"]
788 pub fn add_error(mut self, error: LogError) -> Self {
789 self.errors.push(error);
790 self
791 }
792
793 #[must_use = "You must call build on your builder to consume its result"]
795 pub fn set_message(mut self, msg: impl Into<String>) -> Self {
796 self.msg = Some(msg.into());
797 self
798 }
799
800 #[must_use = "You must call build on your builder to consume its result"]
802 pub fn set_file(mut self, file: impl Into<String>) -> Self {
803 self.file = Some(file.into());
804 self
805 }
806
807 #[must_use = "You must call build on your builder to consume its result"]
809 pub fn set_line(mut self, line: u64) -> Self {
810 self.line = Some(line);
811 self
812 }
813
814 #[must_use = "You must call build on your builder to consume its result"]
816 pub fn add_key(mut self, kvp: Property<LogsField>) -> Self {
817 self.keys.push(kvp);
818 self
819 }
820
821 #[must_use = "You must call build on your builder to consume its result"]
823 pub fn add_tag(mut self, tag: impl Into<String>) -> Self {
824 self.tags.push(tag.into());
825 self
826 }
827}
828
829impl Data<Logs> {
830 pub fn for_logs(
832 moniker: ExtendedMoniker,
833 payload: Option<LogsHierarchy>,
834 timestamp: impl Into<Timestamp>,
835 component_url: Option<FlyStr>,
836 severity: impl Into<Severity>,
837 errors: Vec<LogError>,
838 ) -> Self {
839 let errors = if errors.is_empty() { None } else { Some(errors) };
840
841 Data {
842 moniker,
843 version: SCHEMA_VERSION,
844 data_source: DataSource::Logs,
845 payload,
846 metadata: LogsMetadata {
847 timestamp: timestamp.into(),
848 component_url,
849 severity: severity.into(),
850 raw_severity: None,
851 errors,
852 file: None,
853 line: None,
854 pid: None,
855 tags: None,
856 tid: None,
857 dropped: None,
858 size_bytes: None,
859 },
860 }
861 }
862
863 pub fn set_raw_severity(&mut self, raw_severity: u8) {
866 self.metadata.raw_severity = Some(raw_severity);
867 self.metadata.severity = Severity::from(raw_severity);
868 }
869
870 pub fn set_severity(&mut self, severity: Severity) {
872 self.metadata.severity = severity;
873 self.metadata.raw_severity = None;
874 }
875
876 pub fn msg(&self) -> Option<&str> {
878 self.payload_message().as_ref().and_then(|p| {
879 p.properties.iter().find_map(|property| match property {
880 LogsProperty::String(LogsField::MsgStructured, msg) => Some(msg.as_str()),
881 _ => None,
882 })
883 })
884 }
885
886 pub fn msg_mut(&mut self) -> Option<&mut String> {
888 self.payload_message_mut().and_then(|p| {
889 p.properties.iter_mut().find_map(|property| match property {
890 LogsProperty::String(LogsField::MsgStructured, msg) => Some(msg),
891 _ => None,
892 })
893 })
894 }
895
896 pub fn payload_message(&self) -> Option<&DiagnosticsHierarchy<LogsField>> {
898 self.payload
899 .as_ref()
900 .and_then(|p| p.children.iter().find(|property| property.name.as_str() == "message"))
901 }
902
903 pub fn payload_keys(&self) -> Option<&DiagnosticsHierarchy<LogsField>> {
905 self.payload
906 .as_ref()
907 .and_then(|p| p.children.iter().find(|property| property.name.as_str() == "keys"))
908 }
909
910 pub fn metadata(&self) -> &LogsMetadata {
911 &self.metadata
912 }
913
914 pub fn payload_keys_strings(&self) -> Box<dyn Iterator<Item = String> + Send + '_> {
916 let maybe_iter = self.payload_keys().map(|p| {
917 Box::new(p.properties.iter().filter_map(|property| match property {
918 LogsProperty::String(LogsField::Tag, _tag) => None,
919 LogsProperty::String(LogsField::ProcessId, _tag) => None,
920 LogsProperty::String(LogsField::ThreadId, _tag) => None,
921 LogsProperty::String(LogsField::Dropped, _tag) => None,
922 LogsProperty::String(LogsField::Msg, _tag) => None,
923 LogsProperty::String(LogsField::FilePath, _tag) => None,
924 LogsProperty::String(LogsField::LineNumber, _tag) => None,
925 LogsProperty::String(
926 key @ (LogsField::Other(_) | LogsField::MsgStructured),
927 value,
928 ) => Some(format!("{key}={value}")),
929 LogsProperty::Bytes(key @ (LogsField::Other(_) | LogsField::MsgStructured), _) => {
930 Some(format!("{key} = <bytes>"))
931 }
932 LogsProperty::Int(
933 key @ (LogsField::Other(_) | LogsField::MsgStructured),
934 value,
935 ) => Some(format!("{key}={value}")),
936 LogsProperty::Uint(
937 key @ (LogsField::Other(_) | LogsField::MsgStructured),
938 value,
939 ) => Some(format!("{key}={value}")),
940 LogsProperty::Double(
941 key @ (LogsField::Other(_) | LogsField::MsgStructured),
942 value,
943 ) => Some(format!("{key}={value}")),
944 LogsProperty::Bool(
945 key @ (LogsField::Other(_) | LogsField::MsgStructured),
946 value,
947 ) => Some(format!("{key}={value}")),
948 LogsProperty::DoubleArray(
949 key @ (LogsField::Other(_) | LogsField::MsgStructured),
950 value,
951 ) => Some(format!("{key}={value:?}")),
952 LogsProperty::IntArray(
953 key @ (LogsField::Other(_) | LogsField::MsgStructured),
954 value,
955 ) => Some(format!("{key}={value:?}")),
956 LogsProperty::UintArray(
957 key @ (LogsField::Other(_) | LogsField::MsgStructured),
958 value,
959 ) => Some(format!("{key}={value:?}")),
960 LogsProperty::StringList(
961 key @ (LogsField::Other(_) | LogsField::MsgStructured),
962 value,
963 ) => Some(format!("{key}={value:?}")),
964 _ => None,
965 }))
966 });
967 match maybe_iter {
968 Some(i) => Box::new(i),
969 None => Box::new(std::iter::empty()),
970 }
971 }
972
973 pub fn payload_message_mut(&mut self) -> Option<&mut DiagnosticsHierarchy<LogsField>> {
975 self.payload.as_mut().and_then(|p| {
976 p.children.iter_mut().find(|property| property.name.as_str() == "message")
977 })
978 }
979
980 pub fn file_path(&self) -> Option<&str> {
982 self.metadata.file.as_deref()
983 }
984
985 pub fn line_number(&self) -> Option<&u64> {
987 self.metadata.line.as_ref()
988 }
989
990 pub fn pid(&self) -> Option<u64> {
992 self.metadata.pid
993 }
994
995 pub fn tid(&self) -> Option<u64> {
997 self.metadata.tid
998 }
999
1000 pub fn tags(&self) -> Option<&Vec<String>> {
1002 self.metadata.tags.as_ref()
1003 }
1004
1005 pub fn severity(&self) -> Severity {
1007 self.metadata.severity
1008 }
1009
1010 pub fn dropped_logs(&self) -> Option<u64> {
1012 self.metadata.errors.as_ref().and_then(|errors| {
1013 errors.iter().find_map(|e| match e {
1014 LogError::DroppedLogs { count } => Some(*count),
1015 _ => None,
1016 })
1017 })
1018 }
1019
1020 pub fn rolled_out_logs(&self) -> Option<u64> {
1022 self.metadata.errors.as_ref().and_then(|errors| {
1023 errors.iter().find_map(|e| match e {
1024 LogError::RolledOutLogs { count } => Some(*count),
1025 _ => None,
1026 })
1027 })
1028 }
1029
1030 pub fn component_name(&self) -> Cow<'_, str> {
1032 match &self.moniker {
1033 ExtendedMoniker::ComponentManager => {
1034 Cow::Borrowed(EXTENDED_MONIKER_COMPONENT_MANAGER_STR)
1035 }
1036 ExtendedMoniker::ComponentInstance(moniker) => {
1037 if moniker.is_root() {
1038 Cow::Borrowed(ROOT_MONIKER_REPR)
1039 } else {
1040 Cow::Owned(moniker.leaf().unwrap().to_string())
1041 }
1042 }
1043 }
1044 }
1045}
1046
1047#[derive(Clone, Copy, Debug)]
1049pub struct LogTextDisplayOptions {
1050 pub show_full_moniker: bool,
1052
1053 pub show_metadata: bool,
1055
1056 pub show_tags: bool,
1058
1059 pub show_file: bool,
1061
1062 pub color: LogTextColor,
1064
1065 pub time_format: LogTimeDisplayFormat,
1067}
1068
1069impl Default for LogTextDisplayOptions {
1070 fn default() -> Self {
1071 Self {
1072 show_full_moniker: true,
1073 show_metadata: true,
1074 show_tags: true,
1075 show_file: true,
1076 color: Default::default(),
1077 time_format: Default::default(),
1078 }
1079 }
1080}
1081
1082#[derive(Clone, Copy, Debug, Default)]
1084pub enum LogTextColor {
1085 #[default]
1087 None,
1088
1089 BySeverity,
1091
1092 Highlight,
1094}
1095
1096impl LogTextColor {
1097 fn begin_record(&self, f: &mut fmt::Formatter<'_>, severity: Severity) -> fmt::Result {
1098 match self {
1099 LogTextColor::BySeverity => match severity {
1100 Severity::Fatal => {
1101 write!(f, "{}{}", color::Bg(color::Red), color::Fg(color::White))?
1102 }
1103 Severity::Error => write!(f, "{}", color::Fg(color::Red))?,
1104 Severity::Warn => write!(f, "{}", color::Fg(color::Yellow))?,
1105 Severity::Info => (),
1106 Severity::Debug => write!(f, "{}", color::Fg(color::LightBlue))?,
1107 Severity::Trace => write!(f, "{}", color::Fg(color::LightMagenta))?,
1108 },
1109 LogTextColor::Highlight => write!(f, "{}", color::Fg(color::LightYellow))?,
1110 LogTextColor::None => {}
1111 }
1112 Ok(())
1113 }
1114
1115 fn begin_lost_message_counts(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1116 if let LogTextColor::BySeverity = self {
1117 write!(f, "{}", color::Fg(color::Yellow))?;
1119 }
1120 Ok(())
1121 }
1122
1123 fn end_record(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1124 match self {
1125 LogTextColor::BySeverity | LogTextColor::Highlight => write!(f, "{}", style::Reset)?,
1126 LogTextColor::None => {}
1127 };
1128 Ok(())
1129 }
1130}
1131
1132#[derive(Clone, Copy, Debug, PartialEq)]
1134pub enum Timezone {
1135 Local,
1137
1138 Utc,
1140}
1141
1142impl Timezone {
1143 fn format(&self, seconds: i64, rem_nanos: u32) -> impl std::fmt::Display {
1144 const TIMESTAMP_FORMAT: &str = "%Y-%m-%d %H:%M:%S.%3f";
1145 match self {
1146 Timezone::Local => {
1147 Local.timestamp_opt(seconds, rem_nanos).unwrap().format(TIMESTAMP_FORMAT)
1148 }
1149 Timezone::Utc => {
1150 Utc.timestamp_opt(seconds, rem_nanos).unwrap().format(TIMESTAMP_FORMAT)
1151 }
1152 }
1153 }
1154}
1155
1156#[derive(Clone, Copy, Debug, Default)]
1158pub enum LogTimeDisplayFormat {
1159 #[default]
1161 Original,
1162
1163 WallTime {
1165 tz: Timezone,
1167
1168 offset: i64,
1171 },
1172}
1173
1174impl LogTimeDisplayFormat {
1175 fn write_timestamp(&self, f: &mut fmt::Formatter<'_>, time: Timestamp) -> fmt::Result {
1176 const NANOS_IN_SECOND: i64 = 1_000_000_000;
1177
1178 match self {
1179 Self::Original | Self::WallTime { offset: 0, .. } => {
1182 let time: Duration =
1183 Duration::from_nanos(time.into_nanos().try_into().unwrap_or(0));
1184 write!(f, "[{:05}.{:06}]", time.as_secs(), time.as_micros() % MICROS_IN_SEC)?;
1185 }
1186 Self::WallTime { tz, offset } => {
1187 let adjusted = time.into_nanos() + offset;
1188 let seconds = adjusted / NANOS_IN_SECOND;
1189 let rem_nanos = (adjusted % NANOS_IN_SECOND) as u32;
1190 let formatted = tz.format(seconds, rem_nanos);
1191 write!(f, "[{formatted}]")?;
1192 }
1193 }
1194 Ok(())
1195 }
1196}
1197
1198pub struct LogTextPresenter<'a> {
1200 log: &'a Data<Logs>,
1202
1203 options: LogTextDisplayOptions,
1205}
1206
1207impl<'a> LogTextPresenter<'a> {
1208 pub fn new(log: &'a Data<Logs>, options: LogTextDisplayOptions) -> Self {
1212 Self { log, options }
1213 }
1214}
1215
1216impl fmt::Display for Data<Logs> {
1217 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1218 LogTextPresenter::new(self, Default::default()).fmt(f)
1219 }
1220}
1221
1222impl Deref for LogTextPresenter<'_> {
1223 type Target = Data<Logs>;
1224 fn deref(&self) -> &Self::Target {
1225 self.log
1226 }
1227}
1228
1229impl fmt::Display for LogTextPresenter<'_> {
1230 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1231 self.options.color.begin_record(f, self.log.severity())?;
1232 self.options.time_format.write_timestamp(f, self.metadata.timestamp)?;
1233
1234 if self.options.show_metadata {
1235 match self.pid() {
1236 Some(pid) => write!(f, "[{pid}]")?,
1237 None => write!(f, "[]")?,
1238 }
1239 match self.tid() {
1240 Some(tid) => write!(f, "[{tid}]")?,
1241 None => write!(f, "[]")?,
1242 }
1243 }
1244
1245 let moniker = if self.options.show_full_moniker {
1246 match &self.moniker {
1247 ExtendedMoniker::ComponentManager => {
1248 Cow::Borrowed(EXTENDED_MONIKER_COMPONENT_MANAGER_STR)
1249 }
1250 ExtendedMoniker::ComponentInstance(instance) => {
1251 if instance.is_root() {
1252 Cow::Borrowed(ROOT_MONIKER_REPR)
1253 } else {
1254 Cow::Owned(instance.to_string())
1255 }
1256 }
1257 }
1258 } else {
1259 self.component_name()
1260 };
1261 write!(f, "[{moniker}]")?;
1262
1263 if self.options.show_tags {
1264 match &self.metadata.tags {
1265 Some(tags) if !tags.is_empty() => {
1266 let mut filtered =
1267 tags.iter().filter(|tag| *tag != moniker.as_ref()).peekable();
1268 if filtered.peek().is_some() {
1269 write!(f, "[{}]", filtered.join(","))?;
1270 }
1271 }
1272 _ => {}
1273 }
1274 }
1275
1276 write!(f, " {}:", self.metadata.severity)?;
1277
1278 if self.options.show_file {
1279 match (&self.metadata.file, &self.metadata.line) {
1280 (Some(file), Some(line)) => write!(f, " [{file}({line})]")?,
1281 (Some(file), None) => write!(f, " [{file}]")?,
1282 _ => (),
1283 }
1284 }
1285
1286 if let Some(mut msg) = self.msg() {
1287 if let Some(nul) = msg.find("\0") {
1288 msg = &msg[0..nul];
1289 }
1290 write!(f, " {msg}")?;
1291 } else {
1292 write!(f, " <missing message>")?;
1293 }
1294 for kvp in self.payload_keys_strings() {
1295 write!(f, " {kvp}")?;
1296 }
1297
1298 let dropped = self.log.dropped_logs().unwrap_or_default();
1299 let rolled = self.log.rolled_out_logs().unwrap_or_default();
1300 if dropped != 0 || rolled != 0 {
1301 self.options.color.begin_lost_message_counts(f)?;
1302 if dropped != 0 {
1303 write!(f, " [dropped={dropped}]")?;
1304 }
1305 if rolled != 0 {
1306 write!(f, " [rolled={rolled}]")?;
1307 }
1308 }
1309
1310 self.options.color.end_record(f)?;
1311
1312 Ok(())
1313 }
1314}
1315
1316impl Eq for Data<Logs> {}
1317
1318impl PartialOrd for Data<Logs> {
1319 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1320 Some(self.cmp(other))
1321 }
1322}
1323
1324impl Ord for Data<Logs> {
1325 fn cmp(&self, other: &Self) -> Ordering {
1326 self.metadata.timestamp.cmp(&other.metadata.timestamp)
1327 }
1328}
1329
1330#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, PartialOrd, Ord, Serialize)]
1337pub enum LogsField {
1338 ProcessId,
1339 ThreadId,
1340 Dropped,
1341 Tag,
1342 Msg,
1343 MsgStructured,
1344 FilePath,
1345 LineNumber,
1346 Other(String),
1347}
1348
1349impl fmt::Display for LogsField {
1350 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1351 match self {
1352 LogsField::ProcessId => write!(f, "pid"),
1353 LogsField::ThreadId => write!(f, "tid"),
1354 LogsField::Dropped => write!(f, "num_dropped"),
1355 LogsField::Tag => write!(f, "tag"),
1356 LogsField::Msg => write!(f, "message"),
1357 LogsField::MsgStructured => write!(f, "value"),
1358 LogsField::FilePath => write!(f, "file_path"),
1359 LogsField::LineNumber => write!(f, "line_number"),
1360 LogsField::Other(name) => write!(f, "{name}"),
1361 }
1362 }
1363}
1364
1365pub const PID_LABEL: &str = "pid";
1369pub const TID_LABEL: &str = "tid";
1371pub const DROPPED_LABEL: &str = "num_dropped";
1373pub const TAG_LABEL: &str = "tag";
1375pub const MESSAGE_LABEL_STRUCTURED: &str = "value";
1377pub const MESSAGE_LABEL: &str = "message";
1379pub const FILE_PATH_LABEL: &str = "file";
1381pub const LINE_NUMBER_LABEL: &str = "line";
1383
1384impl AsRef<str> for LogsField {
1385 fn as_ref(&self) -> &str {
1386 match self {
1387 Self::ProcessId => PID_LABEL,
1388 Self::ThreadId => TID_LABEL,
1389 Self::Dropped => DROPPED_LABEL,
1390 Self::Tag => TAG_LABEL,
1391 Self::Msg => MESSAGE_LABEL,
1392 Self::FilePath => FILE_PATH_LABEL,
1393 Self::LineNumber => LINE_NUMBER_LABEL,
1394 Self::MsgStructured => MESSAGE_LABEL_STRUCTURED,
1395 Self::Other(str) => str.as_str(),
1396 }
1397 }
1398}
1399
1400impl<T> From<T> for LogsField
1401where
1402 T: Deref<Target = str>,
1404{
1405 fn from(s: T) -> Self {
1406 match s.as_ref() {
1407 PID_LABEL => Self::ProcessId,
1408 TID_LABEL => Self::ThreadId,
1409 DROPPED_LABEL => Self::Dropped,
1410 TAG_LABEL => Self::Tag,
1411 MESSAGE_LABEL => Self::Msg,
1412 FILE_PATH_LABEL => Self::FilePath,
1413 LINE_NUMBER_LABEL => Self::LineNumber,
1414 MESSAGE_LABEL_STRUCTURED => Self::MsgStructured,
1415 _ => Self::Other(s.to_string()),
1416 }
1417 }
1418}
1419
1420impl FromStr for LogsField {
1421 type Err = ();
1422 fn from_str(s: &str) -> Result<Self, Self::Err> {
1423 Ok(Self::from(s))
1424 }
1425}
1426
1427#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
1430#[derive(Clone, Deserialize, Debug, Eq, PartialEq, Serialize)]
1431pub enum LogError {
1432 #[serde(rename = "dropped_logs")]
1435 DroppedLogs { count: u64 },
1436 #[serde(rename = "rolled_out_logs")]
1439 RolledOutLogs { count: u64 },
1440 #[serde(rename = "parse_record")]
1441 FailedToParseRecord(String),
1442 #[serde(rename = "other")]
1443 Other { message: String },
1444}
1445
1446const DROPPED_PAYLOAD_MSG: &str = "Schema failed to fit component budget.";
1447
1448impl MetadataError for LogError {
1449 fn dropped_payload() -> Self {
1450 Self::Other { message: DROPPED_PAYLOAD_MSG.into() }
1451 }
1452
1453 fn message(&self) -> Option<&str> {
1454 match self {
1455 Self::FailedToParseRecord(msg) => Some(msg.as_str()),
1456 Self::Other { message } => Some(message.as_str()),
1457 _ => None,
1458 }
1459 }
1460}
1461
1462#[derive(Debug, PartialEq, Clone, Eq)]
1465pub struct InspectError {
1466 pub message: String,
1467}
1468
1469impl MetadataError for InspectError {
1470 fn dropped_payload() -> Self {
1471 Self { message: "Schema failed to fit component budget.".into() }
1472 }
1473
1474 fn message(&self) -> Option<&str> {
1475 Some(self.message.as_str())
1476 }
1477}
1478
1479impl fmt::Display for InspectError {
1480 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1481 write!(f, "{}", self.message)
1482 }
1483}
1484
1485impl Borrow<str> for InspectError {
1486 fn borrow(&self) -> &str {
1487 &self.message
1488 }
1489}
1490
1491impl Serialize for InspectError {
1492 fn serialize<S: Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
1493 self.message.serialize(ser)
1494 }
1495}
1496
1497impl<'de> Deserialize<'de> for InspectError {
1498 fn deserialize<D>(de: D) -> Result<Self, D::Error>
1499 where
1500 D: Deserializer<'de>,
1501 {
1502 let message = String::deserialize(de)?;
1503 Ok(Self { message })
1504 }
1505}
1506
1507#[cfg(test)]
1508mod tests {
1509 use super::*;
1510 use diagnostics_hierarchy::hierarchy;
1511 use selectors::FastError;
1512 use serde_json::json;
1513 use test_case::test_case;
1514
1515 const TEST_URL: &str = "fuchsia-pkg://test";
1516
1517 #[fuchsia::test]
1518 fn test_canonical_json_inspect_formatting() {
1519 let mut hierarchy = hierarchy! {
1520 root: {
1521 x: "foo",
1522 }
1523 };
1524
1525 hierarchy.sort();
1526 let json_schema = InspectDataBuilder::new(
1527 "a/b/c/d".try_into().unwrap(),
1528 TEST_URL,
1529 Timestamp::from_nanos(123456i64),
1530 )
1531 .with_hierarchy(hierarchy)
1532 .with_name(InspectHandleName::filename("test_file_plz_ignore.inspect"))
1533 .build();
1534
1535 let result_json =
1536 serde_json::to_value(&json_schema).expect("serialization should succeed.");
1537
1538 let expected_json = json!({
1539 "moniker": "a/b/c/d",
1540 "version": 1,
1541 "data_source": "Inspect",
1542 "payload": {
1543 "root": {
1544 "x": "foo"
1545 }
1546 },
1547 "metadata": {
1548 "component_url": TEST_URL,
1549 "filename": "test_file_plz_ignore.inspect",
1550 "timestamp": 123456,
1551 }
1552 });
1553
1554 pretty_assertions::assert_eq!(result_json, expected_json, "golden diff failed.");
1555 }
1556
1557 #[fuchsia::test]
1558 fn test_errorful_json_inspect_formatting() {
1559 let json_schema = InspectDataBuilder::new(
1560 "a/b/c/d".try_into().unwrap(),
1561 TEST_URL,
1562 Timestamp::from_nanos(123456i64),
1563 )
1564 .with_name(InspectHandleName::filename("test_file_plz_ignore.inspect"))
1565 .with_errors(vec![InspectError { message: "too much fun being had.".to_string() }])
1566 .build();
1567
1568 let result_json =
1569 serde_json::to_value(&json_schema).expect("serialization should succeed.");
1570
1571 let expected_json = json!({
1572 "moniker": "a/b/c/d",
1573 "version": 1,
1574 "data_source": "Inspect",
1575 "payload": null,
1576 "metadata": {
1577 "component_url": TEST_URL,
1578 "errors": ["too much fun being had."],
1579 "filename": "test_file_plz_ignore.inspect",
1580 "timestamp": 123456,
1581 }
1582 });
1583
1584 pretty_assertions::assert_eq!(result_json, expected_json, "golden diff failed.");
1585 }
1586
1587 fn parse_selectors(strings: Vec<&str>) -> Vec<Selector> {
1588 strings
1589 .iter()
1590 .map(|s| match selectors::parse_selector::<FastError>(s) {
1591 Ok(selector) => selector,
1592 Err(e) => panic!("Couldn't parse selector {s}: {e}"),
1593 })
1594 .collect::<Vec<_>>()
1595 }
1596
1597 #[fuchsia::test]
1598 fn test_filter_returns_none_on_empty_hierarchy() {
1599 let data = InspectDataBuilder::new(
1600 "a/b/c/d".try_into().unwrap(),
1601 TEST_URL,
1602 Timestamp::from_nanos(123456i64),
1603 )
1604 .build();
1605 let selectors = parse_selectors(vec!["a/b/c/d:foo"]);
1606 assert_eq!(data.filter(&selectors).expect("Filter OK"), None);
1607 }
1608
1609 #[fuchsia::test]
1610 fn test_filter_returns_none_on_selector_mismatch() {
1611 let mut hierarchy = hierarchy! {
1612 root: {
1613 x: "foo",
1614 }
1615 };
1616 hierarchy.sort();
1617 let data = InspectDataBuilder::new(
1618 "b/c/d/e".try_into().unwrap(),
1619 TEST_URL,
1620 Timestamp::from_nanos(123456i64),
1621 )
1622 .with_hierarchy(hierarchy)
1623 .build();
1624 let selectors = parse_selectors(vec!["a/b/c/d:foo"]);
1625 assert_eq!(data.filter(&selectors).expect("Filter OK"), None);
1626 }
1627
1628 #[fuchsia::test]
1629 fn test_filter_returns_none_on_data_mismatch() {
1630 let mut hierarchy = hierarchy! {
1631 root: {
1632 x: "foo",
1633 }
1634 };
1635 hierarchy.sort();
1636 let data = InspectDataBuilder::new(
1637 "a/b/c/d".try_into().unwrap(),
1638 TEST_URL,
1639 Timestamp::from_nanos(123456i64),
1640 )
1641 .with_hierarchy(hierarchy)
1642 .build();
1643 let selectors = parse_selectors(vec!["a/b/c/d:foo"]);
1644
1645 assert_eq!(data.filter(&selectors).expect("FIlter OK"), None);
1646 }
1647
1648 #[fuchsia::test]
1649 fn test_filter_returns_matching_data() {
1650 let mut hierarchy = hierarchy! {
1651 root: {
1652 x: "foo",
1653 y: "bar",
1654 }
1655 };
1656 hierarchy.sort();
1657 let data = InspectDataBuilder::new(
1658 "a/b/c/d".try_into().unwrap(),
1659 TEST_URL,
1660 Timestamp::from_nanos(123456i64),
1661 )
1662 .with_name(InspectHandleName::filename("test_file_plz_ignore.inspect"))
1663 .with_hierarchy(hierarchy)
1664 .build();
1665 let selectors = parse_selectors(vec!["a/b/c/d:root:x"]);
1666
1667 let expected_json = json!({
1668 "moniker": "a/b/c/d",
1669 "version": 1,
1670 "data_source": "Inspect",
1671 "payload": {
1672 "root": {
1673 "x": "foo"
1674 }
1675 },
1676 "metadata": {
1677 "component_url": TEST_URL,
1678 "filename": "test_file_plz_ignore.inspect",
1679 "timestamp": 123456,
1680 }
1681 });
1682
1683 let result_json = serde_json::to_value(data.filter(&selectors).expect("Filter Ok"))
1684 .expect("serialization should succeed.");
1685
1686 pretty_assertions::assert_eq!(result_json, expected_json, "golden diff failed.");
1687 }
1688
1689 #[fuchsia::test]
1690 fn default_builder_test() {
1691 let builder = LogsDataBuilder::new(BuilderArgs {
1692 component_url: Some("url".into()),
1693 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
1694 severity: Severity::Info,
1695 timestamp: Timestamp::from_nanos(0),
1696 });
1697 let expected_json = json!({
1699 "moniker": "moniker",
1700 "version": 1,
1701 "data_source": "Logs",
1702 "payload": {
1703 "root":
1704 {
1705 "message":{}
1706 }
1707 },
1708 "metadata": {
1709 "component_url": "url",
1710 "severity": "INFO",
1711 "tags": [],
1712
1713 "timestamp": 0,
1714 }
1715 });
1716 let result_json =
1717 serde_json::to_value(builder.build()).expect("serialization should succeed.");
1718 pretty_assertions::assert_eq!(result_json, expected_json, "golden diff failed.");
1719 }
1720
1721 #[fuchsia::test]
1722 fn regular_message_test() {
1723 let builder = LogsDataBuilder::new(BuilderArgs {
1724 component_url: Some("url".into()),
1725 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
1726 severity: Severity::Info,
1727 timestamp: Timestamp::from_nanos(0),
1728 })
1729 .set_message("app")
1730 .set_file("test file.cc")
1731 .set_line(420)
1732 .set_pid(1001)
1733 .set_tid(200)
1734 .set_dropped(2)
1735 .add_tag("You're")
1736 .add_tag("IT!")
1737 .add_key(LogsProperty::String(LogsField::Other("key".to_string()), "value".to_string()));
1738 let expected_json = json!({
1740 "moniker": "moniker",
1741 "version": 1,
1742 "data_source": "Logs",
1743 "payload": {
1744 "root":
1745 {
1746 "keys":{
1747 "key":"value"
1748 },
1749 "message":{
1750 "value":"app"
1751 }
1752 }
1753 },
1754 "metadata": {
1755 "errors": [],
1756 "component_url": "url",
1757 "errors": [{"dropped_logs":{"count":2}}],
1758 "file": "test file.cc",
1759 "line": 420,
1760 "pid": 1001,
1761 "severity": "INFO",
1762 "tags": ["You're", "IT!"],
1763 "tid": 200,
1764
1765 "timestamp": 0,
1766 }
1767 });
1768 let result_json =
1769 serde_json::to_value(builder.build()).expect("serialization should succeed.");
1770 pretty_assertions::assert_eq!(result_json, expected_json, "golden diff failed.");
1771 }
1772
1773 #[fuchsia::test]
1774 fn display_for_logs() {
1775 let data = LogsDataBuilder::new(BuilderArgs {
1776 timestamp: Timestamp::from_nanos(12345678000i64),
1777 component_url: Some(FlyStr::from("fake-url")),
1778 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
1779 severity: Severity::Info,
1780 })
1781 .set_pid(123)
1782 .set_tid(456)
1783 .set_message("some message".to_string())
1784 .set_file("some_file.cc".to_string())
1785 .set_line(420)
1786 .add_tag("foo")
1787 .add_tag("bar")
1788 .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
1789 .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
1790 .build();
1791
1792 assert_eq!(
1793 "[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test",
1794 format!("{data}")
1795 )
1796 }
1797
1798 #[fuchsia::test]
1799 fn display_for_logs_with_duplicate_moniker() {
1800 let data = LogsDataBuilder::new(BuilderArgs {
1801 timestamp: Timestamp::from_nanos(12345678000i64),
1802 component_url: Some(FlyStr::from("fake-url")),
1803 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
1804 severity: Severity::Info,
1805 })
1806 .set_pid(123)
1807 .set_tid(456)
1808 .set_message("some message".to_string())
1809 .set_file("some_file.cc".to_string())
1810 .set_line(420)
1811 .add_tag("moniker")
1812 .add_tag("bar")
1813 .add_tag("moniker")
1814 .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
1815 .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
1816 .build();
1817
1818 assert_eq!(
1819 "[00012.345678][123][456][moniker][bar] INFO: [some_file.cc(420)] some message test=property value=test",
1820 format!("{data}")
1821 )
1822 }
1823
1824 #[fuchsia::test]
1825 fn display_for_logs_with_duplicate_moniker_and_no_other_tags() {
1826 let data = LogsDataBuilder::new(BuilderArgs {
1827 timestamp: Timestamp::from_nanos(12345678000i64),
1828 component_url: Some(FlyStr::from("fake-url")),
1829 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
1830 severity: Severity::Info,
1831 })
1832 .set_pid(123)
1833 .set_tid(456)
1834 .set_message("some message".to_string())
1835 .set_file("some_file.cc".to_string())
1836 .set_line(420)
1837 .add_tag("moniker")
1838 .add_tag("moniker")
1839 .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
1840 .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
1841 .build();
1842
1843 assert_eq!(
1844 "[00012.345678][123][456][moniker] INFO: [some_file.cc(420)] some message test=property value=test",
1845 format!("{data}")
1846 )
1847 }
1848
1849 #[fuchsia::test]
1850 fn display_for_logs_partial_moniker() {
1851 let data = LogsDataBuilder::new(BuilderArgs {
1852 timestamp: Timestamp::from_nanos(12345678000i64),
1853 component_url: Some(FlyStr::from("fake-url")),
1854 moniker: ExtendedMoniker::parse_str("test/moniker").unwrap(),
1855 severity: Severity::Info,
1856 })
1857 .set_pid(123)
1858 .set_tid(456)
1859 .set_message("some message".to_string())
1860 .set_file("some_file.cc".to_string())
1861 .set_line(420)
1862 .add_tag("foo")
1863 .add_tag("bar")
1864 .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
1865 .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
1866 .build();
1867
1868 assert_eq!(
1869 "[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test",
1870 format!(
1871 "{}",
1872 LogTextPresenter::new(
1873 &data,
1874 LogTextDisplayOptions { show_full_moniker: false, ..Default::default() }
1875 )
1876 )
1877 )
1878 }
1879
1880 #[fuchsia::test]
1881 fn display_for_logs_exclude_metadata() {
1882 let data = LogsDataBuilder::new(BuilderArgs {
1883 timestamp: Timestamp::from_nanos(12345678000i64),
1884 component_url: Some(FlyStr::from("fake-url")),
1885 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
1886 severity: Severity::Info,
1887 })
1888 .set_pid(123)
1889 .set_tid(456)
1890 .set_message("some message".to_string())
1891 .set_file("some_file.cc".to_string())
1892 .set_line(420)
1893 .add_tag("foo")
1894 .add_tag("bar")
1895 .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
1896 .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
1897 .build();
1898
1899 assert_eq!(
1900 "[00012.345678][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test",
1901 format!(
1902 "{}",
1903 LogTextPresenter::new(
1904 &data,
1905 LogTextDisplayOptions { show_metadata: false, ..Default::default() }
1906 )
1907 )
1908 )
1909 }
1910
1911 #[fuchsia::test]
1912 fn display_for_logs_exclude_tags() {
1913 let data = LogsDataBuilder::new(BuilderArgs {
1914 timestamp: Timestamp::from_nanos(12345678000i64),
1915 component_url: Some(FlyStr::from("fake-url")),
1916 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
1917 severity: Severity::Info,
1918 })
1919 .set_pid(123)
1920 .set_tid(456)
1921 .set_message("some message".to_string())
1922 .set_file("some_file.cc".to_string())
1923 .set_line(420)
1924 .add_tag("foo")
1925 .add_tag("bar")
1926 .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
1927 .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
1928 .build();
1929
1930 assert_eq!(
1931 "[00012.345678][123][456][moniker] INFO: [some_file.cc(420)] some message test=property value=test",
1932 format!(
1933 "{}",
1934 LogTextPresenter::new(
1935 &data,
1936 LogTextDisplayOptions { show_tags: false, ..Default::default() }
1937 )
1938 )
1939 )
1940 }
1941
1942 #[fuchsia::test]
1943 fn display_for_logs_exclude_file() {
1944 let data = LogsDataBuilder::new(BuilderArgs {
1945 timestamp: Timestamp::from_nanos(12345678000i64),
1946 component_url: Some(FlyStr::from("fake-url")),
1947 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
1948 severity: Severity::Info,
1949 })
1950 .set_pid(123)
1951 .set_tid(456)
1952 .set_message("some message".to_string())
1953 .set_file("some_file.cc".to_string())
1954 .set_line(420)
1955 .add_tag("foo")
1956 .add_tag("bar")
1957 .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
1958 .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
1959 .build();
1960
1961 assert_eq!(
1962 "[00012.345678][123][456][moniker][foo,bar] INFO: some message test=property value=test",
1963 format!(
1964 "{}",
1965 LogTextPresenter::new(
1966 &data,
1967 LogTextDisplayOptions { show_file: false, ..Default::default() }
1968 )
1969 )
1970 )
1971 }
1972
1973 #[fuchsia::test]
1974 fn display_for_logs_include_color_by_severity() {
1975 let data = LogsDataBuilder::new(BuilderArgs {
1976 timestamp: Timestamp::from_nanos(12345678000i64),
1977 component_url: Some(FlyStr::from("fake-url")),
1978 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
1979 severity: Severity::Error,
1980 })
1981 .set_pid(123)
1982 .set_tid(456)
1983 .set_message("some message".to_string())
1984 .set_file("some_file.cc".to_string())
1985 .set_line(420)
1986 .add_tag("foo")
1987 .add_tag("bar")
1988 .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
1989 .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
1990 .build();
1991
1992 assert_eq!(
1993 format!(
1994 "{}[00012.345678][123][456][moniker][foo,bar] ERROR: [some_file.cc(420)] some message test=property value=test{}",
1995 color::Fg(color::Red),
1996 style::Reset
1997 ),
1998 format!(
1999 "{}",
2000 LogTextPresenter::new(
2001 &data,
2002 LogTextDisplayOptions { color: LogTextColor::BySeverity, ..Default::default() }
2003 )
2004 )
2005 )
2006 }
2007
2008 #[fuchsia::test]
2009 fn display_for_logs_highlight_line() {
2010 let data = LogsDataBuilder::new(BuilderArgs {
2011 timestamp: Timestamp::from_nanos(12345678000i64),
2012 component_url: Some(FlyStr::from("fake-url")),
2013 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
2014 severity: Severity::Info,
2015 })
2016 .set_pid(123)
2017 .set_tid(456)
2018 .set_message("some message".to_string())
2019 .set_file("some_file.cc".to_string())
2020 .set_line(420)
2021 .add_tag("foo")
2022 .add_tag("bar")
2023 .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
2024 .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
2025 .build();
2026
2027 assert_eq!(
2028 format!(
2029 "{}[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test{}",
2030 color::Fg(color::LightYellow),
2031 style::Reset
2032 ),
2033 LogTextPresenter::new(
2034 &data,
2035 LogTextDisplayOptions { color: LogTextColor::Highlight, ..Default::default() }
2036 )
2037 .to_string()
2038 )
2039 }
2040
2041 #[fuchsia::test]
2042 fn display_for_logs_with_wall_time() {
2043 let data = LogsDataBuilder::new(BuilderArgs {
2044 timestamp: Timestamp::from_nanos(12345678000i64),
2045 component_url: Some(FlyStr::from("fake-url")),
2046 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
2047 severity: Severity::Info,
2048 })
2049 .set_pid(123)
2050 .set_tid(456)
2051 .set_message("some message".to_string())
2052 .set_file("some_file.cc".to_string())
2053 .set_line(420)
2054 .add_tag("foo")
2055 .add_tag("bar")
2056 .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
2057 .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
2058 .build();
2059
2060 assert_eq!(
2061 "[1970-01-01 00:00:12.345][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test",
2062 LogTextPresenter::new(
2063 &data,
2064 LogTextDisplayOptions {
2065 time_format: LogTimeDisplayFormat::WallTime { tz: Timezone::Utc, offset: 1 },
2066 ..Default::default()
2067 }
2068 )
2069 .to_string()
2070 );
2071
2072 assert_eq!(
2073 "[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test",
2074 LogTextPresenter::new(
2075 &data,
2076 LogTextDisplayOptions {
2077 time_format: LogTimeDisplayFormat::WallTime { tz: Timezone::Utc, offset: 0 },
2078 ..Default::default()
2079 }
2080 )
2081 .to_string(),
2082 "should fall back to monotonic if offset is 0"
2083 );
2084 }
2085
2086 #[fuchsia::test]
2087 fn display_for_logs_with_dropped_count() {
2088 let data = LogsDataBuilder::new(BuilderArgs {
2089 timestamp: Timestamp::from_nanos(12345678000i64),
2090 component_url: Some(FlyStr::from("fake-url")),
2091 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
2092 severity: Severity::Info,
2093 })
2094 .set_dropped(5)
2095 .set_pid(123)
2096 .set_tid(456)
2097 .set_message("some message".to_string())
2098 .set_file("some_file.cc".to_string())
2099 .set_line(420)
2100 .add_tag("foo")
2101 .add_tag("bar")
2102 .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
2103 .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
2104 .build();
2105
2106 assert_eq!(
2107 "[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test [dropped=5]",
2108 format!("{}", LogTextPresenter::new(&data, LogTextDisplayOptions::default())),
2109 );
2110
2111 assert_eq!(
2112 format!(
2113 "[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test{} [dropped=5]{}",
2114 color::Fg(color::Yellow),
2115 style::Reset
2116 ),
2117 LogTextPresenter::new(
2118 &data,
2119 LogTextDisplayOptions { color: LogTextColor::BySeverity, ..Default::default() }
2120 )
2121 .to_string()
2122 );
2123 }
2124
2125 #[fuchsia::test]
2126 fn display_for_logs_with_rolled_count() {
2127 let data = LogsDataBuilder::new(BuilderArgs {
2128 timestamp: Timestamp::from_nanos(12345678000i64),
2129 component_url: Some(FlyStr::from("fake-url")),
2130 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
2131 severity: Severity::Info,
2132 })
2133 .set_rolled_out(10)
2134 .set_pid(123)
2135 .set_tid(456)
2136 .set_message("some message".to_string())
2137 .set_file("some_file.cc".to_string())
2138 .set_line(420)
2139 .add_tag("foo")
2140 .add_tag("bar")
2141 .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
2142 .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
2143 .build();
2144
2145 assert_eq!(
2146 "[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test [rolled=10]",
2147 format!("{}", LogTextPresenter::new(&data, LogTextDisplayOptions::default())),
2148 );
2149
2150 assert_eq!(
2151 format!(
2152 "[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test{} [rolled=10]{}",
2153 color::Fg(color::Yellow),
2154 style::Reset
2155 ),
2156 LogTextPresenter::new(
2157 &data,
2158 LogTextDisplayOptions { color: LogTextColor::BySeverity, ..Default::default() }
2159 )
2160 .to_string()
2161 );
2162 }
2163
2164 #[fuchsia::test]
2165 fn display_for_logs_with_dropped_and_rolled_counts() {
2166 let data = LogsDataBuilder::new(BuilderArgs {
2167 timestamp: Timestamp::from_nanos(12345678000i64),
2168 component_url: Some(FlyStr::from("fake-url")),
2169 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
2170 severity: Severity::Info,
2171 })
2172 .set_dropped(5)
2173 .set_rolled_out(10)
2174 .set_pid(123)
2175 .set_tid(456)
2176 .set_message("some message".to_string())
2177 .set_file("some_file.cc".to_string())
2178 .set_line(420)
2179 .add_tag("foo")
2180 .add_tag("bar")
2181 .add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
2182 .add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
2183 .build();
2184
2185 assert_eq!(
2186 "[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test [dropped=5] [rolled=10]",
2187 format!("{}", LogTextPresenter::new(&data, LogTextDisplayOptions::default())),
2188 );
2189
2190 assert_eq!(
2191 format!(
2192 "[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test{} [dropped=5] [rolled=10]{}",
2193 color::Fg(color::Yellow),
2194 style::Reset
2195 ),
2196 LogTextPresenter::new(
2197 &data,
2198 LogTextDisplayOptions { color: LogTextColor::BySeverity, ..Default::default() }
2199 )
2200 .to_string()
2201 );
2202 }
2203
2204 #[fuchsia::test]
2205 fn display_for_logs_no_tags() {
2206 let data = LogsDataBuilder::new(BuilderArgs {
2207 timestamp: Timestamp::from_nanos(12345678000i64),
2208 component_url: Some(FlyStr::from("fake-url")),
2209 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
2210 severity: Severity::Info,
2211 })
2212 .set_pid(123)
2213 .set_tid(456)
2214 .set_message("some message".to_string())
2215 .build();
2216
2217 assert_eq!("[00012.345678][123][456][moniker] INFO: some message", format!("{data}"))
2218 }
2219
2220 #[fuchsia::test]
2221 fn size_bytes_deserialize_backwards_compatibility() {
2222 let original_json = json!({
2223 "moniker": "a/b",
2224 "version": 1,
2225 "data_source": "Logs",
2226 "payload": {
2227 "root": {
2228 "message":{}
2229 }
2230 },
2231 "metadata": {
2232 "component_url": "url",
2233 "severity": "INFO",
2234 "tags": [],
2235
2236 "timestamp": 123,
2237 }
2238 });
2239 let expected_data = LogsDataBuilder::new(BuilderArgs {
2240 component_url: Some("url".into()),
2241 moniker: ExtendedMoniker::parse_str("a/b").unwrap(),
2242 severity: Severity::Info,
2243 timestamp: Timestamp::from_nanos(123),
2244 })
2245 .build();
2246 let original_data: LogsData = serde_json::from_value(original_json).unwrap();
2247 assert_eq!(original_data, expected_data);
2248 assert_eq!(original_data.metadata.size_bytes, None);
2250 }
2251
2252 #[fuchsia::test]
2253 fn display_for_logs_with_null_terminator() {
2254 let data = LogsDataBuilder::new(BuilderArgs {
2255 timestamp: Timestamp::from_nanos(12345678000i64),
2256 component_url: Some(FlyStr::from("fake-url")),
2257 moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
2258 severity: Severity::Info,
2259 })
2260 .set_pid(123)
2261 .set_tid(456)
2262 .set_message("some message\0garbage".to_string())
2263 .set_file("some_file.cc".to_string())
2264 .set_line(420)
2265 .add_tag("foo")
2266 .add_tag("bar")
2267 .build();
2268
2269 assert_eq!(
2270 "[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message",
2271 format!("{data}")
2272 )
2273 }
2274
2275 #[fuchsia::test]
2276 fn dropped_deserialize_backwards_compatibility() {
2277 let original_json = json!({
2278 "moniker": "a/b",
2279 "version": 1,
2280 "data_source": "Logs",
2281 "payload": {
2282 "root": {
2283 "message":{}
2284 }
2285 },
2286 "metadata": {
2287 "dropped": 0,
2288 "component_url": "url",
2289 "severity": "INFO",
2290 "tags": [],
2291
2292 "timestamp": 123,
2293 }
2294 });
2295 let expected_data = LogsDataBuilder::new(BuilderArgs {
2296 component_url: Some("url".into()),
2297 moniker: ExtendedMoniker::parse_str("a/b").unwrap(),
2298 severity: Severity::Info,
2299 timestamp: Timestamp::from_nanos(123),
2300 })
2301 .build();
2302 let original_data: LogsData = serde_json::from_value(original_json).unwrap();
2303 assert_eq!(original_data, expected_data);
2304 assert_eq!(original_data.metadata.dropped, None);
2306 }
2307
2308 #[fuchsia::test]
2309 fn severity_aliases() {
2310 assert_eq!(Severity::from_str("warn").unwrap(), Severity::Warn);
2311 assert_eq!(Severity::from_str("warning").unwrap(), Severity::Warn);
2312 }
2313
2314 #[fuchsia::test]
2315 fn test_metadata_merge() {
2316 let mut meta = InspectMetadata {
2317 errors: Some(vec![InspectError { message: "error1".to_string() }]),
2318 name: InspectHandleName::name("test"),
2319 component_url: "fuchsia-pkg://test".into(),
2320 timestamp: Timestamp::from_nanos(100),
2321 escrowed: false,
2322 };
2323
2324 meta.merge(InspectMetadata {
2325 errors: Some(vec![InspectError { message: "error2".to_string() }]),
2326 name: InspectHandleName::name("test"),
2327 component_url: "fuchsia-pkg://test".into(),
2328 timestamp: Timestamp::from_nanos(200),
2329 escrowed: false,
2330 });
2331
2332 assert_eq!(
2333 meta,
2334 InspectMetadata {
2335 errors: Some(vec![
2336 InspectError { message: "error1".to_string() },
2337 InspectError { message: "error2".to_string() },
2338 ]),
2339 name: InspectHandleName::name("test"),
2340 component_url: "fuchsia-pkg://test".into(),
2341 timestamp: Timestamp::from_nanos(200),
2342 escrowed: false,
2343 }
2344 );
2345 }
2346
2347 #[fuchsia::test]
2348 fn test_metadata_merge_older_timestamp_noop() {
2349 let mut meta = InspectMetadata {
2350 errors: None,
2351 name: InspectHandleName::name("test"),
2352 component_url: TEST_URL.into(),
2353 timestamp: Timestamp::from_nanos(200),
2354 escrowed: false,
2355 };
2356 meta.merge(InspectMetadata {
2357 errors: None,
2358 name: InspectHandleName::name("test"),
2359 component_url: TEST_URL.into(),
2360 timestamp: Timestamp::from_nanos(100),
2361 escrowed: false,
2362 });
2363 assert_eq!(
2364 meta,
2365 InspectMetadata {
2366 errors: None,
2367 name: InspectHandleName::name("test"),
2368 component_url: TEST_URL.into(),
2369 timestamp: Timestamp::from_nanos(200),
2370 escrowed: false,
2371 }
2372 );
2373 }
2374
2375 fn new_test_data(moniker: &str, payload_val: Option<&str>, timestamp: i64) -> InspectData {
2376 let mut builder = InspectDataBuilder::new(
2377 moniker.try_into().unwrap(),
2378 TEST_URL,
2379 Timestamp::from_nanos(timestamp),
2380 );
2381 if let Some(val) = payload_val {
2382 builder = builder.with_hierarchy(hierarchy! { root: { "key": val } });
2383 }
2384 builder.build()
2385 }
2386
2387 #[fuchsia::test]
2388 fn test_data_merge() {
2389 let mut data = new_test_data("a/b/c", Some("val1"), 100);
2390 let mut other = new_test_data("a/b/c", Some("val2"), 200);
2391 other.metadata.errors = Some(vec![InspectError { message: "error".into() }]);
2392
2393 data.merge(other);
2394
2395 let expected_payload = hierarchy! { root: { "key": "val2" } };
2396 assert_eq!(data.payload, Some(expected_payload));
2397 assert_eq!(data.metadata.timestamp, Timestamp::from_nanos(200));
2398 assert_eq!(data.metadata.errors, Some(vec![InspectError { message: "error".into() }]));
2399 }
2400
2401 #[test_case(new_test_data("a/b/d", Some("v2"), 100); "different moniker")]
2402 #[test_case(
2403 {
2404 let mut d = new_test_data("a/b/c", Some("v2"), 100);
2405 d.version = 2;
2406 d
2407 }; "different version")]
2408 #[test_case(
2409 {
2410 let mut d = new_test_data("a/b/c", Some("v2"), 100);
2411 d.data_source = DataSource::Logs;
2412 d
2413 }; "different data source")]
2414 #[fuchsia::test]
2415 fn test_data_merge_noop(other: InspectData) {
2416 let mut data = new_test_data("a/b/c", Some("v1"), 100);
2417 let original = data.clone();
2418 data.merge(other);
2419 assert_eq!(data, original);
2420 }
2421
2422 #[test_case(None, Some("val2"), Some("val2") ; "none_with_some")]
2423 #[test_case(Some("val1"), None, Some("val1") ; "some_with_none")]
2424 #[test_case(Some("val1"), Some("val2"), Some("val2") ; "some_with_some")]
2425 #[fuchsia::test]
2426 fn test_data_merge_payloads(
2427 payload: Option<&str>,
2428 other_payload: Option<&str>,
2429 expected: Option<&str>,
2430 ) {
2431 let mut data = new_test_data("a/b/c", payload, 100);
2432 let other = new_test_data("a/b/c", other_payload, 100);
2433
2434 data.merge(other);
2435 assert_eq!(data, new_test_data("a/b/c", expected, 100));
2436 }
2437}