"""Unit tests for devx.cli module.""" from unittest.mock import MagicMock, patch import click import pytest from click.testing import CliRunner from devx.cli import cli class TestCliGroups: def test_cli_help(self) -> None: runner = CliRunner() result = runner.invoke(cli, ["--help"]) assert result.exit_code == 0 assert "devx" in result.output assert "ci" in result.output assert "tools" in result.output assert "molecule" in result.output def test_ci_group_help(self) -> None: runner = CliRunner() result = runner.invoke(cli, ["ci", "--help"]) assert result.exit_code == 0 assert "auto-merge" in result.output assert "release" in result.output assert "publish" in result.output def test_tools_group_help(self) -> None: runner = CliRunner() result = runner.invoke(cli, ["tools", "--help"]) assert result.exit_code == 0 assert "setup" in result.output assert "install-tools" in result.output assert "configure-repo" in result.output def test_molecule_group_help(self) -> None: runner = CliRunner() result = runner.invoke(cli, ["molecule", "--help"]) assert result.exit_code == 0 assert "distribute" in result.output assert "guard" in result.output assert "all" in result.output class TestCiCommands: @patch("devx.cli._run_module") def test_ci_auto_merge(self, mock_run: MagicMock) -> None: runner = CliRunner() result = runner.invoke(cli, ["ci", "auto-merge", "branch", "title", "repo", "1"]) assert result.exit_code == 0 mock_run.assert_called_once_with("devx.ci.auto_merge", ["branch", "title", "repo", "1"]) @patch("devx.cli._run_module") def test_ci_check_translations(self, mock_run: MagicMock) -> None: runner = CliRunner() result = runner.invoke(cli, ["ci", "check-translations"]) assert result.exit_code == 0 mock_run.assert_called_once_with("devx.ci.check_translations", []) @patch("devx.cli._run_module") def test_ci_classify_changes(self, mock_run: MagicMock) -> None: runner = CliRunner() result = runner.invoke(cli, ["ci", "classify-changes", "positional"]) assert result.exit_code == 0 mock_run.assert_called_once_with("devx.ci.classify_changes", ["positional"]) @patch("devx.cli._run_module") def test_ci_detect_release_commit(self, mock_run: MagicMock) -> None: runner = CliRunner() result = runner.invoke(cli, ["ci", "detect-release-commit"]) assert result.exit_code == 0 mock_run.assert_called_once_with("devx.ci.detect_release_commit", []) @patch("devx.cli._run_module") def test_ci_doc_coverage(self, mock_run: MagicMock) -> None: runner = CliRunner() result = runner.invoke(cli, ["ci", "doc-coverage"]) assert result.exit_code == 0 mock_run.assert_called_once_with("devx.ci.doc_coverage", []) @patch("devx.cli._run_module") def test_ci_lint_docs(self, mock_run: MagicMock) -> None: runner = CliRunner() result = runner.invoke(cli, ["ci", "lint-docs", "--", "--root", "."]) assert result.exit_code == 0 mock_run.assert_called_once_with("devx.ci.lint_docs", ["--root", "."]) @patch("devx.cli._run_module") def test_ci_notify_failure(self, mock_run: MagicMock) -> None: runner = CliRunner() result = runner.invoke(cli, ["ci", "notify-failure", "job1"]) assert result.exit_code == 0 mock_run.assert_called_once_with("devx.ci.notify_failure", ["job1"]) @patch("devx.cli._run_module") def test_ci_post_merge(self, mock_run: MagicMock) -> None: runner = CliRunner() result = runner.invoke(cli, ["ci", "post-merge", "DEVX-1"]) assert result.exit_code == 0 mock_run.assert_called_once_with("devx.ci.post_merge", ["DEVX-1"]) @patch("devx.cli._run_module") def test_ci_pr_review(self, mock_run: MagicMock) -> None: runner = CliRunner() result = runner.invoke(cli, ["ci", "pr-review", "42"]) assert result.exit_code == 0 mock_run.assert_called_once_with("devx.ci.pr_review", ["42"]) @patch("devx.cli._run_module") def test_ci_publish(self, mock_run: MagicMock) -> None: runner = CliRunner() result = runner.invoke(cli, ["ci", "publish", "v1.0.0", "owner/repo"]) assert result.exit_code == 0 mock_run.assert_called_once_with("devx.ci.publish", ["v1.0.0", "owner/repo"]) @patch("devx.cli._run_module") def test_ci_push_badges(self, mock_run: MagicMock) -> None: runner = CliRunner() result = runner.invoke(cli, ["ci", "push-badges"]) assert result.exit_code == 0 mock_run.assert_called_once_with("devx.ci.push_badges", []) @patch("devx.cli._run_module") def test_ci_release(self, mock_run: MagicMock) -> None: runner = CliRunner() result = runner.invoke(cli, ["ci", "release", "positional"]) assert result.exit_code == 0 mock_run.assert_called_once_with("devx.ci.release", ["positional"]) @patch("devx.cli._run_module") def test_ci_sync_wiki(self, mock_run: MagicMock) -> None: runner = CliRunner() result = runner.invoke(cli, ["ci", "sync-wiki"]) assert result.exit_code == 0 mock_run.assert_called_once_with("devx.ci.sync_wiki", []) @patch("devx.cli._run_module") def test_ci_validate_commit_msg(self, mock_run: MagicMock) -> None: runner = CliRunner() result = runner.invoke(cli, ["ci", "validate-commit-msg", "msg"]) assert result.exit_code == 0 mock_run.assert_called_once_with("devx.ci.validate_commit_msg", ["msg"]) @patch("devx.cli._run_module") def test_ci_wait_for_checks(self, mock_run: MagicMock) -> None: runner = CliRunner() result = runner.invoke(cli, ["ci", "wait-for-checks", "--", "--job-name", "molecule-tests"]) assert result.exit_code == 0 mock_run.assert_called_once_with("devx.ci.wait_for_checks", ["--job-name", "molecule-tests"]) class TestToolsCommands: @patch("devx.cli._run_module") def test_tools_check_test_speed(self, mock_run: MagicMock) -> None: runner = CliRunner() result = runner.invoke(cli, ["tools", "check-test-speed"]) assert result.exit_code == 0 mock_run.assert_called_once_with("devx.tools.check_test_speed", []) @patch("devx.cli._run_module") def test_tools_configure_repo(self, mock_run: MagicMock) -> None: runner = CliRunner() result = runner.invoke(cli, ["tools", "configure-repo", "myrepo"]) assert result.exit_code == 0 mock_run.assert_called_once_with("devx.tools.configure_repo", ["myrepo"]) @patch("devx.cli._run_module") def test_tools_generate_badges(self, mock_run: MagicMock) -> None: runner = CliRunner() result = runner.invoke(cli, ["tools", "generate-badges"]) assert result.exit_code == 0 mock_run.assert_called_once_with("devx.tools.generate_badges", []) @patch("devx.cli._run_module") def test_tools_generate_cliff_config(self, mock_run: MagicMock) -> None: runner = CliRunner() result = runner.invoke(cli, ["tools", "generate-cliff-config"]) assert result.exit_code == 0 mock_run.assert_called_once_with("devx.tools.generate_cliff_config", []) @patch("devx.cli._run_module") def test_tools_install_checkmake(self, mock_run: MagicMock) -> None: runner = CliRunner() result = runner.invoke(cli, ["tools", "install-checkmake"]) assert result.exit_code == 0 mock_run.assert_called_once_with("devx.tools.install_checkmake", []) @patch("devx.cli._run_module") def test_tools_install_tools(self, mock_run: MagicMock) -> None: runner = CliRunner() result = runner.invoke(cli, ["tools", "install-tools"]) assert result.exit_code == 0 mock_run.assert_called_once_with("devx.tools.install_tools", []) @patch("devx.cli._run_module") def test_tools_setup(self, mock_run: MagicMock) -> None: runner = CliRunner() result = runner.invoke(cli, ["tools", "setup"]) assert result.exit_code == 0 mock_run.assert_called_once_with("devx.tools.setup", []) @patch("devx.cli._run_module") def test_tools_rebase(self, mock_run: MagicMock) -> None: runner = CliRunner() result = runner.invoke(cli, ["tools", "rebase", "--", "--no-push"]) assert result.exit_code == 0 mock_run.assert_called_once_with("devx.tools.rebase", ["--no-push"]) @patch("devx.cli._run_module") def test_tools_pr_rebase(self, mock_run: MagicMock) -> None: runner = CliRunner() result = runner.invoke(cli, ["tools", "pr-rebase", "--", "--pr", "42"]) assert result.exit_code == 0 mock_run.assert_called_once_with("devx.tools.pr_rebase", ["--pr", "42"]) class TestMoleculeCommands: @patch("devx.cli._run_module") def test_molecule_distribute(self, mock_run: MagicMock) -> None: runner = CliRunner() result = runner.invoke(cli, ["molecule", "distribute", "positional"]) assert result.exit_code == 0 mock_run.assert_called_once_with("devx.molecule.distribute_molecule", ["positional"]) @patch("devx.cli._run_module") def test_molecule_discover_runners(self, mock_run: MagicMock) -> None: runner = CliRunner() result = runner.invoke(cli, ["molecule", "discover-runners"]) assert result.exit_code == 0 mock_run.assert_called_once_with("devx.molecule.discover_runners", []) @patch("devx.cli._run_module") def test_molecule_guard(self, mock_run: MagicMock) -> None: runner = CliRunner() result = runner.invoke(cli, ["molecule", "guard"]) assert result.exit_code == 0 mock_run.assert_called_once_with("devx.molecule.molecule_ci_guard", []) @patch("devx.cli._run_module") def test_molecule_all(self, mock_run: MagicMock) -> None: runner = CliRunner() result = runner.invoke(cli, ["molecule", "all"]) assert result.exit_code == 0 mock_run.assert_called_once_with("devx.molecule.molecule_all", []) class TestNewCiCommands: @patch("devx.cli._run_module") def test_ci_distribute_files(self, mock_run: MagicMock) -> None: runner = CliRunner() result = runner.invoke(cli, ["ci", "distribute-files", "--", "--pattern", "*.py"]) assert result.exit_code == 0 mock_run.assert_called_once_with("devx.ci.distribute_files", ["--pattern", "*.py"]) @patch("devx.cli._run_module") def test_ci_integration_guard(self, mock_run: MagicMock) -> None: runner = CliRunner() result = runner.invoke(cli, ["ci", "integration-guard", "--", "-v"]) assert result.exit_code == 0 mock_run.assert_called_once_with("devx.ci.integration_guard", ["-v"]) class TestRunModule: @patch("importlib.import_module") def test_run_module_success(self, mock_import: MagicMock) -> None: from devx.cli import _run_module mock_module = MagicMock() mock_main = MagicMock() mock_module.main = mock_main mock_import.return_value = mock_module _run_module("devx.ci.release", ["--dry-run"]) mock_import.assert_called_once_with("devx.ci.release") mock_main.assert_called_once() @patch("importlib.import_module") def test_run_module_no_main_raises(self, mock_import: MagicMock) -> None: from devx.cli import _run_module mock_module = MagicMock() mock_module.main = None mock_import.return_value = mock_module with pytest.raises(SystemExit) as exc_info: _run_module("devx.ci.nonexistent", []) assert exc_info.value.code == 1 @patch("importlib.import_module") def test_run_module_click_exception(self, mock_import: MagicMock) -> None: from devx.cli import _run_module mock_module = MagicMock() mock_main = MagicMock() mock_main.side_effect = click.ClickException("test error") mock_module.main = mock_main mock_import.return_value = mock_module with pytest.raises(SystemExit): _run_module("devx.ci.release", []) @patch("importlib.import_module") def test_run_module_abort(self, mock_import: MagicMock) -> None: from devx.cli import _run_module mock_module = MagicMock() mock_main = MagicMock() mock_main.side_effect = click.exceptions.Abort() mock_module.main = mock_main mock_import.return_value = mock_module with pytest.raises(SystemExit) as exc_info: _run_module("devx.ci.release", []) assert exc_info.value.code == 1 @patch("importlib.import_module") def test_run_module_click_command_object(self, mock_import: MagicMock) -> None: """When main is not callable (a click Command), uses .main() method.""" from devx.cli import _run_module mock_module = MagicMock() # Create a non-callable object with a .main method (like click.Command) # callable() returns False only if __call__ is not defined class NonCallableMain: def __init__(self) -> None: self.main = MagicMock() non_callable = NonCallableMain() assert not callable(non_callable) mock_module.main = non_callable mock_import.return_value = mock_module _run_module("devx.ci.release", ["arg1"]) non_callable.main.assert_called_once() class TestVersionOption: def test_version_flag(self) -> None: runner = CliRunner() result = runner.invoke(cli, ["--version"]) assert result.exit_code == 0