-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Improved testing using a new glob macro (#95)
This PR sets up a new `glob` attribute macro, intended to be used in tests. This helps split each of the larger snapshot tests up into smaller functions, allowing for parallelism between the snapshotted files when running `cargo nextest run` or `cargo test`. There wasn't an existing crate that I could use that had `glob` at macro level, and to introduce a new proc macro, we need to set up a separate crate; this is set up as a crate within the same workspace. In the future, if I find this useful for other use cases too, I might split it off as a separate crate entirely, but for now, it can sit in this workspace. It is easiest to see the impact of this change by looking at the output of `cargo nextest run` (or ~equivalently, `cargo test`). Before this PR, the entirety of `verus-snapshot/**/*.rs` ran within a single test function, but after this, each file gets its own function.
- Loading branch information
Showing
6 changed files
with
190 additions
and
16 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
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
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,14 @@ | ||
[package] | ||
name = "glob-macro" | ||
version = "0.1.0" | ||
edition = "2021" | ||
|
||
[lib] | ||
proc-macro = true | ||
|
||
[dependencies] | ||
glob = "0.3.1" | ||
proc-macro2 = "1.0.86" | ||
quote = "1.0.37" | ||
syn = "2.0.77" | ||
syn-mid = { version = "0.6.0", features = ["clone-impls"] } |
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,15 @@ | ||
# Glob Macro | ||
|
||
A small and simple crate that lets one apply a [`glob`](https://docs.rs/glob/latest/glob/) in a macro position. | ||
|
||
## Usage | ||
|
||
The main intended use case for this is to write tests that run over all files in some directory. For example: | ||
|
||
```rs | ||
#[glob("./path/to/**/*.inp")] | ||
#[test] | ||
fn test(path: &Path) { | ||
assert!(path.exists()); | ||
} | ||
``` |
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,120 @@ | ||
use proc_macro::TokenStream; | ||
use proc_macro2::{Span, TokenStream as TokenStream2}; | ||
use quote::quote; | ||
use syn::Error; | ||
use syn_mid::ItemFn; | ||
|
||
#[proc_macro_attribute] | ||
pub fn glob(args: TokenStream, function: TokenStream) -> TokenStream { | ||
if args.is_empty() { | ||
return Error::new(Span::call_site(), "#[glob] attribute requires an argument") | ||
.to_compile_error() | ||
.into(); | ||
} | ||
|
||
let glob_path = match syn::parse(args) { | ||
Ok(p) => p, | ||
Err(err) => return err.to_compile_error().into(), | ||
}; | ||
|
||
let function: ItemFn = syn::parse_macro_input!(function); | ||
|
||
if !function | ||
.attrs | ||
.iter() | ||
.any(|attr| attr.path().is_ident("test")) | ||
{ | ||
return Error::new( | ||
Span::call_site(), | ||
"#[glob] attribute currently only supports running on #[test] functions", | ||
) | ||
.to_compile_error() | ||
.into(); | ||
} | ||
|
||
glob2(glob_path, function).into() | ||
} | ||
|
||
fn glob2(glob_path: syn::LitStr, function: ItemFn) -> TokenStream2 { | ||
let paths = match glob::glob(&glob_path.value()) { | ||
Ok(paths) => paths, | ||
Err(err) => { | ||
return Error::new( | ||
glob_path.span(), | ||
format!("#[glob] called with invalid value: {err:?}"), | ||
) | ||
.to_compile_error(); | ||
} | ||
}; | ||
|
||
let paths = match paths.collect::<Result<Vec<_>, _>>() { | ||
Ok(p) => p, | ||
Err(err) => { | ||
return Error::new(glob_path.span(), format!("#[glob] error: {err:?}")) | ||
.to_compile_error(); | ||
} | ||
}; | ||
let counter_width = (paths.len() - 1).to_string().len(); | ||
|
||
let common_ancestor = paths | ||
.iter() | ||
.fold(paths[0].clone(), |ancestor, path| { | ||
ancestor | ||
.components() | ||
.zip(path.components()) | ||
.take_while(|(a, b)| a == b) | ||
.map(|(a, _)| a) | ||
.collect() | ||
}) | ||
.components() | ||
.count(); | ||
|
||
let mut functions = vec![]; | ||
|
||
for (i, path) in paths.iter().enumerate() { | ||
let function_name = syn::Ident::new( | ||
&format!( | ||
"{}__{:0width$}__{}", | ||
function.sig.ident, | ||
i, | ||
path.components() | ||
.skip(common_ancestor) | ||
.map(|p| p.as_os_str().to_string_lossy()) | ||
.collect::<Vec<_>>() | ||
.join("/") | ||
.replace("/", "__") | ||
.replace(".", "_") | ||
.replace("-", "_") | ||
.replace(" ", "_"), | ||
width = counter_width | ||
), | ||
function.sig.ident.span(), | ||
); | ||
|
||
let path_buf = syn::LitStr::new(&path.to_string_lossy(), Span::call_site()); | ||
|
||
let mut inner_function = function.clone(); | ||
inner_function.sig.ident = syn::Ident::new("test", function.sig.ident.span()); | ||
inner_function.attrs = inner_function | ||
.attrs | ||
.into_iter() | ||
.filter(|attr| !attr.path().is_ident("test")) | ||
.collect(); | ||
|
||
let function = quote! { | ||
#[test] | ||
#[allow(non_snake_case)] | ||
fn #function_name() { | ||
#inner_function | ||
let path: &str = #path_buf; | ||
test(std::path::Path::new(&path)); | ||
} | ||
}; | ||
|
||
functions.push(function); | ||
} | ||
|
||
quote! { | ||
#(#functions)* | ||
} | ||
} |
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