diff --git a/Cargo.toml b/Cargo.toml index f3eec51..37c8061 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,3 +17,6 @@ quote = "1.0.36" proc-macro2 = "1.0.81" syn = { version = "2.0.59", features = ["full"] } unicode-ident = "1.0.12" + +[dev-dependencies] +tokio = { version = "1.45.0" , features = ["rt", "macros"]} \ No newline at end of file diff --git a/README.md b/README.md index 668ee2c..e8301ca 100644 --- a/README.md +++ b/README.md @@ -117,8 +117,20 @@ The expression that is called on each file can also be a closure, for example: test_each_file! { in "./resources" => |c: &str| assert!(c.contains("Hello World")) } ``` +Multiple attributes can optionally be applied to each test, for example: + +```rust +test_each_file! { #[ignore, cfg(target_os = "linux")] in "./resources" => test } +``` + +You can specify that each test is async, in this case a test macro such as `#[tokio::test]` must be specified explicitly. For example: + +```rust +test_each_file! { #[tokio::test] async in "./resources" => test } +``` + All the options above can be combined, for example: ```rust -test_each_file! { for ["in", "out"] in "./resources" as example => |[a, b]: [&str; 2]| assert_eq!(a, b) } +test_each_file! { #[tokio::test, ignore, cfg(target_os = "linux")] async for ["in", "out"] in "./resources" as example => async |[a, b]: [&str; 2]| assert_eq!(a, b) } ``` diff --git a/examples/readme/main.rs b/examples/readme/main.rs index 2e985c9..a41b8e0 100644 --- a/examples/readme/main.rs +++ b/examples/readme/main.rs @@ -64,4 +64,14 @@ mod tests { test_each_file! { in "./examples/readme/duplicate_names/" => empty} } + + mod attributes_async { + use test_each_file::test_each_file; + + async fn run(input: &str) { + assert!(input.split_whitespace().all(|n| n.parse::().is_ok())); + } + + test_each_file! { #[tokio::test] async in "./examples/readme/resources_simple/" as simple => run } + } } diff --git a/src/lib.rs b/src/lib.rs index cfd1431..0e3a761 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,7 +6,8 @@ use std::ffi::OsString; use std::path::{Path, PathBuf}; use syn::parse::{Parse, ParseStream}; use syn::punctuated::Punctuated; -use syn::{bracketed, parse_macro_input, Expr, LitStr, Token}; +use syn::token::Async; +use syn::{bracketed, parse_macro_input, Expr, LitStr, Meta, Token}; use unicode_ident::{is_xid_continue, is_xid_start}; struct TestEachArgs { @@ -14,6 +15,8 @@ struct TestEachArgs { module: Option, function: Expr, extensions: Vec, + attributes: Vec, + async_fn: Option, } macro_rules! abort { @@ -30,6 +33,33 @@ macro_rules! abort_token_stream { impl Parse for TestEachArgs { fn parse(input: ParseStream) -> syn::Result { + // Optionally parse attributes if `#` is used. Aborts if none are given. + let attributes: Vec = input + .parse::() + .and_then(|_| { + let content; + bracketed!(content in input); + + match Punctuated::::parse_separated_nonempty(&content) { + Ok(attributes) => Ok(attributes.into_iter().collect()), + Err(e) => abort!(e.span(), "Expected at least one attribute to be given."), + } + }) + .unwrap_or_default(); + + // Optionally mark as async. + // The async keyword is the error span if we did not specify an attribute. + let async_span = input.span(); + let async_fn = match input.parse::() { + Ok(token) => { + if attributes.is_empty() { + abort!(async_span, "Expected at least one attribute (e.g., `#[tokio::test]`) when `async` is given."); + } + Some(token) + } + Err(_) => None, + }; + // Optionally parse extensions if the keyword `for` is used. Aborts if none are given. let extensions = input .parse::() @@ -81,6 +111,8 @@ impl Parse for TestEachArgs { module, function, extensions, + attributes, + async_fn, }) } } @@ -231,12 +263,29 @@ fn generate_from_tree( quote!([#arguments]) }; - stream.extend(quote! { - #[test] - fn #file_name() { - (#function)(#arguments) - } - }); + for attribute in &parsed.attributes { + stream.extend(quote! { + #[#attribute] + }); + } + + if let Some(async_keyword) = &parsed.async_fn { + // For async functions, we'd need something like `#[tokio::test]` instead of `#[test]`. + // Here we assume the user will have already provided that in the list of attributes. + stream.extend(quote! { + #async_keyword fn #file_name() { + (#function)(#arguments).await + } + }); + } else { + // Default, non-async test. + stream.extend(quote! { + #[test] + fn #file_name() { + (#function)(#arguments) + } + }); + } } Ok(())