1use super::UIfmt;
2use alloy_primitives::{Address, Bytes, FixedBytes, I256, U256};
3use comfy_table::{Table, TableComponent, presets::UTF8_FULL};
4use std::fmt::{self, Write};
5
6#[derive(Clone, Debug, PartialEq, Eq)]
8pub enum Piece<'a> {
9 String(&'a str),
11 NextArgument(FormatSpec),
13}
14
15#[derive(Clone, Debug, Default, PartialEq, Eq)]
17pub enum FormatSpec {
18 #[default]
20 String,
21 Number,
23 Integer,
25 Object,
27 Exponential(Option<usize>),
29 Hexadecimal,
31}
32
33impl fmt::Display for FormatSpec {
34 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35 f.write_str("%")?;
36 match *self {
37 Self::String => f.write_str("s"),
38 Self::Number => f.write_str("d"),
39 Self::Integer => f.write_str("i"),
40 Self::Object => f.write_str("o"),
41 Self::Exponential(Some(n)) => write!(f, "{n}e"),
42 Self::Exponential(None) => f.write_str("e"),
43 Self::Hexadecimal => f.write_str("x"),
44 }
45 }
46}
47
48enum ParseArgError {
49 Err,
51 Skip,
53}
54
55#[derive(Debug)]
57pub struct Parser<'a> {
58 input: &'a str,
59 chars: std::str::CharIndices<'a>,
60}
61
62impl<'a> Parser<'a> {
63 pub fn new(input: &'a str) -> Self {
65 Self { input, chars: input.char_indices() }
66 }
67
68 fn string(&mut self, start: usize, mut skip: usize) -> &'a str {
73 while let Some((pos, c)) = self.peek() {
74 if c == '%' {
75 if skip == 0 {
76 return &self.input[start..pos];
77 }
78 skip -= 1;
79 }
80 self.chars.next();
81 }
82 &self.input[start..]
83 }
84
85 fn argument(&mut self) -> Result<FormatSpec, ParseArgError> {
90 let (start, ch) = self.peek().ok_or(ParseArgError::Err)?;
91 let simple_spec = match ch {
92 's' => Some(FormatSpec::String),
93 'd' => Some(FormatSpec::Number),
94 'i' => Some(FormatSpec::Integer),
95 'o' => Some(FormatSpec::Object),
96 'e' => Some(FormatSpec::Exponential(None)),
97 'x' => Some(FormatSpec::Hexadecimal),
98 '%' => return Err(ParseArgError::Skip),
100 _ => None,
101 };
102 if let Some(spec) = simple_spec {
103 self.chars.next();
104 return Ok(spec);
105 }
106
107 if ch.is_ascii_digit() {
109 let n = self.integer(start);
110 if let Some((_, 'e')) = self.peek() {
111 self.chars.next();
112 return Ok(FormatSpec::Exponential(n));
113 }
114 }
115
116 Err(ParseArgError::Err)
117 }
118
119 fn integer(&mut self, start: usize) -> Option<usize> {
120 let mut end = start;
121 while let Some((pos, ch)) = self.peek() {
122 if !ch.is_ascii_digit() {
123 end = pos;
124 break;
125 }
126 self.chars.next();
127 }
128 self.input[start..end].parse().ok()
129 }
130
131 fn current_pos(&mut self) -> usize {
132 self.peek().map(|(n, _)| n).unwrap_or(self.input.len())
133 }
134
135 fn peek(&mut self) -> Option<(usize, char)> {
136 self.peek_n(0)
137 }
138
139 fn peek_n(&mut self, n: usize) -> Option<(usize, char)> {
140 self.chars.clone().nth(n)
141 }
142}
143
144impl<'a> Iterator for Parser<'a> {
145 type Item = Piece<'a>;
146
147 fn next(&mut self) -> Option<Self::Item> {
148 let (mut start, ch) = self.peek()?;
149 let mut skip = 0;
150 if ch == '%' {
151 let prev = self.chars.clone();
152 self.chars.next();
153 match self.argument() {
154 Ok(arg) => {
155 debug_assert_eq!(arg.to_string(), self.input[start..self.current_pos()]);
156 return Some(Piece::NextArgument(arg));
157 }
158
159 Err(ParseArgError::Skip) => {
161 start = self.current_pos();
162 skip += 1;
163 }
164
165 Err(ParseArgError::Err) => {
168 self.chars = prev;
169 skip += 1;
170 }
171 }
172 }
173 Some(Piece::String(self.string(start, skip)))
174 }
175}
176
177pub trait ConsoleFmt {
179 fn fmt(&self, spec: FormatSpec) -> String;
181}
182
183impl ConsoleFmt for String {
184 fn fmt(&self, spec: FormatSpec) -> String {
185 match spec {
186 FormatSpec::String => self.clone(),
187 FormatSpec::Object => format!("'{}'", self.clone()),
188 FormatSpec::Number
189 | FormatSpec::Integer
190 | FormatSpec::Exponential(_)
191 | FormatSpec::Hexadecimal => Self::from("NaN"),
192 }
193 }
194}
195
196impl ConsoleFmt for bool {
197 fn fmt(&self, spec: FormatSpec) -> String {
198 match spec {
199 FormatSpec::String => self.pretty(),
200 FormatSpec::Object => format!("'{}'", self.pretty()),
201 FormatSpec::Number => (*self as i32).to_string(),
202 FormatSpec::Integer | FormatSpec::Exponential(_) | FormatSpec::Hexadecimal => {
203 String::from("NaN")
204 }
205 }
206 }
207}
208
209impl ConsoleFmt for U256 {
210 fn fmt(&self, spec: FormatSpec) -> String {
211 match spec {
212 FormatSpec::String | FormatSpec::Object | FormatSpec::Number | FormatSpec::Integer => {
213 self.pretty()
214 }
215 FormatSpec::Hexadecimal => {
216 let hex = format!("{self:x}");
217 format!("0x{}", hex.trim_start_matches('0'))
218 }
219 FormatSpec::Exponential(None) => {
220 let log = self.pretty().len() - 1;
221 let exp10 = Self::from(10).pow(Self::from(log));
222 let amount = *self;
223 let integer = amount / exp10;
224 let decimal = (amount % exp10).to_string();
225 let decimal = format!("{decimal:0>log$}").trim_end_matches('0').to_string();
226 if decimal.is_empty() {
227 format!("{integer}e{log}")
228 } else {
229 format!("{integer}.{decimal}e{log}")
230 }
231 }
232 FormatSpec::Exponential(Some(precision)) => {
233 let exp10 = Self::from(10).pow(Self::from(precision));
234 let amount = *self;
235 let integer = amount / exp10;
236 let decimal = (amount % exp10).to_string();
237 let decimal = format!("{decimal:0>precision$}").trim_end_matches('0').to_string();
238 if decimal.is_empty() {
239 format!("{integer}")
240 } else {
241 format!("{integer}.{decimal}")
242 }
243 }
244 }
245 }
246}
247
248impl ConsoleFmt for I256 {
249 fn fmt(&self, spec: FormatSpec) -> String {
250 match spec {
251 FormatSpec::String | FormatSpec::Object | FormatSpec::Number | FormatSpec::Integer => {
252 self.pretty()
253 }
254 FormatSpec::Hexadecimal => {
255 let hex = format!("{self:x}");
256 format!("0x{}", hex.trim_start_matches('0'))
257 }
258 FormatSpec::Exponential(None) => {
259 let amount = *self;
260 let sign = if amount.is_negative() { "-" } else { "" };
261 let log = if amount.is_negative() {
262 self.pretty().len() - 2
263 } else {
264 self.pretty().len() - 1
265 };
266 let exp10 = Self::exp10(log);
267 let integer = (amount / exp10).twos_complement();
268 let decimal = (amount % exp10).twos_complement().to_string();
269 let decimal = format!("{decimal:0>log$}").trim_end_matches('0').to_string();
270 if decimal.is_empty() {
271 format!("{sign}{integer}e{log}")
272 } else {
273 format!("{sign}{integer}.{decimal}e{log}")
274 }
275 }
276 FormatSpec::Exponential(Some(precision)) => {
277 let amount = *self;
278 let sign = if amount.is_negative() { "-" } else { "" };
279 let exp10 = Self::exp10(precision);
280 let integer = (amount / exp10).twos_complement();
281 let decimal = (amount % exp10).twos_complement().to_string();
282 let decimal = format!("{decimal:0>precision$}").trim_end_matches('0').to_string();
283 if decimal.is_empty() {
284 format!("{sign}{integer}")
285 } else {
286 format!("{sign}{integer}.{decimal}")
287 }
288 }
289 }
290 }
291}
292
293impl ConsoleFmt for Address {
294 fn fmt(&self, spec: FormatSpec) -> String {
295 match spec {
296 FormatSpec::String | FormatSpec::Hexadecimal => self.pretty(),
297 FormatSpec::Object => format!("'{}'", self.pretty()),
298 FormatSpec::Number | FormatSpec::Integer | FormatSpec::Exponential(_) => {
299 String::from("NaN")
300 }
301 }
302 }
303}
304
305impl ConsoleFmt for Vec<u8> {
306 fn fmt(&self, spec: FormatSpec) -> String {
307 self[..].fmt(spec)
308 }
309}
310
311impl ConsoleFmt for Bytes {
312 fn fmt(&self, spec: FormatSpec) -> String {
313 self[..].fmt(spec)
314 }
315}
316
317impl<const N: usize> ConsoleFmt for [u8; N] {
318 fn fmt(&self, spec: FormatSpec) -> String {
319 self[..].fmt(spec)
320 }
321}
322
323impl<const N: usize> ConsoleFmt for FixedBytes<N> {
324 fn fmt(&self, spec: FormatSpec) -> String {
325 self[..].fmt(spec)
326 }
327}
328
329impl ConsoleFmt for [u8] {
330 fn fmt(&self, spec: FormatSpec) -> String {
331 match spec {
332 FormatSpec::String | FormatSpec::Hexadecimal => self.pretty(),
333 FormatSpec::Object => format!("'{}'", self.pretty()),
334 FormatSpec::Number | FormatSpec::Integer | FormatSpec::Exponential(_) => {
335 String::from("NaN")
336 }
337 }
338 }
339}
340
341pub fn console_format(spec: &str, values: &[&dyn ConsoleFmt]) -> String {
367 let mut values = values.iter().copied();
368 let mut result = String::with_capacity(spec.len());
369
370 let mut write_space = if spec.is_empty() {
372 false
373 } else {
374 format_spec(spec, &mut values, &mut result);
375 true
376 };
377
378 for v in values {
380 let fmt = v.fmt(FormatSpec::String);
381 if write_space {
382 result.push(' ');
383 }
384 result.push_str(&fmt);
385 write_space = true;
386 }
387
388 result
389}
390
391fn format_spec<'a>(
392 s: &str,
393 mut values: impl Iterator<Item = &'a dyn ConsoleFmt>,
394 result: &mut String,
395) {
396 for piece in Parser::new(s) {
397 match piece {
398 Piece::String(s) => result.push_str(s),
399 Piece::NextArgument(spec) => {
400 if let Some(value) = values.next() {
401 result.push_str(&value.fmt(spec));
402 } else {
403 write!(result, "{spec}").unwrap();
405 }
406 }
407 }
408 }
409}
410
411pub fn console_table_format(
412 keys: Option<&[&dyn ConsoleFmt]>,
413 values: &[&dyn ConsoleFmt],
414) -> String {
415 let keys_strings: Vec<String> = match keys {
416 Some(keys) => keys.iter().map(|k| k.fmt(FormatSpec::String)).collect(),
417 None => (0..values.len()).map(|i| i.to_string()).collect(),
418 };
419 let values_strings: Vec<String> = values.iter().map(|v| v.fmt(FormatSpec::String)).collect();
420
421 let mut table = Table::new();
422 table.load_preset(UTF8_FULL);
423 table.set_style(TableComponent::VerticalLines, '│');
424 table.set_style(TableComponent::HeaderLines, '─');
425 table.set_style(TableComponent::MiddleHeaderIntersections, '┼');
426 table.set_style(TableComponent::LeftHeaderIntersection, '├');
427 table.set_style(TableComponent::RightHeaderIntersection, '┤');
428 table.set_header(vec!["(index)", "Values"]);
429 table.remove_style(TableComponent::HorizontalLines);
430 table.remove_style(TableComponent::MiddleIntersections);
431 table.remove_style(TableComponent::LeftBorderIntersections);
432 table.remove_style(TableComponent::RightBorderIntersections);
433 for i in 0..keys_strings.len().max(values_strings.len()) {
434 let key = keys_strings.get(i).map(String::as_str).unwrap_or("");
435 let value = values_strings.get(i).map(String::as_str).unwrap_or("");
436 table.add_row(vec![key, value]);
437 }
438 table.to_string()
439}
440
441#[cfg(test)]
442mod tests {
443 use super::*;
444 use alloy_primitives::{B256, address};
445 use foundry_macros::ConsoleFmt;
446 use std::str::FromStr;
447
448 macro_rules! logf1 {
449 ($a:ident) => {
450 console_format(&$a.p_0, &[&$a.p_1])
451 };
452 }
453
454 macro_rules! logf2 {
455 ($a:ident) => {
456 console_format(&$a.p_0, &[&$a.p_1, &$a.p_2])
457 };
458 }
459
460 macro_rules! logf3 {
461 ($a:ident) => {
462 console_format(&$a.p_0, &[&$a.p_1, &$a.p_2, &$a.p_3])
463 };
464 }
465
466 #[derive(Clone, Debug, ConsoleFmt)]
467 struct Log1 {
468 p_0: String,
469 p_1: U256,
470 }
471
472 #[derive(Clone, Debug, ConsoleFmt)]
473 struct Log2 {
474 p_0: String,
475 p_1: bool,
476 p_2: U256,
477 }
478
479 #[derive(Clone, Debug, ConsoleFmt)]
480 struct Log3 {
481 p_0: String,
482 p_1: Address,
483 p_2: bool,
484 p_3: U256,
485 }
486
487 #[expect(unused)]
488 #[derive(Clone, Debug, ConsoleFmt)]
489 enum Logs {
490 Log1(Log1),
491 Log2(Log2),
492 Log3(Log3),
493 }
494
495 #[test]
496 fn test_console_log_format_specifiers() {
497 let fmt_1 = |spec: &str, arg: &dyn ConsoleFmt| console_format(spec, &[arg]);
498
499 assert_eq!("foo", fmt_1("%s", &String::from("foo")));
500 assert_eq!("NaN", fmt_1("%d", &String::from("foo")));
501 assert_eq!("NaN", fmt_1("%i", &String::from("foo")));
502 assert_eq!("NaN", fmt_1("%e", &String::from("foo")));
503 assert_eq!("NaN", fmt_1("%x", &String::from("foo")));
504 assert_eq!("'foo'", fmt_1("%o", &String::from("foo")));
505 assert_eq!("%s foo", fmt_1("%%s", &String::from("foo")));
506 assert_eq!("% foo", fmt_1("%", &String::from("foo")));
507 assert_eq!("% foo", fmt_1("%%", &String::from("foo")));
508
509 assert_eq!("true", fmt_1("%s", &true));
510 assert_eq!("1", fmt_1("%d", &true));
511 assert_eq!("0", fmt_1("%d", &false));
512 assert_eq!("NaN", fmt_1("%i", &true));
513 assert_eq!("NaN", fmt_1("%e", &true));
514 assert_eq!("NaN", fmt_1("%x", &true));
515 assert_eq!("'true'", fmt_1("%o", &true));
516
517 let b32 =
518 B256::from_str("0xdeadbeef00000000000000000000000000000000000000000000000000000000")
519 .unwrap();
520 assert_eq!(
521 "0xdeadbeef00000000000000000000000000000000000000000000000000000000",
522 fmt_1("%s", &b32)
523 );
524 assert_eq!(
525 "0xdeadbeef00000000000000000000000000000000000000000000000000000000",
526 fmt_1("%x", &b32)
527 );
528 assert_eq!("NaN", fmt_1("%d", &b32));
529 assert_eq!("NaN", fmt_1("%i", &b32));
530 assert_eq!("NaN", fmt_1("%e", &b32));
531 assert_eq!(
532 "'0xdeadbeef00000000000000000000000000000000000000000000000000000000'",
533 fmt_1("%o", &b32)
534 );
535
536 let addr = address!("0xdEADBEeF00000000000000000000000000000000");
537 assert_eq!("0xdEADBEeF00000000000000000000000000000000", fmt_1("%s", &addr));
538 assert_eq!("NaN", fmt_1("%d", &addr));
539 assert_eq!("NaN", fmt_1("%i", &addr));
540 assert_eq!("NaN", fmt_1("%e", &addr));
541 assert_eq!("0xdEADBEeF00000000000000000000000000000000", fmt_1("%x", &addr));
542 assert_eq!("'0xdEADBEeF00000000000000000000000000000000'", fmt_1("%o", &addr));
543
544 let bytes = Bytes::from_str("0xdeadbeef").unwrap();
545 assert_eq!("0xdeadbeef", fmt_1("%s", &bytes));
546 assert_eq!("NaN", fmt_1("%d", &bytes));
547 assert_eq!("NaN", fmt_1("%i", &bytes));
548 assert_eq!("NaN", fmt_1("%e", &bytes));
549 assert_eq!("0xdeadbeef", fmt_1("%x", &bytes));
550 assert_eq!("'0xdeadbeef'", fmt_1("%o", &bytes));
551
552 assert_eq!("100", fmt_1("%s", &U256::from(100)));
553 assert_eq!("100", fmt_1("%d", &U256::from(100)));
554 assert_eq!("100", fmt_1("%i", &U256::from(100)));
555 assert_eq!("1e2", fmt_1("%e", &U256::from(100)));
556 assert_eq!("1.0023e6", fmt_1("%e", &U256::from(1002300)));
557 assert_eq!("1.23e5", fmt_1("%e", &U256::from(123000)));
558 assert_eq!("0x64", fmt_1("%x", &U256::from(100)));
559 assert_eq!("100", fmt_1("%o", &U256::from(100)));
560
561 assert_eq!("100", fmt_1("%s", &I256::try_from(100).unwrap()));
562 assert_eq!("100", fmt_1("%d", &I256::try_from(100).unwrap()));
563 assert_eq!("100", fmt_1("%i", &I256::try_from(100).unwrap()));
564 assert_eq!("1e2", fmt_1("%e", &I256::try_from(100).unwrap()));
565 assert_eq!("-1e2", fmt_1("%e", &I256::try_from(-100).unwrap()));
566 assert_eq!("-1.0023e6", fmt_1("%e", &I256::try_from(-1002300).unwrap()));
567 assert_eq!("-1.23e5", fmt_1("%e", &I256::try_from(-123000).unwrap()));
568 assert_eq!("1.0023e6", fmt_1("%e", &I256::try_from(1002300).unwrap()));
569 assert_eq!("1.23e5", fmt_1("%e", &I256::try_from(123000).unwrap()));
570
571 assert_eq!("10", fmt_1("%1e", &I256::try_from(100).unwrap()));
573 assert_eq!("-1", fmt_1("%2e", &I256::try_from(-100).unwrap()));
574 assert_eq!("123000", fmt_1("%0e", &I256::try_from(123000).unwrap()));
575 assert_eq!("12300", fmt_1("%1e", &I256::try_from(123000).unwrap()));
576 assert_eq!("0.0123", fmt_1("%7e", &I256::try_from(123000).unwrap()));
577 assert_eq!("-0.0123", fmt_1("%7e", &I256::try_from(-123000).unwrap()));
578
579 assert_eq!("0x64", fmt_1("%x", &I256::try_from(100).unwrap()));
580 assert_eq!(
581 "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9c",
582 fmt_1("%x", &I256::try_from(-100).unwrap())
583 );
584 assert_eq!(
585 "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffe8b7891800",
586 fmt_1("%x", &I256::try_from(-100000000000i64).unwrap())
587 );
588 assert_eq!("100", fmt_1("%o", &I256::try_from(100).unwrap()));
589
590 assert_eq!("%333d%3e%5F", console_format("%333d%3e%5F", &[]));
592 assert_eq!(
593 "%5d123456.789%2f%3f%e1",
594 console_format("%5d%3e%2f%3f%e1", &[&U256::from(123456789)])
595 );
596 }
597
598 #[test]
599 fn test_console_log_format() {
600 let mut log1 = Log1 { p_0: "foo %s".to_string(), p_1: U256::from(100) };
601 assert_eq!("foo 100", logf1!(log1));
602 log1.p_0 = String::from("foo");
603 assert_eq!("foo 100", logf1!(log1));
604 log1.p_0 = String::from("%s foo");
605 assert_eq!("100 foo", logf1!(log1));
606
607 let mut log2 = Log2 { p_0: "foo %s %s".to_string(), p_1: true, p_2: U256::from(100) };
608 assert_eq!("foo true 100", logf2!(log2));
609 log2.p_0 = String::from("foo");
610 assert_eq!("foo true 100", logf2!(log2));
611 log2.p_0 = String::from("%s %s foo");
612 assert_eq!("true 100 foo", logf2!(log2));
613
614 let log3 = Log3 {
615 p_0: String::from("foo %s %%s %s and %d foo %%"),
616 p_1: address!("0xdEADBEeF00000000000000000000000000000000"),
617 p_2: true,
618 p_3: U256::from(21),
619 };
620 assert_eq!(
621 "foo 0xdEADBEeF00000000000000000000000000000000 %s true and 21 foo %",
622 logf3!(log3)
623 );
624
625 let log4 = Log1 { p_0: String::from("%5e"), p_1: U256::from(123456789) };
627 assert_eq!("1234.56789", logf1!(log4));
628
629 let log5 = Log1 { p_0: String::from("foo %3e bar"), p_1: U256::from(123456789) };
630 assert_eq!("foo 123456.789 bar", logf1!(log5));
631
632 let log6 =
633 Log2 { p_0: String::from("%e and %12e"), p_1: false, p_2: U256::from(123456789) };
634 assert_eq!("NaN and 0.000123456789", logf2!(log6));
635 }
636
637 #[test]
638 fn test_derive_format() {
639 let log1 = Log1 { p_0: String::from("foo %s bar"), p_1: U256::from(42) };
640 assert_eq!(log1.fmt(Default::default()), "foo 42 bar");
641 let call = Logs::Log1(log1);
642 assert_eq!(call.fmt(Default::default()), "foo 42 bar");
643 }
644
645 #[test]
646 fn test_console_table_format() {
647 let values: &[&dyn ConsoleFmt] = &[&U256::from(100), &U256::from(200), &U256::from(300)];
649 assert_eq!(
650 console_table_format(None, values),
651 "┌─────────┬────────┐\n\
652 │ (index) │ Values │\n\
653 ├─────────┼────────┤\n\
654 │ 0 │ 100 │\n\
655 │ 1 │ 200 │\n\
656 │ 2 │ 300 │\n\
657 └─────────┴────────┘"
658 );
659
660 let keys: &[&dyn ConsoleFmt] =
663 &[&String::from("alice"), &String::from("bob"), &String::from("charlie123")];
664 let values: &[&dyn ConsoleFmt] = &[
665 &U256::from(1),
666 &U256::from_str("20000000000000000").unwrap(),
667 &U256::from_str("30000000000").unwrap(),
668 ];
669 assert_eq!(
670 console_table_format(Some(keys), values),
671 "┌────────────┬───────────────────┐\n\
672 │ (index) │ Values │\n\
673 ├────────────┼───────────────────┤\n\
674 │ alice │ 1 │\n\
675 │ bob │ 20000000000000000 │\n\
676 │ charlie123 │ 30000000000 │\n\
677 └────────────┴───────────────────┘"
678 );
679
680 assert_eq!(
682 console_table_format(None, &[]),
683 "┌─────────┬────────┐\n\
684 │ (index) │ Values │\n\
685 ├─────────┼────────┤\n\
686 └─────────┴────────┘"
687 );
688
689 let keys: &[&dyn ConsoleFmt] =
691 &[&String::from("alice"), &String::from("bob"), &String::from("charlie")];
692 let values: &[&dyn ConsoleFmt] = &[&U256::from(1), &U256::from(2)];
693 assert_eq!(
694 console_table_format(Some(keys), values),
695 "┌─────────┬────────┐\n\
696 │ (index) │ Values │\n\
697 ├─────────┼────────┤\n\
698 │ alice │ 1 │\n\
699 │ bob │ 2 │\n\
700 │ charlie │ │\n\
701 └─────────┴────────┘"
702 );
703
704 let keys: &[&dyn ConsoleFmt] = &[&String::from("alice"), &String::from("bob")];
706 let values: &[&dyn ConsoleFmt] =
707 &[&U256::from(1), &U256::from(2), &U256::from(3), &U256::from(4)];
708 assert_eq!(
709 console_table_format(Some(keys), values),
710 "┌─────────┬────────┐\n\
711 │ (index) │ Values │\n\
712 ├─────────┼────────┤\n\
713 │ alice │ 1 │\n\
714 │ bob │ 2 │\n\
715 │ │ 3 │\n\
716 │ │ 4 │\n\
717 └─────────┴────────┘"
718 );
719 }
720}