feat(repository): integrate ignore filtering into selection pipeline + add unit tests

This commit introduces proper handling of the `ignore: true` flag in the
repository selection mechanism and adds comprehensive unit tests for both
`ignored.py` and `selected.py`.

- `get_selected_repos()` now filters ignored repositories in all implicit
  selection modes:
    • filter-only mode (string/category/tag)
    • `--all` mode
    • CWD-based selection

- Explicit identifiers (e.g. `pkgmgr install ignored-repo`) **bypass**
  ignore filtering, so the user can still operate directly on ignored
  repositories if they ask for them explicitly.

- Added `_maybe_filter_ignored()` helper to handle logic cleanly and allow
  future extension (e.g. integrating a CLI flag `--include-ignored`).

Under `tests/unit/pkgmgr/core/repository`:

1. **test_ignored.py**
   • Ensures `filter_ignored()` removes repos with `ignore: true`
   • Ensures empty lists are handled correctly

2. **test_selected.py**
   Comprehensive coverage of the selection logic:
   • Explicit identifiers bypass ignore filtering
   • Filter-only mode excludes ignored repos unless `include_ignored=True`
   • `--all` mode excludes ignored repos unless explicitly overridden
   • CWD-based detection filters ignored repos unless explicitly overridden

Before this change, ignored repositories still appeared in `pkgmgr list`,
`pkgmgr status`, and other commands using `get_selected_repos()`.
This was unintuitive and broke the expected semantics of the `ignore`
attribute.
The new logic ensures ignored repositories are truly invisible unless
explicitly requested.

https://chatgpt.com/share/69383b41-50a0-800f-a2b9-c680cd96d9e9
This commit is contained in:
Kevin Veen-Birkenbach
2025-12-09 16:07:39 +01:00
parent 05ff250251
commit de8c3f768d
4 changed files with 249 additions and 10 deletions

View File

@@ -0,0 +1,29 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import unittest
from pkgmgr.core.repository.ignored import filter_ignored
class TestFilterIgnored(unittest.TestCase):
def test_filter_ignored_removes_repos_with_ignore_true(self) -> None:
repos = [
{"provider": "github.com", "account": "user", "repository": "a", "ignore": True},
{"provider": "github.com", "account": "user", "repository": "b", "ignore": False},
{"provider": "github.com", "account": "user", "repository": "c"},
]
result = filter_ignored(repos)
identifiers = {(r["provider"], r["account"], r["repository"]) for r in result}
self.assertNotIn(("github.com", "user", "a"), identifiers)
self.assertIn(("github.com", "user", "b"), identifiers)
self.assertIn(("github.com", "user", "c"), identifiers)
def test_filter_ignored_empty_list_returns_empty_list(self) -> None:
self.assertEqual(filter_ignored([]), [])
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,180 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import unittest
from types import SimpleNamespace
from unittest.mock import patch
from pkgmgr.core.repository.selected import get_selected_repos
def _repo(
provider: str,
account: str,
repository: str,
ignore: bool | None = None,
directory: str | None = None,
):
repo = {
"provider": provider,
"account": account,
"repository": repository,
}
if ignore is not None:
repo["ignore"] = ignore
if directory is not None:
repo["directory"] = directory
return repo
class TestGetSelectedRepos(unittest.TestCase):
def setUp(self) -> None:
self.repo_ignored = _repo(
"github.com",
"user",
"ignored-repo",
ignore=True,
directory="/repos/github.com/user/ignored-repo",
)
self.repo_visible = _repo(
"github.com",
"user",
"visible-repo",
ignore=False,
directory="/repos/github.com/user/visible-repo",
)
self.all_repos = [self.repo_ignored, self.repo_visible]
# ------------------------------------------------------------------
# 1) Explizite Identifier ignorierte Repos dürfen ausgewählt werden
# ------------------------------------------------------------------
def test_identifiers_bypass_ignore_filter(self) -> None:
args = SimpleNamespace(
identifiers=["ignored-repo"], # matches by repository name
all=False,
category=[],
string="",
tag=[],
include_ignored=False, # should be ignored for explicit identifiers
)
selected = get_selected_repos(args, self.all_repos)
self.assertEqual(len(selected), 1)
self.assertIs(selected[0], self.repo_ignored)
# ------------------------------------------------------------------
# 2) Filter-only Modus ignorierte Repos werden rausgefiltert
# ------------------------------------------------------------------
def test_filter_mode_excludes_ignored_by_default(self) -> None:
# string-Filter, der beide Repos matchen würde
args = SimpleNamespace(
identifiers=[],
all=False,
category=[],
string="repo", # substring in beiden Namen
tag=[],
include_ignored=False,
)
selected = get_selected_repos(args, self.all_repos)
self.assertEqual(len(selected), 1)
self.assertIs(selected[0], self.repo_visible)
def test_filter_mode_can_include_ignored_when_flag_set(self) -> None:
args = SimpleNamespace(
identifiers=[],
all=False,
category=[],
string="repo",
tag=[],
include_ignored=True,
)
selected = get_selected_repos(args, self.all_repos)
# Beide Repos sollten erscheinen, weil include_ignored=True
self.assertEqual({r["repository"] for r in selected}, {"ignored-repo", "visible-repo"})
# ------------------------------------------------------------------
# 3) --all Modus ignorierte Repos werden per Default entfernt
# ------------------------------------------------------------------
def test_all_mode_excludes_ignored_by_default(self) -> None:
args = SimpleNamespace(
identifiers=[],
all=True,
category=[],
string="",
tag=[],
include_ignored=False,
)
selected = get_selected_repos(args, self.all_repos)
self.assertEqual(len(selected), 1)
self.assertIs(selected[0], self.repo_visible)
def test_all_mode_can_include_ignored_when_flag_set(self) -> None:
args = SimpleNamespace(
identifiers=[],
all=True,
category=[],
string="",
tag=[],
include_ignored=True,
)
selected = get_selected_repos(args, self.all_repos)
self.assertEqual(len(selected), 2)
self.assertCountEqual(
[r["repository"] for r in selected],
["ignored-repo", "visible-repo"],
)
# ------------------------------------------------------------------
# 4) CWD-Modus Repo anhand des aktuellen Verzeichnisses auswählen
# ------------------------------------------------------------------
def test_cwd_selection_excludes_ignored_by_default(self) -> None:
# Wir lassen CWD auf das Verzeichnis des ignorierten Repos zeigen.
cwd = os.path.abspath(self.repo_ignored["directory"])
args = SimpleNamespace(
identifiers=[],
all=False,
category=[],
string="",
tag=[],
include_ignored=False,
)
with patch("os.getcwd", return_value=cwd):
selected = get_selected_repos(args, self.all_repos)
# Da das einzige Repo für dieses Verzeichnis ignoriert ist,
# sollte die Auswahl leer sein.
self.assertEqual(selected, [])
def test_cwd_selection_can_include_ignored_when_flag_set(self) -> None:
cwd = os.path.abspath(self.repo_ignored["directory"])
args = SimpleNamespace(
identifiers=[],
all=False,
category=[],
string="",
tag=[],
include_ignored=True,
)
with patch("os.getcwd", return_value=cwd):
selected = get_selected_repos(args, self.all_repos)
self.assertEqual(len(selected), 1)
self.assertIs(selected[0], self.repo_ignored)
if __name__ == "__main__":
unittest.main()