91 lines
2.9 KiB
Python
91 lines
2.9 KiB
Python
"""Tests for ``deerflow.utils.time``."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from datetime import UTC, datetime, timedelta, timezone
|
|
|
|
from deerflow.utils.time import coerce_iso, now_iso
|
|
|
|
_ISO_RE = re.compile(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}")
|
|
|
|
|
|
def test_now_iso_is_utc_iso8601() -> None:
|
|
value = now_iso()
|
|
assert _ISO_RE.match(value), value
|
|
parsed = datetime.fromisoformat(value)
|
|
assert parsed.tzinfo is not None
|
|
assert parsed.tzinfo.utcoffset(parsed) == UTC.utcoffset(parsed)
|
|
|
|
|
|
def test_coerce_iso_passes_iso_through() -> None:
|
|
iso = "2026-04-27T01:13:30.411334+00:00"
|
|
assert coerce_iso(iso) == iso
|
|
|
|
|
|
def test_coerce_iso_converts_unix_float_string() -> None:
|
|
legacy = "1777252410.411327"
|
|
out = coerce_iso(legacy)
|
|
assert _ISO_RE.match(out), out
|
|
# Round-trip: parsed timestamp matches the original epoch.
|
|
parsed = datetime.fromisoformat(out)
|
|
assert abs(parsed.timestamp() - 1777252410.411327) < 1e-3
|
|
|
|
|
|
def test_coerce_iso_converts_unix_int_string() -> None:
|
|
out = coerce_iso("1700000000")
|
|
assert _ISO_RE.match(out), out
|
|
|
|
|
|
def test_coerce_iso_converts_numeric_types() -> None:
|
|
out_float = coerce_iso(1777252410.411327)
|
|
out_int = coerce_iso(1700000000)
|
|
assert _ISO_RE.match(out_float)
|
|
assert _ISO_RE.match(out_int)
|
|
|
|
|
|
def test_coerce_iso_handles_empty_and_none() -> None:
|
|
assert coerce_iso(None) == ""
|
|
assert coerce_iso("") == ""
|
|
|
|
|
|
def test_coerce_iso_does_not_misinterpret_short_numeric() -> None:
|
|
# A 4-digit year should never be parsed as a unix timestamp; only
|
|
# 10-digit unix-second strings match the legacy pattern.
|
|
assert coerce_iso("2026") == "2026"
|
|
|
|
|
|
def test_coerce_iso_handles_unparseable_string() -> None:
|
|
assert coerce_iso("not-a-timestamp") == "not-a-timestamp"
|
|
|
|
|
|
def test_coerce_iso_rejects_bool() -> None:
|
|
# ``bool`` is a subclass of ``int`` — must not be treated as epoch 0/1.
|
|
assert coerce_iso(True) == "True"
|
|
assert coerce_iso(False) == "False"
|
|
|
|
|
|
def test_coerce_iso_handles_tz_aware_datetime() -> None:
|
|
# str(datetime) would emit a space separator; coerce_iso must use ``T``.
|
|
dt = datetime(2026, 4, 27, 1, 13, 30, 411327, tzinfo=UTC)
|
|
out = coerce_iso(dt)
|
|
assert out == "2026-04-27T01:13:30.411327+00:00"
|
|
assert "T" in out and " " not in out
|
|
|
|
|
|
def test_coerce_iso_handles_tz_naive_datetime_as_utc() -> None:
|
|
dt = datetime(2026, 4, 27, 1, 13, 30, 411327)
|
|
out = coerce_iso(dt)
|
|
assert out == "2026-04-27T01:13:30.411327+00:00"
|
|
parsed = datetime.fromisoformat(out)
|
|
assert parsed.tzinfo is not None
|
|
assert parsed.utcoffset() == timedelta(0)
|
|
|
|
|
|
def test_coerce_iso_normalises_non_utc_datetime_to_utc() -> None:
|
|
# +08:00 wall-clock 09:13 == UTC 01:13.
|
|
plus_eight = timezone(timedelta(hours=8))
|
|
dt = datetime(2026, 4, 27, 9, 13, 30, 411327, tzinfo=plus_eight)
|
|
out = coerce_iso(dt)
|
|
assert out == "2026-04-27T01:13:30.411327+00:00"
|