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 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.
112 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`].
122 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 test_that::prelude::*;
136 /// # use test_that::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 /// ```
141 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 test_that::prelude::*;
156 /// # use test_that::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 /// ```
165 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 test_that::prelude::*;
180 /// # use test_that::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 /// ```
189 pub fn enumerate(self) -> Self {
190 Self { elements: self.elements.enumerate(), ..self }
191 }
192
193 /// Returns the length of elements.
194 pub fn len(&self) -> usize {
195 self.elements.len()
196 }
197
198 /// Returns whether the set of elements is empty.
199 pub fn is_empty(&self) -> bool {
200 self.elements.is_empty()
201 }
202}
203
204impl Display for Description {
205 fn fmt(&self, f: &mut Formatter) -> Result {
206 self.elements.render(f, self.initial_indentation)
207 }
208}
209
210impl<ElementT: Into<Cow<'static, str>>> FromIterator<ElementT> for Description {
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
219impl FromIterator<Description> for 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
228impl<T: Into<Cow<'static, str>>> From<T> for Description {
229 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)]
237mod tests {
238 use super::Description;
239 use crate::prelude::*;
240 use alloc::string::ToString;
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}