Skip to content

Commit 523b814

Browse files
Stabilise zoneinfo64 tests (#7856)
Fixes #7813 ## Changelog N/A
1 parent 6927d00 commit 523b814

File tree

7 files changed

+85
-119
lines changed

7 files changed

+85
-119
lines changed

Cargo.lock

Lines changed: 7 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

utils/zoneinfo64/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,10 @@ calendrical_calculations = { workspace = true }
3333
chrono = { version = "0.4", optional = true }
3434

3535
[dev-dependencies]
36-
chrono-tz = "0.10.4"
3736
itertools.workspace = true
3837
jiff = { workspace = true, features = ["tzdb-bundle-always", "std"] }
38+
chrono-tz-2025b = { package = "chrono-tz", version = "=0.10.4" }
39+
jiff-tzdb-2025b = { package = "jiff-tzdb", version = "=0.1.4" }
3940

4041
[features]
4142
chrono = ["dep:chrono"]

utils/zoneinfo64/README.md

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
145 KB
Binary file not shown.

utils/zoneinfo64/src/lib.rs

Lines changed: 55 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
//! # use zoneinfo64::{Offset, PossibleOffset, ZoneInfo64, UtcOffset};
2222
//!
2323
//! // Needs to be u32-aligned
24-
//! let resb = resb::include_bytes_as_u32!("./data/zoneinfo64.res");
24+
//! let resb = resb::include_bytes_as_u32!("./data/2026a.res");
2525
//! // Then we parse the data
2626
//! let zoneinfo = ZoneInfo64::try_from_u32s(resb)
2727
//! .expect("Error processing resource bundle file");
@@ -71,7 +71,7 @@ mod deserialize;
7171

7272
/// A bundled zoneinfo64.res that can be used for testing. No guarantee is made
7373
/// as to the version in use; though we will try to keep it up to date.
74-
pub const ZONEINFO64_RES_FOR_TESTING: &[u32] = resb::include_bytes_as_u32!("./data/zoneinfo64.res");
74+
pub const ZONEINFO64_RES_FOR_TESTING: &[u32] = resb::include_bytes_as_u32!("./data/2026a.res");
7575

7676
const EPOCH: RataDie = calendrical_calculations::gregorian::fixed_from_gregorian(1970, 1, 1);
7777
const SECONDS_IN_UTC_DAY: i64 = 24 * 60 * 60;
@@ -181,21 +181,6 @@ impl<'a> ZoneInfo64<'a> {
181181
deserialize::deserialize(resb)
182182
}
183183
#[cfg(test)]
184-
fn is_alias(&self, iana: &str) -> bool {
185-
let Some(idx) = self
186-
.names
187-
.binary_search_by(|&n| n.chars().cmp(iana.chars()))
188-
.ok()
189-
else {
190-
return false;
191-
};
192-
193-
#[expect(clippy::indexing_slicing)] // zones and names have the same length
194-
let zone = &self.zones[idx];
195-
196-
matches!(zone, &TzZone::Int(_))
197-
}
198-
#[cfg(test)]
199184
fn iter(&'a self) -> impl Iterator<Item = Zone<'a>> {
200185
(0..self.names.len()).map(move |i| Zone::from_raw_parts((i as u16, self)))
201186
}
@@ -724,29 +709,41 @@ impl<'a> Zone<'a> {
724709
#[cfg(test)]
725710
mod tests {
726711
use super::*;
727-
use chrono_tz::Tz;
728712
use itertools::Itertools;
713+
use jiff::ToSpan;
729714
use std::{str::FromStr, sync::LazyLock};
730715

716+
// We test on 2025b as that's the latest version that is available in both chrono and jiff.
717+
// The goal here is not to assert up-to-date TZDB data, but to test that zoneinfo64, chrono,
718+
// and jiff agree.
731719
pub(crate) static TZDB: LazyLock<ZoneInfo64> = LazyLock::new(|| {
732-
ZoneInfo64::try_from_u32s(ZONEINFO64_RES_FOR_TESTING)
720+
ZoneInfo64::try_from_u32s(resb::include_bytes_as_u32!("../tests/data/2025b.res"))
733721
.expect("Error processing resource bundle file")
734722
});
735723

724+
const _: () = assert!(matches!(
725+
chrono_tz_2025b::IANA_TZDB_VERSION.as_bytes(),
726+
b"2025b"
727+
));
728+
const _: () = assert!(matches!(
729+
jiff_tzdb_2025b::VERSION.unwrap().as_bytes(),
730+
b"2025b"
731+
));
732+
736733
/// Tests an invariant we rely on in our code
737734
#[test]
738735
fn test_monotonic_transition_times() {
739-
for chrono in time_zones_to_test() {
740-
let iana = chrono.name();
741-
let zoneinfo64 = TZDB.get(iana).unwrap().simple();
736+
for zone in TZDB.iter() {
737+
let zoneinfo64 = zone.simple();
742738

743739
for (prev, curr) in (-1..zoneinfo64.transition_count())
744740
.map(|idx| zoneinfo64.transition_offset_at(idx))
745741
.tuple_windows::<(_, _)>()
746742
{
747743
assert!(
748744
prev.since < curr.since,
749-
"{iana}: Transition times should be strictly increasing ({prev:?}, {curr:?})"
745+
"{:?}: Transition times should be strictly increasing ({prev:?}, {curr:?})",
746+
zone.name()
750747
);
751748
}
752749
}
@@ -755,9 +752,8 @@ mod tests {
755752
/// Tests an invariant we rely on in our code
756753
#[test]
757754
fn test_transition_local_times_do_not_overlap() {
758-
for chrono in time_zones_to_test() {
759-
let iana = chrono.name();
760-
let zoneinfo64 = TZDB.get(iana).unwrap().simple();
755+
for zone in TZDB.iter() {
756+
let zoneinfo64 = zone.simple();
761757

762758
for (prev, curr) in (-1..zoneinfo64.transition_count())
763759
.map(|idx| zoneinfo64.transition_offset_at(idx))
@@ -768,26 +764,22 @@ mod tests {
768764

769765
assert!(
770766
prev_wall < curr_wall,
771-
"{iana}: Transitions should not be so close as to create a ambiguity ({prev:?}, {curr:?}"
767+
"{:?}: Transitions should not be so close as to create a ambiguity ({prev:?}, {curr:?}",
768+
zone.name()
772769
);
773770
}
774771
}
775772
}
776773

777-
pub(crate) fn time_zones_to_test() -> impl Iterator<Item = Tz> {
778-
chrono_tz::TZ_VARIANTS
779-
.iter()
780-
.copied()
781-
.filter(|tz| !TZDB.is_alias(tz.name()))
782-
}
783-
784774
fn has_rearguard_diff(iana: &str) -> bool {
785775
matches!(
786776
iana,
787777
"Africa/Casablanca"
788778
| "Africa/El_Aaiun"
789779
| "Africa/Windhoek"
780+
| "Eire"
790781
| "Europe/Dublin"
782+
| "Europe/Bratislava"
791783
| "Europe/Prague"
792784
)
793785
}
@@ -796,16 +788,10 @@ mod tests {
796788
fn test_against_chrono() {
797789
use chrono::Offset;
798790
use chrono::TimeZone;
799-
use chrono_tz::OffsetComponents;
791+
use chrono_tz_2025b::OffsetComponents;
800792

801-
for chrono in time_zones_to_test() {
793+
for chrono in chrono_tz_2025b::TZ_VARIANTS {
802794
let iana = chrono.name();
803-
804-
if iana == "America/Tijuana" {
805-
// 2025c not yet in chrono
806-
continue;
807-
}
808-
809795
let zoneinfo64 = TZDB.get(iana).unwrap();
810796

811797
for seconds_since_epoch in transitions(iana, false)
@@ -850,67 +836,45 @@ mod tests {
850836
}
851837

852838
fn transitions(iana: &str, require_offset_change: bool) -> Vec<Transition> {
853-
let tz = jiff::tz::TimeZone::get(iana).unwrap();
854-
let mut transitions = tz
855-
// Chrono only evaluates rules until 2100
856-
.preceding(jiff::Timestamp::from_str("2100-01-01T00:00:00Z").unwrap())
857-
.map(|t| Transition {
858-
since: t.timestamp().as_second(),
859-
offset: UtcOffset(t.offset().seconds()),
860-
rule_applies: t.dst().is_dst(),
839+
let max = jiff::Timestamp::from_str("2100-01-01T00:00:00Z").unwrap();
840+
let tz = jiff::tz::TimeZone::tzif(iana, jiff_tzdb_2025b::get(iana).unwrap().1).unwrap();
841+
tz.following(jiff::Timestamp::MIN)
842+
// Evaluate rules until 2100
843+
.take_while(|t| t.timestamp() < max)
844+
.filter_map(|t| {
845+
let before = tz.to_offset_info(t.timestamp() - 1.second());
846+
(if require_offset_change {
847+
t.offset() != before.offset()
848+
} else {
849+
before.offset() != t.offset()
850+
|| before.dst() != t.dst()
851+
// This is a super weird transition in Europe/Paris that would be removed by
852+
// our rule, but we want to keep it because it's in zoneinfo64.
853+
// 1944-04-03T01:00:00Z, (1.0, 1.0)
854+
// 1944-08-24T22:00:00Z, (0.0, 2.0) <- same offset and also DST
855+
// 1944-10-07T23:00:00Z, (0.0, 1.0)
856+
|| t.timestamp().as_second() == -800071200
857+
})
858+
.then_some(Transition {
859+
since: t.timestamp().as_second(),
860+
offset: UtcOffset(t.offset().seconds()),
861+
rule_applies: t.dst().is_dst(),
862+
})
861863
})
862-
.collect::<Vec<_>>();
863-
864-
transitions.reverse();
865-
866-
// jiff returns transitions also if only the name changes, we don't
867-
transitions.retain(|t| {
868-
let before = tz.to_offset_info(jiff::Timestamp::from_second(t.since - 1).unwrap());
869-
if require_offset_change {
870-
before.offset().seconds() != t.offset.0
871-
} else {
872-
before.offset().seconds() != t.offset.0
873-
|| before.dst().is_dst() != t.rule_applies
874-
// This is a super weird transition that would be removed by our rule,
875-
// but we want to keep it because it's in zoneinfo64.
876-
// 1944-04-03T01:00:00Z, (1.0, 1.0)
877-
// 1944-08-24T22:00:00Z, (0.0, 2.0) <- same offset and also DST
878-
// 1944-10-07T23:00:00Z, (0.0, 1.0)
879-
|| (iana == "Europe/Paris" && t.since == -800071200)
880-
}
881-
});
882-
883-
transitions
864+
.collect()
884865
}
885866

886867
#[test]
887868
fn test_transition_against_jiff() {
888-
for (zone, require_offset_change) in
889-
time_zones_to_test().cartesian_product([true, false].into_iter())
869+
for (iana, require_offset_change) in
870+
jiff_tzdb_2025b::available().cartesian_product([true, false].into_iter())
890871
{
891-
let iana = zone.name();
892872
let transitions = transitions(iana, require_offset_change);
893873

894874
if has_rearguard_diff(iana) || transitions.is_empty() {
895875
continue;
896876
}
897877

898-
// TODO: investigate why these zones don't work with jiff/tzdb-bundle-always
899-
// https://github.com/unicode-org/icu4x/issues/7813
900-
if matches!(
901-
iana,
902-
"America/Ciudad_Juarez"
903-
| "America/Indiana/Petersburg"
904-
| "America/Indiana/Vincennes"
905-
| "America/Indiana/Winamac"
906-
| "America/Metlakatla"
907-
| "America/North_Dakota/Beulah"
908-
// Broke in the 2025c update
909-
| "Europe/Chisinau"
910-
) {
911-
continue;
912-
}
913-
914878
let zoneinfo64 = TZDB.get(iana).unwrap();
915879

916880
assert_eq!(

0 commit comments

Comments
 (0)