-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathenhanced_test_state.py
More file actions
562 lines (457 loc) · 20.1 KB
/
Copy pathenhanced_test_state.py
File metadata and controls
562 lines (457 loc) · 20.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
#!/usr/bin/env python3
"""
Enhanced Test State
This module extends the base TestState with enhanced capabilities for more
detailed analysis and tracking of test execution results.
"""
import os
import re
import logging
import traceback
from collections import defaultdict
# Import from base implementation
from enhanced_mcts_test_generator import TestState
logger = logging.getLogger("enhanced_test_state")
class EnhancedTestState(TestState):
"""
Enhanced version of TestState with more detailed analysis and tracking
"""
def __init__(self, test_code, class_name, package_name, project_dir, source_code=None, project_type='maven'):
"""
Initialize enhanced test state
Parameters:
test_code (str): Test code
class_name (str): Class name
package_name (str): Package name
project_dir (str): Project directory
source_code (str): Source code (optional)
project_type (str): Project type ('maven' or 'gradle')
"""
# Call parent constructor
super().__init__(test_code, class_name, package_name, project_dir, source_code, project_type)
# Enhanced tracking properties
self.method_coverage = {} # Coverage per test method
self.branch_coverage = 0.0 # Branch coverage percentage
self.line_coverage = 0.0 # Line coverage percentage
self.method_execution_time = {} # Execution time per test method
# Additional bug information
self.bug_categories = defaultdict(int) # Count bugs by category
self.bug_severity_counts = defaultdict(int) # Count bugs by severity
# Track verified bugs
self.verified_bugs = []
# Track test quality metrics
self.avg_assertions_per_method = 0.0
self.test_method_complexity = {}
self.test_diversity_score = 0.0
# Track assertions
self.assertions = []
# Track mutation score
self.mutation_score = 0.0
# Expanded bug categories for richer tracking
self.bug_types = {
# Logical bugs
"logical": {
"count": 0,
"subtypes": defaultdict(int),
"verified_count": 0
},
# Resource management bugs
"resource_management": {
"count": 0,
"subtypes": defaultdict(int),
"verified_count": 0
},
# Data operation bugs
"data_operation": {
"count": 0,
"subtypes": defaultdict(int),
"verified_count": 0
},
# Exception handling bugs
"exception_handling": {
"count": 0,
"subtypes": defaultdict(int),
"verified_count": 0
},
# Concurrency bugs
"concurrency": {
"count": 0,
"subtypes": defaultdict(int),
"verified_count": 0
},
# Input validation bugs
"validation": {
"count": 0,
"subtypes": defaultdict(int),
"verified_count": 0
},
# Security vulnerabilities
"security": {
"count": 0,
"subtypes": defaultdict(int),
"verified_count": 0
}
}
# Extract assertions from test methods
self._extract_assertions()
def _extract_assertions(self):
"""Extract assertions from test methods"""
assertion_pattern = r'assert\w+\s*\([^;]+\);'
self.assertions = []
total_assertions = 0
for method in self.test_methods:
if isinstance(method, dict) and "code" in method:
# Find all assertions in this method
method_assertions = re.findall(assertion_pattern, method["code"])
# Add to total count
total_assertions += len(method_assertions)
# Add to assertions list
for assertion in method_assertions:
self.assertions.append({
"method": method.get("name", "unknown"),
"assertion": assertion.strip()
})
# Calculate average assertions per method
if self.test_methods:
self.avg_assertions_per_method = total_assertions / len(self.test_methods)
def calculate_test_diversity_score(self):
"""
Calculate a score representing test diversity
Returns:
float: Diversity score (0.0 to 1.0)
"""
if not self.test_methods:
return 0.0
# Factors that contribute to diversity
assertion_types = set()
input_diversity = 0.0
method_coverage_diversity = 0.0
# Count unique assertion types
for assertion in self.assertions:
assert_type = re.match(r'assert(\w+)', assertion["assertion"])
if assert_type:
assertion_types.add(assert_type.group(1))
# Check input diversity through string literals and numbers
string_literals = set()
number_literals = set()
for method in self.test_methods:
if isinstance(method, dict) and "code" in method:
# Extract string literals
strings = re.findall(r'"([^"]*)"', method["code"])
string_literals.update(strings)
# Extract number literals
numbers = re.findall(r'\b(\d+(?:\.\d+)?)\b', method["code"])
number_literals.update(numbers)
# Calculate diversity factors
assertion_diversity = min(1.0, len(assertion_types) / 5.0) # Normalize to 5 types
input_diversity = min(1.0, (len(string_literals) + len(number_literals)) / 20.0) # Normalize to 20 literals
method_coverage_factor = min(1.0, len(self.method_coverage) / 10.0) # Normalize to 10 methods
# Combined diversity score
diversity_score = (
0.4 * assertion_diversity +
0.4 * input_diversity +
0.2 * method_coverage_factor
)
self.test_diversity_score = diversity_score
return diversity_score
def evaluate(self, validator=None, verify_bugs=False, current_iteration=None):
"""
Run tests and measure coverage with enhanced analytics
Parameters:
validator (TestValidator): Optional validator for fixing test code
verify_bugs (bool): Whether to immediately verify bugs
current_iteration (int): Current MCTS iteration number
"""
# Call parent evaluate method first
super().evaluate(validator, verify_bugs, current_iteration)
if self.executed:
try:
# Calculate additional metrics
self.calculate_test_diversity_score()
# Analyze bug categories with expanded classification
self._categorize_bugs_enhanced()
# Verify bugs if requested
if verify_bugs and self.detected_bugs:
self._verify_bugs()
# Calculate method complexity
self._calculate_method_complexity()
logger.debug(f"Enhanced evaluation: diversity={self.test_diversity_score:.2f}, " +
f"avg_assertions={self.avg_assertions_per_method:.2f}")
except Exception as e:
logger.error(f"Error in enhanced evaluation: {str(e)}")
logger.error(traceback.format_exc())
def _categorize_bugs_enhanced(self):
"""Categorize detected bugs with expanded classification"""
# Reset counters
self.bug_categories = defaultdict(int)
self.bug_severity_counts = defaultdict(int)
# Reset bug type tracking
for category in self.bug_types:
self.bug_types[category]["count"] = 0
self.bug_types[category]["subtypes"] = defaultdict(int)
self.bug_types[category]["verified_count"] = 0
for bug in self.detected_bugs:
# Basic categorization
bug_type = bug.get("type", "unknown")
self.bug_categories[bug_type] += 1
# Categorize by severity
severity = bug.get("severity", "medium")
self.bug_severity_counts[severity] += 1
# Enhanced categorization
bug_category = bug.get("bug_category", self._infer_bug_category(bug))
bug["bug_category"] = bug_category # Ensure category is stored in bug info
# Track in bug types structure
if bug_category in self.bug_types:
self.bug_types[bug_category]["count"] += 1
# Track subtype if available
subtype = bug.get("subtype", bug.get("bug_type", bug_type))
self.bug_types[bug_category]["subtypes"][subtype] += 1
# Track if verified
if bug.get("verified", False) and bug.get("is_real_bug", False):
self.bug_types[bug_category]["verified_count"] += 1
def _infer_bug_category(self, bug):
"""
Infer bug category from error type and other info
Parameters:
bug (dict): Bug information
Returns:
str: Inferred bug category
"""
error = bug.get("error", "")
description = bug.get("description", "")
# Check for resource management issues
if ("ClosedChannelException" in error or
"IOException" in error or
"FileNotFoundException" in error or
"leak" in description.lower() or
"close" in description.lower()):
return "resource_management"
# Check for data operation issues
if ("ClassCastException" in error or
"NumberFormatException" in error or
"ArithmeticException" in error or
"ArrayIndexOutOfBoundsException" in error or
"conversion" in description.lower() or
"overflow" in description.lower() or
"underflow" in description.lower()):
return "data_operation"
# Check for exception handling issues
if ("RuntimeException" in error or
"try-catch" in description.lower() or
"exception handling" in description.lower() or
"swallowed" in description.lower() or
"empty catch" in description.lower()):
return "exception_handling"
# Check for null pointer and validation issues
if ("NullPointerException" in error or
"IllegalArgumentException" in error or
"validation" in description.lower() or
"null check" in description.lower() or
"empty check" in description.lower()):
return "validation"
# Check for concurrency issues
if ("InterruptedException" in error or
"ConcurrentModificationException" in error or
"concurrent" in description.lower() or
"race condition" in description.lower() or
"deadlock" in description.lower() or
"thread" in description.lower()):
return "concurrency"
# Check for security issues
if ("security" in description.lower() or
"SQL" in description or
"injection" in description.lower() or
"credential" in description.lower() or
"vulnerability" in description.lower()):
return "security"
# Default to logical for other bugs
return "logical"
def _verify_bugs(self):
"""
Verify detected bugs and update metrics
This is called when verify_bugs is True in evaluate()
"""
self.verified_bugs = []
for bug in self.detected_bugs:
# Check if the bug has been verified
if bug.get("verified", False):
# If it's a real bug (verified positive), add to verified bugs list
if bug.get("is_real_bug", False):
self.verified_bugs.append(bug)
def _analyze_bug_likelihood(self, bug):
"""
Analyze the likelihood that a detected bug is real
Parameters:
bug (dict): Bug information
Returns:
float: Likelihood (0.0 to 1.0) that the bug is real
"""
# Start with moderate likelihood
likelihood = 0.5
# Check bug properties
if bug.get("verified", False):
# Already verified, use that result
return 1.0 if bug.get("is_real_bug", False) else 0.0
# Check assertion patterns
if re.search(r'assert\w+', bug.get("description", "")):
likelihood += 0.1
# Check error type
error_type = bug.get("error", "")
if error_type in ["NullPointerException", "IndexOutOfBoundsException", "ClassCastException"]:
likelihood += 0.1
# Check test method code if available
method_name = bug.get("test_method", "")
for method in self.test_methods:
if method.get("name") == method_name:
method_code = method.get("code", "")
# Check for thorough test method
if len(re.findall(r'assert\w+', method_code)) > 2:
likelihood += 0.1
if "try" in method_code and "catch" in method_code:
likelihood += 0.05
break
return min(likelihood, 1.0)
def _calculate_method_complexity(self):
"""Calculate complexity metrics for test methods"""
for method in self.test_methods:
if isinstance(method, dict) and "code" in method:
code = method.get("code", "")
name = method.get("name", "unknown")
# Simple complexity metrics
lines = len(code.split("\n"))
assertions = len(re.findall(r'assert\w+', code))
branches = len(re.findall(r'if|else|for|while|switch|case', code))
# Calculate complexity score
complexity = 1 + (branches * 0.2) + (assertions * 0.1)
# Store in test method complexity
self.test_method_complexity[name] = {
"lines": lines,
"assertions": assertions,
"branches": branches,
"complexity_score": complexity
}
def calculate_method_coverage(self, coverage_data):
"""
Calculate coverage per method
Parameters:
coverage_data (dict): Coverage data from JaCoCo
"""
if not coverage_data:
return
# Extract method coverage if available
method_coverage = coverage_data.get("methods", {})
for method_name, coverage in method_coverage.items():
self.method_coverage[method_name] = coverage
def get_best_test_methods(self, max_methods=5):
"""
Get the test methods with the highest quality score
Parameters:
max_methods (int): Maximum number of methods to return
Returns:
list: List of best test methods
"""
if not self.test_methods:
return []
# Score each method based on:
# 1. Assertion count
# 2. Bug finding ability
# 3. Coverage
# 4. Complexity
method_scores = {}
for method in self.test_methods:
if not isinstance(method, dict) or "name" not in method:
continue
name = method["name"]
code = method.get("code", "")
# Basic score
score = 1.0
# Assertions score
assertions = len(re.findall(r'assert\w+', code))
score += assertions * 0.5
# Bug finding score - check if this method found a bug
found_bug = False
for bug in self.detected_bugs:
if bug.get("test_method") == name:
found_bug = True
# Extra points for verified bugs
if bug.get("verified", False) and bug.get("is_real_bug", False):
score += 3.0
else:
score += 1.0
# Coverage score
if name in self.method_coverage:
score += self.method_coverage[name] * 0.01 # 1 point per 100% coverage
# Complexity score
if name in self.test_method_complexity:
complexity = self.test_method_complexity[name]["complexity_score"]
# Reward moderate complexity, penalize extremely simple or complex
if 1.0 <= complexity <= 3.0:
score += complexity * 0.3
elif complexity > 3.0:
score += 0.9 - (complexity - 3.0) * 0.1 # Diminishing returns
method_scores[name] = score
# Sort methods by score
sorted_methods = sorted(
[(name, score) for name, score in method_scores.items()],
key=lambda x: x[1],
reverse=True
)
# Return top methods
return [name for name, _ in sorted_methods[:max_methods]]
def get_bug_distribution(self):
"""
Get the distribution of bugs by category
Returns:
dict: Bug distribution by category
"""
total_bugs = sum(self.bug_types[category]["count"] for category in self.bug_types)
if total_bugs == 0:
return {category: 0.0 for category in self.bug_types}
distribution = {}
for category in self.bug_types:
count = self.bug_types[category]["count"]
distribution[category] = count / total_bugs
return distribution
def get_bug_summary(self):
"""
Get a summary of bugs found
Returns:
dict: Bug summary
"""
return {
"total_bugs": len(self.detected_bugs),
"verified_bugs": len(self.verified_bugs),
"categories": {
category: {
"count": self.bug_types[category]["count"],
"verified_count": self.bug_types[category]["verified_count"],
"subtypes": dict(self.bug_types[category]["subtypes"])
}
for category in self.bug_types
},
"severity": dict(self.bug_severity_counts)
}
def get_bug_finding_methods_by_category(self, category=None):
"""
Get test methods that found bugs in a specific category
Parameters:
category (str): Bug category to filter by
Returns:
list: Test methods that found bugs in the specified category
"""
methods = []
for bug in self.detected_bugs:
# Skip if not in specified category
if category and bug.get("bug_category") != category:
continue
# Skip if not verified
if not bug.get("verified", False) or not bug.get("is_real_bug", False):
continue
method_name = bug.get("test_method")
if method_name:
# Find the method in test_methods
for method in self.test_methods:
if method.get("name") == method_name:
methods.append(method)
break
return methods