feat(cli): add --repo and --exclude filtering (fixes #5)

Add argparse-based CLI parsing with repeatable --repo/-r (include) and
--exclude/-x (exclude) options. Filtering is case-insensitive substring
matching, applied post-fetch in collect_all() per ADR-005.

- parse_args() separated from main() for testability
- main(argv=None) accepts argv for test injection
- collect_all() gains optional include/exclude parameters
- 14 new tests (8 filtering + 6 CLI parsing/integration)
- All 51 tests pass, backward compatible (no args = v1.0.0 behavior)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
sylvain
2026-03-11 04:35:42 +01:00
parent 0f8e34edf3
commit 2232260821
5 changed files with 231 additions and 19 deletions

View File

@@ -22,10 +22,10 @@ class TestMainNominal:
mock_collect.return_value = []
with patch.dict("os.environ", env, clear=False):
main()
main([])
mock_client_cls.assert_called_once_with("http://localhost:3000", "test-token-123")
mock_collect.assert_called_once_with(mock_client)
mock_collect.assert_called_once_with(mock_client, include=None, exclude=None)
mock_render.assert_called_once_with(mock_collect.return_value)
@patch("gitea_dashboard.cli.render_dashboard")
@@ -38,7 +38,7 @@ class TestMainNominal:
mock_collect.return_value = []
with patch.dict("os.environ", env, clear=True):
main()
main([])
mock_client_cls.assert_called_once_with("http://192.168.0.106:3000", "my-token")
@@ -50,7 +50,7 @@ class TestMainMissingToken:
"""main() exits with code 1 and prints message mentioning GITEA_TOKEN."""
with patch.dict("os.environ", {}, clear=True):
with pytest.raises(SystemExit) as exc_info:
main()
main([])
assert exc_info.value.code == 1
captured = capsys.readouterr()
@@ -70,7 +70,7 @@ class TestMainConnectionErrors:
with patch.dict("os.environ", env, clear=True):
with pytest.raises(SystemExit) as exc_info:
main()
main([])
assert exc_info.value.code == 1
@@ -84,7 +84,7 @@ class TestMainConnectionErrors:
with patch.dict("os.environ", env, clear=True):
with pytest.raises(SystemExit) as exc_info:
main()
main([])
assert exc_info.value.code == 1
@@ -98,7 +98,7 @@ class TestMainConnectionErrors:
with patch.dict("os.environ", env, clear=True):
with pytest.raises(SystemExit) as exc_info:
main()
main([])
assert exc_info.value.code == 1
@@ -120,8 +120,76 @@ class TestMainConnectionErrors:
with patch.dict("os.environ", env, clear=True):
mock_collect.side_effect = make_exc(_os.environ)
with pytest.raises(SystemExit):
main()
main([])
captured = capsys.readouterr()
assert env["GITEA_TOKEN"] not in captured.out
assert env["GITEA_TOKEN"] not in captured.err
class TestParseArgs:
"""Test parse_args function."""
def test_no_options(self):
"""No arguments returns None for both repo and exclude."""
from gitea_dashboard.cli import parse_args
args = parse_args([])
assert args.repo is None
assert args.exclude is None
def test_single_repo(self):
"""--repo foo returns repo=["foo"]."""
from gitea_dashboard.cli import parse_args
args = parse_args(["--repo", "foo"])
assert args.repo == ["foo"]
def test_multiple_repo(self):
"""--repo foo --repo bar returns repo=["foo", "bar"]."""
from gitea_dashboard.cli import parse_args
args = parse_args(["--repo", "foo", "--repo", "bar"])
assert args.repo == ["foo", "bar"]
def test_short_flags(self):
"""-r foo -x bar works like long forms."""
from gitea_dashboard.cli import parse_args
args = parse_args(["-r", "foo", "-x", "bar"])
assert args.repo == ["foo"]
assert args.exclude == ["bar"]
class TestMainWithFilters:
"""Test main() passes filters to collect_all."""
@patch("gitea_dashboard.cli.render_dashboard")
@patch("gitea_dashboard.cli.collect_all")
@patch("gitea_dashboard.cli.GiteaClient")
def test_main_passes_filters(self, mock_client_cls, mock_collect, mock_render):
"""main() passes include/exclude from CLI args to collect_all."""
env = {"GITEA_TOKEN": "test-token"}
mock_client = MagicMock()
mock_client_cls.return_value = mock_client
mock_collect.return_value = []
with patch.dict("os.environ", env, clear=True):
main(["--repo", "dash", "--exclude", "old"])
mock_collect.assert_called_once_with(mock_client, include=["dash"], exclude=["old"])
@patch("gitea_dashboard.cli.render_dashboard")
@patch("gitea_dashboard.cli.collect_all")
@patch("gitea_dashboard.cli.GiteaClient")
def test_main_no_filters_passes_none(self, mock_client_cls, mock_collect, mock_render):
"""Without options, collect_all is called with include=None, exclude=None."""
env = {"GITEA_TOKEN": "test-token"}
mock_client = MagicMock()
mock_client_cls.return_value = mock_client
mock_collect.return_value = []
with patch.dict("os.environ", env, clear=True):
main([])
mock_collect.assert_called_once_with(mock_client, include=None, exclude=None)