| from __future__ import annotations | |
| import datetime | |
| import sys | |
| import time | |
| import pytest | |
| import env | |
| from pybind11_tests import chrono as m | |
| def test_chrono_system_clock(): | |
| # Get the time from both c++ and datetime | |
| date0 = datetime.datetime.today() | |
| date1 = m.test_chrono1() | |
| date2 = datetime.datetime.today() | |
| # The returned value should be a datetime | |
| assert isinstance(date1, datetime.datetime) | |
| # The numbers should vary by a very small amount (time it took to execute) | |
| diff_python = abs(date2 - date0) | |
| diff = abs(date1 - date2) | |
| # There should never be a days difference | |
| assert diff.days == 0 | |
| # Since datetime.datetime.today() calls time.time(), and on some platforms | |
| # that has 1 second accuracy, we compare this way | |
| assert diff.seconds <= diff_python.seconds | |
| def test_chrono_system_clock_roundtrip(): | |
| date1 = datetime.datetime.today() | |
| # Roundtrip the time | |
| date2 = m.test_chrono2(date1) | |
| # The returned value should be a datetime | |
| assert isinstance(date2, datetime.datetime) | |
| # They should be identical (no information lost on roundtrip) | |
| diff = abs(date1 - date2) | |
| assert diff == datetime.timedelta(0) | |
| @pytest.mark.parametrize("day", [1, 2]) | |
| def test_chrono_system_clock_roundtrip_epoch(day): | |
| # Python's fold-aware naive timestamp implementation probes the previous | |
| # day, which Windows cannot represent before the epoch. | |
| value = datetime.datetime(1970, 1, day, 12) | |
| assert m.test_chrono2(value) == value | |
| def test_chrono_system_clock_roundtrip_date(): | |
| date1 = datetime.date.today() | |
| # Roundtrip the time | |
| datetime2 = m.test_chrono2(date1) | |
| date2 = datetime2.date() | |
| time2 = datetime2.time() | |
| # The returned value should be a datetime | |
| assert isinstance(datetime2, datetime.datetime) | |
| assert isinstance(date2, datetime.date) | |
| assert isinstance(time2, datetime.time) | |
| # They should be identical (no information lost on roundtrip) | |
| diff = abs(date1 - date2) | |
| assert diff.days == 0 | |
| assert diff.seconds == 0 | |
| assert diff.microseconds == 0 | |
| # Year, Month & Day should be the same after the round trip | |
| assert date1 == date2 | |
| # There should be no time information | |
| assert time2.hour == 0 | |
| assert time2.minute == 0 | |
| assert time2.second == 0 | |
| assert time2.microsecond == 0 | |
| @pytest.fixture | |
| def local_timezone(monkeypatch): | |
| if not hasattr(time, "tzset"): | |
| pytest.skip("Changing the local timezone requires time.tzset") | |
| if env.GRAALPY and sys.graalpy_version_info < (25, 1): | |
| pytest.skip("GraalPy before 25.1 does not update its timezone after tzset") | |
| try: | |
| with monkeypatch.context() as patch: | |
| def set_timezone(tz): | |
| patch.setenv("TZ", tz) | |
| time.tzset() | |
| yield set_timezone | |
| finally: | |
| time.tzset() | |
| def epoch_us(utc): | |
| delta = utc - datetime.datetime(1970, 1, 1) | |
| return (delta.days * 86400 + delta.seconds) * 1_000_000 + delta.microseconds | |
| # POSIX rules avoid a dependency on the host's IANA timezone data. GraalPy's | |
| # Java timezone parser requires named zones instead of POSIX rule strings. | |
| PACIFIC_TZ = "America/Los_Angeles" if env.GRAALPY else "PST8PDT,M3.2.0/2,M11.1.0/2" | |
| HALF_HOUR_TZ = ( | |
| "Australia/Lord_Howe" if env.GRAALPY else "XST-10:30XDT-11,M10.1.0/2,M4.1.0/2" | |
| ) | |
| DST_CASES = [ | |
| pytest.param( | |
| PACIFIC_TZ, | |
| datetime.datetime(2026, 11, 1, 1, 30, 42, 123456, fold=0), | |
| datetime.timedelta(hours=-7), | |
| 0, | |
| id="fall-first", | |
| ), | |
| pytest.param( | |
| PACIFIC_TZ, | |
| datetime.datetime(2026, 11, 1, 1, 30, 42, 123456, fold=1), | |
| datetime.timedelta(hours=-8), | |
| 0, | |
| id="fall-second", | |
| ), | |
| pytest.param( | |
| PACIFIC_TZ, | |
| datetime.datetime(2026, 3, 8, 2, 30, 42, 654321, fold=0), | |
| datetime.timedelta(hours=-8), | |
| 1, | |
| id="spring-gap-first", | |
| ), | |
| pytest.param( | |
| PACIFIC_TZ, | |
| datetime.datetime(2026, 3, 8, 2, 30, 42, 654321, fold=1), | |
| datetime.timedelta(hours=-7), | |
| -1, | |
| id="spring-gap-second", | |
| ), | |
| pytest.param( | |
| HALF_HOUR_TZ, | |
| datetime.datetime(2026, 4, 5, 1, 45, 42, 123456, fold=0), | |
| datetime.timedelta(hours=11), | |
| 0, | |
| id="half-hour-first", | |
| ), | |
| pytest.param( | |
| HALF_HOUR_TZ, | |
| datetime.datetime(2026, 4, 5, 1, 45, 42, 123456, fold=1), | |
| datetime.timedelta(hours=10, minutes=30), | |
| 0, | |
| id="half-hour-second", | |
| ), | |
| ] | |
| @pytest.mark.parametrize(("tz", "local", "offset", "_gap_hours"), DST_CASES) | |
| def test_chrono_system_clock_load_dst(tz, local, offset, _gap_hours, local_timezone): | |
| local_timezone(tz) | |
| assert m.test_chrono_system_clock_as_us(local) == epoch_us(local - offset) | |
| @pytest.mark.parametrize(("tz", "local", "offset", "gap_hours"), DST_CASES) | |
| def test_chrono_system_clock_cast_dst(tz, local, offset, gap_hours, local_timezone): | |
| local_timezone(tz) | |
| value = epoch_us(local - offset) | |
| # Use known wall times rather than Python's conversion as the oracle, so | |
| # an unsupported TZ cannot make both conversions silently agree in UTC. | |
| expected = ( | |
| (local + datetime.timedelta(hours=gap_hours)).replace(fold=0) | |
| if gap_hours | |
| else local | |
| ) | |
| result = m.test_chrono_system_clock_from_us(value) | |
| assert result == expected | |
| # datetime equality ignores fold when comparing naive values. | |
| assert result.fold == expected.fold | |
| assert result.tzinfo is None | |
| @pytest.mark.parametrize("fold", [0, 1]) | |
| def test_chrono_system_clock_non_dst_fold(fold, local_timezone): | |
| local_timezone("Europe/Kyiv") | |
| first = epoch_us(datetime.datetime(1990, 6, 30, 21, 30)) | |
| second = first + 3600_000_000 | |
| # Both sides of this political offset change are daylight time. Skip if | |
| # the system lacks the corresponding IANA timezone data. | |
| for value in (first, second): | |
| parts = time.localtime(value // 1_000_000) | |
| if parts[:6] != (1990, 7, 1, 1, 30, 0) or parts.tm_isdst != 1: | |
| pytest.skip("Europe/Kyiv's 1990 offset change is unavailable") | |
| local = datetime.datetime(1990, 7, 1, 1, 30, fold=fold) | |
| value = first if fold == 0 else second | |
| assert m.test_chrono_system_clock_as_us(local) == value | |
| result = m.test_chrono_system_clock_from_us(value) | |
| assert result == local | |
| assert result.fold == fold | |
| @pytest.mark.parametrize( | |
| "value", | |
| [ | |
| -1_000_001, | |
| -1, | |
| 0, | |
| 1, | |
| 1_000_001, | |
| epoch_us(datetime.datetime(1900, 1, 1, 0, 0, 0, 123456)), | |
| epoch_us(datetime.datetime(2200, 1, 1)), | |
| epoch_us(datetime.datetime(2250, 1, 1, 0, 0, 0, 123456)), | |
| ], | |
| ) | |
| def test_chrono_system_clock_microseconds(value, local_timezone): | |
| local_timezone("UTC0") | |
| try: | |
| datetime.datetime.fromtimestamp(value // 1_000_000) | |
| except (OverflowError, OSError): | |
| pytest.skip("Timestamp is outside the platform's supported range") | |
| expected = datetime.datetime(1970, 1, 1) + datetime.timedelta(microseconds=value) | |
| assert m.test_chrono_system_clock_as_us(expected) == value | |
| assert m.test_chrono_system_clock_from_us(value) == expected | |
| class DatetimeWithOverriddenTimestamp(datetime.datetime): | |
| def timestamp(self): | |
| raise AssertionError("The system-clock caster should read datetime fields") | |
| @pytest.mark.parametrize( | |
| ("source", "expected"), | |
| [ | |
| (datetime.date(2026, 11, 1), datetime.datetime(2026, 11, 1)), | |
| ( | |
| datetime.time(12, 34, 56, 123456), | |
| datetime.datetime(1970, 1, 1, 12, 34, 56, 123456), | |
| ), | |
| ( | |
| datetime.time( | |
| 12, | |
| 34, | |
| 56, | |
| 123456, | |
| tzinfo=datetime.timezone(datetime.timedelta(hours=12)), | |
| fold=1, | |
| ), | |
| datetime.datetime(1970, 1, 1, 12, 34, 56, 123456), | |
| ), | |
| ( | |
| datetime.datetime( | |
| 2026, | |
| 11, | |
| 1, | |
| 1, | |
| 30, | |
| 42, | |
| 123456, | |
| tzinfo=datetime.timezone(datetime.timedelta(hours=12)), | |
| ), | |
| datetime.datetime(2026, 11, 1, 1, 30, 42, 123456), | |
| ), | |
| ( | |
| DatetimeWithOverriddenTimestamp(2026, 11, 1, 1, 30, 42, 123456), | |
| datetime.datetime(2026, 11, 1, 1, 30, 42, 123456), | |
| ), | |
| ], | |
| ) | |
| def test_chrono_system_clock_input_fields(source, expected, local_timezone): | |
| local_timezone("UTC0") | |
| assert m.test_chrono_system_clock_as_us(source) == epoch_us(expected) | |
| SKIP_TZ_ENV_ON_WIN = pytest.mark.skipif( | |
| "env.WIN", reason="TZ environment variable only supported on POSIX" | |
| ) | |
| @pytest.mark.parametrize( | |
| "time1", | |
| [ | |
| datetime.datetime.today().time(), | |
| datetime.time(0, 0, 0), | |
| datetime.time(0, 0, 0, 1), | |
| datetime.time(0, 28, 45, 109827), | |
| datetime.time(0, 59, 59, 999999), | |
| datetime.time(1, 0, 0), | |
| datetime.time(5, 59, 59, 0), | |
| datetime.time(5, 59, 59, 1), | |
| ], | |
| ) | |
| @pytest.mark.parametrize( | |
| "tz", | |
| [ | |
| None, | |
| pytest.param("Europe/Brussels", marks=SKIP_TZ_ENV_ON_WIN), | |
| pytest.param("Asia/Pyongyang", marks=SKIP_TZ_ENV_ON_WIN), | |
| pytest.param("America/New_York", marks=SKIP_TZ_ENV_ON_WIN), | |
| ], | |
| ) | |
| def test_chrono_system_clock_roundtrip_time(time1, tz, request): | |
| if tz is not None: | |
| local_tz = tz if env.GRAALPY else f"/usr/share/zoneinfo/{tz}" | |
| request.getfixturevalue("local_timezone")(local_tz) | |
| # Roundtrip the time | |
| datetime2 = m.test_chrono2(time1) | |
| date2 = datetime2.date() | |
| time2 = datetime2.time() | |
| # The returned value should be a datetime | |
| assert isinstance(datetime2, datetime.datetime) | |
| assert isinstance(date2, datetime.date) | |
| assert isinstance(time2, datetime.time) | |
| # Hour, Minute, Second & Microsecond should be the same after the round trip | |
| assert time1 == time2 | |
| # There should be no date information (i.e. date = python base date) | |
| assert date2.year == 1970 | |
| assert date2.month == 1 | |
| assert date2.day == 1 | |
| def test_chrono_duration_roundtrip(): | |
| # Get the difference between two times (a timedelta) | |
| date1 = datetime.datetime.today() | |
| date2 = datetime.datetime.today() | |
| diff = date2 - date1 | |
| # Make sure this is a timedelta | |
| assert isinstance(diff, datetime.timedelta) | |
| cpp_diff = m.test_chrono3(diff) | |
| assert cpp_diff == diff | |
| # Negative timedelta roundtrip | |
| diff = datetime.timedelta(microseconds=-1) | |
| cpp_diff = m.test_chrono3(diff) | |
| assert cpp_diff == diff | |
| def test_chrono_duration_subtraction_equivalence(): | |
| date1 = datetime.datetime.today() | |
| date2 = datetime.datetime.today() | |
| diff = date2 - date1 | |
| cpp_diff = m.test_chrono4(date2, date1) | |
| assert cpp_diff == diff | |
| def test_chrono_duration_subtraction_equivalence_date(): | |
| date1 = datetime.date.today() | |
| date2 = datetime.date.today() | |
| diff = date2 - date1 | |
| cpp_diff = m.test_chrono4(date2, date1) | |
| assert cpp_diff == diff | |
| def test_chrono_steady_clock(): | |
| time1 = m.test_chrono5() | |
| assert isinstance(time1, datetime.timedelta) | |
| def test_chrono_steady_clock_roundtrip(): | |
| time1 = datetime.timedelta(days=10, seconds=10, microseconds=100) | |
| time2 = m.test_chrono6(time1) | |
| assert isinstance(time2, datetime.timedelta) | |
| # They should be identical (no information lost on roundtrip) | |
| assert time1 == time2 | |
| def test_floating_point_duration(): | |
| # Test using a floating point number in seconds | |
| time = m.test_chrono7(35.525123) | |
| assert isinstance(time, datetime.timedelta) | |
| assert time.seconds == 35 | |
| assert 525122 <= time.microseconds <= 525123 | |
| diff = m.test_chrono_float_diff(43.789012, 1.123456) | |
| assert diff.seconds == 42 | |
| assert 665556 <= diff.microseconds <= 665557 | |
| def test_nano_timepoint(): | |
| time = datetime.datetime.now() | |
| time1 = m.test_nano_timepoint(time, datetime.timedelta(seconds=60)) | |
| assert time1 == time + datetime.timedelta(seconds=60) | |
| def test_chrono_different_resolutions(): | |
| resolutions = m.different_resolutions() | |
| time = datetime.datetime.now() | |
| resolutions.timestamp_h = time | |
| resolutions.timestamp_m = time | |
| resolutions.timestamp_s = time | |
| resolutions.timestamp_ms = time | |
| resolutions.timestamp_us = time |