1 use std::io::{Error, ErrorKind, Result};
2
3 use super::Exporter;
4 use crate::benchmark_result::BenchmarkResult;
5 use crate::format::format_duration_value;
6 use crate::relative_speed::{self, BenchmarkResultWithRelativeSpeed};
7 use crate::units::Unit;
8
9 #[derive(Default)]
10 pub struct MarkdownExporter {}
11
12 impl Exporter for MarkdownExporter {
serialize(&self, results: &[BenchmarkResult], unit: Option<Unit>) -> Result<Vec<u8>>13 fn serialize(&self, results: &[BenchmarkResult], unit: Option<Unit>) -> Result<Vec<u8>> {
14 let unit = if let Some(unit) = unit {
15 // Use the given unit for all entries.
16 unit
17 } else if let Some(first_result) = results.first() {
18 // Use the first BenchmarkResult entry to determine the unit for all entries.
19 format_duration_value(first_result.mean, None).1
20 } else {
21 // Default to `Second`.
22 Unit::Second
23 };
24
25 if let Some(annotated_results) = relative_speed::compute(results) {
26 let mut destination = start_table(unit);
27
28 for result in annotated_results {
29 add_table_row(&mut destination, &result, unit);
30 }
31
32 Ok(destination)
33 } else {
34 Err(Error::new(
35 ErrorKind::Other,
36 "Relative speed comparison is not available for Markdown export.",
37 ))
38 }
39 }
40 }
41
table_header(unit_short_name: String) -> String42 fn table_header(unit_short_name: String) -> String {
43 format!(
44 "| Command | Mean [{unit}] | Min [{unit}] | Max [{unit}] | Relative |\n|:---|---:|---:|---:|---:|\n",
45 unit = unit_short_name
46 )
47 }
48
start_table(unit: Unit) -> Vec<u8>49 fn start_table(unit: Unit) -> Vec<u8> {
50 table_header(unit.short_name()).bytes().collect()
51 }
52
add_table_row(dest: &mut Vec<u8>, entry: &BenchmarkResultWithRelativeSpeed, unit: Unit)53 fn add_table_row(dest: &mut Vec<u8>, entry: &BenchmarkResultWithRelativeSpeed, unit: Unit) {
54 let result = &entry.result;
55 let mean_str = format_duration_value(result.mean, Some(unit)).0;
56 let stddev_str = format_duration_value(result.stddev, Some(unit)).0;
57 let min_str = format_duration_value(result.min, Some(unit)).0;
58 let max_str = format_duration_value(result.max, Some(unit)).0;
59 let rel_str = format!("{:.2}", entry.relative_speed);
60 let rel_stddev_str = if entry.is_fastest {
61 "".into()
62 } else {
63 format!(" ± {:.2}", entry.relative_speed_stddev)
64 };
65
66 dest.extend(
67 format!(
68 "| `{command}` | {mean} ± {stddev} | {min} | {max} | {rel}{rel_stddev} |\n",
69 command = result.command.replace("|", "\\|"),
70 mean = mean_str,
71 stddev = stddev_str,
72 min = min_str,
73 max = max_str,
74 rel = rel_str,
75 rel_stddev = rel_stddev_str,
76 )
77 .as_bytes(),
78 );
79 }
80
81 /// Ensure the markdown output includes the table header and the multiple
82 /// benchmark results as a table. The list of actual times is not included
83 /// in the output.
84 ///
85 /// This also demonstrates that the first entry's units (ms) are used to set
86 /// the units for all entries when the time unit is not given.
87 #[test]
test_markdown_format_ms()88 fn test_markdown_format_ms() {
89 use std::collections::BTreeMap;
90 let exporter = MarkdownExporter::default();
91
92 let timing_results = vec![
93 BenchmarkResult::new(
94 String::from("sleep 0.1"),
95 0.1057, // mean
96 0.0016, // std dev
97 0.1057, // median
98 0.0009, // user_mean
99 0.0011, // system_mean
100 0.1023, // min
101 0.1080, // max
102 vec![0.1, 0.1, 0.1], // times
103 vec![Some(0), Some(0), Some(0)], // exit codes
104 BTreeMap::new(), // parameter
105 ),
106 BenchmarkResult::new(
107 String::from("sleep 2"),
108 2.0050, // mean
109 0.0020, // std dev
110 2.0050, // median
111 0.0009, // user_mean
112 0.0012, // system_mean
113 2.0020, // min
114 2.0080, // max
115 vec![2.0, 2.0, 2.0], // times
116 vec![Some(0), Some(0), Some(0)], // exit codes
117 BTreeMap::new(), // parameter
118 ),
119 ];
120
121 let formatted = String::from_utf8(exporter.serialize(&timing_results, None).unwrap()).unwrap();
122
123 let formatted_expected = format!(
124 "{}\
125 | `sleep 0.1` | 105.7 ± 1.6 | 102.3 | 108.0 | 1.00 |
126 | `sleep 2` | 2005.0 ± 2.0 | 2002.0 | 2008.0 | 18.97 ± 0.29 |
127 ",
128 table_header("ms".to_string())
129 );
130
131 assert_eq!(formatted_expected, formatted);
132 }
133
134 /// This (again) demonstrates that the first entry's units (s) are used to set
135 /// the units for all entries when the time unit is not given.
136 #[test]
test_markdown_format_s()137 fn test_markdown_format_s() {
138 use std::collections::BTreeMap;
139 let exporter = MarkdownExporter::default();
140
141 let timing_results = vec![
142 BenchmarkResult::new(
143 String::from("sleep 2"),
144 2.0050, // mean
145 0.0020, // std dev
146 2.0050, // median
147 0.0009, // user_mean
148 0.0012, // system_mean
149 2.0020, // min
150 2.0080, // max
151 vec![2.0, 2.0, 2.0], // times
152 vec![Some(0), Some(0), Some(0)], // exit codes
153 BTreeMap::new(), // parameter
154 ),
155 BenchmarkResult::new(
156 String::from("sleep 0.1"),
157 0.1057, // mean
158 0.0016, // std dev
159 0.1057, // median
160 0.0009, // user_mean
161 0.0011, // system_mean
162 0.1023, // min
163 0.1080, // max
164 vec![0.1, 0.1, 0.1], // times
165 vec![Some(0), Some(0), Some(0)], // exit codes
166 BTreeMap::new(), // parameter
167 ),
168 ];
169
170 let formatted = String::from_utf8(exporter.serialize(&timing_results, None).unwrap()).unwrap();
171
172 let formatted_expected = format!(
173 "{}\
174 | `sleep 2` | 2.005 ± 0.002 | 2.002 | 2.008 | 18.97 ± 0.29 |
175 | `sleep 0.1` | 0.106 ± 0.002 | 0.102 | 0.108 | 1.00 |
176 ",
177 table_header("s".to_string())
178 );
179
180 assert_eq!(formatted_expected, formatted);
181 }
182
183 /// The given time unit (s) is used to set the units for all entries.
184 #[test]
test_markdown_format_time_unit_s()185 fn test_markdown_format_time_unit_s() {
186 use std::collections::BTreeMap;
187 let exporter = MarkdownExporter::default();
188
189 let timing_results = vec![
190 BenchmarkResult::new(
191 String::from("sleep 0.1"),
192 0.1057, // mean
193 0.0016, // std dev
194 0.1057, // median
195 0.0009, // user_mean
196 0.0011, // system_mean
197 0.1023, // min
198 0.1080, // max
199 vec![0.1, 0.1, 0.1], // times
200 vec![Some(0), Some(0), Some(0)], // exit codes
201 BTreeMap::new(), // parameter
202 ),
203 BenchmarkResult::new(
204 String::from("sleep 2"),
205 2.0050, // mean
206 0.0020, // std dev
207 2.0050, // median
208 0.0009, // user_mean
209 0.0012, // system_mean
210 2.0020, // min
211 2.0080, // max
212 vec![2.0, 2.0, 2.0], // times
213 vec![Some(0), Some(0), Some(0)], // exit codes
214 BTreeMap::new(), // parameter
215 ),
216 ];
217
218 let formatted = String::from_utf8(
219 exporter
220 .serialize(&timing_results, Some(Unit::Second))
221 .unwrap(),
222 )
223 .unwrap();
224
225 let formatted_expected = format!(
226 "{}\
227 | `sleep 0.1` | 0.106 ± 0.002 | 0.102 | 0.108 | 1.00 |
228 | `sleep 2` | 2.005 ± 0.002 | 2.002 | 2.008 | 18.97 ± 0.29 |
229 ",
230 table_header("s".to_string())
231 );
232
233 assert_eq!(formatted_expected, formatted);
234 }
235
236 /// This (again) demonstrates that the given time unit (ms) is used to set
237 /// the units for all entries.
238 #[test]
test_markdown_format_time_unit_ms()239 fn test_markdown_format_time_unit_ms() {
240 use std::collections::BTreeMap;
241 let exporter = MarkdownExporter::default();
242
243 let timing_results = vec![
244 BenchmarkResult::new(
245 String::from("sleep 2"),
246 2.0050, // mean
247 0.0020, // std dev
248 2.0050, // median
249 0.0009, // user_mean
250 0.0012, // system_mean
251 2.0020, // min
252 2.0080, // max
253 vec![2.0, 2.0, 2.0], // times
254 vec![Some(0), Some(0), Some(0)], // exit codes
255 BTreeMap::new(), // parameter
256 ),
257 BenchmarkResult::new(
258 String::from("sleep 0.1"),
259 0.1057, // mean
260 0.0016, // std dev
261 0.1057, // median
262 0.0009, // user_mean
263 0.0011, // system_mean
264 0.1023, // min
265 0.1080, // max
266 vec![0.1, 0.1, 0.1], // times
267 vec![Some(0), Some(0), Some(0)], // exit codes
268 BTreeMap::new(), // parameter
269 ),
270 ];
271
272 let formatted = String::from_utf8(
273 exporter
274 .serialize(&timing_results, Some(Unit::MilliSecond))
275 .unwrap(),
276 )
277 .unwrap();
278
279 let formatted_expected = format!(
280 "{}\
281 | `sleep 2` | 2005.0 ± 2.0 | 2002.0 | 2008.0 | 18.97 ± 0.29 |
282 | `sleep 0.1` | 105.7 ± 1.6 | 102.3 | 108.0 | 1.00 |
283 ",
284 table_header("ms".to_string())
285 );
286
287 assert_eq!(formatted_expected, formatted);
288 }
289