blob: ce074e0f0b04eedddf69ca3ff7cf4df30e75e04e [file] [log] [blame]
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::fmt::{Display, Formatter, Result};
/// Helper structure to build better output of
/// [`Matcher::describe`][crate::matcher::Matcher::describe] and
/// [`Matcher::explain_match`][crate::matcher::Matcher::explain_match]. This
/// is especially useful with composed matchers and matchers over containers.
///
/// It provides simple operations to lazily format lists of strings.
///
/// Usage:
/// ```ignore
/// let iter: impl Iterator<String> = ...
/// format!("{}", iter.collect::<Description>().indent().bullet_list())
/// ```
///
/// To construct a [`Description`], use `Iterator<Item=String>::collect()`.
/// Each element of the collected iterator will be separated by a
/// newline when displayed. The elements may be multi-line, but they will
/// nevertheless be indented consistently.
///
/// Note that a newline will only be added between each element, but not
/// after the last element. This makes it simpler to keep
/// [`Matcher::describe`][crate::matcher::Matcher::describe]
/// and [`Matcher::explain_match`][crate::matcher::Matcher::explain_match]
/// consistent with simpler [`Matchers`][crate::matcher::Matcher].
///
/// They can also be indented, enumerated and or
/// bullet listed if [`Description::indent`], [`Description::enumerate`], or
/// respectively [`Description::bullet_list`] has been called.
#[derive(Debug)]
pub struct Description {
elements: Vec<String>,
indent_mode: IndentMode,
list_style: ListStyle,
}
#[derive(Debug)]
enum IndentMode {
NoIndent,
EveryLine,
AllExceptFirstLine,
}
#[derive(Debug)]
enum ListStyle {
NoList,
Bullet,
Enumerate,
}
struct IndentationSizes {
first_line_indent: usize,
first_line_of_element_indent: usize,
enumeration_padding: usize,
other_line_indent: usize,
}
/// Number of space used to indent lines when no alignement is required.
const INDENTATION_SIZE: usize = 2;
impl Description {
/// Indents the lines in elements of this description.
///
/// This operation will be performed lazily when [`self`] is displayed.
///
/// This will indent every line inside each element.
///
/// For example:
///
/// ```
/// # use googletest::prelude::*;
/// # use googletest::matcher_support::description::Description;
/// let description = std::iter::once("A B C\nD E F".to_string()).collect::<Description>();
/// verify_that!(description.indent(), displays_as(eq(" A B C\n D E F")))
/// # .unwrap();
/// ```
pub fn indent(self) -> Self {
Self { indent_mode: IndentMode::EveryLine, ..self }
}
/// Indents the lines in elements of this description except for the first
/// line.
///
/// This is similar to [`Self::indent`] except that the first line is not
/// indented. This is useful when the first line has already been indented
/// in the output.
///
/// For example:
///
/// ```
/// # use googletest::prelude::*;
/// # use googletest::matcher_support::description::Description;
/// let description = std::iter::once("A B C\nD E F".to_string()).collect::<Description>();
/// verify_that!(description.indent_except_first_line(), displays_as(eq("A B C\n D E F")))
/// # .unwrap();
/// ```
pub fn indent_except_first_line(self) -> Self {
Self { indent_mode: IndentMode::AllExceptFirstLine, ..self }
}
/// Bullet lists the elements of [`self`].
///
/// This operation will be performed lazily when [`self`] is displayed.
///
/// Note that this will only bullet list each element, not each line
/// in each element.
///
/// For instance:
///
/// ```
/// # use googletest::prelude::*;
/// # use googletest::matcher_support::description::Description;
/// let description = std::iter::once("A B C\nD E F".to_string()).collect::<Description>();
/// verify_that!(description.bullet_list(), displays_as(eq("* A B C\n D E F")))
/// # .unwrap();
/// ```
pub fn bullet_list(self) -> Self {
Self { list_style: ListStyle::Bullet, ..self }
}
/// Enumerates the elements of [`self`].
///
/// This operation will be performed lazily when [`self`] is displayed.
///
/// Note that this will only enumerate each element, not each line in
/// each element.
///
/// For instance:
///
/// ```
/// # use googletest::prelude::*;
/// # use googletest::matcher_support::description::Description;
/// let description = std::iter::once("A B C\nD E F".to_string()).collect::<Description>();
/// verify_that!(description.enumerate(), displays_as(eq("0. A B C\n D E F")))
/// # .unwrap();
/// ```
pub fn enumerate(self) -> Self {
Self { list_style: ListStyle::Enumerate, ..self }
}
/// Returns the length of elements.
pub fn len(&self) -> usize {
self.elements.len()
}
/// Returns whether the set of elements is empty.
pub fn is_empty(&self) -> bool {
self.elements.is_empty()
}
fn indentation_sizes(&self) -> IndentationSizes {
let first_line_indent =
if matches!(self.indent_mode, IndentMode::EveryLine) { INDENTATION_SIZE } else { 0 };
let first_line_of_element_indent =
if !matches!(self.indent_mode, IndentMode::NoIndent) { INDENTATION_SIZE } else { 0 };
// Number of digit of the last index. For instance, an array of length 13 will
// have 12 as last index (we start at 0), which have a digit size of 2.
let enumeration_padding = if self.elements.len() > 1 {
((self.elements.len() - 1) as f64).log10().floor() as usize + 1
} else {
// Avoid negative logarithm when there is only 0 or 1 element.
1
};
let other_line_indent = first_line_of_element_indent
+ match self.list_style {
ListStyle::NoList => 0,
ListStyle::Bullet => "* ".len(),
ListStyle::Enumerate => enumeration_padding + ". ".len(),
};
IndentationSizes {
first_line_indent,
first_line_of_element_indent,
enumeration_padding,
other_line_indent,
}
}
}
impl Display for Description {
fn fmt(&self, f: &mut Formatter) -> Result {
let IndentationSizes {
mut first_line_indent,
first_line_of_element_indent,
enumeration_padding,
other_line_indent,
} = self.indentation_sizes();
let mut first = true;
for (idx, element) in self.elements.iter().enumerate() {
let mut lines = element.lines();
if let Some(line) = lines.next() {
if first {
first = false;
} else {
writeln!(f)?;
}
match self.list_style {
ListStyle::NoList => {
write!(f, "{:first_line_indent$}{line}", "")?;
}
ListStyle::Bullet => {
write!(f, "{:first_line_indent$}* {line}", "")?;
}
ListStyle::Enumerate => {
write!(
f,
"{:first_line_indent$}{:>enumeration_padding$}. {line}",
"", idx,
)?;
}
}
}
for line in lines {
writeln!(f)?;
write!(f, "{:other_line_indent$}{line}", "")?;
}
first_line_indent = first_line_of_element_indent;
}
Ok(())
}
}
impl FromIterator<String> for Description {
fn from_iter<T>(iter: T) -> Self
where
T: IntoIterator<Item = String>,
{
Self {
elements: iter.into_iter().collect(),
indent_mode: IndentMode::NoIndent,
list_style: ListStyle::NoList,
}
}
}
#[cfg(test)]
mod tests {
use super::Description;
use crate::prelude::*;
use indoc::indoc;
#[test]
fn description_single_element() -> Result<()> {
let description = ["A B C".to_string()].into_iter().collect::<Description>();
verify_that!(description, displays_as(eq("A B C")))
}
#[test]
fn description_two_elements() -> Result<()> {
let description =
["A B C".to_string(), "D E F".to_string()].into_iter().collect::<Description>();
verify_that!(description, displays_as(eq("A B C\nD E F")))
}
#[test]
fn description_indent_single_element() -> Result<()> {
let description = ["A B C".to_string()].into_iter().collect::<Description>().indent();
verify_that!(description, displays_as(eq(" A B C")))
}
#[test]
fn description_indent_two_elements() -> Result<()> {
let description = ["A B C".to_string(), "D E F".to_string()]
.into_iter()
.collect::<Description>()
.indent();
verify_that!(description, displays_as(eq(" A B C\n D E F")))
}
#[test]
fn description_indent_two_elements_except_first_line() -> Result<()> {
let description = ["A B C".to_string(), "D E F".to_string()]
.into_iter()
.collect::<Description>()
.indent_except_first_line();
verify_that!(description, displays_as(eq("A B C\n D E F")))
}
#[test]
fn description_indent_single_element_two_lines() -> Result<()> {
let description =
["A B C\nD E F".to_string()].into_iter().collect::<Description>().indent();
verify_that!(description, displays_as(eq(" A B C\n D E F")))
}
#[test]
fn description_indent_single_element_two_lines_except_first_line() -> Result<()> {
let description = ["A B C\nD E F".to_string()]
.into_iter()
.collect::<Description>()
.indent_except_first_line();
verify_that!(description, displays_as(eq("A B C\n D E F")))
}
#[test]
fn description_bullet_single_element() -> Result<()> {
let description = ["A B C".to_string()].into_iter().collect::<Description>().bullet_list();
verify_that!(description, displays_as(eq("* A B C")))
}
#[test]
fn description_bullet_two_elements() -> Result<()> {
let description = ["A B C".to_string(), "D E F".to_string()]
.into_iter()
.collect::<Description>()
.bullet_list();
verify_that!(description, displays_as(eq("* A B C\n* D E F")))
}
#[test]
fn description_bullet_single_element_two_lines() -> Result<()> {
let description =
["A B C\nD E F".to_string()].into_iter().collect::<Description>().bullet_list();
verify_that!(description, displays_as(eq("* A B C\n D E F")))
}
#[test]
fn description_bullet_single_element_two_lines_indent_except_first_line() -> Result<()> {
let description = ["A B C\nD E F".to_string()]
.into_iter()
.collect::<Description>()
.bullet_list()
.indent_except_first_line();
verify_that!(description, displays_as(eq("* A B C\n D E F")))
}
#[test]
fn description_bullet_two_elements_indent_except_first_line() -> Result<()> {
let description = ["A B C".to_string(), "D E F".to_string()]
.into_iter()
.collect::<Description>()
.bullet_list()
.indent_except_first_line();
verify_that!(description, displays_as(eq("* A B C\n * D E F")))
}
#[test]
fn description_enumerate_single_element() -> Result<()> {
let description = ["A B C".to_string()].into_iter().collect::<Description>().enumerate();
verify_that!(description, displays_as(eq("0. A B C")))
}
#[test]
fn description_enumerate_two_elements() -> Result<()> {
let description = ["A B C".to_string(), "D E F".to_string()]
.into_iter()
.collect::<Description>()
.enumerate();
verify_that!(description, displays_as(eq("0. A B C\n1. D E F")))
}
#[test]
fn description_enumerate_single_element_two_lines() -> Result<()> {
let description =
["A B C\nD E F".to_string()].into_iter().collect::<Description>().enumerate();
verify_that!(description, displays_as(eq("0. A B C\n D E F")))
}
#[test]
fn description_enumerate_correct_indentation_with_large_index() -> Result<()> {
let description = ["A B C\nD E F"; 11]
.into_iter()
.map(str::to_string)
.collect::<Description>()
.enumerate();
verify_that!(
description,
displays_as(eq(indoc!(
"
0. A B C
D E F
1. A B C
D E F
2. A B C
D E F
3. A B C
D E F
4. A B C
D E F
5. A B C
D E F
6. A B C
D E F
7. A B C
D E F
8. A B C
D E F
9. A B C
D E F
10. A B C
D E F"
)))
)
}
}