Skip to main content

googletest/
description.rs

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
15use std::{
16    borrow::Cow,
17    fmt::{Display, Formatter, Result},
18};
19
20use 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)]
89pub struct Description {
90    elements: List,
91    initial_indentation: usize,
92    is_conjunction: bool,
93    is_disjunction: bool,
94}
95
96impl Description {
97    /// Returns a new empty [`Description`].
98    pub fn new() -> Self {
99        Default::default()
100    }
101
102    /// Appends a block of text to this instance.
103    ///
104    /// The block is indented uniformly when this instance is rendered.
105    pub fn text(mut self, text: impl Into<Cow<'static, str>>) -> Self {
106        self.elements.push_literal(text.into());
107        self
108    }
109
110    /// Appends a nested [`Description`] to this instance.
111    ///
112    /// The nested [`Description`] `inner` is indented uniformly at the next
113    /// level of indentation when this instance is rendered.
114    pub fn nested(mut self, inner: Description) -> Self {
115        self.elements.push_nested(inner.elements);
116        self
117    }
118
119    /// Appends all [`Description`] in the given sequence `inner` to this
120    /// instance.
121    ///
122    /// Each element is treated as a nested [`Description`] in the sense of
123    /// [`Self::nested`].
124    pub fn collect(self, inner: impl IntoIterator<Item = Description>) -> Self {
125        inner.into_iter().fold(self, |outer, inner| outer.nested(inner))
126    }
127
128    /// Indents the lines in elements of this description.
129    ///
130    /// This operation will be performed lazily when [`self`] is displayed.
131    ///
132    /// This will indent every line inside each element.
133    ///
134    /// For example:
135    ///
136    /// ```
137    /// # use googletest::prelude::*;
138    /// # use googletest::description::Description;
139    /// let description = std::iter::once("A B C\nD E F".to_string()).collect::<Description>();
140    /// verify_that!(description.indent(), displays_as(eq("  A B C\n  D E F")))
141    /// # .unwrap();
142    /// ```
143    pub fn indent(self) -> Self {
144        Self { initial_indentation: INDENTATION_SIZE, ..self }
145    }
146
147    /// Instructs this instance to render its elements as a bullet list.
148    ///
149    /// Each element (from either [`Description::text`] or
150    /// [`Description::nested`]) is rendered as a bullet point. If an element
151    /// contains multiple lines, the following lines are aligned with the first
152    /// one in the block.
153    ///
154    /// For instance:
155    ///
156    /// ```
157    /// # use googletest::prelude::*;
158    /// # use googletest::description::Description;
159    /// let description = Description::new()
160    ///     .text("First line\nsecond line")
161    ///     .bullet_list();
162    /// verify_that!(description, displays_as(eq("\
163    /// * First line
164    ///   second line")))
165    /// # .unwrap();
166    /// ```
167    pub fn bullet_list(self) -> Self {
168        Self { elements: self.elements.bullet_list(), ..self }
169    }
170
171    /// Instructs this instance to render its elements as an enumerated list.
172    ///
173    /// Each element (from either [`Description::text`] or
174    /// [`Description::nested`]) is rendered with its zero-based index. If an
175    /// element contains multiple lines, the following lines are aligned with
176    /// the first one in the block.
177    ///
178    /// For instance:
179    ///
180    /// ```
181    /// # use googletest::prelude::*;
182    /// # use googletest::description::Description;
183    /// let description = Description::new()
184    ///     .text("First line\nsecond line")
185    ///     .enumerate();
186    /// verify_that!(description, displays_as(eq("\
187    /// 0. First line
188    ///    second line")))
189    /// # .unwrap();
190    /// ```
191    pub fn enumerate(self) -> Self {
192        Self { elements: self.elements.enumerate(), ..self }
193    }
194
195    /// Returns the length of elements.
196    pub fn len(&self) -> usize {
197        self.elements.len()
198    }
199
200    /// Returns whether the set of elements is empty.
201    pub fn is_empty(&self) -> bool {
202        self.elements.is_empty()
203    }
204
205    pub(crate) fn push_in_last_nested(mut self, inner: Description) -> Self {
206        self.elements.push_at_end(inner.elements);
207        self
208    }
209
210    pub(crate) fn conjunction_description(self) -> Self {
211        Self { is_conjunction: true, ..self }
212    }
213
214    pub(crate) fn is_conjunction_description(&self) -> bool {
215        self.is_conjunction
216    }
217
218    pub(crate) fn disjunction_description(self) -> Self {
219        Self { is_disjunction: true, ..self }
220    }
221
222    pub(crate) fn is_disjunction_description(&self) -> bool {
223        self.is_disjunction
224    }
225}
226
227impl Display for Description {
228    fn fmt(&self, f: &mut Formatter) -> Result {
229        self.elements.render(f, self.initial_indentation)
230    }
231}
232
233impl<ElementT: Into<Cow<'static, str>>> FromIterator<ElementT> for Description {
234    fn from_iter<T>(iter: T) -> Self
235    where
236        T: IntoIterator<Item = ElementT>,
237    {
238        Self { elements: iter.into_iter().map(ElementT::into).collect(), ..Default::default() }
239    }
240}
241
242impl FromIterator<Description> for Description {
243    fn from_iter<T>(iter: T) -> Self
244    where
245        T: IntoIterator<Item = Description>,
246    {
247        Self { elements: iter.into_iter().map(|s| s.elements).collect(), ..Default::default() }
248    }
249}
250
251impl<T: Into<Cow<'static, str>>> From<T> for Description {
252    fn from(value: T) -> Self {
253        let mut elements = List::default();
254        elements.push_literal(value.into());
255        Self { elements, ..Default::default() }
256    }
257}
258
259#[cfg(test)]
260mod tests {
261    use super::Description;
262    use crate::prelude::*;
263    use crate::Result;
264    use indoc::indoc;
265
266    #[test]
267    fn renders_single_fragment() -> Result<()> {
268        let description: Description = "A B C".into();
269        verify_that!(description, displays_as(eq("A B C")))
270    }
271
272    #[test]
273    fn renders_two_fragments() -> Result<()> {
274        let description =
275            ["A B C".to_string(), "D E F".to_string()].into_iter().collect::<Description>();
276        verify_that!(description, displays_as(eq("A B C\nD E F")))
277    }
278
279    #[test]
280    fn nested_description_is_indented() -> Result<()> {
281        let description = Description::new()
282            .text("Header")
283            .nested(["A B C".to_string()].into_iter().collect::<Description>());
284        verify_that!(description, displays_as(eq("Header\n  A B C")))
285    }
286
287    #[test]
288    fn nested_description_indents_two_elements() -> Result<()> {
289        let description = Description::new().text("Header").nested(
290            ["A B C".to_string(), "D E F".to_string()].into_iter().collect::<Description>(),
291        );
292        verify_that!(description, displays_as(eq("Header\n  A B C\n  D E F")))
293    }
294
295    #[test]
296    fn nested_description_indents_one_element_on_two_lines() -> Result<()> {
297        let description = Description::new().text("Header").nested("A B C\nD E F".into());
298        verify_that!(description, displays_as(eq("Header\n  A B C\n  D E F")))
299    }
300
301    #[test]
302    fn single_fragment_renders_with_bullet_when_bullet_list_enabled() -> Result<()> {
303        let description = Description::new().text("A B C").bullet_list();
304        verify_that!(description, displays_as(eq("* A B C")))
305    }
306
307    #[test]
308    fn single_nested_fragment_renders_with_bullet_when_bullet_list_enabled() -> Result<()> {
309        let description = Description::new().nested("A B C".into()).bullet_list();
310        verify_that!(description, displays_as(eq("* A B C")))
311    }
312
313    #[test]
314    fn two_fragments_render_with_bullet_when_bullet_list_enabled() -> Result<()> {
315        let description = Description::new().text("A B C").text("D E F").bullet_list();
316        verify_that!(description, displays_as(eq("* A B C\n* D E F")))
317    }
318
319    #[test]
320    fn two_nested_fragments_render_with_bullet_when_bullet_list_enabled() -> Result<()> {
321        let description =
322            Description::new().nested("A B C".into()).nested("D E F".into()).bullet_list();
323        verify_that!(description, displays_as(eq("* A B C\n* D E F")))
324    }
325
326    #[test]
327    fn single_fragment_with_more_than_one_line_renders_with_one_bullet() -> Result<()> {
328        let description = Description::new().text("A B C\nD E F").bullet_list();
329        verify_that!(description, displays_as(eq("* A B C\n  D E F")))
330    }
331
332    #[test]
333    fn single_fragment_renders_with_enumeration_when_enumerate_enabled() -> Result<()> {
334        let description = Description::new().text("A B C").enumerate();
335        verify_that!(description, displays_as(eq("0. A B C")))
336    }
337
338    #[test]
339    fn two_fragments_render_with_enumeration_when_enumerate_enabled() -> Result<()> {
340        let description = Description::new().text("A B C").text("D E F").enumerate();
341        verify_that!(description, displays_as(eq("0. A B C\n1. D E F")))
342    }
343
344    #[test]
345    fn single_fragment_with_two_lines_renders_with_one_enumeration_label() -> Result<()> {
346        let description = Description::new().text("A B C\nD E F").enumerate();
347        verify_that!(description, displays_as(eq("0. A B C\n   D E F")))
348    }
349
350    #[test]
351    fn multi_digit_enumeration_renders_with_correct_offset() -> Result<()> {
352        let description = ["A B C\nD E F"; 11]
353            .into_iter()
354            .map(str::to_string)
355            .collect::<Description>()
356            .enumerate();
357        verify_that!(
358            description,
359            displays_as(eq(indoc!(
360                "
361                 0. A B C
362                    D E F
363                 1. A B C
364                    D E F
365                 2. A B C
366                    D E F
367                 3. A B C
368                    D E F
369                 4. A B C
370                    D E F
371                 5. A B C
372                    D E F
373                 6. A B C
374                    D E F
375                 7. A B C
376                    D E F
377                 8. A B C
378                    D E F
379                 9. A B C
380                    D E F
381                10. A B C
382                    D E F"
383            )))
384        )
385    }
386
387    #[test]
388    fn new_is_empty() -> Result<()> {
389        verify_that!(Description::new(), predicate(Description::is_empty))
390    }
391
392    #[test]
393    fn text_is_not_empty() -> Result<()> {
394        verify_that!(Description::new().text("something"), not(predicate(Description::is_empty)))
395    }
396
397    #[test]
398    fn new_zero_length() -> Result<()> {
399        verify_that!(Description::new().len(), eq(0))
400    }
401
402    #[test]
403    fn text_one_length() -> Result<()> {
404        verify_that!(Description::new().text("something").len(), eq(1))
405    }
406}