diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e52178..234800d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ +main: + * Automatically choose between pretty or compact `Debug` output, unless overridden. + * Print a diff for failed binary comparisons. + * Allow end-users to change the output of `assert2` with the `ASSERT2` environment variable. + * Support the `NO_COLOR` environment variable in addition to `CLICOLOR`. + v0.3.11 - 2023-05-24: - * Remove use of `source_text()` on stable since current it gives the source text of only one token tree. + * Remove use of `source_text()` on stable since currently it gives the source text of only one token tree. v0.3.10 - 2023-02-14: * Replace unmaintained `atty` dependency with `is-terminal`. diff --git a/Cargo.toml b/Cargo.toml index 64bccc1..4417922 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "assert2" -description = "assert!(...) and check!(...) macros inspired by Catch2" +description = "assert!(...) and check!(...) macros inspired by Catch2, now with diffs!" version = "0.3.11" license = "BSD-2-Clause" authors = [ @@ -21,6 +21,7 @@ categories = ["development-tools::debugging", "development-tools::testing"] assert2-macros = { version = "=0.3.11", path = "assert2-macros" } yansi = "0.5.0" is-terminal = "0.4.3" +diff = "0.1.13" [workspace] resolver = "2" diff --git a/README.md b/README.md index c608838..52b7903 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,25 @@ # assert2 -All-purpose [`assert!(...)`](https://docs.rs/assert2/latest/assert2/macro.assert.html) and [`check!(...)`](https://docs.rs/assert2/latest/assert2/macro.check.html) macros, inspired by [Catch2](https://github.com/catchorg/Catch2). -There is also a [`debug_assert!(...)`](https://docs.rs/assert2/latest/assert2/macro.debug_assert.html) macro that is disabled on optimized builds by default. -As cherry on top there is a [`let_assert!(...)`](https://docs.rs/assert2/latest/assert2/macro.let_assert.html) macro that lets you test a pattern while capturing parts of it. +All-purpose [`assert!(...)`](macro.assert.html) and [`check!(...)`](macro.check.html) macros, inspired by [Catch2](https://github.com/catchorg/Catch2). +There is also a [`debug_assert!(...)`](macro.debug_assert.html) macro that is disabled on optimized builds by default. +As cherry on top there is a [`let_assert!(...)`](macro.let_assert.html) macro that lets you test a pattern while capturing parts of it. ## Why these macros? These macros offer some benefits over the assertions from the standard library: * The macros parse your expression to detect comparisons and adjust the error message accordingly. - No more `assert_eq` or `assert_ne`, just write `assert!(1 + 1 == 2)`, or even `assert!(1 + 1 > 1)`! + No more `assert_eq!(a, b)` or `assert_ne!(c, d)`, just write `assert!(1 + 1 == 2)`, or even `assert!(1 + 1 > 1)`! * You can test for pattern matches: `assert!(let Err(_) = File::open("/non/existing/file"))`. * You can capture parts of the pattern for further testing by using the `let_assert!(...)` macro. * The `check` macro can be used to perform multiple checks before panicking. - * The macros provide more information when the assertion fails. - * Colored failure messages! + * The macros provide more information than the standard `std::assert!()` when the assertion fails. + * Colored failure messages with diffs! The macros also accept additional arguments for a custom message, so it is fully compatible with `std::assert`. -That means you don't have to worry about overwriting the standard `assert` with `use assert2::assert`. +This means that you can import the macro as a drop in replacement: +```rust +use assert2::assert; +``` ## Examples @@ -24,15 +27,23 @@ That means you don't have to worry about overwriting the standard `assert` with check!(6 + 1 <= 2 * 3); ``` -![Assertion error](https://github.com/de-vri-es/assert2-rs/raw/2db44c46e4580ec87d2881a698815e1ec5fcdf3f/binary-operator.png) +![Output](https://raw.githubusercontent.com/de-vri-es/assert2-rs/ba98984a32d6381e6710e34eb1fb83e65e851236/binary-operator.png) ---------- ```rust -check!(true && false); +check!(scrappy == coco); ``` -![Assertion error](https://github.com/de-vri-es/assert2-rs/raw/2db44c46e4580ec87d2881a698815e1ec5fcdf3f/boolean-expression.png) +![Output](https://raw.githubusercontent.com/de-vri-es/assert2-rs/54ee3141e9b23a0d9038697d34f29f25ef7fe810/multiline-diff.png) + +---------- + +```rust +check!((3, Some(4)) == [1, 2, 3].iter().size_hint()); +``` + +![Output](https://raw.githubusercontent.com/de-vri-es/assert2-rs/54ee3141e9b23a0d9038697d34f29f25ef7fe810/single-line-diff.png) ---------- @@ -40,7 +51,7 @@ check!(true && false); check!(let Ok(_) = File::open("/non/existing/file")); ``` -![Assertion error](https://github.com/de-vri-es/assert2-rs/raw/2db44c46e4580ec87d2881a698815e1ec5fcdf3f/pattern-match.png) +![Output](https://raw.githubusercontent.com/de-vri-es/assert2-rs/54ee3141e9b23a0d9038697d34f29f25ef7fe810/pattern-match.png) ---------- @@ -48,7 +59,8 @@ check!(let Ok(_) = File::open("/non/existing/file")); let_assert!(Err(e) = File::open("/non/existing/file")); check!(e.kind() == ErrorKind::PermissionDenied); ``` -![Assertion error](https://github.com/de-vri-es/assert2-rs/raw/573a686d1f19e0513cb235df38d157defdadbec0/let-assert.png) + +![Output](https://github.com/de-vri-es/assert2-rs/blob/54ee3141e9b23a0d9038697d34f29f25ef7fe810/let-assert.png?raw=true) ## `assert` vs `check` The crate provides two macros: `check!(...)` and `assert!(...)`. @@ -68,7 +80,7 @@ On stable and beta, it falls back to stringifying the expression. This makes the output a bit more readable on nightly. ## The `let_assert!()` macro -You can also use the [`let_assert!(...)`](https://docs.rs/assert2/latest/assert2/macro.let_assert.html). +You can also use the [`let_assert!(...)`](macro.let_assert.html). It is very similar to `assert!(let ...)`, but all placeholders will be made available as variables in the calling scope. @@ -85,12 +97,25 @@ check!(e.name() == "bogus name"); check!(e.to_string() == "invalid name: bogus name"); ``` +## Controlling the output format. -## Controlling colored output. +As an end-user, you can influence the way that `assert2` formats failed assertions by changing the `ASSERT2` environment variable. +You can specify any combination of options, separated by a comma. +The supported options are: +* `auto`: Automatically select the compact or pretty `Debug` format for an assertion based on the length (default). +* `pretty`: Always use the pretty `Debug` format for assertion messages (`{:#?}`). +* `compact`: Always use the compact `Debug` format for assertion messages (`{:?}`). +* `no-color`: Disable colored output, even when the output is going to a terminal. +* `color`: Enable colored output, even when the output is not going to a terminal. + +For example, you can run the following command to force the use of the compact `Debug` format with colored output: +```shell +ASSERT2=compact,color cargo test +``` -Colored output can be controlled using environment variables, -as per the [clicolors spec](https://bixense.com/clicolors/): +If neither the `color` or the `no-color` options are set, +then `assert2` follows the [clicolors specification](https://bixense.com/clicolors/): - * `CLICOLOR != 0`: ANSI colors are supported and should be used when the program isn't piped. - * `CLICOLOR == 0`: Don't output ANSI color escape codes. - * `CLICOLOR_FORCE != 0`: ANSI colors should be enabled no matter what. + * `NO_COLOR != 0` or `CLICOLOR == 0`: Write plain output without color codes. + * `CLICOLOR != 0`: Write colored output when the output is going to a terminal. + * `CLICOLOR_FORCE != 0`: Write colored output even when it is not going to a terminal. diff --git a/examples/images.rs b/examples/images.rs index ac5092e..a20a247 100644 --- a/examples/images.rs +++ b/examples/images.rs @@ -12,4 +12,29 @@ fn main() { let_assert!(Err(e) = File::open("/non/existing/file")); check!(e.kind() == ErrorKind::PermissionDenied); + + #[derive(Debug, Eq, PartialEq)] + struct Pet { + name: String, + age: u32, + kind: String, + shaved: bool, + } + + let scrappy = Pet { + name: "Scrappy".into(), + age: 7, + kind: "Bearded Collie".into(), + shaved: false, + }; + + let coco = Pet { + name: "Coco".into(), + age: 7, + kind: "Bearded Collie".into(), + shaved: true, + }; + check!(scrappy == coco); + + check!((3, Some(4)) == [1, 2, 3].iter().size_hint()); } diff --git a/src/__assert2_impl/print/diff.rs b/src/__assert2_impl/print/diff.rs new file mode 100644 index 0000000..4537bac --- /dev/null +++ b/src/__assert2_impl/print/diff.rs @@ -0,0 +1,208 @@ +use std::fmt::Write; +use yansi::Paint; + +/// A line diff between two inputs. +pub struct MultiLineDiff<'a> { + /// The actual diff results from the [`diff`] crate. + line_diffs: Vec>, +} + +impl<'a> MultiLineDiff<'a> { + /// Create a new diff between a left and right input. + pub fn new(left: &'a str, right: &'a str) -> Self { + Self { + line_diffs: diff::lines(left, right), + } + } + + /// Write the left and right input interleaved with eachother, highlighting the differences between the two. + pub fn write_interleaved(&self, buffer: &mut String) { + let mut removed = None; + for diff in &self.line_diffs { + match *diff { + diff::Result::Left(left) => { + if let Some(prev) = removed.take() { + writeln!(buffer, "{}", Paint::cyan(format_args!("< {prev}"))).unwrap(); + } + removed = Some(left); + }, + diff::Result::Right(right) => { + if let Some(left) = removed.take() { + let diff = SingleLineDiff::new(left, right); + write!(buffer, "{} ", diff.left_highlights.normal.paint("<")).unwrap(); + diff.write_left(buffer); + write!(buffer, "\n{} ", diff.right_highlights.normal.paint(">")).unwrap(); + diff.write_right(buffer); + buffer.push('\n'); + } else { + writeln!(buffer, "{}", Paint::yellow(format_args!("> {right}"))).unwrap(); + } + }, + diff::Result::Both(text, _) => { + if let Some(prev) = removed.take() { + writeln!(buffer, "{}", Paint::cyan(format_args!("< {prev}"))).unwrap(); + } + writeln!(buffer, " {}", Paint::default(text).dimmed()).unwrap(); + }, + } + } + // Remove last newline. + buffer.pop(); + } +} + +/// A character/word based diff between two single-line inputs. +pub struct SingleLineDiff<'a> { + /// The left line. + left: &'a str, + + /// The right line. + right: &'a str, + + /// The highlighting for the left line. + left_highlights: Highlighter, + + /// The highlighting for the right line. + right_highlights: Highlighter, +} + +impl<'a> SingleLineDiff<'a> { + /// Create a new word diff between two input lines. + pub fn new(left: &'a str, right: &'a str) -> Self { + let left_words = Self::split_words(left); + let right_words = Self::split_words(right); + let diffs = diff::slice(&left_words, &right_words); + + let mut left_highlights = Highlighter::new(yansi::Color::Cyan); + let mut right_highlights = Highlighter::new(yansi::Color::Yellow); + for diff in &diffs { + match diff { + diff::Result::Left(left) => { + left_highlights.push(left.len(), true); + }, + diff::Result::Right(right) => { + right_highlights.push(right.len(), true); + }, + diff::Result::Both(left, right) => { + left_highlights.push(left.len(), false); + right_highlights.push(right.len(), false); + } + } + } + + Self { + left, + right, + left_highlights, + right_highlights, + } + } + + /// Write the left line with highlighting. + /// + /// This does not write a line break to the buffer. + pub fn write_left(&self, buffer: &mut String) { + self.left_highlights.write_highlighted(buffer, self.left); + } + + /// Write the right line with highlighting. + /// + /// This does not write a line break to the buffer. + pub fn write_right(&self, buffer: &mut String) { + self.right_highlights.write_highlighted(buffer, self.right); + } + + /// Split an input line into individual words. + fn split_words(mut input: &str) -> Vec<&str> { + /// Check if there should be a word break between character `a` and `b`. + fn is_break_point(a: char, b: char) -> bool { + if a.is_alphabetic() { + !b.is_alphabetic() || (a.is_lowercase() && !b.is_lowercase()) + } else if a.is_ascii_digit() { + !b.is_ascii_digit() + } else if a.is_whitespace() { + !b.is_whitespace() + } else { + true + } + } + + let mut output = Vec::new(); + while !input.is_empty() { + let split = input.chars() + .zip(input.char_indices().skip(1)) + .find_map(|(a, (pos, b))| Some(pos).filter(|_| is_break_point(a, b))) + .unwrap_or(input.len()); + let (head, tail) = input.split_at(split); + output.push(head); + input = tail; + } + output + } +} + +/// Highlighter that incrementaly builds a range of alternating styles. +struct Highlighter { + /// The ranges of alternating highlighting. + /// + /// If the boolean is true, the range should be printed with the `highlight` style. + /// If the boolean is false, the range should be printed with the `normal` style. + ranges: Vec<(bool, std::ops::Range)>, + + /// The total length of the highlighted ranges (in bytes, not characters or terminal cells). + total_highlighted: usize, + + /// The style for non-highlighted words. + normal: yansi::Style, + + /// The style for highlighted words. + highlight: yansi::Style, +} + +impl Highlighter { + /// Create a new highlighter with the given color. + fn new(color: yansi::Color) -> Self { + let normal = yansi::Style::new(color); + let highlight = yansi::Style::new(yansi::Color::Black).bg(color).bold(); + Self { + ranges: Vec::new(), + total_highlighted: 0, + normal, + highlight, + } + } + + /// Push a range to the end of the highlighter. + fn push(&mut self, len: usize, highlight: bool) { + if highlight { + self.total_highlighted += len; + } + if let Some(last) = self.ranges.last_mut() { + if last.0 == highlight { + last.1.end += len; + } else { + let start = last.1.end; + self.ranges.push((highlight, start..start + len)); + } + } else { + self.ranges.push((highlight, 0..len)) + } + } + + /// Write the data using the highlight ranges. + fn write_highlighted(&self, buffer: &mut String, data: &str) { + let not_highlighted = data.len() - self.total_highlighted; + if not_highlighted < self.total_highlighted + self.total_highlighted / 2 { + write!(buffer, "{}", self.normal.paint(data)).unwrap(); + } else { + for (highlight, range) in self.ranges.iter().cloned() { + let piece = if highlight { + self.highlight.paint(&data[range]) + } else { + self.normal.paint(&data[range]) + }; + write!(buffer, "{}", piece).unwrap(); + } + } + } +} diff --git a/src/__assert2_impl/print.rs b/src/__assert2_impl/print/mod.rs similarity index 60% rename from src/__assert2_impl/print.rs rename to src/__assert2_impl/print/mod.rs index 0de5be9..bd98965 100644 --- a/src/__assert2_impl/print.rs +++ b/src/__assert2_impl/print/mod.rs @@ -2,24 +2,11 @@ use std::fmt::Debug; use yansi::Paint; use std::fmt::Write; -fn should_color() -> bool { - if std::env::var_os("CLICOLOR").map(|x| x == "0").unwrap_or(false) { - false - } else if std::env::var_os("CLICOLOR_FORCE").map(|x| x != "0").unwrap_or(false) { - true - } else { - use is_terminal::IsTerminal; - std::io::stderr().is_terminal() - } -} +mod diff; +use diff::{MultiLineDiff, SingleLineDiff}; -fn set_color() { - if should_color() { - Paint::enable() - } else { - Paint::disable() - } -} +mod options; +use options::{AssertOptions, ExpansionFormat}; pub struct FailedCheck<'a, T> { pub macro_name: &'a str, @@ -58,7 +45,6 @@ pub struct MatchExpr<'a, Value> { impl<'a, T: CheckExpression> FailedCheck<'a, T> { #[rustfmt::skip] pub fn print(&self) { - set_color(); let mut print_message = String::new(); writeln!(&mut print_message, "{msg} at {file}:{line}:{column}:", msg = Paint::red("Assertion failed").bold(), @@ -83,8 +69,6 @@ impl<'a, T: CheckExpression> FailedCheck<'a, T> { ).unwrap(); } } - writeln!(&mut print_message, "with expansion:").unwrap(); - write!(&mut print_message, " ").unwrap(); self.expression.write_expansion(&mut print_message); writeln!(&mut print_message, ).unwrap(); if let Some(msg) = self.custom_msg { @@ -99,19 +83,44 @@ impl<'a, T: CheckExpression> FailedCheck<'a, T> { #[rustfmt::skip] impl CheckExpression for BinaryOp<'_, Left, Right> { - fn write_expression(&self, buffer: &mut String) { - write!(buffer, "{left} {op} {right}", + fn write_expression(&self, print_message: &mut String) { + write!(print_message, "{left} {op} {right}", left = Paint::cyan(self.left_expr), op = Paint::blue(self.operator).bold(), right = Paint::yellow(self.right_expr), ).unwrap(); } - fn write_expansion(&self, buffer: &mut String) { - write!(buffer, "{left:?} {op} {right:?}", - left = Paint::cyan(self.left), - op = Paint::blue(self.operator).bold(), - right = Paint::yellow(self.right), - ).unwrap(); + + fn write_expansion(&self, print_message: &mut String) { + let style = AssertOptions::get(); + + if !style.expand.force_pretty() { + let left = format!("{:?}", self.left); + let right = format!("{:?}", self.right); + if style.expand.force_compact() || ExpansionFormat::is_compact_good(&[&left, &right]) { + writeln!(print_message, "with expansion:").unwrap(); + let diff = SingleLineDiff::new(&left, &right); + print_message.push_str(" "); + diff.write_left(print_message); + write!(print_message, " {} ", Paint::blue(self.operator)).unwrap(); + diff.write_right(print_message); + if left == right { + if self.operator == "==" { + write!(print_message, "\n{}", Paint::red("Note: Left and right compared as unequal, but the Debug output of left and right is identical!")).unwrap(); + } else { + write!(print_message, "\n{}", Paint::default("Note: Debug output of left and right is identical.").bold()).unwrap(); + } + } + return + } + } + + // Compact expansion was disabled or not compact enough, so go full-on pretty debug format. + let left = format!("{:#?}", self.left); + let right = format!("{:#?}", self.right); + writeln!(print_message, "with diff:").unwrap(); + MultiLineDiff::new(&left, &right) + .write_interleaved(print_message); } } @@ -120,8 +129,10 @@ impl CheckExpression for BooleanExpr<'_> { fn write_expression(&self, print_message: &mut String) { write!(print_message, "{}", Paint::cyan(self.expression)).unwrap(); } + fn write_expansion(&self, print_message: &mut String) { - write!(print_message, "{:?}", Paint::cyan(false)).unwrap(); + writeln!(print_message, "with expansion:").unwrap(); + write!(print_message, " {:?}", Paint::cyan(false)).unwrap(); } } @@ -137,7 +148,15 @@ impl CheckExpression for MatchExpr<'_, Value> { expr = Paint::yellow(self.expression), ).unwrap(); } + fn write_expansion(&self, print_message: &mut String) { - write!(print_message, "{:?}", Paint::yellow(self.value)).unwrap(); + writeln!(print_message, "with expansion:").unwrap(); + let [value] = AssertOptions::get().expand.expand_all([&self.value]); + let message = Paint::yellow(value).to_string(); + for line in message.lines() { + writeln!(print_message, " {line}").unwrap(); + } + // Remove last newline. + print_message.pop(); } } diff --git a/src/__assert2_impl/print/options.rs b/src/__assert2_impl/print/options.rs new file mode 100644 index 0000000..0666a4b --- /dev/null +++ b/src/__assert2_impl/print/options.rs @@ -0,0 +1,172 @@ +/// End-user configurable options for `assert2`. +#[derive(Copy, Clone)] +pub struct AssertOptions { + /// The expansion format for variables. + pub expand: ExpansionFormat, + + /// If true, use colors in the output. + pub color: bool, +} + +impl AssertOptions { + /// Get the global options for `assert2`. + /// + /// The default format is `ExpansionFormat::Auto`. + /// This can be overridden by adding the `pretty` or `compact` option to the `ASSERT2` environment variable. + /// + /// By default, colored output is enabled if `stderr` is conntected to a terminal. + /// If the `CLICOLOR` environment variable is set to `0`, colored output is disabled by default. + /// If the `CLICOLOR_FORCE` environment variable is set to something other than `0`, + /// color is enabled by default, even if `stderr` is not connected to a terminal. + /// The `color` and `no-color` options in the `ASSERT2` environment variable unconditionally enable and disable colored output. + /// + /// Multiple options can be combined in the `ASSERT2` environment variable by separating them with a comma. + /// Whitespace around the comma is ignored. + /// For example: `ASSERT2=color,pretty` to force colored output and the pretty debug format. + /// + pub fn get() -> AssertOptions { + use std::sync::RwLock; + + static STYLE: RwLock> = RwLock::new(None); + loop { + // If it's already initialized, just return it. + if let Some(style) = *STYLE.read().unwrap() { + return style; + } + + // Style wasn't set yet, so try to get a write lock to initialize the style. + match STYLE.try_write() { + // If we fail to get a write lock, another thread is already initializing the style, + // so we just loop back to the start of the function and try the read lock again. + Err(_) => continue, + + // If we get the write lock it is up to use to initialize the style. + Ok(mut style) => { + let style = style.get_or_insert_with(AssertOptions::from_env); + if style.color { + yansi::Paint::enable() + } else { + yansi::Paint::disable() + } + return *style; + }, + } + } + } + + /// Parse the options from the `ASSERT2` environment variable. + fn from_env() -> Self { + // If there is no valid `ASSERT2` environment variable, default to an empty string. + let format = std::env::var_os("ASSERT2"); + let format = format.as_ref() + .and_then(|x| x.to_str()) + .unwrap_or(""); + + // Start with the defaults. + let mut output = Self { + expand: ExpansionFormat::Auto, + color: should_color(), + }; + + // And modify them based on the options in the environment variables. + for word in format.split(',') { + let word = word.trim(); + if word.eq_ignore_ascii_case("pretty") { + output.expand = ExpansionFormat::Pretty; + } else if word.eq_ignore_ascii_case("compact") { + output.expand = ExpansionFormat::Compact; + } else if word.eq_ignore_ascii_case("color") { + output.color = true; + } else if word.eq_ignore_ascii_case("no-color") { + output.color = false; + } + } + + output + } +} + +/// The expansion format for `assert2`. +#[derive(Copy, Clone, Eq, PartialEq)] +pub enum ExpansionFormat { + /// Automatically choose compact or pretty output depending on the values. + /// + /// If the compact debug format for all involved variables is short enough, the compact format is used. + /// Otherwise, all variables are expanded using the pretty debug format. + Auto, + + /// Expand variables using the pretty debug format (as with `format!("{:#?}", ..."`). + Pretty, + + /// Expand variables using the compact debug format (as with `format!("{:?}", ..."`). + Compact, +} + +impl ExpansionFormat { + /// Check if the format forces the pretty debug format. + pub fn force_pretty(self) -> bool { + self == Self::Pretty + } + + /// Check if the format forces the compact debug format. + pub fn force_compact(self) -> bool { + self == Self::Compact + } + + /// Expand all items according to the style. + pub fn expand_all(self, values: [&dyn std::fmt::Debug; N]) -> [String; N] { + if !self.force_pretty() { + let expanded = values.map(|x| format!("{x:?}")); + if self.force_compact() || Self::is_compact_good(&expanded) { + return expanded + } + } + values.map(|x| format!("{x:#?}")) + } + + /// Heuristicly determine if a compact debug representation is good for all expanded items. + pub fn is_compact_good(expanded: &[impl AsRef]) -> bool { + for value in expanded { + if value.as_ref().len() > 40 { + return false; + } + } + for value in expanded { + if value.as_ref().contains('\n') { + return false; + } + } + true + } + +} + +/// Check if the clicolors spec thinks we should use colors. +fn should_color() -> bool { + use std::ffi::OsStr; + + /// Check if an environment variable has a false-like value. + /// + /// Returns `false` if the variable is empty. + fn is_false(value: impl AsRef) -> bool { + let value = value.as_ref(); + value == "0" || value.eq_ignore_ascii_case("false") || value.eq_ignore_ascii_case("no") + } + + fn is_true(value: impl AsRef) -> bool { + let value = value.as_ref(); + value == "1" || value.eq_ignore_ascii_case("true") || value.eq_ignore_ascii_case("yes") + } + + #[allow(clippy::if_same_then_else)] // shut up clippy + if std::env::var_os("NO_COLOR").is_some_and(is_true) { + false + } else if std::env::var_os("CLICOLOR").is_some_and(is_false) { + false + } else if std::env::var_os("CLICOLOR_FORCE").is_some_and(is_true) { + true + } else { + use is_terminal::IsTerminal; + std::io::stderr().is_terminal() + } +} diff --git a/src/lib.rs b/src/lib.rs index c657aa9..9a733e5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,15 +9,18 @@ //! //! These macros offer some benefits over the assertions from the standard library: //! * The macros parse your expression to detect comparisons and adjust the error message accordingly. -//! No more `assert_eq` or `assert_ne`, just write `assert!(1 + 1 == 2)`, or even `assert!(1 + 1 > 1)`! +//! No more `assert_eq!(a, b)` or `assert_ne!(c, d)`, just write `assert!(1 + 1 == 2)`, or even `assert!(1 + 1 > 1)`! //! * You can test for pattern matches: `assert!(let Err(_) = File::open("/non/existing/file"))`. //! * You can capture parts of the pattern for further testing by using the `let_assert!(...)` macro. //! * The `check` macro can be used to perform multiple checks before panicking. -//! * The macros provide more information when the assertion fails. -//! * Colored failure messages! +//! * The macros provide more information than the standard `std::assert!()` when the assertion fails. +//! * Colored failure messages with diffs! //! //! The macros also accept additional arguments for a custom message, so it is fully compatible with `std::assert`. -//! That means you don't have to worry about overwriting the standard `assert` with `use assert2::assert`. +//! This means that you can import the macro as a drop in replacement: +//! ``` +//! use assert2::assert; +//! ``` //! //! # Examples //! @@ -26,16 +29,47 @@ //! check!(6 + 1 <= 2 * 3); //! ``` //! -//! ![Assertion error](https://github.com/de-vri-es/assert2-rs/raw/2db44c46e4580ec87d2881a698815e1ec5fcdf3f/binary-operator.png) +//! ![Output](https://raw.githubusercontent.com/de-vri-es/assert2-rs/ba98984a32d6381e6710e34eb1fb83e65e851236/binary-operator.png) +//! +//! ---------- +//! +//! ```should_panic +//! # use assert2::check; +//! # use assert2::let_assert; +//! # use std::fs::File; +//! # use std::io::ErrorKind; +//! # #[derive(Debug, Eq, PartialEq)] +//! # struct Pet { +//! # name: String, +//! # age: u32, +//! # kind: String, +//! # shaved: bool, +//! # } +//! # let scrappy = Pet { +//! # name: "Scrappy".into(), +//! # age: 7, +//! # kind: "Bearded Collie".into(), +//! # shaved: false, +//! # }; +//! # let coco = Pet { +//! # name: "Coco".into(), +//! # age: 7, +//! # kind: "Bearded Collie".into(), +//! # shaved: true, +//! # }; +//! check!(scrappy == coco); +//! ``` +//! +//! ![Output](https://raw.githubusercontent.com/de-vri-es/assert2-rs/54ee3141e9b23a0d9038697d34f29f25ef7fe810/multiline-diff.png) //! //! ---------- //! //! ```should_panic //! # use assert2::check; -//! check!(true && false); +//! check!((3, Some(4)) == [1, 2, 3].iter().size_hint()); //! ``` //! -//! ![Assertion error](https://github.com/de-vri-es/assert2-rs/raw/2db44c46e4580ec87d2881a698815e1ec5fcdf3f/boolean-expression.png) +//! ![Output](https://raw.githubusercontent.com/de-vri-es/assert2-rs/54ee3141e9b23a0d9038697d34f29f25ef7fe810/single-line-diff.png) //! //! ---------- //! @@ -45,7 +79,7 @@ //! check!(let Ok(_) = File::open("/non/existing/file")); //! ``` //! -//! ![Assertion error](https://github.com/de-vri-es/assert2-rs/raw/2db44c46e4580ec87d2881a698815e1ec5fcdf3f/pattern-match.png) +//! ![Output](https://raw.githubusercontent.com/de-vri-es/assert2-rs/54ee3141e9b23a0d9038697d34f29f25ef7fe810/pattern-match.png) //! //! ---------- //! @@ -57,7 +91,8 @@ //! let_assert!(Err(e) = File::open("/non/existing/file")); //! check!(e.kind() == ErrorKind::PermissionDenied); //! ``` -//! ![Assertion error](https://github.com/de-vri-es/assert2-rs/raw/573a686d1f19e0513cb235df38d157defdadbec0/let-assert.png) +//! +//! ![Output](https://github.com/de-vri-es/assert2-rs/blob/54ee3141e9b23a0d9038697d34f29f25ef7fe810/let-assert.png?raw=true) //! //! # `assert` vs `check` //! The crate provides two macros: `check!(...)` and `assert!(...)`. @@ -127,15 +162,28 @@ //! # } //! ``` //! +//! # Controlling the output format. //! -//! # Controlling colored output. +//! As an end-user, you can influence the way that `assert2` formats failed assertions by changing the `ASSERT2` environment variable. +//! You can specify any combination of options, separated by a comma. +//! The supported options are: +//! * `auto`: Automatically select the compact or pretty `Debug` format for an assertion based on the length (default). +//! * `pretty`: Always use the pretty `Debug` format for assertion messages (`{:#?}`). +//! * `compact`: Always use the compact `Debug` format for assertion messages (`{:?}`). +//! * `no-color`: Disable colored output, even when the output is going to a terminal. +//! * `color`: Enable colored output, even when the output is not going to a terminal. +//! +//! For example, you can run the following command to force the use of the compact `Debug` format with colored output: +//! ```shell +//! ASSERT2=compact,color cargo test +//! ``` //! -//! Colored output can be controlled using environment variables, -//! as per the [clicolors spec](https://bixense.com/clicolors/): +//! If neither the `color` or the `no-color` options are set, +//! then `assert2` follows the [clicolors specification](https://bixense.com/clicolors/): //! -//! * `CLICOLOR != 0`: ANSI colors are supported and should be used when the program isn't piped. -//! * `CLICOLOR == 0`: Don't output ANSI color escape codes. -//! * `CLICOLOR_FORCE != 0`: ANSI colors should be enabled no matter what. +//! * `NO_COLOR != 0` or `CLICOLOR == 0`: Write plain output without color codes. +//! * `CLICOLOR != 0`: Write colored output when the output is going to a terminal. +//! * `CLICOLOR_FORCE != 0`: Write colored output even when it is not going to a terminal. #[doc(hidden)] pub mod __assert2_impl;