Skip to content

Commit bf4da40

Browse files
committed
feat: support outer doc comments for naming matrix tests
Adds support for using outer doc comments (`///`) instead of auto-generated tests names for parameterized matrix tests. e.g. ```rust [values( /// All present InputDirConfig { build_info: true, data_files: true, mtree: true, package_info: true, scriptlet: true, }, /// No data files InputDirConfig { build_info: true, data_files: false, mtree: true, package_info: true, scriptlet: true, }, } ``` The generated tests will look like this: ``` values_1_All_present values_2_No_data_files ``` This implementation also changes the internal `sanitize_ident` function to allow sanitizing whitespaces as `_`. Fixes <#320> Signed-off-by: Orhun Parmaksız <orhunparmaksiz@gmail.com>
1 parent f68a743 commit bf4da40

File tree

3 files changed

+90
-25
lines changed

3 files changed

+90
-25
lines changed

rstest_macros/src/parse/vlist.rs

Lines changed: 59 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,10 @@ use proc_macro2::TokenStream;
22
use quote::ToTokens;
33
use syn::{
44
parse::{Parse, ParseStream, Result},
5-
Expr, Ident, Pat, Token,
5+
Attribute, Expr, ExprLit, Ident, Lit, Meta, MetaNameValue, Pat, Token,
66
};
77

8-
use crate::refident::IntoPat;
9-
10-
use super::expressions::Expressions;
8+
use crate::{refident::IntoPat, utils::sanitize_ident};
119

1210
#[derive(Debug, PartialEq, Clone)]
1311
pub(crate) struct Value {
@@ -39,25 +37,51 @@ pub(crate) struct ValueList {
3937
pub(crate) values: Vec<Value>,
4038
}
4139

40+
/// Extracts the documentation comment from the attributes.
41+
fn extract_doc_comment(attrs: &[Attribute]) -> Option<String> {
42+
attrs.iter().find_map(|attr| match &attr.meta {
43+
Meta::NameValue(MetaNameValue {
44+
path,
45+
value: Expr::Lit(ExprLit {
46+
lit: Lit::Str(doc), ..
47+
}),
48+
..
49+
}) if path.is_ident("doc") => Some(sanitize_ident(&doc.value().trim(), false)),
50+
_ => None,
51+
})
52+
}
53+
4254
impl Parse for ValueList {
4355
fn parse(input: ParseStream) -> Result<Self> {
4456
let ident: Ident = input.parse()?;
4557
let _to: Token![=>] = input.parse()?;
4658
let content;
4759
let paren = syn::bracketed!(content in input);
48-
let values: Expressions = content.parse()?;
4960

50-
let ret = Self {
51-
arg: ident.into_pat(),
52-
values: values.take().into_iter().map(|e| e.into()).collect(),
53-
};
54-
if ret.values.is_empty() {
61+
let mut values = Vec::new();
62+
while !content.is_empty() {
63+
// Set the description from doc comments if possible
64+
let attrs: Vec<Attribute> = content.call(Attribute::parse_outer)?;
65+
let description = extract_doc_comment(&attrs);
66+
67+
// Parse the expression inside the brackets
68+
let expr: Expr = content.parse()?;
69+
values.push(Value::new(expr, description));
70+
71+
// Consume the trailing comma
72+
let _ = content.parse::<Token![,]>();
73+
}
74+
75+
if values.is_empty() {
5576
Err(syn::Error::new(
5677
paren.span.join(),
5778
"Values list should not be empty",
5879
))
5980
} else {
60-
Ok(ret)
81+
Ok(Self {
82+
arg: ident.into_pat(),
83+
values,
84+
})
6185
}
6286
}
6387
}
@@ -119,5 +143,29 @@ mod should {
119143
fn forget_brackets() {
120144
parse_values_list(r#"other => 42"#);
121145
}
146+
147+
#[test]
148+
fn doc_comment_overrides_description() {
149+
let vl = parse_values_list(
150+
r#"
151+
number => [
152+
/// forty two
153+
42,
154+
/// one hundred
155+
100,
156+
/// Sanitized 999 Value 😁
157+
999
158+
2,
159+
]
160+
"#,
161+
);
162+
163+
assert_eq!("number", &vl.arg.display_code());
164+
assert_eq!("forty_two", vl.values[0].description());
165+
assert_eq!("one_hundred", vl.values[1].description());
166+
assert_eq!("Sanitized_999_Value_", vl.values[2].description());
167+
// fallback to the expression name
168+
assert_eq!("2", vl.values[3].description());
169+
}
122170
}
123171
}

rstest_macros/src/render/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ impl ValueList {
109109
) -> impl Iterator<Item = (String, ArgumentDataResolver<'a>)> + 'a {
110110
let max_len = self.values.len();
111111
self.values.iter().enumerate().map(move |(index, value)| {
112-
let description = sanitize_ident(&value.description());
112+
let description = sanitize_ident(&value.description(), true);
113113
let arg = info.arguments.inner_pat(&self.arg);
114114

115115
let arg_name = arg

rstest_macros/src/utils.rs

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -281,13 +281,19 @@ pub(crate) fn fn_arg_mutability(arg: &FnArg) -> Option<syn::token::Mut> {
281281
}
282282
}
283283

284-
pub(crate) fn sanitize_ident(name: &str) -> String {
284+
pub(crate) fn sanitize_ident(name: &str, filter_whitespace: bool) -> String {
285285
name.chars()
286-
.filter(|c| !c.is_whitespace())
286+
.filter(|c| {
287+
if filter_whitespace {
288+
!c.is_whitespace()
289+
} else {
290+
true
291+
}
292+
})
287293
.map(|c| match c {
288294
'"' | '\'' => "__".to_owned(),
289295
':' | '(' | ')' | '{' | '}' | '[' | ']' | ',' | '.' | '*' | '+' | '/' | '-' | '%'
290-
| '^' | '!' | '&' | '|' => "_".to_owned(),
296+
| '^' | '!' | '&' | '|' | ' ' => "_".to_owned(),
291297
_ => c.to_string(),
292298
})
293299
.collect::<String>()
@@ -403,22 +409,33 @@ mod test {
403409
}
404410

405411
#[rstest]
406-
#[case("1", "1")]
407-
#[case(r#""1""#, "__1__")]
408-
#[case(r#"Some::SomeElse"#, "Some__SomeElse")]
409-
#[case(r#""minnie".to_owned()"#, "__minnie___to_owned__")]
412+
#[case("1", "1", true)]
413+
#[case(r#""1""#, "__1__", true)]
414+
#[case(r#"Some::SomeElse"#, "Some__SomeElse", true)]
415+
#[case(r#""minnie".to_owned()"#, "__minnie___to_owned__", true)]
410416
#[case(
411417
r#"vec![1 , 2,
412418
3]"#,
413-
"vec__1_2_3_"
419+
"vec__1_2_3_",
420+
true
414421
)]
415422
#[case(
416423
r#"some_macro!("first", {second}, [third])"#,
417-
"some_macro____first____second___third__"
424+
"some_macro____first____second___third__",
425+
true
418426
)]
419-
#[case(r#"'x'"#, "__x__")]
420-
#[case::ops(r#"a*b+c/d-e%f^g"#, "a_b_c_d_e_f_g")]
421-
fn sanitaze_ident_name(#[case] expression: impl AsRef<str>, #[case] expected: impl AsRef<str>) {
422-
assert_eq!(expected.as_ref(), sanitize_ident(expression.as_ref()));
427+
#[case(r#"'x'"#, "__x__", true)]
428+
#[case::ops(r#"a*b+c/d-e%f^g"#, "a_b_c_d_e_f_g", true)]
429+
#[case(r#"filter whitespace"#, "filterwhitespace", true)]
430+
#[case(r#"do not filter whitespace"#, "do_not_filter_whitespace", false)]
431+
fn sanitaze_ident_name(
432+
#[case] expression: impl AsRef<str>,
433+
#[case] expected: impl AsRef<str>,
434+
#[case] filter_whitespace: bool,
435+
) {
436+
assert_eq!(
437+
expected.as_ref(),
438+
sanitize_ident(expression.as_ref(), filter_whitespace)
439+
);
423440
}
424441
}

0 commit comments

Comments
 (0)