Skip to main content

diskann_benchmark_runner/report/
mod.rs

1/*
2 * Copyright (c) Microsoft Corporation.
3 * Licensed under the MIT license.
4 */
5
6//! # Baseline Checking
7//!
8//! The [`Regression`](crate::benchmark::Regression) provides a means
9//! of performing before/after comparisons against previously generated results. However,
10//! presentation of these results is largely left to the devices of the implementors.
11//!
12//! This module provides a means of aggregating all match failures (if any) and presenting
13//! all failures as a single unit.
14
15use std::borrow::Cow;
16
17use serde::{Serialize, Serializer};
18
19pub mod tolerance;
20
21/// A [`Check`], [`Table`], or nested grouping of multiple [`Report`]s used to gather
22/// regression checks across arbitrary benchamrks and reporting the overall success or failure.
23///
24/// On success, all attributes are printed.
25/// ```
26/// use diskann_benchmark_runner::report::{Report, Check};
27///
28/// let mut builder = Report::builder();
29/// {
30///     let mut nested = Report::builder();
31///     nested.push("foo", Report::equal(1, 1));
32///     nested.push("bar", Report::info("cat"));
33///     builder.push("nested", nested.finish());
34/// }
35///
36/// let expected = "
37/// nested:
38///   foo: 1
39///   bar: cat
40/// ".trim();
41///
42/// let report = builder.finish();
43/// assert_eq!(report.to_string(), expected);
44/// ```
45///
46/// On failure, only the failing subset will be displayed.
47#[derive(Debug, Serialize)]
48#[serde(transparent)]
49#[must_use = "reports are result types"]
50pub struct Report {
51    inner: ReportInner,
52}
53
54impl Report {
55    /// Construct an empty [`Builder`] for constructing hierarchical reports.
56    /// ```
57    /// use diskann_benchmark_runner::report::Report;
58    ///
59    /// let mut builder = Report::builder();
60    /// builder.push("foo", Report::info(10));
61    /// builder.push("baz", Report::equal(10, 10));
62    /// builder.push("bar", Report::equal(10, 20));
63    /// let report = builder.finish();
64    ///
65    /// let s = report.to_string();
66    /// let expected = "
67    /// foo: 10
68    /// baz: 10
69    /// bar: (old) 10 != (new) 20 | FAIL
70    /// ".trim();
71    /// assert_eq!(s, expected);
72    /// assert!(report.status().is_fail());
73    /// ```
74    pub fn builder() -> Builder {
75        Builder::new()
76    }
77
78    /// Construct a [`Table`] using the provided list of headers.
79    /// ```
80    /// use diskann_benchmark_runner::report::{Report, Check};
81    ///
82    /// // Convenience for testing two renders.
83    /// fn process(x: &str) -> &str {
84    ///     let x = x.strip_prefix('\n').unwrap_or(x);
85    ///     x.strip_suffix('\n').unwrap_or(x)
86    /// }
87    ///
88    /// let mut table = Report::table(["hello", "world"]);
89    ///
90    /// // Tables are built one row at a time.
91    /// //
92    /// // Rows must be `finish`ed to be committed. Otherwise,
93    /// // their contents are // dropped.
94    /// let mut row = table.new_row();
95    /// row.push(Check::equal(10, 20)); // Failing check.
96    /// row.push(Check::info("bar"));
97    /// table.push_row(row).unwrap();
98    /// let report = table.finish();
99    ///
100    /// let s = report.to_string();
101    /// let expected = process("
102    ///                  hello,   world,   status
103    /// =========================================
104    /// (old) 10 != (new) 20 !,     bar,     FAIL
105    /// ");
106    /// assert_eq!(process(&s), expected);
107    /// ```
108    pub fn table<I>(headers: I) -> Table
109    where
110        I: IntoIterator<Item: Into<Cow<'static, str>>>,
111    {
112        Table::new(headers)
113    }
114
115    /// Report `info` for diagnostic purposes.
116    ///
117    /// The information will be displayed and serialized unchanged.
118    pub fn info<T>(info: T) -> Self
119    where
120        T: std::fmt::Display + Serialize + 'static,
121    {
122        Check::info(info).into()
123    }
124
125    /// Report an `error`.
126    ///
127    /// Errors are considered a failure for diagnostic purposes.
128    ///
129    /// The error will be displayed and serialized using its [`ToString`] representation.
130    pub fn error<T>(error: T) -> Self
131    where
132        T: ToString,
133    {
134        Check::error(error).into()
135    }
136
137    /// Assert that `old == new`.
138    ///
139    /// If the check succeeds, one argument will be retained and displayed for informational
140    /// purposes.
141    ///
142    /// If the check fails, both values will be retained and the report fails.
143    pub fn equal<T>(old: T, new: T) -> Self
144    where
145        T: std::fmt::Display + Serialize + PartialEq + 'static,
146    {
147        Check::equal(old, new).into()
148    }
149
150    /// Construct a new [`Report`] using [`tolerance::EqualWithTolerance`].
151    ///
152    /// ```
153    /// use diskann_benchmark_runner::report::{
154    ///     Report, Status,
155    ///     tolerance::{FmtSpec, MaxAbsoluteChange},
156    /// };
157    ///
158    /// let tolerance = MaxAbsoluteChange::from_u64(2);
159    /// let report = Report::equal_with_tolerance(&tolerance, 100u64, 102, FmtSpec::new());
160    /// assert_eq!(report.status(), Status::Pass);
161    /// ```
162    pub fn equal_with_tolerance<T, U>(
163        tolerance: &T,
164        old: U,
165        new: U,
166        spec: tolerance::FmtSpec,
167    ) -> Self
168    where
169        T: tolerance::EqualWithTolerance<U>,
170    {
171        tolerance.equal_with_tolerance(old, new, spec).into()
172    }
173
174    /// Construct a new [`Report`] using [`tolerance::HigherIsBetter`].
175    pub fn higher_is_better<T, U>(tolerance: &T, old: U, new: U, spec: tolerance::FmtSpec) -> Self
176    where
177        T: tolerance::HigherIsBetter<U>,
178    {
179        tolerance.higher_is_better(old, new, spec).into()
180    }
181
182    /// Construct a new [`Report`] using [`tolerance::LowerIsBetter`].
183    pub fn lower_is_better<T, U>(tolerance: &T, old: U, new: U, spec: tolerance::FmtSpec) -> Self
184    where
185        T: tolerance::LowerIsBetter<U>,
186    {
187        tolerance.lower_is_better(old, new, spec).into()
188    }
189
190    /// Return the [`Status`] of this report.
191    ///
192    /// [`Status::Pass`] will only be reported if all nested [`Report`]s, [`Check`]s, and
193    /// [`Table`]s report a pass.
194    pub fn status(&self) -> Status {
195        self.inner.status()
196    }
197
198    /// Convert `self` into a [`Finished`].
199    ///
200    /// Is this benchmark is a fail, then constructed `Finished` will only render the paths
201    /// that failed. Otherwise, all paths will be rendered.
202    pub fn pass_fail(self) -> crate::benchmark::PassFail<Finished, Finished> {
203        let status = self.status();
204        let rendering = match status {
205            Status::Pass => Rendering::All,
206            Status::Fail => Rendering::FailOnly,
207        };
208
209        let finished = Finished::new(self, rendering);
210        match status {
211            Status::Pass => crate::benchmark::PassFail::Pass(finished),
212            Status::Fail => crate::benchmark::PassFail::Fail(finished),
213        }
214    }
215}
216
217impl std::fmt::Display for Report {
218    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
219        Render::new(self, Rendering::All).fmt(f)
220    }
221}
222
223impl From<Table> for Report {
224    fn from(table: Table) -> Self {
225        Self {
226            inner: ReportInner::Table(table),
227        }
228    }
229}
230
231impl From<Check> for Report {
232    fn from(check: Check) -> Self {
233        Self {
234            inner: ReportInner::Check(check),
235        }
236    }
237}
238
239/// The success or failure of checks.
240#[derive(Debug, Clone, Copy, PartialEq, Eq)]
241pub enum Status {
242    Pass,
243    Fail,
244}
245
246impl Status {
247    /// Return `true` if `self == Status::Fail`.
248    pub fn is_fail(self) -> bool {
249        self == Self::Fail
250    }
251
252    /// Return `true` if `self == Status::Success`.
253    pub fn is_success(self) -> bool {
254        self == Self::Pass
255    }
256
257    fn merge(&mut self, status: Self) {
258        if status == Status::Fail {
259            *self = Status::Fail;
260        }
261    }
262
263    fn as_str(self) -> &'static str {
264        match self {
265            Self::Pass => "PASS",
266            Self::Fail => "FAIL",
267        }
268    }
269}
270
271impl std::fmt::Display for Status {
272    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
273        f.write_str(self.as_str())
274    }
275}
276
277impl Serialize for Status {
278    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
279    where
280        S: Serializer,
281    {
282        serializer.serialize_str(self.as_str())
283    }
284}
285
286/// A finished [`Report`].
287///
288/// This applies a dynamic [`std::fmt::Display`] policy depending on the final status of
289/// the report.
290#[derive(Debug, Serialize)]
291#[serde(transparent)]
292pub struct Finished {
293    report: Report,
294    #[serde(skip)]
295    rendering: Rendering,
296}
297
298impl Finished {
299    fn new(report: Report, rendering: Rendering) -> Self {
300        Self { report, rendering }
301    }
302}
303
304impl std::fmt::Display for Finished {
305    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
306        Render::new(&self.report, self.rendering).fmt(f)
307    }
308}
309
310#[derive(Debug, Clone, Copy, PartialEq, Eq)]
311enum Rendering {
312    All,
313    FailOnly,
314}
315
316fn render(report: &Report, policy: Rendering) -> Option<String> {
317    let ctx = FmtContext::Tree;
318    match &report.inner {
319        ReportInner::Table(table) => match policy {
320            Rendering::All => Some(table.to_string()),
321            Rendering::FailOnly => {
322                if table.status() == Status::Fail {
323                    Some(table.to_string())
324                } else {
325                    None
326                }
327            }
328        },
329        ReportInner::Check(check) => match policy {
330            Rendering::All => Some(check.render(ctx)),
331            Rendering::FailOnly => {
332                if check.status() == Status::Fail {
333                    Some(check.render(ctx))
334                } else {
335                    None
336                }
337            }
338        },
339        ReportInner::Nested { children, status } => {
340            if policy == Rendering::FailOnly && *status == Status::Pass {
341                return None;
342            }
343
344            let mut kv = crate::utils::fmt::KeyValue::new();
345
346            // Allocate numbuffers to format all the integer keys in `children`.
347            let keys: Vec<_> = children.iter().map(|c| c.0.remove_position()).collect();
348
349            for (key, child) in std::iter::zip(keys.iter(), children.iter()) {
350                if let Some(rendered) = render(&child.1, policy) {
351                    let force_nest = match &child.1.inner {
352                        ReportInner::Table(_) => true,
353                        ReportInner::Check(_) => false,
354                        ReportInner::Nested { .. } => true,
355                    };
356
357                    if force_nest {
358                        kv.push_eager_nested(key, rendered);
359                    } else {
360                        kv.push_eager(key, rendered);
361                    }
362                }
363            }
364
365            if kv.is_empty() {
366                None
367            } else {
368                Some(kv.to_string())
369            }
370        }
371    }
372}
373
374struct Render<'a> {
375    report: &'a Report,
376    policy: Rendering,
377}
378
379impl<'a> Render<'a> {
380    fn new(report: &'a Report, policy: Rendering) -> Self {
381        Self { report, policy }
382    }
383}
384
385impl std::fmt::Display for Render<'_> {
386    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
387        match render(self.report, self.policy) {
388            Some(s) => f.write_str(&s),
389            None => Ok(()),
390        }
391    }
392}
393
394#[derive(Debug, Serialize)]
395enum ReportInner {
396    /// Report is formatted as a table.
397    ///
398    /// Table must be inspected for success/failure.
399    Table(Table),
400
401    /// An equal check.
402    Check(Check),
403
404    /// Report is formatted as a nested report.
405    Nested {
406        children: Vec<(Key, Report)>,
407        status: Status,
408    },
409}
410
411impl ReportInner {
412    fn status(&self) -> Status {
413        match self {
414            Self::Table(table) => table.status(),
415            Self::Check(check) => check.status(),
416            Self::Nested { status, .. } => *status,
417        }
418    }
419}
420
421/////////////
422// Builder //
423/////////////
424
425/// A builder for constructing nested [`Report`]s.
426pub struct Builder {
427    children: Vec<(Key, Report)>,
428    status: Status,
429}
430
431impl Builder {
432    /// Construct a new empty builder.
433    pub fn new() -> Self {
434        Self {
435            children: Vec::new(),
436            status: Status::Pass,
437        }
438    }
439
440    /// Add `report` to the collection and associate it with `key`.
441    ///
442    /// Reports are collected in the order they are pushed and no uniqueness requirements
443    /// exist for `key`. If building reports positionally, [`Builder::len`] can be used to
444    /// get the current number of items.
445    pub fn push(&mut self, key: impl Into<Key>, report: Report) {
446        self.status.merge(report.status());
447        self.children.push((key.into(), report))
448    }
449
450    /// Finish building the [`Report`].
451    pub fn finish(self) -> Report {
452        let Self { children, status } = self;
453        Report {
454            inner: ReportInner::Nested { children, status },
455        }
456    }
457
458    /// Return the current number of [`Report`]s.
459    pub fn len(&self) -> usize {
460        self.children.len()
461    }
462
463    /// Return `true` only if [`Builder::len`] return 0.
464    pub fn is_empty(&self) -> bool {
465        self.len() == 0
466    }
467}
468
469impl Default for Builder {
470    fn default() -> Self {
471        Self::new()
472    }
473}
474
475/// A key for a nested [`Report`] when given to [`Builder::push`].
476#[derive(Debug, Clone, PartialEq, Eq)]
477pub enum Key {
478    Str(&'static str),
479    Position(usize),
480    String(String),
481}
482
483impl Key {
484    fn remove_position(&self) -> Cow<'_, str> {
485        match self {
486            Self::Str(s) => Cow::Borrowed(s),
487            Self::Position(p) => Cow::Owned(p.to_string()),
488            Self::String(s) => Cow::Borrowed(s),
489        }
490    }
491}
492
493impl std::fmt::Display for Key {
494    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
495        match self {
496            Self::Str(s) => f.write_str(s),
497            Self::Position(i) => write!(f, "{}", i),
498            Self::String(s) => f.write_str(s),
499        }
500    }
501}
502
503impl Serialize for Key {
504    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
505    where
506        S: Serializer,
507    {
508        match self {
509            Self::Str(s) => serializer.serialize_str(s),
510            Self::Position(i) => serializer.serialize_u64(*i as u64),
511            Self::String(s) => serializer.serialize_str(s),
512        }
513    }
514}
515
516impl From<&'static str> for Key {
517    fn from(s: &'static str) -> Key {
518        Key::Str(s)
519    }
520}
521
522impl From<usize> for Key {
523    fn from(i: usize) -> Key {
524        Key::Position(i)
525    }
526}
527
528impl From<String> for Key {
529    fn from(s: String) -> Key {
530        Key::String(s)
531    }
532}
533
534///////////
535// Table //
536///////////
537
538/// A more horizontally compact leaf for [`Report`].
539#[derive(Debug)]
540pub struct Table {
541    headers: Vec<Cow<'static, str>>,
542    rows: Vec<Row>,
543    status: Status,
544}
545
546impl Table {
547    /// Create a new empty [`Table`] with the specified headers.
548    ///
549    /// Trying to add a new row via [`Table::push_row`] will fail is the row does not
550    /// contain the same number of entries as `headers`.
551    pub fn new<I>(headers: I) -> Self
552    where
553        I: IntoIterator<Item: Into<Cow<'static, str>>>,
554    {
555        Self {
556            headers: headers.into_iter().map(|h| h.into()).collect(),
557            rows: Vec::new(),
558            status: Status::Pass,
559        }
560    }
561
562    /// Return the overall status of the table.
563    ///
564    /// [`Status::Fail`] will be returned if any internal check is a failure.
565    pub fn status(&self) -> Status {
566        self.status
567    }
568
569    /// Return the number of columns in the table.
570    ///
571    /// This is equal to the number of headers provided to [`Table::new`].
572    pub fn ncols(&self) -> usize {
573        self.headers.len()
574    }
575
576    /// Return the number of rows in the table.
577    pub fn nrows(&self) -> usize {
578        self.rows.len()
579    }
580
581    /// Construct a new [`Row`].
582    ///
583    /// The returned [`Row`] will not be automatically added to the table. Use
584    /// [`Table::push_row`] to add it.
585    pub fn new_row(&self) -> Row {
586        Row::new()
587    }
588
589    /// Add `row` to the `self`.
590    ///
591    /// Returns an error if `row.len() != self.ncols()`.
592    pub fn push_row(&mut self, row: Row) -> anyhow::Result<()> {
593        if row.len() != self.ncols() {
594            anyhow::bail!(
595                "finished row has {} columns when {} were expected",
596                row.len(),
597                self.ncols(),
598            );
599        }
600
601        self.status.merge(row.status);
602        self.rows.push(row);
603        Ok(())
604    }
605
606    /// Complete the table.
607    pub fn finish(self) -> Report {
608        self.into()
609    }
610}
611
612impl Serialize for Table {
613    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
614    where
615        S: Serializer,
616    {
617        use serde::ser::SerializeStruct;
618
619        let columns: Vec<Vec<&Check>> = (0..self.ncols())
620            .map(|column| self.rows.iter().map(|row| &row.checks[column]).collect())
621            .collect();
622
623        let row_status: Vec<Status> = self.rows.iter().map(|row| row.status).collect();
624
625        let mut state = serializer.serialize_struct("Table", 4)?;
626        state.serialize_field("headers", &self.headers)?;
627        state.serialize_field("columns", &columns)?;
628        state.serialize_field("row_status", &row_status)?;
629        state.serialize_field("status", &self.status)?;
630        state.end()
631    }
632}
633
634const EMPTY_TABLE: &str = "<empty table>";
635
636impl std::fmt::Display for Table {
637    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
638        // Nothing to write for empty tables.
639        if self.ncols() == 0 {
640            return f.write_str(EMPTY_TABLE);
641        }
642
643        // If this table is the source of a failure, we render an additional column to
644        // identify which rows caused the overall failure.
645        //
646        // To cut down on visual noise:
647        //
648        // * We don't print out "PASS". This makes "FAIL" easier to identify.
649        // * We do not render this column when the entire table is a pass.
650        let nrows = self.nrows();
651        let mut table = if self.status == Status::Pass {
652            crate::utils::fmt::Table::new(self.headers.iter().cloned(), nrows)
653        } else {
654            let last_column = Cow::Borrowed("status");
655            crate::utils::fmt::Table::new(
656                self.headers
657                    .iter()
658                    .cloned()
659                    .chain(std::iter::once(last_column)),
660                nrows,
661            )
662        };
663
664        for (row_index, row) in self.rows.iter().enumerate() {
665            let mut out = table.row(row_index);
666            let mut col_index = 0;
667            for check in row.checks.iter() {
668                out.insert(check.render(FmtContext::Table), col_index);
669                col_index += 1;
670            }
671
672            if row.status == Status::Fail {
673                out.insert(row.status, col_index);
674            }
675        }
676
677        write!(f, "{}", table)
678    }
679}
680
681/// A new row for [`Table`].
682///
683/// Use [`Table::push_row`] to add a row to a [`Table`].
684#[derive(Debug)]
685pub struct Row {
686    checks: Vec<Check>,
687    status: Status,
688}
689
690impl Row {
691    /// Create a new empty [`Row`].
692    pub fn new() -> Self {
693        Self {
694            checks: Vec::new(),
695            status: Status::Pass,
696        }
697    }
698
699    /// Add a new [`Check`] to the row. This increases the column count of the row by 1.
700    pub fn push(&mut self, check: Check) {
701        self.status.merge(check.status());
702        self.checks.push(check);
703    }
704
705    /// Return the current number of columns.
706    pub fn len(&self) -> usize {
707        self.checks.len()
708    }
709
710    /// Return `true` only if the row currently has no columns.
711    pub fn is_empty(&self) -> bool {
712        self.len() == 0
713    }
714}
715
716impl Default for Row {
717    fn default() -> Self {
718        Self::new()
719    }
720}
721
722///////////
723// Check //
724///////////
725
726/// A singular regression check.
727///
728/// This is one of the leaf elements of [`Report`].
729#[must_use = "checks are result types"]
730pub struct Check {
731    inner: Box<dyn Checkable>,
732}
733
734impl Check {
735    /// Return the [`Status`] of the check.
736    pub fn status(&self) -> Status {
737        self.inner.status()
738    }
739
740    /// Report `info` for diagnostic purposes.
741    ///
742    /// See also: [`Report::info`].
743    pub fn info<T>(info: T) -> Self
744    where
745        T: std::fmt::Display + Serialize + 'static,
746    {
747        Self::new(Info(info))
748    }
749
750    /// Report an `error`.
751    ///
752    /// See also: [`Report::error`].
753    pub fn error<T>(error: T) -> Self
754    where
755        T: ToString,
756    {
757        Self::new(Error(error.to_string()))
758    }
759
760    /// Assert that `old == new`.
761    ///
762    /// See also: [`Report::error`].
763    pub fn equal<T>(old: T, new: T) -> Self
764    where
765        T: std::fmt::Display + Serialize + PartialEq + 'static,
766    {
767        if old == new {
768            Self::new(Equal(old))
769        } else {
770            Self::new(NotEqual { old, new })
771        }
772    }
773
774    /// Construct a new [`Check`] using [`tolerance::EqualWithTolerance`].
775    ///
776    /// See also: [`Report::equal_with_tolerance`].
777    pub fn equal_with_tolerance<T, U>(
778        tolerance: &T,
779        old: U,
780        new: U,
781        spec: tolerance::FmtSpec,
782    ) -> Self
783    where
784        T: tolerance::EqualWithTolerance<U>,
785    {
786        tolerance.equal_with_tolerance(old, new, spec)
787    }
788
789    /// Construct a new [`Check`] using [`tolerance::HigherIsBetter`].
790    ///
791    /// See also: [`Report::higher_is_better`].
792    pub fn higher_is_better<T, U>(tolerance: &T, old: U, new: U, spec: tolerance::FmtSpec) -> Self
793    where
794        T: tolerance::HigherIsBetter<U>,
795    {
796        tolerance.higher_is_better(old, new, spec)
797    }
798
799    /// Construct a new [`Report`] using [`tolerance::LowerIsBetter`].
800    ///
801    /// See also: [`Report::lower_is_better`].
802    pub fn lower_is_better<T, U>(tolerance: &T, old: U, new: U, spec: tolerance::FmtSpec) -> Self
803    where
804        T: tolerance::LowerIsBetter<U>,
805    {
806        tolerance.lower_is_better(old, new, spec)
807    }
808
809    fn new<T>(inner: T) -> Self
810    where
811        T: Checkable + 'static,
812    {
813        Self {
814            inner: Box::new(inner),
815        }
816    }
817
818    fn render(&self, ctx: FmtContext) -> String {
819        let mut s = String::new();
820
821        #[expect(clippy::expect_used)]
822        self.inner
823            .fmt(&mut s, ctx)
824            .expect("rust treats string formatting as infallible");
825        s
826    }
827}
828
829impl std::fmt::Debug for Check {
830    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
831        let inner = self.render(FmtContext::Tree);
832        let status = self.status();
833        f.debug_struct("Check")
834            .field("kind", &inner)
835            .field("status", &status)
836            .finish()
837    }
838}
839
840impl Serialize for Check {
841    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
842    where
843        S: Serializer,
844    {
845        let mut serializer = Some(serializer);
846        let mut result = None;
847
848        self.inner.serialize(&mut |value| {
849            #[expect(clippy::expect_used)]
850            let serializer = serializer
851                .take()
852                .expect("serialization callback called more than once");
853            result = Some(value.serialize(serializer));
854        });
855
856        #[expect(clippy::expect_used)]
857        result.expect("serialization callback was not called")
858    }
859}
860
861//-------//
862// Equal //
863//-------//
864
865#[derive(Debug)]
866struct Equal<T>(T);
867
868impl<T> Checkable for Equal<T>
869where
870    T: std::fmt::Display + Serialize,
871{
872    fn status(&self) -> Status {
873        Status::Pass
874    }
875
876    fn fmt(&self, f: &mut dyn std::fmt::Write, _ctx: FmtContext) -> std::fmt::Result {
877        write!(f, "{}", self.0)
878    }
879
880    fn serialize(&self, f: &mut dyn FnMut(&dyn erased_serde::Serialize)) {
881        f(&SerEqual::pass(&self.0))
882    }
883}
884
885#[derive(Debug)]
886struct NotEqual<T> {
887    old: T,
888    new: T,
889}
890
891impl<T> Checkable for NotEqual<T>
892where
893    T: std::fmt::Display + Serialize,
894{
895    fn status(&self) -> Status {
896        Status::Fail
897    }
898
899    fn fmt(&self, f: &mut dyn std::fmt::Write, ctx: FmtContext) -> std::fmt::Result {
900        write!(
901            f,
902            "(old) {} != (new) {} {}",
903            self.old,
904            self.new,
905            ctx.fail_marker()
906        )
907    }
908
909    fn serialize(&self, f: &mut dyn FnMut(&dyn erased_serde::Serialize)) {
910        f(&SerEqual::fail(&self.old, &self.new))
911    }
912}
913
914/// Serializer for [`Equal`] and [`NotEqual`].
915///
916/// If `new` is present, this is assumed to be a failure.
917struct SerEqual<'a, T> {
918    old: &'a T,
919    new: Option<&'a T>,
920}
921
922impl<'a, T> SerEqual<'a, T> {
923    fn pass(value: &'a T) -> Self {
924        Self {
925            old: value,
926            new: None,
927        }
928    }
929
930    fn fail(old: &'a T, new: &'a T) -> Self {
931        Self {
932            old,
933            new: Some(new),
934        }
935    }
936}
937
938impl<T> Serialize for SerEqual<'_, T>
939where
940    T: Serialize,
941{
942    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
943    where
944        S: Serializer,
945    {
946        use serde::ser::SerializeStruct;
947
948        let (new, status) = match self.new {
949            Some(new) => (new, Status::Fail),
950            None => (self.old, Status::Pass),
951        };
952
953        let mut state = serializer.serialize_struct("Equal", 3)?;
954        state.serialize_field("old", self.old)?;
955        state.serialize_field("new", new)?;
956        state.serialize_field("status", &status)?;
957        state.end()
958    }
959}
960
961//------//
962// Info //
963//------//
964
965#[derive(Debug)]
966struct Info<T>(T);
967
968impl<T> Checkable for Info<T>
969where
970    T: std::fmt::Display + Serialize,
971{
972    fn status(&self) -> Status {
973        Status::Pass
974    }
975
976    fn fmt(&self, f: &mut dyn std::fmt::Write, _ctx: FmtContext) -> std::fmt::Result {
977        write!(f, "{}", self.0)
978    }
979
980    fn serialize(&self, f: &mut dyn FnMut(&dyn erased_serde::Serialize)) {
981        f(&self.0)
982    }
983}
984
985//-------//
986// Error //
987//-------//
988
989#[derive(Debug)]
990struct Error(String);
991
992impl Checkable for Error {
993    fn status(&self) -> Status {
994        Status::Fail
995    }
996
997    fn fmt(&self, f: &mut dyn std::fmt::Write, ctx: FmtContext) -> std::fmt::Result {
998        write!(f, "{} {}", self.0, ctx.fail_marker())
999    }
1000
1001    fn serialize(&self, f: &mut dyn FnMut(&dyn erased_serde::Serialize)) {
1002        struct Ser<'a>(&'a str);
1003
1004        impl Serialize for Ser<'_> {
1005            fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1006            where
1007                S: Serializer,
1008            {
1009                use serde::ser::SerializeStruct;
1010
1011                let mut state = serializer.serialize_struct("Error", 2)?;
1012                state.serialize_field("error", self.0)?;
1013                state.serialize_field("status", &Status::Fail)?;
1014                state.end()
1015            }
1016        }
1017
1018        f(&Ser(&self.0))
1019    }
1020}
1021
1022/// Are we rendering in a [`Table`] or as part of the normal tree rendering in [`Report`]?
1023#[derive(Debug, Clone, Copy)]
1024enum FmtContext {
1025    Tree,
1026    Table,
1027}
1028
1029impl FmtContext {
1030    // Select how to mark failures depending on the context.
1031    //
1032    // In a table, we just use `!` since we already attach a row-level `FAIL`.
1033    //
1034    // This keeps things a little more compact horizontally.
1035    fn fail_marker(&self) -> &'static str {
1036        match self {
1037            Self::Tree => "| FAIL",
1038            Self::Table => "!",
1039        }
1040    }
1041}
1042
1043/// Trait object for checks.
1044trait Checkable {
1045    fn status(&self) -> Status;
1046    fn fmt(&self, f: &mut dyn std::fmt::Write, ctx: FmtContext) -> std::fmt::Result;
1047    fn serialize(&self, f: &mut dyn FnMut(&dyn erased_serde::Serialize));
1048}
1049
1050///////////
1051// Tests //
1052///////////
1053
1054#[cfg(test)]
1055mod tests {
1056    use super::*;
1057
1058    #[test]
1059    fn test_key_display_and_serialization() {
1060        let cases = [
1061            (Key::Str("name"), "name", serde_json::json!("name")),
1062            (Key::Position(42), "42", serde_json::json!(42)),
1063            (
1064                Key::String(String::from("owned")),
1065                "owned",
1066                serde_json::json!("owned"),
1067            ),
1068        ];
1069
1070        for (key, rendered, serialized) in cases {
1071            assert_eq!(key.to_string(), rendered);
1072            assert_eq!(serde_json::to_value(&key).unwrap(), serialized);
1073        }
1074    }
1075
1076    use diskann_utils::assert_contains;
1077
1078    //-------//
1079    // Check //
1080    //-------//
1081
1082    #[test]
1083    fn test_check_info() {
1084        let cases = [
1085            (
1086                Check::info(u64::MAX),
1087                "18446744073709551615",
1088                serde_json::json!(u64::MAX),
1089            ),
1090            (Check::info(0.25), "0.25", serde_json::json!(0.25)),
1091            (Check::info(false), "false", serde_json::json!(false)),
1092            (
1093                Check::info(String::from("first\nsecond")),
1094                "first\nsecond",
1095                serde_json::json!("first\nsecond"),
1096            ),
1097        ];
1098
1099        for (check, rendered, serialized) in cases {
1100            assert_eq!(check.status(), Status::Pass);
1101            assert_eq!(check.render(FmtContext::Tree), rendered);
1102            assert_eq!(check.render(FmtContext::Table), rendered);
1103            assert_eq!(serde_json::to_value(&check).unwrap(), serialized);
1104        }
1105    }
1106
1107    #[test]
1108    fn test_check_equal() {
1109        let cases = [
1110            (
1111                Check::equal(u64::MAX, u64::MAX),
1112                Status::Pass,
1113                "18446744073709551615",
1114                "18446744073709551615",
1115                serde_json::json!({
1116                    "old": u64::MAX,
1117                    "new": u64::MAX,
1118                    "status": "PASS",
1119                }),
1120            ),
1121            (
1122                Check::equal(-1, 2),
1123                Status::Fail,
1124                "(old) -1 != (new) 2 | FAIL",
1125                "(old) -1 != (new) 2 !",
1126                serde_json::json!({ "old": -1, "new": 2, "status": "FAIL" }),
1127            ),
1128            (
1129                Check::equal("same", "same"),
1130                Status::Pass,
1131                "same",
1132                "same",
1133                serde_json::json!({ "old": "same", "new": "same", "status": "PASS" }),
1134            ),
1135            (
1136                Check::equal("before", "after"),
1137                Status::Fail,
1138                "(old) before != (new) after | FAIL",
1139                "(old) before != (new) after !",
1140                serde_json::json!({ "old": "before", "new": "after", "status": "FAIL" }),
1141            ),
1142        ];
1143
1144        for (check, status, tree, table, serialized) in cases {
1145            assert_eq!(check.status(), status);
1146            assert_eq!(check.render(FmtContext::Tree), tree);
1147            assert_eq!(check.render(FmtContext::Table), table);
1148            assert_eq!(serde_json::to_value(&check).unwrap(), serialized);
1149        }
1150    }
1151
1152    #[test]
1153    fn test_check_error() {
1154        let cases = [
1155            (
1156                Check::error("invalid input"),
1157                "invalid input | FAIL",
1158                "invalid input !",
1159                serde_json::json!({ "error": "invalid input", "status": "FAIL" }),
1160            ),
1161            (
1162                Check::error("invalid \"value\"\ntry again"),
1163                "invalid \"value\"\ntry again | FAIL",
1164                "invalid \"value\"\ntry again !",
1165                serde_json::json!({
1166                    "error": "invalid \"value\"\ntry again",
1167                    "status": "FAIL",
1168                }),
1169            ),
1170        ];
1171
1172        for (check, tree, table, serialized) in cases {
1173            assert_eq!(check.status(), Status::Fail);
1174            assert_eq!(check.render(FmtContext::Tree), tree);
1175            assert_eq!(check.render(FmtContext::Table), table);
1176            assert_eq!(serde_json::to_value(&check).unwrap(), serialized);
1177        }
1178    }
1179
1180    //-------//
1181    // Table //
1182    //-------//
1183
1184    fn check_render(expected: &str, rendered: &str) {
1185        // To support `expected` coming from raw strings, we remove leading and trailing
1186        // newlines.
1187        let expected = {
1188            let temp = expected.strip_prefix('\n').unwrap_or(expected);
1189            temp.strip_suffix('\n').unwrap_or(temp)
1190        };
1191
1192        struct Pretty<'a> {
1193            expected: &'a str,
1194            rendered: &'a str,
1195        }
1196
1197        impl std::fmt::Display for Pretty<'_> {
1198            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1199                writeln!(
1200                    f,
1201                    "EXPECTED\n\n{}\n\nRENDERED\n\n{}",
1202                    self.expected, self.rendered
1203                )
1204            }
1205        }
1206
1207        let pretty = Pretty { expected, rendered };
1208
1209        // Check number of lines first for better error diagnostics.
1210        let expected_lines: Vec<&str> = expected.lines().collect();
1211        let rendered_lines: Vec<&str> = rendered.lines().collect();
1212        assert_eq!(
1213            expected_lines.len(),
1214            rendered_lines.len(),
1215            "expected {} lines - got {}\n\n{}",
1216            expected_lines.len(),
1217            rendered_lines.len(),
1218            pretty
1219        );
1220
1221        for (i, (expected, rendered)) in
1222            std::iter::zip(expected_lines.iter(), rendered_lines.iter()).enumerate()
1223        {
1224            // We trim the end because the expected tables trim trailing whitespace.
1225            //
1226            // We do keep leading whitespace, though, for slightly more accurate checks.
1227            //
1228            // Also, in the error message, we display the lines horizontally aligned to help
1229            // find whitespace woes.
1230            let expected = expected.trim_end();
1231            let rendered = rendered.trim_end();
1232            assert_eq!(
1233                expected,
1234                rendered,
1235                "mismatch on line {} of {}\n\n{}",
1236                i + 1,
1237                expected_lines.len(),
1238                pretty
1239            );
1240        }
1241    }
1242
1243    #[test]
1244    fn test_table_serialization() {
1245        let mut table = Table::new(["name", "value"]);
1246        for (name, check) in [("good", Check::info(3)), ("bad", Check::equal(1, 2))] {
1247            let mut row = table.new_row();
1248            row.push(Check::info(name));
1249            row.push(check);
1250            table.push_row(row).unwrap();
1251        }
1252
1253        assert_eq!(
1254            serde_json::to_value(&table).unwrap(),
1255            serde_json::json!({
1256                "headers": ["name", "value"],
1257                "columns": [
1258                    ["good", "bad"],
1259                    [3, { "old": 1, "new": 2, "status": "FAIL" }],
1260                ],
1261                "row_status": ["PASS", "FAIL"],
1262                "status": "FAIL",
1263            }),
1264        );
1265    }
1266
1267    #[test]
1268    fn test_table_rendering() {
1269        let mut table = Table::new(["foo", "bar"]);
1270
1271        let mut row = table.new_row();
1272        assert!(row.is_empty());
1273        assert_eq!(row.len(), 0);
1274
1275        row.push(Check::info(10));
1276        assert!(!row.is_empty());
1277        assert_eq!(row.len(), 1);
1278
1279        row.push(Check::info("hello"));
1280        assert!(!row.is_empty());
1281        assert_eq!(row.len(), 2);
1282
1283        table.push_row(row).unwrap();
1284
1285        let mut row = table.new_row();
1286        row.push(Check::info("world"));
1287        row.push(Check::info(20));
1288        table.push_row(row).unwrap();
1289
1290        // Check that incorrect table lenghts are rejected.
1291        assert_contains!(
1292            table.push_row(Row::new()).unwrap_err().to_string(),
1293            "finished row has 0 columns when 2 were expected"
1294        );
1295
1296        assert!(table.status().is_success());
1297        let expected = "
1298  foo,     bar
1299==============
1300   10,   hello
1301world,      20";
1302
1303        check_render(expected, &table.to_string());
1304    }
1305
1306    #[test]
1307    fn test_table_rendering_mixed_status() {
1308        let mut table = Table::new(["value"]);
1309        for check in [Check::equal(1, 2), Check::error("invalid"), Check::info(3)] {
1310            let mut row = table.new_row();
1311            row.push(check);
1312            table.push_row(row).unwrap();
1313        }
1314
1315        assert!(table.status().is_fail());
1316        let expected = "
1317               value,   status
1318==============================
1319(old) 1 != (new) 2 !,     FAIL
1320           invalid !,     FAIL
1321                   3,";
1322
1323        check_render(expected, &table.to_string());
1324        check_render(expected, &table.finish().to_string());
1325    }
1326
1327    #[test]
1328    fn test_table_rendering_zero_rows() {
1329        let table = Table::new(["foo", "bar"]);
1330
1331        assert!(table.status().is_success());
1332        let expected = "
1333foo,   bar
1334==========";
1335
1336        check_render(expected, &table.to_string());
1337        check_render(expected, &table.finish().to_string());
1338    }
1339
1340    #[test]
1341    fn test_table_rendering_zero_columns() {
1342        let mut table = Table::new([] as [&str; 0]);
1343        table.push_row(table.new_row()).unwrap();
1344
1345        assert!(table.status().is_success());
1346        check_render(EMPTY_TABLE, &table.to_string());
1347        check_render(EMPTY_TABLE, &table.finish().to_string());
1348    }
1349
1350    #[test]
1351    fn test_table_rendering_empty() {
1352        let table = Table::new([] as [&str; 0]);
1353
1354        assert!(table.status().is_success());
1355        check_render(EMPTY_TABLE, &table.to_string());
1356        check_render(EMPTY_TABLE, &table.finish().to_string());
1357    }
1358
1359    #[test]
1360    fn test_report_rendering() {
1361        let mut good = Report::builder();
1362        good.push(0, Report::info("ready"));
1363        good.push(String::from("count"), Report::equal(2, 2));
1364
1365        let mut mixed = Report::builder();
1366        mixed.push("keep", Report::info(7));
1367        mixed.push("bad", Report::equal(1, 2));
1368        mixed.push("err", Report::error("invalid"));
1369
1370        {
1371            let mut nested = Report::builder();
1372            nested.push("should be newline", Report::info(true));
1373            mixed.push("nested", nested.finish());
1374        }
1375
1376        let mut table = Report::table(["value"]);
1377        for check in [Check::equal(3, 3), Check::equal(1, 2)] {
1378            let mut row = table.new_row();
1379            row.push(check);
1380            table.push_row(row).unwrap();
1381        }
1382
1383        let mut builder = Report::builder();
1384        builder.push("info", Report::info("example"));
1385        builder.push("good", good.finish());
1386        builder.push("mixed", mixed.finish());
1387        builder.push("table", table.finish());
1388        builder.push("omitted", Builder::new().finish());
1389        let report = builder.finish();
1390
1391        assert!(report.status().is_fail());
1392        check_render(
1393            "
1394info:  example
1395good:
1396  0:     ready
1397  count: 2
1398mixed:
1399  keep:   7
1400  bad:    (old) 1 != (new) 2 | FAIL
1401  err:    invalid | FAIL
1402  nested:
1403    should be newline: true
1404table:
1405                 value,   status
1406  ==============================
1407                     3,
1408  (old) 1 != (new) 2 !,     FAIL",
1409            &report.to_string(),
1410        );
1411        check_render(
1412            "
1413mixed:
1414  bad: (old) 1 != (new) 2 | FAIL
1415  err: invalid | FAIL
1416table:
1417                 value,   status
1418  ==============================
1419                     3,
1420  (old) 1 != (new) 2 !,     FAIL",
1421            &Finished::new(report, Rendering::FailOnly).to_string(),
1422        );
1423    }
1424
1425    #[test]
1426    fn test_finished_report_rendering_success() {
1427        let mut builder = Report::builder();
1428        builder.push("info", Report::info("ready"));
1429        builder.push("same", Report::equal(2, 2));
1430        let report = builder.finish();
1431
1432        assert!(report.status().is_success());
1433        let expected = "
1434info: ready
1435same: 2";
1436
1437        check_render(expected, &Finished::new(report, Rendering::All).to_string());
1438    }
1439}