| | |
| | |
| |
|
| | """ |
| | Unit tests for the Performance Analyzer service |
| | """ |
| |
|
| | import unittest |
| | from unittest.mock import patch, MagicMock, mock_open |
| | import os |
| | import sys |
| | import re |
| | from pathlib import Path |
| |
|
| | |
| | project_root = Path(__file__).resolve().parent.parent |
| | sys.path.insert(0, str(project_root)) |
| |
|
| | from src.services.performance_analyzer import PerformanceAnalyzer |
| |
|
| |
|
| | class TestPerformanceAnalyzer(unittest.TestCase): |
| | """Test cases for the PerformanceAnalyzer class""" |
| |
|
| | def setUp(self): |
| | """Set up test fixtures""" |
| | self.analyzer = PerformanceAnalyzer() |
| | self.test_repo_path = "/test/repo" |
| | |
| | def test_analyze_python_performance(self): |
| | """Test analyze_python_performance method""" |
| | |
| | python_code = """ |
| | def slow_function(): |
| | # This is a slow list comprehension with nested loops |
| | result = [x * y for x in range(1000) for y in range(1000)] |
| | |
| | # Inefficient string concatenation in a loop |
| | s = "" |
| | for i in range(1000): |
| | s += str(i) |
| | |
| | # Using a list where a set would be more efficient |
| | items = [1, 2, 3, 4, 5] |
| | if 3 in items: # O(n) operation |
| | print("Found") |
| | """ |
| | |
| | |
| | with patch.object(self.analyzer, '_find_files', return_value=['/test/repo/test.py']), \ |
| | patch('builtins.open', mock_open(read_data=python_code)): |
| | |
| | |
| | result = self.analyzer.analyze_python_performance(self.test_repo_path) |
| | |
| | |
| | self.assertGreater(len(result['issues']), 0) |
| | self.assertGreater(result['issue_count'], 0) |
| | |
| | |
| | nested_loop_issue = next((issue for issue in result['issues'] |
| | if 'nested loop' in issue['message'].lower()), None) |
| | string_concat_issue = next((issue for issue in result['issues'] |
| | if 'string concatenation' in issue['message'].lower()), None) |
| | list_vs_set_issue = next((issue for issue in result['issues'] |
| | if 'list' in issue['message'].lower() and 'set' in issue['message'].lower()), None) |
| | |
| | self.assertIsNotNone(nested_loop_issue) |
| | self.assertIsNotNone(string_concat_issue) |
| | self.assertIsNotNone(list_vs_set_issue) |
| | |
| | def test_analyze_javascript_performance(self): |
| | """Test analyze_javascript_performance method""" |
| | |
| | js_code = """ |
| | function slowFunction() { |
| | // Inefficient DOM manipulation in a loop |
| | for (let i = 0; i < 1000; i++) { |
| | document.getElementById('myElement').innerHTML += 'item ' + i; |
| | } |
| | |
| | // Memory leak with event listeners |
| | document.getElementById('button').addEventListener('click', function() { |
| | console.log('clicked'); |
| | }); |
| | |
| | // Blocking the main thread |
| | let start = Date.now(); |
| | while (Date.now() - start < 1000) { |
| | // Busy wait for 1 second |
| | } |
| | } |
| | """ |
| | |
| | |
| | with patch.object(self.analyzer, '_find_files', return_value=['/test/repo/test.js']), \ |
| | patch('builtins.open', mock_open(read_data=js_code)): |
| | |
| | |
| | result = self.analyzer.analyze_javascript_performance(self.test_repo_path) |
| | |
| | |
| | self.assertGreater(len(result['issues']), 0) |
| | self.assertGreater(result['issue_count'], 0) |
| | |
| | |
| | dom_issue = next((issue for issue in result['issues'] |
| | if 'dom' in issue['message'].lower()), None) |
| | memory_leak_issue = next((issue for issue in result['issues'] |
| | if 'memory leak' in issue['message'].lower() or 'event listener' in issue['message'].lower()), None) |
| | blocking_issue = next((issue for issue in result['issues'] |
| | if 'blocking' in issue['message'].lower() or 'main thread' in issue['message'].lower()), None) |
| | |
| | self.assertIsNotNone(dom_issue) |
| | self.assertIsNotNone(memory_leak_issue) |
| | self.assertIsNotNone(blocking_issue) |
| | |
| | def test_analyze_typescript_performance(self): |
| | """Test analyze_typescript_performance method""" |
| | |
| | ts_code = """ |
| | function slowFunction(): void { |
| | // Inefficient array operations |
| | const array: number[] = []; |
| | for (let i = 0; i < 1000; i++) { |
| | array.unshift(i); // O(n) operation |
| | } |
| | |
| | // Excessive type casting |
| | let value: any = "123"; |
| | let num: number = <number><any>value; |
| | |
| | // Inefficient async/await usage |
| | async function fetchData(): Promise<void> { |
| | const promises = []; |
| | for (let i = 0; i < 10; i++) { |
| | const result = await fetch(`https://api.example.com/data/${i}`); // Sequential fetches |
| | promises.push(result); |
| | } |
| | } |
| | } |
| | """ |
| | |
| | |
| | with patch.object(self.analyzer, '_find_files', return_value=['/test/repo/test.ts']), \ |
| | patch('builtins.open', mock_open(read_data=ts_code)): |
| | |
| | |
| | result = self.analyzer.analyze_typescript_performance(self.test_repo_path) |
| | |
| | |
| | self.assertGreater(len(result['issues']), 0) |
| | self.assertGreater(result['issue_count'], 0) |
| | |
| | |
| | array_issue = next((issue for issue in result['issues'] |
| | if 'array' in issue['message'].lower() and 'unshift' in issue['message'].lower()), None) |
| | type_casting_issue = next((issue for issue in result['issues'] |
| | if 'type casting' in issue['message'].lower()), None) |
| | async_issue = next((issue for issue in result['issues'] |
| | if 'async' in issue['message'].lower() or 'await' in issue['message'].lower()), None) |
| | |
| | self.assertIsNotNone(array_issue) |
| | self.assertIsNotNone(type_casting_issue) |
| | self.assertIsNotNone(async_issue) |
| | |
| | def test_analyze_java_performance(self): |
| | """Test analyze_java_performance method""" |
| | |
| | java_code = """ |
| | public class SlowClass { |
| | public void slowMethod() { |
| | // Inefficient string concatenation |
| | String result = ""; |
| | for (int i = 0; i < 1000; i++) { |
| | result += i; // Creates a new string each time |
| | } |
| | |
| | // Using ArrayList where HashSet would be more efficient for lookups |
| | ArrayList<Integer> list = new ArrayList<>(); |
| | for (int i = 0; i < 1000; i++) { |
| | list.add(i); |
| | } |
| | boolean contains = list.contains(500); // O(n) operation |
| | |
| | // Excessive object creation |
| | for (int i = 0; i < 1000; i++) { |
| | Integer obj = new Integer(i); // Creates 1000 objects |
| | } |
| | } |
| | } |
| | """ |
| | |
| | |
| | with patch.object(self.analyzer, '_find_files', return_value=['/test/repo/SlowClass.java']), \ |
| | patch('builtins.open', mock_open(read_data=java_code)): |
| | |
| | |
| | result = self.analyzer.analyze_java_performance(self.test_repo_path) |
| | |
| | |
| | self.assertGreater(len(result['issues']), 0) |
| | self.assertGreater(result['issue_count'], 0) |
| | |
| | |
| | string_concat_issue = next((issue for issue in result['issues'] |
| | if 'string concatenation' in issue['message'].lower()), None) |
| | collection_issue = next((issue for issue in result['issues'] |
| | if 'arraylist' in issue['message'].lower() and 'hashset' in issue['message'].lower()), None) |
| | object_creation_issue = next((issue for issue in result['issues'] |
| | if 'object creation' in issue['message'].lower()), None) |
| | |
| | self.assertIsNotNone(string_concat_issue) |
| | self.assertIsNotNone(collection_issue) |
| | self.assertIsNotNone(object_creation_issue) |
| | |
| | def test_analyze_go_performance(self): |
| | """Test analyze_go_performance method""" |
| | |
| | go_code = """ |
| | package main |
| | |
| | import ( |
| | "fmt" |
| | "sync" |
| | ) |
| | |
| | func slowFunction() { |
| | // Inefficient slice operations |
| | slice := []int{} |
| | for i := 0; i < 1000; i++ { |
| | slice = append(slice, i) // May cause reallocation |
| | } |
| | |
| | // Mutex instead of atomic operations |
| | var mu sync.Mutex |
| | counter := 0 |
| | for i := 0; i < 1000; i++ { |
| | mu.Lock() |
| | counter++ |
| | mu.Unlock() |
| | } |
| | |
| | // Inefficient string concatenation |
| | result := "" |
| | for i := 0; i < 1000; i++ { |
| | result += fmt.Sprintf("%d", i) // Creates a new string each time |
| | } |
| | } |
| | """ |
| | |
| | |
| | with patch.object(self.analyzer, '_find_files', return_value=['/test/repo/main.go']), \ |
| | patch('builtins.open', mock_open(read_data=go_code)): |
| | |
| | |
| | result = self.analyzer.analyze_go_performance(self.test_repo_path) |
| | |
| | |
| | self.assertGreater(len(result['issues']), 0) |
| | self.assertGreater(result['issue_count'], 0) |
| | |
| | |
| | slice_issue = next((issue for issue in result['issues'] |
| | if 'slice' in issue['message'].lower() and 'append' in issue['message'].lower()), None) |
| | mutex_issue = next((issue for issue in result['issues'] |
| | if 'mutex' in issue['message'].lower() or 'atomic' in issue['message'].lower()), None) |
| | string_concat_issue = next((issue for issue in result['issues'] |
| | if 'string concatenation' in issue['message'].lower()), None) |
| | |
| | self.assertIsNotNone(slice_issue) |
| | self.assertIsNotNone(mutex_issue) |
| | self.assertIsNotNone(string_concat_issue) |
| | |
| | def test_analyze_rust_performance(self): |
| | """Test analyze_rust_performance method""" |
| | |
| | rust_code = """ |
| | fn slow_function() { |
| | // Inefficient string operations |
| | let mut result = String::new(); |
| | for i in 0..1000 { |
| | result.push_str(&i.to_string()); // Allocates a new string each time |
| | } |
| | |
| | // Excessive cloning |
| | let data = vec![1, 2, 3, 4, 5]; |
| | let copied = data.clone(); // Clones the entire vector |
| | |
| | // Inefficient iteration |
| | let mut sum = 0; |
| | for i in 0..data.len() { |
| | sum += data[i]; // Bounds checking on each access |
| | } |
| | } |
| | """ |
| | |
| | |
| | with patch.object(self.analyzer, '_find_files', return_value=['/test/repo/main.rs']), \ |
| | patch('builtins.open', mock_open(read_data=rust_code)): |
| | |
| | |
| | result = self.analyzer.analyze_rust_performance(self.test_repo_path) |
| | |
| | |
| | self.assertGreater(len(result['issues']), 0) |
| | self.assertGreater(result['issue_count'], 0) |
| | |
| | |
| | string_issue = next((issue for issue in result['issues'] |
| | if 'string' in issue['message'].lower()), None) |
| | clone_issue = next((issue for issue in result['issues'] |
| | if 'clone' in issue['message'].lower()), None) |
| | iteration_issue = next((issue for issue in result['issues'] |
| | if 'iteration' in issue['message'].lower() or 'bounds checking' in issue['message'].lower()), None) |
| | |
| | self.assertIsNotNone(string_issue) |
| | self.assertIsNotNone(clone_issue) |
| | self.assertIsNotNone(iteration_issue) |
| | |
| | def test_analyze_repository(self): |
| | """Test analyze_repository method""" |
| | |
| | self.analyzer.analyze_python_performance = MagicMock(return_value={ |
| | 'issues': [ |
| | {'file': 'file1.py', 'line': 10, 'message': 'Inefficient list comprehension'}, |
| | {'file': 'file1.py', 'line': 20, 'message': 'Inefficient string concatenation'} |
| | ], |
| | 'issue_count': 2 |
| | }) |
| | self.analyzer.analyze_javascript_performance = MagicMock(return_value={ |
| | 'issues': [ |
| | {'file': 'file1.js', 'line': 15, 'message': 'DOM manipulation in loop'} |
| | ], |
| | 'issue_count': 1 |
| | }) |
| | |
| | |
| | result = self.analyzer.analyze_repository(self.test_repo_path, ['Python', 'JavaScript']) |
| | |
| | |
| | self.assertEqual(len(result['language_results']), 2) |
| | self.assertIn('Python', result['language_results']) |
| | self.assertIn('JavaScript', result['language_results']) |
| | self.assertEqual(result['language_results']['Python']['issue_count'], 2) |
| | self.assertEqual(result['language_results']['JavaScript']['issue_count'], 1) |
| | |
| | |
| | self.assertEqual(len(result['hotspots']), 1) |
| | self.assertEqual(result['hotspots'][0]['file'], 'file1.py') |
| | self.assertEqual(result['hotspots'][0]['issue_count'], 2) |
| | |
| | |
| | self.analyzer.analyze_python_performance.assert_called_once_with(self.test_repo_path) |
| | self.analyzer.analyze_javascript_performance.assert_called_once_with(self.test_repo_path) |
| | |
| | def test_identify_hotspots(self): |
| | """Test _identify_hotspots method""" |
| | |
| | language_results = { |
| | 'Python': { |
| | 'issues': [ |
| | {'file': 'file1.py', 'line': 10, 'message': 'Issue 1'}, |
| | {'file': 'file1.py', 'line': 20, 'message': 'Issue 2'}, |
| | {'file': 'file2.py', 'line': 5, 'message': 'Issue 3'} |
| | ], |
| | 'issue_count': 3 |
| | }, |
| | 'JavaScript': { |
| | 'issues': [ |
| | {'file': 'file1.js', 'line': 15, 'message': 'Issue 4'}, |
| | {'file': 'file3.js', 'line': 25, 'message': 'Issue 5'}, |
| | {'file': 'file3.js', 'line': 30, 'message': 'Issue 6'} |
| | ], |
| | 'issue_count': 3 |
| | } |
| | } |
| | |
| | |
| | hotspots = self.analyzer._identify_hotspots(language_results) |
| | |
| | |
| | self.assertEqual(len(hotspots), 2) |
| | |
| | |
| | file1_py_hotspot = next((h for h in hotspots if h['file'] == 'file1.py'), None) |
| | file3_js_hotspot = next((h for h in hotspots if h['file'] == 'file3.js'), None) |
| | |
| | self.assertIsNotNone(file1_py_hotspot) |
| | self.assertIsNotNone(file3_js_hotspot) |
| | self.assertEqual(file1_py_hotspot['issue_count'], 2) |
| | self.assertEqual(file3_js_hotspot['issue_count'], 2) |
| | |
| | @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) |
| | |
| | def test_analyze_file_with_patterns(self): |
| | """Test _analyze_file_with_patterns method""" |
| | |
| | file_content = """ |
| | def slow_function(): |
| | # This is a slow list comprehension |
| | result = [x * y for x in range(1000) for y in range(1000)] |
| | |
| | # Inefficient string concatenation |
| | s = "" |
| | for i in range(1000): |
| | s += str(i) |
| | """ |
| | |
| | patterns = [ |
| | (re.compile(r'\[.*for.*for.*\]', re.MULTILINE), "Nested list comprehension can be inefficient"), |
| | (re.compile(r'\s+s\s\+=\s', re.MULTILINE), "String concatenation in a loop is inefficient") |
| | ] |
| | |
| | |
| | issues = self.analyzer._analyze_file_with_patterns('/test/repo/test.py', file_content, patterns) |
| | |
| | |
| | self.assertEqual(len(issues), 2) |
| | self.assertEqual(issues[0]['file'], 'test.py') |
| | self.assertEqual(issues[1]['file'], 'test.py') |
| | self.assertIn('Nested list comprehension', issues[0]['message']) |
| | self.assertIn('String concatenation', issues[1]['message']) |
| |
|
| |
|
| | if __name__ == "__main__": |
| | unittest.main() |