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()
|
captured = capsys.readouterr()
|
||||||
parsed = json.loads(captured.out)
|
parsed = json.loads(captured.out)
|
||||||
assert isinstance(parsed, list)
|
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 unittest.mock import MagicMock
|
||||||
|
|
||||||
from gitea_dashboard.collector import RepoData, collect_all
|
from gitea_dashboard.collector import (
|
||||||
|
RepoData,
|
||||||
|
collect_all,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _make_repo(
|
def _make_repo(
|
||||||
@@ -311,3 +314,158 @@ class TestCollectAllFiltering:
|
|||||||
result_empty = collect_all(client, include=[])
|
result_empty = collect_all(client, include=[])
|
||||||
|
|
||||||
assert [r.name for r in result_empty] == [r.name for r in result_none]
|
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")
|
result = sort_repos(repos, "activity")
|
||||||
assert [r.name for r in result] == ["recent", "old-commit", "inactive"]
|
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 '[]'."""
|
"""Empty repo list produces '[]'."""
|
||||||
output = export_json([])
|
output = export_json([])
|
||||||
assert json.loads(output) == []
|
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