testgenie-py 0.1.5__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.
Files changed (34) hide show
  1. testgen/.coverage +0 -0
  2. testgen/analyzer/random_feedback_analyzer.py +3 -3
  3. testgen/code_to_test/__init__.py +0 -0
  4. testgen/code_to_test/boolean.py +146 -0
  5. testgen/code_to_test/calculator.py +29 -0
  6. testgen/code_to_test/code_to_fuzz.py +234 -0
  7. testgen/code_to_test/code_to_fuzz_lite.py +397 -0
  8. testgen/code_to_test/decisions.py +57 -0
  9. testgen/code_to_test/math_utils.py +47 -0
  10. testgen/code_to_test/no_types.py +35 -0
  11. testgen/code_to_test/sample_code_bin.py +141 -0
  12. testgen/controller/cli_controller.py +119 -99
  13. testgen/controller/docker_controller.py +34 -19
  14. testgen/docker/Dockerfile +2 -2
  15. testgen/docker/poetry.lock +246 -8
  16. testgen/docker/pyproject.toml +9 -2
  17. testgen/q_table/global_q_table.json +1 -1
  18. testgen/reinforcement/environment.py +42 -10
  19. testgen/reinforcement/statement_coverage_state.py +1 -1
  20. testgen/service/analysis_service.py +8 -2
  21. testgen/service/cfg_service.py +1 -1
  22. testgen/service/generator_service.py +11 -3
  23. testgen/service/logging_service.py +100 -0
  24. testgen/service/service.py +81 -41
  25. testgen/testgen.db +0 -0
  26. testgen/util/coverage_utils.py +41 -14
  27. testgen/util/coverage_visualizer.py +2 -2
  28. testgen/util/file_utils.py +46 -0
  29. testgen/util/randomizer.py +27 -12
  30. testgen/util/z3_utils/z3_test_case.py +26 -11
  31. {testgenie_py-0.1.5.dist-info → testgenie_py-0.1.7.dist-info}/METADATA +2 -1
  32. {testgenie_py-0.1.5.dist-info → testgenie_py-0.1.7.dist-info}/RECORD +34 -22
  33. {testgenie_py-0.1.5.dist-info → testgenie_py-0.1.7.dist-info}/WHEEL +0 -0
  34. {testgenie_py-0.1.5.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
- args = parser.parse_args()
80
-
81
- if args.select_all:
82
- self.view.display_message("Selecting all from SQLite database...")
83
- # Assuming you have a method in your service to handle this
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.view.display_message("Running in local mode...")
109
- self.execute_generation(args)
176
+ self.service.set_test_generator_format(UNITTEST_FORMAT)
110
177
 
111
- def execute_generation(self, args: argparse.Namespace):
112
- try:
113
- self.service.set_file_path(args.file_path)
114
- if args.test_format == "pytest":
115
- self.service.set_test_generator_format(PYTEST_FORMAT)
116
- elif args.test_format == "doctest":
117
- self.service.set_test_generator_format(DOCTEST_FORMAT)
118
- else:
119
- self.service.set_test_generator_format(UNITTEST_FORMAT)
120
- if args.test_mode == "random":
121
- self.view.display_message("Using Random Feedback-Directed Test Generation Strategy.")
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("Generating function code using AST analysis...")
136
- generated_file_path = self.service.generate_function_code()
137
- self.view.display_message(f"Generated code saved to: {generated_file_path}")
138
- if not args.generate_only:
139
- self.view.display_message("Using Simple AST Traversal Test Generation Strategy...")
140
- self.service.set_test_analysis_strategy(AST_STRAT)
141
-
142
- test_file = self.service.generate_tests(args.output)
143
- self.view.display_message(f"Unit tests saved to: {test_file}")
144
- self.view.display_message("Running coverage...")
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
- print("Docker image not found. Running locally...")
37
+ self.logger.info("Docker image not found. Running locally...")
34
38
  return False
35
39
 
36
- docker_args = [os.path.basename(args.file_path)] + [arg for arg in sys.argv[2:] if arg != "--safe"]
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
- print(logs_output)
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
- print(f"SERVICE target path after logs: {target_path}")
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
- print(f"Filepath in CLI CONTROLLER: {file_path}")
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
- print(f"Using Dockerfile at: {dockerfile_path}")
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"}}, # Mount current dir
126
- environment={"RUNNING_IN_DOCKER": "1"},
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
- @staticmethod
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
- print(f"Project root {project_root}")
139
- print(f"Docker directory: {os.path.dirname(dockerfile_path)}")
140
- print(f"Docker rel path: {dockerfile_rel_path}")
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
- print(f"CHUNK: {chunk}")
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
- print(f"Docker build error: {chunk['error']}")
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 /app
12
+ WORKDIR /controller
13
13
 
14
14
  # Copy poetry files
15
15
  COPY . .
16
16
 
17
- ENV PYTHONPATH=/app:/app/testgen
17
+ ENV PYTHONPATH=/controller:/controller/testgen
18
18
 
19
19
  RUN poetry install --no-root
20
20