From ebf72c9a562dbd83484966ec806b888dbcc5fdc4 Mon Sep 17 00:00:00 2001 From: sylvain Date: Fri, 13 Mar 2026 03:46:09 +0100 Subject: [PATCH] test(v1.4.0-p2): add failing tests for milestones and columns RED phase: 5 tests in test_collector.py (collect_milestones), 10 tests in test_display.py (render_milestones, parse_columns), 2 tests in test_exporter.py (milestones JSON), 7 tests in test_cli.py (--milestones, --columns). Co-Authored-By: Claude Opus 4.6 --- tests/test_cli.py | 91 +++++++++++++++++++++++ tests/test_collector.py | 160 +++++++++++++++++++++++++++++++++++++++- tests/test_display.py | 127 +++++++++++++++++++++++++++++++ tests/test_exporter.py | 28 +++++++ 4 files changed, 405 insertions(+), 1 deletion(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index c910051..40b294f 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -389,3 +389,94 @@ class TestMainFormatJson: captured = capsys.readouterr() parsed = json.loads(captured.out) assert isinstance(parsed, list) + + +class TestParseArgsMilestones: + """Test --milestones argument parsing.""" + + def test_parse_args_milestones(self): + """--milestones sets milestones=True.""" + from gitea_dashboard.cli import parse_args + + args = parse_args(["--milestones"]) + assert args.milestones is True + + def test_parse_args_milestones_default(self): + """Without --milestones, milestones is False.""" + from gitea_dashboard.cli import parse_args + + args = parse_args([]) + assert args.milestones is False + + +class TestParseArgsColumns: + """Test --columns argument parsing.""" + + def test_parse_args_columns(self): + """--columns name,issues sets columns='name,issues'.""" + from gitea_dashboard.cli import parse_args + + args = parse_args(["--columns", "name,issues"]) + assert args.columns == "name,issues" + + def test_parse_args_columns_default(self): + """Without --columns, columns is None.""" + from gitea_dashboard.cli import parse_args + + args = parse_args([]) + assert args.columns is None + + +class TestMainMilestonesMode: + """Test main() with --milestones.""" + + @patch("gitea_dashboard.cli.render_milestones") + @patch("gitea_dashboard.cli.collect_milestones") + @patch("gitea_dashboard.cli.GiteaClient") + def test_main_milestones_mode(self, mock_client_cls, mock_collect_ms, mock_render_ms): + """--milestones routes to collect_milestones + render_milestones.""" + env = {"GITEA_TOKEN": "test-tok"} + mock_client_cls.return_value = MagicMock() + mock_collect_ms.return_value = [] + + with patch.dict("os.environ", env, clear=True): + main(["--milestones"]) + + mock_collect_ms.assert_called_once() + mock_render_ms.assert_called_once() + + +class TestMainColumnsHelp: + """Test main() with --columns help.""" + + @patch("gitea_dashboard.cli.GiteaClient") + def test_main_columns_help(self, mock_client_cls, capsys): + """--columns help displays available columns and exits.""" + env = {"GITEA_TOKEN": "test-tok"} + mock_client_cls.return_value = MagicMock() + + with patch.dict("os.environ", env, clear=True): + main(["--columns", "help"]) + + captured = capsys.readouterr() + # Should list column names + assert "name" in captured.out or "name" in captured.err + + @patch("gitea_dashboard.cli.render_dashboard") + @patch("gitea_dashboard.cli.collect_all") + @patch("gitea_dashboard.cli.GiteaClient") + def test_main_no_desc_and_columns_compat(self, mock_client_cls, mock_collect, mock_render): + """--no-desc and --columns -commit both apply cumulatively.""" + env = {"GITEA_TOKEN": "test-tok"} + mock_client_cls.return_value = MagicMock() + mock_collect.return_value = [] + + with patch.dict("os.environ", env, clear=True): + main(["--no-desc", "--columns", "-commit"]) + + # render_dashboard should be called with columns excluding both description and commit + call_kwargs = mock_render.call_args + columns = call_kwargs[1].get("columns") if call_kwargs[1] else None + if columns is not None: + assert "description" not in columns + assert "commit" not in columns diff --git a/tests/test_collector.py b/tests/test_collector.py index 3e4f9e5..7d67572 100644 --- a/tests/test_collector.py +++ b/tests/test_collector.py @@ -2,7 +2,10 @@ from unittest.mock import MagicMock -from gitea_dashboard.collector import RepoData, collect_all +from gitea_dashboard.collector import ( + RepoData, + collect_all, +) def _make_repo( @@ -311,3 +314,158 @@ class TestCollectAllFiltering: result_empty = collect_all(client, include=[]) assert [r.name for r in result_empty] == [r.name for r in result_none] + + +class TestCollectMilestones: + """Test collect_milestones function.""" + + def _setup_client(self, repo_names, milestones_by_repo=None): + """Create a mock client with repos and milestones.""" + client = MagicMock() + client.get_repos.return_value = [ + _make_repo(name=n, full_name=f"admin/{n}") for n in repo_names + ] + + if milestones_by_repo is None: + milestones_by_repo = {} + + def get_milestones_side_effect(owner, repo, state="all"): + return milestones_by_repo.get(repo, []) + + client.get_milestones.side_effect = get_milestones_side_effect + return client + + def test_collect_milestones_basic(self): + """2 repos with milestones returns flat list of MilestoneData.""" + from gitea_dashboard.collector import MilestoneData, collect_milestones + + client = self._setup_client( + ["repo-a", "repo-b"], + { + "repo-a": [ + { + "title": "v1.0", + "open_issues": 2, + "closed_issues": 3, + "due_on": None, + "state": "open", + }, + ], + "repo-b": [ + { + "title": "v2.0", + "open_issues": 0, + "closed_issues": 5, + "due_on": "2026-04-01T00:00:00Z", + "state": "closed", + }, + ], + }, + ) + + result = collect_milestones(client) + + assert len(result) == 2 + assert all(isinstance(m, MilestoneData) for m in result) + assert result[0].repo_name == "repo-a" + assert result[0].title == "v1.0" + assert result[1].repo_name == "repo-b" + + def test_collect_milestones_empty_repo(self): + """Repo without milestones produces no entries.""" + from gitea_dashboard.collector import collect_milestones + + client = self._setup_client(["empty-repo"], {"empty-repo": []}) + + result = collect_milestones(client) + + assert result == [] + + def test_collect_milestones_progress_calculation(self): + """3 open + 7 closed = progress_pct 70.""" + from gitea_dashboard.collector import collect_milestones + + client = self._setup_client( + ["repo"], + { + "repo": [ + { + "title": "v1.0", + "open_issues": 3, + "closed_issues": 7, + "due_on": None, + "state": "open", + }, + ], + }, + ) + + result = collect_milestones(client) + + assert result[0].progress_pct == 70 + + def test_collect_milestones_with_include_filter(self): + """Include filter is respected.""" + from gitea_dashboard.collector import collect_milestones + + client = self._setup_client( + ["gitea-dashboard", "infra"], + { + "gitea-dashboard": [ + { + "title": "v1.0", + "open_issues": 1, + "closed_issues": 1, + "due_on": None, + "state": "open", + }, + ], + "infra": [ + { + "title": "v2.0", + "open_issues": 0, + "closed_issues": 5, + "due_on": None, + "state": "closed", + }, + ], + }, + ) + + result = collect_milestones(client, include=["dashboard"]) + + assert len(result) == 1 + assert result[0].repo_name == "gitea-dashboard" + + def test_collect_milestones_with_exclude_filter(self): + """Exclude filter is respected.""" + from gitea_dashboard.collector import collect_milestones + + client = self._setup_client( + ["gitea-dashboard", "old-fork"], + { + "gitea-dashboard": [ + { + "title": "v1.0", + "open_issues": 1, + "closed_issues": 1, + "due_on": None, + "state": "open", + }, + ], + "old-fork": [ + { + "title": "v2.0", + "open_issues": 0, + "closed_issues": 5, + "due_on": None, + "state": "closed", + }, + ], + }, + ) + + result = collect_milestones(client, exclude=["fork"]) + + assert len(result) == 1 + assert result[0].repo_name == "gitea-dashboard" diff --git a/tests/test_display.py b/tests/test_display.py index 6197b37..9f5f553 100644 --- a/tests/test_display.py +++ b/tests/test_display.py @@ -401,3 +401,130 @@ class TestSortRepos: ] result = sort_repos(repos, "activity") assert [r.name for r in result] == ["recent", "old-commit", "inactive"] + + +class TestRenderMilestones: + """Test the dedicated milestones table rendering.""" + + def test_render_milestones_basic(self): + """Milestones table displays expected columns.""" + from gitea_dashboard.collector import MilestoneData + from gitea_dashboard.display import render_milestones + + console, buf = _make_console() + milestones = [ + MilestoneData( + repo_name="my-repo", + title="v1.0", + open_issues=2, + closed_issues=8, + progress_pct=80, + due_on="2026-04-01T00:00:00Z", + state="open", + ), + ] + + render_milestones(milestones, console=console) + output = buf.getvalue() + + assert "my-repo" in output + assert "v1.0" in output + assert "80" in output + + def test_render_milestones_empty(self): + """Empty list shows informative message.""" + from gitea_dashboard.display import render_milestones + + console, buf = _make_console() + + render_milestones([], console=console) + output = buf.getvalue() + + assert "Aucune milestone" in output + + def test_render_milestones_progress_colors(self): + """Progress coloring: green > 80%, yellow 50-80%, red < 50%.""" + from gitea_dashboard.collector import MilestoneData + from gitea_dashboard.display import render_milestones + + console, buf = _make_console() + milestones = [ + MilestoneData("repo", "high", 1, 9, 90, None, "open"), + MilestoneData("repo", "mid", 3, 3, 50, None, "open"), + MilestoneData("repo", "low", 8, 2, 20, None, "open"), + ] + + render_milestones(milestones, console=console) + output = buf.getvalue() + + # All three should appear without crash + assert "high" in output + assert "mid" in output + assert "low" in output + + +class TestParseColumns: + """Test parse_columns function.""" + + def test_parse_columns_all_default(self): + """None returns all columns.""" + from gitea_dashboard.display import AVAILABLE_COLUMNS, parse_columns + + result = parse_columns(None) + + assert result == list(AVAILABLE_COLUMNS.keys()) + + def test_parse_columns_inclusion(self): + """'name,issues' returns only those columns.""" + from gitea_dashboard.display import parse_columns + + result = parse_columns("name,issues") + + assert result == ["name", "issues"] + + def test_parse_columns_exclusion(self): + """'-description,-commit' returns all except those.""" + from gitea_dashboard.display import AVAILABLE_COLUMNS, parse_columns + + result = parse_columns("-description,-commit") + + assert "description" not in result + assert "commit" not in result + assert len(result) == len(AVAILABLE_COLUMNS) - 2 + + def test_parse_columns_unknown_raises(self): + """Unknown column raises ValueError.""" + from gitea_dashboard.display import parse_columns + + with pytest.raises(ValueError, match="unknown"): + parse_columns("unknown") + + def test_parse_columns_help(self): + """'help' returns sentinel list.""" + from gitea_dashboard.display import parse_columns + + result = parse_columns("help") + + assert result == ["__help__"] + + def test_parse_columns_no_desc_compat(self): + """no_desc=True excludes description column.""" + from gitea_dashboard.display import parse_columns + + result = parse_columns(None, no_desc=True) + + assert "description" not in result + + def test_render_dashboard_with_columns(self): + """Only specified columns appear in the output.""" + from gitea_dashboard.display import render_dashboard + + console, buf = _make_console() + repos = [_make_repo(name="test", open_issues=5)] + + render_dashboard(repos, console=console, columns=["name", "issues"]) + output = buf.getvalue() + + assert "test" in output + assert "Description" not in output + assert "Release" not in output diff --git a/tests/test_exporter.py b/tests/test_exporter.py index 0a40bc9..ed7e9aa 100644 --- a/tests/test_exporter.py +++ b/tests/test_exporter.py @@ -138,3 +138,31 @@ class TestExportJson: """Empty repo list produces '[]'.""" output = export_json([]) assert json.loads(output) == [] + + +class TestExportMilestonesJson: + """Test milestones JSON export.""" + + def test_export_milestones_json_basic(self): + """MilestoneData list produces valid JSON.""" + from gitea_dashboard.collector import MilestoneData + from gitea_dashboard.exporter import export_milestones_json + + milestones = [ + MilestoneData("repo", "v1.0", 2, 8, 80, "2026-04-01T00:00:00Z", "open"), + ] + + output = export_milestones_json(milestones) + parsed = json.loads(output) + + assert len(parsed) == 1 + assert parsed[0]["repo_name"] == "repo" + assert parsed[0]["title"] == "v1.0" + assert parsed[0]["progress_pct"] == 80 + + def test_export_milestones_json_empty(self): + """Empty milestone list produces '[]'.""" + from gitea_dashboard.exporter import export_milestones_json + + output = export_milestones_json([]) + assert json.loads(output) == []