foundry_debugger/tui/
draw.rs

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