diff --git a/Cargo.toml b/Cargo.toml index 12c77fe..36154ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "crates/scion", "crates/scion-grpc", "crates/scion-proto", + "crates/test-utils", ] [workspace.dependencies] diff --git a/crates/scion-proto/Cargo.toml b/crates/scion-proto/Cargo.toml index f1f7442..ffe3032 100644 --- a/crates/scion-proto/Cargo.toml +++ b/crates/scion-proto/Cargo.toml @@ -19,6 +19,7 @@ criterion = "0.5.1" prost-types = { workspace = true } rand = "0.8.5" rand_xorshift = "0.3.0" +test-utils = { version = "0.1.0", path = "../test-utils" } [[bench]] name = "checksum" diff --git a/crates/scion-proto/src/address/asn.rs b/crates/scion-proto/src/address/asn.rs index 9a4fc5e..d3efc22 100644 --- a/crates/scion-proto/src/address/asn.rs +++ b/crates/scion-proto/src/address/asn.rs @@ -148,92 +148,66 @@ impl FromStr for Asn { #[cfg(test)] mod tests { - use super::*; - - mod conversion { - use super::*; - - macro_rules! test_success { - ($name:ident, $number:expr, $asn:expr) => { - #[test] - fn $name() { - assert_eq!(Asn::try_from($number).unwrap(), $asn); - assert_eq!(u64::from($asn), $number); - } - }; - } + use test_utils::param_test; - test_success!(wildcard, 0, Asn::WILDCARD); - test_success!(max_value, 0xffff_ffff_ffff, Asn::MAX); + use super::*; - #[test] - fn out_of_range() { - assert_eq!( - Asn::try_from(Asn::MAX.to_u64() + 1).unwrap_err(), - AddressParseError(AddressKind::Asn) - ); - } + param_test! { + converts_from_number: [ + wildcard: (0, Ok(Asn::WILDCARD)), + max_value: (0xffff_ffff_ffff, Ok(Asn::MAX)), + out_of_range: (0xffff_ffff_ffff + 1, Err(AddressParseError(AddressKind::Asn))) + ] } - - mod parse { - use super::*; - - macro_rules! test_success { - ($name:ident, $input:expr, $expected:expr) => { - #[test] - fn $name() { - assert_eq!(Asn::from_str($input).unwrap(), $expected); - } - }; - } - - test_success!(zero, "0", Asn::WILDCARD); - test_success!(zero_with_colon, "0:0:0", Asn::WILDCARD); - test_success!(low_bit, "0:0:1", Asn(1)); - test_success!(high_bit, "1:0:0", Asn(0x000100000000)); - test_success!(max, "ffff:ffff:ffff", Asn::MAX); - test_success!(bgp_asn, "65535", Asn(65535)); - - macro_rules! test_error { - ($name:ident, $input:expr) => { - #[test] - fn $name() { - assert_eq!( - Asn::from_str($input).unwrap_err(), - AddressParseError(AddressKind::Asn) - ); - } - }; - } - - test_error!(errs_large_decimal_format, "65536"); - test_error!(errs_on_only_colon, ":"); - test_error!(errs_extra_colon, "0:0:0:"); - test_error!(errs_too_few, "0:0"); - test_error!(errs_invalid_part, ":0:0"); - test_error!(errs_out_of_range, "10000:0:0"); - test_error!(errs_out_of_range2, "0:0:10000"); - test_error!(errs_invalid_format, "0:0x0:0"); + fn converts_from_number(numeric_value: u64, expected: Result) { + assert_eq!(Asn::try_from(numeric_value), expected); } - mod display { - use super::*; + param_test! { + successfully_parses_valid_strings: [ + zero: ("0", Asn::WILDCARD), + zero_with_colon: ("0:0:0", Asn::WILDCARD), + low_bit: ("0:0:1", Asn(1)), + high_bit: ("1:0:0", Asn(0x000100000000)), + max: ("ffff:ffff:ffff", Asn::MAX), + bgp_asn: ("65535", Asn(65535)) + ] + } + fn successfully_parses_valid_strings(asn_str: &str, expected: Asn) { + assert_eq!(Ok(expected), asn_str.parse()); + } - macro_rules! test_display { - ($name:ident, $asn:expr, $expected:expr) => { - #[test] - fn $name() { - assert_eq!($asn.to_string(), $expected); - } - }; - } + param_test! { + parse_rejects_invalid_strings: [ + large_decimal_format: ("65536"), + only_colon: (":"), + extra_colon: ("0:0:0:"), + too_few: ("0:0"), + invalid_part: (":0:0"), + out_of_range: ("10000:0:0"), + out_of_range2: ("0:0:10000"), + invalid_format: ("0:0x0:0"), + ] + } + fn parse_rejects_invalid_strings(asn_str: &str) { + assert_eq!( + Asn::from_str(asn_str), + Err(AddressParseError(AddressKind::Asn)) + ); + } - test_display!(large, Asn(0xff00000000ab), "ff00:0:ab"); - test_display!(large_symmetric, Asn(0x0001fcd10001), "1:fcd1:1"); - test_display!(max, Asn::MAX, "ffff:ffff:ffff"); - test_display!(wildcard, Asn(0), "0"); - test_display!(bgp_asn, Asn(1), "1"); - test_display!(bgp_asn_max, Asn(65535), "65535"); - test_display!(outside_bgp_asn, Asn(65536), "0:1:0"); + param_test! { + correctly_displays_asn: [ + large: (Asn(0xff00000000ab), "ff00:0:ab"), + large_symmetric: (Asn(0x0001fcd10001), "1:fcd1:1"), + max: (Asn::MAX, "ffff:ffff:ffff"), + wildcard: (Asn(0), "0"), + bgp_asn: (Asn(1), "1"), + bgp_asn_max: (Asn(65535), "65535"), + outside_bgp_asn: (Asn(65536), "0:1:0"), + ] + } + fn correctly_displays_asn(asn: Asn, expected: &str) { + assert_eq!(asn.to_string(), expected); } } diff --git a/crates/scion-proto/src/address/ia.rs b/crates/scion-proto/src/address/ia.rs index 9b31fd1..591bd6d 100644 --- a/crates/scion-proto/src/address/ia.rs +++ b/crates/scion-proto/src/address/ia.rs @@ -162,138 +162,93 @@ impl From for IsdAsn { #[cfg(test)] mod tests { + use test_utils::param_test; + use super::*; use crate::address::{Asn, Isd}; - macro_rules! test_new_and_get { - ($name:ident, $ia:expr, $isd:expr, $asn:expr) => { - mod $name { - use super::*; - - #[test] - fn construct() { - assert_eq!($ia, IsdAsn::new($isd, $asn)); - } - - #[test] - fn get_isd() { - assert_eq!($isd, $ia.isd()); - } - - #[test] - fn get_asn() { - assert_eq!($asn, $ia.asn()); - } - } - }; + param_test! { + constructs_from_parts: [ + arbitrary: (Isd::new(1), Asn::new(0xff00_0000_00ab), IsdAsn(0x1_ff00_0000_00ab)), + wildcard: (Isd::WILDCARD, Asn::WILDCARD, IsdAsn::WILDCARD), + ] } - - test_new_and_get!(wildcard, IsdAsn(0), Isd::new(0), Asn::new(0)); - test_new_and_get!( - long, - IsdAsn(0x0001_ff00_0000_00ab), - Isd::new(1), - Asn::new(0xff00_0000_00ab) - ); - test_new_and_get!( - max_and_min, - IsdAsn(0xffff_0000_0000_0000), - Isd::new(0xffff), - Asn::new(0) - ); - test_new_and_get!( - min_and_max, - IsdAsn(0x0000_ffff_ffff_ffff), - Isd::new(0), - Asn::new(0xffff_ffff_ffff) - ); - - mod conversion { - use super::*; - - #[test] - fn as_u64() { - assert_eq!( - IsdAsn::new(Isd::new(0x0123), Asn::new(0x4567_89ab_cdef)).to_u64(), - 0x0123_4567_89ab_cdef - ) - } - - macro_rules! test_success { - ($name:ident, $number:expr, $ia:expr) => { - #[test] - fn $name() { - assert_eq!(IsdAsn::from($number), $ia); - assert_eq!(u64::from($ia), $number); - } - }; - } - - test_success!(wildcard, 0, IsdAsn::new(Isd::WILDCARD, Asn::WILDCARD)); - test_success!(max_value, -1_i64 as u64, IsdAsn(0xffff_ffff_ffff_ffff)); + fn constructs_from_parts(isd: Isd, asn: Asn, expected: IsdAsn) { + assert_eq!(IsdAsn::new(isd, asn), expected); } - mod display { - use super::*; + #[test] + fn gets_isd() { + assert_eq!(IsdAsn(0x2_ff00_0000_1101).isd(), Isd::new(2)); + } - #[test] - fn debug() { - assert_eq!( - format!("{:?}", IsdAsn(0x0001_ff00_0000_00ab)), - "IA(0x0001ff00000000ab)" - ); - } + #[test] + fn gets_asn() { + assert_eq!(IsdAsn(0x2_ff00_0000_1101).asn(), Asn::new(0xff00_0000_1101)); + } - #[test] - fn simple() { - assert_eq!(IsdAsn(0x0001_ff00_0000_00ab).to_string(), "1-ff00:0:ab"); - } + #[test] + fn to_u64() { + assert_eq!( + IsdAsn::new(Isd::new(0x0123), Asn::new(0x4567_89ab_cdef)).to_u64(), + 0x0123_4567_89ab_cdef + ) + } - #[test] - fn wildcard() { - assert_eq!(IsdAsn(0).to_string(), "0-0"); - } + #[test] + fn correctly_formats_debug_repr() { + assert_eq!( + format!("{:?}", IsdAsn(0x0001_ff00_0000_00ab)), + "IA(0x0001ff00000000ab)" + ); + } - #[test] - fn max_ia() { - assert_eq!( - IsdAsn(0xffff_ffff_ffff_ffff).to_string(), - "65535-ffff:ffff:ffff" - ); - } + param_test! { + correctly_displays_ia: [ + simple: (IsdAsn(0x0001_ff00_0000_00ab), "1-ff00:0:ab"), + wildcard: (IsdAsn::WILDCARD, "0-0"), + max_ia: (IsdAsn::MAX, "65535-ffff:ffff:ffff"), + ] + } + fn correctly_displays_ia(ia: IsdAsn, expected: &str) { + assert_eq!(ia.to_string(), expected); } - mod parse { - use super::*; - - macro_rules! test_success { - ($name:ident, $input:expr, $expected:expr) => { - #[test] - fn $name() { - assert_eq!(IsdAsn::from_str($input).unwrap(), $expected); - assert_eq!(IsdAsn::try_from($input.to_string()).unwrap(), $expected); - } - }; - } + param_test! { + from_str_parses_valid_strings: [ + wildcard: ("0-0", IsdAsn::WILDCARD), + max_ia: ("65535-ffff:ffff:ffff", IsdAsn::MAX), + min_non_wildcard: ("1-0:0:1", IsdAsn(0x0001_0000_0000_0001)), + ] + } + fn from_str_parses_valid_strings(ia_str: &str, expected: IsdAsn) { + assert_eq!(IsdAsn::from_str(ia_str), Ok(expected)); + } - test_success!(max, "65535-ffff:ffff:ffff", IsdAsn(0xffff_ffff_ffff_ffff)); - test_success!(wildcard, "0-0", IsdAsn::WILDCARD); - test_success!(min_non_wildcard, "1-0:0:1", IsdAsn(0x0001_0000_0000_0001)); + param_test! { + try_from_str_parses_valid_strings: [ + wildcard: ("0-0", IsdAsn::WILDCARD), + max_ia: ("65535-ffff:ffff:ffff", IsdAsn::MAX), + min_non_wildcard: ("1-0:0:1", IsdAsn(0x0001_0000_0000_0001)), + ] + } + fn try_from_str_parses_valid_strings(ia_str: &str, expected: IsdAsn) { + assert_eq!(IsdAsn::try_from(ia_str.to_string()), Ok(expected)); + } - #[test] - fn invalid() { - assert_eq!( - IsdAsn::from_str("a-0:0:1").unwrap_err(), - AddressParseError(AddressKind::IsdAsn) - ); - } + #[test] + fn invalid() { + assert_eq!( + IsdAsn::from_str("a-0:0:1").unwrap_err(), + AddressParseError(AddressKind::IsdAsn) + ); + } - #[test] - fn invalid_parts() { - assert_eq!( - IsdAsn::from_str("1-1-0:0:1").unwrap_err(), - AddressParseError(AddressKind::IsdAsn) - ); - } + #[test] + fn invalid_parts() { + assert_eq!( + IsdAsn::from_str("1-1-0:0:1").unwrap_err(), + AddressParseError(AddressKind::IsdAsn) + ); } } diff --git a/crates/test-utils/Cargo.toml b/crates/test-utils/Cargo.toml new file mode 100644 index 0000000..3252284 --- /dev/null +++ b/crates/test-utils/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "test-utils" +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" +publish = false + +[dependencies] + +[lints] +workspace = true + +[dev-dependencies] +tokio = { version = "1.35.1", features = ["test-util", "tokio-macros", "macros"] } diff --git a/crates/test-utils/src/lib.rs b/crates/test-utils/src/lib.rs new file mode 100644 index 0000000..c5bfdd0 --- /dev/null +++ b/crates/test-utils/src/lib.rs @@ -0,0 +1,175 @@ +//! Test utilities shared between various crates. + +/// Shortcut for `.parse().unwrap()`. +#[macro_export] +macro_rules! parse { + ($string:literal) => { + $string.parse().unwrap() + }; +} + +/// Macro for creating parametrized *synchronous* tests. +/// +/// The `param_test!` macro accepts the name of an existing function, followed by a list of case +/// names and their arguments. It expands to a module with a `#[test]` function for each of the +/// cases. Each test case calls the existing, named function with their provided arguments. +/// +/// See [`async_param_test`] for a similar macro that works with `async` function. +/// +/// # Examples +/// +/// Calling a simple test function can be done as follows +/// +/// ``` +/// # use test_utils::param_test; +/// # +/// param_test! { +/// test_sum: [ +/// positive_sums: (10, 7, 17), +/// negative_sums: (-5, -3, -8) +/// ] +/// } +/// fn test_sum(lhs: i32, rhs: i32, sum: i32) { +/// assert_eq!(lhs + rhs, sum); +/// } +/// ``` +/// +/// Additionally, test functions can also have return types, such as a [`Result`]: +/// +/// ``` +/// # use std::error::Error; +/// # use test_utils::param_test; +/// # +/// param_test! { +/// test_parses -> Result<(), Box>: [ +/// positive: ("21", 21), +/// negative: ("-17", -17) +/// ] +/// } +/// fn test_parses(to_parse: &str, expected: i32) -> Result<(), Box> { +/// assert_eq!(expected, to_parse.parse()?); +/// Ok(()) +/// } +/// ``` +/// +/// Finally, attributes such as as `#[ignore]` may be added to individual tests: +/// +/// ``` +/// # use std::error::Error; +/// # use test_utils::param_test; +/// # +/// param_test! { +/// test_parses -> Result<(), Box>: [ +/// #[ignore] positive: ("21", 21), +/// negative: ("-17", -17) +/// ] +/// } +/// fn test_parses(to_parse: &str, expected: i32) -> Result<(), Box> { +/// assert_eq!(expected, to_parse.parse()?); +/// Ok(()) +/// } +/// ``` +#[macro_export] +macro_rules! param_test { + ($func_name:ident -> $return_ty:ty: [ + $( $(#[$outer:meta])* $case_name:ident: ( $($args:expr),+ ) ),+$(,)? + ]) => { + mod $func_name { + use super::*; + + $( + #[test] + $(#[$outer])* + fn $case_name() -> $return_ty { + $func_name($($args),+) + } + )* + } + }; + ($func_name:ident: [ + $( $(#[$outer:meta])* $case_name:ident: ( $($args:expr),+ ) ),+$(,)? + ]) => { + param_test!($func_name -> (): [ $( $(#[$outer])* $case_name: ( $($args),+ ) ),+ ]); + }; +} + +/// Macro for creating parametrized *asynchronous* tests. +/// +/// This macro behaves similarly to the [`param_test`] macro, however it must be used with an +/// `async` function. For convenience, the macro expands the test cases with the `#[tokio::test]` +/// attribute. If specifying any additional attributes to any test case, it is necessary to +/// re-specify the `#[tokio::test]` macro for *every* test case. +/// +/// See [`param_test`] for more information and examples. +#[macro_export] +macro_rules! async_param_test { + ($func_name:ident -> $return_ty:ty: [ + $( $(#[$outer:meta])+ $case_name:ident: ( $($args:expr),+ ) ),+$(,)? + ]) => { + mod $func_name { + use super::*; + + $( + $(#[$outer])+ + async fn $case_name() -> $return_ty { + $func_name($($args),+).await + } + )* + } + }; + ($func_name:ident: [ + $( $(#[$outer:meta])+ $case_name:ident: ( $($args:expr),+ ) ),+$(,)? + ]) => { + async_param_test!( $func_name -> (): [ $( $(#[$outer])+ $case_name: ($($args),+) ),* ] ); + }; + + ($func_name:ident: [ + $( $case_name:ident: ( $($args:expr),+ ) ),+$(,)? + ]) => { + async_param_test!( $func_name -> (): [ $( #[tokio::test] $case_name: ($($args),+) ),* ] ); + }; + ($func_name:ident -> $return_ty:ty: [ + $( $case_name:ident: ( $($args:expr),+ ) ),+$(,)? + ]) => { + async_param_test!( + $func_name -> $return_ty: [ $( #[tokio::test] $case_name: ( $($args),+ ) ),* ] + ); + } +} + +#[cfg(test)] +mod tests { + use std::error::Error; + + param_test! { + test_with_no_return: [ + case1: (true, 1, 1), + case2: (false, 3, 4) + ] + } + fn test_with_no_return(bool_arg: bool, usize_arg: usize, u32_arg: u32) { + assert_eq!(bool_arg, usize_arg == u32_arg as usize); + } + + param_test! { + test_with_return -> Result<(), Box>: [ + case1: ("5", 5), + case2: ("7", 7) + ] + } + fn test_with_return(to_parse: &str, parsed: usize) -> Result<(), Box> { + assert_eq!(parsed, to_parse.parse()?); + Ok(()) + } + + async_param_test! { + async_test_with_return -> Result<(), Box>: [ + case1: ("5", 5), + case2: ("7", 7) + ] + } + async fn async_test_with_return(to_parse: &str, parsed: usize) -> Result<(), Box> { + assert_eq!(parsed, to_parse.parse()?); + Ok(()) + } +}