"""Unit tests for devx.tools.pr_status.""" from __future__ import annotations from unittest.mock import MagicMock, patch import pytest from click.testing import CliRunner from devx.api_clients import GiteaClient from devx.tools.pr_status import ( _get_pr_sha, _get_symbol, cli, print_status, wait_for_completion, ) class TestGetSymbol: def test_success(self) -> None: assert _get_symbol("success") == "[OK]" def test_failure(self) -> None: assert _get_symbol("failure") == "[FAIL]" def test_pending(self) -> None: assert _get_symbol("pending") == "[..]" def test_unknown(self) -> None: assert _get_symbol("weird") == "[weird]" class TestGetPrSha: def test_returns_head_sha(self) -> None: client = MagicMock(spec=GiteaClient) client.get_pr.return_value = {"head": {"sha": "abc123"}} assert _get_pr_sha(client, 42) == "abc123" def test_returns_empty_when_missing(self) -> None: client = MagicMock(spec=GiteaClient) client.get_pr.return_value = {"head": {}} assert _get_pr_sha(client, 42) == "" class TestPrintStatus: def test_no_statuses(self, capsys: pytest.CaptureFixture) -> None: client = MagicMock(spec=GiteaClient) client.get_commit_status.return_value = [] result = print_status(client, "abc123") assert result == "none" def test_all_success(self, capsys: pytest.CaptureFixture) -> None: client = MagicMock(spec=GiteaClient) client.get_commit_status.return_value = [ {"context": "CI / quality", "status": "success"}, {"context": "CI / lint", "status": "success"}, ] result = print_status(client, "abc123") assert result == "success" def test_has_failure(self, capsys: pytest.CaptureFixture) -> None: client = MagicMock(spec=GiteaClient) client.get_commit_status.return_value = [ {"context": "CI / quality", "status": "success"}, {"context": "CI / lint", "status": "failure"}, ] result = print_status(client, "abc123") assert result == "failure" def test_pending(self, capsys: pytest.CaptureFixture) -> None: client = MagicMock(spec=GiteaClient) client.get_commit_status.return_value = [ {"context": "CI / quality", "status": "pending"}, ] result = print_status(client, "abc123") assert result == "pending" def test_skipped_still_success(self, capsys: pytest.CaptureFixture) -> None: client = MagicMock(spec=GiteaClient) client.get_commit_status.return_value = [ {"context": "CI / quality", "status": "success"}, {"context": "CI / molecule", "status": "skipped"}, ] result = print_status(client, "abc123") assert result == "success" class TestWaitForCompletion: @patch("devx.tools.pr_status.time.sleep") @patch("devx.tools.pr_status.time.time", side_effect=[0, 0, 100, 200]) def test_success_after_pending( self, mock_time: MagicMock, mock_sleep: MagicMock, capsys: pytest.CaptureFixture ) -> None: client = MagicMock(spec=GiteaClient) client.get_commit_status.side_effect = [ [{"context": "CI / quality", "status": "pending"}], [{"context": "CI / quality", "status": "success"}], ] result = wait_for_completion(client, "abc", timeout=600, interval=1) assert result == "success" @patch("devx.tools.pr_status.time.sleep") @patch("devx.tools.pr_status.time.time", side_effect=[0, 0, 100, 200]) def test_failure_after_pending(self, mock_time: MagicMock, mock_sleep: MagicMock) -> None: client = MagicMock(spec=GiteaClient) client.get_commit_status.side_effect = [ [{"context": "CI / quality", "status": "pending"}], [{"context": "CI / quality", "status": "failure"}], ] result = wait_for_completion(client, "abc", timeout=600, interval=1) assert result == "failure" @patch("devx.tools.pr_status.time.sleep") @patch("devx.tools.pr_status.time.time", side_effect=[0, 700]) def test_timeout(self, mock_time: MagicMock, mock_sleep: MagicMock) -> None: client = MagicMock(spec=GiteaClient) client.get_commit_status.return_value = [{"context": "CI / quality", "status": "pending"}] result = wait_for_completion(client, "abc", timeout=600, interval=1) assert result == "pending" class TestCli: @patch("devx.tools.pr_status.subprocess.run") def test_no_token_raises(self, mock_subproc: MagicMock, monkeypatch: pytest.MonkeyPatch) -> None: for name in ("DEVELOPER_GITEA_API_TOKEN", "CI_GITEA_API_TOKEN", "CI_GITEA_TOKEN"): monkeypatch.delenv(name, raising=False) runner = CliRunner() result = runner.invoke( cli, ["--pr", "42"], env={"DEVELOPER_GITEA_API_TOKEN": "", "CI_GITEA_API_TOKEN": "", "CI_GITEA_TOKEN": ""}, ) assert result.exit_code != 0 assert "CI_GITEA_TOKEN" in result.output @patch("devx.tools.pr_status.subprocess.run") @patch("devx.tools.pr_status.REPO_OWNER", "") @patch("devx.tools.pr_status.get_repo_name", side_effect=Exception("should not reach")) def test_no_owner_raises( self, mock_repo_name: MagicMock, mock_subproc: MagicMock, monkeypatch: pytest.MonkeyPatch ) -> None: monkeypatch.setenv("CI_GITEA_TOKEN", "tok") runner = CliRunner() result = runner.invoke(cli, ["--pr", "42"]) assert result.exit_code != 0 assert "owner" in result.output.lower() @patch("devx.tools.pr_status.subprocess.run") @patch("devx.tools.pr_status.GiteaClient") def test_check_pr_status( self, mock_client_cls: MagicMock, mock_subproc: MagicMock, monkeypatch: pytest.MonkeyPatch ) -> None: monkeypatch.setenv("CI_GITEA_TOKEN", "tok") monkeypatch.setenv("DEVX_REPO_OWNER", "owner") monkeypatch.setenv("DEVX_REPO_NAME", "repo") client = mock_client_cls.return_value client.get_pr.return_value = {"head": {"sha": "abc123"}} client.get_commit_status.return_value = [ {"context": "CI / quality", "status": "success"}, ] runner = CliRunner() result = runner.invoke(cli, ["--pr", "42"]) assert result.exit_code == 0 assert "[OK]" in result.output @patch("devx.tools.pr_status.subprocess.run") @patch("devx.tools.pr_status.GiteaClient") def test_check_sha_directly( self, mock_client_cls: MagicMock, mock_subproc: MagicMock, monkeypatch: pytest.MonkeyPatch ) -> None: monkeypatch.setenv("CI_GITEA_TOKEN", "tok") monkeypatch.setenv("DEVX_REPO_OWNER", "owner") monkeypatch.setenv("DEVX_REPO_NAME", "repo") client = mock_client_cls.return_value client.get_commit_status.return_value = [ {"context": "CI / quality", "status": "success"}, ] runner = CliRunner() result = runner.invoke(cli, ["--sha", "abc123"]) assert result.exit_code == 0 assert "[OK]" in result.output @patch("devx.tools.pr_status.subprocess.run") @patch("devx.tools.pr_status.GiteaClient") def test_failure_raises_exception( self, mock_client_cls: MagicMock, mock_subproc: MagicMock, monkeypatch: pytest.MonkeyPatch ) -> None: monkeypatch.setenv("CI_GITEA_TOKEN", "tok") monkeypatch.setenv("DEVX_REPO_OWNER", "owner") monkeypatch.setenv("DEVX_REPO_NAME", "repo") client = mock_client_cls.return_value client.get_pr.return_value = {"head": {"sha": "abc123"}} client.get_commit_status.return_value = [ {"context": "CI / quality", "status": "failure"}, ] runner = CliRunner() result = runner.invoke(cli, ["--pr", "42"]) assert result.exit_code != 0 assert "failed" in result.output.lower() @patch("devx.tools.pr_status.GiteaClient") @patch("devx.tools.pr_status.subprocess.run") def test_auto_detect_branch( self, mock_subprocess: MagicMock, mock_client_cls: MagicMock, monkeypatch: pytest.MonkeyPatch ) -> None: monkeypatch.setenv("CI_GITEA_TOKEN", "tok") monkeypatch.setenv("DEVX_REPO_OWNER", "owner") monkeypatch.setenv("DEVX_REPO_NAME", "repo") mock_subprocess.return_value = MagicMock(returncode=0, stdout="feature-branch\n") client = mock_client_cls.return_value client.list_prs.return_value = [{"number": 42, "head": {"ref": "feature-branch"}}] client.get_pr.return_value = {"head": {"sha": "abc123"}} client.get_commit_status.return_value = [{"context": "CI / quality", "status": "success"}] runner = CliRunner() result = runner.invoke(cli, []) assert result.exit_code == 0 assert "PR #42" in result.output @patch("devx.tools.pr_status.GiteaClient") @patch("devx.tools.pr_status.subprocess.run") def test_auto_detect_no_pr_found( self, mock_subprocess: MagicMock, mock_client_cls: MagicMock, monkeypatch: pytest.MonkeyPatch ) -> None: monkeypatch.setenv("CI_GITEA_TOKEN", "tok") monkeypatch.setenv("DEVX_REPO_OWNER", "owner") monkeypatch.setenv("DEVX_REPO_NAME", "repo") mock_subprocess.return_value = MagicMock(returncode=0, stdout="feature-branch\n") client = mock_client_cls.return_value client.list_prs.return_value = [] runner = CliRunner() result = runner.invoke(cli, []) assert result.exit_code != 0 assert "No open PR" in result.output @patch("devx.tools.pr_status.GiteaClient") @patch("devx.tools.pr_status.subprocess.run") def test_auto_detect_branch_error( self, mock_subprocess: MagicMock, mock_client_cls: MagicMock, monkeypatch: pytest.MonkeyPatch ) -> None: monkeypatch.setenv("CI_GITEA_TOKEN", "tok") monkeypatch.setenv("DEVX_REPO_OWNER", "owner") monkeypatch.setenv("DEVX_REPO_NAME", "repo") mock_subprocess.return_value = MagicMock(returncode=1, stderr="git error\n") runner = CliRunner() result = runner.invoke(cli, []) assert result.exit_code != 0 assert "Could not detect" in result.output @patch("devx.tools.pr_status.subprocess.run") @patch("devx.tools.pr_status.GiteaClient") def test_no_sha_raises( self, mock_client_cls: MagicMock, mock_subproc: MagicMock, monkeypatch: pytest.MonkeyPatch ) -> None: monkeypatch.setenv("CI_GITEA_TOKEN", "tok") monkeypatch.setenv("DEVX_REPO_OWNER", "owner") monkeypatch.setenv("DEVX_REPO_NAME", "repo") client = mock_client_cls.return_value client.get_pr.return_value = {"head": {}} runner = CliRunner() result = runner.invoke(cli, ["--pr", "42"]) assert result.exit_code != 0 assert "SHA" in result.output @patch("devx.tools.pr_status._get_current_branch_pr") @patch("devx.tools.pr_status.time.sleep") @patch("devx.tools.pr_status.time.time", side_effect=[0, 0, 100, 200]) @patch("devx.tools.pr_status.GiteaClient") def test_wait_success( self, mock_client_cls: MagicMock, mock_time: MagicMock, mock_sleep: MagicMock, mock_branch_pr: MagicMock, monkeypatch: pytest.MonkeyPatch, ) -> None: monkeypatch.setenv("CI_GITEA_TOKEN", "tok") monkeypatch.setenv("DEVX_REPO_OWNER", "owner") monkeypatch.setenv("DEVX_REPO_NAME", "repo") client = mock_client_cls.return_value client.get_pr.return_value = {"head": {"sha": "abc123"}} client.get_commit_status.side_effect = [ [{"context": "CI / quality", "status": "pending"}], [{"context": "CI / quality", "status": "success"}], ] runner = CliRunner() result = runner.invoke(cli, ["--pr", "42", "--wait", "--timeout", "600", "--interval", "1"]) assert result.exit_code == 0 assert "[OK]" in result.output @patch("devx.tools.pr_status._get_current_branch_pr") @patch("devx.tools.pr_status.time.sleep") @patch("devx.tools.pr_status.time.time", side_effect=[0, 700]) @patch("devx.tools.pr_status.GiteaClient") def test_wait_timeout( self, mock_client_cls: MagicMock, mock_time: MagicMock, mock_sleep: MagicMock, mock_branch_pr: MagicMock, monkeypatch: pytest.MonkeyPatch, ) -> None: monkeypatch.setenv("CI_GITEA_TOKEN", "tok") monkeypatch.setenv("DEVX_REPO_OWNER", "owner") monkeypatch.setenv("DEVX_REPO_NAME", "repo") client = mock_client_cls.return_value client.get_pr.return_value = {"head": {"sha": "abc123"}} client.get_commit_status.return_value = [{"context": "CI / quality", "status": "pending"}] runner = CliRunner() result = runner.invoke(cli, ["--pr", "42", "--wait", "--timeout", "600", "--interval", "1"]) assert result.exit_code != 0 assert "timeout" in result.output.lower()