iquery/
text_formatter.rs

1// Copyright 2019 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 diagnostics_data::{InspectData, InspectHandleName, InspectMetadata};
6use diagnostics_hierarchy::{
7    ArrayContent, DiagnosticsHierarchy, ExponentialHistogram, LinearHistogram, Property,
8};
9use either::Either;
10use nom::HexDisplay;
11use num_traits::{Bounded, Zero};
12use std::fmt;
13use std::ops::{Add, AddAssign, Mul, MulAssign};
14
15const INDENT: usize = 2;
16const HEX_DISPLAY_CHUNK_SIZE: usize = 16;
17
18pub fn format<W>(w: &mut W, path: &str, diagnostics_hierarchy: DiagnosticsHierarchy) -> fmt::Result
19where
20    W: fmt::Write,
21{
22    writeln!(w, "{path}:")?;
23    output_hierarchy(w, &diagnostics_hierarchy, 1)
24}
25
26pub fn output_schema<W>(w: &mut W, schema: &InspectData) -> fmt::Result
27where
28    W: fmt::Write,
29{
30    let InspectData {
31        moniker,
32        metadata: InspectMetadata { errors, name, component_url, timestamp, escrowed },
33        payload,
34        data_source: _,
35        version: _,
36    } = schema;
37
38    writeln!(w, "{moniker}:")?;
39    writeln!(w, "  metadata:")?;
40    if let Some(errors) = &errors {
41        let errors = errors.join(", ");
42        writeln!(w, "    errors = {errors}")?;
43    }
44
45    match &name {
46        InspectHandleName::Filename(f) => {
47            writeln!(w, "    filename = {f}")?;
48        }
49        InspectHandleName::Name(n) => {
50            writeln!(w, "    name = {n}")?;
51        }
52    }
53
54    writeln!(w, "    component_url = {component_url}")?;
55    writeln!(w, "    timestamp = {}", timestamp.into_nanos())?;
56
57    if *escrowed {
58        writeln!(w, "    escrowed = true")?;
59    }
60
61    match &payload {
62        Some(hierarchy) => {
63            writeln!(w, "  payload:")?;
64            output_hierarchy(w, hierarchy, 2)
65        }
66        None => writeln!(w, "  payload: null"),
67    }
68}
69
70fn output_hierarchy<W>(
71    w: &mut W,
72    diagnostics_hierarchy: &DiagnosticsHierarchy,
73    indent: usize,
74) -> fmt::Result
75where
76    W: fmt::Write,
77{
78    let name_indent = " ".repeat(INDENT * indent);
79    let value_indent = " ".repeat(INDENT * (indent + 1));
80    let array_broken_line_indent = " ".repeat(INDENT * (indent + 2));
81
82    writeln!(w, "{}{}:", name_indent, diagnostics_hierarchy.name)?;
83
84    for property in &diagnostics_hierarchy.properties {
85        match property {
86            Property::String(name, value) => writeln!(w, "{value_indent}{name} = {value}")?,
87            Property::Int(name, value) => writeln!(w, "{value_indent}{name} = {value}")?,
88            Property::Uint(name, value) => writeln!(w, "{value_indent}{name} = {value}")?,
89            Property::Double(name, value) => writeln!(w, "{value_indent}{name} = {value:.6}")?,
90            Property::Bytes(name, array) => {
91                let byte_str = array.to_hex(HEX_DISPLAY_CHUNK_SIZE);
92                writeln!(w, "{}{} = Binary:\n{}", value_indent, name, byte_str.trim())?;
93            }
94            Property::Bool(name, value) => writeln!(w, "{value_indent}{name} = {value}")?,
95            Property::IntArray(name, array) => output_array(w, &value_indent, name, array)?,
96            Property::UintArray(name, array) => output_array(w, &value_indent, name, array)?,
97            Property::DoubleArray(name, array) => output_array(w, &value_indent, name, array)?,
98            Property::StringList(name, list) => {
99                let max_line_length = 100;
100                let length_of_brackets = 2;
101                let length_of_comma_space = 2;
102                let total_len = list
103                    .iter()
104                    .fold(length_of_brackets, |acc, v| acc + v.len() + length_of_comma_space);
105                if total_len < max_line_length {
106                    writeln!(w, "{value_indent}{name} = {list:?}")?;
107                } else {
108                    writeln!(w, "{value_indent}{name} = [")?;
109                    for v in list {
110                        writeln!(w, r#"{array_broken_line_indent}"{v}","#)?;
111                    }
112                    writeln!(w, "{value_indent}]")?;
113                }
114            }
115        }
116    }
117
118    for child in &diagnostics_hierarchy.children {
119        output_hierarchy(w, child, indent + 1)?;
120    }
121    Ok(())
122}
123
124trait FromUsize {
125    fn from_usize(n: usize) -> Self;
126}
127
128impl FromUsize for i64 {
129    fn from_usize(n: usize) -> Self {
130        i64::try_from(n).unwrap()
131    }
132}
133
134impl FromUsize for u64 {
135    fn from_usize(n: usize) -> Self {
136        u64::try_from(n).unwrap()
137    }
138}
139
140impl FromUsize for f64 {
141    fn from_usize(n: usize) -> Self {
142        u64::try_from(n).unwrap() as f64
143    }
144}
145
146trait ExponentialBucketBound {
147    fn bound(floor: Self, initial_step: Self, step_multiplier: Self, index: usize) -> Self;
148}
149
150impl ExponentialBucketBound for i64 {
151    fn bound(floor: Self, initial_step: Self, step_multiplier: Self, index: usize) -> Self {
152        floor + initial_step * i64::pow(step_multiplier, index as u32)
153    }
154}
155
156impl ExponentialBucketBound for u64 {
157    fn bound(floor: Self, initial_step: Self, step_multiplier: Self, index: usize) -> Self {
158        floor + initial_step * u64::pow(step_multiplier, index as u32)
159    }
160}
161
162impl ExponentialBucketBound for f64 {
163    fn bound(floor: Self, initial_step: Self, step_multiplier: Self, index: usize) -> Self {
164        floor + initial_step * f64::powi(step_multiplier, (index as u64) as i32)
165    }
166}
167
168struct ExponentialBucketBoundsArgs<T> {
169    floor: T,
170    initial_step: T,
171    step_multiplier: T,
172    index: usize,
173    size: usize,
174}
175
176fn exponential_bucket_bounds<T>(args: ExponentialBucketBoundsArgs<T>) -> (T, T)
177where
178    T: Bounded + Add<Output = T> + ExponentialBucketBound + Copy,
179{
180    let ExponentialBucketBoundsArgs { floor, initial_step, step_multiplier, index, size } = args;
181    match index {
182        0 => (T::min_value(), floor),
183        1 if size == 2 => (floor, T::max_value()),
184        1 => (floor, floor + initial_step),
185        index if index == size - 1 => {
186            (T::bound(floor, initial_step, step_multiplier, index - 2), T::max_value())
187        }
188        _ => (
189            T::bound(floor, initial_step, step_multiplier, index - 2),
190            T::bound(floor, initial_step, step_multiplier, index - 1),
191        ),
192    }
193}
194
195struct LinearBucketBoundsArgs<T> {
196    floor: T,
197    step: T,
198    index: usize,
199    size: usize,
200}
201
202fn linear_bucket_bounds<T>(args: LinearBucketBoundsArgs<T>) -> (T, T)
203where
204    T: Bounded + Add<Output = T> + Mul<Output = T> + FromUsize + Copy,
205{
206    let LinearBucketBoundsArgs { floor, step, index, size } = args;
207    match index {
208        0 => (T::min_value(), floor),
209        index if index == size - 1 => (floor + step * T::from_usize(index - 1), T::max_value()),
210        _ => (floor + step * T::from_usize(index - 1), floor + step * T::from_usize(index)),
211    }
212}
213
214struct OutputHistogramArgs<'a, T, F> {
215    indent: &'a str,
216    name: &'a str,
217    histogram_type: &'a str,
218    counts: &'a [T],
219    indexes: Option<&'a [usize]>,
220    size: usize,
221    bound_calculator: F,
222}
223
224fn output_histogram<T, F, W>(w: &mut W, args: OutputHistogramArgs<'_, T, F>) -> fmt::Result
225where
226    W: fmt::Write,
227    T: NumberFormat + fmt::Display + PartialOrd + Zero,
228    F: Fn(usize) -> (T, T),
229{
230    let OutputHistogramArgs {
231        indent,
232        name,
233        histogram_type,
234        counts,
235        indexes,
236        size,
237        bound_calculator,
238    } = args;
239    let value_indent = format!("{}{}", indent, " ".repeat(INDENT));
240    write!(
241        w,
242        "{indent}{name}:\n\
243                {value_indent}type = {histogram_type}\n\
244                {value_indent}size = {size}\n\
245                {value_indent}buckets = ["
246    )?;
247    let mut nonzero_counts = match indexes {
248        None => Either::Left(counts.iter().enumerate().filter(|(_, count)| **count > T::zero())),
249        Some(indexes) => Either::Right(
250            indexes
251                .iter()
252                .zip(counts)
253                .filter(|(_, count)| **count > T::zero())
254                .map(|(u, t)| (*u, t)),
255        ),
256    }
257    .peekable();
258    while let Some((index, count)) = nonzero_counts.next() {
259        let (low_bound, high_bound) = bound_calculator(index);
260        write!(w, "[{},{})={}", low_bound.format(), high_bound.format(), count)?;
261        if nonzero_counts.peek().is_some() {
262            write!(w, ", ")?;
263        }
264    }
265    writeln!(w, "]")
266}
267
268fn output_array<T, W>(
269    w: &mut W,
270    value_indent: &str,
271    name: &str,
272    array: &ArrayContent<T>,
273) -> fmt::Result
274where
275    W: fmt::Write,
276    T: AddAssign
277        + MulAssign
278        + Copy
279        + Add<Output = T>
280        + fmt::Display
281        + NumberFormat
282        + Bounded
283        + Mul<Output = T>
284        + ExponentialBucketBound
285        + FromUsize
286        + PartialOrd
287        + Zero,
288{
289    match array {
290        ArrayContent::Values(values) => {
291            write!(w, "{value_indent}{name} = [")?;
292            for (i, value) in values.iter().enumerate() {
293                write!(w, "{value}")?;
294                if i < values.len() - 1 {
295                    w.write_str(", ")?;
296                }
297            }
298            writeln!(w, "]")
299        }
300        ArrayContent::LinearHistogram(LinearHistogram { floor, step, counts, indexes, size }) => {
301            let bucket_bounder = |index| {
302                linear_bucket_bounds::<T>(LinearBucketBoundsArgs {
303                    floor: *floor,
304                    step: *step,
305                    index,
306                    size: *size,
307                })
308            };
309            output_histogram(
310                w,
311                OutputHistogramArgs {
312                    indent: value_indent,
313                    name,
314                    histogram_type: "linear",
315                    counts,
316                    indexes: indexes.as_deref(),
317                    size: *size,
318                    bound_calculator: bucket_bounder,
319                },
320            )
321        }
322        ArrayContent::ExponentialHistogram(ExponentialHistogram {
323            floor,
324            initial_step,
325            step_multiplier,
326            counts,
327            indexes,
328            size,
329        }) => {
330            let bucket_bounder = |index| {
331                exponential_bucket_bounds::<T>(ExponentialBucketBoundsArgs {
332                    floor: *floor,
333                    initial_step: *initial_step,
334                    step_multiplier: *step_multiplier,
335                    index,
336                    size: *size,
337                })
338            };
339            output_histogram(
340                w,
341                OutputHistogramArgs {
342                    indent: value_indent,
343                    name,
344                    histogram_type: "exponential",
345                    counts,
346                    indexes: indexes.as_deref(),
347                    size: *size,
348                    bound_calculator: bucket_bounder,
349                },
350            )
351        }
352    }
353}
354
355trait NumberFormat {
356    fn format(&self) -> String;
357}
358
359impl NumberFormat for i64 {
360    fn format(&self) -> String {
361        match *self {
362            i64::MAX => "<max>".to_string(),
363            std::i64::MIN => "<min>".to_string(),
364            x => format!("{x}"),
365        }
366    }
367}
368
369impl NumberFormat for u64 {
370    fn format(&self) -> String {
371        match *self {
372            u64::MAX => "<max>".to_string(),
373            x => format!("{x}"),
374        }
375    }
376}
377
378impl NumberFormat for usize {
379    fn format(&self) -> String {
380        match *self {
381            std::usize::MAX => "<max>".to_string(),
382            x => format!("{x}"),
383        }
384    }
385}
386
387impl NumberFormat for f64 {
388    fn format(&self) -> String {
389        if *self == f64::MAX || *self == f64::INFINITY {
390            "inf".to_string()
391        } else if *self == f64::MIN || *self == f64::NEG_INFINITY {
392            "-inf".to_string()
393        } else {
394            format!("{self}")
395        }
396    }
397}
398
399#[cfg(test)]
400mod tests {
401    use super::*;
402    use diagnostics_data::{InspectDataBuilder, Timestamp};
403    use test_case::test_case;
404
405    #[fuchsia::test]
406    fn test_array_line_breaks() {
407        let h = DiagnosticsHierarchy {
408            name: "test".into(),
409            properties: vec![Property::StringList(
410                "short_array".to_owned(),
411                vec!["short".into(), "array".into()],
412            )],
413            children: vec![],
414            missing: vec![],
415        };
416        let mut buf = String::new();
417        output_hierarchy(&mut buf, &h, 2).unwrap();
418
419        let expected = r#"    test:
420      short_array = ["short", "array"]
421"#;
422        assert_eq!(expected, buf);
423
424        // joined up would be 104 characters
425        let long_array = vec![
426            "12345678".into(),
427            "12345678".into(),
428            "12345678".into(),
429            "12345678".into(),
430            "12345678".into(),
431            "12345678".into(),
432            "12345678".into(),
433            "12345678".into(),
434            "12345678".into(),
435            "12345678".into(),
436            "12345678".into(),
437            "12345678".into(),
438            "12345678".into(),
439        ];
440
441        let h = DiagnosticsHierarchy {
442            name: "test".into(),
443            properties: vec![Property::StringList("long_array".to_owned(), long_array)],
444            children: vec![],
445            missing: vec![],
446        };
447        let mut buf = String::new();
448        output_hierarchy(&mut buf, &h, 2).unwrap();
449
450        let expected = r#"    test:
451      long_array = [
452        "12345678",
453        "12345678",
454        "12345678",
455        "12345678",
456        "12345678",
457        "12345678",
458        "12345678",
459        "12345678",
460        "12345678",
461        "12345678",
462        "12345678",
463        "12345678",
464        "12345678",
465      ]
466"#;
467        assert_eq!(expected, buf);
468    }
469
470    #[test_case(LinearBucketBoundsArgs { floor: -10, step: 2, index: 0, size: 4 }, (i64::MIN, -10))]
471    #[test_case(LinearBucketBoundsArgs { floor: -10, step: 2, index: 1, size: 4 }, (-10, -8))]
472    #[test_case(LinearBucketBoundsArgs { floor: -10, step: 2, index: 2, size: 4 }, (-8, -6))]
473    #[test_case(LinearBucketBoundsArgs { floor: -10, step: 2, index: 3, size: 4 }, (-6, i64::MAX))]
474    #[fuchsia::test]
475    fn test_linear_bucket_bounds_i64(args: LinearBucketBoundsArgs<i64>, bounds: (i64, i64)) {
476        // floor, step, index, size
477        assert_eq!(linear_bucket_bounds(args), bounds);
478    }
479
480    // u64 min is 0, so the underflow bucket may be weird
481    #[test_case(LinearBucketBoundsArgs { floor: 0, step: 2, index: 0, size: 4 }, (0, 0))]
482    #[test_case(LinearBucketBoundsArgs { floor: 0, step: 2, index: 1, size: 4 }, (0, 2))]
483    #[test_case(LinearBucketBoundsArgs { floor: 0, step: 2, index: 2, size: 4 }, (2, 4))]
484    #[test_case(LinearBucketBoundsArgs { floor: 0, step: 2, index: 3, size: 4 }, (4, u64::MAX))]
485    #[test_case(LinearBucketBoundsArgs { floor: 1, step: 2, index: 0, size: 4 }, (0, 1))]
486    #[test_case(LinearBucketBoundsArgs { floor: 1, step: 2, index: 1, size: 4 }, (1, 3))]
487    #[test_case(LinearBucketBoundsArgs { floor: 1, step: 2, index: 2, size: 4 }, (3, 5))]
488    #[test_case(LinearBucketBoundsArgs { floor: 1, step: 2, index: 3, size: 4 }, (5, u64::MAX))]
489    #[fuchsia::test]
490    fn test_linear_bucket_bounds_u64(args: LinearBucketBoundsArgs<u64>, bounds: (u64, u64)) {
491        assert_eq!(linear_bucket_bounds(args), bounds);
492    }
493
494    #[test_case(
495        LinearBucketBoundsArgs { floor: -0.5, step: 0.5, index: 0, size: 4 },
496        (f64::MIN, -0.5)
497    )]
498    #[test_case(LinearBucketBoundsArgs { floor: -0.5, step: 0.5, index: 1, size: 4 }, (-0.5, 0.0))]
499    #[test_case(LinearBucketBoundsArgs { floor: -0.5, step: 0.5, index: 2, size: 4 }, (0.0, 0.5))]
500    #[test_case(
501        LinearBucketBoundsArgs { floor: -0.5, step: 0.5, index: 3, size: 4 },
502        (0.5, f64::MAX)
503    )]
504    #[fuchsia::test]
505    fn test_linear_bucket_bounds_f64(args: LinearBucketBoundsArgs<f64>, bounds: (f64, f64)) {
506        assert_eq!(linear_bucket_bounds(args), bounds);
507    }
508
509    // Check logic for few-bucket histograms. 2-bucket shouldn't occur, but let's treat it
510    // well anyway. (Using i64)
511    #[test_case(LinearBucketBoundsArgs { floor: -10, step: 2, index: 0, size: 3 }, (i64::MIN, -10))]
512    #[test_case(LinearBucketBoundsArgs { floor: -10, step: 2, index: 1, size: 3 }, (-10, -8))]
513    #[test_case(LinearBucketBoundsArgs { floor: -10, step: 2, index: 2, size: 3 }, (-8, i64::MAX))]
514    #[test_case(LinearBucketBoundsArgs { floor: -10, step: 2, index: 0, size: 2 }, (i64::MIN, -10))]
515    #[test_case(LinearBucketBoundsArgs { floor: -10, step: 2, index: 1, size: 2 }, (-10, i64::MAX))]
516    #[fuchsia::test]
517    fn test_linear_small_bucket_bounds(args: LinearBucketBoundsArgs<i64>, bounds: (i64, i64)) {
518        // floor, step, index, size
519        assert_eq!(linear_bucket_bounds(args), bounds);
520    }
521
522    // Test cases for i64 exponential
523    #[test_case(
524        ExponentialBucketBoundsArgs {
525            floor: -10,
526            initial_step: 2,
527            step_multiplier: 3,
528            index: 0,
529            size: 5 },
530        (i64::MIN, -10)
531    )]
532    #[test_case(
533        ExponentialBucketBoundsArgs {
534            floor: -10,
535            initial_step: 2,
536            step_multiplier: 3,
537            index: 1,
538            size: 5 },
539        (-10, -8)
540    )]
541    #[test_case(
542        ExponentialBucketBoundsArgs {
543            floor: -10,
544            initial_step: 2,
545            step_multiplier: 3,
546            index: 2,
547            size: 5
548        },
549        (-8, -4)
550    )]
551    #[test_case(
552        ExponentialBucketBoundsArgs {
553            floor: -10,
554            initial_step: 2,
555            step_multiplier: 3,
556            index: 3,
557            size: 5
558        },
559        (-4, 8)
560    )]
561    #[test_case(
562        ExponentialBucketBoundsArgs {
563            floor: -10,
564            initial_step: 2,
565            step_multiplier: 3,
566            index: 4,
567            size: 5
568        },
569        (8, i64::MAX)
570    )]
571    #[fuchsia::test]
572    fn test_exponential_bucket_bounds_i64(
573        args: ExponentialBucketBoundsArgs<i64>,
574        bounds: (i64, i64),
575    ) {
576        assert_eq!(exponential_bucket_bounds(args), bounds);
577    }
578
579    // Test cases for u64 exponential
580    #[test_case(
581        ExponentialBucketBoundsArgs {
582            floor: 0,
583            initial_step: 2,
584            step_multiplier: 3,
585            index: 0,
586            size: 5
587        },
588        (0, 0)
589    )]
590    #[test_case(
591        ExponentialBucketBoundsArgs {
592            floor: 0,
593            initial_step: 2,
594            step_multiplier: 3,
595            index: 1,
596            size: 5
597        },
598        (0, 2)
599    )]
600    #[test_case(
601        ExponentialBucketBoundsArgs {
602            floor: 0,
603            initial_step: 2,
604            step_multiplier: 3,
605            index: 2,
606            size: 5
607        },
608        (2, 6)
609    )]
610    #[test_case(
611        ExponentialBucketBoundsArgs {
612            floor: 0,
613            initial_step: 2,
614            step_multiplier: 3,
615            index: 3,
616            size: 5
617        },
618        (6, 18)
619    )]
620    #[test_case(
621        ExponentialBucketBoundsArgs {
622        floor: 0,
623        initial_step: 2,
624        step_multiplier: 3,
625        index: 4,
626        size: 5 },
627        (18, u64::MAX)
628    )]
629    #[test_case(
630        ExponentialBucketBoundsArgs {
631            floor: 1,
632            initial_step: 2,
633            step_multiplier: 3,
634            index: 0,
635            size: 5
636        },
637        (0, 1)
638    )]
639    #[test_case(
640        ExponentialBucketBoundsArgs {
641            floor: 1,
642            initial_step: 2,
643            step_multiplier: 3,
644            index: 1,
645            size: 5
646        },
647        (1, 3)
648    )]
649    #[test_case(
650        ExponentialBucketBoundsArgs {
651            floor: 1,
652            initial_step: 2,
653            step_multiplier: 3,
654            index: 2,
655            size: 5
656        },
657        (3, 7)
658    )]
659    #[test_case(
660        ExponentialBucketBoundsArgs {
661            floor: 1,
662            initial_step: 2,
663            step_multiplier: 3,
664            index: 3,
665            size: 5
666        },
667        (7, 19)
668    )]
669    #[test_case(
670        ExponentialBucketBoundsArgs {
671            floor: 1,
672            initial_step: 2,
673            step_multiplier: 3,
674            index: 4,
675            size: 5
676        },
677        (19, u64::MAX)
678    )]
679    #[fuchsia::test]
680    fn test_exponential_bucket_bounds_u64(
681        args: ExponentialBucketBoundsArgs<u64>,
682        bounds: (u64, u64),
683    ) {
684        assert_eq!(exponential_bucket_bounds(args), bounds);
685    }
686
687    // Test cases for f64 exponential
688    #[test_case(
689        ExponentialBucketBoundsArgs {
690            floor: -0.5,
691            initial_step: 0.5,
692            step_multiplier: 3.0,
693            index: 0,
694            size: 5 },
695        (f64::MIN, -0.5)
696    )]
697    #[test_case(
698        ExponentialBucketBoundsArgs {
699            floor: -0.5,
700            initial_step: 0.5,
701            step_multiplier: 3.0,
702            index: 1,
703            size: 5 },
704        (-0.5, 0.0)
705    )]
706    #[test_case(
707        ExponentialBucketBoundsArgs {
708            floor: -0.5,
709            initial_step: 0.5,
710            step_multiplier: 3.0,
711            index: 2,
712            size: 5 },
713        (0.0, 1.0)
714    )]
715    #[test_case(
716        ExponentialBucketBoundsArgs {
717            floor: -0.5,
718            initial_step: 0.5,
719            step_multiplier: 3.0,
720            index: 3,
721            size: 5 },
722        (1.0, 4.0)
723    )]
724    #[test_case(
725        ExponentialBucketBoundsArgs {
726            floor: -0.5,
727            initial_step: 0.5,
728            step_multiplier: 3.0,
729            index: 4,
730            size: 5 },
731            (4.0, f64::MAX)
732        )]
733    #[fuchsia::test]
734    fn test_exponential_bucket_bounds_f64(
735        args: ExponentialBucketBoundsArgs<f64>,
736        bounds: (f64, f64),
737    ) {
738        assert_eq!(exponential_bucket_bounds(args), bounds);
739    }
740
741    // Check logic for few-bucket histograms. 2-bucket shouldn't occur, but let's treat it
742    // well anyway. (Using i64)
743    #[test_case(
744        ExponentialBucketBoundsArgs {
745            floor: -10,
746            initial_step: 2,
747            step_multiplier: 3,
748            index: 0,
749            size: 4
750        },
751        (i64::MIN, -10)
752    )]
753    #[test_case(
754        ExponentialBucketBoundsArgs {
755            floor: -10,
756            initial_step: 2,
757            step_multiplier: 3,
758            index: 1,
759            size: 4
760        },
761        (-10, -8)
762    )]
763    #[test_case(
764        ExponentialBucketBoundsArgs {
765            floor: -10,
766            initial_step: 2,
767            step_multiplier: 3,
768            index: 2,
769            size: 4
770        },
771        (-8, -4)
772    )]
773    #[test_case(
774        ExponentialBucketBoundsArgs {
775            floor: -10,
776            initial_step: 2,
777            step_multiplier: 3,
778            index: 3,
779            size: 4
780        },
781        (-4, i64::MAX)
782    )]
783    #[test_case(
784        ExponentialBucketBoundsArgs {
785            floor: -10,
786            initial_step: 2,
787            step_multiplier: 3,
788            index: 0,
789            size: 3
790        },
791        (i64::MIN, -10)
792    )]
793    #[test_case(
794        ExponentialBucketBoundsArgs {
795            floor: -10,
796            initial_step: 2,
797            step_multiplier: 3,
798            index: 1,
799            size: 3
800        },
801        (-10, -8)
802    )]
803    #[test_case(
804        ExponentialBucketBoundsArgs {
805            floor: -10,
806            initial_step: 2,
807            step_multiplier: 3,
808            index: 2,
809            size: 3
810        },
811        (-8, i64::MAX)
812    )]
813    #[test_case(
814        ExponentialBucketBoundsArgs {
815            floor: -10,
816            initial_step: 2,
817            step_multiplier: 3,
818            index: 0,
819            size: 2
820        },
821        (i64::MIN, -10)
822    )]
823    #[test_case(
824        ExponentialBucketBoundsArgs {
825            floor: -10,
826            initial_step: 2,
827            step_multiplier: 3,
828            index: 1,
829            size: 2
830        },
831        (-10, i64::MAX)
832    )]
833    #[fuchsia::test]
834    fn test_exponential_small_bucket_bounds(
835        args: ExponentialBucketBoundsArgs<i64>,
836        bounds: (i64, i64),
837    ) {
838        assert_eq!(exponential_bucket_bounds(args), bounds);
839    }
840
841    #[fuchsia::test]
842    fn render_escrowed_data() {
843        let data = InspectDataBuilder::new(
844            "a/b/c/d".try_into().unwrap(),
845            "test-url",
846            Timestamp::from_nanos(123456i64),
847        )
848        .escrowed(true)
849        .build();
850        let mut buf = String::new();
851        output_schema(&mut buf, &data).unwrap();
852        let expected = r#"a/b/c/d:
853  metadata:
854    name = root
855    component_url = test-url
856    timestamp = 123456
857    escrowed = true
858  payload: null
859"#;
860        assert_eq!(expected, buf);
861    }
862}