nitrokey_test/
lib.rs

1// lib.rs
2
3// *************************************************************************
4// * Copyright (C) 2019-2021 Daniel Mueller (deso@posteo.net)              *
5// *                                                                       *
6// * This program is free software: you can redistribute it and/or modify  *
7// * it under the terms of the GNU General Public License as published by  *
8// * the Free Software Foundation, either version 3 of the License, or     *
9// * (at your option) any later version.                                   *
10// *                                                                       *
11// * This program is distributed in the hope that it will be useful,       *
12// * but WITHOUT ANY WARRANTY; without even the implied warranty of        *
13// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
14// * GNU General Public License for more details.                          *
15// *                                                                       *
16// * You should have received a copy of the GNU General Public License     *
17// * along with this program.  If not, see <http://www.gnu.org/licenses/>. *
18// *************************************************************************
19
20#![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
63//! A crate providing supporting testing infrastructure for the
64//! `nitrokey` crate and its users.
65//!
66//! The crate simplifies test creation by providing an attribute macro
67//! that generates code for running a test on up to three devices (
68//! Nitrokey Pro, Nitrokey Storage, and Librem Key), takes care of
69//! serializing all tests tagged with this attribute, and causes them to
70//! be skipped if the respective device is not present.
71//!
72//! It also provides support for running tests belonging to a certain
73//! group. There are four groups: "nodev" (representing tests that run
74//! when no device is present), "librem" (comprised of all tests that
75//! can run on the Librem Key), "pro" (encompassing tests eligible to
76//! run on the Nitrokey Pro), and "storage" (for tests running against a
77//! Nitrokey Storage device).
78//! Running tests of a specific group (and only those) can be
79//! accomplished by setting the `NITROKEY_TEST_GROUP` environment
80//! variable to the group of interest. Note that in this mode tests will
81//! fail if the respective device is not present.
82//!
83//! Right now we make a few simplifying assumptions that, although not
84//! changing what can be expressed and tested, can lead to unexpected
85//! error messages when not known:
86//! - the parameter has to be an owned object, not a reference
87//! - parameter types are pattern matched against "Storage", "Pro", and
88//!   "DeviceWrapper"; that means `use ... as` declarations will not work
89//!   properly
90
91use 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
101/// The name of an optional environment variable we honor that can be
102/// set to one of the supported groups and will cause only tests of this
103/// particular group to be run.
104const NITROKEY_TEST_GROUP: &str = "NITROKEY_TEST_GROUP";
105/// The name of the group containing tests that run when no device is
106/// present.
107const NITROKEY_GROUP_NODEV: &str = "nodev";
108/// The name of the group containing tests that run when the Librem Key
109/// is present.
110const NITROKEY_GROUP_LIBREM: &str = "librem";
111/// The name of the group containing tests that run when the Nitrokey
112/// Pro is present.
113const NITROKEY_GROUP_PRO: &str = "pro";
114/// The name of the group containing tests that run when the Nitrokey
115/// Storage is present.
116const NITROKEY_GROUP_STORAGE: &str = "storage";
117
118
119/// The kind of argument a test function accepts.
120#[derive(Clone, Copy, Debug, PartialEq, Eq)]
121enum ArgumentType {
122  /// Pass an actual device handle to the test function.
123  Device,
124  /// Pass in a device wrapper to the test function.
125  DeviceWrapper,
126  /// Pass a `Model` object to the test function.
127  Model,
128}
129
130/// A type used to determine what Nitrokey device to test on.
131#[derive(Clone, Copy, Debug, PartialEq, Eq)]
132enum SupportedDevice {
133  /// Only the Librem Key is supported.
134  Librem,
135  /// Only the Nitrokey Pro is supported.
136  Pro,
137  /// Only the Nitrokey Storage is supported.
138  Storage,
139  /// Both the Nitrokey Pro and Storage are supported.
140  Any,
141}
142
143/// A type used for "filtering" what device types to emit test code for.
144#[derive(Clone, Copy, Debug, PartialEq, Eq)]
145enum Filter {
146  /// Only emit tests for a Librem Key.
147  Librem,
148  /// Only emit tests for a Nitrokey Pro.
149  Pro,
150  /// Only emit tests for a Nitrokey Storage.
151  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
167/// Apply a filter to a `SupportedDevice`.
168///
169/// Filtering basically produces the following outcomes:
170/// #[test]          fn foo();                      -> no device
171/// #[test(librem)]  fn foo();                      -> librem
172/// #[test(pro)]     fn foo();                      -> pro
173/// #[test(storage)] fn foo();                      -> storage
174///
175/// #[test]          fn foo(device: Librem);        -> librem
176/// #[test(librem)]  fn foo(device: Librem);        -> librem
177/// #[test(pro)]     fn foo(device: Librem);        -> error
178/// #[test(storage)] fn foo(device: Librem);        -> error
179///
180/// #[test]          fn foo(device: Pro);           -> pro
181/// #[test(librem)]  fn foo(device: Pro);           -> error
182/// #[test(pro)]     fn foo(device: Pro);           -> pro
183/// #[test(storage)] fn foo(device: Pro);           -> error
184///
185/// #[test]          fn foo(device: Storage);       -> storage
186/// #[test(librem)]  fn foo(device: Storage);       -> error
187/// #[test(pro)]     fn foo(device: Storage);       -> error
188/// #[test(storage)] fn foo(device: Storage);       -> storage
189///
190/// #[test]          fn foo(device: DeviceWrapper); -> any
191/// #[test(librem)]  fn foo(device: DeviceWrapper); -> librem
192/// #[test(pro)]     fn foo(device: DeviceWrapper); -> pro
193/// #[test(storage)] fn foo(device: DeviceWrapper); -> storage
194///
195/// #[test]          fn foo(model: Model);          -> any
196/// #[test(librem)]  fn foo(model: Model);          -> librem
197/// #[test(pro)]     fn foo(model: Model);          -> pro
198/// #[test(storage)] fn foo(model: Model);          -> storage
199fn 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      // As can be seen from the table above, we have some logic in here
208      // that is no longer strictly a filter, but rather an addition.
209      // That is done mostly for the user's convenience.
210      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/// The group a particular device belongs to.
243#[derive(Clone, Copy, Debug)]
244enum DeviceGroup {
245  /// The group encompassing all tests that require no device to be
246  /// present.
247  No,
248  /// The group containing all tests for the Librem Key.
249  Librem,
250  /// The group containing all tests for the Nitrokey Pro.
251  Pro,
252  /// The group containing all tests for the Nitrokey Storage.
253  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/// A procedural macro for the `test` attribute.
289///
290/// The attribute can be used to define a test that accepts a Nitrokey
291/// device object (which can be any of `nitrokey::Pro`,
292/// `nitrokey::Storage`, or `nitrokey::DeviceWrapper`), and runs a test
293/// against that device. If the device type was specified as
294/// `nitrokey::DeviceWrapper`, the test will actually be invoked for a
295/// Nitrokey Pro as well as a Nitrokey Storage. Irrespective, the test
296/// is skipped if the device cannot be found.
297/// It also supports running tests when no device is present, which is
298/// required for tasks such as handling of error conditions. The test
299/// function must not accept a device object in that case (i.e., have no
300/// parameters).
301///
302/// # Example
303///
304/// Test functionality on an arbitrary Nitrokey device (i.e., Pro or
305/// Storage):
306/// ```rust,no_run
307/// // Note that no test would actually run, regardless of `no_run`,
308/// // because we do not invoke the function.
309/// #[nitrokey_test::test]
310/// fn some_nitrokey_test(device: nitrokey::DeviceWrapper) {
311///   assert_eq!(device.get_serial_number().unwrap().len(), 8);
312/// }
313/// ```
314///
315/// Test functionality on any Nitrokey device, but leave the device
316/// connection to the user and just provide the model:
317/// ```rust,no_run
318/// #[nitrokey_test::test]
319/// fn some_other_nitrokey_test(model: nitrokey::Model) {
320///   // Connect to a device of the provided model.
321/// }
322/// ```
323///
324/// Test functionality on a Nitrokey Pro device:
325/// ```rust,no_run
326/// #[nitrokey_test::test]
327/// fn some_pro_test(device: nitrokey::Pro) {
328///   assert_eq!(device.get_model(), nitrokey::Model::Pro);
329/// }
330/// ```
331///
332/// Test functionality on a Nitrokey Pro device, but leave the device
333/// connection to the user:
334/// ```rust,no_run
335/// #[nitrokey_test::test(pro)]
336/// fn some_other_pro_test() {
337///   // Do something on a Pro device.
338/// }
339/// ```
340///
341/// A model can be provided optionally, like so:
342/// ```rust,no_run
343/// #[nitrokey_test::test(pro)]
344/// fn some_other_pro_test(model: nitrokey::Model) {
345///   assert_eq!(model, nitrokey::Model::Pro);
346/// }
347/// ```
348///
349/// Test functionality on a Nitrokey Storage device:
350/// ```rust,no_run
351/// #[nitrokey_test::test]
352/// fn some_storage_test(device: nitrokey::Storage) {
353///   assert_eq!(device.get_model(), nitrokey::Model::Storage);
354/// }
355/// ```
356///
357/// Test functionality when no device is present:
358/// ```rust,no_run
359/// #[nitrokey_test::test]
360/// fn no_device() {
361///   assert!(nitrokey::connect().is_err());
362/// }
363/// ```
364#[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  // Make clippy happy.
376  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      // Emit a test for all supported devices.
403      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    // The only two valid return types for a test function are no return
417    // value or a Result. We assume a Result<V, E> in this path. Note
418    // that we furthermore assume that V=(). Once the trait
419    // std::process::Termination stabilized we should be able to do away
420    // with the latter assumption.
421    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          // Check if we can connect. Skip the test if we can't due to the
477          // device not being present.
478          let result = #connect;
479          if #skip {
480            // Note that tests have a "special" stdout, used by
481            // println!() for example, that has a thread-local buffer
482            // and is not actually printed by default but only when the
483            // --nocapture option is present. Alternatively, they can
484            // use std::io::stdout directly, which will always appear.
485            // Unfortunately, neither works properly in concurrent
486            // contexts. That is, output can always be interleaved
487            // randomly. Note that we do serialize tests, but there will
488            // always be a window for races, because we have to release
489            // the mutex before the "outer" test infrastructure prints
490            // the result.
491            // For that matter, we use the thread local version to print
492            // information about whether a test is skipped. This way,
493            // the user can get this information but given that it
494            // likely is somewhat garbled we do not want it to manifest
495            // by default. This is really a short coming of the Rust
496            // testing infrastructure and there is nothing we can do
497            // about that. It is a surprise, though, that even the
498            // thread-locally buffered version has this problem.
499            ::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  // Based on the device we want to emit a test function for we recreate
518  // the expected full path of the type. That is necessary because
519  // client code may have a "use" and may just contain a `Pro`, for
520  // example, while we really need to work with the absolute path.
521  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      // Make sure that if no device is passed in the user is still
602      // allowed to use nitrokey::take successfully by not keeping a
603      // Manager object lying around. We just need it to check whether or
604      // not to skip the test.
605      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
624/// Emit code for a wrapper function around a Nitrokey test function.
625fn 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  // Note that we need to rely on proc_macro2 here, because while the
635  // compiler provided proc_macro has `Ident` and `Span` types, they
636  // cannot be interpolated with quote!{} for lack of quote::ToTokens
637  // implementations.
638  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      // Note that mutexes (and other locks) come with a poisoning
659      // mechanism that (by default) prevents an acquisition of a mutex
660      // that was held while the thread holding it panic'ed. We don't
661      // care about that protection. There are no real invariants that
662      // our mutex is protecting, it just synchronizes accesses to the
663      // nitrokey device. As such, just override the protection.
664      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
700/// Determine the kind of Nitrokey device a test function support, based
701/// on the type of its only parameter.
702fn 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}