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