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