Skip to main content

foundry_common_fmt/
console.rs

1use super::UIfmt;
2use alloy_primitives::{Address, Bytes, FixedBytes, I256, U256};
3use comfy_table::{ContentLineStyle, LineStyle, Table, TableStyle};
4use std::fmt::{self, Write};
5
6/// Maximum accepted `%<n>e` precision.
7const MAX_EXPONENTIAL_PRECISION: usize = 1024;
8
9/// A piece is a portion of the format string which represents the next part to emit.
10#[derive(Clone, Debug, PartialEq, Eq)]
11pub enum Piece<'a> {
12    /// A literal string which should directly be emitted.
13    String(&'a str),
14    /// A format specifier which should be replaced with the next argument.
15    NextArgument(FormatSpec),
16}
17
18/// A format specifier.
19#[derive(Clone, Debug, Default, PartialEq, Eq)]
20pub enum FormatSpec {
21    /// `%s`
22    #[default]
23    String,
24    /// `%d`
25    Number,
26    /// `%i`
27    Integer,
28    /// `%o`
29    Object,
30    /// `%e`, `%18e`
31    Exponential(Option<usize>),
32    /// `%x`
33    Hexadecimal,
34}
35
36impl fmt::Display for FormatSpec {
37    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38        f.write_str("%")?;
39        match *self {
40            Self::String => f.write_str("s"),
41            Self::Number => f.write_str("d"),
42            Self::Integer => f.write_str("i"),
43            Self::Object => f.write_str("o"),
44            Self::Exponential(Some(n)) => write!(f, "{n}e"),
45            Self::Exponential(None) => f.write_str("e"),
46            Self::Hexadecimal => f.write_str("x"),
47        }
48    }
49}
50
51enum ParseArgError {
52    /// Failed to parse the argument.
53    Err,
54    /// Escape `%%`.
55    Skip,
56}
57
58/// Parses a format string into a sequence of [pieces][Piece].
59#[derive(Debug)]
60pub struct Parser<'a> {
61    input: &'a str,
62    chars: std::str::CharIndices<'a>,
63}
64
65impl<'a> Parser<'a> {
66    /// Creates a new parser for the given input.
67    pub fn new(input: &'a str) -> Self {
68        Self { input, chars: input.char_indices() }
69    }
70
71    /// Parses a string until the next format specifier.
72    ///
73    /// `skip` is the number of format specifier characters (`%`) to ignore before returning the
74    /// string.
75    fn string(&mut self, start: usize, mut skip: usize) -> &'a str {
76        while let Some((pos, c)) = self.peek() {
77            if c == '%' {
78                if skip == 0 {
79                    return &self.input[start..pos];
80                }
81                skip -= 1;
82            }
83            self.chars.next();
84        }
85        &self.input[start..]
86    }
87
88    /// Parses a format specifier.
89    ///
90    /// If `Err` is returned, the internal iterator may have been advanced and it may be in an
91    /// invalid state.
92    fn argument(&mut self) -> Result<FormatSpec, ParseArgError> {
93        let (start, ch) = self.peek().ok_or(ParseArgError::Err)?;
94        let simple_spec = match ch {
95            's' => Some(FormatSpec::String),
96            'd' => Some(FormatSpec::Number),
97            'i' => Some(FormatSpec::Integer),
98            'o' => Some(FormatSpec::Object),
99            'e' => Some(FormatSpec::Exponential(None)),
100            'x' => Some(FormatSpec::Hexadecimal),
101            // "%%" is a literal '%'.
102            '%' => return Err(ParseArgError::Skip),
103            _ => None,
104        };
105        if let Some(spec) = simple_spec {
106            self.chars.next();
107            return Ok(spec);
108        }
109
110        // %<n>e
111        if ch.is_ascii_digit() {
112            let n = self.integer(start);
113            if let Some((_, 'e')) = self.peek() {
114                self.chars.next();
115                return n
116                    .filter(|&precision| precision <= MAX_EXPONENTIAL_PRECISION)
117                    .map(|precision| FormatSpec::Exponential(Some(precision)))
118                    .ok_or(ParseArgError::Err);
119            }
120        }
121
122        Err(ParseArgError::Err)
123    }
124
125    fn integer(&mut self, start: usize) -> Option<usize> {
126        let mut end = start;
127        while let Some((pos, ch)) = self.peek() {
128            if !ch.is_ascii_digit() {
129                end = pos;
130                break;
131            }
132            self.chars.next();
133        }
134        self.input[start..end].parse().ok()
135    }
136
137    fn current_pos(&mut self) -> usize {
138        self.peek().map(|(n, _)| n).unwrap_or(self.input.len())
139    }
140
141    fn peek(&mut self) -> Option<(usize, char)> {
142        self.peek_n(0)
143    }
144
145    fn peek_n(&mut self, n: usize) -> Option<(usize, char)> {
146        self.chars.clone().nth(n)
147    }
148}
149
150impl<'a> Iterator for Parser<'a> {
151    type Item = Piece<'a>;
152
153    fn next(&mut self) -> Option<Self::Item> {
154        let (mut start, ch) = self.peek()?;
155        let mut skip = 0;
156        if ch == '%' {
157            let prev = self.chars.clone();
158            self.chars.next();
159            match self.argument() {
160                Ok(arg) => {
161                    debug_assert_eq!(arg.to_string(), self.input[start..self.current_pos()]);
162                    return Some(Piece::NextArgument(arg));
163                }
164
165                // Skip the argument if we encountered "%%".
166                Err(ParseArgError::Skip) => {
167                    start = self.current_pos();
168                    skip += 1;
169                }
170
171                // Reset the iterator if we failed to parse the argument, and include any
172                // parsed and unparsed specifier in `String`.
173                Err(ParseArgError::Err) => {
174                    self.chars = prev;
175                    skip += 1;
176                }
177            }
178        }
179        Some(Piece::String(self.string(start, skip)))
180    }
181}
182
183/// Formats a value using a [FormatSpec].
184pub trait ConsoleFmt {
185    /// Formats a value using a [FormatSpec].
186    fn fmt(&self, spec: FormatSpec) -> String;
187}
188
189impl ConsoleFmt for String {
190    fn fmt(&self, spec: FormatSpec) -> String {
191        match spec {
192            FormatSpec::String => self.clone(),
193            FormatSpec::Object => format!("'{}'", self.clone()),
194            FormatSpec::Number
195            | FormatSpec::Integer
196            | FormatSpec::Exponential(_)
197            | FormatSpec::Hexadecimal => Self::from("NaN"),
198        }
199    }
200}
201
202impl ConsoleFmt for bool {
203    fn fmt(&self, spec: FormatSpec) -> String {
204        match spec {
205            FormatSpec::String => self.pretty(),
206            FormatSpec::Object => format!("'{}'", self.pretty()),
207            FormatSpec::Number => (*self as i32).to_string(),
208            FormatSpec::Integer | FormatSpec::Exponential(_) | FormatSpec::Hexadecimal => {
209                String::from("NaN")
210            }
211        }
212    }
213}
214
215impl ConsoleFmt for U256 {
216    fn fmt(&self, spec: FormatSpec) -> String {
217        match spec {
218            FormatSpec::String | FormatSpec::Object | FormatSpec::Number | FormatSpec::Integer => {
219                self.pretty()
220            }
221            FormatSpec::Hexadecimal => {
222                let hex = format!("{self:x}");
223                let digits = hex.trim_start_matches('0');
224                format!("0x{}", if digits.is_empty() { "0" } else { digits })
225            }
226            FormatSpec::Exponential(None) => {
227                let log = self.pretty().len() - 1;
228                let exp10 = Self::from(10).pow(Self::from(log));
229                let amount = *self;
230                let integer = amount / exp10;
231                let decimal = (amount % exp10).to_string();
232                let decimal = format!("{decimal:0>log$}").trim_end_matches('0').to_string();
233                if decimal.is_empty() {
234                    format!("{integer}e{log}")
235                } else {
236                    format!("{integer}.{decimal}e{log}")
237                }
238            }
239            FormatSpec::Exponential(Some(precision)) => format_fixed(*self, "", precision),
240        }
241    }
242}
243
244impl ConsoleFmt for I256 {
245    fn fmt(&self, spec: FormatSpec) -> String {
246        match spec {
247            FormatSpec::String | FormatSpec::Object | FormatSpec::Number | FormatSpec::Integer => {
248                self.pretty()
249            }
250            FormatSpec::Hexadecimal => {
251                let hex = format!("{self:x}");
252                let digits = hex.trim_start_matches('0');
253                format!("0x{}", if digits.is_empty() { "0" } else { digits })
254            }
255            FormatSpec::Exponential(None) => {
256                let amount = *self;
257                let sign = if amount.is_negative() { "-" } else { "" };
258                let log = if amount.is_negative() {
259                    self.pretty().len() - 2
260                } else {
261                    self.pretty().len() - 1
262                };
263                let exp10 = Self::exp10(log);
264                let integer = (amount / exp10).twos_complement();
265                let decimal = (amount % exp10).twos_complement().to_string();
266                let decimal = format!("{decimal:0>log$}").trim_end_matches('0').to_string();
267                if decimal.is_empty() {
268                    format!("{sign}{integer}e{log}")
269                } else {
270                    format!("{sign}{integer}.{decimal}e{log}")
271                }
272            }
273            FormatSpec::Exponential(Some(precision)) => {
274                let amount = *self;
275                let sign = if amount.is_negative() { "-" } else { "" };
276                format_fixed(amount.unsigned_abs(), sign, precision)
277            }
278        }
279    }
280}
281
282fn format_fixed(amount: U256, sign: &str, precision: usize) -> String {
283    let (integer, decimal) = U256::from(10)
284        .checked_pow(U256::from(precision))
285        .map_or((U256::ZERO, amount), |exp10| (amount / exp10, amount % exp10));
286    let decimal = decimal.to_string();
287    let decimal = format!("{decimal:0>precision$}").trim_end_matches('0').to_string();
288    if decimal.is_empty() {
289        format!("{sign}{integer}")
290    } else {
291        format!("{sign}{integer}.{decimal}")
292    }
293}
294
295impl ConsoleFmt for Address {
296    fn fmt(&self, spec: FormatSpec) -> String {
297        match spec {
298            FormatSpec::String | FormatSpec::Hexadecimal => self.pretty(),
299            FormatSpec::Object => format!("'{}'", self.pretty()),
300            FormatSpec::Number | FormatSpec::Integer | FormatSpec::Exponential(_) => {
301                String::from("NaN")
302            }
303        }
304    }
305}
306
307impl ConsoleFmt for Vec<u8> {
308    fn fmt(&self, spec: FormatSpec) -> String {
309        self[..].fmt(spec)
310    }
311}
312
313impl ConsoleFmt for Bytes {
314    fn fmt(&self, spec: FormatSpec) -> String {
315        self[..].fmt(spec)
316    }
317}
318
319impl<const N: usize> ConsoleFmt for [u8; N] {
320    fn fmt(&self, spec: FormatSpec) -> String {
321        self[..].fmt(spec)
322    }
323}
324
325impl<const N: usize> ConsoleFmt for FixedBytes<N> {
326    fn fmt(&self, spec: FormatSpec) -> String {
327        self[..].fmt(spec)
328    }
329}
330
331impl ConsoleFmt for [u8] {
332    fn fmt(&self, spec: FormatSpec) -> String {
333        match spec {
334            FormatSpec::String | FormatSpec::Hexadecimal => self.pretty(),
335            FormatSpec::Object => format!("'{}'", self.pretty()),
336            FormatSpec::Number | FormatSpec::Integer | FormatSpec::Exponential(_) => {
337                String::from("NaN")
338            }
339        }
340    }
341}
342
343/// Formats a string using the input values.
344///
345/// Formatting rules are the same as Hardhat. The supported format specifiers are as follows:
346/// - %s: Converts the value using its String representation. This is equivalent to applying
347///   [`UIfmt::pretty()`] on the format string.
348/// - %o: Treats the format value as a javascript "object" and converts it to its string
349///   representation.
350/// - %d, %i: Converts the value to an integer. If a non-numeric value, such as String or Address,
351///   is passed, then the spec is formatted as `NaN`.
352/// - %x: Converts the value to a hexadecimal string. If a non-numeric value, such as String or
353///   Address, is passed, then the spec is formatted as `NaN`.
354/// - %e: Converts the value to an exponential notation string. If a non-numeric value, such as
355///   String or Address, is passed, then the spec is formatted as `NaN`.
356/// - %%: This is parsed as a single percent sign ('%') without consuming any input value.
357///
358/// Unformatted values are appended to the end of the formatted output using [`UIfmt::pretty()`].
359/// If there are more format specifiers than values, then the remaining unparsed format specifiers
360/// appended to the formatted output as-is.
361///
362/// # Examples
363///
364/// ```ignore (not implemented for integers)
365/// let formatted = foundry_common::fmt::console_format("%s has %d characters", &[&"foo", &3]);
366/// assert_eq!(formatted, "foo has 3 characters");
367/// ```
368pub fn console_format(spec: &str, values: &[&dyn ConsoleFmt]) -> String {
369    let mut values = values.iter().copied();
370    let mut result = String::with_capacity(spec.len());
371
372    // for the first space
373    let mut write_space = if spec.is_empty() {
374        false
375    } else {
376        format_spec(spec, &mut values, &mut result);
377        true
378    };
379
380    // append any remaining values with the standard format
381    for v in values {
382        let fmt = v.fmt(FormatSpec::String);
383        if write_space {
384            result.push(' ');
385        }
386        result.push_str(&fmt);
387        write_space = true;
388    }
389
390    result
391}
392
393fn format_spec<'a>(
394    s: &str,
395    mut values: impl Iterator<Item = &'a dyn ConsoleFmt>,
396    result: &mut String,
397) {
398    for piece in Parser::new(s) {
399        match piece {
400            Piece::String(s) => result.push_str(s),
401            Piece::NextArgument(spec) => {
402                if let Some(value) = values.next() {
403                    result.push_str(&value.fmt(spec));
404                } else {
405                    // Write the format specifier as-is if there are no more values.
406                    write!(result, "{spec}").unwrap();
407                }
408            }
409        }
410    }
411}
412
413pub fn console_table_format(
414    keys: Option<&[&dyn ConsoleFmt]>,
415    values: &[&dyn ConsoleFmt],
416) -> String {
417    let keys_strings: Vec<String> = match keys {
418        Some(keys) => keys.iter().map(|k| k.fmt(FormatSpec::String)).collect(),
419        None => (0..values.len()).map(|i| i.to_string()).collect(),
420    };
421    let values_strings: Vec<String> = values.iter().map(|v| v.fmt(FormatSpec::String)).collect();
422
423    const STYLE: TableStyle = TableStyle::new()
424        .top_border(LineStyle::new('┌', '─', '┬', '┐'))
425        .header_lines(ContentLineStyle::new('│', '│', '│'))
426        .header_separator(LineStyle::new('├', '─', '┼', '┤'))
427        .content_lines(ContentLineStyle::new('│', '│', '│'))
428        .bottom_border(LineStyle::new('└', '─', '┴', '┘'));
429
430    let mut table = Table::new();
431    table.load_style(STYLE);
432    table.set_header(vec!["(index)", "Values"]);
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
444    use super::*;
445    use alloy_primitives::{address, b256, bytes};
446    use foundry_macros::ConsoleFmt;
447    use std::str::FromStr;
448
449    macro_rules! logf1 {
450        ($a:ident) => {
451            console_format(&$a.p_0, &[&$a.p_1])
452        };
453    }
454
455    macro_rules! logf2 {
456        ($a:ident) => {
457            console_format(&$a.p_0, &[&$a.p_1, &$a.p_2])
458        };
459    }
460
461    macro_rules! logf3 {
462        ($a:ident) => {
463            console_format(&$a.p_0, &[&$a.p_1, &$a.p_2, &$a.p_3])
464        };
465    }
466
467    #[derive(Clone, Debug, ConsoleFmt)]
468    struct Log1 {
469        p_0: String,
470        p_1: U256,
471    }
472
473    #[derive(Clone, Debug, ConsoleFmt)]
474    struct Log2 {
475        p_0: String,
476        p_1: bool,
477        p_2: U256,
478    }
479
480    #[derive(Clone, Debug, ConsoleFmt)]
481    struct Log3 {
482        p_0: String,
483        p_1: Address,
484        p_2: bool,
485        p_3: U256,
486    }
487
488    #[expect(unused)]
489    #[derive(Clone, Debug, ConsoleFmt)]
490    enum Logs {
491        Log1(Log1),
492        Log2(Log2),
493        Log3(Log3),
494    }
495
496    #[test]
497    fn test_console_log_format_specifiers() {
498        let fmt_1 = |spec: &str, arg: &dyn ConsoleFmt| console_format(spec, &[arg]);
499
500        assert_eq!("foo", fmt_1("%s", &String::from("foo")));
501        assert_eq!("NaN", fmt_1("%d", &String::from("foo")));
502        assert_eq!("NaN", fmt_1("%i", &String::from("foo")));
503        assert_eq!("NaN", fmt_1("%e", &String::from("foo")));
504        assert_eq!("NaN", fmt_1("%x", &String::from("foo")));
505        assert_eq!("'foo'", fmt_1("%o", &String::from("foo")));
506        assert_eq!("%s foo", fmt_1("%%s", &String::from("foo")));
507        assert_eq!("% foo", fmt_1("%", &String::from("foo")));
508        assert_eq!("% foo", fmt_1("%%", &String::from("foo")));
509
510        assert_eq!("true", fmt_1("%s", &true));
511        assert_eq!("1", fmt_1("%d", &true));
512        assert_eq!("0", fmt_1("%d", &false));
513        assert_eq!("NaN", fmt_1("%i", &true));
514        assert_eq!("NaN", fmt_1("%e", &true));
515        assert_eq!("NaN", fmt_1("%x", &true));
516        assert_eq!("'true'", fmt_1("%o", &true));
517
518        let b32 = b256!("0xdeadbeef00000000000000000000000000000000000000000000000000000000");
519        assert_eq!(
520            "0xdeadbeef00000000000000000000000000000000000000000000000000000000",
521            fmt_1("%s", &b32)
522        );
523        assert_eq!(
524            "0xdeadbeef00000000000000000000000000000000000000000000000000000000",
525            fmt_1("%x", &b32)
526        );
527        assert_eq!("NaN", fmt_1("%d", &b32));
528        assert_eq!("NaN", fmt_1("%i", &b32));
529        assert_eq!("NaN", fmt_1("%e", &b32));
530        assert_eq!(
531            "'0xdeadbeef00000000000000000000000000000000000000000000000000000000'",
532            fmt_1("%o", &b32)
533        );
534
535        let addr = address!("0xdEADBEeF00000000000000000000000000000000");
536        assert_eq!("0xdEADBEeF00000000000000000000000000000000", fmt_1("%s", &addr));
537        assert_eq!("NaN", fmt_1("%d", &addr));
538        assert_eq!("NaN", fmt_1("%i", &addr));
539        assert_eq!("NaN", fmt_1("%e", &addr));
540        assert_eq!("0xdEADBEeF00000000000000000000000000000000", fmt_1("%x", &addr));
541        assert_eq!("'0xdEADBEeF00000000000000000000000000000000'", fmt_1("%o", &addr));
542
543        let bytes = bytes!("0xdeadbeef");
544        assert_eq!("0xdeadbeef", fmt_1("%s", &bytes));
545        assert_eq!("NaN", fmt_1("%d", &bytes));
546        assert_eq!("NaN", fmt_1("%i", &bytes));
547        assert_eq!("NaN", fmt_1("%e", &bytes));
548        assert_eq!("0xdeadbeef", fmt_1("%x", &bytes));
549        assert_eq!("'0xdeadbeef'", fmt_1("%o", &bytes));
550
551        assert_eq!("100", fmt_1("%s", &U256::from(100)));
552        assert_eq!("100", fmt_1("%d", &U256::from(100)));
553        assert_eq!("100", fmt_1("%i", &U256::from(100)));
554        assert_eq!("1e2", fmt_1("%e", &U256::from(100)));
555        assert_eq!("1.0023e6", fmt_1("%e", &U256::from(1002300)));
556        assert_eq!("1.23e5", fmt_1("%e", &U256::from(123000)));
557        assert_eq!("0x64", fmt_1("%x", &U256::from(100)));
558        assert_eq!("0x0", fmt_1("%x", &U256::ZERO));
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!("0x0", fmt_1("%x", &I256::ZERO));
581        assert_eq!(
582            "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9c",
583            fmt_1("%x", &I256::try_from(-100).unwrap())
584        );
585        assert_eq!(
586            "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffe8b7891800",
587            fmt_1("%x", &I256::try_from(-100000000000i64).unwrap())
588        );
589        assert_eq!("100", fmt_1("%o", &I256::try_from(100).unwrap()));
590
591        // make sure that %byte values are not consumed when there are no values
592        assert_eq!("%333d%3e%5F", console_format("%333d%3e%5F", &[]));
593        assert_eq!(
594            "%5d123456.789%2f%3f%e1",
595            console_format("%5d%3e%2f%3f%e1", &[&U256::from(123456789)])
596        );
597    }
598
599    // Overflow used to panic or silently produce incorrect digits.
600    #[test]
601    fn test_console_log_exponential_precision_overflow() {
602        let fmt_1 = |spec: &str, arg: &dyn ConsoleFmt| console_format(spec, &[arg]);
603
604        // 10^256 wraps to zero with unchecked exponentiation.
605        assert_eq!(format!("0.{}1", "0".repeat(255)), fmt_1("%256e", &U256::ONE));
606
607        // 10^78 overflows U256; 10^77 still fits.
608        let ten_pow_77 = U256::from(10).pow(U256::from(77u64));
609        assert_eq!("0.1", fmt_1("%78e", &ten_pow_77));
610
611        assert_eq!("1", fmt_1("%77e", &ten_pow_77));
612
613        // 10^77 exceeds I256::MAX.
614        assert_eq!(format!("0.{}1", "0".repeat(76)), fmt_1("%77e", &I256::try_from(1).unwrap()));
615        assert_eq!(format!("-0.{}1", "0".repeat(76)), fmt_1("%77e", &I256::try_from(-1).unwrap()));
616
617        // Preserve the value at the maximum accepted precision.
618        assert_eq!(format!("0.{}1", "0".repeat(1023)), fmt_1("%1024e", &U256::ONE));
619
620        // Invalid precisions remain literal and do not consume the value.
621        assert_eq!("%1025e 1", fmt_1("%1025e", &U256::ONE));
622        assert_eq!("%99999999999999999999e 1", fmt_1("%99999999999999999999e", &U256::ONE));
623
624        assert_eq!("1", fmt_1("%18e", &U256::from(1_000_000_000_000_000_000u64)));
625
626        assert_eq!("0", fmt_1("%0e", &U256::ZERO));
627        assert_eq!("0", fmt_1("%256e", &U256::ZERO));
628
629        // Check signed and unsigned extrema at their overflow boundaries.
630        let expect_fallback = |digits: String, precision: usize, sign: &str| {
631            let padded = format!("{digits:0>precision$}");
632            let trimmed = padded.trim_end_matches('0');
633            if trimmed.is_empty() { format!("{sign}0") } else { format!("{sign}0.{trimmed}") }
634        };
635        assert_eq!(expect_fallback(U256::MAX.to_string(), 78, ""), fmt_1("%78e", &U256::MAX));
636        assert_eq!(
637            expect_fallback(I256::MIN.unsigned_abs().to_string(), 77, "-"),
638            fmt_1("%77e", &I256::MIN)
639        );
640        assert_eq!(
641            expect_fallback(I256::MAX.unsigned_abs().to_string(), 77, ""),
642            fmt_1("%77e", &I256::MAX)
643        );
644    }
645
646    #[test]
647    fn test_console_log_format() {
648        let mut log1 = Log1 { p_0: "foo %s".to_string(), p_1: U256::from(100) };
649        assert_eq!("foo 100", logf1!(log1));
650        log1.p_0 = String::from("foo");
651        assert_eq!("foo 100", logf1!(log1));
652        log1.p_0 = String::from("%s foo");
653        assert_eq!("100 foo", logf1!(log1));
654
655        let mut log2 = Log2 { p_0: "foo %s %s".to_string(), p_1: true, p_2: U256::from(100) };
656        assert_eq!("foo true 100", logf2!(log2));
657        log2.p_0 = String::from("foo");
658        assert_eq!("foo true 100", logf2!(log2));
659        log2.p_0 = String::from("%s %s foo");
660        assert_eq!("true 100 foo", logf2!(log2));
661
662        let log3 = Log3 {
663            p_0: String::from("foo %s %%s %s and %d foo %%"),
664            p_1: address!("0xdEADBEeF00000000000000000000000000000000"),
665            p_2: true,
666            p_3: U256::from(21),
667        };
668        assert_eq!(
669            "foo 0xdEADBEeF00000000000000000000000000000000 %s true and 21 foo %",
670            logf3!(log3)
671        );
672
673        // %ne
674        let log4 = Log1 { p_0: String::from("%5e"), p_1: U256::from(123456789) };
675        assert_eq!("1234.56789", logf1!(log4));
676
677        let log5 = Log1 { p_0: String::from("foo %3e bar"), p_1: U256::from(123456789) };
678        assert_eq!("foo 123456.789 bar", logf1!(log5));
679
680        let log6 =
681            Log2 { p_0: String::from("%e and %12e"), p_1: false, p_2: U256::from(123456789) };
682        assert_eq!("NaN and 0.000123456789", logf2!(log6));
683    }
684
685    #[test]
686    fn test_derive_format() {
687        let log1 = Log1 { p_0: String::from("foo %s bar"), p_1: U256::from(42) };
688        assert_eq!(log1.fmt(Default::default()), "foo 42 bar");
689        let call = Logs::Log1(log1);
690        assert_eq!(call.fmt(Default::default()), "foo 42 bar");
691    }
692
693    #[test]
694    fn test_console_table_format() {
695        // auto-indexed, uint256 values
696        let values: &[&dyn ConsoleFmt] = &[&U256::from(100), &U256::from(200), &U256::from(300)];
697        assert_eq!(
698            console_table_format(None, values),
699            "┌─────────┬────────┐\n\
700             │ (index) │ Values │\n\
701             ├─────────┼────────┤\n\
702             │ 0       │ 100    │\n\
703             │ 1       │ 200    │\n\
704             │ 2       │ 300    │\n\
705             └─────────┴────────┘"
706        );
707
708        // string keys, uint256 values
709        // key col expands to fit "charlie123" and value col expands to fit "20000000000000000"
710        let keys: &[&dyn ConsoleFmt] =
711            &[&String::from("alice"), &String::from("bob"), &String::from("charlie123")];
712        let values: &[&dyn ConsoleFmt] = &[
713            &U256::ONE,
714            &U256::from_str("20000000000000000").unwrap(),
715            &U256::from_str("30000000000").unwrap(),
716        ];
717        assert_eq!(
718            console_table_format(Some(keys), values),
719            "┌────────────┬───────────────────┐\n\
720             │ (index)    │ Values            │\n\
721             ├────────────┼───────────────────┤\n\
722             │ alice      │ 1                 │\n\
723             │ bob        │ 20000000000000000 │\n\
724             │ charlie123 │ 30000000000       │\n\
725             └────────────┴───────────────────┘"
726        );
727
728        // empty table
729        assert_eq!(
730            console_table_format(None, &[]),
731            "┌─────────┬────────┐\n\
732             │ (index) │ Values │\n\
733             ├─────────┼────────┤\n\
734             └─────────┴────────┘"
735        );
736
737        // more keys than values
738        let keys: &[&dyn ConsoleFmt] =
739            &[&String::from("alice"), &String::from("bob"), &String::from("charlie")];
740        let values: &[&dyn ConsoleFmt] = &[&U256::ONE, &U256::from(2)];
741        assert_eq!(
742            console_table_format(Some(keys), values),
743            "┌─────────┬────────┐\n\
744             │ (index) │ Values │\n\
745             ├─────────┼────────┤\n\
746             │ alice   │ 1      │\n\
747             │ bob     │ 2      │\n\
748             │ charlie │        │\n\
749             └─────────┴────────┘"
750        );
751
752        // more values than keys
753        let keys: &[&dyn ConsoleFmt] = &[&String::from("alice"), &String::from("bob")];
754        let values: &[&dyn ConsoleFmt] =
755            &[&U256::ONE, &U256::from(2), &U256::from(3), &U256::from(4)];
756        assert_eq!(
757            console_table_format(Some(keys), values),
758            "┌─────────┬────────┐\n\
759             │ (index) │ Values │\n\
760             ├─────────┼────────┤\n\
761             │ alice   │ 1      │\n\
762             │ bob     │ 2      │\n\
763             │         │ 3      │\n\
764             │         │ 4      │\n\
765             └─────────┴────────┘"
766        );
767    }
768}