Quick fixes & tests

This commit is contained in:
Artem Kokos
2026-03-28 23:55:46 +07:00
parent 06cd57e2c4
commit 7bc6e024c0
13 changed files with 455 additions and 112 deletions

View File

@@ -1,12 +1,21 @@
import pytest
import os
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
from odf.opendocument import OpenDocument, newdoc
def _make_empty_odt_bytes() -> bytes:
"""Создаёт минимальный валидный ODT-документ в памяти."""
doc = newdoc(doctype="odt")
buf = io.BytesIO()
doc.save(buf)
return buf.getvalue()
def make_fake_report_rows() -> List[ReportRow]:
@@ -16,9 +25,7 @@ def make_fake_report_rows() -> List[ReportRow]:
- Проект B: версия <N/A> (1 задача)
- Проект C: версия v1.0 (1 задача), версия v1.1 (2 задачи)
"""
return [
# Проект A, v1.0
{
"project": "Проект A",
"version": "v1.0",
@@ -39,7 +46,6 @@ def make_fake_report_rows() -> List[ReportRow]:
"status_ru": "Решена",
"time_text": "",
},
# Проект A, v2.0
{
"project": "Проект A",
"version": "v2.0",
@@ -50,7 +56,6 @@ def make_fake_report_rows() -> List[ReportRow]:
"status_ru": "Ожидание",
"time_text": "",
},
# Проект B, без версии
{
"project": "Проект B",
"version": "<N/A>",
@@ -61,7 +66,6 @@ def make_fake_report_rows() -> List[ReportRow]:
"status_ru": "Закрыто",
"time_text": "",
},
# Проект C, v1.0
{
"project": "Проект C",
"version": "v1.0",
@@ -72,7 +76,6 @@ def make_fake_report_rows() -> List[ReportRow]:
"status_ru": "В работе",
"time_text": "3ч 15м",
},
# Проект C, v1.1
{
"project": "Проект C",
"version": "v1.1",
@@ -101,78 +104,111 @@ def fake_rows():
return make_fake_report_rows()
def _get_formatter_output_text(result):
"""Преобразует результат форматтера в строку для проверки содержимого."""
@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
if isinstance(result, OpenDocument):
return ""
elif isinstance(result, str):
return result
else:
raise TypeError(f"Unexpected formatter output type: {type(result)}")
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")
FORMATTER_FACTORIES = [
# -- Параметризованные тесты текстовых форматтеров --
TEXT_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, 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, formatter_factory", FORMATTER_FACTORIES)
def test_formatter_contains_key_content(fake_rows, name, formatter_factory):
"""Проверяем, что вывод содержит ключевые элементы."""
@pytest.mark.parametrize("name, factory", TEXT_FORMATTER_FACTORIES)
def test_text_formatter_contains_key_content(fake_rows, name, factory):
"""Вывод содержит ключевые данные из отчёта."""
output = factory().format(fake_rows)
formatter = formatter_factory()
result = formatter.format(fake_rows)
assert "Проект A" in output
assert "Проект B" in output
assert "В работе" in output
assert "<N/A>" in output
assert "6ч 45м" in output
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
# В CSV issue_id и subject -- отдельные колонки
assert "101" in output
assert "Реализовать фичу X" in output
else:
# В остальных — вместе
assert "101. Реализовать фичу X" in output_text
assert "101. Реализовать фичу X" in output
def test_odt_save_creates_valid_file(fake_rows, tmp_path):
"""Проверяем, что ODT можно сохранить и он открывается как ZIP."""
@pytest.mark.parametrize("name, factory", TEXT_FORMATTER_FACTORIES)
def test_text_formatter_empty_rows(name, factory):
"""Форматтер не падает на пустом списке строк."""
result = factory().format([])
assert isinstance(result, str)
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))
# -- Тесты консольных форматтеров --
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()
with open(output_file, "rb") as f:
assert f.read(2) == b"PK" # сигнатура ZIP
assert output_file.read_bytes()[:2] == b"PK" # сигнатура ZIP