List open Redmine issues assigned to the current (or overridden) user, sorted by priority rank (P1..P5) then version due date with a stable id tie-break. Renders an OSC 8 hyperlinked console table on stdout and a counts/hours summary line on stderr; version due dates are fetched once per unique version, unavailable versions degrade to an empty due date. Also migrates the codebase to ruff 0.17.3 defaults (PEP 585/604 annotations, targeted noqa for intentional blind excepts and CLI root logging). Behavior-neutral: 408 tests pass with -W error.
131 lines
3.4 KiB
Python
131 lines
3.4 KiB
Python
from redmine_reporter.utils import (
|
||
get_month_name_from_range,
|
||
get_version,
|
||
hours_to_human,
|
||
)
|
||
|
||
|
||
def test_hours_to_human_zero():
|
||
assert hours_to_human(0) == "0ч"
|
||
assert hours_to_human(-1) == "0ч"
|
||
|
||
|
||
def test_hours_to_human_whole_hours():
|
||
assert hours_to_human(1.0) == "1ч"
|
||
assert hours_to_human(8.0) == "8ч"
|
||
|
||
|
||
def test_hours_to_human_minutes_only():
|
||
assert hours_to_human(0.75) == "45м"
|
||
assert hours_to_human(0.5) == "30м"
|
||
|
||
|
||
def test_hours_to_human_mixed():
|
||
assert hours_to_human(2.5) == "2ч 30м"
|
||
assert hours_to_human(1.5) == "1ч 30м"
|
||
|
||
|
||
def test_hours_to_human_rounding():
|
||
assert hours_to_human(3.1666) == "3ч 10м" # 190 минут -> 3ч 10м
|
||
|
||
|
||
def test_get_month_name_from_range():
|
||
assert get_month_name_from_range("2026-01-01", "2026-01-31") == "Январь"
|
||
assert get_month_name_from_range("2025-12-01", "2026-02-15") == "Февраль"
|
||
|
||
|
||
def test_get_month_name_from_range_all_months():
|
||
months = [
|
||
"Январь",
|
||
"Февраль",
|
||
"Март",
|
||
"Апрель",
|
||
"Май",
|
||
"Июнь",
|
||
"Июль",
|
||
"Август",
|
||
"Сентябрь",
|
||
"Октябрь",
|
||
"Ноябрь",
|
||
"Декабрь",
|
||
]
|
||
for i, name in enumerate(months, start=1):
|
||
to_date = f"2026-{i:02d}-01"
|
||
assert get_month_name_from_range("2026-01-01", to_date) == name
|
||
|
||
|
||
def test_get_month_name_from_range_invalid_fallback():
|
||
"""Невалидная дата -- возвращается 'Январь'."""
|
||
assert get_month_name_from_range("invalid", "also_invalid") == "Январь"
|
||
|
||
|
||
def test_get_version_with_attribute():
|
||
class MockIssue:
|
||
fixed_version = "v2.5.0"
|
||
|
||
assert get_version(MockIssue()) == "v2.5.0"
|
||
|
||
|
||
def test_get_version_without_attribute():
|
||
class MockIssue:
|
||
pass
|
||
|
||
assert get_version(MockIssue()) == "<N/A>"
|
||
|
||
|
||
def test_get_version_none_attribute():
|
||
class MockIssue:
|
||
fixed_version = None
|
||
|
||
assert get_version(MockIssue()) == "<N/A>"
|
||
|
||
|
||
def test_get_version_with_redminelib_version_object():
|
||
"""redminelib Version: str() возвращает ID, .name — человекочитаемое имя."""
|
||
|
||
class MockVersion:
|
||
"""Имитирует redminelib.resources.Version — str() даёт ID."""
|
||
|
||
def __init__(self, vid, name):
|
||
self.id = vid
|
||
self.name = name
|
||
|
||
def __str__(self):
|
||
return str(self.id)
|
||
|
||
class MockIssue:
|
||
fixed_version = MockVersion(42, "v2.5.0")
|
||
|
||
assert get_version(MockIssue()) == "v2.5.0"
|
||
|
||
|
||
def test_get_version_falls_back_to_str_when_no_name():
|
||
"""Если у объекта версии нет .name — fallback на str()."""
|
||
|
||
class MockVersionNoName:
|
||
def __str__(self):
|
||
return "fallback-id"
|
||
|
||
class MockIssue:
|
||
fixed_version = MockVersionNoName()
|
||
|
||
assert get_version(MockIssue()) == "fallback-id"
|
||
|
||
|
||
def test_get_version_custom_default():
|
||
"""Явный default заменяет <N/A>, когда версия не назначена."""
|
||
|
||
class MockIssue:
|
||
pass
|
||
|
||
assert get_version(MockIssue(), default="") == ""
|
||
|
||
|
||
def test_get_version_custom_default_not_used_when_version_present():
|
||
"""При наличии версии default не применяется."""
|
||
|
||
class MockIssue:
|
||
fixed_version = "v1.0"
|
||
|
||
assert get_version(MockIssue(), default="") == "v1.0"
|