1use crate::{
7 config::scripts::ScriptId,
8 list::{OwnedTestInstanceId, Styles, TestInstanceId},
9 reporter::events::{AbortStatus, StressIndex},
10 run_mode::NextestRunMode,
11 write_str::WriteStr,
12};
13use camino::{Utf8Path, Utf8PathBuf};
14use console::AnsiCodeIterator;
15use nextest_metadata::TestCaseName;
16use owo_colors::{OwoColorize, Style};
17use std::{fmt, io, ops::ControlFlow, path::PathBuf, process::ExitStatus, time::Duration};
18use swrite::{SWrite, swrite};
19use unicode_width::UnicodeWidthChar;
20
21pub mod plural {
23 use crate::run_mode::NextestRunMode;
24
25 pub fn were_plural_if(plural: bool) -> &'static str {
27 if plural { "were" } else { "was" }
28 }
29
30 pub fn setup_scripts_str(count: usize) -> &'static str {
32 if count == 1 {
33 "setup script"
34 } else {
35 "setup scripts"
36 }
37 }
38
39 pub fn tests_str(mode: NextestRunMode, count: usize) -> &'static str {
44 tests_plural_if(mode, count != 1)
45 }
46
47 pub fn tests_plural_if(mode: NextestRunMode, plural: bool) -> &'static str {
52 match (mode, plural) {
53 (NextestRunMode::Test, true) => "tests",
54 (NextestRunMode::Test, false) => "test",
55 (NextestRunMode::Benchmark, true) => "benchmarks",
56 (NextestRunMode::Benchmark, false) => "benchmark",
57 }
58 }
59
60 pub fn tests_plural(mode: NextestRunMode) -> &'static str {
62 match mode {
63 NextestRunMode::Test => "tests",
64 NextestRunMode::Benchmark => "benchmarks",
65 }
66 }
67
68 pub fn binaries_str(count: usize) -> &'static str {
70 if count == 1 { "binary" } else { "binaries" }
71 }
72
73 pub fn paths_str(count: usize) -> &'static str {
75 if count == 1 { "path" } else { "paths" }
76 }
77
78 pub fn files_str(count: usize) -> &'static str {
80 if count == 1 { "file" } else { "files" }
81 }
82
83 pub fn directories_str(count: usize) -> &'static str {
85 if count == 1 {
86 "directory"
87 } else {
88 "directories"
89 }
90 }
91
92 pub fn this_crate_str(count: usize) -> &'static str {
94 if count == 1 {
95 "this crate"
96 } else {
97 "these crates"
98 }
99 }
100
101 pub fn libraries_str(count: usize) -> &'static str {
103 if count == 1 { "library" } else { "libraries" }
104 }
105
106 pub fn filters_str(count: usize) -> &'static str {
108 if count == 1 { "filter" } else { "filters" }
109 }
110
111 pub fn sections_str(count: usize) -> &'static str {
113 if count == 1 { "section" } else { "sections" }
114 }
115
116 pub fn iterations_str(count: u32) -> &'static str {
118 if count == 1 {
119 "iteration"
120 } else {
121 "iterations"
122 }
123 }
124
125 pub fn runs_str(count: usize) -> &'static str {
127 if count == 1 { "run" } else { "runs" }
128 }
129
130 pub fn orphans_str(count: usize) -> &'static str {
132 if count == 1 { "orphan" } else { "orphans" }
133 }
134
135 pub fn errors_str(count: usize) -> &'static str {
137 if count == 1 { "error" } else { "errors" }
138 }
139
140 pub fn exist_str(count: usize) -> &'static str {
142 if count == 1 { "exists" } else { "exist" }
143 }
144
145 pub fn remains_str(count: usize) -> &'static str {
147 if count == 1 { "remains" } else { "remain" }
148 }
149}
150
151pub struct DisplayTestInstance<'a> {
153 stress_index: Option<StressIndex>,
154 display_counter_index: Option<DisplayCounterIndex>,
155 instance: TestInstanceId<'a>,
156 styles: &'a Styles,
157 max_width: Option<usize>,
158}
159
160impl<'a> DisplayTestInstance<'a> {
161 pub fn new(
163 stress_index: Option<StressIndex>,
164 display_counter_index: Option<DisplayCounterIndex>,
165 instance: TestInstanceId<'a>,
166 styles: &'a Styles,
167 ) -> Self {
168 Self {
169 stress_index,
170 display_counter_index,
171 instance,
172 styles,
173 max_width: None,
174 }
175 }
176
177 pub(crate) fn with_max_width(mut self, max_width: usize) -> Self {
178 self.max_width = Some(max_width);
179 self
180 }
181}
182
183impl fmt::Display for DisplayTestInstance<'_> {
184 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
185 let stress_index_str = if let Some(stress_index) = self.stress_index {
187 format!(
188 "[{}] ",
189 DisplayStressIndex {
190 stress_index,
191 count_style: self.styles.count,
192 }
193 )
194 } else {
195 String::new()
196 };
197 let counter_index_str = if let Some(display_counter_index) = &self.display_counter_index {
198 format!("{display_counter_index} ")
199 } else {
200 String::new()
201 };
202 let binary_id_str = format!("{} ", self.instance.binary_id.style(self.styles.binary_id));
203 let test_name_str = format!(
204 "{}",
205 DisplayTestName::new(self.instance.test_name, self.styles)
206 );
207
208 if let Some(max_width) = self.max_width {
210 let stress_index_width = text_width(&stress_index_str);
214 let counter_index_width = text_width(&counter_index_str);
215 let binary_id_width = text_width(&binary_id_str);
216 let test_name_width = text_width(&test_name_str);
217
218 let mut stress_index_resolved_width = stress_index_width;
225 let mut counter_index_resolved_width = counter_index_width;
226 let mut binary_id_resolved_width = binary_id_width;
227 let mut test_name_resolved_width = test_name_width;
228
229 if stress_index_resolved_width > max_width {
231 stress_index_resolved_width = max_width;
232 }
233
234 let remaining_width = max_width.saturating_sub(stress_index_resolved_width);
236 if counter_index_resolved_width > remaining_width {
237 counter_index_resolved_width = remaining_width;
238 }
239
240 let remaining_width = max_width
242 .saturating_sub(stress_index_resolved_width)
243 .saturating_sub(counter_index_resolved_width);
244 if binary_id_resolved_width > remaining_width {
245 binary_id_resolved_width = remaining_width;
246 }
247
248 let remaining_width = max_width
250 .saturating_sub(stress_index_resolved_width)
251 .saturating_sub(counter_index_resolved_width)
252 .saturating_sub(binary_id_resolved_width);
253 if test_name_resolved_width > remaining_width {
254 test_name_resolved_width = remaining_width;
255 }
256
257 let test_name_truncated_str = if test_name_resolved_width == test_name_width {
259 test_name_str
260 } else {
261 truncate_ansi_aware(
263 &test_name_str,
264 test_name_width.saturating_sub(test_name_resolved_width),
265 test_name_width,
266 )
267 };
268 let binary_id_truncated_str = if binary_id_resolved_width == binary_id_width {
269 binary_id_str
270 } else {
271 truncate_ansi_aware(&binary_id_str, 0, binary_id_resolved_width)
273 };
274 let counter_index_truncated_str = if counter_index_resolved_width == counter_index_width
275 {
276 counter_index_str
277 } else {
278 truncate_ansi_aware(&counter_index_str, 0, counter_index_resolved_width)
280 };
281 let stress_index_truncated_str = if stress_index_resolved_width == stress_index_width {
282 stress_index_str
283 } else {
284 truncate_ansi_aware(&stress_index_str, 0, stress_index_resolved_width)
286 };
287
288 write!(
289 f,
290 "{}{}{}{}",
291 stress_index_truncated_str,
292 counter_index_truncated_str,
293 binary_id_truncated_str,
294 test_name_truncated_str,
295 )
296 } else {
297 write!(
298 f,
299 "{}{}{}{}",
300 stress_index_str, counter_index_str, binary_id_str, test_name_str
301 )
302 }
303 }
304}
305
306fn text_width(text: &str) -> usize {
307 strip_ansi_escapes::strip_str(text)
315 .chars()
316 .map(|c| c.width().unwrap_or(0))
317 .sum()
318}
319
320fn truncate_ansi_aware(text: &str, start: usize, end: usize) -> String {
321 let mut pos = 0;
322 let mut res = String::new();
323 for (s, is_ansi) in AnsiCodeIterator::new(text) {
324 if is_ansi {
325 res.push_str(s);
326 continue;
327 } else if pos >= end {
328 continue;
331 }
332
333 for c in s.chars() {
334 let c_width = c.width().unwrap_or(0);
335 if start <= pos && pos + c_width <= end {
336 res.push(c);
337 }
338 pos += c_width;
339 if pos > end {
340 break;
342 }
343 }
344 }
345
346 res
347}
348
349pub(crate) struct DisplayScriptInstance {
350 stress_index: Option<StressIndex>,
351 script_id: ScriptId,
352 full_command: String,
353 script_id_style: Style,
354 count_style: Style,
355}
356
357impl DisplayScriptInstance {
358 pub(crate) fn new(
359 stress_index: Option<StressIndex>,
360 script_id: ScriptId,
361 command: &str,
362 args: &[String],
363 script_id_style: Style,
364 count_style: Style,
365 ) -> Self {
366 let full_command =
367 shell_words::join(std::iter::once(command).chain(args.iter().map(|arg| arg.as_ref())));
368
369 Self {
370 stress_index,
371 script_id,
372 full_command,
373 script_id_style,
374 count_style,
375 }
376 }
377}
378
379impl fmt::Display for DisplayScriptInstance {
380 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
381 if let Some(stress_index) = self.stress_index {
382 write!(
383 f,
384 "[{}] ",
385 DisplayStressIndex {
386 stress_index,
387 count_style: self.count_style,
388 }
389 )?;
390 }
391 write!(
392 f,
393 "{}: {}",
394 self.script_id.style(self.script_id_style),
395 self.full_command,
396 )
397 }
398}
399
400struct DisplayStressIndex {
401 stress_index: StressIndex,
402 count_style: Style,
403}
404
405impl fmt::Display for DisplayStressIndex {
406 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
407 match self.stress_index.total {
408 Some(total) => {
409 write!(
410 f,
411 "{:>width$}/{}",
412 (self.stress_index.current + 1).style(self.count_style),
413 total.style(self.count_style),
414 width = u32_decimal_char_width(total.get()),
415 )
416 }
417 None => {
418 write!(
419 f,
420 "{}",
421 (self.stress_index.current + 1).style(self.count_style)
422 )
423 }
424 }
425 }
426}
427
428pub enum DisplayCounterIndex {
430 Counter {
432 current: usize,
434 total: usize,
436 },
437 Padded {
439 character: char,
441 width: usize,
443 },
444}
445
446impl DisplayCounterIndex {
447 pub fn new_counter(current: usize, total: usize) -> Self {
449 Self::Counter { current, total }
450 }
451
452 pub fn new_padded(character: char, width: usize) -> Self {
454 Self::Padded { character, width }
455 }
456}
457
458impl fmt::Display for DisplayCounterIndex {
459 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
460 match self {
461 Self::Counter { current, total } => {
462 write!(
463 f,
464 "({:>width$}/{})",
465 current,
466 total,
467 width = usize_decimal_char_width(*total)
468 )
469 }
470 Self::Padded { character, width } => {
471 let s: String = std::iter::repeat_n(*character, 2 * *width + 1).collect();
476 write!(f, "({s})")
477 }
478 }
479 }
480}
481
482pub(crate) fn usize_decimal_char_width(n: usize) -> usize {
483 (n.checked_ilog10().unwrap_or(0) + 1).try_into().unwrap()
487}
488
489pub(crate) fn u32_decimal_char_width(n: u32) -> usize {
490 (n.checked_ilog10().unwrap_or(0) + 1).try_into().unwrap()
494}
495
496pub(crate) fn u64_decimal_char_width(n: u64) -> usize {
497 (n.checked_ilog10().unwrap_or(0) + 1).try_into().unwrap()
501}
502
503pub(crate) fn write_test_name(
505 name: &TestCaseName,
506 style: &Styles,
507 writer: &mut dyn WriteStr,
508) -> io::Result<()> {
509 let (module_path, trailing) = name.module_path_and_name();
510 if let Some(module_path) = module_path {
511 write!(
512 writer,
513 "{}{}",
514 module_path.style(style.module_path),
515 "::".style(style.module_path)
516 )?;
517 }
518 write!(writer, "{}", trailing.style(style.test_name))?;
519
520 Ok(())
521}
522
523pub(crate) struct DisplayTestName<'a> {
525 name: &'a TestCaseName,
526 styles: &'a Styles,
527}
528
529impl<'a> DisplayTestName<'a> {
530 pub(crate) fn new(name: &'a TestCaseName, styles: &'a Styles) -> Self {
531 Self { name, styles }
532 }
533}
534
535impl fmt::Display for DisplayTestName<'_> {
536 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
537 let (module_path, trailing) = self.name.module_path_and_name();
538 if let Some(module_path) = module_path {
539 write!(
540 f,
541 "{}{}",
542 module_path.style(self.styles.module_path),
543 "::".style(self.styles.module_path)
544 )?;
545 }
546 write!(f, "{}", trailing.style(self.styles.test_name))?;
547
548 Ok(())
549 }
550}
551
552pub(crate) fn convert_build_platform(
553 platform: nextest_metadata::BuildPlatform,
554) -> guppy::graph::cargo::BuildPlatform {
555 match platform {
556 nextest_metadata::BuildPlatform::Target => guppy::graph::cargo::BuildPlatform::Target,
557 nextest_metadata::BuildPlatform::Host => guppy::graph::cargo::BuildPlatform::Host,
558 }
559}
560
561pub(crate) fn dylib_path_envvar() -> &'static str {
568 if cfg!(windows) {
569 "PATH"
570 } else if cfg!(target_os = "macos") {
571 "DYLD_FALLBACK_LIBRARY_PATH"
587 } else {
588 "LD_LIBRARY_PATH"
589 }
590}
591
592pub(crate) fn dylib_path() -> Vec<PathBuf> {
597 match std::env::var_os(dylib_path_envvar()) {
598 Some(var) => std::env::split_paths(&var).collect(),
599 None => Vec::new(),
600 }
601}
602
603#[cfg(windows)]
605pub(crate) fn convert_rel_path_to_forward_slash(rel_path: &Utf8Path) -> Utf8PathBuf {
606 if !rel_path.is_relative() {
607 panic!("path for conversion to forward slash '{rel_path}' is not relative");
608 }
609 rel_path.as_str().replace('\\', "/").into()
610}
611
612#[cfg(not(windows))]
613pub(crate) fn convert_rel_path_to_forward_slash(rel_path: &Utf8Path) -> Utf8PathBuf {
614 rel_path.to_path_buf()
615}
616
617#[cfg(windows)]
619pub(crate) fn convert_rel_path_to_main_sep(rel_path: &Utf8Path) -> Utf8PathBuf {
620 if !rel_path.is_relative() {
621 panic!("path for conversion to backslash '{rel_path}' is not relative");
622 }
623 rel_path.as_str().replace('/', "\\").into()
624}
625
626#[cfg(not(windows))]
627pub(crate) fn convert_rel_path_to_main_sep(rel_path: &Utf8Path) -> Utf8PathBuf {
628 rel_path.to_path_buf()
629}
630
631pub(crate) fn rel_path_join(rel_path: &Utf8Path, path: &Utf8Path) -> Utf8PathBuf {
633 assert!(rel_path.is_relative(), "rel_path {rel_path} is relative");
634 assert!(path.is_relative(), "path {path} is relative",);
635 format!("{rel_path}/{path}").into()
636}
637
638#[derive(Debug)]
639pub(crate) struct FormattedDuration(pub(crate) Duration);
640
641impl fmt::Display for FormattedDuration {
642 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
643 let duration = self.0.as_secs_f64();
644 if duration > 60.0 {
645 write!(f, "{}m {:.2}s", duration as u32 / 60, duration % 60.0)
646 } else {
647 write!(f, "{duration:.2}s")
648 }
649 }
650}
651
652#[derive(Debug)]
653pub(crate) struct FormattedRelativeDuration(pub(crate) Duration);
654
655impl fmt::Display for FormattedRelativeDuration {
656 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
657 fn item(unit: &'static str, value: u64) -> ControlFlow<(&'static str, u64)> {
661 if value > 0 {
662 ControlFlow::Break((unit, value))
663 } else {
664 ControlFlow::Continue(())
665 }
666 }
667
668 fn fmt(f: Duration) -> ControlFlow<(&'static str, u64), ()> {
672 let secs = f.as_secs();
673 let nanos = f.subsec_nanos();
674
675 let years = secs / 31_557_600; let year_days = secs % 31_557_600;
677 let months = year_days / 2_630_016; let month_days = year_days % 2_630_016;
679 let days = month_days / 86400;
680 let day_secs = month_days % 86400;
681 let hours = day_secs / 3600;
682 let minutes = day_secs % 3600 / 60;
683 let seconds = day_secs % 60;
684
685 let millis = nanos / 1_000_000;
686 let micros = nanos / 1_000;
687
688 item("y", years)?;
693 item("mo", months)?;
694 item("d", days)?;
695 item("h", hours)?;
696 item("m", minutes)?;
697 item("s", seconds)?;
698 item("ms", u64::from(millis))?;
699 item("us", u64::from(micros))?;
700 item("ns", u64::from(nanos))?;
701 ControlFlow::Continue(())
702 }
703
704 match fmt(self.0) {
705 ControlFlow::Break((unit, value)) => write!(f, "{value}{unit}"),
706 ControlFlow::Continue(()) => write!(f, "0s"),
707 }
708 }
709}
710
711#[derive(Clone, Debug)]
716pub struct ThemeCharacters {
717 hbar: char,
718 progress_chars: &'static str,
719 use_unicode: bool,
720}
721
722impl Default for ThemeCharacters {
723 fn default() -> Self {
724 Self {
725 hbar: '-',
726 progress_chars: "=> ",
727 use_unicode: false,
728 }
729 }
730}
731
732impl ThemeCharacters {
733 pub fn use_unicode(&mut self) {
735 self.hbar = '─';
736 self.progress_chars = "█▉▊▋▌▍▎▏ ";
738 self.use_unicode = true;
739 }
740
741 pub fn hbar_char(&self) -> char {
743 self.hbar
744 }
745
746 pub fn hbar(&self, width: usize) -> String {
748 std::iter::repeat_n(self.hbar, width).collect()
749 }
750
751 pub fn progress_chars(&self) -> &'static str {
753 self.progress_chars
754 }
755
756 pub fn tree_branch(&self) -> &'static str {
758 if self.use_unicode { "├─" } else { "|-" }
759 }
760
761 pub fn tree_last(&self) -> &'static str {
763 if self.use_unicode { "└─" } else { "\\-" }
764 }
765
766 pub fn tree_continuation(&self) -> &'static str {
768 if self.use_unicode { "│ " } else { "| " }
769 }
770
771 pub fn tree_space(&self) -> &'static str {
773 " "
774 }
775}
776
777pub(crate) fn display_exited_with(exit_status: ExitStatus) -> String {
779 match AbortStatus::extract(exit_status) {
780 Some(abort_status) => display_abort_status(abort_status),
781 None => match exit_status.code() {
782 Some(code) => format!("exited with exit code {code}"),
783 None => "exited with an unknown error".to_owned(),
784 },
785 }
786}
787
788pub(crate) fn display_abort_status(abort_status: AbortStatus) -> String {
790 match abort_status {
791 #[cfg(unix)]
792 AbortStatus::UnixSignal(sig) => match crate::helpers::signal_str(sig) {
793 Some(s) => {
794 format!("aborted with signal {sig} (SIG{s})")
795 }
796 None => {
797 format!("aborted with signal {sig}")
798 }
799 },
800 #[cfg(windows)]
801 AbortStatus::WindowsNtStatus(nt_status) => {
802 format!(
803 "aborted with code {}",
804 crate::helpers::display_nt_status(nt_status, Style::new())
806 )
807 }
808 #[cfg(windows)]
809 AbortStatus::JobObject => "terminated via job object".to_string(),
810 }
811}
812
813#[cfg(unix)]
814pub(crate) fn signal_str(signal: i32) -> Option<&'static str> {
815 match signal {
822 1 => Some("HUP"),
823 2 => Some("INT"),
824 3 => Some("QUIT"),
825 4 => Some("ILL"),
826 5 => Some("TRAP"),
827 6 => Some("ABRT"),
828 8 => Some("FPE"),
829 9 => Some("KILL"),
830 11 => Some("SEGV"),
831 13 => Some("PIPE"),
832 14 => Some("ALRM"),
833 15 => Some("TERM"),
834 _ => None,
835 }
836}
837
838#[cfg(windows)]
839pub(crate) fn display_nt_status(
840 nt_status: windows_sys::Win32::Foundation::NTSTATUS,
841 bold_style: Style,
842) -> String {
843 let bolded_status = format!("{:#010x}", nt_status.style(bold_style));
847
848 match windows_nt_status_message(nt_status) {
849 Some(message) => format!("{bolded_status}: {message}"),
850 None => bolded_status,
851 }
852}
853
854#[cfg(windows)]
856pub(crate) fn windows_nt_status_message(
857 nt_status: windows_sys::Win32::Foundation::NTSTATUS,
858) -> Option<smol_str::SmolStr> {
859 let win32_code = unsafe { windows_sys::Win32::Foundation::RtlNtStatusToDosError(nt_status) };
861
862 if win32_code == windows_sys::Win32::Foundation::ERROR_MR_MID_NOT_FOUND {
863 return None;
865 }
866
867 Some(smol_str::SmolStr::new(
868 io::Error::from_raw_os_error(win32_code as i32).to_string(),
869 ))
870}
871
872#[derive(Copy, Clone, Debug)]
873pub(crate) struct QuotedDisplay<'a, T: ?Sized>(pub(crate) &'a T);
874
875impl<T: ?Sized> fmt::Display for QuotedDisplay<'_, T>
876where
877 T: fmt::Display,
878{
879 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
880 write!(f, "'{}'", self.0)
881 }
882}
883
884unsafe extern "C" {
886 fn __nextest_external_symbol_that_does_not_exist();
887}
888
889pub fn format_interceptor_too_many_tests(
891 cli_opt_name: &str,
892 mode: NextestRunMode,
893 test_count: usize,
894 test_instances: &[OwnedTestInstanceId],
895 list_styles: &Styles,
896 count_style: Style,
897) -> String {
898 let mut msg = format!(
899 "--{} requires exactly one {}, but {} {} were selected:",
900 cli_opt_name,
901 plural::tests_plural_if(mode, false),
902 test_count.style(count_style),
903 plural::tests_str(mode, test_count)
904 );
905
906 for test_instance in test_instances {
907 let display = DisplayTestInstance::new(None, None, test_instance.as_ref(), list_styles);
908 swrite!(msg, "\n {}", display);
909 }
910
911 if test_count > test_instances.len() {
912 let remaining = test_count - test_instances.len();
913 swrite!(
914 msg,
915 "\n ... and {} more {}",
916 remaining.style(count_style),
917 plural::tests_str(mode, remaining)
918 );
919 }
920
921 msg
922}
923
924#[inline]
925#[expect(dead_code)]
926pub(crate) fn statically_unreachable() -> ! {
927 unsafe {
928 __nextest_external_symbol_that_does_not_exist();
929 }
930 unreachable!("linker symbol above cannot be resolved")
931}
932
933#[cfg(test)]
934mod test {
935 use super::*;
936
937 #[test]
938 fn test_decimal_char_width() {
939 assert_eq!(1, usize_decimal_char_width(0));
940 assert_eq!(1, usize_decimal_char_width(1));
941 assert_eq!(1, usize_decimal_char_width(5));
942 assert_eq!(1, usize_decimal_char_width(9));
943 assert_eq!(2, usize_decimal_char_width(10));
944 assert_eq!(2, usize_decimal_char_width(11));
945 assert_eq!(2, usize_decimal_char_width(99));
946 assert_eq!(3, usize_decimal_char_width(100));
947 assert_eq!(3, usize_decimal_char_width(999));
948 }
949
950 #[test]
951 fn test_u64_decimal_char_width() {
952 assert_eq!(1, u64_decimal_char_width(0));
953 assert_eq!(1, u64_decimal_char_width(1));
954 assert_eq!(1, u64_decimal_char_width(9));
955 assert_eq!(2, u64_decimal_char_width(10));
956 assert_eq!(2, u64_decimal_char_width(99));
957 assert_eq!(3, u64_decimal_char_width(100));
958 assert_eq!(3, u64_decimal_char_width(999));
959 assert_eq!(6, u64_decimal_char_width(999_999));
960 assert_eq!(7, u64_decimal_char_width(1_000_000));
961 assert_eq!(8, u64_decimal_char_width(10_000_000));
962 assert_eq!(8, u64_decimal_char_width(11_000_000));
963 }
964}