Revision control
Copy as Markdown
Other Tools
use num_conv::prelude::*;
use quickcheck::{Arbitrary, TestResult};
use quickcheck_macros::quickcheck;
use time::macros::{format_description, time};
use time::Weekday::*;
use time::{Date, Duration, Month, OffsetDateTime, PrimitiveDateTime, Time, UtcOffset, Weekday};
macro_rules! test_shrink {
($type:ty,
$fn_name:ident,
$($method:ident()).+
$(, min=$min_value:literal)?
) => {
#[quickcheck]
fn $fn_name(v: $type) -> TestResult {
let method_value = v.$($method()).+;
if method_value == test_shrink!(@min_or_zero $($min_value)?) {
TestResult::discard()
} else {
TestResult::from_bool(v.shrink().any(|shrunk|
if method_value > 0 {
shrunk.$($method()).+ < v.$($method()).+
} else {
shrunk.$($method()).+ > v.$($method()).+
}
))
}
}
};
(@min_or_zero) => { 0 };
(@min_or_zero $min:literal) => { $min };
}
macro_rules! no_panic {
($($x:tt)*) => {
std::panic::catch_unwind(|| {
$($x)*
})
.is_ok()
}
}
#[quickcheck]
fn date_yo_roundtrip(d: Date) -> bool {
Date::from_ordinal_date(d.year(), d.ordinal()) == Ok(d)
}
#[quickcheck]
fn date_ymd_roundtrip(d: Date) -> bool {
Date::from_calendar_date(d.year(), d.month(), d.day()) == Ok(d)
}
#[quickcheck]
fn date_ywd_roundtrip(d: Date) -> bool {
let (year, week, weekday) = d.to_iso_week_date();
Date::from_iso_week_date(year, week, weekday) == Ok(d)
}
#[quickcheck]
fn date_format_century_last_two_equivalent(d: Date) -> bool {
let split_format = format_description!("[year repr:century][year repr:last_two]-[month]-[day]");
let split = d.format(&split_format).expect("formatting failed");
let combined_format = format_description!("[year]-[month]-[day]");
let combined = d.format(&combined_format).expect("formatting failed");
split == combined
}
#[quickcheck]
fn date_parse_century_last_two_equivalent_extended(d: Date) -> TestResult {
// With the extended range, there is an ambiguity when parsing a year with fewer than six
// digits, as the first four are consumed by the century, leaving at most one for the last
// two digits.
if !matches!(d.year().unsigned_abs().to_string().len(), 6) {
return TestResult::discard();
}
let split_format = format_description!("[year repr:century][year repr:last_two]-[month]-[day]");
let combined_format = format_description!("[year]-[month]-[day]");
let combined = d.format(&combined_format).expect("formatting failed");
TestResult::from_bool(Date::parse(&combined, &split_format).expect("parsing failed") == d)
}
#[quickcheck]
fn date_parse_century_last_two_equivalent_standard(d: Date) -> TestResult {
// With the standard range, the year must be at most four digits.
if !matches!(d.year(), -9999..=9999) {
return TestResult::discard();
}
let split_format = format_description!(
"[year repr:century range:standard][year repr:last_two range:standard]-[month]-[day]"
);
let combined_format = format_description!("[year range:standard]-[month]-[day]");
let combined = d.format(&combined_format).expect("formatting failed");
TestResult::from_bool(Date::parse(&combined, &split_format).expect("parsing failed") == d)
}
#[quickcheck]
fn julian_day_roundtrip(d: Date) -> bool {
Date::from_julian_day(d.to_julian_day()) == Ok(d)
}
#[quickcheck]
fn duration_roundtrip(d: Duration) -> bool {
Duration::new(d.whole_seconds(), d.subsec_nanoseconds()) == d
}
#[quickcheck]
fn time_roundtrip(t: Time) -> bool {
Time::from_hms_nano(t.hour(), t.minute(), t.second(), t.nanosecond()) == Ok(t)
}
#[quickcheck]
fn primitive_date_time_roundtrip(a: PrimitiveDateTime) -> bool {
PrimitiveDateTime::new(a.date(), a.time()) == a
}
#[quickcheck]
fn utc_offset_roundtrip(o: UtcOffset) -> bool {
let (hours, minutes, seconds) = o.as_hms();
UtcOffset::from_hms(hours, minutes, seconds) == Ok(o)
}
#[quickcheck]
fn offset_date_time_roundtrip(a: OffsetDateTime) -> TestResult {
// Values near the edge of what is allowed may panic if the conversion brings the underlying
// value outside the valid range of values.
if a.date() == Date::MIN || a.date() == Date::MAX {
return TestResult::discard();
}
TestResult::from_bool(PrimitiveDateTime::new(a.date(), a.time()).assume_offset(a.offset()) == a)
}
#[quickcheck]
fn unix_timestamp_roundtrip(odt: OffsetDateTime) -> TestResult {
match odt.date() {
Date::MIN | Date::MAX => TestResult::discard(),
_ => TestResult::from_bool({
// nanoseconds are not stored in the basic Unix timestamp
let odt = odt - Duration::nanoseconds(odt.nanosecond().into());
OffsetDateTime::from_unix_timestamp(odt.unix_timestamp()) == Ok(odt)
}),
}
}
#[quickcheck]
fn unix_timestamp_nanos_roundtrip(odt: OffsetDateTime) -> TestResult {
match odt.date() {
Date::MIN | Date::MAX => TestResult::discard(),
_ => TestResult::from_bool(
OffsetDateTime::from_unix_timestamp_nanos(odt.unix_timestamp_nanos()) == Ok(odt),
),
}
}
#[quickcheck]
fn number_from_monday_roundtrip(w: Weekday) -> bool {
Monday.nth_next(w.number_from_monday() + 7 - 1) == w
}
#[quickcheck]
fn number_from_sunday_roundtrip(w: Weekday) -> bool {
Sunday.nth_next(w.number_from_sunday() + 7 - 1) == w
}
#[quickcheck]
fn number_days_from_monday_roundtrip(w: Weekday) -> bool {
Monday.nth_next(w.number_days_from_monday()) == w
}
#[quickcheck]
fn number_days_from_sunday_roundtrip(w: Weekday) -> bool {
Sunday.nth_next(w.number_days_from_sunday()) == w
}
#[quickcheck]
fn weekday_supports_arbitrary(w: Weekday) -> bool {
(1..=7).contains(&w.number_from_monday())
}
#[quickcheck]
fn weekday_can_shrink(w: Weekday) -> bool {
match w {
Monday => w.shrink().next().is_none(),
_ => w.shrink().next() == Some(w.previous()),
}
}
#[quickcheck]
fn month_supports_arbitrary(m: Month) -> bool {
(1..=12).contains(&u8::from(m))
}
#[quickcheck]
fn month_can_shrink(m: Month) -> bool {
match m {
Month::January => m.shrink().next().is_none(),
_ => m.shrink().next() == Some(m.previous()),
}
}
#[quickcheck]
fn date_replace_year(date: Date, year: i32) -> bool {
date.replace_year(year) == Date::from_calendar_date(year, date.month(), date.day())
}
#[quickcheck]
fn date_replace_month(date: Date, month: Month) -> bool {
date.replace_month(month) == Date::from_calendar_date(date.year(), month, date.day())
}
#[quickcheck]
fn date_replace_day(date: Date, day: u8) -> bool {
date.replace_day(day) == Date::from_calendar_date(date.year(), date.month(), day)
}
#[quickcheck]
fn time_replace_hour(time: Time, hour: u8) -> bool {
time.replace_hour(hour)
== Time::from_hms_nano(hour, time.minute(), time.second(), time.nanosecond())
}
#[quickcheck]
fn pdt_replace_year(pdt: PrimitiveDateTime, year: i32) -> bool {
pdt.replace_year(year)
== Date::from_calendar_date(year, pdt.month(), pdt.day())
.map(|date| date.with_time(pdt.time()))
}
#[quickcheck]
fn pdt_replace_month(pdt: PrimitiveDateTime, month: Month) -> bool {
pdt.replace_month(month)
== Date::from_calendar_date(pdt.year(), month, pdt.day())
.map(|date| date.with_time(pdt.time()))
}
#[quickcheck]
fn pdt_replace_day(pdt: PrimitiveDateTime, day: u8) -> bool {
pdt.replace_day(day)
== Date::from_calendar_date(pdt.year(), pdt.month(), day)
.map(|date| date.with_time(pdt.time()))
}
// Regression test for #481
#[quickcheck]
fn time_sub_time_no_panic(time_a: Time, time_b: Time) -> bool {
no_panic! {
let _ = time_a - time_b;
let _ = time_b - time_a;
}
}
#[quickcheck]
fn time_duration_until_since_range(time_a: Time, time_b: Time) -> bool {
let a_until_b = time_a.duration_until(time_b);
let b_until_a = time_b.duration_until(time_a);
let a_since_b = time_a.duration_since(time_b);
let b_since_a = time_b.duration_since(time_a);
(Duration::ZERO..Duration::DAY).contains(&a_until_b)
&& (Duration::ZERO..Duration::DAY).contains(&b_until_a)
&& (Duration::ZERO..Duration::DAY).contains(&a_since_b)
&& (Duration::ZERO..Duration::DAY).contains(&b_since_a)
}
#[quickcheck]
fn time_duration_until_since_arithmetic(time_a: Time, time_b: Time) -> bool {
let a_until_b = time_a.duration_until(time_b);
let b_until_a = time_b.duration_until(time_a);
let a_since_b = time_a.duration_since(time_b);
let b_since_a = time_b.duration_since(time_a);
time_a + a_until_b == time_b
&& time_b + b_until_a == time_a
&& time_b + a_since_b == time_a
&& time_a + b_since_a == time_b
}
#[quickcheck]
fn from_julian_day_no_panic(julian_day: i32) -> TestResult {
if !(Date::MIN.to_julian_day()..=Date::MAX.to_julian_day()).contains(&julian_day) {
return TestResult::discard();
}
TestResult::from_bool(
std::panic::catch_unwind(|| {
let _ = Date::from_julian_day(julian_day);
})
.is_ok(),
)
}
#[quickcheck]
fn odt_eq_no_panic(left: OffsetDateTime, right: OffsetDateTime) -> bool {
no_panic! {
let _ = left == right;
}
}
#[quickcheck]
fn odt_ord_no_panic(left: OffsetDateTime, right: OffsetDateTime) -> bool {
no_panic! {
let _ = left < right;
let _ = left > right;
}
}
#[quickcheck]
fn odt_sub_no_panic(left: OffsetDateTime, right: OffsetDateTime) -> bool {
no_panic! {
let _ = left - right;
}
}
#[quickcheck]
fn odt_to_offset_no_panic(odt: OffsetDateTime, offset: UtcOffset) -> TestResult {
let offset_difference = offset.whole_seconds() - odt.offset().whole_seconds();
if Date::MIN
.midnight()
.assume_utc()
.checked_add(Duration::seconds(offset_difference.extend()))
.is_none()
|| Date::MAX
.with_time(time!(23:59:59.999_999_999))
.assume_utc()
.checked_add(Duration::seconds(offset_difference.extend()))
.is_none()
{
return TestResult::discard();
}
TestResult::from_bool(no_panic! {
let _ = odt.to_offset(offset);
})
}
#[quickcheck]
fn odt_replace_offset_no_panic(odt: OffsetDateTime, offset: UtcOffset) -> TestResult {
if Date::MIN
.midnight()
.assume_offset(odt.offset())
.checked_add(Duration::seconds(offset.whole_seconds().extend()))
.is_none()
|| Date::MAX
.with_time(time!(23:59:59.999_999_999))
.assume_offset(odt.offset())
.checked_add(Duration::seconds(offset.whole_seconds().extend()))
.is_none()
{
return TestResult::discard();
}
TestResult::from_bool(no_panic! {
let _ = odt.replace_offset(offset);
})
}
test_shrink!(Date, date_can_shrink_year, year());
test_shrink!(Date, date_can_shrink_ordinal, ordinal(), min = 1);
test_shrink!(Duration, duration_can_shrink_seconds, whole_seconds());
test_shrink!(Duration, duration_can_shrink_ns, subsec_nanoseconds());
test_shrink!(Time, time_can_shrink_hour, hour());
test_shrink!(Time, time_can_shrink_minute, minute());
test_shrink!(Time, time_can_shrink_second, second());
test_shrink!(Time, time_can_shrink_nanosecond, nanosecond());
test_shrink!(
PrimitiveDateTime,
primitive_date_time_can_shrink_year,
year()
);
test_shrink!(
PrimitiveDateTime,
primitive_date_time_can_shrink_ordinal,
ordinal(),
min = 1
);
test_shrink!(
PrimitiveDateTime,
primitive_date_time_can_shrink_hour,
hour()
);
test_shrink!(
PrimitiveDateTime,
primitive_date_time_can_shrink_minute,
minute()
);
test_shrink!(
PrimitiveDateTime,
primitive_date_time_can_shrink_second,
second()
);
test_shrink!(
PrimitiveDateTime,
primitive_date_time_can_shrink_nanosecond,
nanosecond()
);
test_shrink!(UtcOffset, utc_offset_can_shrink, whole_seconds());
test_shrink!(
OffsetDateTime,
offset_date_time_can_shrink_offset,
offset().whole_seconds()
);
test_shrink!(OffsetDateTime, offset_date_time_can_shrink_year, year());
test_shrink!(
OffsetDateTime,
offset_date_time_can_shrink_ordinal,
ordinal(),
min = 1
);
test_shrink!(OffsetDateTime, offset_date_time_can_shrink_hour, hour());
test_shrink!(OffsetDateTime, offset_date_time_can_shrink_minute, minute());
test_shrink!(OffsetDateTime, offset_date_time_can_shrink_second, second());
test_shrink!(
OffsetDateTime,
offset_date_time_can_shrink_nanosecond,
nanosecond()
);