Source code for tests.test_humanize
# Copyright Kevin Deldycke <[email protected]> and contributors.
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
"""Tests for the human-readable renderings of machine quantities."""
from __future__ import annotations
import time
from datetime import UTC, date, datetime, timedelta
import pytest
from repomatic.humanize import (
SECONDS_PER_DAY,
format_age,
format_countdown,
format_elapsed,
format_file_size,
format_relative,
parse_iso_datetime,
shift_months,
today_utc,
utc_midnight,
)
[docs]
@pytest.mark.parametrize(
("size_bytes", "expected"),
[
(0, "0 B"),
(512, "512 B"),
(1023, "1,023 B"),
(1024, "1.0 KB"),
(1536, "1.5 KB"),
(10240, "10.0 KB"),
(1048576, "1.0 MB"),
(1572864, "1.5 MB"),
(1073741824, "1.0 GB"),
],
)
def test_format_file_size(size_bytes: int, expected: str) -> None:
"""Human-readable file size formatting."""
assert format_file_size(size_bytes) == expected
[docs]
@pytest.mark.parametrize(
("days_ago", "expected"),
[
(0, "today"),
(1, "1 day"),
(2, "2 days"),
(30, "30 days"),
],
)
def test_format_age(days_ago: int, expected: str) -> None:
"""An mtime renders as whole days, singular for one."""
# Offset by a few seconds so a boundary case never rounds up mid-test.
mtime = time.time() - days_ago * SECONDS_PER_DAY - 5
assert format_age(mtime) == expected
def _utc(year, month, day, hour=0, minute=0):
"""Shorthand for an aware UTC instant."""
return datetime(year, month, day, hour, minute, tzinfo=UTC)
[docs]
def test_utc_midnight_starts_the_day() -> None:
"""A date carries no time of day, so it enters a countdown at midnight."""
assert utc_midnight(date(2026, 6, 21)) == _utc(2026, 6, 21)
[docs]
@pytest.mark.parametrize(
("instant", "months", "expected"),
[
(_utc(2026, 3, 31, 12), -1, _utc(2026, 2, 28, 12)),
(_utc(2024, 3, 31, 12), -1, _utc(2024, 2, 29, 12)),
(_utc(2026, 3, 31, 12), 1, _utc(2026, 4, 30, 12)),
(_utc(2026, 1, 15, 6), -13, _utc(2024, 12, 15, 6)),
(_utc(2026, 1, 15, 6), 0, _utc(2026, 1, 15, 6)),
],
)
def test_shift_months_clamps_the_day(instant, months, expected) -> None:
"""A month shift lands on the last day of a shorter target month."""
assert shift_months(instant, months) == expected
[docs]
@pytest.mark.parametrize(
("offset", "ahead", "behind"),
[
(timedelta(seconds=9), "just now", "just now"),
(timedelta(seconds=44), "in 44 seconds", "44 seconds ago"),
(timedelta(seconds=119), "in a minute", "a minute ago"),
(timedelta(minutes=2, seconds=59), "in 2 minutes", "2 minutes ago"),
(timedelta(minutes=59), "in 59 minutes", "59 minutes ago"),
(timedelta(hours=1, minutes=59), "in an hour", "an hour ago"),
# Truncated, never rounded: 4:59 is still 4 hours.
(timedelta(hours=4, minutes=59), "in 4 hours", "4 hours ago"),
(timedelta(hours=23, minutes=59), "in 23 hours", "23 hours ago"),
(timedelta(days=1, hours=23), "in a day", "a day ago"),
(timedelta(days=6, hours=23), "in 6 days", "6 days ago"),
(timedelta(days=13), "in a week", "a week ago"),
(timedelta(days=14), "in 2 weeks", "2 weeks ago"),
# Past a week, a remainder over 14 days counts as a calendar month.
(timedelta(days=21), "in a month", "a month ago"),
(timedelta(days=100), "in 3 months", "3 months ago"),
(timedelta(days=400), "in a year", "a year ago"),
(timedelta(days=800), "in 2 years", "2 years ago"),
],
)
def test_format_relative_buckets(offset, ahead, behind) -> None:
"""Each gap falls in one bucket, phrased ahead of or behind the reference."""
reference = _utc(2026, 6, 21, 9)
assert format_relative(reference + offset, reference) == ahead
assert format_relative(reference - offset, reference) == behind
[docs]
@pytest.mark.parametrize(
("deadline", "reference", "expected"),
[
(_utc(2026, 6, 25), _utc(2026, 6, 21), "2026-06-25 (in 4 days)"),
# Hours left, not a whole day: the countdown says so instead of
# collapsing to `just now`, which reads as a deadline already passed.
(_utc(2026, 6, 21, 14), _utc(2026, 6, 21, 9), "2026-06-21 (in 5 hours)"),
(_utc(2026, 6, 21, 9, 30), _utc(2026, 6, 21, 9), "2026-06-21 (in 30 minutes)"),
# Already passed: the countdown would read as noise, so it is dropped.
(_utc(2026, 6, 20), _utc(2026, 6, 21), "2026-06-20"),
(_utc(2026, 6, 21, 9), _utc(2026, 6, 21, 9), "2026-06-21"),
],
)
def test_format_countdown(deadline, reference, expected) -> None:
"""A countdown follows the precision of the instants it is given."""
assert format_countdown(deadline, reference) == expected
[docs]
@pytest.mark.parametrize(
("instant", "reference", "expected"),
[
(_utc(2026, 6, 21), _utc(2026, 6, 25), "2026-06-21 (4 days ago)"),
# Hours, not a whole day: the mirror of the countdown case above.
(_utc(2026, 6, 21, 9), _utc(2026, 6, 21, 14), "2026-06-21 (5 hours ago)"),
(_utc(2026, 6, 21, 9), _utc(2026, 6, 21, 9), "2026-06-21 (just now)"),
# Still ahead: `in 4 days` would contradict the column this sits in.
(_utc(2026, 6, 25), _utc(2026, 6, 21), "2026-06-25"),
],
)
def test_format_elapsed(instant, reference, expected) -> None:
"""The past-facing mirror drops its phrase in the other direction."""
assert format_elapsed(instant, reference) == expected
[docs]
def test_today_utc_is_the_utc_date():
"""The date is read on the UTC clock, not on the local one."""
before = datetime.now(UTC).date()
today = today_utc()
assert before <= today <= datetime.now(UTC).date()
assert type(today) is date
[docs]
@pytest.mark.parametrize(
("value", "expected"),
(
(None, None),
("", None),
("not-a-date", None),
("2026-06-24T10:00:00Z", "2026-06-24T10:00:00+00:00"),
# A `Z` suffix and a nanosecond fraction, truncated to microseconds.
("2026-06-24T10:00:00.123456789Z", "2026-06-24T10:00:00.123456+00:00"),
# No offset reads as UTC.
("2026-06-24T10:00:00", "2026-06-24T10:00:00+00:00"),
),
)
def test_parse_iso_datetime(value, expected):
"""An instant parses to an aware datetime; nothing, or garbage, to `None`."""
parsed = parse_iso_datetime(value)
assert (parsed.isoformat() if parsed else None) == expected