Skip to content

Commit

Permalink
docs and name formatting
Browse files Browse the repository at this point in the history
  • Loading branch information
remkop22 committed Jun 1, 2023
1 parent 725d96c commit 97fe8e8
Show file tree
Hide file tree
Showing 4 changed files with 171 additions and 55 deletions.
36 changes: 26 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,35 +9,35 @@ This crate contains three attributes that all generate tests based on a file glo

### Text files

Receive file contents as `&'static str` with `test_each::file`. This ignores any matched directories.
Receive file contents as [`&'static str`](std::str) with [`test_each::file`](crate::file). This ignores any matched directories.

```rust
#[test_each::file("data/*.txt")]
#[test_each::file(glob = "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:
If `data` contains the files `foo.txt` and `bar.txt`, the following code will be generated:

```rust
#[test]
fn test_file_foo_txt_0() {
fn test_file_foo() {
test_file(include_str("data/foo.txt"))
}

#[test]
fn test_file_bar_txt_1() {
fn test_file_bar() {
test_file(include_str("data/bar.txt"))
}
```

### Binary files

Receive file contents as `&'static [u8]` with `test_each::blob`. This ignores any matched directories.
Receive file contents as [`&'static [u8]`](std::slice) with [`test_each::blob`](crate::file). This ignores any matched directories.

```rust
#[test_each::blob("data/*.bin")]
#[test_each::blob(glob = "data/*.bin")]
fn test_bytes(content: &[u8]) {
// check contents
}
Expand All @@ -46,23 +46,39 @@ fn test_bytes(content: &[u8]) {
Declare a second parameter in order to additionally receive the path of file.

```rust
#[test_each::blob("data/*.bin")]
#[test_each::blob(glob = "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.
Receive file path as [`PathBuf`](std::path::PathBuf) with [`test_each::path`](crate::path). This includes any matched directories.

```rust
#[test_each::path("data/*")]
#[test_each::path(glob = "data/*")]
fn test_bytes(path: PathBuf) {
// check path
}
```

### Customizing the function name

By default the name of the generated test will consist of the escaped file name without extension. Use the `name` attribute to change how the function names are formatted.

Use `name(segments = <n>)` to add `n` amount of path segments (from right to left) to the name.

Use `name(index)` to add a unique index to the end of the test name. This will prevent name collisions.

Use `name(extension)` to include the file extension the end of the test name.

```rust
/// The generated function name will be `test_file_bar_baz_data_txt_0`
#[test_each::file(glob = "foo/bar/baz/data.txt", name(segments = 3, index, extension))]
fn test_file(_: &str) {}
```

## 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.
Expand Down
45 changes: 42 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,32 @@
///
/// # Usage
/// ```rust
/// #[test_each::file("data/*.txt")]
/// #[test_each::file(glob = "data/*.txt")]
/// fn test_file(content: &str) {
/// // test contents
/// }
/// ```
///
/// Add a second parameter of type `PathBuf` to receive the path of the file.
/// Add a second parameter of type [`PathBuf`](std::path::PathBuf) to receive the path of the file.
/// ```rust
/// #[test_each::file("data/*.txt")]
/// fn test_file(content: &str, path: PathBuf) {
/// // test contents
/// }
/// ```
///
/// ## Customizing the function name
///
/// Use `name(segments = <n>)` to use up to `n` path segments in the generated function name.
///
/// Use `name(extension)` to include the file extension in the generated function name.
///
/// Use `name(index)` to include a unique index in the generated function name.
///
/// ```rust
/// #[test_each::file("data/*.txt", name(segments = 2, extension, index))]
/// fn test_file(_: &str) { }
/// ```
pub use test_each_codegen::test_each_file as file;

#[doc(inline)]
Expand All @@ -38,13 +51,26 @@ pub use test_each_codegen::test_each_file as file;
/// }
/// ```
///
/// Add a second parameter of type `PathBuf` to receive the path of the file.
/// Add a second parameter of type [`PathBuf`](std::path::PathBuf) to receive the path of the file.
/// ```rust
/// #[test_each::blob("data/*.bin")]
/// fn test_bytes(content: &[u8], path: PathBuf) {
/// // test contents
/// }
/// ```
///
/// ## Customizing the function name
///
/// Use `name(segments = <n>)` to use up to `n` path segments in the generated function name.
///
/// Use `name(extension)` to include the file extension in the generated function name.
///
/// Use `name(index)` to include a unique index in the generated function name.
///
/// ```rust
/// #[test_each::blob("data/*.txt", name(segments = 2, extension, index))]
/// fn test_file(_: &[u8]) { }
/// ```
pub use test_each_codegen::test_each_blob as blob;

#[doc(inline)]
Expand All @@ -60,4 +86,17 @@ pub use test_each_codegen::test_each_blob as blob;
/// // test contents
/// }
/// ```
///
/// ## Customizing the function name
///
/// Use `name(segments = <n>)` to use up to `n` path segments in the generated function name.
///
/// Use `name(extension)` to include the file extension in the generated function name.
///
/// Use `name(index)` to include a unique index in the generated function name.
///
/// ```rust
/// #[test_each::path("data/*.txt", name(segments = 2, extension, index))]
/// fn test_file(_: PathBuf) { }
/// ```
pub use test_each_codegen::test_each_path as path;
137 changes: 99 additions & 38 deletions test-each-codegen/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,30 +1,89 @@
use proc_macro::TokenStream;
use quote::{format_ident, quote, ToTokens};
use syn::{
parse::Parser, parse_macro_input, punctuated::Punctuated, Error, ItemFn, LitStr, Result, Token,
meta::ParseNestedMeta, parse_macro_input, spanned::Spanned, Error, ItemFn, LitInt, LitStr,
Result,
};

struct Attrs {
glob: Option<String>,
segments: usize,
index: bool,
extension: bool,
}

impl Attrs {
pub fn new() -> Self {
Self {
glob: None,
segments: 1,
index: false,
extension: false,
}
}

fn parse(&mut self, meta: ParseNestedMeta) -> Result<()> {
if meta.path.is_ident("glob") {
let glob: LitStr = meta.value()?.parse()?;
self.glob = Some(glob.value());
} else if meta.path.is_ident("name") {
meta.parse_nested_meta(|nested| {
if nested.path.is_ident("segments") {
let path_segments: LitInt = nested.value()?.parse()?;
self.segments = path_segments.base10_parse()?;
} else if nested.path.is_ident("index") {
self.index = true
} else if nested.path.is_ident("extension") {
self.extension = true
} else {
return Err(nested.error(
"unsupported property, specify `segments = <num>`, `index` or `extension`",
));
}

Ok(())
})?;
} else {
return Err(meta.error("unsupported property, specify `glob` or `name`"));
}

Ok(())
}
}

#[proc_macro_attribute]
pub fn test_each_file(attrs: TokenStream, input: TokenStream) -> TokenStream {
pub fn test_each_file(args: TokenStream, input: TokenStream) -> TokenStream {
let mut attrs = Attrs::new();
let attr_parser = syn::meta::parser(|meta| attrs.parse(meta));
let input = parse_macro_input!(input as ItemFn);
parse_macro_input!(args with attr_parser);

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 {
pub fn test_each_blob(args: TokenStream, input: TokenStream) -> TokenStream {
let mut attrs = Attrs::new();
let attr_parser = syn::meta::parser(|meta| attrs.parse(meta));
let input = parse_macro_input!(input as ItemFn);
parse_macro_input!(args with attr_parser);

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 {
pub fn test_each_path(args: TokenStream, input: TokenStream) -> TokenStream {
let mut attrs = Attrs::new();
let attr_parser = syn::meta::parser(|meta| attrs.parse(meta));
let input = parse_macro_input!(input as ItemFn);
parse_macro_input!(args with attr_parser);

match test_each(attrs, input, Kind::Path) {
Ok(output) => output,
Err(err) => err.into_compile_error().into(),
Expand All @@ -37,53 +96,55 @@ enum Kind {
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()];
fn test_each(attrs: Attrs, input: ItemFn, kind: Kind) -> Result<TokenStream> {
let mut functions = vec![input.to_token_stream()];

let name = input.sig.ident;
let vis = input.vis;
let ret = input.sig.output;
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 pattern = attrs
.glob
.as_ref()
.ok_or_else(|| Error::new(input.span(), "missing `glob` attribute"))?;

let files = glob::glob(pattern).map_err(|err| {
Error::new(
lits[0].span(),
format!("invalid path glob pattern: {}", err),
)
})?;
let files = glob::glob(pattern)
.map_err(|err| Error::new(input.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))
})?;
let mut file = file
.map_err(|err| Error::new(input.span(), format!("could not read directory: {}", err)))?
.canonicalize()
.map_err(|err| Error::new(input.span(), format!("could not read file: {}", 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 path = file.to_string_lossy().to_string();

if !attrs.extension {
file.set_extension("");
}

let test_name = format_ident!("{}_{}{}", name, file_name, i);
let mut path_segments = file
.iter()
.rev()
.take(attrs.segments)
.map(|s| make_safe_ident(&s.to_string_lossy()))
.collect::<Vec<_>>();

let path = file
.canonicalize()
.map_err(|err| Error::new(lits[0].span(), format!("could not read file: {}", err)))?
.to_string_lossy()
.to_string();
path_segments.reverse();

let path_name = path_segments.join("_");

let test_name = if attrs.index {
format_ident!("{}_{}_{}", name, path_name, i)
} else {
format_ident!("{}_{}", name, path_name)
};

let into_path = quote!(::std::path::PathBuf::from(#path));

Expand Down Expand Up @@ -117,5 +178,5 @@ fn make_safe_ident(value: &str) -> String {
}
}

result
result.trim_matches('_').to_string()
}
8 changes: 4 additions & 4 deletions tests/integration.rs
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
use std::{io::BufRead, path::PathBuf};

#[test_each::file("tests/data/*.txt")]
#[test_each::file(glob = "tests/data/*.txt", name(index))]
fn test_file(content: &str) {
assert_eq!(Some("hello world"), content.lines().next())
}

#[test_each::file("tests/data/*.txt")]
#[test_each::file(glob = "tests/data/*.txt", name(segments = 2, extension))]
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")]
#[test_each::blob(glob = "tests/data/*.txt", name(segments = 3, extension, index))]
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")]
#[test_each::path(glob = "tests/data/*.txt")]
fn test_path(path: PathBuf) {
match path.file_name().and_then(|s| s.to_str()) {
Some("foo.txt" | "bar.txt") => {}
Expand Down

0 comments on commit 97fe8e8

Please sign in to comment.