Spaces:
Sleeping
Sleeping
#!/usr/bin/env python3 | |
# -*- coding: utf-8 -*- | |
""" | |
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 | |
# Add the project root directory to the Python 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" | |
def test_analyze_python_code(self, mock_run, mock_exists): | |
"""Test analyze_python_code method""" | |
# Set up the mocks | |
mock_exists.return_value = True | |
# Mock the subprocess.run result | |
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 | |
# Mock the file discovery | |
with patch.object(self.analyzer, '_find_files', return_value=['/test/repo/test.py']): | |
# Call the method | |
result = self.analyzer.analyze_python_code(self.test_repo_path) | |
# Verify the result | |
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') | |
def test_analyze_javascript_code(self, mock_run, mock_exists): | |
"""Test analyze_javascript_code method""" | |
# Set up the mocks | |
mock_exists.return_value = True | |
# Mock the subprocess.run result | |
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 | |
# Mock the file discovery | |
with patch.object(self.analyzer, '_find_files', return_value=['/test/repo/test.js']): | |
# Call the method | |
result = self.analyzer.analyze_javascript_code(self.test_repo_path) | |
# Verify the result | |
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.') | |
def test_analyze_typescript_code(self, mock_run, mock_exists): | |
"""Test analyze_typescript_code method""" | |
# Set up the mocks | |
mock_exists.return_value = True | |
# Mock the subprocess.run results | |
# First for ESLint | |
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 | |
} | |
]) | |
# Then for TSC | |
tsc_process = MagicMock() | |
tsc_process.returncode = 2 # Error code for TypeScript compiler | |
tsc_process.stderr = "test.ts(10,15): error TS2339: Property 'foo' does not exist on type 'Bar'." | |
# Set up the mock to return different values on consecutive calls | |
mock_run.side_effect = [eslint_process, tsc_process] | |
# Mock the file discovery | |
with patch.object(self.analyzer, '_find_files', return_value=['/test/repo/test.ts']): | |
# Call the method | |
result = self.analyzer.analyze_typescript_code(self.test_repo_path) | |
# Verify the result | |
self.assertEqual(len(result['issues']), 2) # One from ESLint, one from TSC | |
self.assertEqual(result['issue_count'], 2) | |
# Check the ESLint issue | |
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.") | |
# Check the TSC issue | |
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'.") | |
def test_analyze_java_code(self, mock_run, mock_exists): | |
"""Test analyze_java_code method""" | |
# Set up the mocks | |
mock_exists.return_value = True | |
# Mock the subprocess.run result | |
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 | |
# Mock the file discovery | |
with patch.object(self.analyzer, '_find_files', return_value=['/test/repo/Test.java']): | |
# Call the method | |
result = self.analyzer.analyze_java_code(self.test_repo_path) | |
# Verify the result | |
self.assertEqual(len(result['issues']), 1) | |
self.assertEqual(result['issue_count'], 1) | |
self.assertEqual(result['issues'][0]['type'], 'warning') # Priority 3 maps to 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'.") | |
def test_analyze_go_code(self, mock_run, mock_exists): | |
"""Test analyze_go_code method""" | |
# Set up the mocks | |
mock_exists.return_value = True | |
# Mock the subprocess.run result | |
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 | |
# Call the method | |
result = self.analyzer.analyze_go_code(self.test_repo_path) | |
# Verify the result | |
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') | |
def test_analyze_rust_code(self, mock_run, mock_exists): | |
"""Test analyze_rust_code method""" | |
# Set up the mocks | |
mock_exists.return_value = True | |
# Mock the subprocess.run result | |
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 | |
# Call the method | |
result = self.analyzer.analyze_rust_code(self.test_repo_path) | |
# Verify the result | |
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""" | |
# Mock the language-specific analysis methods | |
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 | |
}) | |
# Call the method | |
result = self.analyzer.analyze_code(self.test_repo_path, ['Python', 'JavaScript']) | |
# Verify the result | |
self.assertEqual(len(result), 2) # Two languages | |
self.assertIn('Python', result) | |
self.assertIn('JavaScript', result) | |
self.assertEqual(result['Python']['issue_count'], 1) | |
self.assertEqual(result['JavaScript']['issue_count'], 1) | |
# Verify the method calls | |
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) | |
def test_find_files(self, mock_walk): | |
"""Test _find_files method""" | |
# Set up the mock | |
mock_walk.return_value = [ | |
('/test/repo', ['dir1'], ['file1.py', 'file2.js']), | |
('/test/repo/dir1', [], ['file3.py']) | |
] | |
# Call the method | |
python_files = self.analyzer._find_files(self.test_repo_path, '.py') | |
# Verify the result | |
self.assertEqual(len(python_files), 2) | |
self.assertIn('/test/repo/file1.py', python_files) | |
self.assertIn('/test/repo/dir1/file3.py', python_files) | |
def test_check_tool_availability(self, mock_exists): | |
"""Test _check_tool_availability method""" | |
# Set up the mock | |
mock_exists.side_effect = [True, False] # First tool exists, second doesn't | |
# Call the method | |
result1 = self.analyzer._check_tool_availability('tool1') | |
result2 = self.analyzer._check_tool_availability('tool2') | |
# Verify the result | |
self.assertTrue(result1) | |
self.assertFalse(result2) | |
def test_run_command(self, mock_run): | |
"""Test _run_command method""" | |
# Set up the mock | |
mock_process = MagicMock() | |
mock_process.returncode = 0 | |
mock_process.stdout = "Test output" | |
mock_run.return_value = mock_process | |
# Call the method | |
returncode, output = self.analyzer._run_command(['test', 'command']) | |
# Verify the result | |
self.assertEqual(returncode, 0) | |
self.assertEqual(output, "Test output") | |
mock_run.assert_called_once() | |
if __name__ == "__main__": | |
unittest.main() |