Skip to content

Commit bb5417a

Browse files
authored
[nextest-runner] slightly smarter test name truncation (#2735)
Truncate the test name first, then the other components.
1 parent 67f12ff commit bb5417a

File tree

2 files changed

+199
-84
lines changed

2 files changed

+199
-84
lines changed

nextest-runner/src/helpers.rs

Lines changed: 188 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ use crate::{
1010
write_str::WriteStr,
1111
};
1212
use camino::{Utf8Path, Utf8PathBuf};
13+
use console::AnsiCodeIterator;
1314
use owo_colors::{OwoColorize, Style};
1415
use std::{fmt, io, path::PathBuf, process::ExitStatus, time::Duration};
16+
use unicode_width::UnicodeWidthChar;
1517

1618
/// Utilities for pluralizing various words based on count or plurality.
1719
pub mod plural {
@@ -102,6 +104,7 @@ pub(crate) struct DisplayTestInstance<'a> {
102104
display_counter_index: Option<DisplayCounterIndex>,
103105
instance: TestInstanceId<'a>,
104106
styles: &'a Styles,
107+
max_width: Option<usize>,
105108
}
106109

107110
impl<'a> DisplayTestInstance<'a> {
@@ -116,34 +119,180 @@ impl<'a> DisplayTestInstance<'a> {
116119
display_counter_index,
117120
instance,
118121
styles,
122+
max_width: None,
119123
}
120124
}
125+
126+
pub(crate) fn with_max_width(mut self, max_width: usize) -> Self {
127+
self.max_width = Some(max_width);
128+
self
129+
}
121130
}
122131

123132
impl fmt::Display for DisplayTestInstance<'_> {
124133
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
125-
if let Some(stress_index) = self.stress_index {
126-
write!(
127-
f,
134+
// Figure out the widths for each component.
135+
let stress_index_str = if let Some(stress_index) = self.stress_index {
136+
format!(
128137
"[{}] ",
129138
DisplayStressIndex {
130139
stress_index,
131140
count_style: self.styles.count,
132141
}
133-
)?;
142+
)
143+
} else {
144+
String::new()
145+
};
146+
let counter_index_str = if let Some(display_counter_index) = &self.display_counter_index {
147+
format!("{display_counter_index} ")
148+
} else {
149+
String::new()
150+
};
151+
let binary_id_str = format!("{} ", self.instance.binary_id.style(self.styles.binary_id));
152+
let test_name_str = format!(
153+
"{}",
154+
DisplayTestName::new(self.instance.test_name, self.styles)
155+
);
156+
157+
// If a max width is defined, trim strings until they fit into it.
158+
if let Some(max_width) = self.max_width {
159+
// We have to be careful while computing string width -- the strings
160+
// above include ANSI escape codes which have a display width of
161+
// zero.
162+
let stress_index_width = text_width(&stress_index_str);
163+
let counter_index_width = text_width(&counter_index_str);
164+
let binary_id_width = text_width(&binary_id_str);
165+
let test_name_width = text_width(&test_name_str);
166+
167+
// Truncate components in order, from most important to keep to least:
168+
//
169+
// * stress-index (left-aligned)
170+
// * counter index (left-aligned)
171+
// * binary ID (left-aligned)
172+
// * test name (right-aligned)
173+
let mut stress_index_resolved_width = stress_index_width;
174+
let mut counter_index_resolved_width = counter_index_width;
175+
let mut binary_id_resolved_width = binary_id_width;
176+
let mut test_name_resolved_width = test_name_width;
177+
178+
// Truncate stress-index first.
179+
if stress_index_resolved_width > max_width {
180+
stress_index_resolved_width = max_width;
181+
}
182+
183+
// Truncate counter index next.
184+
let remaining_width = max_width.saturating_sub(stress_index_resolved_width);
185+
if counter_index_resolved_width > remaining_width {
186+
counter_index_resolved_width = remaining_width;
187+
}
188+
189+
// Truncate binary ID next.
190+
let remaining_width = max_width
191+
.saturating_sub(stress_index_resolved_width)
192+
.saturating_sub(counter_index_resolved_width);
193+
if binary_id_resolved_width > remaining_width {
194+
binary_id_resolved_width = remaining_width;
195+
}
196+
197+
// Truncate test name last.
198+
let remaining_width = max_width
199+
.saturating_sub(stress_index_resolved_width)
200+
.saturating_sub(counter_index_resolved_width)
201+
.saturating_sub(binary_id_resolved_width);
202+
if test_name_resolved_width > remaining_width {
203+
test_name_resolved_width = remaining_width;
204+
}
205+
206+
// Now truncate the strings if applicable.
207+
let test_name_truncated_str = if test_name_resolved_width == test_name_width {
208+
test_name_str
209+
} else {
210+
// Right-align the test name.
211+
truncate_ansi_aware(
212+
&test_name_str,
213+
test_name_width.saturating_sub(test_name_resolved_width),
214+
test_name_width,
215+
)
216+
};
217+
let binary_id_truncated_str = if binary_id_resolved_width == binary_id_width {
218+
binary_id_str
219+
} else {
220+
// Left-align the binary ID.
221+
truncate_ansi_aware(&binary_id_str, 0, binary_id_resolved_width)
222+
};
223+
let counter_index_truncated_str = if counter_index_resolved_width == counter_index_width
224+
{
225+
counter_index_str
226+
} else {
227+
// Left-align the counter index.
228+
truncate_ansi_aware(&counter_index_str, 0, counter_index_resolved_width)
229+
};
230+
let stress_index_truncated_str = if stress_index_resolved_width == stress_index_width {
231+
stress_index_str
232+
} else {
233+
// Left-align the stress index.
234+
truncate_ansi_aware(&stress_index_str, 0, stress_index_resolved_width)
235+
};
236+
237+
write!(
238+
f,
239+
"{}{}{}{}",
240+
stress_index_truncated_str,
241+
counter_index_truncated_str,
242+
binary_id_truncated_str,
243+
test_name_truncated_str,
244+
)
245+
} else {
246+
write!(
247+
f,
248+
"{}{}{}{}",
249+
stress_index_str, counter_index_str, binary_id_str, test_name_str
250+
)
134251
}
252+
}
253+
}
135254

136-
if let Some(display_counter_index) = &self.display_counter_index {
137-
write!(f, "{display_counter_index} ")?
255+
fn text_width(text: &str) -> usize {
256+
// Technically, the width of a string may not be the same as the sum of the
257+
// widths of its characters. But managing truncation is pretty difficult. See
258+
// https://docs.rs/unicode-width/latest/unicode_width/#rules-for-determining-width.
259+
//
260+
// This is quite difficult to manage truncation for, so we just use the sum
261+
// of the widths of the string's characters (both here and in
262+
// truncate_ansi_aware below).
263+
strip_ansi_escapes::strip_str(text)
264+
.chars()
265+
.map(|c| c.width().unwrap_or(0))
266+
.sum()
267+
}
268+
269+
fn truncate_ansi_aware(text: &str, start: usize, end: usize) -> String {
270+
let mut pos = 0;
271+
let mut res = String::new();
272+
for (s, is_ansi) in AnsiCodeIterator::new(text) {
273+
if is_ansi {
274+
res.push_str(s);
275+
continue;
276+
} else if pos >= end {
277+
// We retain ANSI escape codes, so this is `continue` rather than
278+
// `break`.
279+
continue;
138280
}
139281

140-
write!(
141-
f,
142-
"{} ",
143-
self.instance.binary_id.style(self.styles.binary_id),
144-
)?;
145-
fmt_write_test_name(self.instance.test_name, self.styles, f)
282+
for c in s.chars() {
283+
let c_width = c.width().unwrap_or(0);
284+
if start <= pos && pos + c_width <= end {
285+
res.push(c);
286+
}
287+
pos += c_width;
288+
if pos > end {
289+
// no need to iterate over the rest of s
290+
break;
291+
}
292+
}
146293
}
294+
295+
res
147296
}
148297

149298
pub(crate) struct DisplayScriptInstance {
@@ -300,26 +449,35 @@ pub(crate) fn write_test_name(
300449
Ok(())
301450
}
302451

303-
/// Write out a test name, `std::fmt::Write` version.
304-
pub(crate) fn fmt_write_test_name(
305-
name: &str,
306-
style: &Styles,
307-
writer: &mut dyn fmt::Write,
308-
) -> fmt::Result {
309-
// Look for the part of the test after the last ::, if any.
310-
let mut splits = name.rsplitn(2, "::");
311-
let trailing = splits.next().expect("test should have at least 1 element");
312-
if let Some(rest) = splits.next() {
313-
write!(
314-
writer,
315-
"{}{}",
316-
rest.style(style.module_path),
317-
"::".style(style.module_path)
318-
)?;
452+
/// Wrapper for displaying a test name with styling.
453+
pub(crate) struct DisplayTestName<'a> {
454+
name: &'a str,
455+
styles: &'a Styles,
456+
}
457+
458+
impl<'a> DisplayTestName<'a> {
459+
pub(crate) fn new(name: &'a str, styles: &'a Styles) -> Self {
460+
Self { name, styles }
319461
}
320-
write!(writer, "{}", trailing.style(style.test_name))?;
462+
}
321463

322-
Ok(())
464+
impl fmt::Display for DisplayTestName<'_> {
465+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
466+
// Look for the part of the test after the last ::, if any.
467+
let mut splits = self.name.rsplitn(2, "::");
468+
let trailing = splits.next().expect("test should have at least 1 element");
469+
if let Some(rest) = splits.next() {
470+
write!(
471+
f,
472+
"{}{}",
473+
rest.style(self.styles.module_path),
474+
"::".style(self.styles.module_path)
475+
)?;
476+
}
477+
write!(f, "{}", trailing.style(self.styles.test_name))?;
478+
479+
Ok(())
480+
}
323481
}
324482

325483
pub(crate) fn convert_build_platform(

nextest-runner/src/reporter/displayer/progress.rs

Lines changed: 11 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ use crate::{
1010
helpers::Styles,
1111
},
1212
};
13-
use console::AnsiCodeIterator;
1413
use indicatif::{ProgressBar, ProgressDrawTarget, ProgressStyle};
1514
use nextest_metadata::RustBinaryId;
1615
use owo_colors::OwoColorize;
@@ -24,7 +23,6 @@ use std::{
2423
};
2524
use swrite::{SWrite, swrite};
2625
use tracing::debug;
27-
use unicode_width::UnicodeWidthChar as _;
2826

2927
/// The maximum number of running tests to display with
3028
/// `--show-progress=running` or `only`.
@@ -132,63 +130,22 @@ impl RunningTest {
132130
elapsed.as_secs() / 60,
133131
elapsed.as_secs() % 60,
134132
);
135-
let mut test = format!(
136-
"{}",
137-
DisplayTestInstance::new(
138-
None,
139-
None,
140-
TestInstanceId {
141-
binary_id: &self.binary_id,
142-
143-
test_name: &self.test_name
144-
},
145-
&styles.list_styles
146-
)
147-
);
148133
let max_width = width.saturating_sub(25);
149-
let test_width = measure_text_width(&test);
150-
if test_width > max_width {
151-
test = ansi_get(&test, test_width - max_width, test_width)
152-
}
134+
let test = DisplayTestInstance::new(
135+
None,
136+
None,
137+
TestInstanceId {
138+
binary_id: &self.binary_id,
139+
140+
test_name: &self.test_name,
141+
},
142+
&styles.list_styles,
143+
)
144+
.with_max_width(max_width);
153145
format!(" {} [{:>9}] {}", status, elapsed, test)
154146
}
155147
}
156148

157-
pub fn measure_text_width(s: &str) -> usize {
158-
AnsiCodeIterator::new(s)
159-
.filter_map(|(s, is_ansi)| match is_ansi {
160-
false => Some(s.chars().count()),
161-
true => None,
162-
})
163-
.sum()
164-
}
165-
166-
pub fn ansi_get(text: &str, start: usize, end: usize) -> String {
167-
let mut pos = 0;
168-
let mut res = String::new();
169-
for (s, is_ansi) in AnsiCodeIterator::new(text) {
170-
if is_ansi {
171-
res.push_str(s);
172-
continue;
173-
} else if pos >= end {
174-
continue;
175-
}
176-
177-
for c in s.chars() {
178-
let c_width = c.width().unwrap_or(0);
179-
if start <= pos && pos + c_width <= end {
180-
res.push(c);
181-
}
182-
pos += c_width;
183-
if pos > end {
184-
// no need to iterate over the rest of s
185-
break;
186-
}
187-
}
188-
}
189-
res
190-
}
191-
192149
#[derive(Debug)]
193150
pub(super) struct ProgressBarState {
194151
bar: ProgressBar,

0 commit comments

Comments
 (0)