mirror of
https://github.com/loot/libloot.git
synced 2026-07-27 14:16:01 -07:00
Rename and tidy up the parameterized-test crate
It needed to be renamed to allow it to be published on crates.io, as there's already a parameterized_test crate there.
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "array-parameterized-test"
|
||||
description = "Parameterized testing using an input array."
|
||||
categories = ["development-tools::testing"]
|
||||
version.workspace = true
|
||||
repository.workspace = true
|
||||
edition.workspace = true
|
||||
license = "MIT"
|
||||
|
||||
[dependencies]
|
||||
proc-macro2 = "1.0.95"
|
||||
quote = "1.0.40"
|
||||
syn = { version = "2.0.104", features = ["full"] }
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,33 @@
|
||||
# array-parameterized-test
|
||||
|
||||
A few macros to support parameterized test cases where the inputs are defined using an const array. This can be used to concisely define several different parameterized tests that share the same set of inputs.
|
||||
|
||||
Use it like this:
|
||||
|
||||
```rust
|
||||
use array_parameterized_test::{test_parameter, parameterized_test};
|
||||
|
||||
#[test_parameter]
|
||||
const INT_INPUTS: [u32; 2] = [1, 2];
|
||||
|
||||
#[parameterized_test(INT_INPUTS)]
|
||||
fn test_with_ints(value: u32) {
|
||||
assert!(value > 0 && value < 3);
|
||||
}
|
||||
```
|
||||
|
||||
which produces tests that look like this:
|
||||
|
||||
```
|
||||
test test_with_ints::_1 ... ok
|
||||
test test_with_ints::_2 ... ok
|
||||
```
|
||||
|
||||
See the tests for more examples.
|
||||
|
||||
The macros that implement this functionality may not work with arbitrary array element types. They've been tested with:
|
||||
|
||||
- u32
|
||||
- &str
|
||||
- (u32, bool)
|
||||
- Unit-only enums
|
||||
@@ -0,0 +1,144 @@
|
||||
use proc_macro2::{Group, TokenStream};
|
||||
use quote::{ToTokens, format_ident, quote};
|
||||
use syn::{Expr, Ident, ItemConst, ItemFn, Token, parse, parse_macro_input};
|
||||
|
||||
/// Use as an attribute to annotate a parameterized test function that accepts a single parameter. The attribute must be given the name of a const array that is annotated with the `test_parameter` attribute. That const array will be used to supply parameter values to the test function.
|
||||
#[proc_macro_attribute]
|
||||
pub fn parameterized_test(
|
||||
input: proc_macro::TokenStream,
|
||||
annotated_item: proc_macro::TokenStream,
|
||||
) -> proc_macro::TokenStream {
|
||||
let macro_name = parse_macro_input!(input as Ident);
|
||||
let test = parse_macro_input!(annotated_item as ItemFn);
|
||||
|
||||
let inner_func_name = test.sig.ident.clone();
|
||||
|
||||
quote! {
|
||||
mod #inner_func_name {
|
||||
use super::*;
|
||||
|
||||
#test
|
||||
|
||||
#macro_name!{#inner_func_name}
|
||||
}
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Use as an attribute to annotate a const array that will be used to supply the input values for a parameterized test.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the annotated item is not a const array expression. Will also panic if the array contains a path expression that is somehow empty.
|
||||
#[expect(clippy::expect_used, clippy::panic)]
|
||||
#[proc_macro_attribute]
|
||||
pub fn test_parameter(
|
||||
_input: proc_macro::TokenStream,
|
||||
annotated_item: proc_macro::TokenStream,
|
||||
) -> proc_macro::TokenStream {
|
||||
let cloned_item_tokens = annotated_item.clone();
|
||||
let item = parse_macro_input!(cloned_item_tokens as ItemConst);
|
||||
|
||||
let Expr::Array(array) = item.expr.as_ref() else {
|
||||
panic!("Expected expression to be an array");
|
||||
};
|
||||
|
||||
let values: Vec<_> = array
|
||||
.elems
|
||||
.iter()
|
||||
.map(|n| {
|
||||
if let Expr::Path(path) = n {
|
||||
path.path
|
||||
.segments
|
||||
.last()
|
||||
.expect("path expressions in the const array to have at least one segment")
|
||||
.ident
|
||||
.to_token_stream()
|
||||
} else {
|
||||
n.to_token_stream()
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let annotated_item = TokenStream::from(annotated_item);
|
||||
|
||||
let const_item_name = item.ident;
|
||||
let macro_name = format_ident!("{}_macro", &const_item_name);
|
||||
|
||||
let macro_output = quote! {
|
||||
macro_rules! #macro_name {
|
||||
( $inner_test_name:ident ) => {
|
||||
array_parameterized_test::generate_tests!{
|
||||
$inner_test_name,
|
||||
#const_item_name,
|
||||
[#(#values),*]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[allow(unused_imports)]
|
||||
pub(crate) use #macro_name as #const_item_name;
|
||||
|
||||
#annotated_item
|
||||
};
|
||||
|
||||
macro_output.into()
|
||||
}
|
||||
|
||||
struct GenerateTestsInput {
|
||||
inner_test_name: Ident,
|
||||
const_item_name: Ident,
|
||||
const_item_values: Group,
|
||||
}
|
||||
|
||||
impl syn::parse::Parse for GenerateTestsInput {
|
||||
fn parse(input: parse::ParseStream) -> syn::Result<Self> {
|
||||
let inner_test_name = input.parse()?;
|
||||
let _: Token![,] = input.parse()?;
|
||||
let const_item_name = input.parse()?;
|
||||
let _: Token![,] = input.parse()?;
|
||||
let const_item_values = input.parse()?;
|
||||
|
||||
Ok(GenerateTestsInput {
|
||||
inner_test_name,
|
||||
const_item_name,
|
||||
const_item_values,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A macro used to generate multiple test functions given a parameterized test function name, a const array identifier and the array's values.
|
||||
#[proc_macro]
|
||||
pub fn generate_tests(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||
let GenerateTestsInput {
|
||||
inner_test_name,
|
||||
const_item_name,
|
||||
const_item_values,
|
||||
} = parse_macro_input!(input as GenerateTestsInput);
|
||||
|
||||
let tokens: proc_macro2::TokenStream = const_item_values
|
||||
.stream()
|
||||
.into_iter()
|
||||
.step_by(2)
|
||||
.enumerate()
|
||||
.flat_map(|(i, value)| {
|
||||
let suffix = value
|
||||
.to_string()
|
||||
.escape_default()
|
||||
.collect::<String>()
|
||||
.replace(|c: char| !c.is_ascii_alphanumeric(), "_");
|
||||
|
||||
let test_name = format_ident!("_{suffix}");
|
||||
|
||||
quote! {
|
||||
#[test]
|
||||
#[allow(non_snake_case)]
|
||||
fn #test_name() {
|
||||
#inner_test_name(#const_item_name[#i]);
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
tokens.into()
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
use array_parameterized_test::{parameterized_test, test_parameter};
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq)]
|
||||
enum TestValue {
|
||||
A,
|
||||
B,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test_parameter]
|
||||
const INT_INPUTS: [u32; 2] = [1, 2];
|
||||
|
||||
#[test_parameter]
|
||||
const ENUM_INPUTS: [TestValue; 2] = [TestValue::A, TestValue::B];
|
||||
|
||||
#[test_parameter]
|
||||
const TUPLE_INPUTS: [(u32, bool); 5] =
|
||||
[(1, false), (2, true), (3, false), (4, true), (5, false)];
|
||||
|
||||
#[expect(clippy::non_ascii_literal)]
|
||||
#[test_parameter]
|
||||
const NON_ASCII_INPUTS: [&str; 2] = ["nonÁscii", "藏"];
|
||||
|
||||
#[parameterized_test(INT_INPUTS)]
|
||||
fn test_with_ints(value: u32) {
|
||||
assert!(value > 0 && value < 3);
|
||||
}
|
||||
|
||||
#[parameterized_test(ENUM_INPUTS)]
|
||||
fn test_with_enums(value: TestValue) {
|
||||
assert!(value == TestValue::A || value == TestValue::B);
|
||||
}
|
||||
|
||||
#[parameterized_test(NON_ASCII_INPUTS)]
|
||||
fn test_with_non_ascii_strings(value: &str) {
|
||||
assert!(!value.is_ascii());
|
||||
}
|
||||
|
||||
#[parameterized_test(TUPLE_INPUTS)]
|
||||
fn test_with_tuples(value: (u32, bool)) {
|
||||
assert_eq!(value.1, value.0.is_multiple_of(2));
|
||||
}
|
||||
|
||||
#[parameterized_test(TUPLE_INPUTS)]
|
||||
fn test_with_tuples_destructured((value, expected): (u32, bool)) {
|
||||
assert_eq!(expected, value.is_multiple_of(2));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
use std::path::Path;
|
||||
|
||||
use array_parameterized_test::{parameterized_test, test_parameter};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||
#[non_exhaustive]
|
||||
pub enum GameType {
|
||||
Oblivion,
|
||||
Skyrim,
|
||||
Fallout3,
|
||||
FalloutNV,
|
||||
Fallout4,
|
||||
SkyrimSE,
|
||||
Fallout4VR,
|
||||
SkyrimVR,
|
||||
Morrowind,
|
||||
Starfield,
|
||||
OpenMW,
|
||||
OblivionRemastered,
|
||||
}
|
||||
|
||||
fn has_ascii_extension(path: &Path, extension: &str) -> bool {
|
||||
path.extension()
|
||||
.is_some_and(|e| e.eq_ignore_ascii_case(extension))
|
||||
}
|
||||
|
||||
fn has_plugin_file_extension(game_type: GameType, plugin_path: &Path) -> bool {
|
||||
let extension = if game_type != GameType::OpenMW && has_ascii_extension(plugin_path, "ghost") {
|
||||
plugin_path
|
||||
.file_stem()
|
||||
.and_then(|s| Path::new(s).extension())
|
||||
} else {
|
||||
plugin_path.extension()
|
||||
};
|
||||
|
||||
if let Some(extension) = extension {
|
||||
if extension.eq_ignore_ascii_case("esp")
|
||||
|| extension.eq_ignore_ascii_case("esm")
|
||||
|| (game_type == GameType::OpenMW
|
||||
&& (extension.eq_ignore_ascii_case("omwaddon")
|
||||
|| extension.eq_ignore_ascii_case("omwgame")
|
||||
|| extension.eq_ignore_ascii_case("omwscripts")))
|
||||
{
|
||||
true
|
||||
} else {
|
||||
matches!(
|
||||
game_type,
|
||||
GameType::Fallout4
|
||||
| GameType::Fallout4VR
|
||||
| GameType::SkyrimSE
|
||||
| GameType::SkyrimVR
|
||||
| GameType::Starfield
|
||||
) && extension.eq_ignore_ascii_case("esl")
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test_parameter]
|
||||
const ALL_GAME_TYPES: [GameType; 12] = [
|
||||
GameType::Oblivion,
|
||||
GameType::Skyrim,
|
||||
GameType::Fallout3,
|
||||
GameType::FalloutNV,
|
||||
GameType::Fallout4,
|
||||
GameType::SkyrimSE,
|
||||
GameType::Fallout4VR,
|
||||
GameType::SkyrimVR,
|
||||
GameType::Morrowind,
|
||||
GameType::Starfield,
|
||||
GameType::OpenMW,
|
||||
GameType::OblivionRemastered,
|
||||
];
|
||||
|
||||
#[parameterized_test(ALL_GAME_TYPES)]
|
||||
fn should_be_true_if_file_ends_in_dot_esp_or_dot_esm(game_type: GameType) {
|
||||
assert!(has_plugin_file_extension(game_type, Path::new("file.esp")));
|
||||
assert!(has_plugin_file_extension(game_type, Path::new("file.esm")));
|
||||
assert!(!has_plugin_file_extension(game_type, Path::new("file.bsa")));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user