testgenie-py 0.1.0__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/__init__.py +0 -0
- testgen/analyzer/__init__.py +0 -0
- testgen/analyzer/ast_analyzer.py +149 -0
- testgen/analyzer/contracts/__init__.py +0 -0
- testgen/analyzer/contracts/contract.py +13 -0
- testgen/analyzer/contracts/no_exception_contract.py +16 -0
- testgen/analyzer/contracts/nonnull_contract.py +15 -0
- testgen/analyzer/fuzz_analyzer.py +106 -0
- testgen/analyzer/random_feedback_analyzer.py +291 -0
- testgen/analyzer/reinforcement_analyzer.py +75 -0
- testgen/analyzer/test_case_analyzer.py +46 -0
- testgen/analyzer/test_case_analyzer_context.py +58 -0
- testgen/controller/__init__.py +0 -0
- testgen/controller/cli_controller.py +194 -0
- testgen/controller/docker_controller.py +169 -0
- testgen/docker/Dockerfile +22 -0
- testgen/docker/poetry.lock +361 -0
- testgen/docker/pyproject.toml +22 -0
- testgen/generator/__init__.py +0 -0
- testgen/generator/code_generator.py +66 -0
- testgen/generator/doctest_generator.py +208 -0
- testgen/generator/generator.py +55 -0
- testgen/generator/pytest_generator.py +77 -0
- testgen/generator/test_generator.py +26 -0
- testgen/generator/unit_test_generator.py +84 -0
- testgen/inspector/__init__.py +0 -0
- testgen/inspector/inspector.py +61 -0
- testgen/main.py +13 -0
- testgen/models/__init__.py +0 -0
- testgen/models/analysis_context.py +56 -0
- testgen/models/function_metadata.py +61 -0
- testgen/models/generator_context.py +63 -0
- testgen/models/test_case.py +8 -0
- testgen/presentation/__init__.py +0 -0
- testgen/presentation/cli_view.py +12 -0
- testgen/q_table/global_q_table.json +1 -0
- testgen/reinforcement/__init__.py +0 -0
- testgen/reinforcement/abstract_state.py +7 -0
- testgen/reinforcement/agent.py +153 -0
- testgen/reinforcement/environment.py +215 -0
- testgen/reinforcement/statement_coverage_state.py +33 -0
- testgen/service/__init__.py +0 -0
- testgen/service/analysis_service.py +260 -0
- testgen/service/cfg_service.py +55 -0
- testgen/service/generator_service.py +169 -0
- testgen/service/service.py +389 -0
- testgen/sqlite/__init__.py +0 -0
- testgen/sqlite/db.py +84 -0
- testgen/sqlite/db_service.py +219 -0
- testgen/tree/__init__.py +0 -0
- testgen/tree/node.py +7 -0
- testgen/tree/tree_utils.py +79 -0
- testgen/util/__init__.py +0 -0
- testgen/util/coverage_utils.py +168 -0
- testgen/util/coverage_visualizer.py +154 -0
- testgen/util/file_utils.py +110 -0
- testgen/util/randomizer.py +122 -0
- testgen/util/utils.py +143 -0
- testgen/util/z3_utils/__init__.py +0 -0
- testgen/util/z3_utils/ast_to_z3.py +99 -0
- testgen/util/z3_utils/branch_condition.py +72 -0
- testgen/util/z3_utils/constraint_extractor.py +36 -0
- testgen/util/z3_utils/variable_finder.py +10 -0
- testgen/util/z3_utils/z3_test_case.py +94 -0
- testgenie_py-0.1.0.dist-info/METADATA +24 -0
- testgenie_py-0.1.0.dist-info/RECORD +68 -0
- testgenie_py-0.1.0.dist-info/WHEEL +4 -0
- testgenie_py-0.1.0.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,208 @@
|
|
1
|
+
import inspect
|
2
|
+
from typing import Optional, List, Set, Tuple
|
3
|
+
|
4
|
+
import astor
|
5
|
+
from testgen.generator.test_generator import TestGenerator
|
6
|
+
from testgen.models.generator_context import GeneratorContext
|
7
|
+
|
8
|
+
class DocTestGenerator(TestGenerator):
|
9
|
+
def __init__(self, generator_context: GeneratorContext):
|
10
|
+
super().__init__(generator_context)
|
11
|
+
|
12
|
+
# Implementing abstract methods from TestGenerator
|
13
|
+
def generate_test_header(self):
|
14
|
+
pass
|
15
|
+
|
16
|
+
def generate_test_function(self, unique_func_name, func_name, cases) -> None:
|
17
|
+
"""Generate a test function with doctests."""
|
18
|
+
doctest_examples = []
|
19
|
+
|
20
|
+
for inputs, expected in cases:
|
21
|
+
input_args = ', '.join(map(repr, inputs))
|
22
|
+
|
23
|
+
if self._generator_context.class_name:
|
24
|
+
test_call = f"{self._generator_context.class_name}().{func_name}({input_args})"
|
25
|
+
else:
|
26
|
+
test_call = f"{func_name}({input_args})"
|
27
|
+
|
28
|
+
result_str = repr(expected)
|
29
|
+
# Format with >>> and proper spacing
|
30
|
+
doctest = f">>> {test_call}\n{result_str}"
|
31
|
+
doctest_examples.append(doctest)
|
32
|
+
|
33
|
+
# Inject the doctests into the original function's docstring
|
34
|
+
self._inject_docstring(func_name, doctest_examples)
|
35
|
+
|
36
|
+
def save_file(self):
|
37
|
+
"""Save the generated test content to a file."""
|
38
|
+
return self._generator_context.filepath
|
39
|
+
|
40
|
+
def _inject_docstring(self, func_name, doctest_examples):
|
41
|
+
"""Inject doctest examples into the function's docstring."""
|
42
|
+
module_path = self._generator_context.filepath
|
43
|
+
|
44
|
+
# Read the source file
|
45
|
+
with open(module_path, 'r') as f:
|
46
|
+
lines = f.readlines()
|
47
|
+
|
48
|
+
func_line_num, func_indent = self._get_func_line_num_and_indent(lines, func_name)
|
49
|
+
|
50
|
+
if func_line_num == -1:
|
51
|
+
print(f"Function {func_name} not found in {module_path}")
|
52
|
+
return
|
53
|
+
|
54
|
+
has_docstring, docstring_end = self._get_docstring_end(func_line_num, lines)
|
55
|
+
|
56
|
+
all_examples, seen_examples = self._collect_existing_docstrings(func_line_num, lines, has_docstring, docstring_end)
|
57
|
+
|
58
|
+
|
59
|
+
all_examples, seen_examples = self._add_new_docstrings(doctest_examples, all_examples, seen_examples)
|
60
|
+
|
61
|
+
docstring_lines = self._create_docstrings(func_indent, all_examples)
|
62
|
+
|
63
|
+
# Modify the source code
|
64
|
+
if has_docstring:
|
65
|
+
# Replace existing docstring
|
66
|
+
lines = lines[:func_line_num + 1] + [line + '\n' for line in docstring_lines] + lines[docstring_end + 1:]
|
67
|
+
else:
|
68
|
+
# Insert new docstring after function definition
|
69
|
+
lines = lines[:func_line_num + 1] + [line + '\n' for line in docstring_lines] + lines[func_line_num + 1:]
|
70
|
+
|
71
|
+
# Write back to the file
|
72
|
+
with open(module_path, 'w') as f:
|
73
|
+
f.writelines(lines)
|
74
|
+
|
75
|
+
def _extract_doctest_examples(self, docstring_lines: List[str]) -> List[Tuple[str, str]]:
|
76
|
+
examples = []
|
77
|
+
current_call = None
|
78
|
+
current_result_lines = []
|
79
|
+
|
80
|
+
# Process docstring line by line
|
81
|
+
for line in docstring_lines:
|
82
|
+
stripped_line = line.strip()
|
83
|
+
|
84
|
+
# Check if this is a test call line (starts with >>>)
|
85
|
+
if stripped_line.startswith('>>>'):
|
86
|
+
# If we were already collecting a test, save it
|
87
|
+
if current_call is not None:
|
88
|
+
result = '\n'.join([line.strip() for line in current_result_lines if line.strip()])
|
89
|
+
examples.append((current_call, result))
|
90
|
+
current_result_lines = []
|
91
|
+
|
92
|
+
# Extract the call part (everything after >>>)
|
93
|
+
current_call = stripped_line[3:].strip()
|
94
|
+
|
95
|
+
# Otherwise, if we have a current call, add to the result
|
96
|
+
elif current_call is not None:
|
97
|
+
current_result_lines.append(stripped_line)
|
98
|
+
|
99
|
+
# Don't forget the last example if there is one
|
100
|
+
if current_call is not None and current_result_lines:
|
101
|
+
result = '\n'.join([line.strip() for line in current_result_lines if line.strip()])
|
102
|
+
examples.append((current_call, result))
|
103
|
+
|
104
|
+
return examples
|
105
|
+
|
106
|
+
|
107
|
+
def _is_class_method(self, function_name: str) -> bool:
|
108
|
+
"""Check if a function is a method of a class in the module."""
|
109
|
+
for name, obj in inspect.getmembers(self._generator_context.module):
|
110
|
+
if inspect.isclass(obj) and hasattr(obj, function_name):
|
111
|
+
return True
|
112
|
+
return False
|
113
|
+
|
114
|
+
def _get_class_name(self, method_name: str) -> Optional[str]:
|
115
|
+
"""Get the name of the class that contains the method."""
|
116
|
+
for name, obj in inspect.getmembers(self._generator_context.module):
|
117
|
+
if inspect.isclass(obj) and hasattr(obj, method_name):
|
118
|
+
return name
|
119
|
+
return None
|
120
|
+
|
121
|
+
def _get_func_line_num_and_indent(self, lines: List[str], func_name: str) -> Tuple[int, str]:
|
122
|
+
func_line_num = -1
|
123
|
+
func_indent = ""
|
124
|
+
|
125
|
+
for i, line in enumerate(lines):
|
126
|
+
line_stripped = line.lstrip()
|
127
|
+
if line_stripped.startswith('def '):
|
128
|
+
# Extract function name
|
129
|
+
potential_name = line_stripped[4:].split('(')[0].strip()
|
130
|
+
if potential_name == func_name:
|
131
|
+
func_line_num = i
|
132
|
+
# Calculate indentation by counting spaces before 'def'
|
133
|
+
func_indent = line[:len(line) - len(line_stripped)]
|
134
|
+
break
|
135
|
+
return (func_line_num, func_indent)
|
136
|
+
|
137
|
+
def _get_docstring_end(self, func_line_num: int, lines: List[str]) -> Tuple[bool, int]:
|
138
|
+
has_docstring = False
|
139
|
+
docstring_end = -1
|
140
|
+
|
141
|
+
if func_line_num + 1 < len(lines) and '"""' in lines[func_line_num + 1]:
|
142
|
+
has_docstring = True
|
143
|
+
# Find the end of the docstring
|
144
|
+
for i in range(func_line_num + 2, len(lines)):
|
145
|
+
if '"""' in lines[i]:
|
146
|
+
docstring_end = i
|
147
|
+
break
|
148
|
+
return (has_docstring, docstring_end)
|
149
|
+
|
150
|
+
def _create_docstrings(self, func_indent: int, all_examples: List[str]) -> List[str]:
|
151
|
+
# Create the new docstring with all examples
|
152
|
+
docstring_lines = []
|
153
|
+
docstring_lines.append(func_indent + ' """')
|
154
|
+
|
155
|
+
# Add all examples with consistent indentation
|
156
|
+
for i, example in enumerate(all_examples):
|
157
|
+
# Split example into lines
|
158
|
+
example_lines = example.split('\n')
|
159
|
+
|
160
|
+
# Add each line with proper indentation
|
161
|
+
for j, line in enumerate(example_lines):
|
162
|
+
docstring_lines.append(func_indent + ' ' + line)
|
163
|
+
|
164
|
+
# Add blank line between examples (not after the last one)
|
165
|
+
if i < len(all_examples) - 1:
|
166
|
+
docstring_lines.append('')
|
167
|
+
|
168
|
+
# Close the docstring
|
169
|
+
docstring_lines.append(func_indent + ' """')
|
170
|
+
return docstring_lines
|
171
|
+
|
172
|
+
def _collect_existing_docstrings(self, func_line_num: int, lines: List[str], has_docstring: bool, docstring_end: int) -> Tuple[List[str], Set[str]]:
|
173
|
+
# Collect all examples (existing)
|
174
|
+
all_examples = []
|
175
|
+
seen_examples = set()
|
176
|
+
|
177
|
+
if has_docstring and docstring_end > 0:
|
178
|
+
# Extract existing examples from docstring
|
179
|
+
existing_examples = self._extract_doctest_examples(lines[func_line_num + 2:docstring_end])
|
180
|
+
|
181
|
+
for call, result in existing_examples:
|
182
|
+
# Create normalized example
|
183
|
+
example = f">>> {call}\n{result}"
|
184
|
+
|
185
|
+
if example not in seen_examples:
|
186
|
+
all_examples.append(example)
|
187
|
+
seen_examples.add(example)
|
188
|
+
|
189
|
+
return (all_examples, seen_examples)
|
190
|
+
|
191
|
+
def _add_new_docstrings(self, doctest_examples: List[str], all_examples: List[str], seen_examples: Set[str]) -> Tuple[List[str], Set[str]]:
|
192
|
+
# Add new examples
|
193
|
+
for example in doctest_examples:
|
194
|
+
# Normalize the example format
|
195
|
+
parts = example.split('\n', 1)
|
196
|
+
if len(parts) == 2:
|
197
|
+
call_line, result = parts
|
198
|
+
call = call_line[4:].strip() # Remove >>> prefix and whitespace
|
199
|
+
result = result.strip()
|
200
|
+
|
201
|
+
# Create normalized example
|
202
|
+
normalized_example = f">>> {call}\n{result}"
|
203
|
+
|
204
|
+
if normalized_example not in seen_examples:
|
205
|
+
all_examples.append(normalized_example)
|
206
|
+
seen_examples.add(normalized_example)
|
207
|
+
|
208
|
+
return (all_examples, seen_examples)
|
@@ -0,0 +1,55 @@
|
|
1
|
+
from typing import List
|
2
|
+
from testgen.tree.node import Node
|
3
|
+
from testgen.tree.tree_utils import apply_operation
|
4
|
+
|
5
|
+
|
6
|
+
class CodeGenerator:
|
7
|
+
|
8
|
+
def __init__(self):
|
9
|
+
self = self
|
10
|
+
|
11
|
+
def generate_code_from_tree(self, func_name: str, root: Node, params: List[str], operation) -> str:
|
12
|
+
def traverse(node, depth, path=[], indent_level=1):
|
13
|
+
base_indent = " " * indent_level
|
14
|
+
|
15
|
+
if depth == len(params):
|
16
|
+
path = path + [node.value]
|
17
|
+
result = operation(self, *path)
|
18
|
+
return f"{base_indent}return {result}\n"
|
19
|
+
|
20
|
+
param = params[depth]
|
21
|
+
|
22
|
+
if isinstance(node.value, bool):
|
23
|
+
path = path + [node.value]
|
24
|
+
|
25
|
+
true_branch = f"{base_indent}if {param} == True:\n"
|
26
|
+
false_branch = f"{base_indent}else:\n"
|
27
|
+
|
28
|
+
true_code = traverse(node.children[0], depth + 1, path, indent_level + 1)
|
29
|
+
false_code = traverse(node.children[1], depth + 1, path, indent_level + 1)
|
30
|
+
|
31
|
+
return f"{true_branch}{true_code}{false_branch}{false_code}"
|
32
|
+
|
33
|
+
function_code = f"def {func_name}_generated_function({', '.join(params)}):\n"
|
34
|
+
body_code = traverse(root, 0)
|
35
|
+
|
36
|
+
return function_code + body_code
|
37
|
+
|
38
|
+
def generate_class(self, class_name):
|
39
|
+
branched_class_name = f"Generated{class_name}"
|
40
|
+
file_path = f"generated_{class_name.lower()}.py"
|
41
|
+
class_file = open(f"{file_path}", "w")
|
42
|
+
class_file.write(f"class {branched_class_name}:\n")
|
43
|
+
return class_file
|
44
|
+
|
45
|
+
"""def generate_all_functions_code(self, class_name, operation):
|
46
|
+
functions = self.inspect_class(class_name)
|
47
|
+
trees = self.build_func_trees(functions)
|
48
|
+
all_functions_code = {}
|
49
|
+
|
50
|
+
for func, root, params in trees:
|
51
|
+
code = self.generate_code_from_tree(root, params, operation)
|
52
|
+
all_functions_code[func.__name__] = code
|
53
|
+
print(f"Generated code for function '{func.__name__}':\n{code}\n")
|
54
|
+
|
55
|
+
return all_functions_code"""
|
@@ -0,0 +1,77 @@
|
|
1
|
+
from testgen.generator.test_generator import TestGenerator
|
2
|
+
from testgen.util.file_utils import get_import_info
|
3
|
+
from testgen.models.generator_context import GeneratorContext
|
4
|
+
|
5
|
+
|
6
|
+
class PyTestGenerator(TestGenerator):
|
7
|
+
def __init__(self, generator_context: GeneratorContext):
|
8
|
+
super().__init__(generator_context)
|
9
|
+
self.test_code = []
|
10
|
+
|
11
|
+
def generate_test_header(self):
|
12
|
+
self.test_code.append("import pytest\n")
|
13
|
+
import_info = get_import_info(self._generator_context.filepath)
|
14
|
+
if self._generator_context.class_name == "" or self._generator_context.class_name is None:
|
15
|
+
if import_info['is_package']:
|
16
|
+
self.test_code.append(f"import {import_info['import_path']} as {self._generator_context.module.__name__}\n")
|
17
|
+
else:
|
18
|
+
self.test_code.append(f"import {import_info['import_path']} as {self._generator_context.module.__name__}\n")
|
19
|
+
else:
|
20
|
+
if import_info['is_package']:
|
21
|
+
self.test_code.append(f"from {import_info['import_path']} import {self._generator_context.class_name}\n")
|
22
|
+
else:
|
23
|
+
self.test_code.append(f"from {import_info['import_path']} import {self._generator_context.class_name}\n")
|
24
|
+
|
25
|
+
def generate_test_function(self, unique_func_name, func_name, cases):
|
26
|
+
self.test_code.append(f"def test_{unique_func_name}():")
|
27
|
+
for inputs, expected in cases:
|
28
|
+
args_str: str = self.generate_args_statement(inputs)
|
29
|
+
expected_str: str = self.generate_expected_statement(expected)
|
30
|
+
is_class: bool = self._generator_context.class_name != "" and self._generator_context.class_name is not None
|
31
|
+
class_or_mod_name: str = self.generate_class_or_mod_name(is_class)
|
32
|
+
result_statement: str = self.generate_result_statement(class_or_mod_name, func_name, inputs)
|
33
|
+
assert_statement: str = self.generate_assert_statement()
|
34
|
+
|
35
|
+
self.test_code.append(args_str)
|
36
|
+
self.test_code.append(expected_str)
|
37
|
+
self.test_code.append(result_statement)
|
38
|
+
self.test_code.append(assert_statement)
|
39
|
+
|
40
|
+
def save_file(self):
|
41
|
+
with open(self._generator_context.output_path, "w") as f:
|
42
|
+
f.write("\n".join(self.test_code))
|
43
|
+
|
44
|
+
|
45
|
+
def generate_class_or_mod_name(self, is_class: bool) -> str:
|
46
|
+
if is_class:
|
47
|
+
return f"{self._generator_context.class_name}()"
|
48
|
+
else:
|
49
|
+
return f"{self._generator_context.module.__name__}"
|
50
|
+
|
51
|
+
@staticmethod
|
52
|
+
def generate_args_statement(inputs) -> str:
|
53
|
+
input_args = ', '.join(map(repr, inputs))
|
54
|
+
if len(inputs) == 1:
|
55
|
+
return f" args = {input_args}"
|
56
|
+
else:
|
57
|
+
return f" args = ({input_args})"
|
58
|
+
|
59
|
+
@staticmethod
|
60
|
+
def generate_expected_statement(expected) -> str:
|
61
|
+
if isinstance(expected, str):
|
62
|
+
return f" expected = '{expected}'"
|
63
|
+
else:
|
64
|
+
return f" expected = {expected}"
|
65
|
+
|
66
|
+
|
67
|
+
@staticmethod
|
68
|
+
def generate_result_statement(class_or_mod_name, func_name, inputs) -> str:
|
69
|
+
if len(inputs) == 1:
|
70
|
+
return f" result = {class_or_mod_name}.{func_name}(args)"
|
71
|
+
else:
|
72
|
+
return f" result = {class_or_mod_name}.{func_name}(*args)"
|
73
|
+
|
74
|
+
@staticmethod
|
75
|
+
def generate_assert_statement() -> str:
|
76
|
+
return f" assert result == expected\n"
|
77
|
+
|
@@ -0,0 +1,26 @@
|
|
1
|
+
from abc import ABC, abstractmethod
|
2
|
+
from testgen.models.generator_context import GeneratorContext
|
3
|
+
|
4
|
+
class TestGenerator(ABC):
|
5
|
+
def __init__(self, generator_context: GeneratorContext):
|
6
|
+
self._generator_context = generator_context
|
7
|
+
|
8
|
+
@property
|
9
|
+
def generator_context(self) -> GeneratorContext:
|
10
|
+
return self._generator_context
|
11
|
+
|
12
|
+
@generator_context.setter
|
13
|
+
def generator_context(self, value: GeneratorContext):
|
14
|
+
self._generator_context = value
|
15
|
+
|
16
|
+
@abstractmethod
|
17
|
+
def generate_test_header(self):
|
18
|
+
pass
|
19
|
+
|
20
|
+
@abstractmethod
|
21
|
+
def generate_test_function(self, unique_func_name, func_name, cases):
|
22
|
+
pass
|
23
|
+
|
24
|
+
@abstractmethod
|
25
|
+
def save_file(self):
|
26
|
+
pass
|
@@ -0,0 +1,84 @@
|
|
1
|
+
from testgen.util.file_utils import get_import_info
|
2
|
+
from testgen.generator.test_generator import TestGenerator
|
3
|
+
from testgen.models.generator_context import GeneratorContext
|
4
|
+
|
5
|
+
AST_STRAT = 1
|
6
|
+
FUZZ_STRAT = 2
|
7
|
+
RANDOM_STRAT = 3
|
8
|
+
|
9
|
+
class UnitTestGenerator(TestGenerator):
|
10
|
+
def __init__(self, generator_context: GeneratorContext):
|
11
|
+
super().__init__(generator_context)
|
12
|
+
self.test_code = []
|
13
|
+
|
14
|
+
def generate_test_header(self):
|
15
|
+
|
16
|
+
import_info = get_import_info(self._generator_context.filepath)
|
17
|
+
|
18
|
+
self.test_code.append("import unittest\n")
|
19
|
+
if self._generator_context.class_name == "" or self._generator_context.class_name is None:
|
20
|
+
if import_info['is_package']:
|
21
|
+
self.test_code.append(f"import {import_info['import_path']} as {self._generator_context.module.__name__}\n")
|
22
|
+
else:
|
23
|
+
self.test_code.append(f"import {import_info['import_path']} as {self._generator_context.module.__name__}\n")
|
24
|
+
else:
|
25
|
+
if import_info['is_package']:
|
26
|
+
self.test_code.append(f"from {import_info['import_path']} import {self._generator_context.class_name}\n")
|
27
|
+
else:
|
28
|
+
self.test_code.append(f"from {import_info['import_path']} import {self._generator_context.class_name}\n")
|
29
|
+
self.test_code.append(f"class Test{self._generator_context.class_name}(unittest.TestCase): \n")
|
30
|
+
|
31
|
+
def generate_test_function(self, unique_func_name, func_name, cases):
|
32
|
+
print(f"Cases: {cases}")
|
33
|
+
self.test_code.append(f" def test_{unique_func_name}(self):")
|
34
|
+
for inputs, expected in cases:
|
35
|
+
args_str: str = self.generate_args_statement(inputs)
|
36
|
+
expected_str: str = self.generate_expected_statement(expected)
|
37
|
+
is_class: bool = self._generator_context.class_name != "" and self._generator_context.class_name is not None
|
38
|
+
class_or_mod_name: str = self.generate_class_or_mod_name(is_class)
|
39
|
+
result_statement: str = self.generate_result_statement(class_or_mod_name, func_name, inputs)
|
40
|
+
assert_statement: str = self.generate_assert_statement()
|
41
|
+
|
42
|
+
|
43
|
+
self.test_code.append(args_str)
|
44
|
+
self.test_code.append(expected_str)
|
45
|
+
self.test_code.append(result_statement)
|
46
|
+
self.test_code.append(assert_statement)
|
47
|
+
|
48
|
+
def save_file(self):
|
49
|
+
with open(self._generator_context.output_path, "w") as f:
|
50
|
+
f.write("\n".join(self.test_code))
|
51
|
+
|
52
|
+
def generate_class_or_mod_name(self, is_class: bool) -> str:
|
53
|
+
if is_class:
|
54
|
+
return f"{self._generator_context.class_name}()"
|
55
|
+
else:
|
56
|
+
return f"{self._generator_context.module.__name__}"
|
57
|
+
|
58
|
+
@staticmethod
|
59
|
+
def generate_args_statement(inputs) -> str:
|
60
|
+
input_args = ', '.join(map(repr, inputs))
|
61
|
+
if len(inputs) == 1:
|
62
|
+
return f" args = {input_args}"
|
63
|
+
else:
|
64
|
+
return f" args = ({input_args})"
|
65
|
+
|
66
|
+
@staticmethod
|
67
|
+
def generate_expected_statement(expected) -> str:
|
68
|
+
if isinstance(expected, str):
|
69
|
+
return f" expected = '{expected}'"
|
70
|
+
else:
|
71
|
+
return f" expected = {expected}"
|
72
|
+
|
73
|
+
|
74
|
+
@staticmethod
|
75
|
+
def generate_result_statement(class_or_mod_name, func_name, inputs) -> str:
|
76
|
+
if len(inputs) == 1:
|
77
|
+
return f" result = {class_or_mod_name}.{func_name}(args)"
|
78
|
+
else:
|
79
|
+
return f" result = {class_or_mod_name}.{func_name}(*args)"
|
80
|
+
|
81
|
+
@staticmethod
|
82
|
+
def generate_assert_statement() -> str:
|
83
|
+
return f" self.assertEqual(result, expected)\n"
|
84
|
+
|
File without changes
|
@@ -0,0 +1,61 @@
|
|
1
|
+
from ast import FunctionType
|
2
|
+
import inspect
|
3
|
+
from testgen.code_to_test import sample_code_bin
|
4
|
+
|
5
|
+
|
6
|
+
class Inspector:
|
7
|
+
|
8
|
+
@staticmethod
|
9
|
+
def get_functions(file):
|
10
|
+
return inspect.getmembers(file, inspect.isfunction)
|
11
|
+
|
12
|
+
@staticmethod
|
13
|
+
def get_signature(func: FunctionType):
|
14
|
+
return inspect.signature(func)
|
15
|
+
|
16
|
+
@staticmethod
|
17
|
+
def get_code(func: FunctionType):
|
18
|
+
return inspect.getsource(func)
|
19
|
+
|
20
|
+
@staticmethod
|
21
|
+
def get_params(sig: inspect.Signature):
|
22
|
+
return sig.parameters
|
23
|
+
|
24
|
+
@staticmethod
|
25
|
+
def get_params_not_self(sig: inspect.Signature):
|
26
|
+
params = sig.parameters
|
27
|
+
return [param for param, value in params.items() if param != 'self']
|
28
|
+
|
29
|
+
def func_inspect(self) -> list[tuple]:
|
30
|
+
test_cases: list[tuple] = []
|
31
|
+
|
32
|
+
functions = inspect.getmembers(sample_code_bin, inspect.isfunction)
|
33
|
+
|
34
|
+
for name, func in functions:
|
35
|
+
print(f"Function Name: {name}")
|
36
|
+
signature = inspect.signature(func)
|
37
|
+
print(f"Signature: {signature}")
|
38
|
+
for param in signature.parameters:
|
39
|
+
print(f"Param: {param}")
|
40
|
+
print(f"Function Code: {inspect.getsource(func)}")
|
41
|
+
|
42
|
+
docstring: str = inspect.getdoc(func)
|
43
|
+
print(f"Docstring: {docstring}")
|
44
|
+
|
45
|
+
cases: list[str] = docstring.split(",")
|
46
|
+
|
47
|
+
for case in cases:
|
48
|
+
io: list[str] = case.split("-")
|
49
|
+
input: str = io[0][7:].strip()
|
50
|
+
output: str = io[1][8:].strip()
|
51
|
+
print(f"Input: {input}")
|
52
|
+
print(f"Output: {output}")
|
53
|
+
test_cases.append((name, (input, output)))
|
54
|
+
|
55
|
+
return test_cases
|
56
|
+
|
57
|
+
|
58
|
+
if __name__ == "__main__":
|
59
|
+
inspector = Inspector()
|
60
|
+
test_cases = inspector.func_inspect()
|
61
|
+
print(f"Collected Test Cases: {test_cases}")
|
testgen/main.py
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
from testgen.service.service import Service
|
2
|
+
from testgen.controller.cli_controller import CLIController
|
3
|
+
from testgen.generator.unit_test_generator import UnitTestGenerator
|
4
|
+
from testgen.presentation.cli_view import CLIView
|
5
|
+
|
6
|
+
def main():
|
7
|
+
service = Service()
|
8
|
+
view = CLIView()
|
9
|
+
controller = CLIController(service, view)
|
10
|
+
controller.run()
|
11
|
+
|
12
|
+
if __name__ == '__main__':
|
13
|
+
main()
|
File without changes
|
@@ -0,0 +1,56 @@
|
|
1
|
+
from types import ModuleType
|
2
|
+
from typing import List
|
3
|
+
|
4
|
+
from testgen.models.function_metadata import FunctionMetadata
|
5
|
+
|
6
|
+
|
7
|
+
class AnalysisContext:
|
8
|
+
def __init__(self, filepath: str, filename: str, class_name: str, module: ModuleType, function_data: List[FunctionMetadata] = None):
|
9
|
+
self._filepath: str = filepath
|
10
|
+
self._filename: str = filename
|
11
|
+
self._class_name: str = class_name
|
12
|
+
self._module: ModuleType = module
|
13
|
+
self._function_data: List[FunctionMetadata] = function_data
|
14
|
+
|
15
|
+
@property
|
16
|
+
def filepath(self) -> str:
|
17
|
+
return self._filepath
|
18
|
+
|
19
|
+
@filepath.setter
|
20
|
+
def filepath(self, value: str):
|
21
|
+
self._filepath = value
|
22
|
+
|
23
|
+
@property
|
24
|
+
def filename(self) -> str:
|
25
|
+
return self._filename
|
26
|
+
|
27
|
+
@filename.setter
|
28
|
+
def filename(self, value: str):
|
29
|
+
self._filename = value
|
30
|
+
|
31
|
+
@property
|
32
|
+
def class_name(self) -> str:
|
33
|
+
return self._class_name
|
34
|
+
|
35
|
+
@class_name.setter
|
36
|
+
def class_name(self, value: str):
|
37
|
+
self._class_name = value
|
38
|
+
|
39
|
+
@property
|
40
|
+
def module(self) -> ModuleType:
|
41
|
+
return self._module
|
42
|
+
|
43
|
+
@module.setter
|
44
|
+
def module(self, value: ModuleType):
|
45
|
+
self._module = value
|
46
|
+
|
47
|
+
@property
|
48
|
+
def function_data(self) -> List[FunctionMetadata]:
|
49
|
+
return self._function_data
|
50
|
+
|
51
|
+
@function_data.setter
|
52
|
+
def function_data(self, value: List[FunctionMetadata]):
|
53
|
+
self._function_data = value
|
54
|
+
|
55
|
+
|
56
|
+
|
@@ -0,0 +1,61 @@
|
|
1
|
+
import ast
|
2
|
+
from types import ModuleType
|
3
|
+
|
4
|
+
|
5
|
+
class FunctionMetadata:
|
6
|
+
def __init__(self, filename: str, module: ModuleType, class_name: str, function_name: str, func_def: ast.FunctionDef, params: dict):
|
7
|
+
self._filename: str = filename
|
8
|
+
self._module: ModuleType = module
|
9
|
+
self._class_name: str = class_name
|
10
|
+
self._function_name: str = function_name
|
11
|
+
self._func_def: ast.FunctionDef = func_def
|
12
|
+
self._params: dict = params
|
13
|
+
|
14
|
+
@property
|
15
|
+
def filename(self) -> str:
|
16
|
+
return self._filename
|
17
|
+
|
18
|
+
@filename.setter
|
19
|
+
def filename(self, filename: str):
|
20
|
+
self._filename = filename
|
21
|
+
|
22
|
+
@property
|
23
|
+
def module(self) -> ModuleType:
|
24
|
+
return self._module
|
25
|
+
|
26
|
+
@module.setter
|
27
|
+
def module(self, module: ModuleType):
|
28
|
+
self._module = module
|
29
|
+
|
30
|
+
@property
|
31
|
+
def class_name(self) -> str:
|
32
|
+
return self._class_name
|
33
|
+
|
34
|
+
@class_name.setter
|
35
|
+
def class_name(self, class_name: str):
|
36
|
+
self._class_name = class_name
|
37
|
+
|
38
|
+
@property
|
39
|
+
def function_name(self) -> str:
|
40
|
+
return self._function_name
|
41
|
+
|
42
|
+
@function_name.setter
|
43
|
+
def function_name(self, func_name: str):
|
44
|
+
self._function_name = func_name
|
45
|
+
|
46
|
+
@property
|
47
|
+
def func_def(self) -> ast.FunctionDef:
|
48
|
+
return self._func_def
|
49
|
+
|
50
|
+
@func_def.setter
|
51
|
+
def func_def(self, func_def: ast.FunctionDef):
|
52
|
+
self._func_def = func_def
|
53
|
+
|
54
|
+
@property
|
55
|
+
def params(self) -> dict:
|
56
|
+
return self._params
|
57
|
+
|
58
|
+
@params.setter
|
59
|
+
def params(self, params: dict):
|
60
|
+
self._params = params
|
61
|
+
|