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 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) == []
|
||||
|
||||
Reference in New Issue
Block a user