Skip to content

Commit 8cb3b6a

Browse files
committed
Rewrite Description to use the new description renderer introduced in the previous commit.
Unit tests of `Description` are adjusted to use more idiomatic APIs. This also changes the pre-existing direct users of `Description` to use idiomatic APIs rather than producing a `String` and wrapping that in a `Description` as they did before. Otherwise, this change would introduce several regressions in these matcher's explanations. This removes the now unused method `Description::indent_except_first_line`. Finally, the changes introduced earlier to indent the diff output for the `eq` and related matchers are now removed, since the indentation is now handled by the renderer.
1 parent ef44195 commit 8cb3b6a

17 files changed

+220
-335
lines changed

googletest/src/description.rs

Lines changed: 78 additions & 191 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
use std::fmt::{Display, Formatter, Result};
15+
use std::{
16+
borrow::Cow,
17+
fmt::{Display, Formatter, Result},
18+
};
19+
20+
use crate::internal::description_renderer::{List, INDENTATION_SIZE};
1621

1722
/// Helper structure to build better output of
1823
/// [`Matcher::describe`][crate::matcher::Matcher::describe] and
@@ -43,38 +48,40 @@ use std::fmt::{Display, Formatter, Result};
4348
/// respectively [`Description::bullet_list`] has been called.
4449
#[derive(Debug, Default)]
4550
pub struct Description {
46-
elements: Vec<String>,
47-
indent_mode: IndentMode,
48-
list_style: ListStyle,
51+
elements: List,
52+
initial_indentation: usize,
4953
}
5054

51-
#[derive(Debug, Default)]
52-
enum IndentMode {
53-
#[default]
54-
NoIndent,
55-
EveryLine,
56-
AllExceptFirstLine,
57-
}
55+
impl Description {
56+
/// Returns a new empty [`Description`].
57+
pub fn new() -> Self {
58+
Default::default()
59+
}
5860

59-
#[derive(Debug, Default)]
60-
enum ListStyle {
61-
#[default]
62-
NoList,
63-
Bullet,
64-
Enumerate,
65-
}
61+
/// Appends a block of text to this instance.
62+
///
63+
/// The block is indented uniformly when this instance is rendered.
64+
pub fn text(mut self, text: impl Into<Cow<'static, str>>) -> Self {
65+
self.elements.push_literal(text.into());
66+
self
67+
}
6668

67-
struct IndentationSizes {
68-
first_line_indent: usize,
69-
first_line_of_element_indent: usize,
70-
enumeration_padding: usize,
71-
other_line_indent: usize,
72-
}
69+
/// Appends a nested [`Description`] to this instance.
70+
///
71+
/// The nested [`Description`] `inner` is indented uniformly at the next level of indentation
72+
/// when this instance is rendered.
73+
pub fn nested(mut self, inner: Description) -> Self {
74+
self.elements.push_nested(inner.elements);
75+
self
76+
}
7377

74-
/// Number of space used to indent lines when no alignement is required.
75-
const INDENTATION_SIZE: usize = 2;
78+
/// Appends all [`Description`] in the given sequence `inner` to this instance.
79+
///
80+
/// Each element is treated as a nested [`Description`] in the sense of [`Self::nested`].
81+
pub fn collect(self, inner: impl IntoIterator<Item = Description>) -> Self {
82+
inner.into_iter().fold(self, |outer, inner| outer.nested(inner))
83+
}
7684

77-
impl Description {
7885
/// Indents the lines in elements of this description.
7986
///
8087
/// This operation will be performed lazily when [`self`] is displayed.
@@ -91,27 +98,7 @@ impl Description {
9198
/// # .unwrap();
9299
/// ```
93100
pub fn indent(self) -> Self {
94-
Self { indent_mode: IndentMode::EveryLine, ..self }
95-
}
96-
97-
/// Indents the lines in elements of this description except for the first
98-
/// line.
99-
///
100-
/// This is similar to [`Self::indent`] except that the first line is not
101-
/// indented. This is useful when the first line has already been indented
102-
/// in the output.
103-
///
104-
/// For example:
105-
///
106-
/// ```
107-
/// # use googletest::prelude::*;
108-
/// # use googletest::description::Description;
109-
/// let description = std::iter::once("A B C\nD E F".to_string()).collect::<Description>();
110-
/// verify_that!(description.indent_except_first_line(), displays_as(eq("A B C\n D E F")))
111-
/// # .unwrap();
112-
/// ```
113-
pub fn indent_except_first_line(self) -> Self {
114-
Self { indent_mode: IndentMode::AllExceptFirstLine, ..self }
101+
Self { initial_indentation: INDENTATION_SIZE, ..self }
115102
}
116103

117104
/// Bullet lists the elements of [`self`].
@@ -131,7 +118,7 @@ impl Description {
131118
/// # .unwrap();
132119
/// ```
133120
pub fn bullet_list(self) -> Self {
134-
Self { list_style: ListStyle::Bullet, ..self }
121+
Self { elements: self.elements.bullet_list(), ..self }
135122
}
136123

137124
/// Enumerates the elements of [`self`].
@@ -151,7 +138,7 @@ impl Description {
151138
/// # .unwrap();
152139
/// ```
153140
pub fn enumerate(self) -> Self {
154-
Self { list_style: ListStyle::Enumerate, ..self }
141+
Self { elements: self.elements.enumerate(), ..self }
155142
}
156143

157144
/// Returns the length of elements.
@@ -163,80 +150,11 @@ impl Description {
163150
pub fn is_empty(&self) -> bool {
164151
self.elements.is_empty()
165152
}
166-
167-
fn indentation_sizes(&self) -> IndentationSizes {
168-
let first_line_indent =
169-
if matches!(self.indent_mode, IndentMode::EveryLine) { INDENTATION_SIZE } else { 0 };
170-
let first_line_of_element_indent =
171-
if !matches!(self.indent_mode, IndentMode::NoIndent) { INDENTATION_SIZE } else { 0 };
172-
// Number of digit of the last index. For instance, an array of length 13 will
173-
// have 12 as last index (we start at 0), which have a digit size of 2.
174-
let enumeration_padding = if self.elements.len() > 1 {
175-
((self.elements.len() - 1) as f64).log10().floor() as usize + 1
176-
} else {
177-
// Avoid negative logarithm when there is only 0 or 1 element.
178-
1
179-
};
180-
181-
let other_line_indent = first_line_of_element_indent
182-
+ match self.list_style {
183-
ListStyle::NoList => 0,
184-
ListStyle::Bullet => "* ".len(),
185-
ListStyle::Enumerate => enumeration_padding + ". ".len(),
186-
};
187-
IndentationSizes {
188-
first_line_indent,
189-
first_line_of_element_indent,
190-
enumeration_padding,
191-
other_line_indent,
192-
}
193-
}
194153
}
195154

196155
impl Display for Description {
197156
fn fmt(&self, f: &mut Formatter) -> Result {
198-
let IndentationSizes {
199-
mut first_line_indent,
200-
first_line_of_element_indent,
201-
enumeration_padding,
202-
other_line_indent,
203-
} = self.indentation_sizes();
204-
205-
let mut first = true;
206-
for (idx, element) in self.elements.iter().enumerate() {
207-
let mut lines = element.lines();
208-
if let Some(line) = lines.next() {
209-
if first {
210-
first = false;
211-
} else {
212-
writeln!(f)?;
213-
}
214-
match self.list_style {
215-
ListStyle::NoList => {
216-
write!(f, "{:first_line_indent$}{line}", "")?;
217-
}
218-
ListStyle::Bullet => {
219-
write!(f, "{:first_line_indent$}* {line}", "")?;
220-
}
221-
ListStyle::Enumerate => {
222-
write!(
223-
f,
224-
"{:first_line_indent$}{:>enumeration_padding$}. {line}",
225-
"", idx,
226-
)?;
227-
}
228-
}
229-
}
230-
for line in lines {
231-
writeln!(f)?;
232-
write!(f, "{:other_line_indent$}{line}", "")?;
233-
}
234-
if element.ends_with("\n") {
235-
writeln!(f)?;
236-
}
237-
first_line_indent = first_line_of_element_indent;
238-
}
239-
Ok(())
157+
self.elements.render(f, self.initial_indentation)
240158
}
241159
}
242160

@@ -254,13 +172,15 @@ impl FromIterator<Description> for Description {
254172
where
255173
T: IntoIterator<Item = Description>,
256174
{
257-
Self { elements: iter.into_iter().map(|s| format!("{s}")).collect(), ..Default::default() }
175+
Self { elements: iter.into_iter().map(|s| s.elements).collect(), ..Default::default() }
258176
}
259177
}
260178

261179
impl<T: Into<String>> From<T> for Description {
262180
fn from(value: T) -> Self {
263-
Self { elements: vec![value.into()], ..Default::default() }
181+
let mut elements = List::default();
182+
elements.push_literal(value.into().into());
183+
Self { elements, ..Default::default() }
264184
}
265185
}
266186

@@ -271,124 +191,91 @@ mod tests {
271191
use indoc::indoc;
272192

273193
#[test]
274-
fn description_single_element() -> Result<()> {
275-
let description = ["A B C".to_string()].into_iter().collect::<Description>();
194+
fn renders_single_fragment() -> Result<()> {
195+
let description: Description = "A B C".into();
276196
verify_that!(description, displays_as(eq("A B C")))
277197
}
278198

279199
#[test]
280-
fn description_two_elements() -> Result<()> {
200+
fn renders_two_fragments() -> Result<()> {
281201
let description =
282202
["A B C".to_string(), "D E F".to_string()].into_iter().collect::<Description>();
283203
verify_that!(description, displays_as(eq("A B C\nD E F")))
284204
}
285205

286206
#[test]
287-
fn description_indent_single_element() -> Result<()> {
288-
let description = ["A B C".to_string()].into_iter().collect::<Description>().indent();
289-
verify_that!(description, displays_as(eq(" A B C")))
207+
fn nested_description_is_indented() -> Result<()> {
208+
let description = Description::new()
209+
.text("Header")
210+
.nested(["A B C".to_string()].into_iter().collect::<Description>());
211+
verify_that!(description, displays_as(eq("Header\n A B C")))
290212
}
291213

292214
#[test]
293-
fn description_indent_two_elements() -> Result<()> {
294-
let description = ["A B C".to_string(), "D E F".to_string()]
295-
.into_iter()
296-
.collect::<Description>()
297-
.indent();
298-
verify_that!(description, displays_as(eq(" A B C\n D E F")))
215+
fn nested_description_indents_two_elements() -> Result<()> {
216+
let description = Description::new().text("Header").nested(
217+
["A B C".to_string(), "D E F".to_string()].into_iter().collect::<Description>(),
218+
);
219+
verify_that!(description, displays_as(eq("Header\n A B C\n D E F")))
299220
}
300221

301222
#[test]
302-
fn description_indent_two_elements_except_first_line() -> Result<()> {
303-
let description = ["A B C".to_string(), "D E F".to_string()]
304-
.into_iter()
305-
.collect::<Description>()
306-
.indent_except_first_line();
307-
verify_that!(description, displays_as(eq("A B C\n D E F")))
223+
fn nested_description_indents_one_element_on_two_lines() -> Result<()> {
224+
let description = Description::new().text("Header").nested("A B C\nD E F".into());
225+
verify_that!(description, displays_as(eq("Header\n A B C\n D E F")))
308226
}
309227

310228
#[test]
311-
fn description_indent_single_element_two_lines() -> Result<()> {
312-
let description =
313-
["A B C\nD E F".to_string()].into_iter().collect::<Description>().indent();
314-
verify_that!(description, displays_as(eq(" A B C\n D E F")))
315-
}
316-
317-
#[test]
318-
fn description_indent_single_element_two_lines_except_first_line() -> Result<()> {
319-
let description = ["A B C\nD E F".to_string()]
320-
.into_iter()
321-
.collect::<Description>()
322-
.indent_except_first_line();
323-
verify_that!(description, displays_as(eq("A B C\n D E F")))
229+
fn single_fragment_renders_with_bullet_when_bullet_list_enabled() -> Result<()> {
230+
let description = Description::new().text("A B C").bullet_list();
231+
verify_that!(description, displays_as(eq("* A B C")))
324232
}
325233

326234
#[test]
327-
fn description_bullet_single_element() -> Result<()> {
328-
let description = ["A B C".to_string()].into_iter().collect::<Description>().bullet_list();
235+
fn single_nested_fragment_renders_with_bullet_when_bullet_list_enabled() -> Result<()> {
236+
let description = Description::new().nested("A B C".into()).bullet_list();
329237
verify_that!(description, displays_as(eq("* A B C")))
330238
}
331239

332240
#[test]
333-
fn description_bullet_two_elements() -> Result<()> {
334-
let description = ["A B C".to_string(), "D E F".to_string()]
335-
.into_iter()
336-
.collect::<Description>()
337-
.bullet_list();
241+
fn two_fragments_render_with_bullet_when_bullet_list_enabled() -> Result<()> {
242+
let description = Description::new().text("A B C").text("D E F").bullet_list();
338243
verify_that!(description, displays_as(eq("* A B C\n* D E F")))
339244
}
340245

341246
#[test]
342-
fn description_bullet_single_element_two_lines() -> Result<()> {
247+
fn two_nested_fragments_render_with_bullet_when_bullet_list_enabled() -> Result<()> {
343248
let description =
344-
["A B C\nD E F".to_string()].into_iter().collect::<Description>().bullet_list();
345-
verify_that!(description, displays_as(eq("* A B C\n D E F")))
346-
}
347-
348-
#[test]
349-
fn description_bullet_single_element_two_lines_indent_except_first_line() -> Result<()> {
350-
let description = ["A B C\nD E F".to_string()]
351-
.into_iter()
352-
.collect::<Description>()
353-
.bullet_list()
354-
.indent_except_first_line();
355-
verify_that!(description, displays_as(eq("* A B C\n D E F")))
249+
Description::new().nested("A B C".into()).nested("D E F".into()).bullet_list();
250+
verify_that!(description, displays_as(eq("* A B C\n* D E F")))
356251
}
357252

358253
#[test]
359-
fn description_bullet_two_elements_indent_except_first_line() -> Result<()> {
360-
let description = ["A B C".to_string(), "D E F".to_string()]
361-
.into_iter()
362-
.collect::<Description>()
363-
.bullet_list()
364-
.indent_except_first_line();
365-
verify_that!(description, displays_as(eq("* A B C\n * D E F")))
254+
fn single_fragment_with_more_than_one_line_renders_with_one_bullet() -> Result<()> {
255+
let description = Description::new().text("A B C\nD E F").bullet_list();
256+
verify_that!(description, displays_as(eq("* A B C\n D E F")))
366257
}
367258

368259
#[test]
369-
fn description_enumerate_single_element() -> Result<()> {
370-
let description = ["A B C".to_string()].into_iter().collect::<Description>().enumerate();
260+
fn single_fragment_renders_with_enumeration_when_enumerate_enabled() -> Result<()> {
261+
let description = Description::new().text("A B C").enumerate();
371262
verify_that!(description, displays_as(eq("0. A B C")))
372263
}
373264

374265
#[test]
375-
fn description_enumerate_two_elements() -> Result<()> {
376-
let description = ["A B C".to_string(), "D E F".to_string()]
377-
.into_iter()
378-
.collect::<Description>()
379-
.enumerate();
266+
fn two_fragments_render_with_enumeration_when_enumerate_enabled() -> Result<()> {
267+
let description = Description::new().text("A B C").text("D E F").enumerate();
380268
verify_that!(description, displays_as(eq("0. A B C\n1. D E F")))
381269
}
382270

383271
#[test]
384-
fn description_enumerate_single_element_two_lines() -> Result<()> {
385-
let description =
386-
["A B C\nD E F".to_string()].into_iter().collect::<Description>().enumerate();
272+
fn single_fragment_with_two_lines_renders_with_one_enumeration_label() -> Result<()> {
273+
let description = Description::new().text("A B C\nD E F").enumerate();
387274
verify_that!(description, displays_as(eq("0. A B C\n D E F")))
388275
}
389276

390277
#[test]
391-
fn description_enumerate_correct_indentation_with_large_index() -> Result<()> {
278+
fn multi_digit_enumeration_renders_with_correct_offset() -> Result<()> {
392279
let description = ["A B C\nD E F"; 11]
393280
.into_iter()
394281
.map(str::to_string)

0 commit comments

Comments
 (0)