Skip to main content

foundry_debugger/tui/
draw.rs

1//! TUI draw implementation.
2
3use super::context::{StatusKind, TUIContext};
4use crate::{debugger::DebuggerStats, op::OpcodeParam};
5use alloy_primitives::Address;
6use foundry_compilers::artifacts::sourcemap::SourceElement;
7use foundry_evm_core::buffer::{BufferKind, get_buffer_accesses};
8use foundry_evm_traces::debug::SourceData;
9use ratatui::{
10    Frame,
11    layout::{Alignment, Constraint, Direction, Layout, Rect},
12    style::{Color, Modifier, Style},
13    text::{Line, Span, Text},
14    widgets::{Block, Borders, List, ListItem, ListState, Paragraph, Wrap},
15};
16use revm_inspectors::tracing::types::CallKind;
17use std::{collections::VecDeque, fmt::Write};
18
19impl TUIContext<'_> {
20    pub(crate) fn draw_layout(&self, f: &mut Frame<'_>) {
21        // We need 100 columns to display a 32 byte word in the memory and stack panes.
22        let area = f.area();
23        let min_width = 100;
24        let min_height = 16;
25        if area.width < min_width || area.height < min_height {
26            self.size_too_small(f, min_width, min_height);
27            return;
28        }
29
30        // The horizontal layout draws these panes at 50% width.
31        let min_column_width_for_horizontal = 200;
32        if area.width >= min_column_width_for_horizontal {
33            self.horizontal_layout(f);
34        } else {
35            self.vertical_layout(f);
36        }
37    }
38
39    fn size_too_small(&self, f: &mut Frame<'_>, min_width: u16, min_height: u16) {
40        let mut lines = Vec::with_capacity(4);
41
42        let l1 = "Terminal size too small:";
43        lines.push(Line::from(l1));
44
45        let area = f.area();
46        let width_color = if area.width >= min_width { Color::Green } else { Color::Red };
47        let height_color = if area.height >= min_height { Color::Green } else { Color::Red };
48        let l2 = vec![
49            Span::raw("Width = "),
50            Span::styled(area.width.to_string(), Style::new().fg(width_color)),
51            Span::raw(" Height = "),
52            Span::styled(area.height.to_string(), Style::new().fg(height_color)),
53        ];
54        lines.push(Line::from(l2));
55
56        let l3 = "Needed for current config:";
57        lines.push(Line::from(l3));
58        let l4 = format!("Width = {min_width} Height = {min_height}");
59        lines.push(Line::from(l4));
60
61        let paragraph =
62            Paragraph::new(lines).alignment(Alignment::Center).wrap(Wrap { trim: true });
63        f.render_widget(paragraph, area)
64    }
65
66    /// Draws the layout in vertical mode.
67    ///
68    /// ```text
69    /// |-----------------------------|
70    /// |             op              |
71    /// |-----------------------------|
72    /// |            stack            |
73    /// |-----------------------------|
74    /// |             buf             |
75    /// |-----------------------------|
76    /// |                             |
77    /// |             src             |
78    /// |                             |
79    /// |-----------------------------|
80    /// ```
81    fn vertical_layout(&self, f: &mut Frame<'_>) {
82        let area = f.area();
83        let footer_height = self.footer_height();
84
85        // NOTE: `Layout::split` always returns a slice of the same length as the number of
86        // constraints, so the `else` branch is unreachable.
87
88        // Split off footer.
89        let [app, footer] = Layout::new(
90            Direction::Vertical,
91            [Constraint::Min(0), Constraint::Length(footer_height)],
92        )
93        .split(area)[..] else {
94            unreachable!()
95        };
96
97        // Split the app in 4 vertically to construct all the panes.
98        let [op_pane, stack_pane, memory_pane, src_pane] = Layout::new(
99            Direction::Vertical,
100            [
101                Constraint::Ratio(1, 6),
102                Constraint::Ratio(1, 6),
103                Constraint::Ratio(1, 6),
104                Constraint::Ratio(3, 6),
105            ],
106        )
107        .split(app)[..] else {
108            unreachable!()
109        };
110
111        if footer_height > 0 {
112            self.draw_footer(f, footer);
113        }
114        self.draw_src(f, src_pane);
115        self.draw_op_list(f, op_pane);
116        self.draw_stack(f, stack_pane);
117        self.draw_buffer(f, memory_pane);
118    }
119
120    /// Draws the layout in horizontal mode.
121    ///
122    /// ```text
123    /// |-----------------|-----------|
124    /// |        op       |   stack   |
125    /// |-----------------|-----------|
126    /// |                 |           |
127    /// |       src       |    buf    |
128    /// |                 |           |
129    /// |-----------------|-----------|
130    /// ```
131    fn horizontal_layout(&self, f: &mut Frame<'_>) {
132        let area = f.area();
133        let footer_height = self.footer_height();
134
135        // Split off footer.
136        let [app, footer] = Layout::new(
137            Direction::Vertical,
138            [Constraint::Min(0), Constraint::Length(footer_height)],
139        )
140        .split(area)[..] else {
141            unreachable!()
142        };
143
144        // Split app in 2 horizontally.
145        let [app_left, app_right] =
146            Layout::new(Direction::Horizontal, [Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)])
147                .split(app)[..]
148        else {
149            unreachable!()
150        };
151
152        // Split left pane in 2 vertically to opcode list and source.
153        let [op_pane, src_pane] =
154            Layout::new(Direction::Vertical, [Constraint::Ratio(1, 4), Constraint::Ratio(3, 4)])
155                .split(app_left)[..]
156        else {
157            unreachable!()
158        };
159
160        // Split right pane horizontally to construct stack and memory.
161        let [stack_pane, memory_pane] =
162            Layout::new(Direction::Vertical, [Constraint::Ratio(1, 4), Constraint::Ratio(3, 4)])
163                .split(app_right)[..]
164        else {
165            unreachable!()
166        };
167
168        if footer_height > 0 {
169            self.draw_footer(f, footer);
170        }
171        self.draw_src(f, src_pane);
172        self.draw_op_list(f, op_pane);
173        self.draw_stack(f, stack_pane);
174        self.draw_buffer(f, memory_pane);
175    }
176
177    fn footer_height(&self) -> u16 {
178        let status_or_input = u16::from(self.pc_input.is_some() || self.status.is_some());
179        let shortcuts = if self.show_shortcuts { 2 } else { 0 };
180        status_or_input + shortcuts
181    }
182
183    fn draw_footer(&self, f: &mut Frame<'_>, area: Rect) {
184        let mut lines = Vec::with_capacity(self.footer_height() as usize);
185
186        if let Some(input) = &self.pc_input {
187            lines.push(Line::from(vec![
188                Span::styled(
189                    "Goto PC: ",
190                    Style::new().fg(Color::Cyan).add_modifier(Modifier::BOLD),
191                ),
192                Span::raw(input.as_str()),
193                Span::styled("█", Style::new().fg(Color::Cyan)),
194                Span::styled(
195                    "  Enter: jump | Esc: cancel | hex: 0x2a/2a | decimal: d:42",
196                    Style::new().add_modifier(Modifier::DIM),
197                ),
198            ]));
199        } else if let Some(status) = &self.status {
200            let style = match status.kind {
201                StatusKind::Info => Style::new().fg(Color::Green),
202                StatusKind::Error => Style::new().fg(Color::Red).add_modifier(Modifier::BOLD),
203            };
204            lines.push(Line::from(Span::styled(status.text.as_str(), style)));
205        }
206
207        let l1 = "[q]: quit | [k/j]: prev/next op | [a/s]: prev/next jump | [c/C]: prev/next call | [g/G]: start/end | [p]: goto PC | [b]: cycle memory/calldata/returndata buffers";
208        let l2 = "[t]: stack labels | [m]: buffer decoding | [shift + j/k]: scroll stack | [ctrl + j/k]: scroll buffer | ['<char>]: goto breakpoint | [h] toggle help";
209        let dimmed = Style::new().add_modifier(Modifier::DIM);
210        if self.show_shortcuts {
211            lines.push(Line::from(Span::styled(l1, dimmed)));
212            lines.push(Line::from(Span::styled(l2, dimmed)));
213        }
214
215        let paragraph =
216            Paragraph::new(lines).alignment(Alignment::Center).wrap(Wrap { trim: false });
217        f.render_widget(paragraph, area);
218    }
219
220    fn draw_src(&self, f: &mut Frame<'_>, area: Rect) {
221        let (text_output, source_name) = self.src_text(area);
222        let call_kind_text = match self.call_kind() {
223            CallKind::Create | CallKind::Create2 => "Contract creation",
224            CallKind::Call => "Contract call",
225            CallKind::StaticCall => "Contract staticcall",
226            CallKind::CallCode => "Contract callcode",
227            CallKind::DelegateCall => "Contract delegatecall",
228            CallKind::AuthCall => "Contract authcall",
229        };
230        let title = format!(
231            "{} {} ",
232            call_kind_text,
233            source_name.map(|s| format!("| {s}")).unwrap_or_default()
234        );
235        let block = Block::default().title(title).borders(Borders::ALL);
236        let paragraph = Paragraph::new(text_output).block(block).wrap(Wrap { trim: false });
237        f.render_widget(paragraph, area);
238    }
239
240    fn src_text(&self, area: Rect) -> (Text<'_>, Option<&str>) {
241        let (source_element, source) = match self.src_map() {
242            Ok(r) => r,
243            Err(e) => return (Text::from(e), None),
244        };
245
246        // We are handed a vector of SourceElements that give us a span of sourcecode that is
247        // currently being executed. This includes an offset and length.
248        // This vector is in instruction pointer order, meaning the location of the instruction
249        // minus `sum(push_bytes[..pc])`.
250        let offset = source_element.offset() as usize;
251        let len = source_element.length() as usize;
252        let max = source.source.len();
253
254        // Split source into before, relevant, and after chunks, split by line, for formatting.
255        let actual_start = offset.min(max);
256        let actual_end = (offset + len).min(max);
257
258        let mut before: Vec<_> = source.source[..actual_start].split_inclusive('\n').collect();
259        let actual: Vec<_> =
260            source.source[actual_start..actual_end].split_inclusive('\n').collect();
261        let mut after: VecDeque<_> = source.source[actual_end..].split_inclusive('\n').collect();
262
263        let num_lines = before.len() + actual.len() + after.len();
264        let height = area.height as usize;
265        let needed_highlight = actual.len();
266        let mid_len = before.len() + actual.len();
267
268        // adjust what text we show of the source code
269        let (start_line, end_line) = if needed_highlight > height {
270            // highlighted section is more lines than we have available
271            let start_line = before.len().saturating_sub(1);
272            (start_line, before.len() + needed_highlight)
273        } else if height > num_lines {
274            // we can fit entire source
275            (0, num_lines)
276        } else {
277            let remaining = height - needed_highlight;
278            let mut above = remaining / 2;
279            let mut below = remaining / 2;
280            if below > after.len() {
281                // unused space below the highlight
282                above += below - after.len();
283            } else if above > before.len() {
284                // we have unused space above the highlight
285                below += above - before.len();
286            } else {
287                // no unused space
288            }
289
290            // since above is subtracted from before.len(), and the resulting
291            // start_line is used to index into before, above must be at least
292            // 1 to avoid out-of-range accesses.
293            if above == 0 {
294                above = 1;
295            }
296            (before.len().saturating_sub(above), mid_len + below)
297        };
298
299        // Unhighlighted line number: gray.
300        let u_num = Style::new().fg(Color::Gray);
301        // Unhighlighted text: default, dimmed.
302        let u_text = Style::new().add_modifier(Modifier::DIM);
303        // Highlighted line number: cyan.
304        let h_num = Style::new().fg(Color::Cyan);
305        // Highlighted text: cyan, bold.
306        let h_text = Style::new().fg(Color::Cyan).add_modifier(Modifier::BOLD);
307
308        let mut lines = SourceLines::new(start_line, end_line);
309
310        // We check if there is other text on the same line before the highlight starts.
311        if let Some(last) = before.pop() {
312            let last_has_nl = last.ends_with('\n');
313
314            if last_has_nl {
315                before.push(last);
316            }
317            for line in &before[start_line..] {
318                lines.push(u_num, line, u_text);
319            }
320
321            let first = if last_has_nl {
322                0
323            } else {
324                lines.push_raw(h_num, &[Span::raw(last), Span::styled(actual[0], h_text)]);
325                1
326            };
327
328            // Skip the first line if it has already been handled above.
329            for line in &actual[first..] {
330                lines.push(h_num, line, h_text);
331            }
332        } else {
333            // No text before the current line.
334            for line in &actual {
335                lines.push(h_num, line, h_text);
336            }
337        }
338
339        // Fill in the rest of the line as unhighlighted.
340        if let Some(last) = actual.last()
341            && !last.ends_with('\n')
342            && let Some(post) = after.pop_front()
343            && let Some(last) = lines.lines.last_mut()
344        {
345            last.spans.push(Span::raw(post));
346        }
347
348        // Add after highlighted text.
349        while mid_len + after.len() > end_line {
350            after.pop_back();
351        }
352        for line in after {
353            lines.push(u_num, line, u_text);
354        }
355
356        // pad with empty to each line to ensure the previous text is cleared
357        for line in &mut lines.lines {
358            // note that the \n is not included in the line length
359            if area.width as usize > line.width() + 1 {
360                line.push_span(Span::raw(" ".repeat(area.width as usize - line.width() - 1)));
361            }
362        }
363
364        (Text::from(lines.lines), source.path.to_str())
365    }
366
367    /// Returns source map, source code and source name of the current line.
368    fn src_map(&self) -> Result<(SourceElement, &SourceData), String> {
369        let address = self.address();
370        let Some(contract_name) = self.debugger_context.identified_contracts.get(address) else {
371            return Err(format!("Unknown contract at address {address}"));
372        };
373
374        self.debugger_context
375            .contracts_sources
376            .find_source_mapping(
377                contract_name,
378                self.current_step().pc as u32,
379                self.debug_call().kind.is_any_create(),
380            )
381            .ok_or_else(|| format!("No source map for contract {contract_name}"))
382    }
383
384    fn draw_op_list(&self, f: &mut Frame<'_>, area: Rect) {
385        let debug_steps = self.debug_steps();
386        let max_pc = debug_steps.iter().map(|step| step.pc).max().unwrap_or(0);
387        let max_pc_len = hex_digits(max_pc);
388
389        let items = debug_steps
390            .iter()
391            .enumerate()
392            .map(|(i, step)| {
393                let mut content = String::with_capacity(64);
394                write!(content, "{:0>max_pc_len$x}|", step.pc).unwrap();
395                if let Some(op) = self.opcode_list.get(i) {
396                    content.push_str(op);
397                }
398                ListItem::new(Span::styled(content, Style::new().fg(Color::White)))
399            })
400            .collect::<Vec<_>>();
401
402        let step = self.current_step();
403        let call_gas_used = self.debug_call().gas_limit.saturating_sub(step.gas_remaining);
404        let title = op_list_title(
405            self.address(),
406            step.pc,
407            step.gas_remaining,
408            call_gas_used,
409            step.gas_refund_counter,
410            self.debugger_context.stats,
411        );
412        let block = Block::default().title(title).borders(Borders::ALL);
413        let list = List::new(items)
414            .block(block)
415            .highlight_symbol("▶")
416            .highlight_style(Style::new().fg(Color::White).bg(Color::DarkGray))
417            .scroll_padding(1);
418        let mut state = ListState::default().with_selected(Some(self.current_step));
419        f.render_stateful_widget(list, area, &mut state);
420    }
421
422    fn draw_stack(&self, f: &mut Frame<'_>, area: Rect) {
423        let step = self.current_step();
424        let stack = step.stack.as_ref();
425        let stack_len = stack.map_or(0, |s| s.len());
426
427        let min_len = decimal_digits(stack_len).max(2);
428
429        let params = OpcodeParam::of(step.op.get());
430
431        let text: Vec<Line<'_>> = stack
432            .map(|stack| {
433                stack
434                    .iter()
435                    .rev()
436                    .enumerate()
437                    .skip(self.draw_memory.current_stack_startline)
438                    .map(|(i, stack_item)| {
439                        let param = params.iter().find(|param| param.index == i);
440                        let mut spans = Vec::with_capacity(1 + 32 * 2 + 3);
441
442                        // Stack index.
443                        spans.push(Span::styled(
444                            format!("{i:0min_len$}| "),
445                            Style::new().fg(Color::White),
446                        ));
447
448                        // Item hex bytes.
449                        hex_bytes_spans(&stack_item.to_be_bytes::<32>(), &mut spans, |_, _| {
450                            if param.is_some() {
451                                Style::new().fg(Color::Cyan)
452                            } else {
453                                Style::new().fg(Color::White)
454                            }
455                        });
456
457                        if self.stack_labels
458                            && let Some(param) = param
459                        {
460                            spans.push(Span::raw("| "));
461                            spans.push(Span::raw(param.name));
462                        }
463
464                        spans.push(Span::raw("\n"));
465
466                        Line::from(spans)
467                    })
468                    .collect()
469            })
470            .unwrap_or_default();
471
472        let title = format!("Stack: {stack_len}");
473        let block = Block::default().title(title).borders(Borders::ALL);
474        let paragraph = Paragraph::new(text).block(block).wrap(Wrap { trim: true });
475        f.render_widget(paragraph, area);
476    }
477
478    fn draw_buffer(&self, f: &mut Frame<'_>, area: Rect) {
479        let call = self.debug_call();
480        let step = self.current_step();
481        let buf = match self.active_buffer {
482            BufferKind::Memory => step.memory.as_ref().unwrap().as_ref(),
483            BufferKind::Calldata => call.calldata.as_ref(),
484            BufferKind::Returndata => step.returndata.as_ref(),
485        };
486
487        let min_len = hex_digits(buf.len());
488
489        // Color memory region based on read/write.
490        let mut offset = None;
491        let mut len = None;
492        let mut write_offset = None;
493        let mut write_size = None;
494        let mut color = None;
495        let stack_len = step.stack.as_ref().map_or(0, |s| s.len());
496        if stack_len > 0
497            && let Some(stack) = step.stack.as_ref()
498            && let Some(accesses) = get_buffer_accesses(step.op.get(), stack)
499        {
500            if let Some(read_access) = accesses.read {
501                offset = Some(read_access.1.offset);
502                len = Some(read_access.1.len);
503                color = Some(Color::Cyan);
504            }
505            if let Some(write_access) = accesses.write
506                && self.active_buffer == BufferKind::Memory
507            {
508                write_offset = Some(write_access.offset);
509                write_size = Some(write_access.len);
510            }
511        }
512
513        // color word on previous write op
514        // TODO: technically it's possible for this to conflict with the current op, ie, with
515        // subsequent MCOPYs, but solc can't seem to generate that code even with high optimizer
516        // settings
517        if self.current_step > 0 {
518            let prev_step = self.current_step - 1;
519            let prev_step = &self.debug_steps()[prev_step];
520            if let Some(stack) = prev_step.stack.as_ref()
521                && let Some(write_access) =
522                    get_buffer_accesses(prev_step.op.get(), stack).and_then(|a| a.write)
523                && self.active_buffer == BufferKind::Memory
524            {
525                offset = Some(write_access.offset);
526                len = Some(write_access.len);
527                color = Some(Color::Green);
528            }
529        }
530
531        let height = area.height as usize;
532        let end_line = self.draw_memory.current_buf_startline + height;
533
534        let text: Vec<Line<'_>> = buf
535            .chunks(32)
536            .enumerate()
537            .skip(self.draw_memory.current_buf_startline)
538            .take_while(|(i, _)| *i < end_line)
539            .map(|(i, buf_word)| {
540                let mut spans = Vec::with_capacity(1 + 32 * 2 + 1 + 32 / 4 + 1);
541
542                // Buffer index.
543                spans.push(Span::styled(
544                    format!("{:0min_len$x}| ", i * 32),
545                    Style::new().fg(Color::White),
546                ));
547
548                // Word hex bytes.
549                hex_bytes_spans(buf_word, &mut spans, |j, _| {
550                    let mut byte_color = Color::White;
551                    let mut end = None;
552                    let idx = i * 32 + j;
553                    if let (Some(offset), Some(len), Some(color)) = (offset, len, color) {
554                        end = Some(offset + len);
555                        if (offset..offset + len).contains(&idx) {
556                            // [offset, offset + len] is the memory region to be colored.
557                            // If a byte at row i and column j in the memory panel
558                            // falls in this region, set the color.
559                            byte_color = color;
560                        }
561                    }
562                    if let (Some(write_offset), Some(write_size)) = (write_offset, write_size) {
563                        // check for overlap with read region
564                        let write_end = write_offset + write_size;
565                        if let Some(read_end) = end {
566                            let read_start = offset.unwrap();
567                            if (write_offset..write_end).contains(&read_end) {
568                                // if it contains end, start from write_start up to read_end
569                                if (write_offset..read_end).contains(&idx) {
570                                    return Style::new().fg(Color::Yellow);
571                                }
572                            } else if (write_offset..write_end).contains(&read_start) {
573                                // otherwise if it contains read start, start from read_start up to
574                                // write_end
575                                if (read_start..write_end).contains(&idx) {
576                                    return Style::new().fg(Color::Yellow);
577                                }
578                            }
579                        }
580                        if (write_offset..write_end).contains(&idx) {
581                            byte_color = Color::Red;
582                        }
583                    }
584
585                    Style::new().fg(byte_color)
586                });
587
588                if self.buf_utf {
589                    spans.push(Span::raw("|"));
590                    for utf in buf_word.chunks(4) {
591                        if let Ok(utf_str) = std::str::from_utf8(utf) {
592                            spans.push(Span::raw(utf_str.replace('\0', ".")));
593                        } else {
594                            spans.push(Span::raw("."));
595                        }
596                    }
597                }
598
599                spans.push(Span::raw("\n"));
600
601                Line::from(spans)
602            })
603            .collect();
604
605        let title = self.active_buffer.title(buf.len());
606        let block = Block::default().title(title).borders(Borders::ALL);
607        let paragraph = Paragraph::new(text).block(block).wrap(Wrap { trim: true });
608        f.render_widget(paragraph, area);
609    }
610}
611
612fn op_list_title(
613    address: &Address,
614    pc: usize,
615    gas_remaining: u64,
616    call_gas_used: u64,
617    gas_refund_counter: u64,
618    stats: Option<DebuggerStats>,
619) -> String {
620    let address = full_checksum_address(address);
621    let mut title = format!(
622        "address: {address} | pc: 0x{pc:x} ({pc}) | gasLeft: {gas_remaining} | \
623         callGasUsed: {call_gas_used} | gasRefund: {gas_refund_counter}"
624    );
625
626    if let Some(stats) = stats {
627        write!(
628            title,
629            " | sessionTraceGasUsed: {} | sessionSubcalls: {}",
630            stats.session_trace_gas_used, stats.session_subcalls
631        )
632        .unwrap();
633    }
634
635    title
636}
637
638fn full_checksum_address(address: &Address) -> String {
639    address.to_string()
640}
641
642/// Wrapper around a list of [`Line`]s that prepends the line number on each new line.
643struct SourceLines<'a> {
644    lines: Vec<Line<'a>>,
645    start_line: usize,
646    max_line_num: usize,
647}
648
649impl<'a> SourceLines<'a> {
650    fn new(start_line: usize, end_line: usize) -> Self {
651        Self { lines: Vec::new(), start_line, max_line_num: decimal_digits(end_line) }
652    }
653
654    fn push(&mut self, line_number_style: Style, line: &'a str, line_style: Style) {
655        self.push_raw(line_number_style, &[Span::styled(line, line_style)]);
656    }
657
658    fn push_raw(&mut self, line_number_style: Style, spans: &[Span<'a>]) {
659        let mut line_spans = Vec::with_capacity(4);
660
661        let line_number = format!(
662            "{number: >width$} ",
663            number = self.start_line + self.lines.len() + 1,
664            width = self.max_line_num
665        );
666        line_spans.push(Span::styled(line_number, line_number_style));
667
668        // Space between line number and line text.
669        line_spans.push(Span::raw("  "));
670
671        line_spans.extend_from_slice(spans);
672
673        self.lines.push(Line::from(line_spans));
674    }
675}
676
677fn hex_bytes_spans(bytes: &[u8], spans: &mut Vec<Span<'_>>, f: impl Fn(usize, u8) -> Style) {
678    for (i, &byte) in bytes.iter().enumerate() {
679        if i > 0 {
680            spans.push(Span::raw(" "));
681        }
682        spans.push(Span::styled(alloy_primitives::hex::encode([byte]), f(i, byte)));
683    }
684}
685
686/// Returns the number of decimal digits in the given number.
687///
688/// This is the same as `n.to_string().len()`.
689fn decimal_digits(n: usize) -> usize {
690    n.checked_ilog10().unwrap_or(0) as usize + 1
691}
692
693/// Returns the number of hexadecimal digits in the given number.
694///
695/// This is the same as `format!("{n:x}").len()`.
696fn hex_digits(n: usize) -> usize {
697    n.checked_ilog(16).unwrap_or(0) as usize + 1
698}
699
700#[cfg(test)]
701mod tests {
702    use crate::debugger::DebuggerStats;
703    use alloy_primitives::{Address, address};
704
705    #[test]
706    fn op_list_title_includes_gas_and_subcall_stats() {
707        let stats = DebuggerStats { session_trace_gas_used: 789_012, session_subcalls: 3 };
708        let address = Address::from([0x42; 20]);
709        let title = super::op_list_title(&address, 0x2a, 123_456, 42, 7, Some(stats));
710
711        assert!(title.contains("pc: 0x2a (42)"));
712        assert!(title.contains(&format!("address: {}", super::full_checksum_address(&address))));
713        assert!(title.contains("gasLeft: 123456"));
714        assert!(title.contains("sessionTraceGasUsed: 789012"));
715        assert!(title.contains("sessionSubcalls: 3"));
716        assert!(title.contains("callGasUsed: 42"));
717        assert!(title.contains("gasRefund: 7"));
718    }
719
720    #[test]
721    fn op_list_title_omits_aggregate_stats_when_unavailable() {
722        let title = super::op_list_title(&Address::from([0x42; 20]), 0x2a, 123_456, 42, 7, None);
723
724        assert!(!title.contains("sessionTraceGasUsed"));
725        assert!(!title.contains("sessionSubcalls"));
726    }
727
728    #[test]
729    fn op_list_title_uses_full_checksum_address() {
730        let address = address!("0xd8da6bf26964af9d7eed9e03e53415d37aa96045");
731        let title = super::op_list_title(&address, 0x2a, 123_456, 42, 7, None);
732
733        assert!(title.contains("address: 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"));
734        assert!(!title.contains('…'));
735    }
736
737    #[test]
738    fn decimal_digits() {
739        assert_eq!(super::decimal_digits(0), 1);
740        assert_eq!(super::decimal_digits(1), 1);
741        assert_eq!(super::decimal_digits(2), 1);
742        assert_eq!(super::decimal_digits(9), 1);
743        assert_eq!(super::decimal_digits(10), 2);
744        assert_eq!(super::decimal_digits(11), 2);
745        assert_eq!(super::decimal_digits(50), 2);
746        assert_eq!(super::decimal_digits(99), 2);
747        assert_eq!(super::decimal_digits(100), 3);
748        assert_eq!(super::decimal_digits(101), 3);
749        assert_eq!(super::decimal_digits(201), 3);
750        assert_eq!(super::decimal_digits(999), 3);
751        assert_eq!(super::decimal_digits(1000), 4);
752        assert_eq!(super::decimal_digits(1001), 4);
753    }
754
755    #[test]
756    fn hex_digits() {
757        assert_eq!(super::hex_digits(0), 1);
758        assert_eq!(super::hex_digits(1), 1);
759        assert_eq!(super::hex_digits(2), 1);
760        assert_eq!(super::hex_digits(9), 1);
761        assert_eq!(super::hex_digits(10), 1);
762        assert_eq!(super::hex_digits(11), 1);
763        assert_eq!(super::hex_digits(15), 1);
764        assert_eq!(super::hex_digits(16), 2);
765        assert_eq!(super::hex_digits(17), 2);
766        assert_eq!(super::hex_digits(0xff), 2);
767        assert_eq!(super::hex_digits(0x100), 3);
768        assert_eq!(super::hex_digits(0x101), 3);
769    }
770}