testgenie-py 0.3.6__py3-none-any.whl → 0.3.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- testgen/analyzer/ast_analyzer.py +2 -11
- testgen/analyzer/fuzz_analyzer.py +1 -6
- testgen/analyzer/random_feedback_analyzer.py +20 -293
- testgen/analyzer/reinforcement_analyzer.py +59 -57
- testgen/analyzer/test_case_analyzer_context.py +0 -6
- testgen/controller/cli_controller.py +35 -29
- testgen/controller/docker_controller.py +3 -2
- testgen/db/dao.py +68 -0
- testgen/db/dao_impl.py +226 -0
- testgen/{sqlite → db}/db.py +15 -6
- testgen/generator/pytest_generator.py +2 -10
- testgen/generator/unit_test_generator.py +2 -11
- testgen/main.py +1 -3
- testgen/models/coverage_data.py +56 -0
- testgen/models/db_test_case.py +65 -0
- testgen/models/function.py +56 -0
- testgen/models/function_metadata.py +11 -1
- testgen/models/generator_context.py +32 -2
- testgen/models/source_file.py +29 -0
- testgen/models/test_result.py +38 -0
- testgen/models/test_suite.py +20 -0
- testgen/reinforcement/agent.py +1 -27
- testgen/reinforcement/environment.py +11 -93
- testgen/reinforcement/statement_coverage_state.py +5 -4
- testgen/service/analysis_service.py +31 -22
- testgen/service/cfg_service.py +3 -1
- testgen/service/coverage_service.py +115 -0
- testgen/service/db_service.py +140 -0
- testgen/service/generator_service.py +77 -20
- testgen/service/logging_service.py +2 -2
- testgen/service/service.py +62 -231
- testgen/service/test_executor_service.py +145 -0
- testgen/util/coverage_utils.py +38 -116
- testgen/util/coverage_visualizer.py +10 -9
- testgen/util/file_utils.py +10 -111
- testgen/util/randomizer.py +0 -26
- testgen/util/utils.py +197 -38
- {testgenie_py-0.3.6.dist-info → testgenie_py-0.3.8.dist-info}/METADATA +1 -1
- testgenie_py-0.3.8.dist-info/RECORD +72 -0
- testgen/inspector/inspector.py +0 -59
- testgen/presentation/__init__.py +0 -0
- testgen/presentation/cli_view.py +0 -12
- testgen/sqlite/__init__.py +0 -0
- testgen/sqlite/db_service.py +0 -239
- testgen/testgen.db +0 -0
- testgenie_py-0.3.6.dist-info/RECORD +0 -67
- /testgen/{inspector → db}/__init__.py +0 -0
- {testgenie_py-0.3.6.dist-info → testgenie_py-0.3.8.dist-info}/WHEEL +0 -0
- {testgenie_py-0.3.6.dist-info → testgenie_py-0.3.8.dist-info}/entry_points.txt +0 -0
testgen/util/coverage_utils.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
import ast
|
2
|
-
from typing import List
|
2
|
+
from typing import List, Any
|
3
3
|
|
4
4
|
import coverage
|
5
5
|
|
@@ -11,144 +11,64 @@ from testgen.util.utils import get_function_boundaries
|
|
11
11
|
from testgen.util.z3_utils.constraint_extractor import extract_branch_conditions
|
12
12
|
|
13
13
|
|
14
|
-
def
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
func(*args)
|
19
|
-
|
20
|
-
cov.stop()
|
21
|
-
cov.save()
|
22
|
-
|
23
|
-
analysis = cov.analysis2(file_name)
|
24
|
-
|
25
|
-
branches = analysis.arcs()
|
26
|
-
return branches
|
27
|
-
|
28
|
-
|
29
|
-
def get_coverage_analysis(file_name, class_name: str | None, func_name, args) -> tuple:
|
30
|
-
tree = load_and_parse_file_for_tree(file_name)
|
31
|
-
func_node = None
|
32
|
-
func_start = None
|
33
|
-
func_end = None
|
34
|
-
|
35
|
-
# Process tree body
|
36
|
-
for i, node in enumerate(tree.body):
|
37
|
-
# Handle class methods
|
38
|
-
if isinstance(node, ast.ClassDef) and class_name is not None:
|
39
|
-
# Search within class body with its own index
|
40
|
-
for j, class_node in enumerate(node.body):
|
41
|
-
if isinstance(class_node, ast.FunctionDef) and class_node.name == func_name:
|
42
|
-
func_node = class_node
|
43
|
-
func_start = class_node.lineno
|
44
|
-
|
45
|
-
# Now correctly check if this is the last method in the class
|
46
|
-
if j == len(node.body) - 1:
|
47
|
-
# Last method in class - find maximum line in method
|
48
|
-
max_lines = [line.lineno for line in ast.walk(class_node)
|
49
|
-
if hasattr(line, 'lineno') and line.lineno]
|
50
|
-
func_end = max(max_lines) if max_lines else func_start
|
51
|
-
else:
|
52
|
-
# Not last method - use next method's line number minus 1
|
53
|
-
next_node = node.body[j + 1] # Correct index now
|
54
|
-
if hasattr(next_node, 'lineno'):
|
55
|
-
func_end = next_node.lineno - 1
|
56
|
-
else:
|
57
|
-
# Fallback using max line in method
|
58
|
-
max_lines = [line.lineno for line in ast.walk(class_node)
|
59
|
-
if hasattr(line, 'lineno') and line.lineno]
|
60
|
-
func_end = max(max_lines) if max_lines else func_start
|
61
|
-
break
|
62
|
-
if isinstance(node, ast.FunctionDef) and node.name == func_name:
|
63
|
-
func_node = node
|
64
|
-
func_start = node.lineno
|
65
|
-
|
66
|
-
if i == len(tree.body) - 1:
|
67
|
-
max_lines = [line.lineno for line in ast.walk(node) if hasattr(line, 'lineno') and line.lineno]
|
68
|
-
func_end = max(max_lines) if max_lines else func_start
|
69
|
-
else:
|
70
|
-
next_node = tree.body[i + 1]
|
71
|
-
if hasattr(next_node, 'lineno'):
|
72
|
-
func_end = next_node.lineno - 1
|
73
|
-
else:
|
74
|
-
max_lines = [line.lineno for line in ast.walk(node)
|
75
|
-
if hasattr(line, 'lineno') and line.lineno]
|
76
|
-
func_end = max(max_lines) if max_lines else func_start
|
77
|
-
break
|
78
|
-
|
79
|
-
if not func_node:
|
80
|
-
raise ValueError(f"Function {func_name} not found in {file_name}")
|
81
|
-
|
82
|
-
# Enable branch coverage
|
83
|
-
cov = coverage.Coverage(branch=True)
|
84
|
-
cov.start()
|
85
|
-
module = load_module(file_name)
|
86
|
-
|
87
|
-
if class_name is not None:
|
88
|
-
class_obj = getattr(module, class_name)
|
89
|
-
instance = class_obj()
|
90
|
-
func = getattr(instance, func_name)
|
91
|
-
else:
|
92
|
-
func = getattr(module, func_name)
|
14
|
+
def get_coverage_analysis(filepath: str, function_metadata: FunctionMetadata, args) -> tuple:
|
15
|
+
func_node = function_metadata.func_def
|
16
|
+
func_start = func_node.lineno
|
17
|
+
func_end = max([node.lineno for node in ast.walk(func_node) if hasattr(node, 'lineno')], default=func_start)
|
93
18
|
|
94
|
-
func
|
19
|
+
func = function_metadata.func
|
95
20
|
|
96
|
-
|
97
|
-
cov.save()
|
21
|
+
analysis_list = _get_coverage_analysis_list(func, filepath, args)
|
98
22
|
|
99
|
-
analysis = cov.analysis2(file_name)
|
100
|
-
analysis_list = list(analysis)
|
101
|
-
# Filter executable and missed lines to function range
|
102
23
|
analysis_list[1] = [line for line in analysis_list[1] if func_start <= line <= func_end]
|
103
24
|
analysis_list[3] = [line for line in analysis_list[3] if func_start <= line <= func_end]
|
104
25
|
|
105
|
-
|
106
|
-
branch_lines = []
|
107
|
-
for node in ast.walk(func_node):
|
108
|
-
if isinstance(node, ast.If):
|
109
|
-
# Add the 'if' line
|
110
|
-
branch_lines.append(node.lineno)
|
111
|
-
|
112
|
-
# Find 'else' lines by analyzing orelse block
|
113
|
-
if node.orelse:
|
114
|
-
for else_item in node.orelse:
|
115
|
-
if hasattr(else_item, 'lineno'):
|
116
|
-
# Add line before the first statement in else block
|
117
|
-
else_line = else_item.lineno - 1
|
118
|
-
branch_lines.append(else_line)
|
119
|
-
break
|
26
|
+
branch_lines = _get_branch_lines(func_node)
|
120
27
|
|
121
|
-
# Add branch lines to executable statements if not already present
|
122
28
|
for line in branch_lines:
|
123
29
|
if func_start <= line <= func_end and line not in analysis_list[1]:
|
124
30
|
analysis_list[1].append(line)
|
125
31
|
analysis_list[1].sort()
|
126
32
|
|
127
|
-
# Make sure func_start is in executable and not in missed
|
128
33
|
if func_start not in analysis_list[1]:
|
129
34
|
analysis_list[1].append(func_start)
|
130
35
|
analysis_list[1].sort()
|
36
|
+
|
131
37
|
if func_start in analysis_list[3]:
|
132
38
|
analysis_list[3].remove(func_start)
|
133
39
|
|
134
40
|
return tuple(analysis_list)
|
135
41
|
|
136
42
|
|
137
|
-
def
|
138
|
-
|
139
|
-
|
140
|
-
covered_statements = total_statements - missed_statements
|
141
|
-
return (covered_statements / total_statements) * 100 if total_statements > 0 else 0
|
43
|
+
def _get_coverage_analysis_list(func: Any, filepath: str, args) -> list:
|
44
|
+
cov = coverage.Coverage(branch=True)
|
45
|
+
cov.start()
|
142
46
|
|
47
|
+
func(*args)
|
143
48
|
|
144
|
-
|
145
|
-
|
49
|
+
cov.stop()
|
50
|
+
cov.save()
|
146
51
|
|
52
|
+
analysis = cov.analysis2(filepath)
|
53
|
+
analysis_list = list(analysis)
|
54
|
+
return analysis_list
|
55
|
+
|
56
|
+
def _get_branch_lines(func_node: ast.FunctionDef) -> list:
|
57
|
+
branch_lines = []
|
58
|
+
for node in ast.walk(func_node):
|
59
|
+
if isinstance(node, ast.If):
|
60
|
+
branch_lines.append(node.lineno)
|
61
|
+
if node.orelse:
|
62
|
+
for else_item in node.orelse:
|
63
|
+
if hasattr(else_item, 'lineno'):
|
64
|
+
else_line = else_item.lineno - 1
|
65
|
+
branch_lines.append(else_line)
|
66
|
+
break
|
67
|
+
return branch_lines
|
147
68
|
|
148
69
|
def get_list_of_covered_statements(analysis: tuple) -> list:
|
149
70
|
return [x for x in analysis[1] if x not in analysis[3]]
|
150
71
|
|
151
|
-
|
152
72
|
def get_uncovered_lines_for_func(file_name: str, class_name: str | None, func_node: ast.FunctionDef, test_cases: List[TestCase]) -> List[int]:
|
153
73
|
# Get normal uncovered lines
|
154
74
|
func_name = func_node.name
|
@@ -196,17 +116,19 @@ def get_uncovered_lines_for_func(file_name: str, class_name: str | None, func_no
|
|
196
116
|
|
197
117
|
return uncovered_branch_lines
|
198
118
|
|
199
|
-
def get_all_executable_statements(
|
119
|
+
def get_all_executable_statements(filepath: str, func: FunctionMetadata, test_cases):
|
200
120
|
import ast
|
121
|
+
from testgen.util.randomizer import new_random_test_case
|
201
122
|
|
202
123
|
if not test_cases:
|
203
|
-
|
124
|
+
temp_case = new_random_test_case(filepath, func.class_name, func.func_def)
|
125
|
+
analysis = get_coverage_analysis(filepath, func, temp_case.inputs)
|
204
126
|
else:
|
205
|
-
analysis = get_coverage_analysis(
|
127
|
+
analysis = get_coverage_analysis(filepath, func, test_cases[0].inputs)
|
206
128
|
|
207
129
|
executable_lines = list(analysis[1])
|
208
130
|
|
209
|
-
with open(
|
131
|
+
with open(filepath, 'r') as f:
|
210
132
|
source = f.read()
|
211
133
|
|
212
134
|
tree = ast.parse(source)
|
@@ -219,7 +141,7 @@ def get_all_executable_statements(analysis_context: AnalysisContext, func: Funct
|
|
219
141
|
continue
|
220
142
|
else_line = if_node.orelse[0].lineno - 1
|
221
143
|
|
222
|
-
with open(
|
144
|
+
with open(filepath, 'r') as f:
|
223
145
|
lines = f.readlines()
|
224
146
|
if else_line <= len(lines):
|
225
147
|
line_content = lines[else_line - 1].strip()
|
@@ -3,6 +3,7 @@ import os
|
|
3
3
|
from typing import Dict, List, Set
|
4
4
|
import coverage
|
5
5
|
|
6
|
+
from testgen.models.function_metadata import FunctionMetadata
|
6
7
|
from testgen.service.logging_service import get_logger
|
7
8
|
import testgen.util.coverage_utils
|
8
9
|
from testgen.models.test_case import TestCase
|
@@ -18,20 +19,20 @@ class CoverageVisualizer:
|
|
18
19
|
def set_service(self, service):
|
19
20
|
self.service = service
|
20
21
|
|
21
|
-
def get_covered_lines(self, file_path: str,
|
22
|
-
if
|
23
|
-
self.covered_lines[
|
22
|
+
def get_covered_lines(self, file_path: str, function_data: FunctionMetadata, test_cases: List[TestCase]):
|
23
|
+
if function_data.function_name not in self.covered_lines:
|
24
|
+
self.covered_lines[function_data.function_name] = set()
|
24
25
|
|
25
|
-
for test_case in [tc for tc in test_cases if tc.func_name ==
|
26
|
-
analysis = testgen.util.coverage_utils.get_coverage_analysis(file_path,
|
26
|
+
for test_case in [tc for tc in test_cases if tc.func_name == function_data.function_name]:
|
27
|
+
analysis = testgen.util.coverage_utils.get_coverage_analysis(file_path, function_data, test_case.inputs)
|
27
28
|
covered = testgen.util.coverage_utils.get_list_of_covered_statements(analysis)
|
28
29
|
if covered:
|
29
|
-
self.covered_lines[
|
30
|
+
self.covered_lines[function_data.function_name].update(covered)
|
30
31
|
|
31
|
-
if
|
32
|
-
self.logger.debug(f"Covered lines for {
|
32
|
+
if function_data.function_name in self.covered_lines:
|
33
|
+
self.logger.debug(f"Covered lines for {function_data.function_name}: {self.covered_lines[function_data.function_name]}")
|
33
34
|
else:
|
34
|
-
self.logger.debug(f"No coverage data found for {
|
35
|
+
self.logger.debug(f"No coverage data found for {function_data.function_name}")
|
35
36
|
|
36
37
|
def generate_colored_cfg(self, function_name, output_path):
|
37
38
|
"""Generate colored CFG for a function showing test coverage"""
|
testgen/util/file_utils.py
CHANGED
@@ -1,63 +1,8 @@
|
|
1
1
|
import ast
|
2
2
|
import importlib.util
|
3
3
|
import os
|
4
|
-
import sys
|
5
4
|
from _ast import Module
|
6
|
-
from importlib import util
|
7
5
|
from types import ModuleType
|
8
|
-
from typing import Dict
|
9
|
-
|
10
|
-
def get_import_info(filepath: str) -> Dict[str, str]:
|
11
|
-
if not os.path.exists(filepath) or not filepath.endswith('.py'):
|
12
|
-
raise ValueError(f"Invalid Python file: {filepath}")
|
13
|
-
|
14
|
-
# Get the directory and filename
|
15
|
-
file_dir = os.path.dirname(os.path.abspath(filepath))
|
16
|
-
module_name = os.path.splitext(os.path.basename(filepath))[0]
|
17
|
-
|
18
|
-
# Check if this is part of a package (has __init__.py)
|
19
|
-
is_package = os.path.exists(os.path.join(file_dir, '__init__.py'))
|
20
|
-
|
21
|
-
# Find the project root by looking for setup.py or a .git directory
|
22
|
-
project_root = find_project_root(file_dir)
|
23
|
-
|
24
|
-
# Build the import path based on the file's location relative to the project root
|
25
|
-
if project_root:
|
26
|
-
rel_path = os.path.relpath(file_dir, project_root)
|
27
|
-
if rel_path == '.':
|
28
|
-
# File is directly in the project root
|
29
|
-
import_path = module_name
|
30
|
-
package_name = ''
|
31
|
-
else:
|
32
|
-
# File is in a subdirectory
|
33
|
-
path_parts = rel_path.replace('\\', '/').split('/')
|
34
|
-
# Filter out any empty parts
|
35
|
-
path_parts = [part for part in path_parts if part]
|
36
|
-
|
37
|
-
if path_parts:
|
38
|
-
package_name = path_parts[0]
|
39
|
-
# Construct the full import path
|
40
|
-
import_path = '.'.join(path_parts) + '.' + module_name
|
41
|
-
else:
|
42
|
-
package_name = ''
|
43
|
-
import_path = module_name
|
44
|
-
else:
|
45
|
-
# Fallback if we can't find a project root
|
46
|
-
package_name = ''
|
47
|
-
import_path = module_name
|
48
|
-
|
49
|
-
info = {
|
50
|
-
'module_name': module_name,
|
51
|
-
'package_name': package_name,
|
52
|
-
'import_path': import_path,
|
53
|
-
'is_package': is_package,
|
54
|
-
'project_root': project_root,
|
55
|
-
'file_dir': file_dir
|
56
|
-
}
|
57
|
-
|
58
|
-
#print(f"INFO: {info}")
|
59
|
-
|
60
|
-
return info
|
61
6
|
|
62
7
|
def find_project_root(start_dir: str) -> str | None:
|
63
8
|
current_dir = start_dir
|
@@ -123,64 +68,18 @@ def adjust_file_path_for_docker(file_path: str) -> str:
|
|
123
68
|
print(f"Docker - adjusted to: {adjusted_path}")
|
124
69
|
return adjusted_path
|
125
70
|
|
126
|
-
|
127
|
-
"""def adjust_file_path_for_docker(file_path) -> str:
|
128
|
-
#Adjust file path for Docker environment to handle subdirectories and relative paths.
|
129
|
-
print(f"Docker - adjusting path: {file_path}")
|
130
|
-
|
131
|
-
# Try direct path first (maybe it's correct already)
|
132
|
-
if os.path.isfile(file_path):
|
133
|
-
print(f"Docker - found file at direct path: {file_path}")
|
134
|
-
return file_path
|
135
|
-
|
136
|
-
# Try relative to /controller (root of mount)
|
137
|
-
controller_path = os.path.join('/controller', file_path)
|
138
|
-
if os.path.isfile(controller_path):
|
139
|
-
print(f"Docker - found file at: {controller_path}")
|
140
|
-
return controller_path
|
141
|
-
|
142
|
-
# Try /controller/testgen/code_to_test path - this is where the file actually is
|
143
|
-
testgen_path = os.path.join('/controller/testgen', file_path)
|
144
|
-
if os.path.isfile(testgen_path):
|
145
|
-
print(f"Docker - found file at: {testgen_path}")
|
146
|
-
return testgen_path
|
147
|
-
|
148
|
-
# If it's just a filename, search in common locations
|
149
|
-
if os.path.basename(file_path) == file_path:
|
150
|
-
for search_dir in ['/controller', '/controller/code_to_test', '/controller/testgen/code_to_test', '/controller/testgen']:
|
151
|
-
test_path = os.path.join(search_dir, file_path)
|
152
|
-
if os.path.isfile(test_path):
|
153
|
-
print(f"Docker - found file at: {test_path}")
|
154
|
-
return test_path
|
155
|
-
|
156
|
-
# Debug output to help diagnose issues
|
157
|
-
print("Docker - available files in /controller:")
|
158
|
-
os.system("find /controller -name '*.py' | grep boolean")
|
159
|
-
|
160
|
-
# Return original path if we couldn't find a better match
|
161
|
-
print(f"Docker - couldn't find file, returning original: {file_path}")
|
162
|
-
return file_path"""
|
163
|
-
|
164
|
-
|
165
|
-
|
166
71
|
def get_project_root_in_docker(script_path) -> str:
|
167
|
-
#
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
72
|
+
# Try to find project root by looking for pyproject.toml or similar
|
73
|
+
current_dir = os.path.dirname(os.path.abspath(script_path))
|
74
|
+
while current_dir != '/':
|
75
|
+
if os.path.exists(os.path.join(current_dir, 'pyproject.toml')) or \
|
76
|
+
os.path.exists(os.path.join(current_dir, 'setup.py')):
|
77
|
+
project_root = current_dir
|
78
|
+
break
|
79
|
+
current_dir = os.path.dirname(current_dir)
|
172
80
|
else:
|
173
|
-
#
|
174
|
-
|
175
|
-
while current_dir != '/':
|
176
|
-
if os.path.exists(os.path.join(current_dir, 'pyproject.toml')) or \
|
177
|
-
os.path.exists(os.path.join(current_dir, 'setup.py')):
|
178
|
-
project_root = current_dir
|
179
|
-
break
|
180
|
-
current_dir = os.path.dirname(current_dir)
|
181
|
-
else:
|
182
|
-
# Fallback - use parent of script dir
|
183
|
-
project_root = os.path.dirname(os.path.dirname(os.path.abspath(script_path)))
|
81
|
+
# Fallback - use parent of script dir
|
82
|
+
project_root = os.path.dirname(os.path.dirname(os.path.abspath(script_path)))
|
184
83
|
|
185
84
|
print(f"Project root directory: {project_root}")
|
186
85
|
return project_root
|
testgen/util/randomizer.py
CHANGED
@@ -15,32 +15,6 @@ except ImportError:
|
|
15
15
|
solve_branch_condition = None
|
16
16
|
from testgen.models.test_case import TestCase
|
17
17
|
|
18
|
-
def make_random_move(file_name: str, class_name: str | None, func_node: ast.FunctionDef, test_cases: List[TestCase]) -> List[TestCase]:
|
19
|
-
random_choice = random.randint(1, 4)
|
20
|
-
func_name = func_node.name
|
21
|
-
# new random test case
|
22
|
-
if random_choice == 1:
|
23
|
-
test_cases.append(new_random_test_case(file_name, class_name, func_node))
|
24
|
-
# combine test cases
|
25
|
-
if random_choice == 2:
|
26
|
-
test_cases.append(combine_cases(test_cases))
|
27
|
-
# delete test case
|
28
|
-
if random_choice == 3:
|
29
|
-
test_cases = remove_case(test_cases)
|
30
|
-
|
31
|
-
if random_choice == 4:
|
32
|
-
# TODO: Not sure what to use for test case args/inputs i.e. test_cases[0].inputs is WRONG
|
33
|
-
function_test_cases = [tc for tc in test_cases if tc.func_name == func_name]
|
34
|
-
|
35
|
-
if function_test_cases:
|
36
|
-
uncovered_lines = testgen.util.coverage_utils.get_uncovered_lines_for_func(file_name, class_name, func_name)
|
37
|
-
|
38
|
-
if len(uncovered_lines) > 0:
|
39
|
-
z3_test_cases = solve_branch_condition(file_name, func_node, uncovered_lines)
|
40
|
-
test_cases.extend(z3_test_cases)
|
41
|
-
|
42
|
-
return test_cases
|
43
|
-
|
44
18
|
def new_random_test_case(file_name: str, class_name: str | None, func_node: ast.FunctionDef) -> TestCase:
|
45
19
|
func_name = func_node.name
|
46
20
|
param_types: dict = utils.extract_parameter_types(func_node)
|