Coverage for tests / scripts / test_ci_post_pr_comment.py: 99%
96 statements
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
1#!/usr/bin/env python3
2"""Integration tests for ci-post-pr-comment.sh script.
4Tests the complete functionality of the CI post PR comment script including
5its interaction with the GitHub comment utilities.
7Google-style docstrings are used per project standards.
8"""
10from __future__ import annotations
12import os
13import subprocess
14import tempfile
15from pathlib import Path
16from unittest.mock import patch
18import pytest
19from assertpy import assert_that
22@pytest.fixture
23def ci_script_path() -> Path:
24 """Get path to ci-post-pr-comment.sh script.
26 Returns:
27 Path: Absolute path to the script.
28 """
29 return (
30 Path(__file__).parent.parent.parent
31 / "scripts"
32 / "ci"
33 / "github"
34 / "ci-post-pr-comment.sh"
35 )
38@pytest.fixture
39def sample_data_dir() -> Path:
40 """Get path to test_samples directory.
42 Returns:
43 Path: Absolute path to test_samples directory.
44 """
45 return Path(__file__).parent.parent.parent / "test_samples"
48def test_script_help_output(ci_script_path: Path) -> None:
49 """Test that the script displays help when requested.
51 Args:
52 ci_script_path: Path to the script being tested.
53 """
54 result = subprocess.run(
55 [str(ci_script_path), "--help"],
56 capture_output=True,
57 text=True,
58 check=True,
59 )
61 assert_that(result.stdout).contains("Usage:")
62 assert_that(result.stdout).contains("CI Post PR Comment Script")
63 assert_that(result.stdout).contains("GitHub Actions CI environment")
64 assert_that(result.returncode).is_equal_to(0)
67def test_script_help_short_flag(ci_script_path: Path) -> None:
68 """Test that the script displays help with -h flag.
70 Args:
71 ci_script_path: Path to the script being tested.
72 """
73 result = subprocess.run(
74 [str(ci_script_path), "-h"],
75 capture_output=True,
76 text=True,
77 check=True,
78 )
80 assert_that(result.stdout).contains("Usage:")
81 assert_that(result.returncode).is_equal_to(0)
84def test_script_exits_when_not_in_pr_context(ci_script_path: Path) -> None:
85 """Test that script exits gracefully when not in PR context.
87 Args:
88 ci_script_path: Path to the script being tested.
89 """
90 # Create a temporary comment file
91 with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f:
92 f.write("Test PR comment content")
93 comment_file = f.name
95 try:
96 # Run without PR environment variables
97 env = os.environ.copy()
98 # Remove PR-related environment variables if they exist
99 env.pop("GITHUB_EVENT_NAME", None)
100 env.pop("PR_NUMBER", None)
102 result = subprocess.run(
103 [str(ci_script_path), comment_file],
104 capture_output=True,
105 text=True,
106 env=env,
107 )
109 assert_that(result.returncode).is_equal_to(0)
110 output = result.stdout + result.stderr
111 assert_that(output).contains("Not in a PR context")
112 finally:
113 os.unlink(comment_file)
116def test_script_fails_with_missing_comment_file(ci_script_path: Path) -> None:
117 """Test that script fails when comment file doesn't exist.
119 Args:
120 ci_script_path: Path to the script being tested.
121 """
122 # Set up minimal PR context environment
123 env = os.environ.copy()
124 env.update(
125 {
126 "GITHUB_EVENT_NAME": "pull_request",
127 "PR_NUMBER": "123",
128 "GITHUB_REPOSITORY": "test/repo",
129 "GITHUB_TOKEN": "fake-token",
130 },
131 )
133 result = subprocess.run(
134 [str(ci_script_path), "nonexistent-file.txt"],
135 capture_output=True,
136 text=True,
137 env=env,
138 )
140 assert_that(result.returncode).is_equal_to(1)
141 # Error message might go to stdout or stderr, and may contain color codes
142 error_output = result.stdout + result.stderr
143 assert_that(error_output).contains("Comment file")
144 assert_that(error_output).contains("not found")
147def test_script_uses_default_comment_file(ci_script_path: Path) -> None:
148 """Test that script uses default comment file when none specified.
150 Args:
151 ci_script_path: Path to the script being tested.
152 """
153 # Create default comment file
154 with tempfile.TemporaryDirectory() as temp_dir:
155 comment_file = Path(temp_dir) / "pr-comment.txt"
156 comment_file.write_text("Default comment content")
158 # Set up minimal PR context environment
159 env = os.environ.copy()
160 env.update(
161 {
162 "GITHUB_EVENT_NAME": "pull_request",
163 "PR_NUMBER": "123",
164 "GITHUB_REPOSITORY": "test/repo",
165 "GITHUB_TOKEN": "fake-token",
166 },
167 )
169 # Change to temp directory so default file is found
170 result = subprocess.run(
171 [str(ci_script_path)],
172 capture_output=True,
173 text=True,
174 cwd=temp_dir,
175 env=env,
176 )
178 # Script should try to process the default file
179 # (may fail due to no actual GitHub API)
180 # but shouldn't fail due to missing file
181 error_output = result.stdout + result.stderr
182 assert_that(error_output).does_not_contain(
183 "Comment file pr-comment.txt not found",
184 )
187@patch.dict(
188 os.environ,
189 {
190 "GITHUB_EVENT_NAME": "pull_request",
191 "PR_NUMBER": "123",
192 "GITHUB_REPOSITORY": "test/repo",
193 "GITHUB_TOKEN": "fake-token",
194 },
195)
196def test_script_integrates_with_python_utilities(
197 ci_script_path: Path,
198 sample_data_dir: Path,
199) -> None:
200 """Test that script correctly integrates with Python utilities.
202 This test verifies the shell script calls the Python utilities with correct
203 arguments.
205 Args:
206 ci_script_path: Path to the script being tested.
207 sample_data_dir: Path to test sample files.
208 """
209 # Create a test comment file
210 with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f:
211 f.write("<!-- test-marker -->\n\nNew test comment content")
212 comment_file = f.name
214 try:
215 # Set environment for marker-based update
216 env = os.environ.copy()
217 env.update(
218 {
219 "MARKER": "<!-- test-marker -->",
220 "GITHUB_EVENT_NAME": "pull_request",
221 "PR_NUMBER": "123",
222 "GITHUB_REPOSITORY": "test/repo",
223 "GITHUB_TOKEN": "fake-token",
224 },
225 )
227 # Mock gh command to simulate finding existing comments
228 with patch("subprocess.run") as mock_run:
229 # First call will be to check for gh command
230 # Second call will be the actual API call that we want to test
231 # preparation for
232 mock_run.side_effect = [
233 # gh command check
234 subprocess.CompletedProcess(
235 args=["command", "-v", "gh"],
236 returncode=1, # gh not found, will use curl path
237 ),
238 # Our script should complete without API calls in test environment
239 ]
241 subprocess.run(
242 [str(ci_script_path), comment_file],
243 capture_output=True,
244 text=True,
245 env=env,
246 )
248 # The script should attempt to process the marker logic
249 # Even if it fails at the API call level, it should get through
250 # the utility calls
251 # In a real environment this would make API calls, but in our test
252 # environment
253 # it should at least validate the basic flow
255 finally:
256 os.unlink(comment_file)
259def test_script_syntax_check(ci_script_path: Path) -> None:
260 """Test that the script has valid bash syntax.
262 Args:
263 ci_script_path: Path to the script being tested.
265 Raises:
266 AssertionError: If the script has syntax errors.
267 """
268 result = subprocess.run(
269 ["bash", "-n", str(ci_script_path)],
270 capture_output=True,
271 text=True,
272 )
274 assert_that(result.returncode).is_equal_to(0)
275 if result.stderr:
276 raise AssertionError(f"Syntax error in script: {result.stderr}")
279def test_script_has_proper_shebang(ci_script_path: Path) -> None:
280 """Test that the script has proper shebang line.
282 Args:
283 ci_script_path: Path to the script being tested.
284 """
285 with open(ci_script_path) as f:
286 first_line = f.readline().strip()
288 # Accept both portable and traditional shebangs
289 assert_that(first_line).matches(r"^#!(/usr/bin/env bash|/bin/bash)")
292def test_script_sources_utilities(ci_script_path: Path) -> None:
293 """Test that the script sources the required utilities.
295 Args:
296 ci_script_path: Path to the script being tested.
297 """
298 content = ci_script_path.read_text()
300 # Should source the shared utilities using absolute path via SCRIPT_DIR
301 assert_that(content).contains('source "$SCRIPT_DIR/../../utils/utils.sh"')
303 # Should call the Python utilities we created
304 assert_that(content).contains("find_comment_with_marker.py")
305 assert_that(content).contains("extract_comment_body.py")
306 assert_that(content).contains("json_encode_body.py")
307 assert_that(content).contains("merge_pr_comment.py")
310def test_script_handles_marker_logic(ci_script_path: Path) -> None:
311 """Test that the script contains proper marker handling logic.
313 Args:
314 ci_script_path: Path to the script being tested.
315 """
316 content = ci_script_path.read_text()
318 # Should have marker-related logic
319 assert_that(content).contains('MARKER="${MARKER:-}"')
320 assert_that(content).contains('if [ -n "$MARKER" ];')
321 assert_that(content.lower()).contains("marker provided")
323 # Should handle both update and new comment scenarios
324 assert_that(content.lower()).contains("update existing comment")
325 assert_that(content.lower()).contains("create a new comment")
328def test_python_utilities_are_executable() -> None:
329 """Test that all Python utilities have executable permissions."""
330 utils_dir = Path(__file__).parent.parent.parent / "scripts" / "utils"
332 utilities = [
333 "find_comment_with_marker.py",
334 "extract_comment_body.py",
335 "json_encode_body.py",
336 ]
338 for utility in utilities:
339 utility_path = utils_dir / utility
340 assert_that(utility_path.exists()).is_true()
341 assert_that(os.access(utility_path, os.X_OK)).is_true()