1#![recursion_limit = "128"]
21#![warn(
22 bad_style,
23 broken_intra_doc_links,
24 dead_code,
25 future_incompatible,
26 illegal_floating_point_literal_pattern,
27 improper_ctypes,
28 late_bound_lifetime_arguments,
29 missing_copy_implementations,
30 missing_debug_implementations,
31 no_mangle_generic_items,
32 non_shorthand_field_patterns,
33 nonstandard_style,
34 overflowing_literals,
35 path_statements,
36 patterns_in_fns_without_body,
37 private_in_public,
38 proc_macro_derive_resolution_fallback,
39 renamed_and_removed_lints,
40 rust_2018_compatibility,
41 rust_2018_idioms,
42 safe_packed_borrows,
43 stable_features,
44 trivial_bounds,
45 trivial_numeric_casts,
46 type_alias_bounds,
47 tyvar_behind_raw_pointer,
48 unconditional_recursion,
49 unreachable_code,
50 unreachable_patterns,
51 unstable_features,
52 unstable_name_collisions,
53 unused,
54 unused_comparisons,
55 unused_import_braces,
56 unused_lifetimes,
57 unused_qualifications,
58 unused_results,
59 where_clauses_object_safety,
60 while_true
61)]
62
63use proc_macro::TokenStream;
92use proc_macro2::Ident;
93use proc_macro2::Literal;
94use proc_macro2::Span;
95use proc_macro2::TokenStream as Tokens;
96use quote::quote;
97use quote::TokenStreamExt;
98use syn::punctuated;
99
100
101const NITROKEY_TEST_GROUP: &str = "NITROKEY_TEST_GROUP";
105const NITROKEY_GROUP_NODEV: &str = "nodev";
108const NITROKEY_GROUP_LIBREM: &str = "librem";
111const NITROKEY_GROUP_PRO: &str = "pro";
114const NITROKEY_GROUP_STORAGE: &str = "storage";
117
118
119#[derive(Clone, Copy, Debug, PartialEq, Eq)]
121enum ArgumentType {
122 Device,
124 DeviceWrapper,
126 Model,
128}
129
130#[derive(Clone, Copy, Debug, PartialEq, Eq)]
132enum SupportedDevice {
133 Librem,
135 Pro,
137 Storage,
139 Any,
141}
142
143#[derive(Clone, Copy, Debug, PartialEq, Eq)]
145enum Filter {
146 Librem,
148 Pro,
150 Storage,
152}
153
154impl Filter {
155 pub fn from_attribute(attr: &TokenStream) -> Option<Self> {
156 match attr.to_string().as_ref() {
157 "librem" => Some(Filter::Librem),
158 "pro" => Some(Filter::Pro),
159 "storage" => Some(Filter::Storage),
160 "" => None,
161 _ => panic!("unexpected filter argument: {}", attr),
162 }
163 }
164}
165
166
167fn filter_device(
200 device: Option<SupportedDevice>,
201 filter: Option<Filter>,
202) -> Option<SupportedDevice>
203{
204 match device {
205 None => match filter {
206 None => None,
207 Some(Filter::Librem) => Some(SupportedDevice::Librem),
211 Some(Filter::Pro) => Some(SupportedDevice::Pro),
212 Some(Filter::Storage) => Some(SupportedDevice::Storage),
213 },
214 Some(SupportedDevice::Librem) => match filter {
215 None |
216 Some(Filter::Librem) => Some(SupportedDevice::Librem),
217 Some(Filter::Pro) => panic!("unable to combine 'pro' filter with Librem device"),
218 Some(Filter::Storage) => panic!("unable to combine 'storage' filter with Librem device"),
219 },
220 Some(SupportedDevice::Pro) => match filter {
221 None |
222 Some(Filter::Pro) => Some(SupportedDevice::Pro),
223 Some(Filter::Librem) => panic!("unable to combine 'librem' filter with Pro device"),
224 Some(Filter::Storage) => panic!("unable to combine 'storage' filter with Pro device"),
225 },
226 Some(SupportedDevice::Storage) => match filter {
227 None |
228 Some(Filter::Storage) => Some(SupportedDevice::Storage),
229 Some(Filter::Librem) => panic!("unable to combine 'librem' filter with Storage device"),
230 Some(Filter::Pro) => panic!("unable to combine 'pro' filter with Storage device"),
231 },
232 Some(SupportedDevice::Any) => match filter {
233 None => Some(SupportedDevice::Any),
234 Some(Filter::Librem) => Some(SupportedDevice::Librem),
235 Some(Filter::Pro) => Some(SupportedDevice::Pro),
236 Some(Filter::Storage) => Some(SupportedDevice::Storage),
237 },
238 }
239}
240
241
242#[derive(Clone, Copy, Debug)]
244enum DeviceGroup {
245 No,
248 Librem,
250 Pro,
252 Storage,
254}
255
256impl AsRef<str> for DeviceGroup {
257 fn as_ref(&self) -> &str {
258 match *self {
259 DeviceGroup::No => NITROKEY_GROUP_NODEV,
260 DeviceGroup::Librem => NITROKEY_GROUP_LIBREM,
261 DeviceGroup::Pro => NITROKEY_GROUP_PRO,
262 DeviceGroup::Storage => NITROKEY_GROUP_STORAGE,
263 }
264 }
265}
266
267impl From<Option<SupportedDevice>> for DeviceGroup {
268 fn from(device: Option<SupportedDevice>) -> Self {
269 match device {
270 None => DeviceGroup::No,
271 Some(device) => match device {
272 SupportedDevice::Librem => DeviceGroup::Librem,
273 SupportedDevice::Pro => DeviceGroup::Pro,
274 SupportedDevice::Storage => DeviceGroup::Storage,
275 SupportedDevice::Any => panic!("an Any device cannot belong to a group"),
276 }
277 }
278 }
279}
280
281impl quote::ToTokens for DeviceGroup {
282 fn to_tokens(&self, tokens: &mut Tokens) {
283 tokens.append(Literal::string(self.as_ref()))
284 }
285}
286
287
288#[proc_macro_attribute]
365pub fn test(attr: TokenStream, item: TokenStream) -> TokenStream {
366 let input = syn::parse_macro_input!(item as syn::ItemFn);
367 let filter = Filter::from_attribute(&attr);
368 let dev_type = determine_device(&input.sig.inputs);
369 let (device, argument) = dev_type
370 .map_or((None, None), |(device, argument)| {
371 (Some(device), Some(argument))
372 });
373 let device = filter_device(device, filter);
374
375 drop(attr);
377
378 match device {
379 None => {
380 let name = format!("{}", &input.sig.ident);
381 expand_wrapper(name, None, argument, &input)
382 },
383 Some(SupportedDevice::Librem)
384 | Some(SupportedDevice::Pro)
385 | Some(SupportedDevice::Storage) => {
386 let name = format!("{}", &input.sig.ident);
387 expand_wrapper(name, device, argument, &input)
388 },
389 Some(SupportedDevice::Any) => {
390 let name = format!("{}_librem", &input.sig.ident);
391 let dev = Some(SupportedDevice::Librem);
392 let librem = expand_wrapper(name, dev, argument, &input);
393
394 let name = format!("{}_pro", &input.sig.ident);
395 let dev = Some(SupportedDevice::Pro);
396 let pro = expand_wrapper(name, dev, argument, &input);
397
398 let name = format!("{}_storage", &input.sig.ident);
399 let dev = Some(SupportedDevice::Storage);
400 let storage = expand_wrapper(name, dev, argument, &input);
401
402 quote! {
404 #librem
405 #pro
406 #storage
407 }
408 }
409 }
410 .into()
411}
412
413fn expand_connect(group: DeviceGroup, ret_type: &syn::ReturnType) -> Tokens {
414 let (ret, check) = match ret_type {
415 syn::ReturnType::Default => (quote! { return }, quote! {.unwrap()}),
416 syn::ReturnType::Type(_, _) => (quote! { return Ok(()) }, quote! {?}),
422 };
423
424 let connect = match group {
425 DeviceGroup::No => quote! { manager.connect() },
426 DeviceGroup::Librem => quote! { manager.connect_librem() },
427 DeviceGroup::Pro => quote! { manager.connect_pro() },
428 DeviceGroup::Storage => quote! { manager.connect_storage() },
429 };
430
431 let connect_cond = if let DeviceGroup::No = group {
432 quote! { }
433 } else {
434 quote! { #connect#check }
435 };
436
437 let connect_err = quote! {
438 ::nitrokey::Error::CommunicationError(::nitrokey::CommunicationError::NotConnected)
439 };
440 let skip = if let DeviceGroup::No = group {
441 quote! {let Err(#connect_err) = result {} else}
442 } else {
443 quote! {let Err(#connect_err) = result}
444 };
445
446 let result = if let DeviceGroup::No = group {
447 quote! { }
448 } else {
449 quote! { result#check }
450 };
451
452 quote! {
453 {
454 use ::std::io::Write;
455 match ::std::env::var(#NITROKEY_TEST_GROUP) {
456 Ok(group) => {
457 match group.as_ref() {
458 #NITROKEY_GROUP_NODEV |
459 #NITROKEY_GROUP_LIBREM |
460 #NITROKEY_GROUP_PRO |
461 #NITROKEY_GROUP_STORAGE => {
462 if group == #group {
463 #connect_cond
464 } else {
465 ::std::println!("skipped");
466 #ret
467 }
468 },
469 x => ::std::panic!("unsupported {} value: {}", #NITROKEY_TEST_GROUP, x),
470 }
471 },
472 Err(::std::env::VarError::NotUnicode(_)) => {
473 ::std::panic!("{} value is not valid unicode", #NITROKEY_TEST_GROUP)
474 },
475 Err(::std::env::VarError::NotPresent) => {
476 let result = #connect;
479 if #skip {
480 ::std::println!("skipped");
500 #ret
501 }
502 #result
503 },
504 }
505 }
506 }
507}
508
509fn expand_arg<P>(
510 device: Option<SupportedDevice>,
511 argument: Option<ArgumentType>,
512 args: &punctuated::Punctuated<syn::FnArg, P>,
513) -> Tokens
514where
515 P: quote::ToTokens,
516{
517 let arg_type = match device {
522 None => quote! {},
523 Some(device) => match argument {
524 None => quote! {},
525 Some(ArgumentType::Device) => match device {
526 SupportedDevice::Librem => quote! { ::nitrokey::Librem },
527 SupportedDevice::Pro => quote! { ::nitrokey::Pro },
528 SupportedDevice::Storage => quote! { ::nitrokey::Storage },
529 SupportedDevice::Any => unreachable!(),
530 },
531 Some(ArgumentType::DeviceWrapper) => quote! { ::nitrokey::DeviceWrapper },
532 Some(ArgumentType::Model) => quote! { ::nitrokey::Model },
533 },
534 };
535
536 match args.first() {
537 Some(arg) => match arg {
538 syn::FnArg::Typed(pat_type) => {
539 let arg = syn::FnArg::Typed(syn::PatType {
540 attrs: Vec::new(),
541 pat: pat_type.pat.clone(),
542 colon_token: pat_type.colon_token,
543 ty: Box::new(syn::Type::Path(syn::parse_quote! { #arg_type })),
544 });
545 quote! { #arg }
546 }
547 _ => panic!("unexpected test function argument"),
548 },
549 None => quote! {},
550 }
551}
552
553fn expand_call(
554 device: Option<SupportedDevice>,
555 argument: Option<ArgumentType>,
556 wrappee: &syn::ItemFn,
557) -> Tokens
558{
559 let test_name = &wrappee.sig.ident;
560 let group = DeviceGroup::from(device);
561 let connect = expand_connect(group, &wrappee.sig.output);
562
563 let call = match device {
564 None => quote! { #test_name() },
565 Some(device) => match argument {
566 None => quote! { #test_name() },
567 Some(ArgumentType::Device) => quote! { #test_name(device) },
568 Some(ArgumentType::DeviceWrapper) => match device {
569 SupportedDevice::Librem => {
570 quote! {
571 #test_name(::nitrokey::DeviceWrapper::Librem(device))
572 }
573 },
574 SupportedDevice::Pro => {
575 quote! {
576 #test_name(::nitrokey::DeviceWrapper::Pro(device))
577 }
578 },
579 SupportedDevice::Storage => {
580 quote! {
581 #test_name(::nitrokey::DeviceWrapper::Storage(device))
582 }
583 },
584 SupportedDevice::Any => unreachable!(),
585 },
586 Some(ArgumentType::Model) => {
587 let model = match device {
588 SupportedDevice::Librem => quote! { ::nitrokey::Model::Librem },
589 SupportedDevice::Pro => quote! { ::nitrokey::Model::Pro },
590 SupportedDevice::Storage => quote! { ::nitrokey::Model::Storage },
591 SupportedDevice::Any => unreachable!(),
592 };
593 quote! { #test_name(#model) }
594 }
595 },
596 };
597
598 match argument {
599 None |
600 Some(ArgumentType::Model) => {
601 quote! {
606 {
607 let mut manager = ::nitrokey::force_take().unwrap();
608 let _ = #connect;
609 }
610 #call
611 }
612 },
613 Some(ArgumentType::Device) |
614 Some(ArgumentType::DeviceWrapper) => {
615 quote! {
616 let mut manager = ::nitrokey::force_take().unwrap();
617 let device = #connect;
618 #call
619 }
620 }
621 }
622}
623
624fn expand_wrapper<S>(
626 fn_name: S,
627 device: Option<SupportedDevice>,
628 argument: Option<ArgumentType>,
629 wrappee: &syn::ItemFn,
630) -> Tokens
631where
632 S: AsRef<str>,
633{
634 let name = Ident::new(fn_name.as_ref(), Span::call_site());
639 let attrs = &wrappee.attrs;
640 let body = &wrappee.block;
641 let test_name = &wrappee.sig.ident;
642 let test_arg = expand_arg(device, argument, &wrappee.sig.inputs);
643 let test_call = expand_call(device, argument, wrappee);
644
645 let ret_type = match &wrappee.sig.output {
646 syn::ReturnType::Default => quote! {()},
647 syn::ReturnType::Type(_, type_) => quote! {#type_},
648 };
649
650 quote! {
651 #[test]
652 #(#attrs)*
653 fn #name() -> #ret_type {
654 fn #test_name(#test_arg) -> #ret_type {
655 #body
656 }
657
658 let _guard = ::nitrokey_test_state::mutex()
665 .lock()
666 .map_err(|err| err.into_inner());
667 #test_call
668 }
669 }
670}
671
672fn determine_device_for_arg(arg: &syn::FnArg) -> (SupportedDevice, ArgumentType) {
673 match arg {
674 syn::FnArg::Typed(pat_type) => {
675 let type_ = &pat_type.ty;
676 match &**type_ {
677 syn::Type::Path(path) => {
678 if path.path.segments.is_empty() {
679 panic!("invalid function argument type: {}", quote! {#path});
680 }
681
682 let type_ = format!("{}", path.path.segments.last().unwrap().ident);
683 match type_.as_ref() {
684 "Model" => (SupportedDevice::Any, ArgumentType::Model),
685 "Storage" => (SupportedDevice::Storage, ArgumentType::Device),
686 "Pro" => (SupportedDevice::Pro, ArgumentType::Device),
687 "Librem" => (SupportedDevice::Librem, ArgumentType::Device),
688 "DeviceWrapper" => (SupportedDevice::Any, ArgumentType::DeviceWrapper),
689 _ => panic!("unsupported function argument type: {}", type_),
690 }
691 },
692 _ => panic!("unexpected function argument type: {} (expected owned object)",
693 quote!{#type_}),
694 }
695 }
696 _ => panic!("unexpected function argument signature: {}", quote! {#arg}),
697 }
698}
699
700fn determine_device<P>(
703 args: &punctuated::Punctuated<syn::FnArg, P>,
704) -> Option<(SupportedDevice, ArgumentType)>
705where
706 P: quote::ToTokens,
707{
708 match args.len() {
709 0 => None,
710 1 => Some(determine_device_for_arg(&args[0])),
711 _ => panic!("functions used as Nitrokey tests can only have zero or one argument"),
712 }
713}
714
715
716#[cfg(test)]
717mod tests {
718 use super::ArgumentType;
719 use super::determine_device;
720 use super::SupportedDevice;
721
722 use syn;
723
724
725 #[test]
726 fn determine_nitrokey_none() {
727 let input: syn::ItemFn = syn::parse_quote! {
728 #[nitrokey_test::test]
729 fn test_none() {}
730 };
731 let dev_type = determine_device(&input.sig.inputs);
732
733 assert_eq!(dev_type, None);
734 }
735
736 #[test]
737 fn determine_librem() {
738 let input: syn::ItemFn = syn::parse_quote! {
739 #[nitrokey_test::test]
740 fn test_librem(device: nitrokey::Librem) {}
741 };
742 let dev_type = determine_device(&input.sig.inputs);
743
744 assert_eq!(dev_type, Some((SupportedDevice::Librem, ArgumentType::Device)));
745 }
746
747 #[test]
748 fn determine_nitrokey_pro() {
749 let input: syn::ItemFn = syn::parse_quote! {
750 #[nitrokey_test::test]
751 fn test_pro(device: nitrokey::Pro) {}
752 };
753 let dev_type = determine_device(&input.sig.inputs);
754
755 assert_eq!(dev_type, Some((SupportedDevice::Pro, ArgumentType::Device)));
756 }
757
758 #[test]
759 fn determine_nitrokey_storage() {
760 let input: syn::ItemFn = syn::parse_quote! {
761 #[nitrokey_test::test]
762 fn test_storage(device: nitrokey::Storage) {}
763 };
764 let dev_type = determine_device(&input.sig.inputs);
765
766 assert_eq!(dev_type, Some((SupportedDevice::Storage, ArgumentType::Device)));
767 }
768
769 #[test]
770 fn determine_any_nitrokey() {
771 let input: syn::ItemFn = syn::parse_quote! {
772 #[nitrokey_test::test]
773 fn test_any(device: nitrokey::DeviceWrapper) {}
774 };
775 let dev_type = determine_device(&input.sig.inputs);
776
777 assert_eq!(dev_type, Some((SupportedDevice::Any, ArgumentType::DeviceWrapper)));
778 }
779
780 #[test]
781 #[should_panic(expected = "functions used as Nitrokey tests can only have zero or one argument")]
782 fn determine_wrong_argument_count() {
783 let input: syn::ItemFn = syn::parse_quote! {
784 #[nitrokey_test::test]
785 fn test_pro(device: nitrokey::Pro, _: i32) {}
786 };
787 let _ = determine_device(&input.sig.inputs);
788 }
789
790 #[test]
791 #[should_panic(expected = "unexpected function argument signature: & self")]
792 fn determine_wrong_function_type() {
793 let input: syn::ItemFn = syn::parse_quote! {
794 #[nitrokey_test::test]
795 fn test_self(&self) {}
796 };
797 let _ = determine_device(&input.sig.inputs);
798 }
799
800 #[test]
801 #[should_panic(expected = "unexpected function argument type: & nitrokey \
802 :: DeviceWrapper (expected owned object)")]
803 fn determine_wrong_argument_type() {
804 let input: syn::ItemFn = syn::parse_quote! {
805 #[nitrokey_test::test]
806 fn test_any(device: &nitrokey::DeviceWrapper) {}
807 };
808 let _ = determine_device(&input.sig.inputs);
809 }
810
811 #[test]
812 #[should_panic(expected = "unsupported function argument type: FooBarBaz")]
813 fn determine_invalid_argument_type() {
814 let input: syn::ItemFn = syn::parse_quote! {
815 #[nitrokey_test::test]
816 fn test_foobarbaz(device: nitrokey::FooBarBaz) {}
817 };
818 let _ = determine_device(&input.sig.inputs);
819 }
820}