nextest_runner/
helpers.rs

1// Copyright (c) The nextest Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! General support code for nextest-runner.
5
6use crate::{
7    config::scripts::ScriptId,
8    list::{OwnedTestInstanceId, Styles, TestInstanceId},
9    reporter::events::{AbortStatus, StressIndex},
10    write_str::WriteStr,
11};
12use camino::{Utf8Path, Utf8PathBuf};
13use console::AnsiCodeIterator;
14use owo_colors::{OwoColorize, Style};
15use std::{fmt, io, path::PathBuf, process::ExitStatus, time::Duration};
16use swrite::{SWrite, swrite};
17use unicode_width::UnicodeWidthChar;
18
19/// Utilities for pluralizing various words based on count or plurality.
20pub mod plural {
21    /// Returns "were" if `plural` is true, otherwise "was".
22    pub fn were_plural_if(plural: bool) -> &'static str {
23        if plural { "were" } else { "was" }
24    }
25
26    /// Returns "setup script" if `count` is 1, otherwise "setup scripts".
27    pub fn setup_scripts_str(count: usize) -> &'static str {
28        if count == 1 {
29            "setup script"
30        } else {
31            "setup scripts"
32        }
33    }
34
35    /// Returns "test" if `count` is 1, otherwise "tests".
36    pub fn tests_str(count: usize) -> &'static str {
37        tests_plural_if(count != 1)
38    }
39
40    /// Returns "tests" if `plural` is true, otherwise "test".
41    pub fn tests_plural_if(plural: bool) -> &'static str {
42        if plural { "tests" } else { "test" }
43    }
44
45    /// Returns "binary" if `count` is 1, otherwise "binaries".
46    pub fn binaries_str(count: usize) -> &'static str {
47        if count == 1 { "binary" } else { "binaries" }
48    }
49
50    /// Returns "path" if `count` is 1, otherwise "paths".
51    pub fn paths_str(count: usize) -> &'static str {
52        if count == 1 { "path" } else { "paths" }
53    }
54
55    /// Returns "file" if `count` is 1, otherwise "files".
56    pub fn files_str(count: usize) -> &'static str {
57        if count == 1 { "file" } else { "files" }
58    }
59
60    /// Returns "directory" if `count` is 1, otherwise "directories".
61    pub fn directories_str(count: usize) -> &'static str {
62        if count == 1 {
63            "directory"
64        } else {
65            "directories"
66        }
67    }
68
69    /// Returns "this crate" if `count` is 1, otherwise "these crates".
70    pub fn this_crate_str(count: usize) -> &'static str {
71        if count == 1 {
72            "this crate"
73        } else {
74            "these crates"
75        }
76    }
77
78    /// Returns "library" if `count` is 1, otherwise "libraries".
79    pub fn libraries_str(count: usize) -> &'static str {
80        if count == 1 { "library" } else { "libraries" }
81    }
82
83    /// Returns "filter" if `count` is 1, otherwise "filters".
84    pub fn filters_str(count: usize) -> &'static str {
85        if count == 1 { "filter" } else { "filters" }
86    }
87
88    /// Returns "section" if `count` is 1, otherwise "sections".
89    pub fn sections_str(count: usize) -> &'static str {
90        if count == 1 { "section" } else { "sections" }
91    }
92
93    /// Returns "iteration" if `count` is 1, otherwise "iterations".
94    pub fn iterations_str(count: u32) -> &'static str {
95        if count == 1 {
96            "iteration"
97        } else {
98            "iterations"
99        }
100    }
101}
102
103/// A helper for displaying test instances with formatting.
104pub struct DisplayTestInstance<'a> {
105    stress_index: Option<StressIndex>,
106    display_counter_index: Option<DisplayCounterIndex>,
107    instance: TestInstanceId<'a>,
108    styles: &'a Styles,
109    max_width: Option<usize>,
110}
111
112impl<'a> DisplayTestInstance<'a> {
113    /// Creates a new display formatter for a test instance.
114    pub fn new(
115        stress_index: Option<StressIndex>,
116        display_counter_index: Option<DisplayCounterIndex>,
117        instance: TestInstanceId<'a>,
118        styles: &'a Styles,
119    ) -> Self {
120        Self {
121            stress_index,
122            display_counter_index,
123            instance,
124            styles,
125            max_width: None,
126        }
127    }
128
129    pub(crate) fn with_max_width(mut self, max_width: usize) -> Self {
130        self.max_width = Some(max_width);
131        self
132    }
133}
134
135impl fmt::Display for DisplayTestInstance<'_> {
136    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
137        // Figure out the widths for each component.
138        let stress_index_str = if let Some(stress_index) = self.stress_index {
139            format!(
140                "[{}] ",
141                DisplayStressIndex {
142                    stress_index,
143                    count_style: self.styles.count,
144                }
145            )
146        } else {
147            String::new()
148        };
149        let counter_index_str = if let Some(display_counter_index) = &self.display_counter_index {
150            format!("{display_counter_index} ")
151        } else {
152            String::new()
153        };
154        let binary_id_str = format!("{} ", self.instance.binary_id.style(self.styles.binary_id));
155        let test_name_str = format!(
156            "{}",
157            DisplayTestName::new(self.instance.test_name, self.styles)
158        );
159
160        // If a max width is defined, trim strings until they fit into it.
161        if let Some(max_width) = self.max_width {
162            // We have to be careful while computing string width -- the strings
163            // above include ANSI escape codes which have a display width of
164            // zero.
165            let stress_index_width = text_width(&stress_index_str);
166            let counter_index_width = text_width(&counter_index_str);
167            let binary_id_width = text_width(&binary_id_str);
168            let test_name_width = text_width(&test_name_str);
169
170            // Truncate components in order, from most important to keep to least:
171            //
172            // * stress-index (left-aligned)
173            // * counter index (left-aligned)
174            // * binary ID (left-aligned)
175            // * test name (right-aligned)
176            let mut stress_index_resolved_width = stress_index_width;
177            let mut counter_index_resolved_width = counter_index_width;
178            let mut binary_id_resolved_width = binary_id_width;
179            let mut test_name_resolved_width = test_name_width;
180
181            // Truncate stress-index first.
182            if stress_index_resolved_width > max_width {
183                stress_index_resolved_width = max_width;
184            }
185
186            // Truncate counter index next.
187            let remaining_width = max_width.saturating_sub(stress_index_resolved_width);
188            if counter_index_resolved_width > remaining_width {
189                counter_index_resolved_width = remaining_width;
190            }
191
192            // Truncate binary ID next.
193            let remaining_width = max_width
194                .saturating_sub(stress_index_resolved_width)
195                .saturating_sub(counter_index_resolved_width);
196            if binary_id_resolved_width > remaining_width {
197                binary_id_resolved_width = remaining_width;
198            }
199
200            // Truncate test name last.
201            let remaining_width = max_width
202                .saturating_sub(stress_index_resolved_width)
203                .saturating_sub(counter_index_resolved_width)
204                .saturating_sub(binary_id_resolved_width);
205            if test_name_resolved_width > remaining_width {
206                test_name_resolved_width = remaining_width;
207            }
208
209            // Now truncate the strings if applicable.
210            let test_name_truncated_str = if test_name_resolved_width == test_name_width {
211                test_name_str
212            } else {
213                // Right-align the test name.
214                truncate_ansi_aware(
215                    &test_name_str,
216                    test_name_width.saturating_sub(test_name_resolved_width),
217                    test_name_width,
218                )
219            };
220            let binary_id_truncated_str = if binary_id_resolved_width == binary_id_width {
221                binary_id_str
222            } else {
223                // Left-align the binary ID.
224                truncate_ansi_aware(&binary_id_str, 0, binary_id_resolved_width)
225            };
226            let counter_index_truncated_str = if counter_index_resolved_width == counter_index_width
227            {
228                counter_index_str
229            } else {
230                // Left-align the counter index.
231                truncate_ansi_aware(&counter_index_str, 0, counter_index_resolved_width)
232            };
233            let stress_index_truncated_str = if stress_index_resolved_width == stress_index_width {
234                stress_index_str
235            } else {
236                // Left-align the stress index.
237                truncate_ansi_aware(&stress_index_str, 0, stress_index_resolved_width)
238            };
239
240            write!(
241                f,
242                "{}{}{}{}",
243                stress_index_truncated_str,
244                counter_index_truncated_str,
245                binary_id_truncated_str,
246                test_name_truncated_str,
247            )
248        } else {
249            write!(
250                f,
251                "{}{}{}{}",
252                stress_index_str, counter_index_str, binary_id_str, test_name_str
253            )
254        }
255    }
256}
257
258fn text_width(text: &str) -> usize {
259    // Technically, the width of a string may not be the same as the sum of the
260    // widths of its characters. But managing truncation is pretty difficult. See
261    // https://docs.rs/unicode-width/latest/unicode_width/#rules-for-determining-width.
262    //
263    // This is quite difficult to manage truncation for, so we just use the sum
264    // of the widths of the string's characters (both here and in
265    // truncate_ansi_aware below).
266    strip_ansi_escapes::strip_str(text)
267        .chars()
268        .map(|c| c.width().unwrap_or(0))
269        .sum()
270}
271
272fn truncate_ansi_aware(text: &str, start: usize, end: usize) -> String {
273    let mut pos = 0;
274    let mut res = String::new();
275    for (s, is_ansi) in AnsiCodeIterator::new(text) {
276        if is_ansi {
277            res.push_str(s);
278            continue;
279        } else if pos >= end {
280            // We retain ANSI escape codes, so this is `continue` rather than
281            // `break`.
282            continue;
283        }
284
285        for c in s.chars() {
286            let c_width = c.width().unwrap_or(0);
287            if start <= pos && pos + c_width <= end {
288                res.push(c);
289            }
290            pos += c_width;
291            if pos > end {
292                // no need to iterate over the rest of s
293                break;
294            }
295        }
296    }
297
298    res
299}
300
301pub(crate) struct DisplayScriptInstance {
302    stress_index: Option<StressIndex>,
303    script_id: ScriptId,
304    full_command: String,
305    script_id_style: Style,
306    count_style: Style,
307}
308
309impl DisplayScriptInstance {
310    pub(crate) fn new(
311        stress_index: Option<StressIndex>,
312        script_id: ScriptId,
313        command: &str,
314        args: &[String],
315        script_id_style: Style,
316        count_style: Style,
317    ) -> Self {
318        let full_command =
319            shell_words::join(std::iter::once(command).chain(args.iter().map(|arg| arg.as_ref())));
320
321        Self {
322            stress_index,
323            script_id,
324            full_command,
325            script_id_style,
326            count_style,
327        }
328    }
329}
330
331impl fmt::Display for DisplayScriptInstance {
332    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
333        if let Some(stress_index) = self.stress_index {
334            write!(
335                f,
336                "[{}] ",
337                DisplayStressIndex {
338                    stress_index,
339                    count_style: self.count_style,
340                }
341            )?;
342        }
343        write!(
344            f,
345            "{}: {}",
346            self.script_id.style(self.script_id_style),
347            self.full_command,
348        )
349    }
350}
351
352struct DisplayStressIndex {
353    stress_index: StressIndex,
354    count_style: Style,
355}
356
357impl fmt::Display for DisplayStressIndex {
358    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
359        match self.stress_index.total {
360            Some(total) => {
361                write!(
362                    f,
363                    "{:>width$}/{}",
364                    (self.stress_index.current + 1).style(self.count_style),
365                    total.style(self.count_style),
366                    width = u32_decimal_char_width(total.get()),
367                )
368            }
369            None => {
370                write!(
371                    f,
372                    "{}",
373                    (self.stress_index.current + 1).style(self.count_style)
374                )
375            }
376        }
377    }
378}
379
380/// Counter index display for test instances.
381pub enum DisplayCounterIndex {
382    /// A counter with current and total counts.
383    Counter {
384        /// Current count.
385        current: usize,
386        /// Total count.
387        total: usize,
388    },
389    /// A padded display.
390    Padded {
391        /// Character to use for padding.
392        character: char,
393        /// Width to pad to.
394        width: usize,
395    },
396}
397
398impl DisplayCounterIndex {
399    /// Creates a new counter display.
400    pub fn new_counter(current: usize, total: usize) -> Self {
401        Self::Counter { current, total }
402    }
403
404    /// Creates a new padded display.
405    pub fn new_padded(character: char, width: usize) -> Self {
406        Self::Padded { character, width }
407    }
408}
409
410impl fmt::Display for DisplayCounterIndex {
411    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
412        match self {
413            Self::Counter { current, total } => {
414                write!(
415                    f,
416                    "({:>width$}/{})",
417                    current,
418                    total,
419                    width = usize_decimal_char_width(*total)
420                )
421            }
422            Self::Padded { character, width } => {
423                // Rendered as:
424                //
425                // (  20/5000)
426                // (---------)
427                let s: String = std::iter::repeat_n(*character, 2 * *width + 1).collect();
428                write!(f, "({s})")
429            }
430        }
431    }
432}
433
434pub(crate) fn usize_decimal_char_width(n: usize) -> usize {
435    // checked_ilog10 returns 0 for 1-9, 1 for 10-99, 2 for 100-999, etc. (And
436    // None for 0 which we unwrap to the same as 1). Add 1 to it to get the
437    // actual number of digits.
438    (n.checked_ilog10().unwrap_or(0) + 1).try_into().unwrap()
439}
440
441pub(crate) fn u32_decimal_char_width(n: u32) -> usize {
442    // checked_ilog10 returns 0 for 1-9, 1 for 10-99, 2 for 100-999, etc. (And
443    // None for 0 which we unwrap to the same as 1). Add 1 to it to get the
444    // actual number of digits.
445    (n.checked_ilog10().unwrap_or(0) + 1).try_into().unwrap()
446}
447
448/// Write out a test name.
449pub(crate) fn write_test_name(
450    name: &str,
451    style: &Styles,
452    writer: &mut dyn WriteStr,
453) -> io::Result<()> {
454    // Look for the part of the test after the last ::, if any.
455    let mut splits = name.rsplitn(2, "::");
456    let trailing = splits.next().expect("test should have at least 1 element");
457    if let Some(rest) = splits.next() {
458        write!(
459            writer,
460            "{}{}",
461            rest.style(style.module_path),
462            "::".style(style.module_path)
463        )?;
464    }
465    write!(writer, "{}", trailing.style(style.test_name))?;
466
467    Ok(())
468}
469
470/// Wrapper for displaying a test name with styling.
471pub(crate) struct DisplayTestName<'a> {
472    name: &'a str,
473    styles: &'a Styles,
474}
475
476impl<'a> DisplayTestName<'a> {
477    pub(crate) fn new(name: &'a str, styles: &'a Styles) -> Self {
478        Self { name, styles }
479    }
480}
481
482impl fmt::Display for DisplayTestName<'_> {
483    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
484        // Look for the part of the test after the last ::, if any.
485        let mut splits = self.name.rsplitn(2, "::");
486        let trailing = splits.next().expect("test should have at least 1 element");
487        if let Some(rest) = splits.next() {
488            write!(
489                f,
490                "{}{}",
491                rest.style(self.styles.module_path),
492                "::".style(self.styles.module_path)
493            )?;
494        }
495        write!(f, "{}", trailing.style(self.styles.test_name))?;
496
497        Ok(())
498    }
499}
500
501pub(crate) fn convert_build_platform(
502    platform: nextest_metadata::BuildPlatform,
503) -> guppy::graph::cargo::BuildPlatform {
504    match platform {
505        nextest_metadata::BuildPlatform::Target => guppy::graph::cargo::BuildPlatform::Target,
506        nextest_metadata::BuildPlatform::Host => guppy::graph::cargo::BuildPlatform::Host,
507    }
508}
509
510// ---
511// Functions below copied from cargo-util to avoid pulling in a bunch of dependencies
512// ---
513
514/// Returns the name of the environment variable used for searching for
515/// dynamic libraries.
516pub(crate) fn dylib_path_envvar() -> &'static str {
517    if cfg!(windows) {
518        "PATH"
519    } else if cfg!(target_os = "macos") {
520        // When loading and linking a dynamic library or bundle, dlopen
521        // searches in LD_LIBRARY_PATH, DYLD_LIBRARY_PATH, PWD, and
522        // DYLD_FALLBACK_LIBRARY_PATH.
523        // In the Mach-O format, a dynamic library has an "install path."
524        // Clients linking against the library record this path, and the
525        // dynamic linker, dyld, uses it to locate the library.
526        // dyld searches DYLD_LIBRARY_PATH *before* the install path.
527        // dyld searches DYLD_FALLBACK_LIBRARY_PATH only if it cannot
528        // find the library in the install path.
529        // Setting DYLD_LIBRARY_PATH can easily have unintended
530        // consequences.
531        //
532        // Also, DYLD_LIBRARY_PATH appears to have significant performance
533        // penalty starting in 10.13. Cargo's testsuite ran more than twice as
534        // slow with it on CI.
535        "DYLD_FALLBACK_LIBRARY_PATH"
536    } else {
537        "LD_LIBRARY_PATH"
538    }
539}
540
541/// Returns a list of directories that are searched for dynamic libraries.
542///
543/// Note that some operating systems will have defaults if this is empty that
544/// will need to be dealt with.
545pub(crate) fn dylib_path() -> Vec<PathBuf> {
546    match std::env::var_os(dylib_path_envvar()) {
547        Some(var) => std::env::split_paths(&var).collect(),
548        None => Vec::new(),
549    }
550}
551
552/// On Windows, convert relative paths to always use forward slashes.
553#[cfg(windows)]
554pub(crate) fn convert_rel_path_to_forward_slash(rel_path: &Utf8Path) -> Utf8PathBuf {
555    if !rel_path.is_relative() {
556        panic!("path for conversion to forward slash '{rel_path}' is not relative");
557    }
558    rel_path.as_str().replace('\\', "/").into()
559}
560
561#[cfg(not(windows))]
562pub(crate) fn convert_rel_path_to_forward_slash(rel_path: &Utf8Path) -> Utf8PathBuf {
563    rel_path.to_path_buf()
564}
565
566/// On Windows, convert relative paths to use the main separator.
567#[cfg(windows)]
568pub(crate) fn convert_rel_path_to_main_sep(rel_path: &Utf8Path) -> Utf8PathBuf {
569    if !rel_path.is_relative() {
570        panic!("path for conversion to backslash '{rel_path}' is not relative");
571    }
572    rel_path.as_str().replace('/', "\\").into()
573}
574
575#[cfg(not(windows))]
576pub(crate) fn convert_rel_path_to_main_sep(rel_path: &Utf8Path) -> Utf8PathBuf {
577    rel_path.to_path_buf()
578}
579
580/// Join relative paths using forward slashes.
581pub(crate) fn rel_path_join(rel_path: &Utf8Path, path: &Utf8Path) -> Utf8PathBuf {
582    assert!(rel_path.is_relative(), "rel_path {rel_path} is relative");
583    assert!(path.is_relative(), "path {path} is relative",);
584    format!("{rel_path}/{path}").into()
585}
586
587#[derive(Debug)]
588pub(crate) struct FormattedDuration(pub(crate) Duration);
589
590impl fmt::Display for FormattedDuration {
591    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
592        let duration = self.0.as_secs_f64();
593        if duration > 60.0 {
594            write!(f, "{}m {:.2}s", duration as u32 / 60, duration % 60.0)
595        } else {
596            write!(f, "{duration:.2}s")
597        }
598    }
599}
600
601// "exited with"/"terminated via"
602pub(crate) fn display_exited_with(exit_status: ExitStatus) -> String {
603    match AbortStatus::extract(exit_status) {
604        Some(abort_status) => display_abort_status(abort_status),
605        None => match exit_status.code() {
606            Some(code) => format!("exited with exit code {code}"),
607            None => "exited with an unknown error".to_owned(),
608        },
609    }
610}
611
612/// Displays the abort status.
613pub(crate) fn display_abort_status(abort_status: AbortStatus) -> String {
614    match abort_status {
615        #[cfg(unix)]
616        AbortStatus::UnixSignal(sig) => match crate::helpers::signal_str(sig) {
617            Some(s) => {
618                format!("aborted with signal {sig} (SIG{s})")
619            }
620            None => {
621                format!("aborted with signal {sig}")
622            }
623        },
624        #[cfg(windows)]
625        AbortStatus::WindowsNtStatus(nt_status) => {
626            format!(
627                "aborted with code {}",
628                // TODO: pass down a style here
629                crate::helpers::display_nt_status(nt_status, Style::new())
630            )
631        }
632        #[cfg(windows)]
633        AbortStatus::JobObject => "terminated via job object".to_string(),
634    }
635}
636
637#[cfg(unix)]
638pub(crate) fn signal_str(signal: i32) -> Option<&'static str> {
639    // These signal numbers are the same on at least Linux, macOS, FreeBSD and illumos.
640    //
641    // TODO: glibc has sigabbrev_np, and POSIX-1.2024 adds sig2str which has been available on
642    // illumos for many years:
643    // https://pubs.opengroup.org/onlinepubs/9799919799/functions/sig2str.html. We should use these
644    // if available.
645    match signal {
646        1 => Some("HUP"),
647        2 => Some("INT"),
648        3 => Some("QUIT"),
649        4 => Some("ILL"),
650        5 => Some("TRAP"),
651        6 => Some("ABRT"),
652        8 => Some("FPE"),
653        9 => Some("KILL"),
654        11 => Some("SEGV"),
655        13 => Some("PIPE"),
656        14 => Some("ALRM"),
657        15 => Some("TERM"),
658        _ => None,
659    }
660}
661
662#[cfg(windows)]
663pub(crate) fn display_nt_status(
664    nt_status: windows_sys::Win32::Foundation::NTSTATUS,
665    bold_style: Style,
666) -> String {
667    // 10 characters ("0x" + 8 hex digits) is how an NTSTATUS with the high bit
668    // set is going to be displayed anyway. This makes all possible displays
669    // uniform.
670    let bolded_status = format!("{:#010x}", nt_status.style(bold_style));
671    // Convert the NTSTATUS to a Win32 error code.
672    let win32_code = unsafe { windows_sys::Win32::Foundation::RtlNtStatusToDosError(nt_status) };
673
674    if win32_code == windows_sys::Win32::Foundation::ERROR_MR_MID_NOT_FOUND {
675        // The Win32 code was not found.
676        return bolded_status;
677    }
678
679    format!(
680        "{bolded_status}: {}",
681        io::Error::from_raw_os_error(win32_code as i32)
682    )
683}
684
685#[derive(Copy, Clone, Debug)]
686pub(crate) struct QuotedDisplay<'a, T: ?Sized>(pub(crate) &'a T);
687
688impl<T: ?Sized> fmt::Display for QuotedDisplay<'_, T>
689where
690    T: fmt::Display,
691{
692    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
693        write!(f, "'{}'", self.0)
694    }
695}
696
697// From https://twitter.com/8051Enthusiast/status/1571909110009921538
698unsafe extern "C" {
699    fn __nextest_external_symbol_that_does_not_exist();
700}
701
702/// Formats an interceptor (debugger or tracer) error message for too many tests.
703pub fn format_interceptor_too_many_tests(
704    cli_opt_name: &str,
705    test_count: usize,
706    test_instances: &[OwnedTestInstanceId],
707    list_styles: &Styles,
708    count_style: Style,
709) -> String {
710    let mut msg = format!(
711        "--{} requires exactly one test, but {} {} were selected:",
712        cli_opt_name,
713        test_count.style(count_style),
714        plural::tests_str(test_count)
715    );
716
717    for test_instance in test_instances {
718        let display = DisplayTestInstance::new(None, None, test_instance.as_ref(), list_styles);
719        swrite!(msg, "\n  {}", display);
720    }
721
722    if test_count > test_instances.len() {
723        let remaining = test_count - test_instances.len();
724        swrite!(
725            msg,
726            "\n  ... and {} more {}",
727            remaining.style(count_style),
728            plural::tests_str(remaining)
729        );
730    }
731
732    msg
733}
734
735#[inline]
736#[expect(dead_code)]
737pub(crate) fn statically_unreachable() -> ! {
738    unsafe {
739        __nextest_external_symbol_that_does_not_exist();
740    }
741    unreachable!("linker symbol above cannot be resolved")
742}
743
744#[cfg(test)]
745mod test {
746    use super::*;
747
748    #[test]
749    fn test_decimal_char_width() {
750        assert_eq!(1, usize_decimal_char_width(0));
751        assert_eq!(1, usize_decimal_char_width(1));
752        assert_eq!(1, usize_decimal_char_width(5));
753        assert_eq!(1, usize_decimal_char_width(9));
754        assert_eq!(2, usize_decimal_char_width(10));
755        assert_eq!(2, usize_decimal_char_width(11));
756        assert_eq!(2, usize_decimal_char_width(99));
757        assert_eq!(3, usize_decimal_char_width(100));
758        assert_eq!(3, usize_decimal_char_width(999));
759    }
760}