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:
@@ -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
|
||||
|
||||
@@ -611,3 +611,43 @@ class TestGetEmailConfig:
|
||||
|
||||
Config._app = AppConfig.from_yaml(yaml_path)
|
||||
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