Skip to content

Commit 962263f

Browse files
Merge pull request #10 from robgjansen/attr
Support adding attributes to each test and async tests
2 parents aed364f + 7ff8df4 commit 962263f

4 files changed

Lines changed: 82 additions & 8 deletions

File tree

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,6 @@ quote = "1.0.36"
1717
proc-macro2 = "1.0.81"
1818
syn = { version = "2.0.59", features = ["full"] }
1919
unicode-ident = "1.0.12"
20+
21+
[dev-dependencies]
22+
tokio = { version = "1.45.0" , features = ["rt", "macros"]}

README.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,20 @@ The expression that is called on each file can also be a closure, for example:
117117
test_each_file! { in "./resources" => |c: &str| assert!(c.contains("Hello World")) }
118118
```
119119

120+
Multiple attributes can optionally be applied to each test, for example:
121+
122+
```rust
123+
test_each_file! { #[ignore, cfg(target_os = "linux")] in "./resources" => test }
124+
```
125+
126+
You can specify that each test is async, in this case a test macro such as `#[tokio::test]` must be specified explicitly. For example:
127+
128+
```rust
129+
test_each_file! { #[tokio::test] async in "./resources" => test }
130+
```
131+
120132
All the options above can be combined, for example:
121133

122134
```rust
123-
test_each_file! { for ["in", "out"] in "./resources" as example => |[a, b]: [&str; 2]| assert_eq!(a, b) }
135+
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) }
124136
```

examples/readme/main.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,14 @@ mod tests {
6464

6565
test_each_file! { in "./examples/readme/duplicate_names/" => empty}
6666
}
67+
68+
mod attributes_async {
69+
use test_each_file::test_each_file;
70+
71+
async fn run(input: &str) {
72+
assert!(input.split_whitespace().all(|n| n.parse::<usize>().is_ok()));
73+
}
74+
75+
test_each_file! { #[tokio::test] async in "./examples/readme/resources_simple/" as simple => run }
76+
}
6777
}

src/lib.rs

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,17 @@ use std::ffi::OsString;
66
use std::path::{Path, PathBuf};
77
use syn::parse::{Parse, ParseStream};
88
use syn::punctuated::Punctuated;
9-
use syn::{bracketed, parse_macro_input, Expr, LitStr, Token};
9+
use syn::token::Async;
10+
use syn::{bracketed, parse_macro_input, Expr, LitStr, Meta, Token};
1011
use unicode_ident::{is_xid_continue, is_xid_start};
1112

1213
struct TestEachArgs {
1314
path: LitStr,
1415
module: Option<Ident>,
1516
function: Expr,
1617
extensions: Vec<String>,
18+
attributes: Vec<Meta>,
19+
async_fn: Option<Async>,
1720
}
1821

1922
macro_rules! abort {
@@ -30,6 +33,33 @@ macro_rules! abort_token_stream {
3033

3134
impl Parse for TestEachArgs {
3235
fn parse(input: ParseStream) -> syn::Result<Self> {
36+
// Optionally parse attributes if `#` is used. Aborts if none are given.
37+
let attributes: Vec<Meta> = input
38+
.parse::<Token![#]>()
39+
.and_then(|_| {
40+
let content;
41+
bracketed!(content in input);
42+
43+
match Punctuated::<Meta, Token![,]>::parse_separated_nonempty(&content) {
44+
Ok(attributes) => Ok(attributes.into_iter().collect()),
45+
Err(e) => abort!(e.span(), "Expected at least one attribute to be given."),
46+
}
47+
})
48+
.unwrap_or_default();
49+
50+
// Optionally mark as async.
51+
// The async keyword is the error span if we did not specify an attribute.
52+
let async_span = input.span();
53+
let async_fn = match input.parse::<Token![async]>() {
54+
Ok(token) => {
55+
if attributes.is_empty() {
56+
abort!(async_span, "Expected at least one attribute (e.g., `#[tokio::test]`) when `async` is given.");
57+
}
58+
Some(token)
59+
}
60+
Err(_) => None,
61+
};
62+
3363
// Optionally parse extensions if the keyword `for` is used. Aborts if none are given.
3464
let extensions = input
3565
.parse::<Token![for]>()
@@ -81,6 +111,8 @@ impl Parse for TestEachArgs {
81111
module,
82112
function,
83113
extensions,
114+
attributes,
115+
async_fn,
84116
})
85117
}
86118
}
@@ -231,12 +263,29 @@ fn generate_from_tree(
231263
quote!([#arguments])
232264
};
233265

234-
stream.extend(quote! {
235-
#[test]
236-
fn #file_name() {
237-
(#function)(#arguments)
238-
}
239-
});
266+
for attribute in &parsed.attributes {
267+
stream.extend(quote! {
268+
#[#attribute]
269+
});
270+
}
271+
272+
if let Some(async_keyword) = &parsed.async_fn {
273+
// For async functions, we'd need something like `#[tokio::test]` instead of `#[test]`.
274+
// Here we assume the user will have already provided that in the list of attributes.
275+
stream.extend(quote! {
276+
#async_keyword fn #file_name() {
277+
(#function)(#arguments).await
278+
}
279+
});
280+
} else {
281+
// Default, non-async test.
282+
stream.extend(quote! {
283+
#[test]
284+
fn #file_name() {
285+
(#function)(#arguments)
286+
}
287+
});
288+
}
240289
}
241290

242291
Ok(())

0 commit comments

Comments
 (0)