430 lines
12 KiB
Rust
430 lines
12 KiB
Rust
|
|
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()
|
||
|
|
);
|