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
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,32 @@ fn test([input, output]: [&Path; 2]) {
}
```

## Ignoring specific tests

You can selectively ignore tests based on file names using the `ignore` keyword.
This is useful for tests that are slow, require external services, or should only be run explicitly.

Provide a map of file names to ignore reasons:

```rust
test_each_path! {
#[cfg(feature = "integration")]
for ["in", "out"]
in "./tests/integration"
as integration
=> test
ignore {
"slow" => "Load tests, ignored for taking two hours",
"external" => "Requires external services"
}
}
```

This will add `#[ignore = "reason"]` to matching tests, allowing you to document why certain tests are ignored.
The ignore patterns match against the tests' names.

Ignored tests can be run explicitly with `cargo test -- --ignored`.

## More examples

The expression that is called on each file can also be a closure, for example:
Expand Down
28 changes: 28 additions & 0 deletions examples/readme/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,32 @@ mod tests {

test_each_file! { #[tokio::test] async in "./examples/readme/resources_simple/" as simple => run }
}

mod ignore {
use std::path::Path;
use test_each_file::{test_each_file, test_each_path};

fn test_path(_input: &Path) {}
async fn test_async(_input: &str) {}

test_each_path! {
in "./examples/readme/resources_simple"
as basic
=> test_path
ignore: {
"a" => "Slow test"
}
}

test_each_file! {
#[tokio::test]
async
in "./examples/readme/resources_simple"
as with_async
=> test_async
ignore {
"b" => "Requires setup"
}
}
}
}
79 changes: 76 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#![doc = include_str!("../README.md")]
use proc_macro2::{Ident, Span, TokenStream};
use quote::{format_ident, quote};
use std::collections::{HashMap, HashSet};
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
use std::env;
use std::ffi::OsString;
use std::path::{Path, PathBuf};
Expand All @@ -18,6 +18,7 @@ struct TestEachArgs {
extensions: Vec<String>,
attributes: Vec<Meta>,
async_fn: Option<Async>,
ignore_patterns: IgnorePatterns,
}

macro_rules! abort {
Expand All @@ -32,6 +33,68 @@ macro_rules! abort_token_stream {
};
}

#[derive(Default)]
struct IgnorePatterns {
patterns: HashMap<String, String>,
}

impl Parse for IgnorePatterns {
fn parse(input: ParseStream) -> syn::Result<Self> {
if !input
.fork()
.parse::<Ident>()
.ok()
.is_some_and(|id| id == "ignore")
{
return Ok(IgnorePatterns::default());
}
let _: Ident = input.parse().unwrap();

// Optional ":" separator
let _ = input.parse::<Token![:]>();

let content;
syn::braced!(content in input);

let mut patterns = HashMap::new();
while !content.is_empty() {
let key: LitStr = match content.parse() {
Ok(k) => k,
Err(e) => abort!(
e.span(),
"Expected a string literal for ignore pattern name."
),
};

if let Err(e) = content.parse::<Token![=>]>() {
abort!(e.span(), "Expected `=>` after ignore pattern name.");
}

let reason: LitStr = match content.parse() {
Ok(r) => r,
Err(e) => abort!(e.span(), "Expected a string literal for ignore reason."),
};

patterns.insert(key.value(), reason.value());

// Optional comma
let _ = content.parse::<Token![,]>();
}

Ok(IgnorePatterns { patterns })
}
}

impl IgnorePatterns {
/// Check if a given name should be ignored
///
/// Returns Some(reason) if ignored, or None if not ignored
fn should_ignore(&self, name: &str) -> Option<String> {
let lookup_name = name.strip_prefix("r#").unwrap_or(name);
self.patterns.get(lookup_name).cloned()
}
}

impl Parse for TestEachArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
// Optionally parse attributes if `#` is used. Aborts if none are given.
Expand Down Expand Up @@ -107,21 +170,25 @@ impl Parse for TestEachArgs {
Err(e) => abort!(e.span(), "Expected a function to call after `=>`."),
};

// Optionally parse ignore patterns if the keyword `ignore` is used.
let ignore_patterns = IgnorePatterns::parse(input)?;

Ok(Self {
path,
module,
function,
extensions,
attributes,
async_fn,
ignore_patterns,
})
}
}

#[derive(Default)]
struct Tree {
children: HashMap<PathBuf, Tree>,
here: HashSet<PathBuf>,
children: BTreeMap<PathBuf, Tree>,
here: BTreeSet<PathBuf>,
Comment on lines +190 to +191
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the only non-obvious change.
Without this, we are iterating in random order and there might be a case (which is why generate_name) exists that we need to reasign the testcase name.

This changes this to a deterministic sorting.

}

impl Tree {
Expand Down Expand Up @@ -270,6 +337,12 @@ fn generate_from_tree(
});
}

if let Some(reason) = parsed.ignore_patterns.should_ignore(&file_name.to_string()) {
stream.extend(quote! {
#[ignore = #reason]
});
}

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.
Expand Down
Loading