From 1f77088c218cf7d34ba07e37abeae307eb8737d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D1=91=D0=BC=20=D0=9A=D0=BE=D0=BA=D0=BE?= =?UTF-8?q?=D1=81?= Date: Sun, 25 Jan 2026 11:19:04 +0700 Subject: [PATCH] Add tests for all formatters --- tests/test_cli.py | 20 ----- tests/test_formatter_odt.py | 73 --------------- tests/test_formatters.py | 175 ++++++++++++++++++++++++++++++++++++ 3 files changed, 175 insertions(+), 93 deletions(-) delete mode 100644 tests/test_cli.py delete mode 100644 tests/test_formatter_odt.py create mode 100644 tests/test_formatters.py diff --git a/tests/test_cli.py b/tests/test_cli.py deleted file mode 100644 index 4d8319e..0000000 --- a/tests/test_cli.py +++ /dev/null @@ -1,20 +0,0 @@ -import pytest -from redmine_reporter.cli import parse_date_range - - -def test_parse_date_range_valid(): - assert parse_date_range("2025-01-01--2025-12-31") == ("2025-01-01", "2025-12-31") - - -def test_parse_date_range_with_spaces(): - assert parse_date_range("2025-01-01 -- 2025-12-31") == ("2025-01-01", "2025-12-31") - - -def test_parse_date_range_invalid_no_separator(): - with pytest.raises(ValueError, match="must be in format"): - parse_date_range("2025-01-01") - - -def test_parse_date_range_invalid_parts(): - with pytest.raises(ValueError, match="Invalid date range format"): - parse_date_range("2025-01-01--") diff --git a/tests/test_formatter_odt.py b/tests/test_formatter_odt.py deleted file mode 100644 index 9cd5bb9..0000000 --- a/tests/test_formatter_odt.py +++ /dev/null @@ -1,73 +0,0 @@ -import tempfile -from types import SimpleNamespace -from redmine_reporter.formatter_odt import format_odt - - -def make_mock_issue(id_, project, subject, status, fixed_version=None): - """Создаёт лёгкий mock-объект, имитирующий Issue из redminelib.""" - - issue = SimpleNamespace() - issue.id = id_ - issue.project = project - issue.subject = subject - issue.status = status - - if fixed_version is not None: - issue.fixed_version = fixed_version - return issue - - -def test_format_odt_basic(): - issues = [ - (make_mock_issue(101, "Камеры", "Поддержка нового датчика", "In Progress", "v2.5.0"), 2.5), - (make_mock_issue(102, "Камеры", "Исправить утечку памяти", "Resolved", "v2.5.0"), 4.0), - (make_mock_issue(103, "ПО", "Обновить документацию", "Pending", None), 12.0), - ] - - doc = format_odt(issues) - - # Сохраняем и проверяем содержимое - with tempfile.NamedTemporaryFile(suffix=".odt") as tmp: - doc.save(tmp.name) - - # Проверяем, что файл - это ZIP (ODT основан на ZIP) - with open(tmp.name, "rb") as f: - assert f.read(2) == b"PK" - - # Извлекаем content.xml - import zipfile - with zipfile.ZipFile(tmp.name) as zf: - content_xml = zf.read("content.xml").decode("utf-8") - - # Проверяем заголовки - assert "Проект" in content_xml - assert "Версия" in content_xml - assert "Задача" in content_xml - assert "Статус" in content_xml - assert "Затрачено" in content_xml - - # Проверяем данные задач - assert "101. Поддержка нового датчика" in content_xml - assert "102. Исправить утечку памяти" in content_xml - assert "103. Обновить документацию" in content_xml - - # Проверяем проекты и версии - assert "Камеры" in content_xml - assert "ПО" in content_xml - assert "v2.5.0" in content_xml - assert "<N/A>" in content_xml or "" in content_xml # зависит от экранирования - - # Проверяем перевод статусов - assert "В работе" in content_xml # In Progress - assert "Решена" in content_xml # Resolved - assert "Ожидание" in content_xml # Pending - - # Проверяем формат времени - assert "2ч 30м" in content_xml - assert "4ч" in content_xml - assert "12ч" in content_xml - - # Проверяем группировку: "Камеры" должен встречаться только один раз явно - # (вторая строка — пустая ячейка) - cam_occurrences = content_xml.count(">Камеры<") - assert cam_occurrences == 1 diff --git a/tests/test_formatters.py b/tests/test_formatters.py new file mode 100644 index 0000000..3c42d39 --- /dev/null +++ b/tests/test_formatters.py @@ -0,0 +1,175 @@ +import pytest +import os +from typing import List +from redmine_reporter.types import ReportRow +from redmine_reporter.formatters.console import TableFormatter, CompactFormatter +from redmine_reporter.formatters.csv import CSVFormatter +from redmine_reporter.formatters.markdown import MarkdownFormatter +from redmine_reporter.formatters.odt import ODTFormatter +from odf.opendocument import OpenDocument + + +def make_fake_report_rows() -> List[ReportRow]: + """ + Генерирует фейковый отчёт с полным покрытием логики группировки: + - Проект A: версия v1.0 (2 задачи), версия v2.0 (1 задача) + - Проект B: версия (1 задача) + - Проект C: версия v1.0 (1 задача), версия v1.1 (2 задачи) + """ + + return [ + # Проект A, v1.0 + { + "project": "Проект A", + "version": "v1.0", + "display_project": "Проект A", + "display_version": "v1.0", + "issue_id": 101, + "subject": "Реализовать фичу X", + "status_ru": "В работе", + "time_text": "4ч 30м" + }, + { + "project": "Проект A", + "version": "v1.0", + "display_project": "", + "display_version": "", + "issue_id": 102, + "subject": "Исправить баг Y", + "status_ru": "Решена", + "time_text": "2ч" + }, + # Проект A, v2.0 + { + "project": "Проект A", + "version": "v2.0", + "display_project": "", + "display_version": "v2.0", + "issue_id": 103, + "subject": "Документация Z", + "status_ru": "Ожидание", + "time_text": "1ч" + }, + # Проект B, без версии + { + "project": "Проект B", + "version": "", + "display_project": "Проект B", + "display_version": "", + "issue_id": 201, + "subject": "Обновить README", + "status_ru": "Закрыто", + "time_text": "0ч" + }, + # Проект C, v1.0 + { + "project": "Проект C", + "version": "v1.0", + "display_project": "Проект C", + "display_version": "v1.0", + "issue_id": 301, + "subject": "Настроить CI", + "status_ru": "В работе", + "time_text": "3ч 15м" + }, + # Проект C, v1.1 + { + "project": "Проект C", + "version": "v1.1", + "display_project": "", + "display_version": "v1.1", + "issue_id": 302, + "subject": "Добавить тесты", + "status_ru": "В работе", + "time_text": "5ч" + }, + { + "project": "Проект C", + "version": "v1.1", + "display_project": "", + "display_version": "", + "issue_id": 303, + "subject": "Рефакторинг", + "status_ru": "Решена", + "time_text": "6ч 45м" + }, + ] + + +@pytest.fixture +def fake_rows(): + return make_fake_report_rows() + + +def _get_formatter_output_text(result): + """Преобразует результат форматтера в строку для проверки содержимого.""" + + if isinstance(result, OpenDocument): + return "" + elif isinstance(result, str): + return result + else: + raise TypeError(f"Unexpected formatter output type: {type(result)}") + + +FORMATTER_FACTORIES = [ + ("table", lambda: TableFormatter()), + ("compact", lambda: CompactFormatter()), + ("csv", lambda: CSVFormatter()), + ("markdown", lambda: MarkdownFormatter()), + ("odt", lambda: ODTFormatter(author="Тест Автор", from_date="2026-01-01", to_date="2026-01-31")), +] + + +@pytest.mark.parametrize("name, formatter_factory", FORMATTER_FACTORIES) +def test_formatter_does_not_crash(fake_rows, name, formatter_factory): + """Проверяем, что форматтер не падает на валидных данных.""" + + formatter = formatter_factory() + result = formatter.format(fake_rows) + + if name == "odt": + assert isinstance(result, OpenDocument) + else: + assert isinstance(result, str) + assert len(result.strip()) > 0 + + +@pytest.mark.parametrize("name, formatter_factory", FORMATTER_FACTORIES) +def test_formatter_contains_key_content(fake_rows, name, formatter_factory): + """Проверяем, что вывод содержит ключевые элементы.""" + + formatter = formatter_factory() + result = formatter.format(fake_rows) + + output_text = _get_formatter_output_text(result) + if not output_text: + return # Пропускаем ODT + + # Общие элементы + assert "Проект A" in output_text + assert "Проект B" in output_text + assert "В работе" in output_text + assert "" in output_text + assert "6ч 45м" in output_text + + # Специфика по форматам + if name == "csv": + # В CSV ID и subject — отдельные колонки + assert "101" in output_text + assert "Реализовать фичу X" in output_text + else: + # В остальных — вместе + assert "101. Реализовать фичу X" in output_text + + +def test_odt_save_creates_valid_file(fake_rows, tmp_path): + """Проверяем, что ODT можно сохранить и он открывается как ZIP.""" + + output_file = tmp_path / "report.odt" + formatter = ODTFormatter(author="Тест", from_date="2026-01-01", to_date="2026-01-31") + formatter.save(fake_rows, str(output_file)) + + assert output_file.exists() + with open(output_file, "rb") as f: + assert f.read(2) == b"PK" # сигнатура ZIP