-
Notifications
You must be signed in to change notification settings - Fork 0
🧪 [Add unit tests and implementation for get_pr_status.py] #193
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
7177cfd
ec46db5
a64fbbe
5258f9a
84a1e20
d700a11
24b063e
38234c5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,59 @@ | ||||||||||||||
| #!/usr/bin/env python3 | ||||||||||||||
| import subprocess | ||||||||||||||
| import json | ||||||||||||||
| import sys | ||||||||||||||
| from typing import Sequence | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| def run_cmd(argv: Sequence[str]) -> str: | ||||||||||||||
| """Runs a command and returns stripped stdout.""" | ||||||||||||||
| result = subprocess.run(argv, shell=False, capture_output=True, text=True, check=True, timeout=30) | ||||||||||||||
| return result.stdout.strip() | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| def get_pr_status(pr_id: str = "") -> None: | ||||||||||||||
| """Fetches and prints the status of a PR using gh CLI.""" | ||||||||||||||
| cmd = ["gh", "pr", "view"] | ||||||||||||||
| if pr_id: | ||||||||||||||
| cmd.append(pr_id) | ||||||||||||||
| cmd.extend(["--json", "state,reviewDecision,statusCheckRollup"]) | ||||||||||||||
|
|
||||||||||||||
| try: | ||||||||||||||
| output = run_cmd(cmd) | ||||||||||||||
| data = json.loads(output) | ||||||||||||||
|
|
||||||||||||||
| state = data.get("state") | ||||||||||||||
| review = data.get("reviewDecision") or "NONE" | ||||||||||||||
| checks = data.get("statusCheckRollup", []) | ||||||||||||||
|
|
||||||||||||||
| # Summarize checks | ||||||||||||||
| success_count = 0 | ||||||||||||||
| total_count = len(checks) | ||||||||||||||
| for check in checks: | ||||||||||||||
| # gh CLI returns conclusion for CheckRun and state for StatusContext | ||||||||||||||
| conclusion = check.get("conclusion") or check.get("state") | ||||||||||||||
| if conclusion == "SUCCESS": | ||||||||||||||
| success_count += 1 | ||||||||||||||
|
Comment on lines
+34
to
+36
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||
|
|
||||||||||||||
| print(f"PR Status: {state}") | ||||||||||||||
| print(f"Review Decision: {review}") | ||||||||||||||
| print(f"Checks: {success_count}/{total_count} passed") | ||||||||||||||
|
|
||||||||||||||
| except subprocess.CalledProcessError as e: | ||||||||||||||
| print(f"Error: Failed to fetch PR status: {e.stderr.strip()}", file=sys.stderr) | ||||||||||||||
|
Comment on lines
+42
to
+43
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If Using
Suggested change
|
||||||||||||||
| sys.exit(1) | ||||||||||||||
| except json.JSONDecodeError: | ||||||||||||||
| print("Error: Failed to parse gh CLI output", file=sys.stderr) | ||||||||||||||
| sys.exit(1) | ||||||||||||||
| except Exception as e: | ||||||||||||||
| print(f"An unexpected error occurred: {e}", file=sys.stderr) | ||||||||||||||
| sys.exit(1) | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| def main(): | ||||||||||||||
| pr_id = sys.argv[1] if len(sys.argv) > 1 else "" | ||||||||||||||
| get_pr_status(pr_id) | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| if __name__ == "__main__": | ||||||||||||||
| main() | ||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,84 @@ | ||||||||||||||||||||||
| import pytest | ||||||||||||||||||||||
| import subprocess | ||||||||||||||||||||||
| import json | ||||||||||||||||||||||
| from unittest.mock import patch, MagicMock | ||||||||||||||||||||||
| from scripts.get_pr_status import run_cmd, get_pr_status | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| def test_run_cmd_success(): | ||||||||||||||||||||||
| output = run_cmd(["echo", "hello"]) | ||||||||||||||||||||||
| assert output == "hello" | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| def test_run_cmd_strips_whitespace(): | ||||||||||||||||||||||
| output = run_cmd(["echo", " hello "]) | ||||||||||||||||||||||
| assert output == "hello" | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| def test_run_cmd_error(): | ||||||||||||||||||||||
| with pytest.raises(subprocess.CalledProcessError): | ||||||||||||||||||||||
| run_cmd(["false"]) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| @patch("scripts.get_pr_status.subprocess.run") | ||||||||||||||||||||||
| def test_run_cmd_timeout(mock_run): | ||||||||||||||||||||||
| # run_cmd has a 30s timeout. We mock subprocess.run to raise it immediately. | ||||||||||||||||||||||
| mock_run.side_effect = subprocess.TimeoutExpired(["sleep", "0.1"], 30) | ||||||||||||||||||||||
| with pytest.raises(subprocess.TimeoutExpired): | ||||||||||||||||||||||
| run_cmd(["sleep", "0.1"]) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| @patch("scripts.get_pr_status.run_cmd") | ||||||||||||||||||||||
| def test_get_pr_status_success(mock_run_cmd, capsys): | ||||||||||||||||||||||
| mock_data = { | ||||||||||||||||||||||
| "state": "OPEN", | ||||||||||||||||||||||
| "reviewDecision": "APPROVED", | ||||||||||||||||||||||
| "statusCheckRollup": [ | ||||||||||||||||||||||
| {"conclusion": "SUCCESS", "name": "test1"}, | ||||||||||||||||||||||
| {"state": "SUCCESS", "name": "test2"}, | ||||||||||||||||||||||
| {"conclusion": "FAILURE", "name": "test3"} | ||||||||||||||||||||||
| ] | ||||||||||||||||||||||
|
Comment on lines
+31
to
+35
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Update the mocked status check data to use a valid state/conclusion (such as
Suggested change
|
||||||||||||||||||||||
| } | ||||||||||||||||||||||
| mock_run_cmd.return_value = json.dumps(mock_data) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| get_pr_status("123") | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| captured = capsys.readouterr() | ||||||||||||||||||||||
| assert "PR Status: OPEN" in captured.out | ||||||||||||||||||||||
| assert "Review Decision: APPROVED" in captured.out | ||||||||||||||||||||||
| assert "Checks: 2/3 passed" in captured.out | ||||||||||||||||||||||
| mock_run_cmd.assert_called_once_with(["gh", "pr", "view", "123", "--json", "state,reviewDecision,statusCheckRollup"]) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| @patch("scripts.get_pr_status.run_cmd") | ||||||||||||||||||||||
| def test_get_pr_status_no_id(mock_run_cmd, capsys): | ||||||||||||||||||||||
| mock_data = { | ||||||||||||||||||||||
| "state": "MERGED", | ||||||||||||||||||||||
| "reviewDecision": None, | ||||||||||||||||||||||
| "statusCheckRollup": [] | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| mock_run_cmd.return_value = json.dumps(mock_data) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| get_pr_status() | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| captured = capsys.readouterr() | ||||||||||||||||||||||
| assert "PR Status: MERGED" in captured.out | ||||||||||||||||||||||
| assert "Review Decision: NONE" in captured.out | ||||||||||||||||||||||
| assert "Checks: 0/0 passed" in captured.out | ||||||||||||||||||||||
| mock_run_cmd.assert_called_once_with(["gh", "pr", "view", "--json", "state,reviewDecision,statusCheckRollup"]) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| @patch("scripts.get_pr_status.run_cmd") | ||||||||||||||||||||||
| def test_get_pr_status_error(mock_run_cmd, capsys): | ||||||||||||||||||||||
| mock_run_cmd.side_effect = subprocess.CalledProcessError(1, ["gh"], stderr="gh not found") | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| with pytest.raises(SystemExit) as e: | ||||||||||||||||||||||
| get_pr_status("123") | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| assert e.value.code == 1 | ||||||||||||||||||||||
| captured = capsys.readouterr() | ||||||||||||||||||||||
| assert "Error: Failed to fetch PR status: gh not found" in captured.err | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| @patch("scripts.get_pr_status.run_cmd") | ||||||||||||||||||||||
| def test_get_pr_status_invalid_json(mock_run_cmd, capsys): | ||||||||||||||||||||||
| mock_run_cmd.return_value = "invalid json" | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| with pytest.raises(SystemExit) as e: | ||||||||||||||||||||||
| get_pr_status("123") | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| assert e.value.code == 1 | ||||||||||||||||||||||
| captured = capsys.readouterr() | ||||||||||||||||||||||
| assert "Error: Failed to parse gh CLI output" in captured.err | ||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If
statusCheckRollupis present in the JSON but has anullvalue (which is common when there are no status checks),data.get("statusCheckRollup", [])will returnNoneinstead of[]. This will cause aTypeErrorwhen callinglen(checks)or iterating overchecks.Using
data.get("statusCheckRollup") or []ensureschecksis always a list.