#!/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 "" 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): ", 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()