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.
Files changed (68) hide show
  1. testgen/__init__.py +0 -0
  2. testgen/analyzer/__init__.py +0 -0
  3. testgen/analyzer/ast_analyzer.py +149 -0
  4. testgen/analyzer/contracts/__init__.py +0 -0
  5. testgen/analyzer/contracts/contract.py +13 -0
  6. testgen/analyzer/contracts/no_exception_contract.py +16 -0
  7. testgen/analyzer/contracts/nonnull_contract.py +15 -0
  8. testgen/analyzer/fuzz_analyzer.py +106 -0
  9. testgen/analyzer/random_feedback_analyzer.py +291 -0
  10. testgen/analyzer/reinforcement_analyzer.py +75 -0
  11. testgen/analyzer/test_case_analyzer.py +46 -0
  12. testgen/analyzer/test_case_analyzer_context.py +58 -0
  13. testgen/controller/__init__.py +0 -0
  14. testgen/controller/cli_controller.py +194 -0
  15. testgen/controller/docker_controller.py +169 -0
  16. testgen/docker/Dockerfile +22 -0
  17. testgen/docker/poetry.lock +361 -0
  18. testgen/docker/pyproject.toml +22 -0
  19. testgen/generator/__init__.py +0 -0
  20. testgen/generator/code_generator.py +66 -0
  21. testgen/generator/doctest_generator.py +208 -0
  22. testgen/generator/generator.py +55 -0
  23. testgen/generator/pytest_generator.py +77 -0
  24. testgen/generator/test_generator.py +26 -0
  25. testgen/generator/unit_test_generator.py +84 -0
  26. testgen/inspector/__init__.py +0 -0
  27. testgen/inspector/inspector.py +61 -0
  28. testgen/main.py +13 -0
  29. testgen/models/__init__.py +0 -0
  30. testgen/models/analysis_context.py +56 -0
  31. testgen/models/function_metadata.py +61 -0
  32. testgen/models/generator_context.py +63 -0
  33. testgen/models/test_case.py +8 -0
  34. testgen/presentation/__init__.py +0 -0
  35. testgen/presentation/cli_view.py +12 -0
  36. testgen/q_table/global_q_table.json +1 -0
  37. testgen/reinforcement/__init__.py +0 -0
  38. testgen/reinforcement/abstract_state.py +7 -0
  39. testgen/reinforcement/agent.py +153 -0
  40. testgen/reinforcement/environment.py +215 -0
  41. testgen/reinforcement/statement_coverage_state.py +33 -0
  42. testgen/service/__init__.py +0 -0
  43. testgen/service/analysis_service.py +260 -0
  44. testgen/service/cfg_service.py +55 -0
  45. testgen/service/generator_service.py +169 -0
  46. testgen/service/service.py +389 -0
  47. testgen/sqlite/__init__.py +0 -0
  48. testgen/sqlite/db.py +84 -0
  49. testgen/sqlite/db_service.py +219 -0
  50. testgen/tree/__init__.py +0 -0
  51. testgen/tree/node.py +7 -0
  52. testgen/tree/tree_utils.py +79 -0
  53. testgen/util/__init__.py +0 -0
  54. testgen/util/coverage_utils.py +168 -0
  55. testgen/util/coverage_visualizer.py +154 -0
  56. testgen/util/file_utils.py +110 -0
  57. testgen/util/randomizer.py +122 -0
  58. testgen/util/utils.py +143 -0
  59. testgen/util/z3_utils/__init__.py +0 -0
  60. testgen/util/z3_utils/ast_to_z3.py +99 -0
  61. testgen/util/z3_utils/branch_condition.py +72 -0
  62. testgen/util/z3_utils/constraint_extractor.py +36 -0
  63. testgen/util/z3_utils/variable_finder.py +10 -0
  64. testgen/util/z3_utils/z3_test_case.py +94 -0
  65. testgenie_py-0.1.0.dist-info/METADATA +24 -0
  66. testgenie_py-0.1.0.dist-info/RECORD +68 -0
  67. testgenie_py-0.1.0.dist-info/WHEEL +4 -0
  68. testgenie_py-0.1.0.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,46 @@
1
+ import ast
2
+ from abc import ABC, abstractmethod
3
+ from typing import List, Dict
4
+
5
+ from testgen.models.test_case import TestCase
6
+ from testgen.models.analysis_context import AnalysisContext
7
+ from testgen.models.function_metadata import FunctionMetadata
8
+
9
+ class TestCaseAnalyzerStrategy(ABC):
10
+ def __init__(self, analysis_context: AnalysisContext = None):
11
+ self._analysis_context = analysis_context
12
+
13
+ @abstractmethod
14
+ def collect_test_cases(self, function_metadata: FunctionMetadata) -> List[TestCase]:
15
+ pass
16
+
17
+ def get_function_metadata(self, func_name: str) -> FunctionMetadata | None:
18
+ if self._analysis_context and self._analysis_context.function_data:
19
+ for func_metadata in self._analysis_context.function_data:
20
+ if func_metadata.function_name == func_name:
21
+ return func_metadata
22
+ return None
23
+
24
+ def get_function_metadata_by_node(self, func_node: ast.FunctionDef) -> FunctionMetadata:
25
+ return self.get_function_metadata(func_node.name)
26
+
27
+ def get_param_types(self, func_node: ast.FunctionDef) -> Dict[str, str] | None:
28
+ func_metadata = self.get_function_metadata_by_node(func_node)
29
+ if func_metadata:
30
+ return func_metadata.params
31
+
32
+ return None
33
+
34
+ @property
35
+ def analysis_context(self) -> AnalysisContext:
36
+ return self._analysis_context
37
+
38
+ @analysis_context.setter
39
+ def analysis_context(self, context: AnalysisContext):
40
+ self._analysis_context = context
41
+
42
+
43
+
44
+
45
+
46
+
@@ -0,0 +1,58 @@
1
+ import time
2
+ from typing import List
3
+
4
+ from testgen.analyzer.test_case_analyzer import TestCaseAnalyzerStrategy
5
+ from testgen.models.test_case import TestCase
6
+ from testgen.models.analysis_context import AnalysisContext
7
+
8
+
9
+ class TestCaseAnalyzerContext:
10
+ def __init__(self, analysis_context: AnalysisContext, test_case_analyzer: TestCaseAnalyzerStrategy):
11
+ self._test_case_analyzer = test_case_analyzer
12
+ self._analysis_context = analysis_context
13
+ self._test_cases = []
14
+
15
+ # TODO: GET RID OF THIS STUPID METHOD IT IS POINTLESS
16
+ # JUST CALL INSIDE ANALYZER_SERVICE
17
+ def do_logic(self) -> List[TestCase]:
18
+ """Run the analysis process"""
19
+ self.do_strategy(20)
20
+
21
+ def do_strategy(self, time_limit=None) -> List[TestCase]:
22
+ """Execute the analysis strategy for all functions with an optional time limit"""
23
+ start_time = time.time()
24
+ for func_metadata in self._analysis_context.function_data:
25
+ if time_limit and (time.time() - start_time) >= time_limit:
26
+ break
27
+ test_cases = self._test_case_analyzer.collect_test_cases(func_metadata)
28
+ for test_case in test_cases:
29
+ print(f"Test Case: {test_case.func_name}, {test_case.inputs}, {test_case.expected}")
30
+ self._test_cases.extend(test_cases)
31
+
32
+ @property
33
+ def strategy(self) -> TestCaseAnalyzerStrategy:
34
+ return self._test_case_analyzer
35
+
36
+ @property
37
+ def test_cases(self):
38
+ return self._test_cases
39
+
40
+ @strategy.setter
41
+ def strategy(self, test_case_analyzer: TestCaseAnalyzerStrategy) -> None:
42
+ self._test_case_analyzer = test_case_analyzer
43
+ if self._analysis_context:
44
+ self._test_case_analyzer.analysis_context = self._analysis_context
45
+
46
+ @property
47
+ def analysis_context(self) -> AnalysisContext:
48
+ return self._analysis_context
49
+
50
+ @analysis_context.setter
51
+ def analysis_context(self, context: AnalysisContext) -> None:
52
+ self._analysis_context = context
53
+ if self._test_case_analyzer:
54
+ self._test_case_analyzer.analysis_context = context
55
+
56
+
57
+
58
+
File without changes
@@ -0,0 +1,194 @@
1
+ import argparse
2
+ import inspect
3
+ import os
4
+ import sys
5
+
6
+ import docker
7
+ from docker import DockerClient
8
+ from docker import errors
9
+
10
+ from testgen.controller.docker_controller import DockerController
11
+ from testgen.service.service import Service
12
+ from testgen.presentation.cli_view import CLIView
13
+ from testgen.sqlite.db_service import DBService
14
+
15
+ AST_STRAT = 1
16
+ FUZZ_STRAT = 2
17
+ RANDOM_STRAT = 3
18
+ REINFORCE_STRAT = 4
19
+
20
+ UNITTEST_FORMAT = 1
21
+ PYTEST_FORMAT = 2
22
+ DOCTEST_FORMAT = 3
23
+
24
+ class CLIController:
25
+ #TODO: Possibly create a view 'interface' and use dependency injection to extend other views
26
+ def __init__(self, service: Service, view: CLIView):
27
+ self.service = service
28
+ self.view = view
29
+
30
+ def run(self):
31
+ parser = argparse.ArgumentParser(description="A CLI tool for generating unit tests.")
32
+ parser.add_argument("file_path", type=str, help="Path to the Python file.")
33
+ parser.add_argument("--output", "-o", type=str, help="Path to output directory.")
34
+ parser.add_argument(
35
+ "--generate-only", "-g",
36
+ action="store_true",
37
+ help="Generate branched code but skip running unit tests and coverage."
38
+ )
39
+ parser.add_argument(
40
+ "--test-mode",
41
+ choices=["ast", "random", "fuzz", "reinforce"],
42
+ default="ast",
43
+ help="Set the test generation analysis technique"
44
+ )
45
+ parser.add_argument(
46
+ "--reinforce-mode",
47
+ choices=["train", "collect"],
48
+ default="train",
49
+ help="Set mode for reinforcement learning"
50
+ )
51
+ parser.add_argument(
52
+ "--test-format",
53
+ choices=["unittest", "pytest", "doctest"],
54
+ default="unittest",
55
+ help="Set the test generation format"
56
+ )
57
+ parser.add_argument(
58
+ "--safe",
59
+ action="store_true",
60
+ help="Run test generation from within a docker container."
61
+ )
62
+ parser.add_argument(
63
+ "--db",
64
+ type=str,
65
+ default="testgen.db",
66
+ help="Path to SQLite database file (default: testgen.db)"
67
+ )
68
+ parser.add_argument(
69
+ "--select-all",
70
+ action="store_true",
71
+ help="Select all from sqlite db"
72
+ )
73
+ parser.add_argument(
74
+ "--visualize",
75
+ action="store_true",
76
+ help = "Visualize the tests with graphviz"
77
+ )
78
+
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)
107
+ else:
108
+ self.view.display_message("Running in local mode...")
109
+ self.execute_generation(args)
110
+
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)
134
+ 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
180
+
181
+ def docker_available(self) -> DockerClient | None:
182
+ try:
183
+ client = docker.from_env()
184
+ client.ping()
185
+ self.view.display_message("Docker daemon is running and connected.")
186
+ return client
187
+ except docker.errors.DockerException as err:
188
+ print(f"Docker is not available: {err}")
189
+ print(f"Make sure the Docker daemon is running, and try again.")
190
+ choice = input("Continue without Docker (y/n)?")
191
+ if choice.lower() == 'y':
192
+ return None
193
+ else:
194
+ sys.exit(1)
@@ -0,0 +1,169 @@
1
+ import os
2
+ import sys
3
+ from argparse import Namespace
4
+ import docker
5
+ from docker import DockerClient, client
6
+ from docker import errors
7
+ from docker.models.containers import Container
8
+
9
+ from testgen.service.service import Service
10
+
11
+ AST_STRAT = 1
12
+ FUZZ_STRAT = 2
13
+ RANDOM_STRAT = 3
14
+
15
+ UNITTEST_FORMAT = 1
16
+ PYTEST_FORMAT = 2
17
+ DOCTEST_FORMAT = 3
18
+
19
+ class DockerController:
20
+ def __init__(self):
21
+ self.service = Service()
22
+ self.args = None
23
+
24
+ def run_in_docker(self, project_root: str, docker_client: DockerClient, args: Namespace) -> bool:
25
+ self.args = args
26
+ os.environ["RUNNING_IN_DOCKER"] = "1"
27
+
28
+ # Check if Docker image exists, build it if not
29
+ image_name = "testgen-runner"
30
+ # If args.safe is set to false it means the image was not found and the system will try to run_locally
31
+ self.get_image(docker_client, image_name, project_root)
32
+ if not self.args.safe:
33
+ print("Docker image not found. Running locally...")
34
+ return False
35
+
36
+ docker_args = [os.path.basename(args.file_path)] + [arg for arg in sys.argv[2:] if arg != "--safe"]
37
+
38
+ # Run the container with the same arguments
39
+ try:
40
+ container = self.run_container(docker_client, image_name, docker_args, project_root)
41
+
42
+ # Stream the logs to the console
43
+ logs_output = self.get_logs(container)
44
+ print(logs_output)
45
+
46
+ try:
47
+ # Create the target directory if it doesn't exist
48
+ if args.output is None:
49
+ target_path = os.path.join(os.getcwd(), 'tests')
50
+ else:
51
+ target_path = args.output
52
+ os.makedirs(target_path, exist_ok=True)
53
+
54
+ print(f"SERVICE target path after logs: {target_path}")
55
+
56
+ test_cases = self.service.parse_test_cases_from_logs(logs_output)
57
+
58
+ print(f"Extracted {len(test_cases)} test cases from container.")
59
+
60
+ file_path = os.path.abspath(args.file_path)
61
+ print(f"Filepath in CLI CONTROLLER: {file_path}")
62
+ self.service.set_file_path(file_path)
63
+
64
+ if args.test_format == "pytest":
65
+ self.service.set_test_generator_format(PYTEST_FORMAT)
66
+ elif args.test_format == "doctest":
67
+ self.service.set_test_generator_format(DOCTEST_FORMAT)
68
+ else:
69
+ self.service.set_test_generator_format(UNITTEST_FORMAT)
70
+
71
+ test_file = self.service.generate_test_file(test_cases, target_path)
72
+ print(f"Unit tests saved to: {test_file}")
73
+
74
+ if not args.generate_only:
75
+ print("Running coverage...")
76
+ import traceback
77
+ print(traceback.format_exc())
78
+ self.service.run_coverage(test_file)
79
+
80
+ except Exception as e:
81
+ print(f"Error running container: {e}")
82
+ sys.exit(1)
83
+
84
+ except Exception as e:
85
+ print(f"Error running container: {e}")
86
+ sys.exit(1)
87
+
88
+ def get_image(self, docker_client: DockerClient, image_name: str, project_root: str):
89
+ try:
90
+ docker_client.images.get(image_name)
91
+ print(f"Using existing {image_name} Docker image")
92
+ except docker.errors.ImageNotFound:
93
+ print(f"Building {image_name} Docker image...")
94
+
95
+ # Look for Dockerfile in the project root
96
+ docker_dir = os.path.join(project_root, "testgen", "docker")
97
+ dockerfile_path = os.path.join(docker_dir, "Dockerfile")
98
+ if not os.path.exists(dockerfile_path):
99
+ print(f"Dockerfile not found at {dockerfile_path}")
100
+ sys.exit(1)
101
+
102
+ print(f"Using Dockerfile at: {dockerfile_path}")
103
+
104
+ if not self.build_docker_image(docker_client, image_name, dockerfile_path, project_root):
105
+ print("Failed to build Docker image. Continuing without safe mode.")
106
+ self.args.safe = False
107
+
108
+ @staticmethod
109
+ def get_logs(container) -> str:
110
+ # Stream the logs to the console
111
+ print("Running in Docker container...")
112
+ logs = container.logs(stream=True)
113
+ logs_output = ""
114
+ for log in logs:
115
+ log_line = log.decode()
116
+ logs_output += log_line
117
+ print(log_line, end="")
118
+ return logs_output
119
+
120
+ @staticmethod
121
+ def run_container(docker_client: DockerClient, image_name: str, docker_args: list, project_root: str) -> Container:
122
+ return docker_client.containers.run(
123
+ image=image_name,
124
+ 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"},
127
+ detach=True,
128
+ remove=True,
129
+ stdout=True,
130
+ stderr=True
131
+ )
132
+
133
+ @staticmethod
134
+ def build_docker_image(docker_client, image_name, dockerfile_path, project_root):
135
+ try:
136
+ print(f"Starting Docker build for image: {image_name}")
137
+ 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}")
141
+ build_progress = docker_client.api.build(
142
+ path=os.path.join(project_root, "testgen", "docker"),
143
+ dockerfile=os.path.join(project_root, "testgen", "docker", "Dockerfile"),
144
+ tag=image_name,
145
+ rm=True,
146
+ decode=True
147
+ )
148
+
149
+ for chunk in build_progress:
150
+ print(f"CHUNK: {chunk}")
151
+ if 'stream' in chunk:
152
+ for line in chunk['stream'].splitlines():
153
+ if line.strip():
154
+ print(f"Docker: {line.strip()}")
155
+ elif 'error' in chunk:
156
+ print(f"Docker build error: {chunk['error']}")
157
+ return False
158
+ print(f"Docker image built successfully: {image_name}")
159
+ return True
160
+
161
+ except docker.errors.BuildError as e:
162
+ print(f"Docker build error: {e}")
163
+ except docker.errors.APIError as e:
164
+ print(f"Docker API error: {e}")
165
+ except Exception as e:
166
+ print(f"Unexpected error during Docker build: {str(e)}")
167
+ return False
168
+
169
+
@@ -0,0 +1,22 @@
1
+ FROM python:3.10
2
+
3
+ RUN apt-get update && apt-get install -y curl build-essential
4
+
5
+ RUN curl -sSL https://install.python-poetry.org | python3 - \
6
+ && ln -s /root/.local/bin/poetry /usr/local/bin/poetry
7
+
8
+ ENV POETRY_VIRTUALENVS_CREATE=false \
9
+ PYTHONUNBUFFERED=1 \
10
+ RUNNING_IN_DOCKER=true
11
+
12
+ WORKDIR /app
13
+
14
+ # Copy poetry files
15
+ COPY . .
16
+
17
+ ENV PYTHONPATH=/app:/app/testgen
18
+
19
+ RUN poetry install --no-root
20
+
21
+ # Entrypoint (Can be overridden by Docker SDK in Python)
22
+ CMD ["poetry", "run", "python", "-m", "testgen.main"]