From a0a59b3a3f8a50abdaa618ff00394eeeeb8b9a0f Mon Sep 17 00:00:00 2001 From: Shon Feder Date: Sun, 21 Mar 2021 20:00:12 -0400 Subject: [PATCH] Move time proptest generators into their own crate (#829) * Move time proptest generators into their own crate Closes #828 This allows us to use the timestamp and DateTime generators in any other crate in our workspace. It should help advance #822 and #821. * Fix clippy warning * Correct documentation example * Update pbt-gen/src/time.rs Co-authored-by: Thane Thomson * Update pbt-gen/src/time.rs Co-authored-by: Thane Thomson * Fix typos in pbt-gen/Cargo.toml * Rename pbt-gen crate and guard modules with feature * Correct wording Co-authored-by: Thane Thomson --- Cargo.toml | 3 +- pbt-gen/Cargo.toml | 22 +++++ pbt-gen/README.md | 10 +++ pbt-gen/src/lib.rs | 19 +++++ pbt-gen/src/time.rs | 182 +++++++++++++++++++++++++++++++++++++++++ tendermint/Cargo.toml | 1 + tendermint/src/time.rs | 140 +------------------------------ 7 files changed, 240 insertions(+), 137 deletions(-) create mode 100644 pbt-gen/Cargo.toml create mode 100644 pbt-gen/README.md create mode 100644 pbt-gen/src/lib.rs create mode 100644 pbt-gen/src/time.rs diff --git a/Cargo.toml b/Cargo.toml index 9f6ff41bb..9963a6afe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,8 @@ members = [ "proto", "rpc", "tendermint", - "testgen" + "testgen", + "pbt-gen" ] exclude = [ diff --git a/pbt-gen/Cargo.toml b/pbt-gen/Cargo.toml new file mode 100644 index 000000000..d1c0e99c2 --- /dev/null +++ b/pbt-gen/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "tendermint-pbt-gen" +version = "0.1.0" +authors = ["Shon Feder "] +edition = "2018" +description = """ + An internal crate providing proptest generators used across our + crates and not depending on any code internal to those crates. + """ + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[features] + +default = ["time"] + +time = ["chrono"] + +[dependencies] + +chrono = { version = "0.4", features = ["serde"], optional = true} +proptest = "0.10.1" diff --git a/pbt-gen/README.md b/pbt-gen/README.md new file mode 100644 index 000000000..feca82af4 --- /dev/null +++ b/pbt-gen/README.md @@ -0,0 +1,10 @@ +# PBT Gen + +This internal crate provides [proptest](https://github.com/AltSysrq/proptest) +strategies and other PBT utilities used for testing. + +To view the documentation (which includes examples) run + +```sh +cargo doc --open -p tendermint-pbt-gen +``` diff --git a/pbt-gen/src/lib.rs b/pbt-gen/src/lib.rs new file mode 100644 index 000000000..9815eef46 --- /dev/null +++ b/pbt-gen/src/lib.rs @@ -0,0 +1,19 @@ +//! This internal crate provides [proptest](https://github.com/AltSysrq/proptest) +//! strategies and other PBT utilities used for testing. +//! +//! Conditions for inclusion in this crate are: +//! +//! 1. The utilities are relatively general. +//! 2. The utilities don't rely on any code internal to the other crates of +//! this repository. +//! +//! The each module of this crate (and the module's dependencies) are guarded by +//! a feature, documented along with the module. +//! +//! The default features are: +//! +//! - "time" + +/// Enabled with the "time" feature: +#[cfg(feature = "time")] +pub mod time; diff --git a/pbt-gen/src/time.rs b/pbt-gen/src/time.rs new file mode 100644 index 000000000..25b76e855 --- /dev/null +++ b/pbt-gen/src/time.rs @@ -0,0 +1,182 @@ +//! Provides [proptest](https://github.com/AltSysrq/proptest) generators for +//! time-like objects. + +use std::convert::TryInto; + +use chrono::{DateTime, NaiveDate, TimeZone, Timelike, Utc}; +use proptest::prelude::*; + +/// Any higher, and we're at seconds +pub const MAX_NANO_SECS: u32 = 999_999_999u32; + +/// The most distant time in the past for which chrono produces correct +/// times from [Utc.timestamp](chrono::Utc.timestamp). +/// +/// See . +/// +/// ``` +/// use pbt_gen; +/// +/// assert_eq!(pbt_gen::time::min_time().to_string(), "1653-02-10 06:13:21 UTC".to_string()); +/// ``` +pub fn min_time() -> DateTime { + Utc.timestamp(-9999999999, 0) +} + +/// The most distant time in the future for which chrono produces correct +/// times from [Utc.timestamp](chrono::Utc.timestamp). +/// +/// See . +/// +/// ``` +/// use pbt_gen; +/// +/// assert_eq!(pbt_gen::time::max_time().to_string(), "5138-11-16 09:46:39 UTC".to_string()); +/// ``` +pub fn max_time() -> DateTime { + Utc.timestamp(99999999999, 0) +} + +fn num_days_in_month(year: i32, month: u32) -> u32 { + // Using chrono, we get the duration beteween this month and the next, + // then count the number of days in that duration. See + // https://stackoverflow.com/a/58188385/1187277 + let given_month = NaiveDate::from_ymd(year, month, 1); + let next_month = NaiveDate::from_ymd( + if month == 12 { year + 1 } else { year }, + if month == 12 { 1 } else { month + 1 }, + 1, + ); + next_month + .signed_duration_since(given_month) + .num_days() + .try_into() + .unwrap() +} + +prop_compose! { + /// An abitrary [chrono::DateTime] that is between the given `min` + /// and `max`. + /// + /// # Examples + /// + /// ``` + /// use chrono::{TimeZone, Utc}; + /// use pbt_gen; + /// use proptest::prelude::*; + /// + /// proptest!{ + /// fn rosa_luxemburg_and_octavia_butler_were_not_alive_at_the_same_time( + /// time_in_luxemburgs_lifespan in pbt_gen::time::arb_datetime_in_range( + /// Utc.ymd(1871, 3, 5).and_hms(0,0,0), // DOB + /// Utc.ymd(1919, 1, 15).and_hms(0,0,0), // DOD + /// ), + /// time_in_butlers_lifespan in pbt_gen::time::arb_datetime_in_range( + /// Utc.ymd(1947, 6, 22).and_hms(0,0,0), // DOB + /// Utc.ymd(2006, 2, 24).and_hms(0,0,0), // DOD + /// ), + /// ) { + /// prop_assert!(time_in_luxemburgs_lifespan != time_in_butlers_lifespan) + /// } + /// } + /// ``` + pub fn arb_datetime_in_range(min: DateTime, max: DateTime)( + secs in min.timestamp()..max.timestamp() + )( + // min mano secods is only relevant if we happen to hit the minimum + // seconds on the nose. + nano in (if secs == min.timestamp() { min.nanosecond() } else { 0 })..MAX_NANO_SECS, + // Make secs in scope + secs in Just(secs), + ) -> DateTime { + println!(">> Secs {:?}", secs); + Utc.timestamp(secs, nano) + } +} + +prop_compose! { + /// An abitrary [chrono::DateTime] (between [min_time] and [max_time]). + pub fn arb_datetime() + ( + d in arb_datetime_in_range(min_time(), max_time()) + ) -> DateTime { + d + } +} + +// The following components of the timestamp follow +// Section 5.6 of RFC3339: https://tools.ietf.org/html/rfc3339#ref-ABNF. + +prop_compose! { + // See https://tools.ietf.org/html/rfc3339#appendix-A + fn arb_rfc339_time_offset()( + sign in "[+-]", + hour in 0..23u8, + min in 0..59u8, + ) -> String { + format!("{:}{:0>2}:{:0>2}", sign, hour, min) + } +} + +fn arb_rfc3339_offset() -> impl Strategy { + prop_oneof![arb_rfc339_time_offset(), Just("Z".to_owned())] +} + +prop_compose! { + fn arb_rfc3339_partial_time()( + hour in 0..23u8, + min in 0..59u8, + sec in 0..59u8, + secfrac in proptest::option::of(0..u64::MAX), + ) -> String { + let frac = match secfrac { + None => "".to_owned(), + Some(frac) => format!(".{:}", frac) + }; + format!("{:0>2}:{:0>2}:{:0>2}{:}", hour, min, sec, frac) + } +} + +prop_compose! { + fn arb_rfc3339_full_time()( + time in arb_rfc3339_partial_time(), + offset in arb_rfc3339_offset() + ) -> String { + format!("{:}{:}", time, offset) + } +} + +prop_compose! { + fn arb_rfc3339_day_of_year_and_month(year: i32, month: u32) + ( + d in 1..num_days_in_month(year, month) + ) -> u32 { + d + } +} + +prop_compose! { + fn arb_rfc3339_full_date()(year in 0..9999i32, month in 1..12u32) + ( + day in arb_rfc3339_day_of_year_and_month(year, month), + year in Just(year), + month in Just(month), + ) -> String { + format!("{:0>4}-{:0>2}-{:0>2}", year, month, day) + } +} + +prop_compose! { + /// An aribtrary RFC3339 timestamp + /// + /// For example: `1985-04-12T23:20:50.52Z` + /// + /// The implementaiton follows + /// [Section 5.6 of RFC3339](https://tools.ietf.org/html/rfc3339#ref-ABNF) + pub fn arb_rfc3339_timestamp()( + date in arb_rfc3339_full_date(), + time in arb_rfc3339_full_time() + ) -> String { + format!("{:}T{:}", date, time) + } +} diff --git a/tendermint/Cargo.toml b/tendermint/Cargo.toml index 1eb6fd84e..f1fda670d 100644 --- a/tendermint/Cargo.toml +++ b/tendermint/Cargo.toml @@ -68,3 +68,4 @@ secp256k1 = ["k256", "ripemd160"] [dev-dependencies] proptest = "0.10.1" +tendermint-pbt-gen = { path = "../pbt-gen" } diff --git a/tendermint/src/time.rs b/tendermint/src/time.rs index 43e7c2aed..fbd1ef4d9 100644 --- a/tendermint/src/time.rs +++ b/tendermint/src/time.rs @@ -144,141 +144,9 @@ pub trait ParseTimestamp { #[cfg(test)] mod tests { - use std::convert::TryInto; - use super::*; - - // TODO(shon) Extract arbitrary generators into own library - - use chrono::{DateTime, NaiveDate, TimeZone, Timelike, Utc}; use proptest::{prelude::*, sample::select}; - - // Any higher, and we're at seconds - const MAX_NANO_SECS: u32 = 999_999_999u32; - - // With values larger or smaller then these, chrono produces invalid rfc3339 - // timestamps. See https://github.com/chronotope/chrono/issues/537 - fn min_time() -> DateTime { - Utc.timestamp(-9999999999, 0) - } - - fn max_time() -> DateTime { - Utc.timestamp(99999999999, 0) - } - - fn num_days_in_month(year: i32, month: u32) -> u32 { - // Using chrono, we get the duration beteween this month and the next, - // then count the number of days in that duration. See - // https://stackoverflow.com/a/58188385/1187277 - let given_month = NaiveDate::from_ymd(year, month, 1); - let next_month = NaiveDate::from_ymd( - if month == 12 { year + 1 } else { year }, - if month == 12 { 1 } else { month + 1 }, - 1, - ); - next_month - .signed_duration_since(given_month) - .num_days() - .try_into() - .unwrap() - } - - prop_compose! { - /// An abitrary `chrono::DateTime` that is between `min` and `max` - /// DateTimes. - fn arb_datetime_in_range(min: DateTime, max: DateTime)( - secs in min.timestamp()..max.timestamp() - )( - // min mano secods is only relevant if we happen to hit the minimum - // seconds on the nose. - nano in (if secs == min.timestamp() { min.nanosecond() } else { 0 })..MAX_NANO_SECS, - // Make secs in scope - secs in Just(secs), - ) -> DateTime { - println!(">> Secs {:?}", secs); - Utc.timestamp(secs, nano) - } - } - - prop_compose! { - /// An abitrary `chrono::DateTime` - fn arb_datetime() - ( - d in arb_datetime_in_range(min_time(), max_time()) - ) -> DateTime { - d - } - } - - prop_compose! { - fn arb_rfc339_time_offset()( - sign in "[+-]", - hour in 0..23u8, - min in 0..59u8, - ) -> String { - format!("{:}{:0>2}:{:0>2}", sign, hour, min) - } - } - - fn arb_rfc3339_offset() -> impl Strategy { - prop_oneof![arb_rfc339_time_offset(), Just("Z".to_owned())] - } - - prop_compose! { - fn arb_rfc3339_partial_time()( - hour in 0..23u8, - min in 0..59u8, - sec in 0..59u8, - secfrac in proptest::option::of(0..u64::MAX), - ) -> String { - let frac = match secfrac { - None => "".to_owned(), - Some(frac) => format!(".{:}", frac) - }; - format!("{:0>2}:{:0>2}:{:0>2}{:}", hour, min, sec, frac) - } - } - - prop_compose! { - fn arb_rfc3339_full_time()( - time in arb_rfc3339_partial_time(), - offset in arb_rfc3339_offset() - ) -> String { - format!("{:}{:}", time, offset) - } - } - - prop_compose! { - fn arb_rfc3339_day_of_year_and_month(year: i32, month: u32) - ( - d in 1..num_days_in_month(year, month) - ) -> u32 { - d - } - } - - prop_compose! { - fn arb_rfc3339_full_date()(year in 0..9999i32, month in 1..12u32) - ( - day in arb_rfc3339_day_of_year_and_month(year, month), - year in Just(year), - month in Just(month), - ) -> String { - format!("{:0>4}-{:0>2}-{:0>2}", year, month, day) - } - } - - prop_compose! { - /// An aribtrary rfc3339 timestamp - /// - /// Follows https://tools.ietf.org/html/rfc3339#section-5.6 - fn arb_rfc3339_timestamp()( - date in arb_rfc3339_full_date(), - time in arb_rfc3339_full_time() - ) -> String { - format!("{:}T{:}", date, time) - } - } + use tendermint_pbt_gen as pbt; // We want to make sure that these timestamps specifically get tested. fn particular_rfc3339_timestamps() -> impl Strategy { @@ -309,13 +177,13 @@ mod tests { proptest! { #[test] - fn can_parse_rfc3339_timestamps(stamp in arb_rfc3339_timestamp()) { + fn can_parse_rfc3339_timestamps(stamp in pbt::time::arb_rfc3339_timestamp()) { prop_assert!(stamp.parse::