use std::fs;
use std::io::Read;
use std::panic::{self, AssertUnwindSafe};
use attr_macro_visitor::AttributeMacroVisitor;
use syn::punctuated::Punctuated;
use syn::{Path, Token};
mod attr_macro_visitor;
pub fn emulate_macro_expansion_fallible<F>(
mut file: fs::File,
macro_path: &str,
proc_macro_fn: F,
) -> Result<(), Error>
where
F: Fn(proc_macro2::TokenStream) -> proc_macro2::TokenStream,
{
struct MacroVisitor<F: Fn(proc_macro2::TokenStream) -> proc_macro2::TokenStream> {
macro_path: syn::Path,
proc_macro_fn: AssertUnwindSafe<F>,
}
impl<'ast, F> syn::visit::Visit<'ast> for MacroVisitor<F>
where
F: Fn(proc_macro2::TokenStream) -> proc_macro2::TokenStream,
{
fn visit_macro(&mut self, macro_item: &'ast syn::Macro) {
if macro_item.path == self.macro_path {
(*self.proc_macro_fn)(macro_item.tokens.clone());
}
}
}
let proc_macro_fn = AssertUnwindSafe(proc_macro_fn);
let mut content = String::new();
file.read_to_string(&mut content)
.map_err(Error::IoError)?;
let ast =
AssertUnwindSafe(syn::parse_file(content.as_str()).map_err(Error::ParseError)?);
let macro_path: syn::Path = syn::parse_str(macro_path).map_err(Error::ParseError)?;
panic::catch_unwind(|| {
syn::visit::visit_file(
&mut MacroVisitor::<F> {
macro_path,
proc_macro_fn,
},
&ast,
);
})
.map_err(|_| {
Error::ParseError(syn::parse::Error::new(
proc_macro2::Span::call_site(),
"macro expansion panicked",
))
})?;
Ok(())
}
fn uses_derive(attrs: &[syn::Attribute], derive_name: &syn::Path) -> Result<bool, Error> {
for attr in attrs {
if attr.path().is_ident("derive") {
if let syn::Meta::List(ml) = &attr.meta {
let nested = ml
.parse_args_with(Punctuated::<Path, Token![,]>::parse_terminated)
.map_err(Error::ParseError)?;
let uses_derive = nested.iter().any(|nested_meta| nested_meta == derive_name);
if uses_derive {
return Ok(true);
}
}
}
}
Ok(false)
}
pub fn emulate_derive_expansion_fallible<F>(
mut file: fs::File,
macro_path: &str,
derive_fn: F,
) -> Result<(), Error>
where
F: Fn(syn::DeriveInput) -> proc_macro2::TokenStream,
{
struct MacroVisitor<F: Fn(syn::DeriveInput) -> proc_macro2::TokenStream> {
macro_path: syn::Path,
derive_fn: AssertUnwindSafe<F>,
}
impl<'ast, F> syn::visit::Visit<'ast> for MacroVisitor<F>
where
F: Fn(syn::DeriveInput) -> proc_macro2::TokenStream,
{
fn visit_item_struct(&mut self, node: &'ast syn::ItemStruct) {
match uses_derive(&node.attrs, &self.macro_path) {
Ok(uses) => {
if uses {
(*self.derive_fn)(node.clone().into());
}
}
Err(e) => panic!(
"Failed expanding derive macro for {:?}: {}",
self.macro_path, e
),
}
}
fn visit_item_enum(&mut self, node: &'ast syn::ItemEnum) {
match uses_derive(&node.attrs, &self.macro_path) {
Ok(uses) => {
if uses {
(*self.derive_fn)(node.clone().into());
}
}
Err(e) => panic!(
"Failed expanding derive macro for {:?}: {}",
self.macro_path, e
),
}
}
}
let derive_fn = AssertUnwindSafe(derive_fn);
let mut content = String::new();
file.read_to_string(&mut content)
.map_err(Error::IoError)?;
let ast =
AssertUnwindSafe(syn::parse_file(content.as_str()).map_err(Error::ParseError)?);
let macro_path: syn::Path = syn::parse_str(macro_path).map_err(Error::ParseError)?;
panic::catch_unwind(|| {
syn::visit::visit_file(
&mut MacroVisitor::<F> {
macro_path,
derive_fn,
},
&ast,
);
})
.map_err(|_| {
Error::ParseError(syn::parse::Error::new(
proc_macro2::Span::call_site(),
"macro expansion panicked",
))
})?;
Ok(())
}
pub fn emulate_attribute_expansion_fallible<Arg, Res>(
mut file: fs::File,
macro_path: &str,
macro_fn: impl Fn(Arg, Arg) -> Res,
) -> Result<(), Error>
where
Arg: From<proc_macro2::TokenStream>,
Res: Into<proc_macro2::TokenStream>,
{
let macro_fn = AssertUnwindSafe(
|attr: proc_macro2::TokenStream, item: proc_macro2::TokenStream| {
macro_fn(attr.into(), item.into()).into()
},
);
let mut content = String::new();
file.read_to_string(&mut content).map_err(Error::IoError)?;
let ast = AssertUnwindSafe(syn::parse_file(content.as_str()).map_err(Error::ParseError)?);
let macro_path: syn::Path = syn::parse_str(macro_path).map_err(Error::ParseError)?;
panic::catch_unwind(|| {
syn::visit::visit_file(&mut AttributeMacroVisitor::new(macro_path, macro_fn), &ast);
})
.map_err(|_| {
Error::ParseError(syn::parse::Error::new(
proc_macro2::Span::call_site(),
"macro expansion panicked",
))
})?;
Ok(())
}
#[deprecated]
pub fn emulate_macro_expansion<F>(file: fs::File, macro_path: &str, proc_macro_fn: F)
where
F: Fn(proc_macro2::TokenStream) -> proc_macro2::TokenStream,
{
emulate_macro_expansion_fallible(file, macro_path, proc_macro_fn).unwrap()
}
#[derive(Debug)]
pub enum Error {
IoError(std::io::Error),
ParseError(syn::parse::Error),
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Error::IoError(e) => e.fmt(f),
Error::ParseError(e) => e.fmt(f),
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Error::IoError(e) => e.source(),
Error::ParseError(e) => e.source(),
}
}
}
#[cfg(test)]
mod tests {
use cargo_tarpaulin::config::Config;
use cargo_tarpaulin::launch_tarpaulin;
use std::{
env,
panic::UnwindSafe,
sync::{Arc, Mutex, Once},
time,
};
static mut TARPAULIN_MUTEX: Option<Arc<Mutex<()>>> = None;
static SETUP_TEST_MUTEX: Once = Once::new();
pub(crate) fn test_mutex() -> Arc<Mutex<()>> {
unsafe {
SETUP_TEST_MUTEX.call_once(|| {
TARPAULIN_MUTEX = Some(Arc::new(Mutex::new(())));
});
Arc::clone(TARPAULIN_MUTEX.as_ref().unwrap())
}
}
pub(crate) fn with_test_lock<F, R>(f: F) -> R
where
R: Send + 'static,
F: FnOnce() -> R + Send + UnwindSafe + 'static,
{
let test_mutex = test_mutex();
let test_lock = test_mutex.lock().expect("Failed to acquire test lock");
let res = f();
drop(test_lock);
res
}
#[test]
fn proc_macro_coverage() {
with_test_lock(|| {
let mut config = Config::default();
let test_dir = env::current_dir()
.unwrap()
.join("examples")
.join("custom_assert");
config.set_manifest(test_dir.join("Cargo.toml"));
config.test_timeout = time::Duration::from_secs(60);
let (_trace_map, return_code) = launch_tarpaulin(&config, &None).unwrap();
assert_eq!(return_code, 0);
})
}
#[test]
fn derive_macro_coverage() {
with_test_lock(|| {
let mut config = Config::default();
let test_dir = env::current_dir()
.unwrap()
.join("examples")
.join("custom_derive");
config.set_manifest(test_dir.join("Cargo.toml"));
config.test_timeout = time::Duration::from_secs(60);
let (_trace_map, return_code) = launch_tarpaulin(&config, &None).unwrap();
assert_eq!(return_code, 0);
})
}
#[test]
fn attribute_macro_coverage() {
with_test_lock(|| {
let mut config = Config::default();
let test_dir = env::current_dir()
.unwrap()
.join("examples")
.join("custom_attribute");
config.set_manifest(test_dir.join("Cargo.toml"));
config.test_timeout = time::Duration::from_secs(60);
let (_trace_map, return_code) = launch_tarpaulin(&config, &None).unwrap();
assert_eq!(return_code, 0);
})
}
}