From 838805489eb9d9117ab822c4dbcbb735d7b30589 Mon Sep 17 00:00:00 2001 From: DongHun Kwak Date: Thu, 20 Apr 2023 09:05:13 +0900 Subject: [PATCH 1/1] Import rstest_reuse 0.5.0 --- .cargo_vcs_info.json | 6 + Cargo.toml | 57 +++ Cargo.toml.orig | 35 ++ README.md | 268 +++++++++++ build.rs | 10 + checkoutlist.md | 15 + src/lib.rs | 455 ++++++++++++++++++ tests/acceptance.rs | 189 ++++++++ .../copy_args_attributes_from_template.rs | 66 +++ tests/resources/deny_docs.rs | 13 + tests/resources/export_not_used.rs | 9 + tests/resources/export_template.rs | 39 ++ tests/resources/export_template_root.rs | 8 + tests/resources/import_template.rs | 8 + tests/resources/in_mod.rs | 20 + .../no_local_macro_should_not_compile.rs | 17 + tests/resources/not_used.rs | 8 + tests/resources/qualify_template_use.rs | 31 ++ tests/resources/simple_example.rs | 22 + tests/resources/templates_with_same_name.rs | 27 ++ tests/resources/use_before_define.rs | 11 + 21 files changed, 1314 insertions(+) create mode 100644 .cargo_vcs_info.json create mode 100644 Cargo.toml create mode 100644 Cargo.toml.orig create mode 100644 README.md create mode 100644 build.rs create mode 100644 checkoutlist.md create mode 100644 src/lib.rs create mode 100644 tests/acceptance.rs create mode 100644 tests/resources/copy_args_attributes_from_template.rs create mode 100644 tests/resources/deny_docs.rs create mode 100644 tests/resources/export_not_used.rs create mode 100644 tests/resources/export_template.rs create mode 100644 tests/resources/export_template_root.rs create mode 100644 tests/resources/import_template.rs create mode 100644 tests/resources/in_mod.rs create mode 100644 tests/resources/no_local_macro_should_not_compile.rs create mode 100644 tests/resources/not_used.rs create mode 100644 tests/resources/qualify_template_use.rs create mode 100644 tests/resources/simple_example.rs create mode 100644 tests/resources/templates_with_same_name.rs create mode 100644 tests/resources/use_before_define.rs diff --git a/.cargo_vcs_info.json b/.cargo_vcs_info.json new file mode 100644 index 0000000..4894f04 --- /dev/null +++ b/.cargo_vcs_info.json @@ -0,0 +1,6 @@ +{ + "git": { + "sha1": "b12f16cfe4e714805fdb00de6f15ecfc986ecfd8" + }, + "path_in_vcs": "rstest_reuse" +} \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..0c59879 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,57 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2018" +name = "rstest_reuse" +version = "0.5.0" +authors = ["Michele d'Amico "] +description = """ +Reuse rstest attributes: create a set of tests and apply it +to every scenario you want to test. +""" +homepage = "https://github.com/la10736/rstest" +readme = "README.md" +keywords = [ + "test", + "fixture", +] +categories = ["development-tools::testing"] +license = "MIT/Apache-2.0" +repository = "https://github.com/la10736/rstest" + +[lib] +proc-macro = true + +[dependencies.quote] +version = "1.0.9" + +[dependencies.rand] +version = "0.8.5" + +[dependencies.syn] +version = "1.0.72" +features = ["full"] + +[dev-dependencies.lazy_static] +version = "1.4.0" + +[dev-dependencies.rstest] +version = "0.16.0" + +[dev-dependencies.rstest_test] +version = "0.10.0" + +[dev-dependencies.temp_testdir] +version = "0.2.3" + +[build-dependencies.rustc_version] +version = "0.4.0" diff --git a/Cargo.toml.orig b/Cargo.toml.orig new file mode 100644 index 0000000..484ebdf --- /dev/null +++ b/Cargo.toml.orig @@ -0,0 +1,35 @@ +[package] +authors = ["Michele d'Amico "] +categories = ["development-tools::testing"] +description = """ +Reuse rstest attributes: create a set of tests and apply it +to every scenario you want to test. +""" +edition = "2018" +homepage = "https://github.com/la10736/rstest" +keywords = ["test", "fixture"] +license = "MIT/Apache-2.0" +name = "rstest_reuse" +readme = "README.md" +repository = "https://github.com/la10736/rstest" +version = "0.5.0" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +proc-macro = true + +[dependencies] +quote = "1.0.9" +rand = "0.8.5" +syn = {version = "1.0.72", features = ["full"]} + +[dev-dependencies] +lazy_static = "1.4.0" +rstest_test = {version = "0.10.0", path = "../rstest_test"} +temp_testdir = "0.2.3" +# To compile doc tests +rstest = {version = "0.16.0"} + +[build-dependencies] +rustc_version = "0.4.0" diff --git a/README.md b/README.md new file mode 100644 index 0000000..2276283 --- /dev/null +++ b/README.md @@ -0,0 +1,268 @@ +[![Crate][crate-image]][crate-link] +[![Status][test-action-image]][test-action-link] +[![Apache 2.0 Licensed][license-apache-image]][license-apache-link] +[![MIT Licensed][license-mit-image]][license-mit-link] + +# Reuse `rstest`'s parametrized cases + +:warning: [**Version 0.5.0 introduce a breaking change**](#dismiss-macro_use-attribute-support) + +This crate give a way to define a tests set and apply them to every case you need to +test. With `rstest` crate you can define a tests list but if you want to apply the same tests +to another test function you must rewrite all cases or write some macros that do the job. + +Both solutions have some drawbreak: +- intruduce duplication +- macros makes code harder to read and shift out the focus from tests core + +The aim of this crate is solve this problem. `rstest_resuse` expose two attributes: +- `#[template]`: to define a template +- `#[apply]`: to apply a defined template to create tests + +Here is a simple example: + +```rust +use rstest::rstest; +use rstest_reuse::{self, *}; +// Here we define the template. This define +// * The test list name to `two_simple_cases` +// * cases: here two cases +#[template] +#[rstest] +#[case(2, 2)] +#[case(4/2, 2)] +// Define a and b as cases arguments +fn two_simple_cases(#[case] a: u32, #[case] b: u32) {} +// Here we apply the `two_simple_cases` template: That is expanded in +// #[template] +// #[rstest] +// #[case(2, 2)] +// #[case(4/2, 2)] +// fn it_works(#[case] a: u32,#[case] b: u32) { +// assert!(a == b); +// } +#[apply(two_simple_cases)] +fn it_works(a: u32, b: u32) { + assert!(a == b); +} +// Here we reuse the `two_simple_cases` template to create two +// other tests +#[apply(two_simple_cases)] +fn it_fail(a: u32, b: u32) { + assert!(a != b); +} +``` + +If we run `cargo test` we have: + +```text + Finished test [unoptimized + debuginfo] target(s) in 0.05s + Running target/debug/deps/playground-8a1212f8b5eb00ce +running 4 tests +test it_fail::case_1 ... FAILED +test it_works::case_1 ... ok +test it_works::case_2 ... ok +test it_fail::case_2 ... FAILED +failures: +---- it_fail::case_1 stdout ---- +thread 'it_fail::case_1' panicked at 'assertion failed: a != b', src/main.rs:34:5 +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace +---- it_fail::case_2 stdout ---- +thread 'it_fail::case_2' panicked at 'assertion failed: a != b', src/main.rs:34:5 +failures: + it_fail::case_1 + it_fail::case_2 +test result: FAILED. 2 passed; 2 failed; 0 ignored; 0 measured; 0 filtered out +error: test failed, to rerun pass '--bin playground' +``` + +Simple and neat! + +Note that if the test arguments names match the template's ones you can don't +repeate the arguments attributes. + +## Composition and Values + +If you need to add some cases or values when apply a template you can leverage on +composition. Here a simple example: + +```rust +#[template] +#[rstest] +#[case(2, 2)] +#[case(4/2, 2)] +fn base(#[case] a: u32, #[case] b: u32) {} + +// Here we add a new case and an argument in a value list: +#[apply(base)] +#[case(9/3, 3)] +fn it_works(a: u32, b: u32, #[values("a", "b")] t: &str) { + assert!(a == b); + assert!("abcd".contains(t)) +} +``` + +run 6 tests: + +``` +running 6 tests +test it_works::case_1::t_2 ... ok +test it_works::case_2::t_2 ... ok +test it_works::case_2::t_1 ... ok +test it_works::case_3::t_2 ... ok +test it_works::case_3::t_1 ... ok +test it_works::case_1::t_1 ... ok +``` + +Template can also used for values and with arguments if you need: + +```rust +#[template] +#[rstest] +fn base(#[with(42)] fix: u32, #[values(1,2,3)] v: u32) {} + +#[fixture] +fn fix(#[default(0)] inner: u32) -> u32 { + inner +} + +#[apply(base)] +fn use_it_with_fixture(fix: u32, v: u32) { + assert!(fix%v == 0); +} + +#[apply(base)] +fn use_it_without_fixture(v: u32) { + assert!(24 % v == 0); +} +``` + +Run also 6 tests: + +``` +running 6 tests +test use_it_with_fixture::v_1 ... ok +test use_it_without_fixture::v_1 ... ok +test use_it_with_fixture::v_3 ... ok +test use_it_without_fixture::v_2 ... ok +test use_it_without_fixture::v_3 ... ok +test use_it_with_fixture::v_2 ... ok +``` + +## Cavelets + +### `use rstest_resuse` at the top of your crate +You **should** add `use rstest_resuse` at the top of your crate: + +```rust +#[cfg(test)] +use rstest_reuse; +``` + +This is due `rstest_reuse::template` define a macro that need to call a `rstest_resuse`'s macro. +I hope to remove this in the future but for now we should live with it. + +Note that + +```rust +use rstest_reuse::*; +``` +is not enougth: this statment doesn't include `rstest_reuse` but just its public items. + +## `#[export]` Attribute + +:warning: **Version 0.5.0 introduce a breaking change** + +Now `#[export]` attribute give you the possibility to export your template across crates +but don't lift the macro definition at the top of your crate (that was the default behaviour +prior the 0.5.0 version). + +Now if you want put your template at the root of your crate you can define it in the root +module or reexport it at the top with something like the following line at the top of +your crate: + +```rust +pub use my::modules::path::of::my::template::my_template; +``` + +When you want to export your template you should also take care to declare `rstest_reuse` as `pub` +at the top of your crate to enable to use it from the modules that would import the template. + +So in this case in the crate that would export template you should put at the root of your +crate + +```rust +#[cfg(test)] +pub use rstest_reuse; +``` + +And not just `use rstest_reuse` like in the standard cases. + +## Dismiss `#[macro_use]` Attribute Support + +:warning: **Version 0.5.0 introduce a breaking change** + +Till version 0.4.0 you can use `#[macro_use]` to annotate your modules and lift your +macro template to the up level. Now `rstest` leverege only on import and paths like all +othter function and original macro is hidden by a random name. + +So now if you would use your template from other module you should import it like any +other symbol. + +```rust +mod inner { + pub(crate) mod sub { + use rstest_reuse::*; + #[template] + #[rstest(a, b, + case(2, 2), + case(4/2, 2), + ) + ] + fn two_simple_cases(a: u32, b: u32) {} + } +} +use rstest_reuse::*; +use rstest::*; + +#[apply(inner::sub::two_simple_cases)] +fn it_works_by_path(a: u32, b: u32) { + assert!(a == b); +} + +use inner::sub::two_simple_cases +#[apply(inner::sub::two_simple_cases)] +fn it_works_after_use(a: u32, b: u32) { + assert!(a == b); +} + +``` + +## Disclamer + +This crate is in a development stage. I don't know if I'll include it in `rstest` or change some syntax in the future. + +I did't test it in a lot of cases: if you have some cases where it doesn't works file a ticket on [`rstest`][rstest-link] + + +## License + +Licensed under either of + +* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or +[license-apache-link]) + +* MIT license [LICENSE-MIT](LICENSE-MIT) or [license-MIT-link] +at your option. + +[//]: # (links) + +[crate-image]: https://img.shields.io/crates/v/rstest_reuse.svg +[crate-link]: https://crates.io/crates/rstest_reuse +[test-action-image]: https://github.com/la10736/rstest/workflows/Test/badge.svg +[test-action-link]: https://github.com/la10736/rstest/actions?query=workflow:Test +[license-apache-image]: https://img.shields.io/badge/license-Apache2.0-blue.svg +[license-mit-image]: https://img.shields.io/badge/license-MIT-blue.svg +[license-apache-link]: http://www.apache.org/licenses/LICENSE-2.0 +[license-MIT-link]: http://opensource.org/licenses/MIT +[rstest-link]: https://github.com/la10736/rstest diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..e6d6328 --- /dev/null +++ b/build.rs @@ -0,0 +1,10 @@ +use rustc_version::{Version, version}; + +fn main() { + let ver = version().unwrap(); + assert!(ver.major >= 1); + + if ver <= Version::parse("1.50.0").unwrap() { + println!("cargo:rustc-cfg=sanitize_multiple_should_panic_compiler_bug"); + } +} diff --git a/checkoutlist.md b/checkoutlist.md new file mode 100644 index 0000000..ebc1f0b --- /dev/null +++ b/checkoutlist.md @@ -0,0 +1,15 @@ +# TODO list + +- [ ] Update rustup +- [ ] Update dependency `cargo upgrade` +- [ ] Run all test + - [ ] Stable: `RSTEST_TEST_CHANNEL=stable; cargo +${RSTEST_TEST_CHANNEL} test` + - [ ] Beta: `RSTEST_TEST_CHANNEL=beta; cargo +${RSTEST_TEST_CHANNEL} test` + - [ ] Nightly: `RSTEST_TEST_CHANNEL=nightly; cargo +${RSTEST_TEST_CHANNEL} test` +- [ ] Check Cargo.toml version +- [ ] Check README +- [ ] prepare deploy `cargo publish --dry-run` +- [ ] deploy `cargo publish` +- [ ] Update `rstest` dependency +- [ ] Change next version + - [ ] `Cargo.toml` diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..5372aab --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,455 @@ +//! # Reuse `rstest`'s parametrized cases +//! +//! This crate give a way to define a tests set and apply them to every case you need to +//! test. +//! +//! With `rstest` crate you can define a tests list but if you want to apply the same tests +//! to another test function you must rewrite all cases or write some macros that do the job. +//! Both solutions have some drawbreak: +//! +//! - introduce duplication +//! - macros makes code harder to read and shift out the focus from tests core +//! +//! The aim of this crate is solve this problem. `rstest_reuse` expose two attributes: +//! +//! - `#[template]`: to define a template +//! - `#[apply]`: to apply a defined template to create tests +//! +//! Here is a simple example: +//! +//! ``` +//! use rstest::rstest; +//! use rstest_reuse::{self, *}; +//! +//! // Here we define the template. This define +//! // * The test list name to `two_simple_cases` +//! // * cases: here two cases that feed the `a`, `b` values +//! #[template] +//! #[rstest] +//! #[case(2, 2)] +//! #[case(4/2, 2)] +//! fn two_simple_cases(#[case] a: u32,#[case] b: u32) {} +//! +//! // Here we apply the `two_simple_cases` template: That is expanded in +//! // #[rstest] +//! // #[case(2, 2)] +//! // #[case(4/2, 2)] +//! // fn it_works(#[case] a: u32,#[case] b: u32) { +//! // assert!(a == b); +//! // } +//! #[apply(two_simple_cases)] +//! fn it_works(a: u32, b: u32) { +//! assert!(a == b); +//! } +//! +//! +//! // Here we reuse the `two_simple_cases` template to create two +//! // other tests +//! #[apply(two_simple_cases)] +//! fn it_fail(a: u32, b: u32) { +//! assert!(a != b); +//! } +//! ``` +//! If we run `cargo test` we have: +//! +//! ```text +//! Finished test [unoptimized + debuginfo] target(s) in 0.05s +//! Running target/debug/deps/playground-8a1212f8b5eb00ce +//! +//! running 4 tests +//! test it_fail::case_1 ... FAILED +//! test it_works::case_1 ... ok +//! test it_works::case_2 ... ok +//! test it_fail::case_2 ... FAILED +//! +//! failures: +//! +//! ---- it_fail::case_1 stdout ---- +//! thread 'it_fail::case_1' panicked at 'assertion failed: a != b', src/main.rs:34:5 +//! note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace +//! +//! ---- it_fail::case_2 stdout ---- +//! thread 'it_fail::case_2' panicked at 'assertion failed: a != b', src/main.rs:34:5 +//! +//! +//! failures: +//! it_fail::case_1 +//! it_fail::case_2 +//! +//! test result: FAILED. 2 passed; 2 failed; 0 ignored; 0 measured; 0 filtered out +//! +//! error: test failed, to rerun pass '--bin playground' +//! ``` +//! +//! Simple and neat! +//! +//! Note that if the test arguments names match the template's ones you can don't +//! repeate the arguments attributes. +//! +//! ## Composition and Values +//! +//! If you need to add some cases or values when apply a template you can leverage on +//! composition. Here a simple example: +//! +//! ``` +//! use rstest::rstest; +//! use rstest_reuse::{self, *}; +//! +//! #[template] +//! #[rstest] +//! #[case(2, 2)] +//! #[case(4/2, 2)] +//! fn base(#[case] a: u32, #[case] b: u32) {} +//! +//! // Here we add a new case and an argument in a value list: +//! #[apply(base)] +//! #[case(9/3, 3)] +//! fn it_works(a: u32, b: u32, #[values("a", "b")] t: &str) { +//! assert!(a == b); +//! assert!("abcd".contains(t)) +//! } +//! ``` +//! +//! `cargo test` runs 6 tests: +//! +//! ```text +//! running 6 tests +//! test it_works::case_1::t_2 ... ok +//! test it_works::case_2::t_2 ... ok +//! test it_works::case_2::t_1 ... ok +//! test it_works::case_3::t_2 ... ok +//! test it_works::case_3::t_1 ... ok +//! test it_works::case_1::t_1 ... ok +//! ``` +//! +//! Template can also used for `#[values]` and `#[with]` arguments if you need: +//! +//! ``` +//! use rstest::*; +//! use rstest_reuse::{self, *}; +//! +//! #[template] +//! #[rstest] +//! fn base(#[with(42)] fix: u32, #[values(1,2,3)] v: u32) {} +//! +//! #[fixture] +//! fn fix(#[default(0)] inner: u32) -> u32 { +//! inner +//! } +//! +//! #[apply(base)] +//! fn use_it_with_fixture(fix: u32, v: u32) { +//! assert!(fix%v == 0); +//! } +//! +//! #[apply(base)] +//! fn use_it_without_fixture(v: u32) { +//! assert!(24 % v == 0); +//! } +//! ``` +//! +//! `cargo test` runs 6 tests: +//! +//! ```text +//! running 6 tests +//! test use_it_with_fixture::v_1 ... ok +//! test use_it_without_fixture::v_1 ... ok +//! test use_it_with_fixture::v_3 ... ok +//! test use_it_without_fixture::v_2 ... ok +//! test use_it_without_fixture::v_3 ... ok +//! test use_it_with_fixture::v_2 ... ok +//! ``` +//! +//! +//! ## Cavelets +//! +//! ### `use rstest_reuse` at the top of your crate +//! +//! You **should** add `use rstest_reuse` at the top of your crate: +//! +//! ``` +//! #[cfg(test)] +//! use rstest_reuse; +//! ``` +//! +//! This is due `rstest_reuse::template` define a macro that need to call a `rstest_reuse`'s macro. +//! I hope to remove this in the future but for now we should live with it. +//! +//! Note that +//! ``` +//! use rstest_reuse::*; +//! ``` +//! is not enougth: this statment doesn't include `rstest_reuse` but just its public items. +//! +//! ## Disclamer +//! +//! This crate is in developer stage. I don't know if I'll include it in `rstest` or changing some syntax in +//! the future. +//! +//! I did't test it in a lot of cases: if you have some cases where it doesn't works file a ticket on +//! [`rstest`](https://github.com/la10736/rstest) + +extern crate proc_macro; +use std::collections::HashMap; + +use proc_macro::TokenStream; +use quote::{format_ident, quote}; +use syn::{ + self, parse, parse::Parse, parse_macro_input, Attribute, Ident, ItemFn, PatType, Path, Token, +}; + +struct MergeAttrs { + template: ItemFn, + function: ItemFn, +} + +impl Parse for MergeAttrs { + fn parse(input: parse::ParseStream) -> syn::Result { + let template = input.parse()?; + let _comma: Token![,] = input.parse()?; + let function = input.parse()?; + Ok(Self { template, function }) + } +} + +#[cfg(sanitize_multiple_should_panic_compiler_bug)] +fn is_should_panic(attr: &syn::Attribute) -> bool { + let should_panic: Ident = syn::parse_str("should_panic").unwrap(); + attr.path.is_ident(&should_panic) +} + +#[cfg(sanitize_multiple_should_panic_compiler_bug)] +fn sanitize_should_panic_duplication_bug( + mut attributes: Vec, +) -> Vec { + if attributes.len() != 2 || attributes[0] != attributes[1] || !is_should_panic(&attributes[0]) { + // Nothing to do + return attributes; + } + attributes.pop(); + attributes +} + +fn collect_template_args(template: &ItemFn) -> HashMap<&Ident, &PatType> { + template + .sig + .inputs + .iter() + .filter_map(|arg| match arg { + syn::FnArg::Typed(a) => Some(a), + _ => None, + }) + .filter_map(|arg| match *arg.pat { + syn::Pat::Ident(ref id) => Some((&id.ident, arg)), + _ => None, + }) + .collect() +} + +fn merge_arg_attributes(dest: &mut Vec, source: &[Attribute]) { + for s in source.iter() { + if !dest.contains(s) { + dest.push(s.clone()) + } + } +} + +fn resolve_template_arg<'a>( + template: &HashMap<&'a Ident, &'a PatType>, + arg: &Ident, +) -> Option<&'a PatType> { + let id_name = arg.to_string(); + match (template.get(arg), id_name.starts_with('_')) { + (Some(&arg), _) => Some(arg), + (None, true) => template.get(&format_ident!("{}", id_name[1..])).copied(), + _ => None, + } +} + +fn expand_function_arguments(dest: &mut ItemFn, source: &ItemFn) { + let to_merge_args = collect_template_args(source); + + for arg in dest.sig.inputs.iter_mut() { + if let syn::FnArg::Typed(a) = arg { + if let syn::Pat::Ident(ref id) = *a.pat { + if let Some(source_arg) = resolve_template_arg(&to_merge_args, &id.ident) { + merge_arg_attributes(&mut a.attrs, &source_arg.attrs); + } + } + } + } +} + +#[doc(hidden)] +#[proc_macro] +pub fn merge_attrs(item: TokenStream) -> TokenStream { + let MergeAttrs { + template, + mut function, + } = parse_macro_input!(item as MergeAttrs); + + expand_function_arguments(&mut function, &template); + + let mut attrs = template.attrs; + #[cfg(sanitize_multiple_should_panic_compiler_bug)] + { + function.attrs = sanitize_should_panic_duplication_bug(function.attrs); + } + attrs.append(&mut function.attrs); + function.attrs = attrs; + + let tokens = quote! { + #function + }; + tokens.into() +} + +fn get_export(attributes: &[Attribute]) -> Option<&Attribute> { + attributes + .iter() + .find(|&attr| attr.path.is_ident(&format_ident!("export"))) +} + +/// Define a template where the name is given from the function name. This attribute register all +/// attributes. The function signature don't really mater but to make it clear is better that you +/// use a signature like if you're wrinting a standard `rstest`. +/// +/// If you need to export the template at the root of your crate or use it from another crate you +/// should annotate it with `#[export]` attribute. This attribute add `#[macro_export]` attribute to +/// the template macro and make possible to use it from another crate. +/// +/// When define a template you can also set the arguments attributes like `#[case]`, `#[values]` +/// and `#[with]`: when you apply it attributes will be copied to the matched by name arguments. +/// +#[proc_macro_attribute] +pub fn template(_args: proc_macro::TokenStream, input: proc_macro::TokenStream) -> TokenStream { + let mut template: ItemFn = parse(input).unwrap(); + + let rstest_index = template + .attrs + .iter() + .position(|attr| attr.path.is_ident(&format_ident!("rstest"))); + + let mut attributes = template.attrs; + + template.attrs = match rstest_index { + Some(idx) => attributes.split_off(idx), + None => std::mem::take(&mut attributes), + }; + + let (macro_attribute, visibility) = match get_export(&attributes) { + Some(_) => ( + quote! { + #[macro_export] + }, + quote! { + pub + }, + ), + None => ( + quote! {}, + quote! { + pub(crate) + }, + ), + }; + + let macro_name = template.sig.ident.clone(); + let macro_name_rand = format_ident!("{}_{}", macro_name, rand::random::()); + + let tokens = quote! { + /// Apply #macro_name template to given body + #macro_attribute + macro_rules! #macro_name_rand { + ( $test:item ) => { + $crate::rstest_reuse::merge_attrs! { + #template, + $test + } + } + } + #[allow(unused_imports)] + #visibility use #macro_name_rand as #macro_name; + }; + tokens.into() +} + +/// Apply a defined template. The function signature should satisfy the template attributes +/// but can also add some other fixtures. +/// Example: +/// +/// ``` +/// use rstest::{rstest, fixture}; +/// use rstest_reuse::{self, *}; +/// +/// #[fixture] +/// fn empty () -> Vec { +/// Vec::new() +/// } +/// +/// #[template] +/// #[rstest] +/// #[case(2, 2)] +/// #[case(4/2, 2)] +/// fn two_simple_cases(#[case] a: u32, #[case] b: u32) {} +/// +/// #[apply(two_simple_cases)] +/// fn it_works(mut empty: Vec, a: u32, b: u32) { +/// empty.append(a); +/// assert!(empty.last() == b); +/// } +/// ``` +/// When use `#[apply]` you can also +/// 1. Use a path for template +/// 2. Ignore an argument by underscore +/// 3. add some cases +/// 4. add some values +/// +/// +/// ``` +/// use rstest::{rstest, fixture}; +/// use rstest_reuse::{self, *}; +/// +/// #[fixture] +/// fn fix (#[default(0)] inner: u32) -> u32 { +/// inner +/// } +/// +/// mod outer { +/// pub(crate) mod inner { +/// use rstest_reuse::template; +/// +/// #[template] +/// #[rstest] +/// #[case(2, 2)] +/// #[case(4/2, 2)] +/// fn two_simple_cases(#[case] a: u32, #[case] b: u32) {} +/// } +/// } +/// +/// +/// #[apply(outer::inner::two_simple_cases)] +/// // Add a case +/// #[case(9/3, 3)] +/// // Use fixture with 42 as argument +/// // Ignore b case values +/// // add 2 cases with other in 4, 5 for each case +/// fn lot_of_tests(fix: u32, a: u32, _b: u32, #[values(4, 5)] other: u32) { +/// assert_eq!(fix, 42); +/// assert_eq!(a, 2); +/// assert!([4, 5].contains(other)); +/// } +/// ``` +/// + +#[proc_macro_attribute] +pub fn apply(args: proc_macro::TokenStream, input: proc_macro::TokenStream) -> TokenStream { + let template: Path = parse(args).unwrap(); + let test: ItemFn = parse(input).unwrap(); + let tokens = quote! { + #template! { + #test + } + }; + tokens.into() +} diff --git a/tests/acceptance.rs b/tests/acceptance.rs new file mode 100644 index 0000000..0309c03 --- /dev/null +++ b/tests/acceptance.rs @@ -0,0 +1,189 @@ +use rstest_test::{ + assert_in, assert_not_in, sanitize_name, testname, Project, Stringable, TestResults, +}; + +use lazy_static::lazy_static; + +use rstest::rstest; +use std::path::{Path, PathBuf}; +use temp_testdir::TempDir; + +pub fn resources>(name: O) -> PathBuf { + Path::new("tests").join("resources").join(name) +} + +fn create_prj(name: &str) -> Project { + let prj = ROOT_PROJECT.subproject(name); + prj.add_local_dependency("rstest_reuse"); + prj.add_dependency("rstest", r#""*""#); + prj +} + +fn prj(res: impl AsRef) -> Project { + let prj_name = sanitize_name(testname()); + + let prj = create_prj(&prj_name); + prj.set_code_file(resources(res)) +} + +fn run_test(res: impl AsRef) -> (std::process::Output, String) { + let prj = prj(res); + ( + prj.run_tests().unwrap(), + prj.get_name().to_owned().to_string(), + ) +} + +#[test] +fn simple_example() { + let (output, _) = run_test("simple_example.rs"); + + TestResults::new() + .ok("it_works::case_1") + .ok("it_works::case_2") + .fail("it_fail::case_1") + .fail("it_fail::case_2") + .ok("it_fail_but_ok::case_1") + .ok("it_fail_but_ok::case_2") + .assert(output); +} + +#[test] +fn use_before_define() { + let (output, _) = run_test("use_before_define.rs"); + + TestResults::new() + .ok("it_works::case_1") + .ok("it_works::case_2") + .assert(output); +} + +#[rstest] +#[case::simple("simple_example.rs")] +#[case::export_not_used("export_not_used.rs")] +fn not_show_any_warning(#[case] path: &str) { + let (output, _) = run_test(path); + + assert_not_in!(output.stderr.str(), "warning:"); +} + +#[test] +fn should_show_warning_if_not_used_template() { + let (output, _) = run_test("not_used.rs"); + + assert_in!(output.stderr.str(), "warning:"); +} + +#[test] +fn in_mod() { + let (output, _) = run_test("in_mod.rs"); + + TestResults::new() + .ok("sub::it_works::case_1") + .ok("sub::it_works::case_2") + .fail("sub::it_fail::case_1") + .fail("sub::it_fail::case_2") + .assert(output); +} + +#[test] +fn import_from_mod() { + let (output, _) = run_test("qualify_template_use.rs"); + + TestResults::new() + .ok("user::it_works::case_1") + .ok("user::it_works::case_2") + .ok("qualify::it_works::case_1") + .ok("qualify::it_works::case_2") + .assert(output); +} + +#[test] +fn copy_case_attributes_from_template() { + let (output, _) = run_test("copy_args_attributes_from_template.rs"); + + TestResults::new() + .ok("cases::it_works::case_1") + .ok("cases::it_works::case_2") + .ok("cases::should_not_copy_attributes_if_already_present::case_1") + .ok("cases::should_not_copy_attributes_if_already_present::case_2") + .ok("cases::add_a_case::case_1") + .ok("cases::add_a_case::case_2") + .ok("cases::add_a_case::case_3_more") + .ok_in("cases::add_values::case_1::_add_some_tests_1") + .ok_in("cases::add_values::case_1::_add_some_tests_2") + .ok_in("cases::add_values::case_1::_add_some_tests_3") + .ok_in("cases::add_values::case_2::_add_some_tests_1") + .ok_in("cases::add_values::case_2::_add_some_tests_2") + .ok_in("cases::add_values::case_2::_add_some_tests_3") + .ok("cases::should_copy_cases_also_from_underscored_attrs::case_1") + .ok("cases::should_copy_cases_also_from_underscored_attrs::case_2") + .ok_in("values::it_works::cases_1") + .ok_in("values::it_works::cases_2") + .ok_in("values::add_a_case::case_1_more::cases_1") + .ok_in("values::add_a_case::case_1_more::cases_2") + .ok_with("values::add_values::a_1", false, 2) + .ok_with("values::add_values::a_2", false, 2) + .ok_in("values::should_copy_values_also_from_underscored_attrs::_cases_1") + .ok_in("values::should_copy_values_also_from_underscored_attrs::_cases_2") + .assert(output); +} + +#[test] +fn deny_docs() { + let (output, _) = run_test("deny_docs.rs"); + + TestResults::new() + .ok("it_works::case_1") + .ok("it_works::case_2") + .assert(output); +} + +#[test] +fn enable_export_macros() { + let (output, _) = run_test("export_template.rs"); + + TestResults::new() + .ok("foo::bar::test::case_1") + .ok("test_path::case_1") + .ok("test_import::case_1") + .assert(output); +} + +#[test] +fn use_same_name_for_more_templates() { + let (output, _) = run_test("templates_with_same_name.rs"); + + TestResults::new() + .ok("inner1::it_works::case_1") + .ok("inner1::it_works::case_2") + .ok("inner2::it_works::case_1") + .ok("inner2::it_works::case_2") + .assert(output); +} + +#[test] +fn no_local_macro_should_not_compile() { + let (output, _) = run_test("no_local_macro_should_not_compile.rs"); + + assert!(!output.status.success()); +} + +#[test] +fn should_export_main_root() { + // Add project with template + let _prj_template = + create_prj("export_template_root").set_code_file(resources("export_template_root.rs")); + + // Main test project that use template + let prj = prj("import_template.rs"); + prj.add_path_dependency("export_template_root", "../export_template_root"); + + let output = prj.run_tests().unwrap(); + TestResults::new().ok("test::case_1").assert(output); +} + +lazy_static! { + static ref ROOT_DIR: TempDir = TempDir::new(std::env::temp_dir().join("rstest_reuse"), false); + static ref ROOT_PROJECT: Project = Project::new(ROOT_DIR.as_ref()); +} diff --git a/tests/resources/copy_args_attributes_from_template.rs b/tests/resources/copy_args_attributes_from_template.rs new file mode 100644 index 0000000..39347a7 --- /dev/null +++ b/tests/resources/copy_args_attributes_from_template.rs @@ -0,0 +1,66 @@ +use rstest_reuse; + +mod cases { + use rstest::rstest; + use rstest_reuse::*; + + #[template] + #[rstest] + #[case(2, 2)] + #[case(4/2, 2)] + fn copy_cases(#[case] a: u32, #[case] b: u32) {} + + #[apply(copy_cases)] + fn it_works(a: u32, b: u32) { + assert!(a == b); + } + + #[apply(copy_cases)] + fn should_not_copy_attributes_if_already_present(#[case] a: u32, b: u32) { + assert!(a == b); + } + + #[apply(copy_cases)] + #[case::more(8/4, 2)] + fn add_a_case(a: u32, b: u32) { + assert!(a == b); + } + + #[apply(copy_cases)] + fn add_values(a: u32, b: u32, #[values(1, 2, 3)] _add_some_tests: u32) { + assert!(a == b); + } + + #[apply(copy_cases)] + fn should_copy_cases_also_from_underscored_attrs(_a: u32, _b: u32) {} +} + +mod values { + use rstest::rstest; + use rstest_reuse::*; + + #[template] + #[rstest] + fn copy_values(#[values(1, 2)] cases: u32) {} + + #[apply(copy_values)] + fn it_works(cases: u32) { + assert!([1, 2].contains(&cases)); + } + + #[apply(copy_values)] + #[case::more(8/4, 2)] + fn add_a_case(#[case] a: u32, #[case] b: u32, cases: u32) { + assert!([1, 2].contains(&cases)); + assert!(a == b); + } + + #[apply(copy_values)] + fn add_values(#[values(3, 4)] a: u32, cases: u32) { + assert!([1, 2].contains(&cases)); + assert!([3, 4].contains(&a)); + } + + #[apply(copy_values)] + fn should_copy_values_also_from_underscored_attrs(_cases: u32) {} +} diff --git a/tests/resources/deny_docs.rs b/tests/resources/deny_docs.rs new file mode 100644 index 0000000..04dda7a --- /dev/null +++ b/tests/resources/deny_docs.rs @@ -0,0 +1,13 @@ +#![deny(missing_docs)] + +use rstest::rstest; +use rstest_reuse::{self, *}; + +#[template] +#[rstest(a, b, case(2, 2), case(4/2, 2))] +fn two_simple_cases(a: u32, b: u32) {} + +#[apply(two_simple_cases)] +fn it_works(a: u32, b: u32) { + assert!(a == b); +} diff --git a/tests/resources/export_not_used.rs b/tests/resources/export_not_used.rs new file mode 100644 index 0000000..b58ec19 --- /dev/null +++ b/tests/resources/export_not_used.rs @@ -0,0 +1,9 @@ +mod foo { + use rstest_reuse::template; + + #[template] + #[export] + #[rstest] + #[case("bar")] + fn not_used(#[case] s: &str) {} +} diff --git a/tests/resources/export_template.rs b/tests/resources/export_template.rs new file mode 100644 index 0000000..534eae9 --- /dev/null +++ b/tests/resources/export_template.rs @@ -0,0 +1,39 @@ +use rstest_reuse; + +mod foo { + pub(crate) mod bar { + use rstest::rstest; + use rstest_reuse::{self, *}; + + #[template] + #[export] + #[rstest] + #[case("bar")] + fn my_template(#[case] s: &str) {} + + #[apply(my_template)] + fn test(#[case] s: &str) { + assert_eq!("bar", s); + } + } +} + +use rstest::rstest; +use rstest_reuse::*; + +#[apply(foo::bar::my_template)] +fn test_path(#[case] s: &str) { + assert_eq!("bar", s); +} + +use foo::bar::my_template; +#[apply(my_template)] +fn test_import(#[case] s: &str) { + assert_eq!("bar", s); +} + +#[template] +#[export] +#[rstest] +#[case("bar")] +fn root_level(#[case] s: &str) {} diff --git a/tests/resources/export_template_root.rs b/tests/resources/export_template_root.rs new file mode 100644 index 0000000..8c7a33a --- /dev/null +++ b/tests/resources/export_template_root.rs @@ -0,0 +1,8 @@ +pub use rstest_reuse; +use rstest_reuse::template; + +#[template] +#[export] +#[rstest] +#[case("bar")] +fn root_level(#[case] s: &str) {} diff --git a/tests/resources/import_template.rs b/tests/resources/import_template.rs new file mode 100644 index 0000000..129b52d --- /dev/null +++ b/tests/resources/import_template.rs @@ -0,0 +1,8 @@ +use export_template_root::root_level; +use rstest::*; +use rstest_reuse::apply; + +#[apply(root_level)] +fn test(#[case] s: &str) { + assert_eq!("bar", s); +} diff --git a/tests/resources/in_mod.rs b/tests/resources/in_mod.rs new file mode 100644 index 0000000..80f0b36 --- /dev/null +++ b/tests/resources/in_mod.rs @@ -0,0 +1,20 @@ +use rstest_reuse; + +mod sub { + use rstest::rstest; + use rstest_reuse::*; + + #[template] + #[rstest(a, b, case(2, 2), case(4/2, 2))] + fn two_simple_cases(a: u32, b: u32) {} + + #[apply(two_simple_cases)] + fn it_works(a: u32, b: u32) { + assert!(a == b); + } + + #[apply(two_simple_cases)] + fn it_fail(a: u32, b: u32) { + assert!(a != b); + } +} diff --git a/tests/resources/no_local_macro_should_not_compile.rs b/tests/resources/no_local_macro_should_not_compile.rs new file mode 100644 index 0000000..b45d6df --- /dev/null +++ b/tests/resources/no_local_macro_should_not_compile.rs @@ -0,0 +1,17 @@ +use rstest_reuse; + +mod foo { + use rstest_reuse::{self, *}; + + #[template] + #[rstest] + #[case("bar")] + fn my_template(#[case] s: &str) {} +} +use rstest::rstest; +use rstest_reuse::apply; + +#[apply(my_template)] +fn test(#[case] s: &str) { + assert_eq!("bar", s); +} diff --git a/tests/resources/not_used.rs b/tests/resources/not_used.rs new file mode 100644 index 0000000..7fbb04d --- /dev/null +++ b/tests/resources/not_used.rs @@ -0,0 +1,8 @@ +mod foo { + use rstest_reuse::template; + + #[template] + #[rstest] + #[case("bar")] + fn not_used(#[case] s: &str) {} +} diff --git a/tests/resources/qualify_template_use.rs b/tests/resources/qualify_template_use.rs new file mode 100644 index 0000000..1f4b5ef --- /dev/null +++ b/tests/resources/qualify_template_use.rs @@ -0,0 +1,31 @@ +use rstest_reuse; + +mod template { + use rstest::rstest; + use rstest_reuse::template; + + #[template] + #[rstest(a, b, case(2, 2), case(4/2, 2))] + fn two_simple_cases(a: u32, b: u32) {} +} + +mod user { + use rstest::rstest; + use rstest_reuse::apply; + use crate::template::two_simple_cases; + + #[apply(two_simple_cases)] + fn it_works(a: u32, b: u32) { + assert!(a == b); + } +} + +mod qualify { + use rstest::rstest; + use rstest_reuse::apply; + + #[apply(crate::template::two_simple_cases)] + fn it_works(a: u32, b: u32) { + assert!(a == b); + } +} diff --git a/tests/resources/simple_example.rs b/tests/resources/simple_example.rs new file mode 100644 index 0000000..01388f2 --- /dev/null +++ b/tests/resources/simple_example.rs @@ -0,0 +1,22 @@ +use rstest::rstest; +use rstest_reuse::{self, *}; + +#[template] +#[rstest(a, b, case(2, 2), case(4/2, 2))] +fn two_simple_cases(a: u32, b: u32) {} + +#[apply(two_simple_cases)] +fn it_works(a: u32, b: u32) { + assert!(a == b); +} + +#[apply(two_simple_cases)] +fn it_fail(a: u32, b: u32) { + assert!(a != b); +} + +#[apply(two_simple_cases)] +#[should_panic] +fn it_fail_but_ok(a: u32, b: u32) { + assert!(a != b); +} diff --git a/tests/resources/templates_with_same_name.rs b/tests/resources/templates_with_same_name.rs new file mode 100644 index 0000000..7a3554a --- /dev/null +++ b/tests/resources/templates_with_same_name.rs @@ -0,0 +1,27 @@ +use rstest_reuse; + +mod inner1 { + use rstest::rstest; + use rstest_reuse::*; + + #[template] + #[rstest(a, b, case(2, 2), case(4/2, 2))] + fn my_template(a: u32, b: u32) {} + + #[apply(my_template)] + fn it_works(a: u32, b: u32) { + assert!(a == b); + } +} + +mod inner2 { + use rstest::rstest; + use rstest_reuse::*; + + #[template] + #[rstest(a, case(2), case(4))] + fn my_template(a: u32) {} + + #[apply(my_template)] + fn it_works(a: u32) {} +} diff --git a/tests/resources/use_before_define.rs b/tests/resources/use_before_define.rs new file mode 100644 index 0000000..e5c0adb --- /dev/null +++ b/tests/resources/use_before_define.rs @@ -0,0 +1,11 @@ +use rstest::rstest; +use rstest_reuse::{self, *}; + +#[apply(two_simple_cases)] +fn it_works(a: u32, b: u32) { + assert!(a == b); +} + +#[template] +#[rstest(a, b, case(2, 2), case(4/2, 2))] +fn two_simple_cases(a: u32, b: u32) {} -- 2.34.1