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