1use serde::{Deserialize, Serialize};
2use std::fmt::Write;
3use std::fmt::{self};
4
5#[derive(Default, Debug, Serialize, Deserialize, Clone)]
6pub struct AssertionsReport {
7 pub ran: Vec<RanAssertion>,
8 pub max: Option<usize>,
9}
10
11#[derive(Debug, Serialize, Deserialize, Clone)]
12pub struct RanAssertion {
13 pub id: String,
14 pub result: Result<RanAssertionResult, String>,
15}
16
17#[derive(Debug, Serialize, Deserialize, Clone)]
18pub struct RanAssertionResult {
19 pub analysis: Option<String>,
20 pub passed: bool,
21}
22
23impl AssertionsReport {
24 pub fn new(max: Option<usize>) -> Self {
25 AssertionsReport {
26 ran: Vec::new(),
27 max,
28 }
29 }
30
31 pub fn error(msg: String) -> Self {
32 let assert = RanAssertion {
33 id: "no-unhandled-errors".into(),
34 result: Err(msg),
35 };
36 AssertionsReport {
37 ran: vec![assert],
38 max: Some(1),
39 }
40 }
41
42 pub fn is_empty(&self) -> bool {
43 self.ran.is_empty()
44 }
45
46 pub fn total_count(&self) -> usize {
47 self.run_count().max(self.max.unwrap_or(0))
48 }
49
50 pub fn run_count(&self) -> usize {
51 self.ran.len()
52 }
53
54 pub fn passed_count(&self) -> usize {
55 self.ran
56 .iter()
57 .filter(|a| a.result.as_ref().is_ok_and(|result| result.passed))
58 .count()
59 }
60
61 pub fn passed_percentage(&self) -> f32 {
62 if self.total_count() == 0 {
63 0.0
64 } else {
65 (self.passed_count() as f32 / self.total_count() as f32) * 100.0
66 }
67 }
68}
69
70const ROUND_WIDTH: usize = "Round".len();
71const ASSERTIONS_WIDTH: usize = 42;
72const RESULTS_WIDTH: usize = 8;
73
74pub fn print_table_header() {
75 println!(
76 "┌─{}─┬─{}─┬─{}─┐",
77 "─".repeat(ROUND_WIDTH),
78 "─".repeat(ASSERTIONS_WIDTH),
79 "─".repeat(RESULTS_WIDTH)
80 );
81
82 println!(
83 "│ {:^ROUND_WIDTH$} │ {:^ASSERTIONS_WIDTH$} │ {:^RESULTS_WIDTH$} │",
84 "Round", "Assertion", "Result"
85 );
86
87 println!(
88 "├─{}─┼─{}─┼─{}─┤",
89 "─".repeat(ROUND_WIDTH),
90 "─".repeat(ASSERTIONS_WIDTH),
91 "─".repeat(RESULTS_WIDTH)
92 )
93}
94
95pub fn display_error_row(f: &mut String, round: usize, error: String) -> fmt::Result {
96 let last_two_columns = ASSERTIONS_WIDTH + RESULTS_WIDTH;
97 writeln!(
98 f,
99 "│ {:^ROUND_WIDTH$} │ {:<last_two_columns$} |",
100 round,
101 truncate(&error, last_two_columns)
102 )
103}
104
105pub fn display_table_row(f: &mut String, round: usize, assertion: &RanAssertion) -> fmt::Result {
106 let result = match &assertion.result {
107 Ok(result) if result.passed => "\x1b[32m✔︎ Passed\x1b[0m",
108 Ok(_) => "\x1b[31m✗ Failed\x1b[0m",
109 Err(_) => "\x1b[31m💥 Judge Error\x1b[0m",
110 };
111
112 writeln!(
113 f,
114 "│ {:^ROUND_WIDTH$} │ {:<ASSERTIONS_WIDTH$} │ {:>RESULTS_WIDTH$} │",
115 round,
116 truncate(&assertion.id, ASSERTIONS_WIDTH),
117 result
118 )
119}
120
121pub fn print_table_round_summary<'a>(
122 round: &str,
123 reports: impl Iterator<Item = &'a AssertionsReport>,
124) {
125 let mut passed = 0;
126 let mut total = 0;
127 for report in reports {
128 passed += report.passed_count();
129 total += report.total_count();
130 }
131
132 println!(
133 "│ {:^ROUND_WIDTH$} │ {:<ASSERTIONS_WIDTH$} │ {:>RESULTS_WIDTH$} │",
134 round,
135 "total",
136 format!("{}%", (passed as f32 / total as f32 * 100.0).floor())
137 )
138}
139
140pub fn print_table_footer() {
141 println!(
142 "└─{}─┴─{}─┴─{}─┘",
143 "─".repeat(ROUND_WIDTH),
144 "─".repeat(ASSERTIONS_WIDTH),
145 "─".repeat(RESULTS_WIDTH)
146 )
147}
148
149pub fn print_table_divider() {
150 println!(
151 "├─{}─┼─{}─┼─{}─┤",
152 "─".repeat(ROUND_WIDTH),
153 "─".repeat(ASSERTIONS_WIDTH),
154 "─".repeat(RESULTS_WIDTH)
155 )
156}
157
158fn truncate(assertion: &str, max_width: usize) -> String {
159 let is_verbose = std::env::var("VERBOSE").is_ok_and(|v| !v.is_empty());
160
161 if assertion.len() <= max_width || is_verbose {
162 assertion.to_string()
163 } else {
164 let mut end_ix = max_width - 1;
165 while !assertion.is_char_boundary(end_ix) {
166 end_ix -= 1;
167 }
168 format!("{}…", &assertion[..end_ix])
169 }
170}