Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]}
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) }
```
10 changes: 10 additions & 0 deletions examples/readme/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<usize>().is_ok()));
}

test_each_file! { #[tokio::test] async in "./examples/readme/resources_simple/" as simple => run }
}
}
63 changes: 56 additions & 7 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,17 @@ 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 {
path: LitStr,
module: Option<Ident>,
function: Expr,
extensions: Vec<String>,
attributes: Vec<Meta>,
async_fn: Option<Async>,
}

macro_rules! abort {
Expand All @@ -30,6 +33,33 @@ macro_rules! abort_token_stream {

impl Parse for TestEachArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
// Optionally parse attributes if `#` is used. Aborts if none are given.
let attributes: Vec<Meta> = input
.parse::<Token![#]>()
.and_then(|_| {
let content;
bracketed!(content in input);

match Punctuated::<Meta, Token![,]>::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::<Token![async]>() {
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::<Token![for]>()
Expand Down Expand Up @@ -81,6 +111,8 @@ impl Parse for TestEachArgs {
module,
function,
extensions,
attributes,
async_fn,
})
}
}
Expand Down Expand Up @@ -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(())
Expand Down
Loading