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

1#!/usr/bin/env python3 

2"""Integration tests for ci-post-pr-comment.sh script. 

3 

4Tests the complete functionality of the CI post PR comment script including 

5its interaction with the GitHub comment utilities. 

6 

7Google-style docstrings are used per project standards. 

8""" 

9 

10from __future__ import annotations 

11 

12import os 

13import subprocess 

14import tempfile 

15from pathlib import Path 

16from unittest.mock import patch 

17 

18import pytest 

19from assertpy import assert_that 

20 

21 

22@pytest.fixture 

23def ci_script_path() -> Path: 

24 """Get path to ci-post-pr-comment.sh script. 

25 

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 ) 

36 

37 

38@pytest.fixture 

39def sample_data_dir() -> Path: 

40 """Get path to test_samples directory. 

41 

42 Returns: 

43 Path: Absolute path to test_samples directory. 

44 """ 

45 return Path(__file__).parent.parent.parent / "test_samples" 

46 

47 

48def test_script_help_output(ci_script_path: Path) -> None: 

49 """Test that the script displays help when requested. 

50 

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 ) 

60 

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) 

65 

66 

67def test_script_help_short_flag(ci_script_path: Path) -> None: 

68 """Test that the script displays help with -h flag. 

69 

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 ) 

79 

80 assert_that(result.stdout).contains("Usage:") 

81 assert_that(result.returncode).is_equal_to(0) 

82 

83 

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. 

86 

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 

94 

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) 

101 

102 result = subprocess.run( 

103 [str(ci_script_path), comment_file], 

104 capture_output=True, 

105 text=True, 

106 env=env, 

107 ) 

108 

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) 

114 

115 

116def test_script_fails_with_missing_comment_file(ci_script_path: Path) -> None: 

117 """Test that script fails when comment file doesn't exist. 

118 

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 ) 

132 

133 result = subprocess.run( 

134 [str(ci_script_path), "nonexistent-file.txt"], 

135 capture_output=True, 

136 text=True, 

137 env=env, 

138 ) 

139 

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") 

145 

146 

147def test_script_uses_default_comment_file(ci_script_path: Path) -> None: 

148 """Test that script uses default comment file when none specified. 

149 

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") 

157 

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 ) 

168 

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 ) 

177 

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 ) 

185 

186 

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. 

201 

202 This test verifies the shell script calls the Python utilities with correct 

203 arguments. 

204 

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 

213 

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 ) 

226 

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 ] 

240 

241 subprocess.run( 

242 [str(ci_script_path), comment_file], 

243 capture_output=True, 

244 text=True, 

245 env=env, 

246 ) 

247 

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 

254 

255 finally: 

256 os.unlink(comment_file) 

257 

258 

259def test_script_syntax_check(ci_script_path: Path) -> None: 

260 """Test that the script has valid bash syntax. 

261 

262 Args: 

263 ci_script_path: Path to the script being tested. 

264 

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 ) 

273 

274 assert_that(result.returncode).is_equal_to(0) 

275 if result.stderr: 

276 raise AssertionError(f"Syntax error in script: {result.stderr}") 

277 

278 

279def test_script_has_proper_shebang(ci_script_path: Path) -> None: 

280 """Test that the script has proper shebang line. 

281 

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() 

287 

288 # Accept both portable and traditional shebangs 

289 assert_that(first_line).matches(r"^#!(/usr/bin/env bash|/bin/bash)") 

290 

291 

292def test_script_sources_utilities(ci_script_path: Path) -> None: 

293 """Test that the script sources the required utilities. 

294 

295 Args: 

296 ci_script_path: Path to the script being tested. 

297 """ 

298 content = ci_script_path.read_text() 

299 

300 # Should source the shared utilities using absolute path via SCRIPT_DIR 

301 assert_that(content).contains('source "$SCRIPT_DIR/../../utils/utils.sh"') 

302 

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") 

308 

309 

310def test_script_handles_marker_logic(ci_script_path: Path) -> None: 

311 """Test that the script contains proper marker handling logic. 

312 

313 Args: 

314 ci_script_path: Path to the script being tested. 

315 """ 

316 content = ci_script_path.read_text() 

317 

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") 

322 

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") 

326 

327 

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" 

331 

332 utilities = [ 

333 "find_comment_with_marker.py", 

334 "extract_comment_body.py", 

335 "json_encode_body.py", 

336 ] 

337 

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()