use super::{Instant, LeapTable, Transition, Uncertainty};
const SECOND: i128 = 1_000_000_000;
const MIDNIGHT: i64 = 1_483_228_800;
fn normal(seconds: i64, nanos: u32) -> Instant {
Instant::new(seconds, nanos).unwrap()
}
#[test]
fn insertion_is_distinct_and_elapsed_seconds_include_it() {
let entries = [Transition::new(MIDNIGHT, 1)];
let table = LeapTable::new(MIDNIGHT - 100, MIDNIGHT + 100, &entries).unwrap();
let before = normal(MIDNIGHT - 1, 0);
let leap = Instant::leap_after(MIDNIGHT - 1, 0).unwrap();
let after = normal(MIDNIGHT, 0);
assert!(normal(MIDNIGHT - 1, 999_999_999) < leap);
assert!(leap < after);
assert_eq!(table.shift(before, SECOND), Ok(leap));
assert_eq!(table.shift(leap, SECOND), Ok(after));
assert_eq!(table.shift(after, -SECOND), Ok(leap));
assert_eq!(table.shift(after, -2 * SECOND), Ok(before));
assert_eq!(
table.shift(before, SECOND / 2),
Ok(normal(MIDNIGHT - 1, 500_000_000))
);
assert_eq!(
table.shift(leap, SECOND / 2),
Instant::leap_after(MIDNIGHT - 1, 500_000_000).ok_or(Uncertainty::OutOfRange)
);
}
#[test]
fn deletion_skips_the_missing_second_in_both_directions() {
let entries = [Transition::new(MIDNIGHT, -1)];
let table = LeapTable::new(MIDNIGHT - 100, MIDNIGHT + 100, &entries).unwrap();
assert_eq!(
table.shift(normal(MIDNIGHT - 2, 0), SECOND),
Ok(normal(MIDNIGHT, 0))
);
assert_eq!(
table.shift(normal(MIDNIGHT, 0), -SECOND),
Ok(normal(MIDNIGHT - 2, 0))
);
assert_eq!(
table.shift(normal(MIDNIGHT - 1, 0), 0),
Err(Uncertainty::NotInUtc)
);
assert_eq!(
table.shift(Instant::leap_after(MIDNIGHT - 1, 0).unwrap(), 0),
Err(Uncertainty::NotInUtc)
);
}
#[test]
fn coverage_is_not_an_assumption_about_future_utc() {
let table = LeapTable::new(0, 10, &[]).unwrap();
assert_eq!(
table.shift(normal(9, 999_999_999), 1),
Err(Uncertainty::OutsideCoverage)
);
assert_eq!(
table.shift(normal(0, 0), -1),
Err(Uncertainty::OutsideCoverage)
);
assert_eq!(
table.shift(normal(10, 0), -SECOND),
Err(Uncertainty::OutsideCoverage)
);
assert_eq!(table.shift(normal(5, 0), 0), Ok(normal(5, 0)));
assert_eq!(
table.shift(normal(5, 0), i128::MAX),
Err(Uncertainty::OutOfRange)
);
}
#[test]
fn table_rejects_conflicting_or_unrepresentable_facts() {
assert!(LeapTable::new(10, 10, &[]).is_err());
assert!(
LeapTable::new(
MIDNIGHT - 100,
MIDNIGHT + 100,
&[Transition::new(MIDNIGHT, 2)]
)
.is_err()
);
assert!(
LeapTable::new(
MIDNIGHT - 100,
MIDNIGHT + 100,
&[Transition::new(MIDNIGHT, 0)]
)
.is_err()
);
assert!(
LeapTable::new(
MIDNIGHT - 100,
MIDNIGHT + 100,
&[Transition::new(MIDNIGHT, 1), Transition::new(MIDNIGHT, 2)]
)
.is_err()
);
assert!(
LeapTable::new(
MIDNIGHT - 100,
MIDNIGHT + 100,
&[Transition::new(MIDNIGHT + 1, 1)]
)
.is_err()
);
assert!(LeapTable::new(MIDNIGHT, MIDNIGHT + 100, &[Transition::new(MIDNIGHT, 1)]).is_err());
assert!(Instant::new(0, 1_000_000_000).is_none());
assert!(Instant::leap_after(i64::MAX, 0).is_none());
}
#[test]
fn mixed_changes_round_trip_without_copying_the_table() {
let next_month = MIDNIGHT + 31 * 86_400;
let entries = [Transition::new(MIDNIGHT, 1), Transition::new(next_month, 0)];
let table = LeapTable::new(MIDNIGHT - 100, next_month + 100, &entries).unwrap();
let at = normal(MIDNIGHT - 2, 123);
let elapsed = i128::from(next_month - (MIDNIGHT - 2)) * SECOND;
assert_eq!(table.shift(at, elapsed), Ok(normal(next_month, 123)));
assert_eq!(table.shift(normal(next_month, 123), -elapsed), Ok(at));
assert!(core::ptr::eq(
table.transitions().as_ptr(),
entries.as_ptr()
));
}
#[test]
fn parsed_leap_label_preserves_offset_and_subnanosecond_precision() {
let parse = |text: &str| {
let der = crate::fixtures::generalized_time(text);
let parser = structured_asn1::Parser::der(crate::fixtures::memory());
let mut reader = parser.read(&der, 0).unwrap();
let element = reader.next_element().unwrap().unwrap();
super::read_generalized(&reader, &element, "test").unwrap()
};
let leap = parse("20161231235960.9999999991Z");
assert!(leap.start().is_leap_second());
assert_eq!(leap, parse("20170101005960.9999999991+0100"));
let changes = [Transition::new(MIDNIGHT, 1)];
let table = LeapTable::new(MIDNIGHT - 100, MIDNIGHT + 100, &changes).unwrap();
assert_eq!(leap.upper_bound(&table), Ok(normal(MIDNIGHT, 0)));
let before = parse("20161231235959.9999999991Z");
assert_eq!(
before.upper_bound(&table),
Ok(Instant::leap_after(MIDNIGHT - 1, 0).unwrap())
);
let no_change = LeapTable::new(MIDNIGHT - 100, MIDNIGHT + 100, &[]).unwrap();
assert_eq!(before.upper_bound(&no_change), Ok(normal(MIDNIGHT, 0)));
assert_eq!(leap.upper_bound(&no_change), Err(Uncertainty::NotInUtc));
}
#[test]
fn end_transition_and_nanosecond_edges_round_trip() {
let entries = [Transition::new(MIDNIGHT, 1)];
let table = LeapTable::new(MIDNIGHT - 2, MIDNIGHT, &entries).unwrap();
let leap = Instant::leap_after(MIDNIGHT - 1, 999_999_999).unwrap();
assert_eq!(table.shift(leap, 0), Ok(leap));
assert_eq!(table.shift(leap, 1), Err(Uncertainty::OutsideCoverage));
for at in [normal(MIDNIGHT - 2, 0), normal(MIDNIGHT - 1, 7), leap] {
for duration in [-SECOND, -1, 0, 1, SECOND] {
if let Ok(moved) = table.shift(at, duration) {
assert_eq!(table.shift(moved, -duration), Ok(at));
}
}
}
let negative = [Transition::new(MIDNIGHT, -1)];
assert!(LeapTable::new(MIDNIGHT - 1, MIDNIGHT + 1, &negative).is_err());
}
#[test]
fn decimal_fraction_keeps_carry_beyond_fixed_precision() {
let der =
crate::fixtures::generalized_time("2017010100.0000000000002777777777777777777777778Z");
let parser = structured_asn1::Parser::der(crate::fixtures::memory());
let mut reader = parser.read(&der, 0).unwrap();
let element = reader.next_element().unwrap().unwrap();
let reading = super::read_generalized(&reader, &element, "test").unwrap();
assert_eq!(reading.start(), normal(MIDNIGHT, 1));
assert!(reading.has_subnanosecond_remainder());
}