use openehr::base::iso8601::{Date, DateTime, Duration, Time};
use openehr::base::object_id::{ArchetypeId, ObjectVersionId};
use proptest::prelude::*;
use std::cmp::Ordering;
use std::fmt::Write as _;
use std::str::FromStr;
fn date_text() -> impl Strategy<Value = String> {
prop_oneof![
(0i32..=9999).prop_map(|y| format!("{y:04}")),
(0i32..=9999, 1u8..=12).prop_map(|(y, m)| format!("{y:04}-{m:02}")),
(0i32..=9999, 1u8..=12, 1u8..=28).prop_map(|(y, m, d)| format!("{y:04}-{m:02}-{d:02}")),
]
}
fn time_text() -> impl Strategy<Value = String> {
(
0u8..=23,
0u8..=59,
0u8..=59,
proptest::option::of(0u32..=999_999),
0u8..=2,
)
.prop_map(|(h, m, s, frac, off)| {
let mut t = format!("{h:02}:{m:02}:{s:02}");
if let Some(f) = frac {
let _ = write!(t, ".{f:06}");
}
match off {
0 => t.push('Z'),
1 => t.push_str("+05:30"),
_ => {}
}
t
})
}
fn datetime_text() -> impl Strategy<Value = String> {
(date_text(), proptest::option::of(time_text())).prop_map(|(d, t)| match t {
Some(t) if d.len() == 10 => format!("{d}T{t}"),
_ => d,
})
}
fn near_miss() -> impl Strategy<Value = String> {
prop_oneof![
datetime_text(),
datetime_text().prop_map(|s| s.replace('-', "")),
datetime_text().prop_map(|s| { s.chars().rev().collect() }),
datetime_text().prop_map(|s| format!("{s}{s}")),
datetime_text().prop_flat_map(|s| {
let n = s.len();
(Just(s), 0..n.max(1)).prop_map(|(s, i)| s[..i.min(s.len())].to_owned())
}),
".*",
prop::collection::vec(any::<char>(), 0..24).prop_map(|v| v.into_iter().collect()),
]
}
proptest! {
#[test]
fn parsers_never_panic(s in near_miss()) {
let _ = Date::from_str(&s);
let _ = Time::from_str(&s);
let _ = DateTime::from_str(&s);
let _ = Duration::from_str(&s);
let _ = ArchetypeId::from_str(&s);
let _ = ObjectVersionId::from_str(&s);
}
#[test]
fn parse_errors_do_not_echo_a_long_input(s in "[A-Za-z0-9:+.-]{200,400}") {
if let Err(e) = DateTime::from_str(&s) {
let rendered = e.to_string();
prop_assert!(
!rendered.contains(&s),
"the error rendered the entire submitted value"
);
}
}
}
#[test]
fn a16_multibyte_offset_returns_err_and_does_not_panic() {
for input in [
"0-\u{10348}",
"12:00:00+\u{10348}",
"2024-01-01T00:00:00+\u{69006}",
"0-\u{a2}\u{a2}",
"0-\u{20ac}\u{41}",
] {
assert!(Time::from_str(input).is_err(), "Time accepted {input:?}");
assert!(
DateTime::from_str(input).is_err(),
"DateTime accepted {input:?}"
);
}
}
proptest! {
#[test]
fn datetime_round_trips_byte_for_byte(s in datetime_text()) {
let parsed = DateTime::from_str(&s)
.map_err(|e| TestCaseError::fail(format!("generator produced invalid text: {e}")))?;
prop_assert_eq!(parsed.as_str(), s.as_str());
prop_assert_eq!(parsed.to_string(), s);
}
#[test]
fn date_round_trips_byte_for_byte(s in date_text()) {
let parsed = Date::from_str(&s)
.map_err(|e| TestCaseError::fail(format!("generator produced invalid text: {e}")))?;
prop_assert_eq!(parsed.as_str(), s.as_str());
}
#[test]
fn time_round_trips_byte_for_byte(s in time_text()) {
let parsed = Time::from_str(&s)
.map_err(|e| TestCaseError::fail(format!("generator produced invalid text: {e}")))?;
prop_assert_eq!(parsed.as_str(), s.as_str());
}
#[test]
fn reparsing_the_rendering_is_stable(s in datetime_text()) {
let a = DateTime::from_str(&s).unwrap();
let b = DateTime::from_str(&a.to_string()).unwrap();
prop_assert_eq!(a.as_str(), b.as_str());
prop_assert_eq!(a.semantic_cmp(&b), Some(Ordering::Equal));
}
}
fn narrow_date_text() -> impl Strategy<Value = String> {
prop_oneof![
(2020i32..=2023).prop_map(|y| format!("{y:04}")),
(2020i32..=2023, 1u8..=4).prop_map(|(y, m)| format!("{y:04}-{m:02}")),
(2020i32..=2023, 1u8..=4, 1u8..=4).prop_map(|(y, m, d)| format!("{y:04}-{m:02}-{d:02}")),
]
}
fn narrow_datetime_text() -> impl Strategy<Value = String> {
(
narrow_date_text(),
proptest::option::of((0u8..=3, 0u8..=3).prop_map(|(h, m)| format!("{h:02}:{m:02}:00"))),
)
.prop_map(|(d, t)| match t {
Some(t) if d.len() == 10 => format!("{d}T{t}"),
_ => d,
})
}
fn two_datetimes() -> impl Strategy<Value = (DateTime, DateTime)> {
(narrow_datetime_text(), narrow_datetime_text()).prop_map(|(a, b)| {
(
DateTime::from_str(&a).unwrap(),
DateTime::from_str(&b).unwrap(),
)
})
}
proptest! {
#[test]
fn comparison_is_reflexive(s in narrow_datetime_text()) {
let a = DateTime::from_str(&s).unwrap();
prop_assert_eq!(a.semantic_cmp(&a), Some(Ordering::Equal));
}
#[test]
fn comparison_is_antisymmetric((a, b) in two_datetimes()) {
let forward = a.semantic_cmp(&b);
let backward = b.semantic_cmp(&a);
prop_assert_eq!(forward, backward.map(Ordering::reverse));
}
#[test]
fn comparison_is_transitive(
x in narrow_datetime_text(), y in narrow_datetime_text(), z in narrow_datetime_text()
) {
let (a, b, c) = (
DateTime::from_str(&x).unwrap(),
DateTime::from_str(&y).unwrap(),
DateTime::from_str(&z).unwrap(),
);
if a.semantic_cmp(&b) == Some(Ordering::Less)
&& b.semantic_cmp(&c) == Some(Ordering::Less)
&& let Some(ord) = a.semantic_cmp(&c)
{
prop_assert_eq!(ord, Ordering::Less, "a < b < c but not a < c");
}
}
#[test]
fn a_coarser_value_is_incomparable_with_its_own_refinement(
y in 2020i32..=2023, m in 1u8..=12, d in 1u8..=28
) {
let year: Date = format!("{y:04}").parse().unwrap();
let month: Date = format!("{y:04}-{m:02}").parse().unwrap();
let day: Date = format!("{y:04}-{m:02}-{d:02}").parse().unwrap();
prop_assert_eq!(year.semantic_cmp(&month), None, "year vs month in that year");
prop_assert_eq!(month.semantic_cmp(&day), None, "month vs day in that month");
prop_assert_eq!(year.semantic_cmp(&day), None, "year vs day in that year");
prop_assert_eq!(month.semantic_cmp(&year), None);
prop_assert_eq!(day.semantic_cmp(&month), None);
prop_assert_eq!(day.semantic_cmp(&year), None);
}
#[test]
fn differing_known_components_are_still_decidable(y in 2020i32..=2023, m in 2u8..=12) {
let year: Date = format!("{y:04}").parse().unwrap();
let later: Date = format!("{:04}-{m:02}", y + 1).parse().unwrap();
prop_assert_eq!(year.semantic_cmp(&later), Some(Ordering::Less));
prop_assert_eq!(later.semantic_cmp(&year), Some(Ordering::Greater));
}
#[test]
fn identical_text_compares_equal(s in narrow_datetime_text()) {
let a = DateTime::from_str(&s).unwrap();
let b = DateTime::from_str(&s).unwrap();
prop_assert_eq!(a.semantic_cmp(&b), Some(Ordering::Equal));
}
}