leapseconddata/testleapseconddata.py
Jeff Epler d8480e67d5 Add timeout & improve coverage
Looks like the NIST FTP server is not happy with urllib and it fails
(ugh!) by hanging for a long time. Maybe I'll have to delete it.
(it works with curl, which uses EPSV and not PASV. python uses PASV
for all ipv4 ftp and EPSV for ipv6, instead of trying EPSV first)

Set a reasonable timeout.

The format of `leapsecond sources` has been modified for better readability.
2025-04-25 08:27:31 +02:00

160 lines
6.2 KiB
Python

#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2021 Jeff Epler
#
# SPDX-License-Identifier: GPL-3.0-only
# ruff: noqa: D101
# ruff: noqa: D102
"""Test most leapseconddata functionality"""
import contextlib
import datetime
import io
import pathlib
import unittest
import leapseconddata
import leapseconddata.__main__
db = leapseconddata.LeapSecondData.from_standard_source(timeout=8)
GMT1 = datetime.timezone(datetime.timedelta(seconds=3600), "GMT1")
bad_sources = [
"data:text/plain;base64,SGVsbG8sIFdvcmxkIQ==",
"data:text/plain,%23h%099dac5845%208acd32c0%202947d462%20daf4a943%20f58d9391%0A",
"file:///doesnotexist",
]
class LeapSecondDataTest(unittest.TestCase):
def run_main(self, *args: str) -> None:
buf = io.StringIO()
with contextlib.redirect_stdout(buf):
try:
leapseconddata.__main__.cli(args)
except SystemExit as e:
self.assertEqual(e.code, 0)
def test_main(self) -> None:
self.run_main("info")
self.run_main("table", "2009-1-1", "2016-1-1")
self.run_main("convert", "--to-utc", "2009-01-01 00:00:33")
self.run_main("convert", "--to-utc", "2009-01-01 00:00:34")
self.run_main("convert", "2009-01-01 00:00:33")
self.run_main("convert")
self.run_main("offset", "2009-01-01 00:00:33")
self.run_main("offset", "--tai", "2009-01-01 00:00:33")
self.run_main("next-leapsecond", "2009-2-2")
self.run_main("next-leapsecond", "2100-2-2")
self.run_main("previous-leapsecond", "2009-2-2")
self.run_main("previous-leapsecond", "1960-2-2")
self.run_main("sources", "--timeout", "8")
self.run_main("sources", *bad_sources)
def test_corrupt(self) -> None:
self.assertRaises(
leapseconddata.InvalidHashError,
leapseconddata.LeapSecondData.from_data,
"#h 0 0 0 0 0\n",
)
self.assertRaises(
leapseconddata.InvalidHashError,
leapseconddata.LeapSecondData.from_data,
b"#h 0 0 0 0 0\n",
)
self.assertRaises(
leapseconddata.InvalidHashError,
leapseconddata.LeapSecondData.from_data,
"#\n",
)
self.assertIsNotNone(leapseconddata.LeapSecondData.from_data("#h 0 0 0 0 0\n", check_hash=False))
def test_invalid(self) -> None:
valid_until = db.valid_until
assert valid_until
self.assertRaises(
leapseconddata.ValidityError,
db.tai_offset,
valid_until + datetime.timedelta(seconds=1),
)
db1 = leapseconddata.LeapSecondData(
[
leapseconddata.LeapSecondInfo(
datetime.datetime(1970, 1, 1, tzinfo=datetime.timezone.utc),
datetime.timedelta(seconds=1),
),
],
)
self.assertRaises(leapseconddata.ValidityError, db1.tai_offset, db.valid_until)
self.assertEqual(db1.tai_offset(valid_until, check_validity=False), datetime.timedelta(seconds=1))
when = datetime.datetime(1999, 1, 1, tzinfo=datetime.timezone.utc) - datetime.timedelta(seconds=1)
assert when.tzinfo is not None
self.assertRaises(ValueError, db.tai_to_utc, when)
def test_empty(self) -> None:
db1 = leapseconddata.LeapSecondData([])
self.assertEqual(
db1.tai_offset(datetime.datetime(2020, 1, 1, tzinfo=datetime.timezone.utc), check_validity=False),
datetime.timedelta(seconds=0),
)
def test_invalid2(self) -> None:
when = datetime.datetime(datetime.MAXYEAR, 1, 1, tzinfo=datetime.timezone.utc) - datetime.timedelta(seconds=1)
with self.assertRaises(leapseconddata.ValidityError):
leapseconddata.LeapSecondData.from_standard_source(when, custom_sources=bad_sources, timeout=8)
def test_tz(self) -> None:
when = datetime.datetime(1999, 1, 1, tzinfo=datetime.timezone.utc) - datetime.timedelta(seconds=1)
when = when.replace(fold=True)
self.assertTrue(db.is_leap_second(when))
self.assertFalse(db.is_leap_second(when - datetime.timedelta(seconds=1)))
self.assertFalse(db.is_leap_second(when + datetime.timedelta(seconds=1)))
when = when.astimezone(GMT1).replace(fold=True)
self.assertTrue(db.is_leap_second(when))
self.assertFalse(db.is_leap_second(when - datetime.timedelta(seconds=1)))
self.assertFalse(db.is_leap_second(when + datetime.timedelta(seconds=1)))
when_tai = datetime.datetime(1999, 1, 1, 0, 0, 32, tzinfo=leapseconddata.tai)
when_utc = db.tai_to_utc(when_tai)
self.assertIs(when_utc.tzinfo, datetime.timezone.utc)
when_tai = datetime.datetime(1999, 1, 1, 0, 0, 32, tzinfo=None) # noqa: DTZ001
when_utc2 = db.tai_to_utc(when_tai)
self.assertEqual(when_utc, when_utc2)
def test_to_tai(self) -> None:
when = datetime.datetime(1999, 1, 1, tzinfo=datetime.timezone.utc) - datetime.timedelta(seconds=1)
when_tai = db.to_tai(when)
when_tai2 = db.to_tai(when_tai)
assert when != when_tai
assert when_tai == when_tai2
assert when_tai.tzinfo is leapseconddata.tai
assert when_tai2.tzinfo is leapseconddata.tai
def test_to_tai_naive(self) -> None:
when = datetime.datetime(1999, 1, 1, tzinfo=None) - datetime.timedelta(seconds=1) # noqa: DTZ001
when_tai = db.to_tai(when)
when2 = datetime.datetime(1999, 1, 1, tzinfo=datetime.timezone.utc) - datetime.timedelta(seconds=1)
when_tai2 = db.to_tai(when2)
self.assertEqual(when_tai, when_tai2)
def assertPrints(self, code: str, expected: str) -> None: # noqa: N802
buf = io.StringIO()
with contextlib.redirect_stdout(buf):
exec(code, {}, {})
self.assertEqual(expected, buf.getvalue())
def test_doc(self) -> None:
docs = pathlib.Path(__file__).parent / "docs"
for expected in docs.rglob("**/*.py.exp"):
py = expected.with_suffix("") # Pop off the ".exp" suffix
self.assertPrints(py.read_text(encoding="utf-8"), expected.read_text(encoding="utf-8"))
if __name__ == "__main__": # pragma: no cover
unittest.main()