feat: add report.no_time config option for automatic modes
Add YAML section `report.no_time` that controls `--no-time` behavior in automatic modes (`--commit`, `--send`). CLI flag `--no-time` always wins. Manual `--output` ignores the YAML setting. - Config.get_report_no_time() reads the YAML value (defaults to false) - cli._resolve_no_time() encapsulates CLI > YAML priority - --init-config now generates the report section - docs updated in README.md and docs/CONFIG.md Closes #49
This commit is contained in:
@@ -17,7 +17,7 @@ CLI-инструмент для генерации отчётов по зада
|
|||||||
- Экспорт в ODT, CSV, Markdown, HTML, JSON и Excel (.xlsx).
|
- Экспорт в ODT, CSV, Markdown, HTML, JSON и Excel (.xlsx).
|
||||||
- Excel-отчёт с merge-ячейками по проекту/версии, итогами, автошириной, автофильтром и закреплённой шапкой.
|
- Excel-отчёт с merge-ячейками по проекту/версии, итогами, автошириной, автофильтром и закреплённой шапкой.
|
||||||
- Сводка по времени (`--summary`).
|
- Сводка по времени (`--summary`).
|
||||||
- YAML-конфиг (`~/.config/redmine-reporter/config.yml`): шаблон имени файла, путь по умолчанию, период, email.
|
- YAML-конфиг (`~/.config/redmine-reporter/config.yml`): шаблон имени файла, путь по умолчанию, период, email, настройки содержимого отчёта (`report.no_time`).
|
||||||
- Умное разрешение `--output`: bare-формат (`xlsx`) → путь по шаблону, без расширения → автодописывание.
|
- Умное разрешение `--output`: bare-формат (`xlsx`) → путь по шаблону, без расширения → автодописывание.
|
||||||
- `--commit`: автосохранение отчёта в файл + фиксация периода в YAML-конфиге для следующего запуска.
|
- `--commit`: автосохранение отчёта в файл + фиксация периода в YAML-конфиге для следующего запуска.
|
||||||
- `--send`: отправка отчёта по email через SMTP сразу после генерации.
|
- `--send`: отправка отчёта по email через SMTP сразу после генерации.
|
||||||
@@ -80,6 +80,9 @@ output:
|
|||||||
filename: "{author}_{from}_{to}.{ext}"
|
filename: "{author}_{from}_{to}.{ext}"
|
||||||
default_format: xlsx
|
default_format: xlsx
|
||||||
|
|
||||||
|
report:
|
||||||
|
no_time: false
|
||||||
|
|
||||||
email:
|
email:
|
||||||
smtp:
|
smtp:
|
||||||
host: smtp.example.com
|
host: smtp.example.com
|
||||||
@@ -239,6 +242,8 @@ redmine-reporter --by-activity
|
|||||||
redmine-reporter --by-activity --summary
|
redmine-reporter --by-activity --summary
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`--no-time` можно задать в YAML-конфиге (`report.no_time: true`), чтобы автоматические режимы (`--commit`, `--send`) не включали затраченное время без явного флага. При ручном `--output` YAML-значение не применяется — только CLI-флаг `--no-time`.
|
||||||
|
|
||||||
Сводка:
|
Сводка:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -43,6 +43,9 @@ output:
|
|||||||
filename: "{author}_{from}_{to}.{ext}"
|
filename: "{author}_{from}_{to}.{ext}"
|
||||||
default_format: xlsx
|
default_format: xlsx
|
||||||
|
|
||||||
|
report:
|
||||||
|
no_time: false
|
||||||
|
|
||||||
email:
|
email:
|
||||||
smtp:
|
smtp:
|
||||||
host: smtp.example.com
|
host: smtp.example.com
|
||||||
@@ -273,6 +276,36 @@ email:
|
|||||||
**не запрещены** — если вписать `api_key: "abc123"` напрямую, система примет.
|
**не запрещены** — если вписать `api_key: "abc123"` напрямую, система примет.
|
||||||
Права `0600` — основная защита.
|
Права `0600` — основная защита.
|
||||||
|
|
||||||
|
### `report` — настройки содержимого отчёта
|
||||||
|
|
||||||
|
Секция управляет тем, что попадает в сгенерированный отчёт.
|
||||||
|
|
||||||
|
| Поле | Тип | По умолчанию | Описание |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `no_time` | bool | `false` | Не включать затраченное время в файл отчёта |
|
||||||
|
|
||||||
|
`report.no_time` применяется только в автоматических режимах (`--commit`, `--send`).
|
||||||
|
При ручном `--output` YAML-настройка игнорируется — там работает только CLI-флаг `--no-time`.
|
||||||
|
CLI-флаг `--no-time` всегда имеет приоритет над YAML.
|
||||||
|
|
||||||
|
**Примеры:**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
report:
|
||||||
|
no_time: true
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Автоматический режим: время не выводится
|
||||||
|
redmine-reporter --commit
|
||||||
|
|
||||||
|
# Ручной режим: report.no_time игнорируется, время выводится
|
||||||
|
redmine-reporter --output report.odt
|
||||||
|
|
||||||
|
# Ручной режим с явным флагом: время не выводится
|
||||||
|
redmine-reporter --output report.odt --no-time
|
||||||
|
```
|
||||||
|
|
||||||
## Разрешение выходного пути
|
## Разрешение выходного пути
|
||||||
|
|
||||||
Функция `resolve_output_path()` определяет итоговый путь к файлу:
|
Функция `resolve_output_path()` определяет итоговый путь к файлу:
|
||||||
|
|||||||
@@ -72,6 +72,9 @@ def _run_init_config(config_path: str, force: bool) -> int:
|
|||||||
"filename": "{author}_{from}_{to}.{ext}",
|
"filename": "{author}_{from}_{to}.{ext}",
|
||||||
"default_format": "xlsx",
|
"default_format": "xlsx",
|
||||||
},
|
},
|
||||||
|
"report": {
|
||||||
|
"no_time": False,
|
||||||
|
},
|
||||||
"email": {
|
"email": {
|
||||||
"smtp": {
|
"smtp": {
|
||||||
"host": "",
|
"host": "",
|
||||||
@@ -127,6 +130,21 @@ def _compute_dedup_cutoff() -> Optional[datetime]:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_no_time(cli_flag: bool, is_auto_mode: bool) -> bool:
|
||||||
|
"""Возвращает финальное значение no_time.
|
||||||
|
|
||||||
|
Приоритет:
|
||||||
|
1. CLI-флаг --no-time (всегда побеждает).
|
||||||
|
2. YAML report.no_time (только в автоматических режимах --commit/--send).
|
||||||
|
3. Иначе False.
|
||||||
|
"""
|
||||||
|
if cli_flag:
|
||||||
|
return True
|
||||||
|
if is_auto_mode:
|
||||||
|
return Config.get_report_no_time()
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _save_and_maybe_send(
|
def _save_and_maybe_send(
|
||||||
rows,
|
rows,
|
||||||
output_arg: str,
|
output_arg: str,
|
||||||
@@ -378,9 +396,12 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|||||||
|
|
||||||
print(f"✅ Total issues: {len(issue_hours)} [{date_arg}]", file=sys.stderr)
|
print(f"✅ Total issues: {len(issue_hours)} [{date_arg}]", file=sys.stderr)
|
||||||
|
|
||||||
|
is_auto_mode = args.commit or args.send
|
||||||
|
no_time = _resolve_no_time(args.no_time, is_auto_mode)
|
||||||
|
|
||||||
rows = build_grouped_report(
|
rows = build_grouped_report(
|
||||||
issue_hours,
|
issue_hours,
|
||||||
fill_time=not args.no_time,
|
fill_time=not no_time,
|
||||||
by_activity=args.by_activity,
|
by_activity=args.by_activity,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -434,7 +455,7 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|||||||
author=Config.get_author(args.author),
|
author=Config.get_author(args.author),
|
||||||
from_date=from_date,
|
from_date=from_date,
|
||||||
to_date=to_date,
|
to_date=to_date,
|
||||||
no_time=args.no_time,
|
no_time=no_time,
|
||||||
do_send=args.send,
|
do_send=args.send,
|
||||||
)
|
)
|
||||||
if ret != 0:
|
if ret != 0:
|
||||||
@@ -465,7 +486,7 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|||||||
author=Config.get_author(args.author),
|
author=Config.get_author(args.author),
|
||||||
from_date=from_date,
|
from_date=from_date,
|
||||||
to_date=to_date,
|
to_date=to_date,
|
||||||
no_time=args.no_time,
|
no_time=no_time,
|
||||||
do_send=True,
|
do_send=True,
|
||||||
)
|
)
|
||||||
if ret != 0:
|
if ret != 0:
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ class AppConfig:
|
|||||||
output_dir: str = ""
|
output_dir: str = ""
|
||||||
output_filename: str = "{author}_{from}_{to}.{ext}"
|
output_filename: str = "{author}_{from}_{to}.{ext}"
|
||||||
output_default_format: str = "xlsx"
|
output_default_format: str = "xlsx"
|
||||||
|
report_no_time: bool = False
|
||||||
email: EmailConfig = field(default_factory=EmailConfig)
|
email: EmailConfig = field(default_factory=EmailConfig)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -86,6 +87,7 @@ class AppConfig:
|
|||||||
"redmine",
|
"redmine",
|
||||||
"period",
|
"period",
|
||||||
"output",
|
"output",
|
||||||
|
"report",
|
||||||
"email",
|
"email",
|
||||||
}
|
}
|
||||||
for key in raw:
|
for key in raw:
|
||||||
@@ -108,6 +110,7 @@ class AppConfig:
|
|||||||
or "{author}_{from}_{to}.{ext}",
|
or "{author}_{from}_{to}.{ext}",
|
||||||
output_default_format=cls._resolve_str(raw, "output", "default_format")
|
output_default_format=cls._resolve_str(raw, "output", "default_format")
|
||||||
or "xlsx",
|
or "xlsx",
|
||||||
|
report_no_time=cls._resolve_bool(raw, "report", "no_time"),
|
||||||
email=cls._resolve_email(raw),
|
email=cls._resolve_email(raw),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -344,6 +347,13 @@ class Config:
|
|||||||
return cls._app.output_default_format or "xlsx"
|
return cls._app.output_default_format or "xlsx"
|
||||||
return "xlsx"
|
return "xlsx"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_report_no_time(cls) -> bool:
|
||||||
|
"""Возвращает report.no_time из YAML-конфига (по умолчанию False)."""
|
||||||
|
if cls._app:
|
||||||
|
return cls._app.report_no_time
|
||||||
|
return False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_email_config(cls) -> "EmailConfig | None":
|
def get_email_config(cls) -> "EmailConfig | None":
|
||||||
"""Возвращает EmailConfig из YAML-конфига или None, если не настроен."""
|
"""Возвращает EmailConfig из YAML-конфига или None, если не настроен."""
|
||||||
|
|||||||
@@ -403,6 +403,21 @@ class TestInitConfig:
|
|||||||
assert data["redmine"]["url"] == ""
|
assert data["redmine"]["url"] == ""
|
||||||
assert data["redmine"]["api_key"] == ""
|
assert data["redmine"]["api_key"] == ""
|
||||||
|
|
||||||
|
@mock.patch.dict(os.environ, VALID_ENV, clear=True)
|
||||||
|
def test_init_config_includes_report_section(self, tmp_path):
|
||||||
|
"""--init-config генерирует секцию report с no_time: false."""
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
config_path = tmp_path / "config.yml"
|
||||||
|
code = main(["--init-config", "--config-path", str(config_path)])
|
||||||
|
assert code == 0
|
||||||
|
|
||||||
|
with open(config_path) as fh:
|
||||||
|
data = yaml.safe_load(fh)
|
||||||
|
|
||||||
|
assert "report" in data
|
||||||
|
assert data["report"]["no_time"] is False
|
||||||
|
|
||||||
|
|
||||||
@mock.patch.dict(os.environ, {}, clear=True)
|
@mock.patch.dict(os.environ, {}, clear=True)
|
||||||
@mock.patch("redmine_reporter.cli.fetch_issues_with_spent_time")
|
@mock.patch("redmine_reporter.cli.fetch_issues_with_spent_time")
|
||||||
@@ -994,3 +1009,252 @@ class TestSendFlag:
|
|||||||
mock_fetch.return_value = None
|
mock_fetch.return_value = None
|
||||||
code = main(["--date", "2026-06-01--2026-06-30", "--send"])
|
code = main(["--date", "2026-06-01--2026-06-30", "--send"])
|
||||||
assert code == 0
|
assert code == 0
|
||||||
|
|
||||||
|
|
||||||
|
class TestResolveNoTime:
|
||||||
|
"""Tests for _resolve_no_time()."""
|
||||||
|
|
||||||
|
def test_cli_flag_wins_in_manual_mode(self):
|
||||||
|
"""CLI --no-time побеждает в ручном режиме."""
|
||||||
|
from redmine_reporter.cli import _resolve_no_time
|
||||||
|
|
||||||
|
assert _resolve_no_time(True, False) is True
|
||||||
|
|
||||||
|
def test_cli_flag_wins_in_auto_mode(self):
|
||||||
|
"""CLI --no-time побеждает в автоматическом режиме."""
|
||||||
|
from redmine_reporter.cli import _resolve_no_time
|
||||||
|
|
||||||
|
assert _resolve_no_time(True, True) is True
|
||||||
|
|
||||||
|
@mock.patch("redmine_reporter.cli.Config.get_report_no_time", return_value=True)
|
||||||
|
def test_yaml_applies_in_auto_mode(self, mock_get):
|
||||||
|
"""YAML report.no_time применяется в автоматическом режиме."""
|
||||||
|
from redmine_reporter.cli import _resolve_no_time
|
||||||
|
|
||||||
|
assert _resolve_no_time(False, True) is True
|
||||||
|
mock_get.assert_called_once()
|
||||||
|
|
||||||
|
def test_default_in_auto_mode(self):
|
||||||
|
"""Без YAML report.no_time в автоматическом режиме — False."""
|
||||||
|
from redmine_reporter.cli import _resolve_no_time
|
||||||
|
|
||||||
|
assert _resolve_no_time(False, True) is False
|
||||||
|
|
||||||
|
@mock.patch("redmine_reporter.cli.Config.get_report_no_time", return_value=True)
|
||||||
|
def test_yaml_ignored_in_manual_mode(self, mock_get):
|
||||||
|
"""YAML report.no_time игнорируется в ручном режиме --output."""
|
||||||
|
from redmine_reporter.cli import _resolve_no_time
|
||||||
|
|
||||||
|
assert _resolve_no_time(False, False) is False
|
||||||
|
mock_get.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
class TestReportNoTimeIntegration:
|
||||||
|
"""Интеграционные тесты для report.no_time из YAML."""
|
||||||
|
|
||||||
|
@mock.patch.dict(os.environ, VALID_ENV, clear=True)
|
||||||
|
@mock.patch("redmine_reporter.cli.fetch_issues_with_spent_time")
|
||||||
|
@mock.patch("redmine_reporter.cli.save_period_to_config")
|
||||||
|
def test_commit_with_report_no_time_true(self, mock_save, mock_fetch, tmp_path):
|
||||||
|
"""--commit с report.no_time: true передаёт no_time=True в форматтер."""
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
issue = _MockIssue()
|
||||||
|
mock_fetch.return_value = [(issue, 1.0)]
|
||||||
|
|
||||||
|
config_path = tmp_path / "config.yml"
|
||||||
|
config_path.write_text(
|
||||||
|
yaml.dump(
|
||||||
|
{
|
||||||
|
"redmine": {"url": "https://x.com", "api_key": "token"},
|
||||||
|
"report": {"no_time": True},
|
||||||
|
"period": {"precision": "date", "dynamic": True},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
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-06-01--2026-06-30",
|
||||||
|
"--commit",
|
||||||
|
"--output",
|
||||||
|
str(tmp_path / "report.xlsx"),
|
||||||
|
"--config-path",
|
||||||
|
str(config_path),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
assert code == 0
|
||||||
|
|
||||||
|
_, kwargs = mock_get.call_args
|
||||||
|
assert kwargs.get("no_time") is True
|
||||||
|
|
||||||
|
@mock.patch.dict(os.environ, VALID_ENV, clear=True)
|
||||||
|
@mock.patch("redmine_reporter.cli.fetch_issues_with_spent_time")
|
||||||
|
@mock.patch("redmine_reporter.cli.save_period_to_config")
|
||||||
|
def test_commit_with_report_no_time_false(self, mock_save, mock_fetch, tmp_path):
|
||||||
|
"""--commit без report.no_time передаёт no_time=False в форматтер."""
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
issue = _MockIssue()
|
||||||
|
mock_fetch.return_value = [(issue, 1.0)]
|
||||||
|
|
||||||
|
config_path = tmp_path / "config.yml"
|
||||||
|
config_path.write_text(
|
||||||
|
yaml.dump(
|
||||||
|
{
|
||||||
|
"redmine": {"url": "https://x.com", "api_key": "token"},
|
||||||
|
"report": {"no_time": False},
|
||||||
|
"period": {"precision": "date", "dynamic": True},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
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-06-01--2026-06-30",
|
||||||
|
"--commit",
|
||||||
|
"--output",
|
||||||
|
str(tmp_path / "report.xlsx"),
|
||||||
|
"--config-path",
|
||||||
|
str(config_path),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
assert code == 0
|
||||||
|
|
||||||
|
_, kwargs = mock_get.call_args
|
||||||
|
assert kwargs.get("no_time") is False
|
||||||
|
|
||||||
|
@mock.patch.dict(os.environ, VALID_ENV, clear=True)
|
||||||
|
@mock.patch("redmine_reporter.cli.fetch_issues_with_spent_time")
|
||||||
|
@mock.patch("redmine_reporter.cli.save_period_to_config")
|
||||||
|
def test_commit_cli_no_time_overrides_yaml(self, mock_save, mock_fetch, tmp_path):
|
||||||
|
"""--commit с --no-time побеждает report.no_time: false."""
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
issue = _MockIssue()
|
||||||
|
mock_fetch.return_value = [(issue, 1.0)]
|
||||||
|
|
||||||
|
config_path = tmp_path / "config.yml"
|
||||||
|
config_path.write_text(
|
||||||
|
yaml.dump(
|
||||||
|
{
|
||||||
|
"redmine": {"url": "https://x.com", "api_key": "token"},
|
||||||
|
"report": {"no_time": False},
|
||||||
|
"period": {"precision": "date", "dynamic": True},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
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-06-01--2026-06-30",
|
||||||
|
"--commit",
|
||||||
|
"--no-time",
|
||||||
|
"--output",
|
||||||
|
str(tmp_path / "report.xlsx"),
|
||||||
|
"--config-path",
|
||||||
|
str(config_path),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
assert code == 0
|
||||||
|
|
||||||
|
_, kwargs = mock_get.call_args
|
||||||
|
assert kwargs.get("no_time") is True
|
||||||
|
|
||||||
|
@mock.patch.dict(os.environ, VALID_ENV, clear=True)
|
||||||
|
@mock.patch("redmine_reporter.cli.fetch_issues_with_spent_time")
|
||||||
|
def test_output_ignores_report_no_time_true(self, mock_fetch, tmp_path):
|
||||||
|
"""--output (ручной режим) игнорирует report.no_time: true."""
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
issue = _MockIssue()
|
||||||
|
mock_fetch.return_value = [(issue, 1.0)]
|
||||||
|
|
||||||
|
config_path = tmp_path / "config.yml"
|
||||||
|
config_path.write_text(
|
||||||
|
yaml.dump(
|
||||||
|
{
|
||||||
|
"redmine": {"url": "https://x.com", "api_key": "token"},
|
||||||
|
"report": {"no_time": True},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
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-06-01--2026-06-30",
|
||||||
|
"--output",
|
||||||
|
str(tmp_path / "report.xlsx"),
|
||||||
|
"--config-path",
|
||||||
|
str(config_path),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
assert code == 0
|
||||||
|
|
||||||
|
_, kwargs = mock_get.call_args
|
||||||
|
assert kwargs.get("no_time") is False
|
||||||
|
|
||||||
|
@mock.patch.dict(os.environ, VALID_ENV, clear=True)
|
||||||
|
@mock.patch("redmine_reporter.cli.fetch_issues_with_spent_time")
|
||||||
|
@mock.patch("redmine_reporter.cli.send_report")
|
||||||
|
def test_send_with_report_no_time_true(self, mock_send, mock_fetch, tmp_path):
|
||||||
|
"""--send с report.no_time: true передаёт no_time=True в форматтер."""
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
issue = _MockIssue()
|
||||||
|
mock_fetch.return_value = [(issue, 1.0)]
|
||||||
|
|
||||||
|
config_path = tmp_path / "config.yml"
|
||||||
|
config_path.write_text(
|
||||||
|
yaml.dump(
|
||||||
|
{
|
||||||
|
"redmine": {"url": "https://x.com", "api_key": "token"},
|
||||||
|
"report": {"no_time": True},
|
||||||
|
"email": {
|
||||||
|
"smtp": {
|
||||||
|
"host": "smtp.example.com",
|
||||||
|
"port": 587,
|
||||||
|
"user": "bot",
|
||||||
|
"password": "secret",
|
||||||
|
},
|
||||||
|
"from": "bot@example.com",
|
||||||
|
"to": ["boss@example.com"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
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-06-01--2026-06-30",
|
||||||
|
"--send",
|
||||||
|
"--output",
|
||||||
|
str(tmp_path / "report.xlsx"),
|
||||||
|
"--config-path",
|
||||||
|
str(config_path),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
assert code == 0
|
||||||
|
|
||||||
|
_, kwargs = mock_get.call_args
|
||||||
|
assert kwargs.get("no_time") is True
|
||||||
|
|||||||
@@ -611,3 +611,43 @@ class TestGetEmailConfig:
|
|||||||
|
|
||||||
Config._app = AppConfig.from_yaml(yaml_path)
|
Config._app = AppConfig.from_yaml(yaml_path)
|
||||||
assert Config.get_email_config() is None
|
assert Config.get_email_config() is None
|
||||||
|
|
||||||
|
|
||||||
|
class TestReportNoTime:
|
||||||
|
"""Tests for Config.get_report_no_time()."""
|
||||||
|
|
||||||
|
@mock.patch.dict(os.environ, {}, clear=True)
|
||||||
|
def test_get_report_no_time_from_yaml(self):
|
||||||
|
"""report.no_time: true загружается из YAML."""
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
yaml_path = Path(tmp) / "config.yml"
|
||||||
|
yaml_path.write_text("report:\n no_time: true\n")
|
||||||
|
|
||||||
|
Config._app = AppConfig.from_yaml(yaml_path)
|
||||||
|
assert Config.get_report_no_time() is True
|
||||||
|
|
||||||
|
@mock.patch.dict(os.environ, {}, clear=True)
|
||||||
|
def test_get_report_no_time_defaults_to_false(self):
|
||||||
|
"""Без YAML-конфига report.no_time по умолчанию False."""
|
||||||
|
Config._app = None
|
||||||
|
assert Config.get_report_no_time() is False
|
||||||
|
|
||||||
|
@mock.patch.dict(os.environ, {}, clear=True)
|
||||||
|
def test_get_report_no_time_from_yaml_false(self):
|
||||||
|
"""report.no_time: false явно загружается из YAML."""
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
yaml_path = Path(tmp) / "config.yml"
|
||||||
|
yaml_path.write_text("report:\n no_time: false\n")
|
||||||
|
|
||||||
|
Config._app = AppConfig.from_yaml(yaml_path)
|
||||||
|
assert Config.get_report_no_time() is False
|
||||||
|
|
||||||
|
@mock.patch.dict(os.environ, {}, clear=True)
|
||||||
|
def test_get_report_no_time_invalid_type_defaults_to_false(self):
|
||||||
|
"""report.no_time со строковым значением приводится к False."""
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
yaml_path = Path(tmp) / "config.yml"
|
||||||
|
yaml_path.write_text("report:\n no_time: 'invalid'\n")
|
||||||
|
|
||||||
|
Config._app = AppConfig.from_yaml(yaml_path)
|
||||||
|
assert Config.get_report_no_time() is False
|
||||||
|
|||||||
Reference in New Issue
Block a user