Skip to main content

test_that/
description.rs

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