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