feat: add --my-tasks mode for open assigned issues (#71)
List open Redmine issues assigned to the current (or overridden) user, sorted by priority rank (P1..P5) then version due date with a stable id tie-break. Renders an OSC 8 hyperlinked console table on stdout and a counts/hours summary line on stderr; version due dates are fetched once per unique version, unavailable versions degrade to an empty due date. Also migrates the codebase to ruff 0.17.3 defaults (PEP 585/604 annotations, targeted noqa for intentional blind excepts and CLI root logging). Behavior-neutral: 408 tests pass with -W error.
This commit is contained in:
160
tests/test_task_table.py
Normal file
160
tests/test_task_table.py
Normal file
@@ -0,0 +1,160 @@
|
||||
"""Тесты консольного форматтера таблицы режима --my-tasks (#71)."""
|
||||
|
||||
import re
|
||||
from typing import cast
|
||||
|
||||
import pytest
|
||||
from redmine_reporter.formatters.task_table import TaskTableFormatter
|
||||
from redmine_reporter.types import TaskRow
|
||||
|
||||
BASE_URL = "https://red.eltex.loc"
|
||||
|
||||
# OSC 8-гиперссылка: esc-последовательность до текста, текст, закрытие ссылки.
|
||||
OSC8_RE = re.compile(r"\x1b\]8;;[^\x1b]*\x1b\\")
|
||||
|
||||
|
||||
def make_row(**overrides) -> TaskRow:
|
||||
row: TaskRow = {
|
||||
"issue_id": 42,
|
||||
"subject": "Починить баг",
|
||||
"status_ru": "Новая",
|
||||
"priority": "P2",
|
||||
"version_name": "v2.5.0",
|
||||
"due_text": "2026-10-15",
|
||||
"overdue": False,
|
||||
}
|
||||
# **-распаковка dict[str, Any] в TypedDict-литерале mypy не поддерживает,
|
||||
# а kwargs-словарь типизируется только как dict[str, Any], поэтому приведение.
|
||||
row.update(cast(TaskRow, overrides))
|
||||
return row
|
||||
|
||||
|
||||
def expected_hyperlink(issue_id, base_url=BASE_URL):
|
||||
return f"\x1b]8;;{base_url}/issues/{issue_id}\x1b\\{issue_id}\x1b]8;;\x1b\\"
|
||||
|
||||
|
||||
# -- OSC 8-гиперссылка на ID --
|
||||
|
||||
|
||||
def test_id_wrapped_in_exact_osc8_sequence():
|
||||
output = TaskTableFormatter(base_url=BASE_URL).format([make_row()])
|
||||
assert expected_hyperlink(42) in output
|
||||
|
||||
|
||||
def test_base_url_trailing_slash_not_duplicated():
|
||||
"""base_url с завершающим слэшем не даёт двойного слэша перед /issues."""
|
||||
output = TaskTableFormatter(base_url=BASE_URL + "/").format([make_row()])
|
||||
assert expected_hyperlink(42) in output
|
||||
assert "//issues/" not in output
|
||||
|
||||
|
||||
def test_hyperlink_text_is_issue_number_only():
|
||||
"""Текст ссылки -- только номер, тема не попадает внутрь OSC 8."""
|
||||
output = TaskTableFormatter(base_url=BASE_URL).format(
|
||||
[make_row(issue_id=7, subject="Тема задачи")]
|
||||
)
|
||||
assert expected_hyperlink(7) in output
|
||||
assert "Тема задачи" in output
|
||||
start = output.index("Тема задачи")
|
||||
assert "\x1b" not in output[:start].replace(expected_hyperlink(7), "")
|
||||
|
||||
|
||||
def test_hyperlink_uses_row_issue_id():
|
||||
output = TaskTableFormatter(base_url=BASE_URL).format([make_row(issue_id=1234)])
|
||||
assert expected_hyperlink(1234) in output
|
||||
|
||||
|
||||
# -- Заполнение пустых полей --
|
||||
|
||||
|
||||
def test_empty_version_rendered_as_dash():
|
||||
output = TaskTableFormatter(base_url=BASE_URL).format([make_row(version_name="")])
|
||||
assert "-" in output
|
||||
|
||||
|
||||
def test_empty_due_rendered_as_dash():
|
||||
output = TaskTableFormatter(base_url=BASE_URL).format([make_row(due_text="")])
|
||||
assert "-" in output
|
||||
|
||||
|
||||
def test_nonempty_fields_not_replaced_by_dash():
|
||||
output = TaskTableFormatter(base_url=BASE_URL).format([make_row()])
|
||||
assert "v2.5.0" in output
|
||||
assert "2026-10-15" in output
|
||||
|
||||
|
||||
# -- Маркер просрочки --
|
||||
|
||||
|
||||
def test_overdue_due_appends_marker():
|
||||
output = TaskTableFormatter(base_url=BASE_URL).format(
|
||||
[make_row(due_text="2026-10-15", overdue=True)]
|
||||
)
|
||||
assert "2026-10-15 (!)" in output
|
||||
|
||||
|
||||
def test_not_overdue_due_without_marker():
|
||||
output = TaskTableFormatter(base_url=BASE_URL).format(
|
||||
[make_row(due_text="2026-10-15", overdue=False)]
|
||||
)
|
||||
assert "2026-10-15" in output
|
||||
assert "(!)" not in output
|
||||
|
||||
|
||||
# -- Заголовки и содержимое --
|
||||
|
||||
|
||||
def test_column_headers_present():
|
||||
output = TaskTableFormatter(base_url=BASE_URL).format([make_row()])
|
||||
for header in ("ID", "Тема", "Статус", "Приор.", "Версия", "Срок"):
|
||||
assert header in output
|
||||
|
||||
|
||||
def test_status_passed_through_as_is():
|
||||
"""status_ru из TaskRow попадает в таблицу без изменений."""
|
||||
output = TaskTableFormatter(base_url=BASE_URL).format(
|
||||
[make_row(status_ru="В работе")]
|
||||
)
|
||||
assert "В работе" in output
|
||||
|
||||
|
||||
def test_subject_and_priority_present():
|
||||
output = TaskTableFormatter(base_url=BASE_URL).format(
|
||||
[make_row(subject="Обновить драйвер", priority="P1")]
|
||||
)
|
||||
assert "Обновить драйвер" in output
|
||||
assert "P1" in output
|
||||
|
||||
|
||||
def test_multiple_rows_rendered():
|
||||
rows = [make_row(issue_id=1), make_row(issue_id=2)]
|
||||
output = TaskTableFormatter(base_url=BASE_URL).format(rows)
|
||||
assert expected_hyperlink(1) in output
|
||||
assert expected_hyperlink(2) in output
|
||||
|
||||
|
||||
def test_empty_rows_renders_headers_only():
|
||||
output = TaskTableFormatter(base_url=BASE_URL).format([])
|
||||
assert "ID" in output
|
||||
assert expected_hyperlink(42) not in output
|
||||
|
||||
|
||||
# -- ESC только внутри ссылок --
|
||||
|
||||
|
||||
def test_esc_only_inside_hyperlinks():
|
||||
"""После вырезания OSC 8-ссылок в строке не остаётся ESC-символов."""
|
||||
rows = [
|
||||
make_row(issue_id=1),
|
||||
make_row(issue_id=2, version_name=""),
|
||||
make_row(issue_id=3, due_text="", overdue=True),
|
||||
]
|
||||
output = TaskTableFormatter(base_url=BASE_URL).format(rows)
|
||||
cleaned = OSC8_RE.sub("", output)
|
||||
assert "\x1b" not in cleaned
|
||||
|
||||
|
||||
def test_save_raises_not_implemented():
|
||||
formatter = TaskTableFormatter(base_url=BASE_URL)
|
||||
with pytest.raises(NotImplementedError):
|
||||
formatter.save([], "unused.xlsx")
|
||||
Reference in New Issue
Block a user