| | |
| | |
| |
|
| | """ |
| | Unit tests for the Code Analyzer service |
| | """ |
| |
|
| | import unittest |
| | from unittest.mock import patch, MagicMock, mock_open |
| | import os |
| | import sys |
| | import json |
| | from pathlib import Path |
| |
|
| | |
| | project_root = Path(__file__).resolve().parent.parent |
| | sys.path.insert(0, str(project_root)) |
| |
|
| | from src.services.code_analyzer import CodeAnalyzer |
| |
|
| |
|
| | class TestCodeAnalyzer(unittest.TestCase): |
| | """Test cases for the CodeAnalyzer class""" |
| |
|
| | def setUp(self): |
| | """Set up test fixtures""" |
| | self.analyzer = CodeAnalyzer() |
| | self.test_repo_path = "/test/repo" |
| | |
| | @patch('os.path.exists') |
| | @patch('subprocess.run') |
| | def test_analyze_python_code(self, mock_run, mock_exists): |
| | """Test analyze_python_code method""" |
| | |
| | mock_exists.return_value = True |
| | |
| | |
| | mock_process = MagicMock() |
| | mock_process.returncode = 0 |
| | mock_process.stdout = json.dumps({ |
| | "messages": [ |
| | { |
| | "type": "convention", |
| | "module": "test_module", |
| | "obj": "", |
| | "line": 10, |
| | "column": 0, |
| | "path": "test.py", |
| | "symbol": "missing-docstring", |
| | "message": "Missing module docstring", |
| | "message-id": "C0111" |
| | } |
| | ] |
| | }) |
| | mock_run.return_value = mock_process |
| | |
| | |
| | with patch.object(self.analyzer, '_find_files', return_value=['/test/repo/test.py']): |
| | |
| | result = self.analyzer.analyze_python_code(self.test_repo_path) |
| | |
| | |
| | self.assertEqual(len(result['issues']), 1) |
| | self.assertEqual(result['issue_count'], 1) |
| | self.assertEqual(result['issues'][0]['type'], 'convention') |
| | self.assertEqual(result['issues'][0]['file'], 'test.py') |
| | self.assertEqual(result['issues'][0]['line'], 10) |
| | self.assertEqual(result['issues'][0]['message'], 'Missing module docstring') |
| | |
| | @patch('os.path.exists') |
| | @patch('subprocess.run') |
| | def test_analyze_javascript_code(self, mock_run, mock_exists): |
| | """Test analyze_javascript_code method""" |
| | |
| | mock_exists.return_value = True |
| | |
| | |
| | mock_process = MagicMock() |
| | mock_process.returncode = 0 |
| | mock_process.stdout = json.dumps([ |
| | { |
| | "filePath": "/test/repo/test.js", |
| | "messages": [ |
| | { |
| | "ruleId": "semi", |
| | "severity": 2, |
| | "message": "Missing semicolon.", |
| | "line": 5, |
| | "column": 20, |
| | "nodeType": "ExpressionStatement" |
| | } |
| | ], |
| | "errorCount": 1, |
| | "warningCount": 0, |
| | "fixableErrorCount": 1, |
| | "fixableWarningCount": 0 |
| | } |
| | ]) |
| | mock_run.return_value = mock_process |
| | |
| | |
| | with patch.object(self.analyzer, '_find_files', return_value=['/test/repo/test.js']): |
| | |
| | result = self.analyzer.analyze_javascript_code(self.test_repo_path) |
| | |
| | |
| | self.assertEqual(len(result['issues']), 1) |
| | self.assertEqual(result['issue_count'], 1) |
| | self.assertEqual(result['issues'][0]['type'], 'error') |
| | self.assertEqual(result['issues'][0]['file'], 'test.js') |
| | self.assertEqual(result['issues'][0]['line'], 5) |
| | self.assertEqual(result['issues'][0]['message'], 'Missing semicolon.') |
| | |
| | @patch('os.path.exists') |
| | @patch('subprocess.run') |
| | def test_analyze_typescript_code(self, mock_run, mock_exists): |
| | """Test analyze_typescript_code method""" |
| | |
| | mock_exists.return_value = True |
| | |
| | |
| | |
| | eslint_process = MagicMock() |
| | eslint_process.returncode = 0 |
| | eslint_process.stdout = json.dumps([ |
| | { |
| | "filePath": "/test/repo/test.ts", |
| | "messages": [ |
| | { |
| | "ruleId": "@typescript-eslint/no-unused-vars", |
| | "severity": 1, |
| | "message": "'x' is defined but never used.", |
| | "line": 3, |
| | "column": 7, |
| | "nodeType": "Identifier" |
| | } |
| | ], |
| | "errorCount": 0, |
| | "warningCount": 1, |
| | "fixableErrorCount": 0, |
| | "fixableWarningCount": 0 |
| | } |
| | ]) |
| | |
| | |
| | tsc_process = MagicMock() |
| | tsc_process.returncode = 2 |
| | tsc_process.stderr = "test.ts(10,15): error TS2339: Property 'foo' does not exist on type 'Bar'." |
| | |
| | |
| | mock_run.side_effect = [eslint_process, tsc_process] |
| | |
| | |
| | with patch.object(self.analyzer, '_find_files', return_value=['/test/repo/test.ts']): |
| | |
| | result = self.analyzer.analyze_typescript_code(self.test_repo_path) |
| | |
| | |
| | self.assertEqual(len(result['issues']), 2) |
| | self.assertEqual(result['issue_count'], 2) |
| | |
| | |
| | eslint_issue = next(issue for issue in result['issues'] if issue['source'] == 'eslint') |
| | self.assertEqual(eslint_issue['type'], 'warning') |
| | self.assertEqual(eslint_issue['file'], 'test.ts') |
| | self.assertEqual(eslint_issue['line'], 3) |
| | self.assertEqual(eslint_issue['message'], "'x' is defined but never used.") |
| | |
| | |
| | tsc_issue = next(issue for issue in result['issues'] if issue['source'] == 'tsc') |
| | self.assertEqual(tsc_issue['type'], 'error') |
| | self.assertEqual(tsc_issue['file'], 'test.ts') |
| | self.assertEqual(tsc_issue['line'], 10) |
| | self.assertEqual(tsc_issue['message'], "Property 'foo' does not exist on type 'Bar'.") |
| | |
| | @patch('os.path.exists') |
| | @patch('subprocess.run') |
| | def test_analyze_java_code(self, mock_run, mock_exists): |
| | """Test analyze_java_code method""" |
| | |
| | mock_exists.return_value = True |
| | |
| | |
| | mock_process = MagicMock() |
| | mock_process.returncode = 0 |
| | mock_process.stdout = """ |
| | <?xml version="1.0" encoding="UTF-8"?> |
| | <pmd version="6.55.0" timestamp="2023-06-01T12:00:00.000"> |
| | <file name="/test/repo/Test.java"> |
| | <violation beginline="10" endline="10" begincolumn="5" endcolumn="20" rule="UnusedLocalVariable" ruleset="Best Practices" class="Test" method="main" variable="unusedVar" externalInfoUrl="https://pmd.github.io/pmd-6.55.0/pmd_rules_java_bestpractices.html#unusedlocalvariable" priority="3"> |
| | Avoid unused local variables such as 'unusedVar'. |
| | </violation> |
| | </file> |
| | </pmd> |
| | """ |
| | mock_run.return_value = mock_process |
| | |
| | |
| | with patch.object(self.analyzer, '_find_files', return_value=['/test/repo/Test.java']): |
| | |
| | result = self.analyzer.analyze_java_code(self.test_repo_path) |
| | |
| | |
| | self.assertEqual(len(result['issues']), 1) |
| | self.assertEqual(result['issue_count'], 1) |
| | self.assertEqual(result['issues'][0]['type'], 'warning') |
| | self.assertEqual(result['issues'][0]['file'], 'Test.java') |
| | self.assertEqual(result['issues'][0]['line'], 10) |
| | self.assertEqual(result['issues'][0]['message'], "Avoid unused local variables such as 'unusedVar'.") |
| | |
| | @patch('os.path.exists') |
| | @patch('subprocess.run') |
| | def test_analyze_go_code(self, mock_run, mock_exists): |
| | """Test analyze_go_code method""" |
| | |
| | mock_exists.return_value = True |
| | |
| | |
| | mock_process = MagicMock() |
| | mock_process.returncode = 0 |
| | mock_process.stdout = json.dumps({ |
| | "Issues": [ |
| | { |
| | "FromLinter": "gosimple", |
| | "Text": "S1000: should use a simple channel send/receive instead of select with a single case", |
| | "Pos": { |
| | "Filename": "test.go", |
| | "Line": 15, |
| | "Column": 2 |
| | }, |
| | "Severity": "warning" |
| | } |
| | ] |
| | }) |
| | mock_run.return_value = mock_process |
| | |
| | |
| | result = self.analyzer.analyze_go_code(self.test_repo_path) |
| | |
| | |
| | self.assertEqual(len(result['issues']), 1) |
| | self.assertEqual(result['issue_count'], 1) |
| | self.assertEqual(result['issues'][0]['type'], 'warning') |
| | self.assertEqual(result['issues'][0]['file'], 'test.go') |
| | self.assertEqual(result['issues'][0]['line'], 15) |
| | self.assertEqual(result['issues'][0]['message'], 'S1000: should use a simple channel send/receive instead of select with a single case') |
| | |
| | @patch('os.path.exists') |
| | @patch('subprocess.run') |
| | def test_analyze_rust_code(self, mock_run, mock_exists): |
| | """Test analyze_rust_code method""" |
| | |
| | mock_exists.return_value = True |
| | |
| | |
| | mock_process = MagicMock() |
| | mock_process.returncode = 0 |
| | mock_process.stdout = json.dumps({ |
| | "reason": "compiler-message", |
| | "message": { |
| | "rendered": "warning: unused variable: `x`\n --> src/main.rs:2:9\n |\n2 | let x = 5;\n | ^ help: if this is intentional, prefix it with an underscore: `_x`\n |\n = note: `#[warn(unused_variables)]` on by default\n\n", |
| | "children": [], |
| | "code": { |
| | "code": "unused_variables", |
| | "explanation": null |
| | }, |
| | "level": "warning", |
| | "message": "unused variable: `x`", |
| | "spans": [ |
| | { |
| | "byte_end": 26, |
| | "byte_start": 25, |
| | "column_end": 10, |
| | "column_start": 9, |
| | "expansion": null, |
| | "file_name": "src/main.rs", |
| | "is_primary": true, |
| | "label": "help: if this is intentional, prefix it with an underscore: `_x`", |
| | "line_end": 2, |
| | "line_start": 2, |
| | "suggested_replacement": "_x", |
| | "suggestion_applicability": "MachineApplicable", |
| | "text": [ |
| | { |
| | "highlight_end": 10, |
| | "highlight_start": 9, |
| | "text": " let x = 5;" |
| | } |
| | ] |
| | } |
| | ] |
| | } |
| | }) |
| | mock_run.return_value = mock_process |
| | |
| | |
| | result = self.analyzer.analyze_rust_code(self.test_repo_path) |
| | |
| | |
| | self.assertEqual(len(result['issues']), 1) |
| | self.assertEqual(result['issue_count'], 1) |
| | self.assertEqual(result['issues'][0]['type'], 'warning') |
| | self.assertEqual(result['issues'][0]['file'], 'src/main.rs') |
| | self.assertEqual(result['issues'][0]['line'], 2) |
| | self.assertEqual(result['issues'][0]['message'], 'unused variable: `x`') |
| | |
| | def test_analyze_code(self): |
| | """Test analyze_code method""" |
| | |
| | self.analyzer.analyze_python_code = MagicMock(return_value={ |
| | 'issues': [{'type': 'convention', 'file': 'test.py', 'line': 10, 'message': 'Test issue'}], |
| | 'issue_count': 1 |
| | }) |
| | self.analyzer.analyze_javascript_code = MagicMock(return_value={ |
| | 'issues': [{'type': 'error', 'file': 'test.js', 'line': 5, 'message': 'Test issue'}], |
| | 'issue_count': 1 |
| | }) |
| | |
| | |
| | result = self.analyzer.analyze_code(self.test_repo_path, ['Python', 'JavaScript']) |
| | |
| | |
| | self.assertEqual(len(result), 2) |
| | self.assertIn('Python', result) |
| | self.assertIn('JavaScript', result) |
| | self.assertEqual(result['Python']['issue_count'], 1) |
| | self.assertEqual(result['JavaScript']['issue_count'], 1) |
| | |
| | |
| | self.analyzer.analyze_python_code.assert_called_once_with(self.test_repo_path) |
| | self.analyzer.analyze_javascript_code.assert_called_once_with(self.test_repo_path) |
| | |
| | @patch('os.walk') |
| | def test_find_files(self, mock_walk): |
| | """Test _find_files method""" |
| | |
| | mock_walk.return_value = [ |
| | ('/test/repo', ['dir1'], ['file1.py', 'file2.js']), |
| | ('/test/repo/dir1', [], ['file3.py']) |
| | ] |
| | |
| | |
| | python_files = self.analyzer._find_files(self.test_repo_path, '.py') |
| | |
| | |
| | self.assertEqual(len(python_files), 2) |
| | self.assertIn('/test/repo/file1.py', python_files) |
| | self.assertIn('/test/repo/dir1/file3.py', python_files) |
| | |
| | @patch('os.path.exists') |
| | def test_check_tool_availability(self, mock_exists): |
| | """Test _check_tool_availability method""" |
| | |
| | mock_exists.side_effect = [True, False] |
| | |
| | |
| | result1 = self.analyzer._check_tool_availability('tool1') |
| | result2 = self.analyzer._check_tool_availability('tool2') |
| | |
| | |
| | self.assertTrue(result1) |
| | self.assertFalse(result2) |
| | |
| | @patch('subprocess.run') |
| | def test_run_command(self, mock_run): |
| | """Test _run_command method""" |
| | |
| | mock_process = MagicMock() |
| | mock_process.returncode = 0 |
| | mock_process.stdout = "Test output" |
| | mock_run.return_value = mock_process |
| | |
| | |
| | returncode, output = self.analyzer._run_command(['test', 'command']) |
| | |
| | |
| | self.assertEqual(returncode, 0) |
| | self.assertEqual(output, "Test output") |
| | mock_run.assert_called_once() |
| |
|
| |
|
| | if __name__ == "__main__": |
| | unittest.main() |