testgenie-py 0.1.6__py3-none-any.whl → 0.1.7__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/.coverage +0 -0
- testgen/analyzer/random_feedback_analyzer.py +3 -3
- testgen/code_to_test/__init__.py +0 -0
- testgen/code_to_test/boolean.py +146 -0
- testgen/code_to_test/calculator.py +29 -0
- testgen/code_to_test/code_to_fuzz.py +234 -0
- testgen/code_to_test/code_to_fuzz_lite.py +397 -0
- testgen/code_to_test/decisions.py +57 -0
- testgen/code_to_test/math_utils.py +47 -0
- testgen/code_to_test/no_types.py +35 -0
- testgen/code_to_test/sample_code_bin.py +141 -0
- testgen/controller/cli_controller.py +119 -99
- testgen/controller/docker_controller.py +34 -19
- testgen/docker/Dockerfile +2 -2
- testgen/docker/poetry.lock +246 -8
- testgen/docker/pyproject.toml +9 -2
- testgen/q_table/global_q_table.json +1 -1
- testgen/reinforcement/environment.py +42 -10
- testgen/reinforcement/statement_coverage_state.py +1 -1
- testgen/service/analysis_service.py +8 -2
- testgen/service/cfg_service.py +1 -1
- testgen/service/generator_service.py +11 -3
- testgen/service/logging_service.py +100 -0
- testgen/service/service.py +81 -41
- testgen/testgen.db +0 -0
- testgen/util/coverage_utils.py +41 -14
- testgen/util/coverage_visualizer.py +2 -2
- testgen/util/file_utils.py +46 -0
- testgen/util/randomizer.py +27 -12
- testgen/util/z3_utils/z3_test_case.py +26 -11
- {testgenie_py-0.1.6.dist-info → testgenie_py-0.1.7.dist-info}/METADATA +1 -1
- {testgenie_py-0.1.6.dist-info → testgenie_py-0.1.7.dist-info}/RECORD +34 -22
- {testgenie_py-0.1.6.dist-info → testgenie_py-0.1.7.dist-info}/WHEEL +0 -0
- {testgenie_py-0.1.6.dist-info → testgenie_py-0.1.7.dist-info}/entry_points.txt +0 -0
@@ -6,7 +6,8 @@ import sys
|
|
6
6
|
import docker
|
7
7
|
from docker import DockerClient
|
8
8
|
from docker import errors
|
9
|
-
|
9
|
+
from testgen.service.logging_service import LoggingService, get_logger
|
10
|
+
from testgen.util.file_utils import adjust_file_path_for_docker, get_project_root_in_docker
|
10
11
|
from testgen.controller.docker_controller import DockerController
|
11
12
|
from testgen.service.service import Service
|
12
13
|
from testgen.presentation.cli_view import CLIView
|
@@ -28,6 +29,85 @@ class CLIController:
|
|
28
29
|
self.view = view
|
29
30
|
|
30
31
|
def run(self):
|
32
|
+
|
33
|
+
parser = self.add_arguments()
|
34
|
+
|
35
|
+
args = parser.parse_args()
|
36
|
+
|
37
|
+
LoggingService.get_instance().initialize(
|
38
|
+
debug_mode=args.debug if hasattr(args, 'debug') else False,
|
39
|
+
log_file=args.log_file if hasattr(args, 'log_file') else None,
|
40
|
+
console_output=True
|
41
|
+
)
|
42
|
+
|
43
|
+
logger = get_logger()
|
44
|
+
|
45
|
+
if args.select_all:
|
46
|
+
self.view.display_message("Selecting all from SQLite database...")
|
47
|
+
# Assuming you have a method in your service to handle this
|
48
|
+
self.service.select_all_from_db()
|
49
|
+
return
|
50
|
+
|
51
|
+
running_in_docker = os.environ.get("RUNNING_IN_DOCKER") is not None
|
52
|
+
if running_in_docker:
|
53
|
+
args.file_path = adjust_file_path_for_docker(args.file_path)
|
54
|
+
self.execute_generation(args, True)
|
55
|
+
elif args.safe and not running_in_docker:
|
56
|
+
client = self.docker_available()
|
57
|
+
# Skip Docker-dependent operations if client is None
|
58
|
+
if client is None and args.safe:
|
59
|
+
self.view.display_message("Running with --safe flag requires Docker. Continuing without safe mode.")
|
60
|
+
args.safe = False
|
61
|
+
self.execute_generation(args)
|
62
|
+
else:
|
63
|
+
docker_controller = DockerController()
|
64
|
+
project_root = get_project_root_in_docker(args.file_path)
|
65
|
+
successful: bool = docker_controller.run_in_docker(project_root, client, args)
|
66
|
+
if not successful:
|
67
|
+
if hasattr(args, 'db') and args.db:
|
68
|
+
self.service.db_service = DBService(args.db)
|
69
|
+
self.view.display_message(f"Using database: {args.db}")
|
70
|
+
self.execute_generation(args)
|
71
|
+
# Else successful, do nothing - we're done
|
72
|
+
else:
|
73
|
+
# Initialize database service with specified path
|
74
|
+
if hasattr(args, 'db') and args.db:
|
75
|
+
self.service.db_service = DBService(args.db)
|
76
|
+
self.view.display_message(f"Using database: {args.db}")
|
77
|
+
self.view.display_message("Running in local mode...")
|
78
|
+
self.execute_generation(args)
|
79
|
+
|
80
|
+
def execute_generation(self, args: argparse.Namespace, running_in_docker: bool = False):
|
81
|
+
try:
|
82
|
+
self.set_service_args(args)
|
83
|
+
|
84
|
+
if running_in_docker:
|
85
|
+
self.view.display_message("Running in Docker mode...")
|
86
|
+
self.service.generate_test_cases()
|
87
|
+
|
88
|
+
else:
|
89
|
+
test_file = self.service.generate_tests(args.output)
|
90
|
+
self.view.display_message(f"Unit tests saved to: {test_file}")
|
91
|
+
self.view.display_message("Running coverage...")
|
92
|
+
self.service.run_coverage(test_file)
|
93
|
+
self.view.display_message("Tests and coverage data saved to database.")
|
94
|
+
|
95
|
+
if args.visualize:
|
96
|
+
self.service.visualize_test_coverage()
|
97
|
+
|
98
|
+
except Exception as e:
|
99
|
+
self.view.display_error(f"An error occurred: {e}")
|
100
|
+
# Make sure to close the DB connection on error
|
101
|
+
if hasattr(self.service, 'db_service'):
|
102
|
+
self.service.db_service.close()
|
103
|
+
|
104
|
+
def set_service_args(self, args: argparse.Namespace):
|
105
|
+
self.service.set_file_path(args.file_path)
|
106
|
+
self.service.set_debug_mode(args.debug)
|
107
|
+
self.set_test_format(args)
|
108
|
+
self.set_test_strategy(args)
|
109
|
+
|
110
|
+
def add_arguments(self) -> argparse.ArgumentParser:
|
31
111
|
parser = argparse.ArgumentParser(description="A CLI tool for generating unit tests.")
|
32
112
|
parser.add_argument("file_path", type=str, help="Path to the Python file.")
|
33
113
|
parser.add_argument("--output", "-o", type=str, help="Path to output directory.")
|
@@ -75,108 +155,48 @@ class CLIController:
|
|
75
155
|
action="store_true",
|
76
156
|
help = "Visualize the tests with graphviz"
|
77
157
|
)
|
158
|
+
parser.add_argument(
|
159
|
+
"--debug",
|
160
|
+
action="store_true",
|
161
|
+
help="Enable debug logging"
|
162
|
+
)
|
163
|
+
parser.add_argument(
|
164
|
+
"--log-file",
|
165
|
+
type=str,
|
166
|
+
help="Path to log file (if not specified, logs will only go to console)"
|
167
|
+
)
|
168
|
+
return parser
|
78
169
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
self.service.select_all_from_db()
|
85
|
-
return
|
86
|
-
|
87
|
-
# Initialize database service with specified path
|
88
|
-
if hasattr(args, 'db') and args.db:
|
89
|
-
self.service.db_service = DBService(args.db)
|
90
|
-
self.view.display_message(f"Using database: {args.db}")
|
91
|
-
|
92
|
-
running_in_docker = os.environ.get("RUNNING_IN_DOCKER") is not None
|
93
|
-
if running_in_docker:
|
94
|
-
args.file_path = self.adjust_file_path_for_docker(args.file_path)
|
95
|
-
self.execute_generation(args)
|
96
|
-
elif args.safe and not running_in_docker:
|
97
|
-
client = self.docker_available()
|
98
|
-
# Skip Docker-dependent operations if client is None
|
99
|
-
if client is None and args.safe:
|
100
|
-
self.view.display_message("Running with --safe flag requires Docker. Continuing without safe mode.")
|
101
|
-
args.safe = False
|
102
|
-
docker_controller = DockerController()
|
103
|
-
project_root = self.get_project_root_in_docker(args.file_path)
|
104
|
-
successful: bool = docker_controller.run_in_docker(project_root, client, args)
|
105
|
-
if not successful:
|
106
|
-
self.execute_generation(args)
|
170
|
+
def set_test_format(self, args: argparse.Namespace):
|
171
|
+
if args.test_format == "pytest":
|
172
|
+
self.service.set_test_generator_format(PYTEST_FORMAT)
|
173
|
+
elif args.test_format == "doctest":
|
174
|
+
self.service.set_test_generator_format(DOCTEST_FORMAT)
|
107
175
|
else:
|
108
|
-
self.
|
109
|
-
self.execute_generation(args)
|
176
|
+
self.service.set_test_generator_format(UNITTEST_FORMAT)
|
110
177
|
|
111
|
-
def
|
112
|
-
|
113
|
-
self.
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
if args.
|
121
|
-
self.view.display_message("
|
122
|
-
self.service.set_test_analysis_strategy(RANDOM_STRAT)
|
123
|
-
elif args.test_mode == "fuzz":
|
124
|
-
self.view.display_message("Using Fuzz Test Generation Strategy...")
|
125
|
-
self.service.set_test_analysis_strategy(FUZZ_STRAT)
|
126
|
-
elif args.test_mode == "reinforce":
|
127
|
-
self.view.display_message("Using Reinforcement Learning Test Generation Strategy...")
|
128
|
-
if args.reinforce_mode == "train":
|
129
|
-
self.view.display_message("Training mode enabled - will update Q-table")
|
130
|
-
else:
|
131
|
-
self.view.display_message("Training mode disabled - will use existing Q-table")
|
132
|
-
self.service.set_test_analysis_strategy(REINFORCE_STRAT)
|
133
|
-
self.service.set_reinforcement_mode(args.reinforce_mode)
|
178
|
+
def set_test_strategy(self, args: argparse.Namespace):
|
179
|
+
if args.test_mode == "random":
|
180
|
+
self.view.display_message("Using Random Feedback-Directed Test Generation Strategy.")
|
181
|
+
self.service.set_test_analysis_strategy(RANDOM_STRAT)
|
182
|
+
elif args.test_mode == "fuzz":
|
183
|
+
self.view.display_message("Using Fuzz Test Generation Strategy...")
|
184
|
+
self.service.set_test_analysis_strategy(FUZZ_STRAT)
|
185
|
+
elif args.test_mode == "reinforce":
|
186
|
+
self.view.display_message("Using Reinforcement Learning Test Generation Strategy...")
|
187
|
+
if args.reinforce_mode == "train":
|
188
|
+
self.view.display_message("Training mode enabled - will update Q-table")
|
134
189
|
else:
|
135
|
-
self.view.display_message("
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
self.service.run_coverage(test_file)
|
146
|
-
self.view.display_message("Tests and coverage data saved to database.")
|
147
|
-
|
148
|
-
if args.visualize:
|
149
|
-
self.service.visualize_test_coverage()
|
150
|
-
|
151
|
-
except Exception as e:
|
152
|
-
self.view.display_error(f"An error occurred: {e}")
|
153
|
-
# Make sure to close the DB connection on error
|
154
|
-
if hasattr(self.service, 'db_service'):
|
155
|
-
self.service.db_service.close()
|
156
|
-
|
157
|
-
def adjust_file_path_for_docker(self, file_path) -> str:
|
158
|
-
file_dir = os.path.abspath(os.path.dirname(file_path))
|
159
|
-
sys.path.append(file_dir)
|
160
|
-
sys.path.append('/controller')
|
161
|
-
file_abs_path = os.path.abspath(file_path)
|
162
|
-
if not os.path.exists(file_abs_path):
|
163
|
-
testgen_path = os.path.join('/controller/testgen', os.path.basename(file_path))
|
164
|
-
if os.path.exists(testgen_path):
|
165
|
-
file_path = testgen_path
|
166
|
-
else:
|
167
|
-
app_path = os.path.join('/controller', os.path.basename(file_path))
|
168
|
-
if os.path.exists(app_path):
|
169
|
-
file_path = app_path
|
170
|
-
return file_path
|
171
|
-
|
172
|
-
def get_project_root_in_docker(self, script_path) -> str:
|
173
|
-
script_path = os.path.abspath(sys.argv[0])
|
174
|
-
print(f"Script path: {script_path}")
|
175
|
-
script_dir = os.path.dirname(script_path)
|
176
|
-
print(f"Script directory: {script_dir}")
|
177
|
-
project_root = os.path.dirname(script_dir)
|
178
|
-
print(f"Project root directory: {project_root}")
|
179
|
-
return project_root
|
190
|
+
self.view.display_message("Training mode disabled - will use existing Q-table")
|
191
|
+
self.service.set_test_analysis_strategy(REINFORCE_STRAT)
|
192
|
+
self.service.set_reinforcement_mode(args.reinforce_mode)
|
193
|
+
else:
|
194
|
+
self.view.display_message("Generating function code using AST analysis...")
|
195
|
+
generated_file_path = self.service.generate_function_code()
|
196
|
+
self.view.display_message(f"Generated code saved to: {generated_file_path}")
|
197
|
+
if not args.generate_only:
|
198
|
+
self.view.display_message("Using Simple AST Traversal Test Generation Strategy...")
|
199
|
+
self.service.set_test_analysis_strategy(AST_STRAT)
|
180
200
|
|
181
201
|
def docker_available(self) -> DockerClient | None:
|
182
202
|
try:
|
@@ -6,6 +6,7 @@ from docker import DockerClient, client
|
|
6
6
|
from docker import errors
|
7
7
|
from docker.models.containers import Container
|
8
8
|
|
9
|
+
from testgen.service.logging_service import get_logger
|
9
10
|
from testgen.service.service import Service
|
10
11
|
|
11
12
|
AST_STRAT = 1
|
@@ -19,10 +20,13 @@ DOCTEST_FORMAT = 3
|
|
19
20
|
class DockerController:
|
20
21
|
def __init__(self):
|
21
22
|
self.service = Service()
|
23
|
+
self.debug_mode = False
|
22
24
|
self.args = None
|
25
|
+
self.logger = get_logger()
|
23
26
|
|
24
27
|
def run_in_docker(self, project_root: str, docker_client: DockerClient, args: Namespace) -> bool:
|
25
28
|
self.args = args
|
29
|
+
self.debug_mode = True if args.debug else False
|
26
30
|
os.environ["RUNNING_IN_DOCKER"] = "1"
|
27
31
|
|
28
32
|
# Check if Docker image exists, build it if not
|
@@ -30,18 +34,19 @@ class DockerController:
|
|
30
34
|
# If args.safe is set to false it means the image was not found and the system will try to run_locally
|
31
35
|
self.get_image(docker_client, image_name, project_root)
|
32
36
|
if not self.args.safe:
|
33
|
-
|
37
|
+
self.logger.info("Docker image not found. Running locally...")
|
34
38
|
return False
|
35
39
|
|
36
|
-
docker_args = [
|
40
|
+
docker_args = [args.file_path] + [arg for arg in sys.argv[2:] if arg != "--safe"]
|
37
41
|
|
38
42
|
# Run the container with the same arguments
|
39
43
|
try:
|
44
|
+
self.debug(f"project_root: {project_root}")
|
40
45
|
container = self.run_container(docker_client, image_name, docker_args, project_root)
|
41
46
|
|
42
47
|
# Stream the logs to the console
|
43
48
|
logs_output = self.get_logs(container)
|
44
|
-
|
49
|
+
self.debug(logs_output)
|
45
50
|
|
46
51
|
try:
|
47
52
|
# Create the target directory if it doesn't exist
|
@@ -51,14 +56,14 @@ class DockerController:
|
|
51
56
|
target_path = args.output
|
52
57
|
os.makedirs(target_path, exist_ok=True)
|
53
58
|
|
54
|
-
|
59
|
+
self.debug(f"SERVICE target path after logs: {target_path}")
|
55
60
|
|
56
61
|
test_cases = self.service.parse_test_cases_from_logs(logs_output)
|
57
62
|
|
58
63
|
print(f"Extracted {len(test_cases)} test cases from container.")
|
59
64
|
|
60
65
|
file_path = os.path.abspath(args.file_path)
|
61
|
-
|
66
|
+
self.debug(f"Filepath in CLI CONTROLLER: {file_path}")
|
62
67
|
self.service.set_file_path(file_path)
|
63
68
|
|
64
69
|
if args.test_format == "pytest":
|
@@ -73,9 +78,10 @@ class DockerController:
|
|
73
78
|
|
74
79
|
if not args.generate_only:
|
75
80
|
print("Running coverage...")
|
76
|
-
import traceback
|
77
|
-
print(traceback.format_exc())
|
78
81
|
self.service.run_coverage(test_file)
|
82
|
+
|
83
|
+
# Add explicit return True here
|
84
|
+
return True
|
79
85
|
|
80
86
|
except Exception as e:
|
81
87
|
print(f"Error running container: {e}")
|
@@ -99,7 +105,7 @@ class DockerController:
|
|
99
105
|
print(f"Dockerfile not found at {dockerfile_path}")
|
100
106
|
sys.exit(1)
|
101
107
|
|
102
|
-
|
108
|
+
self.debug(f"Using Dockerfile at: {dockerfile_path}")
|
103
109
|
|
104
110
|
if not self.build_docker_image(docker_client, image_name, dockerfile_path, project_root):
|
105
111
|
print("Failed to build Docker image. Continuing without safe mode.")
|
@@ -108,7 +114,6 @@ class DockerController:
|
|
108
114
|
@staticmethod
|
109
115
|
def get_logs(container) -> str:
|
110
116
|
# Stream the logs to the console
|
111
|
-
print("Running in Docker container...")
|
112
117
|
logs = container.logs(stream=True)
|
113
118
|
logs_output = ""
|
114
119
|
for log in logs:
|
@@ -119,25 +124,32 @@ class DockerController:
|
|
119
124
|
|
120
125
|
@staticmethod
|
121
126
|
def run_container(docker_client: DockerClient, image_name: str, docker_args: list, project_root: str) -> Container:
|
127
|
+
# Create Docker-specific environment variables
|
128
|
+
docker_env = {
|
129
|
+
"RUNNING_IN_DOCKER": "1",
|
130
|
+
"PYTHONPATH": "/controller",
|
131
|
+
"COVERAGE_FILE": "/tmp/.coverage", # Move coverage file to /tmp
|
132
|
+
"DB_PATH": "/tmp/testgen.db" # Move DB to /tmp
|
133
|
+
}
|
134
|
+
|
122
135
|
return docker_client.containers.run(
|
123
136
|
image=image_name,
|
124
137
|
command=["poetry", "run", "python", "-m", "testgen.main"] + docker_args,
|
125
|
-
volumes={project_root: {"bind": "/controller", "mode": "rw"}},
|
126
|
-
environment=
|
138
|
+
volumes={project_root: {"bind": "/controller", "mode": "rw"}},
|
139
|
+
environment=docker_env,
|
127
140
|
detach=True,
|
128
141
|
remove=True,
|
129
142
|
stdout=True,
|
130
143
|
stderr=True
|
131
144
|
)
|
132
145
|
|
133
|
-
|
134
|
-
def build_docker_image(docker_client, image_name, dockerfile_path, project_root):
|
146
|
+
def build_docker_image(self, docker_client, image_name, dockerfile_path, project_root):
|
135
147
|
try:
|
136
148
|
print(f"Starting Docker build for image: {image_name}")
|
137
149
|
dockerfile_rel_path = os.path.relpath(dockerfile_path, project_root)
|
138
|
-
|
139
|
-
|
140
|
-
|
150
|
+
self.debug(f"Project root {project_root}")
|
151
|
+
self.debug(f"Docker directory: {os.path.dirname(dockerfile_path)}")
|
152
|
+
self.debug(f"Docker rel path: {dockerfile_rel_path}")
|
141
153
|
build_progress = docker_client.api.build(
|
142
154
|
path=os.path.join(project_root, "testgen", "docker"),
|
143
155
|
dockerfile=os.path.join(project_root, "testgen", "docker", "Dockerfile"),
|
@@ -147,13 +159,13 @@ class DockerController:
|
|
147
159
|
)
|
148
160
|
|
149
161
|
for chunk in build_progress:
|
150
|
-
|
162
|
+
self.debug(f"CHUNK: {chunk}")
|
151
163
|
if 'stream' in chunk:
|
152
164
|
for line in chunk['stream'].splitlines():
|
153
165
|
if line.strip():
|
154
166
|
print(f"Docker: {line.strip()}")
|
155
167
|
elif 'error' in chunk:
|
156
|
-
|
168
|
+
self.debug(f"Docker build error: {chunk['error']}")
|
157
169
|
return False
|
158
170
|
print(f"Docker image built successfully: {image_name}")
|
159
171
|
return True
|
@@ -166,4 +178,7 @@ class DockerController:
|
|
166
178
|
print(f"Unexpected error during Docker build: {str(e)}")
|
167
179
|
return False
|
168
180
|
|
169
|
-
|
181
|
+
def debug(self, message: str):
|
182
|
+
"""Log debug message"""
|
183
|
+
if self.debug_mode:
|
184
|
+
self.logger.debug(message)
|
testgen/docker/Dockerfile
CHANGED
@@ -9,12 +9,12 @@ ENV POETRY_VIRTUALENVS_CREATE=false \
|
|
9
9
|
PYTHONUNBUFFERED=1 \
|
10
10
|
RUNNING_IN_DOCKER=true
|
11
11
|
|
12
|
-
WORKDIR /
|
12
|
+
WORKDIR /controller
|
13
13
|
|
14
14
|
# Copy poetry files
|
15
15
|
COPY . .
|
16
16
|
|
17
|
-
ENV PYTHONPATH=/
|
17
|
+
ENV PYTHONPATH=/controller:/controller/testgen
|
18
18
|
|
19
19
|
RUN poetry install --no-root
|
20
20
|
|