1use std::borrow::Cow;
16
17use serde::{Serialize, Serializer};
18
19pub mod tolerance;
20
21#[derive(Debug, Serialize)]
48#[serde(transparent)]
49#[must_use = "reports are result types"]
50pub struct Report {
51 inner: ReportInner,
52}
53
54impl Report {
55 pub fn builder() -> Builder {
75 Builder::new()
76 }
77
78 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 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 pub fn error<T>(error: T) -> Self
131 where
132 T: ToString,
133 {
134 Check::error(error).into()
135 }
136
137 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 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 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 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 pub fn status(&self) -> Status {
195 self.inner.status()
196 }
197
198 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
241pub enum Status {
242 Pass,
243 Fail,
244}
245
246impl Status {
247 pub fn is_fail(self) -> bool {
249 self == Self::Fail
250 }
251
252 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#[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 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 Table(Table),
400
401 Check(Check),
403
404 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
421pub struct Builder {
427 children: Vec<(Key, Report)>,
428 status: Status,
429}
430
431impl Builder {
432 pub fn new() -> Self {
434 Self {
435 children: Vec::new(),
436 status: Status::Pass,
437 }
438 }
439
440 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 pub fn finish(self) -> Report {
452 let Self { children, status } = self;
453 Report {
454 inner: ReportInner::Nested { children, status },
455 }
456 }
457
458 pub fn len(&self) -> usize {
460 self.children.len()
461 }
462
463 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#[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#[derive(Debug)]
540pub struct Table {
541 headers: Vec<Cow<'static, str>>,
542 rows: Vec<Row>,
543 status: Status,
544}
545
546impl Table {
547 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 pub fn status(&self) -> Status {
566 self.status
567 }
568
569 pub fn ncols(&self) -> usize {
573 self.headers.len()
574 }
575
576 pub fn nrows(&self) -> usize {
578 self.rows.len()
579 }
580
581 pub fn new_row(&self) -> Row {
586 Row::new()
587 }
588
589 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 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 if self.ncols() == 0 {
640 return f.write_str(EMPTY_TABLE);
641 }
642
643 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#[derive(Debug)]
685pub struct Row {
686 checks: Vec<Check>,
687 status: Status,
688}
689
690impl Row {
691 pub fn new() -> Self {
693 Self {
694 checks: Vec::new(),
695 status: Status::Pass,
696 }
697 }
698
699 pub fn push(&mut self, check: Check) {
701 self.status.merge(check.status());
702 self.checks.push(check);
703 }
704
705 pub fn len(&self) -> usize {
707 self.checks.len()
708 }
709
710 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#[must_use = "checks are result types"]
730pub struct Check {
731 inner: Box<dyn Checkable>,
732}
733
734impl Check {
735 pub fn status(&self) -> Status {
737 self.inner.status()
738 }
739
740 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 pub fn error<T>(error: T) -> Self
754 where
755 T: ToString,
756 {
757 Self::new(Error(error.to_string()))
758 }
759
760 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 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 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 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#[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
914struct 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#[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#[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#[derive(Debug, Clone, Copy)]
1024enum FmtContext {
1025 Tree,
1026 Table,
1027}
1028
1029impl FmtContext {
1030 fn fail_marker(&self) -> &'static str {
1036 match self {
1037 Self::Tree => "| FAIL",
1038 Self::Table => "!",
1039 }
1040 }
1041}
1042
1043trait 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#[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 #[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 fn check_render(expected: &str, rendered: &str) {
1185 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 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 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 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}