ntest_test_cases-0.9.3/.cargo_vcs_info.json0000644000000001560000000000100144210ustar { "git": { "sha1": "5f025ba9cd586249c6d011d3c4580d12d551e533" }, "path_in_vcs": "ntest_test_cases" }ntest_test_cases-0.9.3/Cargo.toml0000644000000021320000000000100124130ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2018" name = "ntest_test_cases" version = "0.9.3" authors = ["Armin Becher "] description = "Test cases for ntest framework." documentation = "https://docs.rs/ntest" readme = "README.md" keywords = [ "test", "tests", "unit", "testing", "test-cases", ] categories = [ "development-tools", "development-tools::testing", ] license = "MIT" repository = "https://github.com/becheran/ntest" [lib] name = "ntest_test_cases" proc-macro = true [dependencies.proc-macro2] version = "1.0" [dependencies.quote] version = "1.0" [dependencies.syn] version = "1.0" features = ["full"] ntest_test_cases-0.9.3/Cargo.toml.orig000064400000000000000000000011150072674642500161240ustar 00000000000000[package] name = "ntest_test_cases" version = "0.9.3" authors = [ "Armin Becher ",] edition = "2018" description = "Test cases for ntest framework." keywords = [ "test", "tests", "unit", "testing", "test-cases",] categories = [ "development-tools", "development-tools::testing",] readme = "README.md" license = "MIT" repository = "https://github.com/becheran/ntest" documentation = "https://docs.rs/ntest" [lib] name = "ntest_test_cases" proc-macro = true [dependencies] quote = "1.0" proc-macro2 = "1.0" [dependencies.syn] version = "1.0" features = [ "full",] ntest_test_cases-0.9.3/LICENSE000064400000000000000000000000120072674642500142350ustar 00000000000000../LICENSEntest_test_cases-0.9.3/README.md000064400000000000000000000013630072674642500145210ustar 00000000000000# NTest TestCases Part of the [NTest library](https://crates.io/crates/ntest). Add test cases to the rust test framework using [procedural macros](https://doc.rust-lang.org/reference/procedural-macros.html). ## Examples Example with a single argument: ```rust #[test_case(13)] #[test_case(42)] fn one_arg(x: u32) { assert!(x == 13 || x == 42) } ``` The test cases above will be parsed at compile time and two rust test functions will be generated instead: ```rust #[test] fn one_arg_13() { x = 13; assert!(x == 13 || x == 42) } #[test] fn one_arg_42() { x = 42; assert!(x == 13 || x == 42) } ``` For more examples and information read the [documentation](https://docs.rs/ntest_test_cases/). ntest_test_cases-0.9.3/src/lib.rs000064400000000000000000000246460072674642500151560ustar 00000000000000//! Part of the ntest library. Add test cases to the rust test framework. extern crate proc_macro; extern crate syn; use proc_macro::TokenStream; use proc_macro2::Span; use quote::quote; use syn::parse_macro_input; mod syn_helper; /// Test cases can be used to have multiple inputs for a given function. /// With the `#[test_case]` attribute multiple tests will be generated using the /// [Procedural Macros](https://blog.rust-lang.org/2018/12/21/Procedural-Macros-in-Rust-2018.html) /// capabilities of rust. /// /// The function input can be of type `int`, `bool`, or `str`, or a path to an /// enum or constant of those types. /// /// Please note that rust functions can only contain alphanumeric characters and '_' signs. /// Special characters will be escaped using a meaning full replacement (for example `#` will be replaced with `_hash`), /// or as a default the '_' sign. /// /// A function annotated with a `#[test_case]` attribute will be split into multiple rust functions annotated with the `#[test]` attribute. /// /// # Examples /// /// Example with a single argument /// ```ignore /// #[test_case(13)] /// #[test_case(42)] /// fn one_arg(x: u32) { /// assert!(x == 13 || x == 42) /// } /// ``` /// /// The test cases above will be parsed at compile time and two rust test functions will be generated instead: /// ```ignore /// #[test] /// fn one_arg_13() { /// x = 13; /// assert!(x == 13 || x == 42) /// } /// /// #[test] /// fn one_arg_42() { /// x = 42; /// assert!(x == 13 || x == 42) /// } /// ``` /// /// Example with multiple arguments: /// ```ignore /// #[test_case(true, "true", 1)] /// fn test_mix(x: bool, y: &str, z: u16) { /// assert!(x); /// assert_eq!(y, "true"); /// assert_eq!(z, 1); /// } /// ``` /// /// Example with name attribute: /// ```ignore /// #[test_case(42, name="my_fancy_test")] /// fn with_name(x: u32) { /// assert_eq!(x, 42) /// } /// ``` /// /// Example with rust test attributes. /// All attributes after a test case will be appended after the generated `#[test]` attribute. /// For example the following test cases... /// /// ```ignore /// #[test_case(18)] /// #[ignore] /// #[test_case(15)] /// #[should_panic(expected = "I am panicing")] /// fn attributes_test_case(x: u32) { /// panic!("I am panicing"); /// } /// ``` /// /// ... will be compiled to these two tests. One gets ignored and the other succeeds: /// /// ```ignore /// #[test] /// #[ignore] /// fn attributes_test_case_18 { /// let x = 18; /// panic!("I am panicing"); /// } /// /// #[test] /// #[should_panic(expected = "I am panicing")] /// fn attributes_test_case_15() { /// let x = 15; /// panic!("I am panicing"); /// } /// ``` /// /// Test functions with a `Result` return are also supported: /// /// ```ignore /// #[test_case(27)] /// #[test_case(33)] /// fn returns_result(x: u32) -> Result<(), ()> { /// Ok(()) /// } /// ``` #[proc_macro_attribute] pub fn test_case(attr: TokenStream, item: TokenStream) -> TokenStream { let input = parse_macro_input!(item as syn::ItemFn); let attribute_args = parse_macro_input!(attr as syn::AttributeArgs); let test_descriptions: Vec = collect_test_descriptions(&input, &attribute_args); let fn_body = &input.block; let (fn_args_idents, fn_args_ty ) = collect_function_arg_idents(&input); let fn_return = &input.sig.output; let mut result = proc_macro2::TokenStream::new(); for test_description in test_descriptions { let test_case_name = syn::Ident::new(&test_description.name, Span::call_site()); let literals = test_description.args; let attributes = test_description.attributes; if literals.len() != fn_args_idents.len() { panic!("Test case arguments and function input signature mismatch."); } let test_case_quote = quote! { #[test] #[allow(non_snake_case)] #(#attributes)* fn #test_case_name() #fn_return { #(let #fn_args_idents: #fn_args_ty = #literals;)* #fn_body } }; result.extend(test_case_quote); } result.into() } fn collect_function_arg_idents(input: &syn::ItemFn) -> (Vec, Vec) { let mut fn_args_idents: Vec = vec![]; let mut fn_types: Vec = vec![]; let fn_args = &input.sig.inputs; for i in fn_args { match i { syn::FnArg::Typed(t) => { let ubox_t = *(t.pat.clone()); match ubox_t { syn::Pat::Ident(i) => { fn_args_idents.push(i.ident.clone()); } _ => panic!("Unexpected function identifier."), } fn_types.push(*t.ty.clone()); } syn::FnArg::Receiver(_) => { panic!("Receiver function not expected for test case attribute.") } } } (fn_args_idents, fn_types) } struct TestDescription { args: Vec, name: String, attributes: Vec, } fn collect_test_descriptions( input: &syn::ItemFn, attribute_args: &syn::AttributeArgs, ) -> Vec { let mut test_case_descriptions: Vec = vec![]; let fn_name = input.sig.ident.to_string(); let test_case_parameter = parse_test_case_attributes(attribute_args); let test_name = calculate_test_name(&test_case_parameter, &fn_name); let curr_test_attributes = TestDescription { args: test_case_parameter.args, name: test_name, attributes: vec![], }; test_case_descriptions.push(curr_test_attributes); for attribute in &input.attrs { let meta = attribute.parse_meta(); match meta { Ok(m) => match m { syn::Meta::Path(p) => { let identifier = p.get_ident().expect("Expected identifier!"); if identifier == "test_case" { panic!("Test case attributes need at least one argument such as #[test_case(42)]."); } else { test_case_descriptions .last_mut() .unwrap() .attributes .push(attribute.clone()); } } syn::Meta::List(ml) => { let identifier = ml.path.get_ident().expect("Expected identifier!"); if identifier == "test_case" { let argument_args: syn::AttributeArgs = ml.nested.into_iter().collect(); let test_case_parameter = parse_test_case_attributes(&argument_args); let test_name = calculate_test_name(&test_case_parameter, &fn_name); let curr_test_attributes = TestDescription { args: test_case_parameter.args, name: test_name, attributes: vec![], }; test_case_descriptions.push(curr_test_attributes); } else { test_case_descriptions .last_mut() .unwrap() .attributes .push(attribute.clone()); } } syn::Meta::NameValue(_) => { test_case_descriptions .last_mut() .unwrap() .attributes .push(attribute.clone()); } }, Err(e) => panic!("Could not determine meta data. Error {}.", e), } } test_case_descriptions } struct TestCaseAttributes { args: Vec, custom_name: Option, } fn parse_test_case_attributes(attr: &syn::AttributeArgs) -> TestCaseAttributes { let mut args: Vec = vec![]; let mut custom_name: Option = None; for a in attr { match a { syn::NestedMeta::Meta(m) => match m { syn::Meta::Path(path) => { args.push(syn::ExprPath { attrs: vec![], qself: None, path: path.clone() }.into()); } syn::Meta::List(_) => { panic!("Metalist not expected."); } syn::Meta::NameValue(nv) => { let identifier = nv.path.get_ident().expect("Expected identifier!"); if identifier == "test_name" || identifier == "name" { if custom_name.is_some() { panic!("Test name can only be defined once."); } match &nv.lit { syn::Lit::Str(_) => { custom_name = Some(syn_helper::lit_to_str(&nv.lit)); } _ => unimplemented!("Unexpected type for test name. Expected string."), } } else { panic!("Unexpected identifier '{}'", identifier) } } }, syn::NestedMeta::Lit(lit) => { args.push(syn::ExprLit { attrs: vec![], lit: lit.clone() }.into()); } } } TestCaseAttributes { args, custom_name, } } fn calculate_test_name(attr: &TestCaseAttributes, fn_name: &str) -> String { let mut name = "".to_string(); match &attr.custom_name { None => { name.push_str(fn_name); for expr in &attr.args { match expr { syn::Expr::Lit(lit) => name.push_str(&format!("_{}", syn_helper::lit_to_str(&lit.lit))), syn::Expr::Path(path) => name.push_str(&format!("_{}", path.path.segments.last().expect("Path to contain at least one segment").ident)), _ => unimplemented!("Unexpected expr type when calculating test name."), } } } Some(custom_name) => name = custom_name.to_string(), } name } ntest_test_cases-0.9.3/src/syn_helper.rs000064400000000000000000000046220072674642500165500ustar 00000000000000pub fn lit_to_str(lit: &syn::Lit) -> String { match lit { syn::Lit::Bool(s) => s.value.to_string(), syn::Lit::Str(s) => string_to_identifier(&s.value()), syn::Lit::Int(s) => number_to_identifier(s.base10_digits()), syn::Lit::Float(s) => number_to_identifier(s.base10_digits()), _ => unimplemented!("String conversion for literal. Only bool, str, positive int, and float values are supported."), } } fn number_to_identifier(num: &str) -> String { num.chars() .map(|x| match x { '.' => 'd', '0'..='9' => x, '-' => 'n', _ => panic!("This is not a valid number. Contains unknown sign {}", x), }) .collect() } fn string_to_identifier(num: &str) -> String { num.chars() .map(|x| match x { '0'..='9' => x.to_string(), 'a'..='z' => x.to_string(), 'A'..='Z' => x.to_string(), '!' => "_exclamation".to_string(), '"' => "_double_quote".to_string(), '#' => "_hash".to_string(), '$' => "_dollar".to_string(), '%' => "_percent".to_string(), '&' => "_ampercand".to_string(), '\'' => "_quote".to_string(), '(' => "_left_paranthesis".to_string(), ')' => "_right_paranthesis".to_string(), '*' => "_asterisk".to_string(), '+' => "_plus".to_string(), ',' => "_comma".to_string(), '-' => "_minus".to_string(), '.' => "_full_stop".to_string(), '/' => "_slash".to_string(), ':' => "_colon".to_string(), ';' => "_semicolon".to_string(), '<' => "_less_than".to_string(), '=' => "_equal".to_string(), '>' => "_greater_than".to_string(), '?' => "_questionmark".to_string(), '@' => "_at".to_string(), '[' => "_left_bracket".to_string(), '\\' => "_back_slash".to_string(), ']' => "_right_bracket".to_string(), '^' => "_caret".to_string(), '`' => "_backtick".to_string(), '{' => "_left_brace".to_string(), '|' => "_vertical_bar".to_string(), '}' => "_right_brace".to_string(), '~' => "_tilde".to_string(), _ => '_'.to_string(), }) .collect() } ntest_test_cases-0.9.3/tests/test_cases.rs000064400000000000000000000024220072674642500171040ustar 00000000000000extern crate ntest_test_cases; use ntest_test_cases::test_case; #[test_case(42)] fn one_arg(x: u32) { assert_eq!(x, 42) } #[test_case(1, 42)] #[test_case(9, 18)] #[test_case(5, 20)] fn two_args(x: u8, y: u32) { assert!(x < 10); assert!(y > 10); } #[test_case(42.42)] fn float(x: f64) { assert_eq!(x, 42.42) } #[test_case("walter", "white")] fn test_string(x: &str, y: &str) { assert_eq!(x, "walter"); assert_eq!(y, "white"); } #[test_case("-390)(#$*Q)")] fn test_string_special_chars(x: &str) { assert_eq!(x, "-390)(#$*Q)"); } #[test_case(true)] fn test_bool(x: bool) { assert!(x); } #[test_case(true, "true", 1)] fn test_mix(x: bool, y: &str, z: u16) { assert!(x); assert_eq!(y, "true"); assert_eq!(z, 1); } #[test_case(42, name="my_fancy_test")] fn with_name(x: u32) { assert_eq!(x, 42) } #[test_case(42, name="my_snd_fancy_testSPECIALCHARS^$(*")] fn with_name(x: u32) { assert_eq!(x, 42) } #[test_case(18)] #[ignore] #[test_case(15)] #[should_panic(expected = "I am panicing")] fn attributes_test_case(x: u32) { panic!("I am panicing {}", x); } #[test_case(42)] fn return_result(x: u32) -> core::result::Result<(), ()> { assert_eq!(x, 42); Ok(()) }