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:
Кокос Артем Николаевич
2026-07-10 14:38:07 +07:00
parent 5e1c366a60
commit 863ad50cc3
6 changed files with 377 additions and 4 deletions

View File

@@ -403,6 +403,21 @@ class TestInitConfig:
assert data["redmine"]["url"] == ""
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("redmine_reporter.cli.fetch_issues_with_spent_time")
@@ -994,3 +1009,252 @@ class TestSendFlag:
mock_fetch.return_value = None
code = main(["--date", "2026-06-01--2026-06-30", "--send"])
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