testgenie-py 0.3.7__py3-none-any.whl → 0.3.9__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.
Files changed (49) hide show
  1. testgen/analyzer/ast_analyzer.py +2 -11
  2. testgen/analyzer/fuzz_analyzer.py +1 -6
  3. testgen/analyzer/random_feedback_analyzer.py +20 -293
  4. testgen/analyzer/reinforcement_analyzer.py +59 -57
  5. testgen/analyzer/test_case_analyzer_context.py +0 -6
  6. testgen/controller/cli_controller.py +35 -29
  7. testgen/controller/docker_controller.py +1 -0
  8. testgen/db/dao.py +68 -0
  9. testgen/db/dao_impl.py +226 -0
  10. testgen/{sqlite → db}/db.py +15 -6
  11. testgen/generator/pytest_generator.py +2 -10
  12. testgen/generator/unit_test_generator.py +2 -11
  13. testgen/main.py +1 -3
  14. testgen/models/coverage_data.py +56 -0
  15. testgen/models/db_test_case.py +65 -0
  16. testgen/models/function.py +56 -0
  17. testgen/models/function_metadata.py +11 -1
  18. testgen/models/generator_context.py +30 -3
  19. testgen/models/source_file.py +29 -0
  20. testgen/models/test_result.py +38 -0
  21. testgen/models/test_suite.py +20 -0
  22. testgen/reinforcement/agent.py +1 -27
  23. testgen/reinforcement/environment.py +11 -93
  24. testgen/reinforcement/statement_coverage_state.py +5 -4
  25. testgen/service/analysis_service.py +31 -22
  26. testgen/service/cfg_service.py +3 -1
  27. testgen/service/coverage_service.py +115 -0
  28. testgen/service/db_service.py +140 -0
  29. testgen/service/generator_service.py +77 -20
  30. testgen/service/logging_service.py +2 -2
  31. testgen/service/service.py +62 -231
  32. testgen/service/test_executor_service.py +145 -0
  33. testgen/util/coverage_utils.py +38 -116
  34. testgen/util/coverage_visualizer.py +10 -9
  35. testgen/util/file_utils.py +10 -111
  36. testgen/util/randomizer.py +0 -26
  37. testgen/util/utils.py +197 -38
  38. {testgenie_py-0.3.7.dist-info → testgenie_py-0.3.9.dist-info}/METADATA +1 -1
  39. testgenie_py-0.3.9.dist-info/RECORD +72 -0
  40. testgen/inspector/inspector.py +0 -59
  41. testgen/presentation/__init__.py +0 -0
  42. testgen/presentation/cli_view.py +0 -12
  43. testgen/sqlite/__init__.py +0 -0
  44. testgen/sqlite/db_service.py +0 -239
  45. testgen/testgen.db +0 -0
  46. testgenie_py-0.3.7.dist-info/RECORD +0 -67
  47. /testgen/{inspector → db}/__init__.py +0 -0
  48. {testgenie_py-0.3.7.dist-info → testgenie_py-0.3.9.dist-info}/WHEEL +0 -0
  49. {testgenie_py-0.3.7.dist-info → testgenie_py-0.3.9.dist-info}/entry_points.txt +0 -0
@@ -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 get_branch_coverage(file_name, func, *args) -> list:
15
- cov = coverage.Coverage(branch=True)
16
- cov.start()
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(*args)
19
+ func = function_metadata.func
95
20
 
96
- cov.stop()
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
- # Find all branching statements (if/else) in function
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 get_coverage_percentage(analysis: tuple) -> float:
138
- total_statements = len(analysis[1])
139
- missed_statements = len(analysis[3])
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
- def get_list_of_missed_lines(analysis: tuple) -> list:
145
- return analysis[3] # analysis[3] is list of missed line numbers
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(analysis_context: AnalysisContext, func: FunctionMetadata, test_cases):
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
- print("Warning: No test cases available to determine executable statements")
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(analysis_context.filepath, analysis_context.class_name, func.function_name, test_cases[0].inputs)
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(analysis_context.filepath, 'r') as f:
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(analysis_context.filepath, 'r') as f:
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, class_name: str | None, func_def: ast.FunctionDef, test_cases: List[TestCase]):
22
- if func_def.name not in self.covered_lines:
23
- self.covered_lines[func_def.name] = set()
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 == func_def.name]:
26
- analysis = testgen.util.coverage_utils.get_coverage_analysis(file_path, class_name, func_def.name, test_case.inputs)
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[func_def.name].update(covered)
30
+ self.covered_lines[function_data.function_name].update(covered)
30
31
 
31
- if func_def.name in self.covered_lines:
32
- self.logger.debug(f"Covered lines for {func_def.name}: {self.covered_lines[func_def.name]}")
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 {func_def.name}")
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"""
@@ -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
- # Don't use sys.argv[0] as it points to the virtualenv
168
- # Instead, find the actual project root
169
- if os.path.exists('/mnt/c/Users/cjsei/thesis/dev/testgen'):
170
- # Hard-coded path for now
171
- project_root = '/mnt/c/Users/cjsei/thesis/dev/testgen'
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
- # Try to find project root by looking for pyproject.toml or similar
174
- current_dir = os.path.dirname(os.path.abspath(script_path))
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
@@ -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)