#!/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 = """ Avoid unused local variables such as 'unusedVar'. """ 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()