Extend 'pkgmgr version' command with multi-source version detection (pyproject, flake, PKGBUILD, debian, spec, AnsibleGalaxy), implement SemVer parsing, consistency warnings, full E2E + unit test coverage.
Ref: https://chatgpt.com/share/6936ef7e-ad5c-800f-96b2-e5d0f32b39ca
This commit is contained in:
187
tests/e2e/test_integration_version_commands.py
Normal file
187
tests/e2e/test_integration_version_commands.py
Normal file
@@ -0,0 +1,187 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
End-to-end tests for the `pkgmgr version` command.
|
||||
|
||||
We verify three usage patterns:
|
||||
|
||||
1) pkgmgr version
|
||||
- Run from inside the package-manager repository
|
||||
so that "current repository" resolution works.
|
||||
|
||||
2) pkgmgr version pkgmgr
|
||||
- Run from inside the package-manager repository
|
||||
with an explicit identifier.
|
||||
|
||||
3) pkgmgr version --all
|
||||
- Run from the project root (or wherever the tests are started),
|
||||
ensuring that the --all flag does not depend on the current
|
||||
working directory.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import runpy
|
||||
import sys
|
||||
import unittest
|
||||
from typing import List
|
||||
|
||||
from pkgmgr.load_config import load_config
|
||||
|
||||
# Resolve project root (the repo where main.py lives, e.g. /src)
|
||||
PROJECT_ROOT = os.path.abspath(
|
||||
os.path.join(os.path.dirname(__file__), "..", "..")
|
||||
)
|
||||
CONFIG_PATH = os.path.join(PROJECT_ROOT, "config", "config.yaml")
|
||||
|
||||
|
||||
def _load_pkgmgr_repo_dir() -> str:
|
||||
"""
|
||||
Load the merged configuration (defaults + user config) and determine
|
||||
the directory of the package-manager repository managed by pkgmgr.
|
||||
|
||||
We keep this lookup deliberately flexible to avoid depending on
|
||||
specific provider/account values. We match either
|
||||
|
||||
repository == "package-manager" OR
|
||||
alias == "pkgmgr"
|
||||
|
||||
and then derive the repository directory from either an explicit
|
||||
'directory' field or from the base repositories directory plus
|
||||
provider/account/repository.
|
||||
"""
|
||||
cfg = load_config(CONFIG_PATH) or {}
|
||||
|
||||
directories = cfg.get("directories", {})
|
||||
base_repos_dir = os.path.expanduser(directories.get("repositories", ""))
|
||||
|
||||
candidates: List[dict] = cfg.get("repositories", []) or []
|
||||
for repo in candidates:
|
||||
repo_name = (repo.get("repository") or "").strip()
|
||||
alias = (repo.get("alias") or "").strip()
|
||||
|
||||
if repo_name == "package-manager" or alias == "pkgmgr":
|
||||
# Prefer an explicit directory if present.
|
||||
repo_dir = repo.get("directory")
|
||||
if not repo_dir:
|
||||
provider = (repo.get("provider") or "").strip()
|
||||
account = (repo.get("account") or "").strip()
|
||||
|
||||
# Best-effort reconstruction of the directory path.
|
||||
if provider and account and repo_name:
|
||||
repo_dir = os.path.join(
|
||||
base_repos_dir, provider, account, repo_name
|
||||
)
|
||||
elif repo_name:
|
||||
# Fallback: place directly under the base repo dir
|
||||
repo_dir = os.path.join(base_repos_dir, repo_name)
|
||||
else:
|
||||
# If we still have nothing usable, skip this entry.
|
||||
continue
|
||||
|
||||
return os.path.expanduser(repo_dir)
|
||||
|
||||
raise RuntimeError(
|
||||
"Could not locate a 'package-manager' repository entry in the merged "
|
||||
"configuration (no entry with repository='package-manager' or "
|
||||
"alias='pkgmgr' found)."
|
||||
)
|
||||
|
||||
|
||||
class TestIntegrationVersionCommands(unittest.TestCase):
|
||||
"""
|
||||
E2E tests for the pkgmgr 'version' command.
|
||||
|
||||
Important:
|
||||
- We treat any non-zero SystemExit as a test failure and print
|
||||
helpful diagnostics (command, working directory, exit code).
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
# Determine the package-manager repo directory from the merged config
|
||||
cls.pkgmgr_repo_dir = _load_pkgmgr_repo_dir()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Helper
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _run_pkgmgr_version(self, extra_args, cwd: str | None = None) -> None:
|
||||
"""
|
||||
Run `pkgmgr version` with optional extra arguments and
|
||||
an optional working directory override.
|
||||
|
||||
Any non-zero exit code is turned into an AssertionError
|
||||
with additional diagnostics.
|
||||
"""
|
||||
if extra_args is None:
|
||||
extra_args = []
|
||||
|
||||
cmd_repr = "pkgmgr version " + " ".join(extra_args)
|
||||
original_argv = list(sys.argv)
|
||||
original_cwd = os.getcwd()
|
||||
|
||||
try:
|
||||
if cwd is not None:
|
||||
os.chdir(cwd)
|
||||
|
||||
sys.argv = ["pkgmgr", "version"] + extra_args
|
||||
|
||||
try:
|
||||
runpy.run_module("main", run_name="__main__")
|
||||
except SystemExit as exc:
|
||||
code = exc.code if isinstance(exc.code, int) else str(exc.code)
|
||||
if code != 0:
|
||||
print("[TEST] SystemExit caught while running pkgmgr version")
|
||||
print(f"[TEST] Command : {cmd_repr}")
|
||||
print(f"[TEST] Working directory: {os.getcwd()}")
|
||||
print(f"[TEST] Exit code: {code}")
|
||||
raise AssertionError(
|
||||
f"{cmd_repr!r} failed with exit code {code}. "
|
||||
"Scroll up to inspect the output printed before failure."
|
||||
) from exc
|
||||
# exit code 0 is considered success
|
||||
|
||||
finally:
|
||||
# Restore environment
|
||||
os.chdir(original_cwd)
|
||||
sys.argv = original_argv
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Tests
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def test_version_current_repo(self) -> None:
|
||||
"""
|
||||
Run: pkgmgr version
|
||||
|
||||
We run this from inside the package-manager repository so that
|
||||
"current repository" resolution works and no identifier lookup
|
||||
for 'src' (or similar) is performed.
|
||||
"""
|
||||
self._run_pkgmgr_version(extra_args=[], cwd=self.pkgmgr_repo_dir)
|
||||
|
||||
def test_version_specific_identifier(self) -> None:
|
||||
"""
|
||||
Run: pkgmgr version pkgmgr
|
||||
|
||||
Also executed from inside the package-manager repository, but
|
||||
with an explicit identifier.
|
||||
"""
|
||||
self._run_pkgmgr_version(extra_args=["pkgmgr"], cwd=self.pkgmgr_repo_dir)
|
||||
|
||||
def test_version_all_repositories(self) -> None:
|
||||
"""
|
||||
Run: pkgmgr version --all
|
||||
|
||||
This does not depend on the current working directory, but we
|
||||
run it from PROJECT_ROOT for clarity and to mirror typical usage
|
||||
in CI.
|
||||
"""
|
||||
self._run_pkgmgr_version(extra_args=["--all"], cwd=PROJECT_ROOT)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
241
tests/unit/pkgmgr/test_cli.py
Normal file
241
tests/unit/pkgmgr/test_cli.py
Normal file
@@ -0,0 +1,241 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Unit tests for the pkgmgr CLI (version command).
|
||||
|
||||
These tests focus on the 'version' subcommand and its interaction with:
|
||||
- git tags (SemVer),
|
||||
- pyproject.toml version,
|
||||
- and the mismatch warning logic.
|
||||
|
||||
Important:
|
||||
- Uses only the Python standard library unittest framework.
|
||||
- Does not use pytest.
|
||||
- Does not rely on a real git repository or real config files.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import textwrap
|
||||
import unittest
|
||||
from contextlib import redirect_stdout
|
||||
from typing import Any, Dict, List
|
||||
from unittest import mock
|
||||
|
||||
from pkgmgr import cli
|
||||
|
||||
|
||||
def _fake_config() -> Dict[str, Any]:
|
||||
"""
|
||||
Provide a minimal configuration dict sufficient for cli.main()
|
||||
to start without touching real config files.
|
||||
"""
|
||||
return {
|
||||
"directories": {
|
||||
"repositories": "/tmp/pkgmgr-repos",
|
||||
"binaries": "/tmp/pkgmgr-bin",
|
||||
"workspaces": "/tmp/pkgmgr-workspaces",
|
||||
},
|
||||
# The actual list of repositories is not used directly by the tests,
|
||||
# because we mock get_selected_repos(). It must exist, though.
|
||||
"repositories": [],
|
||||
}
|
||||
|
||||
|
||||
class TestCliVersion(unittest.TestCase):
|
||||
"""
|
||||
Tests for the 'pkgmgr version' command.
|
||||
|
||||
Each test:
|
||||
- Runs in a temporary working directory.
|
||||
- Uses a fake configuration via load_config().
|
||||
- Uses a mocked get_selected_repos() that returns a single repo
|
||||
pointing to the temporary directory.
|
||||
"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
# Create a temporary directory and switch into it
|
||||
self._tmp_dir = tempfile.TemporaryDirectory()
|
||||
self._old_cwd = os.getcwd()
|
||||
os.chdir(self._tmp_dir.name)
|
||||
|
||||
# Patch load_config so cli.main() does not read real config files
|
||||
self._patch_load_config = mock.patch(
|
||||
"pkgmgr.cli.load_config", return_value=_fake_config()
|
||||
)
|
||||
self.mock_load_config = self._patch_load_config.start()
|
||||
|
||||
# Patch get_selected_repos so that 'version' operates on our temp dir
|
||||
def _fake_selected_repos(all_flag: bool, repos: List[dict], identifiers: List[str]):
|
||||
# We always return exactly one "repository" whose directory is the temp dir.
|
||||
return [
|
||||
{
|
||||
"provider": "github.com",
|
||||
"account": "test",
|
||||
"repository": "pkgmgr-test",
|
||||
"directory": self._tmp_dir.name,
|
||||
}
|
||||
]
|
||||
|
||||
self._patch_get_selected_repos = mock.patch(
|
||||
"pkgmgr.cli.get_selected_repos", side_effect=_fake_selected_repos
|
||||
)
|
||||
self.mock_get_selected_repos = self._patch_get_selected_repos.start()
|
||||
|
||||
# Keep a reference to the original sys.argv, so we can restore it
|
||||
self._old_argv = list(sys.argv)
|
||||
|
||||
def tearDown(self) -> None:
|
||||
# Restore sys.argv
|
||||
sys.argv = self._old_argv
|
||||
|
||||
# Stop all patches
|
||||
self._patch_get_selected_repos.stop()
|
||||
self._patch_load_config.stop()
|
||||
|
||||
# Restore working directory
|
||||
os.chdir(self._old_cwd)
|
||||
|
||||
# Cleanup temp directory
|
||||
self._tmp_dir.cleanup()
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Helpers
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def _write_pyproject(self, version: str) -> str:
|
||||
"""
|
||||
Write a minimal PEP 621-style pyproject.toml into the temp directory.
|
||||
"""
|
||||
content = textwrap.dedent(
|
||||
f"""
|
||||
[project]
|
||||
name = "pkgmgr-test"
|
||||
version = "{version}"
|
||||
"""
|
||||
).strip() + "\n"
|
||||
|
||||
path = os.path.join(self._tmp_dir.name, "pyproject.toml")
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
return path
|
||||
|
||||
def _run_cli_version_and_capture(self, extra_args: List[str] | None = None) -> str:
|
||||
"""
|
||||
Run 'pkgmgr version [extra_args]' via cli.main() and return captured stdout.
|
||||
"""
|
||||
if extra_args is None:
|
||||
extra_args = []
|
||||
|
||||
sys.argv = ["pkgmgr", "version"] + list(extra_args)
|
||||
buf = io.StringIO()
|
||||
with redirect_stdout(buf):
|
||||
try:
|
||||
cli.main()
|
||||
except SystemExit as exc:
|
||||
# Re-raise as AssertionError to make failures easier to read
|
||||
raise AssertionError(
|
||||
f"'pkgmgr version' exited with code {exc.code}"
|
||||
) from exc
|
||||
return buf.getvalue()
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Tests
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def test_version_matches_tag(self) -> None:
|
||||
"""
|
||||
If the latest SemVer tag matches the pyproject.toml version,
|
||||
the CLI should:
|
||||
- show both values
|
||||
- NOT emit a mismatch warning.
|
||||
"""
|
||||
# Arrange: pyproject.toml with version 1.2.3
|
||||
self._write_pyproject("1.2.3")
|
||||
|
||||
# Arrange: mock git tags
|
||||
with mock.patch(
|
||||
"pkgmgr.git_utils.get_tags",
|
||||
return_value=["v1.2.0", "v1.2.3", "v1.0.0"],
|
||||
):
|
||||
# Act
|
||||
stdout = self._run_cli_version_and_capture()
|
||||
|
||||
# Basic header
|
||||
self.assertIn("pkgmgr version info", stdout)
|
||||
self.assertIn("Repository:", stdout)
|
||||
|
||||
# Git SemVer tag line
|
||||
self.assertIn("Git (latest SemVer tag):", stdout)
|
||||
self.assertIn("v1.2.3", stdout)
|
||||
self.assertIn("(parsed: 1.2.3)", stdout)
|
||||
|
||||
# pyproject line
|
||||
self.assertIn("pyproject.toml:", stdout)
|
||||
self.assertIn("1.2.3", stdout)
|
||||
|
||||
# No warning expected if versions are equal
|
||||
self.assertNotIn("[WARN]", stdout)
|
||||
|
||||
def test_version_mismatch_warns(self) -> None:
|
||||
"""
|
||||
If the latest SemVer tag differs from the pyproject.toml version,
|
||||
the CLI should emit a mismatch warning.
|
||||
"""
|
||||
# Arrange: pyproject.toml says 1.2.4
|
||||
self._write_pyproject("1.2.4")
|
||||
|
||||
# Arrange: mock git tags (latest is 1.2.3)
|
||||
with mock.patch(
|
||||
"pkgmgr.git_utils.get_tags",
|
||||
return_value=["v1.2.3"],
|
||||
):
|
||||
stdout = self._run_cli_version_and_capture()
|
||||
|
||||
# Git line
|
||||
self.assertIn("Git (latest SemVer tag):", stdout)
|
||||
self.assertIn("v1.2.3", stdout)
|
||||
|
||||
# pyproject line
|
||||
self.assertIn("pyproject.toml:", stdout)
|
||||
self.assertIn("1.2.4", stdout)
|
||||
|
||||
# Mismatch warning must be printed
|
||||
self.assertIn("[WARN]", stdout)
|
||||
self.assertIn("Version mismatch", stdout)
|
||||
|
||||
def test_version_no_tags(self) -> None:
|
||||
"""
|
||||
If no tags exist at all, the CLI should handle this gracefully,
|
||||
show "<none found>" for tags and still display the pyproject version.
|
||||
No mismatch warning should be emitted because there is no tag.
|
||||
"""
|
||||
# Arrange: pyproject.toml exists
|
||||
self._write_pyproject("0.0.1")
|
||||
|
||||
# Arrange: no tags returned
|
||||
with mock.patch(
|
||||
"pkgmgr.git_utils.get_tags",
|
||||
return_value=[],
|
||||
):
|
||||
stdout = self._run_cli_version_and_capture()
|
||||
|
||||
# Indicates that no SemVer tag was found
|
||||
self.assertIn("Git (latest SemVer tag): <none found>", stdout)
|
||||
|
||||
# pyproject version is still shown
|
||||
self.assertIn("pyproject.toml:", stdout)
|
||||
self.assertIn("0.0.1", stdout)
|
||||
|
||||
# No mismatch warning expected
|
||||
self.assertNotIn("Version mismatch", stdout)
|
||||
self.assertNotIn("[WARN]", stdout)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user