Skip to content

Commit

Permalink
Create modules instead of underscore-seperated names
Browse files Browse the repository at this point in the history
  • Loading branch information
JonathanBrouwer committed Sep 30, 2023
1 parent 49cf6a0 commit 8a41886
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 89 deletions.
53 changes: 1 addition & 52 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions test-each-file/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "test_each_file"
version = "0.0.2"
version = "0.1.0"
authors = ["Jonathan Brouwer <jonathantbrouwer@gmail.com>", "Julia Dijkstra"]
description = "Generates a test for each file in a specified directory."
keywords = ["test", "proc-macro"]
Expand All @@ -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"
19 changes: 13 additions & 6 deletions test-each-file/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
104 changes: 75 additions & 29 deletions test-each-file/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -25,8 +24,14 @@ impl Parse for ForEachFile {
let content;
bracketed!(content in input);

let extensions = Punctuated::<LitStr, Token![,]>::parse_terminated(&content)?.into_iter().map(|s| s.value()).collect::<Vec<_>>();
assert!(!extensions.is_empty(), "Expected at least one extension to be given.");
let extensions = Punctuated::<LitStr, Token![,]>::parse_terminated(&content)?
.into_iter()
.map(|s| s.value())
.collect::<Vec<_>>();
assert!(
!extensions.is_empty(),
"Expected at least one extension to be given."
);

extensions
} else {
Expand All @@ -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<PathBuf, Tree>,
here: HashSet<PathBuf>,
}

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;

Expand All @@ -104,13 +119,44 @@ 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)
}
});
}

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)
}

0 comments on commit 8a41886

Please sign in to comment.