use proc_macro2::TokenTree;
use std::path::Path;
use syn::{Attribute, Item, Meta};
use thiserror::Error;
use crate::constants::{ATTR_CFG, MODULE_NAME_TEST};
pub(crate) const MAX_FILE_BYTES: u64 = 10 * 1024 * 1024;
#[derive(Debug, Error)]
pub(crate) enum ReadFileError {
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("file too large: {size} bytes (limit {limit} bytes)")]
TooLarge { size: u64, limit: u64 },
}
pub(crate) fn read_source_file(path: &Path) -> Result<String, ReadFileError> {
let size = std::fs::metadata(path)?.len();
if size > MAX_FILE_BYTES {
return Err(ReadFileError::TooLarge {
size,
limit: MAX_FILE_BYTES,
});
}
Ok(std::fs::read_to_string(path)?)
}
pub(crate) fn descend_inline_module<'a, T: AsRef<str>>(
items: &'a [Item],
path: &[T],
) -> Option<&'a [Item]> {
let (head, tail) = path.split_first()?;
let head = head.as_ref();
for item in items {
if let Item::Mod(m) = item
&& m.ident == head
&& let Some((_, nested)) = &m.content
{
return if tail.is_empty() {
Some(nested)
} else {
descend_inline_module(nested, tail)
};
}
}
None
}
pub(crate) fn has_cfg_test(attrs: &[Attribute]) -> bool {
fn stream_contains_test(stream: proc_macro2::TokenStream) -> bool {
let tokens: Vec<TokenTree> = stream.into_iter().collect();
let mut i = 0;
while i < tokens.len() {
match &tokens[i] {
TokenTree::Ident(ident) if ident == MODULE_NAME_TEST => return true,
TokenTree::Ident(ident) if ident == "not" => {
if matches!(tokens.get(i + 1), Some(TokenTree::Group(_))) {
i += 2;
continue;
}
}
TokenTree::Group(group) if stream_contains_test(group.stream()) => {
return true;
}
_ => {}
}
i += 1;
}
false
}
for attr in attrs {
if let Meta::List(meta_list) = &attr.meta
&& meta_list.path.is_ident(ATTR_CFG)
&& stream_contains_test(meta_list.tokens.clone())
{
return true;
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
use syn::parse_quote;
#[test]
fn test_simple_cfg_test() {
let attrs: Vec<Attribute> = vec![parse_quote!(#[cfg(test)])];
assert!(has_cfg_test(&attrs));
}
#[test]
fn test_cfg_all_with_test() {
let attrs: Vec<Attribute> = vec![parse_quote!(#[cfg(all(test, feature = "foo"))])];
assert!(has_cfg_test(&attrs));
}
#[test]
fn test_cfg_any_with_test() {
let attrs: Vec<Attribute> = vec![parse_quote!(#[cfg(any(test, doc))])];
assert!(has_cfg_test(&attrs));
}
#[test]
fn test_cfg_test_among_multiple_attrs() {
let attrs: Vec<Attribute> = vec![
parse_quote!(#[derive(Debug)]),
parse_quote!(#[cfg(test)]),
parse_quote!(#[allow(unused)]),
];
assert!(has_cfg_test(&attrs));
}
#[test]
fn test_cfg_not_test_returns_false() {
let attrs: Vec<Attribute> = vec![parse_quote!(#[cfg(not(test))])];
assert!(!has_cfg_test(&attrs));
}
#[test]
fn test_no_cfg_attr_returns_false() {
let attrs: Vec<Attribute> = vec![parse_quote!(#[derive(Debug)])];
assert!(!has_cfg_test(&attrs));
}
#[test]
fn test_cfg_other_target_returns_false() {
let attrs: Vec<Attribute> = vec![parse_quote!(#[cfg(target_os = "linux")])];
assert!(!has_cfg_test(&attrs));
}
#[test]
fn test_empty_attrs_returns_false() {
assert!(!has_cfg_test(&[]));
}
#[test]
fn test_cfg_all_with_not_test_returns_false() {
let attrs: Vec<Attribute> = vec![parse_quote!(#[cfg(all(not(test)))])];
assert!(!has_cfg_test(&attrs));
}
#[test]
fn test_cfg_all_test_and_not_test() {
let attrs: Vec<Attribute> = vec![parse_quote!(#[cfg(all(test, not(feature = "x")))])];
assert!(has_cfg_test(&attrs));
}
fn inline_mod(name: &str, items: &[Item]) -> Item {
let ident: syn::Ident = syn::parse_str(name).expect("valid ident");
Item::Mod(syn::parse_quote! { mod #ident { #(#items)* } })
}
fn struct_item(name: &str) -> Item {
let ident: syn::Ident = syn::parse_str(name).expect("valid ident");
syn::parse_quote! { struct #ident; }
}
#[test]
fn descend_inline_module_finds_single_level() {
let inner = struct_item("Foo");
let items = vec![inline_mod("utils", &[inner])];
let result = descend_inline_module(&items, &["utils"]);
assert!(result.is_some());
assert_eq!(result.unwrap().len(), 1);
}
#[test]
fn descend_inline_module_finds_two_levels() {
let leaf = struct_item("Leaf");
let inner = inline_mod("inner", &[leaf]);
let items = vec![inline_mod("outer", &[inner])];
let result = descend_inline_module(&items, &["outer", "inner"]);
assert!(result.is_some());
assert_eq!(result.unwrap().len(), 1);
}
#[test]
fn descend_inline_module_empty_path_returns_none() {
let items = vec![struct_item("Foo")];
assert!(descend_inline_module(&items, &[] as &[&str]).is_none());
}
#[test]
fn descend_inline_module_missing_segment_returns_none() {
let items = vec![inline_mod("present", &[])];
assert!(descend_inline_module(&items, &["absent"]).is_none());
}
#[test]
fn descend_inline_module_no_body_returns_none() {
let items: Vec<Item> = vec![syn::parse_quote! { mod foo; }];
assert!(descend_inline_module(&items, &["foo"]).is_none());
}
#[test]
fn descend_inline_module_works_with_owned_strings() {
let items = vec![inline_mod("utils", &[struct_item("Bar")])];
let path: Vec<String> = vec!["utils".to_owned()];
assert!(descend_inline_module(&items, &path).is_some());
}
#[test]
fn read_source_file_rejects_file_exceeding_limit() {
let file = tempfile::NamedTempFile::new().expect("create temp file");
file.as_file()
.set_len(MAX_FILE_BYTES + 1)
.expect("set file length");
let err = read_source_file(file.path()).unwrap_err();
assert!(matches!(
err,
ReadFileError::TooLarge { size, limit }
if size == MAX_FILE_BYTES + 1 && limit == MAX_FILE_BYTES
));
}
}