-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 725d96c
Showing
10 changed files
with
362 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
/target | ||
/Cargo.lock |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
[package] | ||
name = "test-each" | ||
description = "Generate tests at compile-time based on file resources" | ||
documentation = "https://docs.rs/test-each" | ||
readme = "README.md" | ||
version.workspace = true | ||
authors.workspace = true | ||
edition.workspace = true | ||
repository.workspace = true | ||
homepage.workspace = true | ||
license-file.workspace = true | ||
keywords.workspace = true | ||
categories.workspace = true | ||
|
||
[workspace.package] | ||
version = "0.1.0" | ||
authors = ["Remo Pas <remo.pas22@gmail.com>"] | ||
edition = "2021" | ||
repository = "https://github.com/remkop22/test-each" | ||
homepage = "https://github.com/remkop22/test-each" | ||
license-file = "LICENSE" | ||
keywords = ["test", "proc-macro", "codegen"] | ||
categories = ["development-tools::testing", "development-tools::build-utils", "filesystem"] | ||
|
||
[lib] | ||
doctest = false | ||
|
||
[workspace] | ||
members = ["test-each-codegen"] | ||
|
||
[dependencies] | ||
test-each-codegen = { version = "0.1.0", path = "test-each-codegen" } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
MIT License | ||
|
||
Copyright (c) 2023 Remo | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy | ||
of this software and associated documentation files (the "Software"), to deal | ||
in the Software without restriction, including without limitation the rights | ||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
copies of the Software, and to permit persons to whom the Software is | ||
furnished to do so, subject to the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be included in all | ||
copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
SOFTWARE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
|
||
# test-each | ||
|
||
Generate tests at compile-time based on files and directories. | ||
|
||
## Usage | ||
|
||
This crate contains three attributes that all generate tests based on a file glob pattern. Each attribute generates tests with different argument types. The generated tests will be named after sanitized versions of the file names. | ||
|
||
### Text files | ||
|
||
Receive file contents as `&'static str` with `test_each::file`. This ignores any matched directories. | ||
|
||
```rust | ||
#[test_each::file("data/*.txt")] | ||
fn test_file(content: &str) { | ||
// check contents | ||
} | ||
``` | ||
|
||
If data contains the files `foo.txt` and `bar.txt`, the following code will be generated: | ||
|
||
```rust | ||
#[test] | ||
fn test_file_foo_txt_0() { | ||
test_file(include_str("data/foo.txt")) | ||
} | ||
|
||
#[test] | ||
fn test_file_bar_txt_1() { | ||
test_file(include_str("data/bar.txt")) | ||
} | ||
``` | ||
|
||
### Binary files | ||
|
||
Receive file contents as `&'static [u8]` with `test_each::blob`. This ignores any matched directories. | ||
|
||
```rust | ||
#[test_each::blob("data/*.bin")] | ||
fn test_bytes(content: &[u8]) { | ||
// check contents | ||
} | ||
``` | ||
|
||
Declare a second parameter in order to additionally receive the path of file. | ||
|
||
```rust | ||
#[test_each::blob("data/*.bin")] | ||
fn test_bytes(content: &[u8], path: PathBuf) { | ||
// check contents and path | ||
} | ||
``` | ||
|
||
### Paths to files and directories | ||
|
||
Receive file path as `PathBuf` with `test_each::path`. This includes any matched directories. | ||
|
||
```rust | ||
#[test_each::path("data/*")] | ||
fn test_bytes(path: PathBuf) { | ||
// check path | ||
} | ||
``` | ||
|
||
## Notes | ||
|
||
Any change to an already included file will correctly trigger a recompilation, but creating a new file that matches the glob might not cause a recompilation. | ||
To fix this issue add a build file that emits `cargo-rerun-if-changed={<glob directories>}`. | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
#![deny(missing_docs)] | ||
#![doc = include_str!("../README.md")] | ||
|
||
#[doc(inline)] | ||
/// Generate a series of tests that receive file contents as strings, | ||
/// based on the result of a glob pattern. | ||
/// | ||
/// This excludes any matched directories. | ||
/// | ||
/// # Usage | ||
/// ```rust | ||
/// #[test_each::file("data/*.txt")] | ||
/// fn test_file(content: &str) { | ||
/// // test contents | ||
/// } | ||
/// ``` | ||
/// | ||
/// Add a second parameter of type `PathBuf` to receive the path of the file. | ||
/// ```rust | ||
/// #[test_each::file("data/*.txt")] | ||
/// fn test_file(content: &str, path: PathBuf) { | ||
/// // test contents | ||
/// } | ||
/// ``` | ||
pub use test_each_codegen::test_each_file as file; | ||
|
||
#[doc(inline)] | ||
/// Generate a series of tests that receive file contents as byte slices, | ||
/// based on the result of a glob pattern. | ||
/// | ||
/// This excludes any matched directories. | ||
/// | ||
/// # Usage | ||
/// ```rust | ||
/// #[test_each::blob("data/*.bin")] | ||
/// fn test_bytes(content: &[u8]) { | ||
/// // test contents | ||
/// } | ||
/// ``` | ||
/// | ||
/// Add a second parameter of type `PathBuf` to receive the path of the file. | ||
/// ```rust | ||
/// #[test_each::blob("data/*.bin")] | ||
/// fn test_bytes(content: &[u8], path: PathBuf) { | ||
/// // test contents | ||
/// } | ||
/// ``` | ||
pub use test_each_codegen::test_each_blob as blob; | ||
|
||
#[doc(inline)] | ||
/// Generate a series of tests that receive file paths, | ||
/// based on the result of a glob pattern. | ||
/// | ||
/// This includes any matched directories. | ||
/// | ||
/// # Usage | ||
/// ```rust | ||
/// #[test_each::path("data/*")] | ||
/// fn test_paths(path: PathBuf) { | ||
/// // test contents | ||
/// } | ||
/// ``` | ||
pub use test_each_codegen::test_each_path as path; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
[package] | ||
name = "test-each-codegen" | ||
description = "Internal proc-macro crate for `test-each`" | ||
version.workspace = true | ||
authors.workspace = true | ||
edition.workspace = true | ||
repository.workspace = true | ||
homepage.workspace = true | ||
license-file.workspace = true | ||
keywords.workspace = true | ||
categories.workspace = true | ||
|
||
[lib] | ||
proc-macro = true | ||
|
||
[dependencies] | ||
quote = "1.0.28" | ||
syn = { version = "2.0.17", features = ["full"] } | ||
glob = "0.3.1" | ||
proc-macro2 = "1.0.59" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
use proc_macro::TokenStream; | ||
use quote::{format_ident, quote, ToTokens}; | ||
use syn::{ | ||
parse::Parser, parse_macro_input, punctuated::Punctuated, Error, ItemFn, LitStr, Result, Token, | ||
}; | ||
|
||
#[proc_macro_attribute] | ||
pub fn test_each_file(attrs: TokenStream, input: TokenStream) -> TokenStream { | ||
let input = parse_macro_input!(input as ItemFn); | ||
match test_each(attrs, input, Kind::File) { | ||
Ok(output) => output, | ||
Err(err) => err.into_compile_error().into(), | ||
} | ||
} | ||
|
||
#[proc_macro_attribute] | ||
pub fn test_each_blob(attrs: TokenStream, input: TokenStream) -> TokenStream { | ||
let input = parse_macro_input!(input as ItemFn); | ||
match test_each(attrs, input, Kind::Blob) { | ||
Ok(output) => output, | ||
Err(err) => err.into_compile_error().into(), | ||
} | ||
} | ||
|
||
#[proc_macro_attribute] | ||
pub fn test_each_path(attrs: TokenStream, input: TokenStream) -> TokenStream { | ||
let input = parse_macro_input!(input as ItemFn); | ||
match test_each(attrs, input, Kind::Path) { | ||
Ok(output) => output, | ||
Err(err) => err.into_compile_error().into(), | ||
} | ||
} | ||
|
||
enum Kind { | ||
File, | ||
Blob, | ||
Path, | ||
} | ||
|
||
fn test_each(attrs: TokenStream, input: ItemFn, kind: Kind) -> Result<TokenStream> { | ||
let lits = Punctuated::<LitStr, Token![,]>::parse_terminated.parse(attrs)?; | ||
let mut functions = vec![input.clone().to_token_stream()]; | ||
|
||
let name = input.sig.ident; | ||
let vis = input.vis; | ||
let ret = input.sig.output; | ||
let n_args = input.sig.inputs.len(); | ||
|
||
if lits.len() != 1 { | ||
return Err(Error::new( | ||
name.span(), | ||
"expected a single path glob literal", | ||
)); | ||
} | ||
|
||
let pattern = &lits[0].value(); | ||
|
||
let files = glob::glob(pattern).map_err(|err| { | ||
Error::new( | ||
lits[0].span(), | ||
format!("invalid path glob pattern: {}", err), | ||
) | ||
})?; | ||
|
||
for (i, file) in files.enumerate() { | ||
let file = file.map_err(|err| { | ||
Error::new(lits[0].span(), format!("could not read directory: {}", err)) | ||
})?; | ||
|
||
match kind { | ||
Kind::File | Kind::Blob if file.is_dir() => continue, | ||
_ => {} | ||
}; | ||
|
||
let file_name = file | ||
.file_name() | ||
.map(|name| format!("{}_", make_safe_ident(&name.to_string_lossy()))) | ||
.unwrap_or_default(); | ||
|
||
let test_name = format_ident!("{}_{}{}", name, file_name, i); | ||
|
||
let path = file | ||
.canonicalize() | ||
.map_err(|err| Error::new(lits[0].span(), format!("could not read file: {}", err)))? | ||
.to_string_lossy() | ||
.to_string(); | ||
|
||
let into_path = quote!(::std::path::PathBuf::from(#path)); | ||
|
||
let call = match kind { | ||
Kind::File if n_args < 2 => quote!(#name(include_str!(#path))), | ||
Kind::File => quote!(#name(include_str!(#path), #into_path)), | ||
Kind::Blob if n_args < 2 => quote!(#name(include_bytes!(#path))), | ||
Kind::Blob => quote!(#name(include_bytes!(#path), #into_path)), | ||
Kind::Path => quote!(#name(#into_path)), | ||
}; | ||
|
||
functions.push(quote! { | ||
#[test] | ||
#vis fn #test_name() #ret { | ||
#call | ||
} | ||
}); | ||
} | ||
|
||
Ok(quote!( #(#functions)* ).into()) | ||
} | ||
|
||
fn make_safe_ident(value: &str) -> String { | ||
let mut result = String::with_capacity(value.len()); | ||
|
||
for c in value.chars() { | ||
if c.is_alphanumeric() { | ||
result.push(c); | ||
} else { | ||
result.push('_'); | ||
} | ||
} | ||
|
||
result | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
hello world | ||
bar.txt |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
hello world | ||
foo.txt |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
use std::{io::BufRead, path::PathBuf}; | ||
|
||
#[test_each::file("tests/data/*.txt")] | ||
fn test_file(content: &str) { | ||
assert_eq!(Some("hello world"), content.lines().next()) | ||
} | ||
|
||
#[test_each::file("tests/data/*.txt")] | ||
fn test_file_with_path(content: &str, path: PathBuf) { | ||
let mut lines = content.lines(); | ||
assert_eq!(Some("hello world"), lines.next()); | ||
assert_eq!(path.file_name().and_then(|s| s.to_str()), lines.next()); | ||
} | ||
|
||
#[test_each::blob("tests/data/*.txt")] | ||
fn test_blob(content: &[u8]) { | ||
assert_eq!( | ||
Some(b"hello world".to_vec()), | ||
BufRead::split(content, b'\n').next().transpose().unwrap() | ||
) | ||
} | ||
|
||
#[test_each::path("tests/data/*.txt")] | ||
fn test_path(path: PathBuf) { | ||
match path.file_name().and_then(|s| s.to_str()) { | ||
Some("foo.txt" | "bar.txt") => {} | ||
other => panic!("expected either `foo.txt` or `bar.txt` found: {:?}", other), | ||
} | ||
} |