diff --git a/Cargo.lock b/Cargo.lock index 77a7c8e..78d91aa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -41,15 +41,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - [[package]] name = "syn" version = "2.0.37" @@ -70,14 +61,13 @@ dependencies = [ [[package]] name = "test_each_file" -version = "0.0.1" +version = "0.1.0" dependencies = [ "itertools", "pathdiff", "proc-macro2", "quote", "syn", - "walkdir", ] [[package]] @@ -85,44 +75,3 @@ name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" - -[[package]] -name = "walkdir" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" -dependencies = [ - "winapi", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/test-each-file/Cargo.toml b/test-each-file/Cargo.toml index 133343d..0854bd3 100644 --- a/test-each-file/Cargo.toml +++ b/test-each-file/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "test_each_file" -version = "0.0.2" +version = "0.1.0" authors = ["Jonathan Brouwer ", "Julia Dijkstra"] description = "Generates a test for each file in a specified directory." keywords = ["test", "proc-macro"] @@ -15,6 +15,5 @@ proc-macro = true quote = "1.0.33" proc-macro2 = "1.0.67" syn = {version="2.0.33", features=["full"]} -walkdir = "2.4.0" pathdiff = "0.2.1" itertools = "0.11.0" \ No newline at end of file diff --git a/test-each-file/README.md b/test-each-file/README.md index 01199ee..5de9260 100644 --- a/test-each-file/README.md +++ b/test-each-file/README.md @@ -35,19 +35,24 @@ fn b() { test(include_str!("../resources/b.txt")) } -#[test] -fn extra_c() { - test(include_str!("../resources/extra/c.txt")) +mod extra { + use super::*; + + #[test] + fn c() { + test(include_str!("../resources/extra/c.txt")) + } } ``` -## Name prefix +## Generate submodule -The names of the generated tests can be prefixed with a specified name, by using the `as` keyword. For example: +The tests can automatically be inserted into a module, by using the `as` keyword. For example: ```rust test_each_file! { in "./resources" as example => test } ``` -The names of the tests will instead be `example_a`, `example_b`, and `example_extra_c`. + +This will wrap the tests above in an additional `mod example { ... }`. This feature is useful when `test_each_file!` is used multiple times in a single file, to prevent that the generated tests have the same name. ## File grouping @@ -78,6 +83,8 @@ Both the `.in` and `.out` files must exist and be located in the same directory, - main.rs ``` +Note that `.in` and `.out` are just examples here - any number of unique extensions can be given of arbitrary types. + ## More examples The expression that is called on each file can also be a closure, for example: diff --git a/test-each-file/src/lib.rs b/test-each-file/src/lib.rs index c1d92da..b39db52 100644 --- a/test-each-file/src/lib.rs +++ b/test-each-file/src/lib.rs @@ -1,14 +1,13 @@ -use std::collections::HashSet; +use pathdiff::diff_paths; use proc_macro2::{Ident, TokenStream}; +use std::collections::{HashMap, HashSet}; use std::fs::canonicalize; -use itertools::Itertools; -use pathdiff::diff_paths; +use std::path::{Path, PathBuf}; -use syn::{parse_macro_input, Expr, Token, bracketed, LitStr}; use quote::{format_ident, quote}; use syn::parse::{Parse, ParseStream}; use syn::punctuated::Punctuated; -use walkdir::WalkDir; +use syn::{bracketed, parse_macro_input, Expr, LitStr, Token}; struct ForEachFile { path: String, @@ -25,8 +24,14 @@ impl Parse for ForEachFile { let content; bracketed!(content in input); - let extensions = Punctuated::::parse_terminated(&content)?.into_iter().map(|s| s.value()).collect::>(); - assert!(!extensions.is_empty(), "Expected at least one extension to be given."); + let extensions = Punctuated::::parse_terminated(&content)? + .into_iter() + .map(|s| s.value()) + .collect::>(); + assert!( + !extensions.is_empty(), + "Expected at least one extension to be given." + ); extensions } else { @@ -50,38 +55,48 @@ impl Parse for ForEachFile { path, prefix, function, - extensions + extensions, }) } } -#[proc_macro] -pub fn test_each_file(input: proc_macro::TokenStream) -> proc_macro::TokenStream { - let parsed = parse_macro_input!(input as ForEachFile); +#[derive(Default)] +struct Tree { + children: HashMap, + here: HashSet, +} - let mut tokens = TokenStream::new(); - let mut files = HashSet::new(); - for entry in WalkDir::new(&parsed.path).into_iter().filter_map(|e| e.ok()) { - let path = entry.path(); - if path.is_file() { - let mut file = path.to_path_buf(); - if !parsed.extensions.is_empty() { - file.set_extension(""); +impl Tree { + fn new(base: &Path, ignore_extensions: bool) -> Self { + assert!(base.is_dir()); + let mut tree = Self::default(); + for entry in base.read_dir().unwrap() { + let mut entry = entry.unwrap().path(); + if entry.is_file() { + if ignore_extensions { + entry.set_extension(""); + } + tree.here.insert(entry); + } else if entry.is_dir() { + tree.children.insert( + entry.as_path().to_path_buf(), + Self::new(entry.as_path(), ignore_extensions), + ); + } else { + panic!("Unsupported path.") } - files.insert(file); } + tree } +} - for file in files { - let mut diff = diff_paths(&file, &parsed.path).unwrap(); +fn generate_from_tree(tree: &Tree, parsed: &ForEachFile, stream: &mut TokenStream) { + for file in &tree.here { + let mut diff = diff_paths(file, &parsed.path).unwrap(); diff.set_extension(""); - let file_name = diff.components().map(|c| c.as_os_str().to_str().expect("Expected file names to be UTF-8.")).format("_"); + let file_name = diff.file_name().unwrap().to_str().unwrap(); - let file_name = if let Some(prefix) = &parsed.prefix { - format_ident!("{prefix}_{file_name}") - } else { - format_ident!("{file_name}") - }; + let file_name = format_ident!("{file_name}"); let function = &parsed.function; @@ -104,7 +119,7 @@ pub fn test_each_file(input: proc_macro::TokenStream) -> proc_macro::TokenStream quote!([#content]) }; - tokens.extend(quote! { + stream.extend(quote! { #[test] fn #file_name() { (#function)(#content) @@ -112,5 +127,36 @@ pub fn test_each_file(input: proc_macro::TokenStream) -> proc_macro::TokenStream }); } + for (name, directory) in &tree.children { + let mut sub_stream = TokenStream::new(); + generate_from_tree(directory, parsed, &mut sub_stream); + let name = format_ident!("{}", name.file_name().unwrap().to_str().unwrap()); + stream.extend(quote! { + mod #name { + use super::*; + #sub_stream + } + }) + } +} + +#[proc_macro] +pub fn test_each_file(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let parsed = parse_macro_input!(input as ForEachFile); + + let mut tokens = TokenStream::new(); + let files = Tree::new(parsed.path.as_ref(), !parsed.extensions.is_empty()); + generate_from_tree(&files, &parsed, &mut tokens); + + if let Some(prefix) = parsed.prefix { + tokens = quote! { + #[cfg(test)] + mod #prefix { + use super::*; + #tokens + } + } + } + proc_macro::TokenStream::from(tokens) }