testgenie-py 0.2.0__tar.gz → 0.2.2__tar.gz
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.
- {testgenie_py-0.2.0 → testgenie_py-0.2.2}/PKG-INFO +2 -1
- {testgenie_py-0.2.0 → testgenie_py-0.2.2}/pyproject.toml +2 -1
- testgenie_py-0.2.2/testgen/analyzer/random_feedback_analyzer.py +521 -0
- {testgenie_py-0.2.0 → testgenie_py-0.2.2}/testgen/controller/cli_controller.py +5 -10
- {testgenie_py-0.2.0 → testgenie_py-0.2.2}/testgen/controller/docker_controller.py +56 -38
- testgenie_py-0.2.2/testgen/docker/Dockerfile +31 -0
- testgenie_py-0.2.2/testgen/generated_samplecodebin.py +545 -0
- {testgenie_py-0.2.0 → testgenie_py-0.2.2}/testgen/generator/code_generator.py +36 -18
- {testgenie_py-0.2.0 → testgenie_py-0.2.2}/testgen/reinforcement/environment.py +1 -1
- {testgenie_py-0.2.0 → testgenie_py-0.2.2}/testgen/service/generator_service.py +16 -1
- {testgenie_py-0.2.0 → testgenie_py-0.2.2}/testgen/service/service.py +124 -32
- {testgenie_py-0.2.0 → testgenie_py-0.2.2}/testgen/sqlite/db_service.py +22 -2
- {testgenie_py-0.2.0 → testgenie_py-0.2.2}/testgen/util/coverage_utils.py +35 -0
- {testgenie_py-0.2.0 → testgenie_py-0.2.2}/testgen/util/file_utils.py +40 -10
- {testgenie_py-0.2.0 → testgenie_py-0.2.2}/testgen/util/randomizer.py +29 -12
- testgenie_py-0.2.0/testgen/.coverage +0 -0
- testgenie_py-0.2.0/testgen/analyzer/random_feedback_analyzer.py +0 -291
- testgenie_py-0.2.0/testgen/code_to_test/boolean.py +0 -146
- testgenie_py-0.2.0/testgen/code_to_test/calculator.py +0 -29
- testgenie_py-0.2.0/testgen/code_to_test/code_to_fuzz.py +0 -234
- testgenie_py-0.2.0/testgen/code_to_test/code_to_fuzz_lite.py +0 -397
- testgenie_py-0.2.0/testgen/code_to_test/decisions.py +0 -57
- testgenie_py-0.2.0/testgen/code_to_test/math_utils.py +0 -47
- testgenie_py-0.2.0/testgen/code_to_test/no_types.py +0 -35
- testgenie_py-0.2.0/testgen/code_to_test/sample_code_bin.py +0 -141
- testgenie_py-0.2.0/testgen/docker/Dockerfile +0 -22
- testgenie_py-0.2.0/testgen/docker/poetry.lock +0 -599
- testgenie_py-0.2.0/testgen/docker/pyproject.toml +0 -29
- testgenie_py-0.2.0/testgen/q_table/global_q_table.json +0 -1
- testgenie_py-0.2.0/testgen/testgen.db +0 -0
- testgenie_py-0.2.0/testgen/tests/test_decisions.py +0 -195
- testgenie_py-0.2.0/testgen/util/__init__.py +0 -0
- testgenie_py-0.2.0/testgen/util/z3_utils/__init__.py +0 -0
- {testgenie_py-0.2.0 → testgenie_py-0.2.2}/README.md +0 -0
- {testgenie_py-0.2.0 → testgenie_py-0.2.2}/testgen/__init__.py +0 -0
- {testgenie_py-0.2.0 → testgenie_py-0.2.2}/testgen/analyzer/__init__.py +0 -0
- {testgenie_py-0.2.0 → testgenie_py-0.2.2}/testgen/analyzer/ast_analyzer.py +0 -0
- {testgenie_py-0.2.0 → testgenie_py-0.2.2}/testgen/analyzer/contracts/__init__.py +0 -0
- {testgenie_py-0.2.0 → testgenie_py-0.2.2}/testgen/analyzer/contracts/contract.py +0 -0
- {testgenie_py-0.2.0 → testgenie_py-0.2.2}/testgen/analyzer/contracts/no_exception_contract.py +0 -0
- {testgenie_py-0.2.0 → testgenie_py-0.2.2}/testgen/analyzer/contracts/nonnull_contract.py +0 -0
- {testgenie_py-0.2.0 → testgenie_py-0.2.2}/testgen/analyzer/fuzz_analyzer.py +0 -0
- {testgenie_py-0.2.0 → testgenie_py-0.2.2}/testgen/analyzer/reinforcement_analyzer.py +0 -0
- {testgenie_py-0.2.0 → testgenie_py-0.2.2}/testgen/analyzer/test_case_analyzer.py +0 -0
- {testgenie_py-0.2.0 → testgenie_py-0.2.2}/testgen/analyzer/test_case_analyzer_context.py +0 -0
- {testgenie_py-0.2.0/testgen/code_to_test → testgenie_py-0.2.2/testgen/controller}/__init__.py +0 -0
- {testgenie_py-0.2.0/testgen/controller → testgenie_py-0.2.2/testgen/generator}/__init__.py +0 -0
- {testgenie_py-0.2.0 → testgenie_py-0.2.2}/testgen/generator/doctest_generator.py +0 -0
- {testgenie_py-0.2.0 → testgenie_py-0.2.2}/testgen/generator/generator.py +0 -0
- {testgenie_py-0.2.0 → testgenie_py-0.2.2}/testgen/generator/pytest_generator.py +0 -0
- {testgenie_py-0.2.0 → testgenie_py-0.2.2}/testgen/generator/test_generator.py +0 -0
- {testgenie_py-0.2.0 → testgenie_py-0.2.2}/testgen/generator/unit_test_generator.py +0 -0
- {testgenie_py-0.2.0/testgen/generator → testgenie_py-0.2.2/testgen/inspector}/__init__.py +0 -0
- {testgenie_py-0.2.0 → testgenie_py-0.2.2}/testgen/inspector/inspector.py +0 -0
- {testgenie_py-0.2.0 → testgenie_py-0.2.2}/testgen/main.py +0 -0
- {testgenie_py-0.2.0/testgen/inspector → testgenie_py-0.2.2/testgen/models}/__init__.py +0 -0
- {testgenie_py-0.2.0 → testgenie_py-0.2.2}/testgen/models/analysis_context.py +0 -0
- {testgenie_py-0.2.0 → testgenie_py-0.2.2}/testgen/models/function_metadata.py +0 -0
- {testgenie_py-0.2.0 → testgenie_py-0.2.2}/testgen/models/generator_context.py +0 -0
- {testgenie_py-0.2.0 → testgenie_py-0.2.2}/testgen/models/test_case.py +0 -0
- {testgenie_py-0.2.0/testgen/models → testgenie_py-0.2.2/testgen/presentation}/__init__.py +0 -0
- {testgenie_py-0.2.0 → testgenie_py-0.2.2}/testgen/presentation/cli_view.py +0 -0
- {testgenie_py-0.2.0/testgen/presentation → testgenie_py-0.2.2/testgen/reinforcement}/__init__.py +0 -0
- {testgenie_py-0.2.0 → testgenie_py-0.2.2}/testgen/reinforcement/abstract_state.py +0 -0
- {testgenie_py-0.2.0 → testgenie_py-0.2.2}/testgen/reinforcement/agent.py +0 -0
- {testgenie_py-0.2.0 → testgenie_py-0.2.2}/testgen/reinforcement/statement_coverage_state.py +0 -0
- {testgenie_py-0.2.0/testgen/reinforcement → testgenie_py-0.2.2/testgen/service}/__init__.py +0 -0
- {testgenie_py-0.2.0 → testgenie_py-0.2.2}/testgen/service/analysis_service.py +0 -0
- {testgenie_py-0.2.0 → testgenie_py-0.2.2}/testgen/service/cfg_service.py +0 -0
- {testgenie_py-0.2.0 → testgenie_py-0.2.2}/testgen/service/logging_service.py +0 -0
- {testgenie_py-0.2.0/testgen/service → testgenie_py-0.2.2/testgen/sqlite}/__init__.py +0 -0
- {testgenie_py-0.2.0 → testgenie_py-0.2.2}/testgen/sqlite/db.py +0 -0
- {testgenie_py-0.2.0/testgen/sqlite → testgenie_py-0.2.2/testgen/tree}/__init__.py +0 -0
- {testgenie_py-0.2.0 → testgenie_py-0.2.2}/testgen/tree/node.py +0 -0
- {testgenie_py-0.2.0 → testgenie_py-0.2.2}/testgen/tree/tree_utils.py +0 -0
- {testgenie_py-0.2.0/testgen/tests → testgenie_py-0.2.2/testgen/util}/__init__.py +0 -0
- {testgenie_py-0.2.0 → testgenie_py-0.2.2}/testgen/util/coverage_visualizer.py +0 -0
- {testgenie_py-0.2.0 → testgenie_py-0.2.2}/testgen/util/utils.py +0 -0
- {testgenie_py-0.2.0/testgen/tree → testgenie_py-0.2.2/testgen/util/z3_utils}/__init__.py +0 -0
- {testgenie_py-0.2.0 → testgenie_py-0.2.2}/testgen/util/z3_utils/ast_to_z3.py +0 -0
- {testgenie_py-0.2.0 → testgenie_py-0.2.2}/testgen/util/z3_utils/branch_condition.py +0 -0
- {testgenie_py-0.2.0 → testgenie_py-0.2.2}/testgen/util/z3_utils/constraint_extractor.py +0 -0
- {testgenie_py-0.2.0 → testgenie_py-0.2.2}/testgen/util/z3_utils/variable_finder.py +0 -0
- {testgenie_py-0.2.0 → testgenie_py-0.2.2}/testgen/util/z3_utils/z3_test_case.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: testgenie-py
|
3
|
-
Version: 0.2.
|
3
|
+
Version: 0.2.2
|
4
4
|
Summary:
|
5
5
|
Author: cjseitz
|
6
6
|
Author-email: charlesjseitz@gmail.com
|
@@ -19,6 +19,7 @@ Requires-Dist: klara (==0.6.3)
|
|
19
19
|
Requires-Dist: pygraphviz (>=1.14,<2.0)
|
20
20
|
Requires-Dist: pytest (>=8.3.5,<9.0.0)
|
21
21
|
Requires-Dist: staticfg (>=0.9.5,<0.10.0)
|
22
|
+
Requires-Dist: tabulate (>=0.9.0,<0.10.0)
|
22
23
|
Requires-Dist: typed-ast (==1.5.5)
|
23
24
|
Requires-Dist: z3-solver (==4.13.3.0)
|
24
25
|
Description-Content-Type: text/markdown
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[tool.poetry]
|
2
2
|
name = "testgenie-py"
|
3
|
-
version = "0.2.
|
3
|
+
version = "0.2.2"
|
4
4
|
description = ""
|
5
5
|
authors = ["cjseitz <charlesjseitz@gmail.com>"]
|
6
6
|
readme = "README.md"
|
@@ -25,6 +25,7 @@ staticfg = "^0.9.5"
|
|
25
25
|
pytest = "^8.3.5"
|
26
26
|
pygraphviz = "^1.14"
|
27
27
|
docker = "^7.1.0"
|
28
|
+
tabulate = "^0.9.0"
|
28
29
|
|
29
30
|
[build-system]
|
30
31
|
requires = ["poetry-core"]
|
@@ -0,0 +1,521 @@
|
|
1
|
+
import ast
|
2
|
+
import importlib
|
3
|
+
import random
|
4
|
+
import time
|
5
|
+
import traceback
|
6
|
+
from typing import List, Dict, Set
|
7
|
+
import z3
|
8
|
+
|
9
|
+
import testgen.util.randomizer
|
10
|
+
import testgen.util.utils as utils
|
11
|
+
import testgen.util.coverage_utils as coverage_utils
|
12
|
+
from testgen.analyzer.contracts.contract import Contract
|
13
|
+
from testgen.analyzer.contracts.no_exception_contract import NoExceptionContract
|
14
|
+
from testgen.analyzer.contracts.nonnull_contract import NonNullContract
|
15
|
+
from testgen.models.test_case import TestCase
|
16
|
+
from testgen.analyzer.test_case_analyzer import TestCaseAnalyzerStrategy
|
17
|
+
from abc import ABC
|
18
|
+
|
19
|
+
from testgen.models.function_metadata import FunctionMetadata
|
20
|
+
from testgen.util.z3_utils.constraint_extractor import extract_branch_conditions
|
21
|
+
from testgen.util.z3_utils.ast_to_z3 import ast_to_z3_constraint
|
22
|
+
|
23
|
+
|
24
|
+
# Citation in which this method and algorithm were taken from:
|
25
|
+
# C. Pacheco, S. K. Lahiri, M. D. Ernst and T. Ball, "Feedback-Directed Random Test Generation," 29th International
|
26
|
+
# Conference on Software Engineering (ICSE'07), Minneapolis, MN, USA, 2007, pp. 75-84, doi: 10.1109/ICSE.2007.37.
|
27
|
+
# keywords: {System testing;Contracts;Object oriented modeling;Law;Legal factors;Open source software;Software
|
28
|
+
# testing;Feedback;Filters;Error correction codes},
|
29
|
+
|
30
|
+
class RandomFeedbackAnalyzer(TestCaseAnalyzerStrategy, ABC):
|
31
|
+
def __init__(self, analysis_context=None):
|
32
|
+
super().__init__(analysis_context)
|
33
|
+
self.test_cases = []
|
34
|
+
self.covered_lines: Dict[str, Set[int]] = {}
|
35
|
+
self.covered_functions: Set[str] = set()
|
36
|
+
|
37
|
+
def collect_test_cases(self, function_metadata: FunctionMetadata, time_limit: int = 5) -> List[TestCase]:
|
38
|
+
self.test_cases = []
|
39
|
+
start_time = time.time()
|
40
|
+
|
41
|
+
while (time.time() - start_time) < time_limit:
|
42
|
+
|
43
|
+
try:
|
44
|
+
param_values = self.generate_random_inputs(function_metadata.params)
|
45
|
+
module = self.analysis_context.module
|
46
|
+
func_name = function_metadata.function_name
|
47
|
+
|
48
|
+
if self._analysis_context.class_name:
|
49
|
+
cls = getattr(module, self._analysis_context.class_name)
|
50
|
+
obj = cls()
|
51
|
+
function = getattr(obj, func_name)
|
52
|
+
else:
|
53
|
+
function = getattr(module, func_name)
|
54
|
+
|
55
|
+
import inspect
|
56
|
+
sig = inspect.signature(function)
|
57
|
+
param_names = [p.name for p in sig.parameters.values() if p.name != 'self']
|
58
|
+
|
59
|
+
ordered_args = []
|
60
|
+
for name in param_names:
|
61
|
+
if name in param_values:
|
62
|
+
ordered_args.append(param_values[name])
|
63
|
+
else:
|
64
|
+
ordered_args.append(None)
|
65
|
+
|
66
|
+
result = function(*ordered_args)
|
67
|
+
test_case = TestCase(func_name, tuple(ordered_args), result)
|
68
|
+
|
69
|
+
if not self.is_duplicate_test_case(test_case):
|
70
|
+
self.test_cases.append(test_case)
|
71
|
+
|
72
|
+
covered = self.covered(function_metadata)
|
73
|
+
if covered:
|
74
|
+
break
|
75
|
+
else:
|
76
|
+
# Optionally log duplicate detection
|
77
|
+
print(f"Skipping duplicate test case: {func_name}{test_case.inputs}")
|
78
|
+
|
79
|
+
except Exception as e:
|
80
|
+
print(f"Error testing {function_metadata.function_name}: {e}")
|
81
|
+
|
82
|
+
return self.test_cases
|
83
|
+
|
84
|
+
def is_duplicate_test_case(self, new_test_case: TestCase) -> bool:
|
85
|
+
for existing_test_case in self.test_cases:
|
86
|
+
if (existing_test_case.func_name == new_test_case.func_name and
|
87
|
+
existing_test_case.inputs == new_test_case.inputs):
|
88
|
+
return True
|
89
|
+
return False
|
90
|
+
|
91
|
+
def covered(self, func: FunctionMetadata) -> bool:
|
92
|
+
if func.function_name not in self.covered_lines:
|
93
|
+
self.covered_lines[func.function_name] = set()
|
94
|
+
|
95
|
+
for test_case in [tc for tc in self.test_cases if tc.func_name == func.function_name]:
|
96
|
+
analysis = coverage_utils.get_coverage_analysis(self._analysis_context.filepath, self._analysis_context.class_name,
|
97
|
+
func.function_name, test_case.inputs)
|
98
|
+
covered = coverage_utils.get_list_of_covered_statements(analysis)
|
99
|
+
self.covered_lines[func.function_name].update(covered)
|
100
|
+
print(f"Covered lines for {func.function_name}: {self.covered_lines[func.function_name]}")
|
101
|
+
|
102
|
+
executable_statements = set(self.get_all_executable_statements(func))
|
103
|
+
print(f"Executable statements for {func.function_name}: {executable_statements}")
|
104
|
+
|
105
|
+
return self.covered_lines[func.function_name] == executable_statements
|
106
|
+
|
107
|
+
def execute_sequence(self, sequence, contracts: List[Contract]):
|
108
|
+
"""Execute a sequence and check contract violations"""
|
109
|
+
func_name, args_dict = sequence
|
110
|
+
args = tuple(args_dict.values()) # Convert dict values to tuple
|
111
|
+
|
112
|
+
try:
|
113
|
+
# Use module from analysis context if available
|
114
|
+
module = self.analysis_context.module
|
115
|
+
|
116
|
+
if self._analysis_context.class_name:
|
117
|
+
cls = getattr(module, self._analysis_context.class_name, None)
|
118
|
+
if cls is None:
|
119
|
+
raise AttributeError(f"Class '{self._analysis_context.class_name}' not found")
|
120
|
+
obj = cls() # Instantiate the class
|
121
|
+
func = getattr(obj, func_name, None)
|
122
|
+
|
123
|
+
import inspect
|
124
|
+
sig = inspect.signature(func)
|
125
|
+
param_names = [p.name for p in sig.parameters.values() if p.name != 'self']
|
126
|
+
else:
|
127
|
+
func = getattr(module, func_name, None)
|
128
|
+
|
129
|
+
import inspect
|
130
|
+
sig = inspect.signature(func)
|
131
|
+
param_names = [p.name for p in sig.parameters.values()]
|
132
|
+
|
133
|
+
# Create ordered arguments based on function signature
|
134
|
+
ordered_args = []
|
135
|
+
for name in param_names:
|
136
|
+
if name in args_dict:
|
137
|
+
ordered_args.append(args_dict[name])
|
138
|
+
|
139
|
+
# Check preconditions
|
140
|
+
for contract in contracts:
|
141
|
+
if not contract.check_preconditions(tuple(ordered_args)):
|
142
|
+
print(f"Preconditions failed for {func_name} with {tuple(ordered_args)}")
|
143
|
+
return None, True
|
144
|
+
|
145
|
+
# Execute function with properly ordered arguments
|
146
|
+
output = func(*ordered_args)
|
147
|
+
exception = None
|
148
|
+
|
149
|
+
except Exception as e:
|
150
|
+
print(f"EXCEPTION IN RANDOM FEEDBACK: {e}")
|
151
|
+
print(traceback.format_exc())
|
152
|
+
output = None
|
153
|
+
exception = e
|
154
|
+
|
155
|
+
# Check postconditions
|
156
|
+
for contract in contracts:
|
157
|
+
if not contract.check_postconditions(tuple(ordered_args), output, exception):
|
158
|
+
print(f"Postcondition failed for {func_name} with {tuple(ordered_args)}")
|
159
|
+
return output, True
|
160
|
+
|
161
|
+
return output, False
|
162
|
+
|
163
|
+
|
164
|
+
# TODO: Currently only getting random vals of primitives, extend to sequences
|
165
|
+
def random_seqs_and_vals(self, param_types, non_error_seqs=None):
|
166
|
+
return self.generate_random_inputs(param_types)
|
167
|
+
|
168
|
+
@staticmethod
|
169
|
+
def extract_parameter_types(func_node):
|
170
|
+
"""Extract parameter types from a function node."""
|
171
|
+
param_types = {}
|
172
|
+
for arg in func_node.args.args:
|
173
|
+
param_name = arg.arg
|
174
|
+
if arg.annotation:
|
175
|
+
param_type = ast.unparse(arg.annotation)
|
176
|
+
param_types[param_name] = param_type
|
177
|
+
else:
|
178
|
+
if param_name != 'self':
|
179
|
+
param_types[param_name] = None
|
180
|
+
return param_types
|
181
|
+
|
182
|
+
@staticmethod
|
183
|
+
def generate_random_inputs(param_types):
|
184
|
+
"""Generate inputs for fuzzing based on parameter types."""
|
185
|
+
inputs = {}
|
186
|
+
for param, param_type in param_types.items():
|
187
|
+
if param_type == "int":
|
188
|
+
random_integer = random.randint(-500, 500) # Wider range for better edge cases
|
189
|
+
inputs[param] = random_integer
|
190
|
+
elif param_type == "bool":
|
191
|
+
random_choice = random.choice([True, False])
|
192
|
+
inputs[param] = random_choice
|
193
|
+
elif param_type == "float":
|
194
|
+
random_float = random.uniform(-500.0, 500.0) # Wider range for better edge cases
|
195
|
+
inputs[param] = random_float
|
196
|
+
elif param_type == "str":
|
197
|
+
# Generate diverse strings instead of always "abc"
|
198
|
+
string_type = random.choice([
|
199
|
+
"empty", "short", "medium", "long", "special", "numeric", "whitespace"
|
200
|
+
])
|
201
|
+
|
202
|
+
if string_type == "empty":
|
203
|
+
inputs[param] = ""
|
204
|
+
elif string_type == "short":
|
205
|
+
inputs[param] = ''.join(random.choices('abcdefghijklmnopqrstuvwxyz', k=random.randint(1, 3)))
|
206
|
+
elif string_type == "medium":
|
207
|
+
inputs[param] = ''.join(random.choices('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', k=random.randint(4, 10)))
|
208
|
+
elif string_type == "long":
|
209
|
+
inputs[param] = ''.join(random.choices('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', k=random.randint(11, 30)))
|
210
|
+
elif string_type == "special":
|
211
|
+
inputs[param] = ''.join(random.choices('!@#$%^&*()_+-=[]{}|;:,./<>?', k=random.randint(1, 8)))
|
212
|
+
elif string_type == "numeric":
|
213
|
+
inputs[param] = ''.join(random.choices('0123456789', k=random.randint(1, 10)))
|
214
|
+
else: # whitespace
|
215
|
+
inputs[param] = ' ' * random.randint(1, 5)
|
216
|
+
else:
|
217
|
+
# For unknown types, try a default value
|
218
|
+
inputs[param] = None
|
219
|
+
|
220
|
+
return inputs
|
221
|
+
|
222
|
+
# Algorithm described in above article
|
223
|
+
# Classes is the classes for which we want to generate sequences
|
224
|
+
# Contracts express invariant properties that hold both at entry and exit from a call
|
225
|
+
# Contract takes as input the current state of the system (runtime values created in the sequence so far, and any exception thrown by the last call), and returns satisfied or violated
|
226
|
+
# Output is the runtime values and boolean flag violated
|
227
|
+
# Filters determine which values of a sequence are extensible and should be used as inputs
|
228
|
+
def generate_sequences(self, function_metadata: List[FunctionMetadata], classes=None, contracts: List[Contract] = None, filters=None, time_limit=20):
|
229
|
+
contracts = [NonNullContract(), NoExceptionContract()]
|
230
|
+
error_seqs = [] # execution violates a contract
|
231
|
+
non_error_seqs = [] # execution does not violate a contract
|
232
|
+
|
233
|
+
functions = self._analysis_context.function_data
|
234
|
+
start_time = time.time()
|
235
|
+
while(time.time() - start_time) >= time_limit:
|
236
|
+
# Get random function
|
237
|
+
func = random.choice(functions)
|
238
|
+
param_types: dict = func.params
|
239
|
+
vals: dict = self.random_seqs_and_vals(param_types)
|
240
|
+
new_seq = (func.function_name, vals)
|
241
|
+
if new_seq in error_seqs or new_seq in non_error_seqs:
|
242
|
+
continue
|
243
|
+
outs_violated: tuple = self.execute_sequence(new_seq, contracts)
|
244
|
+
violated: bool = outs_violated[1]
|
245
|
+
# Create tuple of sequence ((func name, args), output)
|
246
|
+
new_seq_out = (new_seq, outs_violated[0])
|
247
|
+
if violated:
|
248
|
+
error_seqs.append(new_seq_out)
|
249
|
+
else:
|
250
|
+
# Question: Should I use the failed contract to be the assertion in unit test??
|
251
|
+
non_error_seqs.append(new_seq_out)
|
252
|
+
return error_seqs, non_error_seqs
|
253
|
+
|
254
|
+
def generate_sequences_new(self, contracts: List[Contract] = None, filters=None, time_limit=20):
|
255
|
+
contracts = [NonNullContract(), NoExceptionContract()]
|
256
|
+
error_seqs = [] # execution violates a contract
|
257
|
+
non_error_seqs = [] # execution does not violate a contract
|
258
|
+
|
259
|
+
functions = self._analysis_context.function_data.copy()
|
260
|
+
start_time = time.time()
|
261
|
+
|
262
|
+
while (time.time() - start_time) < time_limit:
|
263
|
+
# Get random function
|
264
|
+
func = random.choice(functions)
|
265
|
+
param_types: dict = func.params
|
266
|
+
vals: dict = self.random_seqs_and_vals(param_types)
|
267
|
+
new_seq = (func.function_name, vals)
|
268
|
+
|
269
|
+
if new_seq in [seq[0] for seq in error_seqs] or new_seq in [seq[0] for seq in non_error_seqs]:
|
270
|
+
continue
|
271
|
+
|
272
|
+
outs_violated: tuple = self.execute_sequence(new_seq, contracts)
|
273
|
+
violated: bool = outs_violated[1]
|
274
|
+
|
275
|
+
# Create tuple of sequence ((func name, args), output)
|
276
|
+
new_seq_out = (new_seq, outs_violated[0])
|
277
|
+
|
278
|
+
if violated:
|
279
|
+
error_seqs.append(new_seq_out)
|
280
|
+
|
281
|
+
else:
|
282
|
+
non_error_seqs.append(new_seq_out)
|
283
|
+
|
284
|
+
test_case = TestCase(new_seq_out[0][0], tuple(new_seq_out[0][1].values()), new_seq_out[1])
|
285
|
+
self.test_cases.append(test_case)
|
286
|
+
fully_covered = self.covered(func)
|
287
|
+
if fully_covered:
|
288
|
+
print(f"Function {func.function_name} is fully covered")
|
289
|
+
functions.remove(func)
|
290
|
+
|
291
|
+
if not functions:
|
292
|
+
self.test_cases.sort(key=lambda tc: tc.func_name)
|
293
|
+
print("All functions covered")
|
294
|
+
break
|
295
|
+
|
296
|
+
self.test_cases.sort(key=lambda tc: tc.func_name)
|
297
|
+
return error_seqs, non_error_seqs
|
298
|
+
|
299
|
+
def get_all_executable_statements(self, func: FunctionMetadata):
|
300
|
+
import ast
|
301
|
+
|
302
|
+
test_cases = [tc for tc in self.test_cases if tc.func_name == func.function_name]
|
303
|
+
|
304
|
+
if not test_cases:
|
305
|
+
print("Warning: No test cases available to determine executable statements")
|
306
|
+
from testgen.util.randomizer import new_random_test_case
|
307
|
+
temp_case = new_random_test_case(self._analysis_context.filepath, func.func_def)
|
308
|
+
analysis = coverage_utils.get_coverage_analysis(self._analysis_context.filepath, self._analysis_context.class_name, func.function_name,
|
309
|
+
temp_case.inputs)
|
310
|
+
else:
|
311
|
+
analysis = coverage_utils.get_coverage_analysis(self._analysis_context.filepath, self._analysis_context.class_name, func.function_name, test_cases[0].inputs)
|
312
|
+
|
313
|
+
executable_lines = list(analysis[1])
|
314
|
+
|
315
|
+
with open(self._analysis_context.filepath, 'r') as f:
|
316
|
+
source = f.read()
|
317
|
+
|
318
|
+
tree = ast.parse(source)
|
319
|
+
|
320
|
+
for node in ast.walk(tree):
|
321
|
+
if isinstance(node, ast.FunctionDef) and node.name == func.func_def.name:
|
322
|
+
for if_node in ast.walk(node):
|
323
|
+
if isinstance(if_node, ast.If) and if_node.orelse:
|
324
|
+
if isinstance(if_node.orelse[0], ast.If):
|
325
|
+
continue
|
326
|
+
else_line = if_node.orelse[0].lineno - 1
|
327
|
+
|
328
|
+
with open(self._analysis_context.filepath, 'r') as f:
|
329
|
+
lines = f.readlines()
|
330
|
+
if else_line <= len(lines):
|
331
|
+
line_content = lines[else_line - 1].strip()
|
332
|
+
if line_content == "else:":
|
333
|
+
if else_line not in executable_lines:
|
334
|
+
executable_lines.append(else_line)
|
335
|
+
|
336
|
+
return sorted(executable_lines)
|
337
|
+
|
338
|
+
"""
|
339
|
+
def collect_test_cases_with_z3(self, function_metadata: FunctionMetadata) -> List[TestCase]:
|
340
|
+
test_cases = []
|
341
|
+
|
342
|
+
z3_test_cases = self.generate_z3_test_cases(function_metadata)
|
343
|
+
if z3_test_cases:
|
344
|
+
test_cases.extend(z3_test_cases)
|
345
|
+
|
346
|
+
if not test_cases:
|
347
|
+
test_cases = self.generate_sequences_new()[1]
|
348
|
+
|
349
|
+
self.test_cases = test_cases
|
350
|
+
return test_cases
|
351
|
+
|
352
|
+
def generate_z3_test_cases(self, function_metadata: FunctionMetadata) -> List[TestCase]:
|
353
|
+
test_cases = []
|
354
|
+
|
355
|
+
branch_conditions, param_types = extract_branch_conditions(function_metadata.func_def)
|
356
|
+
|
357
|
+
if not branch_conditions:
|
358
|
+
random_inputs = self.generate_random_inputs(function_metadata.params)
|
359
|
+
try:
|
360
|
+
module = self.analysis_context.module
|
361
|
+
func_name = function_metadata.function_name
|
362
|
+
|
363
|
+
if self._analysis_context.class_name:
|
364
|
+
cls = getattr(module, self._analysis_context.class_name)
|
365
|
+
obj = cls()
|
366
|
+
func = getattr(obj, func_name)
|
367
|
+
ordered_args = self._order_arguments(func, random_inputs)
|
368
|
+
output = func(*ordered_args)
|
369
|
+
else:
|
370
|
+
func = getattr(module, func_name)
|
371
|
+
ordered_args = self._order_arguments(func, random_inputs)
|
372
|
+
output = func(*ordered_args)
|
373
|
+
|
374
|
+
test_cases.append(TestCase(func_name, tuple(ordered_args), output))
|
375
|
+
except Exception as e:
|
376
|
+
print(f"Error executing function with random inputs: {e}")
|
377
|
+
|
378
|
+
return test_cases
|
379
|
+
|
380
|
+
for branch_condition in branch_conditions:
|
381
|
+
try:
|
382
|
+
z3_expr, z3_vars = ast_to_z3_constraint(branch_condition, function_metadata.params)
|
383
|
+
|
384
|
+
solver = z3.Solver()
|
385
|
+
solver.add(z3_expr)
|
386
|
+
|
387
|
+
neg_solver = z3.Solver()
|
388
|
+
neg_solver.add(z3.Not(z3_expr))
|
389
|
+
|
390
|
+
for current_solver in [solver, neg_solver]:
|
391
|
+
if current_solver.check() == z3.sat:
|
392
|
+
model = current_solver.model()
|
393
|
+
|
394
|
+
param_values = self._extract_z3_solution(model, z3_vars, function_metadata.params)
|
395
|
+
|
396
|
+
ordered_params = self._order_parameters(function_metadata.func_def, param_values)
|
397
|
+
|
398
|
+
try:
|
399
|
+
module = self.analysis_context.module
|
400
|
+
func_name = function_metadata.function_name
|
401
|
+
|
402
|
+
if self._analysis_context.class_name:
|
403
|
+
cls = getattr(module, self._analysis_context.class_name)
|
404
|
+
obj = cls()
|
405
|
+
func = getattr(obj, func_name)
|
406
|
+
else:
|
407
|
+
func = getattr(module, func_name)
|
408
|
+
|
409
|
+
result = func(*ordered_params)
|
410
|
+
test_cases.append(TestCase(func_name, tuple(ordered_params), result))
|
411
|
+
except Exception as e:
|
412
|
+
print(f"Error executing function with Z3 solution: {e}")
|
413
|
+
self._add_random_test_case(function_metadata, test_cases)
|
414
|
+
else:
|
415
|
+
self._add_random_test_case(function_metadata, test_cases)
|
416
|
+
|
417
|
+
except Exception as e:
|
418
|
+
print(f"Error processing branch condition with Z3: {e}")
|
419
|
+
self._add_random_test_case(function_metadata, test_cases)
|
420
|
+
|
421
|
+
return test_cases
|
422
|
+
|
423
|
+
def _extract_z3_solution(self, model, z3_vars, param_types):
|
424
|
+
param_values = {}
|
425
|
+
|
426
|
+
for var_name, z3_var in z3_vars.items():
|
427
|
+
if var_name in param_types:
|
428
|
+
try:
|
429
|
+
model_value = model.evaluate(z3_var)
|
430
|
+
|
431
|
+
if param_types[var_name] == "int":
|
432
|
+
param_values[var_name] = model_value.as_long()
|
433
|
+
elif param_types[var_name] == "float":
|
434
|
+
param_values[var_name] = float(model_value.as_decimal(10))
|
435
|
+
elif param_types[var_name] == "bool":
|
436
|
+
param_values[var_name] = z3.is_true(model_value)
|
437
|
+
elif param_types[var_name] == "str":
|
438
|
+
str_val = str(model_value)
|
439
|
+
if str_val.startswith('"') and str_val.endswith('"'):
|
440
|
+
str_val = str_val[1:-1]
|
441
|
+
param_values[var_name] = str_val
|
442
|
+
else:
|
443
|
+
# Default to int for unrecognized types
|
444
|
+
param_values[var_name] = model_value.as_long()
|
445
|
+
except Exception as e:
|
446
|
+
print(f"Couldn't get {var_name} from model: {e}")
|
447
|
+
# Use default values for parameters not in the model
|
448
|
+
if param_types[var_name] == "int":
|
449
|
+
param_values[var_name] = 0
|
450
|
+
elif param_types[var_name] == "float":
|
451
|
+
param_values[var_name] = 0.0
|
452
|
+
elif param_types[var_name] == "bool":
|
453
|
+
param_values[var_name] = False
|
454
|
+
elif param_types[var_name] == "str":
|
455
|
+
param_values[var_name] = ""
|
456
|
+
else:
|
457
|
+
param_values[var_name] = None
|
458
|
+
|
459
|
+
return param_values
|
460
|
+
|
461
|
+
def _order_parameters(self, func_node, param_values):
|
462
|
+
ordered_params = []
|
463
|
+
|
464
|
+
for arg in func_node.args.args:
|
465
|
+
arg_name = arg.arg
|
466
|
+
if arg_name == 'self': # Skip self parameter
|
467
|
+
continue
|
468
|
+
if arg_name in param_values:
|
469
|
+
ordered_params.append(param_values[arg_name])
|
470
|
+
else:
|
471
|
+
# Default value handling if parameter not in solution
|
472
|
+
if arg.annotation and hasattr(arg.annotation, 'id'):
|
473
|
+
if arg.annotation.id == 'int':
|
474
|
+
ordered_params.append(0)
|
475
|
+
elif arg.annotation.id == 'float':
|
476
|
+
ordered_params.append(0.0)
|
477
|
+
elif arg.annotation.id == 'bool':
|
478
|
+
ordered_params.append(False)
|
479
|
+
elif arg.annotation.id == 'str':
|
480
|
+
ordered_params.append('')
|
481
|
+
else:
|
482
|
+
ordered_params.append(None)
|
483
|
+
else:
|
484
|
+
ordered_params.append(None)
|
485
|
+
|
486
|
+
return ordered_params
|
487
|
+
|
488
|
+
def _order_arguments(self, func, args_dict):
|
489
|
+
import inspect
|
490
|
+
sig = inspect.signature(func)
|
491
|
+
param_names = [p.name for p in sig.parameters.values() if p.name != 'self']
|
492
|
+
|
493
|
+
ordered_args = []
|
494
|
+
for name in param_names:
|
495
|
+
if name in args_dict:
|
496
|
+
ordered_args.append(args_dict[name])
|
497
|
+
else:
|
498
|
+
ordered_args.append(None) # Default to None if missing
|
499
|
+
|
500
|
+
return ordered_args
|
501
|
+
|
502
|
+
def _add_random_test_case(self, function_metadata, test_cases):
|
503
|
+
random_inputs = self.generate_random_inputs(function_metadata.params)
|
504
|
+
try:
|
505
|
+
module = self.analysis_context.module
|
506
|
+
func_name = function_metadata.function_name
|
507
|
+
|
508
|
+
if self._analysis_context.class_name:
|
509
|
+
cls = getattr(module, self._analysis_context.class_name)
|
510
|
+
obj = cls()
|
511
|
+
func = getattr(obj, func_name)
|
512
|
+
else:
|
513
|
+
func = getattr(module, func_name)
|
514
|
+
|
515
|
+
ordered_args = self._order_arguments(func, random_inputs)
|
516
|
+
|
517
|
+
output = func(*ordered_args)
|
518
|
+
test_cases.append(TestCase(func_name, tuple(ordered_args), output))
|
519
|
+
except Exception as e:
|
520
|
+
print(f"Error executing function with random inputs: {e}")
|
521
|
+
"""
|
@@ -42,10 +42,9 @@ class CLIController:
|
|
42
42
|
|
43
43
|
logger = get_logger()
|
44
44
|
|
45
|
-
if args.
|
46
|
-
|
47
|
-
|
48
|
-
self.service.select_all_from_db()
|
45
|
+
if args.query:
|
46
|
+
print(f"Querying database for file: {args.file_path}")
|
47
|
+
self.service.query_test_file_data(args.file_path)
|
49
48
|
return
|
50
49
|
|
51
50
|
running_in_docker = os.environ.get("RUNNING_IN_DOCKER") is not None
|
@@ -111,6 +110,7 @@ class CLIController:
|
|
111
110
|
parser = argparse.ArgumentParser(description="A CLI tool for generating unit tests.")
|
112
111
|
parser.add_argument("file_path", type=str, help="Path to the Python file.")
|
113
112
|
parser.add_argument("--output", "-o", type=str, help="Path to output directory.")
|
113
|
+
parser.add_argument("-q", "--query", action="store_true", help="Query the database for test cases, coverage data, and test results for a specific file")
|
114
114
|
parser.add_argument(
|
115
115
|
"--generate-only", "-g",
|
116
116
|
action="store_true",
|
@@ -146,12 +146,7 @@ class CLIController:
|
|
146
146
|
help="Path to SQLite database file (default: testgen.db)"
|
147
147
|
)
|
148
148
|
parser.add_argument(
|
149
|
-
"
|
150
|
-
action="store_true",
|
151
|
-
help="Select all from sqlite db"
|
152
|
-
)
|
153
|
-
parser.add_argument(
|
154
|
-
"--visualize",
|
149
|
+
"-viz", "--visualize",
|
155
150
|
action="store_true",
|
156
151
|
help = "Visualize the tests with graphviz"
|
157
152
|
)
|