├── .cargo-rdme.toml ├── lets_expect_core ├── .gitignore ├── src │ ├── assertions │ │ ├── mod.rs │ │ ├── assertion_result.rs │ │ └── assertion_error.rs │ ├── execution │ │ ├── mod.rs │ │ ├── test_result.rs │ │ ├── test_failure.rs │ │ ├── executed_assertion.rs │ │ ├── executed_expectation.rs │ │ └── executed_test_case.rs │ ├── utils │ │ ├── mod.rs │ │ ├── mutable_token.rs │ │ ├── reference_token.rs │ │ ├── indent.rs │ │ ├── parse_expression.rs │ │ ├── expr_dependencies.rs │ │ └── to_ident.rs │ ├── core │ │ ├── mode.rs │ │ ├── after_block.rs │ │ ├── before_block.rs │ │ ├── mod.rs │ │ ├── ident_from_pat.rs │ │ ├── create_module.rs │ │ ├── when_block.rs │ │ ├── keyword.rs │ │ ├── story_block.rs │ │ ├── expect_block.rs │ │ ├── to_block.rs │ │ ├── story_expect_to.rs │ │ ├── expect.rs │ │ ├── runtime.rs │ │ ├── story.rs │ │ ├── topological_sort.rs │ │ ├── when.rs │ │ ├── create_test.rs │ │ ├── to.rs │ │ └── context.rs │ ├── expectations │ │ ├── mod.rs │ │ ├── expectation_type.rs │ │ ├── expectation_tokens.rs │ │ ├── panic.rs │ │ ├── not_panic.rs │ │ ├── change_inner.rs │ │ ├── change_expression.rs │ │ ├── be_some_and.rs │ │ ├── be_ok_and.rs │ │ ├── be_err_and.rs │ │ ├── expression.rs │ │ ├── expectation.rs │ │ ├── change_many.rs │ │ ├── not_change.rs │ │ ├── change.rs │ │ ├── many.rs │ │ ├── inner.rs │ │ ├── have.rs │ │ ├── make.rs │ │ └── return_value.rs │ └── lib.rs └── Cargo.toml ├── lets_expect_macro ├── .gitignore ├── Cargo.toml └── src │ └── lib.rs ├── lets_expect_assertions ├── .gitignore ├── src │ ├── assertions.rs │ ├── expected_err.rs │ ├── lib.rs │ ├── bool.rs │ ├── option.rs │ ├── result.rs │ ├── panic.rs │ ├── match_pattern.rs │ ├── equality.rs │ ├── iterator.rs │ ├── change.rs │ └── partial_ord.rs └── Cargo.toml ├── tests ├── panic.rs ├── ranges.rs ├── tokio.rs ├── to.rs ├── change.rs ├── mut.rs ├── options.rs ├── results.rs ├── custom_change_assertion.rs ├── enums.rs ├── point.rs ├── make.rs ├── vecs.rs ├── let.rs ├── array.rs ├── when.rs ├── custom_assertion.rs ├── before_and_after.rs ├── story.rs └── have.rs ├── .devcontainer ├── console.sh └── devcontainer.json ├── .gitignore ├── .github ├── release.yaml └── workflows │ └── build.yaml ├── LICENSE ├── Cargo.toml └── README.md /.cargo-rdme.toml: -------------------------------------------------------------------------------- 1 | readme-path = "README.md" 2 | -------------------------------------------------------------------------------- /lets_expect_core/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | -------------------------------------------------------------------------------- /lets_expect_macro/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | -------------------------------------------------------------------------------- /lets_expect_assertions/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | -------------------------------------------------------------------------------- /lets_expect_core/src/assertions/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod assertion_error; 2 | pub mod assertion_result; 3 | -------------------------------------------------------------------------------- /lets_expect_core/src/assertions/assertion_result.rs: -------------------------------------------------------------------------------- 1 | use super::assertion_error::AssertionError; 2 | 3 | pub type AssertionResult = Result<(), AssertionError>; -------------------------------------------------------------------------------- /lets_expect_core/src/execution/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod executed_assertion; 2 | pub mod executed_expectation; 3 | pub mod executed_test_case; 4 | pub mod test_failure; 5 | pub mod test_result; 6 | -------------------------------------------------------------------------------- /lets_expect_core/src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod expr_dependencies; 2 | pub mod indent; 3 | pub mod mutable_token; 4 | pub mod parse_expression; 5 | pub mod reference_token; 6 | pub mod to_ident; 7 | -------------------------------------------------------------------------------- /lets_expect_core/src/core/mode.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Clone, Copy)] 2 | pub enum Mode { 3 | Test, 4 | PubMethod, 5 | PubAsyncMethod, 6 | #[cfg(feature = "tokio")] 7 | TokioTest, 8 | } 9 | -------------------------------------------------------------------------------- /lets_expect_core/src/assertions/assertion_error.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Debug, PartialEq, Eq)] 2 | pub struct AssertionError { 3 | pub message: Vec, 4 | } 5 | 6 | impl AssertionError { 7 | pub fn new(message: Vec) -> Self { 8 | Self { message } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tests/panic.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests { 3 | use lets_expect::lets_expect; 4 | 5 | lets_expect! { 6 | expect(panic!("I panicked!")) { 7 | to panic 8 | } 9 | 10 | expect(true) { 11 | to not_panic 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lets_expect_assertions/src/assertions.rs: -------------------------------------------------------------------------------- 1 | pub use super::bool::*; 2 | pub use super::change::*; 3 | pub use super::equality::*; 4 | pub use super::iterator::*; 5 | pub use super::match_pattern::*; 6 | pub use super::option::*; 7 | pub use super::panic::*; 8 | pub use super::partial_ord::*; 9 | pub use super::result::*; 10 | -------------------------------------------------------------------------------- /lets_expect_core/src/utils/mutable_token.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Span, TokenStream}; 2 | use quote::{quote, quote_spanned}; 3 | 4 | pub fn mutable_token(mutable: bool, span: &Span) -> TokenStream { 5 | if mutable { 6 | quote_spanned! { *span => mut } 7 | } else { 8 | quote! {} 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lets_expect_core/src/utils/reference_token.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Span, TokenStream}; 2 | use quote::{quote, quote_spanned}; 3 | 4 | pub fn reference_token(reference: bool, span: &Span) -> TokenStream { 5 | if reference { 6 | quote_spanned! { *span => & } 7 | } else { 8 | quote! {} 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lets_expect_core/src/utils/indent.rs: -------------------------------------------------------------------------------- 1 | const INDENT: &str = " "; 2 | 3 | pub fn indent(lines: &[String], levels: u8) -> Vec { 4 | let prefix = INDENT.repeat(levels as usize); 5 | 6 | lines 7 | .iter() 8 | .map(|line| format!("{}{}", prefix, line)) 9 | .collect::>() 10 | } 11 | -------------------------------------------------------------------------------- /.devcontainer/console.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | CONTAINER_ID=`docker container ls | grep vsc-lets_expect | awk '{print $1}'` 4 | 5 | if [ -z "$CONTAINER_ID" ] 6 | then 7 | echo "Devcontainer not found" 8 | exit 1 9 | fi 10 | 11 | echo "Opening console on $CONTAINER_ID..." 12 | 13 | docker exec -it $CONTAINER_ID /bin/bash 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | -------------------------------------------------------------------------------- /tests/ranges.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests { 3 | use lets_expect::lets_expect; 4 | 5 | lets_expect! { 6 | expect(1..4) { 7 | to contain_expected_values { 8 | have(contains(&2)) equal(true), 9 | have(contains(&5)) not_equal(true), 10 | have(len()) equal(3) 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/tokio.rs: -------------------------------------------------------------------------------- 1 | #[cfg(all(test, feature = "tokio"))] 2 | mod tests { 3 | use lets_expect::lets_expect; 4 | 5 | lets_expect! { #tokio_test 6 | let value = 5; 7 | let spawned = tokio::spawn(async move { 8 | value 9 | }); 10 | 11 | expect(spawned.await) { 12 | to match_pattern!(Ok(5)) 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lets_expect_assertions/src/expected_err.rs: -------------------------------------------------------------------------------- 1 | use lets_expect_core::assertions::{assertion_result::AssertionResult, assertion_error::AssertionError}; 2 | 3 | pub(super) fn expected_err(lines: Vec<&str>) -> AssertionResult { 4 | let message = lines 5 | .into_iter() 6 | .map(|line| line.to_string()) 7 | .collect::>(); 8 | Err(AssertionError::new(message)) 9 | } -------------------------------------------------------------------------------- /lets_expect_core/src/core/after_block.rs: -------------------------------------------------------------------------------- 1 | use syn::Block; 2 | 3 | use super::keyword; 4 | 5 | pub struct AfterBlock { 6 | pub keyword: keyword::after, 7 | pub after: Block, 8 | } 9 | 10 | impl AfterBlock { 11 | pub fn new(keyword: keyword::after, block: Block) -> Self { 12 | Self { 13 | keyword, 14 | after: block, 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lets_expect_core/src/core/before_block.rs: -------------------------------------------------------------------------------- 1 | use syn::Block; 2 | 3 | use super::keyword; 4 | 5 | pub struct BeforeBlock { 6 | pub keyword: keyword::before, 7 | pub before: Block, 8 | } 9 | 10 | impl BeforeBlock { 11 | pub fn new(keyword: keyword::before, block: Block) -> Self { 12 | Self { 13 | keyword, 14 | before: block, 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lets_expect_core/src/core/mod.rs: -------------------------------------------------------------------------------- 1 | mod after_block; 2 | mod before_block; 3 | mod create_module; 4 | mod create_test; 5 | mod expect; 6 | mod expect_block; 7 | mod ident_from_pat; 8 | mod mode; 9 | mod story; 10 | mod story_block; 11 | mod story_expect_to; 12 | mod to; 13 | mod to_block; 14 | mod topological_sort; 15 | mod when; 16 | mod when_block; 17 | 18 | pub mod context; 19 | pub(crate) mod keyword; 20 | pub mod runtime; 21 | -------------------------------------------------------------------------------- /lets_expect_core/src/execution/test_result.rs: -------------------------------------------------------------------------------- 1 | use super::{test_failure::TestFailure, executed_test_case::ExecutedTestCase}; 2 | 3 | pub type TestResult = Result<(), TestFailure>; 4 | 5 | pub fn test_result_from_cases(test_cases: Vec) -> TestResult { 6 | if test_cases.iter().any(|test_case| test_case.failed()) { 7 | Err(TestFailure::new(test_cases)) 8 | } else { 9 | Ok(()) 10 | } 11 | } -------------------------------------------------------------------------------- /.github/release.yaml: -------------------------------------------------------------------------------- 1 | changelog: 2 | categories: 3 | - title: Breaking Changes 🛠 4 | labels: 5 | - Semver-Major 6 | - breaking-change 7 | - title: New Features 🎉 8 | labels: 9 | - Semver-Minor 10 | - enhancement 11 | - title: Fixes 🐞 12 | labels: 13 | - Semver-Path 14 | - bug 15 | - title: Documentation 📖 16 | labels: 17 | - documentation -------------------------------------------------------------------------------- /tests/to.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests { 3 | use lets_expect::lets_expect; 4 | 5 | lets_expect! { 6 | expect(2 + 2) to equal(4) 7 | 8 | expect(2 + 3) { 9 | to equal(5) 10 | to not_equal(6) 11 | } 12 | 13 | expect(2 + 4) { 14 | to equal_6 { 15 | equal(6), 16 | not_equal(5) 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lets_expect_core/src/core/ident_from_pat.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::Ident; 2 | use syn::Pat; 3 | 4 | use super::topological_sort::TopologicalSortError; 5 | 6 | pub(crate) fn ident_from_pat(pat: &Pat) -> Result { 7 | match pat { 8 | Pat::Ident(pat) => Ok(pat.ident.clone()), 9 | Pat::Type(pat) => Ok(ident_from_pat(&pat.pat)?), 10 | _ => Err(TopologicalSortError::IdentExpected), 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tests/change.rs: -------------------------------------------------------------------------------- 1 | mod point; 2 | 3 | #[cfg(test)] 4 | mod tests { 5 | use crate::point::Point; 6 | use lets_expect::lets_expect; 7 | 8 | lets_expect! { 9 | expect(point.x = 5) { 10 | let mut point = Point { x: 1, y: 2 }; 11 | 12 | to change_only_x { 13 | change(point.x) { from(1), to(5), by(4) }, 14 | not_change(point.y) 15 | } 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lets_expect_core/src/expectations/mod.rs: -------------------------------------------------------------------------------- 1 | mod be_err_and; 2 | mod be_ok_and; 3 | mod be_some_and; 4 | mod change; 5 | mod change_expression; 6 | mod change_inner; 7 | mod change_many; 8 | mod expression; 9 | mod have; 10 | mod inner; 11 | mod make; 12 | mod many; 13 | mod not_change; 14 | mod not_panic; 15 | mod panic; 16 | mod return_value; 17 | 18 | pub(crate) mod expectation; 19 | pub(crate) mod expectation_tokens; 20 | pub(crate) mod expectation_type; 21 | -------------------------------------------------------------------------------- /lets_expect_core/src/core/create_module.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Ident, Span, TokenStream}; 2 | use quote::quote_spanned; 3 | 4 | pub fn create_module(span: &Span, identifier: &Ident, content: &TokenStream) -> TokenStream { 5 | let content = content; 6 | 7 | quote_spanned! { *span => 8 | pub mod #identifier { 9 | #[allow(unused_imports)] 10 | pub use super::*; 11 | 12 | #content 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/mut.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests { 3 | use lets_expect::lets_expect; 4 | 5 | lets_expect! { 6 | expect({a += 1; a}) { 7 | let mut a = 1; 8 | 9 | to equal_2 { 10 | equal(2), 11 | not_equal(1) 12 | } 13 | } 14 | 15 | expect(a += 1) { 16 | when(mut a: i64 = 1) { 17 | to change(a) { from(1), to(2) } 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lets_expect_core/src/core/when_block.rs: -------------------------------------------------------------------------------- 1 | use super::{keyword, runtime::Runtime, when::When}; 2 | use proc_macro2::TokenStream; 3 | 4 | pub struct WhenBlock { 5 | keyword: keyword::when, 6 | when: When, 7 | } 8 | 9 | impl WhenBlock { 10 | pub fn new(keyword: keyword::when, when: When) -> Self { 11 | Self { keyword, when } 12 | } 13 | 14 | pub fn to_tokens(&self, runtime: &Runtime) -> TokenStream { 15 | self.when.to_tokens(&self.keyword, runtime) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lets_expect_core/src/core/keyword.rs: -------------------------------------------------------------------------------- 1 | use syn::custom_keyword; 2 | 3 | custom_keyword!(after); 4 | custom_keyword!(before); 5 | custom_keyword!(expect); 6 | custom_keyword!(when); 7 | custom_keyword!(to); 8 | custom_keyword!(story); 9 | custom_keyword!(have); 10 | custom_keyword!(make); 11 | custom_keyword!(change); 12 | custom_keyword!(not_change); 13 | custom_keyword!(panic); 14 | custom_keyword!(not_panic); 15 | custom_keyword!(be_some_and); 16 | custom_keyword!(be_ok_and); 17 | custom_keyword!(be_err_and); 18 | -------------------------------------------------------------------------------- /lets_expect_core/src/core/story_block.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | 3 | use super::{keyword, runtime::Runtime, story::Story}; 4 | 5 | pub struct StoryBlock { 6 | pub keyword: keyword::story, 7 | pub story: Story, 8 | } 9 | 10 | impl StoryBlock { 11 | pub fn new(keyword: keyword::story, story: Story) -> Self { 12 | Self { keyword, story } 13 | } 14 | 15 | pub fn to_tokens(&self, runtime: &Runtime) -> TokenStream { 16 | self.story.to_tokens(runtime) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lets_expect_core/src/core/expect_block.rs: -------------------------------------------------------------------------------- 1 | use super::{expect::Expect, keyword, runtime::Runtime}; 2 | use proc_macro2::TokenStream; 3 | 4 | pub struct ExpectBlock { 5 | keyword: keyword::expect, 6 | expect: Expect, 7 | } 8 | 9 | impl ExpectBlock { 10 | pub fn new(keyword: keyword::expect, expect: Expect) -> Self { 11 | Self { keyword, expect } 12 | } 13 | 14 | pub fn to_tokens(&self, runtime: &Runtime) -> TokenStream { 15 | self.expect.to_tokens(&self.keyword, runtime) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/options.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests { 3 | use lets_expect::lets_expect; 4 | 5 | lets_expect! { 6 | expect(Some(1u8) as Option) { 7 | to be_some { 8 | equal(Some(1)), 9 | be_some 10 | } 11 | 12 | to be_some_and equal(1) 13 | } 14 | 15 | expect(None as Option) { 16 | to be_none { 17 | equal(None), 18 | be_none 19 | } 20 | } 21 | 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lets_expect_core/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn( 2 | clippy::use_self, 3 | clippy::cognitive_complexity, 4 | clippy::cloned_instead_of_copied, 5 | clippy::derive_partial_eq_without_eq, 6 | clippy::equatable_if_let, 7 | clippy::explicit_into_iter_loop, 8 | clippy::format_push_string, 9 | clippy::get_unwrap, 10 | clippy::match_same_arms, 11 | clippy::needless_for_each, 12 | clippy::todo 13 | )] 14 | 15 | pub mod assertions; 16 | pub mod core; 17 | pub mod execution; 18 | pub mod expectations; 19 | pub mod utils; 20 | -------------------------------------------------------------------------------- /lets_expect_core/src/expectations/expectation_type.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use proc_macro2::{Ident, Span}; 4 | use syn::parse::Parse; 5 | 6 | use super::expectation_tokens::ExpectationTokens; 7 | 8 | pub(crate) trait ExpectationType: Parse { 9 | fn span(&self) -> Span; 10 | fn identifier_string(&self) -> &str; 11 | fn tokens( 12 | &self, 13 | ident_prefix: &str, 14 | subject_reference: bool, 15 | subject_mutable: bool, 16 | ) -> ExpectationTokens; 17 | fn dependencies(&self) -> HashSet; 18 | } 19 | -------------------------------------------------------------------------------- /tests/results.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests { 3 | use lets_expect::lets_expect; 4 | 5 | lets_expect! { 6 | expect(Ok(1) as Result) { 7 | to be_ok { 8 | equal(Ok(1)), 9 | be_ok 10 | } 11 | 12 | to be_ok_and equal(1) 13 | } 14 | 15 | expect(Err(2) as Result) { 16 | to be_err { 17 | equal(Err(2)), 18 | be_err 19 | } 20 | 21 | to be_err_and equal(2) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lets_expect_core/src/core/to_block.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use super::{keyword, runtime::Runtime, to::To}; 4 | use proc_macro2::{Ident, TokenStream}; 5 | 6 | pub struct ToBlock { 7 | pub keyword: keyword::to, 8 | pub to: To, 9 | } 10 | 11 | impl ToBlock { 12 | pub fn new(keyword: keyword::to, to: To) -> Self { 13 | Self { keyword, to } 14 | } 15 | 16 | pub fn to_tokens(&self, runtime: &Runtime) -> (TokenStream, HashSet) { 17 | self.to.to_tokens(runtime) 18 | } 19 | 20 | pub fn identifier(&self) -> Ident { 21 | self.to.identifier() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lets_expect_core/src/execution/test_failure.rs: -------------------------------------------------------------------------------- 1 | use super::executed_test_case::ExecutedTestCase; 2 | use std::fmt::Debug; 3 | 4 | pub struct TestFailure { 5 | test_cases: Vec, 6 | } 7 | 8 | impl TestFailure { 9 | pub fn new(test_cases: Vec) -> Self { 10 | Self { test_cases } 11 | } 12 | } 13 | 14 | impl Debug for TestFailure { 15 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 16 | let test_cases = self.test_cases.iter().map(|test_case| test_case.to_string()).collect::>().join("\n"); 17 | write!(f, "\n\n{}", test_cases) 18 | } 19 | } -------------------------------------------------------------------------------- /lets_expect_assertions/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lets_expect_assertions" 3 | description = "lets_expect assertions. This crate is internal to the lets_expect crate and should not be used directly." 4 | version.workspace = true 5 | edition.workspace = true 6 | license.workspace = true 7 | repository.workspace = true 8 | rust-version.workspace = true 9 | categories.workspace = true 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [dependencies] 14 | lets_expect_core = { version = "=0.5.1", path = "../lets_expect_core" } 15 | colored.workspace = true 16 | 17 | [features] 18 | tokio = ["lets_expect_core/tokio"] 19 | -------------------------------------------------------------------------------- /lets_expect_assertions/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn( 2 | clippy::use_self, 3 | clippy::cognitive_complexity, 4 | clippy::cloned_instead_of_copied, 5 | clippy::derive_partial_eq_without_eq, 6 | clippy::equatable_if_let, 7 | clippy::explicit_into_iter_loop, 8 | clippy::format_push_string, 9 | clippy::get_unwrap, 10 | clippy::match_same_arms, 11 | clippy::needless_for_each, 12 | clippy::todo 13 | )] 14 | 15 | pub mod assertions; 16 | 17 | pub mod bool; 18 | pub mod change; 19 | pub mod equality; 20 | pub mod iterator; 21 | pub mod match_pattern; 22 | pub mod option; 23 | pub mod panic; 24 | pub mod partial_ord; 25 | pub mod result; 26 | 27 | #[cfg(test)] 28 | mod expected_err; 29 | -------------------------------------------------------------------------------- /lets_expect_core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lets_expect_core" 3 | description = "Core lets_expect code. This crate is internal to the lets_expect crate and should not be used directly." 4 | version.workspace = true 5 | edition.workspace = true 6 | license.workspace = true 7 | repository.workspace = true 8 | rust-version.workspace = true 9 | categories.workspace = true 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [dependencies] 14 | quote.workspace = true 15 | syn.workspace = true 16 | proc-macro2.workspace = true 17 | colored.workspace = true 18 | 19 | english-numbers = "0.3.3" 20 | topological-sort = "0.2.2" 21 | tokio = { workspace = true, optional = true } 22 | 23 | [features] 24 | tokio = ["dep:tokio"] 25 | -------------------------------------------------------------------------------- /tests/custom_change_assertion.rs: -------------------------------------------------------------------------------- 1 | use lets_expect::{AssertionError, AssertionResult}; 2 | 3 | fn by_multiplying_by(x: i32) -> impl Fn(&i32, &i32) -> AssertionResult { 4 | move |before, after| { 5 | if *after == *before * x { 6 | Ok(()) 7 | } else { 8 | Err(AssertionError::new(vec![format!( 9 | "Expected {} to be multiplied by {} to be {}, but it was {} instead", 10 | before, 11 | x, 12 | before * x, 13 | after 14 | )])) 15 | } 16 | } 17 | } 18 | 19 | #[cfg(test)] 20 | mod tests { 21 | use super::*; 22 | use lets_expect::lets_expect; 23 | 24 | lets_expect! { 25 | expect(a *= 5) { 26 | let mut a = 5; 27 | 28 | to change(a) by_multiplying_by(5) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/enums.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Debug, PartialEq)] 2 | enum Response { 3 | UserCreated, 4 | ValidationFailed(&'static str), 5 | } 6 | 7 | #[cfg(test)] 8 | mod tests { 9 | use super::*; 10 | use lets_expect::lets_expect; 11 | 12 | lets_expect! { 13 | expect(Response::UserCreated) { 14 | to be_user_created { 15 | equal(Response::UserCreated), 16 | not_equal(Response::ValidationFailed("Username is already taken")), 17 | match_pattern!(Response::UserCreated) 18 | } 19 | } 20 | 21 | expect(Response::ValidationFailed("email")) { 22 | to match_email { 23 | match_pattern!(Response::ValidationFailed("email")), 24 | not_match_pattern!(Response::ValidationFailed("email2")) 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/point.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt::Debug, fmt::Display, ops::Add}; 2 | 3 | #[derive(Clone, PartialEq)] 4 | pub struct Point { 5 | pub x: i32, 6 | pub y: i32, 7 | } 8 | 9 | impl Add for Point { 10 | type Output = Point; 11 | 12 | fn add(self, other: Point) -> Point { 13 | Point { 14 | x: self.x + other.x, 15 | y: self.y + other.y, 16 | } 17 | } 18 | } 19 | 20 | impl Display for Point { 21 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 22 | write!(f, "({}, {})", self.x, self.y) 23 | } 24 | } 25 | 26 | impl Debug for Point { 27 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 28 | write!(f, "Point({}, {})", self.x, self.y) 29 | } 30 | } 31 | 32 | #[derive(PartialEq)] 33 | pub struct Segment { 34 | pub start: Point, 35 | pub end: Point, 36 | } 37 | -------------------------------------------------------------------------------- /lets_expect_macro/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lets_expect_macro" 3 | description = "lets_expect! macro. This crate is internal to the lets_expect crate and should not be used directly." 4 | version.workspace = true 5 | edition.workspace = true 6 | license.workspace = true 7 | repository.workspace = true 8 | rust-version.workspace = true 9 | categories.workspace = true 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [lib] 14 | name = "lets_expect_macro" 15 | path = "src/lib.rs" 16 | proc-macro = true 17 | 18 | [dependencies] 19 | lets_expect_core = { version = "=0.5.1", path = "../lets_expect_core" } 20 | quote.workspace = true 21 | syn.workspace = true 22 | proc-macro2.workspace = true 23 | 24 | [dev-dependencies] 25 | lets_expect_assertions = { version = "=0.5.1", path = "../lets_expect_assertions" } 26 | 27 | [features] 28 | tokio = ["lets_expect_core/tokio"] 29 | -------------------------------------------------------------------------------- /tests/make.rs: -------------------------------------------------------------------------------- 1 | mod point; 2 | 3 | #[cfg(test)] 4 | mod tests { 5 | use crate::point::Point; 6 | use crate::point::Segment; 7 | use lets_expect::lets_expect; 8 | 9 | lets_expect! { 10 | expect(point.x = 5) { 11 | let mut point = Point { x: 1, y: 2 }; 12 | let unrelated = 5; 13 | 14 | to have_valid_coordinates { 15 | make(point.x) equal(5), 16 | make(point.x) { not_equal(4) }, 17 | make(point.y) { not_equal(1), equal(2) }, 18 | make(unrelated) equal(5) 19 | } 20 | } 21 | 22 | expect(Segment { start: Point { x: 1, y: 2 }, end: Point { x: 3, y: 4 } }) { 23 | to pass_the_same_make_assertion_twice { 24 | make(subject.start.clone()) equal(Point { x: 1, y: 2 }), 25 | make(subject.start) equal(Point { x: 1, y: 2 }) 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/vecs.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests { 3 | use lets_expect::lets_expect; 4 | 5 | lets_expect! { 6 | expect(empty_vec) { 7 | let empty_vec: Vec = vec![]; 8 | } 9 | 10 | expect(vec![1, 2, 3, 4, 5]) as vec_one_to_five { 11 | to have(contains(&4)) be_true 12 | } 13 | 14 | expect(vec![1, 2, 3]) { 15 | to contain_expected_values { 16 | have(len()) equal(3) 17 | } 18 | 19 | to have(mut iter()) all(be_greater_than(0)) 20 | to have(first()) equal(Some(&1)) 21 | } 22 | 23 | expect(mut vec![1, 2, 3]) { 24 | to have(remove(1)) equal(2) 25 | } 26 | 27 | expect(mut vec.iter()) { 28 | let vec = vec![1, 2, 3]; 29 | to all(be_greater_than(0)) 30 | } 31 | 32 | expect(vec.remove(1)) { 33 | when(mut vec = vec![1, 2, 3]) { 34 | to equal(2) 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/let.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests { 3 | use lets_expect::lets_expect; 4 | 5 | lets_expect! { 6 | expect(a) { 7 | let a: u8 = 1; 8 | 9 | to equal(1) 10 | } 11 | 12 | expect(a + b) { 13 | let a = 3; 14 | let b = 1; 15 | 16 | to equal(4) 17 | } 18 | 19 | expect(a + b + c == 5) { 20 | let a = 3; 21 | let b = 1; 22 | let c = 1; 23 | 24 | to be_true 25 | } 26 | 27 | expect(multiplied_by_2) { 28 | let value = 5; 29 | let multiplied_by_2 = value * 2; 30 | 31 | when(value = 10) { 32 | to equal(20) 33 | } 34 | 35 | when(value = value * 3) { 36 | to equal(30) 37 | } 38 | 39 | when(value: u128 = 1267650600228229401496703205376) { 40 | to equal(2535301200456458802993406410752) 41 | } 42 | } 43 | 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/array.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests { 3 | use lets_expect::lets_expect; 4 | 5 | struct Array { 6 | data: Vec, 7 | } 8 | 9 | lets_expect! { 10 | expect([] as [u8; 0]) { 11 | to have(len()) equal(0) 12 | } 13 | 14 | expect([1, 2 ,3]) { 15 | to contain_valid_elements { 16 | have(len()) equal(3), 17 | have(contains(&1)) be_true 18 | } 19 | to have(mut iter()) all(be_greater_than(0)) 20 | to have(mut iter()) any(be_less_or_equal_to(1)) 21 | } 22 | 23 | expect(Array { data: vec![1, 2, 3] }) { 24 | to have(data[0]) equal(1) 25 | to have(data[1..].to_vec()) equal(vec![2, 3]) 26 | } 27 | 28 | expect(Array { data: vec![vec![1,2,3], vec![0]]}) as array_of_non_copy_type { 29 | let unrelated_array = Array { data: vec![vec!["a"]] }; 30 | to have(&data[1]) equal(vec![0]) 31 | to make(&unrelated_array.data[0]) equal(vec!["a"]) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lets_expect_macro/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn( 2 | clippy::use_self, 3 | clippy::cognitive_complexity, 4 | clippy::cloned_instead_of_copied, 5 | clippy::derive_partial_eq_without_eq, 6 | clippy::equatable_if_let, 7 | clippy::explicit_into_iter_loop, 8 | clippy::format_push_string, 9 | clippy::get_unwrap, 10 | clippy::match_same_arms, 11 | clippy::needless_for_each, 12 | clippy::todo 13 | )] 14 | 15 | extern crate proc_macro; 16 | 17 | use lets_expect_core::core::{context::Context, runtime::Runtime}; 18 | use proc_macro::TokenStream; 19 | use proc_macro2::Span; 20 | use quote::quote; 21 | use syn::parse_macro_input; 22 | 23 | #[proc_macro] 24 | pub fn lets_expect(input: TokenStream) -> TokenStream { 25 | lets_expect_macro(input) 26 | } 27 | 28 | fn lets_expect_macro(input: TokenStream) -> TokenStream { 29 | let main_context = parse_macro_input!(input as Context); 30 | let tests = main_context.to_tokens(&Span::call_site(), &Runtime::default()); 31 | 32 | quote! { 33 | use lets_expect::*; 34 | 35 | #tests 36 | } 37 | .into() 38 | } 39 | -------------------------------------------------------------------------------- /lets_expect_core/src/utils/parse_expression.rs: -------------------------------------------------------------------------------- 1 | use syn::{parenthesized, parse::ParseBuffer, Error, Expr, Token}; 2 | 3 | pub struct ExpectationExpression { 4 | pub(crate) mutable: bool, 5 | pub(crate) reference: bool, 6 | pub(crate) expr: Expr, 7 | } 8 | 9 | pub fn parse_expectation_expression(input: &ParseBuffer) -> Result { 10 | let content; 11 | parenthesized!(content in input); 12 | 13 | let mut reference = false; 14 | if content.peek(Token![&]) { 15 | content.parse::()?; 16 | reference = true; 17 | } 18 | 19 | let mut mutable = false; 20 | if content.peek(Token![mut]) { 21 | content.parse::()?; 22 | mutable = true; 23 | } 24 | 25 | let expr = content.parse::()?; 26 | 27 | Ok(ExpectationExpression { 28 | mutable, 29 | reference, 30 | expr, 31 | }) 32 | } 33 | 34 | pub fn parse_expr(input: &ParseBuffer) -> Result { 35 | let content; 36 | parenthesized!(content in input); 37 | 38 | let expr = content.parse::()?; 39 | Ok(expr) 40 | } 41 | -------------------------------------------------------------------------------- /tests/when.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests { 3 | use lets_expect::lets_expect; 4 | 5 | lets_expect! { 6 | expect(a + b + c) { 7 | when(a = 2) { 8 | when ( 9 | b = 1, 10 | c = 1 11 | ) { 12 | to equal_4 { 13 | equal(4), 14 | not_equal(5) 15 | } 16 | } 17 | } 18 | 19 | when(a = 3, b = 3, c = 3) as everything_is_three { 20 | to equal(9) 21 | } 22 | 23 | when(c = 3) { 24 | expect(two + c + 10) { 25 | let two = 2; 26 | 27 | to equal(15) 28 | } 29 | } 30 | 31 | when all_numbers_are_negative { 32 | let a = -1; 33 | let b = -2; 34 | let c = -3; 35 | 36 | to equal(-6) 37 | } 38 | } 39 | 40 | expect(array) { 41 | when(array = [1, 2, 3]) to equal([1, 2, 3]) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Tomek Piotrowski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/custom_assertion.rs: -------------------------------------------------------------------------------- 1 | mod point; 2 | use lets_expect::{AssertionError, AssertionResult}; 3 | 4 | #[cfg(test)] 5 | mod tests { 6 | use super::*; 7 | use crate::point::Point; 8 | use lets_expect::lets_expect; 9 | 10 | fn have_positive_coordinates(point: &Point) -> AssertionResult { 11 | if point.x > 0 && point.y > 0 { 12 | Ok(()) 13 | } else { 14 | Err(AssertionError::new(vec![format!( 15 | "Expected ({}, {}) to be positive coordinates", 16 | point.x, point.y 17 | )])) 18 | } 19 | } 20 | 21 | fn have_x_coordinate_equal(x: i32) -> impl Fn(&Point) -> AssertionResult { 22 | move |point| { 23 | if point.x == x { 24 | Ok(()) 25 | } else { 26 | Err(AssertionError::new(vec![format!( 27 | "Expected x coordinate to be {}, but it was {}", 28 | x, point.x 29 | )])) 30 | } 31 | } 32 | } 33 | 34 | lets_expect! { 35 | expect(Point { x: 2, y: 22 }) { 36 | to have_valid_coordinates { 37 | have_positive_coordinates, 38 | have_x_coordinate_equal(2) 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | [workspace.package] 4 | version = "0.5.1" 5 | authors = ["Tomek Piotrowski"] 6 | documentation = "https://docs.rs/lets_expect" 7 | rust-version = "1.64" 8 | edition = "2021" 9 | license = "MIT" 10 | repository = "https://github.com/tomekpiotrowski/lets_expect" 11 | categories = ["development-tools::testing"] 12 | 13 | [workspace.dependencies] 14 | tokio = { version = "1", features = ["macros"] } 15 | syn = { version = "1.0.103", features = ["full", "extra-traits"] } 16 | quote = "1.0.21" 17 | proc-macro2 = "1.0.47" 18 | colored = "2.0.0" 19 | 20 | [package] 21 | name = "lets_expect" 22 | description = "Clean tests for Rust" 23 | version.workspace = true 24 | edition.workspace = true 25 | license.workspace = true 26 | repository.workspace = true 27 | rust-version.workspace = true 28 | categories.workspace = true 29 | 30 | [dependencies] 31 | lets_expect_core = { version = "=0.5.1", path = "lets_expect_core" } 32 | lets_expect_assertions = { version = "=0.5.1", path = "lets_expect_assertions" } 33 | lets_expect_macro = { version = "=0.5.1", path = "lets_expect_macro" } 34 | tokio = { workspace = true, optional = true } 35 | 36 | [dev-dependencies] 37 | tokio-test = { version = "0.4.2" } 38 | 39 | [features] 40 | tokio = ["dep:tokio", "lets_expect_assertions/tokio", "lets_expect_core/tokio", "lets_expect_macro/tokio"] 41 | -------------------------------------------------------------------------------- /tests/before_and_after.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests { 3 | use lets_expect::lets_expect; 4 | 5 | lets_expect! { 6 | let mut messages: Vec<&str> = Vec::new(); 7 | let mut _used_only_in_before = 0; 8 | let mut _used_only_in_after = 0; 9 | 10 | before { 11 | messages.push("first message"); 12 | _used_only_in_before += 1; 13 | } 14 | 15 | after { 16 | _used_only_in_after += 1; 17 | messages.clear(); 18 | } 19 | 20 | expect(messages.len()) { to equal(1) } 21 | expect(messages.push("new message")) { 22 | to change(messages.len()) { from(1), to(2) } 23 | } 24 | 25 | when there_are_two_messages { 26 | before { 27 | messages.push("second message"); 28 | } 29 | 30 | after { 31 | messages.remove(1); 32 | } 33 | 34 | expect(messages.len()) { to equal(2) } 35 | expect(*messages.get(1).unwrap()) as second_message { to equal("second message") } 36 | 37 | } 38 | 39 | story expect_messages_to_not_be_empty { 40 | expect(messages.len()) to equal(1) 41 | 42 | messages.push("new message"); 43 | 44 | expect(&messages) to have(len()) equal(2) 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lets_expect_core/src/execution/executed_assertion.rs: -------------------------------------------------------------------------------- 1 | use crate::{assertions::assertion_result::AssertionResult, utils::indent::indent}; 2 | use colored::Colorize; 3 | use std::fmt::Debug; 4 | 5 | #[derive(Clone, PartialEq, Eq)] 6 | pub struct ExecutedAssertion { 7 | pub assertion: String, 8 | pub result: AssertionResult, 9 | } 10 | 11 | impl ExecutedAssertion { 12 | pub fn new(assertion: String, result: AssertionResult) -> Self { 13 | Self { assertion, result } 14 | } 15 | 16 | pub fn failed(&self) -> bool { 17 | self.result.is_err() 18 | } 19 | 20 | pub fn pretty_print(&self) -> Vec { 21 | match &self.result { 22 | Ok(_) => vec![format!("{} {}", "✓", self.assertion) 23 | .green() 24 | .bold() 25 | .to_string()], 26 | Err(_) => { 27 | let mut lines = vec![format!("{} {}", "✗", self.assertion) 28 | .red() 29 | .bold() 30 | .to_string()]; 31 | lines.extend(indent(&self.result.as_ref().unwrap_err().message, 1)); 32 | 33 | lines 34 | } 35 | } 36 | } 37 | } 38 | 39 | impl Debug for ExecutedAssertion { 40 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 41 | write!(f, "{}", self.assertion) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lets_expect_core/src/expectations/expectation_tokens.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | 3 | #[derive(Clone)] 4 | pub struct SingleAssertionTokens { 5 | pub(crate) expression: String, 6 | pub(crate) assertion: TokenStream, 7 | } 8 | 9 | impl SingleAssertionTokens { 10 | pub fn new(expression: String, assertion: TokenStream) -> Self { 11 | Self { 12 | expression, 13 | assertion, 14 | } 15 | } 16 | } 17 | 18 | #[derive(Clone)] 19 | pub struct GroupAssertionTokens { 20 | pub(crate) label: String, 21 | pub(crate) argument: String, 22 | pub(crate) guard: Option, 23 | pub(crate) context: Option, 24 | pub(crate) inner: Box, 25 | } 26 | 27 | impl GroupAssertionTokens { 28 | pub fn new( 29 | label: String, 30 | expression: String, 31 | guard: Option, 32 | context: Option, 33 | inner: AssertionTokens, 34 | ) -> Self { 35 | Self { 36 | label, 37 | argument: expression, 38 | guard, 39 | context, 40 | inner: Box::new(inner), 41 | } 42 | } 43 | } 44 | 45 | #[derive(Clone)] 46 | pub enum AssertionTokens { 47 | Single(SingleAssertionTokens), 48 | Group(GroupAssertionTokens), 49 | Many(Vec), 50 | } 51 | 52 | #[derive(Clone)] 53 | pub(crate) struct ExpectationTokens { 54 | pub before_subject_evaluation: TokenStream, 55 | pub assertions: AssertionTokens, 56 | } 57 | -------------------------------------------------------------------------------- /lets_expect_assertions/src/bool.rs: -------------------------------------------------------------------------------- 1 | use colored::Colorize; 2 | use lets_expect_core::assertions::{ 3 | assertion_error::AssertionError, assertion_result::AssertionResult, 4 | }; 5 | 6 | pub fn be_true(value: &bool) -> AssertionResult { 7 | if *value { 8 | Ok(()) 9 | } else { 10 | let value = format!("{:?}", value).red().bold(); 11 | Err(AssertionError { 12 | message: vec![format!("Expected {} to be true", value)], 13 | }) 14 | } 15 | } 16 | 17 | pub fn be_false(value: &bool) -> AssertionResult { 18 | if !*value { 19 | Ok(()) 20 | } else { 21 | let value = format!("{:?}", value).red().bold(); 22 | Err(AssertionError { 23 | message: vec![format!("Expected {} to be false", value)], 24 | }) 25 | } 26 | } 27 | 28 | #[cfg(test)] 29 | mod test_super { 30 | use super::*; 31 | use crate::expected_err::expected_err; 32 | use colored::control::set_override; 33 | 34 | #[test] 35 | fn test_be_true_ok() { 36 | assert_eq!(be_true(&true), Ok(())); 37 | } 38 | 39 | #[test] 40 | fn test_be_true_err() { 41 | set_override(false); 42 | assert_eq!( 43 | be_true(&false), 44 | expected_err(vec!["Expected false to be true"]) 45 | ); 46 | } 47 | 48 | #[test] 49 | fn test_be_false() { 50 | assert_eq!(be_false(&false), Ok(())); 51 | } 52 | 53 | #[test] 54 | fn test_be_false_err() { 55 | set_override(false); 56 | assert_eq!( 57 | be_false(&true), 58 | expected_err(vec!["Expected true to be false"]) 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /lets_expect_core/src/execution/executed_expectation.rs: -------------------------------------------------------------------------------- 1 | use colored::Colorize; 2 | 3 | use crate::utils::indent::indent; 4 | 5 | use super::executed_assertion::ExecutedAssertion; 6 | 7 | pub enum ExecutedExpectation { 8 | Single(ExecutedAssertion), 9 | Group(String, String, Box), 10 | Many(Vec), 11 | } 12 | 13 | impl ExecutedExpectation { 14 | pub fn failed(&self) -> bool { 15 | match self { 16 | Self::Single(assertion) => assertion.failed(), 17 | Self::Group(_, _, assertion) => assertion.failed(), 18 | Self::Many(assertions) => assertions.iter().any(|assertion| assertion.failed()), 19 | } 20 | } 21 | 22 | pub fn pretty_print(&self) -> Vec { 23 | match self { 24 | Self::Single(assertion) => assertion.pretty_print(), 25 | Self::Group(label, arg, assertion) => { 26 | let assertion = assertion.pretty_print(); 27 | 28 | if assertion.len() == 1 { 29 | vec![format!( 30 | "{} {} {}", 31 | label.cyan(), 32 | arg.yellow().bold(), 33 | assertion[0] 34 | )] 35 | } else { 36 | let assertion = indent(&assertion, 1); 37 | let mut result = vec![format!("{} {}", label.cyan(), arg.yellow().bold())]; 38 | result.extend(assertion); 39 | result 40 | } 41 | } 42 | Self::Many(assertions) => assertions.iter().flat_map(Self::pretty_print).collect(), 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/rust 3 | { 4 | "name": "Rust", 5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 6 | "image": "mcr.microsoft.com/devcontainers/rust:0-1-bullseye", 7 | 8 | // Features to add to the dev container. More info: https://containers.dev/features. 9 | // "features": {}, 10 | 11 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 12 | // "forwardPorts": [], 13 | 14 | // Use 'postCreateCommand' to run commands after the container is created. 15 | // "postCreateCommand": "rustc --version", 16 | 17 | // Configure tool-specific properties. 18 | "customizations": { 19 | // Configure properties specific to VS Code. 20 | "vscode": { 21 | // Set *default* container specific settings.json values on container create. 22 | "settings": { 23 | "lldb.executable": "/usr/bin/lldb", 24 | // VS Code don't watch files under ./target 25 | "files.watcherExclude": { 26 | "**/target/**": true 27 | }, 28 | "rust-analyzer.checkOnSave.command": "clippy" 29 | }, 30 | // Add the IDs of extensions you want installed when the container is created. 31 | "extensions": [ 32 | "vadimcn.vscode-lldb", 33 | "mutantdino.resourcemonitor", 34 | "rust-lang.rust-analyzer", 35 | "tamasfe.even-better-toml", 36 | "serayuzgur.crates", 37 | "GitHub.vscode-pull-request-github" 38 | ] 39 | } 40 | } 41 | 42 | 43 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 44 | // "remoteUser": "root" 45 | } 46 | -------------------------------------------------------------------------------- /lets_expect_core/src/expectations/panic.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use proc_macro2::{Ident, Span, TokenStream}; 4 | use quote::quote_spanned; 5 | use syn::{parse::Parse, spanned::Spanned}; 6 | 7 | use crate::core::keyword; 8 | 9 | use super::expectation_tokens::{AssertionTokens, ExpectationTokens, SingleAssertionTokens}; 10 | 11 | pub(crate) struct PanicExpectation { 12 | keyword: keyword::panic, 13 | identifier_string: String, 14 | } 15 | 16 | impl PanicExpectation { 17 | pub fn new(keyword: keyword::panic) -> Self { 18 | Self { 19 | keyword, 20 | identifier_string: "panic".to_string(), 21 | } 22 | } 23 | 24 | pub fn peek(input: &syn::parse::ParseStream) -> bool { 25 | input.peek(keyword::panic) 26 | } 27 | 28 | pub fn span(&self) -> Span { 29 | self.keyword.span() 30 | } 31 | 32 | pub fn identifier_string(&self) -> &str { 33 | &self.identifier_string 34 | } 35 | 36 | pub(crate) fn tokens(&self) -> ExpectationTokens { 37 | let assertions = AssertionTokens::Single(SingleAssertionTokens::new( 38 | "panic".to_string(), 39 | quote_spanned! { self.keyword.span() => 40 | panic(&subject) 41 | }, 42 | )); 43 | 44 | ExpectationTokens { 45 | before_subject_evaluation: TokenStream::new(), 46 | assertions, 47 | } 48 | } 49 | 50 | pub fn dependencies(&self) -> HashSet { 51 | HashSet::new() 52 | } 53 | } 54 | 55 | impl Parse for PanicExpectation { 56 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 57 | let keyword = input.parse::()?; 58 | Ok(Self::new(keyword)) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lets_expect_core/src/expectations/not_panic.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use proc_macro2::{Ident, Span, TokenStream}; 4 | use quote::quote_spanned; 5 | use syn::{parse::Parse, spanned::Spanned}; 6 | 7 | use crate::core::keyword; 8 | 9 | use super::expectation_tokens::{AssertionTokens, ExpectationTokens, SingleAssertionTokens}; 10 | 11 | pub(crate) struct NotPanicExpectation { 12 | keyword: keyword::not_panic, 13 | identifier_string: String, 14 | } 15 | 16 | impl NotPanicExpectation { 17 | pub fn new(keyword: keyword::not_panic) -> Self { 18 | Self { 19 | keyword, 20 | identifier_string: "not_panic".to_string(), 21 | } 22 | } 23 | 24 | pub fn peek(input: &syn::parse::ParseStream) -> bool { 25 | input.peek(keyword::not_panic) 26 | } 27 | 28 | pub fn span(&self) -> Span { 29 | self.keyword.span() 30 | } 31 | 32 | pub fn identifier_string(&self) -> &str { 33 | &self.identifier_string 34 | } 35 | 36 | pub(crate) fn tokens(&self) -> ExpectationTokens { 37 | let assertions = AssertionTokens::Single(SingleAssertionTokens::new( 38 | "not_panic".to_string(), 39 | quote_spanned! { self.keyword.span() => 40 | not_panic(&subject) 41 | }, 42 | )); 43 | 44 | ExpectationTokens { 45 | before_subject_evaluation: TokenStream::new(), 46 | assertions, 47 | } 48 | } 49 | 50 | pub fn dependencies(&self) -> HashSet { 51 | HashSet::new() 52 | } 53 | } 54 | 55 | impl Parse for NotPanicExpectation { 56 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 57 | let keyword = input.parse::()?; 58 | Ok(Self::new(keyword)) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lets_expect_assertions/src/option.rs: -------------------------------------------------------------------------------- 1 | use colored::Colorize; 2 | use lets_expect_core::assertions::{ 3 | assertion_error::AssertionError, assertion_result::AssertionResult, 4 | }; 5 | use std::fmt::Debug; 6 | 7 | pub fn be_some(value: &Option) -> AssertionResult 8 | where 9 | R: Debug, 10 | { 11 | if value.is_some() { 12 | Ok(()) 13 | } else { 14 | let value = format!("{:?}", value).red().bold(); 15 | Err(AssertionError { 16 | message: vec![format!("Expected {} to be Some", value)], 17 | }) 18 | } 19 | } 20 | 21 | pub fn be_none(value: &Option) -> AssertionResult 22 | where 23 | R: Debug, 24 | { 25 | if value.is_none() { 26 | Ok(()) 27 | } else { 28 | let value = format!("{:?}", value).red().bold(); 29 | Err(AssertionError { 30 | message: vec![format!("Expected {} to be None", value)], 31 | }) 32 | } 33 | } 34 | 35 | #[cfg(test)] 36 | mod tests { 37 | use super::*; 38 | use crate::expected_err::expected_err; 39 | use colored::control::set_override; 40 | 41 | #[test] 42 | fn test_be_some_ok() { 43 | assert_eq!(be_some(&Some(1)), Ok(())); 44 | } 45 | 46 | #[test] 47 | fn test_be_some_err() { 48 | set_override(false); 49 | assert_eq!( 50 | be_some(&(None as Option)), 51 | expected_err(vec!["Expected None to be Some"]) 52 | ); 53 | } 54 | 55 | #[test] 56 | fn test_be_none_ok() { 57 | assert_eq!(be_none(&(None as Option)), Ok(())); 58 | } 59 | 60 | #[test] 61 | fn test_be_none_err() { 62 | set_override(false); 63 | assert_eq!( 64 | be_none(&Some(1)), 65 | expected_err(vec!["Expected Some(1) to be None"]) 66 | ); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /tests/story.rs: -------------------------------------------------------------------------------- 1 | struct User { 2 | name: String, 3 | password: String, 4 | } 5 | 6 | #[derive(Clone, Debug, PartialEq, Eq)] 7 | struct AuthenticationError { 8 | message: String, 9 | } 10 | 11 | struct Page { 12 | pub logged_in: bool, 13 | } 14 | 15 | impl Page { 16 | pub fn new() -> Self { 17 | Self { logged_in: false } 18 | } 19 | 20 | pub fn login(&mut self, user: &User) -> Result<(), AuthenticationError> { 21 | if user.name == "valid_name" && user.password == "valid_password" { 22 | self.logged_in = true; 23 | 24 | Ok(()) 25 | } else { 26 | Err(AuthenticationError { 27 | message: "Invalid credentials".to_string(), 28 | }) 29 | } 30 | } 31 | } 32 | 33 | #[cfg(test)] 34 | mod tests { 35 | use super::*; 36 | use lets_expect::*; 37 | 38 | lets_expect! { 39 | let mut page = Page::new(); 40 | 41 | let invalid_user = User { 42 | name: "invalid".to_string(), 43 | password: "invalid".to_string() 44 | }; 45 | let valid_user = User { 46 | name: "valid_name".to_string(), 47 | password: "valid_password".to_string() 48 | }; 49 | 50 | story login_is_successful { 51 | expect(page.logged_in) to be_false 52 | 53 | let login_result = page.login(&invalid_user); 54 | 55 | expect(login_result.clone()) to be_err 56 | expect(login_result) to equal(Err(AuthenticationError { message: "Invalid credentials".to_string() })) 57 | expect(page.logged_in) to be_false 58 | 59 | let login_result = page.login(&valid_user); 60 | 61 | expect(login_result) to be_ok 62 | expect(page.logged_in) to be_true 63 | } 64 | 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /lets_expect_core/src/expectations/change_inner.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use proc_macro2::{Ident, TokenStream}; 4 | use syn::parse::Parse; 5 | 6 | use super::{ 7 | change_expression::ChangeExpressionExpectation, change_many::ChangeManyExpectation, 8 | expectation_tokens::ExpectationTokens, expression::ExpressionExpectation, 9 | many::ManyExpectation, 10 | }; 11 | 12 | pub enum ChangeInnerExpectation { 13 | Expression(ChangeExpressionExpectation), 14 | Many(ChangeManyExpectation), 15 | } 16 | 17 | impl Parse for ChangeInnerExpectation { 18 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 19 | if ManyExpectation::::peek(&input) { 20 | Ok(Self::Many(input.parse::()?)) 21 | } else { 22 | Ok(Self::Expression( 23 | input.parse::()?, 24 | )) 25 | } 26 | } 27 | } 28 | 29 | impl ChangeInnerExpectation { 30 | pub fn identifier_string(&self) -> &str { 31 | match self { 32 | Self::Expression(expectation) => expectation.identifier_string(), 33 | Self::Many(expectation) => expectation.identifier_string(), 34 | } 35 | } 36 | 37 | pub(crate) fn tokens(&self, ident_prefix: &str, expression: &TokenStream) -> ExpectationTokens { 38 | match self { 39 | Self::Expression(expectation) => expectation.tokens(ident_prefix, expression), 40 | Self::Many(expectation) => expectation.tokens(ident_prefix, expression), 41 | } 42 | } 43 | 44 | pub fn dependencies(&self) -> HashSet { 45 | match self { 46 | Self::Expression(expectation) => expectation.dependencies(), 47 | Self::Many(expectation) => expectation.dependencies(), 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/have.rs: -------------------------------------------------------------------------------- 1 | mod point; 2 | 3 | struct StructWithNestedFields { 4 | field: i32, 5 | nested: NestedStruct, 6 | } 7 | 8 | struct NestedStruct { 9 | field: i32, 10 | } 11 | 12 | #[cfg(test)] 13 | mod tests { 14 | use super::*; 15 | use crate::point::Point; 16 | use crate::point::Segment; 17 | use lets_expect::lets_expect; 18 | 19 | lets_expect! { 20 | expect(segment) { 21 | let segment = Segment { start: Point { x: 0, y: 0 }, end: Point { x: 1, y: 1 } }; 22 | 23 | to have_valid_coordinates { 24 | have(start) equal(Point { x: 0, y: 0 }), 25 | have(end) equal(Point { x: 1, y: 1 }) 26 | } 27 | 28 | to access_the_same_value_twice { 29 | have(start.clone()) equal(Point { x: 0, y: 0 }), 30 | have(start) equal(Point { x: 0, y: 0 }) 31 | } 32 | } 33 | 34 | expect(a + b) { 35 | let a = Point { x: 1, y: 2 }; 36 | let b = Point { x: 3, y: 4 }; 37 | 38 | to have_valid_coordinates { 39 | have(x) equal(4), 40 | have(y) { equal(6), not_equal(5) } 41 | } 42 | 43 | when(valid_sum = "(4, 6)".to_string()) { 44 | to have(to_string()) equal(valid_sum) 45 | } 46 | } 47 | 48 | expect(struct_with_nested_fields) { 49 | let struct_with_nested_fields = StructWithNestedFields { 50 | field: 1, 51 | nested: NestedStruct { field: 2 } 52 | }; 53 | 54 | to have_valid_fields { 55 | have(field) equal(1), 56 | have(nested) { 57 | have(field) equal(2) 58 | } 59 | } 60 | 61 | to have(nested.field) equal(2) 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lets_expect_assertions/src/result.rs: -------------------------------------------------------------------------------- 1 | use colored::Colorize; 2 | use lets_expect_core::assertions::{ 3 | assertion_error::AssertionError, assertion_result::AssertionResult, 4 | }; 5 | use std::fmt::Debug; 6 | 7 | pub fn be_ok(value: &Result) -> AssertionResult 8 | where 9 | R: Debug, 10 | E: Debug, 11 | { 12 | if value.is_ok() { 13 | Ok(()) 14 | } else { 15 | let value = format!("{:?}", value).red().bold(); 16 | Err(AssertionError { 17 | message: vec![format!("Expected {} to be Ok", value)], 18 | }) 19 | } 20 | } 21 | 22 | pub fn be_err(value: &Result) -> AssertionResult 23 | where 24 | R: Debug, 25 | E: Debug, 26 | { 27 | if value.is_err() { 28 | Ok(()) 29 | } else { 30 | let value = format!("{:?}", value).red().bold(); 31 | Err(AssertionError { 32 | message: vec![format!("Expected {} to be Err", value)], 33 | }) 34 | } 35 | } 36 | 37 | #[cfg(test)] 38 | mod tests { 39 | use super::*; 40 | use crate::expected_err::expected_err; 41 | use colored::control::set_override; 42 | 43 | #[test] 44 | fn test_be_ok_ok() { 45 | assert_eq!(be_ok(&(Ok(1) as Result)), Ok(())); 46 | } 47 | 48 | #[test] 49 | fn test_be_ok_err() { 50 | set_override(false); 51 | assert_eq!( 52 | be_ok(&(Err(1) as Result)), 53 | expected_err(vec!["Expected Err(1) to be Ok"]) 54 | ); 55 | } 56 | 57 | #[test] 58 | fn test_be_err_ok() { 59 | assert_eq!(be_err(&(Err(1) as Result)), Ok(())); 60 | } 61 | 62 | #[test] 63 | fn test_be_err_err() { 64 | set_override(false); 65 | assert_eq!( 66 | be_err(&(Ok(1) as Result)), 67 | expected_err(vec!["Expected Ok(1) to be Err"]) 68 | ); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /lets_expect_core/src/execution/executed_test_case.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | use colored::Colorize; 4 | 5 | use crate::utils::indent::indent; 6 | 7 | use super::executed_expectation::ExecutedExpectation; 8 | 9 | pub struct ExecutedTestCase { 10 | subject: String, 11 | whens: Vec, 12 | expectation: ExecutedExpectation, 13 | } 14 | 15 | impl ExecutedTestCase { 16 | pub fn new(subject: String, whens: Vec<&str>, expectation: ExecutedExpectation) -> Self { 17 | Self { 18 | subject, 19 | whens: whens.iter().map(|when| when.to_string()).collect(), 20 | expectation, 21 | } 22 | } 23 | 24 | pub fn failed(&self) -> bool { 25 | self.expectation.failed() 26 | } 27 | } 28 | 29 | impl Display for ExecutedTestCase { 30 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 31 | let expect = "Expect".cyan(); 32 | let subject = self.subject.yellow().bold(); 33 | let mut whens = self 34 | .whens 35 | .iter() 36 | .enumerate() 37 | .map(|(index, when)| { 38 | format!( 39 | "{}{} {}", 40 | " ".repeat((index + 1) * 4), 41 | "When".cyan(), 42 | &when.yellow().bold() 43 | ) 44 | }) 45 | .collect::>() 46 | .join("\n"); 47 | 48 | if !whens.is_empty() { 49 | whens.push('\n'); 50 | } 51 | 52 | let expectations = self.expectation.pretty_print(); 53 | let expectations = indent(&expectations, (self.whens.len() + 1) as u8); 54 | 55 | write!( 56 | f, 57 | "{} {} {}\n{}{}\n", 58 | expect, 59 | subject, 60 | "to".cyan(), 61 | whens, 62 | expectations.join("\n") 63 | ) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /lets_expect_assertions/src/panic.rs: -------------------------------------------------------------------------------- 1 | use lets_expect_core::assertions::{ 2 | assertion_error::AssertionError, assertion_result::AssertionResult, 3 | }; 4 | use std::any::Any; 5 | 6 | pub fn panic(result: &Result>) -> AssertionResult { 7 | match result { 8 | Ok(_) => Err(AssertionError { 9 | message: vec![format!("Expected subject to panic, but it didn't")], 10 | }), 11 | Err(_) => Ok(()), 12 | } 13 | } 14 | 15 | pub fn not_panic(result: &Result>) -> AssertionResult { 16 | match result { 17 | Ok(_) => Ok(()), 18 | Err(_) => Err(AssertionError { 19 | message: vec![format!("Expected subject to not panic, but it did")], 20 | }), 21 | } 22 | } 23 | 24 | #[cfg(test)] 25 | mod tests { 26 | use super::*; 27 | use crate::expected_err::expected_err; 28 | use colored::control::set_override; 29 | 30 | #[test] 31 | fn test_panic_ok() { 32 | let result: Result> = Err(Box::new("test")); 33 | assert_eq!(panic(&result), Ok(())); 34 | } 35 | 36 | #[test] 37 | fn test_panic_err() { 38 | let result: Result> = Ok("test".to_string()); 39 | set_override(false); 40 | assert_eq!( 41 | panic(&result), 42 | expected_err(vec!["Expected subject to panic, but it didn't"]) 43 | ); 44 | } 45 | 46 | #[test] 47 | fn test_not_panic_ok() { 48 | let result: Result> = Ok("test".to_string()); 49 | assert_eq!(not_panic(&result), Ok(())); 50 | } 51 | 52 | #[test] 53 | fn test_not_panic_err() { 54 | let result: Result> = Err(Box::new("test")); 55 | set_override(false); 56 | assert_eq!( 57 | not_panic(&result), 58 | expected_err(vec!["Expected subject to not panic, but it did"]) 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /lets_expect_core/src/expectations/change_expression.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use proc_macro2::{Ident, TokenStream}; 4 | use quote::{quote_spanned, ToTokens}; 5 | use syn::{parse::Parse, spanned::Spanned, Expr}; 6 | 7 | use crate::{ 8 | expectations::expectation_tokens::SingleAssertionTokens, 9 | utils::{expr_dependencies::expr_dependencies, to_ident::expr_to_ident}, 10 | }; 11 | 12 | use super::expectation_tokens::{AssertionTokens, ExpectationTokens}; 13 | 14 | pub struct ChangeExpressionExpectation { 15 | expression: Expr, 16 | identifier_string: String, 17 | } 18 | 19 | impl Parse for ChangeExpressionExpectation { 20 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 21 | let expression = input.parse::()?; 22 | 23 | Ok(Self { 24 | identifier_string: expr_to_ident(&expression), 25 | expression, 26 | }) 27 | } 28 | } 29 | 30 | impl ChangeExpressionExpectation { 31 | pub fn identifier_string(&self) -> &str { 32 | &self.identifier_string 33 | } 34 | 35 | pub(crate) fn tokens( 36 | &self, 37 | _ident_prefix: &str, 38 | change_expression: &TokenStream, 39 | ) -> ExpectationTokens { 40 | ExpectationTokens { 41 | before_subject_evaluation: TokenStream::new(), 42 | assertions: { 43 | let assertion_label = self.expression.to_token_stream().to_string(); 44 | let before_variable_ident = Ident::new("from_value", self.expression.span()); 45 | 46 | let expression = self.expression.to_token_stream(); 47 | AssertionTokens::Single(SingleAssertionTokens::new( 48 | assertion_label, 49 | quote_spanned! { change_expression.span() => 50 | #expression(&#before_variable_ident, &#change_expression) 51 | }, 52 | )) 53 | }, 54 | } 55 | } 56 | 57 | pub fn dependencies(&self) -> HashSet { 58 | expr_dependencies(&self.expression) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lets_expect_assertions/src/match_pattern.rs: -------------------------------------------------------------------------------- 1 | pub use colored::Colorize; 2 | pub use lets_expect_core::assertions::assertion_error::AssertionError; 3 | 4 | #[macro_export] 5 | macro_rules! match_pattern { 6 | ($($pattern:pat_param)|+) => { 7 | |received: &_| { 8 | match *received { 9 | $($pattern)|+ => Ok(()), 10 | _ => { 11 | let received = format!("{:?}", received).red().bold(); 12 | Err(AssertionError { message: vec![format!("Expected {} to match pattern", received)] }) 13 | }, 14 | } 15 | } 16 | }; 17 | } 18 | 19 | #[macro_export] 20 | macro_rules! not_match_pattern { 21 | ($($pattern:pat_param)|+) => { 22 | |received: &_| { 23 | match *received { 24 | $($pattern)|+ => { 25 | let received = format!("{:?}", received).red().bold(); 26 | Err(AssertionError { message: vec![format!("Expected {} to not match pattern", received)] }) 27 | }, 28 | _ => Ok(()), 29 | } 30 | } 31 | }; 32 | } 33 | 34 | pub use match_pattern; 35 | pub use not_match_pattern; 36 | 37 | #[cfg(test)] 38 | mod tests { 39 | use super::*; 40 | use crate::expected_err::expected_err; 41 | use colored::control::set_override; 42 | 43 | #[test] 44 | fn test_match_pattern_ok() { 45 | assert_eq!(match_pattern!(1)(&1), Ok(())); 46 | } 47 | 48 | #[test] 49 | fn test_match_pattern_err() { 50 | set_override(false); 51 | assert_eq!( 52 | match_pattern!(1)(&2), 53 | expected_err(vec!["Expected 2 to match pattern"]) 54 | ); 55 | } 56 | 57 | #[test] 58 | fn test_not_match_pattern_ok() { 59 | assert_eq!(not_match_pattern!(1)(&2), Ok(())); 60 | } 61 | 62 | #[test] 63 | fn test_not_match_pattern_err() { 64 | set_override(false); 65 | assert_eq!( 66 | not_match_pattern!(1)(&1), 67 | expected_err(vec!["Expected 1 to not match pattern"]) 68 | ); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /lets_expect_assertions/src/equality.rs: -------------------------------------------------------------------------------- 1 | use colored::Colorize; 2 | use lets_expect_core::assertions::{ 3 | assertion_error::AssertionError, assertion_result::AssertionResult, 4 | }; 5 | use std::fmt::Debug; 6 | 7 | pub fn equal(expected: R) -> impl Fn(&R) -> AssertionResult 8 | where 9 | R: Debug + PartialEq, 10 | { 11 | move |received| { 12 | if expected == *received { 13 | Ok(()) 14 | } else { 15 | let expected = format!("{:?}", expected).green().bold(); 16 | let received = format!("{:?}", received).red().bold(); 17 | Err(AssertionError { 18 | message: vec![ 19 | format!("Expected: {}", expected), 20 | format!("Received: {}", received), 21 | ], 22 | }) 23 | } 24 | } 25 | } 26 | 27 | pub fn not_equal(expected: R) -> impl Fn(&R) -> AssertionResult 28 | where 29 | R: Debug + PartialEq, 30 | { 31 | move |received| { 32 | if expected != *received { 33 | Ok(()) 34 | } else { 35 | let expected = format!("{:?}", expected).green().bold(); 36 | Err(AssertionError { 37 | message: vec![format!("Expected something else than {}", expected)], 38 | }) 39 | } 40 | } 41 | } 42 | 43 | #[cfg(test)] 44 | mod tests { 45 | use super::*; 46 | use crate::expected_err::expected_err; 47 | use colored::control::set_override; 48 | 49 | #[test] 50 | fn test_equal_ok() { 51 | assert_eq!(equal(1)(&1), Ok(())); 52 | } 53 | 54 | #[test] 55 | fn test_equal_err() { 56 | set_override(false); 57 | assert_eq!( 58 | equal(1)(&2), 59 | expected_err(vec!["Expected: 1", "Received: 2"]) 60 | ); 61 | } 62 | 63 | #[test] 64 | fn test_not_equal_ok() { 65 | assert_eq!(not_equal(1)(&2), Ok(())); 66 | } 67 | 68 | #[test] 69 | fn test_not_equal_err() { 70 | set_override(false); 71 | assert_eq!( 72 | not_equal(1)(&1), 73 | expected_err(vec!["Expected something else than 1"]) 74 | ); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /lets_expect_core/src/expectations/be_some_and.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use proc_macro2::{Ident, Span}; 4 | use quote::quote_spanned; 5 | use syn::parse::{Parse, ParseStream}; 6 | 7 | use crate::core::keyword; 8 | 9 | use super::{ 10 | expectation_tokens::{AssertionTokens, ExpectationTokens, GroupAssertionTokens}, 11 | expectation_type::ExpectationType, 12 | inner::InnerExpectation, 13 | }; 14 | 15 | pub struct BeSomeAndExpectation { 16 | inner: Box, 17 | identifier_string: String, 18 | } 19 | 20 | impl BeSomeAndExpectation { 21 | pub(crate) fn new(expectation: InnerExpectation) -> Self { 22 | let identifier_string = format!("be_some_and_{}", expectation.identifier_string()); 23 | Self { 24 | inner: Box::new(expectation), 25 | identifier_string, 26 | } 27 | } 28 | 29 | pub fn peek(input: &syn::parse::ParseStream) -> bool { 30 | input.peek(keyword::be_some_and) 31 | } 32 | 33 | pub fn span(&self) -> Span { 34 | self.inner.span() 35 | } 36 | 37 | pub fn identifier_string(&self) -> &str { 38 | &self.identifier_string 39 | } 40 | 41 | pub fn dependencies(&self) -> HashSet { 42 | self.inner.dependencies() 43 | } 44 | 45 | pub(crate) fn tokens(&self, ident_prefix: &str) -> ExpectationTokens { 46 | let inner_tokens = self.inner.tokens(ident_prefix, false, false); 47 | let before_subject = inner_tokens.before_subject_evaluation; 48 | 49 | let guard = quote_spanned! { self.span() => let Some(subject) = subject }; 50 | 51 | let assertions = AssertionTokens::Group(GroupAssertionTokens::new( 52 | "be_some_and".to_string(), 53 | "".to_string(), 54 | Some(guard), 55 | None, 56 | inner_tokens.assertions, 57 | )); 58 | 59 | ExpectationTokens { 60 | before_subject_evaluation: before_subject, 61 | assertions, 62 | } 63 | } 64 | } 65 | 66 | impl Parse for BeSomeAndExpectation { 67 | fn parse(input: ParseStream) -> syn::Result { 68 | input.parse::()?; 69 | 70 | let inner = input.parse::()?; 71 | 72 | Ok(Self::new(inner)) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /lets_expect_core/src/core/story_expect_to.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use proc_macro2::{Ident, TokenStream}; 4 | use quote::quote_spanned; 5 | use syn::{parse::Parse, spanned::Spanned, Expr, Token}; 6 | 7 | use crate::core::{keyword, to::To}; 8 | 9 | use super::{runtime::Runtime, to_block::ToBlock}; 10 | 11 | pub struct StoryExpectTo { 12 | keyword: keyword::expect, 13 | mutable: bool, 14 | subject: Expr, 15 | to: ToBlock, 16 | } 17 | 18 | impl StoryExpectTo { 19 | pub fn new(keyword: keyword::expect, subject: Expr, mutable: bool, to: ToBlock) -> Self { 20 | Self { 21 | keyword, 22 | subject, 23 | mutable, 24 | to, 25 | } 26 | } 27 | } 28 | 29 | impl Parse for StoryExpectTo { 30 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 31 | let keyword = input.parse::()?; 32 | 33 | let content; 34 | syn::parenthesized!(content in input); 35 | 36 | let mut mutable = false; 37 | 38 | if content.peek(Token![mut]) { 39 | content.parse::()?; 40 | mutable = true; 41 | } 42 | 43 | let subject = content.parse::()?; 44 | 45 | let to_keyword = input.parse::()?; 46 | 47 | let to = input.parse::()?; 48 | let to = ToBlock::new(to_keyword, to); 49 | 50 | Ok(Self::new(keyword, subject, mutable, to)) 51 | } 52 | } 53 | 54 | impl StoryExpectTo { 55 | pub fn to_tokens(&self, runtime: &Runtime) -> (TokenStream, HashSet) { 56 | let runtime = runtime.extend( 57 | Some((self.mutable, self.subject.clone())), 58 | &[], 59 | &Vec::new(), 60 | &Vec::new(), 61 | None, 62 | ); 63 | let (to_tokens, dependencies) = self.to.to_tokens(&runtime); 64 | 65 | ( 66 | quote_spanned! { self.keyword.span() => 67 | let test_case = { #to_tokens }; 68 | let failed = test_case.failed(); 69 | 70 | test_cases.push(test_case); 71 | 72 | if failed { 73 | return test_result_from_cases(test_cases); 74 | } 75 | }, 76 | dependencies, 77 | ) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /lets_expect_core/src/expectations/be_ok_and.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use proc_macro2::{Ident, Span}; 4 | use quote::quote_spanned; 5 | use syn::parse::{Parse, ParseStream}; 6 | 7 | use crate::core::keyword; 8 | 9 | use super::{ 10 | expectation_tokens::{AssertionTokens, ExpectationTokens, GroupAssertionTokens}, 11 | expectation_type::ExpectationType, 12 | inner::InnerExpectation, 13 | }; 14 | 15 | pub struct BeOkAndExpectation { 16 | expectation: Box, 17 | identifier_string: String, 18 | } 19 | 20 | impl BeOkAndExpectation { 21 | pub(crate) fn new(expectation: InnerExpectation) -> Self { 22 | let identifier_string = format!("be_ok_and_{}", expectation.identifier_string()); 23 | 24 | Self { 25 | expectation: Box::new(expectation), 26 | identifier_string, 27 | } 28 | } 29 | 30 | pub fn peek(input: &syn::parse::ParseStream) -> bool { 31 | input.peek(keyword::be_ok_and) 32 | } 33 | 34 | pub fn span(&self) -> Span { 35 | self.expectation.span() 36 | } 37 | 38 | pub fn identifier_string(&self) -> &str { 39 | &self.identifier_string 40 | } 41 | 42 | pub fn dependencies(&self) -> HashSet { 43 | self.expectation.dependencies() 44 | } 45 | 46 | pub(crate) fn tokens(&self, ident_prefix: &str) -> ExpectationTokens { 47 | let inner_tokens = self.expectation.tokens(ident_prefix, false, false); 48 | let before_subject = inner_tokens.before_subject_evaluation; 49 | 50 | let guard = quote_spanned! { self.span() => let Ok(subject) = subject }; 51 | 52 | let assertions = AssertionTokens::Group(GroupAssertionTokens::new( 53 | "be_ok_and".to_string(), 54 | "".to_string(), 55 | Some(guard), 56 | None, 57 | inner_tokens.assertions, 58 | )); 59 | 60 | ExpectationTokens { 61 | before_subject_evaluation: before_subject, 62 | assertions, 63 | } 64 | } 65 | } 66 | 67 | impl Parse for BeOkAndExpectation { 68 | fn parse(input: ParseStream) -> syn::Result { 69 | input.parse::()?; 70 | 71 | let inner = input.parse::()?; 72 | 73 | Ok(Self::new(inner)) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /lets_expect_core/src/expectations/be_err_and.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use proc_macro2::{Ident, Span}; 4 | use quote::quote_spanned; 5 | use syn::parse::{Parse, ParseStream}; 6 | 7 | use crate::core::keyword; 8 | 9 | use super::{ 10 | expectation_tokens::{AssertionTokens, ExpectationTokens, GroupAssertionTokens}, 11 | expectation_type::ExpectationType, 12 | inner::InnerExpectation, 13 | }; 14 | 15 | pub struct BeErrAndExpectation { 16 | expectation: Box, 17 | identifier_string: String, 18 | } 19 | 20 | impl BeErrAndExpectation { 21 | pub(crate) fn new(expectation: InnerExpectation) -> Self { 22 | let identifier_string = format!("be_err_and_{}", expectation.identifier_string()); 23 | 24 | Self { 25 | expectation: Box::new(expectation), 26 | identifier_string, 27 | } 28 | } 29 | 30 | pub fn peek(input: &syn::parse::ParseStream) -> bool { 31 | input.peek(keyword::be_err_and) 32 | } 33 | 34 | pub fn span(&self) -> Span { 35 | self.expectation.span() 36 | } 37 | 38 | pub fn identifier_string(&self) -> &str { 39 | &self.identifier_string 40 | } 41 | 42 | pub fn dependencies(&self) -> HashSet { 43 | self.expectation.dependencies() 44 | } 45 | 46 | pub(crate) fn tokens(&self, ident_prefix: &str) -> ExpectationTokens { 47 | let inner_tokens = self.expectation.tokens(ident_prefix, false, false); 48 | let before_subject = inner_tokens.before_subject_evaluation; 49 | 50 | let guard = quote_spanned! { self.span() => let Err(subject) = subject }; 51 | 52 | let assertions = AssertionTokens::Group(GroupAssertionTokens::new( 53 | "be_err_and".to_string(), 54 | "".to_string(), 55 | Some(guard), 56 | None, 57 | inner_tokens.assertions, 58 | )); 59 | 60 | ExpectationTokens { 61 | before_subject_evaluation: before_subject, 62 | assertions, 63 | } 64 | } 65 | } 66 | 67 | impl Parse for BeErrAndExpectation { 68 | fn parse(input: ParseStream) -> syn::Result { 69 | input.parse::()?; 70 | 71 | let inner = input.parse::()?; 72 | 73 | Ok(Self::new(inner)) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /lets_expect_core/src/expectations/expression.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use proc_macro2::{Ident, TokenStream}; 4 | use quote::{quote_spanned, ToTokens}; 5 | use syn::{parse::Parse, spanned::Spanned, Expr}; 6 | 7 | use crate::utils::{ 8 | expr_dependencies::expr_dependencies, mutable_token::mutable_token, 9 | reference_token::reference_token, to_ident::expr_to_ident, 10 | }; 11 | 12 | use super::{ 13 | expectation_tokens::{AssertionTokens, ExpectationTokens, SingleAssertionTokens}, 14 | expectation_type::ExpectationType, 15 | }; 16 | 17 | pub struct ExpressionExpectation { 18 | expression: Expr, 19 | identifier_string: String, 20 | } 21 | 22 | impl Parse for ExpressionExpectation { 23 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 24 | let expression = input.parse::()?; 25 | 26 | Ok(Self { 27 | identifier_string: expr_to_ident(&expression), 28 | expression, 29 | }) 30 | } 31 | } 32 | 33 | impl ExpectationType for ExpressionExpectation { 34 | fn span(&self) -> proc_macro2::Span { 35 | self.expression.span() 36 | } 37 | 38 | fn identifier_string(&self) -> &str { 39 | &self.identifier_string 40 | } 41 | 42 | fn tokens( 43 | &self, 44 | _ident_prefix: &str, 45 | subject_reference: bool, 46 | subject_mutable: bool, 47 | ) -> ExpectationTokens { 48 | ExpectationTokens { 49 | before_subject_evaluation: TokenStream::new(), 50 | assertions: { 51 | let expression = &self.expression; 52 | let assertion_label = expression.to_token_stream().to_string(); 53 | let reference_token = reference_token(!subject_reference, &expression.span()); 54 | let mutable_token = mutable_token(subject_mutable, &expression.span()); 55 | 56 | AssertionTokens::Single(SingleAssertionTokens::new( 57 | assertion_label, 58 | quote_spanned! { expression.span() => 59 | #expression(#reference_token #mutable_token subject) 60 | }, 61 | )) 62 | }, 63 | } 64 | } 65 | 66 | fn dependencies(&self) -> HashSet { 67 | expr_dependencies(&self.expression) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /lets_expect_core/src/core/expect.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::to_ident::expr_to_ident; 2 | 3 | use super::{context::Context, create_module::create_module, keyword, runtime::Runtime}; 4 | use proc_macro2::{Ident, TokenStream}; 5 | use syn::{ 6 | braced, parenthesized, 7 | parse::{Parse, ParseStream}, 8 | spanned::Spanned, 9 | token::Brace, 10 | Error, Expr, Token, 11 | }; 12 | 13 | pub struct Expect { 14 | context: Context, 15 | subject_identifier: Ident, 16 | mutable: bool, 17 | subject: Expr, 18 | } 19 | 20 | impl Parse for Expect { 21 | fn parse(input: ParseStream) -> Result { 22 | let content; 23 | parenthesized!(content in input); 24 | 25 | let mut mutable = false; 26 | 27 | if content.peek(Token![mut]) { 28 | content.parse::()?; 29 | mutable = true; 30 | } 31 | 32 | let subject = content.parse::()?; 33 | 34 | let subject_identifier = if input.peek(Token![as]) { 35 | input.parse::()?; 36 | input.parse::()? 37 | } else { 38 | let mut subject_identifier = String::new(); 39 | if mutable { 40 | subject_identifier = "mut_".to_string(); 41 | } 42 | subject_identifier.push_str(&expr_to_ident(&subject)); 43 | Ident::new(&subject_identifier, subject.span()) 44 | }; 45 | 46 | let context = if input.peek(Brace) { 47 | let content; 48 | braced!(content in input); 49 | content.parse::()? 50 | } else { 51 | Context::from_single_item(input)? 52 | }; 53 | 54 | Ok(Self { 55 | context, 56 | subject_identifier, 57 | mutable, 58 | subject, 59 | }) 60 | } 61 | } 62 | 63 | impl Expect { 64 | pub fn to_tokens(&self, keyword: &keyword::expect, runtime: &Runtime) -> TokenStream { 65 | let runtime = runtime.extend( 66 | Some((self.mutable, self.subject.clone())), 67 | &Vec::new(), 68 | &Vec::new(), 69 | &Vec::new(), 70 | None, 71 | ); 72 | let context = self.context.to_tokens(&keyword.span(), &runtime); 73 | let module_identifier = Ident::new( 74 | &format!("expect_{}", self.subject_identifier), 75 | self.subject_identifier.span(), 76 | ); 77 | 78 | create_module(&keyword.span(), &module_identifier, &context) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /lets_expect_core/src/core/runtime.rs: -------------------------------------------------------------------------------- 1 | use syn::{Block, Expr, Local}; 2 | 3 | use super::mode::Mode; 4 | 5 | #[derive(Debug, Default)] 6 | pub struct Runtime { 7 | pub subject: Option<(bool, Expr)>, 8 | pub lets: Vec, 9 | pub befores: Vec, 10 | pub afters: Vec, 11 | pub mode: Option, 12 | pub whens: Vec, 13 | } 14 | 15 | impl Runtime { 16 | pub fn extend( 17 | &self, 18 | subject: Option<(bool, Expr)>, 19 | lets: &[Local], 20 | befores: &[Block], 21 | afters: &[Block], 22 | mode: Option, 23 | ) -> Self { 24 | let new_subject = if let Some(subject) = subject { 25 | Some(subject) 26 | } else { 27 | self.subject.clone() 28 | }; 29 | 30 | let new_lets = { 31 | let mut new_lets = self.lets.clone(); 32 | new_lets.extend(lets.to_vec()); 33 | new_lets 34 | }; 35 | 36 | let new_befores = { 37 | let mut new_befores = self.befores.clone(); 38 | new_befores.extend(befores.to_owned()); 39 | new_befores 40 | }; 41 | 42 | let new_afters = { 43 | let mut new_afters = afters.to_owned(); 44 | new_afters.extend(self.afters.clone()); 45 | new_afters 46 | }; 47 | 48 | let new_mode = if mode.is_some() { mode } else { self.mode }; 49 | 50 | Self { 51 | subject: new_subject, 52 | lets: new_lets, 53 | befores: new_befores, 54 | afters: new_afters, 55 | mode: new_mode, 56 | whens: self.whens.clone(), 57 | } 58 | } 59 | 60 | pub fn add_when(&self, when: String) -> Self { 61 | let mut new_whens = self.whens.clone(); 62 | new_whens.push(when); 63 | 64 | Self { 65 | subject: self.subject.clone(), 66 | lets: self.lets.clone(), 67 | befores: self.befores.clone(), 68 | afters: self.afters.clone(), 69 | mode: self.mode, 70 | whens: new_whens, 71 | } 72 | } 73 | 74 | pub fn add_lets(&self, lets: &[Local]) -> Self { 75 | let mut new_lets = self.lets.clone(); 76 | new_lets.extend(lets.to_vec()); 77 | 78 | Self { 79 | subject: self.subject.clone(), 80 | lets: new_lets, 81 | befores: self.befores.clone(), 82 | afters: self.afters.clone(), 83 | mode: self.mode, 84 | whens: self.whens.clone(), 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /lets_expect_assertions/src/iterator.rs: -------------------------------------------------------------------------------- 1 | use std::slice::Iter; 2 | 3 | use lets_expect_core::assertions::{ 4 | assertion_error::AssertionError, assertion_result::AssertionResult, 5 | }; 6 | 7 | pub fn all( 8 | assertion: impl Fn(&R) -> AssertionResult, 9 | ) -> impl FnOnce(&mut Iter) -> AssertionResult { 10 | move |iter| { 11 | let results: Vec = iter.map(assertion).collect(); 12 | 13 | if results.iter().all(|result| result.is_ok()) { 14 | Ok(()) 15 | } else { 16 | let mut errors = vec![]; 17 | for result in results { 18 | if let Err(err) = result { 19 | errors.push(err.message.join(" ")); 20 | } 21 | } 22 | Err(AssertionError::new(errors)) 23 | } 24 | } 25 | } 26 | 27 | pub fn any( 28 | assertion: impl Fn(&R) -> AssertionResult, 29 | ) -> impl FnOnce(&mut dyn Iterator) -> AssertionResult { 30 | move |iter| { 31 | let results: Vec = iter.map(assertion).collect(); 32 | 33 | if results.iter().any(|result| result.is_ok()) { 34 | Ok(()) 35 | } else { 36 | let mut errors = vec![]; 37 | for result in results { 38 | if let Err(err) = result { 39 | errors.push(err.message.join(" ")); 40 | } 41 | } 42 | Err(AssertionError::new(errors)) 43 | } 44 | } 45 | } 46 | 47 | #[cfg(test)] 48 | mod tests { 49 | use super::*; 50 | 51 | #[test] 52 | fn test_all_ones() { 53 | assert!(all(|&x| if x == 1 { 54 | Ok(()) 55 | } else { 56 | Err(AssertionError::new(vec!["err".to_string()])) 57 | })(&mut vec![1, 1, 1].iter()) 58 | .is_ok()); 59 | } 60 | 61 | #[test] 62 | fn test_not_all_ones() { 63 | assert!(all(|&x| if x == 1 { 64 | Ok(()) 65 | } else { 66 | Err(AssertionError::new(vec!["err".to_string()])) 67 | })(&mut vec![1, 2, 1].iter()) 68 | .is_err()); 69 | } 70 | 71 | #[test] 72 | fn test_any_success() { 73 | assert!(any(|&x| if x == 1 { 74 | Ok(()) 75 | } else { 76 | Err(AssertionError::new(vec!["err".to_string()])) 77 | })(&mut vec![1, 0, 0].iter()) 78 | .is_ok()); 79 | } 80 | 81 | #[test] 82 | fn test_any_failure() { 83 | assert!(any(|&x| if x == 1 { 84 | Ok(()) 85 | } else { 86 | Err(AssertionError::new(vec!["err".to_string()])) 87 | })(&mut vec![0, 0, 0].iter()) 88 | .is_ok()); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /lets_expect_core/src/expectations/expectation.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use proc_macro2::Ident; 4 | use syn::parse::{Parse, ParseStream}; 5 | 6 | use super::{ 7 | expectation_tokens::ExpectationTokens, expectation_type::ExpectationType, 8 | not_panic::NotPanicExpectation, panic::PanicExpectation, return_value::ReturnValueExpectation, 9 | }; 10 | 11 | pub(crate) enum Expectation { 12 | Result(ReturnValueExpectation), 13 | Panic(PanicExpectation), 14 | NotPanic(NotPanicExpectation), 15 | } 16 | 17 | impl Parse for Expectation { 18 | fn parse(input: ParseStream) -> syn::Result { 19 | if PanicExpectation::peek(&input) { 20 | Ok(Self::Panic(input.parse::()?)) 21 | } else if NotPanicExpectation::peek(&input) { 22 | Ok(Self::NotPanic(input.parse::()?)) 23 | } else { 24 | Ok(Self::Result(input.parse::()?)) 25 | } 26 | } 27 | } 28 | 29 | impl ExpectationType for Expectation { 30 | fn span(&self) -> proc_macro2::Span { 31 | match self { 32 | Self::Panic(expectation) => expectation.span(), 33 | Self::NotPanic(expectation) => expectation.span(), 34 | Self::Result(expectation) => expectation.span(), 35 | } 36 | } 37 | 38 | fn identifier_string(&self) -> &str { 39 | match self { 40 | Self::Panic(expectation) => expectation.identifier_string(), 41 | Self::NotPanic(expectation) => expectation.identifier_string(), 42 | Self::Result(expectation) => expectation.identifier_string(), 43 | } 44 | } 45 | 46 | fn dependencies(&self) -> HashSet { 47 | match self { 48 | Self::Panic(expectation) => expectation.dependencies(), 49 | Self::NotPanic(expectation) => expectation.dependencies(), 50 | Self::Result(expectation) => expectation.dependencies(), 51 | } 52 | } 53 | 54 | fn tokens( 55 | &self, 56 | ident_prefix: &str, 57 | subject_reference: bool, 58 | subject_mutable: bool, 59 | ) -> ExpectationTokens { 60 | match self { 61 | Self::Panic(expectation) => expectation.tokens(), 62 | Self::NotPanic(expectation) => expectation.tokens(), 63 | Self::Result(expectation) => { 64 | expectation.tokens(ident_prefix, subject_reference, subject_mutable) 65 | } 66 | } 67 | } 68 | } 69 | 70 | impl Expectation { 71 | pub(crate) fn is_panic(&self) -> bool { 72 | match self { 73 | Self::NotPanic(_) | Self::Panic(_) => true, 74 | Self::Result(_) => false, 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /lets_expect_core/src/expectations/change_many.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use proc_macro2::TokenStream; 4 | use syn::{parse::Parse, punctuated::Punctuated, Ident}; 5 | 6 | use super::{ 7 | change_inner::ChangeInnerExpectation, 8 | expectation_tokens::{AssertionTokens, ExpectationTokens}, 9 | }; 10 | 11 | pub struct ChangeManyExpectation { 12 | inner: Vec, 13 | identifier_string: String, 14 | } 15 | 16 | impl ChangeManyExpectation { 17 | pub fn new(identifier: Option, inner: Vec) -> Self { 18 | let identifier_string = identifier.as_ref().map_or_else( 19 | || { 20 | { 21 | inner 22 | .iter() 23 | .map(|expectation| expectation.identifier_string()) 24 | .collect::>() 25 | .join("_and_") 26 | } 27 | }, 28 | |ident| ident.to_string(), 29 | ); 30 | 31 | Self { 32 | inner, 33 | identifier_string, 34 | } 35 | } 36 | 37 | pub fn identifier_string(&self) -> &str { 38 | &self.identifier_string 39 | } 40 | 41 | pub(crate) fn tokens(&self, ident_prefix: &str, expression: &TokenStream) -> ExpectationTokens { 42 | let ident = format!("{}_{}", ident_prefix, self.identifier_string()); 43 | let mut before_subject = TokenStream::new(); 44 | let mut assertions = Vec::new(); 45 | 46 | self.inner.iter().for_each(|inner| { 47 | let inner_tokens = inner.tokens(&ident, expression); 48 | 49 | before_subject.extend(inner_tokens.before_subject_evaluation); 50 | assertions.push(inner_tokens.assertions); 51 | }); 52 | 53 | ExpectationTokens { 54 | before_subject_evaluation: before_subject, 55 | assertions: AssertionTokens::Many(assertions), 56 | } 57 | } 58 | 59 | pub fn dependencies(&self) -> HashSet { 60 | self.inner 61 | .iter() 62 | .flat_map(|inner| inner.dependencies()) 63 | .collect() 64 | } 65 | } 66 | 67 | impl Parse for ChangeManyExpectation { 68 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 69 | let identifier = if input.peek(syn::Ident) { 70 | Some(input.parse::()?) 71 | } else { 72 | None 73 | }; 74 | 75 | let content; 76 | syn::braced!(content in input); 77 | 78 | let inner = 79 | Punctuated::::parse_terminated(&content)? 80 | .into_iter() 81 | .collect(); 82 | 83 | Ok(Self::new(identifier, inner)) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /lets_expect_core/src/core/story.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use proc_macro2::TokenStream; 4 | use quote::quote; 5 | use quote::quote_spanned; 6 | use syn::{ 7 | parse::{Parse, ParseStream}, 8 | Ident, Result, Stmt, 9 | }; 10 | 11 | use crate::utils::expr_dependencies::stmt_dependencies; 12 | 13 | use super::runtime::Runtime; 14 | use super::{create_test::create_test, story_expect_to::StoryExpectTo}; 15 | 16 | pub enum StoryElement { 17 | Statement(Box), 18 | Expect(Box), 19 | } 20 | 21 | pub struct Story { 22 | pub identifier: Ident, 23 | elements: Vec, 24 | } 25 | 26 | impl Story { 27 | pub fn new(identifier: Ident, elements: Vec) -> Self { 28 | Self { 29 | identifier, 30 | elements, 31 | } 32 | } 33 | } 34 | 35 | impl Parse for Story { 36 | fn parse(input: ParseStream) -> Result { 37 | let identifier = input.parse::()?; 38 | 39 | let content; 40 | syn::braced!(content in input); 41 | 42 | let mut elements = Vec::new(); 43 | 44 | while !content.is_empty() { 45 | if content.peek(Ident) && content.cursor().ident().unwrap().0 == "expect" { 46 | elements.push(StoryElement::Expect(content.parse()?)); 47 | } else { 48 | elements.push(StoryElement::Statement(content.parse()?)); 49 | } 50 | } 51 | 52 | Ok(Self::new(identifier, elements)) 53 | } 54 | } 55 | 56 | impl Story { 57 | pub fn to_tokens(&self, runtime: &Runtime) -> TokenStream { 58 | let (elements, dependencies): (Vec, HashSet) = 59 | self.elements.iter().fold( 60 | (Vec::new(), HashSet::new()), 61 | |(mut token_streams, mut dependencies), element| { 62 | match element { 63 | StoryElement::Statement(statement) => { 64 | token_streams.push(quote! { #statement }); 65 | dependencies.extend(stmt_dependencies(statement)); 66 | } 67 | StoryElement::Expect(expect) => { 68 | let (token_stream, idents) = expect.to_tokens(runtime); 69 | token_streams.push(token_stream); 70 | dependencies.extend(idents); 71 | } 72 | }; 73 | 74 | (token_streams, dependencies) 75 | }, 76 | ); 77 | 78 | let content = quote_spanned! { self.identifier.span() => 79 | let mut test_cases = Vec::new(); 80 | 81 | #(#elements)* 82 | 83 | test_cases 84 | }; 85 | 86 | create_test(&self.identifier, runtime, &content, &dependencies) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /lets_expect_core/src/expectations/not_change.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use proc_macro2::{Ident, Span}; 4 | use quote::{quote_spanned, ToTokens}; 5 | use syn::{parenthesized, spanned::Spanned, Expr}; 6 | 7 | use crate::{ 8 | core::keyword, 9 | utils::{expr_dependencies::expr_dependencies, to_ident::expr_to_ident}, 10 | }; 11 | 12 | use super::expectation_tokens::{ 13 | AssertionTokens, ExpectationTokens, GroupAssertionTokens, SingleAssertionTokens, 14 | }; 15 | 16 | pub struct NotChangeExpectation { 17 | identifier_string: String, 18 | expression: Expr, 19 | } 20 | 21 | impl NotChangeExpectation { 22 | pub fn new(expr: Expr) -> Self { 23 | let identifier_string = format!("not_change_{}", expr_to_ident(&expr)); 24 | 25 | Self { 26 | identifier_string, 27 | expression: expr, 28 | } 29 | } 30 | pub fn peek(input: &syn::parse::ParseStream) -> bool { 31 | input.peek(keyword::not_change) 32 | } 33 | 34 | pub fn span(&self) -> Span { 35 | self.expression.span() 36 | } 37 | 38 | pub fn identifier_string(&self) -> &str { 39 | &self.identifier_string 40 | } 41 | 42 | pub(crate) fn tokens(&self, ident_prefix: &str) -> ExpectationTokens { 43 | let ident = format!("{}_{}", ident_prefix, self.identifier_string()); 44 | 45 | let before_variable_name = format!("{}_before", ident); 46 | let before_variable_ident = Ident::new(&before_variable_name, self.span()); 47 | 48 | let expr = &self.expression; 49 | 50 | let before_subject = quote_spanned! { expr.span() => 51 | let #before_variable_ident = #expr; 52 | }; 53 | 54 | let assertions = AssertionTokens::Single(SingleAssertionTokens::new( 55 | "".to_string(), 56 | quote_spanned! { expr.span() => 57 | equal(from_value)(&#expr) 58 | }, 59 | )); 60 | 61 | let context = quote_spanned! { self.span() => 62 | let from_value = #before_variable_ident; 63 | }; 64 | 65 | let assertions = AssertionTokens::Group(GroupAssertionTokens::new( 66 | "not_change".to_string(), 67 | self.expression.to_token_stream().to_string(), 68 | None, 69 | Some(context), 70 | assertions, 71 | )); 72 | 73 | ExpectationTokens { 74 | before_subject_evaluation: before_subject, 75 | assertions, 76 | } 77 | } 78 | 79 | pub fn dependencies(&self) -> HashSet { 80 | expr_dependencies(&self.expression) 81 | } 82 | } 83 | 84 | impl syn::parse::Parse for NotChangeExpectation { 85 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 86 | input.parse::()?; 87 | let content; 88 | parenthesized!(content in input); 89 | let expr = content.parse::()?; 90 | 91 | Ok(Self::new(expr)) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | # Publish semver tags as releases. 7 | tags: [ 'v*.*.*' ] 8 | pull_request: 9 | branches: [ "main" ] 10 | 11 | jobs: 12 | build: 13 | timeout-minutes: 5 14 | permissions: 15 | contents: read 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - name: Checkout code 20 | uses: actions/checkout@v3 21 | - name: Install Rust toolchain 22 | uses: actions-rs/toolchain@v1 23 | with: 24 | toolchain: 1.64.0 25 | # - uses: actions-rs/install@v0.1 26 | # with: 27 | # crate: cargo-rdme 28 | # version: latest 29 | # use-tool-cache: true 30 | # - name: Verify README is up to date 31 | # run: cargo rdme --check 32 | - name: Check default features 33 | uses: actions-rs/cargo@v1 34 | with: 35 | command: check 36 | - name: Check all features 37 | uses: actions-rs/cargo@v1 38 | with: 39 | command: check 40 | args: --all-features 41 | - name: Run tests 42 | uses: actions-rs/cargo@v1 43 | with: 44 | command: test 45 | args: --all-features 46 | - name: Run clippy 47 | uses: actions-rs/cargo@v1 48 | with: 49 | command: clippy 50 | args: --all-features 51 | publish: 52 | if: startsWith(github.ref, 'refs/tags/') 53 | needs: 54 | - build 55 | runs-on: ubuntu-latest 56 | steps: 57 | - uses: actions/checkout@v2 58 | - uses: actions-rs/toolchain@v1 59 | with: 60 | toolchain: 1.64.0 61 | - name: Publish lets_expect_core 62 | uses: nick-fields/retry@v2 63 | env: 64 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 65 | with: 66 | timeout_seconds: 300 67 | max_attempts: 5 68 | command: cd lets_expect_core && cargo publish --token $CARGO_REGISTRY_TOKEN 69 | - name: Publish lets_expect_assertions 70 | uses: nick-fields/retry@v2 71 | env: 72 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 73 | with: 74 | timeout_seconds: 300 75 | max_attempts: 5 76 | command: cd lets_expect_assertions && cargo publish --token $CARGO_REGISTRY_TOKEN 77 | - name: Publish lets_expect_macro 78 | uses: nick-fields/retry@v2 79 | env: 80 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 81 | with: 82 | timeout_seconds: 300 83 | max_attempts: 5 84 | command: cd lets_expect_macro && cargo publish --token $CARGO_REGISTRY_TOKEN 85 | - name: Publish lets_expect 86 | uses: nick-fields/retry@v2 87 | env: 88 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 89 | with: 90 | timeout_seconds: 300 91 | max_attempts: 5 92 | command: cargo publish --token $CARGO_REGISTRY_TOKEN 93 | - name: Release 94 | uses: softprops/action-gh-release@v1 95 | with: 96 | generate_release_notes: true -------------------------------------------------------------------------------- /lets_expect_core/src/core/topological_sort.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashMap, HashSet}; 2 | 3 | use syn::{Ident, Local}; 4 | use topological_sort::TopologicalSort; 5 | 6 | use crate::utils::expr_dependencies::expr_dependencies; 7 | 8 | use super::ident_from_pat::ident_from_pat; 9 | 10 | #[derive(Clone)] 11 | struct Let { 12 | dependencies: HashSet, 13 | statements: Vec, 14 | } 15 | 16 | #[derive(Debug)] 17 | pub enum TopologicalSortError { 18 | CyclicDependency(Vec), 19 | IdentExpected, 20 | } 21 | 22 | pub fn topological_sort(lets: &[Local]) -> Result, TopologicalSortError> { 23 | let let_idents: HashSet = lets 24 | .iter() 25 | .map(|l| ident_from_pat(&l.pat)) 26 | .collect::, TopologicalSortError>>()?; 27 | let sorted: HashMap = 28 | lets.iter() 29 | .try_fold(HashMap::new(), |mut variables, r#let| { 30 | add_let_statement(r#let, &mut variables, &let_idents)?; 31 | Ok(variables) 32 | })?; 33 | 34 | let mut ts = TopologicalSort::<&Ident>::new(); 35 | 36 | for (ident, r#let) in sorted.iter() { 37 | ts.insert(ident); 38 | 39 | r#let.dependencies.iter().for_each(|dependency| { 40 | ts.add_dependency(dependency, ident); 41 | }); 42 | } 43 | 44 | let mut result = Vec::new(); 45 | 46 | while let Some(ident) = ts.pop() { 47 | let r#let = sorted 48 | .get(ident) 49 | .expect("TopologicalSort returned an unknown ident"); 50 | 51 | result.extend(r#let.statements.iter().cloned()); 52 | } 53 | 54 | if !ts.is_empty() { 55 | return Err(TopologicalSortError::CyclicDependency( 56 | sorted.keys().cloned().collect(), 57 | )); 58 | } 59 | 60 | Ok(result) 61 | } 62 | 63 | fn add_let_statement( 64 | r#let: &Local, 65 | variables: &mut HashMap, 66 | defined_idents: &HashSet, 67 | ) -> Result<(), TopologicalSortError> { 68 | let ident = ident_from_pat(&r#let.pat)?; 69 | let dependencies = if let Some(init) = &r#let.init { 70 | expr_dependencies(&init.1) 71 | } else { 72 | HashSet::new() 73 | }; 74 | let dependencies: HashSet = dependencies.intersection(defined_idents).cloned().collect(); 75 | let depends_on_itself = dependencies.contains(&ident); 76 | let dependencies_without_itself = dependencies 77 | .into_iter() 78 | .filter(|dependency| *dependency != ident); 79 | 80 | let existing_lets = variables.get_mut(&ident); 81 | if let Some(existing_lets) = existing_lets { 82 | if !depends_on_itself { 83 | existing_lets.dependencies.clear(); 84 | existing_lets.statements.clear(); 85 | } 86 | 87 | existing_lets 88 | .dependencies 89 | .extend(dependencies_without_itself); 90 | existing_lets.statements.push(r#let.clone()); 91 | } else { 92 | variables.insert( 93 | ident.clone(), 94 | Let { 95 | dependencies: dependencies_without_itself.collect(), 96 | statements: vec![r#let.clone()], 97 | }, 98 | ); 99 | } 100 | 101 | Ok(()) 102 | } 103 | -------------------------------------------------------------------------------- /lets_expect_core/src/expectations/change.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use proc_macro2::{Ident, Span}; 4 | use quote::{quote_spanned, ToTokens}; 5 | use syn::{ 6 | parenthesized, 7 | parse::{Parse, ParseStream}, 8 | spanned::Spanned, 9 | Expr, 10 | }; 11 | 12 | use super::{ 13 | change_inner::ChangeInnerExpectation, 14 | expectation_tokens::{AssertionTokens, ExpectationTokens, GroupAssertionTokens}, 15 | }; 16 | use crate::{ 17 | core::keyword, 18 | utils::{expr_dependencies::expr_dependencies, to_ident::expr_to_ident}, 19 | }; 20 | 21 | pub struct ChangeExpectation { 22 | expression: Expr, 23 | inner: Box, 24 | identifier_string: String, 25 | } 26 | 27 | impl ChangeExpectation { 28 | pub fn new(expr: Expr, inner: Box) -> Self { 29 | let identifier_string = format!( 30 | "change_{}_{}", 31 | expr_to_ident(&expr), 32 | inner.identifier_string() 33 | ); 34 | 35 | Self { 36 | expression: expr, 37 | inner, 38 | identifier_string, 39 | } 40 | } 41 | 42 | pub fn peek(input: &ParseStream) -> bool { 43 | input.peek(keyword::change) 44 | } 45 | 46 | pub fn span(&self) -> Span { 47 | self.expression.span() 48 | } 49 | 50 | pub fn identifier_string(&self) -> &str { 51 | &self.identifier_string 52 | } 53 | 54 | pub(crate) fn tokens(&self, ident_prefix: &str) -> ExpectationTokens { 55 | let ident = format!("{}_{}", ident_prefix, self.identifier_string()); 56 | 57 | let before_variable_name = format!("{}_before", ident); 58 | let before_variable_ident = Ident::new(&before_variable_name, self.span()); 59 | 60 | let expr = &self.expression; 61 | 62 | let before_subject = quote_spanned! { expr.span() => 63 | let #before_variable_ident = #expr; 64 | }; 65 | 66 | let inner_tokens = self 67 | .inner 68 | .tokens(ident_prefix, &self.expression.to_token_stream()); 69 | 70 | let context = quote_spanned! { self.span() => 71 | let from_value = #before_variable_ident; 72 | }; 73 | 74 | let assertions = AssertionTokens::Group(GroupAssertionTokens::new( 75 | "change".to_string(), 76 | self.expression.to_token_stream().to_string(), 77 | None, 78 | Some(context), 79 | inner_tokens.assertions, 80 | )); 81 | 82 | ExpectationTokens { 83 | before_subject_evaluation: before_subject, 84 | assertions, 85 | } 86 | } 87 | 88 | pub fn dependencies(&self) -> HashSet { 89 | let mut dependencies = expr_dependencies(&self.expression); 90 | dependencies.extend(self.inner.dependencies()); 91 | dependencies 92 | } 93 | } 94 | 95 | impl Parse for ChangeExpectation { 96 | fn parse(input: ParseStream) -> syn::Result { 97 | input.parse::()?; 98 | let content; 99 | parenthesized!(content in input); 100 | let expr = content.parse::()?; 101 | 102 | let inner = input.parse::()?; 103 | 104 | Ok(Self::new(expr, Box::new(inner))) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /lets_expect_assertions/src/change.rs: -------------------------------------------------------------------------------- 1 | use colored::Colorize; 2 | 3 | use lets_expect_core::assertions::{ 4 | assertion_error::AssertionError, assertion_result::AssertionResult, 5 | }; 6 | use std::{fmt::Debug, ops::Sub}; 7 | 8 | pub fn from(expected: R) -> impl Fn(&R, &R) -> AssertionResult 9 | where 10 | R: Debug + PartialEq, 11 | { 12 | move |from, _| { 13 | if expected == *from { 14 | Ok(()) 15 | } else { 16 | let expected = format!("{:?}", expected).green().bold(); 17 | let from = format!("{:?}", from).red().bold(); 18 | Err(AssertionError { 19 | message: vec![format!( 20 | "Expected to change from {}, but it was {} instead", 21 | expected, from 22 | )], 23 | }) 24 | } 25 | } 26 | } 27 | 28 | pub fn to(expected: R) -> impl Fn(&R, &R) -> AssertionResult 29 | where 30 | R: Debug + PartialEq, 31 | { 32 | move |_, to| { 33 | if expected == *to { 34 | Ok(()) 35 | } else { 36 | let expected = format!("{:?}", expected).green().bold(); 37 | let to = format!("{:?}", to).red().bold(); 38 | Err(AssertionError { 39 | message: vec![format!( 40 | "Expected to change to {}, but it was {} instead", 41 | expected, to 42 | )], 43 | }) 44 | } 45 | } 46 | } 47 | 48 | pub fn by(expected: R) -> impl Fn(&R, &R) -> AssertionResult 49 | where 50 | R: Debug + PartialEq + Sub + Clone, 51 | { 52 | move |from, to| { 53 | let diff = to.clone() - from.clone(); 54 | if expected == diff { 55 | Ok(()) 56 | } else { 57 | let expected = format!("{:?}", expected).green().bold(); 58 | let diff = format!("{:?}", diff).red().bold(); 59 | Err(AssertionError { 60 | message: vec![format!( 61 | "Expected to change by {}, but it was changed by {} instead", 62 | expected, diff 63 | )], 64 | }) 65 | } 66 | } 67 | } 68 | 69 | #[cfg(test)] 70 | mod tests { 71 | use super::*; 72 | use crate::expected_err::expected_err; 73 | use colored::control::set_override; 74 | 75 | #[test] 76 | fn test_from_ok() { 77 | assert_eq!(from(1)(&1, &2), Ok(())); 78 | } 79 | 80 | #[test] 81 | fn test_from_err() { 82 | set_override(false); 83 | assert_eq!( 84 | from(1)(&2, &2), 85 | expected_err(vec!["Expected to change from 1, but it was 2 instead"]) 86 | ); 87 | } 88 | 89 | #[test] 90 | fn test_to_ok() { 91 | assert_eq!(to(2)(&1, &2), Ok(())); 92 | } 93 | 94 | #[test] 95 | fn test_to_err() { 96 | set_override(false); 97 | assert_eq!( 98 | to(2)(&1, &1), 99 | expected_err(vec!["Expected to change to 2, but it was 1 instead"]) 100 | ); 101 | } 102 | 103 | #[test] 104 | fn test_by_ok() { 105 | assert_eq!(by(1)(&1, &2), Ok(())); 106 | } 107 | 108 | #[test] 109 | fn test_by_err() { 110 | set_override(false); 111 | assert_eq!( 112 | by(1)(&1, &1), 113 | expected_err(vec![ 114 | "Expected to change by 1, but it was changed by 0 instead" 115 | ]) 116 | ); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /lets_expect_core/src/expectations/many.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use proc_macro2::{Span, TokenStream}; 4 | use syn::{parse::Parse, punctuated::Punctuated, Ident}; 5 | 6 | use super::{ 7 | expectation_tokens::{AssertionTokens, ExpectationTokens}, 8 | expectation_type::ExpectationType, 9 | }; 10 | 11 | pub(crate) struct ManyExpectation { 12 | identifier: Option, 13 | inner: Vec, 14 | content_span: Span, 15 | identifier_string: String, 16 | } 17 | 18 | impl ManyExpectation { 19 | pub fn new(identifier: Option, inner: Vec, content_span: Span) -> Self { 20 | let identifier_string = identifier.as_ref().map_or_else( 21 | || { 22 | { 23 | inner 24 | .iter() 25 | .map(|expectation| expectation.identifier_string()) 26 | .collect::>() 27 | .join("_and_") 28 | } 29 | }, 30 | |ident| ident.to_string(), 31 | ); 32 | 33 | Self { 34 | identifier, 35 | inner, 36 | content_span, 37 | identifier_string, 38 | } 39 | } 40 | pub fn peek(input: &syn::parse::ParseStream) -> bool { 41 | input.peek(syn::token::Brace) || (input.peek(syn::Ident) && input.peek2(syn::token::Brace)) 42 | } 43 | } 44 | 45 | impl ExpectationType for ManyExpectation { 46 | fn span(&self) -> Span { 47 | self.identifier 48 | .as_ref() 49 | .map_or_else(|| self.content_span, |identifier| identifier.span()) 50 | } 51 | 52 | fn identifier_string(&self) -> &str { 53 | &self.identifier_string 54 | } 55 | 56 | fn tokens( 57 | &self, 58 | ident_prefix: &str, 59 | subject_reference: bool, 60 | subject_mutable: bool, 61 | ) -> ExpectationTokens { 62 | let ident = format!("{}_{}", ident_prefix, self.identifier_string()); 63 | let mut before_subject = TokenStream::new(); 64 | let mut assertions: Vec = Vec::new(); 65 | 66 | self.inner.iter().for_each(|inner| { 67 | let inner_tokens = inner.tokens(&ident, subject_reference, subject_mutable); 68 | 69 | before_subject.extend(inner_tokens.before_subject_evaluation); 70 | assertions.push(inner_tokens.assertions); 71 | }); 72 | 73 | ExpectationTokens { 74 | before_subject_evaluation: before_subject, 75 | assertions: AssertionTokens::Many(assertions), 76 | } 77 | } 78 | 79 | fn dependencies(&self) -> HashSet { 80 | self.inner 81 | .iter() 82 | .flat_map(|inner| inner.dependencies()) 83 | .collect() 84 | } 85 | } 86 | 87 | impl Parse for ManyExpectation { 88 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 89 | let identifier = if input.peek(syn::Ident) { 90 | Some(input.parse::()?) 91 | } else { 92 | None 93 | }; 94 | 95 | let content; 96 | syn::braced!(content in input); 97 | let content_span = content.span(); 98 | 99 | let inner = Punctuated::::parse_terminated(&content)? 100 | .into_iter() 101 | .collect(); 102 | 103 | Ok(Self::new(identifier, inner, content_span)) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /lets_expect_core/src/expectations/inner.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use proc_macro2::Ident; 4 | use syn::parse::Parse; 5 | 6 | use super::{ 7 | be_err_and::BeErrAndExpectation, be_ok_and::BeOkAndExpectation, 8 | be_some_and::BeSomeAndExpectation, expectation_tokens::ExpectationTokens, 9 | expectation_type::ExpectationType, expression::ExpressionExpectation, have::HaveExpectation, 10 | many::ManyExpectation, 11 | }; 12 | 13 | pub(crate) enum InnerExpectation { 14 | Expression(ExpressionExpectation), 15 | Many(ManyExpectation), 16 | Have(HaveExpectation), 17 | BeSomeAnd(BeSomeAndExpectation), 18 | BeOkAndAnd(BeOkAndExpectation), 19 | BeErrAnd(BeErrAndExpectation), 20 | } 21 | 22 | impl Parse for InnerExpectation { 23 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 24 | if HaveExpectation::peek(&input) { 25 | Ok(Self::Have(input.parse::()?)) 26 | } else if ManyExpectation::::peek(&input) { 27 | Ok(Self::Many(input.parse::>()?)) 28 | } else if BeSomeAndExpectation::peek(&input) { 29 | Ok(Self::BeSomeAnd(input.parse::()?)) 30 | } else if BeOkAndExpectation::peek(&input) { 31 | Ok(Self::BeOkAndAnd(input.parse::()?)) 32 | } else if BeErrAndExpectation::peek(&input) { 33 | Ok(Self::BeErrAnd(input.parse::()?)) 34 | } else { 35 | Ok(Self::Expression(input.parse::()?)) 36 | } 37 | } 38 | } 39 | 40 | impl ExpectationType for InnerExpectation { 41 | fn span(&self) -> proc_macro2::Span { 42 | match self { 43 | Self::Expression(expectation) => expectation.span(), 44 | Self::Many(expectation) => expectation.span(), 45 | Self::Have(expectation) => expectation.span(), 46 | Self::BeSomeAnd(expectation) => expectation.span(), 47 | Self::BeOkAndAnd(expectation) => expectation.span(), 48 | Self::BeErrAnd(expectation) => expectation.span(), 49 | } 50 | } 51 | 52 | fn identifier_string(&self) -> &str { 53 | match self { 54 | Self::Expression(expectation) => expectation.identifier_string(), 55 | Self::Many(expectation) => expectation.identifier_string(), 56 | Self::Have(expectation) => expectation.identifier_string(), 57 | Self::BeSomeAnd(expectation) => expectation.identifier_string(), 58 | Self::BeOkAndAnd(expectation) => expectation.identifier_string(), 59 | Self::BeErrAnd(expectation) => expectation.identifier_string(), 60 | } 61 | } 62 | 63 | fn tokens( 64 | &self, 65 | ident_prefix: &str, 66 | subject_reference: bool, 67 | subject_mutable: bool, 68 | ) -> ExpectationTokens { 69 | match self { 70 | Self::Expression(expectation) => { 71 | expectation.tokens(ident_prefix, subject_reference, subject_mutable) 72 | } 73 | Self::Many(expectation) => { 74 | expectation.tokens(ident_prefix, subject_reference, subject_mutable) 75 | } 76 | Self::Have(expectation) => expectation.tokens(ident_prefix), 77 | Self::BeSomeAnd(expectation) => expectation.tokens(ident_prefix), 78 | Self::BeOkAndAnd(expectation) => expectation.tokens(ident_prefix), 79 | Self::BeErrAnd(expectation) => expectation.tokens(ident_prefix), 80 | } 81 | } 82 | 83 | fn dependencies(&self) -> HashSet { 84 | match self { 85 | Self::Expression(expectation) => expectation.dependencies(), 86 | Self::Many(expectation) => expectation.dependencies(), 87 | Self::Have(expectation) => expectation.dependencies(), 88 | Self::BeSomeAnd(expectation) => expectation.dependencies(), 89 | Self::BeOkAndAnd(expectation) => expectation.dependencies(), 90 | Self::BeErrAnd(expectation) => expectation.dependencies(), 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /lets_expect_core/src/expectations/have.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use proc_macro2::{Ident, Span}; 4 | use quote::{quote_spanned, ToTokens}; 5 | use syn::{ 6 | parse::{Parse, ParseStream}, 7 | spanned::Spanned, 8 | }; 9 | 10 | use crate::{ 11 | core::keyword, 12 | utils::{ 13 | expr_dependencies::expr_dependencies, 14 | mutable_token::mutable_token, 15 | parse_expression::{parse_expectation_expression, ExpectationExpression}, 16 | reference_token::reference_token, 17 | to_ident::expr_to_ident, 18 | }, 19 | }; 20 | 21 | use super::{ 22 | expectation_tokens::{AssertionTokens, ExpectationTokens, GroupAssertionTokens}, 23 | expectation_type::ExpectationType, 24 | inner::InnerExpectation, 25 | }; 26 | 27 | pub(crate) struct HaveExpectation { 28 | expectation_expression: ExpectationExpression, 29 | inner: Box, 30 | identifier: String, 31 | } 32 | 33 | impl HaveExpectation { 34 | pub fn new( 35 | expectation_expression: ExpectationExpression, 36 | inner: Box, 37 | ) -> Self { 38 | let expr_ident = expr_to_ident(&expectation_expression.expr); 39 | 40 | let ref_string = if expectation_expression.reference { 41 | "ref_" 42 | } else { 43 | "" 44 | }; 45 | 46 | let mutable_string = if expectation_expression.mutable { 47 | "mut_" 48 | } else { 49 | "" 50 | }; 51 | 52 | let identifier = format!( 53 | "have_{}{}{}_{}", 54 | ref_string, 55 | mutable_string, 56 | expr_ident, 57 | inner.identifier_string() 58 | ); 59 | 60 | Self { 61 | expectation_expression, 62 | inner, 63 | identifier, 64 | } 65 | } 66 | pub fn peek(input: &ParseStream) -> bool { 67 | input.peek(keyword::have) 68 | } 69 | 70 | pub fn span(&self) -> Span { 71 | self.expectation_expression.expr.span() 72 | } 73 | 74 | pub fn identifier_string(&self) -> &str { 75 | &self.identifier 76 | } 77 | 78 | pub fn tokens(&self, ident_prefix: &str) -> ExpectationTokens { 79 | let inner_tokens = self.inner.tokens( 80 | ident_prefix, 81 | self.expectation_expression.reference, 82 | self.expectation_expression.mutable, 83 | ); 84 | let before_subject = inner_tokens.before_subject_evaluation; 85 | 86 | let expr = &self.expectation_expression.expr; 87 | 88 | let reference = reference_token(self.expectation_expression.reference, &expr.span()); 89 | let mutable = mutable_token(self.expectation_expression.mutable, &expr.span()); 90 | let context = quote_spanned! { expr.span() => 91 | let #mutable subject = #reference subject.#expr; 92 | }; 93 | 94 | let assertions = AssertionTokens::Group(GroupAssertionTokens::new( 95 | "have".to_string(), 96 | self.expectation_expression 97 | .expr 98 | .to_token_stream() 99 | .to_string(), 100 | None, 101 | Some(context), 102 | inner_tokens.assertions, 103 | )); 104 | 105 | ExpectationTokens { 106 | before_subject_evaluation: before_subject, 107 | assertions, 108 | } 109 | } 110 | 111 | pub fn dependencies(&self) -> HashSet { 112 | expr_dependencies(&self.expectation_expression.expr) 113 | .into_iter() 114 | .chain(self.inner.dependencies().into_iter()) 115 | .collect() 116 | } 117 | } 118 | 119 | impl Parse for HaveExpectation { 120 | fn parse(input: ParseStream) -> syn::Result { 121 | input.parse::()?; 122 | let expectation_expression = parse_expectation_expression(input)?; 123 | 124 | let inner = input.parse::()?; 125 | 126 | Ok(Self::new(expectation_expression, Box::new(inner))) 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /lets_expect_core/src/expectations/make.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use proc_macro2::{Ident, Span}; 4 | use quote::{quote_spanned, ToTokens}; 5 | use syn::{ 6 | parse::{Parse, ParseStream}, 7 | spanned::Spanned, 8 | }; 9 | 10 | use crate::{ 11 | core::keyword, 12 | utils::{ 13 | expr_dependencies::expr_dependencies, 14 | mutable_token::mutable_token, 15 | parse_expression::{parse_expectation_expression, ExpectationExpression}, 16 | reference_token::reference_token, 17 | to_ident::expr_to_ident, 18 | }, 19 | }; 20 | 21 | use super::{ 22 | expectation_tokens::{AssertionTokens, GroupAssertionTokens}, 23 | expectation_type::ExpectationType, 24 | inner::InnerExpectation, 25 | }; 26 | 27 | use crate::expectations::expectation_tokens::ExpectationTokens; 28 | 29 | pub(crate) struct MakeExpectation { 30 | expectation_expression: ExpectationExpression, 31 | inner: Box, 32 | identifier_string: String, 33 | } 34 | 35 | impl MakeExpectation { 36 | pub fn new( 37 | expectation_expression: ExpectationExpression, 38 | inner: Box, 39 | ) -> Self { 40 | let expr_ident = expr_to_ident(&expectation_expression.expr); 41 | 42 | let ref_string = if expectation_expression.reference { 43 | "ref_" 44 | } else { 45 | "" 46 | }; 47 | 48 | let mutable_string = if expectation_expression.mutable { 49 | "mut_" 50 | } else { 51 | "" 52 | }; 53 | 54 | let identifier_string = format!( 55 | "make_{}{}{}_{}", 56 | ref_string, 57 | mutable_string, 58 | expr_ident, 59 | inner.identifier_string() 60 | ); 61 | 62 | Self { 63 | expectation_expression, 64 | inner, 65 | identifier_string, 66 | } 67 | } 68 | pub fn peek(input: &ParseStream) -> bool { 69 | input.peek(keyword::make) 70 | } 71 | 72 | pub fn span(&self) -> Span { 73 | self.expectation_expression.expr.span() 74 | } 75 | 76 | pub fn identifier_string(&self) -> &str { 77 | &self.identifier_string 78 | } 79 | 80 | pub(crate) fn tokens(&self, ident_prefix: &str) -> ExpectationTokens { 81 | let inner_tokens = self.inner.tokens( 82 | ident_prefix, 83 | self.expectation_expression.reference, 84 | self.expectation_expression.mutable, 85 | ); 86 | let before_subject = inner_tokens.before_subject_evaluation; 87 | let expr = &self.expectation_expression.expr; 88 | 89 | let reference = reference_token(self.expectation_expression.reference, &expr.span()); 90 | let mutable = mutable_token(self.expectation_expression.mutable, &expr.span()); 91 | 92 | let context = quote_spanned! { expr.span() => 93 | let #mutable subject = #reference #expr; 94 | }; 95 | let assertions = AssertionTokens::Group(GroupAssertionTokens::new( 96 | "make".to_string(), 97 | self.expectation_expression 98 | .expr 99 | .to_token_stream() 100 | .to_string(), 101 | None, 102 | Some(context), 103 | inner_tokens.assertions, 104 | )); 105 | ExpectationTokens { 106 | before_subject_evaluation: before_subject, 107 | assertions, 108 | } 109 | } 110 | 111 | pub fn dependencies(&self) -> HashSet { 112 | expr_dependencies(&self.expectation_expression.expr) 113 | .into_iter() 114 | .chain(self.inner.dependencies().into_iter()) 115 | .collect() 116 | } 117 | } 118 | 119 | impl Parse for MakeExpectation { 120 | fn parse(input: ParseStream) -> syn::Result { 121 | input.parse::()?; 122 | let expectation_expression = parse_expectation_expression(input)?; 123 | 124 | let inner = input.parse::()?; 125 | 126 | Ok(Self::new(expectation_expression, Box::new(inner))) 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /lets_expect_core/src/core/when.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Ident, Span, TokenStream}; 2 | use syn::punctuated::Punctuated; 3 | use syn::spanned::Spanned; 4 | use syn::token::{Brace, Comma, Paren}; 5 | use syn::{braced, parenthesized, parse::Parse}; 6 | 7 | use crate::utils::to_ident::local_to_ident; 8 | 9 | use super::context::Context; 10 | use super::create_module::create_module; 11 | use super::keyword; 12 | use super::runtime::Runtime; 13 | use syn::{Attribute, Expr, Local, Pat, Type}; 14 | use syn::{PatType, Token}; 15 | 16 | const WHEN_IDENT_PREFIX: &str = "when_"; 17 | 18 | struct WhenLet { 19 | pub attrs: Vec, 20 | pub pat: Pat, 21 | pub init: (Token![=], Box), 22 | } 23 | 24 | impl Parse for WhenLet { 25 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 26 | let attrs = input.call(Attribute::parse_outer)?; 27 | let mut pat = input.parse()?; 28 | 29 | if input.peek(Token![:]) { 30 | let colon_token: Token![:] = input.parse()?; 31 | let ty: Type = input.parse()?; 32 | pat = Pat::Type(PatType { 33 | attrs: Vec::new(), 34 | pat: Box::new(pat), 35 | colon_token, 36 | ty: Box::new(ty), 37 | }); 38 | } 39 | 40 | let init = (input.parse()?, input.parse()?); 41 | Ok(Self { attrs, pat, init }) 42 | } 43 | } 44 | 45 | impl WhenLet { 46 | pub fn to_local(&self) -> Local { 47 | Local { 48 | attrs: self.attrs.clone(), 49 | let_token: Default::default(), 50 | pat: self.pat.clone(), 51 | init: Some(self.init.clone()), 52 | semi_token: Default::default(), 53 | } 54 | } 55 | } 56 | 57 | pub struct When { 58 | context: Context, 59 | identifier: Ident, 60 | string: String, 61 | lets: Vec, 62 | } 63 | 64 | impl Parse for When { 65 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 66 | let (lets, identifier, string) = if input.peek(Paren) { 67 | parse_lets_in_parentheses(input)? 68 | } else { 69 | let identifier = input.parse::()?; 70 | ( 71 | Vec::new(), 72 | Ident::new( 73 | &format!("{}{}", WHEN_IDENT_PREFIX, identifier), 74 | identifier.span(), 75 | ), 76 | identifier.to_string(), 77 | ) 78 | }; 79 | 80 | let identifier = if input.peek(Token![as]) { 81 | input.parse::()?; 82 | let ident = input.parse::()?; 83 | Ident::new(&format!("{}{}", WHEN_IDENT_PREFIX, ident), ident.span()) 84 | } else { 85 | identifier 86 | }; 87 | 88 | let context = if input.peek(Brace) { 89 | let content; 90 | braced!(content in input); 91 | content.parse::()? 92 | } else { 93 | Context::from_single_item(input)? 94 | }; 95 | 96 | Ok(Self { 97 | lets, 98 | identifier, 99 | string, 100 | context, 101 | }) 102 | } 103 | } 104 | 105 | fn parse_lets_in_parentheses( 106 | input: &syn::parse::ParseBuffer, 107 | ) -> Result<(Vec, Ident, String), syn::Error> { 108 | let content; 109 | parenthesized!(content in input); 110 | 111 | let string = content.to_string(); 112 | let when_lets: Punctuated = Punctuated::parse_separated_nonempty(&content)?; 113 | let lets: Vec = when_lets.iter().map(WhenLet::to_local).collect(); 114 | 115 | if lets.is_empty() { 116 | return Err(syn::Error::new( 117 | Span::call_site(), 118 | "Expected at least one assignment", 119 | )); 120 | } 121 | 122 | let name = WHEN_IDENT_PREFIX.to_string() 123 | + lets 124 | .iter() 125 | .map(local_to_ident) 126 | .collect::>() 127 | .join("_") 128 | .as_str(); 129 | let identifier = Ident::new(name.as_str(), input.span()); 130 | Ok((lets, identifier, string)) 131 | } 132 | 133 | impl When { 134 | pub fn to_tokens(&self, keyword: &keyword::when, runtime: &Runtime) -> TokenStream { 135 | let runtime = runtime.add_when(self.string.clone()).add_lets(&self.lets); 136 | let context = self.context.to_tokens(&keyword.span(), &runtime); 137 | create_module(&keyword.span(), &self.identifier, &context) 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /lets_expect_core/src/core/create_test.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use proc_macro2::{Ident, TokenStream}; 4 | use quote::quote_spanned; 5 | use syn::Local; 6 | 7 | use crate::utils::expr_dependencies::{block_dependencies, expr_dependencies}; 8 | 9 | use super::{ 10 | ident_from_pat::ident_from_pat, 11 | mode::Mode, 12 | runtime::Runtime, 13 | topological_sort::{topological_sort, TopologicalSortError}, 14 | }; 15 | 16 | pub fn create_test( 17 | identifier: &Ident, 18 | runtime: &Runtime, 19 | content: &TokenStream, 20 | dependencies: &HashSet, 21 | ) -> TokenStream { 22 | let befores = &runtime.befores; 23 | let afters = &runtime.afters; 24 | let before_dependencies: HashSet = befores.iter().flat_map(block_dependencies).collect(); 25 | let after_dependencies: HashSet = afters.iter().flat_map(block_dependencies).collect(); 26 | 27 | let mut used_lets = HashSet::new(); 28 | 29 | for dependency in dependencies 30 | .iter() 31 | .chain(before_dependencies.iter()) 32 | .chain(after_dependencies.iter()) 33 | { 34 | recursive_dependencies(&runtime.lets, dependency, &mut used_lets); 35 | } 36 | 37 | let used: Vec = runtime 38 | .lets 39 | .iter() 40 | .cloned() 41 | .filter(|l| { 42 | let ident = ident_from_pat(&l.pat).unwrap(); 43 | used_lets.contains(&ident) 44 | }) 45 | .collect(); 46 | 47 | let lets = topological_sort(&used); 48 | 49 | let lets = match lets { 50 | Ok(lets) => lets, 51 | Err(error) => { 52 | return match error { 53 | TopologicalSortError::CyclicDependency(idents) => { 54 | let error_message = format!( 55 | "Cyclic dependency between variables detected: {}", 56 | idents 57 | .iter() 58 | .map(Ident::to_string) 59 | .collect::>() 60 | .join(", ") 61 | ); 62 | 63 | quote_spanned! { identifier.span() => 64 | compile_error!(#error_message); 65 | } 66 | } 67 | TopologicalSortError::IdentExpected => { 68 | quote_spanned! { identifier.span() => 69 | compile_error!("Expected an identifier in `let`"); 70 | } 71 | } 72 | } 73 | } 74 | }; 75 | 76 | let test_declaration = test_declaration(identifier, runtime.mode.unwrap_or(Mode::Test)); 77 | 78 | quote_spanned! { identifier.span() => 79 | #test_declaration { 80 | #(#lets)* 81 | 82 | #(#befores)* 83 | 84 | let test_cases = { 85 | #content 86 | }; 87 | 88 | #(#afters)* 89 | 90 | test_result_from_cases(test_cases) 91 | } 92 | } 93 | } 94 | 95 | fn recursive_dependencies(lets: &[Local], ident: &Ident, dependencies: &mut HashSet) { 96 | if !dependencies.contains(ident) { 97 | let r#let = lets.iter().find(|l| { 98 | let let_ident = ident_from_pat(&l.pat).expect("Expected an identifier"); 99 | let_ident == *ident 100 | }); 101 | 102 | if let Some(r#let) = r#let { 103 | dependencies.insert(ident.clone()); 104 | 105 | let let_dependencies = expr_dependencies(&r#let.init.as_ref().unwrap().1); 106 | 107 | for dependency in let_dependencies { 108 | recursive_dependencies(lets, &dependency, dependencies); 109 | } 110 | } 111 | } 112 | } 113 | 114 | fn test_declaration(identifier: &Ident, mode: Mode) -> TokenStream { 115 | match mode { 116 | Mode::Test => quote_spanned! { identifier.span() => 117 | #[test] 118 | fn #identifier() -> Result<(), TestFailure> 119 | }, 120 | Mode::PubMethod => quote_spanned! { identifier.span() => 121 | pub fn #identifier() -> Result<(), TestFailure> 122 | }, 123 | Mode::PubAsyncMethod => quote_spanned! { identifier.span() => 124 | pub async fn #identifier() -> Result<(), TestFailure> 125 | }, 126 | #[cfg(feature = "tokio")] 127 | Mode::TokioTest => quote_spanned! { identifier.span() => 128 | #[tokio::test] 129 | async fn #identifier() -> Result<(), TestFailure> 130 | }, 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /lets_expect_core/src/expectations/return_value.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use proc_macro2::Ident; 4 | use syn::parse::{Parse, ParseStream}; 5 | 6 | use super::{ 7 | be_err_and::BeErrAndExpectation, be_ok_and::BeOkAndExpectation, 8 | be_some_and::BeSomeAndExpectation, change::ChangeExpectation, 9 | expectation_tokens::ExpectationTokens, expectation_type::ExpectationType, 10 | expression::ExpressionExpectation, have::HaveExpectation, make::MakeExpectation, 11 | many::ManyExpectation, not_change::NotChangeExpectation, 12 | }; 13 | 14 | pub(crate) enum ReturnValueExpectation { 15 | Expression(ExpressionExpectation), 16 | Many(ManyExpectation), 17 | Have(HaveExpectation), 18 | Make(MakeExpectation), 19 | Change(ChangeExpectation), 20 | NotChange(NotChangeExpectation), 21 | BeSomeAnd(BeSomeAndExpectation), 22 | BeOkAndAnd(BeOkAndExpectation), 23 | BeErrAnd(BeErrAndExpectation), 24 | } 25 | 26 | impl Parse for ReturnValueExpectation { 27 | fn parse(input: ParseStream) -> syn::Result { 28 | if HaveExpectation::peek(&input) { 29 | Ok(Self::Have(input.parse::()?)) 30 | } else if MakeExpectation::peek(&input) { 31 | Ok(Self::Make(input.parse::()?)) 32 | } else if ChangeExpectation::peek(&input) { 33 | Ok(Self::Change(input.parse::()?)) 34 | } else if NotChangeExpectation::peek(&input) { 35 | Ok(Self::NotChange(input.parse::()?)) 36 | } else if ManyExpectation::::peek(&input) { 37 | Ok(Self::Many(input.parse::>()?)) 38 | } else if BeSomeAndExpectation::peek(&input) { 39 | Ok(Self::BeSomeAnd(input.parse::()?)) 40 | } else if BeOkAndExpectation::peek(&input) { 41 | Ok(Self::BeOkAndAnd(input.parse::()?)) 42 | } else if BeErrAndExpectation::peek(&input) { 43 | Ok(Self::BeErrAnd(input.parse::()?)) 44 | } else { 45 | Ok(Self::Expression(input.parse::()?)) 46 | } 47 | } 48 | } 49 | 50 | impl ExpectationType for ReturnValueExpectation { 51 | fn span(&self) -> proc_macro2::Span { 52 | match self { 53 | Self::Expression(expectation) => expectation.span(), 54 | Self::Many(expectation) => expectation.span(), 55 | Self::Have(expectation) => expectation.span(), 56 | Self::Make(expectation) => expectation.span(), 57 | Self::Change(expectation) => expectation.span(), 58 | Self::NotChange(expectation) => expectation.span(), 59 | Self::BeSomeAnd(expectation) => expectation.span(), 60 | Self::BeOkAndAnd(expectation) => expectation.span(), 61 | Self::BeErrAnd(expectation) => expectation.span(), 62 | } 63 | } 64 | 65 | fn identifier_string(&self) -> &str { 66 | match self { 67 | Self::Expression(expectation) => expectation.identifier_string(), 68 | Self::Many(expectation) => expectation.identifier_string(), 69 | Self::Have(expectation) => expectation.identifier_string(), 70 | Self::Make(expectation) => expectation.identifier_string(), 71 | Self::Change(expectation) => expectation.identifier_string(), 72 | Self::NotChange(expectation) => expectation.identifier_string(), 73 | Self::BeSomeAnd(expectation) => expectation.identifier_string(), 74 | Self::BeOkAndAnd(expectation) => expectation.identifier_string(), 75 | Self::BeErrAnd(expectation) => expectation.identifier_string(), 76 | } 77 | } 78 | 79 | fn dependencies(&self) -> HashSet { 80 | match self { 81 | Self::Expression(expectation) => expectation.dependencies(), 82 | Self::Many(expectation) => expectation.dependencies(), 83 | Self::Have(expectation) => expectation.dependencies(), 84 | Self::Make(expectation) => expectation.dependencies(), 85 | Self::Change(expectation) => expectation.dependencies(), 86 | Self::NotChange(expectation) => expectation.dependencies(), 87 | Self::BeSomeAnd(expectation) => expectation.dependencies(), 88 | Self::BeOkAndAnd(expectation) => expectation.dependencies(), 89 | Self::BeErrAnd(expectation) => expectation.dependencies(), 90 | } 91 | } 92 | 93 | fn tokens( 94 | &self, 95 | ident_prefix: &str, 96 | subject_reference: bool, 97 | subject_mutable: bool, 98 | ) -> ExpectationTokens { 99 | match self { 100 | Self::Expression(expectation) => { 101 | expectation.tokens(ident_prefix, subject_reference, subject_mutable) 102 | } 103 | Self::Many(expectation) => { 104 | expectation.tokens(ident_prefix, subject_reference, subject_mutable) 105 | } 106 | Self::Have(expectation) => expectation.tokens(ident_prefix), 107 | Self::Make(expectation) => expectation.tokens(ident_prefix), 108 | Self::Change(expectation) => expectation.tokens(ident_prefix), 109 | Self::NotChange(expectation) => expectation.tokens(ident_prefix), 110 | Self::BeSomeAnd(expectation) => expectation.tokens(ident_prefix), 111 | Self::BeOkAndAnd(expectation) => expectation.tokens(ident_prefix), 112 | Self::BeErrAnd(expectation) => expectation.tokens(ident_prefix), 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /lets_expect_core/src/core/to.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use proc_macro2::TokenStream; 4 | use syn::parse::{Parse, ParseStream}; 5 | 6 | use syn::spanned::Spanned; 7 | 8 | use syn::Ident; 9 | 10 | use crate::expectations::expectation::Expectation; 11 | use crate::expectations::expectation_tokens::AssertionTokens; 12 | use crate::utils::expr_dependencies::expr_dependencies; 13 | use crate::utils::mutable_token::mutable_token; 14 | 15 | use super::runtime::Runtime; 16 | use crate::expectations::expectation_type::ExpectationType; 17 | use quote::{quote, quote_spanned, ToTokens}; 18 | 19 | const TEST_NAME_PREFIX: &str = "to_"; 20 | 21 | pub struct To { 22 | expectation: Expectation, 23 | } 24 | 25 | impl Parse for To { 26 | fn parse(input: ParseStream) -> syn::Result { 27 | Ok(Self { 28 | expectation: input.parse::()?, 29 | }) 30 | } 31 | } 32 | 33 | impl To { 34 | pub fn identifier(&self) -> Ident { 35 | Ident::new( 36 | format!( 37 | "{}{}", 38 | TEST_NAME_PREFIX, 39 | &self.expectation.identifier_string() 40 | ) 41 | .as_str(), 42 | self.expectation.span(), 43 | ) 44 | } 45 | 46 | pub fn to_tokens(&self, runtime: &Runtime) -> (TokenStream, HashSet) { 47 | let identifier = self.identifier(); 48 | let subject = runtime.subject.as_ref().expect("No subject set"); 49 | 50 | let expectation_tokens = self.expectation.tokens("", false, subject.0); 51 | let before_subject = expectation_tokens.before_subject_evaluation; 52 | 53 | let subject_label = subject.1.to_token_stream().to_string(); 54 | let subject_tokens = self.subject_tokens(subject, &identifier, self.expectation.is_panic()); 55 | let subject_dependencies = expr_dependencies(&subject.1); 56 | 57 | let expectation_dependencies = self.expectation.dependencies(); 58 | 59 | let expectation = self.assertion_tokens(&expectation_tokens.assertions); 60 | 61 | let dependencies = subject_dependencies 62 | .union(&expectation_dependencies) 63 | .cloned() 64 | .collect::>(); 65 | 66 | let whens = &runtime.whens; 67 | 68 | let token_stream = quote_spanned! { identifier.span() => 69 | #before_subject 70 | 71 | #[allow(unused_variables)] 72 | #subject_tokens 73 | 74 | let expectation_result = #expectation; 75 | 76 | ExecutedTestCase::new(#subject_label.to_string(), vec![#(#whens),*], expectation_result) 77 | }; 78 | 79 | (token_stream, dependencies) 80 | } 81 | 82 | fn subject_tokens( 83 | &self, 84 | subject: &(bool, syn::Expr), 85 | identifier: &Ident, 86 | is_panic: bool, 87 | ) -> TokenStream { 88 | let mutable_token = mutable_token(subject.0, &subject.1.span()); 89 | let subject = &subject.1; 90 | 91 | if is_panic { 92 | quote_spanned! { identifier.span() => 93 | #[allow(clippy::no_effect)] 94 | let subject = std::panic::catch_unwind(|| { #subject; }); 95 | } 96 | } else { 97 | quote_spanned! { identifier.span() => 98 | #[allow(clippy::let_unit_value)] 99 | let #mutable_token subject = #subject; 100 | } 101 | } 102 | } 103 | 104 | fn assertion_tokens(&self, tokens: &AssertionTokens) -> TokenStream { 105 | match tokens { 106 | AssertionTokens::Single(assertion) => { 107 | let assertion_label = &assertion.expression; 108 | let assertion = &assertion.assertion; 109 | 110 | quote! { 111 | { 112 | let result = #assertion; 113 | ExecutedExpectation::Single(ExecutedAssertion::new(#assertion_label.to_string(), result)) 114 | } 115 | } 116 | } 117 | AssertionTokens::Group(tokens) => { 118 | let assertion_tokens = self.assertion_tokens(&tokens.inner); 119 | 120 | let label = &tokens.label; 121 | let arg = &tokens.argument; 122 | let assertion_tokens = if let Some(context) = tokens.context.as_ref() { 123 | quote_spanned! { self.expectation.span() => 124 | { 125 | #context 126 | Box::new(#assertion_tokens) 127 | } 128 | } 129 | } else { 130 | quote_spanned! { self.expectation.span() => 131 | Box::new(#assertion_tokens) 132 | } 133 | }; 134 | 135 | if let Some(guard) = &tokens.guard { 136 | quote_spanned! { self.expectation.span() => 137 | if #guard { 138 | ExecutedExpectation::Group(#label.to_string(), #arg.to_string(), #assertion_tokens) 139 | } else { 140 | ExecutedExpectation::Single(ExecutedAssertion::new(#label.to_string(), Err(AssertionError::new(vec!["Guard failed".to_string()])))) 141 | } 142 | } 143 | } else { 144 | quote_spanned! { self.expectation.span() => 145 | ExecutedExpectation::Group(#label.to_string(), #arg.to_string(), #assertion_tokens) 146 | } 147 | } 148 | } 149 | AssertionTokens::Many(assertions) => { 150 | let assertions = assertions 151 | .iter() 152 | .map(|tokens| self.assertion_tokens(tokens)) 153 | .collect::>(); 154 | 155 | quote! { 156 | ExecutedExpectation::Many( 157 | vec![ 158 | #(#assertions),* 159 | ] 160 | ) 161 | } 162 | } 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /lets_expect_core/src/core/context.rs: -------------------------------------------------------------------------------- 1 | use super::{ 2 | after_block::AfterBlock, before_block::BeforeBlock, create_test::create_test, expect::Expect, 3 | expect_block::ExpectBlock, keyword, mode::Mode, runtime::Runtime, story::Story, 4 | story_block::StoryBlock, to::To, to_block::ToBlock, when::When, when_block::WhenBlock, 5 | }; 6 | use proc_macro2::{Span, TokenStream}; 7 | use quote::quote_spanned; 8 | use syn::{ 9 | parse::{Parse, ParseBuffer, ParseStream}, 10 | spanned::Spanned, 11 | Block, Error, Ident, Local, Stmt, Token, 12 | }; 13 | 14 | #[derive(Default)] 15 | pub struct Context { 16 | lets: Vec, 17 | tos: Vec, 18 | 19 | befores: Vec, 20 | afters: Vec, 21 | 22 | expects: Vec, 23 | whens: Vec, 24 | stories: Vec, 25 | 26 | mode: Option, 27 | } 28 | 29 | impl Parse for Context { 30 | fn parse(input: ParseStream) -> syn::Result { 31 | let mut context = Self::default(); 32 | 33 | if input.peek(Token![#]) { 34 | input.parse::()?; 35 | let mode_ident = input.parse::()?; 36 | 37 | context.mode = Some(match mode_ident.to_string().as_str() { 38 | "test" => Mode::Test, 39 | "method" => Mode::PubMethod, 40 | "method_async" => Mode::PubAsyncMethod, 41 | #[cfg(feature = "tokio")] 42 | "tokio_test" => Mode::TokioTest, 43 | _ => return Err(Error::new(mode_ident.span(), "Unknown mode")), 44 | }); 45 | } 46 | 47 | while !input.is_empty() { 48 | parse_single_context_item(input, &mut context)?; 49 | } 50 | 51 | Ok(context) 52 | } 53 | } 54 | 55 | fn parse_single_context_item(input: &ParseBuffer, context: &mut Context) -> Result<(), Error> { 56 | let next = input.lookahead1(); 57 | 58 | if next.peek(Token![let]) { 59 | handle_let(&mut context.lets, input)?; 60 | } else if next.peek(keyword::before) { 61 | let keyword = input.parse::()?; 62 | let before = handle_before(keyword, input)?; 63 | context.befores.push(before); 64 | } else if next.peek(keyword::after) { 65 | let keyword = input.parse::()?; 66 | let after = handle_after(keyword, input)?; 67 | context.afters.push(after); 68 | } else if next.peek(keyword::to) { 69 | let keyword = input.parse::()?; 70 | let to = handle_to(keyword, input)?; 71 | context.tos.push(to); 72 | } else if next.peek(keyword::when) { 73 | let keyword = input.parse::()?; 74 | let when = handle_when(keyword, input)?; 75 | context.whens.push(when); 76 | } else if next.peek(keyword::expect) { 77 | let keyword = input.parse::()?; 78 | let expect = handle_expect(keyword, input)?; 79 | context.expects.push(expect); 80 | } else if next.peek(keyword::story) { 81 | let keyword = input.parse::()?; 82 | let story = handle_story(keyword, input)?; 83 | context.stories.push(story); 84 | } else { 85 | return Err(next.error()); 86 | } 87 | 88 | Ok(()) 89 | } 90 | 91 | fn handle_before(keyword: keyword::before, input: ParseStream) -> syn::Result { 92 | let block = input.parse::()?; 93 | Ok(BeforeBlock::new(keyword, block)) 94 | } 95 | 96 | fn handle_after(keyword: keyword::after, input: ParseStream) -> syn::Result { 97 | let block = input.parse::()?; 98 | Ok(AfterBlock::new(keyword, block)) 99 | } 100 | 101 | fn handle_expect(keyword: keyword::expect, input: &ParseBuffer) -> syn::Result { 102 | let expect = input.parse::()?; 103 | Ok(ExpectBlock::new(keyword, expect)) 104 | } 105 | 106 | fn handle_when(keyword: keyword::when, input: &ParseBuffer) -> syn::Result { 107 | let when = input.parse::()?; 108 | Ok(WhenBlock::new(keyword, when)) 109 | } 110 | 111 | fn handle_to(keyword: keyword::to, input: &ParseBuffer) -> syn::Result { 112 | let to = input.parse::()?; 113 | Ok(ToBlock::new(keyword, to)) 114 | } 115 | 116 | fn handle_let(lets: &mut Vec, input: &ParseBuffer) -> syn::Result<()> { 117 | let r#let = input.parse::()?; 118 | 119 | match r#let { 120 | Stmt::Local(local) => { 121 | lets.push(local); 122 | } 123 | _ => return Err(Error::new(r#let.span(), "Expected a `let` statement")), 124 | } 125 | Ok(()) 126 | } 127 | 128 | fn handle_story(keyword: keyword::story, input: &ParseBuffer) -> Result { 129 | let story = input.parse::()?; 130 | Ok(StoryBlock::new(keyword, story)) 131 | } 132 | 133 | impl Context { 134 | pub fn from_single_item(input: ParseStream) -> syn::Result { 135 | let mut context = Self::default(); 136 | 137 | parse_single_context_item(input, &mut context)?; 138 | 139 | Ok(context) 140 | } 141 | 142 | pub fn to_tokens(&self, span: &Span, runtime: &Runtime) -> TokenStream { 143 | let runtime = runtime.extend( 144 | None, 145 | &self.lets, 146 | &self 147 | .befores 148 | .iter() 149 | .map(|before| before.before.clone()) 150 | .collect::>(), 151 | &self 152 | .afters 153 | .iter() 154 | .map(|before| before.after.clone()) 155 | .collect::>(), 156 | self.mode, 157 | ); 158 | 159 | let tos = self.tos.iter().map(|to| { 160 | let (to_tokens, dependencies) = to.to_tokens(&runtime); 161 | let identifier = to.identifier(); 162 | 163 | let content = quote_spanned! { identifier.span() => 164 | let test_case = { 165 | #to_tokens 166 | }; 167 | 168 | vec![test_case] 169 | }; 170 | 171 | create_test(&identifier, &runtime, &content, &dependencies) 172 | }); 173 | 174 | let stories = self.stories.iter().map(|story| story.to_tokens(&runtime)); 175 | let expects = self.expects.iter().map(|child| child.to_tokens(&runtime)); 176 | let whens = self.whens.iter().map(|child| child.to_tokens(&runtime)); 177 | 178 | quote_spanned! { *span => 179 | #(#tos)* 180 | #(#stories)* 181 | #(#expects)* 182 | #(#whens)* 183 | } 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /lets_expect_assertions/src/partial_ord.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fmt::Debug, 3 | ops::{Add, Sub}, 4 | }; 5 | 6 | use colored::Colorize; 7 | use lets_expect_core::assertions::{ 8 | assertion_error::AssertionError, assertion_result::AssertionResult, 9 | }; 10 | 11 | pub fn be_greater_than(expected: R) -> impl Fn(&R) -> AssertionResult 12 | where 13 | R: Debug + PartialOrd, 14 | { 15 | move |received| { 16 | if expected < *received { 17 | Ok(()) 18 | } else { 19 | let expected = format!("{:?}", expected).green().bold(); 20 | let received = format!("{:?}", received).red().bold(); 21 | Err(AssertionError { 22 | message: vec![format!( 23 | "Expected {} to be greater than {}", 24 | received, expected 25 | )], 26 | }) 27 | } 28 | } 29 | } 30 | 31 | pub fn be_greater_or_equal_to(expected: R) -> impl Fn(&R) -> AssertionResult 32 | where 33 | R: Debug + PartialOrd, 34 | { 35 | move |received| { 36 | if expected <= *received { 37 | Ok(()) 38 | } else { 39 | let expected = format!("{:?}", expected).green().bold(); 40 | let received = format!("{:?}", received).red().bold(); 41 | Err(AssertionError { 42 | message: vec![format!( 43 | "Expected {} to be greater or equal to {}", 44 | received, expected 45 | )], 46 | }) 47 | } 48 | } 49 | } 50 | 51 | pub fn be_less_than(expected: R) -> impl Fn(&R) -> AssertionResult 52 | where 53 | R: Debug + PartialOrd, 54 | { 55 | move |received| { 56 | if expected > *received { 57 | Ok(()) 58 | } else { 59 | let expected = format!("{:?}", expected).green().bold(); 60 | let received = format!("{:?}", received).red().bold(); 61 | Err(AssertionError { 62 | message: vec![format!( 63 | "Expected {} to be less than {}", 64 | received, expected 65 | )], 66 | }) 67 | } 68 | } 69 | } 70 | 71 | pub fn be_less_or_equal_to(expected: R) -> impl Fn(&R) -> AssertionResult 72 | where 73 | R: Debug + PartialOrd, 74 | { 75 | move |received| { 76 | if expected >= *received { 77 | Ok(()) 78 | } else { 79 | let expected = format!("{:?}", expected).green().bold(); 80 | let received = format!("{:?}", received).red().bold(); 81 | Err(AssertionError { 82 | message: vec![format!( 83 | "Expected {} to be less or equal to {}", 84 | received, expected 85 | )], 86 | }) 87 | } 88 | } 89 | } 90 | 91 | pub fn be_between(lower: R, upper: R) -> impl Fn(&R) -> AssertionResult 92 | where 93 | R: Debug + PartialOrd, 94 | { 95 | move |received| { 96 | if lower <= *received && *received <= upper { 97 | Ok(()) 98 | } else { 99 | let lower = format!("{:?}", lower).green().bold(); 100 | let upper = format!("{:?}", upper).green().bold(); 101 | let received = format!("{:?}", received).red().bold(); 102 | Err(AssertionError { 103 | message: vec![format!( 104 | "Expected {} to be between {} and {}", 105 | received, lower, upper 106 | )], 107 | }) 108 | } 109 | } 110 | } 111 | 112 | pub fn be_close_to(expected: R, delta: R) -> impl Fn(&R) -> AssertionResult 113 | where 114 | R: Debug + PartialOrd + Sub + Add + Clone, 115 | { 116 | move |received| { 117 | if expected.clone() - delta.clone() <= *received 118 | && *received <= expected.clone() + delta.clone() 119 | { 120 | Ok(()) 121 | } else { 122 | let expected = format!("{:?}", expected).green().bold(); 123 | let delta = format!("{:?}", delta).green().bold(); 124 | let received = format!("{:?}", received).red().bold(); 125 | Err(AssertionError { 126 | message: vec![format!( 127 | "Expected {} to be close to {} with a delta of {}", 128 | received, expected, delta 129 | )], 130 | }) 131 | } 132 | } 133 | } 134 | 135 | #[cfg(test)] 136 | mod tests { 137 | use colored::control::set_override; 138 | 139 | use crate::expected_err::expected_err; 140 | 141 | use super::*; 142 | 143 | #[test] 144 | fn be_greater_than_ok() { 145 | assert_eq!(be_greater_than(1)(&2), Ok(())); 146 | } 147 | 148 | #[test] 149 | fn be_greater_than_err() { 150 | set_override(false); 151 | assert_eq!( 152 | be_greater_than(2)(&1), 153 | expected_err(vec!["Expected 1 to be greater than 2"]) 154 | ); 155 | } 156 | 157 | #[test] 158 | fn be_greater_or_equal_to_ok() { 159 | assert_eq!(be_greater_or_equal_to(1)(&1), Ok(())); 160 | } 161 | 162 | #[test] 163 | fn be_greater_or_equal_to_err() { 164 | set_override(false); 165 | assert_eq!( 166 | be_greater_or_equal_to(2)(&1), 167 | expected_err(vec!["Expected 1 to be greater or equal to 2"]) 168 | ); 169 | } 170 | 171 | #[test] 172 | fn be_less_than_ok() { 173 | assert_eq!(be_less_than(2)(&1), Ok(())); 174 | } 175 | 176 | #[test] 177 | fn be_less_than_err() { 178 | set_override(false); 179 | assert_eq!( 180 | be_less_than(1)(&2), 181 | expected_err(vec!["Expected 2 to be less than 1"]) 182 | ); 183 | } 184 | 185 | #[test] 186 | fn be_less_or_equal_to_ok() { 187 | assert_eq!(be_less_or_equal_to(2)(&2), Ok(())); 188 | } 189 | 190 | #[test] 191 | fn be_less_or_equal_to_err() { 192 | set_override(false); 193 | assert_eq!( 194 | be_less_or_equal_to(1)(&2), 195 | expected_err(vec!["Expected 2 to be less or equal to 1"]) 196 | ); 197 | } 198 | 199 | #[test] 200 | fn be_between_ok() { 201 | assert_eq!(be_between(1, 3)(&2), Ok(())); 202 | } 203 | 204 | #[test] 205 | fn be_between_err() { 206 | set_override(false); 207 | assert_eq!( 208 | be_between(1, 3)(&4), 209 | expected_err(vec!["Expected 4 to be between 1 and 3"]) 210 | ); 211 | } 212 | 213 | #[test] 214 | fn be_close_to_ok() { 215 | assert_eq!(be_close_to(1f32, 0.5)(&1.5), Ok(())); 216 | } 217 | 218 | #[test] 219 | fn be_close_to_err() { 220 | set_override(false); 221 | assert_eq!( 222 | be_close_to(1f64, 0.5)(&2.5), 223 | expected_err(vec!["Expected 2.5 to be close to 1.0 with a delta of 0.5"]) 224 | ); 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /lets_expect_core/src/utils/expr_dependencies.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use proc_macro2::{Ident, TokenTree}; 4 | use syn::{Block, Expr, Pat, Stmt}; 5 | 6 | /// Returns the identifiers used in an expression. 7 | pub(crate) fn expr_dependencies(expr: &Expr) -> HashSet { 8 | let mut dependencies = HashSet::new(); 9 | 10 | match expr { 11 | Expr::Binary(binary) => { 12 | dependencies.extend(expr_dependencies(&binary.left)); 13 | dependencies.extend(expr_dependencies(&binary.right)); 14 | } 15 | Expr::Unary(unary) => { 16 | dependencies.extend(expr_dependencies(&unary.expr)); 17 | } 18 | Expr::Assign(assign) => { 19 | dependencies.extend(expr_dependencies(&assign.left)); 20 | dependencies.extend(expr_dependencies(&assign.right)); 21 | } 22 | Expr::AssignOp(assign_op) => { 23 | dependencies.extend(expr_dependencies(&assign_op.left)); 24 | dependencies.extend(expr_dependencies(&assign_op.right)); 25 | } 26 | Expr::Path(path) => { 27 | if let Some(ident) = path.path.get_ident() { 28 | dependencies.insert(ident.clone()); 29 | } 30 | } 31 | Expr::Call(call) => { 32 | dependencies.extend(expr_dependencies(&call.func)); 33 | dependencies.extend(call.args.iter().flat_map(expr_dependencies)); 34 | } 35 | Expr::MethodCall(method_call) => { 36 | dependencies.extend(expr_dependencies(&method_call.receiver)); 37 | dependencies.extend(method_call.args.iter().flat_map(expr_dependencies)); 38 | } 39 | Expr::Field(field) => { 40 | dependencies.extend(expr_dependencies(&field.base)); 41 | } 42 | Expr::Index(index) => { 43 | dependencies.extend(expr_dependencies(&index.expr)); 44 | dependencies.extend(expr_dependencies(&index.index)); 45 | } 46 | Expr::Range(range) => { 47 | if let Some(from) = &range.from { 48 | dependencies.extend(expr_dependencies(from)); 49 | } 50 | if let Some(to) = &range.to { 51 | dependencies.extend(expr_dependencies(to)); 52 | } 53 | } 54 | Expr::Reference(reference) => { 55 | dependencies.extend(expr_dependencies(&reference.expr)); 56 | } 57 | Expr::Paren(paren) => { 58 | dependencies.extend(expr_dependencies(&paren.expr)); 59 | } 60 | Expr::Group(group) => { 61 | dependencies.extend(expr_dependencies(&group.expr)); 62 | } 63 | Expr::Block(block) => { 64 | dependencies.extend(block_dependencies(&block.block)); 65 | } 66 | Expr::If(r#if) => { 67 | dependencies.extend(expr_dependencies(&r#if.cond)); 68 | dependencies.extend(r#if.then_branch.stmts.iter().flat_map(stmt_dependencies)); 69 | if let Some(else_branch) = &r#if.else_branch { 70 | dependencies.extend(expr_dependencies(&else_branch.1)); 71 | } 72 | } 73 | Expr::Match(match_) => { 74 | dependencies.extend(expr_dependencies(&match_.expr)); 75 | dependencies.extend( 76 | match_ 77 | .arms 78 | .iter() 79 | .map(|arm| &*arm.body) 80 | .flat_map(expr_dependencies), 81 | ); 82 | } 83 | Expr::Closure(closure) => { 84 | dependencies.extend(closure.inputs.iter().flat_map(pat_dependencies)); 85 | dependencies.extend(expr_dependencies(&closure.body)); 86 | } 87 | Expr::Unsafe(unsafe_) => { 88 | dependencies.extend(unsafe_.block.stmts.iter().flat_map(stmt_dependencies)); 89 | } 90 | Expr::Loop(r#loop) => { 91 | dependencies.extend(r#loop.body.stmts.iter().flat_map(stmt_dependencies)); 92 | } 93 | Expr::While(while_) => { 94 | dependencies.extend(expr_dependencies(&while_.cond)); 95 | dependencies.extend(while_.body.stmts.iter().flat_map(stmt_dependencies)); 96 | } 97 | Expr::ForLoop(for_loop) => { 98 | dependencies.extend(pat_dependencies(&for_loop.pat)); 99 | dependencies.extend(expr_dependencies(&for_loop.expr)); 100 | dependencies.extend(for_loop.body.stmts.iter().flat_map(stmt_dependencies)); 101 | } 102 | Expr::Break(r#break) => { 103 | if let Some(expr) = &r#break.expr { 104 | dependencies.extend(expr_dependencies(expr)); 105 | } 106 | } 107 | Expr::Return(return_) => { 108 | if let Some(expr) = &return_.expr { 109 | dependencies.extend(expr_dependencies(expr)); 110 | } 111 | } 112 | Expr::Yield(yield_) => { 113 | if let Some(expr) = &yield_.expr { 114 | dependencies.extend(expr_dependencies(expr)); 115 | } 116 | } 117 | Expr::Try(try_) => { 118 | dependencies.extend(expr_dependencies(&try_.expr)); 119 | } 120 | Expr::Async(async_) => { 121 | dependencies.extend(async_.block.stmts.iter().flat_map(stmt_dependencies)); 122 | } 123 | Expr::Await(r#await) => { 124 | dependencies.extend(expr_dependencies(&r#await.base)); 125 | } 126 | Expr::Macro(macro_) => { 127 | dependencies.extend( 128 | macro_ 129 | .mac 130 | .path 131 | .segments 132 | .iter() 133 | .map(|segment| segment.ident.clone()), 134 | ); 135 | dependencies.extend(macro_.mac.tokens.clone().into_iter().flat_map(|token| { 136 | if let TokenTree::Ident(ident) = token { 137 | Some(ident) 138 | } else { 139 | None 140 | } 141 | })); 142 | } 143 | Expr::Tuple(tuple) => { 144 | dependencies.extend(tuple.elems.iter().flat_map(expr_dependencies)); 145 | } 146 | Expr::Array(array) => { 147 | dependencies.extend(array.elems.iter().flat_map(expr_dependencies)); 148 | } 149 | Expr::Repeat(repeat) => { 150 | dependencies.extend(expr_dependencies(&repeat.expr)); 151 | dependencies.extend(expr_dependencies(&repeat.len)); 152 | } 153 | Expr::Struct(r#struct) => { 154 | dependencies.extend( 155 | r#struct 156 | .fields 157 | .iter() 158 | .flat_map(|field| expr_dependencies(&field.expr)), 159 | ); 160 | dependencies.extend( 161 | r#struct 162 | .fields 163 | .iter() 164 | .flat_map(|field| expr_dependencies(&field.expr)), 165 | ); 166 | } 167 | _ => {} 168 | } 169 | 170 | dependencies 171 | } 172 | 173 | pub fn block_dependencies(block: &Block) -> HashSet { 174 | block.stmts.iter().flat_map(stmt_dependencies).collect() 175 | } 176 | 177 | fn pat_dependencies(pat: &Pat) -> HashSet { 178 | let mut dependencies = HashSet::new(); 179 | 180 | match pat { 181 | Pat::Ident(ident) => { 182 | dependencies.insert(ident.ident.clone()); 183 | } 184 | Pat::Type(pat_type) => { 185 | dependencies.extend(pat_dependencies(&pat_type.pat)); 186 | } 187 | _ => {} 188 | } 189 | 190 | dependencies 191 | } 192 | 193 | pub fn stmt_dependencies(stmt: &Stmt) -> HashSet { 194 | match stmt { 195 | Stmt::Local(local) => { 196 | if let Some(init) = &local.init { 197 | expr_dependencies(&init.1) 198 | } else { 199 | HashSet::new() 200 | } 201 | } 202 | Stmt::Item(_) => HashSet::new(), 203 | Stmt::Expr(expr) | Stmt::Semi(expr, _) => expr_dependencies(expr), 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /lets_expect_core/src/utils/to_ident.rs: -------------------------------------------------------------------------------- 1 | use english_numbers::{convert, Formatting}; 2 | use proc_macro2::Ident; 3 | use syn::{ 4 | punctuated::Punctuated, token::Comma, BinOp, Expr, ExprAssign, ExprRange, Lit, Local, Member, 5 | Pat, Stmt, Type, UnOp, 6 | }; 7 | 8 | pub fn stmt_to_ident(stmt: &Stmt) -> String { 9 | match stmt { 10 | Stmt::Local(local) => local_to_ident(local), 11 | Stmt::Item(_) => unimplemented!("Item not supported"), 12 | Stmt::Expr(expr) => expr_to_ident(expr), 13 | Stmt::Semi(_, _) => unimplemented!("Semi not supported"), 14 | } 15 | } 16 | 17 | pub fn local_to_ident(local: &Local) -> String { 18 | let ident = pat_to_ident(&local.pat).to_string(); 19 | ident 20 | + "_is_" 21 | + expr_to_ident( 22 | &local 23 | .init 24 | .as_ref() 25 | .expect("When `let` statements are expected to have an initial value") 26 | .1, 27 | ) 28 | .as_str() 29 | } 30 | 31 | pub fn pat_to_ident(pat: &Pat) -> Ident { 32 | match pat { 33 | Pat::Ident(pat) => pat.ident.clone(), 34 | Pat::Type(pat) => pat_to_ident(&pat.pat), 35 | _ => unimplemented!("Unable to convert pattern to identifier"), 36 | } 37 | } 38 | 39 | pub fn expr_to_ident(expr: &syn::Expr) -> String { 40 | match expr { 41 | Expr::Array(array) => { 42 | if array.elems.is_empty() { 43 | "empty_array".to_string() 44 | } else { 45 | punctuated_to_ident(&array.elems) 46 | } 47 | } 48 | Expr::Assign(assign) => expr_assign_to_ident(assign), 49 | Expr::AssignOp(assign_op) => { 50 | expr_to_ident(&assign_op.left) 51 | + "_" 52 | + binary_op_to_ident(&assign_op.op) 53 | + "_" 54 | + &expr_to_ident(&assign_op.right) 55 | } 56 | Expr::Async(_) => unimplemented!("Async not supported"), 57 | Expr::Await(expr_await) => "await_".to_string() + &expr_to_ident(&expr_await.base), 58 | Expr::Binary(binary) => { 59 | expr_to_ident(&binary.left) 60 | + "_" 61 | + binary_op_to_ident(&binary.op) 62 | + "_" 63 | + &expr_to_ident(&binary.right) 64 | } 65 | Expr::Block(block) => block_to_ident(block), 66 | Expr::Box(boxed) => "boxed_".to_string() + &expr_to_ident(&boxed.expr), 67 | Expr::Break(_) => unimplemented!("Break not supported"), 68 | Expr::Call(call) => { 69 | expr_to_ident(&call.func) 70 | + if !call.args.is_empty() { 71 | "_".to_string() + punctuated_to_ident(&call.args).as_str() 72 | } else { 73 | "".to_string() 74 | } 75 | .as_str() 76 | } 77 | Expr::Cast(cast) => expr_to_ident(&cast.expr) + "_as_" + &type_to_ident(&cast.ty), 78 | Expr::Closure(_) => unimplemented!("Closure not supported"), 79 | Expr::Continue(_) => unimplemented!("Continue not supported"), 80 | Expr::Field(field) => { 81 | expr_to_ident(&field.base) + "_" + member_to_ident(&field.member).as_str() 82 | } 83 | Expr::ForLoop(_) => unimplemented!("ForLoop not supported"), 84 | Expr::Group(_) => unimplemented!("Group not supported"), 85 | Expr::If(if_expr) => "if_".to_string() + &expr_to_ident(&if_expr.cond), 86 | Expr::Index(index) => format!( 87 | "{}_at_{}", 88 | expr_to_ident(&index.expr), 89 | expr_to_ident(&index.index) 90 | ), 91 | Expr::Let(_) => unimplemented!("Let not supported"), 92 | Expr::Lit(lit) => expr_lit_to_ident(lit), 93 | Expr::Loop(_) => unimplemented!("Loop not supported"), 94 | Expr::Macro(mac) => mac.mac.path.get_ident().unwrap().to_string().to_lowercase(), 95 | Expr::Match(_) => "match".to_string(), 96 | Expr::MethodCall(method_call) => { 97 | expr_to_ident(&method_call.receiver) 98 | + "_" 99 | + method_call.method.to_string().to_lowercase().as_str() 100 | + if !method_call.args.is_empty() { 101 | "_".to_string() + punctuated_to_ident(&method_call.args).as_str() 102 | } else { 103 | "".to_string() 104 | } 105 | .as_str() 106 | } 107 | Expr::Paren(paren) => expr_to_ident(&paren.expr), 108 | Expr::Path(path) => path_to_ident(path), 109 | Expr::Range(range) => range_to_ident(range), 110 | Expr::Reference(reference) => expr_to_ident(&reference.expr), 111 | Expr::Repeat(_) => unimplemented!("Repeat not supported"), 112 | Expr::Return(_) => unimplemented!("Return not supported"), 113 | Expr::Struct(struc) => struc.path.get_ident().unwrap().to_string().to_lowercase(), 114 | Expr::Try(_) => unimplemented!("Try not supported"), 115 | Expr::TryBlock(_) => unimplemented!("TryBlock not supported"), 116 | Expr::Tuple(tuple) => punctuated_to_ident(&tuple.elems), 117 | Expr::Type(_) => unimplemented!("Type not supported"), 118 | Expr::Unary(unary) => { 119 | unary_op_to_ident(&unary.op).to_string() + "_" + &expr_to_ident(&unary.expr) 120 | } 121 | Expr::Unsafe(_) => unimplemented!("Unsafe not supported"), 122 | Expr::Verbatim(_) => unimplemented!("Verbatim not supported"), 123 | Expr::While(_) => unimplemented!("While not supported"), 124 | Expr::Yield(_) => unimplemented!("Yield not supported"), 125 | _ => unimplemented!("Expected Expr, got {:?}", expr), 126 | } 127 | } 128 | 129 | fn type_to_ident(ty: &Type) -> String { 130 | match ty { 131 | Type::Array(array) => format!( 132 | "array_of_{}_{}", 133 | expr_to_ident(&array.len), 134 | type_to_ident(&array.elem) 135 | ), 136 | Type::BareFn(_) => unimplemented!(), 137 | Type::Group(_) => unimplemented!(), 138 | Type::ImplTrait(_) => unimplemented!(), 139 | Type::Infer(_) => unimplemented!(), 140 | Type::Macro(_) => unimplemented!(), 141 | Type::Never(_) => unimplemented!(), 142 | Type::Paren(_) => unimplemented!(), 143 | Type::Path(path) => path 144 | .path 145 | .segments 146 | .last() 147 | .unwrap() 148 | .ident 149 | .to_string() 150 | .to_lowercase(), 151 | Type::Ptr(_) => unimplemented!(), 152 | Type::Reference(_) => unimplemented!(), 153 | Type::Slice(_) => unimplemented!(), 154 | Type::TraitObject(_) => unimplemented!(), 155 | Type::Tuple(_) => unimplemented!(), 156 | Type::Verbatim(_) => unimplemented!(), 157 | _ => unimplemented!(), 158 | } 159 | } 160 | 161 | pub fn path_to_ident(path: &syn::ExprPath) -> String { 162 | path.path 163 | .segments 164 | .iter() 165 | .map(|segment| segment.ident.to_string().to_lowercase()) 166 | .collect::>() 167 | .join("_") 168 | } 169 | 170 | fn block_to_ident(block: &syn::ExprBlock) -> String { 171 | block 172 | .block 173 | .stmts 174 | .last() 175 | .map(stmt_to_ident) 176 | .unwrap_or_else(|| "noop".to_string()) 177 | } 178 | 179 | fn member_to_ident(member: &Member) -> String { 180 | match member { 181 | Member::Named(named) => named.to_string(), 182 | Member::Unnamed(unnamed) => unnamed.index.to_string(), 183 | } 184 | } 185 | 186 | fn range_to_ident(range: &ExprRange) -> String { 187 | let from = range 188 | .from 189 | .as_ref() 190 | .map(|expr| format!("_from_{}", expr_to_ident(expr))) 191 | .unwrap_or_default(); 192 | let to = range 193 | .to 194 | .as_ref() 195 | .map(|expr| format!("_to_{}", expr_to_ident(expr))) 196 | .unwrap_or_default(); 197 | format!("range{}{}", from, to) 198 | } 199 | 200 | fn expr_assign_to_ident(assign: &ExprAssign) -> String { 201 | expr_to_ident(&assign.left) + "_is_" + &expr_to_ident(&assign.right) 202 | } 203 | 204 | pub fn punctuated_to_ident(punctuated: &Punctuated) -> String { 205 | punctuated 206 | .iter() 207 | .map(expr_to_ident) 208 | .collect::>() 209 | .join("_") 210 | } 211 | 212 | // pub fn punctuated_assignments_to_ident(punctuated: &Punctuated) -> String { 213 | // punctuated.iter().map(|assignment| format!("{}_is_{}", assignment.name, expr_to_ident(&assignment.value))).collect::>().join("_and_") 214 | // } 215 | 216 | fn expr_lit_to_ident(lit: &syn::ExprLit) -> String { 217 | match &lit.lit { 218 | Lit::Str(_) => "string".to_string(), 219 | Lit::ByteStr(_) => unimplemented!(), 220 | Lit::Byte(_) => unimplemented!(), 221 | Lit::Char(_) => unimplemented!(), 222 | Lit::Int(value) => { 223 | if let Ok(parsed) = value.base10_parse::() { 224 | humanize(parsed) 225 | } else { 226 | "number_".to_string() + value.to_string().as_str() 227 | } 228 | } 229 | Lit::Float(value) => { 230 | let value: f64 = value.base10_parse().unwrap(); 231 | let formatted = format!("{:.2}", value); 232 | let parts = formatted.split('.').collect::>(); 233 | let int_part = parts[0].parse::().unwrap(); 234 | let fraction_part = parts[1].parse::().unwrap(); 235 | let int_part = humanize(int_part); 236 | let fraction_part = humanize(fraction_part); 237 | 238 | format!("{}_point_{}", int_part, fraction_part) 239 | } 240 | Lit::Bool(value) => value.value.to_string(), 241 | Lit::Verbatim(_) => unimplemented!(), 242 | } 243 | } 244 | 245 | fn humanize(value: i64) -> String { 246 | convert( 247 | value, 248 | Formatting { 249 | dashes: false, 250 | title_case: false, 251 | ..Formatting::default() 252 | }, 253 | ) 254 | } 255 | 256 | fn unary_op_to_ident(op: &UnOp) -> &'static str { 257 | match op { 258 | UnOp::Deref(_) => "deref", 259 | UnOp::Not(_) => "not", 260 | UnOp::Neg(_) => "neg", 261 | } 262 | } 263 | 264 | fn binary_op_to_ident(op: &BinOp) -> &'static str { 265 | match op { 266 | BinOp::Add(_) => "plus", 267 | BinOp::Sub(_) => "minus", 268 | BinOp::Mul(_) => "times", 269 | BinOp::Div(_) => "divided_by", 270 | BinOp::Rem(_) => "remainder", 271 | BinOp::And(_) => "and", 272 | BinOp::Or(_) => "or", 273 | BinOp::BitXor(_) => "bit_xor", 274 | BinOp::BitAnd(_) => "bit_and", 275 | BinOp::BitOr(_) => "bit_or", 276 | BinOp::Shl(_) => "shift_left_by", 277 | BinOp::Shr(_) => "shift_right_by", 278 | BinOp::Eq(_) => "equals", 279 | BinOp::Lt(_) => "less_than", 280 | BinOp::Le(_) => "less_equal_than", 281 | BinOp::Ne(_) => "not_equal", 282 | BinOp::Ge(_) => "greater_equal_than", 283 | BinOp::Gt(_) => "greater_than", 284 | BinOp::AddEq(_) => "add_equal", 285 | BinOp::SubEq(_) => "subtract_equal", 286 | BinOp::MulEq(_) => "multiply_equal", 287 | BinOp::DivEq(_) => "divide_equal", 288 | BinOp::RemEq(_) => "remainder_equal", 289 | BinOp::BitXorEq(_) => "xor_equal", 290 | BinOp::BitAndEq(_) => "and_equal", 291 | BinOp::BitOrEq(_) => "or_equal", 292 | BinOp::ShlEq(_) => "shift_left_equal", 293 | BinOp::ShrEq(_) => "shift_right_equal", 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/tomekpiotrowski/lets_expect/build.yaml?branch=main) 2 | ![Crates.io](https://img.shields.io/crates/v/lets_expect) 3 | ![GitHub](https://img.shields.io/github/license/tomekpiotrowski/lets_expect) 4 | 5 | # Let's Expect 6 | 7 | 8 | 9 | Clean tests in Rust. 10 | 11 | ```rust 12 | expect(a + 2) { 13 | when(a = 2) { 14 | to equal(4) 15 | } 16 | } 17 | ``` 18 | 19 | ## Table of Contents 20 | 21 | 1. [Introduction](#introduction) 22 | 2. [Installation](#installation) 23 | 3. [Usage](#usage) 24 | * [How does it work?](#how-does-it-work) 25 | * [Where to put my tests?](#where-to-put-my-tests) 26 | * [`expect` and `to`](#expect-and-to) 27 | * [`let`](#let) 28 | * [`when`](#when) 29 | * [`have`](#have) 30 | * [`make`](#make) 31 | * [`change`](#change) 32 | * [`before` and `after`](#before-and-after) 33 | * [Explicit identifiers for `expect` and `when`](#explicit-identifiers-for-expect-and-when) 34 | * [Stories](#stories) 35 | * [Mutable variables and references](#mutable-variables-and-references) 36 | 4. [Assertions](#assertions) 37 | * [`bool`](#bool) 38 | * [`equality`](#equality) 39 | * [Numbers](#numbers) 40 | * [`match_pattern!`](#match_pattern) 41 | * [`Option` and `Result`](#option_and_result) 42 | * [`panic`](#panic) 43 | * [Iterators](#iterators) 44 | * [Custom assertions](#custom-assertions) 45 | * [Custom `change` assertions](#custom-change-assertions) 46 | * [Assertions module](#assertions-module) 47 | 5. [Supported libraries](#supported-libraries) 48 | * [Tokio](#tokio) 49 | 6. [More examples](#more-examples) 50 | 7. [Known issues and limitations](#known-issues-and-limitations) 51 | 8. [Debugging](#debugging) 52 | 9. [License](#license) 53 | 54 | ### Introduction 55 | 56 | How often when you see a Rust test you think to yourself "wow, this is a really beautifully written test"? Not often, right? 57 | Classic Rust tests do not provide any structure beyond the test function itself. This often results in a lot of boilerplate code, ad-hoc test structure and overall 58 | poor quality. 59 | 60 | Tests are about verifying that a given piece of code run under certain conditions works as expected. A good testing framework embraces this way of thinking. 61 | It makes it easy to structure your code in a way that reflects it. Folks in other communities have been doing this for a long time with tools like 62 | [RSpec](https://relishapp.com/rspec) and [Jasmine](https://jasmine.github.io/). 63 | 64 | If you want beautiful, high-quality tests that are a pleasure to read and write you need something else. Using Rust's procedural macros `lets_expect` introduces 65 | a syntax that let's you clearly state **what** you're testing, under what **conditions** and what is the **expected result**. 66 | 67 | The outcome is: 68 | * easy to read, DRY, TDD-friendly tests 69 | * less boilerplate, less code 70 | * nicer error messages 71 | * more fun 72 | 73 | #### Example 74 | 75 | ```rust 76 | expect(posts.create_post(title, category_id)) { 77 | before { posts.push(Post {title: "Post 1" }) } 78 | after { posts.clear() } 79 | 80 | when(title = valid_title) { 81 | when(category_id = valid_category) to create_a_post { 82 | be_ok, 83 | have(as_ref().unwrap().title) equal(valid_title), 84 | change(posts.len()) { from(1), to(2) } 85 | } 86 | 87 | when(category_id = invalid_category) to return_an_error { 88 | be_err, 89 | have(as_ref().unwrap_err().message) equal("Invalid category"), 90 | not_change(posts.len()) 91 | } 92 | } 93 | 94 | when(title = invalid_title, category_id = valid_category) to be_err 95 | } 96 | ``` 97 | 98 | Now let's compare it to a classic Rust test that does the same thing: 99 | 100 | 101 | ```rust 102 | fn run_setup(test: T) -> () 103 | where T: FnOnce(&mut Posts) -> () + panic::UnwindSafe 104 | { 105 | let mut posts = Posts { posts: vec![] }; 106 | posts.push(Post { title: "Post 1" }); 107 | let posts = Mutex::new(posts); 108 | let result = panic::catch_unwind(|| { 109 | test(posts.try_lock().unwrap().deref_mut()) 110 | }); 111 | 112 | posts.try_lock().unwrap().clear(); 113 | assert!(result.is_ok()); 114 | } 115 | 116 | #[test] 117 | fn creates_a_post() { 118 | run_setup(|posts: &mut Posts| { 119 | let before_count = posts.len(); 120 | let result = posts.create_post(VALID_TITLE, VALID_CATEGORY); 121 | let after_count = posts.len(); 122 | assert!(result.is_ok()); 123 | assert_eq!(VALID_TITLE, result.unwrap().title); 124 | assert_eq!(after_count - before_count, 1); 125 | }) 126 | } 127 | 128 | #[test] 129 | fn returns_an_error_when_category_is_invalid() { 130 | run_setup(|posts: &mut Posts| { 131 | let before_count = posts.len(); 132 | let result = posts.create_post(VALID_TITLE, INVALID_CATEGORY); 133 | let after_count = posts.len(); 134 | assert!(result.is_err()); 135 | assert_eq!("Invalid category", result.unwrap_err().message); 136 | assert_eq!(after_count, before_count); 137 | }) 138 | } 139 | 140 | #[test] 141 | fn returns_an_error_when_title_is_empty() { 142 | run_setup(|posts: &mut Posts| { 143 | let result = posts.create_post("", VALID_CATEGORY); 144 | assert!(result.is_err()); 145 | }) 146 | } 147 | 148 | ``` 149 | 150 | ### Installation 151 | 152 | Add the following to your `Cargo.toml`: 153 | 154 | ```toml 155 | [dev-dependencies] 156 | lets_expect = "0" 157 | ``` 158 | 159 | ### Usage 160 | 161 | #### How does it work? 162 | 163 | Under the hood `lets_expect` generates a single classic test function for each `to` block. It names those tests automatically based on what you're testing and 164 | organizes those tests into modules. This means you can run those tests using `cargo test` and you can use all `cargo test` features. IDE extensions will 165 | also work as expected. 166 | 167 | #### Where to put my tests? 168 | 169 | `lets_expect` tests need to be placed inside of a `lets_expect!` macro, which in turn needs to be placed inside of a `tests` module: 170 | 171 | ```rust 172 | #[cfg(test)] 173 | mod tests { 174 | use super::*; 175 | use lets_expect::lets_expect; 176 | 177 | lets_expect! { 178 | expect(subject) { 179 | to expectation 180 | } 181 | } 182 | } 183 | ``` 184 | 185 | It might be a good idea to define a code snippet in your IDE to avoid having to type this piece of boilerplate every time. 186 | 187 | The examples here omit the macro for brevity. 188 | 189 | #### `expect` and `to` 190 | 191 | `expect` sets the subject of the test. It can be any Rust expression (including a block). `to` introduces expectations. It can be followed 192 | by a single expectation or a block of expectations. In the latter case you must provide a name for the test, which needs to be a valid Rust identifier. 193 | 194 | ```rust 195 | expect(2) { 196 | to equal(2) 197 | } 198 | ``` 199 | 200 | If there are multiple assertions in a `to` block they need to be separated by a comma. 201 | 202 | ```rust 203 | expect({ 1 + 1 }) { 204 | to be_actually_2 { 205 | equal(2), 206 | not_equal(3) 207 | } 208 | } 209 | ``` 210 | 211 | One `to` block generates a single test. This means the subject will be executed once and then all the assertions inside that `to` block will be run. 212 | If you want to generate multiple tests you can use multiple `to` blocks: 213 | 214 | ```rust 215 | expect(files.create_file()) { 216 | to make(files.try_to_remove_file()) be_true 217 | to make(files.file_exists()) be_true 218 | } 219 | ``` 220 | 221 | If your `expect` contains a single item you can omit the braces: 222 | 223 | ```rust 224 | expect(a + 2) when(a = 2) { 225 | to equal(4) 226 | } 227 | ``` 228 | 229 | 230 | #### `let` 231 | 232 | Inside the top level `lets_expect!` macro as well as `expect` and `when` blocks you can use `let` to define variables. 233 | 234 | ```rust 235 | expect(a) { 236 | let a = 2; 237 | 238 | to equal(2) 239 | } 240 | ``` 241 | 242 | Variables can be overwritten in nested blocks. New definitions can use values from outer blocks. 243 | 244 | ```rust 245 | expect(a) { 246 | let a = 2; 247 | 248 | when a_is_4 { 249 | let a = a + 2; 250 | 251 | to equal(4) 252 | } 253 | } 254 | ``` 255 | 256 | Variables don't have to be defined in the order they're used. 257 | 258 | ```rust 259 | expect(sum) { 260 | let sum = a + b; 261 | let a = 2; 262 | 263 | when b_is_three { 264 | let b = 3; 265 | 266 | to equal(5) 267 | } 268 | } 269 | ``` 270 | 271 | #### `when` 272 | 273 | `when` sets a value of one or more variables for a given block. This keyword is this library's secret sauce. It allows you to define values of variables 274 | for multiples tests in a concise and readable way, without having to repeat it in every test. 275 | 276 | ```rust 277 | expect(a + b + c) { 278 | let a = 2; 279 | 280 | when(c = 5) { 281 | when(b = 3) { 282 | to equal(10) 283 | } 284 | 285 | when(a = 10, b = 10) { 286 | to equal(25) 287 | } 288 | } 289 | } 290 | ``` 291 | 292 | You can use similar syntax as in `let` to define variables. The only difference being the `let` keyword itself is ommited. 293 | 294 | ```rust 295 | expect(a += 1) { 296 | when(mut a: i64 = 1) { 297 | to change(a.clone()) { from(1), to(2) } 298 | } 299 | } 300 | ``` 301 | 302 | You can also use `when` with an identifier. This will simply create a new context with the given identifier. No new variables are defined. 303 | 304 | ```rust 305 | expect(login(username, password)) { 306 | when credentials_are_invalid { 307 | let username = "invalid"; 308 | let password = "invalid"; 309 | 310 | to be_false 311 | } 312 | } 313 | ``` 314 | 315 | If your `when` contains only one item the braces can be ommited: 316 | 317 | ```rust 318 | expect(a + 2) when(a = 2) to equal(4) 319 | ``` 320 | 321 | `when` blocks do not have to be placed inside of `expect` blocks. Their order can be reversed. 322 | 323 | ```rust 324 | when(a = 2) { 325 | expect(a + 2) to equal(4) 326 | } 327 | ``` 328 | 329 | #### `have` 330 | 331 | `have` is used to test values of attributes or return values of methods of the subject. 332 | 333 | ```rust 334 | let response = Response { status: 200, content: ResponseContent::new("admin", "123") }; 335 | 336 | expect(response) { 337 | to be_valid { 338 | have(status) equal(200), 339 | have(is_ok()) be_true, 340 | have(content) { 341 | have(username) equal("admin".to_string()), 342 | have(token) equal("123".to_string()), 343 | } 344 | } 345 | } 346 | ``` 347 | 348 | Multiple assertions can be provided to `have` by wrapping them in curly braces and separating them with commas. 349 | 350 | #### `make` 351 | 352 | `make` is used to test values of arbitrary expressions. 353 | 354 | ```rust 355 | expect(posts.push((user_id, "new post"))) { 356 | let user_id = 1; 357 | 358 | to make(user_has_posts(user_id)) be_true 359 | } 360 | ``` 361 | 362 | Multiple assertions can be provided to `make` by wrapping them in curly braces and separating them with commas. 363 | 364 | #### `change` 365 | 366 | `change` is used to test if and how a value changes after subject is executed. The expression given as an argument to `change` is evaluated twice. Once before the subject is executed and once after. 367 | The two values are then provided to the assertions specified in the `change` block. 368 | 369 | ```rust 370 | expect(posts.create_post(title, category_id)) { 371 | after { posts.clear() } 372 | 373 | when(title = valid_title) { 374 | when(category_id = valid_category) { 375 | to change(posts.len()) { from(0), to(1) } 376 | } 377 | 378 | when(category_id = invalid_category) { 379 | to not_change(posts.len()) 380 | } 381 | } 382 | } 383 | ``` 384 | 385 | #### `before` and `after` 386 | 387 | The contents of the `before` blocks are executed before the subject is evaluated, but after the `let` bindings are executed. The contents of the `after` blocks are executed 388 | after the subject is evaluated and the assertions are verified. 389 | 390 | `before` blocks are run in the order they are defined. Parent `before` blocks being run before child `before` blocks. The reverse is true for `after` blocks. 391 | `after` blocks are guaranteed to run even if assertions fail. They however will not run if the let statements, before blocks, subject evaluation or assertions panic. 392 | 393 | ```rust 394 | let mut messages: Vec<&str> = Vec::new(); 395 | before { 396 | messages.push("first message"); 397 | } 398 | after { 399 | messages.clear(); 400 | } 401 | expect(messages.len()) { to equal(1) } 402 | expect(messages.push("new message")) { 403 | to change(messages.len()) { from(1), to(2) } 404 | } 405 | ``` 406 | 407 | #### Explicit identifiers for `expect` and `when` 408 | 409 | Because `lets_expect` uses standard Rust tests under the hood it has to come up with a unique identifier for each test. To make those identifiers 410 | readable `lets_expect` uses the expressions in `expect` and `when` to generate the name. This works well for simple expressions but can get a bit 411 | messy for more complex expressions. Sometimes it can also result in duplicated names. To solve those issues you can use the `as` keyword to give 412 | the test an explicit name: 413 | 414 | ```rust 415 | expect(a + b + c) as sum_of_three { 416 | when(a = 1, b = 1, c = 1) as everything_is_one to equal(3) 417 | } 418 | ``` 419 | 420 | This will create a test_named: 421 | ```text 422 | expect_sum_of_three::when_everything_is_one::to_equal_three 423 | ``` 424 | 425 | instead of 426 | 427 | ```text 428 | expect_a_plus_b_plus_c::when_a_is_one_b_is_one_c_is_one::to_equal_three 429 | ``` 430 | 431 | #### Stories 432 | 433 | `lets_expect` promotes tests that only test one piece of code at a time. Up until this point all the test we've seen define a subject, run that subject and 434 | verify the result. However there can be situations where we want to run and test multiple pieces of code in sequence. This could be for example because executing a piece 435 | of code might be time consuming and we want to avoid doing it multiple times in multiple tests. 436 | 437 | To address this `lets_expect` provides the `story` keyword. Stories are a bit more similar to classic tests in that they allow 438 | arbitrary statements to be interleaved with assertions. 439 | 440 | Please note that the `expect` keyword inside stories has to be followed by `to` and can't open a block. 441 | 442 | ```rust 443 | story login_is_successful { 444 | expect(page.logged_in) to be_false 445 | 446 | let login_result = page.login(&invalid_user); 447 | 448 | expect(&login_result) to be_err 449 | expect(&login_result) to equal(Err(AuthenticationError { message: "Invalid credentials".to_string() })) 450 | expect(page.logged_in) to be_false 451 | 452 | let login_result = page.login(&valid_user); 453 | 454 | expect(login_result) to be_ok 455 | expect(page.logged_in) to be_true 456 | } 457 | ``` 458 | 459 | > 460 | > **NOTE:** For now `expect` blocks can't be placed inside of loops or closures. They need to be top-level items in a story. 461 | > 462 | 463 | #### Mutable variables and references 464 | 465 | For some tests you may need to make the tested value mutable or you may need to pass a mutable reference to the assertions. In `expect`, `have` and `make` you can 466 | use the `mut` keyword to do that. 467 | 468 | ```rust 469 | expect(mut vec![1, 2, 3]) { // make the subject mutable 470 | to have(remove(1)) equal(2) 471 | } 472 | 473 | expect(mut vec.iter()) { // pass a mutable reference to the iterator to the assertion 474 | let vec = vec![1, 2, 3]; 475 | to all(be_greater_than(0)) 476 | } 477 | 478 | expect(vec![1, 2, 3]) { 479 | to have(mut iter()) all(be_greater_than(0)) // pass a mutable reference to the iterator to the assertion 480 | } 481 | ``` 482 | 483 | `let` and `when` statements also support `mut`. 484 | 485 | 486 | ### Assertions 487 | 488 | #### `bool` 489 | 490 | ```rust 491 | expect(2 == 2) to be_true 492 | expect(2 != 2) to be_false 493 | ``` 494 | 495 | #### `equality` 496 | 497 | ```rust 498 | expect(2) to be_actually_two { 499 | equal(2), 500 | not_equal(3) 501 | } 502 | ``` 503 | 504 | #### Numbers 505 | 506 | ```rust 507 | expect(2.1) { 508 | to be_close_to(2.0, 0.2) 509 | to be_greater_than(2.0) 510 | to be_less_or_equal_to(2.1) 511 | } 512 | ``` 513 | 514 | #### `match_pattern!` 515 | 516 | `match_pattern!` is used to test if a value matches a pattern. It's functionality is similar to [`matches!`](https://doc.rust-lang.org/std/macro.matches.html) macro. 517 | 518 | ```rust 519 | expect(Response::UserCreated) { 520 | to match_pattern!(Response::UserCreated) 521 | } 522 | 523 | expect(Response::ValidationFailed("email")) { 524 | to match_email { 525 | match_pattern!(Response::ValidationFailed("email")), 526 | not_match_pattern!(Response::ValidationFailed("email2")) 527 | } 528 | } 529 | ``` 530 | 531 | #### `Option` and `Result` 532 | 533 | `lets_expect` provides a set of assertions for `Option` and `Result` types. 534 | 535 | ```rust 536 | expect(Some(1u8) as Option) { 537 | to be_some_and equal(1) 538 | 539 | to be_some { 540 | equal(Some(1)), 541 | be_some 542 | } 543 | } 544 | 545 | expect(None as Option) { 546 | to be_none { 547 | equal(None), 548 | be_none 549 | } 550 | } 551 | 552 | expect(Ok(1u8) as Result) { 553 | to be_ok_and equal(1) 554 | 555 | to be_ok { 556 | be_ok, 557 | equal(Ok(1)), 558 | } 559 | } 560 | 561 | expect(Err(2) as Result<(), i32>) { 562 | to be_err_and equal(2) 563 | 564 | to be_err { 565 | be_err, 566 | equal(Err(2)), 567 | } 568 | } 569 | ``` 570 | 571 | #### `panic!` 572 | 573 | ```rust 574 | expect(panic!("I panicked!")) { 575 | to panic 576 | } 577 | 578 | expect(2) { 579 | to not_panic 580 | } 581 | ``` 582 | 583 | `panic` and `not_panic` assertions can be the only assertions present in a `to` block. 584 | 585 | 586 | #### Iterators 587 | 588 | ```rust 589 | expect(vec![1, 2, 3]) { 590 | to have(mut iter()) all(be_greater_than(0)) 591 | to have(mut iter()) any(be_greater_than(2)) 592 | } 593 | ``` 594 | 595 | #### Custom assertions 596 | 597 | `lets_expect` provides a way to define custom assertions. An assertion is a function that takes the reference to the 598 | subject and returns an [`AssertionResult`](../lets_expect_core/assertions/assertion_result/index.html). 599 | 600 | Here's two custom assertions: 601 | 602 | ```rust 603 | use lets_expect::*; 604 | 605 | fn have_positive_coordinates(point: &Point) -> AssertionResult { 606 | if point.x > 0 && point.y > 0 { 607 | Ok(()) 608 | } else { 609 | Err(AssertionError::new(vec![format!( 610 | "Expected ({}, {}) to be positive coordinates", 611 | point.x, point.y 612 | )])) 613 | } 614 | } 615 | 616 | fn have_x_coordinate_equal(x: i32) -> impl Fn(&Point) -> AssertionResult { 617 | move |point: &Point| { 618 | if point.x == x { 619 | Ok(()) 620 | } else { 621 | Err(AssertionError::new(vec![format!( 622 | "Expected x coordinate to be {}, but it was {}", 623 | x, point.x 624 | )])) 625 | } 626 | } 627 | } 628 | ``` 629 | 630 | And here's how to use them: 631 | 632 | ```rust 633 | expect(Point { x: 2, y: 22 }) { 634 | to have_valid_coordinates { 635 | have_positive_coordinates, 636 | have_x_coordinate_equal(2) 637 | } 638 | } 639 | ``` 640 | 641 | Remember to import your custom assertions in your test module. 642 | 643 | #### Custom change assertions 644 | 645 | Similarly custom change assertions can be defined: 646 | 647 | ```rust 648 | use lets_expect::*; 649 | 650 | fn by_multiplying_by(x: i32) -> impl Fn(&i32, &i32) -> AssertionResult { 651 | move |before, after| { 652 | if *after == *before * x { 653 | Ok(()) 654 | } else { 655 | Err(AssertionError::new(vec![format!( 656 | "Expected {} to be multiplied by {} to be {}, but it was {} instead", 657 | before, 658 | x, 659 | before * x, 660 | after 661 | )])) 662 | } 663 | } 664 | } 665 | ``` 666 | 667 | And used like so: 668 | 669 | ```rust 670 | expect(a *= 5) { 671 | let mut a = 5; 672 | 673 | to change(a.clone()) by_multiplying_by(5) 674 | } 675 | ``` 676 | 677 | #### Assertions 678 | 679 | This library has fairly few builtin assertions compared to other similar ones. This is because the use of `have`, `make` and `match_pattern!` allows for 680 | expressive and flexible conditions without the need for a lot of different assertions. 681 | 682 | The full list of assertions is available in the [assertions module](https://docs.rs/lets_expect_assertions). 683 | 684 | 685 | 686 | ### Supported libraries 687 | 688 | #### Tokio 689 | 690 | `lets_expect` works with [Tokio](https://tokio.rs/). To use Tokio in your tests you need to add the `tokio` feature in your `Cargo.toml`: 691 | 692 | ```toml 693 | lets_expect = { version = "*", features = ["tokio"] } 694 | ``` 695 | 696 | Then whenever you want to use Tokio in your tests you need to add the `tokio_test` attribute to your `lets_expect!` macros like so: 697 | 698 | ```rust 699 | lets_expect! { #tokio_test 700 | } 701 | ``` 702 | 703 | This will make `lets_expect` use `#[tokio::test]` instead of `#[test]` in generated tests. 704 | 705 | Here's an example of a test using Tokio: 706 | 707 | ```rust 708 | let value = 5; 709 | let spawned = tokio::spawn(async move { 710 | value 711 | }); 712 | 713 | expect(spawned.await) { 714 | to match_pattern!(Ok(5)) 715 | } 716 | ``` 717 | 718 | 719 | ### More examples 720 | 721 | `lets_expect` repository contains tests that might be useful as examples of using the library. 722 | You can find them [here](https://github.com/tomekpiotrowski/lets_expect/tree/main/tests). 723 | 724 | ### Known issues and limitations 725 | 726 | * rust-analyzer's auto-import doesn't seem to work well from inside of macros. It might be necessary to manually add `use` statements for types from outside of the module. 727 | * Syntax highlighting doesn't work with `lets_expect` syntax. Currently there's no way for Rust macros to export their syntax to language tools. 728 | * Shared contexts (similar to [RSpec](https://relishapp.com/rspec/rspec-core/docs/example-groups/shared-context)) seem to be impossible to implement without 729 | [eager macro expansion](https://rustc-dev-guide.rust-lang.org/macro-expansion.html#eager-expansion). 730 | 731 | ### Debugging 732 | 733 | If you're having trouble with your tests you can use [cargo-expand](https://github.com/dtolnay/cargo-expand) to see what code is generated by `lets_expect`. 734 | The generated code is not always easy to read and is not guaranteed to be stable between versions. Still it can be useful for debugging. 735 | 736 | ### License 737 | 738 | This project is licensed under the terms of the MIT license. 739 | 740 | 741 | --------------------------------------------------------------------------------