Skip to main content

foundry_common_fmt/
console.rs

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/// A piece is a portion of the format string which represents the next part to emit.
7#[derive(Clone, Debug, PartialEq, Eq)]
8pub enum Piece<'a> {
9    /// A literal string which should directly be emitted.
10    String(&'a str),
11    /// A format specifier which should be replaced with the next argument.
12    NextArgument(FormatSpec),
13}
14
15/// A format specifier.
16#[derive(Clone, Debug, Default, PartialEq, Eq)]
17pub enum FormatSpec {
18    /// `%s`
19    #[default]
20    String,
21    /// `%d`
22    Number,
23    /// `%i`
24    Integer,
25    /// `%o`
26    Object,
27    /// `%e`, `%18e`
28    Exponential(Option<usize>),
29    /// `%x`
30    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    /// Failed to parse the argument.
50    Err,
51    /// Escape `%%`.
52    Skip,
53}
54
55/// Parses a format string into a sequence of [pieces][Piece].
56#[derive(Debug)]
57pub struct Parser<'a> {
58    input: &'a str,
59    chars: std::str::CharIndices<'a>,
60}
61
62impl<'a> Parser<'a> {
63    /// Creates a new parser for the given input.
64    pub fn new(input: &'a str) -> Self {
65        Self { input, chars: input.char_indices() }
66    }
67
68    /// Parses a string until the next format specifier.
69    ///
70    /// `skip` is the number of format specifier characters (`%`) to ignore before returning the
71    /// string.
72    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    /// Parses a format specifier.
86    ///
87    /// If `Err` is returned, the internal iterator may have been advanced and it may be in an
88    /// invalid state.
89    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            // "%%" is a literal '%'.
99            '%' => 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        // %<n>e
108        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                // Skip the argument if we encountered "%%".
160                Err(ParseArgError::Skip) => {
161                    start = self.current_pos();
162                    skip += 1;
163                }
164
165                // Reset the iterator if we failed to parse the argument, and include any
166                // parsed and unparsed specifier in `String`.
167                Err(ParseArgError::Err) => {
168                    self.chars = prev;
169                    skip += 1;
170                }
171            }
172        }
173        Some(Piece::String(self.string(start, skip)))
174    }
175}
176
177/// Formats a value using a [FormatSpec].
178pub trait ConsoleFmt {
179    /// Formats a value using a [FormatSpec].
180    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
341/// Formats a string using the input values.
342///
343/// Formatting rules are the same as Hardhat. The supported format specifiers are as follows:
344/// - %s: Converts the value using its String representation. This is equivalent to applying
345///   [`UIfmt::pretty()`] on the format string.
346/// - %o: Treats the format value as a javascript "object" and converts it to its string
347///   representation.
348/// - %d, %i: Converts the value to an integer. If a non-numeric value, such as String or Address,
349///   is passed, then the spec is formatted as `NaN`.
350/// - %x: Converts the value to a hexadecimal string. If a non-numeric value, such as String or
351///   Address, is passed, then the spec is formatted as `NaN`.
352/// - %e: Converts the value to an exponential notation string. If a non-numeric value, such as
353///   String or Address, is passed, then the spec is formatted as `NaN`.
354/// - %%: This is parsed as a single percent sign ('%') without consuming any input value.
355///
356/// Unformatted values are appended to the end of the formatted output using [`UIfmt::pretty()`].
357/// If there are more format specifiers than values, then the remaining unparsed format specifiers
358/// appended to the formatted output as-is.
359///
360/// # Examples
361///
362/// ```ignore (not implemented for integers)
363/// let formatted = foundry_common::fmt::console_format("%s has %d characters", &[&"foo", &3]);
364/// assert_eq!(formatted, "foo has 3 characters");
365/// ```
366pub 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    // for the first space
371    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    // append any remaining values with the standard format
379    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 the format specifier as-is if there are no more values.
404                    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        // %ne
572        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        // make sure that %byte values are not consumed when there are no values
591        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        // %ne
626        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        // auto-indexed, uint256 values
648        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        // string keys, uint256 values
661        // key col expands to fit "charlie123" and value col expands to fit "20000000000000000"
662        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        // empty table
681        assert_eq!(
682            console_table_format(None, &[]),
683            "┌─────────┬────────┐\n\
684             │ (index) │ Values │\n\
685             ├─────────┼────────┤\n\
686             └─────────┴────────┘"
687        );
688
689        // more keys than values
690        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        // more values than keys
705        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}