diff --git a/README.md b/README.md index c6271e2..3df1507 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/examples/readme/main.rs b/examples/readme/main.rs index a41b8e0..3c2e8d0 100644 --- a/examples/readme/main.rs +++ b/examples/readme/main.rs @@ -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" + } + } + } } diff --git a/src/lib.rs b/src/lib.rs index a6400d5..f4864ab 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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}; @@ -18,6 +18,7 @@ struct TestEachArgs { extensions: Vec, attributes: Vec, async_fn: Option, + ignore_patterns: IgnorePatterns, } macro_rules! abort { @@ -32,6 +33,68 @@ macro_rules! abort_token_stream { }; } +#[derive(Default)] +struct IgnorePatterns { + patterns: HashMap, +} + +impl Parse for IgnorePatterns { + fn parse(input: ParseStream) -> syn::Result { + if !input + .fork() + .parse::() + .ok() + .is_some_and(|id| id == "ignore") + { + return Ok(IgnorePatterns::default()); + } + let _: Ident = input.parse().unwrap(); + + // Optional ":" separator + let _ = input.parse::(); + + 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::]>() { + 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::(); + } + + 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 { + 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 { // Optionally parse attributes if `#` is used. Aborts if none are given. @@ -107,6 +170,9 @@ 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, @@ -114,14 +180,15 @@ impl Parse for TestEachArgs { extensions, attributes, async_fn, + ignore_patterns, }) } } #[derive(Default)] struct Tree { - children: HashMap, - here: HashSet, + children: BTreeMap, + here: BTreeSet, } impl Tree { @@ -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.