Repository-wide formatting pass over src/ and tests/: sorted imports and __all__ entries, dropped redundant `# -*- coding: utf-8 -*-` headers, unquoted forward-reference annotations (safe under `from __future__ import annotations`), merged nested `with` statements, moved `Iterable` and friends from `typing` to `collections.abc`, and removed `# noqa: F401` markers that no longer suppress anything. No behavioural changes. Unit and integration suites show the same four pre-existing failures before and after the pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
250 lines
7.7 KiB
Python
250 lines
7.7 KiB
Python
#!/usr/bin/env python3
|
|
|
|
"""
|
|
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 the selection logic. 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 the same selection logic as the new CLI:
|
|
* dispatch_command() calls _select_repo_for_current_directory()
|
|
when there is no explicit selection.
|
|
"""
|
|
|
|
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)
|
|
|
|
# Define a fake repo pointing to our temp dir
|
|
self._fake_repo = {
|
|
"provider": "github.com",
|
|
"account": "test",
|
|
"repository": "pkgmgr-test",
|
|
"directory": 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 the "current directory" selection used by dispatch_command().
|
|
# This matches the new behaviour: without explicit identifiers,
|
|
# version uses _select_repo_for_current_directory(ctx).
|
|
self._patch_select_repo_for_current_directory = mock.patch(
|
|
"pkgmgr.cli.dispatch._select_repo_for_current_directory",
|
|
return_value=[self._fake_repo],
|
|
)
|
|
self.mock_select_repo_for_current_directory = (
|
|
self._patch_select_repo_for_current_directory.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_select_repo_for_current_directory.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 used by handle_version
|
|
with mock.patch(
|
|
"pkgmgr.cli.commands.version.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.cli.commands.version.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.cli.commands.version.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()
|