get_version() now returns version.name instead of str(version), which returned the numeric ID from redminelib.resources.Version. Falls back to str() when .name is absent (#14). Add issue.id as tertiary sort key in build_grouped_report so tasks within the same project+version group always appear in the same order regardless of API response ordering (#33). Remove redundant sort from client.fetch_issues_with_spent_time — the sort already runs in report_builder.build_grouped_report, so doing it twice wastes CPU. Also remove the now-unused get_version import (#32). Closes #14, closes #32, closes #33
113 lines
2.9 KiB
Python
113 lines
2.9 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"
|