From ab6d7263162b4b6798130e3c1b208881db0d0481 Mon Sep 17 00:00:00 2001 From: Tom Milligan Date: Sun, 2 May 2021 21:17:31 +0100 Subject: [PATCH] lib: add helpers for generating custom assert_{eq,ne} macros --- Cargo.toml | 2 + pretty_assertions/Cargo.toml | 1 + pretty_assertions/src/lib.rs | 190 +++++------- pretty_assertions_bench/Cargo.toml | 1 + pretty_assertions_derive/Cargo.toml | 12 + pretty_assertions_derive/src/lib.rs | 284 ++++++++++++++++++ pretty_assertions_derive_tests/Cargo.toml | 8 + .../examples/assert_emoji.rs | 17 ++ pretty_assertions_derive_tests/src/lib.rs | 49 +++ .../tests/assert_eq.rs | 18 ++ scripts/check | 1 + 11 files changed, 475 insertions(+), 108 deletions(-) create mode 100644 pretty_assertions_derive/Cargo.toml create mode 100644 pretty_assertions_derive/src/lib.rs create mode 100644 pretty_assertions_derive_tests/Cargo.toml create mode 100644 pretty_assertions_derive_tests/examples/assert_emoji.rs create mode 100644 pretty_assertions_derive_tests/src/lib.rs create mode 100644 pretty_assertions_derive_tests/tests/assert_eq.rs diff --git a/Cargo.toml b/Cargo.toml index 35e74d4..a9efb28 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,4 +3,6 @@ members = [ "pretty_assertions", "pretty_assertions_bench", + "pretty_assertions_derive", + "pretty_assertions_derive_tests", ] diff --git a/pretty_assertions/Cargo.toml b/pretty_assertions/Cargo.toml index 90b343e..ab4bd4a 100644 --- a/pretty_assertions/Cargo.toml +++ b/pretty_assertions/Cargo.toml @@ -23,6 +23,7 @@ travis-ci = { repository = "colin-kiegel/rust-pretty-assertions" } [dependencies] ansi_term = "0.12.1" diff = "0.1.12" +pretty_assertions_derive = { version = "0.1.0", path = "../pretty_assertions_derive" } [target.'cfg(windows)'.dependencies] output_vt100 = "0.1.2" diff --git a/pretty_assertions/src/lib.rs b/pretty_assertions/src/lib.rs index caf7be9..ad2af3a 100644 --- a/pretty_assertions/src/lib.rs +++ b/pretty_assertions/src/lib.rs @@ -126,115 +126,89 @@ where } } -/// Asserts that two expressions are equal to each other (using [`PartialEq`]). -/// -/// On panic, this macro will print a diff derived from [`Debug`] representation of -/// each value. -/// -/// This is a drop in replacement for [`std::assert_eq!`]. -/// You can provide a custom panic message if desired. -/// -/// # Examples -/// -/// ``` -/// use pretty_assertions::assert_eq; -/// -/// let a = 3; -/// let b = 1 + 2; -/// assert_eq!(a, b); -/// -/// assert_eq!(a, b, "we are testing addition with {} and {}", a, b); -/// ``` -#[macro_export] -macro_rules! assert_eq { - ($left:expr, $right:expr$(,)?) => ({ - $crate::assert_eq!(@ $left, $right, "", ""); - }); - ($left:expr, $right:expr, $($arg:tt)*) => ({ - $crate::assert_eq!(@ $left, $right, ": ", $($arg)+); - }); - (@ $left:expr, $right:expr, $maybe_semicolon:expr, $($arg:tt)*) => ({ - match (&($left), &($right)) { - (left_val, right_val) => { - if !(*left_val == *right_val) { - ::std::panic!("assertion failed: `(left == right)`{}{}\ - \n\ - \n{}\ - \n", - $maybe_semicolon, - format_args!($($arg)*), - $crate::Comparison::new(left_val, right_val) - ) - } - } - } - }); +pretty_assertions_derive::derive_assert_eq! { + /// Asserts that two expressions are equal to each other (using [`PartialEq`]). + /// + /// On panic, this macro will print a diff derived from [`Debug`] representation of + /// each value. + /// + /// This is a drop in replacement for [`std::assert_eq!`]. + /// You can provide a custom panic message if desired. + /// + /// # Examples + /// + /// ``` + /// use pretty_assertions::assert_eq; + /// + /// let a = 3; + /// let b = 1 + 2; + /// assert_eq!(a, b); + /// + /// assert_eq!(a, b, "we are testing addition with {} and {}", a, b); + /// ``` + (assert_eq, |left_val, right_val, has_message, message| { + ::std::panic!("assertion failed: `(left == right)`{}{}\ + \n\ + \n{}\ + \n", + if has_message { ": " } else { "" }, + message, + $crate::Comparison::new(left_val, right_val) + ) + }) } -/// Asserts that two expressions are not equal to each other (using [`PartialEq`]). -/// -/// On panic, this macro will print the values of the expressions with their -/// [`Debug`] representations. -/// -/// This is a drop in replacement for [`std::assert_ne!`]. -/// You can provide a custom panic message if desired. -/// -/// # Examples -/// -/// ``` -/// use pretty_assertions::assert_ne; -/// -/// let a = 3; -/// let b = 2; -/// assert_ne!(a, b); -/// -/// assert_ne!(a, b, "we are testing that the values are not equal"); -/// ``` -#[macro_export] -macro_rules! assert_ne { - ($left:expr, $right:expr$(,)?) => ({ - $crate::assert_ne!(@ $left, $right, "", ""); - }); - ($left:expr, $right:expr, $($arg:tt)+) => ({ - $crate::assert_ne!(@ $left, $right, ": ", $($arg)+); - }); - (@ $left:expr, $right:expr, $maybe_semicolon:expr, $($arg:tt)+) => ({ - match (&($left), &($right)) { - (left_val, right_val) => { - if *left_val == *right_val { - let left_dbg = ::std::format!("{:?}", &*left_val); - let right_dbg = ::std::format!("{:?}", &*right_val); - if left_dbg != right_dbg { - ::std::panic!("assertion failed: `(left != right)`{}{}\ - \n\ - \n{}\ - \n{}: According to the `PartialEq` implementation, both of the values \ - are partially equivalent, even if the `Debug` outputs differ.\ - \n\ - \n", - $maybe_semicolon, - format_args!($($arg)+), - $crate::Comparison::new(left_val, right_val), - $crate::Style::new() - .bold() - .underline() - .paint("Note") - ) - } - - ::std::panic!("assertion failed: `(left != right)`{}{}\ - \n\ - \n{}:\ - \n{:#?}\ - \n\ - \n", - $maybe_semicolon, - format_args!($($arg)+), - $crate::Style::new().bold().paint("Both sides"), - left_val - ) - } - } +pretty_assertions_derive::derive_assert_ne! { + /// Asserts that two expressions are not equal to each other (using [`PartialEq`]). + /// + /// On panic, this macro will print the values of the expressions with their + /// [`Debug`] representations. + /// + /// This is a drop in replacement for [`std::assert_ne!`]. + /// You can provide a custom panic message if desired. + /// + /// # Examples + /// + /// ``` + /// use pretty_assertions::assert_ne; + /// + /// let a = 3; + /// let b = 2; + /// assert_ne!(a, b); + /// + /// assert_ne!(a, b, "we are testing that the values are not equal"); + /// ``` + (assert_ne, |left_val, right_val, has_message, message| { + let left_dbg = ::std::format!("{:?}", &*left_val); + let right_dbg = ::std::format!("{:?}", &*right_val); + if left_dbg != right_dbg { + ::std::panic!("assertion failed: `(left != right)`{}{}\ + \n\ + \n{}\ + \n{}: According to the `PartialEq` implementation, both of the values \ + are partially equivalent, even if the `Debug` outputs differ.\ + \n\ + \n", + if has_message { ": " } else { "" }, + message, + $crate::Comparison::new(left_val, right_val), + $crate::Style::new() + .bold() + .underline() + .paint("Note") + ) } - }); + + ::std::panic!("assertion failed: `(left != right)`{}{}\ + \n\ + \n{}:\ + \n{:#?}\ + \n\ + \n", + if has_message { ": " } else { "" }, + message, + $crate::Style::new().bold().paint("Both sides"), + left_val + ) + }) } diff --git a/pretty_assertions_bench/Cargo.toml b/pretty_assertions_bench/Cargo.toml index a44f32b..a4edc9c 100644 --- a/pretty_assertions_bench/Cargo.toml +++ b/pretty_assertions_bench/Cargo.toml @@ -3,6 +3,7 @@ name = "pretty_assertions_bench" version = "0.1.0" authors = ["Tom Milligan "] edition = "2018" +publish = false [dependencies] criterion = { version = "0.3.4", features = ["html_reports"] } diff --git a/pretty_assertions_derive/Cargo.toml b/pretty_assertions_derive/Cargo.toml new file mode 100644 index 0000000..4c3c15c --- /dev/null +++ b/pretty_assertions_derive/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "pretty_assertions_derive" +version = "0.1.0" +edition = "2018" + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1.0.26" +quote = "1.0.9" +syn = { version = "1.0.71", features = ["full"] } diff --git a/pretty_assertions_derive/src/lib.rs b/pretty_assertions_derive/src/lib.rs new file mode 100644 index 0000000..dd12f1c --- /dev/null +++ b/pretty_assertions_derive/src/lib.rs @@ -0,0 +1,284 @@ +//! # pretty_assertions_derive +//! +//! Used to produce custom `assert_eq` and `assert_ne` drop-in implementations, without +//! the boilerplate. +//! +//! For further documentation, see [`derive_assert_eq!`] and [`derive_assert_ne!`]. + +#![deny(clippy::all, unsafe_code)] + +extern crate proc_macro; + +use proc_macro::TokenStream; +use quote::quote; + +struct ParsedItem { + attrs: Vec, + body: Box, + name: syn::PathSegment, + ident_left: syn::Ident, + ident_right: syn::Ident, + ident_has_message: syn::Ident, + ident_message: syn::Ident, +} + +impl From for ParsedItem { + fn from(item: syn::ExprTuple) -> Self { + let syn::ExprTuple { attrs, elems, .. } = item; + let mut arguments = elems.into_iter(); + let mut name_segments = match arguments + .next() + .expect("input must be a tuple of two elements") + { + syn::Expr::Path(expr_path) => expr_path.path.segments.into_iter(), + _ => panic!("first element must be an ident"), + }; + let name = name_segments + .next() + .expect("first element must be an ident"); + if name_segments.next().is_some() { + panic!("first element must be an ident"); + } + let closure = match arguments + .next() + .expect("input must be a tuple of two elements") + { + syn::Expr::Closure(closure) => closure, + _ => panic!("second element must be a closure"), + }; + + let syn::ExprClosure { body, inputs, .. } = closure; + let mut closure_idents = inputs + .into_iter() + .enumerate() + .map(|(index, input)| match input { + syn::Pat::Ident(pat_ident) => pat_ident.ident, + _ => panic!("closure input at position '{}' was not an ident", index), + }); + let ident_left = closure_idents + .next() + .expect("source closure must have four arguments"); + let ident_right = closure_idents + .next() + .expect("source closure must have four arguments"); + let ident_has_message = closure_idents + .next() + .expect("source closure must have four arguments"); + let ident_message = closure_idents + .next() + .expect("source closure must have four arguments"); + + Self { + attrs, + body, + name, + ident_left, + ident_right, + ident_has_message, + ident_message, + } + } +} + +/// Derive a drop in replacement for `assert_eq`. When the equality check fails, +/// the given closure will be called. +/// +/// ## Limitations +/// +/// Derived macro definitions cannot be called within the same crate. You muct define your custom +/// macros in an external crate, and then import them before use in your tests. +/// +/// If you call a derived macro within the same crate, you will see a compiler error such as: +/// +/// ```log +/// error: macro-expanded `macro_export` macros from the current crate cannot be referred to by absolute paths +/// --> src/lib.rs:107:1 +/// | +/// 4 | / pretty_assertions_derive::derive_assert_eq! { +/// 5 | | (assert_eq_custom, |left, right, has_message, message| { +/// ... | // your custom macro definition here +/// 20 | | } +/// 19 | | } +/// | |_^ +/// 20 | +/// 21 | assert_eq_custom!(3, 2); +/// | ------------------------------ in this macro invocation +/// | +/// = note: `#[deny(macro_expanded_macro_exports_accessed_by_absolute_paths)]` on by default +/// = warning: this was previously accepted by the compiler but is being phased out; it will become a hard error in a future release! +/// = note: for more information, see issue #52234 +/// ``` +/// +/// ## Arguments +/// +/// [`derive_assert_eq!`] accepts a single tuple with two items. Any annotations present on the tuple +/// (such as docstrings) will be transferred to the generated macro definiton. +/// +/// ### Name +/// +/// The first argument must be the name of the macro to produce. +/// +/// ### Closure +/// +/// The closure will be called when the equality check of the two values fails, and must accept +/// exactly four arguments itself. These are: +/// +/// - `left: &T`, a reference to the left value +/// - `right: &U`, a reference to the right value +/// - `has_message: bool`, `true` if a custom panic message was given in the invocation +/// - `message: String`, the value of the custom message, or an empty `String` +/// +/// ## Examples +/// +/// Derive a macro named `assert_eq_emoji`, which adds emoji flair around each value. +/// +/// ```rust +/// pretty_assertions_derive::derive_assert_eq! { +/// /// Emojified `assert_eq`. +/// (assert_eq_emoji, |left, right, has_message, message| { +/// ::std::panic!("šŸ˜± Values should be equal! šŸ˜±{}{}\ +/// \n\ +/// \nšŸŽÆ {} šŸŽÆ\ +/// \n\ +/// \nšŸ’„ {} šŸ’„\ +/// \n", +/// if has_message { "\n-> with custom message: " } else { "" }, +/// message, +/// left, +/// right, +/// ) +/// }) +/// } +/// ``` +/// +/// A sample failure: +/// +/// ```log +/// thread 'main' panicked at 'šŸ˜± Values should be equal! šŸ˜± +/// +/// šŸŽÆ 3 šŸŽÆ +/// +/// šŸ’„ 2 šŸ’„ +/// ', pretty_assertions_derive_tests/examples/assert_emoji.rs:6:46 +/// note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace +/// ``` +#[proc_macro] +pub fn derive_assert_eq(item: TokenStream) -> TokenStream { + let item_tuple = syn::parse_macro_input!(item as syn::ExprTuple); + let ParsedItem { + attrs, + body, + name, + ident_left, + ident_right, + ident_has_message, + ident_message, + } = ParsedItem::from(item_tuple); + + let result = quote! { + #(#attrs)* + #[macro_export] + macro_rules! #name { + ($left:expr, $right:expr $(,)?) => ({ + $crate::#name!(@ $left, $right, false, ""); + }); + ($left:expr, $right:expr, $($arg:tt)*) => ({ + $crate::#name!(@ $left, $right, true, $($arg)+); + }); + (@ $left:expr, $right:expr, $has_additional_args:expr, $($arg:tt)*) => ({ + match (&($left), &($right)) { + (__pretty_assertions_derive_left_val, __pretty_assertions_derive_right_val) => { + if !(*__pretty_assertions_derive_left_val == *__pretty_assertions_derive_right_val) { + let #ident_left = __pretty_assertions_derive_left_val; + let #ident_right = __pretty_assertions_derive_right_val; + let #ident_has_message = $has_additional_args; + let #ident_message = ::std::format!($($arg)*); + #body + } + } + } + }); + } + }; + result.into() +} + +/// Derive a drop in replacement for `assert_ne`. When the equality check passes, +/// the given closure will be called. +/// +/// See [`derive_assert_eq!`] for further details, usage is identical. +/// +/// ## Examples +/// +/// Derive a macro named `assert_ne_emoji`, which adds emoji flair around each value. +/// +/// ```rust +/// pretty_assertions_derive::derive_assert_ne! { +/// /// Emojified `assert_ne`. +/// (assert_ne_emoji, |left, right, has_message, message| { +/// ::std::panic!("šŸ˜± Values should not be equal! šŸ˜±{}{}\ +/// \n\ +/// \nšŸ”„ {} šŸ”„\ +/// \n\ +/// \nšŸ”„ {} šŸ”„\ +/// \n", +/// if has_message { "\n-> with custom message: " } else { "" }, +/// message, +/// left, +/// right, +/// ) +/// }) +/// } +/// ``` +/// +/// A sample failure: +/// +/// ```log +/// thread 'main' panicked at 'šŸ˜± Values should not be equal! šŸ˜± +/// -> with custom message: additional details +/// +/// šŸ”„ 3 šŸ”„ +/// +/// šŸ”„ 3 šŸ”„ +/// ', pretty_assertions_derive_tests/examples/assert_emoji.rs:12:46 +/// ``` +#[proc_macro] +pub fn derive_assert_ne(item: TokenStream) -> TokenStream { + let item_tuple = syn::parse_macro_input!(item as syn::ExprTuple); + let ParsedItem { + attrs, + body, + name, + ident_left, + ident_right, + ident_has_message, + ident_message, + } = ParsedItem::from(item_tuple); + + let result = quote! { + #(#attrs)* + #[macro_export] + macro_rules! #name { + ($left:expr, $right:expr $(,)?) => ({ + $crate::#name!(@ $left, $right, false, ""); + }); + ($left:expr, $right:expr, $($arg:tt)*) => ({ + $crate::#name!(@ $left, $right, true, $($arg)+); + }); + (@ $left:expr, $right:expr, $has_additional_args:expr, $($arg:tt)*) => ({ + match (&($left), &($right)) { + (__pretty_assertions_derive_left_val, __pretty_assertions_derive_right_val) => { + if *__pretty_assertions_derive_left_val == *__pretty_assertions_derive_right_val { + let #ident_left = __pretty_assertions_derive_left_val; + let #ident_right = __pretty_assertions_derive_right_val; + let #ident_has_message = $has_additional_args; + let #ident_message = ::std::format!($($arg)*); + #body + } + } + } + }); + } + }; + result.into() +} diff --git a/pretty_assertions_derive_tests/Cargo.toml b/pretty_assertions_derive_tests/Cargo.toml new file mode 100644 index 0000000..73e6217 --- /dev/null +++ b/pretty_assertions_derive_tests/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "pretty_assertions_derive_tests" +version = "0.1.0" +edition = "2018" +publish = false + +[dependencies] +pretty_assertions_derive = { path = "../pretty_assertions_derive" } diff --git a/pretty_assertions_derive_tests/examples/assert_emoji.rs b/pretty_assertions_derive_tests/examples/assert_emoji.rs new file mode 100644 index 0000000..1f60c0d --- /dev/null +++ b/pretty_assertions_derive_tests/examples/assert_emoji.rs @@ -0,0 +1,17 @@ +#![allow(clippy::eq_op)] + +use pretty_assertions_derive_tests::{assert_eq_emoji, assert_ne_emoji}; + +fn main() { + println!("Deliberate `assert_eq` panic:"); + println!("---"); + let result = std::panic::catch_unwind(|| assert_eq_emoji!(3, 2)); + assert!(result.is_err(), "example did not panic"); + println!(); + + println!("Deliberate `assert_ne` panic:"); + println!("---"); + let result = std::panic::catch_unwind(|| assert_ne_emoji!(3, 3, "additional {}", "details")); + assert!(result.is_err(), "example did not panic"); + println!(); +} diff --git a/pretty_assertions_derive_tests/src/lib.rs b/pretty_assertions_derive_tests/src/lib.rs new file mode 100644 index 0000000..402331b --- /dev/null +++ b/pretty_assertions_derive_tests/src/lib.rs @@ -0,0 +1,49 @@ +//! Derives some custom macros for use in tests examples. Due to macro hoising/import limitations, +//! this must be done in a separate crate. + +#![deny(clippy::all, missing_docs, unsafe_code)] + +pretty_assertions_derive::derive_assert_eq!( + /// A simple plain text assertion format for easy reading tests. + (assert_eq_custom, |l, r, h, m| { + if h { + ::std::panic!("{:?} != {:?}: {}", l, r, m) + } else { + ::std::panic!("{:?} != {:?}: ", l, r) + } + }) +); + +pretty_assertions_derive::derive_assert_eq! { + /// Emojified `assert_eq`. + (assert_eq_emoji, |left, right, has_message, message| { + ::std::panic!("šŸ˜± Values should be equal! šŸ˜±{}{}\ + \n\ + \nšŸŽÆ {} šŸŽÆ\ + \n\ + \nšŸ’„ {} šŸ’„\ + \n", + if has_message { "\n-> with custom message: " } else { "" }, + message, + left, + right, + ) + }) +} + +pretty_assertions_derive::derive_assert_ne! { + /// Emojified `assert_ne`. + (assert_ne_emoji, |left, right, has_message, message| { + ::std::panic!("šŸ˜± Values should not be equal! šŸ˜±{}{}\ + \n\ + \nšŸ”„ {} šŸ”„\ + \n\ + \nšŸ”„ {} šŸ”„\ + \n", + if has_message { "\n-> with custom message: " } else { "" }, + message, + left, + right, + ) + }) +} diff --git a/pretty_assertions_derive_tests/tests/assert_eq.rs b/pretty_assertions_derive_tests/tests/assert_eq.rs new file mode 100644 index 0000000..e5f0459 --- /dev/null +++ b/pretty_assertions_derive_tests/tests/assert_eq.rs @@ -0,0 +1,18 @@ +use pretty_assertions_derive_tests::assert_eq_custom; + +#[test] +fn assert_eq_custom_pass() { + assert_eq_custom!(3, 1 + 2); +} + +#[test] +#[should_panic(expected = r#"3 != 2: "#)] +fn assert_eq_custom_fail() { + assert_eq_custom!(3, 2); +} + +#[test] +#[should_panic(expected = r#"3 != 2: message with var: 71"#)] +fn assert_eq_custom_fail_message() { + assert_eq_custom!(3, 2, "message with var: {}", 71); +} diff --git a/scripts/check b/scripts/check index 0ee0598..604a265 100755 --- a/scripts/check +++ b/scripts/check @@ -24,3 +24,4 @@ cargo doc --no-deps eprintln "Running examples" cargo run --example standard_assertion cargo run --example pretty_assertion +cargo run --example assert_emoji