Add tests for all formatters
This commit was merged in pull request #10.
This commit is contained in:
@@ -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--")
|
|
||||||
@@ -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 "<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 "4ч" in content_xml
|
|
||||||
assert "12ч" in content_xml
|
|
||||||
|
|
||||||
# Проверяем группировку: "Камеры" должен встречаться только один раз явно
|
|
||||||
# (вторая строка — пустая ячейка)
|
|
||||||
cam_occurrences = content_xml.count(">Камеры<")
|
|
||||||
assert cam_occurrences == 1
|
|
||||||
175
tests/test_formatters.py
Normal file
175
tests/test_formatters.py
Normal 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": "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": "<N/A>",
|
||||||
|
"display_project": "Проект B",
|
||||||
|
"display_version": "<N/A>",
|
||||||
|
"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 "<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
|
||||||
Reference in New Issue
Block a user