219 lines
7.4 KiB
Python
219 lines
7.4 KiB
Python
import pytest
|
||
import io
|
||
from typing import List
|
||
from unittest import mock
|
||
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, OpenDocumentText
|
||
|
||
|
||
def _make_empty_odt_bytes() -> bytes:
|
||
"""Создаёт минимальный валидный ODT-документ в памяти."""
|
||
doc = OpenDocumentText()
|
||
buf = io.BytesIO()
|
||
doc.save(buf)
|
||
return buf.getvalue()
|
||
|
||
|
||
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 [
|
||
{
|
||
"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ч",
|
||
},
|
||
{
|
||
"project": "Проект A",
|
||
"version": "v2.0",
|
||
"display_project": "",
|
||
"display_version": "v2.0",
|
||
"issue_id": 103,
|
||
"subject": "Документация Z",
|
||
"status_ru": "Ожидание",
|
||
"time_text": "1ч",
|
||
},
|
||
{
|
||
"project": "Проект B",
|
||
"version": "<N/A>",
|
||
"display_project": "Проект B",
|
||
"display_version": "<N/A>",
|
||
"issue_id": 201,
|
||
"subject": "Обновить README",
|
||
"status_ru": "Закрыто",
|
||
"time_text": "0ч",
|
||
},
|
||
{
|
||
"project": "Проект C",
|
||
"version": "v1.0",
|
||
"display_project": "Проект C",
|
||
"display_version": "v1.0",
|
||
"issue_id": 301,
|
||
"subject": "Настроить CI",
|
||
"status_ru": "В работе",
|
||
"time_text": "3ч 15м",
|
||
},
|
||
{
|
||
"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()
|
||
|
||
|
||
@pytest.fixture
|
||
def odt_formatter():
|
||
"""ODTFormatter с замоканной загрузкой шаблона."""
|
||
odt_bytes = _make_empty_odt_bytes()
|
||
mock_file = mock.MagicMock()
|
||
mock_file.__enter__ = mock.MagicMock(return_value=io.BytesIO(odt_bytes))
|
||
mock_file.__exit__ = mock.MagicMock(return_value=False)
|
||
mock_path = mock.MagicMock()
|
||
mock_path.open.return_value = mock_file
|
||
|
||
with mock.patch(
|
||
"redmine_reporter.formatters.odt.resources.files",
|
||
return_value=mock.MagicMock(
|
||
joinpath=mock.MagicMock(
|
||
return_value=mock.MagicMock(open=mock.MagicMock(return_value=mock_file))
|
||
)
|
||
),
|
||
):
|
||
yield ODTFormatter(
|
||
author="Тест Автор", from_date="2026-01-01", to_date="2026-01-31"
|
||
)
|
||
|
||
|
||
# -- Параметризованные тесты текстовых форматтеров --
|
||
|
||
TEXT_FORMATTER_FACTORIES = [
|
||
("table", lambda: TableFormatter()),
|
||
("compact", lambda: CompactFormatter()),
|
||
("csv", lambda: CSVFormatter()),
|
||
("markdown", lambda: MarkdownFormatter()),
|
||
]
|
||
|
||
|
||
@pytest.mark.parametrize("name, factory", TEXT_FORMATTER_FACTORIES)
|
||
def test_text_formatter_returns_nonempty_string(fake_rows, name, factory):
|
||
"""Текстовые форматтеры возвращают непустую строку."""
|
||
result = factory().format(fake_rows)
|
||
assert isinstance(result, str)
|
||
assert len(result.strip()) > 0
|
||
|
||
|
||
@pytest.mark.parametrize("name, factory", TEXT_FORMATTER_FACTORIES)
|
||
def test_text_formatter_contains_key_content(fake_rows, name, factory):
|
||
"""Вывод содержит ключевые данные из отчёта."""
|
||
output = factory().format(fake_rows)
|
||
|
||
assert "Проект A" in output
|
||
assert "Проект B" in output
|
||
assert "В работе" in output
|
||
assert "<N/A>" in output
|
||
assert "6ч 45м" in output
|
||
|
||
if name == "csv":
|
||
# В CSV issue_id и subject -- отдельные колонки
|
||
assert "101" in output
|
||
assert "Реализовать фичу X" in output
|
||
else:
|
||
assert "101. Реализовать фичу X" in output
|
||
|
||
|
||
@pytest.mark.parametrize("name, factory", TEXT_FORMATTER_FACTORIES)
|
||
def test_text_formatter_empty_rows(name, factory):
|
||
"""Форматтер не падает на пустом списке строк."""
|
||
result = factory().format([])
|
||
assert isinstance(result, str)
|
||
|
||
|
||
# -- Тесты консольных форматтеров --
|
||
|
||
|
||
def test_table_formatter_save_raises(fake_rows):
|
||
with pytest.raises(NotImplementedError):
|
||
TableFormatter().save(fake_rows, "/dev/null")
|
||
|
||
|
||
def test_compact_formatter_save_raises(fake_rows):
|
||
with pytest.raises(NotImplementedError):
|
||
CompactFormatter().save(fake_rows, "/dev/null")
|
||
|
||
|
||
# -- Тесты ODT форматтера --
|
||
|
||
|
||
def test_odt_formatter_returns_opendocument(fake_rows, odt_formatter):
|
||
"""ODTFormatter.format() возвращает объект OpenDocument."""
|
||
result = odt_formatter.format(fake_rows)
|
||
assert isinstance(result, OpenDocument)
|
||
|
||
|
||
def test_odt_formatter_save_creates_valid_file(fake_rows, tmp_path):
|
||
"""ODT можно сохранить -- файл валиден (сигнатура ZIP)."""
|
||
odt_bytes = _make_empty_odt_bytes()
|
||
mock_file = mock.MagicMock()
|
||
mock_file.__enter__ = mock.MagicMock(return_value=io.BytesIO(odt_bytes))
|
||
mock_file.__exit__ = mock.MagicMock(return_value=False)
|
||
|
||
with mock.patch(
|
||
"redmine_reporter.formatters.odt.resources.files",
|
||
return_value=mock.MagicMock(
|
||
joinpath=mock.MagicMock(
|
||
return_value=mock.MagicMock(open=mock.MagicMock(return_value=mock_file))
|
||
)
|
||
),
|
||
):
|
||
formatter = ODTFormatter(
|
||
author="Тест", from_date="2026-01-01", to_date="2026-01-31"
|
||
)
|
||
output_file = tmp_path / "report.odt"
|
||
formatter.save(fake_rows, str(output_file))
|
||
|
||
assert output_file.exists()
|
||
assert output_file.read_bytes()[:2] == b"PK" # сигнатура ZIP
|