diff options
Diffstat (limited to 'debian/tests/python')
-rwxr-xr-x | debian/tests/python | 179 |
1 files changed, 179 insertions, 0 deletions
diff --git a/debian/tests/python b/debian/tests/python new file mode 100755 index 0000000..25044a9 --- /dev/null +++ b/debian/tests/python @@ -0,0 +1,179 @@ +#!/usr/bin/python3 + +# Author: Benjamin Drung <bdrung@ubuntu.com> + +"""Test timezones using Python's zoneinfo module.""" + +import datetime +import os +import pathlib +import re +import sys +import typing +import unittest +import zoneinfo + +ROOT_DIR = pathlib.Path(__file__).parent.parent.parent +HAS_LEGACY_TIMEZONES = "NO_LEGACY_TIMEZONES" not in os.environ + + +def read_backwards_links(backwards_file: pathlib.Path) -> dict[str, str]: + """Read backwards compatibility links from the upstream backwards file.""" + backwards_links = {} + for line in backwards_file.read_text(encoding="utf-8").splitlines(): + match = re.match(r"^Link\t(?P<target>\S+)\t+(?P<link_name>\S+)", line) + if not match: + continue + backwards_links[match.group("link_name")] = match.group("target") + return backwards_links + + +class TestZoneinfo(unittest.TestCase): + """Test timezones using Python's zoneinfo module.""" + + def _hours(self, delta: typing.Optional[datetime.timedelta]) -> int: + assert delta is not None + total_seconds = int(delta.total_seconds()) + self.assertEqual(total_seconds % 3600, 0) + return total_seconds // 3600 + + def test_available_timezones_count(self) -> None: + """Test available_timezones() count to be reasonable.""" + zones = len(zoneinfo.available_timezones()) + expected_zones = 485 + if HAS_LEGACY_TIMEZONES: + expected_zones = 597 + self.assertGreaterEqual(zones, expected_zones, "less zones than 2023c-8") + self.assertLess( + zones, round(expected_zones * 1.1), ">10% more zones than 2023c-8" + ) + + def test_daylight_saving_transition(self) -> None: + """Test daylight saving time transition from Python documentation.""" + tzinfo = zoneinfo.ZoneInfo("America/Los_Angeles") + date = datetime.datetime(2020, 10, 31, 12, tzinfo=tzinfo) + self.assertEqual(date.tzname(), "PDT") + next_day = date + datetime.timedelta(days=1) + self.assertEqual(next_day.tzname(), "PST") + + def _assert_equal_zones_at_date( + self, + date: datetime.datetime, + timezone1: zoneinfo.ZoneInfo, + timezone2: zoneinfo.ZoneInfo, + ) -> None: + date1 = date.replace(tzinfo=timezone1) + date2 = date.replace(tzinfo=timezone2) + self.assertEqual(date1 - date2, datetime.timedelta(seconds=0)) + self.assertEqual(date1.tzname(), date2.tzname()) + + def _assert_equal_zones( + self, timezone1: zoneinfo.ZoneInfo, timezone2: zoneinfo.ZoneInfo + ) -> None: + """Test timezones to be heuristically equal regardless of the name.""" + october_2020 = datetime.datetime(2020, 10, 31, 12) + self._assert_equal_zones_at_date(october_2020, timezone1, timezone2) + july_2021 = datetime.datetime(2021, 7, 3, 12) + self._assert_equal_zones_at_date(july_2021, timezone1, timezone2) + + @unittest.skipIf(os.environ.get("PYTHONTZPATH"), "requires installed tzdata") + def test_localtime(self) -> None: + """Test 'localtime' timezone.""" + localtime = pathlib.Path("/etc/localtime") + zone = str(localtime.resolve().relative_to("/usr/share/zoneinfo")) + tzinfo = zoneinfo.ZoneInfo("localtime") + self._assert_equal_zones(tzinfo, zoneinfo.ZoneInfo(zone)) + + def _test_timezone(self, zone: str) -> None: + """Test zone to load, have a name, and have a reasonable offset.""" + tzinfo = zoneinfo.ZoneInfo(zone) + self.assertEqual(str(tzinfo), zone) + date = datetime.datetime(2020, 10, 31, 12, tzinfo=tzinfo) + + tzname = date.tzname() + assert tzname is not None + self.assertGreaterEqual(len(tzname), 3, tzname) + self.assertLessEqual(len(tzname), 5, tzname) + + utc_offset = date.utcoffset() + assert utc_offset is not None + self.assertEqual(int(utc_offset.total_seconds()) % 900, 0) + self.assertLessEqual(utc_offset, datetime.timedelta(hours=14)) + + def test_pre_1970_timestamps(self) -> None: + """Test pre-1970 timestamps of Berlin and Oslo being different.""" + berlin = zoneinfo.ZoneInfo("Europe/Berlin") + date = datetime.datetime(1960, 7, 1, tzinfo=berlin) + self.assertEqual(self._hours(date.utcoffset()), 1) + oslo = zoneinfo.ZoneInfo("Europe/Oslo") + self.assertEqual(self._hours(date.replace(tzinfo=oslo).utcoffset()), 2) + + def test_post_1970_symlinks_consistency(self) -> None: + """Test that post-1970 symlinks are consistent with pre-1970 timezones. + + Building tzdata with PACKRATDATA=backzone will result in separate + time zones for time zones that differ only before 1970. These time + zones should behave identical after 1970. Building tzdata without + PACKRATDATA=backzone will result in one of the time zones become a + symlink to the other time zone. + """ + links = read_backwards_links(ROOT_DIR / "backward") + for link_name, target in links.items(): + with self.subTest(f"{link_name} -> {target}"): + try: + tz_link = zoneinfo.ZoneInfo(link_name) + except zoneinfo.ZoneInfoNotFoundError: + if not HAS_LEGACY_TIMEZONES: + continue + raise + tz_target = zoneinfo.ZoneInfo(target) + now = datetime.datetime.now() + self._assert_equal_zones_at_date(now, tz_link, tz_target) + future = now + datetime.timedelta(days=30 * 6) + self._assert_equal_zones_at_date(future, tz_link, tz_target) + + def test_timezones(self) -> None: + """Test all zones to load, have a name, and have a reasonable offset.""" + for zone in zoneinfo.available_timezones(): + with self.subTest(zone=zone): + self._test_timezone(zone) + + def test_2022g(self) -> None: + """Test new zone America/Ciudad_Juarez from 2022g release.""" + tzinfo = zoneinfo.ZoneInfo("America/Ciudad_Juarez") + date = datetime.datetime(2022, 12, 1, tzinfo=tzinfo) + self.assertEqual(self._hours(date.utcoffset()), -7) + + def test_2023a(self) -> None: + """Test Egypt uses DST again from 2023a release.""" + tzinfo = zoneinfo.ZoneInfo("Africa/Cairo") + date = datetime.datetime(2023, 4, 28, 12, 0, tzinfo=tzinfo) + self.assertEqual(self._hours(date.utcoffset()), 3) + + def test_2023c(self) -> None: + """Test Lebanon's reverted DST delay from 2023c release.""" + tzinfo = zoneinfo.ZoneInfo("Asia/Beirut") + date = datetime.datetime(2023, 4, 2, tzinfo=tzinfo) + self.assertEqual(self._hours(date.utcoffset()), 3) + + def test_2023d(self) -> None: + """Test 2023d release: Vostok being on +05 from 2023-12-18 on.""" + tzinfo = zoneinfo.ZoneInfo("Antarctica/Vostok") + date = datetime.datetime(2023, 12, 19, tzinfo=tzinfo) + self.assertEqual(self._hours(date.utcoffset()), 5) + + def test_2024a(self) -> None: + """Test 2024a release: Kazakhstan being on +05 from 2024-03-01 on.""" + tzinfo = zoneinfo.ZoneInfo("Asia/Almaty") + date = datetime.datetime(2024, 3, 2, tzinfo=tzinfo) + self.assertEqual(self._hours(date.utcoffset()), 5) + +def main() -> None: + """Run unit tests in verbose mode.""" + argv = sys.argv.copy() + argv.insert(1, "-v") + unittest.main(argv=argv) + + +if __name__ == "__main__": + main() |