1 // Copyright 2023 Google LLC
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 //      http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 
15 use std::{
16     borrow::Cow,
17     fmt::{Display, Formatter, Result},
18 };
19 
20 use crate::internal::description_renderer::{List, INDENTATION_SIZE};
21 
22 /// A structured description, either of a (composed) matcher or of an
23 /// assertion failure.
24 ///
25 /// One can compose blocks of text into a `Description`. Each one appears on a
26 /// new line. For example:
27 ///
28 /// ```
29 /// # use googletest::prelude::*;
30 /// # use googletest::description::Description;
31 /// let description = Description::new()
32 ///     .text("A block")
33 ///     .text("Another block");
34 /// verify_that!(description, displays_as(eq("A block\nAnother block")))
35 /// # .unwrap();
36 /// ```
37 ///
38 /// One can embed nested descriptions into a `Description`. The resulting
39 /// nested description is then rendered with an additional level of
40 /// indentation. For example:
41 ///
42 /// ```
43 /// # use googletest::prelude::*;
44 /// # use googletest::description::Description;
45 /// let inner_description = Description::new()
46 ///     .text("A block")
47 ///     .text("Another block");
48 /// let outer_description = Description::new()
49 ///     .text("Header")
50 ///     .nested(inner_description);
51 /// verify_that!(outer_description, displays_as(eq("\
52 /// Header
53 ///   A block
54 ///   Another block")))
55 /// # .unwrap();
56 /// ```
57 ///
58 /// One can also enumerate or bullet list the elements of a `Description`:
59 ///
60 /// ```
61 /// # use googletest::prelude::*;
62 /// # use googletest::description::Description;
63 /// let description = Description::new()
64 ///     .text("First item")
65 ///     .text("Second item")
66 ///     .bullet_list();
67 /// verify_that!(description, displays_as(eq("\
68 /// * First item
69 /// * Second item")))
70 /// # .unwrap();
71 /// ```
72 ///
73 /// One can construct a `Description` from a [`String`] or a string slice, an
74 /// iterator thereof, or from an iterator over other `Description`s:
75 ///
76 /// ```
77 /// # use googletest::description::Description;
78 /// let single_element_description: Description =
79 ///     "A single block description".into();
80 /// let two_element_description: Description =
81 ///     ["First item", "Second item"].into_iter().collect();
82 /// let two_element_description_from_strings: Description =
83 ///     ["First item".to_string(), "Second item".to_string()].into_iter().collect();
84 /// ```
85 ///
86 /// No newline is added after the last element during rendering. This makes it
87 /// easier to support single-line matcher descriptions and match explanations.
88 #[derive(Debug, Default)]
89 pub struct Description {
90     elements: List,
91     initial_indentation: usize,
92 }
93 
94 impl Description {
95     /// Returns a new empty [`Description`].
new() -> Self96     pub fn new() -> Self {
97         Default::default()
98     }
99 
100     /// Appends a block of text to this instance.
101     ///
102     /// The block is indented uniformly when this instance is rendered.
text(mut self, text: impl Into<Cow<'static, str>>) -> Self103     pub fn text(mut self, text: impl Into<Cow<'static, str>>) -> Self {
104         self.elements.push_literal(text.into());
105         self
106     }
107 
108     /// Appends a nested [`Description`] to this instance.
109     ///
110     /// The nested [`Description`] `inner` is indented uniformly at the next
111     /// level of indentation when this instance is rendered.
nested(mut self, inner: Description) -> Self112     pub fn nested(mut self, inner: Description) -> Self {
113         self.elements.push_nested(inner.elements);
114         self
115     }
116 
117     /// Appends all [`Description`] in the given sequence `inner` to this
118     /// instance.
119     ///
120     /// Each element is treated as a nested [`Description`] in the sense of
121     /// [`Self::nested`].
collect(self, inner: impl IntoIterator<Item = Description>) -> Self122     pub fn collect(self, inner: impl IntoIterator<Item = Description>) -> Self {
123         inner.into_iter().fold(self, |outer, inner| outer.nested(inner))
124     }
125 
126     /// Indents the lines in elements of this description.
127     ///
128     /// This operation will be performed lazily when [`self`] is displayed.
129     ///
130     /// This will indent every line inside each element.
131     ///
132     /// For example:
133     ///
134     /// ```
135     /// # use googletest::prelude::*;
136     /// # use googletest::description::Description;
137     /// let description = std::iter::once("A B C\nD E F".to_string()).collect::<Description>();
138     /// verify_that!(description.indent(), displays_as(eq("  A B C\n  D E F")))
139     /// # .unwrap();
140     /// ```
indent(self) -> Self141     pub fn indent(self) -> Self {
142         Self { initial_indentation: INDENTATION_SIZE, ..self }
143     }
144 
145     /// Instructs this instance to render its elements as a bullet list.
146     ///
147     /// Each element (from either [`Description::text`] or
148     /// [`Description::nested`]) is rendered as a bullet point. If an element
149     /// contains multiple lines, the following lines are aligned with the first
150     /// one in the block.
151     ///
152     /// For instance:
153     ///
154     /// ```
155     /// # use googletest::prelude::*;
156     /// # use googletest::description::Description;
157     /// let description = Description::new()
158     ///     .text("First line\nsecond line")
159     ///     .bullet_list();
160     /// verify_that!(description, displays_as(eq("\
161     /// * First line
162     ///   second line")))
163     /// # .unwrap();
164     /// ```
bullet_list(self) -> Self165     pub fn bullet_list(self) -> Self {
166         Self { elements: self.elements.bullet_list(), ..self }
167     }
168 
169     /// Instructs this instance to render its elements as an enumerated list.
170     ///
171     /// Each element (from either [`Description::text`] or
172     /// [`Description::nested`]) is rendered with its zero-based index. If an
173     /// element contains multiple lines, the following lines are aligned with
174     /// the first one in the block.
175     ///
176     /// For instance:
177     ///
178     /// ```
179     /// # use googletest::prelude::*;
180     /// # use googletest::description::Description;
181     /// let description = Description::new()
182     ///     .text("First line\nsecond line")
183     ///     .enumerate();
184     /// verify_that!(description, displays_as(eq("\
185     /// 0. First line
186     ///    second line")))
187     /// # .unwrap();
188     /// ```
enumerate(self) -> Self189     pub fn enumerate(self) -> Self {
190         Self { elements: self.elements.enumerate(), ..self }
191     }
192 
193     /// Returns the length of elements.
len(&self) -> usize194     pub fn len(&self) -> usize {
195         self.elements.len()
196     }
197 
198     /// Returns whether the set of elements is empty.
is_empty(&self) -> bool199     pub fn is_empty(&self) -> bool {
200         self.elements.is_empty()
201     }
202 }
203 
204 impl Display for Description {
fmt(&self, f: &mut Formatter) -> Result205     fn fmt(&self, f: &mut Formatter) -> Result {
206         self.elements.render(f, self.initial_indentation)
207     }
208 }
209 
210 impl<ElementT: Into<Cow<'static, str>>> FromIterator<ElementT> for Description {
from_iter<T>(iter: T) -> Self where T: IntoIterator<Item = ElementT>,211     fn from_iter<T>(iter: T) -> Self
212     where
213         T: IntoIterator<Item = ElementT>,
214     {
215         Self { elements: iter.into_iter().map(ElementT::into).collect(), ..Default::default() }
216     }
217 }
218 
219 impl FromIterator<Description> for Description {
from_iter<T>(iter: T) -> Self where T: IntoIterator<Item = Description>,220     fn from_iter<T>(iter: T) -> Self
221     where
222         T: IntoIterator<Item = Description>,
223     {
224         Self { elements: iter.into_iter().map(|s| s.elements).collect(), ..Default::default() }
225     }
226 }
227 
228 impl<T: Into<Cow<'static, str>>> From<T> for Description {
from(value: T) -> Self229     fn from(value: T) -> Self {
230         let mut elements = List::default();
231         elements.push_literal(value.into());
232         Self { elements, ..Default::default() }
233     }
234 }
235 
236 #[cfg(test)]
237 mod tests {
238     use super::Description;
239     use crate::prelude::*;
240     use indoc::indoc;
241 
242     #[test]
renders_single_fragment() -> Result<()>243     fn renders_single_fragment() -> Result<()> {
244         let description: Description = "A B C".into();
245         verify_that!(description, displays_as(eq("A B C")))
246     }
247 
248     #[test]
renders_two_fragments() -> Result<()>249     fn renders_two_fragments() -> Result<()> {
250         let description =
251             ["A B C".to_string(), "D E F".to_string()].into_iter().collect::<Description>();
252         verify_that!(description, displays_as(eq("A B C\nD E F")))
253     }
254 
255     #[test]
nested_description_is_indented() -> Result<()>256     fn nested_description_is_indented() -> Result<()> {
257         let description = Description::new()
258             .text("Header")
259             .nested(["A B C".to_string()].into_iter().collect::<Description>());
260         verify_that!(description, displays_as(eq("Header\n  A B C")))
261     }
262 
263     #[test]
nested_description_indents_two_elements() -> Result<()>264     fn nested_description_indents_two_elements() -> Result<()> {
265         let description = Description::new().text("Header").nested(
266             ["A B C".to_string(), "D E F".to_string()].into_iter().collect::<Description>(),
267         );
268         verify_that!(description, displays_as(eq("Header\n  A B C\n  D E F")))
269     }
270 
271     #[test]
nested_description_indents_one_element_on_two_lines() -> Result<()>272     fn nested_description_indents_one_element_on_two_lines() -> Result<()> {
273         let description = Description::new().text("Header").nested("A B C\nD E F".into());
274         verify_that!(description, displays_as(eq("Header\n  A B C\n  D E F")))
275     }
276 
277     #[test]
single_fragment_renders_with_bullet_when_bullet_list_enabled() -> Result<()>278     fn single_fragment_renders_with_bullet_when_bullet_list_enabled() -> Result<()> {
279         let description = Description::new().text("A B C").bullet_list();
280         verify_that!(description, displays_as(eq("* A B C")))
281     }
282 
283     #[test]
single_nested_fragment_renders_with_bullet_when_bullet_list_enabled() -> Result<()>284     fn single_nested_fragment_renders_with_bullet_when_bullet_list_enabled() -> Result<()> {
285         let description = Description::new().nested("A B C".into()).bullet_list();
286         verify_that!(description, displays_as(eq("* A B C")))
287     }
288 
289     #[test]
two_fragments_render_with_bullet_when_bullet_list_enabled() -> Result<()>290     fn two_fragments_render_with_bullet_when_bullet_list_enabled() -> Result<()> {
291         let description = Description::new().text("A B C").text("D E F").bullet_list();
292         verify_that!(description, displays_as(eq("* A B C\n* D E F")))
293     }
294 
295     #[test]
two_nested_fragments_render_with_bullet_when_bullet_list_enabled() -> Result<()>296     fn two_nested_fragments_render_with_bullet_when_bullet_list_enabled() -> Result<()> {
297         let description =
298             Description::new().nested("A B C".into()).nested("D E F".into()).bullet_list();
299         verify_that!(description, displays_as(eq("* A B C\n* D E F")))
300     }
301 
302     #[test]
single_fragment_with_more_than_one_line_renders_with_one_bullet() -> Result<()>303     fn single_fragment_with_more_than_one_line_renders_with_one_bullet() -> Result<()> {
304         let description = Description::new().text("A B C\nD E F").bullet_list();
305         verify_that!(description, displays_as(eq("* A B C\n  D E F")))
306     }
307 
308     #[test]
single_fragment_renders_with_enumeration_when_enumerate_enabled() -> Result<()>309     fn single_fragment_renders_with_enumeration_when_enumerate_enabled() -> Result<()> {
310         let description = Description::new().text("A B C").enumerate();
311         verify_that!(description, displays_as(eq("0. A B C")))
312     }
313 
314     #[test]
two_fragments_render_with_enumeration_when_enumerate_enabled() -> Result<()>315     fn two_fragments_render_with_enumeration_when_enumerate_enabled() -> Result<()> {
316         let description = Description::new().text("A B C").text("D E F").enumerate();
317         verify_that!(description, displays_as(eq("0. A B C\n1. D E F")))
318     }
319 
320     #[test]
single_fragment_with_two_lines_renders_with_one_enumeration_label() -> Result<()>321     fn single_fragment_with_two_lines_renders_with_one_enumeration_label() -> Result<()> {
322         let description = Description::new().text("A B C\nD E F").enumerate();
323         verify_that!(description, displays_as(eq("0. A B C\n   D E F")))
324     }
325 
326     #[test]
multi_digit_enumeration_renders_with_correct_offset() -> Result<()>327     fn multi_digit_enumeration_renders_with_correct_offset() -> Result<()> {
328         let description = ["A B C\nD E F"; 11]
329             .into_iter()
330             .map(str::to_string)
331             .collect::<Description>()
332             .enumerate();
333         verify_that!(
334             description,
335             displays_as(eq(indoc!(
336                 "
337                  0. A B C
338                     D E F
339                  1. A B C
340                     D E F
341                  2. A B C
342                     D E F
343                  3. A B C
344                     D E F
345                  4. A B C
346                     D E F
347                  5. A B C
348                     D E F
349                  6. A B C
350                     D E F
351                  7. A B C
352                     D E F
353                  8. A B C
354                     D E F
355                  9. A B C
356                     D E F
357                 10. A B C
358                     D E F"
359             )))
360         )
361     }
362 }
363