coverage_check_test.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. #!/usr/bin/env python3
  2. """
  3. Tests for coverage_check script.
  4. """
  5. import os
  6. import sys
  7. import unittest
  8. import subprocess
  9. import tempfile
  10. from unittest.mock import patch, MagicMock, call, mock_open
  11. # Add parent directory to path so we can import coverage modules
  12. sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
  13. from coverage_check import extract_coverage, compare_coverage, set_verbose, generate_comment, post_comment, set_github_output
  14. from coverage_check.util import log, file_exists, get_file_size, list_directory
  15. class TestCoverage(unittest.TestCase):
  16. # Class variables to store coverage files
  17. temp_dir = None
  18. extension_coverage_file = None
  19. webview_coverage_file = None
  20. @classmethod
  21. def setUpClass(cls):
  22. """Set up test environment once for all tests."""
  23. # Create temporary directory for test files
  24. cls.temp_dir = tempfile.TemporaryDirectory()
  25. cls.extension_coverage_file = os.path.join(cls.temp_dir.name, 'extension_coverage.txt')
  26. cls.webview_coverage_file = os.path.join(cls.temp_dir.name, 'webview_coverage.txt')
  27. # Run actual tests to generate coverage reports
  28. cls.generate_coverage_reports()
  29. # Verify files exist and are not empty
  30. assert os.path.exists(cls.extension_coverage_file), \
  31. f"Extension coverage file {cls.extension_coverage_file} does not exist"
  32. assert os.path.getsize(cls.extension_coverage_file) > 0, \
  33. f"Extension coverage file {cls.extension_coverage_file} is empty"
  34. assert os.path.exists(cls.webview_coverage_file), \
  35. f"Webview coverage file {cls.webview_coverage_file} does not exist"
  36. assert os.path.getsize(cls.webview_coverage_file) > 0, \
  37. f"Webview coverage file {cls.webview_coverage_file} is empty"
  38. @classmethod
  39. def tearDownClass(cls):
  40. """Clean up test environment after all tests."""
  41. if cls.temp_dir:
  42. cls.temp_dir.cleanup()
  43. @classmethod
  44. def generate_coverage_reports(cls):
  45. """Generate real coverage reports by running tests."""
  46. log("Generating coverage reports (this may take a while)...")
  47. # Run extension tests with coverage
  48. try:
  49. # Get absolute paths
  50. root_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../..'))
  51. webview_dir = os.path.join(root_dir, 'webview-ui')
  52. # Use xvfb-run on Linux
  53. if sys.platform.startswith('linux'):
  54. cmd = f"cd {root_dir} && xvfb-run -a npm run test:coverage > {cls.extension_coverage_file} 2>&1"
  55. else:
  56. cmd = f"cd {root_dir} && npm run test:coverage > {cls.extension_coverage_file} 2>&1"
  57. log("Running extension tests...")
  58. log(f"Command: {cmd}")
  59. result = subprocess.run(cmd, shell=True, check=False, capture_output=True, text=True)
  60. log(f"Extension tests exit code: {result.returncode}")
  61. # Run webview tests with coverage
  62. log("Running webview tests...")
  63. cmd = f"cd {webview_dir} && npm run test:coverage > {cls.webview_coverage_file} 2>&1"
  64. log(f"Command: {cmd}")
  65. result = subprocess.run(cmd, shell=True, check=False, capture_output=True, text=True)
  66. log(f"Webview tests exit code: {result.returncode}")
  67. # Verify files were created
  68. if file_exists(cls.extension_coverage_file):
  69. ext_size = get_file_size(cls.extension_coverage_file)
  70. log(f"Extension coverage file created: {cls.extension_coverage_file} (size: {ext_size} bytes)")
  71. else:
  72. log(f"WARNING: Extension coverage file was not created: {cls.extension_coverage_file}")
  73. if file_exists(cls.webview_coverage_file):
  74. web_size = get_file_size(cls.webview_coverage_file)
  75. log(f"Webview coverage file created: {cls.webview_coverage_file} (size: {web_size} bytes)")
  76. else:
  77. log(f"WARNING: Webview coverage file was not created: {cls.webview_coverage_file}")
  78. log("Coverage reports generation completed.")
  79. except Exception as e:
  80. log(f"Error generating coverage reports: {e}")
  81. import traceback
  82. log(traceback.format_exc())
  83. # Create empty files if tests fail
  84. log("Creating fallback coverage files...")
  85. with open(cls.extension_coverage_file, 'w') as f:
  86. f.write("No coverage data available")
  87. with open(cls.webview_coverage_file, 'w') as f:
  88. f.write("No coverage data available")
  89. def test_extract_coverage(self):
  90. """Test extract_coverage function with both extension and webview coverage."""
  91. # Check if verbose mode is enabled
  92. if '-v' in sys.argv or '--verbose' in sys.argv:
  93. set_verbose(True)
  94. # Verify files exist before testing
  95. self.assertTrue(file_exists(self.extension_coverage_file),
  96. f"Extension coverage file does not exist: {self.extension_coverage_file}")
  97. self.assertTrue(file_exists(self.webview_coverage_file),
  98. f"Webview coverage file does not exist: {self.webview_coverage_file}")
  99. # Log file sizes
  100. ext_size = get_file_size(self.extension_coverage_file)
  101. web_size = get_file_size(self.webview_coverage_file)
  102. log(f"Extension coverage file size: {ext_size} bytes")
  103. log(f"Webview coverage file size: {web_size} bytes")
  104. # Test extension coverage
  105. log("Testing extension coverage extraction...")
  106. ext_coverage_pct = extract_coverage(self.extension_coverage_file, 'extension')
  107. # Check that coverage percentage is a float
  108. self.assertIsInstance(ext_coverage_pct, float)
  109. # Check that coverage percentage is between 0 and 100
  110. self.assertGreaterEqual(ext_coverage_pct, 0)
  111. self.assertLessEqual(ext_coverage_pct, 100)
  112. # Log coverage percentage for debugging
  113. log(f"Extension coverage: {ext_coverage_pct}%")
  114. # Test webview coverage
  115. log("Testing webview coverage extraction...")
  116. web_coverage_pct = extract_coverage(self.webview_coverage_file, 'webview')
  117. # Convert to float if it's an integer
  118. if isinstance(web_coverage_pct, int):
  119. web_coverage_pct = float(web_coverage_pct)
  120. # Check that coverage percentage is a float
  121. self.assertIsInstance(web_coverage_pct, float)
  122. # Check that coverage percentage is between 0 and 100
  123. self.assertGreaterEqual(web_coverage_pct, 0)
  124. self.assertLessEqual(web_coverage_pct, 100)
  125. # Log coverage percentage for debugging
  126. log(f"Webview coverage: {web_coverage_pct}%")
  127. def test_compare_coverage(self):
  128. """Test compare_coverage function."""
  129. # Test with coverage increase
  130. decreased, diff = compare_coverage(80, 90)
  131. self.assertFalse(decreased)
  132. self.assertEqual(diff, 10)
  133. # Test with coverage decrease
  134. decreased, diff = compare_coverage(90, 80)
  135. self.assertTrue(decreased)
  136. self.assertEqual(diff, 10)
  137. # Test with no change
  138. decreased, diff = compare_coverage(80, 80)
  139. self.assertFalse(decreased)
  140. self.assertEqual(diff, 0)
  141. def test_generate_comment(self):
  142. """Test generate_comment function."""
  143. comment = generate_comment(
  144. 80, 90, 'false', 10,
  145. 70, 75, 'false', 5
  146. )
  147. # Check that comment contains expected sections
  148. self.assertIn('Coverage Report', comment)
  149. self.assertIn('Extension Coverage', comment)
  150. self.assertIn('Webview Coverage', comment)
  151. self.assertIn('Overall Assessment', comment)
  152. # Check that comment contains coverage percentages
  153. self.assertIn('Base branch: 80%', comment)
  154. self.assertIn('PR branch: 90%', comment)
  155. self.assertIn('Base branch: 70%', comment)
  156. self.assertIn('PR branch: 75%', comment)
  157. # Check that comment contains correct assessment
  158. self.assertIn('Coverage increased or remained the same', comment)
  159. self.assertIn('Test coverage has been maintained or improved', comment)
  160. @patch('coverage_check.requests.get')
  161. @patch('coverage_check.requests.post')
  162. @patch('coverage_check.requests.patch')
  163. def test_post_comment_new(self, mock_patch, mock_post, mock_get):
  164. """Test post_comment function when creating a new comment."""
  165. # Create a temporary comment file
  166. comment_file = os.path.join(self.temp_dir.name, 'comment.md')
  167. with open(comment_file, 'w') as f:
  168. f.write('<!-- COVERAGE_REPORT -->\nTest comment')
  169. # Mock the API responses
  170. mock_get.return_value = MagicMock(status_code=200, json=lambda: [])
  171. mock_post.return_value = MagicMock(status_code=201)
  172. # Test post_comment function
  173. post_comment(comment_file, '123', 'owner/repo', 'token')
  174. # Check that the correct API calls were made
  175. mock_get.assert_called_once()
  176. mock_post.assert_called_once()
  177. mock_patch.assert_not_called()
  178. @patch('coverage_check.requests.get')
  179. @patch('coverage_check.requests.post')
  180. @patch('coverage_check.requests.patch')
  181. def test_post_comment_update(self, mock_patch, mock_post, mock_get):
  182. """Test post_comment function when updating an existing comment."""
  183. # Create a temporary comment file
  184. comment_file = os.path.join(self.temp_dir.name, 'comment.md')
  185. with open(comment_file, 'w') as f:
  186. f.write('<!-- COVERAGE_REPORT -->\nTest comment')
  187. # Mock the API responses
  188. mock_get.return_value = MagicMock(
  189. status_code=200,
  190. json=lambda: [{'id': 456, 'body': '<!-- COVERAGE_REPORT -->\nOld comment'}]
  191. )
  192. mock_patch.return_value = MagicMock(status_code=200)
  193. # Test post_comment function
  194. post_comment(comment_file, '123', 'owner/repo', 'token')
  195. # Check that the correct API calls were made
  196. mock_get.assert_called_once()
  197. mock_patch.assert_called_once()
  198. mock_post.assert_not_called()
  199. def test_set_github_output(self):
  200. """Test set_github_output function."""
  201. # Capture stdout
  202. with patch('sys.stdout', new=MagicMock()) as mock_stdout:
  203. # Mock environment without GITHUB_OUTPUT
  204. with patch.dict('os.environ', {}, clear=True):
  205. set_github_output('test_name', 'test_value')
  206. # Check that the correct output was printed to stdout
  207. mock_stdout.assert_has_calls([
  208. # GitHub Actions output format (deprecated method)
  209. call.write('::set-output name=test_name::test_value\n'),
  210. call.flush(),
  211. # Human readable format
  212. call.write('test_name: test_value\n'),
  213. call.flush()
  214. ], any_order=False)
  215. # Reset mock for next test
  216. mock_stdout.reset_mock()
  217. # Test with GITHUB_OUTPUT environment variable
  218. with patch.dict('os.environ', {'GITHUB_OUTPUT': '/tmp/github_output'}), \
  219. patch('builtins.open', mock_open()) as mock_file:
  220. set_github_output('test_name', 'test_value')
  221. # Check that file was written to
  222. mock_file.assert_called_once_with('/tmp/github_output', 'a')
  223. mock_file().write.assert_called_once_with('test_name=test_value\n')
  224. # Check that human readable output was printed
  225. mock_stdout.assert_has_calls([
  226. call.write('test_name: test_value\n'),
  227. call.flush()
  228. ], any_order=False)
  229. if __name__ == '__main__':
  230. unittest.main()