#43 — Имя файла и пути по умолчанию: - resolve_output_path() в yaml_config.py: резолвит --output с учётом output.dir, filename_template, default_format из YAML-конфига - Bare format (xlsx/odt/...) → путь по шаблону - Без расширения → автодописывается default_format (.xlsx) - Config.get_output_dir/filename/default_format() #47 — datetime precision и дедупликация: - period.last_used.from/to в YAML-конфиге и AppConfig - period.precision: date|datetime - _compute_dedup_cutoff() в cli.py: при precision=datetime вычисляет cutoff из period.last_used.to - _parse_datetime() + AND-логика дедупликации в client.py: запись исключается если created_on И updated_on < cutoff - Пропуск issues без часов после дедупликации Closes #43 Closes #47
This commit is contained in:
@@ -97,13 +97,27 @@ def test_cli_unknown_output_extension(mock_fetch, tmp_path):
|
||||
|
||||
@mock.patch.dict(os.environ, VALID_ENV, clear=True)
|
||||
@mock.patch("redmine_reporter.cli.fetch_issues_with_spent_time")
|
||||
def test_cli_output_without_extension(mock_fetch, tmp_path):
|
||||
"""Файл без расширения -- выход 1 с подсказкой."""
|
||||
@mock.patch("redmine_reporter.cli.get_formatter_by_extension")
|
||||
def test_cli_output_without_extension(mock_get, mock_fetch, tmp_path):
|
||||
"""Файл без расширения — приложение расширяет default_format (#43)."""
|
||||
issue = _MockIssue()
|
||||
mock_fetch.return_value = [(issue, 1.0)]
|
||||
mock_formatter = mock.MagicMock()
|
||||
mock_get.return_value = mock_formatter
|
||||
output = str(tmp_path / "report")
|
||||
code = main(["--date", "2026-01-01--2026-01-31", "--output", output])
|
||||
assert code == 1
|
||||
code = main(
|
||||
[
|
||||
"--date",
|
||||
"2026-01-01--2026-01-31",
|
||||
"--output",
|
||||
output,
|
||||
"--config-path",
|
||||
str(tmp_path / "nonexistent.yml"),
|
||||
]
|
||||
)
|
||||
assert code == 0
|
||||
saved_path = mock_formatter.save.call_args.args[1]
|
||||
assert saved_path.endswith(".xlsx")
|
||||
|
||||
|
||||
@mock.patch.dict(os.environ, VALID_ENV, clear=True)
|
||||
@@ -407,3 +421,120 @@ def test_cli_autoloads_yaml_config(mock_fetch, tmp_path):
|
||||
mock_fetch.return_value = None
|
||||
code = main(["--date", "2026-01-01--2026-01-31", "--config-path", str(config_path)])
|
||||
assert code == 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# #43: --output path resolution tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestOutputPathResolution:
|
||||
"""Tests for --output flag with path defaults."""
|
||||
|
||||
@mock.patch.dict(os.environ, VALID_ENV, clear=True)
|
||||
@mock.patch("redmine_reporter.cli.fetch_issues_with_spent_time")
|
||||
def test_bare_format_name_xlsx_uses_template_path(self, mock_fetch, tmp_path):
|
||||
"""--output xlsx без пути → использует шаблон из конфига."""
|
||||
import yaml
|
||||
|
||||
config_path = tmp_path / "config.yml"
|
||||
config_path.write_text(
|
||||
yaml.dump(
|
||||
{
|
||||
"output": {
|
||||
"dir": str(tmp_path / "reports"),
|
||||
"filename": "report_{date}.{ext}",
|
||||
"default_format": "xlsx",
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
mock_fetch.return_value = None
|
||||
code = main(
|
||||
[
|
||||
"--date",
|
||||
"2026-01-01--2026-01-31",
|
||||
"--output",
|
||||
"xlsx",
|
||||
"--config-path",
|
||||
str(config_path),
|
||||
]
|
||||
)
|
||||
assert code == 0
|
||||
|
||||
@mock.patch.dict(os.environ, VALID_ENV, clear=True)
|
||||
@mock.patch("redmine_reporter.cli.fetch_issues_with_spent_time")
|
||||
def test_output_without_extension_appends_default_format(self, mock_fetch, tmp_path):
|
||||
"""--output report без расширения → добавляет .xlsx."""
|
||||
issue = _MockIssue()
|
||||
mock_fetch.return_value = [(issue, 1.0)]
|
||||
|
||||
output = str(tmp_path / "report")
|
||||
with mock.patch("redmine_reporter.cli.get_formatter_by_extension") as mock_get:
|
||||
mock_formatter = mock.MagicMock()
|
||||
mock_get.return_value = mock_formatter
|
||||
code = main(
|
||||
[
|
||||
"--date",
|
||||
"2026-01-01--2026-01-31",
|
||||
"--output",
|
||||
output,
|
||||
"--config-path",
|
||||
str(tmp_path / "nonexistent.yml"),
|
||||
]
|
||||
)
|
||||
assert code == 0
|
||||
mock_formatter.save.assert_called_once()
|
||||
saved_path = mock_formatter.save.call_args.args[1]
|
||||
assert saved_path.endswith(".xlsx")
|
||||
|
||||
@mock.patch.dict(os.environ, VALID_ENV, clear=True)
|
||||
@mock.patch("redmine_reporter.cli.fetch_issues_with_spent_time")
|
||||
def test_output_with_known_extension_unchanged(self, mock_fetch, tmp_path):
|
||||
"""--output report.csv с явным расширением передаётся как есть."""
|
||||
issue = _MockIssue()
|
||||
mock_fetch.return_value = [(issue, 1.0)]
|
||||
|
||||
with mock.patch("redmine_reporter.cli.get_formatter_by_extension") as mock_get:
|
||||
mock_formatter = mock.MagicMock()
|
||||
mock_get.return_value = mock_formatter
|
||||
code = main(
|
||||
[
|
||||
"--date",
|
||||
"2026-01-01--2026-01-31",
|
||||
"--output",
|
||||
str(tmp_path / "report.csv"),
|
||||
"--config-path",
|
||||
str(tmp_path / "nonexistent.yml"),
|
||||
]
|
||||
)
|
||||
assert code == 0
|
||||
mock_formatter.save.assert_called_once()
|
||||
saved_path = mock_formatter.save.call_args.args[1]
|
||||
assert saved_path.endswith(".csv")
|
||||
|
||||
@mock.patch.dict(os.environ, VALID_ENV, clear=True)
|
||||
@mock.patch("redmine_reporter.cli.fetch_issues_with_spent_time")
|
||||
def test_output_bare_format_invalid_is_treated_as_path(self, mock_fetch, tmp_path):
|
||||
"""--output notanxlsx (no path, not known format) → treated as path + .xlsx appended."""
|
||||
issue = _MockIssue()
|
||||
mock_fetch.return_value = [(issue, 1.0)]
|
||||
|
||||
with mock.patch("redmine_reporter.cli.get_formatter_by_extension") as mock_get:
|
||||
mock_formatter = mock.MagicMock()
|
||||
mock_get.return_value = mock_formatter
|
||||
code = main(
|
||||
[
|
||||
"--date",
|
||||
"2026-01-01--2026-01-31",
|
||||
"--output",
|
||||
"myreport",
|
||||
"--config-path",
|
||||
str(tmp_path / "nonexistent.yml"),
|
||||
]
|
||||
)
|
||||
assert code == 0
|
||||
mock_formatter.save.assert_called_once()
|
||||
saved_path = mock_formatter.save.call_args.args[1]
|
||||
assert saved_path == "myreport.xlsx"
|
||||
|
||||
@@ -435,3 +435,239 @@ def test_fetch_chunks_large_issue_count(mock_redmine_class):
|
||||
assert len(call_chunks[2].split(",")) == 50
|
||||
assert result is not None
|
||||
assert len(result) == 250
|
||||
|
||||
|
||||
# -- #47: Дедупликация time entries по datetime --
|
||||
|
||||
|
||||
@mock.patch.dict(os.environ, PASSWORD_ENV, clear=True)
|
||||
@mock.patch("redmine_reporter.client.Redmine")
|
||||
def test_dedup_filters_entries_created_before_cutoff(mock_redmine_class):
|
||||
"""Записи с created_on/updated_on раньше dedup_before исключаются."""
|
||||
from datetime import datetime, timezone
|
||||
|
||||
mock_redmine = mock_redmine_class.return_value
|
||||
_configure_current_user(mock_redmine, user_id=123)
|
||||
|
||||
dedup_cutoff = datetime(2026, 7, 1, 12, 0, 0, tzinfo=timezone.utc)
|
||||
|
||||
e1 = mock.MagicMock()
|
||||
e1.issue.id = 1
|
||||
e1.hours = 2.0
|
||||
e1.created_on = datetime(2026, 7, 1, 10, 0, 0, tzinfo=timezone.utc)
|
||||
e1.updated_on = datetime(2026, 7, 1, 10, 0, 0, tzinfo=timezone.utc)
|
||||
|
||||
e2 = mock.MagicMock()
|
||||
e2.issue.id = 2
|
||||
e2.hours = 1.0
|
||||
e2.created_on = datetime(2026, 7, 1, 14, 0, 0, tzinfo=timezone.utc)
|
||||
e2.updated_on = datetime(2026, 7, 1, 14, 0, 0, tzinfo=timezone.utc)
|
||||
|
||||
mock_redmine.time_entry.filter.return_value = [e1, e2]
|
||||
|
||||
mock_issue1 = mock.MagicMock()
|
||||
mock_issue1.id = 1
|
||||
mock_issue1.project = "P"
|
||||
mock_issue2 = mock.MagicMock()
|
||||
mock_issue2.id = 2
|
||||
mock_issue2.project = "P"
|
||||
mock_redmine.issue.filter.return_value = [mock_issue1, mock_issue2]
|
||||
|
||||
result = fetch_issues_with_spent_time("2026-07-01", "2026-07-01", dedup_before=dedup_cutoff)
|
||||
|
||||
assert result is not None
|
||||
assert len(result) == 1
|
||||
assert result[0][0].id == 2
|
||||
|
||||
|
||||
@mock.patch.dict(os.environ, PASSWORD_ENV, clear=True)
|
||||
@mock.patch("redmine_reporter.client.Redmine")
|
||||
def test_dedup_filters_entries_with_old_created_even_if_updated_recently(mock_redmine_class):
|
||||
"""Записи с created_on < cutoff исключаются даже при updated_on >= cutoff (AND-логика)."""
|
||||
from datetime import datetime, timezone
|
||||
|
||||
mock_redmine = mock_redmine_class.return_value
|
||||
_configure_current_user(mock_redmine, user_id=123)
|
||||
|
||||
dedup_cutoff = datetime(2026, 7, 1, 12, 0, 0, tzinfo=timezone.utc)
|
||||
|
||||
e1 = mock.MagicMock()
|
||||
e1.issue.id = 1
|
||||
e1.hours = 2.0
|
||||
e1.created_on = datetime(2026, 7, 1, 10, 0, 0, tzinfo=timezone.utc)
|
||||
e1.updated_on = datetime(2026, 7, 1, 15, 0, 0, tzinfo=timezone.utc)
|
||||
|
||||
mock_redmine.time_entry.filter.return_value = [e1]
|
||||
mock_redmine.issue.filter.return_value = []
|
||||
|
||||
result = fetch_issues_with_spent_time("2026-07-01", "2026-07-01", dedup_before=dedup_cutoff)
|
||||
|
||||
assert result is None
|
||||
|
||||
|
||||
@mock.patch.dict(os.environ, PASSWORD_ENV, clear=True)
|
||||
@mock.patch("redmine_reporter.client.Redmine")
|
||||
def test_dedup_keeps_entries_created_after_cutoff(mock_redmine_class):
|
||||
"""Записи с created_on/updated_on >= dedup_before сохраняются."""
|
||||
from datetime import datetime, timezone
|
||||
|
||||
mock_redmine = mock_redmine_class.return_value
|
||||
_configure_current_user(mock_redmine, user_id=123)
|
||||
|
||||
dedup_cutoff = datetime(2026, 7, 1, 12, 0, 0, tzinfo=timezone.utc)
|
||||
|
||||
e1 = mock.MagicMock()
|
||||
e1.issue.id = 1
|
||||
e1.hours = 2.0
|
||||
e1.created_on = datetime(2026, 7, 1, 12, 0, 0, tzinfo=timezone.utc)
|
||||
e1.updated_on = datetime(2026, 7, 1, 12, 0, 0, tzinfo=timezone.utc)
|
||||
|
||||
mock_redmine.time_entry.filter.return_value = [e1]
|
||||
|
||||
mock_issue1 = mock.MagicMock()
|
||||
mock_issue1.id = 1
|
||||
mock_issue1.project = "P"
|
||||
mock_issue1.subject = "T"
|
||||
mock_issue1.status = "New"
|
||||
mock_redmine.issue.filter.return_value = [mock_issue1]
|
||||
|
||||
result = fetch_issues_with_spent_time("2026-07-01", "2026-07-01", dedup_before=dedup_cutoff)
|
||||
|
||||
assert result is not None
|
||||
assert len(result) == 1
|
||||
|
||||
|
||||
@mock.patch.dict(os.environ, PASSWORD_ENV, clear=True)
|
||||
@mock.patch("redmine_reporter.client.Redmine")
|
||||
def test_dedup_entries_without_created_on_are_kept(mock_redmine_class):
|
||||
"""Записи без created_on/updated_on не фильтруются (сохраняются)."""
|
||||
from datetime import datetime, timezone
|
||||
|
||||
mock_redmine = mock_redmine_class.return_value
|
||||
_configure_current_user(mock_redmine, user_id=123)
|
||||
|
||||
dedup_cutoff = datetime(2026, 7, 1, 12, 0, 0, tzinfo=timezone.utc)
|
||||
|
||||
e1 = mock.MagicMock()
|
||||
e1.issue.id = 1
|
||||
e1.hours = 2.0
|
||||
e1.created_on = None
|
||||
e1.updated_on = None
|
||||
|
||||
mock_redmine.time_entry.filter.return_value = [e1]
|
||||
|
||||
mock_issue1 = mock.MagicMock()
|
||||
mock_issue1.id = 1
|
||||
mock_issue1.project = "P"
|
||||
mock_issue1.subject = "T"
|
||||
mock_issue1.status = "New"
|
||||
mock_redmine.issue.filter.return_value = [mock_issue1]
|
||||
|
||||
result = fetch_issues_with_spent_time("2026-07-01", "2026-07-01", dedup_before=dedup_cutoff)
|
||||
|
||||
assert result is not None
|
||||
assert len(result) == 1
|
||||
|
||||
|
||||
@mock.patch.dict(os.environ, PASSWORD_ENV, clear=True)
|
||||
@mock.patch("redmine_reporter.client.Redmine")
|
||||
def test_dedup_handles_string_created_on(mock_redmine_class):
|
||||
"""created_on может быть строкой ISO — парсим корректно."""
|
||||
from datetime import datetime, timezone
|
||||
|
||||
mock_redmine = mock_redmine_class.return_value
|
||||
_configure_current_user(mock_redmine, user_id=123)
|
||||
|
||||
dedup_cutoff = datetime(2026, 7, 1, 12, 0, 0, tzinfo=timezone.utc)
|
||||
|
||||
e1 = mock.MagicMock()
|
||||
e1.issue.id = 1
|
||||
e1.hours = 2.0
|
||||
e1.created_on = "2026-07-01T10:00:00Z"
|
||||
e1.updated_on = "2026-07-01T10:00:00Z"
|
||||
|
||||
mock_redmine.time_entry.filter.return_value = [e1]
|
||||
mock_redmine.issue.filter.return_value = []
|
||||
|
||||
result = fetch_issues_with_spent_time("2026-07-01", "2026-07-01", dedup_before=dedup_cutoff)
|
||||
|
||||
assert result is None
|
||||
|
||||
|
||||
@mock.patch.dict(os.environ, PASSWORD_ENV, clear=True)
|
||||
@mock.patch("redmine_reporter.client.Redmine")
|
||||
def test_no_dedup_when_cutoff_is_none(mock_redmine_class):
|
||||
"""Без dedup_before фильтрация не применяется."""
|
||||
mock_redmine = mock_redmine_class.return_value
|
||||
_configure_current_user(mock_redmine, user_id=123)
|
||||
|
||||
e1 = mock.MagicMock()
|
||||
e1.issue.id = 1
|
||||
e1.hours = 2.0
|
||||
e1.created_on = None
|
||||
e1.updated_on = None
|
||||
|
||||
mock_redmine.time_entry.filter.return_value = [e1]
|
||||
|
||||
mock_issue1 = mock.MagicMock()
|
||||
mock_issue1.id = 1
|
||||
mock_issue1.project = "P"
|
||||
mock_issue1.subject = "T"
|
||||
mock_issue1.status = "New"
|
||||
mock_redmine.issue.filter.return_value = [mock_issue1]
|
||||
|
||||
result = fetch_issues_with_spent_time("2026-07-01", "2026-07-01", dedup_before=None)
|
||||
|
||||
assert result is not None
|
||||
assert len(result) == 1
|
||||
|
||||
|
||||
@mock.patch.dict(os.environ, PASSWORD_ENV, clear=True)
|
||||
@mock.patch("redmine_reporter.client.Redmine")
|
||||
def test_dedup_mixed_entries_correct_filtering(mock_redmine_class):
|
||||
"""Смешанные записи: старые исключаются, новые сохраняются, аки идут."""
|
||||
from datetime import datetime, timezone
|
||||
|
||||
mock_redmine = mock_redmine_class.return_value
|
||||
_configure_current_user(mock_redmine, user_id=123)
|
||||
|
||||
dedup_cutoff = datetime(2026, 7, 1, 12, 0, 0, tzinfo=timezone.utc)
|
||||
|
||||
e_old = mock.MagicMock()
|
||||
e_old.issue.id = 10
|
||||
e_old.hours = 1.0
|
||||
e_old.created_on = datetime(2026, 7, 1, 9, 0, 0, tzinfo=timezone.utc)
|
||||
e_old.updated_on = datetime(2026, 7, 1, 9, 0, 0, tzinfo=timezone.utc)
|
||||
|
||||
e_new = mock.MagicMock()
|
||||
e_new.issue.id = 20
|
||||
e_new.hours = 3.0
|
||||
e_new.created_on = datetime(2026, 7, 1, 14, 0, 0, tzinfo=timezone.utc)
|
||||
e_new.updated_on = datetime(2026, 7, 1, 14, 0, 0, tzinfo=timezone.utc)
|
||||
|
||||
e_akiy = mock.MagicMock()
|
||||
e_akiy.issue.id = 30
|
||||
e_akiy.hours = 5.0
|
||||
e_akiy.created_on = None
|
||||
e_akiy.updated_on = None
|
||||
|
||||
mock_redmine.time_entry.filter.return_value = [e_old, e_new, e_akiy]
|
||||
|
||||
mock_issue_new = mock.MagicMock()
|
||||
mock_issue_new.id = 20
|
||||
mock_issue_new.project = "P"
|
||||
mock_issue_new.subject = "T"
|
||||
mock_issue_new.status = "New"
|
||||
mock_issue_akiy = mock.MagicMock()
|
||||
mock_issue_akiy.id = 30
|
||||
mock_issue_akiy.project = "P"
|
||||
mock_issue_akiy.subject = "T2"
|
||||
mock_issue_akiy.status = "New"
|
||||
mock_redmine.issue.filter.return_value = [mock_issue_new, mock_issue_akiy]
|
||||
|
||||
result = fetch_issues_with_spent_time("2026-07-01", "2026-07-01", dedup_before=dedup_cutoff)
|
||||
|
||||
assert result is not None
|
||||
assert len(result) == 2
|
||||
ids = {r[0].id for r in result}
|
||||
assert ids == {20, 30}
|
||||
|
||||
@@ -154,6 +154,9 @@ period:
|
||||
default_from: "2026-06-01"
|
||||
default_to: "2026-06-30"
|
||||
dynamic: true
|
||||
last_used:
|
||||
from: "2026-06-30T09:00:00"
|
||||
to: "2026-06-30T12:00:00"
|
||||
|
||||
output:
|
||||
dir: ~/reports
|
||||
@@ -196,6 +199,8 @@ class TestAppConfigFromYaml:
|
||||
assert cfg.period_default_from == "2026-06-01"
|
||||
assert cfg.period_default_to == "2026-06-30"
|
||||
assert cfg.period_dynamic is True
|
||||
assert cfg.period_last_used_from == "2026-06-30T09:00:00"
|
||||
assert cfg.period_last_used_to == "2026-06-30T12:00:00"
|
||||
assert cfg.output_dir == "~/reports"
|
||||
assert cfg.output_filename == "{author}_{from}_{to}.{ext}"
|
||||
assert cfg.output_default_format == "xlsx"
|
||||
@@ -387,3 +392,62 @@ class TestConfigYamlFallback:
|
||||
clear=True,
|
||||
):
|
||||
assert Config.get_default_date_range() == "2026-07-01--2026-07-15"
|
||||
|
||||
|
||||
class TestConfigPeriodPrecision:
|
||||
"""Tests for period.precision and period.last_used."""
|
||||
|
||||
@mock.patch.dict(os.environ, {}, clear=True)
|
||||
def test_get_period_precision_defaults_to_date(self):
|
||||
"""Без YAML-конфига precision == 'date'."""
|
||||
Config._app = None
|
||||
assert Config.get_period_precision() == "date"
|
||||
|
||||
@mock.patch.dict(os.environ, {}, clear=True)
|
||||
def test_get_period_precision_from_yaml(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
yaml_path = Path(tmp) / "config.yml"
|
||||
yaml_path.write_text("period:\n precision: datetime\n")
|
||||
|
||||
Config._app = AppConfig.from_yaml(yaml_path)
|
||||
assert Config.get_period_precision() == "datetime"
|
||||
|
||||
@mock.patch.dict(os.environ, {}, clear=True)
|
||||
def test_get_last_used_from_yaml(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
yaml_path = Path(tmp) / "config.yml"
|
||||
yaml_path.write_text(
|
||||
"period:\n last_used:\n from: '2026-06-30T09:00:00'\n to: '2026-06-30T12:00:00'\n"
|
||||
)
|
||||
|
||||
Config._app = AppConfig.from_yaml(yaml_path)
|
||||
assert Config.get_last_used_from() == "2026-06-30T09:00:00"
|
||||
assert Config.get_last_used_to() == "2026-06-30T12:00:00"
|
||||
|
||||
@mock.patch.dict(os.environ, {}, clear=True)
|
||||
def test_get_last_used_returns_empty_when_not_set(self):
|
||||
Config._app = AppConfig()
|
||||
assert Config.get_last_used_from() == ""
|
||||
assert Config.get_last_used_to() == ""
|
||||
|
||||
@mock.patch.dict(os.environ, {}, clear=True)
|
||||
def test_yaml_loads_last_used_field(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
yaml_path = Path(tmp) / "config.yml"
|
||||
yaml_path.write_text(
|
||||
"period:\n last_used:\n from: '2026-07-01T08:00:00'\n to: '2026-07-01T18:00:00'\n"
|
||||
)
|
||||
|
||||
cfg = AppConfig.from_yaml(yaml_path)
|
||||
assert cfg.period_last_used_from == "2026-07-01T08:00:00"
|
||||
assert cfg.period_last_used_to == "2026-07-01T18:00:00"
|
||||
|
||||
@mock.patch.dict(os.environ, {}, clear=True)
|
||||
def test_yaml_without_last_used_has_empty_fields(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
yaml_path = Path(tmp) / "config.yml"
|
||||
yaml_path.write_text("period:\n precision: date\n")
|
||||
|
||||
cfg = AppConfig.from_yaml(yaml_path)
|
||||
assert cfg.period_last_used_from == ""
|
||||
assert cfg.period_last_used_to == ""
|
||||
|
||||
@@ -122,3 +122,87 @@ class TestExpandFilenameTemplate:
|
||||
ext="xlsx",
|
||||
)
|
||||
assert result == "Кокос_А.А._{unknown}.xlsx"
|
||||
|
||||
|
||||
class TestResolveOutputPath:
|
||||
"""Tests for resolve_output_path()."""
|
||||
|
||||
def test_none_returns_none(self):
|
||||
from redmine_reporter.yaml_config import resolve_output_path
|
||||
|
||||
assert resolve_output_path(None) is None
|
||||
|
||||
def test_explicit_path_with_known_extension_is_unchanged(self):
|
||||
from redmine_reporter.yaml_config import resolve_output_path
|
||||
|
||||
result = resolve_output_path("/tmp/report.xlsx")
|
||||
assert result == "/tmp/report.xlsx"
|
||||
|
||||
def test_explicit_path_with_unknown_extension_is_unchanged(self):
|
||||
from redmine_reporter.yaml_config import resolve_output_path
|
||||
|
||||
result = resolve_output_path("report.odt")
|
||||
assert result == "report.odt"
|
||||
|
||||
def test_no_extension_appends_default_format(self):
|
||||
from redmine_reporter.yaml_config import resolve_output_path
|
||||
|
||||
result = resolve_output_path("report", default_format="xlsx")
|
||||
assert result == "report.xlsx"
|
||||
|
||||
def test_no_extension_appends_custom_default_format(self):
|
||||
from redmine_reporter.yaml_config import resolve_output_path
|
||||
|
||||
result = resolve_output_path("file", default_format="csv")
|
||||
assert result == "file.csv"
|
||||
|
||||
def test_bare_format_name_resolves_to_template_path(self):
|
||||
from redmine_reporter.yaml_config import resolve_output_path
|
||||
|
||||
result = resolve_output_path(
|
||||
"xlsx",
|
||||
output_dir="/tmp/reports",
|
||||
filename_template="{date}.{ext}",
|
||||
author="Кокос А.А.",
|
||||
from_date="2026-06-01",
|
||||
to_date="2026-06-30",
|
||||
default_format="xlsx",
|
||||
)
|
||||
assert result == "/tmp/reports/30_06_2026.xlsx"
|
||||
|
||||
def test_bare_format_name_odt_resolves_to_template_path(self):
|
||||
from redmine_reporter.yaml_config import resolve_output_path
|
||||
|
||||
result = resolve_output_path(
|
||||
"odt",
|
||||
output_dir="~/reports",
|
||||
filename_template="{author}_{from}_{to}.{ext}",
|
||||
author="Кокос А.А.",
|
||||
from_date="2026-06-01",
|
||||
to_date="2026-06-30",
|
||||
default_format="xlsx",
|
||||
)
|
||||
assert result.startswith("/") and result.endswith(
|
||||
"/reports/Кокос_А.А._2026-06-01_2026-06-30.odt"
|
||||
)
|
||||
assert "~" not in result
|
||||
|
||||
def test_bare_format_name_with_empty_output_dir_uses_cwd(self):
|
||||
from redmine_reporter.yaml_config import resolve_output_path
|
||||
|
||||
result = resolve_output_path(
|
||||
"csv",
|
||||
output_dir="",
|
||||
filename_template="report.{ext}",
|
||||
author="A",
|
||||
from_date="2026-01-01",
|
||||
to_date="2026-01-31",
|
||||
)
|
||||
assert not result.startswith("/")
|
||||
assert "report.csv" in result
|
||||
|
||||
def test_unknown_bare_format_is_treated_as_path(self):
|
||||
from redmine_reporter.yaml_config import resolve_output_path
|
||||
|
||||
result = resolve_output_path("unknown_format")
|
||||
assert result == "unknown_format.xlsx"
|
||||
|
||||
Reference in New Issue
Block a user