#![recursion_limit = "128"]
#![warn(
bad_style,
broken_intra_doc_links,
dead_code,
future_incompatible,
illegal_floating_point_literal_pattern,
improper_ctypes,
late_bound_lifetime_arguments,
missing_copy_implementations,
missing_debug_implementations,
no_mangle_generic_items,
non_shorthand_field_patterns,
nonstandard_style,
overflowing_literals,
path_statements,
patterns_in_fns_without_body,
private_in_public,
proc_macro_derive_resolution_fallback,
renamed_and_removed_lints,
rust_2018_compatibility,
rust_2018_idioms,
safe_packed_borrows,
stable_features,
trivial_bounds,
trivial_numeric_casts,
type_alias_bounds,
tyvar_behind_raw_pointer,
unconditional_recursion,
unreachable_code,
unreachable_patterns,
unstable_features,
unstable_name_collisions,
unused,
unused_comparisons,
unused_import_braces,
unused_lifetimes,
unused_qualifications,
unused_results,
where_clauses_object_safety,
while_true
)]
use proc_macro::TokenStream;
use proc_macro2::Ident;
use proc_macro2::Literal;
use proc_macro2::Span;
use proc_macro2::TokenStream as Tokens;
use quote::quote;
use quote::TokenStreamExt;
use syn::punctuated;
const NITROKEY_TEST_GROUP: &str = "NITROKEY_TEST_GROUP";
const NITROKEY_GROUP_NODEV: &str = "nodev";
const NITROKEY_GROUP_LIBREM: &str = "librem";
const NITROKEY_GROUP_PRO: &str = "pro";
const NITROKEY_GROUP_STORAGE: &str = "storage";
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum ArgumentType {
Device,
DeviceWrapper,
Model,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum SupportedDevice {
Librem,
Pro,
Storage,
Any,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Filter {
Librem,
Pro,
Storage,
}
impl Filter {
pub fn from_attribute(attr: &TokenStream) -> Option<Self> {
match attr.to_string().as_ref() {
"librem" => Some(Filter::Librem),
"pro" => Some(Filter::Pro),
"storage" => Some(Filter::Storage),
"" => None,
_ => panic!("unexpected filter argument: {}", attr),
}
}
}
fn filter_device(
device: Option<SupportedDevice>,
filter: Option<Filter>,
) -> Option<SupportedDevice>
{
match device {
None => match filter {
None => None,
Some(Filter::Librem) => Some(SupportedDevice::Librem),
Some(Filter::Pro) => Some(SupportedDevice::Pro),
Some(Filter::Storage) => Some(SupportedDevice::Storage),
},
Some(SupportedDevice::Librem) => match filter {
None |
Some(Filter::Librem) => Some(SupportedDevice::Librem),
Some(Filter::Pro) => panic!("unable to combine 'pro' filter with Librem device"),
Some(Filter::Storage) => panic!("unable to combine 'storage' filter with Librem device"),
},
Some(SupportedDevice::Pro) => match filter {
None |
Some(Filter::Pro) => Some(SupportedDevice::Pro),
Some(Filter::Librem) => panic!("unable to combine 'librem' filter with Pro device"),
Some(Filter::Storage) => panic!("unable to combine 'storage' filter with Pro device"),
},
Some(SupportedDevice::Storage) => match filter {
None |
Some(Filter::Storage) => Some(SupportedDevice::Storage),
Some(Filter::Librem) => panic!("unable to combine 'librem' filter with Storage device"),
Some(Filter::Pro) => panic!("unable to combine 'pro' filter with Storage device"),
},
Some(SupportedDevice::Any) => match filter {
None => Some(SupportedDevice::Any),
Some(Filter::Librem) => Some(SupportedDevice::Librem),
Some(Filter::Pro) => Some(SupportedDevice::Pro),
Some(Filter::Storage) => Some(SupportedDevice::Storage),
},
}
}
#[derive(Clone, Copy, Debug)]
enum DeviceGroup {
No,
Librem,
Pro,
Storage,
}
impl AsRef<str> for DeviceGroup {
fn as_ref(&self) -> &str {
match *self {
DeviceGroup::No => NITROKEY_GROUP_NODEV,
DeviceGroup::Librem => NITROKEY_GROUP_LIBREM,
DeviceGroup::Pro => NITROKEY_GROUP_PRO,
DeviceGroup::Storage => NITROKEY_GROUP_STORAGE,
}
}
}
impl From<Option<SupportedDevice>> for DeviceGroup {
fn from(device: Option<SupportedDevice>) -> Self {
match device {
None => DeviceGroup::No,
Some(device) => match device {
SupportedDevice::Librem => DeviceGroup::Librem,
SupportedDevice::Pro => DeviceGroup::Pro,
SupportedDevice::Storage => DeviceGroup::Storage,
SupportedDevice::Any => panic!("an Any device cannot belong to a group"),
}
}
}
}
impl quote::ToTokens for DeviceGroup {
fn to_tokens(&self, tokens: &mut Tokens) {
tokens.append(Literal::string(self.as_ref()))
}
}
#[proc_macro_attribute]
pub fn test(attr: TokenStream, item: TokenStream) -> TokenStream {
let input = syn::parse_macro_input!(item as syn::ItemFn);
let filter = Filter::from_attribute(&attr);
let dev_type = determine_device(&input.sig.inputs);
let (device, argument) = dev_type
.map_or((None, None), |(device, argument)| {
(Some(device), Some(argument))
});
let device = filter_device(device, filter);
drop(attr);
match device {
None => {
let name = format!("{}", &input.sig.ident);
expand_wrapper(name, None, argument, &input)
},
Some(SupportedDevice::Librem)
| Some(SupportedDevice::Pro)
| Some(SupportedDevice::Storage) => {
let name = format!("{}", &input.sig.ident);
expand_wrapper(name, device, argument, &input)
},
Some(SupportedDevice::Any) => {
let name = format!("{}_librem", &input.sig.ident);
let dev = Some(SupportedDevice::Librem);
let librem = expand_wrapper(name, dev, argument, &input);
let name = format!("{}_pro", &input.sig.ident);
let dev = Some(SupportedDevice::Pro);
let pro = expand_wrapper(name, dev, argument, &input);
let name = format!("{}_storage", &input.sig.ident);
let dev = Some(SupportedDevice::Storage);
let storage = expand_wrapper(name, dev, argument, &input);
quote! {
#librem
#pro
#storage
}
}
}
.into()
}
fn expand_connect(group: DeviceGroup, ret_type: &syn::ReturnType) -> Tokens {
let (ret, check) = match ret_type {
syn::ReturnType::Default => (quote! { return }, quote! {.unwrap()}),
syn::ReturnType::Type(_, _) => (quote! { return Ok(()) }, quote! {?}),
};
let connect = match group {
DeviceGroup::No => quote! { manager.connect() },
DeviceGroup::Librem => quote! { manager.connect_librem() },
DeviceGroup::Pro => quote! { manager.connect_pro() },
DeviceGroup::Storage => quote! { manager.connect_storage() },
};
let connect_cond = if let DeviceGroup::No = group {
quote! { }
} else {
quote! { #connect#check }
};
let connect_err = quote! {
::nitrokey::Error::CommunicationError(::nitrokey::CommunicationError::NotConnected)
};
let skip = if let DeviceGroup::No = group {
quote! {let Err(#connect_err) = result {} else}
} else {
quote! {let Err(#connect_err) = result}
};
let result = if let DeviceGroup::No = group {
quote! { }
} else {
quote! { result#check }
};
quote! {
{
use ::std::io::Write;
match ::std::env::var(#NITROKEY_TEST_GROUP) {
Ok(group) => {
match group.as_ref() {
#NITROKEY_GROUP_NODEV |
#NITROKEY_GROUP_LIBREM |
#NITROKEY_GROUP_PRO |
#NITROKEY_GROUP_STORAGE => {
if group == #group {
#connect_cond
} else {
::std::println!("skipped");
#ret
}
},
x => ::std::panic!("unsupported {} value: {}", #NITROKEY_TEST_GROUP, x),
}
},
Err(::std::env::VarError::NotUnicode(_)) => {
::std::panic!("{} value is not valid unicode", #NITROKEY_TEST_GROUP)
},
Err(::std::env::VarError::NotPresent) => {
let result = #connect;
if #skip {
::std::println!("skipped");
#ret
}
#result
},
}
}
}
}
fn expand_arg<P>(
device: Option<SupportedDevice>,
argument: Option<ArgumentType>,
args: &punctuated::Punctuated<syn::FnArg, P>,
) -> Tokens
where
P: quote::ToTokens,
{
let arg_type = match device {
None => quote! {},
Some(device) => match argument {
None => quote! {},
Some(ArgumentType::Device) => match device {
SupportedDevice::Librem => quote! { ::nitrokey::Librem },
SupportedDevice::Pro => quote! { ::nitrokey::Pro },
SupportedDevice::Storage => quote! { ::nitrokey::Storage },
SupportedDevice::Any => unreachable!(),
},
Some(ArgumentType::DeviceWrapper) => quote! { ::nitrokey::DeviceWrapper },
Some(ArgumentType::Model) => quote! { ::nitrokey::Model },
},
};
match args.first() {
Some(arg) => match arg {
syn::FnArg::Typed(pat_type) => {
let arg = syn::FnArg::Typed(syn::PatType {
attrs: Vec::new(),
pat: pat_type.pat.clone(),
colon_token: pat_type.colon_token,
ty: Box::new(syn::Type::Path(syn::parse_quote! { #arg_type })),
});
quote! { #arg }
}
_ => panic!("unexpected test function argument"),
},
None => quote! {},
}
}
fn expand_call(
device: Option<SupportedDevice>,
argument: Option<ArgumentType>,
wrappee: &syn::ItemFn,
) -> Tokens
{
let test_name = &wrappee.sig.ident;
let group = DeviceGroup::from(device);
let connect = expand_connect(group, &wrappee.sig.output);
let call = match device {
None => quote! { #test_name() },
Some(device) => match argument {
None => quote! { #test_name() },
Some(ArgumentType::Device) => quote! { #test_name(device) },
Some(ArgumentType::DeviceWrapper) => match device {
SupportedDevice::Librem => {
quote! {
#test_name(::nitrokey::DeviceWrapper::Librem(device))
}
},
SupportedDevice::Pro => {
quote! {
#test_name(::nitrokey::DeviceWrapper::Pro(device))
}
},
SupportedDevice::Storage => {
quote! {
#test_name(::nitrokey::DeviceWrapper::Storage(device))
}
},
SupportedDevice::Any => unreachable!(),
},
Some(ArgumentType::Model) => {
let model = match device {
SupportedDevice::Librem => quote! { ::nitrokey::Model::Librem },
SupportedDevice::Pro => quote! { ::nitrokey::Model::Pro },
SupportedDevice::Storage => quote! { ::nitrokey::Model::Storage },
SupportedDevice::Any => unreachable!(),
};
quote! { #test_name(#model) }
}
},
};
match argument {
None |
Some(ArgumentType::Model) => {
quote! {
{
let mut manager = ::nitrokey::force_take().unwrap();
let _ = #connect;
}
#call
}
},
Some(ArgumentType::Device) |
Some(ArgumentType::DeviceWrapper) => {
quote! {
let mut manager = ::nitrokey::force_take().unwrap();
let device = #connect;
#call
}
}
}
}
fn expand_wrapper<S>(
fn_name: S,
device: Option<SupportedDevice>,
argument: Option<ArgumentType>,
wrappee: &syn::ItemFn,
) -> Tokens
where
S: AsRef<str>,
{
let name = Ident::new(fn_name.as_ref(), Span::call_site());
let attrs = &wrappee.attrs;
let body = &wrappee.block;
let test_name = &wrappee.sig.ident;
let test_arg = expand_arg(device, argument, &wrappee.sig.inputs);
let test_call = expand_call(device, argument, wrappee);
let ret_type = match &wrappee.sig.output {
syn::ReturnType::Default => quote! {()},
syn::ReturnType::Type(_, type_) => quote! {#type_},
};
quote! {
#[test]
#(#attrs)*
fn #name() -> #ret_type {
fn #test_name(#test_arg) -> #ret_type {
#body
}
let _guard = ::nitrokey_test_state::mutex()
.lock()
.map_err(|err| err.into_inner());
#test_call
}
}
}
fn determine_device_for_arg(arg: &syn::FnArg) -> (SupportedDevice, ArgumentType) {
match arg {
syn::FnArg::Typed(pat_type) => {
let type_ = &pat_type.ty;
match &**type_ {
syn::Type::Path(path) => {
if path.path.segments.is_empty() {
panic!("invalid function argument type: {}", quote! {#path});
}
let type_ = format!("{}", path.path.segments.last().unwrap().ident);
match type_.as_ref() {
"Model" => (SupportedDevice::Any, ArgumentType::Model),
"Storage" => (SupportedDevice::Storage, ArgumentType::Device),
"Pro" => (SupportedDevice::Pro, ArgumentType::Device),
"Librem" => (SupportedDevice::Librem, ArgumentType::Device),
"DeviceWrapper" => (SupportedDevice::Any, ArgumentType::DeviceWrapper),
_ => panic!("unsupported function argument type: {}", type_),
}
},
_ => panic!("unexpected function argument type: {} (expected owned object)",
quote!{#type_}),
}
}
_ => panic!("unexpected function argument signature: {}", quote! {#arg}),
}
}
fn determine_device<P>(
args: &punctuated::Punctuated<syn::FnArg, P>,
) -> Option<(SupportedDevice, ArgumentType)>
where
P: quote::ToTokens,
{
match args.len() {
0 => None,
1 => Some(determine_device_for_arg(&args[0])),
_ => panic!("functions used as Nitrokey tests can only have zero or one argument"),
}
}
#[cfg(test)]
mod tests {
use super::ArgumentType;
use super::determine_device;
use super::SupportedDevice;
use syn;
#[test]
fn determine_nitrokey_none() {
let input: syn::ItemFn = syn::parse_quote! {
#[nitrokey_test::test]
fn test_none() {}
};
let dev_type = determine_device(&input.sig.inputs);
assert_eq!(dev_type, None);
}
#[test]
fn determine_librem() {
let input: syn::ItemFn = syn::parse_quote! {
#[nitrokey_test::test]
fn test_librem(device: nitrokey::Librem) {}
};
let dev_type = determine_device(&input.sig.inputs);
assert_eq!(dev_type, Some((SupportedDevice::Librem, ArgumentType::Device)));
}
#[test]
fn determine_nitrokey_pro() {
let input: syn::ItemFn = syn::parse_quote! {
#[nitrokey_test::test]
fn test_pro(device: nitrokey::Pro) {}
};
let dev_type = determine_device(&input.sig.inputs);
assert_eq!(dev_type, Some((SupportedDevice::Pro, ArgumentType::Device)));
}
#[test]
fn determine_nitrokey_storage() {
let input: syn::ItemFn = syn::parse_quote! {
#[nitrokey_test::test]
fn test_storage(device: nitrokey::Storage) {}
};
let dev_type = determine_device(&input.sig.inputs);
assert_eq!(dev_type, Some((SupportedDevice::Storage, ArgumentType::Device)));
}
#[test]
fn determine_any_nitrokey() {
let input: syn::ItemFn = syn::parse_quote! {
#[nitrokey_test::test]
fn test_any(device: nitrokey::DeviceWrapper) {}
};
let dev_type = determine_device(&input.sig.inputs);
assert_eq!(dev_type, Some((SupportedDevice::Any, ArgumentType::DeviceWrapper)));
}
#[test]
#[should_panic(expected = "functions used as Nitrokey tests can only have zero or one argument")]
fn determine_wrong_argument_count() {
let input: syn::ItemFn = syn::parse_quote! {
#[nitrokey_test::test]
fn test_pro(device: nitrokey::Pro, _: i32) {}
};
let _ = determine_device(&input.sig.inputs);
}
#[test]
#[should_panic(expected = "unexpected function argument signature: & self")]
fn determine_wrong_function_type() {
let input: syn::ItemFn = syn::parse_quote! {
#[nitrokey_test::test]
fn test_self(&self) {}
};
let _ = determine_device(&input.sig.inputs);
}
#[test]
#[should_panic(expected = "unexpected function argument type: & nitrokey \
:: DeviceWrapper (expected owned object)")]
fn determine_wrong_argument_type() {
let input: syn::ItemFn = syn::parse_quote! {
#[nitrokey_test::test]
fn test_any(device: &nitrokey::DeviceWrapper) {}
};
let _ = determine_device(&input.sig.inputs);
}
#[test]
#[should_panic(expected = "unsupported function argument type: FooBarBaz")]
fn determine_invalid_argument_type() {
let input: syn::ItemFn = syn::parse_quote! {
#[nitrokey_test::test]
fn test_foobarbaz(device: nitrokey::FooBarBaz) {}
};
let _ = determine_device(&input.sig.inputs);
}
}