Skip to content

Commit

Permalink
feat: add include_str and include_bytes file input behaviour
Browse files Browse the repository at this point in the history
Adds the ability to set the file input format using the
`#[file_mode = "..."]` attribute. The attribute accepts `path`,
`include_str`, and `include_bytes`. `path` is the default behaviour,
`include_str` and `include_bytes` pass the file contents instead of the
path of the file.

Closes #295.
  • Loading branch information
lucascool12 committed Feb 9, 2025
1 parent e0b735e commit 6ee9dd1
Show file tree
Hide file tree
Showing 4 changed files with 190 additions and 7 deletions.
44 changes: 43 additions & 1 deletion rstest/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -617,7 +617,7 @@ pub use rstest_macros::fixture;
/// - Arguments Attributes:
/// - [`#[case]`](#test-parametrized-cases) define an argument parametrized by test cases
/// - [`#[values(...)]`](#values-lists) define an argument that can be a list of values
/// - [`#[files(...)]`-`#[exclude(...)]`-`#[base_dir = ... ]`](#files-path-as-input-arguments)
/// - [`#[files(...)]`-`#[exclude(...)]`-`#[base_dir = ... ]`-`#[file_mode = "..."]`](#files-path-as-input-arguments)
/// define an argument that can be a list of path based on a glob pattern
/// - [`#[from(...)]`-`#[with(...)]`](#injecting-fixtures) handling injected fixture
/// - [`#[future]`](#async) implement future boilerplate argument
Expand Down Expand Up @@ -1032,6 +1032,48 @@ pub use rstest_macros::fixture;
/// }
/// ```
///
/// ### `#[file_mode = "..."]`
///
/// Using the `#[file_mode = "..."]` attribute on an argument tagged with
/// `#[files(...)]` and friends will set the way the file is passed as an argument.
/// It accepts the following arguments with the following behaviour:
///
/// - `path`: the default behaviour, passes each file path as a [PathBuf][std::path::PathBuf].
/// - `include_str`: passes the contents of each file as a [&str][str] using [include_str].
/// - `include_bytes`: passes the contents of each files as a `&[u8]` using [include_bytes].
///
/// Trying to pass directories as arguments will cause a compile error when using `include_str` and
/// `include_bytes`.
///
/// Note on `println!("cargo::rerun-if-changed=tests/resources");`:
/// All files that get embedded by [include_str] or [include_bytes] will be checked for changes
/// when recompiling. So the `println` is not strictly needed when all necessary files are included
/// with `#[file_mode = "include_str"]` or `#[file_mode = "include_bytes"]`.
///
/// ```
/// # use rstest::rstest;
/// #[rstest]
/// fn for_each_path(
/// #[files("src/**/*.rs")] #[exclude("test")] #[file_mode = "path"] path: PathBuf
/// ) {
/// assert!(contents.len() >= 0)
/// }
///
/// #[rstest]
/// fn for_each_txt_file(
/// #[files("src/**/*.rs")] #[exclude("test")] #[file_mode = "include_str"] contents: &str
/// ) {
/// assert!(contents.len() >= 0)
/// }
///
/// #[rstest]
/// fn for_each_bin_file(
/// #[files("src/**/*.rs")] #[exclude("test")] #[file_mode = "include_bytes"] contents: &[u8]
/// ) {
/// assert!(contents.len() >= 0)
/// }
/// ```
///
/// ## Use Parametrize definition in more tests
///
/// If you need to use a test list for more than one test you can use
Expand Down
38 changes: 38 additions & 0 deletions rstest/tests/resources/rstest/files.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,22 @@ fn start_with_name(
assert!(contents.starts_with(name.to_str().unwrap()))
}

#[rstest]
fn start_with_name_file_mode(
#[files("files/**/*.txt")]
#[exclude("exclude")]
#[files("../files_test_sub_folder/**/*.txt")]
#[file_mode = "path"]
path: PathBuf,
) {
let name = path.file_name().unwrap();
let mut f = File::open(&path).unwrap();
let mut contents = String::new();
f.read_to_string(&mut contents).unwrap();

assert!(contents.starts_with(name.to_str().unwrap()))
}

#[rstest]
fn start_with_name_with_include(
#[files("files/**/*.txt")]
Expand Down Expand Up @@ -91,6 +107,28 @@ fn env_vars_base_dir(
assert!(contents.starts_with(name.to_str().unwrap()))
}

#[rstest]
fn include_str(
#[files("files/**/*.txt")]
#[exclude("exclude")]
#[files("../files_test_sub_folder/**/*.txt")]
#[file_mode = "include_str"]
contents: &str,
) {
assert!(contents.len() != 0)
}

#[rstest]
fn include_bytes(
#[files("files/**/*.txt")]
#[exclude("exclude")]
#[files("../files_test_sub_folder/**/*.txt")]
#[file_mode = "include_bytes"]
contents: &[u8],
) {
assert!(contents.len() != 0)
}

mod module {
#[rstest::rstest]
fn pathbuf_need_not_be_in_scope(
Expand Down
18 changes: 18 additions & 0 deletions rstest/tests/rstest/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,18 @@ fn files() {
.ok("ignore_missing_env_vars::path_3_files_element_2_txt")
.ok("ignore_missing_env_vars::path_4_files_element_3_txt")
.ok("ignore_missing_env_vars::path_5_files_sub_sub_dir_file_txt")
.ok("include_bytes::contents_1__UP_files_test_sub_folder_from_parent_folder_txt")
.ok("include_bytes::contents_2_files_element_0_txt")
.ok("include_bytes::contents_3_files_element_1_txt")
.ok("include_bytes::contents_4_files_element_2_txt")
.ok("include_bytes::contents_5_files_element_3_txt")
.ok("include_bytes::contents_6_files_sub_sub_dir_file_txt")
.ok("include_str::contents_1__UP_files_test_sub_folder_from_parent_folder_txt")
.ok("include_str::contents_2_files_element_0_txt")
.ok("include_str::contents_3_files_element_1_txt")
.ok("include_str::contents_4_files_element_2_txt")
.ok("include_str::contents_5_files_element_3_txt")
.ok("include_str::contents_6_files_sub_sub_dir_file_txt")
.ok("module::pathbuf_need_not_be_in_scope::path_1_files__ignore_me_txt")
.ok("module::pathbuf_need_not_be_in_scope::path_2_files_element_0_txt")
.ok("module::pathbuf_need_not_be_in_scope::path_3_files_element_1_txt")
Expand All @@ -96,6 +108,12 @@ fn files() {
.ok("start_with_name::path_4_files_element_2_txt")
.ok("start_with_name::path_5_files_element_3_txt")
.ok("start_with_name::path_6_files_sub_sub_dir_file_txt")
.ok("start_with_name_file_mode::path_1__UP_files_test_sub_folder_from_parent_folder_txt")
.ok("start_with_name_file_mode::path_2_files_element_0_txt")
.ok("start_with_name_file_mode::path_3_files_element_1_txt")
.ok("start_with_name_file_mode::path_4_files_element_2_txt")
.ok("start_with_name_file_mode::path_5_files_element_3_txt")
.ok("start_with_name_file_mode::path_6_files_sub_sub_dir_file_txt")
.ok("start_with_name_with_include::path_1_files__ignore_me_txt")
.ok("start_with_name_with_include::path_2_files_element_0_txt")
.ok("start_with_name_with_include::path_3_files_element_1_txt")
Expand Down
97 changes: 91 additions & 6 deletions rstest_macros/src/parse/rstest/files.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::{env, path::PathBuf};
use std::{env, path::PathBuf, str::FromStr};

use crate::{
error::ErrorsVec,
Expand All @@ -22,6 +22,7 @@ pub(crate) struct FilesGlobReferences {
exclude: Vec<Exclude>,
ignore_dot_files: bool,
ignore_missing_env_vars: bool,
files_mode: FilesMode,
}

impl FilesGlobReferences {
Expand Down Expand Up @@ -122,6 +123,34 @@ impl FilesGlobReferences {
}
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum FilesMode {
Path,
IncludeStr,
IncludeBytes,
}

impl Default for FilesMode {
fn default() -> Self {
Self::Path
}
}

pub(crate) struct NotAFilesMode;

impl FromStr for FilesMode {
type Err = NotAFilesMode;

fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"path" => Ok(Self::Path),
"include_str" => Ok(Self::IncludeStr),
"include_bytes" => Ok(Self::IncludeBytes),
_ => Err(NotAFilesMode),
}
}
}

trait RaiseError: ToTokens {
fn error(&self, msg: &str) -> syn::Error {
syn::Error::new_spanned(self, msg)
Expand All @@ -142,13 +171,15 @@ impl FilesGlobReferences {
exclude: Vec<Exclude>,
ignore_dot_files: bool,
ignore_missing_env_vars: bool,
files_mode: FilesMode,
) -> Self {
Self {
glob,
base_dir: None,
exclude,
ignore_dot_files,
ignore_missing_env_vars,
files_mode,
}
}

Expand Down Expand Up @@ -237,7 +268,7 @@ impl TryFrom<Attribute> for Exclude {

impl From<Vec<LitStrAttr>> for FilesGlobReferences {
fn from(value: Vec<LitStrAttr>) -> Self {
Self::new(value, Default::default(), true, false)
Self::new(value, Default::default(), true, false, Default::default())
}
}

Expand Down Expand Up @@ -337,6 +368,20 @@ impl ValueFilesExtractor {
},
)
}

fn extract_file_mode(&mut self, node: &mut FnArg) -> Vec<LitStrAttr> {
self.extract_argument_attrs(
node,
|a| attr_is(a, "file_mode"),
|attr| {
LitStrAttr::parse_meta_name_value(&attr).map_err(|_| {
attr.error(
"Use #[file_mode = \"...\"] to define the argument of the file input",
)
})
},
)
}
}

impl VisitMut for ValueFilesExtractor {
Expand Down Expand Up @@ -369,6 +414,17 @@ impl VisitMut for ValueFilesExtractor {
.push(attr.error(r#"Cannot use #[base_dir = "..."] more than once"#))
})
}
let files_mode_attr = self.extract_file_mode(node);
let files_mode = if let Some(value) = files_mode_attr.first() {
files_mode_attr.iter().skip(1).for_each(|attr| {
self.errors
.push(attr.error(r#"Cannot use #[file_mode = "..."] more than once"#))
});
FilesMode::from_str(&value.value())
.unwrap_or(Default::default())
} else {
Default::default()
};
if !files.is_empty() {
self.files.push((
name,
Expand All @@ -377,6 +433,7 @@ impl VisitMut for ValueFilesExtractor {
excludes,
include_dot_files.is_empty(),
!ignore_missing_env_vars.is_empty(),
files_mode,
)
.with_base_dir_opt(base_dir.into_iter().next()),
))
Expand Down Expand Up @@ -523,10 +580,19 @@ impl ValueListFromFiles<'_> {
}

let path_str = abs_path.to_string_lossy();
values.push((
parse_quote! {
let value = match refs.files_mode {
FilesMode::Path => parse_quote! {
<::std::path::PathBuf as std::str::FromStr>::from_str(#path_str).unwrap()
},
FilesMode::IncludeStr => parse_quote! {
include_str!(#path_str)
},
FilesMode::IncludeBytes => parse_quote! {
include_bytes!(#path_str)
},
};
values.push((
value,
render_file_description(&relative_path),
));
}
Expand Down Expand Up @@ -688,6 +754,7 @@ mod should {
ex.as_ref().iter().map(|&ex| ex.into()).collect(),
*ignore,
*ignore_envvars,
Default::default(),
)
.with_base_dir_opt(base_dir.map(base_dir_attr)),
)
Expand Down Expand Up @@ -743,6 +810,10 @@ mod should {
r#"fn f(#[files("some")] #[base_dir = 123] a: PathBuf) {}"#,
"base directory for the glob path"
)]
#[case::multiple_file_modes(
r#"fn f(#[files("some")] #[file_mode = "include_str"] #[file_mode = "include_str"] a: PathBuf) {}"#,
r#"Cannot use #[file_mode = \"...\"] more than once"#
)]
fn raise_error(#[case] item_fn: &str, #[case] message: &str) {
let mut item_fn: ItemFn = item_fn.ast();

Expand Down Expand Up @@ -896,7 +967,13 @@ mod should {
let values = ValueListFromFiles::new(FakeBaseDir::from(bdir), resolver)
.to_value_list(vec![(
ident("a"),
FilesGlobReferences::new(paths, exclude, ignore_dot_files, false),
FilesGlobReferences::new(
paths,
exclude,
ignore_dot_files,
false,
Default::default()
),
)])
.unwrap();

Expand Down Expand Up @@ -958,6 +1035,7 @@ mod should {
Default::default(),
true,
false,
Default::default(),
),
)])
.unwrap();
Expand All @@ -974,6 +1052,7 @@ mod should {
Default::default(),
true,
false,
Default::default(),
),
)])
.unwrap();
Expand Down Expand Up @@ -1014,7 +1093,13 @@ mod should {
.collect(),
);
let refs =
FilesGlobReferences::new(vec![], Default::default(), true, ignore_missing_env_vars);
FilesGlobReferences::new(
vec![],
Default::default(),
true,
ignore_missing_env_vars,
Default::default()
);

let result = refs.replace_env_vars(&files_attr(glob), resolver);
match (&result, expected) {
Expand Down

0 comments on commit 6ee9dd1

Please sign in to comment.