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