Files
redmine-reporter/tests/test_report_builder.py
Артём Кокос da069993b9 Extract grouping logic into shared function (#19)
Add group_rows_by_project_and_version() to report_builder.py and replace
the duplicated inline grouping in both html.py and odt.py with a single
call to the shared function. This ensures consistent grouping behavior
across formatters and removes a maintenance burden — any change to
grouping logic now happens in one place.

Closes #19
2026-06-26 00:21:58 +07:00

225 lines
7.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from redmine_reporter.report_builder import (
STATUS_TRANSLATION,
build_grouped_report,
group_rows_by_project_and_version,
)
class MockIssue:
def __init__(self, project, subject, status, fixed_version=None, issue_id=999):
self.id = issue_id
self.project = project
self.subject = subject
self.status = status
if fixed_version is not None:
self.fixed_version = fixed_version
# -- Таблица переводов статусов --
def test_status_translation():
assert STATUS_TRANSLATION["Closed"] == "Закрыто"
assert STATUS_TRANSLATION["New"] == "В работе"
assert STATUS_TRANSLATION["Resolved"] == "Решена"
assert STATUS_TRANSLATION["Pending"] == "Ожидание"
def test_status_translation_unknown_passthrough():
"""Неизвестный статус возвращается как есть."""
from redmine_reporter.report_builder import STATUS_TRANSLATION
assert "SomeNewStatus" not in STATUS_TRANSLATION
# build_grouped_report вернёт оригинальное значение
issue = MockIssue("P", "S", "SomeNewStatus", "v1.0", 1)
rows = build_grouped_report([(issue, 1.0)])
assert rows[0]["status_ru"] == "SomeNewStatus"
# -- Основная логика группировки --
def test_build_grouped_report_grouping():
issues = [
(MockIssue("Камеры", "Фича A", "New", "v2.5.0", 101), 2.0),
(MockIssue("Камеры", "Баг B", "Resolved", "v2.5.0", 102), 1.5),
(MockIssue("ПО", "Доки", "Pending", None, 201), 4.0),
]
rows = build_grouped_report(issues)
assert len(rows) == 3
# Первая строка -- полное название проекта и версии
assert rows[0]["display_project"] == "Камеры"
assert rows[0]["display_version"] == "v2.5.0"
# Вторая -- пустые display_* из-за совпадения проекта+версии
assert rows[1]["display_project"] == ""
assert rows[1]["display_version"] == ""
# Третья -- новый проект
assert rows[2]["display_project"] == "ПО"
assert rows[2]["display_version"] == "<N/A>"
assert rows[0]["status_ru"] == "В работе"
assert rows[0]["time_text"] == ""
assert rows[1]["time_text"] == "1ч 30м"
def test_build_grouped_report_new_version_same_project():
"""Смена версии внутри одного проекта -- display_project пустой, display_version новая."""
issues = [
(MockIssue("Камеры", "Задача 1", "New", "v1.0", 1), 1.0),
(MockIssue("Камеры", "Задача 2", "New", "v2.0", 2), 1.0),
]
rows = build_grouped_report(issues)
assert rows[0]["display_project"] == "Камеры"
assert rows[0]["display_version"] == "v1.0"
assert rows[1]["display_project"] == ""
assert rows[1]["display_version"] == "v2.0"
def test_build_grouped_report_sorts_input():
"""Несортированный вход -- результат всё равно корректно сгруппирован."""
issues = [
(MockIssue("ПО", "Задача B", "New", "v1.0", 2), 1.0),
(MockIssue("Камеры", "Задача A", "New", "v1.0", 1), 2.0),
]
rows = build_grouped_report(issues)
# После сортировки "Камеры" < "ПО" (лексикографически по кириллице)
assert rows[0]["project"] == "Камеры"
assert rows[1]["project"] == "ПО"
# Оба display_project непустые -- разные проекты
assert rows[0]["display_project"] == "Камеры"
assert rows[1]["display_project"] == "ПО"
def test_build_grouped_report_empty():
"""Пустой вход -- пустой результат, без исключений."""
rows = build_grouped_report([])
assert rows == []
def test_build_grouped_report_no_time():
"""fill_time=False -- time_text пустой для всех строк."""
issues = [(MockIssue("P", "S", "New", "v1.0", 1), 3.5)]
rows = build_grouped_report(issues, fill_time=False)
assert rows[0]["time_text"] == ""
def test_build_grouped_report_preserves_issue_id_and_subject():
issues = [(MockIssue("P", "Моя задача", "Closed", "v1.0", 42), 0.5)]
rows = build_grouped_report(issues)
assert rows[0]["issue_id"] == 42
assert rows[0]["subject"] == "Моя задача"
def test_build_grouped_report_deterministic_order_within_group():
"""Задачи в одной группе проект+версия упорядочены по issue.id
независимо от порядка на входе (#33)."""
issues = [
(MockIssue("P", "Task C", "New", "v1.0", 103), 1.0),
(MockIssue("P", "Task A", "New", "v1.0", 101), 1.0),
(MockIssue("P", "Task B", "New", "v1.0", 102), 1.0),
]
rows = build_grouped_report(issues)
assert [r["issue_id"] for r in rows] == [101, 102, 103]
# -- #19: Общая функция группировки --
def test_group_rows_basic():
"""Два проекта, в одном две версии — структура корректна."""
rows = [
{
"project": "A",
"version": "v1",
"issue_id": 1,
"subject": "T1",
"status_ru": "S",
"time_text": "",
},
{
"project": "A",
"version": "v2",
"issue_id": 2,
"subject": "T2",
"status_ru": "S",
"time_text": "",
},
{
"project": "B",
"version": "v1",
"issue_id": 3,
"subject": "T3",
"status_ru": "S",
"time_text": "",
},
]
grouped = group_rows_by_project_and_version(rows)
assert list(grouped.keys()) == ["A", "B"]
assert list(grouped["A"].keys()) == ["v1", "v2"]
assert list(grouped["B"].keys()) == ["v1"]
assert len(grouped["A"]["v1"]) == 1
assert len(grouped["A"]["v2"]) == 1
assert len(grouped["B"]["v1"]) == 1
def test_group_rows_multiple_tasks_per_version():
"""Несколько задач в одной версии — все попадают в список."""
rows = [
{
"project": "A",
"version": "v1",
"issue_id": 1,
"subject": "T1",
"status_ru": "S",
"time_text": "",
},
{
"project": "A",
"version": "v1",
"issue_id": 2,
"subject": "T2",
"status_ru": "S",
"time_text": "",
},
{
"project": "A",
"version": "v1",
"issue_id": 3,
"subject": "T3",
"status_ru": "S",
"time_text": "",
},
]
grouped = group_rows_by_project_and_version(rows)
assert len(grouped["A"]["v1"]) == 3
assert [r["issue_id"] for r in grouped["A"]["v1"]] == [1, 2, 3]
def test_group_rows_preserves_row_data():
"""Строки в группировке — те же объекты, без потери данных."""
rows = [
{
"project": "P",
"version": "v1",
"issue_id": 42,
"subject": "Task",
"status_ru": "Готово",
"time_text": "",
},
]
grouped = group_rows_by_project_and_version(rows)
row = grouped["P"]["v1"][0]
assert row["issue_id"] == 42
assert row["subject"] == "Task"
assert row["status_ru"] == "Готово"
assert row["time_text"] == ""
def test_group_rows_empty():
"""Пустой список — пустой словарь."""
assert group_rows_by_project_and_version([]) == {}