Add tests for all formatters

This commit was merged in pull request #10.
This commit is contained in:
Артём Кокос
2026-01-25 11:19:04 +07:00
parent 8278864b01
commit 1f77088c21
3 changed files with 175 additions and 93 deletions

View File

@@ -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--")

View File

@@ -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 "&lt;N/A&gt;" in content_xml or "<N/A>" 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 "" in content_xml
assert "12ч" in content_xml
# Проверяем группировку: "Камеры" должен встречаться только один раз явно
# (вторая строка — пустая ячейка)
cam_occurrences = content_xml.count(">Камеры<")
assert cam_occurrences == 1

175
tests/test_formatters.py Normal file
View File

@@ -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: версия <N/A> (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": ""
},
# Проект A, v2.0
{
"project": "Проект A",
"version": "v2.0",
"display_project": "",
"display_version": "v2.0",
"issue_id": 103,
"subject": "Документация Z",
"status_ru": "Ожидание",
"time_text": ""
},
# Проект B, без версии
{
"project": "Проект B",
"version": "<N/A>",
"display_project": "Проект B",
"display_version": "<N/A>",
"issue_id": 201,
"subject": "Обновить README",
"status_ru": "Закрыто",
"time_text": ""
},
# Проект 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": ""
},
{
"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 "<N/A>" 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