CodeReviewAgent / tests /test_code_analyzer.py
c1r3x's picture
Review Agent: first commit
88d205f
raw
history blame
15.4 kB
#!/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"
@patch('os.path.exists')
@patch('subprocess.run')
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')
@patch('os.path.exists')
@patch('subprocess.run')
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.')
@patch('os.path.exists')
@patch('subprocess.run')
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'.")
@patch('os.path.exists')
@patch('subprocess.run')
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'.")
@patch('os.path.exists')
@patch('subprocess.run')
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')
@patch('os.path.exists')
@patch('subprocess.run')
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)
@patch('os.walk')
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)
@patch('os.path.exists')
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)
@patch('subprocess.run')
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()