Skip to main content

diagnostics_hierarchy/serialization/
deserialize.rs

1// Copyright 2020 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use crate::{ArrayContent, DiagnosticsHierarchy, ExponentialHistogram, LinearHistogram, Property};
6use base64::engine::Engine as _;
7use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
8use serde::de::{self, MapAccess, SeqAccess, Visitor};
9use serde::{Deserialize, Deserializer};
10use std::collections::HashMap;
11use std::fmt;
12use std::hash::Hash;
13use std::marker::PhantomData;
14use std::str::FromStr;
15
16#[cfg(feature = "json_schema")]
17use schemars::Schema;
18
19struct RootVisitor<Key> {
20    // Key is unused.
21    marker: PhantomData<Key>,
22}
23
24impl<'de, Key> Visitor<'de> for RootVisitor<Key>
25where
26    Key: FromStr + Clone + Hash + Eq + AsRef<str>,
27{
28    type Value = DiagnosticsHierarchy<Key>;
29
30    fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
31        formatter.write_str("there should be a single root")
32    }
33
34    fn visit_map<V>(self, mut map: V) -> Result<DiagnosticsHierarchy<Key>, V::Error>
35    where
36        V: MapAccess<'de>,
37    {
38        let result = match map.next_entry::<String, FieldValue<Key>>()? {
39            Some((map_key, value)) => {
40                let key = Key::from_str(&map_key)
41                    .map_err(|_| de::Error::custom("failed to parse key"))?;
42                value.into_node(&key)
43            }
44            None => return Err(de::Error::invalid_length(0, &"expected a root node")),
45        };
46
47        let mut found = 1;
48        while map.next_key::<String>()?.is_some() {
49            found += 1;
50        }
51
52        if found > 1 {
53            return Err(de::Error::invalid_length(found, &"expected a single root"));
54        }
55
56        result.ok_or_else(|| de::Error::custom("expected node for root"))
57    }
58}
59
60impl<'de, Key> Deserialize<'de> for DiagnosticsHierarchy<Key>
61where
62    Key: FromStr + Clone + Hash + Eq + AsRef<str>,
63{
64    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
65    where
66        D: Deserializer<'de>,
67    {
68        deserializer.deserialize_map(RootVisitor { marker: PhantomData })
69    }
70}
71
72trait IntoProperty<Key> {
73    fn into_property(self, key: &Key) -> Option<Property<Key>>;
74}
75
76/// The value of an inspect tree field (node or property).
77enum FieldValue<Key> {
78    String(String),
79    Bytes(Vec<u8>),
80    Int(i64),
81    Uint(u64),
82    Double(f64),
83    Bool(bool),
84    Array(Vec<NumericValue>),
85    LinearIntHistogram(LinearHistogram<i64>),
86    LinearUintHistogram(LinearHistogram<u64>),
87    LinearDoubleHistogram(LinearHistogram<f64>),
88    ExponentialIntHistogram(ExponentialHistogram<i64>),
89    ExponentialUintHistogram(ExponentialHistogram<u64>),
90    ExponentialDoubleHistogram(ExponentialHistogram<f64>),
91    Node(HashMap<Key, FieldValue<Key>>),
92    StringList(Vec<String>),
93}
94
95impl<Key: Clone> IntoProperty<Key> for FieldValue<Key> {
96    fn into_property(self, key: &Key) -> Option<Property<Key>> {
97        match self {
98            Self::String(value) => Some(Property::String(key.clone(), value)),
99            Self::Bytes(value) => Some(Property::Bytes(key.clone(), value)),
100            Self::Int(value) => Some(Property::Int(key.clone(), value)),
101            Self::Uint(value) => Some(Property::Uint(key.clone(), value)),
102            Self::Double(value) => Some(Property::Double(key.clone(), value)),
103            Self::Bool(value) => Some(Property::Bool(key.clone(), value)),
104            Self::Array(values) => values.into_property(key),
105            Self::ExponentialIntHistogram(histogram) => {
106                Some(Property::IntArray(key.clone(), ArrayContent::ExponentialHistogram(histogram)))
107            }
108            Self::ExponentialUintHistogram(histogram) => Some(Property::UintArray(
109                key.clone(),
110                ArrayContent::ExponentialHistogram(histogram),
111            )),
112            Self::ExponentialDoubleHistogram(histogram) => Some(Property::DoubleArray(
113                key.clone(),
114                ArrayContent::ExponentialHistogram(histogram),
115            )),
116            Self::LinearIntHistogram(histogram) => {
117                Some(Property::IntArray(key.clone(), ArrayContent::LinearHistogram(histogram)))
118            }
119            Self::LinearUintHistogram(histogram) => {
120                Some(Property::UintArray(key.clone(), ArrayContent::LinearHistogram(histogram)))
121            }
122            Self::LinearDoubleHistogram(histogram) => {
123                Some(Property::DoubleArray(key.clone(), ArrayContent::LinearHistogram(histogram)))
124            }
125            Self::StringList(list) => Some(Property::StringList(key.clone(), list)),
126            Self::Node(_) => None,
127        }
128    }
129}
130
131impl<Key: AsRef<str> + Clone> FieldValue<Key> {
132    fn is_property(&self) -> bool {
133        !matches!(self, Self::Node(_))
134    }
135
136    fn into_node(self, key: &Key) -> Option<DiagnosticsHierarchy<Key>> {
137        match self {
138            Self::Node(map) => {
139                let mut properties = vec![];
140                let mut children = vec![];
141                for (map_key, value) in map {
142                    if value.is_property() {
143                        properties.push(value.into_property(&map_key).unwrap());
144                    } else {
145                        children.push(value.into_node(&map_key).unwrap());
146                    }
147                }
148                Some(DiagnosticsHierarchy::new(key.as_ref(), properties, children))
149            }
150            _ => None,
151        }
152    }
153}
154
155impl<'de, Key> Deserialize<'de> for FieldValue<Key>
156where
157    Key: FromStr + Hash + Eq,
158{
159    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
160    where
161        D: Deserializer<'de>,
162    {
163        deserializer.deserialize_any(FieldVisitor { marker: PhantomData })
164    }
165}
166
167struct FieldVisitor<Key> {
168    marker: PhantomData<Key>,
169}
170
171// Histogram type may be: linear or exponential; double, int, or uint.
172// If it's linear, step will be Some and initial_step and step_multiplier will be None.
173// if it's exponential, step will be None and initial_step and step_multiplier will be Some.
174// In all cases, size must be usize (u64).
175// indexes will be either None or Some(Vec<NumericValue>) which must be convertible to Vec<usize>.
176// counts will be Vec<NumericValue>.
177// If the histogram is double, every number must be f64 and counts must convert to Vec<f64>.
178// If it's not double, the numbers will be some mix of i64 and u64. If even one can't fit in
179// an i64, then the histogram must be represented as u64 and none of the numbers can be
180// negative.
181// The deserializer will have used i64 for any number that fits, and we can assume most
182// histograms will fit in i64, so first we check if all numbers are i64 and counts can convert
183// to Vec<i64>. If not, then we have to check if a mix of i64 and u64 can all be u64.
184
185fn value_as_u64<Key>(value: &FieldValue<Key>) -> Option<u64> {
186    match value {
187        FieldValue::Uint(value) => Some(*value),
188        FieldValue::Int(value) => u64::try_from(*value).ok(),
189        _ => None,
190    }
191}
192
193fn value_as_i64<Key>(value: &FieldValue<Key>) -> Option<i64> {
194    match value {
195        FieldValue::Int(value) => Some(*value),
196        FieldValue::Uint(value) => i64::try_from(*value).ok(),
197        _ => None,
198    }
199}
200
201// Try to convert indexes to Option<Vec<usize>> and declared_len to usize, and check consistency of
202// several sizes. Return Err(()) if anything goes wrong, and the converted (indexes, declared_len)
203// if it's all good,
204fn sanitize_histogram_parameters<Key>(
205    indexes: Option<&FieldValue<Key>>,
206    n_parameters: usize,
207    dict_len: usize,
208    counts: &FieldValue<Key>,
209    size: &FieldValue<Key>,
210) -> Result<(Option<Vec<usize>>, usize), ()> {
211    let size = match size {
212        FieldValue::Uint(size) => *size as usize,
213        FieldValue::Int(size) if *size >= 0 => *size as usize,
214        _ => return Err(()),
215    };
216    // An empty array will be cast as a StringList. A condensed histogram of all-zeroes will thus
217    // have empty StringList counts and indexes. If we sanitize successfully, we'll return an
218    // empty Vec<usize> for indexes, and we will have verified that counts is empty too.
219    let counts_len = match counts {
220        FieldValue::Array(counts) => counts.len(),
221        FieldValue::StringList(counts) if counts.is_empty() => 0,
222        _ => return Err(()),
223    };
224    if size < 3 {
225        // We need at least 3 (overflow + 1) buckets to be a valid histogram
226        return Err(());
227    }
228    // It's OK if indexes is None. If it's Some, it must be a Vec<NumericValue> that converts to a
229    // Vec<usize>. Also, check that various lengths are consistent.
230    match indexes {
231        None => {
232            if counts_len != size || dict_len != n_parameters - 1 {
233                return Err(());
234            }
235            Ok((None, size))
236        }
237        Some(FieldValue::StringList(indexes)) if indexes.is_empty() => {
238            if counts_len != 0 || dict_len != n_parameters {
239                return Err(());
240            }
241            Ok((Some(vec![]), size))
242        }
243        Some(FieldValue::Array(indexes)) => {
244            if indexes.len() != counts_len || counts_len > size || dict_len != n_parameters {
245                return Err(());
246            }
247            let indexes = indexes
248                .iter()
249                .map(|value| match value.as_u64() {
250                    None => None,
251                    Some(i) if (i as usize) < size => Some(i as usize),
252                    _ => None,
253                })
254                .collect::<Option<Vec<_>>>();
255            match indexes {
256                None => Err(()),
257                Some(indexes) => Ok((Some(indexes), size)),
258            }
259        }
260        _ => Err(()),
261    }
262}
263
264fn match_linear_histogram<Key>(
265    floor: &FieldValue<Key>,
266    step: &FieldValue<Key>,
267    counts: &FieldValue<Key>,
268    indexes: Option<&FieldValue<Key>>,
269    size: &FieldValue<Key>,
270    dict_len: usize,
271) -> Option<FieldValue<Key>> {
272    let (indexes, size) = match sanitize_histogram_parameters(indexes, 5, dict_len, counts, size) {
273        Ok((indexes, size)) => (indexes, size),
274        Err(()) => return None,
275    };
276    // A double histogram will have all types double. An int or uint histogram may have a mix
277    // of int and uint members.
278    match (floor, step, counts) {
279        (FieldValue::Double(floor), FieldValue::Double(step), counts) => {
280            let counts = parse_f64_list(counts)?;
281            Some(FieldValue::LinearDoubleHistogram(LinearHistogram {
282                floor: *floor,
283                step: *step,
284                counts,
285                indexes,
286                size,
287            }))
288        }
289        (floor, step, counts) => {
290            let counts_i64 = parse_i64_list(counts);
291            if let (Some(counts), Some(floor), Some(step)) =
292                (counts_i64, value_as_i64(floor), value_as_i64(step))
293            {
294                return Some(FieldValue::LinearIntHistogram(LinearHistogram {
295                    floor,
296                    step,
297                    counts,
298                    indexes,
299                    size,
300                }));
301            }
302            // At this point, it's unsigned, or nothing.
303            if let (Some(counts), Some(floor), Some(step)) =
304                (parse_u64_list(counts), value_as_u64(floor), value_as_u64(step))
305            {
306                return Some(FieldValue::LinearUintHistogram(LinearHistogram {
307                    floor,
308                    step,
309                    counts,
310                    indexes,
311                    size,
312                }));
313            }
314            None
315        }
316    }
317}
318
319fn match_exponential_histogram<Key>(
320    floor: &FieldValue<Key>,
321    initial_step: &FieldValue<Key>,
322    step_multiplier: &FieldValue<Key>,
323    counts: &FieldValue<Key>,
324    indexes: Option<&FieldValue<Key>>,
325    size: &FieldValue<Key>,
326    dict_len: usize,
327) -> Option<FieldValue<Key>> {
328    let (indexes, size) = match sanitize_histogram_parameters(indexes, 6, dict_len, counts, size) {
329        Ok((indexes, size)) => (indexes, size),
330        Err(()) => return None,
331    };
332    match (floor, initial_step, step_multiplier, counts) {
333        (
334            FieldValue::Double(floor),
335            FieldValue::Double(initial_step),
336            FieldValue::Double(step_multiplier),
337            counts,
338        ) => {
339            let counts = parse_f64_list(counts)?;
340            Some(FieldValue::ExponentialDoubleHistogram(ExponentialHistogram {
341                floor: *floor,
342                initial_step: *initial_step,
343                step_multiplier: *step_multiplier,
344                counts,
345                indexes,
346                size,
347            }))
348        }
349        (floor, initial_step, step_multiplier, counts) => {
350            let counts_i64 = parse_i64_list(counts);
351            if let (Some(counts), Some(floor), Some(initial_step), Some(step_multiplier)) = (
352                counts_i64,
353                value_as_i64(floor),
354                value_as_i64(initial_step),
355                value_as_i64(step_multiplier),
356            ) {
357                return Some(FieldValue::ExponentialIntHistogram(ExponentialHistogram {
358                    floor,
359                    initial_step,
360                    step_multiplier,
361                    counts,
362                    indexes,
363                    size,
364                }));
365            }
366            // At this point, it's unsigned, or nothing.
367            if let (Some(counts), Some(floor), Some(initial_step), Some(step_multiplier)) = (
368                parse_u64_list(counts),
369                value_as_u64(floor),
370                value_as_u64(initial_step),
371                value_as_u64(step_multiplier),
372            ) {
373                return Some(FieldValue::ExponentialUintHistogram(ExponentialHistogram {
374                    floor,
375                    initial_step,
376                    step_multiplier,
377                    counts,
378                    indexes,
379                    size,
380                }));
381            }
382            None
383        }
384    }
385}
386
387fn match_histogram<Key>(dict: &HashMap<Key, FieldValue<Key>>) -> Option<FieldValue<Key>>
388where
389    Key: FromStr + Hash + Eq,
390{
391    // Quick checks for efficiency - most maps won't be histograms.
392    let dict_len = dict.len();
393    if !(4..=6).contains(&dict_len) {
394        return None;
395    }
396    let get_key = |name: &str| -> Option<&FieldValue<Key>> {
397        let key = Key::from_str(name).ok()?;
398        dict.get(&key)
399    };
400    let floor = get_key("floor")?;
401    let step = get_key("step");
402    let initial_step = get_key("initial_step");
403    let step_multiplier = get_key("step_multiplier");
404    let counts = get_key("counts");
405    let indexes = get_key("indexes");
406    let size = get_key("size");
407    // Indexes may be None if the histogram isn't condensed.
408    match (step, initial_step, step_multiplier, counts, indexes, size) {
409        (Some(step), None, None, Some(counts), indexes, Some(size)) => {
410            match_linear_histogram(floor, step, counts, indexes, size, dict_len)
411        }
412        (None, Some(initial_step), Some(step_multiplier), Some(counts), indexes, Some(size)) => {
413            match_exponential_histogram(
414                floor,
415                initial_step,
416                step_multiplier,
417                counts,
418                indexes,
419                size,
420                dict_len,
421            )
422        }
423        _ => None,
424    }
425}
426
427impl<'de, Key> Visitor<'de> for FieldVisitor<Key>
428where
429    Key: FromStr + Hash + Eq,
430{
431    type Value = FieldValue<Key>;
432
433    fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
434        formatter.write_str("failed to field")
435    }
436
437    fn visit_map<V>(self, mut map: V) -> Result<Self::Value, V::Error>
438    where
439        V: MapAccess<'de>,
440    {
441        let mut entries = vec![];
442        while let Some(entry) = map.next_entry::<String, FieldValue<Key>>()? {
443            entries.push(entry);
444        }
445
446        let node = entries
447            .into_iter()
448            .map(|(key, value)| Key::from_str(&key).map(|key| (key, value)))
449            .collect::<Result<HashMap<Key, FieldValue<Key>>, _>>()
450            .map_err(|_| de::Error::custom("failed to parse key"))?;
451        if let Some(histogram) = match_histogram(&node) {
452            Ok(histogram)
453        } else {
454            Ok(FieldValue::Node(node))
455        }
456    }
457
458    fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E> {
459        Ok(FieldValue::Int(value))
460    }
461
462    fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E> {
463        Ok(FieldValue::Uint(value))
464    }
465
466    fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E> {
467        Ok(FieldValue::Double(value))
468    }
469
470    fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E> {
471        Ok(FieldValue::Bool(value))
472    }
473
474    fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
475    where
476        E: de::Error,
477    {
478        if value.starts_with("b64:") {
479            let bytes64 = value.replace("b64:", "");
480            let bytes = BASE64_STANDARD
481                .decode(&bytes64)
482                .map_err(|_| de::Error::custom("failed to decode bytes"))?;
483            return Ok(FieldValue::Bytes(bytes));
484        }
485        Ok(FieldValue::String(value.to_string()))
486    }
487
488    fn visit_seq<S>(self, mut seq: S) -> Result<Self::Value, S::Error>
489    where
490        S: SeqAccess<'de>,
491    {
492        let mut result = vec![];
493        while let Some(elem) = seq.next_element::<SeqItem>()? {
494            result.push(elem);
495        }
496        // There can be two types of sequences: regular arrays (containing numeric values),
497        // and string lists (containing only strings). There cannot be a
498        // sequence containing a mix of them.
499        let mut array = vec![];
500        let mut strings = vec![];
501        for item in result {
502            match item {
503                SeqItem::Value(x) => array.push(x),
504                SeqItem::StringValue(x) => strings.push(x),
505            }
506        }
507
508        match (!array.is_empty(), !strings.is_empty()) {
509            (true, false) => Ok(FieldValue::Array(array)),
510            (false, _) => {
511                // Numeric arrays cannot be empty, but string lists can.
512                // Histograms can contain empty arrays of numbers, but we'll check for empty
513                // StringList in the histogram-matching code, and know what it means.
514                Ok(FieldValue::StringList(strings))
515            }
516            _ => Err(de::Error::custom("unexpected sequence containing mixed values")),
517        }
518    }
519}
520
521#[derive(Deserialize)]
522#[serde(untagged)]
523enum SeqItem {
524    Value(NumericValue),
525    StringValue(String),
526}
527
528enum NumericValue {
529    Positive(u64),
530    Negative(i64),
531    Double(f64),
532}
533
534impl NumericValue {
535    #[inline]
536    fn as_i64(&self) -> Option<i64> {
537        match self {
538            Self::Positive(x) if *x <= i64::MAX as u64 => Some(*x as i64),
539            Self::Negative(x) => Some(*x),
540            _ => None,
541        }
542    }
543
544    #[inline]
545    fn as_u64(&self) -> Option<u64> {
546        match self {
547            Self::Positive(x) => Some(*x),
548            _ => None,
549        }
550    }
551
552    #[inline]
553    fn as_f64(&self) -> Option<f64> {
554        match self {
555            Self::Double(x) => Some(*x),
556            _ => None,
557        }
558    }
559}
560
561impl<'de> Deserialize<'de> for NumericValue {
562    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
563    where
564        D: Deserializer<'de>,
565    {
566        deserializer.deserialize_any(NumericValueVisitor)
567    }
568}
569
570impl<Key: Clone> IntoProperty<Key> for Vec<NumericValue> {
571    fn into_property(self, key: &Key) -> Option<Property<Key>> {
572        if let Some(values) = parse_f64_vec(&self) {
573            return Some(Property::DoubleArray(key.clone(), ArrayContent::Values(values)));
574        }
575        if let Some(values) = parse_i64_vec(&self) {
576            return Some(Property::IntArray(key.clone(), ArrayContent::Values(values)));
577        }
578        if let Some(values) = parse_u64_vec(&self) {
579            return Some(Property::UintArray(key.clone(), ArrayContent::Values(values)));
580        }
581        None
582    }
583}
584
585macro_rules! parse_numeric_vec_impls {
586    ($($type:ty),*) => {
587        $(
588            paste::paste! {
589                fn [<parse_ $type _vec>](vec: &[NumericValue]) -> Option<Vec<$type>> {
590                    vec.iter().map(|value| value.[<as_ $type>]()).collect::<Option<Vec<_>>>()
591                }
592
593                // Histograms can contain empty lists for counts and indexes. These will be
594                // deserialized as empty StringLists and should parse to empty vecs
595                // of any numeric type.
596                fn [<parse_ $type _list>]<Key>(list: &FieldValue<Key>) -> Option<Vec<$type>> {
597                    match list {
598                        FieldValue::StringList(list) if list.len() == 0 => Some(vec![]),
599                        FieldValue::Array(vec) => [<parse_ $type _vec>](vec),
600                        _ => None,
601                    }
602                }
603            }
604        )*
605    };
606}
607
608// Generates the following functions:
609// fn parse_f64_vec()
610// fn parse_u64_vec()
611// fn parse_i64_vec()
612// fn parse_f64_list()
613// fn parse_u64_list()
614// fn parse_i64_list()
615parse_numeric_vec_impls!(f64, u64, i64);
616
617struct NumericValueVisitor;
618
619impl Visitor<'_> for NumericValueVisitor {
620    type Value = NumericValue;
621
622    fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
623        formatter.write_str("failed to deserialize bucket array")
624    }
625
626    fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E> {
627        if value < 0 {
628            return Ok(NumericValue::Negative(value));
629        }
630        Ok(NumericValue::Positive(value as u64))
631    }
632
633    fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E> {
634        Ok(NumericValue::Positive(value))
635    }
636
637    fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E> {
638        Ok(NumericValue::Double(value))
639    }
640}
641
642// Due to the custom encoding/decoding in use, the json schema does not adhere to the structure of
643// the rust definition, so must be implemented manually here.
644#[cfg(feature = "json_schema")]
645impl<T> schemars::JsonSchema for DiagnosticsHierarchy<T> {
646    fn schema_name() -> std::borrow::Cow<'static, str> {
647        "DiagnosticsHierarchy".into()
648    }
649
650    fn json_schema(generator: &mut schemars::SchemaGenerator) -> Schema {
651        let defs_path =
652            generator.settings().definitions_path.trim_start_matches('#').trim_end_matches('/');
653        let self_ref = format!("#{}/{}", defs_path, Self::schema_name());
654        let property_schema = schemars::json_schema!({
655            "description": "A property, which can be any standard object, or an array, or a histogram",
656            "anyOf": [
657                String::json_schema(generator),
658                Vec::<f64>::json_schema(generator),
659                Vec::<i64>::json_schema(generator),
660                Vec::<u64>::json_schema(generator),
661                Vec::<String>::json_schema(generator),
662                LinearHistogram::<u64>::json_schema(generator),
663                LinearHistogram::<i64>::json_schema(generator),
664                LinearHistogram::<f64>::json_schema(generator),
665                ExponentialHistogram::<u64>::json_schema(generator),
666                ExponentialHistogram::<i64>::json_schema(generator),
667                ExponentialHistogram::<f64>::json_schema(generator),
668                i64::json_schema(generator),
669                u64::json_schema(generator),
670                f64::json_schema(generator),
671                bool::json_schema(generator),
672                // Includes a recursive self-reference.
673                {
674                    "type": "object",
675                    "$ref": self_ref
676                }
677            ]
678        });
679        schemars::json_schema!({
680            "type": "object",
681            "minProperties": 1,
682            "patternProperties": {
683                "^.*$": property_schema
684            }
685        })
686    }
687}
688
689#[cfg(test)]
690mod tests {
691    use super::*;
692    use crate::ArrayFormat;
693
694    #[cfg(feature = "json_schema")]
695    use ffx_writer::VerifiedMachineWriter;
696
697    #[fuchsia::test]
698    fn deserialize_json() {
699        let json_string = get_single_json_hierarchy();
700        let mut parsed_hierarchy: DiagnosticsHierarchy =
701            serde_json::from_str(&json_string).expect("deserialized");
702        let mut expected_hierarchy = get_unambigious_deserializable_hierarchy();
703        parsed_hierarchy.sort();
704        expected_hierarchy.sort();
705        assert_eq!(expected_hierarchy, parsed_hierarchy);
706    }
707
708    #[cfg(feature = "json_schema")]
709    #[fuchsia::test]
710    fn verify_schema() {
711        let json_string = get_single_json_hierarchy();
712        let data: serde_json::Value =
713            serde_json::from_str(&json_string).expect("valid json string");
714        let root = data.get("root").expect("expected root node");
715        if let Err(e) = VerifiedMachineWriter::<DiagnosticsHierarchy>::verify_schema(root) {
716            panic!("Error verifying schema of `{data:#?}`: {e:#?}")
717        }
718    }
719
720    #[fuchsia::test]
721    fn reversible_deserialize() {
722        let mut original_hierarchy = get_unambigious_deserializable_hierarchy();
723        let result =
724            serde_json::to_string(&original_hierarchy).expect("failed to format hierarchy");
725        let mut parsed_hierarchy: DiagnosticsHierarchy =
726            serde_json::from_str(&result).expect("deserialized");
727        parsed_hierarchy.sort();
728        original_hierarchy.sort();
729        assert_eq!(original_hierarchy, parsed_hierarchy);
730    }
731
732    #[fuchsia::test]
733    fn test_exp_histogram() {
734        let mut hierarchy = DiagnosticsHierarchy::new(
735            "root".to_string(),
736            vec![Property::IntArray(
737                "histogram".to_string(),
738                ArrayContent::new(
739                    vec![1000, 1000, 2, 1, 2, 3, 4, 5, 6],
740                    ArrayFormat::ExponentialHistogram,
741                )
742                .unwrap(),
743            )],
744            vec![],
745        );
746        let expected_json = serde_json::json!({
747            "root": {
748                "histogram": {
749                    "floor": 1000,
750                    "initial_step": 1000,
751                    "step_multiplier": 2,
752                    "counts": [1, 2, 3, 4, 5, 6],
753                    "size": 6
754                }
755            }
756        });
757        let result_json = serde_json::json!(hierarchy);
758        assert_eq!(result_json, expected_json);
759        let mut parsed_hierarchy: DiagnosticsHierarchy =
760            serde_json::from_value(result_json).expect("deserialized");
761        parsed_hierarchy.sort();
762        hierarchy.sort();
763        assert_eq!(hierarchy, parsed_hierarchy);
764    }
765
766    // Creates a hierarchy that isn't lossy due to its unambigious values.
767    fn get_unambigious_deserializable_hierarchy() -> DiagnosticsHierarchy {
768        DiagnosticsHierarchy::new(
769            "root",
770            vec![
771                Property::UintArray(
772                    "array".to_string(),
773                    ArrayContent::Values(vec![0, 2, u64::MAX]),
774                ),
775                Property::Bool("bool_true".to_string(), true),
776                Property::Bool("bool_false".to_string(), false),
777                Property::StringList(
778                    "string_list".to_string(),
779                    vec!["foo".to_string(), "bar".to_string()],
780                ),
781                Property::StringList("empty_string_list".to_string(), vec![]),
782            ],
783            vec![
784                DiagnosticsHierarchy::new(
785                    "a",
786                    vec![
787                        Property::Double("double".to_string(), 2.5),
788                        Property::DoubleArray(
789                            "histogram".to_string(),
790                            ArrayContent::new(
791                                vec![0.0, 2.0, 4.0, 1.0, 3.0, 4.0, 7.0],
792                                ArrayFormat::ExponentialHistogram,
793                            )
794                            .unwrap(),
795                        ),
796                    ],
797                    vec![],
798                ),
799                DiagnosticsHierarchy::new(
800                    "b",
801                    vec![
802                        Property::Int("int".to_string(), -2),
803                        Property::String("string".to_string(), "some value".to_string()),
804                        Property::IntArray(
805                            "histogram".to_string(),
806                            ArrayContent::new(vec![0, 2, 4, 1, 3], ArrayFormat::LinearHistogram)
807                                .unwrap(),
808                        ),
809                    ],
810                    vec![],
811                ),
812            ],
813        )
814    }
815
816    pub fn get_single_json_hierarchy() -> String {
817        "{ \"root\": {
818                \"a\": {
819                    \"double\": 2.5,
820                    \"histogram\": {
821                        \"floor\": 0.0,
822                        \"initial_step\": 2.0,
823                        \"step_multiplier\": 4.0,
824                        \"counts\": [1.0, 3.0, 4.0, 7.0],
825                        \"size\": 4
826                    }
827                },
828                \"array\": [
829                    0,
830                    2,
831                    18446744073709551615
832                ],
833                \"string_list\": [\"foo\", \"bar\"],
834                \"empty_string_list\": [],
835                \"b\": {
836                    \"histogram\": {
837                        \"floor\": 0,
838                        \"step\": 2,
839                        \"counts\": [4, 1, 3],
840                        \"size\": 3
841                    },
842                    \"int\": -2,
843                    \"string\": \"some value\"
844                },
845                \"bool_false\": false,
846                \"bool_true\": true
847            }}"
848        .to_string()
849    }
850}